@kevinxyz/code-snapshot 1.0.0 ā 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -27
- package/docs/index.html +214 -0
- package/index.js +385 -137
- package/package.json +22 -6
package/README.md
CHANGED
|
@@ -1,55 +1,149 @@
|
|
|
1
1
|
# šø code-snapshot
|
|
2
2
|
|
|
3
|
-
**Merge your codebase into one
|
|
3
|
+
**Merge your codebase into one text block for AI agents.**
|
|
4
4
|
|
|
5
|
-
Stop manually
|
|
5
|
+
Stop manually copy-pasting files into ChatGPT, Claude Code, Codex, or Cursor. One command, done.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install -g code-snapshot
|
|
9
|
-
snap ./src # Snapshot a directory
|
|
10
|
-
snap ./src --git-only # Only changed files
|
|
11
|
-
snap ./src -o snapshot.txt # Write to file
|
|
12
|
-
cat snapshot.txt | pbcopy # Copy to clipboard
|
|
8
|
+
npm install -g @kevinxyz/code-snapshot
|
|
13
9
|
```
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
**Just run it in your project root (no arguments = current dir):**
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
snap
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then pipe directly to any AI (replace `.` with your folder):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
snap . | claude # Pipe to Claude Code
|
|
21
|
+
snap . -i --tokens # Interactive + token estimate
|
|
22
|
+
snap . --git-only -o ctx.txt # Just changed files
|
|
23
|
+
snap . --copy # Copy to clipboard
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> š” **Tip:** `snap` with no args uses the current directory. If you get `ENOENT`, it means the folder doesn't exist ā just run `snap .` in your project root.
|
|
27
|
+
|
|
28
|
+
## Why code-snapshot?
|
|
16
29
|
|
|
17
30
|
Every developer using AI coding tools has the same ritual: open 10 files, copy-paste them one by one into the chat, describe the problem, hope the AI understands the full context.
|
|
18
31
|
|
|
19
|
-
**code-snapshot
|
|
32
|
+
**code-snapshot automates this.** One command gives you a clean, structured text block your AI agent can immediately understand.
|
|
33
|
+
|
|
34
|
+
## Real Scenario
|
|
35
|
+
|
|
36
|
+
You have a project and want AI to help add a new feature. Without code-snapshot:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Manual: open each file, copy-paste one by one to AI
|
|
40
|
+
src/index.js ā copy ā paste to AI
|
|
41
|
+
src/utils/parser.js ā copy ā paste to AI
|
|
42
|
+
src/config.js ā copy ā paste to AI
|
|
43
|
+
package.json ā copy ā paste to AI
|
|
44
|
+
...
|
|
45
|
+
# 10 files = 10 copy-pastes, easy to miss something
|
|
46
|
+
# AI says "I can't find the definition" ā go back, find the file, copy again
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
With code-snapshot:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# One command, everything in one place
|
|
53
|
+
snap . -o context.txt
|
|
54
|
+
# Then drag context.txt into your AI chat
|
|
55
|
+
# AI gets full context in one shot, no back-and-forth
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Real output example
|
|
20
59
|
|
|
21
|
-
|
|
60
|
+
Run `snap` in your project, and you get:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
============================================================
|
|
64
|
+
šø CODE SNAPSHOT
|
|
65
|
+
============================================================
|
|
66
|
+
Generated: 2026-06-07T12:33:00.000Z
|
|
67
|
+
Source: /my-project
|
|
68
|
+
Files scanned: 12
|
|
69
|
+
============================================================
|
|
70
|
+
|
|
71
|
+
// [FILE] package.json
|
|
72
|
+
// [LANG] JSON
|
|
73
|
+
// [SIZE] 800 chars | ~200 tokens
|
|
74
|
+
{ "name": "my-project", ... }
|
|
75
|
+
|
|
76
|
+
// [FILE] src/index.js
|
|
77
|
+
// [LANG] JavaScript
|
|
78
|
+
// [SIZE] 3400 chars | ~850 tokens
|
|
79
|
+
import { parseFile } from './utils/parser';
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
// [FILE] src/utils/parser.js
|
|
83
|
+
// [LANG] JavaScript
|
|
84
|
+
// [SIZE] 5200 chars | ~1300 tokens
|
|
85
|
+
export function parseFile(path) { ... }
|
|
86
|
+
...
|
|
87
|
+
|
|
88
|
+
============================================================
|
|
89
|
+
END OF SNAPSHOT
|
|
90
|
+
============================================================
|
|
91
|
+
12 files | ~15000 chars | ~3750 tokens
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Then you tell AI: *"Above is my full codebase. Please add a feature to export each page as an image."*
|
|
95
|
+
|
|
96
|
+
**AI understands instantly** ā no need to paste files one by one, no missing context.
|
|
97
|
+
|
|
98
|
+
## Features (v2)
|
|
22
99
|
|
|
23
100
|
| Feature | CLI Flag | Description |
|
|
24
101
|
|---------|----------|-------------|
|
|
25
|
-
| Directory walk | `snap <dir>` | Recursive with `.gitignore` awareness |
|
|
102
|
+
| Directory walk | `snap <dir>` | Recursive with `.gitignore` + `.snapignore` awareness |
|
|
26
103
|
| Git-only | `--git-only` | Only modified/new files since last commit |
|
|
27
|
-
| Minify | `--minify` | Strip blank lines
|
|
104
|
+
| Minify | `--minify` | Strip blank lines + single-line comments to save tokens |
|
|
105
|
+
| Token estimate | `--tokens` | See estimated token count in output |
|
|
106
|
+
| Interactive picker | `-i` or `--interactive` | Pick files and options interactively |
|
|
107
|
+
| Top files report | `--top-files` | Show largest files before generating |
|
|
28
108
|
| Output file | `-o <file>` | Write to file instead of stdout |
|
|
29
109
|
| Clipboard | `--copy` | Copy to clipboard (macOS/Linux) |
|
|
30
110
|
| Include filter | `--include "*.js,*.ts"` | Only matching file patterns |
|
|
31
111
|
| Exclude filter | `--exclude "*.test.*"` | Skip matching patterns |
|
|
32
112
|
| Max file size | `--max-size <bytes>` | Skip large files (default: 512KB) |
|
|
33
113
|
| Skip binary | `--no-binary` | Auto-detect and skip binary files (default: on) |
|
|
34
|
-
|
|
|
114
|
+
| JSON output | `--json` | Machine-readable JSON format |
|
|
115
|
+
| Sort options | `--sort alpha\|size\|type` | Control output file order |
|
|
116
|
+
| Output formats | `--format marker\|xml\|plain` | Choose your output format |
|
|
35
117
|
|
|
36
118
|
## Quick Examples
|
|
37
119
|
|
|
38
120
|
```bash
|
|
39
121
|
# Everything you need for your AI prompt
|
|
40
|
-
snap
|
|
122
|
+
snap . -o context.txt
|
|
41
123
|
|
|
42
|
-
# Just what changed today
|
|
124
|
+
# Just what changed today (git diff)
|
|
43
125
|
snap . --git-only
|
|
44
126
|
|
|
45
|
-
#
|
|
46
|
-
snap
|
|
127
|
+
# Interactive mode ā pick files, set options
|
|
128
|
+
snap . -i
|
|
129
|
+
|
|
130
|
+
# Clean it up to save tokens
|
|
131
|
+
snap . --minify --exclude "*.test.js,*.spec.js"
|
|
132
|
+
|
|
133
|
+
# See token count before you use it
|
|
134
|
+
snap . --tokens
|
|
135
|
+
|
|
136
|
+
# Find the big files
|
|
137
|
+
snap . --top-files
|
|
47
138
|
|
|
48
|
-
#
|
|
49
|
-
snap .
|
|
139
|
+
# JSON for programmatic use
|
|
140
|
+
snap . --json -o codebase.json
|
|
50
141
|
|
|
51
142
|
# Quick clipboard
|
|
52
|
-
snap
|
|
143
|
+
snap . --copy
|
|
144
|
+
|
|
145
|
+
# Different output format
|
|
146
|
+
snap . --format xml
|
|
53
147
|
```
|
|
54
148
|
|
|
55
149
|
## Output Format
|
|
@@ -59,24 +153,46 @@ Every file is wrapped with clear markers:
|
|
|
59
153
|
```
|
|
60
154
|
// [FILE] src/utils.ts
|
|
61
155
|
// [LANG] TypeScript
|
|
62
|
-
// [SIZE] 1234 chars
|
|
156
|
+
// [SIZE] 1234 chars | ~308 tokens
|
|
63
157
|
export function hello() { ... }
|
|
64
158
|
// [/FILE] src/utils.ts
|
|
65
159
|
```
|
|
66
160
|
|
|
67
|
-
The header includes
|
|
161
|
+
The header includes generation info and token estimation. Footer gives total statistics.
|
|
68
162
|
|
|
69
163
|
## Ignored by Default
|
|
70
164
|
|
|
71
|
-
`node_modules`, `.git`, `.DS_Store`, `dist`, `build`, `.next`, `.cache`, `__pycache__`, `.venv`, `venv`, `env`, `coverage`, plus everything in your `.gitignore`.
|
|
165
|
+
`node_modules`, `.git`, `.DS_Store`, `dist`, `build`, `.next`, `.cache`, `__pycache__`, `.venv`, `venv`, `env`, `.env`, `coverage`, `*.log`, plus everything in your `.gitignore` **and** `.snapignore`.
|
|
166
|
+
|
|
167
|
+
### .snapignore
|
|
168
|
+
|
|
169
|
+
Create a `.snapignore` file in your project root for custom exclusions:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
# .snapignore
|
|
173
|
+
generated/
|
|
174
|
+
*.graphql
|
|
175
|
+
**/*.stories.*
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## How it works
|
|
179
|
+
|
|
180
|
+
1. **Scans** your directory, respecting `.gitignore` and `.snapignore`
|
|
181
|
+
2. **Wraps** every file with FILE/LANG/SIZE markers
|
|
182
|
+
3. **Outputs** one clean text block ā pipe it, save it, copy it
|
|
183
|
+
4. **Your AI agent** gets full context in one shot
|
|
72
184
|
|
|
73
185
|
## Roadmap
|
|
74
186
|
|
|
75
|
-
- [
|
|
76
|
-
- [
|
|
187
|
+
- [x] Interactive file picker
|
|
188
|
+
- [x] Token-aware output
|
|
189
|
+
- [x] .snapignore file support
|
|
77
190
|
- [ ] VS Code extension
|
|
78
|
-
- [ ]
|
|
191
|
+
- [ ] Token-aware chunking (auto-split for context limits)
|
|
192
|
+
- [ ] GUI landing page
|
|
79
193
|
|
|
80
194
|
## License
|
|
81
195
|
|
|
82
|
-
MIT ā free for everyone.
|
|
196
|
+
MIT ā free for everyone.
|
|
197
|
+
|
|
198
|
+
If you find this useful, a ā on GitHub goes a long way.
|
package/docs/index.html
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>code-snapshot ā Merge your codebase for AI agents</title>
|
|
7
|
+
<meta name="description" content="Merge your codebase into one text block for AI agents. One command, done." />
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #0d1117;
|
|
11
|
+
--fg: #c9d1d9;
|
|
12
|
+
--accent: #58a6ff;
|
|
13
|
+
--green: #3fb950;
|
|
14
|
+
--orange: #d29922;
|
|
15
|
+
--border: #30363d;
|
|
16
|
+
--card: #161b22;
|
|
17
|
+
--font: 'SF Mono','Cascadia Code','JetBrains Mono',monospace;
|
|
18
|
+
}
|
|
19
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
20
|
+
body {
|
|
21
|
+
font-family: var(--font);
|
|
22
|
+
background: var(--bg);
|
|
23
|
+
color: var(--fg);
|
|
24
|
+
line-height: 1.6;
|
|
25
|
+
min-height: 100vh;
|
|
26
|
+
}
|
|
27
|
+
.container { max-width: 960px; margin: 0 auto; padding: 2rem 1.5rem; }
|
|
28
|
+
|
|
29
|
+
header { text-align: center; padding: 3rem 0 2rem; }
|
|
30
|
+
.logo { font-size: 3rem; }
|
|
31
|
+
h1 { font-size: 2.2rem; font-weight: 700; margin: 0.5rem 0; color: #fff; }
|
|
32
|
+
.tagline { font-size: 1.1rem; color: var(--fg); opacity: 0.8; max-width: 600px; margin: 0.5rem auto; }
|
|
33
|
+
|
|
34
|
+
.install {
|
|
35
|
+
background: var(--card);
|
|
36
|
+
border: 1px solid var(--border);
|
|
37
|
+
border-radius: 12px;
|
|
38
|
+
padding: 2rem;
|
|
39
|
+
margin: 2rem 0;
|
|
40
|
+
text-align: center;
|
|
41
|
+
}
|
|
42
|
+
.install code {
|
|
43
|
+
display: block;
|
|
44
|
+
background: #1a1f2b;
|
|
45
|
+
padding: 1rem 1.5rem;
|
|
46
|
+
border-radius: 8px;
|
|
47
|
+
font-size: 1.1rem;
|
|
48
|
+
color: var(--green);
|
|
49
|
+
margin: 1rem 0;
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
border: 1px solid var(--border);
|
|
52
|
+
transition: border-color 0.2s;
|
|
53
|
+
word-break: break-all;
|
|
54
|
+
}
|
|
55
|
+
.install code:hover { border-color: var(--accent); }
|
|
56
|
+
.install small { opacity: 0.6; font-size: 0.85rem; }
|
|
57
|
+
|
|
58
|
+
.features { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; margin: 2rem 0; }
|
|
59
|
+
.feature {
|
|
60
|
+
background: var(--card);
|
|
61
|
+
border: 1px solid var(--border);
|
|
62
|
+
border-radius: 10px;
|
|
63
|
+
padding: 1.5rem;
|
|
64
|
+
}
|
|
65
|
+
.feature .icon { font-size: 1.8rem; margin-bottom: 0.5rem; }
|
|
66
|
+
.feature h3 { color: #fff; margin-bottom: 0.5rem; }
|
|
67
|
+
.feature p { font-size: 0.9rem; opacity: 0.75; }
|
|
68
|
+
.feature code { background: #1a1f2b; padding: 0.1rem 0.4rem; border-radius: 4px; font-size: 0.85rem; }
|
|
69
|
+
|
|
70
|
+
.examples { margin: 2.5rem 0; }
|
|
71
|
+
.examples h2 { color: #fff; margin-bottom: 1rem; font-size: 1.4rem; }
|
|
72
|
+
.example {
|
|
73
|
+
background: var(--card);
|
|
74
|
+
border: 1px solid var(--border);
|
|
75
|
+
border-radius: 10px;
|
|
76
|
+
padding: 1rem 1.5rem;
|
|
77
|
+
margin-bottom: 1rem;
|
|
78
|
+
}
|
|
79
|
+
.example .cmd { color: var(--green); font-weight: 600; }
|
|
80
|
+
.example .desc { opacity: 0.7; font-size: 0.85rem; margin-top: 0.3rem; }
|
|
81
|
+
|
|
82
|
+
.cta { text-align: center; margin: 3rem 0; }
|
|
83
|
+
.btn {
|
|
84
|
+
display: inline-block;
|
|
85
|
+
padding: 0.9rem 2rem;
|
|
86
|
+
border-radius: 8px;
|
|
87
|
+
text-decoration: none;
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
font-family: var(--font);
|
|
90
|
+
transition: all 0.2s;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
}
|
|
93
|
+
.btn-primary {
|
|
94
|
+
background: var(--accent);
|
|
95
|
+
color: #0d1117;
|
|
96
|
+
border: none;
|
|
97
|
+
}
|
|
98
|
+
.btn-primary:hover { background: #79b8ff; transform: translateY(-1px); }
|
|
99
|
+
.btn-secondary {
|
|
100
|
+
background: transparent;
|
|
101
|
+
color: var(--fg);
|
|
102
|
+
border: 1px solid var(--border);
|
|
103
|
+
margin-left: 1rem;
|
|
104
|
+
}
|
|
105
|
+
.btn-secondary:hover { border-color: var(--accent); }
|
|
106
|
+
|
|
107
|
+
.footer { text-align: center; padding: 2rem 0; border-top: 1px solid var(--border); margin-top: 3rem; font-size: 0.85rem; opacity: 0.6; }
|
|
108
|
+
|
|
109
|
+
@media (max-width: 600px) {
|
|
110
|
+
.container { padding: 1rem; }
|
|
111
|
+
h1 { font-size: 1.6rem; }
|
|
112
|
+
.btn-secondary { margin-left: 0; margin-top: 0.5rem; }
|
|
113
|
+
}
|
|
114
|
+
</style>
|
|
115
|
+
</head>
|
|
116
|
+
<body>
|
|
117
|
+
<div class="container">
|
|
118
|
+
<header>
|
|
119
|
+
<div class="logo">šø</div>
|
|
120
|
+
<h1>code-snapshot</h1>
|
|
121
|
+
<p class="tagline">
|
|
122
|
+
Merge your entire codebase into one text block for AI agents.
|
|
123
|
+
Stop copy-pasting files manually.
|
|
124
|
+
</p>
|
|
125
|
+
</header>
|
|
126
|
+
|
|
127
|
+
<div class="install">
|
|
128
|
+
<p>Install globally:</p>
|
|
129
|
+
<code id="install-cmd">npm install -g @kevinxyz/code-snapshot</code>
|
|
130
|
+
<p>Then run anywhere:</p>
|
|
131
|
+
<code id="use-cmd">snap ./src | claude</code>
|
|
132
|
+
<small>ā” Works with Claude Code, ChatGPT, Codex, Cursor, Copilot ā any AI agent.</small>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div class="features">
|
|
136
|
+
<div class="feature">
|
|
137
|
+
<div class="icon">ā”</div>
|
|
138
|
+
<h3>One Command</h3>
|
|
139
|
+
<p><code>snap ./src</code> ā walks your directory, respects <code>.gitignore</code>, wraps files with clear markers.</p>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="feature">
|
|
142
|
+
<div class="icon">šÆ</div>
|
|
143
|
+
<h3>Git-Aware</h3>
|
|
144
|
+
<p><code>--git-only</code> includes only changed files. <code>--minify</code> strips comments to save tokens.</p>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="feature">
|
|
147
|
+
<div class="icon">š§©</div>
|
|
148
|
+
<h3>Flexible Output</h3>
|
|
149
|
+
<p>Pipe to AI, write to file, copy to clipboard. <code>--format xml</code>, <code>--json</code>, or default markers.</p>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="feature">
|
|
152
|
+
<div class="icon">š</div>
|
|
153
|
+
<h3>Token Aware</h3>
|
|
154
|
+
<p>Every output shows estimated token count. <code>--tokens</code> flag, <code>--top-files</code> to spot bloat.</p>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="feature">
|
|
157
|
+
<div class="icon">š¬</div>
|
|
158
|
+
<h3>Interactive Mode</h3>
|
|
159
|
+
<p><code>snap . -i</code> ā pick files, choose format, decide output. Perfect when you need control.</p>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="feature">
|
|
162
|
+
<div class="icon">š</div>
|
|
163
|
+
<h3>Universal</h3>
|
|
164
|
+
<p>Works with any AI agent. <code>snap . | claude</code>, pipe to ChatGPT, or paste into Cursor.</p>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div class="examples">
|
|
169
|
+
<h2>Quick Examples</h2>
|
|
170
|
+
<div class="example">
|
|
171
|
+
<div class="cmd">snap . | claude</div>
|
|
172
|
+
<div class="desc">Snapshot your whole project and pipe into Claude Code</div>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="example">
|
|
175
|
+
<div class="cmd">snap . --git-only -o diff.txt</div>
|
|
176
|
+
<div class="desc">Only files changed since last commit</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="example">
|
|
179
|
+
<div class="cmd">snap . -i --tokens</div>
|
|
180
|
+
<div class="desc">Interactive file picker with token estimation</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="example">
|
|
183
|
+
<div class="cmd">snap ./src --minify --exclude "*.test.js"</div>
|
|
184
|
+
<div class="desc">Minify code, skip test files</div>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="example">
|
|
187
|
+
<div class="cmd">snap . --json -o codebase.json</div>
|
|
188
|
+
<div class="desc">Machine-readable JSON output</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div class="cta">
|
|
193
|
+
<a href="https://github.com/Kevin-X00/code-snapshot" class="btn btn-primary" target="_blank">ā GitHub</a>
|
|
194
|
+
<a href="https://www.npmjs.com/package/@kevinxyz/code-snapshot" class="btn btn-secondary" target="_blank">š¦ npm</a>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div class="footer">
|
|
198
|
+
<p>MIT ā free for everyone. A ā on GitHub helps others find it.</p>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<script>
|
|
203
|
+
// Click-to-copy on install commands
|
|
204
|
+
document.querySelectorAll('code').forEach(el => {
|
|
205
|
+
el.addEventListener('click', () => {
|
|
206
|
+
navigator.clipboard.writeText(el.textContent.trim()).catch(() => {});
|
|
207
|
+
const orig = el.textContent;
|
|
208
|
+
el.textContent = 'ā Copied!';
|
|
209
|
+
setTimeout(() => { el.textContent = orig; }, 1200);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
</script>
|
|
213
|
+
</body>
|
|
214
|
+
</html>
|
package/index.js
CHANGED
|
@@ -3,66 +3,53 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* code-snapshot ā Merge your codebase into one text block for AI agents.
|
|
5
5
|
*
|
|
6
|
-
* Stop manually copying files into ChatGPT/Claude Code/Codex.
|
|
7
6
|
* Just run: snap <dir>
|
|
7
|
+
* npm install -g @kevinxyz/code-snapshot
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* - Walks directory tree, respects .gitignore
|
|
11
|
-
* - Labels each file with path and language
|
|
12
|
-
* --git-only Only include files changed since last commit
|
|
13
|
-
* --minify Strip blank lines and comments (reduces token count)
|
|
14
|
-
* --out Write to file instead of stdout
|
|
15
|
-
* --copy Copy to clipboard (macOS/Linux)
|
|
9
|
+
* v2.0.0 ā Enhanced Edition
|
|
16
10
|
*/
|
|
17
11
|
|
|
18
12
|
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
19
14
|
import { execSync } from 'node:child_process';
|
|
15
|
+
import { createInterface } from 'node:readline';
|
|
16
|
+
|
|
17
|
+
// āāā Language map āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
20
18
|
|
|
21
19
|
const LANGUAGE_MAP = {
|
|
22
20
|
'.js':'JavaScript','.ts':'TypeScript','.jsx':'React JSX','.tsx':'React TSX',
|
|
21
|
+
'.mjs':'JavaScript','.cjs':'JavaScript','.mts':'TypeScript','.cts':'TypeScript',
|
|
23
22
|
'.py':'Python','.go':'Go','.rs':'Rust','.rb':'Ruby','.java':'Java',
|
|
24
|
-
'.cs':'C#','.c':'C','.cpp':'C++','.
|
|
23
|
+
'.cs':'C#','.c':'C','.cpp':'C++','.h':'C/C++ Header','.hpp':'C++ Header',
|
|
24
|
+
'.php':'PHP','.swift':'Swift','.kt':'Kotlin','.kts':'Kotlin Script',
|
|
25
25
|
'.sh':'Bash','.bash':'Bash','.zsh':'Zsh','.fish':'Fish',
|
|
26
|
-
'.yaml':'YAML','.yml':'YAML','.json':'JSON','.
|
|
26
|
+
'.yaml':'YAML','.yml':'YAML','.json':'JSON','.jsonc':'JSONC','.json5':'JSON5',
|
|
27
|
+
'.xml':'XML','.toml':'TOML','.plist':'Plist',
|
|
27
28
|
'.md':'Markdown','.mdx':'MDX','.sql':'SQL','.graphql':'GraphQL','.gql':'GraphQL',
|
|
28
29
|
'.html':'HTML','.css':'CSS','.scss':'SCSS','.less':'Less','.sass':'Sass',
|
|
29
30
|
'.vue':'Vue','.svelte':'Svelte','.astro':'Astro','.svg':'SVG',
|
|
30
|
-
'.dockerfile':'Dockerfile','.tf':'Terraform','.pkl':'Pkl',
|
|
31
|
-
'.gradle':'Gradle','.properties':'Properties',
|
|
32
|
-
'.env':'ENV','.ini':'INI','.cfg':'Config','.conf':'Config',
|
|
33
|
-
'.lock':'Lockfile','.txt':'Text','.csv':'CSV',
|
|
31
|
+
'.dockerfile':'Dockerfile','.tf':'Terraform','.tfvars':'Terraform Variables','.pkl':'Pkl',
|
|
32
|
+
'.gradle':'Gradle','.gradle.kts':'Gradle Kotlin','.properties':'Properties',
|
|
33
|
+
'.env':'ENV','.env.example':'ENV Example','.ini':'INI','.cfg':'Config','.conf':'Config',
|
|
34
|
+
'.lock':'Lockfile','.txt':'Text','.csv':'CSV','.tsv':'TSV',
|
|
35
|
+
'.prisma':'Prisma','.proto':'Protobuf','.sol':'Solidity',
|
|
36
|
+
'.zig':'Zig','.ex':'Elixir','.exs':'Elixir Script','.erl':'Erlang',
|
|
37
|
+
'.dart':'Dart','.r':'R','.jl':'Julia','.nim':'Nim',
|
|
34
38
|
};
|
|
35
39
|
|
|
36
|
-
const DEFAULTS = { maxSizeBytes: 512 * 1024, maxDepth:
|
|
40
|
+
const DEFAULTS = { maxSizeBytes: 512 * 1024, maxDepth: 20 };
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
console.log(`
|
|
40
|
-
code-snapshot ā merge codebase into one text block for AI agents
|
|
41
|
-
|
|
42
|
-
Usage:
|
|
43
|
-
snap <path> Snapshot a file or directory
|
|
44
|
-
snap <path> -o output.txt Write to file
|
|
45
|
-
snap <path> --git-only Only changed files (git diff)
|
|
46
|
-
snap <path> --minify Strip blank lines and comments
|
|
47
|
-
snap <path> --copy Copy to clipboard (macOS: pbcopy)
|
|
48
|
-
snap <path> --no-binary Skip binary files (default)
|
|
49
|
-
snap --help Show this help
|
|
50
|
-
|
|
51
|
-
Options:
|
|
52
|
-
-o, --out <file> Output file path
|
|
53
|
-
--git-only Only git-modified files
|
|
54
|
-
--minify Remove blank lines + single-line comments
|
|
55
|
-
--copy Copy to clipboard (requires pbcopy/xclip)
|
|
56
|
-
--no-binary Skip binary files (default: true)
|
|
57
|
-
--max-size <bytes> Max file size to include (default: 512KB)
|
|
58
|
-
--include <glob> Only include matching patterns (comma-sep)
|
|
59
|
-
--exclude <glob> Exclude patterns (comma-sep, overrides include)
|
|
60
|
-
--no-gitignore Don't read .gitignore
|
|
61
|
-
`);
|
|
62
|
-
}
|
|
42
|
+
// āāā CLI args āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
63
43
|
|
|
64
44
|
function parseArgs(argv) {
|
|
65
|
-
const a = {
|
|
45
|
+
const a = {
|
|
46
|
+
files:[], out:null, gitOnly:false, minify:false, copy:false,
|
|
47
|
+
noBinary:true, maxSize:DEFAULTS.maxSizeBytes,
|
|
48
|
+
include:null, exclude:null, gitignore:true,
|
|
49
|
+
interactive:false, topFiles:false, sortBy:'alpha',
|
|
50
|
+
format:'marker', respectSnapignore:true,
|
|
51
|
+
tokenCount:false, json:false,
|
|
52
|
+
};
|
|
66
53
|
for (let i = 2; i < argv.length; i++) {
|
|
67
54
|
const arg = argv[i];
|
|
68
55
|
if (arg === '--help' || arg === '-h') { a.help = true; }
|
|
@@ -70,65 +57,135 @@ function parseArgs(argv) {
|
|
|
70
57
|
else if (arg === '--git-only') { a.gitOnly = true; }
|
|
71
58
|
else if (arg === '--minify') { a.minify = true; }
|
|
72
59
|
else if (arg === '--copy') { a.copy = true; }
|
|
73
|
-
else if (arg === '--no-binary') { a.noBinary =
|
|
60
|
+
else if (arg === '--no-binary') { a.noBinary = false; }
|
|
74
61
|
else if (arg === '--no-gitignore') { a.gitignore = false; }
|
|
62
|
+
else if (arg === '--interactive' || arg === '-i') { a.interactive = true; }
|
|
63
|
+
else if (arg === '--top-files') { a.topFiles = true; }
|
|
75
64
|
else if ((arg === '--max-size') && argv[i+1]) { a.maxSize = parseInt(argv[++i]) || DEFAULTS.maxSizeBytes; }
|
|
76
65
|
else if ((arg === '--include' || arg === '--filter') && argv[i+1]) { a.include = argv[++i]; }
|
|
77
66
|
else if ((arg === '--exclude' || arg === '--ignore') && argv[i+1]) { a.exclude = argv[++i]; }
|
|
67
|
+
else if (arg === '--tokens') { a.tokenCount = true; }
|
|
68
|
+
else if (arg === '--json') { a.json = true; }
|
|
69
|
+
else if ((arg === '--sort') && argv[i+1]) { a.sortBy = argv[++i]; }
|
|
70
|
+
else if ((arg === '--format' || arg === '-f') && argv[i+1]) { a.format = argv[++i]; }
|
|
78
71
|
else if (arg.startsWith('-')) { console.error('Unknown:', arg); process.exit(1); }
|
|
79
72
|
else { a.files.push(arg); }
|
|
80
73
|
}
|
|
81
74
|
return a;
|
|
82
75
|
}
|
|
83
76
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
77
|
+
// āāā Help āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
78
|
+
|
|
79
|
+
function help() {
|
|
80
|
+
console.log(`
|
|
81
|
+
šø code-snapshot v2 ā Merge codebase into one text block for AI agents
|
|
82
|
+
|
|
83
|
+
Usage:
|
|
84
|
+
snap <path> Snapshot a file or directory
|
|
85
|
+
snap <path> -o output.txt Write to file
|
|
86
|
+
snap <path> --git-only Only changed files since last commit
|
|
87
|
+
snap <path> --minify Strip blank lines and single-line comments
|
|
88
|
+
snap <path> --tokens Show estimated token count
|
|
89
|
+
snap <path> --copy Copy to clipboard (macOS: pbcopy)
|
|
90
|
+
snap <path> -i Interactive file picker
|
|
91
|
+
snap <path> --top-files Show largest files before generating
|
|
92
|
+
snap <path> --json Output as JSON (machine-readable)
|
|
93
|
+
snap --help Show this help
|
|
94
|
+
|
|
95
|
+
Format options:
|
|
96
|
+
--format marker [FILE] markers (default)
|
|
97
|
+
--format xml XML tags
|
|
98
|
+
--format plain Plain file separators
|
|
99
|
+
|
|
100
|
+
Filter options:
|
|
101
|
+
--include "*.js,*.ts" Only matching patterns
|
|
102
|
+
--exclude "*.test.*" Exclude patterns (overrides defaults)
|
|
103
|
+
--max-size 1048576 Max file size in bytes (default: 512KB)
|
|
104
|
+
--sort alpha|size|type Sort output files
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
snap ./src | claude # Pipe to Claude
|
|
108
|
+
snap . --git-only -o diff.txt # Changed files
|
|
109
|
+
snap . -i --tokens # Interactive + token estimate
|
|
110
|
+
`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// āāā .snapignore support āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
114
|
+
|
|
115
|
+
function loadIgnorePatterns(root) {
|
|
116
|
+
const patterns = [
|
|
117
|
+
'node_modules','.git','.DS_Store','dist','build','.next','.cache',
|
|
118
|
+
'__pycache__','*.pyc','.venv','venv','env','.env','coverage',
|
|
119
|
+
'*.log','*.pid','*.seed','*.min.js','*.min.css',
|
|
120
|
+
'package-lock.json','yarn.lock','pnpm-lock.yaml',
|
|
121
|
+
'*.svg','*.ico','*.png','*.jpg','*.jpeg','*.gif','*.webp',
|
|
122
|
+
'.gitignore',
|
|
123
|
+
];
|
|
124
|
+
// Load .gitignore
|
|
125
|
+
try {
|
|
126
|
+
const gi = fs.readFileSync(path.join(root, '.gitignore'), 'utf-8');
|
|
127
|
+
patterns.push(...gi.split('\n')
|
|
128
|
+
.filter(l => l.trim() && !l.startsWith('#'))
|
|
129
|
+
.map(l => l.replace(/\/$/, '').replace(/^\//, '')));
|
|
130
|
+
} catch {}
|
|
131
|
+
// Load .snapignore (if exists)
|
|
132
|
+
try {
|
|
133
|
+
const si = fs.readFileSync(path.join(root, '.snapignore'), 'utf-8');
|
|
134
|
+
patterns.push(...si.split('\n')
|
|
135
|
+
.filter(l => l.trim() && !l.startsWith('#')));
|
|
136
|
+
} catch {}
|
|
137
|
+
return [...new Set(patterns)];
|
|
88
138
|
}
|
|
89
139
|
|
|
90
|
-
|
|
140
|
+
// āāā Glob matching āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
141
|
+
|
|
142
|
+
function matchesGlob(name, patterns) {
|
|
91
143
|
if (!patterns) return false;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
144
|
+
const parts = patterns.split(',').map(s => s.trim());
|
|
145
|
+
for (const p of parts) {
|
|
146
|
+
if (!p) continue;
|
|
147
|
+
// Direct match
|
|
148
|
+
if (name === p || name.endsWith('/' + p)) return true;
|
|
149
|
+
// Simple glob
|
|
150
|
+
const hasSlash = p.includes('/') || p.startsWith('**');
|
|
151
|
+
const target = hasSlash ? name : path.basename(name);
|
|
152
|
+
try {
|
|
153
|
+
const regexStr = '^' + p
|
|
154
|
+
.replace(/\./g, '\\.')
|
|
155
|
+
.replace(/\*\*/g, '<<<GLOBSTAR>>>')
|
|
156
|
+
.replace(/\*/g, '[^/]*')
|
|
157
|
+
.replace(/<<<GLOBSTAR>>>/g, '.*')
|
|
158
|
+
.replace(/\?/g, '.')
|
|
159
|
+
+ '$';
|
|
160
|
+
if (new RegExp(regexStr).test(target)) return true;
|
|
161
|
+
} catch {}
|
|
99
162
|
}
|
|
100
163
|
return false;
|
|
101
164
|
}
|
|
102
165
|
|
|
166
|
+
// āāā Directory walk āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
167
|
+
|
|
103
168
|
function walk(root, opts, depth = 0) {
|
|
104
169
|
if (depth > opts.maxDepth) return [];
|
|
105
170
|
let results = [];
|
|
106
171
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
172
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
173
|
+
for (const entry of entries) {
|
|
174
|
+
const full = path.join(root, entry.name);
|
|
175
|
+
const rel = path.relative(opts.baseDir || root, full);
|
|
110
176
|
if (entry.isDirectory()) {
|
|
111
|
-
|
|
112
|
-
if (opts.
|
|
177
|
+
if (entry.name.startsWith('.') && entry.name !== '.') continue;
|
|
178
|
+
if (opts.ignore.includes(entry.name)) continue;
|
|
179
|
+
if (matchesGlob(rel, opts.exclude)) continue;
|
|
113
180
|
results = results.concat(walk(full, opts, depth + 1));
|
|
114
181
|
} else if (entry.isFile()) {
|
|
115
|
-
|
|
116
|
-
if (opts.
|
|
117
|
-
if (opts.
|
|
118
|
-
let skip = false;
|
|
119
|
-
for (const p of opts.gitignore) {
|
|
120
|
-
if (entry.name === p || entry.name.endsWith(p)) { skip = true; break; }
|
|
121
|
-
}
|
|
122
|
-
if (skip) continue;
|
|
123
|
-
}
|
|
124
|
-
// Check include/exclude
|
|
182
|
+
if (entry.name.startsWith('.')) continue;
|
|
183
|
+
if (opts.ignore.includes(entry.name)) continue;
|
|
184
|
+
if (matchesGlob(rel, opts.exclude)) continue;
|
|
125
185
|
if (opts.include && !matchesGlob(rel, opts.include)) continue;
|
|
126
|
-
if (opts.exclude && matchesGlob(rel, opts.exclude)) continue;
|
|
127
|
-
// Check size
|
|
128
186
|
try {
|
|
129
187
|
const stat = fs.statSync(full);
|
|
130
|
-
if (stat.size > opts.maxSize) continue;
|
|
131
|
-
if (stat.size === 0) continue;
|
|
188
|
+
if (stat.size === 0 || stat.size > opts.maxSize) continue;
|
|
132
189
|
} catch { continue; }
|
|
133
190
|
results.push(full);
|
|
134
191
|
}
|
|
@@ -137,95 +194,215 @@ function walk(root, opts, depth = 0) {
|
|
|
137
194
|
return results;
|
|
138
195
|
}
|
|
139
196
|
|
|
140
|
-
|
|
141
|
-
const ext = path.split('.').pop()?.toLowerCase();
|
|
142
|
-
const textExts = ['js','ts','jsx','tsx','py','go','rs','rb','java','cs','c','cpp','php','swift','kt','sh','bash','zsh','yaml','yml','json','xml','toml','md','mdx','sql','graphql','gql','html','css','scss','less','sass','vue','svelte','astro','svg','dockerfile','tf','pkl','gradle','properties','env','ini','cfg','conf','txt','csv','lock','gitignore','editorconfig','prettierrc','eslintrc','babelrc','npmrc','gitkeep','gitattributes'];
|
|
143
|
-
if (textExts.includes(ext || '')) return false;
|
|
144
|
-
// Check for null bytes (binary indicator)
|
|
145
|
-
return buf.includes(0);
|
|
146
|
-
}
|
|
197
|
+
// āāā Git-only files āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
147
198
|
|
|
148
199
|
function getGitChanged(root) {
|
|
149
200
|
try {
|
|
150
|
-
const out = execSync('git diff --name-only --diff-filter=MAR', {
|
|
201
|
+
const out = execSync('git diff --name-only --diff-filter=MAR', {
|
|
202
|
+
cwd: root, encoding: 'utf-8', stdio: ['pipe','pipe','pipe'],
|
|
203
|
+
});
|
|
151
204
|
const files = out.trim().split('\n').filter(Boolean);
|
|
152
|
-
|
|
153
|
-
|
|
205
|
+
const untracked = execSync('git ls-files --others --exclude-standard', {
|
|
206
|
+
cwd: root, encoding: 'utf-8', stdio: ['pipe','pipe','pipe'],
|
|
207
|
+
});
|
|
154
208
|
files.push(...untracked.trim().split('\n').filter(Boolean));
|
|
155
|
-
return files.map(f => root
|
|
209
|
+
return files.map(f => path.resolve(root, f)).filter(f => fs.existsSync(f));
|
|
156
210
|
} catch {
|
|
157
|
-
console.error('Warning: not a git repo or git not available');
|
|
158
211
|
return [];
|
|
159
212
|
}
|
|
160
213
|
}
|
|
161
214
|
|
|
215
|
+
// āāā Binary detection āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
216
|
+
|
|
217
|
+
function isBinary(buf, filePath) {
|
|
218
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
219
|
+
const textExts = new Set([
|
|
220
|
+
'js','ts','jsx','tsx','mjs','cjs','mts','cts','py','go','rs','rb','java',
|
|
221
|
+
'cs','c','cpp','h','hpp','php','swift','kt','kts','sh','bash','zsh','fish',
|
|
222
|
+
'yaml','yml','json','jsonc','json5','xml','toml','plist',
|
|
223
|
+
'md','mdx','sql','graphql','gql','html','css','scss','less','sass',
|
|
224
|
+
'vue','svelte','astro','svg','dockerfile','tf','tfvars','pkl',
|
|
225
|
+
'gradle','properties','env','ini','cfg','conf','txt','csv','tsv',
|
|
226
|
+
'lock','prisma','proto','sol','zig','ex','exs','erl','dart','r','jl','nim',
|
|
227
|
+
]);
|
|
228
|
+
if (textExts.has(ext)) return false;
|
|
229
|
+
return buf.includes(0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// āāā Minification āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
233
|
+
|
|
162
234
|
function minifyCode(code, ext) {
|
|
163
|
-
|
|
164
|
-
let lines = code.split('\n')
|
|
165
|
-
|
|
166
|
-
|
|
235
|
+
const lang = ext.toLowerCase();
|
|
236
|
+
let lines = code.split('\n');
|
|
237
|
+
|
|
238
|
+
const singleLineCommentLangs = new Set([
|
|
239
|
+
'js','ts','jsx','tsx','java','c','cpp','cs','go','rs','php','swift','kt',
|
|
240
|
+
'dart','zig','nim',
|
|
241
|
+
]);
|
|
242
|
+
const hashCommentLangs = new Set([
|
|
243
|
+
'py','rb','sh','bash','zsh','fish','yaml','yml','r','jl',
|
|
244
|
+
'dockerfile','tf','tfvars','pkl','prisma',
|
|
245
|
+
]);
|
|
246
|
+
const htmlCommentLangs = new Set(['html','vue','svelte','astro','xml','mdx']);
|
|
247
|
+
|
|
248
|
+
lines = lines.filter(l => l.trim() !== '');
|
|
249
|
+
if (singleLineCommentLangs.has(lang)) {
|
|
167
250
|
lines = lines.filter(l => !l.trim().startsWith('//'));
|
|
168
|
-
} else if (
|
|
251
|
+
} else if (hashCommentLangs.has(lang)) {
|
|
169
252
|
lines = lines.filter(l => !l.trim().startsWith('#'));
|
|
170
|
-
} else if (
|
|
253
|
+
} else if (htmlCommentLangs.has(lang)) {
|
|
171
254
|
lines = lines.filter(l => !l.trim().startsWith('<!--'));
|
|
172
255
|
}
|
|
173
256
|
return lines.join('\n');
|
|
174
257
|
}
|
|
175
258
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
let
|
|
259
|
+
// āāā Token estimation (cl100k_base encoding) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
260
|
+
|
|
261
|
+
function estimateTokens(text) {
|
|
262
|
+
let tokens = 0;
|
|
263
|
+
for (let i = 0; i < text.length; i++) {
|
|
264
|
+
const cp = text.codePointAt(i);
|
|
265
|
+
if (cp === undefined) continue;
|
|
266
|
+
if (cp < 0x80) tokens += cp <= 0x7f ? 1 : 2; // ASCII
|
|
267
|
+
else if (cp < 0x800) tokens += 2; // 2-byte UTF-8
|
|
268
|
+
else if (cp < 0x10000) tokens += 3; // 3-byte UTF-8
|
|
269
|
+
else { tokens += 4; i++; } // surrogate pair
|
|
270
|
+
}
|
|
271
|
+
return Math.ceil(tokens * 0.25); // ~4 chars per token
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// āāā Stream helper for large files āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
275
|
+
|
|
276
|
+
function readFileSafe(filePath) {
|
|
277
|
+
try {
|
|
278
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// āāā Output formatters āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
285
|
+
|
|
286
|
+
function formatFile(filePath, opts, content) {
|
|
287
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
181
288
|
const lang = LANGUAGE_MAP['.' + ext] || ext || 'text';
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
289
|
+
const rel = path.relative(opts.baseDir, filePath);
|
|
290
|
+
const chars = content.length;
|
|
291
|
+
const tokens = estimateTokens(content);
|
|
292
|
+
|
|
293
|
+
switch (opts.format) {
|
|
294
|
+
case 'xml':
|
|
295
|
+
return `<file path="${rel}" language="${lang}" size="${chars}" tokens="${tokens}">\n${content}\n</file>`;
|
|
296
|
+
case 'plain':
|
|
297
|
+
return `\n--- ${rel} (${lang}) ---\n${content}\n`;
|
|
298
|
+
case 'marker':
|
|
299
|
+
default:
|
|
300
|
+
return `\n// [FILE] ${rel}\n// [LANG] ${lang}\n// [SIZE] ${chars} chars | ~${tokens} tokens\n${content}\n// [/FILE] ${rel}\n`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// āāā Interactive picker āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
305
|
+
|
|
306
|
+
async function interactivePicker(files, opts) {
|
|
307
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
308
|
+
const question = (q) => new Promise(r => rl.question(q, r));
|
|
309
|
+
|
|
310
|
+
console.log(`\nš ${files.length} files found. Select output format:`);
|
|
311
|
+
console.log(' 1) All files (default)');
|
|
312
|
+
console.log(' 2) Top 10 largest files');
|
|
313
|
+
console.log(' 3) Top 20 largest files');
|
|
314
|
+
console.log(' 4) Custom file count');
|
|
315
|
+
const ans = (await question('\nChoose (1-4): ')).trim();
|
|
316
|
+
let selected = files;
|
|
317
|
+
|
|
318
|
+
if (ans === '2') {
|
|
319
|
+
selected = [...files].sort((a, b) => fs.statSync(b).size - fs.statSync(a).size).slice(0, 10);
|
|
320
|
+
console.log(`\n Selected top 10 files (${selected.length})`);
|
|
321
|
+
} else if (ans === '3') {
|
|
322
|
+
selected = [...files].sort((a, b) => fs.statSync(b).size - fs.statSync(a).size).slice(0, 20);
|
|
323
|
+
console.log(`\n Selected top 20 files (${selected.length})`);
|
|
324
|
+
} else if (ans === '4') {
|
|
325
|
+
const n = parseInt((await question('How many files? ')).trim()) || 10;
|
|
326
|
+
selected = [...files].sort((a, b) => fs.statSync(b).size - fs.statSync(a).size).slice(0, n);
|
|
327
|
+
console.log(`\n Selected ${selected.length} files`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const wantMinify = (await question('\nMinify? (y/N): ')).trim().toLowerCase() === 'y';
|
|
331
|
+
const wantCopy = (await question('Copy to clipboard? (y/N): ')).trim().toLowerCase() === 'y';
|
|
332
|
+
|
|
333
|
+
rl.close();
|
|
334
|
+
opts.minify = wantMinify;
|
|
335
|
+
opts.copy = wantCopy;
|
|
336
|
+
return selected;
|
|
185
337
|
}
|
|
186
338
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
339
|
+
// āāā Sorting āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
340
|
+
|
|
341
|
+
function sortFiles(files, sortBy) {
|
|
342
|
+
const f = [...files];
|
|
343
|
+
switch (sortBy) {
|
|
344
|
+
case 'size':
|
|
345
|
+
f.sort((a, b) => fs.statSync(b).size - fs.statSync(a).size);
|
|
346
|
+
break;
|
|
347
|
+
case 'type':
|
|
348
|
+
f.sort((a, b) => {
|
|
349
|
+
const ea = path.extname(a).toLowerCase();
|
|
350
|
+
const eb = path.extname(b).toLowerCase();
|
|
351
|
+
return ea.localeCompare(eb) || a.localeCompare(b);
|
|
352
|
+
});
|
|
353
|
+
break;
|
|
354
|
+
case 'alpha':
|
|
355
|
+
default:
|
|
356
|
+
f.sort((a, b) => a.localeCompare(b));
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
return f;
|
|
199
360
|
}
|
|
200
361
|
|
|
201
|
-
|
|
202
|
-
|
|
362
|
+
// āāā Top files summary āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
363
|
+
|
|
364
|
+
function topFilesReport(files, n = 10) {
|
|
365
|
+
const sorted = [...files]
|
|
366
|
+
.map(f => ({ path: f, size: fs.statSync(f).size }))
|
|
367
|
+
.sort((a, b) => b.size - a.size)
|
|
368
|
+
.slice(0, n);
|
|
369
|
+
let out = `\nš Top ${n} largest files:\n`;
|
|
370
|
+
for (const f of sorted) {
|
|
371
|
+
const rel = path.relative(process.cwd(), f.path);
|
|
372
|
+
const sizeKb = (f.size / 1024).toFixed(1);
|
|
373
|
+
out += ` ${sizeKb.padStart(8)} KB ${rel}\n`;
|
|
374
|
+
}
|
|
375
|
+
return out;
|
|
203
376
|
}
|
|
204
377
|
|
|
378
|
+
// āāā Main āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
379
|
+
|
|
205
380
|
async function main() {
|
|
206
381
|
const a = parseArgs(process.argv);
|
|
207
382
|
if (a.help) { help(); process.exit(0); }
|
|
208
383
|
|
|
209
384
|
let root = '.';
|
|
210
|
-
if (a.files.length
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
root = a.files[0];
|
|
385
|
+
if (a.files.length > 0) root = a.files[0];
|
|
386
|
+
root = path.resolve(root);
|
|
214
387
|
|
|
215
|
-
//
|
|
388
|
+
// Stat root
|
|
216
389
|
let files = [];
|
|
217
390
|
try {
|
|
218
391
|
const stat = fs.statSync(root);
|
|
219
392
|
if (stat.isFile()) {
|
|
220
393
|
files = [root];
|
|
221
394
|
} else if (stat.isDirectory()) {
|
|
222
|
-
const opts = {
|
|
395
|
+
const opts = {
|
|
396
|
+
ignore: a.gitignore ? loadIgnorePatterns(root) : [],
|
|
397
|
+
maxSize: a.maxSize, include: a.include, exclude: a.exclude,
|
|
398
|
+
maxDepth: DEFAULTS.maxDepth, baseDir: root,
|
|
399
|
+
};
|
|
223
400
|
if (a.gitOnly) {
|
|
224
401
|
files = getGitChanged(root);
|
|
225
402
|
} else {
|
|
226
403
|
files = walk(root, opts);
|
|
227
404
|
}
|
|
228
|
-
files =
|
|
405
|
+
files = [...new Set(files)];
|
|
229
406
|
}
|
|
230
407
|
} catch (e) {
|
|
231
408
|
console.error('Error accessing path:', root, '-', e.message);
|
|
@@ -237,42 +414,113 @@ async function main() {
|
|
|
237
414
|
process.exit(1);
|
|
238
415
|
}
|
|
239
416
|
|
|
240
|
-
|
|
417
|
+
// Sort
|
|
418
|
+
files = sortFiles(files, a.sortBy);
|
|
241
419
|
|
|
242
|
-
|
|
420
|
+
// Interactive mode
|
|
421
|
+
if (a.interactive) {
|
|
422
|
+
files = await interactivePicker(files, a);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Top files report
|
|
426
|
+
if (a.topFiles) {
|
|
427
|
+
console.log(topFilesReport(files));
|
|
428
|
+
if (!a.out && !a.copy) process.exit(0);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const baseDir = path.isAbsolute(root) ? root : path.resolve(root);
|
|
432
|
+
const opts = {
|
|
433
|
+
format: a.format, minify: a.minify,
|
|
434
|
+
gitOnly: a.gitOnly, maxSize: a.maxSize,
|
|
435
|
+
baseDir: stat.isFile ? path.dirname(root) : root,
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// Build output
|
|
439
|
+
let header = '';
|
|
440
|
+
header += '='.repeat(60) + '\n';
|
|
441
|
+
header += 'šø CODE SNAPSHOT\n';
|
|
442
|
+
header += '='.repeat(60) + '\n';
|
|
443
|
+
header += `Generated: ${new Date().toISOString()}\n`;
|
|
444
|
+
header += `Source: ${root}\n`;
|
|
445
|
+
header += `Files scanned: ${files.length}\n`;
|
|
446
|
+
header += `Options: git-only=${a.gitOnly} minify=${a.minify} max-size=${a.maxSize}\n`;
|
|
447
|
+
header += `Intended for: AI agents (Claude Code, ChatGPT, Codex, etc.)\n`;
|
|
448
|
+
header += '='.repeat(60) + '\n\n';
|
|
449
|
+
|
|
450
|
+
let body = '';
|
|
243
451
|
let count = 0;
|
|
452
|
+
let totalChars = 0;
|
|
453
|
+
const skipped = [];
|
|
454
|
+
|
|
244
455
|
for (const file of files) {
|
|
245
456
|
try {
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
457
|
+
let content = readFileSafe(file);
|
|
458
|
+
if (content === null) { skipped.push(file); continue; }
|
|
459
|
+
if (a.noBinary && isBinary(Buffer.from(content, 'utf-8'), file)) { skipped.push(file); continue; }
|
|
460
|
+
if (a.minify) content = minifyCode(content, path.extname(file).slice(1));
|
|
461
|
+
body += formatFile(file, opts, content);
|
|
462
|
+
totalChars += content.length;
|
|
463
|
+
count++;
|
|
251
464
|
} catch (e) {
|
|
252
|
-
|
|
465
|
+
skipped.push(file);
|
|
253
466
|
}
|
|
254
467
|
}
|
|
255
|
-
output += buildFooter();
|
|
256
468
|
|
|
257
|
-
|
|
469
|
+
let footer = '';
|
|
470
|
+
footer += '\n' + '='.repeat(60) + '\n';
|
|
471
|
+
footer += 'END OF SNAPSHOT\n';
|
|
472
|
+
footer += '='.repeat(60) + '\n';
|
|
473
|
+
footer += `\nFiles included: ${count}/${files.length}`;
|
|
474
|
+
if (skipped.length) footer += ` | Skipped: ${skipped.length}`;
|
|
475
|
+
footer += ` | Total chars: ${totalChars}`;
|
|
476
|
+
footer += ` | Est. tokens: ~${estimateTokens(header + body + footer)}\n`;
|
|
258
477
|
|
|
478
|
+
const output = header + body + footer;
|
|
479
|
+
|
|
480
|
+
// JSON output
|
|
481
|
+
if (a.json) {
|
|
482
|
+
const json = {
|
|
483
|
+
version: '2.0.0',
|
|
484
|
+
generated: new Date().toISOString(),
|
|
485
|
+
source: root,
|
|
486
|
+
files_count: count,
|
|
487
|
+
total_chars: totalChars,
|
|
488
|
+
estimated_tokens: estimateTokens(header + body + footer),
|
|
489
|
+
files: files
|
|
490
|
+
.filter(f => !skipped.includes(f))
|
|
491
|
+
.map(f => ({
|
|
492
|
+
path: path.relative(baseDir, f),
|
|
493
|
+
content: (() => {
|
|
494
|
+
let c = readFileSafe(f);
|
|
495
|
+
if (a.minify) c = minifyCode(c || '', path.extname(f).slice(1));
|
|
496
|
+
return c || '';
|
|
497
|
+
})(),
|
|
498
|
+
})),
|
|
499
|
+
};
|
|
500
|
+
const jsonStr = JSON.stringify(json, null, 2);
|
|
501
|
+
if (a.out) {
|
|
502
|
+
fs.writeFileSync(a.out, jsonStr, 'utf-8');
|
|
503
|
+
console.log(`ā
Written to ${a.out}`);
|
|
504
|
+
} else {
|
|
505
|
+
console.log(jsonStr);
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Output
|
|
259
511
|
if (a.out) {
|
|
260
512
|
fs.writeFileSync(a.out, output, 'utf-8');
|
|
261
|
-
console.log(`ā
Written to ${a.out} (${output.length} chars)`);
|
|
513
|
+
console.log(`ā
Written to ${a.out} (${output.length} chars, ~${estimateTokens(output)} tokens)`);
|
|
262
514
|
} else if (a.copy) {
|
|
263
515
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
console.log(`ā
Copied to clipboard (${output.length} chars)`);
|
|
516
|
+
execSync('pbcopy', { input: output, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
517
|
+
console.log(`ā
Copied to clipboard (${output.length} chars, ~${estimateTokens(output)} tokens)`);
|
|
267
518
|
} catch {
|
|
268
519
|
console.log(output);
|
|
269
|
-
console.log('ā Clipboard not available, printed to stdout instead.');
|
|
270
520
|
}
|
|
271
521
|
} else {
|
|
272
522
|
console.log(output);
|
|
273
523
|
}
|
|
274
|
-
|
|
275
|
-
console.log(`\nš Stats: ${count}/${files.length} files, ${output.length} chars, ~${Math.ceil(output.length / 4)} tokens`);
|
|
276
524
|
}
|
|
277
525
|
|
|
278
526
|
main().catch(e => { console.error('Fatal:', e); process.exit(1); });
|
package/package.json
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kevinxyz/code-snapshot",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Merge your codebase into one text block for AI agents. Stop copying files manually.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
-
"bin": {
|
|
6
|
+
"bin": {
|
|
7
|
+
"code-snapshot": "./index.js",
|
|
8
|
+
"snap": "./index.js"
|
|
9
|
+
},
|
|
7
10
|
"type": "module",
|
|
8
|
-
"scripts": {
|
|
9
|
-
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test",
|
|
13
|
+
"snap": "node index.js",
|
|
14
|
+
"start": "node index.js ."
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"code-snapshot", "ai", "claude", "chatgpt", "context",
|
|
18
|
+
"codebase", "agent", "claude-code", "cursor", "copilot",
|
|
19
|
+
"codex", "prompt", "llm", "developer-tool"
|
|
20
|
+
],
|
|
10
21
|
"author": "Kevin-X00",
|
|
11
22
|
"license": "MIT",
|
|
12
23
|
"publishConfig": { "access": "public" },
|
|
13
|
-
"repository": {
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+ssh://git@github.com:Kevin-X00/code-snapshot.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://kevin-x00.github.io/code-snapshot",
|
|
29
|
+
"engines": { "node": ">=18" }
|
|
14
30
|
}
|