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 +17 -0
- package/CONTRIBUTING.md +3 -2
- package/README.md +33 -20
- package/dist/adapters/claude-ds.js +1 -1
- package/dist/adapters/openrouter.js +13 -7
- package/dist/cli.js +24 -7
- package/dist/config.js +13 -7
- package/dist/jobs.js +50 -14
- package/dist/openrouter-run.js +1 -1
- package/dist/or-free.js +216 -0
- package/docs/briefs/WORKING.md +11 -6
- package/docs/demo-notes.md +3 -3
- package/docs/fixtures/generate-hero-demo.sh +3 -0
- package/docs/fixtures/hero-demo.log +5 -3
- package/llms.txt +8 -6
- package/package.json +1 -1
- package/skills/route-orch/SKILL.md +12 -7
- package/src/adapters/claude-ds.ts +1 -1
- package/src/adapters/openrouter.test.ts +32 -3
- package/src/adapters/openrouter.ts +17 -7
- package/src/adapters/types.ts +2 -2
- package/src/cli.test.ts +266 -5
- package/src/cli.ts +22 -6
- package/src/config.ts +23 -9
- package/src/jobs.ts +47 -15
- package/src/openrouter-run.ts +1 -1
- package/src/or-free.test.ts +222 -0
- package/src/or-free.ts +243 -0
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;
|
|
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
|
|
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`
|
|
31
|
-
| OpenRouter free models | OpenRouter API (free tier) | `--lane easy`
|
|
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.
|
|
119
|
+
0.1.13
|
|
120
120
|
|
|
121
121
|
$ CURSOR_ROUTE_RELAXED=1 cursor-route health
|
|
122
|
-
cursor-route v0.1.
|
|
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
|
|
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
|
|
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=
|
|
162
|
+
export CURSOR_ROUTE_DS_MODEL=flash # also honors ANTHROPIC_MODEL; --model overrides
|
|
161
163
|
```
|
|
162
164
|
|
|
163
|
-
|
|
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
|
|
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 "…" #
|
|
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
|
|
211
|
-
|
|
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=
|
|
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`)
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
237
|
-
//
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
263
|
+
const choice = defaultDsModelFromEnv();
|
|
264
|
+
dsAlias = choice.alias;
|
|
265
|
+
model = choice.alias;
|
|
266
|
+
modelId = opts.modelId ?? choice.id;
|
|
265
267
|
}
|
|
266
268
|
catch {
|
|
267
|
-
|
|
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
|
-
|
|
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({
|
package/dist/openrouter-run.js
CHANGED
|
@@ -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") {
|