@koreanhole/opencodex 3.2.8 → 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,9 +89,9 @@ 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
- bun run test:e2e:gemini-oauth
94
95
  bun run test:e2e:gemini-tool
95
96
  bun run test:e2e:gemini-reasoning
96
97
  bun run test:e2e:codex-restore
@@ -99,6 +100,7 @@ bun run test:e2e:codex-restore
99
100
  Report whether each evidence path used a deterministic fixture or a real
100
101
  vendor binary/account. The Gemini command above is a hard live gate: missing
101
102
  binaries, authentication, a Flash Lite model, or cleanup evidence must fail.
102
- The OAuth gate additionally removes API-key/Vertex environment values and the
103
- vendor `.env`.
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.
104
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,8 +165,8 @@ 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
- bun run test:e2e:gemini-oauth
164
170
  bun run test:e2e:gemini-slow
165
171
  bun run test:e2e:gemini-parent-tool
166
172
  bun run privacy:scan
@@ -172,9 +178,11 @@ a real Codex client and a real authenticated Gemini CLI. It runs both the minima
172
178
  completion and a 12-second pre-event slow-start regression. It fails, not skips,
173
179
  on a missing binary, authentication, Flash Lite model, response marker, timeout,
174
180
  or child cleanup assertion. `test:e2e:gemini-slow` runs only the slow-start case.
175
- `test:e2e:gemini-oauth` strips API-key and Vertex environment values, removes the
176
- vendor `.env`, and requires a real OAuth-backed completion through Codex and the
177
- installed Gemini CLI.
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.
178
186
 
179
187
  Optional overrides are `CLINE_BIN_PATH`, `GEMINI_BIN_PATH`, and
180
188
  `CODEX_BIN_PATH`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@koreanhole/opencodex",
3
- "version": "3.2.8",
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,8 +30,8 @@
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
- "test:e2e:gemini-oauth": "bun test ./tests/e2e/gemini-codex.live.ts -t \"uses an existing Gemini CLI OAuth login without an API key\"",
35
35
  "test:e2e:gemini-slow": "bun test ./tests/e2e/gemini-codex.live.ts -t \"full-stack: a slow Gemini start\"",
36
36
  "test:e2e:gemini-tool": "bun test ./tests/e2e/gemini-codex.live.ts -t \"one short Gemini turn leaves the workspace unchanged\"",
37
37
  "test:e2e:gemini-multiturn": "bun test ./tests/e2e/gemini-codex.live.ts -t \"reuses a live Codex multi-turn session\"",
@@ -188,8 +188,8 @@ function isLegacyProviderRow(name: AdapterProviderId, value: unknown): boolean {
188
188
  return row?.adapter === name;
189
189
  }
190
190
 
