@koreanhole/opencodex 2.95.8 → 2.95.9

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.
@@ -1,7 +1,6 @@
1
1
  import { fileURLToPath } from "node:url";
2
- import { existsSync } from "node:fs";
3
2
  import { writeFile } from "node:fs/promises";
4
- import { dirname, join } from "node:path";
3
+ import { join } from "node:path";
5
4
  import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../../types";
6
5
  import type { AdapterRequest, ProviderAdapter } from "../../base";
7
6
  import { buildCodingAgentToolCatalog, CodingAgentToolCatalogError } from "../../coding-agent/tool-catalog";
@@ -11,18 +10,26 @@ import { isStandaloneBinary } from "../../../lib/standalone";
11
10
  import { runCliAgentTurn } from "../turn";
12
11
  import type { CliAgentTurnDeps } from "../types";
13
12
  import {
14
- assertClineSelectionSafe,
13
+ assertClineAdvisoryDestinationSafe,
15
14
  buildClineChildEnv,
16
15
  ClineSettingsError,
17
16
  mergeClineDisabledTools,
18
17
  preflightClineTools,
19
- probeClineFlagCapabilities,
20
18
  resolveClineProviderSettingsPath,
21
19
  stageClineTurn,
22
- type ClineFlagCapabilities,
23
- type ClineSelection,
24
20
  type ClineStagedTurn,
25
21
  } from "./settings";
