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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@
4
4
 
5
5
  _(none)_
6
6
 
7
+ ## 0.1.13 — 2026-08-29
8
+
9
+ - OpenRouter live-pick: do **not** cache the fetch-fail fallback (`openrouter/free`) — health no longer presents a stale fallback as `now openrouter/free`. Health says `cached <id>` only for a ranked catalog hit; otherwise `no catalog cache, fetch-fail fallback openrouter/free`.
10
+ - Catalog fetch is an unauthenticated `GET /models` (no `Authorization` on curl argv).
11
+ - Invalid `CURSOR_ROUTE_DS_MODEL` / `ANTHROPIC_MODEL` names those env vars instead of saying `Invalid --model`.
12
+ - Vision auto-pick uses `\bimage\b` so "ImageMagick" stays Flash.
13
+ - Hero demo fixture regenerated to **0.1.13**.
14
+
15
+ ## 0.1.12 — 2026-08-29
16
+
17
+ - **Flash-first:** `--model flash` is the cheap mid default even when Grok **usage** is out. `--model pro` is harder mid / **hard backup only**, not the default Grok-out stand-in. Grok **auth** (`grok login` / `XAI_API_KEY`) still ≠ usage-out.
18
+ - `--model vision` (and `deepseek-v4-flash-vision-exp`) on `claude-ds` / `--worker deepseek`. Mid default stays Flash. If `--model` and `CURSOR_ROUTE_DS_MODEL` / `ANTHROPIC_MODEL` are unset, screenshot/image/png/jpg/jpeg/webp/ui mock/multimodal/vision prompts auto-pick vision Flash. Explicit `--model flash|pro|vision` always wins.
19
+ - Easy lane **live OpenRouter free pick** at request time (`GET /models`, rank `:free` or $0 text models, cache ~15 min). Do not hardcode a third-party model id as the default. Pin with `CURSOR_ROUTE_OPENROUTER_MODEL` or `--model provider/model`. Empty / `free` = live pick. Fetch-fail / empty catalog falls back to the OpenRouter **router** `openrouter/free` (fallback only). Health never fetches (cache or fallback only). Tests: `CURSOR_ROUTE_OR_OFFLINE=1` or `CURSOR_ROUTE_OR_CATALOG_JSON`.
20
+ - `route-orch`: ProgRouter (step-wise re-route) + MoRe (do not auto-spawn N panes for multi-perspective). Flash-first table + live OpenRouter wording.
21
+ - Hero demo fixture regenerated (`docs/fixtures/generate-hero-demo.sh`) so `--version` / health banner are 0.1.12.
22
+ - Headless `kill`: if SIGKILL was sent, succeed even when sandboxed `ps` (EPERM) treats unreaped zombies as alive. `refreshStatus` still fail-safes to alive.
23
+
7
24
  ## 0.1.11 — 2026-08-21
8
25
 
9
26
  - `--worker opencode --model free` now **ranks the live OpenCode Zen catalog** (`GET https://opencode.ai/zen/v1/models`, cached ~15 min) instead of hardcoding `opencode/big-pickle`. Ox Alpha (`opencode/x-preview-f-free`) wins while it is listed and free; coding `-free` models next; `big-pickle` last among non-contributor free. Fetch-fail fallback is Ox Alpha.
package/CONTRIBUTING.md CHANGED
@@ -10,6 +10,7 @@ bun run typecheck
10
10
  CURSOR_ROUTE_RELAXED=1 ./bin/cursor-route health # without tmux
11
11
  ./bin/cursor-route start --dry-run --worker grok "ping"
12
12
  ./bin/cursor-route start --dry-run --lane mid --model flash "ping"
13
+ ./bin/cursor-route start --dry-run --lane mid --model vision "ping"
13
14
  ./bin/cursor-route start --dry-run --lane mid --model pro "ping"
14
15
  ./bin/cursor-route start --dry-run --worker deepseek --model flash "ping" # needs dsh + DEEPSEEK_API_KEY for real starts
15
16
  ./bin/cursor-route start --dry-run --worker opencode --model free "ping" # needs opencode for real starts
@@ -23,7 +24,7 @@ Keep `skills/route-orch/SKILL.md` and `.cursor/skills/route-orch/SKILL.md` ident
23
24
  - Do not vendor private `agent-toolkit` paths
