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/cli.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
config,
|
|
9
9
|
WORKERS,
|
|
10
10
|
LANES,
|
|
11
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
240
|
+
|
|
241
|
+
let worker: WorkerKind | undefined;
|
|
242
|
+
let lane: Lane | undefined;
|
|
239
243
|
try {
|
|
240
|
-
|
|
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
|
|
281
|
-
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(
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
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")
|
|
26
|
-
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
69
|
-
|
|
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).
|
|
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
|
-
|
|
282
|
-
|
|
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
|
}
|