@mushi-mushi/mcp 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONTRIBUTING.md CHANGED
@@ -27,12 +27,24 @@ pnpm build
27
27
 
28
28
  ```bash
29
29
  pnpm dev # Start all dev servers (admin on :6464)
30
+ ```
31
+
32
+ | Surface | URL |
33
+ | --- | --- |
34
+ | Admin console | http://localhost:6464 |
35
+ | Docs site | http://localhost:3000 |
36
+ | CLI console override | `MUSHI_CONSOLE_URL=http://localhost:6464` |
37
+
38
+ ```bash
30
39
  pnpm test # Run Vitest across all packages
31
40
  pnpm typecheck # TypeScript checks
32
41
  pnpm lint # ESLint
33
42
  pnpm format # Prettier
43
+ pnpm smoke:cli-setup # Playwright: onboarding ?setup=cli + success panel (skips headless if unsigned)
34
44
  ```
35
45
 
46
+ Set `CLI_SETUP_SMOKE_STRICT=1` to fail the smoke script when Playwright is not signed in (CI gate).
47
+
36
48
  Ad-hoc screenshots captured during UI reviews can live temporarily at the repo
37
49
  root, but root-level `*.png` files are intentionally ignored. Canonical
38
50
  screenshots that should be versioned belong under `docs/screenshots/`.
@@ -58,7 +70,7 @@ packages/
58
70
  react-native/ React Native SDK (MIT)
59
71
  cli/ CLI tool (MIT)
60
72
  mcp/ MCP server for coding agents (MIT)
61
- server/ Supabase Edge Functions (AGPLv3)
73
+ server/ Supabase Edge Functions (AGPLv3; ee/ is commercial)
62
74
  agents/ Agentic fix pipeline (AGPLv3)
63
75
  verify/ Fix verification (AGPLv3)
64
76
  apps/
@@ -69,6 +81,18 @@ tooling/
69
81
  tsconfig/ Shared TypeScript configs
