multicorn-shield 0.4.0 → 0.6.1

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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ![Multicorn Shield](readme-header.svg)
1
+ ![Multicorn Shield](https://multicorn.ai/images/og-image-shield.png)
2
2
 
3
3
  # Multicorn Shield
4
4
 
@@ -38,6 +38,8 @@ npm install -g multicorn-shield
38
38
  npx multicorn-proxy init
39
39
  ```
40
40
 
41
+ The init wizard supports multiple agents. Run it again to add agents on different platforms (OpenClaw, Claude Code, Cursor) without losing existing config. Use `npx multicorn-proxy agents` to see configured agents.
42
+
41
43
  **Step 3: Wrap your MCP server**
42
44
 
43
45
  ```bash
@@ -194,7 +196,7 @@ With the dashboard you can:
194
196
  The dashboard works with both the SDK integration and the MCP proxy. No extra setup needed.
195
197
 
196
198
  <p align="center">
197
- <img src="https://multicorn.ai/images/screenshots/overview-page.png" alt="Dashboard overview showing total actions, blocked count, spend, and live activity feed" width="800" />
199
+ <img src="https://multicorn.ai/images/og-image-shield.png" alt="Dashboard overview showing total actions, blocked count, spend, and live activity feed" width="800" />
198
200
  </p>
199
201
 
200
202
  <p align="center">
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync } from 'fs';
3
- import { readFile, mkdir, writeFile, unlink } from 'fs/promises';
3
+ import { readFile, writeFile, mkdir, unlink } from 'fs/promises';
4
4
  import { join } from 'path';
5
5
  import { homedir } from 'os';
6
6
  import { createInterface } from 'readline';
@@ -52,21 +52,99 @@ function stripAnsi(str) {
52
52
  function normalizeAgentName(raw) {
53
53
  return raw.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
54
54
  }
55
+ function isErrnoException(e) {
56
+ return typeof e === "object" && e !== null && "code" in e;
57
+ }
55
58
  function isProxyConfig(value) {
56
59
  if (typeof value !== "object" || value === null) return false;
57
60
  const obj = value;
58
61
  return typeof obj["apiKey"] === "string" && typeof obj["baseUrl"] === "string";
59
62
  }
63
+ function isAgentEntry(value) {
64
+ if (typeof value !== "object" || value === null) return false;
65
+ const o = value;
66
+ return typeof o["name"] === "string" && typeof o["platform"] === "string";
67
+ }
68
+ function getAgentByPlatform(config, platform) {
69
+ const list = config.agents;
70
+ if (list === void 0 || list.length === 0) return void 0;
71
+ return list.find((a) => a.platform === platform);
72
+ }
73
+ function getDefaultAgent(config) {
74
+ const list = config.agents;
75
+ if (list === void 0 || list.length === 0) return void 0;
76
+ const defName = config.defaultAgent;
77
+ if (typeof defName === "string" && defName.length > 0) {
78
+ const match = list.find((a) => a.name === defName);
79
+ if (match !== void 0) return match;
80
+ }
81
+ return list[0];
82
+ }
83
+ function collectAgentsFromConfig(cfg) {
84
+ if (cfg === null) return [];
85
+ if (cfg.agents !== void 0 && cfg.agents.length > 0) {
86
+ return cfg.agents.map((a) => ({ name: a.name, platform: a.platform }));
87
+ }
88
+ const raw = cfg;
89
+ const legacyName = raw["agentName"];
90
+ const legacyPlatform = raw["platform"];
91
+ if (typeof legacyName === "string" && legacyName.length > 0) {
92
+ const plat = typeof legacyPlatform === "string" && legacyPlatform.length > 0 ? legacyPlatform : "unknown";
93
+ return [{ name: legacyName, platform: plat }];
94
+ }
95
+ return [];
96
+ }
60
97
  async function loadConfig() {
61
98
  try {
62
99
  const raw = await readFile(CONFIG_PATH, "utf8");
63
100
  const parsed = JSON.parse(raw);
64
101
  if (!isProxyConfig(parsed)) return null;
65
- return parsed;
102
+ const obj = parsed;
103
+ const agentNameRaw = obj["agentName"];
104
+ const agentsRaw = obj["agents"];
105
+ const hasNonEmptyAgents = Array.isArray(agentsRaw) && agentsRaw.length > 0 && agentsRaw.every((e) => isAgentEntry(e));
106
+ const needsMigrate = typeof agentNameRaw === "string" && agentNameRaw.length > 0 && !hasNonEmptyAgents;
107
+ if (!needsMigrate) {
108
+ return parsed;
109
+ }
110
+ const platform = typeof obj["platform"] === "string" && obj["platform"].length > 0 ? obj["platform"] : "unknown";
111
+ const next = { ...obj };
112
+ delete next["agentName"];
113
+ delete next["platform"];
114
+ next["agents"] = [{ name: agentNameRaw, platform }];
115
+ next["defaultAgent"] = agentNameRaw;
116
+ const migrated = next;
117
+ await saveConfig(migrated);
118
+ return migrated;
66
119
  } catch {
67
120
  return null;
68
121
  }
69
122
  }
123
+ async function deleteAgentByName(name) {
124
+ const config = await loadConfig();
125
+ if (config === null) return false;
126
+ const agents = collectAgentsFromConfig(config);
127
+ const idx = agents.findIndex((a) => a.name === name);
128
+ if (idx === -1) return false;
129
+ const nextAgents = agents.filter((_, i) => i !== idx);
130
+ let defaultAgent = config.defaultAgent;
131
+ if (defaultAgent === name) {
132
+ defaultAgent = void 0;
133
+ }
134
+ const raw = { ...config };
135
+ if (nextAgents.length > 0) {
136
+ raw["agents"] = nextAgents;
137
+ } else {
138
+ delete raw["agents"];
139
+ }
140
+ if (defaultAgent !== void 0 && defaultAgent.length > 0) {
141
+ raw["defaultAgent"] = defaultAgent;
142
+ } else {
143
+ delete raw["defaultAgent"];
144
+ }
145
+ await saveConfig(raw);
146
+ return true;
147
+ }
70
148
  async function saveConfig(config) {
71
149
  await mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
72
150
  await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
@@ -74,6 +152,111 @@ async function saveConfig(config) {
74
152
  mode: 384
75
153
  });
76
154
  }
