@koreanhole/opencodex 2.96.0 → 2.98.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.
package/AGENTS_INSTALL.md CHANGED
@@ -23,21 +23,34 @@ To use Gemini instead, authenticate it through its official CLI and select the
23
23
  provider:
24
24
 
25
25
  ```bash
26
- gemini # official login or existing GEMINI_API_KEY configuration
26
+ gemini # configure a Gemini Developer API or Vertex AI API key
27
27
  ocx provider add gemini-cli --set-default
28
28
  ocx start
29
+ ocx codex sync
29
30
  ```
30
31
 
32
+ `ocx codex sync` refreshes the Codex app catalog with locally discovered Gemini
33
+ models and reasoning levels. Restart the Codex app/app-server after refresh. The
34
+ catalog file is written only when `model_catalog_json` names an opencodex catalog
35
+ or through the documented default path; unrelated Codex settings are not changed.
36
+
31
37
  ## Credential rules
32
38
 
33
39
  - Use the operator's existing Cline or Gemini CLI sign-in. Do not copy vendor
34
- tokens into config, environment files, logs, prompts, or package content.
40
+ tokens into config, environment files, logs, prompts, or package content. The
41
+ only exception is Cline's mode-0600 request-scoped staging copy.
35
42
  - Do not authenticate on a request path. If Cline reports `auth_required`, stop
36
43
  and ask the operator to run `ocx login cline-cli`.
37
- - Do not edit the operator's real Cline provider store unless the operator asks.
44
+ - Do not edit the operator's real Cline provider store. Read it only to create a
45
+ mode-0600 request-scoped copy; remove that copy after the turn.
38
46
  - Do not import or replay Gemini OAuth/API-key material; the Gemini CLI remains
39
47
  the authentication authority.
40
- - The proxy writes only under `$OPENCODEX_HOME` (default `~/.opencodex`).
48
+ - Third-party use must use a Gemini Developer API key or Vertex AI GenAI API key.
49
+ Gemini CLI's official FAQ identifies third-party use of its personal Google
50
+ OAuth authentication as a terms violation. OAuth is not a supported
51
+ opencodex setup path.
52
+ - The proxy writes under `$OPENCODEX_HOME` (default `~/.opencodex`) and may update
53
+ the active opencodex Codex model catalog selected by `model_catalog_json`.
41
54
 
42
55
  ## Operation
43
56
 
@@ -59,6 +72,7 @@ bun run typecheck
59
72
  bun test tests/e2e/cline-only.test.ts
60
73
  bun run test:e2e:gemini
61
74
  bun run test:e2e:gemini-tool
75
+ bun run test:e2e:gemini-reasoning
62
76
  ```
63
77
 
64
78
  Report whether each evidence path used a deterministic ACP fixture or a real
package/README.md CHANGED
@@ -17,13 +17,23 @@ npm install -g cline
17
17
  cline auth
18
18
 
19
19
  npm install -g @google/gemini-cli
20
- gemini # use the official login or configure GEMINI_API_KEY
20
+ gemini # configure GEMINI_API_KEY before using it through opencodex
21
21
  ```
22
22
 
23
+ Cline must advertise ACP (`cline --help` includes `--acp`). opencodex uses ACP
24
+ only and fails closed with `cline_acp_unsupported` for older one-shot-only
25
+ builds.
26
+
23
27
  Gemini authentication remains in Gemini CLI's own home/settings. The proxy never
24
28
  copies, logs, or stores an API key, OAuth token, bearer token, or full request
25
29
  body.
26
30
 
31
+ For third-party clients such as opencodex, use a Gemini Developer API key
32
+ (Google AI Studio) or a Vertex AI GenAI API key. Do not use Gemini CLI's
33
+ personal Google OAuth flow: Gemini CLI's official FAQ identifies third-party
34
+ software piggybacking on its OAuth authentication as a terms violation and says
35
+ the supported third-party method is an API key.
36
+
27
37
  ## Install
28
38
 
29
39
  ```bash
@@ -56,6 +66,7 @@ name = "opencodex (Cline or Gemini CLI)"
56
66
  base_url = "http://127.0.0.1:10100/v1"
57
67
  wire_api = "responses"
58
68
  requires_openai_auth = false
69
+ model_catalog_url = "http://127.0.0.1:10100/v1/model-catalog"
59
70
  ```
