cursor-route 0.1.15 → 0.1.16
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 +6 -0
- package/README.md +8 -7
- package/SECURITY.md +1 -1
- package/dist/adapters/opencode.js +4 -4
- package/dist/config.js +1 -1
- package/dist/zen-free.js +30 -15
- package/docs/briefs/WORKING.md +9 -6
- package/docs/demo-notes.md +2 -2
- package/docs/fixtures/hero-demo.log +2 -2
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/skills/route-orch/SKILL.md +1 -1
- package/src/adapters/opencode.test.ts +1 -1
- package/src/adapters/opencode.ts +4 -4
- package/src/cli.test.ts +3 -3
- package/src/config.ts +1 -1
- package/src/zen-free.test.ts +83 -13
- package/src/zen-free.ts +29 -12
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
_(none)_
|
|
6
6
|
|
|
7
|
+
## 0.1.16 — 2026-09-16
|
|
8
|
+
|
|
9
|
+
- `--worker opencode --model free` now ranks the **strongest listed-free coding model** by capability tokens (ultra/pro/max > preview > generic > flash/lite), then coding-family, then `context_length`. No locked live winner id.
|
|
10
|
+
- Ox Alpha (`opencode/x-preview-f-free`) is the offline / fetch-fail / empty-catalog **fallback only**, not a live pin. Pin `CURSOR_ROUTE_OPENCODE_MODEL` to freeze an id.
|
|
11
|
+
- `hy3-free` is may-train (−20), not a coding-family boost. Contributor-free is penalized. Mid lane still `claude-ds`; no worker swap. Hero demo fixture regenerated to **0.1.16**.
|
|
12
|
+
|
|
7
13
|
## 0.1.15 — 2026-09-11
|
|
8
14
|
|
|
9
15
|
- Mid default is `deepseek-flash` (DeepSeek V4.1 Flash, native vision). `--model pro` and `--model vision` are legacy aliases to the same id. Pro is off rotation.
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ If you already live in Cursor, X Premium (Grok CLI), and DeepSeek — stop payin
|
|
|
29
29
|
| Grok CLI | X Premium | `--lane hard` implement |
|
|
30
30
|
| DeepSeek via claude-ds | DeepSeek API / plan | `--lane mid` implement (**Flash** default; `--model vision` for screenshots; `--model pro` harder mid / hard backup only) |
|
|
31
31
|
| OpenRouter free models | OpenRouter API (free tier) | `--lane easy` live free pick at start — non-secret prompts only (see Security) |
|
|
32
|
-
| OpenCode (opt-in) | OpenCode Zen free models | `--worker opencode` implement on live Zen free pick (
|
|
32
|
+
| OpenCode (opt-in) | OpenCode Zen free models | `--worker opencode` implement on live Zen free pick (strongest listed-free; no locked id) |
|
|
33
33
|
| Codex / extra Claude | Optional | Not required for v0 |
|
|
34
34
|
|
|
35
35
|
Exact dollars vary — the point is **reuse subscriptions you already have**.
|
|
@@ -116,10 +116,10 @@ docs/fixtures/generate-hero-demo.sh # regenerate docs/fixtures/hero-demo.lo
|
|
|
116
116
|
|
|
117
117
|
```text
|
|
118
118
|
$ cursor-route --version
|
|
119
|
-
0.1.
|
|
119
|
+
0.1.16
|
|
120
120
|
|
|
121
121
|
$ CURSOR_ROUTE_RELAXED=1 cursor-route health
|
|
122
|
-
cursor-route v0.1.
|
|
122
|
+
cursor-route v0.1.16
|
|
123
123
|
health: OK
|
|
124
124
|
|
|
125
125
|
$ cursor-route start --lane mid --model flash --dry-run "Add a unit test for shellQuote"
|
|
@@ -243,9 +243,10 @@ key-shaped material in `start` / `send`, and the runner re-checks the prompt fil
|
|
|
243
243
|
## OpenCode setup (opt-in free coding worker)
|
|
244
244
|
|
|
245
245
|
`--worker opencode` runs [OpenCode](https://opencode.ai) as a **coding agent** on
|
|
246
|
-
live OpenCode Zen free models (`--model free` fetches the catalog and ranks
|
|
247
|
-
same idea as the OpenRouter free picker).
|
|
248
|
-
|
|
246
|
+
live OpenCode Zen free models (`--model free` fetches the catalog and ranks the
|
|
247
|
+
strongest listed-free coding model, same idea as the OpenRouter free picker).
|
|
248
|
+
There is no locked live id. Offline / empty-catalog fallback is
|
|
249
|
+
`opencode/x-preview-f-free`. This is **not** a lane default — mid stays
|
|
249
250
|
`claude-ds`; easy stays OpenRouter chat (no tools). Use it to burn fewer Grok /
|
|
250
251
|
DeepSeek tokens on implement work.
|
|
251
252
|
|
|
@@ -271,7 +272,7 @@ for ~15 minutes (`CURSOR_ROUTE_ZEN_CACHE_MINUTES`).
|
|
|
271
272
|
**Non-secret prompts:** several Zen free models may log or train on prompts during
|
|
272
273
|
their free period (see [OpenCode Zen](https://opencode.ai/docs/zen/) privacy notes).
|
|
273
274
|
The same refuse gate as every lane still applies. `opencode/x-preview-f-free`
|
|
274
|
-
(Ox Alpha) is the zero-retention
|
|
275
|
+
(Ox Alpha) is the zero-retention **fallback** option if you need it.
|
|
275
276
|
|
|
276
277
|
## Jobs directory
|
|
277
278
|
|
package/SECURITY.md
CHANGED
|
@@ -15,7 +15,7 @@ Do **not** open a public issue for credential leaks or RCE-class bugs.
|
|
|
15
15
|
|
|
16
16
|
1. **No secrets in prompts or job files.** Do not paste API keys, SSH private keys, Discord tokens, or `.env` contents into `cursor-route start` / `send` prompts.
|
|
17
17
|
2. **Free OpenRouter easy-lane models may log prompts.** `--lane easy` / `--worker openrouter` is for **non-secret** wording/drafts only. The refuse gate (`start` / `send`, plus the runner's re-check) applies to every lane — never send key-shaped material to the easy lane.
|
|
18
|
-
3. **OpenCode Zen free models may log or train during their free period.** `--worker opencode --model free`
|
|
18
|
+
3. **OpenCode Zen free models may log or train during their free period.** `--worker opencode --model free` ranks the strongest listed-free coding model. Treat free Zen models like the easy lane for secrets. Pin `CURSOR_ROUTE_OPENCODE_MODEL` for a frozen id. Ox Alpha (`opencode/x-preview-f-free`) remains the zero-retention **fallback** option.
|
|
19
19
|
4. **Always-approve is powerful.** Default headless workers skip interactive permission prompts (`--always-approve` / `--dangerously-skip-permissions` / OpenCode `--auto`). Opt out with `--ask` / `CURSOR_ROUTE_ASK=1` when you need HITL.
|
|
20
20
|
5. **Install from known channels only.** Prefer `npm i -g cursor-route` or a git clone of this repo. Do not pipe unknown `curl | bash` installers as the primary path.
|
|
21
21
|
6. **Workers inherit your auth.** Grok CLI, Claude Code / claude-ds, and OpenCode use *your* local login. Treat worker panes like you would treat your own shell.
|
|
@@ -4,9 +4,9 @@ import { shellQuote } from "../util.js";
|
|
|
4
4
|
import { openCodeModel, OPENCODE_DEFAULT_MODEL, cachedZenFreePick } from "../config.js";
|
|
5
5
|
/**
|
|
6
6
|
* Opt-in OpenCode worker (`opencode run`) — a coding agent on live OpenCode
|
|
7
|
-
* Zen free models (`--model free` ranks the
|
|
8
|
-
*
|
|
9
|
-
* chat (no tools).
|
|
7
|
+
* Zen free models (`--model free` ranks the strongest listed-free coding
|
|
8
|
+
* model). Not a lane default: mid stays claude-ds; easy stays OpenRouter
|
|
9
|
+
* chat (no tools). Pin `CURSOR_ROUTE_OPENCODE_MODEL` to freeze an id.
|
|
10
10
|
*
|
|
11
11
|
* We never rewrite ~/.config/opencode/opencode.json (parallel jobs would
|
|
12
12
|
* race). Always-approve maps to `opencode run --auto` (still honors explicit
|
|
@@ -42,7 +42,7 @@ export const opencodeAdapter = {
|
|
|
42
42
|
detail: "opencode not found — install: npm i -g opencode-ai (or brew install opencode). Then: opencode auth login. Mid default remains claude-ds.",
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
-
// Health stays offline: show a fresh cache hit, else the
|
|
45
|
+
// Health stays offline: show a fresh cache hit, else the fallback id.
|
|
46
46
|
const pick = cachedZenFreePick() || OPENCODE_DEFAULT_MODEL;
|
|
47
47
|
return {
|
|
48
48
|
worker: "opencode",
|
package/dist/config.js
CHANGED
package/dist/zen-free.js
CHANGED
|
@@ -11,12 +11,15 @@ import { join } from "node:path";
|
|
|
11
11
|
* `big-pickle` / `x-preview-f-free`; the CLI wants `opencode/<id>`.
|
|
12
12
|
*/
|
|
13
13
|
export const ZEN_MODELS_URL_DEFAULT = "https://opencode.ai/zen/v1/models";
|
|
14
|
-
/** Offline / fetch-fail fallback —
|
|
14
|
+
/** Offline / fetch-fail fallback only — not a live pin. */
|
|
15
15
|
export const OPENCODE_DEFAULT_MODEL = "opencode/x-preview-f-free";
|
|
16
16
|
const EXCLUDE_RE = /lyria|whisper|tts|embed|embedding|image|vision-only|audio|diffusion|flux|stable-diffusion|moderation/i;
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
17
|
+
const POWER_ULTRA_RE = /ultra|(^|[-/])opus($|[-/])|(^|[-/_])pro($|[-/])|(^|[-/])max($|[-/])|plus|(^|[-/])large($|[-/])/;
|
|
18
|
+
const POWER_PREVIEW_RE = /x-preview|ox-alpha|oxalpha|(^|[-/])alpha($|[-/])|preview/;
|
|
19
|
+
const POWER_FLASH_RE = /flash|lightning|lite|mini|nano|small|haiku|fin-/;
|
|
20
|
+
const CODING_FAMILY_RE = /coder|code|instruct|laguna|glm|qwen|kimi|deepseek|gpt-oss|north|gemma|nemotron|mimo|muse/;
|
|
21
|
+
const CONTRIBUTOR_RE = /contributor-free/;
|
|
22
|
+
const MAY_TRAIN_RE = /(^|\/)big-pickle$|(^|\/)hy3-free$/;
|
|
20
23
|
function zenModelsUrl() {
|
|
21
24
|
return (process.env.CURSOR_ROUTE_ZEN_MODELS_URL || ZEN_MODELS_URL_DEFAULT).trim();
|
|
22
25
|
}
|
|
@@ -65,20 +68,32 @@ export function isZenFreeModel(m) {
|
|
|
65
68
|
/(^|\/)big-pickle$/i.test(id);
|
|
66
69
|
return tagged || pricedFree;
|
|
67
70
|
}
|
|
68
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Higher boost wins. Generic capability tokens only — never a locked model id.
|
|
73
|
+
* Coding-family matches the model slug so the provider `opencode/` does not
|
|
74
|
+
* trip `code`.
|
|
75
|
+
*/
|
|
69
76
|
export function zenFreeBoost(id) {
|
|
70
77
|
const s = id.toLowerCase();
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
const slug = s.includes("/") ? s.slice(s.lastIndexOf("/") + 1) : s;
|
|
79
|
+
let score = 0;
|
|
80
|
+
if (POWER_ULTRA_RE.test(s))
|
|
81
|
+
score += 50;
|
|
82
|
+
else if (POWER_PREVIEW_RE.test(s))
|
|
83
|
+
score += 42;
|
|
84
|
+
else if (POWER_FLASH_RE.test(s))
|
|
85
|
+
score += 15;
|
|
86
|
+
else
|
|
87
|
+
score += 30;
|
|
88
|
+
if (CODING_FAMILY_RE.test(slug))
|
|
89
|
+
score += 20;
|
|
90
|
+
else if (s.endsWith("-free") || s.endsWith(":free"))
|
|
91
|
+
score += 10;
|
|
73
92
|
if (CONTRIBUTOR_RE.test(s))
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return 10;
|
|
79
|
-
if (/(^|\/)big-pickle$/.test(s))
|
|
80
|
-
return 5;
|
|
81
|
-
return 0;
|
|
93
|
+
score -= 25;
|
|
94
|
+
if (MAY_TRAIN_RE.test(s))
|
|
95
|
+
score -= 20;
|
|
96
|
+
return score;
|
|
82
97
|
}
|
|
83
98
|
export function rankZenFreeModels(models) {
|
|
84
99
|
const out = [];
|
package/docs/briefs/WORKING.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: cursor-route workspace — working brief (edit in place)
|
|
3
3
|
repo: ~/Projects/cursor-route
|
|
4
|
-
npm: cursor-route@0.1.
|
|
4
|
+
npm: cursor-route@0.1.16
|
|
5
5
|
created: 2026-08-12
|
|
6
|
-
updated: 2026-09-
|
|
6
|
+
updated: 2026-09-16
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# cursor-route — living brief
|
|
@@ -21,11 +21,11 @@ Cursor Agent plans. Workers run in tmux via `cursor-route`:
|
|
|
21
21
|
| `easy` | OpenRouter free (live pick) | Wording / drafts — non-secret prompts only |
|
|
22
22
|
| `mid` | claude-ds (DeepSeek behind Claude Code) | Default implement (**Flash** `deepseek-flash`; `--model vision` / `--model pro` are legacy aliases to the same id) |
|
|
23
23
|
| `hard` | Grok CLI | Hard implement |
|
|
24
|
-
| opt-in | OpenCode | `--worker opencode` coding agent; `--model free` ranks
|
|
24
|
+
| opt-in | OpenCode | `--worker opencode` coding agent; `--model free` ranks strongest listed-free Zen coding model (no locked id; offline fallback `opencode/x-preview-f-free`) |
|
|
25
25
|
|
|
26
26
|
Always-approve on for coding worktrees (`--ask` / `CURSOR_ROUTE_ASK=1` to opt out) — not LIVE Discord/trading. Jobs live in `~/.local/share/cursor-route/jobs`, not in this clone.
|
|
27
27
|
|
|
28
|
-
Install: `npm i -g cursor-route` → **0.1.
|
|
28
|
+
Install: `npm i -g cursor-route` → **0.1.16** (parent publishes). Release notes: [CHANGELOG.md](../../CHANGELOG.md).
|
|
29
29
|
|
|
30
30
|
## Open (edit / check off)
|
|
31
31
|
|
|
@@ -44,8 +44,9 @@ Install: `npm i -g cursor-route` → **0.1.15** (not LIVE until parent publishes
|
|
|
44
44
|
- [x] **Flash-first + vision + live OpenRouter pick** — 0.1.12: `--model flash` stays the cheap Grok-out default; `--model vision` + screenshot auto-pick; easy lane live-picks free OpenRouter text models (`openrouter/free` is fetch-fail fallback only). Health never fetches `/models`. `route-orch` ProgRouter + MoRe one-liners. Hero fixture regenerated to 0.1.12. No npm publish in this slice.
|
|
45
45
|
- [x] **Kimi 0.1.13 follow-up** — do not cache OpenRouter fallback; health labels uncached fallback; no Authorization on GET /models; env-invalid model error names the env vars; `\bimage\b` vision trigger.
|
|
46
46
|
- [x] **Super-audit OR ranker parity (0.1.14)** — tier-rank Qwen/GLM/Kimi above Nemotron 550B in `or-free.ts` (match agent-toolkit `select-openrouter-free-model.ps1`); brief `docs/briefs/2026-09-02_super-audit-or-ranker-parity.md`
|
|
47
|
-
- [x] **V4.1 Flash wire (0.1.15)** — mid default `deepseek-flash` (native vision). `--model pro` / `--model vision` are legacy aliases to the same id. Pro is off rotation. Mid still `claude-ds`. Hero fixture regenerated.
|
|
47
|
+
- [x] **V4.1 Flash wire (0.1.15)** — mid default `deepseek-flash` (native vision). `--model pro` / `--model vision` are legacy aliases to the same id. Pro is off rotation. Mid still `claude-ds`. Hero fixture regenerated. Brief `docs/briefs/2026-09-11_v41-flash-not-pro.md`
|
|
48
48
|
- [ ] **Hero GIF** — still outstanding; dry-run fixture ships as the substitute for now (`docs/fixtures/hero-demo.log` — see `docs/DEMO_GIF.md`)
|
|
49
|
+
- [x] **Zen strongest-free ranker (0.1.16)** — `zenFreeBoost` ports capability tokens (ultra/pro/max > preview > generic > flash/lite), then coding-family, then `context_length`. No locked live id. Ox Alpha is offline fallback only. SIP: `docs/briefs/2026-09-16_zen-strongest-free.md`. Mid still `claude-ds`.
|
|
49
50
|
- [x] **Do not** paste private `ROUTE_KIT`, SIP, prod paths, or hang-watchdog env into this public repo
|
|
50
51
|
|
|
51
52
|
## Repo map
|
|
@@ -84,4 +85,6 @@ Install: `npm i -g cursor-route` → **0.1.15** (not LIVE until parent publishes
|
|
|
84
85
|
| 2026-08-29 | Kimi audit follow-up → 0.1.13 LIVE on npm: no fallback cache, health labels, unauth GET /models, env error wording. |
|
|
85
86
|
| 2026-09-02 | Tier-rank OpenRouter free picks in `or-free.ts` (Qwen 100 / GLM-Kimi 95 / DeepSeek 90 / coding 70 / Nemotron 15 / default 40; ctx bonus capped at 131072) → 0.1.14. Commit + tag `v0.1.14`. Mid stays claude-ds. |
|
|
86
87
|
| 2026-09-02 | Kimi audit follow-up → ranker tests (seven tiers, score/id ties, 131072 cap, cache round-trip) then **0.1.14 LIVE** on npm. |
|
|
87
|
-
| 2026-09-11 | Map mid models to `deepseek-flash` (V4.1 Flash, native vision). `--model pro` / `--model vision` are legacy aliases. Pro is off rotation. Mid still `claude-ds` → 0.1.15. |
|
|
88
|
+
| 2026-09-11 | Map mid models to `deepseek-flash` (V4.1 Flash, native vision). `--model pro` / `--model vision` are legacy aliases. Pro is off rotation. Mid still `claude-ds` → **0.1.15 LIVE** on npm. |
|
|
89
|
+
| 2026-09-16 | Pickup brief: `docs/briefs/2026-09-16_zen-strongest-free.md`. Port private `/route` Zen capability-token ranker so `--model free` picks strongest listed-free coding model. Target npm `0.1.16`. Open checkbox added. |
|
|
90
|
+
| 2026-09-16 | Ship Zen strongest-free ranker → **0.1.16** (commit + tag; parent publishes npm). Ox Alpha is offline fallback only, not a live pin. Mid still `claude-ds`. |
|
package/docs/demo-notes.md
CHANGED
package/llms.txt
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
> Cursor stays the planner. DeepSeek (mid), Grok CLI (hard), and OpenRouter free models (easy) run parallel coding workers in tmux. `--worker opencode` is an opt-in coding agent on OpenCode Zen free models.
|
|
4
4
|
|
|
5
|
-
MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.
|
|
5
|
+
MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.16**)
|
|
6
6
|
GitHub: https://github.com/cemini23/cursor-route
|
|
7
7
|
|
|
8
8
|
## FAQ
|
|
9
9
|
|
|
10
10
|
### What is cursor-route?
|
|
11
|
-
cursor-route is a public MIT CLI and Cursor skill that runs parallel coding workers in tmux while Cursor remains the planner. DeepSeek handles the mid lane, Grok CLI handles the hard lane, and OpenRouter free models handle the easy lane (wording/drafts, non-secret prompts only). `--worker opencode` is an opt-in coding agent (`--model free` ranks the
|
|
11
|
+
cursor-route is a public MIT CLI and Cursor skill that runs parallel coding workers in tmux while Cursor remains the planner. DeepSeek handles the mid lane, Grok CLI handles the hard lane, and OpenRouter free models handle the easy lane (wording/drafts, non-secret prompts only). `--worker opencode` is an opt-in coding agent (`--model free` ranks the strongest listed-free Zen coding model; pin to freeze) — not a lane default.
|
|
12
12
|
|
|
13
13
|
### How is this different from Codex orchestrator?
|
|
14
14
|
It uses the familiar strategist and worker-pane shape, but it is not a Codex clone. cursor-route uses Cursor as the planner and DeepSeek plus Grok CLI as workers. Codex is not required.
|
|
@@ -26,7 +26,7 @@ No. The mid worker is DeepSeek. Claude Code is the harness, configured with `ANT
|
|
|
26
26
|
This map matches DeepSeek's 2026-09-10 API id `deepseek-flash`.
|
|
27
27
|
|
|
28
28
|
### How do I use OpenCode free models?
|
|
29
|
-
Install OpenCode (`npm i -g opencode-ai`), run `opencode auth login`, then `cursor-route start --worker opencode "…"`. `--model free` ranks
|
|
29
|
+
Install OpenCode (`npm i -g opencode-ai`), run `opencode auth login`, then `cursor-route start --worker opencode "…"`. `--model free` ranks the strongest listed-free Zen coding model (offline fallback `opencode/x-preview-f-free`). Mid stays `claude-ds`.
|
|
30
30
|
|
|
31
31
|
### How do I install?
|
|
32
32
|
Run `npm i -g cursor-route`, install tmux if needed, then run `cursor-route health`. Copy the Cursor skill from `$(npm root -g)/cursor-route/skills/route-orch`. Source: https://github.com/cemini23/cursor-route
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-route",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter easy lane + OpenCode (opt-in free) are the parallel army \u2014 lane-aware /route orchestration in tmux.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -75,7 +75,7 @@ cursor-route start --worker opencode --model free --dir "$PWD" "…"
|
|
|
75
75
|
cursor-route start --worker opencode --model opencode/hy3-free --dir "$PWD" "…"
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
Health ✓ needs `opencode` on PATH (override `CURSOR_ROUTE_OPENCODE_BIN`). `--model free` / unset ranks the
|
|
78
|
+
Health ✓ needs `opencode` on PATH (override `CURSOR_ROUTE_OPENCODE_BIN`). `--model free` / unset ranks the strongest listed-free OpenCode Zen coding model (pin with `CURSOR_ROUTE_OPENCODE_MODEL` or `--model provider/model` to freeze). Always-approve → `opencode run --auto`; `--ask` omits `--auto`. Never rewrites `~/.config/opencode/opencode.json`. Free Zen models may log/train — keep secrets off this worker (same refuse gate as easy). Ox Alpha (`opencode/x-preview-f-free`) is the zero-retention fallback option.
|
|
79
79
|
|
|
80
80
|
## Workflow
|
|
81
81
|
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "../config.ts";
|
|
12
12
|
|
|
13
13
|
describe("openCodeModel", () => {
|
|
14
|
-
test("defaults to live Zen pick (
|
|
14
|
+
test("defaults to live Zen pick (fallback id when offline)", () => {
|
|
15
15
|
const prev = {
|
|
16
16
|
model: process.env.CURSOR_ROUTE_OPENCODE_MODEL,
|
|
17
17
|
offline: process.env.CURSOR_ROUTE_ZEN_OFFLINE,
|
package/src/adapters/opencode.ts
CHANGED
|
@@ -6,9 +6,9 @@ import { openCodeModel, OPENCODE_DEFAULT_MODEL, cachedZenFreePick } from "../con
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Opt-in OpenCode worker (`opencode run`) — a coding agent on live OpenCode
|
|
9
|
-
* Zen free models (`--model free` ranks the
|
|
10
|
-
*
|
|
11
|
-
* chat (no tools).
|
|
9
|
+
* Zen free models (`--model free` ranks the strongest listed-free coding
|
|
10
|
+
* model). Not a lane default: mid stays claude-ds; easy stays OpenRouter
|
|
11
|
+
* chat (no tools). Pin `CURSOR_ROUTE_OPENCODE_MODEL` to freeze an id.
|
|
12
12
|
*
|
|
13
13
|
* We never rewrite ~/.config/opencode/opencode.json (parallel jobs would
|
|
14
14
|
* race). Always-approve maps to `opencode run --auto` (still honors explicit
|
|
@@ -47,7 +47,7 @@ export const opencodeAdapter: Adapter = {
|
|
|
47
47
|
"opencode not found — install: npm i -g opencode-ai (or brew install opencode). Then: opencode auth login. Mid default remains claude-ds.",
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
// Health stays offline: show a fresh cache hit, else the
|
|
50
|
+
// Health stays offline: show a fresh cache hit, else the fallback id.
|
|
51
51
|
const pick = cachedZenFreePick() || OPENCODE_DEFAULT_MODEL;
|
|
52
52
|
return {
|
|
53
53
|
worker: "opencode",
|
package/src/cli.test.ts
CHANGED
|
@@ -935,7 +935,7 @@ describe("health", () => {
|
|
|
935
935
|
test("returns structured report", () => {
|
|
936
936
|
const r = runHealth();
|
|
937
937
|
expect(r.product).toBe("cursor-route");
|
|
938
|
-
expect(r.version).toBe("0.1.
|
|
938
|
+
expect(r.version).toBe("0.1.16");
|
|
939
939
|
expect(r.checks.length).toBeGreaterThan(3);
|
|
940
940
|
expect(r.checks.some((c) => c.name === "tmux")).toBe(true);
|
|
941
941
|
expect(r.checks.some((c) => c.name === "cursor_cli")).toBe(true);
|
|
@@ -947,8 +947,8 @@ describe("health", () => {
|
|
|
947
947
|
expect(r.checks.some((c) => c.name === "worker:deepseek")).toBe(true);
|
|
948
948
|
});
|
|
949
949
|
|
|
950
|
-
test("config version is 0.1.
|
|
951
|
-
expect(config.version).toBe("0.1.
|
|
950
|
+
test("config version is 0.1.16", () => {
|
|
951
|
+
expect(config.version).toBe("0.1.16");
|
|
952
952
|
});
|
|
953
953
|
|
|
954
954
|
test("OR-gate: ok can be true while worker:opencode is false", () => {
|
package/src/config.ts
CHANGED
package/src/zen-free.test.ts
CHANGED
|
@@ -38,30 +38,91 @@ describe("zen free catalog", () => {
|
|
|
38
38
|
).toBe(true);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test("
|
|
42
|
-
expect(zenFreeBoost("opencode/
|
|
43
|
-
expect(zenFreeBoost("opencode/
|
|
41
|
+
test("capability tokens: ultra > preview > generic coding > flash > may-train", () => {
|
|
42
|
+
expect(zenFreeBoost("opencode/nemotron-3-ultra-free")).toBe(70);
|
|
43
|
+
expect(zenFreeBoost("opencode/x-preview-f-free")).toBe(52);
|
|
44
|
+
expect(zenFreeBoost("opencode/laguna-s-2.1-free")).toBe(50);
|
|
45
|
+
expect(zenFreeBoost("opencode/deepseek-v4-flash-free")).toBe(35);
|
|
46
|
+
expect(zenFreeBoost("opencode/muse-spark-1.2-contributor-free")).toBe(25);
|
|
44
47
|
expect(zenFreeBoost("opencode/hy3-free")).toBe(20);
|
|
45
|
-
expect(zenFreeBoost("opencode/big-pickle")).toBe(
|
|
46
|
-
expect(zenFreeBoost("opencode/muse-spark-1.2-contributor-free")).toBe(1);
|
|
48
|
+
expect(zenFreeBoost("opencode/big-pickle")).toBe(10);
|
|
47
49
|
});
|
|
48
50
|
|
|
49
|
-
test("
|
|
51
|
+
test("relative boosts: ultra > preview > flash > big-pickle", () => {
|
|
52
|
+
expect(zenFreeBoost("opencode/nemotron-3-ultra-free")).toBeGreaterThan(
|
|
53
|
+
zenFreeBoost("opencode/x-preview-f-free"),
|
|
54
|
+
);
|
|
55
|
+
expect(zenFreeBoost("opencode/x-preview-f-free")).toBeGreaterThan(
|
|
56
|
+
zenFreeBoost("opencode/deepseek-v4-flash-free"),
|
|
57
|
+
);
|
|
58
|
+
expect(zenFreeBoost("opencode/deepseek-v4-flash-free")).toBeGreaterThan(
|
|
59
|
+
zenFreeBoost("opencode/big-pickle"),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("hy3-free is may-train, not coding-family", () => {
|
|
64
|
+
// else 30 + -free 10 − 20 may-train = 20. Coding-family would be 30.
|
|
65
|
+
expect(zenFreeBoost("opencode/hy3-free")).toBe(20);
|
|
66
|
+
expect(zenFreeBoost("opencode/hy3-free")).toBeLessThan(
|
|
67
|
+
zenFreeBoost("opencode/laguna-s-2.1-free"),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("rank: paid/whisper dropped; preview may beat laguna (52 > 50)", () => {
|
|
50
72
|
const ranked = rankZenFreeModels(CATALOG);
|
|
51
73
|
expect(ranked[0]?.id).toBe("opencode/x-preview-f-free");
|
|
52
74
|
expect(ranked.map((r) => r.id)).not.toContain("opencode/claude-opus-4-6");
|
|
53
75
|
expect(ranked.map((r) => r.id)).not.toContain("opencode/whisper-free");
|
|
54
|
-
expect(ranked.at(-1)?.id).toBe("opencode/
|
|
76
|
+
expect(ranked.at(-1)?.id).toBe("opencode/big-pickle");
|
|
55
77
|
});
|
|
56
78
|
|
|
57
|
-
test("rank without
|
|
79
|
+
test("rank without preview: coding free beats big-pickle", () => {
|
|
58
80
|
const ranked = rankZenFreeModels([
|
|
59
81
|
{ id: "big-pickle" },
|
|
60
82
|
{ id: "laguna-s-2.1-free" },
|
|
61
83
|
{ id: "muse-spark-1.2-contributor-free" },
|
|
62
84
|
]);
|
|
63
85
|
expect(ranked[0]?.id).toBe("opencode/laguna-s-2.1-free");
|
|
64
|
-
expect(ranked[1]?.id).toBe("opencode/
|
|
86
|
+
expect(ranked[1]?.id).toBe("opencode/muse-spark-1.2-contributor-free");
|
|
87
|
+
expect(ranked[2]?.id).toBe("opencode/big-pickle");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("hy3 + preview + big-pickle → preview", () => {
|
|
91
|
+
expect(
|
|
92
|
+
pickZenFreeModel({
|
|
93
|
+
catalog: [{ id: "hy3-free" }, { id: "x-preview-f-free" }, { id: "big-pickle" }],
|
|
94
|
+
}),
|
|
95
|
+
).toBe("opencode/x-preview-f-free");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("hy3 + big-pickle + flash → flash coding", () => {
|
|
99
|
+
expect(
|
|
100
|
+
pickZenFreeModel({
|
|
101
|
+
catalog: [
|
|
102
|
+
{ id: "hy3-free" },
|
|
103
|
+
{ id: "big-pickle" },
|
|
104
|
+
{ id: "deepseek-v4-flash-free" },
|
|
105
|
+
],
|
|
106
|
+
}),
|
|
107
|
+
).toBe("opencode/deepseek-v4-flash-free");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("flash + ultra + preview → ultra", () => {
|
|
111
|
+
expect(
|
|
112
|
+
pickZenFreeModel({
|
|
113
|
+
catalog: [
|
|
114
|
+
{ id: "deepseek-v4-flash-free" },
|
|
115
|
+
{ id: "nemotron-3-ultra-free" },
|
|
116
|
+
{ id: "x-preview-f-free" },
|
|
117
|
+
],
|
|
118
|
+
}),
|
|
119
|
+
).toBe("opencode/nemotron-3-ultra-free");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("only big-pickle → big-pickle", () => {
|
|
123
|
+
expect(pickZenFreeModel({ catalog: [{ id: "big-pickle" }] })).toBe(
|
|
124
|
+
"opencode/big-pickle",
|
|
125
|
+
);
|
|
65
126
|
});
|
|
66
127
|
|
|
67
128
|
test("asOpenCodeZenId prefixes bare Zen ids", () => {
|
|
@@ -73,8 +134,11 @@ describe("zen free catalog", () => {
|
|
|
73
134
|
expect(pickZenFreeModel({ catalog: CATALOG })).toBe("opencode/x-preview-f-free");
|
|
74
135
|
});
|
|
75
136
|
|
|
76
|
-
test("offline / empty catalog falls back to Ox Alpha", () => {
|
|
137
|
+
test("offline / empty / paid-only catalog falls back to Ox Alpha", () => {
|
|
77
138
|
expect(pickZenFreeModel({ catalog: [] })).toBe(OPENCODE_DEFAULT_MODEL);
|
|
139
|
+
expect(pickZenFreeModel({ catalog: [{ id: "claude-opus-4-6" }] })).toBe(
|
|
140
|
+
OPENCODE_DEFAULT_MODEL,
|
|
141
|
+
);
|
|
78
142
|
expect(OPENCODE_DEFAULT_MODEL).toBe("opencode/x-preview-f-free");
|
|
79
143
|
});
|
|
80
144
|
});
|
|
@@ -119,10 +183,16 @@ describe("openCodeModel live pick", () => {
|
|
|
119
183
|
});
|
|
120
184
|
});
|
|
121
185
|
|
|
122
|
-
test("free + injected catalog →
|
|
186
|
+
test("free + injected catalog → strongest listed-free (ultra)", () => {
|
|
123
187
|
isolate(() => {
|
|
124
|
-
process.env.CURSOR_ROUTE_ZEN_CATALOG_JSON = JSON.stringify({
|
|
125
|
-
|
|
188
|
+
process.env.CURSOR_ROUTE_ZEN_CATALOG_JSON = JSON.stringify({
|
|
189
|
+
data: [
|
|
190
|
+
{ id: "deepseek-v4-flash-free" },
|
|
191
|
+
{ id: "nemotron-3-ultra-free" },
|
|
192
|
+
{ id: "x-preview-f-free" },
|
|
193
|
+
],
|
|
194
|
+
});
|
|
195
|
+
expect(openCodeModel("free")).toBe("opencode/nemotron-3-ultra-free");
|
|
126
196
|
});
|
|
127
197
|
});
|
|
128
198
|
|
package/src/zen-free.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { join } from "node:path";
|
|
|
14
14
|
|
|
15
15
|
export const ZEN_MODELS_URL_DEFAULT = "https://opencode.ai/zen/v1/models";
|
|
16
16
|
|
|
17
|
-
/** Offline / fetch-fail fallback —
|
|
17
|
+
/** Offline / fetch-fail fallback only — not a live pin. */
|
|
18
18
|
export const OPENCODE_DEFAULT_MODEL = "opencode/x-preview-f-free";
|
|
19
19
|
|
|
20
20
|
export interface ZenModel {
|
|
@@ -36,10 +36,14 @@ export interface ZenFreePick {
|
|
|
36
36
|
|
|
37
37
|
const EXCLUDE_RE =
|
|
38
38
|
/lyria|whisper|tts|embed|embedding|image|vision-only|audio|diffusion|flux|stable-diffusion|moderation/i;
|
|
39
|
-
const
|
|
40
|
-
/
|
|
41
|
-
const
|
|
42
|
-
const
|
|
39
|
+
const POWER_ULTRA_RE =
|
|
40
|
+
/ultra|(^|[-/])opus($|[-/])|(^|[-/_])pro($|[-/])|(^|[-/])max($|[-/])|plus|(^|[-/])large($|[-/])/;
|
|
41
|
+
const POWER_PREVIEW_RE = /x-preview|ox-alpha|oxalpha|(^|[-/])alpha($|[-/])|preview/;
|
|
42
|
+
const POWER_FLASH_RE = /flash|lightning|lite|mini|nano|small|haiku|fin-/;
|
|
43
|
+
const CODING_FAMILY_RE =
|
|
44
|
+
/coder|code|instruct|laguna|glm|qwen|kimi|deepseek|gpt-oss|north|gemma|nemotron|mimo|muse/;
|
|
45
|
+
const CONTRIBUTOR_RE = /contributor-free/;
|
|
46
|
+
const MAY_TRAIN_RE = /(^|\/)big-pickle$|(^|\/)hy3-free$/;
|
|
43
47
|
|
|
44
48
|
function zenModelsUrl(): string {
|
|
45
49
|
return (process.env.CURSOR_ROUTE_ZEN_MODELS_URL || ZEN_MODELS_URL_DEFAULT).trim();
|
|
@@ -94,15 +98,28 @@ export function isZenFreeModel(m: ZenModel): boolean {
|
|
|
94
98
|
return tagged || pricedFree;
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Higher boost wins. Generic capability tokens only — never a locked model id.
|
|
103
|
+
* Coding-family matches the model slug so the provider `opencode/` does not
|
|
104
|
+
* trip `code`.
|
|
105
|
+
*/
|
|
98
106
|
export function zenFreeBoost(id: string): number {
|
|
99
107
|
const s = id.toLowerCase();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
if (
|
|
105
|
-
|
|
108
|
+
const slug = s.includes("/") ? s.slice(s.lastIndexOf("/") + 1) : s;
|
|
109
|
+
let score = 0;
|
|
110
|
+
|
|
111
|
+
if (POWER_ULTRA_RE.test(s)) score += 50;
|
|
112
|
+
else if (POWER_PREVIEW_RE.test(s)) score += 42;
|
|
113
|
+
else if (POWER_FLASH_RE.test(s)) score += 15;
|
|
114
|
+
else score += 30;
|
|
115
|
+
|
|
116
|
+
if (CODING_FAMILY_RE.test(slug)) score += 20;
|
|
117
|
+
else if (s.endsWith("-free") || s.endsWith(":free")) score += 10;
|
|
118
|
+
|
|
119
|
+
if (CONTRIBUTOR_RE.test(s)) score -= 25;
|
|
120
|
+
if (MAY_TRAIN_RE.test(s)) score -= 20;
|
|
121
|
+
|
|
122
|
+
return score;
|
|
106
123
|
}
|
|
107
124
|
|
|
108
125
|
export function rankZenFreeModels(models: ZenModel[]): Array<{
|