Something went wrong. Try again.
presents omp as codex for use with chatgpt remote
Something went wrong. Try again.
9.7 kB · 202 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203import fs from "node:fs/promises";import os from "node:os";import path from "node:path";import { fileURLToPath } from "node:url";import { spawnSync } from "node:child_process";import { CodexAppServer } from "./app-server.mjs";import { daemonStatus, runtimePaths, startDaemon, stopDaemon } from "./daemon.mjs";import { loadOmpSdk } from "./backend/omp.mjs";import { loadBridgeConfig } from "./config.mjs";import { proxyStdioToUnix } from "./transports/proxy.mjs";import { serveStdio } from "./transports/stdio.mjs";import { serveTcpWebSocket, serveUnixWebSocket } from "./transports/websocket.mjs";import { BRIDGE_VERSION, PROTOCOL_COMPAT_VERSION, log, parseArgv, parseListenUri } from "./util.mjs";
export async function main(argv) { const { positional, options } = parseArgv(argv); if (argv.includes("--version") || argv.includes("-V") || positional[0] === "version") { process.stdout.write(`codex-cli ${PROTOCOL_COMPAT_VERSION}\n`); return; } if (argv.includes("--help") || argv.includes("-h") || !positional.length) { printHelp(); return; }
switch (positional[0]) { case "app-server": return runAppServer(positional.slice(1), options); case "remote-control": return runRemoteControl(positional.slice(1), options); case "doctor": return doctor(options); case "logs": case "log": return showLogs(options); case "bridge-version": process.stdout.write(`omp-codex-bridge ${BRIDGE_VERSION}\nCodex protocol source ${PROTOCOL_COMPAT_VERSION}\n`); return; default: throw new Error(`Unsupported codex command: ${positional.join(" ")}`); }}
async function runAppServer(positional, options) { if (positional[0] === "proxy") { const socket = options.get("socket") || options.get("listen") || process.env.OMP_CODEX_SOCKET || runtimePaths().socket; const parsed = parseListenUri(String(socket)); if (parsed.kind !== "unix") throw new Error("app-server proxy currently requires a Unix socket"); return proxyStdioToUnix(parsed.path); }
const listenValue = options.get("listen") || options.get("address") || "stdio://"; const listen = parseListenUri(String(listenValue));
if (listen.kind === "stdio" && !wantsOwnServer(options)) { const socket = await sharedDaemonSocket(); if (socket) { log("attaching stdio client to the shared daemon at", socket); return proxyStdioToUnix(socket); } log("shared daemon unavailable; serving this stdio client in-process"); }
const server = new CodexAppServer({ backend: options.get("backend") || process.env.OMP_CODEX_BACKEND, stateFile: options.get("state-file") || process.env.OMP_CODEX_STATE_FILE, configFile: options.get("config") || process.env.OMP_CODEX_CONFIG, cwd: options.get("cwd") || process.cwd(), });
if (listen.kind === "stdio") { try { await serveStdio(server); } finally { await server.close(); } return; }
const listener = listen.kind === "unix" ? await serveUnixWebSocket(server, listen.path) : await serveTcpWebSocket(server, { host: listen.host, port: listen.port }); const address = listen.kind === "unix" ? `unix://${listen.path}` : `ws://${listen.host}:${listener.address().port}`; log("app-server listening on", address); const shutdown = async (signal) => { process.off("SIGINT", onSigint); process.off("SIGTERM", onSigterm); await listener.cleanup?.().catch(() => {}); await server.close().catch(() => {}); if (signal) process.exitCode = 0; }; const onSigint = () => shutdown("SIGINT"); const onSigterm = () => shutdown("SIGTERM"); process.on("SIGINT", onSigint); process.on("SIGTERM", onSigterm); await new Promise((resolve, reject) => { listener.once("close", resolve); listener.once("error", reject); });}
/** * Every client has to end up inside the same server process. * * ChatGPT Desktop spawns `codex app-server` on stdio while iOS reaches the * daemon over its Unix socket, and two in-process servers sharing one state * file take turns overwriting the file with their own stale snapshot: whole * threads - the fork ChatGPT just made included - disappear on the next * unrelated write, and each client reads a different history for the same * conversation. So a stdio client is a thin proxy into the daemon. */function wantsOwnServer(options) { if (options.get("standalone") || process.env.OMP_CODEX_STANDALONE === "1") return true; // A daemon we start inherits this environment, and one already running was // started from the same shell, so only per-invocation flags a running daemon // could not know about force a private server. return ["backend", "state-file", "config", "cwd"].some((name) => options.has(name));}
/** The daemon's socket, started on demand, or null when it cannot be reached. */async function sharedDaemonSocket() { const paths = runtimePaths(); try { const existing = await daemonStatus(paths); if (existing.reachable) return paths.socket; const executable = fileURLToPath(new URL("../bin/codex.mjs", import.meta.url)); const started = await startDaemon({ executable }); return started?.reachable ? paths.socket : null; } catch (error) { log("could not reach the shared daemon:", error?.message || error); return null; }}
async function runRemoteControl(positional, options) { const action = positional[0] || "status"; const executable = fileURLToPath(new URL("../bin/codex.mjs", import.meta.url)); if (action === "start") { const status = await startDaemon({ executable, backend: options.get("backend") }); process.stdout.write(`${JSON.stringify(status)}\n`); } else if (action === "stop") { process.stdout.write(`${JSON.stringify(await stopDaemon())}\n`); } else if (action === "status") { const status = await daemonStatus(); process.stdout.write(`${JSON.stringify(status)}\n`); if (!status.running) process.exitCode = 1; } else { throw new Error(`Unknown remote-control action: ${action}`); }}
async function doctor(options) { const configFile = typeof options.get("config") === "string" ? options.get("config") : undefined; const config = await loadBridgeConfig(configFile).catch(() => null); const backendSelection = options.get("backend") || process.env.OMP_CODEX_BACKEND || config?.backend || "auto"; const sdk = await loadOmpSdk({ module: options.get("omp-module") }); const ompBin = options.get("omp-bin") || process.env.OMP_CODEX_OMP_BIN || "omp"; const primeBin = options.get("prime-bin") || process.env.OMP_CODEX_PRIME_BIN || "prime-agent"; const which = process.platform === "win32" ? "where" : "which"; const found = spawnSync(which, [ompBin], { encoding: "utf8" }); const primeFound = spawnSync(which, [primeBin], { encoding: "utf8" }); const resolved = (result) => (result.status === 0 ? result.stdout.trim().split(/\r?\n/)[0] : null); const paths = runtimePaths(); const stateDir = path.dirname(process.env.OMP_CODEX_STATE_FILE || path.join(os.homedir(), ".omp", "codex-bridge", "state.json")); const report = { bridgeVersion: BRIDGE_VERSION, codexProtocolSourceVersion: PROTOCOL_COMPAT_VERSION, node: process.version, platform: `${process.platform}/${process.arch}`, ompSdk: sdk ? { found: true, specifier: sdk.specifier } : { found: false }, ompExecutable: resolved(found), primeExecutable: resolved(primeFound), backendSelection, stateDir, runtime: paths, }; process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); if (backendSelection === "prime") { if (!report.primeExecutable) process.exitCode = 2; } else if (backendSelection === "both") { if (!report.primeExecutable || (!sdk && found.status !== 0)) process.exitCode = 2; } else if (!sdk && found.status !== 0) { process.exitCode = 2; }}async function showLogs(options) { const file = process.env.OMP_CODEX_LOG_FILE || path.join(os.homedir(), ".omp", "codex-bridge", "bridge.log"); const count = Number(options.get("n") || options.get("lines") || 50); try { const content = await fs.readFile(file, "utf8"); const all = content.trim().split("\n"); const slice = all.slice(-count); process.stdout.write(`${slice.join("\n")}\n`); } catch (error) { if (error?.code === "ENOENT") { process.stdout.write(`No log file found at ${file}\n`); } else { throw error; } }}
function printHelp() { process.stdout.write(`Oh My Pi Codex app-server bridge\n\nUsage:\n codex --version\n codex app-server [--listen stdio://] [--backend auto|sdk|rpc|prime|both|mock]\n codex app-server --listen unix:///path/to/app-server.sock\n codex app-server proxy [--socket /path/to/app-server.sock]\n codex remote-control start|stop|status [--backend auto|sdk|rpc|prime|both|mock]\n codex doctor [--backend <mode>] [--omp-bin omp] [--prime-bin prime-agent]\n codex logs [-n 50]\n\nEnvironment:\n OMP_CODEX_BACKEND=auto|sdk|rpc|prime|both|mock\n OMP_CODEX_OMP_MODULE=<module-or-file-url>\n OMP_CODEX_OMP_BIN=omp\n OMP_CODEX_PRIME_BIN=prime-agent\n OMP_CODEX_PRIME_RPC_ARGS=<json-array-of-extra-flags>\n OMP_CODEX_STATE_FILE=<path>\n OMP_CODEX_SOCKET=<path>\n OMP_CODEX_LOG_FILE=<path>\n\nBackends:\n auto OMP SDK if importable, otherwise the omp CLI in RPC mode\n sdk OMP coding-agent module in-process\n rpc omp --mode rpc-ui subprocess\n prime prime-agent subprocess (Prime 0.8.0+); publishes its own model catalog,\n and runs commands and file edits with no approval prompts and no sandbox\n both OMP and Prime together; choose rows labelled (OMP) or (Prime Agent)\n mock in-memory backend for tests and offline development\n`);}