@node9/proxy 1.26.3 → 1.27.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
@@ -6647,6 +6647,200 @@ var init_setup_opencode_shim = __esm({
6647
6647
  }
6648
6648
  });
6649
6649
 
6650
+ // src/setup-pi-shim.ts
6651
+ function renderPiShim(input) {
6652
+ const { node9Argv, version: version2 } = input;
6653
+ return `// Auto-generated by \`node9 init\`. Do not edit \u2014 re-run init to upgrade.
6654
+ // NODE9_SHIM_VERSION = "${version2}"
6655
+ //
6656
+ // node9 protection shim for Pi (https://pi.dev). Wires four hooks
6657
+ // against the agent's extension API and shells out to the node9 CLI
6658
+ // for verdicts.
6659
+ //
6660
+ // Contract (verified against opensources/pi-main):
6661
+ // - tool_call \u2192 return { block: true, reason } to block (NOT throw)
6662
+ // - tool_result \u2192 audit fire-and-forget
6663
+ // - input \u2192 return { action: "handled" } to block, { action: "continue" } to allow
6664
+ // - user_bash \u2192 return synthetic BashResult { output, exitCode: 1, ... } to block
6665
+ // (UserBashEventResult has no block field by design)
6666
+
6667
+ const { spawnSync } = require("node:child_process");
6668
+ const fs = require("node:fs");
6669
+ const path = require("node:path");
6670
+ const os = require("node:os");
6671
+
6672
+ function debugLog(entry) {
6673
+ // Best-effort write to ~/.node9/hook-debug.log so audit-log failures
6674
+ // in the tool_result catch leave a breadcrumb. Pi has no equivalent
6675
+ // of node9's own hook-debug.log path, and silent swallow makes
6676
+ // misconfiguration invisible until block-rate dashboards catch on.
6677
+ // Wrapped in try because this hook NEVER throws \u2014 see catch comment.
6678
+ try {
6679
+ const logPath = path.join(os.homedir(), ".node9", "hook-debug.log");
6680
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
6681
+ fs.appendFileSync(logPath, JSON.stringify({
6682
+ ts: new Date().toISOString(),
6683
+ source: "pi-shim",
6684
+ ...entry,
6685
+ }) + "\\n");
6686
+ } catch (_) {
6687
+ // ignore \u2014 last-resort breadcrumb is best-effort
6688
+ }
6689
+ }
6690
+
6691
+ // argv prefix for invoking the node9 CLI. argv[0] is the executable
6692
+ // (either the npm-installed wrapper script or the node binary); the
6693
+ // remaining entries (if any) are passed as the leading args before
6694
+ // the subcommand name (e.g. ["/path/to/dist/cli.js"] for dev mode).
6695
+ const NODE9_ARGV = ${JSON.stringify(node9Argv)};
6696
+ const HOOK_TIMEOUT_MS = 30000;
6697
+ const LOG_TIMEOUT_MS = 5000;
6698
+
6699
+ // Pi tool names \u2192 Claude tool names. Policy rules in ~/.node9/config.yaml
6700
+ // match against Claude's PascalCase names (Bash/Read/Edit/Write/...).
6701
+ // Without this mapping every DLP and project-jail rule silently no-ops
6702
+ // for pi payloads (design R5).
6703
+ const TOOL_NAME_MAP = {
6704
+ bash: "Bash",
6705
+ read: "Read",
6706
+ edit: "Edit",
6707
+ write: "Write",
6708
+ grep: "Grep",
6709
+ find: "Glob",
6710
+ ls: "LS",
6711
+ };
6712
+
6713
+ function normalizeToolName(piName) {
6714
+ return TOOL_NAME_MAP[piName] || piName;
6715
+ }
6716
+
6717
+ function parseReason(stdout) {
6718
+ // node9 check emits {decision, reason, message} JSON on stdout when
6719
+ // blocking. Fall back to a generic string if anything goes wrong.
6720
+ try {
6721
+ const v = JSON.parse(stdout || "");
6722
+ return v && (v.reason || v.message);
6723
+ } catch (e) {
6724
+ return null;
6725
+ }
6726
+ }
6727
+
6728
+ function runCheck(payload, timeoutMs) {
6729
+ return spawnSync(NODE9_ARGV[0], [...NODE9_ARGV.slice(1), "check"], {
6730
+ input: JSON.stringify(payload),
6731
+ encoding: "utf-8",
6732
+ timeout: timeoutMs,
6733
+ });
6734
+ }
6735
+
6736
+ module.exports = function (pi) {
6737
+ pi.on("tool_call", async (event, ctx) => {
6738
+ const payload = {
6739
+ hook_event_name: "PreToolUse",
6740
+ tool_name: normalizeToolName(event.toolName),
6741
+ tool_input: event.input,
6742
+ cwd: ctx.cwd,
6743
+ meta: { agent: "Pi" },
6744
+ };
6745
+ const r = runCheck(payload, HOOK_TIMEOUT_MS);
6746
+ if (r.status === 0) return undefined;
6747
+ const reason = parseReason(r.stdout) || "blocked by node9";
6748
+ return { block: true, reason: "[node9] " + reason };
6749
+ });
6750
+
6751
+ pi.on("tool_result", async (event, ctx) => {
6752
+ // Fire-and-forget audit log. Failures here must NEVER throw or
6753
+ // return an error result \u2014 we'd retroactively "fail" a tool call
6754
+ // that already completed.
6755
+ const payload = {
6756
+ hook_event_name: "PostToolUse",
6757
+ tool_name: normalizeToolName(event.toolName),
6758
+ tool_input: event.input,
6759
+ cwd: ctx.cwd,
6760
+ meta: { agent: "Pi" },
6761
+ };
6762
+ try {
6763
+ spawnSync(NODE9_ARGV[0], [...NODE9_ARGV.slice(1), "log"], {
6764
+ input: JSON.stringify(payload),
6765
+ encoding: "utf-8",
6766
+ timeout: LOG_TIMEOUT_MS,
6767
+ });
6768
+ } catch (e) {
6769
+ // Swallow + breadcrumb. Audit log gaps are preferable to crashing
6770
+ // the agent, but a persistent failure here (e.g. NODE9_ARGV[0]
6771
+ // no longer exists after a node-version bump) used to be invisible
6772
+ // because pi has no hook-debug surface. Write a one-line entry to
6773
+ // ~/.node9/hook-debug.log so dashboards can catch silent drift.
6774
+ debugLog({
6775
+ event: "tool_result-spawn-failed",
6776
+ tool: payload.tool_name,
6777
+ agent: "Pi",
6778
+ error: e && e.message ? e.message : String(e),
6779
+ });
6780
+ }
6781
+ return undefined;
6782
+ });
6783
+
6784
+ pi.on("input", async (event, ctx) => {
6785
+ // Pre-prompt DLP. event.text is the user's typed/pasted prompt.
6786
+ // Block via { action: "handled" }; allow via { action: "continue" }.
6787
+ // We do NOT use { action: "transform" } in v1 \u2014 block-only mirrors
6788
+ // Claude/Codex/Opencode behavior. Redact-mode is a future option.
6789
+ const payload = {
6790
+ hook_event_name: "UserPromptSubmit",
6791
+ prompt: event.text,
6792
+ cwd: ctx.cwd,
6793
+ meta: { agent: "Pi" },
6794
+ };
6795
+ const r = runCheck(payload, HOOK_TIMEOUT_MS);
6796
+ if (r.status === 0) return { action: "continue" };
6797
+ const reason = parseReason(r.stdout) || "prompt blocked";
6798
+ if (ctx.hasUI) {
6799
+ ctx.ui.notify("[node9] " + reason, "error");
6800
+ }
6801
+ return { action: "handled" };
6802
+ });
6803
+
6804
+ pi.on("user_bash", async (event, ctx) => {
6805
+ // The !/!! prompt-escape side channel (design R4). Synthesize a
6806
+ // Bash-shaped PreToolUse payload so the same dangerous-words / DLP
6807
+ // rules that gate tool_call(bash) also gate user_bash.
6808
+ //
6809
+ // UserBashEventResult has no { block } field \u2014 by design, since
6810
+ // pi.on("user_bash") was meant for execution-replacement. We block
6811
+ // by returning a synthetic BashResult that looks like the command
6812
+ // failed with our reason as the output (exitCode: 1).
6813
+ const payload = {
6814
+ hook_event_name: "PreToolUse",
6815
+ tool_name: "Bash",
6816
+ tool_input: { command: event.command },
6817
+ cwd: event.cwd,
6818
+ meta: { agent: "Pi" },
6819
+ };
6820
+ const r = runCheck(payload, HOOK_TIMEOUT_MS);
6821
+ if (r.status === 0) return undefined;
6822
+ const reason = parseReason(r.stdout) || "blocked by node9";
6823
+ if (ctx.hasUI) {
6824
+ ctx.ui.notify("[node9] " + reason, "error");
6825
+ }
6826
+ return {
6827
+ result: {
6828
+ output: "[node9] blocked: " + reason,
6829
+ exitCode: 1,
6830
+ cancelled: false,
6831
+ truncated: false,
6832
+ },
6833
+ };
6834
+ });
6835
+ };
6836
+ `;
6837
+ }
6838
+ var init_setup_pi_shim = __esm({
6839
+ "src/setup-pi-shim.ts"() {
6840
+ "use strict";
6841
+ }
6842
+ });
6843
+
6650
6844
  // src/setup.ts
