@dylanrussell/agent-router 1.1.1 → 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/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # 2.0.0
2
+
3
+ - Target OpenCode 2.0.8 with server and terminal `setup` entrypoints.
4
+ - Include literal root `index.ts`/`tui.ts` wrappers for the 2.0.8 local-directory resolver; published packages retain their exports.
5
+ - Add an isolated real-host integration check for package activation, terminal discovery, and RPC input validation through a disposable test probe.
6
+ - Preserve all six router tools, stack operations, history, sidebar and six slash commands.
7
+ - Register terminal commands under a mounted app slot so OpenCode 2.0.8 provides its Keymap context. Sidebar slots return real renderables with reactive child insertion.
8
+ - Register terminal plugins in `cli.json` and use native `plugins` entries, including package/options objects.
9
+ - Validate advertised model variants using the V2 model catalog. Unsupported `max` variants must be changed explicitly to an advertised variant such as `high`.
10
+ - Preserve legacy frontmatter and support capture/apply of native `model#variant` references.
11
+ - Queue fallback selection for the next explicit user prompt after retryable HTTP 429/500/502/503/504. Veto automatic same-turn retry for routed failures; never submit prompts or replay tools.
12
+
13
+ ## Operational limits
14
+
15
+ - Applying/backing out stacks still requires restarting OpenCode to reload agent files.
16
+ - Terminal stack operations access local files; remote-server filesystem management is not supported.
17
+ - Fallback notices are server logs. The sidebar shows configured routing, not session-local fallback state.
18
+ - OpenCode 2.0.8 does not identify request kind on retry hooks. The plugin conservatively tracks the last model-request kind and only handles primary requests. Concurrent auxiliary requests can suppress fallback handling.
19
+ - Session defaults must resolve to explicit agent/model selections before routing. Fallback routing is bounded to 1,024 tracked sessions per plugin instance.
package/README.md CHANGED
@@ -68,6 +68,8 @@ Stacks are config, history is state — point `stacksDir` somewhere dotfile-mana
68
68
 
69
69
  ## Install
70
70
 
71
+ Version 2 targets **OpenCode 2.0.8**. Version 1 remains the legacy OpenCode release. See [release notes and operational limits](./CHANGELOG.md).
72
+
71
73
  ```bash
72
74
  npx -y @dylanrussell/agent-router init
73
75
  ```
@@ -75,13 +77,13 @@ npx -y @dylanrussell/agent-router init
75
77
  `init` will:
76
78
 
77
79
  1. Capture your agents' current models into a first stack (`default`) and mark it active.
78
- 2. Add `@dylanrussell/agent-router@latest` to the `plugin` array in `opencode.json` (backing it up first).
79
- 3. Add the same entry to `tui.json` — this loads the sidebar + `/agent-*` commands (opencode ≥ 1.17).
80
+ 2. Add `@dylanrussell/agent-router@latest` to the `plugins` array in `opencode.json` (backing it up first).
81
+ 3. Add the same entry to `cli.json` — this loads the sidebar + `/agent-*` commands.
80
82
  4. Remove the legacy `@dylanrussell/omo-router` plugin entry if present.
81
83
 
82
84
  Then **restart opencode** so it picks up the plugin.
83
85
 
84
- `@opencode-ai/plugin` is a required peer dependency: the server bundle imports it at runtime. npm installs it on a clean install; do not omit peer dependencies when installing the plugin.
86
+ The API peer is pinned to `@opencode/plugin@2.0.8`. OpenCode provides the terminal renderer at runtime. Both package entrypoints default-export V2 plugin definitions.
85
87
 
86
88
  ## Quickstart
87
89
 
@@ -94,7 +96,7 @@ agent-router current # print the agent → model mapping in fro
94
96
  agent-router status # print active stack name
95
97
  agent-router show cheap # print a stack's JSON
96
98
  agent-router edit cheap # open a stack in $EDITOR
97
- agent-router validate --all # check every stack against `opencode models`
99
+ agent-router validate --all # check model IDs and variants against the V2 catalog
98
100
  agent-router history # list recent switches
99
101
  agent-router import my-mix <file> # import a stack from a file
100
102
  agent-router export my-mix <file> # export a stack to a file
@@ -125,7 +127,7 @@ A stack file needs one thing: an `agents` record whose entries carry a `model` s
125
127
  - Reserved opencode framework keys (`description`, `mode`, `permission`, `tools`, `prompt`, `steps`, `color`, `name`) are never transcribed — a stack entry can't clobber them.
