@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
package/src/cli/cli.js CHANGED
@@ -2,426 +2,297 @@ 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';
9
+ import { ensureSnapshotsInGitignore } from '../utils/fileUtils.js';
5
10
 
6
11
  const __filename = fileURLToPath(import.meta.url);
7
12
  const __dirname = path.dirname(__filename);
8
13
 
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) {
14
+ async function getGlobalConfig() {
15
+ const configPath = path.join(os.homedir(), '.eck', 'cli-config.json');
36
16
  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
- };
17
+ const data = await fs.readFile(configPath, 'utf-8');
18
+ return JSON.parse(data);
19
+ } catch (e) {
20
+ const newConfig = { instanceId: crypto.randomUUID(), telemetryEnabled: true };
21
+ await fs.mkdir(path.dirname(configPath), { recursive: true }).catch(() => {});
22
+ await fs.writeFile(configPath, JSON.stringify(newConfig, null, 2)).catch(() => {});
23
+ return newConfig;
64
24
  }
65
25
  }
66
26
 
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
- });
27
+ async function setGlobalConfig(updates) {
28
+ const configPath = path.join(os.homedir(), '.eck', 'cli-config.json');
29
+ const current = await getGlobalConfig();
30
+ const next = { ...current, ...updates };
31
+ await fs.writeFile(configPath, JSON.stringify(next, null, 2)).catch(() => {});
32
+ return next;
98
33
  }
99
34
 
