@xelth/eck-snapshot 5.4.0 → 5.4.2

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 CHANGED
@@ -1,106 +1,128 @@
1
+ # eck-snapshot
1
2
 
2
- # eckSnapshot (v5.4.0)
3
+ A CLI tool that packs your entire Git repository into a single text file optimized for LLMs (Claude, Gemini, ChatGPT). Give any AI full project context in one copy-paste.
3
4
 
4
- A specialized CLI tool designed to create and restore single-file text snapshots of Git repositories. It is specifically optimized for providing full project context to Large Language Models (LLMs) like Claude, Gemini, and OpenCode.
5
+ ```bash
6
+ npm install -g @xelth/eck-snapshot
7
+ ```
5
8
 
6
- šŸŽ‰ **WE ARE BACK ON NPM!** Version 5.4.0 and onwards are officially available via the npm registry.
9
+ ## Core Workflow
7
10
 
8
- ## šŸš€ Quick Start
11
+ ### 1. Full Snapshot
9
12
 
10
- ```bash
11
- # Install globally via npm
12
- npm install -g @xelth/eck-snapshot
13
+ Run `eck-snapshot` in your project root. It scans every tracked file, filters out noise (lock files, build artifacts, secrets), and produces a single `.md` file ready for an AI chat.
13
14
 
14
- # Create a snapshot of your current project
15
+ ```bash
15
16
  eck-snapshot
17
+ # -> .eck/snapshots/eckMyProject_26-02-15_12-00_abc1234.md
16
18
  ```
17
19
 
18
- ## ✨ Core Features
20
+ That's it. Upload the file to your AI and start working.
21
+
22
+ ### 2. Incremental Update
23
+
24
+ After you make changes, don't re-send the entire project. Send only what changed since the last full snapshot:
25
+
26
+ ```bash
27
+ eck-snapshot update
28
+ # -> .eck/snapshots/eckMyProject_26-02-15_14-30_abc1234_up1.md
29
+ ```
19
30
 
20
- - **Skeleton Mode:** Strips function bodies using Tree-sitter and Babel to save massive amounts of context tokens.
21
- - **Delta Updates:** Tracks changes via Git anchors and generates incremental snapshots (`eck-snapshot update`).
22
- - **Royal Court Architecture:** Multi-agent protocol with dedicated modes for Claude Sonnet (JAS), Claude Opus (JAO), and Gemini (JAG).
23
- - **GLM Z.AI Worker Fleet:** Built-in MCP server integration for delegating heavy coding tasks to specialized AI workers.
24
- - **Security:** Built-in `SecretScanner` automatically redacts API keys and sensitive credentials before they hit the LLM context.
25
- - **Context Profiles:** Smart filtering using auto-detected or manual profiles (e.g., `--profile backend`).
31
+ This uses a Git anchor (saved automatically during full snapshot) to detect all modified files and includes their full content. No redundant diffs, no wasted tokens.
26
32
 
27
- ---
33
+ ### 3. Lazy Loading
28
34
 
29
- ## šŸ› ļø The Core Workflow
35
+ If the AI asks to see specific files that weren't in the snapshot, show them instantly:
30
36
 
31
- ### 1. Initial Context (Maximum Compression)
32
- Create a lightweight map of your entire project. Bodies of functions are hidden, allowing huge monoliths to fit into the AI's context window.
33
37
  ```bash
34
- eck-snapshot --skeleton
35
- # -> Generates: .eck/snapshots/eck[Name]_[Hash]_sk.md
38
+ eck-snapshot show src/auth.rs src/handlers/sync.rs
36
39
  ```
37
40
 
38
- ### 2. Lazy Loading (On-Demand Details)
39
- If the AI needs to see the exact implementation of specific files, it can request them on demand.
41
+ ## Context Profiles
42
+
43
+ Large repositories waste tokens on irrelevant code. Profiles let you partition the codebase so the AI only sees what matters.
44
+
45
+ ### Auto-Detection
46
+
47
+ Let AI scan your directory tree and generate profiles automatically:
48
+
40
49
  ```bash
