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/CHANGELOG.md +40 -0
- package/CONTRIBUTING.md +14 -5
- package/README.md +60 -10
- package/dist/adapters/claude-ds.js +43 -29
- package/dist/adapters/deepseek.js +127 -8
- package/dist/cli.js +37 -13
- package/dist/config.js +48 -12
- package/dist/jobs.js +28 -3
- package/docs/DEMO_GIF.md +19 -2
- package/docs/briefs/WORKING.md +18 -7
- package/docs/demo-notes.md +5 -3
- package/docs/fixtures/generate-hero-demo.sh +109 -0
- package/docs/fixtures/hero-demo.log +52 -0
- package/llms.txt +9 -2
- package/package.json +2 -1
- package/skills/route-orch/SKILL.md +15 -2
- package/src/adapters/claude-ds.ts +45 -27
- package/src/adapters/deepseek.ts +145 -11
- package/src/adapters/types.ts +5 -1
- package/src/cli.test.ts +506 -19
- package/src/cli.ts +41 -13
- package/src/config.ts +59 -10
- package/src/jobs.ts +30 -5
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
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
+
When a real GIF lands, check off the **Hero GIF** item in `docs/briefs/WORKING.md` and reference it here.
|
package/docs/briefs/WORKING.md
CHANGED
|
@@ -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.
|
|
4
|
+
npm: cursor-route@0.1.8 (LIVE on npm latest)
|
|
5
5
|
created: 2026-08-12
|
|
6
|
-
updated: 2026-08-
|
|
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
|
|
35
|
-
- [
|
|
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` |
|
|
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. |
|
package/docs/demo-notes.md
CHANGED
|
@@ -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.
|
|
9
|
+
0.1.8
|
|
8
10
|
|
|
9
11
|
$ cursor-route health
|
|
10
|
-
cursor-route v0.1.
|
|
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
|
|
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`.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
75
|
+
if (candidate) {
|
|
76
|
+
return isDeepSeekBaseUrl(candidate) ? candidate : null;
|
|
77
|
+
}
|
|
74
78
|
}
|
|
75
|
-
return
|
|
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
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
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 (
|
|
214
|
+
if (isShim) {
|
|
198
215
|
const parts = [
|
|
199
216
|
shellQuote(resolved.binary),
|
|
200
217
|
"-PromptFile",
|
|
201
218
|
shellQuote(promptFile),
|
|
202
219
|
"-Model",
|
|
203
|
-
shellQuote(
|
|
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(
|
|
237
|
+
shellQuote(choice.id),
|
|
220
238
|
];
|
|
221
239
|
if (skip) parts.push("--dangerously-skip-permissions");
|
|
222
240
|
return {
|