multicorn-shield 0.2.2 → 0.6.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.
@@ -45,17 +45,106 @@ function withSpinner(message) {
45
45
  var CONFIG_DIR = join(homedir(), ".multicorn");
46
46
  var CONFIG_PATH = join(CONFIG_DIR, "config.json");
47
47
  var OPENCLAW_CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
48
- var OPENCLAW_MIN_VERSION = "2026.2.26";
48
+ var ANSI_PATTERN = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*[a-zA-Z]", "g");
49
+ function stripAnsi(str) {
50
+ return str.replace(ANSI_PATTERN, "");
51
+ }
52
+ function normalizeAgentName(raw) {
53
+ return raw.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
54
+ }
55
+ function isErrnoException(e) {
56
+ return typeof e === "object" && e !== null && "code" in e;
57
+ }
58
+ function isProxyConfig(value) {
59
+ if (typeof value !== "object" || value === null) return false;
60
+ const obj = value;
61
+ return typeof obj["apiKey"] === "string" && typeof obj["baseUrl"] === "string";
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
+ }
49
97
  async function loadConfig() {
50
98
  try {
51
99
  const raw = await readFile(CONFIG_PATH, "utf8");
52
100
  const parsed = JSON.parse(raw);
53
101
  if (!isProxyConfig(parsed)) return null;
54
- 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;
55
119
  } catch {
56
120
  return null;
57
121
  }
58
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
+ }
59
148
  async function saveConfig(config) {
60
149
  await mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
61
150
  await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
@@ -63,8 +152,44 @@ async function saveConfig(config) {
63
152
  mode: 384
64
153
  });
65
154
  }
66
- function isErrnoException(e) {
67
- return typeof e === "object" && e !== null && "code" in e;
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;
68
193
  }
69
194
  async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
70
195
  let raw;
@@ -132,44 +257,6 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
132
257
  });
133
258
  return "updated";
