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.
@@ -6,6 +6,7 @@ import { homedir } from 'os';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { createRequire } from 'module';
8
8
  import { createInterface } from 'readline';
9
+ import { parse, stringify } from 'yaml';
9
10
  import { spawn } from 'child_process';
10
11
  import { createHash } from 'crypto';
11
12
  import 'stream';
@@ -938,6 +939,22 @@ require(${JSON.stringify(destPost)});
938
939
  await writeFile(preWrapper, preContent, { encoding: "utf8", mode: 493 });
939
940
  await writeFile(postWrapper, postContent, { encoding: "utf8", mode: 493 });
940
941
  }
942
+ function getOpenCodeGlobalPluginsDir() {
943
+ return join(homedir(), ".config", "opencode", "plugins");
944
+ }
945
+ async function installOpenCodeNativePlugin() {
946
+ const root = multicornShieldPackageRoot();
947
+ const src = join(root, "plugins", "opencode", "multicorn-shield.ts");
948
+ if (!existsSync(src)) {
949
+ throw new Error(
950
+ `Could not find Shield OpenCode plugin at ${src}. If you use npm, install the latest multicorn-shield package.`
951
+ );
952
+ }
953
+ const destDir = getOpenCodeGlobalPluginsDir();
954
+ await mkdir(destDir, { recursive: true });
955
+ const dest = join(destDir, "multicorn-shield.ts");
956
+ await copyFile(src, dest);
957
+ }
941
958
  async function promptClineIntegrationMode(ask) {
942
959
  process.stderr.write("\n" + style.bold("Cline integration") + "\n");
943
960
  process.stderr.write(
@@ -1297,6 +1314,23 @@ async function promptGeminiCliIntegrationMode(ask) {
1297
1314
  }
1298
1315
  return choice === 1 ? "native" : "hosted";
1299
1316
  }
1317
+ async function promptOpencodeIntegrationMode(ask) {
1318
+ process.stderr.write("\n" + style.bold("OpenCode integration") + "\n");
1319
+ process.stderr.write(
1320
+ " " + style.violet("1") + ". Native plugin (recommended) - Shield checks primary-agent tool execution via OpenCode Hooks\n"
1321
+ );
1322
+ process.stderr.write(
1323
+ " " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via opencode.json (full subagent coverage when tools use MCP through Shield)\n"
1324
+ );
1325
+ let choice = 0;
1326
+ while (choice === 0) {
1327
+ const input = await ask("Choose integration (1-2): ");
1328
+ const num = parseInt(input.trim(), 10);
1329
+ if (num === 1) choice = 1;
1330
+ if (num === 2) choice = 2;
1331
+ }
1332
+ return choice === 1 ? "native" : "hosted";
1333
+ }
1300
1334
  function getClaudeDesktopConfigPath() {
1301
1335
  switch (process.platform) {
1302
1336
  case "win32":
@@ -1360,15 +1394,18 @@ function getClineMcpSettingsPath() {
1360
1394
  );
1361
1395
  }
1362
1396
  }
1363
- function getContinueConfigJsonPath() {
1364
- return join(homedir(), ".continue", "config.json");
1365
- }
1366
1397
  var INIT_WIZARD_PLATFORM_REGISTRY = [
1367
1398
  { slug: "openclaw", displayName: "OpenClaw", section: "native" },
1368
1399
  { slug: "claude-code", displayName: "Claude Code", section: "native" },
1369
1400
  { slug: "windsurf", displayName: "Windsurf", section: "native" },
1370
1401
  { slug: "cline", displayName: "Cline", section: "native" },
1371
1402
  { slug: "gemini-cli", displayName: "Gemini CLI", section: "native" },
1403
+ {
1404
+ slug: "opencode",
1405
+ displayName: "OpenCode",
1406
+ section: "native",
1407
+ prereqUrl: "https://opencode.ai"
1408
+ },
1372
1409
  {
1373
1410
  slug: "cursor",
1374
1411
  displayName: "Cursor",
@@ -1730,13 +1767,56 @@ function writeMcpAddedLine(shortName, filePath) {
1730
1767
  style.green("\u2713") + ' MCP server "' + shortName + '" added to ' + style.cyan(filePath) + "\n"
1731
1768
  );
1732
1769
  }
1733
- async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
1770
+ function sanitiseYamlValue(value) {
1771
+ if (value.length === 0) {
1772
+ return "''";
1773
+ }
1774
+ const needsQuoting = /[:#\n{}]/.test(value) || value.includes("[") || value.includes("]") || value !== value.trim() || value.includes("'");
1775
+ if (!needsQuoting) {
1776
+ return value;
1777
+ }
1778
+ return `'${value.replace(/'/g, "''")}'`;
1779
+ }
1780
+ function gitignoreLikelyCoversPath(relPosixPath, gitignoreBody) {
1781
+ const norm = relPosixPath.replace(/\\/g, "/").replace(/^\.\//, "");
1782
+ const lines = gitignoreBody.split(/\r?\n/);
1783
+ for (const raw of lines) {
1784
+ const line = raw.trim();
1785
+ if (!line || line.startsWith("#") || line.startsWith("!")) continue;
1786
+ const pat = line.replace(/^\//, "");
1787
+ if (pat === norm || pat === `./${norm}`) return true;
1788
+ if (!pat.includes("*")) {
1789
+ if (pat.endsWith("/")) {
1790
+ const dir = pat.slice(0, -1);
1791
+ if (norm === dir || norm.startsWith(`${dir}/`)) return true;
1792
+ }
1793
+ }
1794
+ }
1795
+ return false;
1796
+ }
1797
+ async function warnIfApiKeyFileNotGitignored(workspaceRoot, relativePosixPath) {
1798
+ const gitignorePath = join(workspaceRoot, ".gitignore");
1799
+ let content;
1800
+ try {
1801
+ content = await readFile(gitignorePath, "utf8");
1802
+ } catch (e) {
1803
+ if (isErrnoException(e) && e.code === "ENOENT") return;
1804
+ throw e;
1805
+ }
1806
+ const norm = relativePosixPath.replace(/\\/g, "/").replace(/^\.\//, "");
1807
+ if (gitignoreLikelyCoversPath(norm, content)) return;
1808
+ process.stderr.write(
1809
+ style.yellow("\u26A0") + " Config contains your API key. Add " + style.cyan(norm) + " to .gitignore to avoid committing credentials.\n"
1810
+ );
1811
+ }
1812
+ async function mergeTopLevelKeyedJsonFile(filePath, topLevelKey, shortName, entry, options) {
1734
1813
  let root = {};
1735
1814
  try {
1736
1815
  const raw = await readFile(filePath, "utf8");
1816
+ const toParse = options.stripJsonComments ? raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "") : raw;
1737
1817
  let parsed;
1738
1818
  try {
1739
- parsed = JSON.parse(raw);
1819
+ parsed = JSON.parse(toParse);
1740
1820
  } catch {
1741
1821
  return "parse-error";
1742
1822
  }
@@ -1752,15 +1832,25 @@ async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
1752
1832
  throw e;
1753
1833
  }
1754
1834
  }
1755
- const mcpRaw = root["mcpServers"];
1756
- const mcpServers = typeof mcpRaw === "object" && mcpRaw !== null && !Array.isArray(mcpRaw) ? { ...mcpRaw } : {};
1757
- mcpServers[shortName] = entry;
1758
- root["mcpServers"] = mcpServers;
1835
+ const bucketRaw = root[topLevelKey];
1836
+ const bucket = typeof bucketRaw === "object" && bucketRaw !== null && !Array.isArray(bucketRaw) ? { ...bucketRaw } : {};
1837
+ if (options.onExisting === "skip" && bucket[shortName] !== void 0) {
1838
+ return "unchanged";
1839
+ }
1840
+ bucket[shortName] = entry;
1841
+ root[topLevelKey] = bucket;
1759
1842
  await mkdir(dirname(filePath), { recursive: true });
1760
1843
  await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1761
1844
  writeMcpAddedLine(shortName, filePath);
1762
1845
  return "ok";
1763
1846
  }
1847
+ async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
1848
+ const result = await mergeTopLevelKeyedJsonFile(filePath, "mcpServers", shortName, entry, {
1849
+ stripJsonComments: false,
1850
+ onExisting: "overwrite"
1851
+ });
1852
+ return result === "parse-error" ? "parse-error" : "ok";
1853
+ }
1764
1854
  async function mergeClaudeDesktopHostedMcpRemote(shortName, proxyUrl, apiKey) {
1765
1855
  const entry = {
1766
1856
  command: "npx",
@@ -1768,8 +1858,30 @@ async function mergeClaudeDesktopHostedMcpRemote(shortName, proxyUrl, apiKey) {
1768
1858
  };
1769
1859
  return mergeMcpServersObjectStyle(getClaudeDesktopConfigPath(), shortName, entry);
1770
1860
  }
1771
- async function mergeContinueHostedMcp(shortName, proxyUrl, apiKey) {
1772
- const filePath = getContinueConfigJsonPath();
1861
+ async function mergeContinueHostedMcp(workspacePath, shortName, proxyUrl, apiKey) {
1862
+ const dir = join(workspacePath, ".continue", "mcpServers");
1863
+ const filePath = join(dir, `${shortName}.yaml`);
1864
+ const sn = sanitiseYamlValue(shortName);
1865
+ const urlEsc = sanitiseYamlValue(proxyUrl);
1866
+ const authEsc = sanitiseYamlValue(`Bearer ${apiKey}`);
1867
+ const yaml = `name: ${sn}
1868
+ version: 0.0.1
1869
+ schema: v1
1870
+ mcpServers:
1871
+ - name: ${sn}
1872
+ type: streamable-http
1873
+ url: ${urlEsc}
1874
+ headers:
1875
+ Authorization: ${authEsc}
1876
+ `;
1877
+ await mkdir(dir, { recursive: true });
1878
+ await writeFile(filePath, yaml, SECRET_JSON_FILE_OPTIONS);
1879
+ writeMcpAddedLine(shortName, filePath);
1880
+ await warnIfApiKeyFileNotGitignored(workspacePath, `.continue/mcpServers/${shortName}.yaml`);
1881
+ return "ok";
1882
+ }
1883
+ async function mergeCopilotVscodeMcp(workspacePath, shortName, proxyUrl, apiKey) {
1884
+ const filePath = join(workspacePath, ".vscode", "mcp.json");
1773
1885
  let root = {};
1774
1886
  try {
1775
1887
  const raw = await readFile(filePath, "utf8");
@@ -1791,43 +1903,93 @@ async function mergeContinueHostedMcp(shortName, proxyUrl, apiKey) {
1791
1903
  throw e;
1792
1904
  }
1793
1905
  }
1794
- const rawServers = root["mcpServers"];
1795
- if (rawServers !== void 0) {
1796
- if (Array.isArray(rawServers)) ; else if (typeof rawServers === "object" && rawServers !== null) {
1797
- return "parse-error";
1798
- } else {
1799
- return "parse-error";
1800
- }
1801
- }
1802
- const servers = Array.isArray(rawServers) ? rawServers.map((s) => ({ ...s })) : [];
1803
- const entry = {
1804
- name: shortName,
1805
- type: "streamable-http",
1906
+ const serversRaw = root["servers"];
1907
+ const servers = typeof serversRaw === "object" && serversRaw !== null && !Array.isArray(serversRaw) ? { ...serversRaw } : {};
1908
+ const existed = servers[shortName] !== void 0;
1909
+ servers[shortName] = {
1910
+ type: "http",
1806
1911
  url: proxyUrl,
1807
- headers: {
1808
- Authorization: `Bearer ${apiKey}`
1809
- }
1912
+ headers: { Authorization: `Bearer ${apiKey}` }
1810
1913
  };
1811
- const idx = servers.findIndex((s) => s["name"] === shortName);
1812
- if (idx >= 0) {
1813
- servers[idx] = entry;
1814
- } else {
1815
- servers.push(entry);
1816
- }
1817
- root["mcpServers"] = servers;
1914
+ root["servers"] = servers;
1818
1915
  await mkdir(dirname(filePath), { recursive: true });
1819
1916
  await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1820
- writeMcpAddedLine(shortName, filePath);
1917
+ if (existed) {
1918
+ process.stderr.write(
1919
+ style.dim(`Updated existing server entry for ${shortName} in .vscode/mcp.json`) + "\n"
1920
+ );
1921
+ } else {
1922
+ writeMcpAddedLine(shortName, filePath);
1923
+ }
1924
+ await warnIfApiKeyFileNotGitignored(workspacePath, ".vscode/mcp.json");
1821
1925
  return "ok";
1822
1926
  }
1823
1927
  async function mergeKiloCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKey) {
1824
- const filePath = join(workspacePath, ".kilocode", "mcp.json");
1825
- return mergeMcpServersObjectStyle(filePath, shortName, {
1826
- url: proxyUrl,
1827
- headers: {
1828
- Authorization: `Bearer ${apiKey}`
1928
+ const filePath = join(workspacePath, ".kilo", "kilo.jsonc");
1929
+ const result = await mergeTopLevelKeyedJsonFile(
1930
+ filePath,
1931
+ "mcp",
1932
+ shortName,
1933
+ {
1934
+ type: "remote",
1935
+ url: proxyUrl,
1936
+ headers: { Authorization: `Bearer ${apiKey}` },
1937
+ enabled: true
1938
+ },
1939
+ {
1940
+ stripJsonComments: true,
1941
+ onExisting: "skip"
1829
1942
  }
1830
- });
1943
+ );
1944
+ if (result === "parse-error") return "parse-error";
1945
+ if (result === "ok") {
1946
+ await warnIfApiKeyFileNotGitignored(workspacePath, ".kilo/kilo.jsonc");
1947
+ }
1948
+ return "ok";
1949
+ }
1950
+ var OPENCODE_CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
1951
+ async function injectOpencodeSchemaIntoConfigIfMissing(filePath) {
1952
+ try {
1953
+ const raw = await readFile(filePath, "utf8");
1954
+ const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
1955
+ let parsed;
1956
+ try {
1957
+ parsed = JSON.parse(stripped);
1958
+ } catch {
1959
+ return;
1960
+ }
1961
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return;
1962
+ const root = parsed;
1963
+ const existingSchema = root["$schema"];
1964
+ if (typeof existingSchema === "string" && existingSchema.length > 0) return;
1965
+ root["$schema"] = OPENCODE_CONFIG_SCHEMA_URL;
1966
+ await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1967
+ } catch {
1968
+ }
1969
+ }
1970
+ async function mergeOpenCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKey) {
1971
+ const filePath = join(workspacePath, "opencode.json");
1972
+ const result = await mergeTopLevelKeyedJsonFile(
1973
+ filePath,
1974
+ "mcp",
1975
+ shortName,
1976
+ {
1977
+ type: "remote",
1978
+ url: proxyUrl,
1979
+ headers: { Authorization: `Bearer ${apiKey}` },
1980
+ enabled: true
1981
+ },
1982
+ {
1983
+ stripJsonComments: true,
1984
+ onExisting: "skip"
1985
+ }
1986
+ );
1987
+ if (result === "parse-error") return "parse-error";
1988
+ await injectOpencodeSchemaIntoConfigIfMissing(filePath);
1989
+ if (result === "ok") {
1990
+ await warnIfApiKeyFileNotGitignored(workspacePath, "opencode.json");
1991
+ }
1992
+ return "ok";
1831
1993
  }
1832
1994
  function printHostedProxyJsonParseWarning(filePath) {
1833
1995
  process.stderr.write(
@@ -1868,7 +2030,14 @@ function printHostedProxyPostWriteHints(platform, shortName) {
1868
2030
  }
1869
2031
  if (platform === "kilo-code") {
1870
2032
  process.stderr.write(
1871
- style.dim("Restart Kilo Code or reload the window so it picks up .kilocode/mcp.json.") + "\n"
2033
+ style.dim("Restart Kilo Code or reload the window so it picks up .kilo/kilo.jsonc.") + "\n"
2034
+ );
2035
+ }
2036
+ if (platform === "opencode") {
2037
+ process.stderr.write(
2038
+ style.dim(
2039
+ "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."
2040
+ ) + "\n"
1872
2041
  );
1873
2042
  }
1874
2043
  }
@@ -1885,15 +2054,40 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
1885
2054
  return;
1886
2055
  }
1887
2056
  if (platform === "github-copilot") {
1888
- process.stderr.write(
1889
- "\n" + style.dim(
1890
- "GitHub Copilot uses VS Code settings - paste the snippet below into your VS Code Settings (JSON)."
1891
- ) + "\n"
1892
- );
2057
+ try {
2058
+ const result = await mergeCopilotVscodeMcp(
2059
+ workspacePath,
2060
+ shortName,
2061
+ proxyUrlWithKeyWhenNeeded,
2062
+ apiKey
2063
+ );
2064
+ if (result === "ok") {
2065
+ printHostedProxyPostWriteHints(platform, shortName);
2066
+ return;
2067
+ }
2068
+ printHostedProxyJsonParseWarning(join(workspacePath, ".vscode", "mcp.json"));
2069
+ } catch (err) {
2070
+ process.stderr.write(
2071
+ `${style.yellow("!")} Could not auto-write config: ${err instanceof Error ? err.message : String(err)}
2072
+ `
2073
+ );
2074
+ }
1893
2075
  printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1894
2076
  return;
1895
2077
  }
1896
2078
  if (platform === "goose") {
2079
+ try {
2080
+ const result = await mergeGooseConfig(shortName, proxyUrlWithKeyWhenNeeded, apiKey);
2081
+ if (result === "ok") {
2082
+ printHostedProxyPostWriteHints(platform, shortName);
2083
+ return;
2084
+ }
2085
+ } catch (err) {
2086
+ process.stderr.write(
2087
+ `${style.yellow("!")} Could not auto-write config: ${err instanceof Error ? err.message : String(err)}
2088
+ `
2089
+ );
2090
+ }
1897
2091
  printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1898
2092
  return;
1899
2093
  }
@@ -1940,12 +2134,29 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
1940
2134
  apiKey
1941
2135
  );
1942
2136
  if (result === "parse-error") {
1943
- printHostedProxyJsonParseWarning(join(workspacePath, ".kilocode", "mcp.json"));
2137
+ printHostedProxyJsonParseWarning(join(workspacePath, ".kilo", "kilo.jsonc"));
2138
+ }
2139
+ } else if (platform === "opencode") {
2140
+ result = await mergeOpenCodeProjectMcp(
2141
+ workspacePath,
2142
+ shortName,
2143
+ proxyUrlWithKeyWhenNeeded,
2144
+ apiKey
2145
+ );
2146
+ if (result === "parse-error") {
2147
+ printHostedProxyJsonParseWarning(join(workspacePath, "opencode.json"));
1944
2148
  }
1945
2149
  } else if (platform === "continue-dev") {
1946
- result = await mergeContinueHostedMcp(shortName, proxyUrlWithKeyWhenNeeded, apiKey);
2150
+ result = await mergeContinueHostedMcp(
2151
+ workspacePath,
2152
+ shortName,
2153
+ proxyUrlWithKeyWhenNeeded,
2154
+ apiKey
2155
+ );
1947
2156
  if (result === "parse-error") {
1948
- printHostedProxyJsonParseWarning(getContinueConfigJsonPath());
2157
+ printHostedProxyJsonParseWarning(
2158
+ join(workspacePath, ".continue", "mcpServers", `${shortName}.yaml`)
2159
+ );
1949
2160
  }
1950
2161
  } else {
1951
2162
  result = await mergeMcpServersObjectStyle(getCursorMcpJsonPath(), shortName, {
@@ -1969,16 +2180,95 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
1969
2180
  }
1970
2181
  printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
1971
2182
  }
1972
- function gooseHostedProxyYaml(shortName, proxyUrl, bearerHeader) {
1973
- return `extensions:
1974
- ${shortName}:
2183
+ function printGooseConfigYamlParseErrorToStderr() {
2184
+ process.stderr.write(
2185
+ style.yellow("!") + " Could not parse ~/.config/goose/config.yaml - check for syntax errors or invalid YAML\n"
2186
+ );
2187
+ }
2188
+ function gooseExtensionYaml(shortName, proxyUrl, bearerHeader) {
2189
+ const sn = sanitiseYamlValue(shortName);
2190
+ const urlEsc = sanitiseYamlValue(proxyUrl);
2191
+ const authEsc = sanitiseYamlValue(bearerHeader);
2192
+ return ` ${sn}:
2193
+ enabled: true
1975
2194
  type: streamable_http
1976
- url: ${proxyUrl}
2195
+ name: ${sn}
2196
+ description: ''
2197
+ uri: ${urlEsc}
2198
+ envs: {}
2199
+ env_keys: []
1977
2200
  headers:
1978
- Authorization: ${bearerHeader}
1979
- enabled: true
2201
+ Authorization: ${authEsc}
1980
2202
  timeout: 300
2203
+ socket: null
2204
+ bundled: null
2205
+ available_tools: []
2206
+ `;
2207
+ }
2208
+ function gooseHostedProxyYaml(shortName, proxyUrl, bearerHeader) {
2209
+ return `extensions:
2210
+ ` + gooseExtensionYaml(shortName, proxyUrl, bearerHeader);
2211
+ }
2212
+ function isYamlPlainObject(value) {
2213
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2214
+ }
2215
+ async function mergeGooseConfig(shortName, proxyUrl, apiKey) {
2216
+ const filePath = join(homedir(), ".config", "goose", "config.yaml");
2217
+ const bearerHeader = `Bearer ${apiKey}`;
2218
+ let content = "";
2219
+ try {
2220
+ content = await readFile(filePath, "utf8");
2221
+ } catch (e) {
2222
+ if (isErrnoException(e) && e.code === "ENOENT") {
2223
+ content = "";
2224
+ } else {
2225
+ throw e;
2226
+ }
2227
+ }
2228
+ let root;
2229
+ try {
2230
+ const data = content.trim().length === 0 ? {} : parse(content);
2231
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
2232
+ printGooseConfigYamlParseErrorToStderr();
2233
+ return "parse-error";
2234
+ }
2235
+ root = data;
2236
+ } catch {
2237
+ printGooseConfigYamlParseErrorToStderr();
2238
+ return "parse-error";
2239
+ }
2240
+ const extensionsRaw = root["extensions"];
2241
+ let extensions;
2242
+ if (isYamlPlainObject(extensionsRaw)) {
2243
+ extensions = { ...extensionsRaw };
2244
+ } else if (extensionsRaw === void 0) {
2245
+ extensions = {};
2246
+ } else {
2247
+ printGooseConfigYamlParseErrorToStderr();
2248
+ return "parse-error";
2249
+ }
2250
+ extensions[shortName] = {
2251
+ enabled: true,
2252
+ type: "streamable_http",
2253
+ name: shortName,
2254
+ description: "",
2255
+ uri: proxyUrl,
2256
+ envs: {},
2257
+ env_keys: [],
2258
+ headers: { Authorization: bearerHeader },
2259
+ timeout: 300,
2260
+ socket: null,
2261
+ bundled: null,
2262
+ available_tools: []
2263
+ };
2264
+ root["extensions"] = extensions;
2265
+ const out = stringify(root, { indent: 2, lineWidth: 0 });
2266
+ const body = out.endsWith("\n") ? out : `${out}
1981
2267
  `;
2268
+ await mkdir(dirname(filePath), { recursive: true });
2269
+ await writeFile(filePath, body, SECRET_JSON_FILE_OPTIONS);
2270
+ writeMcpAddedLine(shortName, filePath);
2271
+ return "ok";
1982
2272
  }
1983
2273
  function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1984
2274
  const hostedInlinePlatforms = /* @__PURE__ */ new Set([
@@ -1990,7 +2280,8 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1990
2280
  "kilo-code",
1991
2281
  "github-copilot",
1992
2282
  "continue-dev",
1993
- "goose"
2283
+ "goose",
2284
+ "opencode"
1994
2285
  ]);
1995
2286
  const usesInlineKey = hostedInlinePlatforms.has(platform);
1996
2287
  const authHeader = usesInlineKey ? `Bearer ${apiKey}` : "Bearer YOUR_SHIELD_API_KEY";
@@ -1999,14 +2290,12 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1999
2290
  if (platform === "github-copilot") {
2000
2291
  snippetText = JSON.stringify(
2001
2292
  {
2002
- mcp: {
2003
- servers: {
2004
- [shortName]: {
2005
- type: "http",
2006
- url: urlInSnippet,
2007
- headers: {
2008
- Authorization: authHeader
2009
- }
2293
+ servers: {
2294
+ [shortName]: {
2295
+ type: "http",
2296
+ url: urlInSnippet,
2297
+ headers: {
2298
+ Authorization: authHeader
2010
2299
  }
2011
2300
  }
2012
2301
  }
@@ -2045,18 +2334,50 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
2045
2334
  2
2046
2335
  );
2047
2336
  } else if (platform === "continue-dev") {
2337
+ const sn = sanitiseYamlValue(shortName);
2338
+ const urlEsc = sanitiseYamlValue(urlInSnippet);
2339
+ const authEsc = sanitiseYamlValue(authHeader);
2340
+ snippetText = `name: ${sn}
2341
+ version: 0.0.1
2342
+ schema: v1
2343
+ mcpServers:
2344
+ - name: ${sn}
2345
+ type: streamable-http
2346
+ url: ${urlEsc}
2347
+ headers:
2348
+ Authorization: ${authEsc}
2349
+ `;
2350
+ } else if (platform === "kilo-code") {
2048
2351
  snippetText = JSON.stringify(
2049
2352
  {
2050
- mcpServers: [
2051
- {
2052
- name: shortName,
2053
- type: "streamable-http",
2353
+ mcp: {
2354
+ [shortName]: {
2355
+ type: "remote",
2054
2356
  url: urlInSnippet,
2055
2357
  headers: {
2056
2358
  Authorization: authHeader
2057
- }
2359
+ },
2360
+ enabled: true
2361
+ }
2362
+ }
2363
+ },
2364
+ null,
2365
+ 2
2366
+ );
2367
+ } else if (platform === "opencode") {
2368
+ snippetText = JSON.stringify(
2369
+ {
2370
+ $schema: OPENCODE_CONFIG_SCHEMA_URL,
2371
+ mcp: {
2372
+ [shortName]: {
2373
+ type: "remote",
2374
+ url: urlInSnippet,
2375
+ headers: {
2376
+ Authorization: authHeader
2377
+ },
2378
+ enabled: true
2058
2379
  }
2059
- ]
2380
+ }
2060
2381
  },
2061
2382
  null,
2062
2383
  2
@@ -2096,16 +2417,24 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
2096
2417
  );
2097
2418
  } else if (platform === "kilo-code") {
2098
2419
  process.stderr.write(
2099
- "\n" + style.dim(`Add this to ${join(resolve(process.cwd()), ".kilocode", "mcp.json")}:`) + "\n\n"
2420
+ "\n" + style.dim(`Add this to ${join(resolve(process.cwd()), ".kilo", "kilo.jsonc")}:`) + "\n\n"
2421
+ );
2422
+ } else if (platform === "opencode") {
2423
+ process.stderr.write(
2424
+ "\n" + style.dim(
2425
+ "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."
2426
+ ) + "\n\n"
2100
2427
  );
2101
2428
  } else if (platform === "github-copilot") {
2102
2429
  process.stderr.write(
2103
2430
  "\n" + style.dim(
2104
- "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."
2431
+ "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."
2105
2432
  ) + "\n\n"
2106
2433
  );
2107
2434
  } else if (platform === "continue-dev") {
2108
- process.stderr.write("\n" + style.dim(`Add this to ${getContinueConfigJsonPath()}:`) + "\n\n");
2435
+ process.stderr.write(
2436
+ "\n" + style.dim(`Save this as .continue/mcpServers/${shortName}.yaml in your workspace root.`) + "\n\n"
2437
+ );
2109
2438
  } else if (platform === "goose") {
2110
2439
  process.stderr.write(
2111
2440
  "\n" + style.dim("Add this to ~/.config/goose/config.yaml under the extensions key.") + "\n\n"
@@ -2159,6 +2488,11 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
2159
2488
  if (platform === "goose") {
2160
2489
  process.stderr.write(style.dim("Start a new Goose session after updating config.") + "\n");
2161
2490
  }
2491
+ if (platform === "opencode") {
2492
+ process.stderr.write(
2493
+ style.dim("Restart OpenCode or start a new session after saving opencode.json.") + "\n"
2494
+ );
2495
+ }
2162
2496
  }
2163
2497
  function agentDisplayNameDedupeKey(name) {
2164
2498
  return name.trim().normalize("NFKC").toLowerCase();
@@ -2282,7 +2616,7 @@ async function runInit(explicitBaseUrl, options) {
2282
2616
  }
2283
2617
  await warnIfInstalledShieldIsOutdated();
2284
2618
  const configuredAgents = [];
2285
- let currentAgents = collectAgentsFromConfig(existing);
2619
+ let currentAgents = mergeAgentsForUniqueNames(collectAgentsFromConfig(existing));
2286
2620
  let lastConfig = {
2287
2621
  apiKey,
2288
2622
  baseUrl: resolvedBaseUrl,
@@ -2719,6 +3053,95 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2719
3053
  setupSucceeded = true;
2720
3054
  }
2721
3055
  }
3056
+ } else if (selectedPlatform === "opencode") {
3057
+ const opencodeMode = await promptOpencodeIntegrationMode(ask);
3058
+ if (opencodeMode === "native") {
3059
+ try {
3060
+ await installOpenCodeNativePlugin();
3061
+ process.stderr.write("\n" + style.bold("Shield OpenCode plugin installed") + "\n\n");
3062
+ process.stderr.write(
3063
+ style.dim("Plugin file: ") + style.cyan(getOpenCodeGlobalPluginsDir()) + "\n"
3064
+ );
3065
+ process.stderr.write("\n");
3066
+ process.stderr.write(
3067
+ style.dim(
3068
+ "Shield plugin saved under ~/.config/opencode/plugins/. Restart OpenCode. Every tool call from the primary agent will be checked by Shield."
3069
+ ) + "\n"
3070
+ );
3071
+ process.stderr.write("\n");
3072
+ process.stderr.write(
3073
+ style.dim(
3074
+ "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."
3075
+ ) + "\n"
3076
+ );
3077
+ configuredAgents.push({
3078
+ selection,
3079
+ platform: selectedPlatform,
3080
+ platformLabel: selectedLabel,
3081
+ agentName,
3082
+ opencodeCliIntegration: "native"
3083
+ });
3084
+ setupSucceeded = true;
3085
+ } catch (error) {
3086
+ const detail = error instanceof Error ? error.message : String(error);
3087
+ process.stderr.write(style.red("\u2717 ") + detail + "\n");
3088
+ }
3089
+ } else {
3090
+ const { targetUrl, shortName, upstreamHeaders } = await promptProxyConfig(ask, agentName);
3091
+ let proxyUrl = "";
3092
+ let created = false;
3093
+ while (!created) {
3094
+ const spinner = withSpinner("Creating proxy config...");
3095
+ try {
3096
+ proxyUrl = await createProxyConfig(
3097
+ resolvedBaseUrl,
3098
+ apiKey,
3099
+ agentName,
3100
+ targetUrl,
3101
+ shortName,
3102
+ selectedPlatform,
3103
+ upstreamHeaders
3104
+ );
3105
+ spinner.stop(true, "Proxy config created!");
3106
+ created = true;
3107
+ } catch (error) {
3108
+ const detail = error instanceof Error ? error.message : String(error);
3109
+ spinner.stop(false, detail);
3110
+ const retry = await ask("Try again? (Y/n) ");
3111
+ if (retry.trim().toLowerCase() === "n") {
3112
+ break;
3113
+ }
3114
+ }
3115
+ }
3116
+ if (created && proxyUrl.length > 0) {
3117
+ process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
3118
+ process.stderr.write(
3119
+ " " + style.cyan(formatHostedProxyUrlForStderr(selectedPlatform, proxyUrl, apiKey)) + "\n"
3120
+ );
3121
+ await applyHostedProxyMcpConfig(
3122
+ selectedPlatform,
3123
+ proxyUrl,
3124
+ shortName,
3125
+ apiKey,
3126
+ initWorkspacePath
3127
+ );
3128
+ process.stderr.write(
3129
+ "\n" + style.dim(
3130
+ "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."
3131
+ ) + "\n"
3132
+ );
3133
+ configuredAgents.push({
3134
+ selection,
3135
+ platform: selectedPlatform,
3136
+ platformLabel: selectedLabel,
3137
+ agentName,
3138
+ shortName,
3139
+ proxyUrl,
3140
+ opencodeCliIntegration: "hosted"
3141
+ });
3142
+ setupSucceeded = true;
3143
+ }
3144
+ }
2722
3145
  } else if (selectedPlatform === "cline") {
2723
3146
  const clineMode = await promptClineIntegrationMode(ask);
2724
3147
  if (clineMode === "native") {
@@ -2943,23 +3366,27 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2943
3366
  );
2944
3367
  }
2945
3368
  if (configuredPlatforms.has("kilo-code")) {
3369
+ const kiloLabel = mcpPromptLabel2("kilo-code");
2946
3370
  blocks.push(
2947
- "\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"
3371
+ "\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'
2948
3372
  );
2949
3373
  }
2950
3374
  if (configuredPlatforms.has("github-copilot")) {
3375
+ const copilotLabel = mcpPromptLabel2("github-copilot");
2951
3376
  blocks.push(
2952
- "\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"
3377
+ "\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'
2953
3378
  );
2954
3379
  }
2955
3380
  if (configuredPlatforms.has("continue-dev")) {
3381
+ const continueLabel = mcpPromptLabel2("continue-dev");
2956
3382
  blocks.push(
2957
- "\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"
3383
+ "\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'
2958
3384
  );
2959
3385
  }
2960
3386
  if (configuredPlatforms.has("goose")) {
3387
+ const gooseLabel = mcpPromptLabel2("goose");
2961
3388
  blocks.push(
2962
- "\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"
3389
+ "\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'
2963
3390
  );
2964
3391
  }
2965
3392
  const windsurfNativeConfigured = configuredAgents.some(
@@ -3010,6 +3437,23 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3010
3437
  "\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"
3011
3438
  );
3012
3439
  }
3440
+ const opencodeNativeConfigured = configuredAgents.some(
3441
+ (a) => a.platform === "opencode" && a.opencodeCliIntegration === "native"
3442
+ );
3443
+ const opencodeHostedConfigured = configuredAgents.some(
3444
+ (a) => a.platform === "opencode" && a.opencodeCliIntegration === "hosted"
3445
+ );
3446
+ if (opencodeNativeConfigured) {
3447
+ blocks.push(
3448
+ "\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"
3449
+ );
3450
+ }
3451
+ if (opencodeHostedConfigured) {
3452
+ const ocLabel = mcpPromptLabel2("opencode");
3453
+ blocks.push(
3454
+ "\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'
3455
+ );
3456
+ }
3013
3457
  if (configuredPlatforms.has("other-mcp")) {
3014
3458
  blocks.push(
3015
3459
  "\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"
@@ -3915,7 +4359,7 @@ async function restoreClaudeDesktopMcpFromBackup() {
3915
4359
 
3916
4360
  // package.json
3917
4361
  var package_default = {
3918
- version: "1.6.0"};
4362
+ version: "1.8.0"};
3919
4363
 
3920
4364
  // src/package-meta.ts
3921
4365
  var PACKAGE_VERSION = package_default.version;