multicorn-shield 0.2.2 → 0.4.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,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync } from 'fs';
3
- import { readFile, writeFile, mkdir, unlink } from 'fs/promises';
3
+ import { readFile, mkdir, writeFile, unlink } from 'fs/promises';
4
4
  import { join } from 'path';
5
5
  import { homedir } from 'os';
6
6
  import { createInterface } from 'readline';
@@ -45,7 +45,18 @@ 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 isProxyConfig(value) {
56
+ if (typeof value !== "object" || value === null) return false;
57
+ const obj = value;
58
+ return typeof obj["apiKey"] === "string" && typeof obj["baseUrl"] === "string";
59
+ }
49
60
  async function loadConfig() {
50
61
  try {
51
62
  const raw = await readFile(CONFIG_PATH, "utf8");
@@ -63,113 +74,6 @@ async function saveConfig(config) {
63
74
  mode: 384
64
75
  });
65
76
  }
66
- function isErrnoException(e) {
67
- return typeof e === "object" && e !== null && "code" in e;
68
- }
69
- async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
70
- let raw;
71
- try {
72
- raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
73
- } catch (e) {
74
- if (isErrnoException(e) && e.code === "ENOENT") {
75
- return "not-found";
76
- }
77
- throw e;
78
- }
79
- let obj;
80
- try {
81
- obj = JSON.parse(raw);
82
- } catch {
83
- return "parse-error";
84
- }
85
- let hooks = obj["hooks"];
86
- if (hooks === void 0 || typeof hooks !== "object") {
87
- hooks = {};
88
- obj["hooks"] = hooks;
89
- }
90
- let internal = hooks["internal"];
91
- if (internal === void 0 || typeof internal !== "object") {
92
- internal = { enabled: true, entries: {} };
93
- hooks["internal"] = internal;
94
- }
95
- let entries = internal["entries"];
96
- if (entries === void 0 || typeof entries !== "object") {
97
- entries = {};
98
- internal["entries"] = entries;
99
- }
100
- let shield = entries["multicorn-shield"];
101
- if (shield === void 0 || typeof shield !== "object") {
102
- shield = { enabled: true, env: {} };
103
- entries["multicorn-shield"] = shield;
104
- }
105
- let env = shield["env"];
106
- if (env === void 0 || typeof env !== "object") {
107
- env = {};
108
- shield["env"] = env;
109
- }
110
- env["MULTICORN_API_KEY"] = apiKey;
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
- }
130
- await writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n", {
131
- encoding: "utf8"
132
- });
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;
172
- }
173
77
  async function validateApiKey(apiKey, baseUrl) {
174
78
  try {
175
79
  const response = await fetch(`${baseUrl}/api/v1/agents`, {
@@ -194,9 +98,6 @@ async function validateApiKey(apiKey, baseUrl) {
194
98
  };
195
99
  }
196
100
  }
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
101
  async function isOpenClawConnected() {
201
102
  try {
202
103
  const raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
@@ -219,84 +120,227 @@ function isClaudeCodeConnected() {
219
120
  return false;
220
121
  }
221
122
  }
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";
123
+ function getCursorConfigPath() {
124
+ return join(homedir(), ".cursor", "mcp.json");
283
125
  }
284
- async function isClaudeDesktopConnected() {
126
+ async function isCursorConnected() {
285
127
  try {
286
- const raw = await readFile(getClaudeDesktopConfigPath(), "utf8");
128
+ const raw = await readFile(getCursorConfigPath(), "utf8");
287
129
  const obj = JSON.parse(raw);
288
130
  const mcpServers = obj["mcpServers"];
289
131
  if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
290
132
  for (const entry of Object.values(mcpServers)) {
291
133
  if (typeof entry !== "object" || entry === null) continue;
292
- const args = entry["args"];
134
+ const rec = entry;
135
+ const url = rec["url"];
136
+ if (typeof url === "string" && url.includes("multicorn")) return true;
137
+ const args = rec["args"];
293
138
  if (Array.isArray(args) && args.includes("multicorn-proxy")) return true;
294
139
  }
295
140
  return false;
296
- } catch {
141
+ } catch (err) {
142
+ process.stderr.write(
143
+ `Warning: could not check Cursor connection status: ${err instanceof Error ? err.message : String(err)}
144
+ `
145
+ );
297
146
  return false;
298
147
  }
299
148
  }
149
+ var PLATFORM_LABELS = ["OpenClaw", "Claude Code", "Cursor"];
150
+ var PLATFORM_BY_SELECTION = {
151
+ 1: "openclaw",
152
+ 2: "claude-code",
153
+ 3: "cursor"
154
+ };
155
+ var DEFAULT_AGENT_NAMES = {
156
+ openclaw: "my-openclaw-agent",
157
+ "claude-code": "my-claude-code-agent",
158
+ cursor: "my-cursor-agent"
159
+ };
160
+ async function promptPlatformSelection(ask) {
161
+ process.stderr.write(
162
+ "\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n"
163
+ );
164
+ const connectedFlags = [
165
+ await isOpenClawConnected(),
166
+ isClaudeCodeConnected(),
167
+ await isCursorConnected()
168
+ ];
169
+ for (let i = 0; i < PLATFORM_LABELS.length; i++) {
170
+ const marker = connectedFlags[i] ? " " + style.green("\u2713") + style.dim(" connected") : "";
171
+ process.stderr.write(
172
+ ` ${style.violet(String(i + 1))}. ${PLATFORM_LABELS[i] ?? ""}${marker}
173
+ `
174
+ );
175
+ }
176
+ let selection = 0;
177
+ while (selection === 0) {
178
+ const input = await ask("Select (1-3): ");
179
+ const num = parseInt(input.trim(), 10);
180
+ if (num >= 1 && num <= 3) {
181
+ selection = num;
182
+ }
183
+ }
184
+ return selection;
185
+ }
186
+ async function promptAgentName(ask, platform) {
187
+ const defaultAgentName = DEFAULT_AGENT_NAMES[platform] ?? "my-agent";
188
+ let agentName = "";
189
+ while (agentName.length === 0) {
190
+ const input = await ask(
191
+ `
192
+ What would you like to call this agent? ${style.dim(`(${defaultAgentName})`)} `
193
+ );
194
+ const raw = input.trim().length > 0 ? input.trim() : defaultAgentName;
195
+ const transformed = normalizeAgentName(raw);
196
+ if (transformed.length === 0) {
197
+ process.stderr.write(
198
+ style.red("Agent name must contain letters or numbers. Please try again.") + "\n"
199
+ );
200
+ continue;
201
+ }
202
+ if (transformed !== raw) {
203
+ process.stderr.write(style.yellow("Agent name set to: ") + style.cyan(transformed) + "\n");
204
+ }
205
+ agentName = transformed;
206
+ }
207
+ return agentName;
208
+ }
209
+ async function promptProxyConfig(ask, agentName) {
210
+ let targetUrl = "";
211
+ while (targetUrl.length === 0) {
212
+ process.stderr.write(
213
+ "\n" + style.bold("Target MCP server URL:") + "\n" + style.dim(
214
+ "The URL of the MCP server you want Shield to protect. Example: https://your-server.example.com/mcp"
215
+ ) + "\n"
216
+ );
217
+ const input = await ask("URL: ");
218
+ if (input.trim().length === 0) {
219
+ process.stderr.write(style.red("MCP server URL is required.") + "\n");
220
+ continue;
221
+ }
222
+ try {
223
+ new URL(input.trim());
224
+ } catch {
225
+ process.stderr.write(
226
+ style.red(
227
+ "\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)."
228
+ ) + "\n"
229
+ );
230
+ continue;
231
+ }
232
+ targetUrl = input.trim();
233
+ }
234
+ const defaultShortName = normalizeAgentName(agentName) || "shield-mcp";
235
+ const shortNameInput = await ask(
236
+ `
237
+ Short name (a nickname for this connection, used in your proxy URL): ${style.dim(`(${defaultShortName})`)} `
238
+ );
239
+ const shortName = shortNameInput.trim().length > 0 ? normalizeAgentName(shortNameInput.trim()) || defaultShortName : defaultShortName;
240
+ return { targetUrl, shortName };
241
+ }
242
+ async function createProxyConfig(baseUrl, apiKey, agentName, targetUrl, serverName, platform) {
243
+ let response;
244
+ try {
245
+ response = await fetch(`${baseUrl}/api/v1/proxy/config`, {
246
+ method: "POST",
247
+ headers: {
248
+ "Content-Type": "application/json",
249
+ "X-Multicorn-Key": apiKey
250
+ },
251
+ body: JSON.stringify({
252
+ server_name: serverName,
253
+ target_url: targetUrl,
254
+ platform,
255
+ agent_name: agentName
256
+ }),
257
+ signal: AbortSignal.timeout(1e4)
258
+ });
259
+ } catch (error) {
260
+ const detail = error instanceof Error ? error.message : String(error);
261
+ throw new Error(`Failed to create proxy config: ${detail}`);
262
+ }
263
+ if (!response.ok) {
264
+ let errorMsg = `Shield API returned an error (HTTP ${String(response.status)}). Check your agent name and target URL, then try again.`;
265
+ try {
266
+ const errBody = await response.json();
267
+ const errObj = errBody["error"];
268
+ if (typeof errObj?.["message"] === "string") {
269
+ errorMsg = stripAnsi(errObj["message"]);
270
+ } else if (typeof errBody["message"] === "string") {
271
+ errorMsg = stripAnsi(errBody["message"]);
272
+ } else if (typeof errBody["detail"] === "string") {
273
+ errorMsg = stripAnsi(errBody["detail"]);
274
+ }
275
+ } catch {
276
+ }
277
+ throw new Error(errorMsg);
278
+ }
279
+ const envelope = await response.json();
280
+ const data = envelope["data"];
281
+ return typeof data?.["proxy_url"] === "string" ? data["proxy_url"] : "";
282
+ }
283
+ function printPlatformSnippet(platform, routingToken, shortName) {
284
+ const mcpSnippet = JSON.stringify(
285
+ {
286
+ mcpServers: {
287
+ [shortName]: {
288
+ url: routingToken,
289
+ headers: {
290
+ Authorization: "Bearer YOUR_SHIELD_API_KEY"
291
+ }
292
+ }
293
+ }
294
+ },
295
+ null,
296
+ 2
297
+ );
298
+ if (platform === "openclaw") {
299
+ process.stderr.write("\n" + style.dim("Add this to your OpenClaw agent config:") + "\n\n");
300
+ } else if (platform === "claude-code") {
301
+ process.stderr.write("\n" + style.dim("Add this to your Claude Code MCP config:") + "\n\n");
302
+ } else {
303
+ process.stderr.write("\n" + style.dim("Add this to ~/.cursor/mcp.json:") + "\n\n");
304
+ }
305
+ 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
+ );
311
+ if (platform === "cursor") {
312
+ process.stderr.write(
313
+ style.dim(
314
+ "Then restart Cursor and check Settings > Tools & MCPs for a green status indicator."
315
+ ) + "\n"
316
+ );
317
+ process.stderr.write(
318
+ style.dim(
319
+ `Ask Cursor to use your MCP server by its short name. For example: "use the ${shortName} tool to list files in /tmp"`
320
+ ) + "\n"
321
+ );
322
+ }
323
+ }
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
+ }
300
344
  async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
301
345
  if (!process.stdin.isTTY) {
302
346
  process.stderr.write(
@@ -305,20 +349,15 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
305
349
  process.exit(1);
306
350
  }
307
351
  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
- }
352
+ const ask = (question) => new Promise((resolve) => {
353
+ rl.question(question, resolve);
354
+ });
315
355
  process.stderr.write("\n" + BANNER + "\n");
316
356
  process.stderr.write(style.dim("Agent governance for the AI era") + "\n\n");
317
357
  process.stderr.write(style.bold(style.violet("Multicorn Shield proxy setup")) + "\n\n");
318
358
  process.stderr.write(
319
359
  style.dim("Get your API key at https://app.multicorn.ai/settings/api-keys") + "\n\n"
320
360
  );
321
- let apiKey = "";
322
361
  const existing = await loadConfig().catch(() => null);
323
362
  if (baseUrl === "https://api.multicorn.ai") {
324
363
  if (existing !== null && existing.baseUrl.length > 0) {
@@ -330,6 +369,7 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
330
369
  }
331
370
  }
332
371
  }
372
+ let apiKey = "";
333
373
  if (existing !== null && existing.apiKey.startsWith("mcs_") && existing.apiKey.length >= 8) {
334
374
  const masked = "mcs_..." + existing.apiKey.slice(-4);
335
375
  process.stderr.write("Found existing API key: " + style.cyan(masked) + "\n");
@@ -360,7 +400,14 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
360
400
  spinner.stop(true, "Key validated");
361
401
  apiKey = key;
362
402
  }
363
- const configuredPlatforms = /* @__PURE__ */ new Set();
403
+ if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost") && !baseUrl.startsWith("http://127.0.0.1")) {
404
+ process.stderr.write(
405
+ style.red(`\u2717 Shield API base URL must use HTTPS. Got: ${baseUrl}`) + "\n"
406
+ );
407
+ rl.close();
408
+ return null;
409
+ }
410
+ const configuredAgents = [];
364
411
  let lastConfig = {
365
412
  apiKey,
366
413
  baseUrl,
@@ -368,239 +415,55 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
368
415
  };
