@mushi-mushi/cli 0.10.0 → 0.11.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
@@ -263,6 +263,71 @@ export default function App() {
263
263
  import { Mushi } from '@mushi-mushi/capacitor'
264
264
 
265
265
  await Mushi.configure({ projectId: '${projectId}', apiKey: '${apiKey}' })`
266
+ },
267
+ express: {
268
+ id: "express",
269
+ label: "Express",
270
+ packageName: "@mushi-mushi/node",
271
+ needsWebPackage: false,
272
+ snippet: (apiKey, projectId) => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
273
+ import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
274
+ import { mushiExpressErrorHandler } from '@mushi-mushi/node/express'
275
+ import type { Express } from 'express'
276
+
277
+ export const mushi = new MushiNodeClient({
278
+ projectId: '${projectId}',
279
+ apiKey: '${apiKey}',
280
+ environment: process.env.NODE_ENV ?? 'production',
281
+ })
282
+ attachUnhandledHook({ client: mushi })
283
+
284
+ export function attachMushi(app: Express) {
285
+ app.use(mushiExpressErrorHandler({ client: mushi }))
286
+ }`
287
+ },
288
+ fastify: {
289
+ id: "fastify",
290
+ label: "Fastify",
291
+ packageName: "@mushi-mushi/node",
292
+ needsWebPackage: false,
293
+ snippet: (apiKey, projectId) => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
294
+ import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
295
+ import { mushiFastifyPlugin } from '@mushi-mushi/node/fastify'
296
+ import Fastify from 'fastify'
297
+
298
+ export const mushi = new MushiNodeClient({
299
+ projectId: '${projectId}',
300
+ apiKey: '${apiKey}',
301
+ environment: process.env.NODE_ENV ?? 'production',
302
+ })
303
+ attachUnhandledHook({ client: mushi })
304
+
305
+ const app = Fastify()
306
+ mushiFastifyPlugin(app, { client: mushi })`
307
+ },
308
+ hono: {
309
+ id: "hono",
310
+ label: "Hono",
311
+ packageName: "@mushi-mushi/node",
312
+ needsWebPackage: false,
313
+ snippet: (apiKey, projectId) => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
314
+ import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
315
+ import { mushiHonoErrorHandler } from '@mushi-mushi/node/hono'
316
+ import { Hono } from 'hono'
317
+
318
+ export const mushi = new MushiNodeClient({
319
+ projectId: '${projectId}',
320
+ apiKey: '${apiKey}',
321
+ environment: process.env.NODE_ENV ?? 'production',
322
+ })
323
+ attachUnhandledHook({ client: mushi })
324
+
325
+ const app = new Hono()
326
+ app.onError(
327
+ mushiHonoErrorHandler({ client: mushi }, (err, c) =>
328
+ c.text('Internal Server Error', 500),
329
+ ),
330
+ )`
266
331
  },
