@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.
- package/README.md +321 -190
- package/index.js +1 -1
- package/package.json +15 -2
- package/scripts/mcp-eck-core.js +143 -13
- package/setup.json +119 -81
- package/src/cli/cli.js +256 -385
- package/src/cli/commands/createSnapshot.js +391 -175
- package/src/cli/commands/recon.js +308 -0
- package/src/cli/commands/setupMcp.js +280 -19
- package/src/cli/commands/trainTokens.js +42 -32
- package/src/cli/commands/updateSnapshot.js +136 -43
- package/src/core/depthConfig.js +54 -0
- package/src/core/skeletonizer.js +280 -21
- package/src/templates/architect-prompt.template.md +34 -0
- package/src/templates/multiAgent.md +68 -15
- package/src/templates/opencode/coder.template.md +53 -17
- package/src/templates/opencode/junior-architect.template.md +54 -15
- package/src/templates/skeleton-instruction.md +1 -1
- package/src/templates/update-prompt.template.md +2 -0
- package/src/utils/aiHeader.js +57 -27
- package/src/utils/claudeMdGenerator.js +182 -88
- package/src/utils/fileUtils.js +217 -149
- package/src/utils/gitUtils.js +12 -8
- package/src/utils/opencodeAgentsGenerator.js +8 -2
- package/src/utils/projectDetector.js +66 -21
- package/src/utils/tokenEstimator.js +11 -7
- package/src/cli/commands/consilium.js +0 -86
- package/src/cli/commands/detectProfiles.js +0 -98
- package/src/cli/commands/envSync.js +0 -319
- package/src/cli/commands/generateProfileGuide.js +0 -144
- package/src/cli/commands/pruneSnapshot.js +0 -106
- package/src/cli/commands/restoreSnapshot.js +0 -173
- package/src/cli/commands/setupGemini.js +0 -149
- package/src/cli/commands/setupGemini.test.js +0 -115
- package/src/cli/commands/showFile.js +0 -39
- package/src/services/claudeCliService.js +0 -626
- package/src/services/claudeCliService.test.js +0 -267
|
@@ -15,16 +15,18 @@ import {
|
|
|
15
15
|
scanDirectoryRecursively, loadGitignore, readFileWithSizeCheck,
|
|
16
16
|
generateDirectoryTree, loadConfig, displayProjectInfo, loadProjectEckManifest,
|
|
17
17
|
ensureSnapshotsInGitignore, initializeEckManifest, generateTimestamp,
|
|
18
|
-
getShortRepoName, SecretScanner
|
|
18
|
+
getShortRepoName, SecretScanner, getProjectFiles, readMlModelMetadata
|
|
19
19
|
} from '../../utils/fileUtils.js';
|
|
20
|
-
import { detectProjectType, getProjectSpecificFiltering } from '../../utils/projectDetector.js';
|
|
20
|
+
import { detectProjectType, getProjectSpecificFiltering, getAllDetectedTypes } from '../../utils/projectDetector.js';
|
|
21
21
|
import { estimateTokensWithPolynomial, generateTrainingCommand } from '../../utils/tokenEstimator.js';
|
|
22
22
|
import { loadSetupConfig, getProfile } from '../../config.js';
|
|
23
23
|
import { applyProfileFilter } from '../../utils/fileUtils.js';
|
|
24
24
|
import { saveGitAnchor } from '../../utils/gitUtils.js';
|
|
25
25
|
import { skeletonize } from '../../core/skeletonizer.js';
|
|
26
|
+
import { getDepthConfig } from '../../core/depthConfig.js';
|
|
26
27
|
import { updateClaudeMd } from '../../utils/claudeMdGenerator.js';
|
|
27
28
|
import { generateOpenCodeAgents } from '../../utils/opencodeAgentsGenerator.js';
|
|
29
|
+
import { ensureProjectMcpConfig, ensureProjectOpenCodeConfig, ensureProjectCodexConfig } from './setupMcp.js';
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* Creates dynamic project context based on detection results
|
|
@@ -199,47 +201,7 @@ function generateClaudeMdContent(confidentialFiles, repoPath) {
|
|
|
199
201
|
return content.join('\n');
|
|
200
202
|
}
|
|
201
203
|
|
|
202
|
-
//
|
|
203
|
-
const GLOBAL_HARD_IGNORE_DIRS = ['node_modules', '.git', '.idea', '.vscode'];
|
|
204
|
-
const GLOBAL_HARD_IGNORE_FILES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'go.sum'];
|
|
205
|
-
|
|
206
|
-
function shouldHardIgnore(entryName, isDirectory) {
|
|
207
|
-
if (isDirectory) {
|
|
208
|
-
return GLOBAL_HARD_IGNORE_DIRS.includes(entryName);
|
|
209
|
-
}
|
|
210
|
-
return GLOBAL_HARD_IGNORE_FILES.includes(entryName);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async function getProjectFiles(projectPath, config) {
|
|
214
|
-
const isGitRepo = await checkGitRepository(projectPath);
|
|
215
|
-
if (isGitRepo) {
|
|
216
|
-
const { stdout } = await execa('git', ['ls-files'], { cwd: projectPath });
|
|
217
|
-
const gitFiles = stdout.split('\n').filter(Boolean);
|
|
218
|
-
|
|
219
|
-
// Build effective dirsToIgnore list (global hard-ignores + config)
|
|
220
|
-
const dirsToIgnore = [...GLOBAL_HARD_IGNORE_DIRS, ...(config.dirsToIgnore || []).map(d => d.replace(/\/$/, ''))];
|
|
221
|
-
const filesToIgnore = [...GLOBAL_HARD_IGNORE_FILES, ...(config.filesToIgnore || [])];
|
|
222
|
-
const extensionsToIgnore = config.extensionsToIgnore || [];
|
|
223
|
-
|
|
224
|
-
const filteredFiles = gitFiles.filter(file => {
|
|
225
|
-
if (isHiddenPath(file)) return false;
|
|
226
|
-
const fileName = file.split('/').pop();
|
|
227
|
-
const fileExt = path.extname(fileName);
|
|
228
|
-
// Check if any parent directory should be ignored
|
|
229
|
-
const pathParts = file.split('/');
|
|
230
|
-
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
231
|
-
if (dirsToIgnore.includes(pathParts[i])) return false;
|
|
232
|
-
}
|
|
233
|
-
// Check filesToIgnore
|
|
234
|
-
if (filesToIgnore.includes(fileName)) return false;
|
|
235
|
-
// Check extensionsToIgnore
|
|
236
|
-
if (fileExt && extensionsToIgnore.includes(fileExt)) return false;
|
|
237
|
-
return true;
|
|
238
|
-
});
|
|
239
|
-
return filteredFiles;
|
|
240
|
-
}
|
|
241
|
-
return scanDirectoryRecursively(projectPath, config);
|
|
242
|
-
}
|
|
204
|
+
// getProjectFiles is now imported from fileUtils.js
|
|
243
205
|
|
|
244
206
|
async function getGitCommitHash(projectPath) {
|
|
245
207
|
try {
|
|
@@ -254,14 +216,14 @@ async function getGitCommitHash(projectPath) {
|
|
|
254
216
|
return null;
|
|
255
217
|
}
|
|
256
218
|
|
|
257
|
-
async function estimateProjectTokens(projectPath, config,
|
|
219
|
+
async function estimateProjectTokens(projectPath, config, projectTypes = null) {
|
|
258
220
|
// Get project-specific filtering if not provided
|
|
259
|
-
if (!
|
|
221
|
+
if (!projectTypes) {
|
|
260
222
|
const detection = await detectProjectType(projectPath);
|
|
261
|
-
|
|
223
|
+
projectTypes = getAllDetectedTypes(detection);
|
|
262
224
|
}
|
|
263
225
|
|
|
264
|
-
const projectSpecific = await getProjectSpecificFiltering(
|
|
226
|
+
const projectSpecific = await getProjectSpecificFiltering(projectTypes);
|
|
265
227
|
|
|
266
228
|
// Merge project-specific filters with global config (same as in scanDirectoryRecursively)
|
|
267
229
|
const effectiveConfig = {
|
|
@@ -314,15 +276,17 @@ async function estimateProjectTokens(projectPath, config, projectType = null) {
|
|
|
314
276
|
}
|
|
315
277
|
|
|
316
278
|
// Use adaptive polynomial estimation
|
|
317
|
-
|
|
279
|
+
// Token estimation uses the primary type for coefficient lookup
|
|
280
|
+
const primaryType = Array.isArray(projectTypes) ? projectTypes[0] : projectTypes;
|
|
281
|
+
const estimatedTokens = await estimateTokensWithPolynomial(primaryType, totalSize);
|
|
318
282
|
|
|
319
283
|
return { estimatedTokens, totalSize, includedFiles };
|
|
320
284
|
}
|
|
321
285
|
|
|
322
|
-
async function processProjectFiles(repoPath, options, config,
|
|
323
|
-
// Merge project-specific filtering rules (
|
|
324
|
-
if (
|
|
325
|
-
const projectSpecific = await getProjectSpecificFiltering(
|
|
286
|
+
async function processProjectFiles(repoPath, options, config, projectTypes = null) {
|
|
287
|
+
// Merge project-specific filtering rules for ALL detected types (polyglot monorepo support)
|
|
288
|
+
if (projectTypes) {
|
|
289
|
+
const projectSpecific = await getProjectSpecificFiltering(projectTypes);
|
|
326
290
|
config = {
|
|
327
291
|
...config,
|
|
328
292
|
dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
|
|
@@ -371,7 +335,7 @@ async function processProjectFiles(repoPath, options, config, projectType = null
|
|
|
371
335
|
|
|
372
336
|
if (filterResult.notFoundProfiles.length > 0) {
|
|
373
337
|
errorMsg += `\n\nā Profile(s) not found: ${filterResult.notFoundProfiles.join(', ')}`;
|
|
374
|
-
errorMsg += `\n\nš” Run 'eck-snapshot --profile' to see available profiles.`;
|
|
338
|
+
errorMsg += `\n\nš” Run 'eck-snapshot snapshot --profile' to see available profiles.`;
|
|
375
339
|
errorMsg += `\n Or run 'eck-snapshot generate-profile-guide' to create profiles.`;
|
|
376
340
|
} else if (filterResult.foundProfiles.length > 0) {
|
|
377
341
|
errorMsg += `\n\nā Profile(s) found: ${filterResult.foundProfiles.join(', ')}`;
|
|
@@ -430,8 +394,12 @@ async function processProjectFiles(repoPath, options, config, projectType = null
|
|
|
430
394
|
return null;
|
|
431
395
|
}
|
|
432
396
|
|
|
433
|
-
|
|
434
|
-
|
|
397
|
+
const mlExt = path.extname(filePath).toLowerCase();
|
|
398
|
+
const ML_EXTENSIONS = ['.safetensors', '.onnx', '.pt', '.pth', '.h5', '.pb', '.bin', '.ckpt', '.gguf'];
|
|
399
|
+
const isMlModel = ML_EXTENSIONS.includes(mlExt);
|
|
400
|
+
|
|
401
|
+
// Check if binary file (bypass if it's an ML model we want to peek into)
|
|
402
|
+
if (isBinaryPath(filePath) && !isMlModel) {
|
|
435
403
|
stats.binaryFiles++;
|
|
436
404
|
trackSkippedFile(normalizedPath, 'Binary files');
|
|
437
405
|
return null;
|
|
@@ -457,13 +425,18 @@ async function processProjectFiles(repoPath, options, config, projectType = null
|
|
|
457
425
|
stats.totalSize += fileStats.size;
|
|
458
426
|
|
|
459
427
|
const maxFileSize = parseSize(config.maxFileSize);
|
|
460
|
-
|
|
461
|
-
stats.oversizedFiles++;
|
|
462
|
-
trackSkippedFile(normalizedPath, `File too large (${formatSize(fileStats.size)} > ${formatSize(maxFileSize)})`);
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
428
|
+
let content;
|
|
465
429
|
|
|
466
|
-
|
|
430
|
+
if (isMlModel) {
|
|
431
|
+
content = await readMlModelMetadata(fullPath);
|
|
432
|
+
} else {
|
|
433
|
+
if (fileStats.size > maxFileSize) {
|
|
434
|
+
stats.oversizedFiles++;
|
|
435
|
+
trackSkippedFile(normalizedPath, `File too large (${formatSize(fileStats.size)} > ${formatSize(maxFileSize)})`);
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
content = await readFileWithSizeCheck(fullPath, maxFileSize);
|
|
439
|
+
}
|
|
467
440
|
|
|
468
441
|
// Security scan for secrets
|
|
469
442
|
if (config.security?.scanForSecrets !== false) {
|
|
@@ -476,14 +449,13 @@ async function processProjectFiles(repoPath, options, config, projectType = null
|
|
|
476
449
|
}
|
|
477
450
|
|
|
478
451
|
stats.includedFiles++;
|
|
479
|
-
stats.processedSize += fileStats.size;
|
|
480
452
|
|
|
481
453
|
// Apply skeletonization if enabled
|
|
482
454
|
if (options.skeleton) {
|
|
483
455
|
// Check if file should be focused (kept full)
|
|
484
456
|
const isFocused = options.focus && micromatch.isMatch(normalizedPath, options.focus);
|
|
485
457
|
if (!isFocused) {
|
|
486
|
-
content = await skeletonize(content, normalizedPath);
|
|
458
|
+
content = await skeletonize(content, normalizedPath, { preserveDocs: options.preserveDocs !== false });
|
|
487
459
|
}
|
|
488
460
|
}
|
|
489
461
|
|
|
@@ -498,10 +470,14 @@ async function processProjectFiles(repoPath, options, config, projectType = null
|
|
|
498
470
|
}
|
|
499
471
|
}
|
|
500
472
|
|
|
473
|
+
const formattedContent = `--- File: /${normalizedPath} ---\n\n${outputBody}\n\n`;
|
|
474
|
+
const finalSize = Buffer.byteLength(formattedContent, 'utf-8');
|
|
475
|
+
stats.processedSize += finalSize;
|
|
476
|
+
|
|
501
477
|
return {
|
|
502
|
-
content:
|
|
478
|
+
content: formattedContent,
|
|
503
479
|
path: normalizedPath,
|
|
504
|
-
size:
|
|
480
|
+
size: finalSize
|
|
505
481
|
};
|
|
506
482
|
} catch (error) {
|
|
507
483
|
stats.errors.push(`${normalizedPath}: ${error.message}`);
|
|
@@ -531,7 +507,65 @@ async function processProjectFiles(repoPath, options, config, projectType = null
|
|
|
531
507
|
}
|
|
532
508
|
}
|
|
533
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Groups files by directory and packs them into chunks of a given maximum size.
|
|
512
|
+
* Preserves directory locality for better RAG retrieval in NotebookLM.
|
|
513
|
+
*/
|
|
514
|
+
function packFilesForNotebookLM(successfulFileObjects, maxChunkSizeBytes = 2.5 * 1024 * 1024) {
|
|
515
|
+
const dirGroups = new Map();
|
|
516
|
+
|
|
517
|
+
for (const fileObj of successfulFileObjects) {
|
|
518
|
+
const dir = fileObj.path.includes('/') ? fileObj.path.substring(0, fileObj.path.lastIndexOf('/')) : './';
|
|
519
|
+
if (!dirGroups.has(dir)) dirGroups.set(dir, { size: 0, files: [] });
|
|
520
|
+
const group = dirGroups.get(dir);
|
|
521
|
+
group.files.push(fileObj);
|
|
522
|
+
group.size += fileObj.size;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const sortedDirs = Array.from(dirGroups.keys()).sort();
|
|
526
|
+
const chunks = [];
|
|
527
|
+
let currentChunk = { size: 0, contentArray: [] };
|
|
528
|
+
|
|
529
|
+
for (const dir of sortedDirs) {
|
|
530
|
+
const group = dirGroups.get(dir);
|
|
531
|
+
|
|
532
|
+
if (group.size > maxChunkSizeBytes) {
|
|
533
|
+
// Directory too large ā pack files individually
|
|
534
|
+
for (const fileObj of group.files) {
|
|
535
|
+
if (currentChunk.size + fileObj.size > maxChunkSizeBytes && currentChunk.contentArray.length > 0) {
|
|
536
|
+
chunks.push(currentChunk);
|
|
537
|
+
currentChunk = { size: 0, contentArray: [] };
|
|
538
|
+
}
|
|
539
|
+
currentChunk.contentArray.push(fileObj.content);
|
|
540
|
+
currentChunk.size += fileObj.size;
|
|
541
|
+
}
|
|
542
|
+
} else if (currentChunk.size + group.size <= maxChunkSizeBytes) {
|
|
543
|
+
// Directory fits in current chunk
|
|
544
|
+
for (const f of group.files) currentChunk.contentArray.push(f.content);
|
|
545
|
+
currentChunk.size += group.size;
|
|
546
|
+
} else {
|
|
547
|
+
// Start a new chunk
|
|
548
|
+
chunks.push(currentChunk);
|
|
549
|
+
currentChunk = { size: 0, contentArray: [] };
|
|
550
|
+
for (const f of group.files) currentChunk.contentArray.push(f.content);
|
|
551
|
+
currentChunk.size += group.size;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (currentChunk.contentArray.length > 0) chunks.push(currentChunk);
|
|
556
|
+
return chunks;
|
|
557
|
+
}
|
|
558
|
+
|
|
534
559
|
export async function createRepoSnapshot(repoPath, options) {
|
|
560
|
+
// Handle linked/scout project depth settings before processing
|
|
561
|
+
if (options.isLinkedProject || (options.notebooklm && options.linkDepth !== undefined)) {
|
|
562
|
+
const depthCfg = getDepthConfig(options.linkDepth !== undefined ? options.linkDepth : 0);
|
|
563
|
+
if (depthCfg.skipContent) options.skipContent = true;
|
|
564
|
+
if (depthCfg.skeleton !== undefined) options.skeleton = depthCfg.skeleton;
|
|
565
|
+
if (depthCfg.preserveDocs !== undefined) options.preserveDocs = depthCfg.preserveDocs;
|
|
566
|
+
if (depthCfg.maxLinesPerFile !== undefined) options.maxLinesPerFile = depthCfg.maxLinesPerFile;
|
|
567
|
+
}
|
|
568
|
+
|
|
535
569
|
const spinner = ora('Analyzing project...').start();
|
|
536
570
|
try {
|
|
537
571
|
// Ensure snapshots/ is in .gitignore to prevent accidental commits
|
|
@@ -570,7 +604,7 @@ export async function createRepoSnapshot(repoPath, options) {
|
|
|
570
604
|
// Generate ready-to-copy command with all profiles
|
|
571
605
|
const allProfilesString = profileNames.join(',');
|
|
572
606
|
console.log(chalk.cyan('š Ready-to-Copy Command (all profiles):'));
|
|
573
|
-
console.log(chalk.bold(`\neck-snapshot
|
|
607
|
+
console.log(chalk.bold(`\neck-snapshot '{"name": "eck_snapshot", "arguments": {"profile": "${allProfilesString}"}}'\n`));
|
|
574
608
|
console.log(chalk.gray('š” Tip: Copy the command above and remove profiles you don\'t need'));
|
|
575
609
|
process.exit(0);
|
|
576
610
|
} catch (error) {
|
|
@@ -629,19 +663,19 @@ export async function createRepoSnapshot(repoPath, options) {
|
|
|
629
663
|
...options // Command-line options have the final say
|
|
630
664
|
};
|
|
631
665
|
|
|
632
|
-
// Detect architect modes
|
|
633
|
-
const isJas = options.jas;
|
|
634
|
-
const isJao = options.jao;
|
|
635
|
-
const isJaz = options.jaz;
|
|
636
|
-
|
|
637
|
-
// If NOT in Junior Architect mode, hide JA-specific documentation to prevent context pollution
|
|
638
|
-
if (!options.withJa && !isJas && !isJao && !isJaz) {
|
|
639
|
-
if (!config.filesToIgnore) config.filesToIgnore = [];
|
|
640
|
-
config.filesToIgnore.push(
|
|
641
|
-
'COMMANDS_REFERENCE.md',
|
|
642
|
-
'codex_delegation_snapshot.md'
|
|
643
|
-
);
|
|
644
|
-
}
|
|
666
|
+
// Detect architect modes
|
|
667
|
+
const isJas = options.jas;
|
|
668
|
+
const isJao = options.jao;
|
|
669
|
+
const isJaz = options.jaz;
|
|
670
|
+
|
|
671
|
+
// If NOT in Junior Architect mode, hide JA-specific documentation to prevent context pollution
|
|
672
|
+
if (!options.withJa && !isJas && !isJao && !isJaz) {
|
|
673
|
+
if (!config.filesToIgnore) config.filesToIgnore = [];
|
|
674
|
+
config.filesToIgnore.push(
|
|
675
|
+
'COMMANDS_REFERENCE.md',
|
|
676
|
+
'codex_delegation_snapshot.md'
|
|
677
|
+
);
|
|
678
|
+
}
|
|
645
679
|
|
|
646
680
|
// Apply defaults for options that may not be provided via command line
|
|
647
681
|
if (!config.output) {
|
|
@@ -658,7 +692,8 @@ export async function createRepoSnapshot(repoPath, options) {
|
|
|
658
692
|
config.includeHidden = setupConfig.fileFiltering?.includeHidden ?? false;
|
|
659
693
|
}
|
|
660
694
|
|
|
661
|
-
const
|
|
695
|
+
const allTypesForEstimation = getAllDetectedTypes(projectDetection);
|
|
696
|
+
const estimation = await estimateProjectTokens(repoPath, config, allTypesForEstimation);
|
|
662
697
|
spinner.info(`Estimated project size: ~${Math.round(estimation.estimatedTokens).toLocaleString()} tokens.`);
|
|
663
698
|
|
|
664
699
|
spinner.succeed('Creating snapshots...');
|
|
@@ -669,7 +704,8 @@ export async function createRepoSnapshot(repoPath, options) {
|
|
|
669
704
|
|
|
670
705
|
let stats, contentArray, successfulFileObjects, allFiles, processedRepoPath;
|
|
671
706
|
|
|
672
|
-
const
|
|
707
|
+
const allTypes = getAllDetectedTypes(projectDetection);
|
|
708
|
+
const result = await processProjectFiles(repoPath, options, config, allTypes);
|
|
673
709
|
stats = result.stats;
|
|
674
710
|
contentArray = result.contentArray;
|
|
675
711
|
successfulFileObjects = result.successfulFileObjects;
|
|
@@ -717,86 +753,230 @@ export async function createRepoSnapshot(repoPath, options) {
|
|
|
717
753
|
let architectFilePath = null;
|
|
718
754
|
let jaFilePath = null;
|
|
719
755
|
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
//
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
//
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
//
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
}
|
|
799
|
-
|
|
756
|
+
// --- NotebookLM Chunked Export (Brain + Body) ---
|
|
757
|
+
if (options.notebooklm) {
|
|
758
|
+
const mode = options.notebooklm; // 'hybrid', 'link', 'scout', 'architect'
|
|
759
|
+
console.log(chalk.blue(`\nš Packing project for NotebookLM (${mode.toUpperCase()} Mode)...`));
|
|
760
|
+
|
|
761
|
+
// If options.skipContent is true (Depth 0), chunks will be empty, only part0_BRAIN is generated
|
|
762
|
+
const chunks = options.skipContent ? [] : packFilesForNotebookLM(successfulFileObjects);
|
|
763
|
+
const shortRepoName = getShortRepoName(repoName);
|
|
764
|
+
const absPath = processedRepoPath.replace(/\\/g, '/');
|
|
765
|
+
const filePrefix = mode; // e.g. hybrid, link, scout
|
|
766
|
+
|
|
767
|
+
// Clean up old notebooklm chunks for this prefix
|
|
768
|
+
try {
|
|
769
|
+
const existingFiles = await fs.readdir(outputPath);
|
|
770
|
+
for (const file of existingFiles) {
|
|
771
|
+
if (file.includes(`_${filePrefix}_part`)) {
|
|
772
|
+
await fs.unlink(path.join(outputPath, file));
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
} catch (e) { /* ignore */ }
|
|
776
|
+
|
|
777
|
+
let systemPrompt = '';
|
|
778
|
+
if (mode === 'architect') {
|
|
779
|
+
systemPrompt += `You are the Senior Software Architect for this project.\n`;
|
|
780
|
+
systemPrompt += `Analyze the provided source documents to solve complex structural problems, design new features, and propose refactoring strategies.\n\n`;
|
|
781
|
+
systemPrompt += `RULES FOR CODE GENERATION:\n`;
|
|
782
|
+
systemPrompt += `1. Output precise code modifications using Eck-Protocol v2.\n`;
|
|
783
|
+
systemPrompt += `2. Wrap the entire response in quadruple backticks (\`\`\`\`).\n`;
|
|
784
|
+
systemPrompt += `3. Use \`<file path="..." action="replace">\` XML tags for files.\n`;
|
|
785
|
+
systemPrompt += `4. Always consult the BRAIN document (part 0) before answering to understand project constraints.\n`;
|
|
786
|
+
systemPrompt += `5. ANTI-CONTAMINATION: Verify that any new file uploads belong to this project context. If not, WARN the user and stop.\n`;
|
|
787
|
+
} else if (mode === 'hybrid') {
|
|
788
|
+
systemPrompt += `You are a Senior Software Architect managing a multi-repository ecosystem.\n\n`;
|
|
789
|
+
systemPrompt += `YOUR DATA SOURCES:\n`;
|
|
790
|
+
systemPrompt += `1. Primary Project (part0_BRAIN, part1, etc.): The main repository you are actively developing.\n`;
|
|
791
|
+
systemPrompt += `2. Linked Projects (link_part*): Companion repositories (e.g., backend + mobile). You CAN modify code here if cross-project sync is needed.\n`;
|
|
792
|
+
systemPrompt += `3. Scouted Projects (scout_part*): External repositories loaded STRICTLY for read-only reference. NEVER write code for scouted projects.\n\n`;
|
|
793
|
+
systemPrompt += `RULES:\n`;
|
|
794
|
+
systemPrompt += `- Use Eck-Protocol v2 format (quadruple backticks \`\`\`\`, <file> tags) for ALL code generation.\n`;
|
|
795
|
+
systemPrompt += `- If modifying a Linked Project, clearly specify the absolute project path in the <file> tag.\n`;
|
|
796
|
+
systemPrompt += `- If you need missing file contents from linked/scouted projects (because they were truncated/skeletonized), output bash commands to fetch them: \`cd /path/to/project && eck-snapshot fetch "**/api.rs"\`.\n`;
|
|
797
|
+
systemPrompt += `- ANTI-CONTAMINATION: Verify that any new file uploads belong to your known primary/linked contexts. If not, WARN the user and stop.\n`;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// --- Part 0: The Brain (Manifests + Tree ONLY) ---
|
|
801
|
+
let part0 = `# š§ NOTEBOOKLM KNOWLEDGE BASE ā PART 0 (THE BRAIN)\n`;
|
|
802
|
+
if (mode === 'link') {
|
|
803
|
+
part0 += `**Linked Companion Project:** ${repoName}\n**Absolute Path:** ${absPath}\n`;
|
|
804
|
+
part0 += `*(Note: This is a linked project. You can modify code here if necessary for cross-project integration.)*\n\n`;
|
|
805
|
+
} else if (mode === 'scout') {
|
|
806
|
+
part0 += `**Scouted External Project:** ${repoName}\n**Absolute Path:** ${absPath}\n`;
|
|
807
|
+
part0 += `*(Note: STRICTLY READ-ONLY reference project. Do not write code for this project.)*\n\n`;
|
|
808
|
+
} else {
|
|
809
|
+
part0 += `**Primary Project:** ${repoName}\n**Absolute Path:** ${absPath}\n\n`;
|
|
810
|
+
part0 += `*(Note: Your core instructions are configured in the Chat Settings / Custom Instructions)*\n\n`;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Add .eck manifests
|
|
814
|
+
if (eckManifest) {
|
|
815
|
+
part0 += `## š Project Context & Manifests\n\n`;
|
|
816
|
+
if (eckManifest.context) part0 += `### CONTEXT\n${eckManifest.context}\n\n`;
|
|
817
|
+
if (eckManifest.techDebt) part0 += `### TECH DEBT\n${eckManifest.techDebt}\n\n`;
|
|
818
|
+
if (eckManifest.roadmap) part0 += `### ROADMAP\n${eckManifest.roadmap}\n\n`;
|
|
819
|
+
if (eckManifest.operations) part0 += `### OPERATIONS\n${eckManifest.operations}\n\n`;
|
|
820
|
+
if (eckManifest.dynamicFiles) {
|
|
821
|
+
for (const [name, content] of Object.entries(eckManifest.dynamicFiles)) {
|
|
822
|
+
part0 += `### ${name.replace('.md', '').toUpperCase()}\n${content}\n\n`;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Add full directory tree
|
|
828
|
+
if (directoryTree) {
|
|
829
|
+
part0 += `## š³ Global Directory Structure\n\`\`\`text\n${directoryTree}\n\`\`\`\n`;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const part0Name = `eck_${shortRepoName}_${filePrefix}_part0_BRAIN.md`;
|
|
833
|
+
await fs.writeFile(path.join(outputPath, part0Name), part0);
|
|
834
|
+
console.log(chalk.magenta(` š§ Part 0 (Brain): ${part0Name}`));
|
|
835
|
+
|
|
836
|
+
// --- Parts 1-N: The Body (Source Code Only) ---
|
|
837
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
838
|
+
const chunk = chunks[i];
|
|
839
|
+
const header = `--- NOTEBOOKLM SOURCE CODE ā PART ${i + 1} OF ${chunks.length} ---\n\n`;
|
|
840
|
+
const body = header + chunk.contentArray.join('');
|
|
841
|
+
|
|
842
|
+
const fname = `eck_${shortRepoName}_${filePrefix}_part${i + 1}.md`;
|
|
843
|
+
await fs.writeFile(path.join(outputPath, fname), body);
|
|
844
|
+
console.log(chalk.cyan(` š Part ${i + 1}/${chunks.length}: ${fname} (${formatSize(chunk.size)})`));
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
console.log(chalk.green(`\nā
NotebookLM export complete in ${outputPath}`));
|
|
848
|
+
|
|
849
|
+
// --- CONSOLE OUTPUT INSTRUCTIONS ---
|
|
850
|
+
console.log('\nāļø ' + chalk.yellow.bold(`NOTEBOOKLM UPLOAD INSTRUCTIONS (${mode.toUpperCase()} MODE):`));
|
|
851
|
+
console.log('---------------------------------------------------');
|
|
852
|
+
|
|
853
|
+
if (mode === 'hybrid' || mode === 'architect') {
|
|
854
|
+
console.log(`1. Open NotebookLM and go to: ${chalk.bold('Chat konfigurieren -> Benutzerdefiniert')} (Configure Chat -> Custom)`);
|
|
855
|
+
console.log(`2. Copy the text below and paste it into the prompt window:`);
|
|
856
|
+
console.log('\n' + chalk.bgWhite.black(' --- COPY BELOW THIS LINE --- '));
|
|
857
|
+
console.log(chalk.cyan(systemPrompt));
|
|
858
|
+
console.log(chalk.bgWhite.black(' --- COPY ABOVE THIS LINE --- ') + '\n');
|
|
859
|
+
console.log(`3. Upload Part 0 and Parts 1-${chunks.length} as sources.`);
|
|
860
|
+
if (mode === 'hybrid') {
|
|
861
|
+
console.log(`4. Upload your linked/scouted files as additional sources.`);
|
|
862
|
+
}
|
|
863
|
+
} else {
|
|
864
|
+
// Link or Scout mode
|
|
865
|
+
console.log(`1. Upload Part 0${chunks.length > 0 ? ` and Parts 1-${chunks.length}` : ''} as sources to your EXISTING NotebookLM project.`);
|
|
866
|
+
console.log(`2. No new System Prompt is needed. The primary project's prompt already handles ${mode} files.`);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
await saveGitAnchor(processedRepoPath);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// --- Standard Snapshot Mode ---
|
|
874
|
+
let fileBody = '';
|
|
875
|
+
if (directoryTree) {
|
|
876
|
+
fileBody += `\n## Directory Structure\n\n\`\`\`\n${directoryTree}\`\`\`\n\n`;
|
|
877
|
+
}
|
|
878
|
+
if (!options.skipContent) {
|
|
879
|
+
fileBody += contentArray.join('');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Helper to write snapshot file
|
|
883
|
+
const writeSnapshot = async (suffix, isAgentMode) => {
|
|
884
|
+
let header = '';
|
|
885
|
+
if (options.isLinkedProject) {
|
|
886
|
+
const absPath = processedRepoPath.replace(/\\/g, '/');
|
|
887
|
+
header = `# š LINKED PROJECT: [${repoName}]\n\n`;
|
|
888
|
+
header += `**ABSOLUTE PATH:** \`${absPath}\`\n`;
|
|
889
|
+
header += `**CROSS-CONTEXT MODE:** This is a linked companion project provided for reference. DO NOT generate code for it directly in your response unless explicitly asked. To inspect files inside this project, use your tool (or ask the user) to run ONE of the following commands:\n\n`;
|
|
890
|
+
header += `**Option A: Short format (Best for Windows PowerShell / CMD)**\n`;
|
|
891
|
+
header += `\`eck-snapshot fetch "${absPath}/src/example.js"\`\n\n`;
|
|
892
|
+
header += `**Option B: Pure JSON format (Best for Linux/Mac Bash/Zsh)**\n`;
|
|
893
|
+
header += `\`eck-snapshot '{"name": "eck_fetch", "arguments": {"patterns": ["${absPath}/src/example.js"]}}'\`\n\n`;
|
|
894
|
+
if (options.skipContent) {
|
|
895
|
+
header += `*(Source code omitted due to linkDepth=0. Directory structure only.)*\n\n`;
|
|
896
|
+
}
|
|
897
|
+
} else {
|
|
898
|
+
const opts = { ...options, agent: false, jas: isJas, jao: isJao, jaz: isJaz };
|
|
899
|
+
header = await generateEnhancedAIHeader({ stats, repoName, mode: 'file', eckManifest, options: opts, repoPath: processedRepoPath }, isGitRepo);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Compact filename format
|
|
903
|
+
const shortHash = gitHash ? gitHash.substring(0, 7) : '';
|
|
904
|
+
const shortRepoName = getShortRepoName(repoName);
|
|
905
|
+
|
|
906
|
+
let fname = options.isLinkedProject ? `link_${shortRepoName}${timestamp}` : `eck${shortRepoName}${timestamp}`;
|
|
907
|
+
if (shortHash) fname += `_${shortHash}`;
|
|
908
|
+
|
|
909
|
+
// Add mode suffix
|
|
910
|
+
if (options.skeleton) {
|
|
911
|
+
fname += '_sk';
|
|
912
|
+
} else if (suffix) {
|
|
913
|
+
fname += suffix;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const fullContent = header + fileBody;
|
|
917
|
+
const sizeKB = Math.max(1, Math.round(Buffer.byteLength(fullContent, 'utf-8') / 1024));
|
|
918
|
+
fname += `_${sizeKB}kb.${fileExtension}`;
|
|
919
|
+
const fpath = path.join(outputPath, fname);
|
|
920
|
+
await fs.writeFile(fpath, fullContent);
|
|
921
|
+
const approxTokens = Math.round(fullContent.length / 4);
|
|
922
|
+
const tokensStr = approxTokens < 1000 ? `${approxTokens}` : `${(approxTokens / 1000).toFixed(1)}k`;
|
|
923
|
+
console.log(`š Generated Snapshot: ${fname} (${sizeKB} KB | ~${tokensStr} tokens)`);
|
|
924
|
+
|
|
925
|
+
// --- FEATURE: Active Snapshot ---
|
|
926
|
+
if (!isAgentMode) {
|
|
927
|
+
try {
|
|
928
|
+
if (options.isLinkedProject) {
|
|
929
|
+
// Link snapshots go to .eck/links/
|
|
930
|
+
const linksDir = path.join(originalCwd, '.eck', 'links');
|
|
931
|
+
await fs.mkdir(linksDir, { recursive: true });
|
|
932
|
+
await fs.writeFile(path.join(linksDir, fname), fullContent);
|
|
933
|
+
const approxTokens = Math.round(fullContent.length / 4);
|
|
934
|
+
const tokensStr = approxTokens < 1000 ? `${approxTokens}` : `${(approxTokens / 1000).toFixed(1)}k`;
|
|
935
|
+
console.log(chalk.cyan(`š Link saved to .eck/links/${fname}`));
|
|
936
|
+
console.log(chalk.gray(` Size: ${sizeKB} KB | ~${tokensStr} tokens`));
|
|
937
|
+
} else {
|
|
938
|
+
// Main snapshots go to .eck/lastsnapshot/
|
|
939
|
+
const snapDir = path.join(originalCwd, '.eck', 'lastsnapshot');
|
|
940
|
+
await fs.mkdir(snapDir, { recursive: true });
|
|
941
|
+
|
|
942
|
+
// Clean up OLD snapshots (keep AnswerToSA.md)
|
|
943
|
+
const existingFiles = await fs.readdir(snapDir);
|
|
944
|
+
for (const file of existingFiles) {
|
|
945
|
+
if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
|
|
946
|
+
await fs.unlink(path.join(snapDir, file));
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
await fs.writeFile(path.join(snapDir, fname), fullContent);
|
|
951
|
+
console.log(chalk.cyan(`š Active snapshot updated in .eck/lastsnapshot/: ${fname}`));
|
|
952
|
+
}
|
|
953
|
+
} catch (e) {
|
|
954
|
+
// Non-critical failure
|
|
955
|
+
console.warn(chalk.yellow(`ā ļø Could not update active snapshot: ${e.message}`));
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
// --------------------------------------------
|
|
959
|
+
|
|
960
|
+
return fpath;
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
// Generate snapshot file for ALL modes
|
|
964
|
+
if (isJas) {
|
|
965
|
+
architectFilePath = await writeSnapshot('_jas', true);
|
|
966
|
+
} else if (isJao) {
|
|
967
|
+
architectFilePath = await writeSnapshot('_jao', true);
|
|
968
|
+
} else if (isJaz) {
|
|
969
|
+
architectFilePath = await writeSnapshot('_jaz', true);
|
|
970
|
+
} else {
|
|
971
|
+
// Standard snapshot behavior
|
|
972
|
+
architectFilePath = await writeSnapshot('', false);
|
|
973
|
+
|
|
974
|
+
// --- File 2: Junior Architect Snapshot (legacy --with-ja support) ---
|
|
975
|
+
if (options.withJa && fileExtension === 'md') {
|
|
976
|
+
console.log('šļø Generating Junior Architect (_ja) snapshot...');
|
|
977
|
+
jaFilePath = await writeSnapshot('_ja', true);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
800
980
|
|
|
801
981
|
// Save git anchor for future delta updates
|
|
802
982
|
await saveGitAnchor(processedRepoPath);
|
|
@@ -816,20 +996,48 @@ export async function createRepoSnapshot(repoPath, options) {
|
|
|
816
996
|
console.log('š Scanning for confidential files...');
|
|
817
997
|
const confidentialFiles = await scanEckForConfidentialFiles(processedRepoPath, config);
|
|
818
998
|
|
|
819
|
-
let claudeMode = 'coder';
|
|
820
|
-
if (isJas) claudeMode = 'jas';
|
|
821
|
-
if (isJao) claudeMode = 'jao';
|
|
822
|
-
if (isJaz) claudeMode = 'jaz';
|
|
823
|
-
|
|
824
|
-
// Claude Code exclusively uses CLAUDE.md
|
|
825
|
-
if (isJas || isJao || (!isJaz && !options.withJa)) {
|
|
826
|
-
await updateClaudeMd(processedRepoPath, claudeMode, directoryTree, confidentialFiles, { zh: options.zh });
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
999
|
+
let claudeMode = 'coder';
|
|
1000
|
+
if (isJas) claudeMode = 'jas';
|
|
1001
|
+
if (isJao) claudeMode = 'jao';
|
|
1002
|
+
if (isJaz) claudeMode = 'jaz';
|
|
1003
|
+
|
|
1004
|
+
// Claude Code exclusively uses CLAUDE.md
|
|
1005
|
+
if (isJas || isJao || (!isJaz && !options.withJa)) {
|
|
1006
|
+
await updateClaudeMd(processedRepoPath, claudeMode, directoryTree, confidentialFiles, { zh: options.zh });
|
|
1007
|
+
// Ensure .mcp.json with eck-core is present so Claude Code agents have MCP tools
|
|
1008
|
+
try {
|
|
1009
|
+
const mcpCreated = await ensureProjectMcpConfig(processedRepoPath);
|
|
1010
|
+
if (mcpCreated) {
|
|
1011
|
+
console.log(chalk.green('š Created .mcp.json with eck-core MCP server'));
|
|
1012
|
+
}
|
|
1013
|
+
} catch (e) {
|
|
1014
|
+
// Non-critical ā agent can still use manual fallback
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// OpenCode exclusively uses AGENTS.md
|
|
1019
|
+
if (isJaz || (!isJas && !isJao && !options.withJa)) {
|
|
1020
|
+
await generateOpenCodeAgents(processedRepoPath, claudeMode, directoryTree, confidentialFiles, { zh: options.zh });
|
|
1021
|
+
// Ensure local opencode.json has eck-core MCP server
|
|
1022
|
+
try {
|
|
1023
|
+
const mcpCreated = await ensureProjectOpenCodeConfig(processedRepoPath);
|
|
1024
|
+
if (mcpCreated) {
|
|
1025
|
+
console.log(chalk.green('š Added eck-core to local opencode.json'));
|
|
1026
|
+
}
|
|
1027
|
+
} catch (e) {
|
|
1028
|
+
// Non-critical ā agent can still use manual fallback
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Ensure Codex config if the directory exists
|
|
1032
|
+
try {
|
|
1033
|
+
const codexMcpCreated = await ensureProjectCodexConfig(processedRepoPath);
|
|
1034
|
+
if (codexMcpCreated) {
|
|
1035
|
+
console.log(chalk.green('š Added eck-core to .codex/config.toml'));
|
|
1036
|
+
}
|
|
1037
|
+
} catch (e) {
|
|
1038
|
+
// Non-critical
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
833
1041
|
|
|
834
1042
|
// --- Combined Report ---
|
|
835
1043
|
console.log('\nā
Snapshot generation complete!');
|
|
@@ -924,6 +1132,14 @@ export async function createRepoSnapshot(repoPath, options) {
|
|
|
924
1132
|
console.log(' Replace [ACTUAL_TOKENS_HERE] with the real token count from your LLM');
|
|
925
1133
|
}
|
|
926
1134
|
|
|
1135
|
+
// Output AI Prompt Suggestion for stubborn LLMs
|
|
1136
|
+
console.log('\nš¤ AI PROMPT SUGGESTION (Crucial for ChatGPT, helpful for others):');
|
|
1137
|
+
console.log('---------------------------------------------------');
|
|
1138
|
+
console.log(chalk.yellow('š” Tip: Gemini and Grok handle large files best. ChatGPT works but can be slow.'));
|
|
1139
|
+
console.log('If your AI ignores the file instructions and acts as an external reviewer,');
|
|
1140
|
+
console.log('copy and paste this exact text as your FIRST prompt along with the snapshot file:\n');
|
|
1141
|
+
console.log(chalk.cyan.bold('Read the SYSTEM DIRECTIVE at the very beginning of the attached file. Immediately assume the role of Senior Architect as instructed, then await my first task.\n'));
|
|
1142
|
+
|
|
927
1143
|
} finally {
|
|
928
1144
|
process.chdir(originalCwd); // Final reset back to original CWD
|
|
929
1145
|
}
|