@mushi-mushi/cli 0.27.0 → 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.
@@ -1,18 +1,23 @@
1
1
  import {
2
2
  CONFIG_PATH,
3
- assertEndpoint,
3
+ describeUnsafeSdkKey,
4
+ probeKeyScope,
4
5
  requireUuid,
5
6
  saveConfig,
7
+ savedSdkKeyFor,
6
8
  waitForIngestReady
7
- } from "./chunk-TZL3F3GT.js";
9
+ } from "./chunk-AV3RKAEE.js";
8
10
  import {
9
11
  MUSHI_MCP_PIN_SPEC
10
- } from "./chunk-LFZ5ZIBI.js";
12
+ } from "./chunk-U2VGB63C.js";
11
13
  import {
12
14
  detectFramework,
13
15
  envVarsToWrite,
14
16
  readPackageJson
15
17
  } from "./chunk-AIZRI665.js";
18
+ import {
19
+ assertEndpoint
20
+ } from "./chunk-SXSI2W6Z.js";
16
21
  import "./chunk-3RG5ZIWI.js";
17
22
 
18
23
  // src/connect.ts
@@ -23,39 +28,62 @@ import { join, resolve } from "path";
23
28
  import { mkdir, readFile, writeFile } from "fs/promises";
24
29
  import { dirname } from "path";
25
30
  var DEFAULT_MUSHI_FEATURES = "triage,fixes,inventory,setup,docs";
31
+ function isPlainObject(value) {
32
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33
+ }
26
34
  async function writeMcpServerEntry(opts) {
27
35
  const { configPath, serverName, serverBlock, dryRun = false } = opts;
28
- let merged = { mcpServers: {} };
29
- let created = false;
36
+ let existing = null;
30
37
  try {
31
- const existing = await readFile(configPath, "utf8");
38
+ existing = await readFile(configPath, "utf8");
39
+ } catch {
40
+ existing = null;
41
+ }
42
+ let merged = {};
43
+ if (existing !== null && existing.trim() !== "") {
44
+ let parsed;
32
45
  try {
33
- merged = JSON.parse(existing);
46
+ parsed = JSON.parse(existing);
34
47
  } catch {
35
- merged = { mcpServers: {} };
48
+ throw new Error(`${configPath} is not valid JSON \u2014 fix or remove it, then re-run (nothing was written).`);
36
49
  }
37
- } catch {
38
- created = true;
39
- merged = { mcpServers: {} };
50
+ if (!isPlainObject(parsed)) {
51
+ throw new Error(`${configPath} is not a JSON object \u2014 fix or remove it, then re-run (nothing was written).`);
52
+ }
53
+ merged = parsed;
54
+ }
55
+ const currentServers = merged.mcpServers;
56
+ if (currentServers !== void 0 && !isPlainObject(currentServers)) {
57
+ throw new Error(`${configPath} has an "mcpServers" field that is not an object \u2014 fix it, then re-run (nothing was written).`);
40
58
  }
41
- const servers = merged.mcpServers ?? {};
42
- servers[serverName] = serverBlock;
43
- merged.mcpServers = servers;
59
+ merged.mcpServers = { ...currentServers ?? {}, [serverName]: serverBlock };
44
60
  const output = JSON.stringify(merged, null, 2) + "\n";
45
61
  if (!dryRun) {
46
62
  await mkdir(dirname(configPath), { recursive: true });
47
63
  await writeFile(configPath, output, "utf8");
48
64
  }
49
- return { created, path: configPath };
65
+ return { created: existing === null, path: configPath };
66
+ }
67
+ function apiKeyPlaceholderFor(client) {
68
+ switch (client) {
69
+ case "cursor":
70
+ return "${env:MUSHI_API_KEY}";
71
+ case "claude":
72
+ return "${MUSHI_API_KEY:-}";
73
+ case "continue":
74
+ case "zed":
75
+ return null;
76
+ }
50
77
  }
