@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 +4 -4
- package/dist/chunk-5DQLHMVZ.js +8 -0
- package/dist/{chunk-YAVF3ZUN.js → chunk-D3ABBB6H.js} +260 -90
- package/dist/{chunk-B5USMAHY.js → chunk-GLA6OXI3.js} +2 -2
- package/dist/{chunk-KGZXODOG.js → chunk-MRQEXRAP.js} +1 -1
- package/dist/chunk-R4ZRRD4B.js +10 -0
- package/dist/{chunk-PAL6GBXT.js → chunk-TFNFJXCK.js} +2 -1
- package/dist/{connect-NZ2RNS7F.js → connect-PJ2KR2FI.js} +3 -3
- package/dist/{connect-MOD2FT6S.js → connect-X5OPJFCR.js} +2 -2
- package/dist/{daemon-FN3VM4R6.js → daemon-VC4WW5GU.js} +5 -3
- package/dist/index.js +29 -9
- package/dist/init.js +5 -3
- package/dist/{version-X5ZV5IMP.js → version-5GNUKBUB.js} +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-SC376RQS.js +0 -10
- package/dist/chunk-XNC2T2WO.js +0 -8
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
|
|
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` (
|
|
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 #
|
|
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
|
|
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
|
|
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
requireConfig,
|
|
9
9
|
requireUuid,
|
|
10
10
|
tryLoadWorkerKey
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-TFNFJXCK.js";
|
|
12
12
|
import {
|
|
13
13
|
MUSHI_CLI_VERSION
|
|
14
|
-
} from "./chunk-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
751
|
-
|
|
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) ||
|
|
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}`,
|
|
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
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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 ||
|
|
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.",
|
|
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
|
-
|
|
901
|
-
|
|
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
|
|
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(
|
|
946
|
-
const state =
|
|
947
|
-
const detail = p.timedOut ? `timed out after ${CURSOR_STATUS_TIMEOUT_MS / 1e3} s` :
|
|
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:
|
|
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
|
-
|
|
956
|
-
|
|
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",
|
|
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",
|
|
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
|
|
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 (
|
|
983
|
-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
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:
|
|
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
|
|
1501
|
-
|
|
1502
|
-
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2377
|
-
model: lane.config?.model
|
|
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
|
|
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({
|
|
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
|
-
...
|
|
2755
|
-
|
|
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-
|
|
12
|
+
} from "./chunk-TFNFJXCK.js";
|
|
13
13
|
import {
|
|
14
14
|
MUSHI_MCP_PIN_SPEC
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-R4ZRRD4B.js";
|
|
16
16
|
import {
|
|
17
17
|
assertEndpoint,
|
|
18
18
|
normalizeEndpoint
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
MUSHI_CLI_VERSION
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
5
|
+
} from "./chunk-GLA6OXI3.js";
|
|
6
6
|
import "./chunk-KOBEOGNF.js";
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
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-
|
|
9
|
+
} from "./chunk-MRQEXRAP.js";
|
|
10
10
|
import {
|
|
11
11
|
MUSHI_MCP_PIN_SPEC
|
|
12
|
-
} from "./chunk-
|
|
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-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
92
|
+
} from "./chunk-TFNFJXCK.js";
|
|
91
93
|
import {
|
|
92
94
|
MUSHI_CLI_VERSION,
|
|
93
95
|
MUSHI_MCP_PIN_SPEC
|
|
94
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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
|
|
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.
|
|
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-
|
|
28
|
+
} from "./chunk-MRQEXRAP.js";
|
|
29
29
|
import {
|
|
30
30
|
MUSHI_CLI_VERSION
|
|
31
|
-
} from "./chunk-
|
|
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-
|
|
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: {
|
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.
|
|
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
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mushi-mushi/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.1",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "Mushi Mushi CLI: SDK setup, MCP wiring, triage.
|
|
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
|
},
|
package/dist/chunk-SC376RQS.js
DELETED