@s-berdia/opencode-router 1.0.1 → 1.1.1

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
@@ -1,6 +1,12 @@
1
1
  # opencode-router
2
2
 
3
- Automatic model routing for [OpenCode](https://opencode.ai) on the **Go**, **Go Plus**, and **Free** plans.
3
+ [![npm version](https://img.shields.io/npm/v/@s-berdia/opencode-router)](https://www.npmjs.com/package/@s-berdia/opencode-router)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@s-berdia/opencode-router)](https://www.npmjs.com/package/@s-berdia/opencode-router)
5
+ [![CI](https://github.com/s-berdia/Opencode_router/actions/workflows/ci.yml/badge.svg)](https://github.com/s-berdia/Opencode_router/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
7
+
8
+ **Switch models before you hit your [OpenCode](https://opencode.ai) limit.** Automatic
9
+ model routing and quota failover for the **Go**, **Go Plus**, and **Free** plans.
4
10
 
5
11
  ![opencode-router demo: the scorecard gives each agent a model, and the build agent changes to the next model at 85% of its 5-hour limit](docs/router-demo.gif)
6
12
 
@@ -41,6 +47,15 @@ the router, read its [known limits](./USAGE.md#11-known-limits).
41
47
 
42
48
  ## Quick start
43
49
 
50
+ If you have the OpenCode command-line tool, run this command. Then close OpenCode, start
51
+ it again, and go to step 4.
52
+
53
+ ```bash
54
+ opencode plugin @s-berdia/opencode-router --global
55
+ ```
56
+
57
+ If you do not have the command-line tool, edit the config file:
58
+
44
59
  1. Open your global OpenCode config file in a text editor:
45
60
  - Windows: `C:\Users\<your-user-name>\.config\opencode\opencode.json`
46
61
  - macOS and Linux: `~/.config/opencode/opencode.json`
@@ -66,6 +81,10 @@ the router, read its [known limits](./USAGE.md#11-known-limits).
66
81
 
67
82
  The router now selects models automatically. You do not have to do more.
68
83
 
84
+ OpenCode does not update plugins automatically. When a new version of the router is
85
+ available, the router shows a note. Then type `/routing-update` and restart OpenCode.
86
+ See [Update the router](./USAGE.md#12-update-the-router).
87
+
69
88
  ## Commands
70
89
 
71
90
  ### In OpenCode
@@ -76,6 +95,7 @@ The router now selects models automatically. You do not have to do more.
76
95
  | `/routing-status` | Show the model in use for each agent (after failover) and its 5-hour quota use |
77
96
  | `/routing-plan go-plus` | Save a plan profile (`go`, `go-plus`, or `free`) and apply it immediately |
78
97
  | `/routing-eval` | Send one test prompt to the `build` models and write a report |
98
+ | `/routing-update` | Look for a newer version and remove the old copy that OpenCode keeps. Then restart OpenCode |
79
99
 
80
100
  ### In a terminal
81
101
 
@@ -88,6 +108,7 @@ npx @s-berdia/opencode-router status # models in use and quota u
88
108
  npx @s-berdia/opencode-router plan go-plus # save the plan profile and rebuild
89
109
  npx @s-berdia/opencode-router cat # catalog file, date, model count
90
110
  npx @s-berdia/opencode-router install --scope global # write slash-command files
111
+ npx @s-berdia/opencode-router@latest update # remove OpenCode's old copy
91
112
  npx @s-berdia/opencode-router --help # all commands and options
92
113
  ```
93
114
 
@@ -140,6 +161,7 @@ or a value that is not valid, the router ignores it and shows a note in the scor
140
161
  | `blend` | `{ "input": 0.75, "output": 0.25, "cacheRead": 0.1 }` | Token mix used to calculate the cost of one million tokens |
141
162
  | `enduranceScale` | `{ "minTokensPer5h": 0.25, "maxTokensPer5h": 40 }` | Millions of tokens per 5 hours that give an endurance score of 0 and 100 |
142
163
  | `feeds` | `{ "enabled": false }` | Optional benchmark feed: `{ "enabled": true, "url": "https://...", "timeoutMs": 5000 }` |
164
+ | `updateCheck` | `true` | Once each 24 hours, ask npm for the newest router version. `false` stops the check |
143
165
 
144
166
  The [usage guide](./USAGE.md#6-change-the-router-settings) has examples for each key.
145
167
 
@@ -172,6 +194,7 @@ score = w_intelligence * intelligence
172
194
  | `~/.config/opencode/router/routing-table.json` | The last routing table |
173
195
  | `~/.config/opencode/router/last-apply.json` | The result of the last live apply |
174
196
  | `~/.config/opencode/router/eval-report.md` | The report of the last `/routing-eval` |
197
+ | `~/.config/opencode/router/update-check.json` | The time of the last update check and the newest version on npm |
175
198
  | `opencode.json` or `opencode.jsonc` (global, or the project file with `--scope project`) | `model`, `small_model`, and `agent.<role>.model` and `variant`. Comments and all other keys stay. A file with a JSON error is not changed |
176
199
 
177
200
  If `XDG_CONFIG_HOME` is set, the router uses `$XDG_CONFIG_HOME/opencode` in place of
@@ -183,6 +206,10 @@ Models with `-free` in their name can collect your prompts for training. The sco
183
206
  marks them `free*`. To keep your main work off these models, set
184
207
  `"privacy": { "freeModelsOk": false }`. To never use them, set `"denyModels": ["*-free"]`.
185
208
 
209
+ Once each 24 hours, the router sends one request to `registry.npmjs.org` to get the
210
+ newest version number of the router. It sends no other data. To stop this, set
211
+ `"updateCheck": false`.
212
+
186
213
  ## Development
187
214
 
188
215
  ```bash
@@ -198,6 +225,12 @@ To use your local build in OpenCode, put the absolute path of the folder in the
198
225
  list. Example for Windows: `"plugin": ["file:///C:/code/Opencode_router"]`. Run
199
226
  `npm run build` after each change, then restart OpenCode.
200
227
 
228
+ ## Contributing
229
+
230
+ Bug reports, model scores, and code changes are welcome. Read
231
+ [CONTRIBUTING.md](./CONTRIBUTING.md). For questions and ideas, use
232
+ [Discussions](https://github.com/s-berdia/Opencode_router/discussions).
233
+
201
234
  ## License
202
235
 
203
236
  [MIT](./LICENSE)
package/USAGE.md CHANGED
@@ -16,7 +16,8 @@ Do the steps in sequence. Each command has a label that tells you which shell to
16
16
  9. [Files and folders](#9-files-and-folders)
17
17
  10. [Troubleshooting](#10-troubleshooting)
18
18
  11. [Known limits](#11-known-limits)
19
- 12. [Remove the router](#12-remove-the-router)
19
+ 12. [Update the router](#12-update-the-router)
20
+ 13. [Remove the router](#13-remove-the-router)
20
21
 
21
22
  ## 1. Before you start
22
23
 
@@ -41,7 +42,22 @@ Make sure that you have these items:
41
42
 
42
43
  ## 2. Install the plugin
43
44
 
44
- ### Option A: from npm (recommended)
45
+ ### Option A: with the OpenCode command-line tool (fastest)
46
+
47
+ 1. Run this command. It works in PowerShell, CMD, Git Bash, macOS, and Linux:
48
+
49
+ ```bash
50
+ opencode plugin @s-berdia/opencode-router --global
51
+ ```
52
+
53
+ The command downloads the plugin and adds `"@s-berdia/opencode-router"` to the
54
+ `plugin` list of your global config file.
55
+ 2. Close OpenCode. Start OpenCode again.
56
+
57
+ If the `opencode` command is not found, use Option B. Or install the command-line tool
58
+ with `npm install -g opencode-ai`, and then do step 1 again.
59
+
60
+ ### Option B: from npm, by hand
45
61
 
46
62
  1. Open the global OpenCode config file.
47
63
 
@@ -81,7 +97,7 @@ Make sure that you have these items:
81
97
  To use the router in one project only, put the `plugin` entry in `opencode.json` in the
82
98
  root folder of that project, not in the global file.
83
99
 
84
- ### Option B: from source
100
+ ### Option C: from source
85
101
 
86
102
  Use this option to test changes to the code.
87
103
 
@@ -119,8 +135,8 @@ Use this option to test changes to the code.
119
135
 
120
136
  1. In the OpenCode prompt, type `/` and look at the list of commands. Make sure that
121
137
  these commands are in the list: `/refresh-routing`, `/routing-status`,
122
- `/routing-plan`, and `/routing-eval`. If they are not in the list, go to
123
- [Troubleshooting](#10-troubleshooting).
138
+ `/routing-plan`, `/routing-eval`, and `/routing-update`. If they are not in the list,
139
+ go to [Troubleshooting](#10-troubleshooting).
124
140
  2. Type this command and push Enter:
125
141
 
126
142
  ```text
@@ -311,6 +327,19 @@ models of that family.
311
327
  }
312
328
  ```
313
329
 
330
+ ### Stop the update check
331
+
332
+ Once each 24 hours, the router asks npm for the newest version number of the router. It
333
+ sends one request to `registry.npmjs.org` and no other data. When a newer version is
334
+ available, the router shows a toast in OpenCode and a line under the status and the
335
+ scorecard. To stop the check:
336
+
337
+ ```json
338
+ {
339
+ "updateCheck": false
340
+ }
341
+ ```
342
+
314
343
  ### Advanced cost settings
315
344
 
316
345
  | Key | Default | Meaning |
@@ -356,6 +385,7 @@ These commands work in PowerShell, CMD, Git Bash, macOS, and Linux.
356
385
  | `opencode-router plan go-plus --scope global` | Save the plan in the global settings, then calculate and write the routing table |
357
386
  | `opencode-router cat` | Show the catalog path, its date, and the number of models |
358
387
  | `opencode-router install --scope global` | Write the slash commands as files in `~/.config/opencode/command/`. If `router.config.json` does not exist, it also writes an empty one |
388
+ | `opencode-router update` | Ask npm for the newest version. If the copy that OpenCode keeps is older, remove it. See [Update the router](#12-update-the-router) |
359
389
 
360
390
  The terminal tool reads the global `router.config.json` and the project file of the
361
391
  current folder, in the same way as the plugin.
@@ -436,6 +466,7 @@ The router writes these files:
436
466
  | `~/.config/opencode/router/routing-table.json` | Plugin | The last routing table |
437
467
  | `~/.config/opencode/router/last-apply.json` | Plugin | The result of the last live apply |
438
468
  | `~/.config/opencode/router/eval-report.md` | `/routing-eval` | The test report |
469
+ | `~/.config/opencode/router/update-check.json` | Plugin, `status`, `refresh` | The time of the last update check and the newest version on npm |
439
470
  | `~/.config/opencode/opencode.json` | `/refresh-routing`, `/routing-plan` | `model`, `small_model`, and `agent.<role>.model` and `variant` |
440
471
  | `~/.config/opencode/router.config.json` | `/routing-plan`, `install` (only if missing) | Your router settings |
441
472
 
@@ -466,7 +497,7 @@ Do not commit the `router/` folder. It contains your usage data.
466
497
 
467
498
  ## 11. Known limits
468
499
 
469
- These are the limits of version 1.0.1.
500
+ These are the limits of version 1.1.1.
470
501
 
471
502
  1. The router sets the model of the `plan`, `build`, `general`, and `explore` agents at
472
503
  each message. If you select a different model by hand, the router replaces it. To
@@ -483,7 +514,59 @@ These are the limits of version 1.0.1.
483
514
  rate-limit error for a full session puts only the model of that session into a
484
515
  cooldown.
485
516
 
486
- ## 12. Remove the router
517
+ ## 12. Update the router
518
+
519
+ OpenCode downloads the plugin one time and then uses its own copy. It does not download
520
+ a newer version automatically. The router looks for a newer version once each 24 hours
521
+ and tells you when one is available.
522
+
523
+ ### With the slash command
524
+
525
+ 1. In the OpenCode prompt, type this command and push Enter:
526
+
527
+ ```text
528
+ /routing-update
529
+ ```
530
+
531
+ 2. Read the result:
532
+ - `OpenCode has opencode-router <version>, the newest version.`: you do not have to
533
+ do more.
534
+ - `Removed the cached copy: ...`: go to step 3.
535
+ - `opencode.json pins an older version ...`: do the steps in
536
+ [With a fixed version](#with-a-fixed-version).
537
+ 3. Close OpenCode. Start OpenCode again. OpenCode downloads the newest version when it
538
+ starts.
539
+ 4. Type `/routing-status`. If the line `opencode-router <version> is available` does not
540
+ show, the update is complete.
541
+
542
+ Between steps 2 and 3, the router slash commands do not work, because the files that they
543
+ use are removed. Restart OpenCode immediately.
544
+
545
+ ### From a terminal
546
+
547
+ If the slash commands do not work, close OpenCode and run this command. Then start
548
+ OpenCode again.
549
+
550
+ ```bash
551
+ npx @s-berdia/opencode-router@latest update
552
+ ```
553
+
554
+ This command works in PowerShell, CMD, Git Bash, macOS, and Linux.
555
+
556
+ ### With a fixed version
557
+
558
+ To select the version yourself, write it in the `plugin` list:
559
+
560
+ ```json
561
+ {
562
+ "plugin": ["@s-berdia/opencode-router@1.1.1"]
563
+ }
564
+ ```
565
+
566
+ To update, change the number to the new version and restart OpenCode. The newest version
567
+ is on <https://www.npmjs.com/package/@s-berdia/opencode-router>.
568
+
569
+ ## 13. Remove the router
487
570
 
488
571
  1. Open `opencode.json` (see [step 2](#2-install-the-plugin)).
489
572
  2. Remove `"@s-berdia/opencode-router"` from the `plugin` list.
@@ -509,5 +592,6 @@ These are the limits of version 1.0.1.
509
592
  ```
510
593
 
511
594
  6. If you ran `install`, delete these files from `~/.config/opencode/command/`:
512
- `refresh-routing.md`, `routing-status.md`, `routing-plan.md`, and `routing-eval.md`.
595
+ `refresh-routing.md`, `routing-status.md`, `routing-plan.md`, `routing-eval.md`, and
596
+ `routing-update.md`.
513
597
  7. Restart OpenCode.
package/dist/bin.js CHANGED
@@ -822,12 +822,12 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
822
822
  }
823
823
  return result;
824
824
  }
825
- function findNodeAtLocation(root, path5) {
825
+ function findNodeAtLocation(root, path6) {
826
826
  if (!root) {
827
827
  return void 0;
828
828
  }
829
829
  let node = root;
830
- for (let segment of path5) {
830
+ for (let segment of path6) {
831
831
  if (typeof segment === "string") {
832
832
  if (node.type !== "object" || !Array.isArray(node.children)) {
833
833
  return void 0;
@@ -1181,14 +1181,14 @@ function getNodeType(value) {
1181
1181
 
1182
1182
  // node_modules/jsonc-parser/lib/esm/impl/edit.js
1183
1183
  function setProperty(text, originalPath, value, options) {
1184
- const path5 = originalPath.slice();
1184
+ const path6 = originalPath.slice();
1185
1185
  const errors = [];
1186
1186
  const root = parseTree(text, errors);
1187
1187
  let parent = void 0;
1188
1188
  let lastSegment = void 0;
1189
- while (path5.length > 0) {
1190
- lastSegment = path5.pop();
1191
- parent = findNodeAtLocation(root, path5);
1189
+ while (path6.length > 0) {
1190
+ lastSegment = path6.pop();
1191
+ parent = findNodeAtLocation(root, path6);
1192
1192
  if (parent === void 0 && value !== void 0) {
1193
1193
  if (typeof lastSegment === "string") {
1194
1194
  value = { [lastSegment]: value };
@@ -1413,8 +1413,8 @@ function printParseErrorCode(code) {
1413
1413
  }
1414
1414
  return "<unknown ParseErrorCode>";
1415
1415
  }
1416
- function modify(text, path5, value, options) {
1417
- return setProperty(text, path5, value, options);
1416
+ function modify(text, path6, value, options) {
1417
+ return setProperty(text, path6, value, options);
1418
1418
  }
1419
1419
  function applyEdits(text, edits) {
1420
1420
  let sortedEdits = edits.slice(0).sort((a, b) => {
@@ -1443,10 +1443,13 @@ function globalConfigDir() {
1443
1443
  const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".config");
1444
1444
  return path.join(base, "opencode");
1445
1445
  }
1446
- function catalogCacheFile() {
1446
+ function opencodeCacheDir() {
1447
1447
  const xdg = process.env.XDG_CACHE_HOME;
1448
1448
  const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".cache");
1449
- return path.join(base, "opencode", "models.json");
1449
+ return path.join(base, "opencode");
1450
+ }
1451
+ function catalogCacheFile() {
1452
+ return path.join(opencodeCacheDir(), "models.json");
1450
1453
  }
1451
1454
  function globalRouterConfigFile() {
1452
1455
  return path.join(globalConfigDir(), "router.config.json");
@@ -1814,6 +1817,7 @@ function sanitizeUserConfig(raw, file, notes) {
1814
1817
  if (threshold !== void 0 && !(typeof threshold === "number" && threshold > 0 && threshold <= 1)) {
1815
1818
  drop("softThreshold", "must be a number above 0 and at most 1");
1816
1819
  }
1820
+ if (config.updateCheck !== void 0 && typeof config.updateCheck !== "boolean") drop("updateCheck", "must be true or false");
1817
1821
  for (const key of ["denyModels", "allowModels"]) {
1818
1822
  const list = config[key];
1819
1823
  if (list !== void 0 && !(Array.isArray(list) && list.every((item) => typeof item === "string"))) {
@@ -2542,7 +2546,8 @@ var SLASH_COMMANDS = [
2542
2546
  { name: "refresh-routing", description: "Recompute and apply OpenCode model routing", args: "refresh" },
2543
2547
  { name: "routing-status", description: "Show the model in use per role and its 5-hour quota use", args: "status" },
2544
2548
  { name: "routing-plan", description: "Switch the routing plan (go | go-plus | free) and rebuild", args: "plan $1" },
2545
- { name: "routing-eval", description: "Run the router scoring stress test", args: "eval" }
2549
+ { name: "routing-eval", description: "Run the router scoring stress test", args: "eval" },
2550
+ { name: "routing-update", description: "Update opencode-router to the newest version", args: "update" }
2546
2551
  ];
2547
2552
  var OUTPUT_BEGIN = "---BEGIN ROUTER OUTPUT---";
2548
2553
  function cliScriptPath() {
@@ -2594,6 +2599,9 @@ function ledgerFile() {
2594
2599
  function evalReportFile() {
2595
2600
  return path4.join(stateDir(), "eval-report.md");
2596
2601
  }
2602
+ function updateCheckFile() {
2603
+ return path4.join(stateDir(), "update-check.json");
2604
+ }
2597
2605
  function applyMarkerFile() {
2598
2606
  return path4.join(stateDir(), "last-apply.json");
2599
2607
  }
@@ -2740,6 +2748,97 @@ async function runEval(opts = {}) {
2740
2748
  return 0;
2741
2749
  }
2742
2750
 
2751
+ // src/update.ts
2752
+ import fs6 from "fs";
2753
+ import path5 from "path";
2754
+ import { fileURLToPath as fileURLToPath2 } from "url";
2755
+ var PACKAGE_NAME = "@s-berdia/opencode-router";
2756
+ var LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME.replace("/", "%2f")}/latest`;
2757
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2758
+ var TIMEOUT_MS = 3e3;
2759
+ function parseVersion(version) {
2760
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
2761
+ return match ? match.slice(1).map(Number) : null;
2762
+ }
2763
+ function isNewerVersion(a, b) {
2764
+ const left = parseVersion(a);
2765
+ const right = parseVersion(b);
2766
+ if (!left || !right) return false;
2767
+ for (let i = 0; i < 3; i++) {
2768
+ if (left[i] !== right[i]) return left[i] > right[i];
2769
+ }
2770
+ return false;
2771
+ }
2772
+ function installedVersion() {
2773
+ const file = fileURLToPath2(new URL("../package.json", import.meta.url));
2774
+ return readJson(file)?.version ?? "0.0.0";
2775
+ }
2776
+ async function fetchLatestVersion() {
2777
+ const controller = new AbortController();
2778
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
2779
+ try {
2780
+ const res = await fetch(LATEST_URL, { signal: controller.signal, headers: { accept: "application/json" } });
2781
+ if (!res.ok) return void 0;
2782
+ const body = await res.json();
2783
+ return typeof body.version === "string" ? body.version : void 0;
2784
+ } catch {
2785
+ return void 0;
2786
+ } finally {
2787
+ clearTimeout(timeout);
2788
+ }
2789
+ }
2790
+ async function checkForUpdate(opts = {}) {
2791
+ try {
2792
+ if (loadUserConfig(opts.projectDir).config.updateCheck === false) return null;
2793
+ const current = opts.current ?? installedVersion();
2794
+ const now = opts.now ?? Date.now();
2795
+ const state = readJson(updateCheckFile());
2796
+ let latest = state?.latest;
2797
+ const fresh = state !== void 0 && now >= state.checkedAt && now - state.checkedAt < CHECK_INTERVAL_MS;
2798
+ if (!fresh) {
2799
+ latest = await fetchLatestVersion() ?? latest;
2800
+ writeJson(updateCheckFile(), { checkedAt: now, latest });
2801
+ }
2802
+ return latest && isNewerVersion(latest, current) ? { current, latest } : null;
2803
+ } catch {
2804
+ return null;
2805
+ }
2806
+ }
2807
+ function updateNotice(update) {
2808
+ return `opencode-router ${update.latest} is available (installed: ${update.current}). To update, type /routing-update in OpenCode.`;
2809
+ }
2810
+ function clearOutdatedCopies(latest) {
2811
+ const [scope, name] = PACKAGE_NAME.split("/");
2812
+ const scopeDir = path5.join(opencodeCacheDir(), "packages", scope);
2813
+ const result = { removed: [], pinned: [], current: [], failed: [] };
2814
+ let entries = [];
2815
+ try {
2816
+ entries = fs6.readdirSync(scopeDir);
2817
+ } catch {
2818
+ return result;
2819
+ }
2820
+ for (const entry of entries) {
2821
+ const dir = path5.join(scopeDir, entry);
2822
+ if (entry === name || entry === `${name}@latest`) {
2823
+ const manifest = path5.join(dir, "node_modules", scope, name, "package.json");
2824
+ const version = readJson(manifest)?.version ?? "0.0.0";
2825
+ if (!isNewerVersion(latest, version)) {
2826
+ result.current.push(dir);
2827
+ continue;
2828
+ }
2829
+ try {
2830
+ fs6.rmSync(dir, { recursive: true, force: true, maxRetries: 3 });
2831
+ result.removed.push(dir);
2832
+ } catch (error) {
2833
+ result.failed.push({ dir, error: error instanceof Error ? error.message : String(error) });
2834
+ }
2835
+ } else if (entry.startsWith(`${name}@`) && isNewerVersion(latest, entry.slice(name.length + 1))) {
2836
+ result.pinned.push(entry);
2837
+ }
2838
+ }
2839
+ return result;
2840
+ }
2841
+
2743
2842
  // src/cli.ts
2744
2843
  var HELP = `opencode-router - dynamic model routing for OpenCode Go / Go Plus / Free
2745
2844
 
@@ -2750,6 +2849,7 @@ Usage:
2750
2849
  opencode-router install [--scope] Write slash-command files and a starter config
2751
2850
  opencode-router cat Show catalog metadata (path, date, model count)
2752
2851
  opencode-router eval Run the scoring stress test
2852
+ opencode-router update Look for a newer version and clear OpenCode's cached copy
2753
2853
 
2754
2854
  Options:
2755
2855
  --plan <go|go-plus|free> Plan profile for this run
@@ -2814,6 +2914,12 @@ function parseCommandLine(argv, cwd) {
2814
2914
  function build(opts) {
2815
2915
  return buildRoutingTable({ plan: opts.plan, catalogFile: opts.catalog, projectDir: opts.cwd });
2816
2916
  }
2917
+ async function writeUpdateNotice(opts) {
2918
+ const update = await checkForUpdate({ projectDir: opts.cwd });
2919
+ if (update) process.stdout.write(`
2920
+ ${updateNotice(update)}
2921
+ `);
2922
+ }
2817
2923
  async function cmdRefresh(opts) {
2818
2924
  const { table } = await build(opts);
2819
2925
  if (opts.json) {
@@ -2845,6 +2951,7 @@ Applied: ${detail}
2845
2951
  "A running OpenCode session recomputes routing on its next message (within 15 s). Restart to reload the UI display now.\n"
2846
2952
  );
2847
2953
  }
2954
+ await writeUpdateNotice(opts);
2848
2955
  }
2849
2956
  return 0;
2850
2957
  }
@@ -2858,6 +2965,7 @@ async function cmdStatus(opts) {
2858
2965
  process.stdout.write(JSON.stringify({ table: result.table, rows, usage }, null, 2) + "\n");
2859
2966
  } else {
2860
2967
  process.stdout.write(formatStatus(result.table, rows) + "\n");
2968
+ await writeUpdateNotice(opts);
2861
2969
  }
2862
2970
  return 0;
2863
2971
  }
@@ -2901,6 +3009,44 @@ function cmdInstall(opts) {
2901
3009
  );
2902
3010
  return 0;
2903
3011
  }
3012
+ async function cmdUpdate() {
3013
+ const latest = await fetchLatestVersion();
3014
+ if (!latest) {
3015
+ process.stderr.write("error: could not reach the npm registry. Try again later.\n");
3016
+ return 1;
3017
+ }
3018
+ const { removed, pinned, current, failed } = clearOutdatedCopies(latest);
3019
+ if (failed.length > 0) {
3020
+ for (const { dir, error } of failed) process.stderr.write(`error: could not remove ${dir} (${error})
3021
+ `);
3022
+ process.stderr.write(`Close OpenCode, then run this command in a terminal: npx ${PACKAGE_NAME}@latest update
3023
+ `);
3024
+ return 1;
3025
+ }
3026
+ for (const dir of removed) process.stdout.write(`Removed the cached copy: ${dir}
3027
+ `);
3028
+ if (removed.length > 0) {
3029
+ process.stdout.write(`Restart OpenCode. It then installs opencode-router ${latest}.
3030
+ `);
3031
+ }
3032
+ if (pinned.length > 0) {
3033
+ process.stdout.write(
3034
+ `opencode.json pins an older version (${pinned.join(", ")}). Change the plugin entry to "${PACKAGE_NAME}@${latest}", then restart OpenCode.
3035
+ `
3036
+ );
3037
+ }
3038
+ if (removed.length > 0 || pinned.length > 0) return 0;
3039
+ if (current.length > 0) {
3040
+ process.stdout.write(`OpenCode has opencode-router ${latest}, the newest version.
3041
+ `);
3042
+ } else {
3043
+ process.stdout.write(
3044
+ `OpenCode has no cached copy of opencode-router. The newest version is ${latest}. If you use a local build, run git pull and npm run build. If your OpenCode has the command "opencode plugin update", run it.
3045
+ `
3046
+ );
3047
+ }
3048
+ return 0;
3049
+ }
2904
3050
  function usageError(error) {
2905
3051
  process.stderr.write(`error: ${error.message}
2906
3052
 
@@ -2928,6 +3074,8 @@ async function main(argv, cwd = process.cwd()) {
2928
3074
  return cmdInstall(opts);
2929
3075
  case "eval":
2930
3076
  return await runEval({ plan: opts.plan, catalog: opts.catalog, json: opts.json, projectDir: opts.cwd });
3077
+ case "update":
3078
+ return await cmdUpdate();
2931
3079
  default:
2932
3080
  throw new UsageError(`unknown command "${command}"`);
2933
3081
  }