@mushi-mushi/cli 0.20.0 → 0.21.0

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/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  sanitizeProjectId,
19
19
  saveConfig,
20
20
  writeMcpServerEntry
21
- } from "./chunk-D5IDNBJO.js";
21
+ } from "./chunk-R2HPR2FV.js";
22
22
  import {
23
23
  CLOUD_API_ENDPOINT,
24
24
  TEST_REPORT_FETCH_TIMEOUT_MS,
@@ -110,13 +110,13 @@ function getAbortSignal(external) {
110
110
  }
111
111
 
112
112
  // src/version.ts
113
- var MUSHI_CLI_VERSION = true ? "0.20.0" : "0.0.0-dev";
113
+ var MUSHI_CLI_VERSION = true ? "0.21.0" : "0.0.0-dev";
114
114
 
115
115
  // src/init.ts
116
116
  import * as p from "@clack/prompts";
117
117
  import { spawn } from "child_process";
118
118
  import { randomUUID } from "crypto";
119
- import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
119
+ import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
120
120
  import { join as join3 } from "path";
121
121
 
122
122
  // src/console-url.ts
@@ -292,13 +292,13 @@ async function apiCall(path, config, options = {}) {
292
292
  }
293
293
  if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
294
294
  const b = body;
295
+ const nested = b["error"];
296
+ const errCode = nested?.["code"] ?? b["code"] ?? `HTTP_${res.status}`;
297
+ const errMsg = nested?.["message"] ?? b["message"] ?? `Request failed (${res.status})`;
295
298
  return {
296
299
  ok: false,
297
300
  httpStatus: res.status,
298
- error: {
299
- code: b["code"] ?? `HTTP_${res.status}`,
300
- message: b["message"] ?? `Request failed (${res.status})`
301
- }
301
+ error: { code: errCode, message: errMsg }
302
302
  };
303
303
  }
304
304
  return body;
@@ -327,6 +327,11 @@ function die(result, exitCode = 1) {
327
327
  const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
328
328
  process.stderr.write(`error${status}: ${code} \u2014 ${message}
329
329
  `);
330
+ if (code === "INSUFFICIENT_SCOPE") {
331
+ process.stderr.write(
332
+ "\n Your current API key does not have the required scope for this command.\n New keys minted by the wizard already include both scopes. Existing keys\n can be upgraded with:\n\n mushi login --upgrade-scope\n\n Or visit your console \u2192 Projects \u2192 API Keys to mint a new key.\n"
333
+ );
334
+ }
330
335
  process.exit(exitCode);
331
336
  }
332
337
  function requireConfig(opts = {}) {
@@ -424,8 +429,12 @@ async function waitForCliToken(endpoint, session, opts = {}) {
424
429
  const deadline = now() + (session.expires_in || 600) * 1e3;
425
430
  const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
426
431
  let consecutiveErrors = 0;
432
+ let firstPoll = true;
427
433
  while (now() < deadline) {
428
- await sleep(intervalMs);
434
+ if (!firstPoll) {
435
+ await sleep(intervalMs);
436
+ }
437
+ firstPoll = false;
429
438
  const outcome = await pollDeviceToken(endpoint, session.device_code);
430
439
  switch (outcome.status) {
431
440
  case "approved":
@@ -653,6 +662,53 @@ function getFrameworkFromPkg(pkg) {
653
662
  return void 0;
654
663
  }
655
664
 
665
+ // src/auth-ui.ts
666
+ var LINE = "\u2500".repeat(58);
667
+ function printAuthBanner(userCode, verificationUri) {
668
+ console.log("");
669
+ console.log(` \u250C${LINE}\u2510`);
670
+ console.log(` \u2502 Browser sign-in \u2014 follow these 3 steps \u2502`);
671
+ console.log(` \u251C${LINE}\u2524`);
672
+ console.log(` \u2502 \u2502`);
673
+ console.log(` \u2502 1. A browser tab will open (or visit the URL below) \u2502`);
674
+ console.log(` \u2502 2. Sign in if prompted, then click "Approve" \u2502`);
675
+ console.log(` \u2502 3. Come back here \u2014 setup continues automatically \u2502`);
676
+ console.log(` \u2502 \u2502`);
677
+ console.log(` \u251C${LINE}\u2524`);
678
+ console.log(` \u2502 Verification code (show in browser \u2014 NOT terminal): \u2502`);
679
+ console.log(` \u2502 \u2502`);
680
+ console.log(` \u2502 ${userCode.padEnd(52)}\u2502`);
681
+ console.log(` \u2502 \u2502`);
682
+ console.log(` \u2502 \u26A0 Do NOT paste or type this code in your terminal \u2502`);
683
+ console.log(` \u2514${LINE}\u2518`);
684
+ console.log("");
685
+ console.log(` If the browser didn't open, visit this URL and approve:`);
686
+ console.log(` ${verificationUri}`);
687
+ console.log("");
688
+ console.log(" \u23F3 Waiting for browser approval\u2026 (Ctrl+C to cancel)");
689
+ console.log("");
690
+ }
691
+ function printAuthApproved() {
692
+ console.log("");
693
+ console.log(" \u2713 Approved! Continuing setup in this terminal\u2026");
694
+ console.log("");
695
+ }
696
+ function printAuthFailed(reason, detail) {
697
+ console.log("");
698
+ if (reason === "denied") {
699
+ console.error(" \u2717 Browser sign-in was denied.");
700
+ console.error(" If that was a mistake, run mushi login again.");
701
+ } else if (reason === "timeout") {
702
+ console.error(" \u2717 Browser sign-in timed out (10 minutes elapsed).");
703
+ console.error(" Run mushi login again \u2014 a new code will be generated.");
704
+ } else {
705
+ console.error(` \u2717 Browser sign-in failed: ${detail ?? "unknown error"}`);
706
+ console.error(" Run mushi login again, or use:");
707
+ console.error(" mushi login --api-key <key> --project-id <uuid>");
708
+ }
709
+ console.log("");
710
+ }
711
+
656
712
  // src/init.ts
657
713
  var ENV_FILES = [".env.local", ".env"];
658
714
  var PROJECT_ID_PATTERN = /^(?:proj_[A-Za-z0-9_-]{10,}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
@@ -688,7 +744,7 @@ async function runInit(options = {}) {
688
744
  } else {
689
745
  p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
690
746
  }
691
- writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
747
+ await writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework, endpoint, Boolean(options.yes));
692
748
  persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
693
749
  const enableRewards = await maybeEnableRewards(options);
694
750
  printNextSteps(framework, consoleBase, enableRewards);
@@ -759,7 +815,7 @@ async function acquireCredentials(options, consoleBase, endpoint) {
759
815
  }
760
816
  }
761
817
  if (options.yes) {
762
- const creds = await runBrowserSignIn(options, endpoint);
818
+ const creds = await runBrowserSignIn(options, endpoint, consoleBase);
763
819
  if (creds) return creds;
764
820
  p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
765
821
  } else {
@@ -775,7 +831,7 @@ async function acquireCredentials(options, consoleBase, endpoint) {
775
831
  {
776
832
  value: "manual",
777
833
  label: "Paste a Project ID + API key",
778
- hint: "Self-hosted or expert setup"
834
+ hint: "Self-hosted or expert setup \u2014 run npx mushi-mushi (not mushi setup)"
779
835
  }
780
836
  ]
781
837
  });
@@ -784,14 +840,14 @@ async function acquireCredentials(options, consoleBase, endpoint) {
784
840
  process.exit(0);
785
841
  }
786
842
  if (method === "browser") {
787
- const creds = await runBrowserSignIn(options, endpoint);
843
+ const creds = await runBrowserSignIn(options, endpoint, consoleBase);
788
844
  if (creds) return creds;
789
845
  p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
790
846
  }
791
847
  }
792
848
  return collectCredentialsManually(options, consoleBase);
793
849
  }
