cursor-route 0.1.4 → 0.1.6
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/README.md +53 -12
- package/SECURITY.md +6 -5
- package/dist/adapters/claude-ds.js +33 -9
- package/dist/adapters/deepseek.js +19 -0
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/openrouter.js +78 -0
- package/dist/cli.js +36 -11
- package/dist/config.js +30 -3
- package/dist/jobs.js +4 -1
- package/dist/openrouter-run.js +95 -0
- package/docs/briefs/WORKING.md +55 -0
- package/docs/demo-notes.md +7 -4
- package/llms.txt +4 -4
- package/package.json +4 -2
- package/skills/route-orch/SKILL.md +24 -6
- package/src/adapters/claude-ds.ts +38 -9
- package/src/adapters/deepseek.ts +24 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/openrouter.test.ts +57 -0
- package/src/adapters/openrouter.ts +80 -0
- package/src/adapters/types.ts +3 -1
- package/src/cli.test.ts +75 -2
- package/src/cli.ts +41 -11
- package/src/config.ts +37 -5
- package/src/integration.test.ts +7 -0
- package/src/jobs.ts +16 -1
- package/src/openrouter-run.ts +102 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* One-shot OpenRouter easy-lane runner — reads a prompt file and POSTs it to
|
|
4
|
+
* OpenRouter's chat/completions, printing the assistant reply to stdout.
|
|
5
|
+
* No runtime npm deps (Node 20+ global fetch). Invoked by the openrouter
|
|
6
|
+
* adapter (dist via node, else src via bun). Never echoes OPENROUTER_API_KEY.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { openRouterModel, openRouterBaseUrl } from "./config.js";
|
|
10
|
+
import { looksLikeSecretMaterial } from "./secrets.js";
|
|
11
|
+
const SYSTEM_PROMPT = "You are a drafting/rewrite helper for cursor-route. Never invent credentials, API keys, or secrets; if asked for secret material, refuse. Provide educational, general-purpose help.";
|
|
12
|
+
function fail(msg, code) {
|
|
13
|
+
console.error(`cursor-route/openrouter-run: ${msg}`);
|
|
14
|
+
process.exit(code);
|
|
15
|
+
}
|
|
16
|
+
function parseArgs(argv) {
|
|
17
|
+
const flags = {};
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const a = argv[i];
|
|
20
|
+
if (a === "-h" || a === "--help") {
|
|
21
|
+
console.log("usage: cursor-route/openrouter-run --prompt-file <path>\n" +
|
|
22
|
+
"env: OPENROUTER_API_KEY (required), CURSOR_ROUTE_OPENROUTER_MODEL, OPENROUTER_BASE_URL");
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
if (a === "--prompt-file") {
|
|
26
|
+
const v = argv[i + 1];
|
|
27
|
+
if (!v || v.startsWith("--"))
|
|
28
|
+
fail("--prompt-file requires a value", 2);
|
|
29
|
+
flags.promptFile = v;
|
|
30
|
+
i++;
|
|
31
|
+
}
|
|
32
|
+
else if (a.startsWith("--prompt-file=")) {
|
|
33
|
+
flags.promptFile = a.slice("--prompt-file=".length);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
fail(`unknown argument: ${a}`, 2);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return flags;
|
|
40
|
+
}
|
|
41
|
+
async function main() {
|
|
42
|
+
const { promptFile } = parseArgs(process.argv.slice(2));
|
|
43
|
+
if (!promptFile)
|
|
44
|
+
fail("--prompt-file <path> is required", 2);
|
|
45
|
+
let prompt;
|
|
46
|
+
try {
|
|
47
|
+
prompt = readFileSync(promptFile, "utf8");
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
fail(`cannot read prompt file: ${e.message}`, 2);
|
|
51
|
+
}
|
|
52
|
+
if (!prompt.trim())
|
|
53
|
+
fail("prompt file is empty", 2);
|
|
54
|
+
if (looksLikeSecretMaterial(prompt)) {
|
|
55
|
+
fail("refusing prompt: looks like secret key material — easy lane is for non-secret drafts", 3);
|
|
56
|
+
}
|
|
57
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
58
|
+
if (!apiKey)
|
|
59
|
+
fail("OPENROUTER_API_KEY is not set", 2);
|
|
60
|
+
const base = openRouterBaseUrl().replace(/\/+$/, "");
|
|
61
|
+
const url = `${base}/chat/completions`;
|
|
62
|
+
let res;
|
|
63
|
+
try {
|
|
64
|
+
res = await fetch(url, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
Authorization: `Bearer ${apiKey}`,
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
// OpenRouter etiquette: identify the app so the provider can see usage source.
|
|
70
|
+
"HTTP-Referer": "https://github.com/cemini23/cursor-route",
|
|
71
|
+
"X-Title": "cursor-route",
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
model: openRouterModel(),
|
|
75
|
+
messages: [
|
|
76
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
77
|
+
{ role: "user", content: prompt },
|
|
78
|
+
],
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
fail(`network error calling ${url}: ${e.message}`, 1);
|
|
84
|
+
}
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
const body = await res.text().catch(() => "");
|
|
87
|
+
fail(`OpenRouter API ${res.status}: ${body.slice(0, 500)}`, 1);
|
|
88
|
+
}
|
|
89
|
+
const json = (await res.json());
|
|
90
|
+
const content = json.choices?.[0]?.message?.content ?? "";
|
|
91
|
+
if (!content)
|
|
92
|
+
fail("OpenRouter returned no assistant content", 1);
|
|
93
|
+
process.stdout.write(content.endsWith("\n") ? content : content + "\n");
|
|
94
|
+
}
|
|
95
|
+
main().catch((e) => fail(e instanceof Error ? e.message : String(e), 1));
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: cursor-route workspace — working brief (edit in place)
|
|
3
|
+
repo: ~/Projects/cursor-route
|
|
4
|
+
npm: cursor-route@0.1.6
|
|
5
|
+
created: 2026-08-12
|
|
6
|
+
updated: 2026-08-12
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# cursor-route — living brief
|
|
10
|
+
|
|
11
|
+
**Edit this file.** It is the working notes for this public repo (CLI + `route-orch` skill + adapters). Not the private Cemini `/route` skill (`agent-toolkit` / federation).
|
|
12
|
+
|
|
13
|
+
When a chunk is accepted: apply it to `src/`, `README.md`, and/or `skills/route-orch/SKILL.md` (keep `.cursor/skills/route-orch/SKILL.md` in sync). Then tick **Open** and add an **Edit log** line.
|
|
14
|
+
|
|
15
|
+
## What this repo is
|
|
16
|
+
|
|
17
|
+
Cursor Agent plans. Workers run in tmux via `cursor-route`:
|
|
18
|
+
|
|
19
|
+
| Lane | Worker | Intent |
|
|
20
|
+
|------|--------|--------|
|
|
21
|
+
| `easy` | OpenRouter free | Wording / drafts — non-secret prompts only |
|
|
22
|
+
| `mid` | claude-ds (DeepSeek behind Claude Code) | Default implement (**Flash**) |
|
|
23
|
+
| `hard` | Grok CLI | Hard implement |
|
|
24
|
+
|
|
25
|
+
Always-approve on (`--ask` / `CURSOR_ROUTE_ASK=1` to opt out). Jobs live in `~/.local/share/cursor-route/jobs`, not in this clone.
|
|
26
|
+
|
|
27
|
+
## Open (edit / check off)
|
|
28
|
+
|
|
29
|
+
- [x] **Flash vs Pro on the CLI** — public default **Flash**; `--model pro` → `deepseek-v4-pro`
|
|
30
|
+
- [x] pass `claude-ds -Model deepseek-v4-flash|deepseek-v4-pro` from the adapter
|
|
31
|
+
- [x] add `--model flash|pro` on `start`
|
|
32
|
+
- [x] document Grok **auth** ≠ usage-out (`grok login`) vs quota → Pro stand-in
|
|
33
|
+
- [x] **Skill `route-orch`** — Flash/Pro table in `skills/` + `.cursor/skills/`
|
|
34
|
+
- [x] **Official DeepSeek Harness** — `deepseek` adapter slot present; mid stays on claude-ds
|
|
35
|
+
- [ ] **Hero GIF** — still outstanding (`docs/DEMO_GIF.md`)
|
|
36
|
+
- [x] **Do not** paste private `ROUTE_KIT`, SIP, prod paths, or hang-watchdog env into this public repo
|
|
37
|
+
|
|
38
|
+
## Repo map
|
|
39
|
+
|
|
40
|
+
| Path | Role |
|
|
41
|
+
|------|------|
|
|
42
|
+
| `src/cli.ts` | `health` / `start` / `jobs` / `capture` / `send` / `kill` |
|
|
43
|
+
| `src/adapters/claude-ds.ts` | Mid worker; DeepSeek URL required; `--model` → `-Model` |
|
|
44
|
+
| `src/adapters/deepseek.ts` | Reserved unreleased harness slot |
|
|
45
|
+
| `src/adapters/grok.ts` | Hard worker |
|
|
46
|
+
| `src/adapters/openrouter.ts` | Easy worker |
|
|
47
|
+
| `skills/route-orch/SKILL.md` | Cursor skill — spawn CLI, do not implement in-session |
|
|
48
|
+
| `SECURITY.md` | Secret refuse gate |
|
|
49
|
+
|
|
50
|
+
## Edit log
|
|
51
|
+
|
|
52
|
+
| Date | Change |
|
|
53
|
+
|------|--------|
|
|
54
|
+
| 2026-08-12 | Brief created in this repo. Flash/Pro CLI + skill still open. |
|
|
55
|
+
| 2026-08-12 | Shipped Flash default + `--model pro`, deepseek slot, skill table → 0.1.6. |
|
package/docs/demo-notes.md
CHANGED
|
@@ -4,29 +4,32 @@ Simulated capture output for README / tweet assets — replace with a real GIF o
|
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
$ cursor-route --version
|
|
7
|
-
0.1.
|
|
7
|
+
0.1.6
|
|
8
8
|
|
|
9
9
|
$ cursor-route health
|
|
10
|
-
cursor-route v0.1.
|
|
10
|
+
cursor-route v0.1.6
|
|
11
11
|
health: OK
|
|
12
12
|
✓ tmux
|
|
13
13
|
✓ runtime bun ok
|
|
14
14
|
✓ script(1)
|
|
15
15
|
✓ worker:grok
|
|
16
16
|
✓ worker:claude-ds
|
|
17
|
+
✓ worker:openrouter
|
|
18
|
+
✗ worker:deepseek unreleased — mid lane uses claude-ds
|
|
17
19
|
✓ jobs_dir ~/.local/share/cursor-route/jobs
|
|
18
20
|
|
|
19
21
|
$ cursor-route start --lane mid "Add a failing test then make it pass"
|
|
20
|
-
started a1b2c3d4 (claude-ds)
|
|
22
|
+
started a1b2c3d4 (claude-ds/flash)
|
|
21
23
|
session: cursor-route-a1b2c3d4
|
|
22
24
|
attach: tmux attach -t cursor-route-a1b2c3d4
|
|
23
25
|
|
|
24
26
|
$ cursor-route jobs --json
|
|
25
|
-
[{ "id": "a1b2c3d4", "status": "running", "worker": "claude-ds", ... }]
|
|
27
|
+
[{ "id": "a1b2c3d4", "status": "running", "worker": "claude-ds", "model": "flash", ... }]
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
Verified locally (2026-08-10): headless `claude-ds` smoke returned `CURSOR_ROUTE_SMOKE_OK`.
|
|
29
31
|
Grok smoke hit 402 (Build usage balance exhausted) — auth/PATH wiring works; top up Grok Build for live demos.
|
|
32
|
+
Use `--model pro` when Grok **usage** is out (not the same as a missing `grok login`).
|
|
30
33
|
|
|
31
34
|
Current commands: `health`, `start`, `jobs`, `status`, `capture`, `send`, `attach`, `kill`, `sessions`, `clean`.
|
|
32
35
|
Headless demos (no tmux) use `--no-tmux` and `capture`/`status` instead of `attach`/`send`.
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# cursor-route
|
|
2
2
|
|
|
3
|
-
> Cursor stays the planner. DeepSeek (mid)
|
|
3
|
+
> Cursor stays the planner. DeepSeek (mid), Grok CLI (hard), and OpenRouter free models (easy) run parallel coding workers in tmux.
|
|
4
4
|
|
|
5
5
|
MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route
|
|
6
6
|
GitHub: https://github.com/cemini23/cursor-route
|
|
@@ -8,19 +8,19 @@ GitHub: https://github.com/cemini23/cursor-route
|
|
|
8
8
|
## FAQ
|
|
9
9
|
|
|
10
10
|
### What is cursor-route?
|
|
11
|
-
cursor-route is a public MIT CLI and Cursor skill that runs parallel coding workers in tmux while Cursor remains the planner. DeepSeek handles the mid lane,
|
|
11
|
+
cursor-route is a public MIT CLI and Cursor skill that runs parallel coding workers in tmux while Cursor remains the planner. DeepSeek handles the mid lane, Grok CLI handles the hard lane, and OpenRouter free models handle the easy lane (wording/drafts, non-secret prompts only).
|
|
12
12
|
|
|
13
13
|
### How is this different from Codex orchestrator?
|
|
14
14
|
It uses the familiar strategist and worker-pane shape, but it is not a Codex clone. cursor-route uses Cursor as the planner and DeepSeek plus Grok CLI as workers. Codex is not required.
|
|
15
15
|
|
|
16
16
|
### Does mid lane use Anthropic Claude?
|
|
17
|
-
No. The mid worker is DeepSeek. Claude Code is the harness, configured with `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic` and a DeepSeek key in `ANTHROPIC_AUTH_TOKEN`.
|
|
17
|
+
No. The mid worker is DeepSeek. Claude Code is the harness, configured with `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic` and a DeepSeek key in `ANTHROPIC_AUTH_TOKEN`. Default model is Flash (`--model flash`); use `--model pro` for harder mid work or when Grok usage is exhausted (not the same as a missing `grok login`).
|
|
18
18
|
|
|
19
19
|
### How do I install?
|
|
20
20
|
Run `npm i -g cursor-route`, install tmux if needed, then run `cursor-route health`. The package is available at https://www.npmjs.com/package/cursor-route, and the source is at https://github.com/cemini23/cursor-route.
|
|
21
21
|
|
|
22
22
|
### Is it free?
|
|
23
|
-
The cursor-route code is open source under MIT. It does not make the worker services free. Your costs depend on Cursor, DeepSeek API usage, and the Grok access or balance available to you.
|
|
23
|
+
The cursor-route code is open source under MIT. It does not make the worker services free. Your costs depend on Cursor, DeepSeek API usage, and the Grok access or balance available to you. The easy lane can be free on OpenRouter's free-model route (`openrouter/free`).
|
|
24
24
|
|
|
25
25
|
## Install
|
|
26
26
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-route",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) are the parallel army \u2014 lane-aware /route orchestration in tmux.",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter easy lane are the parallel army \u2014 lane-aware /route orchestration in tmux.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"skills",
|
|
18
18
|
"docs/DEMO_GIF.md",
|
|
19
19
|
"docs/demo-notes.md",
|
|
20
|
+
"docs/briefs/WORKING.md",
|
|
20
21
|
"docs/fixtures",
|
|
21
22
|
"LICENSE",
|
|
22
23
|
"SECURITY.md",
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"grok",
|
|
39
40
|
"deepseek",
|
|
40
41
|
"claude-ds",
|
|
42
|
+
"openrouter",
|
|
41
43
|
"tmux",
|
|
42
44
|
"orchestrator",
|
|
43
45
|
"agents",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: route-orch
|
|
3
3
|
description: >-
|
|
4
|
-
Delegate coding work from Cursor to parallel Grok CLI / claude-ds (DeepSeek)
|
|
5
|
-
workers via cursor-route. Use when the user says
|
|
6
|
-
parallel agents with cursor-route, or explicitly
|
|
7
|
-
implementation to Grok/DeepSeek panes — not for private
|
|
4
|
+
Delegate coding work from Cursor to parallel Grok CLI / claude-ds (DeepSeek) /
|
|
5
|
+
OpenRouter (easy lane) workers via cursor-route. Use when the user says
|
|
6
|
+
/route-orch, spawn workers, parallel agents with cursor-route, or explicitly
|
|
7
|
+
asks to outsource implementation to Grok/DeepSeek panes — not for private
|
|
8
|
+
Cemini /route.
|
|
8
9
|
---
|
|
9
10
|
|
|
10
11
|
# route-orch (cursor-route)
|
|
@@ -23,10 +24,27 @@ You are the **orchestrator**. Do **not** implement bulk code in this Cursor sess
|
|
|
23
24
|
|
|
24
25
|
| Lane | Worker | Use when |
|
|
25
26
|
|------|--------|----------|
|
|
27
|
+
| `easy` | `openrouter` (OpenRouter free models) | Wording / drafts — non-secret prompts only |
|
|
26
28
|
| `mid` | `claude-ds` (DeepSeek via Claude Code harness) | Standard implement / refactor |
|
|
27
29
|
| `hard` | `grok` | Premium plan in Cursor → Grok implement |
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
Free OpenRouter models may log prompts — keep secrets off the easy lane (the CLI refuse gate still applies).
|
|
32
|
+
|
|
33
|
+
## claude-ds models
|
|
34
|
+
|
|
35
|
+
One harness. Do not install a second coding loop.
|
|
36
|
+
|
|
37
|
+
| Flag / role | Model | When |
|
|
38
|
+
|-------------|-------|------|
|
|
39
|
+
| default mid (`--model flash`) | `deepseek-v4-flash` | `--lane mid` cheap execute |
|
|
40
|
+
| Grok stand-in (`--model pro`) | `deepseek-v4-pro` | Grok CLI **usage/quota** out (not a missing `grok login`) |
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cursor-route start --lane mid --dir "$PWD" "…"
|
|
44
|
+
cursor-route start --lane mid --model pro --dir "$PWD" "…"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If `worker:grok` is ✗ on health, that is usually **auth** (`grok login` / `XAI_API_KEY`) — not the Pro stand-in case.
|
|
30
48
|
|
|
31
49
|
## Workflow
|
|
32
50
|
|
|
@@ -45,7 +63,7 @@ EOF
|
|
|
45
63
|
)"
|
|
46
64
|
```
|
|
47
65
|
|
|
48
|
-
Or `--worker grok` / `--worker claude-ds
|
|
66
|
+
Or `--worker grok` / `--worker claude-ds` / `--worker openrouter` (or `--lane easy`). Use `--no-tmux` only when tmux is unavailable.
|
|
49
67
|
|
|
50
68
|
4. Monitor: `cursor-route jobs --json` · `cursor-route capture <id>` · `cursor-route send <id> "…"` (tmux only).
|
|
51
69
|
5. Summarize worker results with **verify evidence** — no status-only “done”. If verify fails, `send` a correction or spawn a follow-up — do not invent success.
|
|
@@ -3,6 +3,12 @@ import { readFileSync, existsSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import type { Adapter, WorkerHealth } from "./types.ts";
|
|
6
|
+
import {
|
|
7
|
+
DS_MODEL_IDS,
|
|
8
|
+
type DsModelAlias,
|
|
9
|
+
config,
|
|
10
|
+
resolveDsModelAlias,
|
|
11
|
+
} from "../config.ts";
|
|
6
12
|
import { shellQuote } from "../util.ts";
|
|
7
13
|
|
|
8
14
|
/**
|
|
@@ -114,22 +120,37 @@ function resolveClaudeDs(): { binary: string; mode: string } | null {
|
|
|
114
120
|
return null;
|
|
115
121
|
}
|
|
116
122
|
|
|
123
|
+
function pickModelAlias(requested?: DsModelAlias): DsModelAlias {
|
|
124
|
+
if (requested) return requested;
|
|
125
|
+
// Env override for power users who already export ANTHROPIC_MODEL
|
|
126
|
+
const fromEnv = process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL;
|
|
127
|
+
if (fromEnv) {
|
|
128
|
+
try {
|
|
129
|
+
return resolveDsModelAlias(fromEnv);
|
|
130
|
+
} catch {
|
|
131
|
+
/* fall through to default */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return config.defaultDsModel;
|
|
135
|
+
}
|
|
136
|
+
|
|
117
137
|
/**
|
|
118
138
|
* Env that must reach stock `claude` for DeepSeek routing.
|
|
119
139
|
* Passed via process/tmux env — never interpolated into the printed command.
|
|
120
140
|
*/
|
|
121
|
-
function deepSeekWorkerEnv(): Record<string, string> | undefined {
|
|
141
|
+
function deepSeekWorkerEnv(modelId: string): Record<string, string> | undefined {
|
|
122
142
|
const base = resolvedDeepSeekBaseUrl();
|
|
123
143
|
if (!base) return undefined;
|
|
124
|
-
const env: Record<string, string> = {
|
|
144
|
+
const env: Record<string, string> = {
|
|
145
|
+
ANTHROPIC_BASE_URL: base,
|
|
146
|
+
ANTHROPIC_MODEL: modelId,
|
|
147
|
+
};
|
|
125
148
|
const token =
|
|
126
149
|
process.env.ANTHROPIC_AUTH_TOKEN ||
|
|
127
150
|
process.env.ANTHROPIC_API_KEY ||
|
|
128
151
|
process.env.DEEPSEEK_API_KEY ||
|
|
129
152
|
"";
|
|
130
153
|
if (token) env.ANTHROPIC_AUTH_TOKEN = token;
|
|
131
|
-
const model = process.env.ANTHROPIC_MODEL;
|
|
132
|
-
if (model) env.ANTHROPIC_MODEL = model;
|
|
133
154
|
return env;
|
|
134
155
|
}
|
|
135
156
|
|
|
@@ -153,27 +174,33 @@ export const claudeDsAdapter: Adapter = {
|
|
|
153
174
|
worker: "claude-ds",
|
|
154
175
|
ok: true,
|
|
155
176
|
binary: resolved.binary,
|
|
156
|
-
detail: `ok (${resolved.mode})`,
|
|
177
|
+
detail: `ok (${resolved.mode}; default model ${DS_MODEL_IDS[config.defaultDsModel]})`,
|
|
157
178
|
};
|
|
158
179
|
},
|
|
159
|
-
buildLaunch({ promptFile, cwd, alwaysApprove }) {
|
|
180
|
+
buildLaunch({ promptFile, cwd, alwaysApprove, model }) {
|
|
160
181
|
const resolved = resolveClaudeDs();
|
|
161
182
|
if (!resolved) {
|
|
162
183
|
throw new Error("DeepSeek worker not available — run: cursor-route health");
|
|
163
184
|
}
|
|
164
185
|
|
|
186
|
+
const alias = pickModelAlias(model);
|
|
187
|
+
const modelId = DS_MODEL_IDS[alias];
|
|
188
|
+
|
|
165
189
|
const ask = process.env.CURSOR_ROUTE_ASK === "1" || process.env.CLAUDE_DS_ASK === "1";
|
|
166
190
|
const skip = alwaysApprove && !ask;
|
|
167
191
|
// Stock `claude` needs DeepSeek env injected into the worker process
|
|
168
192
|
// (tmux panes may not inherit client env from a long-lived server).
|
|
169
|
-
const env =
|
|
170
|
-
|
|
193
|
+
const env = resolved.mode.startsWith("claude → DeepSeek")
|
|
194
|
+
? deepSeekWorkerEnv(modelId)
|
|
195
|
+
: undefined;
|
|
171
196
|
|
|
172
|
-
if (resolved.mode.startsWith("claude-ds")) {
|
|
197
|
+
if (resolved.mode.startsWith("claude-ds") || resolved.mode.startsWith("deepseek-claude")) {
|
|
173
198
|
const parts = [
|
|
174
199
|
shellQuote(resolved.binary),
|
|
175
200
|
"-PromptFile",
|
|
176
201
|
shellQuote(promptFile),
|
|
202
|
+
"-Model",
|
|
203
|
+
shellQuote(modelId),
|
|
177
204
|
];
|
|
178
205
|
if (skip) parts.push("--dangerously-skip-permissions");
|
|
179
206
|
return {
|
|
@@ -188,6 +215,8 @@ export const claudeDsAdapter: Adapter = {
|
|
|
188
215
|
shellQuote(resolved.binary),
|
|
189
216
|
"-p",
|
|
190
217
|
`"$(cat ${shellQuote(promptFile)})"`,
|
|
218
|
+
"--model",
|
|
219
|
+
shellQuote(modelId),
|
|
191
220
|
];
|
|
192
221
|
if (skip) parts.push("--dangerously-skip-permissions");
|
|
193
222
|
return {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Adapter, WorkerHealth } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reserved slot for the official DeepSeek coding harness when it ships.
|
|
5
|
+
* Mid lane stays on claude-ds until then — do not route jobs here.
|
|
6
|
+
*/
|
|
7
|
+
export const deepseekAdapter: Adapter = {
|
|
8
|
+
kind: "deepseek",
|
|
9
|
+
label: "Official DeepSeek harness (unreleased)",
|
|
10
|
+
health(): WorkerHealth {
|
|
11
|
+
return {
|
|
12
|
+
worker: "deepseek",
|
|
13
|
+
ok: false,
|
|
14
|
+
binary: null,
|
|
15
|
+
detail:
|
|
16
|
+
"unreleased — mid lane uses claude-ds (DeepSeek behind Claude Code). See README.",
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
buildLaunch() {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"Official DeepSeek harness is not available yet — use --lane mid / --worker claude-ds",
|
|
22
|
+
);
|
|
23
|
+
},
|
|
24
|
+
};
|
package/src/adapters/index.ts
CHANGED
|
@@ -2,10 +2,14 @@ import type { WorkerKind } from "../config.ts";
|
|
|
2
2
|
import type { Adapter } from "./types.ts";
|
|
3
3
|
import { grokAdapter } from "./grok.ts";
|
|
4
4
|
import { claudeDsAdapter } from "./claude-ds.ts";
|
|
5
|
+
import { openRouterAdapter } from "./openrouter.ts";
|
|
6
|
+
import { deepseekAdapter } from "./deepseek.ts";
|
|
5
7
|
|
|
6
8
|
const registry: Record<WorkerKind, Adapter> = {
|
|
7
9
|
grok: grokAdapter,
|
|
8
10
|
"claude-ds": claudeDsAdapter,
|
|
11
|
+
openrouter: openRouterAdapter,
|
|
12
|
+
deepseek: deepseekAdapter,
|
|
9
13
|
};
|
|
10
14
|
|
|
11
15
|
export function getAdapter(worker: WorkerKind): Adapter {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, test, afterEach } from "bun:test";
|
|
2
|
+
import { openRouterAdapter } from "./openrouter.ts";
|
|
3
|
+
|
|
4
|
+
const KEY = "sk-or-v1-local-test-value-000000000000";
|
|
5
|
+
|
|
6
|
+
function setKey(): void {
|
|
7
|
+
process.env.OPENROUTER_API_KEY = KEY;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
delete process.env.OPENROUTER_API_KEY;
|
|
12
|
+
delete process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
|
|
13
|
+
delete process.env.OPENROUTER_BASE_URL;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("openrouter adapter", () => {
|
|
17
|
+
test("health fails without OPENROUTER_API_KEY", () => {
|
|
18
|
+
delete process.env.OPENROUTER_API_KEY;
|
|
19
|
+
const h = openRouterAdapter.health();
|
|
20
|
+
expect(h.worker).toBe("openrouter");
|
|
21
|
+
expect(h.ok).toBe(false);
|
|
22
|
+
expect(h.detail).toContain("OPENROUTER_API_KEY");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("health passes with a fake key (no network)", () => {
|
|
26
|
+
setKey();
|
|
27
|
+
const h = openRouterAdapter.health();
|
|
28
|
+
expect(h.ok).toBe(true);
|
|
29
|
+
expect(h.detail).toContain("openrouter/free");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("buildLaunch passes key via env and never echoes it in the command", () => {
|
|
33
|
+
setKey();
|
|
34
|
+
const plan = openRouterAdapter.buildLaunch({
|
|
35
|
+
promptFile: "/tmp/abc123.prompt",
|
|
36
|
+
cwd: "/tmp",
|
|
37
|
+
alwaysApprove: true,
|
|
38
|
+
});
|
|
39
|
+
expect(plan.worker).toBe("openrouter");
|
|
40
|
+
expect(plan.command).toContain("--prompt-file");
|
|
41
|
+
expect(plan.command).not.toContain(KEY);
|
|
42
|
+
expect(plan.env?.OPENROUTER_API_KEY).toBe(KEY);
|
|
43
|
+
// No approval concept for a pure HTTP call.
|
|
44
|
+
expect(plan.alwaysApprove).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("buildLaunch without key still prints a command (dry-run friendly) and no env", () => {
|
|
48
|
+
delete process.env.OPENROUTER_API_KEY;
|
|
49
|
+
const plan = openRouterAdapter.buildLaunch({
|
|
50
|
+
promptFile: "/tmp/abc123.prompt",
|
|
51
|
+
cwd: "/tmp",
|
|
52
|
+
alwaysApprove: true,
|
|
53
|
+
});
|
|
54
|
+
expect(plan.command).toContain("--prompt-file");
|
|
55
|
+
expect(plan.env).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import type { Adapter, WorkerHealth } from "./types.ts";
|
|
5
|
+
import { shellQuote } from "../util.ts";
|
|
6
|
+
import { openRouterModel, openRouterBaseUrl } from "../config.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve how to invoke the one-shot runner. Prefer the compiled dist via node
|
|
10
|
+
* (no loader); else Bun on src. No npx/tsx — same policy as mark-complete.
|
|
11
|
+
*/
|
|
12
|
+
function resolveRunner(): { command: string } | null {
|
|
13
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const compiled = join(here, "..", "..", "dist", "openrouter-run.js");
|
|
15
|
+
if (existsSync(compiled)) {
|
|
16
|
+
return { command: `node ${shellQuote(compiled)}` };
|
|
17
|
+
}
|
|
18
|
+
const srcFile = join(here, "..", "openrouter-run.ts");
|
|
19
|
+
if (existsSync(srcFile)) {
|
|
20
|
+
return { command: `bun ${shellQuote(srcFile)}` };
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function openRouterEnv(): Record<string, string> | undefined {
|
|
26
|
+
const key = process.env.OPENROUTER_API_KEY;
|
|
27
|
+
if (!key) return undefined;
|
|
28
|
+
const env: Record<string, string> = { OPENROUTER_API_KEY: key };
|
|
29
|
+
const model = process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
|
|
30
|
+
if (model) env.CURSOR_ROUTE_OPENROUTER_MODEL = model;
|
|
31
|
+
const base = process.env.OPENROUTER_BASE_URL;
|
|
32
|
+
if (base) env.OPENROUTER_BASE_URL = base;
|
|
33
|
+
return env;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const openRouterAdapter: Adapter = {
|
|
37
|
+
kind: "openrouter",
|
|
38
|
+
label: "OpenRouter (free easy lane)",
|
|
39
|
+
health(): WorkerHealth {
|
|
40
|
+
const runner = resolveRunner();
|
|
41
|
+
if (!process.env.OPENROUTER_API_KEY) {
|
|
42
|
+
return {
|
|
43
|
+
worker: "openrouter",
|
|
44
|
+
ok: false,
|
|
45
|
+
binary: runner?.command ?? null,
|
|
46
|
+
detail:
|
|
47
|
+
"OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane model defaults to openrouter/free)",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (!runner) {
|
|
51
|
+
return {
|
|
52
|
+
worker: "openrouter",
|
|
53
|
+
ok: false,
|
|
54
|
+
binary: null,
|
|
55
|
+
detail: "openrouter-run not found — run bun run build (or use Bun from a source clone)",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
worker: "openrouter",
|
|
60
|
+
ok: true,
|
|
61
|
+
binary: runner.command,
|
|
62
|
+
detail: `ok (model ${openRouterModel()} @ ${openRouterBaseUrl()})`,
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
buildLaunch({ promptFile }) {
|
|
66
|
+
const runner = resolveRunner();
|
|
67
|
+
if (!runner) throw new Error("openrouter runner not available — run: bun run build");
|
|
68
|
+
// Missing key is tolerated here so `--dry-run` can still print the command;
|
|
69
|
+
// real starts are gated by the health preflight (which requires the key).
|
|
70
|
+
const env = openRouterEnv();
|
|
71
|
+
|
|
72
|
+
// No interactive approval concept for a pure HTTP call — nothing to auto-approve.
|
|
73
|
+
return {
|
|
74
|
+
worker: "openrouter",
|
|
75
|
+
command: `${runner.command} --prompt-file ${shellQuote(promptFile)}`,
|
|
76
|
+
alwaysApprove: false,
|
|
77
|
+
env,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|
package/src/adapters/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WorkerKind } from "../config.ts";
|
|
1
|
+
import type { WorkerKind, DsModelAlias } from "../config.ts";
|
|
2
2
|
|
|
3
3
|
export interface WorkerHealth {
|
|
4
4
|
worker: WorkerKind;
|
|
@@ -24,5 +24,7 @@ export interface Adapter {
|
|
|
24
24
|
promptFile: string;
|
|
25
25
|
cwd: string;
|
|
26
26
|
alwaysApprove: boolean;
|
|
27
|
+
/** Mid-lane DeepSeek flash|pro (ignored by other workers). */
|
|
28
|
+
model?: DsModelAlias;
|
|
27
29
|
}): LaunchPlan;
|
|
28
30
|
}
|