Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ ocx start [--port 10100] # start the proxy
ocx stop # stop + restore native Codex
ocx restore # restore without stopping (alias: ocx eject)
ocx sync # refresh models + re-inject into Codex
ocx codex-shim install # auto-start the proxy whenever `codex` is launched
ocx status # is the proxy running?
ocx login <xai|anthropic|kimi> # OAuth login
ocx logout <provider> # remove a stored login
Expand Down
24 changes: 24 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Usage:
ocx stop Stop the proxy AND restore native Codex (plain codex works again)
ocx restore Restore native Codex without stopping (alias: eject)
ocx service <sub> Run as a background service (install|start|stop|status|uninstall)
ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall)
ocx sync Fetch models from providers and inject into Codex config
ocx status Check proxy server status
ocx login <provider> OAuth login (xai) — opens browser, stores token in ~/.opencodex/auth.json
Expand Down Expand Up @@ -215,6 +216,29 @@ switch (command) {
case "service":
serviceCommand(args[1]);
break;
case "codex-shim": {
const { codexShimStatus, installCodexShim, uninstallCodexShim } = await import("./codex-shim");
switch (args[1]) {
case "install": {
const r = installCodexShim();
console.log(r.installed ? `✅ ${r.message}` : `⚠️ ${r.message}`);
break;
}
case "status":
console.log(codexShimStatus());
break;
case "uninstall":
case "remove": {
const r = uninstallCodexShim();
console.log(r.removed ? `✅ ${r.message}` : `⚠️ ${r.message}`);
break;
}
default:
console.error("Usage: ocx codex-shim <install|status|uninstall>");
process.exit(1);
}
break;
}
case "update": {
const { runUpdate } = await import("./update");
runUpdate();
Expand Down
13 changes: 8 additions & 5 deletions src/codex-catalog.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { atomicWriteFile, websocketsEnabled } from "./config";
Expand Down Expand Up @@ -560,12 +560,15 @@ export function restoreCodexCatalog(): { removed: number; kept: number; path: st
}

/**
* Delete Codex's models cache ($CODEX_HOME/models_cache.json) so the next turn re-fetches /v1/models.
* Codex caches the model list for 5 min (DEFAULT_MODEL_CACHE_TTL); invalidating makes catalog edits
* (enable/disable, subagent reorder) apply on the next turn instead of waiting for the TTL.
* Refresh Codex's models cache ($CODEX_HOME/models_cache.json) from the active catalog.
* Codex caches the model list for 5 min (DEFAULT_MODEL_CACHE_TTL); copying the injected catalog
* makes catalog edits (enable/disable, subagent reorder) apply on the next turn instead of waiting.
*/
export function invalidateCodexModelsCache(): void {
try {
if (existsSync(CODEX_MODELS_CACHE_PATH)) unlinkSync(CODEX_MODELS_CACHE_PATH);
const catalogPath = readCodexCatalogPath();
if (!existsSync(catalogPath)) return;
const catalog = readFileSync(catalogPath, "utf8");
atomicWriteFile(CODEX_MODELS_CACHE_PATH, catalog.endsWith("\n") ? catalog : `${catalog}\n`);
} catch { /* best-effort */ }
}
145 changes: 145 additions & 0 deletions src/codex-shim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { delimiter, dirname, extname, join } from "node:path";
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
import { getConfigDir } from "./config";

const SHIM_MARKER = "opencodex codex autostart shim";
const STATE_PATH = join(getConfigDir(), "codex-shim.json");

interface ShimState {
platform: NodeJS.Platform;
wrapperPath: string;
originalPath: string;
backupPath: string;
}

function cliEntry(): { bun: string; cli: string } {
return { bun: process.execPath, cli: join(import.meta.dir, "cli.ts") };
}

function commandNames(name: string): string[] {
if (process.platform !== "win32") return [name];
const exts = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD;.PS1").split(";").filter(Boolean);
return [name, ...exts.flatMap(ext => [`${name}${ext.toLowerCase()}`, `${name}${ext.toUpperCase()}`])];
}

function isShim(path: string): boolean {
try {
return readFileSync(path, "utf8").includes(SHIM_MARKER);
} catch {
return false;
}
}

function findCodexOnPath(): string | null {
for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
for (const name of commandNames("codex")) {
const path = join(dir, name);
if (!existsSync(path) || isShim(path)) continue;
try {
if (!lstatSync(path).isDirectory()) return path;
} catch {
continue;
}
}
}
return null;
}

function backupPathFor(path: string): string {
const ext = extname(path);
return ext ? `${path.slice(0, -ext.length)}.opencodex-real${ext}` : `${path}.opencodex-real`;
}

export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
return `#!/usr/bin/env sh
# ${SHIM_MARKER}
if [ -z "$OCX_SHIM_BYPASS" ]; then
if ! "${bunPath}" "${cliPath}" status 2>/dev/null | grep -q "Proxy running"; then
mkdir -p "$HOME/.opencodex"
nohup env OCX_SERVICE=1 "${bunPath}" "${cliPath}" start >> "$HOME/.opencodex/shim.log" 2>&1 &
i=0
while [ "$i" -lt 50 ]; do
if "${bunPath}" "${cliPath}" status 2>/dev/null | grep -q "Proxy running"; then
break
fi
sleep 0.1
i=$((i + 1))
done
fi
fi
exec "${realCodexPath}" "$@"
`;
}

