multicorn-shield 1.3.2 → 1.3.5

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.
@@ -344,6 +344,7 @@ function isPermissionShape(value) {
344
344
  }
345
345
 
346
346
  // src/proxy/config.ts
347
+ var SECRET_JSON_FILE_OPTIONS = { encoding: "utf8", mode: 384 };
347
348
  var style = {
348
349
  violet: (s) => `\x1B[38;2;124;58;237m${s}\x1B[0m`,
349
350
  violetLight: (s) => `\x1B[38;2;167;139;250m${s}\x1B[0m`,
@@ -703,9 +704,11 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
703
704
  }
704
705
  }
705
706
  }
706
- await writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n", {
707
- encoding: "utf8"
708
- });
707
+ await writeFile(
708
+ OPENCLAW_CONFIG_PATH,
709
+ JSON.stringify(obj, null, 2) + "\n",
710
+ SECRET_JSON_FILE_OPTIONS
711
+ );
709
712
  return "updated";
710
713
  }
711
714
  async function validateApiKey(apiKey, baseUrl) {
@@ -867,7 +870,7 @@ async function installWindsurfNativeHooks() {
867
870
  base["hooks"] = nextHooks;
868
871
  const hooksDir = dirname(hooksPath);
869
872
  await mkdir(hooksDir, { recursive: true });
870
- await writeFile(hooksPath, JSON.stringify(base, null, 2) + "\n", { encoding: "utf8" });
873
+ await writeFile(hooksPath, JSON.stringify(base, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
871
874
  }
872
875
  function getClineHooksInstallDir() {
873
876
  return join(homedir(), ".multicorn", "cline-hooks");
@@ -946,6 +949,37 @@ function getGeminiCliHooksInstallDir() {
946
949
  function getGeminiCliSettingsPath() {
947
950
  return join(homedir(), ".gemini", "settings.json");
948
951
  }
952
+ async function mergeGeminiHostedMcpServersIntoSettings(shortName, proxyUrl, apiKey) {
953
+ const settingsPath = getGeminiCliSettingsPath();
954
+ let existing = {};
955
+ try {
956
+ const rawText = await readFile(settingsPath, "utf8");
957
+ const parsed = JSON.parse(rawText);
958
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
959
+ existing = parsed;
960
+ }
961
+ } catch (err) {
962
+ if (isErrnoException(err) && err.code === "ENOENT") {
963
+ existing = {};
964
+ } else {
965
+ const detail = err instanceof Error ? err.message : String(err);
966
+ throw new Error(`Could not read or parse Gemini CLI settings at ${settingsPath}: ${detail}`);
967
+ }
968
+ }
969
+ const mcpRaw = existing["mcpServers"];
970
+ const mcpServers = typeof mcpRaw === "object" && mcpRaw !== null && !Array.isArray(mcpRaw) ? { ...mcpRaw } : {};
971
+ mcpServers[shortName] = {
972
+ httpUrl: proxyUrl,
973
+ headers: {
974
+ Authorization: `Bearer ${apiKey}`
975
+ }
976
+ };
977
+ const out = { ...existing, mcpServers };
978
+ await mkdir(dirname(settingsPath), { recursive: true });
979
+ const serialized = JSON.stringify(out, null, 2) + "\n";
980
+ await writeFile(settingsPath, serialized, SECRET_JSON_FILE_OPTIONS);
981
+ writeMcpAddedLine(shortName, settingsPath);
982
+ }
949
983
  function geminiInnerHooksReferenceShield(inner, multicornName) {
950
984
  if (!Array.isArray(inner)) return false;
951
985
  for (const h of inner) {
@@ -1130,7 +1164,7 @@ async function installClaudeCodeUserSettingsHooks(ask) {
1130
1164
  hooksObj["PostToolUse"] = postArr;
1131
1165
  const out = { ...existing, hooks: hooksObj };
1132
1166
  const serialized = JSON.stringify(out, null, 2) + "\n";
1133
- await writeFile(settingsPath, serialized, "utf8");
1167
+ await writeFile(settingsPath, serialized, SECRET_JSON_FILE_OPTIONS);
1134
1168
  process.stderr.write(
1135
1169
  "\n" + style.dim("Wrote ") + style.cyan(settingsPath) + style.dim(":") + "\n"
1136
1170
  );
@@ -1232,7 +1266,7 @@ async function installGeminiCliNativeHooks(ask) {
1232
1266
  AfterTool: afterArr
1233
1267
  };
1234
1268
  await mkdir(dirname(settingsPath), { recursive: true });
1235
- await writeFile(settingsPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
1269
+ await writeFile(settingsPath, JSON.stringify(existing, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1236
1270
  }
1237
1271
  async function promptGeminiCliIntegrationMode(ask) {
1238
1272
  process.stderr.write("\n" + style.bold("Gemini CLI integration") + "\n");
@@ -1271,6 +1305,52 @@ function getClaudeDesktopConfigPath() {
1271
1305
  );
1272
1306
  }
1273
1307
  }
1308
+ function getCursorMcpJsonPath() {
1309
+ return join(homedir(), ".cursor", "mcp.json");
1310
+ }
1311
+ function getWindsurfMcpConfigPath() {
1312
+ return join(homedir(), ".codeium", "windsurf", "mcp_config.json");
1313
+ }
1314
+ function getClineMcpSettingsPath() {
1315
+ switch (process.platform) {
1316
+ case "win32":
1317
+ return join(
1318
+ process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming"),
1319
+ "Code",
1320
+ "User",
1321
+ "globalStorage",
1322
+ "saoudrizwan.claude-dev",
1323
+ "settings",
1324
+ "cline_mcp_settings.json"
1325
+ );
1326
+ case "linux":
1327
+ return join(
1328
+ homedir(),
1329
+ ".config",
1330
+ "Code",
1331
+ "User",
1332
+ "globalStorage",
1333
+ "saoudrizwan.claude-dev",
1334
+ "settings",
1335
+ "cline_mcp_settings.json"
1336
+ );
1337
+ default:
1338
+ return join(
1339
+ homedir(),
1340
+ "Library",
1341
+ "Application Support",
1342
+ "Code",
1343
+ "User",
1344
+ "globalStorage",
1345
+ "saoudrizwan.claude-dev",
1346
+ "settings",
1347
+ "cline_mcp_settings.json"
1348
+ );
1349
+ }
1350
+ }
1351
+ function getContinueConfigJsonPath() {
1352
+ return join(homedir(), ".continue", "config.json");
1353
+ }
1274
1354
  var INIT_WIZARD_PLATFORM_REGISTRY = [
1275
1355
  { slug: "openclaw", displayName: "OpenClaw", section: "native" },
1276
1356
  { slug: "claude-code", displayName: "Claude Code", section: "native" },
@@ -1460,9 +1540,11 @@ async function arrowSelect(options, ask, fallbackLabel) {
1460
1540
  process.stdin.on("data", onData);
1461
1541
  });
1462
1542
  }
1463
- async function promptAgentName(ask, platform) {
1543
+ async function promptAgentName(ask, platform, defaultNameOverride) {
1464
1544
  const dirPart = normalizeAgentName(basename(process.cwd()));
1465
- const defaultAgentName = dirPart.length > 0 ? normalizeAgentName(`${dirPart}-${platform}`) || platform : normalizeAgentName(platform) || platform;
1545
+ const computedDefault = dirPart.length > 0 ? normalizeAgentName(`${dirPart}-${platform}`) || platform : normalizeAgentName(platform) || platform;
1546
+ const fromOverride = defaultNameOverride !== void 0 && defaultNameOverride.trim().length > 0 ? normalizeAgentName(defaultNameOverride.trim()) : "";
1547
+ const defaultAgentName = fromOverride.length > 0 ? fromOverride : computedDefault;
1466
1548
  let agentName = "";
1467
1549
  while (agentName.length === 0) {
1468
1550
  const input = await ask(
@@ -1553,6 +1635,240 @@ async function createProxyConfig(baseUrl, apiKey, agentName, targetUrl, serverNa
1553
1635
  const data = envelope["data"];
1554
1636
  return typeof data?.["proxy_url"] === "string" ? data["proxy_url"] : "";
1555
1637
  }
1638
+ function writeMcpAddedLine(shortName, filePath) {
1639
+ process.stderr.write(
1640
+ style.green("\u2713") + ' MCP server "' + shortName + '" added to ' + style.cyan(filePath) + "\n"
1641
+ );
1642
+ }
1643
+ async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
1644
+ let root = {};
1645
+ try {
1646
+ const raw = await readFile(filePath, "utf8");
1647
+ let parsed;
1648
+ try {
1649
+ parsed = JSON.parse(raw);
1650
+ } catch {
1651
+ return "parse-error";
1652
+ }
1653
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
1654
+ root = parsed;
1655
+ } else {
1656
+ return "parse-error";
1657
+ }
1658
+ } catch (e) {
1659
+ if (isErrnoException(e) && e.code === "ENOENT") {
1660
+ root = {};
1661
+ } else {
1662
+ throw e;
1663
+ }
1664
+ }
1665
+ const mcpRaw = root["mcpServers"];
1666
+ const mcpServers = typeof mcpRaw === "object" && mcpRaw !== null && !Array.isArray(mcpRaw) ? { ...mcpRaw } : {};
1667
+ mcpServers[shortName] = entry;
1668
+ root["mcpServers"] = mcpServers;
1669
+ await mkdir(dirname(filePath), { recursive: true });
1670
+ await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1671
+ writeMcpAddedLine(shortName, filePath);
1672
+ return "ok";
1673
+ }
1674
+ async function mergeClaudeDesktopHostedMcpRemote(shortName, proxyUrl, apiKey) {
1675
+ const entry = {
1676
+ command: "npx",
1677
+ args: ["-y", "mcp-remote", proxyUrl, "--header", `Authorization: Bearer ${apiKey}`]
1678
+ };
1679
+ return mergeMcpServersObjectStyle(getClaudeDesktopConfigPath(), shortName, entry);
1680
+ }
1681
+ async function mergeContinueHostedMcp(shortName, proxyUrl, apiKey) {
1682
+ const filePath = getContinueConfigJsonPath();
1683
+ let root = {};
1684
+ try {
1685
+ const raw = await readFile(filePath, "utf8");
1686
+ let parsed;
1687
+ try {
1688
+ parsed = JSON.parse(raw);
1689
+ } catch {
1690
+ return "parse-error";
1691
+ }
1692
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
1693
+ root = parsed;
1694
+ } else {
1695
+ return "parse-error";
1696
+ }
1697
+ } catch (e) {
1698
+ if (isErrnoException(e) && e.code === "ENOENT") {
1699
+ root = {};
1700
+ } else {
1701
+ throw e;
1702
+ }
1703
+ }
1704
+ const rawServers = root["mcpServers"];
1705
+ if (rawServers !== void 0) {
1706
+ if (Array.isArray(rawServers)) ; else if (typeof rawServers === "object" && rawServers !== null) {
1707
+ return "parse-error";
1708
+ } else {
1709
+ return "parse-error";
1710
+ }
1711
+ }
1712
+ const servers = Array.isArray(rawServers) ? rawServers.map((s) => ({ ...s })) : [];
1713
+ const entry = {
1714
+ name: shortName,
1715
+ type: "streamable-http",
1716
+ url: proxyUrl,
1717
+ headers: {
1718
+ Authorization: `Bearer ${apiKey}`
1719
+ }
1720
+ };
1721
+ const idx = servers.findIndex((s) => s["name"] === shortName);
1722
+ if (idx >= 0) {
1723
+ servers[idx] = entry;
1724
+ } else {
1725
+ servers.push(entry);
1726
+ }
1727
+ root["mcpServers"] = servers;
1728
+ await mkdir(dirname(filePath), { recursive: true });
1729
+ await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1730
+ writeMcpAddedLine(shortName, filePath);
1731
+ return "ok";
1732
+ }
1733
+ async function mergeKiloCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKey) {
1734
+ const filePath = join(workspacePath, ".kilocode", "mcp.json");
1735
+ return mergeMcpServersObjectStyle(filePath, shortName, {
1736
+ url: proxyUrl,
1737
+ headers: {
1738
+ Authorization: `Bearer ${apiKey}`
1739
+ }
1740
+ });
1741
+ }
1742
+ function printHostedProxyJsonParseWarning(filePath) {
1743
+ process.stderr.write(
1744
+ style.yellow("\u26A0") + " Could not parse JSON at " + style.cyan(filePath) + style.dim(" - showing paste snippet instead.") + "\n"
1745
+ );
1746
+ }
1747
+ function printHostedProxyPostWriteHints(platform, shortName) {
1748
+ if (platform === "cursor") {
1749
+ process.stderr.write(
1750
+ style.dim("Restart Cursor and check Settings > Tools & MCPs for a green status indicator. ") + style.dim(`Ask Cursor to use your MCP server by its short name (e.g. ${shortName}).`) + "\n"
1751
+ );
1752
+ }
1753
+ if (platform === "claude-desktop") {
1754
+ process.stderr.write(style.dim("Restart Claude Desktop to load the MCP server.") + "\n");
1755
+ }
1756
+ if (platform === "cline") {
1757
+ process.stderr.write(
1758
+ style.dim(
1759
+ "Restart Cline or reload the VS Code window. Cline will discover the Shield tools automatically."
1760
+ ) + "\n"
1761
+ );
1762
+ }
1763
+ if (platform === "windsurf") {
1764
+ process.stderr.write(style.dim("Restart Windsurf (Cmd/Ctrl+Q, then reopen).") + "\n");
1765
+ process.stderr.write(
1766
+ style.dim(
1767
+ "Open the Cascade panel and verify the server appears with a green status indicator."
1768
+ ) + "\n"
1769
+ );
1770
+ }
1771
+ if (platform === "github-copilot" || platform === "continue-dev") {
1772
+ process.stderr.write(
1773
+ style.dim("Reload the editor window if the MCP server does not appear immediately.") + "\n"
1774
+ );
1775
+ }
1776
+ if (platform === "goose") {
1777
+ process.stderr.write(style.dim("Start a new Goose session after updating config.") + "\n");
1778
+ }
1779
+ if (platform === "kilo-code") {
1780
+ process.stderr.write(
1781
+ style.dim("Restart Kilo Code or reload the window so it picks up .kilocode/mcp.json.") + "\n"
1782
+ );
1783
+ }
1784
+ }
1785
+ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey, workspacePath) {
1786
+ const authHeader = `Bearer ${apiKey}`;
1787
+ if (platform === "gemini-cli") {
1788
+ await mergeGeminiHostedMcpServersIntoSettings(shortName, proxyUrl, apiKey);
1789
+ process.stderr.write(
1790
+ style.dim(
1791
+ "For project-specific config, copy the mcpServers entry into .gemini/settings.json in your project root. Restart Gemini CLI if it is already running."
1792
+ ) + "\n"
1793
+ );
1794
+ return;
1795
+ }
1796
+ if (platform === "github-copilot") {
1797
+ process.stderr.write(
1798
+ "\n" + style.dim(
1799
+ "GitHub Copilot uses VS Code settings - paste the snippet below into your VS Code Settings (JSON)."
1800
+ ) + "\n"
1801
+ );
1802
+ printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1803
+ return;
1804
+ }
1805
+ if (platform === "goose") {
1806
+ printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1807
+ return;
1808
+ }
1809
+ try {
1810
+ let result = "parse-error";
1811
+ if (platform === "cursor") {
1812
+ result = await mergeMcpServersObjectStyle(getCursorMcpJsonPath(), shortName, {
1813
+ url: proxyUrl,
1814
+ headers: { Authorization: authHeader }
1815
+ });
1816
+ if (result === "parse-error") {
1817
+ printHostedProxyJsonParseWarning(getCursorMcpJsonPath());
1818
+ }
1819
+ } else if (platform === "claude-desktop") {
1820
+ result = await mergeClaudeDesktopHostedMcpRemote(shortName, proxyUrl, apiKey);
1821
+ if (result === "parse-error") {
1822
+ printHostedProxyJsonParseWarning(getClaudeDesktopConfigPath());
1823
+ }
1824
+ } else if (platform === "windsurf") {
1825
+ result = await mergeMcpServersObjectStyle(getWindsurfMcpConfigPath(), shortName, {
1826
+ serverUrl: proxyUrl,
1827
+ headers: { Authorization: authHeader }
1828
+ });
1829
+ if (result === "parse-error") {
1830
+ printHostedProxyJsonParseWarning(getWindsurfMcpConfigPath());
1831
+ }
1832
+ } else if (platform === "cline") {
1833
+ result = await mergeMcpServersObjectStyle(getClineMcpSettingsPath(), shortName, {
1834
+ url: proxyUrl,
1835
+ headers: { Authorization: authHeader }
1836
+ });
1837
+ if (result === "parse-error") {
1838
+ printHostedProxyJsonParseWarning(getClineMcpSettingsPath());
1839
+ }
1840
+ } else if (platform === "kilo-code") {
1841
+ result = await mergeKiloCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKey);
1842
+ if (result === "parse-error") {
1843
+ printHostedProxyJsonParseWarning(join(workspacePath, ".kilocode", "mcp.json"));
1844
+ }
1845
+ } else if (platform === "continue-dev") {
1846
+ result = await mergeContinueHostedMcp(shortName, proxyUrl, apiKey);
1847
+ if (result === "parse-error") {
1848
+ printHostedProxyJsonParseWarning(getContinueConfigJsonPath());
1849
+ }
1850
+ } else {
1851
+ result = await mergeMcpServersObjectStyle(getCursorMcpJsonPath(), shortName, {
1852
+ url: proxyUrl,
1853
+ headers: { Authorization: authHeader }
1854
+ });
1855
+ if (result === "parse-error") {
1856
+ printHostedProxyJsonParseWarning(getCursorMcpJsonPath());
1857
+ }
1858
+ }
1859
+ if (result === "ok") {
1860
+ printHostedProxyPostWriteHints(platform, shortName);
1861
+ return;
1862
+ }
1863
+ } catch (e) {
1864
+ const detail = e instanceof Error ? e.message : String(e);
1865
+ process.stderr.write(
1866
+ style.yellow("\u26A0") + ` Could not write MCP config automatically (${detail}).
1867
+ `
1868
+ );
1869
+ }
1870
+ printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1871
+ }
1556
1872
  function gooseHostedProxyYaml(shortName, proxyUrl, bearerHeader) {
1557
1873
  return `extensions:
1558
1874
  ${shortName}:
@@ -1614,6 +1930,36 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1614
1930
  null,
1615
1931
  2
1616
1932
  );
1933
+ } else if (platform === "claude-desktop") {
1934
+ snippetText = JSON.stringify(
1935
+ {
1936
+ mcpServers: {
1937
+ [shortName]: {
1938
+ command: "npx",
1939
+ args: ["-y", "mcp-remote", routingToken, "--header", `Authorization: ${authHeader}`]
1940
+ }
1941
+ }
1942
+ },
1943
+ null,
1944
+ 2
1945
+ );
1946
+ } else if (platform === "continue-dev") {
1947
+ snippetText = JSON.stringify(
1948
+ {
1949
+ mcpServers: [
1950
+ {
1951
+ name: shortName,
1952
+ type: "streamable-http",
1953
+ url: routingToken,
1954
+ headers: {
1955
+ Authorization: authHeader
1956
+ }
1957
+ }
1958
+ ]
1959
+ },
1960
+ null,
1961
+ 2
1962
+ );
1617
1963
  } else {
1618
1964
  const urlKey = platform === "windsurf" ? "serverUrl" : "url";
1619
1965
  snippetText = JSON.stringify(
@@ -1638,52 +1984,33 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1638
1984
  } else if (platform === "claude-desktop") {
1639
1985
  process.stderr.write("\n" + style.dim(`Add this to ${getClaudeDesktopConfigPath()}:`) + "\n\n");
1640
1986
  } else if (platform === "windsurf") {
1641
- process.stderr.write(
1642
- "\n" + style.dim("Add this to ~/.codeium/windsurf/mcp_config.json:") + "\n\n"
1643
- );
1987
+ process.stderr.write("\n" + style.dim(`Add this to ${getWindsurfMcpConfigPath()}:`) + "\n\n");
1644
1988
  } else if (platform === "cline") {
1645
- process.stderr.write("\n" + style.dim("Add this to your Cline MCP settings file:") + "\n");
1646
- process.stderr.write(
1647
- style.dim(
1648
- " macOS: ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json"
1649
- ) + "\n"
1650
- );
1651
- process.stderr.write(
1652
- style.dim(
1653
- " Windows: %APPDATA%\\Code\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json"
1654
- ) + "\n"
1655
- );
1656
- process.stderr.write(
1657
- style.dim(
1658
- " Linux: ~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json"
1659
- ) + "\n\n"
1660
- );
1989
+ process.stderr.write("\n" + style.dim(`Add this to ${getClineMcpSettingsPath()}:`) + "\n\n");
1661
1990
  } else if (platform === "gemini-cli") {
1662
1991
  process.stderr.write(
1663
1992
  "\n" + style.dim(
1664
- "Add this to ~/.gemini/settings.json (create the file if it does not exist). For project-specific config, use .gemini/settings.json in your project root. Restart Gemini CLI after saving. Run /mcp to verify the server is connected."
1993
+ `Merge the snippet below into ${getGeminiCliSettingsPath()} (keep existing hooks and other keys). Restart Gemini CLI if it is already running.`
1665
1994
  ) + "\n\n"
1666
1995
  );
1667
1996
  } else if (platform === "kilo-code") {
1668
1997
  process.stderr.write(
1669
- "\n" + style.dim("Add this to .kilocode/mcp.json in your project root.") + "\n\n"
1998
+ "\n" + style.dim(`Add this to ${join(resolve(process.cwd()), ".kilocode", "mcp.json")}:`) + "\n\n"
1670
1999
  );
1671
2000
  } else if (platform === "github-copilot") {
1672
2001
  process.stderr.write(
1673
2002
  "\n" + style.dim(
1674
- "Open VS Code Settings (JSON) and merge this under the mcp key. If you do not have an mcp section yet, add one. Copilot picks up MCP servers when you use Agent mode."
2003
+ "Merge this snippet under the mcp key in your VS Code Settings (JSON). If you do not have an mcp section yet, add one. Copilot picks up MCP servers when you use Agent mode."
1675
2004
  ) + "\n\n"
1676
2005
  );
1677
2006
  } else if (platform === "continue-dev") {
1678
- process.stderr.write(
1679
- "\n" + style.dim("Save this as .continue/mcpServers/shield.json in your workspace root.") + "\n\n"
1680
- );
2007
+ process.stderr.write("\n" + style.dim(`Add this to ${getContinueConfigJsonPath()}:`) + "\n\n");
1681
2008
  } else if (platform === "goose") {
1682
2009
  process.stderr.write(
1683
2010
  "\n" + style.dim("Add this to ~/.config/goose/config.yaml under the extensions key.") + "\n\n"
1684
2011
  );
1685
2012
  } else {
1686
- process.stderr.write("\n" + style.dim("Add this to ~/.cursor/mcp.json:") + "\n\n");
2013
+ process.stderr.write("\n" + style.dim(`Add this to ${getCursorMcpJsonPath()}:`) + "\n\n");
1687
2014
  }
1688
2015
  process.stderr.write(style.cyan(snippetText) + "\n\n");
1689
2016
  if (!usesInlineKey) {
@@ -1732,20 +2059,45 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1732
2059
  process.stderr.write(style.dim("Start a new Goose session after updating config.") + "\n");
1733
2060
  }
1734
2061
  }
2062
+ function agentDisplayNameDedupeKey(name) {
2063
+ return name.trim().toLowerCase();
2064
+ }
2065
+ function normalizeAgentEntryForMerge(a) {
2066
+ const name = a.name.trim();
2067
+ const ws = typeof a.workspacePath === "string" && a.workspacePath.length > 0 ? a.workspacePath : void 0;
2068
+ return ws !== void 0 ? { name, platform: a.platform, workspacePath: ws } : { name, platform: a.platform };
2069
+ }
2070
+ function mergeAgentEntryDupPair(first, second) {
2071
+ const name = first.name.trim();
2072
+ const platform = first.platform;
2073
+ const ws = typeof first.workspacePath === "string" && first.workspacePath.length > 0 ? first.workspacePath : typeof second.workspacePath === "string" && second.workspacePath.length > 0 ? second.workspacePath : void 0;
2074
+ return ws !== void 0 ? { name, platform, workspacePath: ws } : { name, platform };
2075
+ }
2076
+ function mergeAgentsForUniqueNames(agents) {
2077
+ const byKey = /* @__PURE__ */ new Map();
2078
+ for (const raw of agents) {
2079
+ const key = agentDisplayNameDedupeKey(raw.name);
2080
+ const candidate = normalizeAgentEntryForMerge(raw);
2081
+ const prev = byKey.get(key);
2082
+ byKey.set(key, prev === void 0 ? candidate : mergeAgentEntryDupPair(prev, candidate));
2083
+ }
2084
+ return [...byKey.values()];
2085
+ }
1735
2086
  function mergeAgentsForPlatform(localAgents, remoteAgents, selectedPlatform) {
1736
- const localMatches = localAgents.filter((a) => a.platform === selectedPlatform);
1737
- const seen = new Set(localMatches.map((a) => a.name));
1738
- const out = localMatches.map((a) => ({ ...a }));
2087
+ const merged = [];
2088
+ for (const a of localAgents) {
2089
+ if (a.platform !== selectedPlatform) continue;
2090
+ merged.push(a);
2091
+ }
1739
2092
  for (const r of remoteAgents) {
1740
2093
  if (r.platform !== selectedPlatform) continue;
1741
- if (seen.has(r.name)) continue;
1742
- seen.add(r.name);
1743
- out.push({ name: r.name, platform: selectedPlatform });
2094
+ merged.push({ name: r.name, platform: selectedPlatform });
1744
2095
  }
1745
- return out;
2096
+ return mergeAgentsForUniqueNames(merged);
1746
2097
  }
1747
2098
  var DEFAULT_SHIELD_API_BASE_URL = "https://api.multicorn.ai";
1748
- async function runInit(explicitBaseUrl) {
2099
+ async function runInit(explicitBaseUrl, options) {
2100
+ const verbose = options?.verbose === true;
1749
2101
  if (!process.stdin.isTTY) {
1750
2102
  process.stderr.write(
1751
2103
  style.red("Error: interactive terminal required. Cannot run init with piped input.") + "\n"
@@ -1860,7 +2212,7 @@ async function runInit(explicitBaseUrl) {
1860
2212
  `
1861
2213
  );
1862
2214
  process.stderr.write(
1863
- "\n" + style.bold("Try it:") + " " + style.cyan(
2215
+ "\n" + style.bold("Try it:") + " make a request in your coding agent - Shield will intercept the first tool call and ask for your consent.\n" + style.dim("Example wrap command: ") + style.cyan(
1864
2216
  "npx multicorn-shield --wrap npx @modelcontextprotocol/server-filesystem /tmp"
1865
2217
  ) + "\n"
1866
2218
  );
@@ -1893,11 +2245,13 @@ async function runInit(explicitBaseUrl) {
1893
2245
  (r) => r.platform === selectedPlatform
1894
2246
  ).length;
1895
2247
  const savedSummary = currentAgents.length === 0 ? "none on disk" : currentAgents.map((a) => `${a.name} (${a.platform})`).join(", ");
1896
- process.stderr.write(
1897
- style.dim(
1898
- `[shield init] Menu option ${String(selection)} -> platform slug "${selectedPlatform}". ${String(agentsForPlatform.length)} agent(s) for this platform (local file: ${String(localForPlatformCount)}, account API: ${String(accountForPlatformCount)}). On-disk entries: ${savedSummary}.`
1899
- ) + "\n"
1900
- );
2248
+ if (verbose) {
2249
+ process.stderr.write(
2250
+ style.dim(
2251
+ `[shield init] Menu option ${String(selection)} -> platform slug "${selectedPlatform}". ${String(agentsForPlatform.length)} agent(s) for this platform (local file: ${String(localForPlatformCount)}, account API: ${String(accountForPlatformCount)}). On-disk entries: ${savedSummary}.`
2252
+ ) + "\n"
2253
+ );
2254
+ }
1901
2255
  if (agentsForPlatform.length > 0) {
1902
2256
  process.stderr.write(
1903
2257
  `
@@ -1937,6 +2291,14 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
1937
2291
  }
1938
2292
  }
1939
2293
  }
2294
+ if (selectedPlatform === "cursor" || selectedPlatform === "github-copilot") {
2295
+ const where = selectedPlatform === "cursor" ? "Cursor" : "GitHub Copilot";
2296
+ process.stderr.write(
2297
+ "\n" + style.dim(
2298
+ `Using Claude models (Sonnet, Opus, Haiku) in ${where}? The Claude Code native plugin is recommended - it governs all tool calls, not just MCP traffic. Run init again and select Claude Code.`
2299
+ ) + "\n\n"
2300
+ );
2301
+ }
1940
2302
  const prereqEntry = INIT_WIZARD_PLATFORM_REGISTRY.find((e) => e.slug === selectedPlatform);
1941
2303
  if (prereqEntry?.prereqUrl !== void 0) {
1942
2304
  const proceed = await promptHostedProxyInstallPrereq(
@@ -1952,7 +2314,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
1952
2314
  continue;
1953
2315
  }
1954
2316
  }
1955
- const agentName = await promptAgentName(ask, selectedPlatform);
2317
+ const agentName = await promptAgentName(ask, selectedPlatform, removeAgentNameBeforeSave);
1956
2318
  let setupSucceeded = false;
1957
2319
  if (selectedPlatform === "openclaw") {
1958
2320
  let detection;
@@ -2078,7 +2440,9 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2078
2440
  ) + style.cyan("~/.multicorn/windsurf-hooks") + style.dim(" if that is a concern.") + "\n\n"
2079
2441
  );
2080
2442
  process.stderr.write(
2081
- style.dim("Restart Windsurf (quit fully, then reopen) so hooks load.") + "\n"
2443
+ style.dim(
2444
+ "Try it: make a request in Windsurf - Shield will intercept the first tool call and ask for your consent."
2445
+ ) + "\n"
2082
2446
  );
2083
2447
  configuredAgents.push({
2084
2448
  selection,
@@ -2135,7 +2499,13 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2135
2499
  if (created && proxyUrl.length > 0) {
2136
2500
  process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
2137
2501
  process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
2138
- printPlatformSnippet(selectedPlatform, proxyUrl, shortName, apiKey);
2502
+ await applyHostedProxyMcpConfig(
2503
+ selectedPlatform,
2504
+ proxyUrl,
2505
+ shortName,
2506
+ apiKey,
2507
+ initWorkspacePath
2508
+ );
2139
2509
  configuredAgents.push({
2140
2510
  selection,
2141
2511
  platform: selectedPlatform,
@@ -2220,7 +2590,13 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2220
2590
  if (created && proxyUrl.length > 0) {
2221
2591
  process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
2222
2592
  process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
2223
- printPlatformSnippet(selectedPlatform, proxyUrl, shortName, apiKey);
2593
+ await applyHostedProxyMcpConfig(
2594
+ selectedPlatform,
2595
+ proxyUrl,
2596
+ shortName,
2597
+ apiKey,
2598
+ initWorkspacePath
2599
+ );
2224
2600
  configuredAgents.push({
2225
2601
  selection,
2226
2602
  platform: selectedPlatform,
@@ -2299,7 +2675,13 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2299
2675
  if (created && proxyUrl.length > 0) {
2300
2676
  process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
2301
2677
  process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
2302
- printPlatformSnippet(selectedPlatform, proxyUrl, shortName, apiKey);
2678
+ await applyHostedProxyMcpConfig(
2679
+ selectedPlatform,
2680
+ proxyUrl,
2681
+ shortName,
2682
+ apiKey,
2683
+ initWorkspacePath
2684
+ );
2303
2685
  configuredAgents.push({
2304
2686
  selection,
2305
2687
  platform: selectedPlatform,
@@ -2341,7 +2723,13 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2341
2723
  if (created && proxyUrl.length > 0) {
2342
2724
  process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
2343
2725
  process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
2344
- printPlatformSnippet(selectedPlatform, proxyUrl, shortName, apiKey);
2726
+ await applyHostedProxyMcpConfig(
2727
+ selectedPlatform,
2728
+ proxyUrl,
2729
+ shortName,
2730
+ apiKey,
2731
+ initWorkspacePath
2732
+ );
2345
2733
  configuredAgents.push({
2346
2734
  selection,
2347
2735
  platform: selectedPlatform,
@@ -2355,7 +2743,10 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2355
2743
  }
2356
2744
  if (setupSucceeded) {
2357
2745
  if (removeAgentNameBeforeSave !== void 0) {
2358
- currentAgents = currentAgents.filter((a) => a.name !== removeAgentNameBeforeSave);
2746
+ const removeKey = agentDisplayNameDedupeKey(removeAgentNameBeforeSave);
2747
+ currentAgents = currentAgents.filter(
2748
+ (a) => agentDisplayNameDedupeKey(a.name) !== removeKey
2749
+ );
2359
2750
  }
2360
2751
  currentAgents.push({
2361
2752
  name: agentName,
@@ -2407,42 +2798,42 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2407
2798
  const blocks = [];
2408
2799
  if (configuredPlatforms.has("openclaw")) {
2409
2800
  blocks.push(
2410
- "\n" + style.bold("OpenClaw") + "\n \u2192 Restart your gateway: " + style.cyan("openclaw gateway restart") + "\n \u2192 Start a session: " + style.cyan("openclaw tui") + "\n"
2801
+ "\n" + style.bold("OpenClaw") + "\n \u2192 Restart your gateway: " + style.cyan("openclaw gateway restart") + "\n \u2192 Start a session: " + style.cyan("openclaw tui") + "\n \u2192 Try it: make a request in OpenClaw - Shield will intercept the first tool call and ask for your consent\n"
2411
2802
  );
2412
2803
  }
2413
2804
  if (configuredPlatforms.has("claude-code")) {
2414
2805
  blocks.push(
2415
- "\n" + style.bold("Claude Code") + "\n \u2192 Start Claude Code: " + style.cyan("claude") + "\n \u2192 Shield will intercept tool calls automatically.\n"
2806
+ "\n" + style.bold("Claude Code") + "\n \u2192 Start coding: run " + style.cyan("claude") + " in the terminal, or use Cursor with a Claude model - Shield hooks apply to both\n \u2192 Try it: make a request in Claude Code - Shield will intercept the first tool call and ask for your consent\n"
2416
2807
  );
2417
2808
  }
2418
2809
  if (configuredPlatforms.has("claude-desktop")) {
2419
2810
  blocks.push(
2420
- "\n" + style.bold("Claude Desktop") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
2811
+ "\n" + style.bold("Claude Desktop") + "\n \u2192 Restart Claude Desktop to pick up config changes\n \u2192 Try it: make a request in Claude Desktop - Shield will intercept the first tool call and ask for your consent\n"
2421
2812
  );
2422
2813
  }
2423
2814
  if (configuredPlatforms.has("cursor")) {
2424
2815
  blocks.push(
2425
- "\n" + style.bold("Cursor") + "\n \u2192 If needed, download Cursor from " + style.cyan("https://cursor.com/downloads") + "\n \u2192 Restart Cursor so it loads the MCP server\n \u2192 Ask the agent to use your Shield MCP tools by short name\n"
2816
+ "\n" + style.bold("Cursor") + "\n \u2192 If needed, download Cursor from " + style.cyan("https://www.cursor.com/downloads") + "\n \u2192 Restart Cursor so it loads the MCP server\n \u2192 Try it: make a request in Cursor - Shield will intercept the first tool call and ask for your consent\n"
2426
2817
  );
2427
2818
  }
2428
2819
  if (configuredPlatforms.has("kilo-code")) {
2429
2820
  blocks.push(
2430
- "\n" + style.bold("Kilo Code") + "\n \u2192 Restart the editor or reload the window if the MCP server does not appear\n \u2192 Run your next task in Kilo Code so it picks up Shield\n"
2821
+ "\n" + style.bold("Kilo Code") + "\n \u2192 Restart the editor or reload the window if the MCP server does not appear\n \u2192 Try it: make a request in Kilo Code - Shield will intercept the first tool call and ask for your consent\n"
2431
2822
  );
2432
2823
  }
2433
2824
  if (configuredPlatforms.has("github-copilot")) {
2434
2825
  blocks.push(
2435
- "\n" + style.bold("GitHub Copilot") + "\n \u2192 Reload the editor window if the MCP server does not appear\n \u2192 Use Copilot Agent mode and verify the MCP server connects\n"
2826
+ "\n" + style.bold("GitHub Copilot") + "\n \u2192 Reload the editor window if the MCP server does not appear\n \u2192 Open Copilot Agent mode and confirm the MCP server connects\n \u2192 Try it: make a request in GitHub Copilot - Shield will intercept the first tool call and ask for your consent\n"
2436
2827
  );
2437
2828
  }
2438
2829
  if (configuredPlatforms.has("continue-dev")) {
2439
2830
  blocks.push(
2440
- "\n" + style.bold("Continue") + "\n \u2192 If needed, install Continue from " + style.cyan("https://docs.continue.dev/ide-extensions/install") + "\n \u2192 Reload VS Code and open Continue agent mode\n"
2831
+ "\n" + style.bold("Continue") + "\n \u2192 If needed, install Continue from " + style.cyan("https://docs.continue.dev/ide-extensions/install") + "\n \u2192 Reload VS Code and open Continue agent mode\n \u2192 Try it: make a request in Continue - Shield will intercept the first tool call and ask for your consent\n"
2441
2832
  );
2442
2833
  }
2443
2834
  if (configuredPlatforms.has("goose")) {
2444
2835
  blocks.push(
2445
- "\n" + style.bold("Goose") + "\n \u2192 Start a new Goose session after updating config\n"
2836
+ "\n" + style.bold("Goose") + "\n \u2192 Start a new Goose session after updating config\n \u2192 Try it: make a request in Goose - Shield will intercept the first tool call and ask for your consent\n"
2446
2837
  );
2447
2838
  }
2448
2839
  const windsurfNativeConfigured = configuredAgents.some(
@@ -2453,12 +2844,12 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2453
2844
  );
2454
2845
  if (windsurfNativeConfigured) {
2455
2846
  blocks.push(
2456
- "\n" + style.bold("Windsurf (native)") + "\n \u2192 Restart Windsurf (quit fully, then reopen)\n"
2847
+ "\n" + style.bold("Windsurf (native)") + "\n \u2192 Open Windsurf (or restart if it is already running)\n \u2192 Try it: make a request in Windsurf - Shield will intercept the first tool call and ask for your consent\n"
2457
2848
  );
2458
2849
  }
2459
2850
  if (windsurfHostedConfigured) {
2460
2851
  blocks.push(
2461
- "\n" + style.bold("Windsurf (hosted)") + "\n \u2192 If needed, install from " + style.cyan("https://windsurf.com/download") + "\n \u2192 Restart Windsurf so it loads the MCP server\n"
2852
+ "\n" + style.bold("Windsurf (hosted)") + "\n \u2192 If needed, install from " + style.cyan("https://windsurf.com/download") + "\n \u2192 Restart Windsurf so it loads the MCP server\n \u2192 In Windsurf, open the three-dot menu (top-right of Cascade panel), find your Shield server at the bottom of the list, and toggle it on\n \u2192 Try it: make a request in Windsurf - Shield will intercept the first tool call and ask for your consent\n"
2462
2853
  );
2463
2854
  }
2464
2855
  const clineNativeConfigured = configuredAgents.some(
@@ -2469,12 +2860,12 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2469
2860
  );
2470
2861
  if (clineNativeConfigured) {
2471
2862
  blocks.push(
2472
- "\n" + style.bold("Cline (native)") + "\n \u2192 Enable Hooks in Cline settings (Advanced), then reload the VS Code window\n \u2192 Trigger a tool call to verify Shield is intercepting\n"
2863
+ "\n" + style.bold("Cline (native)") + "\n \u2192 In Cline, click the settings icon \u2192 Feature Settings \u2192 scroll down to Advanced \u2192 enable Hooks, then reload the VS Code window\n \u2192 Try it: make a request in Cline - Shield will intercept the first tool call and ask for your consent\n"
2473
2864
  );
2474
2865
  }
2475
2866
  if (clineHostedConfigured) {
2476
2867
  blocks.push(
2477
- "\n" + style.bold("Cline (hosted)") + "\n \u2192 Restart Cline or reload the VS Code window\n"
2868
+ "\n" + style.bold("Cline (hosted)") + "\n \u2192 Restart Cline or reload the VS Code window\n \u2192 In Cline, open server settings using the plug icon, open the Configure tab, and confirm your Shield server is listed and toggled on\n \u2192 Try it: make a request in Cline - Shield will intercept the first tool call and ask for your consent\n"
2478
2869
  );
2479
2870
  }
2480
2871
  const geminiCliNativeConfigured = configuredAgents.some(
@@ -2485,12 +2876,17 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2485
2876
  );
2486
2877
  if (geminiCliNativeConfigured) {
2487
2878
  blocks.push(
2488
- "\n" + style.bold("Gemini CLI (native)") + "\n \u2192 Restart Gemini CLI to activate Shield governance\n"
2879
+ "\n" + style.bold("Gemini CLI (native)") + "\n \u2192 Start Gemini CLI: run " + style.cyan("gemini") + " in your terminal (exit any existing session first)\n \u2192 Try it: make a request in Gemini CLI - Shield will intercept the first tool call and ask for your consent\n"
2489
2880
  );
2490
2881
  }
2491
2882
  if (geminiCliHostedConfigured) {
2492
2883
  blocks.push(
2493
- "\n" + style.bold("Gemini CLI (hosted)") + "\n \u2192 Restart Gemini CLI, then run " + style.cyan("/mcp") + " to verify the server\n"
2884
+ "\n" + style.bold("Gemini CLI (hosted)") + "\n \u2192 Try it: make a request in Gemini CLI - Shield will intercept the first tool call and ask for your consent\n"
2885
+ );
2886
+ }
2887
+ if (configuredPlatforms.has("other-mcp")) {
2888
+ blocks.push(
2889
+ "\n" + style.bold("Local MCP / Other") + "\n \u2192 Run your configured wrap command (for example " + style.cyan("npx multicorn-shield --wrap ...") + ")\n \u2192 Try it: make a request in your coding agent - Shield will intercept the first tool call and ask for your consent\n"
2494
2890
  );
2495
2891
  }
2496
2892
  if (blocks.length > 0) {
@@ -3381,7 +3777,10 @@ async function restoreClaudeDesktopMcpFromBackup() {
3381
3777
  }
3382
3778
  root["mcpServers"] = backup.mcpServers;
3383
3779
  await mkdir(dirname(configPath), { recursive: true });
3384
- await writeFile(configPath, JSON.stringify(root, null, 2) + "\n", { encoding: "utf8" });
3780
+ await writeFile(configPath, JSON.stringify(root, null, 2) + "\n", {
3781
+ encoding: "utf8",
3782
+ mode: 384
3783
+ });
3385
3784
  }
3386
3785
 
3387
3786
  // bin/multicorn-shield.ts
@@ -3396,6 +3795,7 @@ function parseArgs(argv) {
3396
3795
  let agentName = "";
3397
3796
  let deleteAgentName = "";
3398
3797
  let apiKey = void 0;
3798
+ let verbose = false;
3399
3799
  for (let i = 0; i < args.length; i++) {
3400
3800
  const arg = args[i];
3401
3801
  if (arg === "init") {
@@ -3495,6 +3895,8 @@ function parseArgs(argv) {
3495
3895
  apiKey = next;
3496
3896
  i++;
3497
3897
  }
3898
+ } else if (arg === "--verbose" || arg === "--debug") {
3899
+ verbose = true;
3498
3900
  }
3499
3901
  }
3500
3902
  return {
@@ -3506,7 +3908,8 @@ function parseArgs(argv) {
3506
3908
  dashboardUrl,
3507
3909
  agentName,
3508
3910
  deleteAgentName,
3509
- apiKey
3911
+ apiKey,
3912
+ verbose
3510
3913
  };
3511
3914
  }
3512
3915
  function printHelp() {
@@ -3532,6 +3935,7 @@ function printHelp() {
3532
3935
  " Shield's permission layer.",
3533
3936
  "",
3534
3937
  "Options:",
3938
+ " --verbose, --debug Print extra diagnostics during init (menu selection, agent counts)",
3535
3939
  " --api-key <key> Multicorn API key (overrides MULTICORN_API_KEY env var and config file)",
3536
3940
  " --log-level <level> Log level: debug | info | warn | error (default: info)",
3537
3941
  " --base-url <url> Multicorn API base URL (default: https://api.multicorn.ai)",
@@ -3562,7 +3966,7 @@ async function runCli() {
3562
3966
  process.exit(0);
3563
3967
  }
3564
3968
  if (cli.subcommand === "init") {
3565
- await runInit(cli.baseUrl);
3969
+ await runInit(cli.baseUrl, { verbose: cli.verbose });
3566
3970
  return;
3567
3971
  }
3568
3972
  if (cli.subcommand === "agents") {