@koreanhole/opencodex 3.2.8 → 3.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +8 -6
- package/README.md +17 -9
- package/package.json +3 -3
- package/src/adapter-runtime/config.ts +3 -3
- package/src/adapters/cli-agent/cline/adapter.ts +40 -27
- package/src/adapters/cli-agent/cline/json.ts +13 -24
- package/src/adapters/cli-agent/cline/settings.ts +75 -2
- package/src/adapters/cli-agent/cline/stream.ts +118 -0
- package/src/adapters/cli-agent/gemini/live-models.ts +5 -2
- package/src/adapters/cli-agent/types.ts +2 -0
- package/src/lib/codex-catalog.ts +7 -5
- package/src/server/index.ts +1 -1
- package/src/server/responses.ts +12 -4
package/AGENTS_INSTALL.md
CHANGED
|
@@ -48,8 +48,9 @@ or through the documented default path; unrelated Codex settings are not changed
|
|
|
48
48
|
only exception is Cline's mode-0600 request-scoped staging copy.
|
|
49
49
|
- Do not authenticate on a request path. If Cline reports `auth_required`, stop
|
|
50
50
|
and ask the operator to run `ocx login cline-cli`.
|
|
51
|
-
- Do not edit the operator's real Cline provider store
|
|
52
|
-
|
|
51
|
+
- Do not edit the operator's real Cline provider store or legacy
|
|
52
|
+
`globalState.json`/`secrets.json`. Read them only to create mode-0600
|
|
53
|
+
request-scoped copies; remove those copies after the turn.
|
|
53
54
|
- Do not import or replay Gemini OAuth/API-key material; the Gemini CLI remains
|
|
54
55
|
the authentication authority.
|
|
55
56
|
- Gemini headless turns support explicit Gemini Developer API, Vertex AI GenAI,
|
|
@@ -88,9 +89,9 @@ Before claiming the proxy works, run:
|
|
|
88
89
|
```bash
|
|
89
90
|
bun run verify:upstream
|
|
90
91
|
bun run typecheck
|
|
91
|
-
bun test
|
|
92
|
+
bun run test:e2e
|
|
93
|
+
bun run test:e2e:cline-live
|
|
92
94
|
bun run test:e2e:gemini
|
|
93
|
-
bun run test:e2e:gemini-oauth
|
|
94
95
|
bun run test:e2e:gemini-tool
|
|
95
96
|
bun run test:e2e:gemini-reasoning
|
|
96
97
|
bun run test:e2e:codex-restore
|
|
@@ -99,6 +100,7 @@ bun run test:e2e:codex-restore
|
|
|
99
100
|
Report whether each evidence path used a deterministic fixture or a real
|
|
100
101
|
vendor binary/account. The Gemini command above is a hard live gate: missing
|
|
101
102
|
binaries, authentication, a Flash Lite model, or cleanup evidence must fail.
|
|
102
|
-
The
|
|
103
|
-
|
|
103
|
+
The Cline live command is also a hard gate: missing binary, authentication,
|
|
104
|
+
response, or cleanup evidence must fail. The 2.2.0 HTTP fixture must never be
|
|
105
|
+
reported as a real vendor run.
|
|
104
106
|
Never report a skipped or synthetic run as a real login.
|
package/README.md
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
This distribution keeps the complete original
|
|
4
4
|
[`@bitkyc08/opencodex`](https://github.com/lidge-jun/opencodex) runtime and adds
|
|
5
5
|
locally installed Cline CLI and Gemini CLI as model providers. Cline turns use
|
|
6
|
-
the CLI's one-shot `--json` mode after a runtime `--help` capability check
|
|
7
|
-
|
|
6
|
+
the CLI's one-shot `--json` mode after a runtime `--help` capability check and
|
|
7
|
+
support both the Cline 2.2.0 raw-message contract and the current structured
|
|
8
|
+
contract; Gemini turns use the CLI's one-shot `-p` stream-json mode.
|
|
8
9
|
|
|
9
10
|
The original package is an exact npm dependency. This repository does not copy,
|
|
10
11
|
patch, delete, or rewrite upstream source files. The `ocx` entrypoint loads the
|
|
@@ -37,6 +38,9 @@ does not automate Google OAuth. OAuth reuse requires an account and tier eligibl
|
|
|
37
38
|
for the installed Gemini CLI's service; explicit API-key or Vertex environment
|
|
38
39
|
configuration takes precedence over automatic OAuth selection.
|
|
39
40
|
|
|
41
|
+
Cline 2.2.0 is the minimum supported release. Newer options are used only when
|
|
42
|
+
the installed binary advertises them.
|
|
43
|
+
|
|
40
44
|
## Start
|
|
41
45
|
|
|
42
46
|
```bash
|
|
@@ -51,7 +55,8 @@ through the same `ocx` command.
|
|
|
51
55
|
|
|
52
56
|
The managed provider rows appear as `gemini-cli/...` and `cline-cli/...`.
|
|
53
57
|
Cline model discovery and explicit login use ACP, while Cline model turns use
|
|
54
|
-
the installed CLI's JSON transport
|
|
58
|
+
the installed CLI's JSON transport across the 2.2.0 and current output contracts.
|
|
59
|
+
Gemini runtime model discovery uses the
|
|
55
60
|
installed CLI's ACP session, while reasoning metadata and model turns use the
|
|
56
61
|
installed CLI's headless transport. Parent-declared tools are exposed through a
|
|
57
62
|
request-scoped capture MCP server.
|
|
@@ -131,8 +136,9 @@ the blocker.
|
|
|
131
136
|
- Cline does not receive filesystem or terminal capability from the proxy.
|
|
132
137
|
- Cline runs with request-scoped, mode-0600 settings. Its built-in tools, user
|
|
133
138
|
MCP servers, hooks, plugins, telemetry, updates, and self-approval are disabled.
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
Modern provider state and legacy global/secrets state are copied privately;
|
|
140
|
+
the originals are never modified. Its turn options are used only after the
|
|
141
|
+
installed binary's `--help` output advertises them.
|
|
136
142
|
- Gemini headless turns run in a request-scoped workspace with
|
|
137
143
|
`GEMINI_CLI_TRUST_WORKSPACE=true`, `--skip-trust`, and
|
|
138
144
|
`--approval-mode auto_edit`, but the generated isolated Gemini home settings
|
|
@@ -159,8 +165,8 @@ bun run verify:upstream
|
|
|
159
165
|
bun run typecheck
|
|
160
166
|
bun test tests
|
|
161
167
|
bun run test:e2e
|
|
168
|
+
bun run test:e2e:cline-live
|
|
162
169
|
bun run test:e2e:gemini
|
|
163
|
-
bun run test:e2e:gemini-oauth
|
|
164
170
|
bun run test:e2e:gemini-slow
|
|
165
171
|
bun run test:e2e:gemini-parent-tool
|
|
166
172
|
bun run privacy:scan
|
|
@@ -172,9 +178,11 @@ a real Codex client and a real authenticated Gemini CLI. It runs both the minima
|
|
|
172
178
|
completion and a 12-second pre-event slow-start regression. It fails, not skips,
|
|
173
179
|
on a missing binary, authentication, Flash Lite model, response marker, timeout,
|
|
174
180
|
or child cleanup assertion. `test:e2e:gemini-slow` runs only the slow-start case.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
installed
|
|
181
|
+
|
|
182
|
+
`test:e2e` exercises the Cline 2.2.0-compatible contract through the real HTTP
|
|
183
|
+
server. `test:e2e:cline-live` additionally requires the installed Cline CLI and
|
|
184
|
+
its existing authentication, and fails rather than skipping on a missing
|
|
185
|
+
binary, response marker, or cleanup assertion.
|
|
178
186
|
|
|
179
187
|
Optional overrides are `CLINE_BIN_PATH`, `GEMINI_BIN_PATH`, and
|
|
180
188
|
`CODEX_BIN_PATH`.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koreanhole/opencodex",
|
|
3
|
-
"version": "3.2.
|
|
4
|
-
"description": "Full opencodex runtime with Cline JSON and Gemini CLI headless provider adapters",
|
|
3
|
+
"version": "3.2.10",
|
|
4
|
+
"description": "Full opencodex runtime with Cline 2.2.0+ JSON and Gemini CLI headless provider adapters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
7
7
|
"exports": {
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"start": "bun run src/cli/entry.ts start",
|
|
31
31
|
"test": "bun test tests",
|
|
32
32
|
"test:e2e": "bun test tests/e2e/cline-only.test.ts",
|
|
33
|
+
"test:e2e:cline-live": "bun test ./tests/e2e/cline-live.live.ts",
|
|
33
34
|
"test:e2e:gemini": "bun test ./tests/e2e/gemini-codex.live.ts -t \"full-stack:\"",
|
|
34
|
-
"test:e2e:gemini-oauth": "bun test ./tests/e2e/gemini-codex.live.ts -t \"uses an existing Gemini CLI OAuth login without an API key\"",
|
|
35
35
|
"test:e2e:gemini-slow": "bun test ./tests/e2e/gemini-codex.live.ts -t \"full-stack: a slow Gemini start\"",
|
|
36
36
|
"test:e2e:gemini-tool": "bun test ./tests/e2e/gemini-codex.live.ts -t \"one short Gemini turn leaves the workspace unchanged\"",
|
|
37
37
|
"test:e2e:gemini-multiturn": "bun test ./tests/e2e/gemini-codex.live.ts -t \"reuses a live Codex multi-turn session\"",
|
|
@@ -188,8 +188,8 @@ function isLegacyProviderRow(name: AdapterProviderId, value: unknown): boolean {
|
|
|
188
188
|
return row?.adapter === name;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
function uniqueModels(metadata: AdapterModelMetadata | undefined): string[] {
|
|
192
|
-
return [...new Set(["default", ...(metadata?.models ?? [])].filter(Boolean))];
|
|
191
|
+
function uniqueModels(name: AdapterProviderId, metadata: AdapterModelMetadata | undefined): string[] {
|
|
192
|
+
return [...new Set([...(name === "cline-cli" ? ["default"] : []), ...(metadata?.models ?? [])].filter(Boolean))];
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
export function buildManagedProviderRow(
|
|
@@ -208,7 +208,7 @@ export function buildManagedProviderRow(
|
|
|
208
208
|
allowPrivateNetwork: true,
|
|
209
209
|
liveModels: true,
|
|
210
210
|
defaultModel: "default",
|
|
211
|
-
models: uniqueModels(metadata),
|
|
211
|
+
models: uniqueModels(name, metadata),
|
|
212
212
|
...(metadata.modelDisplayNames ? { modelDisplayNames: metadata.modelDisplayNames } : {}),
|
|
213
213
|
...(metadata.modelContextWindows ? { modelContextWindows: metadata.modelContextWindows } : {}),
|
|
214
214
|
...(metadata.modelInputModalities ? { modelInputModalities: metadata.modelInputModalities } : {}),
|
|
@@ -102,12 +102,23 @@ function toolNameMap(catalog: ReturnType<typeof buildCodingAgentToolCatalog>): M
|
|
|
102
102
|
return names;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
function systemPromptWithToolNames(
|
|
106
|
-
|
|
105
|
+
function systemPromptWithToolNames(
|
|
106
|
+
catalog: ReturnType<typeof buildCodingAgentToolCatalog>,
|
|
107
|
+
legacy: boolean,
|
|
108
|
+
): string {
|
|
107
109
|
const mappings = [...catalog.emittedNameMap]
|
|
108
|
-
.map(([vendorName, wireName]) =>
|
|
110
|
+
.map(([vendorName, wireName]) => legacy
|
|
111
|
+
? `- ${wireName}: call server opencodex tool ${wireName}`
|
|
112
|
+
: `- ${wireName}: call ${vendorName.replace(/^mcp__/, "")}`)
|
|
109
113
|
.join("\n");
|
|
110
|
-
|
|
114
|
+
const legacyCompletion = legacy
|
|
115
|
+
? "Cline 2.2.0 requires the final answer in an attempt_completion tool call. This completion call is an exception to the unavailable built-in tools rule; it only ends the turn and performs no external action."
|
|
116
|
+
: "";
|
|
117
|
+
return [
|
|
118
|
+
CLINE_SYSTEM_PROMPT,
|
|
119
|
+
legacyCompletion,
|
|
120
|
+
...(mappings ? ["External tools in client instructions map to these exact callable tools:", mappings] : []),
|
|
121
|
+
].filter(Boolean).join("\n");
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
function failure(error: unknown): Extract<AdapterEvent, { type: "error" }> {
|
|
@@ -191,7 +202,7 @@ export async function runClineTurn(
|
|
|
191
202
|
wireName,
|
|
192
203
|
wireName.startsWith("opencodex__") ? wireName : `opencodex__${wireName}`,
|
|
193
204
|
]));
|
|
194
|
-
const fixedSystem = systemPromptWithToolNames(catalog);
|
|
205
|
+
const fixedSystem = systemPromptWithToolNames(catalog, !capabilities.toolCatalogJson);
|
|
195
206
|
const actualPrompt = buildClinePrompt(parsed, vendorNameByWire, fixedSystem);
|
|
196
207
|
let args: string[];
|
|
197
208
|
try {
|
|
@@ -215,34 +226,36 @@ export async function runClineTurn(
|
|
|
215
226
|
...(catalog.emittedNameMap.size > 0 ? { mcpLaunch: mcpLaunch() } : {}),
|
|
216
227
|
});
|
|
217
228
|
const env = buildClineChildEnv(staged, sourceEnv);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
env,
|
|
222
|
-
cwd: staged.workspaceDir,
|
|
223
|
-
capabilities,
|
|
224
|
-
deps,
|
|
225
|
-
});
|
|
226
|
-
if (!tools.ok && tools.enabledTools && tools.enabledTools.length > 0) {
|
|
227
|
-
await mergeClineDisabledTools(staged, tools.enabledTools.map(tool => tool.id));
|
|
228
|
-
tools = await preflight({
|
|
229
|
+
if (capabilities.toolCatalogJson) {
|
|
230
|
+
const preflight = deps.preflightTools ?? preflightClineTools;
|
|
231
|
+
let tools = await preflight({
|
|
229
232
|
binary,
|
|
230
233
|
env,
|
|
231
234
|
cwd: staged.workspaceDir,
|
|
232
235
|
capabilities,
|
|
233
236
|
deps,
|
|
234
237
|
});
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
238
|
+
if (!tools.ok && tools.enabledTools && tools.enabledTools.length > 0) {
|
|
239
|
+
await mergeClineDisabledTools(staged, tools.enabledTools.map(tool => tool.id));
|
|
240
|
+
tools = await preflight({
|
|
241
|
+
binary,
|
|
242
|
+
env,
|
|
243
|
+
cwd: staged.workspaceDir,
|
|
244
|
+
capabilities,
|
|
245
|
+
deps,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if (!tools.ok) {
|
|
249
|
+
emit({
|
|
250
|
+
type: "error",
|
|
251
|
+
message: tools.detail || "Cline tool preflight failed.",
|
|
252
|
+
status: 502,
|
|
253
|
+
errorType: "upstream_error",
|
|
254
|
+
code: "cline_tool_preflight_failed",
|
|
255
|
+
retryable: false,
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
246
259
|
}
|
|
247
260
|
|
|
248
261
|
const promptFile = join(staged.root, "prompt.txt");
|
|
@@ -24,7 +24,6 @@ const MAX_PREFLIGHT_STDOUT_BYTES = 2 * 1024 * 1024;
|
|
|
24
24
|
const MAX_STDERR_BYTES = 8 * 1024;
|
|
25
25
|
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
26
26
|
const DEFAULT_KILL_GRACE_MS = 2_000;
|
|
27
|
-
const DEFAULT_TOOL_BATCH_IDLE_MS = 250;
|
|
28
27
|
const MAX_TOOL_CALLS = 16;
|
|
29
28
|
|
|
30
29
|
export class ClineJsonError extends Error {
|
|
@@ -46,6 +45,7 @@ export interface ClineJsonCapabilities {
|
|
|
46
45
|
providerFlag?: "--provider";
|
|
47
46
|
modelFlag?: "--model" | "--modelid";
|
|
48
47
|
systemFlag?: "--system";
|
|
48
|
+
autoApproveFlag?: "--auto-approve";
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export interface ClineJsonTurnInput {
|
|
@@ -109,12 +109,14 @@ export function parseClineJsonHelp(
|
|
|
109
109
|
? "--modelid" as const
|
|
110
110
|
: undefined;
|
|
111
111
|
const systemFlag = optionPresent(text, "--system") ? "--system" as const : undefined;
|
|
112
|
+
const autoApproveFlag = optionPresent(text, "--auto-approve") ? "--auto-approve" as const : undefined;
|
|
112
113
|
return {
|
|
113
114
|
json,
|
|
114
115
|
toolCatalogJson: false,
|
|
115
116
|
...(providerFlag ? { providerFlag } : {}),
|
|
116
117
|
...(modelFlag ? { modelFlag } : {}),
|
|
117
118
|
...(systemFlag ? { systemFlag } : {}),
|
|
119
|
+
...(autoApproveFlag ? { autoApproveFlag } : {}),
|
|
118
120
|
};
|
|
119
121
|
}
|
|
120
122
|
|
|
@@ -240,19 +242,10 @@ export async function probeClineJsonCapabilities(
|
|
|
240
242
|
}
|
|
241
243
|
if (tools.exitCode !== 0) throw probeFailure("cline_tool_catalog_probe_failed", tools.stderr || tools.stdout);
|
|
242
244
|
const toolCatalog = parseClineJsonHelp(tools.stdout + "\n" + tools.stderr, "toolCatalog");
|
|
243
|
-
if (!toolCatalog.toolCatalogJson) {
|
|
244
|
-
throw new ClineJsonError(
|
|
245
|
-
"The installed Cline CLI does not advertise config tools --json; the tool preflight cannot run safely.",
|
|
246
|
-
400,
|
|
247
|
-
"invalid_request_error",
|
|
248
|
-
"cline_tool_catalog_unsupported",
|
|
249
|
-
false,
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
245
|
|
|
253
246
|
const capabilities: ClineJsonCapabilities = {
|
|
254
247
|
...turn,
|
|
255
|
-
toolCatalogJson:
|
|
248
|
+
toolCatalogJson: toolCatalog.toolCatalogJson,
|
|
256
249
|
};
|
|
257
250
|
if (fingerprint) CAPABILITY_CACHE.set(path, { ...fingerprint, capabilities: { ...capabilities } });
|
|
258
251
|
return capabilities;
|
|
@@ -287,6 +280,9 @@ export function buildClineJsonArgs(input: {
|
|
|
287
280
|
if (input.capabilities.systemFlag && input.systemPrompt) {
|
|
288
281
|
args.push(input.capabilities.systemFlag, input.systemPrompt);
|
|
289
282
|
}
|
|
283
|
+
if (input.capabilities.autoApproveFlag) {
|
|
284
|
+
args.push(input.capabilities.autoApproveFlag, "false");
|
|
285
|
+
}
|
|
290
286
|
args.push(input.prompt);
|
|
291
287
|
return args;
|
|
292
288
|
}
|
|
@@ -403,7 +399,6 @@ export async function runClineJsonTurn(
|
|
|
403
399
|
const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
404
400
|
const killGraceMs = deps.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
405
401
|
const reapTimeoutMs = deps.reapTimeoutMs ?? killGraceMs * 2 + 250;
|
|
406
|
-
const toolBatchIdleMs = deps.toolBatchIdleMs ?? DEFAULT_TOOL_BATCH_IDLE_MS;
|
|
407
402
|
const state = createClineStreamState();
|
|
408
403
|
const maxToolCalls = Math.min(input.maxToolCalls, MAX_TOOL_CALLS);
|
|
409
404
|
const stderr: string[] = [];
|
|
@@ -417,7 +412,6 @@ export async function runClineJsonTurn(
|
|
|
417
412
|
let exitCode: number | null = null;
|
|
418
413
|
let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
|
|
419
414
|
let killTimer: ReturnType<typeof setTimeout> | undefined;
|
|
420
|
-
let toolIdleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
421
415
|
|
|
422
416
|
const deliver = (event: AdapterEvent): void => {
|
|
423
417
|
const terminal = event.type === "done" && input.requireToolCall && state.toolCalls === 0
|
|
@@ -484,15 +478,6 @@ export async function runClineJsonTurn(
|
|
|
484
478
|
});
|
|
485
479
|
};
|
|
486
480
|
|
|
487
|
-
const armToolIdle = (): void => {
|
|
488
|
-
if (terminalEmitted || toolBatchStopped || state.toolCalls === 0) return;
|
|
489
|
-
if (toolIdleTimer) clearTimeout(toolIdleTimer);
|
|
490
|
-
toolIdleTimer = setTimeout(() => {
|
|
491
|
-
toolBatchStopped = true;
|
|
492
|
-
kill();
|
|
493
|
-
}, toolBatchIdleMs);
|
|
494
|
-
};
|
|
495
|
-
|
|
496
481
|
let lifecycle: Promise<void> | undefined;
|
|
497
482
|
try {
|
|
498
483
|
const invocation = commandInvocation(input.binary, input.args, platform, { env: input.env });
|
|
@@ -588,7 +573,12 @@ export async function runClineJsonTurn(
|
|
|
588
573
|
}
|
|
589
574
|
for (const event of events) deliver(event);
|
|
590
575
|
if (terminalEmitted) break;
|
|
591
|
-
|
|
576
|
+
if (state.toolCalls > 0) {
|
|
577
|
+
toolBatchStopped = true;
|
|
578
|
+
kill();
|
|
579
|
+
stopStdout();
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
592
582
|
}
|
|
593
583
|
|
|
594
584
|
if (!terminalEmitted && !aborted && !timedOut) {
|
|
@@ -637,7 +627,6 @@ export async function runClineJsonTurn(
|
|
|
637
627
|
if (!terminalEmitted) deliver(errorEvent(error));
|
|
638
628
|
} finally {
|
|
639
629
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
640
|
-
if (toolIdleTimer) clearTimeout(toolIdleTimer);
|
|
641
630
|
input.incoming.abortSignal?.removeEventListener("abort", onAbort);
|
|
642
631
|
try { child?.stdin?.destroy(); } catch { /* already closed */ }
|
|
643
632
|
if (child && child.exitCode === null && !child.killed) kill();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { link, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { baseScopedEnv } from "../env";
|
|
5
5
|
|
|
6
6
|
/** Built-in, subagent, and team tools that a model-only Cline turn must never enable. */
|
|
@@ -59,6 +59,8 @@ export interface ClineStagedTurn {
|
|
|
59
59
|
workspaceDir: string;
|
|
60
60
|
hooksDir: string;
|
|
61
61
|
providerSettingsPath: string;
|
|
62
|
+
legacyGlobalStatePath: string;
|
|
63
|
+
legacySecretsPath?: string;
|
|
62
64
|
globalSettingsPath: string;
|
|
63
65
|
mcpSettingsPath: string;
|
|
64
66
|
catalogPath: string;
|
|
@@ -292,6 +294,69 @@ function clineGlobalSettings(): Record<string, unknown> {
|
|
|
292
294
|
};
|
|
293
295
|
}
|
|
294
296
|
|
|
297
|
+
function legacyClineSafetyState(value: unknown): Record<string, unknown> {
|
|
298
|
+
const state = asRecord(value) ?? {};
|
|
299
|
+
return {
|
|
300
|
+
...state,
|
|
301
|
+
yoloModeToggled: false,
|
|
302
|
+
autoApproveAllToggled: false,
|
|
303
|
+
clineWebToolsEnabled: false,
|
|
304
|
+
subagentsEnabled: false,
|
|
305
|
+
skillsEnabled: false,
|
|
306
|
+
autoApprovalSettings: {
|
|
307
|
+
version: 1,
|
|
308
|
+
enabled: false,
|
|
309
|
+
favorites: [],
|
|
310
|
+
maxRequests: 0,
|
|
311
|
+
actions: {
|
|
312
|
+
readFiles: false,
|
|
313
|
+
readFilesExternally: false,
|
|
314
|
+
editFiles: false,
|
|
315
|
+
editFilesExternally: false,
|
|
316
|
+
executeSafeCommands: false,
|
|
317
|
+
executeAllCommands: false,
|
|
318
|
+
useBrowser: false,
|
|
319
|
+
useMcp: false,
|
|
320
|
+
},
|
|
321
|
+
enableNotifications: false,
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function stageLegacyClineState(
|
|
327
|
+
providerSettingsPath: string,
|
|
328
|
+
dataDir: string,
|
|
329
|
+
): Promise<{ globalStatePath: string; secretsPath?: string }> {
|
|
330
|
+
const settingsDir = dirname(providerSettingsPath);
|
|
331
|
+
const sourceDataDir = basename(settingsDir) === "settings" ? dirname(settingsDir) : undefined;
|
|
332
|
+
const globalStatePath = join(dataDir, "globalState.json");
|
|
333
|
+
let sourceState: unknown;
|
|
334
|
+
if (sourceDataDir) {
|
|
335
|
+
try {
|
|
336
|
+
sourceState = JSON.parse(await readFile(join(sourceDataDir, "globalState.json"), "utf8"));
|
|
337
|
+
} catch {
|
|
338
|
+
sourceState = undefined;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
await writeFile(globalStatePath, `${JSON.stringify(legacyClineSafetyState(sourceState), null, 2)}\n`, {
|
|
342
|
+
encoding: "utf8",
|
|
343
|
+
mode: 0o600,
|
|
344
|
+
flag: "wx",
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
let secretsPath: string | undefined;
|
|
348
|
+
if (sourceDataDir) {
|
|
349
|
+
try {
|
|
350
|
+
const bytes = await readFile(join(sourceDataDir, "secrets.json"));
|
|
351
|
+
secretsPath = join(dataDir, "secrets.json");
|
|
352
|
+
await writeFile(secretsPath, bytes, { mode: 0o600, flag: "wx" });
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return { globalStatePath, ...(secretsPath ? { secretsPath } : {}) };
|
|
358
|
+
}
|
|
359
|
+
|
|
295
360
|
function clineMcpArgs(launch: ClineMcpLaunch, catalogPath: string): string[] {
|
|
296
361
|
return launch.kind === "module"
|
|
297
362
|
? [launch.modulePath, catalogPath]
|
|
@@ -300,13 +365,18 @@ function clineMcpArgs(launch: ClineMcpLaunch, catalogPath: string): string[] {
|
|
|
300
365
|
|
|
301
366
|
function clineMcpSettings(launch: ClineMcpLaunch | undefined, catalogPath: string): Record<string, unknown> {
|
|
302
367
|
if (!launch) return { mcpServers: {} };
|
|
368
|
+
const args = clineMcpArgs(launch, catalogPath);
|
|
303
369
|
return {
|
|
304
370
|
mcpServers: {
|
|
305
371
|
opencodex: {
|
|
372
|
+
command: process.execPath,
|
|
373
|
+
args,
|
|
374
|
+
disabled: false,
|
|
375
|
+
autoApprove: [],
|
|
306
376
|
transport: {
|
|
307
377
|
type: "stdio",
|
|
308
378
|
command: process.execPath,
|
|
309
|
-
args
|
|
379
|
+
args,
|
|
310
380
|
},
|
|
311
381
|
},
|
|
312
382
|
},
|
|
@@ -406,6 +476,7 @@ export async function stageClineTurn(options: {
|
|
|
406
476
|
writeFile(mcpSettingsPath, `${JSON.stringify(clineMcpSettings(options.mcpLaunch, catalogPath), null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" }),
|
|
407
477
|
writeFile(catalogPath, JSON.stringify(options.catalog), { encoding: "utf8", mode: 0o600, flag: "wx" }),
|
|
408
478
|
]);
|
|
479
|
+
const legacyState = await stageLegacyClineState(options.providerSettingsPath, join(configDir, "data"));
|
|
409
480
|
const copiedProviderStore = await copyClineProviderSettings(options.providerSettingsPath, providerSettingsPath);
|
|
410
481
|
await linkClineSettings({
|
|
411
482
|
dataDir,
|
|
@@ -423,6 +494,8 @@ export async function stageClineTurn(options: {
|
|
|
423
494
|
workspaceDir,
|
|
424
495
|
hooksDir,
|
|
425
496
|
providerSettingsPath,
|
|
497
|
+
legacyGlobalStatePath: legacyState.globalStatePath,
|
|
498
|
+
...(legacyState.secretsPath ? { legacySecretsPath: legacyState.secretsPath } : {}),
|
|
426
499
|
globalSettingsPath,
|
|
427
500
|
mcpSettingsPath,
|
|
428
501
|
catalogPath,
|
|
@@ -99,6 +99,121 @@ function usageFrom(value: unknown): OcxUsage | undefined {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
function legacyUsageFrom(value: unknown): OcxUsage | undefined {
|
|
103
|
+
if (typeof value !== "string") return undefined;
|
|
104
|
+
try {
|
|
105
|
+
const record = asRecord(JSON.parse(value));
|
|
106
|
+
if (!record) return undefined;
|
|
107
|
+
return usageFrom({
|
|
108
|
+
inputTokens: record.tokensIn,
|
|
109
|
+
outputTokens: record.tokensOut,
|
|
110
|
+
cacheReadTokens: record.cacheReads,
|
|
111
|
+
cacheWriteTokens: record.cacheWrites,
|
|
112
|
+
});
|
|
113
|
+
} catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function legacyDone(state: CliAgentStreamState, text?: string): AdapterEvent[] {
|
|
119
|
+
if (state.toolCalls > 0) return [];
|
|
120
|
+
state.terminal = true;
|
|
121
|
+
const events: AdapterEvent[] = [];
|
|
122
|
+
if (!state.sawThinking && state.legacyThinking) {
|
|
123
|
+
state.sawThinking = true;
|
|
124
|
+
events.push({ type: "thinking_delta", thinking: state.legacyThinking });
|
|
125
|
+
}
|
|
126
|
+
const finalText = text || state.legacyText;
|
|
127
|
+
if (!state.sawText && finalText) {
|
|
128
|
+
state.sawText = true;
|
|
129
|
+
events.push({ type: "text_delta", text: finalText });
|
|
130
|
+
}
|
|
131
|
+
events.push({ type: "done", stopReason: "stop", ...(state.usage ? { usage: state.usage } : {}) });
|
|
132
|
+
return events;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function legacyError(message: string, state: CliAgentStreamState): AdapterEvent[] {
|
|
136
|
+
state.lastError = message;
|
|
137
|
+
state.terminal = true;
|
|
138
|
+
return [{ type: "error", message, ...classifyError(message) }];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function legacyMcpRequest(
|
|
142
|
+
text: string | undefined,
|
|
143
|
+
state: CliAgentStreamState,
|
|
144
|
+
options: ClineStreamOptions,
|
|
145
|
+
): AdapterEvent[] {
|
|
146
|
+
if (state.toolCalls > 0) return [];
|
|
147
|
+
let record: Record<string, unknown> | undefined;
|
|
148
|
+
if (text) {
|
|
149
|
+
try {
|
|
150
|
+
record = asRecord(JSON.parse(text));
|
|
151
|
+
} catch {
|
|
152
|
+
record = undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!record || record.type !== "use_mcp_tool" || record.serverName !== "opencodex") {
|
|
156
|
+
throw new ClineStreamProtocolError("Cline requested an MCP interaction outside the isolated capture-only server.");
|
|
157
|
+
}
|
|
158
|
+
const toolName = asString(record.toolName);
|
|
159
|
+
if (!toolName) throw new ClineStreamProtocolError("Cline returned an MCP tool call without a name.");
|
|
160
|
+
return mapClineToolIntent(toolName, undefined, record.arguments, state, options);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function mapLegacyMessage(
|
|
164
|
+
message: Record<string, unknown>,
|
|
165
|
+
state: CliAgentStreamState,
|
|
166
|
+
options: ClineStreamOptions,
|
|
167
|
+
): AdapterEvent[] {
|
|
168
|
+
if (message.partial === true) return [];
|
|
169
|
+
const type = asString(message.type);
|
|
170
|
+
if (type === "completion") {
|
|
171
|
+
const status = asString(message.status);
|
|
172
|
+
const text = asString(message.text);
|
|
173
|
+
if (status === "error") {
|
|
174
|
+
return legacyError(safeMessage(message.message ?? text ?? "Cline reported an error."), state);
|
|
175
|
+
}
|
|
176
|
+
if (status === "success") return legacyDone(state, text);
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
if (type === "say") {
|
|
180
|
+
const say = asString(message.say);
|
|
181
|
+
const text = asString(message.text);
|
|
182
|
+
if (say === "text") {
|
|
183
|
+
if (!text) return [];
|
|
184
|
+
state.legacyText = text;
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
if (say === "reasoning") {
|
|
188
|
+
const reasoning = asString(message.reasoning) ?? text;
|
|
189
|
+
if (!reasoning) return [];
|
|
190
|
+
state.legacyThinking = reasoning;
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
if (say === "completion_result") return legacyDone(state, text);
|
|
194
|
+
if (say === "error") return legacyError(safeMessage(text ?? "Cline reported an error."), state);
|
|
195
|
+
if (say === "use_mcp_server") return legacyMcpRequest(text, state, options);
|
|
196
|
+
if (say === "api_req_finished") {
|
|
197
|
+
state.usage = legacyUsageFrom(text) ?? state.usage;
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
if (say === "tool" || say === "command" || say === "command_output" || say === "browser_action"
|
|
201
|
+
|| say === "browser_action_result" || say === "mcp_server_request_started" || say === "mcp_server_response") {
|
|
202
|
+
throw new ClineStreamProtocolError("Cline attempted a tool action outside the capture-only boundary.");
|
|
203
|
+
}
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
if (type === "ask") {
|
|
207
|
+
const ask = asString(message.ask);
|
|
208
|
+
const text = asString(message.text);
|
|
209
|
+
if (ask === "completion_result") return legacyDone(state, text);
|
|
210
|
+
if (ask === "api_req_failed") return legacyError(safeMessage(text ?? "Cline API request failed."), state);
|
|
211
|
+
if (ask === "use_mcp_server") return legacyMcpRequest(text, state, options);
|
|
212
|
+
throw new ClineStreamProtocolError("Cline requested an interactive approval that capture-only headless mode cannot grant.");
|
|
213
|
+
}
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
102
217
|
function mapAgentEvent(
|
|
103
218
|
event: Record<string, unknown>,
|
|
104
219
|
state: CliAgentStreamState,
|
|
@@ -187,6 +302,9 @@ export function mapClineMessage(
|
|
|
187
302
|
): AdapterEvent[] {
|
|
188
303
|
if (state.terminal) return [];
|
|
189
304
|
const type = asString(message.type);
|
|
305
|
+
if (type === "say" || type === "ask" || type === "completion" || type === "task_started") {
|
|
306
|
+
return mapLegacyMessage(message, state, options);
|
|
307
|
+
}
|
|
190
308
|
if (type === "agent_event") {
|
|
191
309
|
const event = asRecord(message.event);
|
|
192
310
|
return event ? mapAgentEvent(event, state, options) : [];
|
|
@@ -204,6 +204,7 @@ export async function discoverGeminiLiveModels(options: GeminiLiveModelsOptions
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
const runtimeIds = runtime?.map(model => model.modelId) ?? [];
|
|
207
|
+
const runtimeAvailable = runtimeIds.length > 0;
|
|
207
208
|
const ids = runtimeIds.length > 0 ? runtimeIds : Object.keys(metadata.capabilities);
|
|
208
209
|
if (ids.length === 0) {
|
|
209
210
|
failureCache.set(fingerprint, timestamp);
|
|
@@ -252,9 +253,11 @@ export async function discoverGeminiLiveModels(options: GeminiLiveModelsOptions
|
|
|
252
253
|
loadSession: false,
|
|
253
254
|
};
|
|
254
255
|
});
|
|
255
|
-
const modelInfoWithDefault =
|
|
256
|
+
const modelInfoWithDefault = runtimeAvailable
|
|
257
|
+
? modelInfo
|
|
258
|
+
: [defaultModelInfo(metadata, modelInfo), ...modelInfo.filter(model => model.id !== "default")];
|
|
256
259
|
const warning = metadata.warning
|
|
257
|
-
?? (
|
|
260
|
+
?? (runtimeAvailable ? undefined : "Gemini runtime model discovery failed; using installed CLI metadata.");
|
|
258
261
|
cache.set(fingerprint, { models: ids, modelInfo: modelInfoWithDefault, source: "models", fetchedAt: timestamp, ...(warning ? { metadataWarning: warning } : {}) });
|
|
259
262
|
failureCache.delete(fingerprint);
|
|
260
263
|
return { ok: true, models: ids, modelInfo: modelInfoWithDefault, source: "models", fingerprint, ...(warning ? { metadataWarning: warning } : {}) };
|
package/src/lib/codex-catalog.ts
CHANGED
|
@@ -178,12 +178,14 @@ function defaultModel(): OcxProviderModel {
|
|
|
178
178
|
return { id: "default" };
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
function normalizedModels(models: readonly OcxProviderModel[]): OcxProviderModel[] {
|
|
181
|
+
function normalizedModels(models: readonly OcxProviderModel[], includeDefault = true): OcxProviderModel[] {
|
|
182
182
|
const defaultInfo = models.find(model => model.id === "default");
|
|
183
|
-
const out: OcxProviderModel[] =
|
|
184
|
-
|
|
183
|
+
const out: OcxProviderModel[] = includeDefault
|
|
184
|
+
? [{ ...defaultModel(), ...(defaultInfo ?? {}), id: "default" }]
|
|
185
|
+
: [];
|
|
186
|
+
const seen = new Set<string>(includeDefault ? ["default"] : []);
|
|
185
187
|
for (const model of models) {
|
|
186
|
-
if (model.id === "default" || seen.has(model.id)) continue;
|
|
188
|
+
if ((includeDefault && model.id === "default") || seen.has(model.id)) continue;
|
|
187
189
|
seen.add(model.id);
|
|
188
190
|
out.push(model);
|
|
189
191
|
}
|
|
@@ -207,7 +209,7 @@ export async function discoverCodexCatalogModels(options: CodexCatalogDiscoveryO
|
|
|
207
209
|
if (!gemini.ok && gemini.reason !== "not_found" && env.OPENCODEX_GEMINI_ACP?.trim() !== "0") warnings.push(`Gemini: ${gemini.detail}`);
|
|
208
210
|
if (gemini.ok && gemini.metadataWarning) warnings.push(`Gemini metadata: ${gemini.metadataWarning}`);
|
|
209
211
|
const clineModels = normalizedModels(cline.ok ? cline.models.map(id => ({ id })) : []);
|
|
210
|
-
const geminiModels = normalizedModels(gemini.ok ? gemini.modelInfo : []);
|
|
212
|
+
const geminiModels = normalizedModels(gemini.ok ? gemini.modelInfo : [], false);
|
|
211
213
|
const geminiDefault = geminiModels.find(model => model.id === "default");
|
|
212
214
|
const geminiContexts = geminiModels.flatMap(model => model.contextWindowTokens !== undefined ? [model.contextWindowTokens] : []);
|
|
213
215
|
if (geminiDefault && geminiContexts.length > 0) {
|
package/src/server/index.ts
CHANGED
|
@@ -118,7 +118,7 @@ async function discoverModels(
|
|
|
118
118
|
}
|
|
119
119
|
if (gemini) {
|
|
120
120
|
models["gemini-cli"] = gemini.ok
|
|
121
|
-
?
|
|
121
|
+
? gemini.modelInfo.map(model => model.id)
|
|
122
122
|
: ["default"];
|
|
123
123
|
}
|
|
124
124
|
return models;
|
package/src/server/responses.ts
CHANGED
|
@@ -379,6 +379,16 @@ const PROVIDER_LABELS: Record<ProviderId, string> = {
|
|
|
379
379
|
[GEMINI_PROVIDER_ID]: "Gemini CLI",
|
|
380
380
|
};
|
|
381
381
|
|
|
382
|
+
function listedProviderEntries(
|
|
383
|
+
known: ProviderModels,
|
|
384
|
+
providerId: ProviderId,
|
|
385
|
+
): readonly ProviderModelEntry[] {
|
|
386
|
+
const entries = known[providerId] ?? [];
|
|
387
|
+
if (providerId === GEMINI_PROVIDER_ID && entries.length > 0) return entries;
|
|
388
|
+
const defaultEntry = entries.find(entry => modelIdOf(entry) === "default") ?? "default";
|
|
389
|
+
return [defaultEntry, ...entries.filter(entry => modelIdOf(entry) !== "default")];
|
|
390
|
+
}
|
|
391
|
+
|
|
382
392
|
export function modelList(
|
|
383
393
|
known: string[] | ProviderModels,
|
|
384
394
|
scope?: ProviderId,
|
|
@@ -394,8 +404,7 @@ export function modelList(
|
|
|
394
404
|
}));
|
|
395
405
|
}
|
|
396
406
|
if (scope) {
|
|
397
|
-
const
|
|
398
|
-
const entries = [defaultEntry, ...(known[scope] ?? []).filter(entry => modelIdOf(entry) !== "default")];
|
|
407
|
+
const entries = listedProviderEntries(known, scope);
|
|
399
408
|
const rows: Array<Record<string, unknown>> = [];
|
|
400
409
|
const seen = new Set<string>();
|
|
401
410
|
for (const entry of entries) {
|
|
@@ -440,8 +449,7 @@ export function modelList(
|
|
|
440
449
|
display_name: "Configured default provider",
|
|
441
450
|
}];
|
|
442
451
|
for (const providerId of [CLINE_PROVIDER_ID, GEMINI_PROVIDER_ID] as const) {
|
|
443
|
-
const
|
|
444
|
-
const entries = [defaultEntry, ...(known[providerId] ?? []).filter(entry => modelIdOf(entry) !== "default")];
|
|
452
|
+
const entries = listedProviderEntries(known, providerId);
|
|
445
453
|
const seen = new Set<string>();
|
|
446
454
|
for (const entry of entries) {
|
|
447
455
|
const id = modelIdOf(entry);
|