@xelth/eck-snapshot 5.9.0 → 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.
Files changed (37) hide show
  1. package/README.md +321 -190
  2. package/index.js +1 -1
  3. package/package.json +15 -2
  4. package/scripts/mcp-eck-core.js +143 -13
  5. package/setup.json +119 -81
  6. package/src/cli/cli.js +256 -385
  7. package/src/cli/commands/createSnapshot.js +391 -175
  8. package/src/cli/commands/recon.js +308 -0
  9. package/src/cli/commands/setupMcp.js +280 -19
  10. package/src/cli/commands/trainTokens.js +42 -32
  11. package/src/cli/commands/updateSnapshot.js +136 -43
  12. package/src/core/depthConfig.js +54 -0
  13. package/src/core/skeletonizer.js +280 -21
  14. package/src/templates/architect-prompt.template.md +34 -0
  15. package/src/templates/multiAgent.md +68 -15
  16. package/src/templates/opencode/coder.template.md +53 -17
  17. package/src/templates/opencode/junior-architect.template.md +54 -15
  18. package/src/templates/skeleton-instruction.md +1 -1
  19. package/src/templates/update-prompt.template.md +2 -0
  20. package/src/utils/aiHeader.js +57 -27
  21. package/src/utils/claudeMdGenerator.js +182 -88
  22. package/src/utils/fileUtils.js +217 -149
  23. package/src/utils/gitUtils.js +12 -8
  24. package/src/utils/opencodeAgentsGenerator.js +8 -2
  25. package/src/utils/projectDetector.js +66 -21
  26. package/src/utils/tokenEstimator.js +11 -7
  27. package/src/cli/commands/consilium.js +0 -86
  28. package/src/cli/commands/detectProfiles.js +0 -98
  29. package/src/cli/commands/envSync.js +0 -319
  30. package/src/cli/commands/generateProfileGuide.js +0 -144
  31. package/src/cli/commands/pruneSnapshot.js +0 -106
  32. package/src/cli/commands/restoreSnapshot.js +0 -173
  33. package/src/cli/commands/setupGemini.js +0 -149
  34. package/src/cli/commands/setupGemini.test.js +0 -115
  35. package/src/cli/commands/showFile.js +0 -39
  36. package/src/services/claudeCliService.js +0 -626
  37. package/src/services/claudeCliService.test.js +0 -267
@@ -0,0 +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
+ 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
+ }