@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
package/src/cli/cli.js CHANGED
@@ -2,426 +2,276 @@ import { Command } from 'commander';
2
2
  import path from 'path';
3
3
  import fs from 'fs/promises';
4
4
  import { fileURLToPath } from 'url';
5
+ import chalk from 'chalk';
6
+ import { createRequire } from 'module';
7
+ import os from 'os';
8
+ import crypto from 'crypto';
5
9
 
6
10
  const __filename = fileURLToPath(import.meta.url);
7
11
  const __dirname = path.dirname(__filename);
8
12
 
9
- import { createRepoSnapshot } from './commands/createSnapshot.js';
10
- import { updateSnapshot, updateSnapshotJson } from './commands/updateSnapshot.js';
11
- import { restoreSnapshot } from './commands/restoreSnapshot.js';
12
- import { pruneSnapshot } from './commands/pruneSnapshot.js';
13
- import { generateConsilium } from './commands/consilium.js';
14
- import { detectProject, testFileParsing } from './commands/detectProject.js';
15
- import { trainTokens, showTokenStats } from './commands/trainTokens.js';
16
- import { executePrompt, executePromptWithSession } from '../services/claudeCliService.js';
17
- import { detectProfiles } from './commands/detectProfiles.js';
18
- import { generateProfileGuide } from './commands/generateProfileGuide.js';
19
- import { setupGemini } from './commands/setupGemini.js';
20
- import { pushTelemetry } from '../utils/telemetry.js';
21
-
22
- import { showFile } from './commands/showFile.js';
23
- import { runDoctor } from './commands/doctor.js';
24
- import { setupMcp } from './commands/setupMcp.js';
25
- import { envPush, envPull } from './commands/envSync.js';
26
- import inquirer from 'inquirer';
27
- import ora from 'ora';
28
- import { execa } from 'execa';
29
- import chalk from 'chalk';
30
- import { createRequire } from 'module';
31
-
32
- /**
33
- * Check code boundaries in a file
34
- */
35
- async function checkCodeBoundaries(filePath, agentId) {
13
+ async function getGlobalConfig() {
14
+ const configPath = path.join(os.homedir(), '.eck', 'cli-config.json');
36
15
  try {
37
- const content = await fs.readFile(filePath, 'utf-8');
38
- const boundaryRegex = /\/\* AGENT_BOUNDARY:\[([^\]]+)\] START \*\/([\s\S]*?)\/\* AGENT_BOUNDARY:\[[^\]]+\] END \*\//g;
39
-
40
- const boundaries = [];
41
- let match;
42
-
43
- while ((match = boundaryRegex.exec(content)) !== null) {
44
- boundaries.push({
45
- owner: match[1],
46
- startIndex: match.index,
47
- endIndex: match.index + match[0].length,
48
- content: match[2]
49
- });
50
- }
51
-
52
- return {
53
- file: filePath,
54
- hasBoundaries: boundaries.length > 0,
55
- boundaries: boundaries,
56
- canModify: boundaries.every(b => b.owner === agentId || b.owner === 'SHARED')
57
- };
58
- } catch (error) {
59
- return {
60
- file: filePath,
61
- error: error.message,
62
- canModify: true // If can't read, assume can modify (new file)
63
- };
16
+ const data = await fs.readFile(configPath, 'utf-8');
17
+ return JSON.parse(data);
18
+ } catch (e) {
19
+ const newConfig = { instanceId: crypto.randomUUID(), telemetryEnabled: true };
20
+ await fs.mkdir(path.dirname(configPath), { recursive: true }).catch(() => {});
21
+ await fs.writeFile(configPath, JSON.stringify(newConfig, null, 2)).catch(() => {});
22
+ return newConfig;
64
23
  }
65
24
  }
66
25
 
