@dylanrussell/agent-router 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dylan Russell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,211 @@
1
+ <div align="center">
2
+
3
+ <img src="./assets/logo.svg" width="128" alt="agent-router logo" />
4
+
5
+ # agent-router
6
+
7
+ **Switch the models assigned to your opencode agents.**
8
+ Named stacks applied to agent frontmatter · one command, one restart, new model crew.
9
+
10
+ [![npm version](https://img.shields.io/npm/v/@dylanrussell/agent-router.svg?color=06b6d4&label=npm&logo=npm&logoColor=white&style=flat-square)](https://www.npmjs.com/package/@dylanrussell/agent-router)
11
+ [![npm downloads](https://img.shields.io/npm/dm/@dylanrussell/agent-router.svg?color=06b6d4&label=downloads&style=flat-square)](https://www.npmjs.com/package/@dylanrussell/agent-router)
12
+ [![license](https://img.shields.io/npm/l/@dylanrussell/agent-router.svg?color=06b6d4&style=flat-square)](./LICENSE)
13
+ [![node](https://img.shields.io/node/v/@dylanrussell/agent-router.svg?color=06b6d4&logo=node.js&logoColor=white&style=flat-square)](https://nodejs.org)
14
+ [![CI](https://img.shields.io/github/actions/workflow/status/dylanrussellmd/agent-router/ci.yml?branch=main&label=CI&logo=github&logoColor=white&style=flat-square)](https://github.com/dylanrussellmd/agent-router/actions/workflows/ci.yml)
15
+ [![types: TypeScript](https://img.shields.io/badge/types-TypeScript-3178C6.svg?logo=typescript&logoColor=white&style=flat-square)](https://www.typescriptlang.org)
16
+ [![tested with vitest](https://img.shields.io/badge/tested%20with-vitest-FCC72B.svg?logo=vitest&logoColor=black&style=flat-square)](https://vitest.dev)
17
+
18
+ [Install](#install) · [Quickstart](#quickstart) · [Stacks](#stacks) · [In the TUI](#in-the-tui) · [FAQ](#faq) · [Issues](https://github.com/dylanrussellmd/agent-router/issues)
19
+
20
+ </div>
21
+
22
+ ---
23
+
24
+ ## What it does
25
+
26
+ opencode agents are markdown files (`~/.config/opencode/agents/*.md`) whose YAML frontmatter carries a `model:` line:
27
+
28
+ ```markdown
29
+ ---
30
+ description: High-reasoning review, debugging, and architecture counsel
31
+ mode: subagent
32
+ model: anthropic/claude-opus-4-8 # ← the ONLY line agent-router touches
33
+ temperature: 0.1
34
+ tools: { write: false, edit: false }
35
+ ---
36
+ <prompt body — owned by you, never touched by agent-router>
37
+ ```
38
+
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
+
41
+ ```json
42
+ {
43
+ "agents": {
44
+ "Omni": { "model": "anthropic/claude-fable-5" },
45
+ "oracle": { "model": "openai/gpt-5.5" },
46
+ "explorer": { "model": "openai/gpt-5.4-mini" },
47
+ "librarian": { "model": "openai/gpt-5.4-mini" },
48
+ "fixer": { "model": "openai/gpt-5.5" }
49
+ }
50
+ }
51
+ ```
52
+
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
+
55
+ ```
56
+ ~/.config/opencode/
57
+ ├── agents/ ← your agent .md files (the live target)
58
+ └── agent-router/
59
+ ├── state.json ← {active, previousActive, …} (machine state)
60
+ ├── stacks/
61
+ │ ├── premium.json ← named stacks (your config)
62
+ │ └── cheap.json
63
+ └── history/ ← rolling 20 most-recent switches
64
+ ```
65
+
66
+ Stacks are config, history is state — point `stacksDir` somewhere dotfile-managed if you version your setup (see [Configuration](#configuration)).
67
+
68
+ ## Install
69
+
70
+ ```bash
71
+ npx -y @dylanrussell/agent-router init
72
+ ```
73
+
74
+ `init` will:
75
+
76
+ 1. Capture your agents' current models into a first stack (`default`) and mark it active.
77
+ 2. Add `@dylanrussell/agent-router@latest` to the `plugin` array in `opencode.json` (backing it up first).
78
+ 3. Add the same entry to `tui.json` — this loads the sidebar + `/agent-*` commands (opencode ≥ 1.17).
79
+ 4. Remove the legacy `@dylanrussell/omo-router` plugin entry if present.
80
+
81
+ Then **restart opencode** so it picks up the plugin.
82
+
83
+ ## Quickstart
84
+
85
+ ```bash
86
+ agent-router capture my-mix # snapshot current frontmatter models as a stack
87
+ agent-router list # show stacks; * marks active
88
+ agent-router use cheap # apply a stack (validates first)
89
+ agent-router back # undo the most recent switch
90
+ agent-router current # print the agent → model mapping in frontmatter
91
+ agent-router status # print active stack name
92
+ agent-router show cheap # print a stack's JSON
93
+ agent-router edit cheap # open a stack in $EDITOR
94
+ agent-router validate --all # check every stack against `opencode models`
95
+ agent-router history # list recent switches
96
+ agent-router import my-mix <file> # import a stack from a file
97
+ agent-router export my-mix <file> # export a stack to a file
98
+ agent-router rm my-mix # remove a stack
99
+ agent-router path # print all paths used (debugging)
100
+ agent-router completion # install shell autocompletion
101
+ ```
102
+
103
+ The everyday loop: tune your agents until you like them → `capture <name>` → repeat with other models → `use <name>` to swap between the results.
104
+
105
+ ## Stacks
106
+
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
+
109
+ `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
+
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.
112
+
113
+ ## Inside opencode
114
+
115
+ The plugin exposes six tools the agent (or you, by asking it) can call:
116
+
117
+ | tool | what it does |
118
+ |---|---|
119
+ | `router_status` | active stack + current frontmatter mapping + available stacks |
120
+ | `router_list` | stack list with `isActive` flags |
121
+ | `router_use({name, validate?})` | apply a stack; pops a TUI toast |
122
+ | `router_capture({name, force?})` | snapshot current models into a stack |
123
+ | `router_back({n?})` | undo last N switches |
124
+ | `router_validate({name?, active?})` | check model IDs against current opencode auth |
125
+
126
+ > *"Switch to cheap"* — your agent calls `router_use`, the toast pops up, you restart opencode.
127
+
128
+ ## In the TUI
129
+
130
+ On opencode ≥ 1.17 the plugin also ships a TUI half (loaded from `tui.json`, wired up by `init`):
131
+
132
+ - **Sidebar panel** — shows the active stack, the stack count, and a `⟳ restart required` badge after any switch. Updates live (≤1.5s) when the CLI or agent switches stacks underneath the TUI.
133
+ - **Commands** — type `/` or open the command palette:
134
+
135
+ | command | what it does |
136
+ |---|---|
137
+ | `/agent-switch` (alias `/ar`) | pick a stack, validate, apply |
138
+ | `/agent-view` | browse a stack's agent → model assignments |
139
+ | `/agent-edit` | reassign a model, picking from your reachable model catalog |
140
+ | `/agent-back` | confirm + revert to the previous stack |
141
+ | `/agent-validate` | check a stack's model IDs against current auth |
142
+ | `/agent-status` | toast the active stack + list |
143
+
144
+ Everything degrades gracefully: on older opencode versions (or if the TUI API changes) the sidebar and commands simply don't appear — the CLI and agent tools keep working.
145
+
146
+ Debugging the TUI half: `AGENT_ROUTER_TUI_DEBUG=/tmp/agent-router-tui.log opencode` writes a trace of the plugin's init steps.
147
+
148
+ ## Configuration
149
+
150
+ Paths resolve in this order: explicit option → `config.json` → env var → default.
151
+
152
+ `~/.config/opencode/agent-router/config.json` (read identically by the CLI and the plugin — the recommended place):
153
+
154
+ ```json
155
+ {
156
+ "agentsDir": "~/.agents/agents",
157
+ "stacksDir": "~/.agents/agent-router/stacks"
158
+ }
159
+ ```
160
+
161
+ | setting | env var | default |
162
+ |---|---|---|
163
+ | agents dir | `AGENT_ROUTER_AGENTS_DIR` | `~/.config/opencode/agents` |
164
+ | stacks dir | `AGENT_ROUTER_STACKS_DIR` | `${routerHome}/stacks` |
165
+ | state home | `AGENT_ROUTER_HOME` (legacy `OMO_ROUTER_HOME`) | `~/.config/opencode/agent-router` |
166
+
167
+ ## ⚠ Things to know
168
+
169
+ - **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.
171
+ - **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
+
174
+ ## FAQ
175
+
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.
179
+
180
+ **Can I have per-project stacks?** Point `AGENT_ROUTER_STACKS_DIR` at a project-local directory in that project's shell env.
181
+
182
+ ## Architecture in 60 seconds
183
+
184
+ ```
185
+ ┌──────────────────────────────────────────────┐
186
+ │ opencode (Bun) │
187
+ │ └─ agents loaded from agents/*.md ──────────┼── reads at startup ─┐
188
+ │ └─ plugin: agent-router (this package) ─────┼─ tools, toast │
189
+ └──────────────────────────────────────────────┘ ▼
190
+ ~/.config/opencode/agents/*.md ◄── `model:` lines rewritten on `use`
191
+ ~/.config/opencode/agent-router/
192
+ stacks/<name>.json ◄── source of truth for each stack
193
+ state.json ◄── pointer to active stack
194
+ history/<ts>__<from>-to-<to>.json ◄── displaced mappings, rolling 20
195
+ ```
196
+
197
+ ## Contributing
198
+
199
+ Issues and PRs welcome. Run locally:
200
+
201
+ ```bash
202
+ git clone https://github.com/dylanrussellmd/agent-router.git
203
+ cd agent-router
204
+ pnpm install
205
+ pnpm test
206
+ pnpm build
207
+ ```
208
+
209
+ ## License
210
+
211
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,45 @@
1
+ <svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="omo-router logo">
2
+ <title>omo-router</title>
3
+ <desc>Three stacked rounded rectangles representing named model stacks; the top one is highlighted in cyan as the active stack. A small cyan dot on the left indicates the selector position. The plate is a soft slate background with rounded corners.</desc>
4
+
5
+ <!-- background plate -->
6
+ <rect width="256" height="256" rx="48" fill="#0F172A"/>
7
+
8
+ <!-- subtle inner highlight -->
9
+ <rect x="2" y="2" width="252" height="252" rx="46" fill="none" stroke="url(#sheen)" stroke-width="1" stroke-opacity="0.35"/>
10
+
11
+ <!-- 3 stacked cards (the 3 stacks) -->
12
+ <!-- bottom card: dim -->
13
+ <rect x="60" y="170" width="148" height="34" rx="9" fill="#334155"/>
14
+ <rect x="74" y="183" width="44" height="8" rx="4" fill="#0F172A" fill-opacity="0.55"/>
15
+
16
+ <!-- middle card: medium -->
17
+ <rect x="60" y="120" width="148" height="34" rx="9" fill="#475569"/>
18
+ <rect x="74" y="133" width="60" height="8" rx="4" fill="#0F172A" fill-opacity="0.55"/>
19
+
20
+ <!-- top card: active (cyan) -->
21
+ <rect x="60" y="70" width="148" height="34" rx="9" fill="url(#activeFill)"/>
22
+ <rect x="74" y="83" width="36" height="8" rx="4" fill="#0F172A" fill-opacity="0.7"/>
23
+ <!-- subtle glow on active -->
24
+ <rect x="60" y="70" width="148" height="34" rx="9" fill="none" stroke="#22D3EE" stroke-width="1.5" stroke-opacity="0.5"/>
25
+
26
+ <!-- selector lane on the left -->
27
+ <line x1="40" y1="64" x2="40" y2="210" stroke="#1E293B" stroke-width="4" stroke-linecap="round"/>
28
+
29
+ <!-- selector dot pointing at active row -->
30
+ <circle cx="40" cy="87" r="8" fill="#06B6D4"/>
31
+ <circle cx="40" cy="87" r="8" fill="none" stroke="#22D3EE" stroke-width="2" stroke-opacity="0.65"/>
32
+
33
+ <!-- gradient defs -->
34
+ <defs>
35
+ <linearGradient id="activeFill" x1="60" y1="70" x2="208" y2="104" gradientUnits="userSpaceOnUse">
36
+ <stop offset="0" stop-color="#0891B2"/>
37
+ <stop offset="1" stop-color="#06B6D4"/>
38
+ </linearGradient>
39
+ <linearGradient id="sheen" x1="0" y1="0" x2="256" y2="256" gradientUnits="userSpaceOnUse">
40
+ <stop offset="0" stop-color="#94A3B8"/>
41
+ <stop offset="0.5" stop-color="#94A3B8" stop-opacity="0"/>
42
+ <stop offset="1" stop-color="#94A3B8"/>
43
+ </linearGradient>
44
+ </defs>
45
+ </svg>
package/dist/cli.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `agent-router` CLI entry point.
3
+ *
4
+ * One file, one tool — uses `cac` for arg parsing because it's tiny (~6kB
5
+ * minified) and we don't need the bells and whistles of yargs/commander.
6
+ *
7
+ * Architecture:
8
+ * - Each subcommand is a small async function that calls into `core/`.
9
+ * - All stdout/stderr formatting lives here (the core throws typed errors;
10
+ * the CLI maps them to exit codes + human-readable output).
11
+ * - We never call `process.exit()` directly — we throw and let `main()`
12
+ * handle it. This makes the CLI testable via dynamic import.
13
+ *
14
+ * Exit codes (see `errors.ts`):
15
+ * 0 success
16
+ * 1 user error (bad args, refused destructive op)
17
+ * 2 stack or agent file not found
18
+ * 3 IO error
19
+ * 4 schema or model-validation failed
20
+ */
21
+ declare function main(argv: ReadonlyArray<string>): Promise<number>;
22
+
23
+ export { main };