6651
6845
  function hasNode9McpServer(servers) {
6652
6846
  const entry = servers["node9"];
@@ -7139,7 +7333,14 @@ function detectAgents(homeDir2 = import_os12.default.homedir()) {
7139
7333
  claudeDesktop: desktopPath !== null && exists(import_path15.default.dirname(desktopPath)),
7140
7334
  // Opencode creates ~/.config/opencode lazily on first launch — fall back
7141
7335
  // to a PATH lookup so installed-but-never-launched CLIs are still wired.
7142
- opencode: exists(import_path15.default.join(homeDir2, ".config", "opencode")) || binaryInPath("opencode")
7336
+ opencode: exists(import_path15.default.join(homeDir2, ".config", "opencode")) || binaryInPath("opencode"),
7337
+ // Pi (https://pi.dev): config dir is ~/.pi/agent (CONFIG_DIR_NAME=".pi"
7338
+ // + agentDir, verified against opensources/pi-main/packages/coding-agent/
7339
+ // src/config.ts:449,475). The Bun-compiled binary path may create this
7340
+ // dir lazily on first launch — same class of bug as opencode's #186
7341
+ // (design R6) — so fall back to PATH lookup for installed-but-never-
7342
+ // launched pi.
7343
+ pi: exists(import_path15.default.join(homeDir2, ".pi", "agent")) || binaryInPath("pi")
7143
7344
  };
7144
7345
  }
