@bitkyc08/opencodex 1.9.4 → 1.9.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "1.9.4",
3
+ "version": "1.9.5",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/cli.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { execFileSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
3
4
  import { restoreNativeCodex } from "./codex-inject";
4
5
  import { loadConfig, readPid, removePid, writePid } from "./config";
5
6
  import { serviceCommand } from "./service";
@@ -35,18 +36,22 @@ Examples:
35
36
  async function syncModelsToCodex(port?: number) {
36
37
  const config = loadConfig();
37
38
  const p = port ?? config.port ?? 10100;
38
- const { injectCodexConfig } = await import("./codex-inject");
39
- const result = await injectCodexConfig(p, config);
39
+ let catalogPath: string | null | undefined;
40
40
  try {
41
41
  const { invalidateCodexModelsCache, syncCatalogModels } = await import("./codex-catalog");
42
42
  const cat = await syncCatalogModels(config);
43
+ catalogPath = existsSync(cat.path) ? cat.path : null;
43
44
  if (cat.added > 0) {
44
45
  invalidateCodexModelsCache();
45
46
  console.log(` + ${cat.added} models appended to Codex catalog (${cat.path})`);
47
+ } else if (catalogPath === null) {
48
+ console.error("catalog sync skipped: no Codex catalog source found; keeping Codex's native catalog.");
46
49
  }
47
50
  } catch (e) {
48
51
  console.error("catalog sync skipped:", e instanceof Error ? e.message : String(e));
49
52
  }
53
+ const { injectCodexConfig } = await import("./codex-inject");
54
+ const result = await injectCodexConfig(p, config, { catalogPath });
50
55
  console.log(result.message);
51
56
  return result;
52
57
  }
@@ -1,11 +1,20 @@
1
1
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { atomicWriteFile, websocketsEnabled } from "./config";
3
3
  import { restoreCodexCatalog } from "./codex-catalog";
4
- import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH, DEFAULT_CATALOG_PATH, parseTomlString, readRootTomlString, tomlString } from "./codex-paths";
4
+ import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH, DEFAULT_CATALOG_PATH, parseTomlString, readRootTomlString, resolveCodexConfigPath, tomlString } from "./codex-paths";
5
5
  import type { OcxConfig } from "./types";
6
6
 
7
7
  const OCX_SECTION_MARKER = "# Auto-injected by opencodex";
8
8
 