41
- eck-snapshot show src/auth.js src/utils/hash.js
50
+ eck-snapshot profile-detect
51
+ # -> Saves profiles to .eck/profiles.json
42
52
  ```
43
53
 
44
- ### 3. Incremental Updates (Delta)
45
- As you apply changes, the AI loses context. Instead of re-sending the full repository, send only what changed since the last snapshot!
54
+ ### Manual Guide
55
+
56
+ For very large repos where auto-detection is too slow, generate a prompt guide, paste it into a powerful Web LLM (Gemini, ChatGPT), and save the resulting JSON:
57
+
46
58
  ```bash
47
- eck-snapshot update
48
- # -> Generates an update snapshot with git diffs and modified files
59
+ eck-snapshot generate-profile-guide
60
+ # -> .eck/profile_generation_guide.md (paste into AI, get profiles back)
49
61
  ```
50
62
 
51
- ---
63
+ ### Using Profiles
52
64
 
53
- ## šŸ‘‘ Royal Court Architecture & GLM Z.AI
65
+ ```bash
66
+ eck-snapshot --profile # List all available profiles
67
+ eck-snapshot --profile backend # Use a named profile
68
+ eck-snapshot --profile backend --skeleton # Profile + skeleton mode
69
+ eck-snapshot --profile "src/**/*.rs,-**/test_*" # Ad-hoc glob filtering
70
+ ```
54
71
 
55
- `eck-snapshot` is designed to orchestrate a hierarchy of AI agents:
72
+ Profiles work with both full snapshots and incremental updates.
56
73
 
57
- - **Senior Architect:** (You / Gemini / ChatGPT) - Directs the high-level strategy.
58
- - **Junior Architects:**
59
- - `JAS` (Sonnet 4.5): Fast manager for standard features. Run `eck-snapshot --jas`.
60
- - `JAO` (Opus 4.5): Deep thinker for critical architecture. Run `eck-snapshot --jao`.
61
- - `JAG` (Gemini 3 Pro): Massive context handler. Run `eck-snapshot --jag`.
74
+ ## Smart Filtering
62
75
 
63
- ### MCP Server Integration
64
- Delegate heavy coding tasks (>100 lines) to the **GLM Z.AI Worker Fleet** to save expensive context window tokens.
76
+ eck-snapshot automatically detects your project type (Rust, Node.js, Android, Python, etc.) and excludes language-specific noise:
65
77
 
66
- 1. Get your API key from [Z.AI](https://z.ai) and export it: `export ZAI_API_KEY="your-key-here"`
67
- 2. Setup the MCP servers for Claude Code or OpenCode:
68
- ```bash
69
- eck-snapshot setup-mcp --both
70
- ```
71
- 3. Your AI will now have access to specialized tools: `glm_zai_frontend`, `glm_zai_backend`, `glm_zai_qa`, `glm_zai_refactor`, and the `eck_finish_task` commit tool.
78
+ - **Rust**: `Cargo.lock`, `target/`
79
+ - **Node.js**: `package-lock.json`, `node_modules/`
80
+ - **Android**: build artifacts, generated code
81
+ - **All projects**: `.git/`, IDE configs, binary files
72
82
 
73
- ---
83
+ The built-in `SecretScanner` also redacts API keys, tokens, and credentials before they reach the AI.
74
84
 
75
- ## 🧩 Context Profiles
85
+ ## Multi-Agent Architecture
76
86
 
77
- If your repository is huge, you can partition it using Context Profiles:
87
+ eck-snapshot generates tailored `CLAUDE.md` instructions for different AI agent roles:
78
88
 
79
89
  ```bash
80
- # Auto-detect profiles using AI
81
- eck-snapshot profile-detect
90
+ eck-snapshot --jas # Junior Architect Sonnet - fast, standard features
91
+ eck-snapshot --jao # Junior Architect Opus - deep, critical architecture
92
+ eck-snapshot --jag # Junior Architect Gemini - massive context tasks
93
+ ```
82
94
 
83
- # List available profiles
84
- eck-snapshot --profile
95
+ ### MCP Server Integration
85
96
 
86
- # Use a specific profile
87
- eck-snapshot --profile backend
97
+ Delegate coding tasks to the GLM Z.AI Worker Fleet via MCP:
88
98
 
89
- # Ad-hoc inclusion/exclusion
90
- eck-snapshot --profile "src/**/*.js,-**/*.test.js"
99
+ ```bash
100
+ export ZAI_API_KEY="your-key"
101
+ eck-snapshot setup-mcp --both # Setup for Claude Code + OpenCode
91
102
  ```
92
103
 
93
- ## šŸ” Environment Syncing
104
+ This gives your AI access to specialized workers: `glm_zai_frontend`, `glm_zai_backend`, `glm_zai_qa`, `glm_zai_refactor`, and the `eck_finish_task` commit tool.
105
+
106
+ ## Skeleton Mode
94
107
 
95
- Securely share your `.eck/` configuration (profiles, roadmap, AI instructions) between machines without committing them to the public git history:
108
+ For extremely large projects, skeleton mode strips function bodies and keeps only signatures, types, and structure:
96
109
 
97
110
  ```bash
