@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
@@ -1,27 +1,49 @@
1
1
  import fs from 'fs/promises';
2
+ import fsSync from 'fs';
2
3
  import path from 'path';
3
4
  import chalk from 'chalk';
4
5
  import ora from 'ora';
5
6
  import os from 'os';
6
7
  import { execa } from 'execa';
7
8
  import { fileURLToPath } from 'url';
9
+ import { ensureSnapshotsInGitignore } from '../../utils/fileUtils.js';
8
10
 
9
11
  const __filename = fileURLToPath(import.meta.url);
10
12
  const __dirname = path.dirname(__filename);
11
13
 
12
14
  /**
13
- * Setup / Restore MCP servers for Claude Code and OpenCode.
15
+ * Walk up from cwd to find the project root (directory containing .git or package.json).
16
+ * Falls back to cwd if no marker is found.
17
+ */
18
+ function findProjectRoot() {
19
+ let dir = process.cwd();
20
+ const root = path.parse(dir).root;
21
+ while (dir !== root) {
22
+ try {
23
+ const entries = fsSync.readdirSync(dir);
24
+ if (entries.includes('.git') || entries.includes('package.json')) {
25
+ return dir;
26
+ }
27
+ } catch { /* unreadable dir, keep going */ }
28
+ dir = path.dirname(dir);
29
+ }
30
+ return process.cwd();
31
+ }
32
+
33
+ /**
34
+ * Setup / Restore MCP servers for Claude Code, OpenCode, and Codex.
14
35
  * Registers:
15
36
  * 1. eck-core (eck_finish_task) - commit + snapshot
16
37
  * 2. glm-zai (glm_zai_*) - GLM-4.7 coding workers
17
38
  *
18
39
  * Usage:
19
- * eck-snapshot setup-mcp # Auto-detect and register for Claude Code
40
+ * eck-snapshot setup-mcp # Auto-detect and register for Claude Code & Codex
20
41
  * eck-snapshot setup-mcp --opencode # Register for OpenCode
21
- * eck-snapshot setup-mcp --both # Register for both
42
+ * eck-snapshot setup-mcp --both # Register for all
22
43
  */
