cursor-route 0.1.11 → 0.1.13

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.
@@ -0,0 +1,216 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ /**
6
+ * Live OpenRouter free-model pick — same shape as zen-free.ts.
7
+ * Probe GET {OPENROUTER_BASE_URL}/models, keep $0 text models, rank, cache
8
+ * ~15 min. Do not hardcode a third-party model id as the happy-path default.
9
+ * `openrouter/free` is the OpenRouter **router** fallback only.
10
+ */
11
+ export const OPENROUTER_MODELS_URL_DEFAULT = "https://openrouter.ai/api/v1/models";
12
+ /** Fetch-fail / empty-rank fallback — OpenRouter live router, not a locked model. */
13
+ export const OPENROUTER_FALLBACK_MODEL = "openrouter/free";
14
+ const EXCLUDE_RE = /lyria|whisper|tts|embed|embedding|image|vision-only|audio|diffusion|flux|stable-diffusion|moderation/i;
15
+ const BOOST_RE = /coder|instruct|chat|nemotron|qwen|llama|gemma|gpt-oss|kimi|glm|deepseek/i;
16
+ function orModelsUrl() {
17
+ const base = (process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1")
18
+ .trim()
19
+ .replace(/\/+$/, "");
20
+ return `${base}/models`;
21
+ }
22
+ function cacheMinutes() {
23
+ const n = Number(process.env.CURSOR_ROUTE_OR_CACHE_MINUTES);
24
+ return Number.isFinite(n) && n > 0 ? n : 15;
25
+ }
26
+ function cachePath() {
27
+ return process.env.CURSOR_ROUTE_OR_CACHE_PATH?.trim() ||
28
+ join(tmpdir(), "cursor-route-or-best-free.json");
29
+ }
30
+ export function assertOpenRouterModel(id) {
31
+ if (!/^[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._\-:]+)+$/i.test(id)) {
32
+ throw new Error(`Invalid OpenRouter model ${id}; expected provider/model (e.g. qwen/qwen3-coder:free) or free`);
33
+ }
34
+ return id;
35
+ }
36
+ function num(v) {
37
+ if (typeof v === "number" && Number.isFinite(v))
38
+ return v;
39
+ if (typeof v === "string" && v.trim()) {
40
+ const n = Number(v);
41
+ if (Number.isFinite(n))
42
+ return n;
43
+ }
44
+ return NaN;
45
+ }
46
+ export function isOrFreeModel(m) {
47
+ const id = (m.id || "").trim();
48
+ if (!id)
49
+ return false;
50
+ if (EXCLUDE_RE.test(id) || EXCLUDE_RE.test(m.name || ""))
51
+ return false;
52
+ const modality = m.architecture?.modality;
53
+ if (typeof modality === "string" && modality.trim() && !/text/i.test(modality)) {
54
+ return false;
55
+ }
56
+ const prompt = num(m.pricing?.prompt);
57
+ const completion = num(m.pricing?.completion);
58
+ const pricedFree = prompt === 0 && completion === 0;
59
+ const tagged = id.endsWith(":free");
60
+ return tagged || pricedFree;
61
+ }
62
+ /** Higher boost wins. Coding/chat families beat generic free. No hardcoded id. */
63
+ export function orFreeBoost(id) {
64
+ return BOOST_RE.test(id) ? 20 : 10;
65
+ }
66
+ export function rankOrFreeModels(models) {
67
+ const out = [];
68
+ for (const m of models) {
69
+ if (!isOrFreeModel(m))
70
+ continue;
71
+ const raw = (m.id || "").trim();
72
+ let id;
73
+ try {
74
+ id = assertOpenRouterModel(raw);
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ const ctx = Number.isFinite(Number(m.context_length)) ? Number(m.context_length) : 0;
80
+ out.push({
81
+ id,
82
+ name: (m.name || raw).trim(),
83
+ context_length: ctx,
84
+ boost: orFreeBoost(id),
85
+ });
86
+ }
87
+ out.sort((a, b) => {
88
+ if (b.boost !== a.boost)
89
+ return b.boost - a.boost;
90
+ if (b.context_length !== a.context_length)
91
+ return b.context_length - a.context_length;
92
+ return a.id.localeCompare(b.id);
93
+ });
94
+ return out;
95
+ }
96
+ function readCache() {
97
+ const p = cachePath();
98
+ if (!existsSync(p))
99
+ return null;
100
+ try {
101
+ const j = JSON.parse(readFileSync(p, "utf8"));
102
+ if (!j?.id)
103
+ return null;
104
+ const ageMs = Date.now() - Date.parse(j.picked_at);
105
+ if (!Number.isFinite(ageMs) || ageMs < 0)
106
+ return null;
107
+ if (ageMs > cacheMinutes() * 60_000)
108
+ return null;
109
+ assertOpenRouterModel(j.id);
110
+ return j;
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ function writeCache(pick) {
117
+ try {
118
+ writeFileSync(cachePath(), JSON.stringify(pick), { mode: 0o600 });
119
+ }
120
+ catch {
121
+ /* cache is optional */
122
+ }
123
+ }
124
+ function httpGetSync(url, timeoutMs = 4000) {
125
+ const sec = Math.max(1, Math.ceil(timeoutMs / 1000));
126
+ // Public GET /models — no Authorization header (key would land in `ps` argv).
127
+ const curl = spawnSync("curl", ["-fsS", "--max-time", String(sec), "-H", "Accept: application/json", url], {
128
+ encoding: "utf8",
129
+ timeout: timeoutMs + 500,
130
+ env: { ...process.env },
131
+ });
132
+ if (curl.status === 0 && curl.stdout?.trim())
133
+ return curl.stdout;
134
+ return null;
135
+ }
136
+ function parseCatalog(raw) {
137
+ const j = JSON.parse(raw);
138
+ if (Array.isArray(j))
139
+ return j;
140
+ if (Array.isArray(j.data))
141
+ return j.data;
142
+ return [];
143
+ }
144
+ export function loadOrCatalog() {
145
+ const inline = process.env.CURSOR_ROUTE_OR_CATALOG_JSON?.trim();
146
+ if (inline)
147
+ return parseCatalog(inline);
148
+ if (process.env.CURSOR_ROUTE_OR_OFFLINE === "1")
149
+ return [];
150
+ const body = httpGetSync(orModelsUrl());
151
+ if (!body)
152
+ return [];
153
+ try {
154
+ return parseCatalog(body);
155
+ }
156
+ catch {
157
+ return [];
158
+ }
159
+ }
160
+ /** Last live pick if the cache is still fresh — health-safe (no network). */
161
+ export function cachedOrFreePick() {
162
+ try {
163
+ return readCache()?.id ?? null;
164
+ }
165
+ catch {
166
+ return null;
167
+ }
168
+ }
169
+ /**
170
+ * Best live OpenRouter free text model, or OPENROUTER_FALLBACK_MODEL if the
171
+ * catalog is empty/offline. `CURSOR_ROUTE_OR_REFRESH=1` skips cache.
172
+ * Tests: `CURSOR_ROUTE_OR_OFFLINE=1` or `CURSOR_ROUTE_OR_CATALOG_JSON`.
173
+ */
174
+ export function pickOrFreeModel(opts) {
175
+ const refresh = opts?.refresh ||
176
+ process.env.CURSOR_ROUTE_OR_REFRESH === "1";
177
+ if (!refresh && opts?.catalog === undefined) {
178
+ const cached = readCache();
179
+ if (cached)
180
+ return cached.id;
181
+ }
182
+ const models = opts?.catalog ?? loadOrCatalog();
183
+ const ranked = rankOrFreeModels(models);
184
+ const winner = ranked[0];
185
+ if (!winner) {
186
+ // Do not cache fallback — a 15-min stale fallback would masquerade as a
187
+ // live pick in health (`now openrouter/free`). Retry the catalog next call.
188
+ return OPENROUTER_FALLBACK_MODEL;
189
+ }
190
+ const pick = {
191
+ id: winner.id,
192
+ name: winner.name,
193
+ context_length: winner.context_length,
194
+ boost: winner.boost,
195
+ picked_at: new Date().toISOString(),
196
+ candidates: ranked.length,
197
+ source: "ranked-free",
198
+ };
199
+ if (opts?.catalog === undefined)
200
+ writeCache(pick);
201
+ return pick.id;
202
+ }
203
+ /**
204
+ * Resolve OpenRouter `--model` / env to a concrete `provider/model` id.
205
+ * Empty or `free` → CURSOR_ROUTE_OPENROUTER_MODEL pin, else live catalog pick.
206
+ */
207
+ export function openRouterModel(raw) {
208
+ const v = (raw ?? "").trim();
209
+ if (!v || v.toLowerCase() === "free") {
210
+ const env = (process.env.CURSOR_ROUTE_OPENROUTER_MODEL ?? "").trim();
211
+ if (env && env.toLowerCase() !== "free")
212
+ return assertOpenRouterModel(env);
213
+ return pickOrFreeModel();
214
+ }
215
+ return assertOpenRouterModel(v);
216
+ }
@@ -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.11 (LIVE on npm latest)
4
+ npm: cursor-route@0.1.13 (publishing this slice)
5
5
  created: 2026-08-12
6
- updated: 2026-08-21
6
+ updated: 2026-08-29
7
7
  ---
8
8
 
9
9
  # cursor-route — living brief
@@ -18,21 +18,21 @@ Cursor Agent plans. Workers run in tmux via `cursor-route`:
18
18
 
19
19
  | Lane | Worker | Intent |
20
20
  |------|--------|--------|
21
- | `easy` | OpenRouter free | Wording / drafts — non-secret prompts only |
22
- | `mid` | claude-ds (DeepSeek behind Claude Code) | Default implement (**Flash**; `--model pro` when needed) |
21
+ | `easy` | OpenRouter free (live pick) | Wording / drafts — non-secret prompts only |
22
+ | `mid` | claude-ds (DeepSeek behind Claude Code) | Default implement (**Flash**; `--model vision` for screenshots; `--model pro` harder mid / hard backup only) |
23
23
  | `hard` | Grok CLI | Hard implement |
24
24
  | opt-in | OpenCode | `--worker opencode` coding agent; `--model free` ranks live Zen catalog (Ox Alpha first while listed) |
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.11**. Release notes: [CHANGELOG.md](../../CHANGELOG.md).
28
+ Install: `npm i -g cursor-route` → **0.1.13**. Release notes: [CHANGELOG.md](../../CHANGELOG.md).
29
29
 
30
30
  ## Open (edit / check off)
31
31
 
32
32
  - [x] **Flash vs Pro on the CLI** — public default **Flash**; `--model pro` → `deepseek-v4-pro` (LIVE 0.1.6+)
33
33
  - [x] pass `claude-ds -Model deepseek-v4-flash|deepseek-v4-pro` from the adapter
34
34
  - [x] add `--model flash|pro` on `start`
35
- - [x] document Grok **auth** ≠ usage-out (`grok login`) vs quota Pro stand-in
35
+ - [x] document Grok **auth** ≠ usage-out (`grok login`); Flash-first when Grok **usage** is out; Pro = harder mid / hard backup only (0.1.12)
36
36
  - [x] **Skill `route-orch`** — Flash/Pro table in `skills/` + `.cursor/skills/`
37
37
  - [x] **Official DeepSeek Harness** — `deepseek` adapter slot present; `@deepseek-ai/dsh` 0.1.0-rc.6 (2026-08-13, github.com/deepseek-ai/deepseek-harness, MIT) is a developer-preview plugin kernel (web UI + `dsh --profile headless "job"`), not a Claude Code replacement; mid does not swap
38
38
  - [x] **0.1.7 debug fixes** — env default wired through `startJob`; Anthropic hatch omits DS ids; preserve `[1m]`
@@ -41,6 +41,8 @@ Install: `npm i -g cursor-route` → **0.1.11**. Release notes: [CHANGELOG.md](.
41
41
  - [x] **Health proves mid DeepSeek + evidence tree** — 0.1.9: `lane:mid` ✓ only when DeepSeek is proven (shim or DeepSeek `ANTHROPIC_BASE_URL`; Anthropic hatch is not proof); health JSON `lanes.mid`; `status --json` evidence tree (`spawn` / `execute` / `verify.claim=unverified`); skill health-before-mid + evidence-tree closeout. Mid stays **claude-ds**.
42
42
  - [x] **Opt-in `--worker opencode`** — 0.1.10: `opencode run --dir` + `--model`; always-approve → `--auto`; `--ask` omits it; never rewrites `~/.config/opencode/opencode.json`. Health ✓ needs `opencode` on PATH (`CURSOR_ROUTE_OPENCODE_BIN`). Mid stays **claude-ds**; easy stays OpenRouter chat. Free Zen may log/train — non-secret prompts.
43
43
  - [x] **Live Zen free pick** — 0.1.11: `--model free` ranks `GET https://opencode.ai/zen/v1/models` (Ox Alpha first while listed; ~15 min cache; Ox Alpha fallback if fetch fails). Pin with `CURSOR_ROUTE_OPENCODE_MODEL`. Health stays offline (cache/fallback only).
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
+ - [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.
44
46
  - [ ] **Hero GIF** — still outstanding; dry-run fixture ships as the substitute for now (`docs/fixtures/hero-demo.log` — see `docs/DEMO_GIF.md`)
45
47
  - [x] **Do not** paste private `ROUTE_KIT`, SIP, prod paths, or hang-watchdog env into this public repo
46
48
 
@@ -55,6 +57,7 @@ Install: `npm i -g cursor-route` → **0.1.11**. Release notes: [CHANGELOG.md](.
55
57
  | `src/adapters/openrouter.ts` | Easy worker |
56
58
  | `src/adapters/opencode.ts` | Opt-in OpenCode worker (`--worker opencode`; mid stays claude-ds) |
57
59
  | `src/zen-free.ts` | Live Zen catalog rank for `--model free` |
60
+ | `src/or-free.ts` | Live OpenRouter catalog rank for easy-lane `--model free` |
58
61
  | `skills/route-orch/SKILL.md` | Cursor skill — spawn CLI, do not implement in-session |
59
62
  | `CHANGELOG.md` | Release notes |
60
63
  | `SECURITY.md` | Secret refuse gate |
@@ -75,3 +78,5 @@ Install: `npm i -g cursor-route` → **0.1.11**. Release notes: [CHANGELOG.md](.
75
78
  | 2026-08-18 | Health `lane:mid` + `lanes.mid` prove DeepSeek; status evidence tree (`verify.claim` stays unverified); skill health-before-mid + closeout tree; mid stays claude-ds → 0.1.9 LIVE. |
76
79
  | 2026-08-21 | Opt-in `--worker opencode` (Zen free `opencode/big-pickle`, `--auto`, no config rewrite); mid stays claude-ds → 0.1.10 LIVE. |
77
80
  | 2026-08-21 | Live Zen free pick (Ox Alpha first while listed; OpenRouter-style catalog rank) → 0.1.11 LIVE. |
81
+ | 2026-08-29 | Flash-first docs, `--model vision` + auto-pick, live OpenRouter free pick, route-orch K318/K322, hero fixture 0.1.12. No npm publish. |
82
+ | 2026-08-29 | Kimi audit follow-up → 0.1.13: no fallback cache, health labels, unauth GET /models, env error wording. |
@@ -6,10 +6,10 @@ Simulated capture output for README / tweet assets — replace with a real GIF o
6
6
 
7
7
  ```
8
8
  $ cursor-route --version
9
- 0.1.9
9
+ 0.1.13
10
10
 
11
11
  $ cursor-route health
12
- cursor-route v0.1.9
12
+ cursor-route v0.1.13
13
13
  health: OK
14
14
  ✓ tmux
15
15
  ✓ runtime bun ok
@@ -32,7 +32,7 @@ $ cursor-route jobs --json
32
32
 
33
33
  Verified locally (2026-08-10): headless `claude-ds` smoke returned `CURSOR_ROUTE_SMOKE_OK`.
34
34
  Grok smoke hit 402 (Build usage balance exhausted) — auth/PATH wiring works; top up Grok Build for live demos.
35
- Use `--model pro` when Grok **usage** is out (not the same as a missing `grok login`).
35
+ Use `--model flash` when Grok **usage** is out (cheap default). `--model pro` is harder mid / hard backup only — not the default Grok-out stand-in. A missing `grok login` is auth, not the Pro case.
36
36
 
37
37
  Current commands: `health`, `start`, `jobs`, `status`, `capture`, `send`, `attach`, `kill`, `sessions`, `clean`.
38
38
  Headless demos (no tmux) use `--no-tmux` and `capture`/`status` instead of `attach`/`send`.
@@ -23,8 +23,11 @@ unset CURSOR_ROUTE_CLAUDE_DS_BIN CURSOR_ROUTE_GROK_BIN CURSOR_ROUTE_DSH_BIN \
23
23
  CURSOR_ROUTE_ALLOW_ANTHROPIC \
24
24
  CURSOR_ROUTE_ALLOW_STOCK_CLAUDE CURSOR_ROUTE_ANTHROPIC_BASE_URL CURSOR_ROUTE_DS_MODEL \
25
25
  CURSOR_ROUTE_OPENROUTER_MODEL OPENROUTER_API_KEY OPENROUTER_BASE_URL \
26
+ CURSOR_ROUTE_OR_CATALOG_JSON CURSOR_ROUTE_OR_CACHE_PATH CURSOR_ROUTE_OR_REFRESH \
26
27
  ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN ANTHROPIC_API_KEY ANTHROPIC_MODEL \
27
28
  DEEPSEEK_API_KEY XAI_API_KEY 2>/dev/null || true
29
+ # Stable easy-lane dry-run: do not fetch the live catalog while generating the fixture.
30
+ export CURSOR_ROUTE_OR_OFFLINE=1
28
31
 
29
32
  TMP="$(mktemp -d)"
30
33
  trap 'rm -rf "$TMP"' EXIT
@@ -1,8 +1,8 @@
1
1
  $ cursor-route --version
2
- 0.1.9
2
+ 0.1.13
3
3
 
4
4
  $ CURSOR_ROUTE_RELAXED=1 cursor-route health
5
- cursor-route v0.1.9
5
+ cursor-route v0.1.13
6
6
  health: OK
7
7
 
8
8
  ✓ tmux ok
@@ -10,8 +10,9 @@ health: OK
10
10
  ✓ script(1) ok (tty log capture)
11
11
  ✓ worker:grok ok (auth checked at first start — run grok login if jobs fail) @ ~/.grok/bin/grok
12
12
  ✓ worker:claude-ds ok (claude-ds (DeepSeek shim); default model deepseek-v4-flash) @ ~/.local/bin/claude-ds
13
- ✗ worker:openrouter OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane model defaults to openrouter/free) @ node '~/Projects/cursor-route/dist/openrouter-run.js'
13
+ ✗ worker:openrouter OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane live-picks a free model at start; pin with CURSOR_ROUTE_OPENROUTER_MODEL) @ node '~/Projects/cursor-route/dist/openrouter-run.js'
14
14
  ✗ worker:deepseek dsh (@deepseek-ai/dsh) not found — install: npm i -g @deepseek-ai/dsh. Mid default remains claude-ds.
15
+ ✓ worker:opencode ok (opencode run; --model free = live Zen pick, now opencode/x-preview-f-free; auth at first start — opencode auth login if jobs fail; mid default remains claude-ds) @ ~/.local/bin/opencode
15
16
  ✓ lane:mid DeepSeek proven (claude-ds (DeepSeek shim))
16
17
  ✓ cursor_cli optional ok (agent on PATH) — v0 supervisor is Cursor skill, not CLI
17
18
  ✓ relaxed CURSOR_ROUTE_RELAXED=1 — tmux optional (headless OK)
@@ -26,6 +27,7 @@ command: cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~
26
27
  $ cursor-route start --lane easy --dry-run "Rewrite this FAQ answer in 3 sentences"
27
28
  dry-run job b2c3d4e5
28
29
  worker: openrouter
30
+ model: openrouter/free
29
31
  command: node '~/Projects/cursor-route/dist/openrouter-run.js' --prompt-file '~/.local/share/cursor-route/jobs/b2c3d4e5.prompt'
30
32
 
31
33
  $ cursor-route start --lane hard --dry-run "Refactor auth module; run tests; report verify evidence"
package/llms.txt CHANGED
@@ -2,7 +2,7 @@
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.11**)
5
+ MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.13**)
6
6
  GitHub: https://github.com/cemini23/cursor-route
7
7
 
8
8
  ## FAQ
@@ -14,13 +14,15 @@ cursor-route is a public MIT CLI and Cursor skill that runs parallel coding work
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.
15
15
 
16
16
  ### Does mid lane use Anthropic Claude?
17
- No. The mid worker is DeepSeek. Claude Code is the harness, configured with `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic` and a DeepSeek key in `ANTHROPIC_AUTH_TOKEN`. Default model is Flash (`--model flash`); use `--model pro` for harder mid work or when Grok usage is exhausted (not the same as a missing `grok login`).
17
+ No. The mid worker is DeepSeek. Claude Code is the harness, configured with `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic` and a DeepSeek key in `ANTHROPIC_AUTH_TOKEN`. Default model is Flash (`--model flash`) keep Flash when Grok **usage** is out. `--model vision` is for screenshots / ui mocks. `--model pro` is harder mid / **hard backup only**, not the default Grok-out stand-in. A missing `grok login` is auth, not the Pro case.
18
18
 
19
19
  ### Flash vs Pro?
20
20
  | Flag | Model | When |
21
21
  |------|-------|------|
22
- | `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute |
23
- | `--model pro` | `deepseek-v4-pro` | Harder mid / Grok usage stand-in |
22
+ | `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute. Prefer this when Grok **usage** is out |
23
+ | `--model vision` | `deepseek-v4-flash-vision-exp` | Screenshots / ui mocks / image prompts (or auto-pick) |
24
+ | `--model pro` | `deepseek-v4-pro` | Harder mid / **hard backup** only — not the default Grok-out stand-in |
25
+ | `--model deepseek-v4-pro[1m]` | preserved SKU | Large-context Pro |
24
26
 
25
27
  ### How do I use OpenCode free models?
26
28
  Install OpenCode (`npm i -g opencode-ai`), run `opencode auth login`, then `cursor-route start --worker opencode "…"`. `--model free` ranks live Zen free models (Ox Alpha first while listed). Mid stays `claude-ds`.
@@ -29,12 +31,12 @@ Install OpenCode (`npm i -g opencode-ai`), run `opencode auth login`, then `curs
29
31
  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
30
32
 
31
33
  ### Is it free?
32
- The cursor-route code is open source under MIT. It does not make the worker services free. Your costs depend on Cursor, DeepSeek API usage, and the Grok access or balance available to you. The easy lane can be free on OpenRouter's free-model route (`openrouter/free`). `--worker opencode` can use OpenCode Zen free models.
34
+ The cursor-route code is open source under MIT. It does not make the worker services free. Your costs depend on Cursor, DeepSeek API usage, and the Grok access or balance available to you. The easy lane live-picks a free OpenRouter text model at start (fallback router `openrouter/free` only if the catalog fetch fails). `--worker opencode` can use OpenCode Zen free models.
33
35
 
34
36
  ## Install
35
37
 
36
38
  - npm: `npm i -g cursor-route`
37
39
  - Health: `cursor-route health`
38
- - Mid: `cursor-route start --lane mid "…"` (Flash) · `--model pro` when needed
40
+ - Mid: `cursor-route start --lane mid "…"` (Flash; keep Flash when Grok usage is out) · `--model vision` for screenshots · `--model pro` harder mid / hard backup only
39
41
  - OpenCode (opt-in): `cursor-route start --worker opencode "…"`
40
42
  - Skill: `/route-orch` (Cursor)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-route",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
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",
@@ -24,27 +24,30 @@ You are the **orchestrator**. Do **not** implement bulk code in this Cursor sess
24
24
 
25
25
  | Lane | Worker | Use when |
26
26
  |------|--------|----------|
27
- | `easy` | `openrouter` (OpenRouter free models) | Wording / drafts — non-secret prompts only |
27
+ | `easy` | `openrouter` (live OpenRouter free pick) | Wording / drafts — non-secret prompts only |
28
28
  | `mid` | `claude-ds` (DeepSeek via Claude Code harness) | Standard implement / refactor |
29
29
  | `hard` | `grok` | Premium plan in Cursor → Grok implement |
30
30
 
31
- Free OpenRouter models may log prompts — keep secrets off the easy lane (the CLI refuse gate still applies).
31
+ Free OpenRouter models may log prompts — keep secrets off the easy lane (the CLI refuse gate still applies). Easy lane **live-picks** the best free text model at request time (unset / `--model free`). Pin with `CURSOR_ROUTE_OPENROUTER_MODEL` or `--model provider/model`. Fetch-fail fallback is the OpenRouter router `openrouter/free` only.
32
32
 
33
33
  ## claude-ds models
34
34
 
35
35
  One harness. Do not install a second coding loop.
36
36
 
37
- | Flag / role | Model | When |
38
- |-------------|-------|------|
39
- | default mid (`--model flash`) | `deepseek-v4-flash` | `--lane mid` cheap execute |
40
- | Grok stand-in (`--model pro`) | `deepseek-v4-pro` | Grok CLI **usage/quota** out (not a missing `grok login`) |
37
+ | Flag | Model id | When |
38
+ |------|----------|------|
39
+ | `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute. Prefer this when Grok **usage** is out |
40
+ | `--model vision` | `deepseek-v4-flash-vision-exp` | Screenshots / ui mocks / image prompts (or auto-pick) |
41
+ | `--model pro` | `deepseek-v4-pro` | Harder mid / **hard backup** only — not the default Grok-out stand-in |
42
+ | `--model deepseek-v4-pro[1m]` | preserved SKU | Large-context Pro |
41
43
 
42
44
  ```bash
43
45
  cursor-route start --lane mid --dir "$PWD" "…"
46
+ cursor-route start --lane mid --model vision --dir "$PWD" "…"
44
47
  cursor-route start --lane mid --model pro --dir "$PWD" "…"
45
48
  ```
46
49
 
47
- If `worker:grok` is ✗ on health, that is usually **auth** (`grok login` / `XAI_API_KEY`) — not the Pro stand-in case.
50
+ If `worker:grok` is ✗ on health, that is usually **auth** (`grok login` / `XAI_API_KEY`) — not the Pro case. When Grok **usage** is out, stay on Flash.
48
51
 
49
52
  ## Experimental: --worker deepseek (dsh)
50
53
 
@@ -116,6 +119,8 @@ Verify criteria are an **external eval contract** (AutoDesign pattern), fixed by
116
119
  - **External eval contract (AutoDesign):** do not rewrite Verify / Success criteria mid-run to make a failing job look green — capture + exit status are the contract (see Verify / claim closeout).
117
120
  - **Skill misevolution:** do not auto-edit `route-orch` or promote skill variants from worker trajectories without operator HITL — write-time approval ≠ safe retrieval later.
118
121
  - **On verify fail:** prefer reconsidering the plan/definition (wrong approach) over grinding the same tactic; attribute failure to stage when possible (spawn vs execute vs verify).
122
+ - **Step-wise re-route (ProgRouter):** `--lane` is the first pick. If progress stalls or verify fails, `send` a correction or spawn a follow-up on a different worker/model — do not lock the first worker for the whole job. Parent still closes on capture/exit evidence.
123
+ - **Do not auto-spawn N panes for multi-perspective:** spawn workers for **implementation parallelism** (independent files/jobs). Multi-perspective reasoning stays in the Cursor parent. Do not fan out “one agent per viewpoint.”
119
124
 
120
125
  ## Always-approve
121
126
 
@@ -212,7 +212,7 @@ export const claudeDsAdapter: Adapter = {
212
212
  resolved.mode.startsWith("claude-ds") || resolved.mode.startsWith("deepseek-claude");
213
213
 
214
214
  // Anthropic escape hatch: do not pass DeepSeek model ids (unknown to Anthropic).
215
- // --model flash|pro is DeepSeek-only; stock Claude uses its own defaults / ANTHROPIC_MODEL.
215
+ // --model flash|pro|vision is DeepSeek-only; stock Claude uses its own defaults / ANTHROPIC_MODEL.
216
216
  if (isAnthropicEscape) {
217
217
  const parts = [
218
218
  shellQuote(resolved.binary),
@@ -11,6 +11,10 @@ afterEach(() => {
11
11
  delete process.env.OPENROUTER_API_KEY;
12
12
  delete process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
13
13
  delete process.env.OPENROUTER_BASE_URL;
14
+ delete process.env.CURSOR_ROUTE_OR_OFFLINE;
15
+ delete process.env.CURSOR_ROUTE_OR_CATALOG_JSON;
16
+ delete process.env.CURSOR_ROUTE_OR_CACHE_PATH;
17
+ delete process.env.CURSOR_ROUTE_OR_REFRESH;
14
18
  });
15
19
 
16
20
  describe("openrouter adapter", () => {
@@ -20,13 +24,38 @@ describe("openrouter adapter", () => {
20
24
  expect(h.worker).toBe("openrouter");
21
25
  expect(h.ok).toBe(false);
22
26
  expect(h.detail).toContain("OPENROUTER_API_KEY");
27
+ expect(h.detail).not.toMatch(/defaults to openrouter\/free/);
23
28
  });
24
29
 
25
30
  test("health passes with a fake key (no network)", () => {
26
31
  setKey();
27
- const h = openRouterAdapter.health();
28
- expect(h.ok).toBe(true);
29
- expect(h.detail).toContain("openrouter/free");
32
+ const prev = {
33
+ model: process.env.CURSOR_ROUTE_OPENROUTER_MODEL,
34
+ offline: process.env.CURSOR_ROUTE_OR_OFFLINE,
35
+ json: process.env.CURSOR_ROUTE_OR_CATALOG_JSON,
36
+ cache: process.env.CURSOR_ROUTE_OR_CACHE_PATH,
37
+ };
38
+ delete process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
39
+ process.env.CURSOR_ROUTE_OR_OFFLINE = "1";
40
+ delete process.env.CURSOR_ROUTE_OR_CATALOG_JSON;
41
+ process.env.CURSOR_ROUTE_OR_CACHE_PATH = "/tmp/cursor-route-or-health-missing.json";
42
+ try {
43
+ const h = openRouterAdapter.health();
44
+ expect(h.ok).toBe(true);
45
+ expect(h.detail.toLowerCase()).toMatch(/live pick/);
46
+ expect(h.detail).toMatch(/fallback/);
47
+ expect(h.detail).not.toMatch(/now openrouter\/free/);
48
+ expect(h.detail).not.toMatch(/defaults to openrouter\/free/);
49
+ } finally {
50
+ if (prev.model === undefined) delete process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
51
+ else process.env.CURSOR_ROUTE_OPENROUTER_MODEL = prev.model;
52
+ if (prev.offline === undefined) delete process.env.CURSOR_ROUTE_OR_OFFLINE;
53
+ else process.env.CURSOR_ROUTE_OR_OFFLINE = prev.offline;
54
+ if (prev.json === undefined) delete process.env.CURSOR_ROUTE_OR_CATALOG_JSON;
55
+ else process.env.CURSOR_ROUTE_OR_CATALOG_JSON = prev.json;
56
+ if (prev.cache === undefined) delete process.env.CURSOR_ROUTE_OR_CACHE_PATH;
57
+ else process.env.CURSOR_ROUTE_OR_CACHE_PATH = prev.cache;
58
+ }
30
59
  });
31
60
 
32
61
  test("buildLaunch passes key via env and never echoes it in the command", () => {
@@ -3,7 +3,11 @@ import { fileURLToPath } from "node:url";
3
3
  import { dirname, join } from "node:path";
4
4
  import type { Adapter, WorkerHealth } from "./types.ts";
5
5
  import { shellQuote } from "../util.ts";
6
- import { openRouterModel, openRouterBaseUrl } from "../config.ts";
6
+ import {
7
+ cachedOrFreePick,
8
+ OPENROUTER_FALLBACK_MODEL,
9
+ openRouterBaseUrl,
10
+ } from "../config.ts";
7
11
 
8
12
  /**
9
13
  * Resolve how to invoke the one-shot runner. Prefer the compiled dist via node
@@ -22,11 +26,11 @@ function resolveRunner(): { command: string } | null {
22
26
  return null;
23
27
  }
24
28
 
25
- function openRouterEnv(): Record<string, string> | undefined {
29
+ function openRouterEnv(modelId?: string): Record<string, string> | undefined {
26
30
  const key = process.env.OPENROUTER_API_KEY;
27
31
  if (!key) return undefined;
28
32
  const env: Record<string, string> = { OPENROUTER_API_KEY: key };
29
- const model = process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
33
+ const model = modelId || process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
30
34
  if (model) env.CURSOR_ROUTE_OPENROUTER_MODEL = model;
31
35
  const base = process.env.OPENROUTER_BASE_URL;
32
36
  if (base) env.OPENROUTER_BASE_URL = base;
@@ -44,7 +48,7 @@ export const openRouterAdapter: Adapter = {
44
48
  ok: false,
45
49
  binary: runner?.command ?? null,
46
50
  detail:
47
- "OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane model defaults to openrouter/free)",
51
+ "OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane live-picks a free model at start; pin with CURSOR_ROUTE_OPENROUTER_MODEL)",
48
52
  };
49
53
  }
50
54
  if (!runner) {
@@ -55,19 +59,25 @@ export const openRouterAdapter: Adapter = {
55
59
  detail: "openrouter-run not found — run bun run build (or use Bun from a source clone)",
56
60
  };
57
61
  }
62
+ // Health stays offline: ranked cache hit, else label the router fallback
63
+ // (do not present openrouter/free as a fresh live pick).
64
+ const cached = cachedOrFreePick();
65
+ const detail = cached
66
+ ? `ok (live pick at start; cached ${cached} @ ${openRouterBaseUrl()})`
67
+ : `ok (live pick at start; no catalog cache, fetch-fail fallback ${OPENROUTER_FALLBACK_MODEL} @ ${openRouterBaseUrl()})`;
58
68
  return {
59
69
  worker: "openrouter",
60
70
  ok: true,
61
71
  binary: runner.command,
62
- detail: `ok (model ${openRouterModel()} @ ${openRouterBaseUrl()})`,
72
+ detail,
63
73
  };
64
74
  },
65
- buildLaunch({ promptFile }) {
75
+ buildLaunch({ promptFile, modelId }) {
66
76
  const runner = resolveRunner();
67
77
  if (!runner) throw new Error("openrouter runner not available — run: bun run build");
68
78
  // Missing key is tolerated here so `--dry-run` can still print the command;
69
79
  // real starts are gated by the health preflight (which requires the key).
70
- const env = openRouterEnv();
80
+ const env = openRouterEnv(modelId);
71
81
 
72
82
  // No interactive approval concept for a pure HTTP call — nothing to auto-approve.
73
83
  return {
@@ -24,9 +24,9 @@ export interface Adapter {
24
24
  promptFile: string;
25
25
  cwd: string;
26
26
  alwaysApprove: boolean;
27
- /** Mid-lane DeepSeek flash|pro (claude-ds + deepseek; ignored by grok/openrouter / Anthropic escape hatch). */
27
+ /** Mid-lane DeepSeek flash|pro|vision (claude-ds + deepseek; ignored by grok / Anthropic escape hatch). */
28
28
  model?: DsModelAlias;
29
- /** Concrete DeepSeek id (preserves pro[1m]) or OpenCode provider/model. */
29
+ /** Concrete DeepSeek id (preserves pro[1m]) or OpenCode/OpenRouter provider/model. */
30
30
  modelId?: string;
31
31
  /** True on --dry-run: adapters may drop artifacts they just wrote (e.g. dsh patch). */
32
32
  dryRun?: boolean;