@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 +26 -57
- 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,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
|
-
**
|
|
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
|
|
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);
|