@link-assistant/hive-mind 0.46.1 → 0.47.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 (63) hide show
  1. package/CHANGELOG.md +10 -15
  2. package/README.md +42 -8
  3. package/package.json +16 -3
  4. package/src/agent.lib.mjs +49 -70
  5. package/src/agent.prompts.lib.mjs +6 -20
  6. package/src/buildUserMention.lib.mjs +4 -17
  7. package/src/claude-limits.lib.mjs +15 -15
  8. package/src/claude.lib.mjs +617 -626
  9. package/src/claude.prompts.lib.mjs +7 -22
  10. package/src/codex.lib.mjs +39 -71
  11. package/src/codex.prompts.lib.mjs +6 -20
  12. package/src/config.lib.mjs +3 -16
  13. package/src/contributing-guidelines.lib.mjs +5 -18
  14. package/src/exit-handler.lib.mjs +4 -4
  15. package/src/git.lib.mjs +7 -7
  16. package/src/github-issue-creator.lib.mjs +17 -17
  17. package/src/github-linking.lib.mjs +8 -33
  18. package/src/github.batch.lib.mjs +20 -16
  19. package/src/github.graphql.lib.mjs +18 -18
  20. package/src/github.lib.mjs +89 -91
  21. package/src/hive.config.lib.mjs +50 -50
  22. package/src/hive.mjs +1293 -1296
  23. package/src/instrument.mjs +7 -11
  24. package/src/interactive-mode.lib.mjs +112 -138
  25. package/src/lenv-reader.lib.mjs +1 -6
  26. package/src/lib.mjs +36 -45
  27. package/src/lino.lib.mjs +2 -2
  28. package/src/local-ci-checks.lib.mjs +15 -14
  29. package/src/memory-check.mjs +52 -60
  30. package/src/model-mapping.lib.mjs +25 -32
  31. package/src/model-validation.lib.mjs +31 -31
  32. package/src/opencode.lib.mjs +37 -62
  33. package/src/opencode.prompts.lib.mjs +7 -21
  34. package/src/protect-branch.mjs +14 -15
  35. package/src/review.mjs +28 -27
  36. package/src/reviewers-hive.mjs +64 -69
  37. package/src/sentry.lib.mjs +13 -10
  38. package/src/solve.auto-continue.lib.mjs +48 -38
  39. package/src/solve.auto-pr.lib.mjs +111 -69
  40. package/src/solve.branch-errors.lib.mjs +17 -46
  41. package/src/solve.branch.lib.mjs +16 -23
  42. package/src/solve.config.lib.mjs +263 -261
  43. package/src/solve.error-handlers.lib.mjs +21 -79
  44. package/src/solve.execution.lib.mjs +10 -18
  45. package/src/solve.feedback.lib.mjs +25 -46
  46. package/src/solve.mjs +59 -60
  47. package/src/solve.preparation.lib.mjs +10 -36
  48. package/src/solve.repo-setup.lib.mjs +4 -19
  49. package/src/solve.repository.lib.mjs +37 -37
  50. package/src/solve.results.lib.mjs +32 -46
  51. package/src/solve.session.lib.mjs +7 -22
  52. package/src/solve.validation.lib.mjs +19 -17
  53. package/src/solve.watch.lib.mjs +20 -33
  54. package/src/start-screen.mjs +24 -24
  55. package/src/task.mjs +38 -44
  56. package/src/telegram-bot.mjs +125 -121
  57. package/src/telegram-top-command.lib.mjs +32 -48
  58. package/src/usage-limit.lib.mjs +9 -13
  59. package/src/version-info.lib.mjs +1 -1
  60. package/src/version.lib.mjs +1 -1
  61. package/src/youtrack/solve.youtrack.lib.mjs +3 -8
  62. package/src/youtrack/youtrack-sync.mjs +8 -14
  63. package/src/youtrack/youtrack.lib.mjs +26 -28
@@ -20,7 +20,7 @@ import { displayBudgetStats } from './claude.budget-stats.lib.mjs';
20
20
  * @param {number|null|undefined} num - Number to format
21
21
  * @returns {string} Formatted number string
22
22
  */
