multicorn-shield 0.1.15 → 0.2.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,17 +1,51 @@
1
1
  #!/usr/bin/env node
2
- import { readFile, mkdir, writeFile } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { readFile, writeFile, mkdir, unlink } from 'fs/promises';
3
4
  import { join } from 'path';
4
5
  import { homedir } from 'os';
5
6
  import { createInterface } from 'readline';
6
7
  import { spawn } from 'child_process';
8
+ import { createHash } from 'crypto';
7
9
  import 'stream';
8
10
 
11
+ var style = {
12
+ violet: (s) => `\x1B[38;2;124;58;237m${s}\x1B[0m`,
13
+ violetLight: (s) => `\x1B[38;2;167;139;250m${s}\x1B[0m`,
14
+ green: (s) => `\x1B[38;2;34;197;94m${s}\x1B[0m`,
15
+ yellow: (s) => `\x1B[38;2;245;158;11m${s}\x1B[0m`,
16
+ red: (s) => `\x1B[38;2;239;68;68m${s}\x1B[0m`,
17
+ cyan: (s) => `\x1B[38;2;6;182;212m${s}\x1B[0m`,
18
+ bold: (s) => `\x1B[1m${s}\x1B[0m`,
19
+ dim: (s) => `\x1B[2m${s}\x1B[0m`
20
+ };
21
+ var BANNER = [
22
+ " \u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588 \u2588 \u2588\u2588\u2584 ",
23
+ " \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588",
24
+ " \u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588 \u2588\u2588 \u2588 \u2588 \u2588",
25
+ " \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588",
26
+ " \u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2580 "
27
+ ].map((line) => style.violet(line)).join("\n");
28
+ function withSpinner(message) {
29
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
30
+ let i = 0;
31
+ const interval = setInterval(() => {
32
+ const frame = frames[i % frames.length];
33
+ process.stderr.write(`\r${style.violet(frame ?? "\u280B")} ${message}`);
34
+ i++;
35
+ }, 80);
36
+ return {
37
+ stop(success, result) {
38
+ clearInterval(interval);
39
+ const icon = success ? style.green("\u2713") : style.red("\u2717");
40
+ process.stderr.write(`\r\x1B[2K${icon} ${result}
41
+ `);
42
+ }
43
+ };
44
+ }
9
45
  var CONFIG_DIR = join(homedir(), ".multicorn");
10
46
  var CONFIG_PATH = join(CONFIG_DIR, "config.json");
11
47
  var OPENCLAW_CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
