@mushi-mushi/cli 0.35.7 → 0.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,7 +21,7 @@ npm install -g @mushi-mushi/cli # optional: a global `mushi` command
21
21
 
22
22
  The wizard detects your framework and package manager, checks your credentials, installs the right SDK, writes framework-prefixed env vars (`NEXT_PUBLIC_`, `VITE_`, `EXPO_PUBLIC_`…) to `.env.local`, prints the snippet to paste, and offers to send a test report. It never overwrites an existing env var or edits your code.
23
23
 
24
- **Credentials** come from the [console](https://kensaur.us/mushi-mushi/admin): the **project ID** is on the Projects page; the **API key** is under **Setup → Verify → Generate API key** (`report:write`), not the AI keys in Settings. No project yet? The wizard can open the console for you.
24
+ **Credentials** come from the [console](https://kensaur.us/mushi-mushi/admin): the **project ID** is on the **Manage apps** page; the **API key** is under **Set up → Test it → Generate API key** (`report:write`), not the AI keys in Settings. No project yet? The wizard can open the console for you.
25
25
 
26
26
  For your editor, `mushi setup` writes the hosted MCP entry by default: the editor opens a browser sign-in on first use and no key is stored on disk. Add `--stdio` for a local server instead.
27
27
 
@@ -32,7 +32,7 @@ For your editor, `mushi setup` writes the hosted MCP entry by default: the edito
32
32
  | Set up | `init`, `login`, `setup`, `connect`, `doctor`, `test` (send a test report), `upgrade`, `nudge`, `completion`, `profile` |
33
33
  | Bugs | `reports list/show/search/triage`, `lessons list/show`, `sentry import` |
34
34
  | Fixes | `fix <reportId>`, `fixes tail/merge/refresh-ci/review/verify`, `verify workflow` |
35
- | Local agent | `work` (a Cursor agent on your own computer fixes a report; you push), `worker install/status/revoke` (keeps it running, steered from the console) |
35
+ | Local agent | `work` (the Cursor CLI or Claude Code on your own computer fixes a report; `mushi work` pushes and opens the draft PR with your `git` and `gh`), `worker install/status/revoke` (keeps it running, steered from the console) |
36
36
  | Ship | `sourcemaps upload`, `deploy check`, `migrate`, `audit`, `releases` |
37
37
  | Test and UX | `qa stories/runs/run`, `tdd gen/pending/approve/improve`, `stories map`, `ux` (including `ux fresh-eyes`, the first-time user test) |
38
38
  | CI | `runner status/compare/bill`: where your tests run, what they cost, and whether a red check was your code or the machine |
@@ -45,7 +45,7 @@ For your editor, `mushi setup` writes the hosted MCP entry by default: the edito
45
45
  ```bash
46
46
  mushi doctor --fix # check SDK, ingest and dispatch; fix what's safe locally
47
47
  mushi reports list --status new --severity critical # what broke
48
- mushi fix <reportId> --wait # an agent opens a draft PR for it (your own AI key)
48
+ mushi fix <reportId> --wait # the built-in fixer opens a draft PR for it
49
49
  mushi fixes merge <fixId> # merge it and mark the report Fixed
50
50
  mushi upgrade # bump @mushi-mushi/* to the latest stable
51
51
  mushi upgrade --check # CI: exit 0 current, 1 outdated, 2 registry down
@@ -62,7 +62,7 @@ MUSHI_API_KEY=mushi_xxx mushi connect --project-id <uuid> --endpoint <url> --wai
62
62
 
63
63
  Other `init` flags: `--framework next`, `--cwd apps/web` (monorepos), `--endpoint <url>` (self-hosted), `--skip-install`, `--audit` (check an existing install instead).
64
64
 
65
- **What needs your own key.** Reading reports, diagnoses and fix prompts does not. `mushi fix` hands the report to an agent that runs on your own key: Anthropic for the built-in fixer (the default), or Cursor with `--agent cursor_cloud`. `mushi work` runs a Cursor agent on your own computer instead.
65
+ **What needs your own key.** Reading reports, diagnoses and fix prompts does not. `mushi fix` hands the report to the built-in fixer by default (`--agent claude_code`): it uses your own Anthropic or OpenAI key when you have added one, and on hosted Mushi Mushi it falls back to Mushi Mushi's key when you have not. `--agent cursor_cloud` needs your own Cursor key. `mushi work` runs the Cursor CLI or Claude Code (`--agent claude`, CLI 0.36.0 or later) on your own computer instead, on the account that tool is signed in to.
66
66
 
67
67
  ## Environment variables
68
68
 
@@ -0,0 +1,8 @@
1
+ // src/version.ts
2
+ var MUSHI_CLI_VERSION = true ? "0.36.1" : "0.0.0-dev";
3
+ var MUSHI_MCP_PIN_SPEC = "@mushi-mushi/mcp@0.30.7";
4
+
5
+ export {
6
+ MUSHI_CLI_VERSION,
7
+ MUSHI_MCP_PIN_SPEC
8
+ };
@@ -8,10 +8,10 @@ import {
8
8
  requireConfig,
9
9
  requireUuid,
10
10
  tryLoadWorkerKey
11
- } from "./chunk-PAL6GBXT.js";
11
+ } from "./chunk-TFNFJXCK.js";
12
12
  import {
13
13
  MUSHI_CLI_VERSION
14
- } from "./chunk-SC376RQS.js";
14
+ } from "./chunk-R4ZRRD4B.js";
15
15
 
16
16
  // src/worker/daemon.ts
17
17
  import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
@@ -302,13 +302,116 @@ function collectHeartbeat(io, mushiVersion) {
302
302
  };
303
303
  }
304
304
 
305
+ // src/commands/work-agents.ts
306
+ import { stripVTControlCharacters } from "util";
307
+ var LOCAL_AGENTS = ["cursor", "claude"];
308
+ function cursorSignInState(p) {
309
+ if (p.code === null && /\bENOENT\b/.test(p.stderr)) return "missing";
310
+ if (p.timedOut || p.code === null) return "unknown";
311
+ const out = stripVTControlCharacters(`${p.stdout}
312
+ ${p.stderr}`);
313
+ if (/\bnot\s+(?:logged|signed)\s+in\b|\blogged\s+out\b/i.test(out)) return "signed_out";
314
+ if (p.code === 0 && /\b(?:logged|signed)\s+in\b/i.test(out)) return "signed_in";
315
+ return "unknown";
316
+ }
317
+ function claudeSignInState(p) {
318
+ if (p.code === null && /\bENOENT\b/.test(p.stderr)) return "missing";
319
+ if (p.timedOut || p.code === null) return "unknown";
320
+ const out = stripVTControlCharacters(p.stdout).trim();
321
+ try {
322
+ const parsed = JSON.parse(out.slice(out.indexOf("{")));
323
+ if (parsed.loggedIn === true) return "signed_in";
324
+ if (parsed.loggedIn === false) return "signed_out";
325
+ } catch {
326
+ }
327
+ if (/\bnot\s+(?:logged|signed)\s+in\b/i.test(`${out}
328
+ ${p.stderr}`)) return "signed_out";
329
+ return "unknown";
330
+ }
331
+ var AGENTS = {
332
+ cursor: {
333
+ label: "Cursor",
334
+ branchTag: "cursor",
335
+ bin: "cursor-agent",
336
+ run: ({ mode, workspace, model, prompt }) => ({
337
+ cmd: "cursor-agent",
338
+ args: [
339
+ "-p",
340
+ ...mode === "ask" ? ["--mode", "ask"] : ["--force"],
341
+ "--trust",
342
+ "--output-format",
343
+ "text",
344
+ "--workspace",
345
+ workspace,
346
+ ...model ? ["--model", model] : [],
347
+ prompt
348
+ ]
349
+ }),
350
+ status: ["status"],
351
+ signInState: cursorSignInState,
352
+ install: "Install it: https://cursor.com/cli",
353
+ login: "Run: cursor-agent login",
354
+ models: "cursor-agent --list-models"
355
+ },
356
+ claude: {
357
+ label: "Claude Code",
358
+ branchTag: "claude",
359
+ bin: "claude",
360
+ // The worktree is the cwd (runProc's), which is Claude Code's workspace.
361
+ run: ({ mode, model, prompt }) => ({
362
+ cmd: "claude",
363
+ args: [
364
+ "-p",
365
+ "--output-format",
366
+ "text",
367
+ // ask: read-only tools, and anything that would prompt is refused.
368
+ ...mode === "ask" ? ["--permission-mode", "dontAsk", "--tools", "Read,Grep,Glob"] : ["--permission-mode", "bypassPermissions"],
369
+ "--strict-mcp-config",
370
+ "--setting-sources",
371
+ "project,local",
372
+ "--no-session-persistence",
373
+ ...model ? ["--model", model] : []
374
+ ],
375
+ input: prompt
376
+ }),
377
+ status: ["auth", "status"],
378
+ signInState: claudeSignInState,
379
+ install: "Install it: https://code.claude.com/docs/en/setup (the native installer puts claude on PATH)",
380
+ login: "Run: claude auth login",
381
+ models: "an alias such as opus, sonnet or haiku, or a full claude-\u2026 id"
382
+ }
383
+ };
384
+ function agentSpec(agent) {
385
+ return AGENTS[agent];
386
+ }
387
+ function agentRun(agent, o) {
388
+ return AGENTS[agent].run(o);
389
+ }
390
+ function parseLocalAgent(raw, where = "--agent") {
391
+ if (raw === void 0 || raw === null || raw === "") return "cursor";
392
+ if (typeof raw === "string" && LOCAL_AGENTS.includes(raw.toLowerCase())) return raw.toLowerCase();
393
+ throw new MushiCliError("E_INVALID_INPUT", `Unknown agent "${String(raw).slice(0, 40)}" (${where}).`, `Use one of: ${LOCAL_AGENTS.join(", ")}.`);
394
+ }
395
+ function isClaudeModel(id) {
396
+ if (/\.|-(?:low|medium|high|xhigh|max|thinking|fast)$/i.test(id)) return false;
397
+ return /^(?:claude-[a-z]|(?:opus|sonnet|haiku|fable)$)/i.test(id);
398
+ }
399
+ var AGENT_UNAVAILABLE = /ActionRequiredError|Increase limits|usage limit|out of (?:extra )?(?:usage|credits)|Cannot use this model|You've (?:hit|reached) your|You're out of usage|(?:spend|usage) limit reached|limit reached\s*[·∙•]\s*resets|credit balance (?:is )?too low|run \/login|Invalid API key|OAuth token (?:has )?(?:expired|refresh failed|unavailable)|issue with the selected model/i;
400
+ function agentUnavailable(run) {
401
+ if (run.code === 0 || run.timedOut) return false;
402
+ const text = stripVTControlCharacters(`${run.stdout}
403
+ ${run.stderr}`).trim();
404
+ return AGENT_UNAVAILABLE.test(text.length > 4e3 ? `${text.slice(0, 2e3)}
405
+ ${text.slice(-2e3)}` : text);
406
+ }
407
+
305
408
  // src/commands/work.ts
306
409
  import { spawn } from "child_process";
307
410
  import { createHash as createHash2 } from "crypto";
308
411
  import { existsSync, mkdirSync, rmSync } from "fs";
309
412
  import { homedir, tmpdir } from "os";
310
413
  import { basename, join as join2 } from "path";
311
- import { stripVTControlCharacters } from "util";
414
+ import { stripVTControlCharacters as stripVTControlCharacters2 } from "util";
312
415
  var WORK_AGENT_KIND = "mcp";
313
416
  var WORK_AGENT_TIMEOUT_MS = 30 * 6e4;
314
417
  var PROMPT_MAX_CHARS = 4e4;
@@ -342,14 +445,14 @@ function freeBranchName(base, onOrigin) {
342
445
  function branchNameFor(reportId, category, agent = "cursor") {
343
446
  const c = (category ?? "bug").toLowerCase();
344
447
  const prefix = c === "other" ? "chore" : ["feature", "bugfix", "hotfix", "refactor", "chore", "docs", "test", "ci"].includes(c) ? c : "bugfix";
345
- return `${prefix}/MUSHI-${reportId}-local-${agent}`;
448
+ return `${prefix}/MUSHI-${reportId}-local-${agentSpec(agent).branchTag}`;
346
449
  }
347
450
  function cap(text, max) {
348
451
  const t = (text ?? "").replace(/\s+/g, " ").trim();
349
452
  return t.length > max ? `${t.slice(0, max)} [cut]` : t;
350
453
  }
351
454
  function failureTail(r, max = 600) {
352
- const t = [r.stdout, r.stderr].map((x) => x.trim()).filter(Boolean).join("\n").replace(/[ \t]+/g, " ");
455
+ const t = stripVTControlCharacters2([r.stdout, r.stderr].map((x) => x.trim()).filter(Boolean).join("\n")).replace(/[ \t]+/g, " ");
353
456
  const lines = t.split("\n").map((l) => l.trim()).filter(Boolean);
354
457
  const kept = [];
355
458
  let size = 0;
@@ -361,7 +464,7 @@ function failureTail(r, max = 600) {
361
464
  return kept.length < lines.length ? `[\u2026] ${kept.join(" / ")}` : kept.join(" / ");
362
465
  }
363
466
  function hookFailureDetail(r, max = 900) {
364
- const text = stripVTControlCharacters([r.stdout, r.stderr].join("\n"));
467
+ const text = stripVTControlCharacters2([r.stdout, r.stderr].join("\n"));
365
468
  const failed = [...text.matchAll(/^\s*🥊\s+([\w:.-]+)/gmu)].map((m) => m[1]);
366
469
  if (!failed.length) return null;
367
470
  const lines = text.split("\n");
@@ -530,7 +633,11 @@ function agentEnv(base, emptyDir) {
530
633
  GIT_CONFIG_GLOBAL: join2(emptyDir, "gitconfig"),
531
634
  GIT_TERMINAL_PROMPT: "0",
532
635
  GIT_ASKPASS: "false",
533
- GIT_SSH_COMMAND: "false"
636
+ GIT_SSH_COMMAND: "false",
637
+ GIT_CONFIG_COUNT: "1",
638
+ GIT_CONFIG_KEY_0: "credential.helper",
639
+ GIT_CONFIG_VALUE_0: "",
640
+ GCM_INTERACTIVE: "never"
534
641
  };
535
642
  }
536
643
  var liveChildren = /* @__PURE__ */ new Set();
@@ -686,6 +793,14 @@ async function earlierFor(api, detail) {
686
793
  }
687
794
  return out;
688
795
  }
796
+ function agentUnavailablePayload(branch, agent, run, now = /* @__PURE__ */ new Date()) {
797
+ return {
798
+ status: "failed",
799
+ branch,
800
+ error: `${WORK_INFRA_PREFIX} the ${agentSpec(agent).label} agent could not run (usage or sign-in): ${failureTail(run, 300)}`.slice(0, 1e3),
801
+ completed_at: now.toISOString()
802
+ };
803
+ }
689
804
  async function workOne(api, detail, o) {
690
805
  const base = await defaultBranch(o.repoDir);
691
806
  await git(o.repoDir, ["fetch", "--quiet", "origin", base]);
@@ -744,14 +859,12 @@ async function workOne(api, detail, o) {
744
859
  await api(`/v1/admin/fixes/${fixId}`, { method: "PATCH", body: JSON.stringify(runningPayload(branch)) }).catch(
745
860
  (err) => o.log(` fix ${fixId}: not marked running (${err instanceof Error ? err.message : String(err)})`)
746
861
  );
747
- o.log(` agent: cursor-agent in ${wt} (up to ${WORK_AGENT_TIMEOUT_MS / 6e4} min); fix ${fixId} shows as running in the console`);
862
+ const spec = agentSpec(o.agent);
863
+ o.log(` agent: ${spec.bin} in ${wt} (up to ${WORK_AGENT_TIMEOUT_MS / 6e4} min); fix ${fixId} shows as running in the console`);
748
864
  stage = "agent";
749
865
  o.onStage?.(stage);
750
- const run = await runProc(
751
- "cursor-agent",
752
- ["-p", "--force", "--trust", "--output-format", "text", "--workspace", wt, ...o.model ? ["--model", o.model] : [], prompt],
753
- { cwd: wt, env: agentEnv(process.env, empty), timeoutMs: WORK_AGENT_TIMEOUT_MS, signal: o.signal }
754
- );
866
+ const inv = agentRun(o.agent, { mode: "edit", workspace: wt, model: o.model, prompt });
867
+ const run = await runProc(inv.cmd, inv.args, { cwd: wt, env: agentEnv(process.env, empty), timeoutMs: WORK_AGENT_TIMEOUT_MS, signal: o.signal, ...inv.input !== void 0 ? { input: inv.input } : {} });
755
868
  const stoppedBy = interruptedBy() ?? (o.signal?.aborted ? "lost lease" : null);
756
869
  if (stoppedBy) {
757
870
  keep = true;
@@ -760,8 +873,14 @@ async function workOne(api, detail, o) {
760
873
  }
761
874
  if (run.timedOut) o.log(" agent: timed out; keeping what it changed");
762
875
  const status = await git(wt, ["status", "--porcelain"]);
876
+ const spent = agentUnavailable(run) || !status && run.code === 0 && !parseVerdict(run.stdout).verdict && agentUnavailable({ ...run, code: 1 });
877
+ if (spent) {
878
+ keep = true;
879
+ await close(agentUnavailablePayload(branch, o.agent, run));
880
+ return { reportId: detail.id, result: "failed", failureKind: "infra", agentUnavailable: true, outcome: `failed (infra): the ${spec.label} agent could not run (usage or sign-in): ${failureTail(run, 300)}`, fixId };
881
+ }
763
882
  if (!status) {
764
- const noPr = run.timedOut ? { verdict: null, reason: `timed out after ${WORK_AGENT_TIMEOUT_MS / 6e4} min without a change; the report may need splitting into one page or cause each` } : run.code === 0 ? parseVerdict(run.stdout) : { verdict: null, reason: failureTail(run, 300) || `cursor-agent exited ${run.code}` };
883
+ const noPr = run.timedOut ? { verdict: null, reason: `timed out after ${WORK_AGENT_TIMEOUT_MS / 6e4} min without a change; the report may need splitting into one page or cause each` } : run.code === 0 ? parseVerdict(run.stdout) : { verdict: null, reason: failureTail(run, 300) || `${spec.bin} exited ${run.code}` };
765
884
  await close(submitPayload({ branch, prUrl: null, commitSha: null, filesChanged: [], linesChanged: 0, summary: "", noPr }));
766
885
  return { reportId: detail.id, result: run.timedOut ? "timeout" : noPr.verdict ? "verdict" : "no_change", outcome: `no change (${noPr.verdict ?? "no verdict"}): ${noPr.reason}`, fixId };
767
886
  }
@@ -774,7 +893,7 @@ async function workOne(api, detail, o) {
774
893
  o.onStage?.(stage);
775
894
  await git(wt, ["add", "-A"]);
776
895
  const last = run.stdout.trim().split("\n").filter((l) => l.trim()).slice(-6).join(" ");
777
- await commitWithHooks(wt, ["--quiet", "-m", prTitle(detail.summary, detail.id), "-m", commitBody(`Mushi report: ${detail.id}`, "Made by a local Cursor agent through `mushi work`.")], o.log);
896
+ await commitWithHooks(wt, ["--quiet", "-m", prTitle(detail.summary, detail.id), "-m", commitBody(`Mushi report: ${detail.id}`, `Made by a local ${spec.label} agent through \`mushi work\`.`)], o.log);
778
897
  const sha = await git(wt, ["rev-parse", "HEAD"]);
