@xelth/eck-snapshot 5.9.0 → 6.4.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.

Potentially problematic release.


This version of @xelth/eck-snapshot might be problematic. Click here for more details.

Files changed (35) hide show
  1. package/README.md +267 -190
  2. package/package.json +15 -2
  3. package/scripts/mcp-eck-core.js +61 -13
  4. package/setup.json +114 -80
  5. package/src/cli/cli.js +235 -385
  6. package/src/cli/commands/createSnapshot.js +336 -122
  7. package/src/cli/commands/recon.js +244 -0
  8. package/src/cli/commands/setupMcp.js +278 -19
  9. package/src/cli/commands/trainTokens.js +42 -32
  10. package/src/cli/commands/updateSnapshot.js +128 -76
  11. package/src/core/depthConfig.js +54 -0
  12. package/src/core/skeletonizer.js +71 -18
  13. package/src/templates/architect-prompt.template.md +34 -0
  14. package/src/templates/multiAgent.md +18 -10
  15. package/src/templates/opencode/coder.template.md +44 -17
  16. package/src/templates/opencode/junior-architect.template.md +45 -15
  17. package/src/templates/skeleton-instruction.md +1 -1
  18. package/src/utils/aiHeader.js +57 -27
  19. package/src/utils/claudeMdGenerator.js +136 -78
  20. package/src/utils/fileUtils.js +1011 -1016
  21. package/src/utils/gitUtils.js +12 -8
  22. package/src/utils/opencodeAgentsGenerator.js +8 -2
  23. package/src/utils/projectDetector.js +66 -21
  24. package/src/utils/tokenEstimator.js +11 -7
  25. package/src/cli/commands/consilium.js +0 -86
  26. package/src/cli/commands/detectProfiles.js +0 -98
  27. package/src/cli/commands/envSync.js +0 -319
  28. package/src/cli/commands/generateProfileGuide.js +0 -144
  29. package/src/cli/commands/pruneSnapshot.js +0 -106
  30. package/src/cli/commands/restoreSnapshot.js +0 -173
  31. package/src/cli/commands/setupGemini.js +0 -149
  32. package/src/cli/commands/setupGemini.test.js +0 -115
  33. package/src/cli/commands/showFile.js +0 -39
  34. package/src/services/claudeCliService.js +0 -626
  35. package/src/services/claudeCliService.test.js +0 -267