export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
return `@echo off\r
rem ${SHIM_MARKER}\r
if not "%OCX_SHIM_BYPASS%"=="" goto run_codex\r
"${bunPath}" "${cliPath}" status 2>nul | findstr /C:"Proxy running" >nul\r
if %ERRORLEVEL% EQU 0 goto run_codex\r
if not exist "%USERPROFILE%\\.opencodex" mkdir "%USERPROFILE%\\.opencodex"\r
start "" /b cmd /c "set OCX_SERVICE=1 && ""${bunPath}"" ""${cliPath}"" start >> ""%USERPROFILE%\\.opencodex\\shim.log"" 2>&1"\r
for /l %%i in (1,1,50) do (\r
"${bunPath}" "${cliPath}" status 2>nul | findstr /C:"Proxy running" >nul\r
if not errorlevel 1 goto run_codex\r
powershell -NoProfile -Command "Start-Sleep -Milliseconds 100" >nul 2>nul\r
)\r
:run_codex\r
"${realCodexPath}" %*\r
`;
}

function readState(): ShimState | null {
try {
return JSON.parse(readFileSync(STATE_PATH, "utf8")) as ShimState;
} catch {
return null;
}
}

function writeState(state: ShimState): void {
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2) + "\n", "utf8");
}

export function installCodexShim(): { installed: boolean; message: string } {
const existing = readState();
if (existing && existsSync(existing.wrapperPath) && existsSync(existing.backupPath)) {
return { installed: false, message: `Codex autostart shim already installed at ${existing.wrapperPath}.` };
}

const originalPath = findCodexOnPath();
if (!originalPath) return { installed: false, message: "Could not find a codex executable on PATH." };

const backupPath = backupPathFor(originalPath);
if (existsSync(backupPath)) return { installed: false, message: `Refusing to overwrite existing backup: ${backupPath}` };

const { bun, cli } = cliEntry();
const wrapperPath = process.platform === "win32" ? join(dirname(originalPath), "codex.cmd") : originalPath;
renameSync(originalPath, backupPath);
if (process.platform === "win32") {
writeFileSync(wrapperPath, buildWindowsCodexShim(backupPath, bun, cli), "utf8");
} else {
writeFileSync(wrapperPath, buildUnixCodexShim(backupPath, bun, cli), "utf8");
chmodSync(wrapperPath, 0o755);
}
writeState({ platform: process.platform, wrapperPath, originalPath, backupPath });
return { installed: true, message: `Codex autostart shim installed at ${wrapperPath}. Original saved at ${backupPath}.` };
}

export function uninstallCodexShim(): { removed: boolean; message: string } {
const state = readState();
if (!state) return { removed: false, message: "Codex autostart shim is not installed." };
if (existsSync(state.wrapperPath) && isShim(state.wrapperPath)) unlinkSync(state.wrapperPath);
if (existsSync(state.backupPath) && !existsSync(state.originalPath)) renameSync(state.backupPath, state.originalPath);
if (existsSync(STATE_PATH)) unlinkSync(STATE_PATH);
return { removed: true, message: `Codex autostart shim removed. Restored ${state.originalPath}.` };
}

export function codexShimStatus(): string {
const state = readState();
if (!state) return "Codex autostart shim is not installed.";
const wrapper = existsSync(state.wrapperPath) ? "present" : "missing";
const backup = existsSync(state.backupPath) ? "present" : "missing";
return `Codex autostart shim: wrapper ${wrapper} at ${state.wrapperPath}; original backup ${backup} at ${state.backupPath}.`;
}
1 change: 1 addition & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ export function startServer(port?: number) {

const server = Bun.serve<WsData>({
port: listenPort,
idleTimeout: 255,
async fetch(req) {
const url = new URL(req.url);

Expand Down
23 changes: 23 additions & 0 deletions tests/codex-shim.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, test } from "bun:test";
import { buildUnixCodexShim, buildWindowsCodexShim } from "../src/codex-shim";

describe("Codex autostart shim", () => {
test("builds a Unix shim that starts ocx before execing Codex", () => {
const script = buildUnixCodexShim("/usr/local/bin/codex-real", "/usr/local/bin/bun", "/opt/opencodex/src/cli.ts");

expect(script).toContain("opencodex codex autostart shim");
expect(script).toContain("status");
expect(script).toContain("OCX_SERVICE=1");
expect(script).toContain("start");
expect(script).toContain('exec "/usr/local/bin/codex-real" "$@"');
});

test("builds a Windows shim that starts ocx before running Codex", () => {
const script = buildWindowsCodexShim("C:\\Tools\\codex-real.exe", "C:\\Bun\\bun.exe", "C:\\ocx\\cli.ts");

expect(script).toContain("opencodex codex autostart shim");
expect(script).toContain("findstr");
expect(script).toContain("OCX_SERVICE=1");
expect(script).toContain('"C:\\Tools\\codex-real.exe" %*');
});
});
Loading