@mushi-mushi/cli 0.13.0 → 0.15.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.
@@ -0,0 +1,6 @@
1
+ // src/version.ts
2
+ var MUSHI_CLI_VERSION = true ? "0.15.0" : "0.0.0-dev";
3
+
4
+ export {
5
+ MUSHI_CLI_VERSION
6
+ };
package/dist/index.js CHANGED
@@ -598,7 +598,7 @@ function getFrameworkFromPkg(pkg) {
598
598
  }
599
599
 
600
600
  // src/version.ts
601
- var MUSHI_CLI_VERSION = true ? "0.13.0" : "0.0.0-dev";
601
+ var MUSHI_CLI_VERSION = true ? "0.15.0" : "0.0.0-dev";
602
602
 
603
603
  // src/init.ts
604
604
  var ENV_FILES = [".env.local", ".env"];
@@ -1088,6 +1088,49 @@ function runMigrate(opts = {}) {
1088
1088
  return { matches };
1089
1089
  }
1090
1090
 
1091
+ // src/sanitize-config.ts
1092
+ var PROJECT_ID_RE = /^(?: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;
1093
+ var API_KEY_RE = /^mushi_[A-Za-z0-9_-]{10,}$/;
1094
+ function sanitizeApiKey(raw) {
1095
+ const key = raw.replace(/[\r\n\0]/g, "");
1096
+ if (!API_KEY_RE.test(key)) {
1097
+ throw new Error(
1098
+ "Invalid API key in config \u2014 run `mushi login --api-key <key>` to refresh credentials."
1099
+ );
1100
+ }
1101
+ return key;
1102
+ }
1103
+ function sanitizeProjectId(raw) {
1104
+ const id = raw.trim();
1105
+ if (!PROJECT_ID_RE.test(id)) {
1106
+ throw new Error(
1107
+ "Invalid project ID in config \u2014 expected a UUID or proj_* slug from the admin console."
1108
+ );
1109
+ }
1110
+ return id;
1111
+ }
1112
+ function sanitizeEndpoint(raw) {
1113
+ return assertEndpoint(normalizeEndpoint(raw));
1114
+ }
1115
+ function sanitizeCliCredentials(config) {
1116
+ if (!config.endpoint || !config.apiKey || !config.projectId) {
1117
+ throw new Error("Missing endpoint, apiKey, or projectId");
1118
+ }
1119
+ return {
1120
+ endpoint: sanitizeEndpoint(config.endpoint),
1121
+ apiKey: sanitizeApiKey(config.apiKey),
1122
+ projectId: sanitizeProjectId(config.projectId)
1123
+ };
1124
+ }
1125
+ function apiKeyHeaders(apiKey, projectId) {
1126
+ const headers = {
1127
+ Authorization: `Bearer ${apiKey}`,
1128
+ "X-Mushi-Api-Key": apiKey
1129
+ };
1130
+ if (projectId) headers["X-Mushi-Project"] = projectId;
1131
+ return headers;
1132
+ }
1133
+
1091
1134
  // src/sourcemaps.ts
1092
1135
  import { createReadStream } from "fs";
1093
1136
  import { readFile, readdir } from "fs/promises";
@@ -1430,12 +1473,11 @@ var IngestSetupHttpError = class extends Error {
1430
1473
  };
1431
1474
  var NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
1432
1475
  async function fetchIngestSetup(config, doFetch = globalThis.fetch) {
1433
- const res = await doFetch(`${config.endpoint}/v1/sync/ingest-setup`, {
1434
- headers: {
1435
- Authorization: `Bearer ${config.apiKey}`,
1436
- "X-Mushi-Api-Key": config.apiKey,
1437
- ...config.projectId ? { "X-Mushi-Project": config.projectId } : {}
1438
- },
1476
+ const endpoint = sanitizeEndpoint(config.endpoint);
1477
+ const apiKey = sanitizeApiKey(config.apiKey);
1478
+ const projectId = config.projectId ? sanitizeProjectId(config.projectId) : void 0;
1479
+ const res = await doFetch(`${endpoint}/v1/sync/ingest-setup`, {
1480
+ headers: apiKeyHeaders(apiKey, projectId),
1439
1481
  signal: AbortSignal.timeout(8e3)
1440
1482
  });
1441
1483
  if (!res.ok) {
@@ -1510,13 +1552,14 @@ function checkCliConfig(config) {
1510
1552
  }
1511
1553
  async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
1512
1554
  try {
1513
- const res = await doFetch(`${endpoint}/health`, {
1555
+ const safeEndpoint = sanitizeEndpoint(endpoint);
1556
+ const res = await doFetch(`${safeEndpoint}/health`, {
1514
1557
  signal: AbortSignal.timeout(5e3)
1515
1558
  });
1516
1559
  return {
1517
1560
  name: "Endpoint reachable",
1518
1561
  ok: res.status === 200,
1519
- detail: `GET ${endpoint}/health \u2192 ${res.status}`
1562
+ detail: `GET ${safeEndpoint}/health \u2192 ${res.status}`
1520
1563
  };
1521
1564
  } catch (err) {
1522
1565
  const msg = err instanceof Error ? err.message : String(err);
@@ -1558,14 +1601,11 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
1558
1601
  ];
1559
1602
  }
1560
1603
  try {
1604
+ const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
1561
1605
  const res = await doFetch(
1562
- `${config.endpoint}/v1/admin/projects/${config.projectId}/preflight`,
1606
+ `${endpoint}/v1/admin/projects/${projectId}/preflight`,
1563
1607
  {
1564
- headers: {
1565
- Authorization: `Bearer ${config.apiKey}`,
1566
- "X-Mushi-Api-Key": config.apiKey,
1567
- "X-Mushi-Project": config.projectId
1568
- },
1608
+ headers: apiKeyHeaders(apiKey, projectId),
1569
1609
  signal: AbortSignal.timeout(8e3)
1570
1610
  }
1571
1611
  );
@@ -1575,7 +1615,7 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
1575
1615
  return serverChecks.map((sc) => ({
1576
1616
  name: `[server] ${sc.label}`,
1577
1617
  ok: sc.ready,
1578
- detail: sc.hint
1618
+ detail: sc.ready ? "" : sc.hint
1579
1619
  }));
1580
1620
  }
1581
1621
  const text2 = await res.text().catch(() => "");
@@ -1613,7 +1653,7 @@ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
1613
1653
  const checks = steps.filter((s) => s.required).map((s) => ({
1614
1654
  name: `[ingest] ${s.label}`,
1615
1655
  ok: s.complete,
1616
- detail: s.hint ?? ""
1656
+ detail: s.complete ? "" : s.hint ?? ""
1617
1657
  }));
1618
1658
  const diag = data.diagnostic;
1619
1659
  if (diag?.last_sdk_seen_at) {
@@ -1629,6 +1669,84 @@ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
1629
1669
  return [{ name: "Ingest setup", ok: false, detail: `Fetch failed: ${msg}` }];
1630
1670
  }
1631
1671
  }
1672
+ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
1673
+ if (!config.projectId || !config.apiKey || !config.endpoint) {
1674
+ return [
1675
+ {
1676
+ name: "QA stories health",
1677
+ ok: false,
1678
+ detail: "Need projectId, apiKey, and endpoint for QA story checks."
1679
+ }
1680
+ ];
1681
+ }
1682
+ const checks = [];
1683
+ try {
1684
+ const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
1685
+ const headers = apiKeyHeaders(apiKey, projectId);
1686
+ const storiesRes = await doFetch(
1687
+ `${endpoint}/v1/admin/projects/${projectId}/qa-coverage`,
1688
+ {
1689
+ headers,
1690
+ signal: AbortSignal.timeout(8e3)
1691
+ }
1692
+ );
1693
+ if (!storiesRes.ok) {
1694
+ checks.push({ name: "[qa] Fetch QA stories", ok: false, detail: `HTTP ${storiesRes.status}` });
1695
+ return checks;
1696
+ }
1697
+ const storiesBody = await storiesRes.json();
1698
+ const stories2 = storiesBody.data?.coverage ?? [];
1699
+ const enabled = stories2.filter((s) => s.enabled);
1700
+ if (enabled.length === 0) {
1701
+ checks.push({ name: "[qa] Enabled QA stories", ok: true, detail: "No enabled stories \u2014 create one at /qa-coverage" });
1702
+ return checks;
1703
+ }
1704
+ checks.push({
1705
+ name: "[qa] Enabled QA stories",
1706
+ ok: true,
1707
+ detail: `${enabled.length} enabled story/stories configured`
1708
+ });
1709
+ const slackRes = await doFetch(
1710
+ `${endpoint}/v1/admin/projects/${projectId}/integrations/probe/slack`,
1711
+ {
1712
+ method: "POST",
1713
+ headers,
1714
+ signal: AbortSignal.timeout(6e3)
1715
+ }
1716
+ );
1717
+ const slackBody = slackRes.ok ? await slackRes.json() : null;
1718
+ const slackOk = slackBody?.status === "ok";
1719
+ checks.push({
1720
+ name: "[qa] Slack notifications configured",
1721
+ ok: slackOk,
1722
+ 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."
1723
+ });
1724
+ const fcRes = await doFetch(
1725
+ `${endpoint}/v1/admin/projects/${projectId}/integrations/probe/firecrawl`,
1726
+ {
1727
+ method: "POST",
1728
+ headers,
1729
+ signal: AbortSignal.timeout(6e3)
1730
+ }
1731
+ );
1732
+ const fcBody = fcRes.ok ? await fcRes.json() : null;
1733
+ const hasFirecrawlStories = enabled.some(
1734
+ (s) => !s.browser_provider || s.browser_provider === "firecrawl_actions"
1735
+ );
1736
+ if (hasFirecrawlStories) {
1737
+ const fcOk = fcBody?.status === "ok";
1738
+ checks.push({
1739
+ name: "[qa] Firecrawl API key configured",
1740
+ ok: fcOk,
1741
+ detail: fcOk ? "Firecrawl key is resolvable \u2014 stories will run without Unauthorized errors" : "No Firecrawl key found \u2014 enabled stories using firecrawl_actions will 401. Add a key at /integrations \u2192 BYOK keys."
1742
+ });
1743
+ }
1744
+ } catch (err) {
1745
+ const msg = err instanceof Error ? err.message : String(err);
1746
+ checks.push({ name: "[qa] QA stories health", ok: false, detail: `Fetch failed: ${msg}` });
1747
+ }
1748
+ return checks;
1749
+ }
1632
1750
  async function runDoctor(config, options = {}) {
1633
1751
  const doFetch = options.fetch ?? globalThis.fetch;
1634
1752
  const checks = [];
@@ -1646,6 +1764,10 @@ async function runDoctor(config, options = {}) {
1646
1764
  const ingestChecks = await checkIngestSetup(config, doFetch);
1647
1765
  checks.push(...ingestChecks);
1648
1766
  }
1767
+ if (options.qaStories) {
1768
+ const qaChecks = await checkQaStoriesHealth(config, doFetch);
1769
+ checks.push(...qaChecks);
1770
+ }
1649
1771
  return { checks, ready: checks.every((c) => c.ok) };
1650
1772
  }
1651
1773
  function formatDoctorResult(result) {
@@ -1654,7 +1776,7 @@ function formatDoctorResult(result) {
1654
1776
  const lines = [];
1655
1777
  for (const c of result.checks) {
1656
1778
  lines.push(`${c.ok ? PASS : FAIL} ${c.name}`);
1657
- lines.push(` ${c.detail}`);
1779
+ if (c.detail) lines.push(` ${c.detail}`);
1658
1780
  }
1659
1781
  const failed = result.checks.filter((c) => !c.ok);
1660
1782
  if (failed.length === 0) {
@@ -1763,7 +1885,6 @@ async function runUpgrade(opts = {}) {
1763
1885
 
1764
1886
  // src/connect.ts
1765
1887
  import { appendFile, mkdir, readFile as readFile2, writeFile } from "fs/promises";
1766
- import { existsSync as existsSync5 } from "fs";
1767
1888
  import { join as join6, resolve as resolve3 } from "path";
1768
1889
  function envKeyPresent(content, key) {
1769
1890
  const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1774,11 +1895,15 @@ async function mergeEnvFile(path, lines) {
1774
1895
  # Mushi \u2014 added by mushi connect
1775
1896
  ${lines.join("\n")}
1776
1897
  `;
1777
- if (existsSync5(path)) {
1778
- const content = await readFile2(path, "utf8");
1898
+ let existing = null;
1899
+ try {
1900
+ existing = await readFile2(path, "utf8");
1901
+ } catch {
1902
+ }
1903
+ if (existing !== null) {
1779
1904
  const needs = lines.filter((line) => {
1780
1905
  const key = line.split("=")[0];
1781
- return !envKeyPresent(content, key);
1906
+ return !envKeyPresent(existing, key);
1782
1907
  });
1783
1908
  if (needs.length === 0) return false;
1784
1909
  await appendFile(path, `
@@ -1793,13 +1918,17 @@ ${needs.join("\n")}
1793
1918
  async function ensureMcpJsonGitignored(cwd, messages) {
1794
1919
  const gitignorePath = join6(cwd, ".gitignore");
1795
1920
  const patterns = [".cursor/mcp.json", ".cursor/"];
1796
- if (!existsSync5(gitignorePath)) {
1921
+ let content = null;
1922
+ try {
1923
+ content = await readFile2(gitignorePath, "utf8");
1924
+ } catch {
1925
+ }
1926
+ if (content === null) {
1797
1927
  messages.push(
1798
1928
  "\u26A0 No .gitignore found \u2014 .cursor/mcp.json contains your API key. Add `.cursor/mcp.json` before committing."
1799
1929
  );
1800
1930
  return;
1801
1931
  }
1802
- const content = await readFile2(gitignorePath, "utf8");
1803
1932
  const covered = patterns.some((p3) => content.split("\n").some((line) => line.trim() === p3 || line.trim() === `${p3}/`));
1804
1933
  if (covered) return;
1805
1934
  await appendFile(gitignorePath, "\n# Mushi \u2014 keep MCP credentials out of git\n.cursor/mcp.json\n", "utf8");
@@ -1843,11 +1972,9 @@ async function runConnect(opts, baseConfig = {}) {
1843
1972
  }
1844
1973
  };
1845
1974
  let merged = { mcpServers: {} };
1846
- if (existsSync5(mcpPath)) {
1847
- try {
1848
- merged = JSON.parse(await readFile2(mcpPath, "utf8"));
1849
- } catch {
1850
- }
1975
+ try {
1976
+ merged = JSON.parse(await readFile2(mcpPath, "utf8"));
1977
+ } catch {
1851
1978
  }
1852
1979
  const servers = merged.mcpServers ?? {};
1853
1980
  servers[serverName] = mcpServerBlock;
@@ -2139,7 +2266,7 @@ Examples:
2139
2266
  mushi config endpoint https://... # set endpoint
2140
2267
  mushi config projectId <uuid> # set project`).action((key, value) => {
2141
2268
  const config = loadConfig();
2142
- const ALLOWED_KEYS = /* @__PURE__ */ new Set(["apiKey", "endpoint", "projectId"]);
2269
+ const ALLOWED_KEYS = /* @__PURE__ */ new Set(["apiKey", "endpoint", "projectId", "consoleUrl"]);
2143
2270
  if (key && value) {
2144
2271
  if (!ALLOWED_KEYS.has(key)) {
2145
2272
  process.stderr.write(`error: unknown config key "${key}". Allowed: ${[...ALLOWED_KEYS].join(", ")}
@@ -2644,7 +2771,7 @@ Typical first-time flow:
2644
2771
  # CLI writes .env.local and .cursor/mcp.json
2645
2772
  # mushi whoami to confirm`).action(async (opts) => {
2646
2773
  const { writeFile: writeFile2, mkdir: mkdir2 } = await import("fs/promises");
2647
- const { existsSync: existsSync6 } = await import("fs");
2774
+ const { existsSync: existsSync5 } = await import("fs");
2648
2775
  const nodePath = await import("path");
2649
2776
  const endpoint = opts.endpoint ?? loadConfig().endpoint ?? "https://api.mushimushi.dev";
2650
2777
  const signUpUrl = "https://kensaur.us/mushi-mushi/sign-up";
@@ -2691,7 +2818,7 @@ Typical first-time flow:
2691
2818
  `MUSHI_API_KEY=${apiKey}`,
2692
2819
  ""
2693
2820
  ];
2694
- const envExisting = existsSync6(envPath);
2821
+ const envExisting = existsSync5(envPath);
2695
2822
  await writeFile2(envPath, envLines.join("\n"), "utf8");
2696
2823
  console.log(`
2697
2824
  \u2713 ${envExisting ? "Updated" : "Created"} .env.local`);
@@ -2711,7 +2838,7 @@ Typical first-time flow:
2711
2838
  }
2712
2839
  }
2713
2840
  };
2714
- const mcpExisting = existsSync6(mcpPath);
2841
+ const mcpExisting = existsSync5(mcpPath);
2715
2842
  if (mcpExisting) {
2716
2843
  try {
2717
2844
  const { readFile: readFile3 } = await import("fs/promises");
@@ -2745,7 +2872,7 @@ Supported IDEs:
2745
2872
 
2746
2873
  The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).action(async (opts) => {
2747
2874
  const { writeFile: writeFile2, mkdir: mkdir2, readFile: readFile3 } = await import("fs/promises");
2748
- const { existsSync: existsSync6 } = await import("fs");
2875
+ const { existsSync: existsSync5 } = await import("fs");
2749
2876
  const nodePath = await import("path");
2750
2877
  const os = await import("os");
2751
2878
  const config = requireConfig({ needsProject: true });
@@ -2777,7 +2904,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2777
2904
  const configPath = nodePath.join(configDir, ideEntry.file);
2778
2905
  if (ideEntry.format === "mcp-json") {
2779
2906
  let merged = { mcpServers: {} };
2780
- if (existsSync6(configPath)) {
2907
+ if (existsSync5(configPath)) {
2781
2908
  try {
2782
2909
  const raw = await readFile3(configPath, "utf8");
2783
2910
  merged = JSON.parse(raw);
@@ -2798,7 +2925,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2798
2925
  }
2799
2926
  } else if (ideEntry.format === "zed") {
2800
2927
  let settings = {};
2801
- if (existsSync6(configPath)) {
2928
+ if (existsSync5(configPath)) {
2802
2929
  try {
2803
2930
  const raw = await readFile3(configPath, "utf8");
2804
2931
  settings = JSON.parse(raw);
@@ -3059,9 +3186,12 @@ program.command("doctor").description(
3059
3186
  ).option(
3060
3187
  "--ingest",
3061
3188
  "Also call GET /v1/sync/ingest-setup for the 4 required ingest steps (API key, SDK heartbeat, first report). Composable with --server."
3189
+ ).option(
3190
+ "--qa-stories",
3191
+ "Check enabled QA stories for common setup issues: missing Firecrawl key, missing target URL, Slack not connected. Requires --server credentials."
3062
3192
  ).action(async (opts) => {
3063
3193
  const config = loadConfig();
3064
- const result = await runDoctor(config, { cwd: opts.cwd, server: opts.server, ingest: opts.ingest });
3194
+ const result = await runDoctor(config, { cwd: opts.cwd, server: opts.server, ingest: opts.ingest, qaStories: opts.qaStories });
3065
3195
  const { checks } = result;
3066
3196
  if (opts.json) {
3067
3197
  console.log(JSON.stringify({ checks, ready: result.ready }, null, 2));
@@ -3373,7 +3503,636 @@ keys.command("add").description("Add a new API key to the pool").requiredOption(
3373
3503
  }
3374
3504
  console.log(`\u2713 Key added \u2014 id: ${res.data.id}`);
3375
3505
  });
3506
+ var integrations = program.command("integrations").description("Manage service integrations");
3507
+ integrations.command("list").description("List all configured integrations and their current health status").option("--json", "Machine-readable output").action(async (opts) => {
3508
+ const config = loadConfig();
3509
+ if (!config.apiKey) {
3510
+ console.error("Run `mushi login` first");
3511
+ process.exit(2);
3512
+ }
3513
+ if (!config.projectId) {
3514
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3515
+ process.exit(2);
3516
+ }
3517
+ const result = await apiCall(
3518
+ `/v1/admin/projects/${config.projectId}/integrations`,
3519
+ config
3520
+ );
3521
+ const rawResult = result;
3522
+ if (!rawResult.integrations && !result.ok) {
3523
+ console.error("Failed:", result.error);
3524
+ process.exit(1);
3525
+ }
3526
+ if (opts.json) {
3527
+ console.log(JSON.stringify(rawResult, null, 2));
3528
+ return;
3529
+ }
3530
+ const rows = rawResult.integrations ?? [];
3531
+ if (rows.length === 0) {
3532
+ console.log("No integrations configured. Visit the Integrations page to connect services.");
3533
+ return;
3534
+ }
3535
+ const icons = {
3536
+ slack: "\u{1F514}",
3537
+ github: "\u{1F419}",
3538
+ sentry: "\u{1FAB2}",
3539
+ langfuse: "\u{1F52D}",
3540
+ discord: "\u{1F4AC}",
3541
+ linear: "\u{1F4D0}",
3542
+ jira: "\u{1F5C2}\uFE0F",
3543
+ cursor_cloud: "\u{1F5B1}\uFE0F",
3544
+ claude_code_agent: "\u{1F916}"
3545
+ };
3546
+ console.log("\nIntegrations:\n");
3547
+ for (const row of rows) {
3548
+ const icon = icons[row.kind] ?? "\u{1F50C}";
3549
+ const statusIcon = row.status === "ok" ? "\u2705" : row.status === "error" ? "\u274C" : "\u26AA";
3550
+ console.log(` ${icon} ${row.kind.padEnd(20)} ${statusIcon} ${row.detail ?? ""}`);
3551
+ }
3552
+ console.log();
3553
+ });
3554
+ integrations.command("test <kind>").description(
3555
+ "Run a health probe for a specific integration (e.g. slack, sentry, github, langfuse, discord, cursor_cloud, claude_code_agent)"
3556
+ ).option("--json", "Machine-readable output").action(async (kind, opts) => {
3557
+ const config = loadConfig();
3558
+ if (!config.apiKey) {
3559
+ console.error("Run `mushi login` first");
3560
+ process.exit(2);
3561
+ }
3562
+ if (!config.projectId) {
3563
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3564
+ process.exit(2);
3565
+ }
3566
+ const result = await apiCall(
3567
+ `/v1/admin/projects/${config.projectId}/integrations/probe/${kind}`,
3568
+ config,
3569
+ { method: "POST" }
3570
+ );
3571
+ if (!result.ok) {
3572
+ console.error("Request failed:", result.error);
3573
+ process.exit(1);
3574
+ }
3575
+ if (opts.json) {
3576
+ console.log(JSON.stringify(result.data, null, 2));
3577
+ return;
3578
+ }
3579
+ const probeOk = result.data?.status === "ok";
3580
+ console.log(
3581
+ probeOk ? `\u2705 ${kind} integration is healthy${result.data.detail ? ": " + result.data.detail : ""}` : `\u274C ${kind} integration check failed${result.data.detail ? ": " + result.data.detail : ""}`
3582
+ );
3583
+ if (!probeOk) process.exit(1);
3584
+ });
3585
+ var slack = program.command("slack").description("Slack integration commands");
3586
+ slack.command("status").description("Show whether Slack is connected and which channel receives notifications").option("--json", "Machine-readable output").action(async (opts) => {
3587
+ const config = loadConfig();
3588
+ if (!config.apiKey) {
3589
+ console.error("Run `mushi login` first");
3590
+ process.exit(2);
3591
+ }
3592
+ if (!config.projectId) {
3593
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3594
+ process.exit(2);
3595
+ }
3596
+ const result = await apiCall(
3597
+ `/v1/admin/projects/${config.projectId}/integrations/probe/slack`,
3598
+ config,
3599
+ { method: "POST" }
3600
+ );
3601
+ if (!result.ok) {
3602
+ console.error("Request failed:", result.error);
3603
+ process.exit(1);
3604
+ }
3605
+ if (opts.json) {
3606
+ console.log(JSON.stringify(result.data, null, 2));
3607
+ return;
3608
+ }
3609
+ if (result.data?.status === "ok") {
3610
+ console.log("\u2705 Slack connected");
3611
+ if (result.data.detail) console.log(` ${result.data.detail}`);
3612
+ console.log("\n To change the channel or notification prefs, visit /integrations in the Mushi console.");
3613
+ } else {
3614
+ console.log("\u26AA Slack not connected");
3615
+ console.log(' Visit /integrations in the Mushi console and click "Add to Slack".');
3616
+ }
3617
+ });
3618
+ slack.command("test").description("Send a test Slack notification to confirm the current channel is working").option("--json", "Machine-readable output").action(async (opts) => {
3619
+ const config = loadConfig();
3620
+ if (!config.apiKey) {
3621
+ console.error("Run `mushi login` first");
3622
+ process.exit(2);
3623
+ }
3624
+ if (!config.projectId) {
3625
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3626
+ process.exit(2);
3627
+ }
3628
+ const result = await apiCall(
3629
+ `/v1/admin/projects/${config.projectId}/integrations/slack/test`,
3630
+ config,
3631
+ { method: "POST" }
3632
+ );
3633
+ if (!result.ok) {
3634
+ console.error("Request failed:", result.error);
3635
+ process.exit(1);
3636
+ }
3637
+ if (opts.json) {
3638
+ console.log(JSON.stringify(result.data, null, 2));
3639
+ return;
3640
+ }
3641
+ if (result.data?.ok) {
3642
+ console.log("\u2705 Test message sent! Check your Slack channel.");
3643
+ } else {
3644
+ console.error("\u274C Test failed:", result.data?.error ?? "unknown error");
3645
+ process.exit(1);
3646
+ }
3647
+ });
3648
+ var qa = program.command("qa").description("QA story management");
3649
+ qa.command("stories").description("List QA stories for the current project").option("--json", "Machine-readable output").option("-n, --limit <n>", "Max stories to return (not applied server-side; all stories returned)", "20").action(async (opts) => {
3650
+ const config = loadConfig();
3651
+ if (!config.apiKey) {
3652
+ console.error("Run `mushi login` first");
3653
+ process.exit(2);
3654
+ }
3655
+ if (!config.projectId) {
3656
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3657
+ process.exit(2);
3658
+ }
3659
+ const result = await apiCall(
3660
+ `/v1/admin/projects/${config.projectId}/qa-coverage`,
3661
+ config
3662
+ );
3663
+ if (!result.ok) {
3664
+ console.error("Failed:", result.error);
3665
+ process.exit(1);
3666
+ }
3667
+ if (opts.json) {
3668
+ console.log(JSON.stringify(result.data, null, 2));
3669
+ return;
3670
+ }
3671
+ const stories2 = result.data?.coverage ?? [];
3672
+ if (stories2.length === 0) {
3673
+ console.log("No QA stories yet. Create one at /qa-coverage in the Mushi console.");
3674
+ return;
3675
+ }
3676
+ console.log(`
3677
+ QA Stories (${stories2.length}):
3678
+ `);
3679
+ for (const s of stories2) {
3680
+ const statusIcon = s.last_run_status === "passed" ? "\u2705" : s.last_run_status === "failed" ? "\u274C" : s.last_run_status === "error" ? "\u{1F6A8}" : "\u26AA";
3681
+ const enabled = s.enabled ? "" : " [disabled]";
3682
+ const sid = s.story_id ?? s.id ?? "\u2014";
3683
+ console.log(` ${statusIcon} ${s.name.slice(0, 50).padEnd(52)} ${sid}${enabled}`);
3684
+ }
3685
+ console.log(`
3686
+ Use 'mushi qa runs <storyId>' to see recent runs for a story.`);
3687
+ console.log();
3688
+ });
3689
+ qa.command("runs <storyId>").description("Show recent runs for a QA story, including error heads").option("--json", "Machine-readable output").option("-n, --limit <n>", "Max runs to return", "10").action(async (storyId, opts) => {
3690
+ const config = loadConfig();
3691
+ if (!config.apiKey) {
3692
+ console.error("Run `mushi login` first");
3693
+ process.exit(2);
3694
+ }
3695
+ if (!config.projectId) {
3696
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3697
+ process.exit(2);
3698
+ }
3699
+ const limit = parseInt(opts.limit ?? "10", 10);
3700
+ const result = await apiCall(
3701
+ `/v1/admin/projects/${config.projectId}/qa-stories/${storyId}/runs?limit=${limit}`,
3702
+ config
3703
+ );
3704
+ if (!result.ok) {
3705
+ console.error("Failed:", result.error);
3706
+ process.exit(1);
3707
+ }
3708
+ if (opts.json) {
3709
+ console.log(JSON.stringify(result.data, null, 2));
3710
+ return;
3711
+ }
3712
+ const runs = result.data?.runs ?? [];
3713
+ if (runs.length === 0) {
3714
+ console.log("No runs yet for this story. Trigger one with `mushi qa run <storyId>`.");
3715
+ return;
3716
+ }
3717
+ console.log(`
3718
+ Recent runs for story ${storyId.slice(0, 8)}\u2026:
3719
+ `);
3720
+ for (const r of runs) {
3721
+ const statusIcon = r.status === "passed" ? "\u2705" : r.status === "failed" ? "\u274C" : r.status === "error" ? "\u{1F6A8}" : "\u23F3";
3722
+ const ts = r.created_at ? new Date(r.created_at).toISOString().slice(0, 16).replace("T", " ") : "\u2014";
3723
+ const latency = r.latency_ms ? ` (${(r.latency_ms / 1e3).toFixed(1)}s)` : "";
3724
+ console.log(` ${statusIcon} ${ts}${latency} ${r.id.slice(0, 8)}`);
3725
+ if (r.error_message) {
3726
+ console.log(` Error: ${r.error_message.slice(0, 120)}`);
3727
+ }
3728
+ if (r.assertion_failures?.length) {
3729
+ for (const af of r.assertion_failures.slice(0, 3)) {
3730
+ console.log(` \xB7 ${String(af).slice(0, 100)}`);
3731
+ }
3732
+ }
3733
+ }
3734
+ const consoleUrl = config.consoleUrl ?? "https://app.mushi.ai";
3735
+ console.log(`
3736
+ Open in console: ${consoleUrl}/qa-coverage?story=${storyId}`);
3737
+ console.log(` Tip: run 'mushi config consoleUrl http://localhost:6464' to set your local console URL`);
3738
+ console.log();
3739
+ });
3740
+ qa.command("run <storyId>").description("Manually trigger a QA story run (fire-and-forget; check results with `mushi qa runs <id>`)").option("--json", "Machine-readable output").action(async (storyId, opts) => {
3741
+ const config = loadConfig();
3742
+ if (!config.apiKey) {
3743
+ console.error("Run `mushi login` first");
3744
+ process.exit(2);
3745
+ }
3746
+ if (!config.projectId) {
3747
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3748
+ process.exit(2);
3749
+ }
3750
+ const result = await apiCall(
3751
+ `/v1/admin/projects/${config.projectId}/qa-stories/${storyId}/run`,
3752
+ config,
3753
+ { method: "POST" }
3754
+ );
3755
+ if (!result.ok) {
3756
+ console.error("Failed:", result.error);
3757
+ process.exit(1);
3758
+ }
3759
+ if (opts.json) {
3760
+ console.log(JSON.stringify(result.data, null, 2));
3761
+ return;
3762
+ }
3763
+ const runId = result.data?.run_id;
3764
+ if (runId) {
3765
+ console.log(`\u25B6 Run triggered: ${runId.slice(0, 8)}\u2026`);
3766
+ console.log(` Check results: mushi qa runs ${storyId}`);
3767
+ } else {
3768
+ console.error("\u274C Trigger failed: no run_id in response", JSON.stringify(result.data));
3769
+ process.exit(1);
3770
+ }
3771
+ });
3772
+ program.command("audit").description("Run a full-stack health audit for the current project").option("--json", "Machine-readable JSON output").option("--project-id <id>", "Project ID to audit (defaults to MUSHI_PROJECT_ID from config)").addHelpText("after", `
3773
+ Description:
3774
+ Fans out to the Mushi backend to run a full-stack health audit:
3775
+ \u2022 DB schema + Supabase advisors (requires Supabase PAT in API Keys)
3776
+ \u2022 Recent backend error logs
3777
+ \u2022 Tables without RLS enabled
3778
+ \u2022 Gate results: API contract (G3), spec drift (G6), orphan endpoints (G7),
3779
+ unknown frontend calls (G8), schema drift, status claim (G5)
3780
+
3781
+ Returns a PM-readable scorecard with severity-ranked findings.
3782
+
3783
+ Prerequisites:
3784
+ 1. Configure your Supabase PAT: mushi settings set supabase-pat <token>
3785
+ 2. Set supabase_project_ref in Admin \u2192 Settings \u2192 Project.
3786
+
3787
+ Examples:
3788
+ mushi audit
3789
+ mushi audit --json
3790
+ mushi audit --project-id abc123`).action(async (opts) => {
3791
+ const config = requireConfig();
3792
+ const rawProjectId = opts.projectId ?? config.projectId;
3793
+ if (!rawProjectId) {
3794
+ process.stderr.write("error: project ID required. Run `mushi login` or pass --project-id\n");
3795
+ process.exit(1);
3796
+ }
3797
+ let endpoint;
3798
+ let projectId;
3799
+ try {
3800
+ endpoint = sanitizeEndpoint(config.endpoint);
3801
+ projectId = sanitizeProjectId(rawProjectId);
3802
+ } catch (err) {
3803
+ const msg = err instanceof Error ? err.message : String(err);
3804
+ process.stderr.write(`error: ${msg}
3805
+ `);
3806
+ process.exit(2);
3807
+ }
3808
+ const headers = {
3809
+ "Content-Type": "application/json",
3810
+ "X-Mushi-Project-Id": projectId
3811
+ };
3812
+ const jwt = config.jwt ?? null;
3813
+ const apiKey = config.apiKey ?? null;
3814
+ if (jwt) {
3815
+ headers["Authorization"] = `Bearer ${jwt.replace(/[\r\n\0]/g, "")}`;
3816
+ } else if (apiKey) {
3817
+ headers["X-Mushi-Api-Key"] = sanitizeApiKey(apiKey);
3818
+ } else {
3819
+ process.stderr.write("error: no credentials found. Run `mushi login` first.\n");
3820
+ process.exit(1);
3821
+ }
3822
+ if (!opts.json) process.stdout.write("Running full-stack audit\u2026 ");
3823
+ try {
3824
+ const controller = new AbortController();
3825
+ const timer = setTimeout(() => controller.abort(), 3e4);
3826
+ const res = await fetch(
3827
+ `${endpoint}/v1/admin/projects/${projectId}/audit`,
3828
+ { method: "POST", headers, body: "{}", signal: controller.signal }
3829
+ );
3830
+ clearTimeout(timer);
3831
+ const body = await res.json();
3832
+ if (!res.ok || !body.ok) {
3833
+ if (opts.json) {
3834
+ console.log(JSON.stringify(body));
3835
+ process.exit(1);
3836
+ }
3837
+ process.stdout.write("FAIL\n");
3838
+ process.stderr.write(`error: ${body.error?.message ?? `HTTP ${res.status}`}
3839
+ `);
3840
+ process.exit(1);
3841
+ }
3842
+ if (opts.json) {
3843
+ console.log(JSON.stringify(body.data, null, 2));
3844
+ return;
3845
+ }
3846
+ const data = body.data;
3847
+ const overallGlyph = data.summary.overall === "fail" ? "\u274C" : data.summary.overall === "warn" ? "\u26A0\uFE0F " : "\u2705";
3848
+ process.stdout.write(`${overallGlyph}
3849
+
3850
+ `);
3851
+ console.log(`Full-Stack Audit \u2014 ${new Date(data.audit_at).toLocaleString()}`);
3852
+ console.log(`Backend linked: ${data.backend_linked ? "yes" : "no (configure Supabase PAT + project ref)"}`);
3853
+ console.log(`Summary: ${data.summary.error_count} error(s) \xB7 ${data.summary.warn_count} warning(s)
3854
+ `);
3855
+ if (data.findings.length === 0) {
3856
+ console.log(" \u2713 No findings. Your project looks healthy.");
3857
+ } else {
3858
+ for (const f of data.findings) {
3859
+ const icon = f.severity === "error" ? "\u{1F534}" : f.severity === "warn" ? "\u{1F7E1}" : "\u2139\uFE0F ";
3860
+ console.log(` ${icon} ${f.title}`);
3861
+ console.log(` ${f.detail.slice(0, 120)}${f.detail.length > 120 ? "\u2026" : ""}`);
3862
+ }
3863
+ }
3864
+ if (data.gate_runs.length > 0) {
3865
+ console.log("\nGate Results:");
3866
+ for (const run of data.gate_runs) {
3867
+ const g = run.status === "pass" ? "\u2713" : run.status === "fail" ? "\u2717" : "~";
3868
+ console.log(` ${g} ${run.gate.padEnd(22)} ${run.status} (${run.findings_count} finding${run.findings_count !== 1 ? "s" : ""})`);
3869
+ }
3870
+ }
3871
+ if (data.summary.overall === "fail") process.exit(1);
3872
+ } catch (err) {
3873
+ const msg = err instanceof Error ? err.message : String(err);
3874
+ if (opts.json) {
3875
+ console.log(JSON.stringify({ ok: false, error: msg }));
3876
+ } else {
3877
+ process.stdout.write("ERROR\n");
3878
+ process.stderr.write(`error: ${msg}
3879
+ `);
3880
+ }
3881
+ process.exit(1);
3882
+ }
3883
+ });
3376
3884
  program.parseAsync().catch((err) => {
3377
3885
  console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
3378
3886
  process.exit(1);
3379
3887
  });
3888
+ var skills = program.command("skills").description("Manage agent skill catalog");
3889
+ skills.command("list").description("List all skills in the catalog").option("--category <cat>", "Filter by category (workflow, debug, test, audit, \u2026)").option("--search <q>", "Search slug, title, or description").option("--page <n>", "Page number (default 1)", "1").option("--limit <n>", "Max results per page (1\u2013200, default 200)", "200").option("--json", "Machine-readable output").action(async (opts) => {
3890
+ const config = loadConfig();
3891
+ if (!config.apiKey) {
3892
+ console.error("Run `mushi login` first");
3893
+ process.exit(2);
3894
+ }
3895
+ const qs = new URLSearchParams();
3896
+ if (opts.category) qs.set("category", opts.category);
3897
+ if (opts.search) qs.set("q", opts.search);
3898
+ qs.set("page", String(Math.max(1, parseInt(opts.page) || 1)));
3899
+ qs.set("limit", String(Math.min(Math.max(1, parseInt(opts.limit) || 200), 200)));
3900
+ const result = await apiCall(
3901
+ `/v1/admin/skills?${qs}`,
3902
+ config
3903
+ );
3904
+ if (!result.ok) {
3905
+ console.error("Failed:", result.error);
3906
+ process.exit(1);
3907
+ }
3908
+ const rows = result.data ?? [];
3909
+ if (opts.json) {
3910
+ console.log(JSON.stringify({ skills: rows, count: rows.length }, null, 2));
3911
+ return;
3912
+ }
3913
+ if (rows.length === 0) {
3914
+ console.log("No skills in catalog. Add a source with `mushi skills sync`.");
3915
+ return;
3916
+ }
3917
+ console.log(`
3918
+ Skill catalog (${rows.length} skills):
3919
+ `);
3920
+ let lastCat = "";
3921
+ for (const s of rows) {
3922
+ if (s.category !== lastCat) {
3923
+ lastCat = s.category;
3924
+ console.log(`
3925
+ [${s.category}]`);
3926
+ }
3927
+ const chain = s.chain_slugs?.length ? ` \u2192 ${s.chain_slugs.length} steps` : "";
3928
+ console.log(` ${s.slug.padEnd(40)} ${s.title}${chain}`);
3929
+ }
3930
+ console.log();
3931
+ });
3932
+ skills.command("show <slug>").description("Show full details and chain for a skill").action(async (slug) => {
3933
+ const config = loadConfig();
3934
+ if (!config.apiKey) {
3935
+ console.error("Run `mushi login` first");
3936
+ process.exit(2);
3937
+ }
3938
+ const result = await apiCall(`/v1/admin/skills/${slug}`, config);
3939
+ if (!result.ok) {
3940
+ console.error("Skill not found:", slug);
3941
+ process.exit(1);
3942
+ }
3943
+ const s = result.data;
3944
+ console.log(`
3945
+ ${s.title} (${s.slug})
3946
+ ${"\u2500".repeat(50)}`);
3947
+ console.log(`Category: ${s.category}`);
3948
+ console.log(`Chain: ${s.chain_slugs?.length ? s.chain_slugs.join(" \u2192 ") : "none"}`);
3949
+ console.log(`
3950
+ Description:
3951
+ ${s.description}
3952
+ `);
3953
+ });
3954
+ skills.command("sync").description("Trigger skill sync for all configured skill sources").option("--source-id <id>", "Sync only a specific source ID").action(async (opts) => {
3955
+ const config = loadConfig();
3956
+ if (!config.apiKey) {
3957
+ console.error("Run `mushi login` first");
3958
+ process.exit(2);
3959
+ }
3960
+ if (!config.projectId) {
3961
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3962
+ process.exit(2);
3963
+ }
3964
+ let ids;
3965
+ if (opts.sourceId) {
3966
+ ids = [opts.sourceId];
3967
+ } else {
3968
+ const sourcesResult = await apiCall(
3969
+ `/v1/admin/skills/sources?project_id=${config.projectId}`,
3970
+ config
3971
+ );
3972
+ if (!sourcesResult.ok) {
3973
+ console.error("Failed to list sources:", sourcesResult.error);
3974
+ process.exit(1);
3975
+ }
3976
+ ids = (sourcesResult.data ?? []).map((s) => s.id);
3977
+ }
3978
+ if (ids.length === 0) {
3979
+ console.log("No skill sources configured. Add one in the Skill Pipelines console page.");
3980
+ return;
3981
+ }
3982
+ for (const id of ids) {
3983
+ console.log(`Syncing source ${id.slice(0, 8)}\u2026`);
3984
+ const result = await apiCall(
3985
+ `/v1/admin/skills/sources/${id}/sync`,
3986
+ config,
3987
+ { method: "POST" }
3988
+ );
3989
+ if (!result.ok) {
3990
+ console.error(" Sync failed:", result.error);
3991
+ } else console.log(` Done: ${result.data?.synced ?? 0} synced, ${result.data?.skipped ?? 0} skipped, ${result.data?.errors ?? 0} errors`);
3992
+ }
3993
+ console.log();
3994
+ });
3995
+ var pipeline = program.command("pipeline").description("Manage skill pipeline runs");
3996
+ pipeline.command("start <reportId>").description("Start a skill pipeline for a report").requiredOption("--skill <slug>", "Root skill slug (e.g. workflow-fix-and-ship)").option("--mode <mode>", "Execution mode: handoff (default) or cloud", "handoff").option("--json", "Machine-readable output").action(async (reportId, opts) => {
3997
+ const config = loadConfig();
3998
+ if (!config.apiKey) {
3999
+ console.error("Run `mushi login` first");
4000
+ process.exit(2);
4001
+ }
4002
+ if (!config.projectId) {
4003
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
4004
+ process.exit(2);
4005
+ }
4006
+ const result = await apiCall(
4007
+ `/v1/admin/skills/pipelines`,
4008
+ config,
4009
+ {
4010
+ method: "POST",
4011
+ body: JSON.stringify({
4012
+ project_id: config.projectId,
4013
+ root_skill_slug: opts.skill,
4014
+ report_id: reportId,
4015
+ mode: opts.mode
4016
+ })
4017
+ }
4018
+ );
4019
+ if (!result.ok) {
4020
+ console.error("Failed:", result.error);
4021
+ process.exit(1);
4022
+ }
4023
+ if (opts.json) {
4024
+ console.log(JSON.stringify(result.data, null, 2));
4025
+ return;
4026
+ }
4027
+ const runId = result.data?.id ?? "";
4028
+ const chain = result.data?.chain_slugs ?? [];
4029
+ console.log(`
4030
+ Pipeline started!
4031
+ `);
4032
+ console.log(` Run ID: ${runId.slice(0, 8)}\u2026 (full: ${runId})`);
4033
+ console.log(` Skill: ${opts.skill}`);
4034
+ console.log(` Chain: ${chain.length > 0 ? chain.join(" \u2192 ") : "(root only)"}`);
4035
+ console.log(` Mode: ${opts.mode}`);
4036
+ if (opts.mode === "handoff") {
4037
+ console.log(`
4038
+ Get context packet: mushi pipeline watch ${runId.slice(0, 8)}`);
4039
+ console.log(` Check in step 0: mushi pipeline checkin ${runId.slice(0, 8)} --step 0 --status passed`);
4040
+ }
4041
+ console.log();
4042
+ });
4043
+ pipeline.command("watch <runIdOrPrefix>").description("Watch a pipeline run and print the context packet").option("--json", "Machine-readable output").action(async (runIdOrPrefix, opts) => {
4044
+ const config = loadConfig();
4045
+ if (!config.apiKey) {
4046
+ console.error("Run `mushi login` first");
4047
+ process.exit(2);
4048
+ }
4049
+ let runId = runIdOrPrefix;
4050
+ if (runIdOrPrefix.length < 36) {
4051
+ const list = await apiCall(
4052
+ `/v1/admin/skills/pipelines?project_id=${config.projectId}&limit=50`,
4053
+ config
4054
+ );
4055
+ if (!list.ok) {
4056
+ console.error("Failed:", list.error);
4057
+ process.exit(1);
4058
+ }
4059
+ const match = list.data.find((r) => r.id.startsWith(runIdOrPrefix));
4060
+ if (!match) {
4061
+ console.error("Run not found:", runIdOrPrefix);
4062
+ process.exit(1);
4063
+ }
4064
+ runId = match.id;
4065
+ }
4066
+ const result = await apiCall(
4067
+ `/v1/admin/skills/pipelines/${runId}`,
4068
+ config
4069
+ );
4070
+ if (!result.ok) {
4071
+ console.error("Failed:", result.error);
4072
+ process.exit(1);
4073
+ }
4074
+ if (opts.json) {
4075
+ console.log(JSON.stringify(result.data, null, 2));
4076
+ return;
4077
+ }
4078
+ const run = result.data;
4079
+ const statusIcon = run.status === "completed" ? "\u2705" : run.status === "failed" ? "\u274C" : run.status === "running" ? "\u23F3" : "\u26AA";
4080
+ console.log(`
4081
+ ${statusIcon} Pipeline ${runId.slice(0, 8)} \xB7 ${run.root_skill_slug} \xB7 ${run.mode}
4082
+ `);
4083
+ const steps = run.steps ?? [];
4084
+ for (const step of steps) {
4085
+ const icon = step.status === "passed" ? "\u2705" : step.status === "failed" ? "\u274C" : step.status === "running" ? "\u23F3" : "\u26AA";
4086
+ const pr = step.pr_url ? ` \u2192 ${step.pr_url}` : "";
4087
+ console.log(` ${icon} Step ${step.step_index + 1}: ${step.skill_slug}${pr}`);
4088
+ if (step.notes) console.log(` ${step.notes}`);
4089
+ }
4090
+ if (run.context_packet) {
4091
+ console.log(`
4092
+ ${"\u2500".repeat(60)}`);
4093
+ console.log(`Context Packet (paste into your Cursor agent):
4094
+ `);
4095
+ console.log(run.context_packet.slice(0, 6e3));
4096
+ if (run.context_packet.length > 6e3) console.log("\n\u2026 [truncated \u2014 full packet via --json]");
4097
+ }
4098
+ console.log();
4099
+ });
4100
+ pipeline.command("checkin <runIdOrPrefix>").description("Check in a pipeline step (CLI agent reports status after completing a step)").requiredOption("--step <n>", "Step index (0-based)", parseInt).requiredOption("--status <status>", "Step status: passed | failed | running | skipped").option("--notes <text>", "Optional notes / output summary").option("--pr-url <url>", "PR URL opened during this step").action(async (runIdOrPrefix, opts) => {
4101
+ const config = loadConfig();
4102
+ if (!config.apiKey) {
4103
+ console.error("Run `mushi login` first");
4104
+ process.exit(2);
4105
+ }
4106
+ let runId = runIdOrPrefix;
4107
+ if (runIdOrPrefix.length < 36) {
4108
+ const list = await apiCall(
4109
+ `/v1/admin/skills/pipelines?project_id=${config.projectId}&limit=50`,
4110
+ config
4111
+ );
4112
+ if (!list.ok) {
4113
+ console.error("Failed:", list.error);
4114
+ process.exit(1);
4115
+ }
4116
+ const match = list.data.find((r) => r.id.startsWith(runIdOrPrefix));
4117
+ if (!match) {
4118
+ console.error("Run not found:", runIdOrPrefix);
4119
+ process.exit(1);
4120
+ }
4121
+ runId = match.id;
4122
+ }
4123
+ const result = await apiCall(
4124
+ `/v1/admin/skills/pipelines/${runId}/steps/${opts.step}/checkin`,
4125
+ config,
4126
+ {
4127
+ method: "POST",
4128
+ body: JSON.stringify({ status: opts.status, notes: opts.notes, pr_url: opts.prUrl })
4129
+ }
4130
+ );
4131
+ if (!result.ok) {
4132
+ console.error("Failed:", result.error);
4133
+ process.exit(1);
4134
+ }
4135
+ console.log(` Step ${opts.step} \u2192 ${opts.status}. Console live flow updated.`);
4136
+ console.log(` Next: mushi pipeline watch ${runId.slice(0, 8)}`);
4137
+ console.log();
4138
+ });
package/dist/init.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-NYPX5KXR.js";
9
9
  import {
10
10
  MUSHI_CLI_VERSION
11
- } from "./chunk-IMDLL4EO.js";
11
+ } from "./chunk-UTST6AEP.js";
12
12
 
13
13
  // src/init.ts
14
14
  import * as p from "@clack/prompts";
package/dist/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-IMDLL4EO.js";
3
+ } from "./chunk-UTST6AEP.js";
4
4
  export {
5
5
  MUSHI_CLI_VERSION
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mushi-mushi/cli",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "license": "MIT",
5
5
  "description": "CLI for Mushi Mushi — `mushi init` wizard installs the right SDK for your framework, plus report triage and pipeline health commands",
6
6
  "bin": {
@@ -1,6 +0,0 @@
1
- // src/version.ts
2
- var MUSHI_CLI_VERSION = true ? "0.13.0" : "0.0.0-dev";
3
-
4
- export {
5
- MUSHI_CLI_VERSION
6
- };