70
82
  ```
71
83
 
84
+ ## What we don't accept
85
+
86
+ Knowing this upfront saves everyone time.
87
+
88
+ - **Automated / bulk / AI-slop PRs** — any PR that was opened by a script, an LLM agent acting without meaningful human review, or as part of a parallel bounty-farming sweep is closed on sight. The commit history, branch name, or helper scripts included in the PR make this obvious.
89
+ - **PRs that target the wrong base** — all PRs must branch from and target `master`. PRs targeting `main` or any other branch are closed (and will show an inflated diff that is not your work).
90
+ - **One PR per issue, one issue per PR** — splitting or duplicating work just to collect acknowledgements isn't welcome.
91
+ - **Good-first-issues without claiming first** — comment on the issue to claim it before starting. If two people open PRs for the same issue, the one that commented first is prioritised.
92
+ - **Documentation-only PRs that merely duplicate existing content** — if the information already exists (e.g. in another file in this repo), adding a redundant copy adds noise without value.
93
+
94
+ If a PR violates these policies it will be closed with a brief explanation. No hard feelings — a genuine contribution on the same issue is always welcome.
95
+
72
96
  ## Making Changes
73
97
 
74
98
  1. Create a feature branch from `master`
@@ -187,7 +211,7 @@ chore: bump dependencies
187
211
  ## License
188
212
 
189
213
  - SDK packages are MIT — your contributions will be MIT-licensed
190
- - Server/agents/verify are AGPLv3 — contributions to those packages fall under AGPLv3
214
+ - Server/agents/verify are AGPLv3 — contributions to those packages fall under AGPLv3. A [commercial license](./COMMERCIAL-LICENSE.md) is available for hosted-SaaS use cases that cannot publish modifications. The `packages/server/ee/` directory is source-available but commercial (production use needs a license); contributions there are accepted under the same EE terms.
191
215
 
192
216
  ## Questions?
193
217
 
package/SECURITY.md CHANGED
@@ -113,7 +113,7 @@ What we treat as in-scope attacker capabilities, and what we don't.
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
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. |
115
115
  | Physical / OS-level attacker on an end-user device | ❌ | Out of scope. |
116
- | Malicious fork using the Mushi name to ship malware | ❌ (technical) ✅ (legal) | The MIT/BSL grant lets the fork exist; the trademark policy (`TRADEMARK.md`) makes shipping it under the Mushi name an infringement we will pursue. |
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
 
118
118
  ## Data handling and PII
119
119
 
package/dist/catalog.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  RESOURCE_CATALOG,
6
6
  TDD_TOOL_CATALOG,
7
7
  TOOL_CATALOG
8
- } from "./chunk-UYZEIJ5G.js";
8
+ } from "./chunk-MM3IMFM4.js";
9
9
  export {
10
10
  ALL_SCOPES,
11
11
  CODEBASE_TOOL_CATALOG,
@@ -534,6 +534,15 @@ var TDD_TOOL_CATALOG = [
534
534
  hints: { readOnly: false, destructive: false, idempotent: false, openWorld: true },
535
535
  useCase: 'Did "Add to Slack" work? Send a test ping to confirm.'
536
536
  },
537
+ // ── Billing / usage tools ─────────────────────────────────────────────────
538
+ {
539
+ name: "get_usage",
540
+ title: "Usage & billing summary",
541
+ description: 'Read-only diagnoses quota and billing summary for the current project: diagnoses used / limit / percentage, spend cap, period start/end, plan name, and whether the project is approaching or over its quota. Use this to answer "how many diagnoses do I have left?" or "am I close to my spend cap?".',
542
+ scope: "mcp:read",
543
+ hints: { readOnly: true, idempotent: true, openWorld: false },
544
+ useCase: "How many diagnoses do I have left this month?"
545
+ },
537
546
  // ── Skill Pipeline tools ─────────────────────────────────────────────────────
538
547
  {
539
548
  name: "list_skills",
@@ -10,6 +10,7 @@ var FEATURE_GROUPS = [
10
10
  "admin",
11
11
  "audit",
12
12
  "docs",
13
+ "usage",
13
14
  /** Deprecated aliases kept for one release — hidden unless `legacy` or `all`. */
14
15
  "legacy"
15
16
  ];
@@ -82,6 +83,8 @@ var TOOL_FEATURE_MAP = {
82
83
  project_settings: "admin",
83
84
  privacy_status: "admin",
84
85
  evolution_history: "admin",
86
+ // usage / billing
87
+ get_usage: "usage",
85
88
  // qa / tdd
86
89
  map_user_stories: "qa",
87
90
  get_map_run_status: "qa",
@@ -0,0 +1,99 @@
1
+ /**
2
+ * FILE: packages/mcp/src/clients.ts
3
+ * PURPOSE: Shared, pure (React-free) client registry + install-config builders for
4
+ * every supported AI agent / IDE client.
5
+ *
6
+ * OVERVIEW:
7
+ * - Exports `MCP_CLIENTS: McpClientDef[]` — one entry per supported client.
8
+ * - Each entry carries metadata (id, label, method, transport) and a pure `build()`
9
+ * function that returns the install artifact (deeplink URL, JSON config, or CLI command).
10
+ * - Both the admin console ConnectStudio and the public docs /connect landing consume
11
+ * this module — the single source of truth so the two surfaces never drift.
12
+ *
13
+ * DEPENDENCIES:
14
+ * - feature-groups.ts (DEFAULT_FEATURE_GROUPS, featuresQueryString, appendFeaturesToUrl)
15
+ * - branding.ts (MUSHI_ICON_PNG_URL)
16
+ *
17
+ * USAGE:
18
+ * import { MCP_CLIENTS } from '@mushi-mushi/mcp/clients'
19
+ * const cursor = MCP_CLIENTS.find(c => c.id === 'cursor')!
20
+ * const result = cursor.build({ projectId, projectName, apiKey, endpoint, mcpHttpUrl })
21
+ * // result.kind === 'deeplink' → window.open(result.url)
22
+ *
23
+ * CLIENT INSTALL MATRIX (verified Jun 2026):
24
+ * - cursor / vscode / vscode-insiders — one-click deeplink
25
+ * - windsurf — copy-config to ~/.codeium/windsurf/mcp_config.json
26
+ * (uses `serverUrl`, NOT `url` — common footgun)
27
+ * - cline — copy-config to cline_mcp_settings.json
28
+ * - claude-code — CLI command: claude mcp add --transport http …
29
+ * - claude-desktop — copy-config to claude_desktop_config.json
30
+ * - zed — copy-config to ~/.config/zed/settings.json
31
+ * - any — hosted Streamable HTTP URL + Bearer key
32
+ *
33
+ * NOTES:
34
+ * - All builders are pure functions — no side-effects, no DOM, no React.
35
+ * - Cursor base64-encodes the config object without a `name` key in the payload.
36
+ * - VS Code includes `type:'stdio'` in the config and `name` as a separate query param.
37
+ * - Windsurf uses `serverUrl` for remote HTTP (NOT `url`) — encoding both would break it.
38
+ * - The `cliIde` field drives `mushi setup --ide <cliIde>` in the CLI lane.
39
+ */
40
+ interface StdioConfig {
41
+ command: string;
42
+ args: string[];
43
+ env: Record<string, string>;
44
+ icon?: string;
45
+ }
46
+ interface HttpConfig {
47
+ type: 'http';
48
+ url: string;
49
+ headers: Record<string, string>;
50
+ icon?: string;
51
+ }
52
+ type McpClientId = 'cursor' | 'vscode' | 'vscode-insiders' | 'windsurf' | 'cline' | 'claude-code' | 'claude-desktop' | 'zed' | 'any';
53
+ /** The install method determines which UI treatment the console/docs page renders. */
54
+ type McpInstallMethod = 'deeplink' | 'config-json' | 'cli-command' | 'remote-url';
55
+ interface McpBuildInput {
56
+ /** Mushi project UUID — optional when building org-level configs. */
57
+ projectId?: string;
58
+ projectName: string;
59
+ apiKey: string;
60
+ /** Supabase edge function base URL (`MUSHI_API_ENDPOINT`). */
61
+ endpoint: string;
62
+ /** Hosted Streamable HTTP MCP URL (`RESOLVED_MCP_HTTP_URL`). */
63
+ mcpHttpUrl: string;
64
+ }
65
+ type McpBuildResult = {
66
+ kind: 'deeplink';
67
+ url: string;
68
+ } | {
69
+ kind: 'config';
70
+ filePath: string;
71
+ json: string;
72
+ } | {
73
+ kind: 'command';
74
+ text: string;
75
+ } | {
76
+ kind: 'remote-url';
77
+ url: string;
78
+ headerSnippet: string;
79
+ };
80
+ interface McpClientDef {
81
+ id: McpClientId;
82
+ label: string;
83
+ /** Short description shown in the client picker. */
84
+ description: string;
85
+ method: McpInstallMethod;
86
+ transport: 'stdio' | 'http';
87
+ /** Drives `mushi setup --ide <cliIde>` in the CLI lane. */
88
+ cliIde?: 'cursor' | 'claude' | 'continue' | 'zed';
89
+ build(input: McpBuildInput): McpBuildResult;
90
+ }
91
+ /** Build a stable, unique, human-readable MCP server slug for a project. */
92
+ declare function projectServerName(projectId: string, projectName: string): string;
93
+ declare function buildStdioConfigObj(projectId: string | undefined, apiKey: string, apiEndpoint: string, features?: readonly string[]): StdioConfig;
94
+ declare function buildHttpConfigObj(projectId: string | undefined, apiKey: string, mcpHttpUrl: string, features?: readonly string[]): HttpConfig;
95
+ declare const MCP_CLIENTS: McpClientDef[];
96
+ /** Look up a client by id — throws if not found. */
97
+ declare function getMcpClient(id: McpClientId): McpClientDef;
98
+
99
+ export { MCP_CLIENTS, type McpBuildInput, type McpBuildResult, type McpClientDef, type McpClientId, type McpInstallMethod, buildHttpConfigObj as buildRawHttpConfig, buildStdioConfigObj as buildRawStdioConfig, getMcpClient, projectServerName };
@@ -0,0 +1,250 @@
1
+ import {
2
+ MUSHI_ICON_PNG_URL
3
+ } from "./chunk-EHB2BGWX.js";
4
+ import {
5
+ DEFAULT_FEATURE_GROUPS,
6
+ appendFeaturesToUrl,
7
+ featuresQueryString
8
+ } from "./chunk-ZX3XIJ7S.js";
9
+
10
+ // src/clients.ts
11
+ function projectServerName(projectId, projectName) {
12
+ const nameSlug = projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 22);
13
+ const idSuffix = projectId.replace(/-/g, "").slice(0, 6);
14
+ return `mushi-${nameSlug}-${idSuffix}`;
15
+ }
16
+ function buildStdioConfigObj(projectId, apiKey, apiEndpoint, features = DEFAULT_FEATURE_GROUPS) {
17
+ const env = {
18
+ MUSHI_API_ENDPOINT: apiEndpoint,
19
+ MUSHI_API_KEY: apiKey,
20
+ MUSHI_FEATURES: featuresQueryString(features)
21
+ };
22
+ if (projectId) env.MUSHI_PROJECT_ID = projectId;
23
+ return {
24
+ command: "npx",
25
+ args: ["-y", "@mushi-mushi/mcp@latest"],
26
+ env,
27
+ icon: MUSHI_ICON_PNG_URL
28
+ };
29
+ }
30
+ function buildHttpConfigObj(projectId, apiKey, mcpHttpUrl, features = DEFAULT_FEATURE_GROUPS) {
31
+ const url = appendFeaturesToUrl(mcpHttpUrl, features);
32
+ const headers = {
33
+ Authorization: `Bearer ${apiKey}`,
34
+ "X-Mushi-Api-Key": apiKey
35
+ };
36
+ if (projectId) headers["X-Mushi-Project-Id"] = projectId;
37
+ return { type: "http", url, headers, icon: MUSHI_ICON_PNG_URL };
38
+ }
39
+ function encodeCursorDeeplink(name, config) {
40
+ const encoded = btoa(JSON.stringify(config));
41
+ return `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(name)}&config=${encodeURIComponent(encoded)}`;
42
+ }
43
+ function toVsCodeConfig(config) {
44
+ if ("type" in config && config.type === "http") return config;
45
+ return { type: "stdio", ...config };
46
+ }
47
+ function encodeVsCodeDeeplink(scheme, name, config) {
48
+ const withType = toVsCodeConfig(config);
49
+ return `${scheme}:mcp/install?name=${encodeURIComponent(name)}&config=${encodeURIComponent(JSON.stringify(withType))}`;
50
+ }
51
+ var MCP_CLIENTS = [
52
+ // ── Cursor ──────────────────────────────────────────────────────────────────
53
+ {
54
+ id: "cursor",
55
+ label: "Cursor",
56
+ description: "One-click install into your project's .cursor/mcp.json",
57
+ method: "deeplink",
58
+ transport: "stdio",
59
+ cliIde: "cursor",
60
+ build({ projectId, projectName, apiKey, endpoint }) {
61
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
62
+ const config = buildStdioConfigObj(projectId, apiKey, endpoint);
63
+ return { kind: "deeplink", url: encodeCursorDeeplink(name, config) };
64
+ }
65
+ },
66
+ // ── VS Code ──────────────────────────────────────────────────────────────────
67
+ {
68
+ id: "vscode",
69
+ label: "VS Code",
70
+ description: "One-click install via VS Code MCP extension",
71
+ method: "deeplink",
72
+ transport: "stdio",
73
+ build({ projectId, projectName, apiKey, endpoint }) {
74
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
75
+ const config = buildStdioConfigObj(projectId, apiKey, endpoint);
76
+ return { kind: "deeplink", url: encodeVsCodeDeeplink("vscode", name, config) };
77
+ }
78
+ },
79
+ // ── VS Code Insiders ─────────────────────────────────────────────────────────
80
+ {
81
+ id: "vscode-insiders",
82
+ label: "VS Code Insiders",
83
+ description: "One-click install via VS Code Insiders MCP extension",
84
+ method: "deeplink",
85
+ transport: "stdio",
86
+ build({ projectId, projectName, apiKey, endpoint }) {
87
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
88
+ const config = buildStdioConfigObj(projectId, apiKey, endpoint);
89
+ return { kind: "deeplink", url: encodeVsCodeDeeplink("vscode-insiders", name, config) };
90
+ }
91
+ },
92
+ // ── Windsurf ─────────────────────────────────────────────────────────────────
93
+ {
94
+ id: "windsurf",
95
+ label: "Windsurf",
96
+ description: "Copy config into ~/.codeium/windsurf/mcp_config.json",
97
+ method: "config-json",
98
+ transport: "http",
99
+ build({ projectId, projectName, apiKey, mcpHttpUrl }) {
100
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
101
+ const url = appendFeaturesToUrl(mcpHttpUrl, DEFAULT_FEATURE_GROUPS);
102
+ const headers = {
103
+ Authorization: `Bearer ${apiKey}`,
104
+ "X-Mushi-Api-Key": apiKey
105
+ };
106
+ if (projectId) headers["X-Mushi-Project-Id"] = projectId;
107
+ const windsurfConfig = { type: "http", serverUrl: url, headers };
108
+ const configJson = JSON.stringify(
109
+ { mcpServers: { [name]: windsurfConfig } },
110
+ null,
111
+ 2
112
+ );
113
+ return {
114
+ kind: "config",
115
+ filePath: "~/.codeium/windsurf/mcp_config.json",
116
+ json: configJson
117
+ };
118
+ }
119
+ },
120
+ // ── Cline ────────────────────────────────────────────────────────────────────
121
+ {
122
+ id: "cline",
123
+ label: "Cline",
124
+ description: "Copy config into cline_mcp_settings.json",
125
+ method: "config-json",
126
+ transport: "stdio",
127
+ build({ projectId, projectName, apiKey, endpoint }) {
128
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
129
+ const env = {
130
+ MUSHI_API_ENDPOINT: endpoint,
131
+ MUSHI_API_KEY: apiKey
132
+ };
133
+ if (projectId) env.MUSHI_PROJECT_ID = projectId;
134
+ const clineConfig = {
135
+ mcpServers: {
136
+ [name]: { command: "npx", args: ["-y", "@mushi-mushi/mcp@latest"], env }
137
+ }
138
+ };
139
+ return {
140
+ kind: "config",
141
+ // Cline stores config in the VS Code extension's globalStorage
142
+ filePath: "~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
143
+ json: JSON.stringify(clineConfig, null, 2)
144
+ };
145
+ }
146
+ },
147
+ // ── Claude Code ──────────────────────────────────────────────────────────────
148
+ {
149
+ id: "claude-code",
150
+ label: "Claude Code",
151
+ description: "Add via claude CLI command (HTTP transport)",
152
+ method: "cli-command",
153
+ transport: "http",
154
+ cliIde: "claude",
155
+ build({ projectId, projectName, apiKey, mcpHttpUrl }) {
156
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
157
+ const url = appendFeaturesToUrl(mcpHttpUrl, DEFAULT_FEATURE_GROUPS);
158
+ const cmd = `claude mcp add --transport http ${name} "${url}" --header "Authorization: Bearer ${apiKey}"`;
159
+ return { kind: "command", text: cmd };
160
+ }
161
+ },
162
+ // ── Claude Desktop ───────────────────────────────────────────────────────────
163
+ {
164
+ id: "claude-desktop",
165
+ label: "Claude Desktop",
166
+ description: "Copy config into claude_desktop_config.json",
167
+ method: "config-json",
168
+ transport: "stdio",
169
+ build({ projectId, projectName, apiKey, endpoint }) {
170
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
171
+ const env = {
172
+ MUSHI_API_ENDPOINT: endpoint,
173
+ MUSHI_API_KEY: apiKey
174
+ };
175
+ if (projectId) env.MUSHI_PROJECT_ID = projectId;
176
+ const desktopConfig = {
177
+ mcpServers: {
178
+ [name]: { command: "npx", args: ["-y", "@mushi-mushi/mcp@latest"], env }
179
+ }
180
+ };
181
+ return {
182
+ kind: "config",
183
+ filePath: "~/Library/Application Support/Claude/claude_desktop_config.json",
184
+ json: JSON.stringify(desktopConfig, null, 2)
185
+ };
186
+ }
187
+ },
188
+ // ── Zed ──────────────────────────────────────────────────────────────────────
189
+ {
190
+ id: "zed",
191
+ label: "Zed",
192
+ description: "Copy config into ~/.config/zed/settings.json",
193
+ method: "config-json",
194
+ transport: "stdio",
195
+ cliIde: "zed",
196
+ build({ projectId, projectName, apiKey, endpoint }) {
197
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
198
+ const env = {
199
+ MUSHI_API_ENDPOINT: endpoint,
200
+ MUSHI_API_KEY: apiKey
201
+ };
202
+ if (projectId) env.MUSHI_PROJECT_ID = projectId;
203
+ const zedConfig = {
204
+ context_servers: {
205
+ [name]: {
206
+ command: {
207
+ path: "npx",
208
+ args: ["-y", "@mushi-mushi/mcp@latest"],
209
+ env
210
+ },
211
+ settings: {}
212
+ }
213
+ }
214
+ };
215
+ return {
216
+ kind: "config",
217
+ filePath: "~/.config/zed/settings.json",
218
+ json: JSON.stringify(zedConfig, null, 2)
219
+ };
220
+ }
221
+ },
222
+ // ── Any MCP client ───────────────────────────────────────────────────────────
223
+ {
224
+ id: "any",
225
+ label: "Any MCP client",
226
+ description: "Streamable HTTP URL \u2014 works with OpenClaw, Hermes, Gemini CLI, ChatGPT, and more",
227
+ method: "remote-url",
228
+ transport: "http",
229
+ build({ projectId, apiKey, mcpHttpUrl }) {
230
+ const url = appendFeaturesToUrl(mcpHttpUrl, DEFAULT_FEATURE_GROUPS);
231
+ const headerSnippet = [
232
+ `Authorization: Bearer ${apiKey}`,
233
+ projectId ? `X-Mushi-Project-Id: ${projectId}` : ""
234
+ ].filter(Boolean).join("\n");
235
+ return { kind: "remote-url", url, headerSnippet };
236
+ }
237
+ }
238
+ ];
239
+ function getMcpClient(id) {
240
+ const client = MCP_CLIENTS.find((c) => c.id === id);
241
+ if (!client) throw new Error(`Unknown MCP client id: ${id}`);
242
+ return client;
243
+ }
244
+ export {
245
+ MCP_CLIENTS,
246
+ buildHttpConfigObj as buildRawHttpConfig,
247
+ buildStdioConfigObj as buildRawStdioConfig,
248
+ getMcpClient,
249
+ projectServerName
250
+ };
@@ -4,7 +4,7 @@
4
4
  * When `features` is omitted → all tools (backward compatible).
5
5
  * New installs default to DEFAULT_FEATURE_GROUPS via deeplink / docs.
6
6
  */
7
- declare const FEATURE_GROUPS: readonly ["triage", "fixes", "inventory", "setup", "qa", "skills", "rewards", "admin", "audit", "docs", "legacy"];
7
+ declare const FEATURE_GROUPS: readonly ["triage", "fixes", "inventory", "setup", "qa", "skills", "rewards", "admin", "audit", "docs", "usage", "legacy"];
8
8
  type FeatureGroup = (typeof FEATURE_GROUPS)[number];
9
9
  /** Lean default for new Cursor installs (~45 tools vs 70 full). */
10
10
  declare const DEFAULT_FEATURE_GROUPS: readonly FeatureGroup[];
@@ -9,7 +9,7 @@ import {
9
9
  parseFeaturesCsv,
10
10
  parseFeaturesParam,
11
11
  toolMatchesFeatures
12
- } from "./chunk-B2QEG35D.js";
12
+ } from "./chunk-ZX3XIJ7S.js";
13
13
  export {
14
14
  ALL_FEATURE_GROUPS,
15
15
  DEFAULT_FEATURE_GROUPS,
package/dist/index.js CHANGED
@@ -457,6 +457,15 @@ var TDD_TOOL_CATALOG = [
457
457
  hints: { readOnly: false, destructive: false, idempotent: false, openWorld: true },
458
458
  useCase: 'Did "Add to Slack" work? Send a test ping to confirm.'
459
459
  },
460
+ // ── Billing / usage tools ─────────────────────────────────────────────────
461
+ {
462
+ name: "get_usage",
463
+ title: "Usage & billing summary",
464
+ description: 'Read-only diagnoses quota and billing summary for the current project: diagnoses used / limit / percentage, spend cap, period start/end, plan name, and whether the project is approaching or over its quota. Use this to answer "how many diagnoses do I have left?" or "am I close to my spend cap?".',
465
+ scope: "mcp:read",
466
+ hints: { readOnly: true, idempotent: true, openWorld: false },
467
+ useCase: "How many diagnoses do I have left this month?"
468
+ },
460
469
  // ── Skill Pipeline tools ─────────────────────────────────────────────────────
461
470
  {
462
471
  name: "list_skills",
@@ -603,6 +612,7 @@ var FEATURE_GROUPS = [
603
612
  "admin",
604
613
  "audit",
605
614
  "docs",
615
+ "usage",
606
616
  /** Deprecated aliases kept for one release — hidden unless `legacy` or `all`. */
607
617
  "legacy"
608
618
  ];
@@ -668,6 +678,8 @@ var TOOL_FEATURE_MAP = {
668
678
  project_settings: "admin",
669
679
  privacy_status: "admin",
670
680
  evolution_history: "admin",
681
+ // usage / billing
682
+ get_usage: "usage",
671
683
  // qa / tdd
672
684
  map_user_stories: "qa",
673
685
  get_map_run_status: "qa",
@@ -921,7 +933,16 @@ function createMushiServer(config) {
921
933
  return body;
922
934
  }
923
935
  async function resolveProjectId(explicitId) {
924
- if (explicitId) return explicitId;
936
+ if (explicitId) {
937
+ if (projectId && explicitId !== projectId) {
938
+ throw new MushiApiError(
939
+ 400,
940
+ "PROJECT_SCOPE_MISMATCH",
941
+ `This MCP entry is bound to project ${projectId}. Either pass projectId="${projectId}", add a second MCP server entry for ${explicitId} (Console \u2192 Connect \u2192 Add to Cursor while that project is selected), or run \`mushi setup --all-projects --ide cursor\` for one entry per repo.`
942
+ );
943
+ }
944
+ return explicitId;
945
+ }
925
946
  if (projectId) return projectId;