779
898
  const numstat = await git(wt, ["diff", "--numstat", `origin/${base}`, "HEAD"]);
780
899
  const files = [];
@@ -798,7 +917,7 @@ async function workOne(api, detail, o) {
798
917
 
799
918
  ${cap(last, 1500) || "See the commits."}
800
919
 
801
- Opened by \`mushi work\` with a local Cursor agent; Mushi's Opus review decides whether it merges.`;
920
+ Opened by \`mushi work\` with a local ${spec.label} agent; Mushi's Opus review decides whether it merges.`;
802
921
  const pr = await runProc("gh", ["pr", "create", "--draft", "--base", base, "--head", branch, "--title", prTitle(detail.summary, detail.id), "--body", body], { cwd: wt });
803
922
  const prUrl = pr.stdout.match(/https:\/\/github\.com\/\S+\/pull\/\d+/)?.[0] ?? null;
804
923
  if (!prUrl) throw new MushiCliError("E_INTERNAL", `gh pr create failed: ${failureTail(pr)}`);
@@ -851,27 +970,31 @@ async function followUpOne(api, f, o) {
851
970
  const empty = join2(tmpdir(), `mushi-work-${process.pid}`);
852
971
  mkdirSync(empty, { recursive: true });
853
972
  o.onStage?.("agent");
854
- o.log(` agent: cursor-agent on ${claim.branch}, round ${claim.round}, ${claim.findingsSent} finding(s)${claim.findingsDropped ? ` (${claim.findingsDropped} left out by the safety screen)` : ""}`);
855
- const run = await runProc(
856
- "cursor-agent",
857
- ["-p", "--force", "--trust", "--output-format", "text", "--workspace", wt, ...o.model ? ["--model", o.model] : [], claim.prompt],
858
- { cwd: wt, env: agentEnv(process.env, empty), timeoutMs: WORK_AGENT_TIMEOUT_MS }
859
- );
973
+ const spec = agentSpec(o.agent);
974
+ o.log(` agent: ${spec.bin} on ${claim.branch}, round ${claim.round}, ${claim.findingsSent} finding(s)${claim.findingsDropped ? ` (${claim.findingsDropped} left out by the safety screen)` : ""}`);
975
+ const inv = agentRun(o.agent, { mode: "edit", workspace: wt, model: o.model, prompt: claim.prompt });
976
+ const run = await runProc(inv.cmd, inv.args, { cwd: wt, env: agentEnv(process.env, empty), timeoutMs: WORK_AGENT_TIMEOUT_MS, ...inv.input !== void 0 ? { input: inv.input } : {} });
860
977
  const stoppedBy = interruptedBy();
861
978
  if (stoppedBy) {
862
979
  keep = true;
863
980
  return { fixId: f.fixId, outcome: `stopped by ${stoppedBy}; the change so far is kept in ${wt}` };
864
981
  }
982
+ if (agentUnavailable(run)) {
983
+ keep = true;
984
+ const why = `${WORK_INFRA_PREFIX} the ${spec.label} agent could not run (usage or sign-in): ${failureTail(run, 300)}`;
985
+ await done(null, why);
986
+ return { fixId: f.fixId, outcome: `failed: ${why}`, agentUnavailable: true };
987
+ }
865
988
  if (run.timedOut) o.log(" agent: timed out; keeping what it changed");
866
989
  const last = run.stdout.trim().split("\n").filter((l) => l.trim()).slice(-6).join(" ");
867
990
  if (!await git(wt, ["status", "--porcelain"])) {
868
991
  const why = run.code === 0 ? cap(last, 300) : failureTail(run, 300);
869
992
  await done(null, why);
870
- return { fixId: f.fixId, outcome: `no change: ${why || `cursor-agent exited ${run.code}`}` };
993
+ return { fixId: f.fixId, outcome: `no change: ${why || `${spec.bin} exited ${run.code}`}` };
871
994
  }
872
995
  o.onStage?.("commit");
873
996
  await git(wt, ["add", "-A"]);
874
- await commitWithHooks(wt, ["--quiet", "-m", `fix: address the Opus review (round ${claim.round})`, "-m", commitBody(cap(last, 1500) || "See the diff.", "Made by a local Cursor agent through `mushi work --followups`.")], o.log);
997
+ await commitWithHooks(wt, ["--quiet", "-m", `fix: address the Opus review (round ${claim.round})`, "-m", commitBody(cap(last, 1500) || "See the diff.", `Made by a local ${spec.label} agent through \`mushi work --followups\`.`)], o.log);
875
998
  const sha = await git(wt, ["rev-parse", "HEAD"]);
876
999
  o.onStage?.("push");
877
1000
  await pushWithHooks(wt, ["--quiet", "origin", `HEAD:refs/heads/${claim.branch}`], o.log);
@@ -897,10 +1020,15 @@ async function installDeps(wt, log) {
897
1020
  const cmd = installCommand((f) => existsSync(join2(wt, f)));
898
1021
  if (!cmd) return;
899
1022
  log(` deps: ${cmd[0]} ${cmd[1][0]} (so the agent can run tests and the commit hooks work)`);
900
- let r = await runProc(cmd[0], cmd[1], { cwd: wt, timeoutMs: 10 * 6e4 });
901
- if (r.code !== 0) r = await runProc(cmd[0], cmd[1], { cwd: wt, timeoutMs: 10 * 6e4 });
1023
+ const [bin, args] = windowsShim(cmd, process.platform);
1024
+ let r = await runProc(bin, args, { cwd: wt, timeoutMs: 10 * 6e4 });
1025
+ if (r.code !== 0) r = await runProc(bin, args, { cwd: wt, timeoutMs: 10 * 6e4 });
902
1026
  if (r.code !== 0) log(` deps: install failed twice, the agent works without them (${failureTail(r, 300)})`);
903
1027
  }
1028
+ function windowsShim(cmd, platform) {
1029
+ if (platform !== "win32") return cmd;
1030
+ return ["cmd.exe", ["/d", "/s", "/c", cmd[0], ...cmd[1]]];
1031
+ }
904
1032
  function isMissingModuleFailure(message) {
905
1033
  return /ERR_MODULE_NOT_FOUND|Cannot find (package|module)/.test(message);
906
1034
  }
@@ -927,41 +1055,37 @@ async function pushWithHooks(wt, args, log) {
927
1055
  log(" push: the hook passed the second time, so the first failure was this machine, not the change");
928
1056
  }
929
1057
  }
930
- function cursorSignInState(p) {
931
- if (p.code === null && /\bENOENT\b/.test(p.stderr)) return "missing";
932
- if (p.timedOut || p.code === null) return "unknown";
933
- const out = stripVTControlCharacters(`${p.stdout}
934
- ${p.stderr}`);
935
- if (/\bnot\s+(?:logged|signed)\s+in\b|\blogged\s+out\b/i.test(out)) return "signed_out";
936
- if (p.code === 0 && /\b(?:logged|signed)\s+in\b/i.test(out)) return "signed_in";
937
- return "unknown";
938
- }
939
1058
  var CURSOR_STATUS_TIMEOUT_MS = 9e4;
940
1059
  var CURSOR_STATUS_RETRY_DELAY_MS = 5e3;
941
- async function checkCursorSignIn(run = runProc, wait = (ms) => new Promise((r) => setTimeout(r, ms))) {
1060
+ async function checkAgentSignIn(agent, run = runProc, wait = (ms) => new Promise((r) => setTimeout(r, ms))) {
1061
+ const spec = agentSpec(agent);
942
1062
  let last = { state: "unknown", detail: "not checked", output: "" };
943
1063
  for (let attempt = 0; attempt < 2; attempt++) {
944
1064
  if (attempt > 0) await wait(CURSOR_STATUS_RETRY_DELAY_MS);
945
- const p = await run("cursor-agent", ["status"], { timeoutMs: CURSOR_STATUS_TIMEOUT_MS });
946
- const state = cursorSignInState(p);
947
- const detail = p.timedOut ? `timed out after ${CURSOR_STATUS_TIMEOUT_MS / 1e3} s` : stripVTControlCharacters(`${p.stdout}
1065
+ const p = await run(spec.bin, spec.status, { timeoutMs: CURSOR_STATUS_TIMEOUT_MS });
1066
+ const state = spec.signInState(p);
1067
+ const detail = p.timedOut ? `timed out after ${CURSOR_STATUS_TIMEOUT_MS / 1e3} s` : stripVTControlCharacters2(`${p.stdout}
948
1068
  ${p.stderr}`).trim().split("\n")[0]?.slice(0, 200) || `exit ${p.code}`;
949
- last = { state, detail, output: stripVTControlCharacters(`${p.stdout}
1069
+ last = { state, detail, output: stripVTControlCharacters2(`${p.stdout}
950
1070
  ${p.stderr}`).slice(0, 2e3) };
951
1071
  if (state === "signed_in" || state === "missing") break;
952
1072
  }
953
1073
  return last;
954
1074
  }
955
- async function preflight(repoDir, warn) {
956
- const signIn = await checkCursorSignIn();
1075
+ function checkCursorSignIn(run, wait) {
1076
+ return checkAgentSignIn("cursor", run, wait);
1077
+ }
1078
+ async function preflight(repoDir, warn, agent = "cursor") {
1079
+ const spec = agentSpec(agent);
1080
+ const signIn = await checkAgentSignIn(agent);
957
1081
  if (signIn.state === "missing") {
958
- throw new MushiCliError("E_FILE_NOT_FOUND", "The Cursor CLI (cursor-agent) is not installed or not on PATH.", "Install it: https://cursor.com/cli");
1082
+ throw new MushiCliError("E_FILE_NOT_FOUND", `${spec.label} (${spec.bin}) is not installed or not on PATH.`, spec.install);
959
1083
  }
960
1084
  if (signIn.state === "signed_out") {
961
- throw new MushiCliError("E_AUTH_MISSING", "The Cursor CLI is not signed in.", "Run: cursor-agent login");
1085
+ throw new MushiCliError("E_AUTH_MISSING", `${spec.label} (${spec.bin}) is not signed in.`, spec.login);
962
1086
  }
963
1087
  if (signIn.state === "unknown") {
964
- warn(`Could not confirm the Cursor CLI sign-in (${signIn.detail}); continuing. If it is signed out, the agent stops at once: run cursor-agent login.`);
1088
+ warn(`Could not confirm the ${spec.label} sign-in (${signIn.detail}); continuing. If it is signed out, the agent stops at once: ${spec.login.replace(/^Run: /, "run ")}.`);
965
1089
  }
966
1090
  const gh = await runProc("gh", ["auth", "status"], { timeoutMs: 3e4 });
967
1091
  if (gh.code !== 0) throw new MushiCliError("E_AUTH_MISSING", "gh is not signed in.", "Run: gh auth login");
@@ -978,15 +1102,19 @@ async function requireWriteScope(cfg) {
978
1102
  const problem = missingWriteScope(scopes, cfg.projectId);
979
1103
  if (problem) throw new MushiCliError("E_AUTH_MISSING", problem, `mushi login --upgrade-scope${cfg.projectId ? ` --project-id ${cfg.projectId}` : ""}`);
980
1104
  }
1105
+ function stopLine(agent, left, what) {
1106
+ const spec = agentSpec(agent);
1107
+ return `Stopped: the ${spec.label} agent could not run (out of usage or signed out). ${left} ${what}${left === 1 ? "" : "s"} left untouched and can be picked up again; the run was closed without a retry cooldown. Check: ${spec.bin} ${spec.status.join(" ")}`;
1108
+ }
981
1109
  function registerWorkCommands(program) {
982
- program.command("work").description("Fix reports with a coding agent on this machine (local Cursor CLI); pushes and opens the draft PR with your git and gh").option("--repo-dir <path>", "A clone of the app repo (only worktrees are created in it); not with --daemon").option("--project <id>", "Project id (overrides MUSHI_PROJECT_ID)").option("--agent <name>", "Local agent: cursor", "cursor").option("--model <id>", "Model for the agent (cursor-agent --list-models)").option("--max <n>", "Reports per run", "3").option("--report <id>", "Work on this report only").option("--followups", 'Instead of new reports: fix the Opus "fix first" findings on PRs mushi work opened, and push to them').option("--keep", "Keep the worktree after the run").option("--dry-run", "Show what would run; change nothing").option("--json", "Machine-readable JSON output").option("--daemon", "Run as a local worker: register this machine, send a heartbeat every 60 s, take the console's lane settings and claim reports (set up with `mushi worker install`)").option("--worker-config <path>", "With --daemon: the lanes file (default ~/.config/mushi/worker.json)").action(async (opts) => {
983
- if (opts.agent !== "cursor") throw new MushiCliError("E_INVALID_INPUT", `Unknown agent "${opts.agent}".`, "Only --agent cursor is supported.");
1110
+ program.command("work").description("Fix reports with a coding agent on this machine (the Cursor CLI or Claude Code); pushes and opens the draft PR with your git and gh").option("--repo-dir <path>", "A clone of the app repo (only worktrees are created in it); not with --daemon").option("--project <id>", "Project id (overrides MUSHI_PROJECT_ID)").option("--agent <name>", "Local agent: cursor (cursor-agent, default) or claude (Claude Code, claude -p); with --daemon, overrides the worker config's agent").option("--model <id>", "Model for the agent (cursor: cursor-agent --list-models; claude: opus, sonnet, haiku or a claude-\u2026 id)").option("--max <n>", "Reports per run", "3").option("--report <id>", "Work on this report only").option("--followups", 'Instead of new reports: fix the Opus "fix first" findings on PRs mushi work opened, and push to them').option("--keep", "Keep the worktree after the run").option("--dry-run", "Show what would run; change nothing").option("--json", "Machine-readable JSON output").option("--daemon", "Run as a local worker: register this machine, send a heartbeat every 60 s, take the console's lane settings and claim reports (set up with `mushi worker install`)").option("--worker-config <path>", "With --daemon: the lanes file (default ~/.config/mushi/worker.json)").action(async (opts) => {
1111
+ const agent = parseLocalAgent(opts.agent);
984
1112
  if (opts.daemon) {
985
1113
  if (opts.repoDir || opts.report || opts.followups) {
986
1114
  throw new MushiCliError("E_INVALID_INPUT", "--daemon takes its lanes and clones from the worker config, not from --repo-dir, --report or --followups.", "mushi worker install");
987
1115
  }
988
- const { runDaemon: runDaemon2 } = await import("./daemon-FN3VM4R6.js");
989
- await runDaemon2({ configPath: opts.workerConfig });
1116
+ const { runDaemon: runDaemon2 } = await import("./daemon-VC4WW5GU.js");
1117
+ await runDaemon2({ configPath: opts.workerConfig, ...opts.agent !== void 0 ? { agent } : {} });
990
1118
  return;
991
1119
  }
992
1120
  if (!opts.repoDir) throw new MushiCliError("E_INVALID_INPUT", "mushi work needs --repo-dir <clone>.", "mushi work --repo-dir ~/src/my-app");
@@ -1012,18 +1140,22 @@ function registerWorkCommands(program) {
1012
1140
  if (!due.length) log("No pull request is waiting for a follow-up.");
1013
1141
  return;
1014
1142
  }
1015
- await preflight(repoDir, warn);
1143
+ await preflight(repoDir, warn, agent);
1016
1144
  await requireWriteScope(cfg);
1017
1145
  const results2 = [];
1018
1146
  for (const f of due) {
1019
1147
  if (interruptedBy()) break;
1020
1148
  log(`follow-up ${f.prUrl ?? f.fixId}`);
1021
- const r = await followUpOne(api, f, { repoDir, agent: "cursor", model: opts.model, keep: opts.keep, log }).catch((err) => ({
1149
+ const r = await followUpOne(api, f, { repoDir, agent, model: opts.model, keep: opts.keep, log }).catch((err) => ({
1022
1150
  fixId: f.fixId,
1023
1151
  outcome: `failed: ${err instanceof Error ? err.message : String(err)}`
1024
1152
  }));
1025
1153
  log(` \u2192 ${r.outcome}`);
1026
1154
  results2.push(r);
1155
+ if (r.agentUnavailable) {
1156
+ warn(stopLine(agent, due.length - results2.length, "pull request"));
1157
+ break;
1158
+ }
1027
1159
  }
1028
1160
  if (results2.some((r) => r.outcome.startsWith("failed:"))) process.exitCode = 1;
1029
1161
  if (opts.json) process.stdout.write(`${JSON.stringify({ results: results2 }, null, 2)}
@@ -1032,20 +1164,20 @@ function registerWorkCommands(program) {
1032
1164
  }
1033
1165
  const picked = await pickReports(api, { max, reportId, projectId: cfg.projectId });
1034
1166
  if (opts.dryRun) {
1035
- const plan = picked.map((d) => ({ reportId: d.id, severity: d.severity ?? null, summary: d.summary, branch: branchNameFor(d.id, d.category) }));
1167
+ const plan = picked.map((d) => ({ reportId: d.id, severity: d.severity ?? null, summary: d.summary, branch: branchNameFor(d.id, d.category, agent) }));
1036
1168
  if (opts.json) process.stdout.write(`${JSON.stringify({ dryRun: true, plan }, null, 2)}
1037
1169
  `);
1038
1170
  else for (const p of plan) log(`would fix ${p.reportId} [${p.severity ?? "?"}] ${cap(p.summary, 80)} \u2192 ${p.branch}`);
1039
1171
  if (!plan.length) log("Nothing to work on.");
1040
1172
  return;
1041
1173
  }
1042
- await preflight(repoDir, warn);
1174
+ await preflight(repoDir, warn, agent);
1043
1175
  await requireWriteScope(cfg);
1044
1176
  const results = [];
1045
1177
  for (const d of picked) {
1046
1178
  if (interruptedBy()) break;
1047
1179
  log(`report ${d.id}: ${cap(d.summary, 80)}`);
1048
- const r = await workOne(api, d, { repoDir, agent: "cursor", model: opts.model, keep: opts.keep, log }).catch((err) => ({
1180
+ const r = await workOne(api, d, { repoDir, agent, model: opts.model, keep: opts.keep, log }).catch((err) => ({
1049
1181
  reportId: d.id,
1050
1182
  result: "failed",
1051
1183
  failureKind: "infra",
@@ -1053,6 +1185,10 @@ function registerWorkCommands(program) {
1053
1185
  }));
1054
1186
  log(` \u2192 ${r.outcome}${r.prUrl ? ` ${r.prUrl}` : ""}`);
1055
1187
  results.push(r);
1188
+ if (r.agentUnavailable) {
1189
+ warn(stopLine(agent, picked.length - results.length, "report"));
1190
+ break;
1191
+ }
1056
1192
  }
1057
1193
  if (results.some((r) => r.result === "failed")) process.exitCode = 1;
1058
1194
  if (opts.json) process.stdout.write(`${JSON.stringify({ results }, null, 2)}
@@ -1283,12 +1419,17 @@ async function auditPass(api, claimId, o) {
1283
1419
  return "error";
1284
1420
  }
1285
1421
  };
1286
- const ask = async (model, prompt) => runProc("cursor-agent", ["-p", "--mode", "ask", "--trust", "--output-format", "text", "--workspace", wt, ...model ? ["--model", model] : [], prompt], {
1287
- cwd: wt,
1288
- env: agentEnv(process.env, empty),
1289
- timeoutMs: ASK_TIMEOUT_MS,
1290
- signal: o.signal
1291
- });
1422
+ const agent = o.agent ?? "cursor";
1423
+ const ask = async (model, prompt) => {
1424
+ const inv = agentRun(agent, { mode: "ask", workspace: wt, model, prompt });
1425
+ return runProc(inv.cmd, inv.args, {
1426
+ cwd: wt,
1427
+ env: agentEnv(process.env, empty),
1428
+ timeoutMs: ASK_TIMEOUT_MS,
1429
+ signal: o.signal,
1430
+ ...inv.input !== void 0 ? { input: inv.input } : {}
1431
+ });
1432
+ };
1292
1433
  let filed = 0;
1293
1434
  let held = 0;
1294
1435
  try {
@@ -1315,7 +1456,7 @@ async function auditPass(api, claimId, o) {
1315
1456
  const proposed = await ask(o.model, proposePrompt(screen, state.pass));
1316
1457
  if (o.signal?.aborted) return { result: "stopped", detail: `stopped (lost the lease) while reading ${screen}` };
1317
1458
  if (proposed.timedOut) return { result: "timeout", failureKind: "code", detail: `the model did not answer about ${screen} within ${ASK_TIMEOUT_MS / 6e4} min` };
1318
- if (proposed.code !== 0) return { result: "failed", failureKind: "infra", detail: `cursor-agent exited ${proposed.code}: ${failureTail(proposed, 200)}` };
1459
+ if (proposed.code !== 0) return { result: "failed", failureKind: "infra", detail: `${agentSpec(agent).bin} exited ${proposed.code}: ${failureTail(proposed, 200)}` };
1319
1460
  const proposals = parseProposals(proposed.stdout);
1320
1461
  let reproduced = 0;
1321
1462
  let vetoes = 0;
@@ -1497,17 +1638,15 @@ async function resolveConflict(t, o) {
1497
1638
  o.log(` conflicts in ${files.length} file(s): ${files.slice(0, 6).join(", ")}${files.length > 6 ? ", \u2026" : ""}`);
1498
1639
  const empty = join4(tmpdir3(), `mushi-conflict-${process.pid}`);
1499
1640
  mkdirSync3(empty, { recursive: true });
1500
- const run = await runProc(
1501
- "cursor-agent",
1502
- ["-p", "--force", "--trust", "--output-format", "text", "--workspace", wt, ...o.model ? ["--model", o.model] : [], conflictPromptLocal({ repo: t.repo, prNumber: t.prNumber, base, files })],
1503
- { cwd: wt, env: agentEnv(process.env, empty), timeoutMs: CONFLICT_AGENT_TIMEOUT_MS, signal: o.signal }
1504
- );
1641
+ const agent = o.agent ?? "cursor";
1642
+ const inv = agentRun(agent, { mode: "edit", workspace: wt, model: o.model, prompt: conflictPromptLocal({ repo: t.repo, prNumber: t.prNumber, base, files }) });
1643
+ const run = await runProc(inv.cmd, inv.args, { cwd: wt, env: agentEnv(process.env, empty), timeoutMs: CONFLICT_AGENT_TIMEOUT_MS, signal: o.signal, ...inv.input !== void 0 ? { input: inv.input } : {} });
1505
1644
  if (o.signal?.aborted) {
1506
1645
  keep = true;
1507
1646
  return { result: "stopped", detail: `stopped (lost the lease); the worktree is kept in ${wt}` };
1508
1647
  }
1509
1648
  if (run.timedOut) return { result: "timeout", failureKind: "code", detail: `the agent did not finish within ${CONFLICT_AGENT_TIMEOUT_MS / 6e4} min` };
1510
- if (run.code !== 0) return { result: "failed", failureKind: classifyFailure(failureTail(run, 300), "agent"), detail: `cursor-agent exited ${run.code}: ${failureTail(run, 200)}` };
1649
+ if (run.code !== 0) return { result: "failed", failureKind: classifyFailure(failureTail(run, 300), "agent"), detail: `${agentSpec(agent).bin} exited ${run.code}: ${failureTail(run, 200)}` };
1511
1650
  agentNote = run.stdout.trim().split("\n").filter((l) => l.trim()).slice(-3).join(" ").slice(0, 300) || "the agent resolved the conflicts";
1512
1651
  if ((await mergeHead()).code !== 0) return { result: "refused", detail: "the merge is no longer in progress (the agent committed, aborted or reset it)" };
1513
1652
  if (await git(wt, ["rev-parse", "HEAD"]) !== t.headSha) return { result: "refused", detail: "HEAD moved during the agent run" };
@@ -1643,7 +1782,7 @@ async function runFreshEyesClaim(t, opts) {
1643
1782
 
1644
1783
  // src/worker/cursor-login.ts
1645
1784
  import { spawn as spawn3 } from "child_process";
1646
- import { stripVTControlCharacters as stripVTControlCharacters2 } from "util";
1785
+ import { stripVTControlCharacters as stripVTControlCharacters3 } from "util";
1647
1786
 
1648
1787
  // src/radar/secret-patterns.ts
1649
1788
  var SECRET_PATTERNS = [
@@ -1759,7 +1898,7 @@ var POLL_MS = 5e3;
1759
1898
  var FINAL = /* @__PURE__ */ new Set(["signed_in", "wrong_account", "failed", "expired"]);
1760
1899
  var LINK_RE = /https:\/\/(?:www\.)?cursor\.com\/[A-Za-z0-9/._~%?&=:+-]+/;
1761
1900
  function findLoginLink(text) {
1762
- return stripVTControlCharacters2(text).match(LINK_RE)?.[0] ?? null;
1901
+ return stripVTControlCharacters3(text).match(LINK_RE)?.[0] ?? null;
1763
1902
  }
1764
1903
  function spawnCursorLogin() {
1765
1904
  const child = spawn3("cursor-agent", ["login"], { env: { ...process.env, NO_OPEN_BROWSER: "1" }, stdio: ["ignore", "pipe", "pipe"] });
@@ -1804,7 +1943,7 @@ var realDeps = () => ({
1804
1943
  sleep: (ms) => new Promise((r) => setTimeout(r, ms))
1805
1944
  });
1806
1945
  function signedInAccount(p) {
1807
- const out = stripVTControlCharacters2(`${p.stdout}
1946
+ const out = stripVTControlCharacters3(`${p.stdout}
1808
1947
  ${p.stderr}`);
1809
1948
  return out.match(/\b(?:logged|signed)\s+in\s+as\s+([^\s]+@[^\s]+)/i)?.[1]?.replace(/[.,;)]+$/, "").toLowerCase() ?? null;
1810
1949
  }
@@ -1873,7 +2012,7 @@ function createCursorLogin(partial) {
1873
2012
  d.log(`worker: Cursor signed in as ${who ? maskEmail(who) : "another account"}, not ${maskEmail(account)}; lanes take nothing.`);
1874
2013
  return set("wrong_account", { detail: `Signed in as ${who ? maskEmail(who) : "another account"}, not ${maskEmail(account)}. Start again and approve with the right account.` });
1875
2014
  }
1876
- const said = stripVTControlCharacters2(`${st.stdout}
2015
+ const said = stripVTControlCharacters3(`${st.stdout}
1877
2016
  ${st.stderr}`).trim().split("\n")[0] ?? "";
1878
2017
  return set("failed", { detail: `After the sign-in, cursor-agent status did not confirm an account${said ? `: "${said.slice(0, 120)}"` : ""}.` });
1879
2018
  }
@@ -2082,6 +2221,7 @@ function validateWorkerConfig(raw) {
2082
2221
  if (c.version !== 1) fail("version must be 1");
2083
2222
  if (c.endpoint !== void 0 && (typeof c.endpoint !== "string" || !/^https?:\/\//.test(c.endpoint))) fail("endpoint must be an http(s) URL");
2084
2223
  if (c.cursorAccount !== void 0 && (typeof c.cursorAccount !== "string" || !/^[^\s@]{1,100}@[^\s@]{1,100}$/.test(c.cursorAccount))) fail("cursorAccount must be the account's email address");
2224
+ if (c.agent !== void 0 && (typeof c.agent !== "string" || !LOCAL_AGENTS.includes(c.agent))) fail(`agent must be one of ${LOCAL_AGENTS.join(", ")}`);
2085
2225
  if (!Array.isArray(c.lanes) || c.lanes.length === 0) fail("list at least one lane");
2086
2226
  if (c.lanes.length > 20) fail("at most 20 lanes");
2087
2227
  const names = /* @__PURE__ */ new Set();
@@ -2110,6 +2250,7 @@ function validateWorkerConfig(raw) {
2110
2250
  version: 1,
2111
2251
  ...typeof c.endpoint === "string" ? { endpoint: c.endpoint } : {},
2112
2252
  ...typeof c.cursorAccount === "string" ? { cursorAccount: c.cursorAccount } : {},
2253
+ ...typeof c.agent === "string" ? { agent: c.agent } : {},
2113
2254
  lanes
2114
2255
  };
2115
2256
  }
@@ -2130,7 +2271,7 @@ var IDLE_MS = 15 * 6e4;
2130
2271
  var BACKOFF_MS = 2 * 6e4;
2131
2272
  var INFRA_BACKOFF_MS = 6e4;
2132
2273
  var INFRA_BACKOFF_MAX_MS = 30 * 6e4;
2133
- var AGENT_UNAVAILABLE = /ActionRequiredError|Increase limits|usage limit|out of (?:usage|credits)|Cannot use this model/i;
2274
+ var AGENT_UNAVAILABLE_WAIT_MS = 30 * 6e4;
2134
2275
  var GH_CHECK_MS = 5 * 6e4;
2135
2276
  var CURSOR_CHECK_MS = 20 * 6e4;
2136
2277
  function infraBackoffMs(streak) {
@@ -2139,6 +2280,10 @@ function infraBackoffMs(streak) {
2139
2280
  function machineFailed(result, failureKind, detail) {
2140
2281
  return result === "failed" && failureKind === "infra" || AGENT_UNAVAILABLE.test(detail);
2141
2282
  }
2283
+ function modelFor(agent, model) {
2284
+ if (!model) return void 0;
2285
+ return agent === "claude" && !isClaudeModel(model) ? void 0 : model;
2286
+ }
2142
2287
  var KEY_REFUSALS_TO_STOP = 3;
2143
2288
  function createDaemon(cfg, deps) {
2144
2289
  const lanes = cfg.lanes.map((local) => ({
@@ -2165,6 +2310,12 @@ function createDaemon(cfg, deps) {
2165
2310
  let lastReadings = null;
2166
2311
  let stopped = null;
2167
2312
  let account = deps.cursorAccount;
2313
+ const agent = deps.agent ?? "cursor";
2314
+ const model = (m) => {
2315
+ const use = modelFor(agent, m);
2316
+ if (m && !use) once(`model:${m}`, null, `the lane model ${m} is not a ${agentSpec(agent).label} model; using ${agentSpec(agent).label}'s default`);
2317
+ return use;
2318
+ };
2168
2319
  const reported = /* @__PURE__ */ new Set();
2169
2320
  const pending = /* @__PURE__ */ new Set();
2170
2321
  const agents = () => lanes.reduce((n, l) => n + l.running.size + (l.followup ? 1 : 0), 0);
@@ -2246,7 +2397,15 @@ function createDaemon(cfg, deps) {
2246
2397
  claims: lanes.flatMap((l) => [...l.running.keys()])
2247
2398
  };
2248
2399
  }
2249
- function afterRun(lane, failedOnMachine) {
2400
+ function afterRun(lane, failedOnMachine, agentDown = false) {
2401
+ if (agentDown) {
2402
+ const until = deps.now() + AGENT_UNAVAILABLE_WAIT_MS;
2403
+ for (const l of lanes) {
2404
+ l.nextClaimAt = Math.max(l.nextClaimAt, until);
2405
+ l.nextFollowupAt = Math.max(l.nextFollowupAt, until);
2406
+ }
2407
+ say(lane, ` the ${agentSpec(agent).label} agent could not run (out of usage or signed out): no lane claims for ${AGENT_UNAVAILABLE_WAIT_MS / 6e4} min`);
2408
+ }
2250
2409
  if (!failedOnMachine) {
2251
2410
  lane.infraStreak = 0;
2252
2411
  return;
@@ -2262,8 +2421,8 @@ function createDaemon(cfg, deps) {
2262
2421
  lane.running.set(claim.id, ac);
2263
2422
  lane.state = "working";
2264
2423
  const t0 = deps.now();
2265
- const model = claim.model ?? lane.config?.model ?? void 0;
2266
- const track = newRun(claim.id, "fix", { reportId: claim.report.id, model, now: t0, stage: "prepare" });
2424
+ const runModel = model(claim.model ?? lane.config?.model);
2425
+ const track = newRun(claim.id, "fix", { reportId: claim.report.id, model: runModel, now: t0, stage: "prepare" });
2267
2426
  lane.tracks.set(claim.id, track);
2268
2427
  say(lane, `claimed ${claim.report.id.slice(0, 8)}: ${(claim.report.summary ?? "").slice(0, 80)}`);
2269
2428
  const run = (async () => {
@@ -2271,8 +2430,8 @@ function createDaemon(cfg, deps) {
2271
2430
  try {
2272
2431
  out = await deps.workOne(api, claim.report, {
2273
2432
  repoDir: lane.local.repoDir,
2274
- agent: "cursor",
2275
- model,
2433
+ agent,
2434
+ model: runModel,
2276
2435
  log: (l) => {
2277
2436
  pushLog(track, l);
2278
2437
  say(lane, l.trim());
@@ -2292,7 +2451,7 @@ function createDaemon(cfg, deps) {
2292
2451
  if (out.result === "failed") lane.counters.errors++;
2293
2452
  lane.last = `${claim.report.id.slice(0, 8)} \u2192 ${out.outcome}`.slice(0, 160);
2294
2453
  say(lane, ` \u2192 ${out.outcome}${out.prUrl ? ` ${out.prUrl}` : ""}`);
2295
- afterRun(lane, machineFailed(out.result, out.failureKind, out.outcome));
2454
+ afterRun(lane, machineFailed(out.result, out.failureKind, out.outcome), out.agentUnavailable === true);
2296
2455
  const done = await deps.call(`/v1/worker/claims/${claim.id}/done`, {
2297
2456
  method: "POST",
2298
2457
  body: JSON.stringify({
@@ -2339,7 +2498,7 @@ function createDaemon(cfg, deps) {
2339
2498
  if (out.result === "failed") lane.counters.errors++;
2340
2499
  lane.last = `${out.result}: ${out.detail}`.slice(0, 160);
2341
2500
  say(lane, ` \u2192 ${out.result}: ${out.detail}`);
2342
- afterRun(lane, machineFailed(out.result, out.failureKind, out.detail));
2501
+ afterRun(lane, machineFailed(out.result, out.failureKind, out.detail), out.result === "failed" && AGENT_UNAVAILABLE.test(out.detail));
2343
2502
  const done = await deps.call(`/v1/worker/claims/${claimId}/done`, {
2344
2503
  method: "POST",
2345
2504
  body: JSON.stringify({
@@ -2373,8 +2532,8 @@ function createDaemon(cfg, deps) {
2373
2532
  say(lane, `follow-up ${f.prUrl ?? f.fixId}`);
2374
2533
  const run = deps.followUpOne(api, f, {
2375
2534
  repoDir: lane.local.repoDir,
2376
- agent: "cursor",
2377
- model: lane.config?.model ?? void 0,
2535
+ agent,
2536
+ model: model(lane.config?.model),
2378
2537
  log: (l) => {
2379
2538
  pushLog(track, l);
2380
2539
  say(lane, l.trim());
@@ -2386,6 +2545,7 @@ function createDaemon(cfg, deps) {
2386
2545
  if (/^failed/.test(r.outcome)) lane.counters.errors++;
2387
2546
  lane.last = `follow-up \u2192 ${r.outcome}`.slice(0, 160);
2388
2547
  say(lane, ` \u2192 ${r.outcome}`);
2548
+ if (r.agentUnavailable) afterRun(lane, true, true);
2389
2549
  }).catch((err) => {
2390
2550
  lane.counters.errors++;
2391
2551
  say(lane, ` follow-up error: ${err instanceof Error ? err.message : String(err)}`);
@@ -2467,7 +2627,7 @@ function createDaemon(cfg, deps) {
2467
2627
  return;
2468
2628
  }
2469
2629
  const k = parsed.value;
2470
- const model = k.model ?? lane.config?.model ?? void 0;
2630
+ const jobModel = model(k.model ?? lane.config?.model);
2471
2631
  if (k.kind === "fix" && (c.mode === "fix" || c.mode === "skill")) {
2472
2632
  startRun(lane, k.raw);
2473
2633
  } else if (k.kind === "conflict" && c.mode === "conflicts") {
@@ -2477,7 +2637,7 @@ function createDaemon(cfg, deps) {
2477
2637
  k.id,
2478
2638
  "conflict",
2479
2639
  `conflict ${t.repo}#${t.prNumber} (${t.headRef} at ${t.headSha.slice(0, 7)})`,
2480
- (signal, log) => (deps.resolveConflict ?? resolveConflict)(t, { repoDir: lane.local.repoDir, model, log, signal })
2640
+ (signal, log) => (deps.resolveConflict ?? resolveConflict)(t, { repoDir: lane.local.repoDir, agent, model: jobModel, log, signal })
2481
2641
  );
2482
2642
  } else if (k.kind === "audit" && c.mode === "audit") {
2483
2643
  startJob(
@@ -2485,7 +2645,7 @@ function createDaemon(cfg, deps) {
2485
2645
  k.id,
2486
2646
  "audit",
2487
2647
  `audit pass (${k.audit.findingsLeft} finding(s) left this hour)`,
2488
- (signal, log) => (deps.auditPass ?? auditPass)(api, k.id, { repoDir: lane.local.repoDir, laneName: lane.local.name, model, reviewModel: k.audit.reviewModel, log, signal })
2648
+ (signal, log) => (deps.auditPass ?? auditPass)(api, k.id, { repoDir: lane.local.repoDir, laneName: lane.local.name, agent, model: jobModel, reviewModel: model(k.audit.reviewModel) ?? null, log, signal })
2489
2649
  );
2490
2650
  } else if (k.kind === "fresh_eyes" && deps.freshEyes) {
2491
2651
  const run = deps.freshEyes;
@@ -2706,6 +2866,8 @@ function createHostReadings(o) {
2706
2866
  }
2707
2867
  async function runDaemon(opts = {}) {
2708
2868
  const cfg = loadWorkerConfig(opts.configPath);
2869
+ const agent = opts.agent ?? cfg.agent ?? "cursor";
2870
+ const cursorAccount = agent === "cursor" ? cfg.cursorAccount : void 0;
2709
2871
  const cli = loadConfig();
2710
2872
  const key = readWorkerKey(cli.apiKey);
2711
2873
  const endpoint = cfg.endpoint ?? process.env["MUSHI_API_ENDPOINT"] ?? cli.endpoint;
@@ -2731,8 +2893,14 @@ async function runDaemon(opts = {}) {
2731
2893
  }
2732
2894
  const conf = { apiKey: key, endpoint };
2733
2895
  let wake = () => void 0;
2734
- const readings = createHostReadings({ cursorAccount: cfg.cursorAccount, now: () => Date.now(), log: (l) => process.stdout.write(`${l}
2735
- `) });
2896
+ const readings = createHostReadings({
2897
+ ...cursorAccount ? { cursorAccount } : {},
2898
+ // The account slot of the readings (and the heartbeat's `cursor` field) holds whichever agent runs.
2899
+ ...agent === "cursor" ? {} : { checkCursor: () => checkAgentSignIn(agent) },
2900
+ now: () => Date.now(),
2901
+ log: (l) => process.stdout.write(`${l}
2902
+ `)
2903
+ });
2736
2904
  const live = { agents: () => 0 };
2737
2905
  const cursorLogin = createCursorLogin({
2738
2906
  now: () => Date.now(),
@@ -2751,9 +2919,9 @@ async function runDaemon(opts = {}) {
2751
2919
  readings: () => readings.read(),
2752
2920
  recheck: () => readings.recheck(),
2753
2921
  notReady,
2754
- ...cfg.cursorAccount ? { cursorAccount: cfg.cursorAccount } : {},
2755
- setCursorAccount: (a) => readings.setAccount(a),
2756
- cursorLogin,
2922
+ ...cursorAccount ? { cursorAccount } : {},
2923
+ agent,
2924
+ ...agent === "cursor" ? { setCursorAccount: (a) => readings.setAccount(a), cursorLogin } : {},
2757
2925
  now: () => Date.now(),
2758
2926
  log: (l) => process.stdout.write(`${l}
2759
2927
  `),
@@ -2764,7 +2932,7 @@ async function runDaemon(opts = {}) {
2764
2932
  });
2765
2933
  live.agents = daemon.agents;
2766
2934
  onInterrupt((signal) => daemon.shutdown(signal));
2767
- process.stdout.write(`mushi worker: ${cfg.lanes.length} lane(s) on ${platform}; heartbeat every ${HEARTBEAT_MS / 1e3} s. Stop with Ctrl-C or systemctl --user stop mushi-worker.
2935
+ process.stdout.write(`mushi worker: ${cfg.lanes.length} lane(s) on ${platform}, agent ${agentSpec(agent).label} (${agentSpec(agent).bin}); heartbeat every ${HEARTBEAT_MS / 1e3} s. Stop with Ctrl-C or systemctl --user stop mushi-worker.
2768
2936
  `);
2769
2937
  while (!daemon.stopped) {
2770
2938
  await daemon.tick().catch((err) => process.stdout.write(`worker: tick failed: ${err instanceof Error ? err.message : String(err)}
@@ -2794,6 +2962,7 @@ export {
2794
2962
  hashHostId,
2795
2963
  UnsupportedHostError,
2796
2964
  collectHeartbeat,
2965
+ parseLocalAgent,
2797
2966
  MODEL_RE,
2798
2967
  LANE_NAME_RE,
2799
2968
  WORKER_CONFIG_PATH,
@@ -2802,6 +2971,7 @@ export {
2802
2971
  loadWorkerConfig,
2803
2972
  infraBackoffMs,
2804
2973
  machineFailed,
2974
+ modelFor,
2805
2975
  createDaemon,
2806
2976
  workerHostId,
2807
2977
  readWorkerKey,
@@ -9,10 +9,10 @@ import {
9
9
  requireUuid,
10
10
  saveConfig,
11
11
  savedSdkKeyFor
12
- } from "./chunk-PAL6GBXT.js";
12
+ } from "./chunk-TFNFJXCK.js";
13
13
  import {
14
14
  MUSHI_MCP_PIN_SPEC
15
- } from "./chunk-SC376RQS.js";
15
+ } from "./chunk-R4ZRRD4B.js";
16
16
  import {
17
17
  assertEndpoint,
18
18
  normalizeEndpoint
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-XNC2T2WO.js";
3
+ } from "./chunk-5DQLHMVZ.js";
4
4
  import {
5
5
  assertEndpoint,
6
6
  normalizeEndpoint
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/version.ts
4
+ var MUSHI_CLI_VERSION = true ? "0.36.1" : "0.0.0-dev";
5
+ var MUSHI_MCP_PIN_SPEC = "@mushi-mushi/mcp@0.30.7";
6
+
7
+ export {
8
+ MUSHI_CLI_VERSION,
9
+ MUSHI_MCP_PIN_SPEC
10
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  MUSHI_CLI_VERSION
4
- } from "./chunk-SC376RQS.js";
4
+ } from "./chunk-R4ZRRD4B.js";
5
5
  import {
6
6
  CLOUD_API_ENDPOINT,
7
7
  normalizeEndpoint
@@ -771,6 +771,7 @@ export {
771
771
  getAbortSignal,
772
772
  isKeychainEnabled,
773
773
  trySaveKeyToKeychain,
774
+ tryLoadKeyFromKeychain,
774
775
  syncKeychainAfterKeyChange,
775
776
  trySaveWorkerKey,
776
777
  tryLoadWorkerKey,
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  resolveSdkEnvKey,
4
4
  runConnect
5
- } from "./chunk-B5USMAHY.js";
5
+ } from "./chunk-GLA6OXI3.js";
6
6
  import "./chunk-KOBEOGNF.js";
7
- import "./chunk-PAL6GBXT.js";
8
- import "./chunk-SC376RQS.js";
7
+ import "./chunk-TFNFJXCK.js";
8
+ import "./chunk-R4ZRRD4B.js";
9
9
  import "./chunk-LGGE3LH7.js";
10
10
  export {
11
11
  resolveSdkEnvKey,
@@ -6,10 +6,10 @@ import {
6
6
  saveConfig,
7
7
  savedSdkKeyFor,
8
8
  waitForIngestReady
9
- } from "./chunk-KGZXODOG.js";
9
+ } from "./chunk-MRQEXRAP.js";
10
10
  import {
11
11
  MUSHI_MCP_PIN_SPEC
12
- } from "./chunk-XNC2T2WO.js";
12
+ } from "./chunk-5DQLHMVZ.js";
13
13
  import {
14
14
  detectFramework,
15
15
  envVarsToWrite,
@@ -6,12 +6,13 @@ import {
6
6
  cursorStateFor,
7
7
  infraBackoffMs,
8
8
  machineFailed,
9
+ modelFor,
9
10
  readWorkerKey,
10
11
  runDaemon,
11
12
  workerHostId
12
- } from "./chunk-YAVF3ZUN.js";
13
- import "./chunk-PAL6GBXT.js";
14
- import "./chunk-SC376RQS.js";
13
+ } from "./chunk-D3ABBB6H.js";
14
+ import "./chunk-TFNFJXCK.js";
15
+ import "./chunk-R4ZRRD4B.js";
15
16
  import "./chunk-LGGE3LH7.js";
16
17
  export {
17
18
  assertWorkerKey,
@@ -20,6 +21,7 @@ export {
20
21
  cursorStateFor,
21
22
  infraBackoffMs,
22
23
  machineFailed,
24
+ modelFor,
23
25
  readWorkerKey,
24
26
  runDaemon,
25
27
  workerHostId
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  sanitizeEndpoint,
23
23
  sanitizeProjectId,
24
24
  writeMcpServerEntry
25
- } from "./chunk-B5USMAHY.js";
25
+ } from "./chunk-GLA6OXI3.js";
26
26
  import {
27
27
  FRAMEWORKS,
28
28
  detectFramework,
@@ -44,12 +44,13 @@ import {
44
44
  hashHostId,
45
45
  loadWorkerConfig,
46
46
  nodeHostIO,
47
+ parseLocalAgent,
47
48
  readMachineId,
48
49
  readWorkerKey,
49
50
  registerUxCommands,
50
51
  registerWorkCommands,
51
52
  validateWorkerConfig
52
- } from "./chunk-YAVF3ZUN.js";
53
+ } from "./chunk-D3ABBB6H.js";
53
54
  import {
54
55
  CONFIG_PATH,
55
56
  MushiCliError,
@@ -85,13 +86,14 @@ import {
85
86
  setGlobalOutputFormat,
86
87
  syncKeychainAfterKeyChange,
87
88
  tryDeleteWorkerKey,
89
+ tryLoadKeyFromKeychain,
88
90
  trySaveKeyToKeychain,
89
91
  trySaveWorkerKey
90
- } from "./chunk-PAL6GBXT.js";
92
+ } from "./chunk-TFNFJXCK.js";
91
93
  import {
92
94
  MUSHI_CLI_VERSION,
93
95
  MUSHI_MCP_PIN_SPEC
94
- } from "./chunk-SC376RQS.js";
96
+ } from "./chunk-R4ZRRD4B.js";
95
97
  import {
96
98
  CLOUD_API_ENDPOINT,
97
99
  CLOUD_MCP_OAUTH_URL,
@@ -426,6 +428,17 @@ function printAuthFailed(reason, detail) {
426
428
  console.log("");
427
429
  }
428
430
 
431
+ // src/env-key-override.ts
432
+ function envKeyOverrideLines(env, savedKey, platform = process.platform) {
433
+ const envKey = env.MUSHI_API_KEY?.trim();
434
+ if (!envKey || savedKey && envKey === savedKey) return [];
435
+ const where = platform === "win32" ? 'Remove it under Windows Settings \u2192 System \u2192 About \u2192 Advanced system settings \u2192 Environment Variables, or open a new terminal after `[Environment]::SetEnvironmentVariable("MUSHI_API_KEY", $null, "User")` in PowerShell.' : "Remove the `export MUSHI_API_KEY=\u2026` line from your shell profile, or run `unset MUSHI_API_KEY` in this terminal.";
436
+ return [
437
+ `\u26A0 MUSHI_API_KEY is set in your environment (${envKey.slice(0, 12)}\u2026), and it overrides ${savedKey ? `the saved key (${savedKey.slice(0, 12)}\u2026)` : "the saved key"}.`,
438
+ ` Commands in this terminal use the environment key. ${where}`
439
+ ];
440
+ }
441
+
429
442
  // src/login.ts
430
443
  function getPostLoginBannerMessage(opts) {
431
444
  if (opts.suppressPostLoginBanner) return null;
@@ -673,8 +686,10 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
673
686
  if (opts.upgradeScope) {
674
687
  console.log(` \u2713 Full-scope CLI key saved (${apiKey.slice(0, 12)}\u2026)`);
675
688
  console.log(` Scopes: report:write \xB7 mcp:read \xB7 mcp:write \u2014 SDK + MCP + admin CLI (incl. writes) all work with this key`);
689
+ for (const line of envKeyOverrideLines(process.env, apiKey)) console.log(` ${line}`);
676
690
  } else {
677
691
  console.log(` \u2713 CLI key saved (${apiKey.slice(0, 12)}\u2026)`);
692
+ for (const line of envKeyOverrideLines(process.env, apiKey)) console.log(` ${line}`);
678
693
  console.log(` Scopes: report:write \xB7 mcp:read \u2014 SDK ingest + MCP read tools.`);
679
694
  console.log(` Admin writes (mushi work, fix merge, billing cap, pipeline start) need: mushi login --upgrade-scope${chosenProjectId ? ` --project-id ${chosenProjectId}` : ""}`);
680
695
  }
@@ -1594,7 +1609,7 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
1594
1609
  }
1595
1610
  async function checkVersionDrift(cwd, doFetch) {
1596
1611
  const checks = [];
1597
- const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version-X5ZV5IMP.js");
1612
+ const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version-5GNUKBUB.js");
1598
1613
  if (MUSHI_CLI_VERSION2 === "0.0.0-dev") return checks;
1599
1614
  const parse2 = (v) => {
1600
1615
  const m = v.replace(/^[~^]/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
@@ -2567,7 +2582,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
2567
2582
  if (p.isCancel(answer) || !answer) return;
2568
2583
  const endpoint = resolveCloudEndpoint(options.endpoint);
2569
2584
  try {
2570
- const { runConnect: runConnect2 } = await import("./connect-NZ2RNS7F.js");
2585
+ const { runConnect: runConnect2 } = await import("./connect-PJ2KR2FI.js");
2571
2586
  const saved = loadConfig();
2572
2587
  const result = await runConnect2(
2573
2588
  {
@@ -2631,6 +2646,8 @@ async function maybeSendTestReport(credentials, options) {
2631
2646
  projectId: credentials.projectId,
2632
2647
  description: "Test report from the mushi-mushi setup wizard",
2633
2648
  category: "other",
2649
+ // Never the project's first real report or its activation (first-report.ts).
2650
+ metadata: { source: "cli_wizard" },
2634
2651
  reporterToken: `wizard-${randomUUID()}`,
2635
2652
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2636
2653
  environment: {
@@ -2912,6 +2929,7 @@ Useful after 'mushi login' to confirm credentials are correct.`).action(async (o
2912
2929
  console.log(" Mint one on the console MCP page, or run `mushi login`.");
2913
2930
  }
2914
2931
  }
2932
+ for (const line of envKeyOverrideLines(process.env, tryLoadKeyFromKeychain() ?? void 0)) console.log(` ${line}`);
2915
2933
  }
2916
2934
  });
2917
2935
  program2.command("ping").description("Check connectivity to the Mushi backend").option("--json", "Machine-readable JSON output").action(async (opts) => {
@@ -13521,7 +13539,7 @@ function printLanes(cfg) {
13521
13539
  }
13522
13540
  function registerWorkerCommands(program2) {
13523
13541
  const worker = program2.command("worker").description("Set up and read the local worker (`mushi work --daemon`): lanes, key and service");
13524
- worker.command("install").description("Write the lanes file, store a worker key (worker:run) and the service that runs `mushi work --daemon`; prints everything and asks first").option("--lanes-conf <path>", "Import lane-v4's lanes.conf (name profile clone projectId model mode)").option("--clone-root <dir>", "With --lanes-conf: the folder that holds the clones", join12(homedir(), ".cache", "mushi-work")).option("--lane <name>", "One lane: its name").option("--project <id>", "One lane: its app id").option("--repo-dir <path>", "One lane: the clone it works in").option("--model <id>", "One lane: its model when it first registers (the console can change it)").option("--stop-at <time>", "Every lane stops taking reports at this time when it first registers (ISO, e.g. 2026-10-31T14:59:00Z); change it in the console later").option("--cursor-account <email>", "Spend only this Cursor account: a CLI signed in as anyone else counts as signed out").option("--endpoint <url>", "API endpoint (default: the CLI login's)").option("--key-stdin", "Read a worker key made in the console (Workers tab) from stdin instead of creating one").option("--no-service", "Write the lanes file and key only").option("--yes", "Do not ask before writing").option("--dry-run", "Print what would be written; write nothing").action(async (opts) => {
13542
+ worker.command("install").description("Write the lanes file, store a worker key (worker:run) and the service that runs `mushi work --daemon`; prints everything and asks first").option("--lanes-conf <path>", "Import lane-v4's lanes.conf (name profile clone projectId model mode)").option("--clone-root <dir>", "With --lanes-conf: the folder that holds the clones", join12(homedir(), ".cache", "mushi-work")).option("--lane <name>", "One lane: its name").option("--project <id>", "One lane: its app id").option("--repo-dir <path>", "One lane: the clone it works in").option("--model <id>", "One lane: its model when it first registers (the console can change it)").option("--stop-at <time>", "Every lane stops taking reports at this time when it first registers (ISO, e.g. 2026-10-31T14:59:00Z); change it in the console later").option("--cursor-account <email>", "Spend only this Cursor account: a CLI signed in as anyone else counts as signed out").option("--agent <name>", "The coding agent every lane runs: cursor (default) or claude (Claude Code, signed in with claude auth login)").option("--endpoint <url>", "API endpoint (default: the CLI login's)").option("--key-stdin", "Read a worker key made in the console (Workers tab) from stdin instead of creating one").option("--no-service", "Write the lanes file and key only").option("--yes", "Do not ask before writing").option("--dry-run", "Print what would be written; write nothing").action(async (opts) => {
13525
13543
  const cli = loadConfig();
13526
13544
  const endpoint = opts.endpoint ?? process.env["MUSHI_API_ENDPOINT"] ?? cli.endpoint;
13527
13545
  let stopAt = null;
@@ -13531,7 +13549,8 @@ function registerWorkerCommands(program2) {
13531
13549
  stopAt = new Date(t).toISOString();
13532
13550
  }
13533
13551
  const lanes = lanesFromOptions(opts).map((l) => stopAt ? { ...l, initial: { ...l.initial, stop_at: stopAt } } : l);
13534
- const cfg = validateWorkerConfig({ version: 1, ...endpoint ? { endpoint } : {}, ...opts.cursorAccount ? { cursorAccount: opts.cursorAccount } : {}, lanes });
13552
+ const agent = opts.agent === void 0 ? void 0 : parseLocalAgent(opts.agent);
13553
+ const cfg = validateWorkerConfig({ version: 1, ...endpoint ? { endpoint } : {}, ...opts.cursorAccount ? { cursorAccount: opts.cursorAccount } : {}, ...agent ? { agent } : {}, lanes });
13535
13554
  const projectIds = [...new Set(cfg.lanes.map((l) => l.projectId))];
13536
13555
  const node = process.execPath;
13537
13556
  const cli0 = cliPath();
@@ -13539,7 +13558,8 @@ function registerWorkerCommands(program2) {
13539
13558
  const service = !opts.service ? null : platform === "win32" ? { kind: "task", args: windowsTaskArgs({ node, cli: cli0 }) } : platform === "linux" ? { kind: "systemd", unit: systemdUnit({ node, cli: cli0, path: process.env["PATH"] ?? "/usr/local/bin:/usr/bin:/bin" }) } : null;
13540
13559
  console.log(`Lanes (${WORKER_CONFIG_PATH}):`);
13541
13560
  printLanes(cfg);
13542
- if (cfg.cursorAccount) console.log(`Cursor: spends only ${cfg.cursorAccount}; signed in as anyone else, the worker waits.`);
13561
+ if (cfg.agent === "claude") console.log("Agent: Claude Code (claude -p) on the plan it is signed in to; lane models that are not Claude ids are not used.");
13562
+ if (cfg.cursorAccount && cfg.agent !== "claude") console.log(`Cursor: spends only ${cfg.cursorAccount}; signed in as anyone else, the worker waits.`);
13543
13563
  const keySource = process.env["MUSHI_WORKER_KEY"] ? "MUSHI_WORKER_KEY" : opts.keyStdin ? "stdin" : `a new worker key for ${projectIds.length} app(s), made with your CLI login`;
13544
13564
  console.log(`
13545
13565
  Key: ${keySource}; stored in ${isKeychainEnabled() ? "the OS keychain" : `${WORKER_KEY_PATH} (mode 0600; no keychain here)`}. It can only take reports and record their fixes.`);
package/dist/init.js CHANGED
@@ -25,10 +25,10 @@ import {
25
25
  saveConfig,
26
26
  savedSdkKeyFor,
27
27
  trySaveKeyToKeychain
28
- } from "./chunk-KGZXODOG.js";
28
+ } from "./chunk-MRQEXRAP.js";
29
29
  import {
30
30
  MUSHI_CLI_VERSION
31
- } from "./chunk-XNC2T2WO.js";
31
+ } from "./chunk-5DQLHMVZ.js";
32
32
  import {
33
33
  FRAMEWORKS,
34
34
  detectFramework,
@@ -2127,7 +2127,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
2127
2127
  if (p.isCancel(answer) || !answer) return;
2128
2128
  const endpoint = resolveCloudEndpoint(options.endpoint);
2129
2129
  try {
2130
- const { runConnect } = await import("./connect-MOD2FT6S.js");
2130
+ const { runConnect } = await import("./connect-X5OPJFCR.js");
2131
2131
  const saved = loadConfig();
2132
2132
  const result = await runConnect(
2133
2133
  {
@@ -2191,6 +2191,8 @@ async function maybeSendTestReport(credentials, options) {
2191
2191
  projectId: credentials.projectId,
2192
2192
  description: "Test report from the mushi-mushi setup wizard",
2193
2193
  category: "other",
2194
+ // Never the project's first real report or its activation (first-report.ts).
2195
+ metadata: { source: "cli_wizard" },
2194
2196
  reporterToken: `wizard-${randomUUID()}`,
2195
2197
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2196
2198
  environment: {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  MUSHI_CLI_VERSION,
4
4
  MUSHI_MCP_PIN_SPEC
5
- } from "./chunk-SC376RQS.js";
5
+ } from "./chunk-R4ZRRD4B.js";
6
6
  export {
7
7
  MUSHI_CLI_VERSION,
8
8
  MUSHI_MCP_PIN_SPEC
package/dist/version.d.ts CHANGED
@@ -14,6 +14,6 @@ declare const MUSHI_CLI_VERSION: string;
14
14
  * setup refreshes the pin. Synced to packages/mcp/package.json by
15
15
  * `scripts/sync-mcp-pin.mjs` — never hand-edit the version.
16
16
  */
17
- declare const MUSHI_MCP_PIN_SPEC = "@mushi-mushi/mcp@0.30.4";
17
+ declare const MUSHI_MCP_PIN_SPEC = "@mushi-mushi/mcp@0.30.7";
18
18
 
19
19
  export { MUSHI_CLI_VERSION, MUSHI_MCP_PIN_SPEC };
package/dist/version.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION,
3
3
  MUSHI_MCP_PIN_SPEC
4
- } from "./chunk-XNC2T2WO.js";
4
+ } from "./chunk-5DQLHMVZ.js";
5
5
  export {
6
6
  MUSHI_CLI_VERSION,
7
7
  MUSHI_MCP_PIN_SPEC
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@mushi-mushi/cli",
3
- "version": "0.35.7",
3
+ "version": "0.36.1",
4
4
  "license": "MIT",
5
- "description": "Mushi Mushi CLI: SDK setup, MCP wiring, triage. The bug mediator for AI-built apps: plain-English diagnosis + a ready fix, in your editor.",
5
+ "description": "Mushi Mushi CLI: SDK setup, MCP wiring, triage. Bug fixing for AI-built apps: plain-English diagnosis + a ready fix, in your editor.",
6
6
  "bin": {
7
7
  "mushi": "./dist/index.js"
8
8
  },
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/version.ts
4
- var MUSHI_CLI_VERSION = true ? "0.35.7" : "0.0.0-dev";
5
- var MUSHI_MCP_PIN_SPEC = "@mushi-mushi/mcp@0.30.4";
6
-
7
- export {
8
- MUSHI_CLI_VERSION,
9
- MUSHI_MCP_PIN_SPEC
10
- };
@@ -1,8 +0,0 @@
1
- // src/version.ts
2
- var MUSHI_CLI_VERSION = true ? "0.35.7" : "0.0.0-dev";
3
- var MUSHI_MCP_PIN_SPEC = "@mushi-mushi/mcp@0.30.4";
4
-
5
- export {
6
- MUSHI_CLI_VERSION,
7
- MUSHI_MCP_PIN_SPEC
8
- };