60
71
 
61
72
  The model list is live. Cline ACP models are exposed as `cline-cli/<encoded-id>`;
@@ -63,6 +74,34 @@ Gemini ACP models are exposed as `gemini-cli/<encoded-id>`. Each provider keeps
63
74
  `default` selector. A provider prefix routes only to that vendor; unprefixed
64
75
  `default` uses the configured default provider.
65
76
 
77
+ Gemini model IDs and reasoning ladders are discovered from the installed Gemini
78
+ CLI. Thinking-capable models expose the locally supported levels; non-thinking
79
+ models do not get an invented ladder. For a running Codex app, refresh the active
80
+ catalog and reload the app/app-server:
81
+
82
+ ```bash
83
+ bun run src/cli/index.ts codex sync
84
+ ```
85
+
86
+ The command updates the root `model_catalog_json` target when it names an
87
+ opencodex catalog, otherwise it writes `$CODEX_HOME/opencodex-catalog.json` and
88
+ prints the activation setting. `model_catalog_url` avoids the file when the app
89
+ catalog is fetched dynamically.
90
+
91
+ Codex 0.165.0 can use the Responses WebSocket transport when the provider sets
92
+ `supports_websockets = true` and the process is started with
93
+ `--enable responses_websockets`. opencodex accepts the WebSocket on
94
+ `/v1/responses`, handles the `generate=false` warmup, and maps the following
95
+ `previous_response_id` onto the stored Gemini session. HTTP remains the fallback.
96
+
97
+ Gemini context capacity, prompt modalities, and session-load support are
98
+ discovered from the installed ACP agent at runtime; the proxy does not hardcode a
99
+ model context window. A reusable Codex conversation uses
100
+ `previous_response_id`, `prompt_cache_key`, or a documented conversation header
101
+ to load the vendor session and send only the new input. Tool-use turns fall back
102
+ to bounded replay because the vendor session is cancelled when a tool intent is
103
+ captured.
104
+
66
105
  ## Claude Code
67
106
 
68
107
  ```bash
@@ -77,18 +116,28 @@ Use a `cline-cli/...` model to route the same Messages surface to Cline.
77
116
  ## Security boundary
78
117
 
79
118
  - Neither Cline nor Gemini receives filesystem or terminal capability from the proxy.
119
+ - Cline turns use a request-scoped Cline home with built-in/plugin tools, hooks,
120
+ telemetry, updates, and automatic approval disabled. The real vendor provider
121
+ store is immutable; a mode-0600 copy inside the staged home lets the vendor CLI
122
+ refresh auth state and is removed after the turn.
80
123
  - Gemini native tools are removed by a per-turn deny policy; permission requests
81
124
  are cancelled, and no ACP filesystem/terminal client capabilities are advertised.
82
125
  User MCP servers, extensions, skills, and hooks are disabled for the session; only
83
126
  the isolated `opencodex` MCP catalog can receive a client tool call.
127
+ - Gemini runs with a staged `GEMINI_CLI_HOME` under opencodex control; only the
128
+ minimum vendor auth files are linked from the real home, and staged session
129
+ state is bounded and reclaimed.
84
130
  - Declared client tools are exposed only through a request-scoped MCP process that
85
131
  records bounded intent and never executes; Codex owns approval and execution.
86
132
  - Vendor credentials remain in the vendor's real home or authenticated
87
- environment. The proxy stores no vendor token.
133
+ environment. Cline's request-scoped copy is never logged or persisted beyond
134
+ the turn. The proxy stores no vendor token.
88
135
  - Request-path traffic never starts an interactive login.
89
136
  - The listener is loopback-only by default; a non-loopback listener requires an
90
137
  explicit API token.
91
- - Image, video, and audio input are rejected before a vendor process starts.
138
+ - Image input is forwarded only when the selected Gemini model and ACP agent
139
+ advertise image capability; video and audio input are rejected before a vendor
140
+ process starts.
92
141
 
93
142
  ## Commands
94
143
 
@@ -114,6 +163,11 @@ bun test tests
114
163
  bun run test:e2e
115
164
  bun run test:e2e:gemini
116
165
  bun run test:e2e:gemini-tool
166
+ bun run test:e2e:gemini-multiturn
167
+ bun run test:e2e:gemini-previous-response
168
+ bun run test:e2e:gemini-websocket
169
+ bun run test:e2e:gemini-websocket-tool
170
+ bun run test:e2e:gemini-reasoning
117
171
  bun run privacy:scan
118
172
  bun run structure:check
119
173
  ```
