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.
@@ -0,0 +1,61 @@
1
+ ---
2
+ title: cursor-route workspace — working brief (edit in place)
3
+ repo: ~/Projects/cursor-route
4
+ npm: cursor-route@0.1.7 (LIVE on npm latest)
5
+ created: 2026-08-12
6
+ updated: 2026-08-12
7
+ ---
8
+
9
+ # cursor-route — living brief
10
+
11
+ **Edit this file.** It is the working notes for this public repo (CLI + `route-orch` skill + adapters). Not the private Cemini `/route` skill (`agent-toolkit` / federation).
12
+
13
+ When a chunk is accepted: apply it to `src/`, `README.md`, and/or `skills/route-orch/SKILL.md` (keep `.cursor/skills/route-orch/SKILL.md` in sync). Then tick **Open** and add an **Edit log** line.
14
+
15
+ ## What this repo is
16
+
17
+ Cursor Agent plans. Workers run in tmux via `cursor-route`:
18
+
19
+ | Lane | Worker | Intent |
20
+ |------|--------|--------|
21
+ | `easy` | OpenRouter free | Wording / drafts — non-secret prompts only |
22
+ | `mid` | claude-ds (DeepSeek behind Claude Code) | Default implement (**Flash**; `--model pro` when needed) |
23
+ | `hard` | Grok CLI | Hard implement |
24
+
25
+ Always-approve on (`--ask` / `CURSOR_ROUTE_ASK=1` to opt out). Jobs live in `~/.local/share/cursor-route/jobs`, not in this clone.
26
+
27
+ Install: `npm i -g cursor-route` → **0.1.7**. Release notes: [CHANGELOG.md](../../CHANGELOG.md).
28
+
29
+ ## Open (edit / check off)
30
+
31
+ - [x] **Flash vs Pro on the CLI** — public default **Flash**; `--model pro` → `deepseek-v4-pro` (LIVE 0.1.6+)
32
+ - [x] pass `claude-ds -Model deepseek-v4-flash|deepseek-v4-pro` from the adapter
33
+ - [x] add `--model flash|pro` on `start`
34
+ - [x] document Grok **auth** ≠ usage-out (`grok login`) vs quota → Pro stand-in
35
+ - [x] **Skill `route-orch`** — Flash/Pro table in `skills/` + `.cursor/skills/`
36
+ - [x] **Official DeepSeek Harness** — `deepseek` adapter slot present; mid stays on claude-ds
37
+ - [x] **0.1.7 debug fixes** — env default wired through `startJob`; Anthropic hatch omits DS ids; preserve `[1m]`
38
+ - [ ] **Hero GIF** — still outstanding (`docs/DEMO_GIF.md`)
39
+ - [x] **Do not** paste private `ROUTE_KIT`, SIP, prod paths, or hang-watchdog env into this public repo
40
+
41
+ ## Repo map
42
+
43
+ | Path | Role |
44
+ |------|------|
45
+ | `src/cli.ts` | `health` / `start` / `jobs` / `capture` / `send` / `kill` |
46
+ | `src/adapters/claude-ds.ts` | Mid worker; DeepSeek URL required; `--model` → `-Model` |
47
+ | `src/adapters/deepseek.ts` | Reserved unreleased harness slot |
48
+ | `src/adapters/grok.ts` | Hard worker |
49
+ | `src/adapters/openrouter.ts` | Easy worker |
50
+ | `skills/route-orch/SKILL.md` | Cursor skill — spawn CLI, do not implement in-session |
51
+ | `CHANGELOG.md` | Release notes |
52
+ | `SECURITY.md` | Secret refuse gate |
53
+
54
+ ## Edit log
55
+
56
+ | Date | Change |
57
+ |------|--------|
58
+ | 2026-08-12 | Brief created in this repo. Flash/Pro CLI + skill still open. |
59
+ | 2026-08-12 | Shipped Flash default + `--model pro`, deepseek slot, skill table → 0.1.6. |
60
+ | 2026-08-12 | npm `cursor-route@0.1.6` LIVE; CHANGELOG + README skill-install path fixed. |
61
+ | 2026-08-12 | Grok debug → 0.1.7: env DS default, Anthropic hatch, `[1m]` preserve. |
@@ -4,10 +4,10 @@ Simulated capture output for README / tweet assets — replace with a real GIF o
4
4
 