369
416
  let configuring = true;
370
417
  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
- }
390
- process.stderr.write(
391
- ` ${style.violet(String(i + 1))}. ${platformLabels[i] ?? ""}${sessionMarker}${connectedMarker}
392
- `
393
- );
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
- );
412
- continue;
413
- }
414
- if (transformed !== input.trim()) {
415
- process.stderr.write(style.yellow("Agent name set to: ") + style.cyan(transformed) + "\n");
416
- }
417
- agentName = transformed;
418
- }
418
+ const selection = await promptPlatformSelection(ask);
419
+ const selectedPlatform = PLATFORM_BY_SELECTION[selection] ?? "cursor";
420
+ const selectedLabel = PLATFORM_LABELS[selection - 1] ?? "Cursor";
421
+ const agentName = await promptAgentName(ask, selectedPlatform);
419
422
  if (selection === 1) {
420
- let detection;
421
- try {
422
- detection = await detectOpenClaw();
423
- } catch (error) {
424
- const detail = error instanceof Error ? error.message : String(error);
425
- process.stderr.write(style.red("\u2717") + ` Failed to read OpenClaw config: ${detail}
426
- `);
427
- rl.close();
428
- return null;
429
- }
430
- if (detection.status === "not-found") {
431
- process.stderr.write(
432
- style.red("\u2717") + " OpenClaw is not installed. Install OpenClaw first, then run npx multicorn-proxy init again.\n"
433
- );
434
- rl.close();
435
- return null;
436
- }
437
- if (detection.status === "parse-error") {
438
- process.stderr.write(
439
- style.red("\u2717") + " Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually.\n"
440
- );
441
- }
442
- if (detection.status === "detected") {
443
- if (detection.version !== null) {
444
- process.stderr.write(
445
- style.green("\u2713") + ` OpenClaw detected ${style.dim(`(${detection.version})`)}
446
- `
447
- );
448
- if (isVersionAtLeast(detection.version, OPENCLAW_MIN_VERSION)) {
449
- process.stderr.write(
450
- style.green("\u2713") + " " + style.green("Version compatible") + "\n"
451
- );
452
- } else {
453
- process.stderr.write(
454
- 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)}.
455
- `
456
- );
457
- const answer = await ask("Continue anyway? (y/N) ");
458
- if (answer.trim().toLowerCase() !== "y") {
459
- rl.close();
460
- return null;
461
- }
462
- }
463
- } else {
464
- process.stderr.write(
465
- style.yellow("\u26A0") + " Could not detect OpenClaw version. Continuing anyway.\n"
466
- );
467
- }
468
- const spinner = withSpinner("Updating OpenClaw config...");
469
- try {
470
- const result = await updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName);
471
- if (result === "not-found") {
472
- spinner.stop(false, "OpenClaw config disappeared unexpectedly.");
473
- rl.close();
474
- return null;
475
- }
476
- if (result === "parse-error") {
477
- spinner.stop(
478
- false,
479
- "Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually."
480
- );
481
- } else {
482
- spinner.stop(
483
- true,
484
- "OpenClaw config updated at " + style.cyan("~/.openclaw/openclaw.json")
485
- );
486
- }
487
- } catch (error) {
488
- const detail = error instanceof Error ? error.message : String(error);
489
- spinner.stop(false, `Failed to update OpenClaw config: ${detail}`);
490
- }
491
- }
423
+ printOpenClawInstructions();
424
+ configuredAgents.push({ platformLabel: selectedLabel, agentName });
492
425
  } else if (selection === 2) {
493
- process.stderr.write("\nTo connect Claude Code to Shield:\n\n");
494
- process.stderr.write(
495
- " " + style.bold("Step 1") + " - Add the Multicorn marketplace:\n " + style.cyan("claude plugin marketplace add Multicorn-AI/multicorn-shield") + "\n\n"
496
- );
497
- process.stderr.write(
498
- " " + style.bold("Step 2") + " - Install the plugin:\n " + style.cyan("claude plugin install multicorn-shield@multicorn-shield") + "\n\n"
499
- );
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
- process.stderr.write(
507
- style.dim("Requires Claude Code to be installed. Get it at https://code.claude.com") + "\n"
508
- );
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...");
426
+ printClaudeCodeInstructions();
427
+ configuredAgents.push({ platformLabel: selectedLabel, agentName });
428
+ } else {
429
+ const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
430
+ let proxyUrl = "";
431
+ let created = false;
432
+ while (!created) {
433
+ const spinner = withSpinner("Creating proxy config...");
539
434
  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
- }
435
+ proxyUrl = await createProxyConfig(
436
+ baseUrl,
437
+ apiKey,
438
+ agentName,
439
+ targetUrl,
440
+ shortName,
441
+ selectedPlatform
442
+ );
443
+ spinner.stop(true, "Proxy config created!");
444
+ created = true;
588
445
  } catch (error) {
589
446
  const detail = error instanceof Error ? error.message : String(error);
590
- spinner.stop(false, `Failed to update Claude Desktop config: ${detail}`);
591
- shouldWrite = false;
447
+ spinner.stop(false, detail);
448
+ const retry = await ask("Try again? (Y/n) ");
449
+ if (retry.trim().toLowerCase() === "n") {
450
+ break;
451
+ }
592
452
  }
593
453
  }
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
- );
454
+ if (created && proxyUrl.length > 0) {
455
+ process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
456
+ process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
457
+ printPlatformSnippet(selectedPlatform, proxyUrl, shortName);
458
+ configuredAgents.push({
459
+ platformLabel: selectedLabel,
460
+ agentName,
461
+ shortName,
462
+ proxyUrl
463
+ });
464
+ }
601
465
  }
602
- configuredPlatforms.add(selection);
603
- lastConfig = { apiKey, baseUrl, agentName, ...{} };
466
+ lastConfig = { apiKey, baseUrl, agentName, platform: selectedPlatform };
604
467
  try {
605
468
  await saveConfig(lastConfig);
606
469
  process.stderr.write(style.green("\u2713") + ` Config saved to ${style.cyan(CONFIG_PATH)}
@@ -609,52 +472,24 @@ async function runInit(baseUrl = "https://api.multicorn.ai", platform) {
609
472
  const detail = error instanceof Error ? error.message : String(error);
610
473
  process.stderr.write(style.red(`Failed to save config: ${detail}`) + "\n");
611
474
  }
612
- if (configuredPlatforms.size >= 4) {
613
- configuring = false;
614
- continue;
615
- }
616
- const another = await ask("\nWould you like to configure another agent? (y/N) ");
617
- if (another.trim().toLowerCase() !== "y") {
475
+ const another = await ask("\nConnect another agent? (Y/n) ");
476
+ if (another.trim().toLowerCase() === "n") {
618
477
  configuring = false;
619
478
  }
620
479
  }
621
480
  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
- );
481
+ if (configuredAgents.length > 0) {
482
+ process.stderr.write("\n" + style.bold(style.violet("Setup complete")) + "\n\n");
483
+ for (const agent of configuredAgents) {
484
+ process.stderr.write(
485
+ ` ${style.green("\u2713")} ${agent.platformLabel} - ${style.cyan(agent.agentName)}${agent.proxyUrl != null ? ` ${style.dim(`(${agent.proxyUrl})`)}` : ""}
486
+ `
487
+ );
488
+ }
489
+ process.stderr.write("\n");
649
490
  }
650
- process.stderr.write(blocks.join("") + "\n");
651
491
  return lastConfig;
652
492
  }
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
493
 
659
494
  // src/types/index.ts
660
495
  var PERMISSION_LEVELS = {
@@ -1340,7 +1175,6 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger, platform)
1340
1175
  const cachedScopes = await loadCachedScopes(agentName, apiKey);
1341
1176
  if (cachedScopes !== null && cachedScopes.length > 0) {
1342
1177
  logger.debug("Loaded scopes from cache.", { agent: agentName, count: cachedScopes.length });
1343
- return { id: "", name: agentName, scopes: cachedScopes };
1344
1178
  }
1345
1179
  let agent = await findAgentByName(agentName, apiKey, baseUrl);
1346
1180
  if (agent?.authInvalid) {
@@ -1357,6 +1191,10 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger, platform)
1357
1191
  return { id: "", name: agentName, scopes: [], authInvalid: true };
1358
1192
  }