22
+ import { resolveClineBinary } from "./binary";
23
+ import {
24
+ buildClineAcpEnv,
25
+ ClineAcpError,
26
+ probeClineAcpSupport,
27
+ runClineAcpTurn,
28
+ stageClineAcpWorkspace,
29
+ type ClineAcpDeps,
30
+ type ClineAcpMode,
31
+ type ClineAcpTurnInput,
32
+ } from "./acp";
26
33
  import {
27
34
  classifyClineFailure,
28
35
  createClineStreamState,
@@ -49,21 +56,13 @@ export interface ClineCliAdapterDeps extends CliAgentTurnDeps {
49
56
  providerSettingsPath?: string;
50
57
  stageTurn?: typeof stageClineTurn;
51
58
  preflightTools?: typeof preflightClineTools;
52
- probeFlags?: typeof probeClineFlagCapabilities;
59
+ probeAcp?: typeof probeClineAcpSupport;
60
+ stageAcp?: typeof stageClineAcpWorkspace;
61
+ runAcp?: (input: ClineAcpTurnInput, emit: (event: AdapterEvent) => void, deps?: ClineAcpDeps) => Promise<void>;
62
+ acpDeps?: ClineAcpDeps;
53
63
  }
54
64
 
55
- /** Prefer the npm wrapper's cached native executable so stdin remains a regular file fd. */
56
- export function resolveClineBinary(
57
- which: WhichFn = whichFromPath,
58
- env: NodeJS.ProcessEnv = process.env,
59
- ): string | undefined {
60
- const explicit = env.CLINE_BIN_PATH?.trim();
61
- if (explicit && existsSync(explicit)) return explicit;
62
- const resolved = which("cline");
63
- if (!resolved) return undefined;
64
- const cached = join(dirname(resolved), ".cline");
65
- return existsSync(cached) ? cached : resolved;
66
- }
65
+ export { resolveClineBinary } from "./binary";
67
66
 
68
67
  function hasUnsupportedMedia(parsed: OcxParsedRequest): boolean {
69
68
  return parsed.context.messages.some(message =>
@@ -107,23 +106,12 @@ export function buildClinePrompt(
107
106
  ].filter(Boolean).join("\n\n");
108
107
  }
109
108
 
110
- export function buildClineArgs(
111
- selection?: ClineSelection,
112
- capabilities: ClineFlagCapabilities = {},
113
- ): string[] {
109
+ export function buildClineArgs(): string[] {
114
110
  // The CLI surface differs between Cline and its forks. Keep the invocation to the
115
- // core JSON mode; config, data, hooks, provider/model, auto-approve, and compaction
116
- // are carried by CLINE_* environment variables, staged settings, and the provider
117
- // store. The fixed bridge text travels in the private stdin prompt.
118
- const args = ["--json"];
119
- if (capabilities.providerFlag && selection?.providerId) {
120
- args.push(capabilities.providerFlag, selection.providerId);
121
- }
122
- if (capabilities.modelFlag && selection?.model) {
123
- args.push(capabilities.modelFlag, selection.model);
124
- }
125
- args.push("Follow the piped conversation and instructions.");
126
- return args;
111
+ // core JSON mode; staged CLINE_* paths and settings carry config/tool policy, while
112
+ // the real HOME and the vendor provider store remain advisory. The fixed bridge
113
+ // text travels in the private stdin prompt.
114
+ return ["--json", "Follow the piped conversation and instructions."];
127
115
  }
128
116
 
129
117
  function toolNameMap(catalog: ReturnType<typeof buildCodingAgentToolCatalog>): Map<string, string> {
@@ -171,6 +159,27 @@ function settingsError(error: unknown): Extract<AdapterEvent, { type: "error" }>
171
159
  };
172
160
  }
173
161
 
162
+ function acpFailure(error: unknown): Extract<AdapterEvent, { type: "error" }> {
163
+ if (error instanceof ClineAcpError) {
164
+ return {
165
+ type: "error",
166
+ message: error.message,
167
+ status: error.status,
168
+ errorType: error.errorType,
169
+ code: error.code,
170
+ retryable: error.retryable,
171
+ };
172
+ }
173
+ return {
174
+ type: "error",
175
+ message: error instanceof Error ? error.message : String(error),
176
+ status: 502,
177
+ errorType: "upstream_error",
178
+ code: "acp_transport_error",
179
+ retryable: false,
180
+ };
181
+ }
182
+
174
183
  /** Create the additive, keyless Cline CLI provider adapter. */
175
184
  export function createClineCliAdapter(
176
185
  provider: OcxProviderConfig,
@@ -199,10 +208,11 @@ export function createClineCliAdapter(
199
208
  });
200
209
  return;
201
210
  }
202
- if (parsed.modelId !== CLINE_CLI_MODEL_ID) {
211
+ const requestedModel = typeof parsed.modelId === "string" ? parsed.modelId.trim() : "";
212
+ if (!requestedModel) {
203
213
  emit({
204
214
  type: "error",
205
- message: `Cline CLI exposes only the ${CLINE_CLI_MODEL_ID} model.`,
215
+ message: "Cline CLI requires a model selector.",
206
216
  status: 400,
207
217
  errorType: "invalid_request_error",
208
218
  code: "model_not_found",
@@ -252,11 +262,106 @@ export function createClineCliAdapter(
252
262
  return;
253
263
  }
254
264
 
265
+ const vendorNames = toolNameMap(catalog);
266
+ const vendorNameByWire = new Map(
267
+ [...catalog.emittedNameMap.values()].map(wireName => [
268
+ wireName,
269
+ wireName.startsWith("opencodex__") ? wireName : `opencodex__${wireName}`,
270
+ ]),
271
+ );
272
+ const fixedSystem = systemPromptWithToolNames(catalog);
273
+ const acpDeps: ClineAcpDeps = {
274
+ ...deps.acpDeps,
275
+ env: sourceEnv,
276
+ ...(deps.spawn ? { spawn: deps.spawn as ClineAcpDeps["spawn"] } : {}),
277
+ ...(deps.platform ? { platform: deps.platform } : {}),
278
+ ...(deps.timeoutMs !== undefined ? { timeoutMs: deps.timeoutMs } : {}),
279
+ ...(deps.killGraceMs !== undefined ? { killGraceMs: deps.killGraceMs } : {}),
280
+ ...(deps.reapTimeoutMs !== undefined ? { reapTimeoutMs: deps.reapTimeoutMs } : {}),
281
+ ...(deps.killWindowsProcessTree ? { killWindowsProcessTree: deps.killWindowsProcessTree } : {}),
282
+ };
283
+ const acpEnv = buildClineAcpEnv(sourceEnv);
284
+ let acpMode: ClineAcpMode = { acp: false };
285
+ try {
286
+ acpMode = await (deps.probeAcp ?? probeClineAcpSupport)(binary, acpEnv, acpDeps);
287
+ } catch {
288
+ acpMode = { acp: false };
289
+ }
290
+
291
+ if (acpMode.acp) {
292
+ let workspace: Awaited<ReturnType<typeof stageClineAcpWorkspace>> | undefined;
293
+ try {
294
+ workspace = await (deps.stageAcp ?? stageClineAcpWorkspace)(catalog.tools);
295
+ } catch (error) {
296
+ emit(settingsError(error));
297
+ return;
298
+ }
299
+ const mcpServers = workspace.catalogPath && catalog.emittedNameMap.size > 0
300
+ ? [{
301
+ type: "stdio",
302
+ name: CLINE_MCP_SERVER_NAME,
303
+ command: process.execPath,
304
+ args: isStandaloneBinary()
305
+ ? ["__cli-agent-mcp", workspace.catalogPath]
306
+ : [CLINE_MCP_SERVER_PATH, workspace.catalogPath],
307
+ env: [],
308
+ }]
309
+ : [];
310
+ let acpOutput = false;
311
+ const acpEmit = (event: AdapterEvent): void => {
312
+ if (event.type === "text_delta" || event.type === "thinking_delta"
313
+ || event.type === "tool_call_start" || event.type === "tool_call_delta" || event.type === "tool_call_end") {
314
+ acpOutput = true;
315
+ }
316
+ emit(event);
317
+ };
318
+ try {
319
+ await (deps.runAcp ?? runClineAcpTurn)({
320
+ binary,
321
+ env: acpEnv,
322
+ cwd: workspace.cwd,
323
+ modelId: requestedModel,
324
+ prompt: buildClinePrompt(parsed, vendorNameByWire, fixedSystem),
325
+ toolNames: vendorNames,
326
+ mcpServers,
327
+ args: acpMode.autoApproveFlag ? ["--acp", "--auto-approve", "false"] : ["--acp"],
328
+ maxToolCalls: 16,
329
+ requireToolCall: catalog.requireToolCall,
330
+ incoming,
331
+ deps: acpDeps,
332
+ }, acpEmit, acpDeps);
333
+ return;
334
+ } catch (error) {
335
+ if (!(error instanceof ClineAcpError && error.fallbackAllowed && !acpOutput && requestedModel === "default")) {
336
+ emit(acpFailure(error));
337
+ return;
338
+ }
339
+ if (error.authRequired) {
340
+ console.warn("[opencodex] Cline ACP authentication required [code=acp_auth_required]; using one-shot fallback.");
341
+ } else {
342
+ console.warn("[opencodex] Cline ACP unavailable; using one-shot fallback.");
343
+ }
344
+ } finally {
345
+ await workspace.cleanup();
346
+ }
347
+ }
348
+
349
+ if (requestedModel !== CLINE_CLI_MODEL_ID) {
350
+ emit({
351
+ type: "error",
352
+ message: `Cline ACP is required to select model "${requestedModel}". Enable ACP or use the default model.`,
353
+ status: 400,
354
+ errorType: "invalid_request_error",
355
+ code: "cline_model_unsupported",
356
+ retryable: false,
357
+ });
358
+ return;
359
+ }
360
+
255
361
  const providerSettingsPath = deps.providerSettingsPath
256
362
  ?? resolveClineProviderSettingsPath(sourceEnv);
257
- let selection: ClineSelection;
258
363
  try {
259
- selection = await assertClineSelectionSafe(providerSettingsPath);
364
+ await assertClineAdvisoryDestinationSafe(providerSettingsPath);
260
365
  } catch (error) {
261
366
  emit(settingsError(error));
262
367
  return;
@@ -307,25 +412,14 @@ export function createClineCliAdapter(
307
412
  message: preflight.detail || "Cline tool preflight failed.",
308
413
  status: 502,
309
414
  errorType: "upstream_error",
310
- code: "cline_tool_preflight_failed",
415
+ code: preflight.unsupported ? "cline_tool_preflight_unsupported" : "cline_tool_preflight_failed",
311
416
  retryable: false,
312
417
  });
313
418
  return;
314
419
  }
315
420
 
316
- const vendorNames = toolNameMap(catalog);
317
- const vendorNameByWire = new Map(
318
- [...catalog.emittedNameMap.values()].map(wireName => [
319
- wireName,
320
- wireName.startsWith("opencodex__") ? wireName : `opencodex__${wireName}`,
321
- ]),
322
- );
323
- const toolNames = vendorNames;
324
421
  const state = createClineStreamState();
325
- const maxToolCalls = 16;
326
- const fixedSystem = systemPromptWithToolNames(catalog);
327
- const capabilities = await (deps.probeFlags ?? probeClineFlagCapabilities)(binary, env);
328
- const args = buildClineArgs(selection, capabilities);
422
+ const capabilities = buildClineArgs();
329
423
  const promptFile = join(staged.root, "prompt.txt");
330
424
  try {
331
425
  await writeFile(promptFile, buildClinePrompt(parsed, vendorNameByWire, fixedSystem), {
@@ -349,7 +443,7 @@ export function createClineCliAdapter(
349
443
  await runCliAgentTurn({
350
444
  label: "Cline CLI",
351
445
  binary,
352
- args,
446
+ args: capabilities,
353
447
  env,
354
448
  cwd: staged.workspaceDir,
355
449
  stdin: "",
@@ -358,8 +452,8 @@ export function createClineCliAdapter(
358
452
  requireToolCall: catalog.requireToolCall,
359
453
  classifyFailure: ({ exitCode, stderr }) => classifyClineFailure(exitCode, stderr),
360
454
  parseLine: (message, current) => mapClineMessage(message, current, {
361
- toolNames,
362
- maxToolCalls,
455
+ toolNames: vendorNames,
456
+ maxToolCalls: 16,
363
457
  }),
364
458
  cleanup: staged.cleanup,
365
459
  }, incoming, emit, deps);
@@ -0,0 +1,29 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { whichFromPath, type WhichFn } from "../../coding-agent/profile";
5
+
6
+ /** Prefer the npm wrapper's cached native executable so stdio remains a regular file fd. */
7
+ export function resolveClineBinary(
8
+ which: WhichFn = whichFromPath,
9
+ env: NodeJS.ProcessEnv = process.env,
10
+ ): string | undefined {
11
+ const explicit = env.CLINE_BIN_PATH?.trim();
12
+ if (explicit && existsSync(explicit)) return explicit;
13
+ const resolved = which("cline");
14
+ if (!resolved) return undefined;
15
+ const cached = join(dirname(resolved), ".cline");
16
+ return existsSync(cached) ? cached : resolved;
17
+ }
18
+
19
+ /** Irreversible cache authority for one installed Cline binary. */
20
+ export function clineBinaryFingerprintSync(binary: string): string {
21
+ const path = resolve(binary);
22
+ try {
23
+ const value = statSync(path);
24
+ return createHash("sha256").update(JSON.stringify([path, value.mtimeMs, value.size])).digest("hex");
25
+ } catch {
26
+ return createHash("sha256").update(path).digest("hex");
27
+ }
28
+ }
29
+
@@ -0,0 +1,68 @@
1
+ import { ClineAcpClient, ClineAcpError, buildClineAcpEnv, clineAcpEnabled, clineModelSource, probeClineAcpSupport, stageClineAcpWorkspace, type ClineAcpDeps } from "./acp";
2
+ import { clineBinaryFingerprintSync, resolveClineBinary } from "./binary";
3
+
4
+ export type ClineLiveModelsFailure = "not_found" | "unsupported" | "auth_required" | "transport";
5
+
6
+ export type ClineLiveModelsResult =
7
+ | {
8
+ ok: true;
9
+ models: string[];
10
+ source: "models" | "configOptions" | "default";
11
+ fingerprint: string;
12
+ }
13
+ | {
14
+ ok: false;
15
+ reason: ClineLiveModelsFailure;
16
+ detail: string;
17
+ fingerprint?: string;
18
+ };
19
+
20
+ export interface ClineLiveModelsOptions {
21
+ binary?: string;
22
+ env?: NodeJS.ProcessEnv;
23
+ deps?: ClineAcpDeps;
24
+ }
25
+
26
+ /**
27
+ * One short ACP discovery session. The caller owns catalog freshness; this function only
28
+ * proves which native models the current binary and vendor account expose.
29
+ */
30
+ export async function discoverClineLiveModels(options: ClineLiveModelsOptions = {}): Promise<ClineLiveModelsResult> {
31
+ const sourceEnv = options.env ?? process.env;
32
+ if (!clineAcpEnabled(sourceEnv)) {
33
+ return { ok: false, reason: "unsupported", detail: "ACP is disabled by OPENCODEX_CLINE_ACP=0." };
34
+ }
35
+ const binary = options.binary ?? resolveClineBinary(undefined, sourceEnv);
36
+ if (!binary) return { ok: false, reason: "not_found", detail: "Cline CLI not found on PATH." };
37
+ const fingerprint = clineBinaryFingerprintSync(binary);
38
+ const env = buildClineAcpEnv(sourceEnv);
39
+ const mode = await probeClineAcpSupport(binary, env, { ...options.deps, env: sourceEnv });
40
+ if (!mode.acp) return { ok: false, reason: "unsupported", detail: "Cline --help does not advertise --acp.", fingerprint };
41
+ const workspace = await stageClineAcpWorkspace();
42
+ let client: ClineAcpClient | undefined;
43
+ try {
44
+ client = await ClineAcpClient.connect({
45
+ binary,
46
+ env,
47
+ cwd: workspace.cwd,
48
+ args: mode.autoApproveFlag ? ["--acp", "--auto-approve", "false"] : ["--acp"],
49
+ }, options.deps);
50
+ const session = await client.newSession({ cwd: workspace.cwd, mcpServers: [] });
51
+ const discovered = clineModelSource(session);
52
+ return { ok: true, models: discovered.ids, source: discovered.source, fingerprint };
53
+ } catch (error) {
54
+ if (error instanceof ClineAcpError && error.authRequired) {
55
+ return { ok: false, reason: "auth_required", detail: error.message, fingerprint };
56
+ }
57
+ return {
58
+ ok: false,
59
+ reason: "transport",
60
+ detail: error instanceof Error ? error.message : String(error),
61
+ fingerprint,
62
+ };
63
+ } finally {
64
+ await client?.terminate();
65
+ await workspace.cleanup();
66
+ }
67
+ }
68
+
@@ -75,6 +75,7 @@ export interface ClineEnabledTool {
75
75
 
76
76
  export interface ClineToolPreflightResult {
77
77
  ok: boolean;
78
+ unsupported?: boolean;
78
79
  detail?: string;
79
80
  enabledTools?: readonly ClineEnabledTool[];
80
81
  }
@@ -278,6 +279,19 @@ export async function assertClineSelectionSafe(providerSettingsPath: string): Pr
278
279
  };
279
280
  }
280
281
 
282
+ /**
283
+ * The vendor store is advisory in one-shot mode: absence or another schema is Cline's
284
+ * business. Only an explicit route back through this proxy remains a hard refusal.
285
+ */
286
+ export async function assertClineAdvisoryDestinationSafe(providerSettingsPath: string): Promise<void> {
287
+ try {
288
+ await assertClineSelectionSafe(providerSettingsPath);
289
+ } catch (error) {
290
+ if (error instanceof ClineSettingsError && error.code === "cli_provider_recursion") throw error;
291
+ if (!(error instanceof ClineSettingsError)) throw error;
292
+ }
293
+ }
294
+
281
295
  function clineGlobalSettings(): Record<string, unknown> {
282
296
  return {
283
297
  telemetryOptOut: true,
@@ -634,16 +648,16 @@ export async function preflightClineTools(input: {
634
648
  try {
635
649
  result = await runPreflightProcess(input.binary, ["config", "tools", "--json"], input.env, input.cwd);
636
650
  } catch (error) {
637
- return { ok: false, detail: error instanceof Error ? error.message : String(error) };
651
+ return { ok: false, unsupported: true, detail: error instanceof Error ? error.message : String(error) };
638
652
  }
639
653
  if (result.exitCode !== 0) {
640
- return { ok: false, detail: result.stderr.trim() || `Cline tool preflight exited with code ${result.exitCode}.` };
654
+ return { ok: false, unsupported: true, detail: result.stderr.trim() || `Cline tool preflight exited with code ${result.exitCode}.` };
641
655
  }
642
656
  let parsed: unknown;
643
657
  try {
644
658
  parsed = JSON.parse(result.stdout);
645
659
  } catch {
646
- return { ok: false, detail: "Cline tool preflight returned non-JSON output." };
660
+ return { ok: false, unsupported: true, detail: "Cline tool preflight returned non-JSON output." };
647
661
  }
648
662
  return validateClineToolCatalog(parsed);
649
663
  }
@@ -104,12 +104,14 @@ function toolArguments(input: unknown): string {
104
104
  return serialized;
105
105
  }
106
106
 
107
- function mapToolCall(
108
- event: Record<string, unknown>,
107
+ /** Map a validated tool intent from either Cline's NDJSON or ACP event shape. */
108
+ export function mapClineToolIntent(
109
+ vendorName: string,
110
+ toolCallId: string | undefined,
111
+ input: unknown,
109
112
  state: CliAgentStreamState,
110
113
  options: ClineStreamOptions,
111
114
  ): AdapterEvent[] {
112
- const vendorName = asString(event.toolName);
113
115
  if (!vendorName) throw new ClineStreamProtocolError("Cline returned a tool call without a name.");
114
116
  const wireName = options.toolNames.get(vendorName);
115
117
  if (!wireName) {
@@ -120,8 +122,8 @@ function mapToolCall(
120
122
  throw new ClineStreamProtocolError("Cline exceeded the per-turn tool-call limit.");
121
123
  }
122
124
  const ordinal = ++state.nextToolOrdinal;
123
- const id = asString(event.toolCallId)?.trim() || `cline-tool-${ordinal}`;
124
- const argumentsJson = toolArguments(event.input);
125
+ const id = toolCallId?.trim() || `cline-tool-${ordinal}`;
126
+ const argumentsJson = toolArguments(input);
125
127
  state.toolCalls++;
126
128
  return [
127
129
  { type: "tool_call_start", id, name: wireName },
@@ -130,6 +132,14 @@ function mapToolCall(
130
132
  ];
131
133
  }
132
134
 
135
+ function mapToolCall(
136
+ event: Record<string, unknown>,
137
+ state: CliAgentStreamState,
138
+ options: ClineStreamOptions,
139
+ ): AdapterEvent[] {
140
+ return mapClineToolIntent(asString(event.toolName) ?? "", asString(event.toolCallId), event.input, state, options);
141
+ }
142
+
133
143
  function mapAgentEvent(
134
144
  event: Record<string, unknown>,
135
145
  state: CliAgentStreamState,
@@ -377,6 +377,8 @@ const commandRunners: Record<string, CommandRunner> = {
377
377
  // It is deliberately not `--code`: that name carries a secret value everywhere else in the
378
378
  // CLI (`ocx account login <provider> --code <value>`) and its operand is scrubbed from stderr.
379
379
  const codeMode = loginArgs.includes("--headless");
380
+ const json = loginArgs.includes("--json");
381
+ const noWait = loginArgs.includes("--no-wait");
380
382
  const positional = loginArgs.filter(arg => !arg.startsWith("--"));
381
383
  // 'ocx login codex' is the command people type first, and until now it answered with
382
384
  // the full provider wall because the Codex pool lives behind 'ocx account login'.
@@ -397,7 +399,11 @@ const commandRunners: Record<string, CommandRunner> = {
397
399
  return code ?? 1;
398
400
  }
399
401
  const { handleLogin } = await import("../oauth/login-cli");
400
- await handleLogin(positional[0], {}, { codeMode });
402
+ if (positional[0] !== "cline-cli" && (json || noWait)) {
403
+ console.error("--json and --no-wait are only supported by `ocx login cline-cli`.");
404
+ return 1;
405
+ }
406
+ await handleLogin(positional[0], {}, { codeMode, json, noWait });
401
407
  return 0;
402
408
  },
403
409
  logout: async deps => {
@@ -237,10 +237,11 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
237
237
  },
238
238
  {
239
239
  name: "login",
240
- usage: "ocx login <provider> [--headless]",
241
- summary: "OAuth or API-key login for a provider.",
240
+ usage: "ocx login <provider> [--headless] [--json] [--no-wait]",
241
+ summary: "OAuth, API-key, or ACP login for a provider.",
242
242
  details: [
243
243
  "--headless (anthropic, anthropic2): no browser and no local port; approve in any browser and paste the displayed code.",
244
+ "cline-cli: ACP auth; --json emits bounded method/instruction state and --no-wait returns after relaying instructions.",
244
245
  "Codex/ChatGPT: ocx login codex runs the Codex account-pool login (same flow as ocx account login codex).",
245
246
  "That one needs a running proxy; the OAuth and API-key providers log in locally.",
246
247
  "'chatgpt' and 'openai' are the same route; an OpenAI platform API key is 'ocx login openai-apikey'.",
@@ -500,6 +500,9 @@ export function suppressedSyntheticMaxCatalogSlugs(
500
500
  export const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai", "google-antigravity"]);
501
501
 
502
502
  export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly<Record<string, ReadonlySet<string>>> = {
503
+ // Cline's live ACP roster names concrete native model ids, while `default` remains the
504
+ // callable compatibility selector for the vendor's current model.
505
+ "cline-cli": new Set(["default"]),
503
506
  // CodeBuddy's vendor defaults are real callable selectors — both the bundled manifests
504
507
  // (`product.json` / `product.internal.json`) and `--model` accept them — but the key-scoped
505
508
  // configuration roster omits them. Without this entry, a successful live roster
@@ -67,6 +67,7 @@ import { fetchQoderModels } from "../../adapters/qoder/live-models";
67
67
  import { resolveQoderProfile } from "../../adapters/qoder/profiles";
68
68
  import { CODEBUDDY_PROFILES, type CodeBuddyProfile } from "../../adapters/codebuddy/profiles";
69
69
  import { fetchCodeBuddyModels } from "../../adapters/codebuddy/live-models";
70
+ import { clineBinaryFingerprintSync, resolveClineBinary } from "../../adapters/cli-agent/cline/binary";
70
71
  import { resolveProfileByBaseUrl } from "../../adapters/coding-agent/profile";
71
72
  import { fetchDevinUsableModels } from "../../adapters/devin/live-models";
72
73
  import { resolveDevinApiBaseUrl } from "../../oauth/devin/api-base";
@@ -272,6 +273,50 @@ export async function fetchProviderModelsWithAuth(
272
273
  }
273
274
  return observed(configured, "authoritative");
274
275
  }
276
+ if (prov.adapter === "cline-cli") {
277
+ const binary = resolveClineBinary(undefined, process.env);
278
+ if (!binary) return observed(configured, "degraded");
279
+ const authorityIdentity = clineBinaryFingerprintSync(binary);
280
+ const fresh = getFreshCached(name, ttlMs, Date.now(), authorityIdentity);
281
+ if (fresh) {
282
+ return observed(withConfiguredRetention(
283
+ applyConfigHintsToCachedModels(name, prov, fresh, contextCap, metadataModelIdCaseFold, captured.effectiveAlias, metadataConfigDir, false),
284
+ ), "authoritative");
285
+ }
286
+ const stale = getStaleCached(name, authorityIdentity);
287
+ if (isModelsFetchCoolingDown(name, 60_000, Date.now(), authorityIdentity)) {
288
+ return observed(withConfiguredRetention(
289
+ stale
290
+ ? applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold, captured.effectiveAlias, metadataConfigDir, false)
291
+ : configured,
292
+ ), "degraded");
293
+ }
294
+ const { discoverClineLiveModels } = await import("../../adapters/cli-agent/cline/live-models");
295
+ const live = await discoverClineLiveModels({ binary });
296
+ if (live.ok) {
297
+ const discovered = live.models.map(id => ({
298
+ id,
299
+ provider: name,
300
+ ...catalogHintsFromProviderConfig(name, prov, id, contextCap, metadataModelIdCaseFold, captured.effectiveAlias, metadataConfigDir, false),
301
+ }));
302
+ const forCache = withConfiguredRetention(discovered, { retainComboTargets: false });
303
+ if (!setCached(name, forCache, Date.now(), cacheGeneration, authorityIdentity)) {
304
+ return observed(withConfiguredRetention(configured), "degraded");
305
+ }
306
+ markProviderDiscoveryOk(name, live.models.length);
307
+ return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
308
+ }
309
+ if (isCurrentCacheGeneration()) {
310
+ markModelsFetchFailure(name, Date.now(), authorityIdentity);
311
+ markProviderDiscoveryFailed(name, { reason: "provider" });
312
+ console.warn(
313
+ `[opencodex] Cline ACP model discovery failed [${live.reason}]${live.detail ? `: ${live.detail}` : ""}; using stale/static catalog degradation.`,
314
+ );
315
+ }
316
+ return observed(withConfiguredRetention(
317
+ stale ? applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold, captured.effectiveAlias, metadataConfigDir, false) : configured,
318
+ ), "degraded");
319
+ }
275
320
  const auth: ModelsAuthResolution = prov.authMode === "oauth" && !mayResolveModelsOAuth(name, prov)
276
321
  ? { apiKey: undefined, observed: resolveAuth.kind === "observed" }
277
322
  : captured.observedAuth ?? (resolveAuth.kind === "refreshing"