cursor-route 0.1.8 → 0.1.10

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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ _(none)_
6
+
7
+ ## 0.1.10 — 2026-08-21
8
+
9
+ - Opt-in `--worker opencode`: OpenCode `run` as a coding agent that defaults to Zen free model `opencode/big-pickle` (alias `--model free`). Not a lane default — mid stays `claude-ds`, easy stays OpenRouter chat.
10
+ - Always-approve → `opencode run --auto` (still honors explicit deny rules); `--ask` / `CURSOR_ROUTE_ASK=1` omits `--auto`. Never rewrites `~/.config/opencode/opencode.json`.
11
+ - `--model provider/model` for opencode (env `CURSOR_ROUTE_OPENCODE_MODEL`); `flash|pro` still DeepSeek-only.
12
+ - Health: `worker:opencode` ✓ when `opencode` is on PATH (override `CURSOR_ROUTE_OPENCODE_BIN`). Auth is `opencode auth login` at first start (same pattern as grok).
13
+ - Dry-run works without opencode installed (falls back to plain `opencode` in the printed command); real starts stay gated by health.
14
+ - Docs / `route-orch`: OpenCode is an opt-in usage-reduction worker, not a second mid harness. Free Zen models may log/train — non-secret prompts only.
15
+
16
+ ## 0.1.9 — 2026-08-18
17
+
18
+ - Health proves mid DeepSeek: dedicated `lane:mid` check is ✓ only when mid is a `claude-ds` / `deepseek-claude` shim or stock `claude` with DeepSeek `ANTHROPIC_BASE_URL`. `CURSOR_ROUTE_ALLOW_ANTHROPIC=1` is not proof. Overall `ok` still uses the existing OR-gate (any worker + `CURSOR_ROUTE_RELAXED=1`); grok-only installs stay OK with `lane:mid` ✗ plus a fix tip.
19
+ - Health JSON includes `lanes.mid.{ worker, deepseek, detail }` (`worker` is always `"claude-ds"`).
20
+ - `status --json` adds an evidence tree (`spawn` / `execute` / `verify`). `verify.claim` is always `"unverified"` — the parent must capture; status never auto-greens.
21
+ - Headless pid liveness: if `ps` is unreadable (`EPERM` / non-zero), treat the pid as **alive** (fail-safe), not dead — avoids false-completed kills on sandboxed `ps`.
22
+ - `route-orch` skill: health-before-mid, evidence-tree closeout, always-approve is coding-worktrees-only (not LIVE Discord/trading), do not fork the mid harness; `--worker deepseek` stays experimental / cheap-to-abandon.
23
+ - Docs: `route-orch` **Eval & skill hygiene** (folded from Unreleased) — external eval contract (AutoDesign): never rewrite Verify / Success criteria mid-run to make a failing job look green (capture + exit status are the contract); skill misevolution: no auto-edit / promote skill variants from worker trajectories without operator HITL (write-time approval ≠ safe retrieval later); verify fail → prefer reconsidering the plan/definition over grinding the same tactic, with stage attribution when possible (spawn vs execute vs verify). Mid stays `claude-ds`.
24
+
3
25
  ## 0.1.8 — 2026-08-14
4
26
 
5
27
  - Experimental `--worker deepseek`: official DeepSeek Harness (`dsh`, `@deepseek-ai/dsh`) runs headless — `dsh --profile headless` with a per-job Cordis patch pinning the model. Not a mid default: `laneWorkers.mid` stays `claude-ds`.
package/CONTRIBUTING.md CHANGED
@@ -12,6 +12,7 @@ CURSOR_ROUTE_RELAXED=1 ./bin/cursor-route health # without tmux
12
12
  ./bin/cursor-route start --dry-run --lane mid --model flash "ping"
13
13
  ./bin/cursor-route start --dry-run --lane mid --model pro "ping"
14
14
  ./bin/cursor-route start --dry-run --worker deepseek --model flash "ping" # needs dsh + DEEPSEEK_API_KEY for real starts