23
- export const formatNumber = (num) => {
23
+ export const formatNumber = num => {
24
24
  if (num === null || num === undefined) return 'N/A';
25
25
  // Convert to string and split on decimal point
26
26
  const parts = num.toString().split('.');
@@ -29,20 +29,18 @@ export const formatNumber = (num) => {
29
29
  // Add spaces every 3 digits from the right
30
30
  const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
31
31
  // Return with decimal part if it exists
32
- return decimalPart !== undefined
33
- ? `${formattedInteger}.${decimalPart}`
34
- : formattedInteger;
32
+ return decimalPart !== undefined ? `${formattedInteger}.${decimalPart}` : formattedInteger;
35
33
  };
36
34
  // Available model configurations
37
35
  export const availableModels = {
38
- 'sonnet': 'claude-sonnet-4-5-20250929', // Sonnet 4.5
39
- 'opus': 'claude-opus-4-5-20251101', // Opus 4.5
40
- 'haiku': 'claude-haiku-4-5-20251001', // Haiku 4.5
36
+ sonnet: 'claude-sonnet-4-5-20250929', // Sonnet 4.5
37
+ opus: 'claude-opus-4-5-20251101', // Opus 4.5
38
+ haiku: 'claude-haiku-4-5-20251001', // Haiku 4.5
41
39
  'haiku-3-5': 'claude-3-5-haiku-20241022', // Haiku 3.5
42
- 'haiku-3': 'claude-3-haiku-20240307', // Haiku 3
40
+ 'haiku-3': 'claude-3-haiku-20240307', // Haiku 3
43
41
  };
44
42
  // Model mapping to translate aliases to full model IDs
45
- export const mapModelToId = (model) => {
43
+ export const mapModelToId = model => {
46
44
  return availableModels[model] || model;
47
45
  };
48
46
  // Function to validate Claude CLI connection with retry logic
@@ -86,21 +84,25 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
86
84
  result = await $`timeout ${Math.floor(timeouts.claudeCli / 1000)} claude --model ${mappedModel} -p hi`;
87
85
  } catch (timeoutError) {
88
86
  if (timeoutError.code === 124) {
89
- await log(`❌ Claude CLI timed out after ${Math.floor(timeouts.claudeCli / 1000)} seconds`, { level: 'error' });
87
+ await log(`❌ Claude CLI timed out after ${Math.floor(timeouts.claudeCli / 1000)} seconds`, {
88
+ level: 'error',
89
+ });
90
90
  await log(' 💡 This may indicate Claude CLI is taking too long to respond', { level: 'error' });
91
- await log(` 💡 Try running 'claude --model ${mappedModel} -p hi' manually to verify it works`, { level: 'error' });
91
+ await log(` 💡 Try running 'claude --model ${mappedModel} -p hi' manually to verify it works`, {
92
+ level: 'error',
93
+ });
92
94
  return false;
93
95
  }
94
96
  // Re-throw if it's not a timeout error
95
97
  throw timeoutError;
96
98
  }
97
99
  }
98
-
100
+
99
101
  // Check for common error patterns
100
102
  const stdout = result.stdout?.toString() || '';
101
103
  const stderr = result.stderr?.toString() || '';
102
104
  // Check for JSON errors in stdout or stderr
103
- const checkForJsonError = (text) => {
105
+ const checkForJsonError = text => {
104
106
  try {
105
107
  // Look for JSON error patterns
106
108
  if (text.includes('"error"') && text.includes('"type"')) {
@@ -115,7 +117,7 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
115
117
  if (global.verboseMode) {
116
118
  reportError(e, {
117
119
  context: 'claude_json_error_parse',
118
- level: 'debug'
120
+ level: 'debug',
119
121
  });
120
122
  }
121
123
  }
@@ -123,20 +125,22 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
123
125
  };
124
126
  const jsonError = checkForJsonError(stdout) || checkForJsonError(stderr);
125
127
  // Check for API overload error pattern
126
- const isOverloadError = (stdout.includes('API Error: 500') && stdout.includes('Overloaded')) ||
127
- (stderr.includes('API Error: 500') && stderr.includes('Overloaded')) ||
128
- (jsonError && jsonError.type === 'api_error' && jsonError.message === 'Overloaded');
129
-
128
+ const isOverloadError = (stdout.includes('API Error: 500') && stdout.includes('Overloaded')) || (stderr.includes('API Error: 500') && stderr.includes('Overloaded')) || (jsonError && jsonError.type === 'api_error' && jsonError.message === 'Overloaded');
129
+
130
130
  // Handle overload errors with retry
131
131
  if (isOverloadError) {
132
132
  if (retryCount < maxRetries) {
133
133
  const delay = baseDelay * Math.pow(2, retryCount);
134
- await log(`⚠️ API overload error during validation. Retrying in ${delay / 1000} seconds...`, { level: 'warning' });
134
+ await log(`⚠️ API overload error during validation. Retrying in ${delay / 1000} seconds...`, {
135
+ level: 'warning',
136
+ });
135
137
  await new Promise(resolve => setTimeout(resolve, delay));
136
138
  retryCount++;
137
139
  return await attemptValidation();
138
140
  } else {
139
- await log(`❌ API overload error persisted after ${maxRetries} retries during validation`, { level: 'error' });
141
+ await log(`❌ API overload error persisted after ${maxRetries} retries during validation`, {
142
+ level: 'error',
143
+ });
140
144
  await log(' The API appears to be heavily loaded. Please try again later.', { level: 'error' });
141
145
  return false;
142
146
  }
@@ -146,7 +150,9 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
146
150
  if (exitCode !== 0) {
147
151
  // Command failed
148
152
  if (jsonError) {
149
- await log(`❌ Claude CLI authentication failed: ${jsonError.type} - ${jsonError.message}`, { level: 'error' });
153
+ await log(`❌ Claude CLI authentication failed: ${jsonError.type} - ${jsonError.message}`, {
154
+ level: 'error',
155
+ });
150
156
  } else {
151
157
  await log(`❌ Claude CLI failed with exit code ${exitCode}`, { level: 'error' });
152
158
  if (stderr) await log(` Error: ${stderr.trim()}`, { level: 'error' });
@@ -161,7 +167,9 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
161
167
  if (jsonError.type === 'api_error' && jsonError.message === 'Overloaded') {
162
168
  if (retryCount < maxRetries) {
163
169
  const delay = baseDelay * Math.pow(2, retryCount);
164
- await log(`⚠️ API overload error in response. Retrying in ${delay / 1000} seconds...`, { level: 'warning' });
170
+ await log(`⚠️ API overload error in response. Retrying in ${delay / 1000} seconds...`, {
171
+ level: 'warning',
172
+ });
165
173
  await new Promise(resolve => setTimeout(resolve, delay));
166
174
  retryCount++;
167
175
  return await attemptValidation();
@@ -181,11 +189,12 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
181
189
  return true;
182
190
  } catch (error) {
183
191
  const errorStr = error.message || error.toString();
184
- if ((errorStr.includes('API Error: 500') && errorStr.includes('Overloaded')) ||
185
- (errorStr.includes('api_error') && errorStr.includes('Overloaded'))) {
192
+ if ((errorStr.includes('API Error: 500') && errorStr.includes('Overloaded')) || (errorStr.includes('api_error') && errorStr.includes('Overloaded'))) {
186
193
  if (retryCount < maxRetries) {
187
194
  const delay = baseDelay * Math.pow(2, retryCount);
188
- await log(`⚠️ API overload error during validation. Retrying in ${delay / 1000} seconds...`, { level: 'warning' });
195
+ await log(`⚠️ API overload error during validation. Retrying in ${delay / 1000} seconds...`, {
196
+ level: 'warning',
197
+ });
189
198
  await new Promise(resolve => setTimeout(resolve, delay));
190
199
  retryCount++;
191
200
  return await attemptValidation();
@@ -203,7 +212,7 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
203
212
  return await attemptValidation();
204
213
  };
205
214
  // Function to handle Claude runtime switching between Node.js and Bun
206
- export const handleClaudeRuntimeSwitch = async (argv) => {
215
+ export const handleClaudeRuntimeSwitch = async argv => {
207
216
  if (argv['force-claude-bun-run']) {
208
217
  await log('\n🔧 Switching Claude runtime to bun...');
209
218
  try {
@@ -213,29 +222,29 @@ export const handleClaudeRuntimeSwitch = async (argv) => {
213
222
  } catch (bunError) {
214
223
  reportError(bunError, {
215
224
  context: 'claude.lib.mjs - bun availability check',
216
- level: 'error'
225
+ level: 'error',
217
226
  });
218
227
  await log('❌ Bun runtime not found. Please install bun first: https://bun.sh/', { level: 'error' });
219
228
  process.exit(1);
220
229
  }
221
-
230
+
222
231
  // Find Claude executable path
223
232
  const claudePathResult = await $`which claude`;
224
233
  const claudePath = claudePathResult.stdout.toString().trim();
225
-
234
+
226
235
  if (!claudePath) {
227
236
  await log('❌ Claude executable not found', { level: 'error' });
228
237
  process.exit(1);
229
238
  }
230
-
239
+
231
240
  await log(` Claude path: ${claudePath}`);
232
-
241
+
233
242
  try {
234
243
  await fs.access(claudePath, fs.constants.W_OK);
235
244
  } catch (accessError) {
236
245
  reportError(accessError, {
237
246
  context: 'claude.lib.mjs - Claude executable write permission check (bun)',
238
- level: 'error'
247
+ level: 'error',
239
248
  });
240
249
  await log('❌ Cannot write to Claude executable (permission denied)', { level: 'error' });
241
250
  await log(' Try running with sudo or changing file permissions', { level: 'error' });
@@ -249,35 +258,34 @@ export const handleClaudeRuntimeSwitch = async (argv) => {
249
258
  await log(' ✅ Claude is already configured to use bun');
250
259
  process.exit(0);
251
260
  }
252
-
261
+
253
262
  // Create backup
254
263
  const backupPath = `${claudePath}.nodejs-backup`;
255
264
  await $`cp "${claudePath}" "${backupPath}"`;
256
265
  await log(` 📦 Backup created: ${backupPath}`);
257
-
266
+
258
267
  // Read file content and replace shebang
259
268
  const content = await fs.readFile(claudePath, 'utf8');
260
269
  const newContent = content.replace(/^#!.*node.*$/m, '#!/usr/bin/env bun');
261
-
270
+
262
271
  if (content === newContent) {
263
272
  await log('⚠️ No Node.js shebang found to replace', { level: 'warning' });
264
273
  await log(` Current shebang: ${currentShebang}`, { level: 'warning' });
265
274
  process.exit(0);
266
275
  }
267
-
276
+
268
277
  await fs.writeFile(claudePath, newContent);
269
278
  await log(' ✅ Claude shebang updated to use bun');
270
279
  await log(' 🔄 Claude will now run with bun runtime');
271
-
272
280
  } catch (error) {
273
281
  await log(`❌ Failed to switch Claude to bun: ${cleanErrorMessage(error)}`, { level: 'error' });
274
282
  process.exit(1);
275
283
  }
276
-
284
+
277
285
  // Exit after switching runtime
278
286
  process.exit(0);
279
287
  }
280
-
288
+
281
289
  if (argv['force-claude-nodejs-run']) {
282
290
  await log('\n🔧 Restoring Claude runtime to Node.js...');
283
291
  try {
@@ -287,29 +295,29 @@ export const handleClaudeRuntimeSwitch = async (argv) => {
287
295
  } catch (nodeError) {
288
296
  reportError(nodeError, {
289
297
  context: 'claude.lib.mjs - Node.js availability check',
290
- level: 'error'
298
+ level: 'error',
291
299
  });
292
300
  await log('❌ Node.js runtime not found. Please install Node.js first', { level: 'error' });
293
301
  process.exit(1);
294
302
  }
295
-
303
+
296
304
  // Find Claude executable path
297
305
  const claudePathResult = await $`which claude`;
298
306
  const claudePath = claudePathResult.stdout.toString().trim();
299
-
307
+
300
308
  if (!claudePath) {
301
309
  await log('❌ Claude executable not found', { level: 'error' });
302
310
  process.exit(1);
303
311
  }
304
-
312
+
305
313
  await log(` Claude path: ${claudePath}`);
306
-
314
+
307
315
  try {
308
316
  await fs.access(claudePath, fs.constants.W_OK);
309
317
  } catch (accessError) {
310
318
  reportError(accessError, {
311
319
  context: 'claude.lib.mjs - Claude executable write permission check (nodejs)',
312
- level: 'error'
320
+ level: 'error',
313
321
  });
314
322
  await log('❌ Cannot write to Claude executable (permission denied)', { level: 'error' });
315
323
  await log(' Try running with sudo or changing file permissions', { level: 'error' });
@@ -323,7 +331,7 @@ export const handleClaudeRuntimeSwitch = async (argv) => {
323
331
  await log(' ✅ Claude is already configured to use Node.js');
324
332
  process.exit(0);
325
333
  }
326
-
334
+
327
335
  const backupPath = `${claudePath}.nodejs-backup`;
328
336
  try {
329
337
  await fs.access(backupPath);
@@ -333,30 +341,29 @@ export const handleClaudeRuntimeSwitch = async (argv) => {
333
341
  } catch (backupError) {
334
342
  reportError(backupError, {
335
343
  context: 'claude_restore_backup',
336
- level: 'info'
344
+ level: 'info',
337
345
  });
338
346
  // No backup available, manually update shebang
339
347
  await log(' 📝 No backup found, manually updating shebang...');
340
348
  const content = await fs.readFile(claudePath, 'utf8');
341
349
  const newContent = content.replace(/^#!.*bun.*$/m, '#!/usr/bin/env node');
342
-
350
+
343
351
  if (content === newContent) {
344
352
  await log('⚠️ No bun shebang found to replace', { level: 'warning' });
345
353
  await log(` Current shebang: ${currentShebang}`, { level: 'warning' });
346
354
  process.exit(0);
347
355
  }
348
-
356
+
349
357
  await fs.writeFile(claudePath, newContent);
350
358
  await log(' ✅ Claude shebang updated to use Node.js');
351
359
  }
352
-
360
+
353
361
  await log(' 🔄 Claude will now run with Node.js runtime');
354
-
355
362
  } catch (error) {
356
363
  await log(`❌ Failed to restore Claude to Node.js: ${cleanErrorMessage(error)}`, { level: 'error' });
357
364
  process.exit(1);
358
365
  }
359
-
366
+
360
367
  // Exit after restoring runtime
361
368
  process.exit(0);
362
369
  }
@@ -394,30 +401,8 @@ export const checkPlaywrightMcpAvailability = async () => {
394
401
  * @param {Object} params - Parameters for Claude execution
395
402
  * @returns {Object} Result of the execution including success status and session info
396
403
  */
397
- export const executeClaude = async (params) => {
398
- const {
399
- issueUrl,
400
- issueNumber,
401
- prNumber,
402
- prUrl,
403
- branchName,
404
- tempDir,
405
- isContinueMode,
406
- mergeStateStatus,
407
- forkedRepo,
408
- feedbackLines,
409
- forkActionsUrl,
410
- owner,
411
- repo,
412
- argv,
413
- log,
414
- setLogFile,
415
- getLogFile,
416
- formatAligned,
417
- getResourceSnapshot,
418
- claudePath,
419
- $
420
- } = params;
404
+ export const executeClaude = async params => {
405
+ const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, setLogFile, getLogFile, formatAligned, getResourceSnapshot, claudePath, $ } = params;
421
406
  // Import prompt building functions from claude.prompts.lib.mjs
422
407
  const { buildUserPrompt, buildSystemPrompt } = await import('./claude.prompts.lib.mjs');
423
408
  // Build the user prompt
@@ -435,7 +420,7 @@ export const executeClaude = async (params) => {
435
420
  forkActionsUrl,
436
421
  owner,
437
422
  repo,
438
- argv
423
+ argv,
439
424
  });
440
425
  // Build the system prompt
441
426
  const systemPrompt = buildSystemPrompt({
@@ -449,7 +434,7 @@ export const executeClaude = async (params) => {
449
434
  tempDir,
450
435
  isContinueMode,
451
436
  forkedRepo,
452
- argv
437
+ argv,
453
438
  });
454
439
  // Log prompt details in verbose mode
455
440
  if (argv.verbose) {
@@ -495,7 +480,7 @@ export const executeClaude = async (params) => {
495
480
  // For interactive mode
496
481
  owner,
497
482
  repo,
498
- prNumber
483
+ prNumber,
499
484
  });
500
485
  };
501
486
  /**
@@ -509,45 +494,47 @@ export const executeClaude = async (params) => {
509
494
  * @param {string} modelId - The model ID (e.g., "claude-sonnet-4-5-20250929")
510
495
  * @returns {Promise<Object|null>} Model information or null if not found
511
496
  */
512
- export const fetchModelInfo = async (modelId) => {
497
+ export const fetchModelInfo = async modelId => {
513
498
  try {
514
499
  const https = (await use('https')).default;
515
500
  return new Promise((resolve, reject) => {
516
- https.get('https://models.dev/api.json', (res) => {
517
- let data = '';
518
- res.on('data', (chunk) => {
519
- data += chunk;
520
- });
521
- res.on('end', () => {
522
- try {
523
- const apiData = JSON.parse(data);
524
- // For public pricing calculation, prefer Anthropic provider for Claude models
525
- // Check Anthropic provider first
526
- if (apiData.anthropic?.models?.[modelId]) {
527
- const modelInfo = apiData.anthropic.models[modelId];
528
- modelInfo.provider = apiData.anthropic.name || 'Anthropic';
529
- resolve(modelInfo);
530
- return;
531
- }
532
- // Search for the model across all other providers
533
- for (const provider of Object.values(apiData)) {
534
- if (provider.models && provider.models[modelId]) {
535
- const modelInfo = provider.models[modelId];
536
- // Add provider info
537
- modelInfo.provider = provider.name || provider.id;
501
+ https
502
+ .get('https://models.dev/api.json', res => {
503
+ let data = '';
504
+ res.on('data', chunk => {
505
+ data += chunk;
506
+ });
507
+ res.on('end', () => {
508
+ try {
509
+ const apiData = JSON.parse(data);
510
+ // For public pricing calculation, prefer Anthropic provider for Claude models
511
+ // Check Anthropic provider first
512
+ if (apiData.anthropic?.models?.[modelId]) {
513
+ const modelInfo = apiData.anthropic.models[modelId];
514
+ modelInfo.provider = apiData.anthropic.name || 'Anthropic';
538
515
  resolve(modelInfo);
539
516
  return;
540
517
  }
518
+ // Search for the model across all other providers
519
+ for (const provider of Object.values(apiData)) {
520
+ if (provider.models && provider.models[modelId]) {
521
+ const modelInfo = provider.models[modelId];
522
+ // Add provider info
523
+ modelInfo.provider = provider.name || provider.id;
524
+ resolve(modelInfo);
525
+ return;
526
+ }
527
+ }
528
+ // Model not found
529
+ resolve(null);
530
+ } catch (parseError) {
531
+ reject(parseError);
541
532
  }
542
- // Model not found
543
- resolve(null);
544
- } catch (parseError) {
545
- reject(parseError);
546
- }
533
+ });
534
+ })
535
+ .on('error', err => {
536
+ reject(err);
547
537
  });
548
- }).on('error', (err) => {
549
- reject(err);
550
- });
551
538
  });
552
539
  } catch {
553
540
  // If we can't fetch model info, return null and continue without it
@@ -570,14 +557,14 @@ export const calculateModelCost = (usage, modelInfo, includeBreakdown = false) =
570
557
  input: { tokens: 0, costPerMillion: 0, cost: 0 },
571
558
  cacheWrite: { tokens: 0, costPerMillion: 0, cost: 0 },
572
559
  cacheRead: { tokens: 0, costPerMillion: 0, cost: 0 },
573
- output: { tokens: 0, costPerMillion: 0, cost: 0 }
560
+ output: { tokens: 0, costPerMillion: 0, cost: 0 },
574
561
  };
575
562
  // Input tokens cost (per million tokens)
576
563
  if (usage.inputTokens && cost.input) {
577
564
  breakdown.input = {
578
565
  tokens: usage.inputTokens,
579
566
  costPerMillion: cost.input,
580
- cost: (usage.inputTokens / 1000000) * cost.input
567
+ cost: (usage.inputTokens / 1000000) * cost.input,
581
568
  };
582
569
  }
583
570
  // Cache creation tokens cost
@@ -585,7 +572,7 @@ export const calculateModelCost = (usage, modelInfo, includeBreakdown = false) =
585
572
  breakdown.cacheWrite = {
586
573
  tokens: usage.cacheCreationTokens,
587
574
  costPerMillion: cost.cache_write,
588
- cost: (usage.cacheCreationTokens / 1000000) * cost.cache_write
575
+ cost: (usage.cacheCreationTokens / 1000000) * cost.cache_write,
589
576
  };
590
577
  }
591
578
  // Cache read tokens cost
@@ -593,7 +580,7 @@ export const calculateModelCost = (usage, modelInfo, includeBreakdown = false) =
593
580
  breakdown.cacheRead = {
594
581
  tokens: usage.cacheReadTokens,
595
582
  costPerMillion: cost.cache_read,
596
- cost: (usage.cacheReadTokens / 1000000) * cost.cache_read
583
+ cost: (usage.cacheReadTokens / 1000000) * cost.cache_read,
597
584
  };
598
585
  }
599
586
  // Output tokens cost
@@ -601,14 +588,14 @@ export const calculateModelCost = (usage, modelInfo, includeBreakdown = false) =
601
588
  breakdown.output = {
602
589
  tokens: usage.outputTokens,
603
590
  costPerMillion: cost.output,
604
- cost: (usage.outputTokens / 1000000) * cost.output
591
+ cost: (usage.outputTokens / 1000000) * cost.output,
605
592
  };
606
593
  }
607
594
  const totalCost = breakdown.input.cost + breakdown.cacheWrite.cost + breakdown.cacheRead.cost + breakdown.output.cost;
608
595
  if (includeBreakdown) {
609
596
  return {
610
597
  total: totalCost,
611
- breakdown
598
+ breakdown,
612
599
  };
613
600
  }
614
601
  return totalCost;
@@ -631,8 +618,11 @@ const displayModelUsage = async (usage, log) => {
631
618
  { label: 'Output modalities', value: info.modalities?.output?.join(', ') || 'N/A' },
632
619
  { label: 'Knowledge cutoff', value: info.knowledge },
633
620
  { label: 'Released', value: info.release_date },
634
- { label: 'Capabilities', value: [info.attachment && 'Attachments', info.reasoning && 'Reasoning', info.temperature && 'Temperature', info.tool_call && 'Tool calls'].filter(Boolean).join(', ') || 'N/A' },
635
- { label: 'Open weights', value: info.open_weights ? 'Yes' : 'No' }
621
+ {
622
+ label: 'Capabilities',
623
+ value: [info.attachment && 'Attachments', info.reasoning && 'Reasoning', info.temperature && 'Temperature', info.tool_call && 'Tool calls'].filter(Boolean).join(', ') || 'N/A',
624
+ },
625
+ { label: 'Open weights', value: info.open_weights ? 'Yes' : 'No' },
636
626
  ];
637
627
  for (const { label, value } of fields) {
638
628
  if (value) await log(` ${label}: ${value}`);
@@ -663,7 +653,7 @@ const displayModelUsage = async (usage, log) => {
663
653
  { key: 'input', label: 'Input' },
664
654
  { key: 'cacheWrite', label: 'Cache write' },
665
655
  { key: 'cacheRead', label: 'Cache read' },
666
- { key: 'output', label: 'Output' }
656
+ { key: 'output', label: 'Output' },
667
657
  ];
668
658
  for (const { key, label } of types) {
669
659
  if (breakdown[key].tokens > 0) {
@@ -715,7 +705,7 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
715
705
  cacheCreation1hTokens: 0,
716
706
  cacheReadTokens: 0,
717
707
  outputTokens: 0,
718
- webSearchRequests: 0
708
+ webSearchRequests: 0,
719
709
  };
720
710
  }
721
711
  // Add input tokens
@@ -754,7 +744,7 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
754
744
  return null;
755
745
  }
756
746
  // Fetch model information for each model
757
- const modelInfoPromises = Object.keys(modelUsage).map(async (modelId) => {
747
+ const modelInfoPromises = Object.keys(modelUsage).map(async modelId => {
758
748
  const modelInfo = await fetchModelInfo(modelId);
759
749
  return { modelId, modelInfo };
760
750
  });
@@ -810,13 +800,13 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
810
800
  cacheReadTokens: totalCacheReadTokens,
811
801
  outputTokens: totalOutputTokens,
812
802
  totalTokens,
813
- totalCostUSD: hasCostData ? totalCostUSD : null
803
+ totalCostUSD: hasCostData ? totalCostUSD : null,
814
804
  };
815
805
  } catch (readError) {
816
806
  throw new Error(`Failed to read session file: ${readError.message}`);
817
807
  }
818
808
  };
819
- export const executeClaudeCommand = async (params) => {
809
+ export const executeClaudeCommand = async params => {
820
810
  const {
821
811
  tempDir,
822
812
  branchName,
@@ -833,11 +823,11 @@ export const executeClaudeCommand = async (params) => {
833
823
  forkedRepo,
834
824
  feedbackLines,
835
825
  claudePath,
836
- $, // Add command-stream $ to params
826
+ $, // Add command-stream $ to params
837
827
  // For interactive mode
838
828
  owner,
839
829
  repo,
840
- prNumber
830
+ prNumber,
841
831
  } = params;
842
832
  // Retry configuration for API overload errors
843
833
  const maxRetries = 3;
@@ -852,24 +842,24 @@ export const executeClaudeCommand = async (params) => {
852
842
  await log(`\n${formatAligned('🔄', 'Retry attempt:', `${retryCount}/${maxRetries}`)}`);
853
843
  }
854
844
  if (argv.verbose) {
855
- // Output the actual model being used
856
- const modelName = argv.model === 'opus' ? 'opus' : 'sonnet';
857
- await log(` Model: ${modelName}`, { verbose: true });
858
- await log(` Working directory: ${tempDir}`, { verbose: true });
859
- await log(` Branch: ${branchName}`, { verbose: true });
860
- await log(` Prompt length: ${prompt.length} chars`, { verbose: true });
861
- await log(` System prompt length: ${systemPrompt.length} chars`, { verbose: true });
862
- if (feedbackLines && feedbackLines.length > 0) {
863
- await log(` Feedback info included: Yes (${feedbackLines.length} lines)`, { verbose: true });
864
- } else {
865
- await log(' Feedback info included: No', { verbose: true });
845
+ // Output the actual model being used
846
+ const modelName = argv.model === 'opus' ? 'opus' : 'sonnet';
847
+ await log(` Model: ${modelName}`, { verbose: true });
848
+ await log(` Working directory: ${tempDir}`, { verbose: true });
849
+ await log(` Branch: ${branchName}`, { verbose: true });
850
+ await log(` Prompt length: ${prompt.length} chars`, { verbose: true });
851
+ await log(` System prompt length: ${systemPrompt.length} chars`, { verbose: true });
852
+ if (feedbackLines && feedbackLines.length > 0) {
853
+ await log(` Feedback info included: Yes (${feedbackLines.length} lines)`, { verbose: true });
854
+ } else {
855
+ await log(' Feedback info included: No', { verbose: true });
856
+ }
866
857
  }
867
- }
868
- // Take resource snapshot before execution
869
- const resourcesBefore = await getResourceSnapshot();
870
- await log('📈 System resources before execution:', { verbose: true });
871
- await log(` Memory: ${resourcesBefore.memory.split('\n')[1]}`, { verbose: true });
872
- await log(` Load: ${resourcesBefore.load}`, { verbose: true });
858
+ // Take resource snapshot before execution
859
+ const resourcesBefore = await getResourceSnapshot();
860
+ await log('📈 System resources before execution:', { verbose: true });
861
+ await log(` Memory: ${resourcesBefore.memory.split('\n')[1]}`, { verbose: true });
862
+ await log(` Load: ${resourcesBefore.load}`, { verbose: true });
873
863
  // Use command-stream's async iteration for real-time streaming with file logging
874
864
  let commandFailed = false;
875
865
  let sessionId = null;
@@ -883,534 +873,527 @@ export const executeClaudeCommand = async (params) => {
883
873
  let stderrErrors = [];
884
874
  let anthropicTotalCostUSD = null; // Capture Anthropic's official total_cost_usd from result
885
875
 
886
- // Create interactive mode handler if enabled
887
- let interactiveHandler = null;
888
- if (argv.interactiveMode && owner && repo && prNumber) {
889
- await log('🔌 Interactive mode: Creating handler for real-time PR comments', { verbose: true });
890
- interactiveHandler = createInteractiveHandler({
891
- owner,
892
- repo,
893
- prNumber,
894
- $,
895
- log,
896
- verbose: argv.verbose
897
- });
898
- } else if (argv.interactiveMode) {
899
- await log('⚠️ Interactive mode: Disabled - missing PR info (owner/repo/prNumber)', { verbose: true });
900
- }
876
+ // Create interactive mode handler if enabled
877
+ let interactiveHandler = null;
878
+ if (argv.interactiveMode && owner && repo && prNumber) {
879
+ await log('🔌 Interactive mode: Creating handler for real-time PR comments', { verbose: true });
880
+ interactiveHandler = createInteractiveHandler({
881
+ owner,
882
+ repo,
883
+ prNumber,
884
+ $,
885
+ log,
886
+ verbose: argv.verbose,
887
+ });
888
+ } else if (argv.interactiveMode) {
889
+ await log('⚠️ Interactive mode: Disabled - missing PR info (owner/repo/prNumber)', { verbose: true });
890
+ }
901
891
 
902
- // Build claude command with optional resume flag
903
- let execCommand;
904
- // Map model alias to full ID
905
- const mappedModel = mapModelToId(argv.model);
906
- // Build claude command arguments
907
- let claudeArgs = `--output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel}`;
908
- if (argv.resume) {
909
- await log(`🔄 Resuming from session: ${argv.resume}`);
910
- claudeArgs = `--resume ${argv.resume} ${claudeArgs}`;
911
- }
912
- claudeArgs += ` -p "${escapedPrompt}" --append-system-prompt "${escapedSystemPrompt}"`;
913
- // Build the full command for display (with jq for formatting as in v0.3.2)
914
- const fullCommand = `(cd "${tempDir}" && ${claudePath} ${claudeArgs} | jq -c .)`;
915
- // Print the actual raw command being executed
916
- await log(`\n${formatAligned('📝', 'Raw command:', '')}`);
917
- await log(`${fullCommand}`);
918
- await log('');
919
- // Output prompts in verbose mode for debugging
920
- if (argv.verbose) {
921
- await log('📋 User prompt:', { verbose: true });
922
- await log('---BEGIN USER PROMPT---', { verbose: true });
923
- await log(prompt, { verbose: true });
924
- await log('---END USER PROMPT---', { verbose: true });
925
- await log('', { verbose: true });
926
- await log('📋 System prompt:', { verbose: true });
927
- await log('---BEGIN SYSTEM PROMPT---', { verbose: true });
928
- await log(systemPrompt, { verbose: true });
929
- await log('---END SYSTEM PROMPT---', { verbose: true });
930
- await log('', { verbose: true });
931
- }
932
- try {
892
+ // Build claude command with optional resume flag
893
+ let execCommand;
894
+ // Map model alias to full ID
895
+ const mappedModel = mapModelToId(argv.model);
896
+ // Build claude command arguments
897
+ let claudeArgs = `--output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel}`;
933
898
  if (argv.resume) {
934
- // When resuming, pass prompt directly with -p flag
935
- // Use simpler escaping - just escape double quotes
936
- const simpleEscapedPrompt = prompt.replace(/"/g, '\\"');
937
- const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
938
- execCommand = $({
939
- cwd: tempDir,
940
- mirror: false
941
- })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
942
- } else {
943
- // When not resuming, pass prompt via stdin
944
- // For system prompt, escape it properly for shell - just escape double quotes
945
- const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
946
- execCommand = $({
947
- cwd: tempDir,
948
- stdin: prompt,
949
- mirror: false
950
- })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel} --append-system-prompt "${simpleEscapedSystem}"`;
899
+ await log(`🔄 Resuming from session: ${argv.resume}`);
900
+ claudeArgs = `--resume ${argv.resume} ${claudeArgs}`;
951
901
  }
952
- await log(`${formatAligned('📋', 'Command details:', '')}`);
953
- await log(formatAligned('📂', 'Working directory:', tempDir, 2));
954
- await log(formatAligned('🌿', 'Branch:', branchName, 2));
955
- await log(formatAligned('🤖', 'Model:', `Claude ${argv.model.toUpperCase()}`, 2));
956
- if (argv.fork && forkedRepo) {
957
- await log(formatAligned('🍴', 'Fork:', forkedRepo, 2));
902
+ claudeArgs += ` -p "${escapedPrompt}" --append-system-prompt "${escapedSystemPrompt}"`;
903
+ // Build the full command for display (with jq for formatting as in v0.3.2)
904
+ const fullCommand = `(cd "${tempDir}" && ${claudePath} ${claudeArgs} | jq -c .)`;
905
+ // Print the actual raw command being executed
906
+ await log(`\n${formatAligned('📝', 'Raw command:', '')}`);
907
+ await log(`${fullCommand}`);
908
+ await log('');
909
+ // Output prompts in verbose mode for debugging
910
+ if (argv.verbose) {
911
+ await log('📋 User prompt:', { verbose: true });
912
+ await log('---BEGIN USER PROMPT---', { verbose: true });
913
+ await log(prompt, { verbose: true });
914
+ await log('---END USER PROMPT---', { verbose: true });
915
+ await log('', { verbose: true });
916
+ await log('📋 System prompt:', { verbose: true });
917
+ await log('---BEGIN SYSTEM PROMPT---', { verbose: true });
918
+ await log(systemPrompt, { verbose: true });
919
+ await log('---END SYSTEM PROMPT---', { verbose: true });
920
+ await log('', { verbose: true });
958
921
  }
959
- await log(`\n${formatAligned('▶️', 'Streaming output:', '')}\n`);
960
- // Use command-stream's async iteration for real-time streaming
961
- let exitCode = 0;
962
- for await (const chunk of execCommand.stream()) {
963
- if (chunk.type === 'stdout') {
964
- const output = chunk.data.toString();
965
- // Split output into individual lines for NDJSON parsing
966
- // Claude CLI outputs NDJSON (newline-delimited JSON) format where each line is a separate JSON object
967
- // This allows us to parse each event independently and extract structured data like session IDs,
968
- // message counts, and error patterns. Attempting to parse the entire chunk as single JSON would fail
969
- // since multiple JSON objects aren't valid JSON together.
970
- const lines = output.split('\n');
971
- for (const line of lines) {
972
- if (!line.trim()) continue;
973
- try {
974
- const data = JSON.parse(line);
975
- // Process event in interactive mode (posts PR comments in real-time)
976
- if (interactiveHandler) {
977
- try {
978
- await interactiveHandler.processEvent(data);
979
- } catch (interactiveError) {
980
- // Don't let interactive mode errors stop the main execution
981
- await log(`⚠️ Interactive mode error: ${interactiveError.message}`, { verbose: true });
922
+ try {
923
+ if (argv.resume) {
924
+ // When resuming, pass prompt directly with -p flag
925
+ // Use simpler escaping - just escape double quotes
926
+ const simpleEscapedPrompt = prompt.replace(/"/g, '\\"');
927
+ const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
928
+ execCommand = $({
929
+ cwd: tempDir,
930
+ mirror: false,
931
+ })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
932
+ } else {
933
+ // When not resuming, pass prompt via stdin
934
+ // For system prompt, escape it properly for shell - just escape double quotes
935
+ const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
936
+ execCommand = $({
937
+ cwd: tempDir,
938
+ stdin: prompt,
939
+ mirror: false,
940
+ })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel} --append-system-prompt "${simpleEscapedSystem}"`;
941
+ }
942
+ await log(`${formatAligned('📋', 'Command details:', '')}`);
943
+ await log(formatAligned('📂', 'Working directory:', tempDir, 2));
944
+ await log(formatAligned('🌿', 'Branch:', branchName, 2));
945
+ await log(formatAligned('🤖', 'Model:', `Claude ${argv.model.toUpperCase()}`, 2));
946
+ if (argv.fork && forkedRepo) {
947
+ await log(formatAligned('🍴', 'Fork:', forkedRepo, 2));
948
+ }
949
+ await log(`\n${formatAligned('▶️', 'Streaming output:', '')}\n`);
950
+ // Use command-stream's async iteration for real-time streaming
951
+ let exitCode = 0;
952
+ for await (const chunk of execCommand.stream()) {
953
+ if (chunk.type === 'stdout') {
954
+ const output = chunk.data.toString();
955
+ // Split output into individual lines for NDJSON parsing
956
+ // Claude CLI outputs NDJSON (newline-delimited JSON) format where each line is a separate JSON object
957
+ // This allows us to parse each event independently and extract structured data like session IDs,
958
+ // message counts, and error patterns. Attempting to parse the entire chunk as single JSON would fail
959
+ // since multiple JSON objects aren't valid JSON together.
960
+ const lines = output.split('\n');
961
+ for (const line of lines) {
962
+ if (!line.trim()) continue;
963
+ try {
964
+ const data = JSON.parse(line);
965
+ // Process event in interactive mode (posts PR comments in real-time)
966
+ if (interactiveHandler) {
967
+ try {
968
+ await interactiveHandler.processEvent(data);
969
+ } catch (interactiveError) {
970
+ // Don't let interactive mode errors stop the main execution
971
+ await log(`⚠️ Interactive mode error: ${interactiveError.message}`, { verbose: true });
972
+ }
982
973
  }
983
- }
984
- // Output formatted JSON as in v0.3.2
985
- await log(JSON.stringify(data, null, 2));
986
- // Capture session ID from the first message
987
- if (!sessionId && data.session_id) {
988
- sessionId = data.session_id;
989
- await log(`📌 Session ID: ${sessionId}`);
990
- // Try to rename log file to include session ID
991
- let sessionLogFile;
992
- try {
993
- const currentLogFile = getLogFile();
994
- const logDir = path.dirname(currentLogFile);
995
- sessionLogFile = path.join(logDir, `${sessionId}.log`);
996
- // Use fs.promises to rename the file
997
- await fs.rename(currentLogFile, sessionLogFile);
998
- // Update the global log file reference
999
- setLogFile(sessionLogFile);
1000
- await log(`📁 Log renamed to: ${sessionLogFile}`);
1001
- } catch (renameError) {
1002
- reportError(renameError, {
1003
- context: 'rename_session_log',
1004
- sessionId,
1005
- sessionLogFile,
1006
- operation: 'rename_log_file'
1007
- });
1008
- // If rename fails, keep original filename
1009
- await log(`⚠️ Could not rename log file: ${renameError.message}`, { verbose: true });
974
+ // Output formatted JSON as in v0.3.2
975
+ await log(JSON.stringify(data, null, 2));
976
+ // Capture session ID from the first message
977
+ if (!sessionId && data.session_id) {
978
+ sessionId = data.session_id;
979
+ await log(`📌 Session ID: ${sessionId}`);
980
+ // Try to rename log file to include session ID
981
+ let sessionLogFile;
982
+ try {
983
+ const currentLogFile = getLogFile();
984
+ const logDir = path.dirname(currentLogFile);
985
+ sessionLogFile = path.join(logDir, `${sessionId}.log`);
986
+ // Use fs.promises to rename the file
987
+ await fs.rename(currentLogFile, sessionLogFile);
988
+ // Update the global log file reference
989
+ setLogFile(sessionLogFile);
990
+ await log(`📁 Log renamed to: ${sessionLogFile}`);
991
+ } catch (renameError) {
992
+ reportError(renameError, {
993
+ context: 'rename_session_log',
994
+ sessionId,
995
+ sessionLogFile,
996
+ operation: 'rename_log_file',
997
+ });
998
+ // If rename fails, keep original filename
999
+ await log(`⚠️ Could not rename log file: ${renameError.message}`, { verbose: true });
1000
+ }
1010
1001
  }
1011
- }
1012
- // Track message and tool use counts
1013
- if (data.type === 'message') {
1014
- messageCount++;
1015
- } else if (data.type === 'tool_use') {
1016
- toolUseCount++;
1017
- }
1018
- // Handle session result type from Claude CLI
1019
- // This is emitted when a session completes, either successfully or with an error
1020
- // Example: {"type": "result", "subtype": "success", "is_error": true, "result": "Session limit reached ∙ resets 10am"}
1021
- if (data.type === 'result') {
1022
- // Capture Anthropic's official total_cost_usd from the result
1023
- if (data.total_cost_usd !== undefined && data.total_cost_usd !== null) {
1024
- anthropicTotalCostUSD = data.total_cost_usd;
1025
- await log(`💰 Anthropic official cost captured: $${anthropicTotalCostUSD.toFixed(6)}`, { verbose: true });
1002
+ // Track message and tool use counts
1003
+ if (data.type === 'message') {
1004
+ messageCount++;
1005
+ } else if (data.type === 'tool_use') {
1006
+ toolUseCount++;
1026
1007
  }
1027
- if (data.is_error === true) {
1028
- commandFailed = true;
1029
- lastMessage = data.result || JSON.stringify(data);
1030
- await log('⚠️ Detected error result from Claude CLI', { verbose: true });
1031
- if (lastMessage.includes('Session limit reached') || lastMessage.includes('limit reached')) {
1032
- limitReached = true;
1033
- await log('⚠️ Detected session limit in result', { verbose: true });
1008
+ // Handle session result type from Claude CLI
1009
+ // This is emitted when a session completes, either successfully or with an error
1010
+ // Example: {"type": "result", "subtype": "success", "is_error": true, "result": "Session limit reached ∙ resets 10am"}
1011
+ if (data.type === 'result') {
1012
+ // Capture Anthropic's official total_cost_usd from the result
1013
+ if (data.total_cost_usd !== undefined && data.total_cost_usd !== null) {
1014
+ anthropicTotalCostUSD = data.total_cost_usd;
1015
+ await log(`💰 Anthropic official cost captured: $${anthropicTotalCostUSD.toFixed(6)}`, {
1016
+ verbose: true,
1017
+ });
1034
1018
  }
1035
- }
1036
- }
1037
- // Store last message for error detection
1038
- if (data.type === 'text' && data.text) {
1039
- lastMessage = data.text;
1040
- } else if (data.type === 'error') {
1041
- lastMessage = data.error || JSON.stringify(data);
1042
- }
1043
- // Check for API overload error and 503 errors
1044
- if (data.type === 'assistant' && data.message && data.message.content) {
1045
- const content = Array.isArray(data.message.content) ? data.message.content : [data.message.content];
1046
- for (const item of content) {
1047
- if (item.type === 'text' && item.text) {
1048
- // Check for the specific 500 overload error pattern
1049
- if (item.text.includes('API Error: 500') &&
1050
- item.text.includes('api_error') &&
1051
- item.text.includes('Overloaded')) {
1052
- isOverloadError = true;
1053
- lastMessage = item.text;
1054
- await log('⚠️ Detected API overload error', { verbose: true });
1019
+ if (data.is_error === true) {
1020
+ commandFailed = true;
1021
+ lastMessage = data.result || JSON.stringify(data);
1022
+ await log('⚠️ Detected error result from Claude CLI', { verbose: true });
1023
+ if (lastMessage.includes('Session limit reached') || lastMessage.includes('limit reached')) {
1024
+ limitReached = true;
1025
+ await log('⚠️ Detected session limit in result', { verbose: true });
1055
1026
  }
1056
- // Check for 503 errors
1057
- if (item.text.includes('API Error: 503') ||
1058
- (item.text.includes('503') && item.text.includes('upstream connect error')) ||
1059
- (item.text.includes('503') && item.text.includes('remote connection failure'))) {
1060
- is503Error = true;
1061
- lastMessage = item.text;
1062
- await log('⚠️ Detected 503 network error', { verbose: true });
1027
+ }
1028
+ }
1029
+ // Store last message for error detection
1030
+ if (data.type === 'text' && data.text) {
1031
+ lastMessage = data.text;
1032
+ } else if (data.type === 'error') {
1033
+ lastMessage = data.error || JSON.stringify(data);
1034
+ }
1035
+ // Check for API overload error and 503 errors
1036
+ if (data.type === 'assistant' && data.message && data.message.content) {
1037
+ const content = Array.isArray(data.message.content) ? data.message.content : [data.message.content];
1038
+ for (const item of content) {
1039
+ if (item.type === 'text' && item.text) {
1040
+ // Check for the specific 500 overload error pattern
1041
+ if (item.text.includes('API Error: 500') && item.text.includes('api_error') && item.text.includes('Overloaded')) {
1042
+ isOverloadError = true;
1043
+ lastMessage = item.text;
1044
+ await log('⚠️ Detected API overload error', { verbose: true });
1045
+ }
1046
+ // Check for 503 errors
1047
+ if (item.text.includes('API Error: 503') || (item.text.includes('503') && item.text.includes('upstream connect error')) || (item.text.includes('503') && item.text.includes('remote connection failure'))) {
1048
+ is503Error = true;
1049
+ lastMessage = item.text;
1050
+ await log('⚠️ Detected 503 network error', { verbose: true });
1051
+ }
1063
1052
  }
1064
1053
  }
1065
1054
  }
1066
- }
1067
- } catch (parseError) {
1068
- // JSON parse errors are expected for non-JSON output
1069
- // Only report in verbose mode
1070
- if (global.verboseMode) {
1071
- reportError(parseError, {
1072
- context: 'parse_claude_output',
1073
- line,
1074
- operation: 'parse_json_output',
1075
- level: 'debug'
1076
- });
1077
- }
1078
- // Not JSON or parsing failed, output as-is if it's not empty
1079
- if (line.trim() && !line.includes('node:internal')) {
1080
- await log(line, { stream: 'raw' });
1081
- lastMessage = line;
1055
+ } catch (parseError) {
1056
+ // JSON parse errors are expected for non-JSON output
1057
+ // Only report in verbose mode
1058
+ if (global.verboseMode) {
1059
+ reportError(parseError, {
1060
+ context: 'parse_claude_output',
1061
+ line,
1062
+ operation: 'parse_json_output',
1063
+ level: 'debug',
1064
+ });
1065
+ }
1066
+ // Not JSON or parsing failed, output as-is if it's not empty
1067
+ if (line.trim() && !line.includes('node:internal')) {
1068
+ await log(line, { stream: 'raw' });
1069
+ lastMessage = line;
1070
+ }
1082
1071
  }
1083
1072
  }
1084
1073
  }
1085
- }
1086
- if (chunk.type === 'stderr') {
1087
- const errorOutput = chunk.data.toString();
1088
- // Log stderr immediately
1089
- if (errorOutput) {
1090
- await log(errorOutput, { stream: 'stderr' });
1091
- // Track stderr errors for failure detection
1092
- const trimmed = errorOutput.trim();
1093
- // Exclude warnings (messages starting with ⚠️) from being treated as errors
1094
- // Example: "⚠️ [BashTool] Pre-flight check is taking longer than expected. Run with ANTHROPIC_LOG=debug to check for failed or slow API requests."
1095
- // Even though this contains the word "failed", it's a warning, not an error
1096
- const isWarning = trimmed.startsWith('⚠️') || trimmed.startsWith('');
1097
- if (trimmed && !isWarning && (trimmed.includes('Error:') || trimmed.includes('error') || trimmed.includes('failed'))) {
1098
- stderrErrors.push(trimmed);
1074
+ if (chunk.type === 'stderr') {
1075
+ const errorOutput = chunk.data.toString();
1076
+ // Log stderr immediately
1077
+ if (errorOutput) {
1078
+ await log(errorOutput, { stream: 'stderr' });
1079
+ // Track stderr errors for failure detection
1080
+ const trimmed = errorOutput.trim();
1081
+ // Exclude warnings (messages starting with ⚠️) from being treated as errors
1082
+ // Example: "⚠️ [BashTool] Pre-flight check is taking longer than expected. Run with ANTHROPIC_LOG=debug to check for failed or slow API requests."
1083
+ // Even though this contains the word "failed", it's a warning, not an error
1084
+ const isWarning = trimmed.startsWith('⚠️') || trimmed.startsWith('⚠');
1085
+ if (trimmed && !isWarning && (trimmed.includes('Error:') || trimmed.includes('error') || trimmed.includes('failed'))) {
1086
+ stderrErrors.push(trimmed);
1087
+ }
1099
1088
  }
1089
+ } else if (chunk.type === 'exit') {
1090
+ exitCode = chunk.code;
1091
+ if (chunk.code !== 0) {
1092
+ commandFailed = true;
1093
+ }
1094
+ // Don't break here - let the loop finish naturally to process all output
1100
1095
  }
1101
- } else if (chunk.type === 'exit') {
1102
- exitCode = chunk.code;
1103
- if (chunk.code !== 0) {
1104
- commandFailed = true;
1105
- }
1106
- // Don't break here - let the loop finish naturally to process all output
1107
1096
  }
1108
- }
1109
1097
 
1110
- // Flush any remaining queued comments from interactive mode
1111
- if (interactiveHandler) {
1112
- try {
1113
- await interactiveHandler.flush();
1114
- } catch (flushError) {
1115
- await log(`⚠️ Interactive mode flush error: ${flushError.message}`, { verbose: true });
1098
+ // Flush any remaining queued comments from interactive mode
1099
+ if (interactiveHandler) {
1100
+ try {
1101
+ await interactiveHandler.flush();
1102
+ } catch (flushError) {
1103
+ await log(`⚠️ Interactive mode flush error: ${flushError.message}`, { verbose: true });
1104
+ }
1116
1105
  }
1117
- }
1118
1106
 
1119
- if ((commandFailed || isOverloadError) &&
1120
- (isOverloadError ||
1121
- (lastMessage.includes('API Error: 500') && lastMessage.includes('Overloaded')) ||
1122
- (lastMessage.includes('api_error') && lastMessage.includes('Overloaded')))) {
1123
- if (retryCount < maxRetries) {
1124
- // Calculate exponential backoff delay
1125
- const delay = baseDelay * Math.pow(2, retryCount);
1126
- await log(`\n⚠️ API overload error detected. Retrying in ${delay / 1000} seconds...`, { level: 'warning' });
1127
- await log(` Error: ${lastMessage.substring(0, 200)}`, { verbose: true });
1128
- // Wait before retrying
1129
- await new Promise(resolve => setTimeout(resolve, delay));
1130
- // Increment retry count and retry
1131
- retryCount++;
1132
- return await executeWithRetry();
1133
- } else {
1134
- await log(`\n\n❌ API overload error persisted after ${maxRetries} retries`, { level: 'error' });
1135
- await log(' The API appears to be heavily loaded. Please try again later.', { level: 'error' });
1136
- return {
1137
- success: false,
1138
- sessionId,
1139
- limitReached: false,
1140
- limitResetTime: null,
1141
- messageCount,
1142
- toolUseCount
1143
- };
1144
- }
1145
- }
1146
- if ((commandFailed || is503Error) && argv.autoResumeOnErrors &&
1147
- (is503Error ||
1148
- lastMessage.includes('API Error: 503') ||
1149
- (lastMessage.includes('503') && lastMessage.includes('upstream connect error')) ||
1150
- (lastMessage.includes('503') && lastMessage.includes('remote connection failure')))) {
1151
- if (retryCount < retryLimits.max503Retries) {
1152
- // Calculate exponential backoff delay starting from 5 minutes
1153
- const delay = retryLimits.initial503RetryDelayMs * Math.pow(retryLimits.retryBackoffMultiplier, retryCount);
1154
- const delayMinutes = Math.round(delay / (1000 * 60));
1155
- await log(`\n⚠️ 503 network error detected. Retrying in ${delayMinutes} minutes...`, { level: 'warning' });
1156
- await log(` Error: ${lastMessage.substring(0, 200)}`, { verbose: true });
1157
- await log(` Retry ${retryCount + 1}/${retryLimits.max503Retries}`, { verbose: true });
1158
- // Show countdown for long waits
1159
- if (delay > 60000) {
1160
- const countdownInterval = 60000; // Every minute
1161
- let remainingMs = delay;
1162
- const countdownTimer = setInterval(async () => {
1163
- remainingMs -= countdownInterval;
1164
- if (remainingMs > 0) {
1165
- const remainingMinutes = Math.round(remainingMs / (1000 * 60));
1166
- await log(`⏳ ${remainingMinutes} minutes remaining until retry...`);
1167
- }
1168
- }, countdownInterval);
1107
+ if ((commandFailed || isOverloadError) && (isOverloadError || (lastMessage.includes('API Error: 500') && lastMessage.includes('Overloaded')) || (lastMessage.includes('api_error') && lastMessage.includes('Overloaded')))) {
1108
+ if (retryCount < maxRetries) {
1109
+ // Calculate exponential backoff delay
1110
+ const delay = baseDelay * Math.pow(2, retryCount);
1111
+ await log(`\n⚠️ API overload error detected. Retrying in ${delay / 1000} seconds...`, { level: 'warning' });
1112
+ await log(` Error: ${lastMessage.substring(0, 200)}`, { verbose: true });
1169
1113
  // Wait before retrying
1170
1114
  await new Promise(resolve => setTimeout(resolve, delay));
1171
- clearInterval(countdownTimer);
1115
+ // Increment retry count and retry
1116
+ retryCount++;
1117
+ return await executeWithRetry();
1172
1118
  } else {
1173
- // Wait before retrying
1174
- await new Promise(resolve => setTimeout(resolve, delay));
1119
+ await log(`\n\n❌ API overload error persisted after ${maxRetries} retries`, { level: 'error' });
1120
+ await log(' The API appears to be heavily loaded. Please try again later.', { level: 'error' });
1121
+ return {
1122
+ success: false,
1123
+ sessionId,
1124
+ limitReached: false,
1125
+ limitResetTime: null,
1126
+ messageCount,
1127
+ toolUseCount,
1128
+ };
1175
1129
  }
1176
- await log('\n🔄 Retrying now...');
1177
- // Increment retry count and retry
1178
- retryCount++;
1179
- return await executeWithRetry();
1180
- } else {
1181
- await log(`\n\n❌ 503 network error persisted after ${retryLimits.max503Retries} retries`, { level: 'error' });
1182
- await log(' The Anthropic API appears to be experiencing network issues.', { level: 'error' });
1183
- await log(' Please try again later or check https://status.anthropic.com/', { level: 'error' });
1184
- return {
1185
- success: false,
1186
- sessionId,
1187
- limitReached: false,
1188
- limitResetTime: null,
1189
- messageCount,
1190
- toolUseCount,
1191
- is503Error: true
1192
- };
1193
1130
  }
1194
- }
1195
- if (commandFailed) {
1196
- // Check for usage limit errors first (more specific)
1197
- const limitInfo = detectUsageLimit(lastMessage);
1198
- if (limitInfo.isUsageLimit) {
1199
- limitReached = true;
1200
- limitResetTime = limitInfo.resetTime;
1131
+ if ((commandFailed || is503Error) && argv.autoResumeOnErrors && (is503Error || lastMessage.includes('API Error: 503') || (lastMessage.includes('503') && lastMessage.includes('upstream connect error')) || (lastMessage.includes('503') && lastMessage.includes('remote connection failure')))) {
1132
+ if (retryCount < retryLimits.max503Retries) {
1133
+ // Calculate exponential backoff delay starting from 5 minutes
1134
+ const delay = retryLimits.initial503RetryDelayMs * Math.pow(retryLimits.retryBackoffMultiplier, retryCount);
1135
+ const delayMinutes = Math.round(delay / (1000 * 60));
1136
+ await log(`\n⚠️ 503 network error detected. Retrying in ${delayMinutes} minutes...`, { level: 'warning' });
1137
+ await log(` Error: ${lastMessage.substring(0, 200)}`, { verbose: true });
1138
+ await log(` Retry ${retryCount + 1}/${retryLimits.max503Retries}`, { verbose: true });
1139
+ // Show countdown for long waits
1140
+ if (delay > 60000) {
1141
+ const countdownInterval = 60000; // Every minute
1142
+ let remainingMs = delay;
1143
+ const countdownTimer = setInterval(async () => {
1144
+ remainingMs -= countdownInterval;
1145
+ if (remainingMs > 0) {
1146
+ const remainingMinutes = Math.round(remainingMs / (1000 * 60));
1147
+ await log(`⏳ ${remainingMinutes} minutes remaining until retry...`);
1148
+ }
1149
+ }, countdownInterval);
1150
+ // Wait before retrying
1151
+ await new Promise(resolve => setTimeout(resolve, delay));
1152
+ clearInterval(countdownTimer);
1153
+ } else {
1154
+ // Wait before retrying
1155
+ await new Promise(resolve => setTimeout(resolve, delay));
1156
+ }
1157
+ await log('\n🔄 Retrying now...');
1158
+ // Increment retry count and retry
1159
+ retryCount++;
1160
+ return await executeWithRetry();
1161
+ } else {
1162
+ await log(`\n\n❌ 503 network error persisted after ${retryLimits.max503Retries} retries`, {
1163
+ level: 'error',
1164
+ });
1165
+ await log(' The Anthropic API appears to be experiencing network issues.', { level: 'error' });
1166
+ await log(' Please try again later or check https://status.anthropic.com/', { level: 'error' });
1167
+ return {
1168
+ success: false,
1169
+ sessionId,
1170
+ limitReached: false,
1171
+ limitResetTime: null,
1172
+ messageCount,
1173
+ toolUseCount,
1174
+ is503Error: true,
1175
+ };
1176
+ }
1177
+ }
1178
+ if (commandFailed) {
1179
+ // Check for usage limit errors first (more specific)
1180
+ const limitInfo = detectUsageLimit(lastMessage);
1181
+ if (limitInfo.isUsageLimit) {
1182
+ limitReached = true;
1183
+ limitResetTime = limitInfo.resetTime;
1201
1184
 
1202
- // Format and display user-friendly message
1203
- const messageLines = formatUsageLimitMessage({
1204
- tool: 'Claude',
1205
- resetTime: limitInfo.resetTime,
1206
- sessionId,
1207
- resumeCommand: argv.url ? `${process.argv[0]} ${process.argv[1]} --auto-continue ${argv.url}` : null
1208
- });
1185
+ // Format and display user-friendly message
1186
+ const messageLines = formatUsageLimitMessage({
1187
+ tool: 'Claude',
1188
+ resetTime: limitInfo.resetTime,
1189
+ sessionId,
1190
+ resumeCommand: argv.url ? `${process.argv[0]} ${process.argv[1]} --auto-continue ${argv.url}` : null,
1191
+ });
1209
1192
 
1210
- for (const line of messageLines) {
1211
- await log(line, { level: 'warning' });
1193
+ for (const line of messageLines) {
1194
+ await log(line, { level: 'warning' });
1195
+ }
1196
+ } else if (lastMessage.includes('context_length_exceeded')) {
1197
+ await log('\n\n❌ Context length exceeded. Try with a smaller issue or split the work.', { level: 'error' });
1198
+ } else {
1199
+ await log(`\n\n❌ Claude command failed with exit code ${exitCode}`, { level: 'error' });
1200
+ if (sessionId && !argv.resume) {
1201
+ await log(`📌 Session ID for resuming: ${sessionId}`);
1202
+ await log('\nTo resume this session, run:');
1203
+ await log(` ${process.argv[0]} ${process.argv[1]} ${argv.url} --resume ${sessionId}`);
1204
+ }
1212
1205
  }
1213
- } else if (lastMessage.includes('context_length_exceeded')) {
1214
- await log('\n\n❌ Context length exceeded. Try with a smaller issue or split the work.', { level: 'error' });
1215
- } else {
1216
- await log(`\n\n❌ Claude command failed with exit code ${exitCode}`, { level: 'error' });
1217
- if (sessionId && !argv.resume) {
1218
- await log(`📌 Session ID for resuming: ${sessionId}`);
1219
- await log('\nTo resume this session, run:');
1220
- await log(` ${process.argv[0]} ${process.argv[1]} ${argv.url} --resume ${sessionId}`);
1206
+ }
1207
+ // Additional failure detection: if no messages were processed and there were stderr errors,
1208
+ // or if the command produced no output at all, treat it as a failure
1209
+ //
1210
+ // This is critical for detecting "silent failures" where:
1211
+ // 1. Claude CLI encounters an internal error (e.g., "kill EPERM" from timeout)
1212
+ // 2. The error is logged to stderr but exit code is 0 or exit event is never sent
1213
+ // 3. Result: messageCount=0, toolUseCount=0, but stderrErrors has content
1214
+ //
1215
+ // Common cause: sudo commands that timeout
1216
+ // - Timeout triggers process.kill() in Claude CLI
1217
+ // - If child process runs with sudo (root), parent can't kill it → EPERM error
1218
+ // - Error logged to stderr, but command doesn't properly fail
1219
+ //
1220
+ // Workaround (applied in system prompt):
1221
+ // - Instruct Claude to run sudo commands (installations) in background
1222
+ // - Background processes avoid timeout kill mechanism
1223
+ // - Prevents EPERM errors and false success reports
1224
+ //
1225
+ // See: docs/dependencies-research/claude-code-issues/README.md for full details
1226
+ if (!commandFailed && stderrErrors.length > 0 && messageCount === 0 && toolUseCount === 0) {
1227
+ commandFailed = true;
1228
+ await log('\n\n❌ Command failed: No messages processed and errors detected in stderr', { level: 'error' });
1229
+ await log('Stderr errors:', { level: 'error' });
1230
+ for (const err of stderrErrors.slice(0, 5)) {
1231
+ await log(` ${err.substring(0, 200)}`, { level: 'error' });
1221
1232
  }
1222
1233
  }
1223
- }
1224
- // Additional failure detection: if no messages were processed and there were stderr errors,
1225
- // or if the command produced no output at all, treat it as a failure
1226
- //
1227
- // This is critical for detecting "silent failures" where:
1228
- // 1. Claude CLI encounters an internal error (e.g., "kill EPERM" from timeout)
1229
- // 2. The error is logged to stderr but exit code is 0 or exit event is never sent
1230
- // 3. Result: messageCount=0, toolUseCount=0, but stderrErrors has content
1231
- //
1232
- // Common cause: sudo commands that timeout
1233
- // - Timeout triggers process.kill() in Claude CLI
1234
- // - If child process runs with sudo (root), parent can't kill it → EPERM error
1235
- // - Error logged to stderr, but command doesn't properly fail
1236
- //
1237
- // Workaround (applied in system prompt):
1238
- // - Instruct Claude to run sudo commands (installations) in background
1239
- // - Background processes avoid timeout kill mechanism
1240
- // - Prevents EPERM errors and false success reports
1241
- //
1242
- // See: docs/dependencies-research/claude-code-issues/README.md for full details
1243
- if (!commandFailed && stderrErrors.length > 0 && messageCount === 0 && toolUseCount === 0) {
1244
- commandFailed = true;
1245
- await log('\n\n❌ Command failed: No messages processed and errors detected in stderr', { level: 'error' });
1246
- await log('Stderr errors:', { level: 'error' });
1247
- for (const err of stderrErrors.slice(0, 5)) {
1248
- await log(` ${err.substring(0, 200)}`, { level: 'error' });
1234
+ if (commandFailed) {
1235
+ // Take resource snapshot after failure
1236
+ const resourcesAfter = await getResourceSnapshot();
1237
+ await log('\n📈 System resources after execution:', { verbose: true });
1238
+ await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
1239
+ await log(` Load: ${resourcesAfter.load}`, { verbose: true });
1240
+ // Log attachment will be handled by solve.mjs when it receives success=false
1241
+ await log('', { verbose: true });
1242
+ return {
1243
+ success: false,
1244
+ sessionId,
1245
+ limitReached,
1246
+ limitResetTime,
1247
+ messageCount,
1248
+ toolUseCount,
1249
+ };
1249
1250
  }
1250
- }
1251
- if (commandFailed) {
1252
- // Take resource snapshot after failure
1253
- const resourcesAfter = await getResourceSnapshot();
1254
- await log('\n📈 System resources after execution:', { verbose: true });
1255
- await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
1256
- await log(` Load: ${resourcesAfter.load}`, { verbose: true });
1257
- // Log attachment will be handled by solve.mjs when it receives success=false
1258
- await log('', { verbose: true });
1259
- return {
1260
- success: false,
1261
- sessionId,
1262
- limitReached,
1263
- limitResetTime,
1264
- messageCount,
1265
- toolUseCount
1266
- };
1267
- }
1268
- await log('\n\n✅ Claude command completed');
1269
- await log(`📊 Total messages: ${messageCount}, Tool uses: ${toolUseCount}`);
1270
- // Calculate and display total token usage from session JSONL file
1271
- if (sessionId && tempDir) {
1272
- try {
1273
- const tokenUsage = await calculateSessionTokens(sessionId, tempDir);
1274
- if (tokenUsage) {
1275
- await log('\n💰 Token Usage Summary:');
1276
- // Display per-model breakdown
1277
- if (tokenUsage.modelUsage) {
1278
- const modelIds = Object.keys(tokenUsage.modelUsage);
1279
- for (const modelId of modelIds) {
1280
- const usage = tokenUsage.modelUsage[modelId];
1281
- await log(`\n 📊 ${usage.modelName || modelId}:`);
1282
- await displayModelUsage(usage, log);
1283
- // Display budget stats if flag is enabled
1284
- if (argv.tokensBudgetStats && usage.modelInfo?.limit) {
1285
- await displayBudgetStats(usage, log);
1286
- }
1287
- }
1288
- // Show totals if multiple models were used
1289
- if (modelIds.length > 1) {
1290
- await log('\n 📈 Total across all models:');
1291
- // Show cost comparison
1292
- await log('\n 💰 Cost estimation:');
1293
- if (tokenUsage.totalCostUSD !== null && tokenUsage.totalCostUSD !== undefined) {
1294
- await log(` Public pricing estimate: $${tokenUsage.totalCostUSD.toFixed(6)} USD`);
1295
- } else {
1296
- await log(' Public pricing estimate: unknown');
1251
+ await log('\n\n✅ Claude command completed');
1252
+ await log(`📊 Total messages: ${messageCount}, Tool uses: ${toolUseCount}`);
1253
+ // Calculate and display total token usage from session JSONL file
1254
+ if (sessionId && tempDir) {
1255
+ try {
1256
+ const tokenUsage = await calculateSessionTokens(sessionId, tempDir);
1257
+ if (tokenUsage) {
1258
+ await log('\n💰 Token Usage Summary:');
1259
+ // Display per-model breakdown
1260
+ if (tokenUsage.modelUsage) {
1261
+ const modelIds = Object.keys(tokenUsage.modelUsage);
1262
+ for (const modelId of modelIds) {
1263
+ const usage = tokenUsage.modelUsage[modelId];
1264
+ await log(`\n 📊 ${usage.modelName || modelId}:`);
1265
+ await displayModelUsage(usage, log);
1266
+ // Display budget stats if flag is enabled
1267
+ if (argv.tokensBudgetStats && usage.modelInfo?.limit) {
1268
+ await displayBudgetStats(usage, log);
1269
+ }
1297
1270
  }
1298
- if (anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined) {
1299
- await log(` Calculated by Anthropic: $${anthropicTotalCostUSD.toFixed(6)} USD`);
1300
- // Show comparison if both are available
1271
+ // Show totals if multiple models were used
1272
+ if (modelIds.length > 1) {
1273
+ await log('\n 📈 Total across all models:');
1274
+ // Show cost comparison
1275
+ await log('\n 💰 Cost estimation:');
1301
1276
  if (tokenUsage.totalCostUSD !== null && tokenUsage.totalCostUSD !== undefined) {
1302
- const difference = anthropicTotalCostUSD - tokenUsage.totalCostUSD;
1303
- const percentDiff = tokenUsage.totalCostUSD > 0 ? ((difference / tokenUsage.totalCostUSD) * 100) : 0;
1304
- await log(` Difference: $${difference.toFixed(6)} (${percentDiff > 0 ? '+' : ''}${percentDiff.toFixed(2)}%)`);
1277
+ await log(` Public pricing estimate: $${tokenUsage.totalCostUSD.toFixed(6)} USD`);
1305
1278
  } else {
1279
+ await log(' Public pricing estimate: unknown');
1280
+ }
1281
+ if (anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined) {
1282
+ await log(` Calculated by Anthropic: $${anthropicTotalCostUSD.toFixed(6)} USD`);
1283
+ // Show comparison if both are available
1284
+ if (tokenUsage.totalCostUSD !== null && tokenUsage.totalCostUSD !== undefined) {
1285
+ const difference = anthropicTotalCostUSD - tokenUsage.totalCostUSD;
1286
+ const percentDiff = tokenUsage.totalCostUSD > 0 ? (difference / tokenUsage.totalCostUSD) * 100 : 0;
1287
+ await log(` Difference: $${difference.toFixed(6)} (${percentDiff > 0 ? '+' : ''}${percentDiff.toFixed(2)}%)`);
1288
+ } else {
1289
+ await log(' Difference: unknown');
1290
+ }
1291
+ } else {
1292
+ await log(' Calculated by Anthropic: unknown');
1306
1293
  await log(' Difference: unknown');
1307
1294
  }
1308
1295
  } else {
1309
- await log(' Calculated by Anthropic: unknown');
1310
- await log(' Difference: unknown');
1311
- }
1312
- } else {
1313
- // Single model - show cost comparison
1314
- await log('\n 💰 Cost estimation:');
1315
- if (tokenUsage.totalCostUSD !== null && tokenUsage.totalCostUSD !== undefined) {
1316
- await log(` Public pricing estimate: $${tokenUsage.totalCostUSD.toFixed(6)} USD`);
1317
- } else {
1318
- await log(' Public pricing estimate: unknown');
1319
- }
1320
- if (anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined) {
1321
- await log(` Calculated by Anthropic: $${anthropicTotalCostUSD.toFixed(6)} USD`);
1322
- // Show comparison if both are available
1296
+ // Single model - show cost comparison
1297
+ await log('\n 💰 Cost estimation:');
1323
1298
  if (tokenUsage.totalCostUSD !== null && tokenUsage.totalCostUSD !== undefined) {
1324
- const difference = anthropicTotalCostUSD - tokenUsage.totalCostUSD;
1325
- const percentDiff = tokenUsage.totalCostUSD > 0 ? ((difference / tokenUsage.totalCostUSD) * 100) : 0;
1326
- await log(` Difference: $${difference.toFixed(6)} (${percentDiff > 0 ? '+' : ''}${percentDiff.toFixed(2)}%)`);
1299
+ await log(` Public pricing estimate: $${tokenUsage.totalCostUSD.toFixed(6)} USD`);
1327
1300
  } else {
1301
+ await log(' Public pricing estimate: unknown');
1302
+ }
1303
+ if (anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined) {
1304
+ await log(` Calculated by Anthropic: $${anthropicTotalCostUSD.toFixed(6)} USD`);
1305
+ // Show comparison if both are available
1306
+ if (tokenUsage.totalCostUSD !== null && tokenUsage.totalCostUSD !== undefined) {
1307
+ const difference = anthropicTotalCostUSD - tokenUsage.totalCostUSD;
1308
+ const percentDiff = tokenUsage.totalCostUSD > 0 ? (difference / tokenUsage.totalCostUSD) * 100 : 0;
1309
+ await log(` Difference: $${difference.toFixed(6)} (${percentDiff > 0 ? '+' : ''}${percentDiff.toFixed(2)}%)`);
1310
+ } else {
1311
+ await log(' Difference: unknown');
1312
+ }
1313
+ } else {
1314
+ await log(' Calculated by Anthropic: unknown');
1328
1315
  await log(' Difference: unknown');
1329
1316
  }
1330
- } else {
1331
- await log(' Calculated by Anthropic: unknown');
1332
- await log(' Difference: unknown');
1317
+ await log(` Total tokens: ${formatNumber(tokenUsage.totalTokens)}`);
1333
1318
  }
1334
- await log(` Total tokens: ${formatNumber(tokenUsage.totalTokens)}`);
1335
- }
1336
- } else {
1337
- // Fallback to old format if modelUsage is not available
1338
- await log(` Input tokens: ${formatNumber(tokenUsage.inputTokens)}`);
1339
- if (tokenUsage.cacheCreationTokens > 0) {
1340
- await log(` Cache creation tokens: ${formatNumber(tokenUsage.cacheCreationTokens)}`);
1341
- }
1342
- if (tokenUsage.cacheReadTokens > 0) {
1343
- await log(` Cache read tokens: ${formatNumber(tokenUsage.cacheReadTokens)}`);
1319
+ } else {
1320
+ // Fallback to old format if modelUsage is not available
1321
+ await log(` Input tokens: ${formatNumber(tokenUsage.inputTokens)}`);
1322
+ if (tokenUsage.cacheCreationTokens > 0) {
1323
+ await log(` Cache creation tokens: ${formatNumber(tokenUsage.cacheCreationTokens)}`);
1324
+ }
1325
+ if (tokenUsage.cacheReadTokens > 0) {
1326
+ await log(` Cache read tokens: ${formatNumber(tokenUsage.cacheReadTokens)}`);
1327
+ }
1328
+ await log(` Output tokens: ${formatNumber(tokenUsage.outputTokens)}`);
1329
+ await log(` Total tokens: ${formatNumber(tokenUsage.totalTokens)}`);
1344
1330
  }
1345
- await log(` Output tokens: ${formatNumber(tokenUsage.outputTokens)}`);
1346
- await log(` Total tokens: ${formatNumber(tokenUsage.totalTokens)}`);
1347
1331
  }
1332
+ } catch (tokenError) {
1333
+ reportError(tokenError, {
1334
+ context: 'calculate_session_tokens',
1335
+ sessionId,
1336
+ operation: 'read_session_jsonl',
1337
+ });
1338
+ await log(` ⚠️ Could not calculate token usage: ${tokenError.message}`, { verbose: true });
1348
1339
  }
1349
- } catch (tokenError) {
1350
- reportError(tokenError, {
1351
- context: 'calculate_session_tokens',
1352
- sessionId,
1353
- operation: 'read_session_jsonl'
1354
- });
1355
- await log(` ⚠️ Could not calculate token usage: ${tokenError.message}`, { verbose: true });
1356
1340
  }
1357
- }
1358
- return {
1359
- success: true,
1360
- sessionId,
1361
- limitReached,
1362
- limitResetTime,
1363
- messageCount,
1364
- toolUseCount,
1365
- anthropicTotalCostUSD // Pass Anthropic's official total cost
1366
- };
1367
- } catch (error) {
1368
- reportError(error, {
1369
- context: 'execute_claude',
1370
- command: params.command,
1371
- claudePath: params.claudePath,
1372
- operation: 'run_claude_command'
1373
- });
1374
- const errorStr = error.message || error.toString();
1375
- if ((errorStr.includes('API Error: 500') && errorStr.includes('Overloaded')) ||
1376
- (errorStr.includes('api_error') && errorStr.includes('Overloaded'))) {
1377
- if (retryCount < maxRetries) {
1378
- // Calculate exponential backoff delay
1379
- const delay = baseDelay * Math.pow(2, retryCount);
1380
- await log(`\n⚠️ API overload error in exception. Retrying in ${delay / 1000} seconds...`, { level: 'warning' });
1381
- // Wait before retrying
1382
- await new Promise(resolve => setTimeout(resolve, delay));
1383
- // Increment retry count and retry
1384
- retryCount++;
1385
- return await executeWithRetry();
1341
+ return {
1342
+ success: true,
1343
+ sessionId,
1344
+ limitReached,
1345
+ limitResetTime,
1346
+ messageCount,
1347
+ toolUseCount,
1348
+ anthropicTotalCostUSD, // Pass Anthropic's official total cost
1349
+ };
1350
+ } catch (error) {
1351
+ reportError(error, {
1352
+ context: 'execute_claude',
1353
+ command: params.command,
1354
+ claudePath: params.claudePath,
1355
+ operation: 'run_claude_command',
1356
+ });
1357
+ const errorStr = error.message || error.toString();
1358
+ if ((errorStr.includes('API Error: 500') && errorStr.includes('Overloaded')) || (errorStr.includes('api_error') && errorStr.includes('Overloaded'))) {
1359
+ if (retryCount < maxRetries) {
1360
+ // Calculate exponential backoff delay
1361
+ const delay = baseDelay * Math.pow(2, retryCount);
1362
+ await log(`\n⚠️ API overload error in exception. Retrying in ${delay / 1000} seconds...`, {
1363
+ level: 'warning',
1364
+ });
1365
+ // Wait before retrying
1366
+ await new Promise(resolve => setTimeout(resolve, delay));
1367
+ // Increment retry count and retry
1368
+ retryCount++;
1369
+ return await executeWithRetry();
1370
+ }
1386
1371
  }
1387
- }
1388
- if (argv.autoResumeOnErrors &&
1389
- (errorStr.includes('API Error: 503') ||
1390
- (errorStr.includes('503') && errorStr.includes('upstream connect error')) ||
1391
- (errorStr.includes('503') && errorStr.includes('remote connection failure')))) {
1392
- if (retryCount < retryLimits.max503Retries) {
1393
- // Calculate exponential backoff delay starting from 5 minutes
1394
- const delay = retryLimits.initial503RetryDelayMs * Math.pow(retryLimits.retryBackoffMultiplier, retryCount);
1395
- const delayMinutes = Math.round(delay / (1000 * 60));
1396
- await log(`\n⚠️ 503 network error in exception. Retrying in ${delayMinutes} minutes...`, { level: 'warning' });
1397
- // Wait before retrying
1398
- await new Promise(resolve => setTimeout(resolve, delay));
1399
- // Increment retry count and retry
1400
- retryCount++;
1401
- return await executeWithRetry();
1372
+ if (argv.autoResumeOnErrors && (errorStr.includes('API Error: 503') || (errorStr.includes('503') && errorStr.includes('upstream connect error')) || (errorStr.includes('503') && errorStr.includes('remote connection failure')))) {
1373
+ if (retryCount < retryLimits.max503Retries) {
1374
+ // Calculate exponential backoff delay starting from 5 minutes
1375
+ const delay = retryLimits.initial503RetryDelayMs * Math.pow(retryLimits.retryBackoffMultiplier, retryCount);
1376
+ const delayMinutes = Math.round(delay / (1000 * 60));
1377
+ await log(`\n⚠️ 503 network error in exception. Retrying in ${delayMinutes} minutes...`, {
1378
+ level: 'warning',
1379
+ });
1380
+ // Wait before retrying
1381
+ await new Promise(resolve => setTimeout(resolve, delay));
1382
+ // Increment retry count and retry
1383
+ retryCount++;
1384
+ return await executeWithRetry();
1385
+ }
1402
1386
  }
1387
+ await log(`\n\n❌ Error executing Claude command: ${error.message}`, { level: 'error' });
1388
+ return {
1389
+ success: false,
1390
+ sessionId,
1391
+ limitReached,
1392
+ limitResetTime: null,
1393
+ messageCount,
1394
+ toolUseCount,
1395
+ };
1403
1396
  }
1404
- await log(`\n\n❌ Error executing Claude command: ${error.message}`, { level: 'error' });
1405
- return {
1406
- success: false,
1407
- sessionId,
1408
- limitReached,
1409
- limitResetTime: null,
1410
- messageCount,
1411
- toolUseCount
1412
- };
1413
- }
1414
1397
  }; // End of executeWithRetry function
1415
1398
  // Start the execution with retry logic
1416
1399
  return await executeWithRetry();
@@ -1440,13 +1423,19 @@ export const checkForUncommittedChanges = async (tempDir, owner, repo, branchNam
1440
1423
  if (pushResult.code === 0) {
1441
1424
  await log('✅ Changes pushed successfully');
1442
1425
  } else {
1443
- await log(`⚠️ Warning: Could not push changes: ${pushResult.stderr?.toString().trim()}`, { level: 'warning' });
1426
+ await log(`⚠️ Warning: Could not push changes: ${pushResult.stderr?.toString().trim()}`, {
1427
+ level: 'warning',
1428
+ });
1444
1429
  }
1445
1430
  } else {
1446
- await log(`⚠️ Warning: Could not commit changes: ${commitResult.stderr?.toString().trim()}`, { level: 'warning' });
1431
+ await log(`⚠️ Warning: Could not commit changes: ${commitResult.stderr?.toString().trim()}`, {
1432
+ level: 'warning',
1433
+ });
1447
1434
  }
1448
1435
  } else {
1449
- await log(`⚠️ Warning: Could not stage changes: ${addResult.stderr?.toString().trim()}`, { level: 'warning' });
1436
+ await log(`⚠️ Warning: Could not stage changes: ${addResult.stderr?.toString().trim()}`, {
1437
+ level: 'warning',
1438
+ });
1450
1439
  }
1451
1440
  return false;
1452
1441
  } else if (autoRestartEnabled) {
@@ -1465,7 +1454,9 @@ export const checkForUncommittedChanges = async (tempDir, owner, repo, branchNam
1465
1454
  return false;
1466
1455
  }
1467
1456
  } else {
1468
- await log(`⚠️ Warning: Could not check git status: ${gitStatusResult.stderr?.toString().trim()}`, { level: 'warning' });
1457
+ await log(`⚠️ Warning: Could not check git status: ${gitStatusResult.stderr?.toString().trim()}`, {
1458
+ level: 'warning',
1459
+ });
1469
1460
  return false;
1470
1461
  }
1471
1462
  } catch (gitError) {
@@ -1481,5 +1472,5 @@ export default {
1481
1472
  executeClaude,
1482
1473
  executeClaudeCommand,
1483
1474
  checkForUncommittedChanges,
1484
- calculateSessionTokens
1485
- };
1475
+ calculateSessionTokens,
1476
+ };