98
- # Encrypt and pack .eck/ config files
99
- eck-snapshot env push
111
+ eck-snapshot --skeleton
112
+ ```
100
113
 
101
- # Decrypt and restore on another machine
102
- eck-snapshot env pull
114
+ Useful for initial orientation in massive codebases, but full snapshots with profiles are usually more practical.
115
+
116
+ ## Other Commands
117
+
118
+ ```bash
119
+ eck-snapshot restore <snapshot> # Restore files from a snapshot to disk
120
+ eck-snapshot prune <snapshot> # AI-powered snapshot size reduction
121
+ eck-snapshot doctor # Check project health
122
+ eck-snapshot env push # Encrypt and sync .eck/ config between machines
123
+ eck-snapshot env pull # Restore .eck/ config on another machine
103
124
  ```
104
125
 
105
126
  ## License
106
- MIT License
127
+
128
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xelth/eck-snapshot",
3
- "version": "5.4.0",
3
+ "version": "5.4.2",
4
4
  "description": "A powerful CLI tool to create and restore single-file text snapshots of Git repositories and directories. Optimized for AI context and LLM workflows.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -10,13 +10,13 @@ import ora from 'ora';
10
10
  import micromatch from 'micromatch';
11
11
  import chalk from 'chalk';
12
12
 
13
- import {
14
- parseSize, formatSize, matchesPattern, checkGitRepository,
15
- scanDirectoryRecursively, loadGitignore, readFileWithSizeCheck,
16
- generateDirectoryTree, loadConfig, displayProjectInfo, loadProjectEckManifest,
17
- ensureSnapshotsInGitignore, initializeEckManifest, generateTimestamp,
18
- getShortRepoName, SecretScanner
19
- } from '../../utils/fileUtils.js';
13
+ import {
14
+ parseSize, formatSize, matchesPattern, checkGitRepository,
15
+ scanDirectoryRecursively, loadGitignore, readFileWithSizeCheck,
16
+ generateDirectoryTree, loadConfig, displayProjectInfo, loadProjectEckManifest,
17
+ ensureSnapshotsInGitignore, initializeEckManifest, generateTimestamp,
18
+ getShortRepoName, SecretScanner
19
+ } from '../../utils/fileUtils.js';
20
20
  import { detectProjectType, getProjectSpecificFiltering } from '../../utils/projectDetector.js';
21
21
  import { estimateTokensWithPolynomial, generateTrainingCommand } from '../../utils/tokenEstimator.js';
22
22
  import { loadSetupConfig, getProfile } from '../../config.js';
@@ -312,6 +312,17 @@ async function estimateProjectTokens(projectPath, config, projectType = null) {
312
312
  }
313
313
 
314
314
  async function processProjectFiles(repoPath, options, config, projectType = null) {
315
+ // Merge project-specific filtering rules (e.g., Cargo.lock for Rust)
316
+ if (projectType) {
317
+ const projectSpecific = await getProjectSpecificFiltering(projectType);
318
+ config = {
319
+ ...config,
320
+ dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
321
+ filesToIgnore: [...(config.filesToIgnore || []), ...(projectSpecific.filesToIgnore || [])],
322
+ extensionsToIgnore: [...(config.extensionsToIgnore || []), ...(projectSpecific.extensionsToIgnore || [])]
323
+ };
324
+ }
325
+
315
326
  const originalCwd = process.cwd();
316
327
  console.log(`\nšŸ“ø Processing files for: ${path.basename(repoPath)}`);
317
328
 
@@ -703,59 +714,59 @@ export async function createRepoSnapshot(repoPath, options) {
703
714
 
704
715
  // Helper to write snapshot file
705
716
  const writeSnapshot = async (suffix, isAgentMode) => {
706
- // CHANGE: Force agent to FALSE for the main snapshot header.
707
- // The snapshot is read by the Human/Senior Arch, not the Agent itself.
708
- // The Agent reads CLAUDE.md.
709
- const opts = { ...options, agent: false, jag: isJag, jas: isJas, jao: isJao };
710
- const header = await generateEnhancedAIHeader({ stats, repoName, mode: 'file', eckManifest, options: opts, repoPath: processedRepoPath }, isGitRepo);
711
-
712
- // Compact filename format: eck{ShortName}{timestamp}_{hash}_{suffix}.md
713
- // getShortRepoName ensures Capitalized Start/End (e.g. SnaOt)
714
- const shortHash = gitHash ? gitHash.substring(0, 7) : '';
715
- const shortRepoName = getShortRepoName(repoName);
716
-
717
- let fname = `eck${shortRepoName}${timestamp}`;
718
- if (shortHash) fname += `_${shortHash}`;
719
-
720
- // Add mode suffix
721
- if (options.skeleton) {
722
- fname += '_sk';
723
- } else if (suffix) {
724
- fname += suffix;
725
- }
726
-
727
- fname += `.${fileExtension}`;
728
- const fpath = path.join(outputPath, fname);
729
- const fullContent = header + fileBody;
730
- await fs.writeFile(fpath, fullContent);
731
- console.log(`šŸ“„ Generated Snapshot: ${fname}`);
732
-
733
- // --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
734
- // Only create .eck/lastsnapshot/ entries for the main snapshot
735
- if (!isAgentMode) {
736
- try {
737
- const snapDir = path.join(originalCwd, '.eck', 'lastsnapshot');
738
- await fs.mkdir(snapDir, { recursive: true });
739
-
740
- // 1. Clean up OLD snapshots in this specific folder
741
- // We keep AnswerToSA.md, but remove old snapshots and legacy answer.md
742
- const existingFiles = await fs.readdir(snapDir);
743
- for (const file of existingFiles) {
744
- if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
745
- await fs.unlink(path.join(snapDir, file));
746
- }
747
- }
748
-
749
- // 2. Save the NEW specific named file
750
- await fs.writeFile(path.join(snapDir, fname), fullContent);
751
-
752
- console.log(chalk.cyan(`šŸ“‹ Active snapshot updated in .eck/lastsnapshot/: ${fname}`));
753
- } catch (e) {
754
- // Non-critical failure
755
- console.warn(chalk.yellow(`āš ļø Could not update .eck/lastsnapshot/: ${e.message}`));
756
- }
757
- }
758
- // --------------------------------------------
717
+ // CHANGE: Force agent to FALSE for the main snapshot header.
718
+ // The snapshot is read by the Human/Senior Arch, not the Agent itself.
719
+ // The Agent reads CLAUDE.md.
720
+ const opts = { ...options, agent: false, jag: isJag, jas: isJas, jao: isJao };
721
+ const header = await generateEnhancedAIHeader({ stats, repoName, mode: 'file', eckManifest, options: opts, repoPath: processedRepoPath }, isGitRepo);
722
+
723
+ // Compact filename format: eck{ShortName}{timestamp}_{hash}_{suffix}.md
724
+ // getShortRepoName ensures Capitalized Start/End (e.g. SnaOt)
725
+ const shortHash = gitHash ? gitHash.substring(0, 7) : '';
726
+ const shortRepoName = getShortRepoName(repoName);
727
+
728
+ let fname = `eck${shortRepoName}${timestamp}`;
729
+ if (shortHash) fname += `_${shortHash}`;
730
+
731
+ // Add mode suffix
732
+ if (options.skeleton) {
733
+ fname += '_sk';
734
+ } else if (suffix) {
735
+ fname += suffix;
736
+ }
737
+
738
+ fname += `.${fileExtension}`;
739
+ const fpath = path.join(outputPath, fname);
740
+ const fullContent = header + fileBody;
741
+ await fs.writeFile(fpath, fullContent);
742
+ console.log(`šŸ“„ Generated Snapshot: ${fname}`);
743
+
744
+ // --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
745
+ // Only create .eck/lastsnapshot/ entries for the main snapshot
746
+ if (!isAgentMode) {
747
+ try {
748
+ const snapDir = path.join(originalCwd, '.eck', 'lastsnapshot');
749
+ await fs.mkdir(snapDir, { recursive: true });
750
+
751
+ // 1. Clean up OLD snapshots in this specific folder
752
+ // We keep AnswerToSA.md, but remove old snapshots and legacy answer.md
753
+ const existingFiles = await fs.readdir(snapDir);
754
+ for (const file of existingFiles) {
755
+ if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
756
+ await fs.unlink(path.join(snapDir, file));
757
+ }
758
+ }
759
+
760
+ // 2. Save the NEW specific named file
761
+ await fs.writeFile(path.join(snapDir, fname), fullContent);
762
+
763
+ console.log(chalk.cyan(`šŸ“‹ Active snapshot updated in .eck/lastsnapshot/: ${fname}`));
764
+ } catch (e) {
765
+ // Non-critical failure
766
+ console.warn(chalk.yellow(`āš ļø Could not update .eck/lastsnapshot/: ${e.message}`));
767
+ }
768
+ }
769
+ // --------------------------------------------
759
770
 
760
771
  return fpath;
761
772
  };
@@ -2,9 +2,10 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import ora from 'ora';
4
4
  import chalk from 'chalk';
5
- import { getGitAnchor, getChangedFiles, getGitDiffOutput } from '../../utils/gitUtils.js';
5
+ import { getGitAnchor, getChangedFiles } from '../../utils/gitUtils.js';
6
6
  import { loadSetupConfig } from '../../config.js';
7
7
  import { readFileWithSizeCheck, parseSize, formatSize, matchesPattern, loadGitignore, generateTimestamp, getShortRepoName } from '../../utils/fileUtils.js';
8
+ import { detectProjectType, getProjectSpecificFiltering } from '../../utils/projectDetector.js';
8
9
  import { fileURLToPath } from 'url';
9
10
 
10
11
  const __filename = fileURLToPath(import.meta.url);
@@ -16,18 +17,22 @@ async function generateSnapshotContent(repoPath, changedFiles, anchor, config, g
16
17
  let includedCount = 0;
17
18
  const fileList = [];
18
19
 
19
- // Check for Agent Report in .eck/lastsnapshot/AnswerToSA.md (STRICT LOCATION)
20
- const reportPath = path.join(repoPath, '.eck', 'lastsnapshot', 'AnswerToSA.md');
21
- let agentReport = null;
22
- try {
23
- agentReport = await fs.readFile(reportPath, 'utf-8');
24
- if (!changedFiles.includes('.eck/lastsnapshot/AnswerToSA.md')) {
25
- changedFiles.push('.eck/lastsnapshot/AnswerToSA.md');
26
- }
27
- } catch (e) { /* No report */ }
20
+ // Check for Agent Report in .eck/lastsnapshot/AnswerToSA.md (STRICT LOCATION)
21
+ const reportPath = path.join(repoPath, '.eck', 'lastsnapshot', 'AnswerToSA.md');
22
+ let agentReport = null;
23
+ try {
24
+ agentReport = await fs.readFile(reportPath, 'utf-8');
25
+ if (!changedFiles.includes('.eck/lastsnapshot/AnswerToSA.md')) {
26
+ changedFiles.push('.eck/lastsnapshot/AnswerToSA.md');
27
+ }
28
+ } catch (e) { /* No report */ }
28
29
 
29
30
  for (const filePath of changedFiles) {
30
- if (config.dirsToIgnore.some(d => filePath.startsWith(d))) continue;
31
+ if (config.dirsToIgnore?.some(d => filePath.startsWith(d))) continue;
32
+ const fileName = path.basename(filePath);
33
+ const fileExt = path.extname(filePath);
34
+ if (config.filesToIgnore?.includes(fileName)) continue;
35
+ if (fileExt && config.extensionsToIgnore?.includes(fileExt)) continue;
31
36
  if (gitignore.ignores(filePath) && filePath !== '.eck/lastsnapshot/AnswerToSA.md') continue;
32
37
 
33
38
  try {
@@ -55,15 +60,12 @@ async function generateSnapshotContent(repoPath, changedFiles, anchor, config, g
55
60
 
56
61
  header = reportSection + header;
57
62
 
58
- const diffOutput = await getGitDiffOutput(repoPath, anchor);
59
- const diffSection = `\n--- GIT DIFF (For Context) ---\n\n\`\`\`diff\n${diffOutput}\n\`\`\``;
60
-
61
- return {
62
- fullContent: header + contentOutput + diffSection,
63
- includedCount,
64
- anchor,
65
- agentReport
66
- };
63
+ return {
64
+ fullContent: header + contentOutput,
65
+ includedCount,
66
+ anchor,
67
+ agentReport
68
+ };
67
69
  }