134
259
  }
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;
172
- }
173
260
  async function validateApiKey(apiKey, baseUrl) {
174
261
  try {
175
262
  const response = await fetch(`${baseUrl}/api/v1/agents`, {
@@ -194,9 +281,6 @@ async function validateApiKey(apiKey, baseUrl) {
194
281
  };
195
282
  }
196
283
  }
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
284
  async function isOpenClawConnected() {
201
285
  try {
202
286
  const raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
@@ -219,85 +303,211 @@ function isClaudeCodeConnected() {
219
303
  return false;
220
304
  }
221
305
  }
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";
306
+ function getCursorConfigPath() {
307
+ return join(homedir(), ".cursor", "mcp.json");
283
308
  }
284
- async function isClaudeDesktopConnected() {
309
+ async function isCursorConnected() {
285
310
  try {
286
- const raw = await readFile(getClaudeDesktopConfigPath(), "utf8");
311
+ const raw = await readFile(getCursorConfigPath(), "utf8");
287
312
  const obj = JSON.parse(raw);
288
313
  const mcpServers = obj["mcpServers"];
289
314
  if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
290
315
  for (const entry of Object.values(mcpServers)) {
291
316
  if (typeof entry !== "object" || entry === null) continue;
292
- const args = entry["args"];
317
+ const rec = entry;
318
+ const url = rec["url"];
319
+ if (typeof url === "string" && url.includes("multicorn")) return true;
320
+ const args = rec["args"];
293
321
  if (Array.isArray(args) && args.includes("multicorn-proxy")) return true;
294
322
  }
295
323
  return false;
296
- } catch {
324
+ } catch (err) {
325
+ process.stderr.write(
326
+ `Warning: could not check Cursor connection status: ${err instanceof Error ? err.message : String(err)}
327
+ `
328
+ );
297
329
  return false;
298
330
  }
299
331
  }
300
- async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
332
+ var PLATFORM_LABELS = ["OpenClaw", "Claude Code", "Cursor"];
333
+ var PLATFORM_BY_SELECTION = {
334
+ 1: "openclaw",
335
+ 2: "claude-code",
336
+ 3: "cursor"
337
+ };
338
+ var DEFAULT_AGENT_NAMES = {
339
+ openclaw: "my-openclaw-agent",
340
+ "claude-code": "my-claude-code-agent",
341
+ cursor: "my-cursor-agent"
342
+ };
343
+ async function promptPlatformSelection(ask) {
344
+ process.stderr.write(
345
+ "\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n"
346
+ );
347
+ const connectedFlags = [
348
+ await isOpenClawConnected(),
349
+ isClaudeCodeConnected(),
350
+ await isCursorConnected()
351
+ ];
352
+ for (let i = 0; i < PLATFORM_LABELS.length; i++) {
353
+ const marker = connectedFlags[i] ? " " + style.green("\u2713") + style.dim(" connected") : "";
354
+ process.stderr.write(
355
+ ` ${style.violet(String(i + 1))}. ${PLATFORM_LABELS[i] ?? ""}${marker}
356
+ `
357
+ );
358
+ }
359
+ let selection = 0;
360
+ while (selection === 0) {
361
+ const input = await ask("Select (1-3): ");
362
+ const num = parseInt(input.trim(), 10);
363
+ if (num >= 1 && num <= 3) {
364
+ selection = num;
365
+ }
366
+ }
367
+ return selection;
368
+ }
369
+ async function promptAgentName(ask, platform) {
370
+ const defaultAgentName = DEFAULT_AGENT_NAMES[platform] ?? "my-agent";
371
+ let agentName = "";
372
+ while (agentName.length === 0) {
373
+ const input = await ask(
374
+ `
375
+ What would you like to call this agent? ${style.dim(`(${defaultAgentName})`)} `
376
+ );
377
+ const raw = input.trim().length > 0 ? input.trim() : defaultAgentName;
378
+ const transformed = normalizeAgentName(raw);
379
+ if (transformed.length === 0) {
380
+ process.stderr.write(
381
+ style.red("Agent name must contain letters or numbers. Please try again.") + "\n"
382
+ );
383
+ continue;
384
+ }
385
+ if (transformed !== raw) {
386
+ process.stderr.write(style.yellow("Agent name set to: ") + style.cyan(transformed) + "\n");
387
+ }
388
+ agentName = transformed;
389
+ }
390
+ return agentName;
391
+ }
392
+ async function promptProxyConfig(ask, agentName) {
393
+ let targetUrl = "";
394
+ while (targetUrl.length === 0) {
395
+ process.stderr.write(
396
+ "\n" + style.bold("Target MCP server URL:") + "\n" + style.dim(
397
+ "The URL of the MCP server you want Shield to protect. Example: https://your-server.example.com/mcp"
398
+ ) + "\n"
399
+ );
400
+ const input = await ask("URL: ");
401
+ if (input.trim().length === 0) {
402
+ process.stderr.write(style.red("MCP server URL is required.") + "\n");
403
+ continue;
404
+ }
405
+ try {
406
+ new URL(input.trim());
407
+ } catch {
408
+ process.stderr.write(
409
+ style.red(
410
+ "\u2717 That does not look like a valid URL. Please enter a full URL including the scheme (e.g. https://your-server.example.com/mcp)."
411
+ ) + "\n"
412
+ );
413
+ continue;
414
+ }
415
+ targetUrl = input.trim();
416
+ }
417
+ const defaultShortName = normalizeAgentName(agentName) || "shield-mcp";
418
+ const shortNameInput = await ask(
419
+ `
420
+ Short name (a nickname for this connection, used in your proxy URL): ${style.dim(`(${defaultShortName})`)} `
421
+ );
422
+ const shortName = shortNameInput.trim().length > 0 ? normalizeAgentName(shortNameInput.trim()) || defaultShortName : defaultShortName;
423
+ return { targetUrl, shortName };
424
+ }
425
+ async function createProxyConfig(baseUrl, apiKey, agentName, targetUrl, serverName, platform) {
426
+ let response;
427
+ try {
428
+ response = await fetch(`${baseUrl}/api/v1/proxy/config`, {
429
+ method: "POST",
430
+ headers: {
431
+ "Content-Type": "application/json",
432
+ "X-Multicorn-Key": apiKey
433
+ },
434
+ body: JSON.stringify({
435
+ server_name: serverName,
436
+ target_url: targetUrl,
437
+ platform,
438
+ agent_name: agentName
439
+ }),
440
+ signal: AbortSignal.timeout(1e4)
441
+ });
442
+ } catch (error) {
443
+ const detail = error instanceof Error ? error.message : String(error);
444
+ throw new Error(`Failed to create proxy config: ${detail}`);
445
+ }
446
+ if (!response.ok) {
447
+ let errorMsg = `Shield API returned an error (HTTP ${String(response.status)}). Check your agent name and target URL, then try again.`;
448
+ try {
449
+ const errBody = await response.json();
450
+ const errObj = errBody["error"];
451
+ if (typeof errObj?.["message"] === "string") {
452
+ errorMsg = stripAnsi(errObj["message"]);
453
+ } else if (typeof errBody["message"] === "string") {
454
+ errorMsg = stripAnsi(errBody["message"]);
455
+ } else if (typeof errBody["detail"] === "string") {
456
+ errorMsg = stripAnsi(errBody["detail"]);
457
+ }
458
+ } catch {
459
+ }
460
+ throw new Error(errorMsg);
461
+ }
462
+ const envelope = await response.json();
463
+ const data = envelope["data"];
464
+ return typeof data?.["proxy_url"] === "string" ? data["proxy_url"] : "";
465
+ }
466
+ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
467
+ const authHeader = platform === "cursor" ? `Bearer ${apiKey}` : "Bearer YOUR_SHIELD_API_KEY";
468
+ const mcpSnippet = JSON.stringify(
469
+ {
470
+ mcpServers: {
471
+ [shortName]: {
472
+ url: routingToken,
473
+ headers: {
474
+ Authorization: authHeader
475
+ }
476
+ }
477
+ }
478
+ },
479
+ null,
480
+ 2
481
+ );
482
+ if (platform === "openclaw") {
483
+ process.stderr.write("\n" + style.dim("Add this to your OpenClaw agent config:") + "\n\n");
484
+ } else if (platform === "claude-code") {
485
+ process.stderr.write("\n" + style.dim("Add this to your Claude Code MCP config:") + "\n\n");
486
+ } else {
487
+ process.stderr.write("\n" + style.dim("Add this to ~/.cursor/mcp.json:") + "\n\n");
488
+ }
489
+ process.stderr.write(style.cyan(mcpSnippet) + "\n\n");
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
+ }
497
+ if (platform === "cursor") {
498
+ process.stderr.write(
499
+ style.dim(
500
+ "Then restart Cursor and check Settings > Tools & MCPs for a green status indicator."
501
+ ) + "\n"
502
+ );
503
+ process.stderr.write(
504
+ style.dim(
505
+ `Ask Cursor to use your MCP server by its short name. For example: "use the ${shortName} tool to list files in /tmp"`
506
+ ) + "\n"
507
+ );
508
+ }
509
+ }
510
+ async function runInit(baseUrl = "https://api.multicorn.ai") {
301
511
  if (!process.stdin.isTTY) {
302
512
  process.stderr.write(
303
513
  style.red("Error: interactive terminal required. Cannot run init with piped input.") + "\n"
@@ -305,20 +515,15 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
305
515
  process.exit(1);
306
516
  }
307
517
  const rl = createInterface({ input: process.stdin, output: process.stderr });
308
- function ask(question) {
309
- return new Promise((resolve) => {
310
- rl.question(question, (answer) => {
311
- resolve(answer);
312
- });
313
- });
314
- }
518
+ const ask = (question) => new Promise((resolve) => {
519
+ rl.question(question, resolve);
520
+ });
315
521
  process.stderr.write("\n" + BANNER + "\n");
316
522
  process.stderr.write(style.dim("Agent governance for the AI era") + "\n\n");
317
523
  process.stderr.write(style.bold(style.violet("Multicorn Shield proxy setup")) + "\n\n");
318
524
  process.stderr.write(
319
525
  style.dim("Get your API key at https://app.multicorn.ai/settings/api-keys") + "\n\n"
320
526
  );
321
- let apiKey = "";
322
527
  const existing = await loadConfig().catch(() => null);
323
528
  if (baseUrl === "https://api.multicorn.ai") {
324
529
  if (existing !== null && existing.baseUrl.length > 0) {
@@ -330,6 +535,7 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
330
535
  }
331
536
  }
332
537
  }
538
+ let apiKey = "";
333
539
  if (existing !== null && existing.apiKey.startsWith("mcs_") && existing.apiKey.length >= 8) {
334
540
  const masked = "mcs_..." + existing.apiKey.slice(-4);
335
541
  process.stderr.write("Found existing API key: " + style.cyan(masked) + "\n");
@@ -360,62 +566,46 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
360
566
  spinner.stop(true, "Key validated");
361
567
  apiKey = key;
362
568
  }
363
- const configuredPlatforms = /* @__PURE__ */ new Set();
569
+ if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost") && !baseUrl.startsWith("http://127.0.0.1")) {
570
+ process.stderr.write(
571
+ style.red(`\u2717 Shield API base URL must use HTTPS. Got: ${baseUrl}`) + "\n"
572
+ );
573
+ rl.close();
574
+ return null;
575
+ }
576
+ const configuredAgents = [];
577
+ let currentAgents = collectAgentsFromConfig(existing);
364
578
  let lastConfig = {
365
579
  apiKey,
366
580
  baseUrl,
367
- ...{}
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
+ } : {}
368
585
  };
369
586
  let configuring = true;
370
587
  while (configuring) {
371
- process.stderr.write(
372
- "\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n"
373
- );
374
- const platformLabels = ["OpenClaw", "Claude Code", "Claude Desktop", "Other MCP Agent"];
375
- const openClawConnected = await isOpenClawConnected();
376
- const claudeCodeConnected = isClaudeCodeConnected();
377
- const claudeDesktopConnected = await isClaudeDesktopConnected();
378
- for (let i = 0; i < platformLabels.length; i++) {
379
- const sessionMarker = configuredPlatforms.has(i + 1) ? " " + style.green("\u2713") : "";
380
- let connectedMarker = "";
381
- if (!configuredPlatforms.has(i + 1)) {
382
- if (i === 0 && openClawConnected) {
383
- connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
384
- } else if (i === 1 && claudeCodeConnected) {
385
- connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
386
- } else if (i === 2 && claudeDesktopConnected) {
387
- connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
388
- }
389
- }
588
+ const selection = await promptPlatformSelection(ask);
589
+ const selectedPlatform = PLATFORM_BY_SELECTION[selection] ?? "cursor";
590
+ const selectedLabel = PLATFORM_LABELS[selection - 1] ?? "Cursor";
591
+ const existingForPlatform = currentAgents.find((a) => a.platform === selectedPlatform);
592
+ if (existingForPlatform !== void 0) {
390
593
  process.stderr.write(
391
- ` ${style.violet(String(i + 1))}. ${platformLabels[i] ?? ""}${sessionMarker}${connectedMarker}
594
+ `
595
+ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.name)}
392
596
  `
393
597
  );
394
- }
395
- let selection = 0;
396
- while (selection === 0) {
397
- const input = await ask("Select (1-4): ");
398
- const num = parseInt(input.trim(), 10);
399
- if (num >= 1 && num <= 4) {
400
- selection = num;
401
- }
402
- }
403
- let agentName = "";
404
- while (agentName.length === 0) {
405
- const input = await ask("\nWhat would you like to call this agent? ");
406
- if (input.trim().length === 0) continue;
407
- const transformed = normalizeAgentName(input);
408
- if (transformed.length === 0) {
409
- process.stderr.write(
410
- style.red("Agent name must contain letters or numbers. Please try again.") + "\n"
411
- );
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
+ }
412
604
  continue;
413
605
  }
414
- if (transformed !== input.trim()) {
415
- process.stderr.write(style.yellow("Agent name set to: ") + style.cyan(transformed) + "\n");
416
- }
417
- agentName = transformed;
418
606
  }
607
+ const agentName = await promptAgentName(ask, selectedPlatform);
608
+ let setupSucceeded = false;
419
609
  if (selection === 1) {
420
610
  let detection;
421
611
  try {
@@ -489,6 +679,13 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
489
679
  spinner.stop(false, `Failed to update OpenClaw config: ${detail}`);
490
680
  }
491
681
  }
682
+ configuredAgents.push({
683
+ selection,
684
+ platform: selectedPlatform,
685
+ platformLabel: selectedLabel,
686
+ agentName
687
+ });
688
+ setupSucceeded = true;
492
689
  } else if (selection === 2) {
493
690
  process.stderr.write("\nTo connect Claude Code to Shield:\n\n");
494
691
  process.stderr.write(
@@ -497,164 +694,126 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
497
694
  process.stderr.write(
498
695
  " " + style.bold("Step 2") + " - Install the plugin:\n " + style.cyan("claude plugin install multicorn-shield@multicorn-shield") + "\n\n"
499
696
  );
500
- process.stderr.write(
501
- " " + style.bold("Step 3") + " - Start Claude Code:\n " + style.cyan("claude") + "\n\n"
502
- );
503
- process.stderr.write(
504
- style.dim("Run /plugin inside Claude Code to confirm multicorn-shield is installed.") + "\n"
505
- );
506
697
  process.stderr.write(
507
698
  style.dim("Requires Claude Code to be installed. Get it at https://code.claude.com") + "\n"
508
699
  );
509
- } else if (selection === 3) {
510
- const mcpCommand = await ask(
511
- "\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: "
512
- );
513
- if (mcpCommand.trim().length === 0) {
514
- const configPath = getClaudeDesktopConfigPath();
515
- process.stderr.write("\n" + style.dim("Add this to your Claude Desktop config at:") + "\n");
516
- process.stderr.write(" " + style.cyan(configPath) + "\n\n");
517
- const snippet = JSON.stringify(
518
- {
519
- mcpServers: {
520
- [agentName]: {
521
- command: "npx",
522
- args: [
523
- "multicorn-proxy",
524
- "--wrap",
525
- "<your-mcp-server-command>",
526
- "--agent-name",
527
- agentName
528
- ]
529
- }
530
- }
531
- },
532
- null,
533
- 2
534
- );
535
- process.stderr.write(style.cyan(snippet) + "\n\n");
536
- } else {
537
- let shouldWrite = true;
538
- const spinner = withSpinner("Updating Claude Desktop config...");
700
+ configuredAgents.push({
701
+ selection,
702
+ platform: selectedPlatform,
703
+ platformLabel: selectedLabel,
704
+ agentName
705
+ });
706
+ setupSucceeded = true;
707
+ } else {
708
+ const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
709
+ let proxyUrl = "";
710
+ let created = false;
711
+ while (!created) {
712
+ const spinner = withSpinner("Creating proxy config...");
539
713
  try {
540
- let result = await updateClaudeDesktopConfig(agentName, mcpCommand.trim());
541
- if (result === "skipped") {
542
- spinner.stop(false, `Agent "${agentName}" already exists in Claude Desktop config.`);
543
- const overwrite = await ask("Overwrite the existing entry? (y/N) ");
544
- if (overwrite.trim().toLowerCase() === "y") {
545
- const retrySpinner = withSpinner("Updating Claude Desktop config...");
546
- result = await updateClaudeDesktopConfig(agentName, mcpCommand.trim(), true);
547
- retrySpinner.stop(
548
- true,
549
- "Claude Desktop config updated at " + style.cyan(getClaudeDesktopConfigPath())
550
- );
551
- } else {
552
- shouldWrite = false;
553
- process.stderr.write(style.dim("Skipped. Existing config left unchanged.") + "\n");
554
- }
555
- } else if (result === "parse-error") {
556
- spinner.stop(false, "Claude Desktop config file contains invalid JSON.");
557
- const configPath = getClaudeDesktopConfigPath();
558
- process.stderr.write(
559
- style.yellow("\u26A0") + " Fix the JSON in " + style.cyan(configPath) + " or add this entry manually:\n\n"
560
- );
561
- const snippet = JSON.stringify(
562
- {
563
- mcpServers: {
564
- [agentName]: {
565
- command: "npx",
566
- args: [
567
- "multicorn-proxy",
568
- "--wrap",
569
- ...mcpCommand.trim().split(/\s+/),
570
- "--agent-name",
571
- agentName
572
- ]
573
- }
574
- }
575
- },
576
- null,
577
- 2
578
- );
579
- process.stderr.write(style.cyan(snippet) + "\n\n");
580
- } else {
581
- const verb = result === "created" ? "Created" : "Updated";
582
- spinner.stop(
583
- true,
584
- verb + " Claude Desktop config at " + style.cyan(getClaudeDesktopConfigPath())
585
- );
586
- process.stderr.write(style.dim("Restart Claude Desktop to pick up changes.") + "\n");
587
- }
714
+ proxyUrl = await createProxyConfig(
715
+ baseUrl,
716
+ apiKey,
717
+ agentName,
718
+ targetUrl,
719
+ shortName,
720
+ selectedPlatform
721
+ );
722
+ spinner.stop(true, "Proxy config created!");
723
+ created = true;
588
724
  } catch (error) {
589
725
  const detail = error instanceof Error ? error.message : String(error);
590
- spinner.stop(false, `Failed to update Claude Desktop config: ${detail}`);
591
- shouldWrite = false;
726
+ spinner.stop(false, detail);
727
+ const retry = await ask("Try again? (Y/n) ");
728
+ if (retry.trim().toLowerCase() === "n") {
729
+ break;
730
+ }
592
731
  }
593
732
  }
594
- } else {
595
- process.stderr.write("\n" + style.dim("Start the Shield proxy with:") + "\n");
596
- process.stderr.write(
597
- " " + style.cyan(
598
- `npx multicorn-proxy --wrap <your-mcp-server-command> --agent-name ${agentName}`
599
- ) + "\n\n"
600
- );
601
- }
602
- configuredPlatforms.add(selection);
603
- lastConfig = { apiKey, baseUrl, agentName, ...{} };
604
- try {
605
- await saveConfig(lastConfig);
606
- process.stderr.write(style.green("\u2713") + ` Config saved to ${style.cyan(CONFIG_PATH)}
607
- `);
608
- } catch (error) {
609
- const detail = error instanceof Error ? error.message : String(error);
610
- process.stderr.write(style.red(`Failed to save config: ${detail}`) + "\n");
733
+ if (created && proxyUrl.length > 0) {
734
+ process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
735
+ process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
736
+ printPlatformSnippet(selectedPlatform, proxyUrl, shortName, apiKey);
737
+ configuredAgents.push({
738
+ selection,
739
+ platform: selectedPlatform,
740
+ platformLabel: selectedLabel,
741
+ agentName,
742
+ shortName,
743
+ proxyUrl
744
+ });
745
+ setupSucceeded = true;
746
+ }
611
747
  }
612
- if (configuredPlatforms.size >= 4) {
613
- configuring = false;
614
- continue;
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
+ }
615
769
  }
616
- const another = await ask("\nWould you like to configure another agent? (y/N) ");
617
- if (another.trim().toLowerCase() !== "y") {
770
+ const another = await ask("\nConnect another agent? (Y/n) ");
771
+ if (another.trim().toLowerCase() === "n") {
618
772
  configuring = false;
619
773
  }
620
774
  }
621
775
  rl.close();
622
- process.stderr.write("\n" + style.bold(style.violet("Setup complete")) + "\n\n");
623
- const allPlatforms = ["OpenClaw", "Claude Code", "Claude Desktop", "Other MCP Agent"];
624
- for (const idx of configuredPlatforms) {
625
- process.stderr.write(` ${style.green("\u2713")} ${allPlatforms[idx - 1] ?? ""}
626
- `);
627
- }
628
- process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
629
- const blocks = [];
630
- if (configuredPlatforms.has(1)) {
631
- blocks.push(
632
- "\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"
633
- );
634
- }
635
- if (configuredPlatforms.has(2)) {
636
- blocks.push(
637
- "\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"
638
- );
639
- }
640
- if (configuredPlatforms.has(3)) {
641
- blocks.push(
642
- "\n" + style.bold("To complete your Claude Desktop setup:") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
643
- );
644
- }
645
- if (configuredPlatforms.has(4)) {
646
- blocks.push(
647
- "\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"
648
- );
776
+ if (configuredAgents.length > 0) {
777
+ process.stderr.write("\n" + style.bold(style.violet("Setup complete")) + "\n\n");
778
+ for (const agent of configuredAgents) {
779
+ process.stderr.write(
780
+ ` ${style.green("\u2713")} ${agent.platformLabel} - ${style.cyan(agent.agentName)}${agent.proxyUrl != null ? ` ${style.dim(`(${agent.proxyUrl})`)}` : ""}
781
+ `
782
+ );
783
+ }
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");
649
814
  }
650
- process.stderr.write(blocks.join("") + "\n");
651
815
  return lastConfig;
652
816
  }
653
- function isProxyConfig(value) {
654
- if (typeof value !== "object" || value === null) return false;
655
- const obj = value;
656
- return typeof obj["apiKey"] === "string" && typeof obj["baseUrl"] === "string";
657
- }
658
817
 
659
818
  // src/types/index.ts
660
819
  var PERMISSION_LEVELS = {
@@ -1340,7 +1499,6 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger, platform)
1340
1499
  const cachedScopes = await loadCachedScopes(agentName, apiKey);
1341
1500
  if (cachedScopes !== null && cachedScopes.length > 0) {
1342
1501
  logger.debug("Loaded scopes from cache.", { agent: agentName, count: cachedScopes.length });
1343
- return { id: "", name: agentName, scopes: cachedScopes };
1344
1502
  }
1345
1503
  let agent = await findAgentByName(agentName, apiKey, baseUrl);
1346
1504
  if (agent?.authInvalid) {
@@ -1357,6 +1515,10 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger, platform)
1357
1515
  return { id: "", name: agentName, scopes: [], authInvalid: true };
1358
1516
  }
1359
1517
  const detail = error instanceof Error ? error.message : String(error);
1518
+ if (cachedScopes !== null && cachedScopes.length > 0) {
1519
+ logger.warn("Service unreachable. Using cached scopes.", { error: detail });
1520
+ return { id: "", name: agentName, scopes: cachedScopes };
1521
+ }
1360
1522
  logger.warn("Could not reach Multicorn service. Running with empty permissions.", {
1361
1523
  error: detail
1362
1524
  });
@@ -1485,16 +1647,26 @@ function createProxyServer(config) {
1485
1647
  }
1486
1648
  const service = extractServiceFromToolName(toolParams.name);
1487
1649
  const action = extractActionFromToolName(toolParams.name);
1650
+ config.logger.debug("Extracted tool identity.", {
1651
+ tool: toolParams.name,
1652
+ service,
1653
+ action
1654
+ });
1488
1655
  const requestedScope = { service, permissionLevel: "execute" };
1489
1656
  const validation = validateScopeAccess(grantedScopes, requestedScope);
1490
- config.logger.debug("Tool call intercepted.", {
1657
+ config.logger.debug("Scope validation result.", {
1491
1658
  tool: toolParams.name,
1492
- service,
1493
- allowed: validation.allowed
1659
+ allowed: validation.allowed,
1660
+ scopeCount: grantedScopes.length
1494
1661
  });
1495
1662
  if (!validation.allowed) {
1496
1663
  await ensureConsent(requestedScope);
1497
1664
  const revalidation = validateScopeAccess(grantedScopes, requestedScope);
1665
+ config.logger.debug("Post-consent revalidation result.", {
1666
+ tool: toolParams.name,
1667
+ allowed: revalidation.allowed,
1668
+ scopeCount: grantedScopes.length
1669
+ });
1498
1670
  if (!revalidation.allowed) {
1499
1671
  if (actionLogger !== null) {
1500
1672
  if (!config.agentName || config.agentName.trim().length === 0) {
@@ -1502,12 +1674,18 @@ function createProxyServer(config) {
1502
1674
  "[multicorn-proxy] Cannot log action: agent name not resolved\n"
1503
1675
  );
1504
1676
  } else {
1677
+ config.logger.debug("Logging blocked action (post-consent).", {
1678
+ agent: config.agentName,
1679
+ service,
1680
+ action
1681
+ });
1505
1682
  await actionLogger.logAction({
1506
1683
  agent: config.agentName,
1507
1684
  service,
1508
1685
  actionType: action,
1509
1686
  status: "blocked"
1510
1687
  });
1688
+ config.logger.debug("Blocked action logged.", { tool: toolParams.name });
1511
1689
  }
1512
1690
  }
1513
1691
  return JSON.stringify(
@@ -1526,12 +1704,18 @@ function createProxyServer(config) {
1526
1704
  "[multicorn-proxy] Cannot log action: agent name not resolved\n"
1527
1705
  );
1528
1706
  } else {
1707
+ config.logger.debug("Logging blocked action (spending).", {
1708
+ agent: config.agentName,
1709
+ service,
1710
+ action
1711
+ });
1529
1712
  await actionLogger.logAction({
1530
1713
  agent: config.agentName,
1531
1714
  service,
1532
1715
  actionType: action,
1533
1716
  status: "blocked"
1534
1717
  });
1718
+ config.logger.debug("Spending-blocked action logged.", { tool: toolParams.name });
1535
1719
  }
1536
1720
  }
1537
1721
  const blocked = buildSpendingBlockedResponse(
@@ -1548,12 +1732,18 @@ function createProxyServer(config) {
1548
1732
  if (!config.agentName || config.agentName.trim().length === 0) {
1549
1733
  process.stderr.write("[multicorn-proxy] Cannot log action: agent name not resolved\n");
1550
1734
  } else {
1735
+ config.logger.debug("Logging approved action.", {
1736
+ agent: config.agentName,
1737
+ service,
1738
+ action
1739
+ });
1551
1740
  await actionLogger.logAction({
1552
1741
  agent: config.agentName,
1553
1742
  service,
1554
1743
  actionType: action,
1555
1744
  status: "approved"
1556
1745
  });
1746
+ config.logger.debug("Approved action logged.", { tool: toolParams.name });
1557
1747
  }
1558
1748
  }
1559
1749
  return null;
@@ -1733,10 +1923,23 @@ function parseArgs(argv) {
1733
1923
  let baseUrl = "https://api.multicorn.ai";
1734
1924
  let dashboardUrl = "";
1735
1925
  let agentName = "";
1926
+ let deleteAgentName = "";
1736
1927
  for (let i = 0; i < args.length; i++) {
1737
1928
  const arg = args[i];
1738
1929
  if (arg === "init") {
1739
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++;
1740
1943
  } else if (arg === "--wrap") {
1741
1944
  subcommand = "wrap";
1742
1945
  const next = args[i + 1];
@@ -1747,6 +1950,38 @@ function parseArgs(argv) {
1747
1950
  }
1748
1951
  wrapCommand = next;
1749
1952
  wrapArgs = args.slice(i + 2);
1953
+ const cleaned = [];
1954
+ for (let j = 0; j < wrapArgs.length; j++) {
1955
+ const token = wrapArgs[j];
1956
+ if (token === "--agent-name") {
1957
+ const value = wrapArgs[j + 1];
1958
+ if (value !== void 0) {
1959
+ agentName = value;
1960
+ j++;
1961
+ }
1962
+ } else if (token === "--log-level") {
1963
+ const value = wrapArgs[j + 1];
1964
+ if (value !== void 0 && isValidLogLevel(value)) {
1965
+ logLevel = value;
1966
+ j++;
1967
+ }
1968
+ } else if (token === "--base-url") {
1969
+ const value = wrapArgs[j + 1];
1970
+ if (value !== void 0) {
1971
+ baseUrl = value;
1972
+ j++;
1973
+ }
1974
+ } else if (token === "--dashboard-url") {
1975
+ const value = wrapArgs[j + 1];
1976
+ if (value !== void 0) {
1977
+ dashboardUrl = value;
1978
+ j++;
1979
+ }
1980
+ } else if (token !== void 0) {
1981
+ cleaned.push(token);
1982
+ }
1983
+ }
1984
+ wrapArgs = cleaned;
1750
1985
  break;
1751
1986
  } else if (arg === "--log-level") {
1752
1987
  const next = args[i + 1];
@@ -1774,7 +2009,16 @@ function parseArgs(argv) {
1774
2009
  }
1775
2010
  }
1776
2011
  }
1777
- 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
+ };
1778
2022
  }
1779
2023
  function printHelp() {
1780
2024
  process.stderr.write(
@@ -1785,6 +2029,12 @@ function printHelp() {
1785
2029
  " npx multicorn-proxy init",
1786
2030
  " Interactive setup. Saves API key to ~/.multicorn/config.json.",
1787
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
+ "",
1788
2038
  " npx multicorn-proxy --wrap <command> [args...]",
1789
2039
  " Start <command> as an MCP server and proxy all tool calls through",
1790
2040
  " Shield's permission layer.",
@@ -1814,6 +2064,40 @@ async function main() {
1814
2064
  await runInit(cli.baseUrl);
1815
2065
  return;
1816
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
+ }
1817
2101
  if (!cli.baseUrl.startsWith("https://") && !cli.baseUrl.startsWith("http://localhost") && !cli.baseUrl.startsWith("http://127.0.0.1")) {
1818
2102
  process.stderr.write(
1819
2103
  `Error: --base-url must use HTTPS. Received: "${cli.baseUrl}"
@@ -1829,9 +2113,11 @@ Use https:// or http://localhost for local development.
1829
2113
  );
1830
2114
  process.exit(1);
1831
2115
  }
1832
- 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);
1833
2117
  const finalBaseUrl = cli.baseUrl !== "https://api.multicorn.ai" ? cli.baseUrl : config.baseUrl;
1834
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";
1835
2121
  const proxy = createProxyServer({
1836
2122
  command: cli.wrapCommand,
1837
2123
  commandArgs: cli.wrapArgs,
@@ -1840,7 +2126,7 @@ Use https:// or http://localhost for local development.
1840
2126
  baseUrl: finalBaseUrl,
1841
2127
  dashboardUrl: finalDashboardUrl,
1842
2128
  logger,
1843
- platform: "other-mcp"
2129
+ platform: platformForServer
1844
2130
  });
1845
2131
  async function shutdown() {
1846
2132
  logger.info("Shutting down.");
@@ -1855,6 +2141,26 @@ Use https:// or http://localhost for local development.
1855
2141
  });
1856
2142
  await proxy.start();
1857
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
+ }
1858
2164
  function deriveAgentName(command) {
1859
2165
  const base = command.split("/").pop() ?? command;
1860
2166
  return base.replace(/\.[cm]?[jt]s$/, "");