@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 +4 -2
- package/README.md +51 -4
- package/package.json +5 -1
- package/src/adapters/cli-agent/acp-client.ts +60 -3
- package/src/adapters/cli-agent/cline/acp.ts +1 -7
- package/src/adapters/cli-agent/cline/adapter.ts +54 -151
- package/src/adapters/cli-agent/cline/settings.ts +38 -260
- package/src/adapters/cli-agent/cline/stream.ts +2 -216
- package/src/adapters/cli-agent/gemini/acp.ts +141 -9
- package/src/adapters/cli-agent/gemini/adapter.ts +179 -16
- package/src/adapters/cli-agent/gemini/live-models.ts +32 -3
- package/src/adapters/cli-agent/gemini/model-metadata.ts +100 -4
- package/src/adapters/cli-agent/gemini/runtime.ts +183 -0
- package/src/adapters/cli-agent/gemini/session-store.ts +202 -0
- package/src/adapters/cli-agent/protocol.ts +60 -9
- package/src/adapters/cli-agent/types.ts +1 -22
- package/src/lib/codex-catalog.ts +46 -13
- package/src/server/index.ts +30 -3
- package/src/server/responses.ts +91 -9
- package/src/server/websocket.ts +261 -0
- package/src/types.ts +12 -1
- package/src/adapters/cli-agent/turn.ts +0 -307
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
|
|
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.
|
|
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
|
|
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**:
|
|
168
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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:
|
|
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:
|
|
320
|
-
status:
|
|
321
|
-
errorType: "
|
|
322
|
-
code:
|
|
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
|
|
329
|
-
const
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
flag: "wx",
|
|
226
|
+
staged = await (deps.stageTurn ?? stageClineTurn)({
|
|
227
|
+
providerSettingsPath,
|
|
228
|
+
catalog: catalog.tools,
|
|
335
229
|
});
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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";
|