@koreanhole/opencodex 3.2.9 → 3.2.10

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/AGENTS_INSTALL.md CHANGED
@@ -48,8 +48,9 @@ or through the documented default path; unrelated Codex settings are not changed
48
48
  only exception is Cline's mode-0600 request-scoped staging copy.
49
49
  - Do not authenticate on a request path. If Cline reports `auth_required`, stop
50
50
  and ask the operator to run `ocx login cline-cli`.
51
- - Do not edit the operator's real Cline provider store. Read it only to create a
52
- mode-0600 request-scoped copy; remove that copy after the turn.
51
+ - Do not edit the operator's real Cline provider store or legacy
52
+ `globalState.json`/`secrets.json`. Read them only to create mode-0600
53
+ request-scoped copies; remove those copies after the turn.
53
54
  - Do not import or replay Gemini OAuth/API-key material; the Gemini CLI remains
54
55
  the authentication authority.
55
56
  - Gemini headless turns support explicit Gemini Developer API, Vertex AI GenAI,
@@ -88,7 +89,8 @@ Before claiming the proxy works, run:
88
89
  ```bash
89
90
  bun run verify:upstream
90
91
  bun run typecheck
91
- bun test tests/e2e/cline-only.test.ts
92
+ bun run test:e2e
93
+ bun run test:e2e:cline-live
92
94
  bun run test:e2e:gemini
93
95
  bun run test:e2e:gemini-tool
94
96
  bun run test:e2e:gemini-reasoning
@@ -98,4 +100,7 @@ bun run test:e2e:codex-restore
98
100
  Report whether each evidence path used a deterministic fixture or a real
99
101
  vendor binary/account. The Gemini command above is a hard live gate: missing
100
102
  binaries, authentication, a Flash Lite model, or cleanup evidence must fail.
103
+ The Cline live command is also a hard gate: missing binary, authentication,
104
+ response, or cleanup evidence must fail. The 2.2.0 HTTP fixture must never be
105
+ reported as a real vendor run.
101
106
  Never report a skipped or synthetic run as a real login.
package/README.md CHANGED
@@ -3,8 +3,9 @@
3
3
  This distribution keeps the complete original
4
4
  [`@bitkyc08/opencodex`](https://github.com/lidge-jun/opencodex) runtime and adds
5
5
  locally installed Cline CLI and Gemini CLI as model providers. Cline turns use
6
- the CLI's one-shot `--json` mode after a runtime `--help` capability check;
7
- Gemini turns use the CLI's one-shot `-p` stream-json mode.
6
+ the CLI's one-shot `--json` mode after a runtime `--help` capability check and
7
+ support both the Cline 2.2.0 raw-message contract and the current structured
8
+ contract; Gemini turns use the CLI's one-shot `-p` stream-json mode.
8
9
 
9
10
  The original package is an exact npm dependency. This repository does not copy,
10
11
  patch, delete, or rewrite upstream source files. The `ocx` entrypoint loads the
@@ -37,6 +38,9 @@ does not automate Google OAuth. OAuth reuse requires an account and tier eligibl
37
38
  for the installed Gemini CLI's service; explicit API-key or Vertex environment
38
39
  configuration takes precedence over automatic OAuth selection.
39
40
 
41
+ Cline 2.2.0 is the minimum supported release. Newer options are used only when
42
+ the installed binary advertises them.
43
+
40
44
  ## Start
41
45
 
42
46
  ```bash
@@ -51,7 +55,8 @@ through the same `ocx` command.
51
55
 
52
56
  The managed provider rows appear as `gemini-cli/...` and `cline-cli/...`.
53
57
  Cline model discovery and explicit login use ACP, while Cline model turns use
54
- the installed CLI's JSON transport. Gemini runtime model discovery uses the
58
+ the installed CLI's JSON transport across the 2.2.0 and current output contracts.
59
+ Gemini runtime model discovery uses the
55
60
  installed CLI's ACP session, while reasoning metadata and model turns use the
56
61
  installed CLI's headless transport. Parent-declared tools are exposed through a
57
62
  request-scoped capture MCP server.