51
78
  function buildMcpServerBlock(opts) {
79
+ const keyValue = opts.inlineKey ? opts.apiKey : apiKeyPlaceholderFor(opts.client);
52
80
  return {
53
81
  command: "npx",
54
82
  args: ["-y", MUSHI_MCP_PIN_SPEC],
55
83
  env: {
56
84
  MUSHI_API_ENDPOINT: opts.endpoint,
57
85
  MUSHI_PROJECT_ID: opts.projectId,
58
- MUSHI_API_KEY: opts.inlineKey ? opts.apiKey : "${MUSHI_API_KEY}",
86
+ ...keyValue !== null ? { MUSHI_API_KEY: keyValue } : {},
59
87
  MUSHI_FEATURES: DEFAULT_MUSHI_FEATURES
60
88
  }
61
89
  };
@@ -70,6 +98,12 @@ function buildMcpServerName(opts) {
70
98
  }
71
99
 
72
100
  // src/connect.ts
101
+ async function resolveSdkEnvKey(opts) {
102
+ const candidate = opts.sdkKey ?? savedSdkKeyFor(opts.baseConfig, opts.projectId) ?? opts.apiKey;
103
+ const probe = await opts.probe(opts.endpoint, candidate, opts.projectId);
104
+ if (probe.result === "ingest-only") return { ok: true, key: candidate };
105
+ return { ok: false, reason: describeUnsafeSdkKey(probe, opts.endpoint) };
106
+ }
73
107
  function envKeyPresent(content, key) {
74
108
  const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75
109
  return new RegExp(`^${escaped}=`, "m").test(content);
@@ -99,43 +133,40 @@ ${needs.join("\n")}
99
133
  await writeFile2(path, block, "utf8");
100
134
  return true;
101
135
  }
102
- async function ensureMcpJsonGitignored(cwd, messages) {
103
- const gitignorePath = join(cwd, ".gitignore");
104
- const patterns = [".cursor/mcp.json", ".cursor/"];
105
- let content = null;
106
- try {
107
- content = await readFile2(gitignorePath, "utf8");
108
- } catch {
109
- }
110
- if (content === null) {
111
- messages.push(
112
- "\u26A0 No .gitignore found \u2014 .cursor/mcp.json contains your API key. Add `.cursor/mcp.json` before committing."
113
- );
114
- return;
115
- }
116
- const covered = patterns.some((p) => content.split("\n").some((line) => line.trim() === p || line.trim() === `${p}/`));
117
- if (covered) return;
118
- await appendFile(gitignorePath, "\n# Mushi \u2014 keep MCP credentials out of git\n.cursor/mcp.json\n", "utf8");
119
- messages.push("\u2713 Added .cursor/mcp.json to .gitignore (contains API key)");
120
- }
121
136
  async function runConnect(opts, baseConfig = {}) {
122
137
  const cwd = resolve(opts.cwd ?? process.cwd());
123
138
  const endpoint = assertEndpoint(opts.endpoint);
124
139
  const projectId = requireUuid(opts.projectId, "projectId");
125
140
  const messages = [];
141
+ const writeEnv = opts.writeEnv !== false;
142
+ const sdkEnvKey = writeEnv ? await resolveSdkEnvKey({
143
+ sdkKey: opts.sdkKey,
144
+ apiKey: opts.apiKey,
145
+ projectId,
146
+ endpoint,
147
+ baseConfig,
148
+ probe: opts.probeKeyScope ?? probeKeyScope
149
+ }) : null;
126
150
  const config = {
127
151
  ...baseConfig,
128
152
  apiKey: opts.apiKey,
129
153
  projectId,
130
- endpoint
154
+ endpoint,
155
+ ...sdkEnvKey?.ok ? { sdkKey: { projectId, key: sdkEnvKey.key } } : {}
131
156
  };
132
157
  saveConfig(config);
133
158
  messages.push(`\u2713 Credentials saved to ${CONFIG_PATH}`);
134
159
  let envPath = null;
135
- if (opts.writeEnv !== false) {
160
+ let envRefused = false;
161
+ if (sdkEnvKey && !sdkEnvKey.ok) {
162
+ envRefused = true;
163
+ messages.push(
164
+ `\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.`
165
+ );
166
+ } else if (sdkEnvKey?.ok) {
136
167
  const pkg = readPackageJson(cwd);
137
168
  const framework = detectFramework(cwd, pkg);
138
- const lines = envVarsToWrite(opts.apiKey, projectId, framework).split("\n");
169
+ const lines = envVarsToWrite(sdkEnvKey.key, projectId, framework).split("\n");
139
170
  envPath = join(cwd, ".env.local");
140
171
  const wrote = await mergeEnvFile(envPath, lines);
141
172
  messages.push(
@@ -157,13 +188,13 @@ async function runConnect(opts, baseConfig = {}) {
157
188
  endpoint,
158
189
  projectId,
159
190
  apiKey: opts.apiKey,
191
+ client: "cursor",
160
192
  inlineKey: false
161
- // use ${MUSHI_API_KEY} placeholder; caller must export the key
193
+ // no secret in the file; the MCP server reads the saved CLI key
162
194
  });
163
195
  await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
164
- await ensureMcpJsonGitignored(cwd, messages);
165
196
  messages.push(`\u2713 Wired ${mcpPath} \u2014 restart Cursor and run "list mushi tools"`);
166
- messages.push(` Set MUSHI_API_KEY="${opts.apiKey}" in your shell / .env.local before restarting the IDE.`);
197
+ messages.push(` The MCP server reads the key saved in ${CONFIG_PATH} (export MUSHI_API_KEY before launching Cursor to override it).`);
167
198
  }
168
199
  let heartbeat = null;
169
200
  if (opts.wait) {
@@ -197,9 +228,10 @@ async function runConnect(opts, baseConfig = {}) {
197
228
  );
198
229
  }
199
230
  }
200
- const ok = !opts.wait || Boolean(heartbeat?.ok);
231
+ const ok = !envRefused && (!opts.wait || Boolean(heartbeat?.ok));
201
232
  return { ok, envPath, mcpPath, heartbeat, messages };
202
233
  }
203
234
  export {
235
+ resolveSdkEnvKey,
204
236
  runConnect
205
237
  };
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ FRAMEWORKS,
4
+ FRAMEWORK_IDS,
5
+ detectFramework,
6
+ detectPackageManager,
7
+ envVarsToWrite,
8
+ installCommand,
9
+ isFrameworkId,
10
+ readPackageJson
11
+ } from "./chunk-KOBEOGNF.js";
12
+ import "./chunk-PDX44BCA.js";
13
+ export {
14
+ FRAMEWORKS,
15
+ FRAMEWORK_IDS,
16
+ detectFramework,
17
+ detectPackageManager,
18
+ envVarsToWrite,
19
+ installCommand,
20
+ isFrameworkId,
21
+ readPackageJson
22
+ };
@@ -0,0 +1,15 @@
1
+ import {
2
+ CLOUD_API_ENDPOINT,
3
+ TEST_REPORT_FETCH_TIMEOUT_MS,
4
+ assertEndpoint,
5
+ normalizeEndpoint,
6
+ resolveCloudEndpoint
7
+ } from "./chunk-SXSI2W6Z.js";
8
+ import "./chunk-3RG5ZIWI.js";
9
+ export {
10
+ CLOUD_API_ENDPOINT,
11
+ TEST_REPORT_FETCH_TIMEOUT_MS,
12
+ assertEndpoint,
13
+ normalizeEndpoint,
14
+ resolveCloudEndpoint
15
+ };
@@ -0,0 +1,2 @@
1
+
2
+ export { }