cursor-route 0.1.7 → 0.1.9
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 +22 -0
- package/CONTRIBUTING.md +1 -0
- package/README.md +52 -10
- package/SUPPORT.md +1 -0
- package/dist/adapters/claude-ds.js +18 -0
- package/dist/adapters/deepseek.js +127 -8
- package/dist/cli.js +13 -8
- package/dist/config.js +5 -2
- package/dist/health.js +16 -0
- package/dist/jobs.js +29 -2
- package/docs/DEMO_GIF.md +17 -1
- package/docs/briefs/WORKING.md +16 -7
- package/docs/demo-notes.md +6 -3
- package/docs/fixtures/generate-hero-demo.sh +109 -0
- package/docs/fixtures/hero-demo.log +53 -0
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/skills/route-orch/SKILL.md +45 -8
- package/src/adapters/claude-ds.ts +19 -0
- package/src/adapters/deepseek.ts +145 -11
- package/src/adapters/types.ts +3 -1
- package/src/cli.test.ts +465 -27
- package/src/cli.ts +13 -7
- package/src/config.ts +9 -3
- package/src/health.ts +21 -0
- package/src/jobs.ts +54 -4
package/src/cli.test.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
mkdirSync,
|
|
4
|
+
writeFileSync,
|
|
5
|
+
chmodSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
} from "node:fs";
|
|
3
11
|
import { join } from "node:path";
|
|
4
12
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { resolveWorker, startJob } from "./jobs.ts";
|
|
13
|
+
import { resolveWorker, startJob, jobEvidence, type Job } from "./jobs.ts";
|
|
6
14
|
import { config, resolveDsModel, resolveDsModelAlias } from "./config.ts";
|
|
7
15
|
import { shellQuote, newJobId } from "./util.ts";
|
|
8
16
|
import { runHealth } from "./health.ts";
|
|
9
17
|
import { looksLikeSecretMaterial, redactSecrets } from "./secrets.ts";
|
|
10
|
-
import {
|
|
11
|
-
|
|
18
|
+
import {
|
|
19
|
+
isDeepSeekRouted,
|
|
20
|
+
isDeepSeekBaseUrl,
|
|
21
|
+
claudeDsAdapter,
|
|
22
|
+
isMidDeepSeekProven,
|
|
23
|
+
} from "./adapters/claude-ds.ts";
|
|
24
|
+
import { deepseekAdapter, patchPathForPrompt } from "./adapters/deepseek.ts";
|
|
12
25
|
import { grokAdapter } from "./adapters/grok.ts";
|
|
13
26
|
|
|
14
27
|
describe("resolveWorker", () => {
|
|
@@ -190,15 +203,56 @@ describe("startJob product path", () => {
|
|
|
190
203
|
}
|
|
191
204
|
});
|
|
192
205
|
|
|
193
|
-
test("--worker deepseek dry-run
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
206
|
+
test("--worker deepseek dry-run succeeds with fake dsh + key", () => {
|
|
207
|
+
const dir = join(tmpdir(), `cr-dsh-start-${process.pid}`);
|
|
208
|
+
mkdirSync(dir, { recursive: true });
|
|
209
|
+
const bin = join(dir, "dsh");
|
|
210
|
+
writeFileSync(bin, "#!/bin/sh\necho fake-dsh\n");
|
|
211
|
+
chmodSync(bin, 0o755);
|
|
212
|
+
const prev = {
|
|
213
|
+
bin: process.env.CURSOR_ROUTE_DSH_BIN,
|
|
214
|
+
key: process.env.DEEPSEEK_API_KEY,
|
|
215
|
+
ds: process.env.CURSOR_ROUTE_DS_MODEL,
|
|
216
|
+
am: process.env.ANTHROPIC_MODEL,
|
|
217
|
+
jobs: process.env.CURSOR_ROUTE_JOBS_DIR,
|
|
218
|
+
};
|
|
219
|
+
process.env.CURSOR_ROUTE_DSH_BIN = bin;
|
|
220
|
+
// Not a real key: short and dash-separated so the CLI refuse gate would not trip.
|
|
221
|
+
process.env.DEEPSEEK_API_KEY = "sk-test-not-a-real-key";
|
|
222
|
+
delete process.env.CURSOR_ROUTE_DS_MODEL;
|
|
223
|
+
delete process.env.ANTHROPIC_MODEL;
|
|
224
|
+
process.env.CURSOR_ROUTE_JOBS_DIR = join(dir, "jobs");
|
|
225
|
+
try {
|
|
226
|
+
const result = startJob({
|
|
227
|
+
prompt: "ping",
|
|
228
|
+
worker: "deepseek",
|
|
229
|
+
dryRun: true,
|
|
230
|
+
});
|
|
231
|
+
expect(result.ok).toBe(true);
|
|
232
|
+
if (!result.ok) return;
|
|
233
|
+
expect(result.job.worker).toBe("deepseek");
|
|
234
|
+
expect(result.job.model).toBe("flash");
|
|
235
|
+
expect(result.command).toContain("--profile headless");
|
|
236
|
+
expect(result.command).toContain("--patch");
|
|
237
|
+
expect(result.command).toContain("$(cat");
|
|
238
|
+
expect(result.command).toContain("DSH_PERMISSION_MODE");
|
|
239
|
+
expect(result.command).not.toContain("sk-test");
|
|
240
|
+
expect(result.command).not.toContain("npx");
|
|
241
|
+
// Dry-run keeps no durable artifacts (prompt + dsh patch both removed)
|
|
242
|
+
expect(readdirSync(join(dir, "jobs")).length).toBe(0);
|
|
243
|
+
} finally {
|
|
244
|
+
if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_DSH_BIN;
|
|
245
|
+
else process.env.CURSOR_ROUTE_DSH_BIN = prev.bin;
|
|
246
|
+
if (prev.key === undefined) delete process.env.DEEPSEEK_API_KEY;
|
|
247
|
+
else process.env.DEEPSEEK_API_KEY = prev.key;
|
|
248
|
+
if (prev.ds === undefined) delete process.env.CURSOR_ROUTE_DS_MODEL;
|
|
249
|
+
else process.env.CURSOR_ROUTE_DS_MODEL = prev.ds;
|
|
250
|
+
if (prev.am === undefined) delete process.env.ANTHROPIC_MODEL;
|
|
251
|
+
else process.env.ANTHROPIC_MODEL = prev.am;
|
|
252
|
+
if (prev.jobs === undefined) delete process.env.CURSOR_ROUTE_JOBS_DIR;
|
|
253
|
+
else process.env.CURSOR_ROUTE_JOBS_DIR = prev.jobs;
|
|
254
|
+
rmSync(dir, { recursive: true, force: true });
|
|
255
|
+
}
|
|
202
256
|
});
|
|
203
257
|
|
|
204
258
|
test("grok command never includes deepseek-v4", () => {
|
|
@@ -220,16 +274,287 @@ describe("startJob product path", () => {
|
|
|
220
274
|
});
|
|
221
275
|
});
|
|
222
276
|
|
|
223
|
-
describe("deepseek adapter
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
277
|
+
describe("deepseek adapter (dsh)", () => {
|
|
278
|
+
/** Executable fake `dsh` in a fresh tmpdir (existsSync passes). */
|
|
279
|
+
const makeFakeDsh = (suffix: string) => {
|
|
280
|
+
const dir = join(tmpdir(), `cr-dsh-${process.pid}-${suffix}`);
|
|
281
|
+
mkdirSync(dir, { recursive: true });
|
|
282
|
+
const bin = join(dir, "dsh");
|
|
283
|
+
writeFileSync(bin, "#!/bin/sh\necho fake-dsh\n");
|
|
284
|
+
chmodSync(bin, 0o755);
|
|
285
|
+
return { dir, bin };
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/** Set/delete env for the duration of fn; restore afterwards. */
|
|
289
|
+
const withEnv = (
|
|
290
|
+
patch: Record<string, string | undefined>,
|
|
291
|
+
fn: () => void,
|
|
292
|
+
) => {
|
|
293
|
+
const prev = new Map<string, string | undefined>();
|
|
294
|
+
for (const k of Object.keys(patch)) {
|
|
295
|
+
prev.set(k, process.env[k]);
|
|
296
|
+
const v = patch[k];
|
|
297
|
+
if (v === undefined) delete process.env[k];
|
|
298
|
+
else process.env[k] = v;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
fn();
|
|
302
|
+
} finally {
|
|
303
|
+
for (const [k, v] of prev) {
|
|
304
|
+
if (v === undefined) delete process.env[k];
|
|
305
|
+
else process.env[k] = v;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
test("health: no binary and no key → not ok, install hint names the npm package", () => {
|
|
311
|
+
withEnv(
|
|
312
|
+
{
|
|
313
|
+
CURSOR_ROUTE_DSH_BIN: join(tmpdir(), `missing-dsh-${process.pid}`),
|
|
314
|
+
DEEPSEEK_API_KEY: undefined,
|
|
315
|
+
},
|
|
316
|
+
() => {
|
|
317
|
+
const h = deepseekAdapter.health();
|
|
318
|
+
expect(h.ok).toBe(false);
|
|
319
|
+
expect(h.detail).toMatch(/npm i -g @deepseek-ai\/dsh/);
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("health: binary but no key → not ok, tells operator to export DEEPSEEK_API_KEY", () => {
|
|
325
|
+
const { bin } = makeFakeDsh("nokey");
|
|
326
|
+
withEnv({ CURSOR_ROUTE_DSH_BIN: bin, DEEPSEEK_API_KEY: undefined }, () => {
|
|
327
|
+
const h = deepseekAdapter.health();
|
|
328
|
+
expect(h.ok).toBe(false);
|
|
329
|
+
expect(h.detail).toMatch(/DEEPSEEK_API_KEY/);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("health: binary + key → ok", () => {
|
|
334
|
+
const { bin } = makeFakeDsh("ok");
|
|
335
|
+
withEnv({ CURSOR_ROUTE_DSH_BIN: bin, DEEPSEEK_API_KEY: "test-key" }, () => {
|
|
336
|
+
const h = deepseekAdapter.health();
|
|
337
|
+
expect(h.ok).toBe(true);
|
|
338
|
+
expect(h.binary).toBe(bin);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("buildLaunch: flash default patch next to prompt; key only in env", () => {
|
|
343
|
+
const { dir, bin } = makeFakeDsh("flash");
|
|
344
|
+
const promptFile = join(dir, "job.prompt");
|
|
345
|
+
writeFileSync(promptFile, "ping");
|
|
346
|
+
withEnv(
|
|
347
|
+
{
|
|
348
|
+
CURSOR_ROUTE_DSH_BIN: bin,
|
|
349
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
350
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
351
|
+
ANTHROPIC_MODEL: undefined,
|
|
352
|
+
},
|
|
353
|
+
() => {
|
|
354
|
+
const plan = deepseekAdapter.buildLaunch({
|
|
355
|
+
promptFile,
|
|
356
|
+
cwd: dir,
|
|
357
|
+
alwaysApprove: true,
|
|
358
|
+
});
|
|
359
|
+
expect(plan.command).toContain("--profile headless");
|
|
360
|
+
expect(plan.command).toContain("--patch");
|
|
361
|
+
expect(plan.command).toContain("$(cat");
|
|
362
|
+
expect(plan.command).not.toContain("npx");
|
|
363
|
+
expect(plan.command).not.toContain("test-key");
|
|
364
|
+
expect(plan.env?.DSH_PERMISSION_MODE).toBe("danger-full-access");
|
|
365
|
+
expect(plan.env?.DEEPSEEK_API_KEY).toBe("test-key");
|
|
366
|
+
const patch = readFileSync(join(dir, "job.dsh-patch.yml"), "utf8");
|
|
367
|
+
expect(patch).toContain("agent-default-model");
|
|
368
|
+
expect(patch).toContain("provider: deepseek-official");
|
|
369
|
+
expect(patch).toContain("deepseek-v4-flash");
|
|
370
|
+
expect(patch).not.toContain("deepseek-v4-pro");
|
|
371
|
+
expect(patch).not.toContain("test-key");
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
rmSync(dir, { recursive: true, force: true });
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("buildLaunch: pro model → deepseek-v4-pro in patch", () => {
|
|
378
|
+
const { dir, bin } = makeFakeDsh("pro");
|
|
379
|
+
const promptFile = join(dir, "job.prompt");
|
|
380
|
+
writeFileSync(promptFile, "ping");
|
|
381
|
+
withEnv(
|
|
382
|
+
{
|
|
383
|
+
CURSOR_ROUTE_DSH_BIN: bin,
|
|
384
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
385
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
386
|
+
ANTHROPIC_MODEL: undefined,
|
|
387
|
+
},
|
|
388
|
+
() => {
|
|
389
|
+
deepseekAdapter.buildLaunch({
|
|
390
|
+
promptFile,
|
|
391
|
+
cwd: dir,
|
|
392
|
+
alwaysApprove: true,
|
|
393
|
+
model: "pro",
|
|
394
|
+
});
|
|
395
|
+
const patch = readFileSync(join(dir, "job.dsh-patch.yml"), "utf8");
|
|
396
|
+
expect(patch).toContain("deepseek-v4-pro");
|
|
397
|
+
expect(patch).not.toContain("deepseek-v4-flash");
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
rmSync(dir, { recursive: true, force: true });
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("buildLaunch: preserves deepseek-v4-pro[1m] in patch via modelId", () => {
|
|
404
|
+
const { dir, bin } = makeFakeDsh("pro1m");
|
|
405
|
+
const promptFile = join(dir, "job.prompt");
|
|
406
|
+
writeFileSync(promptFile, "ping");
|
|
407
|
+
withEnv(
|
|
408
|
+
{
|
|
409
|
+
CURSOR_ROUTE_DSH_BIN: bin,
|
|
410
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
411
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
412
|
+
ANTHROPIC_MODEL: undefined,
|
|
413
|
+
},
|
|
414
|
+
() => {
|
|
415
|
+
deepseekAdapter.buildLaunch({
|
|
416
|
+
promptFile,
|
|
417
|
+
cwd: dir,
|
|
418
|
+
alwaysApprove: true,
|
|
419
|
+
model: "pro",
|
|
420
|
+
modelId: "deepseek-v4-pro[1m]",
|
|
421
|
+
});
|
|
422
|
+
const patch = readFileSync(join(dir, "job.dsh-patch.yml"), "utf8");
|
|
423
|
+
expect(patch).toContain("deepseek-v4-pro[1m]");
|
|
424
|
+
},
|
|
425
|
+
);
|
|
426
|
+
rmSync(dir, { recursive: true, force: true });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("buildLaunch: alwaysApprove false (--ask) → workspace-write", () => {
|
|
430
|
+
const { dir, bin } = makeFakeDsh("ask");
|
|
431
|
+
const promptFile = join(dir, "job.prompt");
|
|
432
|
+
writeFileSync(promptFile, "ping");
|
|
433
|
+
withEnv(
|
|
434
|
+
{
|
|
435
|
+
CURSOR_ROUTE_DSH_BIN: bin,
|
|
436
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
437
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
438
|
+
ANTHROPIC_MODEL: undefined,
|
|
439
|
+
},
|
|
440
|
+
() => {
|
|
441
|
+
const plan = deepseekAdapter.buildLaunch({
|
|
442
|
+
promptFile,
|
|
443
|
+
cwd: dir,
|
|
444
|
+
alwaysApprove: false,
|
|
445
|
+
});
|
|
446
|
+
expect(plan.env?.DSH_PERMISSION_MODE).toBe("workspace-write");
|
|
447
|
+
expect(plan.env?.DSH_PERMISSION_MODE).not.toBe("danger-full-access");
|
|
448
|
+
},
|
|
449
|
+
);
|
|
450
|
+
rmSync(dir, { recursive: true, force: true });
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("buildLaunch: CURSOR_ROUTE_ASK=1 opts out even when alwaysApprove true", () => {
|
|
454
|
+
const { dir, bin } = makeFakeDsh("ask-env");
|
|
455
|
+
const promptFile = join(dir, "job.prompt");
|
|
456
|
+
writeFileSync(promptFile, "ping");
|
|
457
|
+
withEnv(
|
|
458
|
+
{
|
|
459
|
+
CURSOR_ROUTE_DSH_BIN: bin,
|
|
460
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
461
|
+
CURSOR_ROUTE_ASK: "1",
|
|
462
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
463
|
+
ANTHROPIC_MODEL: undefined,
|
|
464
|
+
},
|
|
465
|
+
() => {
|
|
466
|
+
const plan = deepseekAdapter.buildLaunch({
|
|
467
|
+
promptFile,
|
|
468
|
+
cwd: dir,
|
|
469
|
+
alwaysApprove: true,
|
|
470
|
+
});
|
|
471
|
+
expect(plan.env?.DSH_PERMISSION_MODE).toBe("workspace-write");
|
|
472
|
+
expect(plan.alwaysApprove).toBe(false);
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
rmSync(dir, { recursive: true, force: true });
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test("buildLaunch: missing binary falls back to plain `dsh` for dry-run", () => {
|
|
479
|
+
const { dir } = makeFakeDsh("nobin");
|
|
480
|
+
const promptFile = join(dir, "job.prompt");
|
|
481
|
+
writeFileSync(promptFile, "ping");
|
|
482
|
+
// Point PATH at an empty dir so `command -v dsh` finds nothing,
|
|
483
|
+
// regardless of what the dev machine has installed.
|
|
484
|
+
const empty = join(dir, "empty");
|
|
485
|
+
mkdirSync(empty, { recursive: true });
|
|
486
|
+
withEnv(
|
|
487
|
+
{
|
|
488
|
+
CURSOR_ROUTE_DSH_BIN: undefined,
|
|
489
|
+
DEEPSEEK_API_KEY: undefined,
|
|
490
|
+
PATH: empty,
|
|
491
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
492
|
+
ANTHROPIC_MODEL: undefined,
|
|
493
|
+
},
|
|
494
|
+
() => {
|
|
495
|
+
const plan = deepseekAdapter.buildLaunch({
|
|
496
|
+
promptFile,
|
|
497
|
+
cwd: dir,
|
|
498
|
+
alwaysApprove: true,
|
|
499
|
+
dryRun: true,
|
|
500
|
+
});
|
|
501
|
+
expect(plan.command).toContain("'dsh' --profile headless");
|
|
502
|
+
expect(plan.command).toContain("--patch");
|
|
503
|
+
expect(plan.env?.DEEPSEEK_API_KEY).toBeUndefined();
|
|
504
|
+
},
|
|
505
|
+
);
|
|
506
|
+
rmSync(dir, { recursive: true, force: true });
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("buildLaunch: non-.prompt promptFile does not overwrite the prompt", () => {
|
|
510
|
+
const { dir, bin } = makeFakeDsh("noprompt-ext");
|
|
511
|
+
const promptFile = join(dir, "job");
|
|
512
|
+
writeFileSync(promptFile, "keep-me");
|
|
513
|
+
withEnv(
|
|
514
|
+
{
|
|
515
|
+
CURSOR_ROUTE_DSH_BIN: bin,
|
|
516
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
517
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
518
|
+
ANTHROPIC_MODEL: undefined,
|
|
519
|
+
},
|
|
520
|
+
() => {
|
|
521
|
+
expect(patchPathForPrompt(promptFile)).toBe(`${promptFile}.dsh-patch.yml`);
|
|
522
|
+
deepseekAdapter.buildLaunch({
|
|
523
|
+
promptFile,
|
|
524
|
+
cwd: dir,
|
|
525
|
+
alwaysApprove: true,
|
|
526
|
+
});
|
|
527
|
+
expect(readFileSync(promptFile, "utf8")).toBe("keep-me");
|
|
528
|
+
expect(existsSync(`${promptFile}.dsh-patch.yml`)).toBe(true);
|
|
529
|
+
},
|
|
530
|
+
);
|
|
531
|
+
rmSync(dir, { recursive: true, force: true });
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("buildLaunch: rejects modelId that would break YAML", () => {
|
|
535
|
+
const { dir, bin } = makeFakeDsh("badid");
|
|
536
|
+
const promptFile = join(dir, "job.prompt");
|
|
537
|
+
writeFileSync(promptFile, "ping");
|
|
538
|
+
withEnv(
|
|
539
|
+
{
|
|
540
|
+
CURSOR_ROUTE_DSH_BIN: bin,
|
|
541
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
542
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
543
|
+
ANTHROPIC_MODEL: undefined,
|
|
544
|
+
},
|
|
545
|
+
() => {
|
|
546
|
+
expect(() =>
|
|
547
|
+
deepseekAdapter.buildLaunch({
|
|
548
|
+
promptFile,
|
|
549
|
+
cwd: dir,
|
|
550
|
+
alwaysApprove: true,
|
|
551
|
+
model: "flash",
|
|
552
|
+
modelId: "x\n injected: true",
|
|
553
|
+
}),
|
|
554
|
+
).toThrow(/Invalid DeepSeek model id/);
|
|
555
|
+
},
|
|
556
|
+
);
|
|
557
|
+
rmSync(dir, { recursive: true, force: true });
|
|
233
558
|
});
|
|
234
559
|
});
|
|
235
560
|
|
|
@@ -290,23 +615,42 @@ describe("health", () => {
|
|
|
290
615
|
test("returns structured report", () => {
|
|
291
616
|
const r = runHealth();
|
|
292
617
|
expect(r.product).toBe("cursor-route");
|
|
293
|
-
expect(r.version).toBe("0.1.
|
|
618
|
+
expect(r.version).toBe("0.1.9");
|
|
294
619
|
expect(r.checks.length).toBeGreaterThan(3);
|
|
295
620
|
expect(r.checks.some((c) => c.name === "tmux")).toBe(true);
|
|
296
621
|
expect(r.checks.some((c) => c.name === "cursor_cli")).toBe(true);
|
|
622
|
+
const mid = r.checks.find((c) => c.name === "lane:mid");
|
|
623
|
+
expect(mid).toBeDefined();
|
|
624
|
+
expect(r.lanes.mid.worker).toBe("claude-ds");
|
|
625
|
+
expect(r.lanes.mid.deepseek).toBe(mid!.ok);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("config version is 0.1.9", () => {
|
|
629
|
+
expect(config.version).toBe("0.1.9");
|
|
297
630
|
});
|
|
298
631
|
|
|
299
632
|
test("OR-gate: ok can be true while worker:deepseek is false", () => {
|
|
300
|
-
const prev =
|
|
633
|
+
const prev = {
|
|
634
|
+
relaxed: process.env.CURSOR_ROUTE_RELAXED,
|
|
635
|
+
bin: process.env.CURSOR_ROUTE_DSH_BIN,
|
|
636
|
+
key: process.env.DEEPSEEK_API_KEY,
|
|
637
|
+
};
|
|
301
638
|
process.env.CURSOR_ROUTE_RELAXED = "1";
|
|
639
|
+
// Force deepseek unhealthy even on machines that have a real dsh + key.
|
|
640
|
+
process.env.CURSOR_ROUTE_DSH_BIN = join(tmpdir(), `missing-dsh-${process.pid}`);
|
|
641
|
+
delete process.env.DEEPSEEK_API_KEY;
|
|
302
642
|
try {
|
|
303
643
|
const r = runHealth();
|
|
304
644
|
const ds = r.checks.find((c) => c.name === "worker:deepseek");
|
|
305
645
|
expect(ds?.ok).toBe(false);
|
|
306
646
|
expect(r.ok).toBe(true);
|
|
307
647
|
} finally {
|
|
308
|
-
if (prev === undefined) delete process.env.CURSOR_ROUTE_RELAXED;
|
|
309
|
-
else process.env.CURSOR_ROUTE_RELAXED = prev;
|
|
648
|
+
if (prev.relaxed === undefined) delete process.env.CURSOR_ROUTE_RELAXED;
|
|
649
|
+
else process.env.CURSOR_ROUTE_RELAXED = prev.relaxed;
|
|
650
|
+
if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_DSH_BIN;
|
|
651
|
+
else process.env.CURSOR_ROUTE_DSH_BIN = prev.bin;
|
|
652
|
+
if (prev.key === undefined) delete process.env.DEEPSEEK_API_KEY;
|
|
653
|
+
else process.env.DEEPSEEK_API_KEY = prev.key;
|
|
310
654
|
}
|
|
311
655
|
});
|
|
312
656
|
|
|
@@ -322,4 +666,98 @@ describe("health", () => {
|
|
|
322
666
|
else process.env.CURSOR_ROUTE_RELAXED = prev;
|
|
323
667
|
}
|
|
324
668
|
});
|
|
669
|
+
|
|
670
|
+
test("CURSOR_ROUTE_CLAUDE_DS_BIN pointing at an existing file proves mid DeepSeek", () => {
|
|
671
|
+
const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
672
|
+
const fake = join(tmpdir(), `cr-mid-proof-${process.pid}`);
|
|
673
|
+
writeFileSync(fake, "#!/bin/sh\necho ok\n");
|
|
674
|
+
chmodSync(fake, 0o755);
|
|
675
|
+
process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = fake;
|
|
676
|
+
try {
|
|
677
|
+
expect(isMidDeepSeekProven()).toBe(true);
|
|
678
|
+
} finally {
|
|
679
|
+
if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
680
|
+
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
|
|
681
|
+
rmSync(fake, { force: true });
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test("Anthropic hatch is not DeepSeek proof (shim on PATH still is)", () => {
|
|
686
|
+
const shimDir = join(tmpdir(), `cr-mid-hatch-${process.pid}`);
|
|
687
|
+
mkdirSync(shimDir, { recursive: true });
|
|
688
|
+
const claude = join(shimDir, "claude");
|
|
689
|
+
writeFileSync(claude, "#!/bin/sh\necho ok\n");
|
|
690
|
+
chmodSync(claude, 0o755);
|
|
691
|
+
|
|
692
|
+
const prev = {
|
|
693
|
+
bin: process.env.CURSOR_ROUTE_CLAUDE_DS_BIN,
|
|
694
|
+
allow: process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC,
|
|
695
|
+
base: process.env.ANTHROPIC_BASE_URL,
|
|
696
|
+
crBase: process.env.CURSOR_ROUTE_ANTHROPIC_BASE_URL,
|
|
697
|
+
path: process.env.PATH,
|
|
698
|
+
};
|
|
699
|
+
delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
700
|
+
delete process.env.CURSOR_ROUTE_ANTHROPIC_BASE_URL;
|
|
701
|
+
process.env.ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
|
702
|
+
process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC = "1";
|
|
703
|
+
process.env.PATH = `${shimDir}:/usr/bin:/bin`;
|
|
704
|
+
try {
|
|
705
|
+
const detail = claudeDsAdapter.health().detail;
|
|
706
|
+
if (detail.includes("Anthropic")) {
|
|
707
|
+
expect(isMidDeepSeekProven()).toBe(false);
|
|
708
|
+
const r = runHealth();
|
|
709
|
+
const mid = r.checks.find((c) => c.name === "lane:mid");
|
|
710
|
+
expect(mid?.ok).toBe(false);
|
|
711
|
+
expect(r.lanes.mid.deepseek).toBe(false);
|
|
712
|
+
} else {
|
|
713
|
+
// Real claude-ds / deepseek-claude on PATH is proof (hatch never engages).
|
|
714
|
+
expect(isMidDeepSeekProven()).toBe(true);
|
|
715
|
+
}
|
|
716
|
+
} finally {
|
|
717
|
+
if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
718
|
+
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev.bin;
|
|
719
|
+
if (prev.allow === undefined) delete process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC;
|
|
720
|
+
else process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC = prev.allow;
|
|
721
|
+
if (prev.base === undefined) delete process.env.ANTHROPIC_BASE_URL;
|
|
722
|
+
else process.env.ANTHROPIC_BASE_URL = prev.base;
|
|
723
|
+
if (prev.crBase === undefined) delete process.env.CURSOR_ROUTE_ANTHROPIC_BASE_URL;
|
|
724
|
+
else process.env.CURSOR_ROUTE_ANTHROPIC_BASE_URL = prev.crBase;
|
|
725
|
+
process.env.PATH = prev.path || "";
|
|
726
|
+
rmSync(shimDir, { recursive: true, force: true });
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
describe("jobEvidence", () => {
|
|
732
|
+
test("verify.claim is always unverified; spawn/execute keys exist", () => {
|
|
733
|
+
const job: Job = {
|
|
734
|
+
id: "abcd1234",
|
|
735
|
+
schema: "cursor-route.job.v1",
|
|
736
|
+
status: "running",
|
|
737
|
+
worker: "claude-ds",
|
|
738
|
+
lane: "mid",
|
|
739
|
+
model: "flash",
|
|
740
|
+
prompt: "ping",
|
|
741
|
+
cwd: "/tmp",
|
|
742
|
+
alwaysApprove: true,
|
|
743
|
+
tmuxSession: "cursor-route-abcd1234",
|
|
744
|
+
createdAt: "2026-08-18T00:00:00.000Z",
|
|
745
|
+
startedAt: "2026-08-18T00:00:01.000Z",
|
|
746
|
+
logBytes: 42,
|
|
747
|
+
};
|
|
748
|
+
const ev = jobEvidence(job, true);
|
|
749
|
+
expect(ev.verify.claim).toBe("unverified");
|
|
750
|
+
expect(ev.verify.captureHint).toBe("cursor-route capture abcd1234");
|
|
751
|
+
expect(ev.verify.logBytes).toBe(42);
|
|
752
|
+
expect(ev.spawn.jobId).toBe("abcd1234");
|
|
753
|
+
expect(ev.spawn.worker).toBe("claude-ds");
|
|
754
|
+
expect(ev.spawn.lane).toBe("mid");
|
|
755
|
+
expect(ev.spawn.model).toBe("flash");
|
|
756
|
+
expect(ev.spawn.startedAt).toBe("2026-08-18T00:00:01.000Z");
|
|
757
|
+
expect(ev.execute.status).toBe("running");
|
|
758
|
+
expect(ev.execute.exitCode).toBe(null);
|
|
759
|
+
expect(ev.execute.tmuxSession).toBe("cursor-route-abcd1234");
|
|
760
|
+
expect(ev.execute.pid).toBe(null);
|
|
761
|
+
expect(ev.execute.sessionAlive).toBe(true);
|
|
762
|
+
});
|
|
325
763
|
});
|
package/src/cli.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
cleanJobs,
|
|
23
23
|
jobPaths,
|
|
24
24
|
refreshStatus,
|
|
25
|
+
jobEvidence,
|
|
25
26
|
} from "./jobs.ts";
|
|
26
27
|
import {
|
|
27
28
|
capturePane,
|
|
@@ -39,11 +40,11 @@ Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter (easy) are
|
|
|
39
40
|
|
|
40
41
|
Usage:
|
|
41
42
|
cursor-route --version
|
|
42
|
-
cursor-route health [--json]
|
|
43
|
+
cursor-route health [--json] # JSON includes lanes.mid (DeepSeek proof)
|
|
43
44
|
cursor-route start <prompt> [options]
|
|
44
45
|
cursor-route start --prompt-file <path> [options]
|
|
45
46
|
cursor-route jobs [--json] [--limit N]
|
|
46
|
-
cursor-route status <jobId> [--json]
|
|
47
|
+
cursor-route status <jobId> [--json] # JSON includes evidence spawn/execute/verify
|
|
47
48
|
cursor-route capture <jobId> [lines]
|
|
48
49
|
cursor-route send <jobId> <message>
|
|
49
50
|
cursor-route attach <jobId>
|
|
@@ -52,9 +53,9 @@ Usage:
|
|
|
52
53
|
cursor-route clean [--days N]
|
|
53
54
|
|
|
54
55
|
Start options:
|
|
55
|
-
--worker <grok|claude-ds|openrouter> Worker adapter (default: grok; deepseek =
|
|
56
|
+
--worker <grok|claude-ds|openrouter|deepseek> Worker adapter (default: grok; deepseek = experimental official dsh)
|
|
56
57
|
--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
|
|
58
|
+
--model <flash|pro> Mid DeepSeek only (default: flash / CURSOR_ROUTE_DS_MODEL). Parsed for claude-ds + deepseek
|
|
58
59
|
--dir <path> Working directory (default: cwd)
|
|
59
60
|
--ask Disable always-approve for this job
|
|
60
61
|
--dry-run Print launch command; do not start
|
|
@@ -70,6 +71,8 @@ Env:
|
|
|
70
71
|
CURSOR_ROUTE_DS_MODEL Default mid model flash|pro (or deepseek-v4-pro[1m]); overridden by --model
|
|
71
72
|
CURSOR_ROUTE_GROK_BIN Override the grok binary path (tests / power users)
|
|
72
73
|
CURSOR_ROUTE_CLAUDE_DS_BIN Override the claude-ds binary path (tests / power users)
|
|
74
|
+
CURSOR_ROUTE_DSH_BIN Override the dsh binary path (tests / power users)
|
|
75
|
+
DEEPSEEK_API_KEY DeepSeek API key (required for --worker deepseek)
|
|
73
76
|
OPENROUTER_API_KEY OpenRouter key (required for --worker openrouter / --lane easy)
|
|
74
77
|
CURSOR_ROUTE_OPENROUTER_MODEL OpenRouter model (default: openrouter/free)
|
|
75
78
|
OPENROUTER_BASE_URL OpenRouter API base (default: https://openrouter.ai/api/v1)
|
|
@@ -250,8 +253,9 @@ async function main() {
|
|
|
250
253
|
|
|
251
254
|
let model: DsModelAlias | undefined;
|
|
252
255
|
let modelId: string | undefined;
|
|
253
|
-
// --model is DeepSeek
|
|
254
|
-
|
|
256
|
+
// --model is DeepSeek-only (claude-ds mid lane + experimental deepseek worker);
|
|
257
|
+
// ignore (do not validate) for other workers
|
|
258
|
+
if (f.model !== undefined && (resolvedWorker === "claude-ds" || resolvedWorker === "deepseek")) {
|
|
255
259
|
try {
|
|
256
260
|
const choice = asDsModelChoice(f.model);
|
|
257
261
|
if (choice) {
|
|
@@ -386,12 +390,14 @@ async function main() {
|
|
|
386
390
|
}
|
|
387
391
|
})())
|
|
388
392
|
: sessionExists(job.tmuxSession);
|
|
389
|
-
const
|
|
393
|
+
const evidence = jobEvidence(job, alive);
|
|
394
|
+
const view = { ...job, sessionAlive: alive, evidence };
|
|
390
395
|
if (json) console.log(JSON.stringify(view, null, 2));
|
|
391
396
|
else {
|
|
392
397
|
console.log(
|
|
393
398
|
`${job.id} ${job.status} worker=${job.worker}${job.model ? ` model=${job.model}` : ""} sessionAlive=${alive}`,
|
|
394
399
|
);
|
|
400
|
+
console.log("evidence: spawn/execute/verify (claim=unverified)");
|
|
395
401
|
if (job.error) console.log(`error: ${job.error}`);
|
|
396
402
|
}
|
|
397
403
|
return;
|
package/src/config.ts
CHANGED
|
@@ -2,7 +2,10 @@ 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
|
|
|
@@ -86,13 +89,16 @@ function maxConcurrentJobsFromEnv(): number {
|
|
|
86
89
|
*/
|
|
87
90
|
export const config = {
|
|
88
91
|
product: "cursor-route",
|
|
89
|
-
version: "0.1.
|
|
92
|
+
version: "0.1.9",
|
|
90
93
|
get jobsDir(): string {
|
|
91
94
|
return defaultJobsDir();
|
|
92
95
|
},
|
|
93
96
|
tmuxPrefix: "cursor-route",
|
|
94
97
|
defaultWorker: "grok" as WorkerKind,
|
|
95
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Lane → default worker (Cemini /route public core).
|
|
100
|
+
* `deepseek` is experimental only — mid stays on claude-ds.
|
|
101
|
+
*/
|
|
96
102
|
laneWorkers: {
|
|
97
103
|
easy: "openrouter" as WorkerKind,
|
|
98
104
|
mid: "claude-ds" as WorkerKind,
|