@mushi-mushi/cli 0.27.1 → 0.29.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.
@@ -2,7 +2,12 @@
2
2
  import {
3
3
  MUSHI_CLI_VERSION,
4
4
  MUSHI_MCP_PIN_SPEC
5
- } from "./chunk-DBAEB56N.js";
5
+ } from "./chunk-F4EGZNKA.js";
6
+ import {
7
+ detectFramework,
8
+ envVarsToWrite,
9
+ readPackageJson
10
+ } from "./chunk-KOBEOGNF.js";
6
11
  import {
7
12
  CLOUD_API_ENDPOINT,
8
13
  assertEndpoint,
@@ -14,7 +19,7 @@ import {
14
19
 
15
20
  // src/connect.ts
16
21
  import { appendFile, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
17
- import { join as join4, resolve } from "path";
22
+ import { join as join3, resolve } from "path";
18
23
 
19
24
  // src/config.ts
20
25
  import { randomUUID } from "crypto";
@@ -69,6 +74,11 @@ var DEFAULT_PROFILE = "default";
69
74
  function isMultiProfileFile(value) {
70
75
  return typeof value === "object" && value !== null && "profiles" in value && typeof value.profiles === "object" && value.profiles !== null;
71
76
  }
77
+ function savedSdkKeyFor(config, projectId) {
78
+ const saved = config.sdkKey;
79
+ if (!projectId || !saved || typeof saved !== "object") return void 0;
80
+ return saved.projectId === projectId && typeof saved.key === "string" && saved.key ? saved.key : void 0;
81
+ }
72
82
  function resolveProfileName(explicit, fileActive) {
73
83
  const fromEnv = process.env["MUSHI_PROFILE"]?.trim();
74
84
  return explicit?.trim() || fromEnv || fileActive || DEFAULT_PROFILE;
@@ -530,7 +540,30 @@ async function probeEndpointHealth(endpoint) {
530
540
  throw err;
531
541
  }
532
542
  }
533
- async function apiCall(path, config, options = {}) {
543
+ function normalizeApiError(error, httpStatus) {
544
+ const fallbackCode = httpStatus ? `HTTP_${httpStatus}` : "API_ERROR";
545
+ if (typeof error === "string") return { code: fallbackCode, message: error };
546
+ if (error && typeof error === "object") {
547
+ const e = error;
548
+ if (typeof e["code"] === "string" && typeof e["message"] === "string") {
549
+ return e;
550
+ }
551
+ const formErrors = Array.isArray(e["formErrors"]) ? e["formErrors"].map(String) : [];
552
+ const fieldErrors = e["fieldErrors"] && typeof e["fieldErrors"] === "object" ? Object.entries(e["fieldErrors"]).map(
553
+ ([field, msgs]) => `${field}: ${Array.isArray(msgs) ? msgs.join(", ") : String(msgs)}`
554
+ ) : [];
555
+ const parts = [...formErrors, ...fieldErrors];
556
+ if (parts.length > 0) return { code: "VALIDATION_ERROR", message: parts.join("; ") };
557
+ return {
558
+ ...e,
559
+ code: typeof e["code"] === "string" ? e["code"] : fallbackCode,
560
+ message: typeof e["message"] === "string" ? e["message"] : `Request failed${httpStatus ? ` (${httpStatus})` : ""}`
561
+ };
562
+ }
563
+ return { code: fallbackCode, message: `Request failed${httpStatus ? ` (${httpStatus})` : ""}` };
564
+ }
565
+ async function apiCall(path, config, options = {}, callOptions = {}) {
566
+ const timeoutMs = callOptions.timeoutMs ?? API_TIMEOUT_MS;
534
567
  const endpoint = config.endpoint;
535
568
  if (!endpoint) {
536
569
  return {
@@ -542,7 +575,7 @@ async function apiCall(path, config, options = {}) {
542
575
  };
543
576
  }
544
577
  const controller = new AbortController();
545
- const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
578
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
546
579
  const signals = [controller.signal, getAbortSignal()];
547
580
  const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
548
581
  try {
@@ -571,6 +604,7 @@ async function apiCall(path, config, options = {}) {
571
604
  }
572
605
  } else {
573
606
  const text = await res.text();
607
+ if (callOptions.text && res.ok) return { ok: true, data: text };
574
608
  try {
575
609
  body = JSON.parse(text);
576
610
  } catch {
@@ -594,6 +628,13 @@ async function apiCall(path, config, options = {}) {
594
628
  error: { code: errCode, message: errMsg }
595
629
  };
596
630
  }
631
+ if (typeof body === "object" && body !== null && body.ok === false) {
632
+ return {
633
+ ok: false,
634
+ httpStatus: res.status,
635
+ error: normalizeApiError(body.error, res.status)
636
+ };
637
+ }
597
638
  return body;
598
639
  } catch (err) {
599
640
  clearTimeout(timer);
@@ -602,7 +643,7 @@ async function apiCall(path, config, options = {}) {
602
643
  ok: false,
603
644
  error: {
604
645
  code: "TIMEOUT",
605
- message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
646
+ message: `Request timed out after ${timeoutMs / 1e3}s. Check your network or endpoint.`
606
647
  }
607
648
  };
608
649
  }
@@ -670,446 +711,6 @@ function pad(s, width) {
670
711
  return s.length >= width ? s : s + " ".repeat(width - s.length);
671
712
  }
672
713
 
673
- // src/detect.ts
674
- import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
675
- import { join as join3 } from "path";
676
- var FRAMEWORKS = {
677
- next: {
678
- id: "next",
679
- label: "Next.js",
680
- packageName: "@mushi-mushi/react",
681
- needsWebPackage: false,
682
- snippet: () => `// app/providers.tsx (or pages/_app.tsx for /pages router)
683
- 'use client'
684
- import { MushiProvider } from '@mushi-mushi/react'
685
-
686
- export function Providers({ children }: { children: React.ReactNode }) {
687
- return (
688
- <MushiProvider config={{
689
- projectId: process.env.NEXT_PUBLIC_MUSHI_PROJECT_ID!,
690
- apiKey: process.env.NEXT_PUBLIC_MUSHI_API_KEY!,
691
- }}>
692
- {children}
693
- </MushiProvider>
694
- )
695
- }`
696
- },
697
- react: {
698
- id: "react",
699
- label: "React",
700
- packageName: "@mushi-mushi/react",
701
- needsWebPackage: false,
702
- snippet: () => `// src/main.tsx
703
- import { MushiProvider } from '@mushi-mushi/react'
704
-
705
- createRoot(document.getElementById('root')!).render(
706
- <MushiProvider config={{
707
- projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
708
- apiKey: import.meta.env.VITE_MUSHI_API_KEY,
709
- }}>
710
- <App />
711
- </MushiProvider>
712
- )`
713
- },
714
- cra: {
715
- id: "cra",
716
- label: "Create React App",
717
- packageName: "@mushi-mushi/react",
718
- needsWebPackage: false,
719
- // CRA inlines only REACT_APP_*-prefixed vars into process.env at build time.
720
- snippet: () => `// src/index.tsx
721
- import { MushiProvider } from '@mushi-mushi/react'
722
-
723
- root.render(
724
- <MushiProvider config={{
725
- projectId: process.env.REACT_APP_MUSHI_PROJECT_ID,
726
- apiKey: process.env.REACT_APP_MUSHI_API_KEY,
727
- }}>
728
- <App />
729
- </MushiProvider>
730
- )`
731
- },
732
- remix: {
733
- id: "remix",
734
- label: "Remix",
735
- packageName: "@mushi-mushi/react",
736
- needsWebPackage: false,
737
- // Remix doesn't inline client env. Expose public values at runtime via the
738
- // root loader + window.ENV (works on both the classic compiler and Vite).
739
- snippet: () => `// app/root.tsx
740
- import { MushiProvider } from '@mushi-mushi/react'
741
- import { useLoaderData } from '@remix-run/react'
742
-
743
- export async function loader() {
744
- return { ENV: {
745
- MUSHI_PROJECT_ID: process.env.MUSHI_PROJECT_ID,
746
- MUSHI_API_KEY: process.env.MUSHI_API_KEY,
747
- } }
748
- }
749
-
750
- export default function App() {
751
- const { ENV } = useLoaderData<typeof loader>()
752
- return (
753
- <html>
754
- <body>
755
- <MushiProvider config={{ projectId: ENV.MUSHI_PROJECT_ID, apiKey: ENV.MUSHI_API_KEY }}>
756
- <Outlet />
757
- </MushiProvider>
758
- <Scripts />
759
- </body>
760
- </html>
761
- )
762
- }`
763
- },
764
- astro: {
765
- id: "astro",
766
- label: "Astro",
767
- packageName: "@mushi-mushi/web",
768
- needsWebPackage: false,
769
- // Astro exposes only PUBLIC_*-prefixed vars to client code (Vite-based).
770
- snippet: () => `---
771
- // src/layouts/Layout.astro (add this <script> once, e.g. in your base layout)
772
- ---
773
- <script>
774
- import { Mushi } from '@mushi-mushi/web'
775
- Mushi.init({
776
- projectId: import.meta.env.PUBLIC_MUSHI_PROJECT_ID,
777
- apiKey: import.meta.env.PUBLIC_MUSHI_API_KEY,
778
- })
779
- </script>`
780
- },
781
- solid: {
782
- id: "solid",
783
- label: "Solid",
784
- packageName: "@mushi-mushi/web",
785
- needsWebPackage: false,
786
- // Solid / SolidStart are Vite-based → VITE_*-prefixed client env.
787
- snippet: () => `// src/index.tsx (or src/app.tsx for SolidStart)
788
- import { onMount } from 'solid-js'
789
- import { Mushi } from '@mushi-mushi/web'
790
-
791
- onMount(() => {
792
- Mushi.init({
793
- projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
794
- apiKey: import.meta.env.VITE_MUSHI_API_KEY,
795
- })
796
- })`
797
- },
798
- vue: {
799
- id: "vue",
800
- label: "Vue 3",
801
- packageName: "@mushi-mushi/vue",
802
- needsWebPackage: false,
803
- snippet: () => `// src/main.ts
804
- import { MushiPlugin } from '@mushi-mushi/vue'
805
-
806
- app.use(MushiPlugin, {
807
- projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
808
- apiKey: import.meta.env.VITE_MUSHI_API_KEY,
809
- })`
810
- },
811
- nuxt: {
812
- id: "nuxt",
813
- label: "Nuxt",
814
- packageName: "@mushi-mushi/vue",
815
- needsWebPackage: false,
816
- snippet: () => `// plugins/mushi.client.ts
817
- import { MushiPlugin } from '@mushi-mushi/vue'
818
-
819
- export default defineNuxtPlugin((nuxtApp) => {
820
- nuxtApp.vueApp.use(MushiPlugin, {
821
- projectId: import.meta.env.NUXT_PUBLIC_MUSHI_PROJECT_ID,
822
- apiKey: import.meta.env.NUXT_PUBLIC_MUSHI_API_KEY,
823
- })
824
- })`
825
- },
826
- svelte: {
827
- id: "svelte",
828
- label: "Svelte",
829
- packageName: "@mushi-mushi/svelte",
830
- needsWebPackage: false,
831
- snippet: () => `// src/main.ts
832
- import { initMushi } from '@mushi-mushi/svelte'
833
-
834
- initMushi({
835
- projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
836
- apiKey: import.meta.env.VITE_MUSHI_API_KEY,
837
- })`
838
- },
839
- sveltekit: {
840
- id: "sveltekit",
841
- label: "SvelteKit",
842
- packageName: "@mushi-mushi/svelte",
843
- needsWebPackage: false,
844
- snippet: () => `// src/routes/+layout.svelte
845
- <script>
846
- import { onMount } from 'svelte'
847
-
848
- onMount(async () => {
849
- const { initMushi } = await import('@mushi-mushi/svelte')
850
- initMushi({
851
- projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
852
- apiKey: import.meta.env.VITE_MUSHI_API_KEY,
853
- })
854
- })
855
- </script>`
856
- },
857
- angular: {
858
- id: "angular",
859
- label: "Angular",
860
- packageName: "@mushi-mushi/angular",
861
- needsWebPackage: false,
862
- snippet: () => `// src/main.ts
863
- import { provideMushi } from '@mushi-mushi/angular'
864
-
865
- bootstrapApplication(AppComponent, {
866
- providers: [
867
- provideMushi({
868
- projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
869
- apiKey: import.meta.env.VITE_MUSHI_API_KEY,
870
- }),
871
- ],
872
- })`
873
- },
874
- expo: {
875
- id: "expo",
876
- label: "Expo",
877
- packageName: "@mushi-mushi/react-native",
878
- needsWebPackage: false,
879
- snippet: () => `// App.tsx
880
- import { MushiProvider } from '@mushi-mushi/react-native'
881
-
882
- export default function App() {
883
- return (
884
- <MushiProvider
885
- projectId={process.env.EXPO_PUBLIC_MUSHI_PROJECT_ID!}
886
- apiKey={process.env.EXPO_PUBLIC_MUSHI_API_KEY!}
887
- >
888
- <YourApp />
889
- </MushiProvider>
890
- )
891
- }`
892
- },
893
- "react-native": {
894
- id: "react-native",
895
- label: "React Native",
896
- packageName: "@mushi-mushi/react-native",
897
- needsWebPackage: false,
898
- snippet: () => `// App.tsx
899
- import { MushiProvider } from '@mushi-mushi/react-native'
900
-
901
- export default function App() {
902
- return (
903
- <MushiProvider
904
- projectId={process.env.MUSHI_PROJECT_ID!}
905
- apiKey={process.env.MUSHI_API_KEY!}
906
- >
907
- <YourApp />
908
- </MushiProvider>
909
- )
910
- }`
911
- },
912
- capacitor: {
913
- id: "capacitor",
914
- label: "Capacitor (Ionic)",
915
- packageName: "@mushi-mushi/capacitor",
916
- needsWebPackage: false,
917
- /* The Capacitor plugin's public API is `Mushi.configure(...)`, not
918
- * `Mushi.init(...)` — see packages/capacitor/src/definitions.ts. We
919
- * shipped the wrong call here for two releases and users got a runtime
920
- * `TypeError: Mushi.init is not a function`. The accompanying admin
921
- * console snippet (apps/admin/src/lib/sdkSnippets.ts) emits the same
922
- * `Mushi.configure(...)` shape; both are pinned by tests. */
923
- snippet: () => `// src/main.ts \u2014 after install, run \`npx cap sync\`
924
- import { Mushi } from '@mushi-mushi/capacitor'
925
-
926
- await Mushi.configure({
927
- projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
928
- apiKey: import.meta.env.VITE_MUSHI_API_KEY,
929
- })`
930
- },
931
- express: {
932
- id: "express",
933
- label: "Express",
934
- packageName: "@mushi-mushi/node",
935
- needsWebPackage: false,
936
- snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
937
- import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
938
- import { mushiExpressErrorHandler } from '@mushi-mushi/node/express'
939
- import type { Express } from 'express'
940
-
941
- export const mushi = new MushiNodeClient({
942
- projectId: process.env.MUSHI_PROJECT_ID!,
943
- apiKey: process.env.MUSHI_API_KEY!,
944
- environment: process.env.NODE_ENV ?? 'production',
945
- })
946
- attachUnhandledHook({ client: mushi })
947
-
948
- export function attachMushi(app: Express) {
949
- app.use(mushiExpressErrorHandler({ client: mushi }))
950
- }`
951
- },
952
- fastify: {
953
- id: "fastify",
954
- label: "Fastify",
955
- packageName: "@mushi-mushi/node",
956
- needsWebPackage: false,
957
- snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
958
- import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
959
- import { mushiFastifyPlugin } from '@mushi-mushi/node/fastify'
960
- import Fastify from 'fastify'
961
-
962
- export const mushi = new MushiNodeClient({
963
- projectId: process.env.MUSHI_PROJECT_ID!,
964
- apiKey: process.env.MUSHI_API_KEY!,
965
- environment: process.env.NODE_ENV ?? 'production',
966
- })
967
- attachUnhandledHook({ client: mushi })
968
-
969
- const app = Fastify()
970
- mushiFastifyPlugin(app, { client: mushi })`
971
- },
972
- hono: {
973
- id: "hono",
974
- label: "Hono",
975
- packageName: "@mushi-mushi/node",
976
- needsWebPackage: false,
977
- snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
978
- import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
979
- import { mushiHonoErrorHandler } from '@mushi-mushi/node/hono'
980
- import { Hono } from 'hono'
981
-
982
- export const mushi = new MushiNodeClient({
983
- projectId: process.env.MUSHI_PROJECT_ID!,
984
- apiKey: process.env.MUSHI_API_KEY!,
985
- environment: process.env.NODE_ENV ?? 'production',
986
- })
987
- attachUnhandledHook({ client: mushi })
988
-
989
- const app = new Hono()
990
- app.onError(
991
- mushiHonoErrorHandler({ client: mushi }, (err, c) =>
992
- c.text('Internal Server Error', 500),
993
- ),
994
- )`
995
- },
996
- vanilla: {
997
- id: "vanilla",
998
- label: "Vanilla JS / unknown",
999
- packageName: "@mushi-mushi/web",
1000
- needsWebPackage: false,
1001
- snippet: () => `// Anywhere in your client bundle
1002
- import { Mushi } from '@mushi-mushi/web'
1003
-
1004
- Mushi.init({
1005
- projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
1006
- apiKey: import.meta.env.VITE_MUSHI_API_KEY,
1007
- })`
1008
- }
1009
- };
1010
- function readPackageJson(cwd) {
1011
- const path = join3(cwd, "package.json");
1012
- if (!existsSync3(path)) return null;
1013
- try {
1014
- return JSON.parse(readFileSync3(path, "utf-8"));
1015
- } catch {
1016
- return null;
1017
- }
1018
- }
1019
- function detectFramework(cwd, pkg) {
1020
- const deps = collectDeps(pkg);
1021
- if (deps.has("next")) return FRAMEWORKS.next;
1022
- if (deps.has("nuxt")) return FRAMEWORKS.nuxt;
1023
- if (deps.has("@sveltejs/kit")) return FRAMEWORKS.sveltekit;
1024
- if (deps.has("@angular/core")) return FRAMEWORKS.angular;
1025
- if (deps.has("expo")) return FRAMEWORKS.expo;
1026
- if (deps.has("react-native")) return FRAMEWORKS["react-native"];
1027
- if (deps.has("@remix-run/react") || deps.has("@remix-run/node") || deps.has("@remix-run/serve")) {
1028
- return FRAMEWORKS.remix;
1029
- }
1030
- if (deps.has("astro")) return FRAMEWORKS.astro;
1031
- if (deps.has("@solidjs/start") || deps.has("solid-js")) return FRAMEWORKS.solid;
1032
- if (deps.has("@capacitor/core") && deps.has("react")) return FRAMEWORKS.react;
1033
- if (deps.has("@capacitor/core")) return FRAMEWORKS.capacitor;
1034
- if (deps.has("svelte")) return FRAMEWORKS.svelte;
1035
- if (deps.has("vue")) return FRAMEWORKS.vue;
1036
- if (deps.has("react-scripts")) return FRAMEWORKS.cra;
1037
- if (deps.has("react")) return FRAMEWORKS.react;
1038
- if (deps.has("express")) return FRAMEWORKS.express;
1039
- if (deps.has("fastify")) return FRAMEWORKS.fastify;
1040
- if (deps.has("hono") || deps.has("@hono/hono")) return FRAMEWORKS.hono;
1041
- if (existsSync3(join3(cwd, "next.config.js")) || existsSync3(join3(cwd, "next.config.ts"))) {
1042
- return FRAMEWORKS.next;
1043
- }
1044
- if (existsSync3(join3(cwd, "nuxt.config.ts")) || existsSync3(join3(cwd, "nuxt.config.js"))) {
1045
- return FRAMEWORKS.nuxt;
1046
- }
1047
- if (existsSync3(join3(cwd, "svelte.config.js"))) return FRAMEWORKS.svelte;
1048
- if (existsSync3(join3(cwd, "angular.json"))) return FRAMEWORKS.angular;
1049
- if (existsSync3(join3(cwd, "remix.config.js")) || existsSync3(join3(cwd, "remix.config.ts"))) {
1050
- return FRAMEWORKS.remix;
1051
- }
1052
- if (existsSync3(join3(cwd, "astro.config.mjs")) || existsSync3(join3(cwd, "astro.config.ts")) || existsSync3(join3(cwd, "astro.config.js"))) {
1053
- return FRAMEWORKS.astro;
1054
- }
1055
- return FRAMEWORKS.vanilla;
1056
- }
1057
- function detectPackageManager(cwd) {
1058
- if (existsSync3(join3(cwd, "bun.lockb")) || existsSync3(join3(cwd, "bun.lock"))) return "bun";
1059
- if (existsSync3(join3(cwd, "pnpm-lock.yaml"))) return "pnpm";
1060
- if (existsSync3(join3(cwd, "yarn.lock"))) return "yarn";
1061
- if (existsSync3(join3(cwd, "package-lock.json"))) return "npm";
1062
- const userAgent = process.env.npm_config_user_agent ?? "";
1063
- if (userAgent.startsWith("bun")) return "bun";
1064
- if (userAgent.startsWith("pnpm")) return "pnpm";
1065
- if (userAgent.startsWith("yarn")) return "yarn";
1066
- return "npm";
1067
- }
1068
- function installCommand(pm, packages) {
1069
- const verb = pm === "npm" ? "install" : "add";
1070
- return `${pm} ${verb} ${packages.join(" ")}`;
1071
- }
1072
- var SERVER_FRAMEWORK_IDS = /* @__PURE__ */ new Set(["express", "fastify", "hono"]);
1073
- function envVarsToWrite(apiKey, projectId, framework, endpoint) {
1074
- const CLOUD_ENDPOINT = "https://dxptnwrhwsqckaftyymj.supabase.co/functions/v1/api";
1075
- const endpointLine = endpoint && endpoint !== CLOUD_ENDPOINT ? `MUSHI_API_ENDPOINT=${endpoint}` : null;
1076
- if (SERVER_FRAMEWORK_IDS.has(framework.id)) {
1077
- return [
1078
- `MUSHI_PROJECT_ID=${projectId}`,
1079
- `MUSHI_API_KEY=${apiKey}`,
1080
- endpointLine
1081
- ].filter(Boolean).join("\n");
1082
- }
1083
- if (framework.id === "react-native" || framework.id === "remix") {
1084
- return [
1085
- `MUSHI_PROJECT_ID=${projectId}`,
1086
- `MUSHI_API_KEY=${apiKey}`,
1087
- endpointLine
1088
- ].filter(Boolean).join("\n");
1089
- }
1090
- if (framework.id === "expo") {
1091
- return [
1092
- `EXPO_PUBLIC_MUSHI_PROJECT_ID=${projectId}`,
1093
- `EXPO_PUBLIC_MUSHI_API_KEY=${apiKey}`,
1094
- endpointLine ? `EXPO_PUBLIC_${endpointLine}` : null
1095
- ].filter(Boolean).join("\n");
1096
- }
1097
- const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : framework.id === "cra" ? "REACT_APP_" : framework.id === "astro" ? "PUBLIC_" : "VITE_";
1098
- return [
1099
- `${prefix}MUSHI_PROJECT_ID=${projectId}`,
1100
- `${prefix}MUSHI_API_KEY=${apiKey}`,
1101
- endpointLine ? `${prefix}${endpointLine}` : null
1102
- ].filter(Boolean).join("\n");
1103
- }
1104
- function collectDeps(pkg) {
1105
- if (!pkg) return /* @__PURE__ */ new Set();
1106
- return /* @__PURE__ */ new Set([
1107
- ...Object.keys(pkg.dependencies ?? {}),
1108
- ...Object.keys(pkg.devDependencies ?? {}),
1109
- ...Object.keys(pkg.peerDependencies ?? {})
1110
- ]);
1111
- }
1112
-
1113
714
  // src/sanitize-config.ts
1114
715
  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;
1115
716
  var API_KEY_RE = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
@@ -1221,55 +822,133 @@ async function waitForIngestReady(options) {
1221
822
  return { ok: false, payload: lastPayload, attempts: maxAttempts, reason: "timeout" };
1222
823
  }
1223
824
 
825
+ // src/key-scopes.ts
826
+ var SDK_KEY_SCOPES = ["report:write"];
827
+ var CLI_KEY_SCOPES = ["report:write", "mcp:read"];
828
+ var FULL_CLI_KEY_SCOPES = ["report:write", "mcp:read", "mcp:write"];
829
+ var SDK_KEY_LABEL = "sdk-ingest";
830
+ var CLI_KEY_LABEL = "cli-login";
831
+ async function probeKeyScope(endpoint, apiKey, projectId, doFetch = globalThis.fetch) {
832
+ let base = endpoint;
833
+ while (base.endsWith("/")) base = base.slice(0, -1);
834
+ let res;
835
+ try {
836
+ res = await doFetch(`${base}/v1/admin/mcp/account-overview`, {
837
+ headers: { "X-Mushi-Api-Key": apiKey, "X-Mushi-Project": projectId },
838
+ signal: AbortSignal.timeout(8e3)
839
+ });
840
+ } catch (err) {
841
+ return { result: "unreachable", message: err instanceof Error ? err.message : String(err) };
842
+ }
843
+ if (res.ok) return { result: "mcp", status: res.status };
844
+ if (res.status === 401) return { result: "invalid", status: res.status };
845
+ if (res.status === 403) {
846
+ const body = await res.json().catch(() => null);
847
+ const code = body?.error?.code ?? body?.code;
848
+ if (code === "INSUFFICIENT_SCOPE") return { result: "ingest-only", status: res.status };
849
+ }
850
+ return { result: "unknown", status: res.status };
851
+ }
852
+ function describeUnsafeSdkKey(probe, endpoint) {
853
+ switch (probe.result) {
854
+ case "mcp":
855
+ return "this key can read your bug reports (mcp scope), and SDK env vars ship inside the app bundle";
856
+ case "invalid":
857
+ return "the backend rejected this key";
858
+ case "unreachable":
859
+ return `could not reach ${endpoint} to confirm the key is ingest-only (${probe.message ?? "network error"})`;
860
+ case "unknown":
861
+ return `could not confirm the key is ingest-only (HTTP ${probe.status ?? "?"})`;
862
+ case "ingest-only":
863
+ return "the key is ingest-only";
864
+ }
865
+ }
866
+
1224
867
  // src/mcp-config.ts
1225
868
  import { mkdir, readFile, writeFile } from "fs/promises";
1226
869
  import { dirname as dirname3 } from "path";
1227
870
  var DEFAULT_MUSHI_FEATURES = "triage,fixes,inventory,setup,docs";
871
+ var MCP_CLIENTS = ["cursor", "claude", "continue", "zed"];
872
+ function isMcpClient(value) {
873
+ return typeof value === "string" && MCP_CLIENTS.includes(value);
874
+ }
875
+ var MCP_CLIENT_LABEL = {
876
+ cursor: "Cursor",
877
+ claude: "Claude Code",
878
+ continue: "Continue",
879
+ zed: "Zed"
880
+ };
881
+ function buildHostedMcpServerBlock(url) {
882
+ return { type: "http", url };
883
+ }
884
+ function isPlainObject(value) {
885
+ return typeof value === "object" && value !== null && !Array.isArray(value);
886
+ }
1228
887
  async function writeMcpServerEntry(opts) {
1229
888
  const { configPath, serverName, serverBlock, dryRun = false } = opts;
1230
- let merged = { mcpServers: {} };
1231
- let created = false;
889
+ let existing = null;
1232
890
  try {
1233
- const existing = await readFile(configPath, "utf8");
891
+ existing = await readFile(configPath, "utf8");
892
+ } catch {
893
+ existing = null;
894
+ }
895
+ let merged = {};
896
+ if (existing !== null && existing.trim() !== "") {
897
+ let parsed;
1234
898
  try {
1235
- merged = JSON.parse(existing);
899
+ parsed = JSON.parse(existing);
1236
900
  } catch {
1237
- merged = { mcpServers: {} };
901
+ throw new Error(`${configPath} is not valid JSON \u2014 fix or remove it, then re-run (nothing was written).`);
1238
902
  }
1239
- } catch {
1240
- created = true;
1241
- merged = { mcpServers: {} };
903
+ if (!isPlainObject(parsed)) {
904
+ throw new Error(`${configPath} is not a JSON object \u2014 fix or remove it, then re-run (nothing was written).`);
905
+ }
906
+ merged = parsed;
907
+ }
908
+ const currentServers = merged.mcpServers;
909
+ if (currentServers !== void 0 && !isPlainObject(currentServers)) {
910
+ throw new Error(`${configPath} has an "mcpServers" field that is not an object \u2014 fix it, then re-run (nothing was written).`);
1242
911
  }
1243
- const servers = merged.mcpServers ?? {};
1244
- servers[serverName] = serverBlock;
1245
- merged.mcpServers = servers;
912
+ merged.mcpServers = { ...currentServers ?? {}, [serverName]: serverBlock };
1246
913
  const output = JSON.stringify(merged, null, 2) + "\n";
1247
914
  if (!dryRun) {
1248
915
  await mkdir(dirname3(configPath), { recursive: true });
1249
916
  await writeFile(configPath, output, "utf8");
1250
917
  }
1251
- return { created, path: configPath };
918
+ return { created: existing === null, path: configPath };
919
+ }
920
+ function apiKeyPlaceholderFor(client) {
921
+ switch (client) {
922
+ case "cursor":
923
+ return "${env:MUSHI_API_KEY}";
924
+ case "claude":
925
+ return "${MUSHI_API_KEY:-}";
926
+ case "continue":
927
+ case "zed":
928
+ return null;
929
+ }
1252
930
  }
1253
931
  function buildMcpServerBlock(opts) {
932
+ const keyValue = opts.inlineKey ? opts.apiKey : apiKeyPlaceholderFor(opts.client);
1254
933
  return {
1255
934
  command: "npx",
1256
935
  args: ["-y", MUSHI_MCP_PIN_SPEC],
1257
936
  env: {
1258
937
  MUSHI_API_ENDPOINT: opts.endpoint,
1259
938
  MUSHI_PROJECT_ID: opts.projectId,
1260
- MUSHI_API_KEY: opts.inlineKey ? opts.apiKey : "${MUSHI_API_KEY}",
939
+ ...keyValue !== null ? { MUSHI_API_KEY: keyValue } : {},
1261
940
  MUSHI_FEATURES: DEFAULT_MUSHI_FEATURES
1262
941
  }
1263
942
  };
1264
943
  }
1265
- function printKeyExportHint(apiKey) {
1266
- console.log("");
1267
- console.log(" Your API key was NOT written into the config file (prevents git leaks).");
1268
- console.log(" Add it to your shell profile or .env.local:");
944
+ function printKeyExportHint(client, configPath = CONFIG_PATH) {
1269
945
  console.log("");
1270
- console.log(` export MUSHI_API_KEY="${apiKey}"`);
1271
- console.log("");
1272
- console.log(" Then restart your IDE. To write the key inline instead, re-run with --inline-key.");
946
+ console.log(" No API key was written into the MCP config (safe to commit).");
947
+ console.log(` The MCP server uses the key \`mushi login\` saved in ${configPath}.`);
948
+ if (apiKeyPlaceholderFor(client) !== null) {
949
+ console.log(` To use a different key, export MUSHI_API_KEY in the environment ${MCP_CLIENT_LABEL[client]} is launched from.`);
950
+ }
951
+ console.log(" To write the key into the file instead (keep it out of git), re-run with --inline-key.");
1273
952
  }
1274
953
  function buildMcpServerName(opts) {
1275
954
  if (opts.legacy) return "mushi";
@@ -1281,6 +960,12 @@ function buildMcpServerName(opts) {
1281
960
  }
1282
961
 
1283
962
  // src/connect.ts
963
+ async function resolveSdkEnvKey(opts) {
964
+ const candidate = opts.sdkKey ?? savedSdkKeyFor(opts.baseConfig, opts.projectId) ?? opts.apiKey;
965
+ const probe = await opts.probe(opts.endpoint, candidate, opts.projectId);
966
+ if (probe.result === "ingest-only") return { ok: true, key: candidate };
967
+ return { ok: false, reason: describeUnsafeSdkKey(probe, opts.endpoint) };
968
+ }
1284
969
  function envKeyPresent(content, key) {
1285
970
  const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1286
971
  return new RegExp(`^${escaped}=`, "m").test(content);
@@ -1310,44 +995,41 @@ ${needs.join("\n")}
1310
995
  await writeFile2(path, block, "utf8");
1311
996
  return true;
1312
997
  }
1313
- async function ensureMcpJsonGitignored(cwd, messages) {
1314
- const gitignorePath = join4(cwd, ".gitignore");
1315
- const patterns = [".cursor/mcp.json", ".cursor/"];
1316
- let content = null;
1317
- try {
1318
- content = await readFile2(gitignorePath, "utf8");
1319
- } catch {
1320
- }
1321
- if (content === null) {
1322
- messages.push(
1323
- "\u26A0 No .gitignore found \u2014 .cursor/mcp.json contains your API key. Add `.cursor/mcp.json` before committing."
1324
- );
1325
- return;
1326
- }
1327
- const covered = patterns.some((p) => content.split("\n").some((line) => line.trim() === p || line.trim() === `${p}/`));
1328
- if (covered) return;
1329
- await appendFile(gitignorePath, "\n# Mushi \u2014 keep MCP credentials out of git\n.cursor/mcp.json\n", "utf8");
1330
- messages.push("\u2713 Added .cursor/mcp.json to .gitignore (contains API key)");
1331
- }
1332
998
  async function runConnect(opts, baseConfig = {}) {
1333
999
  const cwd = resolve(opts.cwd ?? process.cwd());
1334
1000
  const endpoint = assertEndpoint(opts.endpoint);
1335
1001
  const projectId = requireUuid(opts.projectId, "projectId");
1336
1002
  const messages = [];
1003
+ const writeEnv = opts.writeEnv !== false;
1004
+ const sdkEnvKey = writeEnv ? await resolveSdkEnvKey({
1005
+ sdkKey: opts.sdkKey,
1006
+ apiKey: opts.apiKey,
1007
+ projectId,
1008
+ endpoint,
1009
+ baseConfig,
1010
+ probe: opts.probeKeyScope ?? probeKeyScope
1011
+ }) : null;
1337
1012
  const config = {
1338
1013
  ...baseConfig,
1339
1014
  apiKey: opts.apiKey,
1340
1015
  projectId,
1341
- endpoint
1016
+ endpoint,
1017
+ ...sdkEnvKey?.ok ? { sdkKey: { projectId, key: sdkEnvKey.key } } : {}
1342
1018
  };
1343
1019
  saveConfig(config);
1344
1020
  messages.push(`\u2713 Credentials saved to ${CONFIG_PATH}`);
1345
1021
  let envPath = null;
1346
- if (opts.writeEnv !== false) {
1022
+ let envRefused = false;
1023
+ if (sdkEnvKey && !sdkEnvKey.ok) {
1024
+ envRefused = true;
1025
+ messages.push(
1026
+ `\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.`
1027
+ );
1028
+ } else if (sdkEnvKey?.ok) {
1347
1029
  const pkg = readPackageJson(cwd);
1348
1030
  const framework = detectFramework(cwd, pkg);
1349
- const lines = envVarsToWrite(opts.apiKey, projectId, framework).split("\n");
1350
- envPath = join4(cwd, ".env.local");
1031
+ const lines = envVarsToWrite(sdkEnvKey.key, projectId, framework).split("\n");
1032
+ envPath = join3(cwd, ".env.local");
1351
1033
  const wrote = await mergeEnvFile(envPath, lines);
1352
1034
  messages.push(
1353
1035
  wrote ? `\u2713 Env vars merged into ${envPath}` : `\u2713 Env vars already present in ${envPath} (existing values left untouched)`
@@ -1361,20 +1043,20 @@ async function runConnect(opts, baseConfig = {}) {
1361
1043
  }
1362
1044
  let mcpPath = null;
1363
1045
  if (opts.wireIde !== false) {
1364
- const mcpDir = join4(cwd, ".cursor");
1365
- mcpPath = join4(mcpDir, "mcp.json");
1046
+ const mcpDir = join3(cwd, ".cursor");
1047
+ mcpPath = join3(mcpDir, "mcp.json");
1366
1048
  const serverName = buildMcpServerName({ projectId });
1367
1049
  const serverBlock = buildMcpServerBlock({
1368
1050
  endpoint,
1369
1051
  projectId,
1370
1052
  apiKey: opts.apiKey,
1053
+ client: "cursor",
1371
1054
  inlineKey: false
1372
- // use ${MUSHI_API_KEY} placeholder; caller must export the key
1055
+ // no secret in the file; the MCP server reads the saved CLI key
1373
1056
  });
1374
1057
  await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
1375
- await ensureMcpJsonGitignored(cwd, messages);
1376
1058
  messages.push(`\u2713 Wired ${mcpPath} \u2014 restart Cursor and run "list mushi tools"`);
1377
- messages.push(` Set MUSHI_API_KEY="${opts.apiKey}" in your shell / .env.local before restarting the IDE.`);
1059
+ messages.push(` The MCP server reads the key saved in ${CONFIG_PATH} (export MUSHI_API_KEY before launching Cursor to override it).`);
1378
1060
  }
1379
1061
  let heartbeat = null;
1380
1062
  if (opts.wait) {
@@ -1408,27 +1090,31 @@ async function runConnect(opts, baseConfig = {}) {
1408
1090
  );
1409
1091
  }
1410
1092
  }
1411
- const ok = !opts.wait || Boolean(heartbeat?.ok);
1093
+ const ok = !envRefused && (!opts.wait || Boolean(heartbeat?.ok));
1412
1094
  return { ok, envPath, mcpPath, heartbeat, messages };
1413
1095
  }
1414
1096
 
1415
1097
  export {
1098
+ MushiCliError,
1416
1099
  printAndExit,
1417
1100
  installSignalHandlers,
1418
1101
  trySaveKeyToKeychain,
1102
+ savedSdkKeyFor,
1419
1103
  resolveProfileName,
1104
+ CONFIG_PATH,
1420
1105
  loadConfig,
1421
1106
  ensureClientId,
1422
1107
  maybeShowTelemetryNotice,
1423
1108
  saveConfig,
1424
1109
  listProfiles,
1425
1110
  setActiveProfile,
1426
- FRAMEWORKS,
1427
- readPackageJson,
1428
- detectFramework,
1429
- detectPackageManager,
1430
- installCommand,
1431
- envVarsToWrite,
1111
+ SDK_KEY_SCOPES,
1112
+ CLI_KEY_SCOPES,
1113
+ FULL_CLI_KEY_SCOPES,
1114
+ SDK_KEY_LABEL,
1115
+ CLI_KEY_LABEL,
1116
+ probeKeyScope,
1117
+ describeUnsafeSdkKey,
1432
1118
  consoleUrl,
1433
1119
  resolveConsoleUrlSync,
1434
1120
  resolveConsoleUrl,
@@ -1453,9 +1139,14 @@ export {
1453
1139
  sanitizeCliCredentials,
1454
1140
  apiKeyHeaders,
1455
1141
  fetchIngestSetup,
1142
+ MCP_CLIENTS,
1143
+ isMcpClient,
1144
+ MCP_CLIENT_LABEL,
1145
+ buildHostedMcpServerBlock,
1456
1146
  writeMcpServerEntry,
1457
1147
  buildMcpServerBlock,
1458
1148
  printKeyExportHint,
1459
1149
  buildMcpServerName,
1150
+ resolveSdkEnvKey,
1460
1151
  runConnect
1461
1152
  };