155
+ var OPENCLAW_MIN_VERSION = "2026.2.26";
156
+ async function detectOpenClaw() {
157
+ let raw;
158
+ try {
159
+ raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
160
+ } catch (e) {
161
+ if (isErrnoException(e) && e.code === "ENOENT") {
162
+ return { status: "not-found", version: null };
163
+ }
164
+ throw e;
165
+ }
166
+ let obj;
167
+ try {
168
+ obj = JSON.parse(raw);
169
+ } catch {
170
+ return { status: "parse-error", version: null };
171
+ }
172
+ const meta = obj["meta"];
173
+ if (typeof meta === "object" && meta !== null) {
174
+ const v = meta["lastTouchedVersion"];
175
+ if (typeof v === "string" && v.length > 0) {
176
+ return { status: "detected", version: v };
177
+ }
178
+ }
179
+ return { status: "detected", version: null };
180
+ }
181
+ function isVersionAtLeast(version, minimum) {
182
+ const vParts = version.split(".").map(Number);
183
+ const mParts = minimum.split(".").map(Number);
184
+ const len = Math.max(vParts.length, mParts.length);
185
+ for (let i = 0; i < len; i++) {
186
+ const v = vParts[i] ?? 0;
187
+ const m = mParts[i] ?? 0;
188
+ if (Number.isNaN(v) || Number.isNaN(m)) return false;
189
+ if (v > m) return true;
190
+ if (v < m) return false;
191
+ }
192
+ return true;
193
+ }
194
+ async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
195
+ let raw;
196
+ try {
197
+ raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
198
+ } catch (e) {
199
+ if (isErrnoException(e) && e.code === "ENOENT") {
200
+ return "not-found";
201
+ }
202
+ throw e;
203
+ }
204
+ let obj;
205
+ try {
206
+ obj = JSON.parse(raw);
207
+ } catch {
208
+ return "parse-error";
209
+ }
210
+ let hooks = obj["hooks"];
211
+ if (hooks === void 0 || typeof hooks !== "object") {
212
+ hooks = {};
213
+ obj["hooks"] = hooks;
214
+ }
215
+ let internal = hooks["internal"];
216
+ if (internal === void 0 || typeof internal !== "object") {
217
+ internal = { enabled: true, entries: {} };
218
+ hooks["internal"] = internal;
219
+ }
220
+ let entries = internal["entries"];
221
+ if (entries === void 0 || typeof entries !== "object") {
222
+ entries = {};
223
+ internal["entries"] = entries;
224
+ }
225
+ let shield = entries["multicorn-shield"];
226
+ if (shield === void 0 || typeof shield !== "object") {
227
+ shield = { enabled: true, env: {} };
228
+ entries["multicorn-shield"] = shield;
229
+ }
230
+ let env = shield["env"];
231
+ if (env === void 0 || typeof env !== "object") {
232
+ env = {};
233
+ shield["env"] = env;
234
+ }
235
+ env["MULTICORN_API_KEY"] = apiKey;
236
+ env["MULTICORN_BASE_URL"] = baseUrl;
237
+ if (agentName !== void 0) {
238
+ env["MULTICORN_AGENT_NAME"] = agentName;
239
+ const agentsList = obj["agents"];
240
+ const list = agentsList?.["list"];
241
+ if (Array.isArray(list) && list.length > 0) {
242
+ const first = list[0];
243
+ if (first["id"] !== agentName) {
244
+ first["id"] = agentName;
245
+ first["name"] = agentName;
246
+ }
247
+ } else {
248
+ if (agentsList !== void 0 && typeof agentsList === "object") {
249
+ agentsList["list"] = [{ id: agentName, name: agentName }];
250
+ } else {
251
+ obj["agents"] = { list: [{ id: agentName, name: agentName }] };
252
+ }
253
+ }
254
+ }
255
+ await writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n", {
256
+ encoding: "utf8"
257
+ });
258
+ return "updated";
259
+ }
77
260
  async function validateApiKey(apiKey, baseUrl) {
78
261
  try {
79
262
  const response = await fetch(`${baseUrl}/api/v1/agents`, {
@@ -280,14 +463,15 @@ async function createProxyConfig(baseUrl, apiKey, agentName, targetUrl, serverNa
280
463
  const data = envelope["data"];
281
464
  return typeof data?.["proxy_url"] === "string" ? data["proxy_url"] : "";
282
465
  }
283
- function printPlatformSnippet(platform, routingToken, shortName) {
466
+ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
467
+ const authHeader = platform === "cursor" ? `Bearer ${apiKey}` : "Bearer YOUR_SHIELD_API_KEY";
284
468
  const mcpSnippet = JSON.stringify(
285
469
  {
286
470
  mcpServers: {
287
471
  [shortName]: {
288
472
  url: routingToken,
289
473
  headers: {
290
- Authorization: "Bearer YOUR_SHIELD_API_KEY"
474
+ Authorization: authHeader
291
475
  }
292
476
  }
293
477
  }
@@ -303,11 +487,13 @@ function printPlatformSnippet(platform, routingToken, shortName) {
303
487
  process.stderr.write("\n" + style.dim("Add this to ~/.cursor/mcp.json:") + "\n\n");
304
488
  }
305
489
  process.stderr.write(style.cyan(mcpSnippet) + "\n\n");
306
- process.stderr.write(
307
- style.dim(
308
- "Replace YOUR_SHIELD_API_KEY with your API key. Find it in Settings > API keys at https://app.multicorn.ai/settings/api-keys"
309
- ) + "\n"
310
- );
490
+ if (platform !== "cursor") {
491
+ process.stderr.write(
492
+ style.dim(
493
+ "Replace YOUR_SHIELD_API_KEY with your API key. Find it in Settings > API keys at https://app.multicorn.ai/settings/api-keys"
494
+ ) + "\n"
495
+ );
496
+ }
311
497
  if (platform === "cursor") {
312
498
  process.stderr.write(
313
499
  style.dim(
@@ -321,27 +507,7 @@ function printPlatformSnippet(platform, routingToken, shortName) {
321
507
  );
322
508
  }
323
509
  }
324
- function printOpenClawInstructions() {
325
- process.stderr.write("\n" + style.green("\u2713") + " Agent registered!\n");
326
- process.stderr.write(
327
- "\nTo connect this agent, add the Multicorn Shield plugin to your OpenClaw agent:\n"
328
- );
329
- process.stderr.write("\n " + style.cyan("openclaw plugins add multicorn-shield") + "\n");
330
- process.stderr.write(
331
- "\nThen start your agent. Shield will monitor and protect tool calls automatically.\n"
332
- );
333
- }
334
- function printClaudeCodeInstructions() {
335
- process.stderr.write("\n" + style.green("\u2713") + " Agent registered!\n");
336
- process.stderr.write(
337
- "\nTo connect this agent, install the Multicorn Shield plugin in Claude Code:\n"
338
- );
339
- process.stderr.write("\n " + style.cyan("claude plugins install multicorn-shield") + "\n");
340
- process.stderr.write(
341
- "\nThen start a new Claude Code session. Shield will monitor and protect tool calls automatically.\n"
342
- );
343
- }
344
- async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
510
+ async function runInit(baseUrl = "https://api.multicorn.ai") {
345
511
  if (!process.stdin.isTTY) {
346
512
  process.stderr.write(
347
513
  style.red("Error: interactive terminal required. Cannot run init with piped input.") + "\n"
@@ -408,23 +574,136 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
408
574
  return null;
409
575
  }
410
576
  const configuredAgents = [];
577
+ let currentAgents = collectAgentsFromConfig(existing);
411
578
  let lastConfig = {
412
579
  apiKey,
413
580
  baseUrl,
414
- ...{}
581
+ ...currentAgents.length > 0 ? {
582
+ agents: currentAgents,
583
+ defaultAgent: existing !== null && typeof existing.defaultAgent === "string" && existing.defaultAgent.length > 0 ? existing.defaultAgent : currentAgents[currentAgents.length - 1]?.name ?? ""
584
+ } : {}
415
585
  };
416
586
  let configuring = true;
417
587
  while (configuring) {
418
588
  const selection = await promptPlatformSelection(ask);
419
589
  const selectedPlatform = PLATFORM_BY_SELECTION[selection] ?? "cursor";
420
590
  const selectedLabel = PLATFORM_LABELS[selection - 1] ?? "Cursor";
591
+ const existingForPlatform = currentAgents.find((a) => a.platform === selectedPlatform);
592
+ if (existingForPlatform !== void 0) {
593
+ process.stderr.write(
594
+ `
595
+ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.name)}
596
+ `
597
+ );
598
+ const replace = await ask("Replace it? (Y/n) ");
599
+ if (replace.trim().toLowerCase() === "n") {
600
+ const another2 = await ask("\nConnect another agent? (Y/n) ");
601
+ if (another2.trim().toLowerCase() === "n") {
602
+ configuring = false;
603
+ }
604
+ continue;
605
+ }
606
+ }
421
607
  const agentName = await promptAgentName(ask, selectedPlatform);
608
+ let setupSucceeded = false;
422
609
  if (selection === 1) {
423
- printOpenClawInstructions();
424
- configuredAgents.push({ platformLabel: selectedLabel, agentName });
610
+ let detection;
611
+ try {
612
+ detection = await detectOpenClaw();
613
+ } catch (error) {
614
+ const detail = error instanceof Error ? error.message : String(error);
615
+ process.stderr.write(style.red("\u2717") + ` Failed to read OpenClaw config: ${detail}
616
+ `);
617
+ rl.close();
618
+ return null;
619
+ }
620
+ if (detection.status === "not-found") {
621
+ process.stderr.write(
622
+ style.red("\u2717") + " OpenClaw is not installed. Install OpenClaw first, then run npx multicorn-proxy init again.\n"
623
+ );
624
+ rl.close();
625
+ return null;
626
+ }
627
+ if (detection.status === "parse-error") {
628
+ process.stderr.write(
629
+ style.red("\u2717") + " Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually.\n"
630
+ );
631
+ }
632
+ if (detection.status === "detected") {
633
+ if (detection.version !== null) {
634
+ process.stderr.write(
635
+ style.green("\u2713") + ` OpenClaw detected ${style.dim(`(${detection.version})`)}
636
+ `
637
+ );
638
+ if (isVersionAtLeast(detection.version, OPENCLAW_MIN_VERSION)) {
639
+ process.stderr.write(
640
+ style.green("\u2713") + " " + style.green("Version compatible") + "\n"
641
+ );
642
+ } else {
643
+ process.stderr.write(
644
+ 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)}.
645
+ `
646
+ );
647
+ const answer = await ask("Continue anyway? (y/N) ");
648
+ if (answer.trim().toLowerCase() !== "y") {
649
+ rl.close();
650
+ return null;
651
+ }
652
+ }
653
+ } else {
654
+ process.stderr.write(
655
+ style.yellow("\u26A0") + " Could not detect OpenClaw version. Continuing anyway.\n"
656
+ );
657
+ }
658
+ const spinner = withSpinner("Updating OpenClaw config...");
659
+ try {
660
+ const result = await updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName);
661
+ if (result === "not-found") {
662
+ spinner.stop(false, "OpenClaw config disappeared unexpectedly.");
663
+ rl.close();
664
+ return null;
665
+ }
666
+ if (result === "parse-error") {
667
+ spinner.stop(
668
+ false,
669
+ "Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually."
670
+ );
671
+ } else {
672
+ spinner.stop(
673
+ true,
674
+ "OpenClaw config updated at " + style.cyan("~/.openclaw/openclaw.json")
675
+ );
676
+ }
677
+ } catch (error) {
678
+ const detail = error instanceof Error ? error.message : String(error);
679
+ spinner.stop(false, `Failed to update OpenClaw config: ${detail}`);
680
+ }
681
+ }
682
+ configuredAgents.push({
683
+ selection,
684
+ platform: selectedPlatform,
685
+ platformLabel: selectedLabel,
686
+ agentName
687
+ });
688
+ setupSucceeded = true;
425
689
  } else if (selection === 2) {
426
- printClaudeCodeInstructions();
427
- configuredAgents.push({ platformLabel: selectedLabel, agentName });
690
+ process.stderr.write("\nTo connect Claude Code to Shield:\n\n");
691
+ process.stderr.write(
692
+ " " + style.bold("Step 1") + " - Add the Multicorn marketplace:\n " + style.cyan("claude plugin marketplace add Multicorn-AI/multicorn-shield") + "\n\n"
693
+ );
694
+ process.stderr.write(
695
+ " " + style.bold("Step 2") + " - Install the plugin:\n " + style.cyan("claude plugin install multicorn-shield@multicorn-shield") + "\n\n"
696
+ );
697
+ process.stderr.write(
698
+ style.dim("Requires Claude Code to be installed. Get it at https://code.claude.com") + "\n"
699
+ );
700
+ configuredAgents.push({
701
+ selection,
702
+ platform: selectedPlatform,
703
+ platformLabel: selectedLabel,
704
+ agentName
705
+ });
706
+ setupSucceeded = true;
428
707
  } else {
429
708
  const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
430
709
  let proxyUrl = "";
@@ -454,23 +733,39 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
454
733
  if (created && proxyUrl.length > 0) {
455
734
  process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
456
735
  process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
457
- printPlatformSnippet(selectedPlatform, proxyUrl, shortName);
736
+ printPlatformSnippet(selectedPlatform, proxyUrl, shortName, apiKey);
458
737
  configuredAgents.push({
738
+ selection,
739
+ platform: selectedPlatform,
459
740
  platformLabel: selectedLabel,
460
741
  agentName,
461
742
  shortName,
462
743
  proxyUrl
463
744
  });
745
+ setupSucceeded = true;
464
746
  }
465
747
  }
466
- lastConfig = { apiKey, baseUrl, agentName, platform: selectedPlatform };
467
- try {
468
- await saveConfig(lastConfig);
469
- process.stderr.write(style.green("\u2713") + ` Config saved to ${style.cyan(CONFIG_PATH)}
470
- `);
471
- } catch (error) {
472
- const detail = error instanceof Error ? error.message : String(error);
473
- process.stderr.write(style.red(`Failed to save config: ${detail}`) + "\n");
748
+ if (setupSucceeded) {
749
+ currentAgents = currentAgents.filter((a) => a.platform !== selectedPlatform);
750
+ currentAgents.push({ name: agentName, platform: selectedPlatform });
751
+ const raw = existing !== null ? { ...existing } : {};
752
+ raw["apiKey"] = apiKey;
753
+ raw["baseUrl"] = baseUrl;
754
+ raw["agents"] = currentAgents;
755
+ raw["defaultAgent"] = agentName;
756
+ delete raw["agentName"];
757
+ delete raw["platform"];
758
+ lastConfig = raw;
759
+ try {
760
+ await saveConfig(lastConfig);
761
+ process.stderr.write(
762
+ style.green("\u2713") + ` Config saved to ${style.cyan(CONFIG_PATH)}
763
+ `
764
+ );
765
+ } catch (error) {
766
+ const detail = error instanceof Error ? error.message : String(error);
767
+ process.stderr.write(style.red(`Failed to save config: ${detail}`) + "\n");
768
+ }
474
769
  }
475
770
  const another = await ask("\nConnect another agent? (Y/n) ");
476
771
  if (another.trim().toLowerCase() === "n") {
@@ -487,6 +782,35 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
487
782
  );
488
783
  }
489
784
  process.stderr.write("\n");
785
+ const configuredPlatforms = new Set(configuredAgents.map((a) => a.platform));
786
+ process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
787
+ const blocks = [];
788
+ if (configuredPlatforms.has("openclaw")) {
789
+ blocks.push(
790
+ "\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"
791
+ );
792
+ }
793
+ if (configuredPlatforms.has("claude-code")) {
794
+ blocks.push(
795
+ "\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"
796
+ );
797
+ }
798
+ if (configuredPlatforms.has("claude-desktop")) {
799
+ blocks.push(
800
+ "\n" + style.bold("To complete your Claude Desktop setup:") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
801
+ );
802
+ }
803
+ if (configuredPlatforms.has("cursor")) {
804
+ blocks.push(
805
+ "\n" + style.bold("To complete your Cursor setup:") + "\n \u2192 Restart Cursor to pick up MCP config changes\n"
806
+ );
807
+ }
808
+ if (configuredPlatforms.has("other-mcp")) {
809
+ blocks.push(
810
+ "\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"
811
+ );
812
+ }
813
+ process.stderr.write(blocks.join("") + "\n");
490
814
  }
491
815
  return lastConfig;
492
816
  }
@@ -1599,10 +1923,23 @@ function parseArgs(argv) {
1599
1923
  let baseUrl = "https://api.multicorn.ai";
1600
1924
  let dashboardUrl = "";
1601
1925
  let agentName = "";
1926
+ let deleteAgentName = "";
1602
1927
  for (let i = 0; i < args.length; i++) {
1603
1928
  const arg = args[i];
1604
1929
  if (arg === "init") {
1605
1930
  subcommand = "init";
1931
+ } else if (arg === "agents") {
1932
+ subcommand = "agents";
1933
+ } else if (arg === "delete-agent") {
1934
+ subcommand = "delete-agent";
1935
+ const name = args[i + 1];
1936
+ if (name === void 0 || name.startsWith("-")) {
1937
+ process.stderr.write("Error: delete-agent requires an agent name.\n");
1938
+ process.stderr.write("Example: npx multicorn-proxy delete-agent my-agent\n");
1939
+ process.exit(1);
1940
+ }
1941
+ deleteAgentName = name;
1942
+ i++;
1606
1943
  } else if (arg === "--wrap") {
1607
1944
  subcommand = "wrap";
1608
1945
  const next = args[i + 1];
@@ -1672,7 +2009,16 @@ function parseArgs(argv) {
1672
2009
  }
1673
2010
  }
1674
2011
  }
1675
- return { subcommand, wrapCommand, wrapArgs, logLevel, baseUrl, dashboardUrl, agentName };
2012
+ return {
2013
+ subcommand,
2014
+ wrapCommand,
2015
+ wrapArgs,
2016
+ logLevel,
2017
+ baseUrl,
2018
+ dashboardUrl,
2019
+ agentName,
2020
+ deleteAgentName
2021
+ };
1676
2022
  }
1677
2023
  function printHelp() {
1678
2024
  process.stderr.write(
@@ -1683,6 +2029,12 @@ function printHelp() {
1683
2029
  " npx multicorn-proxy init",
1684
2030
  " Interactive setup. Saves API key to ~/.multicorn/config.json.",
1685
2031
  "",
2032
+ " npx multicorn-proxy agents",
2033
+ " List configured agents and show which is the default.",
2034
+ "",
2035
+ " npx multicorn-proxy delete-agent <name>",
2036
+ " Remove a saved agent.",
2037
+ "",
1686
2038
  " npx multicorn-proxy --wrap <command> [args...]",
1687
2039
  " Start <command> as an MCP server and proxy all tool calls through",
1688
2040
  " Shield's permission layer.",
@@ -1712,6 +2064,40 @@ async function main() {
1712
2064
  await runInit(cli.baseUrl);
1713
2065
  return;
1714
2066
  }
2067
+ if (cli.subcommand === "agents") {
2068
+ const config2 = await loadConfig();
2069
+ if (config2 === null) {
2070
+ process.stderr.write(
2071
+ "No config found. Run `npx multicorn-proxy init` to set up your API key.\n"
2072
+ );
2073
+ process.exit(1);
2074
+ }
2075
+ const agents = collectAgentsFromConfig(config2);
2076
+ if (agents.length === 0) {
2077
+ process.stderr.write("No agents configured. Run `npx multicorn-proxy init` to add one.\n");
2078
+ process.exit(0);
2079
+ }
2080
+ const def = config2.defaultAgent;
2081
+ process.stdout.write("Configured agents:\n");
2082
+ for (const a of agents) {
2083
+ const mark = a.name === def ? " (default)" : "";
2084
+ process.stdout.write(`${a.name} (${a.platform})${mark}
2085
+ `);
2086
+ }
2087
+ return;
2088
+ }
2089
+ if (cli.subcommand === "delete-agent") {
2090
+ const safeName = cli.deleteAgentName.replace(/[^\x20-\x7E]/g, "");
2091
+ const ok = await deleteAgentByName(cli.deleteAgentName);
2092
+ if (!ok) {
2093
+ process.stderr.write(`No agent named "${safeName}" in config.
2094
+ `);
2095
+ process.exit(1);
2096
+ }
2097
+ process.stdout.write(`Removed agent "${safeName}".
2098
+ `);
2099
+ return;
2100
+ }
1715
2101
  if (!cli.baseUrl.startsWith("https://") && !cli.baseUrl.startsWith("http://localhost") && !cli.baseUrl.startsWith("http://127.0.0.1")) {
1716
2102
  process.stderr.write(
1717
2103
  `Error: --base-url must use HTTPS. Received: "${cli.baseUrl}"
@@ -1727,9 +2113,11 @@ Use https:// or http://localhost for local development.
1727
2113
  );
1728
2114
  process.exit(1);
1729
2115
  }
1730
- const agentName = cli.agentName.length > 0 ? cli.agentName : config.agentName !== void 0 && config.agentName.length > 0 ? config.agentName : deriveAgentName(cli.wrapCommand);
2116
+ const agentName = resolveWrapAgentName(cli, config);
1731
2117
  const finalBaseUrl = cli.baseUrl !== "https://api.multicorn.ai" ? cli.baseUrl : config.baseUrl;
1732
2118
  const finalDashboardUrl = cli.dashboardUrl !== "" ? cli.dashboardUrl : deriveDashboardUrl(finalBaseUrl);
2119
+ const legacyPlatform = config.platform;
2120
+ const platformForServer = typeof legacyPlatform === "string" && legacyPlatform.length > 0 ? legacyPlatform : "other-mcp";
1733
2121
  const proxy = createProxyServer({
1734
2122
  command: cli.wrapCommand,
1735
2123
  commandArgs: cli.wrapArgs,
@@ -1738,7 +2126,7 @@ Use https:// or http://localhost for local development.
1738
2126
  baseUrl: finalBaseUrl,
1739
2127
  dashboardUrl: finalDashboardUrl,
1740
2128
  logger,
1741
- platform: config.platform ?? "other-mcp"
2129
+ platform: platformForServer
1742
2130
  });
1743
2131
  async function shutdown() {
1744
2132
  logger.info("Shutting down.");
@@ -1753,6 +2141,26 @@ Use https:// or http://localhost for local development.
1753
2141
  });
1754
2142
  await proxy.start();
1755
2143
  }
2144
+ function resolveWrapAgentName(cli, config) {
2145
+ if (cli.agentName.length > 0) {
2146
+ return cli.agentName;
2147
+ }
2148
+ const legacyPlatform = config.platform;
2149
+ const legacyAgentName = config.agentName;
2150
+ const platformKey = typeof legacyPlatform === "string" && legacyPlatform.length > 0 ? legacyPlatform : "other-mcp";
2151
+ const fromPlatform = getAgentByPlatform(config, platformKey);
2152
+ if (fromPlatform !== void 0) {
2153
+ return fromPlatform.name;
2154
+ }
2155
+ const fallbackDefault = getDefaultAgent(config);
2156
+ if (fallbackDefault !== void 0) {
2157
+ return fallbackDefault.name;
2158
+ }
2159
+ if (typeof legacyAgentName === "string" && legacyAgentName.length > 0) {
2160
+ return legacyAgentName;
2161
+ }
2162
+ return deriveAgentName(cli.wrapCommand);
2163
+ }
1756
2164
  function deriveAgentName(command) {
1757
2165
  const base = command.split("/").pop() ?? command;
1758
2166
  return base.replace(/\.[cm]?[jt]s$/, "");
@@ -492,11 +492,22 @@ function loadMulticornConfig() {
492
492
  return null;
493
493
  }
494
494
  }
495
+ function agentNameFromOpenclawPlatform(cfg) {
496
+ if (cfg === null) return void 0;
497
+ const list = cfg.agents;
498
+ if (!Array.isArray(list)) return void 0;
499
+ for (const e of list) {
500
+ if (typeof e === "object" && e !== null && "platform" in e && "name" in e && e.platform === "openclaw" && typeof e.name === "string") {
501
+ return e.name;
502
+ }
503
+ }
504
+ return void 0;
505
+ }
495
506
  function readConfig() {
496
507
  const pc = pluginConfig ?? {};
497
508
  const resolvedApiKey = asString(cachedMulticornConfig?.apiKey) ?? asString(process.env["MULTICORN_API_KEY"]) ?? "";
498
509
  const resolvedBaseUrl = asString(cachedMulticornConfig?.baseUrl) ?? asString(process.env["MULTICORN_BASE_URL"]) ?? "https://api.multicorn.ai";
499
- const agentName = asString(pc["agentName"]) ?? process.env["MULTICORN_AGENT_NAME"] ?? asString(cachedMulticornConfig?.agentName) ?? null;
510
+ const agentName = asString(pc["agentName"]) ?? process.env["MULTICORN_AGENT_NAME"] ?? agentNameFromOpenclawPlatform(cachedMulticornConfig) ?? asString(cachedMulticornConfig?.agentName) ?? null;
500
511
  const failMode = "closed";
501
512
  return { apiKey: resolvedApiKey, baseUrl: resolvedBaseUrl, agentName, failMode };
502
513
  }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { mkdirSync, appendFileSync } from 'fs';
2
+ import { readFileSync, mkdirSync, appendFileSync } from 'fs';
3
3
  import { readFile, mkdir, writeFile, unlink } from 'fs/promises';
4
4
  import { homedir } from 'os';
5
5
  import { join } from 'path';
@@ -22358,7 +22358,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
22358
22358
 
22359
22359
  // package.json
22360
22360
  var package_default = {
22361
- version: "0.4.0"};
22361
+ version: "0.6.1"};
22362
22362
 
22363
22363
  // src/package-meta.ts
22364
22364
  var PACKAGE_VERSION = package_default.version;
@@ -23522,8 +23522,27 @@ function readBaseUrl() {
23522
23522
  return raw;
23523
23523
  }
23524
23524
  function readAgentName() {
23525
+ try {
23526
+ const rawFile = readFileSync(getMulticornConfigPath(), "utf8");
23527
+ const obj = JSON.parse(rawFile);
23528
+ const agents = obj["agents"];
23529
+ if (Array.isArray(agents)) {
23530
+ for (const e of agents) {
23531
+ if (typeof e === "object" && e !== null && e.platform === "claude-desktop" && typeof e.name === "string") {
23532
+ const n = e.name.trim();
23533
+ if (n.length > 0) {
23534
+ return n;
23535
+ }
23536
+ }
23537
+ }
23538
+ }
23539
+ } catch {
23540
+ }
23525
23541
  const raw = process.env["MULTICORN_AGENT_NAME"]?.trim();
23526
- return raw !== void 0 && raw.length > 0 ? raw : "claude-desktop-shield";
23542
+ if (raw !== void 0 && raw.length > 0 && !raw.startsWith("${")) {
23543
+ return raw;
23544
+ }
23545
+ return "claude-desktop-shield";
23527
23546
  }
23528
23547
  function readLogLevel() {
23529
23548
  const raw = process.env["MULTICORN_LOG_LEVEL"]?.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multicorn-shield",
3
- "version": "0.4.0",
3
+ "version": "0.6.1",
4
4
  "description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
5
5
  "license": "MIT",
6
6
  "type": "module",