cursor-route 0.1.5 → 0.1.7

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 ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ ## 0.1.7 — 2026-08-12
4
+
5
+ - Fix: `CURSOR_ROUTE_DS_MODEL` / `ANTHROPIC_MODEL` now apply on the `start` product path (live default)
6
+ - Fix: Anthropic escape hatch (`CURSOR_ROUTE_ALLOW_ANTHROPIC=1`) no longer passes DeepSeek `--model` ids
7
+ - Fix: preserve `deepseek-v4-pro[1m]` (no silent SKU strip)
8
+ - `--model` validated only for `claude-ds`; ignored for grok/openrouter (including invalid values)
9
+ - `jobs` / `status` text output show `model` when set
10
+ - Tests cover env default via `startJob`, deepseek slot, Anthropic hatch, health OR-gate
11
+
12
+ ## 0.1.6 — 2026-08-12
13
+
14
+ - Mid lane defaults to DeepSeek **Flash** (`deepseek-v4-flash`)
15
+ - `cursor-route start --model flash|pro` (aliases / full ids accepted)
16
+ - `claude-ds` adapter passes `-Model`; stock `claude` → DeepSeek gets `--model`
17
+ - Reserved `deepseek` worker slot (unreleased official harness; mid stays on `claude-ds`)
18
+ - Docs: Grok **auth** (`grok login`) ≠ usage/quota out → use `--model pro`
19
+ - `route-orch` skill Flash/Pro table; living brief in `docs/briefs/WORKING.md`
20
+
21
+ ## 0.1.5
22
+
23
+ - OpenRouter free easy lane (`--lane easy` / `--worker openrouter`)
24
+
25
+ ## 0.1.4
26
+
27
+ - Compiled `dist/` happy path, integration tests, `CURSOR_ROUTE_MAX_JOBS`
28
+
29
+ ## Earlier
30
+
31
+ See git tags `v0.1.1`–`v0.1.3` for security/hardening and first npm ship notes.
package/CONTRIBUTING.md CHANGED
@@ -5,22 +5,30 @@
5
5
  ```bash
6
6
  bun install
7
7
  bun test
8
+ bun run typecheck
8
9
  ./bin/cursor-route health
9
10
  CURSOR_ROUTE_RELAXED=1 ./bin/cursor-route health # without tmux
10
11
  ./bin/cursor-route start --dry-run --worker grok "ping"
12
+ ./bin/cursor-route start --dry-run --lane mid --model flash "ping"
13
+ ./bin/cursor-route start --dry-run --lane mid --model pro "ping"
11
14
  ```
12
15
 
16
+ Keep `skills/route-orch/SKILL.md` and `.cursor/skills/route-orch/SKILL.md` identical when editing the skill.
17
+
13
18
  ## Rules
14
19
 
15
20
  - No secrets in prompts, fixtures, or commits
16
21
  - Do not vendor private `agent-toolkit` paths
17
22
  - Prefer adapters over rewriting the job core
18
23
  - Always-approve defaults stay documented + opt-out (`--ask` / `CURSOR_ROUTE_ASK=1`)
24
+ - Mid default is Flash; document Pro only as harder mid / Grok **usage** stand-in (not missing `grok login`)
19
25
 
20
26
  ## Release checklist
21
27
 
22
- 1. `bun test`
23
- 2. `cursor-route health` (tmux + ≥1 worker)
24
- 3. One real `start` smoke (grok or claude-ds)
25
- 4. `npm pack --dry-run` no secrets, no `node_modules`
26
- 5. Tag `v0.x.y` then `npm publish --access public` (maintainers)
28
+ 1. Update `package.json` version + [CHANGELOG.md](./CHANGELOG.md) + [docs/briefs/WORKING.md](./docs/briefs/WORKING.md)
29
+ 2. `bun test` && `bun run typecheck` && `bun run build`
30
+ 3. `cursor-route health` (tmux + ≥1 worker)
31
+ 4. One real `start` smoke (grok or claude-ds); dry-run both `--model flash` and `--model pro`
32
+ 5. `npm pack --dry-run` no secrets, no `node_modules`
33
+ 6. Commit, tag `v0.x.y`, push
34
+ 7. `npm publish --access public` with an isolated `--userconfig` Automation token (Bypass 2FA); **revoke** any chat-pasted token after
package/README.md CHANGED
@@ -27,7 +27,7 @@ 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 |
30
+ | DeepSeek via claude-ds | DeepSeek API / plan | `--lane mid` implement (**Flash** default; `--model pro` when needed) |
31
31
  | OpenRouter free models | OpenRouter API (free tier) | `--lane easy` wording/drafts — non-secret prompts only (see Security) |
