bopodev-agent-sdk 0.1.9 → 0.1.11
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-typecheck.log +1 -1
- package/LICENSE +1 -1
- package/dist/agent-sdk/src/adapters.d.ts +12 -1
- package/dist/agent-sdk/src/registry.d.ts +4 -1
- package/dist/agent-sdk/src/runtime.d.ts +42 -1
- package/dist/agent-sdk/src/types.d.ts +81 -1
- package/dist/contracts/src/index.d.ts +140 -1
- package/package.json +2 -2
- package/src/adapters.ts +1189 -14
- package/src/registry.ts +38 -2
- package/src/runtime.ts +1254 -37
- package/src/types.ts +90 -1
package/src/runtime.ts
CHANGED
|
@@ -5,6 +5,35 @@ import { dirname, join, resolve } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import type { AgentRuntimeConfig } from "./types";
|
|
7
7
|
|
|
8
|
+
type LocalProvider = "claude_code" | "codex" | "cursor" | "opencode";
|
|
9
|
+
type ClaudeContractDiagnostics = {
|
|
10
|
+
commandOverride: boolean;
|
|
11
|
+
commandLooksClaude: boolean;
|
|
12
|
+
commandWasProviderAlias: boolean;
|
|
13
|
+
hasPromptFlag: boolean;
|
|
14
|
+
hasOutputFormatJson: boolean;
|
|
15
|
+
outputFormat: string | null;
|
|
16
|
+
hasMaxTurnsFlag: boolean;
|
|
17
|
+
hasVerboseFlag: boolean;
|
|
18
|
+
hasDangerouslySkipPermissions: boolean;
|
|
19
|
+
hasJsonSchema: boolean;
|
|
20
|
+
missingRequiredArgs: string[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ParsedUsageRecord = {
|
|
24
|
+
tokenInput?: number;
|
|
25
|
+
tokenOutput?: number;
|
|
26
|
+
usdCost?: number;
|
|
27
|
+
summary?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type CursorParsedStream = {
|
|
31
|
+
usage: ParsedUsageRecord;
|
|
32
|
+
sessionId?: string;
|
|
33
|
+
errorMessage?: string;
|
|
34
|
+
resultSubtype?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
8
37
|
export interface RuntimeExecutionOutput {
|
|
9
38
|
ok: boolean;
|
|
10
39
|
code: number | null;
|
|
@@ -21,6 +50,36 @@ export interface RuntimeExecutionOutput {
|
|
|
21
50
|
usdCost?: number;
|
|
22
51
|
summary?: string;
|
|
23
52
|
};
|
|
53
|
+
structuredOutputSource?: "stdout" | "stderr";
|
|
54
|
+
structuredOutputDiagnostics?: {
|
|
55
|
+
stdoutJsonObjectCount: number;
|
|
56
|
+
stderrJsonObjectCount: number;
|
|
57
|
+
stderrStructuredUsageDetected: boolean;
|
|
58
|
+
stdoutBytes: number;
|
|
59
|
+
stderrBytes: number;
|
|
60
|
+
hasAnyOutput: boolean;
|
|
61
|
+
lastStdoutLine?: string;
|
|
62
|
+
lastStderrLine?: string;
|
|
63
|
+
likelyCause:
|
|
64
|
+
| "no_output_from_runtime"
|
|
65
|
+
| "json_missing"
|
|
66
|
+
| "json_on_stderr_only"
|
|
67
|
+
| "schema_or_shape_mismatch";
|
|
68
|
+
claudeStopReason?: string;
|
|
69
|
+
claudeResultSubtype?: string;
|
|
70
|
+
claudeSessionId?: string;
|
|
71
|
+
claudeContract?: ClaudeContractDiagnostics;
|
|
72
|
+
};
|
|
73
|
+
commandUsed?: string;
|
|
74
|
+
argsUsed?: string[];
|
|
75
|
+
transcript?: RuntimeTranscriptEvent[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface RuntimeTranscriptEvent {
|
|
79
|
+
kind: "system" | "assistant" | "thinking" | "tool_call" | "tool_result" | "result" | "stderr";
|
|
80
|
+
label?: string;
|
|
81
|
+
text?: string;
|
|
82
|
+
payload?: string;
|
|
24
83
|
}
|
|
25
84
|
|
|
26
85
|
export interface RuntimeAttemptTrace {
|
|
@@ -50,7 +109,16 @@ function pickDefaultCommand(provider: "claude_code" | "codex") {
|
|
|
50
109
|
|
|
51
110
|
function providerDefaultArgs(provider: "claude_code" | "codex", config?: AgentRuntimeConfig) {
|
|
52
111
|
if (provider === "claude_code") {
|
|
53
|
-
return [
|
|
112
|
+
return [
|
|
113
|
+
"--print",
|
|
114
|
+
"-",
|
|
115
|
+
"--output-format",
|
|
116
|
+
"stream-json",
|
|
117
|
+
"--verbose",
|
|
118
|
+
"--max-turns",
|
|
119
|
+
"8",
|
|
120
|
+
"--dangerously-skip-permissions"
|
|
121
|
+
];
|
|
54
122
|
}
|
|
55
123
|
// Keep Codex non-interactive, sandboxed, and writable in-workspace by default.
|
|
56
124
|
// Codex CLI rejects combining --full-auto with sandbox bypass flags.
|
|
@@ -92,6 +160,13 @@ function providerConfigArgs(provider: "claude_code" | "codex", config?: AgentRun
|
|
|
92
160
|
return args;
|
|
93
161
|
}
|
|
94
162
|
|
|
163
|
+
function resolveControlPlaneEnvValue(env: NodeJS.ProcessEnv | Record<string, string> | undefined, suffix: string) {
|
|
164
|
+
if (!env) {
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
return String(env[`BOPODEV_${suffix}`] ?? "").trim();
|
|
168
|
+
}
|
|
169
|
+
|
|
95
170
|
function shouldBypassCodexSandbox(config?: AgentRuntimeConfig) {
|
|
96
171
|
if (config?.runPolicy?.sandboxMode === "full_access") {
|
|
97
172
|
return true;
|
|
@@ -101,16 +176,12 @@ function shouldBypassCodexSandbox(config?: AgentRuntimeConfig) {
|
|
|
101
176
|
return false;
|
|
102
177
|
}
|
|
103
178
|
const hasControlPlaneContext =
|
|
104
|
-
|
|
105
|
-
env
|
|
106
|
-
typeof env.BOPOHQ_REQUEST_HEADERS_JSON === "string" &&
|
|
107
|
-
env.BOPOHQ_REQUEST_HEADERS_JSON.trim().length > 0;
|
|
179
|
+
resolveControlPlaneEnvValue(env, "API_BASE_URL").length > 0 &&
|
|
180
|
+
resolveControlPlaneEnvValue(env, "REQUEST_HEADERS_JSON").length > 0;
|
|
108
181
|
if (!hasControlPlaneContext) {
|
|
109
182
|
return false;
|
|
110
183
|
}
|
|
111
|
-
const enforceSandbox =
|
|
112
|
-
.trim()
|
|
113
|
-
.toLowerCase();
|
|
184
|
+
const enforceSandbox = resolveControlPlaneEnvValue(env, "ENFORCE_SANDBOX").toLowerCase();
|
|
114
185
|
if (enforceSandbox === "1" || enforceSandbox === "true") {
|
|
115
186
|
return false;
|
|
116
187
|
}
|
|
@@ -122,31 +193,89 @@ export async function executeAgentRuntime(
|
|
|
122
193
|
prompt: string,
|
|
123
194
|
config?: AgentRuntimeConfig
|
|
124
195
|
): Promise<RuntimeExecutionOutput> {
|
|
196
|
+
const command = resolveProviderCommand(provider, config?.command);
|
|
125
197
|
const commandOverride = Boolean(config?.command && config.command.trim().length > 0);
|
|
126
198
|
const effectiveRetryCount = config?.retryCount ?? (provider === "codex" ? 1 : 0);
|
|
127
|
-
const
|
|
199
|
+
const candidateArgs = [
|
|
128
200
|
...(commandOverride ? [] : providerDefaultArgs(provider, config)),
|
|
129
201
|
...(commandOverride ? [] : providerConfigArgs(provider, config)),
|
|
130
202
|
...(config?.args ?? [])
|
|
131
203
|
];
|
|
132
|
-
|
|
133
|
-
|
|
204
|
+
const mergedArgs =
|
|
205
|
+
provider === "claude_code"
|
|
206
|
+
? ensureClaudeStructuredOutputArgs(command, candidateArgs)
|
|
207
|
+
: candidateArgs;
|
|
208
|
+
let runtime = await executePromptRuntime(
|
|
209
|
+
command,
|
|
134
210
|
prompt,
|
|
135
|
-
{ ...config, args: mergedArgs, retryCount: effectiveRetryCount },
|
|
136
211
|
{
|
|
137
|
-
|
|
212
|
+
...config,
|
|
213
|
+
args: mergedArgs,
|
|
214
|
+
retryCount: effectiveRetryCount,
|
|
215
|
+
timeoutMs: config?.timeoutMs ?? defaultProviderTimeoutMs(provider)
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
provider,
|
|
219
|
+
claudeContract:
|
|
220
|
+
provider === "claude_code" ? inspectClaudeOutputContract(command, mergedArgs, commandOverride) : undefined
|
|
138
221
|
}
|
|
139
222
|
);
|
|
223
|
+
if (provider !== "claude_code") {
|
|
224
|
+
return runtime;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const maxContinuationAttempts = 2;
|
|
228
|
+
let continuationArgs = [...mergedArgs];
|
|
229
|
+
for (let continuation = 0; continuation < maxContinuationAttempts; continuation += 1) {
|
|
230
|
+
if (!runtime.ok) {
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
if (!isClaudeMaxTurnsRuntime(runtime)) {
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
const sessionId = runtime.structuredOutputDiagnostics?.claudeSessionId?.trim();
|
|
237
|
+
if (!sessionId) {
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
continuationArgs = withClaudeResumeArg(continuationArgs, sessionId);
|
|
241
|
+
runtime = await executePromptRuntime(
|
|
242
|
+
command,
|
|
243
|
+
"Continue from current session and finish all remaining assigned issue steps.",
|
|
244
|
+
{
|
|
245
|
+
...config,
|
|
246
|
+
args: continuationArgs,
|
|
247
|
+
retryCount: 0,
|
|
248
|
+
timeoutMs: config?.timeoutMs ?? defaultProviderTimeoutMs(provider)
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
provider,
|
|
252
|
+
claudeContract: inspectClaudeOutputContract(command, continuationArgs, true)
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return runtime;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function defaultProviderTimeoutMs(provider: "claude_code" | "codex") {
|
|
261
|
+
if (provider === "claude_code") {
|
|
262
|
+
return 90 * 1000;
|
|
263
|
+
}
|
|
264
|
+
return 5 * 60 * 1000;
|
|
140
265
|
}
|
|
141
266
|
|
|
142
267
|
export async function executePromptRuntime(
|
|
143
268
|
command: string,
|
|
144
269
|
prompt: string,
|
|
145
270
|
config?: AgentRuntimeConfig,
|
|
146
|
-
options?: {
|
|
271
|
+
options?: {
|
|
272
|
+
provider?: LocalProvider;
|
|
273
|
+
claudeContract?: ClaudeContractDiagnostics;
|
|
274
|
+
}
|
|
147
275
|
): Promise<RuntimeExecutionOutput> {
|
|
148
276
|
const baseArgs = [...(config?.args ?? [])];
|
|
149
277
|
const timeoutMs = config?.timeoutMs ?? 120_000;
|
|
278
|
+
const abortSignal = config?.abortSignal;
|
|
150
279
|
const interruptGraceMs = Math.max(0, config?.interruptGraceSec ?? 2) * 1000;
|
|
151
280
|
const maxAttempts = Math.max(1, Math.min(3, 1 + (config?.retryCount ?? 0)));
|
|
152
281
|
const retryBackoffMs = Math.max(100, config?.retryBackoffMs ?? 400);
|
|
@@ -159,7 +288,11 @@ export async function executePromptRuntime(
|
|
|
159
288
|
const env = providerIsolation.env;
|
|
160
289
|
const provider = options?.provider;
|
|
161
290
|
const injection = await prepareSkillInjection(provider, env);
|
|
162
|
-
const
|
|
291
|
+
const baseWithInjection = [...baseArgs, ...injection.additionalArgs];
|
|
292
|
+
const readsPromptFromStdin =
|
|
293
|
+
(provider === "claude_code" && hasCliFlagValue(baseWithInjection, "--print", "-")) ||
|
|
294
|
+
(provider === "cursor" && (baseWithInjection.includes("-p") || hasCliFlag(baseWithInjection, "--print")));
|
|
295
|
+
const args = readsPromptFromStdin ? baseWithInjection : [...baseWithInjection, prompt];
|
|
163
296
|
const attempts: RuntimeAttemptTrace[] = [];
|
|
164
297
|
let stdout = "";
|
|
165
298
|
let stderr = injection.warning ? `${injection.warning}\n` : "";
|
|
@@ -178,10 +311,12 @@ export async function executePromptRuntime(
|
|
|
178
311
|
const attemptResult = await executeSinglePromptAttempt(
|
|
179
312
|
command,
|
|
180
313
|
args,
|
|
314
|
+
readsPromptFromStdin ? prompt : undefined,
|
|
181
315
|
config?.cwd || process.cwd(),
|
|
182
316
|
env,
|
|
183
317
|
timeoutMs,
|
|
184
|
-
interruptGraceMs
|
|
318
|
+
interruptGraceMs,
|
|
319
|
+
abortSignal
|
|
185
320
|
);
|
|
186
321
|
const normalizedStderr = provider === "codex" ? stripCodexRolloutNoise(attemptResult.stderr) : attemptResult.stderr;
|
|
187
322
|
stdout += attemptResult.stdout;
|
|
@@ -205,6 +340,10 @@ export async function executePromptRuntime(
|
|
|
205
340
|
};
|
|
206
341
|
|
|
207
342
|
if (attemptResult.ok) {
|
|
343
|
+
const claudeStream = provider === "claude_code" ? parseClaudeStreamOutput(stdout) : undefined;
|
|
344
|
+
const cursorStream = provider === "cursor" ? parseCursorStreamOutput(stdout) : undefined;
|
|
345
|
+
const stdoutUsage = cursorStream?.usage ?? claudeStream?.usage ?? parseStructuredUsage(stdout);
|
|
346
|
+
const stderrUsage = parseStructuredUsage(stderr);
|
|
208
347
|
return {
|
|
209
348
|
ok: true,
|
|
210
349
|
code: attemptResult.code,
|
|
@@ -214,7 +353,28 @@ export async function executePromptRuntime(
|
|
|
214
353
|
elapsedMs: attempts.reduce((sum, item) => sum + item.elapsedMs, 0),
|
|
215
354
|
attemptCount: attempts.length,
|
|
216
355
|
attempts,
|
|
217
|
-
parsedUsage:
|
|
356
|
+
parsedUsage: stdoutUsage ?? stderrUsage,
|
|
357
|
+
structuredOutputSource: stdoutUsage ? "stdout" : stderrUsage ? "stderr" : undefined,
|
|
358
|
+
structuredOutputDiagnostics: {
|
|
359
|
+
stdoutJsonObjectCount: extractJsonObjectBlocks(stdout).length,
|
|
360
|
+
stderrJsonObjectCount: extractJsonObjectBlocks(stderr).length,
|
|
361
|
+
stderrStructuredUsageDetected: Boolean(stderrUsage),
|
|
362
|
+
stdoutBytes: Buffer.byteLength(stdout, "utf8"),
|
|
363
|
+
stderrBytes: Buffer.byteLength(stderr, "utf8"),
|
|
364
|
+
hasAnyOutput: stdout.trim().length > 0 || stderr.trim().length > 0,
|
|
365
|
+
lastStdoutLine: tailLine(stdout),
|
|
366
|
+
lastStderrLine: tailLine(stderr),
|
|
367
|
+
likelyCause: classifyStructuredOutputLikelyCause(stdout, stderr, stdoutUsage, stderrUsage),
|
|
368
|
+
...(claudeStream?.stopReason ? { claudeStopReason: claudeStream.stopReason } : {}),
|
|
369
|
+
...(claudeStream?.resultSubtype ? { claudeResultSubtype: claudeStream.resultSubtype } : {}),
|
|
370
|
+
...(claudeStream?.sessionId ? { claudeSessionId: claudeStream.sessionId } : {}),
|
|
371
|
+
...(cursorStream?.sessionId ? { cursorSessionId: cursorStream.sessionId } : {}),
|
|
372
|
+
...(cursorStream?.errorMessage ? { cursorErrorMessage: cursorStream.errorMessage } : {}),
|
|
373
|
+
...(options?.claudeContract ? { claudeContract: options.claudeContract } : {})
|
|
374
|
+
},
|
|
375
|
+
commandUsed: command,
|
|
376
|
+
argsUsed: args,
|
|
377
|
+
transcript: parseRuntimeTranscript(provider, stdout, stderr)
|
|
218
378
|
};
|
|
219
379
|
}
|
|
220
380
|
|
|
@@ -234,6 +394,10 @@ export async function executePromptRuntime(
|
|
|
234
394
|
await sleep(retryBackoffMs * attempt);
|
|
235
395
|
}
|
|
236
396
|
|
|
397
|
+
const claudeStream = provider === "claude_code" ? parseClaudeStreamOutput(stdout) : undefined;
|
|
398
|
+
const cursorStream = provider === "cursor" ? parseCursorStreamOutput(stdout) : undefined;
|
|
399
|
+
const stdoutUsage = cursorStream?.usage ?? claudeStream?.usage ?? parseStructuredUsage(stdout);
|
|
400
|
+
const stderrUsage = parseStructuredUsage(stderr);
|
|
237
401
|
return {
|
|
238
402
|
ok: false,
|
|
239
403
|
code: lastResult?.code ?? null,
|
|
@@ -244,7 +408,28 @@ export async function executePromptRuntime(
|
|
|
244
408
|
attemptCount: attempts.length,
|
|
245
409
|
attempts,
|
|
246
410
|
failureType: classifyFailure(lastResult?.timedOut ?? false, lastResult?.spawnErrorCode, lastResult?.code ?? null),
|
|
247
|
-
parsedUsage:
|
|
411
|
+
parsedUsage: stdoutUsage ?? stderrUsage,
|
|
412
|
+
structuredOutputSource: stdoutUsage ? "stdout" : stderrUsage ? "stderr" : undefined,
|
|
413
|
+
structuredOutputDiagnostics: {
|
|
414
|
+
stdoutJsonObjectCount: extractJsonObjectBlocks(stdout).length,
|
|
415
|
+
stderrJsonObjectCount: extractJsonObjectBlocks(stderr).length,
|
|
416
|
+
stderrStructuredUsageDetected: Boolean(stderrUsage),
|
|
417
|
+
stdoutBytes: Buffer.byteLength(stdout, "utf8"),
|
|
418
|
+
stderrBytes: Buffer.byteLength(stderr, "utf8"),
|
|
419
|
+
hasAnyOutput: stdout.trim().length > 0 || stderr.trim().length > 0,
|
|
420
|
+
lastStdoutLine: tailLine(stdout),
|
|
421
|
+
lastStderrLine: tailLine(stderr),
|
|
422
|
+
likelyCause: classifyStructuredOutputLikelyCause(stdout, stderr, stdoutUsage, stderrUsage),
|
|
423
|
+
...(claudeStream?.stopReason ? { claudeStopReason: claudeStream.stopReason } : {}),
|
|
424
|
+
...(claudeStream?.resultSubtype ? { claudeResultSubtype: claudeStream.resultSubtype } : {}),
|
|
425
|
+
...(claudeStream?.sessionId ? { claudeSessionId: claudeStream.sessionId } : {}),
|
|
426
|
+
...(cursorStream?.sessionId ? { cursorSessionId: cursorStream.sessionId } : {}),
|
|
427
|
+
...(cursorStream?.errorMessage ? { cursorErrorMessage: cursorStream.errorMessage } : {}),
|
|
428
|
+
...(options?.claudeContract ? { claudeContract: options.claudeContract } : {})
|
|
429
|
+
},
|
|
430
|
+
commandUsed: command,
|
|
431
|
+
argsUsed: args,
|
|
432
|
+
transcript: parseRuntimeTranscript(provider, stdout, stderr)
|
|
248
433
|
};
|
|
249
434
|
} finally {
|
|
250
435
|
await providerIsolation.cleanup();
|
|
@@ -252,6 +437,159 @@ export async function executePromptRuntime(
|
|
|
252
437
|
}
|
|
253
438
|
}
|
|
254
439
|
|
|
440
|
+
function ensureClaudeStructuredOutputArgs(command: string, args: string[]) {
|
|
441
|
+
const contract = inspectClaudeOutputContract(command, args, true);
|
|
442
|
+
if (!contract.commandLooksClaude) {
|
|
443
|
+
return args;
|
|
444
|
+
}
|
|
445
|
+
const next = [...args];
|
|
446
|
+
if (!contract.hasPromptFlag) {
|
|
447
|
+
next.push("--print", "-");
|
|
448
|
+
}
|
|
449
|
+
if (!contract.hasOutputFormatJson) {
|
|
450
|
+
next.push("--output-format", "stream-json");
|
|
451
|
+
}
|
|
452
|
+
if (contract.outputFormat === "json" && !contract.hasJsonSchema) {
|
|
453
|
+
next.push("--json-schema", CLAUDE_HEARTBEAT_OUTPUT_SCHEMA);
|
|
454
|
+
}
|
|
455
|
+
if (!contract.hasMaxTurnsFlag) {
|
|
456
|
+
next.push("--max-turns", "8");
|
|
457
|
+
}
|
|
458
|
+
if (!contract.hasVerboseFlag) {
|
|
459
|
+
next.push("--verbose");
|
|
460
|
+
}
|
|
461
|
+
if (!contract.hasDangerouslySkipPermissions) {
|
|
462
|
+
next.push("--dangerously-skip-permissions");
|
|
463
|
+
}
|
|
464
|
+
return next;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function inspectClaudeOutputContract(command: string, args: string[], commandOverride: boolean) {
|
|
468
|
+
const commandToken = command.trim().split(/[\\/]/).pop() ?? "";
|
|
469
|
+
const commandLooksClaude = /\bclaude(?:\.exe)?$/i.test(commandToken);
|
|
470
|
+
const commandWasProviderAlias = /^claude_code$/i.test(commandToken);
|
|
471
|
+
const hasPromptFlag = args.includes("-p") || hasCliFlagValue(args, "--print", "-");
|
|
472
|
+
const outputFormat = resolveCliFlagValue(args, "--output-format");
|
|
473
|
+
const hasOutputFormatJson = outputFormat === "json" || outputFormat === "stream-json";
|
|
474
|
+
const hasMaxTurnsFlag = hasCliFlag(args, "--max-turns");
|
|
475
|
+
const hasVerboseFlag = hasCliFlag(args, "--verbose");
|
|
476
|
+
const hasDangerouslySkipPermissions = hasCliFlag(args, "--dangerously-skip-permissions");
|
|
477
|
+
const hasJsonSchema = hasCliFlag(args, "--json-schema");
|
|
478
|
+
const missingRequiredArgs: string[] = [];
|
|
479
|
+
if (!hasPromptFlag) {
|
|
480
|
+
missingRequiredArgs.push("--print -");
|
|
481
|
+
}
|
|
482
|
+
if (!hasOutputFormatJson) {
|
|
483
|
+
missingRequiredArgs.push("--output-format stream-json");
|
|
484
|
+
}
|
|
485
|
+
if (outputFormat === "json" && !hasJsonSchema) {
|
|
486
|
+
missingRequiredArgs.push("--json-schema <heartbeat-schema>");
|
|
487
|
+
}
|
|
488
|
+
if (!hasMaxTurnsFlag) {
|
|
489
|
+
missingRequiredArgs.push("--max-turns 8");
|
|
490
|
+
}
|
|
491
|
+
if (!hasVerboseFlag) {
|
|
492
|
+
missingRequiredArgs.push("--verbose");
|
|
493
|
+
}
|
|
494
|
+
if (!hasDangerouslySkipPermissions) {
|
|
495
|
+
missingRequiredArgs.push("--dangerously-skip-permissions");
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
commandOverride,
|
|
499
|
+
commandLooksClaude,
|
|
500
|
+
commandWasProviderAlias,
|
|
501
|
+
hasPromptFlag,
|
|
502
|
+
hasOutputFormatJson,
|
|
503
|
+
outputFormat,
|
|
504
|
+
hasMaxTurnsFlag,
|
|
505
|
+
hasVerboseFlag,
|
|
506
|
+
hasDangerouslySkipPermissions,
|
|
507
|
+
hasJsonSchema,
|
|
508
|
+
missingRequiredArgs
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function resolveProviderCommand(provider: "claude_code" | "codex", configuredCommand: string | undefined) {
|
|
513
|
+
const trimmed = configuredCommand?.trim();
|
|
514
|
+
if (!trimmed) {
|
|
515
|
+
return pickDefaultCommand(provider);
|
|
516
|
+
}
|
|
517
|
+
// Normalize accidental provider id aliases used as command strings.
|
|
518
|
+
if (provider === "claude_code" && trimmed === "claude_code") {
|
|
519
|
+
return "claude";
|
|
520
|
+
}
|
|
521
|
+
return trimmed;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function withClaudeResumeArg(args: string[], sessionId: string) {
|
|
525
|
+
const next: string[] = [];
|
|
526
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
527
|
+
const current = args[index] ?? "";
|
|
528
|
+
if (current === "--resume") {
|
|
529
|
+
index += 1;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (current.startsWith("--resume=")) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
next.push(current);
|
|
536
|
+
}
|
|
537
|
+
next.push("--resume", sessionId);
|
|
538
|
+
return next;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function isClaudeMaxTurnsRuntime(runtime: RuntimeExecutionOutput) {
|
|
542
|
+
return (
|
|
543
|
+
runtime.structuredOutputDiagnostics?.claudeStopReason === "max_turns" ||
|
|
544
|
+
runtime.structuredOutputDiagnostics?.claudeResultSubtype === "error_max_turns"
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function hasCliFlag(args: string[], flag: string) {
|
|
549
|
+
return args.some((arg, index) => arg === flag || (arg.startsWith(`${flag}=`) && index >= 0));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function resolveCliFlagValue(args: string[], flag: string) {
|
|
553
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
554
|
+
const current = (args[index] ?? "").trim();
|
|
555
|
+
if (!current) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (current === flag) {
|
|
559
|
+
const next = (args[index + 1] ?? "").trim();
|
|
560
|
+
return next || null;
|
|
561
|
+
}
|
|
562
|
+
if (current.startsWith(`${flag}=`)) {
|
|
563
|
+
return current.slice(flag.length + 1).trim() || null;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function hasCliFlagValue(args: string[], flag: string, expectedValue: string) {
|
|
570
|
+
const expected = expectedValue.toLowerCase();
|
|
571
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
572
|
+
const current = (args[index] ?? "").trim();
|
|
573
|
+
if (!current) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (current === flag) {
|
|
577
|
+
const next = (args[index + 1] ?? "").trim().toLowerCase();
|
|
578
|
+
if (next === expected) {
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (current.startsWith(`${flag}=`)) {
|
|
584
|
+
const inlineValue = current.slice(flag.length + 1).trim().toLowerCase();
|
|
585
|
+
if (inlineValue === expected) {
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
|
|
255
593
|
const SKILLS_DIR_NAME = "skills";
|
|
256
594
|
const CLAUDE_SKILLS_DIR = ".claude/skills";
|
|
257
595
|
const SKILL_MD = "SKILL.md";
|
|
@@ -260,7 +598,17 @@ const DEFAULT_CODEX_HOME_FALLBACK = ".codex";
|
|
|
260
598
|
const CODEX_VOLATILE_STATE_ENTRIES = ["rollouts", "state.db", "data/rollouts", "data/state.db"];
|
|
261
599
|
const CODEX_ROLLOUT_NOISE_RE =
|
|
262
600
|
/^\d{4}-\d{2}-\d{2}T[^\s]+\s+ERROR\s+codex_core::rollout::list:\s+state db missing rollout path for thread\s+[a-z0-9-]+$/i;
|
|
263
|
-
|
|
601
|
+
const CLAUDE_HEARTBEAT_OUTPUT_SCHEMA = JSON.stringify({
|
|
602
|
+
type: "object",
|
|
603
|
+
additionalProperties: false,
|
|
604
|
+
required: ["summary"],
|
|
605
|
+
properties: {
|
|
606
|
+
summary: { type: "string", minLength: 1 },
|
|
607
|
+
tokenInput: { type: "number" },
|
|
608
|
+
tokenOutput: { type: "number" },
|
|
609
|
+
usdCost: { type: "number" }
|
|
610
|
+
}
|
|
611
|
+
});
|
|
264
612
|
type SkillInjectionContext = {
|
|
265
613
|
additionalArgs: string[];
|
|
266
614
|
warning?: string;
|
|
@@ -268,7 +616,7 @@ type SkillInjectionContext = {
|
|
|
268
616
|
};
|
|
269
617
|
|
|
270
618
|
async function prepareSkillInjection(
|
|
271
|
-
provider:
|
|
619
|
+
provider: LocalProvider | undefined,
|
|
272
620
|
env: NodeJS.ProcessEnv
|
|
273
621
|
): Promise<SkillInjectionContext> {
|
|
274
622
|
if (!provider) {
|
|
@@ -279,7 +627,7 @@ async function prepareSkillInjection(
|
|
|
279
627
|
if (!skillsSource) {
|
|
280
628
|
return {
|
|
281
629
|
...noSkillInjection(),
|
|
282
|
-
warning: "[
|
|
630
|
+
warning: "[bopodev] skills injection skipped: no skills directory found."
|
|
283
631
|
};
|
|
284
632
|
}
|
|
285
633
|
|
|
@@ -290,7 +638,31 @@ async function prepareSkillInjection(
|
|
|
290
638
|
} catch (error) {
|
|
291
639
|
return {
|
|
292
640
|
...noSkillInjection(),
|
|
293
|
-
warning: `[
|
|
641
|
+
warning: `[bopodev] skills injection failed for codex: ${String(error)}`
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (provider === "cursor") {
|
|
647
|
+
try {
|
|
648
|
+
await ensureSkillsInjectedAtHome(skillsSource, join(homedir(), ".cursor", "skills"));
|
|
649
|
+
return noSkillInjection();
|
|
650
|
+
} catch (error) {
|
|
651
|
+
return {
|
|
652
|
+
...noSkillInjection(),
|
|
653
|
+
warning: `[bopodev] skills injection failed for cursor: ${String(error)}`
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (provider === "opencode") {
|
|
659
|
+
try {
|
|
660
|
+
await ensureSkillsInjectedAtHome(skillsSource, join(homedir(), ".claude", "skills"));
|
|
661
|
+
return noSkillInjection();
|
|
662
|
+
} catch (error) {
|
|
663
|
+
return {
|
|
664
|
+
...noSkillInjection(),
|
|
665
|
+
warning: `[bopodev] skills injection failed for opencode: ${String(error)}`
|
|
294
666
|
};
|
|
295
667
|
}
|
|
296
668
|
}
|
|
@@ -306,7 +678,7 @@ async function prepareSkillInjection(
|
|
|
306
678
|
} catch (error) {
|
|
307
679
|
return {
|
|
308
680
|
...noSkillInjection(),
|
|
309
|
-
warning: `[
|
|
681
|
+
warning: `[bopodev] skills injection failed for claude_code: ${String(error)}`
|
|
310
682
|
};
|
|
311
683
|
}
|
|
312
684
|
}
|
|
@@ -322,10 +694,12 @@ const TRANSIENT_SPAWN_ERROR_CODES = new Set(["EAGAIN", "EMFILE", "ENFILE", "ETXT
|
|
|
322
694
|
async function executeSinglePromptAttempt(
|
|
323
695
|
command: string,
|
|
324
696
|
args: string[],
|
|
697
|
+
stdinPrompt: string | undefined,
|
|
325
698
|
cwd: string,
|
|
326
699
|
env: NodeJS.ProcessEnv,
|
|
327
700
|
timeoutMs: number,
|
|
328
|
-
interruptGraceMs: number
|
|
701
|
+
interruptGraceMs: number,
|
|
702
|
+
abortSignal?: AbortSignal
|
|
329
703
|
) {
|
|
330
704
|
const startedAt = Date.now();
|
|
331
705
|
return new Promise<{
|
|
@@ -350,9 +724,10 @@ async function executeSinglePromptAttempt(
|
|
|
350
724
|
let resolved = false;
|
|
351
725
|
let timedOut = false;
|
|
352
726
|
let forcedKill = false;
|
|
727
|
+
let abortedBySignal = false;
|
|
353
728
|
let timeoutKillTimer: NodeJS.Timeout | undefined;
|
|
354
|
-
|
|
355
|
-
|
|
729
|
+
let abortListener: (() => void) | undefined;
|
|
730
|
+
const scheduleTermination = () => {
|
|
356
731
|
child.kill("SIGTERM");
|
|
357
732
|
timeoutKillTimer = setTimeout(() => {
|
|
358
733
|
if (!resolved) {
|
|
@@ -360,7 +735,23 @@ async function executeSinglePromptAttempt(
|
|
|
360
735
|
child.kill("SIGKILL");
|
|
361
736
|
}
|
|
362
737
|
}, interruptGraceMs);
|
|
738
|
+
};
|
|
739
|
+
const timeout = setTimeout(() => {
|
|
740
|
+
timedOut = true;
|
|
741
|
+
scheduleTermination();
|
|
363
742
|
}, timeoutMs);
|
|
743
|
+
if (abortSignal) {
|
|
744
|
+
abortListener = () => {
|
|
745
|
+
abortedBySignal = true;
|
|
746
|
+
timedOut = true;
|
|
747
|
+
scheduleTermination();
|
|
748
|
+
};
|
|
749
|
+
if (abortSignal.aborted) {
|
|
750
|
+
abortListener();
|
|
751
|
+
} else {
|
|
752
|
+
abortSignal.addEventListener("abort", abortListener, { once: true });
|
|
753
|
+
}
|
|
754
|
+
}
|
|
364
755
|
|
|
365
756
|
child.stdout.on("data", (chunk) => {
|
|
366
757
|
stdout += String(chunk);
|
|
@@ -368,6 +759,10 @@ async function executeSinglePromptAttempt(
|
|
|
368
759
|
child.stderr.on("data", (chunk) => {
|
|
369
760
|
stderr += String(chunk);
|
|
370
761
|
});
|
|
762
|
+
if (stdinPrompt !== undefined) {
|
|
763
|
+
child.stdin.write(stdinPrompt);
|
|
764
|
+
child.stdin.end();
|
|
765
|
+
}
|
|
371
766
|
|
|
372
767
|
child.on("close", (code, signal) => {
|
|
373
768
|
if (resolved) {
|
|
@@ -378,11 +773,14 @@ async function executeSinglePromptAttempt(
|
|
|
378
773
|
if (timeoutKillTimer) {
|
|
379
774
|
clearTimeout(timeoutKillTimer);
|
|
380
775
|
}
|
|
776
|
+
if (abortSignal && abortListener) {
|
|
777
|
+
abortSignal.removeEventListener("abort", abortListener);
|
|
778
|
+
}
|
|
381
779
|
resolve({
|
|
382
780
|
ok: code === 0 && !timedOut,
|
|
383
781
|
code,
|
|
384
782
|
stdout,
|
|
385
|
-
stderr,
|
|
783
|
+
stderr: abortedBySignal ? [stderr, "Execution aborted by watchdog signal."].filter(Boolean).join("\n") : stderr,
|
|
386
784
|
timedOut,
|
|
387
785
|
elapsedMs: Date.now() - startedAt,
|
|
388
786
|
signal,
|
|
@@ -399,6 +797,9 @@ async function executeSinglePromptAttempt(
|
|
|
399
797
|
if (timeoutKillTimer) {
|
|
400
798
|
clearTimeout(timeoutKillTimer);
|
|
401
799
|
}
|
|
800
|
+
if (abortSignal && abortListener) {
|
|
801
|
+
abortSignal.removeEventListener("abort", abortListener);
|
|
802
|
+
}
|
|
402
803
|
const errorWithCode = error as NodeJS.ErrnoException;
|
|
403
804
|
resolve({
|
|
404
805
|
ok: false,
|
|
@@ -542,7 +943,7 @@ function resolveCodexHome(env: NodeJS.ProcessEnv) {
|
|
|
542
943
|
}
|
|
543
944
|
|
|
544
945
|
function normalizeProviderAuthEnv(
|
|
545
|
-
provider:
|
|
946
|
+
provider: LocalProvider | undefined,
|
|
546
947
|
env: NodeJS.ProcessEnv
|
|
547
948
|
) {
|
|
548
949
|
if (provider !== "codex") {
|
|
@@ -563,7 +964,7 @@ type ProviderIsolationContext = {
|
|
|
563
964
|
};
|
|
564
965
|
|
|
565
966
|
async function withProviderRuntimeIsolation(
|
|
566
|
-
provider:
|
|
967
|
+
provider: LocalProvider | undefined,
|
|
567
968
|
env: NodeJS.ProcessEnv
|
|
568
969
|
): Promise<ProviderIsolationContext> {
|
|
569
970
|
if (provider !== "codex") {
|
|
@@ -572,7 +973,7 @@ async function withProviderRuntimeIsolation(
|
|
|
572
973
|
cleanup: async () => {}
|
|
573
974
|
};
|
|
574
975
|
}
|
|
575
|
-
const forceManagedCodexHome = env
|
|
976
|
+
const forceManagedCodexHome = resolveControlPlaneEnvValue(env, "FORCE_MANAGED_CODEX_HOME") === "true";
|
|
576
977
|
if (env.CODEX_HOME?.trim() && !forceManagedCodexHome) {
|
|
577
978
|
await sanitizeCodexHomeVolatileState(env.CODEX_HOME.trim());
|
|
578
979
|
return {
|
|
@@ -582,7 +983,7 @@ async function withProviderRuntimeIsolation(
|
|
|
582
983
|
}
|
|
583
984
|
const hasApiKey = typeof env.OPENAI_API_KEY === "string" && env.OPENAI_API_KEY.trim().length > 0;
|
|
584
985
|
if (hasApiKey) {
|
|
585
|
-
const runScopedCodexHome = await mkdtemp(join(tmpdir(), "
|
|
986
|
+
const runScopedCodexHome = await mkdtemp(join(tmpdir(), "bopodev-codex-home-run-"));
|
|
586
987
|
await sanitizeCodexHomeVolatileState(runScopedCodexHome);
|
|
587
988
|
return {
|
|
588
989
|
env: {
|
|
@@ -614,10 +1015,24 @@ async function withProviderRuntimeIsolation(
|
|
|
614
1015
|
};
|
|
615
1016
|
}
|
|
616
1017
|
|
|
1018
|
+
async function ensureSkillsInjectedAtHome(skillsSourceDir: string, targetRoot: string) {
|
|
1019
|
+
await mkdir(targetRoot, { recursive: true });
|
|
1020
|
+
const entries = await readdir(skillsSourceDir, { withFileTypes: true });
|
|
1021
|
+
for (const entry of entries) {
|
|
1022
|
+
if (!entry.isDirectory()) continue;
|
|
1023
|
+
const source = join(skillsSourceDir, entry.name);
|
|
1024
|
+
if (!(await hasSkillManifest(source))) continue;
|
|
1025
|
+
const target = join(targetRoot, entry.name);
|
|
1026
|
+
const existing = await lstat(target).catch(() => null);
|
|
1027
|
+
if (existing) continue;
|
|
1028
|
+
await symlink(source, target);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
617
1032
|
function resolveManagedCodexHome(env: NodeJS.ProcessEnv) {
|
|
618
1033
|
const managedRoot = resolveManagedCodexHomeRoot(env);
|
|
619
|
-
const companyId = sanitizePathSegment(env
|
|
620
|
-
const agentId = sanitizePathSegment(env
|
|
1034
|
+
const companyId = sanitizePathSegment(resolveControlPlaneEnvValue(env, "COMPANY_ID"));
|
|
1035
|
+
const agentId = sanitizePathSegment(resolveControlPlaneEnvValue(env, "AGENT_ID"));
|
|
621
1036
|
if (companyId && agentId) {
|
|
622
1037
|
return join(managedRoot, companyId, agentId);
|
|
623
1038
|
}
|
|
@@ -629,7 +1044,7 @@ function resolveManagedCodexHomeRoot(env: NodeJS.ProcessEnv) {
|
|
|
629
1044
|
if (configuredRoot) {
|
|
630
1045
|
return configuredRoot;
|
|
631
1046
|
}
|
|
632
|
-
return join(tmpdir(), "
|
|
1047
|
+
return join(tmpdir(), "bopodev-codex-home");
|
|
633
1048
|
}
|
|
634
1049
|
|
|
635
1050
|
async function prepareManagedCodexHome(targetCodexHome: string, env: NodeJS.ProcessEnv) {
|
|
@@ -690,7 +1105,7 @@ function sanitizePathSegment(value: string | undefined) {
|
|
|
690
1105
|
}
|
|
691
1106
|
|
|
692
1107
|
async function buildClaudeSkillsAddDir(skillsSourceDir: string) {
|
|
693
|
-
const tempRoot = await mkdtemp(join(tmpdir(), "
|
|
1108
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "bopodev-skills-"));
|
|
694
1109
|
const skillsTargetDir = join(tempRoot, CLAUDE_SKILLS_DIR);
|
|
695
1110
|
await mkdir(skillsTargetDir, { recursive: true });
|
|
696
1111
|
|
|
@@ -728,6 +1143,22 @@ async function fileExists(path: string) {
|
|
|
728
1143
|
}
|
|
729
1144
|
|
|
730
1145
|
function parseStructuredUsage(stdout: string) {
|
|
1146
|
+
const whole = stdout.trim();
|
|
1147
|
+
if (whole.startsWith("{") && whole.endsWith("}")) {
|
|
1148
|
+
const parsedWhole = tryParseUsage(whole);
|
|
1149
|
+
if (parsedWhole) {
|
|
1150
|
+
return parsedWhole;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const jsonBlocks = extractJsonObjectBlocks(stdout);
|
|
1155
|
+
for (let index = jsonBlocks.length - 1; index >= 0; index -= 1) {
|
|
1156
|
+
const parsedBlock = tryParseUsage(jsonBlocks[index] ?? "");
|
|
1157
|
+
if (parsedBlock) {
|
|
1158
|
+
return parsedBlock;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
731
1162
|
const lines = stdout
|
|
732
1163
|
.split(/\r?\n/)
|
|
733
1164
|
.map((line) => line.trim())
|
|
@@ -754,9 +1185,730 @@ function parseStructuredUsage(stdout: string) {
|
|
|
754
1185
|
return undefined;
|
|
755
1186
|
}
|
|
756
1187
|
|
|
1188
|
+
function parseClaudeStreamOutput(stdout: string) {
|
|
1189
|
+
let summary = "";
|
|
1190
|
+
let tokenInput: number | undefined;
|
|
1191
|
+
let tokenOutput: number | undefined;
|
|
1192
|
+
let usdCost: number | undefined;
|
|
1193
|
+
let stopReason: string | undefined;
|
|
1194
|
+
let resultSubtype: string | undefined;
|
|
1195
|
+
let sessionId: string | undefined;
|
|
1196
|
+
const assistantTexts: string[] = [];
|
|
1197
|
+
|
|
1198
|
+
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
1199
|
+
const line = rawLine.trim();
|
|
1200
|
+
if (!line) continue;
|
|
1201
|
+
let parsed: Record<string, unknown>;
|
|
1202
|
+
try {
|
|
1203
|
+
parsed = JSON.parse(line) as Record<string, unknown>;
|
|
1204
|
+
} catch {
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
const type = typeof parsed.type === "string" ? parsed.type : "";
|
|
1208
|
+
if (!sessionId && typeof parsed.session_id === "string" && parsed.session_id.trim()) {
|
|
1209
|
+
sessionId = parsed.session_id.trim();
|
|
1210
|
+
}
|
|
1211
|
+
if (type === "assistant") {
|
|
1212
|
+
const message = parsed.message;
|
|
1213
|
+
if (message && typeof message === "object" && !Array.isArray(message)) {
|
|
1214
|
+
const content = (message as Record<string, unknown>).content;
|
|
1215
|
+
if (Array.isArray(content)) {
|
|
1216
|
+
for (const entry of content) {
|
|
1217
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
1218
|
+
const block = entry as Record<string, unknown>;
|
|
1219
|
+
if (block.type === "text" && typeof block.text === "string" && block.text.trim()) {
|
|
1220
|
+
assistantTexts.push(block.text.trim());
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
if (type !== "result") continue;
|
|
1228
|
+
if (typeof parsed.result === "string" && parsed.result.trim()) {
|
|
1229
|
+
summary = parsed.result.trim();
|
|
1230
|
+
}
|
|
1231
|
+
const usage = parsed.usage;
|
|
1232
|
+
if (usage && typeof usage === "object" && !Array.isArray(usage)) {
|
|
1233
|
+
const usageObj = usage as Record<string, unknown>;
|
|
1234
|
+
const inputTokens = toNumber(usageObj.input_tokens);
|
|
1235
|
+
const cacheReadTokens = toNumber(usageObj.cache_read_input_tokens);
|
|
1236
|
+
const outputTokens = toNumber(usageObj.output_tokens);
|
|
1237
|
+
tokenInput =
|
|
1238
|
+
inputTokens !== undefined || cacheReadTokens !== undefined
|
|
1239
|
+
? (inputTokens ?? 0) + (cacheReadTokens ?? 0)
|
|
1240
|
+
: undefined;
|
|
1241
|
+
tokenOutput = outputTokens;
|
|
1242
|
+
}
|
|
1243
|
+
usdCost = toNumber(parsed.total_cost_usd);
|
|
1244
|
+
if (typeof parsed.stop_reason === "string" && parsed.stop_reason.trim()) {
|
|
1245
|
+
stopReason = parsed.stop_reason.trim().toLowerCase();
|
|
1246
|
+
}
|
|
1247
|
+
if (typeof parsed.subtype === "string" && parsed.subtype.trim()) {
|
|
1248
|
+
resultSubtype = parsed.subtype.trim().toLowerCase();
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const resolvedSummary = summary || assistantTexts.join("\n\n").trim();
|
|
1253
|
+
if (!resolvedSummary && tokenInput === undefined && tokenOutput === undefined && usdCost === undefined) {
|
|
1254
|
+
return undefined;
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
usage: {
|
|
1258
|
+
summary: resolvedSummary || undefined,
|
|
1259
|
+
tokenInput,
|
|
1260
|
+
tokenOutput,
|
|
1261
|
+
usdCost
|
|
1262
|
+
},
|
|
1263
|
+
stopReason,
|
|
1264
|
+
resultSubtype,
|
|
1265
|
+
sessionId
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function parseCursorStreamOutput(stdout: string): CursorParsedStream | undefined {
|
|
1270
|
+
let sessionId: string | undefined;
|
|
1271
|
+
let errorMessage: string | undefined;
|
|
1272
|
+
let resultSubtype: string | undefined;
|
|
1273
|
+
let tokenInput = 0;
|
|
1274
|
+
let tokenOutput = 0;
|
|
1275
|
+
let usdCost = 0;
|
|
1276
|
+
let sawUsage = false;
|
|
1277
|
+
const assistantTexts: string[] = [];
|
|
1278
|
+
|
|
1279
|
+
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
1280
|
+
const normalized = normalizeCursorStreamLine(rawLine).line;
|
|
1281
|
+
if (!normalized) {
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
let parsed: Record<string, unknown>;
|
|
1285
|
+
try {
|
|
1286
|
+
parsed = JSON.parse(normalized) as Record<string, unknown>;
|
|
1287
|
+
} catch {
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
const foundSessionId = readCursorSessionId(parsed);
|
|
1291
|
+
if (foundSessionId) {
|
|
1292
|
+
sessionId = foundSessionId;
|
|
1293
|
+
}
|
|
1294
|
+
const type = typeof parsed.type === "string" ? parsed.type.trim().toLowerCase() : "";
|
|
1295
|
+
if (type === "assistant") {
|
|
1296
|
+
assistantTexts.push(...collectCursorAssistantText(parsed.message));
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
if (type === "result") {
|
|
1300
|
+
const usage = parsed.usage;
|
|
1301
|
+
if (usage && typeof usage === "object" && !Array.isArray(usage)) {
|
|
1302
|
+
const usageRecord = usage as Record<string, unknown>;
|
|
1303
|
+
tokenInput += toNumber(usageRecord.input_tokens) ?? toNumber(usageRecord.inputTokens) ?? 0;
|
|
1304
|
+
tokenInput +=
|
|
1305
|
+
toNumber(usageRecord.cached_input_tokens) ??
|
|
1306
|
+
toNumber(usageRecord.cachedInputTokens) ??
|
|
1307
|
+
toNumber(usageRecord.cache_read_input_tokens) ??
|
|
1308
|
+
0;
|
|
1309
|
+
tokenOutput += toNumber(usageRecord.output_tokens) ?? toNumber(usageRecord.outputTokens) ?? 0;
|
|
1310
|
+
sawUsage = true;
|
|
1311
|
+
}
|
|
1312
|
+
usdCost += toNumber(parsed.total_cost_usd) ?? toNumber(parsed.cost_usd) ?? toNumber(parsed.cost) ?? 0;
|
|
1313
|
+
if (typeof parsed.subtype === "string" && parsed.subtype.trim()) {
|
|
1314
|
+
resultSubtype = parsed.subtype.trim().toLowerCase();
|
|
1315
|
+
}
|
|
1316
|
+
const resultText = firstNonEmptyString(parsed.result);
|
|
1317
|
+
if (resultText && assistantTexts.length === 0) {
|
|
1318
|
+
assistantTexts.push(resultText);
|
|
1319
|
+
}
|
|
1320
|
+
const isError = parsed.is_error === true || resultSubtype === "error";
|
|
1321
|
+
if (isError) {
|
|
1322
|
+
const message = asCursorErrorText(parsed.error ?? parsed.message ?? parsed.result);
|
|
1323
|
+
if (message) {
|
|
1324
|
+
errorMessage = message;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
if (type === "error") {
|
|
1330
|
+
const message = asCursorErrorText(parsed.message ?? parsed.error ?? parsed.detail);
|
|
1331
|
+
if (message) {
|
|
1332
|
+
errorMessage = message;
|
|
1333
|
+
}
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
if (type === "system") {
|
|
1337
|
+
const subtype = typeof parsed.subtype === "string" ? parsed.subtype.trim().toLowerCase() : "";
|
|
1338
|
+
if (subtype === "error") {
|
|
1339
|
+
const message = asCursorErrorText(parsed.message ?? parsed.error ?? parsed.detail);
|
|
1340
|
+
if (message) {
|
|
1341
|
+
errorMessage = message;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
if (type === "text") {
|
|
1347
|
+
const part = parsed.part;
|
|
1348
|
+
if (part && typeof part === "object" && !Array.isArray(part)) {
|
|
1349
|
+
const text = firstNonEmptyString((part as Record<string, unknown>).text);
|
|
1350
|
+
if (text) {
|
|
1351
|
+
assistantTexts.push(text);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
if (type === "step_finish") {
|
|
1357
|
+
const part = parsed.part;
|
|
1358
|
+
if (!part || typeof part !== "object" || Array.isArray(part)) {
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
const tokens = (part as Record<string, unknown>).tokens;
|
|
1362
|
+
if (tokens && typeof tokens === "object" && !Array.isArray(tokens)) {
|
|
1363
|
+
const tokenRecord = tokens as Record<string, unknown>;
|
|
1364
|
+
tokenInput += toNumber(tokenRecord.input) ?? 0;
|
|
1365
|
+
tokenOutput += toNumber(tokenRecord.output) ?? 0;
|
|
1366
|
+
const cache = tokenRecord.cache;
|
|
1367
|
+
if (cache && typeof cache === "object" && !Array.isArray(cache)) {
|
|
1368
|
+
tokenInput += toNumber((cache as Record<string, unknown>).read) ?? 0;
|
|
1369
|
+
}
|
|
1370
|
+
sawUsage = true;
|
|
1371
|
+
}
|
|
1372
|
+
usdCost += toNumber((part as Record<string, unknown>).cost) ?? 0;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const summary = assistantTexts.join("\n\n").trim() || errorMessage;
|
|
1377
|
+
if (!summary && !sawUsage && usdCost <= 0) {
|
|
1378
|
+
return undefined;
|
|
1379
|
+
}
|
|
1380
|
+
return {
|
|
1381
|
+
usage: {
|
|
1382
|
+
summary: summary || undefined,
|
|
1383
|
+
tokenInput: sawUsage ? tokenInput : undefined,
|
|
1384
|
+
tokenOutput: sawUsage ? tokenOutput : undefined,
|
|
1385
|
+
usdCost: usdCost > 0 ? usdCost : undefined
|
|
1386
|
+
},
|
|
1387
|
+
sessionId,
|
|
1388
|
+
errorMessage,
|
|
1389
|
+
resultSubtype
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
function parseClaudeTranscript(stdout: string, stderr: string): RuntimeTranscriptEvent[] | undefined {
|
|
1394
|
+
const events: RuntimeTranscriptEvent[] = [];
|
|
1395
|
+
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
1396
|
+
const line = rawLine.trim();
|
|
1397
|
+
if (!line.startsWith("{") || !line.endsWith("}")) {
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
let parsed: Record<string, unknown>;
|
|
1401
|
+
try {
|
|
1402
|
+
parsed = JSON.parse(line) as Record<string, unknown>;
|
|
1403
|
+
} catch {
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
const type = typeof parsed.type === "string" ? parsed.type : "";
|
|
1407
|
+
if (type === "system") {
|
|
1408
|
+
const model = typeof parsed.model === "string" ? parsed.model : "";
|
|
1409
|
+
const subtype = typeof parsed.subtype === "string" ? parsed.subtype : undefined;
|
|
1410
|
+
events.push({
|
|
1411
|
+
kind: "system",
|
|
1412
|
+
label: subtype,
|
|
1413
|
+
text: model ? `model:${model}` : "session init"
|
|
1414
|
+
});
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
if (type === "assistant" || type === "user") {
|
|
1418
|
+
const message = parsed.message;
|
|
1419
|
+
const content =
|
|
1420
|
+
message && typeof message === "object" && !Array.isArray(message)
|
|
1421
|
+
? ((message as Record<string, unknown>).content as unknown[])
|
|
1422
|
+
: undefined;
|
|
1423
|
+
if (!Array.isArray(content)) {
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
for (const entry of content) {
|
|
1427
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
const block = entry as Record<string, unknown>;
|
|
1431
|
+
const blockType = typeof block.type === "string" ? block.type : "";
|
|
1432
|
+
if (blockType === "thinking" && typeof block.thinking === "string") {
|
|
1433
|
+
events.push({ kind: "thinking", text: clipText(block.thinking, 300) });
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
if (blockType === "text" && typeof block.text === "string") {
|
|
1437
|
+
events.push({ kind: "assistant", text: clipText(block.text, 300) });
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
if (blockType === "tool_use") {
|
|
1441
|
+
const label = typeof block.name === "string" ? block.name : "tool";
|
|
1442
|
+
const payload = block.input ? safeJson(block.input) : undefined;
|
|
1443
|
+
events.push({
|
|
1444
|
+
kind: "tool_call",
|
|
1445
|
+
label,
|
|
1446
|
+
payload: payload ? clipText(payload, 520) : undefined
|
|
1447
|
+
});
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
if (blockType === "tool_result") {
|
|
1451
|
+
const payload = typeof block.content === "string" ? block.content : safeJson(block.content);
|
|
1452
|
+
events.push({
|
|
1453
|
+
kind: "tool_result",
|
|
1454
|
+
text: payload ? clipText(payload, 320) : "tool result"
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (type === "result") {
|
|
1461
|
+
const stopReason = typeof parsed.stop_reason === "string" ? parsed.stop_reason : undefined;
|
|
1462
|
+
const result = typeof parsed.result === "string" ? parsed.result : "result event";
|
|
1463
|
+
events.push({
|
|
1464
|
+
kind: "result",
|
|
1465
|
+
label: stopReason,
|
|
1466
|
+
text: clipText(result, 320)
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
const stderrEvents = parseStderrTranscript(stderr);
|
|
1471
|
+
if (stderrEvents) {
|
|
1472
|
+
events.push(...stderrEvents.slice(0, 10));
|
|
1473
|
+
}
|
|
1474
|
+
if (events.length === 0) {
|
|
1475
|
+
return undefined;
|
|
1476
|
+
}
|
|
1477
|
+
return events.slice(0, 120);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
function parseRuntimeTranscript(
|
|
1481
|
+
provider: LocalProvider | undefined,
|
|
1482
|
+
stdout: string,
|
|
1483
|
+
stderr: string
|
|
1484
|
+
): RuntimeTranscriptEvent[] | undefined {
|
|
1485
|
+
const claudeEvents = provider === "claude_code" ? parseClaudeTranscript(stdout, stderr) : undefined;
|
|
1486
|
+
const cursorEvents = provider === "cursor" ? parseCursorTranscript(stdout, stderr) : undefined;
|
|
1487
|
+
const genericEvents = parseGenericTranscript(stdout, stderr);
|
|
1488
|
+
const providerEvents = claudeEvents ?? cursorEvents;
|
|
1489
|
+
if (providerEvents?.length && genericEvents?.length) {
|
|
1490
|
+
return [...providerEvents, ...genericEvents].slice(0, 140);
|
|
1491
|
+
}
|
|
1492
|
+
return providerEvents ?? genericEvents;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function parseCursorTranscript(stdout: string, stderr: string): RuntimeTranscriptEvent[] | undefined {
|
|
1496
|
+
const events: RuntimeTranscriptEvent[] = [];
|
|
1497
|
+
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
1498
|
+
const normalized = normalizeCursorStreamLine(rawLine).line;
|
|
1499
|
+
if (!normalized.startsWith("{") || !normalized.endsWith("}")) {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
let parsed: Record<string, unknown>;
|
|
1503
|
+
try {
|
|
1504
|
+
parsed = JSON.parse(normalized) as Record<string, unknown>;
|
|
1505
|
+
} catch {
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1508
|
+
const type = typeof parsed.type === "string" ? parsed.type.trim().toLowerCase() : "";
|
|
1509
|
+
if (type === "system") {
|
|
1510
|
+
const model = firstNonEmptyString(parsed.model);
|
|
1511
|
+
const sessionId = readCursorSessionId(parsed);
|
|
1512
|
+
const bits = [model ? `model:${model}` : "", sessionId ? `session:${sessionId}` : ""].filter(Boolean);
|
|
1513
|
+
events.push({
|
|
1514
|
+
kind: "system",
|
|
1515
|
+
label: firstNonEmptyString(parsed.subtype),
|
|
1516
|
+
text: bits.join(" ") || "session init"
|
|
1517
|
+
});
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
if (type === "assistant" || type === "user") {
|
|
1521
|
+
const kind = type === "user" ? "system" : "assistant";
|
|
1522
|
+
const content =
|
|
1523
|
+
parsed.message && typeof parsed.message === "object" && !Array.isArray(parsed.message)
|
|
1524
|
+
? (((parsed.message as Record<string, unknown>).content as unknown[]) ?? [])
|
|
1525
|
+
: [];
|
|
1526
|
+
for (const entry of content) {
|
|
1527
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
const block = entry as Record<string, unknown>;
|
|
1531
|
+
const blockType = typeof block.type === "string" ? block.type.trim().toLowerCase() : "";
|
|
1532
|
+
if ((blockType === "thinking" || blockType === "reasoning") && typeof block.text === "string") {
|
|
1533
|
+
events.push({ kind: "thinking", text: clipText(block.text, 300) });
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
if (blockType === "text" || blockType === "output_text") {
|
|
1537
|
+
const text = firstNonEmptyString(block.text);
|
|
1538
|
+
if (text) {
|
|
1539
|
+
events.push({ kind, text: clipText(text, 300) });
|
|
1540
|
+
}
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
if (blockType === "tool_call" || blockType === "tool_use") {
|
|
1544
|
+
const label = firstNonEmptyString(block.name) ?? firstNonEmptyString(block.tool_name) ?? "tool";
|
|
1545
|
+
const payload = safeJson(block.input ?? block.args ?? block.tool_call);
|
|
1546
|
+
events.push({
|
|
1547
|
+
kind: "tool_call",
|
|
1548
|
+
label,
|
|
1549
|
+
...(payload ? { payload: clipText(payload, 520) } : {})
|
|
1550
|
+
});
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
if (blockType === "tool_result") {
|
|
1554
|
+
const payload = safeJson(block.output ?? block.content ?? block.result);
|
|
1555
|
+
events.push({
|
|
1556
|
+
kind: "tool_result",
|
|
1557
|
+
label: firstNonEmptyString(block.tool_use_id) ?? undefined,
|
|
1558
|
+
text: clipText(payload ?? "tool result", 320)
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
if (type === "thinking") {
|
|
1565
|
+
const thinkingText = firstNonEmptyString(parsed.text, parsed.message);
|
|
1566
|
+
if (thinkingText) {
|
|
1567
|
+
events.push({ kind: "thinking", text: clipText(thinkingText, 300) });
|
|
1568
|
+
}
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
if (type === "tool_call") {
|
|
1572
|
+
const subtype = firstNonEmptyString(parsed.subtype);
|
|
1573
|
+
const toolCall = parsed.tool_call;
|
|
1574
|
+
if (toolCall && typeof toolCall === "object" && !Array.isArray(toolCall)) {
|
|
1575
|
+
const [toolName, toolValue] = Object.entries(toolCall as Record<string, unknown>)[0] ?? [];
|
|
1576
|
+
if (subtype === "completed") {
|
|
1577
|
+
events.push({
|
|
1578
|
+
kind: "tool_result",
|
|
1579
|
+
label: firstNonEmptyString(parsed.call_id) ?? toolName ?? undefined,
|
|
1580
|
+
text: clipText(safeJson(toolValue) ?? "tool result", 320)
|
|
1581
|
+
});
|
|
1582
|
+
} else {
|
|
1583
|
+
const payload =
|
|
1584
|
+
toolValue && typeof toolValue === "object" && !Array.isArray(toolValue)
|
|
1585
|
+
? safeJson((toolValue as Record<string, unknown>).args ?? toolValue)
|
|
1586
|
+
: safeJson(toolValue);
|
|
1587
|
+
events.push({
|
|
1588
|
+
kind: "tool_call",
|
|
1589
|
+
label: toolName ?? firstNonEmptyString(parsed.call_id) ?? "tool",
|
|
1590
|
+
...(payload ? { payload: clipText(payload, 520) } : {})
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
if (type === "result" || type === "error") {
|
|
1597
|
+
const label = firstNonEmptyString(parsed.subtype);
|
|
1598
|
+
const text =
|
|
1599
|
+
firstNonEmptyString(parsed.result, parsed.message, parsed.error, parsed.detail) ?? `${type} event`;
|
|
1600
|
+
events.push({
|
|
1601
|
+
kind: type === "error" ? "stderr" : "result",
|
|
1602
|
+
...(label ? { label } : {}),
|
|
1603
|
+
text: clipText(text, 320)
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
const stderrEvents = parseStderrTranscript(stderr);
|
|
1608
|
+
if (stderrEvents) {
|
|
1609
|
+
events.push(...stderrEvents.slice(0, 10));
|
|
1610
|
+
}
|
|
1611
|
+
if (events.length === 0) {
|
|
1612
|
+
return undefined;
|
|
1613
|
+
}
|
|
1614
|
+
return events.slice(0, 120);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
function parseGenericTranscript(stdout: string, stderr: string): RuntimeTranscriptEvent[] | undefined {
|
|
1618
|
+
const events: RuntimeTranscriptEvent[] = [];
|
|
1619
|
+
let lastToolEventIndex = -1;
|
|
1620
|
+
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
1621
|
+
const line = rawLine.trim();
|
|
1622
|
+
if (!line) {
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
const fromJson = parseGenericTranscriptJsonLine(line);
|
|
1626
|
+
if (fromJson) {
|
|
1627
|
+
events.push(fromJson);
|
|
1628
|
+
lastToolEventIndex =
|
|
1629
|
+
fromJson.kind === "tool_call" || fromJson.kind === "tool_result" ? events.length - 1 : lastToolEventIndex;
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
const fromTagged = parseTaggedTranscriptLine(line);
|
|
1633
|
+
if (fromTagged) {
|
|
1634
|
+
events.push(fromTagged);
|
|
1635
|
+
lastToolEventIndex =
|
|
1636
|
+
fromTagged.kind === "tool_call" || fromTagged.kind === "tool_result" ? events.length - 1 : lastToolEventIndex;
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
if (lastToolEventIndex >= 0 && looksLikeToolPayloadLine(line)) {
|
|
1640
|
+
const current = events[lastToolEventIndex];
|
|
1641
|
+
if (!current) {
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1644
|
+
const nextPayload = [current.payload, line].filter(Boolean).join("\n");
|
|
1645
|
+
events[lastToolEventIndex] = {
|
|
1646
|
+
...current,
|
|
1647
|
+
payload: clipText(nextPayload, 520)
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
const stderrEvents = parseStderrTranscript(stderr);
|
|
1652
|
+
if (stderrEvents) {
|
|
1653
|
+
events.push(...stderrEvents.slice(0, 10));
|
|
1654
|
+
}
|
|
1655
|
+
if (events.length === 0) {
|
|
1656
|
+
return undefined;
|
|
1657
|
+
}
|
|
1658
|
+
return events.slice(0, 120);
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function parseGenericTranscriptJsonLine(line: string): RuntimeTranscriptEvent | undefined {
|
|
1662
|
+
if (!line.startsWith("{") || !line.endsWith("}")) {
|
|
1663
|
+
return undefined;
|
|
1664
|
+
}
|
|
1665
|
+
let parsed: Record<string, unknown>;
|
|
1666
|
+
try {
|
|
1667
|
+
parsed = JSON.parse(line) as Record<string, unknown>;
|
|
1668
|
+
} catch {
|
|
1669
|
+
return undefined;
|
|
1670
|
+
}
|
|
1671
|
+
const kind = normalizeTranscriptKind(parsed.type ?? parsed.kind ?? parsed.event);
|
|
1672
|
+
if (!kind) {
|
|
1673
|
+
return undefined;
|
|
1674
|
+
}
|
|
1675
|
+
const label = pickString(parsed.subtype, parsed.name, parsed.tool, parsed.tool_name, parsed.status);
|
|
1676
|
+
const text =
|
|
1677
|
+
pickString(
|
|
1678
|
+
parsed.message,
|
|
1679
|
+
parsed.text,
|
|
1680
|
+
parsed.result,
|
|
1681
|
+
parsed.summary,
|
|
1682
|
+
parsed.content,
|
|
1683
|
+
parsed.detail,
|
|
1684
|
+
parsed.command
|
|
1685
|
+
) ?? `${kind} event`;
|
|
1686
|
+
const payload = safeJson(parsed.input ?? parsed.arguments ?? parsed.params ?? parsed.output ?? parsed.data);
|
|
1687
|
+
return {
|
|
1688
|
+
kind,
|
|
1689
|
+
...(label ? { label: clipText(label, 100) } : {}),
|
|
1690
|
+
text: clipText(text, 300),
|
|
1691
|
+
...(payload ? { payload: clipText(payload, 520) } : {})
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function parseTaggedTranscriptLine(line: string): RuntimeTranscriptEvent | undefined {
|
|
1696
|
+
const timestampPrefix = /^\d{2}:\d{2}:\d{2}\s+/;
|
|
1697
|
+
const withoutTimestamp = timestampPrefix.test(line) ? line.replace(timestampPrefix, "") : line;
|
|
1698
|
+
const match = /^(system|assistant|thinking|tool_call|tool_result|result|stderr)\s*(.*)$/i.exec(
|
|
1699
|
+
withoutTimestamp
|
|
1700
|
+
);
|
|
1701
|
+
if (!match) {
|
|
1702
|
+
return undefined;
|
|
1703
|
+
}
|
|
1704
|
+
const kind = normalizeTranscriptKind(match[1]);
|
|
1705
|
+
if (!kind) {
|
|
1706
|
+
return undefined;
|
|
1707
|
+
}
|
|
1708
|
+
const rest = match[2]?.trim();
|
|
1709
|
+
return {
|
|
1710
|
+
kind,
|
|
1711
|
+
text: clipText(rest || `${kind} event`, 300),
|
|
1712
|
+
...(rest && (kind === "tool_call" || kind === "tool_result") ? { label: clipText(rest, 120) } : {})
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function normalizeTranscriptKind(value: unknown): RuntimeTranscriptEvent["kind"] | undefined {
|
|
1717
|
+
if (typeof value !== "string") {
|
|
1718
|
+
return undefined;
|
|
1719
|
+
}
|
|
1720
|
+
const normalized = value.trim().toLowerCase();
|
|
1721
|
+
if (
|
|
1722
|
+
normalized === "system" ||
|
|
1723
|
+
normalized === "assistant" ||
|
|
1724
|
+
normalized === "thinking" ||
|
|
1725
|
+
normalized === "tool_call" ||
|
|
1726
|
+
normalized === "tool_result" ||
|
|
1727
|
+
normalized === "result" ||
|
|
1728
|
+
normalized === "stderr"
|
|
1729
|
+
) {
|
|
1730
|
+
return normalized;
|
|
1731
|
+
}
|
|
1732
|
+
return undefined;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function pickString(...values: unknown[]) {
|
|
1736
|
+
for (const value of values) {
|
|
1737
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1738
|
+
return value.trim();
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
return undefined;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
function looksLikeToolPayloadLine(line: string) {
|
|
1745
|
+
return (
|
|
1746
|
+
line.startsWith("{") ||
|
|
1747
|
+
line.startsWith("}") ||
|
|
1748
|
+
line.startsWith("\"") ||
|
|
1749
|
+
/^[a-zA-Z_][a-zA-Z0-9_]*\s*:/.test(line)
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function parseStderrTranscript(stderr: string): RuntimeTranscriptEvent[] | undefined {
|
|
1754
|
+
const lines = stderr
|
|
1755
|
+
.split(/\r?\n/)
|
|
1756
|
+
.map((line) => line.trim())
|
|
1757
|
+
.filter(Boolean)
|
|
1758
|
+
.slice(0, 40);
|
|
1759
|
+
if (lines.length === 0) {
|
|
1760
|
+
return undefined;
|
|
1761
|
+
}
|
|
1762
|
+
return lines.map((line) => ({
|
|
1763
|
+
kind: "stderr",
|
|
1764
|
+
text: clipText(line, 300)
|
|
1765
|
+
}));
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
function safeJson(value: unknown) {
|
|
1769
|
+
try {
|
|
1770
|
+
return JSON.stringify(value);
|
|
1771
|
+
} catch {
|
|
1772
|
+
return undefined;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
function normalizeCursorStreamLine(rawLine: string) {
|
|
1777
|
+
const trimmed = rawLine.trim();
|
|
1778
|
+
if (trimmed.startsWith("stdout{") || trimmed.startsWith("stderr{")) {
|
|
1779
|
+
return { line: trimmed.slice(6), stream: trimmed.slice(0, 6) as "stdout" | "stderr" };
|
|
1780
|
+
}
|
|
1781
|
+
return { line: trimmed, stream: undefined };
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
function readCursorSessionId(event: Record<string, unknown>) {
|
|
1785
|
+
return firstNonEmptyString(event.session_id, event.sessionId, event.sessionID);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
function collectCursorAssistantText(message: unknown) {
|
|
1789
|
+
if (typeof message === "string") {
|
|
1790
|
+
return message.trim() ? [message.trim()] : [];
|
|
1791
|
+
}
|
|
1792
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) {
|
|
1793
|
+
return [];
|
|
1794
|
+
}
|
|
1795
|
+
const record = message as Record<string, unknown>;
|
|
1796
|
+
const lines: string[] = [];
|
|
1797
|
+
const directText = firstNonEmptyString(record.text);
|
|
1798
|
+
if (directText) {
|
|
1799
|
+
lines.push(directText);
|
|
1800
|
+
}
|
|
1801
|
+
const content = Array.isArray(record.content) ? record.content : [];
|
|
1802
|
+
for (const entry of content) {
|
|
1803
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
1804
|
+
continue;
|
|
1805
|
+
}
|
|
1806
|
+
const block = entry as Record<string, unknown>;
|
|
1807
|
+
const type = typeof block.type === "string" ? block.type.trim().toLowerCase() : "";
|
|
1808
|
+
if (type !== "output_text" && type !== "text") {
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
const text = firstNonEmptyString(block.text);
|
|
1812
|
+
if (text) {
|
|
1813
|
+
lines.push(text);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
return lines;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
function asCursorErrorText(value: unknown) {
|
|
1820
|
+
if (typeof value === "string" && value.trim()) {
|
|
1821
|
+
return value.trim();
|
|
1822
|
+
}
|
|
1823
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1824
|
+
return "";
|
|
1825
|
+
}
|
|
1826
|
+
const record = value as Record<string, unknown>;
|
|
1827
|
+
const direct = firstNonEmptyString(record.message, record.error, record.code, record.detail);
|
|
1828
|
+
if (direct) {
|
|
1829
|
+
return direct;
|
|
1830
|
+
}
|
|
1831
|
+
return safeJson(record) ?? "";
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function firstNonEmptyString(...values: unknown[]) {
|
|
1835
|
+
for (const value of values) {
|
|
1836
|
+
if (typeof value === "string" && value.trim()) {
|
|
1837
|
+
return value.trim();
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
return undefined;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function clipText(text: string, max = 220) {
|
|
1844
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
1845
|
+
if (normalized.length <= max) {
|
|
1846
|
+
return normalized;
|
|
1847
|
+
}
|
|
1848
|
+
return `${normalized.slice(0, max)}...`;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function extractJsonObjectBlocks(text: string) {
|
|
1852
|
+
const blocks: string[] = [];
|
|
1853
|
+
let depth = 0;
|
|
1854
|
+
let startIndex = -1;
|
|
1855
|
+
let inString = false;
|
|
1856
|
+
let escaped = false;
|
|
1857
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1858
|
+
const char = text[index];
|
|
1859
|
+
if (inString) {
|
|
1860
|
+
if (escaped) {
|
|
1861
|
+
escaped = false;
|
|
1862
|
+
} else if (char === "\\") {
|
|
1863
|
+
escaped = true;
|
|
1864
|
+
} else if (char === "\"") {
|
|
1865
|
+
inString = false;
|
|
1866
|
+
}
|
|
1867
|
+
continue;
|
|
1868
|
+
}
|
|
1869
|
+
if (char === "\"") {
|
|
1870
|
+
inString = true;
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
if (char === "{") {
|
|
1874
|
+
if (depth === 0) {
|
|
1875
|
+
startIndex = index;
|
|
1876
|
+
}
|
|
1877
|
+
depth += 1;
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
if (char === "}") {
|
|
1881
|
+
if (depth === 0) {
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
depth -= 1;
|
|
1885
|
+
if (depth === 0 && startIndex >= 0) {
|
|
1886
|
+
blocks.push(text.slice(startIndex, index + 1));
|
|
1887
|
+
startIndex = -1;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
return blocks;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
757
1894
|
function tryParseUsage(candidate: string) {
|
|
758
1895
|
try {
|
|
759
1896
|
const parsed = JSON.parse(candidate) as Record<string, unknown>;
|
|
1897
|
+
const direct = toUsageRecord(parsed);
|
|
1898
|
+
if (direct) {
|
|
1899
|
+
return direct;
|
|
1900
|
+
}
|
|
1901
|
+
const nested = findNestedUsage(parsed);
|
|
1902
|
+
if (nested) {
|
|
1903
|
+
return nested;
|
|
1904
|
+
}
|
|
1905
|
+
return undefined;
|
|
1906
|
+
} catch {
|
|
1907
|
+
return undefined;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
function toUsageRecord(parsed: Record<string, unknown>) {
|
|
760
1912
|
const tokenInput = toNumber(parsed.tokenInput);
|
|
761
1913
|
const tokenOutput = toNumber(parsed.tokenOutput);
|
|
762
1914
|
const usdCost = toNumber(parsed.usdCost);
|
|
@@ -773,9 +1925,45 @@ function tryParseUsage(candidate: string) {
|
|
|
773
1925
|
return undefined;
|
|
774
1926
|
}
|
|
775
1927
|
return { tokenInput, tokenOutput, usdCost, summary };
|
|
776
|
-
|
|
777
|
-
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
function findNestedUsage(parsed: Record<string, unknown>) {
|
|
1931
|
+
const queue: unknown[] = Object.values(parsed);
|
|
1932
|
+
while (queue.length > 0) {
|
|
1933
|
+
const current = queue.shift();
|
|
1934
|
+
if (!current) {
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
if (typeof current === "string") {
|
|
1938
|
+
const trimmed = current.trim();
|
|
1939
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
1940
|
+
try {
|
|
1941
|
+
const parsedString = JSON.parse(trimmed) as Record<string, unknown>;
|
|
1942
|
+
const usage = toUsageRecord(parsedString);
|
|
1943
|
+
if (usage) {
|
|
1944
|
+
return usage;
|
|
1945
|
+
}
|
|
1946
|
+
queue.push(...Object.values(parsedString));
|
|
1947
|
+
} catch {
|
|
1948
|
+
// ignore malformed nested JSON
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
continue;
|
|
1952
|
+
}
|
|
1953
|
+
if (Array.isArray(current)) {
|
|
1954
|
+
queue.push(...current);
|
|
1955
|
+
continue;
|
|
1956
|
+
}
|
|
1957
|
+
if (typeof current === "object") {
|
|
1958
|
+
const objectValue = current as Record<string, unknown>;
|
|
1959
|
+
const usage = toUsageRecord(objectValue);
|
|
1960
|
+
if (usage) {
|
|
1961
|
+
return usage;
|
|
1962
|
+
}
|
|
1963
|
+
queue.push(...Object.values(objectValue));
|
|
1964
|
+
}
|
|
778
1965
|
}
|
|
1966
|
+
return undefined;
|
|
779
1967
|
}
|
|
780
1968
|
|
|
781
1969
|
function isPromptTemplateUsage(
|
|
@@ -812,3 +2000,32 @@ function toNumber(value: unknown) {
|
|
|
812
2000
|
}
|
|
813
2001
|
return undefined;
|
|
814
2002
|
}
|
|
2003
|
+
|
|
2004
|
+
function tailLine(value: string) {
|
|
2005
|
+
const lines = value
|
|
2006
|
+
.split(/\r?\n/)
|
|
2007
|
+
.map((line) => line.trim())
|
|
2008
|
+
.filter(Boolean);
|
|
2009
|
+
return lines.length > 0 ? lines[lines.length - 1] : undefined;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
function classifyStructuredOutputLikelyCause(
|
|
2013
|
+
stdout: string,
|
|
2014
|
+
stderr: string,
|
|
2015
|
+
stdoutUsage: { summary?: string } | undefined,
|
|
2016
|
+
stderrUsage: { summary?: string } | undefined
|
|
2017
|
+
) {
|
|
2018
|
+
const hasStdout = stdout.trim().length > 0;
|
|
2019
|
+
const hasStderr = stderr.trim().length > 0;
|
|
2020
|
+
if (!hasStdout && !hasStderr) {
|
|
2021
|
+
return "no_output_from_runtime" as const;
|
|
2022
|
+
}
|
|
2023
|
+
if (!stdoutUsage && stderrUsage) {
|
|
2024
|
+
return "json_on_stderr_only" as const;
|
|
2025
|
+
}
|
|
2026
|
+
const jsonLike = /[\{\}\[\]\"]/m.test(stdout) || /[\{\}\[\]\"]/m.test(stderr);
|
|
2027
|
+
if (jsonLike) {
|
|
2028
|
+
return "schema_or_shape_mismatch" as const;
|
|
2029
|
+
}
|
|
2030
|
+
return "json_missing" as const;
|
|
2031
|
+
}
|