@@ -17,14 +17,16 @@ import {
17
17
  ensureSnapshotsInGitignore, initializeEckManifest, generateTimestamp,
18
18
  getShortRepoName, SecretScanner
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
@@ -254,14 +256,14 @@ async function getGitCommitHash(projectPath) {
254
256
  return null;
255
257
  }
256
258
 
257
- async function estimateProjectTokens(projectPath, config, projectType = null) {
259
+ async function estimateProjectTokens(projectPath, config, projectTypes = null) {
258
260
  // Get project-specific filtering if not provided
259
- if (!projectType) {
261
+ if (!projectTypes) {
260
262
  const detection = await detectProjectType(projectPath);
261
- projectType = detection.type;
263
+ projectTypes = getAllDetectedTypes(detection);
262
264
  }
263
265
 
264
- const projectSpecific = await getProjectSpecificFiltering(projectType);
266
+ const projectSpecific = await getProjectSpecificFiltering(projectTypes);
265
267
 
266
268
  // Merge project-specific filters with global config (same as in scanDirectoryRecursively)
267
269
  const effectiveConfig = {
@@ -314,15 +316,17 @@ async function estimateProjectTokens(projectPath, config, projectType = null) {
314
316
  }
315
317
 
316
318
  // Use adaptive polynomial estimation
317
- const estimatedTokens = await estimateTokensWithPolynomial(projectType, totalSize);
319
+ // Token estimation uses the primary type for coefficient lookup
320
+ const primaryType = Array.isArray(projectTypes) ? projectTypes[0] : projectTypes;
321
+ const estimatedTokens = await estimateTokensWithPolynomial(primaryType, totalSize);
318
322
 
319
323
  return { estimatedTokens, totalSize, includedFiles };
320
324
  }
321
325
 
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);
326
+ async function processProjectFiles(repoPath, options, config, projectTypes = null) {
327
+ // Merge project-specific filtering rules for ALL detected types (polyglot monorepo support)
328
+ if (projectTypes) {
329
+ const projectSpecific = await getProjectSpecificFiltering(projectTypes);
326
330
  config = {
327
331
  ...config,
328
332
  dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
@@ -371,7 +375,7 @@ async function processProjectFiles(repoPath, options, config, projectType = null
371
375
 
372
376
  if (filterResult.notFoundProfiles.length > 0) {
373
377
  errorMsg += `\n\nāŒ Profile(s) not found: ${filterResult.notFoundProfiles.join(', ')}`;
374
- errorMsg += `\n\nšŸ’” Run 'eck-snapshot --profile' to see available profiles.`;
378
+ errorMsg += `\n\nšŸ’” Run 'eck-snapshot snapshot --profile' to see available profiles.`;
375
379
  errorMsg += `\n Or run 'eck-snapshot generate-profile-guide' to create profiles.`;
376
380
  } else if (filterResult.foundProfiles.length > 0) {
377
381
  errorMsg += `\n\nāœ“ Profile(s) found: ${filterResult.foundProfiles.join(', ')}`;
@@ -483,7 +487,7 @@ async function processProjectFiles(repoPath, options, config, projectType = null
483
487
  // Check if file should be focused (kept full)
484
488
  const isFocused = options.focus && micromatch.isMatch(normalizedPath, options.focus);
485
489
  if (!isFocused) {
486
- content = await skeletonize(content, normalizedPath);
490
+ content = await skeletonize(content, normalizedPath, { preserveDocs: options.preserveDocs !== false });
487
491
  }
488
492
  }
489
493
 
@@ -531,7 +535,65 @@ async function processProjectFiles(repoPath, options, config, projectType = null
531
535
  }
532
536
  }
533
537
 
538
+ /**
539
+ * Groups files by directory and packs them into chunks of a given maximum size.
540
+ * Preserves directory locality for better RAG retrieval in NotebookLM.
541
+ */
542
+ function packFilesForNotebookLM(successfulFileObjects, maxChunkSizeBytes = 2.5 * 1024 * 1024) {
543
+ const dirGroups = new Map();
544
+
545
+ for (const fileObj of successfulFileObjects) {
546
+ const dir = fileObj.path.includes('/') ? fileObj.path.substring(0, fileObj.path.lastIndexOf('/')) : './';
547
+ if (!dirGroups.has(dir)) dirGroups.set(dir, { size: 0, files: [] });
548
+ const group = dirGroups.get(dir);
549
+ group.files.push(fileObj);
550
+ group.size += fileObj.size;
551
+ }
552
+
553
+ const sortedDirs = Array.from(dirGroups.keys()).sort();
554
+ const chunks = [];
555
+ let currentChunk = { size: 0, contentArray: [] };
556
+
557
+ for (const dir of sortedDirs) {
558
+ const group = dirGroups.get(dir);
559
+
560
+ if (group.size > maxChunkSizeBytes) {
561
+ // Directory too large — pack files individually
562
+ for (const fileObj of group.files) {
563
+ if (currentChunk.size + fileObj.size > maxChunkSizeBytes && currentChunk.contentArray.length > 0) {
564
+ chunks.push(currentChunk);
565
+ currentChunk = { size: 0, contentArray: [] };
566
+ }
567
+ currentChunk.contentArray.push(fileObj.content);
568
+ currentChunk.size += fileObj.size;
569
+ }
570
+ } else if (currentChunk.size + group.size <= maxChunkSizeBytes) {
571
+ // Directory fits in current chunk
572
+ for (const f of group.files) currentChunk.contentArray.push(f.content);
573
+ currentChunk.size += group.size;
574
+ } else {
575
+ // Start a new chunk
576
+ chunks.push(currentChunk);
577
+ currentChunk = { size: 0, contentArray: [] };
578
+ for (const f of group.files) currentChunk.contentArray.push(f.content);
579
+ currentChunk.size += group.size;
580
+ }
581
+ }
582
+
583
+ if (currentChunk.contentArray.length > 0) chunks.push(currentChunk);
584
+ return chunks;
585
+ }
586
+
534
587
  export async function createRepoSnapshot(repoPath, options) {
588
+ // Handle linked project depth settings before processing
589
+ if (options.isLinkedProject) {
590
+ const depthCfg = getDepthConfig(options.linkDepth !== undefined ? options.linkDepth : 0);
591
+ if (depthCfg.skipContent) options.skipContent = true;
592
+ if (depthCfg.skeleton !== undefined) options.skeleton = depthCfg.skeleton;
593
+ if (depthCfg.preserveDocs !== undefined) options.preserveDocs = depthCfg.preserveDocs;
594
+ if (depthCfg.maxLinesPerFile !== undefined) options.maxLinesPerFile = depthCfg.maxLinesPerFile;
595
+ }
596
+
535
597
  const spinner = ora('Analyzing project...').start();
536
598
  try {
537
599
  // Ensure snapshots/ is in .gitignore to prevent accidental commits
@@ -570,7 +632,7 @@ export async function createRepoSnapshot(repoPath, options) {
570
632
  // Generate ready-to-copy command with all profiles
571
633
  const allProfilesString = profileNames.join(',');
572
634
  console.log(chalk.cyan('šŸ“ Ready-to-Copy Command (all profiles):'));
573
- console.log(chalk.bold(`\neck-snapshot --profile "${allProfilesString}"\n`));
635
+ console.log(chalk.bold(`\neck-snapshot '{"name": "eck_snapshot", "arguments": {"profile": "${allProfilesString}"}}'\n`));
574
636
  console.log(chalk.gray('šŸ’” Tip: Copy the command above and remove profiles you don\'t need'));
575
637
  process.exit(0);
576
638
  } catch (error) {
@@ -629,19 +691,19 @@ export async function createRepoSnapshot(repoPath, options) {
629
691
  ...options // Command-line options have the final say
630
692
  };
631
693
 
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
- }
694
+ // Detect architect modes
695
+ const isJas = options.jas;
696
+ const isJao = options.jao;
697
+ const isJaz = options.jaz;
698
+
699
+ // If NOT in Junior Architect mode, hide JA-specific documentation to prevent context pollution
700
+ if (!options.withJa && !isJas && !isJao && !isJaz) {
701
+ if (!config.filesToIgnore) config.filesToIgnore = [];
702
+ config.filesToIgnore.push(
703
+ 'COMMANDS_REFERENCE.md',
704
+ 'codex_delegation_snapshot.md'
705
+ );
706
+ }
645
707
 
646
708
  // Apply defaults for options that may not be provided via command line
647
709
  if (!config.output) {
@@ -658,7 +720,8 @@ export async function createRepoSnapshot(repoPath, options) {
658
720
  config.includeHidden = setupConfig.fileFiltering?.includeHidden ?? false;
659
721
  }
660
722
 
661
- const estimation = await estimateProjectTokens(repoPath, config, projectDetection.type);
723
+ const allTypesForEstimation = getAllDetectedTypes(projectDetection);
724
+ const estimation = await estimateProjectTokens(repoPath, config, allTypesForEstimation);
662
725
  spinner.info(`Estimated project size: ~${Math.round(estimation.estimatedTokens).toLocaleString()} tokens.`);
663
726
 
664
727
  spinner.succeed('Creating snapshots...');
@@ -669,7 +732,8 @@ export async function createRepoSnapshot(repoPath, options) {
669
732
 
670
733
  let stats, contentArray, successfulFileObjects, allFiles, processedRepoPath;
671
734
 
672
- const result = await processProjectFiles(repoPath, options, config, projectDetection.type);
735
+ const allTypes = getAllDetectedTypes(projectDetection);
736
+ const result = await processProjectFiles(repoPath, options, config, allTypes);
673
737
  stats = result.stats;
674
738
  contentArray = result.contentArray;
675
739
  successfulFileObjects = result.successfulFileObjects;
@@ -717,86 +781,200 @@ export async function createRepoSnapshot(repoPath, options) {
717
781
  let architectFilePath = null;
718
782
  let jaFilePath = null;
719
783
 
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
- }
784
+ // --- NotebookLM Chunked Export (Brain + Body) ---
785
+ if (options.notebooklm) {
786
+ const isArchitectMode = options.notebooklm === 'architect';
787
+ const modeName = isArchitectMode ? 'Architect' : 'Scout';
788
+ console.log(chalk.blue(`\nšŸ“š Packing project for NotebookLM (${modeName} Mode)...`));
789
+
790
+ const chunks = packFilesForNotebookLM(successfulFileObjects);
791
+ const shortRepoName = getShortRepoName(repoName);
792
+ const absPath = processedRepoPath.replace(/\\/g, '/');
793
+ const filePrefix = isArchitectMode ? 'notelm' : 'booklm';
794
+
795
+ // Clean up old notebooklm chunks
796
+ try {
797
+ const existingFiles = await fs.readdir(outputPath);
798
+ for (const file of existingFiles) {
799
+ if (file.includes('_booklm_part') || file.includes('_notelm_part')) {
800
+ await fs.unlink(path.join(outputPath, file));
801
+ }
802
+ }
803
+ } catch (e) { /* ignore */ }
804
+
805
+ // --- Part 0: The Brain (Instructions + Manifests + Tree) ---
806
+ let part0 = `# 🧠 NOTEBOOKLM KNOWLEDGE BASE — PART 0 (THE BRAIN)\n`;
807
+ part0 += `**Project:** ${repoName}\n\n`;
808
+
809
+ if (isArchitectMode) {
810
+ part0 += `## YOUR ROLE: THE ARCHITECT\n`;
811
+ part0 += `You are the Senior Software Architect for this project. You have access to the entire codebase across the source documents (Parts 1-${chunks.length}).\n`;
812
+ part0 += `Analyze the codebase to solve complex structural problems, design new features, and propose refactoring strategies. Provide high-level guidance and precise code modifications.\n\n`;
813
+ } else {
814
+ part0 += `## YOUR ROLE: THE SCOUT\n`;
815
+ part0 += `You are an expert code analyst and retrieval specialist. You have access to the entire codebase across the source documents (Parts 1-${chunks.length}). Your goal is NOT to write code, but to help the primary Architect find the exact files they need.\n`;
816
+ part0 += `When asked about a feature, bug, or module — analyze the project structure and codebase, then output precise bash commands using \`eck-snapshot fetch\` so the user can extract the relevant files for their Architect agent.\n\n`;
817
+ part0 += `**RULES FOR FETCH COMMANDS:**\n`;
818
+ part0 += `1. Always start with \`cd ${absPath}\`\n`;
819
+ part0 += `2. Use relative glob patterns: \`eck-snapshot fetch "**/auth.js" "**/userController.js"\`\n`;
820
+ part0 += `3. Output the commands in a bash code block, accompanied by a brief explanation of why you selected those files.\n`;
821
+ part0 += `4. Include both directly relevant files AND adjacent files that provide context (imports, shared types, config).\n\n`;
822
+ }
823
+
824
+ // Add .eck manifests
825
+ if (eckManifest) {
826
+ part0 += `## šŸ“‘ Project Context & Manifests\n\n`;
827
+ if (eckManifest.context) part0 += `### CONTEXT\n${eckManifest.context}\n\n`;
828
+ if (eckManifest.techDebt) part0 += `### TECH DEBT\n${eckManifest.techDebt}\n\n`;
829
+ if (eckManifest.roadmap) part0 += `### ROADMAP\n${eckManifest.roadmap}\n\n`;
830
+ if (eckManifest.operations) part0 += `### OPERATIONS\n${eckManifest.operations}\n\n`;
831
+ // Include any dynamic .eck files
832
+ if (eckManifest.dynamicFiles) {
833
+ for (const [name, content] of Object.entries(eckManifest.dynamicFiles)) {
834
+ part0 += `### ${name.replace('.md', '').toUpperCase()}\n${content}\n\n`;
835
+ }
836
+ }
837
+ }
838
+
839
+ // Add full directory tree
840
+ if (directoryTree) {
841
+ part0 += `## 🌳 Global Directory Structure\n\`\`\`text\n${directoryTree}\n\`\`\`\n`;
842
+ }
843
+
844
+ const part0Name = `eck_${shortRepoName}_${filePrefix}_part0_BRAIN.md`;
845
+ await fs.writeFile(path.join(outputPath, part0Name), part0);
846
+ console.log(chalk.magenta(` 🧠 Part 0 (Brain): ${part0Name}`));
847
+
848
+ // --- Parts 1-N: The Body (Source Code Only) ---
849
+ for (let i = 0; i < chunks.length; i++) {
850
+ const chunk = chunks[i];
851
+ const header = `--- NOTEBOOKLM SOURCE CODE — PART ${i + 1} OF ${chunks.length} ---\n\n`;
852
+ const body = header + chunk.contentArray.join('');
853
+
854
+ const fname = `eck_${shortRepoName}_${filePrefix}_part${i + 1}.md`;
855
+ await fs.writeFile(path.join(outputPath, fname), body);
856
+ console.log(chalk.cyan(` šŸ“„ Part ${i + 1}/${chunks.length}: ${fname} (${formatSize(chunk.size)})`));
857
+ }
858
+
859
+ console.log(chalk.green(`\nāœ… NotebookLM export complete: 1 Brain + ${chunks.length} Source chunk(s) in ${outputPath}`));
860
+ console.log(chalk.gray(` Upload all files as separate sources in NotebookLM (max 50 sources).`));
861
+
862
+ // Starter prompt for NotebookLM chat
863
+ console.log('\nšŸ¤– STARTER PROMPT (paste as your FIRST message in NotebookLM):');
864
+ console.log('---------------------------------------------------');
865
+ console.log(chalk.cyan.bold('Read the source document ending in "_part0_BRAIN.md" completely before answering any questions. This document contains your core instructions, the project context, and the global directory tree. Acknowledge that you understand your role.\n'));
866
+
867
+ await saveGitAnchor(processedRepoPath);
868
+ return;
869
+ }
870
+
871
+ // --- Standard Snapshot Mode ---
872
+ let fileBody = '';
873
+ if (directoryTree) {
874
+ fileBody += `\n## Directory Structure\n\n\`\`\`\n${directoryTree}\`\`\`\n\n`;
875
+ }
876
+ if (!options.skipContent) {
877
+ fileBody += contentArray.join('');
878
+ }
879
+
880
+ // Helper to write snapshot file
881
+ const writeSnapshot = async (suffix, isAgentMode) => {
882
+ let header = '';
883
+ if (options.isLinkedProject) {
884
+ const absPath = processedRepoPath.replace(/\\/g, '/');
885
+ header = `# šŸ”— LINKED PROJECT: [${repoName}]\n\n`;
886
+ header += `**ABSOLUTE PATH:** \`${absPath}\`\n`;
887
+ 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`;
888
+ header += `**Option A: Short format (Best for Windows PowerShell / CMD)**\n`;
889
+ header += `\`eck-snapshot fetch "${absPath}/src/example.js"\`\n\n`;
890
+ header += `**Option B: Pure JSON format (Best for Linux/Mac Bash/Zsh)**\n`;
891
+ header += `\`eck-snapshot '{"name": "eck_fetch", "arguments": {"patterns": ["${absPath}/src/example.js"]}}'\`\n\n`;
892
+ if (options.skipContent) {
893
+ header += `*(Source code omitted due to linkDepth=0. Directory structure only.)*\n\n`;
894
+ }
895
+ } else {
896
+ const opts = { ...options, agent: false, jas: isJas, jao: isJao, jaz: isJaz };
897
+ header = await generateEnhancedAIHeader({ stats, repoName, mode: 'file', eckManifest, options: opts, repoPath: processedRepoPath }, isGitRepo);
898
+ }
899
+
900
+ // Compact filename format
901
+ const shortHash = gitHash ? gitHash.substring(0, 7) : '';
902
+ const shortRepoName = getShortRepoName(repoName);
903
+
904
+ let fname = options.isLinkedProject ? `link_${shortRepoName}${timestamp}` : `eck${shortRepoName}${timestamp}`;
905
+ if (shortHash) fname += `_${shortHash}`;
906
+
907
+ // Add mode suffix
908
+ if (options.skeleton) {
909
+ fname += '_sk';
910
+ } else if (suffix) {
911
+ fname += suffix;
912
+ }
913
+
914
+ const fullContent = header + fileBody;
915
+ const sizeKB = Math.max(1, Math.round(Buffer.byteLength(fullContent, 'utf-8') / 1024));
916
+ fname += `_${sizeKB}kb.${fileExtension}`;
917
+ const fpath = path.join(outputPath, fname);
918
+ await fs.writeFile(fpath, fullContent);
919
+ const approxTokens = Math.round(fullContent.length / 4);
920
+ const tokensStr = approxTokens < 1000 ? `${approxTokens}` : `${(approxTokens / 1000).toFixed(1)}k`;
921
+ console.log(`šŸ“„ Generated Snapshot: ${fname} (${sizeKB} KB | ~${tokensStr} tokens)`);
922
+
923
+ // --- FEATURE: Active Snapshot ---
924
+ if (!isAgentMode) {
925
+ try {
926
+ if (options.isLinkedProject) {
927
+ // Link snapshots go to .eck/links/
928
+ const linksDir = path.join(originalCwd, '.eck', 'links');
929
+ await fs.mkdir(linksDir, { recursive: true });
930
+ await fs.writeFile(path.join(linksDir, fname), fullContent);
931
+ const approxTokens = Math.round(fullContent.length / 4);
932
+ const tokensStr = approxTokens < 1000 ? `${approxTokens}` : `${(approxTokens / 1000).toFixed(1)}k`;
933
+ console.log(chalk.cyan(`šŸ”— Link saved to .eck/links/${fname}`));
934
+ console.log(chalk.gray(` Size: ${sizeKB} KB | ~${tokensStr} tokens`));
935
+ } else {
936
+ // Main snapshots go to .eck/lastsnapshot/
937
+ const snapDir = path.join(originalCwd, '.eck', 'lastsnapshot');
938
+ await fs.mkdir(snapDir, { recursive: true });
939
+
940
+ // Clean up OLD snapshots (keep AnswerToSA.md)
941
+ const existingFiles = await fs.readdir(snapDir);
942
+ for (const file of existingFiles) {
943
+ if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
944
+ await fs.unlink(path.join(snapDir, file));
945
+ }
946
+ }
947
+
948
+ await fs.writeFile(path.join(snapDir, fname), fullContent);
949
+ console.log(chalk.cyan(`šŸ“‹ Active snapshot updated in .eck/lastsnapshot/: ${fname}`));
950
+ }
951
+ } catch (e) {
952
+ // Non-critical failure
953
+ console.warn(chalk.yellow(`āš ļø Could not update active snapshot: ${e.message}`));
954
+ }
955
+ }
956
+ // --------------------------------------------
957
+
958
+ return fpath;
959
+ };
960
+
961
+ // Generate snapshot file for ALL modes
962
+ if (isJas) {
963
+ architectFilePath = await writeSnapshot('_jas', true);
964
+ } else if (isJao) {
965
+ architectFilePath = await writeSnapshot('_jao', true);
966
+ } else if (isJaz) {
967
+ architectFilePath = await writeSnapshot('_jaz', true);
968
+ } else {
969
+ // Standard snapshot behavior
970
+ architectFilePath = await writeSnapshot('', false);
971
+
972
+ // --- File 2: Junior Architect Snapshot (legacy --with-ja support) ---
973
+ if (options.withJa && fileExtension === 'md') {
974
+ console.log('šŸ–‹ļø Generating Junior Architect (_ja) snapshot...');
975
+ jaFilePath = await writeSnapshot('_ja', true);
976
+ }
977
+ }
800
978
 
801
979
  // Save git anchor for future delta updates
802
980
  await saveGitAnchor(processedRepoPath);
@@ -816,20 +994,48 @@ export async function createRepoSnapshot(repoPath, options) {
816
994
  console.log('šŸ” Scanning for confidential files...');
817
995
  const confidentialFiles = await scanEckForConfidentialFiles(processedRepoPath, config);
818
996
 
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
- }
997
+ let claudeMode = 'coder';
998
+ if (isJas) claudeMode = 'jas';
999
+ if (isJao) claudeMode = 'jao';
1000
+ if (isJaz) claudeMode = 'jaz';
1001
+
1002
+ // Claude Code exclusively uses CLAUDE.md
1003
+ if (isJas || isJao || (!isJaz && !options.withJa)) {
1004
+ await updateClaudeMd(processedRepoPath, claudeMode, directoryTree, confidentialFiles, { zh: options.zh });
1005
+ // Ensure .mcp.json with eck-core is present so Claude Code agents have MCP tools
1006
+ try {
1007
+ const mcpCreated = await ensureProjectMcpConfig(processedRepoPath);
1008
+ if (mcpCreated) {
1009
+ console.log(chalk.green('šŸ”Œ Created .mcp.json with eck-core MCP server'));
1010
+ }
1011
+ } catch (e) {
1012
+ // Non-critical — agent can still use manual fallback
1013
+ }
1014
+ }
1015
+
1016
+ // OpenCode exclusively uses AGENTS.md
1017
+ if (isJaz || (!isJas && !isJao && !options.withJa)) {
1018
+ await generateOpenCodeAgents(processedRepoPath, claudeMode, directoryTree, confidentialFiles, { zh: options.zh });
1019
+ // Ensure local opencode.json has eck-core MCP server
1020
+ try {
1021
+ const mcpCreated = await ensureProjectOpenCodeConfig(processedRepoPath);
1022
+ if (mcpCreated) {
1023
+ console.log(chalk.green('šŸ”Œ Added eck-core to local opencode.json'));
1024
+ }
1025
+ } catch (e) {
1026
+ // Non-critical — agent can still use manual fallback
1027
+ }
1028
+
1029
+ // Ensure Codex config if the directory exists
1030
+ try {
1031
+ const codexMcpCreated = await ensureProjectCodexConfig(processedRepoPath);
1032
+ if (codexMcpCreated) {
1033
+ console.log(chalk.green('šŸ”Œ Added eck-core to .codex/config.toml'));
1034
+ }
1035
+ } catch (e) {
1036
+ // Non-critical
1037
+ }
1038
+ }
833
1039
 
834
1040
  // --- Combined Report ---
835
1041
  console.log('\nāœ… Snapshot generation complete!');
@@ -924,6 +1130,14 @@ export async function createRepoSnapshot(repoPath, options) {
924
1130
  console.log(' Replace [ACTUAL_TOKENS_HERE] with the real token count from your LLM');
925
1131
  }
926
1132
 
1133
+ // Output AI Prompt Suggestion for stubborn LLMs
1134
+ console.log('\nšŸ¤– AI PROMPT SUGGESTION (Crucial for ChatGPT, helpful for others):');
1135
+ console.log('---------------------------------------------------');
1136
+ console.log(chalk.yellow('šŸ’” Tip: Gemini and Grok handle large files best. ChatGPT works but can be slow.'));
1137
+ console.log('If your AI ignores the file instructions and acts as an external reviewer,');
1138
+ console.log('copy and paste this exact text as your FIRST prompt along with the snapshot file:\n');
1139
+ 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'));
1140
+
927
1141
  } finally {
928
1142
  process.chdir(originalCwd); // Final reset back to original CWD
929
1143
  }