@mushi-mushi/cli 0.27.1 → 0.28.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-UVO4A2G6.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;
@@ -670,446 +680,6 @@ function pad(s, width) {
670
680
  return s.length >= width ? s : s + " ".repeat(width - s.length);
671
681
  }
672
682
 
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
683
  // src/sanitize-config.ts
1114
684
  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
685
  var API_KEY_RE = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
@@ -1221,55 +791,133 @@ async function waitForIngestReady(options) {
1221
791
  return { ok: false, payload: lastPayload, attempts: maxAttempts, reason: "timeout" };
1222
792
  }
1223
793
 
794
+ // src/key-scopes.ts
795
+ var SDK_KEY_SCOPES = ["report:write"];
796
+ var CLI_KEY_SCOPES = ["report:write", "mcp:read"];
797
+ var FULL_CLI_KEY_SCOPES = ["report:write", "mcp:read", "mcp:write"];
798
+ var SDK_KEY_LABEL = "sdk-ingest";
799
+ var CLI_KEY_LABEL = "cli-login";
800
+ async function probeKeyScope(endpoint, apiKey, projectId, doFetch = globalThis.fetch) {
801
+ let base = endpoint;
802
+ while (base.endsWith("/")) base = base.slice(0, -1);
803
+ let res;
804
+ try {
805
+ res = await doFetch(`${base}/v1/admin/mcp/account-overview`, {
806
+ headers: { "X-Mushi-Api-Key": apiKey, "X-Mushi-Project": projectId },
807
+ signal: AbortSignal.timeout(8e3)
808
+ });
809
+ } catch (err) {
810
+ return { result: "unreachable", message: err instanceof Error ? err.message : String(err) };
811
+ }
812
+ if (res.ok) return { result: "mcp", status: res.status };
813
+ if (res.status === 401) return { result: "invalid", status: res.status };
814
+ if (res.status === 403) {
815
+ const body = await res.json().catch(() => null);
816
+ const code = body?.error?.code ?? body?.code;
817
+ if (code === "INSUFFICIENT_SCOPE") return { result: "ingest-only", status: res.status };
818
+ }
819
+ return { result: "unknown", status: res.status };
820
+ }
821
+ function describeUnsafeSdkKey(probe, endpoint) {
822
+ switch (probe.result) {
823
+ case "mcp":
824
+ return "this key can read your bug reports (mcp scope), and SDK env vars ship inside the app bundle";
825
+ case "invalid":
826
+ return "the backend rejected this key";
827
+ case "unreachable":
828
+ return `could not reach ${endpoint} to confirm the key is ingest-only (${probe.message ?? "network error"})`;
829
+ case "unknown":
830
+ return `could not confirm the key is ingest-only (HTTP ${probe.status ?? "?"})`;
831
+ case "ingest-only":
832
+ return "the key is ingest-only";
833
+ }
834
+ }
835
+
1224
836
  // src/mcp-config.ts
1225
837
  import { mkdir, readFile, writeFile } from "fs/promises";
1226
838
  import { dirname as dirname3 } from "path";
1227
839
  var DEFAULT_MUSHI_FEATURES = "triage,fixes,inventory,setup,docs";
840
+ var MCP_CLIENTS = ["cursor", "claude", "continue", "zed"];
841
+ function isMcpClient(value) {
842
+ return typeof value === "string" && MCP_CLIENTS.includes(value);
843
+ }
844
+ var MCP_CLIENT_LABEL = {
845
+ cursor: "Cursor",
846
+ claude: "Claude Code",
847
+ continue: "Continue",
848
+ zed: "Zed"
849
+ };
850
+ function buildHostedMcpServerBlock(url) {
851
+ return { type: "http", url };
852
+ }
853
+ function isPlainObject(value) {
854
+ return typeof value === "object" && value !== null && !Array.isArray(value);
855
+ }
1228
856
  async function writeMcpServerEntry(opts) {
1229
857
  const { configPath, serverName, serverBlock, dryRun = false } = opts;
1230
- let merged = { mcpServers: {} };
1231
- let created = false;
858
+ let existing = null;
1232
859
  try {
1233
- const existing = await readFile(configPath, "utf8");
860
+ existing = await readFile(configPath, "utf8");
861
+ } catch {
862
+ existing = null;
863
+ }
864
+ let merged = {};
865
+ if (existing !== null && existing.trim() !== "") {
866
+ let parsed;
1234
867
  try {
1235
- merged = JSON.parse(existing);
868
+ parsed = JSON.parse(existing);
1236
869
  } catch {
1237
- merged = { mcpServers: {} };
870
+ throw new Error(`${configPath} is not valid JSON \u2014 fix or remove it, then re-run (nothing was written).`);
1238
871
  }
1239
- } catch {
1240
- created = true;
1241
- merged = { mcpServers: {} };
872
+ if (!isPlainObject(parsed)) {
873
+ throw new Error(`${configPath} is not a JSON object \u2014 fix or remove it, then re-run (nothing was written).`);
874
+ }
875
+ merged = parsed;
1242
876
  }
1243
- const servers = merged.mcpServers ?? {};
1244
- servers[serverName] = serverBlock;
1245
- merged.mcpServers = servers;
877
+ const currentServers = merged.mcpServers;
878
+ if (currentServers !== void 0 && !isPlainObject(currentServers)) {
879
+ throw new Error(`${configPath} has an "mcpServers" field that is not an object \u2014 fix it, then re-run (nothing was written).`);
880
+ }
881
+ merged.mcpServers = { ...currentServers ?? {}, [serverName]: serverBlock };
1246
882
  const output = JSON.stringify(merged, null, 2) + "\n";
1247
883
  if (!dryRun) {
1248
884
  await mkdir(dirname3(configPath), { recursive: true });
1249
885
  await writeFile(configPath, output, "utf8");
1250
886
  }
1251
- return { created, path: configPath };
887
+ return { created: existing === null, path: configPath };
888
+ }
889
+ function apiKeyPlaceholderFor(client) {
890
+ switch (client) {
891
+ case "cursor":
892
+ return "${env:MUSHI_API_KEY}";
893
+ case "claude":
894
+ return "${MUSHI_API_KEY:-}";
895
+ case "continue":
896
+ case "zed":
897
+ return null;
898
+ }
1252
899
  }
