@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.
Files changed (3) hide show
  1. package/README.md +82 -0
  2. package/index.js +278 -0
  3. 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
+ }