multicorn-shield 1.7.0 → 1.9.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.
@@ -939,6 +939,115 @@ require(${JSON.stringify(destPost)});
939
939
  await writeFile(preWrapper, preContent, { encoding: "utf8", mode: 493 });
940
940
  await writeFile(postWrapper, postContent, { encoding: "utf8", mode: 493 });
941
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
+ }
958
+ function getCodexCliHooksInstallDir() {
959
+ return join(homedir(), ".multicorn", "codex-cli-hooks");
960
+ }
961
+ function getCodexConfigTomlPath() {
962
+ return join(homedir(), ".codex", "config.toml");
963
+ }
964
+ function getCodexHooksJsonPath() {
965
+ return join(homedir(), ".codex", "hooks.json");
966
+ }
967
+ async function installCodexCliNativeHooks() {
968
+ const root = multicornShieldPackageRoot();
969
+ const scriptsDir = join(root, "plugins", "codex-cli", "hooks", "scripts");
970
+ const preHook = join(scriptsDir, "pre-tool-use.cjs");
971
+ const postHook = join(scriptsDir, "post-tool-use.cjs");
972
+ const toolMap = join(scriptsDir, "codex-cli-tool-map.cjs");
973
+ const sharedHook = join(scriptsDir, "codex-cli-hooks-shared.cjs");
974
+ if (!existsSync(preHook) || !existsSync(postHook) || !existsSync(toolMap) || !existsSync(sharedHook)) {
975
+ throw new Error(
976
+ `Could not find Shield Codex CLI hook scripts at ${scriptsDir}. If you use npm, install the latest multicorn-shield package.`
977
+ );
978
+ }
979
+ const destDir = getCodexCliHooksInstallDir();
980
+ await mkdir(destDir, { recursive: true });
981
+ await copyFile(preHook, join(destDir, "pre-tool-use.cjs"));
982
+ await copyFile(postHook, join(destDir, "post-tool-use.cjs"));
983
+ await copyFile(toolMap, join(destDir, "codex-cli-tool-map.cjs"));
984
+ await copyFile(sharedHook, join(destDir, "codex-cli-hooks-shared.cjs"));
985
+ const configTomlPath = getCodexConfigTomlPath();
986
+ await mkdir(join(homedir(), ".codex"), { recursive: true });
987
+ let tomlContent = "";
988
+ try {
989
+ tomlContent = await readFile(configTomlPath, "utf8");
990
+ } catch (err) {
991
+ if (!isErrnoException(err) || err.code !== "ENOENT") {
992
+ throw err;
993
+ }
994
+ }
995
+ if (tomlContent.includes("codex_hooks")) {
996
+ tomlContent = tomlContent.replace(/codex_hooks/g, "hooks");
997
+ await writeFile(configTomlPath, tomlContent, "utf8");
998
+ }
999
+ if (!/\bhooks\s*=\s*true/.test(tomlContent)) {
1000
+ tomlContent = tomlContent.includes("[features]") ? tomlContent.replace("[features]", "[features]\nhooks = true") : tomlContent.trimEnd() + (tomlContent.length > 0 ? "\n\n" : "") + "[features]\nhooks = true\n";
1001
+ await writeFile(configTomlPath, tomlContent, "utf8");
1002
+ }
1003
+ const hooksConfig = {
1004
+ hooks: {
1005
+ PreToolUse: [
1006
+ {
1007
+ matcher: "",
1008
+ hooks: [
1009
+ {
1010
+ type: "command",
1011
+ command: `node ${join(destDir, "pre-tool-use.cjs")}`,
1012
+ statusMessage: "Checking Shield permissions"
1013
+ }
1014
+ ]
1015
+ }
1016
+ ],
1017
+ PostToolUse: [
1018
+ {
1019
+ matcher: "",
1020
+ hooks: [
1021
+ {
1022
+ type: "command",
1023
+ command: `node ${join(destDir, "post-tool-use.cjs")}`,
1024
+ timeout: 30,
1025
+ statusMessage: "Logging to Shield"
1026
+ }
1027
+ ]
1028
+ }
1029
+ ]
1030
+ }
1031
+ };
1032
+ await writeFile(getCodexHooksJsonPath(), JSON.stringify(hooksConfig, null, 2) + "\n", "utf8");
1033
+ }
1034
+ async function promptCodexCliIntegrationMode(ask) {
1035
+ process.stderr.write("\n" + style.bold("Codex CLI integration") + "\n");
1036
+ process.stderr.write(
1037
+ " " + style.violet("1") + ". Native plugin - Shield checks terminal (Bash) commands via Codex Hooks\n"
1038
+ );
1039
+ process.stderr.write(
1040
+ " " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via config.toml\n"
1041
+ );
1042
+ let choice = 0;
1043
+ while (choice === 0) {
1044
+ const input = await ask("Choose integration (1-2): ");
1045
+ const num = parseInt(input.trim(), 10);
1046
+ if (num === 1) choice = 1;
1047
+ if (num === 2) choice = 2;
1048
+ }
1049
+ return choice === 1 ? "native" : "hosted";
1050
+ }
942
1051
  async function promptClineIntegrationMode(ask) {
943
1052
  process.stderr.write("\n" + style.bold("Cline integration") + "\n");
944
1053
  process.stderr.write(
@@ -1298,6 +1407,23 @@ async function promptGeminiCliIntegrationMode(ask) {
1298
1407
  }
1299
1408
  return choice === 1 ? "native" : "hosted";
1300
1409
  }
1410
+ async function promptOpencodeIntegrationMode(ask) {
1411
+ process.stderr.write("\n" + style.bold("OpenCode integration") + "\n");
1412
+ process.stderr.write(
1413
+ " " + style.violet("1") + ". Native plugin (recommended) - Shield checks primary-agent tool execution via OpenCode Hooks\n"
1414
+ );
1415
+ process.stderr.write(
1416
+ " " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via opencode.json (full subagent coverage when tools use MCP through Shield)\n"
1417
+ );
1418
+ let choice = 0;
1419
+ while (choice === 0) {
1420
+ const input = await ask("Choose integration (1-2): ");
1421
+ const num = parseInt(input.trim(), 10);
1422
+ if (num === 1) choice = 1;
1423
+ if (num === 2) choice = 2;
1424
+ }
1425
+ return choice === 1 ? "native" : "hosted";
1426
+ }
1301
1427
  function getClaudeDesktopConfigPath() {
1302
1428
  switch (process.platform) {
1303
1429
  case "win32":
@@ -1367,6 +1493,18 @@ var INIT_WIZARD_PLATFORM_REGISTRY = [
1367
1493
  { slug: "windsurf", displayName: "Windsurf", section: "native" },
1368
1494
  { slug: "cline", displayName: "Cline", section: "native" },
1369
1495
  { slug: "gemini-cli", displayName: "Gemini CLI", section: "native" },
1496
+ {
1497
+ slug: "opencode",
1498
+ displayName: "OpenCode",
1499
+ section: "native",
1500
+ prereqUrl: "https://opencode.ai"
1501
+ },
1502
+ {
1503
+ slug: "codex-cli",
1504
+ displayName: "Codex CLI",
1505
+ section: "native",
1506
+ prereqUrl: "https://github.com/openai/codex"
1507
+ },
1370
1508
  {
1371
1509
  slug: "cursor",
1372
1510
  displayName: "Cursor",
@@ -1908,6 +2046,50 @@ async function mergeKiloCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKe
1908
2046
  }
1909
2047
  return "ok";
1910
2048
  }
2049
+ var OPENCODE_CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
2050
+ async function injectOpencodeSchemaIntoConfigIfMissing(filePath) {
2051
+ try {
2052
+ const raw = await readFile(filePath, "utf8");
2053
+ const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
2054
+ let parsed;
2055
+ try {
2056
+ parsed = JSON.parse(stripped);
2057
+ } catch {
2058
+ return;
2059
+ }
2060
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return;
2061
+ const root = parsed;
2062
+ const existingSchema = root["$schema"];
2063
+ if (typeof existingSchema === "string" && existingSchema.length > 0) return;
2064
+ root["$schema"] = OPENCODE_CONFIG_SCHEMA_URL;
2065
+ await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
2066
+ } catch {
2067
+ }
2068
+ }
2069
+ async function mergeOpenCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKey) {
2070
+ const filePath = join(workspacePath, "opencode.json");
2071
+ const result = await mergeTopLevelKeyedJsonFile(
2072
+ filePath,
2073
+ "mcp",
2074
+ shortName,
2075
+ {
2076
+ type: "remote",
2077
+ url: proxyUrl,
2078
+ headers: { Authorization: `Bearer ${apiKey}` },
2079
+ enabled: true
2080
+ },
2081
+ {
2082
+ stripJsonComments: true,
2083
+ onExisting: "skip"
2084
+ }
2085
+ );
2086
+ if (result === "parse-error") return "parse-error";
2087
+ await injectOpencodeSchemaIntoConfigIfMissing(filePath);
2088
+ if (result === "ok") {
2089
+ await warnIfApiKeyFileNotGitignored(workspacePath, "opencode.json");
2090
+ }
2091
+ return "ok";
2092
+ }
1911
2093
  function printHostedProxyJsonParseWarning(filePath) {
1912
2094
  process.stderr.write(
1913
2095
  style.yellow("\u26A0") + " Could not parse JSON at " + style.cyan(filePath) + style.dim(" - showing paste snippet instead.") + "\n"
@@ -1950,6 +2132,13 @@ function printHostedProxyPostWriteHints(platform, shortName) {
1950
2132
  style.dim("Restart Kilo Code or reload the window so it picks up .kilo/kilo.jsonc.") + "\n"
1951
2133
  );
1952
2134
  }
2135
+ if (platform === "opencode") {
2136
+ process.stderr.write(
2137
+ style.dim(
2138
+ "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."
2139
+ ) + "\n"
2140
+ );
2141
+ }
1953
2142
  }
1954
2143
  async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey, workspacePath) {
1955
2144
  const authHeader = `Bearer ${apiKey}`;
@@ -2046,6 +2235,19 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
2046
2235
  if (result === "parse-error") {
2047
2236
  printHostedProxyJsonParseWarning(join(workspacePath, ".kilo", "kilo.jsonc"));
2048
2237
  }
2238
+ } else if (platform === "opencode") {
2239
+ result = await mergeOpenCodeProjectMcp(
2240
+ workspacePath,
2241
+ shortName,
2242
+ proxyUrlWithKeyWhenNeeded,
2243
+ apiKey
2244
+ );
2245
+ if (result === "parse-error") {
2246
+ printHostedProxyJsonParseWarning(join(workspacePath, "opencode.json"));
2247
+ }
2248
+ } else if (platform === "codex-cli") {
2249
+ printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
2250
+ return;
2049
2251
  } else if (platform === "continue-dev") {
2050
2252
  result = await mergeContinueHostedMcp(
2051
2253
  workspacePath,
@@ -2180,7 +2382,9 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
2180
2382
  "kilo-code",
2181
2383
  "github-copilot",
2182
2384
  "continue-dev",
2183
- "goose"
2385
+ "goose",
2386
+ "opencode",
2387
+ "codex-cli"
2184
2388
  ]);
2185
2389
  const usesInlineKey = hostedInlinePlatforms.has(platform);
2186
2390
  const authHeader = usesInlineKey ? `Bearer ${apiKey}` : "Bearer YOUR_SHIELD_API_KEY";
@@ -2263,6 +2467,29 @@ mcpServers:
2263
2467
  null,
2264
2468
  2
2265
2469
  );
