@koreanhole/opencodex 3.2.10 → 3.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS_INSTALL.md CHANGED
@@ -43,11 +43,13 @@ or through the documented default path; unrelated Codex settings are not changed
43
43
 
44
44
  ## Credential rules
45
45
 
46
- - Use the operator's existing Cline or Gemini CLI sign-in. Do not copy vendor
46
+ - Use the operator's existing Cline or Gemini CLI sign-in. Cline login is
47
+ vendor-managed through `cline auth`; the adapter has no ACP login or discovery
48
+ dependency. Do not copy vendor
47
49
  tokens into config, environment files, logs, prompts, or package content. The
48
50
  only exception is Cline's mode-0600 request-scoped staging copy.
49
51
  - Do not authenticate on a request path. If Cline reports `auth_required`, stop
50
- and ask the operator to run `ocx login cline-cli`.
52
+ and ask the operator to run `ocx login cline-cli` or `cline auth` directly.
51
53
  - Do not edit the operator's real Cline provider store or legacy
52
54
  `globalState.json`/`secrets.json`. Read them only to create mode-0600
53
55
  request-scoped copies; remove those copies after the turn.
package/README.md CHANGED
@@ -54,9 +54,11 @@ model management, service lifecycle, and restore behavior remain available
54
54
  through the same `ocx` command.
55
55
 
56
56
  The managed provider rows appear as `gemini-cli/...` and `cline-cli/...`.
57
- Cline model discovery and explicit login use ACP, while Cline model turns use
58
- the installed CLI's JSON transport across the 2.2.0 and current output contracts.
59
- Gemini runtime model discovery uses the
57
+ Cline does not require ACP: it exposes a stable `default` selector that follows
58
+ the model already selected in Cline, while Cline model turns use the installed
59
+ CLI's JSON transport across the 2.2.0 and current output contracts. Explicit
60
+ `ocx login cline-cli` delegates to the vendor `cline auth` command. Gemini runtime
61
+ model discovery uses the
60
62
  installed CLI's ACP session, while reasoning metadata and model turns use the
61
63
  installed CLI's headless transport. Parent-declared tools are exposed through a
62
64
  request-scoped capture MCP server.
@@ -137,8 +139,9 @@ the blocker.
137
139
  - Cline runs with request-scoped, mode-0600 settings. Its built-in tools, user
138
140
  MCP servers, hooks, plugins, telemetry, updates, and self-approval are disabled.
139
141
  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.
142
+ the originals are never modified. No Cline runtime path requires ACP. Its turn
143
+ options are used only after the installed binary's `--help` output advertises
144
+ them.
142
145
  - Gemini headless turns run in a request-scoped workspace with
143
146
  `GEMINI_CLI_TRUST_WORKSPACE=true`, `--skip-trust`, and
144
147
  `--approval-mode auto_edit`, but the generated isolated Gemini home settings
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koreanhole/opencodex",
3
- "version": "3.2.10",
3
+ "version": "3.2.12",
4
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",
@@ -19,7 +19,7 @@ import {
19
19
  type ClineStagedTurn,
20
20
  } from "./settings";
21
21
  import { resolveClineBinary } from "./binary";
