@link-assistant/hive-mind 0.46.1 → 0.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -15
- package/README.md +42 -8
- package/package.json +16 -3
- package/src/agent.lib.mjs +49 -70
- package/src/agent.prompts.lib.mjs +6 -20
- package/src/buildUserMention.lib.mjs +4 -17
- package/src/claude-limits.lib.mjs +15 -15
- package/src/claude.lib.mjs +617 -626
- package/src/claude.prompts.lib.mjs +7 -22
- package/src/codex.lib.mjs +39 -71
- package/src/codex.prompts.lib.mjs +6 -20
- package/src/config.lib.mjs +3 -16
- package/src/contributing-guidelines.lib.mjs +5 -18
- package/src/exit-handler.lib.mjs +4 -4
- package/src/git.lib.mjs +7 -7
- package/src/github-issue-creator.lib.mjs +17 -17
- package/src/github-linking.lib.mjs +8 -33
- package/src/github.batch.lib.mjs +20 -16
- package/src/github.graphql.lib.mjs +18 -18
- package/src/github.lib.mjs +89 -91
- package/src/hive.config.lib.mjs +50 -50
- package/src/hive.mjs +1293 -1296
- package/src/instrument.mjs +7 -11
- package/src/interactive-mode.lib.mjs +112 -138
- package/src/lenv-reader.lib.mjs +1 -6
- package/src/lib.mjs +36 -45
- package/src/lino.lib.mjs +2 -2
- package/src/local-ci-checks.lib.mjs +15 -14
- package/src/memory-check.mjs +52 -60
- package/src/model-mapping.lib.mjs +25 -32
- package/src/model-validation.lib.mjs +31 -31
- package/src/opencode.lib.mjs +37 -62
- package/src/opencode.prompts.lib.mjs +7 -21
- package/src/protect-branch.mjs +14 -15
- package/src/review.mjs +28 -27
- package/src/reviewers-hive.mjs +64 -69
- package/src/sentry.lib.mjs +13 -10
- package/src/solve.auto-continue.lib.mjs +48 -38
- package/src/solve.auto-pr.lib.mjs +111 -69
- package/src/solve.branch-errors.lib.mjs +17 -46
- package/src/solve.branch.lib.mjs +16 -23
- package/src/solve.config.lib.mjs +263 -261
- package/src/solve.error-handlers.lib.mjs +21 -79
- package/src/solve.execution.lib.mjs +10 -18
- package/src/solve.feedback.lib.mjs +25 -46
- package/src/solve.mjs +59 -60
- package/src/solve.preparation.lib.mjs +10 -36
- package/src/solve.repo-setup.lib.mjs +4 -19
- package/src/solve.repository.lib.mjs +37 -37
- package/src/solve.results.lib.mjs +32 -46
- package/src/solve.session.lib.mjs +7 -22
- package/src/solve.validation.lib.mjs +19 -17
- package/src/solve.watch.lib.mjs +20 -33
- package/src/start-screen.mjs +24 -24
- package/src/task.mjs +38 -44
- package/src/telegram-bot.mjs +125 -121
- package/src/telegram-top-command.lib.mjs +32 -48
- package/src/usage-limit.lib.mjs +9 -13
- package/src/version-info.lib.mjs +1 -1
- package/src/version.lib.mjs +1 -1
- package/src/youtrack/solve.youtrack.lib.mjs +3 -8
- package/src/youtrack/youtrack-sync.mjs +8 -14
- package/src/youtrack/youtrack.lib.mjs +26 -28
package/src/claude.lib.mjs
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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',
|
|
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 =
|
|
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`, {
|
|
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`, {
|
|
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 =
|
|
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
|
-
|
|
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...`, {
|
|
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`, {
|
|
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}`, {
|
|
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...`, {
|
|
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...`, {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
data
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
{
|
|
635
|
-
|
|
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
|
|
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
|
|
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
|
-
$,
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
935
|
-
|
|
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
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
for
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
-
|
|
1120
|
-
(
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
1115
|
+
// Increment retry count and retry
|
|
1116
|
+
retryCount++;
|
|
1117
|
+
return await executeWithRetry();
|
|
1172
1118
|
} else {
|
|
1173
|
-
|
|
1174
|
-
await
|
|
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
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
-
|
|
1211
|
-
|
|
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
|
-
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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
|
-
|
|
1252
|
-
//
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1310
|
-
await log('
|
|
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
|
-
|
|
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
|
-
|
|
1331
|
-
await log(' Calculated by Anthropic: unknown');
|
|
1332
|
-
await log(' Difference: unknown');
|
|
1317
|
+
await log(` Total tokens: ${formatNumber(tokenUsage.totalTokens)}`);
|
|
1333
1318
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
await log(`
|
|
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
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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()}`, {
|
|
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()}`, {
|
|
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()}`, {
|
|
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()}`, {
|
|
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
|
+
};
|