@bitkyc08/opencodex 2.7.35 → 2.7.36
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.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +4 -2
- package/README.ru.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/ocx.mjs +52 -0
- package/gui/dist/assets/index-BpX-hoSd.css +1 -0
- package/gui/dist/assets/index-ZmFopEYw.js +52 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/cursor-errors.ts +38 -1
- package/src/adapters/cursor/discovery.ts +1 -0
- package/src/adapters/cursor/effort-map.ts +1 -0
- package/src/adapters/cursor/live-models.ts +22 -5
- package/src/adapters/cursor/live-transport.ts +82 -7
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +5 -2
- package/src/adapters/openai-responses.ts +64 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +10 -0
- package/src/cli/index.ts +88 -9
- package/src/cli/internal-dispatch.ts +20 -0
- package/src/cli/status.ts +15 -4
- package/src/cli/tray-proxy.ts +52 -0
- package/src/codex/auth-api.ts +46 -5
- package/src/codex/autostart-health.ts +149 -0
- package/src/codex/catalog/aggregation.ts +268 -0
- package/src/codex/catalog/bundled.ts +188 -0
- package/src/codex/catalog/effort.ts +263 -0
- package/src/codex/catalog/metadata.ts +176 -0
- package/src/codex/catalog/parsing.ts +399 -0
- package/src/codex/catalog/provider-fetch.ts +609 -0
- package/src/codex/catalog/sync.ts +540 -0
- package/src/codex/catalog.ts +11 -2426
- package/src/codex/inject.ts +165 -3
- package/src/codex/shim.ts +141 -8
- package/src/codex/sync.ts +17 -2
- package/src/config.ts +23 -0
- package/src/lib/errors.ts +11 -0
- package/src/providers/antigravity-models.ts +33 -0
- package/src/providers/kiro-models.ts +2 -0
- package/src/providers/registry.ts +2 -2
- package/src/responses/state.ts +69 -6
- package/src/server/auth-cors.ts +3 -0
- package/src/server/management/agent-settings-routes.ts +536 -0
- package/src/server/management/combo-routes.ts +210 -0
- package/src/server/management/config-routes.ts +302 -0
- package/src/server/management/context.ts +21 -0
- package/src/server/management/logs-usage-routes.ts +176 -0
- package/src/server/management/model-routes.ts +253 -0
- package/src/server/management/oauth-account-routes.ts +301 -0
- package/src/server/management/provider-routes.ts +408 -0
- package/src/server/management/shared.ts +186 -0
- package/src/server/management-api.ts +23 -1806
- package/src/server/responses/collaboration.ts +300 -0
- package/src/server/responses/compact.ts +342 -0
- package/src/server/responses/core.ts +1498 -0
- package/src/server/responses/encrypted-payload.ts +231 -0
- package/src/server/responses/fetch-helpers.ts +157 -0
- package/src/server/responses.ts +9 -2172
- package/src/server/startup-action-control.ts +41 -0
- package/src/server/startup-health-cache.ts +100 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/service.ts +171 -19
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +290 -0
- package/src/tray/windows.ts +628 -0
- package/src/types.ts +5 -0
- package/src/update/index.ts +43 -0
- package/src/update/job.ts +46 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -2
- package/src/usage/summary.ts +42 -7
- package/gui/dist/assets/index-BunUANVE.js +0 -52
- package/gui/dist/assets/index-Sg-7L_oZ.css +0 -1
package/src/cli/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { rmSync } from "node:fs";
|
|
4
|
-
import { restoreNativeCodex, shouldInjectApiAuthHeader } from "../codex/inject";
|
|
4
|
+
import { currentExternalCodexModelProvider, restoreNativeCodex, shouldInjectApiAuthHeader } from "../codex/inject";
|
|
5
5
|
import { restoreLegacyOpenaiHistory } from "../codex/history-provider";
|
|
6
6
|
import { writeJournal, reconcileJournal } from "../codex/journal";
|
|
7
7
|
import {
|
|
@@ -20,13 +20,16 @@ import {
|
|
|
20
20
|
writeRuntimePort,
|
|
21
21
|
} from "../config";
|
|
22
22
|
import { collectStatus } from "./status";
|
|
23
|
+
import { dispatchInternalCliCommand, type InternalCliCommand } from "./internal-dispatch";
|
|
24
|
+
import { runTrayProxyRestart, runTrayProxyStart } from "./tray-proxy";
|
|
23
25
|
import { installCrashGuards } from "../lib/crash-guard";
|
|
24
26
|
import { hasHelpFlag, printSubcommandUsage, printUsage, printVersion } from "./help";
|
|
25
27
|
import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
|
|
26
28
|
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
27
29
|
import { stopProxy } from "../lib/process-control";
|
|
28
30
|
import { loadServiceTokenFromFile } from "../lib/service-secrets";
|
|
29
|
-
import { serviceCommand, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
|
|
31
|
+
import { diagnoseService, serviceCommand, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
|
|
32
|
+
import { startupHealthSummary } from "../codex/autostart-health";
|
|
30
33
|
import { drainAndShutdown, startServer } from "../server";
|
|
31
34
|
import { injectSystemEnv, revertSystemEnv } from "../server/system-env";
|
|
32
35
|
import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
@@ -131,7 +134,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
131
134
|
const serviceToken = loadServiceTokenFromFile(process.env);
|
|
132
135
|
if (serviceToken) process.env.OPENCODEX_API_AUTH_TOKEN = serviceToken;
|
|
133
136
|
const requestedPort = parsePortOption();
|
|
134
|
-
reconcileJournal();
|
|
137
|
+
if (!currentExternalCodexModelProvider()) reconcileJournal();
|
|
135
138
|
const existingPid = readPid();
|
|
136
139
|
if (existingPid) {
|
|
137
140
|
const live = await findLiveProxy();
|
|
@@ -179,7 +182,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
179
182
|
|
|
180
183
|
const config = loadConfig();
|
|
181
184
|
writeRuntimePort({ pid: process.pid, port, hostname: config.hostname });
|
|
182
|
-
writeJournal();
|
|
185
|
+
if (!currentExternalCodexModelProvider()) writeJournal();
|
|
183
186
|
|
|
184
187
|
// Background proactive token refresh. No-op unless config.tokenGuardian.enabled; timer is unref'd
|
|
185
188
|
// so it never keeps the process alive on its own. Stopped in syncCleanup so no refresh fires mid-drain.
|
|
@@ -187,9 +190,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
187
190
|
// Design B upgrade path: keep retrying the one-time opencodex→openai history migration in the
|
|
188
191
|
// background — the first `ocx start` after an update usually races the Codex app's DB lock.
|
|
189
192
|
// Loopback-only (legacy mode still forward-tags) and respects syncResumeHistory opt-out.
|
|
190
|
-
|
|
191
|
-
? startHistoryMigrationGuardian()
|
|
192
|
-
: undefined;
|
|
193
|
+
let historyGuardian: ReturnType<typeof startHistoryMigrationGuardian> | undefined;
|
|
193
194
|
|
|
194
195
|
let cleaned = false;
|
|
195
196
|
const syncCleanup = () => {
|
|
@@ -200,7 +201,9 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
200
201
|
try { revertSystemEnv(); } catch { /* best-effort */ }
|
|
201
202
|
removePid(process.pid);
|
|
202
203
|
removeRuntimePort(process.pid);
|
|
203
|
-
if (!process.env.OCX_SERVICE
|
|
204
|
+
if (!process.env.OCX_SERVICE && !currentExternalCodexModelProvider()) {
|
|
205
|
+
try { restoreNativeCodex(); } catch { /* best-effort restore */ }
|
|
206
|
+
}
|
|
204
207
|
};
|
|
205
208
|
|
|
206
209
|
let shuttingDown = false;
|
|
@@ -246,6 +249,9 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
246
249
|
|
|
247
250
|
await maybeShowStarPrompt(); // once-only [Y/n] GitHub-star prompt on first interactive start
|
|
248
251
|
await syncModelsToCodex(port).catch(() => {});
|
|
252
|
+
if (!currentExternalCodexModelProvider() && !shouldInjectApiAuthHeader(config) && config.syncResumeHistory !== false) {
|
|
253
|
+
historyGuardian = startHistoryMigrationGuardian();
|
|
254
|
+
}
|
|
249
255
|
// Build Desktop 3P alias registry so inbound claude-opus-4-8-{code} aliases (and legacy claude-opus-4-{code}) decode correctly.
|
|
250
256
|
try {
|
|
251
257
|
const { fetchAllModels } = await import("../server/management-api");
|
|
@@ -263,7 +269,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
263
269
|
}
|
|
264
270
|
|
|
265
271
|
async function handleEnsure() {
|
|
266
|
-
reconcileJournal();
|
|
272
|
+
if (!currentExternalCodexModelProvider()) reconcileJournal();
|
|
267
273
|
const config = loadConfig();
|
|
268
274
|
if (!codexAutoStartEnabled(config)) {
|
|
269
275
|
console.log("Codex autostart is disabled.");
|
|
@@ -302,6 +308,47 @@ async function handleEnsure() {
|
|
|
302
308
|
console.log(`✅ Proxy running on port ${port}`);
|
|
303
309
|
}
|
|
304
310
|
|
|
311
|
+
/** Fixed tray action: start the proxy without depending on codexAutoStart. */
|
|
312
|
+
async function handleTrayProxyStart(): Promise<void> {
|
|
313
|
+
const ok = await runTrayProxyStart({
|
|
314
|
+
findLive: findLiveProxy,
|
|
315
|
+
diagnoseService: () => {
|
|
316
|
+
const service = diagnoseService();
|
|
317
|
+
return { installed: service.installed, startable: serviceStartableFromTray(service), summary: service.summary };
|
|
318
|
+
},
|
|
319
|
+
startService: () => serviceCommand("start"),
|
|
320
|
+
startDirect: () => {
|
|
321
|
+
const config = loadConfig();
|
|
322
|
+
const port = (config.port ?? 10100) > 0 ? (config.port ?? 10100) : 10100;
|
|
323
|
+
const child = spawn(process.execPath, startArgv(port), {
|
|
324
|
+
detached: true,
|
|
325
|
+
stdio: "ignore",
|
|
326
|
+
windowsHide: true,
|
|
327
|
+
env: { ...process.env, OCX_SERVICE: "1" },
|
|
328
|
+
});
|
|
329
|
+
child.unref();
|
|
330
|
+
},
|
|
331
|
+
waitForProxy,
|
|
332
|
+
info: message => console.log(message),
|
|
333
|
+
error: message => console.error(message),
|
|
334
|
+
});
|
|
335
|
+
if (!ok) process.exitCode = 1;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function handleTrayProxyRestart(): Promise<void> {
|
|
339
|
+
const ok = await runTrayProxyRestart({
|
|
340
|
+
stop: async () => {
|
|
341
|
+
await handleStop();
|
|
342
|
+
return !process.exitCode || process.exitCode === 0;
|
|
343
|
+
},
|
|
344
|
+
start: async () => {
|
|
345
|
+
await handleTrayProxyStart();
|
|
346
|
+
return !process.exitCode || process.exitCode === 0;
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
if (!ok) process.exitCode = 1;
|
|
350
|
+
}
|
|
351
|
+
|
|
305
352
|
async function handleStop() {
|
|
306
353
|
const stoppedService = stopServiceIfInstalled();
|
|
307
354
|
if (stoppedService) console.log("🛑 Service manager stopped (won't respawn).");
|
|
@@ -382,6 +429,15 @@ async function handleUninstall() {
|
|
|
382
429
|
|
|
383
430
|
await runStep("service removed", () => uninstallServiceIfInstalled());
|
|
384
431
|
|
|
432
|
+
if (process.platform === "win32") {
|
|
433
|
+
await runStep("Windows tray removed", async () => {
|
|
434
|
+
const { getWindowsTrayStatus, uninstallWindowsTray } = await import("../tray/windows");
|
|
435
|
+
const tray = getWindowsTrayStatus();
|
|
436
|
+
if (!tray.installed && !tray.stale && !tray.running) return false;
|
|
437
|
+
uninstallWindowsTray();
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
385
441
|
await runStep("native Codex restored", () => {
|
|
386
442
|
const r = restoreNativeCodex();
|
|
387
443
|
if (!r.success) throw new Error(r.message);
|
|
@@ -448,6 +504,7 @@ async function handleStatus() {
|
|
|
448
504
|
console.log(` Runtime source: ${status.json.runtime.source}${status.json.runtime.overrideEnv ? ` (${status.json.runtime.overrideEnv})` : ""}`);
|
|
449
505
|
console.log(` Default provider: ${status.json.defaultProvider}`);
|
|
450
506
|
console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}`);
|
|
507
|
+
console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}`);
|
|
451
508
|
console.log(` Service: ${status.json.service.summary}`);
|
|
452
509
|
console.log(` ${status.json.codexShim.summary}`);
|
|
453
510
|
if (status.json.codexPlugins.applicable) {
|
|
@@ -594,6 +651,11 @@ switch (command) {
|
|
|
594
651
|
case "service":
|
|
595
652
|
await serviceCommand(...args.slice(1));
|
|
596
653
|
break;
|
|
654
|
+
case "tray": {
|
|
655
|
+
const { windowsTrayCommand } = await import("../tray/windows");
|
|
656
|
+
await windowsTrayCommand(args.slice(1));
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
597
659
|
case "codex-shim": {
|
|
598
660
|
const { codexShimStatus, installCodexShim, uninstallCodexShim } = await import("../codex/shim");
|
|
599
661
|
switch (args[1]) {
|
|
@@ -636,6 +698,23 @@ switch (command) {
|
|
|
636
698
|
await refreshVersionCache(channel);
|
|
637
699
|
break;
|
|
638
700
|
}
|
|
701
|
+
case "__tray-start":
|
|
702
|
+
case "__tray-restart":
|
|
703
|
+
case "__startup-health":
|
|
704
|
+
await dispatchInternalCliCommand(command as InternalCliCommand, {
|
|
705
|
+
trayStart: handleTrayProxyStart,
|
|
706
|
+
trayRestart: handleTrayProxyRestart,
|
|
707
|
+
startupHealth: async () => {
|
|
708
|
+
const { collectStartupHealth } = await import("../codex/autostart-health");
|
|
709
|
+
console.log(JSON.stringify(collectStartupHealth(loadConfig())));
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
break;
|
|
713
|
+
case "__tray-host": {
|
|
714
|
+
const { runWindowsTrayHost } = await import("../tray/windows");
|
|
715
|
+
await runWindowsTrayHost();
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
639
718
|
case "__gui-update-worker": {
|
|
640
719
|
const jobId = args[1];
|
|
641
720
|
if (!jobId) process.exit(1);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type InternalCliCommand = "__tray-start" | "__tray-restart" | "__startup-health";
|
|
2
|
+
|
|
3
|
+
export interface InternalCliHandlers {
|
|
4
|
+
trayStart: () => void | Promise<void>;
|
|
5
|
+
trayRestart: () => void | Promise<void>;
|
|
6
|
+
startupHealth: () => void | Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Dispatch fixed internal commands without accepting caller-selected process arguments. */
|
|
10
|
+
export async function dispatchInternalCliCommand(
|
|
11
|
+
command: InternalCliCommand,
|
|
12
|
+
handlers: InternalCliHandlers,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
switch (command) {
|
|
15
|
+
case "__tray-start": return void await handlers.trayStart();
|
|
16
|
+
case "__tray-restart": return void await handlers.trayRestart();
|
|
17
|
+
case "__startup-health": return void await handlers.startupHealth();
|
|
18
|
+
default: throw new Error(`Unsupported internal CLI command: ${String(command)}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/cli/status.ts
CHANGED
|
@@ -3,7 +3,10 @@ import { codexAutoStartEnabled, getConfigPath, getPidPath, readConfigDiagnostics
|
|
|
3
3
|
import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../codex/plugins-doctor";
|
|
4
4
|
import { isOpencodexHealthz, probeHostname } from "../server/proxy-liveness";
|
|
5
5
|
import type { OcxConfig } from "../types";
|
|
6
|
-
import {
|
|
6
|
+
import { diagnoseService } from "../service";
|
|
7
|
+
import { collectStartupHealth, type StartupHealth } from "../codex/autostart-health";
|
|
8
|
+
import { getCodexRoutingKind } from "../codex/inject";
|
|
9
|
+
import { diagnoseCodexShim } from "../codex/shim";
|
|
7
10
|
|
|
8
11
|
type HealthCheck = {
|
|
9
12
|
ok: boolean;
|
|
@@ -39,6 +42,7 @@ export type CliStatusJson = {
|
|
|
39
42
|
overrideEnv?: string;
|
|
40
43
|
};
|
|
41
44
|
codexAutostart: boolean;
|
|
45
|
+
startup: StartupHealth;
|
|
42
46
|
defaultProvider: string | null;
|
|
43
47
|
config: {
|
|
44
48
|
source: "default" | "file" | "fallback";
|
|
@@ -115,9 +119,15 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
115
119
|
const listen = selectListenTarget(config, pid, pid ? readRuntimePort(pid) : null);
|
|
116
120
|
const health = await checkProxyHealth(listen);
|
|
117
121
|
const bunRuntime = durableBunRuntime();
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
const
|
|
122
|
+
const service = diagnoseService();
|
|
123
|
+
const serviceSummary = service.summary;
|
|
124
|
+
const codexShim = diagnoseCodexShim();
|
|
125
|
+
const codexShimSummary = codexShim.summary;
|
|
126
|
+
const startup = collectStartupHealth(config, {
|
|
127
|
+
service,
|
|
128
|
+
shim: codexShim,
|
|
129
|
+
routingKind: getCodexRoutingKind(),
|
|
130
|
+
});
|
|
121
131
|
const codexPlugins = diagnoseCodexBundledPlugins();
|
|
122
132
|
const proxyLabel = pid && health.ok
|
|
123
133
|
? `running (PID ${pid})`
|
|
@@ -157,6 +167,7 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
157
167
|
...(bunRuntime.source === "override" ? { overrideEnv: bunRuntime.overrideEnv } : {}),
|
|
158
168
|
},
|
|
159
169
|
codexAutostart: codexAutoStartEnabled(config),
|
|
170
|
+
startup,
|
|
160
171
|
defaultProvider: typeof config.defaultProvider === "string" ? config.defaultProvider : null,
|
|
161
172
|
config: {
|
|
162
173
|
source: configDiagnostics.source,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface TrayProxyLive { port: number }
|
|
2
|
+
|
|
3
|
+
export interface TrayProxyServiceState {
|
|
4
|
+
installed: boolean;
|
|
5
|
+
startable: boolean;
|
|
6
|
+
summary: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TrayProxyStartIo {
|
|
10
|
+
findLive: () => Promise<TrayProxyLive | null>;
|
|
11
|
+
diagnoseService: () => TrayProxyServiceState;
|
|
12
|
+
startService: () => void | Promise<void>;
|
|
13
|
+
startDirect: () => void | Promise<void>;
|
|
14
|
+
waitForProxy: () => Promise<TrayProxyLive | null>;
|
|
15
|
+
info: (message: string) => void;
|
|
16
|
+
error: (message: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Side-effect coordinator for the tray's fixed proxy-start action. */
|
|
20
|
+
export async function runTrayProxyStart(io: TrayProxyStartIo): Promise<boolean> {
|
|
21
|
+
const live = await io.findLive();
|
|
22
|
+
if (live) {
|
|
23
|
+
io.info(`Proxy already running on port ${live.port}.`);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const service = io.diagnoseService();
|
|
28
|
+
if (service.installed && !service.startable) {
|
|
29
|
+
io.error(`Cannot start from the tray because the installed service is not viable: ${service.summary}`);
|
|
30
|
+
io.error("Repair or remove the service before starting a direct proxy.");
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (service.startable) await io.startService();
|
|
35
|
+
else await io.startDirect();
|
|
36
|
+
|
|
37
|
+
const started = await io.waitForProxy();
|
|
38
|
+
if (!started) {
|
|
39
|
+
io.error("Proxy did not become healthy after the tray start action.");
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
io.info(`Proxy running on port ${started.port}.`);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runTrayProxyRestart(io: {
|
|
47
|
+
stop: () => boolean | Promise<boolean>;
|
|
48
|
+
start: () => boolean | Promise<boolean>;
|
|
49
|
+
}): Promise<boolean> {
|
|
50
|
+
if (!await io.stop()) return false;
|
|
51
|
+
return io.start();
|
|
52
|
+
}
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { deleteCodexAccount } from "./account-lifecycle";
|
|
|
13
13
|
import { checkAccountIdCollision, readCodexTokens } from "./auth-collision";
|
|
14
14
|
export { checkAccountIdCollision, getMainChatgptAccountId } from "./auth-collision";
|
|
15
15
|
export { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
16
|
-
import { clearAccountNeedsReauth, isAccountNeedsReauth } from "./account-runtime-state";
|
|
16
|
+
import { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
17
17
|
import {
|
|
18
18
|
clearAccountQuota,
|
|
19
19
|
getAccountQuota,
|
|
@@ -32,6 +32,7 @@ export { maskEmail } from "../lib/privacy";
|
|
|
32
32
|
import type { CodexAccount, OcxConfig } from "../types";
|
|
33
33
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
34
34
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
35
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
35
36
|
|
|
36
37
|
function jsonResponse(data: unknown, status = 200): Response {
|
|
37
38
|
return new Response(JSON.stringify(data), {
|
|
@@ -217,18 +218,55 @@ async function mapWithConcurrency<T, R>(
|
|
|
217
218
|
return results;
|
|
218
219
|
}
|
|
219
220
|
|
|
221
|
+
const MAIN_TERMINAL_AUTH_CODES = new Set([
|
|
222
|
+
"invalid_workspace_selected",
|
|
223
|
+
"invalid_refresh_token",
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
async function isTerminalMainAuthResponse(resp: Response): Promise<boolean> {
|
|
227
|
+
if (resp.status === 401) return true;
|
|
228
|
+
if (resp.status !== 403) return false;
|
|
229
|
+
try {
|
|
230
|
+
const body = await readBoundedResponseBody(resp, { totalTimeoutMs: 1_000, inactivityTimeoutMs: 1_000 });
|
|
231
|
+
if (!body.displaySafe) return false;
|
|
232
|
+
const parsed = JSON.parse(body.text) as {
|
|
233
|
+
detail?: { code?: unknown } | string;
|
|
234
|
+
error?: { code?: unknown } | string;
|
|
235
|
+
code?: unknown;
|
|
236
|
+
};
|
|
237
|
+
const code = typeof parsed.detail === "object" && parsed.detail !== null
|
|
238
|
+
? parsed.detail.code
|
|
239
|
+
: typeof parsed.error === "object" && parsed.error !== null
|
|
240
|
+
? parsed.error.code
|
|
241
|
+
: parsed.code;
|
|
242
|
+
return typeof code === "string" && MAIN_TERMINAL_AUTH_CODES.has(code);
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
220
248
|
export async function fetchMainAccountInfo(forceRefresh = false): Promise<{ email: string | null; plan: string | null; quota: Omit<StoredAccountQuota, "updatedAt"> | null }> {
|
|
249
|
+
const tokens = readCodexTokens();
|
|
250
|
+
if (!tokens) {
|
|
251
|
+
mainAccountCache = null;
|
|
252
|
+
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
253
|
+
return { email: null, plan: null, quota: null };
|
|
254
|
+
}
|
|
221
255
|
if (!forceRefresh && mainAccountCache && Date.now() - mainAccountCache.ts < MAIN_CACHE_TTL) {
|
|
222
256
|
return mainAccountCache;
|
|
223
257
|
}
|
|
224
|
-
const tokens = readCodexTokens();
|
|
225
|
-
if (!tokens) return { email: null, plan: null, quota: null };
|
|
226
258
|
try {
|
|
227
259
|
const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
228
260
|
headers: { Authorization: `Bearer ${tokens.access_token}`, "ChatGPT-Account-Id": tokens.account_id },
|
|
229
261
|
signal: AbortSignal.timeout(8000),
|
|
230
262
|
});
|
|
231
|
-
if (!resp.ok)
|
|
263
|
+
if (!resp.ok) {
|
|
264
|
+
if (await isTerminalMainAuthResponse(resp)) {
|
|
265
|
+
mainAccountCache = null;
|
|
266
|
+
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
267
|
+
}
|
|
268
|
+
return { email: null, plan: null, quota: null };
|
|
269
|
+
}
|
|
232
270
|
const data = (await resp.json()) as WhamUsageResponse;
|
|
233
271
|
const result = {
|
|
234
272
|
email: data.email ?? null,
|
|
@@ -237,6 +275,7 @@ export async function fetchMainAccountInfo(forceRefresh = false): Promise<{ emai
|
|
|
237
275
|
ts: Date.now(),
|
|
238
276
|
};
|
|
239
277
|
mainAccountCache = result;
|
|
278
|
+
clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
240
279
|
// Mirror main quota + plan into the shared stores so the rotation engine can
|
|
241
280
|
// score and auto-switch the main account exactly like a pool account (Option A).
|
|
242
281
|
setMainAccountPlan(result.plan);
|
|
@@ -373,12 +412,14 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
|
|
|
373
412
|
: { quota: null, needsReauth: true };
|
|
374
413
|
return poolAccountDto(a, quotaResult, !!cred);
|
|
375
414
|
});
|
|
415
|
+
const hasMainCredential = readCodexTokens() !== null;
|
|
376
416
|
const main: CodexAuthAccountDto = {
|
|
377
417
|
id: MAIN_CODEX_ACCOUNT_ID,
|
|
378
418
|
email: maskEmail(mainInfo.email) ?? "Codex App login",
|
|
379
419
|
plan: mainInfo.plan,
|
|
380
420
|
isMain: true,
|
|
381
|
-
hasCredential:
|
|
421
|
+
hasCredential: hasMainCredential,
|
|
422
|
+
needsReauth: !hasMainCredential || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID),
|
|
382
423
|
quota: mainInfo.quota ? { ...quotaForPlan({ ...mainInfo.quota, updatedAt: Date.now() }, mainInfo.plan) } : null,
|
|
383
424
|
};
|
|
384
425
|
return [main, ...withQuota];
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { codexAutoStartEnabled } from "../config";
|
|
2
|
+
import { diagnoseService, type ServiceDiagnostic } from "../service";
|
|
3
|
+
import type { OcxConfig } from "../types";
|
|
4
|
+
import { getCodexRoutingKind, type CodexRoutingKind } from "./inject";
|
|
5
|
+
import { diagnoseCodexShim, type CodexShimDiagnostic } from "./shim";
|
|
6
|
+
|
|
7
|
+
export type StartupProtection = "service" | "shim" | "none";
|
|
8
|
+
export type StartupHealthStatus = "native" | "protected" | "at-risk";
|
|
9
|
+
export type ShimCoverage = "full" | "cli-only" | "none";
|
|
10
|
+
|
|
11
|
+
export interface StartupHealthInputs {
|
|
12
|
+
routingKind: CodexRoutingKind;
|
|
13
|
+
autostartEnabled: boolean;
|
|
14
|
+
serviceInstalled: boolean;
|
|
15
|
+
serviceViable: boolean;
|
|
16
|
+
serviceEnabled: boolean;
|
|
17
|
+
serviceRunning: boolean;
|
|
18
|
+
serviceStale: boolean;
|
|
19
|
+
serviceConflict: boolean;
|
|
20
|
+
serviceSupported: boolean;
|
|
21
|
+
shimInstalled: boolean;
|
|
22
|
+
shimHealthy: boolean;
|
|
23
|
+
platform: NodeJS.Platform;
|
|
24
|
+
diagnosticStale?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface StartupHealth {
|
|
28
|
+
status: StartupHealthStatus;
|
|
29
|
+
routingKind: CodexRoutingKind;
|
|
30
|
+
routingInjected: boolean;
|
|
31
|
+
localRoutingDependency: boolean;
|
|
32
|
+
autostartEnabled: boolean;
|
|
33
|
+
rebootSafe: boolean;
|
|
34
|
+
protection: StartupProtection;
|
|
35
|
+
serviceInstalled: boolean;
|
|
36
|
+
serviceViable: boolean;
|
|
37
|
+
serviceEnabled: boolean;
|
|
38
|
+
serviceRunning: boolean;
|
|
39
|
+
serviceStale: boolean;
|
|
40
|
+
serviceConflict: boolean;
|
|
41
|
+
shimInstalled: boolean;
|
|
42
|
+
shimHealthy: boolean;
|
|
43
|
+
shimCoverage: ShimCoverage;
|
|
44
|
+
serviceSupported: boolean;
|
|
45
|
+
platform: NodeJS.Platform;
|
|
46
|
+
diagnosticStale: boolean;
|
|
47
|
+
recommendedCommand: string | null;
|
|
48
|
+
commands: {
|
|
49
|
+
installService: string;
|
|
50
|
+
installShim: string;
|
|
51
|
+
restoreNative: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const COMMANDS = {
|
|
56
|
+
installService: "ocx service install",
|
|
57
|
+
installShim: "ocx codex-shim install",
|
|
58
|
+
restoreNative: "ocx restore",
|
|
59
|
+
} as const;
|
|
60
|
+
|
|
61
|
+
export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth {
|
|
62
|
+
const shimEffective = inputs.autostartEnabled && inputs.shimHealthy;
|
|
63
|
+
const routingInjected = inputs.routingKind === "opencodex-local";
|
|
64
|
+
const localRoutingDependency = inputs.routingKind === "opencodex-local"
|
|
65
|
+
|| inputs.routingKind === "custom-local"
|
|
66
|
+
|| inputs.routingKind === "unknown";
|
|
67
|
+
// Script launchers never cover Codex Desktop/app-server surfaces. This is
|
|
68
|
+
// intentionally conservative on every OS and for WSL-shared Codex homes.
|
|
69
|
+
const shimCoverage: ShimCoverage = !shimEffective
|
|
70
|
+
? "none"
|
|
71
|
+
: "cli-only";
|
|
72
|
+
// We can only credit an opencodex service/shim for routing that opencodex owns.
|
|
73
|
+
// An arbitrary localhost gateway has an independent lifecycle that OCX cannot repair.
|
|
74
|
+
const ownsLocalRouting = inputs.routingKind === "opencodex-local";
|
|
75
|
+
const protection: StartupProtection = ownsLocalRouting && inputs.serviceViable
|
|
76
|
+
? "service"
|
|
77
|
+
: ownsLocalRouting && shimEffective
|
|
78
|
+
? "shim"
|
|
79
|
+
: "none";
|
|
80
|
+
const rebootSafe = !localRoutingDependency || (ownsLocalRouting && inputs.serviceViable);
|
|
81
|
+
const status: StartupHealthStatus = !localRoutingDependency
|
|
82
|
+
? "native"
|
|
83
|
+
: rebootSafe
|
|
84
|
+
? "protected"
|
|
85
|
+
: "at-risk";
|
|
86
|
+
const recommendedCommand = status !== "at-risk"
|
|
87
|
+
? null
|
|
88
|
+
: inputs.routingKind === "custom-local" || inputs.routingKind === "unknown"
|
|
89
|
+
? COMMANDS.restoreNative
|
|
90
|
+
: inputs.serviceSupported
|
|
91
|
+
? COMMANDS.installService
|
|
92
|
+
: COMMANDS.restoreNative;
|
|
93
|
+
return {
|
|
94
|
+
...inputs,
|
|
95
|
+
diagnosticStale: inputs.diagnosticStale ?? false,
|
|
96
|
+
routingInjected,
|
|
97
|
+
localRoutingDependency,
|
|
98
|
+
status,
|
|
99
|
+
rebootSafe,
|
|
100
|
+
protection,
|
|
101
|
+
shimCoverage,
|
|
102
|
+
recommendedCommand,
|
|
103
|
+
commands: { ...COMMANDS },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface StartupHealthDiagnostics {
|
|
108
|
+
routingKind?: CodexRoutingKind;
|
|
109
|
+
service?: ServiceDiagnostic;
|
|
110
|
+
shim?: CodexShimDiagnostic;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Collect current machine state without mutating config, services, or shims. */
|
|
114
|
+
export function collectStartupHealth(
|
|
115
|
+
config: Pick<OcxConfig, "codexAutoStart">,
|
|
116
|
+
diagnostics: StartupHealthDiagnostics = {},
|
|
117
|
+
): StartupHealth {
|
|
118
|
+
const shim = diagnostics.shim ?? diagnoseCodexShim();
|
|
119
|
+
const service = diagnostics.service ?? diagnoseService();
|
|
120
|
+
return deriveStartupHealth({
|
|
121
|
+
routingKind: diagnostics.routingKind ?? getCodexRoutingKind(),
|
|
122
|
+
autostartEnabled: codexAutoStartEnabled(config),
|
|
123
|
+
serviceInstalled: service.installed,
|
|
124
|
+
serviceViable: service.viable,
|
|
125
|
+
serviceEnabled: service.enabled,
|
|
126
|
+
serviceRunning: service.running,
|
|
127
|
+
serviceStale: service.stale,
|
|
128
|
+
serviceConflict: service.conflict,
|
|
129
|
+
serviceSupported: service.supported,
|
|
130
|
+
shimInstalled: shim.installed,
|
|
131
|
+
shimHealthy: shim.healthy,
|
|
132
|
+
platform: process.platform,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function startupHealthSummary(health: StartupHealth): string {
|
|
137
|
+
if (health.status === "native") return health.routingKind === "custom-remote"
|
|
138
|
+
? "custom remote Codex routing (no local restart dependency)"
|
|
139
|
+
: "native Codex routing (no opencodex restart dependency)";
|
|
140
|
+
if (health.protection === "service") return "protected by background service";
|
|
141
|
+
const command = health.recommendedCommand ?? health.commands.restoreNative;
|
|
142
|
+
if (health.routingKind === "unknown") return `AT RISK after restart (Codex routing could not be verified; run '${command}')`;
|
|
143
|
+
if (health.routingKind === "custom-local") return `AT RISK after restart (custom local gateway lifecycle is not managed by opencodex; run '${command}')`;
|
|
144
|
+
if (health.shimCoverage === "cli-only") return `AT RISK for Codex Desktop after restart (launcher shim covers CLI scripts only; run '${command}')`;
|
|
145
|
+
if (health.serviceConflict) return `AT RISK after restart (background service managers conflict; run '${command}')`;
|
|
146
|
+
if (health.serviceStale) return `AT RISK after restart (background service files are stale; run '${command}')`;
|
|
147
|
+
if (health.serviceInstalled && !health.serviceViable) return `AT RISK after restart (installed service is disabled, stopped, or unhealthy; run '${command}')`;
|
|
148
|
+
return `AT RISK after restart (no viable background service; run '${command}')`;
|
|
149
|
+
}
|