126
128
  - 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.
127
129
 
128
- `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`).
130
+ `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**. It validates model IDs and variants against the V2 catalog first (skip with `--no-validate`, override with `--force-invalid`).
129
131
 
130
132
  `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.
131
133
 
@@ -154,36 +156,40 @@ Model IDs and variants above are illustrative; use models and variants supported
154
156
  - `fallbacks` is optional, or an array of 0 to 8 objects. Each object contains only `model` (non-whitespace `provider/model`) and optional `variant` (nonempty string). `null`, bare strings, unknown candidate fields, and longer chains are rejected.
155
157
  - Candidate order is preserved. Exact `(model, variant)` duplicates, including the primary, are skipped at runtime. An omitted fallback variant clears the previous candidate's variant.
156
158
  - Only model and variant change at runtime. Agent prompts, permissions, tools, and other provider options remain unchanged; ensure those options are compatible with every candidate.
157
- - Validation checks every primary and fallback ID, reporting paths such as `agents.oracle.fallbacks.0.model`. It checks catalogue membership through `opencode models`, not live provider health, credentials, quota, or variant support. `validate --active` includes applied fallback metadata when its primary still matches frontmatter.
159
+ - Validation checks every primary and fallback ID and variant, reporting paths such as `agents.oracle.fallbacks.0.variant`. Server tools use the connected host catalog; the standalone CLI uses `opencode api model.list`. Catalog membership does not prove live provider health or quota. `validate --active` includes applied fallback metadata when its primary still matches frontmatter.
158
160
  - Failover does not rewrite frontmatter, stacks, history, or router state. `use` stores the applied chains in optional `state.json.fallbackAgents`. Old state files without that field remain valid and have no enabled chains until the stack is reapplied.
159
161
  - `capture` and displaced-history snapshots merge those applied chains with live frontmatter only when primary model and variant still match. Editing a stack alone does not alter applied metadata. Applying a stack without chains clears the applied chains, including agents omitted from that stack.
160
162
  - `back` retains its existing semantics: reapply a previous **named stack as it exists now**, not restore historical snapshot bytes. Runtime failover never adds a history entry.
161
163
 
162
164
  ### Runtime Behavior And Limits
163
165
 
164
- The plugin watches correlated assistant `message.updated` errors, requiring the current user-message parent ID, agent, and selected provider/model. It stages the next candidate only for structured `APIError` with `isRetryable: true` and HTTP **429, 500, 502, 503, or 504**. Native opencode retries may occur before the terminal error reaches this hook. Router does not change their policy or timing.
166
+ The V2 plugin watches `retry` hooks correlated with the admitted user turn, agent and selected model. A retryable HTTP **429, 500, 502, 503, or 504** stages the next candidate and vetoes the same-turn retry. Only primary model requests participate.
165
167
 
166
168
  Authentication/permission errors, context overflow, content filtering, output-length errors, unknown errors, transport errors without an HTTP status, and ambiguous `session.error` notifications do not advance the chain. Router never classifies errors by substring matching.
167
169
 
168
- After a qualifying failure a 15-second warning toast (or log fallback) names the next model and asks for an explicit retry/continue instruction. Every notice is also logged under `agent-router`. **Review partial output and completed tools before continuing.** The mutable `chat.message` hook selects the fallback when the next prompt is admitted; no prompt is submitted by this plugin. The successful fallback stays selected for subsequent turns. Chains advance monotonically with at most eight transitions per agent/session; exhaustion produces a notice, never wraps to the primary. There are no timers, background retries, or automatic failback.
170
+ After a qualifying failure a server log names the next model and asks for an explicit retry/continue instruction. **Review partial output and completed tools before continuing.** The next `prompt` admission hook calls `session.switchModel`; it never submits a prompt. Chains advance monotonically, never wrap, and never replay prompts or tools.
169
171
 
170
- Observed cancellation (`MessageAbortedError`), explicit model/agent switch events, and an incoming model/variant different from both the configured primary and current candidate disable routing for that session until restart. A switch to the same model is distinguishable only when the host emits its switch event. Hosts/UI paths that omit both the event and a changed selection cannot expose that manual intent to this plugin. Switching stacks with router tools disables running failover immediately; external CLI/TUI state changes disable it at the next prompt admission. Restart to activate the newly applied stack.
172
+ Observed interruption, explicit model/agent selection events, and an unexpected incoming model disable routing for that session. Router state changes disable startup routing at the next prompt admission. Restart to activate a newly applied stack.
171
173
 
