multicorn-shield 1.11.0 → 1.12.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.
@@ -644,7 +644,34 @@ function isVersionAtLeast(version, minimum) {
644
644
  }
645
645
  return true;
646
646
  }
647
- async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
647
+ function buildOpenClawShieldPluginConfig(params) {
648
+ return {
649
+ apiKey: OPENCLAW_SHIELD_API_KEY_ENV_REF,
650
+ baseUrl: params.baseUrl,
651
+ agentName: params.agentName,
652
+ failMode: "closed"
653
+ };
654
+ }
655
+ function detectStaleOpenClawHookIntegration(openclawConfig) {
656
+ const hooks = openclawConfig["hooks"];
657
+ if (typeof hooks !== "object" || hooks === null) return false;
658
+ const internal = hooks["internal"];
659
+ if (typeof internal !== "object" || internal === null) return false;
660
+ const entries = internal["entries"];
661
+ if (typeof entries !== "object" || entries === null) return false;
662
+ return Object.prototype.hasOwnProperty.call(entries, "multicorn-shield");
663
+ }
664
+ function staleOpenClawHookIntegrationMessage() {
665
+ return "Deprecated OpenClaw gateway hook config detected at hooks.internal.entries.multicorn-shield. Shield now uses the multicorn-shield plugin under plugins.entries. Disable the old hook manually (for example openclaw hooks disable multicorn-shield) and remove hooks.internal.entries.multicorn-shield when you are ready.";
666
+ }
667
+ function stripLegacyOpenClawShieldEntryFields(shield) {
668
+ delete shield["env"];
669
+ delete shield["apiKey"];
670
+ delete shield["baseUrl"];
671
+ delete shield["agentName"];
672
+ delete shield["failMode"];
673
+ }
674
+ async function updateOpenClawConfigIfPresent(_apiKey, baseUrl, agentName) {
648
675
  let raw;
649
676
  try {
650
677
  raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
@@ -660,50 +687,67 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
660
687
  } catch {
661
688
  return "parse-error";
662
689
  }
663
- let hooks = obj["hooks"];
664
- if (hooks === void 0 || typeof hooks !== "object") {
665
- hooks = {};
666
- obj["hooks"] = hooks;
690
+ if (detectStaleOpenClawHookIntegration(obj)) {
691
+ process.stderr.write(
692
+ style.yellow("\u26A0") + " " + staleOpenClawHookIntegrationMessage() + "\n"
693
+ );
667
694
  }
668
- let internal = hooks["internal"];
669
- if (internal === void 0 || typeof internal !== "object") {
670
- internal = { enabled: true, entries: {} };
671
- hooks["internal"] = internal;
695
+ let plugins = obj["plugins"];
696
+ if (plugins === void 0 || typeof plugins !== "object") {
697
+ plugins = {};
698
+ obj["plugins"] = plugins;
672
699
  }
673
- let entries = internal["entries"];
674
- if (entries === void 0 || typeof entries !== "object") {
675
- entries = {};
676
- internal["entries"] = entries;
700
+ let pluginEntries = plugins["entries"];
701
+ if (pluginEntries === void 0 || typeof pluginEntries !== "object") {
702
+ pluginEntries = {};
703
+ plugins["entries"] = pluginEntries;
677
704
  }
678
- let shield = entries["multicorn-shield"];
705
+ let shield = pluginEntries["multicorn-shield"];
679
706
  if (shield === void 0 || typeof shield !== "object") {
680
- shield = { enabled: true, env: {} };
681
- entries["multicorn-shield"] = shield;
682
- }
683
- let env = shield["env"];
684
- if (env === void 0 || typeof env !== "object") {
685
- env = {};
686
- shield["env"] = env;
687
- }
688
- env["MULTICORN_API_KEY"] = apiKey;
689
- env["MULTICORN_BASE_URL"] = baseUrl;
690
- if (agentName !== void 0) {
691
- env["MULTICORN_AGENT_NAME"] = agentName;
707
+ shield = { enabled: true };
708
+ pluginEntries["multicorn-shield"] = shield;
709
+ }
710
+ stripLegacyOpenClawShieldEntryFields(shield);
711
+ const resolvedAgentName = agentName ?? (typeof shield["config"] === "object" && shield["config"] !== null && typeof shield["config"]["agentName"] === "string" ? String(shield["config"]["agentName"]) : void 0);
712
+ if (resolvedAgentName !== void 0) {
713
+ shield["enabled"] = true;
714
+ shield["config"] = buildOpenClawShieldPluginConfig({
715
+ baseUrl,
716
+ agentName: resolvedAgentName
717
+ });
692
718
  const agentsList = obj["agents"];
693
719
  const list = agentsList?.["list"];
694
720
  if (Array.isArray(list) && list.length > 0) {
695
721
  const first = list[0];
696
- if (first["id"] !== agentName) {
697
- first["id"] = agentName;
698
- first["name"] = agentName;
722
+ if (first["id"] !== resolvedAgentName) {
723
+ first["id"] = resolvedAgentName;
724
+ first["name"] = resolvedAgentName;
699
725
  }
700
726
  } else {
701
727
  if (agentsList !== void 0 && typeof agentsList === "object") {
702
- agentsList["list"] = [{ id: agentName, name: agentName }];
728
+ agentsList["list"] = [{ id: resolvedAgentName, name: resolvedAgentName }];
703
729
  } else {
704
- obj["agents"] = { list: [{ id: agentName, name: agentName }] };
730
+ obj["agents"] = { list: [{ id: resolvedAgentName, name: resolvedAgentName }] };
705
731
  }
706
732
  }
733
+ } else {
734
+ shield["enabled"] = true;
735
+ const existingConfig = shield["config"];
736
+ if (typeof existingConfig === "object" && existingConfig !== null) {
737
+ const cfg = { ...existingConfig };
738
+ cfg["apiKey"] = OPENCLAW_SHIELD_API_KEY_ENV_REF;
739
+ cfg["baseUrl"] = baseUrl;
740
+ if (cfg["failMode"] !== "open" && cfg["failMode"] !== "closed") {
741
+ cfg["failMode"] = "closed";
742
+ }
743
+ shield["config"] = cfg;
744
+ } else {
745
+ shield["config"] = {
746
+ apiKey: OPENCLAW_SHIELD_API_KEY_ENV_REF,
747
+ baseUrl,
748
+ failMode: "closed"
749
+ };
750
+ }
707
751
  }
708
752
  await writeFile(
709
753
  OPENCLAW_CONFIG_PATH,
@@ -712,6 +756,10 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
712
756
  );
713
757
  return "updated";
714
758
  }
759
+ function isPastedShieldApiKeyInput(input) {
760
+ const trimmed = input.trim();
761
+ return trimmed.startsWith("mcs_") && trimmed.length >= 8;
762
+ }
715
763
  async function validateApiKey(apiKey, baseUrl) {
716
764
  try {
717
765
  const response = await fetch(`${baseUrl}/api/v1/agents`, {
@@ -1081,40 +1129,6 @@ async function installCodexCliNativeHooks() {
1081
1129
  };
1082
1130
  await writeFile(getCodexHooksJsonPath(), JSON.stringify(hooksConfig, null, 2) + "\n", "utf8");
1083
1131
  }
1084
- async function promptCodexCliIntegrationMode(ask) {
1085
- process.stderr.write("\n" + style.bold("Codex CLI integration") + "\n");
1086
- process.stderr.write(
1087
- " " + style.violet("1") + ". Native plugin - Shield checks terminal (Bash) commands via Codex Hooks\n"
1088
- );
1089
- process.stderr.write(
1090
- " " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via config.toml\n"
1091
- );
1092
- let choice = 0;
1093
- while (choice === 0) {
1094
- const input = await ask("Choose integration (1-2): ");
1095
- const num = parseInt(input.trim(), 10);
1096
- if (num === 1) choice = 1;
1097
- if (num === 2) choice = 2;
1098
- }
1099
- return choice === 1 ? "native" : "hosted";
1100
- }
1101
- async function promptClineIntegrationMode(ask) {
1102
- process.stderr.write("\n" + style.bold("Cline integration") + "\n");
1103
- process.stderr.write(
1104
- " " + style.violet("1") + ". Native plugin (recommended) - Cline Hooks see every file, terminal, browser, and MCP action\n"
1105
- );
1106
- process.stderr.write(
1107
- " " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Cline MCP settings)\n"
1108
- );
1109
- let choice = 0;
1110
- while (choice === 0) {
1111
- const input = await ask("Choose integration (1-2): ");
1112
- const num = parseInt(input.trim(), 10);
1113
- if (num === 1) choice = 1;
1114
- if (num === 2) choice = 2;
1115
- }
1116
- return choice === 1 ? "native" : "hosted";
1117
- }
1118
1132
  function getGeminiCliHooksInstallDir() {
1119
1133
  return join(homedir(), ".multicorn", "gemini-cli-hooks");
1120
1134
  }
@@ -1440,40 +1454,6 @@ async function installGeminiCliNativeHooks(ask) {
1440
1454
  await mkdir(dirname(settingsPath), { recursive: true });
1441
1455
  await writeFile(settingsPath, JSON.stringify(existing, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1442
1456
  }
1443
- async function promptGeminiCliIntegrationMode(ask) {
1444
- process.stderr.write("\n" + style.bold("Gemini CLI integration") + "\n");
1445
- process.stderr.write(
1446
- " " + style.violet("1") + ". Native plugin (recommended) - Gemini CLI Hooks see every file, terminal, web, and MCP action\n"
1447
- );
1448
- process.stderr.write(
1449
- " " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Gemini CLI settings)\n"
1450
- );
1451
- let choice = 0;
1452
- while (choice === 0) {
1453
- const input = await ask("Choose integration (1-2): ");
1454
- const num = parseInt(input.trim(), 10);
1455
- if (num === 1) choice = 1;
1456
- if (num === 2) choice = 2;
1457
- }
1458
- return choice === 1 ? "native" : "hosted";
1459
- }
1460
- async function promptOpencodeIntegrationMode(ask) {
1461
- process.stderr.write("\n" + style.bold("OpenCode integration") + "\n");
1462
- process.stderr.write(
1463
- " " + style.violet("1") + ". Native plugin (recommended) - Shield checks primary-agent tool execution via OpenCode Hooks\n"
1464
- );
1465
- process.stderr.write(
1466
- " " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via opencode.json (full subagent coverage when tools use MCP through Shield)\n"
1467
- );
1468
- let choice = 0;
1469
- while (choice === 0) {
1470
- const input = await ask("Choose integration (1-2): ");
1471
- const num = parseInt(input.trim(), 10);
1472
- if (num === 1) choice = 1;
1473
- if (num === 2) choice = 2;
1474
- }
1475
- return choice === 1 ? "native" : "hosted";
1476
- }
1477
1457
  function getClaudeDesktopConfigPath() {
1478
1458
  switch (process.platform) {
1479
1459
  case "win32":
@@ -1537,8 +1517,11 @@ function getClineMcpSettingsPath() {
1537
1517
  );
1538
1518
  }
1539
1519
  }
1540
- function platformMenuLabelForSelection(sel) {
1541
- const slug = PLATFORM_BY_SELECTION[sel];
1520
+ function resolveInitHybridIntegrationMode(explicit) {
1521
+ return explicit ?? "native";
1522
+ }
1523
+ function platformMenuLabelForPickerSelection(sel) {
1524
+ const slug = PICKER_SLUG_BY_SELECTION[sel];
1542
1525
  if (slug === void 0) return "Unknown";
1543
1526
  const entry = INIT_WIZARD_PLATFORM_REGISTRY.find((e) => e.slug === slug);
1544
1527
  return entry?.displayName ?? slug;
@@ -1556,48 +1539,31 @@ async function promptPlatformSelection(ask) {
1556
1539
  process.stderr.write(
1557
1540
  "\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n\n"
1558
1541
  );
1542
+ process.stderr.write(" " + style.dim("Native plugin (files, terminal, browser)") + "\n");
1559
1543
  let optionNum = 1;
1560
- for (const section of INIT_WIZARD_MENU_SECTIONS) {
1561
- process.stderr.write(" " + style.dim(section.title) + "\n");
1562
- for (const item of section.items) {
1563
- const indent = optionNum >= 10 ? " " : " ";
1564
- process.stderr.write(`${indent}${style.violet(String(optionNum))}. ${item.label}
1544
+ for (const slug of INIT_WIZARD_PICKER_NATIVE_SLUGS) {
1545
+ const entry = INIT_WIZARD_PLATFORM_REGISTRY.find((e) => e.slug === slug);
1546
+ const label = entry?.displayName ?? slug;
1547
+ const indent = optionNum >= 10 ? " " : " ";
1548
+ process.stderr.write(`${indent}${style.violet(String(optionNum))}. ${label}
1565
1549
  `);
1566
- optionNum++;
1567
- }
1550
+ optionNum++;
1568
1551
  }
1569
- process.stderr.write(
1570
- "\n" + style.dim(
1571
- ` Pick ${String(INIT_WIZARD_SELECTION_MAX)} to wrap a local MCP server with multicorn-shield --wrap.`
1572
- ) + "\n"
1573
- );
1552
+ process.stderr.write("\n");
1553
+ process.stderr.write(" MCP agents (Cursor, Copilot, others) are set up\n");
1554
+ process.stderr.write(` in the dashboard: ${INIT_WIZARD_DASHBOARD_AGENTS_URL}
1555
+
1556
+ `);
1574
1557
  let selection = 0;
1575
1558
  while (selection === 0) {
1576
- const input = await ask(`Select (1-${String(INIT_WIZARD_SELECTION_MAX)}): `);
1559
+ const input = await ask(`Select (1-${String(INIT_WIZARD_PICKER_SELECTION_MAX)}): `);
1577
1560
  const num = parseInt(input.trim(), 10);
1578
- if (num >= 1 && num <= INIT_WIZARD_SELECTION_MAX) {
1561
+ if (num >= 1 && num <= INIT_WIZARD_PICKER_SELECTION_MAX) {
1579
1562
  selection = num;
1580
1563
  }
1581
1564
  }
1582
1565
  return selection;
1583
1566
  }
1584
- async function promptWindsurfIntegrationMode(ask) {
1585
- process.stderr.write("\n" + style.bold("Windsurf integration") + "\n");
1586
- process.stderr.write(
1587
- " " + style.violet("1") + ". Native plugin (recommended) - Cascade Hooks see every file, terminal, and MCP action\n"
1588
- );
1589
- process.stderr.write(
1590
- " " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into mcp_config)\n"
1591
- );
1592
- let choice = 0;
1593
- while (choice === 0) {
1594
- const input = await ask("Choose integration (1-2): ");
1595
- const num = parseInt(input.trim(), 10);
1596
- if (num === 1) choice = 1;
1597
- if (num === 2) choice = 2;
1598
- }
1599
- return choice === 1 ? "native" : "hosted";
1600
- }
1601
1567
  async function arrowSelect(options, ask, fallbackLabel) {
1602
1568
  if (options.length === 1) {
1603
1569
  const only = options[0] ?? "";
@@ -1801,6 +1767,14 @@ async function createProxyConfig(baseUrl, apiKey, agentName, targetUrl, serverNa
1801
1767
  function shouldEmbedKeyInHostedProxyUrl(platform) {
1802
1768
  return HOSTED_PROXY_PLATFORMS_WITH_URL_KEY.has(platform);
1803
1769
  }
1770
+ function buildClineHostedMcpEntry(url) {
1771
+ return {
1772
+ type: "streamableHttp",
1773
+ url,
1774
+ timeout: 60,
1775
+ disabled: false
1776
+ };
1777
+ }
1804
1778
  function hostedProxyUrlWithKeyParam(proxyUrl, apiKey) {
1805
1779
  if (apiKey.length === 0) {
1806
1780
  process.stderr.write(
@@ -2204,9 +2178,11 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
2204
2178
  printHostedProxyJsonParseWarning(getWindsurfMcpConfigPath());
2205
2179
  }
2206
2180
  } else if (platform === "cline") {
2207
- result = await mergeMcpServersObjectStyle(getClineMcpSettingsPath(), shortName, {
2208
- url: proxyUrlWithKeyWhenNeeded
2209
- });
2181
+ result = await mergeMcpServersObjectStyle(
2182
+ getClineMcpSettingsPath(),
2183
+ shortName,
2184
+ buildClineHostedMcpEntry(proxyUrlWithKeyWhenNeeded)
2185
+ );
2210
2186
  if (result === "parse-error") {
2211
2187
  printHostedProxyJsonParseWarning(getClineMcpSettingsPath());
2212
2188
  }
@@ -2496,9 +2472,7 @@ Authorization = "Bearer ${bearerToken}"
2496
2472
  snippetText = platform === "cline" ? JSON.stringify(
2497
2473
  {
2498
2474
  mcpServers: {
2499
- [shortName]: {
2500
- [urlKey]: urlInSnippet
2501
- }
2475
+ [shortName]: buildClineHostedMcpEntry(urlInSnippet)
2502
2476
  }
2503
2477
  },
2504
2478
  null,
@@ -2661,6 +2635,7 @@ function mergeAgentsForPlatform(localAgents, remoteAgents, selectedPlatform) {
2661
2635
  }
2662
2636
  async function runInit(explicitBaseUrl, options) {
2663
2637
  const verbose = options?.verbose === true;
2638
+ const hybridIntegration = resolveInitHybridIntegrationMode(options?.integration);
2664
2639
  if (!process.stdin.isTTY) {
2665
2640
  process.stderr.write(
2666
2641
  style.red("Error: interactive terminal required. Cannot run init with piped input.") + "\n"
@@ -2705,9 +2680,31 @@ async function runInit(explicitBaseUrl, options) {
2705
2680
  if (existing !== null && existing.apiKey.startsWith("mcs_") && existing.apiKey.length >= 8) {
2706
2681
  const masked = "mcs_..." + existing.apiKey.slice(-4);
2707
2682
  process.stderr.write("Found existing API key: " + style.cyan(masked) + "\n");
2708
- const answer = await ask("Use this key? (Y/n) ");
2709
- if (answer.trim().toLowerCase() !== "n") {
2710
- apiKey = existing.apiKey;
2683
+ const answer = await ask("Use this key? (Y/n, or paste a new mcs_ key) ");
2684
+ const trimmed = answer.trim();
2685
+ if (trimmed.toLowerCase() !== "n") {
2686
+ const candidate = isPastedShieldApiKeyInput(trimmed) ? trimmed : existing.apiKey;
2687
+ const spinner = withSpinner("Validating key...");
2688
+ let reuseResult;
2689
+ try {
2690
+ reuseResult = await validateApiKey(candidate, resolvedBaseUrl);
2691
+ } catch (error) {
2692
+ spinner.stop(false, "Validation failed");
2693
+ throw error;
2694
+ }
2695
+ if (reuseResult.valid) {
2696
+ spinner.stop(true, "Key validated");
2697
+ apiKey = candidate;
2698
+ } else {
2699
+ spinner.stop(false, reuseResult.error ?? "Validation failed.");
2700
+ if (isPastedShieldApiKeyInput(trimmed)) {
2701
+ process.stderr.write(style.red("That key was rejected. Enter a new key below.") + "\n");
2702
+ } else {
2703
+ process.stderr.write(
2704
+ style.yellow("Existing key is no longer valid. Enter a new key below.") + "\n"
2705
+ );
2706
+ }
2707
+ }
2711
2708
  }
2712
2709
  }
2713
2710
  if (apiKey.length === 0) {
@@ -2759,8 +2756,8 @@ async function runInit(explicitBaseUrl, options) {
2759
2756
  let removeAgentNameBeforeSave = void 0;
2760
2757
  const initWorkspacePath = resolve(process.cwd());
2761
2758
  const selection = await promptPlatformSelection(ask);
2762
- const selectedPlatform = PLATFORM_BY_SELECTION[selection] ?? "cursor";
2763
- const selectedLabel = platformMenuLabelForSelection(selection);
2759
+ const selectedPlatform = PICKER_SLUG_BY_SELECTION[selection] ?? "openclaw";
2760
+ const selectedLabel = platformMenuLabelForPickerSelection(selection);
2764
2761
  if (selectedPlatform === "other-mcp") {
2765
2762
  const raw = existing !== null ? { ...existing } : {};
2766
2763
  raw["apiKey"] = apiKey;
@@ -2894,7 +2891,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2894
2891
  }
2895
2892
  if (detection.status === "parse-error") {
2896
2893
  process.stderr.write(
2897
- style.red("\u2717") + " Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually.\n"
2894
+ style.red("\u2717") + " Could not update OpenClaw config. Set plugins.entries.multicorn-shield.config.apiKey in ~/.openclaw/openclaw.json, or run npx multicorn-shield init.\n"
2898
2895
  );
2899
2896
  }
2900
2897
  if (detection.status === "detected") {
@@ -2934,7 +2931,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2934
2931
  if (result === "parse-error") {
2935
2932
  spinner.stop(
2936
2933
  false,
2937
- "Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually."
2934
+ "Could not update OpenClaw config. Set plugins.entries.multicorn-shield.config.apiKey in ~/.openclaw/openclaw.json, or run npx multicorn-shield init."
2938
2935
  );
2939
2936
  } else {
2940
2937
  spinner.stop(
@@ -2981,7 +2978,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2981
2978
  });
2982
2979
  setupSucceeded = true;
2983
2980
  } else if (selectedPlatform === "windsurf") {
2984
- const windsurfMode = await promptWindsurfIntegrationMode(ask);
2981
+ const windsurfMode = hybridIntegration;
2985
2982
  if (windsurfMode === "native") {
2986
2983
  try {
2987
2984
  await installWindsurfNativeHooks();
@@ -3080,7 +3077,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3080
3077
  }
3081
3078
  }
3082
3079
  } else if (selectedPlatform === "gemini-cli") {
3083
- const geminiMode = await promptGeminiCliIntegrationMode(ask);
3080
+ const geminiMode = hybridIntegration;
3084
3081
  if (geminiMode === "native") {
3085
3082
  try {
3086
3083
  await installGeminiCliNativeHooks(ask);
@@ -3174,7 +3171,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3174
3171
  }
3175
3172
  }
3176
3173
  } else if (selectedPlatform === "opencode") {
3177
- const opencodeMode = await promptOpencodeIntegrationMode(ask);
3174
+ const opencodeMode = hybridIntegration;
3178
3175
  if (opencodeMode === "native") {
3179
3176
  try {
3180
3177
  await installOpenCodeNativePlugin();
@@ -3263,7 +3260,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3263
3260
  }
3264
3261
  }
3265
3262
  } else if (selectedPlatform === "codex-cli") {
3266
- const codexMode = await promptCodexCliIntegrationMode(ask);
3263
+ const codexMode = hybridIntegration;
3267
3264
  if (codexMode === "native") {
3268
3265
  try {
3269
3266
  await installCodexCliNativeHooks();
@@ -3352,7 +3349,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3352
3349
  }
3353
3350
  }
3354
3351
  } else if (selectedPlatform === "cline") {
3355
- const clineMode = await promptClineIntegrationMode(ask);
3352
+ const clineMode = hybridIntegration;
3356
3353
  if (clineMode === "native") {
3357
3354
  try {
3358
3355
  await installClineNativeHooks();
@@ -3783,9 +3780,7 @@ async function writeLocalMcpEntry(client, agentName, localProxyUrl, apiKey, work
3783
3780
  return await mergeMcpServersObjectStyle(
3784
3781
  filePath,
3785
3782
  entryKey,
3786
- {
3787
- url
3788
- },
3783
+ buildClineHostedMcpEntry(url),
3789
3784
  legacyKeys
3790
3785
  ) === "ok" ? filePath : null;
3791
3786
  case "windsurf":
@@ -3946,7 +3941,7 @@ Authorization = "${authHeader}"
3946
3941
  }
3947
3942
  }
3948
3943
  }
3949
- var SECRET_JSON_FILE_OPTIONS, style, BANNER, NativePluginPrerequisiteMissingError, CONFIG_DIR, CONFIG_PATH, OPENCLAW_CONFIG_PATH, ANSI_PATTERN, UPSTREAM_AUTH_KNOWN_SCHEME_WITH_PAYLOAD, OPENCLAW_MIN_VERSION, INIT_WIZARD_PLATFORM_REGISTRY, INIT_WIZARD_MENU_SECTIONS, INIT_WIZARD_SELECTION_MAX, INIT_EXISTING_AGENTS_PLATFORM_ACTIONS, PLATFORM_BY_SELECTION, HOSTED_PROXY_PLATFORMS_WITH_URL_KEY, OPENCODE_CONFIG_SCHEMA_URL, DEFAULT_SHIELD_API_BASE_URL, CODING_CLIENTS, CLIENT_DISPLAY_NAMES, CLIENT_RELOAD_INSTRUCTIONS, CODING_CLIENT_TO_PLATFORM;
3944
+ var SECRET_JSON_FILE_OPTIONS, style, BANNER, NativePluginPrerequisiteMissingError, CONFIG_DIR, CONFIG_PATH, OPENCLAW_CONFIG_PATH, ANSI_PATTERN, UPSTREAM_AUTH_KNOWN_SCHEME_WITH_PAYLOAD, OPENCLAW_MIN_VERSION, OPENCLAW_SHIELD_API_KEY_ENV_REF, INIT_WIZARD_PLATFORM_REGISTRY, INIT_WIZARD_PICKER_NATIVE_SLUGS, INIT_WIZARD_DASHBOARD_AGENTS_URL, INIT_WIZARD_PICKER_SELECTION_MAX, PICKER_SLUG_BY_SELECTION, INIT_EXISTING_AGENTS_PLATFORM_ACTIONS, HOSTED_PROXY_PLATFORMS_WITH_URL_KEY, OPENCODE_CONFIG_SCHEMA_URL, DEFAULT_SHIELD_API_BASE_URL, CODING_CLIENTS, CLIENT_DISPLAY_NAMES, CLIENT_RELOAD_INSTRUCTIONS, CODING_CLIENT_TO_PLATFORM;
3950
3945
  var init_config = __esm({
3951
3946
  "src/proxy/config.ts"() {
3952
3947
  init_consent();
@@ -3980,6 +3975,7 @@ var init_config = __esm({
3980
3975
  ANSI_PATTERN = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*[a-zA-Z]", "g");
3981
3976
  UPSTREAM_AUTH_KNOWN_SCHEME_WITH_PAYLOAD = /^(Bearer|Basic|Token|ApiKey)(\s+)(.+)$/is;
3982
3977
  OPENCLAW_MIN_VERSION = "2026.2.26";
3978
+ OPENCLAW_SHIELD_API_KEY_ENV_REF = "${MULTICORN_API_KEY}";
3983
3979
  INIT_WIZARD_PLATFORM_REGISTRY = [
3984
3980
  { slug: "openclaw", displayName: "OpenClaw", section: "native" },
3985
3981
  { slug: "claude-code", displayName: "Claude Code", section: "native" },
@@ -4036,25 +4032,17 @@ var init_config = __esm({
4036
4032
  },
4037
4033
  { slug: "other-mcp", displayName: "Local MCP / Other", section: "hosted" }
4038
4034
  ];
4039
- INIT_WIZARD_MENU_SECTIONS = (() => {
4040
- const itemsFor = (section) => INIT_WIZARD_PLATFORM_REGISTRY.filter((e) => e.section === section).map((e) => ({
4041
- platform: e.slug,
4042
- label: e.displayName
4043
- }));
4044
- return [
4045
- { title: "Recommended (native plugin)", items: itemsFor("native") },
4046
- { title: "Hosted proxy (MCP only)", items: itemsFor("hosted") }
4047
- ];
4048
- })();
4049
- INIT_WIZARD_SELECTION_MAX = INIT_WIZARD_PLATFORM_REGISTRY.length;
4035
+ INIT_WIZARD_PICKER_NATIVE_SLUGS = INIT_WIZARD_PLATFORM_REGISTRY.filter((e) => e.section === "native").map((e) => e.slug);
4036
+ INIT_WIZARD_DASHBOARD_AGENTS_URL = "https://app.multicorn.ai/agents/new";
4037
+ INIT_WIZARD_PICKER_SELECTION_MAX = INIT_WIZARD_PICKER_NATIVE_SLUGS.length;
4038
+ PICKER_SLUG_BY_SELECTION = Object.fromEntries(
4039
+ INIT_WIZARD_PICKER_NATIVE_SLUGS.map((slug, i) => [i + 1, slug])
4040
+ );
4050
4041
  INIT_EXISTING_AGENTS_PLATFORM_ACTIONS = [
4051
4042
  "Add a new agent alongside these",
4052
4043
  "Replace an existing agent",
4053
4044
  "Skip - choose a different platform"
4054
4045
  ];
4055
- PLATFORM_BY_SELECTION = Object.fromEntries(
4056
- INIT_WIZARD_PLATFORM_REGISTRY.map((e, i) => [i + 1, e.slug])
4057
- );
4058
4046
  HOSTED_PROXY_PLATFORMS_WITH_URL_KEY = /* @__PURE__ */ new Set([
4059
4047
  "cursor",
4060
4048
  "claude-desktop",
@@ -5062,7 +5050,7 @@ var init_package = __esm({
5062
5050
  "package.json"() {
5063
5051
  package_default = {
5064
5052
  name: "multicorn-shield",
5065
- version: "1.11.0",
5053
+ version: "1.12.0",
5066
5054
  description: "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
5067
5055
  license: "MIT",
5068
5056
  author: "Multicorn AI Pty Ltd",
@@ -5122,7 +5110,7 @@ var init_package = __esm({
5122
5110
  node: ">=20"
5123
5111
  },
5124
5112
  scripts: {
5125
- build: "tsup",
5113
+ build: "tsup && node scripts/stage-local-proxy-server.mjs",
5126
5114
  dev: "tsup --watch",
5127
5115
  lint: "eslint . --no-warn-ignored && prettier --check .",
5128
5116
  "lint:fix": "eslint --fix . --no-warn-ignored && prettier --write .",
@@ -5141,7 +5129,8 @@ var init_package = __esm({
5141
5129
  prepare: "husky || true",
5142
5130
  "release:patch": "npm version patch && pnpm publish",
5143
5131
  "release:minor": "npm version minor && pnpm publish",
5144
- "release:major": "npm version major && pnpm publish"
5132
+ "release:major": "npm version major && pnpm publish",
5133
+ local: "pnpm build &&node dist/multicorn-shield.js init --base-url http://localhost:8080"
5145
5134
  },
5146
5135
  "lint-staged": {
5147
5136
  "*.ts": [
@@ -5169,6 +5158,7 @@ var init_package = __esm({
5169
5158
  "@size-limit/file": "^11.1.6",
5170
5159
  "@types/node": "^22.0.0",
5171
5160
  "@vitest/coverage-v8": "^3.0.5",
5161
+ ajv: "^8.18.0",
5172
5162
  eslint: "^9.19.0",
5173
5163
  "eslint-config-prettier": "^10.0.1",
5174
5164
  "eslint-plugin-unicorn": "^57.0.0",
@@ -5251,6 +5241,94 @@ var init_package_meta = __esm({
5251
5241
  PACKAGE_VERSION = package_default.version;
5252
5242
  }
5253
5243
  });
5244
+ function findMulticornShieldPackageRoot(moduleDir = dirname(fileURLToPath(import.meta.url))) {
5245
+ let current = moduleDir;
5246
+ for (; ; ) {
5247
+ const pkgPath = join(current, "package.json");
5248
+ if (existsSync(pkgPath)) {
5249
+ try {
5250
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
5251
+ if (pkg.name === "multicorn-shield") {
5252
+ return current;
5253
+ }
5254
+ } catch {
5255
+ }
5256
+ }
5257
+ const parent = dirname(current);
5258
+ if (parent === current) {
5259
+ break;
5260
+ }
5261
+ current = parent;
5262
+ }
5263
+ try {
5264
+ const req = createRequire(import.meta.url);
5265
+ return dirname(req.resolve("multicorn-shield/package.json"));
5266
+ } catch {
5267
+ throw new Error(NOT_FOUND_MESSAGE);
5268
+ }
5269
+ }
5270
+ function resolveLocalProxyServerEntry(options) {
5271
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
5272
+ const candidates = [
5273
+ join(findMulticornShieldPackageRoot(moduleDir), "dist", "server.js")
5274
+ ];
5275
+ try {
5276
+ const req = createRequire(import.meta.url);
5277
+ const proxyRoot = dirname(req.resolve("multicorn-proxy/package.json"));
5278
+ candidates.unshift(join(proxyRoot, "dist", "server.js"));
5279
+ } catch {
5280
+ }
5281
+ for (const path of candidates) {
5282
+ if (existsSync(path)) return path;
5283
+ }
5284
+ throw new Error(NOT_FOUND_MESSAGE);
5285
+ }
5286
+ function buildLocalProxySpawnEnv(port, apiBaseUrl) {
5287
+ return {
5288
+ PORT: String(port),
5289
+ HOST: "127.0.0.1",
5290
+ SHIELD_API_BASE_URL: apiBaseUrl,
5291
+ ALLOW_PRIVATE_TARGETS: "true"
5292
+ };
5293
+ }
5294
+ function buildLocalProxySpawnCommand(port, apiBaseUrl, execPath = process.execPath, serverEntryPath = resolveLocalProxyServerEntry()) {
5295
+ const env = buildLocalProxySpawnEnv(port, apiBaseUrl);
5296
+ return {
5297
+ executable: execPath,
5298
+ args: [serverEntryPath],
5299
+ env,
5300
+ serverEntryPath
5301
+ };
5302
+ }
5303
+ function readProxyLogTail(logPath, maxBytes = 8e3) {
5304
+ try {
5305
+ const content = readFileSync(logPath, "utf8");
5306
+ if (content.length <= maxBytes) return content;
5307
+ return content.slice(-maxBytes);
5308
+ } catch {
5309
+ return "";
5310
+ }
5311
+ }
5312
+ function formatLocalProxyStartError(port, logPath, childExited, exitCode) {
5313
+ const logTail = readProxyLogTail(logPath);
5314
+ const reason = childExited ? `Proxy process exited early${exitCode === null ? "" : ` (code ${String(exitCode)})`}.` : `Proxy did not become ready on port ${String(port)} within the timeout.`;
5315
+ let message = `Could not start the local proxy on port ${String(port)}. ${reason}`;
5316
+ message += logTail.length > 0 ? `
5317
+
5318
+ Output from ${logPath}:
5319
+ ${logTail.trimEnd()}` : `
5320
+
5321
+ See ${logPath} for details.`;
5322
+ return message;
5323
+ }
5324
+ var LOCAL_PROXY_READY_POLL_MS, LOCAL_PROXY_READY_MAX_POLLS, NOT_FOUND_MESSAGE;
5325
+ var init_local_proxy_start = __esm({
5326
+ "src/commands/local-proxy-start.ts"() {
5327
+ LOCAL_PROXY_READY_POLL_MS = 500;
5328
+ LOCAL_PROXY_READY_MAX_POLLS = 30;
5329
+ NOT_FOUND_MESSAGE = "Local proxy server entry (dist/server.js) not found. Reinstall multicorn-shield or run pnpm build.";
5330
+ }
5331
+ });
5254
5332
  function pidfilePath(agent) {
5255
5333
  return join(PIDFILE_DIR, `files-${agent}.pid`);
5256
5334
  }
@@ -5270,6 +5348,16 @@ function readPidfile(agent) {
5270
5348
  return null;
5271
5349
  }
5272
5350
  }
5351
+ function supervisorPidFromSession(data) {
5352
+ if (typeof data.supervisorPid === "number") return data.supervisorPid;
5353
+ if (typeof data.pid === "number") return data.pid;
5354
+ return void 0;
5355
+ }
5356
+ function isAgentSessionRunning(data) {
5357
+ if (data === null) return false;
5358
+ const pid = supervisorPidFromSession(data);
5359
+ return typeof pid === "number" && isProcessAlive(pid);
5360
+ }
5273
5361
  function removePidfile(agent) {
5274
5362
  const p = pidfilePath(agent);
5275
5363
  try {
@@ -5310,7 +5398,7 @@ function runStatus() {
5310
5398
  const fsReg = readFsRegistry();
5311
5399
  process.stderr.write("Active sessions:\n\n");
5312
5400
  for (const s of sessions) {
5313
- const supervisorAlive = typeof s.supervisorPid === "number" && isProcessAlive(s.supervisorPid);
5401
+ const supervisorAlive = isAgentSessionRunning(s);
5314
5402
  const fsEntry = fsReg[s.dir];
5315
5403
  const fsAlive = fsEntry !== void 0 && isProcessAlive(fsEntry.pid);
5316
5404
  const proxyAlive = proxyReg !== null && proxyReg.port === s.proxyPort && isProcessAlive(proxyReg.pid);
@@ -5457,9 +5545,7 @@ async function withResourceLock(fn) {
5457
5545
  }
5458
5546
  }
5459
5547
  function liveAgents() {
5460
- return listAllPidfiles().filter(
5461
- (p) => typeof p.supervisorPid === "number" && isProcessAlive(p.supervisorPid)
5462
- );
5548
+ return listAllPidfiles().filter((p) => isAgentSessionRunning(p));
5463
5549
  }
5464
5550
  function agentsReferencingProxy(proxyPort, agents, excludeAgent) {
5465
5551
  return agents.filter((a) => a.agent !== excludeAgent && a.proxyPort === proxyPort);
@@ -5477,27 +5563,37 @@ async function nextFreePort(start, claimed, isBusy, range = FS_PORT_SCAN_RANGE)
5477
5563
  `No free port for the filesystem server in range ${String(start)}-${String(start + range)}.`
5478
5564
  );
5479
5565
  }
5566
+ async function resolveBaseUrl(explicitBaseUrl) {
5567
+ if (explicitBaseUrl !== void 0 && explicitBaseUrl.trim().length > 0) {
5568
+ return explicitBaseUrl.trim();
5569
+ }
5570
+ const envBaseUrl = process.env["MULTICORN_BASE_URL"];
5571
+ if (typeof envBaseUrl === "string" && envBaseUrl.trim().length > 0) {
5572
+ return envBaseUrl.trim();
5573
+ }
5574
+ const config = await loadConfig();
5575
+ if (config !== null && config.baseUrl.length > 0) {
5576
+ return config.baseUrl;
5577
+ }
5578
+ const fromFile = await readBaseUrlFromConfig();
5579
+ if (fromFile !== void 0 && fromFile.length > 0) {
5580
+ return fromFile;
5581
+ }
5582
+ return DEFAULT_SHIELD_API_BASE_URL;
5583
+ }
5480
5584
  async function resolveConfig(opts) {
5585
+ const baseUrl = await resolveBaseUrl(opts.baseUrl);
5481
5586
  const fromFlag = opts.apiKey;
5482
5587
  if (fromFlag && fromFlag.length > 0) {
5483
- return {
5484
- apiKey: fromFlag,
5485
- baseUrl: opts.baseUrl ?? DEFAULT_SHIELD_API_BASE_URL
5486
- };
5588
+ return { apiKey: fromFlag, baseUrl };
5487
5589
  }
5488
5590
  const envKey = process.env["MULTICORN_API_KEY"];
5489
5591
  if (typeof envKey === "string" && envKey.length > 0) {
5490
- return {
5491
- apiKey: envKey,
5492
- baseUrl: opts.baseUrl ?? DEFAULT_SHIELD_API_BASE_URL
5493
- };
5592
+ return { apiKey: envKey, baseUrl };
5494
5593
  }
5495
5594
  const config = await loadConfig();
5496
5595
  if (config !== null) {
5497
- return {
5498
- apiKey: config.apiKey,
5499
- baseUrl: opts.baseUrl ?? config.baseUrl
5500
- };
5596
+ return { apiKey: config.apiKey, baseUrl };
5501
5597
  }
5502
5598
  process.stderr.write(
5503
5599
  "No API key found. Pass --api-key <key> or add it to ~/.multicorn/config.json.\n"
@@ -5537,26 +5633,40 @@ function startLocalProxyDetached(port, apiBaseUrl) {
5537
5633
  const logFile = join(PIDFILE_DIR, "proxy.log");
5538
5634
  const out = openSync(logFile, "a");
5539
5635
  const err = openSync(logFile, "a");
5540
- const child = spawn("npx", ["multicorn-proxy"], {
5636
+ const { executable, args, env: proxyEnv } = buildLocalProxySpawnCommand(port, apiBaseUrl);
5637
+ const child = spawn(executable, [...args], {
5541
5638
  stdio: ["ignore", out, err],
5542
5639
  detached: true,
5543
5640
  env: {
5544
5641
  ...process.env,
5545
- PORT: String(port),
5546
- HOST: "127.0.0.1",
5547
- SHIELD_API_BASE_URL: apiBaseUrl,
5642
+ ...proxyEnv
5548
5643
  // SAFETY: blanket-allow for private targets is acceptable ONLY because
5549
5644
  // this is a local single-user proxy. The only registered target is the
5550
5645
  // filesystem server on the same machine. Do NOT copy this pattern to a
5551
5646
  // multi-tenant or hosted proxy deployment.
5552
- ALLOW_PRIVATE_TARGETS: "true"
5553
5647
  }
5554
5648
  });
5555
5649
  child.unref();
5556
5650
  return child;
5557
5651
  }
5558
- async function ensureProxy(proxyPort, apiBaseUrl) {
5559
- if (await probeProxyHealth(proxyPort)) {
5652
+ async function ensureProxy(proxyPort, apiBaseUrl, options) {
5653
+ const forceRespawn = options?.forceRespawn === true;
5654
+ if (forceRespawn) {
5655
+ const reg2 = readProxyRegistry();
5656
+ if (reg2 !== null && reg2.port === proxyPort && isProcessAlive(reg2.pid)) {
5657
+ killWithEscalation(reg2.pid, true);
5658
+ }
5659
+ if (reg2 !== null && reg2.port === proxyPort) {
5660
+ try {
5661
+ unlinkSync(PROXY_REGISTRY);
5662
+ } catch {
5663
+ }
5664
+ }
5665
+ for (let i = 0; i < 20; i++) {
5666
+ if (!await isPortListening(proxyPort)) break;
5667
+ await sleep3(100);
5668
+ }
5669
+ } else if (await probeProxyHealth(proxyPort)) {
5560
5670
  const reg2 = readProxyRegistry();
5561
5671
  const managed = reg2 !== null && reg2.port === proxyPort && isProcessAlive(reg2.pid);
5562
5672
  return { reused: true, managed };
@@ -5573,9 +5683,11 @@ async function ensureProxy(proxyPort, apiBaseUrl) {
5573
5683
  `A server is already running on port ${String(proxyPort)} but it's not a healthy Shield proxy. Stop it or re-run with --proxy-port <n>.`
5574
5684
  );
5575
5685
  }
5686
+ const logFile = join(PIDFILE_DIR, "proxy.log");
5576
5687
  const child = startLocalProxyDetached(proxyPort, apiBaseUrl);
5577
- for (let i = 0; i < 30; i++) {
5578
- await sleep3(500);
5688
+ for (let i = 0; i < LOCAL_PROXY_READY_MAX_POLLS; i++) {
5689
+ await sleep3(LOCAL_PROXY_READY_POLL_MS);
5690
+ if (child.exitCode !== null || child.signalCode !== null) break;
5579
5691
  if (await probeProxyHealth(proxyPort)) {
5580
5692
  if (child.pid !== void 0) {
5581
5693
  writeJsonFile(PROXY_REGISTRY, { pid: child.pid, port: proxyPort });
@@ -5587,9 +5699,8 @@ async function ensureProxy(proxyPort, apiBaseUrl) {
5587
5699
  if (child.pid !== void 0) killWithEscalation(child.pid, true);
5588
5700
  } catch {
5589
5701
  }
5590
- throw new Error(
5591
- `Could not start the local proxy on port ${String(proxyPort)}. Check the log at ${join(PIDFILE_DIR, "proxy.log")}.`
5592
- );
5702
+ const childExited = child.exitCode !== null || child.signalCode !== null;
5703
+ throw new Error(formatLocalProxyStartError(proxyPort, logFile, childExited, child.exitCode));
5593
5704
  }
5594
5705
  async function ensureFsServer(realDir, requestedPort) {
5595
5706
  const reg = readFsRegistry();
@@ -5791,8 +5902,9 @@ async function runStop(agent) {
5791
5902
  process.exit(1);
5792
5903
  }
5793
5904
  await withResourceLock(() => {
5794
- if (typeof data.supervisorPid === "number" && isProcessAlive(data.supervisorPid)) {
5795
- killWithEscalation(data.supervisorPid);
5905
+ const supervisorPid = supervisorPidFromSession(data);
5906
+ if (typeof supervisorPid === "number" && isProcessAlive(supervisorPid)) {
5907
+ killWithEscalation(supervisorPid);
5796
5908
  }
5797
5909
  removePidfile(agent);
5798
5910
  releaseFsServerIfUnused(data.dir, agent);
@@ -5822,7 +5934,8 @@ Run it once with the folder: npx multicorn-shield files restart <dir> --agent ${
5822
5934
  await runDetached({
5823
5935
  ...opts,
5824
5936
  dir,
5825
- proxyPort: opts.proxyPort ?? existing?.proxyPort});
5937
+ proxyPort: opts.proxyPort ?? existing?.proxyPort,
5938
+ respawnProxy: true});
5826
5939
  }
5827
5940
  async function runDetached(opts) {
5828
5941
  const absDir = resolve(opts.dir);
@@ -5833,8 +5946,7 @@ async function runDetached(opts) {
5833
5946
  }
5834
5947
  const existing = readPidfile(opts.agent);
5835
5948
  if (existing !== null) {
5836
- const alive = typeof existing.supervisorPid === "number" && isProcessAlive(existing.supervisorPid);
5837
- if (alive) {
5949
+ if (isAgentSessionRunning(existing)) {
5838
5950
  process.stderr.write(
5839
5951
  `Already running for agent "${opts.agent}" (fs :${String(existing.fsPort)}, proxy :${String(existing.proxyPort)}).
5840
5952
  Stop with: npx multicorn-shield files stop --agent ${opts.agent}
@@ -5849,6 +5961,7 @@ Stop with: npx multicorn-shield files stop --agent ${opts.agent}
5849
5961
  if (opts.proxyPort !== void 0) args.push("--proxy-port", String(opts.proxyPort));
5850
5962
  if (opts.apiKey !== void 0) args.push("--api-key", opts.apiKey);
5851
5963
  if (opts.baseUrl !== void 0) args.push("--base-url", opts.baseUrl);
5964
+ if (opts.respawnProxy) args.push("--respawn-proxy");
5852
5965
  if (opts.client !== void 0) args.push("--client", opts.client);
5853
5966
  const scriptPath = process.argv[1] ?? resolve("dist/multicorn-shield.js");
5854
5967
  const logFile = join(PIDFILE_DIR, `files-${opts.agent}.log`);
@@ -5932,8 +6045,7 @@ async function runFilesCommand(opts) {
5932
6045
  const proxyPort = opts.proxyPort ?? DEFAULT_PROXY_PORT;
5933
6046
  const existingPidfile = readPidfile(opts.agent);
5934
6047
  if (existingPidfile !== null) {
5935
- const supervisorAlive = typeof existingPidfile.supervisorPid === "number" && isProcessAlive(existingPidfile.supervisorPid);
5936
- if (supervisorAlive) {
6048
+ if (isAgentSessionRunning(existingPidfile)) {
5937
6049
  process.stderr.write(
5938
6050
  `A session for agent "${opts.agent}" is already running. Run 'files stop --agent ${opts.agent}' first, or use a different --agent name.
5939
6051
  `
@@ -5947,7 +6059,9 @@ async function runFilesCommand(opts) {
5947
6059
  let fsReused;
5948
6060
  try {
5949
6061
  const ensured = await withResourceLock(async () => {
5950
- const proxyRes = await ensureProxy(proxyPort, config.baseUrl);
6062
+ const proxyRes = await ensureProxy(proxyPort, config.baseUrl, {
6063
+ forceRespawn: opts.respawnProxy
6064
+ });
5951
6065
  const fsRes = await ensureFsServer(realDir, opts.port);
5952
6066
  writePidfile({
5953
6067
  agent: opts.agent,
@@ -6196,6 +6310,7 @@ var DEFAULT_FS_PORT, DEFAULT_PROXY_PORT, PIDFILE_DIR, PROXY_REGISTRY, FS_REGISTR
6196
6310
  var init_files = __esm({
6197
6311
  "src/commands/files.ts"() {
6198
6312
  init_config();
6313
+ init_local_proxy_start();
6199
6314
  DEFAULT_FS_PORT = 3005;
6200
6315
  DEFAULT_PROXY_PORT = 3001;
6201
6316
  PIDFILE_DIR = process.env["MULTICORN_HOME"] ?? join(homedir(), ".multicorn");
@@ -6242,6 +6357,8 @@ function parseArgs(argv) {
6242
6357
  let filesForeground = false;
6243
6358
  let filesStatus = false;
6244
6359
  let filesRestart = false;
6360
+ let filesRespawnProxy = false;
6361
+ let initIntegration = void 0;
6245
6362
  for (let i = 0; i < args.length; i++) {
6246
6363
  const arg = args[i];
6247
6364
  if (arg === "init") {
@@ -6292,6 +6409,8 @@ function parseArgs(argv) {
6292
6409
  filesStop = true;
6293
6410
  } else if (token === "--foreground") {
6294
6411
  filesForeground = true;
6412
+ } else if (token === "--respawn-proxy") {
6413
+ filesRespawnProxy = true;
6295
6414
  } else if (token === "--detach") ; else if (token === "stop") {
6296
6415
  filesStop = true;
6297
6416
  } else if (token === "status") {
@@ -6400,6 +6519,12 @@ function parseArgs(argv) {
6400
6519
  }
6401
6520
  } else if (arg === "--verbose" || arg === "--debug") {
6402
6521
  verbose = true;
6522
+ } else if (arg === "--integration") {
6523
+ const next = args[i + 1];
6524
+ if (next === "native" || next === "hosted") {
6525
+ initIntegration = next;
6526
+ i++;
6527
+ }
6403
6528
  }
6404
6529
  }
6405
6530
  return {
@@ -6413,6 +6538,7 @@ function parseArgs(argv) {
6413
6538
  deleteAgentName,
6414
6539
  apiKey,
6415
6540
  verbose,
6541
+ initIntegration,
6416
6542
  filesDir,
6417
6543
  filesPort,
6418
6544
  filesProxyPort,
@@ -6420,7 +6546,8 @@ function parseArgs(argv) {
6420
6546
  filesClient,
6421
6547
  filesForeground,
6422
6548
  filesStatus,
6423
- filesRestart
6549
+ filesRestart,
6550
+ filesRespawnProxy
6424
6551
  };
6425
6552
  }
6426
6553
  function printHelp() {
@@ -6430,7 +6557,12 @@ function printHelp() {
6430
6557
  "",
6431
6558
  "Usage:",
6432
6559
  " npx multicorn-shield init",
6433
- " Interactive setup. Saves API key to ~/.multicorn/config.json.",
6560
+ " Interactive setup for native plugins (files, terminal, browser).",
6561
+ " Native: OpenClaw, Claude Code, Windsurf, Cline, Gemini CLI, OpenCode, Codex CLI.",
6562
+ " MCP-only clients (Cursor, Copilot, etc.): https://app.multicorn.ai/agents/new",
6563
+ " Saves API key to ~/.multicorn/config.json.",
6564
+ " --integration hosted Use hosted MCP proxy for Windsurf, Cline, Gemini CLI,",
6565
+ " OpenCode, or Codex CLI (default: native).",
6434
6566
  "",
6435
6567
  " npx multicorn-shield files <dir> --agent <name> [--client <client>]",
6436
6568
  " Share a local folder with a coding agent. Starts a filesystem MCP server",
@@ -6511,7 +6643,10 @@ async function runCli() {
6511
6643
  process.exit(0);
6512
6644
  }
6513
6645
  if (cli.subcommand === "init") {
6514
- await runInit(cli.baseUrl, { verbose: cli.verbose });
6646
+ await runInit(cli.baseUrl, {
6647
+ verbose: cli.verbose,
6648
+ ...cli.initIntegration !== void 0 ? { integration: cli.initIntegration } : {}
6649
+ });
6515
6650
  return;
6516
6651
  }
6517
6652
  if (cli.subcommand === "files") {
@@ -6527,7 +6662,8 @@ async function runCli() {
6527
6662
  client: void 0,
6528
6663
  foreground: true,
6529
6664
  status: true,
6530
- restart: false
6665
+ restart: false,
6666
+ respawnProxy: false
6531
6667
  });
6532
6668
  return;
6533
6669
  }
@@ -6552,7 +6688,8 @@ async function runCli() {
6552
6688
  client: cli.filesClient,
6553
6689
  foreground: cli.filesForeground,
6554
6690
  status: false,
6555
- restart: cli.filesRestart
6691
+ restart: cli.filesRestart,
6692
+ respawnProxy: cli.filesRespawnProxy
6556
6693
  });
6557
6694
  return;
6558
6695
  }