@node9/proxy 1.24.2 → 1.25.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.
package/dist/cli.js CHANGED
@@ -6463,6 +6463,122 @@ var init_mcp_pin = __esm({
6463
6463
  }
6464
6464
  });
6465
6465
 
6466
+ // src/setup-opencode-shim.ts
6467
+ function renderOpencodeShim(input) {
6468
+ const { node9Argv, version: version2 } = input;
6469
+ return `// Auto-generated by \`node9 init\`. Do not edit \u2014 re-run init to upgrade.
6470
+ // NODE9_SHIM_VERSION = "${version2}"
6471
+ //
6472
+ // node9 protection shim for Opencode. Wires three hooks against the
6473
+ // agent's plugin API and shells out to the node9 CLI for verdicts.
6474
+ //
6475
+ // Block by throwing from a hook handler; Opencode's plugin trigger
6476
+ // (packages/opencode/src/plugin/index.ts:273) propagates the error and
6477
+ // halts the tool call.
6478
+
6479
+ const { spawnSync } = require("node:child_process");
6480
+
6481
+ // argv prefix for invoking the node9 CLI. argv[0] is the executable
6482
+ // (either the npm-installed wrapper script or the node binary); the
6483
+ // remaining entries (if any) are passed as the leading args before
6484
+ // the subcommand name (e.g. ["/path/to/dist/cli.js"] for dev mode).
6485
+ const NODE9_ARGV = ${JSON.stringify(node9Argv)};
6486
+ const HOOK_TIMEOUT_MS = 30000;
6487
+ const LOG_TIMEOUT_MS = 5000;
6488
+
6489
+ function parseReason(stdout) {
6490
+ // node9 check emits {decision, reason, message} JSON on stdout when
6491
+ // blocking. Fall back to a generic string if anything goes wrong.
6492
+ try {
6493
+ const v = JSON.parse(stdout || "");
6494
+ return v && (v.reason || v.message);
6495
+ } catch (e) {
6496
+ return null;
6497
+ }
6498
+ }
6499
+
6500
+ function extractPromptText(parts) {
6501
+ // chat.message gives us parts: Part[]. We only DLP-scan text parts
6502
+ // (image / tool_use parts can't contain pasted secrets in any form
6503
+ // node9's scanner currently recognizes).
6504
+ if (!Array.isArray(parts)) return "";
6505
+ return parts
6506
+ .filter((p) => p && p.type === "text")
6507
+ .map((p) => (p && p.text) || "")
6508
+ .join("\\n");
6509
+ }
6510
+
6511
+ module.exports = {
6512
+ id: "node9",
6513
+ server: async (input) => ({
6514
+ "tool.execute.before": async (ctx, mutable) => {
6515
+ const payload = {
6516
+ hook_event_name: "PreToolUse",
6517
+ tool_name: ctx.tool,
6518
+ tool_input: mutable.args,
6519
+ session_id: ctx.sessionID,
6520
+ cwd: input.directory,
6521
+ meta: { agent: "Opencode" },
6522
+ };
6523
+ const r = spawnSync(NODE9_ARGV[0], [...NODE9_ARGV.slice(1), "check"], {
6524
+ input: JSON.stringify(payload),
6525
+ encoding: "utf-8",
6526
+ timeout: HOOK_TIMEOUT_MS,
6527
+ });
6528
+ if (r.status === 0) return;
6529
+ const reason = parseReason(r.stdout) || "blocked by node9";
6530
+ throw new Error("[node9] " + reason);
6531
+ },
6532
+
6533
+ "tool.execute.after": async (ctx) => {
6534
+ // Fire-and-forget audit log \u2014 failures here must NEVER throw,
6535
+ // or we'd retroactively "block" a tool call that already ran.
6536
+ const payload = {
6537
+ hook_event_name: "PostToolUse",
6538
+ tool_name: ctx.tool,
6539
+ session_id: ctx.sessionID,
6540
+ cwd: input.directory,
6541
+ meta: { agent: "Opencode" },
6542
+ };
6543
+ try {
6544
+ spawnSync(NODE9_ARGV[0], [...NODE9_ARGV.slice(1), "log"], {
6545
+ input: JSON.stringify(payload),
6546
+ encoding: "utf-8",
6547
+ timeout: LOG_TIMEOUT_MS,
6548
+ });
6549
+ } catch (e) {
6550
+ // Swallow: audit log gaps are preferable to crashing the agent.
6551
+ }
6552
+ },
6553
+
6554
+ "chat.message": async (ctx, mutable) => {
6555
+ const prompt = extractPromptText(mutable.parts);
6556
+ if (!prompt) return;
6557
+ const payload = {
6558
+ hook_event_name: "UserPromptSubmit",
6559
+ prompt,
6560
+ session_id: ctx.sessionID,
6561
+ meta: { agent: "Opencode" },
6562
+ };
6563
+ const r = spawnSync(NODE9_ARGV[0], [...NODE9_ARGV.slice(1), "check"], {
6564
+ input: JSON.stringify(payload),
6565
+ encoding: "utf-8",
6566
+ timeout: HOOK_TIMEOUT_MS,
6567
+ });
6568
+ if (r.status === 0) return;
6569
+ const reason = parseReason(r.stdout) || "prompt blocked";
6570
+ throw new Error("[node9] " + reason);
6571
+ },
6572
+ }),
6573
+ };
6574
+ `;
6575
+ }
6576
+ var init_setup_opencode_shim = __esm({
6577
+ "src/setup-opencode-shim.ts"() {
6578
+ "use strict";
6579
+ }
6580
+ });
6581
+
6466
6582
  // src/setup.ts