22
- import { buildClineAcpEnv } from "./acp";
22
+ import { buildClineProbeEnv } from "./env";
23
23
  import {
24
24
  buildClineJsonArgs,
25
25
  ClineJsonError,
@@ -189,7 +189,7 @@ export async function runClineTurn(
189
189
  return;
190
190
  }
191
191
 
192
- const probeEnv = buildClineAcpEnv(sourceEnv);
192
+ const probeEnv = buildClineProbeEnv(sourceEnv);
193
193
  let capabilities: ClineJsonCapabilities;
194
194
  try {
195
195
  capabilities = await (deps.probeJson ?? probeClineJsonCapabilities)(binary, probeEnv, deps);
@@ -0,0 +1,26 @@
1
+ import { homedir } from "node:os";
2
+ import { baseScopedEnv } from "../env";
3
+
4
+ function nonEmpty(value: unknown): string | undefined {
5
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
6
+ }
7
+
8
+ /** Build a real-home environment for bounded Cline capability probes. */
9
+ export function buildClineProbeEnv(sourceEnv: NodeJS.ProcessEnv = process.env): Record<string, string> {
10
+ const env = baseScopedEnv(sourceEnv);
11
+ const realHome = nonEmpty(sourceEnv.HOME) ?? nonEmpty(sourceEnv.USERPROFILE) ?? homedir();
12
+ env.HOME = realHome;
13
+ env.USERPROFILE = realHome;
14
+ for (const key of ["XDG_CONFIG_HOME", "XDG_CACHE_HOME", "CLINE_DIR", "CLINE_DATA_DIR", "CLINE_PROVIDER_SETTINGS_PATH"]) {
15
+ const value = nonEmpty(sourceEnv[key]);
16
+ if (value) env[key] = value;
17
+ }
18
+ Object.assign(env, {
19
+ CLINE_NO_AUTO_UPDATE: "1",
20
+ CLINE_TELEMETRY_DISABLED: "1",
21
+ CLINE_DISABLE_CLINE_PASS_NOTICE: "1",
22
+ NO_UPDATE_NOTIFIER: "1",
23
+ NO_COLOR: "1",
24
+ });
25
+ return env;
26
+ }
@@ -8,7 +8,7 @@ import { redactSecretString } from "../../../lib/redact";
8
8
  import { commandInvocation } from "../../../lib/win-exec";
9
9
  import { CodingAgentProtocolError, readJsonLines } from "../protocol";
10
10
  import type { CliAgentTurnDeps } from "../types";
11
- import { buildClineAcpEnv } from "./acp";
11
+ import { buildClineProbeEnv } from "./env";
12
12
  import type { ClineSelection } from "./settings";
13
13
  import {
14
14
  classifyClineFailure,
@@ -197,7 +197,7 @@ function probeFailure(code: string, detail: string): ClineJsonError {
197
197
 
198
198
  export async function probeClineJsonCapabilities(
199
199
  binary: string,
200
- env: Record<string, string> = buildClineAcpEnv(),
200
+ env: Record<string, string> = buildClineProbeEnv(),
201
201
  deps: CliAgentTurnDeps = {},
202
202
  ): Promise<ClineJsonCapabilities> {
203
203
  const path = resolve(binary);
@@ -1,128 +1,21 @@
1
- import {
2
- buildClineAcpEnv,
3
- clineAcpEnabled,
4
- clineModelSource,
5
- probeClineAcpSupport,
6
- stageClineAcpWorkspace,
7
- type ClineAcpDeps,
8
- } from "./acp";
9
- import { clineBinaryFingerprintSync, resolveClineBinary } from "./binary";
10
- import { AcpClient, AcpError } from "../acp-client";
11
-
12
- export type ClineLiveModelsFailure = "not_found" | "unsupported" | "auth_required" | "backoff" | "transport";
1
+ import { whichFromPath, type WhichFn } from "../../coding-agent/profile";
2
+ import type { CliAgentTurnDeps } from "../types";
3
+ import { resolveClineBinary } from "./binary";
13
4
 
14
5
  export type ClineLiveModelsResult =
15
- | {
16
- ok: true;
17
- models: string[];
18
- source: "models" | "configOptions" | "default";
19
- fingerprint: string;
20
- stale?: true;
21
- }
22
- | {
23
- ok: false;
24
- reason: ClineLiveModelsFailure;
25
- detail: string;
26
- fingerprint?: string;
27
- };
6
+ | { ok: true; models: ["default"]; source: "default" }
7
+ | { ok: false; reason: "not_found"; detail: string };
28
8
 
29
9
  export interface ClineLiveModelsOptions {
30
10
  binary?: string;
31
11
  env?: NodeJS.ProcessEnv;
32
- deps?: ClineAcpDeps;
33
- ttlMs?: number;
34
- failureBackoffMs?: number;
35
- now?: () => number;
36
- }
37
-
38
- interface CacheEntry {
39
- models: string[];
40
- source: "models" | "configOptions" | "default";
41
- fetchedAt: number;
42
- lastFailureAt?: number;
12
+ which?: WhichFn;
13
+ deps?: CliAgentTurnDeps;
43
14
  }
44
15
 
45
- const cache = new Map<string, CacheEntry>();
46
- const failureCache = new Map<string, number>();
47
-
48
- export function resetClineLiveModelsCacheForTests(): void {
49
- cache.clear();
50
- failureCache.clear();
51
- }
52
-
53
- function staleResult(fingerprint: string, entry: CacheEntry | undefined): ClineLiveModelsResult | undefined {
54
- if (!entry) return undefined;
55
- return {
56
- ok: true,
57
- models: [...entry.models],
58
- source: entry.source,
59
- fingerprint,
60
- stale: true,
61
- };
62
- }
63
-
64
- /** Discover Cline's native model roster with TTL, stale fallback, and failure backoff. */
16
+ /** Cline has no ACP-free model enumeration; default uses its persisted selection. */
65
17
  export async function discoverClineLiveModels(options: ClineLiveModelsOptions = {}): Promise<ClineLiveModelsResult> {
66
- const sourceEnv = options.env ?? process.env;
67
- const now = options.now ?? Date.now;
68
- const ttlMs = options.ttlMs ?? 5 * 60_000;
69
- const failureBackoffMs = options.failureBackoffMs ?? 60_000;
70
- if (!clineAcpEnabled(sourceEnv)) {
71
- return { ok: false, reason: "unsupported", detail: "ACP is disabled by OPENCODEX_CLINE_ACP=0." };
72
- }
73
- const binary = options.binary ?? resolveClineBinary(undefined, sourceEnv);
18
+ const binary = options.binary ?? resolveClineBinary(options.which ?? whichFromPath, options.env ?? process.env);
74
19
  if (!binary) return { ok: false, reason: "not_found", detail: "Cline CLI not found on PATH." };
75
- const fingerprint = clineBinaryFingerprintSync(binary);
76
- const current = cache.get(fingerprint);
77
- const timestamp = now();
78
- if (current && timestamp - current.fetchedAt < ttlMs) {
79
- return { ok: true, models: [...current.models], source: current.source, fingerprint };
80
- }
81
- const lastFailure = failureCache.get(fingerprint) ?? current?.lastFailureAt;
82
- if (lastFailure !== undefined && timestamp - lastFailure < failureBackoffMs) {
83
- return staleResult(fingerprint, current)
84
- ?? { ok: false, reason: "backoff", detail: "Cline model discovery is cooling down after a failure.", fingerprint };
85
- }
86
-
87
- const env = buildClineAcpEnv(sourceEnv);
88
- const mode = await probeClineAcpSupport(binary, env, { ...options.deps, env: sourceEnv });
89
- if (!mode.acp) {
90
- failureCache.set(fingerprint, timestamp);
91
- if (current) current.lastFailureAt = timestamp;
92
- return staleResult(fingerprint, current)
93
- ?? { ok: false, reason: "unsupported", detail: "Cline --help does not advertise --acp.", fingerprint };
94
- }
95
- const workspace = await stageClineAcpWorkspace();
96
- let client: AcpClient | undefined;
97
- try {
98
- client = await AcpClient.connect({
99
- binary,
100
- env,
101
- cwd: workspace.cwd,
102
- args: mode.autoApproveFlag ? ["--acp", "--auto-approve", "false"] : ["--acp"],
103
- label: "Cline ACP",
104
- fallbackAllowed: true,
105
- }, options.deps);
106
- const session = await client.newSession({ cwd: workspace.cwd, mcpServers: [] });
107
- const discovered = clineModelSource(session);
108
- cache.set(fingerprint, { models: discovered.ids, source: discovered.source, fetchedAt: timestamp });
109
- failureCache.delete(fingerprint);
110
- return { ok: true, models: discovered.ids, source: discovered.source, fingerprint };
111
- } catch (error) {
112
- failureCache.set(fingerprint, timestamp);
113
- if (current) current.lastFailureAt = timestamp;
114
- const stale = staleResult(fingerprint, current);
115
- if (error instanceof AcpError && error.authRequired) {
116
- return stale ?? { ok: false, reason: "auth_required", detail: error.message, fingerprint };
117
- }
118
- return stale ?? {
119
- ok: false,
120
- reason: "transport",
121
- detail: error instanceof Error ? error.message : String(error),
122
- fingerprint,
123
- };
124
- } finally {
125
- await client?.terminate();
126
- await workspace.cleanup();
127
- }
20
+ return { ok: true, models: ["default"], source: "default" };
128
21
  }
@@ -0,0 +1,85 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ import { homedir } from "node:os";
3
+ import { commandInvocation } from "../../../lib/win-exec";
4
+ import { whichFromPath, type WhichFn } from "../../coding-agent/profile";
5
+ import type { CliAgentSpawnFn } from "../types";
6
+ import { resolveClineBinary } from "./binary";
7
+
8
+ const VENDOR_LOGIN_INSTRUCTION = "Cline authentication is managed by Cline CLI. Run `cline auth` in an interactive terminal.";
9
+
10
+ export class ClineLoginError extends Error {
11
+ constructor(
12
+ message: string,
13
+ readonly status: number,
14
+ readonly code: string,
15
+ ) {
16
+ super(message);
17
+ this.name = "ClineLoginError";
18
+ }
19
+ }
20
+
21
+ export interface ClineLoginOptions {
22
+ instructionsOnly?: boolean;
23
+ env?: NodeJS.ProcessEnv;
24
+ which?: WhichFn;
25
+ spawn?: CliAgentSpawnFn;
26
+ platform?: NodeJS.Platform;
27
+ }
28
+
29
+ export interface ClineLoginResult {
30
+ mode: "vendor" | "instruction";
31
+ complete: boolean;
32
+ exitCode: number;
33
+ instructions?: string;
34
+ }
35
+
36
+ /** Explicit login delegates to the real vendor CLI and never uses ACP. */
37
+ export async function loginCline(options: ClineLoginOptions = {}): Promise<ClineLoginResult> {
38
+ const env = options.env ?? process.env;
39
+ const binary = resolveClineBinary(options.which ?? whichFromPath, env);
40
+ if (!binary) {
41
+ throw new ClineLoginError(
42
+ "Cline CLI not found on PATH. Install it with `npm install -g cline` or set CLINE_BIN_PATH.",
43
+ 500,
44
+ "cli_not_found",
45
+ );
46
+ }
47
+ if (options.instructionsOnly) {
48
+ return {
49
+ mode: "instruction",
50
+ complete: false,
51
+ exitCode: 0,
52
+ instructions: VENDOR_LOGIN_INSTRUCTION,
53
+ };
54
+ }
55
+
56
+ const invocation = commandInvocation(binary, ["auth"], options.platform ?? process.platform, { env });
57
+ const spawnFn = options.spawn ?? (nodeSpawn as unknown as CliAgentSpawnFn);
58
+ const cwd = env.HOME?.trim() || env.USERPROFILE?.trim() || homedir();
59
+ return await new Promise<ClineLoginResult>((resolvePromise, reject) => {
60
+ let child;
61
+ try {
62
+ child = spawnFn(invocation.file, invocation.args, {
63
+ ...invocation.options,
64
+ cwd,
65
+ env,
66
+ stdio: "inherit",
67
+ windowsHide: true,
68
+ });
69
+ } catch (error) {
70
+ reject(new ClineLoginError(
71
+ `Cline authentication could not start: ${error instanceof Error ? error.message : String(error)}`,
72
+ 500,
73
+ "cli_spawn_failed",
74
+ ));
75
+ return;
76
+ }
77
+ child.once("error", error => {
78
+ reject(new ClineLoginError(`Cline authentication could not start: ${error.message}`, 500, "cli_spawn_failed"));
79
+ });
80
+ child.once("close", code => {
81
+ const exitCode = code ?? 1;
82
+ resolvePromise({ mode: "vendor", complete: exitCode === 0, exitCode });
83
+ });
84
+ });
85
+ }
@@ -30,7 +30,7 @@ export function createClineStreamState(): CliAgentStreamState {
30
30
  };
31
31
  }
32
32
 
33
- /** Map a validated Cline ACP tool intent into events owned by Codex. */
33
+ /** Map a validated Cline tool intent into events owned by Codex. */
34
34
  export function mapClineToolIntent(
35
35
  vendorName: string,
36
36
  toolCallId: string | undefined,
package/src/cli/index.ts CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  type ProviderId,
18
18
  } from "../config";
19
19
  import { startServer } from "../server";
20
- import { loginClineAcp } from "../adapters/cli-agent/cline/acp";
20
+ import { loginCline } from "../adapters/cli-agent/cline/login";
21
21
  import { runCodingAgentMcpServer } from "../adapters/cli-agent/mcp-server";
22
22
  import { syncCodexCatalog } from "../lib/codex-catalog";
23
23
  import { injectCodexConfig, recordCodexCatalogState, restoreCodexConfig } from "../lib/codex-inject";
@@ -154,21 +154,23 @@ async function loginCommand(args: string[]): Promise<number> {
154
154
  const json = takeFlag(args, "--json");
155
155
  const noWait = takeFlag(args, "--no-wait");
156
156
  if (args.length > 0) throw new ConfigError(`Unknown login option: ${args[0]}`);
157
- const result = await loginClineAcp({ noWait });
157
+ const result = await loginCline({ instructionsOnly: noWait || json, env: process.env });
158
158
  if (json) {
159
159
  console.log(JSON.stringify({
160
160
  ok: true,
161
161
  provider: CLINE_PROVIDER_ID,
162
- methods: result.methods,
163
- selectedMethod: result.method.id,
162
+ selectedMethod: "vendor-cli",
164
163
  complete: result.complete,
165
164
  instructions: result.instructions,
166
165
  }));
167
166
  } else {
168
167
  if (result.instructions) process.stderr.write(result.instructions);
169
- console.log(result.complete ? "Cline login complete." : "Cline login is waiting for completion.");
168
+ if (result.mode === "vendor") {
169
+ if (result.complete) console.log("Cline login complete.");
170
+ else console.error("Cline authentication did not complete.");
171
+ }
170
172
  }
171
- return 0;
173
+ return result.exitCode;
172
174
  }
173
175
 
174
176
  async function startCommand(args: string[]): Promise<number> {
@@ -7,7 +7,7 @@ import { discoverGeminiLiveModels } from "../adapters/cli-agent/gemini/live-mode
7
7
  import type { ClineTurnDeps } from "../adapters/cli-agent/cline/adapter";
8
8
  import type { GeminiTurnDeps } from "../adapters/cli-agent/gemini/adapter";
9
9
 
10
- // Cline ACP descriptors expose no context source; this fallback is Cline-only.
10
+ // Cline exposes no runtime context source; this fallback is Cline-only.
11
11
  const CATALOG_FALLBACK_CONTEXT = 128_000;
12
12
  const BASE_INSTRUCTIONS = "You are a helpful coding assistant.";
13
13
  const MANAGED_PREFIXES = ["cline-cli/", "gemini-cli/"] as const;
@@ -131,7 +131,7 @@ export function buildCodexCatalogModel(providerId: "cline-cli" | "gemini-cli", m
131
131
  : 100;
132
132
  return {
133
133
  slug: `${providerId}/${encodeRoutedId(model.id)}`,
134
- display_name: model.name ?? (isDefault ? `${providerLabel} default` : model.id),
134
+ display_name: model.name ?? (isDefault ? (providerId === "cline-cli" ? "Cline" : `${providerLabel} default`) : model.id),
135
135
  description: model.description ?? `Routed via opencodex -> ${providerLabel}.`,
136
136
  default_reasoning_level: levels.length > 0 ? defaultLevel : "low",
137
137
  supported_reasoning_levels: levels,
@@ -205,7 +205,7 @@ export async function discoverCodexCatalogModels(options: CodexCatalogDiscoveryO
205
205
  }),
206
206
  ]);
207
207
  const warnings: string[] = [];
208
- if (!cline.ok && cline.reason !== "not_found" && env.OPENCODEX_CLINE_ACP?.trim() !== "0") warnings.push(`Cline: ${cline.detail}`);
208
+ if (!cline.ok && cline.reason !== "not_found") warnings.push(`Cline: ${cline.detail}`);
209
209
  if (!gemini.ok && gemini.reason !== "not_found" && env.OPENCODEX_GEMINI_ACP?.trim() !== "0") warnings.push(`Gemini: ${gemini.detail}`);
210
210
  if (gemini.ok && gemini.metadataWarning) warnings.push(`Gemini metadata: ${gemini.metadataWarning}`);
211
211
  const clineModels = normalizedModels(cline.ok ? cline.models.map(id => ({ id })) : []);
@@ -106,9 +106,7 @@ async function discoverModels(
106
106
  ? discoverGeminiLiveModels({ env, ...(geminiDeps ? { deps: geminiDeps } : {}), ...(geminiPackageRoot ? { packageRoot: geminiPackageRoot } : {}) })
107
107
  : undefined,
108
108
  ]);
109
- if (cline && !cline.ok && cline.reason !== "not_found" && env.OPENCODEX_CLINE_ACP?.trim() !== "0") {
110
- console.warn(`[opencodex] Cline model discovery degraded [${cline.reason}]: ${cline.detail}`);
111
- }
109
+ if (cline && !cline.ok && cline.reason !== "not_found") console.warn(`[opencodex] Cline model discovery degraded: ${cline.detail}`);
112
110
  if (gemini && !gemini.ok && gemini.reason !== "not_found" && env.OPENCODEX_GEMINI_ACP?.trim() !== "0") {
113
111
  console.warn(`[opencodex] Gemini model discovery degraded [${gemini.reason}]: ${gemini.detail}`);
114
112
  }
@@ -374,10 +374,9 @@ function modelIdOf(model: ProviderModelEntry): string {
374
374
  return typeof model === "string" ? model : model.id;
375
375
  }
376
376
 
377
- const PROVIDER_LABELS: Record<ProviderId, string> = {
378
- [CLINE_PROVIDER_ID]: "Cline CLI",
379
- [GEMINI_PROVIDER_ID]: "Gemini CLI",
380
- };
377
+ function defaultModelDisplayName(providerId: ProviderId): string {
378
+ return providerId === CLINE_PROVIDER_ID ? "Cline" : "Gemini CLI default";
379
+ }
381
380
 
382
381
  function listedProviderEntries(
383
382
  known: ProviderModels,
@@ -400,7 +399,7 @@ export function modelList(
400
399
  object: "model",
401
400
  created: 0,
402
401
  owned_by: CLINE_PROVIDER_ID,
403
- display_name: id === "default" ? "Cline default" : id,
402
+ display_name: id === "default" ? "Cline" : id,
404
403
  }));
405
404
  }
406
405
  if (scope) {
@@ -418,7 +417,7 @@ export function modelList(
418
417
  object: "model",
419
418
  created: 0,
420
419
  owned_by: scope,
421
- display_name: info.name ?? (id === "default" ? PROVIDER_LABELS[scope] + " default" : id),
420
+ display_name: info.name ?? (id === "default" ? defaultModelDisplayName(scope) : id),
422
421
  ...(info.description ? { description: info.description } : {}),
423
422
  ...(info.contextWindowTokens !== undefined
424
423
  ? {
@@ -462,7 +461,7 @@ export function modelList(
462
461
  object: "model",
463
462
  created: 0,
464
463
  owned_by: providerId,
465
- display_name: info.name ?? (id === "default" ? `${PROVIDER_LABELS[providerId]} default` : id),
464
+ display_name: info.name ?? (id === "default" ? defaultModelDisplayName(providerId) : id),
466
465
  ...(info.description ? { description: info.description } : {}),
467
466
  ...(info.contextWindowTokens !== undefined
468
467
  ? {