@node9/proxy 1.0.16 → 1.0.18
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/README.md +4 -0
- package/dist/cli.js +242 -13
- package/dist/cli.mjs +242 -13
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -44,6 +44,10 @@ Node9 initiates a **Concurrent Race** across all enabled channels. The first cha
|
|
|
44
44
|
|
|
45
45
|
Node9 records every tool call your AI agent makes in real-time — no polling, no log files, no refresh. Two ways to watch:
|
|
46
46
|
|
|
47
|
+
<p align="center">
|
|
48
|
+
<img src="docs/flight-recorder.jpeg" width="100%">
|
|
49
|
+
</p>
|
|
50
|
+
|
|
47
51
|
**Browser Dashboard** (`node9 daemon start` → `localhost:7391`)
|
|
48
52
|
|
|
49
53
|
A live 3-column dashboard. The left column streams every tool call as it happens, updating in-place from `● PENDING` to `✓ ALLOW` or `✗ BLOCK`. The center handles pending approvals. The right sidebar controls shields and persistent decisions — all without ever causing a browser scrollbar.
|
package/dist/cli.js
CHANGED
|
@@ -4730,18 +4730,21 @@ function renderPending(activity) {
|
|
|
4730
4730
|
process.stdout.write(`${formatBase(activity)} ${import_chalk5.default.yellow("\u25CF \u2026")}\r`);
|
|
4731
4731
|
}
|
|
4732
4732
|
async function ensureDaemon() {
|
|
4733
|
+
let pidPort = null;
|
|
4733
4734
|
if (import_fs6.default.existsSync(PID_FILE)) {
|
|
4734
4735
|
try {
|
|
4735
4736
|
const { port } = JSON.parse(import_fs6.default.readFileSync(PID_FILE, "utf-8"));
|
|
4736
|
-
|
|
4737
|
+
pidPort = port;
|
|
4737
4738
|
} catch {
|
|
4739
|
+
console.error(import_chalk5.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
4738
4740
|
}
|
|
4739
4741
|
}
|
|
4742
|
+
const checkPort = pidPort ?? DAEMON_PORT2;
|
|
4740
4743
|
try {
|
|
4741
|
-
const res = await fetch(`http://127.0.0.1:${
|
|
4744
|
+
const res = await fetch(`http://127.0.0.1:${checkPort}/settings`, {
|
|
4742
4745
|
signal: AbortSignal.timeout(500)
|
|
4743
4746
|
});
|
|
4744
|
-
if (res.ok) return
|
|
4747
|
+
if (res.ok) return checkPort;
|
|
4745
4748
|
} catch {
|
|
4746
4749
|
}
|
|
4747
4750
|
console.log(import_chalk5.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
@@ -4767,25 +4770,44 @@ async function ensureDaemon() {
|
|
|
4767
4770
|
async function startTail(options = {}) {
|
|
4768
4771
|
const port = await ensureDaemon();
|
|
4769
4772
|
if (options.clear) {
|
|
4770
|
-
await new Promise((resolve) => {
|
|
4773
|
+
const result = await new Promise((resolve) => {
|
|
4771
4774
|
const req2 = import_http2.default.request(
|
|
4772
4775
|
{ method: "POST", hostname: "127.0.0.1", port, path: "/events/clear" },
|
|
4773
4776
|
(res) => {
|
|
4777
|
+
const status = res.statusCode ?? 0;
|
|
4778
|
+
res.on(
|
|
4779
|
+
"end",
|
|
4780
|
+
() => resolve({
|
|
4781
|
+
ok: status >= 200 && status < 300,
|
|
4782
|
+
code: status >= 200 && status < 300 ? void 0 : `HTTP ${status}`
|
|
4783
|
+
})
|
|
4784
|
+
);
|
|
4774
4785
|
res.resume();
|
|
4775
|
-
res.on("end", resolve);
|
|
4776
4786
|
}
|
|
4777
4787
|
);
|
|
4778
|
-
req2.
|
|
4788
|
+
req2.once("error", (err) => resolve({ ok: false, code: err.code }));
|
|
4789
|
+
req2.setTimeout(2e3, () => {
|
|
4790
|
+
resolve({ ok: false, code: "ETIMEDOUT" });
|
|
4791
|
+
req2.destroy();
|
|
4792
|
+
});
|
|
4779
4793
|
req2.end();
|
|
4780
4794
|
});
|
|
4795
|
+
if (result.ok) {
|
|
4796
|
+
console.log(import_chalk5.default.green("\u2713 Flight Recorder buffer cleared."));
|
|
4797
|
+
} else if (result.code === "ECONNREFUSED") {
|
|
4798
|
+
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
4799
|
+
} else if (result.code === "ETIMEDOUT") {
|
|
4800
|
+
throw new Error("Daemon did not respond in time. Try: node9 daemon restart");
|
|
4801
|
+
} else {
|
|
4802
|
+
throw new Error(`Failed to clear buffer (${result.code ?? "unknown error"})`);
|
|
4803
|
+
}
|
|
4804
|
+
return;
|
|
4781
4805
|
}
|
|
4782
4806
|
const connectionTime = Date.now();
|
|
4783
4807
|
const pending2 = /* @__PURE__ */ new Map();
|
|
4784
4808
|
console.log(import_chalk5.default.cyan.bold(`
|
|
4785
4809
|
\u{1F6F0}\uFE0F Node9 tail `) + import_chalk5.default.dim(`\u2192 localhost:${port}`));
|
|
4786
|
-
if (options.
|
|
4787
|
-
console.log(import_chalk5.default.dim("History cleared. Showing live events. Press Ctrl+C to exit.\n"));
|
|
4788
|
-
} else if (options.history) {
|
|
4810
|
+
if (options.history) {
|
|
4789
4811
|
console.log(import_chalk5.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
4790
4812
|
} else {
|
|
4791
4813
|
console.log(
|
|
@@ -4918,6 +4940,7 @@ function fullPathCommand(subcommand) {
|
|
|
4918
4940
|
if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
|
|
4919
4941
|
const nodeExec = process.execPath;
|
|
4920
4942
|
const cliScript = process.argv[1];
|
|
4943
|
+
if (!cliScript.endsWith(".js")) return `${cliScript} ${subcommand}`;
|
|
4921
4944
|
return `${nodeExec} ${cliScript} ${subcommand}`;
|
|
4922
4945
|
}
|
|
4923
4946
|
function readJson(filePath) {
|
|
@@ -4934,6 +4957,126 @@ function writeJson(filePath, data) {
|
|
|
4934
4957
|
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
4935
4958
|
import_fs3.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
4936
4959
|
}
|
|
4960
|
+
function isNode9Hook(cmd) {
|
|
4961
|
+
if (!cmd) return false;
|
|
4962
|
+
return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
|
|
4963
|
+
}
|
|
4964
|
+
function teardownClaude() {
|
|
4965
|
+
const homeDir2 = import_os3.default.homedir();
|
|
4966
|
+
const hooksPath = import_path5.default.join(homeDir2, ".claude", "settings.json");
|
|
4967
|
+
const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
|
|
4968
|
+
let changed = false;
|
|
4969
|
+
const settings = readJson(hooksPath);
|
|
4970
|
+
if (settings?.hooks) {
|
|
4971
|
+
for (const event of ["PreToolUse", "PostToolUse"]) {
|
|
4972
|
+
const before = settings.hooks[event]?.length ?? 0;
|
|
4973
|
+
settings.hooks[event] = settings.hooks[event]?.filter(
|
|
4974
|
+
(m) => !m.hooks.some((h) => isNode9Hook(h.command))
|
|
4975
|
+
);
|
|
4976
|
+
if ((settings.hooks[event]?.length ?? 0) < before) changed = true;
|
|
4977
|
+
if (settings.hooks[event]?.length === 0) delete settings.hooks[event];
|
|
4978
|
+
}
|
|
4979
|
+
if (changed) {
|
|
4980
|
+
writeJson(hooksPath, settings);
|
|
4981
|
+
console.log(
|
|
4982
|
+
import_chalk3.default.green(" \u2705 Removed PreToolUse / PostToolUse hooks from ~/.claude/settings.json")
|
|
4983
|
+
);
|
|
4984
|
+
} else {
|
|
4985
|
+
console.log(import_chalk3.default.blue(" \u2139\uFE0F No Node9 hooks found in ~/.claude/settings.json"));
|
|
4986
|
+
}
|
|
4987
|
+
}
|
|
4988
|
+
const claudeConfig = readJson(mcpPath);
|
|
4989
|
+
if (claudeConfig?.mcpServers) {
|
|
4990
|
+
let mcpChanged = false;
|
|
4991
|
+
for (const [name, server] of Object.entries(claudeConfig.mcpServers)) {
|
|
4992
|
+
if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
|
|
4993
|
+
const [originalCmd, ...originalArgs] = server.args;
|
|
4994
|
+
claudeConfig.mcpServers[name] = {
|
|
4995
|
+
...server,
|
|
4996
|
+
command: originalCmd,
|
|
4997
|
+
args: originalArgs.length ? originalArgs : void 0
|
|
4998
|
+
};
|
|
4999
|
+
mcpChanged = true;
|
|
5000
|
+
} else if (server.command === "node9") {
|
|
5001
|
+
console.warn(
|
|
5002
|
+
import_chalk3.default.yellow(
|
|
5003
|
+
` \u26A0\uFE0F Cannot unwrap MCP server "${name}" in ~/.claude.json \u2014 args is empty. Remove it manually.`
|
|
5004
|
+
)
|
|
5005
|
+
);
|
|
5006
|
+
}
|
|
5007
|
+
}
|
|
5008
|
+
if (mcpChanged) {
|
|
5009
|
+
writeJson(mcpPath, claudeConfig);
|
|
5010
|
+
console.log(import_chalk3.default.green(" \u2705 Unwrapped MCP servers in ~/.claude.json"));
|
|
5011
|
+
}
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
function teardownGemini() {
|
|
5015
|
+
const homeDir2 = import_os3.default.homedir();
|
|
5016
|
+
const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
|
|
5017
|
+
const settings = readJson(settingsPath);
|
|
5018
|
+
if (!settings) {
|
|
5019
|
+
console.log(import_chalk3.default.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
|
|
5020
|
+
return;
|
|
5021
|
+
}
|
|
5022
|
+
let changed = false;
|
|
5023
|
+
for (const event of ["BeforeTool", "AfterTool"]) {
|
|
5024
|
+
const before = settings.hooks?.[event]?.length ?? 0;
|
|
5025
|
+
if (settings.hooks?.[event]) {
|
|
5026
|
+
settings.hooks[event] = settings.hooks[event].filter(
|
|
5027
|
+
(m) => !m.hooks.some((h) => isNode9Hook(h.command))
|
|
5028
|
+
);
|
|
5029
|
+
if ((settings.hooks[event]?.length ?? 0) < before) changed = true;
|
|
5030
|
+
if (settings.hooks[event]?.length === 0) delete settings.hooks[event];
|
|
5031
|
+
}
|
|
5032
|
+
}
|
|
5033
|
+
if (settings.mcpServers) {
|
|
5034
|
+
for (const [name, server] of Object.entries(settings.mcpServers)) {
|
|
5035
|
+
if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
|
|
5036
|
+
const [originalCmd, ...originalArgs] = server.args;
|
|
5037
|
+
settings.mcpServers[name] = {
|
|
5038
|
+
...server,
|
|
5039
|
+
command: originalCmd,
|
|
5040
|
+
args: originalArgs.length ? originalArgs : void 0
|
|
5041
|
+
};
|
|
5042
|
+
changed = true;
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
if (changed) {
|
|
5047
|
+
writeJson(settingsPath, settings);
|
|
5048
|
+
console.log(import_chalk3.default.green(" \u2705 Removed Node9 hooks from ~/.gemini/settings.json"));
|
|
5049
|
+
} else {
|
|
5050
|
+
console.log(import_chalk3.default.blue(" \u2139\uFE0F No Node9 hooks found in ~/.gemini/settings.json"));
|
|
5051
|
+
}
|
|
5052
|
+
}
|
|
5053
|
+
function teardownCursor() {
|
|
5054
|
+
const homeDir2 = import_os3.default.homedir();
|
|
5055
|
+
const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
|
|
5056
|
+
const mcpConfig = readJson(mcpPath);
|
|
5057
|
+
if (!mcpConfig?.mcpServers) {
|
|
5058
|
+
console.log(import_chalk3.default.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
|
|
5059
|
+
return;
|
|
5060
|
+
}
|
|
5061
|
+
let changed = false;
|
|
5062
|
+
for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
|
|
5063
|
+
if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
|
|
5064
|
+
const [originalCmd, ...originalArgs] = server.args;
|
|
5065
|
+
mcpConfig.mcpServers[name] = {
|
|
5066
|
+
...server,
|
|
5067
|
+
command: originalCmd,
|
|
5068
|
+
args: originalArgs.length ? originalArgs : void 0
|
|
5069
|
+
};
|
|
5070
|
+
changed = true;
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
if (changed) {
|
|
5074
|
+
writeJson(mcpPath, mcpConfig);
|
|
5075
|
+
console.log(import_chalk3.default.green(" \u2705 Unwrapped MCP servers in ~/.cursor/mcp.json"));
|
|
5076
|
+
} else {
|
|
5077
|
+
console.log(import_chalk3.default.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.cursor/mcp.json"));
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
4937
5080
|
async function setupClaude() {
|
|
4938
5081
|
const homeDir2 = import_os3.default.homedir();
|
|
4939
5082
|
const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
|
|
@@ -5557,6 +5700,87 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
5557
5700
|
console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
5558
5701
|
process.exit(1);
|
|
5559
5702
|
});
|
|
5703
|
+
program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
|
|
5704
|
+
let fn;
|
|
5705
|
+
if (target === "claude") fn = teardownClaude;
|
|
5706
|
+
else if (target === "gemini") fn = teardownGemini;
|
|
5707
|
+
else if (target === "cursor") fn = teardownCursor;
|
|
5708
|
+
else {
|
|
5709
|
+
console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
5710
|
+
process.exit(1);
|
|
5711
|
+
}
|
|
5712
|
+
console.log(import_chalk6.default.cyan(`
|
|
5713
|
+
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
5714
|
+
`));
|
|
5715
|
+
try {
|
|
5716
|
+
fn();
|
|
5717
|
+
} catch (err) {
|
|
5718
|
+
console.error(import_chalk6.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
5719
|
+
process.exit(1);
|
|
5720
|
+
}
|
|
5721
|
+
console.log(import_chalk6.default.gray("\n Restart the agent for changes to take effect."));
|
|
5722
|
+
});
|
|
5723
|
+
program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
|
|
5724
|
+
console.log(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
5725
|
+
console.log(import_chalk6.default.bold("Stopping daemon..."));
|
|
5726
|
+
try {
|
|
5727
|
+
stopDaemon();
|
|
5728
|
+
console.log(import_chalk6.default.green(" \u2705 Daemon stopped"));
|
|
5729
|
+
} catch {
|
|
5730
|
+
console.log(import_chalk6.default.blue(" \u2139\uFE0F Daemon was not running"));
|
|
5731
|
+
}
|
|
5732
|
+
console.log(import_chalk6.default.bold("\nRemoving hooks..."));
|
|
5733
|
+
let teardownFailed = false;
|
|
5734
|
+
for (const [label, fn] of [
|
|
5735
|
+
["Claude", teardownClaude],
|
|
5736
|
+
["Gemini", teardownGemini],
|
|
5737
|
+
["Cursor", teardownCursor]
|
|
5738
|
+
]) {
|
|
5739
|
+
try {
|
|
5740
|
+
fn();
|
|
5741
|
+
} catch (err) {
|
|
5742
|
+
teardownFailed = true;
|
|
5743
|
+
console.error(
|
|
5744
|
+
import_chalk6.default.red(
|
|
5745
|
+
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
|
|
5746
|
+
)
|
|
5747
|
+
);
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
if (options.purge) {
|
|
5751
|
+
const node9Dir = import_path9.default.join(import_os7.default.homedir(), ".node9");
|
|
5752
|
+
if (import_fs7.default.existsSync(node9Dir)) {
|
|
5753
|
+
const confirmed = await (0, import_prompts3.confirm)({
|
|
5754
|
+
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
5755
|
+
default: false
|
|
5756
|
+
});
|
|
5757
|
+
if (confirmed) {
|
|
5758
|
+
import_fs7.default.rmSync(node9Dir, { recursive: true });
|
|
5759
|
+
if (import_fs7.default.existsSync(node9Dir)) {
|
|
5760
|
+
console.error(
|
|
5761
|
+
import_chalk6.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
5762
|
+
);
|
|
5763
|
+
} else {
|
|
5764
|
+
console.log(import_chalk6.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
5765
|
+
}
|
|
5766
|
+
} else {
|
|
5767
|
+
console.log(import_chalk6.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
5768
|
+
}
|
|
5769
|
+
} else {
|
|
5770
|
+
console.log(import_chalk6.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
5771
|
+
}
|
|
5772
|
+
} else {
|
|
5773
|
+
console.log(
|
|
5774
|
+
import_chalk6.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
5775
|
+
);
|
|
5776
|
+
}
|
|
5777
|
+
if (teardownFailed) {
|
|
5778
|
+
console.error(import_chalk6.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
5779
|
+
process.exit(1);
|
|
5780
|
+
}
|
|
5781
|
+
console.log(import_chalk6.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
5782
|
+
console.log(import_chalk6.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
5783
|
+
});
|
|
5560
5784
|
program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
5561
5785
|
const homeDir2 = import_os7.default.homedir();
|
|
5562
5786
|
let failures = 0;
|
|
@@ -5967,9 +6191,14 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
5967
6191
|
startDaemon();
|
|
5968
6192
|
}
|
|
5969
6193
|
);
|
|
5970
|
-
program.command("tail").description("Stream live agent activity to the terminal").option("--history", "
|
|
6194
|
+
program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Replay recent history then continue live", false).option("--clear", "Clear the history buffer and exit (does not stream)", false).action(async (options) => {
|
|
5971
6195
|
const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
|
|
5972
|
-
|
|
6196
|
+
try {
|
|
6197
|
+
await startTail2(options);
|
|
6198
|
+
} catch (err) {
|
|
6199
|
+
console.error(import_chalk6.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
6200
|
+
process.exit(1);
|
|
6201
|
+
}
|
|
5973
6202
|
});
|
|
5974
6203
|
program.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
|
|
5975
6204
|
const processPayload = async (raw) => {
|
|
@@ -6052,7 +6281,7 @@ RAW: ${raw}
|
|
|
6052
6281
|
}
|
|
6053
6282
|
const result = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
6054
6283
|
if (result.approved) {
|
|
6055
|
-
if (result.checkedBy)
|
|
6284
|
+
if (result.checkedBy && process.env.NODE9_DEBUG === "1")
|
|
6056
6285
|
process.stderr.write(`\u2713 node9 [${result.checkedBy}]: "${toolName}" allowed
|
|
6057
6286
|
`);
|
|
6058
6287
|
process.exit(0);
|
|
@@ -6063,7 +6292,7 @@ RAW: ${raw}
|
|
|
6063
6292
|
if (daemonReady) {
|
|
6064
6293
|
const retry = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
6065
6294
|
if (retry.approved) {
|
|
6066
|
-
if (retry.checkedBy)
|
|
6295
|
+
if (retry.checkedBy && process.env.NODE9_DEBUG === "1")
|
|
6067
6296
|
process.stderr.write(`\u2713 node9 [${retry.checkedBy}]: "${toolName}" allowed
|
|
6068
6297
|
`);
|
|
6069
6298
|
process.exit(0);
|
package/dist/cli.mjs
CHANGED
|
@@ -4716,18 +4716,21 @@ function renderPending(activity) {
|
|
|
4716
4716
|
process.stdout.write(`${formatBase(activity)} ${chalk5.yellow("\u25CF \u2026")}\r`);
|
|
4717
4717
|
}
|
|
4718
4718
|
async function ensureDaemon() {
|
|
4719
|
+
let pidPort = null;
|
|
4719
4720
|
if (fs6.existsSync(PID_FILE)) {
|
|
4720
4721
|
try {
|
|
4721
4722
|
const { port } = JSON.parse(fs6.readFileSync(PID_FILE, "utf-8"));
|
|
4722
|
-
|
|
4723
|
+
pidPort = port;
|
|
4723
4724
|
} catch {
|
|
4725
|
+
console.error(chalk5.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
4724
4726
|
}
|
|
4725
4727
|
}
|
|
4728
|
+
const checkPort = pidPort ?? DAEMON_PORT2;
|
|
4726
4729
|
try {
|
|
4727
|
-
const res = await fetch(`http://127.0.0.1:${
|
|
4730
|
+
const res = await fetch(`http://127.0.0.1:${checkPort}/settings`, {
|
|
4728
4731
|
signal: AbortSignal.timeout(500)
|
|
4729
4732
|
});
|
|
4730
|
-
if (res.ok) return
|
|
4733
|
+
if (res.ok) return checkPort;
|
|
4731
4734
|
} catch {
|
|
4732
4735
|
}
|
|
4733
4736
|
console.log(chalk5.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
@@ -4753,25 +4756,44 @@ async function ensureDaemon() {
|
|
|
4753
4756
|
async function startTail(options = {}) {
|
|
4754
4757
|
const port = await ensureDaemon();
|
|
4755
4758
|
if (options.clear) {
|
|
4756
|
-
await new Promise((resolve) => {
|
|
4759
|
+
const result = await new Promise((resolve) => {
|
|
4757
4760
|
const req2 = http2.request(
|
|
4758
4761
|
{ method: "POST", hostname: "127.0.0.1", port, path: "/events/clear" },
|
|
4759
4762
|
(res) => {
|
|
4763
|
+
const status = res.statusCode ?? 0;
|
|
4764
|
+
res.on(
|
|
4765
|
+
"end",
|
|
4766
|
+
() => resolve({
|
|
4767
|
+
ok: status >= 200 && status < 300,
|
|
4768
|
+
code: status >= 200 && status < 300 ? void 0 : `HTTP ${status}`
|
|
4769
|
+
})
|
|
4770
|
+
);
|
|
4760
4771
|
res.resume();
|
|
4761
|
-
res.on("end", resolve);
|
|
4762
4772
|
}
|
|
4763
4773
|
);
|
|
4764
|
-
req2.
|
|
4774
|
+
req2.once("error", (err) => resolve({ ok: false, code: err.code }));
|
|
4775
|
+
req2.setTimeout(2e3, () => {
|
|
4776
|
+
resolve({ ok: false, code: "ETIMEDOUT" });
|
|
4777
|
+
req2.destroy();
|
|
4778
|
+
});
|
|
4765
4779
|
req2.end();
|
|
4766
4780
|
});
|
|
4781
|
+
if (result.ok) {
|
|
4782
|
+
console.log(chalk5.green("\u2713 Flight Recorder buffer cleared."));
|
|
4783
|
+
} else if (result.code === "ECONNREFUSED") {
|
|
4784
|
+
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
4785
|
+
} else if (result.code === "ETIMEDOUT") {
|
|
4786
|
+
throw new Error("Daemon did not respond in time. Try: node9 daemon restart");
|
|
4787
|
+
} else {
|
|
4788
|
+
throw new Error(`Failed to clear buffer (${result.code ?? "unknown error"})`);
|
|
4789
|
+
}
|
|
4790
|
+
return;
|
|
4767
4791
|
}
|
|
4768
4792
|
const connectionTime = Date.now();
|
|
4769
4793
|
const pending2 = /* @__PURE__ */ new Map();
|
|
4770
4794
|
console.log(chalk5.cyan.bold(`
|
|
4771
4795
|
\u{1F6F0}\uFE0F Node9 tail `) + chalk5.dim(`\u2192 localhost:${port}`));
|
|
4772
|
-
if (options.
|
|
4773
|
-
console.log(chalk5.dim("History cleared. Showing live events. Press Ctrl+C to exit.\n"));
|
|
4774
|
-
} else if (options.history) {
|
|
4796
|
+
if (options.history) {
|
|
4775
4797
|
console.log(chalk5.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
4776
4798
|
} else {
|
|
4777
4799
|
console.log(
|
|
@@ -4897,6 +4919,7 @@ function fullPathCommand(subcommand) {
|
|
|
4897
4919
|
if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
|
|
4898
4920
|
const nodeExec = process.execPath;
|
|
4899
4921
|
const cliScript = process.argv[1];
|
|
4922
|
+
if (!cliScript.endsWith(".js")) return `${cliScript} ${subcommand}`;
|
|
4900
4923
|
return `${nodeExec} ${cliScript} ${subcommand}`;
|
|
4901
4924
|
}
|
|
4902
4925
|
function readJson(filePath) {
|
|
@@ -4913,6 +4936,126 @@ function writeJson(filePath, data) {
|
|
|
4913
4936
|
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
4914
4937
|
fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
4915
4938
|
}
|
|
4939
|
+
function isNode9Hook(cmd) {
|
|
4940
|
+
if (!cmd) return false;
|
|
4941
|
+
return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
|
|
4942
|
+
}
|
|
4943
|
+
function teardownClaude() {
|
|
4944
|
+
const homeDir2 = os3.homedir();
|
|
4945
|
+
const hooksPath = path5.join(homeDir2, ".claude", "settings.json");
|
|
4946
|
+
const mcpPath = path5.join(homeDir2, ".claude.json");
|
|
4947
|
+
let changed = false;
|
|
4948
|
+
const settings = readJson(hooksPath);
|
|
4949
|
+
if (settings?.hooks) {
|
|
4950
|
+
for (const event of ["PreToolUse", "PostToolUse"]) {
|
|
4951
|
+
const before = settings.hooks[event]?.length ?? 0;
|
|
4952
|
+
settings.hooks[event] = settings.hooks[event]?.filter(
|
|
4953
|
+
(m) => !m.hooks.some((h) => isNode9Hook(h.command))
|
|
4954
|
+
);
|
|
4955
|
+
if ((settings.hooks[event]?.length ?? 0) < before) changed = true;
|
|
4956
|
+
if (settings.hooks[event]?.length === 0) delete settings.hooks[event];
|
|
4957
|
+
}
|
|
4958
|
+
if (changed) {
|
|
4959
|
+
writeJson(hooksPath, settings);
|
|
4960
|
+
console.log(
|
|
4961
|
+
chalk3.green(" \u2705 Removed PreToolUse / PostToolUse hooks from ~/.claude/settings.json")
|
|
4962
|
+
);
|
|
4963
|
+
} else {
|
|
4964
|
+
console.log(chalk3.blue(" \u2139\uFE0F No Node9 hooks found in ~/.claude/settings.json"));
|
|
4965
|
+
}
|
|
4966
|
+
}
|
|
4967
|
+
const claudeConfig = readJson(mcpPath);
|
|
4968
|
+
if (claudeConfig?.mcpServers) {
|
|
4969
|
+
let mcpChanged = false;
|
|
4970
|
+
for (const [name, server] of Object.entries(claudeConfig.mcpServers)) {
|
|
4971
|
+
if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
|
|
4972
|
+
const [originalCmd, ...originalArgs] = server.args;
|
|
4973
|
+
claudeConfig.mcpServers[name] = {
|
|
4974
|
+
...server,
|
|
4975
|
+
command: originalCmd,
|
|
4976
|
+
args: originalArgs.length ? originalArgs : void 0
|
|
4977
|
+
};
|
|
4978
|
+
mcpChanged = true;
|
|
4979
|
+
} else if (server.command === "node9") {
|
|
4980
|
+
console.warn(
|
|
4981
|
+
chalk3.yellow(
|
|
4982
|
+
` \u26A0\uFE0F Cannot unwrap MCP server "${name}" in ~/.claude.json \u2014 args is empty. Remove it manually.`
|
|
4983
|
+
)
|
|
4984
|
+
);
|
|
4985
|
+
}
|
|
4986
|
+
}
|
|
4987
|
+
if (mcpChanged) {
|
|
4988
|
+
writeJson(mcpPath, claudeConfig);
|
|
4989
|
+
console.log(chalk3.green(" \u2705 Unwrapped MCP servers in ~/.claude.json"));
|
|
4990
|
+
}
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
function teardownGemini() {
|
|
4994
|
+
const homeDir2 = os3.homedir();
|
|
4995
|
+
const settingsPath = path5.join(homeDir2, ".gemini", "settings.json");
|
|
4996
|
+
const settings = readJson(settingsPath);
|
|
4997
|
+
if (!settings) {
|
|
4998
|
+
console.log(chalk3.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
|
|
4999
|
+
return;
|
|
5000
|
+
}
|
|
5001
|
+
let changed = false;
|
|
5002
|
+
for (const event of ["BeforeTool", "AfterTool"]) {
|
|
5003
|
+
const before = settings.hooks?.[event]?.length ?? 0;
|
|
5004
|
+
if (settings.hooks?.[event]) {
|
|
5005
|
+
settings.hooks[event] = settings.hooks[event].filter(
|
|
5006
|
+
(m) => !m.hooks.some((h) => isNode9Hook(h.command))
|
|
5007
|
+
);
|
|
5008
|
+
if ((settings.hooks[event]?.length ?? 0) < before) changed = true;
|
|
5009
|
+
if (settings.hooks[event]?.length === 0) delete settings.hooks[event];
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
5012
|
+
if (settings.mcpServers) {
|
|
5013
|
+
for (const [name, server] of Object.entries(settings.mcpServers)) {
|
|
5014
|
+
if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
|
|
5015
|
+
const [originalCmd, ...originalArgs] = server.args;
|
|
5016
|
+
settings.mcpServers[name] = {
|
|
5017
|
+
...server,
|
|
5018
|
+
command: originalCmd,
|
|
5019
|
+
args: originalArgs.length ? originalArgs : void 0
|
|
5020
|
+
};
|
|
5021
|
+
changed = true;
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
if (changed) {
|
|
5026
|
+
writeJson(settingsPath, settings);
|
|
5027
|
+
console.log(chalk3.green(" \u2705 Removed Node9 hooks from ~/.gemini/settings.json"));
|
|
5028
|
+
} else {
|
|
5029
|
+
console.log(chalk3.blue(" \u2139\uFE0F No Node9 hooks found in ~/.gemini/settings.json"));
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
function teardownCursor() {
|
|
5033
|
+
const homeDir2 = os3.homedir();
|
|
5034
|
+
const mcpPath = path5.join(homeDir2, ".cursor", "mcp.json");
|
|
5035
|
+
const mcpConfig = readJson(mcpPath);
|
|
5036
|
+
if (!mcpConfig?.mcpServers) {
|
|
5037
|
+
console.log(chalk3.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
|
|
5038
|
+
return;
|
|
5039
|
+
}
|
|
5040
|
+
let changed = false;
|
|
5041
|
+
for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
|
|
5042
|
+
if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
|
|
5043
|
+
const [originalCmd, ...originalArgs] = server.args;
|
|
5044
|
+
mcpConfig.mcpServers[name] = {
|
|
5045
|
+
...server,
|
|
5046
|
+
command: originalCmd,
|
|
5047
|
+
args: originalArgs.length ? originalArgs : void 0
|
|
5048
|
+
};
|
|
5049
|
+
changed = true;
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
5052
|
+
if (changed) {
|
|
5053
|
+
writeJson(mcpPath, mcpConfig);
|
|
5054
|
+
console.log(chalk3.green(" \u2705 Unwrapped MCP servers in ~/.cursor/mcp.json"));
|
|
5055
|
+
} else {
|
|
5056
|
+
console.log(chalk3.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.cursor/mcp.json"));
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
4916
5059
|
async function setupClaude() {
|
|
4917
5060
|
const homeDir2 = os3.homedir();
|
|
4918
5061
|
const mcpPath = path5.join(homeDir2, ".claude.json");
|
|
@@ -5536,6 +5679,87 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
5536
5679
|
console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
5537
5680
|
process.exit(1);
|
|
5538
5681
|
});
|
|
5682
|
+
program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
|
|
5683
|
+
let fn;
|
|
5684
|
+
if (target === "claude") fn = teardownClaude;
|
|
5685
|
+
else if (target === "gemini") fn = teardownGemini;
|
|
5686
|
+
else if (target === "cursor") fn = teardownCursor;
|
|
5687
|
+
else {
|
|
5688
|
+
console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
5689
|
+
process.exit(1);
|
|
5690
|
+
}
|
|
5691
|
+
console.log(chalk6.cyan(`
|
|
5692
|
+
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
5693
|
+
`));
|
|
5694
|
+
try {
|
|
5695
|
+
fn();
|
|
5696
|
+
} catch (err) {
|
|
5697
|
+
console.error(chalk6.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
5698
|
+
process.exit(1);
|
|
5699
|
+
}
|
|
5700
|
+
console.log(chalk6.gray("\n Restart the agent for changes to take effect."));
|
|
5701
|
+
});
|
|
5702
|
+
program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
|
|
5703
|
+
console.log(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
5704
|
+
console.log(chalk6.bold("Stopping daemon..."));
|
|
5705
|
+
try {
|
|
5706
|
+
stopDaemon();
|
|
5707
|
+
console.log(chalk6.green(" \u2705 Daemon stopped"));
|
|
5708
|
+
} catch {
|
|
5709
|
+
console.log(chalk6.blue(" \u2139\uFE0F Daemon was not running"));
|
|
5710
|
+
}
|
|
5711
|
+
console.log(chalk6.bold("\nRemoving hooks..."));
|
|
5712
|
+
let teardownFailed = false;
|
|
5713
|
+
for (const [label, fn] of [
|
|
5714
|
+
["Claude", teardownClaude],
|
|
5715
|
+
["Gemini", teardownGemini],
|
|
5716
|
+
["Cursor", teardownCursor]
|
|
5717
|
+
]) {
|
|
5718
|
+
try {
|
|
5719
|
+
fn();
|
|
5720
|
+
} catch (err) {
|
|
5721
|
+
teardownFailed = true;
|
|
5722
|
+
console.error(
|
|
5723
|
+
chalk6.red(
|
|
5724
|
+
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
|
|
5725
|
+
)
|
|
5726
|
+
);
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
5729
|
+
if (options.purge) {
|
|
5730
|
+
const node9Dir = path9.join(os7.homedir(), ".node9");
|
|
5731
|
+
if (fs7.existsSync(node9Dir)) {
|
|
5732
|
+
const confirmed = await confirm3({
|
|
5733
|
+
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
5734
|
+
default: false
|
|
5735
|
+
});
|
|
5736
|
+
if (confirmed) {
|
|
5737
|
+
fs7.rmSync(node9Dir, { recursive: true });
|
|
5738
|
+
if (fs7.existsSync(node9Dir)) {
|
|
5739
|
+
console.error(
|
|
5740
|
+
chalk6.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
5741
|
+
);
|
|
5742
|
+
} else {
|
|
5743
|
+
console.log(chalk6.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
5744
|
+
}
|
|
5745
|
+
} else {
|
|
5746
|
+
console.log(chalk6.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
5747
|
+
}
|
|
5748
|
+
} else {
|
|
5749
|
+
console.log(chalk6.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
5750
|
+
}
|
|
5751
|
+
} else {
|
|
5752
|
+
console.log(
|
|
5753
|
+
chalk6.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
5754
|
+
);
|
|
5755
|
+
}
|
|
5756
|
+
if (teardownFailed) {
|
|
5757
|
+
console.error(chalk6.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
5758
|
+
process.exit(1);
|
|
5759
|
+
}
|
|
5760
|
+
console.log(chalk6.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
5761
|
+
console.log(chalk6.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
5762
|
+
});
|
|
5539
5763
|
program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
5540
5764
|
const homeDir2 = os7.homedir();
|
|
5541
5765
|
let failures = 0;
|
|
@@ -5946,9 +6170,14 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
5946
6170
|
startDaemon();
|
|
5947
6171
|
}
|
|
5948
6172
|
);
|
|
5949
|
-
program.command("tail").description("Stream live agent activity to the terminal").option("--history", "
|
|
6173
|
+
program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Replay recent history then continue live", false).option("--clear", "Clear the history buffer and exit (does not stream)", false).action(async (options) => {
|
|
5950
6174
|
const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
|
|
5951
|
-
|
|
6175
|
+
try {
|
|
6176
|
+
await startTail2(options);
|
|
6177
|
+
} catch (err) {
|
|
6178
|
+
console.error(chalk6.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
6179
|
+
process.exit(1);
|
|
6180
|
+
}
|
|
5952
6181
|
});
|
|
5953
6182
|
program.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
|
|
5954
6183
|
const processPayload = async (raw) => {
|
|
@@ -6031,7 +6260,7 @@ RAW: ${raw}
|
|
|
6031
6260
|
}
|
|
6032
6261
|
const result = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
6033
6262
|
if (result.approved) {
|
|
6034
|
-
if (result.checkedBy)
|
|
6263
|
+
if (result.checkedBy && process.env.NODE9_DEBUG === "1")
|
|
6035
6264
|
process.stderr.write(`\u2713 node9 [${result.checkedBy}]: "${toolName}" allowed
|
|
6036
6265
|
`);
|
|
6037
6266
|
process.exit(0);
|
|
@@ -6042,7 +6271,7 @@ RAW: ${raw}
|
|
|
6042
6271
|
if (daemonReady) {
|
|
6043
6272
|
const retry = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
6044
6273
|
if (retry.approved) {
|
|
6045
|
-
if (retry.checkedBy)
|
|
6274
|
+
if (retry.checkedBy && process.env.NODE9_DEBUG === "1")
|
|
6046
6275
|
process.stderr.write(`\u2713 node9 [${retry.checkedBy}]: "${toolName}" allowed
|
|
6047
6276
|
`);
|
|
6048
6277
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
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",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"fix": "npm run format && npm run lint:fix",
|
|
60
60
|
"validate": "npm run format && npm run lint && npm run typecheck && npm run test && npm run test:e2e && npm run build",
|
|
61
61
|
"test:e2e": "NODE9_TESTING=1 bash scripts/e2e.sh",
|
|
62
|
+
"preuninstall": "node9 uninstall || echo 'node9 uninstall failed — remove hooks manually from ~/.claude/settings.json'",
|
|
62
63
|
"prepublishOnly": "npm run validate",
|
|
63
64
|
"test": "NODE_ENV=test vitest --run",
|
|
64
65
|
"test:watch": "NODE_ENV=test vitest",
|