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/src/cli.ts CHANGED
@@ -4,7 +4,15 @@
4
4
  */
5
5
  import { readFileSync, existsSync, realpathSync, statSync } from "node:fs";
6
6
  import { resolve, basename } from "node:path";
7
- import { config, WORKERS, LANES, type WorkerKind, type Lane } from "./config.ts";
7
+ import {
8
+ config,
9
+ WORKERS,
10
+ LANES,
11
+ resolveDsModel,
12
+ type WorkerKind,
13
+ type Lane,
14
+ type DsModelAlias,
15
+ } from "./config.ts";
8
16
  import { runHealth, printHealth } from "./health.ts";
9
17
  import {
10
18
  startJob,
@@ -44,8 +52,9 @@ Usage:
44
52
  cursor-route clean [--days N]
45
53
 
46
54
  Start options:
47
- --worker <grok|claude-ds|openrouter> Worker adapter (default: grok)
55
+ --worker <grok|claude-ds|openrouter> Worker adapter (default: grok; deepseek = reserved slot)
48
56
  --lane <easy|mid|hard> Lane → worker (easy=openrouter, mid=claude-ds, hard=grok)
57
+ --model <flash|pro> Mid DeepSeek only (default: flash / CURSOR_ROUTE_DS_MODEL). Parsed only for claude-ds
49
58
  --dir <path> Working directory (default: cwd)
50
59
  --ask Disable always-approve for this job
51
60
  --dry-run Print launch command; do not start
@@ -57,7 +66,8 @@ Env:
57
66
  CURSOR_ROUTE_JOBS_DIR Override jobs dir (default: ~/.local/share/cursor-route/jobs)
58
67
  CURSOR_ROUTE_MAX_JOBS Max active jobs (default: 50)
59
68
  CURSOR_ROUTE_RELAXED=1 health OK without tmux/workers (CI / infra smoke)
60
- CURSOR_ROUTE_ALLOW_ANTHROPIC=1 Allow mid-lane on Anthropic Claude (expensive; not default)
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
61
71
  CURSOR_ROUTE_GROK_BIN Override the grok binary path (tests / power users)
62
72
  CURSOR_ROUTE_CLAUDE_DS_BIN Override the claude-ds binary path (tests / power users)
63
73
  OPENROUTER_API_KEY OpenRouter key (required for --worker openrouter / --lane easy)
@@ -152,6 +162,12 @@ function asLane(v: unknown): Lane | undefined {
152
162
  throw new Error(`Invalid --lane ${v}; expected ${LANES.join("|")}`);
153
163
  }
154
164
 
165
+ function asDsModelChoice(v: unknown): { alias: DsModelAlias; id: string } | undefined {
166
+ if (v === undefined || v === true) return undefined;
167
+ if (typeof v !== "string") throw new Error(`Invalid --model; expected flash|pro`);
168
+ return resolveDsModel(v);
169
+ }
170
+
155
171
  function refuseSecrets(text: string, context: string): void {
156
172
  if (looksLikeSecretMaterial(text)) {
157
173
  console.error(
@@ -216,9 +232,38 @@ async function main() {
216
232
  if (cmd === "start") {
217
233
  requireStringFlag(f, "worker");
218
234
  requireStringFlag(f, "lane");
235
+ requireStringFlag(f, "model");
219
236
  const promptFile = requireStringFlag(f, "prompt-file");
220
237
  const dirFlag = requireStringFlag(f, "dir");
221
238
 
239
+ let worker: WorkerKind | undefined;
240
+ let lane: Lane | undefined;
241
+ try {
242
+ worker = asWorker(f.worker);
243
+ lane = asLane(f.lane);
244
+ } catch (e) {
245
+ console.error((e as Error).message);
246
+ process.exit(2);
247
+ }
248
+ const resolvedWorker =
249
+ worker ?? (lane ? config.laneWorkers[lane] : config.defaultWorker);
250
+
251
+ let model: DsModelAlias | undefined;
252
+ let modelId: string | undefined;
253
+ // --model is DeepSeek/claude-ds only; ignore (do not validate) for other workers
254
+ if (f.model !== undefined && resolvedWorker === "claude-ds") {
255
+ try {
256
+ const choice = asDsModelChoice(f.model);
257
+ if (choice) {
258
+ model = choice.alias;
259
+ modelId = choice.id;
260
+ }
261
+ } catch (e) {
262
+ console.error((e as Error).message);
263
+ process.exit(2);
264
+ }
265
+ }
266
+
222
267
  let prompt = "";
223
268
  if (promptFile) {
224
269
  const p = resolve(promptFile);
@@ -253,8 +298,10 @@ async function main() {
253
298
 
254
299
  const result = startJob({
255
300
  prompt,
256
- worker: asWorker(f.worker),
257
- lane: asLane(f.lane),
301
+ worker,
302
+ lane,
303
+ model,
304
+ modelId,
258
305
  cwd,
259
306
  alwaysApprove: !f.ask,
260
307
  dryRun: Boolean(f.dryRun),
@@ -280,9 +327,11 @@ async function main() {
280
327
  } else if (result.dryRun) {
281
328
  console.log(`dry-run job ${result.job.id}`);
282
329
  console.log(`worker: ${result.job.worker}`);
330
+ if (result.job.model) console.log(`model: ${result.job.model}`);
283
331
  console.log(`command: ${redactSecrets(result.command || "")}`);
284
332
  } else {
285
- console.log(`started ${result.job.id} (${result.job.worker})`);
333
+ const modelNote = result.job.model ? `/${result.job.model}` : "";
334
+ console.log(`started ${result.job.id} (${result.job.worker}${modelNote})`);
286
335
  console.log(`session: ${result.job.tmuxSession}`);
287
336
  if (String(result.job.tmuxSession).startsWith("headless-")) {
288
337
  console.log(`mode: headless (--no-tmux); use capture/status (no attach/send)`);
@@ -306,8 +355,9 @@ async function main() {
306
355
  } else {
307
356
  for (const j of jobs) {
308
357
  const age = j.startedAt || j.createdAt;
358
+ const modelCol = j.model ? j.model.padEnd(6) : "".padEnd(6);
309
359
  console.log(
310
- `${j.id} ${j.status.padEnd(10)} ${j.worker.padEnd(10)} ${age} ${j.prompt.slice(0, 48).replace(/\n/g, " ")}`,
360
+ `${j.id} ${j.status.padEnd(10)} ${j.worker.padEnd(10)} ${modelCol} ${age} ${j.prompt.slice(0, 48).replace(/\n/g, " ")}`,
311
361
  );
312
362
  }
313
363
  }
@@ -339,7 +389,9 @@ async function main() {
339
389
  const view = { ...job, sessionAlive: alive };
340
390
  if (json) console.log(JSON.stringify(view, null, 2));
341
391
  else {
342
- console.log(`${job.id} ${job.status} worker=${job.worker} sessionAlive=${alive}`);
392
+ console.log(
393
+ `${job.id} ${job.status} worker=${job.worker}${job.model ? ` model=${job.model}` : ""} sessionAlive=${alive}`,
394
+ );
343
395
  if (job.error) console.log(`error: ${job.error}`);
344
396
  }
345
397
  return;
package/src/config.ts CHANGED
@@ -2,11 +2,63 @@ import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { defaultJobsDir } from "./runtime.ts";
4
4
 
5
- export type WorkerKind = "grok" | "claude-ds" | "openrouter";
5
+ /** Workers with a live adapter. `deepseek` is a reserved slot (unreleased harness). */
6
+ export type WorkerKind = "grok" | "claude-ds" | "openrouter" | "deepseek";
6
7
  export type Lane = "easy" | "mid" | "hard";
7
8
 
8
- export const WORKERS: WorkerKind[] = ["grok", "claude-ds", "openrouter"];
9
+ /** Public CLI aliases for mid-lane DeepSeek models. */
10
+ export type DsModelAlias = "flash" | "pro";
11
+
12
+ export interface DsModelChoice {
13
+ alias: DsModelAlias;
14
+ /** Exact id passed to claude-ds `-Model` / stock `claude --model`. */
15
+ id: string;
16
+ }
17
+
18
+ export const WORKERS: WorkerKind[] = ["grok", "claude-ds", "openrouter", "deepseek"];
9
19
  export const LANES: Lane[] = ["easy", "mid", "hard"];
20
+ export const DS_MODELS: DsModelAlias[] = ["flash", "pro"];
21
+
22
+ export const DS_MODEL_IDS: Record<DsModelAlias, string> = {
23
+ flash: "deepseek-v4-flash",
24
+ pro: "deepseek-v4-pro",
25
+ };
26
+
27
+ /**
28
+ * Resolve --model / env to alias + concrete model id.
29
+ * Preserves `deepseek-v4-pro[1m]` (does not silently strip the SKU).
30
+ * Empty → Flash.
31
+ */
32
+ export function resolveDsModel(raw?: string | null): DsModelChoice {
33
+ if (!raw || !raw.trim()) {
34
+ return { alias: "flash", id: DS_MODEL_IDS.flash };
35
+ }
36
+ const v = raw.trim().toLowerCase();
37
+ if (v === "flash" || v === "deepseek-v4-flash") {
38
+ return { alias: "flash", id: DS_MODEL_IDS.flash };
39
+ }
40
+ if (v === "pro" || v === "deepseek-v4-pro") {
41
+ return { alias: "pro", id: DS_MODEL_IDS.pro };
42
+ }
43
+ if (v === "deepseek-v4-pro[1m]") {
44
+ return { alias: "pro", id: "deepseek-v4-pro[1m]" };
45
+ }
46
+ throw new Error(`Invalid --model ${raw}; expected flash|pro`);
47
+ }
48
+
49
+ /** Alias-only helper (tests / callers that do not need the concrete id). */
50
+ export function resolveDsModelAlias(raw?: string | null): DsModelAlias {
51
+ return resolveDsModel(raw).alias;
52
+ }
53
+
54
+ /**
55
+ * Mid default from env: CURSOR_ROUTE_DS_MODEL, else ANTHROPIC_MODEL, else flash.
56
+ * Throws if the env value is set but invalid (fail loud on start).
57
+ */
58
+ export function defaultDsModelFromEnv(): DsModelChoice {
59
+ const raw = process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL;
60
+ return resolveDsModel(raw);
61
+ }
10
62
 
11
63
  /** OpenRouter model for the easy lane (env CURSOR_ROUTE_OPENROUTER_MODEL). */
12
64
  export function openRouterModel(): string {
@@ -34,7 +86,7 @@ function maxConcurrentJobsFromEnv(): number {
34
86
  */
35
87
  export const config = {
36
88
  product: "cursor-route",
37
- version: "0.1.5",
89
+ version: "0.1.7",
38
90
  get jobsDir(): string {
39
91
  return defaultJobsDir();
40
92
  },
@@ -46,6 +98,18 @@ export const config = {
46
98
  mid: "claude-ds" as WorkerKind,
47
99
  hard: "grok" as WorkerKind,
48
100
  },
101
+ /**
102
+ * Default mid DeepSeek model (Flash = cheap execute).
103
+ * Live: CURSOR_ROUTE_DS_MODEL / ANTHROPIC_MODEL; invalid env → flash (health-safe).
104
+ * Override on start: --model. startJob uses defaultDsModelFromEnv() and fails on invalid env.
105
+ */
106
+ get defaultDsModel(): DsModelAlias {
107
+ try {
108
+ return defaultDsModelFromEnv().alias;
109
+ } catch {
110
+ return "flash";
111
+ }
112
+ },
49
113
  jobsListLimit: 20,
50
114
  /** Max simultaneously active (running|pending) jobs. Override: CURSOR_ROUTE_MAX_JOBS. */
51
115
  get maxConcurrentJobs(): number {
package/src/jobs.ts CHANGED
@@ -12,7 +12,15 @@ import {
12
12
  import { join, resolve } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { dirname } from "node:path";
15
- import { config, sessionName, type Lane, type WorkerKind } from "./config.ts";
15
+ import {
16
+ config,
17
+ sessionName,
18
+ defaultDsModelFromEnv,
19
+ DS_MODEL_IDS,
20
+ type Lane,
21
+ type WorkerKind,
22
+ type DsModelAlias,
23
+ } from "./config.ts";
16
24
  import { getAdapter } from "./adapters/index.ts";
17
25
  import { spawn } from "node:child_process";
18
26
  import { spawnSync } from "node:child_process";
@@ -29,6 +37,8 @@ export interface Job {
29
37
  status: JobStatus;
30
38
  worker: WorkerKind;
31
39
  lane?: Lane;
40
+ /** Mid-lane DeepSeek model alias (flash|pro). Only set for claude-ds. */
41
+ model?: DsModelAlias;
32
42
  prompt: string;
33
43
  cwd: string;
34
44
  alwaysApprove: boolean;
@@ -221,6 +231,10 @@ export interface StartOptions {
221
231
  prompt: string;
222
232
  worker?: WorkerKind;
223
233
  lane?: Lane;
234
+ /** Mid-lane DeepSeek: flash (default) | pro. Ignored by grok/openrouter. */
235
+ model?: DsModelAlias;
236
+ /** Concrete DeepSeek id (preserves pro[1m]). Derived from --model / env when unset. */
237
+ modelId?: string;
224
238
  cwd?: string;
225
239
  alwaysApprove?: boolean;
226
240
  dryRun?: boolean;
@@ -268,12 +282,36 @@ export function startJob(opts: StartOptions): {
268
282
  const paths = jobPaths(id);
269
283
  writeSecure(paths.prompt, opts.prompt);
270
284
 
285
+ let model: DsModelAlias | undefined;
286
+ let modelId: string | undefined;
287
+ if (worker === "claude-ds") {
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
+ }
306
+
271
307
  let plan;
272
308
  try {
273
309
  plan = adapter.buildLaunch({
274
310
  promptFile: paths.prompt,
275
311
  cwd,
276
312
  alwaysApprove,
313
+ model,
314
+ modelId,
277
315
  });
278
316
  } catch (e) {
279
317
  try {
@@ -290,6 +328,7 @@ export function startJob(opts: StartOptions): {
290
328
  status: "pending",
291
329
  worker,
292
330
  lane: opts.lane,
331
+ model,
293
332
  prompt: opts.prompt,
294
333
  cwd,
295
334
  alwaysApprove: plan.alwaysApprove,