15
+ ./bin/cursor-route start --dry-run --worker opencode --model free "ping" # needs opencode for real starts
15
16
  ```
16
17
 
17
18
  Keep `skills/route-orch/SKILL.md` and `.cursor/skills/route-orch/SKILL.md` identical when editing the skill.
@@ -23,6 +24,7 @@ Keep `skills/route-orch/SKILL.md` and `.cursor/skills/route-orch/SKILL.md` ident
23
24
  - Prefer adapters over rewriting the job core
24
25
  - Always-approve defaults stay documented + opt-out (`--ask` / `CURSOR_ROUTE_ASK=1`)
25
26
  - Mid default is Flash; document Pro only as harder mid / Grok **usage** stand-in (not missing `grok login`)
27
+ - OpenCode is opt-in (`--worker opencode`); do not make it a lane default — mid stays claude-ds
26
28
 
27
29
  ## Release checklist
28
30
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # cursor-route
2
2
 
3
- **Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter (easy) are the parallel army.**
3
+ **Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter (easy) + OpenCode (opt-in free) 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 + OpenRouter (easy)** | Cost-aware lanes you already pay for |
20
+ | **cursor-route** | **Cursor Agent** | **Grok CLI + claude-ds + OpenRouter (easy) + OpenCode (opt-in)** | 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
 
@@ -29,6 +29,7 @@ If you already live in Cursor, X Premium (Grok CLI), and DeepSeek — stop payin
29
29
  | Grok CLI | X Premium | `--lane hard` implement |
30
30
  | DeepSeek via claude-ds | DeepSeek API / plan | `--lane mid` implement (**Flash** default; `--model pro` when needed) |
31
31
  | OpenRouter free models | OpenRouter API (free tier) | `--lane easy` wording/drafts — non-secret prompts only (see Security) |
32
+ | OpenCode (opt-in) | OpenCode Zen free models | `--worker opencode` implement on free Zen (default `opencode/big-pickle`) |
32
33
  | Codex / extra Claude | Optional | Not required for v0 |
33
34
 
34
35
  Exact dollars vary — the point is **reuse subscriptions you already have**.
@@ -43,6 +44,7 @@ Exact dollars vary — the point is **reuse subscriptions you already have**.
43
44
  | [Bun](https://bun.sh) *(or Node 20+)* | Runs the CLI |
44
45
  | [Grok CLI](https://x.ai/cli) and/or Claude Code + DeepSeek (`claude-ds`) | Workers |
45
46
  | OpenRouter API key (`OPENROUTER_API_KEY`) | Easy lane (`--lane easy` / `--worker openrouter`) |
47
+ | [OpenCode](https://opencode.ai) (`opencode` on PATH) | Opt-in `--worker opencode` (free Zen models) |
46
48
  | `script(1)` | Job logs (macOS/Linux) |
47
49
 
48
50
  ```bash
@@ -57,6 +59,7 @@ npm i -g cursor-route
57
59
  grok login # if using Grok
58
60
  # configure claude-ds — see DeepSeek setup below
59
61
  export OPENROUTER_API_KEY=... # if using the easy lane (see OpenRouter setup below)
62
+ # optional: npm i -g opencode-ai && opencode auth login # --worker opencode
60
63
 
61
64
  cursor-route health
62
65
  # without tmux / workers (CI / headless infra smoke):