24
25
  - Prefer adapters over rewriting the job core
25
26
  - Always-approve defaults stay documented + opt-out (`--ask` / `CURSOR_ROUTE_ASK=1`)
26
- - Mid default is Flash; document Pro only as harder mid / Grok **usage** stand-in (not missing `grok login`)
27
+ - Mid default is Flash first; Pro = harder mid / **hard backup only**. Grok **auth** (`grok login`) ≠ usage-out. Do not document Pro as the default Grok-out stand-in.
27
28
  - OpenCode is opt-in (`--worker opencode`); do not make it a lane default — mid stays claude-ds
28
29
 
29
30
  ## Release checklist
@@ -31,7 +32,7 @@ Keep `skills/route-orch/SKILL.md` and `.cursor/skills/route-orch/SKILL.md` ident
31
32
  1. Update `package.json` version + [CHANGELOG.md](./CHANGELOG.md) + [docs/briefs/WORKING.md](./docs/briefs/WORKING.md)
32
33
  2. `bun test` && `bun run typecheck` && `bun run build`
33
34
  3. `cursor-route health` (tmux + ≥1 worker)
34
- 4. One real `start` smoke (grok or claude-ds); dry-run both `--model flash` and `--model pro`
35
+ 4. One real `start` smoke (grok or claude-ds); dry-run `--model flash`, `--model vision`, and `--model pro`
35
36
  5. `npm pack --dry-run` — no secrets, no `node_modules`
36
37
  6. Commit, tag `v0.x.y`, push
37
38
  7. `npm publish --access public` with an isolated `--userconfig` Automation token (Bypass 2FA); **revoke** any chat-pasted token after
package/README.md CHANGED
@@ -27,8 +27,8 @@ If you already live in Cursor, X Premium (Grok CLI), and DeepSeek — stop payin
27
27
  |------|----------------|------------------|
28
28
  | Cursor | Premium plan | Plan / synthesize / verify (orchestrator) |
29
29
  | Grok CLI | X Premium | `--lane hard` implement |
30
- | DeepSeek via claude-ds | DeepSeek API / plan | `--lane mid` implement (**Flash** default; `--model pro` when needed) |
31
- | OpenRouter free models | OpenRouter API (free tier) | `--lane easy` wording/drafts — non-secret prompts only (see Security) |
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
+ | OpenRouter free models | OpenRouter API (free tier) | `--lane easy` live free pick at start — non-secret prompts only (see Security) |
32
32
  | OpenCode (opt-in) | OpenCode Zen free models | `--worker opencode` implement on live Zen free pick (Ox Alpha while listed) |
33
33
  | Codex / extra Claude | Optional | Not required for v0 |
34
34
 
@@ -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.9
119
+ 0.1.13
120
120
 
121
121
  $ CURSOR_ROUTE_RELAXED=1 cursor-route health
122
- cursor-route v0.1.9
122
+ cursor-route v0.1.13
123
123
  health: OK
124
124
 
125
125
  $ cursor-route start --lane mid --model flash --dry-run "Add a unit test for shellQuote"
@@ -149,18 +149,22 @@ Always-approve is **on** by default for **coding worktrees only**. It does not a
149
149
 
150
150
  | Flag | Model id | When |
151
151
  |------|----------|------|
152
- | `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute |
153
- | `--model pro` | `deepseek-v4-pro` | Harder mid / Grok **usage** stand-in |
152
+ | `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute. Prefer this when Grok **usage** is out |
153
+ | `--model vision` | `deepseek-v4-flash-vision-exp` | Screenshots / ui mocks / image prompts (or auto-pick) |
154
+ | `--model pro` | `deepseek-v4-pro` | Harder mid / **hard backup** only — not the default Grok-out stand-in |
154
155
  | `--model deepseek-v4-pro[1m]` | `deepseek-v4-pro[1m]` | Large-context Pro (SKU preserved) |
155
156
 
156
157
  ```bash
157
158
  cursor-route start --lane mid "…" # Flash
158
- cursor-route start --lane mid --model pro "…" # Pro
159
+ cursor-route start --lane mid --model vision "…" # Vision Flash
160
+ cursor-route start --lane mid --model pro "…" # Pro (harder mid / hard backup)
159
161
  # Or set default without a flag:
160
- export CURSOR_ROUTE_DS_MODEL=pro # also honors ANTHROPIC_MODEL; --model overrides
162
+ export CURSOR_ROUTE_DS_MODEL=flash # also honors ANTHROPIC_MODEL; --model overrides
161
163
  ```
