@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.
Files changed (118) hide show
  1. package/README.md +1 -0
  2. package/bin/ocx.mjs +4 -4
  3. package/gui/dist/assets/index-ByGC8-Bm.css +1 -0
  4. package/gui/dist/assets/index-CkV5xFA8.js +15 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +4 -4
  7. package/src/adapters/anthropic-image-guard.ts +195 -0
  8. package/src/adapters/anthropic.ts +85 -14
  9. package/src/adapters/cursor/cursor-errors.ts +2 -2
  10. package/src/adapters/cursor/live-transport.ts +1 -1
  11. package/src/adapters/cursor/transport-retry.ts +2 -2
  12. package/src/adapters/google-errors.ts +1 -1
  13. package/src/adapters/google-http.ts +1 -1
  14. package/src/adapters/google-truncation.ts +1 -1
  15. package/src/adapters/google.ts +1 -1
  16. package/src/adapters/kiro-errors.ts +1 -1
  17. package/src/adapters/kiro-retry.ts +1 -1
  18. package/src/adapters/kiro-truncation.ts +1 -1
  19. package/src/adapters/kiro.ts +1 -1
  20. package/src/adapters/openai-chat.ts +12 -2
  21. package/src/adapters/openai-responses.ts +126 -3
  22. package/src/bridge.ts +164 -7
  23. package/src/{doctor.ts → cli/doctor.ts} +21 -4
  24. package/src/{cli-help.ts → cli/help.ts} +1 -1
  25. package/src/cli/index.ts +584 -0
  26. package/src/{init.ts → cli/init.ts} +6 -6
  27. package/src/{cli-models.ts → cli/models.ts} +2 -2
  28. package/src/{cli-provider.ts → cli/provider.ts} +12 -8
  29. package/src/{star-prompt.ts → cli/star-prompt.ts} +1 -1
  30. package/src/{cli-status.ts → cli/status.ts} +7 -7
  31. package/src/cli.ts +9 -575
  32. package/src/{codex-account-label.ts → codex/account-label.ts} +1 -1
  33. package/src/{codex-account-lifecycle.ts → codex/account-lifecycle.ts} +6 -6
  34. package/src/{codex-account-store.ts → codex/account-store.ts} +2 -2
  35. package/src/{codex-account-usability.ts → codex/account-usability.ts} +4 -4
  36. package/src/{codex-auth-api.ts → codex/auth-api.ts} +18 -18
  37. package/src/{codex-auth-collision.ts → codex/auth-collision.ts} +4 -4
  38. package/src/{codex-auth-context.ts → codex/auth-context.ts} +9 -9
  39. package/src/{codex-catalog.ts → codex/catalog.ts} +49 -20
  40. package/src/codex/history-migration-guardian.ts +102 -0
  41. package/src/{codex-history-provider.ts → codex/history-provider.ts} +111 -7
  42. package/src/{codex-home.ts → codex/home.ts} +1 -1
  43. package/src/{codex-inject.ts → codex/inject.ts} +204 -26
  44. package/src/{codex-journal.ts → codex/journal.ts} +2 -2
  45. package/src/{codex-main-account.ts → codex/main-account.ts} +2 -2
  46. package/src/{model-cache.ts → codex/model-cache.ts} +1 -1
  47. package/src/{codex-paths.ts → codex/paths.ts} +2 -2
  48. package/src/{codex-plugins-doctor.ts → codex/plugins-doctor.ts} +2 -2
  49. package/src/{codex-refresh.ts → codex/refresh.ts} +4 -4
  50. package/src/{codex-routing.ts → codex/routing.ts} +8 -8
  51. package/src/{codex-shim.ts → codex/shim.ts} +6 -5
  52. package/src/{codex-sync.ts → codex/sync.ts} +4 -4
  53. package/src/{codex-websocket-registry.ts → codex/websocket-registry.ts} +1 -1
  54. package/src/config.ts +2 -0
  55. package/src/generated/jawcode-model-metadata.ts +2 -0
  56. package/src/{bun-runtime.ts → lib/bun-runtime.ts} +1 -1
  57. package/src/{crash-guard.ts → lib/crash-guard.ts} +1 -1
  58. package/src/{process-control.ts → lib/process-control.ts} +1 -1
  59. package/src/{service-secrets.ts → lib/service-secrets.ts} +1 -1
  60. package/src/oauth/callback-server.ts +1 -1
  61. package/src/oauth/google-antigravity.ts +7 -4
  62. package/src/oauth/index.ts +67 -16
  63. package/src/oauth/login-cli.ts +2 -2
  64. package/src/oauth/store.ts +236 -20
  65. package/src/oauth/token-guardian.ts +24 -20
  66. package/src/oauth/types.ts +16 -0
  67. package/src/providers/api-keys.ts +121 -0
  68. package/src/{provider-context-cap.ts → providers/context-cap.ts} +1 -1
  69. package/src/providers/derive.ts +2 -0
  70. package/src/providers/key-failover.ts +145 -0
  71. package/src/{provider-label.ts → providers/label.ts} +1 -1
  72. package/src/{provider-quota.ts → providers/quota.ts} +12 -7
  73. package/src/providers/registry.ts +66 -4
  74. package/src/responses/compaction.ts +117 -0
  75. package/src/responses/parser.ts +89 -13
  76. package/src/responses/reasoning-envelope.ts +52 -0
  77. package/src/responses/schema.ts +15 -3
  78. package/src/responses/state.ts +117 -2
  79. package/src/router.ts +2 -0
  80. package/src/server/auth-cors.ts +231 -0
  81. package/src/server/index.ts +523 -0
  82. package/src/server/lifecycle.ts +73 -0
  83. package/src/server/management-api.ts +628 -0
  84. package/src/{proxy-liveness.ts → server/proxy-liveness.ts} +1 -1
  85. package/src/server/relay.ts +534 -0
  86. package/src/server/request-decompress.ts +46 -0
  87. package/src/server/request-log.ts +310 -0
  88. package/src/server/responses.ts +775 -0
  89. package/src/{ws-bridge.ts → server/ws-bridge.ts} +44 -13
  90. package/src/service.ts +19 -11
  91. package/src/types.ts +27 -0
  92. package/src/{update.ts → update/index.ts} +5 -5
  93. package/src/{update-job.ts → update/job.ts} +7 -6
  94. package/src/{update-notify.ts → update/notify.ts} +3 -3
  95. package/src/{usage-debug.ts → usage/debug.ts} +3 -3
  96. package/src/{usage-log.ts → usage/log.ts} +3 -3
  97. package/src/{usage-summary.ts → usage/summary.ts} +3 -3
  98. package/src/{usage-totals.ts → usage/totals.ts} +1 -1
  99. package/src/vision/describe.ts +3 -3
  100. package/src/vision/index.ts +21 -1
  101. package/src/web-search/executor.ts +4 -4
  102. package/src/web-search/index.ts +1 -1
  103. package/src/web-search/loop.ts +84 -24
  104. package/gui/dist/assets/index-BcHhxo1I.css +0 -1
  105. package/gui/dist/assets/index-DCC1q_Jx.js +0 -15
  106. package/src/server.ts +0 -2501
  107. /package/src/{codex-account-runtime-state.ts → codex/account-runtime-state.ts} +0 -0
  108. /package/src/{codex-quota.ts → codex/quota.ts} +0 -0
  109. /package/src/{abort.ts → lib/abort.ts} +0 -0
  110. /package/src/{debug.ts → lib/debug.ts} +0 -0
  111. /package/src/{errors.ts → lib/errors.ts} +0 -0
  112. /package/src/{open-url.ts → lib/open-url.ts} +0 -0
  113. /package/src/{privacy.ts → lib/privacy.ts} +0 -0
  114. /package/src/{redact.ts → lib/redact.ts} +0 -0
  115. /package/src/{sidecar-tracker.ts → lib/sidecar-tracker.ts} +0 -0
  116. /package/src/{upstream-retry.ts → lib/upstream-retry.ts} +0 -0
  117. /package/src/{win-paths.ts → lib/win-paths.ts} +0 -0
  118. /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 "./config";