5
5
  ```
6
6
  $ cursor-route --version
7
- 0.1.5
7
+ 0.1.6
8
8
 
9
9
  $ cursor-route health
10
- cursor-route v0.1.5
10
+ cursor-route v0.1.6
11
11
  health: OK
12
12
  ✓ tmux
13
13
  ✓ runtime bun ok
@@ -15,19 +15,21 @@ health: OK
15
15
  ✓ worker:grok
16
16
  ✓ worker:claude-ds
17
17
  ✓ worker:openrouter
18
+ ✗ worker:deepseek unreleased — mid lane uses claude-ds
18
19
  ✓ jobs_dir ~/.local/share/cursor-route/jobs
19
20
 
20
21
  $ cursor-route start --lane mid "Add a failing test then make it pass"
21
- started a1b2c3d4 (claude-ds)
22
+ started a1b2c3d4 (claude-ds/flash)
22
23
  session: cursor-route-a1b2c3d4
23
24
  attach: tmux attach -t cursor-route-a1b2c3d4
24
25
 
25
26
  $ cursor-route jobs --json
26
- [{ "id": "a1b2c3d4", "status": "running", "worker": "claude-ds", ... }]
27
+ [{ "id": "a1b2c3d4", "status": "running", "worker": "claude-ds", "model": "flash", ... }]
27
28
  ```
28
29
 
29
30
  Verified locally (2026-08-10): headless `claude-ds` smoke returned `CURSOR_ROUTE_SMOKE_OK`.
30
31
  Grok smoke hit 402 (Build usage balance exhausted) — auth/PATH wiring works; top up Grok Build for live demos.
32
+ Use `--model pro` when Grok **usage** is out (not the same as a missing `grok login`).
31
33
 
32
34
  Current commands: `health`, `start`, `jobs`, `status`, `capture`, `send`, `attach`, `kill`, `sessions`, `clean`.
33
35
  Headless demos (no tmux) use `--no-tmux` and `capture`/`status` instead of `attach`/`send`.
package/llms.txt CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Cursor stays the planner. DeepSeek (mid), Grok CLI (hard), and OpenRouter free models (easy) run parallel coding workers in tmux.
4
4
 
5
- MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route
5
+ MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.7**)
6
6
  GitHub: https://github.com/cemini23/cursor-route
7
7
 
8
8
  ## FAQ
@@ -14,10 +14,16 @@ cursor-route is a public MIT CLI and Cursor skill that runs parallel coding work
14
14
  It uses the familiar strategist and worker-pane shape, but it is not a Codex clone. cursor-route uses Cursor as the planner and DeepSeek plus Grok CLI as workers. Codex is not required.
15
15
 
16
16
  ### Does mid lane use Anthropic Claude?
17
- No. The mid worker is DeepSeek. Claude Code is the harness, configured with `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic` and a DeepSeek key in `ANTHROPIC_AUTH_TOKEN`.
17
+ No. The mid worker is DeepSeek. Claude Code is the harness, configured with `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic` and a DeepSeek key in `ANTHROPIC_AUTH_TOKEN`. Default model is Flash (`--model flash`); use `--model pro` for harder mid work or when Grok usage is exhausted (not the same as a missing `grok login`).
18
+
19
+ ### Flash vs Pro?
20
+ | Flag | Model | When |
21
+ |------|-------|------|
22
+ | `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute |
23
+ | `--model pro` | `deepseek-v4-pro` | Harder mid / Grok usage stand-in |
18
24
 
19
25
  ### How do I install?
20
- 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.
26
+ Run `npm i -g cursor-route`, install tmux if needed, then run `cursor-route health`. Copy the Cursor skill from `$(npm root -g)/cursor-route/skills/route-orch`. Source: https://github.com/cemini23/cursor-route
21
27
 
22
28
  ### Is it free?
23
29
  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`).