35
+ // Import core logic (we bypass the CLI wrapper entirely)
36
+ import { createRepoSnapshot } from './commands/createSnapshot.js';
37
+ import { updateSnapshot, updateSnapshotJson } from './commands/updateSnapshot.js';
38
+ import { setupMcp } from './commands/setupMcp.js';
39
+ import { detectProject } from './commands/detectProject.js';
40
+ import { runDoctor } from './commands/doctor.js';
41
+ import { runReconTool } from './commands/recon.js';
42
+ import { runTokenTools } from './commands/trainTokens.js';
43
+
44
+ // Legacy command shims: translate old positional commands to JSON payloads
45
+ // so internal callers (mcp-eck-core.js) keep working after the JSON migration.
46
+ const LEGACY_COMMANDS = {
47
+ 'update-auto': (args) => {
48
+ const baseIdx = args.indexOf('--base');
49
+ const base = baseIdx !== -1 && args[baseIdx + 1] ? args[baseIdx + 1] : undefined;
50
+ return { name: 'eck_update_auto', arguments: { fail: args.includes('--fail') || args.includes('-f'), base } };
51
+ },
52
+ 'snapshot': () => ({ name: 'eck_snapshot', arguments: {} }),
53
+ 'update': (args) => {
54
+ const baseIdx = args.indexOf('--base');
55
+ const base = baseIdx !== -1 && args[baseIdx + 1] ? args[baseIdx + 1] : undefined;
56
+ return { name: 'eck_update', arguments: { fail: args.includes('--fail') || args.includes('-f'), base } };
57
+ },
58
+ 'setup-mcp': (args) => ({ name: 'eck_setup_mcp', arguments: { opencode: args.includes('--opencode'), both: args.includes('--both') } }),
59
+ 'detect': () => ({ name: 'eck_detect', arguments: {} }),
60
+ 'doctor': () => ({ name: 'eck_doctor', arguments: {} }),
61
+ 'scout': (args) => ({ name: 'eck_scout', arguments: { depth: args[0] !== undefined ? parseInt(args[0], 10) : 0 } }),
62
+ 'fetch': (args) => ({ name: 'eck_fetch', arguments: { patterns: args } }),
63
+ 'link': (args) => ({ name: 'eck_snapshot', arguments: { isLinkedProject: true, linkDepth: args[0] !== undefined ? parseInt(args[0], 10) : 0 } }),
64
+ 'profile': (args) => args[0] ? ({ name: 'eck_snapshot', arguments: { profile: args.join(',') } }) : ({ name: 'eck_snapshot', arguments: { profile: true } }),
65
+ 'booklm': () => ({ name: 'eck_snapshot', arguments: { notebooklm: 'scout' } }),
66
+ 'notelm': () => ({ name: 'eck_snapshot', arguments: { notebooklm: 'architect' } }),
67
+ 'notebook': (args) => {
68
+ if (args[0] === 'link') return { name: 'eck_snapshot', arguments: { notebooklm: 'link', linkDepth: args[1] !== undefined ? parseInt(args[1], 10) : 0 } };
69
+ if (args[0] === 'scout') return { name: 'eck_snapshot', arguments: { notebooklm: 'scout', linkDepth: args[1] !== undefined ? parseInt(args[1], 10) : 0 } };
70
+ return { name: 'eck_snapshot', arguments: { notebooklm: 'hybrid' } };
71
+ },
72
+ 'telemetry': (args) => ({ name: 'eck_telemetry', arguments: { action: args[0] } }),
73
+ };
74
+
100
75
  export function run() {
101
- // Start version check in background (non-blocking)
102
- checkForUpdates();
76
+ // Intercept legacy positional commands before commander parses them
77
+ const rawArgs = process.argv.slice(2);
78
+ const firstArg = rawArgs[0];
79
+ if (firstArg && LEGACY_COMMANDS[firstArg]) {
80
+ const payload = LEGACY_COMMANDS[firstArg](rawArgs.slice(1));
81
+ // Replace argv so commander sees the JSON payload
82
+ process.argv = [process.argv[0], process.argv[1], JSON.stringify(payload)];
83
+ }
103
84
 
104
85
  const program = new Command();
105
-
106
86
  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
87
 
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).
88
+ const helpGuide = `
89
+ eck-snapshot (v${pkg.version}) - AI-Native Repository Context Tool.
90
+ ===================================================================
91
+
92
+ [AI AGENTS: PURE JSON/MCP INTERFACE ACTIVE]
93
+ This CLI is designed to be operated by AI agents using JSON payloads.
94
+ - eck_snapshot : { profile, skeleton, jas, link, linkDepth }
95
+ - eck_update : Delta snapshot
96
+ - eck_scout : { depth: 0-9 }
97
+ - eck_fetch : { patterns: [] }
98
+ - eck_setup_mcp : Configure MCP servers
99
+ - eck_detect : Detect project type
100
+ - eck_doctor : Health check
101
+
102
+ [HUMAN COMMANDS: SHORTHANDS]
103
+ Ranked by frequency of use:
104
+
105
+ 1. eck-snapshot snapshot Full project snapshot
106
+ 2. eck-snapshot update Delta update (changed files only)
107
+ --base <snapshot.md> : Compare against an old snapshot file
108
+ 3. eck-snapshot profile [name] Snapshot filtered by profile (from .eck/profiles.json)
109
+ No arg = list available profiles
110
+ Example: eck-snapshot profile backend
111
+ Multiple: eck-snapshot profile backend,api
112
+ 4. eck-snapshot scout [0-9] Scout external repo. Depths:
113
+ 0: Tree only (default)
114
+ 1-4: Truncated (10, 30, 60, 100 lines)
115
+ 5: Skeleton (Signatures only)
116
+ 6: Skeleton + docs
117
+ 7-9: Full content (500, 1000, unlimited)
118
+ 5. eck-snapshot fetch <glob> Fetch specific files (e.g., "src/**/*.js")
119
+ 6. eck-snapshot link [0-9] Linked companion snapshot (same depths)
120
+ 7. eck-snapshot booklm Export for NotebookLM (Scout - fetch generator)
121
+ 8. eck-snapshot notelm Export for NotebookLM (Architect - experimental)
122
+ 9. eck-snapshot notebook Export for NotebookLM (Primary Project)
123
+ eck-snapshot notebook link 5 Export Linked Project for NotebookLM (chunked)
124
+ eck-snapshot notebook scout 5 Export Scouted Project for NotebookLM (chunked)
125
+ 10. eck-snapshot setup-mcp Configure AI agents (Claude Code, OpenCode)
126
+ 10. eck-snapshot detect Detect project type and active filters
127
+ 11. eck-snapshot doctor Check project health and stubs
128
+
129
+ [FEEDBACK]
130
+ eck-snapshot -e "message" Send feedback/ideas to developers (read by AI)
131
+ eck-snapshot -E "message" Send urgent bug report
132
+
133
+ [DATENSCHUTZ / PRIVACY]
134
+ We respect your privacy. By default, eck-snapshot collects anonymous usage counts
135
+ and crash logs to improve the tool. NO source code or sensitive data is ever sent.
136
+ To completely disable background telemetry:
137
+ eck-snapshot telemetry disable
138
+ To re-enable it:
139
+ eck-snapshot telemetry enable
158
140
  `;
