@mushi-mushi/cli 0.34.0 → 0.35.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.
@@ -1,29 +1,143 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- MUSHI_CLI_VERSION,
4
- MUSHI_MCP_PIN_SPEC
5
- } from "./chunk-FPVAQNHT.js";
3
+ MUSHI_CLI_VERSION
4
+ } from "./chunk-GNTEONTU.js";
6
5
  import {
7
6
  CLOUD_API_ENDPOINT,
8
- assertEndpoint,
9
7
  normalizeEndpoint
10
8
  } from "./chunk-LGGE3LH7.js";
11
- import {
12
- detectFramework,
13
- envVarsToWrite,
14
- readPackageJson
15
- } from "./chunk-KOBEOGNF.js";
16
9
 
17
- // src/connect.ts
18
- import { appendFile, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
19
- import { join as join3, resolve } from "path";
10
+ // src/errors.ts
11
+ var EXIT_CODE_MAP = {
12
+ E_AUTH_MISSING: 2,
13
+ E_AUTH_INVALID: 2,
14
+ E_PROJECT_MISSING: 2,
15
+ E_ENDPOINT_INVALID: 2,
16
+ E_INVALID_INPUT: 2,
17
+ E_NOT_INTERACTIVE: 2,
18
+ E_NETWORK: 3,
19
+ E_TIMEOUT: 3,
20
+ E_API_ERROR: 3,
21
+ E_RATE_LIMITED: 3,
22
+ E_FILE_NOT_FOUND: 1,
23
+ E_FILE_PERMISSION: 1,
24
+ E_FRESHNESS_STALE: 1,
25
+ E_INTERRUPTED: 130,
26
+ // 128 + SIGINT (2). POSIX convention.
27
+ E_INTERNAL: 1
28
+ };
29
+ var MushiCliError = class extends Error {
30
+ code;
31
+ hint;
32
+ cause;
33
+ constructor(code, message, hint, cause) {
34
+ super(message);
35
+ this.name = "MushiCliError";
36
+ this.code = code;
37
+ if (hint) this.hint = hint;
38
+ if (cause !== void 0) this.cause = cause;
39
+ }
40
+ /** Exit code POSIX-aware shell scripts should branch on. */
41
+ get exitCode() {
42
+ return EXIT_CODE_MAP[this.code];
43
+ }
44
+ /**
45
+ * Format for `--json` output. Used by the JSON writer when a CLI
46
+ * subcommand was invoked with `--json` and an error escapes.
47
+ */
48
+ toJSON() {
49
+ return {
50
+ error: {
51
+ code: this.code,
52
+ message: this.message,
53
+ ...this.hint ? { hint: this.hint } : {}
54
+ }
55
+ };
56
+ }
57
+ };
58
+ function printAndExit(err, asJson = false) {
59
+ const cliErr = err instanceof MushiCliError ? err : new MushiCliError(
60
+ "E_INTERNAL",
61
+ err instanceof Error ? err.message : String(err),
62
+ "this is unexpected \u2014 re-run with MUSHI_DEBUG=1 and report at https://github.com/kensaurus/mushi-mushi/issues",
63
+ err
64
+ );
65
+ if (asJson) {
66
+ process.stderr.write(JSON.stringify(cliErr.toJSON()) + "\n");
67
+ } else {
68
+ process.stderr.write(`error [${cliErr.code}]: ${cliErr.message}
69
+ `);
70
+ if (cliErr.hint) process.stderr.write(` \u2192 fix: ${cliErr.hint}
71
+ `);
72
+ }
73
+ if (process.env.MUSHI_CLI_JSON_LOG === "1" || process.env.CI === "true") {
74
+ process.stderr.write(
75
+ JSON.stringify({
76
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
77
+ level: "error",
78
+ scope: "mushi:cli",
79
+ ...cliErr.toJSON(),
80
+ exitCode: cliErr.exitCode
81
+ }) + "\n"
82
+ );
83
+ }
84
+ process.exit(cliErr.exitCode);
85
+ }
20
86
 
21
- // src/config.ts
22
- import { randomUUID } from "crypto";
23
- import { spawnSync } from "child_process";
24
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
25
- import { homedir } from "os";
26
- import { dirname, join } from "path";
87
+ // src/signals.ts
88
+ var installed = false;
89
+ var activeController = null;
90
+ var interrupted = null;
91
+ var cleanups = /* @__PURE__ */ new Set();
92
+ var INTERRUPT_CLEANUP_MS = 8e3;
93
+ function onInterrupt(fn) {
94
+ cleanups.add(fn);
95
+ return () => {
96
+ cleanups.delete(fn);
97
+ };
98
+ }
99
+ function interruptedBy() {
100
+ return interrupted;
101
+ }
102
+ async function handleInterrupt(signal, exit = (code) => process.exit(code), budgetMs = INTERRUPT_CLEANUP_MS) {
103
+ const code = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
104
+ if (interrupted) {
105
+ exit(code);
106
+ return;
107
+ }
108
+ interrupted = signal;
109
+ activeController?.abort(
110
+ new MushiCliError(
111
+ "E_INTERRUPTED",
112
+ `aborted by ${signal}`,
113
+ "partial state on the server is safe to retry \u2014 re-run the same command"
114
+ )
115
+ );
116
+ if (cleanups.size) {
117
+ let timer;
118
+ const budget = new Promise((resolve) => {
119
+ timer = setTimeout(resolve, budgetMs);
120
+ });
121
+ const all = Promise.allSettled([...cleanups].map(async (fn) => fn(signal)));
122
+ await Promise.race([all, budget]);
123
+ clearTimeout(timer);
124
+ }
125
+ await new Promise((resolve) => process.nextTick(resolve));
126
+ exit(code);
127
+ }
128
+ function installSignalHandlers() {
129
+ if (installed) return;
130
+ installed = true;
131
+ process.on("SIGINT", () => void handleInterrupt("SIGINT"));
132
+ process.on("SIGTERM", () => void handleInterrupt("SIGTERM"));
133
+ }
134
+ function getAbortSignal(external) {
135
+ if (external) return external.signal;
136
+ if (!activeController || activeController.signal.aborted) {
137
+ activeController = new AbortController();
138
+ }
139
+ return activeController.signal;
140
+ }
27
141
 
28
142
  // src/keychain.ts
29
143
  import { createRequire } from "module";
@@ -44,12 +158,19 @@ function getEntryClass() {
44
158
  }
45
159
  return _EntryClass ?? null;
46
160
  }
