cursor-route 0.1.6 → 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 +14 -5
- package/dist/adapters/claude-ds.js +43 -29
- package/dist/cli.js +33 -12
- package/dist/config.js +44 -11
- package/dist/jobs.js +26 -2
- package/docs/DEMO_GIF.md +2 -1
- package/docs/briefs/WORKING.md +9 -3
- package/llms.txt +9 -2
- package/package.json +2 -1
- package/src/adapters/claude-ds.ts +45 -27
- package/src/adapters/types.ts +3 -1
- package/src/cli.test.ts +162 -10
- package/src/cli.ts +37 -12
- package/src/config.ts +51 -8
- package/src/jobs.ts +26 -2
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,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 |
|
|
@@ -115,14 +118,19 @@ Always-approve is **on** by default. Opt out: `--ask` or `CURSOR_ROUTE_ASK=1`.
|
|
|
115
118
|
|------|----------|------|
|
|
116
119
|
| `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute |
|
|
117
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) |
|
|
118
122
|
|
|
119
123
|
```bash
|
|
120
124
|
cursor-route start --lane mid "…" # Flash
|
|
121
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
|
|
122
128
|
```
|
|
123
129
|
|
|
124
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.
|
|
125
131
|
|
|
132
|
+
`CURSOR_ROUTE_ALLOW_ANTHROPIC=1` is an expensive escape hatch: it does **not** pass DeepSeek `--model` ids (Anthropic would reject them).
|
|
133
|
+
|
|
126
134
|
## DeepSeek setup (the cheap mid-lane — this is the point)
|
|
127
135
|
|
|
128
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.
|
|
@@ -202,6 +210,7 @@ The cursor-route code is open source under MIT. It does not make the worker serv
|
|
|
202
210
|
|
|
203
211
|
## Related
|
|
204
212
|
|
|
213
|
+
- Changelog: [CHANGELOG.md](./CHANGELOG.md)
|
|
205
214
|
- Newsletter: [Outlier Weekly](https://outlierweekly.substack.com)
|
|
206
215
|
- YouTube: [@Cemini23](https://www.youtube.com/@Cemini23)
|
|
207
216
|
- Agent meta-wiki: [cemini-claude-code-CCC](https://github.com/cemini23/cemini-claude-code-CCC)
|
|
@@ -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");
|
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";
|
|
@@ -31,7 +31,7 @@ Usage:
|
|
|
31
31
|
Start options:
|
|
32
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
|
|
34
|
+
--model <flash|pro> Mid DeepSeek only (default: flash / CURSOR_ROUTE_DS_MODEL). Parsed only for claude-ds
|
|
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,8 +43,8 @@ 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
50
|
OPENROUTER_API_KEY OpenRouter key (required for --worker openrouter / --lane easy)
|
|
@@ -141,12 +141,12 @@ function asLane(v) {
|
|
|
141
141
|
return v;
|
|
142
142
|
throw new Error(`Invalid --lane ${v}; expected ${LANES.join("|")}`);
|
|
143
143
|
}
|
|
144
|
-
function
|
|
144
|
+
function asDsModelChoice(v) {
|
|
145
145
|
if (v === undefined || v === true)
|
|
146
146
|
return undefined;
|
|
147
147
|
if (typeof v !== "string")
|
|
148
148
|
throw new Error(`Invalid --model; expected flash|pro`);
|
|
149
|
-
return
|
|
149
|
+
return resolveDsModel(v);
|
|
150
150
|
}
|
|
151
151
|
function refuseSecrets(text, context) {
|
|
152
152
|
if (looksLikeSecretMaterial(text)) {
|
|
@@ -208,14 +208,33 @@ async function main() {
|
|
|
208
208
|
requireStringFlag(f, "model");
|
|
209
209
|
const promptFile = requireStringFlag(f, "prompt-file");
|
|
210
210
|
const dirFlag = requireStringFlag(f, "dir");
|
|
211
|
-
let
|
|
211
|
+
let worker;
|
|
212
|
+
let lane;
|
|
212
213
|
try {
|
|
213
|
-
|
|
214
|
+
worker = asWorker(f.worker);
|
|
215
|
+
lane = asLane(f.lane);
|
|
214
216
|
}
|
|
215
217
|
catch (e) {
|
|
216
218
|
console.error(e.message);
|
|
217
219
|
process.exit(2);
|
|
218
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
|
+
}
|
|
219
238
|
let prompt = "";
|
|
220
239
|
if (promptFile) {
|
|
221
240
|
const p = resolve(promptFile);
|
|
@@ -250,9 +269,10 @@ async function main() {
|
|
|
250
269
|
}
|
|
251
270
|
const result = startJob({
|
|
252
271
|
prompt,
|
|
253
|
-
worker
|
|
254
|
-
lane
|
|
272
|
+
worker,
|
|
273
|
+
lane,
|
|
255
274
|
model,
|
|
275
|
+
modelId,
|
|
256
276
|
cwd,
|
|
257
277
|
alwaysApprove: !f.ask,
|
|
258
278
|
dryRun: Boolean(f.dryRun),
|
|
@@ -304,7 +324,8 @@ async function main() {
|
|
|
304
324
|
else {
|
|
305
325
|
for (const j of jobs) {
|
|
306
326
|
const age = j.startedAt || j.createdAt;
|
|
307
|
-
|
|
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, " ")}`);
|
|
308
329
|
}
|
|
309
330
|
}
|
|
310
331
|
return;
|
|
@@ -336,7 +357,7 @@ async function main() {
|
|
|
336
357
|
if (json)
|
|
337
358
|
console.log(JSON.stringify(view, null, 2));
|
|
338
359
|
else {
|
|
339
|
-
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}`);
|
|
340
361
|
if (job.error)
|
|
341
362
|
console.log(`error: ${job.error}`);
|
|
342
363
|
}
|
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,7 +65,7 @@ function maxConcurrentJobsFromEnv() {
|
|
|
43
65
|
*/
|
|
44
66
|
export const config = {
|
|
45
67
|
product: "cursor-route",
|
|
46
|
-
version: "0.1.
|
|
68
|
+
version: "0.1.7",
|
|
47
69
|
get jobsDir() {
|
|
48
70
|
return defaultJobsDir();
|
|
49
71
|
},
|
|
@@ -55,8 +77,19 @@ export const config = {
|
|
|
55
77
|
mid: "claude-ds",
|
|
56
78
|
hard: "grok",
|
|
57
79
|
},
|
|
58
|
-
/**
|
|
59
|
-
|
|
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
|
+
},
|
|
60
93
|
jobsListLimit: 20,
|
|
61
94
|
/** Max simultaneously active (running|pending) jobs. Override: CURSOR_ROUTE_MAX_JOBS. */
|
|
62
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,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") {
|
|
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,7 @@ export function startJob(opts) {
|
|
|
225
248
|
cwd,
|
|
226
249
|
alwaysApprove,
|
|
227
250
|
model,
|
|
251
|
+
modelId,
|
|
228
252
|
});
|
|
229
253
|
}
|
|
230
254
|
catch (e) {
|
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
|
package/docs/briefs/WORKING.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.7 (LIVE on npm latest)
|
|
5
5
|
created: 2026-08-12
|
|
6
6
|
updated: 2026-08-12
|
|
7
7
|
---
|
|
@@ -19,19 +19,22 @@ 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.7**. 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
36
|
- [x] **Official DeepSeek Harness** — `deepseek` adapter slot present; mid stays on claude-ds
|
|
37
|
+
- [x] **0.1.7 debug fixes** — env default wired through `startJob`; Anthropic hatch omits DS ids; preserve `[1m]`
|
|
35
38
|
- [ ] **Hero GIF** — still outstanding (`docs/DEMO_GIF.md`)
|
|
36
39
|
- [x] **Do not** paste private `ROUTE_KIT`, SIP, prod paths, or hang-watchdog env into this public repo
|
|
37
40
|
|
|
@@ -45,6 +48,7 @@ Always-approve on (`--ask` / `CURSOR_ROUTE_ASK=1` to opt out). Jobs live in `~/.
|
|
|
45
48
|
| `src/adapters/grok.ts` | Hard worker |
|
|
46
49
|
| `src/adapters/openrouter.ts` | Easy worker |
|
|
47
50
|
| `skills/route-orch/SKILL.md` | Cursor skill — spawn CLI, do not implement in-session |
|
|
51
|
+
| `CHANGELOG.md` | Release notes |
|
|
48
52
|
| `SECURITY.md` | Secret refuse gate |
|
|
49
53
|
|
|
50
54
|
## Edit log
|
|
@@ -53,3 +57,5 @@ Always-approve on (`--ask` / `CURSOR_ROUTE_ASK=1` to opt out). Jobs live in `~/.
|
|
|
53
57
|
|------|--------|
|
|
54
58
|
| 2026-08-12 | Brief created in this repo. Flash/Pro CLI + skill still open. |
|
|
55
59
|
| 2026-08-12 | Shipped Flash default + `--model pro`, deepseek slot, skill table → 0.1.6. |
|
|
60
|
+
| 2026-08-12 | npm `cursor-route@0.1.6` LIVE; CHANGELOG + README skill-install path fixed. |
|
|
61
|
+
| 2026-08-12 | Grok debug → 0.1.7: env DS default, Anthropic hatch, `[1m]` preserve. |
|
package/llms.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Cursor stays the planner. DeepSeek (mid), Grok CLI (hard), and OpenRouter free models (easy) run parallel coding workers in tmux.
|
|
4
4
|
|
|
5
|
-
MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route
|
|
5
|
+
MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.7**)
|
|
6
6
|
GitHub: https://github.com/cemini23/cursor-route
|
|
7
7
|
|
|
8
8
|
## FAQ
|
|
@@ -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.7",
|
|
4
4
|
"description": "Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter easy lane are the parallel army \u2014 lane-aware /route orchestration in tmux.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -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
|
],
|
|
@@ -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 {
|
package/src/adapters/types.ts
CHANGED
|
@@ -24,7 +24,9 @@ export interface Adapter {
|
|
|
24
24
|
promptFile: string;
|
|
25
25
|
cwd: string;
|
|
26
26
|
alwaysApprove: boolean;
|
|
27
|
-
/** Mid-lane DeepSeek flash|pro (ignored by other workers). */
|
|
27
|
+
/** Mid-lane DeepSeek flash|pro (ignored by other workers / Anthropic escape hatch). */
|
|
28
28
|
model?: DsModelAlias;
|
|
29
|
+
/** Concrete DeepSeek model id for -Model/--model (preserves pro[1m]). */
|
|
30
|
+
modelId?: string;
|
|
29
31
|
}): LaunchPlan;
|
|
30
32
|
}
|
package/src/cli.test.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { mkdirSync, writeFileSync, chmodSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { resolveWorker, startJob } from "./jobs.ts";
|
|
6
|
+
import { config, resolveDsModel, resolveDsModelAlias } from "./config.ts";
|
|
4
7
|
import { shellQuote, newJobId } from "./util.ts";
|
|
5
8
|
import { runHealth } from "./health.ts";
|
|
6
9
|
import { looksLikeSecretMaterial, redactSecrets } from "./secrets.ts";
|
|
7
10
|
import { isDeepSeekRouted, isDeepSeekBaseUrl, claudeDsAdapter } from "./adapters/claude-ds.ts";
|
|
8
11
|
import { deepseekAdapter } from "./adapters/deepseek.ts";
|
|
12
|
+
import { grokAdapter } from "./adapters/grok.ts";
|
|
9
13
|
|
|
10
14
|
describe("resolveWorker", () => {
|
|
11
15
|
test("lane easy → openrouter", () => {
|
|
@@ -30,27 +34,32 @@ describe("resolveWorker", () => {
|
|
|
30
34
|
});
|
|
31
35
|
});
|
|
32
36
|
|
|
33
|
-
describe("
|
|
37
|
+
describe("resolveDsModel", () => {
|
|
34
38
|
test("defaults to flash", () => {
|
|
35
|
-
expect(
|
|
39
|
+
expect(resolveDsModel()).toEqual({ alias: "flash", id: "deepseek-v4-flash" });
|
|
36
40
|
expect(resolveDsModelAlias("")).toBe("flash");
|
|
37
41
|
});
|
|
38
42
|
test("accepts aliases and full ids", () => {
|
|
39
|
-
expect(
|
|
40
|
-
expect(
|
|
41
|
-
expect(
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
expect(resolveDsModel("flash").id).toBe("deepseek-v4-flash");
|
|
44
|
+
expect(resolveDsModel("pro").id).toBe("deepseek-v4-pro");
|
|
45
|
+
expect(resolveDsModel("deepseek-v4-pro[1m]")).toEqual({
|
|
46
|
+
alias: "pro",
|
|
47
|
+
id: "deepseek-v4-pro[1m]",
|
|
48
|
+
});
|
|
44
49
|
});
|
|
45
50
|
test("rejects unknown", () => {
|
|
46
|
-
expect(() =>
|
|
51
|
+
expect(() => resolveDsModel("opus")).toThrow(/flash\|pro/);
|
|
47
52
|
});
|
|
48
53
|
});
|
|
49
54
|
|
|
50
55
|
describe("claude-ds -Model", () => {
|
|
51
56
|
test("shim path passes -Model deepseek-v4-flash by default", () => {
|
|
52
57
|
const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
58
|
+
const prevDs = process.env.CURSOR_ROUTE_DS_MODEL;
|
|
59
|
+
const prevAm = process.env.ANTHROPIC_MODEL;
|
|
53
60
|
process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
|
|
61
|
+
delete process.env.CURSOR_ROUTE_DS_MODEL;
|
|
62
|
+
delete process.env.ANTHROPIC_MODEL;
|
|
54
63
|
try {
|
|
55
64
|
const plan = claudeDsAdapter.buildLaunch({
|
|
56
65
|
promptFile: "/tmp/p.prompt",
|
|
@@ -62,6 +71,10 @@ describe("claude-ds -Model", () => {
|
|
|
62
71
|
} finally {
|
|
63
72
|
if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
64
73
|
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
|
|
74
|
+
if (prevDs === undefined) delete process.env.CURSOR_ROUTE_DS_MODEL;
|
|
75
|
+
else process.env.CURSOR_ROUTE_DS_MODEL = prevDs;
|
|
76
|
+
if (prevAm === undefined) delete process.env.ANTHROPIC_MODEL;
|
|
77
|
+
else process.env.ANTHROPIC_MODEL = prevAm;
|
|
65
78
|
}
|
|
66
79
|
});
|
|
67
80
|
test("pro alias maps to deepseek-v4-pro", () => {
|
|
@@ -81,6 +94,130 @@ describe("claude-ds -Model", () => {
|
|
|
81
94
|
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
|
|
82
95
|
}
|
|
83
96
|
});
|
|
97
|
+
test("preserves deepseek-v4-pro[1m] via modelId", () => {
|
|
98
|
+
const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
99
|
+
process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
|
|
100
|
+
try {
|
|
101
|
+
const plan = claudeDsAdapter.buildLaunch({
|
|
102
|
+
promptFile: "/tmp/p.prompt",
|
|
103
|
+
cwd: "/tmp",
|
|
104
|
+
alwaysApprove: true,
|
|
105
|
+
model: "pro",
|
|
106
|
+
modelId: "deepseek-v4-pro[1m]",
|
|
107
|
+
});
|
|
108
|
+
expect(plan.command).toContain("deepseek-v4-pro[1m]");
|
|
109
|
+
} finally {
|
|
110
|
+
if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
111
|
+
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
test("Anthropic escape hatch omits DeepSeek --model", () => {
|
|
115
|
+
const shimDir = join(tmpdir(), `cr-anth-escape-${process.pid}`);
|
|
116
|
+
mkdirSync(shimDir, { recursive: true });
|
|
117
|
+
const claude = join(shimDir, "claude");
|
|
118
|
+
writeFileSync(claude, "#!/bin/sh\necho ok\n");
|
|
119
|
+
chmodSync(claude, 0o755);
|
|
120
|
+
|
|
121
|
+
const prev = {
|
|
122
|
+
bin: process.env.CURSOR_ROUTE_CLAUDE_DS_BIN,
|
|
123
|
+
allow: process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC,
|
|
124
|
+
base: process.env.ANTHROPIC_BASE_URL,
|
|
125
|
+
path: process.env.PATH,
|
|
126
|
+
};
|
|
127
|
+
delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
128
|
+
// Explicit non-DeepSeek base so ~/.claude/settings.json cannot force DeepSeek routing
|
|
129
|
+
process.env.ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
|
130
|
+
process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC = "1";
|
|
131
|
+
process.env.PATH = `${shimDir}:/usr/bin:/bin`;
|
|
132
|
+
try {
|
|
133
|
+
const plan = claudeDsAdapter.buildLaunch({
|
|
134
|
+
promptFile: "/tmp/p.prompt",
|
|
135
|
+
cwd: "/tmp",
|
|
136
|
+
alwaysApprove: true,
|
|
137
|
+
model: "pro",
|
|
138
|
+
});
|
|
139
|
+
expect(plan.command).toContain(claude);
|
|
140
|
+
expect(plan.command).not.toContain("deepseek-v4");
|
|
141
|
+
expect(plan.command).not.toContain("--model");
|
|
142
|
+
} finally {
|
|
143
|
+
if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
144
|
+
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev.bin;
|
|
145
|
+
if (prev.allow === undefined) delete process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC;
|
|
146
|
+
else process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC = prev.allow;
|
|
147
|
+
if (prev.base === undefined) delete process.env.ANTHROPIC_BASE_URL;
|
|
148
|
+
else process.env.ANTHROPIC_BASE_URL = prev.base;
|
|
149
|
+
process.env.PATH = prev.path || "";
|
|
150
|
+
rmSync(shimDir, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("startJob product path", () => {
|
|
156
|
+
test("CURSOR_ROUTE_DS_MODEL=pro applies when --model unset", () => {
|
|
157
|
+
const prev = {
|
|
158
|
+
bin: process.env.CURSOR_ROUTE_CLAUDE_DS_BIN,
|
|
159
|
+
ds: process.env.CURSOR_ROUTE_DS_MODEL,
|
|
160
|
+
am: process.env.ANTHROPIC_MODEL,
|
|
161
|
+
jobs: process.env.CURSOR_ROUTE_JOBS_DIR,
|
|
162
|
+
};
|
|
163
|
+
const jobsDir = join(tmpdir(), `cr-jobs-ds-${process.pid}`);
|
|
164
|
+
mkdirSync(jobsDir, { recursive: true });
|
|
165
|
+
process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
|
|
166
|
+
process.env.CURSOR_ROUTE_DS_MODEL = "pro";
|
|
167
|
+
delete process.env.ANTHROPIC_MODEL;
|
|
168
|
+
process.env.CURSOR_ROUTE_JOBS_DIR = jobsDir;
|
|
169
|
+
try {
|
|
170
|
+
const result = startJob({
|
|
171
|
+
prompt: "ping",
|
|
172
|
+
lane: "mid",
|
|
173
|
+
dryRun: true,
|
|
174
|
+
});
|
|
175
|
+
expect(result.ok).toBe(true);
|
|
176
|
+
if (!result.ok) return;
|
|
177
|
+
expect(result.job.model).toBe("pro");
|
|
178
|
+
expect(result.command).toContain("deepseek-v4-pro");
|
|
179
|
+
expect(result.command).not.toContain("deepseek-v4-flash");
|
|
180
|
+
} finally {
|
|
181
|
+
if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
182
|
+
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev.bin;
|
|
183
|
+
if (prev.ds === undefined) delete process.env.CURSOR_ROUTE_DS_MODEL;
|
|
184
|
+
else process.env.CURSOR_ROUTE_DS_MODEL = prev.ds;
|
|
185
|
+
if (prev.am === undefined) delete process.env.ANTHROPIC_MODEL;
|
|
186
|
+
else process.env.ANTHROPIC_MODEL = prev.am;
|
|
187
|
+
if (prev.jobs === undefined) delete process.env.CURSOR_ROUTE_JOBS_DIR;
|
|
188
|
+
else process.env.CURSOR_ROUTE_JOBS_DIR = prev.jobs;
|
|
189
|
+
rmSync(jobsDir, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("--worker deepseek dry-run fails", () => {
|
|
194
|
+
const result = startJob({
|
|
195
|
+
prompt: "ping",
|
|
196
|
+
worker: "deepseek",
|
|
197
|
+
dryRun: true,
|
|
198
|
+
});
|
|
199
|
+
expect(result.ok).toBe(false);
|
|
200
|
+
if (result.ok) return;
|
|
201
|
+
expect(result.error).toMatch(/not available|claude-ds/i);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("grok command never includes deepseek-v4", () => {
|
|
205
|
+
const prev = process.env.CURSOR_ROUTE_GROK_BIN;
|
|
206
|
+
process.env.CURSOR_ROUTE_GROK_BIN = "/tmp/fake-grok";
|
|
207
|
+
try {
|
|
208
|
+
const plan = grokAdapter.buildLaunch({
|
|
209
|
+
promptFile: "/tmp/p.prompt",
|
|
210
|
+
cwd: "/tmp",
|
|
211
|
+
alwaysApprove: true,
|
|
212
|
+
model: "pro",
|
|
213
|
+
});
|
|
214
|
+
expect(plan.command).not.toContain("deepseek-v4");
|
|
215
|
+
expect(plan.command).not.toMatch(/(^|[\s'-])Model\b/);
|
|
216
|
+
} finally {
|
|
217
|
+
if (prev === undefined) delete process.env.CURSOR_ROUTE_GROK_BIN;
|
|
218
|
+
else process.env.CURSOR_ROUTE_GROK_BIN = prev;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
84
221
|
});
|
|
85
222
|
|
|
86
223
|
describe("deepseek adapter slot", () => {
|
|
@@ -153,11 +290,26 @@ describe("health", () => {
|
|
|
153
290
|
test("returns structured report", () => {
|
|
154
291
|
const r = runHealth();
|
|
155
292
|
expect(r.product).toBe("cursor-route");
|
|
293
|
+
expect(r.version).toBe("0.1.7");
|
|
156
294
|
expect(r.checks.length).toBeGreaterThan(3);
|
|
157
295
|
expect(r.checks.some((c) => c.name === "tmux")).toBe(true);
|
|
158
296
|
expect(r.checks.some((c) => c.name === "cursor_cli")).toBe(true);
|
|
159
297
|
});
|
|
160
298
|
|
|
299
|
+
test("OR-gate: ok can be true while worker:deepseek is false", () => {
|
|
300
|
+
const prev = process.env.CURSOR_ROUTE_RELAXED;
|
|
301
|
+
process.env.CURSOR_ROUTE_RELAXED = "1";
|
|
302
|
+
try {
|
|
303
|
+
const r = runHealth();
|
|
304
|
+
const ds = r.checks.find((c) => c.name === "worker:deepseek");
|
|
305
|
+
expect(ds?.ok).toBe(false);
|
|
306
|
+
expect(r.ok).toBe(true);
|
|
307
|
+
} finally {
|
|
308
|
+
if (prev === undefined) delete process.env.CURSOR_ROUTE_RELAXED;
|
|
309
|
+
else process.env.CURSOR_ROUTE_RELAXED = prev;
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
161
313
|
test("relaxed env can pass without workers", () => {
|
|
162
314
|
const prev = process.env.CURSOR_ROUTE_RELAXED;
|
|
163
315
|
process.env.CURSOR_ROUTE_RELAXED = "1";
|
package/src/cli.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
config,
|
|
9
9
|
WORKERS,
|
|
10
10
|
LANES,
|
|
11
|
-
|
|
11
|
+
resolveDsModel,
|
|
12
12
|
type WorkerKind,
|
|
13
13
|
type Lane,
|
|
14
14
|
type DsModelAlias,
|
|
@@ -54,7 +54,7 @@ Usage:
|
|
|
54
54
|
Start options:
|
|
55
55
|
--worker <grok|claude-ds|openrouter> Worker adapter (default: grok; deepseek = reserved slot)
|
|
56
56
|
--lane <easy|mid|hard> Lane → worker (easy=openrouter, mid=claude-ds, hard=grok)
|
|
57
|
-
--model <flash|pro> Mid DeepSeek
|
|
57
|
+
--model <flash|pro> Mid DeepSeek only (default: flash / CURSOR_ROUTE_DS_MODEL). Parsed only for claude-ds
|
|
58
58
|
--dir <path> Working directory (default: cwd)
|
|
59
59
|
--ask Disable always-approve for this job
|
|
60
60
|
--dry-run Print launch command; do not start
|
|
@@ -66,8 +66,8 @@ Env:
|
|
|
66
66
|
CURSOR_ROUTE_JOBS_DIR Override jobs dir (default: ~/.local/share/cursor-route/jobs)
|
|
67
67
|
CURSOR_ROUTE_MAX_JOBS Max active jobs (default: 50)
|
|
68
68
|
CURSOR_ROUTE_RELAXED=1 health OK without tmux/workers (CI / infra smoke)
|
|
69
|
-
CURSOR_ROUTE_ALLOW_ANTHROPIC=1 Allow mid-lane on Anthropic Claude (expensive; not default)
|
|
70
|
-
CURSOR_ROUTE_DS_MODEL Default mid model flash|pro (overridden by --model
|
|
69
|
+
CURSOR_ROUTE_ALLOW_ANTHROPIC=1 Allow mid-lane on Anthropic Claude (expensive; not default; --model ignored)
|
|
70
|
+
CURSOR_ROUTE_DS_MODEL Default mid model flash|pro (or deepseek-v4-pro[1m]); overridden by --model
|
|
71
71
|
CURSOR_ROUTE_GROK_BIN Override the grok binary path (tests / power users)
|
|
72
72
|
CURSOR_ROUTE_CLAUDE_DS_BIN Override the claude-ds binary path (tests / power users)
|
|
73
73
|
OPENROUTER_API_KEY OpenRouter key (required for --worker openrouter / --lane easy)
|
|
@@ -162,10 +162,10 @@ function asLane(v: unknown): Lane | undefined {
|
|
|
162
162
|
throw new Error(`Invalid --lane ${v}; expected ${LANES.join("|")}`);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
function
|
|
165
|
+
function asDsModelChoice(v: unknown): { alias: DsModelAlias; id: string } | undefined {
|
|
166
166
|
if (v === undefined || v === true) return undefined;
|
|
167
167
|
if (typeof v !== "string") throw new Error(`Invalid --model; expected flash|pro`);
|
|
168
|
-
return
|
|
168
|
+
return resolveDsModel(v);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
function refuseSecrets(text: string, context: string): void {
|
|
@@ -235,13 +235,34 @@ async function main() {
|
|
|
235
235
|
requireStringFlag(f, "model");
|
|
236
236
|
const promptFile = requireStringFlag(f, "prompt-file");
|
|
237
237
|
const dirFlag = requireStringFlag(f, "dir");
|
|
238
|
-
|
|
238
|
+
|
|
239
|
+
let worker: WorkerKind | undefined;
|
|
240
|
+
let lane: Lane | undefined;
|
|
239
241
|
try {
|
|
240
|
-
|
|
242
|
+
worker = asWorker(f.worker);
|
|
243
|
+
lane = asLane(f.lane);
|
|
241
244
|
} catch (e) {
|
|
242
245
|
console.error((e as Error).message);
|
|
243
246
|
process.exit(2);
|
|
244
247
|
}
|
|
248
|
+
const resolvedWorker =
|
|
249
|
+
worker ?? (lane ? config.laneWorkers[lane] : config.defaultWorker);
|
|
250
|
+
|
|
251
|
+
let model: DsModelAlias | undefined;
|
|
252
|
+
let modelId: string | undefined;
|
|
253
|
+
// --model is DeepSeek/claude-ds only; ignore (do not validate) for other workers
|
|
254
|
+
if (f.model !== undefined && resolvedWorker === "claude-ds") {
|
|
255
|
+
try {
|
|
256
|
+
const choice = asDsModelChoice(f.model);
|
|
257
|
+
if (choice) {
|
|
258
|
+
model = choice.alias;
|
|
259
|
+
modelId = choice.id;
|
|
260
|
+
}
|
|
261
|
+
} catch (e) {
|
|
262
|
+
console.error((e as Error).message);
|
|
263
|
+
process.exit(2);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
245
266
|
|
|
246
267
|
let prompt = "";
|
|
247
268
|
if (promptFile) {
|
|
@@ -277,9 +298,10 @@ async function main() {
|
|
|
277
298
|
|
|
278
299
|
const result = startJob({
|
|
279
300
|
prompt,
|
|
280
|
-
worker
|
|
281
|
-
lane
|
|
301
|
+
worker,
|
|
302
|
+
lane,
|
|
282
303
|
model,
|
|
304
|
+
modelId,
|
|
283
305
|
cwd,
|
|
284
306
|
alwaysApprove: !f.ask,
|
|
285
307
|
dryRun: Boolean(f.dryRun),
|
|
@@ -333,8 +355,9 @@ async function main() {
|
|
|
333
355
|
} else {
|
|
334
356
|
for (const j of jobs) {
|
|
335
357
|
const age = j.startedAt || j.createdAt;
|
|
358
|
+
const modelCol = j.model ? j.model.padEnd(6) : "".padEnd(6);
|
|
336
359
|
console.log(
|
|
337
|
-
`${j.id} ${j.status.padEnd(10)} ${j.worker.padEnd(10)} ${age} ${j.prompt.slice(0, 48).replace(/\n/g, " ")}`,
|
|
360
|
+
`${j.id} ${j.status.padEnd(10)} ${j.worker.padEnd(10)} ${modelCol} ${age} ${j.prompt.slice(0, 48).replace(/\n/g, " ")}`,
|
|
338
361
|
);
|
|
339
362
|
}
|
|
340
363
|
}
|
|
@@ -366,7 +389,9 @@ async function main() {
|
|
|
366
389
|
const view = { ...job, sessionAlive: alive };
|
|
367
390
|
if (json) console.log(JSON.stringify(view, null, 2));
|
|
368
391
|
else {
|
|
369
|
-
console.log(
|
|
392
|
+
console.log(
|
|
393
|
+
`${job.id} ${job.status} worker=${job.worker}${job.model ? ` model=${job.model}` : ""} sessionAlive=${alive}`,
|
|
394
|
+
);
|
|
370
395
|
if (job.error) console.log(`error: ${job.error}`);
|
|
371
396
|
}
|
|
372
397
|
return;
|
package/src/config.ts
CHANGED
|
@@ -9,6 +9,12 @@ export type Lane = "easy" | "mid" | "hard";
|
|
|
9
9
|
/** Public CLI aliases for mid-lane DeepSeek models. */
|
|
10
10
|
export type DsModelAlias = "flash" | "pro";
|
|
11
11
|
|
|
12
|
+
export interface DsModelChoice {
|
|
13
|
+
alias: DsModelAlias;
|
|
14
|
+
/** Exact id passed to claude-ds `-Model` / stock `claude --model`. */
|
|
15
|
+
id: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
export const WORKERS: WorkerKind[] = ["grok", "claude-ds", "openrouter", "deepseek"];
|
|
13
19
|
export const LANES: Lane[] = ["easy", "mid", "hard"];
|
|
14
20
|
export const DS_MODELS: DsModelAlias[] = ["flash", "pro"];
|
|
@@ -18,15 +24,42 @@ export const DS_MODEL_IDS: Record<DsModelAlias, string> = {
|
|
|
18
24
|
pro: "deepseek-v4-pro",
|
|
19
25
|
};
|
|
20
26
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Resolve --model / env to alias + concrete model id.
|
|
29
|
+
* Preserves `deepseek-v4-pro[1m]` (does not silently strip the SKU).
|
|
30
|
+
* Empty → Flash.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveDsModel(raw?: string | null): DsModelChoice {
|
|
33
|
+
if (!raw || !raw.trim()) {
|
|
34
|
+
return { alias: "flash", id: DS_MODEL_IDS.flash };
|
|
35
|
+
}
|
|
24
36
|
const v = raw.trim().toLowerCase();
|
|
25
|
-
if (v === "flash" || v === "deepseek-v4-flash")
|
|
26
|
-
|
|
37
|
+
if (v === "flash" || v === "deepseek-v4-flash") {
|
|
38
|
+
return { alias: "flash", id: DS_MODEL_IDS.flash };
|
|
39
|
+
}
|
|
40
|
+
if (v === "pro" || v === "deepseek-v4-pro") {
|
|
41
|
+
return { alias: "pro", id: DS_MODEL_IDS.pro };
|
|
42
|
+
}
|
|
43
|
+
if (v === "deepseek-v4-pro[1m]") {
|
|
44
|
+
return { alias: "pro", id: "deepseek-v4-pro[1m]" };
|
|
45
|
+
}
|
|
27
46
|
throw new Error(`Invalid --model ${raw}; expected flash|pro`);
|
|
28
47
|
}
|
|
29
48
|
|
|
49
|
+
/** Alias-only helper (tests / callers that do not need the concrete id). */
|
|
50
|
+
export function resolveDsModelAlias(raw?: string | null): DsModelAlias {
|
|
51
|
+
return resolveDsModel(raw).alias;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Mid default from env: CURSOR_ROUTE_DS_MODEL, else ANTHROPIC_MODEL, else flash.
|
|
56
|
+
* Throws if the env value is set but invalid (fail loud on start).
|
|
57
|
+
*/
|
|
58
|
+
export function defaultDsModelFromEnv(): DsModelChoice {
|
|
59
|
+
const raw = process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL;
|
|
60
|
+
return resolveDsModel(raw);
|
|
61
|
+
}
|
|
62
|
+
|
|
30
63
|
/** OpenRouter model for the easy lane (env CURSOR_ROUTE_OPENROUTER_MODEL). */
|
|
31
64
|
export function openRouterModel(): string {
|
|
32
65
|
return process.env.CURSOR_ROUTE_OPENROUTER_MODEL || "openrouter/free";
|
|
@@ -53,7 +86,7 @@ function maxConcurrentJobsFromEnv(): number {
|
|
|
53
86
|
*/
|
|
54
87
|
export const config = {
|
|
55
88
|
product: "cursor-route",
|
|
56
|
-
version: "0.1.
|
|
89
|
+
version: "0.1.7",
|
|
57
90
|
get jobsDir(): string {
|
|
58
91
|
return defaultJobsDir();
|
|
59
92
|
},
|
|
@@ -65,8 +98,18 @@ export const config = {
|
|
|
65
98
|
mid: "claude-ds" as WorkerKind,
|
|
66
99
|
hard: "grok" as WorkerKind,
|
|
67
100
|
},
|
|
68
|
-
/**
|
|
69
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Default mid DeepSeek model (Flash = cheap execute).
|
|
103
|
+
* Live: CURSOR_ROUTE_DS_MODEL / ANTHROPIC_MODEL; invalid env → flash (health-safe).
|
|
104
|
+
* Override on start: --model. startJob uses defaultDsModelFromEnv() and fails on invalid env.
|
|
105
|
+
*/
|
|
106
|
+
get defaultDsModel(): DsModelAlias {
|
|
107
|
+
try {
|
|
108
|
+
return defaultDsModelFromEnv().alias;
|
|
109
|
+
} catch {
|
|
110
|
+
return "flash";
|
|
111
|
+
}
|
|
112
|
+
},
|
|
70
113
|
jobsListLimit: 20,
|
|
71
114
|
/** Max simultaneously active (running|pending) jobs. Override: CURSOR_ROUTE_MAX_JOBS. */
|
|
72
115
|
get maxConcurrentJobs(): number {
|
package/src/jobs.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { dirname } from "node:path";
|
|
|
15
15
|
import {
|
|
16
16
|
config,
|
|
17
17
|
sessionName,
|
|
18
|
+
defaultDsModelFromEnv,
|
|
19
|
+
DS_MODEL_IDS,
|
|
18
20
|
type Lane,
|
|
19
21
|
type WorkerKind,
|
|
20
22
|
type DsModelAlias,
|
|
@@ -231,6 +233,8 @@ export interface StartOptions {
|
|
|
231
233
|
lane?: Lane;
|
|
232
234
|
/** Mid-lane DeepSeek: flash (default) | pro. Ignored by grok/openrouter. */
|
|
233
235
|
model?: DsModelAlias;
|
|
236
|
+
/** Concrete DeepSeek id (preserves pro[1m]). Derived from --model / env when unset. */
|
|
237
|
+
modelId?: string;
|
|
234
238
|
cwd?: string;
|
|
235
239
|
alwaysApprove?: boolean;
|
|
236
240
|
dryRun?: boolean;
|
|
@@ -278,8 +282,27 @@ export function startJob(opts: StartOptions): {
|
|
|
278
282
|
const paths = jobPaths(id);
|
|
279
283
|
writeSecure(paths.prompt, opts.prompt);
|
|
280
284
|
|
|
281
|
-
|
|
282
|
-
|
|
285
|
+
let model: DsModelAlias | undefined;
|
|
286
|
+
let modelId: string | undefined;
|
|
287
|
+
if (worker === "claude-ds") {
|
|
288
|
+
if (opts.model) {
|
|
289
|
+
model = opts.model;
|
|
290
|
+
modelId = opts.modelId ?? DS_MODEL_IDS[opts.model];
|
|
291
|
+
} else {
|
|
292
|
+
try {
|
|
293
|
+
const choice = defaultDsModelFromEnv();
|
|
294
|
+
model = choice.alias;
|
|
295
|
+
modelId = opts.modelId ?? choice.id;
|
|
296
|
+
} catch (e) {
|
|
297
|
+
try {
|
|
298
|
+
unlinkSync(paths.prompt);
|
|
299
|
+
} catch {
|
|
300
|
+
/* ignore */
|
|
301
|
+
}
|
|
302
|
+
return { ok: false, error: (e as Error).message };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
283
306
|
|
|
284
307
|
let plan;
|
|
285
308
|
try {
|
|
@@ -288,6 +311,7 @@ export function startJob(opts: StartOptions): {
|
|
|
288
311
|
cwd,
|
|
289
312
|
alwaysApprove,
|
|
290
313
|
model,
|
|
314
|
+
modelId,
|
|
291
315
|
});
|
|
292
316
|
} catch (e) {
|
|
293
317
|
try {
|