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
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,6 +42,7 @@ 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
|
|
@@ -54,6 +56,7 @@ 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
62
|
# without tmux / workers (CI / headless infra smoke):
|
|
@@ -77,7 +80,9 @@ mkdir -p .cursor/skills
|
|
|
77
80
|
cp -R ~/.cursor-route-src/skills/route-orch .cursor/skills/
|
|
78
81
|
```
|
|
79
82
|
|
|
80
|
-
Then say **`/route-orch`** or **spawn workers** in Cursor — the skill delegates to `cursor-route` (does not replace private
|
|
83
|
+
Then say **`/route-orch`** or **spawn workers** in Cursor — the skill delegates to `cursor-route` (does not replace a private in-house `/route` skill).
|
|
84
|
+
|
|
85
|
+
Working notes for this repo (edit in place): [docs/briefs/WORKING.md](./docs/briefs/WORKING.md).
|
|
81
86
|
|
|
82
87
|
## Commands
|
|
83
88
|
|
|
@@ -98,11 +103,26 @@ Then say **`/route-orch`** or **spawn workers** in Cursor — the skill delegate
|
|
|
98
103
|
|
|
99
104
|
| Lane | Worker | Intent |
|
|
100
105
|
|------|--------|--------|
|
|
101
|
-
| `
|
|
106
|
+
| `easy` | `openrouter` | Wording / drafts on OpenRouter free models (non-secret prompts only — see Security) |
|
|
107
|
+
| `mid` | `claude-ds` | Default implement on DeepSeek (**Flash** by default) |
|
|
102
108
|
| `hard` | `grok` | Hard implement on Grok CLI |
|
|
103
109
|
|
|
104
110
|
Always-approve is **on** by default. Opt out: `--ask` or `CURSOR_ROUTE_ASK=1`.
|
|
105
111
|
|
|
112
|
+
### Mid models (Flash vs Pro)
|
|
113
|
+
|
|
114
|
+
| Flag | Model id | When |
|
|
115
|
+
|------|----------|------|
|
|
116
|
+
| `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute |
|
|
117
|
+
| `--model pro` | `deepseek-v4-pro` | Harder mid / Grok **usage** stand-in |
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
cursor-route start --lane mid "…" # Flash
|
|
121
|
+
cursor-route start --lane mid --model pro "…" # Pro
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Grok auth ≠ usage-out:** if `cursor-route health` shows `worker:grok` ✗, run `grok login` (or set `XAI_API_KEY`). That is auth. Quota / subscription usage exhausted is different — use `--lane mid --model pro` as the stand-in, not a missing login.
|
|
125
|
+
|
|
106
126
|
## DeepSeek setup (the cheap mid-lane — this is the point)
|
|
107
127
|
|
|
108
128
|
`cursor-route` mid lane runs **DeepSeek**, not Anthropic. Claude Code is only the terminal harness; bills go to DeepSeek when `ANTHROPIC_BASE_URL` points at them.
|
|
@@ -114,22 +134,44 @@ npm i -g @anthropic-ai/claude-code
|
|
|
114
134
|
|
|
115
135
|
export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
|
|
116
136
|
export ANTHROPIC_AUTH_TOKEN=YOUR_DEEPSEEK_API_KEY # from platform.deepseek.com
|
|
117
|
-
|
|
137
|
+
# Optional shell defaults (CLI --model overrides ANTHROPIC_MODEL for the job):
|
|
118
138
|
export ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash
|
|
119
139
|
export CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash
|
|
120
140
|
|
|
121
141
|
cursor-route health # worker:claude-ds should be ✓
|
|
122
|
-
cursor-route start --lane mid "…"
|
|
142
|
+
cursor-route start --lane mid "…" # Flash (default)
|
|
143
|
+
cursor-route start --lane mid --model pro "…" # Pro when you need it
|
|
123
144
|
```
|
|
124
145
|
|
|
125
146
|
Persist the same vars under `~/.claude/settings.json` → `"env": { … }` if you want them every shell.
|
|
126
147
|
|
|
127
|
-
**Also accepted:** `claude-ds` or `deepseek-claude` on PATH (Cemini shims).
|
|
148
|
+
**Also accepted:** `claude-ds` or `deepseek-claude` on PATH (Cemini shims). The adapter passes `-Model deepseek-v4-flash|deepseek-v4-pro`.
|
|
149
|
+
|
|
150
|
+
**Reserved:** `--worker deepseek` is a slot for an official DeepSeek coding harness — not wired yet. Mid stays on `claude-ds`.
|
|
128
151
|
|
|
129
152
|
**Not the default:** bare `claude` still talking to Anthropic. Health refuses that so a misconfigured install cannot silently burn frontier $ rates. Escape hatch only: `CURSOR_ROUTE_ALLOW_ANTHROPIC=1`.
|
|
130
153
|
|
|
131
154
|
No DeepSeek yet? Use `--lane hard` / `--worker grok` (X Premium).
|
|
132
155
|
|
|
156
|
+
## OpenRouter setup (the free easy lane)
|
|
157
|
+
|
|
158
|
+
`--lane easy` / `--worker openrouter` sends wording/draft prompts to OpenRouter's
|
|
159
|
+
free model route (`openrouter/free`). Get a key at [openrouter.ai/keys](https://openrouter.ai/keys).
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
export OPENROUTER_API_KEY=sk-or-v1-... # from openrouter.ai/keys
|
|
163
|
+
# optional:
|
|
164
|
+
export CURSOR_ROUTE_OPENROUTER_MODEL=openrouter/free # default
|
|
165
|
+
export OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 # default
|
|
166
|
+
|
|
167
|
+
cursor-route health # worker:openrouter should be ✓
|
|
168
|
+
cursor-route start --lane easy "Rewrite this FAQ answer in 3 sentences"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Non-secret prompts only:** free OpenRouter models may log prompts, so the easy lane is for
|
|
172
|
+
**wording/drafts without credentials**. The same refuse gate as every lane blocks
|
|
173
|
+
key-shaped material in `start` / `send`, and the runner re-checks the prompt file.
|
|
174
|
+
|
|
133
175
|
## Jobs directory
|
|
134
176
|
|
|
135
177
|
Jobs default to `~/.local/share/cursor-route/jobs` (override with `CURSOR_ROUTE_JOBS_DIR`).
|
|
@@ -144,19 +186,19 @@ This is **not** inside a git clone of this repo.
|
|
|
144
186
|
## FAQ
|
|
145
187
|
|
|
146
188
|
**What is cursor-route?**
|
|
147
|
-
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,
|
|
189
|
+
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.
|
|
148
190
|
|
|
149
191
|
**How is this different from Codex orchestrator?**
|
|
150
192
|
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.
|
|
151
193
|
|
|
152
194
|
**Does mid lane use Anthropic Claude?**
|
|
153
|
-
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`.
|
|
195
|
+
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`).
|
|
154
196
|
|
|
155
197
|
**How do I install?**
|
|
156
198
|
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.
|
|
157
199
|
|
|
158
200
|
**Is it free?**
|
|
159
|
-
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.
|
|
201
|
+
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`).
|
|
160
202
|
|
|
161
203
|
## Related
|
|
162
204
|
|
|
@@ -202,5 +244,4 @@ MIT © Cemini — see [LICENSE](LICENSE).
|
|
|
202
244
|
- Native DeepSeek harness adapter
|
|
203
245
|
- Codebase map injection (`--map`)
|
|
204
246
|
- Cursor CLI `agent` as alternate supervisor
|
|
205
|
-
- Web UI / CAO-style MCP supervisor
|
|
206
|
-
- OpenRouter easy lane (secrets-safe)
|
|
247
|
+
- Web UI / CAO-style MCP supervisor
|
package/SECURITY.md
CHANGED
|
@@ -14,14 +14,15 @@ 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. **
|
|
18
|
-
3. **
|
|
19
|
-
4. **
|
|
20
|
-
5. **
|
|
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
|
-
- Declared runtime npm dependencies: **none**. System needs: Node ≥20 or Bun, `tmux`,
|
|
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.
|
|
25
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**.
|
|
26
27
|
- Running from a git clone without a build uses Bun to execute `src/` directly (Bun runs TypeScript natively) — also offline-friendly.
|
|
27
28
|
- Review `package.json` before upgrading.
|
|
@@ -2,6 +2,7 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { DS_MODEL_IDS, config, resolveDsModelAlias, } from "../config.js";
|
|
5
6
|
import { shellQuote } from "../util.js";
|
|
6
7
|
/**
|
|
7
8
|
* Mid-lane = DeepSeek (cheap). The Claude Code binary is only a harness —
|
|
@@ -104,24 +105,39 @@ function resolveClaudeDs() {
|
|
|
104
105
|
}
|
|
105
106
|
return null;
|
|
106
107
|
}
|
|
108
|
+
function pickModelAlias(requested) {
|
|
109
|
+
if (requested)
|
|
110
|
+
return requested;
|
|
111
|
+
// Env override for power users who already export ANTHROPIC_MODEL
|
|
112
|
+
const fromEnv = process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL;
|
|
113
|
+
if (fromEnv) {
|
|
114
|
+
try {
|
|
115
|
+
return resolveDsModelAlias(fromEnv);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* fall through to default */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return config.defaultDsModel;
|
|
122
|
+
}
|
|
107
123
|
/**
|
|
108
124
|
* Env that must reach stock `claude` for DeepSeek routing.
|
|
109
125
|
* Passed via process/tmux env — never interpolated into the printed command.
|
|
110
126
|
*/
|
|
111
|
-
function deepSeekWorkerEnv() {
|
|
127
|
+
function deepSeekWorkerEnv(modelId) {
|
|
112
128
|
const base = resolvedDeepSeekBaseUrl();
|
|
113
129
|
if (!base)
|
|
114
130
|
return undefined;
|
|
115
|
-
const env = {
|
|
131
|
+
const env = {
|
|
132
|
+
ANTHROPIC_BASE_URL: base,
|
|
133
|
+
ANTHROPIC_MODEL: modelId,
|
|
134
|
+
};
|
|
116
135
|
const token = process.env.ANTHROPIC_AUTH_TOKEN ||
|
|
117
136
|
process.env.ANTHROPIC_API_KEY ||
|
|
118
137
|
process.env.DEEPSEEK_API_KEY ||
|
|
119
138
|
"";
|
|
120
139
|
if (token)
|
|
121
140
|
env.ANTHROPIC_AUTH_TOKEN = token;
|
|
122
|
-
const model = process.env.ANTHROPIC_MODEL;
|
|
123
|
-
if (model)
|
|
124
|
-
env.ANTHROPIC_MODEL = model;
|
|
125
141
|
return env;
|
|
126
142
|
}
|
|
127
143
|
export const claudeDsAdapter = {
|
|
@@ -144,24 +160,30 @@ export const claudeDsAdapter = {
|
|
|
144
160
|
worker: "claude-ds",
|
|
145
161
|
ok: true,
|
|
146
162
|
binary: resolved.binary,
|
|
147
|
-
detail: `ok (${resolved.mode})`,
|
|
163
|
+
detail: `ok (${resolved.mode}; default model ${DS_MODEL_IDS[config.defaultDsModel]})`,
|
|
148
164
|
};
|
|
149
165
|
},
|
|
150
|
-
buildLaunch({ promptFile, cwd, alwaysApprove }) {
|
|
166
|
+
buildLaunch({ promptFile, cwd, alwaysApprove, model }) {
|
|
151
167
|
const resolved = resolveClaudeDs();
|
|
152
168
|
if (!resolved) {
|
|
153
169
|
throw new Error("DeepSeek worker not available — run: cursor-route health");
|
|
154
170
|
}
|
|
171
|
+
const alias = pickModelAlias(model);
|
|
172
|
+
const modelId = DS_MODEL_IDS[alias];
|
|
155
173
|
const ask = process.env.CURSOR_ROUTE_ASK === "1" || process.env.CLAUDE_DS_ASK === "1";
|
|
156
174
|
const skip = alwaysApprove && !ask;
|
|
157
175
|
// Stock `claude` needs DeepSeek env injected into the worker process
|
|
158
176
|
// (tmux panes may not inherit client env from a long-lived server).
|
|
159
|
-
const env = resolved.mode.startsWith("claude → DeepSeek")
|
|
160
|
-
|
|
177
|
+
const env = resolved.mode.startsWith("claude → DeepSeek")
|
|
178
|
+
? deepSeekWorkerEnv(modelId)
|
|
179
|
+
: undefined;
|
|
180
|
+
if (resolved.mode.startsWith("claude-ds") || resolved.mode.startsWith("deepseek-claude")) {
|
|
161
181
|
const parts = [
|
|
162
182
|
shellQuote(resolved.binary),
|
|
163
183
|
"-PromptFile",
|
|
164
184
|
shellQuote(promptFile),
|
|
185
|
+
"-Model",
|
|
186
|
+
shellQuote(modelId),
|
|
165
187
|
];
|
|
166
188
|
if (skip)
|
|
167
189
|
parts.push("--dangerously-skip-permissions");
|
|
@@ -176,6 +198,8 @@ export const claudeDsAdapter = {
|
|
|
176
198
|
shellQuote(resolved.binary),
|
|
177
199
|
"-p",
|
|
178
200
|
`"$(cat ${shellQuote(promptFile)})"`,
|
|
201
|
+
"--model",
|
|
202
|
+
shellQuote(modelId),
|
|
179
203
|
];
|
|
180
204
|
if (skip)
|
|
181
205
|
parts.push("--dangerously-skip-permissions");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reserved slot for the official DeepSeek coding harness when it ships.
|
|
3
|
+
* Mid lane stays on claude-ds until then — do not route jobs here.
|
|
4
|
+
*/
|
|
5
|
+
export const deepseekAdapter = {
|
|
6
|
+
kind: "deepseek",
|
|
7
|
+
label: "Official DeepSeek harness (unreleased)",
|
|
8
|
+
health() {
|
|
9
|
+
return {
|
|
10
|
+
worker: "deepseek",
|
|
11
|
+
ok: false,
|
|
12
|
+
binary: null,
|
|
13
|
+
detail: "unreleased — mid lane uses claude-ds (DeepSeek behind Claude Code). See README.",
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
buildLaunch() {
|
|
17
|
+
throw new Error("Official DeepSeek harness is not available yet — use --lane mid / --worker claude-ds");
|
|
18
|
+
},
|
|
19
|
+
};
|
package/dist/adapters/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { grokAdapter } from "./grok.js";
|
|
2
2
|
import { claudeDsAdapter } from "./claude-ds.js";
|
|
3
|
+
import { openRouterAdapter } from "./openrouter.js";
|
|
4
|
+
import { deepseekAdapter } from "./deepseek.js";
|
|
3
5
|
const registry = {
|
|
4
6
|
grok: grokAdapter,
|
|
5
7
|
"claude-ds": claudeDsAdapter,
|
|
8
|
+
openrouter: openRouterAdapter,
|
|
9
|
+
deepseek: deepseekAdapter,
|
|
6
10
|
};
|
|
7
11
|
export function getAdapter(worker) {
|
|
8
12
|
const a = registry[worker];
|
|
@@ -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
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* cursor-route CLI — Cursor brain, Grok + DeepSeek workers in tmux.
|
|
3
|
+
* cursor-route CLI — Cursor brain, Grok + DeepSeek + OpenRouter (easy) workers in tmux.
|
|
4
4
|
*/
|
|
5
5
|
import { readFileSync, existsSync, realpathSync, statSync } from "node:fs";
|
|
6
6
|
import { resolve, basename } from "node:path";
|
|
7
|
-
import { config, WORKERS, LANES } from "./config.js";
|
|
7
|
+
import { config, WORKERS, LANES, resolveDsModelAlias, } from "./config.js";
|
|
8
8
|
import { runHealth, printHealth } from "./health.js";
|
|
9
9
|
import { startJob, listJobs, readJob, killJob, cleanJobs, jobPaths, refreshStatus, } from "./jobs.js";
|
|
10
10
|
import { capturePane, sendKeys, attachHint, listManagedSessions, sessionExists, } from "./tmux.js";
|
|
@@ -12,7 +12,7 @@ import { looksLikeSecretMaterial, redactSecrets } from "./secrets.js";
|
|
|
12
12
|
function usage(exitCode = 0) {
|
|
13
13
|
console.log(`cursor-route v${config.version}
|
|
14
14
|
|
|
15
|
-
Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) are the parallel army.
|
|
15
|
+
Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter (easy) are the parallel army.
|
|
16
16
|
|
|
17
17
|
Usage:
|
|
18
18
|
cursor-route --version
|
|
@@ -29,13 +29,14 @@ Usage:
|
|
|
29
29
|
cursor-route clean [--days N]
|
|
30
30
|
|
|
31
31
|
Start options:
|
|
32
|
-
--worker <grok|claude-ds>
|
|
33
|
-
--lane <mid|hard>
|
|
34
|
-
--
|
|
35
|
-
--
|
|
36
|
-
--
|
|
37
|
-
--
|
|
38
|
-
--
|
|
32
|
+
--worker <grok|claude-ds|openrouter> Worker adapter (default: grok; deepseek = reserved slot)
|
|
33
|
+
--lane <easy|mid|hard> Lane → worker (easy=openrouter, mid=claude-ds, hard=grok)
|
|
34
|
+
--model <flash|pro> Mid DeepSeek model (default: flash). Ignored by grok/openrouter
|
|
35
|
+
--dir <path> Working directory (default: cwd)
|
|
36
|
+
--ask Disable always-approve for this job
|
|
37
|
+
--dry-run Print launch command; do not start
|
|
38
|
+
--no-tmux Headless background process (no attach/send)
|
|
39
|
+
--json JSON output where supported
|
|
39
40
|
|
|
40
41
|
Env:
|
|
41
42
|
CURSOR_ROUTE_ASK=1 Opt out of always-approve
|
|
@@ -43,8 +44,12 @@ Env:
|
|
|
43
44
|
CURSOR_ROUTE_MAX_JOBS Max active jobs (default: 50)
|
|
44
45
|
CURSOR_ROUTE_RELAXED=1 health OK without tmux/workers (CI / infra smoke)
|
|
45
46
|
CURSOR_ROUTE_ALLOW_ANTHROPIC=1 Allow mid-lane on Anthropic Claude (expensive; not default)
|
|
47
|
+
CURSOR_ROUTE_DS_MODEL Default mid model flash|pro (overridden by --model)
|
|
46
48
|
CURSOR_ROUTE_GROK_BIN Override the grok binary path (tests / power users)
|
|
47
49
|
CURSOR_ROUTE_CLAUDE_DS_BIN Override the claude-ds binary path (tests / power users)
|
|
50
|
+
OPENROUTER_API_KEY OpenRouter key (required for --worker openrouter / --lane easy)
|
|
51
|
+
CURSOR_ROUTE_OPENROUTER_MODEL OpenRouter model (default: openrouter/free)
|
|
52
|
+
OPENROUTER_BASE_URL OpenRouter API base (default: https://openrouter.ai/api/v1)
|
|
48
53
|
`);
|
|
49
54
|
process.exit(exitCode);
|
|
50
55
|
}
|
|
@@ -136,6 +141,13 @@ function asLane(v) {
|
|
|
136
141
|
return v;
|
|
137
142
|
throw new Error(`Invalid --lane ${v}; expected ${LANES.join("|")}`);
|
|
138
143
|
}
|
|
144
|
+
function asDsModel(v) {
|
|
145
|
+
if (v === undefined || v === true)
|
|
146
|
+
return undefined;
|
|
147
|
+
if (typeof v !== "string")
|
|
148
|
+
throw new Error(`Invalid --model; expected flash|pro`);
|
|
149
|
+
return resolveDsModelAlias(v);
|
|
150
|
+
}
|
|
139
151
|
function refuseSecrets(text, context) {
|
|
140
152
|
if (looksLikeSecretMaterial(text)) {
|
|
141
153
|
console.error(`Refusing ${context}: looks like secret key material. Remove tokens/keys and retry.`);
|
|
@@ -193,8 +205,17 @@ async function main() {
|
|
|
193
205
|
if (cmd === "start") {
|
|
194
206
|
requireStringFlag(f, "worker");
|
|
195
207
|
requireStringFlag(f, "lane");
|
|
208
|
+
requireStringFlag(f, "model");
|
|
196
209
|
const promptFile = requireStringFlag(f, "prompt-file");
|
|
197
210
|
const dirFlag = requireStringFlag(f, "dir");
|
|
211
|
+
let model;
|
|
212
|
+
try {
|
|
213
|
+
model = asDsModel(f.model);
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
console.error(e.message);
|
|
217
|
+
process.exit(2);
|
|
218
|
+
}
|
|
198
219
|
let prompt = "";
|
|
199
220
|
if (promptFile) {
|
|
200
221
|
const p = resolve(promptFile);
|
|
@@ -231,6 +252,7 @@ async function main() {
|
|
|
231
252
|
prompt,
|
|
232
253
|
worker: asWorker(f.worker),
|
|
233
254
|
lane: asLane(f.lane),
|
|
255
|
+
model,
|
|
234
256
|
cwd,
|
|
235
257
|
alwaysApprove: !f.ask,
|
|
236
258
|
dryRun: Boolean(f.dryRun),
|
|
@@ -249,10 +271,13 @@ async function main() {
|
|
|
249
271
|
else if (result.dryRun) {
|
|
250
272
|
console.log(`dry-run job ${result.job.id}`);
|
|
251
273
|
console.log(`worker: ${result.job.worker}`);
|
|
274
|
+
if (result.job.model)
|
|
275
|
+
console.log(`model: ${result.job.model}`);
|
|
252
276
|
console.log(`command: ${redactSecrets(result.command || "")}`);
|
|
253
277
|
}
|
|
254
278
|
else {
|
|
255
|
-
|
|
279
|
+
const modelNote = result.job.model ? `/${result.job.model}` : "";
|
|
280
|
+
console.log(`started ${result.job.id} (${result.job.worker}${modelNote})`);
|
|
256
281
|
console.log(`session: ${result.job.tmuxSession}`);
|
|
257
282
|
if (String(result.job.tmuxSession).startsWith("headless-")) {
|
|
258
283
|
console.log(`mode: headless (--no-tmux); use capture/status (no attach/send)`);
|
package/dist/config.js
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { defaultJobsDir } from "./runtime.js";
|
|
4
|
-
export const WORKERS = ["grok", "claude-ds"];
|
|
5
|
-
export const LANES = ["mid", "hard"];
|
|
4
|
+
export const WORKERS = ["grok", "claude-ds", "openrouter", "deepseek"];
|
|
5
|
+
export const LANES = ["easy", "mid", "hard"];
|
|
6
|
+
export const DS_MODELS = ["flash", "pro"];
|
|
7
|
+
export const DS_MODEL_IDS = {
|
|
8
|
+
flash: "deepseek-v4-flash",
|
|
9
|
+
pro: "deepseek-v4-pro",
|
|
10
|
+
};
|
|
11
|
+
/** Resolve --model flash|pro (or full deepseek-v4-* id) to a CLI alias. Default: flash. */
|
|
12
|
+
export function resolveDsModelAlias(raw) {
|
|
13
|
+
if (!raw || !raw.trim())
|
|
14
|
+
return "flash";
|
|
15
|
+
const v = raw.trim().toLowerCase();
|
|
16
|
+
if (v === "flash" || v === "deepseek-v4-flash")
|
|
17
|
+
return "flash";
|
|
18
|
+
if (v === "pro" || v === "deepseek-v4-pro" || v === "deepseek-v4-pro[1m]")
|
|
19
|
+
return "pro";
|
|
20
|
+
throw new Error(`Invalid --model ${raw}; expected flash|pro`);
|
|
21
|
+
}
|
|
22
|
+
/** OpenRouter model for the easy lane (env CURSOR_ROUTE_OPENROUTER_MODEL). */
|
|
23
|
+
export function openRouterModel() {
|
|
24
|
+
return process.env.CURSOR_ROUTE_OPENROUTER_MODEL || "openrouter/free";
|
|
25
|
+
}
|
|
26
|
+
/** OpenRouter API base URL (env OPENROUTER_BASE_URL). */
|
|
27
|
+
export function openRouterBaseUrl() {
|
|
28
|
+
return process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1";
|
|
29
|
+
}
|
|
6
30
|
function maxConcurrentJobsFromEnv() {
|
|
7
31
|
const raw = process.env.CURSOR_ROUTE_MAX_JOBS;
|
|
8
32
|
if (raw) {
|
|
@@ -19,7 +43,7 @@ function maxConcurrentJobsFromEnv() {
|
|
|
19
43
|
*/
|
|
20
44
|
export const config = {
|
|
21
45
|
product: "cursor-route",
|
|
22
|
-
version: "0.1.
|
|
46
|
+
version: "0.1.6",
|
|
23
47
|
get jobsDir() {
|
|
24
48
|
return defaultJobsDir();
|
|
25
49
|
},
|
|
@@ -27,9 +51,12 @@ export const config = {
|
|
|
27
51
|
defaultWorker: "grok",
|
|
28
52
|
/** Lane → default worker (Cemini /route public core). */
|
|
29
53
|
laneWorkers: {
|
|
54
|
+
easy: "openrouter",
|
|
30
55
|
mid: "claude-ds",
|
|
31
56
|
hard: "grok",
|
|
32
57
|
},
|
|
58
|
+
/** Default mid DeepSeek model (Flash = cheap execute). Override: --model pro */
|
|
59
|
+
defaultDsModel: "flash",
|
|
33
60
|
jobsListLimit: 20,
|
|
34
61
|
/** Max simultaneously active (running|pending) jobs. Override: CURSOR_ROUTE_MAX_JOBS. */
|
|
35
62
|
get maxConcurrentJobs() {
|
package/dist/jobs.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, unlink
|
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
|
-
import { config, sessionName } from "./config.js";
|
|
5
|
+
import { config, sessionName, } from "./config.js";
|
|
6
6
|
import { getAdapter } from "./adapters/index.js";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
@@ -217,12 +217,14 @@ export function startJob(opts) {
|
|
|
217
217
|
const id = newJobId();
|
|
218
218
|
const paths = jobPaths(id);
|
|
219
219
|
writeSecure(paths.prompt, opts.prompt);
|
|
220
|
+
const model = worker === "claude-ds" ? opts.model ?? config.defaultDsModel : undefined;
|
|
220
221
|
let plan;
|
|
221
222
|
try {
|
|
222
223
|
plan = adapter.buildLaunch({
|
|
223
224
|
promptFile: paths.prompt,
|
|
224
225
|
cwd,
|
|
225
226
|
alwaysApprove,
|
|
227
|
+
model,
|
|
226
228
|
});
|
|
227
229
|
}
|
|
228
230
|
catch (e) {
|
|
@@ -240,6 +242,7 @@ export function startJob(opts) {
|
|
|
240
242
|
status: "pending",
|
|
241
243
|
worker,
|
|
242
244
|
lane: opts.lane,
|
|
245
|
+
model,
|
|
243
246
|
prompt: opts.prompt,
|
|
244
247
|
cwd,
|
|
245
248
|
alwaysApprove: plan.alwaysApprove,
|