67
- // Main run function that sets up the CLI
68
- // Check for newer version on npm (non-blocking)
69
- function checkForUpdates() {
70
- const require = createRequire(import.meta.url);
71
- const pkg = require('../../package.json');
72
- const currentVersion = pkg.version;
73
-
74
- // Fire and forget — result captured via closure
75
- let updateMessage = null;
76
-
77
- const check = execa('npm', ['view', '@xelth/eck-snapshot', 'version'], { timeout: 5000 })
78
- .then(({ stdout }) => {
79
- const latest = stdout.trim();
80
- if (latest && latest !== currentVersion) {
81
- const current = currentVersion.split('.').map(Number);
82
- const remote = latest.split('.').map(Number);
83
- const isNewer = remote[0] > current[0] ||
84
- (remote[0] === current[0] && remote[1] > current[1]) ||
85
- (remote[0] === current[0] && remote[1] === current[1] && remote[2] > current[2]);
86
- if (isNewer) {
87
- updateMessage = `\n${chalk.yellow(`⬆ Update available: ${currentVersion} → ${latest}`)} run: ${chalk.cyan('npm i -g @xelth/eck-snapshot')}`;
88
- }
89
- }
90
- })
91
- .catch(() => { /* network error, skip silently */ });
92
-
93
- process.on('exit', () => {
94
- if (updateMessage) {
95
- console.error(updateMessage);
96
- }
97
- });
26
+ async function setGlobalConfig(updates) {
27
+ const configPath = path.join(os.homedir(), '.eck', 'cli-config.json');
28
+ const current = await getGlobalConfig();
29
+ const next = { ...current, ...updates };
30
+ await fs.writeFile(configPath, JSON.stringify(next, null, 2)).catch(() => {});
31
+ return next;
98
32
  }
99
33
 
34
+ // Import core logic (we bypass the CLI wrapper entirely)
35
+ import { createRepoSnapshot } from './commands/createSnapshot.js';
36
+ import { updateSnapshot, updateSnapshotJson } from './commands/updateSnapshot.js';
37
+ import { setupMcp } from './commands/setupMcp.js';
38
+ import { detectProject } from './commands/detectProject.js';
39
+ import { runDoctor } from './commands/doctor.js';
40
+ import { runReconTool } from './commands/recon.js';
41
+ import { runTokenTools } from './commands/trainTokens.js';
42
+
43
+ // Legacy command shims: translate old positional commands to JSON payloads
44
+ // so internal callers (mcp-eck-core.js) keep working after the JSON migration.
45
+ const LEGACY_COMMANDS = {
46
+ 'update-auto': (args) => ({ name: 'eck_update_auto', arguments: { fail: args.includes('--fail') || args.includes('-f') } }),
47
+ 'snapshot': () => ({ name: 'eck_snapshot', arguments: {} }),
48
+ 'update': (args) => ({ name: 'eck_update', arguments: { fail: args.includes('--fail') || args.includes('-f') } }),
49
+ 'setup-mcp': (args) => ({ name: 'eck_setup_mcp', arguments: { opencode: args.includes('--opencode'), both: args.includes('--both') } }),
50
+ 'detect': () => ({ name: 'eck_detect', arguments: {} }),
51
+ 'doctor': () => ({ name: 'eck_doctor', arguments: {} }),
52
+ 'scout': (args) => ({ name: 'eck_scout', arguments: { depth: args[0] ? parseInt(args[0], 10) : 0 } }),
53
+ 'fetch': (args) => ({ name: 'eck_fetch', arguments: { patterns: args } }),
54
+ 'link': (args) => ({ name: 'eck_snapshot', arguments: { isLinkedProject: true, linkDepth: args[0] ? parseInt(args[0], 10) : 0 } }),
55
+ 'profile': (args) => args[0] ? ({ name: 'eck_snapshot', arguments: { profile: args.join(',') } }) : ({ name: 'eck_snapshot', arguments: { profile: true } }),
56
+ 'booklm': () => ({ name: 'eck_snapshot', arguments: { notebooklm: 'scout' } }),
57
+ 'notelm': () => ({ name: 'eck_snapshot', arguments: { notebooklm: 'architect' } }),
58
+ 'telemetry': (args) => ({ name: 'eck_telemetry', arguments: { action: args[0] } }),
59
+ };
60
+
100
61
  export function run() {
101
- // Start version check in background (non-blocking)
102
- checkForUpdates();
62
+ // Intercept legacy positional commands before commander parses them
63
+ const rawArgs = process.argv.slice(2);
64
+ const firstArg = rawArgs[0];
65
+ if (firstArg && LEGACY_COMMANDS[firstArg]) {
66
+ const payload = LEGACY_COMMANDS[firstArg](rawArgs.slice(1));
67
+ // Replace argv so commander sees the JSON payload
68
+ process.argv = [process.argv[0], process.argv[1], JSON.stringify(payload)];
69
+ }
103
70
 
104
71
  const program = new Command();
105
-
106
72
  const pkg = createRequire(import.meta.url)('../../package.json');
107
- const helpGuide = `eck-snapshot (v${pkg.version}) - AI-Native Repository Context Tool.
108
-
109
- --- 🚀 Core Workflow: Optimized for Web LLMs (Gemini/ChatGPT) ---
110
-
111
- 1. Initial Context (Maximum Compression)
112
- Create a lightweight map of your entire project. Bodies of functions are hidden.
113
- This fits huge monoliths into the context window.
114
-
115
- $ eck-snapshot --skeleton
116
- -> Generates: .eck/snapshots/<name>_sk.md (Upload this to AI)
117
-
118
- 2. Lazy Loading (On-Demand Details)
119
- If the AI needs to see the implementation of specific files, it will ask you.
120
- You can display multiple files at once to copy-paste back to the chat.
121
-
122
- $ eck-snapshot show src/auth.js src/utils/hash.js
123
-
124
- 3. Working & Updating
125
- As you apply changes, the AI loses context. Instead of re-sending the full repo,
126
- send only what changed since the last snapshot.
127
-
128
- $ eck-snapshot update
129
- -> Generates: .eck/snapshots/update_<timestamp>.md (Contains changed files + git diff)
130
-
131
- --- 🛠️ Managing Context Profiles ---
132
-
133
- Option A: Auto-Detection (Best for start)
134
- Uses AI to scan folders and suggest profiles (backend, frontend, etc).
135
- $ eck-snapshot profile-detect
136
-
137
- Option B: Manual Guide (Best for large repos)
138
- If the project is too big for auto-detection, this generates a prompt text file
139
- that you can paste into a powerful Web LLM (like Gemini 1.5 Pro) to design profiles manually.
140
-
141
- 1. Run: $ eck-snapshot generate-profile-guide
142
- 2. Open: .eck/profile_generation_guide.md
143
- 3. Copy: Paste the content into your AI chat.
144
- 4. Save: Take the JSON response and save it to .eck/profiles.json
145
-
146
- Option C: Using Profiles
147
- $ eck-snapshot --profile (List all profiles)
148
- $ eck-snapshot --profile backend (Use profile)
149
- $ eck-snapshot --profile "frontend,-**/*.test.js" (Ad-hoc filtering)
150
-
151
- --- 🧩 Auxiliary Commands ---
152
73
 
153
- - restore: Restore files from a snapshot to disk.
154
- - prune: Use AI to shrink a snapshot file by importance.
155
- - ask-claude: Delegate tasks to Claude CLI agent.
156
- - setup-gemini: Configure gemini-cli integration.
157
- - setup-mcp: Setup/restore MCP servers (eck-core + glm-zai).
74
+ const helpGuide = `
75
+ eck-snapshot (v${pkg.version}) - AI-Native Repository Context Tool.
76
+ ===================================================================
77
+
78
+ [AI AGENTS: PURE JSON/MCP INTERFACE ACTIVE]
79
+ This CLI is designed to be operated by AI agents using JSON payloads.
80
+ - eck_snapshot : { profile, skeleton, jas, link, linkDepth }
81
+ - eck_update : Delta snapshot
82
+ - eck_scout : { depth: 0-9 }
83
+ - eck_fetch : { patterns: [] }
84
+ - eck_setup_mcp : Configure MCP servers
85
+ - eck_detect : Detect project type
86
+ - eck_doctor : Health check
87
+
88
+ [HUMAN COMMANDS: SHORTHANDS]
89
+ Ranked by frequency of use:
90
+
91
+ 1. eck-snapshot snapshot Full project snapshot
92
+ 2. eck-snapshot update Delta update (changed files only)
93
+ 3. eck-snapshot profile [name] Snapshot filtered by profile (from .eck/profiles.json)
94
+ No arg = list available profiles
95
+ Example: eck-snapshot profile backend
96
+ Multiple: eck-snapshot profile backend,api
97
+ 4. eck-snapshot scout [0-9] Scout external repo. Depths:
98
+ 0: Tree only (default)
99
+ 1-4: Truncated (10, 30, 60, 100 lines)
100
+ 5: Skeleton (Signatures only)
101
+ 6: Skeleton + docs
102
+ 7-9: Full content (500, 1000, unlimited)
103
+ 5. eck-snapshot fetch <glob> Fetch specific files (e.g., "src/**/*.js")
104
+ 6. eck-snapshot link [0-9] Linked companion snapshot (same depths)
105
+ 7. eck-snapshot booklm Export for NotebookLM (Scout - fetch generator)
106
+ 8. eck-snapshot notelm Export for NotebookLM (Architect - experimental)
107
+ 9. eck-snapshot setup-mcp Configure AI agents (Claude Code, OpenCode)
108
+ 10. eck-snapshot detect Detect project type and active filters
109
+ 11. eck-snapshot doctor Check project health and stubs
110
+
111
+ [FEEDBACK]
112
+ eck-snapshot -e "message" Send feedback/ideas to developers (read by AI)
113
+ eck-snapshot -E "message" Send urgent bug report
114
+
115
+ [DATENSCHUTZ / PRIVACY]
116
+ We respect your privacy. By default, eck-snapshot collects anonymous usage counts
117
+ and crash logs to improve the tool. NO source code or sensitive data is ever sent.
118
+ To completely disable background telemetry:
119
+ eck-snapshot telemetry disable
120
+ To re-enable it:
121
+ eck-snapshot telemetry enable
158
122
  `;
159
123
 
160
124
  program
161
125
  .name('eck-snapshot')
162
- .description('A lightweight, platform-independent CLI for creating project snapshots.')
163
126
  .version(pkg.version)
164
- .addHelpText('before', helpGuide);
165
-
166
- // Main snapshot command
167
- program
168
- .command('snapshot', { isDefault: true })
169
- .description('Create a multi-agent aware snapshot of a repository')
170
- .argument('[repoPath]', 'Path to the repository', process.cwd())
171
- .option('-o, --output <dir>', 'Output directory')
172
- .option('--no-tree', 'Exclude directory tree')
173
- .option('-v, --verbose', 'Show detailed processing')
174
- .option('--max-file-size <size>', 'Maximum file size', '10MB')
175
- .option('--max-total-size <size>', 'Maximum total size', '100MB')
176
- .option('--max-depth <number>', 'Maximum tree depth', (val) => parseInt(val), 10)
177
- .option('--config <path>', 'Configuration file path')
178
- .option('--include-hidden', 'Include hidden files')
179
- .option('--format <type>', 'Output format: md, json', 'md')
180
- .option('--no-ai-header', 'Skip AI instructions')
181
- .option('-d, --dir', 'Directory mode')
182
- .option('--enhanced', 'Use enhanced multi-agent headers (default: true)', true)
183
- .option('--profile [name]', 'Filter files using profiles and/or ad-hoc glob patterns. Run without argument to list available profiles.')
184
- .option('--agent', 'Generate a snapshot optimized for a command-line agent')
185
- .option('--jas', 'Enable Project Mode for Junior Architect Sonnet (Claude Code only)')
186
- .option('--jao', 'Enable Project Mode for Junior Architect Opus (Claude Code only)')
187
- .option('--jaz', 'Enable Project Mode for Junior Architect GLM (OpenCode Z.AI only)')
188
- .option('--zh', 'Communicate with GLM Z.AI workers in Chinese for better quality')
189
- .option('--skeleton', 'Enable skeleton mode: strip function bodies to save context window tokens')
190
- .option('--max-lines-per-file <number>', 'Truncate files to max N lines (e.g., 200 for compact snapshots)', (val) => parseInt(val))
191
- .action(createRepoSnapshot)
192
- .addHelpText('after', `
193
- Quick --profile Examples:
194
- --profile List all available profiles
195
- --profile backend Use the 'backend' profile
196
- --profile "backend,-**/tests/**" Use backend, exclude tests
197
- --profile "src/**/*.js,-**/*.test.js" Ad-hoc: all JS, exclude tests
198
-
199
- See "Managing Context Profiles" section above for profile setup.
200
- `);
201
-
202
- // Update snapshot command
203
- program
204
- .command('update')
205
- .description('Create a delta snapshot of changed files since the last full snapshot')
206
- .argument('[repoPath]', 'Path to the repository', process.cwd())
207
- .option('--config <path>', 'Configuration file path')
208
- .action(updateSnapshot);
209
-
210
- // Auto/Silent Update command for Agents
211
- program
212
- .command('update-auto')
213
- .description('Silent update for AI agents (JSON output)')
214
- .argument('[repoPath]', 'Path to the repository', process.cwd())
215
- .action(updateSnapshotJson);
216
-
217
- // Restore command
218
- program
219
- .command('restore')
220
- .description('Restore files from a snapshot')
221
- .argument('<snapshot_file>', 'Snapshot file path')
222
- .argument('[target_directory]', 'Target directory', process.cwd())
223
- .option('-f, --force', 'Skip confirmation')
224
- .option('-v, --verbose', 'Show detailed progress')
225
- .option('--dry-run', 'Preview without writing')
226
- .option('--include <patterns...>', 'Include patterns')
227
- .option('--exclude <patterns...>', 'Exclude patterns')
228
- .option('--concurrency <number>', 'Concurrent operations', (val) => parseInt(val), 10)
229
- .action(restoreSnapshot);
230
-
231
- // Prune command
232
- program
233
- .command('prune')
234
- .description('Intelligently reduce snapshot size using AI file ranking')
235
- .argument('<snapshot_file>', 'Path to the snapshot file to prune')
236
- .option('--target-size <size>', 'Target size (e.g., 500KB, 1MB)', '500KB')
237
- .action(pruneSnapshot);
238
-
239
- // Consilium command
240
- program
241
- .command('consilium')
242
- .description('Generate a consilium request for complex decisions')
243
- .option('--type <type>', 'Decision type', 'technical_decision')
244
- .option('--title <title>', 'Decision title')
245
- .option('--description <desc>', 'Detailed description')
246
- .option('--complexity <num>', 'Complexity score (1-10)', (val) => parseInt(val), 7)
247
- .option('--constraints <list>', 'Comma-separated constraints')
248
- .option('--snapshot <file>', 'Include snapshot file')
249
- .option('--agent <id>', 'Requesting agent ID')
250
- .option('-o, --output <file>', 'Output file', 'consilium_request.json')
251
- .action(generateConsilium);
252
-
253
- // Check boundaries command
254
- program
255
- .command('check-boundaries')
256
- .description('Check agent boundaries in a file')
257
- .argument('<file>', 'File to check')
258
- .option('--agent <id>', 'Your agent ID')
259
- .action(async (file, options) => {
260
- const result = await checkCodeBoundaries(file, options.agent || 'UNKNOWN');
261
- console.log(JSON.stringify(result, null, 2));
262
- });
263
-
264
- // Project detection command
265
- program
266
- .command('detect')
267
- .description('Detect and display project type and configuration')
268
- .argument('[projectPath]', 'Path to the project', process.cwd())
269
- .option('-v, --verbose', 'Show detailed detection results')
270
- .action(detectProject);
271
-
272
- // Android parsing test command
273
- program
274
- .command('test-android')
275
- .description('Test Android file parsing capabilities')
276
- .argument('<filePath>', 'Path to Android source file (.kt or .java)')
277
- .option('--show-content', 'Show content preview of parsed segments')
278
- .action(testFileParsing);
279
-
280
- // Token training command
281
- program
282
- .command('train-tokens')
283
- .description('Train token estimation with actual results')
284
- .argument('<projectType>', 'Project type (android, nodejs, python, etc.)')
285
- .argument('<fileSizeBytes>', 'File size in bytes')
286
- .argument('<estimatedTokens>', 'Estimated token count')
287
- .argument('<actualTokens>', 'Actual token count from LLM')
288
- .action(trainTokens);
289
-
290
- // Token statistics command
291
- program
292
- .command('token-stats')
293
- .description('Show token estimation statistics and accuracy')
294
- .action(showTokenStats);
295
-
296
- // Profile detection command
297
- program
298
- .command('profile-detect')
299
- .description('Use AI to scan the directory tree and auto-generate local context profiles (saves to .eck/profiles.json)')
300
- .argument('[repoPath]', 'Path to the repository', process.cwd())
301
- .action(detectProfiles);
127
+ .addHelpText('before', helpGuide)
128
+ .argument('[payload]', 'JSON string representing the MCP tool call')
129
+ .option('-e, --feedback <message>', 'Send feedback or report an issue to developers')
130
+ .option('-E, --urgent-feedback <message>', 'Send urgent feedback to developers')
131
+ .action(async (payloadStr, options) => {
132
+ const globalConfig = await getGlobalConfig();
133
+
134
+ // --- Handle Feedback Flags ---
135
+ if (options.feedback || options.urgentFeedback) {
136
+ const msg = options.feedback || options.urgentFeedback;
137
+ const type = options.urgentFeedback ? 'URGENT' : 'NORMAL';
138
+ const queuePath = path.join(process.cwd(), '.eck', 'telemetry_queue.json');
139
+
140
+ let queue = { instanceId: globalConfig.instanceId, feedback: [], usage: {}, errors: [] };
141
+ try {
142
+ const existing = JSON.parse(await fs.readFile(queuePath, 'utf-8'));
143
+ queue = { ...queue, ...existing };
144
+ } catch(e) { /* no existing queue */ }
145
+
146
+ queue.feedback.push({ type, message: msg, date: new Date().toISOString() });
147
+
148
+ await fs.mkdir(path.dirname(queuePath), { recursive: true }).catch(() => {});
149
+ await fs.writeFile(queuePath, JSON.stringify(queue, null, 2));
150
+
151
+ console.log(chalk.green('Feedback saved locally. It will be sent to developers during the next telemetry sync.'));
152
+ console.log(chalk.gray('(Note: Messages are processed by AI for developers)'));
153
+ return;
154
+ }
302
155
 
303
- program
304
- .command('generate-profile-guide')
305
- .description('Generate a markdown guide with a prompt and directory tree for manual profile creation')
306
- .argument('[repoPath]', 'Path to the repository', process.cwd())
307
- .option('--config <path>', 'Configuration file path')
308
- .action((repoPath, options) => generateProfileGuide(repoPath, options));
156
+ // Default behavior for human users: empty call = full snapshot
157
+ if (!payloadStr) {
158
+ console.log(chalk.cyan('🚀 No arguments provided. Defaulting to full repository snapshot...'));
159
+ console.log(chalk.gray('💡 Run `eck-snapshot -h` to see all available JSON tools.\n'));
160
+ payloadStr = '{"name": "eck_snapshot", "arguments": {}}';
161
+ }
309
162
 
310
- // Ask Claude command
311
- program
312
- .command('ask-claude')
313
- .description('Execute a prompt using claude-code CLI and return JSON response')
314
- .argument('<prompt>', 'Prompt to send to Claude')
315
- .option('-c, --continue', 'Continue the most recent conversation')
316
- .action(async (prompt, options) => {
163
+ let payload;
317
164
  try {
318
- const result = await executePrompt(prompt, options.continue);
319
- console.log(JSON.stringify(result, null, 2));
320
- } catch (error) {
321
- console.error(`Failed to execute prompt: ${error.message}`);
165
+ payload = JSON.parse(payloadStr.trim());
166
+ } catch (e) {
167
+ console.error(chalk.red('❌ Error: Input must be a valid JSON string.'));
168
+ console.log(chalk.yellow(`Example: eck-snapshot '{"name": "eck_snapshot"}'`));
322
169
  process.exit(1);
323
170
  }
324
- });
325
171
 
326
- // Ask Claude with specific session
327
- program
328
- .command('ask-claude-session')
329
- .description('Execute a prompt using specific session ID')
330
- .argument('<sessionId>', 'Session ID to resume')
331
- .argument('<prompt>', 'Prompt to send to Claude')
332
- .action(async (sessionId, prompt) => {
333
- try {
334
- // Directly use the provided session ID
335
- const result = await executePromptWithSession(prompt, sessionId);
336
- console.log(JSON.stringify(result, null, 2));
337
- } catch (error) {
338
- console.error('Failed to execute prompt:', error.message);
339
- process.exit(1);
172
+ const toolName = payload.name;
173
+ const args = payload.arguments || {};
174
+
175
+ // --- Handle Telemetry Config Command ---
176
+ if (toolName === 'eck_telemetry') {
177
+ if (args.action === 'disable') {
178
+ await setGlobalConfig({ telemetryEnabled: false });
179
+ console.log(chalk.yellow('Background telemetry has been disabled.'));
180
+ } else if (args.action === 'enable') {
181
+ await setGlobalConfig({ telemetryEnabled: true });
182
+ console.log(chalk.green('Background telemetry has been enabled. Thank you for supporting the project!'));
183
+ } else {
184
+ console.log(chalk.blue(`Telemetry is currently: ${globalConfig.telemetryEnabled ? 'ENABLED' : 'DISABLED'}`));
185
+ console.log(chalk.gray(`Instance ID: ${globalConfig.instanceId}`));
186
+ }
187
+ return;
340
188
  }
341
- });
342
189
 
190
+ const cwd = process.cwd();
191
+ const queuePath = path.join(cwd, '.eck', 'telemetry_queue.json');
343
192
 
193
+ try {
194
+ // --- Track Usage Locally (Guarded by Privacy Opt-in) ---
195
+ if (globalConfig.telemetryEnabled) {
196
+ try {
197
+ let queue = { instanceId: globalConfig.instanceId, feedback: [], usage: {}, errors: [] };
198
+ try {
199
+ const existing = JSON.parse(await fs.readFile(queuePath, 'utf-8'));
200
+ queue = { ...queue, ...existing };
201
+ } catch(e) { /* no existing queue */ }
202
+ queue.usage[toolName] = (queue.usage[toolName] || 0) + 1;
203
+ await fs.mkdir(path.dirname(queuePath), { recursive: true }).catch(() => {});
204
+ await fs.writeFile(queuePath, JSON.stringify(queue, null, 2));
205
+ } catch(e) { /* ignore tracking errors */ }
206
+ }
344
207
 
208
+ switch (toolName) {
209
+ case 'eck_snapshot':
210
+ await createRepoSnapshot(cwd, args);
211
+ break;
212
+ case 'eck_update':
213
+ await updateSnapshot(cwd, args);
214
+ break;
215
+ case 'eck_update_auto':
216
+ await updateSnapshotJson(cwd, args);
217
+ break;
218
+ case 'eck_setup_mcp':
219
+ await setupMcp(args);
220
+ break;
221
+ case 'eck_detect':
222
+ await detectProject(cwd, args);
223
+ break;
224
+ case 'eck_doctor':
225
+ await runDoctor(cwd);
226
+ break;
227
+ case 'eck_scout':
228
+ case 'eck_fetch':
229
+ await runReconTool(payload);
230
+ break;
231
+ case 'eck_train_tokens':
232
+ case 'eck_token_stats':
233
+ await runTokenTools(payload);
234
+ break;
235
+ default:
236
+ console.log(chalk.red(`❌ Unknown tool: "${toolName}"`));
237
+ console.log(chalk.yellow('Run `eck-snapshot -h` to see available JSON tools.'));
238
+ process.exit(1);
239
+ }
240
+ } catch (err) {
241
+ // --- Track Errors Locally (Guarded by Privacy Opt-in) ---
242
+ if (globalConfig.telemetryEnabled) {
243
+ try {
244
+ let queue = { instanceId: globalConfig.instanceId, feedback: [], usage: {}, errors: [] };
245
+ try {
246
+ const existing = JSON.parse(await fs.readFile(queuePath, 'utf-8'));
247
+ queue = { ...queue, ...existing };
248
+ } catch(e) { /* no existing queue */ }
249
+ queue.errors.push({ tool: toolName, error: err.message, date: new Date().toISOString() });
250
+ await fs.mkdir(path.dirname(queuePath), { recursive: true }).catch(() => {});
251
+ await fs.writeFile(queuePath, JSON.stringify(queue, null, 2));
252
+ } catch(e) { /* ignore tracking errors */ }
253
+ }
345
254
 
346
- program
347
- .command('generate-ai-prompt')
348
- .description('Generate a specific AI prompt from a template.')
349
- .option('--role <role>', 'The role for which to generate a prompt', 'architect')
350
- .action(async (options) => {
351
- try {
352
- const templatePath = path.join(__dirname, '..', 'templates', `${options.role}-prompt.template.md`);
353
- const template = await fs.readFile(templatePath, 'utf-8');
354
- // In the future, we can inject dynamic data here from setup.json
355
- console.log(template);
356
- } catch (error) {
357
- console.error(`Failed to generate prompt for role '${options.role}':`, error.message);
255
+ console.error(chalk.red(`❌ Execution failed for ${toolName}:`), err.message);
358
256
  process.exit(1);
359
257
  }
360
258
  });
361
259
 
362
- // Setup Gemini command
363
- program
364
- .command('setup-gemini')
365
- .description('Generate claude.toml configuration for gemini-cli integration with dynamic paths')
366
- .option('-v, --verbose', 'Show detailed output and error information')
367
- .action(setupGemini);
368
-
369
-
370
- // Show file command (for skeleton mode lazy loading)
371
- program
372
- .command('show')
373
- .description('Output the full content of specific file(s) (for AI lazy loading)')
374
- .argument('<filePaths...>', 'Space-separated paths to files')
375
- .action(showFile);
376
-
377
- // Doctor command (health check for manifests)
378
- program
379
- .command('doctor')
380
- .description('Check project health and detect unfinished manifest stubs')
381
- .argument('[repoPath]', 'Path to the repository', process.cwd())
382
- .action(runDoctor);
383
-
384
- // Setup MCP servers command (replaces setup-claude-mcp with unified approach)
385
- program
386
- .command('setup-mcp')
387
- .description('Setup/restore MCP servers (eck-core + glm-zai) for Claude Code and/or OpenCode')
388
- .option('--opencode', 'Setup for OpenCode only')
389
- .option('--both', 'Setup for both Claude Code and OpenCode')
390
- .option('-v, --verbose', 'Show detailed output')
391
- .action(setupMcp);
392
-
393
- // Environment sync commands (encrypted .eck/ transfer between machines)
394
- const envCmd = program.command('env').description('Encrypted environment sync');
395
- envCmd
396
- .command('push')
397
- .description('Pack and encrypt .eck/ config files into .eck-sync.enc')
398
- .option('-v, --verbose', 'Show detailed output')
399
- .action(envPush);
400
- envCmd
401
- .command('pull')
402
- .description('Decrypt and restore .eck/ config files from .eck-sync.enc')
403
- .option('-f, --force', 'Overwrite existing .eck/ files without prompting')
404
- .option('-v, --verbose', 'Show detailed output')
405
- .action(envPull);
406
-
407
- // Telemetry commands
408
- const telemetryCmd = program.command('telemetry').description('Manage Telemetry Hub synchronization');
409
- telemetryCmd
410
- .command('push')
411
- .description('Manually push the latest AnswerToSA.md report to xelth.com/T/report')
412
- .argument('[repoPath]', 'Path to the repository', process.cwd())
413
- .action(async (repoPath) => {
414
- console.log(chalk.blue('Pushing agent telemetry...'));
415
- await pushTelemetry(repoPath, false);
416
- });
417
-
418
- telemetryCmd
419
- .command('sync-weights')
420
- .description('Fetch the latest global token estimation weights from Telemetry Hub')
421
- .action(async () => {
422
- const { syncTokenWeights } = await import('../utils/tokenEstimator.js');
423
- await syncTokenWeights();
424
- });
260
+ // Start version check in background (non-blocking)
261
+ checkForUpdates(pkg.version);
425
262
 
426
263
  program.parse(process.argv);
427
264
  }
265
+
266
+ function checkForUpdates(currentVersion) {
267
+ import('execa').then(({ execa }) => {
268
+ execa('npm', ['view', '@xelth/eck-snapshot', 'version'], { timeout: 5000 })
269
+ .then(({ stdout }) => {
270
+ const latest = stdout.trim();
271
+ if (latest && latest !== currentVersion) {
272
+ console.error(`\n${chalk.yellow(`⬆ Update available: ${currentVersion} → ${latest}`)}`);
273
+ }
274
+ })
275
+ .catch(() => {});
276
+ });
277
+ }