cursor-route 0.1.6 → 0.1.8

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/dist/config.js CHANGED
@@ -8,17 +8,39 @@ export const DS_MODEL_IDS = {
8
8
  flash: "deepseek-v4-flash",
9
9
  pro: "deepseek-v4-pro",
10
10
  };
11
- /** Resolve --model flash|pro (or full deepseek-v4-* id) to a CLI alias. Default: flash. */
12
- export function resolveDsModelAlias(raw) {
13
- if (!raw || !raw.trim())
14
- return "flash";
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
+ }
15
20
  const v = raw.trim().toLowerCase();
16
- if (v === "flash" || v === "deepseek-v4-flash")
17
- return "flash";
18
- if (v === "pro" || v === "deepseek-v4-pro" || v === "deepseek-v4-pro[1m]")
19
- return "pro";
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
+ }
20
30
  throw new Error(`Invalid --model ${raw}; expected flash|pro`);
21
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
+ }
22
44
  /** OpenRouter model for the easy lane (env CURSOR_ROUTE_OPENROUTER_MODEL). */
23
45
  export function openRouterModel() {
24
46
  return process.env.CURSOR_ROUTE_OPENROUTER_MODEL || "openrouter/free";
@@ -43,20 +65,34 @@ function maxConcurrentJobsFromEnv() {
43
65
  */
44
66
  export const config = {
45
67
  product: "cursor-route",
46
- version: "0.1.6",
68
+ version: "0.1.8",
47
69
  get jobsDir() {
48
70
  return defaultJobsDir();
49
71
  },
50
72
  tmuxPrefix: "cursor-route",
51
73
  defaultWorker: "grok",
52
- /** Lane → default worker (Cemini /route public core). */
74
+ /**
75
+ * Lane → default worker (Cemini /route public core).
76
+ * `deepseek` is experimental only — mid stays on claude-ds.
77
+ */
53
78
  laneWorkers: {
54
79
  easy: "openrouter",
55
80
  mid: "claude-ds",
56
81
  hard: "grok",
57
82
  },
58
- /** Default mid DeepSeek model (Flash = cheap execute). Override: --model pro */
59
- defaultDsModel: "flash",
83
+ /**
84
+ * Default mid DeepSeek model (Flash = cheap execute).
85
+ * Live: CURSOR_ROUTE_DS_MODEL / ANTHROPIC_MODEL; invalid env → flash (health-safe).
86
+ * Override on start: --model. startJob uses defaultDsModelFromEnv() and fails on invalid env.
87
+ */
88
+ get defaultDsModel() {
89
+ try {
90
+ return defaultDsModelFromEnv().alias;
91
+ }
92
+ catch {
93
+ return "flash";
94
+ }
95
+ },
60
96
  jobsListLimit: 20,
61
97
  /** Max simultaneously active (running|pending) jobs. Override: CURSOR_ROUTE_MAX_JOBS. */
62
98
  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,7 +217,30 @@ export function startJob(opts) {
217
217
  const id = newJobId();
218
218
  const paths = jobPaths(id);
219
219
  writeSecure(paths.prompt, opts.prompt);
220
- const model = worker === "claude-ds" ? opts.model ?? config.defaultDsModel : undefined;
220
+ let model;
221
+ let modelId;
222
+ if (worker === "claude-ds" || worker === "deepseek") {
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
+ }
221
244
  let plan;
222
245
  try {
223
246
  plan = adapter.buildLaunch({
@@ -225,6 +248,8 @@ export function startJob(opts) {
225
248
  cwd,
226
249
  alwaysApprove,
227
250
  model,
251
+ modelId,
252
+ dryRun: Boolean(opts.dryRun),
228
253
  });
229
254
  }
230
255
  catch (e) {
@@ -397,7 +422,7 @@ export function cleanJobs(olderThanDays = 7) {
397
422
  if (Number.isFinite(t) &&
398
423
  t < cutoff &&
399
424
  (job.status === "completed" || job.status === "failed" || job.status === "killed")) {
400
- for (const ext of [".json", ".prompt", ".log"]) {
425
+ for (const ext of [".json", ".prompt", ".log", ".dsh-patch.yml"]) {
401
426
  const fp = underJobsDir(id, ext);
402
427
  if (existsSync(fp))
403
428
  unlinkSync(fp);
package/docs/DEMO_GIF.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Hero demo recording (GIF)
2
2
 
3
+ ## Status
4
+
5
+ No real GIF yet — the checkbox stays open until a `docs/fixtures/hero.gif` exists.
6
+
7
+ Committed fixture for now: [docs/fixtures/hero-demo.log](./fixtures/hero-demo.log),
8
+ a reproducible dry-run capture (no live workers, no secrets). Regenerate it with:
9
+
10
+ ```bash
11
+ docs/fixtures/generate-hero-demo.sh
12
+ ```
13
+
14
+ Also see [docs/fixtures/claude-ds-smoke.log](./fixtures/claude-ds-smoke.log)
15
+ (headless `claude-ds` smoke proof). Both are machine-agnostic.
16
+
17
+ ## Recording the real GIF (later)
18
+
3
19
  tmux is required for the viral attach/send demo. On this laptop brew needs sudo — record after:
4
20
 
5
21
  ```bash
@@ -13,7 +29,8 @@ cursor-route health
13
29
 
14
30
  # Terminal A — three parallel jobs
15
31
  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"
32
+ cursor-route start --lane mid --dir "$PWD" "Add a unit test for shellQuote" # Flash default
33
+ # Optional: cursor-route start --lane mid --model pro --dir "$PWD" "…"
17
34
  cursor-route start --worker grok --dir "$PWD" "List open TODOs in src/"
18
35
 
19
36
  # Terminal B — watch
@@ -26,4 +43,4 @@ If you previously exported `$HOME/.cursor-route/bin`, that dir is stale — remo
26
43
 
27
44
  Record with [asciinema](https://asciinema.org/) or CleanShot → export GIF → `docs/fixtures/hero.gif`.
28
45
 
29
- Until then, use `docs/fixtures/claude-ds-smoke.log` as the committed proof fixture.
46
+ When a real GIF lands, check off the **Hero GIF** item in `docs/briefs/WORKING.md` and reference it here.
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  title: cursor-route workspace — working brief (edit in place)
3
3
  repo: ~/Projects/cursor-route
4
- npm: cursor-route@0.1.6
4
+ npm: cursor-route@0.1.8 (LIVE on npm latest)
5
5
  created: 2026-08-12
6
- updated: 2026-08-12
6
+ updated: 2026-08-14
7
7
  ---
8
8
 
9
9
  # cursor-route — living brief
@@ -19,20 +19,24 @@ Cursor Agent plans. Workers run in tmux via `cursor-route`:
19
19
  | Lane | Worker | Intent |
20
20
  |------|--------|--------|
21
21
  | `easy` | OpenRouter free | Wording / drafts — non-secret prompts only |
22
- | `mid` | claude-ds (DeepSeek behind Claude Code) | Default implement (**Flash**) |
22
+ | `mid` | claude-ds (DeepSeek behind Claude Code) | Default implement (**Flash**; `--model pro` when needed) |
23
23
  | `hard` | Grok CLI | Hard implement |
24
24
 
25
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
26
 
27
+ Install: `npm i -g cursor-route` → **0.1.8**. Release notes: [CHANGELOG.md](../../CHANGELOG.md).
28
+
27
29
  ## Open (edit / check off)
28
30
 
29
- - [x] **Flash vs Pro on the CLI** — public default **Flash**; `--model pro` → `deepseek-v4-pro`
31
+ - [x] **Flash vs Pro on the CLI** — public default **Flash**; `--model pro` → `deepseek-v4-pro` (LIVE 0.1.6+)
30
32
  - [x] pass `claude-ds -Model deepseek-v4-flash|deepseek-v4-pro` from the adapter
31
33
  - [x] add `--model flash|pro` on `start`
32
34
  - [x] document Grok **auth** ≠ usage-out (`grok login`) vs quota → Pro stand-in
33
35
  - [x] **Skill `route-orch`** — Flash/Pro table in `skills/` + `.cursor/skills/`
34
- - [x] **Official DeepSeek Harness** — `deepseek` adapter slot present; mid stays on claude-ds
35
- - [ ] **Hero GIF** — still outstanding (`docs/DEMO_GIF.md`)
36
+ - [x] **Official DeepSeek Harness** — `deepseek` adapter slot present; `@deepseek-ai/dsh` 0.1.0-rc.6 (2026-08-13, github.com/deepseek-ai/deepseek-harness, MIT) is a developer-preview plugin kernel (web UI + `dsh --profile headless "job"`), not a Claude Code replacement; mid does not swap
37
+ - [x] **0.1.7 debug fixes** — env default wired through `startJob`; Anthropic hatch omits DS ids; preserve `[1m]`
38
+ - [x] **Experimental `--worker deepseek`** — 0.1.8: real dsh adapter (`dsh --profile headless` + per-job Cordis patch pins the model; never writes `~/.dsh/settings.yaml`). Always-approve → `DSH_PERMISSION_MODE=danger-full-access`, `--ask` → `workspace-write`; key via env only. Health ✓ needs `dsh` + `DEEPSEEK_API_KEY` (override `CURSOR_ROUTE_DSH_BIN`). Mid stays **claude-ds**.
39
+ - [ ] **Hero GIF** — still outstanding; dry-run fixture ships as the substitute for now (`docs/fixtures/hero-demo.log` — see `docs/DEMO_GIF.md`)
36
40
  - [x] **Do not** paste private `ROUTE_KIT`, SIP, prod paths, or hang-watchdog env into this public repo
37
41
 
38
42
  ## Repo map
@@ -41,10 +45,11 @@ Always-approve on (`--ask` / `CURSOR_ROUTE_ASK=1` to opt out). Jobs live in `~/.
41
45
  |------|------|
42
46
  | `src/cli.ts` | `health` / `start` / `jobs` / `capture` / `send` / `kill` |
43
47
  | `src/adapters/claude-ds.ts` | Mid worker; DeepSeek URL required; `--model` → `-Model` |
44
- | `src/adapters/deepseek.ts` | Reserved unreleased harness slot |
48
+ | `src/adapters/deepseek.ts` | Experimental dsh worker (`--worker deepseek`; mid stays claude-ds) |
45
49
  | `src/adapters/grok.ts` | Hard worker |
46
50
  | `src/adapters/openrouter.ts` | Easy worker |
47
51
  | `skills/route-orch/SKILL.md` | Cursor skill — spawn CLI, do not implement in-session |
52
+ | `CHANGELOG.md` | Release notes |
48
53
  | `SECURITY.md` | Secret refuse gate |
49
54
 
50
55
  ## Edit log
@@ -53,3 +58,9 @@ Always-approve on (`--ask` / `CURSOR_ROUTE_ASK=1` to opt out). Jobs live in `~/.
53
58
  |------|--------|
54
59
  | 2026-08-12 | Brief created in this repo. Flash/Pro CLI + skill still open. |
55
60
  | 2026-08-12 | Shipped Flash default + `--model pro`, deepseek slot, skill table → 0.1.6. |
61
+ | 2026-08-12 | npm `cursor-route@0.1.6` LIVE; CHANGELOG + README skill-install path fixed. |
62
+ | 2026-08-12 | Grok debug → 0.1.7: env DS default, Anthropic hatch, `[1m]` preserve. |
63
+ | 2026-08-13 | Hero demo dry-run fixture + docs: `docs/fixtures/generate-hero-demo.sh` → `hero-demo.log`; README Demo, DEMO_GIF.md, demo-notes pointer. Real GIF still open. |
64
+ | 2026-08-13 | GPTSOL fixes: scrub inherited env/paths, stable job ids, ignore `docs/briefs/handoffs/`, GIF checkbox wording. |
65
+ | 2026-08-14 | DeepSeek Harness eval: `@deepseek-ai/dsh` 0.1.0-rc.6 is a developer-preview plugin kernel, not a mid replacement; `--worker deepseek` stays unhealthy; mid remains claude-ds (docs-only, no version bump). |
66
+ | 2026-08-14 | Experimental `--worker deepseek` wired to official dsh (headless + per-job patch + `DSH_PERMISSION_MODE` + key-via-env); `--model` applies to claude-ds + deepseek; mid stays claude-ds → 0.1.8 LIVE. |
@@ -1,13 +1,15 @@
1
1
  # Demo fixture log (no secrets)
2
2
 
3
+ > **Current fixture:** [docs/fixtures/hero-demo.log](./fixtures/hero-demo.log) — generated, dry-run, machine-agnostic (regenerate: `docs/fixtures/generate-hero-demo.sh`). Real GIF still pending: [DEMO_GIF.md](./DEMO_GIF.md).
4
+
3
5
  Simulated capture output for README / tweet assets — replace with a real GIF once tmux is available.
4
6
 
5
7
  ```
6
8
  $ cursor-route --version
7
- 0.1.6
9
+ 0.1.8
8
10
 
9
11
  $ cursor-route health
10
- cursor-route v0.1.6
12
+ cursor-route v0.1.8
11
13
  health: OK
12
14
  ✓ tmux
13
15
  ✓ runtime bun ok
@@ -15,7 +17,7 @@ health: OK
15
17
  ✓ worker:grok
16
18
  ✓ worker:claude-ds
17
19
  ✓ worker:openrouter
18
- ✗ worker:deepseek unreleased — mid lane uses claude-ds
20
+ ✗ worker:deepseek dsh not found — mid default remains claude-ds
19
21
  ✓ jobs_dir ~/.local/share/cursor-route/jobs
20
22
 
21
23
  $ cursor-route start --lane mid "Add a failing test then make it pass"
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env bash
2
+ # generate-hero-demo.sh — regenerate docs/fixtures/hero-demo.log
3
+ #
4
+ # Dry-run only: no live Grok, no worker spawned, no secrets. Jobs dir is
5
+ # sandboxed (trap-cleaned). Output is path-scrubbed + id-normalized so the
6
+ # committed fixture is machine-agnostic and regenerates stably.
7
+ #
8
+ # Prerequisites: Bun or Node 20+; runnable ./bin/cursor-route
9
+ # (git clone: bun install; npm global install ships dist/).
10
+ set -euo pipefail
11
+
12
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
13
+ BIN="$ROOT/bin/cursor-route"
14
+ OUT="$ROOT/docs/fixtures/hero-demo.log"
15
+
16
+ if [[ ! -f "$BIN" ]]; then
17
+ echo "missing $BIN — run from a cursor-route checkout (need bun/node 20+)" >&2
18
+ exit 1
19
+ fi
20
+
21
+ # Neutralize inherited overrides that would leak private paths into the log.
22
+ unset CURSOR_ROUTE_CLAUDE_DS_BIN CURSOR_ROUTE_GROK_BIN CURSOR_ROUTE_DSH_BIN \
23
+ CURSOR_ROUTE_ALLOW_ANTHROPIC \
24
+ CURSOR_ROUTE_ALLOW_STOCK_CLAUDE CURSOR_ROUTE_ANTHROPIC_BASE_URL CURSOR_ROUTE_DS_MODEL \
25
+ CURSOR_ROUTE_OPENROUTER_MODEL OPENROUTER_API_KEY OPENROUTER_BASE_URL \
26
+ ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN ANTHROPIC_API_KEY ANTHROPIC_MODEL \
27
+ DEEPSEEK_API_KEY XAI_API_KEY 2>/dev/null || true
28
+
29
+ TMP="$(mktemp -d)"
30
+ trap 'rm -rf "$TMP"' EXIT
31
+ export CURSOR_ROUTE_JOBS_DIR="$TMP/.demo-jobs"
32
+ # Pin dsh to a missing path so worker:deepseek renders identically on machines
33
+ # that do have a real dsh installed (the adapter shows the install hint, no path).
34
+ export CURSOR_ROUTE_DSH_BIN="$TMP/no-such-dsh"
35
+
36
+ RAW="$TMP/hero-demo.raw"
37
+ NORM="$TMP/hero-demo.norm"
38
+
39
+ {
40
+ echo "\$ cursor-route --version"
41
+ "$BIN" --version
42
+ echo
43
+
44
+ echo "\$ CURSOR_ROUTE_RELAXED=1 cursor-route health"
45
+ CURSOR_ROUTE_RELAXED=1 "$BIN" health
46
+ echo
47
+
48
+ echo "\$ cursor-route start --lane mid --model flash --dry-run \"Add a unit test for shellQuote\""
49
+ "$BIN" start --lane mid --model flash --dry-run "Add a unit test for shellQuote"
50
+ echo
51
+
52
+ echo "\$ cursor-route start --lane easy --dry-run \"Rewrite this FAQ answer in 3 sentences\""
53
+ "$BIN" start --lane easy --dry-run "Rewrite this FAQ answer in 3 sentences"
54
+ echo
55
+
56
+ echo "\$ cursor-route start --lane hard --dry-run \"Refactor auth module; run tests; report verify evidence\""
57
+ "$BIN" start --lane hard --dry-run "Refactor auth module; run tests; report verify evidence"
58
+ echo
59
+
60
+ echo "\$ cursor-route start --lane mid --dry-run --json \"Add a failing test then make it pass\""
61
+ "$BIN" start --lane mid --dry-run --json "Add a failing test then make it pass"
62
+ echo
63
+
64
+ echo "\$ cursor-route jobs --json"
65
+ "$BIN" jobs --json
66
+ } > "$RAW"
67
+
68
+ jobs_display="$HOME/.local/share/cursor-route/jobs"
69
+ {
70
+ while IFS= read -r line || [[ -n "$line" ]]; do
71
+ line="${line//$TMP\/.demo-jobs/$jobs_display}"
72
+ line="${line//$ROOT/~/Projects/cursor-route}"
73
+ line="${line//$HOME/~}"
74
+ line="$(printf '%s' "$line" | sed -E \
75
+ -e 's#/Users/[^/\"'\'' ]+#~#g' \
76
+ -e 's#/home/[^/\"'\'' ]+#~#g' \
77
+ -e 's#/opt/cemini[^\"'\'' ]*#~#g' \
78
+ -e 's#/var/folders/[^\"'\'' ]+#/tmp#g')"
79
+ printf '%s\n' "$line"
80
+ done
81
+ } < "$RAW" > "$NORM"
82
+
83
+ python3 - "$NORM" "$OUT" <<'PY'
84
+ import re, sys
85
+ src, dst = sys.argv[1], sys.argv[2]
86
+ text = open(src, encoding="utf-8").read()
87
+ seq = ["a1b2c3d4", "b2c3d4e5", "c3d4e5f6", "d4e5f6a7"]
88
+ seen: dict[str, str] = {}
89
+
90
+ def take(jid: str) -> str:
91
+ if jid not in seen:
92
+ seen[jid] = seq[len(seen)] if len(seen) < len(seq) else f"{len(seen):08x}"
93
+ return seen[jid]
94
+
95
+ out = []
96
+ for line in text.splitlines(True):
97
+ def sub(m: re.Match[str]) -> str:
98
+ return m.group(1) + take(m.group(2)) + m.group(3)
99
+
100
+ line = re.sub(r"(dry-run job )([0-9a-f]{8})(\b)", sub, line)
101
+ line = re.sub(r'("id": ")([0-9a-f]{8})(")', sub, line)
102
+ line = re.sub(r"(jobs/)([0-9a-f]{8})(\.prompt)", sub, line)
103
+ line = re.sub(r"(headless-|cursor-route-)([0-9a-f]{8})(\b)", sub, line)
104
+ line = re.sub(r'("createdAt": ")[^"]+(")', r"\g<1>2026-08-13T00:00:00.000Z\2", line)
105
+ out.append(line)
106
+ open(dst, "w", encoding="utf-8").write("".join(out))
107
+ PY
108
+
109
+ echo "wrote $OUT"
@@ -0,0 +1,52 @@
1
+ $ cursor-route --version
2
+ 0.1.8
3
+
4
+ $ CURSOR_ROUTE_RELAXED=1 cursor-route health
5
+ cursor-route v0.1.8
6
+ health: OK
7
+
8
+ ✓ tmux ok
9
+ ✓ runtime bun ok
10
+ ✓ script(1) ok (tty log capture)
11
+ ✓ worker:grok ok (auth checked at first start — run grok login if jobs fail) @ ~/.grok/bin/grok
12
+ ✓ worker:claude-ds ok (claude-ds (DeepSeek shim); default model deepseek-v4-flash) @ ~/.local/bin/claude-ds
13
+ ✗ worker:openrouter OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane model defaults to openrouter/free) @ node '~/Projects/cursor-route/dist/openrouter-run.js'
14
+ ✗ worker:deepseek dsh (@deepseek-ai/dsh) not found — install: npm i -g @deepseek-ai/dsh. Mid default remains claude-ds.
15
+ ✓ cursor_cli optional ok (agent on PATH) — v0 supervisor is Cursor skill, not CLI
16
+ ✓ relaxed CURSOR_ROUTE_RELAXED=1 — tmux optional (headless OK)
17
+ ✓ jobs_dir ~/.local/share/cursor-route/jobs
18
+
19
+ $ cursor-route start --lane mid --model flash --dry-run "Add a unit test for shellQuote"
20
+ dry-run job a1b2c3d4
21
+ worker: claude-ds
22
+ model: flash
23
+ command: cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~/.local/share/cursor-route/jobs/a1b2c3d4.prompt' -Model 'deepseek-v4-flash' --dangerously-skip-permissions
24
+
25
+ $ cursor-route start --lane easy --dry-run "Rewrite this FAQ answer in 3 sentences"
26
+ dry-run job b2c3d4e5
27
+ worker: openrouter
28
+ command: node '~/Projects/cursor-route/dist/openrouter-run.js' --prompt-file '~/.local/share/cursor-route/jobs/b2c3d4e5.prompt'
29
+
30
+ $ cursor-route start --lane hard --dry-run "Refactor auth module; run tests; report verify evidence"
31
+ dry-run job c3d4e5f6
32
+ worker: grok
33
+ command: '~/.grok/bin/grok' -p "$(cat '~/.local/share/cursor-route/jobs/c3d4e5f6.prompt')" --cwd '~/Projects/cursor-route' --no-auto-update --output-format plain --always-approve
34
+
35
+ $ cursor-route start --lane mid --dry-run --json "Add a failing test then make it pass"
36
+ {
37
+ "id": "d4e5f6a7",
38
+ "schema": "cursor-route.job.v1",
39
+ "status": "pending",
40
+ "worker": "claude-ds",
41
+ "lane": "mid",
42
+ "model": "flash",
43
+ "prompt": "Add a failing test then make it pass",
44
+ "cwd": "~/Projects/cursor-route",
45
+ "alwaysApprove": true,
46
+ "tmuxSession": "cursor-route-d4e5f6a7",
47
+ "createdAt": "2026-08-13T00:00:00.000Z",
48
+ "command": "cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~/.local/share/cursor-route/jobs/d4e5f6a7.prompt' -Model 'deepseek-v4-flash' --dangerously-skip-permissions"
49
+ }
50
+
51
+ $ cursor-route jobs --json
52
+ []
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.8**)
6
6
  GitHub: https://github.com/cemini23/cursor-route
7
7
 
8
8
  ## FAQ
@@ -16,8 +16,14 @@ It uses the familiar strategist and worker-pane shape, but it is not a Codex clo
16
16
  ### Does mid lane use Anthropic Claude?
17
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
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 |
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.6",
3
+ "version": "0.1.8",
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",
@@ -23,6 +23,7 @@
23
23
  "SECURITY.md",
24
24
  "SUPPORT.md",
25
25
  "CONTRIBUTING.md",
26
+ "CHANGELOG.md",
26
27
  "README.md",
27
28
  "llms.txt"
28
29
  ],
@@ -46,6 +46,19 @@ cursor-route start --lane mid --model pro --dir "$PWD" "…"
46
46
 
47
47
  If `worker:grok` is ✗ on health, that is usually **auth** (`grok login` / `XAI_API_KEY`) — not the Pro stand-in case.
48
48
 
49
+ ## Experimental: --worker deepseek (dsh)
50
+
51
+ Official DeepSeek Harness headless as an opt-in worker — **not the mid default** (mid stays `claude-ds`).
52
+
53
+ ```bash
54
+ npm i -g @deepseek-ai/dsh
55
+ export DEEPSEEK_API_KEY=... # platform.deepseek.com
56
+ cursor-route start --worker deepseek --dir "$PWD" "…"
57
+ cursor-route start --worker deepseek --model pro --dir "$PWD" "…" # --model applies here too
58
+ ```
59
+
60
+ Health ✓ needs `dsh` on PATH and `DEEPSEEK_API_KEY` set. The adapter pins `--model` via a per-job `--patch` (never touches `~/.dsh/settings.yaml`); always-approve → `DSH_PERMISSION_MODE=danger-full-access`, `--ask` → `workspace-write`. The key never enters the command or patch.
61
+
49
62
  ## Workflow
50
63
 
51
64
  1. Run `cursor-route health` (or `CURSOR_ROUTE_RELAXED=1` for headless). If the **target worker** is unhealthy, fix before spawning.
@@ -63,7 +76,7 @@ EOF
63
76
  )"
64
77
  ```
65
78
 
66
- Or `--worker grok` / `--worker claude-ds` / `--worker openrouter` (or `--lane easy`). Use `--no-tmux` only when tmux is unavailable.
79
+ Or `--worker grok` / `--worker claude-ds` / `--worker deepseek` (experimental) / `--worker openrouter` (or `--lane easy`). Use `--no-tmux` only when tmux is unavailable.
67
80
 
68
81
  4. Monitor: `cursor-route jobs --json` · `cursor-route capture <id>` · `cursor-route send <id> "…"` (tmux only).
69
82
  5. Summarize worker results with **verify evidence** — no status-only “done”. If verify fails, `send` a correction or spawn a follow-up — do not invent success.
@@ -75,6 +88,6 @@ Defaults on for workers. Opt out: `cursor-route start … --ask` or `CURSOR_ROUT
75
88
  ## Anti-patterns
76
89
 
77
90
  - Do not paste API keys / private keys into prompts or `send`
78
- - Do not claim DeepSeek-native harness until that adapter shipstoday is **claude-ds**
91
+ - Do not claim the official DeepSeek harness (`@deepseek-ai/dsh`) is the mid default `--worker deepseek` is experimental only; mid stays **claude-ds**
79
92
  - Do not open-source or dump private Cemini `agent-toolkit` paths into public handoffs
80
93
  - Do not mark done without reading `capture` / exit status
@@ -7,7 +7,7 @@ import {
7
7
  DS_MODEL_IDS,
8
8
  type DsModelAlias,
9
9
  config,
10
- resolveDsModelAlias,
10
+ resolveDsModel,
11
11
  } from "../config.ts";
12
12
  import { shellQuote } from "../util.ts";
13
13
 
@@ -29,6 +29,8 @@ function which(cmd: string): string | null {
29
29
  execSync(`command -v ${shellQuote(cmd)}`, {
30
30
  encoding: "utf8",
31
31
  stdio: ["ignore", "pipe", "ignore"],
32
+ // Bun may ignore mutated process.env.PATH unless env is passed explicitly
33
+ env: { ...process.env },
32
34
  }).trim() || null
33
35
  );
34
36
  } catch {
@@ -65,14 +67,16 @@ function deepseekBaseFromSettings(): string | null {
65
67
 
66
68
  /** Resolved DeepSeek base URL for the mid-lane harness, or null. */
67
69
  export function resolvedDeepSeekBaseUrl(): string | null {
70
+ // Explicit env wins: a non-DeepSeek ANTHROPIC_BASE_URL must not fall through to settings.
68
71
  for (const candidate of [
69
72
  process.env.ANTHROPIC_BASE_URL,
70
73
  process.env.CURSOR_ROUTE_ANTHROPIC_BASE_URL,
71
- deepseekBaseFromSettings(),
72
74
  ]) {
73
- if (candidate && isDeepSeekBaseUrl(candidate)) return candidate;
75
+ if (candidate) {
76
+ return isDeepSeekBaseUrl(candidate) ? candidate : null;
77
+ }
74
78
  }
75
- return null;
79
+ return deepseekBaseFromSettings();
76
80
  }
77
81
 
78
82
  /** True when Claude Code harness is routed to DeepSeek (cheap path). */
@@ -120,18 +124,16 @@ function resolveClaudeDs(): { binary: string; mode: string } | null {
120
124
  return null;
121
125
  }
122
126
 
123
- function pickModelAlias(requested?: DsModelAlias): DsModelAlias {
124
- if (requested) return requested;
125
- // Env override for power users who already export ANTHROPIC_MODEL
126
- const fromEnv = process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL;
127
- if (fromEnv) {
128
- try {
129
- return resolveDsModelAlias(fromEnv);
130
- } catch {
131
- /* fall through to default */
132
- }
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] };
133
134
  }
134
- return config.defaultDsModel;
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);
135
137
  }