@@ -131,8 +136,9 @@ the blocker.
131
136
  - Cline does not receive filesystem or terminal capability from the proxy.
132
137
  - Cline runs with request-scoped, mode-0600 settings. Its built-in tools, user
133
138
  MCP servers, hooks, plugins, telemetry, updates, and self-approval are disabled.
134
- Its turn options are used only after the installed binary's `--help` output
135
- advertises them.
139
+ Modern provider state and legacy global/secrets state are copied privately;
140
+ the originals are never modified. Its turn options are used only after the
141
+ installed binary's `--help` output advertises them.
136
142
  - Gemini headless turns run in a request-scoped workspace with
137
143
  `GEMINI_CLI_TRUST_WORKSPACE=true`, `--skip-trust`, and
138
144
  `--approval-mode auto_edit`, but the generated isolated Gemini home settings
@@ -159,6 +165,7 @@ bun run verify:upstream
159
165
  bun run typecheck
160
166
  bun test tests
161
167
  bun run test:e2e
168
+ bun run test:e2e:cline-live
162
169
  bun run test:e2e:gemini
163
170
  bun run test:e2e:gemini-slow
164
171
  bun run test:e2e:gemini-parent-tool
@@ -172,5 +179,10 @@ completion and a 12-second pre-event slow-start regression. It fails, not skips,
172
179
  on a missing binary, authentication, Flash Lite model, response marker, timeout,
173
180
  or child cleanup assertion. `test:e2e:gemini-slow` runs only the slow-start case.
174
181
 
182
+ `test:e2e` exercises the Cline 2.2.0-compatible contract through the real HTTP
183
+ server. `test:e2e:cline-live` additionally requires the installed Cline CLI and
184
+ its existing authentication, and fails rather than skipping on a missing
185
+ binary, response marker, or cleanup assertion.
186
+
175
187
  Optional overrides are `CLINE_BIN_PATH`, `GEMINI_BIN_PATH`, and
176
188
  `CODEX_BIN_PATH`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@koreanhole/opencodex",
3
- "version": "3.2.9",
4
- "description": "Full opencodex runtime with Cline JSON and Gemini CLI headless provider adapters",
3
+ "version": "3.2.10",
4
+ "description": "Full opencodex runtime with Cline 2.2.0+ JSON and Gemini CLI headless provider adapters",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
7
7
  "exports": {
@@ -30,6 +30,7 @@
30
30
  "start": "bun run src/cli/entry.ts start",
31
31
  "test": "bun test tests",
32
32
  "test:e2e": "bun test tests/e2e/cline-only.test.ts",
33
+ "test:e2e:cline-live": "bun test ./tests/e2e/cline-live.live.ts",
33
34
  "test:e2e:gemini": "bun test ./tests/e2e/gemini-codex.live.ts -t \"full-stack:\"",
34
35
  "test:e2e:gemini-slow": "bun test ./tests/e2e/gemini-codex.live.ts -t \"full-stack: a slow Gemini start\"",
35
36
  "test:e2e:gemini-tool": "bun test ./tests/e2e/gemini-codex.live.ts -t \"one short Gemini turn leaves the workspace unchanged\"",
@@ -102,12 +102,23 @@ function toolNameMap(catalog: ReturnType<typeof buildCodingAgentToolCatalog>): M
102
102
  return names;
103
103
  }
104
104
 
