@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 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
- return port;
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:${DAEMON_PORT2}/settings`, {
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 DAEMON_PORT2;
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.on("error", resolve);
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.clear) {
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", "Include recent history on connect", false).option("--clear", "Clear history buffer and stream live events fresh", false).action(async (options) => {
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
- await startTail2(options);
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
- return port;
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:${DAEMON_PORT2}/settings`, {
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 DAEMON_PORT2;
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.on("error", resolve);
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.clear) {
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", "Include recent history on connect", false).option("--clear", "Clear history buffer and stream live events fresh", false).action(async (options) => {
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
- await startTail2(options);
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.16",
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",