@koreanhole/opencodex 2.98.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +23 -7
- package/README.md +90 -181
- package/bin/ocx.mjs +1 -1
- package/bin/package-main.mjs +11 -1
- package/package.json +9 -6
- package/src/adapter-runtime/config.ts +381 -0
- package/src/adapter-runtime/gateway.ts +121 -0
- package/src/adapter-runtime/scoped-server.ts +20 -0
- package/src/cli/entry.ts +222 -0
- package/src/cli/index.ts +90 -20
- package/src/lib/codex-catalog.ts +61 -0
- package/src/lib/codex-inject.ts +467 -0
- package/src/server/index.ts +28 -17
- package/src/server/responses.ts +42 -1
- package/src/upstream/cli.ts +12 -0
- package/src/upstream/package.ts +75 -0
package/src/cli/entry.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
|
|
26
|
+
type AdapterCli = (argv: string[]) => Promise<number>;
|
|
27
|
+
type UpstreamLoader = () => Promise<void>;
|
|
28
|
+
type StartPreparer = (env: NodeJS.ProcessEnv) => Promise<unknown>;
|
|
29
|
+
|
|
30
|
+
export interface EntryDeps {
|
|
31
|
+
adapterCli?: AdapterCli;
|
|
32
|
+
loadUpstream?: UpstreamLoader;
|
|
33
|
+
prepareStart?: StartPreparer;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DISTRIBUTION_PACKAGE = "@koreanhole/opencodex";
|
|
37
|
+
|
|
38
|
+
function isAdapterId(value: string | undefined): value is AdapterProviderId {
|
|
39
|
+
return value === "gemini-cli" || value === "cline-cli";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isAdapterOwnedCommand(argv: readonly string[]): boolean {
|
|
43
|
+
const command = argv[0];
|
|
44
|
+
if (command === "__cli-agent-mcp") return true;
|
|
45
|
+
if (command === "update") return true;
|
|
46
|
+
if (command === "system" && argv.slice(1).includes("update")) return true;
|
|
47
|
+
if (command === "login" && (argv[1] === "cline-cli" || argv[1] === "gemini-cli")) return true;
|
|
48
|
+
if (command === "adapter" && argv[1] === "detach") return true;
|
|
49
|
+
if (command !== "provider") return false;
|
|
50
|
+
if (argv[1] === "add" && isAdapterId(argv[2])) return true;
|
|
51
|
+
if (argv[1] === "use" && isAdapterId(argv[2])) return true;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function takeFlag(args: string[], flag: string): boolean {
|
|
56
|
+
const index = args.indexOf(flag);
|
|
57
|
+
if (index < 0) return false;
|
|
58
|
+
args.splice(index, 1);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function adapterProviderCommand(args: string[], env: NodeJS.ProcessEnv): Promise<number> {
|
|
63
|
+
const sub = args.shift();
|
|
64
|
+
if (sub !== "add" && sub !== "use") throw new ConfigError("Unknown provider command: " + String(sub ?? "(missing)"));
|
|
65
|
+
const provider = args.shift();
|
|
66
|
+
if (!isAdapterId(provider)) throw new ConfigError("Only gemini-cli and cline-cli are handled for provider " + sub + ".");
|
|
67
|
+
if (sub === "add") {
|
|
68
|
+
const setDefault = takeFlag(args, "--set-default");
|
|
69
|
+
if (args.length > 0) throw new ConfigError("Unknown provider add option: " + args[0]);
|
|
70
|
+
const runtime = await readAdapterRuntime(env);
|
|
71
|
+
await syncManagedProviderRows({
|
|
72
|
+
env,
|
|
73
|
+
gatewayPort: runtime?.port ?? ADAPTER_DEFAULT_PORT,
|
|
74
|
+
...(setDefault ? { defaultProvider: provider } : {}),
|
|
75
|
+
});
|
|
76
|
+
console.log("Configured " + provider + ".");
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
if (args.length > 0) throw new ConfigError("Unknown provider use option: " + args[0]);
|
|
80
|
+
await setAdapterDefaultProvider(provider, env);
|
|
81
|
+
console.log("Default provider: " + provider);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function detachAdapter(env: NodeJS.ProcessEnv): Promise<number> {
|
|
86
|
+
const result = await restoreManagedProviderRows(env);
|
|
87
|
+
if (!result.restored) {
|
|
88
|
+
console.log("CLI adapters are not attached.");
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
if (result.preservedUserRows.length > 0) {
|
|
92
|
+
console.log("Preserved user-owned provider rows: " + result.preservedUserRows.join(", "));
|
|
93
|
+
}
|
|
94
|
+
console.log("CLI adapter provider rows detached.");
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function printDistributionUpdate(argv: readonly string[]): number {
|
|
99
|
+
const command = "npm install -g " + DISTRIBUTION_PACKAGE + "@latest";
|
|
100
|
+
if (argv.includes("--json")) {
|
|
101
|
+
console.log(JSON.stringify({
|
|
102
|
+
ok: false,
|
|
103
|
+
reason: "distribution_managed",
|
|
104
|
+
package: DISTRIBUTION_PACKAGE,
|
|
105
|
+
command,
|
|
106
|
+
}));
|
|
107
|
+
} else {
|
|
108
|
+
console.error("ocx: updates are managed by " + DISTRIBUTION_PACKAGE + ".");
|
|
109
|
+
console.error("Run: " + command);
|
|
110
|
+
}
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function runAdapterCommand(argv: string[], env: NodeJS.ProcessEnv, adapterCli: AdapterCli): Promise<number> {
|
|
115
|
+
if (argv[0] === "__cli-agent-mcp") return adapterCli(argv);
|
|
116
|
+
if (argv[0] === "login") return adapterCli(argv);
|
|
117
|
+
if (argv[0] === "update" || (argv[0] === "system" && argv.slice(1).includes("update"))) {
|
|
118
|
+
return printDistributionUpdate(argv);
|
|
119
|
+
}
|
|
120
|
+
if (argv[0] === "adapter" && argv[1] === "detach") return detachAdapter(env);
|
|
121
|
+
if (argv[0] === "provider") return adapterProviderCommand(argv.slice(1), env);
|
|
122
|
+
throw new ConfigError("Internal adapter dispatch error.");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function pidAlive(pid: number): boolean {
|
|
126
|
+
try {
|
|
127
|
+
process.kill(pid, 0);
|
|
128
|
+
return true;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Attach the two managed rows and keep the gateway in the serving process. */
|
|
135
|
+
export async function prepareAdapterStart(
|
|
136
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
137
|
+
deps: {
|
|
138
|
+
discoverMetadata?: typeof discoverManagedProviderMetadata;
|
|
139
|
+
startGateway?: typeof startAdapterGateway;
|
|
140
|
+
syncRows?: typeof syncManagedProviderRows;
|
|
141
|
+
} = {},
|
|
142
|
+
): Promise<AdapterGateway | undefined> {
|
|
143
|
+
const existing = await readAdapterRuntime(env);
|
|
144
|
+
const inheritedToken = env[ADAPTER_TOKEN_ENV]?.trim();
|
|
145
|
+
if (existing && existing.pid !== process.pid && pidAlive(existing.pid) && !inheritedToken) {
|
|
146
|
+
throw new AdapterConfigError(
|
|
147
|
+
"The CLI adapter gateway is already running in another process; restart it from that process or stop it first.",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const token = inheritedToken || randomBytes(32).toString("base64url");
|
|
151
|
+
env[ADAPTER_TOKEN_ENV] = token;
|
|
152
|
+
let gateway: AdapterGateway | undefined;
|
|
153
|
+
let gatewayPort: number;
|
|
154
|
+
if (existing && existing.pid !== process.pid && pidAlive(existing.pid)) {
|
|
155
|
+
gatewayPort = existing.port;
|
|
156
|
+
} else {
|
|
157
|
+
if (existing) await removeAdapterRuntimeIfPid(existing.pid, env);
|
|
158
|
+
gateway = (deps.startGateway ?? startAdapterGateway)({ token, env });
|
|
159
|
+
gatewayPort = gateway.port;
|
|
160
|
+
await writeAdapterRuntime({
|
|
161
|
+
version: 1,
|
|
162
|
+
pid: process.pid,
|
|
163
|
+
host: "127.0.0.1",
|
|
164
|
+
port: gatewayPort,
|
|
165
|
+
tokenEnv: ADAPTER_TOKEN_ENV,
|
|
166
|
+
providers: [...ADAPTER_PROVIDER_IDS],
|
|
167
|
+
startedAt: new Date().toISOString(),
|
|
168
|
+
}, env);
|
|
169
|
+
process.once("exit", () => {
|
|
170
|
+
try {
|
|
171
|
+
rmSync(adapterRuntimePath(env), { force: true });
|
|
172
|
+
} catch {
|
|
173
|
+
// best effort
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const metadata = await (deps.discoverMetadata ?? discoverManagedProviderMetadata)(env).catch(() => undefined);
|
|
180
|
+
const synced = await (deps.syncRows ?? syncManagedProviderRows)({
|
|
181
|
+
env,
|
|
182
|
+
gatewayPort,
|
|
183
|
+
...(metadata ? { metadata } : {}),
|
|
184
|
+
});
|
|
185
|
+
if (synced.providerEnvToken) env.OPENCODEX_API_AUTH_TOKEN ??= synced.providerEnvToken;
|
|
186
|
+
return gateway;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
await gateway?.stop().catch(() => undefined);
|
|
189
|
+
await removeAdapterRuntimeIfPid(process.pid, env).catch(() => undefined);
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function runEntry(argv: string[], deps: EntryDeps = {}): Promise<void> {
|
|
195
|
+
const env = process.env;
|
|
196
|
+
env.OCX_DISABLE_UPDATE_CHECK ??= "1";
|
|
197
|
+
if (isAdapterOwnedCommand(argv)) {
|
|
198
|
+
process.exitCode = await runAdapterCommand(argv, env, deps.adapterCli ?? runAdapterCli);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (argv[0] === "start") {
|
|
202
|
+
await (deps.prepareStart ?? prepareAdapterStart)(env);
|
|
203
|
+
}
|
|
204
|
+
const entry = process.argv[1] ?? "ocx";
|
|
205
|
+
const executable = process.argv[0] ?? "bun";
|
|
206
|
+
process.argv = [executable, entry, ...argv];
|
|
207
|
+
await (deps.loadUpstream ?? (() => loadUpstreamCli()))();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function printError(error: unknown): number {
|
|
211
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
+
console.error("ocx: " + message);
|
|
213
|
+
return error instanceof ConfigError || error instanceof AdapterConfigError || error instanceof UpstreamPackageError ? 2 : 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (import.meta.main) {
|
|
217
|
+
try {
|
|
218
|
+
await runEntry(process.argv.slice(2));
|
|
219
|
+
} catch (error) {
|
|
220
|
+
process.exitCode = printError(error);
|
|
221
|
+
}
|
|
222
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { startServer } from "../server";
|
|
|
20
20
|
import { loginClineAcp } from "../adapters/cli-agent/cline/acp";
|
|
21
21
|
import { runCodingAgentMcpServer } from "../adapters/cli-agent/mcp-server";
|
|
22
22
|
import { syncCodexCatalog } from "../lib/codex-catalog";
|
|
23
|
+
import { injectCodexConfig, recordCodexCatalogState, restoreCodexConfig } from "../lib/codex-inject";
|
|
23
24
|
|
|
24
25
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
|
|
@@ -61,7 +62,10 @@ function help(): string {
|
|
|
61
62
|
" provider list [--json] Show supported providers",
|
|
62
63
|
" start [--port <port>] [--host <host>] Start the proxy",
|
|
63
64
|
" status [--json] Check the running proxy",
|
|
64
|
-
" stop [--json] Stop the
|
|
65
|
+
" stop [--json] Stop the proxy and restore native Codex",
|
|
66
|
+
" restore [--json] Restore native Codex without stopping",
|
|
67
|
+
" restore back [--json] Re-point Codex at the running proxy",
|
|
68
|
+
" eject Alias of restore",
|
|
65
69
|
" codex sync [--json] Refresh the Codex model catalog",
|
|
66
70
|
" version Print the package version",
|
|
67
71
|
" help Show this help",
|
|
@@ -179,21 +183,27 @@ async function startCommand(args: string[]): Promise<number> {
|
|
|
179
183
|
config.port = parsed;
|
|
180
184
|
}
|
|
181
185
|
if (host !== undefined) config.host = host;
|
|
186
|
+
const running = startServer(config, { env });
|
|
187
|
+
try {
|
|
188
|
+
await injectCodexConfig({ env, endpoint: { host: running.host, port: running.port }, config });
|
|
189
|
+
} catch (error) {
|
|
190
|
+
await running.stop(true).catch(() => undefined);
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
182
193
|
try {
|
|
183
194
|
const catalog = await syncCodexCatalog({ env });
|
|
184
195
|
if (catalog.ok && catalog.written) {
|
|
196
|
+
await recordCodexCatalogState(env).catch(error => {
|
|
197
|
+
console.warn(`[opencodex] Codex catalog state was not journaled: ${error instanceof Error ? error.message : String(error)}`);
|
|
198
|
+
});
|
|
185
199
|
console.log(`Codex catalog refreshed: ${catalog.path}`);
|
|
186
200
|
console.log("Restart the Codex app/app-server to reload the model picker.");
|
|
187
|
-
if (!catalog.configured) {
|
|
188
|
-
console.log(`Activate it with: model_catalog_json = "${catalog.path}"`);
|
|
189
|
-
}
|
|
190
201
|
} else if (!catalog.ok) {
|
|
191
202
|
console.warn(`[opencodex] Codex catalog was not refreshed: ${catalog.message}`);
|
|
192
203
|
}
|
|
193
204
|
} catch (error) {
|
|
194
205
|
console.warn(`[opencodex] Codex catalog refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
195
206
|
}
|
|
196
|
-
const running = startServer(config, { env });
|
|
197
207
|
await writeRuntimeState({ pid: process.pid, port: running.port, host: running.host, startedAt: new Date().toISOString() }, env);
|
|
198
208
|
console.log(`opencodex listening on http://${running.host}:${running.port}/v1 (default provider: ${config.defaultProvider})`);
|
|
199
209
|
console.log(`Health: http://${running.host}:${running.port}/healthz`);
|
|
@@ -202,10 +212,22 @@ async function startCommand(args: string[]): Promise<number> {
|
|
|
202
212
|
console.log(`\nStopping proxy (${signal})...`);
|
|
203
213
|
await running.stop(true);
|
|
204
214
|
await removeRuntimeStateIfPid(process.pid, env);
|
|
205
|
-
|
|
215
|
+
let exitCode = 0;
|
|
216
|
+
try {
|
|
217
|
+
const restored = await restoreCodexConfig({ env });
|
|
218
|
+
if (!restored.success) {
|
|
219
|
+
exitCode = 1;
|
|
220
|
+
console.error(`ocx: Codex restore did not complete: ${restored.catalog.message}`);
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
exitCode = 1;
|
|
224
|
+
console.error(`ocx: Codex restore failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
225
|
+
}
|
|
226
|
+
process.exit(exitCode);
|
|
206
227
|
};
|
|
207
228
|
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
208
229
|
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
230
|
+
process.once("SIGHUP", () => void shutdown("SIGHUP"));
|
|
209
231
|
await new Promise<never>(() => undefined);
|
|
210
232
|
return 0;
|
|
211
233
|
}
|
|
@@ -232,9 +254,42 @@ async function codexCommand(args: string[]): Promise<number> {
|
|
|
232
254
|
}
|
|
233
255
|
console.log("Codex reload required: restart the Codex app or app-server model picker.");
|
|
234
256
|
}
|
|
257
|
+
if (result.ok) await recordCodexCatalogState(process.env).catch(() => undefined);
|
|
235
258
|
return 0;
|
|
236
259
|
}
|
|
237
260
|
|
|
261
|
+
async function restoreCommand(args: string[]): Promise<number> {
|
|
262
|
+
const env = process.env;
|
|
263
|
+
const back = args[0] === "back";
|
|
264
|
+
if (back) args.shift();
|
|
265
|
+
const json = takeFlag(args, "--json");
|
|
266
|
+
const removeProviderTable = takeFlag(args, "--remove-codex-provider-table");
|
|
267
|
+
if (args.length > 0) throw new ConfigError(`Unknown restore option: ${args[0]}`);
|
|
268
|
+
let result;
|
|
269
|
+
if (back) {
|
|
270
|
+
const state = await readRuntimeState(env);
|
|
271
|
+
if (!state) throw new ConfigError("Proxy is not running; start it before `ocx restore back`.");
|
|
272
|
+
const config = await loadConfig(env);
|
|
273
|
+
if (!isAlive(state.pid) || !await health(state.host, state.port, config, env)) {
|
|
274
|
+
throw new ConfigError("The recorded proxy is not healthy; refusing to restore Codex routing.");
|
|
275
|
+
}
|
|
276
|
+
result = await injectCodexConfig({
|
|
277
|
+
env,
|
|
278
|
+
endpoint: { host: state.host, port: state.port },
|
|
279
|
+
config,
|
|
280
|
+
});
|
|
281
|
+
result = { ...result, action: "restore-back" as const };
|
|
282
|
+
} else {
|
|
283
|
+
result = await restoreCodexConfig({ env, removeProviderTable });
|
|
284
|
+
}
|
|
285
|
+
if (json) console.log(JSON.stringify(result));
|
|
286
|
+
else {
|
|
287
|
+
console.log(result.config.message);
|
|
288
|
+
console.log(result.catalog.message);
|
|
289
|
+
}
|
|
290
|
+
return result.success ? 0 : 1;
|
|
291
|
+
}
|
|
292
|
+
|
|
238
293
|
async function statusCommand(args: string[]): Promise<number> {
|
|
239
294
|
const json = takeFlag(args, "--json");
|
|
240
295
|
if (args.length > 0) throw new ConfigError(`Unknown status option: ${args[0]}`);
|
|
@@ -257,16 +312,24 @@ async function stopCommand(args: string[]): Promise<number> {
|
|
|
257
312
|
if (args.length > 0) throw new ConfigError(`Unknown stop option: ${args[0]}`);
|
|
258
313
|
const state = await readRuntimeState();
|
|
259
314
|
if (!state) {
|
|
260
|
-
|
|
261
|
-
|
|
315
|
+
const restored = await restoreCodexConfig({ env: process.env });
|
|
316
|
+
if (json) console.log(JSON.stringify({ stopped: false, reason: "not-running", restore: restored }));
|
|
317
|
+
else {
|
|
318
|
+
console.log("Proxy is not running.");
|
|
319
|
+
console.log(restored.config.message);
|
|
320
|
+
}
|
|
262
321
|
return 1;
|
|
263
322
|
}
|
|
264
323
|
const config = await loadConfig();
|
|
265
324
|
if (!isAlive(state.pid)) {
|
|
266
325
|
await removeRuntimeStateIfPid(state.pid);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
326
|
+
const restored = await restoreCodexConfig({ env: process.env });
|
|
327
|
+
if (json) console.log(JSON.stringify({ stopped: true, stale: true, restore: restored }));
|
|
328
|
+
else {
|
|
329
|
+
console.log("Removed stale proxy state.");
|
|
330
|
+
console.log(restored.config.message);
|
|
331
|
+
}
|
|
332
|
+
return restored.success ? 0 : 1;
|
|
270
333
|
}
|
|
271
334
|
if (!await health(state.host, state.port, config, process.env)) {
|
|
272
335
|
throw new ConfigError(`Refusing to stop pid ${state.pid}: it is not the opencodex proxy recorded by this home.`);
|
|
@@ -276,12 +339,16 @@ async function stopCommand(args: string[]): Promise<number> {
|
|
|
276
339
|
while (isAlive(state.pid) && Date.now() < deadline) await Bun.sleep(50);
|
|
277
340
|
if (isAlive(state.pid)) process.kill(state.pid, "SIGKILL");
|
|
278
341
|
await removeRuntimeStateIfPid(state.pid);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
342
|
+
const restored = await restoreCodexConfig({ env: process.env });
|
|
343
|
+
if (json) console.log(JSON.stringify({ stopped: true, pid: state.pid, restore: restored }));
|
|
344
|
+
else {
|
|
345
|
+
console.log(`Stopped proxy (pid ${state.pid}).`);
|
|
346
|
+
console.log(restored.config.message);
|
|
347
|
+
}
|
|
348
|
+
return restored.success ? 0 : 1;
|
|
282
349
|
}
|
|
283
350
|
|
|
284
|
-
async function
|
|
351
|
+
export async function runAdapterCli(argv: string[]): Promise<number> {
|
|
285
352
|
if (argv[0] === "__cli-agent-mcp") {
|
|
286
353
|
await runCodingAgentMcpServer(argv[1] ?? "", argv[2]);
|
|
287
354
|
return 0;
|
|
@@ -301,11 +368,14 @@ async function main(argv: string[]): Promise<number> {
|
|
|
301
368
|
if (command === "start") return await startCommand(argv);
|
|
302
369
|
if (command === "status") return await statusCommand(argv);
|
|
303
370
|
if (command === "stop") return await stopCommand(argv);
|
|
304
|
-
|
|
371
|
+
if (command === "restore" || command === "eject") return await restoreCommand(argv);
|
|
372
|
+
throw new ConfigError(`Unknown command: ${command}. Expected login, provider, codex, start, status, stop, restore, eject, version, or help.`);
|
|
305
373
|
}
|
|
306
374
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
+
}
|
|
311
381
|
}
|
package/src/lib/codex-catalog.ts
CHANGED
|
@@ -257,6 +257,67 @@ function readRootTomlString(text: string, key: string): string | undefined {
|
|
|
257
257
|
return match?.[1]?.trim() || undefined;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
export function codexHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
261
|
+
return env.CODEX_HOME?.trim() || join(env.HOME?.trim() || homedir(), ".codex");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Resolve the catalog only when opencodex is allowed to manage that file. */
|
|
265
|
+
export async function resolveCodexCatalogPath(
|
|
266
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
267
|
+
): Promise<string | undefined> {
|
|
268
|
+
const home = codexHome(env);
|
|
269
|
+
let configured: string | undefined;
|
|
270
|
+
try {
|
|
271
|
+
configured = readRootTomlString(await readFile(join(home, "config.toml"), "utf8"), "model_catalog_json");
|
|
272
|
+
} catch {
|
|
273
|
+
configured = undefined;
|
|
274
|
+
}
|
|
275
|
+
if (configured && basename(configured) !== "opencodex-catalog.json") return undefined;
|
|
276
|
+
return configured ? resolve(home, configured) : join(home, "opencodex-catalog.json");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function isManagedCodexCatalogSlug(slug: unknown): boolean {
|
|
280
|
+
return managedSlug(slug);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Remove only opencodex-routed rows; preserve native/user rows. */
|
|
284
|
+
export async function removeManagedCodexCatalogRows(path: string): Promise<{
|
|
285
|
+
changed: boolean;
|
|
286
|
+
removed: number;
|
|
287
|
+
message: string;
|
|
288
|
+
}> {
|
|
289
|
+
let raw: string;
|
|
290
|
+
try {
|
|
291
|
+
raw = await readFile(path, "utf8");
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
294
|
+
return { changed: false, removed: 0, message: "No opencodex catalog file was present." };
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
let parsed: Record<string, unknown>;
|
|
299
|
+
try {
|
|
300
|
+
const value = asRecord(JSON.parse(raw));
|
|
301
|
+
if (!value) throw new Error("catalog is not an object");
|
|
302
|
+
parsed = value;
|
|
303
|
+
} catch {
|
|
304
|
+
throw new Error("The opencodex catalog is malformed; it was preserved.");
|
|
305
|
+
}
|
|
306
|
+
const models = Array.isArray(parsed.models) ? parsed.models : undefined;
|
|
307
|
+
if (!models || models.some(model => !asRecord(model))) {
|
|
308
|
+
throw new Error("The opencodex catalog has an invalid models field; it was preserved.");
|
|
309
|
+
}
|
|
310
|
+
const kept = models.filter(model => !managedSlug(asRecord(model)?.slug));
|
|
311
|
+
const removed = models.length - kept.length;
|
|
312
|
+
if (removed === 0) return { changed: false, removed: 0, message: "No managed catalog rows were present." };
|
|
313
|
+
if (kept.length === 0 && basename(path) === "opencodex-catalog.json") {
|
|
314
|
+
await rm(path, { force: true });
|
|
315
|
+
return { changed: true, removed, message: `Removed ${removed} managed catalog row(s) and the empty opencodex catalog.` };
|
|
316
|
+
}
|
|
317
|
+
await writeCodexCatalog(path, { ...parsed, models: kept } as CodexCatalogDocument);
|
|
318
|
+
return { changed: true, removed, message: `Removed ${removed} managed catalog row(s) from ${path}.` };
|
|
319
|
+
}
|
|
320
|
+
|
|
260
321
|
export async function writeCodexCatalog(
|
|
261
322
|
path: string,
|
|
262
323
|
document: CodexCatalogDocument,
|