267
332
  vanilla: {
268
333
  id: "vanilla",
@@ -296,6 +361,9 @@ function detectFramework(cwd, pkg) {
296
361
  if (deps.has("svelte")) return FRAMEWORKS.svelte;
297
362
  if (deps.has("vue")) return FRAMEWORKS.vue;
298
363
  if (deps.has("react")) return FRAMEWORKS.react;
364
+ if (deps.has("express")) return FRAMEWORKS.express;
365
+ if (deps.has("fastify")) return FRAMEWORKS.fastify;
366
+ if (deps.has("hono") || deps.has("@hono/hono")) return FRAMEWORKS.hono;
299
367
  if (existsSync2(join2(cwd, "next.config.js")) || existsSync2(join2(cwd, "next.config.ts"))) {
300
368
  return FRAMEWORKS.next;
301
369
  }
@@ -321,7 +389,14 @@ function installCommand(pm, packages) {
321
389
  const verb = pm === "npm" ? "install" : "add";
322
390
  return `${pm} ${verb} ${packages.join(" ")}`;
323
391
  }
392
+ var SERVER_FRAMEWORK_IDS = /* @__PURE__ */ new Set(["express", "fastify", "hono"]);
324
393
  function envVarsToWrite(apiKey, projectId, framework) {
394
+ if (SERVER_FRAMEWORK_IDS.has(framework.id)) {
395
+ return [
396
+ `MUSHI_PROJECT_ID=${projectId}`,
397
+ `MUSHI_API_KEY=${apiKey}`
398
+ ].join("\n");
399
+ }
325
400
  const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : "VITE_";
326
401
  return [
327
402
  `${prefix}MUSHI_PROJECT_ID=${projectId}`,
@@ -520,7 +595,7 @@ function getFrameworkFromPkg(pkg) {
520
595
  }
521
596
 
522
597
  // src/version.ts
523
- var MUSHI_CLI_VERSION = true ? "0.10.0" : "0.0.0-dev";
598
+ var MUSHI_CLI_VERSION = true ? "0.11.1" : "0.0.0-dev";
524
599
 
525
600
  // src/init.ts
526
601
  var ENV_FILES = [".env.local", ".env"];
@@ -569,7 +644,7 @@ function ensureInteractiveOrBailOut(options) {
569
644
  );
570
645
  if (hasAllFlags) return;
571
646
  process.stderr.write(
572
- "mushi-mushi: non-interactive terminal detected.\nPass all of --yes (or --framework), --project-id, and --api-key to run unattended.\nExample: npx mushi-mushi --yes --project-id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --api-key mushi_xxx\nYour project ID is the UUID shown in the Projects page of the Mushi admin console.\n"
647
+ "mushi-mushi: non-interactive terminal detected.\nPass all of --yes (or --framework), --project-id, and --api-key to run unattended.\nExample: npx mushi-mushi --yes --project-id <uuid-from-console> --api-key mushi_xxx\nYour project ID is the UUID shown in the Projects page of the Mushi admin console.\n"
573
648
  );
574
649
  process.exit(1);
575
650
  }
@@ -603,9 +678,9 @@ async function collectCredentials(options) {
603
678
  const existing = loadConfig();
604
679
  const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
605
680
  message: "Project ID",
606
- placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
681
+ placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
607
682
  hint: "Where to find it: https://kensaur.us/mushi-mushi/projects \u2192 click your project \u2192 copy the UUID below the project name.",
608
- validate: (v) => PROJECT_ID_PATTERN.test(v) ? void 0 : "Expected a UUID (xxxxxxxx-xxxx-...) \u2014 copy it from the Mushi admin console Projects page."
683
+ validate: (v) => PROJECT_ID_PATTERN.test(v.trim()) ? void 0 : "Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) \u2014 copy it from the Mushi admin console Projects page."
609
684
  });
610
685
  const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
611
686
  message: "API key",
@@ -617,8 +692,7 @@ async function collectCredentials(options) {
617
692
  const apiKey = sanitizeSecret(rawApiKey);
618
693
  if (!PROJECT_ID_PATTERN.test(projectId)) {
619
694
  throw new Error(
620
- `Invalid project ID. Got: ${redact(projectId)}
621
- Expected a UUID (e.g. 542b34e0-019e-41fe-b900-7b637717bb86) \u2014 copy it from the Projects page in the Mushi console at https://kensaur.us/mushi-mushi/projects`
695
+ `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 the Projects page in the Mushi console at https://kensaur.us/mushi-mushi/projects`
622
696
  );
623
697
  }
624
698
  if (!API_KEY_PATTERN.test(apiKey)) {
@@ -963,7 +1037,7 @@ function depsFromPackageJson(pkg) {
963
1037
  }
964
1038
  function runMigrate(opts = {}) {
965
1039
  const cwd = opts.cwd ?? process.cwd();
966
- const log3 = opts.log ?? ((s) => console.log(s));
1040
+ const log3 = opts.log ?? ((s) => console.warn(s));
967
1041
  const pkg = readPackageJson(cwd);
968
1042
  if (!pkg) {
969
1043
  log3(
@@ -1232,6 +1306,254 @@ function getAbortSignal(external) {
1232
1306
  return activeController.signal;
1233
1307
  }
1234
1308
 
1309
+ // src/nudge.ts
1310
+ var PRESETS = {
1311
+ // Alpha: noisy is OK because there are very few users and you need
1312
+ // signal yesterday. Show all triggers, short cooldowns.
1313
+ alpha: {
1314
+ maxProactivePerSession: 3,
1315
+ dismissCooldownHours: 6,
1316
+ suppressAfterDismissals: 5,
1317
+ pageDwellMinutes: 3,
1318
+ firstSessionSeconds: 30,
1319
+ rageClick: true,
1320
+ longTask: true,
1321
+ apiCascade: true,
1322
+ errorBoundary: true,
1323
+ betaMode: true,
1324
+ featureRequestCard: true
1325
+ },
1326
+ // Beta: most apps spend the longest here. Conservative cadence so
1327
+ // we don't poison the well, but every trigger still on.
1328
+ beta: {
1329
+ maxProactivePerSession: 2,
1330
+ dismissCooldownHours: 24,
1331
+ suppressAfterDismissals: 3,
1332
+ pageDwellMinutes: 5,
1333
+ firstSessionSeconds: 45,
1334
+ rageClick: true,
1335
+ longTask: true,
1336
+ apiCascade: true,
1337
+ errorBoundary: true,
1338
+ betaMode: true,
1339
+ featureRequestCard: true
1340
+ },
1341
+ // GA / production: only the technical signals (error boundary, rage
1342
+ // click). No page-dwell, no welcome — those are explicitly beta-only
1343
+ // patterns. Feature-request card still on because it's user-initiated.
1344
+ ga: {
1345
+ maxProactivePerSession: 1,
1346
+ dismissCooldownHours: 168,
1347
+ suppressAfterDismissals: 2,
1348
+ pageDwellMinutes: 0,
1349
+ firstSessionSeconds: 0,
1350
+ rageClick: true,
1351
+ longTask: false,
1352
+ apiCascade: true,
1353
+ errorBoundary: true,
1354
+ betaMode: false,
1355
+ featureRequestCard: true
1356
+ }
1357
+ };
1358
+ function renderNudgeSnippet(opts) {
1359
+ const p3 = { ...PRESETS[opts.phase], ...opts.overrides };
1360
+ const dwellMs = p3.pageDwellMinutes > 0 ? `${p3.pageDwellMinutes} * 60 * 1000` : null;
1361
+ const firstMs = p3.firstSessionSeconds > 0 ? `${p3.firstSessionSeconds} * 1000` : null;
1362
+ const proactiveLines = [];
1363
+ if (p3.rageClick) proactiveLines.push(` rageClick: true,`);
1364
+ if (p3.longTask) proactiveLines.push(` longTask: true,`);
1365
+ if (p3.apiCascade) proactiveLines.push(` apiCascade: true,`);
1366
+ if (p3.errorBoundary) proactiveLines.push(` errorBoundary: true,`);
1367
+ if (dwellMs) proactiveLines.push(` pageDwell: { thresholdMs: ${dwellMs} }, // ${p3.pageDwellMinutes}min on the same route`);
1368
+ if (firstMs) proactiveLines.push(` firstSession: { delayMs: ${firstMs} }, // welcome new users after ${p3.firstSessionSeconds}s`);
1369
+ proactiveLines.push(` cooldown: {`);
1370
+ proactiveLines.push(` maxProactivePerSession: ${p3.maxProactivePerSession},`);
1371
+ proactiveLines.push(` dismissCooldownHours: ${p3.dismissCooldownHours},`);
1372
+ proactiveLines.push(` suppressAfterDismissals: ${p3.suppressAfterDismissals},`);
1373
+ proactiveLines.push(` },`);
1374
+ const widgetLines = [];
1375
+ if (p3.featureRequestCard) widgetLines.push(` featureRequestCard: true,`);
1376
+ if (p3.betaMode) {
1377
+ widgetLines.push(` betaMode: {`);
1378
+ widgetLines.push(` enabled: true,`);
1379
+ widgetLines.push(` label: 'BETA',`);
1380
+ widgetLines.push(` // changelogItems: [{ version: 'v0.42', date: '${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}', items: ['\u2026'] }],`);
1381
+ widgetLines.push(` },`);
1382
+ }
1383
+ return [
1384
+ `// Generated by \`mushi nudge --phase ${opts.phase}\``,
1385
+ `// Phase: ${opts.phase} \u2014 tune via --max, --cooldown, --dwell, --welcome.`,
1386
+ `Mushi.init({`,
1387
+ ` projectId: process.env.MUSHI_PROJECT_ID!,`,
1388
+ ` apiKey: process.env.MUSHI_API_KEY!,`,
1389
+ ` proactive: {`,
1390
+ ...proactiveLines,
1391
+ ` },`,
1392
+ ...widgetLines.length ? [` widget: {`, ...widgetLines, ` },`] : [],
1393
+ `})`,
1394
+ ``
1395
+ ].join("\n");
1396
+ }
1397
+ function renderNudgeExplainer(phase) {
1398
+ const p3 = PRESETS[phase];
1399
+ const lines = [];
1400
+ lines.push(``);
1401
+ lines.push(`Nudge preset for "${phase}" phase:`);
1402
+ lines.push(` - max ${p3.maxProactivePerSession} proactive prompts per session`);
1403
+ lines.push(` - ${p3.dismissCooldownHours}h cooldown after a dismissal`);
1404
+ lines.push(` - suppress permanently after ${p3.suppressAfterDismissals} consecutive dismissals`);
1405
+ if (p3.pageDwellMinutes > 0) lines.push(` - fire page-dwell trigger after ${p3.pageDwellMinutes} continuous minutes on a route`);
1406
+ if (p3.firstSessionSeconds > 0) lines.push(` - welcome new users with a button pulse ${p3.firstSessionSeconds}s after init`);
1407
+ lines.push(` - signals enabled: ${[
1408
+ p3.rageClick && "rage-click",
1409
+ p3.longTask && "long-task",
1410
+ p3.apiCascade && "api-cascade",
1411
+ p3.errorBoundary && "error-boundary"
1412
+ ].filter(Boolean).join(", ")}`);
1413
+ lines.push(` - feature-request card: ${p3.featureRequestCard ? "shown" : "hidden"}`);
1414
+ lines.push(` - beta-mode UI: ${p3.betaMode ? "on" : "off"}`);
1415
+ lines.push(``);
1416
+ return lines.join("\n");
1417
+ }
1418
+
1419
+ // src/doctor.ts
1420
+ function checkCliConfig(config) {
1421
+ return [
1422
+ {
1423
+ name: "CLI config file",
1424
+ ok: Boolean(config.endpoint),
1425
+ detail: config.endpoint ? `endpoint=${config.endpoint}` : "No endpoint in ~/.mushirc \u2014 run `mushi init` or `mushi config endpoint <url>`"
1426
+ },
1427
+ {
1428
+ name: "API key configured",
1429
+ ok: Boolean(config.apiKey),
1430
+ detail: config.apiKey ? `apiKey=${config.apiKey.slice(0, 8)}\u2026${config.apiKey.slice(-4)}` : "No API key set \u2014 run `mushi login --api-key <key>`"
1431
+ },
1432
+ {
1433
+ name: "Project ID configured",
1434
+ ok: Boolean(config.projectId),
1435
+ detail: config.projectId ? `projectId=${config.projectId}` : "No default project \u2014 set via `mushi config projectId <uuid>`"
1436
+ }
1437
+ ];
1438
+ }
1439
+ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
1440
+ try {
1441
+ const res = await doFetch(`${endpoint}/health`, {
1442
+ signal: AbortSignal.timeout(5e3)
1443
+ });
1444
+ return {
1445
+ name: "Endpoint reachable",
1446
+ ok: res.status === 200,
1447
+ detail: `GET ${endpoint}/health \u2192 ${res.status}`
1448
+ };
1449
+ } catch (err) {
1450
+ const msg = err instanceof Error ? err.message : String(err);
1451
+ return { name: "Endpoint reachable", ok: false, detail: `Fetch failed: ${msg}` };
1452
+ }
1453
+ }
1454
+ async function checkSdkInstall(cwd) {
1455
+ try {
1456
+ const { readFile: readFile2, access } = await import("fs/promises");
1457
+ const { join: join6, resolve: resolve2 } = await import("path");
1458
+ const root = resolve2(cwd);
1459
+ const pkgPath = join6(root, "package.json");
1460
+ await access(pkgPath);
1461
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
1462
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1463
+ const sdks = [
1464
+ "@mushi-mushi/react",
1465
+ "@mushi-mushi/web",
1466
+ "@mushi-mushi/core",
1467
+ "@mushi-mushi/react-native"
1468
+ ];
1469
+ const installed2 = sdks.filter((s) => deps[s]);
1470
+ return {
1471
+ name: "SDK installed in this repo",
1472
+ ok: installed2.length > 0,
1473
+ detail: installed2.length > 0 ? installed2.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
1474
+ };
1475
+ } catch {
1476
+ return null;
1477
+ }
1478
+ }
1479
+ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
1480
+ if (!config.projectId || !config.apiKey || !config.endpoint) {
1481
+ return [
1482
+ {
1483
+ name: "Server preflight",
1484
+ ok: false,
1485
+ detail: "Need projectId, apiKey, and endpoint. Run `mushi login` and `mushi config projectId <uuid>`."
1486
+ }
1487
+ ];
1488
+ }
1489
+ try {
1490
+ const res = await doFetch(
1491
+ `${config.endpoint}/v1/admin/projects/${config.projectId}/preflight`,
1492
+ {
1493
+ headers: {
1494
+ Authorization: `Bearer ${config.apiKey}`,
1495
+ "X-Mushi-Api-Key": config.apiKey,
1496
+ "X-Mushi-Project": config.projectId
1497
+ },
1498
+ signal: AbortSignal.timeout(8e3)
1499
+ }
1500
+ );
1501
+ if (res.ok) {
1502
+ const body = await res.json();
1503
+ const serverChecks = body.data?.checks ?? [];
1504
+ return serverChecks.map((sc) => ({
1505
+ name: `[server] ${sc.label}`,
1506
+ ok: sc.ready,
1507
+ detail: sc.hint
1508
+ }));
1509
+ }
1510
+ const text2 = await res.text().catch(() => "");
1511
+ return [
1512
+ {
1513
+ name: "Server preflight",
1514
+ ok: false,
1515
+ detail: `HTTP ${res.status}: ${text2.slice(0, 120)}`
1516
+ }
1517
+ ];
1518
+ } catch (err) {
1519
+ const msg = err instanceof Error ? err.message : String(err);
1520
+ return [{ name: "Server preflight", ok: false, detail: `Fetch failed: ${msg}` }];
1521
+ }
1522
+ }
1523
+ async function runDoctor(config, options = {}) {
1524
+ const doFetch = options.fetch ?? globalThis.fetch;
1525
+ const checks = [];
1526
+ checks.push(...checkCliConfig(config));
1527
+ if (config.endpoint) {
1528
+ checks.push(await checkEndpointReachability(config.endpoint, doFetch));
1529
+ }
1530
+ const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
1531
+ if (sdkCheck) checks.push(sdkCheck);
1532
+ if (options.server) {
1533
+ const serverChecks = await checkServerPreflight(config, doFetch);
1534
+ checks.push(...serverChecks);
1535
+ }
1536
+ return { checks, ready: checks.every((c) => c.ok) };
1537
+ }
1538
+ function formatDoctorResult(result) {
1539
+ const PASS = "\u2713";
1540
+ const FAIL = "\u2717";
1541
+ const lines = [];
1542
+ for (const c of result.checks) {
1543
+ lines.push(`${c.ok ? PASS : FAIL} ${c.name}`);
1544
+ lines.push(` ${c.detail}`);
1545
+ }
1546
+ const failed = result.checks.filter((c) => !c.ok);
1547
+ if (failed.length === 0) {
1548
+ lines.push("\nAll checks passed. The CLI is ready.");
1549
+ } else {
1550
+ lines.push(`
1551
+ ${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
1552
+ lines.push("Fix the items above and re-run `mushi doctor`.");
1553
+ }
1554
+ return lines.join("\n");
1555
+ }
1556
+
1235
1557
  // src/index.ts
1236
1558
  installSignalHandlers();
1237
1559
  var API_TIMEOUT_MS = 15e3;
@@ -1945,6 +2267,260 @@ Examples:
1945
2267
  silent: opts.silent
1946
2268
  });
1947
2269
  });
2270
+ var project = program.command("project").description("Project management");
2271
+ project.command("create").description("Create a new Mushi project, mint an API key, and write config files").option("--name <name>", "Project name (skip the prompt)").option("--no-browser", "Skip opening the browser for the sign-up / magic-link step").option("--endpoint <url>", "Override API endpoint (self-hosted)").addHelpText("after", `
2272
+ Creates a project on app.mushimushi.dev, mints an API key with mcp:read+write scope,
2273
+ and writes the following to the current directory:
2274
+ .env.local \u2014 MUSHI_API_KEY, MUSHI_PROJECT_ID, MUSHI_API_ENDPOINT
2275
+ .cursor/mcp.json \u2014 pre-filled mcpServers.mushi block for Cursor
2276
+
2277
+ Typical first-time flow:
2278
+ npx mushi-mushi project create
2279
+ # Browser opens \u2192 sign up / magic-link \u2192 come back to terminal
2280
+ # CLI writes .env.local and .cursor/mcp.json
2281
+ # mushi whoami to confirm`).action(async (opts) => {
2282
+ const { writeFile, mkdir } = await import("fs/promises");
2283
+ const { existsSync: existsSync5 } = await import("fs");
2284
+ const nodePath = await import("path");
2285
+ const endpoint = opts.endpoint ?? loadConfig().endpoint ?? "https://api.mushimushi.dev";
2286
+ const signUpUrl = "https://kensaur.us/mushi-mushi/sign-up";
2287
+ console.log("");
2288
+ console.log(" Mushi project create");
2289
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2290
+ console.log("");
2291
+ if (opts.browser !== false) {
2292
+ console.log(" 1. Opening the Mushi sign-up page in your browser...");
2293
+ try {
2294
+ const { exec } = await import("child_process");
2295
+ const openCmd = process.platform === "win32" ? `start "" "${signUpUrl}"` : process.platform === "darwin" ? `open "${signUpUrl}"` : `xdg-open "${signUpUrl}"`;
2296
+ exec(openCmd);
2297
+ } catch {
2298
+ }
2299
+ } else {
2300
+ console.log(` 1. Sign up or log in at: ${signUpUrl}`);
2301
+ }
2302
+ console.log("");
2303
+ console.log(" 2. Create a project in the console, then paste your credentials below.");
2304
+ console.log(" (Settings \u2192 API Keys \u2192 New key \u2192 Copy as .env.local)");
2305
+ console.log("");
2306
+ const { createInterface } = await import("readline");
2307
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2308
+ const ask = (q) => new Promise((resolve2) => rl.question(q, (a) => resolve2(a.trim())));
2309
+ const projectId = await ask(" Project ID (uuid): ");
2310
+ const apiKey = await ask(" API key (mushi_...): ");
2311
+ rl.close();
2312
+ if (!projectId || !apiKey) {
2313
+ process.stderr.write("\nerror: Project ID and API key are required.\n");
2314
+ process.exit(2);
2315
+ }
2316
+ const config = loadConfig();
2317
+ config.apiKey = apiKey;
2318
+ config.endpoint = endpoint;
2319
+ config.projectId = projectId;
2320
+ saveConfig(config);
2321
+ const cwd = process.cwd();
2322
+ const envPath = nodePath.join(cwd, ".env.local");
2323
+ const envLines = [
2324
+ "# Mushi MCP \u2014 drop into .env.local (gitignored). The MCP binary picks these up on spawn.",
2325
+ `MUSHI_API_ENDPOINT=${endpoint}`,
2326
+ `MUSHI_PROJECT_ID=${projectId}`,
2327
+ `MUSHI_API_KEY=${apiKey}`,
2328
+ ""
2329
+ ];
2330
+ const envExisting = existsSync5(envPath);
2331
+ await writeFile(envPath, envLines.join("\n"), "utf8");
2332
+ console.log(`
2333
+ \u2713 ${envExisting ? "Updated" : "Created"} .env.local`);
2334
+ const mcpDir = nodePath.join(cwd, ".cursor");
2335
+ await mkdir(mcpDir, { recursive: true });
2336
+ const mcpPath = nodePath.join(mcpDir, "mcp.json");
2337
+ const mcpJson = {
2338
+ mcpServers: {
2339
+ mushi: {
2340
+ command: "npx",
2341
+ args: ["-y", "mushi-mcp@latest"],
2342
+ env: {
2343
+ MUSHI_API_ENDPOINT: endpoint,
2344
+ MUSHI_PROJECT_ID: projectId,
2345
+ MUSHI_API_KEY: apiKey
2346
+ }
2347
+ }
2348
+ }
2349
+ };
2350
+ const mcpExisting = existsSync5(mcpPath);
2351
+ if (mcpExisting) {
2352
+ try {
2353
+ const { readFile: readFile2 } = await import("fs/promises");
2354
+ const raw = JSON.parse(await readFile2(mcpPath, "utf8"));
2355
+ const existing = raw;
2356
+ existing.mcpServers = { ...existing.mcpServers ?? {}, mushi: mcpJson.mcpServers.mushi };
2357
+ await writeFile(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
2358
+ } catch {
2359
+ await writeFile(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf8");
2360
+ }
2361
+ } else {
2362
+ await writeFile(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf8");
2363
+ }
2364
+ console.log(` \u2713 ${mcpExisting ? "Updated" : "Created"} .cursor/mcp.json`);
2365
+ console.log("");
2366
+ console.log(' Done! Restart Cursor and ask: "list mushi tools"');
2367
+ console.log(" Run `mushi whoami` to verify the connection.");
2368
+ console.log("");
2369
+ });
2370
+ program.command("setup").description("Wire Mushi into your IDE with one command").option("--ide <ide>", "Target IDE: cursor | claude | continue | zed", "cursor").option("--project-slug <slug>", "Override the project slug in the server name (default: project ID prefix)").option("--with-rules", "Also write the .cursorrules / .claude/rules/mushi.md lesson-library hook").option("--dry-run", "Print what would be written without making changes").addHelpText("after", `
2371
+ Examples:
2372
+ mushi setup # wire Cursor (default)
2373
+ mushi setup --ide claude # wire Claude Code
2374
+ mushi setup --ide cursor --with-rules # also write .cursorrules
2375
+
2376
+ Supported IDEs:
2377
+ cursor \u2014 writes .cursor/mcp.json
2378
+ claude \u2014 writes .claude/mcp.json (Claude Code / Claude Desktop)
2379
+ continue \u2014 writes .continue/mcp.json
2380
+ zed \u2014 writes ~/.config/zed/settings.json mcpServers block
2381
+
2382
+ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).action(async (opts) => {
2383
+ const { writeFile, mkdir, readFile: readFile2 } = await import("fs/promises");
2384
+ const { existsSync: existsSync5 } = await import("fs");
2385
+ const nodePath = await import("path");
2386
+ const os = await import("os");
2387
+ const config = requireConfig({ needsProject: true });
2388
+ const IDE_CONFIG = {
2389
+ cursor: { dir: ".cursor", file: "mcp.json", format: "mcp-json" },
2390
+ claude: { dir: ".claude", file: "mcp.json", format: "mcp-json" },
2391
+ continue: { dir: ".continue", file: "mcp.json", format: "mcp-json" },
2392
+ zed: { dir: nodePath.join(os.homedir(), ".config", "zed"), file: "settings.json", format: "zed" }
2393
+ };
2394
+ const ideEntry = IDE_CONFIG[opts.ide];
2395
+ if (!ideEntry) {
2396
+ process.stderr.write(`error: unsupported IDE "${opts.ide}". Supported: ${Object.keys(IDE_CONFIG).join(", ")}
2397
+ `);
2398
+ process.exit(2);
2399
+ }
2400
+ const cwd = process.cwd();
2401
+ const slug = opts.projectSlug ?? (config.projectId?.slice(0, 8) ?? "mushi");
2402
+ const serverName = `mushi-${slug}`;
2403
+ const mcpServerBlock = {
2404
+ command: "npx",
2405
+ args: ["-y", "mushi-mcp@latest"],
2406
+ env: {
2407
+ MUSHI_API_ENDPOINT: config.endpoint,
2408
+ MUSHI_PROJECT_ID: config.projectId ?? "",
2409
+ MUSHI_API_KEY: config.apiKey
2410
+ }
2411
+ };
2412
+ const configDir = ideEntry.dir.startsWith("/") ? ideEntry.dir : nodePath.join(cwd, ideEntry.dir);
2413
+ const configPath = nodePath.join(configDir, ideEntry.file);
2414
+ if (ideEntry.format === "mcp-json") {
2415
+ let merged = { mcpServers: {} };
2416
+ if (existsSync5(configPath)) {
2417
+ try {
2418
+ const raw = await readFile2(configPath, "utf8");
2419
+ merged = JSON.parse(raw);
2420
+ } catch {
2421
+ }
2422
+ }
2423
+ const servers = merged.mcpServers ?? {};
2424
+ servers[serverName] = mcpServerBlock;
2425
+ merged.mcpServers = servers;
2426
+ const output = JSON.stringify(merged, null, 2) + "\n";
2427
+ if (opts.dryRun) {
2428
+ console.log(`[dry-run] Would write ${configPath}:`);
2429
+ console.log(output);
2430
+ } else {
2431
+ await mkdir(configDir, { recursive: true });
2432
+ await writeFile(configPath, output, "utf8");
2433
+ console.log(`\u2713 Written ${configPath}`);
2434
+ }
2435
+ } else if (ideEntry.format === "zed") {
2436
+ let settings = {};
2437
+ if (existsSync5(configPath)) {
2438
+ try {
2439
+ const raw = await readFile2(configPath, "utf8");
2440
+ settings = JSON.parse(raw);
2441
+ } catch {
2442
+ }
2443
+ }
2444
+ const servers = settings.context_servers ?? {};
2445
+ servers[serverName] = {
2446
+ command: {
2447
+ path: "npx",
2448
+ args: ["-y", "mushi-mcp@latest"],
2449
+ env: {
2450
+ MUSHI_API_ENDPOINT: config.endpoint,
2451
+ MUSHI_PROJECT_ID: config.projectId ?? "",
2452
+ MUSHI_API_KEY: config.apiKey
2453
+ }
2454
+ },
2455
+ settings: {}
2456
+ };
2457
+ settings.context_servers = servers;
2458
+ const output = JSON.stringify(settings, null, 2) + "\n";
2459
+ if (opts.dryRun) {
2460
+ console.log(`[dry-run] Would write ${configPath}:`);
2461
+ console.log(output);
2462
+ } else {
2463
+ await mkdir(configDir, { recursive: true });
2464
+ await writeFile(configPath, output, "utf8");
2465
+ console.log(`\u2713 Written ${configPath}`);
2466
+ }
2467
+ }
2468
+ if (opts.withRules) {
2469
+ const rulesContent = [
2470
+ "# Mushi Mushi \u2014 evolution-loop coding rules",
2471
+ "#",
2472
+ "# These rules are generated from your project's live lesson library.",
2473
+ "# Run `mushi sync-lessons` to refresh .mushi/lessons.json",
2474
+ "# The MCP server (mushi tools) also injects lessons dynamically at fix time.",
2475
+ "",
2476
+ "## Before writing a fix",
2477
+ "",
2478
+ "1. Call `get_fix_context` (MCP) for the report \u2014 get root cause + blast radius first.",
2479
+ "2. Call `lessons.query` (MCP) or read .mushi/lessons.json \u2014 apply every matching rule.",
2480
+ "3. Prefer the smallest change that makes the test pass. Don't refactor unrelated code.",
2481
+ "",
2482
+ "## After writing a fix",
2483
+ "",
2484
+ "1. Call `submit_fix_result` (MCP) with the branch, PR URL, and files changed.",
2485
+ "2. The judge batch will score the fix overnight \u2014 high-frequency lessons surface in /admin/lessons.",
2486
+ "",
2487
+ "## Mushi lesson library (auto-updated by `mushi sync-lessons`)",
2488
+ "",
2489
+ "<!-- lessons synced from .mushi/lessons.json -->",
2490
+ "<!-- run `mushi sync-lessons` to refresh -->",
2491
+ ""
2492
+ ].join("\n");
2493
+ if (opts.ide === "cursor") {
2494
+ const rulesPath = nodePath.join(cwd, ".cursorrules");
2495
+ if (opts.dryRun) {
2496
+ console.log(`[dry-run] Would write ${rulesPath}`);
2497
+ } else {
2498
+ await writeFile(rulesPath, rulesContent, "utf8");
2499
+ console.log(`\u2713 Written .cursorrules`);
2500
+ }
2501
+ } else if (opts.ide === "claude") {
2502
+ const rulesDir = nodePath.join(cwd, ".claude", "rules");
2503
+ const rulesPath = nodePath.join(rulesDir, "mushi.md");
2504
+ if (opts.dryRun) {
2505
+ console.log(`[dry-run] Would write ${rulesPath}`);
2506
+ } else {
2507
+ await mkdir(rulesDir, { recursive: true });
2508
+ await writeFile(rulesPath, rulesContent, "utf8");
2509
+ console.log(`\u2713 Written .claude/rules/mushi.md`);
2510
+ }
2511
+ }
2512
+ }
2513
+ if (!opts.dryRun) {
2514
+ console.log("");
2515
+ console.log(`Done! Restart ${opts.ide === "cursor" ? "Cursor" : opts.ide === "claude" ? "Claude Code" : opts.ide} and ask: "list mushi tools"`);
2516
+ if (!opts.withRules) {
2517
+ console.log(`Tip: run with --with-rules to also write the lesson-library coding hook.`);
2518
+ }
2519
+ const configRelPath = ideEntry.dir.startsWith("/") ? configPath : nodePath.relative(cwd, configPath);
2520
+ console.log(`
2521
+ Note: ${configRelPath} contains your Mushi API key \u2014 add it to .gitignore if this is a shared repo.`);
2522
+ }
2523
+ });
1948
2524
  var fixCmd = program.command("fix").description("Dispatch an agentic fix for a report");
1949
2525
  fixCmd.argument("<reportId>", "Report UUID to fix").option(
1950
2526
  "--agent <name>",
@@ -2034,4 +2610,151 @@ Examples:
2034
2610
  console.error("Polling timed out after 10 minutes. The fix may still be running.");
2035
2611
  process.exit(1);
2036
2612
  });
2613
+ program.command("nudge").description(
2614
+ "Generate a Mushi.init() snippet tuned for your release phase (alpha, beta, ga). Customises proactive triggers, cooldowns, feature-request card, and beta-mode UI."
2615
+ ).option("--phase <phase>", "Release phase: alpha | beta | ga", "beta").option("--explain", "Print a human-readable summary of what the preset does").option("--max <n>", "Override maxProactivePerSession").option("--cooldown <hours>", "Override dismissCooldownHours").option("--dwell <minutes>", "Override page-dwell threshold (0 disables)").option("--welcome <seconds>", "Override first-session welcome delay (0 disables)").action((opts) => {
2616
+ const validPhases = ["alpha", "beta", "ga"];
2617
+ if (!validPhases.includes(opts.phase)) {
2618
+ console.error(`Unknown phase "${opts.phase}". Use one of: ${validPhases.join(", ")}`);
2619
+ process.exit(1);
2620
+ }
2621
+ const phase = opts.phase;
2622
+ const overrides = {};
2623
+ const parseNumericFlag = (flag, raw, min) => {
2624
+ const n = Number(raw);
2625
+ if (!Number.isFinite(n) || n < min) {
2626
+ console.error(
2627
+ `error: --${flag} must be a finite number >= ${min} (got "${raw}")`
2628
+ );
2629
+ process.exit(1);
2630
+ }
2631
+ return n;
2632
+ };
2633
+ if (opts.max !== void 0) overrides.maxProactivePerSession = parseNumericFlag("max", opts.max, 1);
2634
+ if (opts.cooldown !== void 0) overrides.dismissCooldownHours = parseNumericFlag("cooldown", opts.cooldown, 0);
2635
+ if (opts.dwell !== void 0) overrides.pageDwellMinutes = parseNumericFlag("dwell", opts.dwell, 0);
2636
+ if (opts.welcome !== void 0) overrides.firstSessionSeconds = parseNumericFlag("welcome", opts.welcome, 0);
2637
+ if (opts.explain) {
2638
+ console.log(renderNudgeExplainer(phase));
2639
+ }
2640
+ console.log(renderNudgeSnippet({ phase, overrides }));
2641
+ });
2642
+ program.command("doctor").description(
2643
+ "Run pre-flight checks: CLI config, endpoint reachability, API key shape, SDK install status, and (with --server) the same 4 dispatch-readiness checks shown in the Mushi console. Mirrors the in-console dispatch preflight so you can spot setup gaps before opening the admin UI."
2644
+ ).option("--cwd <path>", "Run package detection from a different directory").option("--json", "Machine-readable output").option(
2645
+ "--server",
2646
+ "Also call GET /preflight on the backend and include the 4 dispatch checks (GitHub repo, codebase indexed, Anthropic key, autofix enabled). Requires a configured projectId and API key."
2647
+ ).action(async (opts) => {
2648
+ const config = loadConfig();
2649
+ const result = await runDoctor(config, { cwd: opts.cwd, server: opts.server });
2650
+ const { checks } = result;
2651
+ if (opts.json) {
2652
+ console.log(JSON.stringify({ checks, ready: result.ready }, null, 2));
2653
+ if (!result.ready) process.exit(1);
2654
+ return;
2655
+ }
2656
+ console.log(formatDoctorResult(result));
2657
+ if (!result.ready) process.exit(1);
2658
+ });
2659
+ program.command("reset [projectId]").description(
2660
+ "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."
2661
+ ).option("--confirm", "Required safety flag \u2014 must pass to proceed").option("--json", "Machine-readable output").action(async (projectId, opts) => {
2662
+ const config = loadConfig();
2663
+ const resolvedId = projectId ?? config.projectId;
2664
+ if (!config.apiKey) {
2665
+ console.error("Run `mushi login` first");
2666
+ process.exit(1);
2667
+ }
2668
+ if (!resolvedId) {
2669
+ console.error("Provide a projectId or set one via `mushi config projectId <uuid>`");
2670
+ process.exit(1);
2671
+ }
2672
+ if (!opts.confirm) {
2673
+ console.error(
2674
+ `This will archive project ${resolvedId} and delete all its reports, fix_attempts, and codebase_files.
2675
+ Re-run with --confirm to proceed.`
2676
+ );
2677
+ process.exit(1);
2678
+ }
2679
+ const data = await apiCall(
2680
+ `/v1/admin/projects/${resolvedId}/reset`,
2681
+ config,
2682
+ { method: "POST" }
2683
+ );
2684
+ if (opts.json) {
2685
+ console.log(JSON.stringify(data, null, 2));
2686
+ } else if (data.ok) {
2687
+ console.log(`Project ${resolvedId} archived and test data wiped.`);
2688
+ } else {
2689
+ console.error("Reset failed:", JSON.stringify(data, null, 2));
2690
+ process.exit(1);
2691
+ }
2692
+ });
2693
+ var fixes = program.command("fixes").description("Fix dispatch management");
2694
+ fixes.command("tail").description(
2695
+ "Stream SSE dispatch events for a report in real time. Useful for headless debugging without opening the browser."
2696
+ ).requiredOption("--report-id <id>", "Report ID to follow").action(async (opts) => {
2697
+ const config = loadConfig();
2698
+ if (!config.apiKey) {
2699
+ console.error("Run `mushi login` first");
2700
+ process.exit(1);
2701
+ }
2702
+ if (!config.endpoint) {
2703
+ console.error("No endpoint configured. Run `mushi init`");
2704
+ process.exit(1);
2705
+ }
2706
+ const url = `${config.endpoint}/v1/admin/reports/${opts.reportId}/dispatch/stream`;
2707
+ console.log(`Tailing dispatch stream for report ${opts.reportId}\u2026`);
2708
+ console.log(`(Ctrl-C to stop)
2709
+ `);
2710
+ const res = await fetch(url, {
2711
+ headers: {
2712
+ "Authorization": `Bearer ${config.apiKey}`,
2713
+ "X-Mushi-Api-Key": config.apiKey,
2714
+ "X-Mushi-Project": config.projectId ?? "",
2715
+ "Accept": "text/event-stream"
2716
+ }
2717
+ });
2718
+ if (!res.ok || !res.body) {
2719
+ console.error(`Failed to connect: HTTP ${res.status}`);
2720
+ const text2 = await res.text().catch(() => "");
2721
+ if (text2) console.error(text2.slice(0, 300));
2722
+ process.exit(1);
2723
+ }
2724
+ const decoder = new TextDecoder();
2725
+ const reader = res.body.getReader();
2726
+ let done = false;
2727
+ process.on("SIGINT", () => {
2728
+ done = true;
2729
+ void reader.cancel();
2730
+ console.log("\nDisconnected.");
2731
+ process.exit(0);
2732
+ });
2733
+ while (!done) {
2734
+ const { value, done: streamDone } = await reader.read();
2735
+ if (streamDone) break;
2736
+ const chunk = decoder.decode(value, { stream: true });
2737
+ for (const line of chunk.split("\n")) {
2738
+ if (line.startsWith("data: ")) {
2739
+ const raw = line.slice(6).trim();
2740
+ if (raw === "[DONE]") {
2741
+ console.log("\n[stream ended]");
2742
+ process.exit(0);
2743
+ }
2744
+ try {
2745
+ const event = JSON.parse(raw);
2746
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2747
+ const type = event.type ?? event.event ?? "event";
2748
+ const status = event.status ?? event.data ?? "";
2749
+ console.log(`${ts} ${type.padEnd(24)} ${status}`);
2750
+ } catch {
2751
+ console.log(line);
2752
+ }
2753
+ } else if (line.startsWith("event: ")) {
2754
+ } else if (line && !line.startsWith(":")) {
2755
+ console.log(line);
2756
+ }
2757
+ }
2758
+ }
2759
+ });
2037
2760
  program.parse();