@dylanrussell/agent-router 1.0.5 → 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 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 # ← the ONLY line agent-router touches
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 `model:` lines on demand. Premium models for the workday, cheap ones for bulk chores, one command to swap the whole crew:
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 every other frontmatter key are yours; agent-router rewrites exactly one line per agent, atomically, through symlinks (dotfile-manager setups survive intact).
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 one are skipped) and writes a stack. There are no bundled seed stacks — your real setup is the seed.
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,15 +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
- - **Dotfile-manager users:** applied switches change your agent `.md` files — re-add/commit them (e.g. `chezmoi add`) when you want the change versioned.
173
189
 
174
190
  ## FAQ
175
191
 
176
- **Where did omo-router go?** This package is its successor. omo-router snapshotted `oh-my-openagent.json`; agent-router targets native opencode agent frontmatter instead no third-party plugin required. `init` removes the old plugin entry; the old package is deprecated on npm.
177
-
178
- **Why no variants/fallback models?** Native agent frontmatter has a single `model:` line. If you want a fallback story, 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`.
179
193
 
180
194
  **Can I have per-project stacks?** Point `AGENT_ROUTER_STACKS_DIR` at a project-local directory in that project's shell env.
181
195
 
@@ -206,51 +220,6 @@ pnpm test
206
220
  pnpm build
207
221
  ```
208
222
 
209
- ### Making changes & releasing
210
-
211
- You never run `npm publish` or touch an npm token. The only npm command in the loop is `npm version` (a local file-edit + git-commit helper), and even that is wrapped by the release script.
212
-
213
- #### One-time setup (already done for this repo)
214
-
215
- Publishing uses npm **Trusted Publishing (OIDC)** — no `NPM_TOKEN` secret is stored. The `Release` workflow mints a short-lived OIDC token that npm trusts because this repo is registered as a trusted publisher on npmjs.com. If a release ever fails with a 403/404 on the publish step, register the trusted publisher on npmjs.com → package settings → *Trusted Publishers* → add:
216
-
217
- - Repository: `dylanrussellmd/agent-router`
218
- - Workflow filename: `release.yml`
219
- - Environment: *(leave blank)*
220
-
221
- #### The workflow
222
-
223
- ```sh
224
- # 1. Make your changes on main (or a PR, merged to main).
225
- pnpm test # full gate: lint + typecheck + build + test + coverage
226
- git add -A && git commit # commit your changes
227
- git push # push to main (CI runs the gate on node 20 + 22)
228
-
229
- # 2. Cut a release. The script:
230
- # - refuses if main is dirty or out of sync with origin
231
- # - bumps package.json + src/version.ts (via the `version` hook)
232
- # - commits both as "<new-version>", tags vX.Y.Z
233
- # - pushes main + the tag
234
- ./scripts/release.sh patch # 1.0.2 → 1.0.3
235
- # ./scripts/release.sh minor # 1.0.2 → 1.1.0
236
- # ./scripts/release.sh major # 1.0.2 → 2.0.0
237
- # ./scripts/release.sh 1.5.0 # explicit version
238
-
239
- # 3. Done. The Release workflow picks up the tag, re-runs the full gate,
240
- # and publishes to npm with signed build provenance. Watch it:
241
- # https://github.com/dylanrussellmd/agent-router/actions/workflows/release.yml
242
- ```
243
-
244
- #### What the release script guards against
245
-
246
- - **Dirty work tree** — uncommitted changes would get swept into the version commit or left behind.
247
- - **Wrong branch** — refuses to cut a release from anything but `main`.
248
- - **Diverged main** — local `main` must match `origin/main` so the tag lands on a commit the runner can check out.
249
-
250
- #### How the version stays in sync
251
-
252
- `package.json` and `src/version.ts` must always agree. `scripts/sync-version.mjs` runs as the npm `version` lifecycle hook during `./scripts/release.sh`: after `npm version` bumps `package.json`, the hook rewrites `src/version.ts` to match and stages it, so both files land in the same commit. The Release workflow also verifies the tag (`vX.Y.Z`) matches `package.json` `version` exactly, failing loudly if they drift.
253
-
254
223
  ## License
255
224
 
256
225
  MIT — see [LICENSE](./LICENSE).
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 readAgentModels(agentsDir) {
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 !== null) out[name] = 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
- pending.push({
16432
- agent,
16433
- filePath,
16434
- next: model === entry.model ? null : setFrontmatterModel(content, entry.model)
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: modelsToStackAgents(await readAgentModels(paths.agentsDir)) };
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 modelsToStackAgents(models) {
16816
+ function entryOptions(entry) {
16817
+ const rec = entry;
16470
16818
  const out = {};
16471
- for (const k of Object.keys(models).sort()) {
16472
- const model = models[k];
16473
- if (model !== void 0) out[k] = { model };
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 models = await readAgentModels(paths.agentsDir);
16511
- const agents = modelsToStackAgents(models);
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.5";
16914
+ var VERSION = "1.0.7";
16555
16915
 
16556
16916
  // src/cli.ts
16557
16917
  var log = (...args) => console.log(...args);