3
- import { markJournalInjectedState, restoreJournalState, writeJournal } from "./codex-journal";
4
- import { restoreCodexCatalog } from "./codex-catalog";
5
- import { syncCodexHistoryProvider } from "./codex-history-provider";
6
- import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH, DEFAULT_CATALOG_PATH, parseTomlString, readRootTomlString, resolveCodexConfigPath, tomlString } from "./codex-paths";
7
- import type { OcxConfig } from "./types";
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
- let content = readFileSync(CODEX_CONFIG_PATH, "utf-8");
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
- // 1) Root key BEFORE the first table header (must be a global, not nested under a table).
275
- content = setRootModelProvider(content);
276
- // 2) Provider table appended at EOF (position-independent).
277
- content = content.trimEnd() + "\n" + buildProviderTableBlock(port, websocketsEnabled(config ?? {}), shouldInjectApiAuthHeader(config), config?.hostname);
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 ?? {}), shouldInjectApiAuthHeader(config), config?.hostname);
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
- ? ` ⚠️ Codex resume history sync SKIPPED: the history DB is locked (Codex app/IDE open?). Close it and rerun 'ocx start'.\n`
294
- : ` Codex resume history: ${history.rows} thread(s) made visible for opencodex; originals backed up for restore.\n`;
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: `Injected opencodex as default provider into Codex config.\n` +
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
- ` Fallback: codex --profile opencodex (same behavior)`,
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
- if (hadRootOcxProvider) out = stripRootRoutedModel(out);
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]") || /^\s*model_provider\s*=\s*"opencodex"/m.test(content);
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 content = readFileSync(CODEX_CONFIG_PATH, "utf-8");
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
- if (had) {
357
- atomicWriteFile(CODEX_CONFIG_PATH, stripOpencodexConfig(content));
358
- } else if (stripOpencodexConfig(content) !== content) {
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
- const history = syncCodexHistoryProvider("openai");
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 "./config";
5
- import { CODEX_HOME, CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "./codex-paths";
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 "./codex-auth-collision";
2
- import { decodeJwtPayload } from "./oauth/chatgpt";
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 "./codex-catalog";
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 "./config";
4
- import { defaultCodexHome } from "./codex-home";
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 "./codex-paths";
4
- import { redactUserPath } from "./redact";
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 "./codex-catalog";
3
- import { CODEX_MODELS_CACHE_PATH } from "./codex-paths";
4
- import { atomicWriteFile } from "./config";
5
- import type { OcxConfig } from "./types";
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 "./config";
2
- import { isCodexAccountGenerationLive, readCodexAccountRecord } from "./codex-account-store";
3
- import { codexAccountLogLabel } from "./codex-account-label";
4
- import { isCodexAccountUsable } from "./codex-account-usability";
5
- import { isAccountNeedsReauth, markAccountNeedsReauth } from "./codex-account-runtime-state";
6
- import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./codex-quota";
7
- import { MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "./codex-main-account";
8
- import type { OcxConfig } from "./types";
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 "./config";
4
- import { durableBunPath } from "./bun-runtime";
5
- import { serviceApiTokenFilePath } from "./service-secrets";
6
- import { windowsEnvIndirectBatchValue } from "./win-paths";
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
- return { bun: durableBunPath(), cli: join(import.meta.dir, "cli.ts") };
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 "./codex-inject";
2
- import { refreshCodexModelCatalog } from "./codex-refresh";
3
- import { applyProxyEnv, loadConfig } from "./config";
4
- import type { OcxConfig } from "./types";
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;
@@ -1,5 +1,5 @@
1
1
  import type { ServerWebSocket } from "bun";
2
- import type { WsData } from "./ws-bridge";
2
+ import type { WsData } from "../server/ws-bridge";
3
3
 
4
4
  const socketsByAccount = new Map<string, Set<ServerWebSocket<WsData>>>();
5
5
 
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 "./config";
3
+ import { getConfigDir } from "../config";
4
4
  import { redactSecretString, redactUrlForLog } from "./redact";
5
5
  import { sidecarBreadcrumb, activityBreadcrumb } from "./sidecar-tracker";
6
6
 
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { loadConfig, readRuntimePort } from "./config";
2
+ import { loadConfig, readRuntimePort } from "../config";
3
3
 
4
4
  export function isProcessAlive(pid: number): boolean {
5
5
  try {
@@ -1,5 +1,5 @@
1
1
  import { join } from "node:path";
2
- import { getConfigDir } from "./config";
2
+ import { getConfigDir } from "../config";
3
3
 
4
4
  export function serviceApiTokenFilePath(): string {
5
5
  return join(getConfigDir(), "service-api-token");
@@ -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
- prompt: "consent",
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> {