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/src/cli.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  config,
9
9
  WORKERS,
10
10
  LANES,
11
- resolveDsModelAlias,
11
+ resolveDsModel,
12
12
  type WorkerKind,
13
13
  type Lane,
14
14
  type DsModelAlias,
@@ -52,9 +52,9 @@ Usage:
52
52
  cursor-route clean [--days N]
53
53
 
54
54
  Start options:
55
- --worker <grok|claude-ds|openrouter> Worker adapter (default: grok; deepseek = reserved slot)
55
+ --worker <grok|claude-ds|openrouter|deepseek> Worker adapter (default: grok; deepseek = experimental official dsh)
56
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
57
+ --model <flash|pro> Mid DeepSeek only (default: flash / CURSOR_ROUTE_DS_MODEL). Parsed for claude-ds + deepseek
58
58
  --dir <path> Working directory (default: cwd)
59
59
  --ask Disable always-approve for this job
60
60
  --dry-run Print launch command; do not start
@@ -66,10 +66,12 @@ Env:
66
66
  CURSOR_ROUTE_JOBS_DIR Override jobs dir (default: ~/.local/share/cursor-route/jobs)
67
67
  CURSOR_ROUTE_MAX_JOBS Max active jobs (default: 50)
68
68
  CURSOR_ROUTE_RELAXED=1 health OK without tmux/workers (CI / infra smoke)
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)
69
+ CURSOR_ROUTE_ALLOW_ANTHROPIC=1 Allow mid-lane on Anthropic Claude (expensive; not default; --model ignored)
70
+ CURSOR_ROUTE_DS_MODEL Default mid model flash|pro (or deepseek-v4-pro[1m]); overridden by --model
71
71
  CURSOR_ROUTE_GROK_BIN Override the grok binary path (tests / power users)
72
72
  CURSOR_ROUTE_CLAUDE_DS_BIN Override the claude-ds binary path (tests / power users)
73
+ CURSOR_ROUTE_DSH_BIN Override the dsh binary path (tests / power users)
74
+ DEEPSEEK_API_KEY DeepSeek API key (required for --worker deepseek)
73
75
  OPENROUTER_API_KEY OpenRouter key (required for --worker openrouter / --lane easy)
74
76
  CURSOR_ROUTE_OPENROUTER_MODEL OpenRouter model (default: openrouter/free)
