cursor-route 0.1.6 → 0.1.8
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 +40 -0
- package/CONTRIBUTING.md +14 -5
- package/README.md +60 -10
- package/dist/adapters/claude-ds.js +43 -29
- package/dist/adapters/deepseek.js +127 -8
- package/dist/cli.js +37 -13
- package/dist/config.js +48 -12
- package/dist/jobs.js +28 -3
- package/docs/DEMO_GIF.md +19 -2
- package/docs/briefs/WORKING.md +18 -7
- package/docs/demo-notes.md +5 -3
- package/docs/fixtures/generate-hero-demo.sh +109 -0
- package/docs/fixtures/hero-demo.log +52 -0
- package/llms.txt +9 -2
- package/package.json +2 -1
- package/skills/route-orch/SKILL.md +15 -2
- package/src/adapters/claude-ds.ts +45 -27
- package/src/adapters/deepseek.ts +145 -11
- package/src/adapters/types.ts +5 -1
- package/src/cli.test.ts +506 -19
- package/src/cli.ts +41 -13
- package/src/config.ts +59 -10
- package/src/jobs.ts +30 -5
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,7 +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
|
+
/** Concrete DeepSeek model id for -Model/--model (preserves pro[1m]). */
|
|
30
|
+
modelId?: string;
|
|
31
|
+
/** True on --dry-run: adapters may drop artifacts they just wrote (e.g. dsh patch). */
|
|
32
|
+
dryRun?: boolean;
|
|
29
33
|
}): LaunchPlan;
|
|
30
34
|
}
|