Compare commits

..

14 Commits

Author SHA1 Message Date
Zayden-Jung 0b7b8802d1 accidently copied llm outputs 2026-05-16 04:02:45 +08:00
Zayden-Jung b4be492dec accidently copied llm outputs 2026-05-16 04:02:14 +08:00
Zayden-Jung d25540d727 draft README.md 2026-05-16 04:01:08 +08:00
Zayden-Jung 242e3fde38 clean dead codes 2026-05-16 03:57:48 +08:00
Zayden-Jung 5e5727f021 fix obscure bugs 2026-05-16 03:56:06 +08:00
Zayden-Jung 3012507fcc a version with known bug fixed but not sure any more obscure ones 2026-05-16 03:55:00 +08:00
Zayden-Jung 615c324d9b preset glob non-recursive issue; supporting more syntax 2026-05-16 03:41:32 +08:00
Zayden-Jung 3912eed832 script for installing the cli 2026-05-16 03:27:02 +08:00
Zayden-Jung e8ab979fd1 delete ===== PROJECT TREE ===== 2026-05-16 03:26:27 +08:00
Zayden-Jung 69afb0a53e first mvp 2026-05-16 01:39:45 +08:00
Zayden-Jung b3a2ae4681 .gitignore 2026-05-16 00:16:43 +08:00
Zayden-Jung 6a1b6554f4 Delete .DS_Store 2026-05-15 16:15:00 +00:00
Zayden-Jung 9587ac85b7 study & make comments 2026-05-15 23:54:10 +08:00
Zayden-Jung 9079152812 try testing 2026-05-15 23:53:59 +08:00
14 changed files with 494 additions and 146 deletions
+22
View File
@@ -0,0 +1,22 @@
config:
path_mode: relative
prompts:
preset: "下面的代码来自文件 {{path}}\n---\n{{content}}"
preset:
all:
- "*"
# - "cmd/*"
# - "internal/tree/*"
# - "internal/utils/*"
# - "internal/config/*"
# - "internal/resolver/*"
# - "main.go"
internal:
- "internal/tree/*"
- "internal/utils/glob.go"
cmd:
- "cmd/*"
+4
View File
@@ -0,0 +1,4 @@
.git
.DS_Store
**/.DS_Store
+93
View File
@@ -0,0 +1,93 @@
# cliprepo
`cliprepo` is a lightweight, efficient CLI tool written in Go that helps you package your repository's file tree and source code content into a formatted Markdown block. It is designed to quickly build comprehensive context for Large Language Models (LLMs) like GPT, Claude, or DeepSeek.
## Features
- **File Tree Generation**: Automatically generates a clean ASCII directory tree, honoring your `.gitignore` rules.
- **Flexible File Filtering**: Match files using flexible presets (`*`, `cmd/*`, or specific file paths).
- **Gitignore Smart Filtering**: Automatically skips files and directories specified in your `.gitignore` (e.g., `.git`, `node_modules`, `.DS_Store`).
- **Clipboard Integration**: Directly copies the generated prompt to your clipboard—no manual file saving required.
- **Customizable Templates**: Define how your code blocks are presented to the LLM via a YAML configuration file.
## Installation
Ensure you have [Go](https://go.dev/) installed (version 1.26 or higher), then clone the repository and run:
```bash
chmod +x install.sh
./install.sh
```
This will compile and install the `cliprepo` binary into your `$GOPATH/bin` directory.
## Configuration
Create a `.cliprepo.yaml` file in the root of your project to manage path modes, custom prompt templates, and file group presets:
```yaml
config:
path_mode: relative # Options: 'relative' or 'absolute'
prompts:
preset: "The following code is from file {{path}}:\n---\n{{content}}"
preset:
all:
- "*" # Scans and includes all tracked files in the repo
internal:
- "internal/tree/*"
- "internal/resolver/resolver.go"
cmd:
- "cmd/*"
```
## Usage
Run the tool using the `run` command by specifying a preset key defined in your `.cliprepo.yaml`.
### Print to Terminal
```bash
cliprepo run --delineate cmd
```
### Copy Directly to Clipboard
```bash
cliprepo run --delineate all --clipboard
```
### Flags
* `--delineate` (string): The preset key to use from `.cliprepo.yaml` (Required).
* `--clipboard` (bool): Copy the output directly to your system clipboard instead of printing it to stdout.
## Output Example
When running `cliprepo run --delineate cmd`, the output format will look like this:
```text
.
├── cmd
│ ├── root.go
│ └── run.go
└── main.go
The following code is from file cmd/root.go:
---
package cmd
...
The following code is from file cmd/run.go:
---
package cmd
...
```
## License
[MIT License](https://www.google.com/search?q=LICENSE)
+92
View File
@@ -0,0 +1,92 @@
# cliprepo
`cliprepo` 是一个用 Go 语言编写的轻量、高效的命令行工具(CLI)。它可以将你代码库的目录结构和指定的文件内容一键打包成格式化的 Markdown 文本,专门用于快速为大语言模型(如 GPT、Claude、DeepSeek)构建完整的上下文。
## 功能特性
- **目录树生成**:自动生成清晰的 ASCII 目录树,并且会自动遵循 `.gitignore` 的过滤规则。
- **灵活的文件过滤**:支持通过预设组合(Preset)匹配文件(如 `*``cmd/*` 或具体文件路径)。
- **智能 Gitignore 过滤**:自动跳过 `.gitignore` 中指定的文件和文件夹(例如 `.git``node_modules``.DS_Store`)。
- **剪贴板集成**:一键将生成的 Prompt 复制到系统剪贴板,省去手动复制或保存文件的烦恼。
- **自定义模板**:支持通过 YAML 配置文件自定义代码块向 LLM 展示的模板。
## 安装指南
请确保你已经安装了 [Go](https://go.dev/)(版本 1.26 或更高),然后克隆本仓库并执行:
```bash
chmod +x install.sh
./install.sh
```
这将会把编译好的 `cliprepo` 二进制文件安装到你的 `$GOPATH/bin` 目录下。
## 配置文件
在项目根目录下创建一个 `.cliprepo.yaml` 文件,用于管理路径模式、自定义提示词模板和文件组预设:
```yaml
config:
path_mode: relative # 可选值: 'relative' (相对路径) 或 'absolute' (绝对路径)
prompts:
preset: "下面的代码来自文件 {{path}}\n---\n{{content}}"
preset:
all:
- "*" # 扫描并包含库中所有未被 ignore 的文件
internal:
- "internal/tree/*"
- "internal/resolver/resolver.go"
cmd:
- "cmd/*"
```
## 使用方法
通过 `run` 命令并指定 `.cliprepo.yaml` 中配置的预设(preset)键名来运行该工具。
### 输出到终端屏幕
```bash
cliprepo run --delineate cmd
```
### 直接复制到剪贴板
```bash
cliprepo run --delineate all --clipboard
```
### 命令行参数(Flags
* `--delineate` (string): 指定要使用的 `.cliprepo.yaml` 中的预设键名(必填)。
* `--clipboard` (bool): 将生成的结果直接复制到系统剪贴板,不再在终端打印。
## 输出示例
当你运行 `cliprepo run --delineate cmd` 时,输出的内容格式如下:
```text
.
├── cmd
│ ├── root.go
│ └── run.go
└── main.go
下面的代码来自文件 cmd/root.go
---
package cmd
...
下面的代码来自文件 cmd/run.go
---
package cmd
...
```
## 开源协议
[MIT License](https://www.google.com/search?q=LICENSE)
-36
View File
@@ -1,36 +0,0 @@
package cmd
import (
"fmt"
"cliprepo/internal/bundle"
"github.com/spf13/cobra"
)
var bundleClipboard bool
var bundleCmd = &cobra.Command{
Use: "bundle [key]",
Short: "Bundle files by key from config",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
key := args[0]
result, err := bundle.Build(key)
if err != nil {
return err
}
if bundleClipboard {
return bundle.Copy(result)
}
fmt.Println(result)
return nil
},
}
func init() {
rootCmd.AddCommand(bundleCmd)
bundleCmd.Flags().BoolVar(&bundleClipboard, "clipboard", false, "Copy to clipboard")
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
var rootCmd = &cobra.Command{
Use: "cliprepo",
Short: "Extract repo structure and bundle code for LLMs",
Short: "LLM context builder from repo",
}
func Execute() {
+98
View File
@@ -0,0 +1,98 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"cliprepo/internal/config"
"cliprepo/internal/resolver"
"cliprepo/internal/tree"
"github.com/atotto/clipboard"
"github.com/spf13/cobra"
)
var delineate string
var useClipboard bool
var runCmd = &cobra.Command{
Use: "run",
Short: "Generate LLM context from repo",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return err
}
treeStr, err := tree.Generate(".")
if err != nil {
return err
}
var builder strings.Builder
builder.WriteString(treeStr)
builder.WriteString("\n\n")
r := resolver.New(".")
raw, ok := cfg.Preset[delineate]
if !ok {
return fmt.Errorf("preset not found: %s", delineate)
}
patterns := []string(raw)
var files []string
for _, pattern := range patterns {
matches, err := r.ResolvePattern(pattern)
if err != nil {
return err
}
files = append(files, matches...)
}
files = r.Unique(files)
sort.Strings(files)
for _, file := range files {
content, err := os.ReadFile(file)
if err != nil {
continue
}
filePath := file
if cfg.Config.PathMode == "absolute" {
abs, _ := filepath.Abs(file)
filePath = abs
}
block := strings.ReplaceAll(template, "{{path}}", filePath)
block = strings.ReplaceAll(block, "{{content}}", string(content))
builder.WriteString(block)
builder.WriteString("\n\n")
}
result := builder.String()
// 6. output
if useClipboard {
return clipboard.WriteAll(result)
}
fmt.Println(result)
return nil
},
}
func init() {
rootCmd.AddCommand(runCmd)
runCmd.Flags().StringVar(&delineate, "delineate", "", "preset key")
runCmd.Flags().BoolVar(&useClipboard, "clipboard", false, "copy output to clipboard")
}
-40
View File
@@ -1,40 +0,0 @@
package cmd
import (
"fmt"
"cliprepo/internal/tree"
"github.com/spf13/cobra"
)
var treeOutput string
var treeClipboard bool
var treeCmd = &cobra.Command{
Use: "tree",
Short: "Print repo tree respecting .gitignore",
RunE: func(cmd *cobra.Command, args []string) error {
result, err := tree.Generate(".")
if err != nil {
return err
}
if treeClipboard {
return tree.Copy(result)
}
if treeOutput != "" {
return tree.Write(treeOutput, result)
}
fmt.Println(result)
return nil
},
}
func init() {
rootCmd.AddCommand(treeCmd)
treeCmd.Flags().StringVarP(&treeOutput, "output", "o", "", "Output file")
treeCmd.Flags().BoolVar(&treeClipboard, "clipboard", false, "Copy to clipboard")
}
+1
View File
@@ -0,0 +1 @@
go install .
-52
View File
@@ -1,52 +0,0 @@
package bundle
import (
"fmt"
"os"
"strings"
"cliprepo/internal/config"
"github.com/atotto/clipboard"
)
const maxFileSize = 1 * 1024 * 1024 // 1MB
func Build(key string) (string, error) {
cfg, err := config.Load()
if err != nil {
return "", err
}
files, ok := cfg.Bundles[key]
if !ok {
return "", fmt.Errorf("bundle not found: %s", key)
}
var builder strings.Builder
for _, file := range files {
info, err := os.Stat(file)
if err != nil {
continue
}
if info.Size() > maxFileSize {
continue
}
data, err := os.ReadFile(file)
if err != nil {
continue
}
builder.WriteString(fmt.Sprintf("===== %s =====\n", file))
builder.Write(data)
builder.WriteString("\n\n")
}
return builder.String(), nil
}
func Copy(content string) error {
return clipboard.WriteAll(content)
}
+45 -3
View File
@@ -1,22 +1,64 @@
package config
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
type StringOrSlice []string
func (s *StringOrSlice) UnmarshalYAML(value *yaml.Node) error {
switch value.Kind {
// "preset: *"
case yaml.ScalarNode:
*s = []string{value.Value}
return nil
// "preset: [a, b]"
case yaml.SequenceNode:
var out []string
for _, v := range value.Content {
out = append(out, v.Value)
}
*s = out
return nil
default:
return fmt.Errorf("invalid preset format")
}
}
type Config struct {
Bundles map[string][]string `yaml:"bundles"`
Config struct {
PathMode string `yaml:"path_mode"`
} `yaml:"config"`
Prompts struct {
Preset string `yaml:"preset"`
} `yaml:"prompts"`
Preset map[string]StringOrSlice `yaml:"preset"`
}
func Load() (*Config, error) {
data, err := os.ReadFile("cliprepo.yaml")
data, err := os.ReadFile(".cliprepo.yaml")
if err != nil {
return nil, err
}
var cfg Config
err = yaml.Unmarshal(data, &cfg)
return &cfg, err
if err != nil {
return nil, err
}
// DEFAULT PROMPT TEMPLATE
if cfg.Prompts.Preset == "" {
cfg.Prompts.Preset = "The following code is from file {{path}}:\n---\n{{content}}"
}
return &cfg, nil
}
+132
View File
@@ -0,0 +1,132 @@
package resolver
import (
"cliprepo/internal/tree"
"os"
"path/filepath"
)
type Resolver struct {
root string
seen map[string]struct{}
}
func New(root string) *Resolver {
return &Resolver{
root: root,
seen: make(map[string]struct{}),
}
}
// ResolvePattern 智能解析各种 pattern 并过滤 ignore 文件
func (r *Resolver) ResolvePattern(pattern string) ([]string, error) {
if pattern == "*" {
return r.WalkAll()
}
var matches []string
// 处理类似 cmd/* 或 internal/tree/* 的情况
// 如果包含 * 且不是以 ** 开头,转换为通过 filepath.Walk 匹配或直接 Glob 过滤
rawMatches, err := filepath.Glob(pattern)
if err != nil {
return nil, err
}
ig, _ := tree.Load(r.root)
for _, m := range rawMatches {
info, err := os.Stat(m)
if err != nil {
continue
}
rel, _ := filepath.Rel(r.root, m)
// 统一检查 .gitignore
if ig != nil && ig.MatchesPath(rel) {
continue
}
if info.IsDir() {
// 如果匹配到的是目录(比如 internal/* 匹配到了 internal/config),则深层遍历该目录
err := filepath.Walk(m, func(path string, fInfo os.FileInfo, walkErr error) error {
if walkErr != nil || fInfo.IsDir() {
return nil
}
fRel, _ := filepath.Rel(r.root, path)
if ig != nil && ig.MatchesPath(fRel) {
return nil
}
r.add(fRel, &matches)
return nil
})
if err != nil {
return nil, err
}
} else {
r.add(rel, &matches)
}
}
return matches, nil
}
// WalkAll 遍历全库并过滤
func (r *Resolver) WalkAll() ([]string, error) {
var result []string
ig, _ := tree.Load(r.root)
err := filepath.Walk(r.root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if info.IsDir() && info.Name() == ".git" {
return filepath.SkipDir
}
rel, _ := filepath.Rel(r.root, path)
if ig != nil && ig.MatchesPath(rel) {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
if info.IsDir() {
return nil
}
r.add(rel, &result)
return nil
})
return result, err
}
func (r *Resolver) AddFile(path string, result *[]string) {
r.add(path, result)
}
func (r *Resolver) add(path string, result *[]string) {
if _, ok := r.seen[path]; ok {
return
}
r.seen[path] = struct{}{}
*result = append(*result, path)
}
func (r *Resolver) Unique(in []string) []string {
seen := make(map[string]struct{})
out := make([]string, 0, len(in))
for _, f := range in {
if _, ok := seen[f]; ok {
continue
}
seen[f] = struct{}{}
out = append(out, f)
}
return out
}
@@ -1,4 +1,4 @@
package gitignore
package tree
import (
"os"
@@ -7,6 +7,7 @@ import (
ignore "github.com/sabhiram/go-gitignore"
)
// Load 加载 .gitignore
func Load(root string) (*ignore.GitIgnore, error) {
path := filepath.Join(root, ".gitignore")
+4 -13
View File
@@ -5,13 +5,11 @@ import (
"os"
"path/filepath"
"strings"
"cliprepo/internal/gitignore"
"github.com/atotto/clipboard"
)
func Generate(root string) (string, error) {
ig, _ := gitignore.Load(root)
ig, _ := Load(root)
var builder strings.Builder
@@ -27,7 +25,8 @@ func Generate(root string) (string, error) {
rel, _ := filepath.Rel(root, path)
if ig.MatchesPath(rel) {
// gitignore filter
if ig != nil && ig.MatchesPath(rel) {
if info.IsDir() {
return filepath.SkipDir
}
@@ -42,12 +41,4 @@ func Generate(root string) (string, error) {
})
return builder.String(), err
}
func Copy(content string) error {
return clipboard.WriteAll(content)
}
func Write(path, content string) error {
return os.WriteFile(path, []byte(content), 0644)
}