136
138
 
137
139
  /**
@@ -177,30 +179,45 @@ export const claudeDsAdapter: Adapter = {
177
179
  detail: `ok (${resolved.mode}; default model ${DS_MODEL_IDS[config.defaultDsModel]})`,
178
180
  };
179
181
  },
180
- buildLaunch({ promptFile, cwd, alwaysApprove, model }) {
182
+ buildLaunch({ promptFile, cwd, alwaysApprove, model, modelId }) {
181
183
  const resolved = resolveClaudeDs();
182
184
  if (!resolved) {
183
185
  throw new Error("DeepSeek worker not available — run: cursor-route health");
184
186
  }
185
187
 
186
- const alias = pickModelAlias(model);
187
- const modelId = DS_MODEL_IDS[alias];
188
-
189
188
  const ask = process.env.CURSOR_ROUTE_ASK === "1" || process.env.CLAUDE_DS_ASK === "1";
190
189
  const skip = alwaysApprove && !ask;
191
- // Stock `claude` needs DeepSeek env injected into the worker process
192
- // (tmux panes may not inherit client env from a long-lived server).
193
- const env = resolved.mode.startsWith("claude → DeepSeek")
194
- ? deepSeekWorkerEnv(modelId)
195
- : 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;
196
213
 
197
- if (resolved.mode.startsWith("claude-ds") || resolved.mode.startsWith("deepseek-claude")) {
214
+ if (isShim) {
198
215
  const parts = [
199
216
  shellQuote(resolved.binary),
200
217
  "-PromptFile",
201
218
  shellQuote(promptFile),
202
219
  "-Model",
203
- shellQuote(modelId),
220
+ shellQuote(choice.id),
204
221
  ];
205
222
  if (skip) parts.push("--dangerously-skip-permissions");
206
223
  return {
@@ -211,12 +228,13 @@ export const claudeDsAdapter: Adapter = {
211
228
  };
212
229
  }
213
230
 
231
+ // Stock claude → DeepSeek
214
232
  const parts = [
215
233
  shellQuote(resolved.binary),
216
234
  "-p",
217
235
  `"$(cat ${shellQuote(promptFile)})"`,
218
236
  "--model",
219
- shellQuote(modelId),
237
+ shellQuote(choice.id),
220
238
  ];
221
239
  if (skip) parts.push("--dangerously-skip-permissions");
222
240
  return {