@mushi-mushi/cli 0.26.0 → 0.26.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/CONTRIBUTING.md CHANGED
@@ -76,6 +76,7 @@ packages/
76
76
  apps/
77
77
  admin/ Admin dashboard (React + Tailwind)
78
78
  docs/ Documentation site (Nextra — apps/docs)
79
+ testers/ Tester marketplace portal (Next.js)
79
80
  tooling/
80
81
  eslint-config/ Shared ESLint flat config
81
82
  tsconfig/ Shared TypeScript configs
@@ -118,15 +119,22 @@ If your PR changes **user-visible SDK or CLI behavior**, update docs in the **sa
118
119
  Guardrails (run locally before pushing):
119
120
 
120
121
  ```bash
121
- pnpm check:docs-stats # root README stat badges
122
+ pnpm check:docs-stats # root README stat badges + glance <sub> line
122
123
  pnpm check:sdk-version-matrix # apps/docs/content/sdks/index.mdx versions
123
124
  pnpm check:internal-doc-links # dead /foo links in MDX
124
- pnpm check:onboarding-drift # phantom MCP env vars / stale tool names
125
+ pnpm check:onboarding-drift # phantom MCP env vars / stale tool names / native install coords
126
+ pnpm check:catalog-sync # MCP catalog ↔ admin counts ↔ prose "N tools"
127
+ pnpm check:mcp-tools-doc # mcp-tools.generated.mdx stale check
125
128
  pnpm check:bundle-docs # @mushi-mushi/web size-limit vs README
129
+ pnpm check:docs-drift # versions + public docs links
126
130
  pnpm gen:llms-txt # regenerate apps/docs/public/llms.txt after nav changes
127
131
  pnpm gen:mcp-tools-doc # regenerate sdks/mcp-tools.generated.mdx from catalog.ts
128
132
  ```
129
133
 
