@xelth/eck-snapshot 6.5.1 → 6.6.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.
@@ -1,283 +1,308 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import chalk from 'chalk';
4
- import micromatch from 'micromatch';
5
- import isBinaryPath from 'is-binary-path';
6
- import {
7
- generateDirectoryTree,
8
- generateTimestamp,
9
- readFileWithSizeCheck,
10
- parseSize,
11
- loadGitignore,
12
- getProjectFiles,
13
- matchesPattern
14
- } from '../../utils/fileUtils.js';
15
- import { detectProjectType, getProjectSpecificFiltering, getAllDetectedTypes } from '../../utils/projectDetector.js';
16
- import { loadSetupConfig } from '../../config.js';
17
- import { getDepthConfig, DEPTH_SCALE } from '../../core/depthConfig.js';
18
- import { skeletonize } from '../../core/skeletonizer.js';
19
-
20
- export async function runReconTool(payload) {
21
- const toolName = payload.name;
22
- const args = payload.arguments || {};
23
-
24
- if (toolName === 'eck_scout') {
25
- const depth = args.depth !== undefined ? parseInt(args.depth, 10) : 0;
26
- await runScout(depth);
27
- } else if (toolName === 'eck_fetch') {
28
- if (!args.patterns || !Array.isArray(args.patterns)) {
29
- console.log(chalk.red('❌ Error: eck_fetch requires an array of "patterns" in arguments.'));
30
- return;
31
- }
32
- await runFetch(args.patterns);
33
- }
34
- }
35
-
36
- async function runScout(depth = 0) {
37
- const depthCfg = getDepthConfig(depth);
38
- const depthInfo = DEPTH_SCALE[depth] || DEPTH_SCALE[0];
39
- console.log(chalk.blue(`🕵️ Scouting repository (depth ${depth}: ${depthInfo.mode})...`));
40
- try {
41
- const repoPath = process.cwd();
42
- const repoName = path.basename(repoPath);
43
- const setupConfig = await loadSetupConfig();
44
- let config = { ...setupConfig.fileFiltering, ...setupConfig.performance };
45
-
46
- // Apply project-specific filtering (was missing in previous versions)
47
- const projectDetection = await detectProjectType(repoPath);
48
- const allTypes = getAllDetectedTypes(projectDetection);
49
- if (allTypes && allTypes.length > 0) {
50
- const projectSpecific = await getProjectSpecificFiltering(allTypes);
51
- config = {
52
- ...config,
53
- dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
54
- filesToIgnore: [...(config.filesToIgnore || []), ...(projectSpecific.filesToIgnore || [])],
55
- extensionsToIgnore: [...(config.extensionsToIgnore || []), ...(projectSpecific.extensionsToIgnore || [])]
56
- };
57
- }
58
-
59
- // Use a deep maxDepth for scout so the AI can see the full structure
60
- config.maxDepth = 15;
61
-
62
- // Use getProjectFiles which respects git tracking natively
63
- let allFiles = await getProjectFiles(repoPath, config);
64
- const gitignore = await loadGitignore(repoPath);
65
-
66
- // Filter binaries, gitignore/eckignore, and file-level ignores
67
- allFiles = allFiles.filter(f => {
68
- const normalized = f.replace(/\\/g, '/');
69
- if (isBinaryPath(f)) return false;
70
- if (gitignore.ignores(normalized)) return false;
71
- if (config.filesToIgnore && matchesPattern(normalized, config.filesToIgnore)) return false;
72
- return true;
73
- });
74
-
75
- const directoryTree = await generateDirectoryTree(repoPath, '', allFiles, 0, config.maxDepth, config);
76
-
77
- // Build file contents section if depth > 0
78
- let fileContentSection = '';
79
- if (!depthCfg.skipContent) {
80
- const maxFileSize = parseSize(config.maxFileSize || '10MB');
81
- let processedCount = 0;
82
-
83
- for (const file of allFiles) {
84
- try {
85
- const fullPath = path.join(repoPath, file);
86
- let content = await readFileWithSizeCheck(fullPath, maxFileSize);
87
-
88
- // Apply skeletonization
89
- if (depthCfg.skeleton) {
90
- content = await skeletonize(content, file, { preserveDocs: depthCfg.preserveDocs !== false });
91
- }
92
-
93
- // Apply line truncation
94
- if (depthCfg.maxLinesPerFile && depthCfg.maxLinesPerFile > 0) {
95
- const lines = content.split('\n');
96
- if (lines.length > depthCfg.maxLinesPerFile) {
97
- content = lines.slice(0, depthCfg.maxLinesPerFile).join('\n');
98
- content += `\n// ... truncated (${lines.length - depthCfg.maxLinesPerFile} more lines)`;
99
- }
100
- }
101
-
102
- fileContentSection += `--- File: /${file} ---\n\n\`\`\`\n${content}\n\`\`\`\n\n`;
103
- processedCount++;
104
- } catch (e) {
105
- fileContentSection += `--- File: /${file} ---\n\n[ERROR: ${e.message}]\n\n`;
106
- }
107
- }
108
-
109
- console.log(chalk.gray(` Processed ${processedCount} files at depth ${depth}`));
110
- }
111
-
112
- const timestamp = generateTimestamp();
113
- const suffix = depth > 0 ? `_d${depth}` : '';
114
- const filename = `scout_tree_${repoName}_${timestamp}${suffix}.md`;
115
-
116
- const depthScaleTable = DEPTH_SCALE.map(d => `| ${d.depth} | ${d.mode} | ${d.description} |`).join('\n');
117
-
118
- let outputContent = `# ⚠️ EXTERNAL REPOSITORY SCOUT: [${repoName}]
119
-
120
- **CRITICAL INSTRUCTION FOR AI:** You are currently working on your primary project. The data below is strictly for REFERENCE from an external repository named \`${repoName}\`. DO NOT assume the role of architect for this repository. DO NOT attempt to write code for this repository.
121
-
122
- **Depth:** ${depth} (${depthInfo.mode} — ${depthInfo.description})
123
-
124
- ## How to request more data from this repository
125
- Use the \`scout\` command with a higher depth level, or \`fetch\` for specific files:
126
-
127
- **Scout with depth (0-9):**
128
- \`\`\`bash
129
- eck-snapshot scout 5 # skeleton mode
130
- eck-snapshot scout 9 # full content
131
- \`\`\`
132
-
133
- **Fetch specific files (run inside this repo's directory):**
134
- \`\`\`bash
135
- cd ${repoPath.replace(/\\/g, '/')}
136
- eck-snapshot fetch "src/**/*.js" "README.md"
137
- \`\`\`
138
-
139
- **⚠️ CRITICAL FETCH RULES:**
140
- 1. **\`fetch\` only works inside the repo it scans.** You MUST \`cd\` into the correct project directory first.
141
- 2. **Use RELATIVE paths or glob patterns**, never absolute paths. Files are matched against the repo root.
142
- 3. **If you need files from multiple repos**, issue SEPARATE fetch commands — one per repo, each with its own \`cd\`.
143
- 4. **Prefer glob patterns over exact paths** — tree paths are easy to misread:
144
- - Instead of \`"src/utils/helper.js"\` use \`"**/helper.js"\`
145
- - Use \`"**/<filename>"\` to find a file anywhere in the tree.
146
-
147
- **Depth scale:**
148
- | Depth | Mode | Description |
149
- |-------|------|-------------|
150
- ${depthScaleTable}
151
-
152
- ## Directory Structure
153
- \`\`\`text
154
- ${directoryTree}
155
- \`\`\`
156
- `;
157
-
158
- if (fileContentSection) {
159
- outputContent += `\n## File Contents (depth ${depth}: ${depthInfo.mode})\n\n${fileContentSection}`;
160
- }
161
-
162
- await fs.mkdir(path.join(repoPath, '.eck', 'scouts'), { recursive: true });
163
- const outputPath = path.join(repoPath, '.eck', 'scouts', filename);
164
- await fs.writeFile(outputPath, outputContent, 'utf-8');
165
-
166
- const sizeBytes = Buffer.byteLength(outputContent, 'utf-8');
167
- const sizeStr = sizeBytes < 1024 ? `${sizeBytes} B` : sizeBytes < 1048576 ? `${(sizeBytes / 1024).toFixed(1)} KB` : `${(sizeBytes / 1048576).toFixed(1)} MB`;
168
- const approxTokens = Math.round(outputContent.length / 4);
169
- const tokensStr = approxTokens < 1000 ? `${approxTokens}` : `${(approxTokens / 1000).toFixed(1)}k`;
170
-
171
- console.log(chalk.green(`✅ Scout complete. Saved to: .eck/scouts/${filename}`));
172
- console.log(chalk.gray(` Size: ${sizeStr} | ~${tokensStr} tokens`));
173
- } catch (error) {
174
- console.error(chalk.red(`❌ Scout failed: ${error.message}`));
175
- }
176
- }
177
-
178
- async function runFetch(patterns) {
179
- console.log(chalk.blue(`🚚 Fetching files matching patterns: ${patterns.join(', ')}...`));
180
- try {
181
- const repoPath = process.cwd();
182
- const repoName = path.basename(repoPath);
183
- const repoPathNorm = repoPath.replace(/\\/g, '/').replace(/\/$/, '') + '/';
184
- const setupConfig = await loadSetupConfig();
185
- let config = { ...setupConfig.fileFiltering, ...setupConfig.performance };
186
-
187
- // Apply project-specific filtering
188
- const projectDetection = await detectProjectType(repoPath);
189
- const allTypes = getAllDetectedTypes(projectDetection);
190
- if (allTypes && allTypes.length > 0) {
191
- const projectSpecific = await getProjectSpecificFiltering(allTypes);
192
- config = {
193
- ...config,
194
- dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
195
- filesToIgnore: [...(config.filesToIgnore || []), ...(projectSpecific.filesToIgnore || [])],
196
- extensionsToIgnore: [...(config.extensionsToIgnore || []), ...(projectSpecific.extensionsToIgnore || [])]
197
- };
198
- }
199
-
200
- let allFiles = await getProjectFiles(repoPath, config);
201
- const gitignore = await loadGitignore(repoPath);
202
-
203
- allFiles = allFiles.filter(f => {
204
- const normalized = f.replace(/\\/g, '/');
205
- if (isBinaryPath(f)) return false;
206
- if (gitignore.ignores(normalized)) return false;
207
- if (config.filesToIgnore && matchesPattern(normalized, config.filesToIgnore)) return false;
208
- return true;
209
- });
210
-
211
- // Normalize patterns: strip absolute cwd prefix, convert backslashes,
212
- // and auto-wrap bare filenames with **/ for convenience
213
- const normalizedPatterns = patterns.map(p => {
214
- let norm = p.replace(/\\/g, '/');
215
- // Strip absolute path prefix matching cwd (case-insensitive on Windows)
216
- if (norm.toLowerCase().startsWith(repoPathNorm.toLowerCase())) {
217
- norm = norm.slice(repoPathNorm.length);
218
- }
219
- // If it looks like an absolute path from another project, extract just the filename
220
- if (path.isAbsolute(norm) || /^[A-Za-z]:\//.test(norm)) {
221
- const basename = path.basename(norm);
222
- console.log(chalk.yellow(` ⚠️ Cross-repo absolute path detected, using: **/${basename}`));
223
- norm = `**/${basename}`;
224
- }
225
- // If it's a plain filename with no glob chars and no path separators, wrap it
226
- if (!norm.includes('/') && !norm.includes('*') && !norm.includes('?')) {
227
- norm = `**/${norm}`;
228
- }
229
- return norm;
230
- });
231
-
232
- const matchedFiles = micromatch(allFiles, normalizedPatterns);
233
-
234
- if (matchedFiles.length === 0) {
235
- console.log(chalk.yellow('⚠️ No files matched the requested patterns.'));
236
- return;
237
- }
238
-
239
- let fileContentStr = '';
240
- let fetchedCount = 0;
241
- const maxFileSize = parseSize(config.maxFileSize || '10MB');
242
-
243
- for (const file of matchedFiles) {
244
- try {
245
- const fullPath = path.join(repoPath, file);
246
- const content = await readFileWithSizeCheck(fullPath, maxFileSize);
247
- fileContentStr += `--- File: /${file} ---\n\n\`\`\`\n${content}\n\`\`\`\n\n`;
248
- fetchedCount++;
249
- } catch (e) {
250
- fileContentStr += `--- File: /${file} ---\n\n[ERROR: ${e.message}]\n\n`;
251
- }
252
- }
253
-
254
- const timestamp = generateTimestamp();
255
- const filename = `scout_data_${repoName}_${timestamp}.md`;
256
-
257
- // Check how many patterns actually matched at least one file
258
- const matchedPatternCount = normalizedPatterns.filter(p => micromatch(allFiles, [p]).length > 0).length;
259
- const missedCount = normalizedPatterns.length - matchedPatternCount;
260
- const missedWarning = missedCount > 0 ? `\n**⚠️ ${missedCount} of ${patterns.length} requested patterns returned no results.** You likely misread the directory tree. Re-check the tree carefully and retry with glob patterns like \`"**/<filename>"\` to match files regardless of nesting depth.\n` : '';
261
-
262
- const finalContent = `# ⚠️ SCOUT FETCH RESULTS: [${repoName}]
263
-
264
- Here are the file contents you requested from the external repository. Use this to inform your work on your primary project.
265
- ${missedWarning}
266
- ${fileContentStr}
267
- `;
268
-
269
- await fs.mkdir(path.join(repoPath, '.eck', 'scouts'), { recursive: true });
270
- const outputPath = path.join(repoPath, '.eck', 'scouts', filename);
271
- await fs.writeFile(outputPath, finalContent, 'utf-8');
272
-
273
- const sizeBytes = Buffer.byteLength(finalContent, 'utf-8');
274
- const sizeStr = sizeBytes < 1024 ? `${sizeBytes} B` : sizeBytes < 1048576 ? `${(sizeBytes / 1024).toFixed(1)} KB` : `${(sizeBytes / 1048576).toFixed(1)} MB`;
275
- const approxTokens = Math.round(finalContent.length / 4);
276
- const tokensStr = approxTokens < 1000 ? `${approxTokens}` : `${(approxTokens / 1000).toFixed(1)}k`;
277
-
278
- console.log(chalk.green(`✅ Fetched ${fetchedCount} files. Saved to: .eck/scouts/${filename}`));
279
- console.log(chalk.gray(` Size: ${sizeStr} | ~${tokensStr} tokens`));
280
- } catch (error) {
281
- console.error(chalk.red(`❌ Fetch failed: ${error.message}`));
282
- }
283
- }
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import micromatch from 'micromatch';
5
+ import isBinaryPath from 'is-binary-path';
6
+ import {
7
+ generateDirectoryTree,
8
+ generateTimestamp,
9
+ readFileWithSizeCheck,
10
+ parseSize,
11
+ loadGitignore,
12
+ getProjectFiles,
13
+ matchesPattern,
14
+ ensureSnapshotsInGitignore,
15
+ readMlModelMetadata
16
+ } from '../../utils/fileUtils.js';
17
+ import { detectProjectType, getProjectSpecificFiltering, getAllDetectedTypes } from '../../utils/projectDetector.js';
18
+ import { loadSetupConfig } from '../../config.js';
19
+ import { getDepthConfig, DEPTH_SCALE } from '../../core/depthConfig.js';
20
+ import { skeletonize } from '../../core/skeletonizer.js';
21
+
22
+ export async function runReconTool(payload) {
23
+ const toolName = payload.name;
24
+ const args = payload.arguments || {};
25
+
26
+ if (toolName === 'eck_scout') {
27
+ const depth = args.depth !== undefined ? parseInt(args.depth, 10) : 0;
28
+ await runScout(depth);
29
+ } else if (toolName === 'eck_fetch') {
30
+ if (!args.patterns || !Array.isArray(args.patterns)) {
31
+ console.log(chalk.red('❌ Error: eck_fetch requires an array of "patterns" in arguments.'));
32
+ return;
33
+ }
34
+ await runFetch(args.patterns);
35
+ }
36
+ }
37
+
38
+ async function runScout(depth = 0) {
39
+ const depthCfg = getDepthConfig(depth);
40
+ const depthInfo = DEPTH_SCALE[depth] || DEPTH_SCALE[0];
41
+ console.log(chalk.blue(`🕵️ Scouting repository (depth ${depth}: ${depthInfo.mode})...`));
42
+ try {
43
+ const repoPath = process.cwd();
44
+ const repoName = path.basename(repoPath);
45
+ const setupConfig = await loadSetupConfig();
46
+ let config = { ...setupConfig.fileFiltering, ...setupConfig.performance };
47
+
48
+ // Apply project-specific filtering (was missing in previous versions)
49
+ const projectDetection = await detectProjectType(repoPath);
50
+ const allTypes = getAllDetectedTypes(projectDetection);
51
+ if (allTypes && allTypes.length > 0) {
52
+ const projectSpecific = await getProjectSpecificFiltering(allTypes);
53
+ config = {
54
+ ...config,
55
+ dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
56
+ filesToIgnore: [...(config.filesToIgnore || []), ...(projectSpecific.filesToIgnore || [])],
57
+ extensionsToIgnore: [...(config.extensionsToIgnore || []), ...(projectSpecific.extensionsToIgnore || [])]
58
+ };
59
+ }
60
+
61
+ // Use a deep maxDepth for scout so the AI can see the full structure
62
+ config.maxDepth = 15;
63
+
64
+ // Use getProjectFiles which respects git tracking natively
65
+ let allFiles = await getProjectFiles(repoPath, config);
66
+ const gitignore = await loadGitignore(repoPath);
67
+
68
+ // Filter binaries, gitignore/eckignore, and file-level ignores
69
+ allFiles = allFiles.filter(f => {
70
+ const normalized = f.replace(/\\/g, '/');
71
+ const mlExt = path.extname(f).toLowerCase();
72
+ const ML_EXTENSIONS = ['.safetensors', '.onnx', '.pt', '.pth', '.h5', '.pb', '.bin', '.ckpt', '.gguf'];
73
+ if (isBinaryPath(f) && !ML_EXTENSIONS.includes(mlExt)) return false;
74
+ if (gitignore.ignores(normalized)) return false;
75
+ if (config.filesToIgnore && matchesPattern(normalized, config.filesToIgnore)) return false;
76
+ return true;
77
+ });
78
+
79
+ const directoryTree = await generateDirectoryTree(repoPath, '', allFiles, 0, config.maxDepth, config);
80
+
81
+ // Build file contents section if depth > 0
82
+ let fileContentSection = '';
83
+ if (!depthCfg.skipContent) {
84
+ const maxFileSize = parseSize(config.maxFileSize || '10MB');
85
+ let processedCount = 0;
86
+
87
+ for (const file of allFiles) {
88
+ try {
89
+ const fullPath = path.join(repoPath, file);
90
+ const mlExt = path.extname(file).toLowerCase();
91
+ const ML_EXTENSIONS = ['.safetensors', '.onnx', '.pt', '.pth', '.h5', '.pb', '.bin', '.ckpt', '.gguf'];
92
+
93
+ let content;
94
+ if (ML_EXTENSIONS.includes(mlExt)) {
95
+ content = await readMlModelMetadata(fullPath);
96
+ } else {
97
+ content = await readFileWithSizeCheck(fullPath, maxFileSize);
98
+ }
99
+
100
+ // Apply skeletonization
101
+ if (depthCfg.skeleton) {
102
+ content = await skeletonize(content, file, { preserveDocs: depthCfg.preserveDocs !== false });
103
+ }
104
+
105
+ // Apply line truncation
106
+ if (depthCfg.maxLinesPerFile && depthCfg.maxLinesPerFile > 0) {
107
+ const lines = content.split('\n');
108
+ if (lines.length > depthCfg.maxLinesPerFile) {
109
+ content = lines.slice(0, depthCfg.maxLinesPerFile).join('\n');
110
+ content += `\n// ... truncated (${lines.length - depthCfg.maxLinesPerFile} more lines)`;
111
+ }
112
+ }
113
+
114
+ fileContentSection += `--- File: /${file} ---\n\n\`\`\`\n${content}\n\`\`\`\n\n`;
115
+ processedCount++;
116
+ } catch (e) {
117
+ fileContentSection += `--- File: /${file} ---\n\n[ERROR: ${e.message}]\n\n`;
118
+ }
119
+ }
120
+
121
+ console.log(chalk.gray(` Processed ${processedCount} files at depth ${depth}`));
122
+ }
123
+
124
+ const timestamp = generateTimestamp();
125
+ const suffix = depth > 0 ? `_d${depth}` : '';
126
+ const filename = `scout_tree_${repoName}_${timestamp}${suffix}.md`;
127
+
128
+ const depthScaleTable = DEPTH_SCALE.map(d => `| ${d.depth} | ${d.mode} | ${d.description} |`).join('\n');
129
+
130
+ let outputContent = `# ⚠️ EXTERNAL REPOSITORY SCOUT: [${repoName}]
131
+
132
+ **CRITICAL INSTRUCTION FOR AI:** You are currently working on your primary project. The data below is strictly for REFERENCE from an external repository named \`${repoName}\`. DO NOT assume the role of architect for this repository. DO NOT attempt to write code for this repository.
133
+
134
+ **Depth:** ${depth} (${depthInfo.mode} — ${depthInfo.description})
135
+
136
+ ## How to request more data from this repository
137
+ Use the \`scout\` command with a higher depth level, or \`fetch\` for specific files:
138
+
139
+ **Scout with depth (0-9):**
140
+ \`\`\`bash
141
+ eck-snapshot scout 5 # skeleton mode
142
+ eck-snapshot scout 9 # full content
143
+ \`\`\`
144
+
145
+ **Fetch specific files (run inside this repo's directory):**
146
+ \`\`\`bash
147
+ cd ${repoPath.replace(/\\/g, '/')}
148
+ eck-snapshot fetch "src/**/*.js" "README.md"
149
+ \`\`\`
150
+
151
+ **⚠️ CRITICAL FETCH RULES:**
152
+ 1. **\`fetch\` only works inside the repo it scans.** You MUST \`cd\` into the correct project directory first.
153
+ 2. **Use RELATIVE paths or glob patterns**, never absolute paths. Files are matched against the repo root.
154
+ 3. **If you need files from multiple repos**, issue SEPARATE fetch commands — one per repo, each with its own \`cd\`.
155
+ 4. **Prefer glob patterns over exact paths** — tree paths are easy to misread:
156
+ - Instead of \`"src/utils/helper.js"\` use \`"**/helper.js"\`
157
+ - Use \`"**/<filename>"\` to find a file anywhere in the tree.
158
+
159
+ **Depth scale:**
160
+ | Depth | Mode | Description |
161
+ |-------|------|-------------|
162
+ ${depthScaleTable}
163
+
164
+ ## Directory Structure
165
+ \`\`\`text
166
+ ${directoryTree}
167
+ \`\`\`
168
+ `;
169
+
170
+ if (fileContentSection) {
171
+ outputContent += `\n## File Contents (depth ${depth}: ${depthInfo.mode})\n\n${fileContentSection}`;
172
+ }
173
+
174
+ await fs.mkdir(path.join(repoPath, '.eck', 'scouts'), { recursive: true });
175
+ await ensureSnapshotsInGitignore(repoPath);
176
+ const outputPath = path.join(repoPath, '.eck', 'scouts', filename);
177
+ await fs.writeFile(outputPath, outputContent, 'utf-8');
178
+
179
+ const sizeBytes = Buffer.byteLength(outputContent, 'utf-8');
180
+ const sizeStr = sizeBytes < 1024 ? `${sizeBytes} B` : sizeBytes < 1048576 ? `${(sizeBytes / 1024).toFixed(1)} KB` : `${(sizeBytes / 1048576).toFixed(1)} MB`;
181
+ const approxTokens = Math.round(outputContent.length / 4);
182
+ const tokensStr = approxTokens < 1000 ? `${approxTokens}` : `${(approxTokens / 1000).toFixed(1)}k`;
183
+
184
+ console.log(chalk.green(`✅ Scout complete. Saved to: .eck/scouts/${filename}`));
185
+ console.log(chalk.gray(` Size: ${sizeStr} | ~${tokensStr} tokens`));
186
+ } catch (error) {
187
+ console.error(chalk.red(`❌ Scout failed: ${error.message}`));
188
+ }
189
+ }
190
+
191
+ async function runFetch(patterns) {
192
+ console.log(chalk.blue(`🚚 Fetching files matching patterns: ${patterns.join(', ')}...`));
193
+ try {
194
+ const repoPath = process.cwd();
195
+ const repoName = path.basename(repoPath);
196
+ const repoPathNorm = repoPath.replace(/\\/g, '/').replace(/\/$/, '') + '/';
197
+ const setupConfig = await loadSetupConfig();
198
+ let config = { ...setupConfig.fileFiltering, ...setupConfig.performance };
199
+
200
+ // Apply project-specific filtering
201
+ const projectDetection = await detectProjectType(repoPath);
202
+ const allTypes = getAllDetectedTypes(projectDetection);
203
+ if (allTypes && allTypes.length > 0) {
204
+ const projectSpecific = await getProjectSpecificFiltering(allTypes);
205
+ config = {
206
+ ...config,
207
+ dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
208
+ filesToIgnore: [...(config.filesToIgnore || []), ...(projectSpecific.filesToIgnore || [])],
209
+ extensionsToIgnore: [...(config.extensionsToIgnore || []), ...(projectSpecific.extensionsToIgnore || [])]
210
+ };
211
+ }
212
+
213
+ let allFiles = await getProjectFiles(repoPath, config);
214
+ const gitignore = await loadGitignore(repoPath);
215
+
216
+ allFiles = allFiles.filter(f => {
217
+ const normalized = f.replace(/\\/g, '/');
218
+ const mlExt = path.extname(f).toLowerCase();
219
+ const ML_EXTENSIONS = ['.safetensors', '.onnx', '.pt', '.pth', '.h5', '.pb', '.bin', '.ckpt', '.gguf'];
220
+ if (isBinaryPath(f) && !ML_EXTENSIONS.includes(mlExt)) return false;
221
+ if (gitignore.ignores(normalized)) return false;
222
+ if (config.filesToIgnore && matchesPattern(normalized, config.filesToIgnore)) return false;
223
+ return true;
224
+ });
225
+
226
+ // Normalize patterns: strip absolute cwd prefix, convert backslashes,
227
+ // and auto-wrap bare filenames with **/ for convenience
228
+ const normalizedPatterns = patterns.map(p => {
229
+ let norm = p.replace(/\\/g, '/');
230
+ // Strip absolute path prefix matching cwd (case-insensitive on Windows)
231
+ if (norm.toLowerCase().startsWith(repoPathNorm.toLowerCase())) {
232
+ norm = norm.slice(repoPathNorm.length);
233
+ }
234
+ // If it looks like an absolute path from another project, extract just the filename
235
+ if (path.isAbsolute(norm) || /^[A-Za-z]:\//.test(norm)) {
236
+ const basename = path.basename(norm);
237
+ console.log(chalk.yellow(` ⚠️ Cross-repo absolute path detected, using: **/${basename}`));
238
+ norm = `**/${basename}`;
239
+ }
240
+ // If it's a plain filename with no glob chars and no path separators, wrap it
241
+ if (!norm.includes('/') && !norm.includes('*') && !norm.includes('?')) {
242
+ norm = `**/${norm}`;
243
+ }
244
+ return norm;
245
+ });
246
+
247
+ const matchedFiles = micromatch(allFiles, normalizedPatterns);
248
+
249
+ if (matchedFiles.length === 0) {
250
+ console.log(chalk.yellow('⚠️ No files matched the requested patterns.'));
251
+ return;
252
+ }
253
+
254
+ let fileContentStr = '';
255
+ let fetchedCount = 0;
256
+ const maxFileSize = parseSize(config.maxFileSize || '10MB');
257
+
258
+ for (const file of matchedFiles) {
259
+ try {
260
+ const fullPath = path.join(repoPath, file);
261
+ const mlExt = path.extname(file).toLowerCase();
262
+ const ML_EXTENSIONS = ['.safetensors', '.onnx', '.pt', '.pth', '.h5', '.pb', '.bin', '.ckpt', '.gguf'];
263
+
264
+ let content;
265
+ if (ML_EXTENSIONS.includes(mlExt)) {
266
+ content = await readMlModelMetadata(fullPath);
267
+ } else {
268
+ content = await readFileWithSizeCheck(fullPath, maxFileSize);
269
+ }
270
+
271
+ fileContentStr += `--- File: /${file} ---\n\n\`\`\`\n${content}\n\`\`\`\n\n`;
272
+ fetchedCount++;
273
+ } catch (e) {
274
+ fileContentStr += `--- File: /${file} ---\n\n[ERROR: ${e.message}]\n\n`;
275
+ }
276
+ }
277
+
278
+ const timestamp = generateTimestamp();
279
+ const filename = `scout_data_${repoName}_${timestamp}.md`;
280
+
281
+ // Check how many patterns actually matched at least one file
282
+ const matchedPatternCount = normalizedPatterns.filter(p => micromatch(allFiles, [p]).length > 0).length;
283
+ const missedCount = normalizedPatterns.length - matchedPatternCount;
284
+ const missedWarning = missedCount > 0 ? `\n**⚠️ ${missedCount} of ${patterns.length} requested patterns returned no results.** You likely misread the directory tree. Re-check the tree carefully and retry with glob patterns like \`"**/<filename>"\` to match files regardless of nesting depth.\n` : '';
285
+
286
+ const finalContent = `# ⚠️ SCOUT FETCH RESULTS: [${repoName}]
287
+
288
+ Here are the file contents you requested from the external repository. Use this to inform your work on your primary project.
289
+ ${missedWarning}
290
+ ${fileContentStr}
291
+ `;
292
+
293
+ await fs.mkdir(path.join(repoPath, '.eck', 'scouts'), { recursive: true });
294
+ await ensureSnapshotsInGitignore(repoPath);
295
+ const outputPath = path.join(repoPath, '.eck', 'scouts', filename);
296
+ await fs.writeFile(outputPath, finalContent, 'utf-8');
297
+
298
+ const sizeBytes = Buffer.byteLength(finalContent, 'utf-8');
299
+ const sizeStr = sizeBytes < 1024 ? `${sizeBytes} B` : sizeBytes < 1048576 ? `${(sizeBytes / 1024).toFixed(1)} KB` : `${(sizeBytes / 1048576).toFixed(1)} MB`;
300
+ const approxTokens = Math.round(finalContent.length / 4);
301
+ const tokensStr = approxTokens < 1000 ? `${approxTokens}` : `${(approxTokens / 1000).toFixed(1)}k`;
302
+
303
+ console.log(chalk.green(`✅ Fetched ${fetchedCount} files. Saved to: .eck/scouts/${filename}`));
304
+ console.log(chalk.gray(` Size: ${sizeStr} | ~${tokensStr} tokens`));
305
+ } catch (error) {
306
+ console.error(chalk.red(`❌ Fetch failed: ${error.message}`));
307
+ }
308
+ }
@@ -6,6 +6,7 @@ import ora from 'ora';
6
6
  import os from 'os';
7
7
  import { execa } from 'execa';
8
8
  import { fileURLToPath } from 'url';
9
+ import { ensureSnapshotsInGitignore } from '../../utils/fileUtils.js';
9
10
 
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = path.dirname(__filename);
@@ -218,6 +219,7 @@ async function setupForClaude(packageRoot, eckCorePath, glmZaiPath, options, pro
218
219
 
219
220
  try {
220
221
  await fs.mkdir(path.dirname(localConfigPath), { recursive: true });
222
+ await ensureSnapshotsInGitignore(projectRoot);
221
223
  await fs.writeFile(localConfigPath, JSON.stringify(localConfig, null, 2));
222
224
  spinner.succeed(`Local config updated: ${chalk.cyan(localConfigPath)}`);
223
225
  } catch (e) {