@@ -26,4 +32,5 @@ The cursor-route code is open source under MIT. It does not make the worker serv
26
32
 
27
33
  - npm: `npm i -g cursor-route`
28
34
  - Health: `cursor-route health`
35
+ - Mid: `cursor-route start --lane mid "…"` (Flash) · `--model pro` when needed
29
36
  - Skill: `/route-orch` (Cursor)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-route",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter easy lane are the parallel army \u2014 lane-aware /route orchestration in tmux.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,11 +17,13 @@
17
17
  "skills",
18
18
  "docs/DEMO_GIF.md",
19
19
  "docs/demo-notes.md",
20
+ "docs/briefs/WORKING.md",
20
21
  "docs/fixtures",
21
22
  "LICENSE",
22
23
  "SECURITY.md",
23
24
  "SUPPORT.md",
24
25
  "CONTRIBUTING.md",
26
+ "CHANGELOG.md",
25
27
  "README.md",
26
28
  "llms.txt"
27
29
  ],
@@ -30,6 +30,22 @@ You are the **orchestrator**. Do **not** implement bulk code in this Cursor sess
30
30
 
31
31
  Free OpenRouter models may log prompts — keep secrets off the easy lane (the CLI refuse gate still applies).
32
32
 
33
+ ## claude-ds models
34
+
35
+ One harness. Do not install a second coding loop.
36
+
37
+ | Flag / role | Model | When |
38
+ |-------------|-------|------|
39
+ | default mid (`--model flash`) | `deepseek-v4-flash` | `--lane mid` cheap execute |
40
+ | Grok stand-in (`--model pro`) | `deepseek-v4-pro` | Grok CLI **usage/quota** out (not a missing `grok login`) |
41
+
42
+ ```bash
43
+ cursor-route start --lane mid --dir "$PWD" "…"
44
+ cursor-route start --lane mid --model pro --dir "$PWD" "…"
45
+ ```
46
+
47
+ If `worker:grok` is ✗ on health, that is usually **auth** (`grok login` / `XAI_API_KEY`) — not the Pro stand-in case.
48
+
33
49
  ## Workflow
34
50
 
35
51
  1. Run `cursor-route health` (or `CURSOR_ROUTE_RELAXED=1` for headless). If the **target worker** is unhealthy, fix before spawning.