@@ -91,10 +94,10 @@ Working notes for this repo (edit in place): [docs/briefs/WORKING.md](./docs/bri
91
94
 
92
95
  | Command | Description |
93
96
  |---------|-------------|
94
- | `health` | Gate #1 — tmux, runtime, workers |
97
+ | `health` | Gate #1 — tmux, runtime, workers; proves `lane:mid` is DeepSeek (`lanes.mid` in `--json`) |
95
98
  | `start <prompt>` | Spawn worker job (`--worker` / `--lane` / `--model` / `--dir` / `--ask` / `--dry-run` / `--no-tmux`) |
96
99
  | `jobs [--json]` | List jobs |
97
- | `status <id>` | Job + sessionAlive |
100
+ | `status <id>` | Job + sessionAlive; `--json` includes evidence tree (`spawn` / `execute` / `verify`) |
98
101
  | `capture <id> [n]` | Last n pane/log lines |
99
102
  | `send <id> <msg>` | Mid-task redirect (tmux send-keys) |
100
103
  | `attach <id>` | Print `tmux attach` hint |
@@ -113,10 +116,10 @@ docs/fixtures/generate-hero-demo.sh # regenerate docs/fixtures/hero-demo.lo
113
116
 
114
117
  ```text
115
118
  $ cursor-route --version
116
- 0.1.8
119
+ 0.1.9
117
120
 
118
121
  $ CURSOR_ROUTE_RELAXED=1 cursor-route health
119
- cursor-route v0.1.8
122
+ cursor-route v0.1.9
120
123
  health: OK
121
124
 
122
125
  $ cursor-route start --lane mid --model flash --dry-run "Add a unit test for shellQuote"
@@ -140,7 +143,7 @@ A real hero GIF is still pending — recording steps live in [docs/DEMO_GIF.md](
140
143
  | `mid` | `claude-ds` | Default implement on DeepSeek (**Flash** by default) |
141
144
  | `hard` | `grok` | Hard implement on Grok CLI |
142
145
 
143
- Always-approve is **on** by default. Opt out: `--ask` or `CURSOR_ROUTE_ASK=1`.
146
+ Always-approve is **on** by default for **coding worktrees only**. It does not authorize LIVE Discord, trading, or irreversible SaaS. Opt out: `--ask` or `CURSOR_ROUTE_ASK=1`.
144
147
 
145
148
  ### Mid models (Flash vs Pro)
146
149
 
@@ -163,7 +166,7 @@ export CURSOR_ROUTE_DS_MODEL=pro # also honors ANTHROPIC_MODEL; --model overri
163
166
 
164
167
  ## DeepSeek setup (the cheap mid-lane — this is the point)
165
168
 
166
- `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.
169
+ `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. `cursor-route health` shows `lane:mid` ✓ only when DeepSeek is proven (`claude-ds` / `deepseek-claude` shim, or stock `claude` with DeepSeek `ANTHROPIC_BASE_URL`) — `CURSOR_ROUTE_ALLOW_ANTHROPIC=1` is not proof.
167
170
 
168
171
  **Recommended (official DeepSeek → Claude Code):**
169
172
 
@@ -176,7 +179,7 @@ export ANTHROPIC_AUTH_TOKEN=YOUR_DEEPSEEK_API_KEY # from platform.deepseek.com
176
179
  export ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash
177
180
  export CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash
178
181
 
179
- cursor-route health # worker:claude-ds should be ✓
182
+ cursor-route health # worker:claude-ds should be ✓; `lane:mid` is only when DeepSeek is proven
180
183
  cursor-route start --lane mid "…" # Flash (default)
181
184
  cursor-route start --lane mid --model pro "…" # Pro when you need it
182
185
  ```
@@ -221,6 +224,36 @@ cursor-route start --lane easy "Rewrite this FAQ answer in 3 sentences"
221
224
  **wording/drafts without credentials**. The same refuse gate as every lane blocks
222
225
  key-shaped material in `start` / `send`, and the runner re-checks the prompt file.
223
226
 
227
+ ## OpenCode setup (opt-in free coding worker)
228
+
229
+ `--worker opencode` runs [OpenCode](https://opencode.ai) as a **coding agent** on
230
+ OpenCode Zen free models (default `opencode/big-pickle`). This is **not** a lane
231
+ default — mid stays `claude-ds`; easy stays OpenRouter chat (no tools). Use it
232
+ to burn fewer Grok / DeepSeek tokens on implement work.
233
+
234
+ ```bash
235
+ npm i -g opencode-ai # or: brew install opencode
236
+ opencode auth login # connect OpenCode Zen (or another provider)
237
+ # optional:
238
+ export CURSOR_ROUTE_OPENCODE_MODEL=opencode/big-pickle # default; alias --model free
239
+
240
+ cursor-route health # worker:opencode should be ✓
241
+ cursor-route start --worker opencode "…" # Big Pickle (free)
242
+ cursor-route start --worker opencode --model free "…"
243
+ cursor-route start --worker opencode --model opencode/hy3-free "…"
244
+ ```
245
+
246
+ The adapter launches `opencode run --dir <cwd> --model <id>` with the prompt via
247
+ `cat` (never interpolated). Always-approve maps to `--auto` (explicit `"deny"`
248
+ rules still apply); `--ask` omits `--auto`. It never rewrites
249
+ `~/.config/opencode/opencode.json`, so parallel jobs don't race. Override the
250
+ binary with `CURSOR_ROUTE_OPENCODE_BIN`.
251
+
252
+ **Non-secret prompts:** several Zen free models may log or train on prompts during
253
+ their free period (see [OpenCode Zen](https://opencode.ai/docs/zen/) privacy notes).
254
+ The same refuse gate as every lane still applies. `opencode/x-preview-f-free`
255
+ (Ox Alpha) is the zero-retention free option if you need it.
256
+
224
257
  ## Jobs directory
225
258
 
226
259
  Jobs default to `~/.local/share/cursor-route/jobs` (override with `CURSOR_ROUTE_JOBS_DIR`).
@@ -235,7 +268,7 @@ This is **not** inside a git clone of this repo.
235
268
  ## FAQ
236
269
 
237
270
  **What is cursor-route?**
238
- 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.
271
+ 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, OpenRouter free models handle the easy lane for wording/drafts, and `--worker opencode` is an opt-in coding agent on OpenCode Zen free models.
239
272
 
240
273
  **How is this different from Codex orchestrator?**
241
274
  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.
@@ -247,7 +280,7 @@ No. The mid worker is DeepSeek. Claude Code is the harness, configured with `ANT
247
280
  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.
248
281
 
249
282
  **Is it free?**
250
- 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`).
283
+ 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`). `--worker opencode` can run OpenCode Zen free models (default `opencode/big-pickle`).
251
284
 
252
285
  ## Related
253
286
 
@@ -276,9 +309,11 @@ If you’d like to tip, use the **donation-only** addresses below (not trading o
276
309
 
277
310
  | Chain family | Address |
278
311
  |--------------|---------|
312
+ | **X Money** (fiat, US) | Request [@Cemini23](https://x.com/Cemini23) in the X app — scan the Request QR |
279
313
  | **EVM** (Ethereum, Polygon, Base, Arbitrum, …) | `0x444C5C2eC439E0382aa5a17F70313c536BcC5D58` |
280
314
  | **Solana / SVM** | `J4zNn4hK9jTrKBFY8sbAGJHLoZvXvQf4B9pQSbSrocZE` |
281
315
  | **Polymarket** (referral) | [polymarket.com/?r=Cemini23](https://polymarket.com/?r=Cemini23) |
316
+ | **Hyperliquid** (referral) | [app.hyperliquid.xyz/join/CEMINI23](https://app.hyperliquid.xyz/join/CEMINI23) |
282
317
 
283
318
  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).
284
319
 
@@ -292,6 +327,7 @@ MIT © Cemini — see [LICENSE](LICENSE).
292
327
 
293
328
  - Homebrew tap
294
329
  - Stabilize the DeepSeek harness adapter (experimental `--worker deepseek` since 0.1.8)
330
+ - Stabilize the OpenCode adapter (`--worker opencode` since 0.1.10)
295
331
  - Codebase map injection (`--map`)
296
332
  - Cursor CLI `agent` as alternate supervisor
297
333
  - Web UI / CAO-style MCP supervisor
package/SECURITY.md CHANGED
@@ -15,14 +15,15 @@ Do **not** open a public issue for credential leaks or RCE-class bugs.
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
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.
18
+ 3. **OpenCode Zen free models may log or train during their free period.** `--worker opencode` defaults to `opencode/big-pickle`. Treat it like the easy lane for secrets. Prefer `opencode/x-preview-f-free` (Ox Alpha) when you need the zero-retention free option.
19
+ 4. **Always-approve is powerful.** Default headless workers skip interactive permission prompts (`--always-approve` / `--dangerously-skip-permissions` / OpenCode `--auto`). Opt out with `--ask` / `CURSOR_ROUTE_ASK=1` when you need HITL.
20
+ 5. **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.
21
+ 6. **Workers inherit your auth.** Grok CLI, Claude Code / claude-ds, and OpenCode use *your* local login. Treat worker panes like you would treat your own shell.
22
+ 7. **No LIVE Discord / trading egress demos** from this tool. Orchestration is local.
22
23
 
23
24
  ## Supply chain
24
25
 
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
+ - Declared runtime npm dependencies: **none**. System needs: Node ≥20 or Bun, `tmux`, worker CLIs (Grok / claude-ds / optional OpenCode), and an OpenRouter API key for the easy lane.
26
27
  - 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
28
  - Running from a git clone without a build uses Bun to execute `src/` directly (Bun runs TypeScript natively) — also offline-friendly.
28
29
  - Review `package.json` before upgrading.
package/SUPPORT.md CHANGED
@@ -6,9 +6,11 @@ Voluntary tips fund wikis, OSS tooling, and public research. **Donation-only add
6
6
 
7
7
  | Chain family | Address |
8
8
  |--------------|---------|
9
+ | **X Money** (fiat, US) | Request [@Cemini23](https://x.com/Cemini23) in the X app — scan the Request QR |
9
10
  | **EVM** (Ethereum, Polygon, Base, Arbitrum, …) | `0x444C5C2eC439E0382aa5a17F70313c536BcC5D58` |
10
11
  | **Solana / SVM** | `J4zNn4hK9jTrKBFY8sbAGJHLoZvXvQf4B9pQSbSrocZE` |
11
12
  | **Polymarket** (referral) | [polymarket.com/?r=Cemini23](https://polymarket.com/?r=Cemini23) |
13
+ | **Hyperliquid** (referral) | [app.hyperliquid.xyz/join/CEMINI23](https://app.hyperliquid.xyz/join/CEMINI23) |
12
14
 
13
15
  If you’d rather follow along or try something we ship:
14
16
 
@@ -108,6 +108,24 @@ function resolveClaudeDs() {
108
108
  }
109
109
  return null;
110
110
  }
111
+ /**
112
+ * Mid lane is proven DeepSeek only via shim (`claude-ds` / `deepseek-claude`)
113
+ * or stock `claude` routed to DeepSeek. The Anthropic escape hatch is not proof.
114
+ */
115
+ export function isMidDeepSeekProven() {
116
+ const resolved = resolveClaudeDs();
117
+ if (!resolved)
118
+ return false;
119
+ return !resolved.mode.startsWith("claude → Anthropic");
120
+ }
121
+ /** Human detail for health `lane:mid` (mode when proven; install hint otherwise). */
122
+ export function midDeepSeekProofDetail() {
123
+ const resolved = resolveClaudeDs();
124
+ if (!resolved || resolved.mode.startsWith("claude → Anthropic")) {
125
+ return "mid not proven DeepSeek — install claude-ds / set ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic. CURSOR_ROUTE_ALLOW_ANTHROPIC=1 is not proof.";
126
+ }
127
+ return `DeepSeek proven (${resolved.mode})`;
128
+ }
111
129
  function pickModel(requested, modelId) {
112
130
  if (modelId) {
113
131
  const alias = requested ?? resolveDsModel(modelId).alias;
@@ -2,11 +2,13 @@ import { grokAdapter } from "./grok.js";
2
2
  import { claudeDsAdapter } from "./claude-ds.js";
3
3
  import { openRouterAdapter } from "./openrouter.js";
4
4
  import { deepseekAdapter } from "./deepseek.js";
5
+ import { opencodeAdapter } from "./opencode.js";
5
6
  const registry = {
6
7
  grok: grokAdapter,
7
8
  "claude-ds": claudeDsAdapter,
8
9
  openrouter: openRouterAdapter,
9
10
  deepseek: deepseekAdapter,
11
+ opencode: opencodeAdapter,
10
12
  };
11
13
  export function getAdapter(worker) {
12
14
  const a = registry[worker];
@@ -0,0 +1,83 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { shellQuote } from "../util.js";
4
+ import { openCodeModel, OPENCODE_DEFAULT_MODEL } from "../config.js";
5
+ /**
6
+ * Opt-in OpenCode worker (`opencode run`) — a coding agent that can use
7
+ * OpenCode Zen free models (default `opencode/big-pickle`) to cut Grok /
8
+ * DeepSeek usage. Not a lane default: mid stays claude-ds; easy stays
9
+ * OpenRouter chat (no tools).
10
+ *
11
+ * We never rewrite ~/.config/opencode/opencode.json (parallel jobs would
12
+ * race). Always-approve maps to `opencode run --auto` (still honors explicit
13
+ * deny rules). Prompt is inlined via cat — never interpolated.
14
+ */
15
+ function findOpencode() {
16
+ // Env override lets tests pin a fake opencode — but it must exist, so a
17
+ // stale override cannot pass health with a dangling path.
18
+ const override = process.env.CURSOR_ROUTE_OPENCODE_BIN;
19
+ if (override)
20
+ return existsSync(override) ? override : null;
21
+ try {
22
+ return (execSync("command -v opencode", {
23
+ encoding: "utf8",
24
+ stdio: ["ignore", "pipe", "ignore"],
25
+ env: { ...process.env },
26
+ }).trim() || null);
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ export const opencodeAdapter = {
33
+ kind: "opencode",
34
+ label: "OpenCode (free Zen / other models)",
35
+ health() {
36
+ const binary = findOpencode();
37
+ if (!binary) {
38
+ return {
39
+ worker: "opencode",
40
+ ok: false,
41
+ binary: null,
42
+ detail: "opencode not found — install: npm i -g opencode-ai (or brew install opencode). Then: opencode auth login. Mid default remains claude-ds.",
43
+ };
44
+ }
45
+ let defaultModel = OPENCODE_DEFAULT_MODEL;
46
+ try {
47
+ defaultModel = openCodeModel();
48
+ }
49
+ catch {
50
+ /* invalid env — health still ok; startJob will fail loud */
51
+ }
52
+ return {
53
+ worker: "opencode",
54
+ ok: true,
55
+ binary,
56
+ detail: `ok (opencode run; default model ${defaultModel}; auth at first start — opencode auth login if jobs fail; mid default remains claude-ds)`,
57
+ };
58
+ },
59
+ buildLaunch({ promptFile, cwd, alwaysApprove, modelId }) {
60
+ // Missing binary is tolerated here so `--dry-run` can still print the
61
+ // command; real starts are gated by the health preflight.
62
+ const binary = findOpencode() || "opencode";
63
+ const id = openCodeModel(modelId);
64
+ const ask = process.env.CURSOR_ROUTE_ASK === "1";
65
+ const skip = alwaysApprove && !ask;
66
+ const parts = [
67
+ shellQuote(binary),
68
+ "run",
69
+ "--dir",
70
+ shellQuote(cwd),
71
+ "--model",
72
+ shellQuote(id),
73
+ ];
74
+ if (skip)
75
+ parts.push("--auto");
76
+ parts.push(`"$(cat ${shellQuote(promptFile)})"`);
77
+ return {
78
+ worker: "opencode",
79
+ command: `cd ${shellQuote(cwd)} && ${parts.join(" ")}`,
80
+ alwaysApprove: skip,
81
+ };
82
+ },
83
+ };
package/dist/cli.js CHANGED
@@ -1,26 +1,26 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * cursor-route CLI — Cursor brain, Grok + DeepSeek + OpenRouter (easy) workers in tmux.
3
+ * cursor-route CLI — Cursor brain, Grok + DeepSeek + OpenRouter (easy) + OpenCode (opt-in) 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, resolveDsModel, } from "./config.js";
7
+ import { config, WORKERS, LANES, resolveDsModel, openCodeModel, } from "./config.js";
8
8
  import { runHealth, printHealth } from "./health.js";
9
- import { startJob, listJobs, readJob, killJob, cleanJobs, jobPaths, refreshStatus, } from "./jobs.js";
9
+ import { startJob, listJobs, readJob, killJob, cleanJobs, jobPaths, refreshStatus, jobEvidence, } from "./jobs.js";
10
10
  import { capturePane, sendKeys, attachHint, listManagedSessions, sessionExists, } from "./tmux.js";
11
11
  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) + OpenRouter (easy) are the parallel army.
15
+ Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter (easy) + OpenCode (opt-in free) are the parallel army.
16
16
 
17
17
  Usage:
18
18
  cursor-route --version
19
- cursor-route health [--json]
19
+ cursor-route health [--json] # JSON includes lanes.mid (DeepSeek proof)
20
20
  cursor-route start <prompt> [options]
21
21
  cursor-route start --prompt-file <path> [options]
22
22
  cursor-route jobs [--json] [--limit N]
23
- cursor-route status <jobId> [--json]
23
+ cursor-route status <jobId> [--json] # JSON includes evidence spawn/execute/verify
24
24
  cursor-route capture <jobId> [lines]
25
25
  cursor-route send <jobId> <message>
26
26
  cursor-route attach <jobId>
@@ -29,9 +29,9 @@ Usage:
29
29
  cursor-route clean [--days N]
30
30
 
31
31
  Start options:
32
- --worker <grok|claude-ds|openrouter|deepseek> Worker adapter (default: grok; deepseek = experimental official dsh)
32
+ --worker <grok|claude-ds|openrouter|deepseek|opencode> Worker adapter (default: grok; deepseek/opencode = opt-in)
33
33
  --lane <easy|mid|hard> Lane → worker (easy=openrouter, mid=claude-ds, hard=grok)
34
- --model <flash|pro> Mid DeepSeek only (default: flash / CURSOR_ROUTE_DS_MODEL). Parsed for claude-ds + deepseek
34
+ --model <flash|pro|free|provider/model> claude-ds/deepseek: flash|pro. opencode: free (default opencode/big-pickle) or provider/model
35
35
  --dir <path> Working directory (default: cwd)
36
36
  --ask Disable always-approve for this job
37
37
  --dry-run Print launch command; do not start
@@ -48,10 +48,12 @@ Env:
48
48
  CURSOR_ROUTE_GROK_BIN Override the grok binary path (tests / power users)
49
49
  CURSOR_ROUTE_CLAUDE_DS_BIN Override the claude-ds binary path (tests / power users)
50
50
  CURSOR_ROUTE_DSH_BIN Override the dsh binary path (tests / power users)
51
+ CURSOR_ROUTE_OPENCODE_BIN Override the opencode binary path (tests / power users)
51
52
  DEEPSEEK_API_KEY DeepSeek API key (required for --worker deepseek)
52
53
  OPENROUTER_API_KEY OpenRouter key (required for --worker openrouter / --lane easy)
53
54
  CURSOR_ROUTE_OPENROUTER_MODEL OpenRouter model (default: openrouter/free)
54
55
  OPENROUTER_BASE_URL OpenRouter API base (default: https://openrouter.ai/api/v1)
56
+ CURSOR_ROUTE_OPENCODE_MODEL OpenCode model (default: opencode/big-pickle); --model overrides
55
57
  `);
56
58
  process.exit(exitCode);
57
59
  }
@@ -150,6 +152,14 @@ function asDsModelChoice(v) {
150
152
  throw new Error(`Invalid --model; expected flash|pro`);
151
153
  return resolveDsModel(v);
152
154
  }
155
+ function asOpenCodeModel(v) {
156
+ if (v === undefined || v === true)
157
+ return undefined;
158
+ if (typeof v !== "string") {
159
+ throw new Error(`Invalid --model; expected provider/model (e.g. opencode/big-pickle) or free`);
160
+ }
161
+ return openCodeModel(v);
162
+ }
153
163
  function refuseSecrets(text, context) {
154
164
  if (looksLikeSecretMaterial(text)) {
155
165
  console.error(`Refusing ${context}: looks like secret key material. Remove tokens/keys and retry.`);
@@ -223,8 +233,8 @@ async function main() {
223
233
  const resolvedWorker = worker ?? (lane ? config.laneWorkers[lane] : config.defaultWorker);
224
234
  let model;
225
235
  let modelId;
226
- // --model is DeepSeek-only (claude-ds mid lane + experimental deepseek worker);
227
- // ignore (do not validate) for other workers
236
+ // --model: DeepSeek flash|pro for claude-ds/deepseek; provider/model (or free) for opencode;
237
+ // ignore (do not validate) for grok/openrouter
228
238
  if (f.model !== undefined && (resolvedWorker === "claude-ds" || resolvedWorker === "deepseek")) {
229
239
  try {
230
240
  const choice = asDsModelChoice(f.model);
@@ -238,6 +248,15 @@ async function main() {
238
248
  process.exit(2);
239
249
  }
240
250
  }
251
+ else if (f.model !== undefined && resolvedWorker === "opencode") {
252
+ try {
253
+ modelId = asOpenCodeModel(f.model);
254
+ }
255
+ catch (e) {
256
+ console.error(e.message);
257
+ process.exit(2);
258
+ }
259
+ }
241
260
  let prompt = "";
242
261
  if (promptFile) {
243
262
  const p = resolve(promptFile);
@@ -327,7 +346,7 @@ async function main() {
327
346
  else {
328
347
  for (const j of jobs) {
329
348
  const age = j.startedAt || j.createdAt;
330
- const modelCol = j.model ? j.model.padEnd(6) : "".padEnd(6);
349
+ const modelCol = j.model ? j.model.padEnd(22) : "".padEnd(22);
331
350
  console.log(`${j.id} ${j.status.padEnd(10)} ${j.worker.padEnd(10)} ${modelCol} ${age} ${j.prompt.slice(0, 48).replace(/\n/g, " ")}`);
332
351
  }
333
352
  }
@@ -356,11 +375,13 @@ async function main() {
356
375
  }
357
376
  })())
358
377
  : sessionExists(job.tmuxSession);
359
- const view = { ...job, sessionAlive: alive };
378
+ const evidence = jobEvidence(job, alive);
379
+ const view = { ...job, sessionAlive: alive, evidence };
360
380
  if (json)
361
381
  console.log(JSON.stringify(view, null, 2));
362
382
  else {
363
383
  console.log(`${job.id} ${job.status} worker=${job.worker}${job.model ? ` model=${job.model}` : ""} sessionAlive=${alive}`);
384
+ console.log("evidence: spawn/execute/verify (claim=unverified)");
364
385
  if (job.error)
365
386
  console.log(`error: ${job.error}`);
366
387
  }
package/dist/config.js CHANGED
@@ -1,7 +1,7 @@
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", "openrouter", "deepseek"];
4
+ export const WORKERS = ["grok", "claude-ds", "openrouter", "deepseek", "opencode"];
5
5
  export const LANES = ["easy", "mid", "hard"];
6
6
  export const DS_MODELS = ["flash", "pro"];
7
7
  export const DS_MODEL_IDS = {
@@ -49,6 +49,33 @@ export function openRouterModel() {
49
49
  export function openRouterBaseUrl() {
50
50
  return process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1";
51
51
  }
52
+ /**
53
+ * Default OpenCode model: OpenCode Zen Big Pickle (free, limited-time).
54
+ * Override: CURSOR_ROUTE_OPENCODE_MODEL or `--model provider/model`.
55
+ * Alias `free` resolves to this default (or the env override).
56
+ */
57
+ export const OPENCODE_DEFAULT_MODEL = "opencode/big-pickle";
58
+ /** Whitelist OpenCode `provider/model` ids (no spaces / injection). */
59
+ export function assertOpenCodeModel(id) {
60
+ if (!/^[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._\-:]+)+$/i.test(id)) {
61
+ throw new Error(`Invalid OpenCode model ${id}; expected provider/model (e.g. opencode/big-pickle) or free`);
62
+ }
63
+ return id;
64
+ }
65
+ /**
66
+ * Resolve OpenCode `--model` / env to a concrete `provider/model` id.
67
+ * Empty or `free` → CURSOR_ROUTE_OPENCODE_MODEL, else OPENCODE_DEFAULT_MODEL.
68
+ */
69
+ export function openCodeModel(raw) {
70
+ const v = (raw ?? "").trim();
71
+ if (!v || v.toLowerCase() === "free") {
72
+ const env = (process.env.CURSOR_ROUTE_OPENCODE_MODEL ?? "").trim();
73
+ if (!env || env.toLowerCase() === "free")
74
+ return OPENCODE_DEFAULT_MODEL;
75
+ return assertOpenCodeModel(env);
76
+ }
77
+ return assertOpenCodeModel(v);
78
+ }
52
79
  function maxConcurrentJobsFromEnv() {
53
80
  const raw = process.env.CURSOR_ROUTE_MAX_JOBS;
54
81
  if (raw) {
@@ -65,7 +92,7 @@ function maxConcurrentJobsFromEnv() {
65
92
  */
66
93
  export const config = {
67
94
  product: "cursor-route",
68
- version: "0.1.8",
95
+ version: "0.1.10",
69
96
  get jobsDir() {
70
97
  return defaultJobsDir();
71
98
  },
@@ -73,7 +100,7 @@ export const config = {
73
100
  defaultWorker: "grok",
74
101
  /**
75
102
  * Lane → default worker (Cemini /route public core).
76
- * `deepseek` is experimental only — mid stays on claude-ds.
103
+ * `deepseek` and `opencode` are opt-in only — mid stays on claude-ds.
77
104
  */
78
105
  laneWorkers: {
79
106
  easy: "openrouter",
package/dist/health.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { allAdapters } from "./adapters/index.js";
3
+ import { isMidDeepSeekProven, midDeepSeekProofDetail } from "./adapters/claude-ds.js";
3
4
  import { config } from "./config.js";
4
5
  import { isTmuxAvailable } from "./tmux.js";
5
6
  import { commandExists } from "./util.js";
@@ -42,6 +43,14 @@ export function runHealth() {
42
43
  detail: h.binary ? `${h.detail} @ ${h.binary}` : h.detail,
43
44
  });
44
45
  }
46
+ // Informational: mid is proven DeepSeek (not the overall OR-gate).
47
+ const midOk = isMidDeepSeekProven();
48
+ const midDetail = midDeepSeekProofDetail();
49
+ checks.push({
50
+ name: "lane:mid",
51
+ ok: midOk,
52
+ detail: midDetail,
53
+ });
45
54
  // Optional supervisor probe (v0 skill-only; Cursor CLI agent is informational)
46
55
  const agentBin = (commandExists("agent") && "agent") ||
47
56
  (commandExists("cursor-agent") && "cursor-agent") ||
@@ -78,6 +87,9 @@ export function runHealth() {
78
87
  product: config.product,
79
88
  version: config.version,
80
89
  checks,
90
+ lanes: {
91
+ mid: { worker: "claude-ds", deepseek: midOk, detail: midDetail },
92
+ },
81
93
  };
82
94
  }
83
95
  export function printHealth(report, asJson) {
@@ -95,6 +107,10 @@ export function printHealth(report, asJson) {
95
107
  if (!report.ok) {
96
108
  console.log("");
97
109
  console.log("Fix the ✗ items, then re-run: cursor-route health");
98
- console.log("Tip: start with one worker (grok OR claude-ds) before parallel demos.");
110
+ console.log("Tip: start with one worker (grok OR claude-ds OR opencode) before parallel demos.");
111
+ }
112
+ else if (report.checks.some((c) => c.name === "lane:mid" && !c.ok)) {
113
+ console.log("");
114
+ console.log("Tip: health OK without a DeepSeek mid — --lane mid will fail until lane:mid is ✓.");
99
115
  }
100
116
  }