@mushi-mushi/cli 0.25.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 +10 -2
- package/SECURITY.md +24 -1
- package/dist/{chunk-3JRQN6S4.js → chunk-2ZLYNNA3.js} +5 -5
- package/dist/chunk-6XYPFV23.js +10 -0
- package/dist/chunk-PCTMFRKN.js +8 -0
- package/dist/{chunk-2WHQONHK.js → chunk-QOY3DULB.js} +118 -2
- package/dist/{connect-22IHGT2L.js → connect-2LCLLN7R.js} +2 -1
- package/dist/{connect-XFWRSEMW.js → connect-PXLW4D64.js} +7 -106
- package/dist/index.js +3855 -3185
- package/dist/init.d.ts +2 -0
- package/dist/init.js +804 -12
- package/dist/snippet-inject-LPRWTQDE.js +83 -0
- package/dist/snippet-inject-NDAPEL2T.js +81 -0
- package/dist/version-5DCYWDRK.js +9 -0
- package/dist/version.d.ts +9 -1
- package/dist/version.js +5 -3
- package/dist/wizard-args.d.ts +45 -0
- package/dist/wizard-args.js +100 -0
- package/package.json +7 -1
- package/dist/chunk-SS42XQ57.js +0 -6
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,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
MUSHI_CLI_VERSION,
|
|
4
|
+
MUSHI_MCP_PIN_SPEC
|
|
5
|
+
} from "./chunk-6XYPFV23.js";
|
|
2
6
|
import {
|
|
3
7
|
CLOUD_API_ENDPOINT,
|
|
4
8
|
assertEndpoint,
|
|
@@ -115,9 +119,6 @@ function tightenDirPermissions(path) {
|
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
// src/version.ts
|
|
119
|
-
var MUSHI_CLI_VERSION = true ? "0.25.0" : "0.0.0-dev";
|
|
120
|
-
|
|
121
122
|
// src/errors.ts
|
|
122
123
|
var EXIT_CODE_MAP = {
|
|
123
124
|
E_AUTH_MISSING: 2,
|
|
@@ -1075,7 +1076,7 @@ async function writeMcpServerEntry(opts) {
|
|
|
1075
1076
|
function buildMcpServerBlock(opts) {
|
|
1076
1077
|
return {
|
|
1077
1078
|
command: "npx",
|
|
1078
|
-
args: ["-y",
|
|
1079
|
+
args: ["-y", MUSHI_MCP_PIN_SPEC],
|
|
1079
1080
|
env: {
|
|
1080
1081
|
MUSHI_API_ENDPOINT: opts.endpoint,
|
|
1081
1082
|
MUSHI_PROJECT_ID: opts.projectId,
|
|
@@ -1225,7 +1226,6 @@ async function runConnect(opts, baseConfig = {}) {
|
|
|
1225
1226
|
export {
|
|
1226
1227
|
printAndExit,
|
|
1227
1228
|
installSignalHandlers,
|
|
1228
|
-
MUSHI_CLI_VERSION,
|
|
1229
1229
|
loadConfig,
|
|
1230
1230
|
ensureClientId,
|
|
1231
1231
|
saveConfig,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MUSHI_CLI_VERSION
|
|
3
|
-
} from "./chunk-
|
|
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,122 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CONFIG_PATH,
|
|
3
3
|
assertEndpoint,
|
|
4
|
-
normalizeEndpoint,
|
|
5
4
|
requireUuid,
|
|
6
|
-
saveConfig
|
|
7
|
-
|
|
5
|
+
saveConfig,
|
|
6
|
+
waitForIngestReady
|
|
7
|
+
} from "./chunk-QOY3DULB.js";
|
|
8
|
+
import {
|
|
9
|
+
MUSHI_MCP_PIN_SPEC
|
|
10
|
+
} from "./chunk-PCTMFRKN.js";
|
|
8
11
|
import {
|
|
9
12
|
detectFramework,
|
|
10
13
|
envVarsToWrite,
|
|
11
14
|
readPackageJson
|
|
12
15
|
} from "./chunk-AIZRI665.js";
|
|
13
|
-
import "./chunk-SS42XQ57.js";
|
|
14
16
|
|
|
15
17
|
// src/connect.ts
|
|
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",
|
|
53
|
+
args: ["-y", MUSHI_MCP_PIN_SPEC],
|
|
153
54
|
env: {
|
|
154
55
|
MUSHI_API_ENDPOINT: opts.endpoint,
|
|
155
56
|
MUSHI_PROJECT_ID: opts.projectId,
|