75
77
  OPENROUTER_BASE_URL OpenRouter API base (default: https://openrouter.ai/api/v1)
@@ -162,10 +164,10 @@ function asLane(v: unknown): Lane | undefined {
162
164
  throw new Error(`Invalid --lane ${v}; expected ${LANES.join("|")}`);
163
165
  }
164
166
 
165
- function asDsModel(v: unknown): DsModelAlias | undefined {
167
+ function asDsModelChoice(v: unknown): { alias: DsModelAlias; id: string } | undefined {
166
168
  if (v === undefined || v === true) return undefined;
167
169
  if (typeof v !== "string") throw new Error(`Invalid --model; expected flash|pro`);
168
- return resolveDsModelAlias(v);
170
+ return resolveDsModel(v);
169
171
  }
170
172
 
171
173
  function refuseSecrets(text: string, context: string): void {
@@ -235,13 +237,35 @@ async function main() {
235
237
  requireStringFlag(f, "model");
236
238
  const promptFile = requireStringFlag(f, "prompt-file");
237
239
  const dirFlag = requireStringFlag(f, "dir");
238
- let model: DsModelAlias | undefined;
240
+
241
+ let worker: WorkerKind | undefined;
242
+ let lane: Lane | undefined;
239
243
  try {
240
- model = asDsModel(f.model);
244
+ worker = asWorker(f.worker);
245
+ lane = asLane(f.lane);
241
246
  } catch (e) {
242
247
  console.error((e as Error).message);
243
248
  process.exit(2);
244
249
  }
250
+ const resolvedWorker =
251
+ worker ?? (lane ? config.laneWorkers[lane] : config.defaultWorker);
252
+
253
+ let model: DsModelAlias | undefined;
254
+ let modelId: string | undefined;
255
+ // --model is DeepSeek-only (claude-ds mid lane + experimental deepseek worker);
256
+ // ignore (do not validate) for other workers
257
+ if (f.model !== undefined && (resolvedWorker === "claude-ds" || resolvedWorker === "deepseek")) {
258
+ try {
259
+ const choice = asDsModelChoice(f.model);
260
+ if (choice) {
261
+ model = choice.alias;
262
+ modelId = choice.id;
263
+ }
264
+ } catch (e) {
265
+ console.error((e as Error).message);
266
+ process.exit(2);
267
+ }
268
+ }
245
269
 
246
270
  let prompt = "";
247
271
  if (promptFile) {
@@ -277,9 +301,10 @@ async function main() {
277
301
 
278
302
  const result = startJob({
279
303
  prompt,
280
- worker: asWorker(f.worker),
281
- lane: asLane(f.lane),
304
+ worker,
305
+ lane,
282
306
  model,
307
+ modelId,
283
308
  cwd,
284
309
  alwaysApprove: !f.ask,
285
310
  dryRun: Boolean(f.dryRun),
@@ -333,8 +358,9 @@ async function main() {
333
358
  } else {
334
359
  for (const j of jobs) {
335
360
  const age = j.startedAt || j.createdAt;
361
+ const modelCol = j.model ? j.model.padEnd(6) : "".padEnd(6);
336
362
  console.log(
337
- `${j.id} ${j.status.padEnd(10)} ${j.worker.padEnd(10)} ${age} ${j.prompt.slice(0, 48).replace(/\n/g, " ")}`,
363
+ `${j.id} ${j.status.padEnd(10)} ${j.worker.padEnd(10)} ${modelCol} ${age} ${j.prompt.slice(0, 48).replace(/\n/g, " ")}`,
338
364
  );
339
365
  }
340
366
  }
@@ -366,7 +392,9 @@ async function main() {
366
392
  const view = { ...job, sessionAlive: alive };
367
393
  if (json) console.log(JSON.stringify(view, null, 2));
368
394
  else {
369
- console.log(`${job.id} ${job.status} worker=${job.worker} sessionAlive=${alive}`);
395
+ console.log(
396
+ `${job.id} ${job.status} worker=${job.worker}${job.model ? ` model=${job.model}` : ""} sessionAlive=${alive}`,
397
+ );
370
398
  if (job.error) console.log(`error: ${job.error}`);
371
399
  }
372
400
  return;
package/src/config.ts CHANGED
@@ -2,13 +2,22 @@ import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { defaultJobsDir } from "./runtime.ts";
4
4
 
5
- /** Workers with a live adapter. `deepseek` is a reserved slot (unreleased harness). */
5
+ /**
6
+ * Workers with a live adapter. `deepseek` is the experimental official
7
+ * DeepSeek Harness (dsh) — an opt-in worker, not a mid default.
8
+ */
6
9
  export type WorkerKind = "grok" | "claude-ds" | "openrouter" | "deepseek";
7
10
  export type Lane = "easy" | "mid" | "hard";
8
11
 
9
12
  /** Public CLI aliases for mid-lane DeepSeek models. */
10
13
  export type DsModelAlias = "flash" | "pro";
11
14
 
15
+ export interface DsModelChoice {
16
+ alias: DsModelAlias;
17
+ /** Exact id passed to claude-ds `-Model` / stock `claude --model`. */
18
+ id: string;
19
+ }
20
+
12
21
  export const WORKERS: WorkerKind[] = ["grok", "claude-ds", "openrouter", "deepseek"];
13
22
  export const LANES: Lane[] = ["easy", "mid", "hard"];
14
23
  export const DS_MODELS: DsModelAlias[] = ["flash", "pro"];
@@ -18,15 +27,42 @@ export const DS_MODEL_IDS: Record<DsModelAlias, string> = {
18
27
  pro: "deepseek-v4-pro",
19
28
  };
20
29
 
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";
30
+ /**
31
+ * Resolve --model / env to alias + concrete model id.
32
+ * Preserves `deepseek-v4-pro[1m]` (does not silently strip the SKU).
33
+ * Empty → Flash.
34
+ */
35
+ export function resolveDsModel(raw?: string | null): DsModelChoice {
36
+ if (!raw || !raw.trim()) {
37
+ return { alias: "flash", id: DS_MODEL_IDS.flash };
38
+ }
24
39
  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";
40
+ if (v === "flash" || v === "deepseek-v4-flash") {
41
+ return { alias: "flash", id: DS_MODEL_IDS.flash };
42
+ }
43
+ if (v === "pro" || v === "deepseek-v4-pro") {
44
+ return { alias: "pro", id: DS_MODEL_IDS.pro };
45
+ }
46
+ if (v === "deepseek-v4-pro[1m]") {
47
+ return { alias: "pro", id: "deepseek-v4-pro[1m]" };
48
+ }
27
49
  throw new Error(`Invalid --model ${raw}; expected flash|pro`);
28
50
  }
29
51
 
52
+ /** Alias-only helper (tests / callers that do not need the concrete id). */
53
+ export function resolveDsModelAlias(raw?: string | null): DsModelAlias {
54
+ return resolveDsModel(raw).alias;
55
+ }
56
+
57
+ /**
58
+ * Mid default from env: CURSOR_ROUTE_DS_MODEL, else ANTHROPIC_MODEL, else flash.
59
+ * Throws if the env value is set but invalid (fail loud on start).
60
+ */
61
+ export function defaultDsModelFromEnv(): DsModelChoice {
62
+ const raw = process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL;
63
+ return resolveDsModel(raw);
64
+ }
65
+
30
66
  /** OpenRouter model for the easy lane (env CURSOR_ROUTE_OPENROUTER_MODEL). */
31
67
  export function openRouterModel(): string {
32
68
  return process.env.CURSOR_ROUTE_OPENROUTER_MODEL || "openrouter/free";
@@ -53,20 +89,33 @@ function maxConcurrentJobsFromEnv(): number {
53
89
  */
54
90
  export const config = {
55
91
  product: "cursor-route",
56
- version: "0.1.6",
92
+ version: "0.1.8",
57
93
  get jobsDir(): string {
58
94
  return defaultJobsDir();
59
95
  },
60
96
  tmuxPrefix: "cursor-route",
61
97
  defaultWorker: "grok" as WorkerKind,
62
- /** Lane → default worker (Cemini /route public core). */
98
+ /**
99
+ * Lane → default worker (Cemini /route public core).
100
+ * `deepseek` is experimental only — mid stays on claude-ds.
101
+ */
63
102
  laneWorkers: {
64
103
  easy: "openrouter" as WorkerKind,
65
104
  mid: "claude-ds" as WorkerKind,
66
105
  hard: "grok" as WorkerKind,
67
106
  },
68
- /** Default mid DeepSeek model (Flash = cheap execute). Override: --model pro */
69
- defaultDsModel: "flash" as DsModelAlias,
107
+ /**
108
+ * Default mid DeepSeek model (Flash = cheap execute).
109
+ * Live: CURSOR_ROUTE_DS_MODEL / ANTHROPIC_MODEL; invalid env → flash (health-safe).
110
+ * Override on start: --model. startJob uses defaultDsModelFromEnv() and fails on invalid env.
111
+ */
112
+ get defaultDsModel(): DsModelAlias {
113
+ try {
114
+ return defaultDsModelFromEnv().alias;
115
+ } catch {
116
+ return "flash";
117
+ }
118
+ },
70
119
  jobsListLimit: 20,
71
120
  /** Max simultaneously active (running|pending) jobs. Override: CURSOR_ROUTE_MAX_JOBS. */
72
121
  get maxConcurrentJobs(): number {
package/src/jobs.ts CHANGED
@@ -15,6 +15,8 @@ import { dirname } from "node:path";
15
15
  import {
16
16
  config,
17
17
  sessionName,
18
+ defaultDsModelFromEnv,
19
+ DS_MODEL_IDS,
18
20
  type Lane,
19
21
  type WorkerKind,
20
22
  type DsModelAlias,
@@ -35,7 +37,7 @@ export interface Job {
35
37
  status: JobStatus;
36
38
  worker: WorkerKind;
37
39
  lane?: Lane;
38
- /** Mid-lane DeepSeek model alias (flash|pro). Only set for claude-ds. */
40
+ /** Mid-lane DeepSeek model alias (flash|pro). Set for claude-ds and deepseek. */
39
41
  model?: DsModelAlias;
40
42
  prompt: string;
41
43
  cwd: string;
@@ -229,8 +231,10 @@ export interface StartOptions {
229
231
  prompt: string;
230
232
  worker?: WorkerKind;
231
233
  lane?: Lane;
232
- /** Mid-lane DeepSeek: flash (default) | pro. Ignored by grok/openrouter. */
234
+ /** Mid-lane DeepSeek: flash (default) | pro (claude-ds + deepseek). Ignored by grok/openrouter. */
233
235
  model?: DsModelAlias;
236
+ /** Concrete DeepSeek id (preserves pro[1m]). Derived from --model / env when unset. */
237
+ modelId?: string;
234
238
  cwd?: string;
235
239
  alwaysApprove?: boolean;
236
240
  dryRun?: boolean;
@@ -278,8 +282,27 @@ export function startJob(opts: StartOptions): {
278
282
  const paths = jobPaths(id);
279
283
  writeSecure(paths.prompt, opts.prompt);
280
284
 
281
- const model =
282
- worker === "claude-ds" ? opts.model ?? config.defaultDsModel : undefined;
285
+ let model: DsModelAlias | undefined;
286
+ let modelId: string | undefined;
287
+ if (worker === "claude-ds" || worker === "deepseek") {
288
+ if (opts.model) {
289
+ model = opts.model;
290
+ modelId = opts.modelId ?? DS_MODEL_IDS[opts.model];
291
+ } else {
292
+ try {
293
+ const choice = defaultDsModelFromEnv();
294
+ model = choice.alias;
295
+ modelId = opts.modelId ?? choice.id;
296
+ } catch (e) {
297
+ try {
298
+ unlinkSync(paths.prompt);
299
+ } catch {
300
+ /* ignore */
301
+ }
302
+ return { ok: false, error: (e as Error).message };
303
+ }
304
+ }
305
+ }
283
306
 
284
307
  let plan;
285
308
  try {
@@ -288,6 +311,8 @@ export function startJob(opts: StartOptions): {
288
311
  cwd,
289
312
  alwaysApprove,
290
313
  model,
314
+ modelId,
315
+ dryRun: Boolean(opts.dryRun),
291
316
  });
292
317
  } catch (e) {
293
318
  try {
@@ -469,7 +494,7 @@ export function cleanJobs(olderThanDays = 7): number {
469
494
  t < cutoff &&
470
495
  (job.status === "completed" || job.status === "failed" || job.status === "killed")
471
496
  ) {
472
- for (const ext of [".json", ".prompt", ".log"] as const) {
497
+ for (const ext of [".json", ".prompt", ".log", ".dsh-patch.yml"] as const) {
473
498
  const fp = underJobsDir(id, ext);
474
499
  if (existsSync(fp)) unlinkSync(fp);
475
500
  }