cursor-route 0.1.1 → 0.1.5

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.
Files changed (45) hide show
  1. package/README.md +79 -12
  2. package/SECURITY.md +8 -5
  3. package/SUPPORT.md +24 -0
  4. package/bin/cursor-route +6 -8
  5. package/bin/cursor-route.js +11 -8
  6. package/dist/adapters/claude-ds.js +189 -0
  7. package/dist/adapters/grok.js +57 -0
  8. package/dist/adapters/index.js +17 -0
  9. package/dist/adapters/openrouter.js +78 -0
  10. package/dist/adapters/types.js +1 -0
  11. package/dist/cli.js +434 -0
  12. package/dist/config.js +54 -0
  13. package/dist/health.js +100 -0
  14. package/dist/jobs.js +410 -0
  15. package/dist/mark-complete.js +38 -0
  16. package/dist/openrouter-run.js +95 -0
  17. package/dist/runtime.js +27 -0
  18. package/dist/secrets.js +39 -0
  19. package/dist/tmux.js +117 -0
  20. package/dist/util.js +21 -0
  21. package/docs/DEMO_GIF.md +9 -2
  22. package/docs/demo-notes.md +9 -1
  23. package/docs/fixtures/claude-ds-smoke.log +1 -1
  24. package/llms.txt +29 -0
  25. package/package.json +13 -6
  26. package/skills/route-orch/SKILL.md +8 -6
  27. package/src/adapters/claude-ds.ts +68 -22
  28. package/src/adapters/grok.ts +10 -5
  29. package/src/adapters/index.ts +2 -0
  30. package/src/adapters/openrouter.test.ts +57 -0
  31. package/src/adapters/openrouter.ts +80 -0
  32. package/src/adapters/types.ts +2 -0
  33. package/src/cli.test.ts +30 -9
  34. package/src/cli.ts +117 -24
  35. package/src/config.ts +37 -6
  36. package/src/health.ts +8 -6
  37. package/src/integration.test.ts +174 -0
  38. package/src/jobs.ts +85 -11
  39. package/src/mark-complete.ts +2 -6
  40. package/src/openrouter-run.ts +102 -0
  41. package/src/runtime.ts +9 -4
  42. package/src/secrets.ts +21 -4
  43. package/src/tmux.ts +20 -8
  44. package/src/util.ts +3 -1
  45. package/docs/audit-2026-08-10-sol-grok-kimi.md +0 -64
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # cursor-route
2
2
 
3
- **Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) are the parallel army.**
3
+ **Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter (easy) are the parallel army.**
4
4
 
5
5
  Lane-aware `/route` orchestration in tmux — not another multi-provider fleet, not a Codex clone.
6
6
 
@@ -17,7 +17,7 @@ cursor-route capture <jobId>
17
17
  |---------|-------|---------|------|
