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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.8 — 2026-08-14
|
|
4
|
+
|
|
5
|
+
- Experimental `--worker deepseek`: official DeepSeek Harness (`dsh`, `@deepseek-ai/dsh`) runs headless — `dsh --profile headless` with a per-job Cordis patch pinning the model. Not a mid default: `laneWorkers.mid` stays `claude-ds`.
|
|
6
|
+
- `--model flash|pro` (and `deepseek-v4-pro[1m]`) now applies to both `claude-ds` and `deepseek`; patch sets `agent-default-model` → `provider: deepseek-official` (never rewrites `~/.dsh/settings.yaml`, so parallel jobs don't race).
|
|
7
|
+
- Always-approve → `DSH_PERMISSION_MODE=danger-full-access`; `--ask` → `workspace-write`. `DEEPSEEK_API_KEY` travels via env only — never in the launch command or patch.
|
|
8
|
+
- Health: `worker:deepseek` ✓ only when `dsh` is on PATH (override `CURSOR_ROUTE_DSH_BIN`) **and** `DEEPSEEK_API_KEY` is set.
|
|
9
|
+
- Dry-run for `--worker deepseek` works without `dsh` installed (falls back to plain `dsh` in the printed command); real starts stay gated by health.
|
|
10
|
+
- `clean` also removes leftover `.dsh-patch.yml` files; patch path never overwrites a non-`.prompt` prompt file.
|
|
11
|
+
|
|
12
|
+
## 0.1.7 — 2026-08-12
|
|
13
|
+
|
|
14
|
+
- Fix: `CURSOR_ROUTE_DS_MODEL` / `ANTHROPIC_MODEL` now apply on the `start` product path (live default)
|
|
15
|
+
- Fix: Anthropic escape hatch (`CURSOR_ROUTE_ALLOW_ANTHROPIC=1`) no longer passes DeepSeek `--model` ids
|
|
16
|
+
- Fix: preserve `deepseek-v4-pro[1m]` (no silent SKU strip)
|
|
17
|
+
- `--model` validated only for `claude-ds`; ignored for grok/openrouter (including invalid values)
|
|
18
|
+
- `jobs` / `status` text output show `model` when set
|
|
19
|
+
- Tests cover env default via `startJob`, deepseek slot, Anthropic hatch, health OR-gate
|
|
20
|
+
|
|
21
|
+
## 0.1.6 — 2026-08-12
|
|
22
|
+
|
|
23
|
+
- Mid lane defaults to DeepSeek **Flash** (`deepseek-v4-flash`)
|
|
24
|
+
- `cursor-route start --model flash|pro` (aliases / full ids accepted)
|
|
25
|
+
- `claude-ds` adapter passes `-Model`; stock `claude` → DeepSeek gets `--model`
|
|
26
|
+
- Reserved `deepseek` worker slot (unreleased official harness; mid stays on `claude-ds`)
|
|
27
|
+
- Docs: Grok **auth** (`grok login`) ≠ usage/quota out → use `--model pro`
|
|
28
|
+
- `route-orch` skill Flash/Pro table; living brief in `docs/briefs/WORKING.md`
|
|
29
|
+
|
|
30
|
+
## 0.1.5
|
|
31
|
+
|
|
32
|
+
- OpenRouter free easy lane (`--lane easy` / `--worker openrouter`)
|
|
33
|
+
|
|
34
|
+
## 0.1.4
|
|
35
|
+
|
|
36
|
+
- Compiled `dist/` happy path, integration tests, `CURSOR_ROUTE_MAX_JOBS`
|
|
37
|
+
|
|
38
|
+
## Earlier
|
|
39
|
+
|
|
40
|
+
See git tags `v0.1.1`–`v0.1.3` for security/hardening and first npm ship notes.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -5,22 +5,31 @@
|
|
|
5
5
|
```bash
|
|
6
6
|
bun install
|
|
7
7
|
bun test
|
|
8
|
+
bun run typecheck
|
|
8
9
|
./bin/cursor-route health
|
|
9
10
|
CURSOR_ROUTE_RELAXED=1 ./bin/cursor-route health # without tmux
|
|
10
11
|
./bin/cursor-route start --dry-run --worker grok "ping"
|
|
12
|
+
./bin/cursor-route start --dry-run --lane mid --model flash "ping"
|
|
13
|
+
./bin/cursor-route start --dry-run --lane mid --model pro "ping"
|
|
14
|
+
./bin/cursor-route start --dry-run --worker deepseek --model flash "ping" # needs dsh + DEEPSEEK_API_KEY for real starts
|
|
11
15
|
```
|
|
12
16
|
|
|
17
|
+
Keep `skills/route-orch/SKILL.md` and `.cursor/skills/route-orch/SKILL.md` identical when editing the skill.
|
|
18
|
+
|
|
13
19
|
## Rules
|
|
14
20
|
|
|
15
21
|
- No secrets in prompts, fixtures, or commits
|
|
16
22
|
- Do not vendor private `agent-toolkit` paths
|
|
17
23
|
- Prefer adapters over rewriting the job core
|
|
18
24
|
- Always-approve defaults stay documented + opt-out (`--ask` / `CURSOR_ROUTE_ASK=1`)
|
|
25
|
+
- Mid default is Flash; document Pro only as harder mid / Grok **usage** stand-in (not missing `grok login`)
|
|
19
26
|
|
|
20
27
|
## Release checklist
|
|
21
28
|
|
|
22
|
-
1. `
|
|
23
|
-
2. `
|
|
24
|
-
3.
|
|
25
|
-
4. `
|
|
26
|
-
5.
|
|
29
|
+
1. Update `package.json` version + [CHANGELOG.md](./CHANGELOG.md) + [docs/briefs/WORKING.md](./docs/briefs/WORKING.md)
|
|
30
|
+
2. `bun test` && `bun run typecheck` && `bun run build`
|
|
31
|
+
3. `cursor-route health` (tmux + ≥1 worker)
|
|
32
|
+
4. One real `start` smoke (grok or claude-ds); dry-run both `--model flash` and `--model pro`
|
|
33
|
+
5. `npm pack --dry-run` — no secrets, no `node_modules`
|
|
34
|
+
6. Commit, tag `v0.x.y`, push
|
|
35
|
+
7. `npm publish --access public` with an isolated `--userconfig` Automation token (Bypass 2FA); **revoke** any chat-pasted token after
|
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ If you already live in Cursor, X Premium (Grok CLI), and DeepSeek — stop payin
|
|
|
27
27
|
|------|----------------|------------------|
|
|
28
28
|
| Cursor | Premium plan | Plan / synthesize / verify (orchestrator) |
|
|
29
29
|
| Grok CLI | X Premium | `--lane hard` implement |
|
|
30
|
-
| DeepSeek via claude-ds | DeepSeek API / plan | `--lane mid` implement |
|
|
30
|
+
| DeepSeek via claude-ds | DeepSeek API / plan | `--lane mid` implement (**Flash** default; `--model pro` when needed) |
|
|
31
31
|
| OpenRouter free models | OpenRouter API (free tier) | `--lane easy` wording/drafts — non-secret prompts only (see Security) |
|
|
32
32
|
| Codex / extra Claude | Optional | Not required for v0 |
|
|
33
33
|
|
|
@@ -73,23 +73,26 @@ cd cursor-route && bun install
|
|
|
73
73
|
|
|
74
74
|
### Cursor skill
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
After `npm i -g cursor-route`, copy the packaged skill into a project (or user skills):
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
|
+
PKG="$(npm root -g)/cursor-route"
|
|
79
80
|
mkdir -p .cursor/skills
|
|
80
|
-
cp -R
|
|
81
|
+
cp -R "$PKG/skills/route-orch" .cursor/skills/
|
|
81
82
|
```
|
|
82
83
|
|
|
84
|
+
From a git clone: `cp -R skills/route-orch .cursor/skills/`.
|
|
85
|
+
|
|
83
86
|
Then say **`/route-orch`** or **spawn workers** in Cursor — the skill delegates to `cursor-route` (does not replace a private in-house `/route` skill).
|
|
84
87
|
|
|
85
|
-
Working notes for this repo (edit in place): [docs/briefs/WORKING.md](./docs/briefs/WORKING.md).
|
|
88
|
+
Working notes for this repo (edit in place): [docs/briefs/WORKING.md](./docs/briefs/WORKING.md). See [CHANGELOG.md](./CHANGELOG.md) for release notes.
|
|
86
89
|
|
|
87
90
|
## Commands
|
|
88
91
|
|
|
89
92
|
| Command | Description |
|
|
90
93
|
|---------|-------------|
|
|
91
94
|
| `health` | Gate #1 — tmux, runtime, workers |
|
|
92
|
-
| `start <prompt>` | Spawn worker job (`--worker` / `--lane` / `--dir` / `--ask` / `--dry-run` / `--no-tmux`) |
|
|
95
|
+
| `start <prompt>` | Spawn worker job (`--worker` / `--lane` / `--model` / `--dir` / `--ask` / `--dry-run` / `--no-tmux`) |
|
|
93
96
|
| `jobs [--json]` | List jobs |
|
|
94
97
|
| `status <id>` | Job + sessionAlive |
|
|
95
98
|
| `capture <id> [n]` | Last n pane/log lines |
|
|
@@ -99,6 +102,36 @@ Working notes for this repo (edit in place): [docs/briefs/WORKING.md](./docs/bri
|
|
|
99
102
|
| `sessions` | Managed tmux sessions |
|
|
100
103
|
| `clean [--days N]` | Drop old job files |
|
|
101
104
|
|
|
105
|
+
## Demo
|
|
106
|
+
|
|
107
|
+
A dry-run capture (no live workers, no secrets) is committed as a reproducible fixture:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Needs Bun or Node 20+ and a runnable ./bin/cursor-route (clone: bun install)
|
|
111
|
+
docs/fixtures/generate-hero-demo.sh # regenerate docs/fixtures/hero-demo.log
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
$ cursor-route --version
|
|
116
|
+
0.1.8
|
|
117
|
+
|
|
118
|
+
$ CURSOR_ROUTE_RELAXED=1 cursor-route health
|
|
119
|
+
cursor-route v0.1.8
|
|
120
|
+
health: OK
|
|
121
|
+
|
|
122
|
+
$ cursor-route start --lane mid --model flash --dry-run "Add a unit test for shellQuote"
|
|
123
|
+
dry-run job a1b2c3d4
|
|
124
|
+
worker: claude-ds
|
|
125
|
+
model: flash
|
|
126
|
+
command: cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~/.local/share/cursor-route/jobs/a1b2c3d4.prompt' -Model 'deepseek-v4-flash' --dangerously-skip-permissions
|
|
127
|
+
|
|
128
|
+
$ cursor-route jobs --json
|
|
129
|
+
[]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Full capture: [docs/fixtures/hero-demo.log](./docs/fixtures/hero-demo.log).
|
|
133
|
+
A real hero GIF is still pending — recording steps live in [docs/DEMO_GIF.md](./docs/DEMO_GIF.md).
|
|
134
|
+
|
|
102
135
|
## Lanes
|
|
103
136
|
|
|
104
137
|
| Lane | Worker | Intent |
|
|
@@ -115,14 +148,19 @@ Always-approve is **on** by default. Opt out: `--ask` or `CURSOR_ROUTE_ASK=1`.
|
|
|
115
148
|
|------|----------|------|
|
|
116
149
|
| `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute |
|
|
117
150
|
| `--model pro` | `deepseek-v4-pro` | Harder mid / Grok **usage** stand-in |
|
|
151
|
+
| `--model deepseek-v4-pro[1m]` | `deepseek-v4-pro[1m]` | Large-context Pro (SKU preserved) |
|
|
118
152
|
|
|
119
153
|
```bash
|
|
120
154
|
cursor-route start --lane mid "…" # Flash
|
|
121
155
|
cursor-route start --lane mid --model pro "…" # Pro
|
|
156
|
+
# Or set default without a flag:
|
|
157
|
+
export CURSOR_ROUTE_DS_MODEL=pro # also honors ANTHROPIC_MODEL; --model overrides
|
|
122
158
|
```
|
|
123
159
|
|
|
124
160
|
**Grok auth ≠ usage-out:** if `cursor-route health` shows `worker:grok` ✗, run `grok login` (or set `XAI_API_KEY`). That is auth. Quota / subscription usage exhausted is different — use `--lane mid --model pro` as the stand-in, not a missing login.
|
|
125
161
|
|
|
162
|
+
`CURSOR_ROUTE_ALLOW_ANTHROPIC=1` is an expensive escape hatch: it does **not** pass DeepSeek `--model` ids (Anthropic would reject them).
|
|
163
|
+
|
|
126
164
|
## DeepSeek setup (the cheap mid-lane — this is the point)
|
|
127
165
|
|
|
128
166
|
`cursor-route` mid lane runs **DeepSeek**, not Anthropic. Claude Code is only the terminal harness; bills go to DeepSeek when `ANTHROPIC_BASE_URL` points at them.
|
|
@@ -147,7 +185,18 @@ Persist the same vars under `~/.claude/settings.json` → `"env": { … }` if yo
|
|
|
147
185
|
|
|
148
186
|
**Also accepted:** `claude-ds` or `deepseek-claude` on PATH (Cemini shims). The adapter passes `-Model deepseek-v4-flash|deepseek-v4-pro`.
|
|
149
187
|
|
|
150
|
-
**
|
|
188
|
+
**Experimental:** `--worker deepseek` runs the official DeepSeek Harness (`dsh`, npm `@deepseek-ai/dsh`) as an opt-in worker — **mid stays on `claude-ds`**; don't default lanes here.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npm i -g @deepseek-ai/dsh
|
|
192
|
+
export DEEPSEEK_API_KEY=YOUR_DEEPSEEK_API_KEY # from platform.deepseek.com
|
|
193
|
+
|
|
194
|
+
cursor-route health # worker:deepseek should be ✓
|
|
195
|
+
cursor-route start --worker deepseek "…" # Flash (default)
|
|
196
|
+
cursor-route start --worker deepseek --model pro "…" # Pro when you need it
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The adapter launches `dsh --profile headless` with a **per-job Cordis patch** (`jobs/<id>.dsh-patch.yml`, mode 0600) that pins `--model flash|pro` (`deepseek-v4-pro[1m]` preserved) — it never rewrites `~/.dsh/settings.yaml`, so parallel jobs don't race. Always-approve maps to `DSH_PERMISSION_MODE=danger-full-access`; `--ask` drops to `workspace-write`. Your `DEEPSEEK_API_KEY` travels via env only — never in the launch command or patch. Override the binary with `CURSOR_ROUTE_DSH_BIN`.
|
|
151
200
|
|
|
152
201
|
**Not the default:** bare `claude` still talking to Anthropic. Health refuses that so a misconfigured install cannot silently burn frontier $ rates. Escape hatch only: `CURSOR_ROUTE_ALLOW_ANTHROPIC=1`.
|
|
153
202
|
|
|
@@ -202,6 +251,7 @@ The cursor-route code is open source under MIT. It does not make the worker serv
|
|
|
202
251
|
|
|
203
252
|
## Related
|
|
204
253
|
|
|
254
|
+
- Changelog: [CHANGELOG.md](./CHANGELOG.md)
|
|
205
255
|
- Newsletter: [Outlier Weekly](https://outlierweekly.substack.com)
|
|
206
256
|
- YouTube: [@Cemini23](https://www.youtube.com/@Cemini23)
|
|
207
257
|
- Agent meta-wiki: [cemini-claude-code-CCC](https://github.com/cemini23/cemini-claude-code-CCC)
|
|
@@ -240,8 +290,8 @@ MIT © Cemini — see [LICENSE](LICENSE).
|
|
|
240
290
|
|
|
241
291
|
## Roadmap (explicitly later)
|
|
242
292
|
|
|
243
|
-
- Homebrew tap
|
|
244
|
-
-
|
|
245
|
-
- Codebase map injection (`--map`)
|
|
246
|
-
- Cursor CLI `agent` as alternate supervisor
|
|
293
|
+
- Homebrew tap
|
|
294
|
+
- Stabilize the DeepSeek harness adapter (experimental `--worker deepseek` since 0.1.8)
|
|
295
|
+
- Codebase map injection (`--map`)
|
|
296
|
+
- Cursor CLI `agent` as alternate supervisor
|
|
247
297
|
- Web UI / CAO-style MCP supervisor
|
|
@@ -2,7 +2,7 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { DS_MODEL_IDS, config,
|
|
5
|
+
import { DS_MODEL_IDS, config, resolveDsModel, } from "../config.js";
|
|
6
6
|
import { shellQuote } from "../util.js";
|
|
7
7
|
/**
|
|
8
8
|
* Mid-lane = DeepSeek (cheap). The Claude Code binary is only a harness —
|
|
@@ -20,6 +20,8 @@ function which(cmd) {
|
|
|
20
20
|
return (execSync(`command -v ${shellQuote(cmd)}`, {
|
|
21
21
|
encoding: "utf8",
|
|
22
22
|
stdio: ["ignore", "pipe", "ignore"],
|
|
23
|
+
// Bun may ignore mutated process.env.PATH unless env is passed explicitly
|
|
24
|
+
env: { ...process.env },
|
|
23
25
|
}).trim() || null);
|
|
24
26
|
}
|
|
25
27
|
catch {
|
|
@@ -55,15 +57,16 @@ function deepseekBaseFromSettings() {
|
|
|
55
57
|
}
|
|
56
58
|
/** Resolved DeepSeek base URL for the mid-lane harness, or null. */
|
|
57
59
|
export function resolvedDeepSeekBaseUrl() {
|
|
60
|
+
// Explicit env wins: a non-DeepSeek ANTHROPIC_BASE_URL must not fall through to settings.
|
|
58
61
|
for (const candidate of [
|
|
59
62
|
process.env.ANTHROPIC_BASE_URL,
|
|
60
63
|
process.env.CURSOR_ROUTE_ANTHROPIC_BASE_URL,
|
|
61
|
-
deepseekBaseFromSettings(),
|
|
62
64
|
]) {
|
|
63
|
-
if (candidate
|
|
64
|
-
return candidate;
|
|
65
|
+
if (candidate) {
|
|
66
|
+
return isDeepSeekBaseUrl(candidate) ? candidate : null;
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
|
-
return
|
|
69
|
+
return deepseekBaseFromSettings();
|
|
67
70
|
}
|
|
68
71
|
/** True when Claude Code harness is routed to DeepSeek (cheap path). */
|
|
69
72
|
export function isDeepSeekRouted() {
|
|
@@ -105,20 +108,16 @@ function resolveClaudeDs() {
|
|
|
105
108
|
}
|
|
106
109
|
return null;
|
|
107
110
|
}
|
|
108
|
-
function
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const fromEnv = process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL;
|
|
113
|
-
if (fromEnv) {
|
|
114
|
-
try {
|
|
115
|
-
return resolveDsModelAlias(fromEnv);
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
/* fall through to default */
|
|
119
|
-
}
|
|
111
|
+
function pickModel(requested, modelId) {
|
|
112
|
+
if (modelId) {
|
|
113
|
+
const alias = requested ?? resolveDsModel(modelId).alias;
|
|
114
|
+
return { alias, id: modelId };
|
|
120
115
|
}
|
|
121
|
-
|
|
116
|
+
if (requested) {
|
|
117
|
+
return { alias: requested, id: DS_MODEL_IDS[requested] };
|
|
118
|
+
}
|
|
119
|
+
// Env default (startJob normally resolves this; kept for direct buildLaunch callers)
|
|
120
|
+
return resolveDsModel(process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL);
|
|
122
121
|
}
|
|
123
122
|
/**
|
|
124
123
|
* Env that must reach stock `claude` for DeepSeek routing.
|
|
@@ -163,27 +162,41 @@ export const claudeDsAdapter = {
|
|
|
163
162
|
detail: `ok (${resolved.mode}; default model ${DS_MODEL_IDS[config.defaultDsModel]})`,
|
|
164
163
|
};
|
|
165
164
|
},
|
|
166
|
-
buildLaunch({ promptFile, cwd, alwaysApprove, model }) {
|
|
165
|
+
buildLaunch({ promptFile, cwd, alwaysApprove, model, modelId }) {
|
|
167
166
|
const resolved = resolveClaudeDs();
|
|
168
167
|
if (!resolved) {
|
|
169
168
|
throw new Error("DeepSeek worker not available — run: cursor-route health");
|
|
170
169
|
}
|
|
171
|
-
const alias = pickModelAlias(model);
|
|
172
|
-
const modelId = DS_MODEL_IDS[alias];
|
|
173
170
|
const ask = process.env.CURSOR_ROUTE_ASK === "1" || process.env.CLAUDE_DS_ASK === "1";
|
|
174
171
|
const skip = alwaysApprove && !ask;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (
|
|
172
|
+
const isAnthropicEscape = resolved.mode.startsWith("claude → Anthropic");
|
|
173
|
+
const isDeepSeekStock = resolved.mode.startsWith("claude → DeepSeek");
|
|
174
|
+
const isShim = resolved.mode.startsWith("claude-ds") || resolved.mode.startsWith("deepseek-claude");
|
|
175
|
+
// Anthropic escape hatch: do not pass DeepSeek model ids (unknown to Anthropic).
|
|
176
|
+
// --model flash|pro is DeepSeek-only; stock Claude uses its own defaults / ANTHROPIC_MODEL.
|
|
177
|
+
if (isAnthropicEscape) {
|
|
178
|
+
const parts = [
|
|
179
|
+
shellQuote(resolved.binary),
|
|
180
|
+
"-p",
|
|
181
|
+
`"$(cat ${shellQuote(promptFile)})"`,
|
|
182
|
+
];
|
|
183
|
+
if (skip)
|
|
184
|
+
parts.push("--dangerously-skip-permissions");
|
|
185
|
+
return {
|
|
186
|
+
worker: "claude-ds",
|
|
187
|
+
command: `cd ${shellQuote(cwd)} && ${parts.join(" ")}`,
|
|
188
|
+
alwaysApprove: skip,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const choice = pickModel(model, modelId);
|
|
192
|
+
const env = isDeepSeekStock ? deepSeekWorkerEnv(choice.id) : undefined;
|
|
193
|
+
if (isShim) {
|
|
181
194
|
const parts = [
|
|
182
195
|
shellQuote(resolved.binary),
|
|
183
196
|
"-PromptFile",
|
|
184
197
|
shellQuote(promptFile),
|
|
185
198
|
"-Model",
|
|
186
|
-
shellQuote(
|
|
199
|
+
shellQuote(choice.id),
|
|
187
200
|
];
|
|
188
201
|
if (skip)
|
|
189
202
|
parts.push("--dangerously-skip-permissions");
|
|
@@ -194,12 +207,13 @@ export const claudeDsAdapter = {
|
|
|
194
207
|
env,
|
|
195
208
|
};
|
|
196
209
|
}
|
|
210
|
+
// Stock claude → DeepSeek
|
|
197
211
|
const parts = [
|
|
198
212
|
shellQuote(resolved.binary),
|
|
199
213
|
"-p",
|
|
200
214
|
`"$(cat ${shellQuote(promptFile)})"`,
|
|
201
215
|
"--model",
|
|
202
|
-
shellQuote(
|
|
216
|
+
shellQuote(choice.id),
|
|
203
217
|
];
|
|
204
218
|
if (skip)
|
|
205
219
|
parts.push("--dangerously-skip-permissions");
|
|
@@ -1,19 +1,138 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, writeFileSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { shellQuote } from "../util.js";
|
|
4
|
+
import { DS_MODEL_IDS, resolveDsModel, } from "../config.js";
|
|
1
5
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
6
|
+
* Experimental official DeepSeek Harness (`dsh`, npm @deepseek-ai/dsh) as a
|
|
7
|
+
* coding worker — `dsh --profile headless` with a per-job Cordis patch that
|
|
8
|
+
* pins the model. Mid lane stays on claude-ds; this is an opt-in worker only
|
|
9
|
+
* (`--worker deepseek`), not a mid replacement.
|
|
10
|
+
*
|
|
11
|
+
* We never write ~/.dsh/settings.yaml (parallel jobs would race) and never
|
|
12
|
+
* put DEEPSEEK_API_KEY in the command or patch — the key travels via plan.env.
|
|
4
13
|
*/
|
|
14
|
+
function findDsh() {
|
|
15
|
+
// Env override lets tests pin a fake dsh — but it must exist, so a stale
|
|
16
|
+
// override cannot pass health with a dangling path.
|
|
17
|
+
const override = process.env.CURSOR_ROUTE_DSH_BIN;
|
|
18
|
+
if (override)
|
|
19
|
+
return existsSync(override) ? override : null;
|
|
20
|
+
try {
|
|
21
|
+
return (execSync("command -v dsh", {
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
24
|
+
// Bun may ignore mutated process.env.PATH unless env is passed explicitly
|
|
25
|
+
env: { ...process.env },
|
|
26
|
+
}).trim() || null);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Same resolution as claude-ds: passed model/modelId wins, else env, else Flash. */
|
|
33
|
+
function pickModel(requested, modelId) {
|
|
34
|
+
if (modelId) {
|
|
35
|
+
const alias = requested ?? resolveDsModel(modelId).alias;
|
|
36
|
+
return { alias, id: modelId };
|
|
37
|
+
}
|
|
38
|
+
if (requested) {
|
|
39
|
+
return { alias: requested, id: DS_MODEL_IDS[requested] };
|
|
40
|
+
}
|
|
41
|
+
// Env default (startJob normally resolves this; kept for direct buildLaunch callers)
|
|
42
|
+
return resolveDsModel(process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL);
|
|
43
|
+
}
|
|
44
|
+
/** Whitelist model ids before interpolating into YAML (no newlines / injection). */
|
|
45
|
+
function assertPatchModelId(modelId) {
|
|
46
|
+
if (!/^[a-z0-9][a-z0-9.\-[\]]*$/i.test(modelId)) {
|
|
47
|
+
throw new Error(`Invalid DeepSeek model id for dsh patch: ${modelId}`);
|
|
48
|
+
}
|
|
49
|
+
return modelId;
|
|
50
|
+
}
|
|
51
|
+
/** Per-job Cordis patch path. Never reuse the prompt path (would overwrite it). */
|
|
52
|
+
export function patchPathForPrompt(promptFile) {
|
|
53
|
+
return promptFile.endsWith(".prompt")
|
|
54
|
+
? promptFile.replace(/\.prompt$/, ".dsh-patch.yml")
|
|
55
|
+
: `${promptFile}.dsh-patch.yml`;
|
|
56
|
+
}
|
|
57
|
+
/** Per-job Cordis patch (whole-row replace). `name` is required or dsh silently skips. */
|
|
58
|
+
function patchYaml(modelId) {
|
|
59
|
+
const id = assertPatchModelId(modelId);
|
|
60
|
+
return [
|
|
61
|
+
"- id: agent-default-model",
|
|
62
|
+
" name: '@deepseek-ai/dsh-agent-default-model'",
|
|
63
|
+
" config:",
|
|
64
|
+
" provider: deepseek-official",
|
|
65
|
+
` model: '${id}'`,
|
|
66
|
+
].join("\n") + "\n";
|
|
67
|
+
}
|
|
5
68
|
export const deepseekAdapter = {
|
|
6
69
|
kind: "deepseek",
|
|
7
|
-
label: "Official DeepSeek
|
|
70
|
+
label: "Official DeepSeek Harness (dsh)",
|
|
8
71
|
health() {
|
|
72
|
+
const binary = findDsh();
|
|
73
|
+
if (!binary) {
|
|
74
|
+
return {
|
|
75
|
+
worker: "deepseek",
|
|
76
|
+
ok: false,
|
|
77
|
+
binary: null,
|
|
78
|
+
detail: "dsh (@deepseek-ai/dsh) not found — install: npm i -g @deepseek-ai/dsh. Mid default remains claude-ds.",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (!process.env.DEEPSEEK_API_KEY) {
|
|
82
|
+
return {
|
|
83
|
+
worker: "deepseek",
|
|
84
|
+
ok: false,
|
|
85
|
+
binary,
|
|
86
|
+
detail: "DEEPSEEK_API_KEY not set — export your DeepSeek API key to use dsh (@deepseek-ai/dsh). Mid default remains claude-ds.",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
9
89
|
return {
|
|
10
90
|
worker: "deepseek",
|
|
11
|
-
ok:
|
|
12
|
-
binary
|
|
13
|
-
detail: "
|
|
91
|
+
ok: true,
|
|
92
|
+
binary,
|
|
93
|
+
detail: "ok (dsh @deepseek-ai/dsh headless; mid default remains claude-ds)",
|
|
14
94
|
};
|
|
15
95
|
},
|
|
16
|
-
buildLaunch() {
|
|
17
|
-
|
|
96
|
+
buildLaunch({ promptFile, cwd, alwaysApprove, model, modelId, dryRun }) {
|
|
97
|
+
// Missing dsh is tolerated here so `--dry-run` can still print the command;
|
|
98
|
+
// real starts are gated by the health preflight (binary + DEEPSEEK_API_KEY).
|
|
99
|
+
const binary = findDsh() || "dsh";
|
|
100
|
+
const choice = pickModel(model, modelId);
|
|
101
|
+
// Per-job patch next to the prompt file (never touch ~/.dsh/settings.yaml).
|
|
102
|
+
const patchFile = patchPathForPrompt(promptFile);
|
|
103
|
+
writeFileSync(patchFile, patchYaml(choice.id), { mode: 0o600 });
|
|
104
|
+
if (dryRun) {
|
|
105
|
+
// Dry-run keeps no durable artifacts (jobs.ts removes the prompt likewise).
|
|
106
|
+
try {
|
|
107
|
+
unlinkSync(patchFile);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
/* ignore */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Launcher flags before the task; prompt inlined via cat (never the key).
|
|
114
|
+
const parts = [
|
|
115
|
+
shellQuote(binary),
|
|
116
|
+
"--profile",
|
|
117
|
+
"headless",
|
|
118
|
+
"--patch",
|
|
119
|
+
shellQuote(patchFile),
|
|
120
|
+
`"$(cat ${shellQuote(promptFile)})"`,
|
|
121
|
+
];
|
|
122
|
+
const ask = process.env.CURSOR_ROUTE_ASK === "1";
|
|
123
|
+
const skip = alwaysApprove && !ask;
|
|
124
|
+
const env = {
|
|
125
|
+
DSH_PERMISSION_MODE: skip ? "danger-full-access" : "workspace-write",
|
|
126
|
+
};
|
|
127
|
+
// Key travels via env only — never interpolated into command or patch.
|
|
128
|
+
if (process.env.DEEPSEEK_API_KEY) {
|
|
129
|
+
env.DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
worker: "deepseek",
|
|
133
|
+
command: `cd ${shellQuote(cwd)} && ${parts.join(" ")}`,
|
|
134
|
+
alwaysApprove: skip,
|
|
135
|
+
env,
|
|
136
|
+
};
|
|
18
137
|
},
|
|
19
138
|
};
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { readFileSync, existsSync, realpathSync, statSync } from "node:fs";
|
|
6
6
|
import { resolve, basename } from "node:path";
|
|
7
|
-
import { config, WORKERS, LANES,
|
|
7
|
+
import { config, WORKERS, LANES, resolveDsModel, } from "./config.js";
|
|
8
8
|
import { runHealth, printHealth } from "./health.js";
|
|
9
9
|
import { startJob, listJobs, readJob, killJob, cleanJobs, jobPaths, refreshStatus, } from "./jobs.js";
|
|
10
10
|
import { capturePane, sendKeys, attachHint, listManagedSessions, sessionExists, } from "./tmux.js";
|
|
@@ -29,9 +29,9 @@ Usage:
|
|
|
29
29
|
cursor-route clean [--days N]
|
|
30
30
|
|
|
31
31
|
Start options:
|
|
32
|
-
--worker <grok|claude-ds|openrouter> Worker adapter (default: grok; deepseek =
|
|
32
|
+
--worker <grok|claude-ds|openrouter|deepseek> Worker adapter (default: grok; deepseek = experimental official dsh)
|
|
33
33
|
--lane <easy|mid|hard> Lane → worker (easy=openrouter, mid=claude-ds, hard=grok)
|
|
34
|
-
--model <flash|pro> Mid DeepSeek
|
|
34
|
+
--model <flash|pro> Mid DeepSeek only (default: flash / CURSOR_ROUTE_DS_MODEL). Parsed for claude-ds + deepseek
|
|
35
35
|
--dir <path> Working directory (default: cwd)
|
|
36
36
|
--ask Disable always-approve for this job
|
|
37
37
|
--dry-run Print launch command; do not start
|
|
@@ -43,10 +43,12 @@ Env:
|
|
|
43
43
|
CURSOR_ROUTE_JOBS_DIR Override jobs dir (default: ~/.local/share/cursor-route/jobs)
|
|
44
44
|
CURSOR_ROUTE_MAX_JOBS Max active jobs (default: 50)
|
|
45
45
|
CURSOR_ROUTE_RELAXED=1 health OK without tmux/workers (CI / infra smoke)
|
|
46
|
-
CURSOR_ROUTE_ALLOW_ANTHROPIC=1 Allow mid-lane on Anthropic Claude (expensive; not default)
|
|
47
|
-
CURSOR_ROUTE_DS_MODEL Default mid model flash|pro (overridden by --model
|
|
46
|
+
CURSOR_ROUTE_ALLOW_ANTHROPIC=1 Allow mid-lane on Anthropic Claude (expensive; not default; --model ignored)
|
|
47
|
+
CURSOR_ROUTE_DS_MODEL Default mid model flash|pro (or deepseek-v4-pro[1m]); overridden by --model
|
|
48
48
|
CURSOR_ROUTE_GROK_BIN Override the grok binary path (tests / power users)
|
|
49
49
|
CURSOR_ROUTE_CLAUDE_DS_BIN Override the claude-ds binary path (tests / power users)
|
|
50
|
+
CURSOR_ROUTE_DSH_BIN Override the dsh binary path (tests / power users)
|
|
51
|
+
DEEPSEEK_API_KEY DeepSeek API key (required for --worker deepseek)
|
|
50
52
|
OPENROUTER_API_KEY OpenRouter key (required for --worker openrouter / --lane easy)
|
|
51
53
|
CURSOR_ROUTE_OPENROUTER_MODEL OpenRouter model (default: openrouter/free)
|
|
52
54
|
OPENROUTER_BASE_URL OpenRouter API base (default: https://openrouter.ai/api/v1)
|
|
@@ -141,12 +143,12 @@ function asLane(v) {
|
|
|
141
143
|
return v;
|
|
142
144
|
throw new Error(`Invalid --lane ${v}; expected ${LANES.join("|")}`);
|
|
143
145
|
}
|
|
144
|
-
function
|
|
146
|
+
function asDsModelChoice(v) {
|
|
145
147
|
if (v === undefined || v === true)
|
|
146
148
|
return undefined;
|
|
147
149
|
if (typeof v !== "string")
|
|
148
150
|
throw new Error(`Invalid --model; expected flash|pro`);
|
|
149
|
-
return
|
|
151
|
+
return resolveDsModel(v);
|
|
150
152
|
}
|
|
151
153
|
function refuseSecrets(text, context) {
|
|
152
154
|
if (looksLikeSecretMaterial(text)) {
|
|
@@ -208,14 +210,34 @@ async function main() {
|
|
|
208
210
|
requireStringFlag(f, "model");
|
|
209
211
|
const promptFile = requireStringFlag(f, "prompt-file");
|
|
210
212
|
const dirFlag = requireStringFlag(f, "dir");
|
|
211
|
-
let
|
|
213
|
+
let worker;
|
|
214
|
+
let lane;
|
|
212
215
|
try {
|
|
213
|
-
|
|
216
|
+
worker = asWorker(f.worker);
|
|
217
|
+
lane = asLane(f.lane);
|
|
214
218
|
}
|
|
215
219
|
catch (e) {
|
|
216
220
|
console.error(e.message);
|
|
217
221
|
process.exit(2);
|
|
218
222
|
}
|
|
223
|
+
const resolvedWorker = worker ?? (lane ? config.laneWorkers[lane] : config.defaultWorker);
|
|
224
|
+
let model;
|
|
225
|
+
let modelId;
|
|
226
|
+
// --model is DeepSeek-only (claude-ds mid lane + experimental deepseek worker);
|
|
227
|
+
// ignore (do not validate) for other workers
|
|
228
|
+
if (f.model !== undefined && (resolvedWorker === "claude-ds" || resolvedWorker === "deepseek")) {
|
|
229
|
+
try {
|
|
230
|
+
const choice = asDsModelChoice(f.model);
|
|
231
|
+
if (choice) {
|
|
232
|
+
model = choice.alias;
|
|
233
|
+
modelId = choice.id;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
console.error(e.message);
|
|
238
|
+
process.exit(2);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
219
241
|
let prompt = "";
|
|
220
242
|
if (promptFile) {
|
|
221
243
|
const p = resolve(promptFile);
|
|
@@ -250,9 +272,10 @@ async function main() {
|
|
|
250
272
|
}
|
|
251
273
|
const result = startJob({
|
|
252
274
|
prompt,
|
|
253
|
-
worker
|
|
254
|
-
lane
|
|
275
|
+
worker,
|
|
276
|
+
lane,
|
|
255
277
|
model,
|
|
278
|
+
modelId,
|
|
256
279
|
cwd,
|
|
257
280
|
alwaysApprove: !f.ask,
|
|
258
281
|
dryRun: Boolean(f.dryRun),
|
|
@@ -304,7 +327,8 @@ async function main() {
|
|
|
304
327
|
else {
|
|
305
328
|
for (const j of jobs) {
|
|
306
329
|
const age = j.startedAt || j.createdAt;
|
|
307
|
-
|
|
330
|
+
const modelCol = j.model ? j.model.padEnd(6) : "".padEnd(6);
|
|
331
|
+
console.log(`${j.id} ${j.status.padEnd(10)} ${j.worker.padEnd(10)} ${modelCol} ${age} ${j.prompt.slice(0, 48).replace(/\n/g, " ")}`);
|
|
308
332
|
}
|
|
309
333
|
}
|
|
310
334
|
return;
|
|
@@ -336,7 +360,7 @@ async function main() {
|
|
|
336
360
|
if (json)
|
|
337
361
|
console.log(JSON.stringify(view, null, 2));
|
|
338
362
|
else {
|
|
339
|
-
console.log(`${job.id} ${job.status} worker=${job.worker} sessionAlive=${alive}`);
|
|
363
|
+
console.log(`${job.id} ${job.status} worker=${job.worker}${job.model ? ` model=${job.model}` : ""} sessionAlive=${alive}`);
|
|
340
364
|
if (job.error)
|
|
341
365
|
console.log(`error: ${job.error}`);
|
|
342
366
|
}
|