codex-model-router 1.1.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.
@@ -0,0 +1,679 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ access,
4
+ mkdir,
5
+ open,
6
+ readFile,
7
+ rename,
8
+ rm,
9
+ rmdir,
10
+ stat
11
+ } from "node:fs/promises";
12
+ import { homedir } from "node:os";
13
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
14
+ import { applyEdits, insertionEdit, parseStringValue, removalEdit, scanToml } from "./toml.js";
15
+
16
+ export const VERSION = "1.0.0";
17
+
18
+ const DEFAULTS = {
19
+ model: "gpt-5.6-terra",
20
+ model_reasoning_effort: "high"
21
+ };
22
+
23
+ const TEMPLATES = {
24
+ luna: {
25
+ relative: ["agents", "luna.toml"],
26
+ content: `name = "luna"
27
+ description = "Low-risk helper for repeated edits, searches, formatting, extraction, counting, and summaries."
28
+ model = "gpt-5.6-luna"
29
+ model_reasoning_effort = "high"
30
+ developer_instructions = """
31
+ Handle only deterministic, low-risk work delegated by Terra.
32
+ Follow the assigned pattern exactly and return a concise result.
33
+ Escalate ambiguous, security-sensitive, or logic-heavy decisions to Terra.
34
+ """
35
+ `
36
+ },
37
+ sol: {
38
+ relative: ["agents", "sol.toml"],
39
+ content: `name = "sol"
40
+ description = "Read-only reviewer for security-sensitive or high-regression-risk logic."
41
+ model = "gpt-5.6-sol"
42
+ model_reasoning_effort = "medium"
43
+ sandbox_mode = "read-only"
44
+ developer_instructions = """
45
+ Review only. Focus on security, authentication, permissions, secrets, destructive actions, financial logic, SQL writes, concurrency, complex state, and regression risk.
46
+ Report concrete findings to Terra; do not apply fixes.
47
+ """
48
+ `
49
+ },
50
+ skill: {
51
+ content: `---
52
+ name: model-router
53
+ description: Route Codex work between Terra, Luna, and Sol with the fewest required agents.
54
+ ---
55
+
56
+ Terra handles ordinary questions, coding, debugging, fixes, testing, and implementation. Never create a Terra subagent.
57
+ Use Luna only for deterministic low-risk repeated edits, bulk patterns, read-heavy searches, formatting, counting, extraction, or summaries; prefer Luna when the same clear operation repeats at least three times.
58
+ Use Sol only as a read-only reviewer for security, authentication, authorization, permissions, secrets, destructive actions, financial logic, SQL writes, concurrency, complex state changes, high-regression-risk logic, or an explicit review. Terra applies fixes.
59
+ Do not spawn a subagent for a simple question. Use the minimum number of agents and run Luna with Sol only when both independent tasks are required.
60
+ `
61
+ }
62
+ };
63
+
64
+ function hash(content) {
65
+ return createHash("sha256").update(content).digest("hex");
66
+ }
67
+
68
+ function quoteToml(value) {
69
+ return JSON.stringify(value);
70
+ }
71
+
72
+ function expectedPaths({ cwd, home, global, env }) {
73
+ const userHome = resolve(home ?? homedir());
74
+ const projectRoot = resolve(cwd);
75
+ const codexHome = global
76
+ ? resolve(env?.CODEX_HOME || join(userHome, ".codex"))
77
+ : join(projectRoot, ".codex");
78
+ const skillsHome = global
79
+ ? join(userHome, ".agents", "skills")
80
+ : join(projectRoot, ".agents", "skills");
81
+ return {
82
+ scope: global ? "global" : "project",
83
+ projectRoot,
84
+ userHome,
85
+ codexHome,
86
+ skillsHome,
87
+ skillsRoot: dirname(skillsHome),
88
+ config: join(codexHome, "config.toml"),
89
+ state: join(codexHome, "model-router-state.json"),
90
+ backup: join(codexHome, "config.toml.codex-model-router.bak"),
91
+ luna: join(codexHome, ...TEMPLATES.luna.relative),
92
+ sol: join(codexHome, ...TEMPLATES.sol.relative),
93
+ skill: join(skillsHome, "model-router", "SKILL.md")
94
+ };
95
+ }
96
+
97
+ async function pathExists(path) {
98
+ try { await access(path); return true; } catch (error) {
99
+ if (error?.code === "ENOENT") return false;
100
+ throw error;
101
+ }
102
+ }
103
+
104
+ async function readText(path) {
105
+ try {
106
+ const bytes = await readFile(path);
107
+ const text = bytes.toString("utf8");
108
+ if (!Buffer.from(text, "utf8").equals(bytes)) {
109
+ const error = new Error(`${path} is not valid UTF-8`);
110
+ error.code = "INVALID_UTF8";
111
+ throw error;
112
+ }
113
+ return { exists: true, text };
114
+ } catch (error) {
115
+ if (error?.code === "ENOENT") return { exists: false, text: undefined };
116
+ throw error;
117
+ }
118
+ }
119
+
120
+ async function directoryExists(path) {
121
+ try { return (await stat(path)).isDirectory(); } catch (error) {
122
+ if (error?.code === "ENOENT") return false;
123
+ throw error;
124
+ }
125
+ }
126
+
127
+ async function atomicWrite(path, content) {
128
+ await mkdir(dirname(path), { recursive: true });
129
+ let mode = 0o600;
130
+ try { mode = (await stat(path)).mode; } catch (error) { if (error?.code !== "ENOENT") throw error; }
131
+ const temporary = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
132
+ let handle;
133
+ try {
134
+ handle = await open(temporary, "wx", mode);
135
+ await handle.writeFile(content, "utf8");
136
+ await handle.sync();
137
+ await handle.close();
138
+ handle = undefined;
139
+ await rename(temporary, path);
140
+ } catch (error) {
141
+ try { await handle?.close(); } catch {}
142
+ try { await rm(temporary, { force: true }); } catch {}
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ function isWithin(path, root) {
148
+ const value = relative(resolve(root), resolve(path));
149
+ return value === "" || (!value.startsWith(`..${sep}`) && value !== ".." && !isAbsolute(value));
150
+ }
151
+
152
+ function assertExactPath(actual, expected, label) {
153
+ if (resolve(actual) !== resolve(expected)) throw new Error(`unsafe state path for ${label}`);
154
+ }
155
+
156
+ function freshState(location) {
157
+ return {
158
+ version: 2,
159
+ packageVersion: VERSION,
160
+ scope: location.scope,
161
+ roots: { codex: location.codexHome, skills: location.skillsHome, skillsRoot: location.skillsRoot },
162
+ config: { path: location.config, createdFile: false, values: {} },
163
+ files: {},
164
+ backup: null,
165
+ createdDirs: []
166
+ };
167
+ }
168
+
169
+ function normalizeLegacyState(raw, location) {
170
+ const state = freshState(location);
171
+ if (!raw?.config || !raw?.files) throw new Error("invalid legacy state structure");
172
+ assertExactPath(raw.config.path, location.config, "config");
173
+ state.config.values = {};
174
+ for (const [key, item] of Object.entries(raw.config.values || {})) {
175
+ if (!Object.hasOwn(DEFAULTS, key) || item?.installed !== DEFAULTS[key]) throw new Error(`invalid legacy state value: ${key}`);
176
+ state.config.values[key] = {
177
+ installed: item.installed,
178
+ previousRaw: item.previous == null ? null : quoteToml(String(item.previous)),
179
+ source: item.previous == null ? "inserted" : "replaced"
180
+ };
181
+ }
182
+ for (const name of ["luna", "sol", "skill"]) {
183
+ const item = raw.files[name];
184
+ if (!item) continue;
185
+ assertExactPath(item.path, location[name], name);
186
+ state.files[name] = { path: location[name], hash: item.hash };
187
+ }
188
+ if (raw.backup) {
189
+ assertExactPath(raw.backup.path, location.backup, "backup");
190
+ state.backup = { path: location.backup, hash: raw.backup.hash };
191
+ }
192
+ return state;
193
+ }
194
+
195
+ function validateState(raw, location) {
196
+ if (!raw || typeof raw !== "object") throw new Error("state is not an object");
197
+ const state = raw.version ? structuredClone(raw) : normalizeLegacyState(raw, location);
198
+ if (state.version !== 2) throw new Error(`unsupported state version: ${state.version}`);
199
+ if (state.scope !== location.scope) throw new Error("state scope does not match this command");
200
+ assertExactPath(state.roots?.codex, location.codexHome, "Codex root");
201
+ assertExactPath(state.roots?.skills, location.skillsHome, "skills root");
202
+ assertExactPath(state.roots?.skillsRoot ?? dirname(state.roots?.skills || ""), location.skillsRoot, "skills parent root");
203
+ assertExactPath(state.config?.path, location.config, "config");
204
+ if (typeof state.config.createdFile !== "boolean" || typeof state.config.values !== "object") throw new Error("invalid config state");
205
+ for (const [key, item] of Object.entries(state.config.values)) {
206
+ if (!Object.hasOwn(DEFAULTS, key)) throw new Error(`unknown managed config key: ${key}`);
207
+ if (item?.installed !== DEFAULTS[key]) throw new Error(`invalid installed value for ${key}`);
208
+ if (!(item.previousRaw === null || typeof item.previousRaw === "string")) throw new Error(`invalid previous value for ${key}`);
209
+ }
210
+ state.files ||= {};
211
+ for (const [name, item] of Object.entries(state.files)) {
212
+ if (!Object.hasOwn(TEMPLATES, name)) throw new Error(`unknown managed file: ${name}`);
213
+ assertExactPath(item.path, location[name], name);
214
+ if (!/^[a-f0-9]{64}$/.test(item.hash || "")) throw new Error(`invalid hash for ${name}`);
215
+ }
216
+ if (state.backup) {
217
+ assertExactPath(state.backup.path, location.backup, "backup");
218
+ if (!/^[a-f0-9]{64}$/.test(state.backup.hash || "")) throw new Error("invalid backup hash");
219
+ }
220
+ if (!Array.isArray(state.createdDirs)) throw new Error("invalid createdDirs state");
221
+ for (const directory of state.createdDirs) {
222
+ if (!isWithin(directory, location.codexHome) && !isWithin(directory, location.skillsRoot)) {
223
+ throw new Error(`unsafe managed directory: ${directory}`);
224
+ }
225
+ }
226
+ state.packageVersion = VERSION;
227
+ return state;
228
+ }
229
+
230
+ async function loadState(location) {
231
+ const file = await readText(location.state);
232
+ if (!file.exists) return { exists: false, state: freshState(location) };
233
+ let raw;
234
+ try { raw = JSON.parse(file.text); } catch { throw new Error("state file is not valid JSON"); }
235
+ return { exists: true, state: validateState(raw, location) };
236
+ }
237
+
238
+ function rootAssignments(scan) {
239
+ const values = new Map();
240
+ for (const assignment of scan.assignments) {
241
+ if (assignment.table.length === 0 && assignment.keySegments.length === 1) {
242
+ const key = assignment.keySegments[0];
243
+ if (values.has(key)) throw new Error(`duplicate top-level ${key}`);
244
+ values.set(key, assignment);
245
+ }
246
+ }
247
+ return values;
248
+ }
249
+
250
+ function semanticValue(assignment) {
251
+ return assignment?.parsedValue?.kind === "string" ? assignment.parsedValue.value : null;
252
+ }
253
+
254
+ function addMessage(plan, status, label, detail) {
255
+ plan.messages.push({ status, label, detail });
256
+ }
257
+
258
+ function addWrite(plan, path, content, status, label) {
259
+ plan.operations.push({ kind: "write", path, content });
260
+ addMessage(plan, status, label);
261
+ }
262
+
263
+ function addDelete(plan, path, label) {
264
+ plan.operations.push({ kind: "delete", path });
265
+ addMessage(plan, "remove", label);
266
+ }
267
+
268
+ function addRemoveDirectory(plan, path) {
269
+ plan.operations.push({ kind: "rmdir", path });
270
+ addMessage(plan, "remove-if-empty", path);
271
+ }
272
+
273
+ function failPlan(plan, message) {
274
+ plan.failed = true;
275
+ addMessage(plan, "fail", "operation", message);
276
+ }
277
+
278
+ function stateJson(state) {
279
+ return `${JSON.stringify(state, null, 2)}\n`;
280
+ }
281
+
282
+ async function missingDirectories(location) {
283
+ const candidates = [
284
+ location.codexHome,
285
+ dirname(location.luna),
286
+ location.skillsRoot,
287
+ location.skillsHome,
288
+ dirname(location.skill)
289
+ ];
290
+ const unique = [...new Set(candidates.map((value) => resolve(value)))].sort((a, b) => a.length - b.length);
291
+ const missing = [];
292
+ for (const directory of unique) if (!(await directoryExists(directory))) missing.push(directory);
293
+ return missing;
294
+ }
295
+
296
+ function patchInstallConfig(original, state, setDefault) {
297
+ const scan = scanToml(original);
298
+ const edits = [];
299
+ const additions = [];
300
+ const nextValues = structuredClone(state.config.values || {});
301
+
302
+ for (const [key, installed] of Object.entries(DEFAULTS)) {
303
+ const assignment = scan.targets.get(key);
304
+ const tracked = nextValues[key];
305
+ if (assignment && assignment.parsedValue.kind !== "string") throw new Error(`${key} must be a TOML string`);
306
+ const current = semanticValue(assignment);
307
+
308
+ if (!assignment) {
309
+ additions.push(`${key} = ${quoteToml(installed)}`);
310
+ nextValues[key] = tracked || { installed, previousRaw: null, source: "inserted" };
311
+ continue;
312
+ }
313
+
314
+ if (!setDefault) {
315
+ if (tracked && current !== installed) {
316
+ // Keep ownership metadata so doctor can identify the later user edit.
317
+ }
318
+ continue;
319
+ }
320
+
321
+ if (current === installed) continue;
322
+ edits.push({ start: assignment.valueStart, end: assignment.valueEnd, text: quoteToml(installed) });
323
+ nextValues[key] = tracked || { installed, previousRaw: assignment.rawValue, source: "replaced" };
324
+ }
325
+
326
+ const insert = insertionEdit(original, scan, additions);
327
+ if (insert) edits.push(insert);
328
+ return { content: edits.length ? applyEdits(original, edits) : original, values: nextValues, changed: edits.length > 0 };
329
+ }
330
+
331
+ function patchUninstallConfig(original, stateValues) {
332
+ const scan = scanToml(original);
333
+ const edits = [];
334
+ const preserved = [];
335
+ for (const [key, item] of Object.entries(stateValues || {})) {
336
+ const assignment = scan.targets.get(key);
337
+ if (!assignment) continue;
338
+ if (semanticValue(assignment) !== item.installed) {
339
+ preserved.push(key);
340
+ continue;
341
+ }
342
+ if (item.previousRaw === null) edits.push(removalEdit(assignment));
343
+ else edits.push({ start: assignment.valueStart, end: assignment.valueEnd, text: item.previousRaw });
344
+ }
345
+ return { content: edits.length ? applyEdits(original, edits) : original, changed: edits.length > 0, preserved };
346
+ }
347
+
348
+ async function planInstall(scope, flags) {
349
+ const location = expectedPaths(scope);
350
+ const plan = { command: "install", location, operations: [], messages: [], failed: false };
351
+ let loaded;
352
+ try { loaded = await loadState(location); } catch (error) { failPlan(plan, error.message); return plan; }
353
+ const state = loaded.state;
354
+ const config = await readText(location.config);
355
+ const original = config.exists ? config.text : "";
356
+ let patched;
357
+ try { patched = patchInstallConfig(original, state, flags.setDefault); }
358
+ catch (error) { failPlan(plan, error.message); return plan; }
359
+
360
+ const createdDirs = await missingDirectories(location);
361
+ plan.createdDirs = createdDirs;
362
+ state.createdDirs = [...new Set([...(state.createdDirs || []), ...createdDirs])];
363
+ state.config.createdFile = state.config.createdFile || (!config.exists && patched.changed);
364
+ state.config.values = patched.values;
365
+
366
+ if (patched.changed) {
367
+ if (!state.backup && config.exists) {
368
+ const backup = await readText(location.backup);
369
+ if (backup.exists) {
370
+ failPlan(plan, `untracked backup already exists: ${location.backup}`);
371
+ return plan;
372
+ }
373
+ state.backup = { path: location.backup, hash: hash(original) };
374
+ addWrite(plan, location.backup, original, "create", "config backup");
375
+ }
376
+ addWrite(plan, location.config, patched.content, config.exists ? "update" : "create", "config.toml");
377
+ } else {
378
+ addMessage(plan, "preserve", "config.toml", flags.setDefault ? "already matches or contains protected values" : "existing defaults preserved");
379
+ }
380
+
381
+ for (const name of ["luna", "sol", "skill"]) {
382
+ const current = await readText(location[name]);
383
+ const tracked = state.files[name];
384
+ const template = TEMPLATES[name].content;
385
+ if (!current.exists) {
386
+ state.files[name] = { path: location[name], hash: hash(template) };
387
+ addWrite(plan, location[name], template, "create", name);
388
+ } else if (tracked && hash(current.text) === tracked.hash) {
389
+ addMessage(plan, "skip", name, "already managed");
390
+ } else {
391
+ addMessage(plan, "preserve", name, tracked ? "user-modified" : "pre-existing");
392
+ }
393
+ }
394
+
395
+ state.packageVersion = VERSION;
396
+ const existingState = await readText(location.state);
397
+ addWrite(plan, location.state, stateJson(state), existingState.exists ? "update" : "create", "state");
398
+ return plan;
399
+ }
400
+
401
+ async function planUninstall(scope) {
402
+ const location = expectedPaths(scope);
403
+ const plan = { command: "uninstall", location, operations: [], messages: [], failed: false };
404
+ let loaded;
405
+ try { loaded = await loadState(location); } catch (error) { failPlan(plan, error.message); return plan; }
406
+ if (!loaded.exists) {
407
+ addMessage(plan, "skip", "state", "not installed");
408
+ return plan;
409
+ }
410
+ const state = loaded.state;
411
+ const config = await readText(location.config);
412
+ if (Object.keys(state.config.values || {}).length && config.exists) {
413
+ let patched;
414
+ try { patched = patchUninstallConfig(config.text, state.config.values); }
415
+ catch (error) { failPlan(plan, error.message); return plan; }
416
+ for (const key of patched.preserved) addMessage(plan, "preserve", key, "user-modified");
417
+ if (patched.changed) {
418
+ const empty = patched.content.replace(/^\uFEFF/, "").trim() === "";
419
+ if (state.config.createdFile && empty) addDelete(plan, location.config, "config.toml");
420
+ else addWrite(plan, location.config, patched.content, "update", "config.toml");
421
+ } else if (!patched.preserved.length) {
422
+ addMessage(plan, "skip", "config.toml", "managed values already missing");
423
+ }
424
+ } else if (Object.keys(state.config.values || {}).length) {
425
+ addMessage(plan, "skip", "config.toml", "already missing");
426
+ }
427
+ state.config.values = {};
428
+
429
+ for (const name of ["luna", "sol", "skill"]) {
430
+ const tracked = state.files[name];
431
+ if (!tracked) continue;
432
+ const current = await readText(location[name]);
433
+ if (!current.exists) {
434
+ addMessage(plan, "skip", name, "already missing");
435
+ } else if (hash(current.text) === tracked.hash) {
436
+ addDelete(plan, location[name], name);
437
+ } else {
438
+ addMessage(plan, "preserve", name, "user-modified");
439
+ }
440
+ delete state.files[name];
441
+ }
442
+
443
+ if (state.backup) {
444
+ const backup = await readText(location.backup);
445
+ if (!backup.exists) addMessage(plan, "skip", "config backup", "already missing");
446
+ else if (hash(backup.text) === state.backup.hash) addDelete(plan, location.backup, "config backup");
447
+ else addMessage(plan, "preserve", "config backup", "user-modified");
448
+ state.backup = null;
449
+ }
450
+
451
+ addDelete(plan, location.state, "state");
452
+ for (const directory of [...state.createdDirs].sort((a, b) => b.length - a.length)) addRemoveDirectory(plan, directory);
453
+ state.createdDirs = [];
454
+ return plan;
455
+ }
456
+
457
+ async function snapshotPath(path) {
458
+ const current = await readText(path);
459
+ return current.exists ? { exists: true, text: current.text } : { exists: false };
460
+ }
461
+
462
+ async function executePlan(plan) {
463
+ if (plan.failed) return { ok: false };
464
+ const mutable = plan.operations.filter((operation) => operation.kind !== "rmdir");
465
+ const snapshots = new Map();
466
+ const applied = [];
467
+ try {
468
+ for (const operation of mutable) {
469
+ if (!snapshots.has(operation.path)) snapshots.set(operation.path, await snapshotPath(operation.path));
470
+ }
471
+ for (const operation of plan.operations) {
472
+ if (operation.kind === "write") await atomicWrite(operation.path, operation.content);
473
+ else if (operation.kind === "delete") await rm(operation.path, { force: true });
474
+ else if (operation.kind === "rmdir") {
475
+ try { await rmdir(operation.path); } catch (error) {
476
+ if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error?.code)) throw error;
477
+ }
478
+ }
479
+ applied.push(operation);
480
+ }
481
+ return { ok: true };
482
+ } catch (error) {
483
+ for (const operation of [...applied].reverse()) {
484
+ if (operation.kind === "rmdir") {
485
+ try { await mkdir(operation.path, { recursive: true }); } catch {}
486
+ continue;
487
+ }
488
+ const snapshot = snapshots.get(operation.path);
489
+ try {
490
+ if (snapshot?.exists) await atomicWrite(operation.path, snapshot.text);
491
+ else await rm(operation.path, { force: true });
492
+ } catch {}
493
+ }
494
+ for (const directory of [...(plan.createdDirs || [])].sort((a, b) => b.length - a.length)) {
495
+ try { await rmdir(directory); } catch {}
496
+ }
497
+ return { ok: false, error };
498
+ }
499
+ }
500
+
501
+ function formatMessage(message, dryRun) {
502
+ const preview = dryRun && ["create", "update", "remove", "remove-if-empty"].includes(message.status)
503
+ ? `would-${message.status}`
504
+ : message.status;
505
+ return `${preview}: ${message.label}${message.detail ? ` (${message.detail})` : ""}`;
506
+ }
507
+
508
+ function printPlan(plan, output, dryRun) {
509
+ for (const message of plan.messages) output?.(formatMessage(message, dryRun));
510
+ }
511
+
512
+ function getAgentValues(content) {
513
+ const scan = scanToml(content);
514
+ return rootAssignments(scan);
515
+ }
516
+
517
+ function getString(assignments, key) {
518
+ const assignment = assignments.get(key);
519
+ return assignment?.parsedValue?.kind === "string" ? assignment.parsedValue.value : null;
520
+ }
521
+
522
+ function validateSkill(content) {
523
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
524
+ if (!match) return "invalid front matter";
525
+ const front = match[1];
526
+ const body = match[2];
527
+ if (!/^name:\s*model-router\s*$/m.test(front)) return "incorrect skill name";
528
+ if (!/^description:\s*\S.+$/m.test(front)) return "missing skill description";
529
+ const requirements = [/Terra/i, /Luna/i, /Sol/i, /read-only/i, /simple question/i, /minimum number|fewest/i];
530
+ if (requirements.some((pattern) => !pattern.test(body))) return "missing required routing rule";
531
+ return null;
532
+ }
533
+
534
+ function doctorStatus(messages, status, label, detail) {
535
+ messages.push({ status, label, detail });
536
+ return status === "healthy";
537
+ }
538
+
539
+ async function doctor(scope) {
540
+ const location = expectedPaths(scope);
541
+ const messages = [];
542
+ let healthy = true;
543
+ let loaded;
544
+ try { loaded = await loadState(location); }
545
+ catch (error) {
546
+ doctorStatus(messages, "unsafe-state", "state", error.message);
547
+ return { healthy: false, messages };
548
+ }
549
+ if (!loaded.exists) {
550
+ doctorStatus(messages, "missing", "state", "run install");
551
+ return { healthy: false, messages };
552
+ }
553
+ const state = loaded.state;
554
+ doctorStatus(messages, "healthy", "state", `schema v${state.version}`);
555
+
556
+ const config = await readText(location.config);
557
+ if (!config.exists) {
558
+ healthy = doctorStatus(messages, "missing", "config.toml");
559
+ } else {
560
+ try {
561
+ const scan = scanToml(config.text);
562
+ for (const [key, expected] of Object.entries(DEFAULTS)) {
563
+ const assignment = scan.targets.get(key);
564
+ if (!assignment) {
565
+ healthy = doctorStatus(messages, "missing", key) && healthy;
566
+ } else if (assignment.parsedValue.kind !== "string") {
567
+ healthy = doctorStatus(messages, "invalid", key, "must be a TOML string") && healthy;
568
+ } else if (assignment.parsedValue.value !== expected) {
569
+ const status = state.config.values[key] ? "user-modified" : "user-override";
570
+ healthy = doctorStatus(messages, status, key, assignment.parsedValue.value ?? assignment.rawValue) && healthy;
571
+ } else {
572
+ doctorStatus(messages, "healthy", key, expected);
573
+ }
574
+ }
575
+ } catch (error) {
576
+ healthy = doctorStatus(messages, "invalid", "config.toml", error.message) && healthy;
577
+ }
578
+ }
579
+
580
+ for (const name of ["luna", "sol"]) {
581
+ const current = await readText(location[name]);
582
+ if (!current.exists) {
583
+ healthy = doctorStatus(messages, "missing", name) && healthy;
584
+ continue;
585
+ }
586
+ try {
587
+ const values = getAgentValues(current.text);
588
+ const expected = name === "luna"
589
+ ? { name: "luna", model: "gpt-5.6-luna", model_reasoning_effort: "high" }
590
+ : { name: "sol", model: "gpt-5.6-sol", model_reasoning_effort: "medium", sandbox_mode: "read-only" };
591
+ const invalid = Object.entries(expected).find(([key, value]) => getString(values, key) !== value);
592
+ if (state.files[name] && hash(current.text) !== state.files[name].hash) {
593
+ const detail = invalid ? `${invalid[0]} is no longer ${invalid[1]}` : "managed file hash changed";
594
+ healthy = doctorStatus(messages, "user-modified", name, detail) && healthy;
595
+ } else if (invalid) {
596
+ healthy = doctorStatus(messages, "invalid", name, `${invalid[0]} must be ${invalid[1]}`) && healthy;
597
+ } else {
598
+ doctorStatus(messages, "healthy", name);
599
+ }
600
+ } catch (error) {
601
+ healthy = doctorStatus(messages, "invalid", name, error.message) && healthy;
602
+ }
603
+ }
604
+
605
+ const skill = await readText(location.skill);
606
+ if (!skill.exists) healthy = doctorStatus(messages, "missing", "skill") && healthy;
607
+ else {
608
+ const invalid = validateSkill(skill.text);
609
+ if (state.files.skill && hash(skill.text) !== state.files.skill.hash) {
610
+ healthy = doctorStatus(messages, "user-modified", "skill", invalid || "managed file hash changed") && healthy;
611
+ } else if (invalid) healthy = doctorStatus(messages, "invalid", "skill", invalid) && healthy;
612
+ else doctorStatus(messages, "healthy", "skill");
613
+ }
614
+
615
+ if (state.backup) {
616
+ const backup = await readText(location.backup);
617
+ if (!backup.exists) healthy = doctorStatus(messages, "missing", "config backup") && healthy;
618
+ else if (hash(backup.text) !== state.backup.hash) healthy = doctorStatus(messages, "user-modified", "config backup") && healthy;
619
+ else doctorStatus(messages, "healthy", "config backup");
620
+ }
621
+
622
+ return { healthy, messages };
623
+ }
624
+
625
+ function usage() {
626
+ return `codex-model-router ${VERSION}\n\nUsage:\n codex-model-router install [--global] [--set-default] [--dry-run]\n codex-model-router uninstall [--global] [--dry-run]\n codex-model-router doctor [--global]\n codex-model-router --version`;
627
+ }
628
+
629
+ function parseArgs(argv) {
630
+ if (argv.length === 1 && ["--version", "-v"].includes(argv[0])) return { version: true };
631
+ if (!argv.length || argv.includes("--help") || argv.includes("-h")) return { help: true };
632
+ const [command, ...rawFlags] = argv;
633
+ if (!["install", "uninstall", "doctor"].includes(command)) return { error: "unknown command" };
634
+ const flags = { global: false, dryRun: false, setDefault: false };
635
+ for (const flag of rawFlags) {
636
+ if (flag === "--global") flags.global = true;
637
+ else if (flag === "--dry-run" && command !== "doctor") flags.dryRun = true;
638
+ else if (flag === "--set-default" && command === "install") flags.setDefault = true;
639
+ else return { error: `unsupported option for ${command}: ${flag}` };
640
+ }
641
+ return { command, flags };
642
+ }
643
+
644
+ export async function run(argv, options = {}) {
645
+ const output = options.output ?? console.log;
646
+ const parsed = parseArgs(argv);
647
+ if (parsed.version) { output(VERSION); return 0; }
648
+ if (parsed.help) { output(usage()); return 0; }
649
+ if (parsed.error) { output(`${parsed.error}\n\n${usage()}`); return 1; }
650
+ const scope = {
651
+ cwd: options.cwd ?? process.cwd(),
652
+ home: options.home,
653
+ env: options.env ?? process.env,
654
+ global: parsed.flags.global
655
+ };
656
+
657
+ try {
658
+ if (parsed.command === "doctor") {
659
+ const result = await doctor(scope);
660
+ for (const message of result.messages) output(formatMessage(message, false));
661
+ return result.healthy ? 0 : 1;
662
+ }
663
+ const plan = parsed.command === "install"
664
+ ? await planInstall(scope, parsed.flags)
665
+ : await planUninstall(scope);
666
+ printPlan(plan, output, parsed.flags.dryRun);
667
+ if (plan.failed) return 1;
668
+ if (parsed.flags.dryRun) return 0;
669
+ const result = await executePlan(plan);
670
+ if (!result.ok) {
671
+ output(`fail: execution (${result.error?.message || "unknown error"})`);
672
+ return 1;
673
+ }
674
+ return 0;
675
+ } catch (error) {
676
+ output(`fail: ${error.message}`);
677
+ return 1;
678
+ }
679
+ }