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.
@@ -658,7 +658,35 @@ function isVersionAtLeast(version, minimum) {
658
658
  }
659
659
  return true;
660
660
  }
661
- async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
661
+ var OPENCLAW_SHIELD_API_KEY_ENV_REF = "${MULTICORN_API_KEY}";
662
+ function buildOpenClawShieldPluginConfig(params) {
663
+ return {
664
+ apiKey: OPENCLAW_SHIELD_API_KEY_ENV_REF,
665
+ baseUrl: params.baseUrl,
666
+ agentName: params.agentName,
667
+ failMode: "closed"
668
+ };
669
+ }
670
+ function detectStaleOpenClawHookIntegration(openclawConfig) {
671
+ const hooks = openclawConfig["hooks"];
672
+ if (typeof hooks !== "object" || hooks === null) return false;
673
+ const internal = hooks["internal"];
674
+ if (typeof internal !== "object" || internal === null) return false;
675
+ const entries = internal["entries"];
676
+ if (typeof entries !== "object" || entries === null) return false;
677
+ return Object.prototype.hasOwnProperty.call(entries, "multicorn-shield");
678
+ }
679
+ function staleOpenClawHookIntegrationMessage() {
680
+ 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.";
681
+ }
682
+ function stripLegacyOpenClawShieldEntryFields(shield) {
683
+ delete shield["env"];
684
+ delete shield["apiKey"];
685
+ delete shield["baseUrl"];
686
+ delete shield["agentName"];
687
+ delete shield["failMode"];
688
+ }
689
+ async function updateOpenClawConfigIfPresent(_apiKey, baseUrl, agentName) {
662
690
  let raw;
663
691
  try {
664
692
  raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
@@ -674,50 +702,67 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
674
702
  } catch {
675
703
  return "parse-error";
676
704
  }
677
- let hooks = obj["hooks"];
678
- if (hooks === void 0 || typeof hooks !== "object") {
679
- hooks = {};
680
- obj["hooks"] = hooks;
705
+ if (detectStaleOpenClawHookIntegration(obj)) {
706
+ process.stderr.write(
707
+ style.yellow("\u26A0") + " " + staleOpenClawHookIntegrationMessage() + "\n"
708
+ );
681
709
  }
682
- let internal = hooks["internal"];
683
- if (internal === void 0 || typeof internal !== "object") {
684
- internal = { enabled: true, entries: {} };
685
- hooks["internal"] = internal;
710
+ let plugins = obj["plugins"];
711
+ if (plugins === void 0 || typeof plugins !== "object") {
712
+ plugins = {};
713
+ obj["plugins"] = plugins;
686
714
  }
687
- let entries = internal["entries"];
688
- if (entries === void 0 || typeof entries !== "object") {
689
- entries = {};
690
- internal["entries"] = entries;
715
+ let pluginEntries = plugins["entries"];
716
+ if (pluginEntries === void 0 || typeof pluginEntries !== "object") {
717
+ pluginEntries = {};
718
+ plugins["entries"] = pluginEntries;
691
719
  }
692
- let shield = entries["multicorn-shield"];
720
+ let shield = pluginEntries["multicorn-shield"];
693
721
  if (shield === void 0 || typeof shield !== "object") {
694
- shield = { enabled: true, env: {} };
695
- entries["multicorn-shield"] = shield;
696
- }
697
- let env = shield["env"];
698
- if (env === void 0 || typeof env !== "object") {
699
- env = {};
700
- shield["env"] = env;
701
- }
702
- env["MULTICORN_API_KEY"] = apiKey;
703
- env["MULTICORN_BASE_URL"] = baseUrl;
704
- if (agentName !== void 0) {
705
- env["MULTICORN_AGENT_NAME"] = agentName;
722
+ shield = { enabled: true };
723
+ pluginEntries["multicorn-shield"] = shield;
724
+ }
725
+ stripLegacyOpenClawShieldEntryFields(shield);
726
+ const resolvedAgentName = agentName ?? (typeof shield["config"] === "object" && shield["config"] !== null && typeof shield["config"]["agentName"] === "string" ? String(shield["config"]["agentName"]) : void 0);
727
+ if (resolvedAgentName !== void 0) {
728
+ shield["enabled"] = true;
729
+ shield["config"] = buildOpenClawShieldPluginConfig({
730
+ baseUrl,
731
+ agentName: resolvedAgentName
732
+ });
706
733
  const agentsList = obj["agents"];
707
734
  const list = agentsList?.["list"];
708
735
  if (Array.isArray(list) && list.length > 0) {
709
736
  const first = list[0];
710
- if (first["id"] !== agentName) {
711
- first["id"] = agentName;
712
- first["name"] = agentName;
737
+ if (first["id"] !== resolvedAgentName) {
738
+ first["id"] = resolvedAgentName;
739
+ first["name"] = resolvedAgentName;
713
740
  }
714
741
  } else {
715
742
  if (agentsList !== void 0 && typeof agentsList === "object") {
716
- agentsList["list"] = [{ id: agentName, name: agentName }];
743
+ agentsList["list"] = [{ id: resolvedAgentName, name: resolvedAgentName }];
717
744
  } else {
718
- obj["agents"] = { list: [{ id: agentName, name: agentName }] };
745
+ obj["agents"] = { list: [{ id: resolvedAgentName, name: resolvedAgentName }] };
719
746
  }
720
747
  }
748
+ } else {
749
+ shield["enabled"] = true;
750
+ const existingConfig = shield["config"];
751
+ if (typeof existingConfig === "object" && existingConfig !== null) {
752
+ const cfg = { ...existingConfig };
753
+ cfg["apiKey"] = OPENCLAW_SHIELD_API_KEY_ENV_REF;
754
+ cfg["baseUrl"] = baseUrl;
755
+ if (cfg["failMode"] !== "open" && cfg["failMode"] !== "closed") {
756
+ cfg["failMode"] = "closed";
757
+ }
758
+ shield["config"] = cfg;
759
+ } else {
760
+ shield["config"] = {
761
+ apiKey: OPENCLAW_SHIELD_API_KEY_ENV_REF,
762
+ baseUrl,
763
+ failMode: "closed"
764
+ };
765
+ }
721
766
  }
722
767
  await writeFile(
723
768
  OPENCLAW_CONFIG_PATH,
@@ -726,6 +771,10 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
726
771
  );
727
772
  return "updated";
728
773
  }
774
+ function isPastedShieldApiKeyInput(input) {
775
+ const trimmed = input.trim();
776
+ return trimmed.startsWith("mcs_") && trimmed.length >= 8;
777
+ }
729
778
  async function validateApiKey(apiKey, baseUrl) {
730
779
  try {
731
780
  const response = await fetch(`${baseUrl}/api/v1/agents`, {
@@ -1095,40 +1144,6 @@ async function installCodexCliNativeHooks() {
1095
1144
  };
1096
1145
  await writeFile(getCodexHooksJsonPath(), JSON.stringify(hooksConfig, null, 2) + "\n", "utf8");
1097
1146
  }
1098
- async function promptCodexCliIntegrationMode(ask) {
1099
- process.stderr.write("\n" + style.bold("Codex CLI integration") + "\n");
1100
- process.stderr.write(
1101
- " " + style.violet("1") + ". Native plugin - Shield checks terminal (Bash) commands via Codex Hooks\n"
1102
- );
1103
- process.stderr.write(
1104
- " " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via config.toml\n"
1105
- );
1106
- let choice = 0;
1107
- while (choice === 0) {
1108
- const input = await ask("Choose integration (1-2): ");
1109
- const num = parseInt(input.trim(), 10);
1110
- if (num === 1) choice = 1;
1111
- if (num === 2) choice = 2;
1112
- }
1113
- return choice === 1 ? "native" : "hosted";
1114
- }
1115
- async function promptClineIntegrationMode(ask) {
1116
- process.stderr.write("\n" + style.bold("Cline integration") + "\n");
1117
- process.stderr.write(
1118
- " " + style.violet("1") + ". Native plugin (recommended) - Cline Hooks see every file, terminal, browser, and MCP action\n"
1119
- );
1120
- process.stderr.write(
1121
- " " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Cline MCP settings)\n"
1122
- );
1123
- let choice = 0;
1124
- while (choice === 0) {
1125
- const input = await ask("Choose integration (1-2): ");
1126
- const num = parseInt(input.trim(), 10);
1127
- if (num === 1) choice = 1;
1128
- if (num === 2) choice = 2;
1129
- }
1130
- return choice === 1 ? "native" : "hosted";
1131
- }
1132
1147
  function getGeminiCliHooksInstallDir() {
1133
1148
  return join(homedir(), ".multicorn", "gemini-cli-hooks");
1134
1149
  }
@@ -1454,40 +1469,6 @@ async function installGeminiCliNativeHooks(ask) {
1454
1469
  await mkdir(dirname(settingsPath), { recursive: true });
1455
1470
  await writeFile(settingsPath, JSON.stringify(existing, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1456
1471
  }
1457
- async function promptGeminiCliIntegrationMode(ask) {
1458
- process.stderr.write("\n" + style.bold("Gemini CLI integration") + "\n");
1459
- process.stderr.write(
1460
- " " + style.violet("1") + ". Native plugin (recommended) - Gemini CLI Hooks see every file, terminal, web, and MCP action\n"
1461
- );
1462
- process.stderr.write(
1463
- " " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Gemini CLI settings)\n"
1464
- );
1465
- let choice = 0;
1466
- while (choice === 0) {
1467
- const input = await ask("Choose integration (1-2): ");
1468
- const num = parseInt(input.trim(), 10);
1469
- if (num === 1) choice = 1;
1470
- if (num === 2) choice = 2;
1471
- }
1472
- return choice === 1 ? "native" : "hosted";
1473
- }
1474
- async function promptOpencodeIntegrationMode(ask) {
1475
- process.stderr.write("\n" + style.bold("OpenCode integration") + "\n");
1476
- process.stderr.write(
1477
- " " + style.violet("1") + ". Native plugin (recommended) - Shield checks primary-agent tool execution via OpenCode Hooks\n"
1478
- );
1479
- process.stderr.write(
1480
- " " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via opencode.json (full subagent coverage when tools use MCP through Shield)\n"
1481
- );
1482
- let choice = 0;
1483
- while (choice === 0) {
1484
- const input = await ask("Choose integration (1-2): ");
1485
- const num = parseInt(input.trim(), 10);
1486
- if (num === 1) choice = 1;
1487
- if (num === 2) choice = 2;
1488
- }
1489
- return choice === 1 ? "native" : "hosted";
1490
- }
1491
1472
  function getClaudeDesktopConfigPath() {
1492
1473
  switch (process.platform) {
1493
1474
  case "win32":
@@ -1607,27 +1588,22 @@ var INIT_WIZARD_PLATFORM_REGISTRY = [
1607
1588
  },
1608
1589
  { slug: "other-mcp", displayName: "Local MCP / Other", section: "hosted" }
1609
1590
  ];
1610
- var INIT_WIZARD_MENU_SECTIONS = (() => {
1611
- const itemsFor = (section) => INIT_WIZARD_PLATFORM_REGISTRY.filter((e) => e.section === section).map((e) => ({
1612
- platform: e.slug,
1613
- label: e.displayName
1614
- }));
1615
- return [
1616
- { title: "Recommended (native plugin)", items: itemsFor("native") },
1617
- { title: "Hosted proxy (MCP only)", items: itemsFor("hosted") }
1618
- ];
1619
- })();
1620
- var INIT_WIZARD_SELECTION_MAX = INIT_WIZARD_PLATFORM_REGISTRY.length;
1591
+ var INIT_WIZARD_PICKER_NATIVE_SLUGS = INIT_WIZARD_PLATFORM_REGISTRY.filter((e) => e.section === "native").map((e) => e.slug);
1592
+ var INIT_WIZARD_DASHBOARD_AGENTS_URL = "https://app.multicorn.ai/agents/new";
1593
+ var INIT_WIZARD_PICKER_SELECTION_MAX = INIT_WIZARD_PICKER_NATIVE_SLUGS.length;
1594
+ var PICKER_SLUG_BY_SELECTION = Object.fromEntries(
1595
+ INIT_WIZARD_PICKER_NATIVE_SLUGS.map((slug, i) => [i + 1, slug])
1596
+ );
1621
1597
  var INIT_EXISTING_AGENTS_PLATFORM_ACTIONS = [
1622
1598
  "Add a new agent alongside these",
1623
1599
  "Replace an existing agent",
1624
1600
  "Skip - choose a different platform"
1625
1601
  ];
1626
- var PLATFORM_BY_SELECTION = Object.fromEntries(
1627
- INIT_WIZARD_PLATFORM_REGISTRY.map((e, i) => [i + 1, e.slug])
1628
- );
1629
- function platformMenuLabelForSelection(sel) {
1630
- const slug = PLATFORM_BY_SELECTION[sel];
1602
+ function resolveInitHybridIntegrationMode(explicit) {
1603
+ return explicit ?? "native";
1604
+ }
1605
+ function platformMenuLabelForPickerSelection(sel) {
1606
+ const slug = PICKER_SLUG_BY_SELECTION[sel];
1631
1607
  if (slug === void 0) return "Unknown";
1632
1608
  const entry = INIT_WIZARD_PLATFORM_REGISTRY.find((e) => e.slug === slug);
1633
1609
  return entry?.displayName ?? slug;
@@ -1645,48 +1621,31 @@ async function promptPlatformSelection(ask) {
1645
1621
  process.stderr.write(
1646
1622
  "\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n\n"
1647
1623
  );
1624
+ process.stderr.write(" " + style.dim("Native plugin (files, terminal, browser)") + "\n");
1648
1625
  let optionNum = 1;
1649
- for (const section of INIT_WIZARD_MENU_SECTIONS) {
1650
- process.stderr.write(" " + style.dim(section.title) + "\n");
1651
- for (const item of section.items) {
1652
- const indent = optionNum >= 10 ? " " : " ";
1653
- process.stderr.write(`${indent}${style.violet(String(optionNum))}. ${item.label}
1626
+ for (const slug of INIT_WIZARD_PICKER_NATIVE_SLUGS) {
1627
+ const entry = INIT_WIZARD_PLATFORM_REGISTRY.find((e) => e.slug === slug);
1628
+ const label = entry?.displayName ?? slug;
1629
+ const indent = optionNum >= 10 ? " " : " ";
1630
+ process.stderr.write(`${indent}${style.violet(String(optionNum))}. ${label}
1654
1631
  `);
1655
- optionNum++;
1656
- }
1632
+ optionNum++;
1657
1633
  }
1658
- process.stderr.write(
1659
- "\n" + style.dim(
1660
- ` Pick ${String(INIT_WIZARD_SELECTION_MAX)} to wrap a local MCP server with multicorn-shield --wrap.`
1661
- ) + "\n"
1662
- );
1634
+ process.stderr.write("\n");
1635
+ process.stderr.write(" MCP agents (Cursor, Copilot, others) are set up\n");
1636
+ process.stderr.write(` in the dashboard: ${INIT_WIZARD_DASHBOARD_AGENTS_URL}
1637
+
1638
+ `);
1663
1639
  let selection = 0;
1664
1640
  while (selection === 0) {
1665
- const input = await ask(`Select (1-${String(INIT_WIZARD_SELECTION_MAX)}): `);
1641
+ const input = await ask(`Select (1-${String(INIT_WIZARD_PICKER_SELECTION_MAX)}): `);
1666
1642
  const num = parseInt(input.trim(), 10);
1667
- if (num >= 1 && num <= INIT_WIZARD_SELECTION_MAX) {
1643
+ if (num >= 1 && num <= INIT_WIZARD_PICKER_SELECTION_MAX) {
1668
1644
  selection = num;
1669
1645
  }
1670
1646
  }
1671
1647
  return selection;
1672
1648
  }
1673
- async function promptWindsurfIntegrationMode(ask) {
1674
- process.stderr.write("\n" + style.bold("Windsurf integration") + "\n");
1675
- process.stderr.write(
1676
- " " + style.violet("1") + ". Native plugin (recommended) - Cascade Hooks see every file, terminal, and MCP action\n"
1677
- );
1678
- process.stderr.write(
1679
- " " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into mcp_config)\n"
1680
- );
1681
- let choice = 0;
1682
- while (choice === 0) {
1683
- const input = await ask("Choose integration (1-2): ");
1684
- const num = parseInt(input.trim(), 10);
1685
- if (num === 1) choice = 1;
1686
- if (num === 2) choice = 2;
1687
- }
1688
- return choice === 1 ? "native" : "hosted";
1689
- }
1690
1649
  async function arrowSelect(options, ask, fallbackLabel) {
1691
1650
  if (options.length === 1) {
1692
1651
  const only = options[0] ?? "";
@@ -1899,6 +1858,14 @@ var HOSTED_PROXY_PLATFORMS_WITH_URL_KEY = /* @__PURE__ */ new Set([
1899
1858
  function shouldEmbedKeyInHostedProxyUrl(platform) {
1900
1859
  return HOSTED_PROXY_PLATFORMS_WITH_URL_KEY.has(platform);
1901
1860
  }
1861
+ function buildClineHostedMcpEntry(url) {
1862
+ return {
1863
+ type: "streamableHttp",
1864
+ url,
1865
+ timeout: 60,
1866
+ disabled: false
1867
+ };
1868
+ }
1902
1869
  function hostedProxyUrlWithKeyParam(proxyUrl, apiKey) {
1903
1870
  if (apiKey.length === 0) {
1904
1871
  process.stderr.write(
@@ -2303,9 +2270,11 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
2303
2270
  printHostedProxyJsonParseWarning(getWindsurfMcpConfigPath());
2304
2271
  }
2305
2272
  } else if (platform === "cline") {
2306
- result = await mergeMcpServersObjectStyle(getClineMcpSettingsPath(), shortName, {
2307
- url: proxyUrlWithKeyWhenNeeded
2308
- });
2273
+ result = await mergeMcpServersObjectStyle(
2274
+ getClineMcpSettingsPath(),
2275
+ shortName,
2276
+ buildClineHostedMcpEntry(proxyUrlWithKeyWhenNeeded)
2277
+ );
2309
2278
  if (result === "parse-error") {
2310
2279
  printHostedProxyJsonParseWarning(getClineMcpSettingsPath());
2311
2280
  }
@@ -2595,9 +2564,7 @@ Authorization = "Bearer ${bearerToken}"
2595
2564
  snippetText = platform === "cline" ? JSON.stringify(
2596
2565
  {
2597
2566
  mcpServers: {
2598
- [shortName]: {
2599
- [urlKey]: urlInSnippet
2600
- }
2567
+ [shortName]: buildClineHostedMcpEntry(urlInSnippet)
2601
2568
  }
2602
2569
  },
2603
2570
  null,
@@ -2761,6 +2728,7 @@ function mergeAgentsForPlatform(localAgents, remoteAgents, selectedPlatform) {
2761
2728
  var DEFAULT_SHIELD_API_BASE_URL = "https://api.multicorn.ai";
2762
2729
  async function runInit(explicitBaseUrl, options) {
2763
2730
  const verbose = options?.verbose === true;
2731
+ const hybridIntegration = resolveInitHybridIntegrationMode(options?.integration);
2764
2732
  if (!process.stdin.isTTY) {
2765
2733
  process.stderr.write(
2766
2734
  style.red("Error: interactive terminal required. Cannot run init with piped input.") + "\n"
@@ -2805,9 +2773,31 @@ async function runInit(explicitBaseUrl, options) {
2805
2773
  if (existing !== null && existing.apiKey.startsWith("mcs_") && existing.apiKey.length >= 8) {
2806
2774
  const masked = "mcs_..." + existing.apiKey.slice(-4);
2807
2775
  process.stderr.write("Found existing API key: " + style.cyan(masked) + "\n");
2808
- const answer = await ask("Use this key? (Y/n) ");
2809
- if (answer.trim().toLowerCase() !== "n") {
2810
- apiKey = existing.apiKey;
2776
+ const answer = await ask("Use this key? (Y/n, or paste a new mcs_ key) ");
2777
+ const trimmed = answer.trim();
2778
+ if (trimmed.toLowerCase() !== "n") {
2779
+ const candidate = isPastedShieldApiKeyInput(trimmed) ? trimmed : existing.apiKey;
2780
+ const spinner = withSpinner("Validating key...");
2781
+ let reuseResult;
2782
+ try {
2783
+ reuseResult = await validateApiKey(candidate, resolvedBaseUrl);
2784
+ } catch (error) {
2785
+ spinner.stop(false, "Validation failed");
2786
+ throw error;
2787
+ }
2788
+ if (reuseResult.valid) {
2789
+ spinner.stop(true, "Key validated");
2790
+ apiKey = candidate;
2791
+ } else {
2792
+ spinner.stop(false, reuseResult.error ?? "Validation failed.");
2793
+ if (isPastedShieldApiKeyInput(trimmed)) {
2794
+ process.stderr.write(style.red("That key was rejected. Enter a new key below.") + "\n");
2795
+ } else {
2796
+ process.stderr.write(
2797
+ style.yellow("Existing key is no longer valid. Enter a new key below.") + "\n"
2798
+ );
2799
+ }
2800
+ }
2811
2801
  }
2812
2802
  }
2813
2803
  if (apiKey.length === 0) {
@@ -2859,8 +2849,8 @@ async function runInit(explicitBaseUrl, options) {
2859
2849
  let removeAgentNameBeforeSave = void 0;
2860
2850
  const initWorkspacePath = resolve(process.cwd());
2861
2851
  const selection = await promptPlatformSelection(ask);
2862
- const selectedPlatform = PLATFORM_BY_SELECTION[selection] ?? "cursor";
2863
- const selectedLabel = platformMenuLabelForSelection(selection);
2852
+ const selectedPlatform = PICKER_SLUG_BY_SELECTION[selection] ?? "openclaw";
2853
+ const selectedLabel = platformMenuLabelForPickerSelection(selection);
2864
2854
  if (selectedPlatform === "other-mcp") {
2865
2855
  const raw = existing !== null ? { ...existing } : {};
2866
2856
  raw["apiKey"] = apiKey;
@@ -2994,7 +2984,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2994
2984
  }
2995
2985
  if (detection.status === "parse-error") {
2996
2986
  process.stderr.write(
2997
- style.red("\u2717") + " Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually.\n"
2987
+ 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"
2998
2988
  );
2999
2989
  }
3000
2990
  if (detection.status === "detected") {
@@ -3034,7 +3024,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3034
3024
  if (result === "parse-error") {
3035
3025
  spinner.stop(
3036
3026
  false,
3037
- "Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually."
3027
+ "Could not update OpenClaw config. Set plugins.entries.multicorn-shield.config.apiKey in ~/.openclaw/openclaw.json, or run npx multicorn-shield init."
3038
3028
  );
3039
3029
  } else {
3040
3030
  spinner.stop(
@@ -3081,7 +3071,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3081
3071
  });
3082
3072
  setupSucceeded = true;
3083
3073
  } else if (selectedPlatform === "windsurf") {
3084
- const windsurfMode = await promptWindsurfIntegrationMode(ask);
3074
+ const windsurfMode = hybridIntegration;
3085
3075
  if (windsurfMode === "native") {
3086
3076
  try {
3087
3077
  await installWindsurfNativeHooks();
@@ -3180,7 +3170,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3180
3170
  }
3181
3171
  }
3182
3172
  } else if (selectedPlatform === "gemini-cli") {
3183
- const geminiMode = await promptGeminiCliIntegrationMode(ask);
3173
+ const geminiMode = hybridIntegration;
3184
3174
  if (geminiMode === "native") {
3185
3175
  try {
3186
3176
  await installGeminiCliNativeHooks(ask);
@@ -3274,7 +3264,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3274
3264
  }
3275
3265
  }
3276
3266
  } else if (selectedPlatform === "opencode") {
3277
- const opencodeMode = await promptOpencodeIntegrationMode(ask);
3267
+ const opencodeMode = hybridIntegration;
3278
3268
  if (opencodeMode === "native") {
3279
3269
  try {
3280
3270
  await installOpenCodeNativePlugin();
@@ -3363,7 +3353,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3363
3353
  }
3364
3354
  }
3365
3355
  } else if (selectedPlatform === "codex-cli") {
3366
- const codexMode = await promptCodexCliIntegrationMode(ask);
3356
+ const codexMode = hybridIntegration;
3367
3357
  if (codexMode === "native") {
3368
3358
  try {
3369
3359
  await installCodexCliNativeHooks();
@@ -3452,7 +3442,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3452
3442
  }
3453
3443
  }
3454
3444
  } else if (selectedPlatform === "cline") {
3455
- const clineMode = await promptClineIntegrationMode(ask);
3445
+ const clineMode = hybridIntegration;
3456
3446
  if (clineMode === "native") {
3457
3447
  try {
3458
3448
  await installClineNativeHooks();
@@ -3935,9 +3925,7 @@ async function writeLocalMcpEntry(client, agentName, localProxyUrl, apiKey, work
3935
3925
  return await mergeMcpServersObjectStyle(
3936
3926
  filePath,
3937
3927
  entryKey,
3938
- {
3939
- url
3940
- },
3928
+ buildClineHostedMcpEntry(url),
3941
3929
  legacyKeys
3942
3930
  ) === "ok" ? filePath : null;
3943
3931
  case "windsurf":
@@ -4985,10 +4973,95 @@ async function restoreClaudeDesktopMcpFromBackup() {
4985
4973
 
4986
4974
  // package.json
4987
4975
  var package_default = {
4988
- version: "1.11.0"};
4976
+ version: "1.12.0"};
4989
4977
 
4990
4978
  // src/package-meta.ts
4991
4979
  var PACKAGE_VERSION = package_default.version;
4980
+ var LOCAL_PROXY_READY_POLL_MS = 500;
4981
+ var LOCAL_PROXY_READY_MAX_POLLS = 30;
4982
+ var NOT_FOUND_MESSAGE = "Local proxy server entry (dist/server.js) not found. Reinstall multicorn-shield or run pnpm build.";
4983
+ function findMulticornShieldPackageRoot(moduleDir = dirname(fileURLToPath(import.meta.url))) {
4984
+ let current = moduleDir;
4985
+ for (; ; ) {
4986
+ const pkgPath = join(current, "package.json");
4987
+ if (existsSync(pkgPath)) {
4988
+ try {
4989
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
4990
+ if (pkg.name === "multicorn-shield") {
4991
+ return current;
4992
+ }
4993
+ } catch {
4994
+ }
4995
+ }
4996
+ const parent = dirname(current);
4997
+ if (parent === current) {
4998
+ break;
4999
+ }
5000
+ current = parent;
5001
+ }
5002
+ try {
5003
+ const req = createRequire(import.meta.url);
5004
+ return dirname(req.resolve("multicorn-shield/package.json"));
5005
+ } catch {
5006
+ throw new Error(NOT_FOUND_MESSAGE);
5007
+ }
5008
+ }
5009
+ function resolveLocalProxyServerEntry(options) {
5010
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
5011
+ const candidates = [
5012
+ join(findMulticornShieldPackageRoot(moduleDir), "dist", "server.js")
5013
+ ];
5014
+ try {
5015
+ const req = createRequire(import.meta.url);
5016
+ const proxyRoot = dirname(req.resolve("multicorn-proxy/package.json"));
5017
+ candidates.unshift(join(proxyRoot, "dist", "server.js"));
5018
+ } catch {
5019
+ }
5020
+ for (const path of candidates) {
5021
+ if (existsSync(path)) return path;
5022
+ }
5023
+ throw new Error(NOT_FOUND_MESSAGE);
5024
+ }
5025
+ function buildLocalProxySpawnEnv(port, apiBaseUrl) {
5026
+ return {
5027
+ PORT: String(port),
5028
+ HOST: "127.0.0.1",
5029
+ SHIELD_API_BASE_URL: apiBaseUrl,
5030
+ ALLOW_PRIVATE_TARGETS: "true"
5031
+ };
5032
+ }
5033
+ function buildLocalProxySpawnCommand(port, apiBaseUrl, execPath = process.execPath, serverEntryPath = resolveLocalProxyServerEntry()) {
5034
+ const env = buildLocalProxySpawnEnv(port, apiBaseUrl);
5035
+ return {
5036
+ executable: execPath,
5037
+ args: [serverEntryPath],
5038
+ env,
5039
+ serverEntryPath
5040
+ };
5041
+ }
5042
+ function readProxyLogTail(logPath, maxBytes = 8e3) {
5043
+ try {
5044
+ const content = readFileSync(logPath, "utf8");
5045
+ if (content.length <= maxBytes) return content;
5046
+ return content.slice(-maxBytes);
5047
+ } catch {
5048
+ return "";
5049
+ }
5050
+ }
5051
+ function formatLocalProxyStartError(port, logPath, childExited, exitCode) {
5052
+ const logTail = readProxyLogTail(logPath);
5053
+ const reason = childExited ? `Proxy process exited early${exitCode === null ? "" : ` (code ${String(exitCode)})`}.` : `Proxy did not become ready on port ${String(port)} within the timeout.`;
5054
+ let message = `Could not start the local proxy on port ${String(port)}. ${reason}`;
5055
+ message += logTail.length > 0 ? `
5056
+
5057
+ Output from ${logPath}:
5058
+ ${logTail.trimEnd()}` : `
5059
+
5060
+ See ${logPath} for details.`;
5061
+ return message;
5062
+ }
5063
+
5064
+ // src/commands/files.ts
4992
5065
  var DEFAULT_FS_PORT = 3005;
4993
5066
  var DEFAULT_PROXY_PORT = 3001;
4994
5067
  var PIDFILE_DIR = process.env["MULTICORN_HOME"] ?? join(homedir(), ".multicorn");
@@ -5023,6 +5096,16 @@ function readPidfile(agent) {
5023
5096
  return null;
5024
5097
  }
5025
5098
  }
5099
+ function supervisorPidFromSession(data) {
5100
+ if (typeof data.supervisorPid === "number") return data.supervisorPid;
5101
+ if (typeof data.pid === "number") return data.pid;
5102
+ return void 0;
5103
+ }
5104
+ function isAgentSessionRunning(data) {
5105
+ if (data === null) return false;
5106
+ const pid = supervisorPidFromSession(data);
5107
+ return typeof pid === "number" && isProcessAlive(pid);
5108
+ }
5026
5109
  function removePidfile(agent) {
5027
5110
  const p = pidfilePath(agent);
5028
5111
  try {
@@ -5063,7 +5146,7 @@ function runStatus() {
5063
5146
  const fsReg = readFsRegistry();
5064
5147
  process.stderr.write("Active sessions:\n\n");
5065
5148
  for (const s of sessions) {
5066
- const supervisorAlive = typeof s.supervisorPid === "number" && isProcessAlive(s.supervisorPid);
5149
+ const supervisorAlive = isAgentSessionRunning(s);
5067
5150
  const fsEntry = fsReg[s.dir];
5068
5151
  const fsAlive = fsEntry !== void 0 && isProcessAlive(fsEntry.pid);
5069
5152
  const proxyAlive = proxyReg !== null && proxyReg.port === s.proxyPort && isProcessAlive(proxyReg.pid);
@@ -5210,9 +5293,7 @@ async function withResourceLock(fn) {
5210
5293
  }
5211
5294
  }
5212
5295
  function liveAgents() {
5213
- return listAllPidfiles().filter(
5214
- (p) => typeof p.supervisorPid === "number" && isProcessAlive(p.supervisorPid)
5215
- );
5296
+ return listAllPidfiles().filter((p) => isAgentSessionRunning(p));
5216
5297
  }
5217
5298
  function agentsReferencingProxy(proxyPort, agents, excludeAgent) {
5218
5299
  return agents.filter((a) => a.agent !== excludeAgent && a.proxyPort === proxyPort);
@@ -5230,27 +5311,37 @@ async function nextFreePort(start, claimed, isBusy, range = FS_PORT_SCAN_RANGE)
5230
5311
  `No free port for the filesystem server in range ${String(start)}-${String(start + range)}.`
5231
5312
  );
5232
5313
  }
5314
+ async function resolveBaseUrl(explicitBaseUrl) {
5315
+ if (explicitBaseUrl !== void 0 && explicitBaseUrl.trim().length > 0) {
5316
+ return explicitBaseUrl.trim();
5317
+ }
5318
+ const envBaseUrl = process.env["MULTICORN_BASE_URL"];
5319
+ if (typeof envBaseUrl === "string" && envBaseUrl.trim().length > 0) {
5320
+ return envBaseUrl.trim();
5321
+ }
5322
+ const config = await loadConfig();
5323
+ if (config !== null && config.baseUrl.length > 0) {
5324
+ return config.baseUrl;
5325
+ }
5326
+ const fromFile = await readBaseUrlFromConfig();
5327
+ if (fromFile !== void 0 && fromFile.length > 0) {
5328
+ return fromFile;
5329
+ }
5330
+ return DEFAULT_SHIELD_API_BASE_URL;
5331
+ }
5233
5332
  async function resolveConfig(opts) {
5333
+ const baseUrl = await resolveBaseUrl(opts.baseUrl);
5234
5334
  const fromFlag = opts.apiKey;
5235
5335
  if (fromFlag && fromFlag.length > 0) {
5236
- return {
5237
- apiKey: fromFlag,
5238
- baseUrl: opts.baseUrl ?? DEFAULT_SHIELD_API_BASE_URL
5239
- };
5336
+ return { apiKey: fromFlag, baseUrl };
5240
5337
  }
5241
5338
  const envKey = process.env["MULTICORN_API_KEY"];
5242
5339
  if (typeof envKey === "string" && envKey.length > 0) {
5243
- return {
5244
- apiKey: envKey,
5245
- baseUrl: opts.baseUrl ?? DEFAULT_SHIELD_API_BASE_URL
5246
- };
5340
+ return { apiKey: envKey, baseUrl };
5247
5341
  }
5248
5342
  const config = await loadConfig();
5249
5343
  if (config !== null) {
5250
- return {
5251
- apiKey: config.apiKey,
5252
- baseUrl: opts.baseUrl ?? config.baseUrl
5253
- };
5344
+ return { apiKey: config.apiKey, baseUrl };
5254
5345
  }
5255
5346
  process.stderr.write(
5256
5347
  "No API key found. Pass --api-key <key> or add it to ~/.multicorn/config.json.\n"
@@ -5290,26 +5381,40 @@ function startLocalProxyDetached(port, apiBaseUrl) {
5290
5381
  const logFile = join(PIDFILE_DIR, "proxy.log");
5291
5382
  const out = openSync(logFile, "a");
5292
5383
  const err = openSync(logFile, "a");
5293
- const child = spawn("npx", ["multicorn-proxy"], {
5384
+ const { executable, args, env: proxyEnv } = buildLocalProxySpawnCommand(port, apiBaseUrl);
5385
+ const child = spawn(executable, [...args], {
5294
5386
  stdio: ["ignore", out, err],
5295
5387
  detached: true,
5296
5388
  env: {
5297
5389
  ...process.env,
5298
- PORT: String(port),
5299
- HOST: "127.0.0.1",
5300
- SHIELD_API_BASE_URL: apiBaseUrl,
5390
+ ...proxyEnv
5301
5391
  // SAFETY: blanket-allow for private targets is acceptable ONLY because
5302
5392
  // this is a local single-user proxy. The only registered target is the
5303
5393
  // filesystem server on the same machine. Do NOT copy this pattern to a
5304
5394
  // multi-tenant or hosted proxy deployment.
5305
- ALLOW_PRIVATE_TARGETS: "true"
5306
5395
  }
5307
5396
  });
5308
5397
  child.unref();
5309
5398
  return child;
5310
5399
  }
5311
- async function ensureProxy(proxyPort, apiBaseUrl) {
5312
- if (await probeProxyHealth(proxyPort)) {
5400
+ async function ensureProxy(proxyPort, apiBaseUrl, options) {
5401
+ const forceRespawn = options?.forceRespawn === true;
5402
+ if (forceRespawn) {
5403
+ const reg2 = readProxyRegistry();
5404
+ if (reg2 !== null && reg2.port === proxyPort && isProcessAlive(reg2.pid)) {
5405
+ killWithEscalation(reg2.pid, true);
5406
+ }
5407
+ if (reg2 !== null && reg2.port === proxyPort) {
5408
+ try {
5409
+ unlinkSync(PROXY_REGISTRY);
5410
+ } catch {
5411
+ }
5412
+ }
5413
+ for (let i = 0; i < 20; i++) {
5414
+ if (!await isPortListening(proxyPort)) break;
5415
+ await sleep3(100);
5416
+ }
5417
+ } else if (await probeProxyHealth(proxyPort)) {
5313
5418
  const reg2 = readProxyRegistry();
5314
5419
  const managed = reg2 !== null && reg2.port === proxyPort && isProcessAlive(reg2.pid);
5315
5420
  return { reused: true, managed };
@@ -5326,9 +5431,11 @@ async function ensureProxy(proxyPort, apiBaseUrl) {
5326
5431
  `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>.`
5327
5432
  );
5328
5433
  }
5434
+ const logFile = join(PIDFILE_DIR, "proxy.log");
5329
5435
  const child = startLocalProxyDetached(proxyPort, apiBaseUrl);
5330
- for (let i = 0; i < 30; i++) {
5331
- await sleep3(500);
5436
+ for (let i = 0; i < LOCAL_PROXY_READY_MAX_POLLS; i++) {
5437
+ await sleep3(LOCAL_PROXY_READY_POLL_MS);
5438
+ if (child.exitCode !== null || child.signalCode !== null) break;
5332
5439
  if (await probeProxyHealth(proxyPort)) {
5333
5440
  if (child.pid !== void 0) {
5334
5441
  writeJsonFile(PROXY_REGISTRY, { pid: child.pid, port: proxyPort });
@@ -5340,9 +5447,8 @@ async function ensureProxy(proxyPort, apiBaseUrl) {
5340
5447
  if (child.pid !== void 0) killWithEscalation(child.pid, true);
5341
5448
  } catch {
5342
5449
  }
5343
- throw new Error(
5344
- `Could not start the local proxy on port ${String(proxyPort)}. Check the log at ${join(PIDFILE_DIR, "proxy.log")}.`
5345
- );
5450
+ const childExited = child.exitCode !== null || child.signalCode !== null;
5451
+ throw new Error(formatLocalProxyStartError(proxyPort, logFile, childExited, child.exitCode));
5346
5452
  }
5347
5453
  async function ensureFsServer(realDir, requestedPort) {
5348
5454
  const reg = readFsRegistry();
@@ -5545,8 +5651,9 @@ async function runStop(agent) {
5545
5651
  process.exit(1);
5546
5652
  }
5547
5653
  await withResourceLock(() => {
5548
- if (typeof data.supervisorPid === "number" && isProcessAlive(data.supervisorPid)) {
5549
- killWithEscalation(data.supervisorPid);
5654
+ const supervisorPid = supervisorPidFromSession(data);
5655
+ if (typeof supervisorPid === "number" && isProcessAlive(supervisorPid)) {
5656
+ killWithEscalation(supervisorPid);
5550
5657
  }
5551
5658
  removePidfile(agent);
5552
5659
  releaseFsServerIfUnused(data.dir, agent);
@@ -5576,7 +5683,8 @@ Run it once with the folder: npx multicorn-shield files restart <dir> --agent ${
5576
5683
  await runDetached({
5577
5684
  ...opts,
5578
5685
  dir,
5579
- proxyPort: opts.proxyPort ?? existing?.proxyPort});
5686
+ proxyPort: opts.proxyPort ?? existing?.proxyPort,
5687
+ respawnProxy: true});
5580
5688
  }
5581
5689
  async function runDetached(opts) {
5582
5690
  const absDir = resolve(opts.dir);
@@ -5587,8 +5695,7 @@ async function runDetached(opts) {
5587
5695
  }
5588
5696
  const existing = readPidfile(opts.agent);
5589
5697
  if (existing !== null) {
5590
- const alive = typeof existing.supervisorPid === "number" && isProcessAlive(existing.supervisorPid);
5591
- if (alive) {
5698
+ if (isAgentSessionRunning(existing)) {
5592
5699
  process.stderr.write(
5593
5700
  `Already running for agent "${opts.agent}" (fs :${String(existing.fsPort)}, proxy :${String(existing.proxyPort)}).
5594
5701
  Stop with: npx multicorn-shield files stop --agent ${opts.agent}
@@ -5603,6 +5710,7 @@ Stop with: npx multicorn-shield files stop --agent ${opts.agent}
5603
5710
  if (opts.proxyPort !== void 0) args.push("--proxy-port", String(opts.proxyPort));
5604
5711
  if (opts.apiKey !== void 0) args.push("--api-key", opts.apiKey);
5605
5712
  if (opts.baseUrl !== void 0) args.push("--base-url", opts.baseUrl);
5713
+ if (opts.respawnProxy) args.push("--respawn-proxy");
5606
5714
  if (opts.client !== void 0) args.push("--client", opts.client);
5607
5715
  const scriptPath = process.argv[1] ?? resolve("dist/multicorn-shield.js");
5608
5716
  const logFile = join(PIDFILE_DIR, `files-${opts.agent}.log`);
@@ -5686,8 +5794,7 @@ async function runFilesCommand(opts) {
5686
5794
  const proxyPort = opts.proxyPort ?? DEFAULT_PROXY_PORT;
5687
5795
  const existingPidfile = readPidfile(opts.agent);
5688
5796
  if (existingPidfile !== null) {
5689
- const supervisorAlive = typeof existingPidfile.supervisorPid === "number" && isProcessAlive(existingPidfile.supervisorPid);
5690
- if (supervisorAlive) {
5797
+ if (isAgentSessionRunning(existingPidfile)) {
5691
5798
  process.stderr.write(
5692
5799
  `A session for agent "${opts.agent}" is already running. Run 'files stop --agent ${opts.agent}' first, or use a different --agent name.
5693
5800
  `
@@ -5701,7 +5808,9 @@ async function runFilesCommand(opts) {
5701
5808
  let fsReused;
5702
5809
  try {
5703
5810
  const ensured = await withResourceLock(async () => {
5704
- const proxyRes = await ensureProxy(proxyPort, config.baseUrl);
5811
+ const proxyRes = await ensureProxy(proxyPort, config.baseUrl, {
5812
+ forceRespawn: opts.respawnProxy
5813
+ });
5705
5814
  const fsRes = await ensureFsServer(realDir, opts.port);
5706
5815
  writePidfile({
5707
5816
  agent: opts.agent,
@@ -5968,6 +6077,8 @@ function parseArgs(argv) {
5968
6077
  let filesForeground = false;
5969
6078
  let filesStatus = false;
5970
6079
  let filesRestart = false;
6080
+ let filesRespawnProxy = false;
6081
+ let initIntegration = void 0;
5971
6082
  for (let i = 0; i < args.length; i++) {
5972
6083
  const arg = args[i];
5973
6084
  if (arg === "init") {
@@ -6018,6 +6129,8 @@ function parseArgs(argv) {
6018
6129
  filesStop = true;
6019
6130
  } else if (token === "--foreground") {
6020
6131
  filesForeground = true;
6132
+ } else if (token === "--respawn-proxy") {
6133
+ filesRespawnProxy = true;
6021
6134
  } else if (token === "--detach") ; else if (token === "stop") {
6022
6135
  filesStop = true;
6023
6136
  } else if (token === "status") {
@@ -6126,6 +6239,12 @@ function parseArgs(argv) {
6126
6239
  }
6127
6240
  } else if (arg === "--verbose" || arg === "--debug") {
6128
6241
  verbose = true;
6242
+ } else if (arg === "--integration") {
6243
+ const next = args[i + 1];
6244
+ if (next === "native" || next === "hosted") {
6245
+ initIntegration = next;
6246
+ i++;
6247
+ }
6129
6248
  }
6130
6249
  }
6131
6250
  return {
@@ -6139,6 +6258,7 @@ function parseArgs(argv) {
6139
6258
  deleteAgentName,
6140
6259
  apiKey,
6141
6260
  verbose,
6261
+ initIntegration,
6142
6262
  filesDir,
6143
6263
  filesPort,
6144
6264
  filesProxyPort,
@@ -6146,7 +6266,8 @@ function parseArgs(argv) {
6146
6266
  filesClient,
6147
6267
  filesForeground,
6148
6268
  filesStatus,
6149
- filesRestart
6269
+ filesRestart,
6270
+ filesRespawnProxy
6150
6271
  };
6151
6272
  }
6152
6273
  function printHelp() {
@@ -6156,7 +6277,12 @@ function printHelp() {
6156
6277
  "",
6157
6278
  "Usage:",
6158
6279
  " npx multicorn-shield init",
6159
- " Interactive setup. Saves API key to ~/.multicorn/config.json.",
6280
+ " Interactive setup for native plugins (files, terminal, browser).",
6281
+ " Native: OpenClaw, Claude Code, Windsurf, Cline, Gemini CLI, OpenCode, Codex CLI.",
6282
+ " MCP-only clients (Cursor, Copilot, etc.): https://app.multicorn.ai/agents/new",
6283
+ " Saves API key to ~/.multicorn/config.json.",
6284
+ " --integration hosted Use hosted MCP proxy for Windsurf, Cline, Gemini CLI,",
6285
+ " OpenCode, or Codex CLI (default: native).",
6160
6286
  "",
6161
6287
  " npx multicorn-shield files <dir> --agent <name> [--client <client>]",
6162
6288
  " Share a local folder with a coding agent. Starts a filesystem MCP server",
@@ -6237,7 +6363,10 @@ async function runCli() {
6237
6363
  process.exit(0);
6238
6364
  }
6239
6365
  if (cli.subcommand === "init") {
6240
- await runInit(cli.baseUrl, { verbose: cli.verbose });
6366
+ await runInit(cli.baseUrl, {
6367
+ verbose: cli.verbose,
6368
+ ...cli.initIntegration !== void 0 ? { integration: cli.initIntegration } : {}
6369
+ });
6241
6370
  return;
6242
6371
  }
6243
6372
  if (cli.subcommand === "files") {
@@ -6253,7 +6382,8 @@ async function runCli() {
6253
6382
  client: void 0,
6254
6383
  foreground: true,
6255
6384
  status: true,
6256
- restart: false
6385
+ restart: false,
6386
+ respawnProxy: false
6257
6387
  });
6258
6388
  return;
6259
6389
  }
@@ -6278,7 +6408,8 @@ async function runCli() {
6278
6408
  client: cli.filesClient,
6279
6409
  foreground: cli.filesForeground,
6280
6410
  status: false,
6281
- restart: cli.filesRestart
6411
+ restart: cli.filesRestart,
6412
+ respawnProxy: cli.filesRespawnProxy
6282
6413
  });
6283
6414
  return;
6284
6415
  }