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