7145
7346
  async function setupCursor() {
@@ -7893,6 +8094,59 @@ function teardownOpencode() {
7893
8094
  console.log(import_chalk.default.blue(" \u2139\uFE0F No node9 entries found in ~/.config/opencode/opencode.json"));
7894
8095
  }
7895
8096
  }
8097
+ async function setupPi() {
8098
+ seedMcpPinsIfMissing();
8099
+ const homeDir2 = import_os12.default.homedir();
8100
+ const extensionsDir = import_path15.default.join(homeDir2, ".pi", "agent", "extensions");
8101
+ const extensionPath = import_path15.default.join(extensionsDir, PI_EXTENSION_NAME);
8102
+ try {
8103
+ import_fs13.default.mkdirSync(extensionsDir, { recursive: true });
8104
+ } catch (err2) {
8105
+ const code = err2.code;
8106
+ console.log(import_chalk.default.yellow(` \u26A0\uFE0F Could not create ${extensionsDir}: ${code ?? String(err2)}`));
8107
+ return;
8108
+ }
8109
+ const shimContent = renderPiShim({
8110
+ node9Argv: node9ArgvForShim(),
8111
+ version: node9Version()
8112
+ });
8113
+ const existingShim = (() => {
8114
+ try {
8115
+ return import_fs13.default.readFileSync(extensionPath, "utf-8");
8116
+ } catch {
8117
+ return null;
8118
+ }
8119
+ })();
8120
+ if (existingShim === shimContent) {
8121
+ console.log(import_chalk.default.blue(" \u2139\uFE0F Node9 is already fully configured for Pi."));
8122
+ return;
8123
+ }
8124
+ import_fs13.default.writeFileSync(extensionPath, shimContent);
8125
+ if (existingShim) {
8126
+ console.log(import_chalk.default.yellow(" \u{1F527} Pi extension shim updated to current version"));
8127
+ } else {
8128
+ console.log(
8129
+ import_chalk.default.green(" \u2705 Pi extension installed \u2192 tool_call / tool_result / input / user_bash")
8130
+ );
8131
+ }
8132
+ console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Pi!"));
8133
+ console.log(import_chalk.default.gray(" Restart Pi for changes to take effect."));
8134
+ printDaemonTip();
8135
+ }
8136
+ function teardownPi() {
8137
+ const homeDir2 = import_os12.default.homedir();
8138
+ const extensionPath = import_path15.default.join(homeDir2, ".pi", "agent", "extensions", PI_EXTENSION_NAME);
8139
+ try {
8140
+ if (import_fs13.default.existsSync(extensionPath)) {
8141
+ import_fs13.default.unlinkSync(extensionPath);
8142
+ console.log(import_chalk.default.green(" \u2705 Removed node9 extension from ~/.pi/agent/extensions/"));
8143
+ } else {
8144
+ console.log(import_chalk.default.blue(" \u2139\uFE0F No Pi extension installed \u2014 nothing to remove"));
8145
+ }
8146
+ } catch (err2) {
8147
+ console.log(import_chalk.default.yellow(` \u26A0\uFE0F Could not remove ${extensionPath}: ${String(err2)}`));
8148
+ }
8149
+ }
7896
8150
  function getAgentsStatus(homeDir2 = import_os12.default.homedir()) {
7897
8151
  const detected = detectAgents(homeDir2);
7898
8152
  const claudeWired = (() => {
@@ -7995,10 +8249,19 @@ function getAgentsStatus(homeDir2 = import_os12.default.homedir()) {
7995
8249
  return !!cfg?.mcp?.["node9"];
7996
8250
  })(),
7997
8251
  mode: detected.opencode ? "hooks" : null
8252
+ },
8253
+ {
8254
+ name: "pi",
8255
+ label: "Pi",
8256
+ installed: detected.pi,
8257
+ // Pi has no MCP path — only the extension file. "wired" is a
8258
+ // simple existence check on the canonical install location.
8259
+ wired: import_fs13.default.existsSync(import_path15.default.join(homeDir2, ".pi", "agent", "extensions", PI_EXTENSION_NAME)),
8260
+ mode: detected.pi ? "hooks" : null
7998
8261
  }
7999
8262
  ];