@@ -131,19 +185,44 @@ Codex's `exec_command`, Codex executes a fixed read-only `printf`, and Gemini
131
185
  returns the final answer. Both commands use no retries. Missing binaries,
132
186
  authentication, or a Flash Lite model cause a hard failure.
133
187
 
188
+ `test:e2e:gemini-multiturn` starts a persisted real Codex session, resumes it with
189
+ `codex exec resume`, and asserts that the proxy loads the Gemini session and
190
+ sends only the new turn. Codex 0.165.0 sends `prompt_cache_key` for this path.
191
+
192
+ `test:e2e:gemini-previous-response` sends a direct Responses continuation with
193
+ `previous_response_id` and verifies that the proxy loads the stored Gemini
194
+ session and sends only the new input.
195
+
196
+ `test:e2e:gemini-websocket` and `test:e2e:gemini-websocket-tool` enable Codex's
197
+ actual WebSocket transport with `supports_websockets = true` and
198
+ `--enable responses_websockets`. They verify the `generate=false` warmup, the
199
+ real request's non-empty `previous_response_id`, and the WebSocket tool loop.
200
+
201
+ `test:e2e:gemini-reasoning` discovers a thinking-capable model and level from the
202
+ local Gemini CLI, validates the refreshed Codex catalog, and runs one minimal
203
+ reasoning-enabled turn. It fails when the authenticated account cannot execute
204
+ the discovered model.
205
+
134
206
  Optional overrides are `CLINE_BIN_PATH`, `GEMINI_BIN_PATH`, and `CODEX_BIN_PATH`.
135
207
 
136
208
  ## Troubleshooting
137
209
 
138
210
  - **Cline not found**: install `cline` and verify `cline --help` works.
139
211
  - **Cline authentication required**: run `ocx login cline-cli`, then retry.
140
- - **Cline ACP unavailable**: the adapter falls back to the one-shot JSON path when
141
- the model is `default`; native model selection requires ACP.
212
+ - **Cline ACP unavailable**: opencodex no longer falls back to the one-shot JSON
213
+ path. Upgrade Cline so `cline --help` advertises `--acp`, or set
214
+ `OPENCODEX_CLINE_ACP=1`; otherwise requests fail with
215
+ `cline_acp_unsupported`.
142
216
  - **Gemini not found**: install it and verify `gemini --help` works, or set
143
217
  `GEMINI_BIN_PATH`.
144
- - **Gemini authentication required**: configure Gemini CLI's official login or API
145
- key; do not pass the key to opencodex.
218
+ - **Gemini authentication required**: configure a Gemini Developer API key or
219
+ Vertex AI API key for Gemini CLI; do not use personal Google OAuth with this
220
+ third-party proxy and do not pass the key to opencodex.
146
221
  - **Gemini ACP unavailable**: upgrade Gemini CLI to a version advertising `--acp`.
222
+ - **Gemini Pro quota exhausted**: reasoning-capable models may require paid Pro
223
+ quota; the catalog stays accurate but that model cannot complete a live turn.
224
+ - **Codex model picker is stale**: run `ocx codex sync`, then restart the Codex
225
+ app/app-server.
147
226
  - **Port in use**: choose another port with `ocx start --port <port>`.
148
227
  - **Stale runtime state**: run `ocx stop`; it removes stale state only when the
149
228
  recorded process and health endpoint cannot be verified.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koreanhole/opencodex",
3
- "version": "2.96.0",
3
+ "version": "2.98.0",
4
4
  "description": "Cline and Gemini CLI provider proxy for OpenAI Codex and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -32,6 +32,11 @@
32
32
  "test:e2e": "bun test tests/e2e/cline-only.test.ts",