105
- function systemPromptWithToolNames(catalog: ReturnType<typeof buildCodingAgentToolCatalog>): string {
106
- if (catalog.emittedNameMap.size === 0) return CLINE_SYSTEM_PROMPT;
105
+ function systemPromptWithToolNames(
106
+ catalog: ReturnType<typeof buildCodingAgentToolCatalog>,
107
+ legacy: boolean,
108
+ ): string {
107
109
  const mappings = [...catalog.emittedNameMap]
108
- .map(([vendorName, wireName]) => `- ${wireName}: call ${vendorName.replace(/^mcp__/, "")}`)
110
+ .map(([vendorName, wireName]) => legacy
111
+ ? `- ${wireName}: call server opencodex tool ${wireName}`
112
+ : `- ${wireName}: call ${vendorName.replace(/^mcp__/, "")}`)
109
113
  .join("\n");
110
- return `${CLINE_SYSTEM_PROMPT}\nExternal tools in client instructions map to these exact callable tools:\n${mappings}`;
114
+ const legacyCompletion = legacy
115
+ ? "Cline 2.2.0 requires the final answer in an attempt_completion tool call. This completion call is an exception to the unavailable built-in tools rule; it only ends the turn and performs no external action."
116
+ : "";
117
+ return [
118
+ CLINE_SYSTEM_PROMPT,
119
+ legacyCompletion,
120
+ ...(mappings ? ["External tools in client instructions map to these exact callable tools:", mappings] : []),
121
+ ].filter(Boolean).join("\n");
111
122
  }
112
123
 
113
124
  function failure(error: unknown): Extract<AdapterEvent, { type: "error" }> {
@@ -191,7 +202,7 @@ export async function runClineTurn(
191
202
  wireName,
192
203
  wireName.startsWith("opencodex__") ? wireName : `opencodex__${wireName}`,
193
204
  ]));
194
- const fixedSystem = systemPromptWithToolNames(catalog);
205
+ const fixedSystem = systemPromptWithToolNames(catalog, !capabilities.toolCatalogJson);
195
206
  const actualPrompt = buildClinePrompt(parsed, vendorNameByWire, fixedSystem);
196
207
  let args: string[];
197
208
  try {
@@ -215,34 +226,36 @@ export async function runClineTurn(
215
226
  ...(catalog.emittedNameMap.size > 0 ? { mcpLaunch: mcpLaunch() } : {}),
216
227
  });
217
228
  const env = buildClineChildEnv(staged, sourceEnv);
