@node9/proxy 1.26.2 → 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 +281 -5
- package/dist/cli.mjs +281 -5
- package/dist/dashboard.mjs +8 -0
- package/package.json +1 -1
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
|
|
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
|
|
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) {
|
package/dist/dashboard.mjs
CHANGED
|
@@ -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
|
|