@kevinxyz/code-snapshot 1.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 +82 -0
- package/index.js +278 -0
- package/package.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# šø code-snapshot
|
|
2
|
+
|
|
3
|
+
**Merge your codebase into one prompt-ready text block for AI agents.**
|
|
4
|
+
|
|
5
|
+
Stop manually copying files into ChatGPT/Claude Code/Codex. One command, done.
|
|
6
|
+
|
|
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
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Why?
|
|
16
|
+
|
|
17
|
+
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
|
+
|
|
19
|
+
**code-snapshot** automates this. One command gives you a clean, structured text block your AI agent can immediately understand.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
| Feature | CLI Flag | Description |
|
|
24
|
+
|---------|----------|-------------|
|
|
25
|
+
| Directory walk | `snap <dir>` | Recursive with `.gitignore` awareness |
|
|
26
|
+
| Git-only | `--git-only` | Only modified/new files since last commit |
|
|
27
|
+
| Minify | `--minify` | Strip blank lines and single-line comments (save tokens) |
|
|
28
|
+
| Output file | `-o <file>` | Write to file instead of stdout |
|
|
29
|
+
| Clipboard | `--copy` | Copy to clipboard (macOS/Linux) |
|
|
30
|
+
| Include filter | `--include "*.js,*.ts"` | Only matching file patterns |
|
|
31
|
+
| Exclude filter | `--exclude "*.test.*"` | Skip matching patterns |
|
|
32
|
+
| Max file size | `--max-size <bytes>` | Skip large files (default: 512KB) |
|
|
33
|
+
| Skip binary | `--no-binary` | Auto-detect and skip binary files (default: on) |
|
|
34
|
+
| Ignore gitignore | `--no-gitignore` | Don't read .gitignore rules |
|
|
35
|
+
|
|
36
|
+
## Quick Examples
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Everything you need for your AI prompt
|
|
40
|
+
snap ./src -o context.txt
|
|
41
|
+
|
|
42
|
+
# Just what changed today
|
|
43
|
+
snap . --git-only
|
|
44
|
+
|
|
45
|
+
# Clean it up (save tokens)
|
|
46
|
+
snap ./lib --minify --exclude "*.test.js,*.spec.js"
|
|
47
|
+
|
|
48
|
+
# Pipe directly to Claude Code (or any AI)
|
|
49
|
+
snap . | claude
|
|
50
|
+
|
|
51
|
+
# Quick clipboard
|
|
52
|
+
snap ./src --copy
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Output Format
|
|
56
|
+
|
|
57
|
+
Every file is wrapped with clear markers:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
// [FILE] src/utils.ts
|
|
61
|
+
// [LANG] TypeScript
|
|
62
|
+
// [SIZE] 1234 chars
|
|
63
|
+
export function hello() { ... }
|
|
64
|
+
// [/FILE] src/utils.ts
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The header includes a summary for token estimation. Footer gives total stats.
|
|
68
|
+
|
|
69
|
+
## Ignored by Default
|
|
70
|
+
|
|
71
|
+
`node_modules`, `.git`, `.DS_Store`, `dist`, `build`, `.next`, `.cache`, `__pycache__`, `.venv`, `venv`, `env`, `coverage`, plus everything in your `.gitignore`.
|
|
72
|
+
|
|
73
|
+
## Roadmap
|
|
74
|
+
|
|
75
|
+
- [ ] Interactive file picker
|
|
76
|
+
- [ ] Token-aware chunking (split large outputs for context limits)
|
|
77
|
+
- [ ] VS Code extension
|
|
78
|
+
- [ ] .snapignore file support
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT ā free for everyone. If you find it useful, a GitHub star goes a long way.
|
package/index.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* code-snapshot ā Merge your codebase into one text block for AI agents.
|
|
5
|
+
*
|
|
6
|
+
* Stop manually copying files into ChatGPT/Claude Code/Codex.
|
|
7
|
+
* Just run: snap <dir>
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
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)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
20
|
+
|
|
21
|
+
const LANGUAGE_MAP = {
|
|
22
|
+
'.js':'JavaScript','.ts':'TypeScript','.jsx':'React JSX','.tsx':'React TSX',
|
|
23
|
+
'.py':'Python','.go':'Go','.rs':'Rust','.rb':'Ruby','.java':'Java',
|
|
24
|
+
'.cs':'C#','.c':'C','.cpp':'C++','.php':'PHP','.swift':'Swift','.kt':'Kotlin',
|
|
25
|
+
'.sh':'Bash','.bash':'Bash','.zsh':'Zsh','.fish':'Fish',
|
|
26
|
+
'.yaml':'YAML','.yml':'YAML','.json':'JSON','.xml':'XML','.toml':'TOML',
|
|
27
|
+
'.md':'Markdown','.mdx':'MDX','.sql':'SQL','.graphql':'GraphQL','.gql':'GraphQL',
|
|
28
|
+
'.html':'HTML','.css':'CSS','.scss':'SCSS','.less':'Less','.sass':'Sass',
|
|
29
|
+
'.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',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const DEFAULTS = { maxSizeBytes: 512 * 1024, maxDepth: 8 };
|
|
37
|
+
|
|
38
|
+
function help() {
|
|
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
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseArgs(argv) {
|
|
65
|
+
const a = { files:[], out:null, gitOnly:false, minify:false, copy:false, noBinary:true, maxSize:DEFAULTS.maxSizeBytes, include:null, exclude:null, gitignore:true };
|
|
66
|
+
for (let i = 2; i < argv.length; i++) {
|
|
67
|
+
const arg = argv[i];
|
|
68
|
+
if (arg === '--help' || arg === '-h') { a.help = true; }
|
|
69
|
+
else if ((arg === '-o' || arg === '--out') && argv[i+1]) { a.out = argv[++i]; }
|
|
70
|
+
else if (arg === '--git-only') { a.gitOnly = true; }
|
|
71
|
+
else if (arg === '--minify') { a.minify = true; }
|
|
72
|
+
else if (arg === '--copy') { a.copy = true; }
|
|
73
|
+
else if (arg === '--no-binary') { a.noBinary = true; }
|
|
74
|
+
else if (arg === '--no-gitignore') { a.gitignore = false; }
|
|
75
|
+
else if ((arg === '--max-size') && argv[i+1]) { a.maxSize = parseInt(argv[++i]) || DEFAULTS.maxSizeBytes; }
|
|
76
|
+
else if ((arg === '--include' || arg === '--filter') && argv[i+1]) { a.include = argv[++i]; }
|
|
77
|
+
else if ((arg === '--exclude' || arg === '--ignore') && argv[i+1]) { a.exclude = argv[++i]; }
|
|
78
|
+
else if (arg.startsWith('-')) { console.error('Unknown:', arg); process.exit(1); }
|
|
79
|
+
else { a.files.push(arg); }
|
|
80
|
+
}
|
|
81
|
+
return a;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function loadGitignore(root) {
|
|
85
|
+
const patterns = ['node_modules','.git','.DS_Store','dist','build','.next','.cache','__pycache__','*.pyc','.venv','venv','env','.env','coverage','*.log','.gitignore'];
|
|
86
|
+
try { patterns.push(...fs.readFileSync(root+'/.gitignore','utf-8').split('\n').filter(l => l.trim() && !l.startsWith('#')).map(l => l.replace(/\/$/, ''))); } catch {}
|
|
87
|
+
return patterns;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function matchesGlob(path, patterns) {
|
|
91
|
+
if (!patterns) return false;
|
|
92
|
+
for (const p of patterns.split(',').map(s => s.trim().replace(/^\*\./, '.').replace(/\*/g, '.*'))) {
|
|
93
|
+
// detect if glob has path separator
|
|
94
|
+
const hasSlash = p.includes('/');
|
|
95
|
+
const name = path.split('/').pop() || path;
|
|
96
|
+
const target = hasSlash ? path : name;
|
|
97
|
+
try { if (new RegExp('^' + p.replace(/\./g,'\\.').replace(/\*/g,'.*') + '$').test(target)) return true; } catch {}
|
|
98
|
+
try { if (new RegExp(p).test(target)) return true; } catch {}
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function walk(root, opts, depth = 0) {
|
|
104
|
+
if (depth > opts.maxDepth) return [];
|
|
105
|
+
let results = [];
|
|
106
|
+
try {
|
|
107
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
108
|
+
const full = root + '/' + entry.name;
|
|
109
|
+
const rel = full.replace(/^\.\//, '');
|
|
110
|
+
if (entry.isDirectory()) {
|
|
111
|
+
const name = entry.name;
|
|
112
|
+
if (opts.gitignore.includes(name) || name.startsWith('.')) continue;
|
|
113
|
+
results = results.concat(walk(full, opts, depth + 1));
|
|
114
|
+
} else if (entry.isFile()) {
|
|
115
|
+
// Skip if excluded
|
|
116
|
+
if (opts.gitignore.includes(entry.name)) continue;
|
|
117
|
+
if (opts.gitignore.length) {
|
|
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
|
|
125
|
+
if (opts.include && !matchesGlob(rel, opts.include)) continue;
|
|
126
|
+
if (opts.exclude && matchesGlob(rel, opts.exclude)) continue;
|
|
127
|
+
// Check size
|
|
128
|
+
try {
|
|
129
|
+
const stat = fs.statSync(full);
|
|
130
|
+
if (stat.size > opts.maxSize) continue;
|
|
131
|
+
if (stat.size === 0) continue;
|
|
132
|
+
} catch { continue; }
|
|
133
|
+
results.push(full);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch {}
|
|
137
|
+
return results;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isBinary(buf, path) {
|
|
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
|
+
}
|
|
147
|
+
|
|
148
|
+
function getGitChanged(root) {
|
|
149
|
+
try {
|
|
150
|
+
const out = execSync('git diff --name-only --diff-filter=MAR', { cwd: root, encoding: 'utf-8', stdio: ['pipe','pipe','pipe'] });
|
|
151
|
+
const files = out.trim().split('\n').filter(Boolean);
|
|
152
|
+
// Also include unstaged
|
|
153
|
+
const untracked = execSync('git ls-files --others --exclude-standard', { cwd: root, encoding: 'utf-8', stdio: ['pipe','pipe','pipe'] });
|
|
154
|
+
files.push(...untracked.trim().split('\n').filter(Boolean));
|
|
155
|
+
return files.map(f => root + '/' + f);
|
|
156
|
+
} catch {
|
|
157
|
+
console.error('Warning: not a git repo or git not available');
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function minifyCode(code, ext) {
|
|
163
|
+
// Strip blank lines
|
|
164
|
+
let lines = code.split('\n').filter(l => l.trim() !== '');
|
|
165
|
+
// Strip single-line comments (for some languages)
|
|
166
|
+
if (['js','ts','jsx','tsx','java','c','cpp','cs','go','rs','php','swift','kt'].includes(ext)) {
|
|
167
|
+
lines = lines.filter(l => !l.trim().startsWith('//'));
|
|
168
|
+
} else if (['py','rb','sh','bash','yaml','yml'].includes(ext)) {
|
|
169
|
+
lines = lines.filter(l => !l.trim().startsWith('#'));
|
|
170
|
+
} else if (['html','vue','svelte'].includes(ext)) {
|
|
171
|
+
lines = lines.filter(l => !l.trim().startsWith('<!--'));
|
|
172
|
+
}
|
|
173
|
+
return lines.join('\n');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function formatFile(path, opts) {
|
|
177
|
+
const buf = fs.readFileSync(path);
|
|
178
|
+
const ext = path.split('.').pop()?.toLowerCase() || '';
|
|
179
|
+
if (opts.noBinary && isBinary(buf, path)) return null;
|
|
180
|
+
let code = buf.toString('utf-8');
|
|
181
|
+
const lang = LANGUAGE_MAP['.' + ext] || ext || 'text';
|
|
182
|
+
if (opts.minify) code = minifyCode(code, ext);
|
|
183
|
+
const rel = path.replace(/^\.\//, '');
|
|
184
|
+
return `\n// [FILE] ${rel}\n// [LANG] ${lang}\n// [SIZE] ${code.length} chars\n${code}\n// [/FILE] ${rel}\n`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function buildHeader(files, opts) {
|
|
188
|
+
const h = [];
|
|
189
|
+
h.push('='.repeat(60));
|
|
190
|
+
h.push('CODE SNAPSHOT');
|
|
191
|
+
h.push('='.repeat(60));
|
|
192
|
+
h.push(`Generated: ${new Date().toISOString()}`);
|
|
193
|
+
h.push(`Total files: ${files.length}`);
|
|
194
|
+
h.push(`Options: git-only=${opts.gitOnly} minify=${opts.minify} max-size=${opts.maxSize}`);
|
|
195
|
+
h.push(`Intended for: AI agents (Claude Code, ChatGPT, Codex, etc.)`);
|
|
196
|
+
h.push('='.repeat(60));
|
|
197
|
+
h.push('');
|
|
198
|
+
return h.join('\n');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildFooter() {
|
|
202
|
+
return '\n' + '='.repeat(60) + '\nEND OF SNAPSHOT\n' + '='.repeat(60) + '\n';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function main() {
|
|
206
|
+
const a = parseArgs(process.argv);
|
|
207
|
+
if (a.help) { help(); process.exit(0); }
|
|
208
|
+
|
|
209
|
+
let root = '.';
|
|
210
|
+
if (a.files.length === 0) {
|
|
211
|
+
a.files.push('.');
|
|
212
|
+
}
|
|
213
|
+
root = a.files[0];
|
|
214
|
+
|
|
215
|
+
// Resolve single file or directory
|
|
216
|
+
let files = [];
|
|
217
|
+
try {
|
|
218
|
+
const stat = fs.statSync(root);
|
|
219
|
+
if (stat.isFile()) {
|
|
220
|
+
files = [root];
|
|
221
|
+
} else if (stat.isDirectory()) {
|
|
222
|
+
const opts = { gitignore: a.gitignore ? loadGitignore(root) : [], maxSize: a.maxSize, include: a.include, exclude: a.exclude, maxDepth: DEFAULTS.maxDepth };
|
|
223
|
+
if (a.gitOnly) {
|
|
224
|
+
files = getGitChanged(root);
|
|
225
|
+
} else {
|
|
226
|
+
files = walk(root, opts);
|
|
227
|
+
}
|
|
228
|
+
files = files.filter((f, i) => files.indexOf(f) === i); // dedupe
|
|
229
|
+
}
|
|
230
|
+
} catch (e) {
|
|
231
|
+
console.error('Error accessing path:', root, '-', e.message);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (files.length === 0) {
|
|
236
|
+
console.error('No files found.');
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log(`šø code-snapshot: scanning ${files.length} file(s)...\n`);
|
|
241
|
+
|
|
242
|
+
let output = buildHeader(files, a);
|
|
243
|
+
let count = 0;
|
|
244
|
+
for (const file of files) {
|
|
245
|
+
try {
|
|
246
|
+
const block = formatFile(file, a);
|
|
247
|
+
if (block) {
|
|
248
|
+
output += block;
|
|
249
|
+
count++;
|
|
250
|
+
}
|
|
251
|
+
} catch (e) {
|
|
252
|
+
console.error(` ā Skipping ${file}: ${e.message}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
output += buildFooter();
|
|
256
|
+
|
|
257
|
+
output += `\nSummary: ${count} files, ${output.length} chars (~${Math.ceil(output.length / 4)} tokens)\n`;
|
|
258
|
+
|
|
259
|
+
if (a.out) {
|
|
260
|
+
fs.writeFileSync(a.out, output, 'utf-8');
|
|
261
|
+
console.log(`ā
Written to ${a.out} (${output.length} chars)`);
|
|
262
|
+
} else if (a.copy) {
|
|
263
|
+
try {
|
|
264
|
+
const proc = execSync('pbcopy 2>/dev/null || xclip -selection clipboard 2>/dev/null || xsel -b 2>/dev/null || echo "no-clipboard"', { input: output, encoding: 'utf-8', stdio: ['pipe','pipe','pipe'] });
|
|
265
|
+
if (proc?.stderr?.includes('no-clipboard')) throw new Error('no clipboard');
|
|
266
|
+
console.log(`ā
Copied to clipboard (${output.length} chars)`);
|
|
267
|
+
} catch {
|
|
268
|
+
console.log(output);
|
|
269
|
+
console.log('ā Clipboard not available, printed to stdout instead.');
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
console.log(output);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(`\nš Stats: ${count}/${files.length} files, ${output.length} chars, ~${Math.ceil(output.length / 4)} tokens`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
main().catch(e => { console.error('Fatal:', e); process.exit(1); });
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kevinxyz/code-snapshot",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Take a snapshot of your codebase ā merge all project files into one text block for AI agents (Claude Code, ChatGPT, Codex, etc.). Stop copying files manually.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": { "code-snapshot": "./index.js", "snap": "./index.js" },
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": { "test": "node --test" },
|
|
9
|
+
"keywords": ["code-snapshot", "ai", "claude", "chatgpt", "context", "codebase", "agent"],
|
|
10
|
+
"author": "Kevin-X00",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"publishConfig": { "access": "public" },
|
|
13
|
+
"repository": { "type": "git", "url": "git+ssh://git@github.com:Kevin-X00/code-snapshot.git" }
|
|
14
|
+
}
|