@dylanrussell/agent-router 1.0.6 → 1.0.7
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/README.md +26 -9
- package/dist/cli.js +376 -16
- package/dist/cli.js.map +1 -1
- package/dist/plugin.js +376 -16
- package/dist/plugin.js.map +1 -1
- package/dist/tui.js +367 -13
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,28 +29,29 @@ opencode agents are markdown files (`~/.config/opencode/agents/*.md`) whose YAML
|
|
|
29
29
|
---
|
|
30
30
|
description: High-reasoning review, debugging, and architecture counsel
|
|
31
31
|
mode: subagent
|
|
32
|
-
model: anthropic/claude-opus-4-8 # ←
|
|
32
|
+
model: anthropic/claude-opus-4-8 # ← agent-router rewrites this
|
|
33
|
+
reasoningEffort: high # ← and any option keys a stack names
|
|
33
34
|
temperature: 0.1
|
|
34
35
|
tools: { write: false, edit: false }
|
|
35
36
|
---
|
|
36
37
|
<prompt body — owned by you, never touched by agent-router>
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
`agent-router` keeps named **stacks** — JSON files mapping agent names to models — and applies them to those
|
|
40
|
+
`agent-router` keeps named **stacks** — JSON files mapping agent names to models (and optionally to provider options) — and applies them to those frontmatter lines on demand. Premium models for the workday, cheap ones for bulk chores, one command to swap the whole crew:
|
|
40
41
|
|
|
41
42
|
```json
|
|
42
43
|
{
|
|
43
44
|
"agents": {
|
|
44
45
|
"Omni": { "model": "anthropic/claude-fable-5" },
|
|
45
|
-
"oracle": { "model": "openai/gpt-5.5" },
|
|
46
|
+
"oracle": { "model": "openai/gpt-5.5", "reasoningEffort": "high" },
|
|
46
47
|
"explorer": { "model": "openai/gpt-5.4-mini" },
|
|
47
48
|
"librarian": { "model": "openai/gpt-5.4-mini" },
|
|
48
|
-
"fixer": { "model": "openai/gpt-5.5" }
|
|
49
|
+
"fixer": { "model": "openai/gpt-5.5", "thinking": { "effort": "low" } }
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
```
|
|
52
53
|
|
|
53
|
-
The prompt body and
|
|
54
|
+
The prompt body and opencode framework keys (`description`, `mode`, `permission`, `tools`, …) are yours; agent-router rewrites only the `model:` line and the option keys a stack entry names, atomically, through symlinks (dotfile-manager setups survive intact).
|
|
54
55
|
|
|
55
56
|
```
|
|
56
57
|
~/.config/opencode/
|
|
@@ -104,11 +105,27 @@ The everyday loop: tune your agents until you like them → `capture <name>` →
|
|
|
104
105
|
|
|
105
106
|
## Stacks
|
|
106
107
|
|
|
107
|
-
A stack file needs one thing: an `agents` record whose entries carry a `model` string. Agent names are the `.md` basenames in your agents dir (`Omni` ↔ `Omni.md`). Unknown keys are preserved round-trip.
|
|
108
|
+
A stack file needs one thing: an `agents` record whose entries carry a `model` string. Agent names are the `.md` basenames in your agents dir (`Omni` ↔ `Omni.md`). Any **other key** on an entry is a provider pass-through option (`reasoningEffort`, `thinking`, `temperature`, `topP`, `maxOutputTokens`, …) that opencode forwards to the model — agent-router transcribes these to the agent's frontmatter alongside `model:` and captures them back. Unknown keys are preserved round-trip.
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"agents": {
|
|
113
|
+
"oracle": {
|
|
114
|
+
"model": "openai/gpt-5.5",
|
|
115
|
+
"reasoningEffort": "high",
|
|
116
|
+
"thinking": { "effort": "low" }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- **Apply** writes/updates only the option keys a stack entry names; options the stack omits are left in place. To **clear** an option, set it to `null` in the stack entry.
|
|
123
|
+
- Reserved opencode framework keys (`description`, `mode`, `permission`, `tools`, `prompt`, `steps`, `color`, `name`) are never transcribed — a stack entry can't clobber them.
|
|
124
|
+
- Option values serialize to a single frontmatter line: scalars bare (quoted only when needed), objects/arrays as JSON flow. Block-style values an apply replaces are collapsed to flow; `capture` parses them back into typed JSON.
|
|
108
125
|
|
|
109
126
|
`use` is strict by design: if a stack references an agent file that doesn't exist, or one without a `model:` line, the switch fails **before anything is written** — your suite is never left half-switched. It also validates every model ID against `opencode models` first (skip with `--no-validate`, override with `--force-invalid`).
|
|
110
127
|
|
|
111
|
-
`capture` is the inverse: it reads the current `model:` line of every agent file (files without
|
|
128
|
+
`capture` is the inverse: it reads the current `model:` line and option keys of every agent file (files without a `model:` line are skipped) and writes a stack. There are no bundled seed stacks — your real setup is the seed.
|
|
112
129
|
|
|
113
130
|
## Inside opencode
|
|
114
131
|
|
|
@@ -167,12 +184,12 @@ Paths resolve in this order: explicit option → `config.json` → env var → d
|
|
|
167
184
|
## ⚠ Things to know
|
|
168
185
|
|
|
169
186
|
- **Restart required.** opencode reads agent files once at startup. After every `agent-router use`, restart opencode for the new models to take effect. The CLI reminds you.
|
|
170
|
-
- **Hand-edits to frontmatter are not auto-saved into stacks.** If you hand-tune a model and want to keep it, `capture` it (or `capture <active> --force`). The next `use` that touches that agent overwrites the hand-edit.
|
|
187
|
+
- **Hand-edits to frontmatter are not auto-saved into stacks.** If you hand-tune a model or option and want to keep it, `capture` it (or `capture <active> --force`). The next `use` that touches that agent overwrites the hand-edit for keys the stack names; keys the stack omits are preserved (set an option to `null` in a stack entry to clear it).
|
|
171
188
|
- **Validation is auth-state-dependent.** `agent-router validate` runs `opencode models`, which only lists models reachable through your current auth. If you revoke a key, previously-valid stacks may suddenly be invalid.
|
|
172
189
|
|
|
173
190
|
## FAQ
|
|
174
191
|
|
|
175
|
-
**Why no variants/fallback models?** Native agent frontmatter has a single `model:` line. If you want a fallback, make it a stack (`cheap`, `free`) and switch to it.
|
|
192
|
+
**Why no variants/fallback models?** Native agent frontmatter has a single `model:` line. If you want a fallback, make it a stack (`cheap`, `free`) and switch to it. For per-stack *behavior* variants (reasoning effort, thinking budget, temperature, …), add the option as a sibling key on the stack entry — it transcribes to frontmatter on `use`.
|
|
176
193
|
|
|
177
194
|
**Can I have per-project stacks?** Point `AGENT_ROUTER_STACKS_DIR` at a project-local directory in that project's shell env.
|
|
178
195
|
|
package/dist/cli.js
CHANGED
|
@@ -16086,8 +16086,300 @@ init_errors();
|
|
|
16086
16086
|
import { existsSync as existsSync3 } from "fs";
|
|
16087
16087
|
import { readFile as readFile2, readdir } from "fs/promises";
|
|
16088
16088
|
import path4 from "path";
|
|
16089
|
+
|
|
16090
|
+
// src/core/yaml-lite.ts
|
|
16091
|
+
var RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
16092
|
+
"null",
|
|
16093
|
+
"true",
|
|
16094
|
+
"false",
|
|
16095
|
+
"yes",
|
|
16096
|
+
"no",
|
|
16097
|
+
"on",
|
|
16098
|
+
"off",
|
|
16099
|
+
"~",
|
|
16100
|
+
// YAML 1.1 nulls
|
|
16101
|
+
"Null",
|
|
16102
|
+
"NULL",
|
|
16103
|
+
"True",
|
|
16104
|
+
"False",
|
|
16105
|
+
"Yes",
|
|
16106
|
+
"No",
|
|
16107
|
+
"On",
|
|
16108
|
+
"Off"
|
|
16109
|
+
]);
|
|
16110
|
+
var BARE_STRING_RE = /^[A-Za-z0-9._\-\/+@$%^&()~]+$/;
|
|
16111
|
+
function needsQuoting(s) {
|
|
16112
|
+
if (s === "") return true;
|
|
16113
|
+
if (RESERVED_WORDS.has(s)) return true;
|
|
16114
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(s)) return true;
|
|
16115
|
+
if (/^0x[0-9A-Fa-f]+$/.test(s)) return true;
|
|
16116
|
+
if (/^[+-]?\.(inf|Inf|INF|nan|NaN|NAN)$/.test(s)) return true;
|
|
16117
|
+
if (!BARE_STRING_RE.test(s)) return true;
|
|
16118
|
+
return false;
|
|
16119
|
+
}
|
|
16120
|
+
function serializeOptionValue(value) {
|
|
16121
|
+
if (value === null || value === void 0) return "null";
|
|
16122
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
16123
|
+
if (typeof value === "number") {
|
|
16124
|
+
if (!Number.isFinite(value)) return String(value);
|
|
16125
|
+
return JSON.stringify(value);
|
|
16126
|
+
}
|
|
16127
|
+
if (typeof value === "bigint") return String(value);
|
|
16128
|
+
if (typeof value === "string") return needsQuoting(value) ? JSON.stringify(value) : value;
|
|
16129
|
+
return JSON.stringify(value);
|
|
16130
|
+
}
|
|
16131
|
+
function stripComment(line) {
|
|
16132
|
+
let inSingle = false;
|
|
16133
|
+
let inDouble = false;
|
|
16134
|
+
for (let i = 0; i < line.length; i++) {
|
|
16135
|
+
const ch = line[i];
|
|
16136
|
+
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
16137
|
+
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
16138
|
+
else if (ch === "#" && !inSingle && !inDouble) {
|
|
16139
|
+
if (i === 0 || /\s/.test(line[i - 1] ?? "")) return line.slice(0, i);
|
|
16140
|
+
}
|
|
16141
|
+
}
|
|
16142
|
+
return line;
|
|
16143
|
+
}
|
|
16144
|
+
function tokenize(block) {
|
|
16145
|
+
const out = [];
|
|
16146
|
+
for (const raw of block.split(/\r?\n/)) {
|
|
16147
|
+
if (raw.trim() === "") continue;
|
|
16148
|
+
const indentMatch = /^( *)/.exec(raw);
|
|
16149
|
+
const indent = indentMatch?.[1]?.length ?? 0;
|
|
16150
|
+
const content = stripComment(raw.slice(indent));
|
|
16151
|
+
if (content.trim() === "") continue;
|
|
16152
|
+
out.push({ indent, text: content.trimEnd(), raw });
|
|
16153
|
+
}
|
|
16154
|
+
return out;
|
|
16155
|
+
}
|
|
16156
|
+
function coerceScalar(raw) {
|
|
16157
|
+
const v = raw.trim();
|
|
16158
|
+
if (v === "" || v === "null" || v === "~" || v === "Null" || v === "NULL") return null;
|
|
16159
|
+
if (v === "true" || v === "True" || v === "TRUE") return true;
|
|
16160
|
+
if (v === "false" || v === "False" || v === "FALSE") return false;
|
|
16161
|
+
if (v === "yes" || v === "Yes" || v === "YES") return true;
|
|
16162
|
+
if (v === "no" || v === "No" || v === "NO") return false;
|
|
16163
|
+
if (v === "on" || v === "On" || v === "ON") return true;
|
|
16164
|
+
if (v === "off" || v === "Off" || v === "OFF") return false;
|
|
16165
|
+
if (/^[+-]?\d+$/.test(v)) return Number.parseInt(v, 10);
|
|
16166
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(v)) return Number.parseFloat(v);
|
|
16167
|
+
if (/^0x[0-9A-Fa-f]+$/.test(v)) return Number.parseInt(v, 16);
|
|
16168
|
+
return v;
|
|
16169
|
+
}
|
|
16170
|
+
function parseQuoted(s) {
|
|
16171
|
+
const quote = s[0] ?? "";
|
|
16172
|
+
if (quote !== '"' && quote !== "'") return null;
|
|
16173
|
+
if (quote === '"') {
|
|
16174
|
+
let out2 = "";
|
|
16175
|
+
for (let i = 1; i < s.length; i++) {
|
|
16176
|
+
const ch = s[i] ?? "";
|
|
16177
|
+
if (ch === "\\" && i + 1 < s.length) {
|
|
16178
|
+
const next = s[i + 1] ?? "";
|
|
16179
|
+
const map2 = {
|
|
16180
|
+
n: "\n",
|
|
16181
|
+
t: " ",
|
|
16182
|
+
r: "\r",
|
|
16183
|
+
'"': '"',
|
|
16184
|
+
"\\": "\\",
|
|
16185
|
+
"/": "/",
|
|
16186
|
+
b: "\b",
|
|
16187
|
+
f: "\f"
|
|
16188
|
+
};
|
|
16189
|
+
out2 += map2[next] ?? next;
|
|
16190
|
+
i++;
|
|
16191
|
+
} else if (ch === '"') {
|
|
16192
|
+
return { value: out2, rest: s.slice(i + 1) };
|
|
16193
|
+
} else {
|
|
16194
|
+
out2 += ch;
|
|
16195
|
+
}
|
|
16196
|
+
}
|
|
16197
|
+
return null;
|
|
16198
|
+
}
|
|
16199
|
+
let out = "";
|
|
16200
|
+
for (let i = 1; i < s.length; i++) {
|
|
16201
|
+
const ch = s[i] ?? "";
|
|
16202
|
+
if (ch === "'") {
|
|
16203
|
+
if (s[i + 1] === "'") {
|
|
16204
|
+
out += "'";
|
|
16205
|
+
i++;
|
|
16206
|
+
} else {
|
|
16207
|
+
return { value: out, rest: s.slice(i + 1) };
|
|
16208
|
+
}
|
|
16209
|
+
} else {
|
|
16210
|
+
out += ch;
|
|
16211
|
+
}
|
|
16212
|
+
}
|
|
16213
|
+
return null;
|
|
16214
|
+
}
|
|
16215
|
+
function splitFlow(raw) {
|
|
16216
|
+
const parts = [];
|
|
16217
|
+
let depth = 0;
|
|
16218
|
+
let inSingle = false;
|
|
16219
|
+
let inDouble = false;
|
|
16220
|
+
let start = 0;
|
|
16221
|
+
for (let i = 0; i < raw.length; i++) {
|
|
16222
|
+
const ch = raw[i];
|
|
16223
|
+
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
16224
|
+
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
16225
|
+
else if (!inSingle && !inDouble) {
|
|
16226
|
+
if (ch === "{" || ch === "[") depth++;
|
|
16227
|
+
else if (ch === "}" || ch === "]") depth--;
|
|
16228
|
+
else if (ch === "," && depth === 0) {
|
|
16229
|
+
parts.push(raw.slice(start, i));
|
|
16230
|
+
start = i + 1;
|
|
16231
|
+
}
|
|
16232
|
+
}
|
|
16233
|
+
}
|
|
16234
|
+
parts.push(raw.slice(start));
|
|
16235
|
+
return parts;
|
|
16236
|
+
}
|
|
16237
|
+
function parseFlowValue(raw) {
|
|
16238
|
+
const s = raw.trim();
|
|
16239
|
+
if (s === "") return null;
|
|
16240
|
+
const first = s[0] ?? "";
|
|
16241
|
+
if (first === "{") {
|
|
16242
|
+
const inner = s.slice(1, s.length - 1);
|
|
16243
|
+
const obj = {};
|
|
16244
|
+
for (const part of splitFlow(inner)) {
|
|
16245
|
+
const t = part.trim();
|
|
16246
|
+
if (t === "") continue;
|
|
16247
|
+
const colon = findColon(t);
|
|
16248
|
+
if (colon < 0) {
|
|
16249
|
+
obj[t] = null;
|
|
16250
|
+
} else {
|
|
16251
|
+
const k = t.slice(0, colon).trim();
|
|
16252
|
+
const v = t.slice(colon + 1).trim();
|
|
16253
|
+
obj[stripKeyQuotes(k)] = parseFlowValue(v);
|
|
16254
|
+
}
|
|
16255
|
+
}
|
|
16256
|
+
return obj;
|
|
16257
|
+
}
|
|
16258
|
+
if (first === "[") {
|
|
16259
|
+
const inner = s.slice(1, s.length - 1);
|
|
16260
|
+
return splitFlow(inner).filter((p) => p.trim() !== "").map((p) => parseFlowValue(p));
|
|
16261
|
+
}
|
|
16262
|
+
if (first === '"' || first === "'") {
|
|
16263
|
+
const q = parseQuoted(s);
|
|
16264
|
+
return q ? q.value : s;
|
|
16265
|
+
}
|
|
16266
|
+
return coerceScalar(s);
|
|
16267
|
+
}
|
|
16268
|
+
function findColon(s) {
|
|
16269
|
+
let inSingle = false;
|
|
16270
|
+
let inDouble = false;
|
|
16271
|
+
let depth = 0;
|
|
16272
|
+
for (let i = 0; i < s.length; i++) {
|
|
16273
|
+
const ch = s[i];
|
|
16274
|
+
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
16275
|
+
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
16276
|
+
else if (!inSingle && !inDouble) {
|
|
16277
|
+
if (ch === "{" || ch === "[") depth++;
|
|
16278
|
+
else if (ch === "}" || ch === "]") depth--;
|
|
16279
|
+
else if (ch === ":" && depth === 0) return i;
|
|
16280
|
+
}
|
|
16281
|
+
}
|
|
16282
|
+
return -1;
|
|
16283
|
+
}
|
|
16284
|
+
function stripKeyQuotes(k) {
|
|
16285
|
+
if (k.length >= 2 && (k[0] === '"' && k.at(-1) === '"' || k[0] === "'" && k.at(-1) === "'")) {
|
|
16286
|
+
return k.slice(1, -1);
|
|
16287
|
+
}
|
|
16288
|
+
return k;
|
|
16289
|
+
}
|
|
16290
|
+
function parseMapping(lines, startIdx, indent) {
|
|
16291
|
+
const out = {};
|
|
16292
|
+
let i = startIdx;
|
|
16293
|
+
while (i < lines.length) {
|
|
16294
|
+
const line = lines[i];
|
|
16295
|
+
if (!line) break;
|
|
16296
|
+
if (line.indent < indent) break;
|
|
16297
|
+
if (line.indent > indent) {
|
|
16298
|
+
i++;
|
|
16299
|
+
continue;
|
|
16300
|
+
}
|
|
16301
|
+
const colon = findColon(line.text);
|
|
16302
|
+
if (colon < 0) {
|
|
16303
|
+
i++;
|
|
16304
|
+
continue;
|
|
16305
|
+
}
|
|
16306
|
+
const key = stripKeyQuotes(line.text.slice(0, colon).trim());
|
|
16307
|
+
if (key === "") {
|
|
16308
|
+
i++;
|
|
16309
|
+
continue;
|
|
16310
|
+
}
|
|
16311
|
+
const rest = line.text.slice(colon + 1).trim();
|
|
16312
|
+
if (rest !== "") {
|
|
16313
|
+
out[key] = parseFlowValue(rest);
|
|
16314
|
+
i++;
|
|
16315
|
+
} else {
|
|
16316
|
+
const childIndent = lines[i + 1]?.indent ?? line.indent + 1;
|
|
16317
|
+
let j = i + 1;
|
|
16318
|
+
while (j < lines.length && (lines[j]?.indent ?? -1) >= childIndent) j++;
|
|
16319
|
+
if (j === i + 1) {
|
|
16320
|
+
out[key] = null;
|
|
16321
|
+
i++;
|
|
16322
|
+
} else {
|
|
16323
|
+
const childBlock = lines.slice(i + 1, j);
|
|
16324
|
+
const firstChild = childBlock[0];
|
|
16325
|
+
if (firstChild?.text.trimStart().startsWith("- ")) {
|
|
16326
|
+
out[key] = parseSequence(childBlock, childIndent);
|
|
16327
|
+
} else {
|
|
16328
|
+
out[key] = parseMapping(childBlock, 0, childIndent).value;
|
|
16329
|
+
}
|
|
16330
|
+
i = j;
|
|
16331
|
+
}
|
|
16332
|
+
}
|
|
16333
|
+
}
|
|
16334
|
+
return { value: out, next: i };
|
|
16335
|
+
}
|
|
16336
|
+
function parseSequence(lines, indent) {
|
|
16337
|
+
const out = [];
|
|
16338
|
+
let i = 0;
|
|
16339
|
+
while (i < lines.length) {
|
|
16340
|
+
const line = lines[i];
|
|
16341
|
+
if (!line) break;
|
|
16342
|
+
if (line.indent < indent) break;
|
|
16343
|
+
const t = line.text.trimStart();
|
|
16344
|
+
if (!t.startsWith("- ")) {
|
|
16345
|
+
i++;
|
|
16346
|
+
continue;
|
|
16347
|
+
}
|
|
16348
|
+
const item = t.slice(2).trim();
|
|
16349
|
+
if (item === "") {
|
|
16350
|
+
const childIndent = lines[i + 1]?.indent ?? line.indent + 1;
|
|
16351
|
+
let j = i + 1;
|
|
16352
|
+
while (j < lines.length && (lines[j]?.indent ?? -1) >= childIndent) j++;
|
|
16353
|
+
out.push(parseMapping(lines.slice(i + 1, j), 0, childIndent).value);
|
|
16354
|
+
i = j;
|
|
16355
|
+
} else {
|
|
16356
|
+
out.push(parseFlowValue(item));
|
|
16357
|
+
i++;
|
|
16358
|
+
}
|
|
16359
|
+
}
|
|
16360
|
+
return out;
|
|
16361
|
+
}
|
|
16362
|
+
function parseFrontmatterBlock(block) {
|
|
16363
|
+
const lines = tokenize(block);
|
|
16364
|
+
if (lines.length === 0) return null;
|
|
16365
|
+
const { value } = parseMapping(lines, 0, 0);
|
|
16366
|
+
return value;
|
|
16367
|
+
}
|
|
16368
|
+
|
|
16369
|
+
// src/core/frontmatter.ts
|
|
16089
16370
|
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
16090
16371
|
var MODEL_LINE_RE = /^model:[ \t]*(.*)$/m;
|
|
16372
|
+
var RESERVED_AGENT_KEYS = /* @__PURE__ */ new Set([
|
|
16373
|
+
"name",
|
|
16374
|
+
"mode",
|
|
16375
|
+
"description",
|
|
16376
|
+
"permission",
|
|
16377
|
+
"color",
|
|
16378
|
+
"tools",
|
|
16379
|
+
"prompt",
|
|
16380
|
+
"steps",
|
|
16381
|
+
"maxSteps"
|
|
16382
|
+
]);
|
|
16091
16383
|
function cleanModelValue(raw) {
|
|
16092
16384
|
let v = raw;
|
|
16093
16385
|
const hash2 = v.search(/[ \t]#/);
|
|
@@ -16116,6 +16408,55 @@ function setFrontmatterModel(content, model) {
|
|
|
16116
16408
|
const nextFm = fm[0].slice(0, blockStart) + nextBlock + fm[0].slice(blockStart + block.length);
|
|
16117
16409
|
return nextFm + content.slice(fm[0].length);
|
|
16118
16410
|
}
|
|
16411
|
+
function getFrontmatterOptions(content) {
|
|
16412
|
+
const fm = FRONTMATTER_RE.exec(content);
|
|
16413
|
+
if (!fm?.[1]) return {};
|
|
16414
|
+
const parsed = parseFrontmatterBlock(fm[1]);
|
|
16415
|
+
if (!parsed) return {};
|
|
16416
|
+
const out = {};
|
|
16417
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
16418
|
+
if (k === "model" || RESERVED_AGENT_KEYS.has(k)) continue;
|
|
16419
|
+
out[k] = v;
|
|
16420
|
+
}
|
|
16421
|
+
return out;
|
|
16422
|
+
}
|
|
16423
|
+
function setFrontmatterOptions(content, options) {
|
|
16424
|
+
const fm = FRONTMATTER_RE.exec(content);
|
|
16425
|
+
if (!fm?.[1]) throw new Error("no frontmatter block");
|
|
16426
|
+
const block = fm[1];
|
|
16427
|
+
const keys = Object.keys(options);
|
|
16428
|
+
if (keys.length === 0) return content;
|
|
16429
|
+
const eol = block.includes("\r\n") ? "\r\n" : "\n";
|
|
16430
|
+
const lines = block.split(/\r?\n/);
|
|
16431
|
+
const handled = /* @__PURE__ */ new Set();
|
|
16432
|
+
for (let i = 0; i < lines.length; i++) {
|
|
16433
|
+
const m = /^([A-Za-z0-9_-]+):[ \t]*(.*)$/.exec(lines[i] ?? "");
|
|
16434
|
+
if (!m) continue;
|
|
16435
|
+
const key = m[1] ?? "";
|
|
16436
|
+
if (!(key in options)) continue;
|
|
16437
|
+
handled.add(key);
|
|
16438
|
+
let end = i + 1;
|
|
16439
|
+
while (end < lines.length && /^[ \t]+/.test(lines[end] ?? "")) end++;
|
|
16440
|
+
const value = options[key];
|
|
16441
|
+
if (value === null || value === void 0) {
|
|
16442
|
+
lines.splice(i, end - i);
|
|
16443
|
+
i--;
|
|
16444
|
+
} else {
|
|
16445
|
+
lines.splice(i, end - i, `${key}: ${serializeOptionValue(value)}`);
|
|
16446
|
+
}
|
|
16447
|
+
}
|
|
16448
|
+
const appended = [];
|
|
16449
|
+
for (const [key, value] of Object.entries(options)) {
|
|
16450
|
+
if (handled.has(key)) continue;
|
|
16451
|
+
if (value === null || value === void 0) continue;
|
|
16452
|
+
appended.push(`${key}: ${serializeOptionValue(value)}`);
|
|
16453
|
+
}
|
|
16454
|
+
const nextBlock = appended.length > 0 ? lines.join(eol) + eol + appended.join(eol) : lines.join(eol);
|
|
16455
|
+
if (nextBlock === block) return content;
|
|
16456
|
+
const blockStart = fm[0].indexOf(block);
|
|
16457
|
+
const nextFm = fm[0].slice(0, blockStart) + nextBlock + fm[0].slice(blockStart + block.length);
|
|
16458
|
+
return nextFm + content.slice(fm[0].length);
|
|
16459
|
+
}
|
|
16119
16460
|
function agentFilePath(agentsDir, name) {
|
|
16120
16461
|
return path4.join(agentsDir, `${name}.md`);
|
|
16121
16462
|
}
|
|
@@ -16129,7 +16470,7 @@ async function listAgentFiles(agentsDir) {
|
|
|
16129
16470
|
}
|
|
16130
16471
|
return names.filter((n) => n.endsWith(".md")).map((n) => n.slice(0, -".md".length)).sort();
|
|
16131
16472
|
}
|
|
16132
|
-
async function
|
|
16473
|
+
async function readAgentEntries(agentsDir) {
|
|
16133
16474
|
const out = {};
|
|
16134
16475
|
for (const name of await listAgentFiles(agentsDir)) {
|
|
16135
16476
|
const filePath = agentFilePath(agentsDir, name);
|
|
@@ -16140,10 +16481,17 @@ async function readAgentModels(agentsDir) {
|
|
|
16140
16481
|
throw new IOError(`Failed to read ${filePath}: ${cause.message}`, cause);
|
|
16141
16482
|
}
|
|
16142
16483
|
const model = getFrontmatterModel(content);
|
|
16143
|
-
if (model
|
|
16484
|
+
if (model === null) continue;
|
|
16485
|
+
out[name] = { model, options: getFrontmatterOptions(content) };
|
|
16144
16486
|
}
|
|
16145
16487
|
return out;
|
|
16146
16488
|
}
|
|
16489
|
+
async function readAgentModels(agentsDir) {
|
|
16490
|
+
const entries = await readAgentEntries(agentsDir);
|
|
16491
|
+
const out = {};
|
|
16492
|
+
for (const [name, entry] of Object.entries(entries)) out[name] = entry.model;
|
|
16493
|
+
return out;
|
|
16494
|
+
}
|
|
16147
16495
|
async function readAgentFileStrict(agentsDir, name) {
|
|
16148
16496
|
const filePath = agentFilePath(agentsDir, name);
|
|
16149
16497
|
if (!existsSync3(filePath)) {
|
|
@@ -16159,7 +16507,7 @@ async function readAgentFileStrict(agentsDir, name) {
|
|
|
16159
16507
|
if (model === null) {
|
|
16160
16508
|
throw new AgentFileError(name, filePath, "no frontmatter `model:` line to rewrite");
|
|
16161
16509
|
}
|
|
16162
|
-
return { filePath, content, model };
|
|
16510
|
+
return { filePath, content, model, options: getFrontmatterOptions(content) };
|
|
16163
16511
|
}
|
|
16164
16512
|
|
|
16165
16513
|
// src/core/history.ts
|
|
@@ -16428,15 +16776,14 @@ async function applyStack(paths, name, options = {}) {
|
|
|
16428
16776
|
const pending = [];
|
|
16429
16777
|
for (const [agent, entry] of Object.entries(target.agents)) {
|
|
16430
16778
|
const { filePath, content, model } = await readAgentFileStrict(paths.agentsDir, agent);
|
|
16431
|
-
|
|
16432
|
-
|
|
16433
|
-
|
|
16434
|
-
|
|
16435
|
-
});
|
|
16779
|
+
const wantOptions = entryOptions(entry);
|
|
16780
|
+
const afterOptions = setFrontmatterOptions(content, wantOptions);
|
|
16781
|
+
const nextContent = entry.model === model ? afterOptions : setFrontmatterModel(afterOptions, entry.model);
|
|
16782
|
+
pending.push({ agent, filePath, next: nextContent === content ? null : nextContent });
|
|
16436
16783
|
}
|
|
16437
16784
|
const prevState = await readState(paths.statePath);
|
|
16438
16785
|
const prevActive = prevState?.active ?? null;
|
|
16439
|
-
const displaced = { agents:
|
|
16786
|
+
const displaced = { agents: entriesToStackAgents(await readAgentEntries(paths.agentsDir)) };
|
|
16440
16787
|
const historyId = await appendHistory(
|
|
16441
16788
|
paths.historyDir,
|
|
16442
16789
|
prevActive ?? "(none)",
|
|
@@ -16466,11 +16813,24 @@ async function applyStack(paths, name, options = {}) {
|
|
|
16466
16813
|
restartRequired: true
|
|
16467
16814
|
};
|
|
16468
16815
|
}
|
|
16469
|
-
function
|
|
16816
|
+
function entryOptions(entry) {
|
|
16817
|
+
const rec = entry;
|
|
16470
16818
|
const out = {};
|
|
16471
|
-
for (const k of Object.
|
|
16472
|
-
|
|
16473
|
-
|
|
16819
|
+
for (const [k, v] of Object.entries(rec)) {
|
|
16820
|
+
if (k === "model" || RESERVED_AGENT_KEYS.has(k)) continue;
|
|
16821
|
+
out[k] = v;
|
|
16822
|
+
}
|
|
16823
|
+
return out;
|
|
16824
|
+
}
|
|
16825
|
+
function entriesToStackAgents(entries) {
|
|
16826
|
+
const out = {};
|
|
16827
|
+
for (const name of Object.keys(entries).sort()) {
|
|
16828
|
+
const entry = entries[name];
|
|
16829
|
+
if (!entry) continue;
|
|
16830
|
+
const { model, options } = entry;
|
|
16831
|
+
const stackEntry = { model };
|
|
16832
|
+
for (const [k, v] of Object.entries(options)) stackEntry[k] = v;
|
|
16833
|
+
out[name] = stackEntry;
|
|
16474
16834
|
}
|
|
16475
16835
|
return out;
|
|
16476
16836
|
}
|
|
@@ -16507,8 +16867,8 @@ async function captureStack(paths, name, options = {}) {
|
|
|
16507
16867
|
if (existsSync6(dest) && !options.force) {
|
|
16508
16868
|
throw new UserError(`Stack "${name}" already exists. Use --force to overwrite.`);
|
|
16509
16869
|
}
|
|
16510
|
-
const
|
|
16511
|
-
const agents =
|
|
16870
|
+
const entries = await readAgentEntries(paths.agentsDir);
|
|
16871
|
+
const agents = entriesToStackAgents(entries);
|
|
16512
16872
|
if (Object.keys(agents).length === 0) {
|
|
16513
16873
|
throw new UserError(
|
|
16514
16874
|
`No agent .md files with a frontmatter \`model:\` line found in ${paths.agentsDir}.`
|
|
@@ -16551,7 +16911,7 @@ async function exportStack(paths, name, toFile) {
|
|
|
16551
16911
|
}
|
|
16552
16912
|
|
|
16553
16913
|
// src/version.ts
|
|
16554
|
-
var VERSION = "1.0.
|
|
16914
|
+
var VERSION = "1.0.7";
|
|
16555
16915
|
|
|
16556
16916
|
// src/cli.ts
|
|
16557
16917
|
var log = (...args) => console.log(...args);
|