@mushi-mushi/cli 0.29.2 → 0.30.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/dist/index.js CHANGED
@@ -53,11 +53,11 @@ import {
53
53
  setGlobalOutputFormat,
54
54
  trySaveKeyToKeychain,
55
55
  writeMcpServerEntry
56
- } from "./chunk-KZW4X6WB.js";
56
+ } from "./chunk-5PAFWBPL.js";
57
57
  import {
58
58
  MUSHI_CLI_VERSION,
59
59
  MUSHI_MCP_PIN_SPEC
60
- } from "./chunk-MKUU3GHS.js";
60
+ } from "./chunk-UVAVIRFO.js";
61
61
  import {
62
62
  FRAMEWORKS,
63
63
  detectFramework,
@@ -273,11 +273,11 @@ async function createProject(endpoint, cliToken, name, opts = {}) {
273
273
  const { id, name: created, slug, apiKey } = json2.data;
274
274
  return { id, name: created, slug, apiKey: apiKey ?? null };
275
275
  }
276
- async function mintProjectKey(endpoint, cliToken, projectId, opts = {}) {
276
+ async function mintProjectKey(endpoint, cliToken, projectId2, opts = {}) {
277
277
  const base = trimTrailingSlash(endpoint);
278
278
  let res;
279
279
  try {
280
- res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
280
+ res = await deviceFetch(`${base}/v1/cli/projects/${projectId2}/keys`, {
281
281
  method: "POST",
282
282
  headers: cliAuthHeaders(cliToken),
283
283
  body: JSON.stringify({
@@ -299,9 +299,9 @@ async function mintProjectKey(endpoint, cliToken, projectId, opts = {}) {
299
299
  res.status
300
300
  );
301
301
  }
302
- async function revokeProjectKey(endpoint, cliToken, projectId, keyPrefix, newKeyId) {
302
+ async function revokeProjectKey(endpoint, cliToken, projectId2, keyPrefix, newKeyId) {
303
303
  const base = trimTrailingSlash(endpoint);
304
- const url = `${base}/v1/cli/projects/${encodeURIComponent(projectId)}/keys/${encodeURIComponent(keyPrefix)}/revoke`;
304
+ const url = `${base}/v1/cli/projects/${encodeURIComponent(projectId2)}/keys/${encodeURIComponent(keyPrefix)}/revoke`;
305
305
  let res;
306
306
  try {
307
307
  res = await deviceFetch(url, {
@@ -469,17 +469,17 @@ async function runLogin(opts = {}) {
469
469
  const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT2;
470
470
  const consoleBase = await resolveConsoleUrl();
471
471
  let apiKey = opts.apiKey;
472
- let projectId = opts.projectId;
472
+ let projectId2 = opts.projectId;
473
473
  if (apiKey) {
474
474
  const config2 = loadConfig();
475
475
  config2.apiKey = apiKey;
476
476
  config2.endpoint = endpoint;
477
- if (projectId) config2.projectId = projectId;
477
+ if (projectId2) config2.projectId = projectId2;
478
478
  config2.consoleUrl = consoleBase;
479
479
  saveConfig(config2);
480
480
  console.log("");
481
481
  console.log(" \u2713 Credentials saved.");
482
- const verifyResult = await apiCall("/v1/sync/whoami", { apiKey, endpoint, projectId });
482
+ const verifyResult = await apiCall("/v1/sync/whoami", { apiKey, endpoint, projectId: projectId2 });
483
483
  if (!verifyResult.ok) {
484
484
  console.warn(` \u26A0 Key saved, but verification failed: ${verifyResult.error?.message ?? "unknown error"}`);
485
485
  console.warn(` Check your key at: ${cliSetupDeepLink(consoleBase)}`);
@@ -487,7 +487,7 @@ async function runLogin(opts = {}) {
487
487
  }
488
488
  const d = verifyResult.data;
489
489
  if (d.project_name) {
490
- if (!projectId && d.project_id) {
490
+ if (!projectId2 && d.project_id) {
491
491
  config2.projectId = d.project_id;
492
492
  saveConfig(config2);
493
493
  }
@@ -544,7 +544,7 @@ error: Could not start login session: ${err instanceof Error ? err.message : Str
544
544
  const cliToken = pollResult.cliToken;
545
545
  printAuthApproved();
546
546
  const projectsList = await listProjects(endpoint, cliToken);
547
- let chosenProjectId = projectId;
547
+ let chosenProjectId = projectId2;
548
548
  let chosenProjectName;
549
549
  const mintScopes = opts.upgradeScope ? FULL_CLI_KEY_SCOPES : CLI_KEY_SCOPES;
550
550
  if (!chosenProjectId) {
@@ -589,12 +589,24 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
589
589
  }
590
590
  }
591
591
  }
592
+ let mintError = null;
592
593
  if (!apiKey && chosenProjectId) {
593
594
  try {
594
595
  apiKey = await mintProjectKey(endpoint, cliToken, chosenProjectId, { scopes: mintScopes }) ?? void 0;
595
- } catch {
596
+ } catch (err) {
597
+ mintError = err instanceof Error ? err.message : String(err);
596
598
  }
597
599
  }
600
+ if (mintError) {
601
+ process.stderr.write(`
602
+ \u2717 No key was created for ${chosenProjectName ?? chosenProjectId}: ${mintError}
603
+ `);
604
+ if (/owner or admin|forbidden|403/i.test(mintError)) {
605
+ process.stderr.write(" Only the project's owners and admins can create keys. Ask one of them to make you an admin, then run this again.\n");
606
+ }
607
+ process.stderr.write(" Your saved login was not changed.\n\n");
608
+ process.exit(1);
609
+ }
598
610
  const config = loadConfig();
599
611
  config.endpoint = endpoint;
600
612
  config.consoleUrl = consoleBase;
@@ -895,9 +907,9 @@ async function checkCliAuthPath(config, doFetch = globalThis.fetch) {
895
907
  async function checkSdkInstall(cwd) {
896
908
  try {
897
909
  const { readFile: readFile3 } = await import("fs/promises");
898
- const { join: join10, resolve: resolve7 } = await import("path");
910
+ const { join: join11, resolve: resolve7 } = await import("path");
899
911
  const root = resolve7(cwd);
900
- const pkgPath = join10(root, "package.json");
912
+ const pkgPath = join11(root, "package.json");
901
913
  const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
902
914
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
903
915
  const sdks = [
@@ -932,9 +944,9 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
932
944
  ];
933
945
  }
934
946
  try {
935
- const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
936
- const res = await doFetch(`${endpoint}/v1/admin/projects/${projectId}/preflight`, {
937
- headers: apiKeyHeaders(apiKey, projectId),
947
+ const { endpoint, apiKey, projectId: projectId2 } = sanitizeCliCredentials(config);
948
+ const res = await doFetch(`${endpoint}/v1/admin/projects/${projectId2}/preflight`, {
949
+ headers: apiKeyHeaders(apiKey, projectId2),
938
950
  signal: AbortSignal.timeout(8e3)
939
951
  });
940
952
  if (res.ok) {
@@ -1032,9 +1044,9 @@ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
1032
1044
  }
1033
1045
  const checks = [];
1034
1046
  try {
1035
- const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
1036
- const headers = apiKeyHeaders(apiKey, projectId);
1037
- const storiesRes = await doFetch(`${endpoint}/v1/admin/projects/${projectId}/qa-coverage`, {
1047
+ const { endpoint, apiKey, projectId: projectId2 } = sanitizeCliCredentials(config);
1048
+ const headers = apiKeyHeaders(apiKey, projectId2);
1049
+ const storiesRes = await doFetch(`${endpoint}/v1/admin/projects/${projectId2}/qa-coverage`, {
1038
1050
  headers,
1039
1051
  signal: AbortSignal.timeout(8e3)
1040
1052
  });
@@ -1063,7 +1075,7 @@ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
1063
1075
  detail: `${enabled.length} enabled story/stories configured`
1064
1076
  });
1065
1077
  const slackRes = await doFetch(
1066
- `${endpoint}/v1/admin/projects/${projectId}/integrations/probe/slack`,
1078
+ `${endpoint}/v1/admin/projects/${projectId2}/integrations/probe/slack`,
1067
1079
  {
1068
1080
  method: "POST",
1069
1081
  headers,
@@ -1078,7 +1090,7 @@ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
1078
1090
  detail: slackOk ? "Slack connected \u2014 failures will notify your channel" : "Slack not connected \u2014 you won't be notified when stories fail. Visit /integrations \u2192 Add to Slack."
1079
1091
  });
1080
1092
  const fcRes = await doFetch(
1081
- `${endpoint}/v1/admin/projects/${projectId}/integrations/probe/firecrawl`,
1093
+ `${endpoint}/v1/admin/projects/${projectId2}/integrations/probe/firecrawl`,
1082
1094
  {
1083
1095
  method: "POST",
1084
1096
  headers,
@@ -1107,9 +1119,9 @@ async function checkHostAppWiring(cwd) {
1107
1119
  const checks = [];
1108
1120
  try {
1109
1121
  const { readFile: readFile3, access } = await import("fs/promises");
1110
- const { join: join10, resolve: resolve7 } = await import("path");
1122
+ const { join: join11, resolve: resolve7 } = await import("path");
1111
1123
  const root = resolve7(cwd);
1112
- const pkgPath = join10(root, "package.json");
1124
+ const pkgPath = join11(root, "package.json");
1113
1125
  const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
1114
1126
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1115
1127
  const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
@@ -1117,7 +1129,7 @@ async function checkHostAppWiring(cwd) {
1117
1129
  let envContent = "";
1118
1130
  for (const f of envCandidates) {
1119
1131
  try {
1120
- envContent = await readFile3(join10(root, f), "utf8");
1132
+ envContent = await readFile3(join11(root, f), "utf8");
1121
1133
  break;
1122
1134
  } catch {
1123
1135
  }
@@ -1132,9 +1144,9 @@ async function checkHostAppWiring(cwd) {
1132
1144
  detail: hasProjectId && hasApiKey ? "VITE_/MUSHI_ project id + API key found" : "Run `mushi connect --write-env` or add VITE_MUSHI_PROJECT_ID + VITE_MUSHI_API_KEY"
1133
1145
  });
1134
1146
  let mcpFound = null;
1135
- for (const rel of [".mcp.json", join10(".cursor", "mcp.json")]) {
1147
+ for (const rel of [".mcp.json", join11(".cursor", "mcp.json")]) {
1136
1148
  try {
1137
- await access(join10(root, rel));
1149
+ await access(join11(root, rel));
1138
1150
  mcpFound = rel;
1139
1151
  break;
1140
1152
  } catch {
@@ -1176,7 +1188,7 @@ var HASH_ROUTER_PATTERNS = [
1176
1188
  var HASH_TEMPLATE_PATTERN = /['"`]#\//;
1177
1189
  async function scanDirForPattern(dir, patterns, maxDepth = 6, maxFiles = 2e3) {
1178
1190
  const { readdir: readdir2, readFile: readFile3 } = await import("fs/promises");
1179
- const { join: join10 } = await import("path");
1191
+ const { join: join11 } = await import("path");
1180
1192
  let count = 0;
1181
1193
  async function walk2(d, depth) {
1182
1194
  if (depth > maxDepth || count > maxFiles) return null;
@@ -1188,7 +1200,7 @@ async function scanDirForPattern(dir, patterns, maxDepth = 6, maxFiles = 2e3) {
1188
1200
  }
1189
1201
  for (const e of entries) {
1190
1202
  if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist") continue;
1191
- const full = join10(d, e.name);
1203
+ const full = join11(d, e.name);
1192
1204
  if (e.isDirectory()) {
1193
1205
  const found = await walk2(full, depth + 1);
1194
1206
  if (found) return found;
@@ -1205,7 +1217,7 @@ async function scanDirForPattern(dir, patterns, maxDepth = 6, maxFiles = 2e3) {
1205
1217
  }
1206
1218
  const { resolve: resolve7 } = await import("path");
1207
1219
  for (const sub of ["src", "app", "pages", "lib", "components"]) {
1208
- const found = await walk2(join10(resolve7(dir), sub), 0);
1220
+ const found = await walk2(join11(resolve7(dir), sub), 0);
1209
1221
  if (found) return found;
1210
1222
  }
1211
1223
  return walk2(resolve7(dir), 0);
@@ -1241,13 +1253,13 @@ function normalizeProjectPath(path) {
1241
1253
  }
1242
1254
  async function collectMcpSources(root, home) {
1243
1255
  const { readFile: readFile3 } = await import("fs/promises");
1244
- const { join: join10 } = await import("path");
1256
+ const { join: join11 } = await import("path");
1245
1257
  const sources = [];
1246
1258
  const invalid = [];
1247
1259
  const plainFiles = [
1248
- { path: join10(root, ".mcp.json"), claudeCode: true },
1249
- { path: join10(root, ".cursor", "mcp.json"), claudeCode: false },
1250
- { path: join10(home, ".cursor", "mcp.json"), claudeCode: false }
1260
+ { path: join11(root, ".mcp.json"), claudeCode: true },
1261
+ { path: join11(root, ".cursor", "mcp.json"), claudeCode: false },
1262
+ { path: join11(home, ".cursor", "mcp.json"), claudeCode: false }
1251
1263
  ];
1252
1264
  for (const file of plainFiles) {
1253
1265
  let raw;
@@ -1264,7 +1276,7 @@ async function collectMcpSources(root, home) {
1264
1276
  invalid.push(file.path);
1265
1277
  }
1266
1278
  }
1267
- const claudeJsonPath = join10(home, ".claude.json");
1279
+ const claudeJsonPath = join11(home, ".claude.json");
1268
1280
  try {
1269
1281
  const parsed = JSON.parse(await readFile3(claudeJsonPath, "utf8"));
1270
1282
  if (isJsonObject(parsed)) {
@@ -1441,12 +1453,12 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
1441
1453
  }
1442
1454
  try {
1443
1455
  const { readFile: readFile3 } = await import("fs/promises");
1444
- const { join: join10, resolve: resolve7 } = await import("path");
1456
+ const { join: join11, resolve: resolve7 } = await import("path");
1445
1457
  const root = resolve7(cwd);
1446
1458
  let envContent = "";
1447
1459
  for (const f of [".env.local", ".env"]) {
1448
1460
  try {
1449
- envContent = await readFile3(join10(root, f), "utf8");
1461
+ envContent = await readFile3(join11(root, f), "utf8");
1450
1462
  break;
1451
1463
  } catch {
1452
1464
  }
@@ -1512,7 +1524,7 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
1512
1524
  }
1513
1525
  async function checkVersionDrift(cwd, doFetch) {
1514
1526
  const checks = [];
1515
- const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version-QRFWGGZY.js");
1527
+ const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version-A76DBKDU.js");
1516
1528
  if (MUSHI_CLI_VERSION2 === "0.0.0-dev") return checks;
1517
1529
  const parse2 = (v) => {
1518
1530
  const m = v.replace(/^[~^]/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
@@ -1542,8 +1554,8 @@ async function checkVersionDrift(cwd, doFetch) {
1542
1554
  }
1543
1555
  try {
1544
1556
  const { readFile: readFile3 } = await import("fs/promises");
1545
- const { join: join10 } = await import("path");
1546
- const pkg = JSON.parse(await readFile3(join10(cwd, "package.json"), "utf8"));
1557
+ const { join: join11 } = await import("path");
1558
+ const pkg = JSON.parse(await readFile3(join11(cwd, "package.json"), "utf8"));
1547
1559
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1548
1560
  const candidates = Object.entries(deps).filter(([name]) => name.startsWith("@mushi-mushi/")).filter(([, v]) => /^[~^]?\d/.test(v));
1549
1561
  const drifted = (await Promise.all(
@@ -1847,10 +1859,10 @@ async function chooseFramework(detected, options) {
1847
1859
  async function acquireCredentials(options, consoleBase, endpoint) {
1848
1860
  const probe = (e, k, pid) => probeKeyScope(e, k, pid);
1849
1861
  if (options.projectId && options.apiKey) {
1850
- const projectId = sanitizeSecret(options.projectId);
1862
+ const projectId2 = sanitizeSecret(options.projectId);
1851
1863
  const sdkKey = sanitizeSecret(options.apiKey);
1852
- await assertUserKeyIsIngestOnly(sdkKey, projectId, endpoint, probe);
1853
- return { projectId, sdkKey };
1864
+ await assertUserKeyIsIngestOnly(sdkKey, projectId2, endpoint, probe);
1865
+ return { projectId: projectId2, sdkKey };
1854
1866
  }
1855
1867
  const existing = loadConfig();
1856
1868
  maybeShowTelemetryNotice(existing);
@@ -1927,8 +1939,8 @@ async function acquireCredentials(options, consoleBase, endpoint) {
1927
1939
  }
1928
1940
  return collectCredentialsManually(options, consoleBase, endpoint, probe);
1929
1941
  }
1930
- async function assertUserKeyIsIngestOnly(key, projectId, endpoint, probe) {
1931
- const result = await probe(endpoint, key, projectId);
1942
+ async function assertUserKeyIsIngestOnly(key, projectId2, endpoint, probe) {
1943
+ const result = await probe(endpoint, key, projectId2);
1932
1944
  if (result.result === "mcp" || result.result === "unknown") {
1933
1945
  throw new Error(
1934
1946
  `Not writing this API key into your app's env vars: ${describeUnsafeSdkKey(result, endpoint)}. Use an ingest-only (report:write) key from the console, or leave out --api-key to sign in with the browser, which mints one.`
@@ -1936,40 +1948,40 @@ async function assertUserKeyIsIngestOnly(key, projectId, endpoint, probe) {
1936
1948
  }
1937
1949
  }
1938
1950
  async function resolveSavedCredentials(saved, endpoint, probe) {
1939
- const projectId = sanitizeSecret(saved.projectId);
1951
+ const projectId2 = sanitizeSecret(saved.projectId);
1940
1952
  const apiKey = sanitizeSecret(saved.apiKey);
1941
- const savedSdkKey = savedSdkKeyFor(saved, projectId);
1953
+ const savedSdkKey = savedSdkKeyFor(saved, projectId2);
1942
1954
  if (savedSdkKey) {
1943
- const sdkProbe = await probe(endpoint, savedSdkKey, projectId);
1955
+ const sdkProbe = await probe(endpoint, savedSdkKey, projectId2);
1944
1956
  if (sdkProbe.result === "ingest-only") {
1945
- return { kind: "ready", credentials: { projectId, sdkKey: savedSdkKey, cliKey: apiKey } };
1957
+ return { kind: "ready", credentials: { projectId: projectId2, sdkKey: savedSdkKey, cliKey: apiKey } };
1946
1958
  }
1947
1959
  }
1948
- const cliProbe = await probe(endpoint, apiKey, projectId);
1960
+ const cliProbe = await probe(endpoint, apiKey, projectId2);
1949
1961
  if (cliProbe.result === "ingest-only") {
1950
- return { kind: "ready", credentials: { projectId, sdkKey: apiKey } };
1962
+ return { kind: "ready", credentials: { projectId: projectId2, sdkKey: apiKey } };
1951
1963
  }
1952
1964
  if (cliProbe.result === "mcp") {
1953
- return { kind: "needs-sdk-key", projectId, cliKey: apiKey };
1965
+ return { kind: "needs-sdk-key", projectId: projectId2, cliKey: apiKey };
1954
1966
  }
1955
1967
  return { kind: "unusable", reason: describeUnsafeSdkKey(cliProbe, endpoint) };
1956
1968
  }
1957
1969
  async function mintWizardKeys(deps, args) {
1958
- let projectId;
1970
+ let projectId2;
1959
1971
  let createdName;
1960
1972
  let sdkKey = null;
1961
1973
  if (args.target.kind === "new") {
1962
1974
  const created = await deps.createProject(args.endpoint, args.cliToken, args.target.name, {
1963
1975
  scopes: SDK_KEY_SCOPES
1964
1976
  });
1965
- projectId = created.id;
1977
+ projectId2 = created.id;
1966
1978
  createdName = created.name;
1967
1979
  sdkKey = created.apiKey;
1968
1980
  } else {
1969
- projectId = args.target.projectId;
1981
+ projectId2 = args.target.projectId;
1970
1982
  }
1971
1983
  if (!sdkKey) {
1972
- sdkKey = await deps.mintProjectKey(args.endpoint, args.cliToken, projectId, {
1984
+ sdkKey = await deps.mintProjectKey(args.endpoint, args.cliToken, projectId2, {
1973
1985
  scopes: SDK_KEY_SCOPES,
1974
1986
  label: SDK_KEY_LABEL
1975
1987
  });
@@ -1978,7 +1990,7 @@ async function mintWizardKeys(deps, args) {
1978
1990
  let cliKeyError;
1979
1991
  if (args.withCliKey) {
1980
1992
  try {
1981
- cliKey = await deps.mintProjectKey(args.endpoint, args.cliToken, projectId, {
1993
+ cliKey = await deps.mintProjectKey(args.endpoint, args.cliToken, projectId2, {
1982
1994
  scopes: CLI_KEY_SCOPES,
1983
1995
  label: CLI_KEY_LABEL
1984
1996
  });
@@ -1986,7 +1998,7 @@ async function mintWizardKeys(deps, args) {
1986
1998
  cliKeyError = err instanceof Error ? err.message : String(err);
1987
1999
  }
1988
2000
  }
1989
- return { projectId, createdName, sdkKey, cliKey, cliKeyError };
2001
+ return { projectId: projectId2, createdName, sdkKey, cliKey, cliKeyError };
1990
2002
  }
1991
2003
  async function runBrowserSignIn(options, endpoint, consoleBase, preset) {
1992
2004
  const startSpin = p.spinner();
@@ -2155,11 +2167,11 @@ async function collectCredentialsManually(options, consoleBase, endpoint, probe)
2155
2167
  hint: apiKeyHint(consoleBase),
2156
2168
  validate: (v) => API_KEY_PATTERN.test(v) ? void 0 : "Expected format: mushi_ followed by 10+ alphanumeric characters"
2157
2169
  });
2158
- const projectId = sanitizeSecret(rawProjectId);
2170
+ const projectId2 = sanitizeSecret(rawProjectId);
2159
2171
  const apiKey = sanitizeSecret(rawApiKey);
2160
- if (!PROJECT_ID_PATTERN.test(projectId)) {
2172
+ if (!PROJECT_ID_PATTERN.test(projectId2)) {
2161
2173
  throw new Error(
2162
- `Invalid project ID. Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) or the proj_* prefixed form. Got: ${redact(projectId)} \u2014 copy it from ${projectIdHint(consoleBase)}`
2174
+ `Invalid project ID. Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) or the proj_* prefixed form. Got: ${redact(projectId2)} \u2014 copy it from ${projectIdHint(consoleBase)}`
2163
2175
  );
2164
2176
  }
2165
2177
  if (!API_KEY_PATTERN.test(apiKey)) {
@@ -2168,9 +2180,9 @@ async function collectCredentialsManually(options, consoleBase, endpoint, probe)
2168
2180
  );
2169
2181
  }
2170
2182
  if (apiKey !== savedApiKey) {
2171
- await assertUserKeyIsIngestOnly(apiKey, projectId, endpoint, probe);
2183
+ await assertUserKeyIsIngestOnly(apiKey, projectId2, endpoint, probe);
2172
2184
  }
2173
- return { projectId, sdkKey: apiKey };
2185
+ return { projectId: projectId2, sdkKey: apiKey };
2174
2186
  }
2175
2187
  async function verifyCredentials(credentials, options, consoleBase) {
2176
2188
  const endpoint = resolveCloudEndpoint(options.endpoint);
@@ -2267,10 +2279,10 @@ function runCommand(pm, packages, cwd) {
2267
2279
  });
2268
2280
  });
2269
2281
  }
2270
- async function writeEnvFile(cwd, apiKey, projectId, framework, endpoint, overwrite) {
2282
+ async function writeEnvFile(cwd, apiKey, projectId2, framework, endpoint, overwrite) {
2271
2283
  const target = ENV_FILES.find((f) => existsSync2(join2(cwd, f))) ?? ENV_FILES[0];
2272
2284
  const targetPath = join2(cwd, target);
2273
- const newVars = envVarsToWrite(apiKey, projectId, framework, endpoint);
2285
+ const newVars = envVarsToWrite(apiKey, projectId2, framework, endpoint);
2274
2286
  let existing = "";
2275
2287
  try {
2276
2288
  existing = readFileSync2(targetPath, "utf-8");
@@ -2436,7 +2448,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
2436
2448
  if (p.isCancel(answer) || !answer) return;
2437
2449
  const endpoint = resolveCloudEndpoint(options.endpoint);
2438
2450
  try {
2439
- const { runConnect: runConnect2 } = await import("./connect-NCBKOV6O.js");
2451
+ const { runConnect: runConnect2 } = await import("./connect-YK4ZIGTY.js");
2440
2452
  const saved = loadConfig();
2441
2453
  const result = await runConnect2(
2442
2454
  {
@@ -3736,7 +3748,7 @@ error: Could not start browser sign-in: ${err instanceof Error ? err.message : S
3736
3748
  const rl = createInterface({ input: process.stdin, output: process.stdout });
3737
3749
  const ask = (q) => new Promise((resolve7) => rl.question(q, (a) => resolve7(a.trim())));
3738
3750
  let chosen;
3739
- let projectId;
3751
+ let projectId2;
3740
3752
  let projectName;
3741
3753
  let apiKey;
3742
3754
  if (!opts.name && projectsList.length > 0) {
@@ -3751,11 +3763,11 @@ error: Could not start browser sign-in: ${err instanceof Error ? err.message : S
3751
3763
  const num2 = parseInt(choice, 10);
3752
3764
  if (num2 >= 1 && num2 <= projectsList.length) {
3753
3765
  chosen = projectsList[num2 - 1];
3754
- projectId = chosen.id;
3766
+ projectId2 = chosen.id;
3755
3767
  projectName = chosen.name;
3756
3768
  }
3757
3769
  }
3758
- if (!projectId) {
3770
+ if (!projectId2) {
3759
3771
  const newName = opts.name?.trim() || await ask(" Project name: ");
3760
3772
  if (!newName) {
3761
3773
  rl.close();
@@ -3764,7 +3776,7 @@ error: Could not start browser sign-in: ${err instanceof Error ? err.message : S
3764
3776
  }
3765
3777
  try {
3766
3778
  const created = await createProject(endpoint, cliToken, newName);
3767
- projectId = created.id;
3779
+ projectId2 = created.id;
3768
3780
  projectName = created.name;
3769
3781
  apiKey = created.apiKey ?? void 0;
3770
3782
  console.log(` \u2713 Created project "${projectName}"`);
@@ -3777,22 +3789,22 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
3777
3789
  }
3778
3790
  }
3779
3791
  rl.close();
3780
- if (projectId && !apiKey) {
3781
- apiKey = await mintProjectKey(endpoint, cliToken, projectId) ?? void 0;
3792
+ if (projectId2 && !apiKey) {
3793
+ apiKey = await mintProjectKey(endpoint, cliToken, projectId2) ?? void 0;
3782
3794
  }
3783
- if (!projectId || !apiKey) {
3795
+ if (!projectId2 || !apiKey) {
3784
3796
  process.stderr.write("\nerror: Could not obtain an SDK key for the project. Run `mushi login` and try again.\n");
3785
3797
  process.exit(1);
3786
3798
  }
3787
3799
  const config = loadConfig();
3788
3800
  config.apiKey = apiKey;
3789
3801
  config.endpoint = endpoint;
3790
- config.projectId = projectId;
3802
+ config.projectId = projectId2;
3791
3803
  config.consoleUrl = consoleBase;
3792
3804
  saveConfig(config);
3793
3805
  const { envUpdated, mcpUpdated } = await writeProjectBootstrapFiles({
3794
3806
  endpoint,
3795
- projectId,
3807
+ projectId: projectId2,
3796
3808
  apiKey
3797
3809
  });
3798
3810
  console.log(`
@@ -5079,9 +5091,9 @@ function registerDoctorCommand(program2) {
5079
5091
  function registerResetCommand(program2) {
5080
5092
  program2.command("reset [projectId]").description(
5081
5093
  "Archive a project and wipe its test data (codebase_files, fix_attempts, reports). Speeds up re-running the full onboarding flow from scratch. Requires `--confirm` to prevent accidents."
5082
- ).option("--confirm", "Required safety flag \u2014 must pass to proceed").option("--json", "Machine-readable output").action(async (projectId, opts) => {
5094
+ ).option("--confirm", "Required safety flag \u2014 must pass to proceed").option("--json", "Machine-readable output").action(async (projectId2, opts) => {
5083
5095
  const config = loadConfig();
5084
- const resolvedId = projectId ?? config.projectId;
5096
+ const resolvedId = projectId2 ?? config.projectId;
5085
5097
  if (!config.apiKey) {
5086
5098
  console.error("Run `mushi login` first");
5087
5099
  process.exit(1);
@@ -5383,7 +5395,7 @@ function registerKeysCommands(program2) {
5383
5395
  }
5384
5396
  );
5385
5397
  });
5386
- keys.command("add").description("Add a new API key to the pool").requiredOption("--provider <p>", "Provider: anthropic, openai, firecrawl, browserbase, cursor, supabase").option(
5398
+ keys.command("add").description("Add a new API key to the pool").requiredOption("--provider <p>", "Provider: anthropic, openai, openrouter, firecrawl, browserbase, cursor, supabase").option(
5387
5399
  "--key <k>",
5388
5400
  "The API key value (prefer the MUSHI_BYOK_KEY env var to keep it out of shell history)"
5389
5401
  ).option("--label <l>", "Human-readable label").option("--priority <n>", "Priority (lower = higher priority)", "100").option("--base-url <url>", "Allow-listed OpenAI-compatible HTTPS base URL").action(
@@ -5872,11 +5884,11 @@ Examples:
5872
5884
  throw new MushiCliError("E_PROJECT_MISSING", "Project ID not configured.", "pass --project-id <id> or run `mushi login --project-id <id>`");
5873
5885
  }
5874
5886
  let endpoint;
5875
- let projectId;
5887
+ let projectId2;
5876
5888
  let apiKey;
5877
5889
  try {
5878
5890
  endpoint = sanitizeEndpoint(config.endpoint);
5879
- projectId = sanitizeProjectId(rawProjectId);
5891
+ projectId2 = sanitizeProjectId(rawProjectId);
5880
5892
  apiKey = sanitizeApiKey(config.apiKey);
5881
5893
  } catch (err) {
5882
5894
  throw new MushiCliError("E_INVALID_INPUT", err instanceof Error ? err.message : String(err));
@@ -5884,9 +5896,9 @@ Examples:
5884
5896
  const json2 = outputIsJson(opts.json);
5885
5897
  if (!json2) process.stdout.write("Running full-stack audit\u2026 ");
5886
5898
  const result = await apiCall(
5887
- `/v1/admin/projects/${projectId}/audit`,
5899
+ `/v1/admin/projects/${projectId2}/audit`,
5888
5900
  { ...config, endpoint, apiKey },
5889
- { method: "POST", body: "{}", headers: { "X-Mushi-Project-Id": projectId } },
5901
+ { method: "POST", body: "{}", headers: { "X-Mushi-Project-Id": projectId2 } },
5890
5902
  { timeoutMs: AUDIT_TIMEOUT_MS }
5891
5903
  );
5892
5904
  if (!result.ok) {
@@ -5926,6 +5938,14 @@ Examples:
5926
5938
  console.log(` ${g} ${run2.gate.padEnd(22)} ${run2.status} (${run2.findings_count} finding${run2.findings_count !== 1 ? "s" : ""})`);
5927
5939
  }
5928
5940
  }
5941
+ const refresh = data.gate_refresh;
5942
+ if (refresh && (refresh.triggered.length > 0 || refresh.skipped.length > 0)) {
5943
+ console.log("\nGate re-runs:");
5944
+ if (refresh.triggered.length > 0) {
5945
+ console.log(` Started: ${refresh.triggered.join(", ")} (results show in the next audit)`);
5946
+ }
5947
+ for (const s of refresh.skipped) console.log(` Not re-run: ${s.gate.padEnd(16)} ${oneLine(s.reason, 110)}`);
5948
+ }
5929
5949
  console.log("\nEach gate finding with its file and line: mushi audit findings");
5930
5950
  if (data.summary.overall === "fail") process.exit(1);
5931
5951
  });
@@ -5939,12 +5959,12 @@ Examples:
5939
5959
  throw new MushiCliError("E_INVALID_INPUT", "--severity must be info, warn or error");
5940
5960
  }
5941
5961
  const config = requireConfig();
5942
- const projectId = resolveProjectId(opts.projectId, config.projectId);
5962
+ const projectId2 = resolveProjectId(opts.projectId, config.projectId);
5943
5963
  const qs = new URLSearchParams();
5944
5964
  if (opts.gate) qs.set("gate", opts.gate);
5945
5965
  if (opts.severity) qs.set("severity", opts.severity);
5946
5966
  const suffix = qs.toString() ? `?${qs}` : "";
5947
- const result = await apiCall(`/v1/admin/inventory/${projectId}/findings${suffix}`, config);
5967
+ const result = await apiCall(`/v1/admin/inventory/${projectId2}/findings${suffix}`, config);
5948
5968
  if (!result.ok) {
5949
5969
  if (result.error.code === "feature_not_in_plan") {
5950
5970
  process.stderr.write("The per-finding list is not on your plan. `mushi audit` (no subcommand) still runs the summary audit.\n");
@@ -6076,7 +6096,8 @@ Description:
6076
6096
  );
6077
6097
  if (!result.ok) {
6078
6098
  console.error(" Sync failed:", result.error);
6079
- } else console.log(` Done: ${result.data?.synced ?? 0} synced, ${result.data?.skipped ?? 0} skipped, ${result.data?.errors ?? 0} errors`);
6099
+ } else if (result.data?.status === "running") console.log(" Still running in the background; `mushi skills list` shows the result in a few minutes.");
6100
+ else console.log(` Done: ${result.data?.synced ?? 0} synced, ${result.data?.skipped ?? 0} skipped, ${result.data?.errors ?? 0} errors`);
6080
6101
  }
6081
6102
  console.log();
6082
6103
  });
@@ -7316,8 +7337,8 @@ Add one step to your existing CI job, after the build step (no new job needed):
7316
7337
  });
7317
7338
  radar.command("show").description("Show every hole check for this project and what each one found").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (opts) => {
7318
7339
  const config = requireConfig({ needsProject: !opts.projectId });
7319
- const projectId = requireUuid(opts.projectId ?? config.projectId, "project id");
7320
- const result = await apiCall(`/v1/admin/projects/${projectId}/radar`, config);
7340
+ const projectId2 = requireUuid(opts.projectId ?? config.projectId, "project id");
7341
+ const result = await apiCall(`/v1/admin/projects/${projectId2}/radar`, config);
7321
7342
  if (!result.ok) die(result);
7322
7343
  if (outputIsJson(opts.json)) {
7323
7344
  console.log(JSON.stringify(result.data, null, 2));
@@ -9088,8 +9109,8 @@ function registerRecipeCommands(program2) {
9088
9109
  });
9089
9110
  recipe.command("show").description("Show this project's recipe: each part and its state").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (opts) => {
9090
9111
  const config = requireConfig({ needsProject: !opts.projectId });
9091
- const projectId = requireUuid(opts.projectId ?? config.projectId, "project id");
9092
- const res = await apiCall(`/v1/admin/projects/${projectId}/recipe`, config);
9112
+ const projectId2 = requireUuid(opts.projectId ?? config.projectId, "project id");
9113
+ const res = await apiCall(`/v1/admin/projects/${projectId2}/recipe`, config);
9093
9114
  if (!res.ok) die(res);
9094
9115
  if (outputIsJson(opts.json)) {
9095
9116
  console.log(JSON.stringify(res.data, null, 2));
@@ -9103,8 +9124,8 @@ function registerRecipeCommands(program2) {
9103
9124
  throw new MushiCliError("E_INVALID_INPUT", `--element must be one of ${SOURCE_ELEMENTS.join(", ")}`);
9104
9125
  }
9105
9126
  const config = requireConfig({ needsProject: !opts.projectId });
9106
- const projectId = requireUuid(opts.projectId ?? config.projectId, "project id");
9107
- const res = await apiCall(`/v1/admin/projects/${projectId}/recipe/sources?element=${opts.element}`, config);
9127
+ const projectId2 = requireUuid(opts.projectId ?? config.projectId, "project id");
9128
+ const res = await apiCall(`/v1/admin/projects/${projectId2}/recipe/sources?element=${opts.element}`, config);
9108
9129
  if (!res.ok) die(res);
9109
9130
  if (outputIsJson(opts.json)) {
9110
9131
  console.log(JSON.stringify(res.data, null, 2));
@@ -9123,8 +9144,8 @@ function registerRecipeCommands(program2) {
9123
9144
  recipe.command("change <jobId>").description("One recipe change job: queued, running, pr_opened, rejected or failed").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (jobId, opts) => {
9124
9145
  const id = requireUuid(jobId, "job id");
9125
9146
  const config = requireConfig({ needsProject: !opts.projectId });
9126
- const projectId = requireUuid(opts.projectId ?? config.projectId, "project id");
9127
- const res = await apiCall(`/v1/admin/projects/${projectId}/recipe/changes/${id}`, config);
9147
+ const projectId2 = requireUuid(opts.projectId ?? config.projectId, "project id");
9148
+ const res = await apiCall(`/v1/admin/projects/${projectId2}/recipe/changes/${id}`, config);
9128
9149
  if (!res.ok) die(res);
9129
9150
  if (outputIsJson(opts.json)) {
9130
9151
  console.log(JSON.stringify(res.data, null, 2));
@@ -9418,10 +9439,11 @@ function renderPortfolioFindings(data) {
9418
9439
  }
9419
9440
  function registerPortfolioCommands(program2) {
9420
9441
  const portfolio = program2.command("portfolio").description("Every app in your team on one screen (needs an account-level key)");
9421
- portfolio.command("show").description("Health, open reports, holes, SDK version and last release for each app").option(ORG_HELP, ORG_DESC).option("--page <n>", "Page of apps (25 per page)", "1").option("--json", "Machine-readable JSON output").action(async (opts) => {
9442
+ portfolio.command("show").description("Health, open reports, holes, SDK version and last release for each app").option(ORG_HELP, ORG_DESC).option("--page <n>", "Page of apps (25 per page)", "1").option("--group <slug>", "Only the apps in this project group (see `mushi portfolio groups list`)").option("--json", "Machine-readable JSON output").action(async (opts) => {
9422
9443
  const config = requireConfig();
9423
9444
  const page = Math.max(1, Number.parseInt(opts.page, 10) || 1);
9424
- const result = await apiCall(`/v1/admin/orgs/${orgSegment(opts.org)}/portfolio?page=${page}`, config);
9445
+ const group = opts.group ? `&group=${encodeURIComponent(opts.group)}` : "";
9446
+ const result = await apiCall(`/v1/admin/orgs/${orgSegment(opts.org)}/portfolio?page=${page}${group}`, config);
9425
9447
  if (!result.ok) dieOrgError(result);
9426
9448
  if (outputIsJson(opts.json)) {
9427
9449
  console.log(JSON.stringify(result.data, null, 2));
@@ -9449,6 +9471,60 @@ function registerPortfolioCommands(program2) {
9449
9471
  }
9450
9472
  for (const line of renderPortfolioResources(result.data)) console.log(line);
9451
9473
  });
9474
+ const groups = portfolio.command("groups").description("Named groups of apps in your team, for the portfolio filter and project switcher");
9475
+ groups.command("list").description("Each group and how many of your apps are in it").option(ORG_HELP, ORG_DESC).option("--json", "Machine-readable JSON output").action(async (opts) => {
9476
+ const config = requireConfig();
9477
+ const result = await apiCall(`/v1/admin/orgs/${orgSegment(opts.org)}/project-groups`, config);
9478
+ if (!result.ok) dieOrgError(result);
9479
+ if (outputIsJson(opts.json)) {
9480
+ console.log(JSON.stringify(result.data.groups, null, 2));
9481
+ return;
9482
+ }
9483
+ if (result.data.groups.length === 0) console.log('No groups yet. Add one: mushi portfolio groups add "Client work"');
9484
+ for (const g of result.data.groups) console.log(`${g.slug.padEnd(24)} ${g.name} (${g.project_ids.length} app${g.project_ids.length === 1 ? "" : "s"}) ${g.id}`);
9485
+ });
9486
+ groups.command("add <name>").description("Create a group (team owners and admins)").option(ORG_HELP, ORG_DESC).option("--json", "Machine-readable JSON output").action(async (name, opts) => {
9487
+ const config = requireConfig();
9488
+ const result = await apiCall(`/v1/admin/orgs/${orgSegment(opts.org)}/project-groups`, config, {
9489
+ method: "POST",
9490
+ body: JSON.stringify({ name })
9491
+ });
9492
+ if (!result.ok) dieOrgError(result);
9493
+ if (outputIsJson(opts.json)) console.log(JSON.stringify(result.data.group, null, 2));
9494
+ else console.log(`Created "${result.data.group.name}" (${result.data.group.slug}) ${result.data.group.id}`);
9495
+ });
9496
+ groups.command("set <groupId> [projectIds...]").description("Set exactly which apps are in a group (team owners and admins); no ids empties it").option(ORG_HELP, ORG_DESC).action(async (groupId, projectIds, opts) => {
9497
+ const config = requireConfig();
9498
+ const gid = requireUuid(groupId, "group id");
9499
+ const ids = (projectIds ?? []).map((p3) => requireUuid(p3, "project id"));
9500
+ const result = await apiCall(
9501
+ `/v1/admin/orgs/${orgSegment(opts.org)}/project-groups/${gid}/projects`,
9502
+ config,
9503
+ { method: "PUT", body: JSON.stringify({ project_ids: ids }) }
9504
+ );
9505
+ if (!result.ok) dieOrgError(result);
9506
+ console.log(`Added ${result.data.added}, removed ${result.data.removed}.`);
9507
+ });
9508
+ groups.command("rename <groupId> <name>").description("Rename a group (team owners and admins)").option(ORG_HELP, ORG_DESC).action(async (groupId, name, opts) => {
9509
+ const config = requireConfig();
9510
+ const result = await apiCall(
9511
+ `/v1/admin/orgs/${orgSegment(opts.org)}/project-groups/${requireUuid(groupId, "group id")}`,
9512
+ config,
9513
+ { method: "PATCH", body: JSON.stringify({ name }) }
9514
+ );
9515
+ if (!result.ok) dieOrgError(result);
9516
+ console.log(`Renamed to "${result.data.group.name}" (${result.data.group.slug}).`);
9517
+ });
9518
+ groups.command("remove <groupId>").description("Delete a group; its apps stay (team owners and admins)").option(ORG_HELP, ORG_DESC).action(async (groupId, opts) => {
9519
+ const config = requireConfig();
9520
+ const result = await apiCall(
9521
+ `/v1/admin/orgs/${orgSegment(opts.org)}/project-groups/${requireUuid(groupId, "group id")}`,
9522
+ config,
9523
+ { method: "DELETE" }
9524
+ );
9525
+ if (!result.ok) dieOrgError(result);
9526
+ console.log("Group deleted. Its apps are unchanged.");
9527
+ });
9452
9528
  portfolio.command("import <file>").description("Record shared resources from a CSV: kind,external_id,project[,role] (team owners and admins)").option(ORG_HELP, ORG_DESC).option("--json", "Machine-readable JSON output").action(async (file, opts) => {
9453
9529
  const read = readTextFileCapped(file, MAX_RESOURCE_CSV_BYTES);
9454
9530
  if (read.kind === "absent") throw new MushiCliError("E_INVALID_INPUT", `Cannot read ${file}.`);
@@ -9683,8 +9759,8 @@ function registerCodeHealthCommands(program2) {
9683
9759
  const codeHealth = program2.command("code-health").description("Oversized files and bundle size from your CI (Code Health page)");
9684
9760
  codeHealth.command("show").description("Findings from the latest code-health run and the size summary").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--days <n>", "Trend window in days (1-365)", "30").option("--json", "Machine-readable JSON output (includes the trend series)").action(async (opts) => {
9685
9761
  const config = requireConfig();
9686
- const projectId = resolveProjectId(opts.projectId, config.projectId);
9687
- const qs = new URLSearchParams({ project_id: projectId, days: opts.days });
9762
+ const projectId2 = resolveProjectId(opts.projectId, config.projectId);
9763
+ const qs = new URLSearchParams({ project_id: projectId2, days: opts.days });
9688
9764
  const result = await apiCall(`/v1/admin/code-health?${qs}`, config);
9689
9765
  if (!result.ok) die(result);
9690
9766
  if (outputIsJson(opts.json)) {
@@ -9695,9 +9771,9 @@ function registerCodeHealthCommands(program2) {
9695
9771
  });
9696
9772
  codeHealth.command("stats").description("Counts only: errors, warnings and oversized files").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (opts) => {
9697
9773
  const config = requireConfig();
9698
- const projectId = resolveProjectId(opts.projectId, config.projectId);
9774
+ const projectId2 = resolveProjectId(opts.projectId, config.projectId);
9699
9775
  const result = await apiCall(
9700
- `/v1/admin/code-health/stats?${new URLSearchParams({ project_id: projectId })}`,
9776
+ `/v1/admin/code-health/stats?${new URLSearchParams({ project_id: projectId2 })}`,
9701
9777
  config
9702
9778
  );
9703
9779
  if (!result.ok) die(result);
@@ -9710,7 +9786,7 @@ function registerCodeHealthCommands(program2) {
9710
9786
  console.log("No code-health run yet.");
9711
9787
  return;
9712
9788
  }
9713
- console.log(`${d.projectName ?? projectId}: ${d.errorCount} error(s), ${d.warnCount} warning(s), ${d.godFileCount} oversized file(s) \xB7 last run ${fmtDate(d.latestRunAt)}`);
9789
+ console.log(`${d.projectName ?? projectId2}: ${d.errorCount} error(s), ${d.warnCount} warning(s), ${d.godFileCount} oversized file(s) \xB7 last run ${fmtDate(d.latestRunAt)}`);
9714
9790
  if (d.topPriorityLabel) console.log(`Next: ${d.topPriorityLabel}`);
9715
9791
  });
9716
9792
  }
@@ -9805,8 +9881,8 @@ ${r.body_md}`);
9805
9881
  });
9806
9882
  releases.command("draft <version>").description("Draft notes from the fixes merged in a window (nothing is sent)").option("--title <title>", "Release title").option("--since <iso>", "Window start (ISO date); default: since the last release").option("--until <iso>", "Window end (ISO date); default: now").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (version, opts) => {
9807
9883
  const config = requireConfig();
9808
- const projectId = resolveProjectId(opts.projectId, config.projectId);
9809
- const body = { project_id: projectId, version };
9884
+ const projectId2 = resolveProjectId(opts.projectId, config.projectId);
9885
+ const body = { project_id: projectId2, version };
9810
9886
  if (opts.title) body["title"] = opts.title;
9811
9887
  if (opts.since) body["window_start"] = opts.since;
9812
9888
  if (opts.until) body["window_end"] = opts.until;
@@ -9950,9 +10026,9 @@ Examples:
9950
10026
  mushi sentry import --query "is:unresolved level:error" --since-days 7`).action(async (issueIds, opts) => {
9951
10027
  const body = buildSentryImportBody(issueIds, opts);
9952
10028
  const config = requireConfig();
9953
- const projectId = resolveProjectId(opts.projectId, config.projectId);
10029
+ const projectId2 = resolveProjectId(opts.projectId, config.projectId);
9954
10030
  const result = await apiCall(
9955
- `/v1/admin/projects/${projectId}/sentry/import`,
10031
+ `/v1/admin/projects/${projectId2}/sentry/import`,
9956
10032
  config,
9957
10033
  { method: "POST", body: JSON.stringify(body) },
9958
10034
  { timeoutMs: IMPORT_TIMEOUT_MS }
@@ -10087,18 +10163,18 @@ function registerBudgetsCommands(program2) {
10087
10163
  const budgets = program2.command("budgets").description("What Mushi may spend for a project: AI budget, auto-fix caps, plan spend cap");
10088
10164
  budgets.command("show").description("Every spend limit and whether auto-fix is on").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (opts) => {
10089
10165
  const config = requireConfig();
10090
- const projectId = resolveProjectId(opts.projectId, config.projectId);
10091
- const qs = new URLSearchParams({ project_id: projectId });
10166
+ const projectId2 = resolveProjectId(opts.projectId, config.projectId);
10167
+ const qs = new URLSearchParams({ project_id: projectId2 });
10092
10168
  const [settings, billing, autofix] = await Promise.all([
10093
10169
  apiCall(`/v1/admin/settings?${qs}`, config),
10094
10170
  apiCall(`/v1/admin/billing/stats?${qs}`, config),
10095
- apiCall(`/v1/admin/projects/${projectId}/autofix`, config)
10171
+ apiCall(`/v1/admin/projects/${projectId2}/autofix`, config)
10096
10172
  ]);
10097
10173
  if (!settings.ok) die(settings);
10098
10174
  if (!billing.ok) die(billing);
10099
10175
  if (!autofix.ok) die(autofix);
10100
10176
  const view = {
10101
- projectId,
10177
+ projectId: projectId2,
10102
10178
  limits: pickLimits(settings.data),
10103
10179
  monthlySpendCapUsd: billing.data.monthlySpendCapUsd ?? null,
10104
10180
  autofixEnabled: autofix.data.autofix_enabled
@@ -10114,14 +10190,14 @@ function registerBudgetsCommands(program2) {
10114
10190
  throw new MushiCliError("E_INVALID_INPUT", "Pass on or off.", "e.g. mushi budgets autofix off");
10115
10191
  }
10116
10192
  const config = requireConfig();
10117
- const projectId = resolveProjectId(opts.projectId, config.projectId);
10193
+ const projectId2 = resolveProjectId(opts.projectId, config.projectId);
10118
10194
  const result = await apiCall(
10119
- `/v1/admin/projects/${projectId}/autofix/toggle`,
10195
+ `/v1/admin/projects/${projectId2}/autofix/toggle`,
10120
10196
  config,
10121
10197
  { method: "POST", body: JSON.stringify({ enabled: state === "on" }) }
10122
10198
  );
10123
10199
  if (!result.ok) die(result);
10124
- console.log(`Auto-fix is ${result.data.autofix_enabled ? "on" : "off"} for project ${projectId}.`);
10200
+ console.log(`Auto-fix is ${result.data.autofix_enabled ? "on" : "off"} for project ${projectId2}.`);
10125
10201
  });
10126
10202
  }
10127
10203
 
@@ -10169,9 +10245,9 @@ function registerRepoCommands(program2) {
10169
10245
  repo.command("digest").description("One token-budgeted text of the repo (tree + the files that matter), secrets removed").option("--report <id>", "Start from the files linked to this report's stack trace and fixes").option("--path <folder>", "Only this folder").option("--ref <ref>", "Branch, tag or commit (default: the default branch)").option("--budget <tokens>", "Token budget").option("--include <globs>", "Comma-separated globs a file must match").option("--exclude <globs>", "Comma-separated globs to leave out").option("--out <file>", "Write the digest text to this file instead of stdout").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output (metadata and text)").action(async (opts) => {
10170
10246
  const qs = digestQuery(opts);
10171
10247
  const config = requireConfig();
10172
- const projectId = resolveProjectId(opts.projectId, config.projectId);
10248
+ const projectId2 = resolveProjectId(opts.projectId, config.projectId);
10173
10249
  const suffix = qs.toString() ? `?${qs}` : "";
10174
- const result = await apiCall(`/v1/admin/projects/${projectId}/codebase/digest${suffix}`, config, {}, { timeoutMs: DIGEST_TIMEOUT_MS });
10250
+ const result = await apiCall(`/v1/admin/projects/${projectId2}/codebase/digest${suffix}`, config, {}, { timeoutMs: DIGEST_TIMEOUT_MS });
10175
10251
  if (!result.ok) die(result);
10176
10252
  if (outputIsJson(opts.json)) {
10177
10253
  console.log(JSON.stringify(result.data, null, 2));
@@ -10191,8 +10267,8 @@ function registerRepoCommands(program2) {
10191
10267
  const diagram = repo.command("diagram").description("The architecture diagram of your repo");
10192
10268
  diagram.command("show").description("The latest diagram as an outline, and its public page if published").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (opts) => {
10193
10269
  const config = requireConfig();
10194
- const projectId = resolveProjectId(opts.projectId, config.projectId);
10195
- const result = await apiCall(`/v1/admin/projects/${projectId}/codebase/diagram`, config);
10270
+ const projectId2 = resolveProjectId(opts.projectId, config.projectId);
10271
+ const result = await apiCall(`/v1/admin/projects/${projectId2}/codebase/diagram`, config);
10196
10272
  if (!result.ok) die(result);
10197
10273
  if (outputIsJson(opts.json)) {
10198
10274
  console.log(JSON.stringify(result.data, null, 2));
@@ -10202,9 +10278,9 @@ function registerRepoCommands(program2) {
10202
10278
  });
10203
10279
  diagram.command("generate").description("Draw the diagram for the latest commit (reuses the one for that commit unless --force)").option("--force", "Draw it again even if this commit already has one").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (opts) => {
10204
10280
  const config = requireConfig();
10205
- const projectId = resolveProjectId(opts.projectId, config.projectId);
10281
+ const projectId2 = resolveProjectId(opts.projectId, config.projectId);
10206
10282
  const result = await apiCall(
10207
- `/v1/admin/projects/${projectId}/codebase/diagram`,
10283
+ `/v1/admin/projects/${projectId2}/codebase/diagram`,
10208
10284
  config,
10209
10285
  { method: "POST", body: JSON.stringify({ force: opts.force === true }) },
10210
10286
  { timeoutMs: DIAGRAM_TIMEOUT_MS }
@@ -10219,8 +10295,8 @@ function registerRepoCommands(program2) {
10219
10295
  });
10220
10296
  diagram.command("publish").description("Publish the latest diagram as a public page (project owners and admins)").option("--yes", "Confirm: the diagram becomes public").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (opts) => {
10221
10297
  const config = requireConfig();
10222
- const projectId = resolveProjectId(opts.projectId, config.projectId);
10223
- const preview = await apiCall(`/v1/admin/projects/${projectId}/codebase/diagram/publish-preview`, config);
10298
+ const projectId2 = resolveProjectId(opts.projectId, config.projectId);
10299
+ const preview = await apiCall(`/v1/admin/projects/${projectId2}/codebase/diagram/publish-preview`, config);
10224
10300
  if (!preview.ok) die(preview);
10225
10301
  const p3 = preview.data;
10226
10302
  if (!p3.can_publish) {
@@ -10229,7 +10305,7 @@ function registerRepoCommands(program2) {
10229
10305
  console.log(`Will publish to ${p3.url}${p3.repo_private ? " (the repo is private on GitHub)" : ""}.`);
10230
10306
  requireYes(opts.yes, "Publishing makes the diagram public.");
10231
10307
  const result = await apiCall(
10232
- `/v1/admin/projects/${projectId}/codebase/diagram/publish`,
10308
+ `/v1/admin/projects/${projectId2}/codebase/diagram/publish`,
10233
10309
  config,
10234
10310
  { method: "POST", body: JSON.stringify({ diagram_id: p3.diagram_id, payload_hash: p3.payload_hash }) },
10235
10311
  { timeoutMs: PUBLISH_TIMEOUT_MS2 }
@@ -10245,8 +10321,8 @@ function registerRepoCommands(program2) {
10245
10321
  diagram.command("unpublish").description("Take the public diagram page down").option("--yes", "Confirm the unpublish").option("--project-id <id>", "Project ID (defaults to the configured project)").action(async (opts) => {
10246
10322
  requireYes(opts.yes, "This removes the public diagram page.");
10247
10323
  const config = requireConfig();
10248
- const projectId = resolveProjectId(opts.projectId, config.projectId);
10249
- const result = await apiCall(`/v1/admin/projects/${projectId}/codebase/diagram/publish`, config, { method: "DELETE" });
10324
+ const projectId2 = resolveProjectId(opts.projectId, config.projectId);
10325
+ const result = await apiCall(`/v1/admin/projects/${projectId2}/codebase/diagram/publish`, config, { method: "DELETE" });
10250
10326
  if (!result.ok) die(result);
10251
10327
  console.log("The public diagram page is down.");
10252
10328
  });
@@ -10407,7 +10483,7 @@ function registerSpendCommands(program2) {
10407
10483
  });
10408
10484
  spend.command("import <file>").description("Import a bill CSV (FOCUS, AWS CUR, or date,service,cost); a re-import of the same days replaces them").requiredOption("--vendor <vendor>", VENDORS.join(" | ")).option("--project-id <id>", "Put every row on this app (default: match an app column by name or slug)").option(ORG_FLAG2, ORG_DESC3).option("--json", "Machine-readable JSON output").action(async (file, opts) => {
10409
10485
  if (!VENDORS.includes(opts.vendor)) throw new MushiCliError("E_INVALID_INPUT", `--vendor must be one of ${VENDORS.join(", ")}`);
10410
- const projectId = opts.projectId ? requireUuid(opts.projectId, "project id") : void 0;
10486
+ const projectId2 = opts.projectId ? requireUuid(opts.projectId, "project id") : void 0;
10411
10487
  const read = readTextFileCapped(file, MAX_BILL_CSV_BYTES);
10412
10488
  if (read.kind === "absent") throw new MushiCliError("E_INVALID_INPUT", `Cannot read ${file}.`);
10413
10489
  if (read.kind === "too_large") throw new MushiCliError("E_INVALID_INPUT", `${file} is over 5 MB.`, "export daily rather than hourly rows, or split it by month");
@@ -10416,7 +10492,7 @@ function registerSpendCommands(program2) {
10416
10492
  const result = await apiCall(
10417
10493
  `/v1/admin/orgs/${orgSegment(opts.org)}/spend/imports`,
10418
10494
  config,
10419
- { method: "POST", body: JSON.stringify({ vendor: opts.vendor, csv, filename: basename2(file), ...projectId ? { projectId } : {} }) },
10495
+ { method: "POST", body: JSON.stringify({ vendor: opts.vendor, csv, filename: basename2(file), ...projectId2 ? { projectId: projectId2 } : {} }) },
10420
10496
  { timeoutMs: IMPORT_TIMEOUT_MS2 }
10421
10497
  );
10422
10498
  if (!result.ok) dieOrgError(result);
@@ -10495,6 +10571,140 @@ function registerDesignCommands(program2) {
10495
10571
  });
10496
10572
  }
10497
10573
 
10574
+ // src/commands/ux.ts
10575
+ import { spawn as spawn2 } from "child_process";
10576
+ import { existsSync as existsSync5 } from "fs";
10577
+ import { dirname as dirname3, join as join10 } from "path";
10578
+ var UX_PACKAGE = "@mushi-mushi/ux@^0.1.0";
10579
+ function cmdQuote(arg) {
10580
+ return /^[\w./:=@-]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '""')}"`;
10581
+ }
10582
+ var NPX_CLI_CANDIDATES = [
10583
+ join10(dirname3(process.execPath), "node_modules", "npm", "bin", "npx-cli.js"),
10584
+ join10(dirname3(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npx-cli.js")
10585
+ ];
10586
+ function uxCommandLine(args, env = process.env, platform = process.platform, npxCli = NPX_CLI_CANDIDATES, hasFile = existsSync5) {
10587
+ const local = env.MUSHI_UX_BIN?.trim();
10588
+ if (local) return { cmd: process.execPath, argv: [local, ...args], shell: false };
10589
+ const npxArgs = ["--yes", "--package", UX_PACKAGE, "mushi-ux", ...args];
10590
+ const found = (typeof npxCli === "string" ? [npxCli] : npxCli).find((p3) => hasFile(p3));
10591
+ if (found) return { cmd: process.execPath, argv: [found, ...npxArgs], shell: false };
10592
+ if (platform === "win32") return { cmd: "npx", argv: npxArgs.map(cmdQuote), shell: true };
10593
+ return { cmd: "npx", argv: npxArgs, shell: false };
10594
+ }
10595
+ function registerUxCommands(program2) {
10596
+ program2.command("ux").description("Run your coding agent over every screen of your app and keep only measured UX improvements (local, uses @mushi-mushi/ux)").argument("[args...]", "Passed to mushi-ux: ui | run | discover | login | models | skills | open").allowUnknownOption().helpOption(false).addHelpText(
10597
+ "after",
10598
+ `
10599
+ Examples:
10600
+ mushi ux ui # studio: pick agent, model, skill; watch every attempt
10601
+ mushi ux models --agent cursor # models your Cursor account can use
10602
+ mushi ux discover --url http://localhost:5173
10603
+ mushi ux run --dev "pnpm dev --port {port}" --agent claude-code
10604
+ mushi ux run --dev "pnpm dev --port {port}" --agent cursor --model <id from \`agent --list-models\`>
10605
+ mushi ux --help`
10606
+ ).action((args) => {
10607
+ const { cmd, argv, shell } = uxCommandLine(args);
10608
+ let env = process.env;
10609
+ if (args.includes("--sync")) {
10610
+ const config = requireConfig({ needsProject: true });
10611
+ env = { ...process.env, MUSHI_API_KEY: config.apiKey, MUSHI_PROJECT_ID: config.projectId, MUSHI_API_ENDPOINT: config.endpoint };
10612
+ } else if (args[0] === "ui") {
10613
+ const config = loadConfig();
10614
+ if (config.apiKey && config.projectId) {
10615
+ env = { ...process.env, MUSHI_API_KEY: config.apiKey, MUSHI_PROJECT_ID: config.projectId, ...config.endpoint ? { MUSHI_API_ENDPOINT: config.endpoint } : {} };
10616
+ }
10617
+ }
10618
+ const child = spawn2(cmd, argv, { stdio: "inherit", shell, env });
10619
+ child.on("error", (err) => {
10620
+ process.stderr.write(`error: could not start mushi-ux (${err.message}). Install it with: npm i -g @mushi-mushi/ux
10621
+ `);
10622
+ process.exit(1);
10623
+ });
10624
+ child.on("exit", (code) => process.exit(code ?? 1));
10625
+ });
10626
+ }
10627
+
10628
+ // src/commands/ux-runs.ts
10629
+ var STATUS_LABEL = {
10630
+ accepted: "improved",
10631
+ reverted: "rolled back",
10632
+ regressed: "moved by another fix",
10633
+ blocked: "could not load",
10634
+ skipped: "no change needed",
10635
+ iterating: "agent working",
10636
+ baseline: "measuring",
10637
+ pending: "queued"
10638
+ };
10639
+ var RUN_ID_RE = /^[0-9]{8}-[0-9]{6}-[a-z0-9]{4}$/;
10640
+ var KEY_RE = /^[a-z0-9][a-z0-9-]{0,79}$/;
10641
+ function projectId() {
10642
+ const config = requireConfig({ needsProject: true });
10643
+ return { config, pid: encodeURIComponent(config.projectId) };
10644
+ }
10645
+ function requireRunId(runId) {
10646
+ if (!RUN_ID_RE.test(runId)) throw new MushiCliError("E_INVALID_INPUT", `"${runId}" is not a run id (like 20261006-011207-qafu).`);
10647
+ return runId;
10648
+ }
10649
+ function registerUxRunsCommands(program2) {
10650
+ const runs = program2.command("ux-runs").description("UX runs synced from `mushi ux run --sync`: which screens were improved, rolled back or moved");
10651
+ runs.command("list").description("Recent UX runs for this project").option("--json", "Machine-readable JSON output").action(async (opts) => {
10652
+ const { config, pid } = projectId();
10653
+ const result = await apiCall(`/v1/admin/projects/${pid}/ux-runs`, config);
10654
+ if (!result.ok) die(result);
10655
+ if (outputIsJson(opts.json)) {
10656
+ console.log(JSON.stringify(result.data.runs, null, 2));
10657
+ return;
10658
+ }
10659
+ if (result.data.runs.length === 0) {
10660
+ console.log('No UX runs yet. Start one with: mushi ux run --dev "pnpm dev --port {port}" --sync');
10661
+ return;
10662
+ }
10663
+ for (const r of result.data.runs) {
10664
+ const c = r.counts;
10665
+ console.log(
10666
+ `${r.local_run_id} ${r.status.padEnd(7)} ${r.agent}${r.model ? ` \xB7 ${r.model}` : ""} improved ${c.accepted ?? 0} \xB7 rolled back ${c.reverted ?? 0} \xB7 moved ${c.regressed ?? 0} \xB7 blocked ${c.blocked ?? 0}`
10667
+ );
10668
+ }
10669
+ });
10670
+ runs.command("show <runId>").description("Every screen of one run, what still needs a look first").option("--json", "Machine-readable JSON output").action(async (runId, opts) => {
10671
+ const { config, pid } = projectId();
10672
+ const result = await apiCall(
10673
+ `/v1/admin/projects/${pid}/ux-runs/${requireRunId(runId)}`,
10674
+ config
10675
+ );
10676
+ if (!result.ok) die(result);
10677
+ if (outputIsJson(opts.json)) {
10678
+ console.log(JSON.stringify(result.data, null, 2));
10679
+ return;
10680
+ }
10681
+ const { run: run2, surfaces } = result.data;
10682
+ if (run2.branch) console.log(`Kept changes: ${run2.branch}
10683
+ `);
10684
+ const order = ["regressed", "reverted", "blocked", "iterating", "baseline", "accepted", "skipped", "pending"];
10685
+ for (const s of [...surfaces].sort((a, b) => order.indexOf(a.status) - order.indexOf(b.status))) {
10686
+ const score = s.penalty_before !== null ? ` score ${s.penalty_before}${s.penalty_after !== null ? `\u2192${s.penalty_after}` : ""}` : "";
10687
+ console.log(`${(STATUS_LABEL[s.status] ?? s.status).padEnd(22)} ${s.label} ${s.path}${score}${s.report_id ? " (filed)" : ""}`);
10688
+ if (s.note) console.log(`${" ".repeat(23)}${s.note}`);
10689
+ }
10690
+ });
10691
+ runs.command("file <runId> <screen>").description("File one screen of a run as a bug (source ux_loop; no AI call)").option("--json", "Machine-readable JSON output").action(async (runId, screen, opts) => {
10692
+ if (!KEY_RE.test(screen)) throw new MushiCliError("E_INVALID_INPUT", `"${screen}" is not a screen key (see \`mushi ux-runs show ${runId}\` --json).`);
10693
+ const { config, pid } = projectId();
10694
+ const result = await apiCall(
10695
+ `/v1/admin/projects/${pid}/ux-runs/${requireRunId(runId)}/surfaces/${screen}/report`,
10696
+ config,
10697
+ { method: "POST", body: "{}" }
10698
+ );
10699
+ if (!result.ok) die(result);
10700
+ if (outputIsJson(opts.json)) {
10701
+ console.log(JSON.stringify(result.data, null, 2));
10702
+ return;
10703
+ }
10704
+ console.log(result.data.reused ? `Already filed: report ${result.data.report_id}` : `Filed report ${result.data.report_id}`);
10705
+ });
10706
+ }
10707
+
10498
10708
  // src/index.ts
10499
10709
  installSignalHandlers();
10500
10710
  var program = new Command().name("mushi").description("Mushi CLI \u2014 set up the SDK, triage a report, fix from your editor").version(MUSHI_CLI_VERSION).option("--profile <name>", "Use a named credential profile for this invocation (or set MUSHI_PROFILE)").option("-o, --output <format>", "Output format: text | json", "text").hook("preAction", (_thisCommand, actionCommand) => {
@@ -10558,6 +10768,8 @@ registerRepoCommands(program);
10558
10768
  registerAccountsCommands(program);
10559
10769
  registerSpendCommands(program);
10560
10770
  registerDesignCommands(program);
10771
+ registerUxCommands(program);
10772
+ registerUxRunsCommands(program);
10561
10773
  registerCompletionCommand(program);
10562
10774
  program.parseAsync().catch((err) => {
10563
10775
  printAndExit(err);