33
33
  "test:e2e:gemini": "bun test ./tests/e2e/gemini-codex.live.ts -t \"one minimal Flash Lite completion\"",
34
34
  "test:e2e:gemini-tool": "bun test ./tests/e2e/gemini-codex.live.ts -t \"one short Gemini tool call\"",
35
+ "test:e2e:gemini-multiturn": "bun test ./tests/e2e/gemini-codex.live.ts -t \"reuses a live Codex multi-turn session\"",
36
+ "test:e2e:gemini-previous-response": "bun test ./tests/e2e/gemini-codex.live.ts -t \"loads a live Gemini session for a previous_response_id continuation\"",
37
+ "test:e2e:gemini-websocket": "bun test ./tests/e2e/gemini-codex.live.ts -t \"runs a live Codex WebSocket turn\"",
38
+ "test:e2e:gemini-websocket-tool": "bun test ./tests/e2e/gemini-codex.live.ts -t \"runs a live Codex WebSocket tool loop\"",
39
+ "test:e2e:gemini-reasoning": "bun test ./tests/e2e/gemini-reasoning.live.ts",
35
40
  "typecheck": "bun check --no-script --noEmit",
36
41
  "privacy:scan": "bun scripts/privacy-scan.ts",
37
42
  "structure:check": "bun scripts/structure-check.ts",
@@ -49,9 +49,19 @@ export interface AcpAuthMethod {
49
49
  description?: string;
50
50
  }
51
51
 
52
+ export interface AcpAgentCapabilities {
53
+ loadSession: boolean;
54
+ prompt: {
55
+ image: boolean;
56
+ audio: boolean;
57
+ embeddedContext: boolean;
58
+ };
59
+ }
60
+
52
61
  export interface AcpInitialize {
53
62
  protocolVersion: number;
54
63
  authMethods: readonly AcpAuthMethod[];
64
+ capabilities: AcpAgentCapabilities;
55
65
  agentVersion?: string;
56
66
  }
57
67
 
@@ -224,7 +234,21 @@ export function parseAcpInitialize(
224
234
  : [];
225
235
  const agentInfo = asRecord(result.agentInfo);
226
236
  const agentVersion = nonEmpty(agentInfo?.version, 256);
227
- return { protocolVersion: version, authMethods, ...(agentVersion ? { agentVersion } : {}) };
237
+ const agentCapabilities = asRecord(result.agentCapabilities);
238
+ const promptCapabilities = asRecord(agentCapabilities?.promptCapabilities);
239
+ return {
240
+ protocolVersion: version,
241
+ authMethods,
242
+ capabilities: {
243
+ loadSession: agentCapabilities?.loadSession === true,
244
+ prompt: {
245
+ image: promptCapabilities?.image === true,
246
+ audio: promptCapabilities?.audio === true,
247
+ embeddedContext: promptCapabilities?.embeddedContext === true,
248
+ },
249
+ },
250
+ ...(agentVersion ? { agentVersion } : {}),
251
+ };
228
252
  }
229
253
 
230
254
  function acpError(error: unknown, fallbackAllowed: boolean, label: string): AcpError {
@@ -443,6 +467,32 @@ export class AcpClient {
443
467
  }
444
468
  }
445
469
 
470
+ async loadSession(params: {
471
+ sessionId: string;
472
+ cwd: string;
473
+ mcpServers: readonly Record<string, unknown>[];
474
+ }): Promise<AcpSessionInfo> {
475
+ try {
476
+ const response = await deadline(
477
+ this.connection.request<unknown>(acp.AGENT_METHODS.session_load, {
478
+ sessionId: params.sessionId,
479
+ cwd: params.cwd,
480
+ mcpServers: params.mcpServers.slice(0, MAX_MCP_SERVERS),
481
+ }),
482
+ this.deps.timeoutMs ?? DEFAULT_TIMEOUT_MS,
483
+ () => this.terminate(),
484
+ this.label,
485
+ this.fallbackAllowed,
486
+ );
487
+ const session = parseAcpSession(response, this.label, this.fallbackAllowed);
488
+ this.sessionId = session.sessionId;
489
+ return session;
490
+ } catch (error) {
491
+ if (error instanceof AcpError) throw error;
492
+ throw acpError(error, this.fallbackAllowed, this.label);
493
+ }
494
+ }
495
+
446
496
  async setModel(sessionId: string, modelId: string): Promise<void> {
447
497
  await deadline(
448
498
  this.connection.request("session/set_model", { sessionId, modelId }),
@@ -499,11 +549,18 @@ export class AcpClient {
499
549
  });
500
550
  }