68
70
 
69
71
  export async function updateSnapshot(repoPath, options) {
@@ -80,11 +82,24 @@ export async function updateSnapshot(repoPath, options) {
80
82
  return;
81
83
  }
82
84
 
83
- const setupConfig = await loadSetupConfig();
84
- const config = { ...setupConfig.fileFiltering, ...setupConfig.performance, ...options };
85
- const gitignore = await loadGitignore(repoPath);
86
-
87
- const { fullContent, includedCount, agentReport } = await generateSnapshotContent(repoPath, changedFiles, anchor, config, gitignore);
85
+ const setupConfig = await loadSetupConfig();
86
+ let config = { ...setupConfig.fileFiltering, ...setupConfig.performance, ...options };
87
+
88
+ // Detect project type and merge project-specific filters
89
+ const projectDetection = await detectProjectType(repoPath);
90
+ if (projectDetection.type) {
91
+ const projectSpecific = await getProjectSpecificFiltering(projectDetection.type);
92
+ config = {
93
+ ...config,
94
+ dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
95
+ filesToIgnore: [...(config.filesToIgnore || []), ...(projectSpecific.filesToIgnore || [])],
96
+ extensionsToIgnore: [...(config.extensionsToIgnore || []), ...(projectSpecific.extensionsToIgnore || [])]
97
+ };
98
+ }
99
+
100
+ const gitignore = await loadGitignore(repoPath);
101
+
102
+ const { fullContent, includedCount, agentReport } = await generateSnapshotContent(repoPath, changedFiles, anchor, config, gitignore);
88
103
 
89
104
  // Determine sequence number
90
105
  let seqNum = 1;
@@ -101,43 +116,43 @@ export async function updateSnapshot(repoPath, options) {
101
116
  await fs.writeFile(counterPath, `${anchor.substring(0, 7)}:${seqNum}`);
102
117
  } catch (e) {}
103
118
 
104
- const timestamp = generateTimestamp();
105
- const shortRepoName = getShortRepoName(path.basename(repoPath));
106
- const outputFilename = `eck${shortRepoName}${timestamp}_${anchor.substring(0, 7)}_up${seqNum}.md`;
107
- const outputPath = path.join(repoPath, '.eck', 'snapshots', outputFilename);
108
-
109
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
110
- await fs.writeFile(outputPath, fullContent);
111
-
112
- spinner.succeed(`Update snapshot created: .eck/snapshots/${outputFilename}`);
113
-
114
- // --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
115
- try {
116
- const snapDir = path.join(repoPath, '.eck', 'lastsnapshot');
117
- await fs.mkdir(snapDir, { recursive: true });
118
-
119
- // 1. Clean up OLD snapshots
120
- const existingFiles = await fs.readdir(snapDir);
121
- for (const file of existingFiles) {
122
- if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
123
- await fs.unlink(path.join(snapDir, file));
124
- }
125
- }
126
-
127
- // 2. Save new file
128
- await fs.writeFile(path.join(snapDir, outputFilename), fullContent);
129
- console.log(chalk.cyan(`šŸ“‹ Active snapshot updated in .eck/lastsnapshot/: ${outputFilename}`));
130
- } catch (e) {
131
- // Non-critical failure
132
- }
133
- // --------------------------------------------
134
-
135
- // Check if agent report was included
136
- if (agentReport) {
137
- console.log(chalk.green('šŸ“Ø Included Agent Report (.eck/lastsnapshot/AnswerToSA.md)'));
138
- }
139
-
140
- console.log(`šŸ“¦ Included ${includedCount} changed files.`);
119
+ const timestamp = generateTimestamp();
120
+ const shortRepoName = getShortRepoName(path.basename(repoPath));
121
+ const outputFilename = `eck${shortRepoName}${timestamp}_${anchor.substring(0, 7)}_up${seqNum}.md`;
122
+ const outputPath = path.join(repoPath, '.eck', 'snapshots', outputFilename);
123
+
124
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
125
+ await fs.writeFile(outputPath, fullContent);
126
+
127
+ spinner.succeed(`Update snapshot created: .eck/snapshots/${outputFilename}`);
128
+
129
+ // --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
130
+ try {
131
+ const snapDir = path.join(repoPath, '.eck', 'lastsnapshot');
132
+ await fs.mkdir(snapDir, { recursive: true });
133
+
134
+ // 1. Clean up OLD snapshots
135
+ const existingFiles = await fs.readdir(snapDir);
136
+ for (const file of existingFiles) {
137
+ if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
138
+ await fs.unlink(path.join(snapDir, file));
139
+ }
140
+ }
141
+
142
+ // 2. Save new file
143
+ await fs.writeFile(path.join(snapDir, outputFilename), fullContent);
144
+ console.log(chalk.cyan(`šŸ“‹ Active snapshot updated in .eck/lastsnapshot/: ${outputFilename}`));
145
+ } catch (e) {
146
+ // Non-critical failure
147
+ }
148
+ // --------------------------------------------
149
+
150
+ // Check if agent report was included
151
+ if (agentReport) {
152
+ console.log(chalk.green('šŸ“Ø Included Agent Report (.eck/lastsnapshot/AnswerToSA.md)'));
153
+ }
154
+
155
+ console.log(`šŸ“¦ Included ${includedCount} changed files.`);
141
156
 
