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