501
551
 
502
- async prompt(sessionId: string, text: string, cancellationSignal?: AbortSignal): Promise<acp.PromptResponse> {
552
+ async prompt(
553
+ sessionId: string,
554
+ content: string | readonly acp.ContentBlock[],
555
+ cancellationSignal?: AbortSignal,
556
+ ): Promise<acp.PromptResponse> {
557
+ const prompt = typeof content === "string"
558
+ ? [{ type: "text" as const, text: content }]
559
+ : content;
503
560
  return await deadline(
504
561
  this.connection.request(
505
562
  acp.AGENT_METHODS.session_prompt,
506
- { sessionId, prompt: [{ type: "text" as const, text }] },
563
+ { sessionId, prompt },
507
564
  { ...(cancellationSignal ? { cancellationSignal } : {}) },
508
565
  ),
509
566
  this.deps.timeoutMs ?? DEFAULT_TIMEOUT_MS,
@@ -522,7 +522,6 @@ export async function runClineAcpTurn(
522
522
  let client: AcpClient | undefined;
523
523
  let session: ClineAcpSessionInfo | undefined;
524
524
  let terminalEmitted = false;
525
- let sawOutput = false;
526
525
  let safetyError: string | undefined;
527
526
  let aborted = false;
528
527
  let timedOut = false;
@@ -531,10 +530,6 @@ export async function runClineAcpTurn(
531
530
  const seenToolCalls = new Set<string>();
532
531
 
533
532
  const deliver = (event: AdapterEvent): void => {
534
- if (event.type === "text_delta" || event.type === "thinking_delta"
535
- || event.type === "tool_call_start" || event.type === "tool_call_delta" || event.type === "tool_call_end") {
536
- sawOutput = true;
537
- }
538
533
  if (event.type === "done" || event.type === "error" || event.type === "incomplete") {
539
534
  if (terminalEmitted) return;
540
535
  terminalEmitted = true;
@@ -593,7 +588,7 @@ export async function runClineAcpTurn(
593
588
  };
594
589
 
595
590
  try {
596
- client = await AcpClient.connect({ binary: input.binary, env: input.env, cwd: input.cwd, hooks, label: "Cline ACP", fallbackAllowed: true, ...(input.args ? { args: input.args } : {}) }, deps);
591
+ client = await AcpClient.connect({ binary: input.binary, env: input.env, cwd: input.cwd, hooks, label: "Cline ACP", fallbackAllowed: false, ...(input.args ? { args: input.args } : {}) }, deps);
597
592
  session = await client.newSession({ cwd: input.cwd, mcpServers: input.mcpServers });
598
593
  await applyClineAcpModel(client, session, input.modelId);
599
594
 
@@ -677,7 +672,6 @@ export async function runClineAcpTurn(
677
672
  });
678
673
  }
679
674
  } catch (error) {
680
- if (error instanceof AcpError && error.fallbackAllowed && !sawOutput) throw error;
681
675
  if (!terminalEmitted) {
682
676
  const failure = timedOut
683
677
  ? new ClineAcpError("Cline ACP turn timed out.", 504, "upstream_error", "timeout", true, false)
@@ -1,19 +1,14 @@
1
1
  import { fileURLToPath } from "node:url";
2
- import { writeFile } from "node:fs/promises";
3
- import { join } from "node:path";
4
2
  import type { AdapterEvent, IncomingMeta, OcxParsedRequest } from "../../../types";
5
3
  import { buildCodingAgentToolCatalog, CodingAgentToolCatalogError } from "../../tool-catalog";
6
4
  import { buildConversationInput, buildSystemPrompt } from "../protocol";
7
5
  import { whichFromPath, type WhichFn } from "../../coding-agent/profile";
8
6
  import { isStandaloneBinary } from "../../../lib/standalone";
9
- import { runCliAgentTurn } from "../turn";
10
7
  import type { CliAgentTurnDeps } from "../types";
11
8
  import {
12
9
  assertClineAdvisoryDestinationSafe,
13
10
  buildClineChildEnv,
14
11
  ClineSettingsError,
15
- mergeClineDisabledTools,
16
- preflightClineTools,
17
12
  resolveClineProviderSettingsPath,
18
13
  stageClineTurn,
19
14
  type ClineStagedTurn,
@@ -23,12 +18,9 @@ import {
23
18
  buildClineAcpEnv,
24
19
  probeClineAcpSupport,
25
20
  runClineAcpTurn,
26
- stageClineAcpWorkspace,
27
21
  type ClineAcpDeps,
28
- type ClineAcpMode,
29
22
  type ClineAcpTurnInput,
30
23
  } from "./acp";
31
- import { classifyClineFailure, createClineStreamState, mapClineMessage } from "./stream";
32
24
  import { AcpError } from "../acp-client";
33
25
 
34
26
  export const CLINE_CLI_BASE_URL = "https://api.cline.bot";
@@ -50,9 +42,7 @@ export interface ClineTurnDeps extends CliAgentTurnDeps {
50
42
  env?: NodeJS.ProcessEnv;
51
43
  providerSettingsPath?: string;
52
44
  stageTurn?: typeof stageClineTurn;
53
- preflightTools?: typeof preflightClineTools;
54
45
  probeAcp?: typeof probeClineAcpSupport;
55
- stageAcp?: typeof stageClineAcpWorkspace;
56
46
  runAcp?: (input: ClineAcpTurnInput, emit: (event: AdapterEvent) => void, deps?: ClineAcpDeps) => Promise<void>;
57
47
  acpDeps?: ClineAcpDeps;
58
48
  }
@@ -92,10 +82,6 @@ export function buildClinePrompt(
92
82
  ].filter(Boolean).join("\n\n");
93
83
  }
94
84
 
95
- export function buildClineArgs(): string[] {
96
- return ["--json", "Follow the piped conversation and instructions."];
97
- }
98
-
99
85
  function toolNameMap(catalog: ReturnType<typeof buildCodingAgentToolCatalog>): Map<string, string> {
100
86
  const names = new Map<string, string>();
101
87
  for (const [vendorName, wireName] of catalog.emittedNameMap) {
@@ -192,12 +178,14 @@ export async function runClineTurn(
192
178
  return;
193
179
  }
194
180
 
195
- const vendorNames = toolNameMap(catalog);
196
- const vendorNameByWire = new Map([...catalog.emittedNameMap.values()].map(wireName => [
197
- wireName,
198
- wireName.startsWith("opencodex__") ? wireName : `opencodex__${wireName}`,
199
- ]));
200
- const fixedSystem = systemPromptWithToolNames(catalog);
181
+ const providerSettingsPath = deps.providerSettingsPath ?? resolveClineProviderSettingsPath(sourceEnv);
182
+ try {
183
+ await assertClineAdvisoryDestinationSafe(providerSettingsPath);
184
+ } catch (error) {
185
+ emit(settingsError(error));
186
+ return;
187
+ }
188
+
201
189
  const acpDeps: ClineAcpDeps = {
202
190
  ...deps.acpDeps,
203
191
  env: sourceEnv,
@@ -209,153 +197,68 @@ export async function runClineTurn(
209
197
  ...(deps.killWindowsProcessTree ? { killWindowsProcessTree: deps.killWindowsProcessTree } : {}),
210
198
  };
211
199
  const acpEnv = buildClineAcpEnv(sourceEnv);
212
- let acpMode: ClineAcpMode = { acp: false };
200
+ let acpMode: { acp: boolean; autoApproveFlag?: boolean } = { acp: false };
213
201
  try {
214
202
  acpMode = await (deps.probeAcp ?? probeClineAcpSupport)(binary, acpEnv, acpDeps);
215
203
  } catch {
216
204
  acpMode = { acp: false };
217
205
  }
218
-
219
- if (acpMode.acp) {
220
- let workspace: Awaited<ReturnType<typeof stageClineAcpWorkspace>> | undefined;
221
- try {
222
- workspace = await (deps.stageAcp ?? stageClineAcpWorkspace)(catalog.tools);
223
- const mcpServers = workspace.catalogPath && catalog.emittedNameMap.size > 0
224
- ? [{
225
- type: "stdio",
226
- name: CLINE_MCP_SERVER_NAME,
227
- command: process.execPath,
228
- args: isStandaloneBinary() ? ["__cli-agent-mcp", workspace.catalogPath] : [CLINE_MCP_SERVER_PATH, workspace.catalogPath],
229
- env: [],
230
- }]
231
- : [];
232
- let acpOutput = false;
233
- const acpEmit = (event: AdapterEvent): void => {
234
- if (event.type === "text_delta" || event.type === "thinking_delta"
235
- || event.type === "tool_call_start" || event.type === "tool_call_delta" || event.type === "tool_call_end") {
236
- acpOutput = true;
237
- }
238
- emit(event);
239
- };
240
- try {
241
- await (deps.runAcp ?? runClineAcpTurn)({
242
- binary,
243
- env: acpEnv,
244
- cwd: workspace.cwd,
245
- modelId: requestedModel,
246
- prompt: buildClinePrompt(parsed, vendorNameByWire, fixedSystem),
247
- toolNames: vendorNames,
248
- mcpServers,
249
- args: acpMode.autoApproveFlag ? ["--acp", "--auto-approve", "false"] : ["--acp"],
250
- maxToolCalls: 16,
251
- requireToolCall: catalog.requireToolCall,
252
- incoming,
253
- deps: acpDeps,
254
- }, acpEmit, acpDeps);
255
- return;
256
- } catch (error) {
257
- if (!(error instanceof AcpError && error.fallbackAllowed && !acpOutput && requestedModel === "default")) {
258
- emit(acpFailure(error));
259
- return;
260
- }
261
- if (error.authRequired) {
262
- console.warn("[opencodex] Cline ACP authentication required [code=acp_auth_required]; using one-shot fallback.");
263
- } else {
264
- console.warn("[opencodex] Cline ACP unavailable; using one-shot fallback.");
265
- }
266
- }
267
- } finally {
268
- await workspace?.cleanup();
269
- }
270
- }
271
-
272
- if (requestedModel !== CLINE_CLI_MODEL_ID) {
273
- emit({ type: "error", message: `Cline ACP is required to select model "${requestedModel}". Enable ACP or use the default model.`, status: 400, errorType: "invalid_request_error", code: "cline_model_unsupported", retryable: false });
274
- return;
275
- }
276
-
277
- const providerSettingsPath = deps.providerSettingsPath ?? resolveClineProviderSettingsPath(sourceEnv);
278
- try {
279
- await assertClineAdvisoryDestinationSafe(providerSettingsPath);
280
- } catch (error) {
281
- emit(settingsError(error));
282
- return;
283
- }
284
-
285
- const mcpLaunch = catalog.emittedNameMap.size > 0
286
- ? (isStandaloneBinary()
287
- ? { kind: "entrypoint" as const, entrypoint: "__cli-agent-mcp" }
288
- : { kind: "module" as const, modulePath: CLINE_MCP_SERVER_PATH })
289
- : undefined;
290
- let staged: ClineStagedTurn;
291
- try {
292
- staged = await (deps.stageTurn ?? stageClineTurn)({
293
- providerSettingsPath,
294
- catalog: catalog.tools,
295
- ...(mcpLaunch ? { mcpLaunch } : {}),
296
- });
297
- } catch (error) {
298
- emit(settingsError(error));
299
- return;
300
- }
301
-
302
- const env = buildClineChildEnv(staged, sourceEnv);
303
- const preflightTools = deps.preflightTools ?? preflightClineTools;
304
- let preflight = await preflightTools({ binary, env, cwd: staged.workspaceDir });
305
- if (!preflight.ok && preflight.enabledTools && preflight.enabledTools.length > 0) {
306
- try {
307
- await mergeClineDisabledTools(staged, preflight.enabledTools.map(tool => tool.id));
308
- } catch (error) {
309
- await staged.cleanup();
310
- emit(settingsError(error));
311
- return;
312
- }
313
- preflight = await preflightTools({ binary, env, cwd: staged.workspaceDir });
314
- }
315
- if (!preflight.ok) {
316
- await staged.cleanup();
206
+ if (!acpMode.acp) {
317
207
  emit({
318
208
  type: "error",
319
- message: preflight.detail || "Cline tool preflight failed.",
320
- status: 502,
321
- errorType: "upstream_error",
322
- code: preflight.unsupported ? "cline_tool_preflight_unsupported" : "cline_tool_preflight_failed",
209
+ message: "The installed Cline CLI does not advertise --acp. Upgrade Cline or enable ACP.",
210
+ status: 400,
211
+ errorType: "invalid_request_error",
212
+ code: "cline_acp_unsupported",
323
213
  retryable: false,
324
214
  });
325
215
  return;
326
216
  }
327
217
 
328
- const state = createClineStreamState();
329
- const promptFile = join(staged.root, "prompt.txt");
218
+ const vendorNames = toolNameMap(catalog);
219
+ const vendorNameByWire = new Map([...catalog.emittedNameMap.values()].map(wireName => [
220
+ wireName,
221
+ wireName.startsWith("opencodex__") ? wireName : `opencodex__${wireName}`,
222
+ ]));
223
+ const fixedSystem = systemPromptWithToolNames(catalog);
224
+ let staged: ClineStagedTurn | undefined;
330
225
  try {
331
- await writeFile(promptFile, buildClinePrompt(parsed, vendorNameByWire, fixedSystem), {
332
- encoding: "utf8",
333
- mode: 0o600,
334
- flag: "wx",
226
+ staged = await (deps.stageTurn ?? stageClineTurn)({
227
+ providerSettingsPath,
228
+ catalog: catalog.tools,
335
229
  });
336
- } catch {
337
- await staged.cleanup();
338
- emit({ type: "error", message: "Cline prompt could not be staged securely.", status: 500, errorType: "upstream_error", code: "cli_prompt_staging_failed", retryable: false });
339
- return;
340
- }
341
-
342
- await runCliAgentTurn({
343
- label: "Cline CLI",
344
- binary,
345
- args: buildClineArgs(),
346
- env,
347
- cwd: staged.workspaceDir,
348
- stdin: "",
349
- stdinFile: promptFile,
350
- state,
351
- requireToolCall: catalog.requireToolCall,
352
- classifyFailure: ({ exitCode, stderr }) => classifyClineFailure(exitCode, stderr),
353
- parseLine: (message, current) => mapClineMessage(message, current, {
230
+ const env = buildClineChildEnv(staged, sourceEnv);
231
+ const mcpServers = catalog.emittedNameMap.size > 0
232
+ ? [{
233
+ type: "stdio",
234
+ name: CLINE_MCP_SERVER_NAME,
235
+ command: process.execPath,
236
+ args: isStandaloneBinary()
237
+ ? ["__cli-agent-mcp", staged.catalogPath]
238
+ : [CLINE_MCP_SERVER_PATH, staged.catalogPath],
239
+ env: [],
240
+ }]
241
+ : [];
242
+ await (deps.runAcp ?? runClineAcpTurn)({
243
+ binary,
244
+ env,
245
+ cwd: staged.workspaceDir,
246
+ modelId: requestedModel,
247
+ prompt: buildClinePrompt(parsed, vendorNameByWire, fixedSystem),
354
248
  toolNames: vendorNames,
249
+ mcpServers,
250
+ args: acpMode.autoApproveFlag ? ["--acp", "--auto-approve", "false"] : ["--acp"],
355
251
  maxToolCalls: 16,
356
- }),
357
- cleanup: staged.cleanup,
358
- }, incoming, emit, deps);
252
+ requireToolCall: catalog.requireToolCall,
253
+ incoming,
254
+ deps: acpDeps,
255
+ }, emit, acpDeps);
256
+ } catch (error) {
257
+ if (error instanceof ClineSettingsError) emit(settingsError(error));
258
+ else emit(acpFailure(error));
259
+ } finally {
260
+ await staged?.cleanup();
261
+ }
359
262
  }
360
263
 
361
264
  export { resolveClineBinary } from "./binary";