23
44
  export async function setupMcp(options = {}) {
24
45
  const packageRoot = path.resolve(__dirname, '../../..');
46
+ const projectRoot = findProjectRoot();
25
47
  const eckCorePath = path.join(packageRoot, 'scripts', 'mcp-eck-core.js');
26
48
  const glmZaiPath = path.join(packageRoot, 'scripts', 'mcp-glm-zai-worker.mjs');
27
49
  const targets = [];
@@ -34,15 +56,30 @@ export async function setupMcp(options = {}) {
34
56
  }
35
57
 
36
58
  console.log(chalk.blue.bold('\nšŸ”§ EckSnapshot MCP Setup\n'));
59
+ if (projectRoot !== process.cwd()) {
60
+ console.log(chalk.gray(` Project root: ${projectRoot}\n`));
61
+ }
37
62
 
38
63
  for (const target of targets) {
39
64
  if (target === 'claude') {
40
- await setupForClaude(packageRoot, eckCorePath, glmZaiPath, options);
65
+ await setupForClaude(packageRoot, eckCorePath, glmZaiPath, options, projectRoot);
41
66
  } else {
42
- await setupForOpenCode(packageRoot, eckCorePath, glmZaiPath, options);
67
+ await setupForOpenCode(packageRoot, eckCorePath, glmZaiPath, options, projectRoot);
43
68
  }
44
69
  }
45
70
 
71
+ // Auto-detect Codex
72
+ const codexDir = path.join(projectRoot, '.codex');
73
+ let hasCodex = false;
74
+ try {
75
+ await fs.access(codexDir);
76
+ hasCodex = true;
77
+ } catch {}
78
+
79
+ if (hasCodex) {
80
+ await setupForCodex(packageRoot, eckCorePath, glmZaiPath, options, projectRoot);
81
+ }
82
+
46
83
  // Print summary
47
84
  console.log(chalk.green.bold('\nāœ… MCP Setup Complete!\n'));
48
85
  console.log(chalk.white('Registered MCP servers:'));
@@ -54,16 +91,15 @@ export async function setupMcp(options = {}) {
54
91
  console.log(chalk.white(' • Get your key at https://z.ai'));
55
92
  console.log('');
56
93
  console.log(chalk.yellow('Next steps:'));
57
- console.log(chalk.white(' 1. Restart your AI coding tool (Claude Code / OpenCode)'));
94
+ console.log(chalk.white(' 1. Restart your AI coding tool'));
58
95
  console.log(chalk.white(' 2. The tools will be available automatically'));
59
- console.log(chalk.white(' 3. Use --jas or --jao flags to generate CLAUDE.md with delegation protocol'));
60
96
  console.log('');
61
97
  }
62
98
 
63
99
  /**
64
100
  * Register MCP servers for Claude Code using `claude mcp add`
65
101
  */
66
- async function setupForClaude(packageRoot, eckCorePath, glmZaiPath, options) {
102
+ async function setupForClaude(packageRoot, eckCorePath, glmZaiPath, options, projectRoot) {
67
103
  const spinner = ora();
68
104
 
69
105
  console.log(chalk.blue('šŸ“¦ Setting up for Claude Code...\n'));
@@ -173,7 +209,7 @@ async function setupForClaude(packageRoot, eckCorePath, glmZaiPath, options) {
173
209
  spinner.succeed(`Config saved: ${chalk.cyan(claudeConfigPath)}`);
174
210
 
175
211
  // Also update the local .eck/claude-mcp-config.json
176
- const localConfigPath = path.join(process.cwd(), '.eck', 'claude-mcp-config.json');
212
+ const localConfigPath = path.join(projectRoot, '.eck', 'claude-mcp-config.json');
177
213
  const localConfig = {
178
214
  mcpServers: {
179
215
  'eck-core': config.mcpServers['eck-core'],
@@ -183,6 +219,7 @@ async function setupForClaude(packageRoot, eckCorePath, glmZaiPath, options) {
183
219
 
184
220
  try {
185
221
  await fs.mkdir(path.dirname(localConfigPath), { recursive: true });
222
+ await ensureSnapshotsInGitignore(projectRoot);
186
223
  await fs.writeFile(localConfigPath, JSON.stringify(localConfig, null, 2));
187
224
  spinner.succeed(`Local config updated: ${chalk.cyan(localConfigPath)}`);
188
225
  } catch (e) {
@@ -196,12 +233,12 @@ async function setupForClaude(packageRoot, eckCorePath, glmZaiPath, options) {
196
233
  * { "mcp": { "name": { "type": "local", "command": [...], "enabled": true } } }
197
234
  * The CLI (`opencode mcp add`) is interactive (TUI), so we write the config directly.
198
235
  */
199
- async function setupForOpenCode(packageRoot, eckCorePath, glmZaiPath, options) {
236
+ async function setupForOpenCode(packageRoot, eckCorePath, glmZaiPath, options, projectRoot) {
200
237
  const spinner = ora();
201
238
 
202
239
  console.log(chalk.blue('šŸ“¦ Setting up for OpenCode...\n'));
203
240
 
204
- const configPath = path.join(process.cwd(), 'opencode.json');
241
+ const configPath = path.join(projectRoot, 'opencode.json');
205
242
 
206
243
  spinner.start('Updating OpenCode config (opencode.json)...');
207
244
 
@@ -236,19 +273,38 @@ async function setupForOpenCode(packageRoot, eckCorePath, glmZaiPath, options) {
236
273
  timeout: 120000,
237
274
  };
238
275
 
239
- // Preserve ZAI_API_KEY in environment if it was set before
240
- if (process.env.ZAI_API_KEY) {
241
- config.mcp['glm-zai'].environment = {
242
- ZAI_API_KEY: process.env.ZAI_API_KEY,
243
- };
244
- }
245
-
246
276
  // Remove old minimax entries if present
247
277
  if (config.mcp['minimax-worker']) {
248
278
  delete config.mcp['minimax-worker'];
249
279
  console.log(chalk.gray(' Removed old minimax-worker from opencode.json'));
250
280
  }
251
281
 
282
+ // Add permissions only if not already configured
283
+ // List all tools explicitly with "allow", then "*" as fallback for future tools
284
+ // Order matters: last matching rule wins, so users can override specific tools
285
+ if (!config.permission) {
286
+ config.permission = {
287
+ "read": "allow",
288
+ "edit": "allow",
289
+ "glob": "allow",
290
+ "grep": "allow",
291
+ "list": "allow",
292
+ "bash": "allow",
293
+ "task": "allow",
294
+ "skill": "allow",
295
+ "lsp": "allow",
296
+ "todoread": "allow",
297
+ "todowrite": "allow",
298
+ "webfetch": "allow",
299
+ "websearch": "allow",
300
+ "codesearch": "allow",
301
+ "external_directory": "allow",
302
+ "doom_loop": "allow",
303
+ "*": "allow"
304
+ };
305
+ console.log(chalk.gray(' Added default permissions (allow all) - modify in opencode.json to restrict'));
306
+ }
307
+
252
308
  // Ensure AGENTS.md is in instructions
253
309
  if (!config.instructions) {
254
310
  config.instructions = ['AGENTS.md'];
@@ -260,5 +316,210 @@ async function setupForOpenCode(packageRoot, eckCorePath, glmZaiPath, options) {
260
316
  spinner.succeed(`OpenCode config updated: ${chalk.cyan(configPath)}`);
261
317
 
262
318
  console.log(chalk.gray('\n OpenCode will read MCP servers from opencode.json on next start.'));
263
- console.log(chalk.gray(' Use `eck-snapshot --jas` or `--jao` to generate AGENTS.md for OpenCode.\n'));
319
+ console.log(chalk.gray(' Use `eck-snapshot \'{"name": "eck_snapshot", "arguments": {"jas": true}}\'` to generate AGENTS.md.\n'));
320
+ }
321
+
322
+ /**
323
+ * Register MCP servers for Codex by appending to .codex/config.toml
324
+ */
325
+ async function setupForCodex(packageRoot, eckCorePath, glmZaiPath, options, projectRoot) {
326
+ const spinner = ora();
327
+ console.log(chalk.blue('šŸ“¦ Setting up for Codex...\n'));
328
+ spinner.start('Updating Codex config (.codex/config.toml)...');
329
+
330
+ try {
331
+ const updated = await ensureProjectCodexConfig(projectRoot);
332
+ if (updated) {
333
+ spinner.succeed(`Codex config updated: ${chalk.cyan('.codex/config.toml')}`);
334
+ } else {
335
+ spinner.info(`Codex config already up to date: ${chalk.cyan('.codex/config.toml')}`);
336
+ }
337
+ } catch (error) {
338
+ spinner.fail(`Failed to update Codex config: ${error.message}`);
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Silently ensure .codex/config.toml exists in target project root with eck-core configured.
344
+ * Called automatically during snapshot creation so any Codex session
345
+ * in that project will have eck_finish_task / eck_fail_task available.
346
+ *
347
+ * @param {string} repoPath - Target project root
348
+ * @returns {boolean} true if config was created/updated, false if already OK
349
+ */
350
+ export async function ensureProjectCodexConfig(repoPath) {
351
+ const codexDir = path.join(repoPath, '.codex');
352
+ try {
353
+ await fs.access(codexDir);
354
+ } catch {
355
+ return false; // No .codex directory, do nothing
356
+ }
357
+
358
+ const packageRoot = path.resolve(__dirname, '../../..');
359
+ const eckCorePath = path.join(packageRoot, 'scripts', 'mcp-eck-core.js');
360
+ const glmZaiPath = path.join(packageRoot, 'scripts', 'mcp-glm-zai-worker.mjs');
361
+ const configPath = path.join(codexDir, 'config.toml');
362
+
363
+ let content = '';
364
+ try {
365
+ content = await fs.readFile(configPath, 'utf-8');
366
+ } catch { /* file might not exist yet */ }
367
+
368
+ let updated = false;
369
+
370
+ // Simple string inclusion check (safe enough for TOML injection)
371
+ if (!content.includes('[mcp_servers.eck-core]')) {
372
+ content += `\n\n[mcp_servers.eck-core]\ncommand = "node"\nargs = ["${eckCorePath.replace(/\\/g, '\\\\')}"]\n`;
373
+ updated = true;
374
+ }
375
+
376
+ if (!content.includes('[mcp_servers.glm-zai]')) {
377
+ content += `\n\n[mcp_servers.glm-zai]\ncommand = "node"\nargs = ["${glmZaiPath.replace(/\\/g, '\\\\')}"]\n`;
378
+ updated = true;
379
+ }
380
+
381
+ if (updated) {
382
+ await fs.writeFile(configPath, content.trim() + '\n', 'utf-8');
383
+ return true;
384
+ }
385
+
386
+ return false;
387
+ }
388
+
389
+ /**
390
+ * Silently ensure .mcp.json exists in target project root with eck-core configured.
391
+ * Called automatically during snapshot creation so any Claude Code session
392
+ * in that project will have eck_finish_task / eck_fail_task available.
393
+ *
394
+ * @param {string} repoPath - Target project root
395
+ * @returns {boolean} true if file was created/updated, false if already OK
396
+ */
397
+ export async function ensureProjectMcpConfig(repoPath) {
398
+ const packageRoot = path.resolve(__dirname, '../../..');
399
+ const eckCorePath = path.join(packageRoot, 'scripts', 'mcp-eck-core.js');
400
+ const mcpJsonPath = path.join(repoPath, '.mcp.json');
401
+
402
+ // Read existing .mcp.json if present
403
+ let config = {};
404
+ try {
405
+ const content = await fs.readFile(mcpJsonPath, 'utf-8');
406
+ config = JSON.parse(content);
407
+ } catch { /* doesn't exist yet */ }
408
+
409
+ // Ensure root mcpServers key exists (required by Claude Code schema)
410
+ if (!config.mcpServers) {
411
+ config.mcpServers = {};
412
+ }
413
+
414
+ // Check if eck-core is already configured with correct path
415
+ if (config.mcpServers['eck-core'] &&
416
+ config.mcpServers['eck-core'].command === 'node' &&
417
+ config.mcpServers['eck-core'].args?.[0] === eckCorePath) {
418
+ return false; // Already up to date
419
+ }
420
+
421
+ // Add/update eck-core inside mcpServers
422
+ config.mcpServers['eck-core'] = {
423
+ command: 'node',
424
+ args: [eckCorePath]
425
+ };
426
+
427
+ await fs.writeFile(mcpJsonPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
428
+
429
+ // Ensure .mcp.json is in .gitignore (it contains absolute paths)
430
+ try {
431
+ const gitignorePath = path.join(repoPath, '.gitignore');
432
+ let gitignore = '';
433
+ try {
434
+ gitignore = await fs.readFile(gitignorePath, 'utf-8');
435
+ } catch { /* no .gitignore */ }
436
+
437
+ if (!gitignore.includes('.mcp.json')) {
438
+ const suffix = gitignore.endsWith('\n') || gitignore === '' ? '' : '\n';
439
+ await fs.writeFile(gitignorePath, gitignore + suffix + '.mcp.json\n', 'utf-8');
440
+ }
441
+ } catch { /* non-critical */ }
442
+
443
+ return true;
444
+ }
445
+
446
+ /**
447
+ * Silently ensure local opencode.json exists in target project root with eck-core configured.
448
+ * Called automatically during snapshot creation so any OpenCode session
449
+ * in that project will have eck_finish_task / eck_fail_task available.
450
+ *
451
+ * @param {string} repoPath - Target project root
452
+ * @returns {boolean} true if config was created/updated, false if already OK
453
+ */
454
+ export async function ensureProjectOpenCodeConfig(repoPath) {
455
+ const packageRoot = path.resolve(__dirname, '../../..');
456
+ const eckCorePath = path.join(packageRoot, 'scripts', 'mcp-eck-core.js');
457
+ const configPath = path.join(repoPath, 'opencode.json');
458
+
459
+ // Read existing config or create new
460
+ let config = {};
461
+ try {
462
+ const content = await fs.readFile(configPath, 'utf-8');
463
+ config = JSON.parse(content);
464
+ } catch { /* new config */ }
465
+
466
+ // Ensure mcp key exists
467
+ if (!config.mcp) {
468
+ config.mcp = {};
469
+ }
470
+
471
+ // Check if eck-core is already configured with correct path
472
+ if (config.mcp['eck-core'] &&
473
+ config.mcp['eck-core'].type === 'local' &&
474
+ config.mcp['eck-core'].command?.[0] === 'node' &&
475
+ config.mcp['eck-core'].command?.[1] === eckCorePath) {
476
+ // Still need to check instructions
477
+ if (config.instructions && config.instructions.includes('AGENTS.md')) {
478
+ return false; // Already up to date
479
+ }
480
+ } else {
481
+ // Add/update eck-core
482
+ config.mcp['eck-core'] = {
483
+ type: 'local',
484
+ command: ['node', eckCorePath],
485
+ enabled: true,
486
+ timeout: 30000,
487
+ };
488
+ }
489
+
490
+ // Ensure AGENTS.md is in instructions
491
+ if (!config.instructions) {
492
+ config.instructions = ['AGENTS.md'];
493
+ } else if (!config.instructions.includes('AGENTS.md')) {
494
+ config.instructions.push('AGENTS.md');
495
+ }
496
+
497
+ // Add permissions only if not already configured
498
+ // List all tools explicitly with "allow", then "*" as fallback for future tools
499
+ // Order matters: last matching rule wins, so users can override specific tools
500
+ if (!config.permission) {
501
+ config.permission = {
502
+ "read": "allow",
503
+ "edit": "allow",
504
+ "glob": "allow",
505
+ "grep": "allow",
506
+ "list": "allow",
507
+ "bash": "allow",
508
+ "task": "allow",
509
+ "skill": "allow",
510
+ "lsp": "allow",
511
+ "todoread": "allow",
512
+ "todowrite": "allow",
513
+ "webfetch": "allow",
514
+ "websearch": "allow",
515
+ "codesearch": "allow",
516
+ "external_directory": "allow",
517
+ "doom_loop": "allow",
518
+ "*": "allow"
519
+ };
520
+ }
521
+
522
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
523
+
524
+ return true;
264
525
  }
@@ -1,38 +1,48 @@
1
- import { addTrainingPoint, showEstimationStats } from '../../utils/tokenEstimator.js';
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { addTrainingPoint, showEstimationStats, syncTokenWeights } from '../../utils/tokenEstimator.js';
2
4
 
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) {
5
+ export async function runTokenTools(payload) {
6
+ const toolName = payload.name;
7
+ const args = payload.arguments || {};
8
+
9
+ if (toolName === 'eck_train_tokens') {
10
+ await handleTrainTokens(args);
11
+ } else if (toolName === 'eck_token_stats') {
12
+ await handleTokenStats();
13
+ }
14
+ }
15
+
16
+ async function handleTrainTokens(args) {
17
+ const { projectType, fileSizeBytes, estimatedTokens, actualTokens } = args;
18
+
19
+ if (!projectType || fileSizeBytes === undefined || estimatedTokens === undefined || actualTokens === undefined) {
20
+ console.log(chalk.red('āŒ Error: Missing required arguments for eck_train_tokens.'));
21
+ console.log(chalk.yellow('Expected: { projectType, fileSizeBytes, estimatedTokens, actualTokens }'));
22
+ return;
23
+ }
24
+
25
+ const spinner = ora('Calibrating token estimation polynomial...').start();
11
26
  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
-
27
+ await addTrainingPoint(
28
+ projectType,
29
+ Number(fileSizeBytes),
30
+ Number(estimatedTokens),
31
+ Number(actualTokens)
32
+ );
33
+ spinner.succeed('Token estimation calibrated successfully.');
26
34
  } 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);
35
+ spinner.fail(`Calibration failed: ${error.message}`);
30
36
  }
31
37
  }
32
38
 
33
- /**
34
- * Show token estimation statistics
35
- */
36
- export async function showTokenStats() {
37
- await showEstimationStats();
38
- }
39
+ async function handleTokenStats() {
40
+ const spinner = ora('Fetching latest token statistics and weights...').start();
41
+ try {
42
+ await syncTokenWeights(true);
43
+ spinner.stop();
44
+ await showEstimationStats();
45
+ } catch (error) {
46
+ spinner.fail(`Failed to fetch statistics: ${error.message}`);
47
+ }
48
+ }