@koreanhole/opencodex 2.97.0 → 2.99.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 +12 -2
- package/README.md +83 -5
- package/package.json +6 -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/cli/index.ts +83 -15
- package/src/lib/codex-catalog.ts +107 -13
- package/src/lib/codex-inject.ts +467 -0
- 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
|
@@ -29,6 +29,10 @@ ocx start
|
|
|
29
29
|
ocx codex sync
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
`ocx start` journals the native Codex config and injects the managed routing.
|
|
33
|
+
`ocx stop` restores it. Use `ocx restore`/`ocx eject` to restore while keeping
|
|
34
|
+
the proxy alive, and `ocx restore back` to re-enable routing.
|
|
35
|
+
|
|
32
36
|
`ocx codex sync` refreshes the Codex app catalog with locally discovered Gemini
|
|
33
37
|
models and reasoning levels. Restart the Codex app/app-server after refresh. The
|
|
34
38
|
catalog file is written only when `model_catalog_json` names an opencodex catalog
|
|
@@ -37,10 +41,12 @@ or through the documented default path; unrelated Codex settings are not changed
|
|
|
37
41
|
## Credential rules
|
|
38
42
|
|
|
39
43
|
- 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.
|
|
44
|
+
tokens into config, environment files, logs, prompts, or package content. The
|
|
45
|
+
only exception is Cline's mode-0600 request-scoped staging copy.
|
|
41
46
|
- Do not authenticate on a request path. If Cline reports `auth_required`, stop
|
|
42
47
|
and ask the operator to run `ocx login cline-cli`.
|
|
43
|
-
- Do not edit the operator's real Cline provider store
|
|
48
|
+
- Do not edit the operator's real Cline provider store. Read it only to create a
|
|
49
|
+
mode-0600 request-scoped copy; remove that copy after the turn.
|
|
44
50
|
- Do not import or replay Gemini OAuth/API-key material; the Gemini CLI remains
|
|
45
51
|
the authentication authority.
|
|
46
52
|
- Third-party use must use a Gemini Developer API key or Vertex AI GenAI API key.
|
|
@@ -55,6 +61,9 @@ or through the documented default path; unrelated Codex settings are not changed
|
|
|
55
61
|
```bash
|
|
56
62
|
ocx status
|
|
57
63
|
ocx stop
|
|
64
|
+
ocx restore [--json] [--remove-codex-provider-table]
|
|
65
|
+
ocx restore back [--json]
|
|
66
|
+
ocx eject
|
|
58
67
|
```
|
|
59
68
|
|
|
60
69
|
The default listener is loopback-only. A non-loopback bind is refused unless a
|
|
@@ -71,6 +80,7 @@ bun test tests/e2e/cline-only.test.ts
|
|
|
71
80
|
bun run test:e2e:gemini
|
|
72
81
|
bun run test:e2e:gemini-tool
|
|
73
82
|
bun run test:e2e:gemini-reasoning
|
|
83
|
+
bun run test:e2e:codex-restore
|
|
74
84
|
```
|
|
75
85
|
|
|
76
86
|
Report whether each evidence path used a deterministic ACP fixture or a real
|
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.
|
|
@@ -51,7 +55,15 @@ Cline-only configs load unchanged and keep Cline as the default.
|
|
|
51
55
|
|
|
52
56
|
## Codex
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
`ocx start` now records a mode-0600 pre-image under `$OPENCODEX_HOME/codex/`,
|
|
59
|
+
injects the loopback Responses provider, and points Codex at the live model
|
|
60
|
+
catalog. The managed block is marked and is the only block opencodex will touch.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
ocx start
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The resulting Codex config is equivalent to:
|
|
55
67
|
|
|
56
68
|
```toml
|
|
57
69
|
model = "gemini-cli/gemini-3.5-flash-lite"
|
|
@@ -65,6 +77,19 @@ requires_openai_auth = false
|
|
|
65
77
|
model_catalog_url = "http://127.0.0.1:10100/v1/model-catalog"
|
|
66
78
|
```
|
|
67
79
|
|
|
80
|
+
Switch back to native Codex without stopping the proxy:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
ocx restore # alias: ocx eject
|
|
84
|
+
ocx restore back # re-point Codex at the running proxy
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`ocx stop`, Ctrl-C/SIGTERM, and SIGHUP also restore native Codex. The restore
|
|
88
|
+
covers Codex config and the opencodex catalog only; resume-history metadata is
|
|
89
|
+
not changed. A pre-existing unmarked `[model_providers.opencodex]` block is
|
|
90
|
+
treated as user-owned and refused. Remove a stale manual block first with
|
|
91
|
+
`ocx restore --remove-codex-provider-table` if needed.
|
|
92
|
+
|
|
68
93
|
The model list is live. Cline ACP models are exposed as `cline-cli/<encoded-id>`;
|
|
69
94
|
Gemini ACP models are exposed as `gemini-cli/<encoded-id>`. Each provider keeps a
|
|
70
95
|
`default` selector. A provider prefix routes only to that vendor; unprefixed
|
|
@@ -84,6 +109,20 @@ opencodex catalog, otherwise it writes `$CODEX_HOME/opencodex-catalog.json` and
|
|
|
84
109
|
prints the activation setting. `model_catalog_url` avoids the file when the app
|
|
85
110
|
catalog is fetched dynamically.
|
|
86
111
|
|
|
112
|
+
Codex 0.165.0 can use the Responses WebSocket transport when the provider sets
|
|
113
|
+
`supports_websockets = true` and the process is started with
|
|
114
|
+
`--enable responses_websockets`. opencodex accepts the WebSocket on
|
|
115
|
+
`/v1/responses`, handles the `generate=false` warmup, and maps the following
|
|
116
|
+
`previous_response_id` onto the stored Gemini session. HTTP remains the fallback.
|
|
117
|
+
|
|
118
|
+
Gemini context capacity, prompt modalities, and session-load support are
|
|
119
|
+
discovered from the installed ACP agent at runtime; the proxy does not hardcode a
|
|
120
|
+
model context window. A reusable Codex conversation uses
|
|
121
|
+
`previous_response_id`, `prompt_cache_key`, or a documented conversation header
|
|
122
|
+
to load the vendor session and send only the new input. Tool-use turns fall back
|
|
123
|
+
to bounded replay because the vendor session is cancelled when a tool intent is
|
|
124
|
+
captured.
|
|
125
|
+
|
|
87
126
|
## Claude Code
|
|
88
127
|
|
|
89
128
|
```bash
|
|
@@ -98,18 +137,31 @@ Use a `cline-cli/...` model to route the same Messages surface to Cline.
|
|
|
98
137
|
## Security boundary
|
|
99
138
|
|
|
100
139
|
- Neither Cline nor Gemini receives filesystem or terminal capability from the proxy.
|
|
140
|
+
- Cline turns use a request-scoped Cline home with built-in/plugin tools, hooks,
|
|
141
|
+
telemetry, updates, and automatic approval disabled. The real vendor provider
|
|
142
|
+
store is immutable; a mode-0600 copy inside the staged home lets the vendor CLI
|
|
143
|
+
refresh auth state and is removed after the turn.
|
|
144
|
+
- Codex restore uses a mode-0600 journal under `$OPENCODEX_HOME/codex/`. The
|
|
145
|
+
journal contains the pre-image needed to reverse only opencodex-managed config
|
|
146
|
+
and catalog changes; it is never logged and is removed after a successful restore.
|
|
101
147
|
- Gemini native tools are removed by a per-turn deny policy; permission requests
|
|
102
148
|
are cancelled, and no ACP filesystem/terminal client capabilities are advertised.
|
|
103
149
|
User MCP servers, extensions, skills, and hooks are disabled for the session; only
|
|
104
150
|
the isolated `opencodex` MCP catalog can receive a client tool call.
|
|
151
|
+
- Gemini runs with a staged `GEMINI_CLI_HOME` under opencodex control; only the
|
|
152
|
+
minimum vendor auth files are linked from the real home, and staged session
|
|
153
|
+
state is bounded and reclaimed.
|
|
105
154
|
- Declared client tools are exposed only through a request-scoped MCP process that
|
|
106
155
|
records bounded intent and never executes; Codex owns approval and execution.
|
|
107
156
|
- Vendor credentials remain in the vendor's real home or authenticated
|
|
108
|
-
environment.
|
|
157
|
+
environment. Cline's request-scoped copy is never logged or persisted beyond
|
|
158
|
+
the turn. The proxy stores no vendor token.
|
|
109
159
|
- Request-path traffic never starts an interactive login.
|
|
110
160
|
- The listener is loopback-only by default; a non-loopback listener requires an
|
|
111
161
|
explicit API token.
|
|
112
|
-
- Image
|
|
162
|
+
- Image input is forwarded only when the selected Gemini model and ACP agent
|
|
163
|
+
advertise image capability; video and audio input are rejected before a vendor
|
|
164
|
+
process starts.
|
|
113
165
|
|
|
114
166
|
## Commands
|
|
115
167
|
|
|
@@ -121,6 +173,9 @@ bun run src/cli/index.ts provider list [--json]
|
|
|
121
173
|
bun run src/cli/index.ts start [--port 10100] [--host 127.0.0.1]
|
|
122
174
|
bun run src/cli/index.ts status [--json]
|
|
123
175
|
bun run src/cli/index.ts stop [--json]
|
|
176
|
+
bun run src/cli/index.ts restore [--json] [--remove-codex-provider-table]
|
|
177
|
+
bun run src/cli/index.ts restore back [--json]
|
|
178
|
+
bun run src/cli/index.ts eject
|
|
124
179
|
```
|
|
125
180
|
|
|
126
181
|
Gemini has no proxy login command. Authenticate it through the official CLI
|
|
@@ -135,7 +190,12 @@ bun test tests
|
|
|
135
190
|
bun run test:e2e
|
|
136
191
|
bun run test:e2e:gemini
|
|
137
192
|
bun run test:e2e:gemini-tool
|
|
193
|
+
bun run test:e2e:gemini-multiturn
|
|
194
|
+
bun run test:e2e:gemini-previous-response
|
|
195
|
+
bun run test:e2e:gemini-websocket
|
|
196
|
+
bun run test:e2e:gemini-websocket-tool
|
|
138
197
|
bun run test:e2e:gemini-reasoning
|
|
198
|
+
bun run test:e2e:codex-restore
|
|
139
199
|
bun run privacy:scan
|
|
140
200
|
bun run structure:check
|
|
141
201
|
```
|
|
@@ -153,6 +213,19 @@ Codex's `exec_command`, Codex executes a fixed read-only `printf`, and Gemini
|
|
|
153
213
|
returns the final answer. Both commands use no retries. Missing binaries,
|
|
154
214
|
authentication, or a Flash Lite model cause a hard failure.
|
|
155
215
|
|
|
216
|
+
`test:e2e:gemini-multiturn` starts a persisted real Codex session, resumes it with
|
|
217
|
+
`codex exec resume`, and asserts that the proxy loads the Gemini session and
|
|
218
|
+
sends only the new turn. Codex 0.165.0 sends `prompt_cache_key` for this path.
|
|
219
|
+
|
|
220
|
+
`test:e2e:gemini-previous-response` sends a direct Responses continuation with
|
|
221
|
+
`previous_response_id` and verifies that the proxy loads the stored Gemini
|
|
222
|
+
session and sends only the new input.
|
|
223
|
+
|
|
224
|
+
`test:e2e:gemini-websocket` and `test:e2e:gemini-websocket-tool` enable Codex's
|
|
225
|
+
actual WebSocket transport with `supports_websockets = true` and
|
|
226
|
+
`--enable responses_websockets`. They verify the `generate=false` warmup, the
|
|
227
|
+
real request's non-empty `previous_response_id`, and the WebSocket tool loop.
|
|
228
|
+
|
|
156
229
|
`test:e2e:gemini-reasoning` discovers a thinking-capable model and level from the
|
|
157
230
|
local Gemini CLI, validates the refreshed Codex catalog, and runs one minimal
|
|
158
231
|
reasoning-enabled turn. It fails when the authenticated account cannot execute
|
|
@@ -164,8 +237,10 @@ Optional overrides are `CLINE_BIN_PATH`, `GEMINI_BIN_PATH`, and `CODEX_BIN_PATH`
|
|
|
164
237
|
|
|
165
238
|
- **Cline not found**: install `cline` and verify `cline --help` works.
|
|
166
239
|
- **Cline authentication required**: run `ocx login cline-cli`, then retry.
|
|
167
|
-
- **Cline ACP unavailable**:
|
|
168
|
-
|
|
240
|
+
- **Cline ACP unavailable**: opencodex no longer falls back to the one-shot JSON
|
|
241
|
+
path. Upgrade Cline so `cline --help` advertises `--acp`, or set
|
|
242
|
+
`OPENCODEX_CLINE_ACP=1`; otherwise requests fail with
|
|
243
|
+
`cline_acp_unsupported`.
|
|
169
244
|
- **Gemini not found**: install it and verify `gemini --help` works, or set
|
|
170
245
|
`GEMINI_BIN_PATH`.
|
|
171
246
|
- **Gemini authentication required**: configure a Gemini Developer API key or
|
|
@@ -176,6 +251,9 @@ Optional overrides are `CLINE_BIN_PATH`, `GEMINI_BIN_PATH`, and `CODEX_BIN_PATH`
|
|
|
176
251
|
quota; the catalog stays accurate but that model cannot complete a live turn.
|
|
177
252
|
- **Codex model picker is stale**: run `ocx codex sync`, then restart the Codex
|
|
178
253
|
app/app-server.
|
|
254
|
+
- **Codex provider conflict**: an existing unmarked
|
|
255
|
+
`[model_providers.opencodex]` was preserved. Reconcile it manually or use
|
|
256
|
+
`ocx restore --remove-codex-provider-table`, then start again.
|
|
179
257
|
- **Port in use**: choose another port with `ocx start --port <port>`.
|
|
180
258
|
- **Stale runtime state**: run `ocx stop`; it removes stale state only when the
|
|
181
259
|
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.
|
|
3
|
+
"version": "2.99.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,7 +32,12 @@
|
|
|
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",
|
|
40
|
+
"test:e2e:codex-restore": "bun test ./tests/e2e/codex-restore.live.ts",
|
|
36
41
|
"typecheck": "bun check --no-script --noEmit",
|
|
37
42
|
"privacy:scan": "bun scripts/privacy-scan.ts",
|
|
38
43
|
"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
|
-
|
|
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";
|