142
157
  } catch (error) {
143
158
  spinner.fail(`Update failed: ${error.message}`);
@@ -159,11 +174,24 @@ export async function updateSnapshotJson(repoPath) {
159
174
  return;
160
175
  }
161
176
 
162
- const setupConfig = await loadSetupConfig();
163
- const config = { ...setupConfig.fileFiltering, ...setupConfig.performance };
164
- const gitignore = await loadGitignore(repoPath);
165
-
166
- const { fullContent, includedCount, agentReport } = await generateSnapshotContent(repoPath, changedFiles, anchor, config, gitignore);
177
+ const setupConfig = await loadSetupConfig();
178
+ let config = { ...setupConfig.fileFiltering, ...setupConfig.performance };
179
+
180
+ // Detect project type and merge project-specific filters
181
+ const projectDetection = await detectProjectType(repoPath);
182
+ if (projectDetection.type) {
183
+ const projectSpecific = await getProjectSpecificFiltering(projectDetection.type);
184
+ config = {
185
+ ...config,
186
+ dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
187
+ filesToIgnore: [...(config.filesToIgnore || []), ...(projectSpecific.filesToIgnore || [])],
188
+ extensionsToIgnore: [...(config.extensionsToIgnore || []), ...(projectSpecific.extensionsToIgnore || [])]
189
+ };
190
+ }
191
+
192
+ const gitignore = await loadGitignore(repoPath);
193
+
194
+ const { fullContent, includedCount, agentReport } = await generateSnapshotContent(repoPath, changedFiles, anchor, config, gitignore);
167
195
 
168
196
  let seqNum = 1;
169
197
  const counterPath = path.join(repoPath, '.eck', 'update_seq');
@@ -179,39 +207,39 @@ export async function updateSnapshotJson(repoPath) {
179
207
  await fs.writeFile(counterPath, `${anchor.substring(0, 7)}:${seqNum}`);
180
208
  } catch (e) {}
181
209
 
182
- const timestamp = generateTimestamp();
183
- const shortRepoName = getShortRepoName(path.basename(repoPath));
184
- const outputFilename = `eck${shortRepoName}${timestamp}_${anchor.substring(0, 7)}_up${seqNum}.md`;
185
- const outputPath = path.join(repoPath, '.eck', 'snapshots', outputFilename);
186
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
187
- await fs.writeFile(outputPath, fullContent);
188
-
189
- // --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
190
- try {
191
- const snapDir = path.join(repoPath, '.eck', 'lastsnapshot');
192
- await fs.mkdir(snapDir, { recursive: true });
193
-
194
- // 1. Clean up OLD snapshots
195
- const existingFiles = await fs.readdir(snapDir);
196
- for (const file of existingFiles) {
197
- if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
198
- await fs.unlink(path.join(snapDir, file));
199
- }
200
- }
201
-
202
- // 2. Save new file
203
- await fs.writeFile(path.join(snapDir, outputFilename), fullContent);
204
- } catch (e) {
205
- // Non-critical failure
206
- }
207
- // --------------------------------------------
208
-
209
- console.log(JSON.stringify({
210
- status: "success",
211
- snapshot_file: `.eck/snapshots/${outputFilename}`,
212
- files_count: includedCount,
213
- timestamp: timestamp
214
- }));
210
+ const timestamp = generateTimestamp();
211
+ const shortRepoName = getShortRepoName(path.basename(repoPath));
212
+ const outputFilename = `eck${shortRepoName}${timestamp}_${anchor.substring(0, 7)}_up${seqNum}.md`;
213
+ const outputPath = path.join(repoPath, '.eck', 'snapshots', outputFilename);
214
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
215
+ await fs.writeFile(outputPath, fullContent);
216
+
217
+ // --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
218
+ try {
219
+ const snapDir = path.join(repoPath, '.eck', 'lastsnapshot');
220
+ await fs.mkdir(snapDir, { recursive: true });
221
+
222
+ // 1. Clean up OLD snapshots
223
+ const existingFiles = await fs.readdir(snapDir);
224
+ for (const file of existingFiles) {
225
+ if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
226
+ await fs.unlink(path.join(snapDir, file));
227
+ }
228
+ }
229
+
230
+ // 2. Save new file
231
+ await fs.writeFile(path.join(snapDir, outputFilename), fullContent);
232
+ } catch (e) {
233
+ // Non-critical failure
234
+ }
235
+ // --------------------------------------------
236
+
237
+ console.log(JSON.stringify({
238
+ status: "success",
239
+ snapshot_file: `.eck/snapshots/${outputFilename}`,
240
+ files_count: includedCount,
241
+ timestamp: timestamp
242
+ }));
215
243
 
