@bitkyc08/opencodex 2.6.17 → 2.6.18
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 +9 -0
- package/bin/ocx.mjs +70 -5
- package/gui/dist/assets/index-DDcEW0Cm.css +1 -0
- package/gui/dist/assets/index-DbTEyo46.js +9 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +9 -2
- package/src/adapters/base.ts +6 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +88 -0
- package/src/adapters/cursor/cursor-errors.ts +85 -0
- package/src/adapters/cursor/discovery.ts +144 -0
- package/src/adapters/cursor/effort-map.ts +74 -0
- package/src/adapters/cursor/exec-policy.ts +44 -0
- package/src/adapters/cursor/framing.ts +136 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +25 -0
- package/src/adapters/cursor/live-models.ts +93 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +758 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +236 -0
- package/src/adapters/cursor/message-mapper.ts +46 -0
- package/src/adapters/cursor/native-exec-common.ts +55 -0
- package/src/adapters/cursor/native-exec-desktop.ts +177 -0
- package/src/adapters/cursor/native-exec-fs.ts +284 -0
- package/src/adapters/cursor/native-exec-mcp.ts +151 -0
- package/src/adapters/cursor/native-exec-network.ts +32 -0
- package/src/adapters/cursor/native-exec-shell.ts +191 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +177 -0
- package/src/adapters/cursor/protobuf-events.ts +309 -0
- package/src/adapters/cursor/protobuf-request.ts +347 -0
- package/src/adapters/cursor/request-builder.ts +98 -0
- package/src/adapters/cursor/tool-definitions.ts +301 -0
- package/src/adapters/cursor/transport-retry.ts +116 -0
- package/src/adapters/cursor/transport.ts +47 -0
- package/src/adapters/cursor/types.ts +36 -0
- package/src/adapters/cursor.ts +99 -0
- package/src/adapters/google.ts +7 -1
- package/src/adapters/kiro.ts +15 -0
- package/src/adapters/openai-chat.ts +7 -2
- package/src/adapters/run-turn-queue.ts +58 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/bridge.ts +7 -1
- package/src/cli-help.ts +9 -2
- package/src/cli-status.ts +7 -5
- package/src/cli.ts +122 -79
- package/src/codex-catalog.ts +213 -71
- package/src/codex-history-provider.ts +31 -14
- package/src/codex-inject.ts +17 -9
- package/src/codex-paths.ts +2 -1
- package/src/codex-shim.ts +30 -7
- package/src/codex-sync.ts +70 -0
- package/src/config.ts +58 -2
- package/src/doctor.ts +4 -2
- package/src/index.ts +1 -0
- package/src/model-cache.ts +22 -2
- package/src/oauth/callback-server.ts +44 -16
- package/src/oauth/cursor.ts +188 -0
- package/src/oauth/index.ts +29 -3
- package/src/oauth/key-providers.ts +20 -33
- package/src/oauth/login-cli.ts +7 -4
- package/src/open-url.ts +5 -1
- package/src/ports.ts +13 -0
- package/src/process-control.ts +76 -0
- package/src/provider-label.ts +10 -5
- package/src/providers/derive.ts +30 -3
- package/src/providers/registry.ts +39 -1
- package/src/proxy-liveness.ts +122 -0
- package/src/responses/parser.ts +1 -0
- package/src/responses/state.ts +83 -0
- package/src/router.ts +38 -23
- package/src/server/adapter-resolve.ts +3 -0
- package/src/server.ts +130 -18
- package/src/service.ts +94 -32
- package/src/types.ts +24 -1
- package/src/update-job.ts +360 -0
- package/src/update.ts +73 -11
- package/src/usage-log.ts +3 -3
- package/src/usage-summary.ts +3 -2
- package/src/win-paths.ts +68 -0
- package/gui/dist/assets/index-DIBiVVC0.css +0 -1
- package/gui/dist/assets/index-DcnD944i.js +0 -9
|
@@ -5,6 +5,7 @@ import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoice
|
|
|
5
5
|
import { mapReasoningEffort } from "../reasoning-effort";
|
|
6
6
|
import { contentPartsToText } from "./image";
|
|
7
7
|
import { neutralizeIdentity } from "./identity";
|
|
8
|
+
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
|
|
8
9
|
|
|
9
10
|
// Z.AI's "glm-5.2[1m]" 1M-context id is a Claude-Code / Anthropic-endpoint-only
|
|
10
11
|
// convention; OpenAI-compatible chat-completions endpoints reject the bracketed
|
|
@@ -20,12 +21,16 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
20
21
|
const { context, options } = parsed;
|
|
21
22
|
let pendingToolCallIds = new Set<string>();
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
const toolCatalogNudge = shouldInjectNonOpenAIToolCatalogNudge(provider)
|
|
25
|
+
? buildNonOpenAIToolCatalogNudgeForTools(context.tools, options.toolChoice)
|
|
26
|
+
: undefined;
|
|
27
|
+
const systemParts = [...(context.systemPrompt ?? []), ...(toolCatalogNudge ? [toolCatalogNudge] : [])];
|
|
28
|
+
if (systemParts.length > 0) {
|
|
24
29
|
// Codex sends its GPT-5 identity prompt for EVERY model (the per-model catalog
|
|
25
30
|
// base_instructions is ignored at request time). Neutralize that one identity line
|
|
26
31
|
// so routed, non-OpenAI models don't misreport themselves as GPT-5 / OpenAI — without
|
|
27
32
|
// leaking the proxy identity into the payload.
|
|
28
|
-
const sys = neutralizeIdentity(
|
|
33
|
+
const sys = neutralizeIdentity(systemParts.join("\n\n"));
|
|
29
34
|
out.push({ role: "system", content: sys });
|
|
30
35
|
}
|
|
31
36
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AdapterEvent } from "../types";
|
|
2
|
+
|
|
3
|
+
type QueueReader = (result: IteratorResult<AdapterEvent>) => void;
|
|
4
|
+
|
|
5
|
+
export interface AdapterEventQueue {
|
|
6
|
+
push(event: AdapterEvent): void;
|
|
7
|
+
close(): void;
|
|
8
|
+
stream(): AsyncIterable<AdapterEvent>;
|
|
9
|
+
collect(): Promise<AdapterEvent[]>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createAdapterEventQueue(): AdapterEventQueue {
|
|
13
|
+
const queued: AdapterEvent[] = [];
|
|
14
|
+
const readers: QueueReader[] = [];
|
|
15
|
+
let closed = false;
|
|
16
|
+
|
|
17
|
+
const push = (event: AdapterEvent): void => {
|
|
18
|
+
if (closed) return;
|
|
19
|
+
const reader = readers.shift();
|
|
20
|
+
if (reader) {
|
|
21
|
+
reader({ done: false, value: event });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
queued.push(event);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const close = (): void => {
|
|
28
|
+
if (closed) return;
|
|
29
|
+
closed = true;
|
|
30
|
+
while (readers.length > 0) {
|
|
31
|
+
readers.shift()?.({ done: true, value: undefined as never });
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
async function* stream(): AsyncIterable<AdapterEvent> {
|
|
36
|
+
while (true) {
|
|
37
|
+
const next = queued.shift();
|
|
38
|
+
if (next) {
|
|
39
|
+
yield next;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (closed) return;
|
|
43
|
+
const result = await new Promise<IteratorResult<AdapterEvent>>(resolve => {
|
|
44
|
+
readers.push(resolve);
|
|
45
|
+
});
|
|
46
|
+
if (result.done) return;
|
|
47
|
+
yield result.value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const collect = async (): Promise<AdapterEvent[]> => {
|
|
52
|
+
const events: AdapterEvent[] = [];
|
|
53
|
+
for await (const event of stream()) events.push(event);
|
|
54
|
+
return events;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return { push, close, stream, collect };
|
|
58
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isAllowedToolChoice,
|
|
3
|
+
namespacedToolName,
|
|
4
|
+
toolAllowedByChoice,
|
|
5
|
+
toolChoiceAliases,
|
|
6
|
+
type OcxRequestOptions,
|
|
7
|
+
type OcxTool,
|
|
8
|
+
type OcxProviderConfig,
|
|
9
|
+
} from "../types";
|
|
10
|
+
|
|
11
|
+
const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS", "apply_patch"] as const;
|
|
12
|
+
|
|
13
|
+
function quoteNames(names: readonly string[]): string {
|
|
14
|
+
return names.map(name => `\`${name}\``).join(", ");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function uniqueNames(names: readonly string[]): string[] {
|
|
18
|
+
return [...new Set(names.filter(name => name.trim().length > 0))];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toolChoiceAllows(tool: Pick<OcxTool, "namespace" | "name">, toolChoice: OcxRequestOptions["toolChoice"] | undefined): boolean {
|
|
22
|
+
if (!toolChoice || toolChoice === "auto" || toolChoice === "required") return true;
|
|
23
|
+
if (toolChoice === "none") return false;
|
|
24
|
+
if (isAllowedToolChoice(toolChoice)) return toolAllowedByChoice(tool, new Set(toolChoice.allowedTools));
|
|
25
|
+
return toolChoiceAliases(tool).includes(toolChoice.name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isOpenAIOrChatGPTHost(hostname: string): boolean {
|
|
29
|
+
return hostname === "openai.com"
|
|
30
|
+
|| hostname.endsWith(".openai.com")
|
|
31
|
+
|| hostname === "chatgpt.com"
|
|
32
|
+
|| hostname.endsWith(".chatgpt.com");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function shouldInjectNonOpenAIToolCatalogNudge(provider: Pick<OcxProviderConfig, "baseUrl">): boolean {
|
|
36
|
+
try {
|
|
37
|
+
return !isOpenAIOrChatGPTHost(new URL(provider.baseUrl).hostname);
|
|
38
|
+
} catch {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildNonOpenAIToolCatalogNudgeFromNames(wireNames: readonly string[] | undefined): string | undefined {
|
|
44
|
+
const names = uniqueNames(wireNames ?? []);
|
|
45
|
+
if (names.length === 0) return undefined;
|
|
46
|
+
|
|
47
|
+
const advertised = new Set(names);
|
|
48
|
+
const unavailableNeighborNames = NEIGHBOR_AGENT_TOOL_NAMES.filter(name => !advertised.has(name));
|
|
49
|
+
|
|
50
|
+
return [
|
|
51
|
+
"Tool contract: use the current tool catalog as ground truth.",
|
|
52
|
+
`Valid tool names for this turn are exactly ${quoteNames(names)}.`,
|
|
53
|
+
"Call only listed names with their listed argument keys; do not invent, translate, or rename tools.",
|
|
54
|
+
unavailableNeighborNames.length > 0
|
|
55
|
+
? `Do not use neighboring-agent tool names ${quoteNames(unavailableNeighborNames)} unless this turn's catalog lists those exact names.`
|
|
56
|
+
: undefined,
|
|
57
|
+
"If you need shell, file search, file read, edit, or discovery behavior, choose the listed tool that provides that capability.",
|
|
58
|
+
"Count a tool call only after its tool result returns; batch independent read-only calls when the runtime supports it.",
|
|
59
|
+
].filter((line): line is string => typeof line === "string").join(" ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildNonOpenAIToolCatalogNudgeForTools(
|
|
63
|
+
tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined,
|
|
64
|
+
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
65
|
+
toWireName: (tool: Pick<OcxTool, "namespace" | "name">) => string = tool => namespacedToolName(tool.namespace, tool.name),
|
|
66
|
+
): string | undefined {
|
|
67
|
+
const visibleNames = tools
|
|
68
|
+
?.filter(tool => toolChoiceAllows(tool, toolChoice))
|
|
69
|
+
.map(toWireName);
|
|
70
|
+
return buildNonOpenAIToolCatalogNudgeFromNames(visibleNames);
|
|
71
|
+
}
|
package/src/bridge.ts
CHANGED
|
@@ -63,6 +63,7 @@ export function bridgeToResponsesSSE(
|
|
|
63
63
|
stallTimeoutSec?: number;
|
|
64
64
|
hideThinkingSummary?: boolean;
|
|
65
65
|
onTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
66
|
+
onCompletedResponse?: (response: Record<string, unknown>) => void;
|
|
66
67
|
},
|
|
67
68
|
): ReadableStream<Uint8Array> {
|
|
68
69
|
// Freeform/custom tools (apply_patch) carry their body in `input`; the model is given a
|
|
@@ -437,8 +438,10 @@ export function bridgeToResponsesSSE(
|
|
|
437
438
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
438
439
|
if (currentToolCall) closeCurrentToolCall();
|
|
439
440
|
if (currentWebSearch) closeCurrentWebSearch("completed", []);
|
|
441
|
+
const response = { ...responseSnapshot("completed", finishedItems), usage: responsesUsage(event.usage) };
|
|
442
|
+
options?.onCompletedResponse?.(response);
|
|
440
443
|
emit("response.completed", {
|
|
441
|
-
response
|
|
444
|
+
response,
|
|
442
445
|
});
|
|
443
446
|
reportTerminal("completed");
|
|
444
447
|
terminated = true;
|
|
@@ -453,6 +456,9 @@ export function bridgeToResponsesSSE(
|
|
|
453
456
|
emit("response.failed", {
|
|
454
457
|
response: {
|
|
455
458
|
...responseSnapshot("failed", finishedItems),
|
|
459
|
+
// Partial consumption from a mid-stream upstream failure: surfaced so the request
|
|
460
|
+
// log can record real tokens instead of usageStatus "unreported" with 0.
|
|
461
|
+
...(event.usage ? { usage: responsesUsage(event.usage) } : {}),
|
|
456
462
|
error: responseError(502, "upstream_error", event.message),
|
|
457
463
|
last_error: responseError(502, "upstream_error", event.message),
|
|
458
464
|
},
|
package/src/cli-help.ts
CHANGED
|
@@ -14,8 +14,14 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
14
14
|
init: { usage: "ocx init", summary: "Interactive setup for providers and Codex config injection." },
|
|
15
15
|
start: { usage: "ocx start [--port <port>]", summary: "Start the proxy server and sync models to Codex." },
|
|
16
16
|
stop: { usage: "ocx stop", summary: "Stop the proxy and restore native Codex config." },
|
|
17
|
-
restore: {
|
|
18
|
-
|
|
17
|
+
restore: {
|
|
18
|
+
usage: "ocx restore [back]",
|
|
19
|
+
summary: "Restore native Codex config without stopping the proxy; `restore back` re-points codex at the running proxy.",
|
|
20
|
+
},
|
|
21
|
+
eject: {
|
|
22
|
+
usage: "ocx eject [back]",
|
|
23
|
+
summary: "Restore native Codex config without stopping the proxy; `eject back` re-points codex at the running proxy.",
|
|
24
|
+
},
|
|
19
25
|
"recover-history": {
|
|
20
26
|
usage: "ocx recover-history --legacy-openai",
|
|
21
27
|
summary: "Explicitly recover pre-backup syncResumeHistory rows.",
|
|
@@ -72,6 +78,7 @@ Usage:
|
|
|
72
78
|
ocx start [--port <port>] Start the proxy server (auto-syncs models to Codex)
|
|
73
79
|
ocx stop Stop the proxy AND restore native Codex (plain codex works again)
|
|
74
80
|
ocx restore Restore native Codex without stopping (alias: eject)
|
|
81
|
+
ocx restore back Re-point codex at the running proxy (undo restore)
|
|
75
82
|
ocx recover-history --legacy-openai
|
|
76
83
|
Explicitly recover pre-backup syncResumeHistory rows
|
|
77
84
|
ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)
|
package/src/cli-status.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { durableBunRuntime } from "./bun-runtime";
|
|
2
2
|
import { codexAutoStartEnabled, getConfigPath, getPidPath, readConfigDiagnostics, readPid, readRuntimePort, type RuntimePortState } from "./config";
|
|
3
3
|
import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "./codex-plugins-doctor";
|
|
4
|
+
import { isOpencodexHealthz, probeHostname } from "./proxy-liveness";
|
|
4
5
|
import type { OcxConfig } from "./types";
|
|
5
6
|
import { serviceStatusSummary } from "./service";
|
|
6
7
|
|
|
@@ -54,9 +55,6 @@ export type CliStatusView = {
|
|
|
54
55
|
healthLabel: string;
|
|
55
56
|
};
|
|
56
57
|
|
|
57
|
-
function healthHost(hostname?: string): string {
|
|
58
|
-
return !hostname || hostname === "0.0.0.0" || hostname === "::" ? "127.0.0.1" : hostname;
|
|
59
|
-
}
|
|
60
58
|
|
|
61
59
|
export type ListenTarget = {
|
|
62
60
|
port: number;
|
|
@@ -78,7 +76,7 @@ export function selectListenTarget(
|
|
|
78
76
|
port,
|
|
79
77
|
hostname,
|
|
80
78
|
source: currentRuntimePort ? "runtime" : "config",
|
|
81
|
-
healthUrl: `http://${
|
|
79
|
+
healthUrl: `http://${probeHostname(hostname)}:${port}/healthz`,
|
|
82
80
|
dashboardUrl: `http://localhost:${port}/`,
|
|
83
81
|
};
|
|
84
82
|
}
|
|
@@ -93,7 +91,11 @@ async function checkProxyHealth(target: ListenTarget): Promise<HealthCheck> {
|
|
|
93
91
|
const message = `returned HTTP ${response.status}`;
|
|
94
92
|
return { ok: false, url, message, label: `${url} ${message}` };
|
|
95
93
|
}
|
|
96
|
-
const body = await response.json().catch(() => null) as { version?: unknown; uptime?: unknown } | null;
|
|
94
|
+
const body = await response.json().catch(() => null) as { service?: unknown; status?: unknown; version?: unknown; uptime?: unknown } | null;
|
|
95
|
+
if (!isOpencodexHealthz(body)) {
|
|
96
|
+
const message = "responded, but not an opencodex proxy";
|
|
97
|
+
return { ok: false, url, message, label: `${url} ${message}` };
|
|
98
|
+
}
|
|
97
99
|
const version = typeof body?.version === "string" ? ` v${body.version}` : "";
|
|
98
100
|
const uptime = typeof body?.uptime === "number" ? `, uptime ${Math.round(body.uptime)}s` : "";
|
|
99
101
|
const message = `ok${version}${uptime}`;
|
package/src/cli.ts
CHANGED
|
@@ -9,9 +9,12 @@ import {
|
|
|
9
9
|
getConfigDir,
|
|
10
10
|
loadConfig,
|
|
11
11
|
readPid,
|
|
12
|
+
readPidFileValue,
|
|
12
13
|
readRuntimePort,
|
|
13
14
|
removePid,
|
|
15
|
+
removePidIfValueIs,
|
|
14
16
|
removeRuntimePort,
|
|
17
|
+
removeRuntimePortIfPidIs,
|
|
15
18
|
saveConfig,
|
|
16
19
|
writePid,
|
|
17
20
|
writeRuntimePort,
|
|
@@ -19,12 +22,15 @@ import {
|
|
|
19
22
|
import { collectStatus } from "./cli-status";
|
|
20
23
|
import { installCrashGuards } from "./crash-guard";
|
|
21
24
|
import { hasHelpFlag, printSubcommandUsage, printUsage, printVersion } from "./cli-help";
|
|
22
|
-
import { findAvailablePort, shouldPersistSelectedPort } from "./ports";
|
|
23
|
-
import {
|
|
25
|
+
import { findAvailablePort, isAddrInUse, shouldPersistSelectedPort } from "./ports";
|
|
26
|
+
import { findLiveProxy, probeHostname, type LiveProxy } from "./proxy-liveness";
|
|
27
|
+
import { stopProxy } from "./process-control";
|
|
24
28
|
import { serviceCommand, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "./service";
|
|
25
29
|
import { drainAndShutdown, startServer } from "./server";
|
|
26
30
|
import { maybeShowStarPrompt } from "./star-prompt";
|
|
27
31
|
import { maybeShowUpdatePrompt } from "./update-notify";
|
|
32
|
+
import { syncModelsToCodex } from "./codex-sync";
|
|
33
|
+
import { normalizeUpdateChannel, runGuiUpdateWorker } from "./update-job";
|
|
28
34
|
|
|
29
35
|
const args = process.argv.slice(2);
|
|
30
36
|
const command = args[0];
|
|
@@ -44,28 +50,6 @@ if (command !== undefined && command !== "help" && hasHelpFlag(args.slice(1))) {
|
|
|
44
50
|
process.exit(0);
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
async function syncModelsToCodex(port?: number) {
|
|
48
|
-
const config = loadConfig();
|
|
49
|
-
const p = port ?? config.port ?? 10100;
|
|
50
|
-
let catalogPath: string | null | undefined;
|
|
51
|
-
try {
|
|
52
|
-
const { refreshCodexModelCatalog } = await import("./codex-refresh");
|
|
53
|
-
const cat = await refreshCodexModelCatalog(config);
|
|
54
|
-
catalogPath = cat.catalogExists ? cat.path : null;
|
|
55
|
-
if (cat.added > 0) {
|
|
56
|
-
console.log(` + ${cat.added} models appended to Codex catalog (${cat.path})`);
|
|
57
|
-
} else if (catalogPath === null) {
|
|
58
|
-
console.error("catalog sync skipped: no Codex catalog source found; keeping Codex's native catalog.");
|
|
59
|
-
}
|
|
60
|
-
} catch (e) {
|
|
61
|
-
console.error("catalog sync skipped:", e instanceof Error ? e.message : String(e));
|
|
62
|
-
}
|
|
63
|
-
const { injectCodexConfig } = await import("./codex-inject");
|
|
64
|
-
const result = await injectCodexConfig(p, config, { catalogPath });
|
|
65
|
-
console.log(result.message);
|
|
66
|
-
return result;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
53
|
function parsePortOption(): number | undefined {
|
|
70
54
|
if (args.length === 1) return undefined;
|
|
71
55
|
if (args.length !== 3 || args[1] !== "--port") {
|
|
@@ -83,26 +67,13 @@ function parsePortOption(): number | undefined {
|
|
|
83
67
|
return port;
|
|
84
68
|
}
|
|
85
69
|
|
|
86
|
-
async function
|
|
87
|
-
const config = loadConfig();
|
|
88
|
-
const p = port ?? config.port ?? 10100;
|
|
89
|
-
try {
|
|
90
|
-
const hostname = !config.hostname || config.hostname === "0.0.0.0" || config.hostname === "::" ? "127.0.0.1" : config.hostname;
|
|
91
|
-
const res = await fetch(`http://${hostname}:${p}/healthz`, {
|
|
92
|
-
signal: AbortSignal.timeout(750),
|
|
93
|
-
});
|
|
94
|
-
return res.ok;
|
|
95
|
-
} catch {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function waitForProxy(timeoutMs = 8_000): Promise<number | null> {
|
|
70
|
+
async function waitForProxy(timeoutMs = 8_000): Promise<LiveProxy | null> {
|
|
101
71
|
const deadline = Date.now() + timeoutMs;
|
|
102
72
|
while (Date.now() < deadline) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
73
|
+
// Runtime-state-first with identity: finds the proxy even when it started on a
|
|
74
|
+
// fallback port, and never mistakes a foreign 200 for our proxy.
|
|
75
|
+
const live = await findLiveProxy();
|
|
76
|
+
if (live) return live;
|
|
106
77
|
await new Promise(resolve => setTimeout(resolve, 150));
|
|
107
78
|
}
|
|
108
79
|
return null;
|
|
@@ -127,9 +98,9 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
127
98
|
reconcileJournal();
|
|
128
99
|
const existingPid = readPid();
|
|
129
100
|
if (existingPid) {
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
console.error(`⚠️ Proxy already running (PID ${existingPid}). Use 'ocx stop' first.`);
|
|
101
|
+
const live = await findLiveProxy();
|
|
102
|
+
if (live) {
|
|
103
|
+
console.error(`⚠️ Proxy already running (PID ${live.pid ?? existingPid}, port ${live.port}). Use 'ocx stop' first.`);
|
|
133
104
|
process.exit(1);
|
|
134
105
|
}
|
|
135
106
|
removePid(existingPid);
|
|
@@ -140,9 +111,20 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
140
111
|
// live daemon holding resources while it overwrites its own binary.
|
|
141
112
|
await maybeShowUpdatePrompt();
|
|
142
113
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
114
|
+
// Port selection is check-then-bind: a concurrent `ocx start`/`ensure` can win the port
|
|
115
|
+
// between the probe and Bun.serve. Retry the pick instead of dying on EADDRINUSE.
|
|
116
|
+
let port = await chooseListenPort(requestedPort);
|
|
117
|
+
let server: ReturnType<typeof startServer>;
|
|
118
|
+
for (let attempt = 0; ; attempt++) {
|
|
119
|
+
try {
|
|
120
|
+
server = startServer(port);
|
|
121
|
+
break;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (!isAddrInUse(err) || attempt >= 2) throw err;
|
|
124
|
+
console.log(`⚠️ Port ${port} was taken while starting; picking another...`);
|
|
125
|
+
port = await chooseListenPort(requestedPort);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
146
128
|
// A single request's streaming error must never crash the daemon serving every
|
|
147
129
|
// other Codex session — capture the full stack to crash.log and stay up.
|
|
148
130
|
installCrashGuards();
|
|
@@ -206,16 +188,17 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
206
188
|
|
|
207
189
|
async function handleEnsure() {
|
|
208
190
|
reconcileJournal();
|
|
209
|
-
|
|
191
|
+
const config = loadConfig();
|
|
210
192
|
if (!codexAutoStartEnabled(config)) {
|
|
211
193
|
console.log("Codex autostart is disabled.");
|
|
212
194
|
return;
|
|
213
195
|
}
|
|
214
|
-
|
|
215
|
-
|
|
196
|
+
const live = await findLiveProxy();
|
|
197
|
+
if (live) {
|
|
198
|
+
await syncModelsToCodex(live.port).catch(e => {
|
|
216
199
|
console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
217
200
|
});
|
|
218
|
-
console.log(`✅ Proxy running on port ${
|
|
201
|
+
console.log(`✅ Proxy running on port ${live.port}`);
|
|
219
202
|
return;
|
|
220
203
|
}
|
|
221
204
|
|
|
@@ -227,19 +210,20 @@ async function handleEnsure() {
|
|
|
227
210
|
});
|
|
228
211
|
child.unref();
|
|
229
212
|
|
|
230
|
-
const port = await waitForProxy();
|
|
213
|
+
const port = (await waitForProxy())?.port;
|
|
231
214
|
if (!port) {
|
|
232
215
|
console.error("❌ Proxy did not become healthy after starting.");
|
|
233
216
|
process.exit(1);
|
|
234
217
|
}
|
|
235
|
-
config
|
|
236
|
-
|
|
218
|
+
// Always sync the LIVE port: after a fallback-port start, config.port still names the
|
|
219
|
+
// busy preferred port — syncing that would point Codex at a dead listener.
|
|
220
|
+
await syncModelsToCodex(port).catch(e => {
|
|
237
221
|
console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
238
222
|
});
|
|
239
|
-
console.log(`✅ Proxy running on port ${
|
|
223
|
+
console.log(`✅ Proxy running on port ${port}`);
|
|
240
224
|
}
|
|
241
225
|
|
|
242
|
-
function handleStop() {
|
|
226
|
+
async function handleStop() {
|
|
243
227
|
const stoppedService = stopServiceIfInstalled();
|
|
244
228
|
if (stoppedService) console.log("🛑 Service manager stopped (won't respawn).");
|
|
245
229
|
|
|
@@ -247,7 +231,9 @@ function handleStop() {
|
|
|
247
231
|
let stopFailed = false;
|
|
248
232
|
if (pid) {
|
|
249
233
|
try {
|
|
250
|
-
|
|
234
|
+
// Graceful-first (management-API drain) — on Windows this is the only path where
|
|
235
|
+
// the proxy's shutdown handlers actually run; taskkill /F is the fallback inside.
|
|
236
|
+
await stopProxy(pid);
|
|
251
237
|
console.log(`✅ Proxy (PID ${pid}) stopped.`);
|
|
252
238
|
removePid(pid);
|
|
253
239
|
removeRuntimePort(pid);
|
|
@@ -255,8 +241,32 @@ function handleStop() {
|
|
|
255
241
|
stopFailed = true;
|
|
256
242
|
console.error(`❌ Failed to stop proxy (PID ${pid}).`);
|
|
257
243
|
}
|
|
258
|
-
} else
|
|
259
|
-
|
|
244
|
+
} else {
|
|
245
|
+
// Snapshot the stale on-disk state BEFORE the async probe: a concurrent `ocx start`
|
|
246
|
+
// can write fresh records mid-probe, and the purge below must never delete those.
|
|
247
|
+
const stalePidValue = readPidFileValue();
|
|
248
|
+
const staleRuntimePid = readRuntimePort()?.pid ?? null;
|
|
249
|
+
// Orphan recovery: a live proxy can outlive its pid file (crash, manual delete,
|
|
250
|
+
// corrupt file). Identity-checked liveness still finds it via the runtime record.
|
|
251
|
+
const live = await findLiveProxy();
|
|
252
|
+
if (live?.pid) {
|
|
253
|
+
try {
|
|
254
|
+
await stopProxy(live.pid);
|
|
255
|
+
console.log(`✅ Proxy (PID ${live.pid}) stopped.`);
|
|
256
|
+
} catch {
|
|
257
|
+
stopFailed = true;
|
|
258
|
+
console.error(`❌ Failed to stop proxy (PID ${live.pid}).`);
|
|
259
|
+
}
|
|
260
|
+
} else if (!stoppedService) {
|
|
261
|
+
console.log("No running proxy found.");
|
|
262
|
+
}
|
|
263
|
+
if (!stopFailed) {
|
|
264
|
+
// `readPid() === null` means the snapshotted pid file was absent, invalid, dead, or
|
|
265
|
+
// not ours — stale by definition. Purge (guarded by the snapshot) so `ocx update`'s
|
|
266
|
+
// stop gate can't wedge on it.
|
|
267
|
+
removePidIfValueIs(stalePidValue);
|
|
268
|
+
removeRuntimePortIfPidIs(staleRuntimePid);
|
|
269
|
+
}
|
|
260
270
|
}
|
|
261
271
|
const r = restoreNativeCodex();
|
|
262
272
|
console.log(`↩️ ${r.message}`);
|
|
@@ -266,9 +276,9 @@ function handleStop() {
|
|
|
266
276
|
async function handleUninstall() {
|
|
267
277
|
const failures: string[] = [];
|
|
268
278
|
|
|
269
|
-
const runStep = (label: string, step: () => void | boolean) => {
|
|
279
|
+
const runStep = async (label: string, step: () => void | boolean | Promise<void | boolean>) => {
|
|
270
280
|
try {
|
|
271
|
-
const changed = step();
|
|
281
|
+
const changed = await step();
|
|
272
282
|
if (changed === false) console.log(`- ${label}: not installed`);
|
|
273
283
|
else console.log(`✅ ${label}`);
|
|
274
284
|
} catch (err) {
|
|
@@ -277,20 +287,20 @@ async function handleUninstall() {
|
|
|
277
287
|
}
|
|
278
288
|
};
|
|
279
289
|
|
|
280
|
-
runStep("service stopped", () => stopServiceIfInstalled());
|
|
290
|
+
await runStep("service stopped", () => stopServiceIfInstalled());
|
|
281
291
|
|
|
282
|
-
runStep("proxy stopped", () => {
|
|
292
|
+
await runStep("proxy stopped", async () => {
|
|
283
293
|
const pid = readPid();
|
|
284
294
|
if (!pid) return false;
|
|
285
|
-
|
|
295
|
+
await stopProxy(pid);
|
|
286
296
|
removePid(pid);
|
|
287
297
|
removeRuntimePort(pid);
|
|
288
298
|
return true;
|
|
289
299
|
});
|
|
290
300
|
|
|
291
|
-
runStep("service removed", () => uninstallServiceIfInstalled());
|
|
301
|
+
await runStep("service removed", () => uninstallServiceIfInstalled());
|
|
292
302
|
|
|
293
|
-
runStep("native Codex restored", () => {
|
|
303
|
+
await runStep("native Codex restored", () => {
|
|
294
304
|
const r = restoreNativeCodex();
|
|
295
305
|
if (!r.success) throw new Error(r.message);
|
|
296
306
|
});
|
|
@@ -305,7 +315,7 @@ async function handleUninstall() {
|
|
|
305
315
|
}
|
|
306
316
|
|
|
307
317
|
if (failures.length === 0) {
|
|
308
|
-
runStep("opencodex config removed", () => {
|
|
318
|
+
await runStep("opencodex config removed", () => {
|
|
309
319
|
rmSync(getConfigDir(), { recursive: true, force: true });
|
|
310
320
|
});
|
|
311
321
|
} else {
|
|
@@ -355,6 +365,11 @@ async function handleStatus() {
|
|
|
355
365
|
console.log(` Suggested: ${status.json.codexPlugins.suggestedRepair}`);
|
|
356
366
|
}
|
|
357
367
|
}
|
|
368
|
+
const { oauthLoginSummary } = await import("./oauth/index");
|
|
369
|
+
console.log(` OAuth logins:`);
|
|
370
|
+
for (const e of oauthLoginSummary()) {
|
|
371
|
+
console.log(` ${e.provider.padEnd(10)} ${e.loggedIn ? `✓ logged in${e.email ? ` (${e.email})` : ""}` : "✗ not logged in"}`);
|
|
372
|
+
}
|
|
358
373
|
}
|
|
359
374
|
|
|
360
375
|
function handleRecoverHistory() {
|
|
@@ -364,6 +379,12 @@ function handleRecoverHistory() {
|
|
|
364
379
|
process.exit(1);
|
|
365
380
|
}
|
|
366
381
|
const r = restoreLegacyOpenaiHistory();
|
|
382
|
+
if (r.failed) {
|
|
383
|
+
console.error(
|
|
384
|
+
"⚠️ Recovery SKIPPED: the Codex history DB is locked (Codex app/IDE open?). Close it and rerun this command.",
|
|
385
|
+
);
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
367
388
|
console.log(`Recovered ${r.rows} legacy thread(s) to openai (${r.files} rollout file(s) updated).`);
|
|
368
389
|
}
|
|
369
390
|
|
|
@@ -377,13 +398,26 @@ switch (command) {
|
|
|
377
398
|
await handleStart();
|
|
378
399
|
break;
|
|
379
400
|
case "stop":
|
|
380
|
-
handleStop();
|
|
401
|
+
await handleStop();
|
|
381
402
|
break;
|
|
382
403
|
case "restore":
|
|
383
404
|
case "eject": {
|
|
405
|
+
if (args[1] === "back") {
|
|
406
|
+
// Reverse switch: re-point plain `codex` at the RUNNING proxy without touching its
|
|
407
|
+
// lifecycle — the counterpart of `ocx restore`. Start/stop triggers are unchanged;
|
|
408
|
+
// this only re-runs the same inject (config + catalog + history) `ocx start` does.
|
|
409
|
+
const live = await findLiveProxy();
|
|
410
|
+
if (!live) {
|
|
411
|
+
console.error("No running proxy found. Run 'ocx start' — it injects opencodex automatically.");
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
await syncModelsToCodex(live.port);
|
|
415
|
+
console.log("Plain `codex` now routes through opencodex again (undo with: ocx restore).");
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
384
418
|
const r = restoreNativeCodex();
|
|
385
419
|
console.log(r.success ? `✅ ${r.message}` : `⚠️ ${r.message}`);
|
|
386
|
-
console.log("Plain `codex` now runs natively (no proxy).");
|
|
420
|
+
console.log("Plain `codex` now runs natively (no proxy). Switch back with: ocx restore back");
|
|
387
421
|
break;
|
|
388
422
|
}
|
|
389
423
|
case "recover-history":
|
|
@@ -417,7 +451,7 @@ switch (command) {
|
|
|
417
451
|
break;
|
|
418
452
|
}
|
|
419
453
|
case "sync": {
|
|
420
|
-
await syncModelsToCodex();
|
|
454
|
+
await syncModelsToCodex((await findLiveProxy())?.port);
|
|
421
455
|
break;
|
|
422
456
|
}
|
|
423
457
|
case "sync-cache": {
|
|
@@ -428,8 +462,10 @@ switch (command) {
|
|
|
428
462
|
case "gui": {
|
|
429
463
|
const cfg = await import("./config");
|
|
430
464
|
const config = cfg.loadConfig();
|
|
431
|
-
|
|
432
|
-
|
|
465
|
+
// Identity-checked liveness (not the pid file + a fixed sleep): finds a fallback-port
|
|
466
|
+
// proxy and waits until the spawned one actually answers before opening the browser.
|
|
467
|
+
let live = await findLiveProxy();
|
|
468
|
+
if (!live) {
|
|
433
469
|
console.log("Proxy not running. Starting...");
|
|
434
470
|
const child = spawn(process.execPath, [process.argv[1], "start"], {
|
|
435
471
|
detached: true,
|
|
@@ -438,19 +474,19 @@ switch (command) {
|
|
|
438
474
|
env: process.env,
|
|
439
475
|
});
|
|
440
476
|
child.unref();
|
|
441
|
-
|
|
442
|
-
pid = cfg.readPid();
|
|
477
|
+
live = await waitForProxy();
|
|
443
478
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const
|
|
479
|
+
// Open the host the proxy actually binds — `localhost` only answers for
|
|
480
|
+
// loopback/wildcard binds, not a concrete LAN/IPv6 hostname.
|
|
481
|
+
const guiHost = probeHostname(live?.hostname ?? config.hostname);
|
|
482
|
+
const guiUrl = `http://${guiHost === "127.0.0.1" ? "localhost" : guiHost}:${live?.port ?? config.port}`;
|
|
447
483
|
console.log(`Opening ${guiUrl}`);
|
|
448
484
|
const { openUrl } = await import("./open-url");
|
|
449
485
|
openUrl(guiUrl);
|
|
450
486
|
break;
|
|
451
487
|
}
|
|
452
488
|
case "service":
|
|
453
|
-
serviceCommand(args[1]);
|
|
489
|
+
await serviceCommand(args[1]);
|
|
454
490
|
break;
|
|
455
491
|
case "codex-shim": {
|
|
456
492
|
const { codexShimStatus, installCodexShim, uninstallCodexShim } = await import("./codex-shim");
|
|
@@ -488,6 +524,13 @@ switch (command) {
|
|
|
488
524
|
await refreshVersionCache(channel);
|
|
489
525
|
break;
|
|
490
526
|
}
|
|
527
|
+
case "__gui-update-worker": {
|
|
528
|
+
const jobId = args[1];
|
|
529
|
+
if (!jobId) process.exit(1);
|
|
530
|
+
const channel = normalizeUpdateChannel(args[2]);
|
|
531
|
+
runGuiUpdateWorker(jobId, channel, args[3] === "restart");
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
491
534
|
case "help":
|
|
492
535
|
case "--help":
|
|
493
536
|
case "-h":
|