@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
|
@@ -1,13 +1,41 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { atomicWriteFile, websocketsEnabled } from "
|
|
3
|
-
import { markJournalInjectedState, restoreJournalState, writeJournal } from "./
|
|
4
|
-
import { restoreCodexCatalog } from "./
|
|
5
|
-
import { syncCodexHistoryProvider } from "./
|
|
6
|
-
import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH, DEFAULT_CATALOG_PATH, parseTomlString, readRootTomlString, resolveCodexConfigPath, tomlString } from "./
|
|
7
|
-
import type { OcxConfig } from "
|
|
2
|
+
import { atomicWriteFile, loadConfig, websocketsEnabled } from "../config";
|
|
3
|
+
import { markJournalInjectedState, restoreJournalState, writeJournal } from "./journal";
|
|
4
|
+
import { restoreCodexCatalog } from "./catalog";
|
|
5
|
+
import { migrateHistoryToOpenai, syncCodexHistoryProvider } from "./history-provider";
|
|
6
|
+
import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH, DEFAULT_CATALOG_PATH, parseTomlString, readRootTomlString, resolveCodexConfigPath, tomlString } from "./paths";
|
|
7
|
+
import type { OcxConfig } from "../types";
|
|
8
8
|
|
|
9
9
|
const OCX_SECTION_MARKER = "# Auto-injected by opencodex";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Detect the file's dominant line ending. Every transform in this module is LF-pure
|
|
13
|
+
* (split("\n") + hard "\n" joins), so CRLF configs (Windows-edited config.toml) are
|
|
14
|
+
* normalized to LF at the pipeline boundary and converted back on write — otherwise a
|
|
15
|
+
* single inject would leave a mixed-EOL file.
|
|
16
|
+
*/
|
|
17
|
+
export function dominantEol(content: string): "\r\n" | "\n" {
|
|
18
|
+
const crlf = (content.match(/\r\n/g) ?? []).length;
|
|
19
|
+
if (crlf === 0) return "\n";
|
|
20
|
+
const bareLf = (content.match(/\n/g) ?? []).length - crlf;
|
|
21
|
+
return crlf >= bareLf ? "\r\n" : "\n";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Normalize all line endings to `eol` (CRLF first collapsed to LF, then expanded). */
|
|
25
|
+
export function applyEol(content: string, eol: "\r\n" | "\n"): string {
|
|
26
|
+
const lf = content.replace(/\r\n/g, "\n");
|
|
27
|
+
return eol === "\n" ? lf : lf.replace(/\n/g, "\r\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Design B (2026-07-06): loopback installs no longer re-tag the provider. Instead of
|
|
32
|
+
* `model_provider = "opencodex"` + a `[model_providers.opencodex]` table, we set the official
|
|
33
|
+
* built-in override `openai_base_url` (codex-rs config_toml.rs) so codex's own `openai`
|
|
34
|
+
* provider points at the proxy. Threads keep `model_provider = "openai"`, so history never
|
|
35
|
+
* needs remapping or restore. Non-loopback binds keep the legacy table injection because the
|
|
36
|
+
* built-in provider cannot carry the `x-opencodex-api-key` env header.
|
|
37
|
+
*/
|
|
38
|
+
|
|
11
39
|
export interface InjectCodexOptions {
|
|
12
40
|
/**
|
|
13
41
|
* Absolute or CODEX_HOME-relative catalog path to advertise to Codex. Pass `null` only when the
|
|
@@ -41,7 +69,7 @@ function providerBaseHost(hostname: string | undefined): string {
|
|
|
41
69
|
return trimmed.includes(":") ? `[${trimmed}]` : trimmed;
|
|
42
70
|
}
|
|
43
71
|
|
|
44
|
-
function shouldInjectApiAuthHeader(config: Pick<OcxConfig, "hostname"> | undefined): boolean {
|
|
72
|
+
export function shouldInjectApiAuthHeader(config: Pick<OcxConfig, "hostname"> | undefined): boolean {
|
|
45
73
|
return !isLoopbackHostname(config?.hostname);
|
|
46
74
|
}
|
|
47
75
|
|
|
@@ -63,6 +91,76 @@ export function buildProviderTableBlock(port: number, supportsWebsockets = false
|
|
|
63
91
|
return lines.join("\n") + "\n";
|
|
64
92
|
}
|
|
65
93
|
|
|
94
|
+
export function buildOpenaiBaseUrlLine(port: number, hostname?: string): string {
|
|
95
|
+
return `openai_base_url = "http://${providerBaseHost(hostname)}:${port}/v1"`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isRootOpenaiBaseUrlLine(line: string): boolean {
|
|
99
|
+
return /^\s*openai_base_url\s*=/.test(line);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Design B root-key injection: place `OCX_SECTION_MARKER` + `openai_base_url` at the document
|
|
104
|
+
* ROOT (before the first table header). Idempotent: an existing marker-owned line is rewritten
|
|
105
|
+
* in place. A user's OWN root `openai_base_url` (no marker above it) is respected — we keep it
|
|
106
|
+
* and inject nothing, reporting `keptUserBaseUrl` so the caller can surface it.
|
|
107
|
+
*/
|
|
108
|
+
export function setRootOpenaiBaseUrl(content: string, port: number, hostname?: string): { content: string; keptUserBaseUrl: boolean } {
|
|
109
|
+
const lines = content.split("\n");
|
|
110
|
+
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
111
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
112
|
+
const key = buildOpenaiBaseUrlLine(port, hostname);
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < rootEnd; i++) {
|
|
115
|
+
if (!isRootOpenaiBaseUrlLine(lines[i])) continue;
|
|
116
|
+
const markerOwned = i > 0 && lines[i - 1].includes(OCX_SECTION_MARKER);
|
|
117
|
+
if (!markerOwned) return { content, keptUserBaseUrl: true };
|
|
118
|
+
lines[i] = key;
|
|
119
|
+
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (firstTable === -1) {
|
|
123
|
+
return { content: content.replace(/\n+$/, "") + "\n" + OCX_SECTION_MARKER + "\n" + key + "\n", keptUserBaseUrl: false };
|
|
124
|
+
}
|
|
125
|
+
let insertAt = firstTable;
|
|
126
|
+
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
127
|
+
lines.splice(insertAt, 0, OCX_SECTION_MARKER, key);
|
|
128
|
+
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Remove the marker-owned root `openai_base_url` (marker line + the key line right after it).
|
|
133
|
+
* A user's own root override (no marker) survives; an orphaned marker with no key line after
|
|
134
|
+
* it is dropped too so repeated strip/inject cycles cannot accumulate marker comments.
|
|
135
|
+
*/
|
|
136
|
+
export function stripInjectedOpenaiBaseUrl(content: string): string {
|
|
137
|
+
const lines = content.split("\n");
|
|
138
|
+
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
139
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
140
|
+
const drop = new Set<number>();
|
|
141
|
+
for (let i = 0; i < rootEnd; i++) {
|
|
142
|
+
if (!lines[i].includes(OCX_SECTION_MARKER)) continue;
|
|
143
|
+
if (i + 1 < rootEnd && isRootOpenaiBaseUrlLine(lines[i + 1])) {
|
|
144
|
+
drop.add(i);
|
|
145
|
+
drop.add(i + 1);
|
|
146
|
+
} else if (i + 1 >= rootEnd || lines[i + 1].trim() === "") {
|
|
147
|
+
drop.add(i); // orphaned marker at root
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (drop.size === 0) return content;
|
|
151
|
+
return lines.filter((_, i) => !drop.has(i)).join("\n");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function hasInjectedOpenaiBaseUrl(content: string): boolean {
|
|
155
|
+
const lines = content.split("\n");
|
|
156
|
+
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
157
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
158
|
+
for (let i = 1; i < rootEnd; i++) {
|
|
159
|
+
if (isRootOpenaiBaseUrlLine(lines[i]) && lines[i - 1].includes(OCX_SECTION_MARKER)) return true;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
66
164
|
/**
|
|
67
165
|
* Strip every existing `model_provider` line that we must not duplicate: any line set to
|
|
68
166
|
* "opencodex" (wherever it sits — including a previously mis-nested one under a table), plus any
|
|
@@ -225,6 +323,20 @@ function stripOpencodexCatalogPath(content: string): string {
|
|
|
225
323
|
|
|
226
324
|
export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string): string {
|
|
227
325
|
const host = providerBaseHost(hostname);
|
|
326
|
+
// Design B (loopback): the reference/fallback file documents the root override form.
|
|
327
|
+
// Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry
|
|
328
|
+
// the x-opencodex-api-key env header).
|
|
329
|
+
if (!includeApiAuthHeader) {
|
|
330
|
+
const lines = [
|
|
331
|
+
"# OpenCodex proxy fallback config (Design B)",
|
|
332
|
+
`# Root override that points Codex's built-in openai provider at the proxy on ${host}:${port}.`,
|
|
333
|
+
"# Merge these root keys into ~/.codex/config.toml manually if auto-injection was removed.",
|
|
334
|
+
buildOpenaiBaseUrlLine(port, hostname),
|
|
335
|
+
];
|
|
336
|
+
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
337
|
+
lines.push("", "[features]", "fast_mode = true", "");
|
|
338
|
+
return lines.join("\n");
|
|
339
|
+
}
|
|
228
340
|
const lines = [
|
|
229
341
|
"# OpenCodex proxy profile — use with: codex --profile opencodex",
|
|
230
342
|
`# Routes all model requests through the opencodex proxy at ${host}:${port}`,
|
|
@@ -254,11 +366,17 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
254
366
|
}
|
|
255
367
|
|
|
256
368
|
writeJournal();
|
|
257
|
-
|
|
369
|
+
const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
|
|
370
|
+
// EOL boundary: transforms below are LF-pure; preserve the file's dominant ending on write.
|
|
371
|
+
const eol = dominantEol(rawContent);
|
|
372
|
+
let content = applyEol(rawContent, "\n");
|
|
258
373
|
|
|
259
374
|
// Idempotent clean-up of any prior injection: drop the provider table (marker-based) and every
|
|
260
375
|
// stray/mis-nested model_provider line, so re-injecting can't duplicate keys or leave the buggy
|
|
261
376
|
// table-nested key behind.
|
|
377
|
+
// Design B form FIRST: removeOcxSection also keys on the marker line, so a root-level
|
|
378
|
+
// marker + openai_base_url pair must be gone before it scans or it would swallow root keys.
|
|
379
|
+
content = stripInjectedOpenaiBaseUrl(content);
|
|
262
380
|
if (content.includes("[model_providers.opencodex]")) {
|
|
263
381
|
content = removeOcxSection(content);
|
|
264
382
|
}
|
|
@@ -271,36 +389,79 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
271
389
|
const catalogPath = chooseCatalogPathForInjection(content, options.catalogPath);
|
|
272
390
|
content = catalogPath ? setRootModelCatalogPath(content, catalogPath) : stripOpencodexCatalogPath(content);
|
|
273
391
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
392
|
+
const legacyMode = shouldInjectApiAuthHeader(config);
|
|
393
|
+
let keptUserBaseUrl = false;
|
|
394
|
+
if (legacyMode) {
|
|
395
|
+
// Legacy (non-loopback) injection: the built-in openai provider cannot carry the
|
|
396
|
+
// x-opencodex-api-key env header, so keep the opencodex provider table + root re-tag.
|
|
397
|
+
// 1) Root key BEFORE the first table header (must be a global, not nested under a table).
|
|
398
|
+
content = setRootModelProvider(content);
|
|
399
|
+
// 2) Provider table appended at EOF (position-independent).
|
|
400
|
+
content = content.trimEnd() + "\n" + buildProviderTableBlock(port, websocketsEnabled(config ?? {}), true, config?.hostname);
|
|
401
|
+
} else {
|
|
402
|
+
// Design B (loopback): a single root override; codex keeps its native `openai` provider id
|
|
403
|
+
// so thread history is never remapped. Any legacy form was already stripped above.
|
|
404
|
+
content = stripInjectedOpenaiBaseUrl(content); // normalize before idempotent re-insert
|
|
405
|
+
const result = setRootOpenaiBaseUrl(content, port, config?.hostname);
|
|
406
|
+
content = result.content;
|
|
407
|
+
keptUserBaseUrl = result.keptUserBaseUrl;
|
|
408
|
+
}
|
|
278
409
|
|
|
279
|
-
const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}),
|
|
410
|
+
const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname);
|
|
411
|
+
content = applyEol(content, eol);
|
|
280
412
|
atomicWriteFile(CODEX_CONFIG_PATH, content);
|
|
281
413
|
atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
|
|
282
414
|
markJournalInjectedState(content, profileContent);
|
|
415
|
+
// Legacy mode still forward-tags history so re-tagged threads stay listable. Design B needs
|
|
416
|
+
// the opposite: a one-time migration of previously re-tagged threads BACK to openai (restore
|
|
417
|
+
// machinery; cheap no-op when there is nothing to migrate).
|
|
283
418
|
const history = config?.syncResumeHistory !== false
|
|
284
|
-
? syncCodexHistoryProvider("opencodex")
|
|
419
|
+
? (legacyMode ? syncCodexHistoryProvider("opencodex") : migrateHistoryToOpenai())
|
|
285
420
|
: { rows: 0, files: 0 };
|
|
286
421
|
|
|
287
422
|
const catalogMessage = catalogPath
|
|
288
423
|
? ` Codex model catalog: ${catalogPath}\n`
|
|
289
424
|
: ` Codex model catalog not injected because no opencodex catalog file exists yet.\n`;
|
|
425
|
+
const migratedRows = (history.rows ?? 0) + ("ejectedRows" in history ? history.ejectedRows ?? 0 : 0);
|
|
290
426
|
const historyMessage = config?.syncResumeHistory === false
|
|
291
427
|
? ` Codex resume history: left unchanged (syncResumeHistory=false).\n`
|
|
292
428
|
: history.failed
|
|
293
|
-
?
|
|
294
|
-
|
|
429
|
+
? (legacyMode
|
|
430
|
+
? ` ⚠️ Codex resume history sync SKIPPED: the history DB is locked (Codex app/IDE open?). Close it and rerun 'ocx start'.\n`
|
|
431
|
+
// Honest in every caller context: the daemon retries in the background while it runs,
|
|
432
|
+
// and this inject path re-runs the migration on every future start/sync anyway.
|
|
433
|
+
: ` ⚠️ Codex resume history migration deferred: the history DB is locked (Codex app/IDE open?). It is retried automatically (while the proxy runs and on every 'ocx start'); to force it now, close the Codex app and run 'ocx sync'.\n`)
|
|
434
|
+
: legacyMode
|
|
435
|
+
? ` Codex resume history: ${history.rows} thread(s) made visible for opencodex; originals backed up for restore.\n`
|
|
436
|
+
: migratedRows > 0
|
|
437
|
+
? ` Codex resume history: ${migratedRows} legacy opencodex-tagged thread(s) migrated back to openai (one-time).\n`
|
|
438
|
+
: ` Codex resume history: untouched (threads keep their native openai tag).\n`;
|
|
439
|
+
// A user-owned root openai_base_url means we did NOT install routing — say so honestly
|
|
440
|
+
// instead of claiming the proxy route is active (catalog/fast_mode were still written).
|
|
441
|
+
if (keptUserBaseUrl) {
|
|
442
|
+
return {
|
|
443
|
+
success: true,
|
|
444
|
+
message: `⚠️ Codex routing NOT injected: your config already sets a root openai_base_url, and opencodex never overwrites a user-owned override.\n` +
|
|
445
|
+
catalogMessage +
|
|
446
|
+
historyMessage +
|
|
447
|
+
` To route plain codex through the proxy, remove your openai_base_url line from ~/.codex/config.toml and rerun 'ocx start'.\n` +
|
|
448
|
+
` Reference config: ${CODEX_PROFILE_PATH}`,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const headline = legacyMode
|
|
452
|
+
? `Injected opencodex as default provider into Codex config.\n`
|
|
453
|
+
: `Pointed Codex's built-in openai provider at the opencodex proxy (openai_base_url).\n`;
|
|
295
454
|
return {
|
|
296
455
|
success: true,
|
|
297
|
-
message:
|
|
456
|
+
message: headline +
|
|
298
457
|
catalogMessage +
|
|
299
458
|
historyMessage +
|
|
300
459
|
` All models now route through opencodex proxy (like OpenRouter).\n` +
|
|
301
460
|
` OpenAI models (gpt-5.5, etc.) are passed through to OpenAI.\n` +
|
|
302
461
|
` Custom models route to their configured providers.\n` +
|
|
303
|
-
|
|
462
|
+
(legacyMode
|
|
463
|
+
? ` Fallback: codex --profile opencodex (same behavior)`
|
|
464
|
+
: ` Fallback reference: ${CODEX_PROFILE_PATH}`),
|
|
304
465
|
};
|
|
305
466
|
}
|
|
306
467
|
|
|
@@ -331,6 +492,8 @@ function removeOcxSection(content: string): string {
|
|
|
331
492
|
export function stripOpencodexConfig(content: string): string {
|
|
332
493
|
let out = content;
|
|
333
494
|
const hadRootOcxProvider = readRootTomlString(out, "model_provider") === "opencodex";
|
|
495
|
+
const hadInjectedBaseUrl = hasInjectedOpenaiBaseUrl(out);
|
|
496
|
+
out = stripInjectedOpenaiBaseUrl(out); // before removeOcxSection — it keys on the marker line too
|
|
334
497
|
if (out.includes("[model_providers.opencodex]")) {
|
|
335
498
|
out = removeOcxSection(out);
|
|
336
499
|
}
|
|
@@ -338,25 +501,32 @@ export function stripOpencodexConfig(content: string): string {
|
|
|
338
501
|
// Regex (not exact-string) removal so compact `model_provider="opencodex"` is stripped too —
|
|
339
502
|
// must match the detection regex above, or a detected line could survive un-removed.
|
|
340
503
|
out = out.split("\n").filter(l => !/^\s*model_provider\s*=\s*"opencodex"\s*$/.test(l)).join("\n");
|
|
341
|
-
|
|
504
|
+
// Routed root model ids (`model = "provider/slug"`) only make sense while the proxy serves
|
|
505
|
+
// them — strip on both the legacy re-tag form and the Design B injected-base-url form.
|
|
506
|
+
if (hadRootOcxProvider || hadInjectedBaseUrl) out = stripRootRoutedModel(out);
|
|
342
507
|
out = stripOpencodexCatalogPath(out);
|
|
343
508
|
return out.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
344
509
|
}
|
|
345
510
|
|
|
346
511
|
function hasOpencodexRouting(content: string): boolean {
|
|
347
|
-
return content.includes("[model_providers.opencodex]")
|
|
512
|
+
return content.includes("[model_providers.opencodex]")
|
|
513
|
+
|| /^\s*model_provider\s*=\s*"opencodex"/m.test(content)
|
|
514
|
+
|| hasInjectedOpenaiBaseUrl(content);
|
|
348
515
|
}
|
|
349
516
|
|
|
350
517
|
export function removeCodexConfig(options: { preserveProfile?: boolean } = {}): { success: boolean; message: string } {
|
|
351
518
|
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
352
519
|
return { success: false, message: "Codex config not found." };
|
|
353
520
|
}
|
|
354
|
-
const
|
|
521
|
+
const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
|
|
522
|
+
// Same EOL boundary as inject: strip in LF space, write back in the file's own ending.
|
|
523
|
+
// The unchanged fast path compares in LF space so an untouched file is never rewritten.
|
|
524
|
+
const eol = dominantEol(rawContent);
|
|
525
|
+
const content = applyEol(rawContent, "\n");
|
|
355
526
|
const had = hasOpencodexRouting(content);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
atomicWriteFile(CODEX_CONFIG_PATH, stripOpencodexConfig(content));
|
|
527
|
+
const stripped = stripOpencodexConfig(content);
|
|
528
|
+
if (had || stripped !== content) {
|
|
529
|
+
atomicWriteFile(CODEX_CONFIG_PATH, applyEol(stripped, eol));
|
|
360
530
|
}
|
|
361
531
|
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH)) unlinkSync(CODEX_PROFILE_PATH);
|
|
362
532
|
return {
|
|
@@ -378,7 +548,15 @@ export function restoreNativeCodex(): { success: boolean; message: string } {
|
|
|
378
548
|
? { success: true, message: "Codex config restored from opencodex journal." }
|
|
379
549
|
: removeCodexConfig({ preserveProfile: journal.profileRestored || journal.profileChanged });
|
|
380
550
|
const cat = restoreCodexCatalog();
|
|
381
|
-
|
|
551
|
+
// Design B (loopback) steady state: threads are already tagged openai, so prove the
|
|
552
|
+
// no-op with a readonly probe instead of write-opening a DB the Codex app may hold
|
|
553
|
+
// (Windows: WAL writer lock -> seconds of stalling + a false warning on every stop).
|
|
554
|
+
// Legacy (non-loopback) installs keep the unconditional write-open restore.
|
|
555
|
+
let skipWhenProvablyNoop = false;
|
|
556
|
+
try {
|
|
557
|
+
skipWhenProvablyNoop = !shouldInjectApiAuthHeader(loadConfig());
|
|
558
|
+
} catch { /* unreadable config: keep the conservative write-open restore */ }
|
|
559
|
+
const history = syncCodexHistoryProvider("openai", undefined, undefined, { skipWhenProvablyNoop });
|
|
382
560
|
const msg = cat.removed > 0
|
|
383
561
|
? `${cfg.message} Catalog restored to ${cat.kept} native model(s) (dropped ${cat.removed} proxy-routed).`
|
|
384
562
|
: cfg.message;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { atomicWriteFile } from "
|
|
5
|
-
import { CODEX_HOME, CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "./
|
|
4
|
+
import { atomicWriteFile } from "../config";
|
|
5
|
+
import { CODEX_HOME, CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "./paths";
|
|
6
6
|
|
|
7
7
|
const JOURNAL_PATH = join(CODEX_HOME, "opencodex-journal.json");
|
|
8
8
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readCodexTokens } from "./
|
|
2
|
-
import { decodeJwtPayload } from "
|
|
1
|
+
import { readCodexTokens } from "./auth-collision";
|
|
2
|
+
import { decodeJwtPayload } from "../oauth/chatgpt";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Stable id under which the "main" Codex account (the Codex CLI login stored in
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* long-running process and the on-disk Codex catalog already persists the last sync across
|
|
8
8
|
* restarts, so an in-memory cache is sufficient here (no SQLite layer needed).
|
|
9
9
|
*/
|
|
10
|
-
import type { CatalogModel } from "./
|
|
10
|
+
import type { CatalogModel } from "./catalog";
|
|
11
11
|
|
|
12
12
|
/** Default freshness window. Matches Codex's own 5-min models cache so the two stay in step. */
|
|
13
13
|
export const DEFAULT_MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
-
import { expandUserPath } from "
|
|
4
|
-
import { defaultCodexHome } from "./
|
|
3
|
+
import { expandUserPath } from "../config";
|
|
4
|
+
import { defaultCodexHome } from "./home";
|
|
5
5
|
|
|
6
6
|
function resolveCodexHome(): string {
|
|
7
7
|
const raw = process.env.CODEX_HOME?.trim();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
|
-
import { CODEX_CONFIG_PATH } from "./
|
|
4
|
-
import { redactUserPath } from "
|
|
3
|
+
import { CODEX_CONFIG_PATH } from "./paths";
|
|
4
|
+
import { redactUserPath } from "../lib/redact";
|
|
5
5
|
|
|
6
6
|
// Mirrors codex-rs core-plugins/src/marketplace.rs MARKETPLACE_MANIFEST_RELATIVE_PATHS.
|
|
7
7
|
// A marketplace root "resolves" only when one of these files exists under it.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { invalidateCodexModelsCache, syncCatalogModels } from "./
|
|
3
|
-
import { CODEX_MODELS_CACHE_PATH } from "./
|
|
4
|
-
import { atomicWriteFile } from "
|
|
5
|
-
import type { OcxConfig } from "
|
|
2
|
+
import { invalidateCodexModelsCache, syncCatalogModels } from "./catalog";
|
|
3
|
+
import { CODEX_MODELS_CACHE_PATH } from "./paths";
|
|
4
|
+
import { atomicWriteFile } from "../config";
|
|
5
|
+
import type { OcxConfig } from "../types";
|
|
6
6
|
|
|
7
7
|
export interface CodexCatalogRefreshResult {
|
|
8
8
|
added: number;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { saveConfig } from "
|
|
2
|
-
import { isCodexAccountGenerationLive, readCodexAccountRecord } from "./
|
|
3
|
-
import { codexAccountLogLabel } from "./
|
|
4
|
-
import { isCodexAccountUsable } from "./
|
|
5
|
-
import { isAccountNeedsReauth, markAccountNeedsReauth } from "./
|
|
6
|
-
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./
|
|
7
|
-
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "./
|
|
8
|
-
import type { OcxConfig } from "
|
|
1
|
+
import { saveConfig } from "../config";
|
|
2
|
+
import { isCodexAccountGenerationLive, readCodexAccountRecord } from "./account-store";
|
|
3
|
+
import { codexAccountLogLabel } from "./account-label";
|
|
4
|
+
import { isCodexAccountUsable } from "./account-usability";
|
|
5
|
+
import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
6
|
+
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./quota";
|
|
7
|
+
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "./main-account";
|
|
8
|
+
import type { OcxConfig } from "../types";
|
|
9
9
|
|
|
10
10
|
type ThreadAffinityEntry = {
|
|
11
11
|
accountId: string;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { delimiter, dirname, extname, join } from "node:path";
|
|
2
2
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { getConfigDir } from "
|
|
4
|
-
import { durableBunPath } from "
|
|
5
|
-
import { serviceApiTokenFilePath } from "
|
|
6
|
-
import { windowsEnvIndirectBatchValue } from "
|
|
3
|
+
import { getConfigDir } from "../config";
|
|
4
|
+
import { durableBunPath } from "../lib/bun-runtime";
|
|
5
|
+
import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
6
|
+
import { windowsEnvIndirectBatchValue } from "../lib/win-paths";
|
|
7
7
|
|
|
8
8
|
const SHIM_MARKER = "opencodex codex autostart shim";
|
|
9
9
|
let lastShimDiscoveryError: string | null = null;
|
|
@@ -48,7 +48,8 @@ interface ShimFileState {
|
|
|
48
48
|
function cliEntry(): { bun: string; cli: string } {
|
|
49
49
|
// Bundled Bun path (survives `ocx update`); all three shim builders
|
|
50
50
|
// (Unix / Windows cmd / Windows PowerShell) receive it via this entry.
|
|
51
|
-
|
|
51
|
+
// This module lives in src/codex/, the CLI entry in src/cli/index.ts.
|
|
52
|
+
return { bun: durableBunPath(), cli: join(import.meta.dir, "..", "cli", "index.ts") };
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
function commandNames(name: string): string[] {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { injectCodexConfig } from "./
|
|
2
|
-
import { refreshCodexModelCatalog } from "./
|
|
3
|
-
import { applyProxyEnv, loadConfig } from "
|
|
4
|
-
import type { OcxConfig } from "
|
|
1
|
+
import { injectCodexConfig } from "./inject";
|
|
2
|
+
import { refreshCodexModelCatalog } from "./refresh";
|
|
3
|
+
import { applyProxyEnv, loadConfig } from "../config";
|
|
4
|
+
import type { OcxConfig } from "../types";
|
|
5
5
|
|
|
6
6
|
export interface CodexSyncResult {
|
|
7
7
|
ok: boolean;
|
package/src/config.ts
CHANGED
|
@@ -472,7 +472,9 @@ export function parsePidFile(raw: string): number | null {
|
|
|
472
472
|
|
|
473
473
|
export function isOcxStartCommandLine(commandLine: string): boolean {
|
|
474
474
|
const normalized = commandLine.toLowerCase().replace(/\\/g, "/");
|
|
475
|
+
// "src/cli.ts" matches pre-restructure installs still running; "src/cli/index.ts" is current.
|
|
475
476
|
const hasOcxEntrypoint = normalized.includes("src/cli.ts")
|
|
477
|
+
|| normalized.includes("src/cli/index.ts")
|
|
476
478
|
|| normalized.includes("@bitkyc08/opencodex")
|
|
477
479
|
|| /(?:^|[\s/"'])(?:ocx|opencodex)(?:\.cmd)?(?:$|[\s"'])/.test(normalized);
|
|
478
480
|
return hasOcxEntrypoint && /(?:^|[\s"'])start(?:$|[\s"'])/.test(normalized);
|
|
@@ -14,6 +14,8 @@ export interface JawcodeModelMetadata {
|
|
|
14
14
|
const PROVIDER_ALIASES: Record<string, string> = {
|
|
15
15
|
"xai": "xai",
|
|
16
16
|
"anthropic": "anthropic",
|
|
17
|
+
"anthropic-apikey": "anthropic",
|
|
18
|
+
"anthropic-key": "anthropic",
|
|
17
19
|
"kimi": "moonshot",
|
|
18
20
|
"opencode-go": "opencode-go",
|
|
19
21
|
"openrouter": "openrouter",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* stable path to that binary. This module is the single source of truth.
|
|
9
9
|
*
|
|
10
10
|
* In a from-source dev checkout the `bun` dependency may be absent; callers fall
|
|
11
|
-
* back to `process.execPath` (which is itself Bun when run via `bun src/cli.ts`).
|
|
11
|
+
* back to `process.execPath` (which is itself Bun when run via `bun src/cli/index.ts`).
|
|
12
12
|
*/
|
|
13
13
|
import { createRequire } from "node:module";
|
|
14
14
|
import { existsSync, statSync } from "node:fs";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { getConfigDir } from "
|
|
3
|
+
import { getConfigDir } from "../config";
|
|
4
4
|
import { redactSecretString, redactUrlForLog } from "./redact";
|
|
5
5
|
import { sidecarBreadcrumb, activityBreadcrumb } from "./sidecar-tracker";
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Handles: port allocation (preferred → random fallback), callback server, CSRF state,
|
|
9
9
|
* manual-input race, 300s timeout. Providers implement generateAuthUrl() + exchangeToken().
|
|
10
10
|
*/
|
|
11
|
-
import { isAddrInUse } from "../ports";
|
|
11
|
+
import { isAddrInUse } from "../server/ports";
|
|
12
12
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
13
13
|
|
|
14
14
|
const DEFAULT_TIMEOUT = 300_000;
|
|
@@ -154,8 +154,9 @@ function credentialsFromPayload(payload: GoogleTokenPayload, refreshFallback = "
|
|
|
154
154
|
|
|
155
155
|
class AntigravityOAuthFlow extends OAuthCallbackFlow {
|
|
156
156
|
#verifier = "";
|
|
157
|
+
#forceAccountSelect: boolean;
|
|
157
158
|
|
|
158
|
-
constructor(ctrl: OAuthController) {
|
|
159
|
+
constructor(ctrl: OAuthController, opts?: { forceAccountSelect?: boolean }) {
|
|
159
160
|
super(ctrl, {
|
|
160
161
|
preferredPort: CALLBACK_PORT,
|
|
161
162
|
callbackPath: CALLBACK_PATH,
|
|
@@ -163,6 +164,7 @@ class AntigravityOAuthFlow extends OAuthCallbackFlow {
|
|
|
163
164
|
callbackBindHostname: "127.0.0.1",
|
|
164
165
|
redirectUri: `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`,
|
|
165
166
|
} satisfies OAuthCallbackFlowOptions);
|
|
167
|
+
this.#forceAccountSelect = opts?.forceAccountSelect === true;
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
|
|
@@ -176,7 +178,8 @@ class AntigravityOAuthFlow extends OAuthCallbackFlow {
|
|
|
176
178
|
code_challenge: pkce.challenge,
|
|
177
179
|
code_challenge_method: "S256",
|
|
178
180
|
access_type: "offline",
|
|
179
|
-
|
|
181
|
+
// select_account lets the user pick a DIFFERENT Google account when adding a second one.
|
|
182
|
+
prompt: this.#forceAccountSelect ? "consent select_account" : "consent",
|
|
180
183
|
state,
|
|
181
184
|
});
|
|
182
185
|
return {
|
|
@@ -207,8 +210,8 @@ class AntigravityOAuthFlow extends OAuthCallbackFlow {
|
|
|
207
210
|
}
|
|
208
211
|
}
|
|
209
212
|
|
|
210
|
-
export async function loginAntigravity(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
211
|
-
return new AntigravityOAuthFlow(ctrl).login();
|
|
213
|
+
export async function loginAntigravity(ctrl: OAuthController, opts?: { forceAccountSelect?: boolean }): Promise<OAuthCredentials> {
|
|
214
|
+
return new AntigravityOAuthFlow(ctrl, opts).login();
|
|
212
215
|
}
|
|
213
216
|
|
|
214
217
|
export async function refreshAntigravityToken(refreshToken: string, signal?: AbortSignal): Promise<OAuthCredentials> {
|