134
+ **Same-PR rule:** if behavior, env vars, CLI commands, MCP tools, or install
135
+ coordinates change, update the matching docs in this PR and run
136
+ `pnpm check:docs-drift && pnpm check:onboarding-drift && pnpm check:catalog-sync`.
137
+
130
138
  README changes on npm packages only appear on [npmjs.com](https://www.npmjs.com) after the next **Changesets publish**.
131
139
 
132
140
  ## Changesets
package/SECURITY.md CHANGED
@@ -111,7 +111,7 @@ What we treat as in-scope attacker capabilities, and what we don't.
111
111
  | User pasting a Stripe / OpenAI / GitHub PAT into a bug report | ✅ | PII scrubber redacts ~15 vendor token formats client-side before the report leaves the device. Mirrors `packages/core/src/pii-scrubber.ts` across iOS, Android, Flutter, React Native. |
112
112
  | Stolen end-user device with the SDK installed | ⚠️ partial | Offline queue is AsyncStorage / Keychain / SharedPreferences — no app-level encryption beyond the OS default. Reports waiting to flush are vulnerable to a forensic attacker. |
113
113
  | Compromised Supabase service-role key | ❌ | Treated as a tier-0 incident; would require key rotation and audit-log forensics. Not defendable in software. |
114
- | Compromise of `kensaurus@gmail.com` | ❌ | Treated as a project-fork event; downstream consumers should pin to the last known-good version and follow the new release channel. |
114
+ | Compromise of `kensaurus@gmail.com` mail account | ❌ | Treated as a project-fork event; downstream consumers should pin to the last known-good version and follow the new release channel. |
115
115
  | Physical / OS-level attacker on an end-user device | ❌ | Out of scope. |
116
116
  | Malicious fork using the Mushi name to ship malware | ❌ (technical) ✅ (legal) | The MIT / AGPLv3 grant lets the fork exist; the trademark policy (`TRADEMARK.md`) makes shipping it under the Mushi name an infringement we will pursue. |
117
117
 
@@ -157,6 +157,29 @@ Structured fields you set explicitly (`metadata.userEmail`,
157
157
  opt-in attribution data, and silently rewriting them would break
158
158
  support workflows.
159
159
 
160
+ ### CLI funnel telemetry
161
+
162
+ The Mushi CLI sends two fire-and-forget signals to `/v1/cli/funnel` on your
163
+ project's endpoint to help us detect where the onboarding wizard drops off:
164
+
165
+ | Signal | When | What is sent |
166
+ |--------|------|-------------|
167
+ | `wizard_env_written` | After `npx mushi-mushi` writes `.env.local` | `event`, `source: "cli"` |
168
+ | `mcp_setup_done` | After `mushi setup --ide` verifies the MCP key | `event`, `source: "cli"` |
169
+
170
+ No file contents, stack traces, or identifiers beyond your project ID and API
171
+ key are sent. The key is already on the endpoint — this adds no new disclosure.
172
+
173
+ **Opt out** by setting `MUSHI_NO_TELEMETRY=1` in your shell or CI environment:
174
+
175
+ ```bash
176
+ MUSHI_NO_TELEMETRY=1 npx mushi-mushi
177
+ MUSHI_NO_TELEMETRY=1 mushi setup --ide cursor
178
+ ```
179
+
180
+ These signals never block or delay the wizard — they time out silently after
181
+ 4 seconds and errors are discarded.
182
+
160
183
  ### Where data lives
161
184
 
162
185
  - **Reports & telemetry** — Supabase Postgres in the `us-west-1` region.
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- MUSHI_CLI_VERSION
4
- } from "./chunk-TYVJU4AH.js";
3
+ MUSHI_CLI_VERSION,
4
+ MUSHI_MCP_PIN_SPEC
5
+ } from "./chunk-6XYPFV23.js";
5
6
  import {
6
7
  CLOUD_API_ENDPOINT,
7
8
  assertEndpoint,
@@ -1075,7 +1076,7 @@ async function writeMcpServerEntry(opts) {
1075
1076
  function buildMcpServerBlock(opts) {
1076
1077
  return {
1077
1078
  command: "npx",
1078
- args: ["-y", "@mushi-mushi/mcp@latest"],
1079
+ args: ["-y", MUSHI_MCP_PIN_SPEC],
1079
1080
  env: {
1080
1081
  MUSHI_API_ENDPOINT: opts.endpoint,
1081
1082
  MUSHI_PROJECT_ID: opts.projectId,
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/version.ts
4
+ var MUSHI_CLI_VERSION = true ? "0.26.1" : "0.0.0-dev";
5
+ var MUSHI_MCP_PIN_SPEC = "@mushi-mushi/mcp@0.19.0";
6
+
7
+ export {
8
+ MUSHI_CLI_VERSION,
9
+ MUSHI_MCP_PIN_SPEC
10
+ };
@@ -0,0 +1,8 @@
1
+ // src/version.ts
2
+ var MUSHI_CLI_VERSION = true ? "0.26.1" : "0.0.0-dev";
3
+ var MUSHI_MCP_PIN_SPEC = "@mushi-mushi/mcp@0.19.0";
4
+
5
+ export {
6
+ MUSHI_CLI_VERSION,
7
+ MUSHI_MCP_PIN_SPEC
8
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-MCVRTQAI.js";
3
+ } from "./chunk-PCTMFRKN.js";
4
4
 
5
5
  // src/config.ts
6
6
  import { randomUUID } from "crypto";
@@ -415,6 +415,117 @@ async function apiCall(path, config, options = {}) {
415
415
  }
416
416
  }
417
417
 
418
+ // src/sanitize-config.ts
419
+ 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;
420
+ var API_KEY_RE = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
421
+ function sanitizeApiKey(raw) {
422
+ const key = raw.replace(/[\r\n\0]/g, "");
423
+ if (!API_KEY_RE.test(key)) {
424
+ throw new Error(
425
+ "Invalid API key in config \u2014 run `mushi login --api-key <key>` to refresh credentials."
426
+ );
427
+ }
428
+ return key;
429
+ }
430
+ function sanitizeProjectId(raw) {
431
+ const id = raw.trim();
432
+ if (!PROJECT_ID_RE.test(id)) {
433
+ throw new Error(
434
+ "Invalid project ID in config \u2014 expected a UUID or proj_* slug from the admin console."
435
+ );
436
+ }
437
+ return id;
438
+ }
439
+ function sanitizeEndpoint(raw) {
440
+ return assertEndpoint(normalizeEndpoint(raw));
441
+ }
442
+ function sanitizeCliCredentials(config) {
443
+ if (!config.endpoint || !config.apiKey || !config.projectId) {
444
+ throw new Error("Missing endpoint, apiKey, or projectId");
445
+ }
446
+ return {
447
+ endpoint: sanitizeEndpoint(config.endpoint),
448
+ apiKey: sanitizeApiKey(config.apiKey),
449
+ projectId: sanitizeProjectId(config.projectId)
450
+ };
451
+ }
452
+ function apiKeyHeaders(apiKey, projectId) {
453
+ const headers = {
454
+ Authorization: `Bearer ${apiKey}`,
455
+ "X-Mushi-Api-Key": apiKey
456
+ };
457
+ if (projectId) headers["X-Mushi-Project"] = projectId;
458
+ return headers;
459
+ }
460
+
461
+ // src/heartbeat-wait.ts
462
+ var IngestSetupHttpError = class extends Error {
463
+ status;
464
+ constructor(status) {
465
+ super(`ingest-setup HTTP ${status}`);
466
+ this.name = "IngestSetupHttpError";
467
+ this.status = status;
468
+ }
469
+ };
470
+ var NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
471
+ async function fetchIngestSetup(config, doFetch = globalThis.fetch) {
472
+ const endpoint = sanitizeEndpoint(config.endpoint);
473
+ const apiKey = sanitizeApiKey(config.apiKey);
474
+ const projectId = config.projectId ? sanitizeProjectId(config.projectId) : void 0;
475
+ const res = await doFetch(`${endpoint}/v1/sync/ingest-setup`, {
476
+ headers: apiKeyHeaders(apiKey, projectId),
477
+ signal: AbortSignal.timeout(8e3)
478
+ });
479
+ if (!res.ok) {
480
+ if (NON_RETRYABLE_STATUSES.has(res.status)) throw new IngestSetupHttpError(res.status);
481
+ return null;
482
+ }
483
+ const body = await res.json();
484
+ return body.ok && body.data ? body.data : null;
485
+ }
486
+ async function waitForIngestReady(options) {
487
+ const doFetch = options.fetch ?? globalThis.fetch;
488
+ const maxAttempts = options.maxAttempts ?? 40;
489
+ const intervalMs = options.intervalMs ?? 3e3;
490
+ let lastPayload = null;
491
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
492
+ if (options.signal?.aborted) {
493
+ return { ok: false, payload: lastPayload, attempts: attempt - 1, reason: "aborted" };
494
+ }
495
+ try {
496
+ lastPayload = await fetchIngestSetup(
497
+ { endpoint: options.endpoint, apiKey: options.apiKey, projectId: options.projectId },
498
+ doFetch
499
+ );
500
+ if (lastPayload) {
501
+ options.onPoll?.(lastPayload, attempt);
502
+ if (lastPayload.ready) {
503
+ return { ok: true, payload: lastPayload, attempts: attempt, reason: "ready" };
504
+ }
505
+ const sdkStep = lastPayload.steps.find((s) => s.id === "sdk_installed");
506
+ if (sdkStep?.complete) {
507
+ return { ok: true, payload: lastPayload, attempts: attempt, reason: "heartbeat" };
508
+ }
509
+ }
510
+ } catch (err) {
511
+ const msg = err instanceof Error ? err.message : String(err);
512
+ if (err instanceof IngestSetupHttpError) {
513
+ return { ok: false, payload: lastPayload, attempts: attempt, reason: "unauthorized", error: msg };
514
+ }
515
+ if (options.signal?.aborted) {
516
+ return { ok: false, payload: lastPayload, attempts: attempt, reason: "aborted", error: msg };
517
+ }
518
+ if (attempt === maxAttempts) {
519
+ return { ok: false, payload: lastPayload, attempts: attempt, reason: "fetch-error", error: msg };
520
+ }
521
+ }
522
+ if (attempt < maxAttempts) {
523
+ await new Promise((r) => setTimeout(r, intervalMs));
524
+ }
525
+ }
526
+ return { ok: false, payload: lastPayload, attempts: maxAttempts, reason: "timeout" };
527
+ }
528
+
418
529
  export {
419
530
  CONFIG_PATH,
420
531
  loadConfig,
@@ -433,5 +544,10 @@ export {
433
544
  assertEndpoint,
434
545
  normalizeEndpoint,
435
546
  requireUuid,
436
- apiCall
547
+ apiCall,
548
+ sanitizeEndpoint,
549
+ sanitizeCliCredentials,
550
+ apiKeyHeaders,
551
+ fetchIngestSetup,
552
+ waitForIngestReady
437
553
  };
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runConnect
4
- } from "./chunk-VIHWH3IG.js";
5
- import "./chunk-TYVJU4AH.js";
4
+ } from "./chunk-2ZLYNNA3.js";
5
+ import "./chunk-6XYPFV23.js";
6
6
  import "./chunk-LDPFTQLE.js";
7
7
  export {
8
8
  runConnect
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  CONFIG_PATH,
3
3
  assertEndpoint,
4
- normalizeEndpoint,
5
4
  requireUuid,
6
- saveConfig
7
- } from "./chunk-L3LUPBFG.js";
8
- import "./chunk-MCVRTQAI.js";
5
+ saveConfig,
6
+ waitForIngestReady
7
+ } from "./chunk-QOY3DULB.js";
8
+ import {
9
+ MUSHI_MCP_PIN_SPEC
10
+ } from "./chunk-PCTMFRKN.js";
9
11
  import {
10
12
  detectFramework,
11
13
  envVarsToWrite,
@@ -16,107 +18,6 @@ import {
16
18
  import { appendFile, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
17
19
  import { join, resolve } from "path";
18
20
 
19
- // src/sanitize-config.ts
20
- 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;
21
- var API_KEY_RE = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
22
- function sanitizeApiKey(raw) {
23
- const key = raw.replace(/[\r\n\0]/g, "");
24
- if (!API_KEY_RE.test(key)) {
25
- throw new Error(
26
- "Invalid API key in config \u2014 run `mushi login --api-key <key>` to refresh credentials."
27
- );
28
- }
29
- return key;
30
- }
31
- function sanitizeProjectId(raw) {
32
- const id = raw.trim();
33
- if (!PROJECT_ID_RE.test(id)) {
34
- throw new Error(
35
- "Invalid project ID in config \u2014 expected a UUID or proj_* slug from the admin console."
36
- );
37
- }
38
- return id;
39
- }
40
- function sanitizeEndpoint(raw) {
41
- return assertEndpoint(normalizeEndpoint(raw));
42
- }
43
- function apiKeyHeaders(apiKey, projectId) {
44
- const headers = {
45
- Authorization: `Bearer ${apiKey}`,
46
- "X-Mushi-Api-Key": apiKey
47
- };
48
- if (projectId) headers["X-Mushi-Project"] = projectId;
49
- return headers;
50
- }
51
-
52
- // src/heartbeat-wait.ts
53
- var IngestSetupHttpError = class extends Error {
54
- status;
55
- constructor(status) {
56
- super(`ingest-setup HTTP ${status}`);
57
- this.name = "IngestSetupHttpError";
58
- this.status = status;
59
- }
60
- };
61
- var NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
62
- async function fetchIngestSetup(config, doFetch = globalThis.fetch) {
63
- const endpoint = sanitizeEndpoint(config.endpoint);
64
- const apiKey = sanitizeApiKey(config.apiKey);
65
- const projectId = config.projectId ? sanitizeProjectId(config.projectId) : void 0;
66
- const res = await doFetch(`${endpoint}/v1/sync/ingest-setup`, {
67
- headers: apiKeyHeaders(apiKey, projectId),
68
- signal: AbortSignal.timeout(8e3)
69
- });
70
- if (!res.ok) {
71
- if (NON_RETRYABLE_STATUSES.has(res.status)) throw new IngestSetupHttpError(res.status);
72
- return null;
73
- }
74
- const body = await res.json();
75
- return body.ok && body.data ? body.data : null;
76
- }
77
- async function waitForIngestReady(options) {
78
- const doFetch = options.fetch ?? globalThis.fetch;
79
- const maxAttempts = options.maxAttempts ?? 40;
80
- const intervalMs = options.intervalMs ?? 3e3;
81
- let lastPayload = null;
82
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
83
- if (options.signal?.aborted) {
84
- return { ok: false, payload: lastPayload, attempts: attempt - 1, reason: "aborted" };
85
- }
86
- try {
87
- lastPayload = await fetchIngestSetup(
88
- { endpoint: options.endpoint, apiKey: options.apiKey, projectId: options.projectId },
89
- doFetch
90
- );
91
- if (lastPayload) {
92
- options.onPoll?.(lastPayload, attempt);
93
- if (lastPayload.ready) {
94
- return { ok: true, payload: lastPayload, attempts: attempt, reason: "ready" };
95
- }
96
- const sdkStep = lastPayload.steps.find((s) => s.id === "sdk_installed");
97
- if (sdkStep?.complete) {
98
- return { ok: true, payload: lastPayload, attempts: attempt, reason: "heartbeat" };
99
- }
100
- }
101
- } catch (err) {
102
- const msg = err instanceof Error ? err.message : String(err);
103
- if (err instanceof IngestSetupHttpError) {
104
- return { ok: false, payload: lastPayload, attempts: attempt, reason: "unauthorized", error: msg };
105
- }
106
- if (options.signal?.aborted) {
107
- return { ok: false, payload: lastPayload, attempts: attempt, reason: "aborted", error: msg };
108
- }
109
- if (attempt === maxAttempts) {
110
- return { ok: false, payload: lastPayload, attempts: attempt, reason: "fetch-error", error: msg };
111
- }
112
- }
113
- if (attempt < maxAttempts) {
114
- await new Promise((r) => setTimeout(r, intervalMs));
115
- }
116
- }
117
- return { ok: false, payload: lastPayload, attempts: maxAttempts, reason: "timeout" };
118
- }
119
-
120
21
  // src/mcp-config.ts
121
22
  import { mkdir, readFile, writeFile } from "fs/promises";
122
23
  import { dirname } from "path";
@@ -149,7 +50,7 @@ async function writeMcpServerEntry(opts) {
149
50
  function buildMcpServerBlock(opts) {
150
51
  return {
151
52
  command: "npx",
152
- args: ["-y", "@mushi-mushi/mcp@latest"],
53
+ args: ["-y", MUSHI_MCP_PIN_SPEC],
153
54
  env: {
154
55
  MUSHI_API_ENDPOINT: opts.endpoint,
155
56
  MUSHI_PROJECT_ID: opts.projectId,