@koreanhole/opencodex 2.97.0 → 2.99.0
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/AGENTS_INSTALL.md +12 -2
- package/README.md +83 -5
- package/package.json +6 -1
- package/src/adapters/cli-agent/acp-client.ts +60 -3
- package/src/adapters/cli-agent/cline/acp.ts +1 -7
- package/src/adapters/cli-agent/cline/adapter.ts +54 -151
- package/src/adapters/cli-agent/cline/settings.ts +38 -260
- package/src/adapters/cli-agent/cline/stream.ts +2 -216
- package/src/adapters/cli-agent/gemini/acp.ts +141 -9
- package/src/adapters/cli-agent/gemini/adapter.ts +179 -16
- package/src/adapters/cli-agent/gemini/live-models.ts +32 -3
- package/src/adapters/cli-agent/gemini/model-metadata.ts +100 -4
- package/src/adapters/cli-agent/gemini/runtime.ts +183 -0
- package/src/adapters/cli-agent/gemini/session-store.ts +202 -0
- package/src/adapters/cli-agent/protocol.ts +60 -9
- package/src/adapters/cli-agent/types.ts +1 -22
- package/src/cli/index.ts +83 -15
- package/src/lib/codex-catalog.ts +107 -13
- package/src/lib/codex-inject.ts +467 -0
- package/src/server/index.ts +30 -3
- package/src/server/responses.ts +91 -9
- package/src/server/websocket.ts +261 -0
- package/src/types.ts +12 -1
- package/src/adapters/cli-agent/turn.ts +0 -307
package/src/cli/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { startServer } from "../server";
|
|
|
20
20
|
import { loginClineAcp } from "../adapters/cli-agent/cline/acp";
|
|
21
21
|
import { runCodingAgentMcpServer } from "../adapters/cli-agent/mcp-server";
|
|
22
22
|
import { syncCodexCatalog } from "../lib/codex-catalog";
|
|
23
|
+
import { injectCodexConfig, recordCodexCatalogState, restoreCodexConfig } from "../lib/codex-inject";
|
|
23
24
|
|
|
24
25
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
|
|
@@ -61,7 +62,10 @@ function help(): string {
|
|
|
61
62
|
" provider list [--json] Show supported providers",
|
|
62
63
|
" start [--port <port>] [--host <host>] Start the proxy",
|
|
63
64
|
" status [--json] Check the running proxy",
|
|
64
|
-
" stop [--json] Stop the
|
|
65
|
+
" stop [--json] Stop the proxy and restore native Codex",
|
|
66
|
+
" restore [--json] Restore native Codex without stopping",
|
|
67
|
+
" restore back [--json] Re-point Codex at the running proxy",
|
|
68
|
+
" eject Alias of restore",
|
|
65
69
|
" codex sync [--json] Refresh the Codex model catalog",
|
|
66
70
|
" version Print the package version",
|
|
67
71
|
" help Show this help",
|
|
@@ -179,21 +183,27 @@ async function startCommand(args: string[]): Promise<number> {
|
|
|
179
183
|
config.port = parsed;
|
|
180
184
|
}
|
|
181
185
|
if (host !== undefined) config.host = host;
|
|
186
|
+
const running = startServer(config, { env });
|
|
187
|
+
try {
|
|
188
|
+
await injectCodexConfig({ env, endpoint: { host: running.host, port: running.port }, config });
|
|
189
|
+
} catch (error) {
|
|
190
|
+
await running.stop(true).catch(() => undefined);
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
182
193
|
try {
|
|
183
194
|
const catalog = await syncCodexCatalog({ env });
|
|
184
195
|
if (catalog.ok && catalog.written) {
|
|
196
|
+
await recordCodexCatalogState(env).catch(error => {
|
|
197
|
+
console.warn(`[opencodex] Codex catalog state was not journaled: ${error instanceof Error ? error.message : String(error)}`);
|
|
198
|
+
});
|
|
185
199
|
console.log(`Codex catalog refreshed: ${catalog.path}`);
|
|
186
200
|
console.log("Restart the Codex app/app-server to reload the model picker.");
|
|
187
|
-
if (!catalog.configured) {
|
|
188
|
-
console.log(`Activate it with: model_catalog_json = "${catalog.path}"`);
|
|
189
|
-
}
|
|
190
201
|
} else if (!catalog.ok) {
|
|
191
202
|
console.warn(`[opencodex] Codex catalog was not refreshed: ${catalog.message}`);
|
|
192
203
|
}
|
|
193
204
|
} catch (error) {
|
|
194
205
|
console.warn(`[opencodex] Codex catalog refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
195
206
|
}
|
|
196
|
-
const running = startServer(config, { env });
|
|
197
207
|
await writeRuntimeState({ pid: process.pid, port: running.port, host: running.host, startedAt: new Date().toISOString() }, env);
|
|
198
208
|
console.log(`opencodex listening on http://${running.host}:${running.port}/v1 (default provider: ${config.defaultProvider})`);
|
|
199
209
|
console.log(`Health: http://${running.host}:${running.port}/healthz`);
|
|
@@ -202,10 +212,22 @@ async function startCommand(args: string[]): Promise<number> {
|
|
|
202
212
|
console.log(`\nStopping proxy (${signal})...`);
|
|
203
213
|
await running.stop(true);
|
|
204
214
|
await removeRuntimeStateIfPid(process.pid, env);
|
|
205
|
-
|
|
215
|
+
let exitCode = 0;
|
|
216
|
+
try {
|
|
217
|
+
const restored = await restoreCodexConfig({ env });
|
|
218
|
+
if (!restored.success) {
|
|
219
|
+
exitCode = 1;
|
|
220
|
+
console.error(`ocx: Codex restore did not complete: ${restored.catalog.message}`);
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
exitCode = 1;
|
|
224
|
+
console.error(`ocx: Codex restore failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
225
|
+
}
|
|
226
|
+
process.exit(exitCode);
|
|
206
227
|
};
|
|
207
228
|
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
208
229
|
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
230
|
+
process.once("SIGHUP", () => void shutdown("SIGHUP"));
|
|
209
231
|
await new Promise<never>(() => undefined);
|
|
210
232
|
return 0;
|
|
211
233
|
}
|
|
@@ -232,9 +254,42 @@ async function codexCommand(args: string[]): Promise<number> {
|
|
|
232
254
|
}
|
|
233
255
|
console.log("Codex reload required: restart the Codex app or app-server model picker.");
|
|
234
256
|
}
|
|
257
|
+
if (result.ok) await recordCodexCatalogState(process.env).catch(() => undefined);
|
|
235
258
|
return 0;
|
|
236
259
|
}
|
|
237
260
|
|
|
261
|
+
async function restoreCommand(args: string[]): Promise<number> {
|
|
262
|
+
const env = process.env;
|
|
263
|
+
const back = args[0] === "back";
|
|
264
|
+
if (back) args.shift();
|
|
265
|
+
const json = takeFlag(args, "--json");
|
|
266
|
+
const removeProviderTable = takeFlag(args, "--remove-codex-provider-table");
|
|
267
|
+
if (args.length > 0) throw new ConfigError(`Unknown restore option: ${args[0]}`);
|
|
268
|
+
let result;
|
|
269
|
+
if (back) {
|
|
270
|
+
const state = await readRuntimeState(env);
|
|
271
|
+
if (!state) throw new ConfigError("Proxy is not running; start it before `ocx restore back`.");
|
|
272
|
+
const config = await loadConfig(env);
|
|
273
|
+
if (!isAlive(state.pid) || !await health(state.host, state.port, config, env)) {
|
|
274
|
+
throw new ConfigError("The recorded proxy is not healthy; refusing to restore Codex routing.");
|
|
275
|
+
}
|
|
276
|
+
result = await injectCodexConfig({
|
|
277
|
+
env,
|
|
278
|
+
endpoint: { host: state.host, port: state.port },
|
|
279
|
+
config,
|
|
280
|
+
});
|
|
281
|
+
result = { ...result, action: "restore-back" as const };
|
|
282
|
+
} else {
|
|
283
|
+
result = await restoreCodexConfig({ env, removeProviderTable });
|
|
284
|
+
}
|
|
285
|
+
if (json) console.log(JSON.stringify(result));
|
|
286
|
+
else {
|
|
287
|
+
console.log(result.config.message);
|
|
288
|
+
console.log(result.catalog.message);
|
|
289
|
+
}
|
|
290
|
+
return result.success ? 0 : 1;
|
|
291
|
+
}
|
|
292
|
+
|
|
238
293
|
async function statusCommand(args: string[]): Promise<number> {
|
|
239
294
|
const json = takeFlag(args, "--json");
|
|
240
295
|
if (args.length > 0) throw new ConfigError(`Unknown status option: ${args[0]}`);
|
|
@@ -257,16 +312,24 @@ async function stopCommand(args: string[]): Promise<number> {
|
|
|
257
312
|
if (args.length > 0) throw new ConfigError(`Unknown stop option: ${args[0]}`);
|
|
258
313
|
const state = await readRuntimeState();
|
|
259
314
|
if (!state) {
|
|
260
|
-
|
|
261
|
-
|
|
315
|
+
const restored = await restoreCodexConfig({ env: process.env });
|
|
316
|
+
if (json) console.log(JSON.stringify({ stopped: false, reason: "not-running", restore: restored }));
|
|
317
|
+
else {
|
|
318
|
+
console.log("Proxy is not running.");
|
|
319
|
+
console.log(restored.config.message);
|
|
320
|
+
}
|
|
262
321
|
return 1;
|
|
263
322
|
}
|
|
264
323
|
const config = await loadConfig();
|
|
265
324
|
if (!isAlive(state.pid)) {
|
|
266
325
|
await removeRuntimeStateIfPid(state.pid);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
326
|
+
const restored = await restoreCodexConfig({ env: process.env });
|
|
327
|
+
if (json) console.log(JSON.stringify({ stopped: true, stale: true, restore: restored }));
|
|
328
|
+
else {
|
|
329
|
+
console.log("Removed stale proxy state.");
|
|
330
|
+
console.log(restored.config.message);
|
|
331
|
+
}
|
|
332
|
+
return restored.success ? 0 : 1;
|
|
270
333
|
}
|
|
271
334
|
if (!await health(state.host, state.port, config, process.env)) {
|
|
272
335
|
throw new ConfigError(`Refusing to stop pid ${state.pid}: it is not the opencodex proxy recorded by this home.`);
|
|
@@ -276,9 +339,13 @@ async function stopCommand(args: string[]): Promise<number> {
|
|
|
276
339
|
while (isAlive(state.pid) && Date.now() < deadline) await Bun.sleep(50);
|
|
277
340
|
if (isAlive(state.pid)) process.kill(state.pid, "SIGKILL");
|
|
278
341
|
await removeRuntimeStateIfPid(state.pid);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
342
|
+
const restored = await restoreCodexConfig({ env: process.env });
|
|
343
|
+
if (json) console.log(JSON.stringify({ stopped: true, pid: state.pid, restore: restored }));
|
|
344
|
+
else {
|
|
345
|
+
console.log(`Stopped proxy (pid ${state.pid}).`);
|
|
346
|
+
console.log(restored.config.message);
|
|
347
|
+
}
|
|
348
|
+
return restored.success ? 0 : 1;
|
|
282
349
|
}
|
|
283
350
|
|
|
284
351
|
async function main(argv: string[]): Promise<number> {
|
|
@@ -301,7 +368,8 @@ async function main(argv: string[]): Promise<number> {
|
|
|
301
368
|
if (command === "start") return await startCommand(argv);
|
|
302
369
|
if (command === "status") return await statusCommand(argv);
|
|
303
370
|
if (command === "stop") return await stopCommand(argv);
|
|
304
|
-
|
|
371
|
+
if (command === "restore" || command === "eject") return await restoreCommand(argv);
|
|
372
|
+
throw new ConfigError(`Unknown command: ${command}. Expected login, provider, codex, start, status, stop, restore, eject, version, or help.`);
|
|
305
373
|
}
|
|
306
374
|
|
|
307
375
|
try {
|
package/src/lib/codex-catalog.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { discoverGeminiLiveModels } from "../adapters/cli-agent/gemini/live-mode
|
|
|
7
7
|
import type { ClineTurnDeps } from "../adapters/cli-agent/cline/adapter";
|
|
8
8
|
import type { GeminiTurnDeps } from "../adapters/cli-agent/gemini/adapter";
|
|
9
9
|
|
|
10
|
+
// Cline ACP descriptors expose no context source; this fallback is Cline-only.
|
|
10
11
|
const CATALOG_FALLBACK_CONTEXT = 128_000;
|
|
11
12
|
const BASE_INSTRUCTIONS = "You are a helpful coding assistant.";
|
|
12
13
|
const MANAGED_PREFIXES = ["cline-cli/", "gemini-cli/"] as const;
|
|
@@ -28,7 +29,7 @@ export interface CodexCatalogModel {
|
|
|
28
29
|
context_window: number;
|
|
29
30
|
max_context_window: number;
|
|
30
31
|
auto_compact_token_limit: number;
|
|
31
|
-
input_modalities:
|
|
32
|
+
input_modalities: Array<"text" | "image" | "audio">;
|
|
32
33
|
node_repl_disabled: false;
|
|
33
34
|
node_repl_auto_review_required: false;
|
|
34
35
|
include_plugin_usage_instructions: false;
|
|
@@ -36,14 +37,14 @@ export interface CodexCatalogModel {
|
|
|
36
37
|
include_skills_usage_instructions: false;
|
|
37
38
|
supports_reasoning_summaries: boolean;
|
|
38
39
|
default_reasoning_summary: string;
|
|
39
|
-
support_verbosity:
|
|
40
|
+
support_verbosity: boolean;
|
|
40
41
|
default_verbosity: "low";
|
|
41
42
|
apply_patch_tool_type: "freeform";
|
|
42
43
|
truncation_policy: { mode: "tokens"; limit: number };
|
|
43
44
|
supports_parallel_tool_calls: true;
|
|
44
45
|
supports_image_detail_original: false;
|
|
45
46
|
experimental_supported_tools: [];
|
|
46
|
-
effective_context_window_percent:
|
|
47
|
+
effective_context_window_percent: number;
|
|
47
48
|
comp_hash: null;
|
|
48
49
|
multi_agent_version: "v1";
|
|
49
50
|
auto_review_model_override: null;
|
|
@@ -115,6 +116,19 @@ export function buildCodexCatalogModel(providerId: "cline-cli" | "gemini-cli", m
|
|
|
115
116
|
const defaultLevel = model.defaultReasoningLevel ?? levels[0]?.effort ?? "low";
|
|
116
117
|
const isDefault = model.id === "default";
|
|
117
118
|
const providerLabel = providerId === "gemini-cli" ? "Gemini CLI" : "Cline CLI";
|
|
119
|
+
const contextWindow = model.contextWindowTokens ?? (providerId === "cline-cli" ? CATALOG_FALLBACK_CONTEXT : undefined);
|
|
120
|
+
if (contextWindow === undefined) {
|
|
121
|
+
throw new Error(`Gemini model "${model.id}" has no runtime context window.`);
|
|
122
|
+
}
|
|
123
|
+
const deterministicThreshold = providerId === "gemini-cli"
|
|
124
|
+
? model.compressionThreshold
|
|
125
|
+
: model.compressionThreshold ?? 0.9;
|
|
126
|
+
const compactLimit = deterministicThreshold !== undefined
|
|
127
|
+
? Math.max(1, Math.floor(contextWindow * deterministicThreshold))
|
|
128
|
+
: contextWindow;
|
|
129
|
+
const effectivePercent = deterministicThreshold !== undefined
|
|
130
|
+
? Math.max(1, Math.min(100, Math.round(deterministicThreshold * 100)))
|
|
131
|
+
: 100;
|
|
118
132
|
return {
|
|
119
133
|
slug: `${providerId}/${encodeRoutedId(model.id)}`,
|
|
120
134
|
display_name: model.name ?? (isDefault ? `${providerLabel} default` : model.id),
|
|
@@ -129,10 +143,10 @@ export function buildCodexCatalogModel(providerId: "cline-cli" | "gemini-cli", m
|
|
|
129
143
|
web_search_tool_type: "text_and_image",
|
|
130
144
|
supports_search_tool: false,
|
|
131
145
|
tool_mode: "code_mode_only",
|
|
132
|
-
context_window:
|
|
133
|
-
max_context_window:
|
|
134
|
-
auto_compact_token_limit:
|
|
135
|
-
input_modalities: ["text"],
|
|
146
|
+
context_window: contextWindow,
|
|
147
|
+
max_context_window: model.maxContextWindowTokens ?? contextWindow,
|
|
148
|
+
auto_compact_token_limit: compactLimit,
|
|
149
|
+
input_modalities: [...(model.inputModalities ?? ["text"])],
|
|
136
150
|
node_repl_disabled: false,
|
|
137
151
|
node_repl_auto_review_required: false,
|
|
138
152
|
include_plugin_usage_instructions: false,
|
|
@@ -140,14 +154,14 @@ export function buildCodexCatalogModel(providerId: "cline-cli" | "gemini-cli", m
|
|
|
140
154
|
include_skills_usage_instructions: false,
|
|
141
155
|
supports_reasoning_summaries: levels.length > 0,
|
|
142
156
|
default_reasoning_summary: levels.length > 0 ? "auto" : "none",
|
|
143
|
-
support_verbosity:
|
|
157
|
+
support_verbosity: false,
|
|
144
158
|
default_verbosity: "low",
|
|
145
159
|
apply_patch_tool_type: "freeform",
|
|
146
160
|
truncation_policy: { mode: "tokens", limit: 10_000 },
|
|
147
161
|
supports_parallel_tool_calls: true,
|
|
148
162
|
supports_image_detail_original: false,
|
|
149
163
|
experimental_supported_tools: [],
|
|
150
|
-
effective_context_window_percent:
|
|
164
|
+
effective_context_window_percent: effectivePercent,
|
|
151
165
|
comp_hash: null,
|
|
152
166
|
multi_agent_version: "v1",
|
|
153
167
|
auto_review_model_override: null,
|
|
@@ -194,11 +208,30 @@ export async function discoverCodexCatalogModels(options: CodexCatalogDiscoveryO
|
|
|
194
208
|
if (gemini.ok && gemini.metadataWarning) warnings.push(`Gemini metadata: ${gemini.metadataWarning}`);
|
|
195
209
|
const clineModels = normalizedModels(cline.ok ? cline.models.map(id => ({ id })) : []);
|
|
196
210
|
const geminiModels = normalizedModels(gemini.ok ? gemini.modelInfo : []);
|
|
211
|
+
const geminiDefault = geminiModels.find(model => model.id === "default");
|
|
212
|
+
const geminiContexts = geminiModels.flatMap(model => model.contextWindowTokens !== undefined ? [model.contextWindowTokens] : []);
|
|
213
|
+
if (geminiDefault && geminiContexts.length > 0) {
|
|
214
|
+
const conservative = Math.min(...geminiContexts);
|
|
215
|
+
geminiDefault.contextWindowTokens = conservative;
|
|
216
|
+
geminiDefault.maxContextWindowTokens = conservative;
|
|
217
|
+
geminiDefault.compressionThreshold = geminiModels.find(model => model.compressionThreshold !== undefined)?.compressionThreshold;
|
|
218
|
+
const modalities = geminiModels.flatMap(model => model.inputModalities ?? []);
|
|
219
|
+
geminiDefault.inputModalities = [...new Set(modalities.length > 0 ? modalities : ["text" as const])]
|
|
220
|
+
.filter((value): value is "text" | "image" | "audio" => value === "text" || value === "image" || value === "audio");
|
|
221
|
+
}
|
|
222
|
+
const models: CodexCatalogModel[] = [];
|
|
223
|
+
for (const model of clineModels) {
|
|
224
|
+
models.push(buildCodexCatalogModel("cline-cli", model, models.length));
|
|
225
|
+
}
|
|
226
|
+
for (const model of geminiModels) {
|
|
227
|
+
if (model.contextWindowTokens === undefined) {
|
|
228
|
+
warnings.push(`Gemini model "${model.id}" has no runtime context window; omitted from the Codex catalog.`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
models.push(buildCodexCatalogModel("gemini-cli", model, models.length));
|
|
232
|
+
}
|
|
197
233
|
return {
|
|
198
|
-
models
|
|
199
|
-
...clineModels.map((model, index) => buildCodexCatalogModel("cline-cli", model, index)),
|
|
200
|
-
...geminiModels.map((model, index) => buildCodexCatalogModel("gemini-cli", model, clineModels.length + index)),
|
|
201
|
-
],
|
|
234
|
+
models,
|
|
202
235
|
warnings,
|
|
203
236
|
};
|
|
204
237
|
}
|
|
@@ -224,6 +257,67 @@ function readRootTomlString(text: string, key: string): string | undefined {
|
|
|
224
257
|
return match?.[1]?.trim() || undefined;
|
|
225
258
|
}
|
|
226
259
|
|
|
260
|
+
export function codexHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
261
|
+
return env.CODEX_HOME?.trim() || join(env.HOME?.trim() || homedir(), ".codex");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Resolve the catalog only when opencodex is allowed to manage that file. */
|
|
265
|
+
export async function resolveCodexCatalogPath(
|
|
266
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
267
|
+
): Promise<string | undefined> {
|
|
268
|
+
const home = codexHome(env);
|
|
269
|
+
let configured: string | undefined;
|
|
270
|
+
try {
|
|
271
|
+
configured = readRootTomlString(await readFile(join(home, "config.toml"), "utf8"), "model_catalog_json");
|
|
272
|
+
} catch {
|
|
273
|
+
configured = undefined;
|
|
274
|
+
}
|
|
275
|
+
if (configured && basename(configured) !== "opencodex-catalog.json") return undefined;
|
|
276
|
+
return configured ? resolve(home, configured) : join(home, "opencodex-catalog.json");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function isManagedCodexCatalogSlug(slug: unknown): boolean {
|
|
280
|
+
return managedSlug(slug);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Remove only opencodex-routed rows; preserve native/user rows. */
|
|
284
|
+
export async function removeManagedCodexCatalogRows(path: string): Promise<{
|
|
285
|
+
changed: boolean;
|
|
286
|
+
removed: number;
|
|
287
|
+
message: string;
|
|
288
|
+
}> {
|
|
289
|
+
let raw: string;
|
|
290
|
+
try {
|
|
291
|
+
raw = await readFile(path, "utf8");
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
294
|
+
return { changed: false, removed: 0, message: "No opencodex catalog file was present." };
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
let parsed: Record<string, unknown>;
|
|
299
|
+
try {
|
|
300
|
+
const value = asRecord(JSON.parse(raw));
|
|
301
|
+
if (!value) throw new Error("catalog is not an object");
|
|
302
|
+
parsed = value;
|
|
303
|
+
} catch {
|
|
304
|
+
throw new Error("The opencodex catalog is malformed; it was preserved.");
|
|
305
|
+
}
|
|
306
|
+
const models = Array.isArray(parsed.models) ? parsed.models : undefined;
|
|
307
|
+
if (!models || models.some(model => !asRecord(model))) {
|
|
308
|
+
throw new Error("The opencodex catalog has an invalid models field; it was preserved.");
|
|
309
|
+
}
|
|
310
|
+
const kept = models.filter(model => !managedSlug(asRecord(model)?.slug));
|
|
311
|
+
const removed = models.length - kept.length;
|
|
312
|
+
if (removed === 0) return { changed: false, removed: 0, message: "No managed catalog rows were present." };
|
|
313
|
+
if (kept.length === 0 && basename(path) === "opencodex-catalog.json") {
|
|
314
|
+
await rm(path, { force: true });
|
|
315
|
+
return { changed: true, removed, message: `Removed ${removed} managed catalog row(s) and the empty opencodex catalog.` };
|
|
316
|
+
}
|
|
317
|
+
await writeCodexCatalog(path, { ...parsed, models: kept } as CodexCatalogDocument);
|
|
318
|
+
return { changed: true, removed, message: `Removed ${removed} managed catalog row(s) from ${path}.` };
|
|
319
|
+
}
|
|
320
|
+
|
|
227
321
|
export async function writeCodexCatalog(
|
|
228
322
|
path: string,
|
|
229
323
|
document: CodexCatalogDocument,
|