@koreanhole/opencodex 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,652 @@
1
+ import { spawn as nodeSpawn, type ChildProcess } from "node:child_process";
2
+ import { closeSync, openSync } from "node:fs";
3
+ import { stat } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { resolve } from "node:path";
6
+ import type { AdapterEvent, IncomingMeta } from "../../../types";
7
+ import { redactSecretString } from "../../../lib/redact";
8
+ import { commandInvocation } from "../../../lib/win-exec";
9
+ import { CodingAgentProtocolError, readJsonLines } from "../protocol";
10
+ import type { CliAgentTurnDeps } from "../types";
11
+ import { buildClineAcpEnv } from "./acp";
12
+ import type { ClineSelection } from "./settings";
13
+ import {
14
+ classifyClineFailure,
15
+ ClineStreamProtocolError,
16
+ createClineStreamState,
17
+ mapClineMessage,
18
+ } from "./stream";
19
+
20
+ const HELP_TIMEOUT_MS = 15_000;
21
+ const PREFLIGHT_TIMEOUT_MS = 15_000;
22
+ const MAX_HELP_BYTES = 256 * 1024;
23
+ const MAX_PREFLIGHT_STDOUT_BYTES = 2 * 1024 * 1024;
24
+ const MAX_STDERR_BYTES = 8 * 1024;
25
+ const DEFAULT_TIMEOUT_MS = 300_000;
26
+ const DEFAULT_KILL_GRACE_MS = 2_000;
27
+ const DEFAULT_TOOL_BATCH_IDLE_MS = 250;
28
+ const MAX_TOOL_CALLS = 16;
29
+
30
+ export class ClineJsonError extends Error {
31
+ constructor(
32
+ message: string,
33
+ readonly status: number,
34
+ readonly errorType: string,
35
+ readonly code: string,
36
+ readonly retryable = false,
37
+ ) {
38
+ super(message);
39
+ this.name = "ClineJsonError";
40
+ }
41
+ }
42
+
43
+ export interface ClineJsonCapabilities {
44
+ json: boolean;
45
+ toolCatalogJson: boolean;
46
+ providerFlag?: "--provider";
47
+ modelFlag?: "--model" | "--modelid";
48
+ systemFlag?: "--system";
49
+ }
50
+
51
+ export interface ClineJsonTurnInput {
52
+ binary: string;
53
+ args: readonly string[];
54
+ env: Record<string, string>;
55
+ cwd: string;
56
+ promptFile: string;
57
+ toolNames: ReadonlyMap<string, string>;
58
+ maxToolCalls: number;
59
+ requireToolCall: boolean;
60
+ incoming: IncomingMeta;
61
+ }
62
+
63
+ export interface ClineEnabledTool {
64
+ id: string;
65
+ kind: "builtin";
66
+ }
67
+
68
+ export interface ClineToolPreflightResult {
69
+ ok: boolean;
70
+ detail?: string;
71
+ enabledTools?: readonly ClineEnabledTool[];
72
+ }
73
+
74
+ interface ProbeResult {
75
+ exitCode: number;
76
+ stdout: string;
77
+ stderr: string;
78
+ }
79
+
80
+ interface CapabilityCacheEntry {
81
+ mtimeMs: number;
82
+ size: number;
83
+ capabilities: ClineJsonCapabilities;
84
+ }
85
+
86
+ const CAPABILITY_CACHE = new Map<string, CapabilityCacheEntry>();
87
+
88
+ export function resetClineJsonCapabilityCacheForTests(): void {
89
+ CAPABILITY_CACHE.clear();
90
+ }
91
+
92
+ function optionPresent(text: string, option: string): boolean {
93
+ const escaped = option.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
94
+ return new RegExp("(?:^|\\s)" + escaped + "(?:\\s|,|=|\\[|<|$)").test(text);
95
+ }
96
+
97
+ export function parseClineJsonHelp(
98
+ text: string,
99
+ scope: "turn" | "toolCatalog" = "turn",
100
+ ): ClineJsonCapabilities {
101
+ const json = optionPresent(text, "--json");
102
+ if (scope === "toolCatalog") {
103
+ return { json: true, toolCatalogJson: json };
104
+ }
105
+ const providerFlag = optionPresent(text, "--provider") ? "--provider" as const : undefined;
106
+ const modelFlag = optionPresent(text, "--model")
107
+ ? "--model" as const
108
+ : optionPresent(text, "--modelid")
109
+ ? "--modelid" as const
110
+ : undefined;
111
+ const systemFlag = optionPresent(text, "--system") ? "--system" as const : undefined;
112
+ return {
113
+ json,
114
+ toolCatalogJson: false,
115
+ ...(providerFlag ? { providerFlag } : {}),
116
+ ...(modelFlag ? { modelFlag } : {}),
117
+ ...(systemFlag ? { systemFlag } : {}),
118
+ };
119
+ }
120
+
121
+ function redactProbeDetail(value: string): string {
122
+ return redactSecretString(value.trim()).slice(0, MAX_STDERR_BYTES);
123
+ }
124
+
125
+ function runProbeProcess(
126
+ binary: string,
127
+ args: readonly string[],
128
+ env: Record<string, string>,
129
+ cwd: string,
130
+ deps: CliAgentTurnDeps,
131
+ timeoutMs: number,
132
+ maxStdoutBytes: number,
133
+ ): Promise<ProbeResult> {
134
+ return new Promise((resolvePromise, reject) => {
135
+ const invocation = commandInvocation(binary, args, deps.platform ?? process.platform, { env });
136
+ const spawnFn = deps.spawn ?? (nodeSpawn as unknown as NonNullable<CliAgentTurnDeps["spawn"]>);
137
+ const stdout: Buffer[] = [];
138
+ const stderr: Buffer[] = [];
139
+ let stdoutBytes = 0;
140
+ let stderrBytes = 0;
141
+ let settled = false;
142
+ let child: ChildProcess;
143
+ const finish = (fn: () => void): void => {
144
+ if (settled) return;
145
+ settled = true;
146
+ clearTimeout(timer);
147
+ fn();
148
+ };
149
+ try {
150
+ child = spawnFn(invocation.file, invocation.args, {
151
+ ...invocation.options,
152
+ cwd,
153
+ env,
154
+ stdio: ["ignore", "pipe", "pipe"],
155
+ windowsHide: true,
156
+ });
157
+ } catch (error) {
158
+ reject(error);
159
+ return;
160
+ }
161
+ const timer = setTimeout(() => {
162
+ try { child.kill("SIGKILL"); } catch { /* already stopped */ }
163
+ finish(() => reject(new Error("Cline help probe timed out.")));
164
+ }, timeoutMs);
165
+ child.stdout?.on("data", (chunk: Buffer) => {
166
+ if (stdoutBytes >= maxStdoutBytes) return;
167
+ const admitted = chunk.subarray(0, maxStdoutBytes - stdoutBytes);
168
+ stdout.push(admitted);
169
+ stdoutBytes += admitted.byteLength;
170
+ });
171
+ child.stderr?.on("data", (chunk: Buffer) => {
172
+ if (stderrBytes >= MAX_STDERR_BYTES) return;
173
+ const admitted = chunk.subarray(0, MAX_STDERR_BYTES - stderrBytes);
174
+ stderr.push(admitted);
175
+ stderrBytes += admitted.byteLength;
176
+ });
177
+ child.once("error", error => finish(() => reject(error)));
178
+ child.once("close", code => finish(() => resolvePromise({
179
+ exitCode: code ?? 1,
180
+ stdout: Buffer.concat(stdout).toString("utf8"),
181
+ stderr: Buffer.concat(stderr).toString("utf8"),
182
+ })));
183
+ });
184
+ }
185
+
186
+ function probeFailure(code: string, detail: string): ClineJsonError {
187
+ return new ClineJsonError(
188
+ "Cline capability probe failed: " + (redactProbeDetail(detail) || "no diagnostic output"),
189
+ 502,
190
+ "upstream_error",
191
+ code,
192
+ false,
193
+ );
194
+ }
195
+
196
+ export async function probeClineJsonCapabilities(
197
+ binary: string,
198
+ env: Record<string, string> = buildClineAcpEnv(),
199
+ deps: CliAgentTurnDeps = {},
200
+ ): Promise<ClineJsonCapabilities> {
201
+ const path = resolve(binary);
202
+ let fingerprint: { mtimeMs: number; size: number } | undefined;
203
+ try {
204
+ const value = await stat(path);
205
+ fingerprint = { mtimeMs: value.mtimeMs, size: value.size };
206
+ } catch {
207
+ fingerprint = undefined;
208
+ }
209
+ if (fingerprint) {
210
+ const cached = CAPABILITY_CACHE.get(path);
211
+ if (cached && cached.mtimeMs === fingerprint.mtimeMs && cached.size === fingerprint.size) {
212
+ return { ...cached.capabilities };
213
+ }
214
+ }
215
+
216
+ const timeoutMs = Math.min(deps.timeoutMs ?? HELP_TIMEOUT_MS, HELP_TIMEOUT_MS);
217
+ let top: ProbeResult;
218
+ try {
219
+ top = await runProbeProcess(binary, ["--help"], env, homedir(), deps, timeoutMs, MAX_HELP_BYTES);
220
+ } catch (error) {
221
+ throw probeFailure("cline_help_probe_failed", error instanceof Error ? error.message : String(error));
222
+ }
223
+ if (top.exitCode !== 0) throw probeFailure("cline_help_probe_failed", top.stderr || top.stdout);
224
+ const turn = parseClineJsonHelp(top.stdout + "\n" + top.stderr);
225
+ if (!turn.json) {
226
+ throw new ClineJsonError(
227
+ "The installed Cline CLI does not advertise --json. Upgrade Cline or set CLINE_BIN_PATH to a compatible binary.",
228
+ 400,
229
+ "invalid_request_error",
230
+ "cline_json_unsupported",
231
+ false,
232
+ );
233
+ }
234
+
235
+ let tools: ProbeResult;
236
+ try {
237
+ tools = await runProbeProcess(binary, ["config", "tools", "--help"], env, homedir(), deps, timeoutMs, MAX_HELP_BYTES);
238
+ } catch (error) {
239
+ throw probeFailure("cline_tool_catalog_probe_failed", error instanceof Error ? error.message : String(error));
240
+ }
241
+ if (tools.exitCode !== 0) throw probeFailure("cline_tool_catalog_probe_failed", tools.stderr || tools.stdout);
242
+ const toolCatalog = parseClineJsonHelp(tools.stdout + "\n" + tools.stderr, "toolCatalog");
243
+ if (!toolCatalog.toolCatalogJson) {
244
+ throw new ClineJsonError(
245
+ "The installed Cline CLI does not advertise config tools --json; the tool preflight cannot run safely.",
246
+ 400,
247
+ "invalid_request_error",
248
+ "cline_tool_catalog_unsupported",
249
+ false,
250
+ );
251
+ }
252
+
253
+ const capabilities: ClineJsonCapabilities = {
254
+ ...turn,
255
+ toolCatalogJson: true,
256
+ };
257
+ if (fingerprint) CAPABILITY_CACHE.set(path, { ...fingerprint, capabilities: { ...capabilities } });
258
+ return capabilities;
259
+ }
260
+
261
+ export function buildClineJsonArgs(input: {
262
+ capabilities: ClineJsonCapabilities;
263
+ modelId: string;
264
+ selection?: ClineSelection;
265
+ systemPrompt?: string;
266
+ prompt: string;
267
+ }): string[] {
268
+ if (!input.capabilities.json) {
269
+ throw new ClineJsonError("The installed Cline CLI does not advertise --json.", 400, "invalid_request_error", "cline_json_unsupported", false);
270
+ }
271
+ const args = ["--json"];
272
+ if (input.capabilities.providerFlag && input.selection?.providerId) {
273
+ args.push(input.capabilities.providerFlag, input.selection.providerId);
274
+ }
275
+ if (input.modelId !== "default") {
276
+ if (!input.capabilities.modelFlag) {
277
+ throw new ClineJsonError(
278
+ 'The installed Cline CLI has no advertised model selector for "' + input.modelId + '".',
279
+ 400,
280
+ "invalid_request_error",
281
+ "cline_model_flag_unsupported",
282
+ false,
283
+ );
284
+ }
285
+ args.push(input.capabilities.modelFlag, input.modelId);
286
+ }
287
+ if (input.capabilities.systemFlag && input.systemPrompt) {
288
+ args.push(input.capabilities.systemFlag, input.systemPrompt);
289
+ }
290
+ args.push(input.prompt);
291
+ return args;
292
+ }
293
+
294
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
295
+ return value && typeof value === "object" && !Array.isArray(value)
296
+ ? value as Record<string, unknown>
297
+ : undefined;
298
+ }
299
+
300
+ function nonEmpty(value: unknown): string | undefined {
301
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
302
+ }
303
+
304
+ export function validateClineToolCatalog(parsed: unknown): ClineToolPreflightResult {
305
+ if (!Array.isArray(parsed)) return { ok: false, detail: "Cline tool preflight returned a non-array catalog." };
306
+ const enabledTools: ClineEnabledTool[] = [];
307
+ let pluginDetail: string | undefined;
308
+ for (const item of parsed) {
309
+ const record = asRecord(item);
310
+ if (!record) continue;
311
+ if (record.type === "plugin") {
312
+ if (record.enabled === true) {
313
+ pluginDetail ??= "Cline enabled plugin tool " + String(record.name ?? "unknown") + ".";
314
+ }
315
+ continue;
316
+ }
317
+ if (record.defaultEnabled !== true) continue;
318
+ const id = nonEmpty(record.id);
319
+ if (!id) return { ok: false, detail: "Cline enabled a built-in tool without an id." };
320
+ enabledTools.push({ id, kind: "builtin" });
321
+ }
322
+ if (pluginDetail) return { ok: false, detail: pluginDetail };
323
+ if (enabledTools.length > 0) {
324
+ return {
325
+ ok: false,
326
+ detail: "Cline enabled built-in tool " + enabledTools[0]!.id + ".",
327
+ enabledTools,
328
+ };
329
+ }
330
+ return { ok: true };
331
+ }
332
+
333
+ export async function preflightClineTools(input: {
334
+ binary: string;
335
+ env: Record<string, string>;
336
+ cwd: string;
337
+ capabilities: ClineJsonCapabilities;
338
+ deps?: CliAgentTurnDeps;
339
+ timeoutMs?: number;
340
+ }): Promise<ClineToolPreflightResult> {
341
+ if (!input.capabilities.toolCatalogJson) {
342
+ return { ok: false, detail: "The installed Cline CLI does not advertise config tools --json." };
343
+ }
344
+ let result: ProbeResult;
345
+ try {
346
+ result = await runProbeProcess(
347
+ input.binary,
348
+ ["config", "tools", "--json"],
349
+ input.env,
350
+ input.cwd,
351
+ input.deps ?? {},
352
+ input.timeoutMs ?? PREFLIGHT_TIMEOUT_MS,
353
+ MAX_PREFLIGHT_STDOUT_BYTES,
354
+ );
355
+ } catch (error) {
356
+ return { ok: false, detail: error instanceof Error ? error.message : String(error) };
357
+ }
358
+ if (result.exitCode !== 0) {
359
+ return { ok: false, detail: redactProbeDetail(result.stderr) || "Cline tool preflight exited with code " + result.exitCode + "." };
360
+ }
361
+ let parsed: unknown;
362
+ try {
363
+ parsed = JSON.parse(result.stdout);
364
+ } catch {
365
+ return { ok: false, detail: "Cline tool preflight returned non-JSON output." };
366
+ }
367
+ return validateClineToolCatalog(parsed);
368
+ }
369
+
370
+ function errorEvent(error: unknown): Extract<AdapterEvent, { type: "error" }> {
371
+ if (error instanceof ClineJsonError) {
372
+ return {
373
+ type: "error",
374
+ message: error.message,
375
+ status: error.status,
376
+ errorType: error.errorType,
377
+ code: error.code,
378
+ retryable: error.retryable,
379
+ };
380
+ }
381
+ return {
382
+ type: "error",
383
+ message: redactSecretString(error instanceof Error ? error.message : String(error)),
384
+ status: 502,
385
+ errorType: "upstream_error",
386
+ code: "cli_protocol_error",
387
+ retryable: false,
388
+ };
389
+ }
390
+
391
+ function killWindowsProcessTree(pid: number): void {
392
+ const taskkill = (process.env.SystemRoot ?? "C:\\Windows") + "\\System32\\taskkill.exe";
393
+ nodeSpawn(taskkill, ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
394
+ }
395
+
396
+ export async function runClineJsonTurn(
397
+ input: ClineJsonTurnInput,
398
+ emit: (event: AdapterEvent) => void,
399
+ deps: CliAgentTurnDeps = {},
400
+ ): Promise<void> {
401
+ const spawnFn = deps.spawn ?? (nodeSpawn as unknown as NonNullable<CliAgentTurnDeps["spawn"]>);
402
+ const platform = deps.platform ?? process.platform;
403
+ const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
404
+ const killGraceMs = deps.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
405
+ const reapTimeoutMs = deps.reapTimeoutMs ?? killGraceMs * 2 + 250;
406
+ const toolBatchIdleMs = deps.toolBatchIdleMs ?? DEFAULT_TOOL_BATCH_IDLE_MS;
407
+ const state = createClineStreamState();
408
+ const maxToolCalls = Math.min(input.maxToolCalls, MAX_TOOL_CALLS);
409
+ const stderr: string[] = [];
410
+ let stderrBytes = 0;
411
+ let child: ChildProcess | undefined;
412
+ let terminalEmitted = false;
413
+ let aborted = false;
414
+ let timedOut = false;
415
+ let toolBatchStopped = false;
416
+ let spawnError: Error | undefined;
417
+ let exitCode: number | null = null;
418
+ let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
419
+ let killTimer: ReturnType<typeof setTimeout> | undefined;
420
+ let toolIdleTimer: ReturnType<typeof setTimeout> | undefined;
421
+
422
+ const deliver = (event: AdapterEvent): void => {
423
+ const terminal = event.type === "done" && input.requireToolCall && state.toolCalls === 0
424
+ ? {
425
+ type: "error" as const,
426
+ message: "Cline returned no tool call for a request that requires one.",
427
+ status: 502,
428
+ errorType: "upstream_error",
429
+ code: "tool_call_required",
430
+ retryable: false,
431
+ }
432
+ : event;
433
+ if (terminal.type === "done" || terminal.type === "error" || terminal.type === "incomplete") {
434
+ if (terminalEmitted) return;
435
+ terminalEmitted = true;
436
+ }
437
+ emit(terminal);
438
+ };
439
+
440
+ const stopStdout = (): void => {
441
+ try { child?.stdout?.destroy(); } catch { /* already closed */ }
442
+ };
443
+
444
+ const kill = (): void => {
445
+ if (!child || child.killed) return;
446
+ if (platform === "win32" && child.pid !== undefined) {
447
+ try {
448
+ (deps.killWindowsProcessTree ?? killWindowsProcessTree)(child.pid);
449
+ return;
450
+ } catch {
451
+ // Fall through to the direct termination ladder.
452
+ }
453
+ }
454
+ try { child.kill("SIGTERM"); } catch { /* already stopped */ }
455
+ killTimer = setTimeout(() => {
456
+ try { child?.kill("SIGKILL"); } catch { /* already stopped */ }
457
+ }, killGraceMs);
458
+ };
459
+
460
+ if (input.incoming.abortSignal?.aborted) {
461
+ deliver({
462
+ type: "error",
463
+ message: "Cline JSON turn was cancelled.",
464
+ status: 499,
465
+ errorType: "invalid_request_error",
466
+ code: "client_cancelled",
467
+ retryable: false,
468
+ });
469
+ return;
470
+ }
471
+
472
+ const onAbort = (): void => {
473
+ if (terminalEmitted) return;
474
+ aborted = true;
475
+ kill();
476
+ stopStdout();
477
+ deliver({
478
+ type: "error",
479
+ message: "Cline JSON turn was cancelled.",
480
+ status: 499,
481
+ errorType: "invalid_request_error",
482
+ code: "client_cancelled",
483
+ retryable: false,
484
+ });
485
+ };
486
+
487
+ const armToolIdle = (): void => {
488
+ if (terminalEmitted || toolBatchStopped || state.toolCalls === 0) return;
489
+ if (toolIdleTimer) clearTimeout(toolIdleTimer);
490
+ toolIdleTimer = setTimeout(() => {
491
+ toolBatchStopped = true;
492
+ kill();
493
+ }, toolBatchIdleMs);
494
+ };
495
+
496
+ let lifecycle: Promise<void> | undefined;
497
+ try {
498
+ const invocation = commandInvocation(input.binary, input.args, platform, { env: input.env });
499
+ let stdinDescriptor: number;
500
+ try {
501
+ stdinDescriptor = openSync(input.promptFile, "r");
502
+ } catch {
503
+ deliver({
504
+ type: "error",
505
+ message: "Cline prompt could not be staged securely.",
506
+ status: 500,
507
+ errorType: "upstream_error",
508
+ code: "cli_prompt_staging_failed",
509
+ retryable: false,
510
+ });
511
+ return;
512
+ }
513
+ try {
514
+ child = spawnFn(invocation.file, invocation.args, {
515
+ ...invocation.options,
516
+ cwd: input.cwd,
517
+ env: input.env,
518
+ stdio: [stdinDescriptor, "pipe", "pipe"],
519
+ windowsHide: true,
520
+ });
521
+ } finally {
522
+ closeSync(stdinDescriptor);
523
+ }
524
+ lifecycle = new Promise<void>(resolvePromise => {
525
+ let settled = false;
526
+ const settle = (): void => {
527
+ if (settled) return;
528
+ settled = true;
529
+ resolvePromise();
530
+ };
531
+ child?.once("error", error => {
532
+ spawnError = error;
533
+ if (child?.pid === undefined) settle();
534
+ });
535
+ child?.once("close", code => {
536
+ exitCode = code;
537
+ settle();
538
+ });
539
+ });
540
+
541
+ input.incoming.abortSignal?.addEventListener("abort", onAbort, { once: true });
542
+ timeoutTimer = setTimeout(() => {
543
+ if (terminalEmitted) return;
544
+ timedOut = true;
545
+ kill();
546
+ stopStdout();
547
+ deliver({
548
+ type: "error",
549
+ message: "Cline JSON turn timed out.",
550
+ status: 504,
551
+ errorType: "upstream_error",
552
+ code: "timeout",
553
+ retryable: true,
554
+ });
555
+ }, timeoutMs);
556
+
557
+ child.stderr?.setEncoding("utf8");
558
+ child.stderr?.on("data", (chunk: string) => {
559
+ if (stderrBytes >= MAX_STDERR_BYTES) return;
560
+ const remaining = MAX_STDERR_BYTES - stderrBytes;
561
+ const admitted = chunk.slice(0, remaining);
562
+ stderr.push(admitted);
563
+ stderrBytes += Buffer.byteLength(admitted);
564
+ });
565
+
566
+ if (!child.stdout) throw new Error("Cline JSON process produced no stdout stream.");
567
+ for await (const message of readJsonLines(child.stdout)) {
568
+ if (terminalEmitted) break;
569
+ let events: AdapterEvent[];
570
+ try {
571
+ events = mapClineMessage(message, state, {
572
+ toolNames: input.toolNames,
573
+ maxToolCalls,
574
+ });
575
+ } catch (error) {
576
+ if (error instanceof ClineStreamProtocolError || error instanceof CodingAgentProtocolError) {
577
+ deliver({
578
+ type: "error",
579
+ message: error.message,
580
+ status: error.status,
581
+ errorType: "upstream_error",
582
+ code: "cli_protocol_error",
583
+ retryable: false,
584
+ });
585
+ break;
586
+ }
587
+ throw error;
588
+ }
589
+ for (const event of events) deliver(event);
590
+ if (terminalEmitted) break;
591
+ armToolIdle();
592
+ }
593
+
594
+ if (!terminalEmitted && !aborted && !timedOut) {
595
+ await Promise.race([
596
+ lifecycle ?? Promise.resolve(),
597
+ new Promise<void>(resolvePromise => setTimeout(resolvePromise, reapTimeoutMs)),
598
+ ]);
599
+ if (state.toolCalls > 0) {
600
+ if (toolBatchStopped || exitCode === 0) {
601
+ deliver({ type: "done", stopReason: "tool_use", ...(state.usage ? { usage: state.usage } : {}) });
602
+ } else {
603
+ deliver(classifyClineFailure(exitCode ?? 1, stderr.join("")));
604
+ }
605
+ } else if (input.requireToolCall) {
606
+ deliver({
607
+ type: "error",
608
+ message: "Cline returned no tool call for a request that requires one.",
609
+ status: 502,
610
+ errorType: "upstream_error",
611
+ code: "tool_call_required",
612
+ retryable: false,
613
+ });
614
+ } else if (spawnError) {
615
+ deliver({
616
+ type: "error",
617
+ message: "Cline JSON process failed to start: " + spawnError.message,
618
+ status: 500,
619
+ errorType: "upstream_error",
620
+ code: "cli_spawn_failed",
621
+ retryable: false,
622
+ });
623
+ } else if (exitCode !== null && exitCode !== 0) {
624
+ deliver(classifyClineFailure(exitCode, stderr.join("")));
625
+ } else {
626
+ deliver({
627
+ type: "error",
628
+ message: "Cline JSON ended without a terminal result.",
629
+ status: 502,
630
+ errorType: "upstream_error",
631
+ code: "cli_protocol_error",
632
+ retryable: false,
633
+ });
634
+ }
635
+ }
636
+ } catch (error) {
637
+ if (!terminalEmitted) deliver(errorEvent(error));
638
+ } finally {
639
+ if (timeoutTimer) clearTimeout(timeoutTimer);
640
+ if (toolIdleTimer) clearTimeout(toolIdleTimer);
641
+ input.incoming.abortSignal?.removeEventListener("abort", onAbort);
642
+ try { child?.stdin?.destroy(); } catch { /* already closed */ }
643
+ if (child && child.exitCode === null && !child.killed) kill();
644
+ if (child && child.exitCode === null) {
645
+ await Promise.race([
646
+ lifecycle ?? new Promise<void>(resolvePromise => child?.once("close", () => resolvePromise())),
647
+ new Promise<void>(resolvePromise => setTimeout(resolvePromise, reapTimeoutMs)),
648
+ ]);
649
+ }
650
+ if (killTimer) clearTimeout(killTimer);
651
+ }
652
+ }