codex-model-router 1.1.0 → 2.0.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/MAINTAINERS.md +92 -0
- package/README.md +84 -95
- package/SECURITY.md +21 -0
- package/lib/manifest.js +207 -0
- package/lib/router-core.js +882 -240
- package/lib/router.js +1 -210
- package/lib/toml.js +11 -15
- package/package.json +16 -5
package/lib/router.js
CHANGED
|
@@ -1,210 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import { run as runCore } from "./router-core.js";
|
|
6
|
-
|
|
7
|
-
export const VERSION = "1.1.0";
|
|
8
|
-
|
|
9
|
-
const SOL_TEMPLATE = `name = "sol"
|
|
10
|
-
description = "High-capability specialist for security-sensitive or high-regression-risk work."
|
|
11
|
-
model = "gpt-5.6-sol"
|
|
12
|
-
model_reasoning_effort = "medium"
|
|
13
|
-
# v1.0.0 used sandbox_mode = "read-only"
|
|
14
|
-
sandbox_mode = "workspace-write"
|
|
15
|
-
developer_instructions = """
|
|
16
|
-
Review or implement only when explicitly delegated by Terra.
|
|
17
|
-
For review-only tasks, report concrete findings without changing files.
|
|
18
|
-
For implementation tasks, make focused changes, run relevant checks, and report the result to Terra.
|
|
19
|
-
Focus on security, authentication, permissions, secrets, destructive actions, financial logic, SQL writes, concurrency, complex state, and regression risk.
|
|
20
|
-
Do not expand the scope beyond the delegated task.
|
|
21
|
-
"""
|
|
22
|
-
`;
|
|
23
|
-
|
|
24
|
-
const SKILL_TEMPLATE = `---
|
|
25
|
-
name: model-router
|
|
26
|
-
description: Route Codex work between Terra, Luna, and Sol with the fewest required agents.
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
Terra handles ordinary questions, coding, debugging, fixes, testing, and implementation. Never create a Terra subagent.
|
|
30
|
-
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.
|
|
31
|
-
Use Sol for security, authentication, authorization, permissions, secrets, destructive actions, financial logic, SQL writes, concurrency, complex state changes, high-regression-risk logic, or an explicit review. Prefer review-only delegation first; when implementation or a confirmed fix is explicitly required, Sol may edit files in workspace-write mode and run relevant checks.
|
|
32
|
-
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.
|
|
33
|
-
`;
|
|
34
|
-
|
|
35
|
-
function hash(content) {
|
|
36
|
-
return createHash("sha256").update(content).digest("hex");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function paths(options, global) {
|
|
40
|
-
const cwd = resolve(options.cwd ?? process.cwd());
|
|
41
|
-
const home = resolve(options.home ?? homedir());
|
|
42
|
-
const env = options.env ?? process.env;
|
|
43
|
-
const codexHome = global ? resolve(env.CODEX_HOME || join(home, ".codex")) : join(cwd, ".codex");
|
|
44
|
-
const skillsHome = global ? join(home, ".agents", "skills") : join(cwd, ".agents", "skills");
|
|
45
|
-
return {
|
|
46
|
-
state: join(codexHome, "model-router-state.json"),
|
|
47
|
-
sol: join(codexHome, "agents", "sol.toml"),
|
|
48
|
-
skill: join(skillsHome, "model-router", "SKILL.md")
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function readText(path) {
|
|
53
|
-
try { return await readFile(path, "utf8"); }
|
|
54
|
-
catch (error) {
|
|
55
|
-
if (error?.code === "ENOENT") return undefined;
|
|
56
|
-
throw error;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function atomicWrite(path, content) {
|
|
61
|
-
await mkdir(dirname(path), { recursive: true });
|
|
62
|
-
let mode = 0o600;
|
|
63
|
-
try { mode = (await stat(path)).mode; } catch (error) { if (error?.code !== "ENOENT") throw error; }
|
|
64
|
-
const temporary = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
65
|
-
let handle;
|
|
66
|
-
try {
|
|
67
|
-
handle = await open(temporary, "wx", mode);
|
|
68
|
-
await handle.writeFile(content, "utf8");
|
|
69
|
-
await handle.sync();
|
|
70
|
-
await handle.close();
|
|
71
|
-
handle = undefined;
|
|
72
|
-
await rename(temporary, path);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
try { await handle?.close(); } catch {}
|
|
75
|
-
try { await rm(temporary, { force: true }); } catch {}
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function upgradeManagedTemplates(options, global) {
|
|
81
|
-
const location = paths(options, global);
|
|
82
|
-
const stateText = await readText(location.state);
|
|
83
|
-
if (!stateText) return;
|
|
84
|
-
|
|
85
|
-
const state = JSON.parse(stateText);
|
|
86
|
-
const updates = [];
|
|
87
|
-
for (const [name, path, template] of [
|
|
88
|
-
["sol", location.sol, SOL_TEMPLATE],
|
|
89
|
-
["skill", location.skill, SKILL_TEMPLATE]
|
|
90
|
-
]) {
|
|
91
|
-
const current = await readText(path);
|
|
92
|
-
const tracked = state.files?.[name];
|
|
93
|
-
if (!current || !tracked || hash(current) !== tracked.hash || current === template) continue;
|
|
94
|
-
updates.push({ name, path, before: current, after: template });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const nextState = structuredClone(state);
|
|
98
|
-
nextState.packageVersion = VERSION;
|
|
99
|
-
for (const update of updates) nextState.files[update.name].hash = hash(update.after);
|
|
100
|
-
const nextStateText = `${JSON.stringify(nextState, null, 2)}\n`;
|
|
101
|
-
if (!updates.length && state.packageVersion === VERSION) return;
|
|
102
|
-
|
|
103
|
-
const snapshots = new Map([[location.state, stateText]]);
|
|
104
|
-
for (const update of updates) snapshots.set(update.path, update.before);
|
|
105
|
-
const applied = [];
|
|
106
|
-
try {
|
|
107
|
-
for (const update of updates) {
|
|
108
|
-
await atomicWrite(update.path, update.after);
|
|
109
|
-
applied.push(update.path);
|
|
110
|
-
}
|
|
111
|
-
await atomicWrite(location.state, nextStateText);
|
|
112
|
-
applied.push(location.state);
|
|
113
|
-
} catch (error) {
|
|
114
|
-
for (const path of applied.reverse()) {
|
|
115
|
-
try { await atomicWrite(path, snapshots.get(path)); } catch {}
|
|
116
|
-
}
|
|
117
|
-
throw error;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function parseGlobal(argv) {
|
|
122
|
-
return argv.includes("--global");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function activeValue(content, key) {
|
|
126
|
-
const match = content.match(new RegExp(`^\\s*${key}\\s*=\\s*"([^"]+)"\\s*$`, "m"));
|
|
127
|
-
return match?.[1];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function validateV11(options, global) {
|
|
131
|
-
const location = paths(options, global);
|
|
132
|
-
const stateText = await readText(location.state);
|
|
133
|
-
if (!stateText) return null;
|
|
134
|
-
|
|
135
|
-
let state;
|
|
136
|
-
try { state = JSON.parse(stateText); }
|
|
137
|
-
catch { return { sol: { status: "unsafe-state", detail: "state file is not valid JSON" }, skill: null }; }
|
|
138
|
-
|
|
139
|
-
const result = {};
|
|
140
|
-
const sol = await readText(location.sol);
|
|
141
|
-
if (!sol) result.sol = { status: "missing" };
|
|
142
|
-
else if (state.files?.sol?.hash !== hash(sol)) result.sol = { status: "user-modified", detail: "managed file hash changed" };
|
|
143
|
-
else if (
|
|
144
|
-
activeValue(sol, "name") !== "sol" ||
|
|
145
|
-
activeValue(sol, "model") !== "gpt-5.6-sol" ||
|
|
146
|
-
activeValue(sol, "model_reasoning_effort") !== "medium" ||
|
|
147
|
-
activeValue(sol, "sandbox_mode") !== "workspace-write"
|
|
148
|
-
) result.sol = { status: "invalid", detail: "expected Sol/medium/workspace-write" };
|
|
149
|
-
else result.sol = { status: "healthy" };
|
|
150
|
-
|
|
151
|
-
const skill = await readText(location.skill);
|
|
152
|
-
if (!skill) result.skill = { status: "missing" };
|
|
153
|
-
else if (state.files?.skill?.hash !== hash(skill)) result.skill = { status: "user-modified", detail: "managed file hash changed" };
|
|
154
|
-
else {
|
|
155
|
-
const required = [/^name:\s*model-router\s*$/m, /Terra/i, /Luna/i, /Sol/i, /workspace-write/i, /simple question/i, /minimum number|fewest/i];
|
|
156
|
-
result.skill = required.every((pattern) => pattern.test(skill))
|
|
157
|
-
? { status: "healthy" }
|
|
158
|
-
: { status: "invalid", detail: "missing required routing rule" };
|
|
159
|
-
}
|
|
160
|
-
return result;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function format(status, label, detail) {
|
|
164
|
-
return `${status}: ${label}${detail ? ` (${detail})` : ""}`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function runDoctor(argv, options, output) {
|
|
168
|
-
const lines = [];
|
|
169
|
-
await runCore(argv, { ...options, output: (line) => lines.push(String(line)) });
|
|
170
|
-
if (lines.some((line) => line.startsWith("unsafe-state: state") || line.startsWith("missing: state"))) {
|
|
171
|
-
for (const line of lines) output(line);
|
|
172
|
-
return 1;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const v11 = await validateV11(options, parseGlobal(argv));
|
|
176
|
-
const filtered = lines.filter((line) => !/^(?:healthy|invalid|user-modified|missing): (?:sol|skill)\b/.test(line));
|
|
177
|
-
for (const line of filtered) output(line);
|
|
178
|
-
if (v11?.sol) output(format(v11.sol.status, "sol", v11.sol.detail));
|
|
179
|
-
if (v11?.skill) output(format(v11.skill.status, "skill", v11.skill.detail));
|
|
180
|
-
|
|
181
|
-
const statuses = [
|
|
182
|
-
...filtered.map((line) => line.split(":", 1)[0]),
|
|
183
|
-
v11?.sol?.status,
|
|
184
|
-
v11?.skill?.status
|
|
185
|
-
].filter(Boolean);
|
|
186
|
-
return statuses.every((status) => status === "healthy") ? 0 : 1;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export async function run(argv, options = {}) {
|
|
190
|
-
const output = options.output ?? console.log;
|
|
191
|
-
if (argv.length === 1 && ["--version", "-v"].includes(argv[0])) {
|
|
192
|
-
output(VERSION);
|
|
193
|
-
return 0;
|
|
194
|
-
}
|
|
195
|
-
if (argv[0] === "doctor") return runDoctor(argv, options, output);
|
|
196
|
-
|
|
197
|
-
const code = await runCore(argv, {
|
|
198
|
-
...options,
|
|
199
|
-
output: (line) => output(String(line).replace("codex-model-router 1.0.0", `codex-model-router ${VERSION}`))
|
|
200
|
-
});
|
|
201
|
-
if (code !== 0 || argv[0] !== "install" || argv.includes("--dry-run")) return code;
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
await upgradeManagedTemplates(options, parseGlobal(argv));
|
|
205
|
-
return 0;
|
|
206
|
-
} catch (error) {
|
|
207
|
-
output(`fail: Sol upgrade (${error.message})`);
|
|
208
|
-
return 1;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
1
|
+
export { run, VERSION } from "./router-core.js";
|
package/lib/toml.js
CHANGED
|
@@ -15,10 +15,6 @@ function consumeNewline(text, index) {
|
|
|
15
15
|
return index + 1;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function countNewline(text, index) {
|
|
19
|
-
return text[index] === "\r" && text[index + 1] === "\n" ? 2 : 1;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
18
|
function skipComment(text, index) {
|
|
23
19
|
while (index < text.length && !isNewline(text, index)) index += 1;
|
|
24
20
|
return index;
|
|
@@ -81,7 +77,7 @@ function parseKey(raw, line) {
|
|
|
81
77
|
const segments = [];
|
|
82
78
|
let index = 0;
|
|
83
79
|
while (index < raw.length) {
|
|
84
|
-
while (index < raw.length && /[
|
|
80
|
+
while (index < raw.length && /[ ]/.test(raw[index])) index += 1;
|
|
85
81
|
if (index >= raw.length) throw syntax("empty key", line);
|
|
86
82
|
let segment;
|
|
87
83
|
if (raw[index] === '"' || raw[index] === "'") {
|
|
@@ -113,7 +109,7 @@ function parseKey(raw, line) {
|
|
|
113
109
|
segment = raw.slice(start, index);
|
|
114
110
|
}
|
|
115
111
|
segments.push(segment);
|
|
116
|
-
while (index < raw.length && /[
|
|
112
|
+
while (index < raw.length && /[ ]/.test(raw[index])) index += 1;
|
|
117
113
|
if (index >= raw.length) break;
|
|
118
114
|
if (raw[index] !== ".") throw syntax("invalid dotted key", line);
|
|
119
115
|
index += 1;
|
|
@@ -134,7 +130,7 @@ function consumeQuoted(text, index, line, quote, multiline) {
|
|
|
134
130
|
if (multiline && isNewline(text, index + 1)) {
|
|
135
131
|
index = consumeNewline(text, index + 1);
|
|
136
132
|
line += 1;
|
|
137
|
-
while (index < text.length && (/[
|
|
133
|
+
while (index < text.length && (/[ ]/.test(text[index]) || isNewline(text, index))) {
|
|
138
134
|
if (isNewline(text, index)) { index = consumeNewline(text, index); line += 1; }
|
|
139
135
|
else index += 1;
|
|
140
136
|
}
|
|
@@ -160,12 +156,13 @@ function consumeQuoted(text, index, line, quote, multiline) {
|
|
|
160
156
|
line += 1;
|
|
161
157
|
} else {
|
|
162
158
|
const code = text.charCodeAt(index);
|
|
163
|
-
if (code < 0x20 && character !== "
|
|
159
|
+
if (code < 0x20 && character !== " ") throw syntax("control character in string", line);
|
|
164
160
|
index += 1;
|
|
165
161
|
}
|
|
166
162
|
}
|
|
167
163
|
throw syntax("unterminated string", line);
|
|
168
164
|
}
|
|
165
|
+
|
|
169
166
|
function scanValue(text, start, line) {
|
|
170
167
|
let index = start;
|
|
171
168
|
let square = 0;
|
|
@@ -209,7 +206,7 @@ function scanValue(text, start, line) {
|
|
|
209
206
|
curly -= 1;
|
|
210
207
|
if (curly < 0) throw syntax("unexpected }", line);
|
|
211
208
|
}
|
|
212
|
-
if (!/[
|
|
209
|
+
if (!/[ ]/.test(character)) lastSignificant = index;
|
|
213
210
|
index += 1;
|
|
214
211
|
}
|
|
215
212
|
|
|
@@ -222,8 +219,7 @@ function scanValue(text, start, line) {
|
|
|
222
219
|
commentStart,
|
|
223
220
|
lineEnd,
|
|
224
221
|
statementEnd,
|
|
225
|
-
next: statementEnd
|
|
226
|
-
newlines: lineEnd < text.length ? 1 : 0
|
|
222
|
+
next: statementEnd
|
|
227
223
|
};
|
|
228
224
|
}
|
|
229
225
|
|
|
@@ -262,11 +258,11 @@ function scanHeader(text, start, line) {
|
|
|
262
258
|
if (!keyRaw) throw syntax("empty table header", line);
|
|
263
259
|
parseKey(keyRaw, line);
|
|
264
260
|
index += closing.length;
|
|
265
|
-
while (index < text.length && /[
|
|
261
|
+
while (index < text.length && /[ ]/.test(text[index])) index += 1;
|
|
266
262
|
if (text[index] === "#") index = skipComment(text, index);
|
|
267
263
|
if (index < text.length && !isNewline(text, index)) throw syntax("unexpected content after table header", line);
|
|
268
264
|
const next = index < text.length ? consumeNewline(text, index) : index;
|
|
269
|
-
return { next
|
|
265
|
+
return { next };
|
|
270
266
|
}
|
|
271
267
|
|
|
272
268
|
function validateValueShape(raw, line) {
|
|
@@ -298,7 +294,7 @@ export function scanToml(text) {
|
|
|
298
294
|
|
|
299
295
|
while (index < text.length) {
|
|
300
296
|
const physicalLineStart = index;
|
|
301
|
-
while (index < text.length && /[
|
|
297
|
+
while (index < text.length && /[ ]/.test(text[index])) index += 1;
|
|
302
298
|
if (index >= text.length) break;
|
|
303
299
|
if (isNewline(text, index)) {
|
|
304
300
|
index = consumeNewline(text, index);
|
|
@@ -353,7 +349,7 @@ export function scanToml(text) {
|
|
|
353
349
|
const keyRaw = text.slice(keyStart, index).trim();
|
|
354
350
|
const keySegments = parseKey(keyRaw, line);
|
|
355
351
|
index += 1;
|
|
356
|
-
while (index < text.length && /[
|
|
352
|
+
while (index < text.length && /[ ]/.test(text[index])) index += 1;
|
|
357
353
|
const valueStart = index;
|
|
358
354
|
const value = scanValue(text, valueStart, line);
|
|
359
355
|
const rawValue = text.slice(valueStart, value.valueEnd);
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-model-router",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Safely install
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Safely install adaptive free-mode Codex routing for Terra, Luna, and Sol.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"codex-model-router": "./bin/codex-model-router.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"check": "node --check bin/codex-model-router.js && node --check lib/router.js && node --check lib/router-core.js && node --check lib/toml.js",
|
|
10
|
+
"check": "node --check bin/codex-model-router.js && node --check lib/manifest.js && node --check lib/router.js && node --check lib/router-core.js && node --check lib/toml.js && node --check scripts/release-metadata.js && node --check scripts/verify-published.js",
|
|
11
11
|
"test": "node --test",
|
|
12
12
|
"test:package": "node scripts/package-smoke.js"
|
|
13
13
|
},
|
|
@@ -18,14 +18,24 @@
|
|
|
18
18
|
"bin",
|
|
19
19
|
"lib",
|
|
20
20
|
"README.md",
|
|
21
|
+
"SECURITY.md",
|
|
22
|
+
"MAINTAINERS.md",
|
|
21
23
|
"LICENSE"
|
|
22
24
|
],
|
|
23
25
|
"keywords": [
|
|
24
26
|
"codex",
|
|
27
|
+
"codex-cli",
|
|
25
28
|
"openai",
|
|
29
|
+
"model-router",
|
|
30
|
+
"subagent",
|
|
26
31
|
"subagents",
|
|
27
|
-
"
|
|
32
|
+
"agent-routing",
|
|
33
|
+
"skills",
|
|
34
|
+
"terra",
|
|
35
|
+
"luna",
|
|
36
|
+
"sol"
|
|
28
37
|
],
|
|
38
|
+
"author": "Honguan",
|
|
29
39
|
"repository": {
|
|
30
40
|
"type": "git",
|
|
31
41
|
"url": "https://github.com/Honguan/codex-model-router.git"
|
|
@@ -35,7 +45,8 @@
|
|
|
35
45
|
"url": "https://github.com/Honguan/codex-model-router/issues"
|
|
36
46
|
},
|
|
37
47
|
"publishConfig": {
|
|
38
|
-
"access": "public"
|
|
48
|
+
"access": "public",
|
|
49
|
+
"provenance": true
|
|
39
50
|
},
|
|
40
51
|
"license": "MIT"
|
|
41
52
|
}
|