32
32
  | Codex / extra Claude | Optional | Not required for v0 |
33
33
 
@@ -73,21 +73,26 @@ cd cursor-route && bun install
73
73
 
74
74
  ### Cursor skill
75
75
 
76
- Copy the skill into your project or user skills:
76
+ After `npm i -g cursor-route`, copy the packaged skill into a project (or user skills):
77
77
 
78
78
  ```bash
79
+ PKG="$(npm root -g)/cursor-route"
79
80
  mkdir -p .cursor/skills
80
- cp -R ~/.cursor-route-src/skills/route-orch .cursor/skills/
81
+ cp -R "$PKG/skills/route-orch" .cursor/skills/
81
82
  ```
82
83
 
83
- Then say **`/route-orch`** or **spawn workers** in Cursor — the skill delegates to `cursor-route` (does not replace private Cemini `/route`).
84
+ From a git clone: `cp -R skills/route-orch .cursor/skills/`.
85
+
86
+ Then say **`/route-orch`** or **spawn workers** in Cursor — the skill delegates to `cursor-route` (does not replace a private in-house `/route` skill).
87
+
88
+ Working notes for this repo (edit in place): [docs/briefs/WORKING.md](./docs/briefs/WORKING.md). See [CHANGELOG.md](./CHANGELOG.md) for release notes.
84
89
 
85
90
  ## Commands
86
91
 
87
92
  | Command | Description |
88
93
  |---------|-------------|
89
94
  | `health` | Gate #1 — tmux, runtime, workers |
90
- | `start <prompt>` | Spawn worker job (`--worker` / `--lane` / `--dir` / `--ask` / `--dry-run` / `--no-tmux`) |
95
+ | `start <prompt>` | Spawn worker job (`--worker` / `--lane` / `--model` / `--dir` / `--ask` / `--dry-run` / `--no-tmux`) |
91
96
  | `jobs [--json]` | List jobs |
92
97
  | `status <id>` | Job + sessionAlive |
93
98
  | `capture <id> [n]` | Last n pane/log lines |
@@ -102,11 +107,30 @@ Then say **`/route-orch`** or **spawn workers** in Cursor — the skill delegate
102
107
  | Lane | Worker | Intent |
103
108
  |------|--------|--------|
104
109
  | `easy` | `openrouter` | Wording / drafts on OpenRouter free models (non-secret prompts only — see Security) |
105
- | `mid` | `claude-ds` | Default implement on DeepSeek |
110
+ | `mid` | `claude-ds` | Default implement on DeepSeek (**Flash** by default) |
106
111
  | `hard` | `grok` | Hard implement on Grok CLI |
107
112
 
108
113
  Always-approve is **on** by default. Opt out: `--ask` or `CURSOR_ROUTE_ASK=1`.
109
114
 