926
947
  try {
927
948
  const data = await apiCall(
@@ -952,6 +973,11 @@ function createMushiServer(config) {
952
973
  );
953
974
  }
954
975
  }
976
+ async function projectScopeHeaders(explicitId) {
977
+ const pid = await resolveProjectId(explicitId ?? void 0);
978
+ const headers = { "X-Mushi-Project-Id": pid };
979
+ return { projectId: pid, headers };
980
+ }
955
981
  function jsonText(value) {
956
982
  return {
957
983
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
@@ -1033,11 +1059,15 @@ function createMushiServer(config) {
1033
1059
  title: titleOf("get_report_detail"),
1034
1060
  description: descOf("get_report_detail"),
1035
1061
  annotations: annotationsFor("get_report_detail"),
1036
- inputSchema: { reportId: z.string().describe("The report UUID") },
1062
+ inputSchema: {
1063
+ reportId: z.string().describe("The report UUID"),
1064
+ project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
1065
+ },
1037
1066
  outputSchema: { report: z.unknown() }
1038
1067
  },
1039
1068
  async (args) => {
1040
- const data = await apiCall(`/v1/admin/reports/${args.reportId}`);
1069
+ const { headers } = await projectScopeHeaders(args.project_id);
1070
+ const data = await apiCall(`/v1/admin/reports/${args.reportId}`, { headers });
1041
1071
  const res = jsonResult({ report: data });
1042
1072
  return {
1043
1073
  ...res,
@@ -1128,7 +1158,10 @@ function createMushiServer(config) {
1128
1158
  title: titleOf("get_fix_context"),
1129
1159
  description: descOf("get_fix_context"),
1130
1160
  annotations: annotationsFor("get_fix_context"),
1131
- inputSchema: { reportId: z.string().describe("The report UUID to fix") },
1161
+ inputSchema: {
1162
+ reportId: z.string().describe("The report UUID to fix"),
1163
+ project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
1164
+ },
1132
1165
  outputSchema: {
1133
1166
  report: z.unknown(),
1134
1167
  fixPrompt: z.unknown(),
@@ -1139,7 +1172,10 @@ function createMushiServer(config) {
1139
1172
  }
1140
1173
  },
1141
1174
  async (args) => {
1142
- const report = await apiCall(`/v1/admin/reports/${args.reportId}`);
1175
+ const { headers } = await projectScopeHeaders(args.project_id);
1176
+ const report = await apiCall(`/v1/admin/reports/${args.reportId}`, {
1177
+ headers
1178
+ });
1143
1179
  return jsonResult({
1144
1180
  report,
1145
1181
  // Paste-ready fix prompt composed server-side by composeFixPacket()
@@ -1159,9 +1195,15 @@ function createMushiServer(config) {
1159
1195
  title: titleOf("get_fix_timeline"),
1160
1196
  description: descOf("get_fix_timeline"),
1161
1197
  annotations: annotationsFor("get_fix_timeline"),
1162
- inputSchema: { fixId: z.string().describe("fix_attempt UUID") }
1198
+ inputSchema: {
1199
+ fixId: z.string().describe("fix_attempt UUID"),
1200
+ project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
1201
+ }
1163
1202
  },
1164
- async (args) => jsonText(await apiCall(`/v1/admin/fixes/${args.fixId}/timeline`))
1203
+ async (args) => {
1204
+ const { headers } = await projectScopeHeaders(args.project_id);
1205
+ return jsonText(await apiCall(`/v1/admin/fixes/${args.fixId}/timeline`, { headers }));
1206
+ }
1165
1207
  );
1166
1208
  server.registerTool(
1167
1209
  "get_blast_radius",
@@ -1791,18 +1833,19 @@ function createMushiServer(config) {
1791
1833
  }
1792
1834
  },
1793
1835
  async (args) => {
1794
- const pid = args.project_id ?? projectId;
1836
+ const { projectId: pid, headers } = await projectScopeHeaders(args.project_id);
1795
1837
  const includeLogs = args.include_logs !== false;
1796
1838
  const [reportRes, evidenceRes, similarRes, fixCtxRes, blastRes, logsRes] = await Promise.allSettled([
1797
- apiCall(`/v1/admin/reports/${args.report_id}`),
1798
- apiCall(`/v1/admin/reports/${args.report_id}/timeline`),
1839
+ apiCall(`/v1/admin/reports/${args.report_id}`, { headers }),
1840
+ apiCall(`/v1/admin/reports/${args.report_id}/timeline`, { headers }),
1799
1841
  apiCall(`/v1/admin/reports/similarity`, {
1800
1842
  method: "POST",
1843
+ headers,
1801
1844
  body: JSON.stringify({ report_id: args.report_id })
1802
1845
  }).catch(() => null),
1803
- pid ? apiCall(`/v1/admin/reports/${args.report_id}/fix-context`).catch(() => null) : Promise.resolve(null),
1804
- pid ? apiCall(`/v1/admin/reports/${args.report_id}/blast-radius`).catch(() => null) : Promise.resolve(null),
1805
- includeLogs && pid ? apiCall(`/v1/admin/mcp/logs/${pid}?limit=20&level=warn`).catch(() => null) : Promise.resolve(null)
1846
+ apiCall(`/v1/admin/reports/${args.report_id}/fix-context`, { headers }).catch(() => null),
1847
+ apiCall(`/v1/admin/reports/${args.report_id}/blast-radius`, { headers }).catch(() => null),
1848
+ includeLogs ? apiCall(`/v1/admin/mcp/logs/${pid}?limit=20&level=warn`, { headers }).catch(() => null) : Promise.resolve(null)
1806
1849
  ]);
1807
1850
  const report = reportRes.status === "fulfilled" ? reportRes.value : null;
1808
1851
  const severity = report ? report.severity : "unknown";
@@ -2699,6 +2742,23 @@ Be specific. No generic advice.`
2699
2742
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2700
2743
  }
2701
2744
  );
2745
+ server.registerTool(
2746
+ "get_usage",
2747
+ {
2748
+ title: titleOf("get_usage"),
2749
+ description: descOf("get_usage"),
2750
+ annotations: annotationsFor("get_usage"),
2751
+ inputSchema: {
2752
+ project_id: z.string().optional().describe("Project ID. Defaults to the configured project.")
2753
+ }
2754
+ },
2755
+ async ({ project_id }) => {
2756
+ const pid = project_id ?? config.projectId;
2757
+ const path = pid ? `/v1/admin/billing/stats?project_id=${pid}` : "/v1/admin/billing/stats";
2758
+ const data = await apiCall(path);
2759
+ return jsonText(data);
2760
+ }
2761
+ );
2702
2762
  server.registerTool(
2703
2763
  "list_skills",
2704
2764
  {
@@ -3020,6 +3080,32 @@ Be specific. No generic advice.`
3020
3080
  }
3021
3081
  }
3022
3082
  }
3083
+ const _nativeFetch = globalThis.fetch;
3084
+ let _firstToolCallSignalled = false;
3085
+ const _signalFirstToolCall = () => {
3086
+ if (_firstToolCallSignalled || !projectId) return;
3087
+ _firstToolCallSignalled = true;
3088
+ void _nativeFetch(`${apiEndpoint}/v1/cli/funnel`, {
3089
+ method: "POST",
3090
+ headers: {
3091
+ "Content-Type": "application/json",
3092
+ "X-Mushi-Api-Key": apiKey,
3093
+ "X-Mushi-Project": projectId
3094
+ },
3095
+ body: JSON.stringify({ event: "mcp_first_tool_call" }),
3096
+ signal: AbortSignal.timeout(4e3)
3097
+ }).catch(() => {
3098
+ });
3099
+ };
3100
+ const llServer = server.server;
3101
+ const existingToolsCallHandler = llServer?._requestHandlers?.get("tools/call");
3102
+ if (typeof existingToolsCallHandler === "function") {
3103
+ llServer._requestHandlers.set("tools/call", async (...args) => {
3104
+ const result = await existingToolsCallHandler(...args);
3105
+ _signalFirstToolCall();
3106
+ return result;
3107
+ });
3108
+ }
3023
3109
  return server;
3024
3110
  }
3025
3111
 
package/dist/server.js CHANGED
@@ -2,13 +2,13 @@ import {
2
2
  CODEBASE_TOOL_CATALOG,
3
3
  TDD_TOOL_CATALOG,
4
4
  TOOL_CATALOG
5
- } from "./chunk-UYZEIJ5G.js";
5
+ } from "./chunk-MM3IMFM4.js";
6
6
  import {
7
7
  MUSHI_SERVER_METADATA
8
8
  } from "./chunk-EHB2BGWX.js";
9
9
  import {
10
10
  toolMatchesFeatures
11
- } from "./chunk-B2QEG35D.js";
11
+ } from "./chunk-ZX3XIJ7S.js";
12
12
 
13
13
  // src/server.ts
14
14
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -199,7 +199,16 @@ function createMushiServer(config) {
199
199
  return body;
200
200
  }
201
201
  async function resolveProjectId(explicitId) {
202
- if (explicitId) return explicitId;
202
+ if (explicitId) {
203
+ if (projectId && explicitId !== projectId) {
204
+ throw new MushiApiError(
205
+ 400,
206
+ "PROJECT_SCOPE_MISMATCH",
207
+ `This MCP entry is bound to project ${projectId}. Either pass projectId="${projectId}", add a second MCP server entry for ${explicitId} (Console \u2192 Connect \u2192 Add to Cursor while that project is selected), or run \`mushi setup --all-projects --ide cursor\` for one entry per repo.`
208
+ );
209
+ }
210
+ return explicitId;
211
+ }
203
212
  if (projectId) return projectId;
204
213
  try {
205
214
  const data = await apiCall(
@@ -230,6 +239,11 @@ function createMushiServer(config) {
230
239
  );
231
240
  }
232
241
  }
242
+ async function projectScopeHeaders(explicitId) {
243
+ const pid = await resolveProjectId(explicitId ?? void 0);
244
+ const headers = { "X-Mushi-Project-Id": pid };
245
+ return { projectId: pid, headers };
246
+ }
233
247
  function jsonText(value) {
234
248
  return {
235
249
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
@@ -311,11 +325,15 @@ function createMushiServer(config) {
311
325
  title: titleOf("get_report_detail"),
312
326
  description: descOf("get_report_detail"),
313
327
  annotations: annotationsFor("get_report_detail"),
314
- inputSchema: { reportId: z.string().describe("The report UUID") },
328
+ inputSchema: {
329
+ reportId: z.string().describe("The report UUID"),
330
+ project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
331
+ },
315
332
  outputSchema: { report: z.unknown() }
316
333
  },
317
334
  async (args) => {
318
- const data = await apiCall(`/v1/admin/reports/${args.reportId}`);
335
+ const { headers } = await projectScopeHeaders(args.project_id);
336
+ const data = await apiCall(`/v1/admin/reports/${args.reportId}`, { headers });
319
337
  const res = jsonResult({ report: data });
320
338
  return {
321
339
  ...res,
@@ -406,7 +424,10 @@ function createMushiServer(config) {
406
424
  title: titleOf("get_fix_context"),
407
425
  description: descOf("get_fix_context"),
408
426
  annotations: annotationsFor("get_fix_context"),
409
- inputSchema: { reportId: z.string().describe("The report UUID to fix") },
427
+ inputSchema: {
428
+ reportId: z.string().describe("The report UUID to fix"),
429
+ project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
430
+ },
410
431
  outputSchema: {
411
432
  report: z.unknown(),
412
433
  fixPrompt: z.unknown(),
@@ -417,7 +438,10 @@ function createMushiServer(config) {
417
438
  }
418
439
  },
419
440
  async (args) => {
420
- const report = await apiCall(`/v1/admin/reports/${args.reportId}`);
441
+ const { headers } = await projectScopeHeaders(args.project_id);
442
+ const report = await apiCall(`/v1/admin/reports/${args.reportId}`, {
443
+ headers
444
+ });
421
445
  return jsonResult({
422
446
  report,
423
447
  // Paste-ready fix prompt composed server-side by composeFixPacket()
@@ -437,9 +461,15 @@ function createMushiServer(config) {
437
461
  title: titleOf("get_fix_timeline"),
438
462
  description: descOf("get_fix_timeline"),
439
463
  annotations: annotationsFor("get_fix_timeline"),
440
- inputSchema: { fixId: z.string().describe("fix_attempt UUID") }
464
+ inputSchema: {
465
+ fixId: z.string().describe("fix_attempt UUID"),
466
+ project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
467
+ }
441
468
  },
442
- async (args) => jsonText(await apiCall(`/v1/admin/fixes/${args.fixId}/timeline`))
469
+ async (args) => {
470
+ const { headers } = await projectScopeHeaders(args.project_id);
471
+ return jsonText(await apiCall(`/v1/admin/fixes/${args.fixId}/timeline`, { headers }));
472
+ }
443
473
  );
444
474
  server.registerTool(
445
475
  "get_blast_radius",
@@ -1069,18 +1099,19 @@ function createMushiServer(config) {
1069
1099
  }
1070
1100
  },
1071
1101
  async (args) => {
1072
- const pid = args.project_id ?? projectId;
1102
+ const { projectId: pid, headers } = await projectScopeHeaders(args.project_id);
1073
1103
  const includeLogs = args.include_logs !== false;
1074
1104
  const [reportRes, evidenceRes, similarRes, fixCtxRes, blastRes, logsRes] = await Promise.allSettled([
1075
- apiCall(`/v1/admin/reports/${args.report_id}`),
1076
- apiCall(`/v1/admin/reports/${args.report_id}/timeline`),
1105
+ apiCall(`/v1/admin/reports/${args.report_id}`, { headers }),
1106
+ apiCall(`/v1/admin/reports/${args.report_id}/timeline`, { headers }),
1077
1107
  apiCall(`/v1/admin/reports/similarity`, {
1078
1108
  method: "POST",
1109
+ headers,
1079
1110
  body: JSON.stringify({ report_id: args.report_id })
1080
1111
  }).catch(() => null),
1081
- pid ? apiCall(`/v1/admin/reports/${args.report_id}/fix-context`).catch(() => null) : Promise.resolve(null),
1082
- pid ? apiCall(`/v1/admin/reports/${args.report_id}/blast-radius`).catch(() => null) : Promise.resolve(null),
1083
- includeLogs && pid ? apiCall(`/v1/admin/mcp/logs/${pid}?limit=20&level=warn`).catch(() => null) : Promise.resolve(null)
1112
+ apiCall(`/v1/admin/reports/${args.report_id}/fix-context`, { headers }).catch(() => null),
1113
+ apiCall(`/v1/admin/reports/${args.report_id}/blast-radius`, { headers }).catch(() => null),
1114
+ includeLogs ? apiCall(`/v1/admin/mcp/logs/${pid}?limit=20&level=warn`, { headers }).catch(() => null) : Promise.resolve(null)
1084
1115
  ]);
1085
1116
  const report = reportRes.status === "fulfilled" ? reportRes.value : null;
1086
1117
  const severity = report ? report.severity : "unknown";
@@ -1977,6 +2008,23 @@ Be specific. No generic advice.`
1977
2008
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1978
2009
  }
1979
2010
  );
2011
+ server.registerTool(
2012
+ "get_usage",
2013
+ {
2014
+ title: titleOf("get_usage"),
2015
+ description: descOf("get_usage"),
2016
+ annotations: annotationsFor("get_usage"),
2017
+ inputSchema: {
2018
+ project_id: z.string().optional().describe("Project ID. Defaults to the configured project.")
2019
+ }
2020
+ },
2021
+ async ({ project_id }) => {
2022
+ const pid = project_id ?? config.projectId;
2023
+ const path = pid ? `/v1/admin/billing/stats?project_id=${pid}` : "/v1/admin/billing/stats";
2024
+ const data = await apiCall(path);
2025
+ return jsonText(data);
2026
+ }
2027
+ );
1980
2028
  server.registerTool(
1981
2029
  "list_skills",
1982
2030
  {
@@ -2298,6 +2346,32 @@ Be specific. No generic advice.`
2298
2346
  }
2299
2347
  }
2300
2348
  }
2349
+ const _nativeFetch = globalThis.fetch;
2350
+ let _firstToolCallSignalled = false;
2351
+ const _signalFirstToolCall = () => {
2352
+ if (_firstToolCallSignalled || !projectId) return;
2353
+ _firstToolCallSignalled = true;
2354
+ void _nativeFetch(`${apiEndpoint}/v1/cli/funnel`, {
2355
+ method: "POST",
2356
+ headers: {
2357
+ "Content-Type": "application/json",
2358
+ "X-Mushi-Api-Key": apiKey,
2359
+ "X-Mushi-Project": projectId
2360
+ },
2361
+ body: JSON.stringify({ event: "mcp_first_tool_call" }),
2362
+ signal: AbortSignal.timeout(4e3)
2363
+ }).catch(() => {
2364
+ });
2365
+ };
2366
+ const llServer = server.server;
2367
+ const existingToolsCallHandler = llServer?._requestHandlers?.get("tools/call");
2368
+ if (typeof existingToolsCallHandler === "function") {
2369
+ llServer._requestHandlers.set("tools/call", async (...args) => {
2370
+ const result = await existingToolsCallHandler(...args);
2371
+ _signalFirstToolCall();
2372
+ return result;
2373
+ });
2374
+ }
2301
2375
  return server;
2302
2376
  }
2303
2377
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mushi-mushi/mcp",
3
3
  "mcpName": "io.github.kensaurus/mushi-mushi",
4
- "version": "0.16.1",
4
+ "version": "0.17.0",
5
5
  "license": "MIT",
6
6
  "description": "MCP server for Mushi Mushi — triage user-felt bugs, pull evidence (screenshots, console logs, network requests), check blast radius, and dispatch fix PRs, all from your AI coding agent",
7
7
  "type": "module",
@@ -25,6 +25,10 @@
25
25
  "./feature-groups": {
26
26
  "types": "./dist/feature-groups.d.ts",
27
27
  "import": "./dist/feature-groups.js"
28
+ },
29
+ "./clients": {
30
+ "types": "./dist/clients.d.ts",
31
+ "import": "./dist/clients.js"
28
32
  }
29
33
  },
30
34
  "main": "./dist/index.js",
@@ -42,15 +46,15 @@
42
46
  ],
43
47
  "dependencies": {
44
48
  "@modelcontextprotocol/sdk": "^1.29.0",
45
- "zod": "^4.4.2",
46
- "@mushi-mushi/core": "^1.16.0"
49
+ "zod": "^4.4.3",
50
+ "@mushi-mushi/core": "^1.21.0"
47
51
  },
48
52
  "devDependencies": {
49
- "@types/node": "^22.19.17",
50
- "eslint": "^10.3.0",
53
+ "@types/node": "^22.19.21",
54
+ "eslint": "^10.4.1",
51
55
  "tsup": "^8.5.1",
52
56
  "typescript": "^6.0.3",
53
- "vitest": "^4.1.5",
57
+ "vitest": "^4.1.8",
54
58
  "@mushi-mushi/eslint-config": "0.0.0"
55
59
  },
56
60
  "repository": {
@@ -58,8 +62,8 @@
58
62
  "url": "https://github.com/kensaurus/mushi-mushi.git",
59
63
  "directory": "packages/mcp"
60
64
  },
61
- "author": "Kenji Sakuramoto",
62
- "homepage": "https://github.com/kensaurus/mushi-mushi/tree/main/packages/mcp#readme",
65
+ "author": "Kenji Sakuramoto (https://bsky.app/profile/mushimushi.dev)",
66
+ "homepage": "https://kensaur.us/mushi-mushi",
63
67
  "bugs": {
64
68
  "url": "https://github.com/kensaurus/mushi-mushi/issues"
65
69
  },
@@ -94,6 +98,10 @@
94
98
  "engines": {
95
99
  "node": ">=20"
96
100
  },
101
+ "funding": {
102
+ "type": "github",
103
+ "url": "https://github.com/sponsors/kensaurus"
104
+ },
97
105
  "scripts": {
98
106
  "build": "tsup",
99
107
  "clean:types": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",