@xelth/eck-snapshot 4.2.4 → 5.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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/index.js +14 -0
  4. package/package.json +64 -9
  5. package/scripts/mcp-eck-core.js +101 -0
  6. package/scripts/mcp-glm-zai-worker.mjs +243 -0
  7. package/scripts/verify_changes.js +68 -0
  8. package/setup.json +845 -0
  9. package/src/cli/cli.js +369 -0
  10. package/src/cli/commands/claudeSettings.js +93 -0
  11. package/src/cli/commands/consilium.js +86 -0
  12. package/src/cli/commands/createSnapshot.js +906 -0
  13. package/src/cli/commands/detectProfiles.js +98 -0
  14. package/src/cli/commands/detectProject.js +112 -0
  15. package/src/cli/commands/doctor.js +60 -0
  16. package/src/cli/commands/envSync.js +319 -0
  17. package/src/cli/commands/generateProfileGuide.js +144 -0
  18. package/src/cli/commands/pruneSnapshot.js +106 -0
  19. package/src/cli/commands/restoreSnapshot.js +173 -0
  20. package/src/cli/commands/setupGemini.js +149 -0
  21. package/src/cli/commands/setupGemini.test.js +115 -0
  22. package/src/cli/commands/setupMcp.js +269 -0
  23. package/src/cli/commands/showFile.js +39 -0
  24. package/src/cli/commands/trainTokens.js +38 -0
  25. package/src/cli/commands/updateSnapshot.js +219 -0
  26. package/src/config.js +125 -0
  27. package/src/core/skeletonizer.js +201 -0
  28. package/src/mcp-server/index.js +211 -0
  29. package/src/services/claudeCliService.js +626 -0
  30. package/src/services/claudeCliService.test.js +267 -0
  31. package/src/templates/agent-prompt.template.md +43 -0
  32. package/src/templates/architect-prompt.template.md +164 -0
  33. package/src/templates/claude-code/README.md +105 -0
  34. package/src/templates/claude-code/mcp-config-template.json +11 -0
  35. package/src/templates/claude-code/mcp-server-template.js +206 -0
  36. package/src/templates/claude-code/settings-claude.json +1 -0
  37. package/src/templates/envScanRequest.md +4 -0
  38. package/src/templates/gitWorkflow.md +32 -0
  39. package/src/templates/multiAgent.md +118 -0
  40. package/src/templates/opencode/coder.template.md +22 -0
  41. package/src/templates/opencode/junior-architect.template.md +85 -0
  42. package/src/templates/skeleton-instruction.md +16 -0
  43. package/src/templates/update-prompt.template.md +19 -0
  44. package/src/utils/aiHeader.js +678 -0
  45. package/src/utils/claudeMdGenerator.js +148 -0
  46. package/src/utils/eckProtocolParser.js +221 -0
  47. package/src/utils/fileUtils.js +1017 -0
  48. package/src/utils/gitUtils.js +44 -0
  49. package/src/utils/opencodeAgentsGenerator.js +271 -0
  50. package/src/utils/projectDetector.js +704 -0
  51. package/src/utils/tokenEstimator.js +201 -0
