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/CHANGELOG.md +31 -0
- package/CONTRIBUTING.md +13 -5
- package/README.md +38 -10
- package/dist/adapters/claude-ds.js +53 -15
- package/dist/adapters/deepseek.js +19 -0
- package/dist/adapters/index.js +2 -0
- package/dist/cli.js +51 -8
- package/dist/config.js +53 -2
- package/dist/jobs.js +28 -1
- package/docs/DEMO_GIF.md +2 -1
- package/docs/briefs/WORKING.md +61 -0
- package/docs/demo-notes.md +6 -4
- package/llms.txt +10 -3
- package/package.json +3 -1
- package/skills/route-orch/SKILL.md +16 -0
- package/src/adapters/claude-ds.ts +61 -14
- package/src/adapters/deepseek.ts +24 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/types.ts +5 -1
- package/src/cli.test.ts +222 -3
- package/src/cli.ts +60 -8
- package/src/config.ts +67 -3
- package/src/jobs.ts +40 -1
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 {
|
|
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
|
|
257
|
-
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 {
|
|
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,
|