@koreanhole/opencodex 2.97.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
@@ -37,10 +37,12 @@ or through the documented default path; unrelated Codex settings are not changed
37
37
  ## Credential rules
38
38
 
39
39
  - Use the operator's existing Cline or Gemini CLI sign-in. Do not copy vendor
40
- 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.
41
42
  - Do not authenticate on a request path. If Cline reports `auth_required`, stop
42
43
  and ask the operator to run `ocx login cline-cli`.
43
- - 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.
44
46
  - Do not import or replay Gemini OAuth/API-key material; the Gemini CLI remains
45
47
  the authentication authority.
46
48
  - Third-party use must use a Gemini Developer API key or Vertex AI GenAI API key.
package/README.md CHANGED
@@ -20,6 +20,10 @@ npm install -g @google/gemini-cli
20
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.
@@ -84,6 +88,20 @@ opencodex catalog, otherwise it writes `$CODEX_HOME/opencodex-catalog.json` and
84
88
  prints the activation setting. `model_catalog_url` avoids the file when the app
85
89
  catalog is fetched dynamically.
86
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
+
87
105
  ## Claude Code
88
106
 
89
107
  ```bash
@@ -98,18 +116,28 @@ Use a `cline-cli/...` model to route the same Messages surface to Cline.
98
116
  ## Security boundary
99
117
 
100
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.
101
123
  - Gemini native tools are removed by a per-turn deny policy; permission requests
102
124
  are cancelled, and no ACP filesystem/terminal client capabilities are advertised.
103
125
  User MCP servers, extensions, skills, and hooks are disabled for the session; only
104
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.
105
130
  - Declared client tools are exposed only through a request-scoped MCP process that
106
131
  records bounded intent and never executes; Codex owns approval and execution.
107
132
  - Vendor credentials remain in the vendor's real home or authenticated
108
- 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.
109
135
  - Request-path traffic never starts an interactive login.
110
136
  - The listener is loopback-only by default; a non-loopback listener requires an
111
137
  explicit API token.
112
- - 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.
113
141
 
114
142
  ## Commands
115
143
 
@@ -135,6 +163,10 @@ bun test tests
135
163
  bun run test:e2e
136
164
  bun run test:e2e:gemini
137
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
138
170
  bun run test:e2e:gemini-reasoning
139
171
  bun run privacy:scan
140
172
  bun run structure:check
@@ -153,6 +185,19 @@ Codex's `exec_command`, Codex executes a fixed read-only `printf`, and Gemini
153
185
  returns the final answer. Both commands use no retries. Missing binaries,
154
186
  authentication, or a Flash Lite model cause a hard failure.
155
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
+
156
201
  `test:e2e:gemini-reasoning` discovers a thinking-capable model and level from the
157
202
  local Gemini CLI, validates the refreshed Codex catalog, and runs one minimal
158
203
  reasoning-enabled turn. It fails when the authenticated account cannot execute
@@ -164,8 +209,10 @@ Optional overrides are `CLINE_BIN_PATH`, `GEMINI_BIN_PATH`, and `CODEX_BIN_PATH`
164
209
 
165
210
  - **Cline not found**: install `cline` and verify `cline --help` works.
166
211
  - **Cline authentication required**: run `ocx login cline-cli`, then retry.
167
- - **Cline ACP unavailable**: the adapter falls back to the one-shot JSON path when
168
- 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`.
169
216
  - **Gemini not found**: install it and verify `gemini --help` works, or set
170
217
  `GEMINI_BIN_PATH`.
171
218
  - **Gemini authentication required**: configure a Gemini Developer API key or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koreanhole/opencodex",
3
- "version": "2.97.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,10 @@
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\"",
35
39
  "test:e2e:gemini-reasoning": "bun test ./tests/e2e/gemini-reasoning.live.ts",
36
40
  "typecheck": "bun check --no-script --noEmit",
37
41
  "privacy:scan": "bun scripts/privacy-scan.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";