orquesta-agent 0.2.142 → 0.2.144
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/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +712 -704
- package/dist/executor.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +1 -0
- package/dist/local-server.js.map +1 -1
- package/dist/socketio-transport.d.ts +1 -1
- package/dist/socketio-transport.d.ts.map +1 -1
- package/dist/socketio-transport.js +8 -3
- package/dist/socketio-transport.js.map +1 -1
- package/dist/ui/manager.d.ts.map +1 -1
- package/dist/ui/manager.js +1 -0
- package/dist/ui/manager.js.map +1 -1
- package/package.json +1 -1
package/dist/executor.js
CHANGED
|
@@ -1258,83 +1258,84 @@ export async function execute(options) {
|
|
|
1258
1258
|
// orquesta-cli. Holds the upstream error text for logging.
|
|
1259
1259
|
let claudeApiError = null;
|
|
1260
1260
|
_activePromptId = promptId;
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1261
|
+
try {
|
|
1262
|
+
// Clear per-execution buffers for new execution (concurrent-safe)
|
|
1263
|
+
outputBufferMap.delete(commandId);
|
|
1264
|
+
clearDetectedRequirements(commandId);
|
|
1265
|
+
clearQABuffer(commandId);
|
|
1266
|
+
// Start log persistence for this execution
|
|
1267
|
+
startLogPersistence(promptId);
|
|
1268
|
+
addLog(promptId, 'info', 'system', `Starting execution: ${content.slice(0, 100)}...`);
|
|
1269
|
+
// Native coordination fast-path: if this is a RogerThat rejoin/wake dispatch
|
|
1270
|
+
// (carries an <<ORQ_COORD>> spec), run the plumbing in code + one LLM call to
|
|
1271
|
+
// draft the reply, instead of making the model orchestrate curls turn-by-turn
|
|
1272
|
+
// (slow + fragile on weak models). Falls through to the normal prompt path if
|
|
1273
|
+
// the spec is absent or malformed.
|
|
1274
|
+
const coordSpec = parseCoordSpec(content);
|
|
1275
|
+
if (coordSpec) {
|
|
1276
|
+
addLog(promptId, 'info', 'system', `Native coordination (${coordSpec.reason}) — channel ${coordSpec.channelId}`);
|
|
1277
|
+
try {
|
|
1278
|
+
// The agent token isn't in process.env — it's injected per-execution
|
|
1279
|
+
// (index.ts sets ORQUESTA_TOKEN on injectedCredentials before execute()).
|
|
1280
|
+
const orquestaToken = getInjectedCredentials()['ORQUESTA_TOKEN'] || process.env.ORQUESTA_TOKEN || '';
|
|
1281
|
+
const summary = await runCoordination(coordSpec, orquestaToken, (line) => {
|
|
1282
|
+
addLog(promptId, 'info', 'output', line);
|
|
1283
|
+
sendOutput(channel, commandId, 'stdout', line + '\n').catch(() => undefined);
|
|
1284
|
+
});
|
|
1285
|
+
addLog(promptId, 'info', 'system', summary);
|
|
1286
|
+
await sendOutput(channel, commandId, 'stdout', `\n${summary}\n`);
|
|
1287
|
+
await updatePromptStatus(promptId, 'completed', { text: summary, key_actions: [], files_modified: [] });
|
|
1288
|
+
await sendComplete(channel, commandId, 0, startTime);
|
|
1289
|
+
}
|
|
1290
|
+
catch (err) {
|
|
1291
|
+
const msg = `Coordination failed: ${err.message}`;
|
|
1292
|
+
addLog(promptId, 'error', 'error', msg);
|
|
1293
|
+
await updatePromptStatus(promptId, 'failed', { text: msg, key_actions: [], files_modified: [] });
|
|
1294
|
+
await sendComplete(channel, commandId, 1, startTime);
|
|
1295
|
+
}
|
|
1296
|
+
return;
|
|
1288
1297
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
await sendComplete(channel, commandId, 1, startTime);
|
|
1298
|
+
logger.info(`Executing prompt: ${content.slice(0, 60)}${content.length > 60 ? '...' : ''}`);
|
|
1299
|
+
logger.info(`Permission mode: ${mode}`);
|
|
1300
|
+
if (attachments && attachments.length > 0) {
|
|
1301
|
+
logger.info(`Attachments: ${attachments.length} file(s)`);
|
|
1294
1302
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
imageReferences = processed.fileReferences;
|
|
1312
|
-
logger.info(`Processed ${attachments.length} attachment(s)`);
|
|
1303
|
+
// Process attachments if present
|
|
1304
|
+
let attachmentContext = '';
|
|
1305
|
+
let attachmentCleanup = null;
|
|
1306
|
+
let imageReferences = [];
|
|
1307
|
+
if (attachments && attachments.length > 0) {
|
|
1308
|
+
try {
|
|
1309
|
+
const processed = await processAttachments(attachments);
|
|
1310
|
+
attachmentContext = processed.contextText;
|
|
1311
|
+
attachmentCleanup = processed.cleanup;
|
|
1312
|
+
imageReferences = processed.fileReferences;
|
|
1313
|
+
logger.info(`Processed ${attachments.length} attachment(s)`);
|
|
1314
|
+
}
|
|
1315
|
+
catch (error) {
|
|
1316
|
+
logger.error(`Failed to process attachments: ${error}`);
|
|
1317
|
+
addLog(promptId, 'error', 'system', `Failed to process attachments: ${error}`);
|
|
1318
|
+
}
|
|
1313
1319
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
? `${phoneBlock}\n${content}${attachmentContext}`
|
|
1334
|
-
: `${content}${attachmentContext}`;
|
|
1335
|
-
if (agentInstructions && agentInstructions.trim()) {
|
|
1336
|
-
// Prepend settings + agent instructions to the prompt
|
|
1337
|
-
fullContent = `[Orquesta Configuration]
|
|
1320
|
+
// Build full prompt with settings, agent instructions, and attachments
|
|
1321
|
+
let fullContent = content + attachmentContext;
|
|
1322
|
+
const settingsHeader = generateSettingsHeader();
|
|
1323
|
+
const browserBlock = BROWSER_TOOLS_PROMPT_BLOCK;
|
|
1324
|
+
const coordinationBlock = await fetchCoordinationContextBlock();
|
|
1325
|
+
const capabilitiesBlock = `${ORQUESTA_CAPABILITIES_PROMPT_BLOCK}${coordinationBlock}`;
|
|
1326
|
+
// Auto-inject any rogerthat phone messages received since the last prompt /
|
|
1327
|
+
// interactive turn. Shared state with the interactive path — see
|
|
1328
|
+
// readNewRogerthatMessages(). No-op when no listener is active.
|
|
1329
|
+
const phoneBlock = readNewRogerthatMessages();
|
|
1330
|
+
if (phoneBlock) {
|
|
1331
|
+
logger.info(`Injecting ${phoneBlock.length} chars of rogerthat phone messages into prompt`);
|
|
1332
|
+
}
|
|
1333
|
+
const userRequestBody = phoneBlock
|
|
1334
|
+
? `${phoneBlock}\n${content}${attachmentContext}`
|
|
1335
|
+
: `${content}${attachmentContext}`;
|
|
1336
|
+
if (agentInstructions && agentInstructions.trim()) {
|
|
1337
|
+
// Prepend settings + agent instructions to the prompt
|
|
1338
|
+
fullContent = `[Orquesta Configuration]
|
|
1338
1339
|
${settingsHeader}
|
|
1339
1340
|
[Agent Instructions from Orquesta]
|
|
1340
1341
|
${agentInstructions}
|
|
@@ -1345,11 +1346,11 @@ ${browserBlock}
|
|
|
1345
1346
|
|
|
1346
1347
|
[User Request]
|
|
1347
1348
|
${userRequestBody}`;
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1349
|
+
logger.info('Including settings and agent instructions in prompt');
|
|
1350
|
+
}
|
|
1351
|
+
else if (settingsHeader) {
|
|
1352
|
+
// Just settings, no agent instructions
|
|
1353
|
+
fullContent = `[Orquesta Configuration]
|
|
1353
1354
|
${settingsHeader}
|
|
1354
1355
|
|
|
1355
1356
|
${capabilitiesBlock}
|
|
@@ -1358,692 +1359,699 @@ ${browserBlock}
|
|
|
1358
1359
|
|
|
1359
1360
|
[User Request]
|
|
1360
1361
|
${userRequestBody}`;
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1362
|
+
logger.info('Including settings in prompt');
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
// No settings — still include capabilities + browser blocks.
|
|
1366
|
+
fullContent = `${capabilitiesBlock}
|
|
1366
1367
|
|
|
1367
1368
|
${browserBlock}
|
|
1368
1369
|
|
|
1369
1370
|
[User Request]
|
|
1370
1371
|
${userRequestBody}`;
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1372
|
+
}
|
|
1373
|
+
// Build environment with authentication
|
|
1374
|
+
const env = {
|
|
1375
|
+
...process.env,
|
|
1376
|
+
...injectedCredentials, // Inject credentials from Orquesta
|
|
1377
|
+
CI: 'true', // Ensure non-interactive mode
|
|
1378
|
+
// Expose the current prompt id so Claude can link generated files back
|
|
1379
|
+
// to this prompt via /api/agent/files (prompt_id=$ORQUESTA_PROMPT_ID).
|
|
1380
|
+
ORQUESTA_PROMPT_ID: promptId,
|
|
1381
|
+
};
|
|
1382
|
+
// Add API key if configured (overrides injected if present)
|
|
1383
|
+
if (anthropicApiKey) {
|
|
1384
|
+
env.ANTHROPIC_API_KEY = anthropicApiKey;
|
|
1385
|
+
}
|
|
1386
|
+
// Strip Claude Code session detection env vars so the spawned claude process
|
|
1387
|
+
// doesn't refuse with "nested sessions" when the agent itself runs inside a
|
|
1388
|
+
// Claude Code session (local dev or systemd inherits the launching shell's env).
|
|
1389
|
+
delete env.CLAUDECODE;
|
|
1390
|
+
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
1391
|
+
delete env.CLAUDE_CODE_VERSION;
|
|
1392
|
+
delete env.CLAUDE_CODE_SESSION_ID;
|
|
1393
|
+
delete env.CLAUDE_CODE_PROJECT_DIR;
|
|
1394
|
+
delete env.CLAUDE_CODE_HEADLESS;
|
|
1395
|
+
// Select CLI based on availability and preference. _forceCli overrides
|
|
1396
|
+
// this on the claude→orquesta failure-retry path.
|
|
1397
|
+
let { cli: selectedCli, reason } = selectCli();
|
|
1398
|
+
if (options._forceCli && (options._forceCli === 'orquesta' ? isOrquestaCliAvailable() : isClaudeCliAvailable())) {
|
|
1399
|
+
selectedCli = options._forceCli;
|
|
1400
|
+
reason = `Forced: ${options._forceCli} (retry after claude failure)`;
|
|
1401
|
+
}
|
|
1402
|
+
if (!selectedCli) {
|
|
1403
|
+
throw new Error('No CLI available. Please install either orquesta-cli or claude CLI.');
|
|
1404
|
+
}
|
|
1405
|
+
// Preflight check: if orquesta CLI is selected, verify its LLM endpoint works
|
|
1406
|
+
// This prevents wasting time on prompts that will fail with 404s
|
|
1407
|
+
if (selectedCli === 'orquesta') {
|
|
1408
|
+
try {
|
|
1409
|
+
const testResult = execSync('orquesta --status 2>&1 || true', { encoding: 'utf-8', timeout: 5000, env });
|
|
1410
|
+
// If orquesta-cli reports no endpoint or connection issues, fall back to claude
|
|
1411
|
+
if (testResult.includes('No endpoint') || testResult.includes('not configured') || testResult.includes('No LLM')) {
|
|
1412
|
+
const hasClaude = isClaudeCliAvailable();
|
|
1413
|
+
if (hasClaude) {
|
|
1414
|
+
logger.warn('Orquesta CLI has no LLM endpoint configured — falling back to Claude CLI');
|
|
1415
|
+
selectedCli = 'claude';
|
|
1416
|
+
reason = 'Fallback: orquesta has no LLM endpoint';
|
|
1417
|
+
}
|
|
1416
1418
|
}
|
|
1417
1419
|
}
|
|
1420
|
+
catch {
|
|
1421
|
+
// --status not supported or timeout, skip check
|
|
1422
|
+
}
|
|
1418
1423
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1424
|
+
const cliCommand = selectedCli;
|
|
1425
|
+
const cwd = workingDirectory || process.cwd();
|
|
1426
|
+
logger.info(`CLI Selection: ${cliCommand} (${reason})`);
|
|
1427
|
+
logger.info(`Spawning: ${cliCommand} -p "${fullContent.slice(0, 50)}..." in ${cwd}`);
|
|
1428
|
+
// Escape content for shell - replace single quotes
|
|
1429
|
+
const escapedContent = fullContent.replace(/'/g, "'\\''");
|
|
1430
|
+
// Build command based on permission mode
|
|
1431
|
+
// Use stream-json format to get structured output including thinking
|
|
1432
|
+
let command;
|
|
1433
|
+
// Different flags for different CLIs
|
|
1434
|
+
// Claude CLI supports --output-format stream-json
|
|
1435
|
+
// Orquesta CLI doesn't need special format flags - it outputs directly
|
|
1436
|
+
const baseFlags = cliCommand === 'orquesta'
|
|
1437
|
+
? '--verbose' // Orquesta: simple verbose output
|
|
1438
|
+
: '--verbose --output-format stream-json'; // Claude: stream-json format
|
|
1439
|
+
const permFlags = mode === 'auto' ? '--dangerously-skip-permissions' : '';
|
|
1440
|
+
// Claude CLI's headless `-p` mode does not accept positional file args as
|
|
1441
|
+
// attachments — image refs are surfaced inline in the prompt as absolute
|
|
1442
|
+
// paths so Claude's Read tool can pick them up.
|
|
1443
|
+
command = `${cliCommand} -p '${escapedContent}' ${permFlags} ${baseFlags}`;
|
|
1444
|
+
if (imageReferences.length > 0) {
|
|
1445
|
+
logger.info(`Image attachments referenced inline: ${imageReferences.length}`);
|
|
1446
|
+
}
|
|
1447
|
+
// Windows has no `script` binary; macOS BSD `script` requires a parent TTY
|
|
1448
|
+
// (which the agent daemon doesn't have, causing tcgetattr/ioctl failure).
|
|
1449
|
+
// Direct-spawn the CLI on those platforms — Claude CLI's `-p` stream-json
|
|
1450
|
+
// mode doesn't need a PTY. Linux util-linux `script` works without a parent
|
|
1451
|
+
// TTY, so we keep PTY wrapping there.
|
|
1452
|
+
const isWindows = process.platform === 'win32';
|
|
1453
|
+
const isDarwin = process.platform === 'darwin';
|
|
1454
|
+
let claude;
|
|
1455
|
+
if (isWindows || isDarwin) {
|
|
1456
|
+
const args = [];
|
|
1457
|
+
args.push('-p', fullContent);
|
|
1458
|
+
if (permFlags)
|
|
1459
|
+
args.push(...permFlags.split(/\s+/).filter(Boolean));
|
|
1460
|
+
if (baseFlags)
|
|
1461
|
+
args.push(...baseFlags.split(/\s+/).filter(Boolean));
|
|
1462
|
+
claude = spawn(cliCommand, args, {
|
|
1463
|
+
cwd,
|
|
1464
|
+
env,
|
|
1465
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1466
|
+
shell: isWindows, // Windows needs shell:true for .cmd shim; macOS spawns directly
|
|
1467
|
+
});
|
|
1421
1468
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
// Orquesta CLI doesn't need special format flags - it outputs directly
|
|
1435
|
-
const baseFlags = cliCommand === 'orquesta'
|
|
1436
|
-
? '--verbose' // Orquesta: simple verbose output
|
|
1437
|
-
: '--verbose --output-format stream-json'; // Claude: stream-json format
|
|
1438
|
-
const permFlags = mode === 'auto' ? '--dangerously-skip-permissions' : '';
|
|
1439
|
-
// Claude CLI's headless `-p` mode does not accept positional file args as
|
|
1440
|
-
// attachments — image refs are surfaced inline in the prompt as absolute
|
|
1441
|
-
// paths so Claude's Read tool can pick them up.
|
|
1442
|
-
command = `${cliCommand} -p '${escapedContent}' ${permFlags} ${baseFlags}`;
|
|
1443
|
-
if (imageReferences.length > 0) {
|
|
1444
|
-
logger.info(`Image attachments referenced inline: ${imageReferences.length}`);
|
|
1445
|
-
}
|
|
1446
|
-
// Windows has no `script` binary; macOS BSD `script` requires a parent TTY
|
|
1447
|
-
// (which the agent daemon doesn't have, causing tcgetattr/ioctl failure).
|
|
1448
|
-
// Direct-spawn the CLI on those platforms — Claude CLI's `-p` stream-json
|
|
1449
|
-
// mode doesn't need a PTY. Linux util-linux `script` works without a parent
|
|
1450
|
-
// TTY, so we keep PTY wrapping there.
|
|
1451
|
-
const isWindows = process.platform === 'win32';
|
|
1452
|
-
const isDarwin = process.platform === 'darwin';
|
|
1453
|
-
let claude;
|
|
1454
|
-
if (isWindows || isDarwin) {
|
|
1455
|
-
const args = [];
|
|
1456
|
-
args.push('-p', fullContent);
|
|
1457
|
-
if (permFlags)
|
|
1458
|
-
args.push(...permFlags.split(/\s+/).filter(Boolean));
|
|
1459
|
-
if (baseFlags)
|
|
1460
|
-
args.push(...baseFlags.split(/\s+/).filter(Boolean));
|
|
1461
|
-
claude = spawn(cliCommand, args, {
|
|
1462
|
-
cwd,
|
|
1463
|
-
env,
|
|
1464
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1465
|
-
shell: isWindows, // Windows needs shell:true for .cmd shim; macOS spawns directly
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
else {
|
|
1469
|
-
// Sandbox wraps the `script` invocation in bwrap when active (Linux only);
|
|
1470
|
-
// no-op otherwise.
|
|
1471
|
-
const { file: spawnFile, args: spawnArgs } = sandboxArgv('script', scriptWrapArgs(command), cwd);
|
|
1472
|
-
if (spawnFile === 'bwrap') {
|
|
1473
|
-
logger.info(`Sandbox active — confining writes to ${cwd}`);
|
|
1469
|
+
else {
|
|
1470
|
+
// Sandbox wraps the `script` invocation in bwrap when active (Linux only);
|
|
1471
|
+
// no-op otherwise.
|
|
1472
|
+
const { file: spawnFile, args: spawnArgs } = sandboxArgv('script', scriptWrapArgs(command), cwd);
|
|
1473
|
+
if (spawnFile === 'bwrap') {
|
|
1474
|
+
logger.info(`Sandbox active — confining writes to ${cwd}`);
|
|
1475
|
+
}
|
|
1476
|
+
claude = spawn(spawnFile, spawnArgs, {
|
|
1477
|
+
cwd,
|
|
1478
|
+
env: sandboxEnv(env),
|
|
1479
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1480
|
+
});
|
|
1474
1481
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
hadValidJson = true; // Successfully parsed JSON
|
|
1513
|
-
// Handle Orquesta CLI eval format (has 'event' field)
|
|
1514
|
-
if (json.event) {
|
|
1515
|
-
if (json.event === 'start') {
|
|
1516
|
-
output += `\n🎯 Starting task...\n`;
|
|
1517
|
-
logs.push(createSystemLog('init', 'Task started', 'info', promptId, json.data));
|
|
1518
|
-
}
|
|
1519
|
-
else if (json.event === 'todo') {
|
|
1520
|
-
const { action, id, title } = json.data;
|
|
1521
|
-
if (action === 'created') {
|
|
1522
|
-
output += `\n📝 Task ${id}: ${title}\n`;
|
|
1482
|
+
logger.info(`Process spawned with PID: ${claude.pid}`);
|
|
1483
|
+
// Track process for cancellation
|
|
1484
|
+
runningProcesses.set(commandId, claude);
|
|
1485
|
+
// Also map promptId -> commandId so cancel can find by either ID
|
|
1486
|
+
if (promptId) {
|
|
1487
|
+
promptIdToCommandId.set(promptId, commandId);
|
|
1488
|
+
}
|
|
1489
|
+
// Buffer for incomplete JSON lines
|
|
1490
|
+
let jsonBuffer = '';
|
|
1491
|
+
// Track current tool use for pairing with results
|
|
1492
|
+
let currentToolUseId = null;
|
|
1493
|
+
let currentToolName = null;
|
|
1494
|
+
// Track streaming content for complete log entries
|
|
1495
|
+
let thinkingBuffer = '';
|
|
1496
|
+
let textBuffer = '';
|
|
1497
|
+
let toolInputBuffer = '';
|
|
1498
|
+
// Parse stream-json output format
|
|
1499
|
+
// Returns { output: string, hadValidJson: boolean, logs: AgentLogEntry[] }
|
|
1500
|
+
const parseStreamJson = (text) => {
|
|
1501
|
+
jsonBuffer += text;
|
|
1502
|
+
let output = '';
|
|
1503
|
+
let hadValidJson = false;
|
|
1504
|
+
const logs = [];
|
|
1505
|
+
// Split by newlines and process complete JSON objects
|
|
1506
|
+
const lines = jsonBuffer.split('\n');
|
|
1507
|
+
jsonBuffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
1508
|
+
for (const line of lines) {
|
|
1509
|
+
if (!line.trim())
|
|
1510
|
+
continue;
|
|
1511
|
+
try {
|
|
1512
|
+
const json = JSON.parse(line);
|
|
1513
|
+
hadValidJson = true; // Successfully parsed JSON
|
|
1514
|
+
// Handle Orquesta CLI eval format (has 'event' field)
|
|
1515
|
+
if (json.event) {
|
|
1516
|
+
if (json.event === 'start') {
|
|
1517
|
+
output += `\n🎯 Starting task...\n`;
|
|
1518
|
+
logs.push(createSystemLog('init', 'Task started', 'info', promptId, json.data));
|
|
1523
1519
|
}
|
|
1524
|
-
else if (
|
|
1525
|
-
|
|
1520
|
+
else if (json.event === 'todo') {
|
|
1521
|
+
const { action, id, title } = json.data;
|
|
1522
|
+
if (action === 'created') {
|
|
1523
|
+
output += `\n📝 Task ${id}: ${title}\n`;
|
|
1524
|
+
}
|
|
1525
|
+
else if (action === 'started') {
|
|
1526
|
+
output += `\n▶️ Working on: ${title}\n`;
|
|
1527
|
+
}
|
|
1528
|
+
else if (action === 'completed') {
|
|
1529
|
+
output += `\n✅ Completed: ${title}\n`;
|
|
1530
|
+
}
|
|
1531
|
+
else if (action === 'failed') {
|
|
1532
|
+
output += `\n❌ Failed: ${title}\n`;
|
|
1533
|
+
}
|
|
1526
1534
|
}
|
|
1527
|
-
else if (
|
|
1528
|
-
|
|
1535
|
+
else if (json.event === 'tool_call') {
|
|
1536
|
+
const { tool, args } = json.data;
|
|
1537
|
+
output += `\n🔧 Tool: ${tool}\n`;
|
|
1538
|
+
const argsStr = JSON.stringify(args, null, 2);
|
|
1539
|
+
if (argsStr.length < 500) {
|
|
1540
|
+
output += `${argsStr}\n`;
|
|
1541
|
+
}
|
|
1542
|
+
logs.push(createToolCallLog(tool, args, 'info', promptId));
|
|
1543
|
+
currentToolName = tool;
|
|
1529
1544
|
}
|
|
1530
|
-
else if (
|
|
1531
|
-
|
|
1545
|
+
else if (json.event === 'tool_result') {
|
|
1546
|
+
const { tool, success, result, error } = json.data;
|
|
1547
|
+
if (success && result) {
|
|
1548
|
+
const preview = result.slice(0, 1000);
|
|
1549
|
+
output += `\n📋 Result: ${preview}${result.length > 1000 ? '...' : ''}\n`;
|
|
1550
|
+
logs.push(createToolResultLog(tool, true, result.slice(0, 5000), undefined, 'success', promptId));
|
|
1551
|
+
}
|
|
1552
|
+
else if (error) {
|
|
1553
|
+
output += `\n❌ Error: ${error}\n`;
|
|
1554
|
+
logs.push(createToolResultLog(tool, false, undefined, error, 'error', promptId));
|
|
1555
|
+
}
|
|
1532
1556
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
output += `${argsStr}\n`;
|
|
1557
|
+
else if (json.event === 'response') {
|
|
1558
|
+
const { content } = json.data;
|
|
1559
|
+
if (content) {
|
|
1560
|
+
output += `\n${content}\n`;
|
|
1561
|
+
logs.push(createOutputLog(content, 'markdown', 'info', promptId));
|
|
1562
|
+
}
|
|
1540
1563
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1564
|
+
else if (json.event === 'end') {
|
|
1565
|
+
const { success, duration_ms, usage } = json.data;
|
|
1566
|
+
const duration = (duration_ms / 1000).toFixed(1);
|
|
1567
|
+
output += `\n${success ? '✅' : '❌'} Completed in ${duration}s\n`;
|
|
1568
|
+
if (usage) {
|
|
1569
|
+
output += `📊 Tokens: ${usage.total_tokens || 0}\n`;
|
|
1570
|
+
}
|
|
1571
|
+
logs.push(createSystemLog('complete', `Execution ${success ? 'succeeded' : 'failed'}`, success ? 'success' : 'error', promptId, json.data));
|
|
1572
|
+
}
|
|
1573
|
+
// Continue to next line - don't process as Claude format
|
|
1574
|
+
continue;
|
|
1543
1575
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1576
|
+
// Handle different message types from Claude CLI stream-json
|
|
1577
|
+
if (json.type === 'assistant') {
|
|
1578
|
+
// Assistant message with content blocks
|
|
1579
|
+
if (json.message?.content) {
|
|
1580
|
+
for (const block of json.message.content) {
|
|
1581
|
+
if (block.type === 'thinking') {
|
|
1582
|
+
output += `\n💭 Thinking:\n${block.thinking}\n`;
|
|
1583
|
+
// Create structured thinking log
|
|
1584
|
+
logs.push(createThinkingLog(block.thinking, 'info', promptId));
|
|
1585
|
+
}
|
|
1586
|
+
else if (block.type === 'text') {
|
|
1587
|
+
output += block.text;
|
|
1588
|
+
// Create structured output log
|
|
1589
|
+
if (block.text.trim()) {
|
|
1590
|
+
logs.push(createOutputLog(block.text, 'markdown', 'info', promptId));
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
else if (block.type === 'tool_use') {
|
|
1594
|
+
output += `\n🔧 Tool: ${block.name}\n`;
|
|
1595
|
+
const inputStr = block.input
|
|
1596
|
+
? (typeof block.input === 'string' ? block.input : JSON.stringify(block.input, null, 2))
|
|
1597
|
+
: '';
|
|
1598
|
+
if (inputStr) {
|
|
1599
|
+
output += `${inputStr.slice(0, 500)}${inputStr.length > 500 ? '...' : ''}\n`;
|
|
1600
|
+
}
|
|
1601
|
+
// Create structured tool call log
|
|
1602
|
+
logs.push(createToolCallLog(block.name, block.input || {}, 'info', promptId, block.id));
|
|
1603
|
+
currentToolUseId = block.id;
|
|
1604
|
+
currentToolName = block.name;
|
|
1605
|
+
// Track file modifications for memory summaries
|
|
1606
|
+
const toolInput = block.input;
|
|
1607
|
+
if (toolInput && typeof toolInput.file_path === 'string') {
|
|
1608
|
+
if (['Edit', 'Write', 'NotebookEdit'].includes(block.name)) {
|
|
1609
|
+
trackFileModified(promptId, toolInput.file_path);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
else if (block.type === 'tool_result') {
|
|
1614
|
+
const content = typeof block.content === 'string'
|
|
1615
|
+
? block.content
|
|
1616
|
+
: JSON.stringify(block.content);
|
|
1617
|
+
output += `\n📋 Result:\n${content.slice(0, 1000)}${content.length > 1000 ? '...' : ''}\n`;
|
|
1618
|
+
// Create structured tool result log
|
|
1619
|
+
const isError = block.is_error === true;
|
|
1620
|
+
logs.push(createToolResultLog(currentToolName || 'Unknown', !isError, isError ? undefined : content.slice(0, 5000), isError ? content : undefined, isError ? 'error' : 'success', promptId, block.tool_use_id));
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1550
1623
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1624
|
+
}
|
|
1625
|
+
else if (json.type === 'user') {
|
|
1626
|
+
// User message (tool results from Claude CLI)
|
|
1627
|
+
if (json.message?.content) {
|
|
1628
|
+
for (const block of json.message.content) {
|
|
1629
|
+
if (block.type === 'tool_result') {
|
|
1630
|
+
const content = typeof block.content === 'string'
|
|
1631
|
+
? block.content
|
|
1632
|
+
: JSON.stringify(block.content);
|
|
1633
|
+
// Show tool results but keep them concise
|
|
1634
|
+
if (content.length > 50) {
|
|
1635
|
+
output += `\n📋 ${content.slice(0, 800)}${content.length > 800 ? '...' : ''}\n`;
|
|
1636
|
+
}
|
|
1637
|
+
// Create structured tool result log
|
|
1638
|
+
const isError = block.is_error === true;
|
|
1639
|
+
logs.push(createToolResultLog(currentToolName || 'Unknown', !isError, isError ? undefined : content.slice(0, 5000), isError ? content : undefined, isError ? 'error' : 'success', promptId, block.tool_use_id));
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1554
1642
|
}
|
|
1555
1643
|
}
|
|
1556
|
-
else if (json.
|
|
1557
|
-
|
|
1558
|
-
if (
|
|
1559
|
-
|
|
1560
|
-
|
|
1644
|
+
else if (json.type === 'content_block_delta') {
|
|
1645
|
+
// Streaming content delta - accumulate in buffers
|
|
1646
|
+
if (json.delta?.type === 'thinking_delta') {
|
|
1647
|
+
const thinking = json.delta.thinking || '';
|
|
1648
|
+
output += thinking;
|
|
1649
|
+
thinkingBuffer += thinking;
|
|
1650
|
+
}
|
|
1651
|
+
else if (json.delta?.type === 'text_delta') {
|
|
1652
|
+
const text = json.delta.text || '';
|
|
1653
|
+
output += text;
|
|
1654
|
+
textBuffer += text;
|
|
1655
|
+
}
|
|
1656
|
+
else if (json.delta?.type === 'input_json_delta') {
|
|
1657
|
+
// Tool input streaming
|
|
1658
|
+
const partialJson = json.delta.partial_json || '';
|
|
1659
|
+
output += partialJson;
|
|
1660
|
+
toolInputBuffer += partialJson;
|
|
1561
1661
|
}
|
|
1562
1662
|
}
|
|
1563
|
-
else if (json.
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1663
|
+
else if (json.type === 'content_block_start') {
|
|
1664
|
+
// Start of a content block - reset buffers
|
|
1665
|
+
if (json.content_block?.type === 'thinking') {
|
|
1666
|
+
output += '\n💭 Thinking:\n';
|
|
1667
|
+
thinkingBuffer = '';
|
|
1668
|
+
}
|
|
1669
|
+
else if (json.content_block?.type === 'tool_use') {
|
|
1670
|
+
output += `\n🔧 Tool: ${json.content_block.name || 'unknown'}\n`;
|
|
1671
|
+
currentToolName = json.content_block.name;
|
|
1672
|
+
currentToolUseId = json.content_block.id;
|
|
1673
|
+
toolInputBuffer = '';
|
|
1674
|
+
}
|
|
1675
|
+
else if (json.content_block?.type === 'text') {
|
|
1676
|
+
textBuffer = '';
|
|
1569
1677
|
}
|
|
1570
|
-
logs.push(createSystemLog('complete', `Execution ${success ? 'succeeded' : 'failed'}`, success ? 'success' : 'error', promptId, json.data));
|
|
1571
1678
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
// Create structured output log
|
|
1588
|
-
if (block.text.trim()) {
|
|
1589
|
-
logs.push(createOutputLog(block.text, 'markdown', 'info', promptId));
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
else if (block.type === 'tool_use') {
|
|
1593
|
-
output += `\n🔧 Tool: ${block.name}\n`;
|
|
1594
|
-
const inputStr = block.input
|
|
1595
|
-
? (typeof block.input === 'string' ? block.input : JSON.stringify(block.input, null, 2))
|
|
1596
|
-
: '';
|
|
1597
|
-
if (inputStr) {
|
|
1598
|
-
output += `${inputStr.slice(0, 500)}${inputStr.length > 500 ? '...' : ''}\n`;
|
|
1599
|
-
}
|
|
1600
|
-
// Create structured tool call log
|
|
1601
|
-
logs.push(createToolCallLog(block.name, block.input || {}, 'info', promptId, block.id));
|
|
1602
|
-
currentToolUseId = block.id;
|
|
1603
|
-
currentToolName = block.name;
|
|
1679
|
+
else if (json.type === 'content_block_stop') {
|
|
1680
|
+
// Block ended - emit structured logs from buffers
|
|
1681
|
+
output += '\n';
|
|
1682
|
+
if (thinkingBuffer.trim()) {
|
|
1683
|
+
logs.push(createThinkingLog(thinkingBuffer, 'info', promptId));
|
|
1684
|
+
thinkingBuffer = '';
|
|
1685
|
+
}
|
|
1686
|
+
if (textBuffer.trim()) {
|
|
1687
|
+
logs.push(createOutputLog(textBuffer, 'markdown', 'info', promptId));
|
|
1688
|
+
textBuffer = '';
|
|
1689
|
+
}
|
|
1690
|
+
if (toolInputBuffer.trim() && currentToolName) {
|
|
1691
|
+
try {
|
|
1692
|
+
const parsedInput = JSON.parse(toolInputBuffer);
|
|
1693
|
+
logs.push(createToolCallLog(currentToolName, parsedInput, 'info', promptId, currentToolUseId || undefined));
|
|
1604
1694
|
// Track file modifications for memory summaries
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
trackFileModified(promptId, toolInput.file_path);
|
|
1695
|
+
if (parsedInput && typeof parsedInput.file_path === 'string') {
|
|
1696
|
+
if (['Edit', 'Write', 'NotebookEdit'].includes(currentToolName)) {
|
|
1697
|
+
trackFileModified(promptId, parsedInput.file_path);
|
|
1609
1698
|
}
|
|
1610
1699
|
}
|
|
1611
1700
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
: JSON.stringify(block.content);
|
|
1616
|
-
output += `\n📋 Result:\n${content.slice(0, 1000)}${content.length > 1000 ? '...' : ''}\n`;
|
|
1617
|
-
// Create structured tool result log
|
|
1618
|
-
const isError = block.is_error === true;
|
|
1619
|
-
logs.push(createToolResultLog(currentToolName || 'Unknown', !isError, isError ? undefined : content.slice(0, 5000), isError ? content : undefined, isError ? 'error' : 'success', promptId, block.tool_use_id));
|
|
1701
|
+
catch {
|
|
1702
|
+
// Invalid JSON input - emit as-is
|
|
1703
|
+
logs.push(createToolCallLog(currentToolName, { raw: toolInputBuffer }, 'info', promptId, currentToolUseId || undefined));
|
|
1620
1704
|
}
|
|
1705
|
+
toolInputBuffer = '';
|
|
1621
1706
|
}
|
|
1622
1707
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1708
|
+
else if (json.type === 'result') {
|
|
1709
|
+
// Final result from Claude CLI.
|
|
1710
|
+
//
|
|
1711
|
+
// Claude reports an upstream account/API failure here by exiting 0
|
|
1712
|
+
// with is_error set and the error string as `result` (e.g. "API
|
|
1713
|
+
// Error: 400 This organization has been disabled."). Treating that
|
|
1714
|
+
// as a successful ✅ result is wrong — the prompt silently
|
|
1715
|
+
// "completes" with the error text. Detect it and flag for the
|
|
1716
|
+
// claude→orquesta fallback in the close handler instead of emitting
|
|
1717
|
+
// it as success.
|
|
1718
|
+
const apiErrStatus = typeof json.api_error_status === 'number' ? json.api_error_status : 0;
|
|
1719
|
+
const isAccountFailure = json.is_error === true &&
|
|
1720
|
+
(apiErrStatus >= 400 || CLAUDE_PROVIDER_FATAL_PATTERN.test(json.result || ''));
|
|
1721
|
+
if (isAccountFailure && selectedCli === 'claude') {
|
|
1722
|
+
claudeApiError = json.result || `claude api_error_status ${apiErrStatus}`;
|
|
1723
|
+
// Don't emit the error as a ✅ success result; the close handler
|
|
1724
|
+
// logs the failure + the orquesta-cli fallback.
|
|
1725
|
+
}
|
|
1726
|
+
else {
|
|
1727
|
+
if (json.result) {
|
|
1728
|
+
output += `\n✅ ${json.result}\n`;
|
|
1729
|
+
}
|
|
1730
|
+
if (json.cost_usd) {
|
|
1731
|
+
output += `💰 Cost: $${json.cost_usd.toFixed(4)}\n`;
|
|
1732
|
+
}
|
|
1733
|
+
// Track usage for persistence to DB
|
|
1734
|
+
if (json.total_tokens) {
|
|
1735
|
+
promptTokensUsed.set(promptId, json.total_tokens);
|
|
1736
|
+
}
|
|
1737
|
+
if (json.cost_usd) {
|
|
1738
|
+
promptCostCents.set(promptId, Math.round(json.cost_usd * 100));
|
|
1639
1739
|
}
|
|
1740
|
+
// Emit system log for completion
|
|
1741
|
+
logs.push(createSystemLog('complete', json.result || 'Execution completed', 'success', promptId, {
|
|
1742
|
+
cost_cents: json.cost_usd ? Math.round(json.cost_usd * 100) : undefined,
|
|
1743
|
+
tokens_used: json.total_tokens,
|
|
1744
|
+
}));
|
|
1640
1745
|
}
|
|
1641
1746
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
thinkingBuffer += thinking;
|
|
1649
|
-
}
|
|
1650
|
-
else if (json.delta?.type === 'text_delta') {
|
|
1651
|
-
const text = json.delta.text || '';
|
|
1652
|
-
output += text;
|
|
1653
|
-
textBuffer += text;
|
|
1654
|
-
}
|
|
1655
|
-
else if (json.delta?.type === 'input_json_delta') {
|
|
1656
|
-
// Tool input streaming
|
|
1657
|
-
const partialJson = json.delta.partial_json || '';
|
|
1658
|
-
output += partialJson;
|
|
1659
|
-
toolInputBuffer += partialJson;
|
|
1747
|
+
else if (json.type === 'system') {
|
|
1748
|
+
// System messages from Claude CLI
|
|
1749
|
+
if (json.message && !json.message.includes('initialization')) {
|
|
1750
|
+
output += `ℹ️ ${json.message}\n`;
|
|
1751
|
+
logs.push(createSystemLog('heartbeat', json.message, 'info', promptId));
|
|
1752
|
+
}
|
|
1660
1753
|
}
|
|
1754
|
+
// Silently ignore: message_start, message_stop, ping, etc.
|
|
1661
1755
|
}
|
|
1662
|
-
|
|
1663
|
-
//
|
|
1664
|
-
|
|
1665
|
-
output += '\n💭 Thinking:\n';
|
|
1666
|
-
thinkingBuffer = '';
|
|
1667
|
-
}
|
|
1668
|
-
else if (json.content_block?.type === 'tool_use') {
|
|
1669
|
-
output += `\n🔧 Tool: ${json.content_block.name || 'unknown'}\n`;
|
|
1670
|
-
currentToolName = json.content_block.name;
|
|
1671
|
-
currentToolUseId = json.content_block.id;
|
|
1672
|
-
toolInputBuffer = '';
|
|
1673
|
-
}
|
|
1674
|
-
else if (json.content_block?.type === 'text') {
|
|
1675
|
-
textBuffer = '';
|
|
1676
|
-
}
|
|
1756
|
+
catch {
|
|
1757
|
+
// Not valid JSON, output as plain text
|
|
1758
|
+
output += line + '\n';
|
|
1677
1759
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1760
|
+
}
|
|
1761
|
+
return { output, hadValidJson, logs };
|
|
1762
|
+
};
|
|
1763
|
+
// Helper to send special events (git commits, deployments)
|
|
1764
|
+
const sendSpecialEvent = async (eventType, details) => {
|
|
1765
|
+
try {
|
|
1766
|
+
await channel.send({
|
|
1767
|
+
type: 'broadcast',
|
|
1768
|
+
event: eventType,
|
|
1769
|
+
payload: {
|
|
1770
|
+
id: commandId,
|
|
1771
|
+
promptId,
|
|
1772
|
+
...details,
|
|
1773
|
+
timestamp: Date.now(),
|
|
1774
|
+
},
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
catch (err) {
|
|
1778
|
+
logger.warn(`Failed to send ${eventType} event: ${err}`);
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
// Stream stdout
|
|
1782
|
+
if (claude.stdout) {
|
|
1783
|
+
claude.stdout.on('data', (data) => {
|
|
1784
|
+
const text = data.toString();
|
|
1785
|
+
// Skip logging if it looks like base64/binary data (long strings without spaces)
|
|
1786
|
+
const preview = text.slice(0, 150);
|
|
1787
|
+
const looksLikeBinary = preview.length > 50 && !preview.includes(' ') && /^[A-Za-z0-9+/=]+$/.test(preview.replace(/\s/g, ''));
|
|
1788
|
+
if (!looksLikeBinary) {
|
|
1789
|
+
logger.info(`[STDOUT] ${text.slice(0, 100)}`);
|
|
1790
|
+
}
|
|
1791
|
+
// Parse stream-json and send formatted output
|
|
1792
|
+
const { output: parsed, hadValidJson, logs: structuredLogs } = parseStreamJson(text);
|
|
1793
|
+
// Only fallback to raw text if it wasn't valid JSON
|
|
1794
|
+
// If it was valid JSON but produced no output (e.g., system init), don't send anything
|
|
1795
|
+
const outputToSend = hadValidJson ? parsed : text;
|
|
1796
|
+
if (outputToSend.trim()) {
|
|
1797
|
+
logger.output('stdout', outputToSend.slice(0, 200));
|
|
1798
|
+
sendOutput(channel, commandId, 'stdout', outputToSend);
|
|
1799
|
+
}
|
|
1800
|
+
// Add structured logs to accumulator for persistence
|
|
1801
|
+
for (const log of structuredLogs) {
|
|
1802
|
+
addStructuredLog(promptId, log);
|
|
1803
|
+
}
|
|
1804
|
+
// Fallback: if no structured logs but we have output, create output log
|
|
1805
|
+
if (structuredLogs.length === 0 && outputToSend.trim() && !hadValidJson) {
|
|
1806
|
+
addLog(promptId, 'info', 'output', outputToSend);
|
|
1807
|
+
}
|
|
1808
|
+
// Detect git commits in the output
|
|
1809
|
+
// Matches various formats:
|
|
1810
|
+
// - [f0f66f3] commit message (git log)
|
|
1811
|
+
// - commit f0f66f3 (git show)
|
|
1812
|
+
// - **Commit**: `f0f66f3` (Claude formatted)
|
|
1813
|
+
// - Commit: f0f66f3 (plain text)
|
|
1814
|
+
const commitMatch = text.match(/\[([a-f0-9]{7,40})\]\s+(.+)/i) ||
|
|
1815
|
+
text.match(/commit\s+([a-f0-9]{7,40})/i) ||
|
|
1816
|
+
text.match(/\*\*Commit\*\*:\s*`([a-f0-9]{7,40})`/i) ||
|
|
1817
|
+
text.match(/Commit:\s*`?([a-f0-9]{7,40})`?/i);
|
|
1818
|
+
if (commitMatch) {
|
|
1819
|
+
// Try to get GitHub URL from git remote
|
|
1820
|
+
let commitUrl = null;
|
|
1821
|
+
try {
|
|
1822
|
+
const remoteUrl = execSync('git config --get remote.origin.url', {
|
|
1823
|
+
cwd: workingDirectory,
|
|
1824
|
+
encoding: 'utf-8',
|
|
1825
|
+
timeout: 5000,
|
|
1826
|
+
}).trim();
|
|
1827
|
+
// Convert git remote URL to GitHub commit URL
|
|
1828
|
+
const githubMatch = remoteUrl.match(/github\.com[:/](.+?)(?:\.git)?$/);
|
|
1829
|
+
if (githubMatch) {
|
|
1830
|
+
const repoPath = githubMatch[1].replace(/\.git$/, '');
|
|
1831
|
+
commitUrl = `https://github.com/${repoPath}/commit/${commitMatch[1]}`;
|
|
1703
1832
|
}
|
|
1704
|
-
toolInputBuffer = '';
|
|
1705
1833
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
// Final result from Claude CLI.
|
|
1709
|
-
//
|
|
1710
|
-
// Claude reports an upstream account/API failure here by exiting 0
|
|
1711
|
-
// with is_error set and the error string as `result` (e.g. "API
|
|
1712
|
-
// Error: 400 This organization has been disabled."). Treating that
|
|
1713
|
-
// as a successful ✅ result is wrong — the prompt silently
|
|
1714
|
-
// "completes" with the error text. Detect it and flag for the
|
|
1715
|
-
// claude→orquesta fallback in the close handler instead of emitting
|
|
1716
|
-
// it as success.
|
|
1717
|
-
const apiErrStatus = typeof json.api_error_status === 'number' ? json.api_error_status : 0;
|
|
1718
|
-
const isAccountFailure = json.is_error === true &&
|
|
1719
|
-
(apiErrStatus >= 400 || CLAUDE_PROVIDER_FATAL_PATTERN.test(json.result || ''));
|
|
1720
|
-
if (isAccountFailure && selectedCli === 'claude') {
|
|
1721
|
-
claudeApiError = json.result || `claude api_error_status ${apiErrStatus}`;
|
|
1722
|
-
// Don't emit the error as a ✅ success result; the close handler
|
|
1723
|
-
// logs the failure + the orquesta-cli fallback.
|
|
1834
|
+
catch {
|
|
1835
|
+
// Ignore errors getting git remote
|
|
1724
1836
|
}
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1837
|
+
sendSpecialEvent('git_commit', {
|
|
1838
|
+
commitSha: commitMatch[1],
|
|
1839
|
+
message: commitMatch[2] || '',
|
|
1840
|
+
commitUrl,
|
|
1841
|
+
});
|
|
1842
|
+
// Track commit as key action for memory summary
|
|
1843
|
+
const commitMsg = commitMatch[2] ? commitMatch[2].slice(0, 50) : 'changes';
|
|
1844
|
+
trackAction(promptId, `Committed: ${commitMsg}`);
|
|
1845
|
+
}
|
|
1846
|
+
// Detect deployments in the output
|
|
1847
|
+
if (text.includes('Deployed to') || text.includes('Production:') || text.includes('vercel.app')) {
|
|
1848
|
+
const urlMatch = text.match(/(https:\/\/[^\s]+\.vercel\.app[^\s]*)/i);
|
|
1849
|
+
if (urlMatch) {
|
|
1850
|
+
sendSpecialEvent('deploy', {
|
|
1851
|
+
url: urlMatch[1],
|
|
1852
|
+
provider: 'vercel',
|
|
1853
|
+
});
|
|
1854
|
+
// Track deployment as key action for memory summary
|
|
1855
|
+
trackAction(promptId, `Deployed to Vercel`);
|
|
1744
1856
|
}
|
|
1745
1857
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1858
|
+
// Detect requirements (API keys, credentials, decisions needed)
|
|
1859
|
+
const requirementResult = detectRequirement(text, commandId);
|
|
1860
|
+
if (requirementResult?.detected && requirementResult.requirement) {
|
|
1861
|
+
logger.warn(`Requirement detected: ${requirementResult.requirement.title}`);
|
|
1862
|
+
// Send via realtime channel
|
|
1863
|
+
sendRequirement(channel, commandId, promptId, requirementResult.requirement);
|
|
1864
|
+
// Also persist to database (creates Linear ticket if connected)
|
|
1865
|
+
persistRequirement(promptId, requirementResult.requirement).catch(err => {
|
|
1866
|
+
logger.warn(`Failed to persist requirement: ${err}`);
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
// Detect QA instructions
|
|
1870
|
+
const qaInstructions = detectQAInstructions(text, commandId);
|
|
1871
|
+
if (qaInstructions) {
|
|
1872
|
+
logger.success(`QA Instructions detected: ${qaInstructions.feature}`);
|
|
1873
|
+
// Send via realtime channel
|
|
1874
|
+
sendQAInstructions(channel, commandId, promptId, qaInstructions);
|
|
1875
|
+
// Persist to database
|
|
1876
|
+
persistQAInstructions(promptId, qaInstructions).catch(err => {
|
|
1877
|
+
logger.warn(`Failed to persist QA instructions: ${err}`);
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
// In supervised mode, check for permission requests
|
|
1881
|
+
if (mode === 'supervised') {
|
|
1882
|
+
const { detected, content: permissionContent } = detectPermissionRequest(text, commandId);
|
|
1883
|
+
if (detected) {
|
|
1884
|
+
handlePermissionRequest(commandId, promptId, channel, claude, permissionContent);
|
|
1751
1885
|
}
|
|
1752
1886
|
}
|
|
1753
|
-
// Silently ignore: message_start, message_stop, ping, etc.
|
|
1754
|
-
}
|
|
1755
|
-
catch {
|
|
1756
|
-
// Not valid JSON, output as plain text
|
|
1757
|
-
output += line + '\n';
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
return { output, hadValidJson, logs };
|
|
1761
|
-
};
|
|
1762
|
-
// Helper to send special events (git commits, deployments)
|
|
1763
|
-
const sendSpecialEvent = async (eventType, details) => {
|
|
1764
|
-
try {
|
|
1765
|
-
await channel.send({
|
|
1766
|
-
type: 'broadcast',
|
|
1767
|
-
event: eventType,
|
|
1768
|
-
payload: {
|
|
1769
|
-
id: commandId,
|
|
1770
|
-
promptId,
|
|
1771
|
-
...details,
|
|
1772
|
-
timestamp: Date.now(),
|
|
1773
|
-
},
|
|
1774
1887
|
});
|
|
1775
1888
|
}
|
|
1776
|
-
|
|
1777
|
-
logger.warn(
|
|
1889
|
+
else {
|
|
1890
|
+
logger.warn('No stdout pipe available');
|
|
1891
|
+
}
|
|
1892
|
+
// Stream stderr
|
|
1893
|
+
if (claude.stderr) {
|
|
1894
|
+
claude.stderr.on('data', (data) => {
|
|
1895
|
+
const raw = data.toString();
|
|
1896
|
+
const text = stripAnsiCodes(raw);
|
|
1897
|
+
if (!text.trim())
|
|
1898
|
+
return;
|
|
1899
|
+
logger.info(`[STDERR] ${text.slice(0, 100)}`);
|
|
1900
|
+
logger.output('stderr', text);
|
|
1901
|
+
sendOutput(channel, commandId, 'stderr', text);
|
|
1902
|
+
// Add to log accumulator for persistence backup
|
|
1903
|
+
addLog(promptId, 'error', 'stderr', text);
|
|
1904
|
+
});
|
|
1778
1905
|
}
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
//
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
// Try to get GitHub URL from git remote
|
|
1819
|
-
let commitUrl = null;
|
|
1820
|
-
try {
|
|
1821
|
-
const remoteUrl = execSync('git config --get remote.origin.url', {
|
|
1822
|
-
cwd: workingDirectory,
|
|
1823
|
-
encoding: 'utf-8',
|
|
1824
|
-
timeout: 5000,
|
|
1825
|
-
}).trim();
|
|
1826
|
-
// Convert git remote URL to GitHub commit URL
|
|
1827
|
-
const githubMatch = remoteUrl.match(/github\.com[:/](.+?)(?:\.git)?$/);
|
|
1828
|
-
if (githubMatch) {
|
|
1829
|
-
const repoPath = githubMatch[1].replace(/\.git$/, '');
|
|
1830
|
-
commitUrl = `https://github.com/${repoPath}/commit/${commitMatch[1]}`;
|
|
1906
|
+
else {
|
|
1907
|
+
logger.warn('No stderr pipe available');
|
|
1908
|
+
}
|
|
1909
|
+
// Wrap process lifecycle in a Promise so callers can await completion
|
|
1910
|
+
return new Promise((resolveExecution) => {
|
|
1911
|
+
// Handle completion
|
|
1912
|
+
claude.on('close', async (code) => {
|
|
1913
|
+
runningProcesses.delete(commandId);
|
|
1914
|
+
if (promptId)
|
|
1915
|
+
promptIdToCommandId.delete(promptId);
|
|
1916
|
+
pendingSupervisionCallbacks.delete(commandId);
|
|
1917
|
+
// Clean up per-execution buffers (concurrent-safe)
|
|
1918
|
+
outputBufferMap.delete(commandId);
|
|
1919
|
+
detectedRequirementsMap.delete(commandId);
|
|
1920
|
+
qaBufferMap.delete(commandId);
|
|
1921
|
+
qaDetectedMap.delete(commandId);
|
|
1922
|
+
// claude→orquesta fallback: if Claude CLI failed at the account/API
|
|
1923
|
+
// level (org disabled, auth) and orquesta-cli is available, re-run the
|
|
1924
|
+
// SAME prompt through it (→ Batuta proxy → Kimi) instead of completing
|
|
1925
|
+
// with the error text. _forceCli guards against re-recursing. The
|
|
1926
|
+
// recursive call owns final completion (sendComplete/flush), so we skip
|
|
1927
|
+
// the rest of this handler and resolve once it returns.
|
|
1928
|
+
if (claudeApiError && selectedCli === 'claude' && !options._forceCli && isOrquestaCliAvailable()) {
|
|
1929
|
+
logger.warn(`Claude CLI unavailable (${claudeApiError.slice(0, 80)}) — retrying via orquesta-cli`);
|
|
1930
|
+
addLog(promptId, 'warning', 'system', 'Claude CLI unavailable — falling back to orquesta-cli');
|
|
1931
|
+
try {
|
|
1932
|
+
await execute({ ...options, _forceCli: 'orquesta' });
|
|
1933
|
+
}
|
|
1934
|
+
catch (err) {
|
|
1935
|
+
logger.error(`orquesta-cli fallback failed: ${err}`);
|
|
1936
|
+
stopLogPersistence(promptId);
|
|
1937
|
+
if (_activePromptId === promptId)
|
|
1938
|
+
_activePromptId = null;
|
|
1939
|
+
try {
|
|
1940
|
+
await flushLogs(promptId);
|
|
1941
|
+
}
|
|
1942
|
+
catch { /* noop */ }
|
|
1943
|
+
executionLogs.delete(promptId);
|
|
1944
|
+
sendComplete(channel, commandId, 1, startTime);
|
|
1831
1945
|
}
|
|
1946
|
+
resolveExecution();
|
|
1947
|
+
return;
|
|
1832
1948
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1949
|
+
// If Claude failed at the account level but we could NOT fall back
|
|
1950
|
+
// (no orquesta-cli, or this IS the forced run), surface it as a real
|
|
1951
|
+
// failure instead of a silent exit-0 "completed" with empty output.
|
|
1952
|
+
// claude exits 0 even on "organization has been disabled".
|
|
1953
|
+
if (claudeApiError) {
|
|
1954
|
+
addLog(promptId, 'error', 'system', `Claude CLI unavailable (no fallback): ${claudeApiError}`);
|
|
1955
|
+
sendOutput(channel, commandId, 'stderr', `\n❌ Claude CLI unavailable: ${claudeApiError}\n`);
|
|
1835
1956
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
// Track commit as key action for memory summary
|
|
1842
|
-
const commitMsg = commitMatch[2] ? commitMatch[2].slice(0, 50) : 'changes';
|
|
1843
|
-
trackAction(promptId, `Committed: ${commitMsg}`);
|
|
1844
|
-
}
|
|
1845
|
-
// Detect deployments in the output
|
|
1846
|
-
if (text.includes('Deployed to') || text.includes('Production:') || text.includes('vercel.app')) {
|
|
1847
|
-
const urlMatch = text.match(/(https:\/\/[^\s]+\.vercel\.app[^\s]*)/i);
|
|
1848
|
-
if (urlMatch) {
|
|
1849
|
-
sendSpecialEvent('deploy', {
|
|
1850
|
-
url: urlMatch[1],
|
|
1851
|
-
provider: 'vercel',
|
|
1852
|
-
});
|
|
1853
|
-
// Track deployment as key action for memory summary
|
|
1854
|
-
trackAction(promptId, `Deployed to Vercel`);
|
|
1957
|
+
const exitCode = claudeApiError ? (code || 1) : (code ?? 0);
|
|
1958
|
+
const duration = Date.now() - startTime;
|
|
1959
|
+
const status = exitCode === 0 ? 'completed' : 'failed';
|
|
1960
|
+
if (exitCode === 0) {
|
|
1961
|
+
logger.success(`Completed in ${(duration / 1000).toFixed(1)}s`);
|
|
1855
1962
|
}
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
const requirementResult = detectRequirement(text, commandId);
|
|
1859
|
-
if (requirementResult?.detected && requirementResult.requirement) {
|
|
1860
|
-
logger.warn(`Requirement detected: ${requirementResult.requirement.title}`);
|
|
1861
|
-
// Send via realtime channel
|
|
1862
|
-
sendRequirement(channel, commandId, promptId, requirementResult.requirement);
|
|
1863
|
-
// Also persist to database (creates Linear ticket if connected)
|
|
1864
|
-
persistRequirement(promptId, requirementResult.requirement).catch(err => {
|
|
1865
|
-
logger.warn(`Failed to persist requirement: ${err}`);
|
|
1866
|
-
});
|
|
1867
|
-
}
|
|
1868
|
-
// Detect QA instructions
|
|
1869
|
-
const qaInstructions = detectQAInstructions(text, commandId);
|
|
1870
|
-
if (qaInstructions) {
|
|
1871
|
-
logger.success(`QA Instructions detected: ${qaInstructions.feature}`);
|
|
1872
|
-
// Send via realtime channel
|
|
1873
|
-
sendQAInstructions(channel, commandId, promptId, qaInstructions);
|
|
1874
|
-
// Persist to database
|
|
1875
|
-
persistQAInstructions(promptId, qaInstructions).catch(err => {
|
|
1876
|
-
logger.warn(`Failed to persist QA instructions: ${err}`);
|
|
1877
|
-
});
|
|
1878
|
-
}
|
|
1879
|
-
// In supervised mode, check for permission requests
|
|
1880
|
-
if (mode === 'supervised') {
|
|
1881
|
-
const { detected, content: permissionContent } = detectPermissionRequest(text, commandId);
|
|
1882
|
-
if (detected) {
|
|
1883
|
-
handlePermissionRequest(commandId, promptId, channel, claude, permissionContent);
|
|
1963
|
+
else {
|
|
1964
|
+
logger.warn(`Exited with code ${exitCode}`);
|
|
1884
1965
|
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
// Stream stderr
|
|
1892
|
-
if (claude.stderr) {
|
|
1893
|
-
claude.stderr.on('data', (data) => {
|
|
1894
|
-
const raw = data.toString();
|
|
1895
|
-
const text = stripAnsiCodes(raw);
|
|
1896
|
-
if (!text.trim())
|
|
1897
|
-
return;
|
|
1898
|
-
logger.info(`[STDERR] ${text.slice(0, 100)}`);
|
|
1899
|
-
logger.output('stderr', text);
|
|
1900
|
-
sendOutput(channel, commandId, 'stderr', text);
|
|
1901
|
-
// Add to log accumulator for persistence backup
|
|
1902
|
-
addLog(promptId, 'error', 'stderr', text);
|
|
1903
|
-
});
|
|
1904
|
-
}
|
|
1905
|
-
else {
|
|
1906
|
-
logger.warn('No stderr pipe available');
|
|
1907
|
-
}
|
|
1908
|
-
// Wrap process lifecycle in a Promise so callers can await completion
|
|
1909
|
-
return new Promise((resolveExecution) => {
|
|
1910
|
-
// Handle completion
|
|
1911
|
-
claude.on('close', async (code) => {
|
|
1912
|
-
runningProcesses.delete(commandId);
|
|
1913
|
-
if (promptId)
|
|
1914
|
-
promptIdToCommandId.delete(promptId);
|
|
1915
|
-
pendingSupervisionCallbacks.delete(commandId);
|
|
1916
|
-
// Clean up per-execution buffers (concurrent-safe)
|
|
1917
|
-
outputBufferMap.delete(commandId);
|
|
1918
|
-
detectedRequirementsMap.delete(commandId);
|
|
1919
|
-
qaBufferMap.delete(commandId);
|
|
1920
|
-
qaDetectedMap.delete(commandId);
|
|
1921
|
-
// claude→orquesta fallback: if Claude CLI failed at the account/API
|
|
1922
|
-
// level (org disabled, auth) and orquesta-cli is available, re-run the
|
|
1923
|
-
// SAME prompt through it (→ Batuta proxy → Kimi) instead of completing
|
|
1924
|
-
// with the error text. _forceCli guards against re-recursing. The
|
|
1925
|
-
// recursive call owns final completion (sendComplete/flush), so we skip
|
|
1926
|
-
// the rest of this handler and resolve once it returns.
|
|
1927
|
-
if (claudeApiError && selectedCli === 'claude' && !options._forceCli && isOrquestaCliAvailable()) {
|
|
1928
|
-
logger.warn(`Claude CLI unavailable (${claudeApiError.slice(0, 80)}) — retrying via orquesta-cli`);
|
|
1929
|
-
addLog(promptId, 'warning', 'system', 'Claude CLI unavailable — falling back to orquesta-cli');
|
|
1966
|
+
// Add completion log entry
|
|
1967
|
+
addLog(promptId, exitCode === 0 ? 'success' : 'error', 'system', `Execution ${status} with exit code ${exitCode} in ${(duration / 1000).toFixed(1)}s`);
|
|
1968
|
+
// Stop periodic log persistence and flush any remaining logs
|
|
1969
|
+
stopLogPersistence(promptId);
|
|
1970
|
+
if (_activePromptId === promptId)
|
|
1971
|
+
_activePromptId = null;
|
|
1930
1972
|
try {
|
|
1931
|
-
await
|
|
1973
|
+
await flushLogs(promptId);
|
|
1974
|
+
executionLogs.delete(promptId);
|
|
1975
|
+
logger.debug('All logs flushed to database');
|
|
1932
1976
|
}
|
|
1933
1977
|
catch (err) {
|
|
1934
|
-
logger.error(`
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1978
|
+
logger.error(`Failed to flush final logs: ${err}`);
|
|
1979
|
+
}
|
|
1980
|
+
// Clear output buffer for this command
|
|
1981
|
+
clearOutputBuffer(commandId);
|
|
1982
|
+
// Clean up attachment temp files
|
|
1983
|
+
if (attachmentCleanup) {
|
|
1984
|
+
attachmentCleanup();
|
|
1985
|
+
}
|
|
1986
|
+
// Send completion via WebSocket (for real-time UI update)
|
|
1987
|
+
sendComplete(channel, commandId, exitCode, startTime);
|
|
1988
|
+
// Generate execution summary for project memory
|
|
1989
|
+
const summary = generateExecutionSummary(promptId, content, exitCode);
|
|
1990
|
+
if (summary) {
|
|
1991
|
+
logger.info(`Generated summary: ${summary.text.slice(0, 100)}...`);
|
|
1992
|
+
logger.debug(`Files modified: ${summary.files_modified.length}, Actions: ${summary.key_actions.length}`);
|
|
1993
|
+
}
|
|
1994
|
+
// Collect token/cost usage tracked during streaming
|
|
1995
|
+
const usage = {};
|
|
1996
|
+
if (promptTokensUsed.has(promptId)) {
|
|
1997
|
+
usage.tokens_used = promptTokensUsed.get(promptId);
|
|
1998
|
+
promptTokensUsed.delete(promptId);
|
|
1943
1999
|
}
|
|
2000
|
+
if (promptCostCents.has(promptId)) {
|
|
2001
|
+
usage.cost_cents = promptCostCents.get(promptId);
|
|
2002
|
+
promptCostCents.delete(promptId);
|
|
2003
|
+
}
|
|
2004
|
+
// Update prompt status in database with summary and usage (backend fix - don't rely on frontend)
|
|
2005
|
+
updatePromptStatus(promptId, status, summary || undefined, Object.keys(usage).length > 0 ? usage : undefined).catch(err => {
|
|
2006
|
+
logger.error(`Failed to update prompt status: ${err}`);
|
|
2007
|
+
});
|
|
1944
2008
|
resolveExecution();
|
|
1945
|
-
return;
|
|
1946
|
-
}
|
|
1947
|
-
// If Claude failed at the account level but we could NOT fall back
|
|
1948
|
-
// (no orquesta-cli, or this IS the forced run), surface it as a real
|
|
1949
|
-
// failure instead of a silent exit-0 "completed" with empty output.
|
|
1950
|
-
// claude exits 0 even on "organization has been disabled".
|
|
1951
|
-
if (claudeApiError) {
|
|
1952
|
-
addLog(promptId, 'error', 'system', `Claude CLI unavailable (no fallback): ${claudeApiError}`);
|
|
1953
|
-
sendOutput(channel, commandId, 'stderr', `\n❌ Claude CLI unavailable: ${claudeApiError}\n`);
|
|
1954
|
-
}
|
|
1955
|
-
const exitCode = claudeApiError ? (code || 1) : (code ?? 0);
|
|
1956
|
-
const duration = Date.now() - startTime;
|
|
1957
|
-
const status = exitCode === 0 ? 'completed' : 'failed';
|
|
1958
|
-
if (exitCode === 0) {
|
|
1959
|
-
logger.success(`Completed in ${(duration / 1000).toFixed(1)}s`);
|
|
1960
|
-
}
|
|
1961
|
-
else {
|
|
1962
|
-
logger.warn(`Exited with code ${exitCode}`);
|
|
1963
|
-
}
|
|
1964
|
-
// Add completion log entry
|
|
1965
|
-
addLog(promptId, exitCode === 0 ? 'success' : 'error', 'system', `Execution ${status} with exit code ${exitCode} in ${(duration / 1000).toFixed(1)}s`);
|
|
1966
|
-
// Stop periodic log persistence and flush any remaining logs
|
|
1967
|
-
stopLogPersistence(promptId);
|
|
1968
|
-
if (_activePromptId === promptId)
|
|
1969
|
-
_activePromptId = null;
|
|
1970
|
-
try {
|
|
1971
|
-
await flushLogs(promptId);
|
|
1972
|
-
logger.debug('All logs flushed to database');
|
|
1973
|
-
}
|
|
1974
|
-
catch (err) {
|
|
1975
|
-
logger.error(`Failed to flush final logs: ${err}`);
|
|
1976
|
-
}
|
|
1977
|
-
// Clear output buffer for this command
|
|
1978
|
-
clearOutputBuffer(commandId);
|
|
1979
|
-
// Clean up attachment temp files
|
|
1980
|
-
if (attachmentCleanup) {
|
|
1981
|
-
attachmentCleanup();
|
|
1982
|
-
}
|
|
1983
|
-
// Send completion via WebSocket (for real-time UI update)
|
|
1984
|
-
sendComplete(channel, commandId, exitCode, startTime);
|
|
1985
|
-
// Generate execution summary for project memory
|
|
1986
|
-
const summary = generateExecutionSummary(promptId, content, exitCode);
|
|
1987
|
-
if (summary) {
|
|
1988
|
-
logger.info(`Generated summary: ${summary.text.slice(0, 100)}...`);
|
|
1989
|
-
logger.debug(`Files modified: ${summary.files_modified.length}, Actions: ${summary.key_actions.length}`);
|
|
1990
|
-
}
|
|
1991
|
-
// Collect token/cost usage tracked during streaming
|
|
1992
|
-
const usage = {};
|
|
1993
|
-
if (promptTokensUsed.has(promptId)) {
|
|
1994
|
-
usage.tokens_used = promptTokensUsed.get(promptId);
|
|
1995
|
-
promptTokensUsed.delete(promptId);
|
|
1996
|
-
}
|
|
1997
|
-
if (promptCostCents.has(promptId)) {
|
|
1998
|
-
usage.cost_cents = promptCostCents.get(promptId);
|
|
1999
|
-
promptCostCents.delete(promptId);
|
|
2000
|
-
}
|
|
2001
|
-
// Update prompt status in database with summary and usage (backend fix - don't rely on frontend)
|
|
2002
|
-
updatePromptStatus(promptId, status, summary || undefined, Object.keys(usage).length > 0 ? usage : undefined).catch(err => {
|
|
2003
|
-
logger.error(`Failed to update prompt status: ${err}`);
|
|
2004
2009
|
});
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2010
|
+
// Handle errors
|
|
2011
|
+
claude.on('error', async (err) => {
|
|
2012
|
+
runningProcesses.delete(commandId);
|
|
2013
|
+
if (promptId)
|
|
2014
|
+
promptIdToCommandId.delete(promptId);
|
|
2015
|
+
pendingSupervisionCallbacks.delete(commandId);
|
|
2016
|
+
logger.error(`Execution error: ${err.message}`);
|
|
2017
|
+
// Add error log entry
|
|
2018
|
+
addLog(promptId, 'error', 'system', `Execution error: ${err.message}`);
|
|
2019
|
+
// Stop log persistence and flush remaining logs
|
|
2020
|
+
stopLogPersistence(promptId);
|
|
2021
|
+
if (_activePromptId === promptId)
|
|
2022
|
+
_activePromptId = null;
|
|
2023
|
+
try {
|
|
2024
|
+
await flushLogs(promptId);
|
|
2025
|
+
}
|
|
2026
|
+
catch (flushErr) {
|
|
2027
|
+
logger.error(`Failed to flush logs on error: ${flushErr}`);
|
|
2028
|
+
}
|
|
2029
|
+
// Clear output buffer
|
|
2030
|
+
clearOutputBuffer(commandId);
|
|
2031
|
+
// Clean up attachment temp files
|
|
2032
|
+
if (attachmentCleanup) {
|
|
2033
|
+
attachmentCleanup();
|
|
2034
|
+
}
|
|
2035
|
+
if (err.message.includes('ENOENT')) {
|
|
2036
|
+
const errPath = err.path;
|
|
2037
|
+
if (errPath === 'script') {
|
|
2038
|
+
sendError(channel, commandId, `Failed to spawn PTY wrapper 'script' (Unix-only). On Windows this code path should be unreachable — please report this bug.`, 'SPAWN_SCRIPT_FAILED');
|
|
2039
|
+
}
|
|
2040
|
+
else {
|
|
2041
|
+
sendError(channel, commandId, `CLI not found: '${errPath || cliCommand}'. Install with: npm install -g @anthropic-ai/claude-code`, 'CLI_NOT_FOUND');
|
|
2042
|
+
}
|
|
2036
2043
|
}
|
|
2037
2044
|
else {
|
|
2038
|
-
sendError(channel, commandId,
|
|
2045
|
+
sendError(channel, commandId, err.message);
|
|
2039
2046
|
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
sendError(channel, commandId, err.message);
|
|
2043
|
-
}
|
|
2044
|
-
resolveExecution();
|
|
2047
|
+
resolveExecution();
|
|
2048
|
+
});
|
|
2045
2049
|
});
|
|
2046
|
-
}
|
|
2050
|
+
}
|
|
2051
|
+
catch (err) {
|
|
2052
|
+
_activePromptId = null;
|
|
2053
|
+
throw err;
|
|
2054
|
+
}
|
|
2047
2055
|
}
|
|
2048
2056
|
// Handle a detected permission request
|
|
2049
2057
|
function handlePermissionRequest(commandId, promptId, channel, process, content) {
|