@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
@@ -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
- // Global hard-ignore patterns (must match exactly in scanDirectoryRecursively)
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, projectType = null) {
219
+ async function estimateProjectTokens(projectPath, config, projectTypes = null) {
258
220
  // Get project-specific filtering if not provided
259
- if (!projectType) {
221
+ if (!projectTypes) {
260
222
  const detection = await detectProjectType(projectPath);
261
- projectType = detection.type;
223
+ projectTypes = getAllDetectedTypes(detection);
262
224
  }
263
225
 
264
- const projectSpecific = await getProjectSpecificFiltering(projectType);
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
- const estimatedTokens = await estimateTokensWithPolynomial(projectType, totalSize);
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, projectType = null) {
323
- // Merge project-specific filtering rules (e.g., Cargo.lock for Rust)
324
- if (projectType) {
325
- const projectSpecific = await getProjectSpecificFiltering(projectType);
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
- // Check if binary file
434
- if (isBinaryPath(filePath)) {
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
- if (fileStats.size > maxFileSize) {
461
- stats.oversizedFiles++;
462
- trackSkippedFile(normalizedPath, `File too large (${formatSize(fileStats.size)} > ${formatSize(maxFileSize)})`);
463
- return null;
464
- }
428
+ let content;
465
429
 
466
- let content = await readFileWithSizeCheck(fullPath, maxFileSize);
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: `--- File: /${normalizedPath} ---\n\n${outputBody}\n\n`,
478
+ content: formattedContent,
503
479
  path: normalizedPath,
504
- size: fileStats.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 --profile "${allProfilesString}"\n`));
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 estimation = await estimateProjectTokens(repoPath, config, projectDetection.type);
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 result = await processProjectFiles(repoPath, options, config, projectDetection.type);
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
- // File body always includes full content
721
- let fileBody = (directoryTree ? `\n## Directory Structure\n\n\`\`\`\n${directoryTree}\`\`\`\n\n` : '') + contentArray.join('');
722
-
723
- // Helper to write snapshot file
724
- const writeSnapshot = async (suffix, isAgentMode) => {
725
- // CHANGE: Force agent to FALSE for the main snapshot header.
726
- // The snapshot is read by the Human/Senior Arch, not the Agent itself.
727
- // The Agent reads CLAUDE.md.
728
- const opts = { ...options, agent: false, jas: isJas, jao: isJao, jaz: isJaz };
729
- const header = await generateEnhancedAIHeader({ stats, repoName, mode: 'file', eckManifest, options: opts, repoPath: processedRepoPath }, isGitRepo);
730
-
731
- // Compact filename format: eck{ShortName}{timestamp}_{hash}_{suffix}.md
732
- // getShortRepoName ensures Capitalized Start/End (e.g. SnaOt)
733
- const shortHash = gitHash ? gitHash.substring(0, 7) : '';
734
- const shortRepoName = getShortRepoName(repoName);
735
-
736
- let fname = `eck${shortRepoName}${timestamp}`;
737
- if (shortHash) fname += `_${shortHash}`;
738
-
739
- // Add mode suffix
740
- if (options.skeleton) {
741
- fname += '_sk';
742
- } else if (suffix) {
743
- fname += suffix;
744
- }
745
-
746
- const fullContent = header + fileBody;
747
- const sizeKB = Math.max(1, Math.round(Buffer.byteLength(fullContent, 'utf-8') / 1024));
748
- fname += `_${sizeKB}kb.${fileExtension}`;
749
- const fpath = path.join(outputPath, fname);
750
- await fs.writeFile(fpath, fullContent);
751
- console.log(`šŸ“„ Generated Snapshot: ${fname}`);
752
-
753
- // --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
754
- // Only create .eck/lastsnapshot/ entries for the main snapshot
755
- if (!isAgentMode) {
756
- try {
757
- const snapDir = path.join(originalCwd, '.eck', 'lastsnapshot');
758
- await fs.mkdir(snapDir, { recursive: true });
759
-
760
- // 1. Clean up OLD snapshots in this specific folder
761
- // We keep AnswerToSA.md, but remove old snapshots and legacy answer.md
762
- const existingFiles = await fs.readdir(snapDir);
763
- for (const file of existingFiles) {
764
- if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
765
- await fs.unlink(path.join(snapDir, file));
766
- }
767
- }
768
-
769
- // 2. Save the NEW specific named file
770
- await fs.writeFile(path.join(snapDir, fname), fullContent);
771
-
772
- console.log(chalk.cyan(`šŸ“‹ Active snapshot updated in .eck/lastsnapshot/: ${fname}`));
773
- } catch (e) {
774
- // Non-critical failure
775
- console.warn(chalk.yellow(`āš ļø Could not update .eck/lastsnapshot/: ${e.message}`));
776
- }
777
- }
778
- // --------------------------------------------
779
-
780
- return fpath;
781
- };
782
-
783
- // Generate snapshot file for ALL modes
784
- if (isJas) {
785
- architectFilePath = await writeSnapshot('_jas', true);
786
- } else if (isJao) {
787
- architectFilePath = await writeSnapshot('_jao', true);
788
- } else if (isJaz) {
789
- architectFilePath = await writeSnapshot('_jaz', true);
790
- } else {
791
- // Standard snapshot behavior
792
- architectFilePath = await writeSnapshot('', false);
793
-
794
- // --- File 2: Junior Architect Snapshot (legacy --with-ja support) ---
795
- if (options.withJa && fileExtension === 'md') {
796
- console.log('šŸ–‹ļø Generating Junior Architect (_ja) snapshot...');
797
- jaFilePath = await writeSnapshot('_ja', true);
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
- // OpenCode exclusively uses AGENTS.md
830
- if (isJaz || (!isJas && !isJao && !options.withJa)) {
831
- await generateOpenCodeAgents(processedRepoPath, claudeMode, directoryTree, confidentialFiles, { zh: options.zh });
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
  }