cursor-route 0.1.10 → 0.1.13

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.
@@ -19,7 +19,7 @@ function parseArgs(argv) {
19
19
  const a = argv[i];
20
20
  if (a === "-h" || a === "--help") {
21
21
  console.log("usage: cursor-route/openrouter-run --prompt-file <path>\n" +
22
- "env: OPENROUTER_API_KEY (required), CURSOR_ROUTE_OPENROUTER_MODEL, OPENROUTER_BASE_URL");
22
+ "env: OPENROUTER_API_KEY (required), CURSOR_ROUTE_OPENROUTER_MODEL (unset/free = live pick), OPENROUTER_BASE_URL");
23
23
  process.exit(0);
24
24
  }
25
25
  if (a === "--prompt-file") {
@@ -0,0 +1,216 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ /**
6
+ * Live OpenRouter free-model pick — same shape as zen-free.ts.
7
+ * Probe GET {OPENROUTER_BASE_URL}/models, keep $0 text models, rank, cache
8
+ * ~15 min. Do not hardcode a third-party model id as the happy-path default.
9
+ * `openrouter/free` is the OpenRouter **router** fallback only.
10
+ */
11
+ export const OPENROUTER_MODELS_URL_DEFAULT = "https://openrouter.ai/api/v1/models";
12
+ /** Fetch-fail / empty-rank fallback — OpenRouter live router, not a locked model. */
13
+ export const OPENROUTER_FALLBACK_MODEL = "openrouter/free";
14
+ const EXCLUDE_RE = /lyria|whisper|tts|embed|embedding|image|vision-only|audio|diffusion|flux|stable-diffusion|moderation/i;
15
+ const BOOST_RE = /coder|instruct|chat|nemotron|qwen|llama|gemma|gpt-oss|kimi|glm|deepseek/i;
16
+ function orModelsUrl() {
17
+ const base = (process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1")
18
+ .trim()
19
+ .replace(/\/+$/, "");
20
+ return `${base}/models`;
21
+ }
22
+ function cacheMinutes() {
23
+ const n = Number(process.env.CURSOR_ROUTE_OR_CACHE_MINUTES);
24
+ return Number.isFinite(n) && n > 0 ? n : 15;
25
+ }
26
+ function cachePath() {
27
+ return process.env.CURSOR_ROUTE_OR_CACHE_PATH?.trim() ||
28
+ join(tmpdir(), "cursor-route-or-best-free.json");
29
+ }
30
+ export function assertOpenRouterModel(id) {
31
+ if (!/^[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._\-:]+)+$/i.test(id)) {
32
+ throw new Error(`Invalid OpenRouter model ${id}; expected provider/model (e.g. qwen/qwen3-coder:free) or free`);
33
+ }
34
+ return id;
35
+ }
36
+ function num(v) {
37
+ if (typeof v === "number" && Number.isFinite(v))
38
+ return v;
39
+ if (typeof v === "string" && v.trim()) {
40
+ const n = Number(v);
41
+ if (Number.isFinite(n))
42
+ return n;
43
+ }
44
+ return NaN;
45
+ }
46
+ export function isOrFreeModel(m) {
47
+ const id = (m.id || "").trim();
48
+ if (!id)
49
+ return false;
50
+ if (EXCLUDE_RE.test(id) || EXCLUDE_RE.test(m.name || ""))
51
+ return false;
52
+ const modality = m.architecture?.modality;
53
+ if (typeof modality === "string" && modality.trim() && !/text/i.test(modality)) {
54
+ return false;
55
+ }
56
+ const prompt = num(m.pricing?.prompt);
57
+ const completion = num(m.pricing?.completion);
58
+ const pricedFree = prompt === 0 && completion === 0;
59
+ const tagged = id.endsWith(":free");
60
+ return tagged || pricedFree;
61
+ }
62
+ /** Higher boost wins. Coding/chat families beat generic free. No hardcoded id. */
63
+ export function orFreeBoost(id) {
64
+ return BOOST_RE.test(id) ? 20 : 10;
65
+ }
66
+ export function rankOrFreeModels(models) {
67
+ const out = [];
68
+ for (const m of models) {
69
+ if (!isOrFreeModel(m))
70
+ continue;
71
+ const raw = (m.id || "").trim();
72
+ let id;
73
+ try {
74
+ id = assertOpenRouterModel(raw);
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ const ctx = Number.isFinite(Number(m.context_length)) ? Number(m.context_length) : 0;
80
+ out.push({
81
+ id,
82
+ name: (m.name || raw).trim(),
83
+ context_length: ctx,
84
+ boost: orFreeBoost(id),
85
+ });
86
+ }
87
+ out.sort((a, b) => {
88
+ if (b.boost !== a.boost)
89
+ return b.boost - a.boost;
90
+ if (b.context_length !== a.context_length)
91
+ return b.context_length - a.context_length;
92
+ return a.id.localeCompare(b.id);
93
+ });
94
+ return out;
95
+ }
96
+ function readCache() {
97
+ const p = cachePath();
98
+ if (!existsSync(p))
99
+ return null;
100
+ try {
101
+ const j = JSON.parse(readFileSync(p, "utf8"));
102
+ if (!j?.id)
103
+ return null;
104
+ const ageMs = Date.now() - Date.parse(j.picked_at);
105
+ if (!Number.isFinite(ageMs) || ageMs < 0)
106
+ return null;
107
+ if (ageMs > cacheMinutes() * 60_000)
108
+ return null;
109
+ assertOpenRouterModel(j.id);
110
+ return j;
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ function writeCache(pick) {
117
+ try {
118
+ writeFileSync(cachePath(), JSON.stringify(pick), { mode: 0o600 });
119
+ }
120
+ catch {
121
+ /* cache is optional */
122
+ }
123
+ }
124
+ function httpGetSync(url, timeoutMs = 4000) {
125
+ const sec = Math.max(1, Math.ceil(timeoutMs / 1000));
126
+ // Public GET /models — no Authorization header (key would land in `ps` argv).
127
+ const curl = spawnSync("curl", ["-fsS", "--max-time", String(sec), "-H", "Accept: application/json", url], {
128
+ encoding: "utf8",
129
+ timeout: timeoutMs + 500,
130
+ env: { ...process.env },
131
+ });
132
+ if (curl.status === 0 && curl.stdout?.trim())
133
+ return curl.stdout;
134
+ return null;
135
+ }
136
+ function parseCatalog(raw) {
137
+ const j = JSON.parse(raw);
138
+ if (Array.isArray(j))
139
+ return j;
140
+ if (Array.isArray(j.data))
141
+ return j.data;
142
+ return [];
143
+ }
144
+ export function loadOrCatalog() {
145
+ const inline = process.env.CURSOR_ROUTE_OR_CATALOG_JSON?.trim();
146
+ if (inline)
147
+ return parseCatalog(inline);
148
+ if (process.env.CURSOR_ROUTE_OR_OFFLINE === "1")
149
+ return [];
150
+ const body = httpGetSync(orModelsUrl());
151
+ if (!body)
152
+ return [];
153
+ try {
154
+ return parseCatalog(body);
155
+ }
156
+ catch {
157
+ return [];
158
+ }
159
+ }
160
+ /** Last live pick if the cache is still fresh — health-safe (no network). */
161
+ export function cachedOrFreePick() {
162
+ try {
163
+ return readCache()?.id ?? null;
164
+ }
165
+ catch {
166
+ return null;
167
+ }
168
+ }
169
+ /**
170
+ * Best live OpenRouter free text model, or OPENROUTER_FALLBACK_MODEL if the
171
+ * catalog is empty/offline. `CURSOR_ROUTE_OR_REFRESH=1` skips cache.
172
+ * Tests: `CURSOR_ROUTE_OR_OFFLINE=1` or `CURSOR_ROUTE_OR_CATALOG_JSON`.
173
+ */
174
+ export function pickOrFreeModel(opts) {
175
+ const refresh = opts?.refresh ||
176
+ process.env.CURSOR_ROUTE_OR_REFRESH === "1";
177
+ if (!refresh && opts?.catalog === undefined) {
178
+ const cached = readCache();
179
+ if (cached)
180
+ return cached.id;
181
+ }
182
+ const models = opts?.catalog ?? loadOrCatalog();
183
+ const ranked = rankOrFreeModels(models);
184
+ const winner = ranked[0];
185
+ if (!winner) {
186
+ // Do not cache fallback — a 15-min stale fallback would masquerade as a
187
+ // live pick in health (`now openrouter/free`). Retry the catalog next call.
188
+ return OPENROUTER_FALLBACK_MODEL;
189
+ }
190
+ const pick = {
191
+ id: winner.id,
192
+ name: winner.name,
193
+ context_length: winner.context_length,
194
+ boost: winner.boost,
195
+ picked_at: new Date().toISOString(),
196
+ candidates: ranked.length,
197
+ source: "ranked-free",
198
+ };
199
+ if (opts?.catalog === undefined)
200
+ writeCache(pick);
201
+ return pick.id;
202
+ }
203
+ /**
204
+ * Resolve OpenRouter `--model` / env to a concrete `provider/model` id.
205
+ * Empty or `free` → CURSOR_ROUTE_OPENROUTER_MODEL pin, else live catalog pick.
206
+ */
207
+ export function openRouterModel(raw) {
208
+ const v = (raw ?? "").trim();
209
+ if (!v || v.toLowerCase() === "free") {
210
+ const env = (process.env.CURSOR_ROUTE_OPENROUTER_MODEL ?? "").trim();
211
+ if (env && env.toLowerCase() !== "free")
212
+ return assertOpenRouterModel(env);
213
+ return pickOrFreeModel();
214
+ }
215
+ return assertOpenRouterModel(v);
216
+ }
@@ -0,0 +1,237 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ /**
6
+ * Live OpenCode Zen free-model pick — same idea as agent-toolkit
7
+ * `select-openrouter-free-model.ps1`: probe the catalog, keep $0 text/coding
8
+ * models, rank, cache ~15 min. Hardcoded ids are fallback only.
9
+ *
10
+ * Catalog: GET https://opencode.ai/zen/v1/models (no key). Zen ids are
11
+ * `big-pickle` / `x-preview-f-free`; the CLI wants `opencode/<id>`.
12
+ */
13
+ export const ZEN_MODELS_URL_DEFAULT = "https://opencode.ai/zen/v1/models";
14
+ /** Offline / fetch-fail fallback — Ox Alpha (zero-retention, currently top-tier free). */
15
+ export const OPENCODE_DEFAULT_MODEL = "opencode/x-preview-f-free";
16
+ const EXCLUDE_RE = /lyria|whisper|tts|embed|embedding|image|vision-only|audio|diffusion|flux|stable-diffusion|moderation/i;
17
+ const CODING_RE = /coder|instruct|chat|laguna|nemotron|glm|qwen|kimi|deepseek|mimo|hy3|gpt-oss|north|gemma/i;
18
+ const OX_ALPHA_RE = /x-preview|ox-alpha|oxalpha/i;
19
+ const CONTRIBUTOR_RE = /contributor-free/i;
20
+ function zenModelsUrl() {
21
+ return (process.env.CURSOR_ROUTE_ZEN_MODELS_URL || ZEN_MODELS_URL_DEFAULT).trim();
22
+ }
23
+ function cacheMinutes() {
24
+ const n = Number(process.env.CURSOR_ROUTE_ZEN_CACHE_MINUTES);
25
+ return Number.isFinite(n) && n > 0 ? n : 15;
26
+ }
27
+ function cachePath() {
28
+ return process.env.CURSOR_ROUTE_ZEN_CACHE_PATH?.trim() ||
29
+ join(tmpdir(), "cursor-route-zen-best-free.json");
30
+ }
31
+ export function assertOpenCodeModel(id) {
32
+ if (!/^[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._\-:]+)+$/i.test(id)) {
33
+ throw new Error(`Invalid OpenCode model ${id}; expected provider/model (e.g. opencode/x-preview-f-free) or free`);
34
+ }
35
+ return id;
36
+ }
37
+ /** Prefix a Zen catalog id (`x-preview-f-free`) for `opencode run --model`. */
38
+ export function asOpenCodeZenId(raw) {
39
+ const id = raw.trim();
40
+ if (!id)
41
+ throw new Error("empty Zen model id");
42
+ return id.includes("/") ? assertOpenCodeModel(id) : assertOpenCodeModel(`opencode/${id}`);
43
+ }
44
+ function num(v) {
45
+ if (typeof v === "number" && Number.isFinite(v))
46
+ return v;
47
+ if (typeof v === "string" && v.trim()) {
48
+ const n = Number(v);
49
+ if (Number.isFinite(n))
50
+ return n;
51
+ }
52
+ return NaN;
53
+ }
54
+ export function isZenFreeModel(m) {
55
+ const id = (m.id || "").trim();
56
+ if (!id)
57
+ return false;
58
+ if (EXCLUDE_RE.test(id) || EXCLUDE_RE.test(m.name || ""))
59
+ return false;
60
+ const prompt = num(m.pricing?.prompt);
61
+ const completion = num(m.pricing?.completion);
62
+ const pricedFree = prompt === 0 && completion === 0;
63
+ const tagged = id.endsWith("-free") ||
64
+ id.endsWith(":free") ||
65
+ /(^|\/)big-pickle$/i.test(id);
66
+ return tagged || pricedFree;
67
+ }
68
+ /** Higher boost wins. Ox Alpha first while it is in the free catalog. */
69
+ export function zenFreeBoost(id) {
70
+ const s = id.toLowerCase();
71
+ if (OX_ALPHA_RE.test(s))
72
+ return 40;
73
+ if (CONTRIBUTOR_RE.test(s))
74
+ return 1;
75
+ if (CODING_RE.test(s))
76
+ return 20;
77
+ if (s.endsWith("-free") || s.endsWith(":free"))
78
+ return 10;
79
+ if (/(^|\/)big-pickle$/.test(s))
80
+ return 5;
81
+ return 0;
82
+ }
83
+ export function rankZenFreeModels(models) {
84
+ const out = [];
85
+ for (const m of models) {
86
+ if (!isZenFreeModel(m))
87
+ continue;
88
+ const raw = (m.id || "").trim();
89
+ let id;
90
+ try {
91
+ id = asOpenCodeZenId(raw);
92
+ }
93
+ catch {
94
+ continue;
95
+ }
96
+ const ctx = Number.isFinite(Number(m.context_length)) ? Number(m.context_length) : 0;
97
+ out.push({
98
+ id,
99
+ name: (m.name || raw).trim(),
100
+ context_length: ctx,
101
+ boost: zenFreeBoost(id),
102
+ });
103
+ }
104
+ out.sort((a, b) => {
105
+ if (b.boost !== a.boost)
106
+ return b.boost - a.boost;
107
+ if (b.context_length !== a.context_length)
108
+ return b.context_length - a.context_length;
109
+ return a.id.localeCompare(b.id);
110
+ });
111
+ return out;
112
+ }
113
+ function readCache() {
114
+ const p = cachePath();
115
+ if (!existsSync(p))
116
+ return null;
117
+ try {
118
+ const j = JSON.parse(readFileSync(p, "utf8"));
119
+ if (!j?.id)
120
+ return null;
121
+ const ageMs = Date.now() - Date.parse(j.picked_at);
122
+ if (!Number.isFinite(ageMs) || ageMs < 0)
123
+ return null;
124
+ if (ageMs > cacheMinutes() * 60_000)
125
+ return null;
126
+ assertOpenCodeModel(j.id);
127
+ return j;
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ function writeCache(pick) {
134
+ try {
135
+ writeFileSync(cachePath(), JSON.stringify(pick), { mode: 0o600 });
136
+ }
137
+ catch {
138
+ /* cache is optional */
139
+ }
140
+ }
141
+ function httpGetSync(url, timeoutMs = 4000) {
142
+ const sec = Math.max(1, Math.ceil(timeoutMs / 1000));
143
+ const curl = spawnSync("curl", ["-fsS", "--max-time", String(sec), "-H", "Accept: application/json", url], { encoding: "utf8", timeout: timeoutMs + 500, env: { ...process.env } });
144
+ if (curl.status === 0 && curl.stdout?.trim())
145
+ return curl.stdout;
146
+ return null;
147
+ }
148
+ function parseCatalog(raw) {
149
+ const j = JSON.parse(raw);
150
+ if (Array.isArray(j))
151
+ return j;
152
+ if (Array.isArray(j.data))
153
+ return j.data;
154
+ return [];
155
+ }
156
+ export function loadZenCatalog() {
157
+ const inline = process.env.CURSOR_ROUTE_ZEN_CATALOG_JSON?.trim();
158
+ if (inline)
159
+ return parseCatalog(inline);
160
+ if (process.env.CURSOR_ROUTE_ZEN_OFFLINE === "1")
161
+ return [];
162
+ const body = httpGetSync(zenModelsUrl());
163
+ if (!body)
164
+ return [];
165
+ try {
166
+ return parseCatalog(body);
167
+ }
168
+ catch {
169
+ return [];
170
+ }
171
+ }
172
+ /** Last live pick if the cache is still fresh — health-safe (no network). */
173
+ export function cachedZenFreePick() {
174
+ try {
175
+ return readCache()?.id ?? null;
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ }
181
+ /**
182
+ * Best live Zen free model, or OPENCODE_DEFAULT_MODEL if the catalog is empty/offline.
183
+ * `CURSOR_ROUTE_ZEN_REFRESH=1` skips cache. Tests: `CURSOR_ROUTE_ZEN_OFFLINE=1` or
184
+ * `CURSOR_ROUTE_ZEN_CATALOG_JSON`.
185
+ */
186
+ export function pickZenFreeModel(opts) {
187
+ const refresh = opts?.refresh ||
188
+ process.env.CURSOR_ROUTE_ZEN_REFRESH === "1";
189
+ if (!refresh && opts?.catalog === undefined) {
190
+ const cached = readCache();
191
+ if (cached)
192
+ return cached.id;
193
+ }
194
+ const models = opts?.catalog ?? loadZenCatalog();
195
+ const ranked = rankZenFreeModels(models);
196
+ const winner = ranked[0];
197
+ if (!winner) {
198
+ const fallback = {
199
+ id: OPENCODE_DEFAULT_MODEL,
200
+ name: "Ox Alpha Free (fallback)",
201
+ context_length: 0,
202
+ boost: 0,
203
+ picked_at: new Date().toISOString(),
204
+ candidates: 0,
205
+ source: "fallback",
206
+ };
207
+ if (opts?.catalog === undefined)
208
+ writeCache(fallback);
209
+ return fallback.id;
210
+ }
211
+ const pick = {
212
+ id: winner.id,
213
+ name: winner.name,
214
+ context_length: winner.context_length,
215
+ boost: winner.boost,
216
+ picked_at: new Date().toISOString(),
217
+ candidates: ranked.length,
218
+ source: "ranked-free",
219
+ };
220
+ if (opts?.catalog === undefined)
221
+ writeCache(pick);
222
+ return pick.id;
223
+ }
224
+ /**
225
+ * Resolve OpenCode `--model` / env to a concrete `provider/model` id.
226
+ * Empty or `free` → CURSOR_ROUTE_OPENCODE_MODEL pin, else live Zen free pick.
227
+ */
228
+ export function openCodeModel(raw) {
229
+ const v = (raw ?? "").trim();
230
+ if (!v || v.toLowerCase() === "free") {
231
+ const env = (process.env.CURSOR_ROUTE_OPENCODE_MODEL ?? "").trim();
232
+ if (env && env.toLowerCase() !== "free")
233
+ return assertOpenCodeModel(env);
234
+ return pickZenFreeModel();
235
+ }
236
+ return assertOpenCodeModel(v);
237
+ }
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  title: cursor-route workspace — working brief (edit in place)
3
3
  repo: ~/Projects/cursor-route
4
- npm: cursor-route@0.1.10 (unreleased OpenCode worker)
4
+ npm: cursor-route@0.1.13 (publishing this slice)
5
5
  created: 2026-08-12
6
- updated: 2026-08-21
6
+ updated: 2026-08-29
7
7
  ---
8
8
 
9
9
  # cursor-route — living brief
@@ -18,28 +18,31 @@ Cursor Agent plans. Workers run in tmux via `cursor-route`:
18
18
 
19
19
  | Lane | Worker | Intent |
20
20
  |------|--------|--------|
21
- | `easy` | OpenRouter free | Wording / drafts — non-secret prompts only |
22
- | `mid` | claude-ds (DeepSeek behind Claude Code) | Default implement (**Flash**; `--model pro` when needed) |
21
+ | `easy` | OpenRouter free (live pick) | Wording / drafts — non-secret prompts only |
22
+ | `mid` | claude-ds (DeepSeek behind Claude Code) | Default implement (**Flash**; `--model vision` for screenshots; `--model pro` harder mid / hard backup only) |
23
23
  | `hard` | Grok CLI | Hard implement |
24
- | opt-in | OpenCode | `--worker opencode` coding agent on Zen free models (default `opencode/big-pickle`) |
24
+ | opt-in | OpenCode | `--worker opencode` coding agent; `--model free` ranks live Zen catalog (Ox Alpha first while listed) |
25
25
 
26
26
  Always-approve on for coding worktrees (`--ask` / `CURSOR_ROUTE_ASK=1` to opt out) — not LIVE Discord/trading. Jobs live in `~/.local/share/cursor-route/jobs`, not in this clone.
27
27
 
28
- Install: `npm i -g cursor-route` → **0.1.9** live; **0.1.10** unreleased (OpenCode worker). Release notes: [CHANGELOG.md](../../CHANGELOG.md).
28
+ Install: `npm i -g cursor-route` → **0.1.13**. Release notes: [CHANGELOG.md](../../CHANGELOG.md).
29
29
 
30
30
  ## Open (edit / check off)
31
31
 
32
32
  - [x] **Flash vs Pro on the CLI** — public default **Flash**; `--model pro` → `deepseek-v4-pro` (LIVE 0.1.6+)
33
33
  - [x] pass `claude-ds -Model deepseek-v4-flash|deepseek-v4-pro` from the adapter
34
34
  - [x] add `--model flash|pro` on `start`
35
- - [x] document Grok **auth** ≠ usage-out (`grok login`) vs quota Pro stand-in
35
+ - [x] document Grok **auth** ≠ usage-out (`grok login`); Flash-first when Grok **usage** is out; Pro = harder mid / hard backup only (0.1.12)
36
36
  - [x] **Skill `route-orch`** — Flash/Pro table in `skills/` + `.cursor/skills/`
37
37
  - [x] **Official DeepSeek Harness** — `deepseek` adapter slot present; `@deepseek-ai/dsh` 0.1.0-rc.6 (2026-08-13, github.com/deepseek-ai/deepseek-harness, MIT) is a developer-preview plugin kernel (web UI + `dsh --profile headless "job"`), not a Claude Code replacement; mid does not swap
38
38
  - [x] **0.1.7 debug fixes** — env default wired through `startJob`; Anthropic hatch omits DS ids; preserve `[1m]`
39
39
  - [x] **Experimental `--worker deepseek`** — 0.1.8: real dsh adapter (`dsh --profile headless` + per-job Cordis patch pins the model; never writes `~/.dsh/settings.yaml`). Always-approve → `DSH_PERMISSION_MODE=danger-full-access`, `--ask` → `workspace-write`; key via env only. Health ✓ needs `dsh` + `DEEPSEEK_API_KEY` (override `CURSOR_ROUTE_DSH_BIN`). Mid stays **claude-ds**.
40
40
  - [x] **route-orch brief steals (2026-08-14)** — AutoDesign / misevolution / Vero habits into the public skill: **Verify / claim closeout** (external eval contract; activity ≠ verification), **Eval & skill hygiene** (mid-run Verify-rewrite ban; skill misevolution HITL — no auto-promotion of worker-trajectory variants; verify-fail → reconsider plan/definition + stage attribution spawn/execute/verify); handoff shape Success criteria + Verify + NEVER; skills synced; mid stays claude-ds.
41
41
  - [x] **Health proves mid DeepSeek + evidence tree** — 0.1.9: `lane:mid` ✓ only when DeepSeek is proven (shim or DeepSeek `ANTHROPIC_BASE_URL`; Anthropic hatch is not proof); health JSON `lanes.mid`; `status --json` evidence tree (`spawn` / `execute` / `verify.claim=unverified`); skill health-before-mid + evidence-tree closeout. Mid stays **claude-ds**.
42
- - [x] **Opt-in `--worker opencode`** — 0.1.10: `opencode run --dir` + `--model` (default Zen free `opencode/big-pickle` / `--model free`); always-approve → `--auto`; `--ask` omits it; never rewrites `~/.config/opencode/opencode.json`. Health ✓ needs `opencode` on PATH (`CURSOR_ROUTE_OPENCODE_BIN`). Mid stays **claude-ds**; easy stays OpenRouter chat. Free Zen may log/train — non-secret prompts.
42
+ - [x] **Opt-in `--worker opencode`** — 0.1.10: `opencode run --dir` + `--model`; always-approve → `--auto`; `--ask` omits it; never rewrites `~/.config/opencode/opencode.json`. Health ✓ needs `opencode` on PATH (`CURSOR_ROUTE_OPENCODE_BIN`). Mid stays **claude-ds**; easy stays OpenRouter chat. Free Zen may log/train — non-secret prompts.
43
+ - [x] **Live Zen free pick** — 0.1.11: `--model free` ranks `GET https://opencode.ai/zen/v1/models` (Ox Alpha first while listed; ~15 min cache; Ox Alpha fallback if fetch fails). Pin with `CURSOR_ROUTE_OPENCODE_MODEL`. Health stays offline (cache/fallback only).
44
+ - [x] **Flash-first + vision + live OpenRouter pick** — 0.1.12: `--model flash` stays the cheap Grok-out default; `--model vision` + screenshot auto-pick; easy lane live-picks free OpenRouter text models (`openrouter/free` is fetch-fail fallback only). Health never fetches `/models`. `route-orch` ProgRouter + MoRe one-liners. Hero fixture regenerated to 0.1.12. No npm publish in this slice.
45
+ - [x] **Kimi 0.1.13 follow-up** — do not cache OpenRouter fallback; health labels uncached fallback; no Authorization on GET /models; env-invalid model error names the env vars; `\bimage\b` vision trigger.
43
46
  - [ ] **Hero GIF** — still outstanding; dry-run fixture ships as the substitute for now (`docs/fixtures/hero-demo.log` — see `docs/DEMO_GIF.md`)
44
47
  - [x] **Do not** paste private `ROUTE_KIT`, SIP, prod paths, or hang-watchdog env into this public repo
45
48
 
@@ -53,6 +56,8 @@ Install: `npm i -g cursor-route` → **0.1.9** live; **0.1.10** unreleased (Open
53
56
  | `src/adapters/grok.ts` | Hard worker |
54
57
  | `src/adapters/openrouter.ts` | Easy worker |
55
58
  | `src/adapters/opencode.ts` | Opt-in OpenCode worker (`--worker opencode`; mid stays claude-ds) |
59
+ | `src/zen-free.ts` | Live Zen catalog rank for `--model free` |
60
+ | `src/or-free.ts` | Live OpenRouter catalog rank for easy-lane `--model free` |
56
61
  | `skills/route-orch/SKILL.md` | Cursor skill — spawn CLI, do not implement in-session |
57
62
  | `CHANGELOG.md` | Release notes |
58
63
  | `SECURITY.md` | Secret refuse gate |
@@ -71,4 +76,7 @@ Install: `npm i -g cursor-route` → **0.1.9** live; **0.1.10** unreleased (Open
71
76
  | 2026-08-14 | Experimental `--worker deepseek` wired to official dsh (headless + per-job patch + `DSH_PERMISSION_MODE` + key-via-env); `--model` applies to claude-ds + deepseek; mid stays claude-ds → 0.1.8 LIVE. |
72
77
  | 2026-08-14 | route-orch brief steals (AutoDesign / misevolution / Vero): **Verify / claim closeout** + **Eval & skill hygiene**; handoff Success criteria + Verify + NEVER; both skill copies synced; mid stays claude-ds. Docs-only, no version bump. |
73
78
  | 2026-08-18 | Health `lane:mid` + `lanes.mid` prove DeepSeek; status evidence tree (`verify.claim` stays unverified); skill health-before-mid + closeout tree; mid stays claude-ds → 0.1.9 LIVE. |
74
- | 2026-08-21 | Opt-in `--worker opencode` (Zen free `opencode/big-pickle`, `--auto`, no config rewrite); mid stays claude-ds → 0.1.10. |
79
+ | 2026-08-21 | Opt-in `--worker opencode` (Zen free `opencode/big-pickle`, `--auto`, no config rewrite); mid stays claude-ds → 0.1.10 LIVE. |
80
+ | 2026-08-21 | Live Zen free pick (Ox Alpha first while listed; OpenRouter-style catalog rank) → 0.1.11 LIVE. |
81
+ | 2026-08-29 | Flash-first docs, `--model vision` + auto-pick, live OpenRouter free pick, route-orch K318/K322, hero fixture 0.1.12. No npm publish. |
82
+ | 2026-08-29 | Kimi audit follow-up → 0.1.13: no fallback cache, health labels, unauth GET /models, env error wording. |
@@ -6,10 +6,10 @@ Simulated capture output for README / tweet assets — replace with a real GIF o
6
6
 
7
7
  ```
8
8
  $ cursor-route --version
9
- 0.1.9
9
+ 0.1.13
10
10
 
11
11
  $ cursor-route health
12
- cursor-route v0.1.9
12
+ cursor-route v0.1.13
13
13
  health: OK
14
14
  ✓ tmux
15
15
  ✓ runtime bun ok
@@ -32,7 +32,7 @@ $ cursor-route jobs --json
32
32
 
33
33
  Verified locally (2026-08-10): headless `claude-ds` smoke returned `CURSOR_ROUTE_SMOKE_OK`.
34
34
  Grok smoke hit 402 (Build usage balance exhausted) — auth/PATH wiring works; top up Grok Build for live demos.
35
- Use `--model pro` when Grok **usage** is out (not the same as a missing `grok login`).
35
+ Use `--model flash` when Grok **usage** is out (cheap default). `--model pro` is harder mid / hard backup only — not the default Grok-out stand-in. A missing `grok login` is auth, not the Pro case.
36
36
 
37
37
  Current commands: `health`, `start`, `jobs`, `status`, `capture`, `send`, `attach`, `kill`, `sessions`, `clean`.
38
38
  Headless demos (no tmux) use `--no-tmux` and `capture`/`status` instead of `attach`/`send`.
@@ -23,8 +23,11 @@ unset CURSOR_ROUTE_CLAUDE_DS_BIN CURSOR_ROUTE_GROK_BIN CURSOR_ROUTE_DSH_BIN \
23
23
  CURSOR_ROUTE_ALLOW_ANTHROPIC \
24
24
  CURSOR_ROUTE_ALLOW_STOCK_CLAUDE CURSOR_ROUTE_ANTHROPIC_BASE_URL CURSOR_ROUTE_DS_MODEL \
25
25
  CURSOR_ROUTE_OPENROUTER_MODEL OPENROUTER_API_KEY OPENROUTER_BASE_URL \
26
+ CURSOR_ROUTE_OR_CATALOG_JSON CURSOR_ROUTE_OR_CACHE_PATH CURSOR_ROUTE_OR_REFRESH \
26
27
  ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN ANTHROPIC_API_KEY ANTHROPIC_MODEL \
27
28
  DEEPSEEK_API_KEY XAI_API_KEY 2>/dev/null || true
29
+ # Stable easy-lane dry-run: do not fetch the live catalog while generating the fixture.
30
+ export CURSOR_ROUTE_OR_OFFLINE=1
28
31
 
29
32
  TMP="$(mktemp -d)"
30
33
  trap 'rm -rf "$TMP"' EXIT
@@ -1,8 +1,8 @@
1
1
  $ cursor-route --version
2
- 0.1.9
2
+ 0.1.13
3
3
 
4
4
  $ CURSOR_ROUTE_RELAXED=1 cursor-route health
5
- cursor-route v0.1.9
5
+ cursor-route v0.1.13
6
6
  health: OK
7
7
 
8
8
  ✓ tmux ok
@@ -10,8 +10,9 @@ health: OK
10
10
  ✓ script(1) ok (tty log capture)
11
11
  ✓ worker:grok ok (auth checked at first start — run grok login if jobs fail) @ ~/.grok/bin/grok
12
12
  ✓ worker:claude-ds ok (claude-ds (DeepSeek shim); default model deepseek-v4-flash) @ ~/.local/bin/claude-ds
13
- ✗ worker:openrouter OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane model defaults to openrouter/free) @ node '~/Projects/cursor-route/dist/openrouter-run.js'
13
+ ✗ worker:openrouter OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane live-picks a free model at start; pin with CURSOR_ROUTE_OPENROUTER_MODEL) @ node '~/Projects/cursor-route/dist/openrouter-run.js'
14
14
  ✗ worker:deepseek dsh (@deepseek-ai/dsh) not found — install: npm i -g @deepseek-ai/dsh. Mid default remains claude-ds.
15
+ ✓ worker:opencode ok (opencode run; --model free = live Zen pick, now opencode/x-preview-f-free; auth at first start — opencode auth login if jobs fail; mid default remains claude-ds) @ ~/.local/bin/opencode
15
16
  ✓ lane:mid DeepSeek proven (claude-ds (DeepSeek shim))
16
17
  ✓ cursor_cli optional ok (agent on PATH) — v0 supervisor is Cursor skill, not CLI
17
18
  ✓ relaxed CURSOR_ROUTE_RELAXED=1 — tmux optional (headless OK)
@@ -26,6 +27,7 @@ command: cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~
26
27
  $ cursor-route start --lane easy --dry-run "Rewrite this FAQ answer in 3 sentences"
27
28
  dry-run job b2c3d4e5
28
29
  worker: openrouter
30
+ model: openrouter/free
29
31
  command: node '~/Projects/cursor-route/dist/openrouter-run.js' --prompt-file '~/.local/share/cursor-route/jobs/b2c3d4e5.prompt'
30
32
 
31
33
  $ cursor-route start --lane hard --dry-run "Refactor auth module; run tests; report verify evidence"
package/llms.txt CHANGED
@@ -2,39 +2,41 @@
2
2
 
3
3
  > Cursor stays the planner. DeepSeek (mid), Grok CLI (hard), and OpenRouter free models (easy) run parallel coding workers in tmux. `--worker opencode` is an opt-in coding agent on OpenCode Zen free models.
4
4
 
5
- MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.10**)
5
+ MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.13**)
6
6
  GitHub: https://github.com/cemini23/cursor-route
7
7
 
8
8
  ## FAQ
9
9
 
10
10
  ### What is cursor-route?
11
- cursor-route is a public MIT CLI and Cursor skill that runs parallel coding workers in tmux while Cursor remains the planner. DeepSeek handles the mid lane, Grok CLI handles the hard lane, and OpenRouter free models handle the easy lane (wording/drafts, non-secret prompts only). `--worker opencode` is an opt-in coding agent (default `opencode/big-pickle`) — not a lane default.
11
+ cursor-route is a public MIT CLI and Cursor skill that runs parallel coding workers in tmux while Cursor remains the planner. DeepSeek handles the mid lane, Grok CLI handles the hard lane, and OpenRouter free models handle the easy lane (wording/drafts, non-secret prompts only). `--worker opencode` is an opt-in coding agent (`--model free` ranks the live Zen catalog; Ox Alpha wins while listed) — not a lane default.
12
12
 
13
13
  ### How is this different from Codex orchestrator?
14
14
  It uses the familiar strategist and worker-pane shape, but it is not a Codex clone. cursor-route uses Cursor as the planner and DeepSeek plus Grok CLI as workers. Codex is not required.
15
15
 
16
16
  ### Does mid lane use Anthropic Claude?
17
- No. The mid worker is DeepSeek. Claude Code is the harness, configured with `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic` and a DeepSeek key in `ANTHROPIC_AUTH_TOKEN`. 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`).
17
+ No. The mid worker is DeepSeek. Claude Code is the harness, configured with `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic` and a DeepSeek key in `ANTHROPIC_AUTH_TOKEN`. Default model is Flash (`--model flash`) keep Flash when Grok **usage** is out. `--model vision` is for screenshots / ui mocks. `--model pro` is harder mid / **hard backup only**, not the default Grok-out stand-in. A missing `grok login` is auth, not the Pro case.
18
18
 
19
19
  ### Flash vs Pro?
20
20
  | Flag | Model | When |
21
21
  |------|-------|------|
22
- | `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute |
23
- | `--model pro` | `deepseek-v4-pro` | Harder mid / Grok usage stand-in |
22
+ | `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute. Prefer this when Grok **usage** is out |
23
+ | `--model vision` | `deepseek-v4-flash-vision-exp` | Screenshots / ui mocks / image prompts (or auto-pick) |
24
+ | `--model pro` | `deepseek-v4-pro` | Harder mid / **hard backup** only — not the default Grok-out stand-in |
25
+ | `--model deepseek-v4-pro[1m]` | preserved SKU | Large-context Pro |
24
26
 
25
27
  ### How do I use OpenCode free models?
26
- Install OpenCode (`npm i -g opencode-ai`), run `opencode auth login`, then `cursor-route start --worker opencode "…"`. Default model is `opencode/big-pickle` (`--model free`). Mid stays `claude-ds`.
28
+ Install OpenCode (`npm i -g opencode-ai`), run `opencode auth login`, then `cursor-route start --worker opencode "…"`. `--model free` ranks live Zen free models (Ox Alpha first while listed). Mid stays `claude-ds`.
27
29
 
28
30
  ### How do I install?
29
31
  Run `npm i -g cursor-route`, install tmux if needed, then run `cursor-route health`. Copy the Cursor skill from `$(npm root -g)/cursor-route/skills/route-orch`. Source: https://github.com/cemini23/cursor-route
30
32
 
31
33
  ### Is it free?
32
- 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 use OpenCode Zen free models.
34
+ 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 live-picks a free OpenRouter text model at start (fallback router `openrouter/free` only if the catalog fetch fails). `--worker opencode` can use OpenCode Zen free models.
33
35
 
34
36
  ## Install
35
37
 
36
38
  - npm: `npm i -g cursor-route`
37
39
  - Health: `cursor-route health`
38
- - Mid: `cursor-route start --lane mid "…"` (Flash) · `--model pro` when needed
40
+ - Mid: `cursor-route start --lane mid "…"` (Flash; keep Flash when Grok usage is out) · `--model vision` for screenshots · `--model pro` harder mid / hard backup only
39
41
  - OpenCode (opt-in): `cursor-route start --worker opencode "…"`
40
42
  - Skill: `/route-orch` (Cursor)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-route",
3
- "version": "0.1.10",
3
+ "version": "0.1.13",
4
4
  "description": "Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter easy lane + OpenCode (opt-in free) are the parallel army \u2014 lane-aware /route orchestration in tmux.",
5
5
  "type": "module",
6
6
  "license": "MIT",