@bitkyc08/opencodex 2.6.26-preview.20260705 → 2.6.28-preview.20260707
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.md +1 -0
- package/bin/ocx.mjs +4 -4
- package/gui/dist/assets/index-ByGC8-Bm.css +1 -0
- package/gui/dist/assets/index-CkV5xFA8.js +15 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/adapters/anthropic-image-guard.ts +195 -0
- package/src/adapters/anthropic.ts +85 -14
- package/src/adapters/cursor/cursor-errors.ts +2 -2
- package/src/adapters/cursor/live-transport.ts +1 -1
- package/src/adapters/cursor/transport-retry.ts +2 -2
- package/src/adapters/google-errors.ts +1 -1
- package/src/adapters/google-http.ts +1 -1
- package/src/adapters/google-truncation.ts +1 -1
- package/src/adapters/google.ts +1 -1
- package/src/adapters/kiro-errors.ts +1 -1
- package/src/adapters/kiro-retry.ts +1 -1
- package/src/adapters/kiro-truncation.ts +1 -1
- package/src/adapters/kiro.ts +1 -1
- package/src/adapters/openai-chat.ts +12 -2
- package/src/adapters/openai-responses.ts +126 -3
- package/src/bridge.ts +164 -7
- package/src/{doctor.ts → cli/doctor.ts} +21 -4
- package/src/{cli-help.ts → cli/help.ts} +1 -1
- package/src/cli/index.ts +584 -0
- package/src/{init.ts → cli/init.ts} +6 -6
- package/src/{cli-models.ts → cli/models.ts} +2 -2
- package/src/{cli-provider.ts → cli/provider.ts} +12 -8
- package/src/{star-prompt.ts → cli/star-prompt.ts} +1 -1
- package/src/{cli-status.ts → cli/status.ts} +7 -7
- package/src/cli.ts +9 -575
- package/src/{codex-account-label.ts → codex/account-label.ts} +1 -1
- package/src/{codex-account-lifecycle.ts → codex/account-lifecycle.ts} +6 -6
- package/src/{codex-account-store.ts → codex/account-store.ts} +2 -2
- package/src/{codex-account-usability.ts → codex/account-usability.ts} +4 -4
- package/src/{codex-auth-api.ts → codex/auth-api.ts} +18 -18
- package/src/{codex-auth-collision.ts → codex/auth-collision.ts} +4 -4
- package/src/{codex-auth-context.ts → codex/auth-context.ts} +9 -9
- package/src/{codex-catalog.ts → codex/catalog.ts} +49 -20
- package/src/codex/history-migration-guardian.ts +102 -0
- package/src/{codex-history-provider.ts → codex/history-provider.ts} +111 -7
- package/src/{codex-home.ts → codex/home.ts} +1 -1
- package/src/{codex-inject.ts → codex/inject.ts} +204 -26
- package/src/{codex-journal.ts → codex/journal.ts} +2 -2
- package/src/{codex-main-account.ts → codex/main-account.ts} +2 -2
- package/src/{model-cache.ts → codex/model-cache.ts} +1 -1
- package/src/{codex-paths.ts → codex/paths.ts} +2 -2
- package/src/{codex-plugins-doctor.ts → codex/plugins-doctor.ts} +2 -2
- package/src/{codex-refresh.ts → codex/refresh.ts} +4 -4
- package/src/{codex-routing.ts → codex/routing.ts} +8 -8
- package/src/{codex-shim.ts → codex/shim.ts} +6 -5
- package/src/{codex-sync.ts → codex/sync.ts} +4 -4
- package/src/{codex-websocket-registry.ts → codex/websocket-registry.ts} +1 -1
- package/src/config.ts +2 -0
- package/src/generated/jawcode-model-metadata.ts +2 -0
- package/src/{bun-runtime.ts → lib/bun-runtime.ts} +1 -1
- package/src/{crash-guard.ts → lib/crash-guard.ts} +1 -1
- package/src/{process-control.ts → lib/process-control.ts} +1 -1
- package/src/{service-secrets.ts → lib/service-secrets.ts} +1 -1
- package/src/oauth/callback-server.ts +1 -1
- package/src/oauth/google-antigravity.ts +7 -4
- package/src/oauth/index.ts +67 -16
- package/src/oauth/login-cli.ts +2 -2
- package/src/oauth/store.ts +236 -20
- package/src/oauth/token-guardian.ts +24 -20
- package/src/oauth/types.ts +16 -0
- package/src/providers/api-keys.ts +121 -0
- package/src/{provider-context-cap.ts → providers/context-cap.ts} +1 -1
- package/src/providers/derive.ts +2 -0
- package/src/providers/key-failover.ts +145 -0
- package/src/{provider-label.ts → providers/label.ts} +1 -1
- package/src/{provider-quota.ts → providers/quota.ts} +12 -7
- package/src/providers/registry.ts +66 -4
- package/src/responses/compaction.ts +117 -0
- package/src/responses/parser.ts +89 -13
- package/src/responses/reasoning-envelope.ts +52 -0
- package/src/responses/schema.ts +15 -3
- package/src/responses/state.ts +117 -2
- package/src/router.ts +2 -0
- package/src/server/auth-cors.ts +231 -0
- package/src/server/index.ts +523 -0
- package/src/server/lifecycle.ts +73 -0
- package/src/server/management-api.ts +628 -0
- package/src/{proxy-liveness.ts → server/proxy-liveness.ts} +1 -1
- package/src/server/relay.ts +534 -0
- package/src/server/request-decompress.ts +46 -0
- package/src/server/request-log.ts +310 -0
- package/src/server/responses.ts +775 -0
- package/src/{ws-bridge.ts → server/ws-bridge.ts} +44 -13
- package/src/service.ts +19 -11
- package/src/types.ts +27 -0
- package/src/{update.ts → update/index.ts} +5 -5
- package/src/{update-job.ts → update/job.ts} +7 -6
- package/src/{update-notify.ts → update/notify.ts} +3 -3
- package/src/{usage-debug.ts → usage/debug.ts} +3 -3
- package/src/{usage-log.ts → usage/log.ts} +3 -3
- package/src/{usage-summary.ts → usage/summary.ts} +3 -3
- package/src/{usage-totals.ts → usage/totals.ts} +1 -1
- package/src/vision/describe.ts +3 -3
- package/src/vision/index.ts +21 -1
- package/src/web-search/executor.ts +4 -4
- package/src/web-search/index.ts +1 -1
- package/src/web-search/loop.ts +84 -24
- package/gui/dist/assets/index-BcHhxo1I.css +0 -1
- package/gui/dist/assets/index-DCC1q_Jx.js +0 -15
- package/src/server.ts +0 -2501
- /package/src/{codex-account-runtime-state.ts → codex/account-runtime-state.ts} +0 -0
- /package/src/{codex-quota.ts → codex/quota.ts} +0 -0
- /package/src/{abort.ts → lib/abort.ts} +0 -0
- /package/src/{debug.ts → lib/debug.ts} +0 -0
- /package/src/{errors.ts → lib/errors.ts} +0 -0
- /package/src/{open-url.ts → lib/open-url.ts} +0 -0
- /package/src/{privacy.ts → lib/privacy.ts} +0 -0
- /package/src/{redact.ts → lib/redact.ts} +0 -0
- /package/src/{sidecar-tracker.ts → lib/sidecar-tracker.ts} +0 -0
- /package/src/{upstream-retry.ts → lib/upstream-retry.ts} +0 -0
- /package/src/{win-paths.ts → lib/win-paths.ts} +0 -0
- /package/src/{ports.ts → server/ports.ts} +0 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { rmSync } from "node:fs";
|
|
4
|
+
import { restoreNativeCodex, shouldInjectApiAuthHeader } from "../codex/inject";
|
|
5
|
+
import { restoreLegacyOpenaiHistory } from "../codex/history-provider";
|
|
6
|
+
import { writeJournal, reconcileJournal } from "../codex/journal";
|
|
7
|
+
import {
|
|
8
|
+
codexAutoStartEnabled,
|
|
9
|
+
getConfigDir,
|
|
10
|
+
loadConfig,
|
|
11
|
+
readPid,
|
|
12
|
+
readPidFileValue,
|
|
13
|
+
readRuntimePort,
|
|
14
|
+
removePid,
|
|
15
|
+
removePidIfValueIs,
|
|
16
|
+
removeRuntimePort,
|
|
17
|
+
removeRuntimePortIfPidIs,
|
|
18
|
+
saveConfig,
|
|
19
|
+
writePid,
|
|
20
|
+
writeRuntimePort,
|
|
21
|
+
} from "../config";
|
|
22
|
+
import { collectStatus } from "./status";
|
|
23
|
+
import { installCrashGuards } from "../lib/crash-guard";
|
|
24
|
+
import { hasHelpFlag, printSubcommandUsage, printUsage, printVersion } from "./help";
|
|
25
|
+
import { findAvailablePort, isAddrInUse, shouldPersistSelectedPort } from "../server/ports";
|
|
26
|
+
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
27
|
+
import { stopProxy } from "../lib/process-control";
|
|
28
|
+
import { serviceCommand, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
|
|
29
|
+
import { drainAndShutdown, startServer } from "../server";
|
|
30
|
+
import { startTokenGuardian } from "../oauth/token-guardian";
|
|
31
|
+
import { startHistoryMigrationGuardian } from "../codex/history-migration-guardian";
|
|
32
|
+
import { maybeShowStarPrompt } from "./star-prompt";
|
|
33
|
+
import { maybeShowUpdatePrompt } from "../update/notify";
|
|
34
|
+
import { syncModelsToCodex } from "../codex/sync";
|
|
35
|
+
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
36
|
+
|
|
37
|
+
const args = process.argv.slice(2);
|
|
38
|
+
const command = args[0];
|
|
39
|
+
|
|
40
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
41
|
+
printVersion();
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (command === "help" && args[1]) {
|
|
46
|
+
printSubcommandUsage(args[1]);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (command !== undefined && command !== "help" && hasHelpFlag(args.slice(1))) {
|
|
51
|
+
printSubcommandUsage(command);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parsePortOption(): number | undefined {
|
|
56
|
+
if (args.length === 1) return undefined;
|
|
57
|
+
if (args.length !== 3 || args[1] !== "--port") {
|
|
58
|
+
console.error("Usage: ocx start [--port <port>]");
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const portIdx = args.indexOf("--port");
|
|
62
|
+
if (portIdx === -1) return undefined;
|
|
63
|
+
const value = args[portIdx + 1];
|
|
64
|
+
const port = value && /^\d+$/.test(value) ? Number(value) : NaN;
|
|
65
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
66
|
+
console.error("Invalid port number");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
return port;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function waitForProxy(timeoutMs = 8_000): Promise<LiveProxy | null> {
|
|
73
|
+
const deadline = Date.now() + timeoutMs;
|
|
74
|
+
while (Date.now() < deadline) {
|
|
75
|
+
// Runtime-state-first with identity: finds the proxy even when it started on a
|
|
76
|
+
// fallback port, and never mistakes a foreign 200 for our proxy.
|
|
77
|
+
const live = await findLiveProxy();
|
|
78
|
+
if (live) return live;
|
|
79
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function chooseListenPort(requestedPort?: number): Promise<number> {
|
|
85
|
+
const config = loadConfig();
|
|
86
|
+
const preferred = requestedPort ?? config.port ?? 10100;
|
|
87
|
+
const selected = await findAvailablePort(preferred, config.hostname ?? "127.0.0.1");
|
|
88
|
+
if (selected !== preferred) {
|
|
89
|
+
console.log(`⚠️ Port ${preferred} is busy; starting opencodex on ${selected}.`);
|
|
90
|
+
}
|
|
91
|
+
if (shouldPersistSelectedPort(config.port, selected, preferred)) {
|
|
92
|
+
config.port = selected;
|
|
93
|
+
saveConfig(config);
|
|
94
|
+
}
|
|
95
|
+
return selected;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function handleStart(options: { block?: boolean } = {}) {
|
|
99
|
+
const requestedPort = parsePortOption();
|
|
100
|
+
reconcileJournal();
|
|
101
|
+
const existingPid = readPid();
|
|
102
|
+
if (existingPid) {
|
|
103
|
+
const live = await findLiveProxy();
|
|
104
|
+
if (live) {
|
|
105
|
+
console.error(`⚠️ Proxy already running (PID ${live.pid ?? existingPid}, port ${live.port}). Use 'ocx stop' first.`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
removePid(existingPid);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Interactive-only update prompt. Must run BEFORE we bind a port / write a
|
|
112
|
+
// PID: choosing "Update now" installs globally and exits, so we never want a
|
|
113
|
+
// live daemon holding resources while it overwrites its own binary.
|
|
114
|
+
await maybeShowUpdatePrompt();
|
|
115
|
+
|
|
116
|
+
// Port selection is check-then-bind: a concurrent `ocx start`/`ensure` can win the port
|
|
117
|
+
// between the probe and Bun.serve. Retry the pick instead of dying on EADDRINUSE.
|
|
118
|
+
let port = await chooseListenPort(requestedPort);
|
|
119
|
+
let server: ReturnType<typeof startServer>;
|
|
120
|
+
for (let attempt = 0; ; attempt++) {
|
|
121
|
+
try {
|
|
122
|
+
server = startServer(port);
|
|
123
|
+
break;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (!isAddrInUse(err) || attempt >= 2) throw err;
|
|
126
|
+
console.log(`⚠️ Port ${port} was taken while starting; picking another...`);
|
|
127
|
+
port = await chooseListenPort(requestedPort);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// A single request's streaming error must never crash the daemon serving every
|
|
131
|
+
// other Codex session — capture the full stack to crash.log and stay up.
|
|
132
|
+
installCrashGuards();
|
|
133
|
+
writePid(process.pid);
|
|
134
|
+
|
|
135
|
+
const config = loadConfig();
|
|
136
|
+
writeRuntimePort({ pid: process.pid, port, hostname: config.hostname });
|
|
137
|
+
writeJournal();
|
|
138
|
+
|
|
139
|
+
// Background proactive token refresh. No-op unless config.tokenGuardian.enabled; timer is unref'd
|
|
140
|
+
// so it never keeps the process alive on its own. Stopped in syncCleanup so no refresh fires mid-drain.
|
|
141
|
+
const guardian = startTokenGuardian();
|
|
142
|
+
// Design B upgrade path: keep retrying the one-time opencodex→openai history migration in the
|
|
143
|
+
// background — the first `ocx start` after an update usually races the Codex app's DB lock.
|
|
144
|
+
// Loopback-only (legacy mode still forward-tags) and respects syncResumeHistory opt-out.
|
|
145
|
+
const historyGuardian = !shouldInjectApiAuthHeader(config) && config.syncResumeHistory !== false
|
|
146
|
+
? startHistoryMigrationGuardian()
|
|
147
|
+
: undefined;
|
|
148
|
+
|
|
149
|
+
let cleaned = false;
|
|
150
|
+
const syncCleanup = () => {
|
|
151
|
+
if (cleaned) return;
|
|
152
|
+
cleaned = true;
|
|
153
|
+
try { guardian.stop(); } catch { /* best-effort */ }
|
|
154
|
+
try { historyGuardian?.stop(); } catch { /* best-effort */ }
|
|
155
|
+
removePid(process.pid);
|
|
156
|
+
removeRuntimePort(process.pid);
|
|
157
|
+
if (!process.env.OCX_SERVICE) { try { restoreNativeCodex(); } catch { /* best-effort restore */ } }
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
let shuttingDown = false;
|
|
161
|
+
let shutdownStartedAt = 0;
|
|
162
|
+
// Terminal Ctrl-C delivers SIGINT to the whole foreground group AND the launcher
|
|
163
|
+
// forwards its own — two signals land within milliseconds. Treat a duplicate inside
|
|
164
|
+
// this window as the same Ctrl-C (one graceful drain); a deliberate later press
|
|
165
|
+
// escalates to an immediate force-exit ("gradual kill").
|
|
166
|
+
const FORCE_AFTER_MS = 500;
|
|
167
|
+
const shutdown = () => {
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
if (shuttingDown) {
|
|
170
|
+
if (now - shutdownStartedAt < FORCE_AFTER_MS) return; // near-simultaneous duplicate — ignore
|
|
171
|
+
console.log("\n⏹ Force shutdown (second signal).");
|
|
172
|
+
try { syncCleanup(); } catch { /* best-effort */ }
|
|
173
|
+
process.exit(130);
|
|
174
|
+
}
|
|
175
|
+
shuttingDown = true;
|
|
176
|
+
shutdownStartedAt = now;
|
|
177
|
+
console.log("\n🛑 Shutting down opencodex proxy...");
|
|
178
|
+
void (async () => {
|
|
179
|
+
try {
|
|
180
|
+
await drainAndShutdown(server, config.shutdownTimeoutMs ?? 5000);
|
|
181
|
+
} finally {
|
|
182
|
+
syncCleanup(); // idempotent (cleaned-guard); also re-run by process.on("exit")
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
})();
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
process.on("SIGINT", shutdown);
|
|
189
|
+
process.on("SIGTERM", shutdown);
|
|
190
|
+
// The launcher (bin/ocx.mjs) forwards SIGHUP too (e.g. terminal close); handle it
|
|
191
|
+
// gracefully here so it drains + cleans up instead of a default immediate kill.
|
|
192
|
+
process.on("SIGHUP", shutdown);
|
|
193
|
+
process.on("exit", syncCleanup);
|
|
194
|
+
|
|
195
|
+
await maybeShowStarPrompt(); // once-only [Y/n] GitHub-star prompt on first interactive start
|
|
196
|
+
await syncModelsToCodex(port).catch(() => {});
|
|
197
|
+
if (options.block ?? true) {
|
|
198
|
+
setInterval(() => {}, 60_000);
|
|
199
|
+
await new Promise<void>(() => {});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function handleEnsure() {
|
|
204
|
+
reconcileJournal();
|
|
205
|
+
const config = loadConfig();
|
|
206
|
+
if (!codexAutoStartEnabled(config)) {
|
|
207
|
+
console.log("Codex autostart is disabled.");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const live = await findLiveProxy();
|
|
211
|
+
if (live) {
|
|
212
|
+
await syncModelsToCodex(live.port).catch(e => {
|
|
213
|
+
console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
214
|
+
});
|
|
215
|
+
console.log(`✅ Proxy running on port ${live.port}`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const child = spawn(process.execPath, [process.argv[1], "start"], {
|
|
220
|
+
detached: true,
|
|
221
|
+
stdio: "ignore",
|
|
222
|
+
windowsHide: true,
|
|
223
|
+
env: { ...process.env, OCX_SERVICE: "1" },
|
|
224
|
+
});
|
|
225
|
+
child.unref();
|
|
226
|
+
|
|
227
|
+
const port = (await waitForProxy())?.port;
|
|
228
|
+
if (!port) {
|
|
229
|
+
console.error("❌ Proxy did not become healthy after starting.");
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
// Always sync the LIVE port: after a fallback-port start, config.port still names the
|
|
233
|
+
// busy preferred port — syncing that would point Codex at a dead listener.
|
|
234
|
+
await syncModelsToCodex(port).catch(e => {
|
|
235
|
+
console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
236
|
+
});
|
|
237
|
+
console.log(`✅ Proxy running on port ${port}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function handleStop() {
|
|
241
|
+
const stoppedService = stopServiceIfInstalled();
|
|
242
|
+
if (stoppedService) console.log("🛑 Service manager stopped (won't respawn).");
|
|
243
|
+
|
|
244
|
+
const pid = readPid();
|
|
245
|
+
let stopFailed = false;
|
|
246
|
+
if (pid) {
|
|
247
|
+
try {
|
|
248
|
+
// Graceful-first (management-API drain) — on Windows this is the only path where
|
|
249
|
+
// the proxy's shutdown handlers actually run; taskkill /F is the fallback inside.
|
|
250
|
+
await stopProxy(pid);
|
|
251
|
+
console.log(`✅ Proxy (PID ${pid}) stopped.`);
|
|
252
|
+
removePid(pid);
|
|
253
|
+
removeRuntimePort(pid);
|
|
254
|
+
} catch {
|
|
255
|
+
stopFailed = true;
|
|
256
|
+
console.error(`❌ Failed to stop proxy (PID ${pid}).`);
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
// Snapshot the stale on-disk state BEFORE the async probe: a concurrent `ocx start`
|
|
260
|
+
// can write fresh records mid-probe, and the purge below must never delete those.
|
|
261
|
+
const stalePidValue = readPidFileValue();
|
|
262
|
+
const staleRuntimePid = readRuntimePort()?.pid ?? null;
|
|
263
|
+
// Orphan recovery: a live proxy can outlive its pid file (crash, manual delete,
|
|
264
|
+
// corrupt file). Identity-checked liveness still finds it via the runtime record.
|
|
265
|
+
const live = await findLiveProxy();
|
|
266
|
+
if (live?.pid) {
|
|
267
|
+
try {
|
|
268
|
+
await stopProxy(live.pid);
|
|
269
|
+
console.log(`✅ Proxy (PID ${live.pid}) stopped.`);
|
|
270
|
+
} catch {
|
|
271
|
+
stopFailed = true;
|
|
272
|
+
console.error(`❌ Failed to stop proxy (PID ${live.pid}).`);
|
|
273
|
+
}
|
|
274
|
+
} else if (!stoppedService) {
|
|
275
|
+
console.log("No running proxy found.");
|
|
276
|
+
}
|
|
277
|
+
if (!stopFailed) {
|
|
278
|
+
// `readPid() === null` means the snapshotted pid file was absent, invalid, dead, or
|
|
279
|
+
// not ours — stale by definition. Purge (guarded by the snapshot) so `ocx update`'s
|
|
280
|
+
// stop gate can't wedge on it.
|
|
281
|
+
removePidIfValueIs(stalePidValue);
|
|
282
|
+
removeRuntimePortIfPidIs(staleRuntimePid);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const r = restoreNativeCodex();
|
|
286
|
+
console.log(`↩️ ${r.message}`);
|
|
287
|
+
if (stopFailed) process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function handleUninstall() {
|
|
291
|
+
const failures: string[] = [];
|
|
292
|
+
|
|
293
|
+
const runStep = async (label: string, step: () => void | boolean | Promise<void | boolean>) => {
|
|
294
|
+
try {
|
|
295
|
+
const changed = await step();
|
|
296
|
+
if (changed === false) console.log(`- ${label}: not installed`);
|
|
297
|
+
else console.log(`✅ ${label}`);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
failures.push(label);
|
|
300
|
+
console.error(`⚠️ ${label} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
await runStep("service stopped", () => stopServiceIfInstalled());
|
|
305
|
+
|
|
306
|
+
await runStep("proxy stopped", async () => {
|
|
307
|
+
const pid = readPid();
|
|
308
|
+
if (!pid) return false;
|
|
309
|
+
await stopProxy(pid);
|
|
310
|
+
removePid(pid);
|
|
311
|
+
removeRuntimePort(pid);
|
|
312
|
+
return true;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await runStep("service removed", () => uninstallServiceIfInstalled());
|
|
316
|
+
|
|
317
|
+
await runStep("native Codex restored", () => {
|
|
318
|
+
const r = restoreNativeCodex();
|
|
319
|
+
if (!r.success) throw new Error(r.message);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const { uninstallCodexShim } = await import("../codex/shim");
|
|
324
|
+
const r = uninstallCodexShim();
|
|
325
|
+
console.log(r.removed ? "✅ Codex autostart shim removed" : "- Codex autostart shim removed: not installed");
|
|
326
|
+
} catch (err) {
|
|
327
|
+
failures.push("Codex autostart shim removed");
|
|
328
|
+
console.error(`⚠️ Codex autostart shim removed failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (failures.length === 0) {
|
|
332
|
+
await runStep("opencodex config removed", () => {
|
|
333
|
+
rmSync(getConfigDir(), { recursive: true, force: true });
|
|
334
|
+
});
|
|
335
|
+
} else {
|
|
336
|
+
console.error("Leaving opencodex config/backups in place so the failed restore step can be retried.");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (failures.length > 0) {
|
|
340
|
+
console.error(`\nUninstall finished with ${failures.length} failed step(s): ${failures.join(", ")}`);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
console.log("\n✅ opencodex local state removed. Remove the package with: npm uninstall -g @bitkyc08/opencodex");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function handleStatus() {
|
|
347
|
+
const statusArgs = args.slice(1);
|
|
348
|
+
const wantsJson = statusArgs.length === 1 && statusArgs[0] === "--json";
|
|
349
|
+
if (statusArgs.length > 1 || (statusArgs.length === 1 && !wantsJson)) {
|
|
350
|
+
console.error("Usage: ocx status [--json]");
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const status = await collectStatus();
|
|
355
|
+
if (wantsJson) {
|
|
356
|
+
console.log(JSON.stringify(status.json, null, 2));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (status.json.proxy.pid || status.json.proxy.health.ok) {
|
|
361
|
+
console.log(`✅ Proxy: ${status.proxyLabel}`);
|
|
362
|
+
} else {
|
|
363
|
+
console.log(`❌ Proxy: ${status.proxyLabel}`);
|
|
364
|
+
}
|
|
365
|
+
console.log(` Health: ${status.healthLabel}`);
|
|
366
|
+
console.log(` Dashboard: ${status.json.dashboard.url}`);
|
|
367
|
+
console.log(` Config: ${status.json.paths.config}`);
|
|
368
|
+
console.log(` PID file: ${status.json.paths.pid}`);
|
|
369
|
+
console.log(` Runtime: ${status.json.paths.runtime}`);
|
|
370
|
+
console.log(` Runtime source: ${status.json.runtime.source}${status.json.runtime.overrideEnv ? ` (${status.json.runtime.overrideEnv})` : ""}`);
|
|
371
|
+
console.log(` Default provider: ${status.json.defaultProvider}`);
|
|
372
|
+
console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}`);
|
|
373
|
+
console.log(` Service: ${status.json.service.summary}`);
|
|
374
|
+
console.log(` ${status.json.codexShim.summary}`);
|
|
375
|
+
if (status.json.codexPlugins.applicable) {
|
|
376
|
+
const icon = status.json.codexPlugins.stale ? "⚠️ " : "✅";
|
|
377
|
+
console.log(` ${icon} Codex bundled plugins: ${status.json.codexPlugins.summary}`);
|
|
378
|
+
if (status.json.codexPlugins.suggestedRepair) {
|
|
379
|
+
console.log(` Suggested: ${status.json.codexPlugins.suggestedRepair}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const { oauthLoginSummary } = await import("../oauth");
|
|
383
|
+
console.log(` OAuth logins:`);
|
|
384
|
+
for (const e of oauthLoginSummary()) {
|
|
385
|
+
console.log(` ${e.provider.padEnd(10)} ${e.loggedIn ? `✓ logged in${e.email ? ` (${e.email})` : ""}` : "✗ not logged in"}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function handleRecoverHistory() {
|
|
390
|
+
if (args[1] !== "--legacy-openai") {
|
|
391
|
+
console.error("Usage: ocx recover-history --legacy-openai");
|
|
392
|
+
console.error("Only use this if an older syncResumeHistory build already remapped OpenAI Codex App history to opencodex before backup support existed.");
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
const r = restoreLegacyOpenaiHistory();
|
|
396
|
+
if (r.failed) {
|
|
397
|
+
console.error(
|
|
398
|
+
"⚠️ Recovery SKIPPED: the Codex history DB is locked (Codex app/IDE open?). Close it and rerun this command.",
|
|
399
|
+
);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
console.log(`Recovered ${r.rows} legacy thread(s) to openai (${r.files} rollout file(s) updated).`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
switch (command) {
|
|
406
|
+
case "init": {
|
|
407
|
+
const { runInit } = await import("./init");
|
|
408
|
+
await runInit();
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case "start":
|
|
412
|
+
await handleStart();
|
|
413
|
+
break;
|
|
414
|
+
case "stop":
|
|
415
|
+
await handleStop();
|
|
416
|
+
break;
|
|
417
|
+
case "restore":
|
|
418
|
+
case "eject": {
|
|
419
|
+
if (args[1] === "back") {
|
|
420
|
+
// Reverse switch: re-point plain `codex` at the RUNNING proxy without touching its
|
|
421
|
+
// lifecycle — the counterpart of `ocx restore`. Start/stop triggers are unchanged;
|
|
422
|
+
// this only re-runs the same inject (config + catalog + history) `ocx start` does.
|
|
423
|
+
const live = await findLiveProxy();
|
|
424
|
+
if (!live) {
|
|
425
|
+
console.error("No running proxy found. Run 'ocx start' — it injects opencodex automatically.");
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
await syncModelsToCodex(live.port);
|
|
429
|
+
console.log("Plain `codex` now routes through opencodex again (undo with: ocx restore).");
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
const r = restoreNativeCodex();
|
|
433
|
+
console.log(r.success ? `✅ ${r.message}` : `⚠️ ${r.message}`);
|
|
434
|
+
console.log("Plain `codex` now runs natively (no proxy). Switch back with: ocx restore back");
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
case "recover-history":
|
|
438
|
+
handleRecoverHistory();
|
|
439
|
+
break;
|
|
440
|
+
case "uninstall":
|
|
441
|
+
case "remove":
|
|
442
|
+
await handleUninstall();
|
|
443
|
+
break;
|
|
444
|
+
case "status":
|
|
445
|
+
await handleStatus();
|
|
446
|
+
break;
|
|
447
|
+
case "doctor": {
|
|
448
|
+
const { runDoctor } = await import("./doctor");
|
|
449
|
+
await runDoctor();
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
case "ensure":
|
|
453
|
+
await handleEnsure();
|
|
454
|
+
break;
|
|
455
|
+
case "login": {
|
|
456
|
+
const { handleLogin } = await import("../oauth/login-cli");
|
|
457
|
+
await handleLogin(args[1]);
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
case "logout": {
|
|
461
|
+
const { removeCredential } = await import("../oauth/store");
|
|
462
|
+
const name = (args[1] ?? "").trim().toLowerCase();
|
|
463
|
+
removeCredential(name);
|
|
464
|
+
console.log(`Logged out of ${name || "(none)"}.`);
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
case "sync": {
|
|
468
|
+
await syncModelsToCodex((await findLiveProxy())?.port);
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
case "sync-cache": {
|
|
472
|
+
const { invalidateCodexModelsCache } = await import("../codex/catalog");
|
|
473
|
+
invalidateCodexModelsCache();
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
case "gui": {
|
|
477
|
+
const cfg = await import("../config");
|
|
478
|
+
const config = cfg.loadConfig();
|
|
479
|
+
// Identity-checked liveness (not the pid file + a fixed sleep): finds a fallback-port
|
|
480
|
+
// proxy and waits until the spawned one actually answers before opening the browser.
|
|
481
|
+
let live = await findLiveProxy();
|
|
482
|
+
if (!live) {
|
|
483
|
+
console.log("Proxy not running. Starting...");
|
|
484
|
+
const child = spawn(process.execPath, [process.argv[1], "start"], {
|
|
485
|
+
detached: true,
|
|
486
|
+
stdio: "ignore",
|
|
487
|
+
windowsHide: true,
|
|
488
|
+
env: process.env,
|
|
489
|
+
});
|
|
490
|
+
child.unref();
|
|
491
|
+
live = await waitForProxy();
|
|
492
|
+
}
|
|
493
|
+
// Open the host the proxy actually binds — `localhost` only answers for
|
|
494
|
+
// loopback/wildcard binds, not a concrete LAN/IPv6 hostname.
|
|
495
|
+
const guiHost = probeHostname(live?.hostname ?? config.hostname);
|
|
496
|
+
const guiUrl = `http://${guiHost === "127.0.0.1" ? "localhost" : guiHost}:${live?.port ?? config.port}`;
|
|
497
|
+
console.log(`Opening ${guiUrl}`);
|
|
498
|
+
const { openUrl } = await import("../lib/open-url");
|
|
499
|
+
openUrl(guiUrl);
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
case "service":
|
|
503
|
+
await serviceCommand(args[1]);
|
|
504
|
+
break;
|
|
505
|
+
case "codex-shim": {
|
|
506
|
+
const { codexShimStatus, installCodexShim, uninstallCodexShim } = await import("../codex/shim");
|
|
507
|
+
switch (args[1]) {
|
|
508
|
+
case "install": {
|
|
509
|
+
const r = installCodexShim();
|
|
510
|
+
console.log(r.installed ? `✅ ${r.message}` : `⚠️ ${r.message}`);
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
case "status":
|
|
514
|
+
console.log(codexShimStatus());
|
|
515
|
+
break;
|
|
516
|
+
case "uninstall":
|
|
517
|
+
case "remove": {
|
|
518
|
+
const r = uninstallCodexShim();
|
|
519
|
+
console.log(r.removed ? `✅ ${r.message}` : `⚠️ ${r.message}`);
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
default:
|
|
523
|
+
console.error("Usage: ocx codex-shim <install|status|uninstall|remove>");
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
case "update": {
|
|
529
|
+
const { runUpdate } = await import("../update");
|
|
530
|
+
await runUpdate();
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
case "__refresh-version": {
|
|
534
|
+
// Hidden, detached helper spawned by the update prompt to refresh the
|
|
535
|
+
// cached latest version without blocking the foreground start. Not in help.
|
|
536
|
+
const { refreshVersionCache } = await import("../update/notify");
|
|
537
|
+
const channel = args[1] === "preview" ? "preview" : "latest";
|
|
538
|
+
await refreshVersionCache(channel);
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
case "__gui-update-worker": {
|
|
542
|
+
const jobId = args[1];
|
|
543
|
+
if (!jobId) process.exit(1);
|
|
544
|
+
const channel = normalizeUpdateChannel(args[2]);
|
|
545
|
+
runGuiUpdateWorker(jobId, channel, args[3] === "restart");
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
case "restart": {
|
|
549
|
+
await handleStop();
|
|
550
|
+
await handleEnsure();
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
case "health": {
|
|
554
|
+
const healthArgs = args.slice(1);
|
|
555
|
+
const wantsHealthJson = healthArgs.includes("--json");
|
|
556
|
+
const live = await findLiveProxy();
|
|
557
|
+
if (wantsHealthJson) {
|
|
558
|
+
console.log(JSON.stringify({ ok: !!live, pid: live?.pid ?? null, port: live?.port ?? null }));
|
|
559
|
+
} else {
|
|
560
|
+
console.log(live ? `Proxy healthy (PID ${live.pid}, port ${live.port})` : "Proxy not healthy");
|
|
561
|
+
}
|
|
562
|
+
process.exit(live ? 0 : 1);
|
|
563
|
+
}
|
|
564
|
+
case "provider": {
|
|
565
|
+
const { handleProviderCommand } = await import("./provider");
|
|
566
|
+
await handleProviderCommand(args.slice(1));
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
case "models": {
|
|
570
|
+
const { handleModels } = await import("./models");
|
|
571
|
+
handleModels(args.slice(1));
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
case "help":
|
|
575
|
+
case "--help":
|
|
576
|
+
case "-h":
|
|
577
|
+
case undefined:
|
|
578
|
+
printUsage();
|
|
579
|
+
break;
|
|
580
|
+
default:
|
|
581
|
+
console.error(`Unknown command: ${command}`);
|
|
582
|
+
printUsage();
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as readline from "node:readline";
|
|
2
|
-
import { injectCodexConfig } from "
|
|
3
|
-
import { getDefaultConfig, isValidProviderName, saveConfig } from "
|
|
4
|
-
import { enrichProviderFromCatalog } from "
|
|
5
|
-
import { deriveInitProviders } from "
|
|
6
|
-
import type { OcxConfig, OcxProviderConfig } from "
|
|
2
|
+
import { injectCodexConfig } from "../codex/inject";
|
|
3
|
+
import { getDefaultConfig, isValidProviderName, saveConfig } from "../config";
|
|
4
|
+
import { enrichProviderFromCatalog } from "../oauth/key-providers";
|
|
5
|
+
import { deriveInitProviders } from "../providers/derive";
|
|
6
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
7
7
|
|
|
8
8
|
function createPrompt(): { ask(question: string): Promise<string>; close(): void } {
|
|
9
9
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -139,7 +139,7 @@ export async function runInit(): Promise<void> {
|
|
|
139
139
|
const shimAnswer = await prompt.ask("Install Codex autostart shim? [Y/n]: ");
|
|
140
140
|
if (shimAnswer.trim().toLowerCase() !== "n") {
|
|
141
141
|
try {
|
|
142
|
-
const { installCodexShim } = await import("
|
|
142
|
+
const { installCodexShim } = await import("../codex/shim");
|
|
143
143
|
const result = installCodexShim();
|
|
144
144
|
console.log(result.installed ? `✅ ${result.message}` : `⚠️ ${result.message}`);
|
|
145
145
|
} catch (err) {
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Usage:
|
|
5
5
|
* ocx models [--provider <name>] [--json]
|
|
6
6
|
*/
|
|
7
|
-
import { hasOwnProvider, loadConfig } from "
|
|
8
|
-
import type { OcxConfig } from "
|
|
7
|
+
import { hasOwnProvider, loadConfig } from "../config";
|
|
8
|
+
import type { OcxConfig } from "../types";
|
|
9
9
|
|
|
10
10
|
interface ModelEntry {
|
|
11
11
|
provider: string;
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* show <name> Show provider config details (secrets masked)
|
|
9
9
|
* set-default <name> Change the default provider
|
|
10
10
|
*/
|
|
11
|
-
import { hasOwnProvider, isValidProviderName, loadConfig, saveConfig } from "
|
|
12
|
-
import { hasHelpFlag } from "./
|
|
13
|
-
import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "
|
|
14
|
-
import { providerConfigSeed } from "
|
|
15
|
-
import type { OcxProviderConfig } from "
|
|
16
|
-
import { findLiveProxy } from "
|
|
17
|
-
import { syncModelsToCodex } from "
|
|
11
|
+
import { hasOwnProvider, isValidProviderName, loadConfig, saveConfig } from "../config";
|
|
12
|
+
import { hasHelpFlag } from "./help";
|
|
13
|
+
import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry";
|
|
14
|
+
import { providerConfigSeed } from "../providers/derive";
|
|
15
|
+
import type { OcxProviderConfig } from "../types";
|
|
16
|
+
import { findLiveProxy } from "../server/proxy-liveness";
|
|
17
|
+
import { syncModelsToCodex } from "../codex/sync";
|
|
18
18
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
// Arg helpers
|
|
@@ -302,7 +302,11 @@ function handleShow(args: string[]): void {
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
const prov = config.providers[name];
|
|
305
|
-
const display = {
|
|
305
|
+
const display = {
|
|
306
|
+
...prov,
|
|
307
|
+
...(prov.apiKey ? { apiKey: maskSecret(prov.apiKey) } : {}),
|
|
308
|
+
...(prov.apiKeyPool ? { apiKeyPool: prov.apiKeyPool.map(e => ({ ...e, key: maskSecret(e.key) })) } : {}),
|
|
309
|
+
};
|
|
306
310
|
|
|
307
311
|
if (wantsJson) {
|
|
308
312
|
console.log(JSON.stringify({ name, isDefault: name === config.defaultProvider, ...display }, null, 2));
|
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
|
-
import { getConfigDir } from "
|
|
5
|
+
import { getConfigDir } from "../config";
|
|
6
6
|
|
|
7
7
|
const REPO = "lidge-jun/opencodex";
|
|
8
8
|
/** Fires exactly once from the first interactive `ocx start`. */
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { durableBunRuntime } from "
|
|
2
|
-
import { codexAutoStartEnabled, getConfigPath, getPidPath, readConfigDiagnostics, readPid, readRuntimePort, type RuntimePortState } from "
|
|
3
|
-
import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "
|
|
4
|
-
import { isOpencodexHealthz, probeHostname } from "
|
|
5
|
-
import type { OcxConfig } from "
|
|
6
|
-
import { serviceStatusSummary } from "
|
|
1
|
+
import { durableBunRuntime } from "../lib/bun-runtime";
|
|
2
|
+
import { codexAutoStartEnabled, getConfigPath, getPidPath, readConfigDiagnostics, readPid, readRuntimePort, type RuntimePortState } from "../config";
|
|
3
|
+
import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../codex/plugins-doctor";
|
|
4
|
+
import { isOpencodexHealthz, probeHostname } from "../server/proxy-liveness";
|
|
5
|
+
import type { OcxConfig } from "../types";
|
|
6
|
+
import { serviceStatusSummary } from "../service";
|
|
7
7
|
|
|
8
8
|
type HealthCheck = {
|
|
9
9
|
ok: boolean;
|
|
@@ -116,7 +116,7 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
116
116
|
const health = await checkProxyHealth(listen);
|
|
117
117
|
const bunRuntime = durableBunRuntime();
|
|
118
118
|
const serviceSummary = serviceStatusSummary();
|
|
119
|
-
const { codexShimStatus } = await import("
|
|
119
|
+
const { codexShimStatus } = await import("../codex/shim");
|
|
120
120
|
const codexShimSummary = codexShimStatus();
|
|
121
121
|
const codexPlugins = diagnoseCodexBundledPlugins();
|
|
122
122
|
const proxyLabel = pid && health.ok
|