216
244
  } catch (error) {
217
245
  console.log(JSON.stringify({ status: "error", message: error.message }));
package/src/config.js CHANGED
@@ -50,16 +50,6 @@ function validateConfigSchema(config) {
50
50
  warnings.push('Missing "aiInstructions" section');
51
51
  }
52
52
 
53
- // Legacy support
54
- if (!config.filesToIgnore || !Array.isArray(config.filesToIgnore)) {
55
- warnings.push('filesToIgnore missing or not an array - using defaults');
56
- config.filesToIgnore = DEFAULT_CONFIG.filesToIgnore;
57
- }
58
- if (!config.dirsToIgnore || !Array.isArray(config.dirsToIgnore)) {
59
- warnings.push('dirsToIgnore missing or not an array - using defaults');
60
- config.dirsToIgnore = DEFAULT_CONFIG.dirsToIgnore;
61
- }
62
-
63
53
  if (warnings.length > 0) {
64
54
  console.warn('\nāš ļø Config Validation Warnings:');
65
55
  warnings.forEach(w => console.warn(` - ${w}`));
@@ -34,9 +34,16 @@ export async function getChangedFiles(repoPath, anchorHash) {
34
34
  }
35
35
  }
36
36
 
37
- export async function getGitDiffOutput(repoPath, anchorHash) {
37
+ export async function getGitDiffOutput(repoPath, anchorHash, excludeFiles = []) {
38
38
  try {
39
- const { stdout } = await execa('git', ['diff', anchorHash, 'HEAD'], { cwd: repoPath });
39
+ const args = ['diff', anchorHash, 'HEAD'];
40
+ if (excludeFiles.length > 0) {
41
+ args.push('--');
42
+ for (const file of excludeFiles) {
43
+ args.push(`:(exclude)${file}`);
44
+ }
45
+ }
46
+ const { stdout } = await execa('git', args, { cwd: repoPath });
40
47
  return stdout;
41
48
  } catch (e) {
42
49
  return '';