8000
8263
  }
8001
- 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;
8264
+ 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, PI_EXTENSION_NAME;
8002
8265
  var init_setup = __esm({
8003
8266
  "src/setup.ts"() {
8004
8267
  "use strict";
@@ -8010,9 +8273,11 @@ var init_setup = __esm({
8010
8273
  import_smol_toml = require("smol-toml");
8011
8274
  init_mcp_pin();
8012
8275
  init_setup_opencode_shim();
8276
+ init_setup_pi_shim();
8013
8277
  NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
8014
8278
  CODEX_PRE_TOOL_MATCHERS = ["^Bash$", "^apply_patch$", "^mcp__.*"];
8015
8279
  OPENCODE_PLUGIN_NAME = "node9.js";
8280
+ PI_EXTENSION_NAME = "node9.js";
8016
8281
  }
8017
8282
  });
8018
8283
 
@@ -16812,7 +17077,15 @@ function registerLogCommand(program2) {
16812
17077
  const payload = JSON.parse(raw);
16813
17078
  const tool = sanitize3(payload.tool_name ?? payload.name ?? "unknown");
16814
17079
  const rawInput = payload.tool_input ?? payload.args ?? {};
16815
- const agent = payload.turn_id !== void 0 ? "Codex" : payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0 ? "Claude Code" : payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0 ? "Gemini CLI" : void 0;
17080
+ const metaTag = (() => {
17081
+ const m = payload.meta;
17082
+ if (m && typeof m === "object") {
17083
+ const tagged = m.agent;
17084
+ if (typeof tagged === "string" && tagged.length > 0) return tagged;
17085
+ }
17086
+ return void 0;
17087
+ })();
17088
+ const agent = metaTag !== void 0 ? metaTag : payload.turn_id !== void 0 ? "Codex" : payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0 ? "Claude Code" : payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0 ? "Gemini CLI" : void 0;
16816
17089
  const entry = {
16817
17090
  ts: (/* @__PURE__ */ new Date()).toISOString(),
16818
17091
  tool,
@@ -18969,6 +19242,7 @@ function registerInitCommand(program2) {
18969
19242
  else if (agent === "vscode") await setupVSCode();
18970
19243
  else if (agent === "claudeDesktop") await setupClaudeDesktop();
18971
19244
  else if (agent === "opencode") await setupOpencode();
19245
+ else if (agent === "pi") await setupPi();
18972
19246
  console.log("");
18973
19247
  }
18974
19248
  if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
@@ -20729,7 +21003,8 @@ var SETUP_FN = {
20729
21003
  windsurf: setupWindsurf,
20730
21004
  vscode: setupVSCode,
20731
21005
  claudeDesktop: setupClaudeDesktop,
20732
- opencode: setupOpencode
21006
+ opencode: setupOpencode,
21007
+ pi: setupPi
20733
21008
  };
20734
21009
  var TEARDOWN_FN = {
20735
21010
  claude: teardownClaude,
@@ -20739,7 +21014,8 @@ var TEARDOWN_FN = {
20739
21014
  windsurf: teardownWindsurf,
20740
21015
  vscode: teardownVSCode,
20741
21016
  claudeDesktop: teardownClaudeDesktop,
20742
- opencode: teardownOpencode
21017
+ opencode: teardownOpencode,
21018
+ pi: teardownPi
20743
21019
  };
20744
21020
  var AGENT_NAMES = Object.keys(SETUP_FN);
20745
21021
  function registerAgentsCommand(program2) {
package/dist/cli.mjs CHANGED
@@ -6622,6 +6622,200 @@ var init_setup_opencode_shim = __esm({
6622
6622
  }
6623
6623
  });
6624
6624
 
6625
+ // src/setup-pi-shim.ts
6626
+ function renderPiShim(input) {
6627
+ const { node9Argv, version: version2 } = input;
6628
+ return `// Auto-generated by \`node9 init\`. Do not edit \u2014 re-run init to upgrade.
6629
+ // NODE9_SHIM_VERSION = "${version2}"
6630
+ //
6631
+ // node9 protection shim for Pi (https://pi.dev). Wires four hooks
6632
+ // against the agent's extension API and shells out to the node9 CLI
6633
+ // for verdicts.
6634
+ //
6635
+ // Contract (verified against opensources/pi-main):
6636
+ // - tool_call \u2192 return { block: true, reason } to block (NOT throw)
6637
+ // - tool_result \u2192 audit fire-and-forget
6638
+ // - input \u2192 return { action: "handled" } to block, { action: "continue" } to allow
6639
+ // - user_bash \u2192 return synthetic BashResult { output, exitCode: 1, ... } to block
6640
+ // (UserBashEventResult has no block field by design)
6641
+
6642
+ const { spawnSync } = require("node:child_process");
6643
+ const fs = require("node:fs");
6644
+ const path = require("node:path");
6645
+ const os = require("node:os");
6646
+
6647
+ function debugLog(entry) {
6648
+ // Best-effort write to ~/.node9/hook-debug.log so audit-log failures
6649
+ // in the tool_result catch leave a breadcrumb. Pi has no equivalent
6650
+ // of node9's own hook-debug.log path, and silent swallow makes
6651
+ // misconfiguration invisible until block-rate dashboards catch on.
6652
+ // Wrapped in try because this hook NEVER throws \u2014 see catch comment.
6653
+ try {
6654
+ const logPath = path.join(os.homedir(), ".node9", "hook-debug.log");
6655
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
6656
+ fs.appendFileSync(logPath, JSON.stringify({
6657
+ ts: new Date().toISOString(),
6658
+ source: "pi-shim",
6659
+ ...entry,
6660
+ }) + "\\n");
6661
+ } catch (_) {
6662
+ // ignore \u2014 last-resort breadcrumb is best-effort
6663
+ }
6664
+ }
6665
+
6666
+ // argv prefix for invoking the node9 CLI. argv[0] is the executable
6667
+ // (either the npm-installed wrapper script or the node binary); the
6668
+ // remaining entries (if any) are passed as the leading args before
6669
+ // the subcommand name (e.g. ["/path/to/dist/cli.js"] for dev mode).
6670
+ const NODE9_ARGV = ${JSON.stringify(node9Argv)};
6671
+ const HOOK_TIMEOUT_MS = 30000;
6672
+ const LOG_TIMEOUT_MS = 5000;
6673
+
6674
+ // Pi tool names \u2192 Claude tool names. Policy rules in ~/.node9/config.yaml
6675
+ // match against Claude's PascalCase names (Bash/Read/Edit/Write/...).
6676
+ // Without this mapping every DLP and project-jail rule silently no-ops
6677
+ // for pi payloads (design R5).
6678
+ const TOOL_NAME_MAP = {
6679
+ bash: "Bash",
6680
+ read: "Read",
6681
+ edit: "Edit",
6682
+ write: "Write",
6683
+ grep: "Grep",
6684
+ find: "Glob",
6685
+ ls: "LS",
6686
+ };
6687
+
6688
+ function normalizeToolName(piName) {
6689
+ return TOOL_NAME_MAP[piName] || piName;
6690
+ }
6691
+
6692
+ function parseReason(stdout) {
6693
+ // node9 check emits {decision, reason, message} JSON on stdout when
6694
+ // blocking. Fall back to a generic string if anything goes wrong.
6695
+ try {
6696
+ const v = JSON.parse(stdout || "");
6697
+ return v && (v.reason || v.message);
6698
+ } catch (e) {
6699
+ return null;
6700
+ }
6701
+ }
6702
+
6703
+ function runCheck(payload, timeoutMs) {
6704
+ return spawnSync(NODE9_ARGV[0], [...NODE9_ARGV.slice(1), "check"], {
6705
+ input: JSON.stringify(payload),
6706
+ encoding: "utf-8",
6707
+ timeout: timeoutMs,
6708
+ });
6709
+ }
6710
+
6711
+ module.exports = function (pi) {
6712
+ pi.on("tool_call", async (event, ctx) => {
6713
+ const payload = {
6714
+ hook_event_name: "PreToolUse",
6715
+ tool_name: normalizeToolName(event.toolName),
6716
+ tool_input: event.input,
6717
+ cwd: ctx.cwd,
6718
+ meta: { agent: "Pi" },
6719
+ };
6720
+ const r = runCheck(payload, HOOK_TIMEOUT_MS);
6721
+ if (r.status === 0) return undefined;
6722
+ const reason = parseReason(r.stdout) || "blocked by node9";
6723
+ return { block: true, reason: "[node9] " + reason };
6724
+ });
6725
+
6726
+ pi.on("tool_result", async (event, ctx) => {
6727
+ // Fire-and-forget audit log. Failures here must NEVER throw or
6728
+ // return an error result \u2014 we'd retroactively "fail" a tool call
6729
+ // that already completed.
6730
+ const payload = {
6731
+ hook_event_name: "PostToolUse",
6732
+ tool_name: normalizeToolName(event.toolName),
6733
+ tool_input: event.input,
6734
+ cwd: ctx.cwd,
6735
+ meta: { agent: "Pi" },
6736
+ };
6737
+ try {
6738
+ spawnSync(NODE9_ARGV[0], [...NODE9_ARGV.slice(1), "log"], {
6739
+ input: JSON.stringify(payload),
6740
+ encoding: "utf-8",
6741
+ timeout: LOG_TIMEOUT_MS,
6742
+ });
6743
+ } catch (e) {
6744
+ // Swallow + breadcrumb. Audit log gaps are preferable to crashing
6745
+ // the agent, but a persistent failure here (e.g. NODE9_ARGV[0]
6746
+ // no longer exists after a node-version bump) used to be invisible
6747
+ // because pi has no hook-debug surface. Write a one-line entry to
6748
+ // ~/.node9/hook-debug.log so dashboards can catch silent drift.
6749
+ debugLog({
6750
+ event: "tool_result-spawn-failed",
6751
+ tool: payload.tool_name,
6752
+ agent: "Pi",
6753
+ error: e && e.message ? e.message : String(e),
6754
+ });
6755
+ }
6756
+ return undefined;
6757
+ });
6758
+
6759
+ pi.on("input", async (event, ctx) => {
6760
+ // Pre-prompt DLP. event.text is the user's typed/pasted prompt.
6761
+ // Block via { action: "handled" }; allow via { action: "continue" }.
6762
+ // We do NOT use { action: "transform" } in v1 \u2014 block-only mirrors
6763
+ // Claude/Codex/Opencode behavior. Redact-mode is a future option.
6764
+ const payload = {
6765
+ hook_event_name: "UserPromptSubmit",
6766
+ prompt: event.text,
6767
+ cwd: ctx.cwd,
6768
+ meta: { agent: "Pi" },
6769
+ };
6770
+ const r = runCheck(payload, HOOK_TIMEOUT_MS);
6771
+ if (r.status === 0) return { action: "continue" };
6772
+ const reason = parseReason(r.stdout) || "prompt blocked";
6773
+ if (ctx.hasUI) {
6774
+ ctx.ui.notify("[node9] " + reason, "error");
6775
+ }
6776
+ return { action: "handled" };
6777
+ });
6778
+
6779
+ pi.on("user_bash", async (event, ctx) => {
6780
+ // The !/!! prompt-escape side channel (design R4). Synthesize a
6781
+ // Bash-shaped PreToolUse payload so the same dangerous-words / DLP
6782
+ // rules that gate tool_call(bash) also gate user_bash.
6783
+ //
6784
+ // UserBashEventResult has no { block } field \u2014 by design, since
6785
+ // pi.on("user_bash") was meant for execution-replacement. We block
6786
+ // by returning a synthetic BashResult that looks like the command
6787
+ // failed with our reason as the output (exitCode: 1).
6788
+ const payload = {
6789
+ hook_event_name: "PreToolUse",
6790
+ tool_name: "Bash",
6791
+ tool_input: { command: event.command },
6792
+ cwd: event.cwd,
6793
+ meta: { agent: "Pi" },
6794
+ };
6795
+ const r = runCheck(payload, HOOK_TIMEOUT_MS);
6796
+ if (r.status === 0) return undefined;
6797
+ const reason = parseReason(r.stdout) || "blocked by node9";
6798
+ if (ctx.hasUI) {
6799
+ ctx.ui.notify("[node9] " + reason, "error");
6800
+ }
6801
+ return {
6802
+ result: {
6803
+ output: "[node9] blocked: " + reason,
6804
+ exitCode: 1,
6805
+ cancelled: false,
6806
+ truncated: false,
6807
+ },
6808
+ };
6809
+ });
6810
+ };
6811
+ `;
6812
+ }
6813
+ var init_setup_pi_shim = __esm({
6814
+ "src/setup-pi-shim.ts"() {
6815
+ "use strict";
6816
+ }
6817
+ });
6818
+
6625
6819
  // src/setup.ts
6626
6820
  import fs13 from "fs";
6627
6821
  import path15 from "path";
@@ -7120,7 +7314,14 @@ function detectAgents(homeDir2 = os12.homedir()) {
7120
7314
  claudeDesktop: desktopPath !== null && exists(path15.dirname(desktopPath)),
7121
7315
  // Opencode creates ~/.config/opencode lazily on first launch — fall back
7122
7316
  // to a PATH lookup so installed-but-never-launched CLIs are still wired.
7123
- opencode: exists(path15.join(homeDir2, ".config", "opencode")) || binaryInPath("opencode")
7317
+ opencode: exists(path15.join(homeDir2, ".config", "opencode")) || binaryInPath("opencode"),
7318
+ // Pi (https://pi.dev): config dir is ~/.pi/agent (CONFIG_DIR_NAME=".pi"
7319
+ // + agentDir, verified against opensources/pi-main/packages/coding-agent/
7320
+ // src/config.ts:449,475). The Bun-compiled binary path may create this
7321
+ // dir lazily on first launch — same class of bug as opencode's #186
7322
+ // (design R6) — so fall back to PATH lookup for installed-but-never-
7323
+ // launched pi.
7324
+ pi: exists(path15.join(homeDir2, ".pi", "agent")) || binaryInPath("pi")
7124
7325
  };
7125
7326
  }
7126
7327
  async function setupCursor() {
@@ -7874,6 +8075,59 @@ function teardownOpencode() {
7874
8075
  console.log(chalk.blue(" \u2139\uFE0F No node9 entries found in ~/.config/opencode/opencode.json"));
7875
8076
  }
7876
8077
  }
8078
+ async function setupPi() {
8079
+ seedMcpPinsIfMissing();
8080
+ const homeDir2 = os12.homedir();
8081
+ const extensionsDir = path15.join(homeDir2, ".pi", "agent", "extensions");
8082
+ const extensionPath = path15.join(extensionsDir, PI_EXTENSION_NAME);
8083
+ try {
8084
+ fs13.mkdirSync(extensionsDir, { recursive: true });
8085
+ } catch (err2) {
8086
+ const code = err2.code;
8087
+ console.log(chalk.yellow(` \u26A0\uFE0F Could not create ${extensionsDir}: ${code ?? String(err2)}`));
8088
+ return;
8089
+ }
8090
+ const shimContent = renderPiShim({
8091
+ node9Argv: node9ArgvForShim(),
8092
+ version: node9Version()
8093
+ });
8094
+ const existingShim = (() => {
8095
+ try {
8096
+ return fs13.readFileSync(extensionPath, "utf-8");
8097
+ } catch {
8098
+ return null;
8099
+ }
8100
+ })();
8101
+ if (existingShim === shimContent) {
8102
+ console.log(chalk.blue(" \u2139\uFE0F Node9 is already fully configured for Pi."));
8103
+ return;
8104
+ }
8105
+ fs13.writeFileSync(extensionPath, shimContent);
8106
+ if (existingShim) {
8107
+ console.log(chalk.yellow(" \u{1F527} Pi extension shim updated to current version"));
8108
+ } else {
8109
+ console.log(
8110
+ chalk.green(" \u2705 Pi extension installed \u2192 tool_call / tool_result / input / user_bash")
8111
+ );
8112
+ }
8113
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Pi!"));
8114
+ console.log(chalk.gray(" Restart Pi for changes to take effect."));
8115
+ printDaemonTip();
8116
+ }
8117
+ function teardownPi() {
8118
+ const homeDir2 = os12.homedir();
8119
+ const extensionPath = path15.join(homeDir2, ".pi", "agent", "extensions", PI_EXTENSION_NAME);
8120
+ try {
8121
+ if (fs13.existsSync(extensionPath)) {
8122
+ fs13.unlinkSync(extensionPath);
8123
+ console.log(chalk.green(" \u2705 Removed node9 extension from ~/.pi/agent/extensions/"));
8124
+ } else {
8125
+ console.log(chalk.blue(" \u2139\uFE0F No Pi extension installed \u2014 nothing to remove"));
8126
+ }
8127
+ } catch (err2) {
8128
+ console.log(chalk.yellow(` \u26A0\uFE0F Could not remove ${extensionPath}: ${String(err2)}`));
8129
+ }
8130
+ }
7877
8131
  function getAgentsStatus(homeDir2 = os12.homedir()) {
7878
8132
  const detected = detectAgents(homeDir2);
7879
8133
  const claudeWired = (() => {
@@ -7976,18 +8230,29 @@ function getAgentsStatus(homeDir2 = os12.homedir()) {
7976
8230
  return !!cfg?.mcp?.["node9"];
7977
8231
  })(),
7978
8232
  mode: detected.opencode ? "hooks" : null
8233
+ },
8234
+ {
8235
+ name: "pi",
8236
+ label: "Pi",
8237
+ installed: detected.pi,
8238
+ // Pi has no MCP path — only the extension file. "wired" is a
8239
+ // simple existence check on the canonical install location.
8240
+ wired: fs13.existsSync(path15.join(homeDir2, ".pi", "agent", "extensions", PI_EXTENSION_NAME)),
8241
+ mode: detected.pi ? "hooks" : null
7979
8242
  }
7980
8243
  ];
7981
8244
  }
7982
- var NODE9_MCP_SERVER_ENTRY, CODEX_PRE_TOOL_MATCHERS, OPENCODE_PLUGIN_NAME;
8245
+ var NODE9_MCP_SERVER_ENTRY, CODEX_PRE_TOOL_MATCHERS, OPENCODE_PLUGIN_NAME, PI_EXTENSION_NAME;
7983
8246
  var init_setup = __esm({
7984
8247
  "src/setup.ts"() {
7985
8248
  "use strict";
7986
8249
  init_mcp_pin();
7987
8250
  init_setup_opencode_shim();
8251
+ init_setup_pi_shim();
7988
8252
  NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7989
8253
  CODEX_PRE_TOOL_MATCHERS = ["^Bash$", "^apply_patch$", "^mcp__.*"];
7990
8254
  OPENCODE_PLUGIN_NAME = "node9.js";
8255
+ PI_EXTENSION_NAME = "node9.js";
7991
8256
  }
7992
8257
  });
7993
8258
 
@@ -16784,7 +17049,15 @@ function registerLogCommand(program2) {
16784
17049
  const payload = JSON.parse(raw);
16785
17050
  const tool = sanitize3(payload.tool_name ?? payload.name ?? "unknown");
16786
17051
  const rawInput = payload.tool_input ?? payload.args ?? {};
16787
- const agent = payload.turn_id !== void 0 ? "Codex" : payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0 ? "Claude Code" : payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0 ? "Gemini CLI" : void 0;
17052
+ const metaTag = (() => {
17053
+ const m = payload.meta;
17054
+ if (m && typeof m === "object") {
17055
+ const tagged = m.agent;
17056
+ if (typeof tagged === "string" && tagged.length > 0) return tagged;
17057
+ }
17058
+ return void 0;
17059
+ })();
17060
+ const agent = metaTag !== void 0 ? metaTag : payload.turn_id !== void 0 ? "Codex" : payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0 ? "Claude Code" : payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0 ? "Gemini CLI" : void 0;
16788
17061
  const entry = {
16789
17062
  ts: (/* @__PURE__ */ new Date()).toISOString(),
16790
17063
  tool,
@@ -18941,6 +19214,7 @@ function registerInitCommand(program2) {
18941
19214
  else if (agent === "vscode") await setupVSCode();
18942
19215
  else if (agent === "claudeDesktop") await setupClaudeDesktop();
18943
19216
  else if (agent === "opencode") await setupOpencode();
19217
+ else if (agent === "pi") await setupPi();
18944
19218
  console.log("");
18945
19219
  }
18946
19220
  if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
@@ -20701,7 +20975,8 @@ var SETUP_FN = {
20701
20975
  windsurf: setupWindsurf,
20702
20976
  vscode: setupVSCode,
20703
20977
  claudeDesktop: setupClaudeDesktop,
20704
- opencode: setupOpencode
20978
+ opencode: setupOpencode,
20979
+ pi: setupPi
20705
20980
  };
20706
20981
  var TEARDOWN_FN = {
20707
20982
  claude: teardownClaude,
@@ -20711,7 +20986,8 @@ var TEARDOWN_FN = {
20711
20986
  windsurf: teardownWindsurf,
20712
20987
  vscode: teardownVSCode,
20713
20988
  claudeDesktop: teardownClaudeDesktop,
20714
- opencode: teardownOpencode
20989
+ opencode: teardownOpencode,
20990
+ pi: teardownPi
20715
20991
  };
20716
20992
  var AGENT_NAMES = Object.keys(SETUP_FN);
20717
20993
  function registerAgentsCommand(program2) {
@@ -3383,6 +3383,13 @@ var init_setup_opencode_shim = __esm({
3383
3383
  }
3384
3384
  });
3385
3385
 
3386
+ // src/setup-pi-shim.ts
3387
+ var init_setup_pi_shim = __esm({
3388
+ "src/setup-pi-shim.ts"() {
3389
+ "use strict";
3390
+ }
3391
+ });
3392
+
3386
3393
  // src/setup.ts
3387
3394
  import chalk2 from "chalk";
3388
3395
  import { confirm } from "@inquirer/prompts";
@@ -3392,6 +3399,7 @@ var init_setup = __esm({
3392
3399
  "use strict";
3393
3400
  init_mcp_pin();
3394
3401
  init_setup_opencode_shim();
3402
+ init_setup_pi_shim();
3395
3403
  }
3396
3404
  });
3397
3405
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.26.3",
3
+ "version": "1.27.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",