1253
900
  function buildMcpServerBlock(opts) {
901
+ const keyValue = opts.inlineKey ? opts.apiKey : apiKeyPlaceholderFor(opts.client);
1254
902
  return {
1255
903
  command: "npx",
1256
904
  args: ["-y", MUSHI_MCP_PIN_SPEC],
1257
905
  env: {
1258
906
  MUSHI_API_ENDPOINT: opts.endpoint,
1259
907
  MUSHI_PROJECT_ID: opts.projectId,
1260
- MUSHI_API_KEY: opts.inlineKey ? opts.apiKey : "${MUSHI_API_KEY}",
908
+ ...keyValue !== null ? { MUSHI_API_KEY: keyValue } : {},
1261
909
  MUSHI_FEATURES: DEFAULT_MUSHI_FEATURES
1262
910
  }
1263
911
  };
1264
912
  }
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:");
1269
- console.log("");
1270
- console.log(` export MUSHI_API_KEY="${apiKey}"`);
913
+ function printKeyExportHint(client, configPath = CONFIG_PATH) {
1271
914
  console.log("");
1272
- console.log(" Then restart your IDE. To write the key inline instead, re-run with --inline-key.");
915
+ console.log(" No API key was written into the MCP config (safe to commit).");
916
+ console.log(` The MCP server uses the key \`mushi login\` saved in ${configPath}.`);
917
+ if (apiKeyPlaceholderFor(client) !== null) {
918
+ console.log(` To use a different key, export MUSHI_API_KEY in the environment ${MCP_CLIENT_LABEL[client]} is launched from.`);
919
+ }
920
+ console.log(" To write the key into the file instead (keep it out of git), re-run with --inline-key.");
1273
921
  }