162
164
 
163
- **Grok auth usage-out:** if `cursor-route health` shows `worker:grok` ✗, run `grok login` (or set `XAI_API_KEY`). That is auth. Quota / subscription usage exhausted is different — use `--lane mid --model pro` as the stand-in, not a missing login.
165
+ If `--model` and `CURSOR_ROUTE_DS_MODEL` / `ANTHROPIC_MODEL` are unset, a prompt that looks like a screenshot/image/png/jpg/jpeg/webp/ui mock/multimodal/vision auto-picks vision Flash. Explicit `--model flash|pro|vision` always wins.
166
+
167
+ **Grok auth ≠ usage-out:** if `cursor-route health` shows `worker:grok` ✗, run `grok login` (or set `XAI_API_KEY`). That is auth, not the Pro case. When Grok **usage** is exhausted, stay on `--lane mid --model flash` (cheap default). `--model pro` is harder mid / hard backup only.
164
168
 
165
169
  `CURSOR_ROUTE_ALLOW_ANTHROPIC=1` is an expensive escape hatch: it does **not** pass DeepSeek `--model` ids (Anthropic would reject them).
166
170
 
@@ -180,8 +184,9 @@ export ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash
180
184
  export CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash
181
185
 
182
186
  cursor-route health # worker:claude-ds should be ✓; `lane:mid` is ✓ only when DeepSeek is proven
183
- cursor-route start --lane mid "…" # Flash (default)
184
- cursor-route start --lane mid --model pro "…" # Pro when you need it
187
+ cursor-route start --lane mid "…" # Flash (default; also when Grok usage is out)
188
+ cursor-route start --lane mid --model vision "…" # screenshots / ui mocks
189
+ cursor-route start --lane mid --model pro "…" # harder mid / hard backup only
185
190
  ```
186
191
 
187
192
  Persist the same vars under `~/.claude/settings.json` → `"env": { … }` if you want them every shell.
@@ -196,10 +201,10 @@ export DEEPSEEK_API_KEY=YOUR_DEEPSEEK_API_KEY # from platform.deepseek.com
196
201
 
197
202
  cursor-route health # worker:deepseek should be ✓
198
203
  cursor-route start --worker deepseek "…" # Flash (default)
199
- cursor-route start --worker deepseek --model pro "…" # Pro when you need it
204
+ cursor-route start --worker deepseek --model pro "…" # harder mid / hard backup only
200
205
  ```
201
206
 
202
- The adapter launches `dsh --profile headless` with a **per-job Cordis patch** (`jobs/<id>.dsh-patch.yml`, mode 0600) that pins `--model flash|pro` (`deepseek-v4-pro[1m]` preserved) — it never rewrites `~/.dsh/settings.yaml`, so parallel jobs don't race. Always-approve maps to `DSH_PERMISSION_MODE=danger-full-access`; `--ask` drops to `workspace-write`. Your `DEEPSEEK_API_KEY` travels via env only — never in the launch command or patch. Override the binary with `CURSOR_ROUTE_DSH_BIN`.
207
+ The adapter launches `dsh --profile headless` with a **per-job Cordis patch** (`jobs/<id>.dsh-patch.yml`, mode 0600) that pins `--model flash|pro|vision` (`deepseek-v4-pro[1m]` preserved) — it never rewrites `~/.dsh/settings.yaml`, so parallel jobs don't race. Always-approve maps to `DSH_PERMISSION_MODE=danger-full-access`; `--ask` drops to `workspace-write`. Your `DEEPSEEK_API_KEY` travels via env only — never in the launch command or patch. Override the binary with `CURSOR_ROUTE_DSH_BIN`.
203
208
 
204
209
  **Not the default:** bare `claude` still talking to Anthropic. Health refuses that so a misconfigured install cannot silently burn frontier $ rates. Escape hatch only: `CURSOR_ROUTE_ALLOW_ANTHROPIC=1`.
205
210
 