159
141
 
160
142
  program
161
143
  .name('eck-snapshot')
162
- .description('A lightweight, platform-independent CLI for creating project snapshots.')
163
144
  .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);
145
+ .addHelpText('before', helpGuide)
146
+ .argument('[payload]', 'JSON string representing the MCP tool call')
147
+ .option('-e, --feedback <message>', 'Send feedback or report an issue to developers')
148
+ .option('-E, --urgent-feedback <message>', 'Send urgent feedback to developers')
149
+ .action(async (payloadStr, options) => {
150
+ const globalConfig = await getGlobalConfig();
151
+
152
+ // --- Handle Feedback Flags ---
153
+ if (options.feedback || options.urgentFeedback) {
154
+ const msg = options.feedback || options.urgentFeedback;
155
+ const type = options.urgentFeedback ? 'URGENT' : 'NORMAL';
156
+ const queuePath = path.join(process.cwd(), '.eck', 'telemetry_queue.json');
157
+
158
+ let queue = { instanceId: globalConfig.instanceId, feedback: [], usage: {}, errors: [] };
159
+ try {
160
+ const existing = JSON.parse(await fs.readFile(queuePath, 'utf-8'));
161
+ queue = { ...queue, ...existing };
162
+ } catch(e) { /* no existing queue */ }
163
+
164
+ queue.feedback.push({ type, message: msg, date: new Date().toISOString() });
165
+
166
+ await fs.mkdir(path.dirname(queuePath), { recursive: true }).catch(() => {});
167
+ await ensureSnapshotsInGitignore(process.cwd()).catch(() => {});
168
+ await fs.writeFile(queuePath, JSON.stringify(queue, null, 2));
169
+
170
+ console.log(chalk.green('Feedback saved locally. It will be sent to developers during the next telemetry sync.'));
171
+ console.log(chalk.gray('(Note: Messages are processed by AI for developers)'));
172
+ return;
173
+ }
302
174
 
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));
175
+ // Default behavior for human users: empty call = full snapshot
176
+ if (!payloadStr) {
177
+ console.log(chalk.cyan('🚀 No arguments provided. Defaulting to full repository snapshot...'));
178
+ console.log(chalk.gray('💡 Run `eck-snapshot -h` to see all available JSON tools.\n'));
179
+ payloadStr = '{"name": "eck_snapshot", "arguments": {}}';
180
+ }
309
181
 
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) => {
182
+ let payload;
317
183
  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}`);
184
+ payload = JSON.parse(payloadStr.trim());
185
+ } catch (e) {
186
+ console.error(chalk.red('❌ Error: Input must be a valid JSON string.'));
187
+ console.log(chalk.yellow(`Example: eck-snapshot '{"name": "eck_snapshot"}'`));
322
188
  process.exit(1);
323
189
  }
324
- });
325
190
 
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);
191
+ const toolName = payload.name;
192
+ const args = payload.arguments || {};
193
+
194
+ // --- Handle Telemetry Config Command ---
195
+ if (toolName === 'eck_telemetry') {
196
+ if (args.action === 'disable') {
197
+ await setGlobalConfig({ telemetryEnabled: false });
198
+ console.log(chalk.yellow('Background telemetry has been disabled.'));
199
+ } else if (args.action === 'enable') {
200
+ await setGlobalConfig({ telemetryEnabled: true });
201
+ console.log(chalk.green('Background telemetry has been enabled. Thank you for supporting the project!'));
202
+ } else {
203
+ console.log(chalk.blue(`Telemetry is currently: ${globalConfig.telemetryEnabled ? 'ENABLED' : 'DISABLED'}`));
204
+ console.log(chalk.gray(`Instance ID: ${globalConfig.instanceId}`));
205
+ }
206
+ return;
340
207
  }
341
- });
342
208
 
209
+ const cwd = process.cwd();
210
+ const queuePath = path.join(cwd, '.eck', 'telemetry_queue.json');
343
211
 
212
+ try {
213
+ // --- Track Usage Locally (Guarded by Privacy Opt-in) ---
214
+ if (globalConfig.telemetryEnabled) {
215
+ try {
216
+ let queue = { instanceId: globalConfig.instanceId, feedback: [], usage: {}, errors: [] };
217
+ try {
218
+ const existing = JSON.parse(await fs.readFile(queuePath, 'utf-8'));
219
+ queue = { ...queue, ...existing };
220
+ } catch(e) { /* no existing queue */ }
221
+ queue.usage[toolName] = (queue.usage[toolName] || 0) + 1;
222
+ await fs.mkdir(path.dirname(queuePath), { recursive: true }).catch(() => {});
223
+ await ensureSnapshotsInGitignore(cwd).catch(() => {});
224
+ await fs.writeFile(queuePath, JSON.stringify(queue, null, 2));
225
+ } catch(e) { /* ignore tracking errors */ }
226
+ }
344
227
 
228
+ switch (toolName) {
229
+ case 'eck_snapshot':
230
+ await createRepoSnapshot(cwd, args);
231
+ break;
232
+ case 'eck_update':
233
+ await updateSnapshot(cwd, args);
234
+ break;
235
+ case 'eck_update_auto':
236
+ await updateSnapshotJson(cwd, args);
237
+ break;
238
+ case 'eck_setup_mcp':
239
+ await setupMcp(args);
240
+ break;
241
+ case 'eck_detect':
242
+ await detectProject(cwd, args);
243
+ break;
244
+ case 'eck_doctor':
245
+ await runDoctor(cwd);
246
+ break;
247
+ case 'eck_scout':
248
+ case 'eck_fetch':
249
+ await runReconTool(payload);
250
+ break;
251
+ case 'eck_train_tokens':
252
+ case 'eck_token_stats':
253
+ await runTokenTools(payload);
254
+ break;
255
+ default:
256
+ console.log(chalk.red(`❌ Unknown tool: "${toolName}"`));
257
+ console.log(chalk.yellow('Run `eck-snapshot -h` to see available JSON tools.'));
258
+ process.exit(1);
259
+ }
260
+ } catch (err) {
261
+ // --- Track Errors Locally (Guarded by Privacy Opt-in) ---
262
+ if (globalConfig.telemetryEnabled) {
263
+ try {
264
+ let queue = { instanceId: globalConfig.instanceId, feedback: [], usage: {}, errors: [] };
265
+ try {
266
+ const existing = JSON.parse(await fs.readFile(queuePath, 'utf-8'));
267
+ queue = { ...queue, ...existing };
268
+ } catch(e) { /* no existing queue */ }
269
+ queue.errors.push({ tool: toolName, error: err.message, date: new Date().toISOString() });
270
+ await fs.mkdir(path.dirname(queuePath), { recursive: true }).catch(() => {});
271
+ await ensureSnapshotsInGitignore(cwd).catch(() => {});
272
+ await fs.writeFile(queuePath, JSON.stringify(queue, null, 2));
273
+ } catch(e) { /* ignore tracking errors */ }
274
+ }
345
275
 
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);
276
+ console.error(chalk.red(`❌ Execution failed for ${toolName}:`), err.message);
358
277
  process.exit(1);
359
278
  }
360
279
  });
361
280
 
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
- });
281
+ // Start version check in background (non-blocking)
282
+ checkForUpdates(pkg.version);
425
283
 
426
284
  program.parse(process.argv);
427
285
  }
286
+
287
+ function checkForUpdates(currentVersion) {
288
+ import('execa').then(({ execa }) => {
289
+ execa('npm', ['view', '@xelth/eck-snapshot', 'version'], { timeout: 5000 })
290
+ .then(({ stdout }) => {
291
+ const latest = stdout.trim();
292
+ if (latest && latest !== currentVersion) {
293
+ console.error(`\n${chalk.yellow(`⬆ Update available: ${currentVersion} → ${latest}`)}`);
294
+ }
295
+ })
296
+ .catch(() => {});
297
+ });
298
+ }