12
- var OPENCLAW_ENOENT_MESSAGE = "OpenClaw config not found at ~/.openclaw/openclaw.json. If you're using OpenClaw, install it and then re-run 'npx multicorn-proxy init' to automatically configure your API key.\n";
13
- var OPENCLAW_PARSE_WARNING = "Multicorn Shield: Could not update ~/.openclaw/openclaw.json - please set MULTICORN_API_KEY manually.\n";
14
- var OPENCLAW_UPDATED_MESSAGE = "OpenClaw config updated at ~/.openclaw/openclaw.json\n";
48
+ var OPENCLAW_MIN_VERSION = "2026.2.26";
15
49
  async function loadConfig() {
16
50
  try {
17
51
  const raw = await readFile(CONFIG_PATH, "utf8");
@@ -32,14 +66,13 @@ async function saveConfig(config) {
32
66
  function isErrnoException(e) {
33
67
  return typeof e === "object" && e !== null && "code" in e;
34
68
  }
35
- async function updateOpenClawConfigIfPresent(apiKey, baseUrl) {
69
+ async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
36
70
  let raw;
37
71
  try {
38
72
  raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
39
73
  } catch (e) {
40
74
  if (isErrnoException(e) && e.code === "ENOENT") {
41
- process.stderr.write(OPENCLAW_ENOENT_MESSAGE);
42
- return;
75
+ return "not-found";
43
76
  }
44
77
  throw e;
45
78
  }
@@ -47,8 +80,7 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl) {
47
80
  try {
48
81
  obj = JSON.parse(raw);
49
82
  } catch {
50
- process.stderr.write(OPENCLAW_PARSE_WARNING);
51
- return;
83
+ return "parse-error";
52
84
  }
53
85
  let hooks = obj["hooks"];
54
86
  if (hooks === void 0 || typeof hooks !== "object") {
@@ -77,10 +109,66 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl) {
77
109
  }
78
110
  env["MULTICORN_API_KEY"] = apiKey;
79
111
  env["MULTICORN_BASE_URL"] = baseUrl;
112
+ if (agentName !== void 0) {
113
+ env["MULTICORN_AGENT_NAME"] = agentName;
114
+ const agentsList = obj["agents"];
115
+ const list = agentsList?.["list"];
116
+ if (Array.isArray(list) && list.length > 0) {
117
+ const first = list[0];
118
+ if (first["id"] !== agentName) {
119
+ first["id"] = agentName;
120
+ first["name"] = agentName;
121
+ }
122
+ } else {
123
+ if (agentsList !== void 0 && typeof agentsList === "object") {
124
+ agentsList["list"] = [{ id: agentName, name: agentName }];
125
+ } else {
126
+ obj["agents"] = { list: [{ id: agentName, name: agentName }] };
127
+ }
128
+ }
129
+ }
80
130
  await writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n", {
81
131
  encoding: "utf8"
82
132
  });
83
- process.stderr.write(OPENCLAW_UPDATED_MESSAGE);
133
+ return "updated";
134
+ }
135
+ async function detectOpenClaw() {
136
+ let raw;
137
+ try {
138
+ raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
139
+ } catch (e) {
140
+ if (isErrnoException(e) && e.code === "ENOENT") {
141
+ return { status: "not-found", version: null };
142
+ }
143
+ throw e;
144
+ }
145
+ let obj;
146
+ try {
147
+ obj = JSON.parse(raw);
148
+ } catch {
149
+ return { status: "parse-error", version: null };
150
+ }
151
+ const meta = obj["meta"];
152
+ if (typeof meta === "object" && meta !== null) {
153
+ const v = meta["lastTouchedVersion"];
154
+ if (typeof v === "string" && v.length > 0) {
155
+ return { status: "detected", version: v };
156
+ }
157
+ }
158
+ return { status: "detected", version: null };
159
+ }
160
+ function isVersionAtLeast(version, minimum) {
161
+ const vParts = version.split(".").map(Number);
162
+ const mParts = minimum.split(".").map(Number);
163
+ const len = Math.max(vParts.length, mParts.length);
164
+ for (let i = 0; i < len; i++) {
165
+ const v = vParts[i] ?? 0;
166
+ const m = mParts[i] ?? 0;
167
+ if (Number.isNaN(v) || Number.isNaN(m)) return false;
168
+ if (v > m) return true;
169
+ if (v < m) return false;
170
+ }
171
+ return true;
84
172
  }
85
173
  async function validateApiKey(apiKey, baseUrl) {
86
174
  try {
@@ -106,7 +194,116 @@ async function validateApiKey(apiKey, baseUrl) {
106
194
  };
107
195
  }
108
196
  }
197
+ function normalizeAgentName(raw) {
198
+ return raw.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
199
+ }
200
+ async function isOpenClawConnected() {
201
+ try {
202
+ const raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
203
+ const obj = JSON.parse(raw);
204
+ const hooks = obj["hooks"];
205
+ const internal = hooks?.["internal"];
206
+ const entries = internal?.["entries"];
207
+ const shield = entries?.["multicorn-shield"];
208
+ const env = shield?.["env"];
209
+ const key = env?.["MULTICORN_API_KEY"];
210
+ return typeof key === "string" && key.length > 0;
211
+ } catch {
212
+ return false;
213
+ }
214
+ }
215
+ function isClaudeCodeConnected() {
216
+ try {
217
+ return existsSync(join(homedir(), ".claude", "plugins", "cache", "multicorn-shield"));
218
+ } catch {
219
+ return false;
220
+ }
221
+ }
222
+ function getClaudeDesktopConfigPath() {
223
+ switch (process.platform) {
224
+ case "win32":
225
+ return join(
226
+ process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming"),
227
+ "Claude",
228
+ "claude_desktop_config.json"
229
+ );
230
+ case "linux":
231
+ return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
232
+ default:
233
+ return join(
234
+ homedir(),
235
+ "Library",
236
+ "Application Support",
237
+ "Claude",
238
+ "claude_desktop_config.json"
239
+ );
240
+ }
241
+ }
242
+ async function updateClaudeDesktopConfig(agentName, mcpServerCommand, overwrite = false) {
243
+ if (!/^[a-zA-Z0-9_-]+$/.test(agentName)) {
244
+ throw new Error("Agent name must contain only letters, numbers, hyphens, and underscores");
245
+ }
246
+ const configPath = getClaudeDesktopConfigPath();
247
+ let obj = {};
248
+ let fileExists = false;
249
+ try {
250
+ const raw = await readFile(configPath, "utf8");
251
+ fileExists = true;
252
+ try {
253
+ obj = JSON.parse(raw);
254
+ } catch {
255
+ return "parse-error";
256
+ }
257
+ } catch (e) {
258
+ if (isErrnoException(e) && e.code === "ENOENT") {
259
+ fileExists = false;
260
+ } else {
261
+ throw e;
262
+ }
263
+ }
264
+ let mcpServers = obj["mcpServers"];
265
+ if (mcpServers === void 0 || typeof mcpServers !== "object") {
266
+ mcpServers = {};
267
+ obj["mcpServers"] = mcpServers;
268
+ }
269
+ if (mcpServers[agentName] !== void 0 && !overwrite) {
270
+ return "skipped";
271
+ }
272
+ const commandParts = mcpServerCommand.trim().split(/\s+/);
273
+ mcpServers[agentName] = {
274
+ command: "npx",
275
+ args: ["multicorn-proxy", "--wrap", ...commandParts, "--agent-name", agentName]
276
+ };
277
+ const configDir = join(configPath, "..");
278
+ if (!fileExists) {
279
+ await mkdir(configDir, { recursive: true });
280
+ }
281
+ await writeFile(configPath, JSON.stringify(obj, null, 2) + "\n", { encoding: "utf8" });
282
+ return fileExists ? "updated" : "created";
283
+ }
284
+ async function isClaudeDesktopConnected() {
285
+ try {
286
+ const raw = await readFile(getClaudeDesktopConfigPath(), "utf8");
287
+ const obj = JSON.parse(raw);
288
+ const mcpServers = obj["mcpServers"];
289
+ if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
290
+ for (const entry of Object.values(mcpServers)) {
291
+ if (typeof entry !== "object" || entry === null) continue;
292
+ const args = entry["args"];
293
+ if (Array.isArray(args) && args.includes("multicorn-proxy")) return true;
294
+ }
295
+ return false;
296
+ } catch {
297
+ return false;
298
+ }
299
+ }
109
300
  async function runInit(baseUrl = "https://api.multicorn.ai") {
301
+ if (!process.stdin.isTTY) {
302
+ process.stderr.write(
303
+ style.red("Error: interactive terminal required. Cannot run init with piped input.") + "\n"
304
+ );
305
+ process.exit(1);
306
+ }
110
307
  const rl = createInterface({ input: process.stdin, output: process.stderr });
111
308
  function ask(question) {
112
309
  return new Promise((resolve) => {
@@ -115,39 +312,332 @@ async function runInit(baseUrl = "https://api.multicorn.ai") {
115
312
  });
116
313
  });
117
314
  }
118
- process.stderr.write("Multicorn Shield proxy setup\n\n");
119
- process.stderr.write("Get your API key at https://app.multicorn.ai/settings/api-keys\n\n");
120
- let config = null;
121
- while (config === null) {
315
+ process.stderr.write("\n" + BANNER + "\n");
316
+ process.stderr.write(style.dim("Agent governance for the AI era") + "\n\n");
317
+ process.stderr.write(style.bold(style.violet("Multicorn Shield proxy setup")) + "\n\n");
318
+ process.stderr.write(
319
+ style.dim("Get your API key at https://app.multicorn.ai/settings/api-keys") + "\n\n"
320
+ );
321
+ let apiKey = "";
322
+ const existing = await loadConfig().catch(() => null);
323
+ if (existing !== null && existing.apiKey.startsWith("mcs_") && existing.apiKey.length >= 8) {
324
+ const masked = "mcs_..." + existing.apiKey.slice(-4);
325
+ process.stderr.write("Found existing API key: " + style.cyan(masked) + "\n");
326
+ const answer = await ask("Use this key? (Y/n) ");
327
+ if (answer.trim().toLowerCase() !== "n") {
328
+ apiKey = existing.apiKey;
329
+ if (baseUrl === "https://api.multicorn.ai") {
330
+ baseUrl = existing.baseUrl;
331
+ }
332
+ }
333
+ }
334
+ while (apiKey.length === 0) {
122
335
  const input = await ask("API key (starts with mcs_): ");
123
- const apiKey = input.trim();
124
- if (apiKey.length === 0) {
125
- process.stderr.write("API key is required.\n");
336
+ const key = input.trim();
337
+ if (key.length === 0) {
338
+ process.stderr.write(style.red("API key is required.") + "\n");
126
339
  continue;
127
340
  }
128
- process.stderr.write("Validating key...\n");
129
- const result = await validateApiKey(apiKey, baseUrl);
341
+ const spinner = withSpinner("Validating key...");
342
+ let result;
343
+ try {
344
+ result = await validateApiKey(key, baseUrl);
345
+ } catch (error) {
346
+ spinner.stop(false, "Validation failed");
347
+ throw error;
348
+ }
130
349
  if (!result.valid) {
131
- process.stderr.write(`${result.error ?? "Validation failed. Try again."}
132
- `);
350
+ spinner.stop(false, result.error ?? "Validation failed. Try again.");
133
351
  continue;
134
352
  }
135
- config = { apiKey, baseUrl };
353
+ spinner.stop(true, "Key validated");
354
+ apiKey = key;
136
355
  }
137
- rl.close();
138
- await saveConfig(config);
139
- try {
140
- await updateOpenClawConfigIfPresent(config.apiKey, config.baseUrl);
141
- } catch {
356
+ const configuredPlatforms = /* @__PURE__ */ new Set();
357
+ let lastConfig = { apiKey, baseUrl };
358
+ let configuring = true;
359
+ while (configuring) {
142
360
  process.stderr.write(
143
- "Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json if you use OpenClaw.\n"
361
+ "\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n"
144
362
  );
363
+ const platformLabels = ["OpenClaw", "Claude Code", "Claude Desktop", "Other MCP Agent"];
364
+ const openClawConnected = await isOpenClawConnected();
365
+ const claudeCodeConnected = isClaudeCodeConnected();
366
+ const claudeDesktopConnected = await isClaudeDesktopConnected();
367
+ for (let i = 0; i < platformLabels.length; i++) {
368
+ const sessionMarker = configuredPlatforms.has(i + 1) ? " " + style.green("\u2713") : "";
369
+ let connectedMarker = "";
370
+ if (!configuredPlatforms.has(i + 1)) {
371
+ if (i === 0 && openClawConnected) {
372
+ connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
373
+ } else if (i === 1 && claudeCodeConnected) {
374
+ connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
375
+ } else if (i === 2 && claudeDesktopConnected) {
376
+ connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
377
+ }
378
+ }
379
+ process.stderr.write(
380
+ ` ${style.violet(String(i + 1))}. ${platformLabels[i] ?? ""}${sessionMarker}${connectedMarker}
381
+ `
382
+ );
383
+ }
384
+ let selection = 0;
385
+ while (selection === 0) {
386
+ const input = await ask("Select (1-4): ");
387
+ const num = parseInt(input.trim(), 10);
388
+ if (num >= 1 && num <= 4) {
389
+ selection = num;
390
+ }
391
+ }
392
+ let agentName = "";
393
+ while (agentName.length === 0) {
394
+ const input = await ask("\nWhat would you like to call this agent? ");
395
+ if (input.trim().length === 0) continue;
396
+ const transformed = normalizeAgentName(input);
397
+ if (transformed.length === 0) {
398
+ process.stderr.write(
399
+ style.red("Agent name must contain letters or numbers. Please try again.") + "\n"
400
+ );
401
+ continue;
402
+ }
403
+ if (transformed !== input.trim()) {
404
+ process.stderr.write(style.yellow("Agent name set to: ") + style.cyan(transformed) + "\n");
405
+ }
406
+ agentName = transformed;
407
+ }
408
+ if (selection === 1) {
409
+ let detection;
410
+ try {
411
+ detection = await detectOpenClaw();
412
+ } catch (error) {
413
+ const detail = error instanceof Error ? error.message : String(error);
414
+ process.stderr.write(style.red("\u2717") + ` Failed to read OpenClaw config: ${detail}
415
+ `);
416
+ rl.close();
417
+ return null;
418
+ }
419
+ if (detection.status === "not-found") {
420
+ process.stderr.write(
421
+ style.red("\u2717") + " OpenClaw is not installed. Install OpenClaw first, then run npx multicorn-proxy init again.\n"
422
+ );
423
+ rl.close();
424
+ return null;
425
+ }
426
+ if (detection.status === "parse-error") {
427
+ process.stderr.write(
428
+ style.red("\u2717") + " Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually.\n"
429
+ );
430
+ }
431
+ if (detection.status === "detected") {
432
+ if (detection.version !== null) {
433
+ process.stderr.write(
434
+ style.green("\u2713") + ` OpenClaw detected ${style.dim(`(${detection.version})`)}
435
+ `
436
+ );
437
+ if (isVersionAtLeast(detection.version, OPENCLAW_MIN_VERSION)) {
438
+ process.stderr.write(
439
+ style.green("\u2713") + " " + style.green("Version compatible") + "\n"
440
+ );
441
+ } else {
442
+ process.stderr.write(
443
+ style.yellow("\u26A0") + ` Shield has been tested with OpenClaw ${style.cyan(OPENCLAW_MIN_VERSION)} and above. Your version (${detection.version}) may work but is untested. We recommend upgrading to at least ${style.cyan(OPENCLAW_MIN_VERSION)}.
444
+ `
445
+ );
446
+ const answer = await ask("Continue anyway? (y/N) ");
447
+ if (answer.trim().toLowerCase() !== "y") {
448
+ rl.close();
449
+ return null;
450
+ }
451
+ }
452
+ } else {
453
+ process.stderr.write(
454
+ style.yellow("\u26A0") + " Could not detect OpenClaw version. Continuing anyway.\n"
455
+ );
456
+ }
457
+ const spinner = withSpinner("Updating OpenClaw config...");
458
+ try {
459
+ const result = await updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName);
460
+ if (result === "not-found") {
461
+ spinner.stop(false, "OpenClaw config disappeared unexpectedly.");
462
+ rl.close();
463
+ return null;
464
+ }
465
+ if (result === "parse-error") {
466
+ spinner.stop(
467
+ false,
468
+ "Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually."
469
+ );
470
+ } else {
471
+ spinner.stop(
472
+ true,
473
+ "OpenClaw config updated at " + style.cyan("~/.openclaw/openclaw.json")
474
+ );
475
+ }
476
+ } catch (error) {
477
+ const detail = error instanceof Error ? error.message : String(error);
478
+ spinner.stop(false, `Failed to update OpenClaw config: ${detail}`);
479
+ }
480
+ }
481
+ } else if (selection === 2) {
482
+ process.stderr.write("\nTo connect Claude Code to Shield:\n\n");
483
+ process.stderr.write(
484
+ " " + style.bold("Step 1") + " - Add the Multicorn marketplace:\n " + style.cyan("claude plugin marketplace add Multicorn-AI/multicorn-shield") + "\n\n"
485
+ );
486
+ process.stderr.write(
487
+ " " + style.bold("Step 2") + " - Install the plugin:\n " + style.cyan("claude plugin install multicorn-shield@multicorn-shield") + "\n\n"
488
+ );
489
+ process.stderr.write(
490
+ " " + style.bold("Step 3") + " - Start Claude Code:\n " + style.cyan("claude") + "\n\n"
491
+ );
492
+ process.stderr.write(
493
+ style.dim("Run /plugin inside Claude Code to confirm multicorn-shield is installed.") + "\n"
494
+ );
495
+ process.stderr.write(
496
+ style.dim("Requires Claude Code to be installed. Get it at https://code.claude.com") + "\n"
497
+ );
498
+ } else if (selection === 3) {
499
+ const mcpCommand = await ask(
500
+ "\nWhat MCP server should Shield govern for this agent?\nThis is the command you'd normally use to start your MCP server.\nExample: npx -y @modelcontextprotocol/server-filesystem /tmp\nLeave blank to skip and configure later: "
501
+ );
502
+ if (mcpCommand.trim().length === 0) {
503
+ const configPath = getClaudeDesktopConfigPath();
504
+ process.stderr.write("\n" + style.dim("Add this to your Claude Desktop config at:") + "\n");
505
+ process.stderr.write(" " + style.cyan(configPath) + "\n\n");
506
+ const snippet = JSON.stringify(
507
+ {
508
+ mcpServers: {
509
+ [agentName]: {
510
+ command: "npx",
511
+ args: [
512
+ "multicorn-proxy",
513
+ "--wrap",
514
+ "<your-mcp-server-command>",
515
+ "--agent-name",
516
+ agentName
517
+ ]
518
+ }
519
+ }
520
+ },
521
+ null,
522
+ 2
523
+ );
524
+ process.stderr.write(style.cyan(snippet) + "\n\n");
525
+ } else {
526
+ let shouldWrite = true;
527
+ const spinner = withSpinner("Updating Claude Desktop config...");
528
+ try {
529
+ let result = await updateClaudeDesktopConfig(agentName, mcpCommand.trim());
530
+ if (result === "skipped") {
531
+ spinner.stop(false, `Agent "${agentName}" already exists in Claude Desktop config.`);
532
+ const overwrite = await ask("Overwrite the existing entry? (y/N) ");
533
+ if (overwrite.trim().toLowerCase() === "y") {
534
+ const retrySpinner = withSpinner("Updating Claude Desktop config...");
535
+ result = await updateClaudeDesktopConfig(agentName, mcpCommand.trim(), true);
536
+ retrySpinner.stop(
537
+ true,
538
+ "Claude Desktop config updated at " + style.cyan(getClaudeDesktopConfigPath())
539
+ );
540
+ } else {
541
+ shouldWrite = false;
542
+ process.stderr.write(style.dim("Skipped. Existing config left unchanged.") + "\n");
543
+ }
544
+ } else if (result === "parse-error") {
545
+ spinner.stop(false, "Claude Desktop config file contains invalid JSON.");
546
+ const configPath = getClaudeDesktopConfigPath();
547
+ process.stderr.write(
548
+ style.yellow("\u26A0") + " Fix the JSON in " + style.cyan(configPath) + " or add this entry manually:\n\n"
549
+ );
550
+ const snippet = JSON.stringify(
551
+ {
552
+ mcpServers: {
553
+ [agentName]: {
554
+ command: "npx",
555
+ args: [
556
+ "multicorn-proxy",
557
+ "--wrap",
558
+ ...mcpCommand.trim().split(/\s+/),
559
+ "--agent-name",
560
+ agentName
561
+ ]
562
+ }
563
+ }
564
+ },
565
+ null,
566
+ 2
567
+ );
568
+ process.stderr.write(style.cyan(snippet) + "\n\n");
569
+ } else {
570
+ const verb = result === "created" ? "Created" : "Updated";
571
+ spinner.stop(
572
+ true,
573
+ verb + " Claude Desktop config at " + style.cyan(getClaudeDesktopConfigPath())
574
+ );
575
+ process.stderr.write(style.dim("Restart Claude Desktop to pick up changes.") + "\n");
576
+ }
577
+ } catch (error) {
578
+ const detail = error instanceof Error ? error.message : String(error);
579
+ spinner.stop(false, `Failed to update Claude Desktop config: ${detail}`);
580
+ shouldWrite = false;
581
+ }
582
+ }
583
+ } else {
584
+ process.stderr.write("\n" + style.dim("Start the Shield proxy with:") + "\n");
585
+ process.stderr.write(
586
+ " " + style.cyan(
587
+ `npx multicorn-proxy --wrap <your-mcp-server-command> --agent-name ${agentName}`
588
+ ) + "\n\n"
589
+ );
590
+ }
591
+ configuredPlatforms.add(selection);
592
+ lastConfig = { apiKey, baseUrl, agentName };
593
+ try {
594
+ await saveConfig(lastConfig);
595
+ process.stderr.write(style.green("\u2713") + ` Config saved to ${style.cyan(CONFIG_PATH)}
596
+ `);
597
+ } catch (error) {
598
+ const detail = error instanceof Error ? error.message : String(error);
599
+ process.stderr.write(style.red(`Failed to save config: ${detail}`) + "\n");
600
+ }
601
+ if (configuredPlatforms.size >= 4) {
602
+ configuring = false;
603
+ continue;
604
+ }
605
+ const another = await ask("\nWould you like to configure another agent? (y/N) ");
606
+ if (another.trim().toLowerCase() !== "y") {
607
+ configuring = false;
608
+ }
145
609
  }
146
- process.stderr.write(`
147
- Config saved to ${CONFIG_PATH}
610
+ rl.close();
611
+ process.stderr.write("\n" + style.bold(style.violet("Setup complete")) + "\n\n");
612
+ const allPlatforms = ["OpenClaw", "Claude Code", "Claude Desktop", "Other MCP Agent"];
613
+ for (const idx of configuredPlatforms) {
614
+ process.stderr.write(` ${style.green("\u2713")} ${allPlatforms[idx - 1] ?? ""}
148
615
  `);
149
- process.stderr.write("Run your agent with: npx multicorn-proxy --wrap <your-mcp-server>\n");
150
- return config;
616
+ }
617
+ process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
618
+ const blocks = [];
619
+ if (configuredPlatforms.has(1)) {
620
+ blocks.push(
621
+ "\n" + style.bold("To complete your OpenClaw setup:") + "\n \u2192 Restart your gateway: " + style.cyan("openclaw gateway restart") + "\n \u2192 Start a session: " + style.cyan("openclaw tui") + "\n"
622
+ );
623
+ }
624
+ if (configuredPlatforms.has(2)) {
625
+ blocks.push(
626
+ "\n" + style.bold("To complete your Claude Code setup:") + "\n \u2192 Add marketplace: " + style.cyan("claude plugin marketplace add Multicorn-AI/multicorn-shield") + "\n \u2192 Install plugin: " + style.cyan("claude plugin install multicorn-shield@multicorn-shield") + "\n"
627
+ );
628
+ }
629
+ if (configuredPlatforms.has(3)) {
630
+ blocks.push(
631
+ "\n" + style.bold("To complete your Claude Desktop setup:") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
632
+ );
633
+ }
634
+ if (configuredPlatforms.has(4)) {
635
+ blocks.push(
636
+ "\n" + style.bold("To complete your Other MCP Agent setup:") + "\n \u2192 Start your agent with: " + style.cyan("npx multicorn-proxy --wrap <your-server> --agent-name <name>") + "\n"
637
+ );
638
+ }
639
+ process.stderr.write(blocks.join("") + "\n");
640
+ return lastConfig;
151
641
  }
152
642
  function isProxyConfig(value) {
153
643
  if (typeof value !== "object" || value === null) return false;
@@ -610,51 +1100,53 @@ function capitalize(str) {
610
1100
  }
611
1101
  var MULTICORN_DIR = join(homedir(), ".multicorn");
612
1102
  var SCOPES_PATH = join(MULTICORN_DIR, "scopes.json");
613
- var CONSENT_POLL_INTERVAL_MS = 3e3;
614
- var CONSENT_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
615
- function deriveDashboardUrl(baseUrl) {
1103
+ var CACHE_META_PATH = join(MULTICORN_DIR, "cache-meta.json");
1104
+ function cacheKey(agentName, apiKey) {
1105
+ return createHash("sha256").update(`${agentName}:${apiKey}`).digest("hex").slice(0, 16);
1106
+ }
1107
+ async function ensureCacheIdentity(apiKey) {
1108
+ const currentHash = createHash("sha256").update(apiKey).digest("hex");
1109
+ let storedHash = null;
616
1110
  try {
617
- const url = new URL(baseUrl);
618
- if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
619
- url.port = "5173";
620
- url.protocol = "http:";
621
- return url.toString();
622
- }
623
- if (url.hostname === "api.multicorn.ai") {
624
- url.hostname = "app.multicorn.ai";
625
- return url.toString();
626
- }
627
- if (url.hostname.includes("api")) {
628
- url.hostname = url.hostname.replace("api", "app");
629
- return url.toString();
1111
+ const raw = await readFile(CACHE_META_PATH, "utf8");
1112
+ const meta = JSON.parse(raw);
1113
+ if (typeof meta === "object" && meta !== null && "apiKeyHash" in meta) {
1114
+ storedHash = meta.apiKeyHash;
630
1115
  }
631
- if (url.protocol === "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
632
- return "https://app.multicorn.ai";
633
- }
634
- return "https://app.multicorn.ai";
635
1116
  } catch {
636
- return "https://app.multicorn.ai";
637
1117
  }
638
- }
639
- var ShieldAuthError = class _ShieldAuthError extends Error {
640
- constructor(message) {
641
- super(message);
642
- this.name = "ShieldAuthError";
643
- Object.setPrototypeOf(this, _ShieldAuthError.prototype);
1118
+ if (storedHash === null || storedHash !== currentHash) {
1119
+ try {
1120
+ await unlink(SCOPES_PATH);
1121
+ } catch {
1122
+ }
644
1123
  }
645
- };
646
- async function loadCachedScopes(agentName) {
1124
+ if (storedHash !== currentHash) {
1125
+ await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
1126
+ await writeFile(CACHE_META_PATH, JSON.stringify({ apiKeyHash: currentHash }, null, 2) + "\n", {
1127
+ encoding: "utf8",
1128
+ mode: 384
1129
+ });
1130
+ }
1131
+ }
1132
+ async function loadCachedScopes(agentName, apiKey) {
1133
+ if (apiKey.length === 0) return null;
1134
+ await ensureCacheIdentity(apiKey);
1135
+ const key = cacheKey(agentName, apiKey);
647
1136
  try {
648
1137
  const raw = await readFile(SCOPES_PATH, "utf8");
649
1138
  const parsed = JSON.parse(raw);
650
1139
  if (!isScopesCacheFile(parsed)) return null;
651
- const entry = parsed[agentName];
1140
+ const entry = parsed[key];
652
1141
  return entry?.scopes ?? null;
653
1142
  } catch {
654
1143
  return null;
655
1144
  }
656
1145
  }
657
- async function saveCachedScopes(agentName, agentId, scopes) {
1146
+ async function saveCachedScopes(agentName, agentId, scopes, apiKey) {
1147
+ if (apiKey.length === 0) return;
1148
+ await ensureCacheIdentity(apiKey);
1149
+ const key = cacheKey(agentName, apiKey);
658
1150
  await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
659
1151
  let existing = {};
660
1152
  try {
@@ -665,7 +1157,7 @@ async function saveCachedScopes(agentName, agentId, scopes) {
665
1157
  }
666
1158
  const updated = {
667
1159
  ...existing,
668
- [agentName]: {
1160
+ [key]: {
669
1161
  agentId,
670
1162
  scopes,
671
1163
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -676,6 +1168,44 @@ async function saveCachedScopes(agentName, agentId, scopes) {
676
1168
  mode: 384
677
1169
  });
678
1170
  }
1171
+ function isScopesCacheFile(value) {
1172
+ return typeof value === "object" && value !== null;
1173
+ }
1174
+
1175
+ // src/proxy/consent.ts
1176
+ var CONSENT_POLL_INTERVAL_MS = 3e3;
1177
+ var CONSENT_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
1178
+ function deriveDashboardUrl(baseUrl) {
1179
+ try {
1180
+ const url = new URL(baseUrl);
1181
+ if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
1182
+ url.port = "5173";
1183
+ url.protocol = "http:";
1184
+ return url.toString();
1185
+ }
1186
+ if (url.hostname === "api.multicorn.ai") {
1187
+ url.hostname = "app.multicorn.ai";
1188
+ return url.toString();
1189
+ }
1190
+ if (url.hostname.includes("api")) {
1191
+ url.hostname = url.hostname.replace("api", "app");
1192
+ return url.toString();
1193
+ }
1194
+ if (url.protocol === "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
1195
+ return "https://app.multicorn.ai";
1196
+ }
1197
+ return "https://app.multicorn.ai";
1198
+ } catch {
1199
+ return "https://app.multicorn.ai";
1200
+ }
1201
+ }
1202
+ var ShieldAuthError = class _ShieldAuthError extends Error {
1203
+ constructor(message) {
1204
+ super(message);
1205
+ this.name = "ShieldAuthError";
1206
+ Object.setPrototypeOf(this, _ShieldAuthError.prototype);
1207
+ }
1208
+ };
679
1209
  async function findAgentByName(agentName, apiKey, baseUrl) {
680
1210
  let response;
681
1211
  try {
@@ -796,7 +1326,7 @@ Waiting for you to grant access in the Multicorn dashboard...
796
1326
  );
797
1327
  }
798
1328
  async function resolveAgentRecord(agentName, apiKey, baseUrl, logger) {
799
- const cachedScopes = await loadCachedScopes(agentName);
1329
+ const cachedScopes = await loadCachedScopes(agentName, apiKey);
800
1330
  if (cachedScopes !== null && cachedScopes.length > 0) {
801
1331
  logger.debug("Loaded scopes from cache.", { agent: agentName, count: cachedScopes.length });
802
1332
  return { id: "", name: agentName, scopes: cachedScopes };
@@ -824,7 +1354,7 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger) {
824
1354
  }
825
1355
  const scopes = await fetchGrantedScopes(agent.id, apiKey, baseUrl);
826
1356
  if (scopes.length > 0) {
827
- await saveCachedScopes(agentName, agent.id, scopes);
1357
+ await saveCachedScopes(agentName, agent.id, scopes, apiKey);
828
1358
  }
829
1359
  return { ...agent, scopes };
830
1360
  }
@@ -862,9 +1392,6 @@ function isPermissionShape(value) {
862
1392
  const obj = value;
863
1393
  return typeof obj["service"] === "string" && typeof obj["read"] === "boolean" && typeof obj["write"] === "boolean" && typeof obj["execute"] === "boolean" && (obj["revoked_at"] === null || typeof obj["revoked_at"] === "string");
864
1394
  }
865
- function isScopesCacheFile(value) {
866
- return typeof value === "object" && value !== null;
867
- }
868
1395
 
869
1396
  // src/proxy/index.ts
870
1397
  var DEFAULT_SCOPE_REFRESH_INTERVAL_MS = 6e4;
@@ -892,7 +1419,7 @@ function createProxyServer(config) {
892
1419
  const scopes = await fetchGrantedScopes(agentId, config.apiKey, config.baseUrl);
893
1420
  grantedScopes = scopes;
894
1421
  if (scopes.length > 0) {
895
- await saveCachedScopes(config.agentName, agentId, scopes);
1422
+ await saveCachedScopes(config.agentName, agentId, scopes, config.apiKey);
896
1423
  }
897
1424
  config.logger.debug("Scopes refreshed.", { count: scopes.length });
898
1425
  } catch (error) {
@@ -921,7 +1448,7 @@ function createProxyServer(config) {
921
1448
  scopeParam
922
1449
  );
923
1450
  grantedScopes = scopes;
924
- await saveCachedScopes(config.agentName, agentId, scopes);
1451
+ await saveCachedScopes(config.agentName, agentId, scopes, config.apiKey);
925
1452
  } finally {
926
1453
  consentInProgress = false;
927
1454
  }
@@ -1286,7 +1813,7 @@ Use https:// or http://localhost for local development.
1286
1813
  );
1287
1814
  process.exit(1);
1288
1815
  }
1289
- const agentName = cli.agentName.length > 0 ? cli.agentName : deriveAgentName(cli.wrapCommand);
1816
+ const agentName = cli.agentName.length > 0 ? cli.agentName : config.agentName !== void 0 && config.agentName.length > 0 ? config.agentName : deriveAgentName(cli.wrapCommand);
1290
1817
  const finalBaseUrl = cli.baseUrl !== "https://api.multicorn.ai" ? cli.baseUrl : config.baseUrl;
1291
1818
  const finalDashboardUrl = cli.dashboardUrl !== "" ? cli.dashboardUrl : deriveDashboardUrl(finalBaseUrl);
1292
1819
  const proxy = createProxyServer({