218
- const preflight = deps.preflightTools ?? preflightClineTools;
219
- let tools = await preflight({
220
- binary,
221
- env,
222
- cwd: staged.workspaceDir,
223
- capabilities,
224
- deps,
225
- });
226
- if (!tools.ok && tools.enabledTools && tools.enabledTools.length > 0) {
227
- await mergeClineDisabledTools(staged, tools.enabledTools.map(tool => tool.id));
228
- tools = await preflight({
229
+ if (capabilities.toolCatalogJson) {
230
+ const preflight = deps.preflightTools ?? preflightClineTools;
231
+ let tools = await preflight({
229
232
  binary,
230
233
  env,
231
234
  cwd: staged.workspaceDir,
232
235
  capabilities,
233
236
  deps,
234
237
  });
235
- }
236
- if (!tools.ok) {
237
- emit({
238
- type: "error",
239
- message: tools.detail || "Cline tool preflight failed.",
240
- status: 502,
241
- errorType: "upstream_error",
242
- code: "cline_tool_preflight_failed",
243
- retryable: false,
244
- });
245
- return;
238
+ if (!tools.ok && tools.enabledTools && tools.enabledTools.length > 0) {
239
+ await mergeClineDisabledTools(staged, tools.enabledTools.map(tool => tool.id));
240
+ tools = await preflight({
241
+ binary,
242
+ env,
243
+ cwd: staged.workspaceDir,
244
+ capabilities,
245
+ deps,
246
+ });
247
+ }
248
+ if (!tools.ok) {
249
+ emit({
250
+ type: "error",
251
+ message: tools.detail || "Cline tool preflight failed.",
252
+ status: 502,
253
+ errorType: "upstream_error",
254
+ code: "cline_tool_preflight_failed",
255
+ retryable: false,
256
+ });
257
+ return;
258
+ }
246
259
  }
247
260
 
248
261
  const promptFile = join(staged.root, "prompt.txt");
@@ -24,7 +24,6 @@ const MAX_PREFLIGHT_STDOUT_BYTES = 2 * 1024 * 1024;
24
24
  const MAX_STDERR_BYTES = 8 * 1024;
25
25
  const DEFAULT_TIMEOUT_MS = 300_000;
26
26
  const DEFAULT_KILL_GRACE_MS = 2_000;
27
- const DEFAULT_TOOL_BATCH_IDLE_MS = 250;
28
27
  const MAX_TOOL_CALLS = 16;
29
28
 
30
29
  export class ClineJsonError extends Error {
@@ -46,6 +45,7 @@ export interface ClineJsonCapabilities {
46
45
  providerFlag?: "--provider";
47
46
  modelFlag?: "--model" | "--modelid";
48
47
  systemFlag?: "--system";
48
+ autoApproveFlag?: "--auto-approve";
49
49
  }
50
50
 
51
51
  export interface ClineJsonTurnInput {
@@ -109,12 +109,14 @@ export function parseClineJsonHelp(
109
109
  ? "--modelid" as const
110
110
  : undefined;
111
111
  const systemFlag = optionPresent(text, "--system") ? "--system" as const : undefined;
112
+ const autoApproveFlag = optionPresent(text, "--auto-approve") ? "--auto-approve" as const : undefined;
112
113
  return {
113
114
  json,
114
115
  toolCatalogJson: false,
115
116
  ...(providerFlag ? { providerFlag } : {}),
116
117
  ...(modelFlag ? { modelFlag } : {}),
117
118
  ...(systemFlag ? { systemFlag } : {}),
119
+ ...(autoApproveFlag ? { autoApproveFlag } : {}),
118
120
  };
119
121
  }
120
122
 
@@ -240,19 +242,10 @@ export async function probeClineJsonCapabilities(
240
242
  }
241
243
  if (tools.exitCode !== 0) throw probeFailure("cline_tool_catalog_probe_failed", tools.stderr || tools.stdout);
242
244
  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
245
 
253
246
  const capabilities: ClineJsonCapabilities = {
254
247
  ...turn,
255
- toolCatalogJson: true,
248
+ toolCatalogJson: toolCatalog.toolCatalogJson,
256
249
  };
257
250
  if (fingerprint) CAPABILITY_CACHE.set(path, { ...fingerprint, capabilities: { ...capabilities } });
258
251
  return capabilities;
@@ -287,6 +280,9 @@ export function buildClineJsonArgs(input: {
287
280
  if (input.capabilities.systemFlag && input.systemPrompt) {
288
281
  args.push(input.capabilities.systemFlag, input.systemPrompt);
289
282
  }
283
+ if (input.capabilities.autoApproveFlag) {
284
+ args.push(input.capabilities.autoApproveFlag, "false");
285
+ }
290
286
  args.push(input.prompt);
291
287
  return args;
292
288
  }
@@ -403,7 +399,6 @@ export async function runClineJsonTurn(
403
399
  const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
404
400
  const killGraceMs = deps.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
405
401
  const reapTimeoutMs = deps.reapTimeoutMs ?? killGraceMs * 2 + 250;
406
- const toolBatchIdleMs = deps.toolBatchIdleMs ?? DEFAULT_TOOL_BATCH_IDLE_MS;
407
402
  const state = createClineStreamState();
408
403
  const maxToolCalls = Math.min(input.maxToolCalls, MAX_TOOL_CALLS);
409
404
  const stderr: string[] = [];
@@ -417,7 +412,6 @@ export async function runClineJsonTurn(
417
412
  let exitCode: number | null = null;
418
413
  let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
419
414
  let killTimer: ReturnType<typeof setTimeout> | undefined;
420
- let toolIdleTimer: ReturnType<typeof setTimeout> | undefined;
421
415
 
422
416
  const deliver = (event: AdapterEvent): void => {
423
417
  const terminal = event.type === "done" && input.requireToolCall && state.toolCalls === 0
@@ -484,15 +478,6 @@ export async function runClineJsonTurn(
484
478
  });
485
479
  };
486
480
 
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
481
  let lifecycle: Promise<void> | undefined;
497
482
  try {
498
483
  const invocation = commandInvocation(input.binary, input.args, platform, { env: input.env });
@@ -588,7 +573,12 @@ export async function runClineJsonTurn(
588
573
  }
589
574
  for (const event of events) deliver(event);
590
575
  if (terminalEmitted) break;
591
- armToolIdle();
576
+ if (state.toolCalls > 0) {
577
+ toolBatchStopped = true;
578
+ kill();
579
+ stopStdout();
580
+ break;
581
+ }
592
582
  }
593
583
 
594
584
  if (!terminalEmitted && !aborted && !timedOut) {
@@ -637,7 +627,6 @@ export async function runClineJsonTurn(
637
627
  if (!terminalEmitted) deliver(errorEvent(error));
638
628
  } finally {
639
629
  if (timeoutTimer) clearTimeout(timeoutTimer);
640
- if (toolIdleTimer) clearTimeout(toolIdleTimer);
641
630
  input.incoming.abortSignal?.removeEventListener("abort", onAbort);
642
631
  try { child?.stdin?.destroy(); } catch { /* already closed */ }
643
632
  if (child && child.exitCode === null && !child.killed) kill();
@@ -1,6 +1,6 @@
1
1
  import { link, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
2
2
  import { homedir, tmpdir } from "node:os";
3
- import { dirname, join, resolve } from "node:path";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
4
  import { baseScopedEnv } from "../env";
5
5
 
6
6
  /** Built-in, subagent, and team tools that a model-only Cline turn must never enable. */
@@ -59,6 +59,8 @@ export interface ClineStagedTurn {
59
59
  workspaceDir: string;
60
60
  hooksDir: string;
61
61
  providerSettingsPath: string;
62
+ legacyGlobalStatePath: string;
63
+ legacySecretsPath?: string;
62
64
  globalSettingsPath: string;
63
65
  mcpSettingsPath: string;
64
66
  catalogPath: string;
@@ -292,6 +294,69 @@ function clineGlobalSettings(): Record<string, unknown> {
292
294
  };
293
295
  }
294
296
 
297
+ function legacyClineSafetyState(value: unknown): Record<string, unknown> {
298
+ const state = asRecord(value) ?? {};
299
+ return {
300
+ ...state,
301
+ yoloModeToggled: false,
302
+ autoApproveAllToggled: false,
303
+ clineWebToolsEnabled: false,
304
+ subagentsEnabled: false,
305
+ skillsEnabled: false,
306
+ autoApprovalSettings: {
307
+ version: 1,
308
+ enabled: false,
309
+ favorites: [],
310
+ maxRequests: 0,
311
+ actions: {
312
+ readFiles: false,
313
+ readFilesExternally: false,
314
+ editFiles: false,
315
+ editFilesExternally: false,
316
+ executeSafeCommands: false,
317
+ executeAllCommands: false,
318
+ useBrowser: false,
319
+ useMcp: false,
320
+ },
321
+ enableNotifications: false,
322
+ },
323
+ };
324
+ }
325
+
326
+ async function stageLegacyClineState(
327
+ providerSettingsPath: string,
328
+ dataDir: string,
329
+ ): Promise<{ globalStatePath: string; secretsPath?: string }> {
330
+ const settingsDir = dirname(providerSettingsPath);
331
+ const sourceDataDir = basename(settingsDir) === "settings" ? dirname(settingsDir) : undefined;
332
+ const globalStatePath = join(dataDir, "globalState.json");
333
+ let sourceState: unknown;
334
+ if (sourceDataDir) {
335
+ try {
336
+ sourceState = JSON.parse(await readFile(join(sourceDataDir, "globalState.json"), "utf8"));
337
+ } catch {
338
+ sourceState = undefined;
339
+ }
340
+ }
341
+ await writeFile(globalStatePath, `${JSON.stringify(legacyClineSafetyState(sourceState), null, 2)}\n`, {
342
+ encoding: "utf8",
343
+ mode: 0o600,
344
+ flag: "wx",
345
+ });
346
+
347
+ let secretsPath: string | undefined;
348
+ if (sourceDataDir) {
349
+ try {
350
+ const bytes = await readFile(join(sourceDataDir, "secrets.json"));
351
+ secretsPath = join(dataDir, "secrets.json");
352
+ await writeFile(secretsPath, bytes, { mode: 0o600, flag: "wx" });
353
+ } catch (error) {
354
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
355
+ }
356
+ }
357
+ return { globalStatePath, ...(secretsPath ? { secretsPath } : {}) };
358
+ }
359
+
295
360
  function clineMcpArgs(launch: ClineMcpLaunch, catalogPath: string): string[] {
296
361
  return launch.kind === "module"
297
362
  ? [launch.modulePath, catalogPath]
@@ -300,13 +365,18 @@ function clineMcpArgs(launch: ClineMcpLaunch, catalogPath: string): string[] {
300
365
 
301
366
  function clineMcpSettings(launch: ClineMcpLaunch | undefined, catalogPath: string): Record<string, unknown> {
302
367
  if (!launch) return { mcpServers: {} };
368
+ const args = clineMcpArgs(launch, catalogPath);
303
369
  return {
304
370
  mcpServers: {
305
371
  opencodex: {
372
+ command: process.execPath,
373
+ args,
374
+ disabled: false,
375
+ autoApprove: [],
306
376
  transport: {
307
377
  type: "stdio",
308
378
  command: process.execPath,
309
- args: clineMcpArgs(launch, catalogPath),
379
+ args,
310
380
  },
311
381
  },
312
382
  },
@@ -406,6 +476,7 @@ export async function stageClineTurn(options: {
406
476
  writeFile(mcpSettingsPath, `${JSON.stringify(clineMcpSettings(options.mcpLaunch, catalogPath), null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" }),
407
477
  writeFile(catalogPath, JSON.stringify(options.catalog), { encoding: "utf8", mode: 0o600, flag: "wx" }),
408
478
  ]);
479
+ const legacyState = await stageLegacyClineState(options.providerSettingsPath, join(configDir, "data"));
409
480
  const copiedProviderStore = await copyClineProviderSettings(options.providerSettingsPath, providerSettingsPath);
410
481
  await linkClineSettings({
411
482
  dataDir,
@@ -423,6 +494,8 @@ export async function stageClineTurn(options: {
423
494
  workspaceDir,
424
495
  hooksDir,
425
496
  providerSettingsPath,
497
+ legacyGlobalStatePath: legacyState.globalStatePath,
498
+ ...(legacyState.secretsPath ? { legacySecretsPath: legacyState.secretsPath } : {}),
426
499
  globalSettingsPath,
427
500
  mcpSettingsPath,
428
501
  catalogPath,
@@ -99,6 +99,121 @@ function usageFrom(value: unknown): OcxUsage | undefined {
99
99
  };
100
100
  }
101
101
 
102
+ function legacyUsageFrom(value: unknown): OcxUsage | undefined {
103
+ if (typeof value !== "string") return undefined;
104
+ try {
105
+ const record = asRecord(JSON.parse(value));
106
+ if (!record) return undefined;
107
+ return usageFrom({
108
+ inputTokens: record.tokensIn,
109
+ outputTokens: record.tokensOut,
110
+ cacheReadTokens: record.cacheReads,
111
+ cacheWriteTokens: record.cacheWrites,
112
+ });
113
+ } catch {
114
+ return undefined;
115
+ }
116
+ }
117
+
118
+ function legacyDone(state: CliAgentStreamState, text?: string): AdapterEvent[] {
119
+ if (state.toolCalls > 0) return [];
120
+ state.terminal = true;
121
+ const events: AdapterEvent[] = [];
122
+ if (!state.sawThinking && state.legacyThinking) {
123
+ state.sawThinking = true;
124
+ events.push({ type: "thinking_delta", thinking: state.legacyThinking });
125
+ }
126
+ const finalText = text || state.legacyText;
127
+ if (!state.sawText && finalText) {
128
+ state.sawText = true;
129
+ events.push({ type: "text_delta", text: finalText });
130
+ }
131
+ events.push({ type: "done", stopReason: "stop", ...(state.usage ? { usage: state.usage } : {}) });
132
+ return events;
133
+ }
134
+
135
+ function legacyError(message: string, state: CliAgentStreamState): AdapterEvent[] {
136
+ state.lastError = message;
137
+ state.terminal = true;
138
+ return [{ type: "error", message, ...classifyError(message) }];
139
+ }
140
+
141
+ function legacyMcpRequest(
142
+ text: string | undefined,
143
+ state: CliAgentStreamState,
144
+ options: ClineStreamOptions,
145
+ ): AdapterEvent[] {
146
+ if (state.toolCalls > 0) return [];
147
+ let record: Record<string, unknown> | undefined;
148
+ if (text) {
149
+ try {
150
+ record = asRecord(JSON.parse(text));
151
+ } catch {
152
+ record = undefined;
153
+ }
154
+ }
155
+ if (!record || record.type !== "use_mcp_tool" || record.serverName !== "opencodex") {
156
+ throw new ClineStreamProtocolError("Cline requested an MCP interaction outside the isolated capture-only server.");
157
+ }
158
+ const toolName = asString(record.toolName);
159
+ if (!toolName) throw new ClineStreamProtocolError("Cline returned an MCP tool call without a name.");
160
+ return mapClineToolIntent(toolName, undefined, record.arguments, state, options);
161
+ }
162
+
163
+ function mapLegacyMessage(
164
+ message: Record<string, unknown>,
165
+ state: CliAgentStreamState,
166
+ options: ClineStreamOptions,
167
+ ): AdapterEvent[] {
168
+ if (message.partial === true) return [];
169
+ const type = asString(message.type);
170
+ if (type === "completion") {
171
+ const status = asString(message.status);
172
+ const text = asString(message.text);
173
+ if (status === "error") {
174
+ return legacyError(safeMessage(message.message ?? text ?? "Cline reported an error."), state);
175
+ }
176
+ if (status === "success") return legacyDone(state, text);
177
+ return [];
178
+ }
179
+ if (type === "say") {
180
+ const say = asString(message.say);
181
+ const text = asString(message.text);
182
+ if (say === "text") {
183
+ if (!text) return [];
184
+ state.legacyText = text;
185
+ return [];
186
+ }
187
+ if (say === "reasoning") {
188
+ const reasoning = asString(message.reasoning) ?? text;
189
+ if (!reasoning) return [];
190
+ state.legacyThinking = reasoning;
191
+ return [];
192
+ }
193
+ if (say === "completion_result") return legacyDone(state, text);
194
+ if (say === "error") return legacyError(safeMessage(text ?? "Cline reported an error."), state);
195
+ if (say === "use_mcp_server") return legacyMcpRequest(text, state, options);
196
+ if (say === "api_req_finished") {
197
+ state.usage = legacyUsageFrom(text) ?? state.usage;
198
+ return [];
199
+ }
200
+ if (say === "tool" || say === "command" || say === "command_output" || say === "browser_action"
201
+ || say === "browser_action_result" || say === "mcp_server_request_started" || say === "mcp_server_response") {
202
+ throw new ClineStreamProtocolError("Cline attempted a tool action outside the capture-only boundary.");
203
+ }
204
+ return [];
205
+ }
206
+ if (type === "ask") {
207
+ const ask = asString(message.ask);
208
+ const text = asString(message.text);
209
+ if (ask === "completion_result") return legacyDone(state, text);
210
+ if (ask === "api_req_failed") return legacyError(safeMessage(text ?? "Cline API request failed."), state);
211
+ if (ask === "use_mcp_server") return legacyMcpRequest(text, state, options);
212
+ throw new ClineStreamProtocolError("Cline requested an interactive approval that capture-only headless mode cannot grant.");
213
+ }
214
+ return [];
215
+ }
216
+
102
217
  function mapAgentEvent(
103
218
  event: Record<string, unknown>,
104
219
  state: CliAgentStreamState,
@@ -187,6 +302,9 @@ export function mapClineMessage(
187
302
  ): AdapterEvent[] {
188
303
  if (state.terminal) return [];
189
304
  const type = asString(message.type);
305
+ if (type === "say" || type === "ask" || type === "completion" || type === "task_started") {
306
+ return mapLegacyMessage(message, state, options);
307
+ }
190
308
  if (type === "agent_event") {
191
309
  const event = asRecord(message.event);
192
310
  return event ? mapAgentEvent(event, state, options) : [];
@@ -15,6 +15,8 @@ export interface CliAgentStreamState {
15
15
  nextToolOrdinal: number;
16
16
  usage?: OcxUsage;
17
17
  lastError?: string;
18
+ legacyText?: string;
19
+ legacyThinking?: string;
18
20
  }
19
21
 
20
22
  export interface CliAgentTurnDeps {