191
- function uniqueModels(metadata: AdapterModelMetadata | undefined): string[] {
192
- return [...new Set(["default", ...(metadata?.models ?? [])].filter(Boolean))];
191
+ function uniqueModels(name: AdapterProviderId, metadata: AdapterModelMetadata | undefined): string[] {
192
+ return [...new Set([...(name === "cline-cli" ? ["default"] : []), ...(metadata?.models ?? [])].filter(Boolean))];
193
193
  }
194
194
 
195
195
  export function buildManagedProviderRow(
@@ -208,7 +208,7 @@ export function buildManagedProviderRow(
208
208
  allowPrivateNetwork: true,
209
209
  liveModels: true,
210
210
  defaultModel: "default",
211
- models: uniqueModels(metadata),
211
+ models: uniqueModels(name, metadata),
212
212
  ...(metadata.modelDisplayNames ? { modelDisplayNames: metadata.modelDisplayNames } : {}),
213
213
  ...(metadata.modelContextWindows ? { modelContextWindows: metadata.modelContextWindows } : {}),
214
214
  ...(metadata.modelInputModalities ? { modelInputModalities: metadata.modelInputModalities } : {}),
@@ -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) : [];
@@ -204,6 +204,7 @@ export async function discoverGeminiLiveModels(options: GeminiLiveModelsOptions
204
204
  }
205
205
 
206
206
  const runtimeIds = runtime?.map(model => model.modelId) ?? [];
207
+ const runtimeAvailable = runtimeIds.length > 0;
207
208
  const ids = runtimeIds.length > 0 ? runtimeIds : Object.keys(metadata.capabilities);
208
209
  if (ids.length === 0) {
209
210
  failureCache.set(fingerprint, timestamp);
@@ -252,9 +253,11 @@ export async function discoverGeminiLiveModels(options: GeminiLiveModelsOptions
252
253
  loadSession: false,
253
254
  };
254
255
  });
255
- const modelInfoWithDefault = [defaultModelInfo(metadata, modelInfo), ...modelInfo.filter(model => model.id !== "default")];
256
+ const modelInfoWithDefault = runtimeAvailable
257
+ ? modelInfo
258
+ : [defaultModelInfo(metadata, modelInfo), ...modelInfo.filter(model => model.id !== "default")];
256
259
  const warning = metadata.warning
257
- ?? (runtimeError ? "Gemini runtime model discovery failed; using installed CLI metadata." : undefined);
260
+ ?? (runtimeAvailable ? undefined : "Gemini runtime model discovery failed; using installed CLI metadata.");
258
261
  cache.set(fingerprint, { models: ids, modelInfo: modelInfoWithDefault, source: "models", fetchedAt: timestamp, ...(warning ? { metadataWarning: warning } : {}) });
259
262
  failureCache.delete(fingerprint);
260
263
  return { ok: true, models: ids, modelInfo: modelInfoWithDefault, source: "models", fingerprint, ...(warning ? { metadataWarning: warning } : {}) };
@@ -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 {
@@ -178,12 +178,14 @@ function defaultModel(): OcxProviderModel {
178
178
  return { id: "default" };
179
179
  }
180
180
 
181
- function normalizedModels(models: readonly OcxProviderModel[]): OcxProviderModel[] {
181
+ function normalizedModels(models: readonly OcxProviderModel[], includeDefault = true): OcxProviderModel[] {
182
182
  const defaultInfo = models.find(model => model.id === "default");
183
- const out: OcxProviderModel[] = [{ ...defaultModel(), ...(defaultInfo ?? {}), id: "default" }];
184
- const seen = new Set<string>(["default"]);
183
+ const out: OcxProviderModel[] = includeDefault
184
+ ? [{ ...defaultModel(), ...(defaultInfo ?? {}), id: "default" }]
185
+ : [];
186
+ const seen = new Set<string>(includeDefault ? ["default"] : []);
185
187
  for (const model of models) {
186
- if (model.id === "default" || seen.has(model.id)) continue;
188
+ if ((includeDefault && model.id === "default") || seen.has(model.id)) continue;
187
189
  seen.add(model.id);
188
190
  out.push(model);
189
191
  }
@@ -207,7 +209,7 @@ export async function discoverCodexCatalogModels(options: CodexCatalogDiscoveryO
207
209
  if (!gemini.ok && gemini.reason !== "not_found" && env.OPENCODEX_GEMINI_ACP?.trim() !== "0") warnings.push(`Gemini: ${gemini.detail}`);
208
210
  if (gemini.ok && gemini.metadataWarning) warnings.push(`Gemini metadata: ${gemini.metadataWarning}`);
209
211
  const clineModels = normalizedModels(cline.ok ? cline.models.map(id => ({ id })) : []);
210
- const geminiModels = normalizedModels(gemini.ok ? gemini.modelInfo : []);
212
+ const geminiModels = normalizedModels(gemini.ok ? gemini.modelInfo : [], false);
211
213
  const geminiDefault = geminiModels.find(model => model.id === "default");
212
214
  const geminiContexts = geminiModels.flatMap(model => model.contextWindowTokens !== undefined ? [model.contextWindowTokens] : []);
213
215
  if (geminiDefault && geminiContexts.length > 0) {
@@ -118,7 +118,7 @@ async function discoverModels(
118
118
  }
119
119
  if (gemini) {
120
120
  models["gemini-cli"] = gemini.ok
121
- ? ["default", ...gemini.modelInfo.filter(model => model.id !== "default")]
121
+ ? gemini.modelInfo.map(model => model.id)
122
122
  : ["default"];
123
123
  }
124
124
  return models;
@@ -379,6 +379,16 @@ const PROVIDER_LABELS: Record<ProviderId, string> = {
379
379
  [GEMINI_PROVIDER_ID]: "Gemini CLI",
380
380
  };
381
381
 
382
+ function listedProviderEntries(
383
+ known: ProviderModels,
384
+ providerId: ProviderId,
385
+ ): readonly ProviderModelEntry[] {
386
+ const entries = known[providerId] ?? [];
387
+ if (providerId === GEMINI_PROVIDER_ID && entries.length > 0) return entries;
388
+ const defaultEntry = entries.find(entry => modelIdOf(entry) === "default") ?? "default";
389
+ return [defaultEntry, ...entries.filter(entry => modelIdOf(entry) !== "default")];
390
+ }
391
+
382
392
  export function modelList(
383
393
  known: string[] | ProviderModels,
384
394
  scope?: ProviderId,
@@ -394,8 +404,7 @@ export function modelList(
394
404
  }));
395
405
  }
396
406
  if (scope) {
397
- const defaultEntry = (known[scope] ?? []).find(entry => modelIdOf(entry) === "default") ?? "default";
398
- const entries = [defaultEntry, ...(known[scope] ?? []).filter(entry => modelIdOf(entry) !== "default")];
407
+ const entries = listedProviderEntries(known, scope);
399
408
  const rows: Array<Record<string, unknown>> = [];
400
409
  const seen = new Set<string>();
401
410
  for (const entry of entries) {
@@ -440,8 +449,7 @@ export function modelList(
440
449
  display_name: "Configured default provider",
441
450
  }];
442
451
  for (const providerId of [CLINE_PROVIDER_ID, GEMINI_PROVIDER_ID] as const) {
443
- const defaultEntry = (known[providerId] ?? []).find(entry => modelIdOf(entry) === "default") ?? "default";
444
- const entries = [defaultEntry, ...(known[providerId] ?? []).filter(entry => modelIdOf(entry) !== "default")];
452
+ const entries = listedProviderEntries(known, providerId);
445
453
  const seen = new Set<string>();
446
454
  for (const entry of entries) {
447
455
  const id = modelIdOf(entry);