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.
@@ -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
- * 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.
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 harness (unreleased)",
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: false,
14
- binary: null,
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
- throw new Error(
21
- "Official DeepSeek harness is not available yet use --lane mid / --worker claude-ds",
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
  };
@@ -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 other workers). */
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
  }