@koreanhole/opencodex 2.97.0 → 2.99.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.
@@ -1,13 +1,7 @@
1
- import { spawn } from "node:child_process";
2
- import { link, mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from "node:fs/promises";
1
+ import { link, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
3
2
  import { homedir, tmpdir } from "node:os";
4
3
  import { dirname, join, resolve } from "node:path";
5
4
  import { baseScopedEnv } from "../env";
6
- import { commandInvocation } from "../../../lib/win-exec";
7
-
8
- const MAX_PREFLIGHT_STDOUT_BYTES = 2 * 1024 * 1024;
9
- const MAX_PREFLIGHT_STDERR_BYTES = 8 * 1024;
10
- const PREFLIGHT_TIMEOUT_MS = 15_000;
11
5
 
12
6
  /** Built-in, subagent, and team tools that a model-only Cline turn must never enable. */
13
7
  export const CLINE_DISABLED_TOOL_IDS = Object.freeze([
@@ -65,31 +59,15 @@ export interface ClineStagedTurn {
65
59
  providerSettingsPath: string;
66
60
  globalSettingsPath: string;
67
61
  mcpSettingsPath: string;
62
+ catalogPath: string;
68
63
  cleanup: () => Promise<void>;
69
64
  }
70
65
 
71
- export interface ClineEnabledTool {
72
- id: string;
73
- kind: "builtin";
74
- }
75
-
76
- export interface ClineToolPreflightResult {
77
- ok: boolean;
78
- unsupported?: boolean;
79
- detail?: string;
80
- enabledTools?: readonly ClineEnabledTool[];
81
- }
82
-
83
66
  export interface ClineSelection {
84
67
  providerId: string;
85
68
  model?: string;
86
69
  }
87
70
 
88
- export interface ClineFlagCapabilities {
89
- providerFlag?: "--provider";
90
- modelFlag?: "--model" | "--modelid";
91
- }
92
-
93
71
  interface ClineSelectionCandidate {
94
72
  providerId: string;
95
73
  model?: string;
@@ -280,8 +258,8 @@ export async function assertClineSelectionSafe(providerSettingsPath: string): Pr
280
258
  }
281
259
 
282
260
  /**
283
- * The vendor store is advisory in one-shot mode: absence or another schema is Cline's
284
- * business. Only an explicit route back through this proxy remains a hard refusal.
261
+ * The vendor store is advisory: absence or another schema is Cline's business.
262
+ * Only an explicit route back through this proxy remains a hard refusal.
285
263
  */
286
264
  export async function assertClineAdvisoryDestinationSafe(providerSettingsPath: string): Promise<void> {
287
265
  try {
@@ -296,10 +274,7 @@ function clineGlobalSettings(): Record<string, unknown> {
296
274
  return {
297
275
  telemetryOptOut: true,
298
276
  autoUpdateEnabled: false,
299
- // Built-in and plugin tools are disabled below; auto-approval lets only the
300
- // capture-only MCP server receive the model's tool call, where it hangs until
301
- // the runner observes the completed intent batch and stops the child.
302
- toolAutoApprove: true,
277
+ toolAutoApprove: false,
303
278
  compactionEnabled: false,
304
279
  disabledTools: [...CLINE_DISABLED_TOOL_IDS],
305
280
  tools: {
@@ -310,33 +285,11 @@ function clineGlobalSettings(): Record<string, unknown> {
310
285
  };
311
286
  }
312
287
 
313
- export type ClineMcpLaunch =
314
- | { kind: "module"; modulePath: string }
315
- | { kind: "entrypoint"; entrypoint: string };
316
-
317
- function clineMcpArgs(launch: ClineMcpLaunch, catalogPath: string): string[] {
318
- return launch.kind === "module"
319
- ? [launch.modulePath, catalogPath]
320
- : [launch.entrypoint, catalogPath];
288
+ function clineMcpSettings(): Record<string, unknown> {
289
+ return { mcpServers: {} };
321
290
  }
322
291
 
323
- function clineMcpSettings(launch: ClineMcpLaunch | undefined, catalogPath: string): Record<string, unknown> {
324
- if (!launch) return { mcpServers: {} };
325
- return {
326
- mcpServers: {
327
- opencodex: {
328
- transport: {
329
- type: "stdio",
330
- command: process.execPath,
331
- args: clineMcpArgs(launch, catalogPath),
332
- },
333
- disabled: false,
334
- },
335
- },
336
- };
337
- }
338
-
339
- /** Share a staged file at a vendor-default path without copying its bytes. */
292
+ /** Share a staged file at a vendor-default path. */
340
293
  async function linkClineFile(target: string, path: string): Promise<void> {
341
294
  try {
342
295
  await symlink(resolve(target), path);
@@ -349,15 +302,28 @@ async function linkClineFile(target: string, path: string): Promise<void> {
349
302
  }
350
303
  }
351
304
 
305
+ /** Copy the auth source into the private turn root so vendor refreshes cannot mutate it. */
306
+ async function copyClineProviderSettings(source: string, target: string): Promise<boolean> {
307
+ let bytes: Buffer;
308
+ try {
309
+ bytes = await readFile(source);
310
+ } catch (error) {
311
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
312
+ throw error;
313
+ }
314
+ await writeFile(target, bytes, { mode: 0o600, flag: "wx" });
315
+ return true;
316
+ }
317
+
352
318
  /**
353
319
  * A fork that ignores CLINE_PROVIDER_SETTINGS_PATH still looks at ~/.cline or
354
- * CLINE_DIR/data/settings. Link the real provider store and the staged settings there.
320
+ * CLINE_DIR/data/settings. Link the staged provider copy and settings there.
355
321
  */
356
322
  async function linkClineSettings(input: {
357
323
  dataDir: string;
358
324
  configDir: string;
359
325
  homeDir: string;
360
- providerSettingsPath: string;
326
+ providerSettingsPath?: string;
361
327
  globalSettingsPath: string;
362
328
  mcpSettingsPath: string;
363
329
  }): Promise<void> {
@@ -367,7 +333,9 @@ async function linkClineSettings(input: {
367
333
  { dir: join(input.homeDir, ".cline", "data", "settings"), settings: true },
368
334
  ];
369
335
  for (const target of targets) {
370
- await linkClineFile(input.providerSettingsPath, join(target.dir, "providers.json"));
336
+ if (input.providerSettingsPath) {
337
+ await linkClineFile(input.providerSettingsPath, join(target.dir, "providers.json"));
338
+ }
371
339
  if (!target.settings) continue;
372
340
  await linkClineFile(input.globalSettingsPath, join(target.dir, "global-settings.json"));
373
341
  await linkClineFile(input.mcpSettingsPath, join(target.dir, "cline_mcp_settings.json"));
@@ -378,7 +346,6 @@ async function linkClineSettings(input: {
378
346
  export async function stageClineTurn(options: {
379
347
  providerSettingsPath: string;
380
348
  catalog: readonly Record<string, unknown>[];
381
- mcpLaunch?: ClineMcpLaunch;
382
349
  }): Promise<ClineStagedTurn> {
383
350
  const root = await mkdtemp(join(tmpdir(), "ocx-cline-cli-"));
384
351
  const configDir = join(root, "config");
@@ -389,6 +356,7 @@ export async function stageClineTurn(options: {
389
356
  const logDir = join(root, "logs");
390
357
  const globalSettingsPath = join(dataDir, "settings", "global-settings.json");
391
358
  const mcpSettingsPath = join(dataDir, "settings", "cline_mcp_settings.json");
359
+ const providerSettingsPath = join(homeDir, ".cline", "data", "settings", "providers.json");
392
360
  const catalogPath = join(root, "tool-catalog.json");
393
361
 
394
362
  const cleanup = async (): Promise<void> => {
@@ -401,6 +369,8 @@ export async function stageClineTurn(options: {
401
369
  mkdir(dirname(globalSettingsPath), { recursive: true, mode: 0o700 }),
402
370
  mkdir(join(configDir, "data", "settings"), { recursive: true, mode: 0o700 }),
403
371
  mkdir(join(homeDir, ".cline", "data", "settings"), { recursive: true, mode: 0o700 }),
372
+ mkdir(join(homeDir, ".config"), { recursive: true, mode: 0o700 }),
373
+ mkdir(join(homeDir, ".cache"), { recursive: true, mode: 0o700 }),
404
374
  mkdir(homeDir, { recursive: true, mode: 0o700 }),
405
375
  mkdir(workspaceDir, { recursive: true, mode: 0o700 }),
406
376
  mkdir(hooksDir, { recursive: true, mode: 0o700 }),
@@ -408,14 +378,15 @@ export async function stageClineTurn(options: {
408
378
  ]);
409
379
  await Promise.all([
410
380
  writeFile(globalSettingsPath, `${JSON.stringify(clineGlobalSettings(), null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" }),
411
- writeFile(mcpSettingsPath, `${JSON.stringify(clineMcpSettings(options.mcpLaunch, catalogPath), null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" }),
381
+ writeFile(mcpSettingsPath, `${JSON.stringify(clineMcpSettings(), null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" }),
412
382
  writeFile(catalogPath, JSON.stringify(options.catalog), { encoding: "utf8", mode: 0o600, flag: "wx" }),
413
383
  ]);
384
+ const copiedProviderStore = await copyClineProviderSettings(options.providerSettingsPath, providerSettingsPath);
414
385
  await linkClineSettings({
415
386
  dataDir,
416
387
  configDir,
417
388
  homeDir,
418
- providerSettingsPath: options.providerSettingsPath,
389
+ ...(copiedProviderStore ? { providerSettingsPath } : {}),
419
390
  globalSettingsPath,
420
391
  mcpSettingsPath,
421
392
  });
@@ -426,9 +397,10 @@ export async function stageClineTurn(options: {
426
397
  homeDir,
427
398
  workspaceDir,
428
399
  hooksDir,
429
- providerSettingsPath: options.providerSettingsPath,
400
+ providerSettingsPath,
430
401
  globalSettingsPath,
431
402
  mcpSettingsPath,
403
+ catalogPath,
432
404
  cleanup,
433
405
  };
434
406
  } catch (error) {
@@ -439,18 +411,10 @@ export async function stageClineTurn(options: {
439
411
 
440
412
  export function buildClineChildEnv(staged: ClineStagedTurn, sourceEnv: NodeJS.ProcessEnv = process.env): Record<string, string> {
441
413
  const env = baseScopedEnv();
442
- // A locally authenticated Cline fork may ignore the CLINE_* path overrides and
443
- // resolve auth from the operator's real HOME/XDG. Preserve that environment; the
444
- // staged CLINE_* paths below still win for a CLI that honors them.
445
- const realHome = nonEmpty(sourceEnv.HOME) ?? nonEmpty(sourceEnv.USERPROFILE) ?? homedir();
446
- if (realHome) {
447
- env.HOME = realHome;
448
- env.USERPROFILE = realHome;
449
- }
450
- const xdgConfig = nonEmpty(sourceEnv.XDG_CONFIG_HOME);
451
- if (xdgConfig) env.XDG_CONFIG_HOME = xdgConfig;
452
- const xdgCache = nonEmpty(sourceEnv.XDG_CACHE_HOME);
453
- if (xdgCache) env.XDG_CACHE_HOME = xdgCache;
414
+ env.HOME = staged.homeDir;
415
+ env.USERPROFILE = staged.homeDir;
416
+ env.XDG_CONFIG_HOME = join(staged.homeDir, ".config");
417
+ env.XDG_CACHE_HOME = join(staged.homeDir, ".cache");
454
418
  const user = sourceEnv.USER ?? sourceEnv.LOGNAME;
455
419
  if (user) env.USER = user;
456
420
  if (sourceEnv.LOGNAME) env.LOGNAME = sourceEnv.LOGNAME;
@@ -475,189 +439,3 @@ export function buildClineChildEnv(staged: ClineStagedTurn, sourceEnv: NodeJS.Pr
475
439
  });
476
440
  return env;
477
441
  }
478
-
479
- function runPreflightProcess(
480
- binary: string,
481
- args: readonly string[],
482
- env: Record<string, string>,
483
- cwd: string,
484
- ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
485
- return new Promise((resolve, reject) => {
486
- const invocation = commandInvocation(binary, [...args], process.platform, { env });
487
- const child = spawn(invocation.file, invocation.args, {
488
- ...invocation.options,
489
- cwd,
490
- env,
491
- stdio: ["ignore", "pipe", "pipe"],
492
- windowsHide: true,
493
- });
494
- const stdout: Buffer[] = [];
495
- const stderr: Buffer[] = [];
496
- let stdoutBytes = 0;
497
- let stderrBytes = 0;
498
- let settled = false;
499
- const finish = (fn: () => void): void => {
500
- if (settled) return;
501
- settled = true;
502
- clearTimeout(timer);
503
- fn();
504
- };
505
- const timer = setTimeout(() => {
506
- try { child.kill("SIGKILL"); } catch { /* ignore */ }
507
- finish(() => reject(new Error("Cline tool preflight timed out.")));
508
- }, PREFLIGHT_TIMEOUT_MS);
509
-
510
- child.stdout.on("data", (chunk: Buffer) => {
511
- if (stdoutBytes >= MAX_PREFLIGHT_STDOUT_BYTES) return;
512
- const remaining = MAX_PREFLIGHT_STDOUT_BYTES - stdoutBytes;
513
- const admitted = chunk.subarray(0, remaining);
514
- stdout.push(admitted);
515
- stdoutBytes += admitted.byteLength;
516
- });
517
- child.stderr.on("data", (chunk: Buffer) => {
518
- if (stderrBytes >= MAX_PREFLIGHT_STDERR_BYTES) return;
519
- const remaining = MAX_PREFLIGHT_STDERR_BYTES - stderrBytes;
520
- const admitted = chunk.subarray(0, remaining);
521
- stderr.push(admitted);
522
- stderrBytes += admitted.byteLength;
523
- });
524
- child.once("error", (error) => finish(() => reject(error)));
525
- child.once("close", (code) => finish(() => resolve({
526
- exitCode: code ?? 1,
527
- stdout: Buffer.concat(stdout).toString("utf8"),
528
- stderr: Buffer.concat(stderr).toString("utf8"),
529
- })));
530
- });
531
- }
532
-
533
- /** Merge discovered built-in IDs into the turn's private staged disable list. */
534
- export async function mergeClineDisabledTools(
535
- staged: Pick<ClineStagedTurn, "globalSettingsPath">,
536
- ids: readonly string[],
537
- ): Promise<void> {
538
- let parsed: unknown;
539
- try {
540
- parsed = JSON.parse(await readFile(staged.globalSettingsPath, "utf8"));
541
- } catch {
542
- throw new ClineSettingsError(
543
- "Cline global settings are malformed; the tool preflight cannot be repaired.",
544
- 500,
545
- "cline_cli_setup_failed",
546
- );
547
- }
548
- const settings = asRecord(parsed);
549
- if (!settings) {
550
- throw new ClineSettingsError(
551
- "Cline global settings are malformed; the tool preflight cannot be repaired.",
552
- 500,
553
- "cline_cli_setup_failed",
554
- );
555
- }
556
- const current = Array.isArray(settings.disabledTools)
557
- ? settings.disabledTools.filter((value): value is string => typeof value === "string")
558
- : [];
559
- const merged = [...new Set([...current, ...ids])];
560
- await writeFile(
561
- staged.globalSettingsPath,
562
- `${JSON.stringify({ ...settings, disabledTools: merged }, null, 2)}\n`,
563
- { encoding: "utf8", mode: 0o600 },
564
- );
565
- }
566
-
567
- /** Verify the isolated Cline config advertises no executable built-in or plugin tool. */
568
- export function validateClineToolCatalog(parsed: unknown): ClineToolPreflightResult {
569
- if (!Array.isArray(parsed)) return { ok: false, detail: "Cline tool preflight returned a non-array catalog." };
570
- const enabledTools: ClineEnabledTool[] = [];
571
- let pluginDetail: string | undefined;
572
- for (const item of parsed) {
573
- const record = asRecord(item);
574
- if (!record) continue;
575
- if (record.type === "plugin") {
576
- if (record.enabled === true) {
577
- pluginDetail ??= `Cline enabled plugin tool ${String(record.name ?? "unknown")}.`;
578
- }
579
- continue;
580
- }
581
- if (record.defaultEnabled !== true) continue;
582
- const id = nonEmpty(record.id);
583
- if (!id) return { ok: false, detail: "Cline enabled a built-in tool without an id." };
584
- enabledTools.push({ id, kind: "builtin" });
585
- }
586
- if (pluginDetail) return { ok: false, detail: pluginDetail };
587
- if (enabledTools.length > 0) {
588
- return {
589
- ok: false,
590
- detail: `Cline enabled built-in tool ${enabledTools[0]!.id}.`,
591
- enabledTools,
592
- };
593
- }
594
- return { ok: true };
595
- }
596
-
597
- export function parseClineHelpCapabilities(text: string): ClineFlagCapabilities {
598
- const providerFlag = /(?:^|\s)--provider(?:\s|,|<|$)/.test(text) ? "--provider" as const : undefined;
599
- const modelFlag = /(?:^|\s)--model(?:\s|,|<|$)/.test(text)
600
- ? "--model" as const
601
- : /(?:^|\s)--modelid(?:\s|,|<|$)/.test(text)
602
- ? "--modelid" as const
603
- : undefined;
604
- return {
605
- ...(providerFlag ? { providerFlag } : {}),
606
- ...(modelFlag ? { modelFlag } : {}),
607
- };
608
- }
609
-
610
- const FLAG_CACHE = new Map<string, { mtimeMs: number; size: number; capabilities: ClineFlagCapabilities }>();
611
-
612
- /** Detect the local CLI's session flags from `--help`; cached by binary fingerprint. */
613
- export async function probeClineFlagCapabilities(
614
- binary: string,
615
- env: Record<string, string>,
616
- ): Promise<ClineFlagCapabilities> {
617
- let fingerprint: { mtimeMs: number; size: number } | undefined;
618
- try {
619
- const value = await stat(binary);
620
- fingerprint = { mtimeMs: value.mtimeMs, size: value.size };
621
- } catch {
622
- fingerprint = undefined;
623
- }
624
- const key = resolve(binary);
625
- if (fingerprint) {
626
- const cached = FLAG_CACHE.get(key);
627
- if (cached && cached.mtimeMs === fingerprint.mtimeMs && cached.size === fingerprint.size) {
628
- return cached.capabilities;
629
- }
630
- }
631
- let capabilities: ClineFlagCapabilities = {};
632
- try {
633
- const result = await runPreflightProcess(binary, ["--help"], env, homedir());
634
- capabilities = parseClineHelpCapabilities(`${result.stdout}\n${result.stderr}`);
635
- } catch {
636
- // No flags: the CLI's current selection remains the fallback.
637
- }
638
- if (fingerprint) FLAG_CACHE.set(key, { ...fingerprint, capabilities });
639
- return capabilities;
640
- }
641
-
642
- export async function preflightClineTools(input: {
643
- binary: string;
644
- env: Record<string, string>;
645
- cwd: string;
646
- }): Promise<ClineToolPreflightResult> {
647
- let result: Awaited<ReturnType<typeof runPreflightProcess>>;
648
- try {
649
- result = await runPreflightProcess(input.binary, ["config", "tools", "--json"], input.env, input.cwd);
650
- } catch (error) {
651
- return { ok: false, unsupported: true, detail: error instanceof Error ? error.message : String(error) };
652
- }
653
- if (result.exitCode !== 0) {
654
- return { ok: false, unsupported: true, detail: result.stderr.trim() || `Cline tool preflight exited with code ${result.exitCode}.` };
655
- }
656
- let parsed: unknown;
657
- try {
658
- parsed = JSON.parse(result.stdout);
659
- } catch {
660
- return { ok: false, unsupported: true, detail: "Cline tool preflight returned non-JSON output." };
661
- }
662
- return validateClineToolCatalog(parsed);
663
- }
@@ -1,9 +1,7 @@
1
- import type { AdapterEvent, OcxUsage } from "../../../types";
1
+ import type { AdapterEvent } from "../../../types";
2
2
  import type { CliAgentStreamState } from "../types";
3
3
  import { mapToolIntent, ToolIntentError } from "../tool-intent";
4
4
 
5
- const MAX_ERROR_CHARS = 2_048;
6
-
7
5
  export class ClineStreamProtocolError extends Error {
8
6
  readonly code = "cli_protocol_error";
9
7
  readonly status = 502;
@@ -29,63 +27,7 @@ export function createClineStreamState(): CliAgentStreamState {
29
27
  };
30
28
  }
31
29
 
32
- function asRecord(value: unknown): Record<string, unknown> | undefined {
33
- return value && typeof value === "object" && !Array.isArray(value)
34
- ? value as Record<string, unknown>
35
- : undefined;
36
- }
37
-
38
- function asString(value: unknown): string | undefined {
39
- return typeof value === "string" ? value : undefined;
40
- }
41
-
42
- function safeMessage(value: unknown): string {
43
- const raw = typeof value === "string"
44
- ? value
45
- : value instanceof Error
46
- ? value.message
47
- : value === undefined
48
- ? "Cline reported an error."
49
- : JSON.stringify(value) ?? String(value);
50
- return raw
51
- .replace(/\bsk-[A-Za-z0-9_-]{6,}\b/g, "[redacted]")
52
- .replace(/(authorization\s*[:=]\s*)\S+/gi, "$1[redacted]")
53
- .slice(0, MAX_ERROR_CHARS);
54
- }
55
-
56
- function classifyError(message: string): { status: number; errorType: string; code: string; retryable: boolean } {
57
- if (/not logged in|please (?:run )?\/?login|sign in|authentication (?:failed|required)|unauthorized|invalid (?:api )?key/i.test(message)) {
58
- return { status: 401, errorType: "authentication_error", code: "cline_cli_not_logged_in", retryable: false };
59
- }
60
- if (/rate limit|too many requests|quota|credit|usage limit|daily limit/i.test(message)) {
61
- return { status: 429, errorType: "rate_limit_error", code: "rate_limit_exceeded", retryable: true };
62
- }
63
- if (/model (?:is )?(?:not found|unavailable|unsupported)|invalid model/i.test(message)) {
64
- return { status: 400, errorType: "invalid_request_error", code: "model_not_found", retryable: false };
65
- }
66
- return { status: 502, errorType: "upstream_error", code: "cli_protocol_error", retryable: false };
67
- }
68
-
69
- function usageFrom(value: unknown): OcxUsage | undefined {
70
- const record = asRecord(value);
71
- if (!record) return undefined;
72
- const count = (input: unknown): number =>
73
- typeof input === "number" && Number.isFinite(input) && input >= 0 ? Math.floor(input) : 0;
74
- const inputTokens = count(record.inputTokens);
75
- const outputTokens = count(record.outputTokens);
76
- const cacheRead = count(record.cacheReadTokens);
77
- const cacheWrite = count(record.cacheWriteTokens);
78
- if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && cacheWrite === 0) return undefined;
79
- return {
80
- inputTokens,
81
- outputTokens,
82
- totalTokens: inputTokens + outputTokens,
83
- ...(cacheRead > 0 ? { cachedInputTokens: cacheRead, cacheReadInputTokens: cacheRead } : {}),
84
- ...(cacheWrite > 0 ? { cacheCreationInputTokens: cacheWrite } : {}),
85
- };
86
- }
87
-
88
- /** Map a validated tool intent from either Cline's NDJSON or ACP event shape. */
30
+ /** Map a validated Cline ACP tool intent into events owned by Codex. */
89
31
  export function mapClineToolIntent(
90
32
  vendorName: string,
91
33
  toolCallId: string | undefined,
@@ -100,159 +42,3 @@ export function mapClineToolIntent(
100
42
  throw error;
101
43
  }
102
44
  }
103
-
104
- function mapToolCall(
105
- event: Record<string, unknown>,
106
- state: CliAgentStreamState,
107
- options: ClineStreamOptions,
108
- ): AdapterEvent[] {
109
- return mapClineToolIntent(asString(event.toolName) ?? "", asString(event.toolCallId), event.input, state, options);
110
- }
111
-
112
- function mapAgentEvent(
113
- event: Record<string, unknown>,
114
- state: CliAgentStreamState,
115
- options: ClineStreamOptions,
116
- ): AdapterEvent[] {
117
- const type = asString(event.type);
118
- if (type === "content_start") {
119
- const contentType = asString(event.contentType);
120
- if (contentType === "text") {
121
- const text = asString(event.text);
122
- if (text) {
123
- state.sawText = true;
124
- return [{ type: "text_delta", text }];
125
- }
126
- return [];
127
- }
128
- if (contentType === "reasoning") {
129
- const thinking = asString(event.reasoning);
130
- if (thinking) {
131
- state.sawThinking = true;
132
- return [{ type: "thinking_delta", thinking }];
133
- }
134
- return [];
135
- }
136
- if (contentType === "tool") return mapToolCall(event, state, options);
137
- return [];
138
- }
139
-
140
- if (type === "content_end") {
141
- const contentType = asString(event.contentType);
142
- if (contentType === "text" && !state.sawText) {
143
- const text = asString(event.text);
144
- if (text) {
145
- state.sawText = true;
146
- return [{ type: "text_delta", text }];
147
- }
148
- }
149
- if (contentType === "reasoning" && !state.sawThinking) {
150
- const thinking = asString(event.reasoning);
151
- if (thinking) {
152
- state.sawThinking = true;
153
- return [{ type: "thinking_delta", thinking }];
154
- }
155
- }
156
- return [];
157
- }
158
-
159
- if (type === "usage") {
160
- const usage = usageFrom(event);
161
- if (usage) state.usage = usage;
162
- return [];
163
- }
164
-
165
- if (type === "error") {
166
- if (event.recoverable === true) return [];
167
- const message = safeMessage(asRecord(event.error)?.message ?? event.error ?? "Cline reported an error.");
168
- state.lastError = message;
169
- state.terminal = true;
170
- return [{ type: "error", message, ...classifyError(message) }];
171
- }
172
-
173
- if (type === "done") {
174
- state.terminal = true;
175
- if (state.toolCalls > 0) return [];
176
- const events: AdapterEvent[] = [];
177
- if (!state.sawText) {
178
- const text = asString(event.text);
179
- if (text) {
180
- state.sawText = true;
181
- events.push({ type: "text_delta", text });
182
- }
183
- }
184
- const usage = usageFrom(event.usage) ?? state.usage;
185
- events.push({ type: "done", stopReason: "stop", ...(usage ? { usage } : {}) });
186
- return events;
187
- }
188
-
189
- if (type === "notice") return [];
190
- return [];
191
- }
192
-
193
- /** Map one bounded Cline NDJSON frame to AdapterEvents. */
194
- export function mapClineMessage(
195
- message: Record<string, unknown>,
196
- state: CliAgentStreamState,
197
- options: ClineStreamOptions,
198
- ): AdapterEvent[] {
199
- const type = asString(message.type);
200
- if (type === "agent_event") {
201
- const event = asRecord(message.event);
202
- return event ? mapAgentEvent(event, state, options) : [];
203
- }
204
-
205
- if (type === "run_result") {
206
- if (state.toolCalls > 0) return [];
207
- const usage = usageFrom(message.usage) ?? state.usage;
208
- if (usage) state.usage = usage;
209
- const finishReason = asString(message.finishReason);
210
- if (finishReason && finishReason !== "completed" && finishReason !== "stop") {
211
- const messageText = safeMessage(message.text ?? `Cline finished with ${finishReason}.`);
212
- state.terminal = true;
213
- return [{ type: "error", message: messageText, ...classifyError(messageText) }];
214
- }
215
- state.terminal = true;
216
- const events: AdapterEvent[] = [];
217
- if (!state.sawText) {
218
- const text = asString(message.text);
219
- if (text) {
220
- state.sawText = true;
221
- events.push({ type: "text_delta", text });
222
- }
223
- }
224
- events.push({ type: "done", stopReason: "stop", ...(usage ? { usage } : {}) });
225
- return events;
226
- }
227
-
228
- if (type === "run_aborted") {
229
- state.terminal = true;
230
- return [{
231
- type: "error",
232
- message: safeMessage(message.message ?? "Cline aborted the turn."),
233
- status: 499,
234
- errorType: "invalid_request_error",
235
- code: "client_cancelled",
236
- retryable: false,
237
- }];
238
- }
239
-
240
- if (type === "error") {
241
- const detail = safeMessage(message.message ?? "Cline reported an error.");
242
- state.lastError = detail;
243
- state.terminal = true;
244
- return [{ type: "error", message: detail, ...classifyError(detail) }];
245
- }
246
-
247
- return [];
248
- }
249
-
250
- /** Convert a nonzero process exit into a stable terminal error. */
251
- export function classifyClineFailure(exitCode: number, stderr: string): Extract<AdapterEvent, { type: "error" }> {
252
- const detail = safeMessage(stderr.trim() || `Cline exited with code ${exitCode}.`);
253
- return {
254
- type: "error",
255
- message: detail,
256
- ...classifyError(detail),
257
- };
258
- }