@@ -3,6 +3,12 @@ import { readFileSync, existsSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import type { Adapter, WorkerHealth } from "./types.ts";
6
+ import {
7
+ DS_MODEL_IDS,
8
+ type DsModelAlias,
9
+ config,
10
+ resolveDsModel,
11
+ } from "../config.ts";
6
12
  import { shellQuote } from "../util.ts";
7
13
 
8
14
  /**
@@ -23,6 +29,8 @@ function which(cmd: string): string | null {
23
29
  execSync(`command -v ${shellQuote(cmd)}`, {
24
30
  encoding: "utf8",
25
31
  stdio: ["ignore", "pipe", "ignore"],
32
+ // Bun may ignore mutated process.env.PATH unless env is passed explicitly
33
+ env: { ...process.env },
26
34
  }).trim() || null
27
35
  );
28
36
  } catch {
@@ -59,14 +67,16 @@ function deepseekBaseFromSettings(): string | null {
59
67
 
60
68
  /** Resolved DeepSeek base URL for the mid-lane harness, or null. */
61
69
  export function resolvedDeepSeekBaseUrl(): string | null {
70
+ // Explicit env wins: a non-DeepSeek ANTHROPIC_BASE_URL must not fall through to settings.
62
71
  for (const candidate of [
63
72
  process.env.ANTHROPIC_BASE_URL,
64
73
  process.env.CURSOR_ROUTE_ANTHROPIC_BASE_URL,
65
- deepseekBaseFromSettings(),
66
74
  ]) {
67
- if (candidate && isDeepSeekBaseUrl(candidate)) return candidate;
75
+ if (candidate) {
76
+ return isDeepSeekBaseUrl(candidate) ? candidate : null;
77
+ }
68
78
  }
69
- return null;
79
+ return deepseekBaseFromSettings();
70
80
  }
71
81
 
72
82
  /** True when Claude Code harness is routed to DeepSeek (cheap path). */
@@ -114,22 +124,35 @@ function resolveClaudeDs(): { binary: string; mode: string } | null {
114
124
  return null;
115
125
  }
116
126
 
127
+ function pickModel(requested?: DsModelAlias, modelId?: string): { alias: DsModelAlias; id: string } {
128
+ if (modelId) {
129
+ const alias = requested ?? resolveDsModel(modelId).alias;
130
+ return { alias, id: modelId };
131
+ }
132
+ if (requested) {
133
+ return { alias: requested, id: DS_MODEL_IDS[requested] };
134
+ }
135
+ // Env default (startJob normally resolves this; kept for direct buildLaunch callers)
136
+ return resolveDsModel(process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL);
137
+ }
138
+
117
139
  /**
118
140
  * Env that must reach stock `claude` for DeepSeek routing.
119
141
  * Passed via process/tmux env — never interpolated into the printed command.
120
142
  */
121
- function deepSeekWorkerEnv(): Record<string, string> | undefined {
143
+ function deepSeekWorkerEnv(modelId: string): Record<string, string> | undefined {
122
144
  const base = resolvedDeepSeekBaseUrl();
123
145
  if (!base) return undefined;
124
- const env: Record<string, string> = { ANTHROPIC_BASE_URL: base };
146
+ const env: Record<string, string> = {
147
+ ANTHROPIC_BASE_URL: base,
148
+ ANTHROPIC_MODEL: modelId,
149
+ };
125
150
  const token =
126
151
  process.env.ANTHROPIC_AUTH_TOKEN ||
127
152
  process.env.ANTHROPIC_API_KEY ||
128
153
  process.env.DEEPSEEK_API_KEY ||
129
154
  "";
130
155
  if (token) env.ANTHROPIC_AUTH_TOKEN = token;
131
- const model = process.env.ANTHROPIC_MODEL;
132
- if (model) env.ANTHROPIC_MODEL = model;
133
156
  return env;
134
157
  }
135
158
 
@@ -153,10 +176,10 @@ export const claudeDsAdapter: Adapter = {
153
176
  worker: "claude-ds",
154
177
  ok: true,
155
178
  binary: resolved.binary,
156
- detail: `ok (${resolved.mode})`,
179
+ detail: `ok (${resolved.mode}; default model ${DS_MODEL_IDS[config.defaultDsModel]})`,
157
180
  };
158
181
  },
159
- buildLaunch({ promptFile, cwd, alwaysApprove }) {
182
+ buildLaunch({ promptFile, cwd, alwaysApprove, model, modelId }) {
160
183
  const resolved = resolveClaudeDs();
161
184
  if (!resolved) {
162
185
  throw new Error("DeepSeek worker not available — run: cursor-route health");
@@ -164,16 +187,37 @@ export const claudeDsAdapter: Adapter = {
164
187
 
165
188
  const ask = process.env.CURSOR_ROUTE_ASK === "1" || process.env.CLAUDE_DS_ASK === "1";
166
189
  const skip = alwaysApprove && !ask;
167
- // Stock `claude` needs DeepSeek env injected into the worker process
168
- // (tmux panes may not inherit client env from a long-lived server).
169
- const env =
170
- resolved.mode.startsWith("claude → DeepSeek") ? deepSeekWorkerEnv() : undefined;
190
+ const isAnthropicEscape = resolved.mode.startsWith("claude Anthropic");
191
+ const isDeepSeekStock = resolved.mode.startsWith("claude DeepSeek");
192
+ const isShim =
193
+ resolved.mode.startsWith("claude-ds") || resolved.mode.startsWith("deepseek-claude");
194
+
195
+ // Anthropic escape hatch: do not pass DeepSeek model ids (unknown to Anthropic).
196
+ // --model flash|pro is DeepSeek-only; stock Claude uses its own defaults / ANTHROPIC_MODEL.
197
+ if (isAnthropicEscape) {
198
+ const parts = [
199
+ shellQuote(resolved.binary),
200
+ "-p",
201
+ `"$(cat ${shellQuote(promptFile)})"`,
202
+ ];
203
+ if (skip) parts.push("--dangerously-skip-permissions");
204
+ return {
205
+ worker: "claude-ds",
206
+ command: `cd ${shellQuote(cwd)} && ${parts.join(" ")}`,
207
+ alwaysApprove: skip,
208
+ };
209
+ }
210
+
211
+ const choice = pickModel(model, modelId);
212
+ const env = isDeepSeekStock ? deepSeekWorkerEnv(choice.id) : undefined;
171
213
 
172
- if (resolved.mode.startsWith("claude-ds")) {
214
+ if (isShim) {
173
215
  const parts = [
174
216
  shellQuote(resolved.binary),
175
217
  "-PromptFile",
176
218
  shellQuote(promptFile),
219
+ "-Model",
220
+ shellQuote(choice.id),
177
221
  ];
178
222
  if (skip) parts.push("--dangerously-skip-permissions");
179
223
  return {
@@ -184,10 +228,13 @@ export const claudeDsAdapter: Adapter = {
184
228
  };
185
229
  }
186
230
 
231
+ // Stock claude → DeepSeek
187
232
  const parts = [
188
233
  shellQuote(resolved.binary),
189
234
  "-p",
190
235
  `"$(cat ${shellQuote(promptFile)})"`,
236
+ "--model",
237
+ shellQuote(choice.id),
191
238
  ];
192
239
  if (skip) parts.push("--dangerously-skip-permissions");
193
240
  return {
@@ -0,0 +1,24 @@
1
+ import type { Adapter, WorkerHealth } from "./types.ts";
2
+
3
+ /**
4
+ * Reserved slot for the official DeepSeek coding harness when it ships.
5
+ * Mid lane stays on claude-ds until then — do not route jobs here.
6
+ */
7
+ export const deepseekAdapter: Adapter = {
8
+ kind: "deepseek",
9
+ label: "Official DeepSeek harness (unreleased)",
10
+ health(): WorkerHealth {
11
+ return {
12
+ worker: "deepseek",
13
+ ok: false,
14
+ binary: null,
15
+ detail:
16
+ "unreleased — mid lane uses claude-ds (DeepSeek behind Claude Code). See README.",
17
+ };
18
+ },
19
+ buildLaunch() {
20
+ throw new Error(
21
+ "Official DeepSeek harness is not available yet — use --lane mid / --worker claude-ds",
22
+ );
23
+ },
24
+ };
@@ -3,11 +3,13 @@ import type { Adapter } from "./types.ts";
3
3
  import { grokAdapter } from "./grok.ts";
4
4
  import { claudeDsAdapter } from "./claude-ds.ts";
5
5
  import { openRouterAdapter } from "./openrouter.ts";
6
+ import { deepseekAdapter } from "./deepseek.ts";
6
7
 
7
8
  const registry: Record<WorkerKind, Adapter> = {
8
9
  grok: grokAdapter,
9
10
  "claude-ds": claudeDsAdapter,
10
11
  openrouter: openRouterAdapter,
12
+ deepseek: deepseekAdapter,
11
13
  };
12
14
 
13
15
  export function getAdapter(worker: WorkerKind): Adapter {
@@ -1,4 +1,4 @@
1
- import type { WorkerKind } from "../config.ts";
1
+ import type { WorkerKind, DsModelAlias } from "../config.ts";
2
2
 
3
3
  export interface WorkerHealth {
4
4
  worker: WorkerKind;
@@ -24,5 +24,9 @@ export interface Adapter {
24
24
  promptFile: string;
25
25
  cwd: string;
26
26
  alwaysApprove: boolean;
27
+ /** Mid-lane DeepSeek flash|pro (ignored by other workers / Anthropic escape hatch). */
28
+ model?: DsModelAlias;
29
+ /** Concrete DeepSeek model id for -Model/--model (preserves pro[1m]). */
30
+ modelId?: string;
27
31
  }): LaunchPlan;
28
32
  }
package/src/cli.test.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { resolveWorker } from "./jobs.ts";
3
- import { config } from "./config.ts";
2
+ import { mkdirSync, writeFileSync, chmodSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { resolveWorker, startJob } from "./jobs.ts";
6
+ import { config, resolveDsModel, resolveDsModelAlias } from "./config.ts";
4
7
  import { shellQuote, newJobId } from "./util.ts";
5
8
  import { runHealth } from "./health.ts";
6
9
  import { looksLikeSecretMaterial, redactSecrets } from "./secrets.ts";
7
- import { isDeepSeekRouted, isDeepSeekBaseUrl } from "./adapters/claude-ds.ts";
10
+ import { isDeepSeekRouted, isDeepSeekBaseUrl, claudeDsAdapter } from "./adapters/claude-ds.ts";
11
+ import { deepseekAdapter } from "./adapters/deepseek.ts";
12
+ import { grokAdapter } from "./adapters/grok.ts";
8
13
 
9
14
  describe("resolveWorker", () => {
10
15
  test("lane easy → openrouter", () => {
@@ -29,6 +34,205 @@ describe("resolveWorker", () => {
29
34
  });
30
35
  });
31
36
 
37
+ describe("resolveDsModel", () => {
38
+ test("defaults to flash", () => {
39
+ expect(resolveDsModel()).toEqual({ alias: "flash", id: "deepseek-v4-flash" });
40
+ expect(resolveDsModelAlias("")).toBe("flash");
41
+ });
42
+ test("accepts aliases and full ids", () => {
43
+ expect(resolveDsModel("flash").id).toBe("deepseek-v4-flash");
44
+ expect(resolveDsModel("pro").id).toBe("deepseek-v4-pro");
45
+ expect(resolveDsModel("deepseek-v4-pro[1m]")).toEqual({
46
+ alias: "pro",
47
+ id: "deepseek-v4-pro[1m]",
48
+ });
49
+ });
50
+ test("rejects unknown", () => {
51
+ expect(() => resolveDsModel("opus")).toThrow(/flash\|pro/);
52
+ });
53
+ });
54
+
55
+ describe("claude-ds -Model", () => {
56
+ test("shim path passes -Model deepseek-v4-flash by default", () => {
57
+ const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
58
+ const prevDs = process.env.CURSOR_ROUTE_DS_MODEL;
59
+ const prevAm = process.env.ANTHROPIC_MODEL;
60
+ process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
61
+ delete process.env.CURSOR_ROUTE_DS_MODEL;
62
+ delete process.env.ANTHROPIC_MODEL;
63
+ try {
64
+ const plan = claudeDsAdapter.buildLaunch({
65
+ promptFile: "/tmp/p.prompt",
66
+ cwd: "/tmp",
67
+ alwaysApprove: true,
68
+ });
69
+ expect(plan.command).toContain("-Model");
70
+ expect(plan.command).toContain("deepseek-v4-flash");
71
+ } finally {
72
+ if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
73
+ else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
74
+ if (prevDs === undefined) delete process.env.CURSOR_ROUTE_DS_MODEL;
75
+ else process.env.CURSOR_ROUTE_DS_MODEL = prevDs;
76
+ if (prevAm === undefined) delete process.env.ANTHROPIC_MODEL;
77
+ else process.env.ANTHROPIC_MODEL = prevAm;
78
+ }
79
+ });
80
+ test("pro alias maps to deepseek-v4-pro", () => {
81
+ const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
82
+ process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
83
+ try {
84
+ const plan = claudeDsAdapter.buildLaunch({
85
+ promptFile: "/tmp/p.prompt",
86
+ cwd: "/tmp",
87
+ alwaysApprove: true,
88
+ model: "pro",
89
+ });
90
+ expect(plan.command).toContain("deepseek-v4-pro");
91
+ expect(plan.command).not.toContain("deepseek-v4-flash");
92
+ } finally {
93
+ if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
94
+ else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
95
+ }
96
+ });
97
+ test("preserves deepseek-v4-pro[1m] via modelId", () => {
98
+ const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
99
+ process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
100
+ try {
101
+ const plan = claudeDsAdapter.buildLaunch({
102
+ promptFile: "/tmp/p.prompt",
103
+ cwd: "/tmp",
104
+ alwaysApprove: true,
105
+ model: "pro",
106
+ modelId: "deepseek-v4-pro[1m]",
107
+ });
108
+ expect(plan.command).toContain("deepseek-v4-pro[1m]");
109
+ } finally {
110
+ if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
111
+ else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
112
+ }
113
+ });
114
+ test("Anthropic escape hatch omits DeepSeek --model", () => {
115
+ const shimDir = join(tmpdir(), `cr-anth-escape-${process.pid}`);
116
+ mkdirSync(shimDir, { recursive: true });
117
+ const claude = join(shimDir, "claude");
118
+ writeFileSync(claude, "#!/bin/sh\necho ok\n");
119
+ chmodSync(claude, 0o755);
120
+
121
+ const prev = {
122
+ bin: process.env.CURSOR_ROUTE_CLAUDE_DS_BIN,
123
+ allow: process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC,
124
+ base: process.env.ANTHROPIC_BASE_URL,
125
+ path: process.env.PATH,
126
+ };
127
+ delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
128
+ // Explicit non-DeepSeek base so ~/.claude/settings.json cannot force DeepSeek routing
129
+ process.env.ANTHROPIC_BASE_URL = "https://api.anthropic.com";
130
+ process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC = "1";
131
+ process.env.PATH = `${shimDir}:/usr/bin:/bin`;
132
+ try {
133
+ const plan = claudeDsAdapter.buildLaunch({
134
+ promptFile: "/tmp/p.prompt",
135
+ cwd: "/tmp",
136
+ alwaysApprove: true,
137
+ model: "pro",
138
+ });
139
+ expect(plan.command).toContain(claude);
140
+ expect(plan.command).not.toContain("deepseek-v4");
141
+ expect(plan.command).not.toContain("--model");
142
+ } finally {
143
+ if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
144
+ else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev.bin;
145
+ if (prev.allow === undefined) delete process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC;
146
+ else process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC = prev.allow;
147
+ if (prev.base === undefined) delete process.env.ANTHROPIC_BASE_URL;
148
+ else process.env.ANTHROPIC_BASE_URL = prev.base;
149
+ process.env.PATH = prev.path || "";
150
+ rmSync(shimDir, { recursive: true, force: true });
151
+ }
152
+ });
153
+ });
154
+
155
+ describe("startJob product path", () => {
156
+ test("CURSOR_ROUTE_DS_MODEL=pro applies when --model unset", () => {
157
+ const prev = {
158
+ bin: process.env.CURSOR_ROUTE_CLAUDE_DS_BIN,
159
+ ds: process.env.CURSOR_ROUTE_DS_MODEL,
160
+ am: process.env.ANTHROPIC_MODEL,
161
+ jobs: process.env.CURSOR_ROUTE_JOBS_DIR,
162
+ };
163
+ const jobsDir = join(tmpdir(), `cr-jobs-ds-${process.pid}`);
164
+ mkdirSync(jobsDir, { recursive: true });
165
+ process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
166
+ process.env.CURSOR_ROUTE_DS_MODEL = "pro";
167
+ delete process.env.ANTHROPIC_MODEL;
168
+ process.env.CURSOR_ROUTE_JOBS_DIR = jobsDir;
169
+ try {
170
+ const result = startJob({
171
+ prompt: "ping",
172
+ lane: "mid",
173
+ dryRun: true,
174
+ });
175
+ expect(result.ok).toBe(true);
176
+ if (!result.ok) return;
177
+ expect(result.job.model).toBe("pro");
178
+ expect(result.command).toContain("deepseek-v4-pro");
179
+ expect(result.command).not.toContain("deepseek-v4-flash");
180
+ } finally {
181
+ if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
182
+ else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev.bin;
183
+ if (prev.ds === undefined) delete process.env.CURSOR_ROUTE_DS_MODEL;
184
+ else process.env.CURSOR_ROUTE_DS_MODEL = prev.ds;
185
+ if (prev.am === undefined) delete process.env.ANTHROPIC_MODEL;
186
+ else process.env.ANTHROPIC_MODEL = prev.am;
187
+ if (prev.jobs === undefined) delete process.env.CURSOR_ROUTE_JOBS_DIR;
188
+ else process.env.CURSOR_ROUTE_JOBS_DIR = prev.jobs;
189
+ rmSync(jobsDir, { recursive: true, force: true });
190
+ }
191
+ });
192
+
193
+ test("--worker deepseek dry-run fails", () => {
194
+ const result = startJob({
195
+ prompt: "ping",
196
+ worker: "deepseek",
197
+ dryRun: true,
198
+ });
199
+ expect(result.ok).toBe(false);
200
+ if (result.ok) return;
201
+ expect(result.error).toMatch(/not available|claude-ds/i);
202
+ });
203
+
204
+ test("grok command never includes deepseek-v4", () => {
205
+ const prev = process.env.CURSOR_ROUTE_GROK_BIN;
206
+ process.env.CURSOR_ROUTE_GROK_BIN = "/tmp/fake-grok";
207
+ try {
208
+ const plan = grokAdapter.buildLaunch({
209
+ promptFile: "/tmp/p.prompt",
210
+ cwd: "/tmp",
211
+ alwaysApprove: true,
212
+ model: "pro",
213
+ });
214
+ expect(plan.command).not.toContain("deepseek-v4");
215
+ expect(plan.command).not.toMatch(/(^|[\s'-])Model\b/);
216
+ } finally {
217
+ if (prev === undefined) delete process.env.CURSOR_ROUTE_GROK_BIN;
218
+ else process.env.CURSOR_ROUTE_GROK_BIN = prev;
219
+ }
220
+ });
221
+ });
222
+
223
+ describe("deepseek adapter slot", () => {
224
+ test("health is not ok and buildLaunch throws", () => {
225
+ expect(deepseekAdapter.health().ok).toBe(false);
226
+ expect(() =>
227
+ deepseekAdapter.buildLaunch({
228
+ promptFile: "/tmp/p",
229
+ cwd: "/tmp",
230
+ alwaysApprove: true,
231
+ }),
232
+ ).toThrow(/not available/);
233
+ });
234
+ });
235
+
32
236
  describe("util", () => {
33
237
  test("shellQuote", () => {
34
238
  expect(shellQuote("a b")).toBe("'a b'");
@@ -86,11 +290,26 @@ describe("health", () => {
86
290
  test("returns structured report", () => {
87
291
  const r = runHealth();
88
292
  expect(r.product).toBe("cursor-route");
293
+ expect(r.version).toBe("0.1.7");
89
294
  expect(r.checks.length).toBeGreaterThan(3);
90
295
  expect(r.checks.some((c) => c.name === "tmux")).toBe(true);
91
296
  expect(r.checks.some((c) => c.name === "cursor_cli")).toBe(true);
92
297
  });
93
298
 
299
+ test("OR-gate: ok can be true while worker:deepseek is false", () => {
300
+ const prev = process.env.CURSOR_ROUTE_RELAXED;
301
+ process.env.CURSOR_ROUTE_RELAXED = "1";
302
+ try {
303
+ const r = runHealth();
304
+ const ds = r.checks.find((c) => c.name === "worker:deepseek");
305
+ expect(ds?.ok).toBe(false);
306
+ expect(r.ok).toBe(true);
307
+ } finally {
308
+ if (prev === undefined) delete process.env.CURSOR_ROUTE_RELAXED;
309
+ else process.env.CURSOR_ROUTE_RELAXED = prev;
310
+ }
311
+ });
312
+
94
313
  test("relaxed env can pass without workers", () => {
95
314
  const prev = process.env.CURSOR_ROUTE_RELAXED;
96
315
  process.env.CURSOR_ROUTE_RELAXED = "1";