cursor-route 0.1.5 → 0.1.6
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/README.md +25 -6
- package/dist/adapters/claude-ds.js +33 -9
- package/dist/adapters/deepseek.js +19 -0
- package/dist/adapters/index.js +2 -0
- package/dist/cli.js +25 -3
- package/dist/config.js +20 -2
- package/dist/jobs.js +4 -1
- package/docs/briefs/WORKING.md +55 -0
- package/docs/demo-notes.md +6 -4
- package/llms.txt +1 -1
- package/package.json +2 -1
- package/skills/route-orch/SKILL.md +16 -0
- package/src/adapters/claude-ds.ts +38 -9
- package/src/adapters/deepseek.ts +24 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/types.ts +3 -1
- package/src/cli.test.ts +69 -2
- package/src/cli.ts +30 -3
- package/src/config.ts +24 -3
- package/src/jobs.ts +16 -1
package/README.md
CHANGED
|
@@ -80,7 +80,9 @@ mkdir -p .cursor/skills
|
|
|
80
80
|
cp -R ~/.cursor-route-src/skills/route-orch .cursor/skills/
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
Then say **`/route-orch`** or **spawn workers** in Cursor — the skill delegates to `cursor-route` (does not replace private
|
|
83
|
+
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
|
+
|
|
85
|
+
Working notes for this repo (edit in place): [docs/briefs/WORKING.md](./docs/briefs/WORKING.md).
|
|
84
86
|
|
|
85
87
|
## Commands
|
|
86
88
|
|
|
@@ -102,11 +104,25 @@ Then say **`/route-orch`** or **spawn workers** in Cursor — the skill delegate
|
|
|
102
104
|
| Lane | Worker | Intent |
|
|
103
105
|
|------|--------|--------|
|
|
104
106
|
| `easy` | `openrouter` | Wording / drafts on OpenRouter free models (non-secret prompts only — see Security) |
|
|
105
|
-
| `mid` | `claude-ds` | Default implement on DeepSeek |
|
|
107
|
+
| `mid` | `claude-ds` | Default implement on DeepSeek (**Flash** by default) |
|
|
106
108
|
| `hard` | `grok` | Hard implement on Grok CLI |
|
|
107
109
|
|
|
108
110
|
Always-approve is **on** by default. Opt out: `--ask` or `CURSOR_ROUTE_ASK=1`.
|
|
109
111
|
|
|
112
|
+
### Mid models (Flash vs Pro)
|
|
113
|
+
|
|
114
|
+
| Flag | Model id | When |
|
|
115
|
+
|------|----------|------|
|
|
116
|
+
| `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute |
|
|
117
|
+
| `--model pro` | `deepseek-v4-pro` | Harder mid / Grok **usage** stand-in |
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
cursor-route start --lane mid "…" # Flash
|
|
121
|
+
cursor-route start --lane mid --model pro "…" # Pro
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**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
|
+
|
|
110
126
|
## DeepSeek setup (the cheap mid-lane — this is the point)
|
|
111
127
|
|
|
112
128
|
`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 +134,20 @@ npm i -g @anthropic-ai/claude-code
|
|
|
118
134
|
|
|
119
135
|
export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
|
|
120
136
|
export ANTHROPIC_AUTH_TOKEN=YOUR_DEEPSEEK_API_KEY # from platform.deepseek.com
|
|
121
|
-
|
|
137
|
+
# Optional shell defaults (CLI --model overrides ANTHROPIC_MODEL for the job):
|
|
122
138
|
export ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash
|
|
123
139
|
export CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash
|
|
124
140
|
|
|
125
141
|
cursor-route health # worker:claude-ds should be ✓
|
|
126
|
-
cursor-route start --lane mid "…"
|
|
142
|
+
cursor-route start --lane mid "…" # Flash (default)
|
|
143
|
+
cursor-route start --lane mid --model pro "…" # Pro when you need it
|
|
127
144
|
```
|
|
128
145
|
|
|
129
146
|
Persist the same vars under `~/.claude/settings.json` → `"env": { … }` if you want them every shell.
|
|
130
147
|
|
|
131
|
-
**Also accepted:** `claude-ds` or `deepseek-claude` on PATH (Cemini shims).
|
|
148
|
+
**Also accepted:** `claude-ds` or `deepseek-claude` on PATH (Cemini shims). The adapter passes `-Model deepseek-v4-flash|deepseek-v4-pro`.
|
|
149
|
+
|
|
150
|
+
**Reserved:** `--worker deepseek` is a slot for an official DeepSeek coding harness — not wired yet. Mid stays on `claude-ds`.
|
|
132
151
|
|
|
133
152
|
**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
153
|
|
|
@@ -173,7 +192,7 @@ cursor-route is a public MIT CLI and Cursor skill that runs parallel coding work
|
|
|
173
192
|
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
193
|
|
|
175
194
|
**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`.
|
|
195
|
+
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
196
|
|
|
178
197
|
**How do I install?**
|
|
179
198
|
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.
|
|
@@ -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, resolveDsModelAlias, } 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 —
|
|
@@ -104,24 +105,39 @@ function resolveClaudeDs() {
|
|
|
104
105
|
}
|
|
105
106
|
return null;
|
|
106
107
|
}
|
|
108
|
+
function pickModelAlias(requested) {
|
|
109
|
+
if (requested)
|
|
110
|
+
return requested;
|
|
111
|
+
// Env override for power users who already export ANTHROPIC_MODEL
|
|
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
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return config.defaultDsModel;
|
|
122
|
+
}
|
|
107
123
|
/**
|
|
108
124
|
* Env that must reach stock `claude` for DeepSeek routing.
|
|
109
125
|
* Passed via process/tmux env — never interpolated into the printed command.
|
|
110
126
|
*/
|
|
111
|
-
function deepSeekWorkerEnv() {
|
|
127
|
+
function deepSeekWorkerEnv(modelId) {
|
|
112
128
|
const base = resolvedDeepSeekBaseUrl();
|
|
113
129
|
if (!base)
|
|
114
130
|
return undefined;
|
|
115
|
-
const env = {
|
|
131
|
+
const env = {
|
|
132
|
+
ANTHROPIC_BASE_URL: base,
|
|
133
|
+
ANTHROPIC_MODEL: modelId,
|
|
134
|
+
};
|
|
116
135
|
const token = process.env.ANTHROPIC_AUTH_TOKEN ||
|
|
117
136
|
process.env.ANTHROPIC_API_KEY ||
|
|
118
137
|
process.env.DEEPSEEK_API_KEY ||
|
|
119
138
|
"";
|
|
120
139
|
if (token)
|
|
121
140
|
env.ANTHROPIC_AUTH_TOKEN = token;
|
|
122
|
-
const model = process.env.ANTHROPIC_MODEL;
|
|
123
|
-
if (model)
|
|
124
|
-
env.ANTHROPIC_MODEL = model;
|
|
125
141
|
return env;
|
|
126
142
|
}
|
|
127
143
|
export const claudeDsAdapter = {
|
|
@@ -144,24 +160,30 @@ export const claudeDsAdapter = {
|
|
|
144
160
|
worker: "claude-ds",
|
|
145
161
|
ok: true,
|
|
146
162
|
binary: resolved.binary,
|
|
147
|
-
detail: `ok (${resolved.mode})`,
|
|
163
|
+
detail: `ok (${resolved.mode}; default model ${DS_MODEL_IDS[config.defaultDsModel]})`,
|
|
148
164
|
};
|
|
149
165
|
},
|
|
150
|
-
buildLaunch({ promptFile, cwd, alwaysApprove }) {
|
|
166
|
+
buildLaunch({ promptFile, cwd, alwaysApprove, model }) {
|
|
151
167
|
const resolved = resolveClaudeDs();
|
|
152
168
|
if (!resolved) {
|
|
153
169
|
throw new Error("DeepSeek worker not available — run: cursor-route health");
|
|
154
170
|
}
|
|
171
|
+
const alias = pickModelAlias(model);
|
|
172
|
+
const modelId = DS_MODEL_IDS[alias];
|
|
155
173
|
const ask = process.env.CURSOR_ROUTE_ASK === "1" || process.env.CLAUDE_DS_ASK === "1";
|
|
156
174
|
const skip = alwaysApprove && !ask;
|
|
157
175
|
// Stock `claude` needs DeepSeek env injected into the worker process
|
|
158
176
|
// (tmux panes may not inherit client env from a long-lived server).
|
|
159
|
-
const env = resolved.mode.startsWith("claude → DeepSeek")
|
|
160
|
-
|
|
177
|
+
const env = resolved.mode.startsWith("claude → DeepSeek")
|
|
178
|
+
? deepSeekWorkerEnv(modelId)
|
|
179
|
+
: undefined;
|
|
180
|
+
if (resolved.mode.startsWith("claude-ds") || resolved.mode.startsWith("deepseek-claude")) {
|
|
161
181
|
const parts = [
|
|
162
182
|
shellQuote(resolved.binary),
|
|
163
183
|
"-PromptFile",
|
|
164
184
|
shellQuote(promptFile),
|
|
185
|
+
"-Model",
|
|
186
|
+
shellQuote(modelId),
|
|
165
187
|
];
|
|
166
188
|
if (skip)
|
|
167
189
|
parts.push("--dangerously-skip-permissions");
|
|
@@ -176,6 +198,8 @@ export const claudeDsAdapter = {
|
|
|
176
198
|
shellQuote(resolved.binary),
|
|
177
199
|
"-p",
|
|
178
200
|
`"$(cat ${shellQuote(promptFile)})"`,
|
|
201
|
+
"--model",
|
|
202
|
+
shellQuote(modelId),
|
|
179
203
|
];
|
|
180
204
|
if (skip)
|
|
181
205
|
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, resolveDsModelAlias, } 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 model (default: flash). Ignored by grok/openrouter
|
|
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
|
|
@@ -43,6 +44,7 @@ Env:
|
|
|
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
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
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 asDsModel(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 resolveDsModelAlias(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,17 @@ 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 model;
|
|
212
|
+
try {
|
|
213
|
+
model = asDsModel(f.model);
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
console.error(e.message);
|
|
217
|
+
process.exit(2);
|
|
218
|
+
}
|
|
201
219
|
let prompt = "";
|
|
202
220
|
if (promptFile) {
|
|
203
221
|
const p = resolve(promptFile);
|
|
@@ -234,6 +252,7 @@ async function main() {
|
|
|
234
252
|
prompt,
|
|
235
253
|
worker: asWorker(f.worker),
|
|
236
254
|
lane: asLane(f.lane),
|
|
255
|
+
model,
|
|
237
256
|
cwd,
|
|
238
257
|
alwaysApprove: !f.ask,
|
|
239
258
|
dryRun: Boolean(f.dryRun),
|
|
@@ -252,10 +271,13 @@ async function main() {
|
|
|
252
271
|
else if (result.dryRun) {
|
|
253
272
|
console.log(`dry-run job ${result.job.id}`);
|
|
254
273
|
console.log(`worker: ${result.job.worker}`);
|
|
274
|
+
if (result.job.model)
|
|
275
|
+
console.log(`model: ${result.job.model}`);
|
|
255
276
|
console.log(`command: ${redactSecrets(result.command || "")}`);
|
|
256
277
|
}
|
|
257
278
|
else {
|
|
258
|
-
|
|
279
|
+
const modelNote = result.job.model ? `/${result.job.model}` : "";
|
|
280
|
+
console.log(`started ${result.job.id} (${result.job.worker}${modelNote})`);
|
|
259
281
|
console.log(`session: ${result.job.tmuxSession}`);
|
|
260
282
|
if (String(result.job.tmuxSession).startsWith("headless-")) {
|
|
261
283
|
console.log(`mode: headless (--no-tmux); use capture/status (no attach/send)`);
|
package/dist/config.js
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
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
|
+
/** Resolve --model flash|pro (or full deepseek-v4-* id) to a CLI alias. Default: flash. */
|
|
12
|
+
export function resolveDsModelAlias(raw) {
|
|
13
|
+
if (!raw || !raw.trim())
|
|
14
|
+
return "flash";
|
|
15
|
+
const v = raw.trim().toLowerCase();
|
|
16
|
+
if (v === "flash" || v === "deepseek-v4-flash")
|
|
17
|
+
return "flash";
|
|
18
|
+
if (v === "pro" || v === "deepseek-v4-pro" || v === "deepseek-v4-pro[1m]")
|
|
19
|
+
return "pro";
|
|
20
|
+
throw new Error(`Invalid --model ${raw}; expected flash|pro`);
|
|
21
|
+
}
|
|
6
22
|
/** OpenRouter model for the easy lane (env CURSOR_ROUTE_OPENROUTER_MODEL). */
|
|
7
23
|
export function openRouterModel() {
|
|
8
24
|
return process.env.CURSOR_ROUTE_OPENROUTER_MODEL || "openrouter/free";
|
|
@@ -27,7 +43,7 @@ function maxConcurrentJobsFromEnv() {
|
|
|
27
43
|
*/
|
|
28
44
|
export const config = {
|
|
29
45
|
product: "cursor-route",
|
|
30
|
-
version: "0.1.
|
|
46
|
+
version: "0.1.6",
|
|
31
47
|
get jobsDir() {
|
|
32
48
|
return defaultJobsDir();
|
|
33
49
|
},
|
|
@@ -39,6 +55,8 @@ export const config = {
|
|
|
39
55
|
mid: "claude-ds",
|
|
40
56
|
hard: "grok",
|
|
41
57
|
},
|
|
58
|
+
/** Default mid DeepSeek model (Flash = cheap execute). Override: --model pro */
|
|
59
|
+
defaultDsModel: "flash",
|
|
42
60
|
jobsListLimit: 20,
|
|
43
61
|
/** Max simultaneously active (running|pending) jobs. Override: CURSOR_ROUTE_MAX_JOBS. */
|
|
44
62
|
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, } 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,14 @@ export function startJob(opts) {
|
|
|
217
217
|
const id = newJobId();
|
|
218
218
|
const paths = jobPaths(id);
|
|
219
219
|
writeSecure(paths.prompt, opts.prompt);
|
|
220
|
+
const model = worker === "claude-ds" ? opts.model ?? config.defaultDsModel : undefined;
|
|
220
221
|
let plan;
|
|
221
222
|
try {
|
|
222
223
|
plan = adapter.buildLaunch({
|
|
223
224
|
promptFile: paths.prompt,
|
|
224
225
|
cwd,
|
|
225
226
|
alwaysApprove,
|
|
227
|
+
model,
|
|
226
228
|
});
|
|
227
229
|
}
|
|
228
230
|
catch (e) {
|
|
@@ -240,6 +242,7 @@ export function startJob(opts) {
|
|
|
240
242
|
status: "pending",
|
|
241
243
|
worker,
|
|
242
244
|
lane: opts.lane,
|
|
245
|
+
model,
|
|
243
246
|
prompt: opts.prompt,
|
|
244
247
|
cwd,
|
|
245
248
|
alwaysApprove: plan.alwaysApprove,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: cursor-route workspace — working brief (edit in place)
|
|
3
|
+
repo: ~/Projects/cursor-route
|
|
4
|
+
npm: cursor-route@0.1.6
|
|
5
|
+
created: 2026-08-12
|
|
6
|
+
updated: 2026-08-12
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# cursor-route — living brief
|
|
10
|
+
|
|
11
|
+
**Edit this file.** It is the working notes for this public repo (CLI + `route-orch` skill + adapters). Not the private Cemini `/route` skill (`agent-toolkit` / federation).
|
|
12
|
+
|
|
13
|
+
When a chunk is accepted: apply it to `src/`, `README.md`, and/or `skills/route-orch/SKILL.md` (keep `.cursor/skills/route-orch/SKILL.md` in sync). Then tick **Open** and add an **Edit log** line.
|
|
14
|
+
|
|
15
|
+
## What this repo is
|
|
16
|
+
|
|
17
|
+
Cursor Agent plans. Workers run in tmux via `cursor-route`:
|
|
18
|
+
|
|
19
|
+
| Lane | Worker | Intent |
|
|
20
|
+
|------|--------|--------|
|
|
21
|
+
| `easy` | OpenRouter free | Wording / drafts — non-secret prompts only |
|
|
22
|
+
| `mid` | claude-ds (DeepSeek behind Claude Code) | Default implement (**Flash**) |
|
|
23
|
+
| `hard` | Grok CLI | Hard implement |
|
|
24
|
+
|
|
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
|
+
|
|
27
|
+
## Open (edit / check off)
|
|
28
|
+
|
|
29
|
+
- [x] **Flash vs Pro on the CLI** — public default **Flash**; `--model pro` → `deepseek-v4-pro`
|
|
30
|
+
- [x] pass `claude-ds -Model deepseek-v4-flash|deepseek-v4-pro` from the adapter
|
|
31
|
+
- [x] add `--model flash|pro` on `start`
|
|
32
|
+
- [x] document Grok **auth** ≠ usage-out (`grok login`) vs quota → Pro stand-in
|
|
33
|
+
- [x] **Skill `route-orch`** — Flash/Pro table in `skills/` + `.cursor/skills/`
|
|
34
|
+
- [x] **Official DeepSeek Harness** — `deepseek` adapter slot present; mid stays on claude-ds
|
|
35
|
+
- [ ] **Hero GIF** — still outstanding (`docs/DEMO_GIF.md`)
|
|
36
|
+
- [x] **Do not** paste private `ROUTE_KIT`, SIP, prod paths, or hang-watchdog env into this public repo
|
|
37
|
+
|
|
38
|
+
## Repo map
|
|
39
|
+
|
|
40
|
+
| Path | Role |
|
|
41
|
+
|------|------|
|
|
42
|
+
| `src/cli.ts` | `health` / `start` / `jobs` / `capture` / `send` / `kill` |
|
|
43
|
+
| `src/adapters/claude-ds.ts` | Mid worker; DeepSeek URL required; `--model` → `-Model` |
|
|
44
|
+
| `src/adapters/deepseek.ts` | Reserved unreleased harness slot |
|
|
45
|
+
| `src/adapters/grok.ts` | Hard worker |
|
|
46
|
+
| `src/adapters/openrouter.ts` | Easy worker |
|
|
47
|
+
| `skills/route-orch/SKILL.md` | Cursor skill — spawn CLI, do not implement in-session |
|
|
48
|
+
| `SECURITY.md` | Secret refuse gate |
|
|
49
|
+
|
|
50
|
+
## Edit log
|
|
51
|
+
|
|
52
|
+
| Date | Change |
|
|
53
|
+
|------|--------|
|
|
54
|
+
| 2026-08-12 | Brief created in this repo. Flash/Pro CLI + skill still open. |
|
|
55
|
+
| 2026-08-12 | Shipped Flash default + `--model pro`, deepseek slot, skill table → 0.1.6. |
|
package/docs/demo-notes.md
CHANGED
|
@@ -4,10 +4,10 @@ Simulated capture output for README / tweet assets — replace with a real GIF o
|
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
$ cursor-route --version
|
|
7
|
-
0.1.
|
|
7
|
+
0.1.6
|
|
8
8
|
|
|
9
9
|
$ cursor-route health
|
|
10
|
-
cursor-route v0.1.
|
|
10
|
+
cursor-route v0.1.6
|
|
11
11
|
health: OK
|
|
12
12
|
✓ tmux
|
|
13
13
|
✓ runtime bun ok
|
|
@@ -15,19 +15,21 @@ health: OK
|
|
|
15
15
|
✓ worker:grok
|
|
16
16
|
✓ worker:claude-ds
|
|
17
17
|
✓ worker:openrouter
|
|
18
|
+
✗ worker:deepseek unreleased — mid lane uses claude-ds
|
|
18
19
|
✓ jobs_dir ~/.local/share/cursor-route/jobs
|
|
19
20
|
|
|
20
21
|
$ cursor-route start --lane mid "Add a failing test then make it pass"
|
|
21
|
-
started a1b2c3d4 (claude-ds)
|
|
22
|
+
started a1b2c3d4 (claude-ds/flash)
|
|
22
23
|
session: cursor-route-a1b2c3d4
|
|
23
24
|
attach: tmux attach -t cursor-route-a1b2c3d4
|
|
24
25
|
|
|
25
26
|
$ cursor-route jobs --json
|
|
26
|
-
[{ "id": "a1b2c3d4", "status": "running", "worker": "claude-ds", ... }]
|
|
27
|
+
[{ "id": "a1b2c3d4", "status": "running", "worker": "claude-ds", "model": "flash", ... }]
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
Verified locally (2026-08-10): headless `claude-ds` smoke returned `CURSOR_ROUTE_SMOKE_OK`.
|
|
30
31
|
Grok smoke hit 402 (Build usage balance exhausted) — auth/PATH wiring works; top up Grok Build for live demos.
|
|
32
|
+
Use `--model pro` when Grok **usage** is out (not the same as a missing `grok login`).
|
|
31
33
|
|
|
32
34
|
Current commands: `health`, `start`, `jobs`, `status`, `capture`, `send`, `attach`, `kill`, `sessions`, `clean`.
|
|
33
35
|
Headless demos (no tmux) use `--no-tmux` and `capture`/`status` instead of `attach`/`send`.
|
package/llms.txt
CHANGED
|
@@ -14,7 +14,7 @@ cursor-route is a public MIT CLI and Cursor skill that runs parallel coding work
|
|
|
14
14
|
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.
|
|
15
15
|
|
|
16
16
|
### Does mid lane use Anthropic Claude?
|
|
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`.
|
|
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
19
|
### How do I install?
|
|
20
20
|
Run `npm i -g cursor-route`, install tmux if needed, then run `cursor-route health`. The package is available at https://www.npmjs.com/package/cursor-route, and the source is at https://github.com/cemini23/cursor-route.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-route",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"skills",
|
|
18
18
|
"docs/DEMO_GIF.md",
|
|
19
19
|
"docs/demo-notes.md",
|
|
20
|
+
"docs/briefs/WORKING.md",
|
|
20
21
|
"docs/fixtures",
|
|
21
22
|
"LICENSE",
|
|
22
23
|
"SECURITY.md",
|
|
@@ -30,6 +30,22 @@ You are the **orchestrator**. Do **not** implement bulk code in this Cursor sess
|
|
|
30
30
|
|
|
31
31
|
Free OpenRouter models may log prompts — keep secrets off the easy lane (the CLI refuse gate still applies).
|
|
32
32
|
|
|
33
|
+
## claude-ds models
|
|
34
|
+
|
|
35
|
+
One harness. Do not install a second coding loop.
|
|
36
|
+
|
|
37
|
+
| Flag / role | Model | When |
|
|
38
|
+
|-------------|-------|------|
|
|
39
|
+
| default mid (`--model flash`) | `deepseek-v4-flash` | `--lane mid` cheap execute |
|
|
40
|
+
| Grok stand-in (`--model pro`) | `deepseek-v4-pro` | Grok CLI **usage/quota** out (not a missing `grok login`) |
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cursor-route start --lane mid --dir "$PWD" "…"
|
|
44
|
+
cursor-route start --lane mid --model pro --dir "$PWD" "…"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If `worker:grok` is ✗ on health, that is usually **auth** (`grok login` / `XAI_API_KEY`) — not the Pro stand-in case.
|
|
48
|
+
|
|
33
49
|
## Workflow
|
|
34
50
|
|
|
35
51
|
1. Run `cursor-route health` (or `CURSOR_ROUTE_RELAXED=1` for headless). If the **target worker** is unhealthy, fix before spawning.
|
|
@@ -3,6 +3,12 @@ import { readFileSync, existsSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import type { Adapter, WorkerHealth } from "./types.ts";
|
|
6
|
+
import {
|
|
7
|
+
DS_MODEL_IDS,
|
|
8
|
+
type DsModelAlias,
|
|
9
|
+
config,
|
|
10
|
+
resolveDsModelAlias,
|
|
11
|
+
} from "../config.ts";
|
|
6
12
|
import { shellQuote } from "../util.ts";
|
|
7
13
|
|
|
8
14
|
/**
|
|
@@ -114,22 +120,37 @@ function resolveClaudeDs(): { binary: string; mode: string } | null {
|
|
|
114
120
|
return null;
|
|
115
121
|
}
|
|
116
122
|
|
|
123
|
+
function pickModelAlias(requested?: DsModelAlias): DsModelAlias {
|
|
124
|
+
if (requested) return requested;
|
|
125
|
+
// Env override for power users who already export ANTHROPIC_MODEL
|
|
126
|
+
const fromEnv = process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL;
|
|
127
|
+
if (fromEnv) {
|
|
128
|
+
try {
|
|
129
|
+
return resolveDsModelAlias(fromEnv);
|
|
130
|
+
} catch {
|
|
131
|
+
/* fall through to default */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return config.defaultDsModel;
|
|
135
|
+
}
|
|
136
|
+
|
|
117
137
|
/**
|
|
118
138
|
* Env that must reach stock `claude` for DeepSeek routing.
|
|
119
139
|
* Passed via process/tmux env — never interpolated into the printed command.
|
|
120
140
|
*/
|
|
121
|
-
function deepSeekWorkerEnv(): Record<string, string> | undefined {
|
|
141
|
+
function deepSeekWorkerEnv(modelId: string): Record<string, string> | undefined {
|
|
122
142
|
const base = resolvedDeepSeekBaseUrl();
|
|
123
143
|
if (!base) return undefined;
|
|
124
|
-
const env: Record<string, string> = {
|
|
144
|
+
const env: Record<string, string> = {
|
|
145
|
+
ANTHROPIC_BASE_URL: base,
|
|
146
|
+
ANTHROPIC_MODEL: modelId,
|
|
147
|
+
};
|
|
125
148
|
const token =
|
|
126
149
|
process.env.ANTHROPIC_AUTH_TOKEN ||
|
|
127
150
|
process.env.ANTHROPIC_API_KEY ||
|
|
128
151
|
process.env.DEEPSEEK_API_KEY ||
|
|
129
152
|
"";
|
|
130
153
|
if (token) env.ANTHROPIC_AUTH_TOKEN = token;
|
|
131
|
-
const model = process.env.ANTHROPIC_MODEL;
|
|
132
|
-
if (model) env.ANTHROPIC_MODEL = model;
|
|
133
154
|
return env;
|
|
134
155
|
}
|
|
135
156
|
|
|
@@ -153,27 +174,33 @@ export const claudeDsAdapter: Adapter = {
|
|
|
153
174
|
worker: "claude-ds",
|
|
154
175
|
ok: true,
|
|
155
176
|
binary: resolved.binary,
|
|
156
|
-
detail: `ok (${resolved.mode})`,
|
|
177
|
+
detail: `ok (${resolved.mode}; default model ${DS_MODEL_IDS[config.defaultDsModel]})`,
|
|
157
178
|
};
|
|
158
179
|
},
|
|
159
|
-
buildLaunch({ promptFile, cwd, alwaysApprove }) {
|
|
180
|
+
buildLaunch({ promptFile, cwd, alwaysApprove, model }) {
|
|
160
181
|
const resolved = resolveClaudeDs();
|
|
161
182
|
if (!resolved) {
|
|
162
183
|
throw new Error("DeepSeek worker not available — run: cursor-route health");
|
|
163
184
|
}
|
|
164
185
|
|
|
186
|
+
const alias = pickModelAlias(model);
|
|
187
|
+
const modelId = DS_MODEL_IDS[alias];
|
|
188
|
+
|
|
165
189
|
const ask = process.env.CURSOR_ROUTE_ASK === "1" || process.env.CLAUDE_DS_ASK === "1";
|
|
166
190
|
const skip = alwaysApprove && !ask;
|
|
167
191
|
// Stock `claude` needs DeepSeek env injected into the worker process
|
|
168
192
|
// (tmux panes may not inherit client env from a long-lived server).
|
|
169
|
-
const env =
|
|
170
|
-
|
|
193
|
+
const env = resolved.mode.startsWith("claude → DeepSeek")
|
|
194
|
+
? deepSeekWorkerEnv(modelId)
|
|
195
|
+
: undefined;
|
|
171
196
|
|
|
172
|
-
if (resolved.mode.startsWith("claude-ds")) {
|
|
197
|
+
if (resolved.mode.startsWith("claude-ds") || resolved.mode.startsWith("deepseek-claude")) {
|
|
173
198
|
const parts = [
|
|
174
199
|
shellQuote(resolved.binary),
|
|
175
200
|
"-PromptFile",
|
|
176
201
|
shellQuote(promptFile),
|
|
202
|
+
"-Model",
|
|
203
|
+
shellQuote(modelId),
|
|
177
204
|
];
|
|
178
205
|
if (skip) parts.push("--dangerously-skip-permissions");
|
|
179
206
|
return {
|
|
@@ -188,6 +215,8 @@ export const claudeDsAdapter: Adapter = {
|
|
|
188
215
|
shellQuote(resolved.binary),
|
|
189
216
|
"-p",
|
|
190
217
|
`"$(cat ${shellQuote(promptFile)})"`,
|
|
218
|
+
"--model",
|
|
219
|
+
shellQuote(modelId),
|
|
191
220
|
];
|
|
192
221
|
if (skip) parts.push("--dangerously-skip-permissions");
|
|
193
222
|
return {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Adapter, WorkerHealth } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reserved slot for the official DeepSeek coding harness when it ships.
|
|
5
|
+
* Mid lane stays on claude-ds until then — do not route jobs here.
|
|
6
|
+
*/
|
|
7
|
+
export const deepseekAdapter: Adapter = {
|
|
8
|
+
kind: "deepseek",
|
|
9
|
+
label: "Official DeepSeek harness (unreleased)",
|
|
10
|
+
health(): WorkerHealth {
|
|
11
|
+
return {
|
|
12
|
+
worker: "deepseek",
|
|
13
|
+
ok: false,
|
|
14
|
+
binary: null,
|
|
15
|
+
detail:
|
|
16
|
+
"unreleased — mid lane uses claude-ds (DeepSeek behind Claude Code). See README.",
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
buildLaunch() {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"Official DeepSeek harness is not available yet — use --lane mid / --worker claude-ds",
|
|
22
|
+
);
|
|
23
|
+
},
|
|
24
|
+
};
|
package/src/adapters/index.ts
CHANGED
|
@@ -3,11 +3,13 @@ import type { Adapter } from "./types.ts";
|
|
|
3
3
|
import { grokAdapter } from "./grok.ts";
|
|
4
4
|
import { claudeDsAdapter } from "./claude-ds.ts";
|
|
5
5
|
import { openRouterAdapter } from "./openrouter.ts";
|
|
6
|
+
import { deepseekAdapter } from "./deepseek.ts";
|
|
6
7
|
|
|
7
8
|
const registry: Record<WorkerKind, Adapter> = {
|
|
8
9
|
grok: grokAdapter,
|
|
9
10
|
"claude-ds": claudeDsAdapter,
|
|
10
11
|
openrouter: openRouterAdapter,
|
|
12
|
+
deepseek: deepseekAdapter,
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
export function getAdapter(worker: WorkerKind): Adapter {
|
package/src/adapters/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WorkerKind } from "../config.ts";
|
|
1
|
+
import type { WorkerKind, DsModelAlias } from "../config.ts";
|
|
2
2
|
|
|
3
3
|
export interface WorkerHealth {
|
|
4
4
|
worker: WorkerKind;
|
|
@@ -24,5 +24,7 @@ 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). */
|
|
28
|
+
model?: DsModelAlias;
|
|
27
29
|
}): LaunchPlan;
|
|
28
30
|
}
|
package/src/cli.test.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { resolveWorker } from "./jobs.ts";
|
|
3
|
-
import { config } from "./config.ts";
|
|
3
|
+
import { config, resolveDsModelAlias } from "./config.ts";
|
|
4
4
|
import { shellQuote, newJobId } from "./util.ts";
|
|
5
5
|
import { runHealth } from "./health.ts";
|
|
6
6
|
import { looksLikeSecretMaterial, redactSecrets } from "./secrets.ts";
|
|
7
|
-
import { isDeepSeekRouted, isDeepSeekBaseUrl } from "./adapters/claude-ds.ts";
|
|
7
|
+
import { isDeepSeekRouted, isDeepSeekBaseUrl, claudeDsAdapter } from "./adapters/claude-ds.ts";
|
|
8
|
+
import { deepseekAdapter } from "./adapters/deepseek.ts";
|
|
8
9
|
|
|
9
10
|
describe("resolveWorker", () => {
|
|
10
11
|
test("lane easy → openrouter", () => {
|
|
@@ -29,6 +30,72 @@ describe("resolveWorker", () => {
|
|
|
29
30
|
});
|
|
30
31
|
});
|
|
31
32
|
|
|
33
|
+
describe("resolveDsModelAlias", () => {
|
|
34
|
+
test("defaults to flash", () => {
|
|
35
|
+
expect(resolveDsModelAlias()).toBe("flash");
|
|
36
|
+
expect(resolveDsModelAlias("")).toBe("flash");
|
|
37
|
+
});
|
|
38
|
+
test("accepts aliases and full ids", () => {
|
|
39
|
+
expect(resolveDsModelAlias("flash")).toBe("flash");
|
|
40
|
+
expect(resolveDsModelAlias("pro")).toBe("pro");
|
|
41
|
+
expect(resolveDsModelAlias("deepseek-v4-flash")).toBe("flash");
|
|
42
|
+
expect(resolveDsModelAlias("deepseek-v4-pro")).toBe("pro");
|
|
43
|
+
expect(resolveDsModelAlias("deepseek-v4-pro[1m]")).toBe("pro");
|
|
44
|
+
});
|
|
45
|
+
test("rejects unknown", () => {
|
|
46
|
+
expect(() => resolveDsModelAlias("opus")).toThrow(/flash\|pro/);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("claude-ds -Model", () => {
|
|
51
|
+
test("shim path passes -Model deepseek-v4-flash by default", () => {
|
|
52
|
+
const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
53
|
+
process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
|
|
54
|
+
try {
|
|
55
|
+
const plan = claudeDsAdapter.buildLaunch({
|
|
56
|
+
promptFile: "/tmp/p.prompt",
|
|
57
|
+
cwd: "/tmp",
|
|
58
|
+
alwaysApprove: true,
|
|
59
|
+
});
|
|
60
|
+
expect(plan.command).toContain("-Model");
|
|
61
|
+
expect(plan.command).toContain("deepseek-v4-flash");
|
|
62
|
+
} finally {
|
|
63
|
+
if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
64
|
+
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
test("pro alias maps to deepseek-v4-pro", () => {
|
|
68
|
+
const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
69
|
+
process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
|
|
70
|
+
try {
|
|
71
|
+
const plan = claudeDsAdapter.buildLaunch({
|
|
72
|
+
promptFile: "/tmp/p.prompt",
|
|
73
|
+
cwd: "/tmp",
|
|
74
|
+
alwaysApprove: true,
|
|
75
|
+
model: "pro",
|
|
76
|
+
});
|
|
77
|
+
expect(plan.command).toContain("deepseek-v4-pro");
|
|
78
|
+
expect(plan.command).not.toContain("deepseek-v4-flash");
|
|
79
|
+
} finally {
|
|
80
|
+
if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
81
|
+
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("deepseek adapter slot", () => {
|
|
87
|
+
test("health is not ok and buildLaunch throws", () => {
|
|
88
|
+
expect(deepseekAdapter.health().ok).toBe(false);
|
|
89
|
+
expect(() =>
|
|
90
|
+
deepseekAdapter.buildLaunch({
|
|
91
|
+
promptFile: "/tmp/p",
|
|
92
|
+
cwd: "/tmp",
|
|
93
|
+
alwaysApprove: true,
|
|
94
|
+
}),
|
|
95
|
+
).toThrow(/not available/);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
32
99
|
describe("util", () => {
|
|
33
100
|
test("shellQuote", () => {
|
|
34
101
|
expect(shellQuote("a b")).toBe("'a b'");
|
package/src/cli.ts
CHANGED
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { readFileSync, existsSync, realpathSync, statSync } from "node:fs";
|
|
6
6
|
import { resolve, basename } from "node:path";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
config,
|
|
9
|
+
WORKERS,
|
|
10
|
+
LANES,
|
|
11
|
+
resolveDsModelAlias,
|
|
12
|
+
type WorkerKind,
|
|
13
|
+
type Lane,
|
|
14
|
+
type DsModelAlias,
|
|
15
|
+
} from "./config.ts";
|
|
8
16
|
import { runHealth, printHealth } from "./health.ts";
|
|
9
17
|
import {
|
|
10
18
|
startJob,
|
|
@@ -44,8 +52,9 @@ Usage:
|
|
|
44
52
|
cursor-route clean [--days N]
|
|
45
53
|
|
|
46
54
|
Start options:
|
|
47
|
-
--worker <grok|claude-ds|openrouter> Worker adapter (default: grok)
|
|
55
|
+
--worker <grok|claude-ds|openrouter> Worker adapter (default: grok; deepseek = reserved slot)
|
|
48
56
|
--lane <easy|mid|hard> Lane → worker (easy=openrouter, mid=claude-ds, hard=grok)
|
|
57
|
+
--model <flash|pro> Mid DeepSeek model (default: flash). Ignored by grok/openrouter
|
|
49
58
|
--dir <path> Working directory (default: cwd)
|
|
50
59
|
--ask Disable always-approve for this job
|
|
51
60
|
--dry-run Print launch command; do not start
|
|
@@ -58,6 +67,7 @@ Env:
|
|
|
58
67
|
CURSOR_ROUTE_MAX_JOBS Max active jobs (default: 50)
|
|
59
68
|
CURSOR_ROUTE_RELAXED=1 health OK without tmux/workers (CI / infra smoke)
|
|
60
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)
|
|
61
71
|
CURSOR_ROUTE_GROK_BIN Override the grok binary path (tests / power users)
|
|
62
72
|
CURSOR_ROUTE_CLAUDE_DS_BIN Override the claude-ds binary path (tests / power users)
|
|
63
73
|
OPENROUTER_API_KEY OpenRouter key (required for --worker openrouter / --lane easy)
|
|
@@ -152,6 +162,12 @@ function asLane(v: unknown): Lane | undefined {
|
|
|
152
162
|
throw new Error(`Invalid --lane ${v}; expected ${LANES.join("|")}`);
|
|
153
163
|
}
|
|
154
164
|
|
|
165
|
+
function asDsModel(v: unknown): DsModelAlias | undefined {
|
|
166
|
+
if (v === undefined || v === true) return undefined;
|
|
167
|
+
if (typeof v !== "string") throw new Error(`Invalid --model; expected flash|pro`);
|
|
168
|
+
return resolveDsModelAlias(v);
|
|
169
|
+
}
|
|
170
|
+
|
|
155
171
|
function refuseSecrets(text: string, context: string): void {
|
|
156
172
|
if (looksLikeSecretMaterial(text)) {
|
|
157
173
|
console.error(
|
|
@@ -216,8 +232,16 @@ async function main() {
|
|
|
216
232
|
if (cmd === "start") {
|
|
217
233
|
requireStringFlag(f, "worker");
|
|
218
234
|
requireStringFlag(f, "lane");
|
|
235
|
+
requireStringFlag(f, "model");
|
|
219
236
|
const promptFile = requireStringFlag(f, "prompt-file");
|
|
220
237
|
const dirFlag = requireStringFlag(f, "dir");
|
|
238
|
+
let model: DsModelAlias | undefined;
|
|
239
|
+
try {
|
|
240
|
+
model = asDsModel(f.model);
|
|
241
|
+
} catch (e) {
|
|
242
|
+
console.error((e as Error).message);
|
|
243
|
+
process.exit(2);
|
|
244
|
+
}
|
|
221
245
|
|
|
222
246
|
let prompt = "";
|
|
223
247
|
if (promptFile) {
|
|
@@ -255,6 +279,7 @@ async function main() {
|
|
|
255
279
|
prompt,
|
|
256
280
|
worker: asWorker(f.worker),
|
|
257
281
|
lane: asLane(f.lane),
|
|
282
|
+
model,
|
|
258
283
|
cwd,
|
|
259
284
|
alwaysApprove: !f.ask,
|
|
260
285
|
dryRun: Boolean(f.dryRun),
|
|
@@ -280,9 +305,11 @@ async function main() {
|
|
|
280
305
|
} else if (result.dryRun) {
|
|
281
306
|
console.log(`dry-run job ${result.job.id}`);
|
|
282
307
|
console.log(`worker: ${result.job.worker}`);
|
|
308
|
+
if (result.job.model) console.log(`model: ${result.job.model}`);
|
|
283
309
|
console.log(`command: ${redactSecrets(result.command || "")}`);
|
|
284
310
|
} else {
|
|
285
|
-
|
|
311
|
+
const modelNote = result.job.model ? `/${result.job.model}` : "";
|
|
312
|
+
console.log(`started ${result.job.id} (${result.job.worker}${modelNote})`);
|
|
286
313
|
console.log(`session: ${result.job.tmuxSession}`);
|
|
287
314
|
if (String(result.job.tmuxSession).startsWith("headless-")) {
|
|
288
315
|
console.log(`mode: headless (--no-tmux); use capture/status (no attach/send)`);
|
package/src/config.ts
CHANGED
|
@@ -2,11 +2,30 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { defaultJobsDir } from "./runtime.ts";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** Workers with a live adapter. `deepseek` is a reserved slot (unreleased harness). */
|
|
6
|
+
export type WorkerKind = "grok" | "claude-ds" | "openrouter" | "deepseek";
|
|
6
7
|
export type Lane = "easy" | "mid" | "hard";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
/** Public CLI aliases for mid-lane DeepSeek models. */
|
|
10
|
+
export type DsModelAlias = "flash" | "pro";
|
|
11
|
+
|
|
12
|
+
export const WORKERS: WorkerKind[] = ["grok", "claude-ds", "openrouter", "deepseek"];
|
|
9
13
|
export const LANES: Lane[] = ["easy", "mid", "hard"];
|
|
14
|
+
export const DS_MODELS: DsModelAlias[] = ["flash", "pro"];
|
|
15
|
+
|
|
16
|
+
export const DS_MODEL_IDS: Record<DsModelAlias, string> = {
|
|
17
|
+
flash: "deepseek-v4-flash",
|
|
18
|
+
pro: "deepseek-v4-pro",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Resolve --model flash|pro (or full deepseek-v4-* id) to a CLI alias. Default: flash. */
|
|
22
|
+
export function resolveDsModelAlias(raw?: string | null): DsModelAlias {
|
|
23
|
+
if (!raw || !raw.trim()) return "flash";
|
|
24
|
+
const v = raw.trim().toLowerCase();
|
|
25
|
+
if (v === "flash" || v === "deepseek-v4-flash") return "flash";
|
|
26
|
+
if (v === "pro" || v === "deepseek-v4-pro" || v === "deepseek-v4-pro[1m]") return "pro";
|
|
27
|
+
throw new Error(`Invalid --model ${raw}; expected flash|pro`);
|
|
28
|
+
}
|
|
10
29
|
|
|
11
30
|
/** OpenRouter model for the easy lane (env CURSOR_ROUTE_OPENROUTER_MODEL). */
|
|
12
31
|
export function openRouterModel(): string {
|
|
@@ -34,7 +53,7 @@ function maxConcurrentJobsFromEnv(): number {
|
|
|
34
53
|
*/
|
|
35
54
|
export const config = {
|
|
36
55
|
product: "cursor-route",
|
|
37
|
-
version: "0.1.
|
|
56
|
+
version: "0.1.6",
|
|
38
57
|
get jobsDir(): string {
|
|
39
58
|
return defaultJobsDir();
|
|
40
59
|
},
|
|
@@ -46,6 +65,8 @@ export const config = {
|
|
|
46
65
|
mid: "claude-ds" as WorkerKind,
|
|
47
66
|
hard: "grok" as WorkerKind,
|
|
48
67
|
},
|
|
68
|
+
/** Default mid DeepSeek model (Flash = cheap execute). Override: --model pro */
|
|
69
|
+
defaultDsModel: "flash" as DsModelAlias,
|
|
49
70
|
jobsListLimit: 20,
|
|
50
71
|
/** Max simultaneously active (running|pending) jobs. Override: CURSOR_ROUTE_MAX_JOBS. */
|
|
51
72
|
get maxConcurrentJobs(): number {
|
package/src/jobs.ts
CHANGED
|
@@ -12,7 +12,13 @@ import {
|
|
|
12
12
|
import { join, resolve } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { dirname } from "node:path";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
config,
|
|
17
|
+
sessionName,
|
|
18
|
+
type Lane,
|
|
19
|
+
type WorkerKind,
|
|
20
|
+
type DsModelAlias,
|
|
21
|
+
} from "./config.ts";
|
|
16
22
|
import { getAdapter } from "./adapters/index.ts";
|
|
17
23
|
import { spawn } from "node:child_process";
|
|
18
24
|
import { spawnSync } from "node:child_process";
|
|
@@ -29,6 +35,8 @@ export interface Job {
|
|
|
29
35
|
status: JobStatus;
|
|
30
36
|
worker: WorkerKind;
|
|
31
37
|
lane?: Lane;
|
|
38
|
+
/** Mid-lane DeepSeek model alias (flash|pro). Only set for claude-ds. */
|
|
39
|
+
model?: DsModelAlias;
|
|
32
40
|
prompt: string;
|
|
33
41
|
cwd: string;
|
|
34
42
|
alwaysApprove: boolean;
|
|
@@ -221,6 +229,8 @@ export interface StartOptions {
|
|
|
221
229
|
prompt: string;
|
|
222
230
|
worker?: WorkerKind;
|
|
223
231
|
lane?: Lane;
|
|
232
|
+
/** Mid-lane DeepSeek: flash (default) | pro. Ignored by grok/openrouter. */
|
|
233
|
+
model?: DsModelAlias;
|
|
224
234
|
cwd?: string;
|
|
225
235
|
alwaysApprove?: boolean;
|
|
226
236
|
dryRun?: boolean;
|
|
@@ -268,12 +278,16 @@ export function startJob(opts: StartOptions): {
|
|
|
268
278
|
const paths = jobPaths(id);
|
|
269
279
|
writeSecure(paths.prompt, opts.prompt);
|
|
270
280
|
|
|
281
|
+
const model =
|
|
282
|
+
worker === "claude-ds" ? opts.model ?? config.defaultDsModel : undefined;
|
|
283
|
+
|
|
271
284
|
let plan;
|
|
272
285
|
try {
|
|
273
286
|
plan = adapter.buildLaunch({
|
|
274
287
|
promptFile: paths.prompt,
|
|
275
288
|
cwd,
|
|
276
289
|
alwaysApprove,
|
|
290
|
+
model,
|
|
277
291
|
});
|
|
278
292
|
} catch (e) {
|
|
279
293
|
try {
|
|
@@ -290,6 +304,7 @@ export function startJob(opts: StartOptions): {
|
|
|
290
304
|
status: "pending",
|
|
291
305
|
worker,
|
|
292
306
|
lane: opts.lane,
|
|
307
|
+
model,
|
|
293
308
|
prompt: opts.prompt,
|
|
294
309
|
cwd,
|
|
295
310
|
alwaysApprove: plan.alwaysApprove,
|