multicorn-shield 1.6.0 → 1.8.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.
@@ -8,6 +8,7 @@ import { readFileSync, existsSync, statSync } from 'fs';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { createRequire } from 'module';
10
10
  import { createInterface } from 'readline';
11
+ import { parse, stringify } from 'yaml';
11
12
  import 'stream';
12
13
 
13
14
  var __defProp = Object.defineProperty;
@@ -924,6 +925,22 @@ require(${JSON.stringify(destPost)});
924
925
  await writeFile(preWrapper, preContent, { encoding: "utf8", mode: 493 });
925
926
  await writeFile(postWrapper, postContent, { encoding: "utf8", mode: 493 });
926
927
  }
928
+ function getOpenCodeGlobalPluginsDir() {
929
+ return join(homedir(), ".config", "opencode", "plugins");
930
+ }
931
+ async function installOpenCodeNativePlugin() {
932
+ const root = multicornShieldPackageRoot();
933
+ const src = join(root, "plugins", "opencode", "multicorn-shield.ts");
934
+ if (!existsSync(src)) {
935
+ throw new Error(
936
+ `Could not find Shield OpenCode plugin at ${src}. If you use npm, install the latest multicorn-shield package.`
937
+ );
938
+ }
939
+ const destDir = getOpenCodeGlobalPluginsDir();
940
+ await mkdir(destDir, { recursive: true });
941
+ const dest = join(destDir, "multicorn-shield.ts");
942
+ await copyFile(src, dest);
943
+ }
927
944
  async function promptClineIntegrationMode(ask) {
928
945
  process.stderr.write("\n" + style.bold("Cline integration") + "\n");
929
946
  process.stderr.write(
@@ -1283,6 +1300,23 @@ async function promptGeminiCliIntegrationMode(ask) {
1283
1300
  }
1284
1301
  return choice === 1 ? "native" : "hosted";
1285
1302
  }
1303
+ async function promptOpencodeIntegrationMode(ask) {
1304
+ process.stderr.write("\n" + style.bold("OpenCode integration") + "\n");
1305
+ process.stderr.write(
1306
+ " " + style.violet("1") + ". Native plugin (recommended) - Shield checks primary-agent tool execution via OpenCode Hooks\n"
1307
+ );
1308
+ process.stderr.write(
1309
+ " " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via opencode.json (full subagent coverage when tools use MCP through Shield)\n"
1310
+ );
1311
+ let choice = 0;
1312
+ while (choice === 0) {
1313
+ const input = await ask("Choose integration (1-2): ");
1314
+ const num = parseInt(input.trim(), 10);
1315
+ if (num === 1) choice = 1;
1316
+ if (num === 2) choice = 2;
1317
+ }
1318
+ return choice === 1 ? "native" : "hosted";
1319
+ }
1286
1320
  function getClaudeDesktopConfigPath() {
1287
1321
  switch (process.platform) {
1288
1322
  case "win32":
@@ -1346,9 +1380,6 @@ function getClineMcpSettingsPath() {
1346
1380
  );
1347
1381
  }
1348
1382
  }
1349
- function getContinueConfigJsonPath() {
1350
- return join(homedir(), ".continue", "config.json");
1351
- }
1352
1383
  function platformMenuLabelForSelection(sel) {
1353
1384
  const slug = PLATFORM_BY_SELECTION[sel];
1354
1385
  if (slug === void 0) return "Unknown";
@@ -1650,13 +1681,56 @@ function writeMcpAddedLine(shortName, filePath) {
1650
1681
  style.green("\u2713") + ' MCP server "' + shortName + '" added to ' + style.cyan(filePath) + "\n"
1651
1682
  );
1652
1683
  }
1653
- async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
1684
+ function sanitiseYamlValue(value) {
1685
+ if (value.length === 0) {
1686
+ return "''";
1687
+ }
1688
+ const needsQuoting = /[:#\n{}]/.test(value) || value.includes("[") || value.includes("]") || value !== value.trim() || value.includes("'");
1689
+ if (!needsQuoting) {
1690
+ return value;
1691
+ }
1692
+ return `'${value.replace(/'/g, "''")}'`;
1693
+ }
1694
+ function gitignoreLikelyCoversPath(relPosixPath, gitignoreBody) {
1695
+ const norm = relPosixPath.replace(/\\/g, "/").replace(/^\.\//, "");
1696
+ const lines = gitignoreBody.split(/\r?\n/);
1697
+ for (const raw of lines) {
1698
+ const line = raw.trim();
1699
+ if (!line || line.startsWith("#") || line.startsWith("!")) continue;
1700
+ const pat = line.replace(/^\//, "");
1701
+ if (pat === norm || pat === `./${norm}`) return true;
1702
+ if (!pat.includes("*")) {
1703
+ if (pat.endsWith("/")) {
1704
+ const dir = pat.slice(0, -1);
1705
+ if (norm === dir || norm.startsWith(`${dir}/`)) return true;
1706
+ }
1707
+ }
1708
+ }
1709
+ return false;
1710
+ }
1711
+ async function warnIfApiKeyFileNotGitignored(workspaceRoot, relativePosixPath) {
1712
+ const gitignorePath = join(workspaceRoot, ".gitignore");
1713
+ let content;
1714
+ try {
1715
+ content = await readFile(gitignorePath, "utf8");
1716
+ } catch (e) {
1717
+ if (isErrnoException(e) && e.code === "ENOENT") return;
1718
+ throw e;
1719
+ }
1720
+ const norm = relativePosixPath.replace(/\\/g, "/").replace(/^\.\//, "");
1721
+ if (gitignoreLikelyCoversPath(norm, content)) return;
1722
+ process.stderr.write(
1723
+ style.yellow("\u26A0") + " Config contains your API key. Add " + style.cyan(norm) + " to .gitignore to avoid committing credentials.\n"
1724
+ );
1725
+ }
1726
+ async function mergeTopLevelKeyedJsonFile(filePath, topLevelKey, shortName, entry, options) {
1654
1727
  let root = {};
1655
1728
  try {
1656
1729
  const raw = await readFile(filePath, "utf8");
1730
+ const toParse = options.stripJsonComments ? raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "") : raw;
1657
1731
  let parsed;
1658
1732
  try {
1659
- parsed = JSON.parse(raw);
1733
+ parsed = JSON.parse(toParse);
1660
1734
  } catch {
1661
1735
  return "parse-error";
1662
1736
  }
@@ -1672,15 +1746,25 @@ async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
1672
1746
  throw e;
1673
1747
  }
1674
1748
  }
1675
- const mcpRaw = root["mcpServers"];
1676
- const mcpServers = typeof mcpRaw === "object" && mcpRaw !== null && !Array.isArray(mcpRaw) ? { ...mcpRaw } : {};
1677
- mcpServers[shortName] = entry;
1678
- root["mcpServers"] = mcpServers;
1749
+ const bucketRaw = root[topLevelKey];
1750
+ const bucket = typeof bucketRaw === "object" && bucketRaw !== null && !Array.isArray(bucketRaw) ? { ...bucketRaw } : {};
1751
+ if (options.onExisting === "skip" && bucket[shortName] !== void 0) {
1752
+ return "unchanged";
1753
+ }
1754
+ bucket[shortName] = entry;
1755
+ root[topLevelKey] = bucket;
1679
1756
  await mkdir(dirname(filePath), { recursive: true });
1680
1757
  await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1681
1758
  writeMcpAddedLine(shortName, filePath);
1682
1759
  return "ok";
1683
1760
  }
1761
+ async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
1762
+ const result = await mergeTopLevelKeyedJsonFile(filePath, "mcpServers", shortName, entry, {
1763
+ stripJsonComments: false,
1764
+ onExisting: "overwrite"
1765
+ });
1766
+ return result === "parse-error" ? "parse-error" : "ok";
1767
+ }
1684
1768
  async function mergeClaudeDesktopHostedMcpRemote(shortName, proxyUrl, apiKey) {
1685
1769
  const entry = {
1686
1770
  command: "npx",
@@ -1688,8 +1772,30 @@ async function mergeClaudeDesktopHostedMcpRemote(shortName, proxyUrl, apiKey) {
1688
1772
  };
1689
1773
  return mergeMcpServersObjectStyle(getClaudeDesktopConfigPath(), shortName, entry);
1690
1774
  }
1691
- async function mergeContinueHostedMcp(shortName, proxyUrl, apiKey) {
1692
- const filePath = getContinueConfigJsonPath();
1775
+ async function mergeContinueHostedMcp(workspacePath, shortName, proxyUrl, apiKey) {
1776
+ const dir = join(workspacePath, ".continue", "mcpServers");
1777
+ const filePath = join(dir, `${shortName}.yaml`);
1778
+ const sn = sanitiseYamlValue(shortName);
1779
+ const urlEsc = sanitiseYamlValue(proxyUrl);
1780
+ const authEsc = sanitiseYamlValue(`Bearer ${apiKey}`);
1781
+ const yaml = `name: ${sn}
1782
+ version: 0.0.1
1783
+ schema: v1
1784
+ mcpServers:
1785
+ - name: ${sn}
1786
+ type: streamable-http
1787
+ url: ${urlEsc}
1788
+ headers:
1789
+ Authorization: ${authEsc}
1790
+ `;
1791
+ await mkdir(dir, { recursive: true });
1792
+ await writeFile(filePath, yaml, SECRET_JSON_FILE_OPTIONS);
1793
+ writeMcpAddedLine(shortName, filePath);
1794
+ await warnIfApiKeyFileNotGitignored(workspacePath, `.continue/mcpServers/${shortName}.yaml`);
1795
+ return "ok";
1796
+ }
1797
+ async function mergeCopilotVscodeMcp(workspacePath, shortName, proxyUrl, apiKey) {
1798
+ const filePath = join(workspacePath, ".vscode", "mcp.json");
1693
1799
  let root = {};
1694
1800
  try {
1695
1801
  const raw = await readFile(filePath, "utf8");
@@ -1711,43 +1817,92 @@ async function mergeContinueHostedMcp(shortName, proxyUrl, apiKey) {
1711
1817
  throw e;
1712
1818
  }
1713
1819
  }
1714
- const rawServers = root["mcpServers"];
1715
- if (rawServers !== void 0) {
1716
- if (Array.isArray(rawServers)) ; else if (typeof rawServers === "object" && rawServers !== null) {
1717
- return "parse-error";
1718
- } else {
1719
- return "parse-error";
1720
- }
1721
- }
1722
- const servers = Array.isArray(rawServers) ? rawServers.map((s) => ({ ...s })) : [];
1723
- const entry = {
1724
- name: shortName,
1725
- type: "streamable-http",
1820
+ const serversRaw = root["servers"];
1821
+ const servers = typeof serversRaw === "object" && serversRaw !== null && !Array.isArray(serversRaw) ? { ...serversRaw } : {};
1822
+ const existed = servers[shortName] !== void 0;
1823
+ servers[shortName] = {
1824
+ type: "http",
1726
1825
  url: proxyUrl,
1727
- headers: {
1728
- Authorization: `Bearer ${apiKey}`
1729
- }
1826
+ headers: { Authorization: `Bearer ${apiKey}` }
1730
1827
  };
1731
- const idx = servers.findIndex((s) => s["name"] === shortName);
1732
- if (idx >= 0) {
1733
- servers[idx] = entry;
1734
- } else {
1735
- servers.push(entry);
1736
- }
1737
- root["mcpServers"] = servers;
1828
+ root["servers"] = servers;
1738
1829
  await mkdir(dirname(filePath), { recursive: true });
1739
1830
  await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1740
- writeMcpAddedLine(shortName, filePath);
1831
+ if (existed) {
1832
+ process.stderr.write(
1833
+ style.dim(`Updated existing server entry for ${shortName} in .vscode/mcp.json`) + "\n"
1834
+ );
1835
+ } else {
1836
+ writeMcpAddedLine(shortName, filePath);
1837
+ }
1838
+ await warnIfApiKeyFileNotGitignored(workspacePath, ".vscode/mcp.json");
1741
1839
  return "ok";
1742
1840
  }
1743
1841
  async function mergeKiloCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKey) {
1744
- const filePath = join(workspacePath, ".kilocode", "mcp.json");
1745
- return mergeMcpServersObjectStyle(filePath, shortName, {
1746
- url: proxyUrl,
1747
- headers: {
1748
- Authorization: `Bearer ${apiKey}`
1842
+ const filePath = join(workspacePath, ".kilo", "kilo.jsonc");
1843
+ const result = await mergeTopLevelKeyedJsonFile(
1844
+ filePath,
1845
+ "mcp",
1846
+ shortName,
1847
+ {
1848
+ type: "remote",
1849
+ url: proxyUrl,
1850
+ headers: { Authorization: `Bearer ${apiKey}` },
1851
+ enabled: true
1852
+ },
1853
+ {
1854
+ stripJsonComments: true,
1855
+ onExisting: "skip"
1749
1856
  }
1750
- });
1857
+ );
1858
+ if (result === "parse-error") return "parse-error";
1859
+ if (result === "ok") {
1860
+ await warnIfApiKeyFileNotGitignored(workspacePath, ".kilo/kilo.jsonc");
1861
+ }
1862
+ return "ok";
1863
+ }
1864
+ async function injectOpencodeSchemaIntoConfigIfMissing(filePath) {
1865
+ try {
1866
+ const raw = await readFile(filePath, "utf8");
1867
+ const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
1868
+ let parsed;
1869
+ try {
1870
+ parsed = JSON.parse(stripped);
1871
+ } catch {
1872
+ return;
1873
+ }
1874
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return;
1875
+ const root = parsed;
1876
+ const existingSchema = root["$schema"];
1877
+ if (typeof existingSchema === "string" && existingSchema.length > 0) return;
1878
+ root["$schema"] = OPENCODE_CONFIG_SCHEMA_URL;
1879
+ await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1880
+ } catch {
1881
+ }
1882
+ }
1883
+ async function mergeOpenCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKey) {
1884
+ const filePath = join(workspacePath, "opencode.json");
1885
+ const result = await mergeTopLevelKeyedJsonFile(
1886
+ filePath,
1887
+ "mcp",
1888
+ shortName,
1889
+ {
1890
+ type: "remote",
1891
+ url: proxyUrl,
1892
+ headers: { Authorization: `Bearer ${apiKey}` },
1893
+ enabled: true
1894
+ },
1895
+ {
1896
+ stripJsonComments: true,
1897
+ onExisting: "skip"
1898
+ }
1899
+ );
1900
+ if (result === "parse-error") return "parse-error";
1901
+ await injectOpencodeSchemaIntoConfigIfMissing(filePath);
1902
+ if (result === "ok") {
1903
+ await warnIfApiKeyFileNotGitignored(workspacePath, "opencode.json");
1904
+ }
1905
+ return "ok";
1751
1906
  }
1752
1907
  function printHostedProxyJsonParseWarning(filePath) {
1753
1908
  process.stderr.write(
@@ -1788,7 +1943,14 @@ function printHostedProxyPostWriteHints(platform, shortName) {
1788
1943
  }
1789
1944
  if (platform === "kilo-code") {
1790
1945
  process.stderr.write(
1791
- style.dim("Restart Kilo Code or reload the window so it picks up .kilocode/mcp.json.") + "\n"
1946
+ style.dim("Restart Kilo Code or reload the window so it picks up .kilo/kilo.jsonc.") + "\n"
1947
+ );
1948
+ }
1949
+ if (platform === "opencode") {
1950
+ process.stderr.write(
1951
+ style.dim(
1952
+ "Restart OpenCode or start a new session so it picks up opencode.json. For global MCP, merge the same snippet into ~/.config/opencode/opencode.json."
1953
+ ) + "\n"
1792
1954
  );
1793
1955
  }
1794
1956
  }
@@ -1805,15 +1967,40 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
1805
1967
  return;
1806
1968
  }
1807
1969
  if (platform === "github-copilot") {
1808
- process.stderr.write(
1809
- "\n" + style.dim(
1810
- "GitHub Copilot uses VS Code settings - paste the snippet below into your VS Code Settings (JSON)."
1811
- ) + "\n"
1812
- );
1970
+ try {
1971
+ const result = await mergeCopilotVscodeMcp(
1972
+ workspacePath,
1973
+ shortName,
1974
+ proxyUrlWithKeyWhenNeeded,
1975
+ apiKey
1976
+ );
1977
+ if (result === "ok") {
1978
+ printHostedProxyPostWriteHints(platform, shortName);
1979
+ return;
1980
+ }
1981
+ printHostedProxyJsonParseWarning(join(workspacePath, ".vscode", "mcp.json"));
1982
+ } catch (err) {
1983
+ process.stderr.write(
1984
+ `${style.yellow("!")} Could not auto-write config: ${err instanceof Error ? err.message : String(err)}
1985
+ `
1986
+ );
1987
+ }
1813
1988
  printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1814
1989
  return;
1815
1990
  }
1816
1991
  if (platform === "goose") {
1992
+ try {
1993
+ const result = await mergeGooseConfig(shortName, proxyUrlWithKeyWhenNeeded, apiKey);
1994
+ if (result === "ok") {
1995
+ printHostedProxyPostWriteHints(platform, shortName);
1996
+ return;
1997
+ }
1998
+ } catch (err) {
1999
+ process.stderr.write(
2000
+ `${style.yellow("!")} Could not auto-write config: ${err instanceof Error ? err.message : String(err)}
2001
+ `
2002
+ );
2003
+ }
1817
2004
  printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1818
2005
  return;
1819
2006
  }
@@ -1860,12 +2047,29 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
1860
2047
  apiKey
1861
2048
  );
1862
2049
  if (result === "parse-error") {
1863
- printHostedProxyJsonParseWarning(join(workspacePath, ".kilocode", "mcp.json"));
2050
+ printHostedProxyJsonParseWarning(join(workspacePath, ".kilo", "kilo.jsonc"));
2051
+ }
2052
+ } else if (platform === "opencode") {
2053
+ result = await mergeOpenCodeProjectMcp(
2054
+ workspacePath,
2055
+ shortName,
2056
+ proxyUrlWithKeyWhenNeeded,
2057
+ apiKey
2058
+ );
2059
+ if (result === "parse-error") {
2060
+ printHostedProxyJsonParseWarning(join(workspacePath, "opencode.json"));
1864
2061
  }
1865
2062
  } else if (platform === "continue-dev") {
1866
- result = await mergeContinueHostedMcp(shortName, proxyUrlWithKeyWhenNeeded, apiKey);
2063
+ result = await mergeContinueHostedMcp(
2064
+ workspacePath,
2065
+ shortName,
2066
+ proxyUrlWithKeyWhenNeeded,
2067
+ apiKey
2068
+ );
1867
2069
  if (result === "parse-error") {
1868
- printHostedProxyJsonParseWarning(getContinueConfigJsonPath());
2070
+ printHostedProxyJsonParseWarning(
2071
+ join(workspacePath, ".continue", "mcpServers", `${shortName}.yaml`)
2072
+ );
1869
2073
  }
1870
2074
  } else {
1871
2075
  result = await mergeMcpServersObjectStyle(getCursorMcpJsonPath(), shortName, {
@@ -1889,17 +2093,96 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
1889
2093
  }
1890
2094
  printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1891
2095
  }
1892
- function gooseHostedProxyYaml(shortName, proxyUrl, bearerHeader) {
1893
- return `extensions:
1894
- ${shortName}:
2096
+ function printGooseConfigYamlParseErrorToStderr() {
2097
+ process.stderr.write(
2098
+ style.yellow("!") + " Could not parse ~/.config/goose/config.yaml - check for syntax errors or invalid YAML\n"
2099
+ );
2100
+ }
2101
+ function gooseExtensionYaml(shortName, proxyUrl, bearerHeader) {
2102
+ const sn = sanitiseYamlValue(shortName);
2103
+ const urlEsc = sanitiseYamlValue(proxyUrl);
2104
+ const authEsc = sanitiseYamlValue(bearerHeader);
2105
+ return ` ${sn}:
2106
+ enabled: true
1895
2107
  type: streamable_http
1896
- url: ${proxyUrl}
2108
+ name: ${sn}
2109
+ description: ''
2110
+ uri: ${urlEsc}
2111
+ envs: {}
2112
+ env_keys: []
1897
2113
  headers:
1898
- Authorization: ${bearerHeader}
1899
- enabled: true
2114
+ Authorization: ${authEsc}
1900
2115
  timeout: 300
2116
+ socket: null
2117
+ bundled: null
2118
+ available_tools: []
1901
2119
  `;
1902
2120
  }
2121
+ function gooseHostedProxyYaml(shortName, proxyUrl, bearerHeader) {
2122
+ return `extensions:
2123
+ ` + gooseExtensionYaml(shortName, proxyUrl, bearerHeader);
2124
+ }
2125
+ function isYamlPlainObject(value) {
2126
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2127
+ }
2128
+ async function mergeGooseConfig(shortName, proxyUrl, apiKey) {
2129
+ const filePath = join(homedir(), ".config", "goose", "config.yaml");
2130
+ const bearerHeader = `Bearer ${apiKey}`;
2131
+ let content = "";
2132
+ try {
2133
+ content = await readFile(filePath, "utf8");
2134
+ } catch (e) {
2135
+ if (isErrnoException(e) && e.code === "ENOENT") {
2136
+ content = "";
2137
+ } else {
2138
+ throw e;
2139
+ }
2140
+ }
2141
+ let root;
2142
+ try {
2143
+ const data = content.trim().length === 0 ? {} : parse(content);
2144
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
2145
+ printGooseConfigYamlParseErrorToStderr();
2146
+ return "parse-error";
2147
+ }
2148
+ root = data;
2149
+ } catch {
2150
+ printGooseConfigYamlParseErrorToStderr();
2151
+ return "parse-error";
2152
+ }
2153
+ const extensionsRaw = root["extensions"];
2154
+ let extensions;
2155
+ if (isYamlPlainObject(extensionsRaw)) {
2156
+ extensions = { ...extensionsRaw };
2157
+ } else if (extensionsRaw === void 0) {
2158
+ extensions = {};
2159
+ } else {
2160
+ printGooseConfigYamlParseErrorToStderr();
2161
+ return "parse-error";
2162
+ }
2163
+ extensions[shortName] = {
2164
+ enabled: true,
2165
+ type: "streamable_http",
2166
+ name: shortName,
2167
+ description: "",
2168
+ uri: proxyUrl,
2169
+ envs: {},
2170
+ env_keys: [],
2171
+ headers: { Authorization: bearerHeader },
2172
+ timeout: 300,
2173
+ socket: null,
2174
+ bundled: null,
2175
+ available_tools: []
2176
+ };
2177
+ root["extensions"] = extensions;
2178
+ const out = stringify(root, { indent: 2, lineWidth: 0 });
2179
+ const body = out.endsWith("\n") ? out : `${out}
2180
+ `;
2181
+ await mkdir(dirname(filePath), { recursive: true });
2182
+ await writeFile(filePath, body, SECRET_JSON_FILE_OPTIONS);
2183
+ writeMcpAddedLine(shortName, filePath);
2184
+ return "ok";
2185
+ }
1903
2186
  function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1904
2187
  const hostedInlinePlatforms = /* @__PURE__ */ new Set([
1905
2188
  "cursor",
@@ -1910,7 +2193,8 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1910
2193
  "kilo-code",
1911
2194
  "github-copilot",
1912
2195
  "continue-dev",
1913
- "goose"
2196
+ "goose",
2197
+ "opencode"
1914
2198
  ]);
1915
2199
  const usesInlineKey = hostedInlinePlatforms.has(platform);
1916
2200
  const authHeader = usesInlineKey ? `Bearer ${apiKey}` : "Bearer YOUR_SHIELD_API_KEY";
@@ -1919,14 +2203,12 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1919
2203
  if (platform === "github-copilot") {
1920
2204
  snippetText = JSON.stringify(
1921
2205
  {
1922
- mcp: {
1923
- servers: {
1924
- [shortName]: {
1925
- type: "http",
1926
- url: urlInSnippet,
1927
- headers: {
1928
- Authorization: authHeader
1929
- }
2206
+ servers: {
2207
+ [shortName]: {
2208
+ type: "http",
2209
+ url: urlInSnippet,
2210
+ headers: {
2211
+ Authorization: authHeader
1930
2212
  }
1931
2213
  }
1932
2214
  }
@@ -1965,18 +2247,50 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1965
2247
  2
1966
2248
  );
1967
2249
  } else if (platform === "continue-dev") {
2250
+ const sn = sanitiseYamlValue(shortName);
2251
+ const urlEsc = sanitiseYamlValue(urlInSnippet);
2252
+ const authEsc = sanitiseYamlValue(authHeader);
2253
+ snippetText = `name: ${sn}
2254
+ version: 0.0.1
2255
+ schema: v1
2256
+ mcpServers:
2257
+ - name: ${sn}
2258
+ type: streamable-http
2259
+ url: ${urlEsc}
2260
+ headers:
2261
+ Authorization: ${authEsc}
2262
+ `;
2263
+ } else if (platform === "kilo-code") {
1968
2264
  snippetText = JSON.stringify(
1969
2265
  {
1970
- mcpServers: [
1971
- {
1972
- name: shortName,
1973
- type: "streamable-http",
2266
+ mcp: {
2267
+ [shortName]: {
2268
+ type: "remote",
1974
2269
  url: urlInSnippet,
1975
2270
  headers: {
1976
2271
  Authorization: authHeader
1977
- }
2272
+ },
2273
+ enabled: true
1978
2274
  }
1979
- ]
2275
+ }
2276
+ },
2277
+ null,
2278
+ 2
2279
+ );
2280
+ } else if (platform === "opencode") {
2281
+ snippetText = JSON.stringify(
2282
+ {
2283
+ $schema: OPENCODE_CONFIG_SCHEMA_URL,
2284
+ mcp: {
2285
+ [shortName]: {
2286
+ type: "remote",
2287
+ url: urlInSnippet,
2288
+ headers: {
2289
+ Authorization: authHeader
2290
+ },
2291
+ enabled: true
2292
+ }
2293
+ }
1980
2294
  },
1981
2295
  null,
1982
2296
  2
@@ -2016,16 +2330,24 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
2016
2330
  );
2017
2331
  } else if (platform === "kilo-code") {
2018
2332
  process.stderr.write(
2019
- "\n" + style.dim(`Add this to ${join(resolve(process.cwd()), ".kilocode", "mcp.json")}:`) + "\n\n"
2333
+ "\n" + style.dim(`Add this to ${join(resolve(process.cwd()), ".kilo", "kilo.jsonc")}:`) + "\n\n"
2334
+ );
2335
+ } else if (platform === "opencode") {
2336
+ process.stderr.write(
2337
+ "\n" + style.dim(
2338
+ "Add this to opencode.json in your project root (or ~/.config/opencode/opencode.json for global config). OpenCode detects configured MCP servers on the next session start."
2339
+ ) + "\n\n"
2020
2340
  );
2021
2341
  } else if (platform === "github-copilot") {
2022
2342
  process.stderr.write(
2023
2343
  "\n" + style.dim(
2024
- "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."
2344
+ "Create .vscode/mcp.json in your workspace root (create the .vscode folder if it does not exist). After saving, reload VS Code and confirm the server appears in Copilot Agent mode under Tools."
2025
2345
  ) + "\n\n"
2026
2346
  );
2027
2347
  } else if (platform === "continue-dev") {
2028
- process.stderr.write("\n" + style.dim(`Add this to ${getContinueConfigJsonPath()}:`) + "\n\n");
2348
+ process.stderr.write(
2349
+ "\n" + style.dim(`Save this as .continue/mcpServers/${shortName}.yaml in your workspace root.`) + "\n\n"
2350
+ );
2029
2351
  } else if (platform === "goose") {
2030
2352
  process.stderr.write(
2031
2353
  "\n" + style.dim("Add this to ~/.config/goose/config.yaml under the extensions key.") + "\n\n"
@@ -2079,6 +2401,11 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
2079
2401
  if (platform === "goose") {
2080
2402
  process.stderr.write(style.dim("Start a new Goose session after updating config.") + "\n");
2081
2403
  }
2404
+ if (platform === "opencode") {
2405
+ process.stderr.write(
2406
+ style.dim("Restart OpenCode or start a new session after saving opencode.json.") + "\n"
2407
+ );
2408
+ }
2082
2409
  }
2083
2410
  function agentDisplayNameDedupeKey(name) {
2084
2411
  return name.trim().normalize("NFKC").toLowerCase();
@@ -2201,7 +2528,7 @@ async function runInit(explicitBaseUrl, options) {
2201
2528
  }
2202
2529
  await warnIfInstalledShieldIsOutdated();
2203
2530
  const configuredAgents = [];
2204
- let currentAgents = collectAgentsFromConfig(existing);
2531
+ let currentAgents = mergeAgentsForUniqueNames(collectAgentsFromConfig(existing));
2205
2532
  let lastConfig = {
2206
2533
  apiKey,
2207
2534
  baseUrl: resolvedBaseUrl,
@@ -2638,6 +2965,95 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2638
2965
  setupSucceeded = true;
2639
2966
  }
2640
2967
  }
2968
+ } else if (selectedPlatform === "opencode") {
2969
+ const opencodeMode = await promptOpencodeIntegrationMode(ask);
2970
+ if (opencodeMode === "native") {
2971
+ try {
2972
+ await installOpenCodeNativePlugin();
2973
+ process.stderr.write("\n" + style.bold("Shield OpenCode plugin installed") + "\n\n");
2974
+ process.stderr.write(
2975
+ style.dim("Plugin file: ") + style.cyan(getOpenCodeGlobalPluginsDir()) + "\n"
2976
+ );
2977
+ process.stderr.write("\n");
2978
+ process.stderr.write(
2979
+ style.dim(
2980
+ "Shield plugin saved under ~/.config/opencode/plugins/. Restart OpenCode. Every tool call from the primary agent will be checked by Shield."
2981
+ ) + "\n"
2982
+ );
2983
+ process.stderr.write("\n");
2984
+ process.stderr.write(
2985
+ style.dim(
2986
+ "Note: Tool calls delegated to subagents via the task tool are not intercepted by this plugin (OpenCode limitation). Use the hosted proxy path for MCP traffic you route through Shield for broader coverage."
2987
+ ) + "\n"
2988
+ );
2989
+ configuredAgents.push({
2990
+ selection,
2991
+ platform: selectedPlatform,
2992
+ platformLabel: selectedLabel,
2993
+ agentName,
2994
+ opencodeCliIntegration: "native"
2995
+ });
2996
+ setupSucceeded = true;
2997
+ } catch (error) {
2998
+ const detail = error instanceof Error ? error.message : String(error);
2999
+ process.stderr.write(style.red("\u2717 ") + detail + "\n");
3000
+ }
3001
+ } else {
3002
+ const { targetUrl, shortName, upstreamHeaders } = await promptProxyConfig(ask, agentName);
3003
+ let proxyUrl = "";
3004
+ let created = false;
3005
+ while (!created) {
3006
+ const spinner = withSpinner("Creating proxy config...");
3007
+ try {
3008
+ proxyUrl = await createProxyConfig(
3009
+ resolvedBaseUrl,
3010
+ apiKey,
3011
+ agentName,
3012
+ targetUrl,
3013
+ shortName,
3014
+ selectedPlatform,
3015
+ upstreamHeaders
3016
+ );
3017
+ spinner.stop(true, "Proxy config created!");
3018
+ created = true;
3019
+ } catch (error) {
3020
+ const detail = error instanceof Error ? error.message : String(error);
3021
+ spinner.stop(false, detail);
3022
+ const retry = await ask("Try again? (Y/n) ");
3023
+ if (retry.trim().toLowerCase() === "n") {
3024
+ break;
3025
+ }
3026
+ }
3027
+ }
3028
+ if (created && proxyUrl.length > 0) {
3029
+ process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
3030
+ process.stderr.write(
3031
+ " " + style.cyan(formatHostedProxyUrlForStderr(selectedPlatform, proxyUrl, apiKey)) + "\n"
3032
+ );
3033
+ await applyHostedProxyMcpConfig(
3034
+ selectedPlatform,
3035
+ proxyUrl,
3036
+ shortName,
3037
+ apiKey,
3038
+ initWorkspacePath
3039
+ );
3040
+ process.stderr.write(
3041
+ "\n" + style.dim(
3042
+ "Add this to opencode.json in your project root (or ~/.config/opencode/opencode.json for global config). OpenCode detects configured MCP servers on the next session start."
3043
+ ) + "\n"
3044
+ );
3045
+ configuredAgents.push({
3046
+ selection,
3047
+ platform: selectedPlatform,
3048
+ platformLabel: selectedLabel,
3049
+ agentName,
3050
+ shortName,
3051
+ proxyUrl,
3052
+ opencodeCliIntegration: "hosted"
3053
+ });
3054
+ setupSucceeded = true;
3055
+ }
3056
+ }
2641
3057
  } else if (selectedPlatform === "cline") {
2642
3058
  const clineMode = await promptClineIntegrationMode(ask);
2643
3059
  if (clineMode === "native") {
@@ -2862,23 +3278,27 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2862
3278
  );
2863
3279
  }
2864
3280
  if (configuredPlatforms.has("kilo-code")) {
3281
+ const kiloLabel = mcpPromptLabel2("kilo-code");
2865
3282
  blocks.push(
2866
- "\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"
3283
+ "\n" + style.bold("Kilo Code") + '\n \u2192 Restart the editor or reload the window if the MCP server does not appear\n \u2192 Confirm connection: Settings \u2192 Agent Behaviour \u2192 MCP Servers\n \u2192 Try it: paste this into Kilo Code:\n "Use the ' + kiloLabel + ' MCP server to list my GitHub repositories"\n'
2867
3284
  );
2868
3285
  }
2869
3286
  if (configuredPlatforms.has("github-copilot")) {
3287
+ const copilotLabel = mcpPromptLabel2("github-copilot");
2870
3288
  blocks.push(
2871
- "\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"
3289
+ "\n" + style.bold("GitHub Copilot") + '\n \u2192 Reload the editor window if the MCP server does not appear\n \u2192 Confirm connection: open Copilot chat in Agent mode and confirm the server appears under Tools\n \u2192 Try it: paste this into GitHub Copilot:\n "Use the ' + copilotLabel + ' MCP server to list my GitHub repositories"\n'
2872
3290
  );
2873
3291
  }
2874
3292
  if (configuredPlatforms.has("continue-dev")) {
3293
+ const continueLabel = mcpPromptLabel2("continue-dev");
2875
3294
  blocks.push(
2876
- "\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"
3295
+ "\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 Confirm connection: Settings \u2192 Tools \u2192 MCP Servers\n \u2192 Try it: paste this into Continue:\n "Use the ' + continueLabel + ' MCP server to list my GitHub repositories"\n'
2877
3296
  );
2878
3297
  }
2879
3298
  if (configuredPlatforms.has("goose")) {
3299
+ const gooseLabel = mcpPromptLabel2("goose");
2880
3300
  blocks.push(
2881
- "\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"
3301
+ "\n" + style.bold("Goose") + '\n \u2192 Start a new Goose session after updating config\n \u2192 Confirm connection: check the Extensions page in the sidebar\n \u2192 Try it: paste this into Goose:\n "Use the ' + gooseLabel + ' MCP server to list my GitHub repositories"\n'
2882
3302
  );
2883
3303
  }
2884
3304
  const windsurfNativeConfigured = configuredAgents.some(
@@ -2929,6 +3349,23 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2929
3349
  "\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"
2930
3350
  );
2931
3351
  }
3352
+ const opencodeNativeConfigured = configuredAgents.some(
3353
+ (a) => a.platform === "opencode" && a.opencodeCliIntegration === "native"
3354
+ );
3355
+ const opencodeHostedConfigured = configuredAgents.some(
3356
+ (a) => a.platform === "opencode" && a.opencodeCliIntegration === "hosted"
3357
+ );
3358
+ if (opencodeNativeConfigured) {
3359
+ blocks.push(
3360
+ "\n" + style.bold("OpenCode (native)") + "\n \u2192 If you just installed OpenCode, open a new terminal tab (or run: source ~/.zshrc)\n \u2192 Restart OpenCode so it loads ~/.config/opencode/plugins/multicorn-shield.ts\n \u2192 Try it: trigger a primary-agent tool call - Shield will intercept the first actionable tool and ask for your consent\n"
3361
+ );
3362
+ }
3363
+ if (opencodeHostedConfigured) {
3364
+ const ocLabel = mcpPromptLabel2("opencode");
3365
+ blocks.push(
3366
+ "\n" + style.bold("OpenCode (hosted)") + '\n \u2192 Restart OpenCode or start a new session after saving opencode.json\n \u2192 Try it: paste this into OpenCode:\n "Use the ' + ocLabel + ' MCP server to list allowed directories"\n'
3367
+ );
3368
+ }
2932
3369
  if (configuredPlatforms.has("other-mcp")) {
2933
3370
  blocks.push(
2934
3371
  "\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"
@@ -2948,7 +3385,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2948
3385
  }
2949
3386
  return lastConfig;
2950
3387
  }
2951
- 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, PLATFORM_BY_SELECTION, HOSTED_PROXY_PLATFORMS_WITH_URL_KEY, DEFAULT_SHIELD_API_BASE_URL;
3388
+ 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, PLATFORM_BY_SELECTION, HOSTED_PROXY_PLATFORMS_WITH_URL_KEY, OPENCODE_CONFIG_SCHEMA_URL, DEFAULT_SHIELD_API_BASE_URL;
2952
3389
  var init_config = __esm({
2953
3390
  "src/proxy/config.ts"() {
2954
3391
  init_consent();
@@ -2988,6 +3425,12 @@ var init_config = __esm({
2988
3425
  { slug: "windsurf", displayName: "Windsurf", section: "native" },
2989
3426
  { slug: "cline", displayName: "Cline", section: "native" },
2990
3427
  { slug: "gemini-cli", displayName: "Gemini CLI", section: "native" },
3428
+ {
3429
+ slug: "opencode",
3430
+ displayName: "OpenCode",
3431
+ section: "native",
3432
+ prereqUrl: "https://opencode.ai"
3433
+ },
2991
3434
  {
2992
3435
  slug: "cursor",
2993
3436
  displayName: "Cursor",
@@ -3048,6 +3491,7 @@ var init_config = __esm({
3048
3491
  "continue-dev",
3049
3492
  "goose"
3050
3493
  ]);
3494
+ OPENCODE_CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
3051
3495
  DEFAULT_SHIELD_API_BASE_URL = "https://api.multicorn.ai";
3052
3496
  }
3053
3497
  });
@@ -3992,7 +4436,7 @@ var init_package = __esm({
3992
4436
  "package.json"() {
3993
4437
  package_default = {
3994
4438
  name: "multicorn-shield",
3995
- version: "1.6.0",
4439
+ version: "1.8.0",
3996
4440
  description: "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
3997
4441
  license: "MIT",
3998
4442
  author: "Multicorn AI Pty Ltd",
@@ -4032,6 +4476,7 @@ var init_package = __esm({
4032
4476
  "plugins/windsurf",
4033
4477
  "plugins/cline",
4034
4478
  "plugins/gemini-cli",
4479
+ "plugins/opencode",
4035
4480
  "LICENSE",
4036
4481
  "README.md",
4037
4482
  "CHANGELOG.md"
@@ -4083,12 +4528,14 @@ var init_package = __esm({
4083
4528
  dependencies: {
4084
4529
  "@modelcontextprotocol/sdk": "^1.27.1",
4085
4530
  lit: "^3.2.0",
4531
+ yaml: "^2.8.2",
4086
4532
  zod: "^4.3.6"
4087
4533
  },
4088
4534
  devDependencies: {
4089
4535
  "@anthropic-ai/mcpb": "^2.1.2",
4090
4536
  "@eslint/js": "^9.19.0",
4091
4537
  "@open-wc/testing-helpers": "^3.0.1",
4538
+ "@opencode-ai/plugin": "^1.14.48",
4092
4539
  "@size-limit/file": "^11.1.6",
4093
4540
  "@types/node": "^22.0.0",
4094
4541
  "@vitest/coverage-v8": "^3.0.5",