1359
1193
  const detail = error instanceof Error ? error.message : String(error);
1194
+ if (cachedScopes !== null && cachedScopes.length > 0) {
1195
+ logger.warn("Service unreachable. Using cached scopes.", { error: detail });
1196
+ return { id: "", name: agentName, scopes: cachedScopes };
1197
+ }
1360
1198
  logger.warn("Could not reach Multicorn service. Running with empty permissions.", {
1361
1199
  error: detail
1362
1200
  });
@@ -1485,16 +1323,26 @@ function createProxyServer(config) {
1485
1323
  }
1486
1324
  const service = extractServiceFromToolName(toolParams.name);
1487
1325
  const action = extractActionFromToolName(toolParams.name);
1326
+ config.logger.debug("Extracted tool identity.", {
1327
+ tool: toolParams.name,
1328
+ service,
1329
+ action
1330
+ });
1488
1331
  const requestedScope = { service, permissionLevel: "execute" };
1489
1332
  const validation = validateScopeAccess(grantedScopes, requestedScope);
1490
- config.logger.debug("Tool call intercepted.", {
1333
+ config.logger.debug("Scope validation result.", {
1491
1334
  tool: toolParams.name,
1492
- service,
1493
- allowed: validation.allowed
1335
+ allowed: validation.allowed,
1336
+ scopeCount: grantedScopes.length
1494
1337
  });
1495
1338
  if (!validation.allowed) {
1496
1339
  await ensureConsent(requestedScope);
1497
1340
  const revalidation = validateScopeAccess(grantedScopes, requestedScope);
1341
+ config.logger.debug("Post-consent revalidation result.", {
1342
+ tool: toolParams.name,
1343
+ allowed: revalidation.allowed,
1344
+ scopeCount: grantedScopes.length
1345
+ });
1498
1346
  if (!revalidation.allowed) {
1499
1347
  if (actionLogger !== null) {
1500
1348
  if (!config.agentName || config.agentName.trim().length === 0) {
@@ -1502,12 +1350,18 @@ function createProxyServer(config) {
1502
1350
  "[multicorn-proxy] Cannot log action: agent name not resolved\n"
1503
1351
  );
1504
1352
  } else {
1353
+ config.logger.debug("Logging blocked action (post-consent).", {
1354
+ agent: config.agentName,
1355
+ service,
1356
+ action
1357
+ });
1505
1358
  await actionLogger.logAction({
1506
1359
  agent: config.agentName,
1507
1360
  service,
1508
1361
  actionType: action,
1509
1362
  status: "blocked"
1510
1363
  });
1364
+ config.logger.debug("Blocked action logged.", { tool: toolParams.name });
1511
1365
  }
1512
1366
  }
1513
1367
  return JSON.stringify(
@@ -1526,12 +1380,18 @@ function createProxyServer(config) {
1526
1380
  "[multicorn-proxy] Cannot log action: agent name not resolved\n"
1527
1381
  );
1528
1382
  } else {
1383
+ config.logger.debug("Logging blocked action (spending).", {
1384
+ agent: config.agentName,
1385
+ service,
1386
+ action
1387
+ });
1529
1388
  await actionLogger.logAction({
1530
1389
  agent: config.agentName,
1531
1390
  service,
1532
1391
  actionType: action,
1533
1392
  status: "blocked"
1534
1393
  });
1394
+ config.logger.debug("Spending-blocked action logged.", { tool: toolParams.name });
1535
1395
  }
1536
1396
  }
1537
1397
  const blocked = buildSpendingBlockedResponse(
@@ -1548,12 +1408,18 @@ function createProxyServer(config) {
1548
1408
  if (!config.agentName || config.agentName.trim().length === 0) {
1549
1409
  process.stderr.write("[multicorn-proxy] Cannot log action: agent name not resolved\n");
1550
1410
  } else {
1411
+ config.logger.debug("Logging approved action.", {
1412
+ agent: config.agentName,
1413
+ service,
1414
+ action
1415
+ });
1551
1416
  await actionLogger.logAction({
1552
1417
  agent: config.agentName,
1553
1418
  service,
1554
1419
  actionType: action,
1555
1420
  status: "approved"
1556
1421
  });
1422
+ config.logger.debug("Approved action logged.", { tool: toolParams.name });
1557
1423
  }
1558
1424
  }
1559
1425
  return null;
@@ -1747,6 +1613,38 @@ function parseArgs(argv) {
1747
1613
  }
1748
1614
  wrapCommand = next;
1749
1615
  wrapArgs = args.slice(i + 2);
1616
+ const cleaned = [];
1617
+ for (let j = 0; j < wrapArgs.length; j++) {
1618
+ const token = wrapArgs[j];
1619
+ if (token === "--agent-name") {
1620
+ const value = wrapArgs[j + 1];
1621
+ if (value !== void 0) {
1622
+ agentName = value;
1623
+ j++;
1624
+ }
1625
+ } else if (token === "--log-level") {
1626
+ const value = wrapArgs[j + 1];
1627
+ if (value !== void 0 && isValidLogLevel(value)) {
1628
+ logLevel = value;
1629
+ j++;
1630
+ }
1631
+ } else if (token === "--base-url") {
1632
+ const value = wrapArgs[j + 1];
1633
+ if (value !== void 0) {
1634
+ baseUrl = value;
1635
+ j++;
1636
+ }
1637
+ } else if (token === "--dashboard-url") {
1638
+ const value = wrapArgs[j + 1];
1639
+ if (value !== void 0) {
1640
+ dashboardUrl = value;
1641
+ j++;
1642
+ }
1643
+ } else if (token !== void 0) {
1644
+ cleaned.push(token);
1645
+ }
1646
+ }
1647
+ wrapArgs = cleaned;
1750
1648
  break;
1751
1649
  } else if (arg === "--log-level") {
1752
1650
  const next = args[i + 1];
@@ -1840,7 +1738,7 @@ Use https:// or http://localhost for local development.
1840
1738
  baseUrl: finalBaseUrl,
1841
1739
  dashboardUrl: finalDashboardUrl,
1842
1740
  logger,
1843
- platform: "other-mcp"
1741
+ platform: config.platform ?? "other-mcp"
1844
1742
  });
1845
1743
  async function shutdown() {
1846
1744
  logger.info("Shutting down.");