18
18
  | [codex-orchestrator](https://github.com/kingbootoshi/codex-orchestrator) | Claude Code | Codex | Viral tmux panes |
19
19
  | [CAO](https://github.com/awslabs/cli-agent-orchestrator) | Supervisor CLI | Many (incl. Cursor CLI) | Enterprise MCP + Web UI |
20
- | **cursor-route** | **Cursor Agent** | **Grok CLI + claude-ds** | Cost-aware lanes you already pay for |
20
+ | **cursor-route** | **Cursor Agent** | **Grok CLI + claude-ds + OpenRouter (easy)** | Cost-aware lanes you already pay for |
21
21
 
22
22
  If you already live in Cursor, X Premium (Grok CLI), and DeepSeek — stop paying a third coding agent just to parallelize.
23
23
 
@@ -28,6 +28,7 @@ If you already live in Cursor, X Premium (Grok CLI), and DeepSeek — stop payin
28
28
  | Cursor | Premium plan | Plan / synthesize / verify (orchestrator) |
29
29
  | Grok CLI | X Premium | `--lane hard` implement |
30
30
  | DeepSeek via claude-ds | DeepSeek API / plan | `--lane mid` implement |
31
+ | OpenRouter free models | OpenRouter API (free tier) | `--lane easy` wording/drafts — non-secret prompts only (see Security) |
31
32
  | Codex / extra Claude | Optional | Not required for v0 |
32
33
 
33
34
  Exact dollars vary — the point is **reuse subscriptions you already have**.
@@ -41,12 +42,13 @@ Exact dollars vary — the point is **reuse subscriptions you already have**.
41
42
  | [tmux](https://github.com/tmux/tmux) | Worker panes |
42
43
  | [Bun](https://bun.sh) *(or Node 20+)* | Runs the CLI |
43
44
  | [Grok CLI](https://x.ai/cli) and/or Claude Code + DeepSeek (`claude-ds`) | Workers |
45
+ | OpenRouter API key (`OPENROUTER_API_KEY`) | Easy lane (`--lane easy` / `--worker openrouter`) |
44
46
  | `script(1)` | Job logs (macOS/Linux) |
45
47
 
46
48
  ```bash
47
49
  # Prerequisites
48
50
  brew install tmux # or build tmux into ~/.local
49
- # Bun optional Node 20+ works via pinned tsx
51
+ # Bun or Node 20+ npm package ships compiled dist/ (Node runs it directly)
50
52
 
51
53
  # Preferred install
52
54
  npm i -g cursor-route
@@ -54,9 +56,10 @@ npm i -g cursor-route
54
56
  # Auth workers
55
57
  grok login # if using Grok
56
58
  # configure claude-ds — see DeepSeek setup below
59
+ export OPENROUTER_API_KEY=... # if using the easy lane (see OpenRouter setup below)
57
60
 
58
61
  cursor-route health
59
- # without tmux (CI / headless):
62
+ # without tmux / workers (CI / headless infra smoke):
60
63
  CURSOR_ROUTE_RELAXED=1 cursor-route health
61
64
  ```
62
65
 
@@ -98,6 +101,7 @@ Then say **`/route-orch`** or **spawn workers** in Cursor — the skill delegate
98
101
 
99
102
  | Lane | Worker | Intent |
100
103
  |------|--------|--------|
104
+ | `easy` | `openrouter` | Wording / drafts on OpenRouter free models (non-secret prompts only — see Security) |
101
105
  | `mid` | `claude-ds` | Default implement on DeepSeek |
102
106
  | `hard` | `grok` | Hard implement on Grok CLI |
103
107
 
@@ -130,6 +134,25 @@ Persist the same vars under `~/.claude/settings.json` → `"env": { … }` if yo
130
134
 
131
135
  No DeepSeek yet? Use `--lane hard` / `--worker grok` (X Premium).
132
136
 
137
+ ## OpenRouter setup (the free easy lane)
138
+
139
+ `--lane easy` / `--worker openrouter` sends wording/draft prompts to OpenRouter's
140
+ free model route (`openrouter/free`). Get a key at [openrouter.ai/keys](https://openrouter.ai/keys).
141
+
142
+ ```bash
143
+ export OPENROUTER_API_KEY=sk-or-v1-... # from openrouter.ai/keys
144
+ # optional:
145
+ export CURSOR_ROUTE_OPENROUTER_MODEL=openrouter/free # default
146
+ export OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 # default
147
+
148
+ cursor-route health # worker:openrouter should be ✓
149
+ cursor-route start --lane easy "Rewrite this FAQ answer in 3 sentences"
150
+ ```
151
+
152
+ **Non-secret prompts only:** free OpenRouter models may log prompts, so the easy lane is for
153
+ **wording/drafts without credentials**. The same refuse gate as every lane blocks
154
+ key-shaped material in `start` / `send`, and the runner re-checks the prompt file.
155
+
133
156
  ## Jobs directory
134
157
 
135
158
  Jobs default to `~/.local/share/cursor-route/jobs` (override with `CURSOR_ROUTE_JOBS_DIR`).
@@ -141,21 +164,65 @@ This is **not** inside a git clone of this repo.
141
164
  - See [SECURITY.md](./SECURITY.md)
142
165
  - Local orchestration only — no LIVE Discord / trading egress demos
143
166
 
144
- ## Also from Cemini
167
+ ## FAQ
168
+
169
+ **What is cursor-route?**
170
+ 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 for wording/drafts.
171
+
172
+ **How is this different from Codex orchestrator?**
173
+ 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.
174
+
175
+ **Does mid lane use Anthropic Claude?**
176
+ 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`.
177
+
178
+ **How do I install?**
179
+ 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.
180
+
181
+ **Is it free?**
182
+ 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`).
183
+
184
+ ## Related
185
+
186
+ - Newsletter: [Outlier Weekly](https://outlierweekly.substack.com)
187
+ - YouTube: [@Cemini23](https://www.youtube.com/@Cemini23)
188
+ - Agent meta-wiki: [cemini-claude-code-CCC](https://github.com/cemini23/cemini-claude-code-CCC)
189
+ - Products: [Atto](https://youratto.com) · [GuruWatcher](https://guruwatcher.com)
190
+ - Agent toolkit: [vet](https://github.com/cemini23/vet) · [wikilint](https://github.com/cemini23/wikilint) · [phase0](https://github.com/cemini23/phase0) · [agent-toolkit-demo](https://github.com/cemini23/agent-toolkit-demo) · [ara-schema](https://github.com/cemini23/ara-schema) · [cursor-audit](https://github.com/cemini23/agent-toolkit-demo/tree/main/skills/cursor-audit) · [super-audit](https://github.com/cemini23/agent-toolkit-demo/tree/main/skills/super-audit)
191
+ - Public wikis: [Gambling](https://github.com/cemini23/Gambling-wiki) · [Game Dev](https://github.com/cemini23/Game-Dev-wiki) · [SEO/GEO](https://github.com/cemini23/SEO-GEO-B-M-Wiki) · [Cybersecurity](https://github.com/cemini23/Cybersecurity-wiki) · [3D Printing](https://github.com/cemini23/3D-Printing-Wiki) · [Image Gen](https://github.com/cemini23/uncensored-image-gen-wiki)
192
+ - Trading: [world-cup-bot](https://github.com/cemini23/world-cup-bot)
193
+ - Donation wallets (canonical): [SUPPORT.md](SUPPORT.md)
194
+
195
+ ## Support
196
+
197
+ Thank you for your support — stars, issues, shares, and tips all help keep this CLI and the broader Cemini open-research stack alive.
198
+
199
+ If you’d like to tip, use the **donation-only** addresses below (not trading or production wallets). Prefer following the work? These are the best places to start:
200
+
201
+ | Project | Link |
202
+ |---------|------|
203
+ | **Outlier Weekly** (methodology newsletter) | [outlierweekly.substack.com](https://outlierweekly.substack.com) |
204
+ | **Atto** — organize Italian family documents on your computer | [youratto.com](https://youratto.com) |
205
+ | **GuruWatcher** — Discord alerts for your newsletter’s price levels | [guruwatcher.com](https://guruwatcher.com) |
206
+ | **YouTube** | [@Cemini23](https://www.youtube.com/@Cemini23) |
207
+
208
+ | Chain family | Address |
209
+ |--------------|---------|
210
+ | **EVM** (Ethereum, Polygon, Base, Arbitrum, …) | `0x444C5C2eC439E0382aa5a17F70313c536BcC5D58` |
211
+ | **Solana / SVM** | `J4zNn4hK9jTrKBFY8sbAGJHLoZvXvQf4B9pQSbSrocZE` |
212
+ | **Polymarket** (referral) | [polymarket.com/?r=Cemini23](https://polymarket.com/?r=Cemini23) |
213
+
214
+ Full wallet note: [SUPPORT.md](SUPPORT.md) · canon also in [CCC SUPPORT.md](https://github.com/cemini23/cemini-claude-code-CCC/blob/main/SUPPORT.md).
145
215
 
146
- - [Atto](https://youratto.com) genealogy / archival agents
147
- - [GuruWatcher](https://guruwatcher.com) — alert workflows
148
- - Cemini research & trading tooling (private)
216
+ We’re grateful you’re here. Thank you for your support.
149
217
 
150
218
  ## License
151
219
 
152
- MIT © Cemini
220
+ MIT © Cemini — see [LICENSE](LICENSE).
153
221
 
154
222
  ## Roadmap (explicitly later)
155
223
 
156
- - npm publish + Homebrew tap
224
+ - Homebrew tap
157
225
  - Native DeepSeek harness adapter
158
226
  - Codebase map injection (`--map`)
159
227
  - Cursor CLI `agent` as alternate supervisor
160
- - Web UI / CAO-style MCP supervisor
161
- - OpenRouter easy lane (secrets-safe)
228
+ - Web UI / CAO-style MCP supervisor
package/SECURITY.md CHANGED
@@ -14,13 +14,16 @@ Do **not** open a public issue for credential leaks or RCE-class bugs.
14
14
  ## Hard rules for users and agents
15
15
 
16
16
  1. **No secrets in prompts or job files.** Do not paste API keys, SSH private keys, Discord tokens, or `.env` contents into `cursor-route start` / `send` prompts.
17
- 2. **Always-approve is powerful.** Default headless workers skip interactive permission prompts (`--always-approve` / `--dangerously-skip-permissions`). Opt out with `--ask` / `CURSOR_ROUTE_ASK=1` when you need HITL.
18
- 3. **Install from known channels only.** Prefer `npm i -g cursor-route` or a git clone of this repo. Do not pipe unknown `curl | bash` installers as the primary path.
19
- 4. **Workers inherit your auth.** Grok CLI and Claude Code / claude-ds use *your* local login. Treat worker panes like you would treat your own shell.
20
- 5. **No LIVE Discord / trading egress demos** from this tool. Orchestration is local.
17
+ 2. **Free OpenRouter easy-lane models may log prompts.** `--lane easy` / `--worker openrouter` is for **non-secret** wording/drafts only. The refuse gate (`start` / `send`, plus the runner's re-check) applies to every lane — never send key-shaped material to the easy lane.
18
+ 3. **Always-approve is powerful.** Default headless workers skip interactive permission prompts (`--always-approve` / `--dangerously-skip-permissions`). Opt out with `--ask` / `CURSOR_ROUTE_ASK=1` when you need HITL.
19
+ 4. **Install from known channels only.** Prefer `npm i -g cursor-route` or a git clone of this repo. Do not pipe unknown `curl | bash` installers as the primary path.
20
+ 5. **Workers inherit your auth.** Grok CLI and Claude Code / claude-ds use *your* local login. Treat worker panes like you would treat your own shell.
21
+ 6. **No LIVE Discord / trading egress demos** from this tool. Orchestration is local.
21
22
 
22
23
  ## Supply chain
23
24
 
24
- - Runtime dependencies: **none** (Node/Bun + system `tmux` + worker CLIs).
25
+ - Declared runtime npm dependencies: **none**. System needs: Node ≥20 or Bun, `tmux`, worker CLIs (Grok / claude-ds), and an OpenRouter API key for the easy lane.
26
+ - The npm package ships a compiled `dist/` (plain JS, no loader). The bin shim and the completion hook run the compiled JS via `node`; there is **no network fetch of `tsx` on the happy path**.
27
+ - Running from a git clone without a build uses Bun to execute `src/` directly (Bun runs TypeScript natively) — also offline-friendly.
25
28
  - Review `package.json` before upgrading.
26
29
  - Prefer pinned git tags for production installs.
package/SUPPORT.md ADDED
@@ -0,0 +1,24 @@
1
+ # Support Cemini open research
2
+
3
+ Thank you for your interest — and for any support, large or small. Tips and kind words both help keep Cemini research and tooling open.
4
+
5
+ Voluntary tips fund wikis, OSS tooling, and public research. **Donation-only addresses** — not trading or production wallets.
6
+
7
+ | Chain family | Address |
8
+ |--------------|---------|
9
+ | **EVM** (Ethereum, Polygon, Base, Arbitrum, …) | `0x444C5C2eC439E0382aa5a17F70313c536BcC5D58` |
10
+ | **Solana / SVM** | `J4zNn4hK9jTrKBFY8sbAGJHLoZvXvQf4B9pQSbSrocZE` |
11
+ | **Polymarket** (referral) | [polymarket.com/?r=Cemini23](https://polymarket.com/?r=Cemini23) |
12
+
13
+ If you’d rather follow along or try something we ship:
14
+
15
+ - Newsletter — [Outlier Weekly](https://outlierweekly.substack.com) (Substack)
16
+ - Genealogy kit — [youratto.com](https://youratto.com)
17
+ - Newsletter parameter alerts — [guruwatcher.com](https://guruwatcher.com)
18
+ - YouTube — [@Cemini23](https://www.youtube.com/@Cemini23)
19
+ - Agent meta-wiki — [cemini-claude-code-CCC](https://github.com/cemini23/cemini-claude-code-CCC)
20
+ - This CLI — [cursor-route](https://github.com/cemini23/cursor-route)
21
+
22
+ Canonical federation copy: [CCC SUPPORT.md](https://github.com/cemini23/cemini-claude-code-CCC/blob/main/SUPPORT.md).
23
+
24
+ We’re grateful you’re here. Thank you for your support.
package/bin/cursor-route CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # cursor-route — thin launcher (Bun preferred, Node+tsx fallback)
2
+ # cursor-route — thin launcher (compiled dist via node, else Bun on src)
3
3
  set -euo pipefail
4
4
  SOURCE="${BASH_SOURCE[0]}"
5
5
  while [ -L "$SOURCE" ]; do
@@ -9,16 +9,14 @@ while [ -L "$SOURCE" ]; do
9
9
  done
10
10
  SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
11
11
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
12
- CLI="$ROOT/src/cli.ts"
13
12
 
14
- if command -v bun >/dev/null 2>&1; then
15
- exec bun "$CLI" "$@"
13
+ if [ -f "$ROOT/dist/cli.js" ] && command -v node >/dev/null 2>&1; then
14
+ exec node "$ROOT/dist/cli.js" "$@"
16
15
  fi
17
16
 
18
- if command -v npx >/dev/null 2>&1; then
19
- # Pin tsx for supply-chain predictability (no floating --yes latest)
20
- exec npx --yes tsx@4.19.4 "$CLI" "$@"
17
+ if command -v bun >/dev/null 2>&1; then
18
+ exec bun "$ROOT/src/cli.ts" "$@"
21
19
  fi
22
20
 
23
- echo "cursor-route: need Bun (https://bun.sh) or Node 20+ with npx" >&2
21
+ echo "cursor-route: no compiled build (run 'bun run build') and no Bun — need Node 20+ or Bun (https://bun.sh)" >&2
24
22
  exit 127
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * npm bin entry — prefers Bun, falls back to pinned tsx.
3
+ * npm bin redirect — prefers compiled dist (node), else Bun on src.
4
+ * The shell launcher (bin/cursor-route) is the primary npm bin entry;
5
+ * this file keeps direct `node bin/cursor-route.js` working the same way.
4
6
  */
5
7
  import { spawnSync } from "node:child_process";
8
+ import { existsSync } from "node:fs";
6
9
  import { fileURLToPath } from "node:url";
7
10
  import { dirname, join } from "node:path";
8
11
 
9
12
  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
10
- const cli = join(root, "src", "cli.ts");
11
13
  const args = process.argv.slice(2);
12
14
 
13
15
  function run(cmd, cmdArgs) {
@@ -16,11 +18,12 @@ function run(cmd, cmdArgs) {
16
18
  process.exit(r.status ?? 1);
17
19
  }
18
20
 
19
- const bun = spawnSync("bun", ["--version"], { encoding: "utf8" });
20
- if (bun.status === 0) {
21
- run("bun", [cli, ...args]);
21
+ function main() {
22
+ const distCli = join(root, "dist", "cli.js");
23
+ if (existsSync(distCli) && run("node", [distCli, ...args]) !== null) return;
24
+ if (run("bun", [join(root, "src", "cli.ts"), ...args]) !== null) return;
25
+ console.error("cursor-route: no compiled build (run 'bun run build') and no Bun — need Node 20+ or Bun (https://bun.sh)");
26
+ process.exit(127);
22
27
  }
23
28
 
24
- run("npx", ["--yes", "tsx@4.19.4", cli, ...args]);
25
- console.error("cursor-route: need Bun (https://bun.sh) or Node 20+ with npx");
26
- process.exit(127);
29
+ main();
@@ -0,0 +1,189 @@
1
+ import { execSync } from "node:child_process";
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { shellQuote } from "../util.js";
6
+ /**
7
+ * Mid-lane = DeepSeek (cheap). The Claude Code binary is only a harness —
8
+ * requests must hit DeepSeek, not Anthropic.
9
+ *
10
+ * Resolution order:
11
+ * 1. claude-ds / deepseek-claude shims
12
+ * 2. stock `claude` when ANTHROPIC_BASE_URL (env or ~/.claude/settings.json)
13
+ * points at api.deepseek.com — auto-ok, no opt-in
14
+ * 3. stock `claude` on Anthropic only if CURSOR_ROUTE_ALLOW_ANTHROPIC=1
15
+ * (explicit expensive escape hatch — not the product default)
16
+ */
17
+ function which(cmd) {
18
+ try {
19
+ return (execSync(`command -v ${shellQuote(cmd)}`, {
20
+ encoding: "utf8",
21
+ stdio: ["ignore", "pipe", "ignore"],
22
+ }).trim() || null);
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ /** True when URL hostname is deepseek.com (or a subdomain). */
29
+ export function isDeepSeekBaseUrl(url) {
30
+ try {
31
+ const u = new URL(url);
32
+ const host = u.hostname.toLowerCase();
33
+ return host === "deepseek.com" || host.endsWith(".deepseek.com");
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ function deepseekBaseFromSettings() {
40
+ // Home settings only — do not trust cwd/.claude/settings.json (spoof / exfil risk)
41
+ const p = join(homedir(), ".claude", "settings.json");
42
+ if (!existsSync(p))
43
+ return null;
44
+ try {
45
+ const j = JSON.parse(readFileSync(p, "utf8"));
46
+ const url = j.env?.ANTHROPIC_BASE_URL;
47
+ if (url && isDeepSeekBaseUrl(url))
48
+ return url;
49
+ }
50
+ catch {
51
+ /* ignore */
52
+ }
53
+ return null;
54
+ }
55
+ /** Resolved DeepSeek base URL for the mid-lane harness, or null. */
56
+ export function resolvedDeepSeekBaseUrl() {
57
+ for (const candidate of [
58
+ process.env.ANTHROPIC_BASE_URL,
59
+ process.env.CURSOR_ROUTE_ANTHROPIC_BASE_URL,
60
+ deepseekBaseFromSettings(),
61
+ ]) {
62
+ if (candidate && isDeepSeekBaseUrl(candidate))
63
+ return candidate;
64
+ }
65
+ return null;
66
+ }
67
+ /** True when Claude Code harness is routed to DeepSeek (cheap path). */
68
+ export function isDeepSeekRouted() {
69
+ return Boolean(resolvedDeepSeekBaseUrl());
70
+ }
71
+ function resolveClaudeDs() {
72
+ // Env override lets tests pin a fake claude-ds (and power users pick a specific binary).
73
+ if (process.env.CURSOR_ROUTE_CLAUDE_DS_BIN) {
74
+ return {
75
+ binary: process.env.CURSOR_ROUTE_CLAUDE_DS_BIN,
76
+ mode: "claude-ds (CURSOR_ROUTE_CLAUDE_DS_BIN override)",
77
+ };
78
+ }
79
+ for (const c of [
80
+ { cmd: "claude-ds", mode: "claude-ds (DeepSeek shim)" },
81
+ { cmd: "deepseek-claude", mode: "deepseek-claude" },
82
+ ]) {
83
+ const path = which(c.cmd);
84
+ if (path)
85
+ return { binary: path, mode: c.mode };
86
+ }
87
+ const claude = which("claude");
88
+ if (!claude)
89
+ return null;
90
+ if (isDeepSeekRouted()) {
91
+ return {
92
+ binary: claude,
93
+ mode: "claude → DeepSeek (ANTHROPIC_BASE_URL)",
94
+ };
95
+ }
96
+ // Expensive Anthropic path — opt-in only so we never silently bill frontier rates
97
+ if (process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC === "1" ||
98
+ process.env.CURSOR_ROUTE_ALLOW_STOCK_CLAUDE === "1" // legacy alias
99
+ ) {
100
+ return {
101
+ binary: claude,
102
+ mode: "claude → Anthropic (CURSOR_ROUTE_ALLOW_ANTHROPIC=1)",
103
+ };
104
+ }
105
+ return null;
106
+ }
107
+ /**
108
+ * Env that must reach stock `claude` for DeepSeek routing.
109
+ * Passed via process/tmux env — never interpolated into the printed command.
110
+ */
111
+ function deepSeekWorkerEnv() {
112
+ const base = resolvedDeepSeekBaseUrl();
113
+ if (!base)
114
+ return undefined;
115
+ const env = { ANTHROPIC_BASE_URL: base };
116
+ const token = process.env.ANTHROPIC_AUTH_TOKEN ||
117
+ process.env.ANTHROPIC_API_KEY ||
118
+ process.env.DEEPSEEK_API_KEY ||
119
+ "";
120
+ if (token)
121
+ env.ANTHROPIC_AUTH_TOKEN = token;
122
+ const model = process.env.ANTHROPIC_MODEL;
123
+ if (model)
124
+ env.ANTHROPIC_MODEL = model;
125
+ return env;
126
+ }
127
+ export const claudeDsAdapter = {
128
+ kind: "claude-ds",
129
+ label: "DeepSeek (via Claude Code harness)",
130
+ health() {
131
+ const resolved = resolveClaudeDs();
132
+ if (!resolved) {
133
+ const hasClaude = Boolean(which("claude"));
134
+ return {
135
+ worker: "claude-ds",
136
+ ok: false,
137
+ binary: null,
138
+ detail: hasClaude
139
+ ? "claude on PATH but ANTHROPIC_BASE_URL is not DeepSeek — set https://api.deepseek.com/anthropic (see README). Refusing Anthropic default so mid-lane stays cheap."
140
+ : "need claude-ds, deepseek-claude, or claude + DeepSeek ANTHROPIC_BASE_URL — see README DeepSeek setup",
141
+ };
142
+ }
143
+ return {
144
+ worker: "claude-ds",
145
+ ok: true,
146
+ binary: resolved.binary,
147
+ detail: `ok (${resolved.mode})`,
148
+ };
149
+ },
150
+ buildLaunch({ promptFile, cwd, alwaysApprove }) {
151
+ const resolved = resolveClaudeDs();
152
+ if (!resolved) {
153
+ throw new Error("DeepSeek worker not available — run: cursor-route health");
154
+ }
155
+ const ask = process.env.CURSOR_ROUTE_ASK === "1" || process.env.CLAUDE_DS_ASK === "1";
156
+ const skip = alwaysApprove && !ask;
157
+ // Stock `claude` needs DeepSeek env injected into the worker process
158
+ // (tmux panes may not inherit client env from a long-lived server).
159
+ const env = resolved.mode.startsWith("claude → DeepSeek") ? deepSeekWorkerEnv() : undefined;
160
+ if (resolved.mode.startsWith("claude-ds")) {
161
+ const parts = [
162
+ shellQuote(resolved.binary),
163
+ "-PromptFile",
164
+ shellQuote(promptFile),
165
+ ];
166
+ if (skip)
167
+ parts.push("--dangerously-skip-permissions");
168
+ return {
169
+ worker: "claude-ds",
170
+ command: `cd ${shellQuote(cwd)} && ${parts.join(" ")}`,
171
+ alwaysApprove: skip,
172
+ env,
173
+ };
174
+ }
175
+ const parts = [
176
+ shellQuote(resolved.binary),
177
+ "-p",
178
+ `"$(cat ${shellQuote(promptFile)})"`,
179
+ ];
180
+ if (skip)
181
+ parts.push("--dangerously-skip-permissions");
182
+ return {
183
+ worker: "claude-ds",
184
+ command: `cd ${shellQuote(cwd)} && ${parts.join(" ")}`,
185
+ alwaysApprove: skip,
186
+ env,
187
+ };
188
+ },
189
+ };
@@ -0,0 +1,57 @@
1
+ import { execSync } from "node:child_process";
2
+ import { shellQuote } from "../util.js";
3
+ function findGrok() {
4
+ // Env override lets tests pin a fake grok (and power users pick a specific binary).
5
+ if (process.env.CURSOR_ROUTE_GROK_BIN)
6
+ return process.env.CURSOR_ROUTE_GROK_BIN;
7
+ try {
8
+ return (execSync("command -v grok", {
9
+ encoding: "utf8",
10
+ stdio: ["ignore", "pipe", "ignore"],
11
+ }).trim() || null);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export const grokAdapter = {
18
+ kind: "grok",
19
+ label: "Grok CLI (xAI)",
20
+ health() {
21
+ const binary = findGrok();
22
+ if (!binary) {
23
+ return {
24
+ worker: "grok",
25
+ ok: false,
26
+ binary: null,
27
+ detail: "grok not on PATH — install from https://x.ai/cli then run: grok login",
28
+ };
29
+ }
30
+ return {
31
+ worker: "grok",
32
+ ok: true,
33
+ binary,
34
+ detail: "ok (auth checked at first start — run grok login if jobs fail)",
35
+ };
36
+ },
37
+ buildLaunch({ promptFile, cwd, alwaysApprove }) {
38
+ const binary = findGrok() || "grok";
39
+ const parts = [
40
+ shellQuote(binary),
41
+ "-p",
42
+ `"$(cat ${shellQuote(promptFile)})"`,
43
+ "--cwd",
44
+ shellQuote(cwd),
45
+ "--no-auto-update",
46
+ "--output-format",
47
+ "plain",
48
+ ];
49
+ if (alwaysApprove)
50
+ parts.push("--always-approve");
51
+ return {
52
+ worker: "grok",
53
+ command: parts.join(" "),
54
+ alwaysApprove,
55
+ };
56
+ },
57
+ };
@@ -0,0 +1,17 @@
1
+ import { grokAdapter } from "./grok.js";
2
+ import { claudeDsAdapter } from "./claude-ds.js";
3
+ import { openRouterAdapter } from "./openrouter.js";
4
+ const registry = {
5
+ grok: grokAdapter,
6
+ "claude-ds": claudeDsAdapter,
7
+ openrouter: openRouterAdapter,
8
+ };
9
+ export function getAdapter(worker) {
10
+ const a = registry[worker];
11
+ if (!a)
12
+ throw new Error(`Unknown worker: ${worker}`);
13
+ return a;
14
+ }
15
+ export function allAdapters() {
16
+ return Object.values(registry);
17
+ }
@@ -0,0 +1,78 @@
1
+ import { existsSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
4
+ import { shellQuote } from "../util.js";
5
+ import { openRouterModel, openRouterBaseUrl } from "../config.js";
6
+ /**
7
+ * Resolve how to invoke the one-shot runner. Prefer the compiled dist via node
8
+ * (no loader); else Bun on src. No npx/tsx — same policy as mark-complete.
9
+ */
10
+ function resolveRunner() {
11
+ const here = dirname(fileURLToPath(import.meta.url));
12
+ const compiled = join(here, "..", "..", "dist", "openrouter-run.js");
13
+ if (existsSync(compiled)) {
14
+ return { command: `node ${shellQuote(compiled)}` };
15
+ }
16
+ const srcFile = join(here, "..", "openrouter-run.ts");
17
+ if (existsSync(srcFile)) {
18
+ return { command: `bun ${shellQuote(srcFile)}` };
19
+ }
20
+ return null;
21
+ }
22
+ function openRouterEnv() {
23
+ const key = process.env.OPENROUTER_API_KEY;
24
+ if (!key)
25
+ return undefined;
26
+ const env = { OPENROUTER_API_KEY: key };
27
+ const model = process.env.CURSOR_ROUTE_OPENROUTER_MODEL;
28
+ if (model)
29
+ env.CURSOR_ROUTE_OPENROUTER_MODEL = model;
30
+ const base = process.env.OPENROUTER_BASE_URL;
31
+ if (base)
32
+ env.OPENROUTER_BASE_URL = base;
33
+ return env;
34
+ }
35
+ export const openRouterAdapter = {
36
+ kind: "openrouter",
37
+ label: "OpenRouter (free easy lane)",
38
+ health() {
39
+ const runner = resolveRunner();
40
+ if (!process.env.OPENROUTER_API_KEY) {
41
+ return {
42
+ worker: "openrouter",
43
+ ok: false,
44
+ binary: runner?.command ?? null,
45
+ detail: "OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane model defaults to openrouter/free)",
46
+ };
47
+ }
48
+ if (!runner) {
49
+ return {
50
+ worker: "openrouter",
51
+ ok: false,
52
+ binary: null,
53
+ detail: "openrouter-run not found — run bun run build (or use Bun from a source clone)",
54
+ };
55
+ }
56
+ return {
57
+ worker: "openrouter",
58
+ ok: true,
59
+ binary: runner.command,
60
+ detail: `ok (model ${openRouterModel()} @ ${openRouterBaseUrl()})`,
61
+ };
62
+ },
63
+ buildLaunch({ promptFile }) {
64
+ const runner = resolveRunner();
65
+ if (!runner)
66
+ throw new Error("openrouter runner not available — run: bun run build");
67
+ // Missing key is tolerated here so `--dry-run` can still print the command;
68
+ // real starts are gated by the health preflight (which requires the key).
69
+ const env = openRouterEnv();
70
+ // No interactive approval concept for a pure HTTP call — nothing to auto-approve.
71
+ return {
72
+ worker: "openrouter",
73
+ command: `${runner.command} --prompt-file ${shellQuote(promptFile)}`,
74
+ alwaysApprove: false,
75
+ env,
76
+ };
77
+ },
78
+ };
@@ -0,0 +1 @@
1
+ export {};