@@ -0,0 +1,269 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import os from 'os';
6
+ import { execa } from 'execa';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ /**
13
+ * Setup / Restore MCP servers for Claude Code and OpenCode.
14
+ * Registers:
15
+ * 1. eck-core (eck_finish_task) - commit + snapshot
16
+ * 2. glm-zai (glm_zai_*) - GLM-4.7 coding workers
17
+ *
18
+ * Usage:
19
+ * eck-snapshot setup-mcp # Auto-detect and register for Claude Code
20
+ * eck-snapshot setup-mcp --opencode # Register for OpenCode
21
+ * eck-snapshot setup-mcp --both # Register for both
22
+ */
23
+ export async function setupMcp(options = {}) {
24
+ const packageRoot = path.resolve(__dirname, '../../..');
25
+ const eckCorePath = path.join(packageRoot, 'scripts', 'mcp-eck-core.js');
26
+ const glmZaiPath = path.join(packageRoot, 'scripts', 'mcp-glm-zai-worker.mjs');
27
+ const mcpServerPath = path.join(packageRoot, 'src', 'mcp-server', 'index.js');
28
+
29
+ const targets = [];
30
+ if (options.opencode && !options.both) {
31
+ targets.push('opencode');
32
+ } else if (options.both) {
33
+ targets.push('claude', 'opencode');
34
+ } else {
35
+ targets.push('claude');
36
+ }
37
+
38
+ console.log(chalk.blue.bold('\nšŸ”§ EckSnapshot MCP Setup\n'));
39
+
40
+ for (const target of targets) {
41
+ if (target === 'claude') {
42
+ await setupForClaude(packageRoot, eckCorePath, glmZaiPath, mcpServerPath, options);
43
+ } else {
44
+ await setupForOpenCode(packageRoot, eckCorePath, glmZaiPath, options);
45
+ }
46
+ }
47
+
48
+ // Print summary
49
+ console.log(chalk.green.bold('\nāœ… MCP Setup Complete!\n'));
50
+ console.log(chalk.white('Registered MCP servers:'));
51
+ console.log(chalk.cyan(' 1. eck-core') + chalk.gray(' → eck_finish_task (commit + snapshot)'));
52
+ console.log(chalk.cyan(' 2. glm-zai') + chalk.gray(' → glm_zai_backend, glm_zai_frontend, glm_zai_qa, glm_zai_refactor, glm_zai_general'));
53
+ console.log('');
54
+ console.log(chalk.yellow('Requirements:'));
55
+ console.log(chalk.white(' • ZAI_API_KEY environment variable must be set for GLM Z.AI workers'));
56
+ console.log(chalk.white(' • Get your key at https://z.ai'));
57
+ console.log('');
58
+ console.log(chalk.yellow('Next steps:'));
59
+ console.log(chalk.white(' 1. Restart your AI coding tool (Claude Code / OpenCode)'));
60
+ console.log(chalk.white(' 2. The tools will be available automatically'));
61
+ console.log(chalk.white(' 3. Use --jas or --jao flags to generate CLAUDE.md with delegation protocol'));
62
+ console.log('');
63
+ }
64
+
65
+ /**
66
+ * Register MCP servers for Claude Code using `claude mcp add`
67
+ */
68
+ async function setupForClaude(packageRoot, eckCorePath, glmZaiPath, mcpServerPath, options) {
69
+ const spinner = ora();
70
+
71
+ console.log(chalk.blue('šŸ“¦ Setting up for Claude Code...\n'));
72
+
73
+ // Method 1: Try `claude mcp add` commands (preferred)
74
+ let usedCliRegistration = false;
75
+
76
+ try {
77
+ spinner.start('Checking if `claude` CLI is available...');
78
+ await execa('claude', ['--version']);
79
+ spinner.succeed('Claude CLI found');
80
+
81
+ // Helper to register a single MCP server via claude CLI
82
+ async function registerClaudeMcp(name, scriptPath) {
83
+ spinner.start(`Registering ${name} MCP server...`);
84
+ try {
85
+ await execa('claude', ['mcp', 'add', name, '--', 'node', scriptPath]);
86
+ spinner.succeed(`${name} registered`);
87
+ } catch (e) {
88
+ // May already exist - try remove then add
89
+ try {
90
+ await execa('claude', ['mcp', 'remove', name]);
91
+ await execa('claude', ['mcp', 'add', name, '--', 'node', scriptPath]);
92
+ spinner.succeed(`${name} re-registered`);
93
+ } catch (e2) {
94
+ spinner.warn(`${name} registration via CLI failed: ${e2.message}`);
95
+ }
96
+ }
97
+ }
98
+
99
+ await registerClaudeMcp('eck-core', eckCorePath);
100
+ await registerClaudeMcp('glm-zai', glmZaiPath);
101
+ await registerClaudeMcp('ecksnapshot', mcpServerPath);
102
+
103
+ usedCliRegistration = true;
104
+ } catch {
105
+ spinner.info('Claude CLI not found, will use config file method');
106
+ }
107
+
108
+ // Method 2: Also update the config file as fallback / alternative
109
+ spinner.start('Updating Claude Code config file...');
110
+
111
+ const homeDir = os.homedir();
112
+ const platform = process.platform;
113
+
114
+ let possibleConfigPaths = [];
115
+ if (platform === 'win32') {
116
+ const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
117
+ possibleConfigPaths = [
118
+ path.join(appData, 'Claude', 'config.json'),
119
+ path.join(homeDir, '.claude', 'config.json'),
120
+ path.join(homeDir, '.config', 'claude', 'config.json'),
121
+ ];
122
+ } else {
123
+ possibleConfigPaths = [
124
+ path.join(homeDir, '.config', 'claude', 'config.json'),
125
+ path.join(homeDir, '.claude', 'config.json'),
126
+ ];
127
+ }
128
+
129
+ let claudeConfigPath = null;
130
+ for (const configPath of possibleConfigPaths) {
131
+ try {
132
+ await fs.access(configPath);
133
+ claudeConfigPath = configPath;
134
+ break;
135
+ } catch { /* continue */ }
136
+ }
137
+
138
+ if (!claudeConfigPath) {
139
+ claudeConfigPath = possibleConfigPaths[0];
140
+ }
141
+
142
+ let config = {};
143
+ try {
144
+ const content = await fs.readFile(claudeConfigPath, 'utf-8');
145
+ config = JSON.parse(content);
146
+ } catch { /* new config */ }
147
+
148
+ if (!config.mcpServers) config.mcpServers = {};
149
+
150
+ config.mcpServers.ecksnapshot = {
151
+ command: 'node',
152
+ args: [mcpServerPath],
153
+ env: {},
154
+ };
155
+
156
+ config.mcpServers['eck-core'] = {
157
+ command: 'node',
158
+ args: [eckCorePath],
159
+ env: {},
160
+ };
161
+
162
+ config.mcpServers['glm-zai'] = {
163
+ command: 'node',
164
+ args: [glmZaiPath],
165
+ env: {},
166
+ };
167
+
168
+ // Remove old minimax-worker if present
169
+ if (config.mcpServers['minimax-worker']) {
170
+ delete config.mcpServers['minimax-worker'];
171
+ console.log(chalk.gray(' Removed old minimax-worker MCP server'));
172
+ }
173
+
174
+ await fs.mkdir(path.dirname(claudeConfigPath), { recursive: true });
175
+ await fs.writeFile(claudeConfigPath, JSON.stringify(config, null, 2));
176
+
177
+ spinner.succeed(`Config saved: ${chalk.cyan(claudeConfigPath)}`);
178
+
179
+ // Also update the local .eck/claude-mcp-config.json
180
+ const localConfigPath = path.join(process.cwd(), '.eck', 'claude-mcp-config.json');
181
+ const localConfig = {
182
+ mcpServers: {
183
+ ecksnapshot: config.mcpServers.ecksnapshot,
184
+ 'eck-core': config.mcpServers['eck-core'],
185
+ 'glm-zai': config.mcpServers['glm-zai'],
186
+ },
187
+ };
188
+
189
+ try {
190
+ await fs.mkdir(path.dirname(localConfigPath), { recursive: true });
191
+ await fs.writeFile(localConfigPath, JSON.stringify(localConfig, null, 2));
192
+ spinner.succeed(`Local config updated: ${chalk.cyan(localConfigPath)}`);
193
+ } catch (e) {
194
+ spinner.warn(`Could not update local config: ${e.message}`);
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Register MCP servers for OpenCode.
200
+ * OpenCode uses opencode.json at project root with its own format:
201
+ * { "mcp": { "name": { "type": "local", "command": [...], "enabled": true } } }
202
+ * The CLI (`opencode mcp add`) is interactive (TUI), so we write the config directly.
203
+ */
204
+ async function setupForOpenCode(packageRoot, eckCorePath, glmZaiPath, options) {
205
+ const spinner = ora();
206
+
207
+ console.log(chalk.blue('šŸ“¦ Setting up for OpenCode...\n'));
208
+
209
+ const configPath = path.join(process.cwd(), 'opencode.json');
210
+
211
+ spinner.start('Updating OpenCode config (opencode.json)...');
212
+
213
+ // Read existing config or create new
214
+ let config = {};
215
+ try {
216
+ const content = await fs.readFile(configPath, 'utf-8');
217
+ config = JSON.parse(content);
218
+ } catch { /* new config */ }
219
+
220
+ // Ensure $schema and base structure
221
+ if (!config.$schema) {
222
+ config.$schema = 'https://opencode.ai/config.json';
223
+ }
224
+ if (!config.mcp) {
225
+ config.mcp = {};
226
+ }
227
+
228
+ // Register eck-core
229
+ config.mcp['eck-core'] = {
230
+ type: 'local',
231
+ command: ['node', eckCorePath],
232
+ enabled: true,
233
+ timeout: 30000,
234
+ };
235
+
236
+ // Register glm-zai
237
+ config.mcp['glm-zai'] = {
238
+ type: 'local',
239
+ command: ['node', glmZaiPath],
240
+ enabled: true,
241
+ timeout: 120000,
242
+ };
243
+
244
+ // Preserve ZAI_API_KEY in environment if it was set before
245
+ if (process.env.ZAI_API_KEY) {
246
+ config.mcp['glm-zai'].environment = {
247
+ ZAI_API_KEY: process.env.ZAI_API_KEY,
248
+ };
249
+ }
250
+
251
+ // Remove old minimax entries if present
252
+ if (config.mcp['minimax-worker']) {
253
+ delete config.mcp['minimax-worker'];
254
+ console.log(chalk.gray(' Removed old minimax-worker from opencode.json'));
255
+ }
256
+
257
+ // Ensure AGENTS.md is in instructions
258
+ if (!config.instructions) {
259
+ config.instructions = ['AGENTS.md'];
260
+ } else if (!config.instructions.includes('AGENTS.md')) {
261
+ config.instructions.push('AGENTS.md');
262
+ }
263
+
264
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
265
+ spinner.succeed(`OpenCode config updated: ${chalk.cyan(configPath)}`);
266
+
267
+ console.log(chalk.gray('\n OpenCode will read MCP servers from opencode.json on next start.'));
268
+ console.log(chalk.gray(' Use `eck-snapshot --jas` or `--jao` to generate AGENTS.md for OpenCode.\n'));
269
+ }
@@ -0,0 +1,39 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ /**
6
+ * Show the full content of specific files
7
+ * Used for AI lazy loading when skeleton mode is active
8
+ * @param {string[]} filePaths - Array of paths to the files to display
9
+ */
10
+ export async function showFile(filePaths) {
11
+ // Ensure input is array (commander passes array for variadic args)
12
+ const files = Array.isArray(filePaths) ? filePaths : [filePaths];
13
+
14
+ if (files.length === 0) {
15
+ console.error(chalk.yellow('No files specified. Usage: eck-snapshot show <file1> [file2] ...'));
16
+ return;
17
+ }
18
+
19
+ for (const filePath of files) {
20
+ try {
21
+ const fullPath = path.resolve(process.cwd(), filePath);
22
+ const content = await fs.readFile(fullPath, 'utf-8');
23
+
24
+ console.log(chalk.green(`\n--- FULL CONTENT: ${filePath} ---\n`));
25
+
26
+ // Detect file extension for syntax highlighting hint
27
+ const ext = path.extname(filePath).slice(1);
28
+ console.log('```' + ext);
29
+ console.log(content);
30
+ console.log('```');
31
+
32
+ console.log(chalk.green(`\n--- END OF FILE: ${filePath} ---\n`));
33
+
34
+ } catch (error) {
35
+ console.error(chalk.red(`Failed to read file ${filePath}: ${error.message}`));
36
+ // Continue to next file even if one fails
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,38 @@
1
+ import { addTrainingPoint, showEstimationStats } from '../../utils/tokenEstimator.js';
2
+
3
+ /**
4
+ * Train token estimation with actual results
5
+ * @param {string} projectType - Type of project (android, nodejs, etc.)
6
+ * @param {string} fileSizeStr - File size in bytes
7
+ * @param {string} estimatedStr - Estimated tokens
8
+ * @param {string} actualStr - Actual tokens (from user input)
9
+ */
10
+ export async function trainTokens(projectType, fileSizeStr, estimatedStr, actualStr) {
11
+ try {
12
+ const fileSizeInBytes = parseInt(fileSizeStr, 10);
13
+ const estimatedTokens = parseInt(estimatedStr, 10);
14
+
15
+ // Parse actual tokens from user input (remove any text like "tokens", commas, etc.)
16
+ const actualTokens = parseInt(actualStr.replace(/[^\d]/g, ''), 10);
17
+
18
+ if (isNaN(fileSizeInBytes) || isNaN(estimatedTokens) || isNaN(actualTokens)) {
19
+ throw new Error('Invalid numeric values provided');
20
+ }
21
+
22
+ await addTrainingPoint(projectType, fileSizeInBytes, estimatedTokens, actualTokens);
23
+
24
+ console.log('\nšŸ“ˆ Updated polynomial coefficients for improved estimation.');
25
+
26
+ } catch (error) {
27
+ console.error(`āŒ Error training token estimation: ${error.message}`);
28
+ console.error('Usage: eck-snapshot train-tokens <project-type> <file-size-bytes> <estimated-tokens> <actual-tokens>');
29
+ process.exit(1);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Show token estimation statistics
35
+ */
36
+ export async function showTokenStats() {
37
+ await showEstimationStats();
38
+ }
@@ -0,0 +1,219 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import ora from 'ora';
4
+ import chalk from 'chalk';
5
+ import { getGitAnchor, getChangedFiles, getGitDiffOutput } from '../../utils/gitUtils.js';
6
+ import { loadSetupConfig } from '../../config.js';
7
+ import { readFileWithSizeCheck, parseSize, formatSize, matchesPattern, loadGitignore, generateTimestamp, getShortRepoName } from '../../utils/fileUtils.js';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ // Shared logic to generate the snapshot content string
14
+ async function generateSnapshotContent(repoPath, changedFiles, anchor, config, gitignore) {
15
+ let contentOutput = '';
16
+ let includedCount = 0;
17
+ const fileList = [];
18
+
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 */ }
28
+
29
+ for (const filePath of changedFiles) {
30
+ if (config.dirsToIgnore.some(d => filePath.startsWith(d))) continue;
31
+ if (gitignore.ignores(filePath) && filePath !== '.eck/lastsnapshot/AnswerToSA.md') continue;
32
+
33
+ try {
34
+ const fullPath = path.join(repoPath, filePath);
35
+ const content = await readFileWithSizeCheck(fullPath, parseSize(config.maxFileSize));
36
+ contentOutput += `--- File: /${filePath} ---\n\n${content}\n\n`;
37
+ fileList.push(`- ${filePath}`);
38
+ includedCount++;
39
+ } catch (e) { /* Skip */ }
40
+ }
41
+
42
+ // Load Template
43
+ const templatePath = path.join(__dirname, '../../templates/update-prompt.template.md');
44
+ let header = await fs.readFile(templatePath, 'utf-8');
45
+
46
+ // Inject Agent Report
47
+ let reportSection = '';
48
+ if (agentReport) {
49
+ reportSection = `\n#######################################################\n# šŸ“Ø MESSAGE FROM EXECUTION AGENT (Claude)\n#######################################################\n${agentReport}\n#######################################################\n\n`;
50
+ }
51
+
52
+ header = header.replace('{{anchor}}', anchor.substring(0, 7))
53
+ .replace('{{timestamp}}', new Date().toLocaleString())
54
+ .replace('{{fileList}}', fileList.join('\n'));
55
+
56
+ header = reportSection + header;
57
+
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
+ };
67
+ }
68
+
69
+ export async function updateSnapshot(repoPath, options) {
70
+ const spinner = ora('Generating update snapshot...').start();
71
+ try {
72
+ const anchor = await getGitAnchor(repoPath);
73
+ if (!anchor) {
74
+ throw new Error('No snapshot anchor found. Run a full snapshot first: eck-snapshot snapshot');
75
+ }
76
+
77
+ const changedFiles = await getChangedFiles(repoPath, anchor);
78
+ if (changedFiles.length === 0) {
79
+ spinner.succeed('No changes detected since last full snapshot.');
80
+ return;
81
+ }
82
+
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);
88
+
89
+ // Determine sequence number
90
+ let seqNum = 1;
91
+ const counterPath = path.join(repoPath, '.eck', 'update_seq');
92
+ try {
93
+ const seqData = await fs.readFile(counterPath, 'utf-8');
94
+ const [savedHash, savedCount] = seqData.split(':');
95
+ if (savedHash && savedHash.trim() === anchor.substring(0, 7).trim()) {
96
+ seqNum = parseInt(savedCount || '0') + 1;
97
+ }
98
+ } catch (e) {}
99
+
100
+ try {
101
+ await fs.writeFile(counterPath, `${anchor.substring(0, 7)}:${seqNum}`);
102
+ } catch (e) {}
103
+
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.`);
141
+
142
+ } catch (error) {
143
+ spinner.fail(`Update failed: ${error.message}`);
144
+ }
145
+ }
146
+
147
+ // New Silent/JSON command for Agents
148
+ export async function updateSnapshotJson(repoPath) {
149
+ try {
150
+ const anchor = await getGitAnchor(repoPath);
151
+ if (!anchor) {
152
+ console.log(JSON.stringify({ status: "error", message: "No snapshot anchor found" }));
153
+ return;
154
+ }
155
+
156
+ const changedFiles = await getChangedFiles(repoPath, anchor);
157
+ if (changedFiles.length === 0) {
158
+ console.log(JSON.stringify({ status: "no_changes", message: "No changes detected" }));
159
+ return;
160
+ }
161
+
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);
167
+
168
+ let seqNum = 1;
169
+ const counterPath = path.join(repoPath, '.eck', 'update_seq');
170
+ try {
171
+ const seqData = await fs.readFile(counterPath, 'utf-8');
172
+ const [savedHash, savedCount] = seqData.split(':');
173
+ if (savedHash && savedHash.trim() === anchor.substring(0, 7).trim()) {
174
+ seqNum = parseInt(savedCount || '0') + 1;
175
+ }
176
+ } catch (e) {}
177
+
178
+ try {
179
+ await fs.writeFile(counterPath, `${anchor.substring(0, 7)}:${seqNum}`);
180
+ } catch (e) {}
181
+
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
+ }));
215
+
216
+ } catch (error) {
217
+ console.log(JSON.stringify({ status: "error", message: error.message }));
218
+ }
219
+ }
package/src/config.js ADDED
@@ -0,0 +1,125 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ let cachedConfig = null;
9
+
10
+ export async function loadSetupConfig() {
11
+ if (cachedConfig) {
12
+ return cachedConfig;
13
+ }
14
+
15
+ try {
16
+ const setupPath = path.join(__dirname, '..', 'setup.json');
17
+ const setupContent = await fs.readFile(setupPath, 'utf-8');
18
+ cachedConfig = JSON.parse(setupContent);
19
+
20
+ // Basic schema validation for critical fields
21
+ validateConfigSchema(cachedConfig);
22
+
23
+ return cachedConfig;
24
+ } catch (error) {
25
+ console.error('Error loading setup.json:', error.message);
26
+ throw new Error('Failed to load setup.json configuration file');
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Validates critical config fields and warns if missing or invalid
32
+ */
33
+ function validateConfigSchema(config) {
34
+ const warnings = [];
35
+
36
+ // Validate fileFiltering section
37
+ if (!config.fileFiltering) {
38
+ warnings.push('Missing "fileFiltering" section');
39
+ } else {
40
+ if (!Array.isArray(config.fileFiltering.filesToIgnore)) {
41
+ warnings.push('"fileFiltering.filesToIgnore" must be an array');
42
+ }
43
+ if (!Array.isArray(config.fileFiltering.dirsToIgnore)) {
44
+ warnings.push('"fileFiltering.dirsToIgnore" must be an array');
45
+ }
46
+ }
47
+
48
+ // Validate aiInstructions section
49
+ if (!config.aiInstructions) {
50
+ warnings.push('Missing "aiInstructions" section');
51
+ }
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
+ if (warnings.length > 0) {
64
+ console.warn('\nāš ļø Config Validation Warnings:');
65
+ warnings.forEach(w => console.warn(` - ${w}`));
66
+ console.warn(' (Falling back to defaults for missing values where possible)\n');
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Loads and merges all profiles (local-first).
72
+ */
73
+ export async function getAllProfiles(repoPath) {
74
+ const globalConfig = await loadSetupConfig();
75
+ const globalProfiles = globalConfig.contextProfiles || {};
76
+
77
+ let localProfiles = {};
78
+ const localProfilePath = path.join(repoPath, '.eck', 'profiles.json');
79
+
80
+ try {
81
+ const localProfileContent = await fs.readFile(localProfilePath, 'utf-8');
82
+ localProfiles = JSON.parse(localProfileContent);
83
+ } catch (e) {
84
+ // No local profiles.json found, which is fine.
85
+ }
86
+
87
+ // Local profiles override global profiles
88
+ return { ...globalProfiles, ...localProfiles };
89
+ }
90
+
91
+ /**
92
+ * Smart profile loader (Step 2 of dynamic profiles).
93
+ * Reads local .eck/profiles.json first, then falls back to global setup.json profiles.
94
+ */
95
+ export async function getProfile(profileName, repoPath) {
96
+ const globalConfig = await loadSetupConfig();
97
+ const globalProfiles = globalConfig.contextProfiles || {};
98
+
99
+ let localProfiles = {};
100
+ const localProfilePath = path.join(repoPath, '.eck', 'profiles.json');
101
+
102
+ try {
103
+ const localProfileContent = await fs.readFile(localProfilePath, 'utf-8');
104
+ localProfiles = JSON.parse(localProfileContent);
105
+ } catch (e) {
106
+ // No local profiles.json found, which is fine. We just use globals.
107
+ }
108
+
109
+ // Local profiles override global profiles
110
+ const allProfiles = { ...globalProfiles, ...localProfiles };
111
+
112
+ return allProfiles[profileName] || null;
113
+ }
114
+
115
+ // Fallback default config for backwards compatibility
116
+ export const DEFAULT_CONFIG = {
117
+ smartModeTokenThreshold: 200000,
118
+ filesToIgnore: ['package-lock.json', '*.log', 'yarn.lock'],
119
+ extensionsToIgnore: ['.sqlite3', '.db', '.DS_Store', '.env', '.pyc'],
120
+ dirsToIgnore: ['node_modules/', '.git/', 'dist/', 'build/'],
121
+ maxFileSize: '10MB',
122
+ maxTotalSize: '100MB',
123
+ maxDepth: 10,
124
+ concurrency: 10
125
+ };