cursor-route 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/CONTRIBUTING.md +13 -5
- package/README.md +38 -10
- package/dist/adapters/claude-ds.js +53 -15
- package/dist/adapters/deepseek.js +19 -0
- package/dist/adapters/index.js +2 -0
- package/dist/cli.js +51 -8
- package/dist/config.js +53 -2
- package/dist/jobs.js +28 -1
- package/docs/DEMO_GIF.md +2 -1
- package/docs/briefs/WORKING.md +61 -0
- package/docs/demo-notes.md +6 -4
- package/llms.txt +10 -3
- package/package.json +3 -1
- package/skills/route-orch/SKILL.md +16 -0
- package/src/adapters/claude-ds.ts +61 -14
- package/src/adapters/deepseek.ts +24 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/types.ts +5 -1
- package/src/cli.test.ts +222 -3
- package/src/cli.ts +60 -8
- package/src/config.ts +67 -3
- package/src/jobs.ts +40 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: cursor-route workspace — working brief (edit in place)
|
|
3
|
+
repo: ~/Projects/cursor-route
|
|
4
|
+
npm: cursor-route@0.1.7 (LIVE on npm latest)
|
|
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**; `--model pro` when needed) |
|
|
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
|
+
Install: `npm i -g cursor-route` → **0.1.7**. Release notes: [CHANGELOG.md](../../CHANGELOG.md).
|
|
28
|
+
|
|
29
|
+
## Open (edit / check off)
|
|
30
|
+
|
|
31
|
+
- [x] **Flash vs Pro on the CLI** — public default **Flash**; `--model pro` → `deepseek-v4-pro` (LIVE 0.1.6+)
|
|
32
|
+
- [x] pass `claude-ds -Model deepseek-v4-flash|deepseek-v4-pro` from the adapter
|
|
33
|
+
- [x] add `--model flash|pro` on `start`
|
|
34
|
+
- [x] document Grok **auth** ≠ usage-out (`grok login`) vs quota → Pro stand-in
|
|
35
|
+
- [x] **Skill `route-orch`** — Flash/Pro table in `skills/` + `.cursor/skills/`
|
|
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]`
|
|
38
|
+
- [ ] **Hero GIF** — still outstanding (`docs/DEMO_GIF.md`)
|
|
39
|
+
- [x] **Do not** paste private `ROUTE_KIT`, SIP, prod paths, or hang-watchdog env into this public repo
|
|
40
|
+
|
|
41
|
+
## Repo map
|
|
42
|
+
|
|
43
|
+
| Path | Role |
|
|
44
|
+
|------|------|
|
|
45
|
+
| `src/cli.ts` | `health` / `start` / `jobs` / `capture` / `send` / `kill` |
|
|
46
|
+
| `src/adapters/claude-ds.ts` | Mid worker; DeepSeek URL required; `--model` → `-Model` |
|
|
47
|
+
| `src/adapters/deepseek.ts` | Reserved unreleased harness slot |
|
|
48
|
+
| `src/adapters/grok.ts` | Hard worker |
|
|
49
|
+
| `src/adapters/openrouter.ts` | Easy worker |
|
|
50
|
+
| `skills/route-orch/SKILL.md` | Cursor skill — spawn CLI, do not implement in-session |
|
|
51
|
+
| `CHANGELOG.md` | Release notes |
|
|
52
|
+
| `SECURITY.md` | Secret refuse gate |
|
|
53
|
+
|
|
54
|
+
## Edit log
|
|
55
|
+
|
|
56
|
+
| Date | Change |
|
|
57
|
+
|------|--------|
|
|
58
|
+
| 2026-08-12 | Brief created in this repo. Flash/Pro CLI + skill still open. |
|
|
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/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
|
@@ -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
|
|
@@ -14,10 +14,16 @@ 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
|
+
|
|
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 |
|
|
18
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",
|
|
@@ -17,11 +17,13 @@
|
|
|
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",
|
|
23
24
|
"SUPPORT.md",
|
|
24
25
|
"CONTRIBUTING.md",
|
|
26
|
+
"CHANGELOG.md",
|
|
25
27
|
"README.md",
|
|
26
28
|
"llms.txt"
|
|
27
29
|
],
|
|
@@ -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
|
+
resolveDsModel,
|
|
11
|
+
} from "../config.ts";
|
|
6
12
|
import { shellQuote } from "../util.ts";
|
|
7
13
|
|
|
8
14
|
/**
|
|
@@ -23,6 +29,8 @@ function which(cmd: string): string | null {
|
|
|
23
29
|
execSync(`command -v ${shellQuote(cmd)}`, {
|
|
24
30
|
encoding: "utf8",
|
|
25
31
|
stdio: ["ignore", "pipe", "ignore"],
|
|
32
|
+
// Bun may ignore mutated process.env.PATH unless env is passed explicitly
|
|
33
|
+
env: { ...process.env },
|
|
26
34
|
}).trim() || null
|
|
27
35
|
);
|
|
28
36
|
} catch {
|
|
@@ -59,14 +67,16 @@ function deepseekBaseFromSettings(): string | null {
|
|
|
59
67
|
|
|
60
68
|
/** Resolved DeepSeek base URL for the mid-lane harness, or null. */
|
|
61
69
|
export function resolvedDeepSeekBaseUrl(): string | null {
|
|
70
|
+
// Explicit env wins: a non-DeepSeek ANTHROPIC_BASE_URL must not fall through to settings.
|
|
62
71
|
for (const candidate of [
|
|
63
72
|
process.env.ANTHROPIC_BASE_URL,
|
|
64
73
|
process.env.CURSOR_ROUTE_ANTHROPIC_BASE_URL,
|
|
65
|
-
deepseekBaseFromSettings(),
|
|
66
74
|
]) {
|
|
67
|
-
if (candidate
|
|
75
|
+
if (candidate) {
|
|
76
|
+
return isDeepSeekBaseUrl(candidate) ? candidate : null;
|
|
77
|
+
}
|
|
68
78
|
}
|
|
69
|
-
return
|
|
79
|
+
return deepseekBaseFromSettings();
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
/** True when Claude Code harness is routed to DeepSeek (cheap path). */
|
|
@@ -114,22 +124,35 @@ function resolveClaudeDs(): { binary: string; mode: string } | null {
|
|
|
114
124
|
return null;
|
|
115
125
|
}
|
|
116
126
|
|
|
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] };
|
|
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);
|
|
137
|
+
}
|
|
138
|
+
|
|
117
139
|
/**
|
|
118
140
|
* Env that must reach stock `claude` for DeepSeek routing.
|
|
119
141
|
* Passed via process/tmux env — never interpolated into the printed command.
|
|
120
142
|
*/
|
|
121
|
-
function deepSeekWorkerEnv(): Record<string, string> | undefined {
|
|
143
|
+
function deepSeekWorkerEnv(modelId: string): Record<string, string> | undefined {
|
|
122
144
|
const base = resolvedDeepSeekBaseUrl();
|
|
123
145
|
if (!base) return undefined;
|
|
124
|
-
const env: Record<string, string> = {
|
|
146
|
+
const env: Record<string, string> = {
|
|
147
|
+
ANTHROPIC_BASE_URL: base,
|
|
148
|
+
ANTHROPIC_MODEL: modelId,
|
|
149
|
+
};
|
|
125
150
|
const token =
|
|
126
151
|
process.env.ANTHROPIC_AUTH_TOKEN ||
|
|
127
152
|
process.env.ANTHROPIC_API_KEY ||
|
|
128
153
|
process.env.DEEPSEEK_API_KEY ||
|
|
129
154
|
"";
|
|
130
155
|
if (token) env.ANTHROPIC_AUTH_TOKEN = token;
|
|
131
|
-
const model = process.env.ANTHROPIC_MODEL;
|
|
132
|
-
if (model) env.ANTHROPIC_MODEL = model;
|
|
133
156
|
return env;
|
|
134
157
|
}
|
|
135
158
|
|
|
@@ -153,10 +176,10 @@ export const claudeDsAdapter: Adapter = {
|
|
|
153
176
|
worker: "claude-ds",
|
|
154
177
|
ok: true,
|
|
155
178
|
binary: resolved.binary,
|
|
156
|
-
detail: `ok (${resolved.mode})`,
|
|
179
|
+
detail: `ok (${resolved.mode}; default model ${DS_MODEL_IDS[config.defaultDsModel]})`,
|
|
157
180
|
};
|
|
158
181
|
},
|
|
159
|
-
buildLaunch({ promptFile, cwd, alwaysApprove }) {
|
|
182
|
+
buildLaunch({ promptFile, cwd, alwaysApprove, model, modelId }) {
|
|
160
183
|
const resolved = resolveClaudeDs();
|
|
161
184
|
if (!resolved) {
|
|
162
185
|
throw new Error("DeepSeek worker not available — run: cursor-route health");
|
|
@@ -164,16 +187,37 @@ export const claudeDsAdapter: Adapter = {
|
|
|
164
187
|
|
|
165
188
|
const ask = process.env.CURSOR_ROUTE_ASK === "1" || process.env.CLAUDE_DS_ASK === "1";
|
|
166
189
|
const skip = alwaysApprove && !ask;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
resolved.mode.startsWith("claude
|
|
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;
|
|
171
213
|
|
|
172
|
-
if (
|
|
214
|
+
if (isShim) {
|
|
173
215
|
const parts = [
|
|
174
216
|
shellQuote(resolved.binary),
|
|
175
217
|
"-PromptFile",
|
|
176
218
|
shellQuote(promptFile),
|
|
219
|
+
"-Model",
|
|
220
|
+
shellQuote(choice.id),
|
|
177
221
|
];
|
|
178
222
|
if (skip) parts.push("--dangerously-skip-permissions");
|
|
179
223
|
return {
|
|
@@ -184,10 +228,13 @@ export const claudeDsAdapter: Adapter = {
|
|
|
184
228
|
};
|
|
185
229
|
}
|
|
186
230
|
|
|
231
|
+
// Stock claude → DeepSeek
|
|
187
232
|
const parts = [
|
|
188
233
|
shellQuote(resolved.binary),
|
|
189
234
|
"-p",
|
|
190
235
|
`"$(cat ${shellQuote(promptFile)})"`,
|
|
236
|
+
"--model",
|
|
237
|
+
shellQuote(choice.id),
|
|
191
238
|
];
|
|
192
239
|
if (skip) parts.push("--dangerously-skip-permissions");
|
|
193
240
|
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,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 / Anthropic escape hatch). */
|
|
28
|
+
model?: DsModelAlias;
|
|
29
|
+
/** Concrete DeepSeek model id for -Model/--model (preserves pro[1m]). */
|
|
30
|
+
modelId?: string;
|
|
27
31
|
}): LaunchPlan;
|
|
28
32
|
}
|
package/src/cli.test.ts
CHANGED
|
@@ -1,10 +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
|
-
import { isDeepSeekRouted, isDeepSeekBaseUrl } from "./adapters/claude-ds.ts";
|
|
10
|
+
import { isDeepSeekRouted, isDeepSeekBaseUrl, claudeDsAdapter } from "./adapters/claude-ds.ts";
|
|
11
|
+
import { deepseekAdapter } from "./adapters/deepseek.ts";
|
|
12
|
+
import { grokAdapter } from "./adapters/grok.ts";
|
|
8
13
|
|
|
9
14
|
describe("resolveWorker", () => {
|
|
10
15
|
test("lane easy → openrouter", () => {
|
|
@@ -29,6 +34,205 @@ describe("resolveWorker", () => {
|
|
|
29
34
|
});
|
|
30
35
|
});
|
|
31
36
|
|
|
37
|
+
describe("resolveDsModel", () => {
|
|
38
|
+
test("defaults to flash", () => {
|
|
39
|
+
expect(resolveDsModel()).toEqual({ alias: "flash", id: "deepseek-v4-flash" });
|
|
40
|
+
expect(resolveDsModelAlias("")).toBe("flash");
|
|
41
|
+
});
|
|
42
|
+
test("accepts aliases and full ids", () => {
|
|
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
|
+
});
|
|
49
|
+
});
|
|
50
|
+
test("rejects unknown", () => {
|
|
51
|
+
expect(() => resolveDsModel("opus")).toThrow(/flash\|pro/);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("claude-ds -Model", () => {
|
|
56
|
+
test("shim path passes -Model deepseek-v4-flash by default", () => {
|
|
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;
|
|
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;
|
|
63
|
+
try {
|
|
64
|
+
const plan = claudeDsAdapter.buildLaunch({
|
|
65
|
+
promptFile: "/tmp/p.prompt",
|
|
66
|
+
cwd: "/tmp",
|
|
67
|
+
alwaysApprove: true,
|
|
68
|
+
});
|
|
69
|
+
expect(plan.command).toContain("-Model");
|
|
70
|
+
expect(plan.command).toContain("deepseek-v4-flash");
|
|
71
|
+
} finally {
|
|
72
|
+
if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
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;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
test("pro alias maps to deepseek-v4-pro", () => {
|
|
81
|
+
const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
82
|
+
process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
|
|
83
|
+
try {
|
|
84
|
+
const plan = claudeDsAdapter.buildLaunch({
|
|
85
|
+
promptFile: "/tmp/p.prompt",
|
|
86
|
+
cwd: "/tmp",
|
|
87
|
+
alwaysApprove: true,
|
|
88
|
+
model: "pro",
|
|
89
|
+
});
|
|
90
|
+
expect(plan.command).toContain("deepseek-v4-pro");
|
|
91
|
+
expect(plan.command).not.toContain("deepseek-v4-flash");
|
|
92
|
+
} finally {
|
|
93
|
+
if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
94
|
+
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
|
|
95
|
+
}
|
|
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
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("deepseek adapter slot", () => {
|
|
224
|
+
test("health is not ok and buildLaunch throws", () => {
|
|
225
|
+
expect(deepseekAdapter.health().ok).toBe(false);
|
|
226
|
+
expect(() =>
|
|
227
|
+
deepseekAdapter.buildLaunch({
|
|
228
|
+
promptFile: "/tmp/p",
|
|
229
|
+
cwd: "/tmp",
|
|
230
|
+
alwaysApprove: true,
|
|
231
|
+
}),
|
|
232
|
+
).toThrow(/not available/);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
32
236
|
describe("util", () => {
|
|
33
237
|
test("shellQuote", () => {
|
|
34
238
|
expect(shellQuote("a b")).toBe("'a b'");
|
|
@@ -86,11 +290,26 @@ describe("health", () => {
|
|
|
86
290
|
test("returns structured report", () => {
|
|
87
291
|
const r = runHealth();
|
|
88
292
|
expect(r.product).toBe("cursor-route");
|
|
293
|
+
expect(r.version).toBe("0.1.7");
|
|
89
294
|
expect(r.checks.length).toBeGreaterThan(3);
|
|
90
295
|
expect(r.checks.some((c) => c.name === "tmux")).toBe(true);
|
|
91
296
|
expect(r.checks.some((c) => c.name === "cursor_cli")).toBe(true);
|
|
92
297
|
});
|
|
93
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
|
+
|
|
94
313
|
test("relaxed env can pass without workers", () => {
|
|
95
314
|
const prev = process.env.CURSOR_ROUTE_RELAXED;
|
|
96
315
|
process.env.CURSOR_ROUTE_RELAXED = "1";
|