115
+ ### Mid models (Flash vs Pro)
116
+
117
+ | Flag | Model id | When |
118
+ |------|----------|------|
119
+ | `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute |
120
+ | `--model pro` | `deepseek-v4-pro` | Harder mid / Grok **usage** stand-in |
121
+ | `--model deepseek-v4-pro[1m]` | `deepseek-v4-pro[1m]` | Large-context Pro (SKU preserved) |
122
+
123
+ ```bash
124
+ cursor-route start --lane mid "…" # Flash
125
+ cursor-route start --lane mid --model pro "…" # Pro
126
+ # Or set default without a flag:
127
+ export CURSOR_ROUTE_DS_MODEL=pro # also honors ANTHROPIC_MODEL; --model overrides
128
+ ```
129
+
130
+ **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.
131
+
132
+ `CURSOR_ROUTE_ALLOW_ANTHROPIC=1` is an expensive escape hatch: it does **not** pass DeepSeek `--model` ids (Anthropic would reject them).
133
+
110
134
  ## DeepSeek setup (the cheap mid-lane — this is the point)
111
135
 
112
136
  `cursor-route` mid lane runs **DeepSeek**, not Anthropic. Claude Code is only the terminal harness; bills go to DeepSeek when `ANTHROPIC_BASE_URL` points at them.
@@ -118,17 +142,20 @@ npm i -g @anthropic-ai/claude-code
118
142
 
119
143
  export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
120
144
  export ANTHROPIC_AUTH_TOKEN=YOUR_DEEPSEEK_API_KEY # from platform.deepseek.com
121
- export ANTHROPIC_MODEL=deepseek-v4-pro[1m]
145
+ # Optional shell defaults (CLI --model overrides ANTHROPIC_MODEL for the job):
122
146
  export ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash
123
147
  export CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash
124
148
 
125
149
  cursor-route health # worker:claude-ds should be ✓
126
- cursor-route start --lane mid "…"
150
+ cursor-route start --lane mid "…" # Flash (default)
151
+ cursor-route start --lane mid --model pro "…" # Pro when you need it
127
152
  ```
128
153
 
129
154
  Persist the same vars under `~/.claude/settings.json` → `"env": { … }` if you want them every shell.
130
155
 
131
- **Also accepted:** `claude-ds` or `deepseek-claude` on PATH (Cemini shims).
156
+ **Also accepted:** `claude-ds` or `deepseek-claude` on PATH (Cemini shims). The adapter passes `-Model deepseek-v4-flash|deepseek-v4-pro`.
157
+
158
+ **Reserved:** `--worker deepseek` is a slot for an official DeepSeek coding harness — not wired yet. Mid stays on `claude-ds`.
132
159
 