2470
+ } else if (platform === "opencode") {
2471
+ snippetText = JSON.stringify(
2472
+ {
2473
+ $schema: OPENCODE_CONFIG_SCHEMA_URL,
2474
+ mcp: {
2475
+ [shortName]: {
2476
+ type: "remote",
2477
+ url: urlInSnippet,
2478
+ headers: {
2479
+ Authorization: authHeader
2480
+ },
2481
+ enabled: true
2482
+ }
2483
+ }
2484
+ },
2485
+ null,
2486
+ 2
2487
+ );
2488
+ } else if (platform === "codex-cli") {
2489
+ snippetText = `[mcp_servers.${shortName}]
2490
+ url = "${urlInSnippet}"
2491
+ bearer_token_env_var = "MULTICORN_API_KEY"
2492
+ `;
2266
2493
  } else {
2267
2494
  const urlKey = platform === "windsurf" ? "serverUrl" : "url";
2268
2495
  snippetText = JSON.stringify(
@@ -2300,6 +2527,18 @@ mcpServers:
2300
2527
  process.stderr.write(
2301
2528
  "\n" + style.dim(`Add this to ${join(resolve(process.cwd()), ".kilo", "kilo.jsonc")}:`) + "\n\n"
2302
2529
  );
2530
+ } else if (platform === "opencode") {
2531
+ process.stderr.write(
2532
+ "\n" + style.dim(
2533
+ "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."
2534
+ ) + "\n\n"
2535
+ );
2536
+ } else if (platform === "codex-cli") {
2537
+ process.stderr.write(
2538
+ "\n" + style.dim(
2539
+ "Add this to ~/.codex/config.toml (create the file if it does not exist). Set the MULTICORN_API_KEY environment variable to your Shield API key. Restart Codex CLI after saving."
2540
+ ) + "\n\n"
2541
+ );
2303
2542
  } else if (platform === "github-copilot") {
2304
2543
  process.stderr.write(
2305
2544
  "\n" + style.dim(
@@ -2363,6 +2602,11 @@ mcpServers:
2363
2602
  if (platform === "goose") {
2364
2603
  process.stderr.write(style.dim("Start a new Goose session after updating config.") + "\n");
2365
2604
  }
2605
+ if (platform === "opencode") {
2606
+ process.stderr.write(
2607
+ style.dim("Restart OpenCode or start a new session after saving opencode.json.") + "\n"
2608
+ );
2609
+ }
2366
2610
  }
2367
2611
  function agentDisplayNameDedupeKey(name) {
2368
2612
  return name.trim().normalize("NFKC").toLowerCase();
@@ -2923,6 +3167,189 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2923
3167
  setupSucceeded = true;
2924
3168
  }
2925
3169
  }
3170
+ } else if (selectedPlatform === "opencode") {
3171
+ const opencodeMode = await promptOpencodeIntegrationMode(ask);
3172
+ if (opencodeMode === "native") {
3173
+ try {
3174
+ await installOpenCodeNativePlugin();
3175
+ process.stderr.write("\n" + style.bold("Shield OpenCode plugin installed") + "\n\n");
3176
+ process.stderr.write(
3177
+ style.dim("Plugin file: ") + style.cyan(getOpenCodeGlobalPluginsDir()) + "\n"
3178
+ );
3179
+ process.stderr.write("\n");
3180
+ process.stderr.write(
3181
+ style.dim(
3182
+ "Shield plugin saved under ~/.config/opencode/plugins/. Restart OpenCode. Every tool call from the primary agent will be checked by Shield."
3183
+ ) + "\n"
3184
+ );
3185
+ process.stderr.write("\n");
3186
+ process.stderr.write(
3187
+ style.dim(
3188
+ "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."
3189
+ ) + "\n"
3190
+ );
3191
+ configuredAgents.push({
3192
+ selection,
3193
+ platform: selectedPlatform,
3194
+ platformLabel: selectedLabel,
3195
+ agentName,
3196
+ opencodeCliIntegration: "native"
3197
+ });
3198
+ setupSucceeded = true;
3199
+ } catch (error) {
3200
+ const detail = error instanceof Error ? error.message : String(error);
3201
+ process.stderr.write(style.red("\u2717 ") + detail + "\n");
3202
+ }
3203
+ } else {
3204
+ const { targetUrl, shortName, upstreamHeaders } = await promptProxyConfig(ask, agentName);
3205
+ let proxyUrl = "";
3206
+ let created = false;
3207
+ while (!created) {
3208
+ const spinner = withSpinner("Creating proxy config...");
3209
+ try {
3210
+ proxyUrl = await createProxyConfig(
3211
+ resolvedBaseUrl,
3212
+ apiKey,
3213
+ agentName,
3214
+ targetUrl,
3215
+ shortName,
3216
+ selectedPlatform,
3217
+ upstreamHeaders
3218
+ );
3219
+ spinner.stop(true, "Proxy config created!");
3220
+ created = true;
3221
+ } catch (error) {
3222
+ const detail = error instanceof Error ? error.message : String(error);
3223
+ spinner.stop(false, detail);
3224
+ const retry = await ask("Try again? (Y/n) ");
3225
+ if (retry.trim().toLowerCase() === "n") {
3226
+ break;
3227
+ }
3228
+ }
3229
+ }
3230
+ if (created && proxyUrl.length > 0) {
3231
+ process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
3232
+ process.stderr.write(
3233
+ " " + style.cyan(formatHostedProxyUrlForStderr(selectedPlatform, proxyUrl, apiKey)) + "\n"
3234
+ );
3235
+ await applyHostedProxyMcpConfig(
3236
+ selectedPlatform,
3237
+ proxyUrl,
3238
+ shortName,
3239
+ apiKey,
3240
+ initWorkspacePath
3241
+ );
3242
+ process.stderr.write(
3243
+ "\n" + style.dim(
3244
+ "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."
3245
+ ) + "\n"
3246
+ );
3247
+ configuredAgents.push({
3248
+ selection,
3249
+ platform: selectedPlatform,
3250
+ platformLabel: selectedLabel,
3251
+ agentName,
3252
+ shortName,
3253
+ proxyUrl,
3254
+ opencodeCliIntegration: "hosted"
3255
+ });
3256
+ setupSucceeded = true;
3257
+ }
3258
+ }
3259
+ } else if (selectedPlatform === "codex-cli") {
3260
+ const codexMode = await promptCodexCliIntegrationMode(ask);
3261
+ if (codexMode === "native") {
3262
+ try {
3263
+ await installCodexCliNativeHooks();
3264
+ process.stderr.write("\n" + style.bold("Shield Codex CLI hooks installed") + "\n\n");
3265
+ process.stderr.write(
3266
+ style.dim("Hook scripts: ") + style.cyan(getCodexCliHooksInstallDir()) + "\n"
3267
+ );
3268
+ process.stderr.write(
3269
+ style.dim("Hooks config: ") + style.cyan(getCodexHooksJsonPath()) + "\n"
3270
+ );
3271
+ process.stderr.write(
3272
+ style.dim("Feature flag: ") + style.cyan(getCodexConfigTomlPath()) + "\n"
3273
+ );
3274
+ process.stderr.write("\n");
3275
+ process.stderr.write(
3276
+ style.dim(
3277
+ "Codex hooks currently intercept terminal (Bash) commands only. File edits and MCP tool calls are not yet covered by hooks."
3278
+ ) + "\n\n"
3279
+ );
3280
+ process.stderr.write(
3281
+ style.dim(
3282
+ "Start Codex CLI, then type /hooks to review the Shield hooks. Press 't' to trust each one before they can run."
3283
+ ) + "\n"
3284
+ );
3285
+ configuredAgents.push({
3286
+ selection,
3287
+ platform: selectedPlatform,
3288
+ platformLabel: selectedLabel,
3289
+ agentName,
3290
+ codexCliIntegration: "native"
3291
+ });
3292
+ setupSucceeded = true;
3293
+ } catch (error) {
3294
+ const detail = error instanceof Error ? error.message : String(error);
3295
+ process.stderr.write(style.red("\u2717 ") + detail + "\n");
3296
+ }
3297
+ } else {
3298
+ const { targetUrl, shortName, upstreamHeaders } = await promptProxyConfig(ask, agentName);
3299
+ let proxyUrl = "";
3300
+ let created = false;
3301
+ while (!created) {
3302
+ const spinner = withSpinner("Creating proxy config...");
3303
+ try {
3304
+ proxyUrl = await createProxyConfig(
3305
+ resolvedBaseUrl,
3306
+ apiKey,
3307
+ agentName,
3308
+ targetUrl,
3309
+ shortName,
3310
+ selectedPlatform,
3311
+ upstreamHeaders
3312
+ );
3313
+ spinner.stop(true, "Proxy config created!");
3314
+ created = true;
3315
+ } catch (error) {
3316
+ const detail = error instanceof Error ? error.message : String(error);
3317
+ spinner.stop(false, detail);
3318
+ const retry = await ask("Try again? (Y/n) ");
3319
+ if (retry.trim().toLowerCase() === "n") {
3320
+ break;
3321
+ }
3322
+ }
3323
+ }
3324
+ if (created && proxyUrl.length > 0) {
3325
+ process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
3326
+ process.stderr.write(
3327
+ " " + style.cyan(formatHostedProxyUrlForStderr(selectedPlatform, proxyUrl, apiKey)) + "\n"
3328
+ );
3329
+ await applyHostedProxyMcpConfig(
3330
+ selectedPlatform,
3331
+ proxyUrl,
3332
+ shortName,
3333
+ apiKey,
3334
+ initWorkspacePath
3335
+ );
3336
+ process.stderr.write(
3337
+ "\n" + style.dim(
3338
+ "Add the TOML snippet above to ~/.codex/config.toml. Set the MULTICORN_API_KEY environment variable to your Shield API key. Restart Codex CLI after saving."
3339
+ ) + "\n"
3340
+ );
3341
+ configuredAgents.push({
3342
+ selection,
3343
+ platform: selectedPlatform,
3344
+ platformLabel: selectedLabel,
3345
+ agentName,
3346
+ shortName,
3347
+ proxyUrl,
3348
+ codexCliIntegration: "hosted"
3349
+ });
3350
+ setupSucceeded = true;
3351
+ }
3352
+ }
2926
3353
  } else if (selectedPlatform === "cline") {
2927
3354
  const clineMode = await promptClineIntegrationMode(ask);
2928
3355
  if (clineMode === "native") {
@@ -3218,6 +3645,39 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3218
3645
  "\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"
3219
3646
  );
3220
3647
  }
3648
+ const opencodeNativeConfigured = configuredAgents.some(
3649
+ (a) => a.platform === "opencode" && a.opencodeCliIntegration === "native"
3650
+ );
3651
+ const opencodeHostedConfigured = configuredAgents.some(
3652
+ (a) => a.platform === "opencode" && a.opencodeCliIntegration === "hosted"
3653
+ );
3654
+ if (opencodeNativeConfigured) {
3655
+ blocks.push(
3656
+ "\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"
3657
+ );
3658
+ }
3659
+ if (opencodeHostedConfigured) {
3660
+ const ocLabel = mcpPromptLabel2("opencode");
3661
+ blocks.push(
3662
+ "\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'
3663
+ );
3664
+ }
3665
+ const codexNativeConfigured = configuredAgents.some(
3666
+ (a) => a.platform === "codex-cli" && a.codexCliIntegration === "native"
3667
+ );
3668
+ const codexHostedConfigured = configuredAgents.some(
3669
+ (a) => a.platform === "codex-cli" && a.codexCliIntegration === "hosted"
3670
+ );
3671
+ if (codexNativeConfigured) {
3672
+ blocks.push(
3673
+ "\n" + style.bold("Codex CLI (native)") + "\n \u2192 Start Codex CLI (run 'codex' in your terminal)\n \u2192 Type /hooks to review the Shield hooks - press 't' to trust each one\n \u2192 Once both hooks are trusted, try a Bash command - Shield will intercept and check permissions\n \u2192 Note: hooks currently cover terminal (Bash) commands only\n"
3674
+ );
3675
+ }
3676
+ if (codexHostedConfigured) {
3677
+ blocks.push(
3678
+ "\n" + style.bold("Codex CLI (hosted)") + "\n \u2192 Set the MULTICORN_API_KEY environment variable to your Shield API key\n \u2192 Restart Codex CLI after saving config.toml\n \u2192 Try it: make a request that uses an MCP tool through Shield\n"
3679
+ );
3680
+ }
3221
3681
  if (configuredPlatforms.has("other-mcp")) {
3222
3682
  blocks.push(
3223
3683
  "\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"
@@ -4123,7 +4583,7 @@ async function restoreClaudeDesktopMcpFromBackup() {
4123
4583
 
4124
4584
  // package.json
4125
4585
  var package_default = {
4126
- version: "1.7.0"};
4586
+ version: "1.9.0"};
4127
4587
 
4128
4588
  // src/package-meta.ts
4129
4589
  var PACKAGE_VERSION = package_default.version;
@@ -22354,6 +22354,18 @@ var INIT_WIZARD_PLATFORM_REGISTRY = [
22354
22354
  { slug: "windsurf", displayName: "Windsurf", section: "native" },
22355
22355
  { slug: "cline", displayName: "Cline", section: "native" },
22356
22356
  { slug: "gemini-cli", displayName: "Gemini CLI", section: "native" },
22357
+ {
22358
+ slug: "opencode",
22359
+ displayName: "OpenCode",
22360
+ section: "native",
22361
+ prereqUrl: "https://opencode.ai"
22362
+ },
22363
+ {
22364
+ slug: "codex-cli",
22365
+ displayName: "Codex CLI",
22366
+ section: "native",
22367
+ prereqUrl: "https://github.com/openai/codex"
22368
+ },
22357
22369
  {
22358
22370
  slug: "cursor",
22359
22371
  displayName: "Cursor",
@@ -22505,7 +22517,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
22505
22517
 
22506
22518
  // package.json
22507
22519
  var package_default = {
22508
- version: "1.7.0"};
22520
+ version: "1.9.0"};
22509
22521
 
22510
22522
  // src/package-meta.ts
22511
22523
  var PACKAGE_VERSION = package_default.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multicorn-shield",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
5
5
  "license": "MIT",
6
6
  "author": "Multicorn AI Pty Ltd",
@@ -40,6 +40,7 @@
40
40
  "plugins/windsurf",
41
41
  "plugins/cline",
42
42
  "plugins/gemini-cli",
43
+ "plugins/opencode",
43
44
  "LICENSE",
44
45
  "README.md",
45
46
  "CHANGELOG.md"
@@ -76,6 +77,7 @@
76
77
  "@anthropic-ai/mcpb": "^2.1.2",
77
78
  "@eslint/js": "^9.19.0",
78
79
  "@open-wc/testing-helpers": "^3.0.1",
80
+ "@opencode-ai/plugin": "^1.14.48",
79
81
  "@size-limit/file": "^11.1.6",
80
82
  "@types/node": "^22.0.0",
81
83
  "@vitest/coverage-v8": "^3.0.5",