@node9/proxy 1.24.3 → 1.26.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.mjs CHANGED
@@ -2251,6 +2251,42 @@ var init_dist = __esm({
2251
2251
  regex: /\bAGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JNLH]{58}\b/,
2252
2252
  severity: "block",
2253
2253
  keywords: ["age-secret-key-"]
2254
+ },
2255
+ // ── Database connection strings ───────────────────────────────────────────
2256
+ // Universal <scheme>://[user]:<password>@<host> shape. Covers the gap
2257
+ // vendor-prefix patterns (AWS / GitHub / Stripe / …) leave open. Matches
2258
+ // the whole URL so maskSecret produces `<scheme>...:****@...<host>` —
2259
+ // the password value never appears in the redacted sample.
2260
+ //
2261
+ // Schemes covered: redis, rediss (TLS), postgres, postgresql,
2262
+ // mongodb, mongodb+srv, mysql, mariadb, amqp, amqps, kafka,
2263
+ // clickhouse, cassandra. HTTP(S) / FTP / SSH are intentionally
2264
+ // excluded — they're not database URLs and adding them would
2265
+ // create false positives on every basic-auth URL in the wild.
2266
+ //
2267
+ // Requires `:password@` (4+ char password) so user-only URLs like
2268
+ // `redis://user@host` don't match. Stopwords ('your', '${', '<your',
2269
+ // 'placeholder', 'changeme', etc.) keep doc/README scans clean.
2270
+ {
2271
+ name: "Database Connection String",
2272
+ regex: /\b(redis|rediss|postgres|postgresql|mongodb|mongodb\+srv|mysql|mariadb|amqp|amqps|kafka|clickhouse|cassandra):\/\/[^:/\s@]*:[^@\s]{4,}@[^\s/]+/,
2273
+ severity: "block",
2274
+ keywords: [
2275
+ "redis://",
2276
+ "rediss://",
2277
+ "postgres://",
2278
+ "postgresql://",
2279
+ "mongodb://",
2280
+ "mongodb+srv://",
2281
+ "mysql://",
2282
+ "mariadb://",
2283
+ "amqp://",
2284
+ "amqps://",
2285
+ "kafka://",
2286
+ "clickhouse://",
2287
+ "cassandra://"
2288
+ ],
2289
+ minEntropy: 3
2254
2290
  }
2255
2291
  ];
2256
2292
  DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