161
+ function isKeychainEnabled() {
162
+ if (process.env["MUSHI_NO_KEYCHAIN"]) return false;
163
+ return getEntryClass() !== null;
164
+ }
47
165
  function trySaveKeyToKeychain(apiKey, profile = "default") {
166
+ return saveSecret(accountName(profile), apiKey);
167
+ }
168
+ function saveSecret(account, apiKey) {
48
169
  if (!apiKey) return false;
49
170
  const EntryClass = getEntryClass();
50
171
  if (!EntryClass) return false;
51
172
  try {
52
- const entry = new EntryClass(KEYCHAIN_SERVICE, accountName(profile));
173
+ const entry = new EntryClass(KEYCHAIN_SERVICE, account);
53
174
  entry.setPassword(apiKey);
54
175
  return true;
55
176
  } catch {
@@ -57,10 +178,13 @@ function trySaveKeyToKeychain(apiKey, profile = "default") {
57
178
  }
58
179
  }
59
180
  function tryLoadKeyFromKeychain(profile = "default") {
181
+ return loadSecret(accountName(profile));
182
+ }
183
+ function loadSecret(account) {
60
184
  const EntryClass = getEntryClass();
61
185
  if (!EntryClass) return null;
62
186
  try {
63
- const entry = new EntryClass(KEYCHAIN_SERVICE, accountName(profile));
187
+ const entry = new EntryClass(KEYCHAIN_SERVICE, account);
64
188
  const key = entry.getPassword();
65
189
  return key && key.length > 0 ? key : null;
66
190
  } catch {
@@ -68,10 +192,13 @@ function tryLoadKeyFromKeychain(profile = "default") {
68
192
  }
69
193
  }
70
194
  function tryDeleteKeyFromKeychain(profile = "default") {
195
+ deleteSecret(accountName(profile));
196
+ }
197
+ function deleteSecret(account) {
71
198
  const EntryClass = getEntryClass();
72
199
  if (!EntryClass) return;
73
200
  try {
74
- const entry = new EntryClass(KEYCHAIN_SERVICE, accountName(profile));
201
+ const entry = new EntryClass(KEYCHAIN_SERVICE, account);
75
202
  entry.deletePassword();
76
203
  } catch {
77
204
  }
@@ -82,8 +209,23 @@ function syncKeychainAfterKeyChange(apiKey, profile = "default") {
82
209
  tryDeleteKeyFromKeychain(profile);
83
210
  return "removed";
84
211
  }
212
+ var WORKER_KEY_ACCOUNT = "worker-key";
213
+ function trySaveWorkerKey(key) {
214
+ return saveSecret(WORKER_KEY_ACCOUNT, key);
215
+ }
216
+ function tryLoadWorkerKey() {
217
+ return loadSecret(WORKER_KEY_ACCOUNT);
218
+ }
219
+ function tryDeleteWorkerKey() {
220
+ deleteSecret(WORKER_KEY_ACCOUNT);
221
+ }
85
222
 
86
223
  // src/config.ts
224
+ import { randomUUID } from "crypto";
225
+ import { spawnSync } from "child_process";
226
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
227
+ import { homedir } from "os";
228
+ import { dirname, join } from "path";
87
229
  var DEFAULT_PROFILE = "default";
88
230
  function isMultiProfileFile(value) {
89
231
  return typeof value === "object" && value !== null && "profiles" in value && typeof value.profiles === "object" && value.profiles !== null;
@@ -308,138 +450,6 @@ function tightenWindowsAcl(path, type) {
308
450
  }
309
451
  }
310
452
 
311
- // src/errors.ts
312
- var EXIT_CODE_MAP = {
313
- E_AUTH_MISSING: 2,
314
- E_AUTH_INVALID: 2,
315
- E_PROJECT_MISSING: 2,
316
- E_ENDPOINT_INVALID: 2,
317
- E_INVALID_INPUT: 2,
318
- E_NOT_INTERACTIVE: 2,
319
- E_NETWORK: 3,
320
- E_TIMEOUT: 3,
321
- E_API_ERROR: 3,
322
- E_RATE_LIMITED: 3,
323
- E_FILE_NOT_FOUND: 1,
324
- E_FILE_PERMISSION: 1,
325
- E_FRESHNESS_STALE: 1,
326
- E_INTERRUPTED: 130,
327
- // 128 + SIGINT (2). POSIX convention.
328
- E_INTERNAL: 1
329
- };
330
- var MushiCliError = class extends Error {
331
- code;
332
- hint;
333
- cause;
334
- constructor(code, message, hint, cause) {
335
- super(message);
336
- this.name = "MushiCliError";
337
- this.code = code;
338
- if (hint) this.hint = hint;
339
- if (cause !== void 0) this.cause = cause;
340
- }
341
- /** Exit code POSIX-aware shell scripts should branch on. */
342
- get exitCode() {
343
- return EXIT_CODE_MAP[this.code];
344
- }
345
- /**
346
- * Format for `--json` output. Used by the JSON writer when a CLI
347
- * subcommand was invoked with `--json` and an error escapes.
348
- */
349
- toJSON() {
350
- return {
351
- error: {
352
- code: this.code,
353
- message: this.message,
354
- ...this.hint ? { hint: this.hint } : {}
355
- }
356
- };
357
- }
358
- };
359
- function printAndExit(err, asJson = false) {
360
- const cliErr = err instanceof MushiCliError ? err : new MushiCliError(
361
- "E_INTERNAL",
362
- err instanceof Error ? err.message : String(err),
363
- "this is unexpected \u2014 re-run with MUSHI_DEBUG=1 and report at https://github.com/kensaurus/mushi-mushi/issues",
364
- err
365
- );
366
- if (asJson) {
367
- process.stderr.write(JSON.stringify(cliErr.toJSON()) + "\n");
368
- } else {
369
- process.stderr.write(`error [${cliErr.code}]: ${cliErr.message}
370
- `);
371
- if (cliErr.hint) process.stderr.write(` \u2192 fix: ${cliErr.hint}
372
- `);
373
- }
374
- if (process.env.MUSHI_CLI_JSON_LOG === "1" || process.env.CI === "true") {
375
- process.stderr.write(
376
- JSON.stringify({
377
- ts: (/* @__PURE__ */ new Date()).toISOString(),
378
- level: "error",
379
- scope: "mushi:cli",
380
- ...cliErr.toJSON(),
381
- exitCode: cliErr.exitCode
382
- }) + "\n"
383
- );
384
- }
385
- process.exit(cliErr.exitCode);
386
- }
387
-
388
- // src/signals.ts
389
- var installed = false;
390
- var activeController = null;
391
- var interrupted = null;
392
- var cleanups = /* @__PURE__ */ new Set();
393
- var INTERRUPT_CLEANUP_MS = 8e3;
394
- function onInterrupt(fn) {
395
- cleanups.add(fn);
396
- return () => {
397
- cleanups.delete(fn);
398
- };
399
- }
400
- function interruptedBy() {
401
- return interrupted;
402
- }
403
- async function handleInterrupt(signal, exit = (code) => process.exit(code), budgetMs = INTERRUPT_CLEANUP_MS) {
404
- const code = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
405
- if (interrupted) {
406
- exit(code);
407
- return;
408
- }
409
- interrupted = signal;
410
- activeController?.abort(
411
- new MushiCliError(
412
- "E_INTERRUPTED",
413
- `aborted by ${signal}`,
414
- "partial state on the server is safe to retry \u2014 re-run the same command"
415
- )
416
- );
417
- if (cleanups.size) {
418
- let timer;
419
- const budget = new Promise((resolve2) => {
420
- timer = setTimeout(resolve2, budgetMs);
421
- });
422
- const all = Promise.allSettled([...cleanups].map(async (fn) => fn(signal)));
423
- await Promise.race([all, budget]);
424
- clearTimeout(timer);
425
- }
426
- await new Promise((resolve2) => process.nextTick(resolve2));
427
- exit(code);
428
- }
429
- function installSignalHandlers() {
430
- if (installed) return;
431
- installed = true;
432
- process.on("SIGINT", () => void handleInterrupt("SIGINT"));
433
- process.on("SIGTERM", () => void handleInterrupt("SIGTERM"));
434
- }
435
- function getAbortSignal(external) {
436
- if (external) return external.signal;
437
- if (!activeController || activeController.signal.aborted) {
438
- activeController = new AbortController();
439
- }
440
- return activeController.signal;
441
- }
442
-
443
453
  // src/console-url.ts
444
454
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
445
455
  import { dirname as dirname2, join as join2 } from "path";
@@ -544,14 +554,14 @@ async function openInBrowser(url) {
544
554
  // is handed to the default browser as one opaque argument.
545
555
  ["rundll32", ["url.dll,FileProtocolHandler", safeUrl]]
546
556
  ) : process.platform === "darwin" ? ["open", [safeUrl]] : ["xdg-open", [safeUrl]];
547
- await new Promise((resolve2) => {
557
+ await new Promise((resolve) => {
548
558
  try {
549
559
  const child = spawn(command, [...args], { stdio: "ignore", shell: false, detached: process.platform !== "win32" });
550
- child.once("spawn", () => resolve2());
551
- child.once("error", () => resolve2());
560
+ child.once("spawn", () => resolve());
561
+ child.once("error", () => resolve());
552
562
  child.unref();
553
563
  } catch {
554
- resolve2();
564
+ resolve();
555
565
  }
556
566
  });
557
567
  }
@@ -752,389 +762,6 @@ function pad(s, width) {
752
762
  return s.length >= width ? s : s + " ".repeat(width - s.length);
753
763
  }
754
764
 
755
- // src/sanitize-config.ts
756
- 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;
757
- var API_KEY_RE = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
758
- function sanitizeApiKey(raw) {
759
- const key = raw.replace(/[\r\n\0]/g, "");
760
- if (!API_KEY_RE.test(key)) {
761
- throw new Error(
762
- "Invalid API key in config \u2014 run `mushi login --api-key <key>` to refresh credentials."
763
- );
764
- }
765
- return key;
766
- }
767
- function sanitizeProjectId(raw) {
768
- const id = raw.trim();
769
- if (!PROJECT_ID_RE.test(id)) {
770
- throw new Error(
771
- "Invalid project ID in config \u2014 expected a UUID or proj_* slug from the admin console."
772
- );
773
- }
774
- return id;
775
- }
776
- function sanitizeEndpoint(raw) {
777
- return assertEndpoint(normalizeEndpoint(raw));
778
- }
779
- function sanitizeCliCredentials(config) {
780
- if (!config.endpoint || !config.apiKey || !config.projectId) {
781
- throw new Error("Missing endpoint, apiKey, or projectId");
782
- }
783
- return {
784
- endpoint: sanitizeEndpoint(config.endpoint),
785
- apiKey: sanitizeApiKey(config.apiKey),
786
- projectId: sanitizeProjectId(config.projectId)
787
- };
788
- }
789
- function apiKeyHeaders(apiKey, projectId) {
790
- const headers = {
791
- Authorization: `Bearer ${apiKey}`,
792
- "X-Mushi-Api-Key": apiKey
793
- };
794
- if (projectId) headers["X-Mushi-Project"] = projectId;
795
- return headers;
796
- }
797
-
798
- // src/heartbeat-wait.ts
799
- var IngestSetupHttpError = class extends Error {
800
- status;
801
- constructor(status) {
802
- super(`ingest-setup HTTP ${status}`);
803
- this.name = "IngestSetupHttpError";
804
- this.status = status;
805
- }
806
- };
807
- var NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
808
- async function fetchIngestSetup(config, doFetch = globalThis.fetch) {
809
- const endpoint = sanitizeEndpoint(config.endpoint);
810
- const apiKey = sanitizeApiKey(config.apiKey);
811
- const projectId = config.projectId ? sanitizeProjectId(config.projectId) : void 0;
812
- const res = await doFetch(`${endpoint}/v1/sync/ingest-setup`, {
813
- headers: apiKeyHeaders(apiKey, projectId),
814
- signal: AbortSignal.timeout(8e3)
815
- });
816
- if (!res.ok) {
817
- if (NON_RETRYABLE_STATUSES.has(res.status)) throw new IngestSetupHttpError(res.status);
818
- return null;
819
- }
820
- const body = await res.json();
821
- return body.ok && body.data ? body.data : null;
822
- }
823
- async function waitForIngestReady(options) {
824
- const doFetch = options.fetch ?? globalThis.fetch;
825
- const maxAttempts = options.maxAttempts ?? 40;
826
- const intervalMs = options.intervalMs ?? 3e3;
827
- let lastPayload = null;
828
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
829
- if (options.signal?.aborted) {
830
- return { ok: false, payload: lastPayload, attempts: attempt - 1, reason: "aborted" };
831
- }
832
- try {
833
- lastPayload = await fetchIngestSetup(
834
- { endpoint: options.endpoint, apiKey: options.apiKey, projectId: options.projectId },
835
- doFetch
836
- );
837
- if (lastPayload) {
838
- options.onPoll?.(lastPayload, attempt);
839
- if (lastPayload.ready) {
840
- return { ok: true, payload: lastPayload, attempts: attempt, reason: "ready" };
841
- }
842
- const sdkStep = lastPayload.steps.find((s) => s.id === "sdk_installed");
843
- if (sdkStep?.complete) {
844
- return { ok: true, payload: lastPayload, attempts: attempt, reason: "heartbeat" };
845
- }
846
- }
847
- } catch (err) {
848
- const msg = err instanceof Error ? err.message : String(err);
849
- if (err instanceof IngestSetupHttpError) {
850
- return { ok: false, payload: lastPayload, attempts: attempt, reason: "unauthorized", error: msg };
851
- }
852
- if (options.signal?.aborted) {
853
- return { ok: false, payload: lastPayload, attempts: attempt, reason: "aborted", error: msg };
854
- }
855
- if (attempt === maxAttempts) {
856
- return { ok: false, payload: lastPayload, attempts: attempt, reason: "fetch-error", error: msg };
857
- }
858
- }
859
- if (attempt < maxAttempts) {
860
- await new Promise((r) => setTimeout(r, intervalMs));
861
- }
862
- }
863
- return { ok: false, payload: lastPayload, attempts: maxAttempts, reason: "timeout" };
864
- }
865
-
866
- // src/key-scopes.ts
867
- var SDK_KEY_SCOPES = ["report:write"];
868
- var CLI_KEY_SCOPES = ["report:write", "mcp:read"];
869
- var FULL_CLI_KEY_SCOPES = ["report:write", "mcp:read", "mcp:write"];
870
- var SDK_KEY_LABEL = "sdk-ingest";
871
- var CLI_KEY_LABEL = "cli-login";
872
- async function probeKeyScope(endpoint, apiKey, projectId, doFetch = globalThis.fetch) {
873
- let base = endpoint;
874
- while (base.endsWith("/")) base = base.slice(0, -1);
875
- let res;
876
- try {
877
- res = await doFetch(`${base}/v1/admin/mcp/account-overview`, {
878
- headers: { "X-Mushi-Api-Key": apiKey, "X-Mushi-Project": projectId },
879
- signal: AbortSignal.timeout(8e3)
880
- });
881
- } catch (err) {
882
- return { result: "unreachable", message: err instanceof Error ? err.message : String(err) };
883
- }
884
- if (res.ok) return { result: "mcp", status: res.status };
885
- if (res.status === 401) return { result: "invalid", status: res.status };
886
- if (res.status === 403) {
887
- const body = await res.json().catch(() => null);
888
- const code = body?.error?.code ?? body?.code;
889
- if (code === "INSUFFICIENT_SCOPE") return { result: "ingest-only", status: res.status };
890
- }
891
- return { result: "unknown", status: res.status };
892
- }
893
- function describeUnsafeSdkKey(probe, endpoint) {
894
- switch (probe.result) {
895
- case "mcp":
896
- return "this key can read your bug reports (mcp scope), and SDK env vars ship inside the app bundle";
897
- case "invalid":
898
- return "the backend rejected this key";
899
- case "unreachable":
900
- return `could not reach ${endpoint} to confirm the key is ingest-only (${probe.message ?? "network error"})`;
901
- case "unknown":
902
- return `could not confirm the key is ingest-only (HTTP ${probe.status ?? "?"})`;
903
- case "ingest-only":
904
- return "the key is ingest-only";
905
- }
906
- }
907
-
908
- // src/mcp-config.ts
909
- import { mkdir, readFile, writeFile } from "fs/promises";
910
- import { dirname as dirname3 } from "path";
911
- var DEFAULT_MUSHI_FEATURES = "triage,fixes,inventory,setup,docs";
912
- var MCP_CLIENTS = ["cursor", "claude", "continue", "zed"];
913
- function isMcpClient(value) {
914
- return typeof value === "string" && MCP_CLIENTS.includes(value);
915
- }
916
- var MCP_CLIENT_LABEL = {
917
- cursor: "Cursor",
918
- claude: "Claude Code",
919
- continue: "Continue",
920
- zed: "Zed"
921
- };
922
- function buildHostedMcpServerBlock(url, oauth) {
923
- return oauth ? { type: "http", url, oauth } : { type: "http", url };
924
- }
925
- function isPlainObject(value) {
926
- return typeof value === "object" && value !== null && !Array.isArray(value);
927
- }
928
- async function writeMcpServerEntry(opts) {
929
- const { configPath, serverName, serverBlock, dryRun = false } = opts;
930
- let existing = null;
931
- try {
932
- existing = await readFile(configPath, "utf8");
933
- } catch {
934
- existing = null;
935
- }
936
- let merged = {};
937
- if (existing !== null && existing.trim() !== "") {
938
- let parsed;
939
- try {
940
- parsed = JSON.parse(existing);
941
- } catch {
942
- throw new Error(`${configPath} is not valid JSON \u2014 fix or remove it, then re-run (nothing was written).`);
943
- }
944
- if (!isPlainObject(parsed)) {
945
- throw new Error(`${configPath} is not a JSON object \u2014 fix or remove it, then re-run (nothing was written).`);
946
- }
947
- merged = parsed;
948
- }
949
- const currentServers = merged.mcpServers;
950
- if (currentServers !== void 0 && !isPlainObject(currentServers)) {
951
- throw new Error(`${configPath} has an "mcpServers" field that is not an object \u2014 fix it, then re-run (nothing was written).`);
952
- }
953
- merged.mcpServers = { ...currentServers ?? {}, [serverName]: serverBlock };
954
- const output = JSON.stringify(merged, null, 2) + "\n";
955
- if (!dryRun) {
956
- await mkdir(dirname3(configPath), { recursive: true });
957
- await writeFile(configPath, output, "utf8");
958
- }
959
- return { created: existing === null, path: configPath };
960
- }
961
- function apiKeyPlaceholderFor(client) {
962
- switch (client) {
963
- case "cursor":
964
- return "${env:MUSHI_API_KEY}";
965
- case "claude":
966
- return "${MUSHI_API_KEY:-}";
967
- case "continue":
968
- case "zed":
969
- return null;
970
- }
971
- }
972
- function buildMcpServerBlock(opts) {
973
- const keyValue = opts.inlineKey ? opts.apiKey : apiKeyPlaceholderFor(opts.client);
974
- return {
975
- command: "npx",
976
- args: ["-y", MUSHI_MCP_PIN_SPEC],
977
- env: {
978
- MUSHI_API_ENDPOINT: opts.endpoint,
979
- MUSHI_PROJECT_ID: opts.projectId,
980
- ...keyValue !== null ? { MUSHI_API_KEY: keyValue } : {},
981
- MUSHI_FEATURES: DEFAULT_MUSHI_FEATURES
982
- }
983
- };
984
- }
985
- function printKeyExportHint(client, configPath = CONFIG_PATH) {
986
- console.log("");
987
- console.log(" No API key was written into the MCP config (safe to commit).");
988
- console.log(` The MCP server uses the key \`mushi login\` saved in ${configPath}.`);
989
- if (apiKeyPlaceholderFor(client) !== null) {
990
- console.log(` To use a different key, export MUSHI_API_KEY in the environment ${MCP_CLIENT_LABEL[client]} is launched from.`);
991
- }
992
- console.log(" To write the key into the file instead (keep it out of git), re-run with --inline-key.");
993
- }
994
- function buildMcpServerName(opts) {
995
- if (opts.legacy) return "mushi";
996
- if (opts.projectName) {
997
- const slug = opts.projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
998
- return `mushi-${slug}`;
999
- }
1000
- return `mushi-${(opts.projectId ?? "unknown").slice(0, 8)}`;
1001
- }
1002
-
1003
- // src/connect.ts
1004
- async function resolveSdkEnvKey(opts) {
1005
- const candidate = opts.sdkKey ?? savedSdkKeyFor(opts.baseConfig, opts.projectId) ?? opts.apiKey;
1006
- const probe = await opts.probe(opts.endpoint, candidate, opts.projectId);
1007
- if (probe.result === "ingest-only") return { ok: true, key: candidate };
1008
- return { ok: false, reason: describeUnsafeSdkKey(probe, opts.endpoint) };
1009
- }
1010
- function envKeyPresent(content, key) {
1011
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1012
- return new RegExp(`^${escaped}=`, "m").test(content);
1013
- }
1014
- async function mergeEnvFile(path, lines) {
1015
- const block = `
1016
- # Mushi \u2014 added by mushi connect
1017
- ${lines.join("\n")}
1018
- `;
1019
- let existing = null;
1020
- try {
1021
- existing = await readFile2(path, "utf8");
1022
- } catch {
1023
- }
1024
- if (existing !== null) {
1025
- const needs = lines.filter((line) => {
1026
- const key = line.split("=")[0];
1027
- return !envKeyPresent(existing, key);
1028
- });
1029
- if (needs.length === 0) return false;
1030
- await appendFile(path, `
1031
- # Mushi \u2014 added by mushi connect
1032
- ${needs.join("\n")}
1033
- `, "utf8");
1034
- return true;
1035
- }
1036
- await writeFile2(path, block, "utf8");
1037
- return true;
1038
- }
1039
- async function runConnect(opts, baseConfig = {}) {
1040
- const cwd = resolve(opts.cwd ?? process.cwd());
1041
- const endpoint = assertEndpoint(opts.endpoint);
1042
- const projectId = requireUuid(opts.projectId, "projectId");
1043
- const messages = [];
1044
- const writeEnv = opts.writeEnv !== false;
1045
- const sdkEnvKey = writeEnv ? await resolveSdkEnvKey({
1046
- sdkKey: opts.sdkKey,
1047
- apiKey: opts.apiKey,
1048
- projectId,
1049
- endpoint,
1050
- baseConfig,
1051
- probe: opts.probeKeyScope ?? probeKeyScope
1052
- }) : null;
1053
- const config = {
1054
- ...baseConfig,
1055
- apiKey: opts.apiKey,
1056
- projectId,
1057
- endpoint,
1058
- ...sdkEnvKey?.ok ? { sdkKey: { projectId, key: sdkEnvKey.key } } : {}
1059
- };
1060
- saveConfig(config);
1061
- messages.push(`\u2713 Credentials saved to ${CONFIG_PATH}`);
1062
- let envPath = null;
1063
- let envRefused = false;
1064
- if (sdkEnvKey && !sdkEnvKey.ok) {
1065
- envRefused = true;
1066
- messages.push(
1067
- `\u2717 Did not write SDK env vars: ${sdkEnvKey.reason}. Pass an ingest-only key with --sdk-key <key> (console \u2192 Projects \u2192 API keys, scope report:write), or run \`npx mushi-mushi\`, which mints one.`
1068
- );
1069
- } else if (sdkEnvKey?.ok) {
1070
- const pkg = readPackageJson(cwd);
1071
- const framework = detectFramework(cwd, pkg);
1072
- const lines = envVarsToWrite(sdkEnvKey.key, projectId, framework).split("\n");
1073
- envPath = join3(cwd, ".env.local");
1074
- const wrote = await mergeEnvFile(envPath, lines);
1075
- messages.push(
1076
- wrote ? `\u2713 Env vars merged into ${envPath}` : `\u2713 Env vars already present in ${envPath} (existing values left untouched)`
1077
- );
1078
- const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
1079
- if (deps["@capacitor/core"] && deps["react"]) {
1080
- messages.push(
1081
- "\u2139 Capacitor + React detected \u2014 install @mushi-mushi/web (or @mushi-mushi/react) and call initMushi() in main.tsx. Optional @mushi-mushi/capacitor for native shell parity."
1082
- );
1083
- }
1084
- }
1085
- let mcpPath = null;
1086
- if (opts.wireIde !== false) {
1087
- const mcpDir = join3(cwd, ".cursor");
1088
- mcpPath = join3(mcpDir, "mcp.json");
1089
- const serverName = buildMcpServerName({ projectId });
1090
- const serverBlock = buildMcpServerBlock({
1091
- endpoint,
1092
- projectId,
1093
- apiKey: opts.apiKey,
1094
- client: "cursor",
1095
- inlineKey: false
1096
- // no secret in the file; the MCP server reads the saved CLI key
1097
- });
1098
- await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
1099
- messages.push(`\u2713 Wired ${mcpPath} \u2014 restart Cursor and run "list mushi tools"`);
1100
- messages.push(` The MCP server reads the key saved in ${CONFIG_PATH} (export MUSHI_API_KEY before launching Cursor to override it).`);
1101
- }
1102
- let heartbeat = null;
1103
- if (opts.wait) {
1104
- const timeoutSec = opts.waitTimeoutSec ?? 120;
1105
- const maxAttempts = Math.max(1, Math.ceil(timeoutSec * 1e3 / 3e3));
1106
- messages.push(`\u2026 Waiting for SDK heartbeat (up to ${timeoutSec}s) \u2014 start your dev server with the snippet installed`);
1107
- heartbeat = await waitForIngestReady({
1108
- endpoint,
1109
- apiKey: opts.apiKey,
1110
- projectId,
1111
- maxAttempts,
1112
- onPoll: (payload, attempt) => {
1113
- if (!opts.json && attempt % 3 === 0) {
1114
- const sdk = payload.steps.find((s) => s.id === "sdk_installed");
1115
- const seen = payload.diagnostic?.last_sdk_seen_at ?? "never";
1116
- process.stdout.write(` poll ${attempt}: sdk_installed=${sdk?.complete ? "yes" : "no"} last_seen=${seen}
1117
- `);
1118
- }
1119
- }
1120
- });
1121
- if (heartbeat.ok) {
1122
- const label = heartbeat.reason === "heartbeat" ? "SDK heartbeat detected" : "Ingest setup complete";
1123
- messages.push(`\u2713 ${label} \u2014 ingest pipeline is live`);
1124
- } else if (heartbeat.reason === "unauthorized") {
1125
- messages.push(
1126
- `\u2717 The backend rejected these credentials (${heartbeat.error ?? "auth error"}). Double-check --api-key, --project-id, and --endpoint, then re-run \`mushi connect --wait\`.`
1127
- );
1128
- } else {
1129
- messages.push(
1130
- `\u2717 No heartbeat before timeout (${heartbeat.reason}). Confirm env vars are in your build, restart the dev server, then re-run \`mushi connect --wait\`.`
1131
- );
1132
- }
1133
- }
1134
- const ok = !envRefused && (!opts.wait || Boolean(heartbeat?.ok));
1135
- return { ok, envPath, mcpPath, heartbeat, messages };
1136
- }
1137
-
1138
765
  export {
1139
766
  MushiCliError,
1140
767
  printAndExit,
@@ -1142,8 +769,12 @@ export {
1142
769
  interruptedBy,
1143
770
  installSignalHandlers,
1144
771
  getAbortSignal,
772
+ isKeychainEnabled,
1145
773
  trySaveKeyToKeychain,
1146
774
  syncKeychainAfterKeyChange,
775
+ trySaveWorkerKey,
776
+ tryLoadWorkerKey,
777
+ tryDeleteWorkerKey,
1147
778
  savedSdkKeyFor,
1148
779
  resolveProfileName,
1149
780
  CONFIG_PATH,
@@ -1153,13 +784,6 @@ export {
1153
784
  saveConfig,
1154
785
  listProfiles,
1155
786
  setActiveProfile,
1156
- SDK_KEY_SCOPES,
1157
- CLI_KEY_SCOPES,
1158
- FULL_CLI_KEY_SCOPES,
1159
- SDK_KEY_LABEL,
1160
- CLI_KEY_LABEL,
1161
- probeKeyScope,
1162
- describeUnsafeSdkKey,
1163
787
  consoleUrl,
1164
788
  resolveConsoleUrlSync,
1165
789
  resolveConsoleUrl,
@@ -1177,21 +801,5 @@ export {
1177
801
  die,
1178
802
  requireConfig,
1179
803
  fmtDate,
1180
- pad,
1181
- sanitizeApiKey,
1182
- sanitizeProjectId,
1183
- sanitizeEndpoint,
1184
- sanitizeCliCredentials,
1185
- apiKeyHeaders,
1186
- fetchIngestSetup,
1187
- MCP_CLIENTS,
1188
- isMcpClient,
1189
- MCP_CLIENT_LABEL,
1190
- buildHostedMcpServerBlock,
1191
- writeMcpServerEntry,
1192
- buildMcpServerBlock,
1193
- printKeyExportHint,
1194
- buildMcpServerName,
1195
- resolveSdkEnvKey,
1196
- runConnect
804
+ pad
1197
805
  };