9
+ export interface InjectCodexOptions {
10
+ /**
11
+ * Absolute or CODEX_HOME-relative catalog path to advertise to Codex. Pass `null` only when the
12
+ * opencodex catalog could not be materialized; Codex will then keep its native catalog instead of
13
+ * failing on a missing model_catalog_json file.
14
+ */
15
+ catalogPath?: string | null;
16
+ }
17
+
9
18
  /**
10
19
  * The `[model_providers.opencodex]` TABLE only. A table is position-independent in TOML, so it is
11
20
  * safe to append at EOF. The bare root key `model_provider = "opencodex"` is NOT included here —
@@ -83,10 +92,20 @@ function readRootModelCatalogPath(content: string): string | null {
83
92
  }
84
93
 
85
94
  function setRootModelCatalogPath(content: string, catalogPath: string): string {
86
- if (readRootModelCatalogPath(content)) return content;
87
95
  const lines = content.split("\n");
88
96
  const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
89
97
  const key = `model_catalog_json = ${tomlString(catalogPath)}`;
98
+ const rootEnd = firstTable === -1 ? lines.length : firstTable;
99
+ for (let i = 0; i < rootEnd; i++) {
100
+ const m = lines[i].match(/^\s*model_catalog_json\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/);
101
+ if (!m) continue;
102
+ const existing = parseTomlString(m[1]);
103
+ if (isOpencodexCatalogPath(existing)) {
104
+ lines[i] = key;
105
+ return lines.join("\n");
106
+ }
107
+ return content;
108
+ }
90
109
  if (firstTable === -1) {
91
110
  return content.replace(/\n+$/, "") + "\n" + key + "\n";
92
111
  }
@@ -157,20 +176,30 @@ function stripOpencodexCatalogPath(content: string): string {
157
176
  .join("\n");
158
177
  }
159
178
 
160
- function buildProfileFile(port: number, catalogPath: string): string {
161
- return [
179
+ export function buildProfileFile(port: number, catalogPath?: string | null): string {
180
+ const lines = [
162
181
  "# OpenCodex proxy profile — use with: codex --profile opencodex",
163
182
  `# Routes all model requests through the opencodex proxy at localhost:${port}`,
164
183
  'model_provider = "opencodex"',
165
- `model_catalog_json = ${tomlString(catalogPath)}`,
166
- "",
167
- "[features]",
168
- "fast_mode = true",
169
- "",
170
- ].join("\n");
184
+ ];
185
+ if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
186
+ lines.push("", "[features]", "fast_mode = true", "");
187
+ return lines.join("\n");
188
+ }
189
+
190
+ export function chooseCatalogPathForInjection(content: string, requested?: string | null): string | null {
191
+ if (requested !== undefined) return requested;
192
+
193
+ const existing = readRootModelCatalogPath(content);
194
+ if (existing) {
195
+ const resolved = resolveCodexConfigPath(existing);
196
+ if (!isOpencodexCatalogPath(resolved) || existsSync(resolved)) return existing;
197
+ }
198
+
199
+ return existsSync(DEFAULT_CATALOG_PATH) ? DEFAULT_CATALOG_PATH : null;
171
200
  }
172
201
 
173
- export async function injectCodexConfig(port: number, config?: OcxConfig): Promise<{ success: boolean; message: string }> {
202
+ export async function injectCodexConfig(port: number, config?: OcxConfig, options: InjectCodexOptions = {}): Promise<{ success: boolean; message: string }> {
174
203
  if (!existsSync(CODEX_CONFIG_PATH)) {
175
204
  return { success: false, message: `Codex config not found at ${CODEX_CONFIG_PATH}. Is Codex installed?` };
176
205
  }
@@ -189,8 +218,8 @@ export async function injectCodexConfig(port: number, config?: OcxConfig): Promi
189
218
  content = normalizeServiceTier(content);
190
219
  content = ensureFastModeFeature(content);
191
220
 
192
- const catalogPath = readRootModelCatalogPath(content) ?? DEFAULT_CATALOG_PATH;
193
- content = setRootModelCatalogPath(content, catalogPath);
221
+ const catalogPath = chooseCatalogPathForInjection(content, options.catalogPath);
222
+ content = catalogPath ? setRootModelCatalogPath(content, catalogPath) : stripOpencodexCatalogPath(content);
194
223
 
195
224
  // 1) Root key BEFORE the first table header (must be a global, not nested under a table).
196
225
  content = setRootModelProvider(content);
@@ -200,9 +229,13 @@ export async function injectCodexConfig(port: number, config?: OcxConfig): Promi
200
229
  writeFileSync(CODEX_CONFIG_PATH, content, "utf-8");
201
230
  writeFileSync(CODEX_PROFILE_PATH, buildProfileFile(port, catalogPath), "utf-8");
202
231
 
232
+ const catalogMessage = catalogPath
233
+ ? ` Codex model catalog: ${catalogPath}\n`
234
+ : ` Codex model catalog not injected because no opencodex catalog file exists yet.\n`;
203
235
  return {
204
236
  success: true,
205
237
  message: `Injected opencodex as default provider into Codex config.\n` +
238
+ catalogMessage +
206
239
  ` All models now route through opencodex proxy (like OpenRouter).\n` +
207
240
  ` OpenAI models (gpt-5.5, etc.) are passed through to OpenAI.\n` +
208
241
  ` Custom models route to their configured providers.\n` +