6467
6583
  function hasNode9McpServer(servers) {
6468
6584
  const entry = servers["node9"];
@@ -6498,6 +6614,13 @@ function isStaleHookCommand(command) {
6498
6614
  }
6499
6615
  return false;
6500
6616
  }
6617
+ function isLegacyHookFormat(command) {
6618
+ if (!command) return false;
6619
+ return command.includes("\\");
6620
+ }
6621
+ function needsRewrite(command) {
6622
+ return isStaleHookCommand(command) || isLegacyHookFormat(command);
6623
+ }
6501
6624
  function readJson(filePath) {
6502
6625
  try {
6503
6626
  if (import_fs13.default.existsSync(filePath)) {
@@ -6670,7 +6793,7 @@ async function setupClaude() {
6670
6793
  for (const h of matcher.hooks) {
6671
6794
  const cmd = h.command ?? "";
6672
6795
  const isNode9 = cmd.includes("node9 check") || cmd.includes("cli.js check");
6673
- if (isNode9 && isStaleHookCommand(cmd)) {
6796
+ if (isNode9 && needsRewrite(cmd)) {
6674
6797
  h.command = fullPathCommand("check");
6675
6798
  console.log(import_chalk.default.yellow(" \u{1F527} PreToolUse hook repaired (stale path \u2192 current binary)"));
6676
6799
  hooksChanged = true;
@@ -6696,7 +6819,7 @@ async function setupClaude() {
6696
6819
  for (const h of matcher.hooks) {
6697
6820
  const cmd = h.command ?? "";
6698
6821
  const isNode9 = cmd.includes("node9 log") || cmd.includes("cli.js log");
6699
- if (isNode9 && isStaleHookCommand(cmd)) {
6822
+ if (isNode9 && needsRewrite(cmd)) {
6700
6823
  h.command = fullPathCommand("log");
6701
6824
  console.log(import_chalk.default.yellow(" \u{1F527} PostToolUse hook repaired (stale path \u2192 current binary)"));
6702
6825
  hooksChanged = true;
@@ -6721,7 +6844,7 @@ async function setupClaude() {
6721
6844
  for (const matcher of settings.hooks.UserPromptSubmit) {
6722
6845
  for (const h of matcher.hooks) {
6723
6846
  const cmd = h.command ?? "";
6724
- if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
6847
+ if (isNode9Hook(cmd) && needsRewrite(cmd)) {
6725
6848
  h.command = fullPathCommand("check");
6726
6849
  console.log(
6727
6850
  import_chalk.default.yellow(" \u{1F527} UserPromptSubmit hook repaired (stale path \u2192 current binary)")
@@ -6933,7 +7056,8 @@ function detectAgents(homeDir2 = import_os12.default.homedir()) {
6933
7056
  codex: exists(import_path15.default.join(homeDir2, ".codex")),
6934
7057
  windsurf: exists(import_path15.default.join(homeDir2, ".codeium", "windsurf")),
6935
7058
  vscode: exists(import_path15.default.join(homeDir2, ".vscode")),
6936
- claudeDesktop: desktopPath !== null && exists(import_path15.default.dirname(desktopPath))
7059
+ claudeDesktop: desktopPath !== null && exists(import_path15.default.dirname(desktopPath)),
7060
+ opencode: exists(import_path15.default.join(homeDir2, ".config", "opencode"))
6937
7061
  };
6938
7062
  }
6939
7063
  async function setupCursor() {
@@ -7040,7 +7164,7 @@ async function setupCodex() {
7040
7164
  } else {
7041
7165
  for (const h of existing.hooks) {
7042
7166
  const cmd = h.command ?? "";
7043
- if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
7167
+ if (isNode9Hook(cmd) && needsRewrite(cmd)) {
7044
7168
  h.command = fullPathCommand("check");
7045
7169
  hooksChanged = true;
7046
7170
  }
@@ -7060,7 +7184,7 @@ async function setupCodex() {
7060
7184
  for (const m of hooksFile.hooks.UserPromptSubmit) {
7061
7185
  for (const h of m.hooks) {
7062
7186
  const cmd = h.command ?? "";
7063
- if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
7187
+ if (isNode9Hook(cmd) && needsRewrite(cmd)) {
7064
7188
  h.command = fullPathCommand("check");
7065
7189
  hooksChanged = true;
7066
7190
  }
@@ -7081,7 +7205,7 @@ async function setupCodex() {
7081
7205
  for (const m of hooksFile.hooks.PostToolUse) {
7082
7206
  for (const h of m.hooks) {
7083
7207
  const cmd = h.command ?? "";
7084
- if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
7208
+ if (isNode9Hook(cmd) && needsRewrite(cmd)) {
7085
7209
  h.command = fullPathCommand("log");
7086
7210
  hooksChanged = true;
7087
7211
  }
@@ -7566,6 +7690,127 @@ function teardownClaudeDesktop() {
7566
7690
  console.log(import_chalk.default.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in Claude Desktop config"));
7567
7691
  }
7568
7692
  }
7693
+ function node9ArgvForShim() {
7694
+ if (process.env.NODE9_TESTING === "1") return ["node9"];
7695
+ const nodeExec = process.execPath;
7696
+ const cliScript = process.argv[1];
7697
+ if (cliScript && cliScript.endsWith(".js")) return [nodeExec, cliScript];
7698
+ return [cliScript];
7699
+ }
7700
+ function node9Version() {
7701
+ try {
7702
+ const pkg = JSON.parse(
7703
+ import_fs13.default.readFileSync(import_path15.default.join(__dirname, "..", "package.json"), "utf-8")
7704
+ );
7705
+ return pkg.version ?? "0.0.0";
7706
+ } catch {
7707
+ return "0.0.0";
7708
+ }
7709
+ }
7710
+ async function setupOpencode() {
7711
+ seedMcpPinsIfMissing();
7712
+ const homeDir2 = import_os12.default.homedir();
7713
+ const configDir = import_path15.default.join(homeDir2, ".config", "opencode");
7714
+ const pluginsDir = import_path15.default.join(configDir, "plugins");
7715
+ const configPath = import_path15.default.join(configDir, "opencode.json");
7716
+ const pluginPath = import_path15.default.join(pluginsDir, OPENCODE_PLUGIN_NAME);
7717
+ try {
7718
+ import_fs13.default.mkdirSync(pluginsDir, { recursive: true });
7719
+ } catch (err2) {
7720
+ const code = err2.code;
7721
+ if (code !== "EEXIST") {
7722
+ console.log(import_chalk.default.yellow(` \u26A0\uFE0F Could not create ${pluginsDir}: ${code ?? String(err2)}`));
7723
+ return;
7724
+ }
7725
+ }
7726
+ const shimContent = renderOpencodeShim({
7727
+ node9Argv: node9ArgvForShim(),
7728
+ version: node9Version()
7729
+ });
7730
+ let pluginChanged = false;
7731
+ const existingShim = (() => {
7732
+ try {
7733
+ return import_fs13.default.readFileSync(pluginPath, "utf-8");
7734
+ } catch {
7735
+ return null;
7736
+ }
7737
+ })();
7738
+ if (existingShim !== shimContent) {
7739
+ import_fs13.default.writeFileSync(pluginPath, shimContent);
7740
+ pluginChanged = true;
7741
+ if (existingShim) {
7742
+ console.log(import_chalk.default.yellow(" \u{1F527} Opencode plugin shim updated to current version"));
7743
+ } else {
7744
+ console.log(
7745
+ import_chalk.default.green(" \u2705 Opencode plugin installed \u2192 tool.execute.before / after, chat.message")
7746
+ );
7747
+ }
7748
+ }
7749
+ const config = readJson(configPath) ?? {};
7750
+ const mcp = config.mcp ?? {};
7751
+ let configChanged = false;
7752
+ const desiredCommand = [...node9ArgvForShim(), "mcp-server"];
7753
+ const desiredEntry = {
7754
+ type: "local",
7755
+ command: desiredCommand,
7756
+ enabled: true
7757
+ };
7758
+ const existing = mcp["node9"];
7759
+ const entryMatches = existing && existing.type === "local" && Array.isArray(existing.command) && existing.command.length === desiredCommand.length && existing.command.every((c, i) => c === desiredCommand[i]) && existing.enabled !== false;
7760
+ if (!entryMatches) {
7761
+ mcp["node9"] = desiredEntry;
7762
+ config.mcp = mcp;
7763
+ configChanged = true;
7764
+ if (existing) {
7765
+ console.log(import_chalk.default.yellow(" \u{1F527} Opencode MCP entry updated (node9)"));
7766
+ } else {
7767
+ console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
7768
+ }
7769
+ }
7770
+ if (configChanged) writeJson(configPath, config);
7771
+ if (pluginChanged || configChanged) {
7772
+ console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Opencode!"));
7773
+ console.log(import_chalk.default.gray(" Restart Opencode for changes to take effect."));
7774
+ printDaemonTip();
7775
+ } else {
7776
+ console.log(import_chalk.default.blue(" \u2139\uFE0F Node9 is already fully configured for Opencode."));
7777
+ }
7778
+ }
7779
+ function teardownOpencode() {
7780
+ const homeDir2 = import_os12.default.homedir();
7781
+ const configDir = import_path15.default.join(homeDir2, ".config", "opencode");
7782
+ const pluginsDir = import_path15.default.join(configDir, "plugins");
7783
+ const configPath = import_path15.default.join(configDir, "opencode.json");
7784
+ const pluginPath = import_path15.default.join(pluginsDir, OPENCODE_PLUGIN_NAME);
7785
+ try {
7786
+ if (import_fs13.default.existsSync(pluginPath)) {
7787
+ import_fs13.default.unlinkSync(pluginPath);
7788
+ console.log(import_chalk.default.green(" \u2705 Removed node9 plugin from ~/.config/opencode/plugins/"));
7789
+ }
7790
+ } catch (err2) {
7791
+ console.log(import_chalk.default.yellow(` \u26A0\uFE0F Could not remove ${pluginPath}: ${String(err2)}`));
7792
+ }
7793
+ const config = readJson(configPath);
7794
+ if (!config) {
7795
+ console.log(import_chalk.default.blue(" \u2139\uFE0F ~/.config/opencode/opencode.json not found \u2014 nothing to remove"));
7796
+ return;
7797
+ }
7798
+ const mcp = config.mcp ?? {};
7799
+ let changed = false;
7800
+ if (mcp["node9"]) {
7801
+ delete mcp["node9"];
7802
+ changed = true;
7803
+ console.log(
7804
+ import_chalk.default.green(" \u2705 Removed node9 MCP server entry from ~/.config/opencode/opencode.json")
7805
+ );
7806
+ }
7807
+ if (changed) {
7808
+ config.mcp = mcp;
7809
+ writeJson(configPath, config);
7810
+ } else {
7811
+ console.log(import_chalk.default.blue(" \u2139\uFE0F No node9 entries found in ~/.config/opencode/opencode.json"));
7812
+ }
7813
+ }
7569
7814
  function getAgentsStatus(homeDir2 = import_os12.default.homedir()) {
7570
7815
  const detected = detectAgents(homeDir2);
7571
7816
  const claudeWired = (() => {
@@ -7648,10 +7893,30 @@ function getAgentsStatus(homeDir2 = import_os12.default.homedir()) {
7648
7893
  return !!(cfg?.mcpServers && hasNode9McpServer(cfg.mcpServers));
7649
7894
  })(),
7650
7895
  mode: detected.claudeDesktop ? "mcp" : null
7896
+ },
7897
+ {
7898
+ name: "opencode",
7899
+ label: "Opencode",
7900
+ installed: detected.opencode,
7901
+ wired: (() => {
7902
+ const pluginPath = import_path15.default.join(
7903
+ homeDir2,
7904
+ ".config",
7905
+ "opencode",
7906
+ "plugins",
7907
+ OPENCODE_PLUGIN_NAME
7908
+ );
7909
+ if (import_fs13.default.existsSync(pluginPath)) return true;
7910
+ const cfg = readJson(
7911
+ import_path15.default.join(homeDir2, ".config", "opencode", "opencode.json")
7912
+ );
7913
+ return !!cfg?.mcp?.["node9"];
7914
+ })(),
7915
+ mode: detected.opencode ? "hooks" : null
7651
7916
  }
7652
7917
  ];
7653
7918
  }
7654
- var import_fs13, import_path15, import_os12, import_chalk, import_prompts, import_smol_toml, NODE9_MCP_SERVER_ENTRY, CODEX_PRE_TOOL_MATCHERS;
7919
+ var import_fs13, import_path15, import_os12, import_chalk, import_prompts, import_smol_toml, NODE9_MCP_SERVER_ENTRY, CODEX_PRE_TOOL_MATCHERS, OPENCODE_PLUGIN_NAME;
7655
7920
  var init_setup = __esm({
7656
7921
  "src/setup.ts"() {
7657
7922
  "use strict";
@@ -7662,8 +7927,10 @@ var init_setup = __esm({
7662
7927
  import_prompts = require("@inquirer/prompts");
7663
7928
  import_smol_toml = require("smol-toml");
7664
7929
  init_mcp_pin();
7930
+ init_setup_opencode_shim();
7665
7931
  NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7666
7932
  CODEX_PRE_TOOL_MATCHERS = ["^Bash$", "^apply_patch$", "^mcp__.*"];
7933
+ OPENCODE_PLUGIN_NAME = "node9.js";
7667
7934
  }
7668
7935
  });
7669
7936
 
@@ -15965,6 +16232,11 @@ function sanitize2(value) {
15965
16232
  return value.replace(/[\x00-\x1F\x7F]/g, "");
15966
16233
  }
15967
16234
  function detectAiAgent(payload) {
16235
+ const meta = payload.meta;
16236
+ if (meta && typeof meta === "object") {
16237
+ const tagged = meta.agent;
16238
+ if (typeof tagged === "string" && tagged.length > 0) return tagged;
16239
+ }
15968
16240
  if (payload.turn_id !== void 0) {
15969
16241
  return "Codex";
15970
16242
  }
@@ -18603,6 +18875,7 @@ function registerInitCommand(program2) {
18603
18875
  else if (agent === "windsurf") await setupWindsurf();
18604
18876
  else if (agent === "vscode") await setupVSCode();
18605
18877
  else if (agent === "claudeDesktop") await setupClaudeDesktop();
18878
+ else if (agent === "opencode") await setupOpencode();
18606
18879
  console.log("");
18607
18880
  }
18608
18881
  if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
@@ -20362,7 +20635,8 @@ var SETUP_FN = {
20362
20635
  codex: setupCodex,
20363
20636
  windsurf: setupWindsurf,
20364
20637
  vscode: setupVSCode,
20365
- claudeDesktop: setupClaudeDesktop
20638
+ claudeDesktop: setupClaudeDesktop,
20639
+ opencode: setupOpencode
20366
20640
  };
20367
20641
  var TEARDOWN_FN = {
20368
20642
  claude: teardownClaude,
@@ -20371,7 +20645,8 @@ var TEARDOWN_FN = {
20371
20645
  codex: teardownCodex,
20372
20646
  windsurf: teardownWindsurf,
20373
20647
  vscode: teardownVSCode,
20374
- claudeDesktop: teardownClaudeDesktop
20648
+ claudeDesktop: teardownClaudeDesktop,
20649
+ opencode: teardownOpencode
20375
20650
  };
20376
20651
  var AGENT_NAMES = Object.keys(SETUP_FN);
20377
20652
  function registerAgentsCommand(program2) {
package/dist/cli.mjs CHANGED
@@ -6438,6 +6438,122 @@ var init_mcp_pin = __esm({
6438
6438
  }
6439
6439
  });
6440
6440
 
6441
+ // src/setup-opencode-shim.ts
6442
+ function renderOpencodeShim(input) {
6443
+ const { node9Argv, version: version2 } = input;
6444
+ return `// Auto-generated by \`node9 init\`. Do not edit \u2014 re-run init to upgrade.
6445
+ // NODE9_SHIM_VERSION = "${version2}"
6446
+ //
6447
+ // node9 protection shim for Opencode. Wires three hooks against the
6448
+ // agent's plugin API and shells out to the node9 CLI for verdicts.
6449
+ //
6450
+ // Block by throwing from a hook handler; Opencode's plugin trigger
6451
+ // (packages/opencode/src/plugin/index.ts:273) propagates the error and
6452
+ // halts the tool call.
6453
+
6454
+ const { spawnSync } = require("node:child_process");
6455
+
6456
+ // argv prefix for invoking the node9 CLI. argv[0] is the executable
6457
+ // (either the npm-installed wrapper script or the node binary); the
6458
+ // remaining entries (if any) are passed as the leading args before
6459
+ // the subcommand name (e.g. ["/path/to/dist/cli.js"] for dev mode).
6460
+ const NODE9_ARGV = ${JSON.stringify(node9Argv)};
6461
+ const HOOK_TIMEOUT_MS = 30000;
6462
+ const LOG_TIMEOUT_MS = 5000;
6463
+
6464
+ function parseReason(stdout) {
6465
+ // node9 check emits {decision, reason, message} JSON on stdout when
6466
+ // blocking. Fall back to a generic string if anything goes wrong.
6467
+ try {
6468
+ const v = JSON.parse(stdout || "");
6469
+ return v && (v.reason || v.message);
6470
+ } catch (e) {
6471
+ return null;
6472
+ }
6473
+ }
6474
+
6475
+ function extractPromptText(parts) {
6476
+ // chat.message gives us parts: Part[]. We only DLP-scan text parts
6477
+ // (image / tool_use parts can't contain pasted secrets in any form
6478
+ // node9's scanner currently recognizes).
6479
+ if (!Array.isArray(parts)) return "";
6480
+ return parts
6481
+ .filter((p) => p && p.type === "text")
6482
+ .map((p) => (p && p.text) || "")
6483
+ .join("\\n");
6484
+ }
6485
+
6486
+ module.exports = {
6487
+ id: "node9",
6488
+ server: async (input) => ({
6489
+ "tool.execute.before": async (ctx, mutable) => {
6490
+ const payload = {
6491
+ hook_event_name: "PreToolUse",
6492
+ tool_name: ctx.tool,
6493
+ tool_input: mutable.args,
6494
+ session_id: ctx.sessionID,
6495
+ cwd: input.directory,
6496
+ meta: { agent: "Opencode" },
6497
+ };
6498
+ const r = spawnSync(NODE9_ARGV[0], [...NODE9_ARGV.slice(1), "check"], {
6499
+ input: JSON.stringify(payload),
6500
+ encoding: "utf-8",
6501
+ timeout: HOOK_TIMEOUT_MS,
6502
+ });
6503
+ if (r.status === 0) return;
6504
+ const reason = parseReason(r.stdout) || "blocked by node9";
6505
+ throw new Error("[node9] " + reason);
6506
+ },
6507
+
6508
+ "tool.execute.after": async (ctx) => {
6509
+ // Fire-and-forget audit log \u2014 failures here must NEVER throw,
6510
+ // or we'd retroactively "block" a tool call that already ran.
6511
+ const payload = {
6512
+ hook_event_name: "PostToolUse",
6513
+ tool_name: ctx.tool,
6514
+ session_id: ctx.sessionID,
6515
+ cwd: input.directory,
6516
+ meta: { agent: "Opencode" },
6517
+ };
6518
+ try {
6519
+ spawnSync(NODE9_ARGV[0], [...NODE9_ARGV.slice(1), "log"], {
6520
+ input: JSON.stringify(payload),
6521
+ encoding: "utf-8",
6522
+ timeout: LOG_TIMEOUT_MS,
6523
+ });
6524
+ } catch (e) {
6525
+ // Swallow: audit log gaps are preferable to crashing the agent.
6526
+ }
6527
+ },
6528
+
6529
+ "chat.message": async (ctx, mutable) => {
6530
+ const prompt = extractPromptText(mutable.parts);
6531
+ if (!prompt) return;
6532
+ const payload = {
6533
+ hook_event_name: "UserPromptSubmit",
6534
+ prompt,
6535
+ session_id: ctx.sessionID,
6536
+ meta: { agent: "Opencode" },
6537
+ };
6538
+ const r = spawnSync(NODE9_ARGV[0], [...NODE9_ARGV.slice(1), "check"], {
6539
+ input: JSON.stringify(payload),
6540
+ encoding: "utf-8",
6541
+ timeout: HOOK_TIMEOUT_MS,
6542
+ });
6543
+ if (r.status === 0) return;
6544
+ const reason = parseReason(r.stdout) || "prompt blocked";
6545
+ throw new Error("[node9] " + reason);
6546
+ },
6547
+ }),
6548
+ };
6549
+ `;
6550
+ }
6551
+ var init_setup_opencode_shim = __esm({
6552
+ "src/setup-opencode-shim.ts"() {
6553
+ "use strict";
6554
+ }
6555
+ });
6556
+
6441
6557
  // src/setup.ts
6442
6558
  import fs13 from "fs";
6443
6559
  import path15 from "path";
@@ -6479,6 +6595,13 @@ function isStaleHookCommand(command) {
6479
6595
  }
6480
6596
  return false;
6481
6597
  }
6598
+ function isLegacyHookFormat(command) {
6599
+ if (!command) return false;
6600
+ return command.includes("\\");
6601
+ }
6602
+ function needsRewrite(command) {
6603
+ return isStaleHookCommand(command) || isLegacyHookFormat(command);
6604
+ }
6482
6605
  function readJson(filePath) {
6483
6606
  try {
6484
6607
  if (fs13.existsSync(filePath)) {
@@ -6651,7 +6774,7 @@ async function setupClaude() {
6651
6774
  for (const h of matcher.hooks) {
6652
6775
  const cmd = h.command ?? "";
6653
6776
  const isNode9 = cmd.includes("node9 check") || cmd.includes("cli.js check");
6654
- if (isNode9 && isStaleHookCommand(cmd)) {
6777
+ if (isNode9 && needsRewrite(cmd)) {
6655
6778
  h.command = fullPathCommand("check");
6656
6779
  console.log(chalk.yellow(" \u{1F527} PreToolUse hook repaired (stale path \u2192 current binary)"));
6657
6780
  hooksChanged = true;
@@ -6677,7 +6800,7 @@ async function setupClaude() {
6677
6800
  for (const h of matcher.hooks) {
6678
6801
  const cmd = h.command ?? "";
6679
6802
  const isNode9 = cmd.includes("node9 log") || cmd.includes("cli.js log");
6680
- if (isNode9 && isStaleHookCommand(cmd)) {
6803
+ if (isNode9 && needsRewrite(cmd)) {
6681
6804
  h.command = fullPathCommand("log");
6682
6805
  console.log(chalk.yellow(" \u{1F527} PostToolUse hook repaired (stale path \u2192 current binary)"));
6683
6806
  hooksChanged = true;
@@ -6702,7 +6825,7 @@ async function setupClaude() {
6702
6825
  for (const matcher of settings.hooks.UserPromptSubmit) {
6703
6826
  for (const h of matcher.hooks) {
6704
6827
  const cmd = h.command ?? "";
6705
- if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
6828
+ if (isNode9Hook(cmd) && needsRewrite(cmd)) {
6706
6829
  h.command = fullPathCommand("check");
6707
6830
  console.log(
6708
6831
  chalk.yellow(" \u{1F527} UserPromptSubmit hook repaired (stale path \u2192 current binary)")
@@ -6914,7 +7037,8 @@ function detectAgents(homeDir2 = os12.homedir()) {
6914
7037
  codex: exists(path15.join(homeDir2, ".codex")),
6915
7038
  windsurf: exists(path15.join(homeDir2, ".codeium", "windsurf")),
6916
7039
  vscode: exists(path15.join(homeDir2, ".vscode")),
6917
- claudeDesktop: desktopPath !== null && exists(path15.dirname(desktopPath))
7040
+ claudeDesktop: desktopPath !== null && exists(path15.dirname(desktopPath)),
7041
+ opencode: exists(path15.join(homeDir2, ".config", "opencode"))
6918
7042
  };
6919
7043
  }
6920
7044
  async function setupCursor() {
@@ -7021,7 +7145,7 @@ async function setupCodex() {
7021
7145
  } else {
7022
7146
  for (const h of existing.hooks) {
7023
7147
  const cmd = h.command ?? "";
7024
- if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
7148
+ if (isNode9Hook(cmd) && needsRewrite(cmd)) {
7025
7149
  h.command = fullPathCommand("check");
7026
7150
  hooksChanged = true;
7027
7151
  }
@@ -7041,7 +7165,7 @@ async function setupCodex() {
7041
7165
  for (const m of hooksFile.hooks.UserPromptSubmit) {
7042
7166
  for (const h of m.hooks) {
7043
7167
  const cmd = h.command ?? "";
7044
- if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
7168
+ if (isNode9Hook(cmd) && needsRewrite(cmd)) {
7045
7169
  h.command = fullPathCommand("check");
7046
7170
  hooksChanged = true;
7047
7171
  }
@@ -7062,7 +7186,7 @@ async function setupCodex() {
7062
7186
  for (const m of hooksFile.hooks.PostToolUse) {
7063
7187
  for (const h of m.hooks) {
7064
7188
  const cmd = h.command ?? "";
7065
- if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
7189
+ if (isNode9Hook(cmd) && needsRewrite(cmd)) {
7066
7190
  h.command = fullPathCommand("log");
7067
7191
  hooksChanged = true;
7068
7192
  }
@@ -7547,6 +7671,127 @@ function teardownClaudeDesktop() {
7547
7671
  console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in Claude Desktop config"));
7548
7672
  }
7549
7673
  }
7674
+ function node9ArgvForShim() {
7675
+ if (process.env.NODE9_TESTING === "1") return ["node9"];
7676
+ const nodeExec = process.execPath;
7677
+ const cliScript = process.argv[1];
7678
+ if (cliScript && cliScript.endsWith(".js")) return [nodeExec, cliScript];
7679
+ return [cliScript];
7680
+ }
7681
+ function node9Version() {
7682
+ try {
7683
+ const pkg = JSON.parse(
7684
+ fs13.readFileSync(path15.join(__dirname, "..", "package.json"), "utf-8")
7685
+ );
7686
+ return pkg.version ?? "0.0.0";
7687
+ } catch {
7688
+ return "0.0.0";
7689
+ }
7690
+ }
7691
+ async function setupOpencode() {
7692
+ seedMcpPinsIfMissing();
7693
+ const homeDir2 = os12.homedir();
7694
+ const configDir = path15.join(homeDir2, ".config", "opencode");
7695
+ const pluginsDir = path15.join(configDir, "plugins");
7696
+ const configPath = path15.join(configDir, "opencode.json");
7697
+ const pluginPath = path15.join(pluginsDir, OPENCODE_PLUGIN_NAME);
7698
+ try {
7699
+ fs13.mkdirSync(pluginsDir, { recursive: true });
7700
+ } catch (err2) {
7701
+ const code = err2.code;
7702
+ if (code !== "EEXIST") {
7703
+ console.log(chalk.yellow(` \u26A0\uFE0F Could not create ${pluginsDir}: ${code ?? String(err2)}`));
7704
+ return;
7705
+ }
7706
+ }
7707
+ const shimContent = renderOpencodeShim({
7708
+ node9Argv: node9ArgvForShim(),
7709
+ version: node9Version()
7710
+ });
7711
+ let pluginChanged = false;
7712
+ const existingShim = (() => {
7713
+ try {
7714
+ return fs13.readFileSync(pluginPath, "utf-8");
7715
+ } catch {
7716
+ return null;
7717
+ }
7718
+ })();
7719
+ if (existingShim !== shimContent) {
7720
+ fs13.writeFileSync(pluginPath, shimContent);
7721
+ pluginChanged = true;
7722
+ if (existingShim) {
7723
+ console.log(chalk.yellow(" \u{1F527} Opencode plugin shim updated to current version"));
7724
+ } else {
7725
+ console.log(
7726
+ chalk.green(" \u2705 Opencode plugin installed \u2192 tool.execute.before / after, chat.message")
7727
+ );
7728
+ }
7729
+ }
7730
+ const config = readJson(configPath) ?? {};
7731
+ const mcp = config.mcp ?? {};
7732
+ let configChanged = false;
7733
+ const desiredCommand = [...node9ArgvForShim(), "mcp-server"];
7734
+ const desiredEntry = {
7735
+ type: "local",
7736
+ command: desiredCommand,
7737
+ enabled: true
7738
+ };
7739
+ const existing = mcp["node9"];
7740
+ const entryMatches = existing && existing.type === "local" && Array.isArray(existing.command) && existing.command.length === desiredCommand.length && existing.command.every((c, i) => c === desiredCommand[i]) && existing.enabled !== false;
7741
+ if (!entryMatches) {
7742
+ mcp["node9"] = desiredEntry;
7743
+ config.mcp = mcp;
7744
+ configChanged = true;
7745
+ if (existing) {
7746
+ console.log(chalk.yellow(" \u{1F527} Opencode MCP entry updated (node9)"));
7747
+ } else {
7748
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
7749
+ }
7750
+ }
7751
+ if (configChanged) writeJson(configPath, config);
7752
+ if (pluginChanged || configChanged) {
7753
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Opencode!"));
7754
+ console.log(chalk.gray(" Restart Opencode for changes to take effect."));
7755
+ printDaemonTip();
7756
+ } else {
7757
+ console.log(chalk.blue(" \u2139\uFE0F Node9 is already fully configured for Opencode."));
7758
+ }
7759
+ }
7760
+ function teardownOpencode() {
7761
+ const homeDir2 = os12.homedir();
7762
+ const configDir = path15.join(homeDir2, ".config", "opencode");
7763
+ const pluginsDir = path15.join(configDir, "plugins");
7764
+ const configPath = path15.join(configDir, "opencode.json");
7765
+ const pluginPath = path15.join(pluginsDir, OPENCODE_PLUGIN_NAME);
7766
+ try {
7767
+ if (fs13.existsSync(pluginPath)) {
7768
+ fs13.unlinkSync(pluginPath);
7769
+ console.log(chalk.green(" \u2705 Removed node9 plugin from ~/.config/opencode/plugins/"));
7770
+ }
7771
+ } catch (err2) {
7772
+ console.log(chalk.yellow(` \u26A0\uFE0F Could not remove ${pluginPath}: ${String(err2)}`));
7773
+ }
7774
+ const config = readJson(configPath);
7775
+ if (!config) {
7776
+ console.log(chalk.blue(" \u2139\uFE0F ~/.config/opencode/opencode.json not found \u2014 nothing to remove"));
7777
+ return;
7778
+ }
7779
+ const mcp = config.mcp ?? {};
7780
+ let changed = false;
7781
+ if (mcp["node9"]) {
7782
+ delete mcp["node9"];
7783
+ changed = true;
7784
+ console.log(
7785
+ chalk.green(" \u2705 Removed node9 MCP server entry from ~/.config/opencode/opencode.json")
7786
+ );
7787
+ }
7788
+ if (changed) {
7789
+ config.mcp = mcp;
7790
+ writeJson(configPath, config);
7791
+ } else {
7792
+ console.log(chalk.blue(" \u2139\uFE0F No node9 entries found in ~/.config/opencode/opencode.json"));
7793
+ }
7794
+ }
7550
7795
  function getAgentsStatus(homeDir2 = os12.homedir()) {
7551
7796
  const detected = detectAgents(homeDir2);
7552
7797
  const claudeWired = (() => {
@@ -7629,16 +7874,38 @@ function getAgentsStatus(homeDir2 = os12.homedir()) {
7629
7874
  return !!(cfg?.mcpServers && hasNode9McpServer(cfg.mcpServers));
7630
7875
  })(),
7631
7876
  mode: detected.claudeDesktop ? "mcp" : null
7877
+ },
7878
+ {
7879
+ name: "opencode",
7880
+ label: "Opencode",
7881
+ installed: detected.opencode,
7882
+ wired: (() => {
7883
+ const pluginPath = path15.join(
7884
+ homeDir2,
7885
+ ".config",
7886
+ "opencode",
7887
+ "plugins",
7888
+ OPENCODE_PLUGIN_NAME
7889
+ );
7890
+ if (fs13.existsSync(pluginPath)) return true;
7891
+ const cfg = readJson(
7892
+ path15.join(homeDir2, ".config", "opencode", "opencode.json")
7893
+ );
7894
+ return !!cfg?.mcp?.["node9"];
7895
+ })(),
7896
+ mode: detected.opencode ? "hooks" : null
7632
7897
  }
7633
7898
  ];
7634
7899
  }
7635
- var NODE9_MCP_SERVER_ENTRY, CODEX_PRE_TOOL_MATCHERS;
7900
+ var NODE9_MCP_SERVER_ENTRY, CODEX_PRE_TOOL_MATCHERS, OPENCODE_PLUGIN_NAME;
7636
7901
  var init_setup = __esm({
7637
7902
  "src/setup.ts"() {
7638
7903
  "use strict";
7639
7904
  init_mcp_pin();
7905
+ init_setup_opencode_shim();
7640
7906
  NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7641
7907
  CODEX_PRE_TOOL_MATCHERS = ["^Bash$", "^apply_patch$", "^mcp__.*"];
7908
+ OPENCODE_PLUGIN_NAME = "node9.js";
7642
7909
  }
7643
7910
  });
7644
7911
 
@@ -15937,6 +16204,11 @@ function sanitize2(value) {
15937
16204
  return value.replace(/[\x00-\x1F\x7F]/g, "");
15938
16205
  }
15939
16206
  function detectAiAgent(payload) {
16207
+ const meta = payload.meta;
16208
+ if (meta && typeof meta === "object") {
16209
+ const tagged = meta.agent;
16210
+ if (typeof tagged === "string" && tagged.length > 0) return tagged;
16211
+ }
15940
16212
  if (payload.turn_id !== void 0) {
15941
16213
  return "Codex";
15942
16214
  }
@@ -18575,6 +18847,7 @@ function registerInitCommand(program2) {
18575
18847
  else if (agent === "windsurf") await setupWindsurf();
18576
18848
  else if (agent === "vscode") await setupVSCode();
18577
18849
  else if (agent === "claudeDesktop") await setupClaudeDesktop();
18850
+ else if (agent === "opencode") await setupOpencode();
18578
18851
  console.log("");
18579
18852
  }
18580
18853
  if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
@@ -20334,7 +20607,8 @@ var SETUP_FN = {
20334
20607
  codex: setupCodex,
20335
20608
  windsurf: setupWindsurf,
20336
20609
  vscode: setupVSCode,
20337
- claudeDesktop: setupClaudeDesktop
20610
+ claudeDesktop: setupClaudeDesktop,
20611
+ opencode: setupOpencode
20338
20612
  };
20339
20613
  var TEARDOWN_FN = {
20340
20614
  claude: teardownClaude,
@@ -20343,7 +20617,8 @@ var TEARDOWN_FN = {
20343
20617
  codex: teardownCodex,
20344
20618
  windsurf: teardownWindsurf,
20345
20619
  vscode: teardownVSCode,
20346
- claudeDesktop: teardownClaudeDesktop
20620
+ claudeDesktop: teardownClaudeDesktop,
20621
+ opencode: teardownOpencode
20347
20622
  };
20348
20623
  var AGENT_NAMES = Object.keys(SETUP_FN);
20349
20624
  function registerAgentsCommand(program2) {
@@ -3340,6 +3340,13 @@ var init_mcp_pin = __esm({
3340
3340
  }
3341
3341
  });
3342
3342
 
3343
+ // src/setup-opencode-shim.ts
3344
+ var init_setup_opencode_shim = __esm({
3345
+ "src/setup-opencode-shim.ts"() {
3346
+ "use strict";
3347
+ }
3348
+ });
3349
+
3343
3350
  // src/setup.ts
3344
3351
  import chalk2 from "chalk";
3345
3352
  import { confirm } from "@inquirer/prompts";
@@ -3348,6 +3355,7 @@ var init_setup = __esm({
3348
3355
  "src/setup.ts"() {
3349
3356
  "use strict";
3350
3357
  init_mcp_pin();
3358
+ init_setup_opencode_shim();
3351
3359
  }
3352
3360
  });
3353
3361
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.24.2",
3
+ "version": "1.25.0",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",