@node9/proxy 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -7,13 +7,14 @@ import { Command } from "commander";
7
7
  import chalk2 from "chalk";
8
8
  import { confirm } from "@inquirer/prompts";
9
9
  import fs from "fs";
10
- import path from "path";
10
+ import path2 from "path";
11
11
  import os from "os";
12
12
  import pm from "picomatch";
13
13
  import { parse } from "sh-syntax";
14
14
 
15
15
  // src/ui/native.ts
16
16
  import { spawn } from "child_process";
17
+ import path from "path";
17
18
  import chalk from "chalk";
18
19
  var isTestEnv = () => {
19
20
  return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
@@ -23,8 +24,29 @@ function smartTruncate(str, maxLen = 500) {
23
24
  const edge = Math.floor(maxLen / 2) - 3;
24
25
  return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
25
26
  }
26
- function formatArgs(args) {
27
- if (args === null || args === void 0) return "(none)";
27
+ function extractContext(text, matchedWord) {
28
+ const lines = text.split("\n");
29
+ if (lines.length <= 7 || !matchedWord) return smartTruncate(text, 500);
30
+ const escaped = matchedWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31
+ const pattern = new RegExp(`\\b${escaped}\\b`, "i");
32
+ const allHits = lines.map((line, i) => ({ i, line })).filter(({ line }) => pattern.test(line));
33
+ if (allHits.length === 0) return smartTruncate(text, 500);
34
+ const nonComment = allHits.find(({ line }) => {
35
+ const trimmed = line.trim();
36
+ return !trimmed.startsWith("//") && !trimmed.startsWith("#");
37
+ });
38
+ const hitIndex = (nonComment ?? allHits[0]).i;
39
+ const start = Math.max(0, hitIndex - 3);
40
+ const end = Math.min(lines.length, hitIndex + 4);
41
+ const snippet = lines.slice(start, end).map((line, i) => `${start + i === hitIndex ? "\u{1F6D1} " : " "}${line}`).join("\n");
42
+ const head = start > 0 ? `... [${start} lines hidden] ...
43
+ ` : "";
44
+ const tail = end < lines.length ? `
45
+ ... [${lines.length - end} lines hidden] ...` : "";
46
+ return `${head}${snippet}${tail}`;
47
+ }
48
+ function formatArgs(args, matchedField, matchedWord) {
49
+ if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
28
50
  let parsed = args;
29
51
  if (typeof args === "string") {
30
52
  const trimmed = args.trim();
@@ -35,11 +57,39 @@ function formatArgs(args) {
35
57
  parsed = args;
36
58
  }
37
59
  } else {
38
- return smartTruncate(args, 600);
60
+ return { message: smartTruncate(args, 600), intent: "EXEC" };
39
61
  }
40
62
  }
41
63
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
42
64
  const obj = parsed;
65
+ if (obj.old_string !== void 0 && obj.new_string !== void 0) {
66
+ const file = obj.file_path ? path.basename(String(obj.file_path)) : "file";
67
+ const oldPreview = smartTruncate(String(obj.old_string), 120);
68
+ const newPreview = extractContext(String(obj.new_string), matchedWord);
69
+ return {
70
+ intent: "EDIT",
71
+ message: `\u{1F4DD} EDITING: ${file}
72
+ \u{1F4C2} PATH: ${obj.file_path}
73
+
74
+ --- REPLACING ---
75
+ ${oldPreview}
76
+
77
+ +++ NEW CODE +++
78
+ ${newPreview}`
79
+ };
80
+ }
81
+ if (matchedField && obj[matchedField] !== void 0) {
82
+ const otherKeys = Object.keys(obj).filter((k) => k !== matchedField);
83
+ const context = otherKeys.length > 0 ? `\u2699\uFE0F Context: ${otherKeys.map((k) => `${k}=${smartTruncate(typeof obj[k] === "object" ? JSON.stringify(obj[k]) : String(obj[k]), 30)}`).join(", ")}
84
+
85
+ ` : "";
86
+ const content = extractContext(String(obj[matchedField]), matchedWord);
87
+ return {
88
+ intent: "EXEC",
89
+ message: `${context}\u{1F6D1} [${matchedField.toUpperCase()}]:
90
+ ${content}`
91
+ };
92
+ }
43
93
  const codeKeys = [
44
94
  "command",
45
95
  "cmd",
@@ -60,14 +110,18 @@ function formatArgs(args) {
60
110
  if (foundKey) {
61
111
  const val = obj[foundKey];
62
112
  const str = typeof val === "string" ? val : JSON.stringify(val);
63
- return `[${foundKey.toUpperCase()}]:
64
- ${smartTruncate(str, 500)}`;
113
+ return {
114
+ intent: "EXEC",
115
+ message: `[${foundKey.toUpperCase()}]:
116
+ ${smartTruncate(str, 500)}`
117
+ };
65
118
  }
66
- return Object.entries(obj).slice(0, 5).map(
119
+ const msg = Object.entries(obj).slice(0, 5).map(
67
120
  ([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
68
121
  ).join("\n");
122
+ return { intent: "EXEC", message: msg };
69
123
  }
70
- return smartTruncate(JSON.stringify(parsed), 200);
124
+ return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
71
125
  }
72
126
  function sendDesktopNotification(title, body) {
73
127
  if (isTestEnv()) return;
@@ -120,10 +174,11 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
120
174
  }
121
175
  return lines.join("\n");
122
176
  }
123
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
177
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
124
178
  if (isTestEnv()) return "deny";
125
- const formattedArgs = formatArgs(args);
126
- const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
179
+ const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
180
+ const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
181
+ const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
127
182
  const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
128
183
  process.stderr.write(chalk.yellow(`
129
184
  \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
@@ -201,10 +256,10 @@ end run`;
201
256
  }
202
257
 
203
258
  // src/core.ts
204
- var PAUSED_FILE = path.join(os.homedir(), ".node9", "PAUSED");
205
- var TRUST_FILE = path.join(os.homedir(), ".node9", "trust.json");
206
- var LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
207
- var HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
259
+ var PAUSED_FILE = path2.join(os.homedir(), ".node9", "PAUSED");
260
+ var TRUST_FILE = path2.join(os.homedir(), ".node9", "trust.json");
261
+ var LOCAL_AUDIT_LOG = path2.join(os.homedir(), ".node9", "audit.log");
262
+ var HOOK_DEBUG_LOG = path2.join(os.homedir(), ".node9", "hook-debug.log");
208
263
  function checkPause() {
209
264
  try {
210
265
  if (!fs.existsSync(PAUSED_FILE)) return { paused: false };
@@ -222,7 +277,7 @@ function checkPause() {
222
277
  }
223
278
  }
224
279
  function atomicWriteSync(filePath, data, options) {
225
- const dir = path.dirname(filePath);
280
+ const dir = path2.dirname(filePath);
226
281
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
227
282
  const tmpPath = `${filePath}.${os.hostname()}.${process.pid}.tmp`;
228
283
  fs.writeFileSync(tmpPath, data, options);
@@ -273,7 +328,7 @@ function writeTrustSession(toolName, durationMs) {
273
328
  }
274
329
  function appendToLog(logPath, entry) {
275
330
  try {
276
- const dir = path.dirname(logPath);
331
+ const dir = path2.dirname(logPath);
277
332
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
278
333
  fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
279
334
  } catch {
@@ -317,9 +372,21 @@ function matchesPattern(text, patterns) {
317
372
  const withoutDotSlash = text.replace(/^\.\//, "");
318
373
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
319
374
  }
320
- function getNestedValue(obj, path6) {
375
+ function getNestedValue(obj, path7) {
321
376
  if (!obj || typeof obj !== "object") return null;
322
- return path6.split(".").reduce((prev, curr) => prev?.[curr], obj);
377
+ return path7.split(".").reduce((prev, curr) => prev?.[curr], obj);
378
+ }
379
+ function shouldSnapshot(toolName, args, config) {
380
+ if (!config.settings.enableUndo) return false;
381
+ const snap = config.policy.snapshot;
382
+ if (!snap.tools.includes(toolName.toLowerCase())) return false;
383
+ const a = args && typeof args === "object" ? args : {};
384
+ const filePath = String(a.file_path ?? a.path ?? a.filename ?? "");
385
+ if (filePath) {
386
+ if (snap.ignorePaths.length && pm(snap.ignorePaths)(filePath)) return false;
387
+ if (snap.onlyPaths.length && !pm(snap.onlyPaths)(filePath)) return false;
388
+ }
389
+ return true;
323
390
  }
324
391
  function evaluateSmartConditions(args, rule) {
325
392
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -508,6 +575,18 @@ var DEFAULT_CONFIG = {
508
575
  "terminal.execute": "command",
509
576
  "postgres:query": "sql"
510
577
  },
578
+ snapshot: {
579
+ tools: [
580
+ "str_replace_based_edit_tool",
581
+ "write_file",
582
+ "edit_file",
583
+ "create_file",
584
+ "edit",
585
+ "replace"
586
+ ],
587
+ onlyPaths: [],
588
+ ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
589
+ },
511
590
  rules: [
512
591
  {
513
592
  action: "rm",
@@ -546,7 +625,7 @@ function _resetConfigCache() {
546
625
  }
547
626
  function getGlobalSettings() {
548
627
  try {
549
- const globalConfigPath = path.join(os.homedir(), ".node9", "config.json");
628
+ const globalConfigPath = path2.join(os.homedir(), ".node9", "config.json");
550
629
  if (fs.existsSync(globalConfigPath)) {
551
630
  const parsed = JSON.parse(fs.readFileSync(globalConfigPath, "utf-8"));
552
631
  const settings = parsed.settings || {};
@@ -570,7 +649,7 @@ function getGlobalSettings() {
570
649
  }
571
650
  function getInternalToken() {
572
651
  try {
573
- const pidFile = path.join(os.homedir(), ".node9", "daemon.pid");
652
+ const pidFile = path2.join(os.homedir(), ".node9", "daemon.pid");
574
653
  if (!fs.existsSync(pidFile)) return null;
575
654
  const data = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
576
655
  process.kill(data.pid, 0);
@@ -674,9 +753,29 @@ async function evaluatePolicy(toolName, args, agent) {
674
753
  })
675
754
  );
676
755
  if (isDangerous) {
756
+ let matchedField;
757
+ if (matchedDangerousWord && args && typeof args === "object" && !Array.isArray(args)) {
758
+ const obj = args;
759
+ for (const [key, value] of Object.entries(obj)) {
760
+ if (typeof value === "string") {
761
+ try {
762
+ if (new RegExp(
763
+ `\\b${matchedDangerousWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
764
+ "i"
765
+ ).test(value)) {
766
+ matchedField = key;
767
+ break;
768
+ }
769
+ } catch {
770
+ }
771
+ }
772
+ }
773
+ }
677
774
  return {
678
775
  decision: "review",
679
- blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`
776
+ blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
777
+ matchedWord: matchedDangerousWord,
778
+ matchedField
680
779
  };
681
780
  }
682
781
  if (config.settings.mode === "strict") {
@@ -688,9 +787,9 @@ async function evaluatePolicy(toolName, args, agent) {
688
787
  }
689
788
  async function explainPolicy(toolName, args) {
690
789
  const steps = [];
691
- const globalPath = path.join(os.homedir(), ".node9", "config.json");
692
- const projectPath = path.join(process.cwd(), "node9.config.json");
693
- const credsPath = path.join(os.homedir(), ".node9", "credentials.json");
790
+ const globalPath = path2.join(os.homedir(), ".node9", "config.json");
791
+ const projectPath = path2.join(process.cwd(), "node9.config.json");
792
+ const credsPath = path2.join(os.homedir(), ".node9", "credentials.json");
694
793
  const waterfall = [
695
794
  {
696
795
  tier: 1,
@@ -994,7 +1093,7 @@ var DAEMON_PORT = 7391;
994
1093
  var DAEMON_HOST = "127.0.0.1";
995
1094
  function isDaemonRunning() {
996
1095
  try {
997
- const pidFile = path.join(os.homedir(), ".node9", "daemon.pid");
1096
+ const pidFile = path2.join(os.homedir(), ".node9", "daemon.pid");
998
1097
  if (!fs.existsSync(pidFile)) return false;
999
1098
  const { pid, port } = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
1000
1099
  if (port !== DAEMON_PORT) return false;
@@ -1006,7 +1105,7 @@ function isDaemonRunning() {
1006
1105
  }
1007
1106
  function getPersistentDecision(toolName) {
1008
1107
  try {
1009
- const file = path.join(os.homedir(), ".node9", "decisions.json");
1108
+ const file = path2.join(os.homedir(), ".node9", "decisions.json");
1010
1109
  if (!fs.existsSync(file)) return null;
1011
1110
  const decisions = JSON.parse(fs.readFileSync(file, "utf-8"));
1012
1111
  const d = decisions[toolName];
@@ -1077,7 +1176,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
1077
1176
  signal: AbortSignal.timeout(3e3)
1078
1177
  });
1079
1178
  }
1080
- async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta) {
1179
+ async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
1081
1180
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1082
1181
  const pauseState = checkPause();
1083
1182
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
@@ -1097,6 +1196,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1097
1196
  }
1098
1197
  const isManual = meta?.agent === "Terminal";
1099
1198
  let explainableLabel = "Local Config";
1199
+ let policyMatchedField;
1200
+ let policyMatchedWord;
1100
1201
  if (config.settings.mode === "audit") {
1101
1202
  if (!isIgnoredTool(toolName)) {
1102
1203
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
@@ -1132,6 +1233,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1132
1233
  };
1133
1234
  }
1134
1235
  explainableLabel = policyResult.blockedByLabel || "Local Config";
1236
+ policyMatchedField = policyResult.matchedField;
1237
+ policyMatchedWord = policyResult.matchedWord;
1135
1238
  const persistent = getPersistentDecision(toolName);
1136
1239
  if (persistent === "allow") {
1137
1240
  if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
@@ -1224,7 +1327,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1224
1327
  racePromises.push(
1225
1328
  (async () => {
1226
1329
  try {
1227
- if (isDaemonRunning() && internalToken) {
1330
+ if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
1228
1331
  viewerId = await notifyDaemonViewer(toolName, args, meta).catch(() => null);
1229
1332
  }
1230
1333
  const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
@@ -1252,7 +1355,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1252
1355
  meta?.agent,
1253
1356
  explainableLabel,
1254
1357
  isRemoteLocked,
1255
- signal
1358
+ signal,
1359
+ policyMatchedField,
1360
+ policyMatchedWord
1256
1361
  );
1257
1362
  if (decision === "always_allow") {
1258
1363
  writeTrustSession(toolName, 36e5);
@@ -1269,7 +1374,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1269
1374
  })()
1270
1375
  );
1271
1376
  }
1272
- if (approvers.browser && isDaemonRunning()) {
1377
+ if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon) {
1273
1378
  racePromises.push(
1274
1379
  (async () => {
1275
1380
  try {
@@ -1405,8 +1510,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1405
1510
  }
1406
1511
  function getConfig() {
1407
1512
  if (cachedConfig) return cachedConfig;
1408
- const globalPath = path.join(os.homedir(), ".node9", "config.json");
1409
- const projectPath = path.join(process.cwd(), "node9.config.json");
1513
+ const globalPath = path2.join(os.homedir(), ".node9", "config.json");
1514
+ const projectPath = path2.join(process.cwd(), "node9.config.json");
1410
1515
  const globalConfig = tryLoadConfig(globalPath);
1411
1516
  const projectConfig = tryLoadConfig(projectPath);
1412
1517
  const mergedSettings = {
@@ -1419,7 +1524,12 @@ function getConfig() {
1419
1524
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
1420
1525
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
1421
1526
  rules: [...DEFAULT_CONFIG.policy.rules],
1422
- smartRules: [...DEFAULT_CONFIG.policy.smartRules]
1527
+ smartRules: [...DEFAULT_CONFIG.policy.smartRules],
1528
+ snapshot: {
1529
+ tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
1530
+ onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
1531
+ ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
1532
+ }
1423
1533
  };
1424
1534
  const applyLayer = (source) => {
1425
1535
  if (!source) return;
@@ -1431,6 +1541,7 @@ function getConfig() {
1431
1541
  if (s.enableHookLogDebug !== void 0)
1432
1542
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
1433
1543
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
1544
+ if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
1434
1545
  if (s.environment !== void 0) mergedSettings.environment = s.environment;
1435
1546
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
1436
1547
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -1439,6 +1550,12 @@ function getConfig() {
1439
1550
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
1440
1551
  if (p.rules) mergedPolicy.rules.push(...p.rules);
1441
1552
  if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
1553
+ if (p.snapshot) {
1554
+ const s2 = p.snapshot;
1555
+ if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
1556
+ if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
1557
+ if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
1558
+ }
1442
1559
  };
1443
1560
  applyLayer(globalConfig);
1444
1561
  applyLayer(projectConfig);
@@ -1446,6 +1563,9 @@ function getConfig() {
1446
1563
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
1447
1564
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
1448
1565
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
1566
+ mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
1567
+ mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
1568
+ mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
1449
1569
  cachedConfig = {
1450
1570
  settings: mergedSettings,
1451
1571
  policy: mergedPolicy,
@@ -1474,7 +1594,7 @@ function getCredentials() {
1474
1594
  };
1475
1595
  }
1476
1596
  try {
1477
- const credPath = path.join(os.homedir(), ".node9", "credentials.json");
1597
+ const credPath = path2.join(os.homedir(), ".node9", "credentials.json");
1478
1598
  if (fs.existsSync(credPath)) {
1479
1599
  const creds = JSON.parse(fs.readFileSync(credPath, "utf-8"));
1480
1600
  const profileName = process.env.NODE9_PROFILE || "default";
@@ -1592,7 +1712,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
1592
1712
 
1593
1713
  // src/setup.ts
1594
1714
  import fs2 from "fs";
1595
- import path2 from "path";
1715
+ import path3 from "path";
1596
1716
  import os2 from "os";
1597
1717
  import chalk3 from "chalk";
1598
1718
  import { confirm as confirm2 } from "@inquirer/prompts";
@@ -1617,14 +1737,14 @@ function readJson(filePath) {
1617
1737
  return null;
1618
1738
  }
1619
1739
  function writeJson(filePath, data) {
1620
- const dir = path2.dirname(filePath);
1740
+ const dir = path3.dirname(filePath);
1621
1741
  if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
1622
1742
  fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
1623
1743
  }
1624
1744
  async function setupClaude() {
1625
1745
  const homeDir2 = os2.homedir();
1626
- const mcpPath = path2.join(homeDir2, ".claude.json");
1627
- const hooksPath = path2.join(homeDir2, ".claude", "settings.json");
1746
+ const mcpPath = path3.join(homeDir2, ".claude.json");
1747
+ const hooksPath = path3.join(homeDir2, ".claude", "settings.json");
1628
1748
  const claudeConfig = readJson(mcpPath) ?? {};
1629
1749
  const settings = readJson(hooksPath) ?? {};
1630
1750
  const servers = claudeConfig.mcpServers ?? {};
@@ -1699,7 +1819,7 @@ async function setupClaude() {
1699
1819
  }
1700
1820
  async function setupGemini() {
1701
1821
  const homeDir2 = os2.homedir();
1702
- const settingsPath = path2.join(homeDir2, ".gemini", "settings.json");
1822
+ const settingsPath = path3.join(homeDir2, ".gemini", "settings.json");
1703
1823
  const settings = readJson(settingsPath) ?? {};
1704
1824
  const servers = settings.mcpServers ?? {};
1705
1825
  let anythingChanged = false;
@@ -1782,8 +1902,8 @@ async function setupGemini() {
1782
1902
  }
1783
1903
  async function setupCursor() {
1784
1904
  const homeDir2 = os2.homedir();
1785
- const mcpPath = path2.join(homeDir2, ".cursor", "mcp.json");
1786
- const hooksPath = path2.join(homeDir2, ".cursor", "hooks.json");
1905
+ const mcpPath = path3.join(homeDir2, ".cursor", "mcp.json");
1906
+ const hooksPath = path3.join(homeDir2, ".cursor", "hooks.json");
1787
1907
  const mcpConfig = readJson(mcpPath) ?? {};
1788
1908
  const hooksFile = readJson(hooksPath) ?? { version: 1 };
1789
1909
  const servers = mcpConfig.mcpServers ?? {};
@@ -2821,7 +2941,7 @@ var UI_HTML_TEMPLATE = ui_default;
2821
2941
  // src/daemon/index.ts
2822
2942
  import http from "http";
2823
2943
  import fs3 from "fs";
2824
- import path3 from "path";
2944
+ import path4 from "path";
2825
2945
  import os3 from "os";
2826
2946
  import { spawn as spawn2 } from "child_process";
2827
2947
  import { randomUUID } from "crypto";
@@ -2829,14 +2949,14 @@ import chalk4 from "chalk";
2829
2949
  var DAEMON_PORT2 = 7391;
2830
2950
  var DAEMON_HOST2 = "127.0.0.1";
2831
2951
  var homeDir = os3.homedir();
2832
- var DAEMON_PID_FILE = path3.join(homeDir, ".node9", "daemon.pid");
2833
- var DECISIONS_FILE = path3.join(homeDir, ".node9", "decisions.json");
2834
- var GLOBAL_CONFIG_FILE = path3.join(homeDir, ".node9", "config.json");
2835
- var CREDENTIALS_FILE = path3.join(homeDir, ".node9", "credentials.json");
2836
- var AUDIT_LOG_FILE = path3.join(homeDir, ".node9", "audit.log");
2837
- var TRUST_FILE2 = path3.join(homeDir, ".node9", "trust.json");
2952
+ var DAEMON_PID_FILE = path4.join(homeDir, ".node9", "daemon.pid");
2953
+ var DECISIONS_FILE = path4.join(homeDir, ".node9", "decisions.json");
2954
+ var GLOBAL_CONFIG_FILE = path4.join(homeDir, ".node9", "config.json");
2955
+ var CREDENTIALS_FILE = path4.join(homeDir, ".node9", "credentials.json");
2956
+ var AUDIT_LOG_FILE = path4.join(homeDir, ".node9", "audit.log");
2957
+ var TRUST_FILE2 = path4.join(homeDir, ".node9", "trust.json");
2838
2958
  function atomicWriteSync2(filePath, data, options) {
2839
- const dir = path3.dirname(filePath);
2959
+ const dir = path4.dirname(filePath);
2840
2960
  if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
2841
2961
  const tmpPath = `${filePath}.${randomUUID()}.tmp`;
2842
2962
  fs3.writeFileSync(tmpPath, data, options);
@@ -2880,7 +3000,7 @@ function appendAuditLog(data) {
2880
3000
  decision: data.decision,
2881
3001
  source: "daemon"
2882
3002
  };
2883
- const dir = path3.dirname(AUDIT_LOG_FILE);
3003
+ const dir = path4.dirname(AUDIT_LOG_FILE);
2884
3004
  if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
2885
3005
  fs3.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
2886
3006
  } catch {
@@ -3085,25 +3205,70 @@ data: ${JSON.stringify(readPersistentDecisions())}
3085
3205
  args: e.args,
3086
3206
  decision: "auto-deny"
3087
3207
  });
3088
- if (e.waiter) e.waiter("deny");
3089
- else e.earlyDecision = "deny";
3208
+ if (e.waiter) e.waiter("deny", "No response \u2014 auto-denied after timeout");
3209
+ else {
3210
+ e.earlyDecision = "deny";
3211
+ e.earlyReason = "No response \u2014 auto-denied after timeout";
3212
+ }
3090
3213
  pending.delete(id);
3091
3214
  broadcast("remove", { id });
3092
3215
  }
3093
3216
  }, AUTO_DENY_MS)
3094
3217
  };
3095
3218
  pending.set(id, entry);
3096
- broadcast("add", {
3097
- id,
3219
+ const browserEnabled = getConfig().settings.approvers?.browser !== false;
3220
+ if (browserEnabled) {
3221
+ broadcast("add", {
3222
+ id,
3223
+ toolName,
3224
+ args,
3225
+ slackDelegated: entry.slackDelegated,
3226
+ agent: entry.agent,
3227
+ mcpServer: entry.mcpServer
3228
+ });
3229
+ if (sseClients.size === 0 && !autoStarted)
3230
+ openBrowser(`http://127.0.0.1:${DAEMON_PORT2}/`);
3231
+ }
3232
+ res.writeHead(200, { "Content-Type": "application/json" });
3233
+ res.end(JSON.stringify({ id }));
3234
+ authorizeHeadless(
3098
3235
  toolName,
3099
3236
  args,
3100
- slackDelegated: entry.slackDelegated,
3101
- agent: entry.agent,
3102
- mcpServer: entry.mcpServer
3237
+ false,
3238
+ {
3239
+ agent: typeof agent === "string" ? agent : void 0,
3240
+ mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
3241
+ },
3242
+ { calledFromDaemon: true }
3243
+ ).then((result) => {
3244
+ const e = pending.get(id);
3245
+ if (!e) return;
3246
+ if (result.noApprovalMechanism) return;
3247
+ clearTimeout(e.timer);
3248
+ const decision = result.approved ? "allow" : "deny";
3249
+ appendAuditLog({ toolName: e.toolName, args: e.args, decision });
3250
+ if (e.waiter) {
3251
+ e.waiter(decision, result.reason);
3252
+ pending.delete(id);
3253
+ broadcast("remove", { id });
3254
+ } else {
3255
+ e.earlyDecision = decision;
3256
+ e.earlyReason = result.reason;
3257
+ }
3258
+ }).catch((err) => {
3259
+ const e = pending.get(id);
3260
+ if (!e) return;
3261
+ clearTimeout(e.timer);
3262
+ const reason = err?.reason || "No response \u2014 request timed out";
3263
+ if (e.waiter) e.waiter("deny", reason);
3264
+ else {
3265
+ e.earlyDecision = "deny";
3266
+ e.earlyReason = reason;
3267
+ }
3268
+ pending.delete(id);
3269
+ broadcast("remove", { id });
3103
3270
  });
3104
- if (sseClients.size === 0 && !autoStarted) openBrowser(`http://127.0.0.1:${DAEMON_PORT2}/`);
3105
- res.writeHead(200, { "Content-Type": "application/json" });
3106
- return res.end(JSON.stringify({ id }));
3271
+ return;
3107
3272
  } catch {
3108
3273
  res.writeHead(400).end();
3109
3274
  }
@@ -3113,12 +3278,18 @@ data: ${JSON.stringify(readPersistentDecisions())}
3113
3278
  const entry = pending.get(id);
3114
3279
  if (!entry) return res.writeHead(404).end();
3115
3280
  if (entry.earlyDecision) {
3281
+ pending.delete(id);
3282
+ broadcast("remove", { id });
3116
3283
  res.writeHead(200, { "Content-Type": "application/json" });
3117
- return res.end(JSON.stringify({ decision: entry.earlyDecision }));
3284
+ const body = { decision: entry.earlyDecision };
3285
+ if (entry.earlyReason) body.reason = entry.earlyReason;
3286
+ return res.end(JSON.stringify(body));
3118
3287
  }
3119
- entry.waiter = (d) => {
3288
+ entry.waiter = (d, reason) => {
3120
3289
  res.writeHead(200, { "Content-Type": "application/json" });
3121
- res.end(JSON.stringify({ decision: d }));
3290
+ const body = { decision: d };
3291
+ if (reason) body.reason = reason;
3292
+ res.end(JSON.stringify(body));
3122
3293
  };
3123
3294
  return;
3124
3295
  }
@@ -3128,7 +3299,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
3128
3299
  const id = pathname.split("/").pop();
3129
3300
  const entry = pending.get(id);
3130
3301
  if (!entry) return res.writeHead(404).end();
3131
- const { decision, persist, trustDuration } = JSON.parse(await readBody(req));
3302
+ const { decision, persist, trustDuration, reason } = JSON.parse(await readBody(req));
3132
3303
  if (decision === "trust" && trustDuration) {
3133
3304
  const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
3134
3305
  writeTrustEntry(entry.toolName, ms);
@@ -3153,8 +3324,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
3153
3324
  decision: resolvedDecision
3154
3325
  });
3155
3326
  clearTimeout(entry.timer);
3156
- if (entry.waiter) entry.waiter(resolvedDecision);
3157
- else entry.earlyDecision = resolvedDecision;
3327
+ if (entry.waiter) entry.waiter(resolvedDecision, reason);
3328
+ else {
3329
+ entry.earlyDecision = resolvedDecision;
3330
+ entry.earlyReason = reason;
3331
+ }
3158
3332
  pending.delete(id);
3159
3333
  broadcast("remove", { id });
3160
3334
  res.writeHead(200);
@@ -3317,16 +3491,16 @@ import { execa } from "execa";
3317
3491
  import chalk5 from "chalk";
3318
3492
  import readline from "readline";
3319
3493
  import fs5 from "fs";
3320
- import path5 from "path";
3494
+ import path6 from "path";
3321
3495
  import os5 from "os";
3322
3496
 
3323
3497
  // src/undo.ts
3324
3498
  import { spawnSync } from "child_process";
3325
3499
  import fs4 from "fs";
3326
- import path4 from "path";
3500
+ import path5 from "path";
3327
3501
  import os4 from "os";
3328
- var SNAPSHOT_STACK_PATH = path4.join(os4.homedir(), ".node9", "snapshots.json");
3329
- var UNDO_LATEST_PATH = path4.join(os4.homedir(), ".node9", "undo_latest.txt");
3502
+ var SNAPSHOT_STACK_PATH = path5.join(os4.homedir(), ".node9", "snapshots.json");
3503
+ var UNDO_LATEST_PATH = path5.join(os4.homedir(), ".node9", "undo_latest.txt");
3330
3504
  var MAX_SNAPSHOTS = 10;
3331
3505
  function readStack() {
3332
3506
  try {
@@ -3337,7 +3511,7 @@ function readStack() {
3337
3511
  return [];
3338
3512
  }
3339
3513
  function writeStack(stack) {
3340
- const dir = path4.dirname(SNAPSHOT_STACK_PATH);
3514
+ const dir = path5.dirname(SNAPSHOT_STACK_PATH);
3341
3515
  if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
3342
3516
  fs4.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
3343
3517
  }
@@ -3355,8 +3529,8 @@ function buildArgsSummary(tool, args) {
3355
3529
  async function createShadowSnapshot(tool = "unknown", args = {}) {
3356
3530
  try {
3357
3531
  const cwd = process.cwd();
3358
- if (!fs4.existsSync(path4.join(cwd, ".git"))) return null;
3359
- const tempIndex = path4.join(cwd, ".git", `node9_index_${Date.now()}`);
3532
+ if (!fs4.existsSync(path5.join(cwd, ".git"))) return null;
3533
+ const tempIndex = path5.join(cwd, ".git", `node9_index_${Date.now()}`);
3360
3534
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
3361
3535
  spawnSync("git", ["add", "-A"], { env });
3362
3536
  const treeRes = spawnSync("git", ["write-tree"], { env });
@@ -3420,7 +3594,7 @@ function applyUndo(hash, cwd) {
3420
3594
  const tracked = spawnSync("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
3421
3595
  const untracked = spawnSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
3422
3596
  for (const file of [...tracked, ...untracked]) {
3423
- const fullPath = path4.join(dir, file);
3597
+ const fullPath = path5.join(dir, file);
3424
3598
  if (!snapshotFiles.has(file) && fs4.existsSync(fullPath)) {
3425
3599
  fs4.unlinkSync(fullPath);
3426
3600
  }
@@ -3434,7 +3608,7 @@ function applyUndo(hash, cwd) {
3434
3608
  // src/cli.ts
3435
3609
  import { confirm as confirm3 } from "@inquirer/prompts";
3436
3610
  var { version } = JSON.parse(
3437
- fs5.readFileSync(path5.join(__dirname, "../package.json"), "utf-8")
3611
+ fs5.readFileSync(path6.join(__dirname, "../package.json"), "utf-8")
3438
3612
  );
3439
3613
  function parseDuration(str) {
3440
3614
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -3627,9 +3801,9 @@ async function runProxy(targetCommand) {
3627
3801
  }
3628
3802
  program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
3629
3803
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
3630
- const credPath = path5.join(os5.homedir(), ".node9", "credentials.json");
3631
- if (!fs5.existsSync(path5.dirname(credPath)))
3632
- fs5.mkdirSync(path5.dirname(credPath), { recursive: true });
3804
+ const credPath = path6.join(os5.homedir(), ".node9", "credentials.json");
3805
+ if (!fs5.existsSync(path6.dirname(credPath)))
3806
+ fs5.mkdirSync(path6.dirname(credPath), { recursive: true });
3633
3807
  const profileName = options.profile || "default";
3634
3808
  let existingCreds = {};
3635
3809
  try {
@@ -3648,7 +3822,7 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
3648
3822
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
3649
3823
  fs5.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
3650
3824
  if (profileName === "default") {
3651
- const configPath = path5.join(os5.homedir(), ".node9", "config.json");
3825
+ const configPath = path6.join(os5.homedir(), ".node9", "config.json");
3652
3826
  let config = {};
3653
3827
  try {
3654
3828
  if (fs5.existsSync(configPath))
@@ -3665,8 +3839,8 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
3665
3839
  };
3666
3840
  approvers.cloud = !options.local;
3667
3841
  s.approvers = approvers;
3668
- if (!fs5.existsSync(path5.dirname(configPath)))
3669
- fs5.mkdirSync(path5.dirname(configPath), { recursive: true });
3842
+ if (!fs5.existsSync(path6.dirname(configPath)))
3843
+ fs5.mkdirSync(path6.dirname(configPath), { recursive: true });
3670
3844
  fs5.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
3671
3845
  }
3672
3846
  if (options.profile && profileName !== "default") {
@@ -3752,7 +3926,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3752
3926
  );
3753
3927
  }
3754
3928
  section("Configuration");
3755
- const globalConfigPath = path5.join(homeDir2, ".node9", "config.json");
3929
+ const globalConfigPath = path6.join(homeDir2, ".node9", "config.json");
3756
3930
  if (fs5.existsSync(globalConfigPath)) {
3757
3931
  try {
3758
3932
  JSON.parse(fs5.readFileSync(globalConfigPath, "utf-8"));
@@ -3763,7 +3937,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3763
3937
  } else {
3764
3938
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
3765
3939
  }
3766
- const projectConfigPath = path5.join(process.cwd(), "node9.config.json");
3940
+ const projectConfigPath = path6.join(process.cwd(), "node9.config.json");
3767
3941
  if (fs5.existsSync(projectConfigPath)) {
3768
3942
  try {
3769
3943
  JSON.parse(fs5.readFileSync(projectConfigPath, "utf-8"));
@@ -3772,7 +3946,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3772
3946
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
3773
3947
  }
3774
3948
  }
3775
- const credsPath = path5.join(homeDir2, ".node9", "credentials.json");
3949
+ const credsPath = path6.join(homeDir2, ".node9", "credentials.json");
3776
3950
  if (fs5.existsSync(credsPath)) {
3777
3951
  pass("Cloud credentials found (~/.node9/credentials.json)");
3778
3952
  } else {
@@ -3782,7 +3956,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3782
3956
  );
3783
3957
  }
3784
3958
  section("Agent Hooks");
3785
- const claudeSettingsPath = path5.join(homeDir2, ".claude", "settings.json");
3959
+ const claudeSettingsPath = path6.join(homeDir2, ".claude", "settings.json");
3786
3960
  if (fs5.existsSync(claudeSettingsPath)) {
3787
3961
  try {
3788
3962
  const cs = JSON.parse(fs5.readFileSync(claudeSettingsPath, "utf-8"));
@@ -3798,7 +3972,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3798
3972
  } else {
3799
3973
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
3800
3974
  }
3801
- const geminiSettingsPath = path5.join(homeDir2, ".gemini", "settings.json");
3975
+ const geminiSettingsPath = path6.join(homeDir2, ".gemini", "settings.json");
3802
3976
  if (fs5.existsSync(geminiSettingsPath)) {
3803
3977
  try {
3804
3978
  const gs = JSON.parse(fs5.readFileSync(geminiSettingsPath, "utf-8"));
@@ -3814,7 +3988,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3814
3988
  } else {
3815
3989
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
3816
3990
  }
3817
- const cursorHooksPath = path5.join(homeDir2, ".cursor", "hooks.json");
3991
+ const cursorHooksPath = path6.join(homeDir2, ".cursor", "hooks.json");
3818
3992
  if (fs5.existsSync(cursorHooksPath)) {
3819
3993
  try {
3820
3994
  const cur = JSON.parse(fs5.readFileSync(cursorHooksPath, "utf-8"));
@@ -3919,7 +4093,7 @@ program.command("explain").description(
3919
4093
  console.log("");
3920
4094
  });
3921
4095
  program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
3922
- const configPath = path5.join(os5.homedir(), ".node9", "config.json");
4096
+ const configPath = path6.join(os5.homedir(), ".node9", "config.json");
3923
4097
  if (fs5.existsSync(configPath) && !options.force) {
3924
4098
  console.log(chalk5.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
3925
4099
  console.log(chalk5.gray(` Run with --force to overwrite.`));
@@ -3934,7 +4108,7 @@ program.command("init").description("Create ~/.node9/config.json with default po
3934
4108
  mode: safeMode
3935
4109
  }
3936
4110
  };
3937
- const dir = path5.dirname(configPath);
4111
+ const dir = path6.dirname(configPath);
3938
4112
  if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
3939
4113
  fs5.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
3940
4114
  console.log(chalk5.green(`\u2705 Global config created: ${configPath}`));
@@ -3954,7 +4128,7 @@ function formatRelativeTime(timestamp) {
3954
4128
  return new Date(timestamp).toLocaleDateString();
3955
4129
  }
3956
4130
  program.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
3957
- const logPath = path5.join(os5.homedir(), ".node9", "audit.log");
4131
+ const logPath = path6.join(os5.homedir(), ".node9", "audit.log");
3958
4132
  if (!fs5.existsSync(logPath)) {
3959
4133
  console.log(
3960
4134
  chalk5.yellow("No audit logs found. Run node9 with an agent to generate entries.")
@@ -4044,8 +4218,8 @@ program.command("status").description("Show current Node9 mode, policy source, a
4044
4218
  console.log("");
4045
4219
  const modeLabel = settings.mode === "audit" ? chalk5.blue("audit") : settings.mode === "strict" ? chalk5.red("strict") : chalk5.white("standard");
4046
4220
  console.log(` Mode: ${modeLabel}`);
4047
- const projectConfig = path5.join(process.cwd(), "node9.config.json");
4048
- const globalConfig = path5.join(os5.homedir(), ".node9", "config.json");
4221
+ const projectConfig = path6.join(process.cwd(), "node9.config.json");
4222
+ const globalConfig = path6.join(os5.homedir(), ".node9", "config.json");
4049
4223
  console.log(
4050
4224
  ` Local: ${fs5.existsSync(projectConfig) ? chalk5.green("Active (node9.config.json)") : chalk5.gray("Not present")}`
4051
4225
  );
@@ -4113,7 +4287,7 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
4113
4287
  } catch (err) {
4114
4288
  const tempConfig = getConfig();
4115
4289
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
4116
- const logPath = path5.join(os5.homedir(), ".node9", "hook-debug.log");
4290
+ const logPath = path6.join(os5.homedir(), ".node9", "hook-debug.log");
4117
4291
  const errMsg = err instanceof Error ? err.message : String(err);
4118
4292
  fs5.appendFileSync(
4119
4293
  logPath,
@@ -4133,9 +4307,9 @@ RAW: ${raw}
4133
4307
  }
4134
4308
  const config = getConfig();
4135
4309
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
4136
- const logPath = path5.join(os5.homedir(), ".node9", "hook-debug.log");
4137
- if (!fs5.existsSync(path5.dirname(logPath)))
4138
- fs5.mkdirSync(path5.dirname(logPath), { recursive: true });
4310
+ const logPath = path6.join(os5.homedir(), ".node9", "hook-debug.log");
4311
+ if (!fs5.existsSync(path6.dirname(logPath)))
4312
+ fs5.mkdirSync(path6.dirname(logPath), { recursive: true });
4139
4313
  fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
4140
4314
  `);
4141
4315
  }
@@ -4173,16 +4347,7 @@ RAW: ${raw}
4173
4347
  return;
4174
4348
  }
4175
4349
  const meta = { agent, mcpServer };
4176
- const STATE_CHANGING_TOOLS_PRE = [
4177
- "write_file",
4178
- "edit_file",
4179
- "edit",
4180
- "replace",
4181
- "terminal.execute",
4182
- "str_replace_based_edit_tool",
4183
- "create_file"
4184
- ];
4185
- if (config.settings.enableUndo && STATE_CHANGING_TOOLS_PRE.includes(toolName.toLowerCase())) {
4350
+ if (shouldSnapshot(toolName, toolInput, config)) {
4186
4351
  await createShadowSnapshot(toolName, toolInput);
4187
4352
  }
4188
4353
  const result = await authorizeHeadless(toolName, toolInput, false, meta);
@@ -4216,7 +4381,7 @@ RAW: ${raw}
4216
4381
  });
4217
4382
  } catch (err) {
4218
4383
  if (process.env.NODE9_DEBUG === "1") {
4219
- const logPath = path5.join(os5.homedir(), ".node9", "hook-debug.log");
4384
+ const logPath = path6.join(os5.homedir(), ".node9", "hook-debug.log");
4220
4385
  const errMsg = err instanceof Error ? err.message : String(err);
4221
4386
  fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
4222
4387
  `);
@@ -4263,20 +4428,12 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
4263
4428
  decision: "allowed",
4264
4429
  source: "post-hook"
4265
4430
  };
4266
- const logPath = path5.join(os5.homedir(), ".node9", "audit.log");
4267
- if (!fs5.existsSync(path5.dirname(logPath)))
4268
- fs5.mkdirSync(path5.dirname(logPath), { recursive: true });
4431
+ const logPath = path6.join(os5.homedir(), ".node9", "audit.log");
4432
+ if (!fs5.existsSync(path6.dirname(logPath)))
4433
+ fs5.mkdirSync(path6.dirname(logPath), { recursive: true });
4269
4434
  fs5.appendFileSync(logPath, JSON.stringify(entry) + "\n");
4270
4435
  const config = getConfig();
4271
- const STATE_CHANGING_TOOLS = [
4272
- "bash",
4273
- "shell",
4274
- "write_file",
4275
- "edit_file",
4276
- "replace",
4277
- "terminal.execute"
4278
- ];
4279
- if (config.settings.enableUndo && STATE_CHANGING_TOOLS.includes(tool.toLowerCase())) {
4436
+ if (shouldSnapshot(tool, {}, config)) {
4280
4437
  await createShadowSnapshot();
4281
4438
  }
4282
4439
  } catch {
@@ -4448,7 +4605,7 @@ process.on("unhandledRejection", (reason) => {
4448
4605
  const isCheckHook = process.argv[2] === "check";
4449
4606
  if (isCheckHook) {
4450
4607
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
4451
- const logPath = path5.join(os5.homedir(), ".node9", "hook-debug.log");
4608
+ const logPath = path6.join(os5.homedir(), ".node9", "hook-debug.log");
4452
4609
  const msg = reason instanceof Error ? reason.message : String(reason);
4453
4610
  fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
4454
4611
  `);