@xelth/eck-snapshot 4.2.4 → 5.4.1

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 +917 -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 +247 -0
  26. package/src/config.js +115 -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 +51 -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,626 @@
1
+ import { execa } from 'execa';
2
+ import { spawn } from 'child_process';
3
+ import pRetry from 'p-retry';
4
+ import { parseWithFallback } from '../utils/eckProtocolParser.js';
5
+
6
+ /**
7
+ * Executes a prompt using the claude-code CLI in non-interactive print mode.
8
+ * @param {string} prompt The prompt to send to Claude.
9
+ * @param {object} options Options object, e.g., { continueConversation: boolean, taskSize: number }.
10
+ * @returns {Promise<object>} A promise that resolves with the final JSON output object from Claude.
11
+ */
12
+ export async function executePrompt(prompt, options = {}) {
13
+ const { continueConversation = false } = options;
14
+ try {
15
+ // Ensure the log directory exists
16
+ try {
17
+ await import('fs/promises').then(fs => fs.mkdir('./.eck/logs', { recursive: true }));
18
+ } catch (e) {
19
+ console.error(`Failed to create log directory: ${e.message}`);
20
+ // Do not block execution if log dir creation fails, just warn
21
+ }
22
+ let sessionId = null;
23
+ if (continueConversation) {
24
+ sessionId = await getLastSessionId();
25
+ if (!sessionId) {
26
+ console.warn('No previous session found, starting new conversation');
27
+ } else {
28
+ console.log(`Continuing conversation with session: ${sessionId}`);
29
+ }
30
+ }
31
+
32
+ return await attemptClaudeExecution(prompt, sessionId, options);
33
+ } catch (error) {
34
+ // Check for claude session limits first
35
+ if (isSessionLimitError(error)) {
36
+ await logSessionLimitError(error, prompt);
37
+ throw new Error(`Claude session limit reached: ${error.message}. Please take a break and try again later.`);
38
+ }
39
+
40
+ // If the first attempt fails (timeout, interactive prompts, etc), try to handle it
41
+ if (error.message.includes('timeout') || error.message.includes('SIGTERM')) {
42
+ console.log('First attempt failed, attempting interactive recovery...');
43
+
44
+ try {
45
+ // Try running claude interactively to see what prompts appear
46
+ const interactiveResult = await execa('claude', [], {
47
+ input: '\n',
48
+ timeout: 10000,
49
+ stdio: ['pipe', 'pipe', 'pipe']
50
+ });
51
+
52
+ // Логируем любое интерактивное взаимодействие
53
+ const interactiveLogFile = `./.eck/logs/claude-interactive-${Date.now()}.log`;
54
+ const interactiveLogContent = `=== Claude Interactive Recovery Log ${new Date().toISOString()} ===\n` +
55
+ `Original prompt: "${prompt}"\n` +
56
+ `Original error: ${error.message}\n` +
57
+ `Recovery command: claude (with newline input)\n` +
58
+ `STDOUT:\n${interactiveResult.stdout}\n` +
59
+ `STDERR:\n${interactiveResult.stderr}\n` +
60
+ `=== End Interactive Log ===\n\n`;
61
+
62
+ await import('fs/promises').then(fs => fs.appendFile(interactiveLogFile, interactiveLogContent, 'utf8'));
63
+ console.log(`Interactive recovery logged to: ${interactiveLogFile}`);
64
+
65
+ // Wait a moment for any setup to be processed
66
+ await new Promise(resolve => setTimeout(resolve, 1000));
67
+
68
+ // Now try the original prompt again
69
+ return await attemptClaudeExecution(prompt, sessionId, options);
70
+ } catch (retryError) {
71
+ // Логируем неудачу восстановления
72
+ const failureLogFile = `./.eck/logs/claude-recovery-failure-${Date.now()}.log`;
73
+ const failureLogContent = `=== Claude Recovery Failure Log ${new Date().toISOString()} ===\n` +
74
+ `Original prompt: "${prompt}"\n` +
75
+ `Original error: ${error.message}\n` +
76
+ `Retry error: ${retryError.message}\n` +
77
+ `Retry stack: ${retryError.stack}\n` +
78
+ `=== End Failure Log ===\n\n`;
79
+
80
+ try {
81
+ await import('fs/promises').then(fs => fs.appendFile(failureLogFile, failureLogContent, 'utf8'));
82
+ console.log(`Recovery failure logged to: ${failureLogFile}`);
83
+ } catch (logError) {
84
+ console.error('Failed to log recovery failure:', logError.message);
85
+ }
86
+
87
+ console.error('Recovery attempt failed:', retryError.message);
88
+ throw new Error(`Failed to execute claude command even after interactive recovery. Original error: ${error.message}, Retry error: ${retryError.message}`);
89
+ }
90
+ }
91
+
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Attempts to execute a claude command and parse the JSON output.
98
+ * @param {string} prompt The prompt to send to Claude.
99
+ * @param {string|null} sessionId Session ID to resume, or null for new session.
100
+ * @param {object} options Options object for task configuration.
101
+ * @returns {Promise<object>} The parsed result object.
102
+ */
103
+ async function attemptClaudeExecution(prompt, sessionId = null, options = {}) {
104
+ const timestamp = new Date().toISOString();
105
+ const logFile = `./.eck/logs/claude-execution-${Date.now()}.log`;
106
+
107
+ try {
108
+ // Use spawn instead of execa for better control over streaming and timeouts
109
+ const result = await executeClaudeWithDynamicTimeout(prompt, sessionId, options);
110
+ const { stdout, stderr } = result;
111
+
112
+ // Логируем весь вывод в файл
113
+ const commandStr = sessionId ?
114
+ `claude "${prompt}" --resume ${sessionId} -p --output-format=stream-json --verbose` :
115
+ `claude "${prompt}" -p --output-format=stream-json --verbose`;
116
+ const logContent = `=== Claude Execution Log ${timestamp} ===\n` +
117
+ `Command: ${commandStr}\n` +
118
+ `STDOUT:\n${stdout}\n` +
119
+ `STDERR:\n${stderr}\n` +
120
+ `=== End Log ===\n\n`;
121
+
122
+ await import('fs/promises').then(fs => fs.appendFile(logFile, logContent, 'utf8'));
123
+ console.log(`Claude execution logged to: ${logFile}`);
124
+
125
+ if (stderr) {
126
+ console.warn('Warning from claude-code process:', stderr);
127
+ }
128
+
129
+ const lines = stdout.trim().split('\n');
130
+
131
+ // Find the final result JSON object
132
+ let resultJson = null;
133
+ for (const line of lines) {
134
+ try {
135
+ const parsed = JSON.parse(line);
136
+ if (parsed.type === 'result') {
137
+ resultJson = parsed;
138
+ }
139
+ } catch (e) {
140
+ // Skip invalid JSON lines
141
+ continue;
142
+ }
143
+ }
144
+
145
+ if (!resultJson) {
146
+ throw new Error('No result JSON found in claude-code output.');
147
+ }
148
+
149
+ // Parse the result using Eck-Protocol v2 parser
150
+ const parsed = parseWithFallback(resultJson.result || '');
151
+
152
+ return {
153
+ result: resultJson.result,
154
+ parsed: parsed, // Structured data from Eck-Protocol v2
155
+ cost: resultJson.total_cost_usd,
156
+ usage: resultJson.usage,
157
+ duration_ms: resultJson.duration_ms
158
+ };
159
+ } catch (error) {
160
+ // Логируем ошибки тоже
161
+ const errorLogContent = `=== Claude Execution Error ${timestamp} ===\n` +
162
+ `Command: claude "${prompt}" -p --output-format=stream-json --verbose\n` +
163
+ `Error: ${error.message}\n` +
164
+ `Stack: ${error.stack}\n` +
165
+ `=== End Error Log ===\n\n`;
166
+
167
+ try {
168
+ await import('fs/promises').then(fs => fs.appendFile(logFile, errorLogContent, 'utf8'));
169
+ console.log(`Claude execution error logged to: ${logFile}`);
170
+ } catch (logError) {
171
+ console.error('Failed to log error:', logError.message);
172
+ }
173
+
174
+ throw error;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Checks if the error is related to Claude session limits.
180
+ * @param {Error} error The error to check.
181
+ * @returns {boolean} True if it's a session limit error.
182
+ */
183
+ function isSessionLimitError(error) {
184
+ // Don't treat simple timeouts as session limits
185
+ if (error.message.includes('Command timed out after') &&
186
+ !error.message.includes('5-hour') &&
187
+ !error.message.includes('limit')) {
188
+ return false;
189
+ }
190
+
191
+ const limitPatterns = [
192
+ /approaching 5-hour limit/i,
193
+ /5-hour limit/i,
194
+ /session limit reached/i,
195
+ /daily limit reached/i,
196
+ /usage limit reached/i,
197
+ /rate limit exceeded/i,
198
+ /quota exceeded/i,
199
+ /too many requests/i,
200
+ /maximum session duration/i,
201
+ /session expired/i
202
+ ];
203
+
204
+ const errorText = error.message + ' ' + (error.stdout || '') + ' ' + (error.stderr || '');
205
+ return limitPatterns.some(pattern => pattern.test(errorText));
206
+ }
207
+
208
+ /**
209
+ * Logs session limit errors with helpful recommendations.
210
+ * @param {Error} error The limit error.
211
+ * @param {string} prompt The original prompt.
212
+ */
213
+ async function logSessionLimitError(error, prompt) {
214
+ const timestamp = new Date().toISOString();
215
+ const currentTime = new Date();
216
+ const limitLogFile = `./.eck/logs/claude-session-limit-${Date.now()}.log`;
217
+
218
+ // Calculate suggested wait times based on error type
219
+ const limitInfo = analyzeLimitType(error.message);
220
+ const waitMinutes = limitInfo.suggestedWaitMinutes;
221
+ const resumeTime = new Date(currentTime.getTime() + waitMinutes * 60000);
222
+
223
+ const recommendations = [
224
+ "🛑 CLAUDE SESSION LIMIT REACHED",
225
+ "",
226
+ "📋 What happened:",
227
+ `- Error: ${error.message}`,
228
+ `- Prompt: "${prompt}"`,
229
+ `- Time: ${timestamp}`,
230
+ `- Limit type: ${limitInfo.type}`,
231
+ limitInfo.extractedFromMessage ? `- Claude said available again at: ${limitInfo.exactEndTime}` : "",
232
+ "",
233
+ "⏰ Timing information:",
234
+ `- Current time: ${currentTime.toLocaleString()}`,
235
+ `- Suggested wait: ${waitMinutes} minutes`,
236
+ `- Try again after: ${resumeTime.toLocaleString()}`,
237
+ `- Resume at: ${resumeTime.toISOString()}`,
238
+ limitInfo.extractedFromMessage ? "- ✅ Time extracted directly from Claude's message" : "- ⚠️ Time estimated based on limit type",
239
+ "",
240
+ "🔄 Recommended actions:",
241
+ `1. Take a break for at least ${waitMinutes} minutes`,
242
+ "2. Try again after the suggested time above",
243
+ limitInfo.type === '5-hour' ? "3. Consider splitting work into shorter sessions (< 4 hours)" : "3. Monitor usage to avoid hitting limits again",
244
+ "4. Check claude status page for any service issues",
245
+ "",
246
+ "⚡ Prevention tips:",
247
+ "- Use shorter, more focused prompts",
248
+ "- Batch multiple questions efficiently",
249
+ "- Take regular breaks during long coding sessions",
250
+ limitInfo.type === '5-hour' ? "- Set reminders to take breaks every 3-4 hours" : "",
251
+ "",
252
+ "📊 Full error details:"
253
+ ].filter(line => line !== ""); // Remove empty strings
254
+
255
+ const limitLogContent = recommendations.join('\n') + '\n' +
256
+ `STDOUT: ${error.stdout || 'N/A'}\n` +
257
+ `STDERR: ${error.stderr || 'N/A'}\n` +
258
+ `Stack: ${error.stack || 'N/A'}\n` +
259
+ `=== End Session Limit Log ===\n\n`;
260
+
261
+ try {
262
+ await import('fs/promises').then(fs => fs.appendFile(limitLogFile, limitLogContent, 'utf8'));
263
+ console.log(`🛑 Session limit error logged to: ${limitLogFile}`);
264
+ console.log(`⏰ Recommendation: Take a break and try again later!`);
265
+ } catch (logError) {
266
+ console.error('Failed to log session limit error:', logError.message);
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Analyzes the limit error message to determine wait time and type.
272
+ * @param {string} errorMessage The error message to analyze.
273
+ * @returns {{type: string, suggestedWaitMinutes: number}} Limit analysis results.
274
+ */
275
+ function analyzeLimitType(errorMessage) {
276
+ const message = errorMessage.toLowerCase();
277
+
278
+ // Try to extract exact end time from claude's message
279
+ const timePatterns = [
280
+ /session will end at (\d{1,2}:\d{2})/i,
281
+ /available again at (\d{1,2}:\d{2})/i,
282
+ /try again after (\d{1,2}:\d{2})/i,
283
+ /resume at (\d{1,2}:\d{2})/i,
284
+ /until (\d{1,2}:\d{2})/i
285
+ ];
286
+
287
+ for (const pattern of timePatterns) {
288
+ const match = errorMessage.match(pattern);
289
+ if (match) {
290
+ const timeString = match[1];
291
+ const [hours, minutes] = timeString.split(':').map(Number);
292
+ const now = new Date();
293
+ const endTime = new Date();
294
+ endTime.setHours(hours, minutes, 0, 0);
295
+
296
+ // If end time is earlier than now, assume it's tomorrow
297
+ if (endTime <= now) {
298
+ endTime.setDate(endTime.getDate() + 1);
299
+ }
300
+
301
+ const waitMinutes = Math.ceil((endTime - now) / (1000 * 60));
302
+ return {
303
+ type: 'exact-time',
304
+ suggestedWaitMinutes: Math.max(waitMinutes, 5), // At least 5 minutes
305
+ exactEndTime: endTime.toLocaleString(),
306
+ extractedFromMessage: true
307
+ };
308
+ }
309
+ }
310
+
311
+ if (message.includes('approaching 5-hour') || message.includes('5-hour limit')) {
312
+ // 5-hour limit - suggest waiting 1 hour (limits usually reset within 1-2 hours)
313
+ return {
314
+ type: '5-hour',
315
+ suggestedWaitMinutes: 60
316
+ };
317
+ }
318
+
319
+ if (message.includes('daily limit') || message.includes('24-hour')) {
320
+ // Daily limit - suggest waiting until next day
321
+ const now = new Date();
322
+ const tomorrow = new Date(now);
323
+ tomorrow.setDate(tomorrow.getDate() + 1);
324
+ tomorrow.setHours(0, 0, 0, 0); // Start of next day
325
+ const minutesUntilMidnight = Math.ceil((tomorrow - now) / (1000 * 60));
326
+
327
+ return {
328
+ type: 'daily',
329
+ suggestedWaitMinutes: Math.min(minutesUntilMidnight, 24 * 60) // Max 24 hours
330
+ };
331
+ }
332
+
333
+ if (message.includes('rate limit') || message.includes('too many requests')) {
334
+ // Rate limit - usually short, suggest 15-30 minutes
335
+ return {
336
+ type: 'rate-limit',
337
+ suggestedWaitMinutes: 30
338
+ };
339
+ }
340
+
341
+ if (message.includes('quota exceeded')) {
342
+ // Quota limit - could be monthly, suggest checking billing/usage
343
+ return {
344
+ type: 'quota',
345
+ suggestedWaitMinutes: 60
346
+ };
347
+ }
348
+
349
+ // Default for unknown limit types
350
+ return {
351
+ type: 'unknown',
352
+ suggestedWaitMinutes: 45
353
+ };
354
+ }
355
+
356
+ /**
357
+ * Extracts the last session_id from recent logs.
358
+ * @returns {Promise<string|null>} The last session_id or null if not found.
359
+ */
360
+ async function getLastSessionId() {
361
+ try {
362
+ const fs = await import('fs/promises');
363
+ const path = await import('path');
364
+
365
+ // Get all log files sorted by modification time (newest first)
366
+ const logFiles = await fs.readdir('./.eck/logs');
367
+ const executionLogs = logFiles
368
+ .filter(file => file.startsWith('claude-execution-') && file.endsWith('.log'))
369
+ .map(file => ({
370
+ name: file,
371
+ path: `./.eck/logs/${file}`,
372
+ time: parseInt(file.match(/claude-execution-(\d+)\.log/)?.[1] || '0')
373
+ }))
374
+ .sort((a, b) => b.time - a.time);
375
+
376
+ // Read the most recent log file
377
+ if (executionLogs.length > 0) {
378
+ const content = await fs.readFile(executionLogs[0].path, 'utf8');
379
+
380
+ // Extract session_id from the log content
381
+ const sessionMatch = content.match(/"session_id":"([^"]+)"/);
382
+ if (sessionMatch) {
383
+ return sessionMatch[1];
384
+ }
385
+ }
386
+
387
+ return null;
388
+ } catch (error) {
389
+ console.warn('Failed to extract session_id from logs:', error.message);
390
+ return null;
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Executes a prompt with a specific session ID.
396
+ * @param {string} prompt The prompt to send to Claude.
397
+ * @param {string} sessionId The specific session ID to resume.
398
+ * @param {object} options Options object for task configuration.
399
+ * @returns {Promise<object>} A promise that resolves with the final JSON output object from Claude.
400
+ */
401
+ export async function executePromptWithSession(prompt, sessionId, options = {}) {
402
+ console.log(`Resuming conversation with session: ${sessionId}`);
403
+ return await attemptClaudeExecution(prompt, sessionId, options);
404
+ }
405
+
406
+ /**
407
+ * Executes claude with dynamic timeout that extends when output is detected.
408
+ * @param {string} prompt The prompt to send to Claude.
409
+ * @param {string|null} sessionId Session ID to resume, or null for new session.
410
+ * @param {object} options Options object with taskSize for calculating dynamic timeout.
411
+ * @returns {Promise<{stdout: string, stderr: string}>} The execution result.
412
+ */
413
+ async function executeClaudeWithDynamicTimeout(prompt, sessionId = null, options = {}) {
414
+ return new Promise((resolve, reject) => {
415
+
416
+ const args = [];
417
+ if (sessionId) {
418
+ args.push('--resume', sessionId);
419
+ }
420
+
421
+ // Always add the skip permissions flag for automation reliability
422
+ args.push('--dangerously-skip-permissions');
423
+
424
+ args.push(prompt, '-p', '--output-format=stream-json', '--verbose');
425
+
426
+ const child = spawn('claude', args, {
427
+ stdio: ['ignore', 'pipe', 'pipe']
428
+ });
429
+
430
+ let stdout = '';
431
+ let stderr = '';
432
+ let lastOutputTime = Date.now();
433
+ let isFinished = false;
434
+
435
+ // Dynamic timeout calculation based on task size
436
+ const taskSize = options.taskSize || 0;
437
+ const BASE_TIMEOUT = 60000; // 60 seconds base
438
+ const PER_ITEM_TIMEOUT = 200; // 200ms per file/item
439
+ const ACTIVITY_TIMEOUT = BASE_TIMEOUT + (taskSize * PER_ITEM_TIMEOUT);
440
+
441
+ console.log(`⏱️ Using dynamic activity timeout: ${(ACTIVITY_TIMEOUT / 1000).toFixed(1)}s for ${taskSize} items`);
442
+
443
+ const INITIAL_TIMEOUT = 30000; // 30 seconds initial
444
+ const MAX_TOTAL_TIME = 20 * 60000; // 20 minutes maximum
445
+
446
+ // Reset timeout whenever we see new output
447
+ const resetTimeout = () => {
448
+ lastOutputTime = Date.now();
449
+ };
450
+
451
+ // Monitor for activity and kill if inactive too long
452
+ const activityChecker = setInterval(() => {
453
+ if (isFinished) return;
454
+
455
+ const timeSinceLastOutput = Date.now() - lastOutputTime;
456
+ const totalTime = Date.now() - lastOutputTime + timeSinceLastOutput;
457
+
458
+ if (totalTime > MAX_TOTAL_TIME) {
459
+ console.log('⏰ Maximum execution time reached (20 minutes)');
460
+ child.kill('SIGTERM');
461
+ clearInterval(activityChecker);
462
+ reject(new Error('Maximum execution time exceeded (20 minutes)'));
463
+ return;
464
+ }
465
+
466
+ if (timeSinceLastOutput > ACTIVITY_TIMEOUT) {
467
+ console.log(`💀 No activity detected for ${(ACTIVITY_TIMEOUT/1000).toFixed(1)}s, killing process`);
468
+ child.kill('SIGTERM');
469
+ clearInterval(activityChecker);
470
+ reject(new Error(`No output received for ${(ACTIVITY_TIMEOUT/1000).toFixed(1)} seconds`));
471
+ return;
472
+ }
473
+
474
+ // Show activity indicators we're looking for
475
+ if (stdout.includes('✻') || stdout.includes('🔍') || stdout.includes('⚙️') ||
476
+ stdout.includes('Forging') || stdout.includes('Processing') || stdout.includes('Searching')) {
477
+ console.log('✨ Claude is active, extending timeout...');
478
+ resetTimeout();
479
+ }
480
+ }, 5000); // Check every 5 seconds
481
+
482
+ child.stdout.on('data', (data) => {
483
+ stdout += data.toString();
484
+ resetTimeout();
485
+
486
+ // Log interesting activity
487
+ const newData = data.toString();
488
+ if (newData.includes('✻') || newData.includes('Forging') || newData.includes('Processing')) {
489
+ console.log('🔄 Activity detected:', newData.trim().substring(0, 50) + '...');
490
+ }
491
+ });
492
+
493
+ child.stderr.on('data', (data) => {
494
+ stderr += data.toString();
495
+ resetTimeout();
496
+ });
497
+
498
+ child.on('close', (code) => {
499
+ isFinished = true;
500
+ clearInterval(activityChecker);
501
+
502
+ if (code === 0) {
503
+ resolve({ stdout, stderr });
504
+ } else {
505
+ reject(new Error(`Claude process exited with code ${code}`));
506
+ }
507
+ });
508
+
509
+ child.on('error', (error) => {
510
+ isFinished = true;
511
+ clearInterval(activityChecker);
512
+ reject(error);
513
+ });
514
+
515
+ // Initial timeout
516
+ setTimeout(() => {
517
+ if (!isFinished && stdout.length === 0) {
518
+ console.log('⏰ Initial timeout - no output received');
519
+ child.kill('SIGTERM');
520
+ clearInterval(activityChecker);
521
+ reject(new Error('Initial timeout - no response from claude'));
522
+ }
523
+ }, INITIAL_TIMEOUT);
524
+ });
525
+ }
526
+
527
+ /**
528
+ * Executes a prompt using gemini-cli delegation with retry logic for transient errors.
529
+ * @param {string} prompt The prompt to send to Claude via gemini-cli.
530
+ * @returns {Promise<object>} A promise that resolves with the response from Claude.
531
+ */
532
+ export async function askClaude(prompt) {
533
+ return pRetry(async () => {
534
+ try {
535
+ const result = await execa('gemini-cli', ['claude', prompt], {
536
+ timeout: 120000 // 2 minute timeout
537
+ });
538
+
539
+ // Parse mcp_feedback if present in prompt
540
+ let mcpFeedback = null;
541
+ try {
542
+ const promptObj = JSON.parse(prompt);
543
+ if (promptObj.payload && promptObj.payload.post_execution_steps && promptObj.payload.post_execution_steps.mcp_feedback) {
544
+ mcpFeedback = promptObj.payload.post_execution_steps.mcp_feedback;
545
+
546
+ // Log if errors array is non-empty
547
+ if (mcpFeedback.errors && Array.isArray(mcpFeedback.errors) && mcpFeedback.errors.length > 0) {
548
+ console.warn('MCP feedback contains errors:', mcpFeedback.errors);
549
+ }
550
+ }
551
+ } catch (parseError) {
552
+ // If prompt is not valid JSON or doesn't contain mcp_feedback, continue normally
553
+ }
554
+
555
+ return {
556
+ stdout: result.stdout,
557
+ stderr: result.stderr,
558
+ success: true,
559
+ mcp_feedback: mcpFeedback
560
+ };
561
+ } catch (error) {
562
+ // Check if this is a transient error that should be retried
563
+ if (isTransientError(error)) {
564
+ console.log(`Transient error detected, retrying: ${error.message}`);
565
+ throw error; // This will trigger a retry
566
+ }
567
+
568
+ // Non-transient errors should not be retried
569
+ console.error(`Non-transient error in askClaude: ${error.message}`);
570
+ return {
571
+ stdout: error.stdout || '',
572
+ stderr: error.stderr || error.message,
573
+ success: false,
574
+ error: error.message
575
+ };
576
+ }
577
+ }, {
578
+ retries: 3,
579
+ minTimeout: 1000,
580
+ maxTimeout: 5000,
581
+ onFailedAttempt: (error) => {
582
+ console.log(`Attempt ${error.attemptNumber} failed. ${error.retriesLeft} retries left.`);
583
+ }
584
+ });
585
+ }
586
+
587
+ /**
588
+ * Checks if an error is transient and should be retried.
589
+ * @param {Error} error The error to check.
590
+ * @returns {boolean} True if the error is transient.
591
+ */
592
+ export function isTransientError(error) {
593
+ const errorMessage = (error.message || '').toLowerCase();
594
+ const stderr = (error.stderr || '').toLowerCase();
595
+ const stdout = (error.stdout || '').toLowerCase();
596
+ const allOutput = `${errorMessage} ${stderr} ${stdout}`;
597
+
598
+ // Network-related errors
599
+ const networkErrors = [
600
+ 'network',
601
+ 'timeout',
602
+ 'connection',
603
+ 'econnreset',
604
+ 'enotfound',
605
+ 'econnrefused',
606
+ 'socket hang up'
607
+ ];
608
+
609
+ // Quota/rate limit errors
610
+ const quotaErrors = [
611
+ 'quota exceeded',
612
+ 'rate limit',
613
+ 'too many requests',
614
+ 'service unavailable',
615
+ 'temporarily unavailable',
616
+ '429',
617
+ '500',
618
+ '502',
619
+ '503',
620
+ '504'
621
+ ];
622
+
623
+ const transientPatterns = [...networkErrors, ...quotaErrors];
624
+
625
+ return transientPatterns.some(pattern => allOutput.includes(pattern));
626
+ }