@@ -2353,7 +2389,7 @@ var init_dist = __esm({
2353
2389
  },
2354
2390
  {
2355
2391
  // Mirrors the JSON shield's `.env` pattern (project-jail.json's
2356
- // review-read-env-any-tool) so the AST FS-op path catches the
2392
+ // block-read-env-any-tool) so the AST FS-op path catches the
2357
2393
  // same set the regex shield does — including Next.js / Vite's
2358
2394
  // `.env.<env>.local` double-suffix overrides which are commonly
2359
2395
  // gitignored AND commonly contain real secrets.
@@ -3099,7 +3135,7 @@ var init_dist = __esm({
3099
3135
  {
3100
3136
  field: "command",
3101
3137
  op: "matches",
3102
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
3138
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
3103
3139
  flags: "i"
3104
3140
  }
3105
3141
  ],
@@ -3113,7 +3149,7 @@ var init_dist = __esm({
3113
3149
  {
3114
3150
  field: "command",
3115
3151
  op: "matches",
3116
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
3152
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
3117
3153
  flags: "i"
3118
3154
  }
3119
3155
  ],
@@ -3127,7 +3163,7 @@ var init_dist = __esm({
3127
3163
  {
3128
3164
  field: "command",
3129
3165
  op: "matches",
3130
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*\\.env(\\.local|\\.production|\\.staging)?\\b",
3166
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
3131
3167
  flags: "i"
3132
3168
  }
3133
3169
  ],
@@ -3141,7 +3177,7 @@ var init_dist = __esm({
3141
3177
  {
3142
3178
  field: "command",
3143
3179
  op: "matches",
3144
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
3180
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
3145
3181
  flags: "i"
3146
3182
  }
3147
3183
  ],
@@ -3177,7 +3213,7 @@ var init_dist = __esm({
3177
3213
  reason: "Reading AWS credentials is blocked by project-jail shield"
3178
3214
  },
3179
3215
  {
3180
- name: "shield:project-jail:review-read-env-any-tool",
3216
+ name: "shield:project-jail:block-read-env-any-tool",
3181
3217
  tool: "*",
3182
3218
  conditions: [
3183
3219
  {
@@ -3187,8 +3223,8 @@ var init_dist = __esm({
3187
3223
  flags: "i"
3188
3224
  }
3189
3225
  ],
3190
- verdict: "review",
3191
- reason: "Reading .env files requires approval (project-jail shield)"
3226
+ verdict: "block",
3227
+ reason: "Reading .env files is blocked by project-jail shield"
3192
3228
  },
3193
3229
  {
3194
3230
  name: "shield:project-jail:review-read-credentials-any-tool",
@@ -3453,6 +3489,30 @@ function writeShieldOverride(shieldName, ruleName, verdict) {
3453
3489
  overrides[shieldName] = { ...overrides[shieldName] ?? {}, [ruleName]: verdict };
3454
3490
  writeShieldsFile({ ...current, overrides });
3455
3491
  }
3492
+ function migrateRenamedRuleKeys() {
3493
+ const current = readShieldsFile();
3494
+ if (!current.overrides || Object.keys(current.overrides).length === 0) return [];
3495
+ const renameMap = new Map(RULE_KEY_MIGRATIONS);
3496
+ const migrated = [];
3497
+ const nextOverrides = {};
3498
+ let anyChange = false;
3499
+ for (const [shield, rules] of Object.entries(current.overrides)) {
3500
+ const nextRules = {};
3501
+ for (const [key, verdict] of Object.entries(rules)) {
3502
+ const newKey = renameMap.get(key);
3503
+ if (newKey) {
3504
+ if (!(newKey in nextRules)) nextRules[newKey] = verdict;
3505
+ migrated.push({ shield, oldKey: key, newKey });
3506
+ anyChange = true;
3507
+ } else {
3508
+ nextRules[key] = verdict;
3509
+ }
3510
+ }
3511
+ if (Object.keys(nextRules).length > 0) nextOverrides[shield] = nextRules;
3512
+ }
3513
+ if (anyChange) writeShieldsFile({ ...current, overrides: nextOverrides });
3514
+ return migrated;
3515
+ }
3456
3516
  function clearShieldOverride(shieldName, ruleName) {
3457
3517
  const current = readShieldsFile();
3458
3518
  if (!current.overrides?.[shieldName]?.[ruleName]) return;
@@ -3501,7 +3561,7 @@ function installShield(name, shieldJson) {
3501
3561
  fs2.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
3502
3562
  fs2.renameSync(tmp, filePath);
3503
3563
  }
3504
- var USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE;
3564
+ var USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE, RULE_KEY_MIGRATIONS;
3505
3565
  var init_shields = __esm({
3506
3566
  "src/shields.ts"() {
3507
3567
  "use strict";
@@ -3510,6 +3570,14 @@ var init_shields = __esm({
3510
3570
  USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
3511
3571
  SHIELDS = buildSHIELDS();
3512
3572
  SHIELDS_STATE_FILE = path2.join(os2.homedir(), ".node9", "shields.json");
3573
+ RULE_KEY_MIGRATIONS = [
3574
+ // 2026-05-21 — project-jail .env reads promoted review → block. Renamed
3575
+ // so the key honestly describes the verdict. Credential-file rules
3576
+ // (.netrc, .npmrc, .docker, .kube, gcloud) deliberately stay at `review`
3577
+ // — see the rationale in packages/policy-engine/src/shell/index.ts
3578
+ // around the review-read-credentials rule.
3579
+ ["shield:project-jail:review-read-env-any-tool", "shield:project-jail:block-read-env-any-tool"]
3580
+ ];
3513
3581
  }
3514
3582
  });
3515
3583
 
@@ -6438,6 +6506,122 @@ var init_mcp_pin = __esm({
6438
6506
  }
6439
6507
  });
6440
6508
 
6509
+ // src/setup-opencode-shim.ts
6510
+ function renderOpencodeShim(input) {
6511
+ const { node9Argv, version: version2 } = input;
6512
+ return `// Auto-generated by \`node9 init\`. Do not edit \u2014 re-run init to upgrade.
6513
+ // NODE9_SHIM_VERSION = "${version2}"
6514
+ //
6515
+ // node9 protection shim for Opencode. Wires three hooks against the
6516
+ // agent's plugin API and shells out to the node9 CLI for verdicts.
6517
+ //
6518
+ // Block by throwing from a hook handler; Opencode's plugin trigger
6519
+ // (packages/opencode/src/plugin/index.ts:273) propagates the error and
6520
+ // halts the tool call.
6521
+
6522
+ const { spawnSync } = require("node:child_process");
6523
+
6524
+ // argv prefix for invoking the node9 CLI. argv[0] is the executable
6525
+ // (either the npm-installed wrapper script or the node binary); the
6526
+ // remaining entries (if any) are passed as the leading args before
6527
+ // the subcommand name (e.g. ["/path/to/dist/cli.js"] for dev mode).
6528
+ const NODE9_ARGV = ${JSON.stringify(node9Argv)};
6529
+ const HOOK_TIMEOUT_MS = 30000;
6530
+ const LOG_TIMEOUT_MS = 5000;
6531
+
6532
+ function parseReason(stdout) {
6533
+ // node9 check emits {decision, reason, message} JSON on stdout when
6534
+ // blocking. Fall back to a generic string if anything goes wrong.
6535
+ try {
6536
+ const v = JSON.parse(stdout || "");
6537
+ return v && (v.reason || v.message);
6538
+ } catch (e) {
6539
+ return null;
6540
+ }
6541
+ }
6542
+
6543
+ function extractPromptText(parts) {
6544
+ // chat.message gives us parts: Part[]. We only DLP-scan text parts
6545
+ // (image / tool_use parts can't contain pasted secrets in any form
6546
+ // node9's scanner currently recognizes).
6547
+ if (!Array.isArray(parts)) return "";
6548
+ return parts
6549
+ .filter((p) => p && p.type === "text")
6550
+ .map((p) => (p && p.text) || "")
6551
+ .join("\\n");
6552
+ }
6553
+
6554
+ module.exports = {
6555
+ id: "node9",
6556
+ server: async (input) => ({
6557
+ "tool.execute.before": async (ctx, mutable) => {
6558
+ const payload = {
6559
+ hook_event_name: "PreToolUse",
6560
+ tool_name: ctx.tool,
6561
+ tool_input: mutable.args,
6562
+ session_id: ctx.sessionID,
6563
+ cwd: input.directory,
6564
+ meta: { agent: "Opencode" },
6565
+ };
6566
+ const r = spawnSync(NODE9_ARGV[0], [...NODE9_ARGV.slice(1), "check"], {
6567
+ input: JSON.stringify(payload),
6568
+ encoding: "utf-8",
6569
+ timeout: HOOK_TIMEOUT_MS,
6570
+ });
6571
+ if (r.status === 0) return;
6572
+ const reason = parseReason(r.stdout) || "blocked by node9";
6573
+ throw new Error("[node9] " + reason);
6574
+ },
6575
+
6576
+ "tool.execute.after": async (ctx) => {
6577
+ // Fire-and-forget audit log \u2014 failures here must NEVER throw,
6578
+ // or we'd retroactively "block" a tool call that already ran.
6579
+ const payload = {
6580
+ hook_event_name: "PostToolUse",
6581
+ tool_name: ctx.tool,
6582
+ session_id: ctx.sessionID,
6583
+ cwd: input.directory,
6584
+ meta: { agent: "Opencode" },
6585
+ };
6586
+ try {
6587
+ spawnSync(NODE9_ARGV[0], [...NODE9_ARGV.slice(1), "log"], {
6588
+ input: JSON.stringify(payload),
6589
+ encoding: "utf-8",
6590
+ timeout: LOG_TIMEOUT_MS,
6591
+ });
6592
+ } catch (e) {
6593
+ // Swallow: audit log gaps are preferable to crashing the agent.
6594
+ }
6595
+ },
6596
+
6597
+ "chat.message": async (ctx, mutable) => {
6598
+ const prompt = extractPromptText(mutable.parts);
6599
+ if (!prompt) return;
6600
+ const payload = {
6601
+ hook_event_name: "UserPromptSubmit",
6602
+ prompt,
6603
+ session_id: ctx.sessionID,
6604
+ meta: { agent: "Opencode" },
6605
+ };
6606
+ const r = spawnSync(NODE9_ARGV[0], [...NODE9_ARGV.slice(1), "check"], {
6607
+ input: JSON.stringify(payload),
6608
+ encoding: "utf-8",
6609
+ timeout: HOOK_TIMEOUT_MS,
6610
+ });
6611
+ if (r.status === 0) return;
6612
+ const reason = parseReason(r.stdout) || "prompt blocked";
6613
+ throw new Error("[node9] " + reason);
6614
+ },
6615
+ }),
6616
+ };
6617
+ `;
6618
+ }
6619
+ var init_setup_opencode_shim = __esm({
6620
+ "src/setup-opencode-shim.ts"() {
6621
+ "use strict";
6622
+ }
6623
+ });
6624
+
6441
6625
  // src/setup.ts
6442
6626
  import fs13 from "fs";
6443
6627
  import path15 from "path";
@@ -6900,6 +7084,18 @@ function claudeDesktopConfigPath(homeDir2 = os12.homedir()) {
6900
7084
  }
6901
7085
  return null;
6902
7086
  }
7087
+ function binaryInPath(binary) {
7088
+ const pathEnv = process.env.PATH ?? "";
7089
+ for (const dir of pathEnv.split(path15.delimiter)) {
7090
+ if (!dir) continue;
7091
+ try {
7092
+ fs13.accessSync(path15.join(dir, binary), fs13.constants.X_OK);
7093
+ return true;
7094
+ } catch {
7095
+ }
7096
+ }
7097
+ return false;
7098
+ }
6903
7099
  function detectAgents(homeDir2 = os12.homedir()) {
6904
7100
  const exists = (p) => {
6905
7101
  try {
@@ -6921,7 +7117,10 @@ function detectAgents(homeDir2 = os12.homedir()) {
6921
7117
  codex: exists(path15.join(homeDir2, ".codex")),
6922
7118
  windsurf: exists(path15.join(homeDir2, ".codeium", "windsurf")),
6923
7119
  vscode: exists(path15.join(homeDir2, ".vscode")),
6924
- claudeDesktop: desktopPath !== null && exists(path15.dirname(desktopPath))
7120
+ claudeDesktop: desktopPath !== null && exists(path15.dirname(desktopPath)),
7121
+ // Opencode creates ~/.config/opencode lazily on first launch — fall back
7122
+ // to a PATH lookup so installed-but-never-launched CLIs are still wired.
7123
+ opencode: exists(path15.join(homeDir2, ".config", "opencode")) || binaryInPath("opencode")
6925
7124
  };
6926
7125
  }
6927
7126
  async function setupCursor() {
@@ -7554,6 +7753,127 @@ function teardownClaudeDesktop() {
7554
7753
  console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in Claude Desktop config"));
7555
7754
  }
7556
7755
  }
7756
+ function node9ArgvForShim() {
7757
+ if (process.env.NODE9_TESTING === "1") return ["node9"];
7758
+ const nodeExec = process.execPath;
7759
+ const cliScript = process.argv[1];
7760
+ if (cliScript && cliScript.endsWith(".js")) return [nodeExec, cliScript];
7761
+ return [cliScript];
7762
+ }
7763
+ function node9Version() {
7764
+ try {
7765
+ const pkg = JSON.parse(
7766
+ fs13.readFileSync(path15.join(__dirname, "..", "package.json"), "utf-8")
7767
+ );
7768
+ return pkg.version ?? "0.0.0";
7769
+ } catch {
7770
+ return "0.0.0";
7771
+ }
7772
+ }
7773
+ async function setupOpencode() {
7774
+ seedMcpPinsIfMissing();
7775
+ const homeDir2 = os12.homedir();
7776
+ const configDir = path15.join(homeDir2, ".config", "opencode");
7777
+ const pluginsDir = path15.join(configDir, "plugins");
7778
+ const configPath = path15.join(configDir, "opencode.json");
7779
+ const pluginPath = path15.join(pluginsDir, OPENCODE_PLUGIN_NAME);
7780
+ try {
7781
+ fs13.mkdirSync(pluginsDir, { recursive: true });
7782
+ } catch (err2) {
7783
+ const code = err2.code;
7784
+ if (code !== "EEXIST") {
7785
+ console.log(chalk.yellow(` \u26A0\uFE0F Could not create ${pluginsDir}: ${code ?? String(err2)}`));
7786
+ return;
7787
+ }
7788
+ }
7789
+ const shimContent = renderOpencodeShim({
7790
+ node9Argv: node9ArgvForShim(),
7791
+ version: node9Version()
7792
+ });
7793
+ let pluginChanged = false;
7794
+ const existingShim = (() => {
7795
+ try {
7796
+ return fs13.readFileSync(pluginPath, "utf-8");
7797
+ } catch {
7798
+ return null;
7799
+ }
7800
+ })();
7801
+ if (existingShim !== shimContent) {
7802
+ fs13.writeFileSync(pluginPath, shimContent);
7803
+ pluginChanged = true;
7804
+ if (existingShim) {
7805
+ console.log(chalk.yellow(" \u{1F527} Opencode plugin shim updated to current version"));
7806
+ } else {
7807
+ console.log(
7808
+ chalk.green(" \u2705 Opencode plugin installed \u2192 tool.execute.before / after, chat.message")
7809
+ );
7810
+ }
7811
+ }
7812
+ const config = readJson(configPath) ?? {};
7813
+ const mcp = config.mcp ?? {};
7814
+ let configChanged = false;
7815
+ const desiredCommand = [...node9ArgvForShim(), "mcp-server"];
7816
+ const desiredEntry = {
7817
+ type: "local",
7818
+ command: desiredCommand,
7819
+ enabled: true
7820
+ };
7821
+ const existing = mcp["node9"];
7822
+ 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;
7823
+ if (!entryMatches) {
7824
+ mcp["node9"] = desiredEntry;
7825
+ config.mcp = mcp;
7826
+ configChanged = true;
7827
+ if (existing) {
7828
+ console.log(chalk.yellow(" \u{1F527} Opencode MCP entry updated (node9)"));
7829
+ } else {
7830
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
7831
+ }
7832
+ }
7833
+ if (configChanged) writeJson(configPath, config);
7834
+ if (pluginChanged || configChanged) {
7835
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Opencode!"));
7836
+ console.log(chalk.gray(" Restart Opencode for changes to take effect."));
7837
+ printDaemonTip();
7838
+ } else {
7839
+ console.log(chalk.blue(" \u2139\uFE0F Node9 is already fully configured for Opencode."));
7840
+ }
7841
+ }
7842
+ function teardownOpencode() {
7843
+ const homeDir2 = os12.homedir();
7844
+ const configDir = path15.join(homeDir2, ".config", "opencode");
7845
+ const pluginsDir = path15.join(configDir, "plugins");
7846
+ const configPath = path15.join(configDir, "opencode.json");
7847
+ const pluginPath = path15.join(pluginsDir, OPENCODE_PLUGIN_NAME);
7848
+ try {
7849
+ if (fs13.existsSync(pluginPath)) {
7850
+ fs13.unlinkSync(pluginPath);
7851
+ console.log(chalk.green(" \u2705 Removed node9 plugin from ~/.config/opencode/plugins/"));
7852
+ }
7853
+ } catch (err2) {
7854
+ console.log(chalk.yellow(` \u26A0\uFE0F Could not remove ${pluginPath}: ${String(err2)}`));
7855
+ }
7856
+ const config = readJson(configPath);
7857
+ if (!config) {
7858
+ console.log(chalk.blue(" \u2139\uFE0F ~/.config/opencode/opencode.json not found \u2014 nothing to remove"));
7859
+ return;
7860
+ }
7861
+ const mcp = config.mcp ?? {};
7862
+ let changed = false;
7863
+ if (mcp["node9"]) {
7864
+ delete mcp["node9"];
7865
+ changed = true;
7866
+ console.log(
7867
+ chalk.green(" \u2705 Removed node9 MCP server entry from ~/.config/opencode/opencode.json")
7868
+ );
7869
+ }
7870
+ if (changed) {
7871
+ config.mcp = mcp;
7872
+ writeJson(configPath, config);
7873
+ } else {
7874
+ console.log(chalk.blue(" \u2139\uFE0F No node9 entries found in ~/.config/opencode/opencode.json"));
7875
+ }
7876
+ }
7557
7877
  function getAgentsStatus(homeDir2 = os12.homedir()) {
7558
7878
  const detected = detectAgents(homeDir2);
7559
7879
  const claudeWired = (() => {
@@ -7636,16 +7956,38 @@ function getAgentsStatus(homeDir2 = os12.homedir()) {
7636
7956
  return !!(cfg?.mcpServers && hasNode9McpServer(cfg.mcpServers));
7637
7957
  })(),
7638
7958
  mode: detected.claudeDesktop ? "mcp" : null
7959
+ },
7960
+ {
7961
+ name: "opencode",
7962
+ label: "Opencode",
7963
+ installed: detected.opencode,
7964
+ wired: (() => {
7965
+ const pluginPath = path15.join(
7966
+ homeDir2,
7967
+ ".config",
7968
+ "opencode",
7969
+ "plugins",
7970
+ OPENCODE_PLUGIN_NAME
7971
+ );
7972
+ if (fs13.existsSync(pluginPath)) return true;
7973
+ const cfg = readJson(
7974
+ path15.join(homeDir2, ".config", "opencode", "opencode.json")
7975
+ );
7976
+ return !!cfg?.mcp?.["node9"];
7977
+ })(),
7978
+ mode: detected.opencode ? "hooks" : null
7639
7979
  }
7640
7980
  ];
7641
7981
  }
7642
- var NODE9_MCP_SERVER_ENTRY, CODEX_PRE_TOOL_MATCHERS;
7982
+ var NODE9_MCP_SERVER_ENTRY, CODEX_PRE_TOOL_MATCHERS, OPENCODE_PLUGIN_NAME;
7643
7983
  var init_setup = __esm({
7644
7984
  "src/setup.ts"() {
7645
7985
  "use strict";
7646
7986
  init_mcp_pin();
7987
+ init_setup_opencode_shim();
7647
7988
  NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7648
7989
  CODEX_PRE_TOOL_MATCHERS = ["^Bash$", "^apply_patch$", "^mcp__.*"];
7990
+ OPENCODE_PLUGIN_NAME = "node9.js";
7649
7991
  }
7650
7992
  });
7651
7993
 
@@ -15944,6 +16286,11 @@ function sanitize2(value) {
15944
16286
  return value.replace(/[\x00-\x1F\x7F]/g, "");
15945
16287
  }
15946
16288
  function detectAiAgent(payload) {
16289
+ const meta = payload.meta;
16290
+ if (meta && typeof meta === "object") {
16291
+ const tagged = meta.agent;
16292
+ if (typeof tagged === "string" && tagged.length > 0) return tagged;
16293
+ }
15947
16294
  if (payload.turn_id !== void 0) {
15948
16295
  return "Codex";
15949
16296
  }
@@ -18451,14 +18798,18 @@ import path39 from "path";
18451
18798
  import os34 from "os";
18452
18799
  import https4 from "https";
18453
18800
  var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "project-jail"];
18454
- function fireTelemetryPing(agents) {
18801
+ function buildTelemetryPayload(agents, firstInstall) {
18802
+ return {
18803
+ event: "init_completed",
18804
+ agents_detected: agents,
18805
+ os: process.platform,
18806
+ node9_version: node9Version(),
18807
+ first_install: firstInstall
18808
+ };
18809
+ }
18810
+ function fireTelemetryPing(agents, firstInstall) {
18455
18811
  try {
18456
- const body = JSON.stringify({
18457
- event: "init_completed",
18458
- agents_detected: agents,
18459
- os: process.platform,
18460
- node9_version: process.env.npm_package_version ?? "unknown"
18461
- });
18812
+ const body = JSON.stringify(buildTelemetryPayload(agents, firstInstall));
18462
18813
  const req = https4.request(
18463
18814
  {
18464
18815
  hostname: "api.node9.ai",
@@ -18491,6 +18842,12 @@ function registerInitCommand(program2) {
18491
18842
  ).action(
18492
18843
  async (options) => {
18493
18844
  console.log(chalk16.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
18845
+ {
18846
+ const migrated = migrateRenamedRuleKeys();
18847
+ for (const m of migrated) {
18848
+ console.log(chalk16.dim(` \u{1F527} Rule renamed: ${m.oldKey} \u2192 ${m.newKey}`));
18849
+ }
18850
+ }
18494
18851
  let chosenMode = options.mode.toLowerCase();
18495
18852
  if (!["standard", "strict", "audit", "observe"].includes(chosenMode)) {
18496
18853
  chosenMode = DEFAULT_CONFIG.settings.mode;
@@ -18525,6 +18882,7 @@ function registerInitCommand(program2) {
18525
18882
  console.log("");
18526
18883
  }
18527
18884
  const configPath = path39.join(os34.homedir(), ".node9", "config.json");
18885
+ const isFirstInstall = !fs38.existsSync(configPath);
18528
18886
  if (fs38.existsSync(configPath) && !options.force) {
18529
18887
  try {
18530
18888
  const existing = JSON.parse(fs38.readFileSync(configPath, "utf-8"));
@@ -18582,6 +18940,7 @@ function registerInitCommand(program2) {
18582
18940
  else if (agent === "windsurf") await setupWindsurf();
18583
18941
  else if (agent === "vscode") await setupVSCode();
18584
18942
  else if (agent === "claudeDesktop") await setupClaudeDesktop();
18943
+ else if (agent === "opencode") await setupOpencode();
18585
18944
  console.log("");
18586
18945
  }
18587
18946
  if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
@@ -18627,7 +18986,7 @@ function registerInitCommand(program2) {
18627
18986
  message: "Send anonymous usage stats to help improve node9? (no code, no args)",
18628
18987
  default: true
18629
18988
  });
18630
- if (sendTelemetry) fireTelemetryPing(found);
18989
+ if (sendTelemetry) fireTelemetryPing(found, isFirstInstall);
18631
18990
  console.log("");
18632
18991
  }
18633
18992
  const agentList = found.join(", ");
@@ -20341,7 +20700,8 @@ var SETUP_FN = {
20341
20700
  codex: setupCodex,
20342
20701
  windsurf: setupWindsurf,
20343
20702
  vscode: setupVSCode,
20344
- claudeDesktop: setupClaudeDesktop
20703
+ claudeDesktop: setupClaudeDesktop,
20704
+ opencode: setupOpencode
20345
20705
  };
20346
20706
  var TEARDOWN_FN = {
20347
20707
  claude: teardownClaude,
@@ -20350,7 +20710,8 @@ var TEARDOWN_FN = {
20350
20710
  codex: teardownCodex,
20351
20711
  windsurf: teardownWindsurf,
20352
20712
  vscode: teardownVSCode,
20353
- claudeDesktop: teardownClaudeDesktop
20713
+ claudeDesktop: teardownClaudeDesktop,
20714
+ opencode: teardownOpencode
20354
20715
  };
20355
20716
  var AGENT_NAMES = Object.keys(SETUP_FN);
20356
20717
  function registerAgentsCommand(program2) {