172
- Duplicate/concurrent error events advance only once per user turn. Selection and bookkeeping run synchronously before notification awaits, so delayed notices cannot overwrite cancellation/manual selection. Sessions and per-agent budgets are isolated; deleted/disabled sessions remain tombstones to prevent rearming. After 1,024 tracked sessions in one plugin instance, additional sessions receive no automatic routing until restart. Untracked sessions and subagents that bypass `chat.message` are deliberately left alone.
174
+ Duplicate failures advance only once per admitted user turn. Routing budgets are isolated by session and agent and bounded to 1,024 sessions. Subagents without user prompt admission are left alone.
173
175
 
174
- **Verified boundary:** inspected opencode **v1.17.15** `packages/opencode/src/session/prompt.ts`: `chat.message` mutates the message before `sessions.updateMessage(info)` and the legacy session loop resolves `lastUser.model`. A deterministic in-process provider harness tests those hook semantics, explicit next-turn continuation, variant selection, and no replay/persistent writes. This is **not a real-server end-to-end provider test** and does not prove that every new v2 execution path invokes these legacy hooks. Hosts that bypass them receive no failover. There is no transparent same-turn continuation guarantee, no automatic recovery of a failed `task` invocation, and no guarantee that an LLM will not choose to repeat a tool when explicitly asked to continue.
176
+ **Verified boundary:** compiled against the published **2.0.8** API types. Deterministic adapter tests cover next-turn selection, retry veto, auxiliary-request exclusion, variants, and no replay or persistent agent writes. These are not real-provider end-to-end tests. An LLM may choose to repeat a tool when explicitly asked to continue.
175
177
 
176
- The installed SDK exposes `/v2` `client.v2.session.switchModel` for subsequent turns, but no atomic compare-and-switch/retry contract was verified. Router deliberately does **not** call it: an asynchronous model write could race a manual selection or cancellation. No direct SDK dependency is needed; plugin hooks are the integration boundary. The sidebar displays configured primary and fallback chains; the TUI model picker and router status display primary assignments. None of these indicate session-local runtime candidates.
178
+ OpenCode 2.0.8 exposes no atomic compare-and-switch in the prompt hook and no request kind in the retry hook. Concurrent manual selections/admissions or auxiliary requests remain host API limitations. The sidebar displays configured routing, not the session-local runtime candidate. Terminal stack operations require local filesystem access; remote server filesystem management is not supported.
177
179
 
178
180
  ### Install This Checkout
179
181
 
180
- Ordered fallbacks require agent-router **1.1.0 or later**. To test a local checkout instead of the published package, run `npm run typecheck`, `npm test`, and `npm run build` using the installed dependencies (a fresh checkout uses `pnpm install --frozen-lockfile`). Replace `/absolute/path/to/agent-router` below with your checkout path, then replace the registry router entry in the **server** `opencode.json` plugin array with:
182
+ To test a local V2 checkout, run `npm run typecheck`, `npm test`, and `npm run build` (a fresh checkout uses `pnpm install --frozen-lockfile`). Replace the registry entry in the server `opencode.json` **plugins** array with the package directory:
181
183
 
182
184
  ```json
183
- "file:///absolute/path/to/agent-router/dist/plugin.js"
185
+ "file:///absolute/path/to/agent-router"
184
186
  ```
185
187
 
186
- Do not load both registry and local server plugins. If using the router TUI plugin, also replace its registry entry in `tui.json` with `file:///absolute/path/to/agent-router/dist/tui.js`. Use the checkout's CLI too: older router builds strictly validate state and cannot read the new `fallbackAgents` field. Add a chain to your stack and run `node /absolute/path/to/agent-router/dist/cli.js use <stack-name>`, then quit and restart opencode. These installation/configuration steps are instructions only; implementation and tests do not modify your live agent configuration. Start a new session, and confirm you see the explicit next-turn notice after a qualifying terminal error. Headless clients must inspect the `agent-router` log.
188
+ Use the same package directory in `cli.json` **plugins** when explicitly registering the terminal half. Do not load both registry and local copies. The local CLI is `node /absolute/path/to/agent-router/dist/cli.js`. Applying a stack still requires restarting OpenCode; inspect server logs for next-turn fallback notices.
189
+
190
+ OpenCode 2.0.8 ignores package exports for local directories. Root `index.ts` and `tui.ts` wrappers load the built files; run `npm run build` before using the source directory.
191
+
192
+ `npm run test:integration` starts an isolated real OpenCode 2.0.8 server, initializes its location, verifies router activation and terminal discovery, and exercises a disposable RPC probe with absent, null, and invalid input. It uses temporary HOME/XDG/router directories and never modifies live configuration. Set `AGENT_ROUTER_TEST_PACKAGE` to an unpacked tarball directory to test release contents. Router itself exposes tools, not RPC methods.
187
193
 
