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 +22 -0
- package/CONTRIBUTING.md +2 -0
- package/README.md +47 -11
- package/SECURITY.md +6 -5
- package/SUPPORT.md +2 -0
- package/dist/adapters/claude-ds.js +18 -0
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/opencode.js +83 -0
- package/dist/cli.js +33 -12
- package/dist/config.js +30 -3
- package/dist/health.js +17 -1
- package/dist/jobs.js +47 -2
- package/docs/briefs/WORKING.md +12 -4
- package/docs/demo-notes.md +3 -2
- package/docs/fixtures/hero-demo.log +3 -2
- package/llms.txt +8 -4
- package/package.json +3 -2
- package/skills/route-orch/SKILL.md +53 -14
- package/src/adapters/claude-ds.ts +19 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/opencode.test.ts +238 -0
- package/src/adapters/opencode.ts +89 -0
- package/src/adapters/types.ts +1 -1
- package/src/cli.test.ts +133 -5
- package/src/cli.ts +31 -10
- package/src/config.ts +38 -6
- package/src/health.ts +22 -1
- package/src/jobs.ts +71 -5
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, defaultDsModelFromEnv, DS_MODEL_IDS, } from "./config.js";
|
|
5
|
+
import { config, sessionName, defaultDsModelFromEnv, DS_MODEL_IDS, openCodeModel, } 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";
|
|
@@ -59,6 +59,29 @@ export function readJob(id) {
|
|
|
59
59
|
export function writeJob(job) {
|
|
60
60
|
writeSecure(jobPaths(job.id).json, JSON.stringify(job, null, 2));
|
|
61
61
|
}
|
|
62
|
+
export function jobEvidence(job, sessionAlive) {
|
|
63
|
+
return {
|
|
64
|
+
spawn: {
|
|
65
|
+
jobId: job.id,
|
|
66
|
+
worker: job.worker,
|
|
67
|
+
lane: job.lane ?? null,
|
|
68
|
+
model: job.model ?? null,
|
|
69
|
+
startedAt: job.startedAt ?? job.createdAt,
|
|
70
|
+
},
|
|
71
|
+
execute: {
|
|
72
|
+
status: job.status,
|
|
73
|
+
exitCode: job.exitCode ?? null,
|
|
74
|
+
tmuxSession: job.tmuxSession,
|
|
75
|
+
pid: job.pid ?? null,
|
|
76
|
+
sessionAlive,
|
|
77
|
+
},
|
|
78
|
+
verify: {
|
|
79
|
+
captureHint: `cursor-route capture ${job.id}`,
|
|
80
|
+
logBytes: job.logBytes ?? null,
|
|
81
|
+
claim: "unverified",
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
62
85
|
function pidAlive(pid) {
|
|
63
86
|
try {
|
|
64
87
|
process.kill(pid, 0);
|
|
@@ -73,6 +96,9 @@ function pidAlive(pid) {
|
|
|
73
96
|
encoding: "utf8",
|
|
74
97
|
stdio: ["ignore", "pipe", "ignore"],
|
|
75
98
|
});
|
|
99
|
+
// spawnSync does not throw on EPERM (macOS sandbox); empty stdout is not "dead".
|
|
100
|
+
if (r.error || r.status !== 0)
|
|
101
|
+
return true;
|
|
76
102
|
const state = (r.stdout || "").trim();
|
|
77
103
|
return state !== "" && !state.startsWith("Z");
|
|
78
104
|
}
|
|
@@ -219,14 +245,17 @@ export function startJob(opts) {
|
|
|
219
245
|
writeSecure(paths.prompt, opts.prompt);
|
|
220
246
|
let model;
|
|
221
247
|
let modelId;
|
|
248
|
+
let dsAlias;
|
|
222
249
|
if (worker === "claude-ds" || worker === "deepseek") {
|
|
223
250
|
if (opts.model) {
|
|
251
|
+
dsAlias = opts.model;
|
|
224
252
|
model = opts.model;
|
|
225
253
|
modelId = opts.modelId ?? DS_MODEL_IDS[opts.model];
|
|
226
254
|
}
|
|
227
255
|
else {
|
|
228
256
|
try {
|
|
229
257
|
const choice = defaultDsModelFromEnv();
|
|
258
|
+
dsAlias = choice.alias;
|
|
230
259
|
model = choice.alias;
|
|
231
260
|
modelId = opts.modelId ?? choice.id;
|
|
232
261
|
}
|
|
@@ -241,13 +270,29 @@ export function startJob(opts) {
|
|
|
241
270
|
}
|
|
242
271
|
}
|
|
243
272
|
}
|
|
273
|
+
else if (worker === "opencode") {
|
|
274
|
+
try {
|
|
275
|
+
const ocModel = openCodeModel(opts.modelId);
|
|
276
|
+
model = ocModel;
|
|
277
|
+
modelId = ocModel;
|
|
278
|
+
}
|
|
279
|
+
catch (e) {
|
|
280
|
+
try {
|
|
281
|
+
unlinkSync(paths.prompt);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
/* ignore */
|
|
285
|
+
}
|
|
286
|
+
return { ok: false, error: e.message };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
244
289
|
let plan;
|
|
245
290
|
try {
|
|
246
291
|
plan = adapter.buildLaunch({
|
|
247
292
|
promptFile: paths.prompt,
|
|
248
293
|
cwd,
|
|
249
294
|
alwaysApprove,
|
|
250
|
-
model,
|
|
295
|
+
model: dsAlias,
|
|
251
296
|
modelId,
|
|
252
297
|
dryRun: Boolean(opts.dryRun),
|
|
253
298
|
});
|
package/docs/briefs/WORKING.md
CHANGED
|
@@ -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.
|
|
4
|
+
npm: cursor-route@0.1.10 (unreleased — OpenCode worker)
|
|
5
5
|
created: 2026-08-12
|
|
6
|
-
updated: 2026-08-
|
|
6
|
+
updated: 2026-08-21
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# cursor-route — living brief
|
|
@@ -21,10 +21,11 @@ Cursor Agent plans. Workers run in tmux via `cursor-route`:
|
|
|
21
21
|
| `easy` | OpenRouter free | Wording / drafts — non-secret prompts only |
|
|
22
22
|
| `mid` | claude-ds (DeepSeek behind Claude Code) | Default implement (**Flash**; `--model pro` when needed) |
|
|
23
23
|
| `hard` | Grok CLI | Hard implement |
|
|
24
|
+
| opt-in | OpenCode | `--worker opencode` coding agent on Zen free models (default `opencode/big-pickle`) |
|
|
24
25
|
|
|
25
|
-
Always-approve on (`--ask` / `CURSOR_ROUTE_ASK=1` to opt out). Jobs live in `~/.local/share/cursor-route/jobs`, not in this clone.
|
|
26
|
+
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.
|
|
26
27
|
|
|
27
|
-
Install: `npm i -g cursor-route` → **0.1.
|
|
28
|
+
Install: `npm i -g cursor-route` → **0.1.9** live; **0.1.10** unreleased (OpenCode worker). Release notes: [CHANGELOG.md](../../CHANGELOG.md).
|
|
28
29
|
|
|
29
30
|
## Open (edit / check off)
|
|
30
31
|
|
|
@@ -36,6 +37,9 @@ Install: `npm i -g cursor-route` → **0.1.8**. Release notes: [CHANGELOG.md](..
|
|
|
36
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
|
|
37
38
|
- [x] **0.1.7 debug fixes** — env default wired through `startJob`; Anthropic hatch omits DS ids; preserve `[1m]`
|
|
38
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
|
+
- [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
|
+
- [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.
|
|
39
43
|
- [ ] **Hero GIF** — still outstanding; dry-run fixture ships as the substitute for now (`docs/fixtures/hero-demo.log` — see `docs/DEMO_GIF.md`)
|
|
40
44
|
- [x] **Do not** paste private `ROUTE_KIT`, SIP, prod paths, or hang-watchdog env into this public repo
|
|
41
45
|
|
|
@@ -48,6 +52,7 @@ Install: `npm i -g cursor-route` → **0.1.8**. Release notes: [CHANGELOG.md](..
|
|
|
48
52
|
| `src/adapters/deepseek.ts` | Experimental dsh worker (`--worker deepseek`; mid stays claude-ds) |
|
|
49
53
|
| `src/adapters/grok.ts` | Hard worker |
|
|
50
54
|
| `src/adapters/openrouter.ts` | Easy worker |
|
|
55
|
+
| `src/adapters/opencode.ts` | Opt-in OpenCode worker (`--worker opencode`; mid stays claude-ds) |
|
|
51
56
|
| `skills/route-orch/SKILL.md` | Cursor skill — spawn CLI, do not implement in-session |
|
|
52
57
|
| `CHANGELOG.md` | Release notes |
|
|
53
58
|
| `SECURITY.md` | Secret refuse gate |
|
|
@@ -64,3 +69,6 @@ Install: `npm i -g cursor-route` → **0.1.8**. Release notes: [CHANGELOG.md](..
|
|
|
64
69
|
| 2026-08-13 | GPTSOL fixes: scrub inherited env/paths, stable job ids, ignore `docs/briefs/handoffs/`, GIF checkbox wording. |
|
|
65
70
|
| 2026-08-14 | DeepSeek Harness eval: `@deepseek-ai/dsh` 0.1.0-rc.6 is a developer-preview plugin kernel, not a mid replacement; `--worker deepseek` stays unhealthy; mid remains claude-ds (docs-only, no version bump). |
|
|
66
71
|
| 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
|
+
| 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
|
+
| 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. |
|
package/docs/demo-notes.md
CHANGED
|
@@ -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
|
+
0.1.9
|
|
10
10
|
|
|
11
11
|
$ cursor-route health
|
|
12
|
-
cursor-route v0.1.
|
|
12
|
+
cursor-route v0.1.9
|
|
13
13
|
health: OK
|
|
14
14
|
✓ tmux
|
|
15
15
|
✓ runtime bun ok
|
|
@@ -18,6 +18,7 @@ health: OK
|
|
|
18
18
|
✓ worker:claude-ds
|
|
19
19
|
✓ worker:openrouter
|
|
20
20
|
✗ worker:deepseek dsh not found — mid default remains claude-ds
|
|
21
|
+
✓ lane:mid DeepSeek proven (claude-ds (DeepSeek shim))
|
|
21
22
|
✓ jobs_dir ~/.local/share/cursor-route/jobs
|
|
22
23
|
|
|
23
24
|
$ cursor-route start --lane mid "Add a failing test then make it pass"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
$ cursor-route --version
|
|
2
|
-
0.1.
|
|
2
|
+
0.1.9
|
|
3
3
|
|
|
4
4
|
$ CURSOR_ROUTE_RELAXED=1 cursor-route health
|
|
5
|
-
cursor-route v0.1.
|
|
5
|
+
cursor-route v0.1.9
|
|
6
6
|
health: OK
|
|
7
7
|
|
|
8
8
|
✓ tmux ok
|
|
@@ -12,6 +12,7 @@ health: OK
|
|
|
12
12
|
✓ worker:claude-ds ok (claude-ds (DeepSeek shim); default model deepseek-v4-flash) @ ~/.local/bin/claude-ds
|
|
13
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'
|
|
14
14
|
✗ worker:deepseek dsh (@deepseek-ai/dsh) not found — install: npm i -g @deepseek-ai/dsh. Mid default remains claude-ds.
|
|
15
|
+
✓ lane:mid DeepSeek proven (claude-ds (DeepSeek shim))
|
|
15
16
|
✓ cursor_cli optional ok (agent on PATH) — v0 supervisor is Cursor skill, not CLI
|
|
16
17
|
✓ relaxed CURSOR_ROUTE_RELAXED=1 — tmux optional (headless OK)
|
|
17
18
|
✓ jobs_dir ~/.local/share/cursor-route/jobs
|
package/llms.txt
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# cursor-route
|
|
2
2
|
|
|
3
|
-
> Cursor stays the planner. DeepSeek (mid), Grok CLI (hard), and OpenRouter free models (easy) run parallel coding workers in tmux.
|
|
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.
|
|
5
|
+
MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.10**)
|
|
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).
|
|
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.
|
|
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.
|
|
@@ -22,15 +22,19 @@ No. The mid worker is DeepSeek. Claude Code is the harness, configured with `ANT
|
|
|
22
22
|
| `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute |
|
|
23
23
|
| `--model pro` | `deepseek-v4-pro` | Harder mid / Grok usage stand-in |
|
|
24
24
|
|
|
25
|
+
### 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`.
|
|
27
|
+
|
|
25
28
|
### How do I install?
|
|
26
29
|
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
|
|
27
30
|
|
|
28
31
|
### Is it free?
|
|
29
|
-
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`).
|
|
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.
|
|
30
33
|
|
|
31
34
|
## Install
|
|
32
35
|
|
|
33
36
|
- npm: `npm i -g cursor-route`
|
|
34
37
|
- Health: `cursor-route health`
|
|
35
38
|
- Mid: `cursor-route start --lane mid "…"` (Flash) · `--model pro` when needed
|
|
39
|
+
- OpenCode (opt-in): `cursor-route start --worker opencode "…"`
|
|
36
40
|
- Skill: `/route-orch` (Cursor)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-route",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter easy lane are the parallel army \u2014 lane-aware /route orchestration in tmux.",
|
|
3
|
+
"version": "0.1.10",
|
|
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",
|
|
7
7
|
"bin": {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"deepseek",
|
|
42
42
|
"claude-ds",
|
|
43
43
|
"openrouter",
|
|
44
|
+
"opencode",
|
|
44
45
|
"tmux",
|
|
45
46
|
"orchestrator",
|
|
46
47
|
"agents",
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
name: route-orch
|
|
3
3
|
description: >-
|
|
4
4
|
Delegate coding work from Cursor to parallel Grok CLI / claude-ds (DeepSeek) /
|
|
5
|
-
OpenRouter (easy lane) workers via cursor-route. Use
|
|
6
|
-
/route-orch, spawn workers, parallel agents with
|
|
7
|
-
asks to outsource implementation to
|
|
8
|
-
Cemini /route.
|
|
5
|
+
OpenRouter (easy lane) / OpenCode (opt-in free) workers via cursor-route. Use
|
|
6
|
+
when the user says /route-orch, spawn workers, parallel agents with
|
|
7
|
+
cursor-route, or explicitly asks to outsource implementation to
|
|
8
|
+
Grok/DeepSeek/OpenCode panes — not for private Cemini /route.
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# route-orch (cursor-route)
|
|
@@ -14,11 +14,11 @@ You are the **orchestrator**. Do **not** implement bulk code in this Cursor sess
|
|
|
14
14
|
|
|
15
15
|
## When to activate
|
|
16
16
|
|
|
17
|
-
- User says `/route-orch`, `spawn workers`, or asks for parallel Grok/DeepSeek via **cursor-route**
|
|
18
|
-
- Mid/hard implementation that should run on a subscription worker (Grok CLI / claude-ds)
|
|
17
|
+
- User says `/route-orch`, `spawn workers`, or asks for parallel Grok/DeepSeek/OpenCode via **cursor-route**
|
|
18
|
+
- Mid/hard implementation that should run on a subscription worker (Grok CLI / claude-ds) or opt-in OpenCode free models
|
|
19
19
|
- Multi-file investigation that benefits from parallel panes
|
|
20
20
|
|
|
21
|
-
**Do not steal federation `/route`.** Private Cemini `/route` (route-task →
|
|
21
|
+
**Do not steal federation `/route`.** Private Cemini `/route` (route-task → verify → Grok/claude-ds chain) is a different skill. This public skill only drives the `cursor-route` CLI.
|
|
22
22
|
|
|
23
23
|
## Lanes (public core)
|
|
24
24
|
|
|
@@ -59,10 +59,24 @@ cursor-route start --worker deepseek --model pro --dir "$PWD" "…" # --model
|
|
|
59
59
|
|
|
60
60
|
Health ✓ needs `dsh` on PATH and `DEEPSEEK_API_KEY` set. The adapter pins `--model` via a per-job `--patch` (never touches `~/.dsh/settings.yaml`); always-approve → `DSH_PERMISSION_MODE=danger-full-access`, `--ask` → `workspace-write`. The key never enters the command or patch.
|
|
61
61
|
|
|
62
|
+
## Experimental: --worker opencode (free Zen)
|
|
63
|
+
|
|
64
|
+
OpenCode as an opt-in **coding agent** on OpenCode Zen free models — **not a lane default** (mid stays `claude-ds`; easy stays OpenRouter chat). Use this to cut Grok / DeepSeek usage on implement work.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm i -g opencode-ai
|
|
68
|
+
opencode auth login
|
|
69
|
+
cursor-route start --worker opencode --dir "$PWD" "…"
|
|
70
|
+
cursor-route start --worker opencode --model free --dir "$PWD" "…"
|
|
71
|
+
cursor-route start --worker opencode --model opencode/hy3-free --dir "$PWD" "…"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Health ✓ needs `opencode` on PATH (override `CURSOR_ROUTE_OPENCODE_BIN`). Default model is `opencode/big-pickle` (`CURSOR_ROUTE_OPENCODE_MODEL` / `--model free`). Always-approve → `opencode run --auto`; `--ask` omits `--auto`. Never rewrites `~/.config/opencode/opencode.json`. Free Zen models may log/train — keep secrets off this worker (same refuse gate as easy).
|
|
75
|
+
|
|
62
76
|
## Workflow
|
|
63
77
|
|
|
64
|
-
1. Run `cursor-route health` (or `CURSOR_ROUTE_RELAXED=1` for headless). If the **target worker** is unhealthy, fix before spawning.
|
|
65
|
-
2. Write a clear handoff prompt with **
|
|
78
|
+
1. Run `cursor-route health` (or `CURSOR_ROUTE_RELAXED=1` for headless). If the **target worker** is unhealthy, fix before spawning. If targeting **mid**, require `lane:mid` ✓ (or health JSON `lanes.mid.deepseek`) before spawn — `CURSOR_ROUTE_ALLOW_ANTHROPIC=1` is not DeepSeek proof.
|
|
79
|
+
2. Write a clear handoff prompt with **Success criteria** + **Verify** + **NEVER** (no secrets, no LIVE Discord).
|
|
66
80
|
3. Spawn:
|
|
67
81
|
|
|
68
82
|
```bash
|
|
@@ -70,24 +84,49 @@ cursor-route start --lane hard --dir "$PWD" "$(cat <<'EOF'
|
|
|
70
84
|
## Task
|
|
71
85
|
...
|
|
72
86
|
|
|
87
|
+
## Success criteria
|
|
88
|
+
- [ ] ...
|
|
89
|
+
|
|
73
90
|
## Verify
|
|
74
91
|
- [ ] ...
|
|
92
|
+
|
|
93
|
+
## NEVER
|
|
94
|
+
- ...
|
|
75
95
|
EOF
|
|
76
96
|
)"
|
|
77
97
|
```
|
|
78
98
|
|
|
79
|
-
Or `--worker grok` / `--worker claude-ds` / `--worker deepseek` (experimental) / `--worker openrouter` (or `--lane easy`). Use `--no-tmux` only when tmux is unavailable.
|
|
99
|
+
Or `--worker grok` / `--worker claude-ds` / `--worker deepseek` (experimental) / `--worker opencode` (opt-in free) / `--worker openrouter` (or `--lane easy`). Use `--no-tmux` only when tmux is unavailable.
|
|
80
100
|
|
|
81
101
|
4. Monitor: `cursor-route jobs --json` · `cursor-route capture <id>` · `cursor-route send <id> "…"` (tmux only).
|
|
82
|
-
5. Summarize worker results with **verify evidence** — no status-only “done
|
|
102
|
+
5. Summarize worker results with **verify evidence** — no status-only “done” (see Verify / claim closeout). If verify fails, reconsider the plan/definition (not only retry) — `send` a correction or spawn a follow-up; do not invent success.
|
|
103
|
+
|
|
104
|
+
## Verify / claim closeout
|
|
105
|
+
|
|
106
|
+
Verify criteria are an **external eval contract** (AutoDesign pattern), fixed by the parent — not a checklist the worker may rewrite:
|
|
107
|
+
|
|
108
|
+
- Workers must **not rewrite Success criteria / Verify** to claim done
|
|
109
|
+
- Parent closeout is an **evidence tree**: report **spawn** (job id, worker, lane, model) + **execute** (status, exit) + **verify** (`capture` excerpt / exit). A single “done” scalar is not enough.
|
|
110
|
+
- Parent closes a job only on **capture / exit evidence** (`cursor-route capture <id>`, job exit status)
|
|
111
|
+
- `cursor-route status --json` `.evidence.verify.claim` stays `"unverified"` until the parent reads capture
|
|
112
|
+
- **activity ≠ verification** — busy panes, many tool calls, or long transcripts do not make a claim true
|
|
113
|
+
|
|
114
|
+
## Eval & skill hygiene
|
|
115
|
+
|
|
116
|
+
- **External eval contract (AutoDesign):** do not rewrite Verify / Success criteria mid-run to make a failing job look green — capture + exit status are the contract (see Verify / claim closeout).
|
|
117
|
+
- **Skill misevolution:** do not auto-edit `route-orch` or promote skill variants from worker trajectories without operator HITL — write-time approval ≠ safe retrieval later.
|
|
118
|
+
- **On verify fail:** prefer reconsidering the plan/definition (wrong approach) over grinding the same tactic; attribute failure to stage when possible (spawn vs execute vs verify).
|
|
83
119
|
|
|
84
120
|
## Always-approve
|
|
85
121
|
|
|
86
|
-
Defaults on for workers. Opt out: `cursor-route start … --ask` or `CURSOR_ROUTE_ASK=1`.
|
|
122
|
+
Defaults on for workers. Opt out: `cursor-route start … --ask` or `CURSOR_ROUTE_ASK=1`. Always-approve is for **coding worktrees only** — it does not authorize LIVE Discord, trading, or irreversible SaaS.
|
|
87
123
|
|
|
88
124
|
## Anti-patterns
|
|
89
125
|
|
|
90
126
|
- Do not paste API keys / private keys into prompts or `send`
|
|
91
|
-
- Do not claim the official DeepSeek harness (`@deepseek-ai/dsh`) is the mid default — `--worker deepseek` is
|
|
92
|
-
- Do not
|
|
127
|
+
- Do not claim the official DeepSeek harness (`@deepseek-ai/dsh`) is the mid default — `--worker deepseek` is an opt-in experiment (cheap to abandon), not a product fork; mid stays **claude-ds**
|
|
128
|
+
- Do not claim OpenCode is the mid default — `--worker opencode` is opt-in for free Zen (or other) models; mid stays **claude-ds**
|
|
129
|
+
- Do not fork a second mid harness
|
|
130
|
+
- Do not open-source or dump private cemini `agent-toolkit` paths into public handoffs
|
|
93
131
|
- Do not mark done without reading `capture` / exit status
|
|
132
|
+
- When editing this skill itself, treat changes as **skill-evolution** — do not auto-promote harmful instructions; prefer **HITL** (no unattended promote from worker trajectories)
|
|
@@ -124,6 +124,25 @@ function resolveClaudeDs(): { binary: string; mode: string } | null {
|
|
|
124
124
|
return null;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Mid lane is proven DeepSeek only via shim (`claude-ds` / `deepseek-claude`)
|
|
129
|
+
* or stock `claude` routed to DeepSeek. The Anthropic escape hatch is not proof.
|
|
130
|
+
*/
|
|
131
|
+
export function isMidDeepSeekProven(): boolean {
|
|
132
|
+
const resolved = resolveClaudeDs();
|
|
133
|
+
if (!resolved) return false;
|
|
134
|
+
return !resolved.mode.startsWith("claude → Anthropic");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Human detail for health `lane:mid` (mode when proven; install hint otherwise). */
|
|
138
|
+
export function midDeepSeekProofDetail(): string {
|
|
139
|
+
const resolved = resolveClaudeDs();
|
|
140
|
+
if (!resolved || resolved.mode.startsWith("claude → Anthropic")) {
|
|
141
|
+
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.";
|
|
142
|
+
}
|
|
143
|
+
return `DeepSeek proven (${resolved.mode})`;
|
|
144
|
+
}
|
|
145
|
+
|
|
127
146
|
function pickModel(requested?: DsModelAlias, modelId?: string): { alias: DsModelAlias; id: string } {
|
|
128
147
|
if (modelId) {
|
|
129
148
|
const alias = requested ?? resolveDsModel(modelId).alias;
|
package/src/adapters/index.ts
CHANGED
|
@@ -4,12 +4,14 @@ import { grokAdapter } from "./grok.ts";
|
|
|
4
4
|
import { claudeDsAdapter } from "./claude-ds.ts";
|
|
5
5
|
import { openRouterAdapter } from "./openrouter.ts";
|
|
6
6
|
import { deepseekAdapter } from "./deepseek.ts";
|
|
7
|
+
import { opencodeAdapter } from "./opencode.ts";
|
|
7
8
|
|
|
8
9
|
const registry: Record<WorkerKind, Adapter> = {
|
|
9
10
|
grok: grokAdapter,
|
|
10
11
|
"claude-ds": claudeDsAdapter,
|
|
11
12
|
openrouter: openRouterAdapter,
|
|
12
13
|
deepseek: deepseekAdapter,
|
|
14
|
+
opencode: opencodeAdapter,
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
export function getAdapter(worker: WorkerKind): Adapter {
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, writeFileSync, chmodSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { opencodeAdapter } from "./opencode.ts";
|
|
6
|
+
import { startJob } from "../jobs.ts";
|
|
7
|
+
import {
|
|
8
|
+
openCodeModel,
|
|
9
|
+
assertOpenCodeModel,
|
|
10
|
+
OPENCODE_DEFAULT_MODEL,
|
|
11
|
+
} from "../config.ts";
|
|
12
|
+
|
|
13
|
+
describe("openCodeModel", () => {
|
|
14
|
+
test("defaults to OpenCode Zen Big Pickle", () => {
|
|
15
|
+
const prev = process.env.CURSOR_ROUTE_OPENCODE_MODEL;
|
|
16
|
+
delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
|
|
17
|
+
try {
|
|
18
|
+
expect(openCodeModel()).toBe("opencode/big-pickle");
|
|
19
|
+
expect(openCodeModel("")).toBe(OPENCODE_DEFAULT_MODEL);
|
|
20
|
+
expect(openCodeModel("free")).toBe("opencode/big-pickle");
|
|
21
|
+
} finally {
|
|
22
|
+
if (prev === undefined) delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
|
|
23
|
+
else process.env.CURSOR_ROUTE_OPENCODE_MODEL = prev;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("env override wins for empty/free", () => {
|
|
28
|
+
const prev = process.env.CURSOR_ROUTE_OPENCODE_MODEL;
|
|
29
|
+
process.env.CURSOR_ROUTE_OPENCODE_MODEL = "opencode/hy3-free";
|
|
30
|
+
try {
|
|
31
|
+
expect(openCodeModel()).toBe("opencode/hy3-free");
|
|
32
|
+
expect(openCodeModel("free")).toBe("opencode/hy3-free");
|
|
33
|
+
} finally {
|
|
34
|
+
if (prev === undefined) delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
|
|
35
|
+
else process.env.CURSOR_ROUTE_OPENCODE_MODEL = prev;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("explicit provider/model wins over env", () => {
|
|
40
|
+
const prev = process.env.CURSOR_ROUTE_OPENCODE_MODEL;
|
|
41
|
+
process.env.CURSOR_ROUTE_OPENCODE_MODEL = "opencode/hy3-free";
|
|
42
|
+
try {
|
|
43
|
+
expect(openCodeModel("opencode/mimo-v2.5-free")).toBe("opencode/mimo-v2.5-free");
|
|
44
|
+
} finally {
|
|
45
|
+
if (prev === undefined) delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
|
|
46
|
+
else process.env.CURSOR_ROUTE_OPENCODE_MODEL = prev;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("accepts OpenRouter-style extra slash and :free", () => {
|
|
51
|
+
expect(assertOpenCodeModel("openrouter/qwen/qwen3-coder:free")).toBe(
|
|
52
|
+
"openrouter/qwen/qwen3-coder:free",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("rejects injection / flash|pro aliases", () => {
|
|
57
|
+
expect(() => openCodeModel("flash")).toThrow(/provider\/model/);
|
|
58
|
+
expect(() => openCodeModel("x\n--evil")).toThrow(/provider\/model/);
|
|
59
|
+
expect(() => openCodeModel("opencode/big pickle")).toThrow(/provider\/model/);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("opencode adapter", () => {
|
|
64
|
+
const makeFake = (suffix: string) => {
|
|
65
|
+
const dir = join(tmpdir(), `cr-oc-${process.pid}-${suffix}`);
|
|
66
|
+
mkdirSync(dir, { recursive: true });
|
|
67
|
+
const bin = join(dir, "opencode");
|
|
68
|
+
writeFileSync(bin, "#!/bin/sh\necho fake-opencode\n");
|
|
69
|
+
chmodSync(bin, 0o755);
|
|
70
|
+
return { dir, bin };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const withEnv = (patch: Record<string, string | undefined>, fn: () => void) => {
|
|
74
|
+
const prev = new Map<string, string | undefined>();
|
|
75
|
+
for (const k of Object.keys(patch)) {
|
|
76
|
+
prev.set(k, process.env[k]);
|
|
77
|
+
const v = patch[k];
|
|
78
|
+
if (v === undefined) delete process.env[k];
|
|
79
|
+
else process.env[k] = v;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
fn();
|
|
83
|
+
} finally {
|
|
84
|
+
for (const [k, v] of prev) {
|
|
85
|
+
if (v === undefined) delete process.env[k];
|
|
86
|
+
else process.env[k] = v;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
test("health: no binary → not ok, install hint names npm package", () => {
|
|
92
|
+
withEnv(
|
|
93
|
+
{ CURSOR_ROUTE_OPENCODE_BIN: join(tmpdir(), `missing-oc-${process.pid}`) },
|
|
94
|
+
() => {
|
|
95
|
+
const h = opencodeAdapter.health();
|
|
96
|
+
expect(h.ok).toBe(false);
|
|
97
|
+
expect(h.detail).toMatch(/npm i -g opencode-ai/);
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("health: binary → ok (auth at first start)", () => {
|
|
103
|
+
const { bin } = makeFake("ok");
|
|
104
|
+
withEnv({ CURSOR_ROUTE_OPENCODE_BIN: bin }, () => {
|
|
105
|
+
const h = opencodeAdapter.health();
|
|
106
|
+
expect(h.ok).toBe(true);
|
|
107
|
+
expect(h.binary).toBe(bin);
|
|
108
|
+
expect(h.detail).toContain("opencode/big-pickle");
|
|
109
|
+
});
|
|
110
|
+
rmSync(join(tmpdir(), `cr-oc-${process.pid}-ok`), { recursive: true, force: true });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("buildLaunch: default free model + --auto; prompt via cat", () => {
|
|
114
|
+
const { dir, bin } = makeFake("launch");
|
|
115
|
+
const promptFile = join(dir, "job.prompt");
|
|
116
|
+
writeFileSync(promptFile, "ping");
|
|
117
|
+
withEnv(
|
|
118
|
+
{
|
|
119
|
+
CURSOR_ROUTE_OPENCODE_BIN: bin,
|
|
120
|
+
CURSOR_ROUTE_OPENCODE_MODEL: undefined,
|
|
121
|
+
CURSOR_ROUTE_ASK: undefined,
|
|
122
|
+
},
|
|
123
|
+
() => {
|
|
124
|
+
const plan = opencodeAdapter.buildLaunch({
|
|
125
|
+
promptFile,
|
|
126
|
+
cwd: dir,
|
|
127
|
+
alwaysApprove: true,
|
|
128
|
+
});
|
|
129
|
+
expect(plan.command).toContain("run");
|
|
130
|
+
expect(plan.command).toContain("--auto");
|
|
131
|
+
expect(plan.command).toContain("opencode/big-pickle");
|
|
132
|
+
expect(plan.command).toContain("$(cat");
|
|
133
|
+
expect(plan.command).toContain("--dir");
|
|
134
|
+
expect(plan.alwaysApprove).toBe(true);
|
|
135
|
+
expect(plan.command).not.toContain("npx");
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
rmSync(dir, { recursive: true, force: true });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("buildLaunch: alwaysApprove false (--ask) omits --auto", () => {
|
|
142
|
+
const { dir, bin } = makeFake("ask");
|
|
143
|
+
const promptFile = join(dir, "job.prompt");
|
|
144
|
+
writeFileSync(promptFile, "ping");
|
|
145
|
+
withEnv({ CURSOR_ROUTE_OPENCODE_BIN: bin }, () => {
|
|
146
|
+
const plan = opencodeAdapter.buildLaunch({
|
|
147
|
+
promptFile,
|
|
148
|
+
cwd: dir,
|
|
149
|
+
alwaysApprove: false,
|
|
150
|
+
modelId: "opencode/hy3-free",
|
|
151
|
+
});
|
|
152
|
+
expect(plan.command).toContain("opencode/hy3-free");
|
|
153
|
+
expect(plan.command).not.toContain("--auto");
|
|
154
|
+
expect(plan.alwaysApprove).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
rmSync(dir, { recursive: true, force: true });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("buildLaunch: CURSOR_ROUTE_ASK=1 opts out even when alwaysApprove true", () => {
|
|
160
|
+
const { dir, bin } = makeFake("ask-env");
|
|
161
|
+
const promptFile = join(dir, "job.prompt");
|
|
162
|
+
writeFileSync(promptFile, "ping");
|
|
163
|
+
withEnv({ CURSOR_ROUTE_OPENCODE_BIN: bin, CURSOR_ROUTE_ASK: "1" }, () => {
|
|
164
|
+
const plan = opencodeAdapter.buildLaunch({
|
|
165
|
+
promptFile,
|
|
166
|
+
cwd: dir,
|
|
167
|
+
alwaysApprove: true,
|
|
168
|
+
});
|
|
169
|
+
expect(plan.command).not.toContain("--auto");
|
|
170
|
+
expect(plan.alwaysApprove).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
rmSync(dir, { recursive: true, force: true });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("buildLaunch: missing binary falls back to plain `opencode` for dry-run", () => {
|
|
176
|
+
const { dir } = makeFake("nobin");
|
|
177
|
+
const promptFile = join(dir, "job.prompt");
|
|
178
|
+
writeFileSync(promptFile, "ping");
|
|
179
|
+
const empty = join(dir, "empty");
|
|
180
|
+
mkdirSync(empty, { recursive: true });
|
|
181
|
+
withEnv(
|
|
182
|
+
{
|
|
183
|
+
CURSOR_ROUTE_OPENCODE_BIN: undefined,
|
|
184
|
+
PATH: empty,
|
|
185
|
+
CURSOR_ROUTE_OPENCODE_MODEL: undefined,
|
|
186
|
+
},
|
|
187
|
+
() => {
|
|
188
|
+
const plan = opencodeAdapter.buildLaunch({
|
|
189
|
+
promptFile,
|
|
190
|
+
cwd: dir,
|
|
191
|
+
alwaysApprove: true,
|
|
192
|
+
dryRun: true,
|
|
193
|
+
});
|
|
194
|
+
expect(plan.command).toContain("'opencode' run");
|
|
195
|
+
expect(plan.command).toContain("--auto");
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
rmSync(dir, { recursive: true, force: true });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("--worker opencode dry-run succeeds with fake binary", () => {
|
|
202
|
+
const dir = join(tmpdir(), `cr-oc-start-${process.pid}`);
|
|
203
|
+
mkdirSync(dir, { recursive: true });
|
|
204
|
+
const bin = join(dir, "opencode");
|
|
205
|
+
writeFileSync(bin, "#!/bin/sh\necho fake-opencode\n");
|
|
206
|
+
chmodSync(bin, 0o755);
|
|
207
|
+
const prev = {
|
|
208
|
+
bin: process.env.CURSOR_ROUTE_OPENCODE_BIN,
|
|
209
|
+
model: process.env.CURSOR_ROUTE_OPENCODE_MODEL,
|
|
210
|
+
jobs: process.env.CURSOR_ROUTE_JOBS_DIR,
|
|
211
|
+
};
|
|
212
|
+
process.env.CURSOR_ROUTE_OPENCODE_BIN = bin;
|
|
213
|
+
delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
|
|
214
|
+
process.env.CURSOR_ROUTE_JOBS_DIR = join(dir, "jobs");
|
|
215
|
+
try {
|
|
216
|
+
const result = startJob({
|
|
217
|
+
prompt: "ping",
|
|
218
|
+
worker: "opencode",
|
|
219
|
+
dryRun: true,
|
|
220
|
+
});
|
|
221
|
+
expect(result.ok).toBe(true);
|
|
222
|
+
if (!result.ok) return;
|
|
223
|
+
expect(result.job.worker).toBe("opencode");
|
|
224
|
+
expect(result.job.model).toBe("opencode/big-pickle");
|
|
225
|
+
expect(result.command).toContain("run");
|
|
226
|
+
expect(result.command).toContain("--auto");
|
|
227
|
+
expect(result.command).toContain("$(cat");
|
|
228
|
+
} finally {
|
|
229
|
+
if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_OPENCODE_BIN;
|
|
230
|
+
else process.env.CURSOR_ROUTE_OPENCODE_BIN = prev.bin;
|
|
231
|
+
if (prev.model === undefined) delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
|
|
232
|
+
else process.env.CURSOR_ROUTE_OPENCODE_MODEL = prev.model;
|
|
233
|
+
if (prev.jobs === undefined) delete process.env.CURSOR_ROUTE_JOBS_DIR;
|
|
234
|
+
else process.env.CURSOR_ROUTE_JOBS_DIR = prev.jobs;
|
|
235
|
+
rmSync(dir, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|