@koreanhole/opencodex 2.99.0 → 3.1.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.
@@ -6,8 +6,10 @@ import { baseScopedEnv } from "../env";
6
6
  /** Built-in, subagent, and team tools that a model-only Cline turn must never enable. */
7
7
  export const CLINE_DISABLED_TOOL_IDS = Object.freeze([
8
8
  "read_files",
9
+ "list_files",
9
10
  "search_codebase",
10
11
  "run_commands",
12
+ "run_terminal_command",
11
13
  "fetch_web_content",
12
14
  "apply_patch",
13
15
  "editor",
@@ -68,6 +70,10 @@ export interface ClineSelection {
68
70
  model?: string;
69
71
  }
70
72
 
73
+ export type ClineMcpLaunch =
74
+ | { kind: "module"; modulePath: string }
75
+ | { kind: "entrypoint"; entrypoint: string };
76
+
71
77
  interface ClineSelectionCandidate {
72
78
  providerId: string;
73
79
  model?: string;
@@ -261,12 +267,13 @@ export async function assertClineSelectionSafe(providerSettingsPath: string): Pr
261
267
  * The vendor store is advisory: absence or another schema is Cline's business.
262
268
  * Only an explicit route back through this proxy remains a hard refusal.
263
269
  */
264
- export async function assertClineAdvisoryDestinationSafe(providerSettingsPath: string): Promise<void> {
270
+ export async function assertClineAdvisoryDestinationSafe(providerSettingsPath: string): Promise<ClineSelection | undefined> {
265
271
  try {
266
- await assertClineSelectionSafe(providerSettingsPath);
272
+ return await assertClineSelectionSafe(providerSettingsPath);
267
273
  } catch (error) {
268
274
  if (error instanceof ClineSettingsError && error.code === "cli_provider_recursion") throw error;
269
275
  if (!(error instanceof ClineSettingsError)) throw error;
276
+ return undefined;
270
277
  }
271
278
  }
272
279
 
@@ -285,8 +292,25 @@ function clineGlobalSettings(): Record<string, unknown> {
285
292
  };
286
293
  }
287
294
 
288
- function clineMcpSettings(): Record<string, unknown> {
289
- return { mcpServers: {} };
295
+ function clineMcpArgs(launch: ClineMcpLaunch, catalogPath: string): string[] {
296
+ return launch.kind === "module"
297
+ ? [launch.modulePath, catalogPath]
298
+ : [launch.entrypoint, catalogPath];
299
+ }
300
+
301
+ function clineMcpSettings(launch: ClineMcpLaunch | undefined, catalogPath: string): Record<string, unknown> {
302
+ if (!launch) return { mcpServers: {} };
303
+ return {
304
+ mcpServers: {
305
+ opencodex: {
306
+ transport: {
307
+ type: "stdio",
308
+ command: process.execPath,
309
+ args: clineMcpArgs(launch, catalogPath),
310
+ },
311
+ },
312
+ },
313
+ };
290
314
  }
291
315
 
292
316
  /** Share a staged file at a vendor-default path. */
@@ -346,6 +370,7 @@ async function linkClineSettings(input: {
346
370
  export async function stageClineTurn(options: {
347
371
  providerSettingsPath: string;
348
372
  catalog: readonly Record<string, unknown>[];
373
+ mcpLaunch?: ClineMcpLaunch;
349
374
  }): Promise<ClineStagedTurn> {
350
375
  const root = await mkdtemp(join(tmpdir(), "ocx-cline-cli-"));
351
376
  const configDir = join(root, "config");
@@ -378,7 +403,7 @@ export async function stageClineTurn(options: {
378
403
  ]);
379
404
  await Promise.all([
380
405
  writeFile(globalSettingsPath, `${JSON.stringify(clineGlobalSettings(), null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" }),
381
- writeFile(mcpSettingsPath, `${JSON.stringify(clineMcpSettings(), null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" }),
406
+ writeFile(mcpSettingsPath, `${JSON.stringify(clineMcpSettings(options.mcpLaunch, catalogPath), null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" }),
382
407
  writeFile(catalogPath, JSON.stringify(options.catalog), { encoding: "utf8", mode: 0o600, flag: "wx" }),
383
408
  ]);
384
409
  const copiedProviderStore = await copyClineProviderSettings(options.providerSettingsPath, providerSettingsPath);
@@ -409,6 +434,40 @@ export async function stageClineTurn(options: {
409
434
  }
410
435
  }
411
436
 
437
+ /** Merge newly discovered built-in IDs into the private staged disable list. */
438
+ export async function mergeClineDisabledTools(
439
+ staged: Pick<ClineStagedTurn, "globalSettingsPath">,
440
+ ids: readonly string[],
441
+ ): Promise<void> {
442
+ let parsed: unknown;
443
+ try {
444
+ parsed = JSON.parse(await readFile(staged.globalSettingsPath, "utf8"));
445
+ } catch {
446
+ throw new ClineSettingsError(
447
+ "Cline global settings are malformed; the tool preflight cannot be repaired.",
448
+ 500,
449
+ "cline_cli_setup_failed",
450
+ );
451
+ }
452
+ const settings = asRecord(parsed);
453
+ if (!settings) {
454
+ throw new ClineSettingsError(
455
+ "Cline global settings are malformed; the tool preflight cannot be repaired.",
456
+ 500,
457
+ "cline_cli_setup_failed",
458
+ );
459
+ }
460
+ const current = Array.isArray(settings.disabledTools)
461
+ ? settings.disabledTools.filter((value): value is string => typeof value === "string")
462
+ : [];
463
+ const merged = [...new Set([...current, ...ids])];
464
+ await writeFile(
465
+ staged.globalSettingsPath,
466
+ `${JSON.stringify({ ...settings, disabledTools: merged }, null, 2)}\n`,
467
+ { encoding: "utf8", mode: 0o600 },
468
+ );
469
+ }
470
+
412
471
  export function buildClineChildEnv(staged: ClineStagedTurn, sourceEnv: NodeJS.ProcessEnv = process.env): Record<string, string> {
413
472
  const env = baseScopedEnv();
414
473
  env.HOME = staged.homeDir;
@@ -1,6 +1,9 @@
1
- import type { AdapterEvent } from "../../../types";
1
+ import type { AdapterEvent, OcxUsage } from "../../../types";
2
2
  import type { CliAgentStreamState } from "../types";
3
3
  import { mapToolIntent, ToolIntentError } from "../tool-intent";
4
+ import { redactSecretString } from "../../../lib/redact";
5
+
6
+ const MAX_ERROR_CHARS = 2_048;
4
7
 
5
8
  export class ClineStreamProtocolError extends Error {
6
9
  readonly code = "cli_protocol_error";
@@ -42,3 +45,200 @@ export function mapClineToolIntent(
42
45
  throw error;
43
46
  }
44
47
  }
48
+
49
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
50
+ return value && typeof value === "object" && !Array.isArray(value)
51
+ ? value as Record<string, unknown>
52
+ : undefined;
53
+ }
54
+
55
+ function asString(value: unknown): string | undefined {
56
+ return typeof value === "string" ? value : undefined;
57
+ }
58
+
59
+ function safeMessage(value: unknown): string {
60
+ const raw = typeof value === "string"
61
+ ? value
62
+ : value instanceof Error
63
+ ? value.message
64
+ : value === undefined
65
+ ? "Cline reported an error."
66
+ : JSON.stringify(value) ?? String(value);
67
+ return redactSecretString(raw).slice(0, MAX_ERROR_CHARS);
68
+ }
69
+
70
+ function classifyError(message: string): { status: number; errorType: string; code: string; retryable: boolean } {
71
+ if (/not logged in|please (?:run )?\/?login|sign in|authentication (?:failed|required)|unauthorized|invalid (?:api )?key/i.test(message)) {
72
+ return { status: 401, errorType: "authentication_error", code: "cline_cli_not_logged_in", retryable: false };
73
+ }
74
+ if (/rate limit|too many requests|quota|credit|usage limit|daily limit/i.test(message)) {
75
+ return { status: 429, errorType: "rate_limit_error", code: "rate_limit_exceeded", retryable: true };
76
+ }
77
+ if (/model (?:is )?(?:not found|unavailable|unsupported)|invalid model/i.test(message)) {
78
+ return { status: 400, errorType: "invalid_request_error", code: "model_not_found", retryable: false };
79
+ }
80
+ return { status: 502, errorType: "upstream_error", code: "cli_protocol_error", retryable: false };
81
+ }
82
+
83
+ function usageFrom(value: unknown): OcxUsage | undefined {
84
+ const record = asRecord(value);
85
+ if (!record) return undefined;
86
+ const count = (input: unknown): number =>
87
+ typeof input === "number" && Number.isFinite(input) && input >= 0 ? Math.floor(input) : 0;
88
+ const inputTokens = count(record.inputTokens);
89
+ const outputTokens = count(record.outputTokens);
90
+ const cacheRead = count(record.cacheReadTokens);
91
+ const cacheWrite = count(record.cacheWriteTokens);
92
+ if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && cacheWrite === 0) return undefined;
93
+ return {
94
+ inputTokens,
95
+ outputTokens,
96
+ totalTokens: inputTokens + outputTokens,
97
+ ...(cacheRead > 0 ? { cachedInputTokens: cacheRead, cacheReadInputTokens: cacheRead } : {}),
98
+ ...(cacheWrite > 0 ? { cacheCreationInputTokens: cacheWrite } : {}),
99
+ };
100
+ }
101
+
102
+ function mapAgentEvent(
103
+ event: Record<string, unknown>,
104
+ state: CliAgentStreamState,
105
+ options: ClineStreamOptions,
106
+ ): AdapterEvent[] {
107
+ const type = asString(event.type);
108
+ if (type === "content_start") {
109
+ const contentType = asString(event.contentType);
110
+ if (contentType === "text") {
111
+ const text = asString(event.text);
112
+ if (!text) return [];
113
+ state.sawText = true;
114
+ return [{ type: "text_delta", text }];
115
+ }
116
+ if (contentType === "reasoning") {
117
+ const thinking = asString(event.reasoning);
118
+ if (!thinking) return [];
119
+ state.sawThinking = true;
120
+ return [{ type: "thinking_delta", thinking }];
121
+ }
122
+ if (contentType === "tool") {
123
+ const vendorName = asString(event.toolName);
124
+ if (!vendorName) throw new ClineStreamProtocolError("Cline returned a tool call without a name.");
125
+ return mapClineToolIntent(vendorName, asString(event.toolCallId), event.input, state, options);
126
+ }
127
+ return [];
128
+ }
129
+
130
+ if (type === "content_end") {
131
+ const contentType = asString(event.contentType);
132
+ if (contentType === "text" && !state.sawText) {
133
+ const text = asString(event.text);
134
+ if (text) {
135
+ state.sawText = true;
136
+ return [{ type: "text_delta", text }];
137
+ }
138
+ }
139
+ if (contentType === "reasoning" && !state.sawThinking) {
140
+ const thinking = asString(event.reasoning);
141
+ if (thinking) {
142
+ state.sawThinking = true;
143
+ return [{ type: "thinking_delta", thinking }];
144
+ }
145
+ }
146
+ return [];
147
+ }
148
+
149
+ if (type === "usage") {
150
+ const usage = usageFrom(event);
151
+ if (usage) state.usage = usage;
152
+ return [];
153
+ }
154
+
155
+ if (type === "error") {
156
+ if (event.recoverable === true) return [];
157
+ const message = safeMessage(asRecord(event.error)?.message ?? event.error ?? "Cline reported an error.");
158
+ state.lastError = message;
159
+ state.terminal = true;
160
+ return [{ type: "error", message, ...classifyError(message) }];
161
+ }
162
+
163
+ if (type === "done") {
164
+ state.terminal = true;
165
+ if (state.toolCalls > 0) return [];
166
+ const events: AdapterEvent[] = [];
167
+ if (!state.sawText) {
168
+ const text = asString(event.text);
169
+ if (text) {
170
+ state.sawText = true;
171
+ events.push({ type: "text_delta", text });
172
+ }
173
+ }
174
+ const usage = usageFrom(event.usage) ?? state.usage;
175
+ events.push({ type: "done", stopReason: "stop", ...(usage ? { usage } : {}) });
176
+ return events;
177
+ }
178
+
179
+ return [];
180
+ }
181
+
182
+ /** Map one bounded Cline JSON frame to provider-neutral events. */
183
+ export function mapClineMessage(
184
+ message: Record<string, unknown>,
185
+ state: CliAgentStreamState,
186
+ options: ClineStreamOptions,
187
+ ): AdapterEvent[] {
188
+ if (state.terminal) return [];
189
+ const type = asString(message.type);
190
+ if (type === "agent_event") {
191
+ const event = asRecord(message.event);
192
+ return event ? mapAgentEvent(event, state, options) : [];
193
+ }
194
+
195
+ if (type === "run_result") {
196
+ if (state.toolCalls > 0) return [];
197
+ const usage = usageFrom(message.usage) ?? state.usage;
198
+ if (usage) state.usage = usage;
199
+ const finishReason = asString(message.finishReason);
200
+ if (finishReason && finishReason !== "completed" && finishReason !== "stop") {
201
+ const messageText = safeMessage(message.text ?? `Cline finished with ${finishReason}.`);
202
+ state.terminal = true;
203
+ return [{ type: "error", message: messageText, ...classifyError(messageText) }];
204
+ }
205
+ state.terminal = true;
206
+ const events: AdapterEvent[] = [];
207
+ if (!state.sawText) {
208
+ const text = asString(message.text);
209
+ if (text) {
210
+ state.sawText = true;
211
+ events.push({ type: "text_delta", text });
212
+ }
213
+ }
214
+ events.push({ type: "done", stopReason: "stop", ...(usage ? { usage } : {}) });
215
+ return events;
216
+ }
217
+
218
+ if (type === "run_aborted") {
219
+ state.terminal = true;
220
+ return [{
221
+ type: "error",
222
+ message: safeMessage(message.message ?? "Cline aborted the turn."),
223
+ status: 499,
224
+ errorType: "invalid_request_error",
225
+ code: "client_cancelled",
226
+ retryable: false,
227
+ }];
228
+ }
229
+
230
+ if (type === "error") {
231
+ const detail = safeMessage(message.message ?? "Cline reported an error.");
232
+ state.lastError = detail;
233
+ state.terminal = true;
234
+ return [{ type: "error", message: detail, ...classifyError(detail) }];
235
+ }
236
+
237
+ return [];
238
+ }
239
+
240
+ /** Convert a nonzero process exit into a stable terminal error. */
241
+ export function classifyClineFailure(exitCode: number, stderr: string): Extract<AdapterEvent, { type: "error" }> {
242
+ const detail = safeMessage(stderr.trim() || `Cline exited with code ${exitCode}.`);
243
+ return { type: "error", message: detail, ...classifyError(detail) };
244
+ }
@@ -177,7 +177,10 @@ async function runGeminiHelp(binary: string, env: Record<string, string>, deps:
177
177
  }
178
178
 
179
179
  /** Build the Gemini ACP environment while leaving vendor auth in its real home. */
180
- export function buildGeminiAcpEnv(sourceEnv: NodeJS.ProcessEnv = process.env): Record<string, string> {
180
+ export function buildGeminiAcpEnv(
181
+ sourceEnv: NodeJS.ProcessEnv = process.env,
182
+ platform: NodeJS.Platform = process.platform,
183
+ ): Record<string, string> {
181
184
  const env = baseScopedEnv();
182
185
  const realHome = nonEmpty(sourceEnv.HOME) ?? nonEmpty(sourceEnv.USERPROFILE) ?? homedir();
183
186
  env.HOME = realHome;
@@ -192,6 +195,8 @@ export function buildGeminiAcpEnv(sourceEnv: NodeJS.ProcessEnv = process.env): R
192
195
  NO_UPDATE_NOTIFIER: "1",
193
196
  NO_COLOR: "1",
194
197
  });
198
+ // Windows Node needs the OS store for corporate TLS roots.
199
+ if (platform === "win32") env.NODE_USE_SYSTEM_CA = "1";
195
200
  return env;
196
201
  }
197
202
 
@@ -287,7 +287,7 @@ export async function runGeminiTurn(
287
287
  ...(deps.reapTimeoutMs !== undefined ? { reapTimeoutMs: deps.reapTimeoutMs } : {}),
288
288
  ...(deps.killWindowsProcessTree ? { killWindowsProcessTree: deps.killWindowsProcessTree } : {}),
289
289
  };
290
- const acpEnv = buildGeminiAcpEnv(sourceEnv);
290
+ const acpEnv = buildGeminiAcpEnv(sourceEnv, deps.platform ?? process.platform);
291
291
  let mode;
292
292
  try {
293
293
  mode = await (deps.probeAcp ?? probeGeminiAcpSupport)(binary, acpEnv, acpDeps);
@@ -105,7 +105,7 @@ export async function discoverGeminiLiveModels(options: GeminiLiveModelsOptions
105
105
  ?? { ok: false, reason: "backoff", detail: "Gemini model discovery is cooling down after a failure.", fingerprint };
106
106
  }
107
107
 
108
- const env = buildGeminiAcpEnv(sourceEnv);
108
+ const env = buildGeminiAcpEnv(sourceEnv, options.deps?.platform ?? process.platform);
109
109
  const mode = await probeGeminiAcpSupport(binary, env, { ...options.deps, env: sourceEnv });
110
110
  if (!mode.acp) {
111
111
  failureCache.set(fingerprint, timestamp);
@@ -23,5 +23,6 @@ export interface CliAgentTurnDeps {
23
23
  timeoutMs?: number;
24
24
  killGraceMs?: number;
25
25
  reapTimeoutMs?: number;
26
+ toolBatchIdleMs?: number;
26
27
  killWindowsProcessTree?: (pid: number) => void;
27
28
  }
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env bun
2
+ import { randomBytes } from "node:crypto";
3
+ import { rmSync } from "node:fs";
4
+ import {
5
+ ADAPTER_DEFAULT_PORT,
6
+ ADAPTER_PROVIDER_IDS,
7
+ ADAPTER_TOKEN_ENV,
8
+ AdapterConfigError,
9
+ adapterRuntimePath,
10
+ discoverManagedProviderMetadata,
11
+ readAdapterRuntime,
12
+ readUpstreamConfig,
13
+ removeAdapterRuntimeIfPid,
14
+ restoreManagedProviderRows,
15
+ setAdapterDefaultProvider,
16
+ syncManagedProviderRows,
17
+ writeAdapterRuntime,
18
+ type AdapterProviderId,
19
+ } from "../adapter-runtime/config";
20
+ import { startAdapterGateway, type AdapterGateway } from "../adapter-runtime/gateway";
21
+ import { ConfigError } from "../config";
22
+ import { UpstreamPackageError, resolveUpstreamPackage } from "../upstream/package";
23
+ import { loadUpstreamCli } from "../upstream/cli";
24
+ import { runAdapterCli } from "./index";
25
+ import packageInfo from "../../package.json";
26
+
27
+ type AdapterCli = (argv: string[]) => Promise<number>;
28
+ type UpstreamLoader = () => Promise<void>;
29
+ type StartPreparer = (env: NodeJS.ProcessEnv) => Promise<unknown>;
30
+
31
+ export interface EntryDeps {
32
+ adapterCli?: AdapterCli;
33
+ loadUpstream?: UpstreamLoader;
34
+ prepareStart?: StartPreparer;
35
+ }
36
+
37
+ const DISTRIBUTION_PACKAGE = "@koreanhole/opencodex";
38
+
39
+ function isAdapterId(value: string | undefined): value is AdapterProviderId {
40
+ return value === "gemini-cli" || value === "cline-cli";
41
+ }
42
+
43
+ export function isAdapterOwnedCommand(argv: readonly string[]): boolean {
44
+ const command = argv[0];
45
+ if (command === "__cli-agent-mcp") return true;
46
+ if (command === "version" || command === "--version" || command === "-v") return true;
47
+ if (command === "update") return true;
48
+ if (command === "system" && argv.slice(1).includes("update")) return true;
49
+ if (command === "login" && (argv[1] === "cline-cli" || argv[1] === "gemini-cli")) return true;
50
+ if (command === "adapter" && argv[1] === "detach") return true;
51
+ if (command !== "provider") return false;
52
+ if (argv[1] === "add" && isAdapterId(argv[2])) return true;
53
+ if (argv[1] === "use" && isAdapterId(argv[2])) return true;
54
+ return false;
55
+ }
56
+
57
+ function takeFlag(args: string[], flag: string): boolean {
58
+ const index = args.indexOf(flag);
59
+ if (index < 0) return false;
60
+ args.splice(index, 1);
61
+ return true;
62
+ }
63
+
64
+ async function adapterProviderCommand(args: string[], env: NodeJS.ProcessEnv): Promise<number> {
65
+ const sub = args.shift();
66
+ if (sub !== "add" && sub !== "use") throw new ConfigError("Unknown provider command: " + String(sub ?? "(missing)"));
67
+ const provider = args.shift();
68
+ if (!isAdapterId(provider)) throw new ConfigError("Only gemini-cli and cline-cli are handled for provider " + sub + ".");
69
+ if (sub === "add") {
70
+ const setDefault = takeFlag(args, "--set-default");
71
+ if (args.length > 0) throw new ConfigError("Unknown provider add option: " + args[0]);
72
+ const runtime = await readAdapterRuntime(env);
73
+ await syncManagedProviderRows({
74
+ env,
75
+ gatewayPort: runtime?.port ?? ADAPTER_DEFAULT_PORT,
76
+ ...(setDefault ? { defaultProvider: provider } : {}),
77
+ });
78
+ console.log("Configured " + provider + ".");
79
+ return 0;
80
+ }
81
+ if (args.length > 0) throw new ConfigError("Unknown provider use option: " + args[0]);
82
+ await setAdapterDefaultProvider(provider, env);
83
+ console.log("Default provider: " + provider);
84
+ return 0;
85
+ }
86
+
87
+ async function detachAdapter(env: NodeJS.ProcessEnv): Promise<number> {
88
+ const result = await restoreManagedProviderRows(env);
89
+ if (!result.restored) {
90
+ console.log("CLI adapters are not attached.");
91
+ return 0;
92
+ }
93
+ if (result.preservedUserRows.length > 0) {
94
+ console.log("Preserved user-owned provider rows: " + result.preservedUserRows.join(", "));
95
+ }
96
+ console.log("CLI adapter provider rows detached.");
97
+ return 0;
98
+ }
99
+
100
+ function printDistributionUpdate(argv: readonly string[]): number {
101
+ const command = "npm install -g " + DISTRIBUTION_PACKAGE + "@latest";
102
+ if (argv.includes("--json")) {
103
+ console.log(JSON.stringify({
104
+ ok: false,
105
+ reason: "distribution_managed",
106
+ package: DISTRIBUTION_PACKAGE,
107
+ command,
108
+ }));
109
+ } else {
110
+ console.error("ocx: updates are managed by " + DISTRIBUTION_PACKAGE + ".");
111
+ console.error("Run: " + command);
112
+ }
113
+ return 2;
114
+ }
115
+
116
+ function printVersion(): number {
117
+ console.log("opencodex " + packageInfo.version);
118
+ return 0;
119
+ }
120
+
121
+ async function runAdapterCommand(argv: string[], env: NodeJS.ProcessEnv, adapterCli: AdapterCli): Promise<number> {
122
+ if (argv[0] === "__cli-agent-mcp") return adapterCli(argv);
123
+ if (argv[0] === "version" || argv[0] === "--version" || argv[0] === "-v") return printVersion();
124
+ if (argv[0] === "login") return adapterCli(argv);
125
+ if (argv[0] === "update" || (argv[0] === "system" && argv.slice(1).includes("update"))) {
126
+ return printDistributionUpdate(argv);
127
+ }
128
+ if (argv[0] === "adapter" && argv[1] === "detach") return detachAdapter(env);
129
+ if (argv[0] === "provider") return adapterProviderCommand(argv.slice(1), env);
130
+ throw new ConfigError("Internal adapter dispatch error.");
131
+ }
132
+
133
+ function pidAlive(pid: number): boolean {
134
+ try {
135
+ process.kill(pid, 0);
136
+ return true;
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ /** Attach the two managed rows and keep the gateway in the serving process. */
143
+ export async function prepareAdapterStart(
144
+ env: NodeJS.ProcessEnv = process.env,
145
+ deps: {
146
+ discoverMetadata?: typeof discoverManagedProviderMetadata;
147
+ startGateway?: typeof startAdapterGateway;
148
+ syncRows?: typeof syncManagedProviderRows;
149
+ } = {},
150
+ ): Promise<AdapterGateway | undefined> {
151
+ const existing = await readAdapterRuntime(env);
152
+ const inheritedToken = env[ADAPTER_TOKEN_ENV]?.trim();
153
+ if (existing && existing.pid !== process.pid && pidAlive(existing.pid) && !inheritedToken) {
154
+ throw new AdapterConfigError(
155
+ "The CLI adapter gateway is already running in another process; restart it from that process or stop it first.",
156
+ );
157
+ }
158
+ const token = inheritedToken || randomBytes(32).toString("base64url");
159
+ env[ADAPTER_TOKEN_ENV] = token;
160
+ let gateway: AdapterGateway | undefined;
161
+ let gatewayPort: number;
162
+ if (existing && existing.pid !== process.pid && pidAlive(existing.pid)) {
163
+ gatewayPort = existing.port;
164
+ } else {
165
+ if (existing) await removeAdapterRuntimeIfPid(existing.pid, env);
166
+ gateway = (deps.startGateway ?? startAdapterGateway)({ token, env });
167
+ gatewayPort = gateway.port;
168
+ await writeAdapterRuntime({
169
+ version: 1,
170
+ pid: process.pid,
171
+ host: "127.0.0.1",
172
+ port: gatewayPort,
173
+ tokenEnv: ADAPTER_TOKEN_ENV,
174
+ providers: [...ADAPTER_PROVIDER_IDS],
175
+ startedAt: new Date().toISOString(),
176
+ }, env);
177
+ process.once("exit", () => {
178
+ try {
179
+ rmSync(adapterRuntimePath(env), { force: true });
180
+ } catch {
181
+ // best effort
182
+ }
183
+ });
184
+ }
185
+
186
+ try {
187
+ const metadata = await (deps.discoverMetadata ?? discoverManagedProviderMetadata)(env).catch(() => undefined);
188
+ const synced = await (deps.syncRows ?? syncManagedProviderRows)({
189
+ env,
190
+ gatewayPort,
191
+ ...(metadata ? { metadata } : {}),
192
+ });
193
+ if (synced.providerEnvToken) env.OPENCODEX_API_AUTH_TOKEN ??= synced.providerEnvToken;
194
+ return gateway;
195
+ } catch (error) {
196
+ await gateway?.stop().catch(() => undefined);
197
+ await removeAdapterRuntimeIfPid(process.pid, env).catch(() => undefined);
198
+ throw error;
199
+ }
200
+ }
201
+
202
+ export async function runEntry(argv: string[], deps: EntryDeps = {}): Promise<void> {
203
+ const env = process.env;
204
+ env.OCX_DISABLE_UPDATE_CHECK ??= "1";
205
+ if (isAdapterOwnedCommand(argv)) {
206
+ process.exitCode = await runAdapterCommand(argv, env, deps.adapterCli ?? runAdapterCli);
207
+ return;
208
+ }
209
+ if (argv[0] === "start") {
210
+ await (deps.prepareStart ?? prepareAdapterStart)(env);
211
+ }
212
+ const entry = process.argv[1] ?? "ocx";
213
+ const executable = process.argv[0] ?? "bun";
214
+ process.argv = [executable, entry, ...argv];
215
+ await (deps.loadUpstream ?? (() => loadUpstreamCli()))();
216
+ }
217
+
218
+ function printError(error: unknown): number {
219
+ const message = error instanceof Error ? error.message : String(error);
220
+ console.error("ocx: " + message);
221
+ return error instanceof ConfigError || error instanceof AdapterConfigError || error instanceof UpstreamPackageError ? 2 : 1;
222
+ }
223
+
224
+ if (import.meta.main) {
225
+ try {
226
+ await runEntry(process.argv.slice(2));
227
+ } catch (error) {
228
+ process.exitCode = printError(error);
229
+ }
230
+ }
package/src/cli/index.ts CHANGED
@@ -348,7 +348,7 @@ async function stopCommand(args: string[]): Promise<number> {
348
348
  return restored.success ? 0 : 1;
349
349
  }
350
350
 
351
- async function main(argv: string[]): Promise<number> {
351
+ export async function runAdapterCli(argv: string[]): Promise<number> {
352
352
  if (argv[0] === "__cli-agent-mcp") {
353
353
  await runCodingAgentMcpServer(argv[1] ?? "", argv[2]);
354
354
  return 0;
@@ -372,8 +372,10 @@ async function main(argv: string[]): Promise<number> {
372
372
  throw new ConfigError(`Unknown command: ${command}. Expected login, provider, codex, start, status, stop, restore, eject, version, or help.`);
373
373
  }
374
374
 
375
- try {
376
- process.exitCode = await main(process.argv.slice(2));
377
- } catch (error) {
378
- process.exitCode = printError(error);
375
+ if (import.meta.main) {
376
+ try {
377
+ process.exitCode = await runAdapterCli(process.argv.slice(2));
378
+ } catch (error) {
379
+ process.exitCode = printError(error);
380
+ }
379
381
  }