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