@bitkyc08/opencodex 2.0.2 → 2.1.3
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 +42 -2
- package/README.md +43 -3
- package/README.zh-CN.md +21 -0
- package/gui/dist/assets/index-34pGgy8q.js +9 -0
- package/gui/dist/assets/{index-cEIM1XWY.css → index-dCS-lwCM.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +7 -3
- package/src/adapters/azure.ts +7 -7
- package/src/adapters/google.ts +4 -3
- package/src/adapters/openai-chat.ts +2 -1
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +142 -26
- package/src/cli.ts +166 -13
- package/src/codex-catalog.ts +1 -0
- package/src/codex-history-provider.ts +86 -0
- package/src/codex-inject.ts +9 -1
- package/src/codex-shim.ts +76 -32
- package/src/config.ts +31 -5
- package/src/init.ts +11 -0
- package/src/oauth/store.ts +10 -4
- package/src/open-url.ts +7 -3
- package/src/ports.ts +30 -0
- package/src/providers/registry.ts +1 -1
- package/src/responses/parser.ts +9 -6
- package/src/responses/schema.ts +1 -0
- package/src/server.ts +208 -43
- package/src/service.ts +29 -2
- package/src/types.ts +10 -0
- package/src/update.ts +12 -2
- package/src/web-search/loop.ts +9 -1
- package/src/ws-bridge.ts +1 -1
- package/gui/dist/assets/index-PrH8v83W.js +0 -9
package/src/cli.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { execFileSync, spawn } from "node:child_process";
|
|
3
|
+
import { rmSync } from "node:fs";
|
|
3
4
|
import { restoreNativeCodex } from "./codex-inject";
|
|
4
|
-
import { loadConfig, readPid, removePid, writePid } from "./config";
|
|
5
|
-
import {
|
|
5
|
+
import { codexAutoStartEnabled, getConfigDir, loadConfig, readPid, removePid, saveConfig, writePid } from "./config";
|
|
6
|
+
import { findAvailablePort } from "./ports";
|
|
7
|
+
import { serviceCommand, stopServiceIfInstalled, uninstallServiceIfInstalled } from "./service";
|
|
6
8
|
import { startServer } from "./server";
|
|
7
9
|
import { maybeShowStarPrompt } from "./star-prompt";
|
|
8
10
|
|
|
@@ -17,9 +19,12 @@ Usage:
|
|
|
17
19
|
ocx start [--port <port>] Start the proxy server (auto-syncs models to Codex)
|
|
18
20
|
ocx stop Stop the proxy AND restore native Codex (plain codex works again)
|
|
19
21
|
ocx restore Restore native Codex without stopping (alias: eject)
|
|
22
|
+
ocx uninstall Remove service/shim/config and restore native Codex
|
|
20
23
|
ocx service <sub> Run as a background service (install|start|stop|status|uninstall)
|
|
21
24
|
ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall)
|
|
25
|
+
ocx ensure Ensure the proxy is running and Codex config/cache are current
|
|
22
26
|
ocx sync Fetch models from providers and inject into Codex config
|
|
27
|
+
ocx sync-cache Refresh Codex's model cache from the active catalog
|
|
23
28
|
ocx status Check proxy server status
|
|
24
29
|
ocx login <provider> OAuth login (xai) — opens browser, stores token in ~/.opencodex/auth.json
|
|
25
30
|
ocx logout <provider> Remove a stored OAuth login
|
|
@@ -55,23 +60,74 @@ async function syncModelsToCodex(port?: number) {
|
|
|
55
60
|
return result;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
|
|
63
|
+
function parsePortOption(): number | undefined {
|
|
64
|
+
const portIdx = args.indexOf("--port");
|
|
65
|
+
if (portIdx === -1) return undefined;
|
|
66
|
+
const value = args[portIdx + 1];
|
|
67
|
+
const port = value ? parseInt(value, 10) : NaN;
|
|
68
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
69
|
+
console.error("Invalid port number");
|
|
62
70
|
process.exit(1);
|
|
63
71
|
}
|
|
72
|
+
return port;
|
|
73
|
+
}
|
|
64
74
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
function healthHost(hostname?: string): string {
|
|
76
|
+
return !hostname || hostname === "0.0.0.0" || hostname === "::" ? "127.0.0.1" : hostname;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function proxyHealthy(port?: number): Promise<boolean> {
|
|
80
|
+
const config = loadConfig();
|
|
81
|
+
const p = port ?? config.port ?? 10100;
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(`http://${healthHost(config.hostname)}:${p}/healthz`, {
|
|
84
|
+
signal: AbortSignal.timeout(750),
|
|
85
|
+
});
|
|
86
|
+
return res.ok;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function waitForProxy(timeoutMs = 8_000): Promise<number | null> {
|
|
93
|
+
const deadline = Date.now() + timeoutMs;
|
|
94
|
+
while (Date.now() < deadline) {
|
|
95
|
+
const config = loadConfig();
|
|
96
|
+
const port = config.port ?? 10100;
|
|
97
|
+
if (await proxyHealthy(port)) return port;
|
|
98
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function chooseListenPort(requestedPort?: number): Promise<number> {
|
|
104
|
+
const config = loadConfig();
|
|
105
|
+
const preferred = requestedPort ?? config.port ?? 10100;
|
|
106
|
+
const selected = await findAvailablePort(preferred, config.hostname ?? "127.0.0.1");
|
|
107
|
+
if (selected !== preferred) {
|
|
108
|
+
console.log(`⚠️ Port ${preferred} is busy; starting opencodex on ${selected}.`);
|
|
109
|
+
}
|
|
110
|
+
if (config.port !== selected) {
|
|
111
|
+
config.port = selected;
|
|
112
|
+
saveConfig(config);
|
|
113
|
+
}
|
|
114
|
+
return selected;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function handleStart(options: { block?: boolean } = {}) {
|
|
118
|
+
const existingPid = readPid();
|
|
119
|
+
if (existingPid) {
|
|
120
|
+
const config = loadConfig();
|
|
121
|
+
if (await proxyHealthy(config.port)) {
|
|
122
|
+
console.error(`⚠️ Proxy already running (PID ${existingPid}). Use 'ocx stop' first.`);
|
|
71
123
|
process.exit(1);
|
|
72
124
|
}
|
|
125
|
+
removePid();
|
|
73
126
|
}
|
|
74
127
|
|
|
128
|
+
const requestedPort = parsePortOption();
|
|
129
|
+
const port = await chooseListenPort(requestedPort);
|
|
130
|
+
|
|
75
131
|
const server = startServer(port);
|
|
76
132
|
writePid(process.pid);
|
|
77
133
|
|
|
@@ -96,6 +152,39 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
96
152
|
}
|
|
97
153
|
}
|
|
98
154
|
|
|
155
|
+
async function handleEnsure() {
|
|
156
|
+
let config = loadConfig();
|
|
157
|
+
if (!codexAutoStartEnabled(config)) {
|
|
158
|
+
console.log("Codex autostart is disabled.");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (await proxyHealthy(config.port)) {
|
|
162
|
+
await syncModelsToCodex(config.port).catch(e => {
|
|
163
|
+
console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
164
|
+
});
|
|
165
|
+
console.log(`✅ Proxy running on port ${config.port}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const child = spawn(process.execPath, [process.argv[1], "start"], {
|
|
170
|
+
detached: true,
|
|
171
|
+
stdio: "ignore",
|
|
172
|
+
env: { ...process.env, OCX_SERVICE: "1" },
|
|
173
|
+
});
|
|
174
|
+
child.unref();
|
|
175
|
+
|
|
176
|
+
const port = await waitForProxy();
|
|
177
|
+
if (!port) {
|
|
178
|
+
console.error("❌ Proxy did not become healthy after starting.");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
config = loadConfig();
|
|
182
|
+
await syncModelsToCodex(config.port ?? port).catch(e => {
|
|
183
|
+
console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
184
|
+
});
|
|
185
|
+
console.log(`✅ Proxy running on port ${config.port ?? port}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
99
188
|
function killProxy(pid: number): void {
|
|
100
189
|
if (!isProcessAlive(pid)) return;
|
|
101
190
|
if (process.platform === "win32") {
|
|
@@ -154,6 +243,58 @@ function handleStop() {
|
|
|
154
243
|
if (stopFailed) process.exit(1);
|
|
155
244
|
}
|
|
156
245
|
|
|
246
|
+
async function handleUninstall() {
|
|
247
|
+
const failures: string[] = [];
|
|
248
|
+
|
|
249
|
+
const runStep = (label: string, step: () => void | boolean) => {
|
|
250
|
+
try {
|
|
251
|
+
const changed = step();
|
|
252
|
+
if (changed === false) console.log(`- ${label}: not installed`);
|
|
253
|
+
else console.log(`✅ ${label}`);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
failures.push(label);
|
|
256
|
+
console.error(`⚠️ ${label} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
runStep("service removed", () => {
|
|
261
|
+
stopServiceIfInstalled();
|
|
262
|
+
return uninstallServiceIfInstalled();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
runStep("proxy stopped", () => {
|
|
266
|
+
const pid = readPid();
|
|
267
|
+
if (!pid) return false;
|
|
268
|
+
killProxy(pid);
|
|
269
|
+
removePid();
|
|
270
|
+
return true;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
runStep("native Codex restored", () => {
|
|
274
|
+
const r = restoreNativeCodex();
|
|
275
|
+
if (!r.success) throw new Error(r.message);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const { uninstallCodexShim } = await import("./codex-shim");
|
|
280
|
+
const r = uninstallCodexShim();
|
|
281
|
+
console.log(r.removed ? "✅ Codex autostart shim removed" : "- Codex autostart shim removed: not installed");
|
|
282
|
+
} catch (err) {
|
|
283
|
+
failures.push("Codex autostart shim removed");
|
|
284
|
+
console.error(`⚠️ Codex autostart shim removed failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
runStep("opencodex config removed", () => {
|
|
288
|
+
rmSync(getConfigDir(), { recursive: true, force: true });
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (failures.length > 0) {
|
|
292
|
+
console.error(`\nUninstall finished with ${failures.length} failed step(s): ${failures.join(", ")}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
console.log("\n✅ opencodex local state removed. Remove the package with: npm uninstall -g @bitkyc08/opencodex");
|
|
296
|
+
}
|
|
297
|
+
|
|
157
298
|
function handleStatus() {
|
|
158
299
|
const pid = readPid();
|
|
159
300
|
if (pid) {
|
|
@@ -182,9 +323,16 @@ switch (command) {
|
|
|
182
323
|
console.log("Plain `codex` now runs natively (no proxy).");
|
|
183
324
|
break;
|
|
184
325
|
}
|
|
326
|
+
case "uninstall":
|
|
327
|
+
case "remove":
|
|
328
|
+
await handleUninstall();
|
|
329
|
+
break;
|
|
185
330
|
case "status":
|
|
186
331
|
handleStatus();
|
|
187
332
|
break;
|
|
333
|
+
case "ensure":
|
|
334
|
+
await handleEnsure();
|
|
335
|
+
break;
|
|
188
336
|
case "login": {
|
|
189
337
|
const { handleLogin } = await import("./oauth/login-cli");
|
|
190
338
|
await handleLogin(args[1]);
|
|
@@ -201,6 +349,11 @@ switch (command) {
|
|
|
201
349
|
await syncModelsToCodex();
|
|
202
350
|
break;
|
|
203
351
|
}
|
|
352
|
+
case "sync-cache": {
|
|
353
|
+
const { invalidateCodexModelsCache } = await import("./codex-catalog");
|
|
354
|
+
invalidateCodexModelsCache();
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
204
357
|
case "gui": {
|
|
205
358
|
const cfg = await import("./config");
|
|
206
359
|
const config = cfg.loadConfig();
|
|
@@ -248,7 +401,7 @@ switch (command) {
|
|
|
248
401
|
}
|
|
249
402
|
case "update": {
|
|
250
403
|
const { runUpdate } = await import("./update");
|
|
251
|
-
runUpdate();
|
|
404
|
+
await runUpdate();
|
|
252
405
|
break;
|
|
253
406
|
}
|
|
254
407
|
case "help":
|
package/src/codex-catalog.ts
CHANGED
|
@@ -150,6 +150,7 @@ export function normalizeRoutedCatalogEntry(entry: RawEntry): RawEntry {
|
|
|
150
150
|
// runs through native gpt-5.4-mini, so image search is available and verbalized for text-only models.
|
|
151
151
|
entry.web_search_tool_type = "text_and_image";
|
|
152
152
|
entry.supports_search_tool = true;
|
|
153
|
+
entry.supports_parallel_tool_calls = false;
|
|
153
154
|
return ensureStrictCatalogFields(entry);
|
|
154
155
|
}
|
|
155
156
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
import { CODEX_HOME } from "./codex-paths";
|
|
5
|
+
|
|
6
|
+
const STATE_DB_PATH = join(CODEX_HOME, "state_5.sqlite");
|
|
7
|
+
const RESUMABLE_SOURCES = ["cli", "vscode"] as const;
|
|
8
|
+
|
|
9
|
+
type CodexHistoryProvider = "openai" | "opencodex";
|
|
10
|
+
|
|
11
|
+
interface ThreadRow {
|
|
12
|
+
id: string;
|
|
13
|
+
rollout_path: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function updateSessionMetaProvider(path: string, provider: CodexHistoryProvider): boolean {
|
|
17
|
+
if (!path || !existsSync(path)) return false;
|
|
18
|
+
const stat = statSync(path);
|
|
19
|
+
const raw = readFileSync(path, "utf8");
|
|
20
|
+
const newline = raw.indexOf("\n");
|
|
21
|
+
const firstLine = newline === -1 ? raw : raw.slice(0, newline);
|
|
22
|
+
const rest = newline === -1 ? "" : raw.slice(newline);
|
|
23
|
+
|
|
24
|
+
let parsed: unknown;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(firstLine);
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!parsed || typeof parsed !== "object") return false;
|
|
32
|
+
const record = parsed as { type?: unknown; payload?: { model_provider?: unknown } };
|
|
33
|
+
if (record.type !== "session_meta" || !record.payload || typeof record.payload !== "object") return false;
|
|
34
|
+
if (record.payload.model_provider === provider) return false;
|
|
35
|
+
|
|
36
|
+
record.payload.model_provider = provider;
|
|
37
|
+
writeFileSync(path, `${JSON.stringify(record)}${rest}`, "utf8");
|
|
38
|
+
utimesSync(path, stat.atime, stat.mtime);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function syncCodexHistoryProvider(provider: CodexHistoryProvider, stateDbPath = STATE_DB_PATH): { rows: number; files: number } {
|
|
43
|
+
if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
|
|
44
|
+
const from = provider === "opencodex" ? "openai" : "opencodex";
|
|
45
|
+
const db = new Database(stateDbPath);
|
|
46
|
+
try {
|
|
47
|
+
const placeholders = RESUMABLE_SOURCES.map(() => "?").join(",");
|
|
48
|
+
const rows = db
|
|
49
|
+
.query<ThreadRow, string[]>(`
|
|
50
|
+
SELECT id, rollout_path
|
|
51
|
+
FROM threads
|
|
52
|
+
WHERE model_provider = ?
|
|
53
|
+
AND source IN (${placeholders})
|
|
54
|
+
`)
|
|
55
|
+
.all(from, ...RESUMABLE_SOURCES);
|
|
56
|
+
|
|
57
|
+
let files = 0;
|
|
58
|
+
for (const row of rows) {
|
|
59
|
+
try {
|
|
60
|
+
if (updateSessionMetaProvider(row.rollout_path, provider)) files++;
|
|
61
|
+
} catch {
|
|
62
|
+
/* best-effort; keep DB migration moving even if one old rollout is malformed */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const update = db.transaction(() => {
|
|
67
|
+
db.query(`
|
|
68
|
+
UPDATE threads
|
|
69
|
+
SET has_user_event = 1
|
|
70
|
+
WHERE source IN (${placeholders})
|
|
71
|
+
AND trim(coalesce(first_user_message, '')) != ''
|
|
72
|
+
`).run(...RESUMABLE_SOURCES);
|
|
73
|
+
db.query(`
|
|
74
|
+
UPDATE threads
|
|
75
|
+
SET model_provider = ?
|
|
76
|
+
WHERE model_provider = ?
|
|
77
|
+
AND source IN (${placeholders})
|
|
78
|
+
`).run(provider, from, ...RESUMABLE_SOURCES);
|
|
79
|
+
});
|
|
80
|
+
update();
|
|
81
|
+
|
|
82
|
+
return { rows: rows.length, files };
|
|
83
|
+
} finally {
|
|
84
|
+
db.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/codex-inject.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { atomicWriteFile, websocketsEnabled } from "./config";
|
|
3
3
|
import { restoreCodexCatalog } from "./codex-catalog";
|
|
4
|
+
import { syncCodexHistoryProvider } from "./codex-history-provider";
|
|
4
5
|
import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH, DEFAULT_CATALOG_PATH, parseTomlString, readRootTomlString, resolveCodexConfigPath, tomlString } from "./codex-paths";
|
|
5
6
|
import type { OcxConfig } from "./types";
|
|
6
7
|
|
|
@@ -228,14 +229,19 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
228
229
|
|
|
229
230
|
writeFileSync(CODEX_CONFIG_PATH, content, "utf-8");
|
|
230
231
|
writeFileSync(CODEX_PROFILE_PATH, buildProfileFile(port, catalogPath), "utf-8");
|
|
232
|
+
const history = syncCodexHistoryProvider("opencodex");
|
|
231
233
|
|
|
232
234
|
const catalogMessage = catalogPath
|
|
233
235
|
? ` Codex model catalog: ${catalogPath}\n`
|
|
234
236
|
: ` Codex model catalog not injected because no opencodex catalog file exists yet.\n`;
|
|
237
|
+
const historyMessage = history.rows > 0
|
|
238
|
+
? ` Codex resume history: ${history.rows} thread(s) mapped to opencodex.\n`
|
|
239
|
+
: "";
|
|
235
240
|
return {
|
|
236
241
|
success: true,
|
|
237
242
|
message: `Injected opencodex as default provider into Codex config.\n` +
|
|
238
243
|
catalogMessage +
|
|
244
|
+
historyMessage +
|
|
239
245
|
` All models now route through opencodex proxy (like OpenRouter).\n` +
|
|
240
246
|
` OpenAI models (gpt-5.5, etc.) are passed through to OpenAI.\n` +
|
|
241
247
|
` Custom models route to their configured providers.\n` +
|
|
@@ -311,10 +317,12 @@ export function removeCodexConfig(): { success: boolean; message: string } {
|
|
|
311
317
|
export function restoreNativeCodex(): { success: boolean; message: string } {
|
|
312
318
|
const cfg = removeCodexConfig();
|
|
313
319
|
const cat = restoreCodexCatalog();
|
|
320
|
+
const history = syncCodexHistoryProvider("openai");
|
|
314
321
|
const msg = cat.removed > 0
|
|
315
322
|
? `${cfg.message} Catalog restored to ${cat.kept} native model(s) (dropped ${cat.removed} proxy-routed).`
|
|
316
323
|
: cfg.message;
|
|
317
|
-
|
|
324
|
+
const historyMsg = history.rows > 0 ? ` Resume history restored to openai (${history.rows} thread(s)).` : "";
|
|
325
|
+
return { success: cfg.success, message: `${msg}${historyMsg}` };
|
|
318
326
|
}
|
|
319
327
|
|
|
320
328
|
export function getCodexConfigPath(): string {
|
package/src/codex-shim.ts
CHANGED
|
@@ -4,6 +4,30 @@ import { getConfigDir } from "./config";
|
|
|
4
4
|
|
|
5
5
|
const SHIM_MARKER = "opencodex codex autostart shim";
|
|
6
6
|
const STATE_PATH = join(getConfigDir(), "codex-shim.json");
|
|
7
|
+
const CODEX_INTERNAL_COMMANDS = [
|
|
8
|
+
"app-server",
|
|
9
|
+
"archive",
|
|
10
|
+
"apply",
|
|
11
|
+
"cloud",
|
|
12
|
+
"completion",
|
|
13
|
+
"debug",
|
|
14
|
+
"delete",
|
|
15
|
+
"doctor",
|
|
16
|
+
"exec",
|
|
17
|
+
"exec-server",
|
|
18
|
+
"features",
|
|
19
|
+
"fork",
|
|
20
|
+
"help",
|
|
21
|
+
"login",
|
|
22
|
+
"logout",
|
|
23
|
+
"mcp",
|
|
24
|
+
"plugin",
|
|
25
|
+
"resume",
|
|
26
|
+
"review",
|
|
27
|
+
"sandbox",
|
|
28
|
+
"unarchive",
|
|
29
|
+
"update",
|
|
30
|
+
];
|
|
7
31
|
|
|
8
32
|
interface ShimState {
|
|
9
33
|
platform: NodeJS.Platform;
|
|
@@ -51,39 +75,33 @@ function backupPathFor(path: string): string {
|
|
|
51
75
|
}
|
|
52
76
|
|
|
53
77
|
export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
78
|
+
const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
|
|
54
79
|
return `#!/usr/bin/env sh
|
|
55
80
|
# ${SHIM_MARKER}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
sleep 0.1
|
|
66
|
-
i=$((i + 1))
|
|
67
|
-
done
|
|
68
|
-
fi
|
|
69
|
-
fi
|
|
81
|
+
case "$1" in
|
|
82
|
+
${internalCommands}|--help|-h|--version|-V)
|
|
83
|
+
;;
|
|
84
|
+
*)
|
|
85
|
+
if [ -z "$OCX_SHIM_BYPASS" ]; then
|
|
86
|
+
"${bunPath}" "${cliPath}" ensure >/dev/null 2>&1 || true
|
|
87
|
+
fi
|
|
88
|
+
;;
|
|
89
|
+
esac
|
|
70
90
|
exec "${realCodexPath}" "$@"
|
|
71
91
|
`;
|
|
72
92
|
}
|
|
73
93
|
|
|
74
94
|
export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
95
|
+
const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
|
|
75
96
|
return `@echo off\r
|
|
76
97
|
rem ${SHIM_MARKER}\r
|
|
77
98
|
if not "%OCX_SHIM_BYPASS%"=="" goto run_codex\r
|
|
78
|
-
|
|
79
|
-
if
|
|
80
|
-
if
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if not errorlevel 1 goto run_codex\r
|
|
85
|
-
powershell -NoProfile -Command "Start-Sleep -Milliseconds 100" >nul 2>nul\r
|
|
86
|
-
)\r
|
|
99
|
+
${internalCommandChecks}\r
|
|
100
|
+
if /I "%~1"=="--help" goto run_codex\r
|
|
101
|
+
if /I "%~1"=="-h" goto run_codex\r
|
|
102
|
+
if /I "%~1"=="--version" goto run_codex\r
|
|
103
|
+
if /I "%~1"=="-V" goto run_codex\r
|
|
104
|
+
"${bunPath}" "${cliPath}" ensure >nul 2>nul\r
|
|
87
105
|
:run_codex\r
|
|
88
106
|
"${realCodexPath}" %*\r
|
|
89
107
|
`;
|
|
@@ -102,11 +120,39 @@ function writeState(state: ShimState): void {
|
|
|
102
120
|
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
103
121
|
}
|
|
104
122
|
|
|
123
|
+
function writeShim(wrapperPath: string, realCodexPath: string): void {
|
|
124
|
+
const { bun, cli } = cliEntry();
|
|
125
|
+
if (process.platform === "win32") {
|
|
126
|
+
writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli), "utf8");
|
|
127
|
+
} else {
|
|
128
|
+
writeFileSync(wrapperPath, buildUnixCodexShim(realCodexPath, bun, cli), "utf8");
|
|
129
|
+
chmodSync(wrapperPath, 0o755);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
105
133
|
export function installCodexShim(): { installed: boolean; message: string } {
|
|
106
134
|
const existing = readState();
|
|
107
|
-
if (existing && existsSync(existing.wrapperPath) && existsSync(existing.backupPath)) {
|
|
135
|
+
if (existing && existsSync(existing.wrapperPath) && existsSync(existing.backupPath) && isShim(existing.wrapperPath)) {
|
|
136
|
+
if (process.platform === "win32" && existing.originalPath && existsSync(existing.originalPath)) {
|
|
137
|
+
renameSync(existing.originalPath, existing.backupPath);
|
|
138
|
+
writeShim(existing.wrapperPath, existing.backupPath);
|
|
139
|
+
writeState({ ...existing, platform: process.platform });
|
|
140
|
+
return {
|
|
141
|
+
installed: true,
|
|
142
|
+
message: `Codex update detected. Backed up new binary and refreshed shim at ${existing.wrapperPath}.`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
108
145
|
return { installed: false, message: `Codex autostart shim already installed at ${existing.wrapperPath}.` };
|
|
109
146
|
}
|
|
147
|
+
if (existing && existsSync(existing.backupPath) && (!existsSync(existing.wrapperPath) || !isShim(existing.wrapperPath))) {
|
|
148
|
+
if (existsSync(existing.wrapperPath)) unlinkSync(existing.wrapperPath);
|
|
149
|
+
writeShim(existing.wrapperPath, existing.backupPath);
|
|
150
|
+
writeState({ ...existing, platform: process.platform });
|
|
151
|
+
return {
|
|
152
|
+
installed: true,
|
|
153
|
+
message: `Codex autostart shim repaired at ${existing.wrapperPath}. Original remains at ${existing.backupPath}.`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
110
156
|
|
|
111
157
|
const originalPath = findCodexOnPath();
|
|
112
158
|
if (!originalPath) return { installed: false, message: "Could not find a codex executable on PATH." };
|
|
@@ -114,15 +160,9 @@ export function installCodexShim(): { installed: boolean; message: string } {
|
|
|
114
160
|
const backupPath = backupPathFor(originalPath);
|
|
115
161
|
if (existsSync(backupPath)) return { installed: false, message: `Refusing to overwrite existing backup: ${backupPath}` };
|
|
116
162
|
|
|
117
|
-
const { bun, cli } = cliEntry();
|
|
118
163
|
const wrapperPath = process.platform === "win32" ? join(dirname(originalPath), "codex.cmd") : originalPath;
|
|
119
164
|
renameSync(originalPath, backupPath);
|
|
120
|
-
|
|
121
|
-
writeFileSync(wrapperPath, buildWindowsCodexShim(backupPath, bun, cli), "utf8");
|
|
122
|
-
} else {
|
|
123
|
-
writeFileSync(wrapperPath, buildUnixCodexShim(backupPath, bun, cli), "utf8");
|
|
124
|
-
chmodSync(wrapperPath, 0o755);
|
|
125
|
-
}
|
|
165
|
+
writeShim(wrapperPath, backupPath);
|
|
126
166
|
writeState({ platform: process.platform, wrapperPath, originalPath, backupPath });
|
|
127
167
|
return { installed: true, message: `Codex autostart shim installed at ${wrapperPath}. Original saved at ${backupPath}.` };
|
|
128
168
|
}
|
|
@@ -139,7 +179,11 @@ export function uninstallCodexShim(): { removed: boolean; message: string } {
|
|
|
139
179
|
export function codexShimStatus(): string {
|
|
140
180
|
const state = readState();
|
|
141
181
|
if (!state) return "Codex autostart shim is not installed.";
|
|
142
|
-
const wrapper = existsSync(state.wrapperPath)
|
|
182
|
+
const wrapper = existsSync(state.wrapperPath)
|
|
183
|
+
? isShim(state.wrapperPath)
|
|
184
|
+
? "shim present"
|
|
185
|
+
: "present but not an opencodex shim"
|
|
186
|
+
: "missing";
|
|
143
187
|
const backup = existsSync(state.backupPath) ? "present" : "missing";
|
|
144
188
|
return `Codex autostart shim: wrapper ${wrapper} at ${state.wrapperPath}; original backup ${backup} at ${state.backupPath}.`;
|
|
145
189
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, chmodSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { OcxConfig } from "./types";
|
|
@@ -10,7 +10,7 @@ let _atomicSeq = 0;
|
|
|
10
10
|
*/
|
|
11
11
|
export function atomicWriteFile(path: string, content: string): void {
|
|
12
12
|
const tmp = `${path}.ocx.${process.pid}.${++_atomicSeq}.tmp`;
|
|
13
|
-
writeFileSync(tmp, content, "utf-8");
|
|
13
|
+
writeFileSync(tmp, content, { encoding: "utf-8", mode: 0o600 });
|
|
14
14
|
renameSync(tmp, path);
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -39,7 +39,22 @@ export function getPidPath(): string {
|
|
|
39
39
|
return PID_PATH;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export function hardenConfigDir(): void {
|
|
43
|
+
if (existsSync(OCX_DIR)) {
|
|
44
|
+
try { chmodSync(OCX_DIR, 0o700); } catch { /* best-effort */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function hardenExistingSecret(path: string): void {
|
|
49
|
+
if (existsSync(path)) {
|
|
50
|
+
try { chmodSync(path, 0o600); } catch { /* best-effort */ }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
export function loadConfig(): OcxConfig {
|
|
55
|
+
hardenConfigDir();
|
|
56
|
+
hardenExistingSecret(CONFIG_PATH);
|
|
57
|
+
hardenExistingSecret(join(OCX_DIR, "auth.json"));
|
|
43
58
|
if (!existsSync(CONFIG_PATH)) {
|
|
44
59
|
return getDefaultConfig();
|
|
45
60
|
}
|
|
@@ -53,15 +68,21 @@ export function loadConfig(): OcxConfig {
|
|
|
53
68
|
|
|
54
69
|
export function saveConfig(config: OcxConfig): void {
|
|
55
70
|
if (!existsSync(OCX_DIR)) {
|
|
56
|
-
mkdirSync(OCX_DIR, { recursive: true });
|
|
71
|
+
mkdirSync(OCX_DIR, { recursive: true, mode: 0o700 });
|
|
72
|
+
} else {
|
|
73
|
+
try { chmodSync(OCX_DIR, 0o700); } catch { /* best-effort on existing dir */ }
|
|
57
74
|
}
|
|
58
|
-
|
|
75
|
+
atomicWriteFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
59
76
|
}
|
|
60
77
|
|
|
61
78
|
export function websocketsEnabled(config: Pick<OcxConfig, "websockets">): boolean {
|
|
62
79
|
return config.websockets === true;
|
|
63
80
|
}
|
|
64
81
|
|
|
82
|
+
export function codexAutoStartEnabled(config: Pick<OcxConfig, "codexAutoStart">): boolean {
|
|
83
|
+
return config.codexAutoStart !== false;
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
export function getDefaultConfig(): OcxConfig {
|
|
66
87
|
// Fresh-install default: works out of the box with Codex's ChatGPT OAuth (no API key).
|
|
67
88
|
// gpt-* requests forward the caller's incoming OAuth headers to the ChatGPT backend.
|
|
@@ -78,6 +99,7 @@ export function getDefaultConfig(): OcxConfig {
|
|
|
78
99
|
defaultProvider: "openai",
|
|
79
100
|
subagentModels: [...DEFAULT_SUBAGENT_MODELS],
|
|
80
101
|
websockets: false,
|
|
102
|
+
codexAutoStart: true,
|
|
81
103
|
};
|
|
82
104
|
}
|
|
83
105
|
|
|
@@ -90,7 +112,11 @@ export function resolveEnvValue(value: string | undefined): string | undefined {
|
|
|
90
112
|
}
|
|
91
113
|
|
|
92
114
|
export function writePid(pid: number): void {
|
|
93
|
-
if (!existsSync(OCX_DIR))
|
|
115
|
+
if (!existsSync(OCX_DIR)) {
|
|
116
|
+
mkdirSync(OCX_DIR, { recursive: true, mode: 0o700 });
|
|
117
|
+
} else {
|
|
118
|
+
hardenConfigDir();
|
|
119
|
+
}
|
|
94
120
|
writeFileSync(PID_PATH, String(pid), "utf-8");
|
|
95
121
|
}
|
|
96
122
|
|
package/src/init.ts
CHANGED
|
@@ -131,6 +131,17 @@ export async function runInit(): Promise<void> {
|
|
|
131
131
|
console.log(result.success ? `✅ ${result.message}` : `⚠️ ${result.message}`);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
const shimAnswer = await prompt.ask("Install Codex autostart shim? [Y/n]: ");
|
|
135
|
+
if (shimAnswer.trim().toLowerCase() !== "n") {
|
|
136
|
+
try {
|
|
137
|
+
const { installCodexShim } = await import("./codex-shim");
|
|
138
|
+
const result = installCodexShim();
|
|
139
|
+
console.log(result.installed ? `✅ ${result.message}` : `⚠️ ${result.message}`);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.log(`⚠️ Codex autostart shim skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
134
145
|
console.log(`\n🚀 Setup complete! Run 'ocx start' to start the proxy.`);
|
|
135
146
|
prompt.close();
|
|
136
147
|
}
|
package/src/oauth/store.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/** OAuth token store at ~/.opencodex/auth.json, keyed by provider name. */
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, chmodSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { getConfigDir } from "../config";
|
|
4
|
+
import { getConfigDir, atomicWriteFile, hardenConfigDir, hardenExistingSecret } from "../config";
|
|
5
5
|
import type { OAuthCredentials } from "./types";
|
|
6
6
|
|
|
7
7
|
const AUTH_PATH = join(getConfigDir(), "auth.json");
|
|
8
8
|
type AuthStore = Record<string, OAuthCredentials>;
|
|
9
9
|
|
|
10
10
|
export function loadAuthStore(): AuthStore {
|
|
11
|
+
hardenConfigDir();
|
|
12
|
+
hardenExistingSecret(AUTH_PATH);
|
|
11
13
|
if (!existsSync(AUTH_PATH)) return {};
|
|
12
14
|
try {
|
|
13
15
|
return JSON.parse(readFileSync(AUTH_PATH, "utf-8")) as AuthStore;
|
|
@@ -18,8 +20,12 @@ export function loadAuthStore(): AuthStore {
|
|
|
18
20
|
|
|
19
21
|
function persist(store: AuthStore): void {
|
|
20
22
|
const dir = getConfigDir();
|
|
21
|
-
if (!existsSync(dir))
|
|
22
|
-
|
|
23
|
+
if (!existsSync(dir)) {
|
|
24
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
25
|
+
} else {
|
|
26
|
+
try { chmodSync(dir, 0o700); } catch { /* best-effort on existing dir */ }
|
|
27
|
+
}
|
|
28
|
+
atomicWriteFile(AUTH_PATH, JSON.stringify(store, null, 2) + "\n");
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
export function getCredential(provider: string): OAuthCredentials | null {
|