@bitkyc08/opencodex 2.5.6 → 2.6.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/README.ko.md +17 -6
- package/README.md +19 -7
- package/README.zh-CN.md +12 -3
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +88 -2
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-BS4X1QDi.js +9 -0
- package/gui/dist/assets/{index-CKqUwc02.css → index-BwvDb198.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +20 -6
- package/src/adapters/anthropic.ts +16 -5
- package/src/adapters/google.ts +9 -2
- package/src/adapters/openai-chat.ts +13 -5
- package/src/bun-runtime.ts +22 -1
- package/src/cli-help.ts +111 -0
- package/src/cli-status.ts +164 -0
- package/src/cli.ts +77 -186
- package/src/codex-account-store.ts +47 -8
- package/src/codex-auth-api.ts +111 -54
- package/src/codex-auth-collision.ts +5 -0
- package/src/codex-catalog.ts +24 -12
- package/src/codex-history-provider.ts +29 -13
- package/src/codex-inject.ts +46 -29
- package/src/codex-journal.ts +77 -13
- package/src/codex-quota.ts +11 -3
- package/src/codex-routing.ts +14 -4
- package/src/codex-shim.ts +71 -24
- package/src/codex-websocket-registry.ts +20 -4
- package/src/config.ts +138 -4
- package/src/init.ts +7 -2
- package/src/oauth/callback-server.ts +22 -15
- package/src/oauth/index.ts +18 -4
- package/src/oauth/login-cli.ts +8 -1
- package/src/oauth/store.ts +2 -1
- package/src/process-control.ts +36 -0
- package/src/provider-label.ts +8 -0
- package/src/responses/parser.ts +18 -1
- package/src/router.ts +61 -5
- package/src/server.ts +878 -94
- package/src/service-secrets.ts +6 -0
- package/src/service.ts +293 -28
- package/src/types.ts +26 -1
- package/src/update.ts +16 -9
- package/src/usage-debug.ts +65 -0
- package/src/usage-log.ts +62 -0
- package/src/usage-summary.ts +0 -0
- package/src/ws-bridge.ts +2 -2
- package/gui/README.md +0 -73
- package/gui/dist/assets/index-CSUvRNAX.js +0 -9
package/src/cli.ts
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
3
|
import { rmSync } from "node:fs";
|
|
4
4
|
import { restoreNativeCodex } from "./codex-inject";
|
|
5
5
|
import { restoreLegacyOpenaiHistory } from "./codex-history-provider";
|
|
6
6
|
import { writeJournal, reconcileJournal } from "./codex-journal";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
codexAutoStartEnabled,
|
|
9
|
+
getConfigDir,
|
|
10
|
+
loadConfig,
|
|
11
|
+
readPid,
|
|
12
|
+
readRuntimePort,
|
|
13
|
+
removePid,
|
|
14
|
+
removeRuntimePort,
|
|
15
|
+
saveConfig,
|
|
16
|
+
writePid,
|
|
17
|
+
writeRuntimePort,
|
|
18
|
+
} from "./config";
|
|
19
|
+
import { collectStatus } from "./cli-status";
|
|
20
|
+
import { hasHelpFlag, printSubcommandUsage, printUsage, printVersion } from "./cli-help";
|
|
8
21
|
import { findAvailablePort, shouldPersistSelectedPort } from "./ports";
|
|
22
|
+
import { killProxy } from "./process-control";
|
|
9
23
|
import { serviceCommand, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "./service";
|
|
10
24
|
import { drainAndShutdown, startServer } from "./server";
|
|
11
25
|
import { maybeShowStarPrompt } from "./star-prompt";
|
|
@@ -13,94 +27,14 @@ import { maybeShowStarPrompt } from "./star-prompt";
|
|
|
13
27
|
const args = process.argv.slice(2);
|
|
14
28
|
const command = args[0];
|
|
15
29
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Usage:
|
|
20
|
-
ocx init Interactive setup (provider + Codex config injection)
|
|
21
|
-
ocx start [--port <port>] Start the proxy server (auto-syncs models to Codex)
|
|
22
|
-
ocx stop Stop the proxy AND restore native Codex (plain codex works again)
|
|
23
|
-
ocx restore Restore native Codex without stopping (alias: eject)
|
|
24
|
-
ocx recover-history --legacy-openai
|
|
25
|
-
Explicitly recover pre-backup syncResumeHistory rows
|
|
26
|
-
ocx uninstall Remove service/shim/config and restore native Codex
|
|
27
|
-
ocx service <sub> Run as a background service (install|start|stop|status|uninstall)
|
|
28
|
-
ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall)
|
|
29
|
-
ocx ensure Ensure the proxy is running and Codex config/cache are current
|
|
30
|
-
ocx sync Fetch models from providers and inject into Codex config
|
|
31
|
-
ocx sync-cache Refresh Codex's model cache from the active catalog
|
|
32
|
-
ocx status Check proxy server status
|
|
33
|
-
ocx login <provider> OAuth login (xai) — opens browser, stores token in ~/.opencodex/auth.json
|
|
34
|
-
ocx logout <provider> Remove a stored OAuth login
|
|
35
|
-
ocx update Update opencodex to the latest published version
|
|
36
|
-
ocx help Show this help message
|
|
37
|
-
|
|
38
|
-
Examples:
|
|
39
|
-
ocx init Set up provider and inject into Codex
|
|
40
|
-
ocx start Start on default port (10100)
|
|
41
|
-
ocx start --port 8080 Start on custom port
|
|
42
|
-
ocx sync Sync available models to Codex`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function hasHelpFlag(values: string[]): boolean {
|
|
46
|
-
return values.some(value => value === "--help" || value === "-h" || value === "help");
|
|
30
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
31
|
+
printVersion();
|
|
32
|
+
process.exit(0);
|
|
47
33
|
}
|
|
48
34
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
console.log("Usage: ocx init\n\nInteractive setup for providers and Codex config injection.");
|
|
53
|
-
break;
|
|
54
|
-
case "start":
|
|
55
|
-
console.log("Usage: ocx start [--port <port>]\n\nStart the proxy server and sync models to Codex.");
|
|
56
|
-
break;
|
|
57
|
-
case "stop":
|
|
58
|
-
console.log("Usage: ocx stop\n\nStop the proxy and restore native Codex config.");
|
|
59
|
-
break;
|
|
60
|
-
case "restore":
|
|
61
|
-
case "eject":
|
|
62
|
-
console.log(`Usage: ocx ${name}\n\nRestore native Codex config without stopping the proxy.`);
|
|
63
|
-
break;
|
|
64
|
-
case "recover-history":
|
|
65
|
-
console.log("Usage: ocx recover-history --legacy-openai\n\nExplicitly recover pre-backup syncResumeHistory rows.");
|
|
66
|
-
break;
|
|
67
|
-
case "uninstall":
|
|
68
|
-
case "remove":
|
|
69
|
-
console.log(`Usage: ocx ${name}\n\nRemove service/shim/config and restore native Codex.`);
|
|
70
|
-
break;
|
|
71
|
-
case "service":
|
|
72
|
-
console.log("Usage: ocx service <install|start|stop|status|uninstall>");
|
|
73
|
-
break;
|
|
74
|
-
case "codex-shim":
|
|
75
|
-
console.log("Usage: ocx codex-shim <install|status|uninstall>");
|
|
76
|
-
break;
|
|
77
|
-
case "ensure":
|
|
78
|
-
console.log("Usage: ocx ensure\n\nEnsure the proxy is running and Codex config/cache are current.");
|
|
79
|
-
break;
|
|
80
|
-
case "sync":
|
|
81
|
-
console.log("Usage: ocx sync\n\nFetch provider models and inject them into Codex config.");
|
|
82
|
-
break;
|
|
83
|
-
case "sync-cache":
|
|
84
|
-
console.log("Usage: ocx sync-cache\n\nRefresh Codex's model cache from the active catalog.");
|
|
85
|
-
break;
|
|
86
|
-
case "status":
|
|
87
|
-
console.log("Usage: ocx status\n\nCheck proxy server status.");
|
|
88
|
-
break;
|
|
89
|
-
case "login":
|
|
90
|
-
console.log("Usage: ocx login <provider>\n\nOAuth or API-key login for a provider.");
|
|
91
|
-
break;
|
|
92
|
-
case "logout":
|
|
93
|
-
console.log("Usage: ocx logout <provider>\n\nRemove a stored provider login.");
|
|
94
|
-
break;
|
|
95
|
-
case "gui":
|
|
96
|
-
console.log("Usage: ocx gui\n\nOpen the opencodex dashboard.");
|
|
97
|
-
break;
|
|
98
|
-
case "update":
|
|
99
|
-
console.log("Usage: ocx update\n\nUpdate opencodex to the latest published version.");
|
|
100
|
-
break;
|
|
101
|
-
default:
|
|
102
|
-
printUsage();
|
|
103
|
-
}
|
|
35
|
+
if (command === "help" && args[1]) {
|
|
36
|
+
printSubcommandUsage(args[1]);
|
|
37
|
+
process.exit(0);
|
|
104
38
|
}
|
|
105
39
|
|
|
106
40
|
if (command !== undefined && command !== "help" && hasHelpFlag(args.slice(1))) {
|
|
@@ -131,10 +65,15 @@ async function syncModelsToCodex(port?: number) {
|
|
|
131
65
|
}
|
|
132
66
|
|
|
133
67
|
function parsePortOption(): number | undefined {
|
|
68
|
+
if (args.length === 1) return undefined;
|
|
69
|
+
if (args.length !== 3 || args[1] !== "--port") {
|
|
70
|
+
console.error("Usage: ocx start [--port <port>]");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
134
73
|
const portIdx = args.indexOf("--port");
|
|
135
74
|
if (portIdx === -1) return undefined;
|
|
136
75
|
const value = args[portIdx + 1];
|
|
137
|
-
const port = value ?
|
|
76
|
+
const port = value && /^\d+$/.test(value) ? Number(value) : NaN;
|
|
138
77
|
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
139
78
|
console.error("Invalid port number");
|
|
140
79
|
process.exit(1);
|
|
@@ -142,15 +81,12 @@ function parsePortOption(): number | undefined {
|
|
|
142
81
|
return port;
|
|
143
82
|
}
|
|
144
83
|
|
|
145
|
-
function healthHost(hostname?: string): string {
|
|
146
|
-
return !hostname || hostname === "0.0.0.0" || hostname === "::" ? "127.0.0.1" : hostname;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
84
|
async function proxyHealthy(port?: number): Promise<boolean> {
|
|
150
85
|
const config = loadConfig();
|
|
151
86
|
const p = port ?? config.port ?? 10100;
|
|
152
87
|
try {
|
|
153
|
-
const
|
|
88
|
+
const hostname = !config.hostname || config.hostname === "0.0.0.0" || config.hostname === "::" ? "127.0.0.1" : config.hostname;
|
|
89
|
+
const res = await fetch(`http://${hostname}:${p}/healthz`, {
|
|
154
90
|
signal: AbortSignal.timeout(750),
|
|
155
91
|
});
|
|
156
92
|
return res.ok;
|
|
@@ -185,6 +121,7 @@ async function chooseListenPort(requestedPort?: number): Promise<number> {
|
|
|
185
121
|
}
|
|
186
122
|
|
|
187
123
|
async function handleStart(options: { block?: boolean } = {}) {
|
|
124
|
+
const requestedPort = parsePortOption();
|
|
188
125
|
reconcileJournal();
|
|
189
126
|
const existingPid = readPid();
|
|
190
127
|
if (existingPid) {
|
|
@@ -196,20 +133,21 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
196
133
|
removePid(existingPid);
|
|
197
134
|
}
|
|
198
135
|
|
|
199
|
-
const requestedPort = parsePortOption();
|
|
200
136
|
const port = await chooseListenPort(requestedPort);
|
|
201
137
|
|
|
202
138
|
const server = startServer(port);
|
|
203
139
|
writePid(process.pid);
|
|
204
|
-
writeJournal();
|
|
205
140
|
|
|
206
141
|
const config = loadConfig();
|
|
142
|
+
writeRuntimePort({ pid: process.pid, port, hostname: config.hostname });
|
|
143
|
+
writeJournal();
|
|
207
144
|
|
|
208
145
|
let cleaned = false;
|
|
209
146
|
const syncCleanup = () => {
|
|
210
147
|
if (cleaned) return;
|
|
211
148
|
cleaned = true;
|
|
212
149
|
removePid(process.pid);
|
|
150
|
+
removeRuntimePort(process.pid);
|
|
213
151
|
if (!process.env.OCX_SERVICE) { try { restoreNativeCodex(); } catch { /* best-effort restore */ } }
|
|
214
152
|
};
|
|
215
153
|
|
|
@@ -269,41 +207,6 @@ async function handleEnsure() {
|
|
|
269
207
|
console.log(`✅ Proxy running on port ${config.port ?? port}`);
|
|
270
208
|
}
|
|
271
209
|
|
|
272
|
-
function killProxy(pid: number): void {
|
|
273
|
-
if (!isProcessAlive(pid)) return;
|
|
274
|
-
if (process.platform === "win32") {
|
|
275
|
-
const taskkill = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\taskkill.exe`;
|
|
276
|
-
try {
|
|
277
|
-
execFileSync(taskkill, ["/PID", String(pid), "/T", "/F"], { stdio: "pipe" });
|
|
278
|
-
} catch (err) {
|
|
279
|
-
if (isProcessAlive(pid)) throw err;
|
|
280
|
-
}
|
|
281
|
-
} else {
|
|
282
|
-
process.kill(pid, "SIGTERM");
|
|
283
|
-
if (!waitForExit(pid, 5000)) process.kill(pid, "SIGKILL");
|
|
284
|
-
}
|
|
285
|
-
if (!waitForExit(pid, 5000)) throw new Error(`process ${pid} did not exit`);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function isProcessAlive(pid: number): boolean {
|
|
289
|
-
try {
|
|
290
|
-
process.kill(pid, 0);
|
|
291
|
-
return true;
|
|
292
|
-
} catch {
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function waitForExit(pid: number, timeoutMs: number): boolean {
|
|
298
|
-
const deadline = Date.now() + timeoutMs;
|
|
299
|
-
const marker = new Int32Array(new SharedArrayBuffer(4));
|
|
300
|
-
while (Date.now() < deadline) {
|
|
301
|
-
if (!isProcessAlive(pid)) return true;
|
|
302
|
-
Atomics.wait(marker, 0, 0, 50);
|
|
303
|
-
}
|
|
304
|
-
return !isProcessAlive(pid);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
210
|
function handleStop() {
|
|
308
211
|
const stoppedService = stopServiceIfInstalled();
|
|
309
212
|
if (stoppedService) console.log("🛑 Service manager stopped (won't respawn).");
|
|
@@ -315,6 +218,7 @@ function handleStop() {
|
|
|
315
218
|
killProxy(pid);
|
|
316
219
|
console.log(`✅ Proxy (PID ${pid}) stopped.`);
|
|
317
220
|
removePid(pid);
|
|
221
|
+
removeRuntimePort(pid);
|
|
318
222
|
} catch {
|
|
319
223
|
stopFailed = true;
|
|
320
224
|
console.error(`❌ Failed to stop proxy (PID ${pid}).`);
|
|
@@ -341,19 +245,19 @@ async function handleUninstall() {
|
|
|
341
245
|
}
|
|
342
246
|
};
|
|
343
247
|
|
|
344
|
-
runStep("service
|
|
345
|
-
stopServiceIfInstalled();
|
|
346
|
-
return uninstallServiceIfInstalled();
|
|
347
|
-
});
|
|
248
|
+
runStep("service stopped", () => stopServiceIfInstalled());
|
|
348
249
|
|
|
349
250
|
runStep("proxy stopped", () => {
|
|
350
251
|
const pid = readPid();
|
|
351
252
|
if (!pid) return false;
|
|
352
253
|
killProxy(pid);
|
|
353
254
|
removePid(pid);
|
|
255
|
+
removeRuntimePort(pid);
|
|
354
256
|
return true;
|
|
355
257
|
});
|
|
356
258
|
|
|
259
|
+
runStep("service removed", () => uninstallServiceIfInstalled());
|
|
260
|
+
|
|
357
261
|
runStep("native Codex restored", () => {
|
|
358
262
|
const r = restoreNativeCodex();
|
|
359
263
|
if (!r.success) throw new Error(r.message);
|
|
@@ -368,9 +272,13 @@ async function handleUninstall() {
|
|
|
368
272
|
console.error(`⚠️ Codex autostart shim removed failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
369
273
|
}
|
|
370
274
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
275
|
+
if (failures.length === 0) {
|
|
276
|
+
runStep("opencodex config removed", () => {
|
|
277
|
+
rmSync(getConfigDir(), { recursive: true, force: true });
|
|
278
|
+
});
|
|
279
|
+
} else {
|
|
280
|
+
console.error("Leaving opencodex config/backups in place so the failed restore step can be retried.");
|
|
281
|
+
}
|
|
374
282
|
|
|
375
283
|
if (failures.length > 0) {
|
|
376
284
|
console.error(`\nUninstall finished with ${failures.length} failed step(s): ${failures.join(", ")}`);
|
|
@@ -379,56 +287,35 @@ async function handleUninstall() {
|
|
|
379
287
|
console.log("\n✅ opencodex local state removed. Remove the package with: npm uninstall -g @bitkyc08/opencodex");
|
|
380
288
|
}
|
|
381
289
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
290
|
+
async function handleStatus() {
|
|
291
|
+
const statusArgs = args.slice(1);
|
|
292
|
+
const wantsJson = statusArgs.length === 1 && statusArgs[0] === "--json";
|
|
293
|
+
if (statusArgs.length > 1 || (statusArgs.length === 1 && !wantsJson)) {
|
|
294
|
+
console.error("Usage: ocx status [--json]");
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
386
297
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
try {
|
|
392
|
-
const response = await fetch(url, { signal: controller.signal });
|
|
393
|
-
if (!response.ok) return { ok: false, label: `${url} returned HTTP ${response.status}` };
|
|
394
|
-
const body = await response.json().catch(() => null) as { version?: unknown; uptime?: unknown } | null;
|
|
395
|
-
const version = typeof body?.version === "string" ? ` v${body.version}` : "";
|
|
396
|
-
const uptime = typeof body?.uptime === "number" ? `, uptime ${Math.round(body.uptime)}s` : "";
|
|
397
|
-
return { ok: true, label: `${url} ok${version}${uptime}` };
|
|
398
|
-
} catch (error) {
|
|
399
|
-
const reason = error instanceof Error && error.name === "AbortError" ? "timed out" : "unreachable";
|
|
400
|
-
return { ok: false, label: `${url} ${reason}` };
|
|
401
|
-
} finally {
|
|
402
|
-
clearTimeout(timer);
|
|
298
|
+
const status = await collectStatus();
|
|
299
|
+
if (wantsJson) {
|
|
300
|
+
console.log(JSON.stringify(status.json, null, 2));
|
|
301
|
+
return;
|
|
403
302
|
}
|
|
404
|
-
}
|
|
405
303
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const port = config.port ?? 10100;
|
|
409
|
-
const pid = readPid();
|
|
410
|
-
const health = await checkProxyHealth(port, config.hostname);
|
|
411
|
-
const proxyLabel = pid && health.ok
|
|
412
|
-
? `running (PID ${pid})`
|
|
413
|
-
: pid
|
|
414
|
-
? `PID file points to PID ${pid}, but health check failed`
|
|
415
|
-
: health.ok
|
|
416
|
-
? "reachable, but PID file is missing or stale"
|
|
417
|
-
: "not running";
|
|
418
|
-
|
|
419
|
-
if (pid || health.ok) {
|
|
420
|
-
console.log(`✅ Proxy: ${proxyLabel}`);
|
|
304
|
+
if (status.json.proxy.pid || status.json.proxy.health.ok) {
|
|
305
|
+
console.log(`✅ Proxy: ${status.proxyLabel}`);
|
|
421
306
|
} else {
|
|
422
|
-
console.log(`❌ Proxy: ${proxyLabel}`);
|
|
307
|
+
console.log(`❌ Proxy: ${status.proxyLabel}`);
|
|
423
308
|
}
|
|
424
|
-
console.log(` Health: ${
|
|
425
|
-
console.log(` Dashboard:
|
|
426
|
-
console.log(` Config: ${
|
|
427
|
-
console.log(`
|
|
428
|
-
console.log(`
|
|
429
|
-
console.log(`
|
|
430
|
-
|
|
431
|
-
console.log(` ${
|
|
309
|
+
console.log(` Health: ${status.healthLabel}`);
|
|
310
|
+
console.log(` Dashboard: ${status.json.dashboard.url}`);
|
|
311
|
+
console.log(` Config: ${status.json.paths.config}`);
|
|
312
|
+
console.log(` PID file: ${status.json.paths.pid}`);
|
|
313
|
+
console.log(` Runtime: ${status.json.paths.runtime}`);
|
|
314
|
+
console.log(` Runtime source: ${status.json.runtime.source}${status.json.runtime.overrideEnv ? ` (${status.json.runtime.overrideEnv})` : ""}`);
|
|
315
|
+
console.log(` Default provider: ${status.json.defaultProvider}`);
|
|
316
|
+
console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}`);
|
|
317
|
+
console.log(` Service: ${status.json.service.summary}`);
|
|
318
|
+
console.log(` ${status.json.codexShim.summary}`);
|
|
432
319
|
}
|
|
433
320
|
|
|
434
321
|
function handleRecoverHistory() {
|
|
@@ -497,8 +384,8 @@ switch (command) {
|
|
|
497
384
|
case "gui": {
|
|
498
385
|
const cfg = await import("./config");
|
|
499
386
|
const config = cfg.loadConfig();
|
|
500
|
-
|
|
501
|
-
if (!
|
|
387
|
+
let pid = cfg.readPid();
|
|
388
|
+
if (!pid) {
|
|
502
389
|
console.log("Proxy not running. Starting...");
|
|
503
390
|
const child = spawn(process.execPath, [process.argv[1], "start"], {
|
|
504
391
|
detached: true,
|
|
@@ -508,7 +395,11 @@ switch (command) {
|
|
|
508
395
|
});
|
|
509
396
|
child.unref();
|
|
510
397
|
await new Promise(r => setTimeout(r, 1000));
|
|
398
|
+
pid = cfg.readPid();
|
|
511
399
|
}
|
|
400
|
+
const runtimePort = pid ? cfg.readRuntimePort(pid) : null;
|
|
401
|
+
const guiPort = runtimePort?.port ?? config.port;
|
|
402
|
+
const guiUrl = `http://localhost:${guiPort}`;
|
|
512
403
|
console.log(`Opening ${guiUrl}`);
|
|
513
404
|
const { openUrl } = await import("./open-url");
|
|
514
405
|
openUrl(guiUrl);
|
|
@@ -535,7 +426,7 @@ switch (command) {
|
|
|
535
426
|
break;
|
|
536
427
|
}
|
|
537
428
|
default:
|
|
538
|
-
console.error("Usage: ocx codex-shim <install|status|uninstall>");
|
|
429
|
+
console.error("Usage: ocx codex-shim <install|status|uninstall|remove>");
|
|
539
430
|
process.exit(1);
|
|
540
431
|
}
|
|
541
432
|
break;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { closeSync, existsSync, readFileSync, mkdirSync, openSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { getConfigDir, atomicWriteFile, hardenConfigDir, hardenExistingSecret } from "./config";
|
|
4
|
+
import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret } from "./config";
|
|
5
5
|
import type { CodexAccountCredentialRecord, CodexAccountCredentials } from "./types";
|
|
6
6
|
|
|
7
7
|
type LegacyCodexAccountStore = Record<string, CodexAccountCredentials>;
|
|
@@ -87,6 +87,7 @@ function loadCodexAccountRecordStore(): CodexAccountStore {
|
|
|
87
87
|
}
|
|
88
88
|
return normalized;
|
|
89
89
|
} catch {
|
|
90
|
+
backupInvalidConfig(path);
|
|
90
91
|
return {};
|
|
91
92
|
}
|
|
92
93
|
}
|
|
@@ -145,10 +146,13 @@ export function saveCodexAccountCredentialIfGeneration(
|
|
|
145
146
|
if (!current || current.generation !== generation || current.deletedAt != null || !current.credential) {
|
|
146
147
|
return false;
|
|
147
148
|
}
|
|
149
|
+
const refreshGrantFingerprint = current.credential.refreshToken === cred.refreshToken
|
|
150
|
+
? current.refreshGrantFingerprint ?? refreshGrantFingerprintForToken(cred.refreshToken)
|
|
151
|
+
: refreshGrantFingerprintForToken(cred.refreshToken);
|
|
148
152
|
store[id] = {
|
|
149
153
|
credential: cred,
|
|
150
154
|
generation: generation + 1,
|
|
151
|
-
refreshGrantFingerprint
|
|
155
|
+
refreshGrantFingerprint,
|
|
152
156
|
replacedAt: current.replacedAt,
|
|
153
157
|
};
|
|
154
158
|
persist(store);
|
|
@@ -191,7 +195,8 @@ export class CodexCredentialRefreshLockTimeoutError extends Error {
|
|
|
191
195
|
}
|
|
192
196
|
|
|
193
197
|
type CodexTokenResult = { accessToken: string; chatgptAccountId: string; generation: number };
|
|
194
|
-
|
|
198
|
+
type CodexRefreshResult = CodexTokenResult & { credential?: CodexAccountCredentials };
|
|
199
|
+
const refreshLocks = new Map<string, Promise<CodexRefreshResult>>();
|
|
195
200
|
|
|
196
201
|
function codexRefreshLockPath(lockKey: string): string {
|
|
197
202
|
const digest = createHash("sha256").update(lockKey).digest("hex").slice(0, 32);
|
|
@@ -283,22 +288,50 @@ export async function getValidCodexToken(id: string): Promise<CodexTokenResult>
|
|
|
283
288
|
|
|
284
289
|
const existing = refreshLocks.get(refreshGrantFingerprint);
|
|
285
290
|
if (existing) {
|
|
286
|
-
await existing;
|
|
291
|
+
const refreshed = await existing;
|
|
292
|
+
const current = readCodexAccountRecord(id);
|
|
293
|
+
const currentCred = current?.deletedAt == null ? current?.credential : undefined;
|
|
294
|
+
if (
|
|
295
|
+
current &&
|
|
296
|
+
currentCred &&
|
|
297
|
+
refreshed.credential &&
|
|
298
|
+
recordGrantFingerprint(current) === refreshGrantFingerprint
|
|
299
|
+
) {
|
|
300
|
+
if (!saveCodexAccountCredentialIfGeneration(id, current.generation, refreshed.credential)) {
|
|
301
|
+
throw new CodexCredentialGenerationConflictError();
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
accessToken: refreshed.credential.accessToken,
|
|
305
|
+
chatgptAccountId: refreshed.credential.chatgptAccountId,
|
|
306
|
+
generation: current.generation + 1,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
287
309
|
return getValidCodexToken(id);
|
|
288
310
|
}
|
|
289
311
|
|
|
290
|
-
const refreshPromise = withCodexRefreshFileLock(refreshGrantFingerprint, async (): Promise<
|
|
312
|
+
const refreshPromise = withCodexRefreshFileLock(refreshGrantFingerprint, async (): Promise<CodexRefreshResult> => {
|
|
291
313
|
const lockedRecord = readCodexAccountRecord(id);
|
|
292
314
|
const lockedCred = lockedRecord?.deletedAt == null ? lockedRecord?.credential : undefined;
|
|
293
315
|
if (!lockedRecord || !lockedCred) throw new CodexCredentialGenerationConflictError();
|
|
294
316
|
const startGeneration = lockedRecord.generation;
|
|
295
317
|
const lockedRefreshGrantFingerprint = recordGrantFingerprint(lockedRecord);
|
|
296
|
-
if (lockedRefreshGrantFingerprint !== refreshGrantFingerprint)
|
|
318
|
+
if (lockedRefreshGrantFingerprint !== refreshGrantFingerprint) {
|
|
319
|
+
if (lockedCred.expiresAt > Date.now() + REFRESH_SKEW_MS) {
|
|
320
|
+
return {
|
|
321
|
+
accessToken: lockedCred.accessToken,
|
|
322
|
+
chatgptAccountId: lockedCred.chatgptAccountId,
|
|
323
|
+
generation: startGeneration,
|
|
324
|
+
credential: lockedCred,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
throw new CodexCredentialGenerationConflictError();
|
|
328
|
+
}
|
|
297
329
|
if (lockedCred.expiresAt > Date.now() + REFRESH_SKEW_MS) {
|
|
298
330
|
return {
|
|
299
331
|
accessToken: lockedCred.accessToken,
|
|
300
332
|
chatgptAccountId: lockedCred.chatgptAccountId,
|
|
301
333
|
generation: startGeneration,
|
|
334
|
+
credential: lockedCred,
|
|
302
335
|
};
|
|
303
336
|
}
|
|
304
337
|
const sameGrantFreshCredential = findFreshCredentialForGrant(refreshGrantFingerprint, id);
|
|
@@ -310,6 +343,7 @@ export async function getValidCodexToken(id: string): Promise<CodexTokenResult>
|
|
|
310
343
|
accessToken: sameGrantFreshCredential.accessToken,
|
|
311
344
|
chatgptAccountId: sameGrantFreshCredential.chatgptAccountId,
|
|
312
345
|
generation: startGeneration + 1,
|
|
346
|
+
credential: sameGrantFreshCredential,
|
|
313
347
|
};
|
|
314
348
|
}
|
|
315
349
|
const res = await fetch(CHATGPT_TOKEN_URL, {
|
|
@@ -345,11 +379,16 @@ export async function getValidCodexToken(id: string): Promise<CodexTokenResult>
|
|
|
345
379
|
if (!saveCodexAccountCredentialIfGeneration(id, startGeneration, updated)) {
|
|
346
380
|
throw new CodexCredentialGenerationConflictError();
|
|
347
381
|
}
|
|
348
|
-
return { accessToken: updated.accessToken, chatgptAccountId: updated.chatgptAccountId, generation: startGeneration + 1 };
|
|
382
|
+
return { accessToken: updated.accessToken, chatgptAccountId: updated.chatgptAccountId, generation: startGeneration + 1, credential: updated };
|
|
349
383
|
}).finally(() => {
|
|
350
384
|
refreshLocks.delete(refreshGrantFingerprint);
|
|
351
385
|
});
|
|
352
386
|
|
|
353
387
|
refreshLocks.set(refreshGrantFingerprint, refreshPromise);
|
|
354
|
-
|
|
388
|
+
const result = await refreshPromise;
|
|
389
|
+
return {
|
|
390
|
+
accessToken: result.accessToken,
|
|
391
|
+
chatgptAccountId: result.chatgptAccountId,
|
|
392
|
+
generation: result.generation,
|
|
393
|
+
};
|
|
355
394
|
}
|