794
- async function runBrowserSignIn(options, endpoint) {
850
+ async function runBrowserSignIn(options, endpoint, consoleBase) {
795
851
  const startSpin = p.spinner();
796
852
  startSpin.start("Starting secure browser sign-in\u2026");
797
853
  let session;
@@ -803,13 +859,11 @@ async function runBrowserSignIn(options, endpoint) {
803
859
  return null;
804
860
  }
805
861
  startSpin.stop("Browser sign-in ready.");
806
- p.log.step(`Confirmation code: ${session.user_code}`);
807
- p.log.info(`Opening ${session.verification_uri}`);
808
862
  try {
809
863
  await openInBrowser(session.verification_uri);
810
864
  } catch {
811
865
  }
812
- p.log.message("Approve the request in your browser to continue. (Ctrl+C to cancel)");
866
+ printAuthBanner(session.user_code, session.verification_uri);
813
867
  const waitSpin = p.spinner();
814
868
  waitSpin.start("Waiting for you to approve in the browser\u2026");
815
869
  let cliToken;
@@ -825,9 +879,14 @@ async function runBrowserSignIn(options, endpoint) {
825
879
  let apiKey;
826
880
  if (!projectId) {
827
881
  let projects = [];
882
+ const fetchSpin = p.spinner();
883
+ fetchSpin.start("Loading your projects\u2026");
828
884
  try {
829
885
  projects = await listProjects(endpoint, cliToken);
830
- } catch {
886
+ fetchSpin.stop(projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet.");
887
+ } catch (err) {
888
+ fetchSpin.stop("Could not load projects \u2014 you can still create a new one.");
889
+ p.log.warn(err instanceof Error ? err.message : String(err));
831
890
  }
832
891
  const NEW = "__new__";
833
892
  const choice = await p.select({
@@ -869,7 +928,24 @@ async function runBrowserSignIn(options, endpoint) {
869
928
  }
870
929
  }
871
930
  if (projectId && !apiKey) {
872
- apiKey = await mintProjectKey(endpoint, cliToken, projectId) ?? void 0;
931
+ const keySpin = p.spinner();
932
+ keySpin.start("Minting SDK key\u2026");
933
+ try {
934
+ const minted = await mintProjectKey(endpoint, cliToken, projectId);
935
+ apiKey = minted ?? void 0;
936
+ if (!apiKey) {
937
+ keySpin.stop("Could not mint an API key.");
938
+ p.log.warn(
939
+ `Open the console Verify tab to generate a key manually: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
940
+ );
941
+ return null;
942
+ }
943
+ keySpin.stop("SDK key ready.");
944
+ } catch (err) {
945
+ keySpin.stop("Could not mint an API key.");
946
+ p.log.warn(err instanceof Error ? err.message : String(err));
947
+ return null;
948
+ }
873
949
  }
874
950
  if (!projectId || !apiKey) return null;
875
951
  return { apiKey, projectId };
@@ -975,13 +1051,37 @@ function runCommand(pm, packages, cwd) {
975
1051
  });
976
1052
  });
977
1053
  }
978
- function writeEnvFile(cwd, apiKey, projectId, framework) {
1054
+ async function writeEnvFile(cwd, apiKey, projectId, framework, endpoint, overwrite) {
979
1055
  const target = ENV_FILES.find((f) => existsSync3(join3(cwd, f))) ?? ENV_FILES[0];
980
1056
  const targetPath = join3(cwd, target);
981
- const newVars = envVarsToWrite(apiKey, projectId, framework);
1057
+ const newVars = envVarsToWrite(apiKey, projectId, framework, endpoint);
982
1058
  const existing = existsSync3(targetPath) ? readFileSync3(targetPath, "utf-8") : "";
983
1059
  if (existing.includes("MUSHI_PROJECT_ID")) {
984
- p.log.warn(`Existing MUSHI_* vars found in ${target} \u2014 leaving them untouched.`);
1060
+ let shouldOverwrite = overwrite;
1061
+ if (!shouldOverwrite) {
1062
+ const answer = await p.confirm({
1063
+ message: `Existing MUSHI_* vars found in ${target}. Update with new credentials?`,
1064
+ initialValue: true
1065
+ });
1066
+ if (p.isCancel(answer)) {
1067
+ p.log.info(`Kept existing env vars in ${target}.`);
1068
+ return;
1069
+ }
1070
+ shouldOverwrite = Boolean(answer);
1071
+ }
1072
+ if (!shouldOverwrite) {
1073
+ p.log.info(`Kept existing env vars in ${target}. Re-run and confirm to overwrite.`);
1074
+ return;
1075
+ }
1076
+ const MUSHI_LINE_RE = /^(NEXT_PUBLIC_|NUXT_PUBLIC_|VITE_|EXPO_PUBLIC_)?MUSHI_[A-Z_]+=.*/gm;
1077
+ const stripped = existing.replace(MUSHI_LINE_RE, "").replace(/\n{3,}/g, "\n\n").trimEnd();
1078
+ const prefix2 = stripped.length > 0 ? "\n" : "";
1079
+ writeFileSync(targetPath, `${stripped}${prefix2}
1080
+ # Mushi Mushi
1081
+ ${newVars}
1082
+ `);
1083
+ p.log.success(`Updated MUSHI_* env vars in ${target}`);
1084
+ warnIfMissingFromGitignore(cwd, target);
985
1085
  return;
986
1086
  }
987
1087
  const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
@@ -1042,12 +1142,14 @@ import { MushiRewardsBadge } from '@mushi-mushi/react';
1042
1142
  p.log.info(`Enable rewards in your project settings at ${consoleUrl(consoleBase, "/rewards")}`);
1043
1143
  p.log.info("Users will earn points for bug reports, screen navigation, and app activity.");
1044
1144
  }
1045
- p.log.message("Verify the install:");
1046
- p.log.message(" \u2022 Start your dev server");
1047
- p.log.message(" \u2022 Look for the \u{1F41B} button in the bottom-right corner (or shake on mobile)");
1048
- p.log.message(` \u2022 Submit a test report \u2014 it should appear at ${reportsUrl(consoleBase)}`);
1049
- p.log.info(
1050
- "Tip: `mushi connect --write-env --wire-ide --wait` also wires Cursor MCP and waits for SDK heartbeat."
1145
+ p.note(
1146
+ [
1147
+ " [ ] 1. Paste the init snippet above into your app entry file",
1148
+ " [ ] 2. Start your dev server",
1149
+ " [ ] 3. Run: mushi connect --write-env --wire-ide --wait",
1150
+ ` [ ] 4. Open the Verify tab to send a test report: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
1151
+ ].join("\n"),
1152
+ "Next steps:"
1051
1153
  );
1052
1154
  }
1053
1155
  async function maybeOfferConnect(credentials, options, _consoleBase) {
@@ -1059,7 +1161,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
1059
1161
  if (p.isCancel(answer) || !answer) return;
1060
1162
  const endpoint = resolveCloudEndpoint(options.endpoint);
1061
1163
  try {
1062
- const { runConnect: runConnect2 } = await import("./connect-W2Q6N7K3.js");
1164
+ const { runConnect: runConnect2 } = await import("./connect-JIEIWTF3.js");
1063
1165
  await runConnect2({
1064
1166
  apiKey: credentials.apiKey,
1065
1167
  projectId: credentials.projectId,
@@ -1344,11 +1446,15 @@ function registerAccountCommands(program2) {
1344
1446
  const { matches } = runMigrate({ cwd: opts.cwd, json: opts.json });
1345
1447
  if (matches.length === 0) process.exit(1);
1346
1448
  });
1347
- program2.command("login").description("Authenticate the CLI via browser (zero copy-paste), or pass --api-key to skip").option("--api-key <key>", "API key (mushi_...) \u2014 non-interactive / CI path").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted)").option("--project-id <id>", "Project UUID \u2014 skip the project picker").option("--no-browser", "Print the verification URL instead of opening the browser").addHelpText("after", `
1449
+ program2.command("login").description("Authenticate the CLI via browser (zero copy-paste), or pass --api-key to skip").option("--api-key <key>", "API key (mushi_...) \u2014 non-interactive / CI path").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted)").option("--project-id <id>", "Project UUID \u2014 skip the project picker").option("--no-browser", "Print the verification URL instead of opening the browser").option(
1450
+ "--upgrade-scope",
1451
+ "Re-authenticate and mint a new key with report:write + mcp:read + mcp:write scopes. Use this to upgrade an existing ingest-only key so MCP and admin commands (including writes like `mushi billing cap`) work."
1452
+ ).addHelpText("after", `
1348
1453
  Examples:
1349
1454
  mushi login # browser-guided (recommended)
1350
1455
  mushi login --api-key mushi_xxx # non-interactive / CI
1351
- mushi login --api-key mushi_xxx --project-id <uuid>`).action(async (opts) => {
1456
+ mushi login --api-key mushi_xxx --project-id <uuid>
1457
+ mushi login --upgrade-scope # re-auth to add mcp:read + mcp:write to your key`).action(async (opts) => {
1352
1458
  const { CLOUD_API_ENDPOINT: CLOUD_API_ENDPOINT2, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
1353
1459
  const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT2;
1354
1460
  const consoleBase = await resolveConsoleUrl();
@@ -1386,8 +1492,16 @@ Examples:
1386
1492
  return;
1387
1493
  }
1388
1494
  console.log("");
1389
- console.log(" Mushi login");
1390
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1495
+ if (opts.upgradeScope) {
1496
+ console.log(" Mushi login \u2014 scope upgrade");
1497
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1498
+ console.log(" Re-authenticating to mint a new key with report:write + mcp:read + mcp:write scopes.");
1499
+ console.log(" This lets the CLI admin commands (including writes) and MCP server both work with a single key.");
1500
+ console.log("");
1501
+ } else {
1502
+ console.log(" Mushi login");
1503
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1504
+ }
1391
1505
  let deviceData;
1392
1506
  try {
1393
1507
  deviceData = await startDeviceAuth(endpoint);
@@ -1400,26 +1514,24 @@ error: Could not start login session: ${err instanceof Error ? err.message : Str
1400
1514
  process.exit(1);
1401
1515
  }
1402
1516
  const verifyUrl = deviceData.verification_uri;
1403
- console.log("");
1404
- console.log(` Your confirmation code: ${deviceData.user_code}`);
1405
- console.log("");
1406
1517
  if (opts.browser !== false) {
1407
- console.log(" Opening the Mushi console in your browser\u2026");
1408
1518
  try {
1409
1519
  await openInBrowser(verifyUrl);
1410
1520
  } catch {
1411
1521
  }
1412
1522
  }
1413
- console.log(` If the browser didn't open: ${verifyUrl}`);
1414
- console.log("");
1415
- console.log(" Waiting for you to approve in the browser\u2026 (Ctrl+C to cancel)");
1523
+ printAuthBanner(deviceData.user_code, verifyUrl);
1416
1524
  let cliToken = null;
1417
1525
  const pollIntervalMs = (deviceData.interval ?? 5) * 1e3;
1418
1526
  const deadline = Date.now() + (deviceData.expires_in ?? 600) * 1e3;
1419
1527
  const MAX_CONSECUTIVE_ERRORS = 5;
1420
1528
  let consecutiveErrors = 0;
1529
+ let firstPoll = true;
1421
1530
  while (Date.now() < deadline) {
1422
- await new Promise((r) => setTimeout(r, pollIntervalMs));
1531
+ if (!firstPoll) {
1532
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
1533
+ }
1534
+ firstPoll = false;
1423
1535
  const outcome = await pollDeviceToken(endpoint, deviceData.device_code);
1424
1536
  if (outcome.status === "approved") {
1425
1537
  cliToken = outcome.cliToken;
@@ -1427,38 +1539,30 @@ error: Could not start login session: ${err instanceof Error ? err.message : Str
1427
1539
  }
1428
1540
  if (outcome.status === "pending") {
1429
1541
  consecutiveErrors = 0;
1430
- process.stdout.write(".");
1431
1542
  continue;
1432
1543
  }
1433
1544
  if (outcome.status === "error") {
1434
1545
  if (outcome.retryable) {
1435
1546
  consecutiveErrors += 1;
1436
- if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) {
1437
- process.stdout.write("\xB7");
1438
- continue;
1439
- }
1547
+ if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) continue;
1440
1548
  }
1441
- console.log("");
1442
- process.stderr.write(`
1443
- error: ${outcome.message}
1444
- `);
1549
+ printAuthFailed("error", outcome.message);
1445
1550
  process.exit(1);
1446
1551
  }
1447
- console.log("");
1448
1552
  if (outcome.status === "denied") {
1449
- process.stderr.write("\nerror: Login denied in the browser.\n");
1450
- } else if (outcome.status === "expired") {
1451
- process.stderr.write("\nerror: Login code expired. Run mushi login again.\n");
1553
+ printAuthFailed("denied");
1554
+ process.exit(1);
1555
+ }
1556
+ if (outcome.status === "expired") {
1557
+ printAuthFailed("timeout");
1558
+ process.exit(1);
1452
1559
  }
1453
- process.exit(1);
1454
1560
  }
1455
1561
  if (!cliToken) {
1456
- console.log("");
1457
- process.stderr.write("\nerror: Login timed out. Run mushi login again.\n");
1562
+ printAuthFailed("timeout");
1458
1563
  process.exit(1);
1459
1564
  }
1460
- console.log("");
1461
- console.log(" \u2713 Approved!");
1565
+ printAuthApproved();
1462
1566
  const projectsList = await listProjects(endpoint, cliToken);
1463
1567
  let chosenProjectId = projectId;
1464
1568
  let chosenProjectName;
@@ -1521,12 +1625,17 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
1521
1625
  console.log("");
1522
1626
  if (chosenProjectName) console.log(` \u2713 Project: ${chosenProjectName}`);
1523
1627
  if (apiKey) {
1524
- console.log(` \u2713 SDK key saved (${apiKey.slice(0, 12)}\u2026)`);
1628
+ console.log(` \u2713 Full-scope CLI key saved (${apiKey.slice(0, 12)}\u2026)`);
1629
+ console.log(` Scopes: report:write \xB7 mcp:read \xB7 mcp:write \u2014 SDK + MCP + admin CLI (incl. writes) all work with this key`);
1525
1630
  } else {
1526
1631
  console.log(` \u2139 Get an SDK key at: ${apiKeyHint(consoleBase)}`);
1527
1632
  }
1528
1633
  console.log("");
1529
- console.log(" Run 'mushi init' to set up the SDK in this project.");
1634
+ if (opts.upgradeScope) {
1635
+ console.log(" Key upgraded! Re-run 'mushi setup' to update your .cursor/mcp.json, then restart Cursor.");
1636
+ } else {
1637
+ console.log(" Run 'mushi init' to set up the SDK in this project.");
1638
+ }
1530
1639
  });
1531
1640
  program2.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
1532
1641
  Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
@@ -2382,33 +2491,24 @@ error: Could not start browser sign-in: ${err instanceof Error ? err.message : S
2382
2491
  process.stderr.write(" Fallback: mushi login --api-key <key> --project-id <uuid>\n");
2383
2492
  process.exit(1);
2384
2493
  }
2385
- console.log(` Confirmation code: ${session.user_code}`);
2386
- console.log("");
2387
2494
  if (opts.browser !== false) {
2388
- console.log(" Opening the Mushi console in your browser\u2026");
2389
2495
  try {
2390
2496
  await openInBrowser(session.verification_uri);
2391
2497
  } catch {
2392
2498
  }
2393
2499
  }
2394
- console.log(` If the browser didn't open: ${session.verification_uri}`);
2395
- console.log("");
2396
- console.log(" Waiting for you to approve in the browser\u2026 (Ctrl+C to cancel)");
2500
+ printAuthBanner(session.user_code, session.verification_uri);
2397
2501
  let cliToken;
2398
2502
  try {
2399
- cliToken = await waitForCliToken(endpoint, session, {
2400
- onPending: () => process.stdout.write("."),
2401
- onTransientError: () => process.stdout.write("\xB7")
2402
- });
2503
+ cliToken = await waitForCliToken(endpoint, session);
2403
2504
  } catch (err) {
2404
- console.log("");
2405
- process.stderr.write(`
2406
- error: ${err instanceof Error ? err.message : String(err)}
2407
- `);
2505
+ const msg = err instanceof Error ? err.message : String(err);
2506
+ if (msg.includes("denied")) printAuthFailed("denied");
2507
+ else if (msg.includes("timed out") || msg.includes("expired")) printAuthFailed("timeout");
2508
+ else printAuthFailed("error", msg);
2408
2509
  process.exit(1);
2409
2510
  }
2410
- console.log("");
2411
- console.log(" \u2713 Approved!");
2511
+ printAuthApproved();
2412
2512
  const projectsList = await listProjects(endpoint, cliToken);
2413
2513
  const { createInterface } = await import("readline");
2414
2514
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -2495,7 +2595,7 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
2495
2595
 
2496
2596
  // src/commands/setup.ts
2497
2597
  function registerSetupCommands(program2) {
2498
- program2.command("setup").description("Wire Cursor MCP into your IDE (reads ~/.config/mushi/config.json \u2014 run `mushi login` first)").option("--ide <ide>", "Target IDE: cursor | claude | continue | zed", "cursor").option("--project-slug <slug>", "Override the project slug in the server name (default: fetched from API or ID prefix)").option("--all-projects", "Write a separate mushi-<name> server entry for every accessible project").option("--with-rules", "Also write the .cursorrules / .claude/rules/mushi.md lesson-library hook").option("--dry-run", "Print what would be written without making changes").addHelpText("after", `
2598
+ program2.command("setup").description("Wire Cursor MCP into your IDE (reads ~/.config/mushi/config.json \u2014 run `mushi login` first)").option("--ide <ide>", "Target IDE: cursor | claude | continue | zed", "cursor").option("--project-slug <slug>", "Override the project slug in the server name (default: fetched from API or ID prefix)").option("--all-projects", "Write a separate mushi-<name> server entry for every accessible project").option("--with-rules", "Also write the .cursorrules / .claude/rules/mushi.md lesson-library hook").option("--dry-run", "Print what would be written without making changes").option("--verify", "Probe the MCP key after writing to confirm it has mcp:read scope (default: on)").option("--no-verify", "Skip the post-write key probe").addHelpText("after", `
2499
2599
  Examples:
2500
2600
  mushi setup # wire Cursor (default)
2501
2601
  mushi setup --all-projects # one server entry per accessible project
@@ -2699,6 +2799,54 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2699
2799
  }
2700
2800
  }
2701
2801
  if (!opts.dryRun) {
2802
+ const shouldVerify = opts.verify !== false;
2803
+ if (shouldVerify && config.endpoint && config.apiKey && config.projectId) {
2804
+ try {
2805
+ const probeRes = await fetch(
2806
+ `${config.endpoint.replace(/\/$/, "")}/v1/admin/mcp/account-overview`,
2807
+ {
2808
+ headers: {
2809
+ "X-Mushi-Api-Key": config.apiKey,
2810
+ "X-Mushi-Project": config.projectId
2811
+ },
2812
+ signal: AbortSignal.timeout(6e3)
2813
+ }
2814
+ );
2815
+ if (probeRes.ok) {
2816
+ console.log("\u2713 MCP key valid \u2014 restart Cursor (or your IDE) to activate");
2817
+ void fetch(
2818
+ `${config.endpoint.replace(/\/$/, "")}/v1/cli/funnel`,
2819
+ {
2820
+ method: "POST",
2821
+ headers: {
2822
+ "Content-Type": "application/json",
2823
+ "X-Mushi-Api-Key": config.apiKey,
2824
+ "X-Mushi-Project": config.projectId
2825
+ },
2826
+ body: JSON.stringify({ event: "mcp_setup_done", source: "cli" }),
2827
+ signal: AbortSignal.timeout(4e3)
2828
+ }
2829
+ ).catch(() => {
2830
+ });
2831
+ } else if (probeRes.status === 403) {
2832
+ let errCode;
2833
+ try {
2834
+ const b = await probeRes.json();
2835
+ errCode = b?.error?.code;
2836
+ } catch {
2837
+ }
2838
+ if (errCode === "INSUFFICIENT_SCOPE") {
2839
+ console.log("\n\u26A0 Your key has report:write scope only \u2014 MCP admin tools will not work.");
2840
+ console.log(" New keys minted by the wizard include both scopes automatically.");
2841
+ console.log(" To upgrade an existing key, run:\n");
2842
+ console.log(" mushi login --upgrade-scope\n");
2843
+ } else {
2844
+ console.log(`\u26A0 Key probe returned HTTP ${probeRes.status} \u2014 check your credentials.`);
2845
+ }
2846
+ }
2847
+ } catch {
2848
+ }
2849
+ }
2702
2850
  console.log("");
2703
2851
  console.log(`Done! Restart ${opts.ide === "cursor" ? "Cursor" : opts.ide === "claude" ? "Claude Code" : opts.ide} and ask: "list mushi tools"`);
2704
2852
  if (!opts.withRules) {
@@ -2909,6 +3057,23 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
2909
3057
  detail: sc.ready ? "" : sc.hint
2910
3058
  }));
2911
3059
  }
3060
+ if (res.status === 403) {
3061
+ let errCode;
3062
+ try {
3063
+ const body = await res.json();
3064
+ errCode = body?.error?.code;
3065
+ } catch {
3066
+ }
3067
+ if (errCode === "INSUFFICIENT_SCOPE") {
3068
+ return [
3069
+ {
3070
+ name: "Server preflight",
3071
+ ok: true,
3072
+ detail: "Skipped \u2014 your key has report:write scope only (pre-Jun 2026 key). Run `mushi login --upgrade-scope` to get mcp:read and unlock admin checks."
3073
+ }
3074
+ ];
3075
+ }
3076
+ }
2912
3077
  const text2 = await res.text().catch(() => "");
2913
3078
  return [
2914
3079
  {
@@ -3111,7 +3276,17 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
3111
3276
  let mcpRaw = null;
3112
3277
  for (const candidate of candidates) {
3113
3278
  try {
3114
- mcpRaw = await readFile2(candidate, "utf8");
3279
+ const raw = await readFile2(candidate, "utf8");
3280
+ let parsed = {};
3281
+ try {
3282
+ parsed = JSON.parse(raw);
3283
+ } catch {
3284
+ }
3285
+ const hasEntries = Object.keys(parsed.mcpServers ?? {}).length > 0;
3286
+ if (!hasEntries && candidates.indexOf(candidate) < candidates.length - 1) {
3287
+ continue;
3288
+ }
3289
+ mcpRaw = raw;
3115
3290
  mcpPath = candidate;
3116
3291
  break;
3117
3292
  } catch {
@@ -3188,10 +3363,20 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
3188
3363
  detail: `Key is valid; ${projectCount} accessible project${projectCount === 1 ? "" : "s"}`
3189
3364
  });
3190
3365
  } else {
3366
+ let detail = `GET /v1/admin/mcp/account-overview \u2192 HTTP ${res.status}. Verify the API key is active.`;
3367
+ try {
3368
+ const errBody = await res.json();
3369
+ const nested = errBody["error"];
3370
+ const errCode = nested?.["code"] ?? errBody["code"] ?? "";
3371
+ if (errCode === "INSUFFICIENT_SCOPE") {
3372
+ detail = `Key has report:write scope only \u2014 run \`mushi login --upgrade-scope\` then \`mushi setup\` to get mcp:read.`;
3373
+ }
3374
+ } catch {
3375
+ }
3191
3376
  checks.push({
3192
3377
  name: "[mcp] account-overview reachable",
3193
3378
  ok: false,
3194
- detail: `GET /v1/admin/mcp/account-overview \u2192 HTTP ${res.status}. Verify the API key is active.`
3379
+ detail
3195
3380
  });
3196
3381
  }
3197
3382
  } catch (err) {
@@ -3221,11 +3406,96 @@ function fixHintForCheck(name) {
3221
3406
  if (name.startsWith("[mcp]")) return FIX_HINTS["[mcp]"];
3222
3407
  return void 0;
3223
3408
  }
3409
+ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = globalThis.fetch) {
3410
+ if (!config.apiKey || !config.projectId || !config.endpoint) {
3411
+ if (config.apiKey && config.endpoint) {
3412
+ return {
3413
+ nextAction: "Select or create a project: run `mushi project create` or pick one with `mushi project list`",
3414
+ ctaPath: "/onboarding?tab=steps&setup=cli",
3415
+ done: false
3416
+ };
3417
+ }
3418
+ return {
3419
+ nextAction: "Sign in to Mushi: run `npx mushi-mushi` (browser opens automatically \u2014 do NOT type the code in the terminal)",
3420
+ ctaPath: "/onboarding?tab=steps&setup=cli",
3421
+ done: false
3422
+ };
3423
+ }
3424
+ try {
3425
+ const { readFile: readFile2 } = await import("fs/promises");
3426
+ const { join: join5, resolve: resolve3 } = await import("path");
3427
+ const root = resolve3(cwd);
3428
+ let envContent = "";
3429
+ for (const f of [".env.local", ".env"]) {
3430
+ try {
3431
+ envContent = await readFile2(join5(root, f), "utf8");
3432
+ break;
3433
+ } catch {
3434
+ }
3435
+ }
3436
+ const hasEnvVars = /(?:VITE_|NEXT_PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_)?MUSHI_PROJECT_ID=/.test(envContent);
3437
+ if (!hasEnvVars) {
3438
+ return {
3439
+ nextAction: "Write env vars to your app: run `npx mushi-mushi` or `mushi connect --write-env`",
3440
+ ctaPath: "/onboarding?tab=sdk",
3441
+ done: false
3442
+ };
3443
+ }
3444
+ } catch {
3445
+ }
3446
+ try {
3447
+ const sdkCheck = await checkSdkInstall(cwd);
3448
+ if (sdkCheck && !sdkCheck.ok) {
3449
+ return {
3450
+ nextAction: `Install the Mushi SDK: ${sdkCheck.detail}`,
3451
+ ctaPath: "/onboarding?tab=sdk",
3452
+ done: false
3453
+ };
3454
+ }
3455
+ } catch {
3456
+ }
3457
+ try {
3458
+ const data = await fetchIngestSetup(
3459
+ { endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
3460
+ doFetch
3461
+ );
3462
+ if (data) {
3463
+ const steps = data.steps ?? [];
3464
+ const incomplete = steps.find((s) => s.required && !s.complete);
3465
+ if (incomplete) {
3466
+ const tabMap = {
3467
+ project_created: "/onboarding?tab=steps&setup=cli",
3468
+ api_key_generated: "/onboarding?tab=verify",
3469
+ sdk_installed: "/onboarding?tab=sdk",
3470
+ first_report_received: "/onboarding?tab=verify"
3471
+ };
3472
+ const tab = tabMap[incomplete.id] ?? "/onboarding?tab=verify";
3473
+ return {
3474
+ nextAction: incomplete.hint ?? `Complete setup step: ${incomplete.label}`,
3475
+ ctaPath: tab,
3476
+ done: false
3477
+ };
3478
+ }
3479
+ return {
3480
+ nextAction: "All 4 required setup steps are complete! Open the console to explore.",
3481
+ ctaPath: "/reports",
3482
+ done: true
3483
+ };
3484
+ }
3485
+ } catch {
3486
+ }
3487
+ return {
3488
+ nextAction: "Open the onboarding wizard in the console to check your setup status.",
3489
+ ctaPath: "/onboarding",
3490
+ done: false
3491
+ };
3492
+ }
3224
3493
  async function runDoctor(config, options = {}) {
3225
3494
  const doFetch = options.fetch ?? globalThis.fetch;
3226
3495
  const checks = [];
3227
- const runServer = options.server !== false;
3228
- const runIngest = options.ingest !== false;
3496
+ const isFull = options.full === true;
3497
+ const runServer = isFull || options.server !== false;
3498
+ const runIngest = isFull || options.ingest !== false;
3229
3499
  checks.push(...checkCliConfig(config));
3230
3500
  if (config.endpoint) {
3231
3501
  checks.push(await checkEndpointReachability(config.endpoint, doFetch));
@@ -3240,26 +3510,40 @@ async function runDoctor(config, options = {}) {
3240
3510
  const ingestChecks = await checkIngestSetup(config, doFetch);
3241
3511
  checks.push(...ingestChecks);
3242
3512
  }
3243
- if (options.qaStories) {
3513
+ if (isFull || options.qaStories) {
3244
3514
  const qaChecks = await checkQaStoriesHealth(config, doFetch);
3245
3515
  checks.push(...qaChecks);
3246
3516
  }
3247
- if (options.hostApp) {
3517
+ if (isFull || options.hostApp) {
3248
3518
  const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
3249
3519
  checks.push(...hostChecks);
3250
3520
  }
3251
- if (options.mcp) {
3521
+ if (isFull || options.mcp) {
3252
3522
  const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
3253
3523
  checks.push(...mcpChecks);
3254
3524
  }
3525
+ const heartbeatPassed = checks.some((c) => c.name === "[ingest] Last SDK heartbeat" && c.ok);
3526
+ if (heartbeatPassed) {
3527
+ const idx = checks.findIndex((c) => c.name === "SDK installed in this repo" && !c.ok);
3528
+ if (idx >= 0) {
3529
+ checks[idx] = {
3530
+ ...checks[idx],
3531
+ ok: true,
3532
+ warn: true,
3533
+ detail: "No @mushi-mushi/* found in cwd package.json \u2014 but live heartbeats confirm the SDK is active. Run from your app repo or install with `mushi init`."
3534
+ };
3535
+ }
3536
+ }
3255
3537
  return { checks, ready: checks.every((c) => c.ok) };
3256
3538
  }
3257
3539
  function formatDoctorResult(result) {
3258
3540
  const PASS = "\u2713";
3541
+ const WARN = "\u26A0";
3259
3542
  const FAIL = "\u2717";
3260
3543
  const lines = [];
3261
3544
  for (const c of result.checks) {
3262
- lines.push(`${c.ok ? PASS : FAIL} ${c.name}`);
3545
+ const icon = !c.ok ? FAIL : c.warn ? WARN : PASS;
3546
+ lines.push(`${icon} ${c.name}`);
3263
3547
  if (c.detail) lines.push(` ${c.detail}`);
3264
3548
  if (!c.ok) {
3265
3549
  const hint = fixHintForCheck(c.name);
@@ -3267,8 +3551,14 @@ function formatDoctorResult(result) {
3267
3551
  }
3268
3552
  }
3269
3553
  const failed = result.checks.filter((c) => !c.ok);
3554
+ const warned = result.checks.filter((c) => c.ok && c.warn);
3270
3555
  if (failed.length === 0) {
3271
- lines.push("\nAll checks passed. The CLI is ready.");
3556
+ if (warned.length > 0) {
3557
+ lines.push(`
3558
+ All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`);
3559
+ } else {
3560
+ lines.push("\nAll checks passed. The CLI is ready.");
3561
+ }
3272
3562
  } else {
3273
3563
  lines.push(`
3274
3564
  ${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
@@ -3555,9 +3845,38 @@ Examples:
3555
3845
  ).option(
3556
3846
  "--fix",
3557
3847
  "Apply safe local fixes when checks fail: write missing .env.local lines and wire Cursor MCP config."
3848
+ ).option(
3849
+ "--onboarding",
3850
+ "Focused onboarding check: prints the single next action you need to take to finish SDK setup, with a console deep link."
3851
+ ).option(
3852
+ "--full",
3853
+ "Run ALL check categories (server, ingest, host-app, mcp, qa-stories) in one shot. Good for first-run diagnostics. Exit 0 only when all checks pass."
3558
3854
  ).action(async (opts) => {
3559
3855
  const config = loadConfig();
3560
- const doctorOpts = { cwd: opts.cwd, server: opts.server, ingest: opts.ingest, qaStories: opts.qaStories, hostApp: opts.hostApp, mcp: opts.mcp };
3856
+ if (opts.onboarding) {
3857
+ const cwd = opts.cwd ?? process.cwd();
3858
+ const consoleBase = await resolveConsoleUrl({ cwd });
3859
+ const status = await checkOnboardingStatus(config, consoleBase, cwd);
3860
+ const base = consoleBase.replace(/\/$/, "");
3861
+ if (status.done) {
3862
+ console.log(`\u2713 Setup complete. ${status.nextAction}`);
3863
+ console.log(` ${base}${status.ctaPath}`);
3864
+ } else {
3865
+ console.log(`\u2192 Next: ${status.nextAction}`);
3866
+ console.log(` Open: ${base}${status.ctaPath}`);
3867
+ }
3868
+ if (!status.done) process.exit(1);
3869
+ return;
3870
+ }
3871
+ const doctorOpts = {
3872
+ cwd: opts.cwd,
3873
+ server: opts.server,
3874
+ ingest: opts.ingest,
3875
+ qaStories: opts.qaStories,
3876
+ hostApp: opts.hostApp,
3877
+ mcp: opts.mcp,
3878
+ full: opts.full
3879
+ };
3561
3880
  let result = await runDoctor(config, doctorOpts);
3562
3881
  if (!result.ready && opts.fix && config.apiKey && config.projectId && config.endpoint) {
3563
3882
  const connectResult = await runConnect({
@@ -4641,6 +4960,97 @@ ${"\u2500".repeat(60)}`);
4641
4960
  });
4642
4961
  }
4643
4962
 
4963
+ // src/commands/billing.ts
4964
+ function registerBillingCommands(program2) {
4965
+ program2.command("usage").description("Show diagnoses used / limit / cap for the current billing period").option("--json", "Machine-readable JSON output").addHelpText("after", `
4966
+ Examples:
4967
+ mushi usage
4968
+ mushi usage --json`).action(async (opts) => {
4969
+ const config = requireConfig();
4970
+ const result = await apiCall("/v1/admin/billing/stats", config);
4971
+ if (!result.ok) die(result);
4972
+ const s = result.data;
4973
+ if (opts.json) {
4974
+ console.log(JSON.stringify(s, null, 2));
4975
+ return;
4976
+ }
4977
+ const limitStr = s.diagnosesLimit != null ? String(s.diagnosesLimit) : "unlimited";
4978
+ const pctStr = s.diagnosesUsagePct != null ? `${Math.round(s.diagnosesUsagePct)}%` : "\u2014";
4979
+ const capStr = s.monthlySpendCapUsd != null ? `$${s.monthlySpendCapUsd}/mo` : "none";
4980
+ const badge = s.overDiagnosisQuota ? " \u26A0 OVER LIMIT" : s.approachingDiagnosisQuota ? " \u26A0 approaching limit" : "";
4981
+ console.log(`
4982
+ Diagnoses this period: ${s.diagnosesUsed} / ${limitStr} (${pctStr})${badge}`);
4983
+ console.log(`Plan: ${s.planId ?? "\u2014"}`);
4984
+ console.log(`Spend cap: ${capStr}`);
4985
+ console.log(`Period ends: ${s.periodEnd ? s.periodEnd.slice(0, 10) : "?"}`);
4986
+ if (s.overDiagnosisQuota) {
4987
+ console.log("\n Diagnoses paused \u2014 upgrade at https://kensaur.us/mushi-mushi/admin/billing");
4988
+ } else if (s.approachingDiagnosisQuota) {
4989
+ console.log("\n You're at 80%+ of your included diagnoses. Consider upgrading.");
4990
+ }
4991
+ });
4992
+ const billing = program2.command("billing").description("Show billing summary and manage spend cap");
4993
+ billing.command("status").description("Show full billing summary (plan, usage, cap)").option("--json", "Machine-readable JSON output").addHelpText("after", `
4994
+ Examples:
4995
+ mushi billing status
4996
+ mushi billing status --json`).action(async (opts) => {
4997
+ const config = requireConfig();
4998
+ const result = await apiCall("/v1/admin/billing", config);
4999
+ if (!result.ok) die(result);
5000
+ const s = result.data;
5001
+ if (opts.json) {
5002
+ console.log(JSON.stringify(s, null, 2));
5003
+ return;
5004
+ }
5005
+ const limitStr = s.diagnosesLimit != null ? String(s.diagnosesLimit) : "unlimited";
5006
+ const pctStr = s.diagnosesUsagePct != null ? `${Math.round(s.diagnosesUsagePct)}%` : "\u2014";
5007
+ const capStr = s.monthlySpendCapUsd != null ? `$${s.monthlySpendCapUsd}/mo` : "none";
5008
+ const overageStr = s.overageRateDiagnoses != null ? `$${s.overageRateDiagnoses}/diagnosis` : "N/A";
5009
+ console.log(`
5010
+ Plan: ${s.planId ?? "\u2014"}`);
5011
+ console.log(`Status: ${s.subscriptionStatus ?? "free"}${s.cancelAtPeriodEnd ? " (cancels at period end)" : ""}`);
5012
+ console.log(`Diagnoses: ${s.diagnosesUsed} / ${limitStr} (${pctStr})`);
5013
+ console.log(`Overage: ${overageStr}`);
5014
+ console.log(`Spend cap: ${capStr}`);
5015
+ console.log(`Renews: ${s.periodEnd ? s.periodEnd.slice(0, 10) : "?"}`);
5016
+ });
5017
+ billing.command("cap [usd]").description("Set or clear the monthly spend cap (pass 0 to clear, omit to show current cap)").addHelpText("after", `
5018
+ Examples:
5019
+ mushi billing cap # show current cap
5020
+ mushi billing cap 100 # set $100/mo hard cap
5021
+ mushi billing cap 0 # clear cap`).action(async (usdArg) => {
5022
+ const config = requireConfig();
5023
+ if (usdArg === void 0) {
5024
+ const result2 = await apiCall("/v1/admin/billing/stats", config);
5025
+ if (!result2.ok) die(result2);
5026
+ const cap2 = result2.data.monthlySpendCapUsd;
5027
+ if (cap2 == null) {
5028
+ console.log("No spend cap set. Pass a dollar amount to set one, e.g.: mushi billing cap 100");
5029
+ } else {
5030
+ console.log(`Current spend cap: $${cap2}/mo`);
5031
+ }
5032
+ return;
5033
+ }
5034
+ const capUsd = parseFloat(usdArg);
5035
+ if (isNaN(capUsd) || capUsd < 0) {
5036
+ console.error("Error: spend cap must be a non-negative number (or 0 to clear).");
5037
+ process.exit(2);
5038
+ }
5039
+ const result = await apiCall("/v1/admin/billing/spend-cap", config, {
5040
+ method: "PUT",
5041
+ headers: { "Content-Type": "application/json" },
5042
+ body: JSON.stringify({ project_id: config.projectId, spend_cap_usd: capUsd === 0 ? null : capUsd })
5043
+ });
5044
+ if (!result.ok) die(result);
5045
+ const cap = result.data.spend_cap_usd;
5046
+ if (cap == null) {
5047
+ console.log("\u2713 Spend cap cleared \u2014 no hard monthly limit.");
5048
+ } else {
5049
+ console.log(`\u2713 Spend cap set to $${cap}/mo. Diagnoses will pause gracefully once reached.`);
5050
+ }
5051
+ });
5052
+ }
5053
+
4644
5054
  // src/index.ts
4645
5055
  installSignalHandlers();
4646
5056
  var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 set up the SDK, manage bug reports, monitor pipeline").version(MUSHI_CLI_VERSION).addHelpText("after", `
@@ -4673,6 +5083,7 @@ registerIntegrationsCommands(program);
4673
5083
  registerQaCommands(program);
4674
5084
  registerAuditCommands(program);
4675
5085
  registerSkillsCommands(program);
5086
+ registerBillingCommands(program);
4676
5087
  program.parseAsync().catch((err) => {
4677
5088
  console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
4678
5089
  process.exit(1);