188
194
  ## Inside opencode
189
195
 
@@ -202,7 +208,7 @@ The plugin exposes six tools the agent (or you, by asking it) can call:
202
208
 
203
209
  ## In the TUI
204
210
 
205
- On opencode ≥ 1.17 the plugin also ships a TUI half (loaded from `tui.json`, wired up by `init`):
211
+ The V2 terminal half loads from the package's `./tui` export (`cli.json`, wired up by `init`):
206
212
 
207
213
  - **Sidebar panel** — lists available stacks with the active one checked. Under **Current Stack → Configured routing**, each agent has an indented **Primary** model and ordered **Fallback 1**, **Fallback 2**, … rows. Explicit variants appear in brackets; agents without fallbacks show only their primary. Model IDs are kept in full. A `⟳ restart required` badge appears when the active stack differs from the one at TUI startup. Updates live (≤1.5s), including fallback-only and variant-only edits.
208
214
  - **Configuration, not live failover** — sidebar chains come from the active stack file, not session-local routing or the applied `state.json.fallbackAgents` snapshot. Editing a stack changes this preview but does not apply it: use the stack and restart opencode to activate changes. A fallback row is a configured candidate, not a claim that failover is enabled or that the model is currently running.
@@ -244,7 +250,7 @@ Paths resolve in this order: explicit option → `config.json` → env var → d
244
250
 
245
251
  - **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.
246
252
  - **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).
247
- - **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.
253
+ - **Validation is catalog-dependent.** `agent-router validate` uses the active V2 model catalog, including variants. Catalog membership does not guarantee provider health, valid credentials, or available quota.
248
254
 
249
255
  ## FAQ
250
256
 
package/dist/cli.js CHANGED
@@ -15282,7 +15282,13 @@ var init_schema = __esm({
15282
15282
  stacksDir: external_exports.string().min(1).optional()
15283
15283
  }).strict();
15284
15284
  OpencodeJsonSchema = external_exports.object({
15285
- plugin: external_exports.array(external_exports.string()).optional()
15285
+ plugin: external_exports.array(external_exports.string()).optional(),
15286
+ plugins: external_exports.array(
15287
+ external_exports.union([
15288
+ external_exports.string(),
15289
+ external_exports.object({ package: external_exports.string(), options: external_exports.record(external_exports.string(), external_exports.unknown()).optional() }).passthrough()
15290
+ ])
15291
+ ).optional()
15286
15292
  }).passthrough();
15287
15293
  }
15288
15294
  });
