@bitkyc08/opencodex 2.5.5-preview.1 → 2.5.5-preview.2
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/gui/dist/assets/{index-CKX3MGK9.js → index-mmIPacc3.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/cli.ts +54 -11
- package/src/codex-catalog.ts +196 -26
- package/src/codex-history-provider.ts +35 -6
- package/src/codex-shim.ts +159 -42
- package/src/config.ts +61 -7
- package/src/open-url.ts +9 -1
- package/src/server.ts +51 -3
- package/src/service.ts +46 -9
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-mmIPacc3.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-CJF4_jax.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { execFileSync, spawn } from "node:child_process";
|
|
|
3
3
|
import { rmSync } from "node:fs";
|
|
4
4
|
import { restoreNativeCodex } from "./codex-inject";
|
|
5
5
|
import { restoreLegacyOpenaiHistory } from "./codex-history-provider";
|
|
6
|
-
import { codexAutoStartEnabled, getConfigDir, loadConfig, readPid, removePid, saveConfig, writePid } from "./config";
|
|
6
|
+
import { codexAutoStartEnabled, getConfigDir, getConfigPath, loadConfig, readPid, removePid, saveConfig, writePid } from "./config";
|
|
7
7
|
import { findAvailablePort } from "./ports";
|
|
8
|
-
import { serviceCommand, stopServiceIfInstalled, uninstallServiceIfInstalled } from "./service";
|
|
8
|
+
import { serviceCommand, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "./service";
|
|
9
9
|
import { drainAndShutdown, startServer } from "./server";
|
|
10
10
|
import { maybeShowStarPrompt } from "./star-prompt";
|
|
11
11
|
|
|
@@ -191,7 +191,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
191
191
|
console.error(`⚠️ Proxy already running (PID ${existingPid}). Use 'ocx stop' first.`);
|
|
192
192
|
process.exit(1);
|
|
193
193
|
}
|
|
194
|
-
removePid();
|
|
194
|
+
removePid(existingPid);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
const requestedPort = parsePortOption();
|
|
@@ -205,7 +205,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
205
205
|
console.log("\n🛑 Shutting down opencodex proxy...");
|
|
206
206
|
void (async () => {
|
|
207
207
|
await drainAndShutdown(server, config.shutdownTimeoutMs ?? 5000);
|
|
208
|
-
removePid();
|
|
208
|
+
removePid(process.pid);
|
|
209
209
|
if (!process.env.OCX_SERVICE) { try { restoreNativeCodex(); } catch { /* best-effort restore */ } }
|
|
210
210
|
process.exit(0);
|
|
211
211
|
})();
|
|
@@ -300,7 +300,7 @@ function handleStop() {
|
|
|
300
300
|
try {
|
|
301
301
|
killProxy(pid);
|
|
302
302
|
console.log(`✅ Proxy (PID ${pid}) stopped.`);
|
|
303
|
-
removePid();
|
|
303
|
+
removePid(pid);
|
|
304
304
|
} catch {
|
|
305
305
|
stopFailed = true;
|
|
306
306
|
console.error(`❌ Failed to stop proxy (PID ${pid}).`);
|
|
@@ -336,7 +336,7 @@ async function handleUninstall() {
|
|
|
336
336
|
const pid = readPid();
|
|
337
337
|
if (!pid) return false;
|
|
338
338
|
killProxy(pid);
|
|
339
|
-
removePid();
|
|
339
|
+
removePid(pid);
|
|
340
340
|
return true;
|
|
341
341
|
});
|
|
342
342
|
|
|
@@ -365,13 +365,56 @@ async function handleUninstall() {
|
|
|
365
365
|
console.log("\n✅ opencodex local state removed. Remove the package with: npm uninstall -g @bitkyc08/opencodex");
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
|
|
368
|
+
type HealthCheck = {
|
|
369
|
+
ok: boolean;
|
|
370
|
+
label: string;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
async function checkProxyHealth(port: number, hostname?: string): Promise<HealthCheck> {
|
|
374
|
+
const url = `http://${healthHost(hostname)}:${port}/healthz`;
|
|
375
|
+
const controller = new AbortController();
|
|
376
|
+
const timer = setTimeout(() => controller.abort(), 800);
|
|
377
|
+
try {
|
|
378
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
379
|
+
if (!response.ok) return { ok: false, label: `${url} returned HTTP ${response.status}` };
|
|
380
|
+
const body = await response.json().catch(() => null) as { version?: unknown; uptime?: unknown } | null;
|
|
381
|
+
const version = typeof body?.version === "string" ? ` v${body.version}` : "";
|
|
382
|
+
const uptime = typeof body?.uptime === "number" ? `, uptime ${Math.round(body.uptime)}s` : "";
|
|
383
|
+
return { ok: true, label: `${url} ok${version}${uptime}` };
|
|
384
|
+
} catch (error) {
|
|
385
|
+
const reason = error instanceof Error && error.name === "AbortError" ? "timed out" : "unreachable";
|
|
386
|
+
return { ok: false, label: `${url} ${reason}` };
|
|
387
|
+
} finally {
|
|
388
|
+
clearTimeout(timer);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function handleStatus() {
|
|
393
|
+
const config = loadConfig();
|
|
394
|
+
const port = config.port ?? 10100;
|
|
369
395
|
const pid = readPid();
|
|
370
|
-
|
|
371
|
-
|
|
396
|
+
const health = await checkProxyHealth(port, config.hostname);
|
|
397
|
+
const proxyLabel = pid && health.ok
|
|
398
|
+
? `running (PID ${pid})`
|
|
399
|
+
: pid
|
|
400
|
+
? `PID file points to PID ${pid}, but health check failed`
|
|
401
|
+
: health.ok
|
|
402
|
+
? "reachable, but PID file is missing or stale"
|
|
403
|
+
: "not running";
|
|
404
|
+
|
|
405
|
+
if (pid || health.ok) {
|
|
406
|
+
console.log(`✅ Proxy: ${proxyLabel}`);
|
|
372
407
|
} else {
|
|
373
|
-
console.log(
|
|
408
|
+
console.log(`❌ Proxy: ${proxyLabel}`);
|
|
374
409
|
}
|
|
410
|
+
console.log(` Health: ${health.label}`);
|
|
411
|
+
console.log(` Dashboard: http://localhost:${port}/`);
|
|
412
|
+
console.log(` Config: ${getConfigPath()}`);
|
|
413
|
+
console.log(` Default provider: ${config.defaultProvider}`);
|
|
414
|
+
console.log(` Codex autostart: ${codexAutoStartEnabled(config) ? "enabled" : "disabled"}`);
|
|
415
|
+
console.log(` Service: ${serviceStatusSummary()}`);
|
|
416
|
+
const { codexShimStatus } = await import("./codex-shim");
|
|
417
|
+
console.log(` ${codexShimStatus()}`);
|
|
375
418
|
}
|
|
376
419
|
|
|
377
420
|
function handleRecoverHistory() {
|
|
@@ -411,7 +454,7 @@ switch (command) {
|
|
|
411
454
|
await handleUninstall();
|
|
412
455
|
break;
|
|
413
456
|
case "status":
|
|
414
|
-
handleStatus();
|
|
457
|
+
await handleStatus();
|
|
415
458
|
break;
|
|
416
459
|
case "ensure":
|
|
417
460
|
await handleEnsure();
|
package/src/codex-catalog.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { atomicWriteFile, getConfigDir, websocketsEnabled } from "./config";
|
|
5
6
|
import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "./codex-paths";
|
|
6
7
|
import { DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCached, setCached } from "./model-cache";
|
|
7
8
|
import { buildModelsRequest, resolveModelsAuthToken } from "./oauth/index";
|
|
@@ -10,8 +11,28 @@ import { CODEX_REASONING_LEVELS, configuredReasoningEfforts, modelRecordValue, s
|
|
|
10
11
|
import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJawcodeModelMetadata, resolveJawcodeProvider } from "./generated/jawcode-model-metadata";
|
|
11
12
|
import { shouldCaseFoldMetadataModelId } from "./providers/derive";
|
|
12
13
|
|
|
13
|
-
const
|
|
14
|
-
|
|
14
|
+
const BUNDLED_CATALOG_CACHE_MS = 60_000;
|
|
15
|
+
let bundledCatalogCache: { expiresAt: number; value: RawCatalog | null } | null = null;
|
|
16
|
+
|
|
17
|
+
function legacyCatalogBackupPath(): string {
|
|
18
|
+
return join(getConfigDir(), "catalog-backup.json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function catalogBackupPathFor(catalogPath: string): string {
|
|
22
|
+
const normalized = process.platform === "win32" ? resolve(catalogPath).toLowerCase() : resolve(catalogPath);
|
|
23
|
+
const id = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
24
|
+
return join(getConfigDir(), `catalog-backup-${id}.json`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function samePath(a: string, b: string): boolean {
|
|
28
|
+
const left = resolve(a);
|
|
29
|
+
const right = resolve(b);
|
|
30
|
+
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isDefaultCatalogPath(path: string): boolean {
|
|
34
|
+
return samePath(path, DEFAULT_CATALOG_PATH);
|
|
35
|
+
}
|
|
15
36
|
|
|
16
37
|
/**
|
|
17
38
|
* Native OpenAI / Codex models served via ChatGPT OAuth passthrough — FALLBACK only. The ChatGPT
|
|
@@ -36,6 +57,7 @@ export function nativeOpenAiSlugs(): string[] {
|
|
|
36
57
|
|
|
37
58
|
export interface CatalogModel { id: string; provider: string; owned_by?: string; reasoningEfforts?: string[]; contextWindow?: number; inputModalities?: string[]; }
|
|
38
59
|
type RawEntry = Record<string, unknown>;
|
|
60
|
+
type RawCatalog = { models?: RawEntry[]; [k: string]: unknown };
|
|
39
61
|
const JAWCODE_CATALOG_AUGMENT_PROVIDERS = new Set(["opencode-go"]);
|
|
40
62
|
|
|
41
63
|
/**
|
|
@@ -74,15 +96,21 @@ export function readCodexCatalogPath(): string {
|
|
|
74
96
|
return DEFAULT_CATALOG_PATH;
|
|
75
97
|
}
|
|
76
98
|
|
|
77
|
-
function
|
|
99
|
+
function parseCatalogJson(raw: string): RawCatalog | null {
|
|
78
100
|
try {
|
|
79
|
-
|
|
80
|
-
const cat = JSON.parse(readFileSync(path, "utf-8"));
|
|
101
|
+
const cat = JSON.parse(raw);
|
|
81
102
|
return (cat && Array.isArray(cat.models)) ? cat : null;
|
|
82
103
|
} catch { return null; }
|
|
83
104
|
}
|
|
84
105
|
|
|
85
|
-
function
|
|
106
|
+
function readCatalog(path: string): RawCatalog | null {
|
|
107
|
+
try {
|
|
108
|
+
if (!existsSync(path)) return null;
|
|
109
|
+
return parseCatalogJson(readFileSync(path, "utf-8"));
|
|
110
|
+
} catch { return null; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function findNativeTemplate(catalog: RawCatalog | null): RawEntry | null {
|
|
86
114
|
return catalog?.models?.find(
|
|
87
115
|
m => typeof m.slug === "string" && !m.slug.includes("/") && "base_instructions" in m,
|
|
88
116
|
) ?? null;
|
|
@@ -179,14 +207,121 @@ function applyJawcodeCatalogMetadata(entry: RawEntry, slug: string): void {
|
|
|
179
207
|
}
|
|
180
208
|
}
|
|
181
209
|
|
|
182
|
-
|
|
210
|
+
type ExecFile = (
|
|
211
|
+
file: string,
|
|
212
|
+
args: string[],
|
|
213
|
+
options: {
|
|
214
|
+
encoding: "utf8";
|
|
215
|
+
stdio: ["ignore", "pipe", "ignore"];
|
|
216
|
+
timeout: number;
|
|
217
|
+
windowsHide: boolean;
|
|
218
|
+
},
|
|
219
|
+
) => string;
|
|
220
|
+
|
|
221
|
+
interface BundledCatalogDeps {
|
|
222
|
+
commandCandidates?: () => string[];
|
|
223
|
+
execFileSync?: ExecFile;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function unique(values: string[]): string[] {
|
|
227
|
+
return [...new Set(values.filter(Boolean))];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function codexCommandCandidates(): string[] {
|
|
231
|
+
const envPath = process.env.CODEX_CLI_PATH?.trim();
|
|
232
|
+
const candidates = envPath ? [envPath] : [];
|
|
233
|
+
candidates.push(...codexShimCommandCandidates());
|
|
234
|
+
if (process.platform === "win32") {
|
|
235
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
236
|
+
candidates.push(join(dir, "codex.exe"), join(dir, "codex.cmd"));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
candidates.push("codex");
|
|
240
|
+
return unique(candidates);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function codexShimCommandCandidates(): string[] {
|
|
244
|
+
try {
|
|
245
|
+
const state = JSON.parse(readFileSync(join(getConfigDir(), "codex-shim.json"), "utf8")) as {
|
|
246
|
+
wrapperPath?: unknown;
|
|
247
|
+
originalPath?: unknown;
|
|
248
|
+
backupPath?: unknown;
|
|
249
|
+
wrappers?: Array<{ wrapperPath?: unknown; originalPath?: unknown; backupPath?: unknown }>;
|
|
250
|
+
};
|
|
251
|
+
const files = Array.isArray(state.wrappers) && state.wrappers.length > 0 ? state.wrappers : [state];
|
|
252
|
+
const out: string[] = [];
|
|
253
|
+
for (const file of files) {
|
|
254
|
+
for (const value of [file.backupPath, file.originalPath, file.wrapperPath]) {
|
|
255
|
+
if (typeof value !== "string" || value.length === 0) continue;
|
|
256
|
+
if (process.platform === "win32" && value.toLowerCase().endsWith(".ps1")) continue;
|
|
257
|
+
out.push(value);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
} catch {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function runCodexDebugModels(command: string, execFile: ExecFile): string {
|
|
267
|
+
const args = ["debug", "models", "--bundled"];
|
|
268
|
+
return execFile(command, args, {
|
|
269
|
+
encoding: "utf8" as const,
|
|
270
|
+
stdio: ["ignore", "pipe", "ignore"] as ["ignore", "pipe", "ignore"],
|
|
271
|
+
timeout: 10_000,
|
|
272
|
+
windowsHide: true,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function loadBundledCodexCatalog(deps: BundledCatalogDeps = {}): RawCatalog | null {
|
|
277
|
+
const useCache = !deps.commandCandidates && !deps.execFileSync;
|
|
278
|
+
if (useCache && bundledCatalogCache && bundledCatalogCache.expiresAt > Date.now()) {
|
|
279
|
+
return bundledCatalogCache.value;
|
|
280
|
+
}
|
|
281
|
+
const candidates = deps.commandCandidates?.() ?? codexCommandCandidates();
|
|
282
|
+
const execFile = deps.execFileSync ?? (execFileSync as unknown as ExecFile);
|
|
283
|
+
for (const command of candidates) {
|
|
284
|
+
try {
|
|
285
|
+
const catalog = parseCatalogJson(runCodexDebugModels(command, execFile));
|
|
286
|
+
if (catalog && findNativeTemplate(catalog)) {
|
|
287
|
+
if (useCache) bundledCatalogCache = { expiresAt: Date.now() + BUNDLED_CATALOG_CACHE_MS, value: catalog };
|
|
288
|
+
return catalog;
|
|
289
|
+
}
|
|
290
|
+
} catch { /* try next candidate */ }
|
|
291
|
+
}
|
|
292
|
+
if (useCache) bundledCatalogCache = { expiresAt: Date.now() + BUNDLED_CATALOG_CACHE_MS, value: null };
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function materializeBundledCodexCatalog(path: string, deps: BundledCatalogDeps = {}): RawCatalog | null {
|
|
297
|
+
const catalog = loadBundledCodexCatalog(deps);
|
|
298
|
+
if (!catalog) return null;
|
|
299
|
+
try {
|
|
300
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
301
|
+
atomicWriteFile(path, JSON.stringify(catalog, null, 2) + "\n");
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
return catalog;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function loadCatalogForSync(path: string): RawCatalog | null {
|
|
309
|
+
const bundled = isDefaultCatalogPath(path) ? loadBundledCodexCatalog() : null;
|
|
310
|
+
if (bundled) return bundled;
|
|
183
311
|
const catalog = readCatalog(path);
|
|
184
312
|
if (catalog && findNativeTemplate(catalog)) return catalog;
|
|
185
|
-
return readCatalog(
|
|
313
|
+
return readCatalog(catalogBackupPathFor(path))
|
|
314
|
+
?? readCatalog(legacyCatalogBackupPath())
|
|
315
|
+
?? readCatalog(CODEX_MODELS_CACHE_PATH)
|
|
316
|
+
?? materializeBundledCodexCatalog(path)
|
|
317
|
+
?? catalog;
|
|
186
318
|
}
|
|
187
319
|
|
|
188
|
-
function readCurrentCatalogOrCache():
|
|
189
|
-
|
|
320
|
+
function readCurrentCatalogOrCache(): RawCatalog | null {
|
|
321
|
+
const path = readCodexCatalogPath();
|
|
322
|
+
return (isDefaultCatalogPath(path) ? loadBundledCodexCatalog() : null)
|
|
323
|
+
?? readCatalog(path)
|
|
324
|
+
?? readCatalog(CODEX_MODELS_CACHE_PATH);
|
|
190
325
|
}
|
|
191
326
|
|
|
192
327
|
/**
|
|
@@ -195,9 +330,12 @@ function readCurrentCatalogOrCache(): { models?: RawEntry[]; [k: string]: unknow
|
|
|
195
330
|
* Returns a deep copy, or null if no catalog/native entry exists.
|
|
196
331
|
*/
|
|
197
332
|
export function loadCatalogTemplate(): RawEntry | null {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
?? findNativeTemplate(readCatalog(
|
|
333
|
+
const catalogPath = readCodexCatalogPath();
|
|
334
|
+
const native = findNativeTemplate(readCatalog(catalogPath))
|
|
335
|
+
?? findNativeTemplate(readCatalog(catalogBackupPathFor(catalogPath)))
|
|
336
|
+
?? findNativeTemplate(readCatalog(legacyCatalogBackupPath()))
|
|
337
|
+
?? findNativeTemplate(readCatalog(CODEX_MODELS_CACHE_PATH))
|
|
338
|
+
?? findNativeTemplate(loadBundledCodexCatalog());
|
|
201
339
|
return native ? JSON.parse(JSON.stringify(native)) : null;
|
|
202
340
|
}
|
|
203
341
|
|
|
@@ -324,8 +462,35 @@ export function listCatalogNativeSlugs(): string[] {
|
|
|
324
462
|
* a featured native gets its low rank, and un-featuring restores its original catalog priority
|
|
325
463
|
* (rather than the modified value left in the live catalog by a previous sync).
|
|
326
464
|
*/
|
|
327
|
-
function
|
|
328
|
-
|
|
465
|
+
function readCatalogBackup(catalogPath: string): RawCatalog | null {
|
|
466
|
+
return readCatalog(catalogBackupPathFor(catalogPath)) ?? readCatalog(legacyCatalogBackupPath());
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function catalogHasRoutedEntries(catalog: RawCatalog | null): boolean {
|
|
470
|
+
return (catalog?.models ?? []).some(m => typeof m.slug === "string" && m.slug.includes("/"));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function writePristineCatalogBackup(backupPath: string, catalogPath: string, catalog: RawCatalog): void {
|
|
474
|
+
if (existsSync(backupPath)) return;
|
|
475
|
+
const onDisk = readCatalog(catalogPath);
|
|
476
|
+
if (onDisk && !catalogHasRoutedEntries(onDisk)) {
|
|
477
|
+
copyFileSync(catalogPath, backupPath);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (!catalogHasRoutedEntries(catalog)) {
|
|
481
|
+
atomicWriteFile(backupPath, JSON.stringify(catalog, null, 2) + "\n");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function ensureCatalogBackup(catalogPath: string, catalog: RawCatalog): void {
|
|
486
|
+
const dir = getConfigDir();
|
|
487
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
488
|
+
writePristineCatalogBackup(catalogBackupPathFor(catalogPath), catalogPath, catalog);
|
|
489
|
+
writePristineCatalogBackup(legacyCatalogBackupPath(), catalogPath, catalog);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function readNativeBaseline(catalogPath: string): Map<string, number> {
|
|
493
|
+
const backup = readCatalogBackup(catalogPath);
|
|
329
494
|
const out = new Map<string, number>();
|
|
330
495
|
for (const e of backup?.models ?? []) {
|
|
331
496
|
if (typeof e.slug === "string" && !e.slug.includes("/") && typeof e.priority === "number") {
|
|
@@ -531,6 +696,11 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ added: num
|
|
|
531
696
|
|
|
532
697
|
const goModels = await gatherRoutedModels(config);
|
|
533
698
|
if (goModels.length === 0) return { added: 0, path: catalogPath };
|
|
699
|
+
try {
|
|
700
|
+
// Once-only: preserve the PRISTINE pre-opencodex catalog as the native-priority baseline
|
|
701
|
+
// (later syncs would otherwise overwrite it with featured-modified priorities).
|
|
702
|
+
ensureCatalogBackup(catalogPath, catalog);
|
|
703
|
+
} catch { /* backup best-effort */ }
|
|
534
704
|
|
|
535
705
|
// Hide disabled models from Codex, then feature the chosen subagent models (native OR routed)
|
|
536
706
|
// by giving them the lowest priority — see buildCatalogEntries for why priority, not array order.
|
|
@@ -543,7 +713,7 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ added: num
|
|
|
543
713
|
// Keep genuine native entries (gpt-*, codex-*) with their real per-model fields, but drop bare
|
|
544
714
|
// duplicates of routed models (replaced by namespaced entries) + any prior "/" entries. Re-derive
|
|
545
715
|
// each native's priority from the pristine baseline so featuring a native is reversible.
|
|
546
|
-
const baseline = readNativeBaseline();
|
|
716
|
+
const baseline = readNativeBaseline(catalogPath);
|
|
547
717
|
const goIds = new Set(enabledGo.map(m => m.id));
|
|
548
718
|
const native = (catalog.models ?? [])
|
|
549
719
|
.filter(m => typeof m.slug === "string" && !(m.slug as string).includes("/") && !goIds.has(m.slug as string))
|
|
@@ -563,12 +733,6 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ added: num
|
|
|
563
733
|
return e;
|
|
564
734
|
});
|
|
565
735
|
|
|
566
|
-
try {
|
|
567
|
-
if (!existsSync(OCX_DIR)) mkdirSync(OCX_DIR, { recursive: true });
|
|
568
|
-
// Once-only: preserve the PRISTINE pre-opencodex catalog as the native-priority baseline
|
|
569
|
-
// (later syncs would otherwise overwrite it with featured-modified priorities).
|
|
570
|
-
if (!existsSync(CATALOG_BACKUP_PATH)) copyFileSync(catalogPath, CATALOG_BACKUP_PATH);
|
|
571
|
-
} catch { /* backup best-effort */ }
|
|
572
736
|
atomicWriteFile(catalogPath, JSON.stringify(catalog, null, 2) + "\n");
|
|
573
737
|
return { added: goEntries.length, path: catalogPath };
|
|
574
738
|
}
|
|
@@ -582,6 +746,12 @@ export function restoreCodexCatalog(): { removed: number; kept: number; path: st
|
|
|
582
746
|
const catalogPath = readCodexCatalogPath();
|
|
583
747
|
const catalog = readCatalog(catalogPath);
|
|
584
748
|
if (!catalog || !Array.isArray(catalog.models)) return { removed: 0, kept: 0, path: catalogPath };
|
|
749
|
+
const exactBackup = readCatalog(catalogBackupPathFor(catalogPath));
|
|
750
|
+
if (exactBackup && Array.isArray(exactBackup.models)) {
|
|
751
|
+
const removed = Math.max(0, catalog.models.length - exactBackup.models.length);
|
|
752
|
+
atomicWriteFile(catalogPath, JSON.stringify(exactBackup, null, 2) + "\n");
|
|
753
|
+
return { removed, kept: exactBackup.models.length, path: catalogPath };
|
|
754
|
+
}
|
|
585
755
|
const before = catalog.models.length;
|
|
586
756
|
const native = catalog.models.filter(m => !(typeof m.slug === "string" && m.slug.includes("/")));
|
|
587
757
|
const removed = before - native.length;
|
|
@@ -106,7 +106,7 @@ function updateSessionMeta(path: string, patch: { provider?: string; source?: st
|
|
|
106
106
|
}
|
|
107
107
|
if (!changed) return false;
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
atomicWriteFile(path, `${JSON.stringify(record)}${rest}`);
|
|
110
110
|
utimesSync(path, stat.atime, stat.mtime);
|
|
111
111
|
return true;
|
|
112
112
|
}
|
|
@@ -162,7 +162,31 @@ function ejectRemainingOpencodexHistory(db: Database): { rows: number; files: nu
|
|
|
162
162
|
return { rows: rows.length, files };
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
function isRecoverableHistoryError(error: unknown): boolean {
|
|
166
|
+
const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : "";
|
|
167
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
168
|
+
return code === "SQLITE_BUSY"
|
|
169
|
+
|| code === "SQLITE_LOCKED"
|
|
170
|
+
|| code === "EBUSY"
|
|
171
|
+
|| code === "EPERM"
|
|
172
|
+
|| code === "EACCES"
|
|
173
|
+
|| message.includes("database is locked")
|
|
174
|
+
|| message.includes("database is busy")
|
|
175
|
+
|| message.includes("resource busy")
|
|
176
|
+
|| message.includes("operation not permitted")
|
|
177
|
+
|| message.includes("permission denied");
|
|
178
|
+
}
|
|
179
|
+
|
|
165
180
|
export function syncCodexHistoryProvider(provider: CodexHistoryProvider, stateDbPath = STATE_DB_PATH, backupPath = HISTORY_BACKUP_PATH): CodexHistorySyncResult {
|
|
181
|
+
try {
|
|
182
|
+
return syncCodexHistoryProviderUnsafe(provider, stateDbPath, backupPath);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (isRecoverableHistoryError(error)) return { rows: 0, files: 0 };
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function syncCodexHistoryProviderUnsafe(provider: CodexHistoryProvider, stateDbPath: string, backupPath: string): CodexHistorySyncResult {
|
|
166
190
|
if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
|
|
167
191
|
if (provider === "openai") return restoreCodexHistoryProvider(stateDbPath, backupPath);
|
|
168
192
|
|
|
@@ -283,11 +307,16 @@ function restoreCodexHistoryProvider(stateDbPath: string, backupPath: string): C
|
|
|
283
307
|
}
|
|
284
308
|
|
|
285
309
|
export function restoreLegacyOpenaiHistory(stateDbPath = STATE_DB_PATH): { rows: number; files: number } {
|
|
286
|
-
if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
|
|
287
|
-
const db = new Database(stateDbPath);
|
|
288
310
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
311
|
+
if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
|
|
312
|
+
const db = new Database(stateDbPath);
|
|
313
|
+
try {
|
|
314
|
+
return ejectRemainingOpencodexHistory(db);
|
|
315
|
+
} finally {
|
|
316
|
+
db.close();
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (isRecoverableHistoryError(error)) return { rows: 0, files: 0 };
|
|
320
|
+
throw error;
|
|
292
321
|
}
|
|
293
322
|
}
|