cursor-route 0.1.7 → 0.1.9
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 +22 -0
- package/CONTRIBUTING.md +1 -0
- package/README.md +52 -10
- package/SUPPORT.md +1 -0
- package/dist/adapters/claude-ds.js +18 -0
- package/dist/adapters/deepseek.js +127 -8
- package/dist/cli.js +13 -8
- package/dist/config.js +5 -2
- package/dist/health.js +16 -0
- package/dist/jobs.js +29 -2
- package/docs/DEMO_GIF.md +17 -1
- package/docs/briefs/WORKING.md +16 -7
- package/docs/demo-notes.md +6 -3
- package/docs/fixtures/generate-hero-demo.sh +109 -0
- package/docs/fixtures/hero-demo.log +53 -0
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/skills/route-orch/SKILL.md +45 -8
- package/src/adapters/claude-ds.ts +19 -0
- package/src/adapters/deepseek.ts +145 -11
- package/src/adapters/types.ts +3 -1
- package/src/cli.test.ts +465 -27
- package/src/cli.ts +13 -7
- package/src/config.ts +9 -3
- package/src/health.ts +21 -0
- package/src/jobs.ts +54 -4
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# generate-hero-demo.sh — regenerate docs/fixtures/hero-demo.log
|
|
3
|
+
#
|
|
4
|
+
# Dry-run only: no live Grok, no worker spawned, no secrets. Jobs dir is
|
|
5
|
+
# sandboxed (trap-cleaned). Output is path-scrubbed + id-normalized so the
|
|
6
|
+
# committed fixture is machine-agnostic and regenerates stably.
|
|
7
|
+
#
|
|
8
|
+
# Prerequisites: Bun or Node 20+; runnable ./bin/cursor-route
|
|
9
|
+
# (git clone: bun install; npm global install ships dist/).
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
13
|
+
BIN="$ROOT/bin/cursor-route"
|
|
14
|
+
OUT="$ROOT/docs/fixtures/hero-demo.log"
|
|
15
|
+
|
|
16
|
+
if [[ ! -f "$BIN" ]]; then
|
|
17
|
+
echo "missing $BIN — run from a cursor-route checkout (need bun/node 20+)" >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Neutralize inherited overrides that would leak private paths into the log.
|
|
22
|
+
unset CURSOR_ROUTE_CLAUDE_DS_BIN CURSOR_ROUTE_GROK_BIN CURSOR_ROUTE_DSH_BIN \
|
|
23
|
+
CURSOR_ROUTE_ALLOW_ANTHROPIC \
|
|
24
|
+
CURSOR_ROUTE_ALLOW_STOCK_CLAUDE CURSOR_ROUTE_ANTHROPIC_BASE_URL CURSOR_ROUTE_DS_MODEL \
|
|
25
|
+
CURSOR_ROUTE_OPENROUTER_MODEL OPENROUTER_API_KEY OPENROUTER_BASE_URL \
|
|
26
|
+
ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN ANTHROPIC_API_KEY ANTHROPIC_MODEL \
|
|
27
|
+
DEEPSEEK_API_KEY XAI_API_KEY 2>/dev/null || true
|
|
28
|
+
|
|
29
|
+
TMP="$(mktemp -d)"
|
|
30
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
31
|
+
export CURSOR_ROUTE_JOBS_DIR="$TMP/.demo-jobs"
|
|
32
|
+
# Pin dsh to a missing path so worker:deepseek renders identically on machines
|
|
33
|
+
# that do have a real dsh installed (the adapter shows the install hint, no path).
|
|
34
|
+
export CURSOR_ROUTE_DSH_BIN="$TMP/no-such-dsh"
|
|
35
|
+
|
|
36
|
+
RAW="$TMP/hero-demo.raw"
|
|
37
|
+
NORM="$TMP/hero-demo.norm"
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
echo "\$ cursor-route --version"
|
|
41
|
+
"$BIN" --version
|
|
42
|
+
echo
|
|
43
|
+
|
|
44
|
+
echo "\$ CURSOR_ROUTE_RELAXED=1 cursor-route health"
|
|
45
|
+
CURSOR_ROUTE_RELAXED=1 "$BIN" health
|
|
46
|
+
echo
|
|
47
|
+
|
|
48
|
+
echo "\$ cursor-route start --lane mid --model flash --dry-run \"Add a unit test for shellQuote\""
|
|
49
|
+
"$BIN" start --lane mid --model flash --dry-run "Add a unit test for shellQuote"
|
|
50
|
+
echo
|
|
51
|
+
|
|
52
|
+
echo "\$ cursor-route start --lane easy --dry-run \"Rewrite this FAQ answer in 3 sentences\""
|
|
53
|
+
"$BIN" start --lane easy --dry-run "Rewrite this FAQ answer in 3 sentences"
|
|
54
|
+
echo
|
|
55
|
+
|
|
56
|
+
echo "\$ cursor-route start --lane hard --dry-run \"Refactor auth module; run tests; report verify evidence\""
|
|
57
|
+
"$BIN" start --lane hard --dry-run "Refactor auth module; run tests; report verify evidence"
|
|
58
|
+
echo
|
|
59
|
+
|
|
60
|
+
echo "\$ cursor-route start --lane mid --dry-run --json \"Add a failing test then make it pass\""
|
|
61
|
+
"$BIN" start --lane mid --dry-run --json "Add a failing test then make it pass"
|
|
62
|
+
echo
|
|
63
|
+
|
|
64
|
+
echo "\$ cursor-route jobs --json"
|
|
65
|
+
"$BIN" jobs --json
|
|
66
|
+
} > "$RAW"
|
|
67
|
+
|
|
68
|
+
jobs_display="$HOME/.local/share/cursor-route/jobs"
|
|
69
|
+
{
|
|
70
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
71
|
+
line="${line//$TMP\/.demo-jobs/$jobs_display}"
|
|
72
|
+
line="${line//$ROOT/~/Projects/cursor-route}"
|
|
73
|
+
line="${line//$HOME/~}"
|
|
74
|
+
line="$(printf '%s' "$line" | sed -E \
|
|
75
|
+
-e 's#/Users/[^/\"'\'' ]+#~#g' \
|
|
76
|
+
-e 's#/home/[^/\"'\'' ]+#~#g' \
|
|
77
|
+
-e 's#/opt/cemini[^\"'\'' ]*#~#g' \
|
|
78
|
+
-e 's#/var/folders/[^\"'\'' ]+#/tmp#g')"
|
|
79
|
+
printf '%s\n' "$line"
|
|
80
|
+
done
|
|
81
|
+
} < "$RAW" > "$NORM"
|
|
82
|
+
|
|
83
|
+
python3 - "$NORM" "$OUT" <<'PY'
|
|
84
|
+
import re, sys
|
|
85
|
+
src, dst = sys.argv[1], sys.argv[2]
|
|
86
|
+
text = open(src, encoding="utf-8").read()
|
|
87
|
+
seq = ["a1b2c3d4", "b2c3d4e5", "c3d4e5f6", "d4e5f6a7"]
|
|
88
|
+
seen: dict[str, str] = {}
|
|
89
|
+
|
|
90
|
+
def take(jid: str) -> str:
|
|
91
|
+
if jid not in seen:
|
|
92
|
+
seen[jid] = seq[len(seen)] if len(seen) < len(seq) else f"{len(seen):08x}"
|
|
93
|
+
return seen[jid]
|
|
94
|
+
|
|
95
|
+
out = []
|
|
96
|
+
for line in text.splitlines(True):
|
|
97
|
+
def sub(m: re.Match[str]) -> str:
|
|
98
|
+
return m.group(1) + take(m.group(2)) + m.group(3)
|
|
99
|
+
|
|
100
|
+
line = re.sub(r"(dry-run job )([0-9a-f]{8})(\b)", sub, line)
|
|
101
|
+
line = re.sub(r'("id": ")([0-9a-f]{8})(")', sub, line)
|
|
102
|
+
line = re.sub(r"(jobs/)([0-9a-f]{8})(\.prompt)", sub, line)
|
|
103
|
+
line = re.sub(r"(headless-|cursor-route-)([0-9a-f]{8})(\b)", sub, line)
|
|
104
|
+
line = re.sub(r'("createdAt": ")[^"]+(")', r"\g<1>2026-08-13T00:00:00.000Z\2", line)
|
|
105
|
+
out.append(line)
|
|
106
|
+
open(dst, "w", encoding="utf-8").write("".join(out))
|
|
107
|
+
PY
|
|
108
|
+
|
|
109
|
+
echo "wrote $OUT"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
$ cursor-route --version
|
|
2
|
+
0.1.9
|
|
3
|
+
|
|
4
|
+
$ CURSOR_ROUTE_RELAXED=1 cursor-route health
|
|
5
|
+
cursor-route v0.1.9
|
|
6
|
+
health: OK
|
|
7
|
+
|
|
8
|
+
✓ tmux ok
|
|
9
|
+
✓ runtime bun ok
|
|
10
|
+
✓ script(1) ok (tty log capture)
|
|
11
|
+
✓ worker:grok ok (auth checked at first start — run grok login if jobs fail) @ ~/.grok/bin/grok
|
|
12
|
+
✓ worker:claude-ds ok (claude-ds (DeepSeek shim); default model deepseek-v4-flash) @ ~/.local/bin/claude-ds
|
|
13
|
+
✗ worker:openrouter OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane model defaults to openrouter/free) @ node '~/Projects/cursor-route/dist/openrouter-run.js'
|
|
14
|
+
✗ worker:deepseek dsh (@deepseek-ai/dsh) not found — install: npm i -g @deepseek-ai/dsh. Mid default remains claude-ds.
|
|
15
|
+
✓ lane:mid DeepSeek proven (claude-ds (DeepSeek shim))
|
|
16
|
+
✓ cursor_cli optional ok (agent on PATH) — v0 supervisor is Cursor skill, not CLI
|
|
17
|
+
✓ relaxed CURSOR_ROUTE_RELAXED=1 — tmux optional (headless OK)
|
|
18
|
+
✓ jobs_dir ~/.local/share/cursor-route/jobs
|
|
19
|
+
|
|
20
|
+
$ cursor-route start --lane mid --model flash --dry-run "Add a unit test for shellQuote"
|
|
21
|
+
dry-run job a1b2c3d4
|
|
22
|
+
worker: claude-ds
|
|
23
|
+
model: flash
|
|
24
|
+
command: cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~/.local/share/cursor-route/jobs/a1b2c3d4.prompt' -Model 'deepseek-v4-flash' --dangerously-skip-permissions
|
|
25
|
+
|
|
26
|
+
$ cursor-route start --lane easy --dry-run "Rewrite this FAQ answer in 3 sentences"
|
|
27
|
+
dry-run job b2c3d4e5
|
|
28
|
+
worker: openrouter
|
|
29
|
+
command: node '~/Projects/cursor-route/dist/openrouter-run.js' --prompt-file '~/.local/share/cursor-route/jobs/b2c3d4e5.prompt'
|
|
30
|
+
|
|
31
|
+
$ cursor-route start --lane hard --dry-run "Refactor auth module; run tests; report verify evidence"
|
|
32
|
+
dry-run job c3d4e5f6
|
|
33
|
+
worker: grok
|
|
34
|
+
command: '~/.grok/bin/grok' -p "$(cat '~/.local/share/cursor-route/jobs/c3d4e5f6.prompt')" --cwd '~/Projects/cursor-route' --no-auto-update --output-format plain --always-approve
|
|
35
|
+
|
|
36
|
+
$ cursor-route start --lane mid --dry-run --json "Add a failing test then make it pass"
|
|
37
|
+
{
|
|
38
|
+
"id": "d4e5f6a7",
|
|
39
|
+
"schema": "cursor-route.job.v1",
|
|
40
|
+
"status": "pending",
|
|
41
|
+
"worker": "claude-ds",
|
|
42
|
+
"lane": "mid",
|
|
43
|
+
"model": "flash",
|
|
44
|
+
"prompt": "Add a failing test then make it pass",
|
|
45
|
+
"cwd": "~/Projects/cursor-route",
|
|
46
|
+
"alwaysApprove": true,
|
|
47
|
+
"tmuxSession": "cursor-route-d4e5f6a7",
|
|
48
|
+
"createdAt": "2026-08-13T00:00:00.000Z",
|
|
49
|
+
"command": "cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~/.local/share/cursor-route/jobs/d4e5f6a7.prompt' -Model 'deepseek-v4-flash' --dangerously-skip-permissions"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
$ cursor-route jobs --json
|
|
53
|
+
[]
|
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 (latest **0.1.
|
|
5
|
+
MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.9**)
|
|
6
6
|
GitHub: https://github.com/cemini23/cursor-route
|
|
7
7
|
|
|
8
8
|
## FAQ
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-route",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|
|
@@ -18,7 +18,7 @@ You are the **orchestrator**. Do **not** implement bulk code in this Cursor sess
|
|
|
18
18
|
- Mid/hard implementation that should run on a subscription worker (Grok CLI / claude-ds)
|
|
19
19
|
- Multi-file investigation that benefits from parallel panes
|
|
20
20
|
|
|
21
|
-
**Do not steal federation `/route`.** Private Cemini `/route` (route-task →
|
|
21
|
+
**Do not steal federation `/route`.** Private Cemini `/route` (route-task → verify → Grok/claude-ds chain) is a different skill. This public skill only drives the `cursor-route` CLI.
|
|
22
22
|
|
|
23
23
|
## Lanes (public core)
|
|
24
24
|
|
|
@@ -46,10 +46,23 @@ cursor-route start --lane mid --model pro --dir "$PWD" "…"
|
|
|
46
46
|
|
|
47
47
|
If `worker:grok` is ✗ on health, that is usually **auth** (`grok login` / `XAI_API_KEY`) — not the Pro stand-in case.
|
|
48
48
|
|
|
49
|
+
## Experimental: --worker deepseek (dsh)
|
|
50
|
+
|
|
51
|
+
Official DeepSeek Harness headless as an opt-in worker — **not the mid default** (mid stays `claude-ds`).
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm i -g @deepseek-ai/dsh
|
|
55
|
+
export DEEPSEEK_API_KEY=... # platform.deepseek.com
|
|
56
|
+
cursor-route start --worker deepseek --dir "$PWD" "…"
|
|
57
|
+
cursor-route start --worker deepseek --model pro --dir "$PWD" "…" # --model applies here too
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Health ✓ needs `dsh` on PATH and `DEEPSEEK_API_KEY` set. The adapter pins `--model` via a per-job `--patch` (never touches `~/.dsh/settings.yaml`); always-approve → `DSH_PERMISSION_MODE=danger-full-access`, `--ask` → `workspace-write`. The key never enters the command or patch.
|
|
61
|
+
|
|
49
62
|
## Workflow
|
|
50
63
|
|
|
51
|
-
1. Run `cursor-route health` (or `CURSOR_ROUTE_RELAXED=1` for headless). If the **target worker** is unhealthy, fix before spawning.
|
|
52
|
-
2. Write a clear handoff prompt with **
|
|
64
|
+
1. Run `cursor-route health` (or `CURSOR_ROUTE_RELAXED=1` for headless). If the **target worker** is unhealthy, fix before spawning. If targeting **mid**, require `lane:mid` ✓ (or health JSON `lanes.mid.deepseek`) before spawn — `CURSOR_ROUTE_ALLOW_ANTHROPIC=1` is not DeepSeek proof.
|
|
65
|
+
2. Write a clear handoff prompt with **Success criteria** + **Verify** + **NEVER** (no secrets, no LIVE Discord).
|
|
53
66
|
3. Spawn:
|
|
54
67
|
|
|
55
68
|
```bash
|
|
@@ -57,24 +70,48 @@ cursor-route start --lane hard --dir "$PWD" "$(cat <<'EOF'
|
|
|
57
70
|
## Task
|
|
58
71
|
...
|
|
59
72
|
|
|
73
|
+
## Success criteria
|
|
74
|
+
- [ ] ...
|
|
75
|
+
|
|
60
76
|
## Verify
|
|
61
77
|
- [ ] ...
|
|
78
|
+
|
|
79
|
+
## NEVER
|
|
80
|
+
- ...
|
|
62
81
|
EOF
|
|
63
82
|
)"
|
|
64
83
|
```
|
|
65
84
|
|
|
66
|
-
Or `--worker grok` / `--worker claude-ds` / `--worker openrouter` (or `--lane easy`). Use `--no-tmux` only when tmux is unavailable.
|
|
85
|
+
Or `--worker grok` / `--worker claude-ds` / `--worker deepseek` (experimental) / `--worker openrouter` (or `--lane easy`). Use `--no-tmux` only when tmux is unavailable.
|
|
67
86
|
|
|
68
87
|
4. Monitor: `cursor-route jobs --json` · `cursor-route capture <id>` · `cursor-route send <id> "…"` (tmux only).
|
|
69
|
-
5. Summarize worker results with **verify evidence** — no status-only “done
|
|
88
|
+
5. Summarize worker results with **verify evidence** — no status-only “done” (see Verify / claim closeout). If verify fails, reconsider the plan/definition (not only retry) — `send` a correction or spawn a follow-up; do not invent success.
|
|
89
|
+
|
|
90
|
+
## Verify / claim closeout
|
|
91
|
+
|
|
92
|
+
Verify criteria are an **external eval contract** (AutoDesign pattern), fixed by the parent — not a checklist the worker may rewrite:
|
|
93
|
+
|
|
94
|
+
- Workers must **not rewrite Success criteria / Verify** to claim done
|
|
95
|
+
- Parent closeout is an **evidence tree**: report **spawn** (job id, worker, lane, model) + **execute** (status, exit) + **verify** (`capture` excerpt / exit). A single “done” scalar is not enough.
|
|
96
|
+
- Parent closes a job only on **capture / exit evidence** (`cursor-route capture <id>`, job exit status)
|
|
97
|
+
- `cursor-route status --json` `.evidence.verify.claim` stays `"unverified"` until the parent reads capture
|
|
98
|
+
- **activity ≠ verification** — busy panes, many tool calls, or long transcripts do not make a claim true
|
|
99
|
+
|
|
100
|
+
## Eval & skill hygiene
|
|
101
|
+
|
|
102
|
+
- **External eval contract (AutoDesign):** do not rewrite Verify / Success criteria mid-run to make a failing job look green — capture + exit status are the contract (see Verify / claim closeout).
|
|
103
|
+
- **Skill misevolution:** do not auto-edit `route-orch` or promote skill variants from worker trajectories without operator HITL — write-time approval ≠ safe retrieval later.
|
|
104
|
+
- **On verify fail:** prefer reconsidering the plan/definition (wrong approach) over grinding the same tactic; attribute failure to stage when possible (spawn vs execute vs verify).
|
|
70
105
|
|
|
71
106
|
## Always-approve
|
|
72
107
|
|
|
73
|
-
Defaults on for workers. Opt out: `cursor-route start … --ask` or `CURSOR_ROUTE_ASK=1`.
|
|
108
|
+
Defaults on for workers. Opt out: `cursor-route start … --ask` or `CURSOR_ROUTE_ASK=1`. Always-approve is for **coding worktrees only** — it does not authorize LIVE Discord, trading, or irreversible SaaS.
|
|
74
109
|
|
|
75
110
|
## Anti-patterns
|
|
76
111
|
|
|
77
112
|
- Do not paste API keys / private keys into prompts or `send`
|
|
78
|
-
- Do not claim DeepSeek
|
|
79
|
-
- Do not
|
|
113
|
+
- Do not claim the official DeepSeek harness (`@deepseek-ai/dsh`) is the mid default — `--worker deepseek` is an opt-in experiment (cheap to abandon), not a product fork; mid stays **claude-ds**
|
|
114
|
+
- Do not fork a second mid harness
|
|
115
|
+
- Do not open-source or dump private cemini `agent-toolkit` paths into public handoffs
|
|
80
116
|
- Do not mark done without reading `capture` / exit status
|
|
117
|
+
- When editing this skill itself, treat changes as **skill-evolution** — do not auto-promote harmful instructions; prefer **HITL** (no unattended promote from worker trajectories)
|
|
@@ -124,6 +124,25 @@ function resolveClaudeDs(): { binary: string; mode: string } | null {
|
|
|
124
124
|
return null;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Mid lane is proven DeepSeek only via shim (`claude-ds` / `deepseek-claude`)
|
|
129
|
+
* or stock `claude` routed to DeepSeek. The Anthropic escape hatch is not proof.
|
|
130
|
+
*/
|
|
131
|
+
export function isMidDeepSeekProven(): boolean {
|
|
132
|
+
const resolved = resolveClaudeDs();
|
|
133
|
+
if (!resolved) return false;
|
|
134
|
+
return !resolved.mode.startsWith("claude → Anthropic");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Human detail for health `lane:mid` (mode when proven; install hint otherwise). */
|
|
138
|
+
export function midDeepSeekProofDetail(): string {
|
|
139
|
+
const resolved = resolveClaudeDs();
|
|
140
|
+
if (!resolved || resolved.mode.startsWith("claude → Anthropic")) {
|
|
141
|
+
return "mid not proven DeepSeek — install claude-ds / set ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic. CURSOR_ROUTE_ALLOW_ANTHROPIC=1 is not proof.";
|
|
142
|
+
}
|
|
143
|
+
return `DeepSeek proven (${resolved.mode})`;
|
|
144
|
+
}
|
|
145
|
+
|
|
127
146
|
function pickModel(requested?: DsModelAlias, modelId?: string): { alias: DsModelAlias; id: string } {
|
|
128
147
|
if (modelId) {
|
|
129
148
|
const alias = requested ?? resolveDsModel(modelId).alias;
|
package/src/adapters/deepseek.ts
CHANGED
|
@@ -1,24 +1,158 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, writeFileSync, unlinkSync } from "node:fs";
|
|
1
3
|
import type { Adapter, WorkerHealth } from "./types.ts";
|
|
4
|
+
import { shellQuote } from "../util.ts";
|
|
5
|
+
import {
|
|
6
|
+
DS_MODEL_IDS,
|
|
7
|
+
resolveDsModel,
|
|
8
|
+
type DsModelAlias,
|
|
9
|
+
} from "../config.ts";
|
|
2
10
|
|
|
3
11
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
12
|
+
* Experimental official DeepSeek Harness (`dsh`, npm @deepseek-ai/dsh) as a
|
|
13
|
+
* coding worker — `dsh --profile headless` with a per-job Cordis patch that
|
|
14
|
+
* pins the model. Mid lane stays on claude-ds; this is an opt-in worker only
|
|
15
|
+
* (`--worker deepseek`), not a mid replacement.
|
|
16
|
+
*
|
|
17
|
+
* We never write ~/.dsh/settings.yaml (parallel jobs would race) and never
|
|
18
|
+
* put DEEPSEEK_API_KEY in the command or patch — the key travels via plan.env.
|
|
6
19
|
*/
|
|
20
|
+
|
|
21
|
+
function findDsh(): string | null {
|
|
22
|
+
// Env override lets tests pin a fake dsh — but it must exist, so a stale
|
|
23
|
+
// override cannot pass health with a dangling path.
|
|
24
|
+
const override = process.env.CURSOR_ROUTE_DSH_BIN;
|
|
25
|
+
if (override) return existsSync(override) ? override : null;
|
|
26
|
+
try {
|
|
27
|
+
return (
|
|
28
|
+
execSync("command -v dsh", {
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
31
|
+
// Bun may ignore mutated process.env.PATH unless env is passed explicitly
|
|
32
|
+
env: { ...process.env },
|
|
33
|
+
}).trim() || null
|
|
34
|
+
);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Same resolution as claude-ds: passed model/modelId wins, else env, else Flash. */
|
|
41
|
+
function pickModel(
|
|
42
|
+
requested?: DsModelAlias,
|
|
43
|
+
modelId?: string,
|
|
44
|
+
): { alias: DsModelAlias; id: string } {
|
|
45
|
+
if (modelId) {
|
|
46
|
+
const alias = requested ?? resolveDsModel(modelId).alias;
|
|
47
|
+
return { alias, id: modelId };
|
|
48
|
+
}
|
|
49
|
+
if (requested) {
|
|
50
|
+
return { alias: requested, id: DS_MODEL_IDS[requested] };
|
|
51
|
+
}
|
|
52
|
+
// Env default (startJob normally resolves this; kept for direct buildLaunch callers)
|
|
53
|
+
return resolveDsModel(process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whitelist model ids before interpolating into YAML (no newlines / injection). */
|
|
57
|
+
function assertPatchModelId(modelId: string): string {
|
|
58
|
+
if (!/^[a-z0-9][a-z0-9.\-[\]]*$/i.test(modelId)) {
|
|
59
|
+
throw new Error(`Invalid DeepSeek model id for dsh patch: ${modelId}`);
|
|
60
|
+
}
|
|
61
|
+
return modelId;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Per-job Cordis patch path. Never reuse the prompt path (would overwrite it). */
|
|
65
|
+
export function patchPathForPrompt(promptFile: string): string {
|
|
66
|
+
return promptFile.endsWith(".prompt")
|
|
67
|
+
? promptFile.replace(/\.prompt$/, ".dsh-patch.yml")
|
|
68
|
+
: `${promptFile}.dsh-patch.yml`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Per-job Cordis patch (whole-row replace). `name` is required or dsh silently skips. */
|
|
72
|
+
function patchYaml(modelId: string): string {
|
|
73
|
+
const id = assertPatchModelId(modelId);
|
|
74
|
+
return [
|
|
75
|
+
"- id: agent-default-model",
|
|
76
|
+
" name: '@deepseek-ai/dsh-agent-default-model'",
|
|
77
|
+
" config:",
|
|
78
|
+
" provider: deepseek-official",
|
|
79
|
+
` model: '${id}'`,
|
|
80
|
+
].join("\n") + "\n";
|
|
81
|
+
}
|
|
82
|
+
|
|
7
83
|
export const deepseekAdapter: Adapter = {
|
|
8
84
|
kind: "deepseek",
|
|
9
|
-
label: "Official DeepSeek
|
|
85
|
+
label: "Official DeepSeek Harness (dsh)",
|
|
10
86
|
health(): WorkerHealth {
|
|
87
|
+
const binary = findDsh();
|
|
88
|
+
if (!binary) {
|
|
89
|
+
return {
|
|
90
|
+
worker: "deepseek",
|
|
91
|
+
ok: false,
|
|
92
|
+
binary: null,
|
|
93
|
+
detail:
|
|
94
|
+
"dsh (@deepseek-ai/dsh) not found — install: npm i -g @deepseek-ai/dsh. Mid default remains claude-ds.",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (!process.env.DEEPSEEK_API_KEY) {
|
|
98
|
+
return {
|
|
99
|
+
worker: "deepseek",
|
|
100
|
+
ok: false,
|
|
101
|
+
binary,
|
|
102
|
+
detail:
|
|
103
|
+
"DEEPSEEK_API_KEY not set — export your DeepSeek API key to use dsh (@deepseek-ai/dsh). Mid default remains claude-ds.",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
11
106
|
return {
|
|
12
107
|
worker: "deepseek",
|
|
13
|
-
ok:
|
|
14
|
-
binary
|
|
15
|
-
detail:
|
|
16
|
-
"unreleased — mid lane uses claude-ds (DeepSeek behind Claude Code). See README.",
|
|
108
|
+
ok: true,
|
|
109
|
+
binary,
|
|
110
|
+
detail: "ok (dsh @deepseek-ai/dsh headless; mid default remains claude-ds)",
|
|
17
111
|
};
|
|
18
112
|
},
|
|
19
|
-
buildLaunch() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
);
|
|
113
|
+
buildLaunch({ promptFile, cwd, alwaysApprove, model, modelId, dryRun }) {
|
|
114
|
+
// Missing dsh is tolerated here so `--dry-run` can still print the command;
|
|
115
|
+
// real starts are gated by the health preflight (binary + DEEPSEEK_API_KEY).
|
|
116
|
+
const binary = findDsh() || "dsh";
|
|
117
|
+
const choice = pickModel(model, modelId);
|
|
118
|
+
|
|
119
|
+
// Per-job patch next to the prompt file (never touch ~/.dsh/settings.yaml).
|
|
120
|
+
const patchFile = patchPathForPrompt(promptFile);
|
|
121
|
+
writeFileSync(patchFile, patchYaml(choice.id), { mode: 0o600 });
|
|
122
|
+
if (dryRun) {
|
|
123
|
+
// Dry-run keeps no durable artifacts (jobs.ts removes the prompt likewise).
|
|
124
|
+
try {
|
|
125
|
+
unlinkSync(patchFile);
|
|
126
|
+
} catch {
|
|
127
|
+
/* ignore */
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Launcher flags before the task; prompt inlined via cat (never the key).
|
|
132
|
+
const parts = [
|
|
133
|
+
shellQuote(binary),
|
|
134
|
+
"--profile",
|
|
135
|
+
"headless",
|
|
136
|
+
"--patch",
|
|
137
|
+
shellQuote(patchFile),
|
|
138
|
+
`"$(cat ${shellQuote(promptFile)})"`,
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const ask = process.env.CURSOR_ROUTE_ASK === "1";
|
|
142
|
+
const skip = alwaysApprove && !ask;
|
|
143
|
+
const env: Record<string, string> = {
|
|
144
|
+
DSH_PERMISSION_MODE: skip ? "danger-full-access" : "workspace-write",
|
|
145
|
+
};
|
|
146
|
+
// Key travels via env only — never interpolated into command or patch.
|
|
147
|
+
if (process.env.DEEPSEEK_API_KEY) {
|
|
148
|
+
env.DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
worker: "deepseek",
|
|
153
|
+
command: `cd ${shellQuote(cwd)} && ${parts.join(" ")}`,
|
|
154
|
+
alwaysApprove: skip,
|
|
155
|
+
env,
|
|
156
|
+
};
|
|
23
157
|
},
|
|
24
158
|
};
|
package/src/adapters/types.ts
CHANGED
|
@@ -24,9 +24,11 @@ export interface Adapter {
|
|
|
24
24
|
promptFile: string;
|
|
25
25
|
cwd: string;
|
|
26
26
|
alwaysApprove: boolean;
|
|
27
|
-
/** Mid-lane DeepSeek flash|pro (ignored by
|
|
27
|
+
/** Mid-lane DeepSeek flash|pro (claude-ds + deepseek; ignored by grok/openrouter / Anthropic escape hatch). */
|
|
28
28
|
model?: DsModelAlias;
|
|
29
29
|
/** Concrete DeepSeek model id for -Model/--model (preserves pro[1m]). */
|
|
30
30
|
modelId?: string;
|
|
31
|
+
/** True on --dry-run: adapters may drop artifacts they just wrote (e.g. dsh patch). */
|
|
32
|
+
dryRun?: boolean;
|
|
31
33
|
}): LaunchPlan;
|
|
32
34
|
}
|