1274
922
  function buildMcpServerName(opts) {
1275
923
  if (opts.legacy) return "mushi";
@@ -1281,6 +929,12 @@ function buildMcpServerName(opts) {
1281
929
  }
1282
930
 
1283
931
  // src/connect.ts
932
+ async function resolveSdkEnvKey(opts) {
933
+ const candidate = opts.sdkKey ?? savedSdkKeyFor(opts.baseConfig, opts.projectId) ?? opts.apiKey;
934
+ const probe = await opts.probe(opts.endpoint, candidate, opts.projectId);
935
+ if (probe.result === "ingest-only") return { ok: true, key: candidate };
936
+ return { ok: false, reason: describeUnsafeSdkKey(probe, opts.endpoint) };
937
+ }
1284
938
  function envKeyPresent(content, key) {
1285
939
  const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1286
940
  return new RegExp(`^${escaped}=`, "m").test(content);
@@ -1310,44 +964,41 @@ ${needs.join("\n")}
1310
964
  await writeFile2(path, block, "utf8");
1311
965
  return true;
1312
966
  }
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
967
  async function runConnect(opts, baseConfig = {}) {
1333
968
  const cwd = resolve(opts.cwd ?? process.cwd());
1334
969
  const endpoint = assertEndpoint(opts.endpoint);
1335
970
  const projectId = requireUuid(opts.projectId, "projectId");
1336
971
  const messages = [];
972
+ const writeEnv = opts.writeEnv !== false;
973
+ const sdkEnvKey = writeEnv ? await resolveSdkEnvKey({
974
+ sdkKey: opts.sdkKey,
975
+ apiKey: opts.apiKey,
976
+ projectId,
977
+ endpoint,
978
+ baseConfig,
979
+ probe: opts.probeKeyScope ?? probeKeyScope
980
+ }) : null;
1337
981
  const config = {
1338
982
  ...baseConfig,
1339
983
  apiKey: opts.apiKey,
1340
984
  projectId,
1341
- endpoint
985
+ endpoint,
986
+ ...sdkEnvKey?.ok ? { sdkKey: { projectId, key: sdkEnvKey.key } } : {}
1342
987
  };
1343
988
  saveConfig(config);
1344
989
  messages.push(`\u2713 Credentials saved to ${CONFIG_PATH}`);
1345
990
  let envPath = null;
1346
- if (opts.writeEnv !== false) {
991
+ let envRefused = false;
992
+ if (sdkEnvKey && !sdkEnvKey.ok) {
993
+ envRefused = true;
994
+ messages.push(
995
+ `\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.`
996
+ );
997
+ } else if (sdkEnvKey?.ok) {
1347
998
  const pkg = readPackageJson(cwd);
1348
999
  const framework = detectFramework(cwd, pkg);
1349
- const lines = envVarsToWrite(opts.apiKey, projectId, framework).split("\n");
1350
- envPath = join4(cwd, ".env.local");
1000
+ const lines = envVarsToWrite(sdkEnvKey.key, projectId, framework).split("\n");
1001
+ envPath = join3(cwd, ".env.local");
1351
1002
  const wrote = await mergeEnvFile(envPath, lines);
1352
1003
  messages.push(
1353
1004
  wrote ? `\u2713 Env vars merged into ${envPath}` : `\u2713 Env vars already present in ${envPath} (existing values left untouched)`
@@ -1361,20 +1012,20 @@ async function runConnect(opts, baseConfig = {}) {
1361
1012
  }
1362
1013
  let mcpPath = null;
1363
1014
  if (opts.wireIde !== false) {
1364
- const mcpDir = join4(cwd, ".cursor");
1365
- mcpPath = join4(mcpDir, "mcp.json");
1015
+ const mcpDir = join3(cwd, ".cursor");
1016
+ mcpPath = join3(mcpDir, "mcp.json");
1366
1017
  const serverName = buildMcpServerName({ projectId });
1367
1018
  const serverBlock = buildMcpServerBlock({
1368
1019
  endpoint,
1369
1020
  projectId,
1370
1021
  apiKey: opts.apiKey,
1022
+ client: "cursor",
1371
1023
  inlineKey: false
1372
- // use ${MUSHI_API_KEY} placeholder; caller must export the key
1024
+ // no secret in the file; the MCP server reads the saved CLI key
1373
1025
  });
1374
1026
  await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
1375
- await ensureMcpJsonGitignored(cwd, messages);
1376
1027
  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.`);
1028
+ messages.push(` The MCP server reads the key saved in ${CONFIG_PATH} (export MUSHI_API_KEY before launching Cursor to override it).`);
1378
1029
  }
1379
1030
  let heartbeat = null;
1380
1031
  if (opts.wait) {
@@ -1408,7 +1059,7 @@ async function runConnect(opts, baseConfig = {}) {
1408
1059
  );
1409
1060
  }
1410
1061
  }
1411
- const ok = !opts.wait || Boolean(heartbeat?.ok);
1062
+ const ok = !envRefused && (!opts.wait || Boolean(heartbeat?.ok));
1412
1063
  return { ok, envPath, mcpPath, heartbeat, messages };
1413
1064
  }
1414
1065
 
@@ -1416,19 +1067,22 @@ export {
1416
1067
  printAndExit,
1417
1068
  installSignalHandlers,
1418
1069
  trySaveKeyToKeychain,
1070
+ savedSdkKeyFor,
1419
1071
  resolveProfileName,
1072
+ CONFIG_PATH,
1420
1073
  loadConfig,
1421
1074
  ensureClientId,
1422
1075
  maybeShowTelemetryNotice,
1423
1076
  saveConfig,
1424
1077
  listProfiles,
1425
1078
  setActiveProfile,
1426
- FRAMEWORKS,
1427
- readPackageJson,
1428
- detectFramework,
1429
- detectPackageManager,
1430
- installCommand,
1431
- envVarsToWrite,
1079
+ SDK_KEY_SCOPES,
1080
+ CLI_KEY_SCOPES,
1081
+ FULL_CLI_KEY_SCOPES,
1082
+ SDK_KEY_LABEL,
1083
+ CLI_KEY_LABEL,
1084
+ probeKeyScope,
1085
+ describeUnsafeSdkKey,
1432
1086
  consoleUrl,
1433
1087
  resolveConsoleUrlSync,
1434
1088
  resolveConsoleUrl,
@@ -1453,9 +1107,14 @@ export {
1453
1107
  sanitizeCliCredentials,
1454
1108
  apiKeyHeaders,
1455
1109
  fetchIngestSetup,
1110
+ MCP_CLIENTS,
1111
+ isMcpClient,
1112
+ MCP_CLIENT_LABEL,
1113
+ buildHostedMcpServerBlock,
1456
1114
  writeMcpServerEntry,
1457
1115
  buildMcpServerBlock,
1458
1116
  printKeyExportHint,
1459
1117
  buildMcpServerName,
1118
+ resolveSdkEnvKey,
1460
1119
  runConnect
1461
1120
  };