@@ -207,19 +212,27 @@ No DeepSeek yet? Use `--lane hard` / `--worker grok` (X Premium).
207
212
 
208
213
  ## OpenRouter setup (the free easy lane)
209
214
 
210
- `--lane easy` / `--worker openrouter` sends wording/draft prompts to OpenRouter's
211
- free model route (`openrouter/free`). Get a key at [openrouter.ai/keys](https://openrouter.ai/keys).
215
+ `--lane easy` / `--worker openrouter` sends wording/draft prompts to OpenRouter
216
+ and **live-picks the best free text model** at request time (`GET /models`, rank
217
+ `:free` or $0 text models). Do not hardcode a specific model id as the default.
218
+ Pin with `CURSOR_ROUTE_OPENROUTER_MODEL` or `--model provider/model`. Empty /
219
+ `free` = live pick. If the catalog fetch fails, the fallback is OpenRouter's
220
+ **router** id `openrouter/free` (a live router, not a locked model). Get a key
221
+ at [openrouter.ai/keys](https://openrouter.ai/keys).
212
222
 
213
223
  ```bash
214
224
  export OPENROUTER_API_KEY=sk-or-v1-... # from openrouter.ai/keys
215
- # optional:
216
- export CURSOR_ROUTE_OPENROUTER_MODEL=openrouter/free # default
225
+ # optional pin (skips the live catalog pick):
226
+ export CURSOR_ROUTE_OPENROUTER_MODEL=qwen/qwen3-coder:free
217
227
  export OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 # default
218
228
 
219
- cursor-route health # worker:openrouter should be ✓
229
+ cursor-route health # worker:openrouter should be ✓ (never fetches /models)
220
230
  cursor-route start --lane easy "Rewrite this FAQ answer in 3 sentences"
231
+ cursor-route start --lane easy --model free "…" # live pick
221
232
  ```
222
233
 
234
+ `health` never fetches the OpenRouter catalog (cache or fallback only), same as Zen.
235
+
223
236
  **Non-secret prompts only:** free OpenRouter models may log prompts, so the easy lane is for
224
237
  **wording/drafts without credentials**. The same refuse gate as every lane blocks
225
238
  key-shaped material in `start` / `send`, and the runner re-checks the prompt file.
@@ -277,13 +290,13 @@ cursor-route is a public MIT CLI and Cursor skill that runs parallel coding work
277
290
  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.
278
291
 
279
292
  **Does mid lane use Anthropic Claude?**
280
- 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`).
293
+ 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.
281
294
 
282
295
  **How do I install?**
283
296
  Run `npm i -g cursor-route`, install tmux if needed, then run `cursor-route health`. The package is available at https://www.npmjs.com/package/cursor-route, and the source is at https://github.com/cemini23/cursor-route.
284
297
 
285
298
  **Is it free?**
286
- 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 run live OpenCode Zen free models (`--model free` ranks the catalog).
299
+ 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 run live OpenCode Zen free models (`--model free` ranks the catalog).
287
300
 
288
301
  ## Related
289
302
 
@@ -191,7 +191,7 @@ export const claudeDsAdapter = {
191
191
  const isDeepSeekStock = resolved.mode.startsWith("claude → DeepSeek");
192
192
  const isShim = resolved.mode.startsWith("claude-ds") || resolved.mode.startsWith("deepseek-claude");
193
193
  // Anthropic escape hatch: do not pass DeepSeek model ids (unknown to Anthropic).
194
- // --model flash|pro is DeepSeek-only; stock Claude uses its own defaults / ANTHROPIC_MODEL.
194
+ // --model flash|pro|vision is DeepSeek-only; stock Claude uses its own defaults / ANTHROPIC_MODEL.
195
195
  if (isAnthropicEscape) {
196
196
  const parts = [
197
197
  shellQuote(resolved.binary),
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { dirname, join } from "node:path";
4
4
  import { shellQuote } from "../util.js";
5
- import { openRouterModel, openRouterBaseUrl } from "../config.js";
5
+ import { cachedOrFreePick, OPENROUTER_FALLBACK_MODEL, openRouterBaseUrl, } from "../config.js";
6
6
  /**
7
7
  * Resolve how to invoke the one-shot runner. Prefer the compiled dist via node
8
8
  * (no loader); else Bun on src. No npx/tsx — same policy as mark-complete.
@@ -19,12 +19,12 @@ function resolveRunner() {
19
19
  }
20
20
  return null;
21
21
  }
22
- function openRouterEnv() {
22
+ function openRouterEnv(modelId) {
23
23
  const key = process.env.OPENROUTER_API_KEY;
24
24
  if (!key)
25
25
  return undefined;
26
26
  const env = { OPENROUTER_API_KEY: key };
27
- const model = process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
27
+ const model = modelId || process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
28
28
  if (model)
29
29
  env.CURSOR_ROUTE_OPENROUTER_MODEL = model;
30
30
  const base = process.env.OPENROUTER_BASE_URL;
@@ -42,7 +42,7 @@ export const openRouterAdapter = {
42
42
  worker: "openrouter",
43
43
  ok: false,
44
44
  binary: runner?.command ?? null,
45
- detail: "OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane model defaults to openrouter/free)",
45
+ detail: "OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane live-picks a free model at start; pin with CURSOR_ROUTE_OPENROUTER_MODEL)",
46
46
  };
47
47
  }
48
48
  if (!runner) {
@@ -53,20 +53,26 @@ export const openRouterAdapter = {
53
53
  detail: "openrouter-run not found — run bun run build (or use Bun from a source clone)",
54
54
  };
55
55
  }
56
+ // Health stays offline: ranked cache hit, else label the router fallback
57
+ // (do not present openrouter/free as a fresh live pick).
58
+ const cached = cachedOrFreePick();
59
+ const detail = cached
60
+ ? `ok (live pick at start; cached ${cached} @ ${openRouterBaseUrl()})`
61
+ : `ok (live pick at start; no catalog cache, fetch-fail fallback ${OPENROUTER_FALLBACK_MODEL} @ ${openRouterBaseUrl()})`;
56
62
  return {
57
63
  worker: "openrouter",
58
64
  ok: true,
59
65
  binary: runner.command,
60
- detail: `ok (model ${openRouterModel()} @ ${openRouterBaseUrl()})`,
66
+ detail,
61
67
  };
62
68
  },
63
- buildLaunch({ promptFile }) {
69
+ buildLaunch({ promptFile, modelId }) {
64
70
  const runner = resolveRunner();
65
71
  if (!runner)
66
72
  throw new Error("openrouter runner not available — run: bun run build");
67
73
  // Missing key is tolerated here so `--dry-run` can still print the command;
68
74
  // real starts are gated by the health preflight (which requires the key).
69
- const env = openRouterEnv();
75
+ const env = openRouterEnv(modelId);
70
76
  // No interactive approval concept for a pure HTTP call — nothing to auto-approve.
71
77
  return {
72
78
  worker: "openrouter",
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { readFileSync, existsSync, realpathSync, statSync } from "node:fs";
6
6
  import { resolve, basename } from "node:path";
7
- import { config, WORKERS, LANES, resolveDsModel, openCodeModel, } from "./config.js";
7
+ import { config, WORKERS, LANES, resolveDsModel, openCodeModel, openRouterModel, } from "./config.js";
8
8
  import { runHealth, printHealth } from "./health.js";
9
9
  import { startJob, listJobs, readJob, killJob, cleanJobs, jobPaths, refreshStatus, jobEvidence, } from "./jobs.js";
10
10
  import { capturePane, sendKeys, attachHint, listManagedSessions, sessionExists, } from "./tmux.js";
@@ -31,7 +31,7 @@ Usage:
31
31
  Start options:
32
32
  --worker <grok|claude-ds|openrouter|deepseek|opencode> Worker adapter (default: grok; deepseek/opencode = opt-in)
33
33
  --lane <easy|mid|hard> Lane → worker (easy=openrouter, mid=claude-ds, hard=grok)
34
- --model <flash|pro|free|provider/model> claude-ds/deepseek: flash|pro. opencode: free (live Zen catalog pick) or provider/model
34
+ --model <flash|pro|vision|free|provider/model> claude-ds/deepseek: flash|pro|vision. opencode/openrouter: free (live catalog pick) or provider/model
35
35
  --dir <path> Working directory (default: cwd)
36
36
  --ask Disable always-approve for this job
37
37
  --dry-run Print launch command; do not start
@@ -44,14 +44,14 @@ Env:
44
44
  CURSOR_ROUTE_MAX_JOBS Max active jobs (default: 50)
45
45
  CURSOR_ROUTE_RELAXED=1 health OK without tmux/workers (CI / infra smoke)
46
46
  CURSOR_ROUTE_ALLOW_ANTHROPIC=1 Allow mid-lane on Anthropic Claude (expensive; not default; --model ignored)
47
- CURSOR_ROUTE_DS_MODEL Default mid model flash|pro (or deepseek-v4-pro[1m]); overridden by --model
47
+ CURSOR_ROUTE_DS_MODEL Default mid model flash|pro|vision (or full ids / deepseek-v4-pro[1m]); overridden by --model
48
48
  CURSOR_ROUTE_GROK_BIN Override the grok binary path (tests / power users)
49
49
  CURSOR_ROUTE_CLAUDE_DS_BIN Override the claude-ds binary path (tests / power users)
50
50
  CURSOR_ROUTE_DSH_BIN Override the dsh binary path (tests / power users)
51
51
  CURSOR_ROUTE_OPENCODE_BIN Override the opencode binary path (tests / power users)
52
52
  DEEPSEEK_API_KEY DeepSeek API key (required for --worker deepseek)
53
53
  OPENROUTER_API_KEY OpenRouter key (required for --worker openrouter / --lane easy)
54
- CURSOR_ROUTE_OPENROUTER_MODEL OpenRouter model (default: openrouter/free)
54
+ CURSOR_ROUTE_OPENROUTER_MODEL OpenRouter pin (unset or free = live catalog pick at start; fetch-fail fallback openrouter/free)
55
55
  OPENROUTER_BASE_URL OpenRouter API base (default: https://openrouter.ai/api/v1)
56
56
  CURSOR_ROUTE_OPENCODE_MODEL OpenCode model pin (unset or free = live Zen free pick); --model overrides
57
57
  `);
@@ -149,7 +149,7 @@ function asDsModelChoice(v) {
149
149
  if (v === undefined || v === true)
150
150
  return undefined;
151
151
  if (typeof v !== "string")
152
- throw new Error(`Invalid --model; expected flash|pro`);
152
+ throw new Error(`Invalid --model; expected flash|pro|vision`);
153
153
  return resolveDsModel(v);
154
154
  }
155
155
  function asOpenCodeModel(v) {
@@ -160,6 +160,14 @@ function asOpenCodeModel(v) {
160
160
  }
161
161
  return openCodeModel(v);
162
162
  }
163
+ function asOpenRouterModel(v) {
164
+ if (v === undefined || v === true)
165
+ return undefined;
166
+ if (typeof v !== "string") {
167
+ throw new Error(`Invalid --model; expected provider/model or free (live OpenRouter catalog pick)`);
168
+ }
169
+ return openRouterModel(v);
170
+ }
163
171
  function refuseSecrets(text, context) {
164
172
  if (looksLikeSecretMaterial(text)) {
165
173
  console.error(`Refusing ${context}: looks like secret key material. Remove tokens/keys and retry.`);
@@ -233,8 +241,8 @@ async function main() {
233
241
  const resolvedWorker = worker ?? (lane ? config.laneWorkers[lane] : config.defaultWorker);
234
242
  let model;
235
243
  let modelId;
236
- // --model: DeepSeek flash|pro for claude-ds/deepseek; provider/model (or free) for opencode;
237
- // ignore (do not validate) for grok/openrouter
244
+ // --model: DeepSeek flash|pro|vision for claude-ds/deepseek;
245
+ // provider/model (or free) for opencode/openrouter; ignore for grok
238
246
  if (f.model !== undefined && (resolvedWorker === "claude-ds" || resolvedWorker === "deepseek")) {
239
247
  try {
240
248
  const choice = asDsModelChoice(f.model);
@@ -257,6 +265,15 @@ async function main() {
257
265
  process.exit(2);
258
266
  }
259
267
  }
268
+ else if (f.model !== undefined && resolvedWorker === "openrouter") {
269
+ try {
270
+ modelId = asOpenRouterModel(f.model);
271
+ }
272
+ catch (e) {
273
+ console.error(e.message);
274
+ process.exit(2);
275
+ }
276
+ }
260
277
  let prompt = "";
261
278
  if (promptFile) {
262
279
  const p = resolve(promptFile);
package/dist/config.js CHANGED
@@ -3,11 +3,17 @@ import { join } from "node:path";
3
3
  import { defaultJobsDir } from "./runtime.js";
4
4
  export const WORKERS = ["grok", "claude-ds", "openrouter", "deepseek", "opencode"];
5
5
  export const LANES = ["easy", "mid", "hard"];
6
- export const DS_MODELS = ["flash", "pro"];
6
+ export const DS_MODELS = ["flash", "pro", "vision"];
7
7
  export const DS_MODEL_IDS = {
8
8
  flash: "deepseek-v4-flash",
9
9
  pro: "deepseek-v4-pro",
10
+ vision: "deepseek-v4-flash-vision-exp",
10
11
  };
12
+ /** Prompt looks like a screenshot / image / ui mock — vision auto-pick. */
13
+ const VISION_PROMPT_RE = /screenshot|\bimage\b|\.png|\.jpe?g|\.webp|ui mock|multimodal|\bvision\b/i;
14
+ export function promptLooksLikeVision(prompt) {
15
+ return VISION_PROMPT_RE.test(prompt);
16
+ }
11
17
  /**
12
18
  * Resolve --model / env to alias + concrete model id.
13
19
  * Preserves `deepseek-v4-pro[1m]` (does not silently strip the SKU).
@@ -24,10 +30,13 @@ export function resolveDsModel(raw) {
24
30
  if (v === "pro" || v === "deepseek-v4-pro") {
25
31
  return { alias: "pro", id: DS_MODEL_IDS.pro };
26
32
  }
33
+ if (v === "vision" || v === "deepseek-v4-flash-vision-exp") {
34
+ return { alias: "vision", id: DS_MODEL_IDS.vision };
35
+ }
27
36
  if (v === "deepseek-v4-pro[1m]") {
28
37
  return { alias: "pro", id: "deepseek-v4-pro[1m]" };
29
38
  }
30
- throw new Error(`Invalid --model ${raw}; expected flash|pro`);
39
+ throw new Error(`Invalid DeepSeek model ${raw}; expected flash|pro|vision`);
31
40
  }
32
41
  /** Alias-only helper (tests / callers that do not need the concrete id). */
33
42
  export function resolveDsModelAlias(raw) {
@@ -41,15 +50,12 @@ export function defaultDsModelFromEnv() {
41
50
  const raw = process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL;
42
51
  return resolveDsModel(raw);
43
52
  }
44
- /** OpenRouter model for the easy lane (env CURSOR_ROUTE_OPENROUTER_MODEL). */
45
- export function openRouterModel() {
46
- return process.env.CURSOR_ROUTE_OPENROUTER_MODEL || "openrouter/free";
47
- }
48
53
  /** OpenRouter API base URL (env OPENROUTER_BASE_URL). */
49
54
  export function openRouterBaseUrl() {
50
55
  return process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1";
51
56
  }
52
57
  export { OPENCODE_DEFAULT_MODEL, assertOpenCodeModel, openCodeModel, cachedZenFreePick, } from "./zen-free.js";
58
+ export { OPENROUTER_FALLBACK_MODEL, assertOpenRouterModel, cachedOrFreePick, openRouterModel, } from "./or-free.js";
53
59
  function maxConcurrentJobsFromEnv() {
54
60
  const raw = process.env.CURSOR_ROUTE_MAX_JOBS;
55
61
  if (raw) {
@@ -66,7 +72,7 @@ function maxConcurrentJobsFromEnv() {
66
72
  */
67
73
  export const config = {
68
74
  product: "cursor-route",
69
- version: "0.1.11",
75
+ version: "0.1.13",
70
76
  get jobsDir() {
71
77
  return defaultJobsDir();
72
78
  },
package/dist/jobs.js CHANGED
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, unlink
2
2
  import { join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { dirname } from "node:path";
5
- import { config, sessionName, defaultDsModelFromEnv, DS_MODEL_IDS, openCodeModel, } from "./config.js";
5
+ import { config, sessionName, defaultDsModelFromEnv, DS_MODEL_IDS, openCodeModel, openRouterModel, promptLooksLikeVision, } from "./config.js";
6
6
  import { getAdapter } from "./adapters/index.js";
7
7
  import { spawn } from "node:child_process";
8
8
  import { spawnSync } from "node:child_process";
@@ -126,14 +126,18 @@ function terminatePid(pid) {
126
126
  return true;
127
127
  sleepMs(100);
128
128
  }
129
- trySignal("SIGKILL", true);
130
- trySignal("SIGKILL", false);
129
+ const gKill = trySignal("SIGKILL", true);
130
+ const pKill = trySignal("SIGKILL", false);
131
131
  for (let i = 0; i < 10; i++) {
132
132
  if (!pidAlive(pid))
133
133
  return true;
134
134
  sleepMs(100);
135
135
  }
136
- return !pidAlive(pid);
136
+ if (!pidAlive(pid))
137
+ return true;
138
+ // Sandboxed `ps` (EPERM) treats unreaped zombies as alive. If SIGKILL was
139
+ // sent, killJob should succeed — refreshStatus still fail-safes to alive.
140
+ return gKill || pKill;
137
141
  }
138
142
  /** Refresh running jobs without inventing success from "session gone". */
139
143
  export function refreshStatus(job) {
@@ -253,20 +257,36 @@ export function startJob(opts) {
253
257
  modelId = opts.modelId ?? DS_MODEL_IDS[opts.model];
254
258
  }
255
259
  else {
256
- try {
257
- const choice = defaultDsModelFromEnv();
258
- dsAlias = choice.alias;
259
- model = choice.alias;
260
- modelId = opts.modelId ?? choice.id;
261
- }
262
- catch (e) {
260
+ const envRaw = (process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL || "").trim();
261
+ if (envRaw) {
263
262
  try {
264
- unlinkSync(paths.prompt);
263
+ const choice = defaultDsModelFromEnv();
264
+ dsAlias = choice.alias;
265
+ model = choice.alias;
266
+ modelId = opts.modelId ?? choice.id;
265
267
  }
266
268
  catch {
267
- /* ignore */
269
+ try {
270
+ unlinkSync(paths.prompt);
271
+ }
272
+ catch {
273
+ /* ignore */
274
+ }
275
+ return {
276
+ ok: false,
277
+ error: `Invalid CURSOR_ROUTE_DS_MODEL/ANTHROPIC_MODEL value ${envRaw}; expected flash|pro|vision`,
278
+ };
268
279
  }
269
- return { ok: false, error: e.message };
280
+ }
281
+ else if (promptLooksLikeVision(opts.prompt)) {
282
+ dsAlias = "vision";
283
+ model = "vision";
284
+ modelId = opts.modelId ?? DS_MODEL_IDS.vision;
285
+ }
286
+ else {
287
+ dsAlias = "flash";
288
+ model = "flash";
289
+ modelId = opts.modelId ?? DS_MODEL_IDS.flash;
270
290
  }
271
291
  }
272
292
  }
@@ -286,6 +306,22 @@ export function startJob(opts) {
286
306
  return { ok: false, error: e.message };
287
307
  }
288
308
  }
309
+ else if (worker === "openrouter") {
310
+ try {
311
+ const orModel = openRouterModel(opts.modelId);
312
+ model = orModel;
313
+ modelId = orModel;
314
+ }
315
+ catch (e) {
316
+ try {
317
+ unlinkSync(paths.prompt);
318
+ }
319
+ catch {
320
+ /* ignore */
321
+ }
322
+ return { ok: false, error: e.message };
323
+ }
324
+ }
289
325
  let plan;
290
326
  try {
291
327
  plan = adapter.buildLaunch({
@@ -19,7 +19,7 @@ function parseArgs(argv) {
19
19
  const a = argv[i];
20
20
  if (a === "-h" || a === "--help") {
21
21
  console.log("usage: cursor-route/openrouter-run --prompt-file <path>\n" +
22
- "env: OPENROUTER_API_KEY (required), CURSOR_ROUTE_OPENROUTER_MODEL, OPENROUTER_BASE_URL");
22
+ "env: OPENROUTER_API_KEY (required), CURSOR_ROUTE_OPENROUTER_MODEL (unset/free = live pick), OPENROUTER_BASE_URL");
23
23
  process.exit(0);
24
24
  }
25
25
  if (a === "--prompt-file") {