133
160
  **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`.
134
161
 
@@ -173,7 +200,7 @@ cursor-route is a public MIT CLI and Cursor skill that runs parallel coding work
173
200
  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.
174
201
 
175
202
  **Does mid lane use Anthropic Claude?**
176
- 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`.
203
+ 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`).
177
204
 
178
205
  **How do I install?**
179
206
  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.
@@ -183,6 +210,7 @@ The cursor-route code is open source under MIT. It does not make the worker serv
183
210
 
184
211
  ## Related
185
212
 
213
+ - Changelog: [CHANGELOG.md](./CHANGELOG.md)
186
214
  - Newsletter: [Outlier Weekly](https://outlierweekly.substack.com)
187
215
  - YouTube: [@Cemini23](https://www.youtube.com/@Cemini23)
188
216
  - Agent meta-wiki: [cemini-claude-code-CCC](https://github.com/cemini23/cemini-claude-code-CCC)
@@ -2,6 +2,7 @@ import { execSync } from "node:child_process";
2
2
  import { readFileSync, existsSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { DS_MODEL_IDS, config, resolveDsModel, } from "../config.js";
5
6
  import { shellQuote } from "../util.js";
6
7
  /**
7
8
  * Mid-lane = DeepSeek (cheap). The Claude Code binary is only a harness —
@@ -19,6 +20,8 @@ function which(cmd) {
19
20
  return (execSync(`command -v ${shellQuote(cmd)}`, {
20
21
  encoding: "utf8",
21
22
  stdio: ["ignore", "pipe", "ignore"],
23
+ // Bun may ignore mutated process.env.PATH unless env is passed explicitly
24
+ env: { ...process.env },
22
25
  }).trim() || null);
23
26
  }
24
27
  catch {
@@ -54,15 +57,16 @@ function deepseekBaseFromSettings() {
54
57
  }
55
58
  /** Resolved DeepSeek base URL for the mid-lane harness, or null. */
56
59
  export function resolvedDeepSeekBaseUrl() {
60
+ // Explicit env wins: a non-DeepSeek ANTHROPIC_BASE_URL must not fall through to settings.
57
61
  for (const candidate of [
58
62
  process.env.ANTHROPIC_BASE_URL,
59
63
  process.env.CURSOR_ROUTE_ANTHROPIC_BASE_URL,
60
- deepseekBaseFromSettings(),
61
64
  ]) {
62
- if (candidate && isDeepSeekBaseUrl(candidate))
63
- return candidate;
65
+ if (candidate) {
66
+ return isDeepSeekBaseUrl(candidate) ? candidate : null;
67
+ }
64
68
  }
65
- return null;
69
+ return deepseekBaseFromSettings();
66
70
  }
67
71
  /** True when Claude Code harness is routed to DeepSeek (cheap path). */
68
72
  export function isDeepSeekRouted() {
@@ -104,24 +108,35 @@ function resolveClaudeDs() {
104
108
  }
105
109
  return null;
106
110
  }
111
+ function pickModel(requested, modelId) {
112
+ if (modelId) {
113
+ const alias = requested ?? resolveDsModel(modelId).alias;
114
+ return { alias, id: modelId };
115
+ }
116
+ if (requested) {
117
+ return { alias: requested, id: DS_MODEL_IDS[requested] };
118
+ }
119
+ // Env default (startJob normally resolves this; kept for direct buildLaunch callers)
120
+ return resolveDsModel(process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL);
121
+ }
107
122
  /**
108
123
  * Env that must reach stock `claude` for DeepSeek routing.
109
124
  * Passed via process/tmux env — never interpolated into the printed command.
110
125
  */
111
- function deepSeekWorkerEnv() {
126
+ function deepSeekWorkerEnv(modelId) {
112
127
  const base = resolvedDeepSeekBaseUrl();
113
128
  if (!base)
114
129
  return undefined;
115
- const env = { ANTHROPIC_BASE_URL: base };
130
+ const env = {
131
+ ANTHROPIC_BASE_URL: base,
132
+ ANTHROPIC_MODEL: modelId,
133
+ };
116
134
  const token = process.env.ANTHROPIC_AUTH_TOKEN ||
117
135
  process.env.ANTHROPIC_API_KEY ||
118
136
  process.env.DEEPSEEK_API_KEY ||
119
137
  "";
120
138
  if (token)
121
139
  env.ANTHROPIC_AUTH_TOKEN = token;
122
- const model = process.env.ANTHROPIC_MODEL;
123
- if (model)
124
- env.ANTHROPIC_MODEL = model;
125
140
  return env;
126
141
  }
127
142
  export const claudeDsAdapter = {
@@ -144,24 +159,44 @@ export const claudeDsAdapter = {
144
159
  worker: "claude-ds",
145
160
  ok: true,
146
161
  binary: resolved.binary,
147
- detail: `ok (${resolved.mode})`,
162
+ detail: `ok (${resolved.mode}; default model ${DS_MODEL_IDS[config.defaultDsModel]})`,
148
163
  };
149
164
  },
150
- buildLaunch({ promptFile, cwd, alwaysApprove }) {
165
+ buildLaunch({ promptFile, cwd, alwaysApprove, model, modelId }) {
151
166
  const resolved = resolveClaudeDs();
152
167
  if (!resolved) {
153
168
  throw new Error("DeepSeek worker not available — run: cursor-route health");
154
169
  }
155
170
  const ask = process.env.CURSOR_ROUTE_ASK === "1" || process.env.CLAUDE_DS_ASK === "1";
156
171
  const skip = alwaysApprove && !ask;
157
- // Stock `claude` needs DeepSeek env injected into the worker process
158
- // (tmux panes may not inherit client env from a long-lived server).
159
- const env = resolved.mode.startsWith("claude → DeepSeek") ? deepSeekWorkerEnv() : undefined;
160
- if (resolved.mode.startsWith("claude-ds")) {
172
+ const isAnthropicEscape = resolved.mode.startsWith("claude Anthropic");
173
+ const isDeepSeekStock = resolved.mode.startsWith("claude DeepSeek");
174
+ const isShim = resolved.mode.startsWith("claude-ds") || resolved.mode.startsWith("deepseek-claude");
175
+ // Anthropic escape hatch: do not pass DeepSeek model ids (unknown to Anthropic).
176
+ // --model flash|pro is DeepSeek-only; stock Claude uses its own defaults / ANTHROPIC_MODEL.
177
+ if (isAnthropicEscape) {
178
+ const parts = [
179
+ shellQuote(resolved.binary),
180
+ "-p",
181
+ `"$(cat ${shellQuote(promptFile)})"`,
182
+ ];
183
+ if (skip)
184
+ parts.push("--dangerously-skip-permissions");
185
+ return {
186
+ worker: "claude-ds",
187
+ command: `cd ${shellQuote(cwd)} && ${parts.join(" ")}`,
188
+ alwaysApprove: skip,
189
+ };
190
+ }
191
+ const choice = pickModel(model, modelId);
192
+ const env = isDeepSeekStock ? deepSeekWorkerEnv(choice.id) : undefined;
193
+ if (isShim) {
161
194
  const parts = [
162
195
  shellQuote(resolved.binary),
163
196
  "-PromptFile",
164
197
  shellQuote(promptFile),
198
+ "-Model",
199
+ shellQuote(choice.id),
165
200
  ];
166
201
  if (skip)
167
202
  parts.push("--dangerously-skip-permissions");
@@ -172,10 +207,13 @@ export const claudeDsAdapter = {
172
207
  env,
173
208
  };
174
209
  }
210
+ // Stock claude → DeepSeek
175
211
  const parts = [
176
212
  shellQuote(resolved.binary),
177
213
  "-p",
178
214
  `"$(cat ${shellQuote(promptFile)})"`,
215
+ "--model",
216
+ shellQuote(choice.id),
179
217
  ];
180
218
  if (skip)
181
219
  parts.push("--dangerously-skip-permissions");
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Reserved slot for the official DeepSeek coding harness when it ships.
3
+ * Mid lane stays on claude-ds until then — do not route jobs here.
4
+ */
5
+ export const deepseekAdapter = {
6
+ kind: "deepseek",
7
+ label: "Official DeepSeek harness (unreleased)",
8
+ health() {
9
+ return {
10
+ worker: "deepseek",
11
+ ok: false,
12
+ binary: null,
13
+ detail: "unreleased — mid lane uses claude-ds (DeepSeek behind Claude Code). See README.",
14
+ };
15
+ },
16
+ buildLaunch() {
17
+ throw new Error("Official DeepSeek harness is not available yet — use --lane mid / --worker claude-ds");
18
+ },
19
+ };
@@ -1,10 +1,12 @@
1
1
  import { grokAdapter } from "./grok.js";
2
2
  import { claudeDsAdapter } from "./claude-ds.js";
3
3
  import { openRouterAdapter } from "./openrouter.js";
4
+ import { deepseekAdapter } from "./deepseek.js";
4
5
  const registry = {
5
6
  grok: grokAdapter,
6
7
  "claude-ds": claudeDsAdapter,
7
8
  openrouter: openRouterAdapter,
9
+ deepseek: deepseekAdapter,
8
10
  };
9
11
  export function getAdapter(worker) {
10
12
  const a = registry[worker];
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 } from "./config.js";
7
+ import { config, WORKERS, LANES, resolveDsModel, } from "./config.js";
8
8
  import { runHealth, printHealth } from "./health.js";
9
9
  import { startJob, listJobs, readJob, killJob, cleanJobs, jobPaths, refreshStatus, } from "./jobs.js";
10
10
  import { capturePane, sendKeys, attachHint, listManagedSessions, sessionExists, } from "./tmux.js";
@@ -29,8 +29,9 @@ Usage:
29
29
  cursor-route clean [--days N]
30
30
 
31
31
  Start options:
32
- --worker <grok|claude-ds|openrouter> Worker adapter (default: grok)
32
+ --worker <grok|claude-ds|openrouter> Worker adapter (default: grok; deepseek = reserved slot)
33
33
  --lane <easy|mid|hard> Lane → worker (easy=openrouter, mid=claude-ds, hard=grok)
34
+ --model <flash|pro> Mid DeepSeek only (default: flash / CURSOR_ROUTE_DS_MODEL). Parsed only for claude-ds
34
35
  --dir <path> Working directory (default: cwd)
35
36
  --ask Disable always-approve for this job
36
37
  --dry-run Print launch command; do not start
@@ -42,7 +43,8 @@ Env:
42
43
  CURSOR_ROUTE_JOBS_DIR Override jobs dir (default: ~/.local/share/cursor-route/jobs)
43
44
  CURSOR_ROUTE_MAX_JOBS Max active jobs (default: 50)
44
45
  CURSOR_ROUTE_RELAXED=1 health OK without tmux/workers (CI / infra smoke)
45
- CURSOR_ROUTE_ALLOW_ANTHROPIC=1 Allow mid-lane on Anthropic Claude (expensive; not default)
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
46
48
  CURSOR_ROUTE_GROK_BIN Override the grok binary path (tests / power users)
47
49
  CURSOR_ROUTE_CLAUDE_DS_BIN Override the claude-ds binary path (tests / power users)
48
50
  OPENROUTER_API_KEY OpenRouter key (required for --worker openrouter / --lane easy)
@@ -139,6 +141,13 @@ function asLane(v) {
139
141
  return v;
140
142
  throw new Error(`Invalid --lane ${v}; expected ${LANES.join("|")}`);
141
143
  }
144
+ function asDsModelChoice(v) {
145
+ if (v === undefined || v === true)
146
+ return undefined;
147
+ if (typeof v !== "string")
148
+ throw new Error(`Invalid --model; expected flash|pro`);
149
+ return resolveDsModel(v);
150
+ }
142
151
  function refuseSecrets(text, context) {
143
152
  if (looksLikeSecretMaterial(text)) {
144
153
  console.error(`Refusing ${context}: looks like secret key material. Remove tokens/keys and retry.`);
@@ -196,8 +205,36 @@ async function main() {
196
205
  if (cmd === "start") {
197
206
  requireStringFlag(f, "worker");
198
207
  requireStringFlag(f, "lane");
208
+ requireStringFlag(f, "model");
199
209
  const promptFile = requireStringFlag(f, "prompt-file");
200
210
  const dirFlag = requireStringFlag(f, "dir");
211
+ let worker;
212
+ let lane;
213
+ try {
214
+ worker = asWorker(f.worker);
215
+ lane = asLane(f.lane);
216
+ }
217
+ catch (e) {
218
+ console.error(e.message);
219
+ process.exit(2);
220
+ }
221
+ const resolvedWorker = worker ?? (lane ? config.laneWorkers[lane] : config.defaultWorker);
222
+ let model;
223
+ let modelId;
224
+ // --model is DeepSeek/claude-ds only; ignore (do not validate) for other workers
225
+ if (f.model !== undefined && resolvedWorker === "claude-ds") {
226
+ try {
227
+ const choice = asDsModelChoice(f.model);
228
+ if (choice) {
229
+ model = choice.alias;
230
+ modelId = choice.id;
231
+ }
232
+ }
233
+ catch (e) {
234
+ console.error(e.message);
235
+ process.exit(2);
236
+ }
237
+ }
201
238
  let prompt = "";
202
239
  if (promptFile) {
203
240
  const p = resolve(promptFile);
@@ -232,8 +269,10 @@ async function main() {
232
269
  }
233
270
  const result = startJob({
234
271
  prompt,
235
- worker: asWorker(f.worker),
236
- lane: asLane(f.lane),
272
+ worker,
273
+ lane,
274
+ model,
275
+ modelId,
237
276
  cwd,
238
277
  alwaysApprove: !f.ask,
239
278
  dryRun: Boolean(f.dryRun),
@@ -252,10 +291,13 @@ async function main() {
252
291
  else if (result.dryRun) {
253
292
  console.log(`dry-run job ${result.job.id}`);
254
293
  console.log(`worker: ${result.job.worker}`);
294
+ if (result.job.model)
295
+ console.log(`model: ${result.job.model}`);
255
296
  console.log(`command: ${redactSecrets(result.command || "")}`);
256
297
  }
257
298
  else {
258
- console.log(`started ${result.job.id} (${result.job.worker})`);
299
+ const modelNote = result.job.model ? `/${result.job.model}` : "";
300
+ console.log(`started ${result.job.id} (${result.job.worker}${modelNote})`);
259
301
  console.log(`session: ${result.job.tmuxSession}`);
260
302
  if (String(result.job.tmuxSession).startsWith("headless-")) {
261
303
  console.log(`mode: headless (--no-tmux); use capture/status (no attach/send)`);
@@ -282,7 +324,8 @@ async function main() {
282
324
  else {
283
325
  for (const j of jobs) {
284
326
  const age = j.startedAt || j.createdAt;
285
- console.log(`${j.id} ${j.status.padEnd(10)} ${j.worker.padEnd(10)} ${age} ${j.prompt.slice(0, 48).replace(/\n/g, " ")}`);
327
+ const modelCol = j.model ? j.model.padEnd(6) : "".padEnd(6);
328
+ console.log(`${j.id} ${j.status.padEnd(10)} ${j.worker.padEnd(10)} ${modelCol} ${age} ${j.prompt.slice(0, 48).replace(/\n/g, " ")}`);
286
329
  }
287
330
  }
288
331
  return;
@@ -314,7 +357,7 @@ async function main() {
314
357
  if (json)
315
358
  console.log(JSON.stringify(view, null, 2));
316
359
  else {
317
- console.log(`${job.id} ${job.status} worker=${job.worker} sessionAlive=${alive}`);
360
+ console.log(`${job.id} ${job.status} worker=${job.worker}${job.model ? ` model=${job.model}` : ""} sessionAlive=${alive}`);
318
361
  if (job.error)
319
362
  console.log(`error: ${job.error}`);
320
363
  }
package/dist/config.js CHANGED
@@ -1,8 +1,46 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { defaultJobsDir } from "./runtime.js";
4
- export const WORKERS = ["grok", "claude-ds", "openrouter"];
4
+ export const WORKERS = ["grok", "claude-ds", "openrouter", "deepseek"];
5
5
  export const LANES = ["easy", "mid", "hard"];
6
+ export const DS_MODELS = ["flash", "pro"];
7
+ export const DS_MODEL_IDS = {
8
+ flash: "deepseek-v4-flash",
9
+ pro: "deepseek-v4-pro",
10
+ };
11
+ /**
12
+ * Resolve --model / env to alias + concrete model id.
13
+ * Preserves `deepseek-v4-pro[1m]` (does not silently strip the SKU).
14
+ * Empty → Flash.
15
+ */
16
+ export function resolveDsModel(raw) {
17
+ if (!raw || !raw.trim()) {
18
+ return { alias: "flash", id: DS_MODEL_IDS.flash };
19
+ }
20
+ const v = raw.trim().toLowerCase();
21
+ if (v === "flash" || v === "deepseek-v4-flash") {
22
+ return { alias: "flash", id: DS_MODEL_IDS.flash };
23
+ }
24
+ if (v === "pro" || v === "deepseek-v4-pro") {
25
+ return { alias: "pro", id: DS_MODEL_IDS.pro };
26
+ }
27
+ if (v === "deepseek-v4-pro[1m]") {
28
+ return { alias: "pro", id: "deepseek-v4-pro[1m]" };
29
+ }
30
+ throw new Error(`Invalid --model ${raw}; expected flash|pro`);
31
+ }
32
+ /** Alias-only helper (tests / callers that do not need the concrete id). */
33
+ export function resolveDsModelAlias(raw) {
34
+ return resolveDsModel(raw).alias;
35
+ }
36
+ /**
37
+ * Mid default from env: CURSOR_ROUTE_DS_MODEL, else ANTHROPIC_MODEL, else flash.
38
+ * Throws if the env value is set but invalid (fail loud on start).
39
+ */
40
+ export function defaultDsModelFromEnv() {
41
+ const raw = process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL;
42
+ return resolveDsModel(raw);
43
+ }
6
44
  /** OpenRouter model for the easy lane (env CURSOR_ROUTE_OPENROUTER_MODEL). */
7
45
  export function openRouterModel() {
8
46
  return process.env.CURSOR_ROUTE_OPENROUTER_MODEL || "openrouter/free";
@@ -27,7 +65,7 @@ function maxConcurrentJobsFromEnv() {
27
65
  */
28
66
  export const config = {
29
67
  product: "cursor-route",
30
- version: "0.1.5",
68
+ version: "0.1.7",
31
69
  get jobsDir() {
32
70
  return defaultJobsDir();
33
71
  },
@@ -39,6 +77,19 @@ export const config = {
39
77
  mid: "claude-ds",
40
78
  hard: "grok",
41
79
  },
80
+ /**
81
+ * Default mid DeepSeek model (Flash = cheap execute).
82
+ * Live: CURSOR_ROUTE_DS_MODEL / ANTHROPIC_MODEL; invalid env → flash (health-safe).
83
+ * Override on start: --model. startJob uses defaultDsModelFromEnv() and fails on invalid env.
84
+ */
85
+ get defaultDsModel() {
86
+ try {
87
+ return defaultDsModelFromEnv().alias;
88
+ }
89
+ catch {
90
+ return "flash";
91
+ }
92
+ },
42
93
  jobsListLimit: 20,
43
94
  /** Max simultaneously active (running|pending) jobs. Override: CURSOR_ROUTE_MAX_JOBS. */
44
95
  get maxConcurrentJobs() {
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 } from "./config.js";
5
+ import { config, sessionName, defaultDsModelFromEnv, DS_MODEL_IDS, } 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";
@@ -217,12 +217,38 @@ export function startJob(opts) {
217
217
  const id = newJobId();
218
218
  const paths = jobPaths(id);
219
219
  writeSecure(paths.prompt, opts.prompt);
220
+ let model;
221
+ let modelId;
222
+ if (worker === "claude-ds") {
223
+ if (opts.model) {
224
+ model = opts.model;
225
+ modelId = opts.modelId ?? DS_MODEL_IDS[opts.model];
226
+ }
227
+ else {
228
+ try {
229
+ const choice = defaultDsModelFromEnv();
230
+ model = choice.alias;
231
+ modelId = opts.modelId ?? choice.id;
232
+ }
233
+ catch (e) {
234
+ try {
235
+ unlinkSync(paths.prompt);
236
+ }
237
+ catch {
238
+ /* ignore */
239
+ }
240
+ return { ok: false, error: e.message };
241
+ }
242
+ }
243
+ }
220
244
  let plan;
221
245
  try {
222
246
  plan = adapter.buildLaunch({
223
247
  promptFile: paths.prompt,
224
248
  cwd,
225
249
  alwaysApprove,
250
+ model,
251
+ modelId,
226
252
  });
227
253
  }
228
254
  catch (e) {
@@ -240,6 +266,7 @@ export function startJob(opts) {
240
266
  status: "pending",
241
267
  worker,
242
268
  lane: opts.lane,
269
+ model,
243
270
  prompt: opts.prompt,
244
271
  cwd,
245
272
  alwaysApprove: plan.alwaysApprove,
package/docs/DEMO_GIF.md CHANGED
@@ -13,7 +13,8 @@ cursor-route health
13
13
 
14
14
  # Terminal A — three parallel jobs
15
15
  cursor-route start --lane hard --dir "$PWD" "Add README section Demo"
16
- cursor-route start --lane mid --dir "$PWD" "Add a unit test for shellQuote"
16
+ cursor-route start --lane mid --dir "$PWD" "Add a unit test for shellQuote" # Flash default
17
+ # Optional: cursor-route start --lane mid --model pro --dir "$PWD" "…"
17
18
  cursor-route start --worker grok --dir "$PWD" "List open TODOs in src/"
18
19
 
19
20
  # Terminal B — watch