@@ -16020,7 +16026,7 @@ function resolvePaths(options = {}) {
16020
16026
  return {
16021
16027
  opencodeConfigDir,
16022
16028
  opencodeJsonPath: path2.join(opencodeConfigDir, "opencode.json"),
16023
- tuiJsonPath: path2.join(opencodeConfigDir, "tui.json"),
16029
+ tuiJsonPath: path2.join(opencodeConfigDir, "cli.json"),
16024
16030
  agentsDir,
16025
16031
  opencodeBackupsDir: path2.join(opencodeConfigDir, ".backups"),
16026
16032
  routerHome,
@@ -16386,6 +16392,11 @@ var RESERVED_AGENT_KEYS = /* @__PURE__ */ new Set([
16386
16392
  "mode",
16387
16393
  "description",
16388
16394
  "permission",
16395
+ "permissions",
16396
+ "disabled",
16397
+ "disable",
16398
+ "system",
16399
+ "hidden",
16389
16400
  "color",
16390
16401
  "tools",
16391
16402
  "prompt",
@@ -16626,31 +16637,35 @@ async function readPluginConfigFile(filePath, label) {
16626
16637
  return result.data;
16627
16638
  }
16628
16639
  function ensurePluginEntry(config2, entry = PLUGIN_REGISTRY_ENTRY) {
16629
- const existing = config2.plugin ?? [];
16640
+ const existing = config2.plugins ?? config2.plugin ?? [];
16630
16641
  const already = existing.some(
16631
- (p) => p === entry || stripVersionTag(p) === stripVersionTag(entry)
16642
+ (p) => pluginPackage(p) === entry || stripVersionTag(pluginPackage(p)) === stripVersionTag(entry)
16632
16643
  );
16633
16644
  if (already) {
16634
- return { config: config2, result: { added: false, plugin: existing } };
16645
+ return { config: config2, result: { added: false, plugin: existing.map(pluginPackage) } };
16635
16646
  }
16636
16647
  const next = [...existing, entry];
16637
16648
  return {
16638
- config: { ...config2, plugin: next },
16639
- result: { added: true, plugin: next }
16649
+ config: { ...config2, plugins: next },
16650
+ result: { added: true, plugin: next.map(pluginPackage) }
16640
16651
  };
16641
16652
  }
16642
16653
  function removePluginEntry(config2, npmName = LEGACY_PLUGIN_NPM_NAME) {
16643
- const existing = config2.plugin ?? [];
16644
- const removed = existing.filter((p) => stripVersionTag(p) === npmName);
16654
+ const key = config2.plugins ? "plugins" : "plugin";
16655
+ const existing = config2[key] ?? [];
16656
+ const removed = existing.filter((p) => stripVersionTag(pluginPackage(p)) === npmName);
16645
16657
  if (removed.length === 0) {
16646
- return { config: config2, result: { removed: [], plugin: existing } };
16658
+ return { config: config2, result: { removed: [], plugin: existing.map(pluginPackage) } };
16647
16659
  }
16648
- const next = existing.filter((p) => stripVersionTag(p) !== npmName);
16660
+ const next = existing.filter((p) => stripVersionTag(pluginPackage(p)) !== npmName);
16649
16661
  return {
16650
- config: { ...config2, plugin: next },
16651
- result: { removed, plugin: next }
16662
+ config: { ...config2, [key]: next },
16663
+ result: { removed: removed.map(pluginPackage), plugin: next.map(pluginPackage) }
16652
16664
  };
16653
16665
  }
16666
+ function pluginPackage(entry) {
16667
+ return typeof entry === "string" ? entry : entry.package;
16668
+ }
16654
16669
  function stripVersionTag(entry) {
16655
16670
  const lastAt = entry.lastIndexOf("@");
16656
16671
  if (lastAt <= 0) return entry;
@@ -16688,7 +16703,7 @@ import { execFile } from "child_process";
16688
16703
  import { promisify } from "util";
16689
16704
  var execFileAsync = promisify(execFile);
16690
16705
  var DEFAULT_RUNNER = async () => {
16691
- const { stdout } = await execFileAsync("opencode", ["models"], {
16706
+ const { stdout } = await execFileAsync("opencode", ["api", "model.list"], {
16692
16707
  encoding: "utf8",
16693
16708
  timeout: 3e4,
16694
16709
  maxBuffer: 4 * 1024 * 1024
@@ -16697,6 +16712,17 @@ var DEFAULT_RUNNER = async () => {
16697
16712
  };
16698
16713
  function parseModelList(stdout) {
16699
16714
  const out = /* @__PURE__ */ new Set();
16715
+ if (/^[\[{]/.test(stdout.trimStart())) {
16716
+ const parsed = JSON.parse(stdout);
16717
+ const models = Array.isArray(parsed) ? parsed : parsed.data;
16718
+ for (const model of models) {
16719
+ if (model.enabled === false) continue;
16720
+ const id = `${model.providerID}/${model.id}`;
16721
+ out.add(id);
16722
+ for (const variant of model.variants ?? []) out.add(`${id}#${variant.id}`);
16723
+ }
16724
+ return out;
16725
+ }
16700
16726
  for (const raw of stdout.split(/\r?\n/)) {
16701
16727
  const line = raw.trim();
16702
16728
  if (!line) continue;
@@ -16721,6 +16747,19 @@ async function validateStack(stack, options = {}) {
16721
16747
  const available = parseModelList(stdout);
16722
16748
  const refs = collectModelRefs(stack);
16723
16749
  const missing = refs.filter((r) => !available.has(r.modelId));
16750
+ if (/^[\[{]/.test(stdout.trimStart())) {
16751
+ for (const [agent, entry] of Object.entries(stack.agents)) {
16752
+ for (const [index, candidate] of [entry, ...entry.fallbacks ?? []].entries()) {
16753
+ if (!candidate.variant) continue;
16754
+ const modelId = `${candidate.model}#${candidate.variant}`;
16755
+ if (!available.has(modelId))
16756
+ missing.push({
16757
+ path: index === 0 ? `agents.${agent}.variant` : `agents.${agent}.fallbacks.${index - 1}.variant`,
16758
+ modelId
16759
+ });
16760
+ }
16761
+ }
16762
+ }
16724
16763
  return { ok: missing.length === 0, checked: refs.length, missing, available };
16725
16764
  }
16726
16765
  async function validateStackOrThrow(stackName, stack, options = {}) {
@@ -16792,8 +16831,15 @@ async function applyStack(paths, name, options = {}) {
16792
16831
  for (const [agent, entry] of Object.entries(target.agents)) {
16793
16832
  const { filePath, content, model } = await readAgentFileStrict(paths.agentsDir, agent);
16794
16833
  const wantOptions = entryOptions(entry);
16834
+ let wantModel = entry.model;
16835
+ if (model.includes("#") || wantModel.includes("#")) {
16836
+ const [base, inline] = wantModel.split("#");
16837
+ const variant = "variant" in entry ? entry.variant : inline ?? model.split("#")[1];
16838
+ wantModel = `${base}${variant ? `#${variant}` : ""}`;
16839
+ wantOptions.variant = null;
16840
+ }
16795
16841
  const afterOptions = setFrontmatterOptions(content, wantOptions);
16796
- const nextContent = entry.model === model ? afterOptions : setFrontmatterModel(afterOptions, entry.model);
16842
+ const nextContent = wantModel === model ? afterOptions : setFrontmatterModel(afterOptions, wantModel);
16797
16843
  pending.push({ agent, filePath, next: nextContent === content ? null : nextContent });
16798
16844
  }
16799
16845
  const prevState = await readState(paths.statePath);
@@ -16860,6 +16906,10 @@ function entriesToStackAgents(entries) {
16860
16906
  for (const [k, v] of Object.entries(options)) {
16861
16907
  if (k !== "fallbacks") stackEntry[k] = v;
16862
16908
  }
16909
+ if (model.includes("#")) {
16910
+ stackEntry.model = model.slice(0, model.indexOf("#"));
16911
+ stackEntry.variant = model.slice(model.indexOf("#") + 1);
16912
+ }
16863
16913
  out[name] = stackEntry;
16864
16914
  }
16865
16915
  return out;
@@ -16951,7 +17001,7 @@ async function exportStack(paths, name, toFile) {
16951
17001
  }
16952
17002
 
16953
17003
  // src/version.ts
16954
- var VERSION = "1.1.1";
17004
+ var VERSION = "2.0.0";
16955
17005
 
16956
17006
  // src/cli.ts
16957
17007
  var log = (...args) => console.log(...args);
@@ -16995,8 +17045,8 @@ async function cmdInit(paths, opts) {
16995
17045
  }
16996
17046
  if (opts.noEditOpencodeJson) {
16997
17047
  log("agent-router: --no-edit-opencode-json passed; not editing opencode.json or tui.json");
16998
- log(` add this to opencode.json plugin[]: "${PLUGIN_REGISTRY_ENTRY}"`);
16999
- log(` add this to tui.json plugin[] (sidebar): "${PLUGIN_REGISTRY_ENTRY}"`);
17048
+ log(` add this to opencode.json plugins[]: "${PLUGIN_REGISTRY_ENTRY}"`);
17049
+ log(` add this to cli.json plugins[] (sidebar): "${PLUGIN_REGISTRY_ENTRY}"`);
17000
17050
  return;
17001
17051
  }
17002
17052
  await editOpencodeJson(paths);
@@ -17009,7 +17059,7 @@ async function editTuiJson(paths) {
17009
17059
  }
17010
17060
  const result = await ensureTuiJsonPluginEntry(paths.tuiJsonPath);
17011
17061
  if (result.added) {
17012
- log(`agent-router: added "${PLUGIN_REGISTRY_ENTRY}" to tui.json plugin[] (sidebar support)`);
17062
+ log(`agent-router: added "${PLUGIN_REGISTRY_ENTRY}" to cli.json plugins[] (sidebar support)`);
17013
17063
  return;
17014
17064
  }
17015
17065
  log("agent-router: tui.json already up to date");
@@ -17034,7 +17084,7 @@ async function editOpencodeJson(paths) {
17034
17084
  );
17035
17085
  }
17036
17086
  if (ensured.result.added) {
17037
- log(`agent-router: added "${PLUGIN_REGISTRY_ENTRY}" to opencode.json plugin[]`);
17087
+ log(`agent-router: added "${PLUGIN_REGISTRY_ENTRY}" to opencode.json plugins[]`);
17038
17088
  }
17039
17089
  }
17040
17090
  async function cmdList(paths) {