@node9/proxy 1.0.7 → 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.js CHANGED
@@ -30,13 +30,14 @@ var import_commander = require("commander");
30
30
  var import_chalk2 = __toESM(require("chalk"));
31
31
  var import_prompts = require("@inquirer/prompts");
32
32
  var import_fs = __toESM(require("fs"));
33
- var import_path = __toESM(require("path"));
33
+ var import_path2 = __toESM(require("path"));
34
34
  var import_os = __toESM(require("os"));
35
35
  var import_picomatch = __toESM(require("picomatch"));
36
36
  var import_sh_syntax = require("sh-syntax");
37
37
 
38
38
  // src/ui/native.ts
39
39
  var import_child_process = require("child_process");
40
+ var import_path = __toESM(require("path"));
40
41
  var import_chalk = __toESM(require("chalk"));
41
42
  var isTestEnv = () => {
42
43
  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";
@@ -46,8 +47,29 @@ function smartTruncate(str, maxLen = 500) {
46
47
  const edge = Math.floor(maxLen / 2) - 3;
47
48
  return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
48
49
  }
49
- function formatArgs(args) {
50
- if (args === null || args === void 0) return "(none)";
50
+ function extractContext(text, matchedWord) {
51
+ const lines = text.split("\n");
52
+ if (lines.length <= 7 || !matchedWord) return smartTruncate(text, 500);
53
+ const escaped = matchedWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
54
+ const pattern = new RegExp(`\\b${escaped}\\b`, "i");
55
+ const allHits = lines.map((line, i) => ({ i, line })).filter(({ line }) => pattern.test(line));
56
+ if (allHits.length === 0) return smartTruncate(text, 500);
57
+ const nonComment = allHits.find(({ line }) => {
58
+ const trimmed = line.trim();
59
+ return !trimmed.startsWith("//") && !trimmed.startsWith("#");
60
+ });
61
+ const hitIndex = (nonComment ?? allHits[0]).i;
62
+ const start = Math.max(0, hitIndex - 3);
63
+ const end = Math.min(lines.length, hitIndex + 4);
64
+ const snippet = lines.slice(start, end).map((line, i) => `${start + i === hitIndex ? "\u{1F6D1} " : " "}${line}`).join("\n");
65
+ const head = start > 0 ? `... [${start} lines hidden] ...
66
+ ` : "";
67
+ const tail = end < lines.length ? `
68
+ ... [${lines.length - end} lines hidden] ...` : "";
69
+ return `${head}${snippet}${tail}`;
70
+ }
71
+ function formatArgs(args, matchedField, matchedWord) {
72
+ if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
51
73
  let parsed = args;
52
74
  if (typeof args === "string") {
53
75
  const trimmed = args.trim();
@@ -58,11 +80,39 @@ function formatArgs(args) {
58
80
  parsed = args;
59
81
  }
60
82
  } else {
61
- return smartTruncate(args, 600);
83
+ return { message: smartTruncate(args, 600), intent: "EXEC" };
62
84
  }
63
85
  }
64
86
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
65
87
  const obj = parsed;
88
+ if (obj.old_string !== void 0 && obj.new_string !== void 0) {
89
+ const file = obj.file_path ? import_path.default.basename(String(obj.file_path)) : "file";
90
+ const oldPreview = smartTruncate(String(obj.old_string), 120);
91
+ const newPreview = extractContext(String(obj.new_string), matchedWord);
92
+ return {
93
+ intent: "EDIT",
94
+ message: `\u{1F4DD} EDITING: ${file}
95
+ \u{1F4C2} PATH: ${obj.file_path}
96
+
97
+ --- REPLACING ---
98
+ ${oldPreview}
99
+
100
+ +++ NEW CODE +++
101
+ ${newPreview}`
102
+ };
103
+ }
104
+ if (matchedField && obj[matchedField] !== void 0) {
105
+ const otherKeys = Object.keys(obj).filter((k) => k !== matchedField);
106
+ 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(", ")}
107
+
108
+ ` : "";
109
+ const content = extractContext(String(obj[matchedField]), matchedWord);
110
+ return {
111
+ intent: "EXEC",
112
+ message: `${context}\u{1F6D1} [${matchedField.toUpperCase()}]:
113
+ ${content}`
114
+ };
115
+ }
66
116
  const codeKeys = [
67
117
  "command",
68
118
  "cmd",
@@ -83,14 +133,18 @@ function formatArgs(args) {
83
133
  if (foundKey) {
84
134
  const val = obj[foundKey];
85
135
  const str = typeof val === "string" ? val : JSON.stringify(val);
86
- return `[${foundKey.toUpperCase()}]:
87
- ${smartTruncate(str, 500)}`;
136
+ return {
137
+ intent: "EXEC",
138
+ message: `[${foundKey.toUpperCase()}]:
139
+ ${smartTruncate(str, 500)}`
140
+ };
88
141
  }
89
- return Object.entries(obj).slice(0, 5).map(
142
+ const msg = Object.entries(obj).slice(0, 5).map(
90
143
  ([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
91
144
  ).join("\n");
145
+ return { intent: "EXEC", message: msg };
92
146
  }
93
- return smartTruncate(JSON.stringify(parsed), 200);
147
+ return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
94
148
  }
95
149
  function sendDesktopNotification(title, body) {
96
150
  if (isTestEnv()) return;
@@ -143,10 +197,11 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
143
197
  }
144
198
  return lines.join("\n");
145
199
  }
146
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
200
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
147
201
  if (isTestEnv()) return "deny";
148
- const formattedArgs = formatArgs(args);
149
- const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
202
+ const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
203
+ const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
204
+ const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
150
205
  const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
151
206
  process.stderr.write(import_chalk.default.yellow(`
152
207
  \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
@@ -224,10 +279,10 @@ end run`;
224
279
  }
225
280
 
226
281
  // src/core.ts
227
- var PAUSED_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "PAUSED");
228
- var TRUST_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "trust.json");
229
- var LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
230
- var HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
282
+ var PAUSED_FILE = import_path2.default.join(import_os.default.homedir(), ".node9", "PAUSED");
283
+ var TRUST_FILE = import_path2.default.join(import_os.default.homedir(), ".node9", "trust.json");
284
+ var LOCAL_AUDIT_LOG = import_path2.default.join(import_os.default.homedir(), ".node9", "audit.log");
285
+ var HOOK_DEBUG_LOG = import_path2.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
231
286
  function checkPause() {
232
287
  try {
233
288
  if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
@@ -245,7 +300,7 @@ function checkPause() {
245
300
  }
246
301
  }
247
302
  function atomicWriteSync(filePath, data, options) {
248
- const dir = import_path.default.dirname(filePath);
303
+ const dir = import_path2.default.dirname(filePath);
249
304
  if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
250
305
  const tmpPath = `${filePath}.${import_os.default.hostname()}.${process.pid}.tmp`;
251
306
  import_fs.default.writeFileSync(tmpPath, data, options);
@@ -296,7 +351,7 @@ function writeTrustSession(toolName, durationMs) {
296
351
  }
297
352
  function appendToLog(logPath, entry) {
298
353
  try {
299
- const dir = import_path.default.dirname(logPath);
354
+ const dir = import_path2.default.dirname(logPath);
300
355
  if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
301
356
  import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
302
357
  } catch {
@@ -340,9 +395,21 @@ function matchesPattern(text, patterns) {
340
395
  const withoutDotSlash = text.replace(/^\.\//, "");
341
396
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
342
397
  }
343
- function getNestedValue(obj, path6) {
398
+ function getNestedValue(obj, path7) {
344
399
  if (!obj || typeof obj !== "object") return null;
345
- return path6.split(".").reduce((prev, curr) => prev?.[curr], obj);
400
+ return path7.split(".").reduce((prev, curr) => prev?.[curr], obj);
401
+ }
402
+ function shouldSnapshot(toolName, args, config) {
403
+ if (!config.settings.enableUndo) return false;
404
+ const snap = config.policy.snapshot;
405
+ if (!snap.tools.includes(toolName.toLowerCase())) return false;
406
+ const a = args && typeof args === "object" ? args : {};
407
+ const filePath = String(a.file_path ?? a.path ?? a.filename ?? "");
408
+ if (filePath) {
409
+ if (snap.ignorePaths.length && (0, import_picomatch.default)(snap.ignorePaths)(filePath)) return false;
410
+ if (snap.onlyPaths.length && !(0, import_picomatch.default)(snap.onlyPaths)(filePath)) return false;
411
+ }
412
+ return true;
346
413
  }
347
414
  function evaluateSmartConditions(args, rule) {
348
415
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -531,6 +598,18 @@ var DEFAULT_CONFIG = {
531
598
  "terminal.execute": "command",
532
599
  "postgres:query": "sql"
533
600
  },
601
+ snapshot: {
602
+ tools: [
603
+ "str_replace_based_edit_tool",
604
+ "write_file",
605
+ "edit_file",
606
+ "create_file",
607
+ "edit",
608
+ "replace"
609
+ ],
610
+ onlyPaths: [],
611
+ ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
612
+ },
534
613
  rules: [
535
614
  {
536
615
  action: "rm",
@@ -569,7 +648,7 @@ function _resetConfigCache() {
569
648
  }
570
649
  function getGlobalSettings() {
571
650
  try {
572
- const globalConfigPath = import_path.default.join(import_os.default.homedir(), ".node9", "config.json");
651
+ const globalConfigPath = import_path2.default.join(import_os.default.homedir(), ".node9", "config.json");
573
652
  if (import_fs.default.existsSync(globalConfigPath)) {
574
653
  const parsed = JSON.parse(import_fs.default.readFileSync(globalConfigPath, "utf-8"));
575
654
  const settings = parsed.settings || {};
@@ -593,7 +672,7 @@ function getGlobalSettings() {
593
672
  }
594
673
  function getInternalToken() {
595
674
  try {
596
- const pidFile = import_path.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
675
+ const pidFile = import_path2.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
597
676
  if (!import_fs.default.existsSync(pidFile)) return null;
598
677
  const data = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
599
678
  process.kill(data.pid, 0);
@@ -697,9 +776,29 @@ async function evaluatePolicy(toolName, args, agent) {
697
776
  })
698
777
  );
699
778
  if (isDangerous) {
779
+ let matchedField;
780
+ if (matchedDangerousWord && args && typeof args === "object" && !Array.isArray(args)) {
781
+ const obj = args;
782
+ for (const [key, value] of Object.entries(obj)) {
783
+ if (typeof value === "string") {
784
+ try {
785
+ if (new RegExp(
786
+ `\\b${matchedDangerousWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
787
+ "i"
788
+ ).test(value)) {
789
+ matchedField = key;
790
+ break;
791
+ }
792
+ } catch {
793
+ }
794
+ }
795
+ }
796
+ }
700
797
  return {
701
798
  decision: "review",
702
- blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`
799
+ blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
800
+ matchedWord: matchedDangerousWord,
801
+ matchedField
703
802
  };
704
803
  }
705
804
  if (config.settings.mode === "strict") {
@@ -711,9 +810,9 @@ async function evaluatePolicy(toolName, args, agent) {
711
810
  }
712
811
  async function explainPolicy(toolName, args) {
713
812
  const steps = [];
714
- const globalPath = import_path.default.join(import_os.default.homedir(), ".node9", "config.json");
715
- const projectPath = import_path.default.join(process.cwd(), "node9.config.json");
716
- const credsPath = import_path.default.join(import_os.default.homedir(), ".node9", "credentials.json");
813
+ const globalPath = import_path2.default.join(import_os.default.homedir(), ".node9", "config.json");
814
+ const projectPath = import_path2.default.join(process.cwd(), "node9.config.json");
815
+ const credsPath = import_path2.default.join(import_os.default.homedir(), ".node9", "credentials.json");
717
816
  const waterfall = [
718
817
  {
719
818
  tier: 1,
@@ -1017,7 +1116,7 @@ var DAEMON_PORT = 7391;
1017
1116
  var DAEMON_HOST = "127.0.0.1";
1018
1117
  function isDaemonRunning() {
1019
1118
  try {
1020
- const pidFile = import_path.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
1119
+ const pidFile = import_path2.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
1021
1120
  if (!import_fs.default.existsSync(pidFile)) return false;
1022
1121
  const { pid, port } = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
1023
1122
  if (port !== DAEMON_PORT) return false;
@@ -1029,7 +1128,7 @@ function isDaemonRunning() {
1029
1128
  }
1030
1129
  function getPersistentDecision(toolName) {
1031
1130
  try {
1032
- const file = import_path.default.join(import_os.default.homedir(), ".node9", "decisions.json");
1131
+ const file = import_path2.default.join(import_os.default.homedir(), ".node9", "decisions.json");
1033
1132
  if (!import_fs.default.existsSync(file)) return null;
1034
1133
  const decisions = JSON.parse(import_fs.default.readFileSync(file, "utf-8"));
1035
1134
  const d = decisions[toolName];
@@ -1100,7 +1199,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
1100
1199
  signal: AbortSignal.timeout(3e3)
1101
1200
  });
1102
1201
  }
1103
- async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta) {
1202
+ async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
1104
1203
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1105
1204
  const pauseState = checkPause();
1106
1205
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
@@ -1120,6 +1219,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1120
1219
  }
1121
1220
  const isManual = meta?.agent === "Terminal";
1122
1221
  let explainableLabel = "Local Config";
1222
+ let policyMatchedField;
1223
+ let policyMatchedWord;
1123
1224
  if (config.settings.mode === "audit") {
1124
1225
  if (!isIgnoredTool(toolName)) {
1125
1226
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
@@ -1155,6 +1256,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1155
1256
  };
1156
1257
  }
1157
1258
  explainableLabel = policyResult.blockedByLabel || "Local Config";
1259
+ policyMatchedField = policyResult.matchedField;
1260
+ policyMatchedWord = policyResult.matchedWord;
1158
1261
  const persistent = getPersistentDecision(toolName);
1159
1262
  if (persistent === "allow") {
1160
1263
  if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
@@ -1247,7 +1350,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1247
1350
  racePromises.push(
1248
1351
  (async () => {
1249
1352
  try {
1250
- if (isDaemonRunning() && internalToken) {
1353
+ if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
1251
1354
  viewerId = await notifyDaemonViewer(toolName, args, meta).catch(() => null);
1252
1355
  }
1253
1356
  const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
@@ -1275,7 +1378,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1275
1378
  meta?.agent,
1276
1379
  explainableLabel,
1277
1380
  isRemoteLocked,
1278
- signal
1381
+ signal,
1382
+ policyMatchedField,
1383
+ policyMatchedWord
1279
1384
  );
1280
1385
  if (decision === "always_allow") {
1281
1386
  writeTrustSession(toolName, 36e5);
@@ -1292,7 +1397,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1292
1397
  })()
1293
1398
  );
1294
1399
  }
1295
- if (approvers.browser && isDaemonRunning()) {
1400
+ if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon) {
1296
1401
  racePromises.push(
1297
1402
  (async () => {
1298
1403
  try {
@@ -1428,8 +1533,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1428
1533
  }
1429
1534
  function getConfig() {
1430
1535
  if (cachedConfig) return cachedConfig;
1431
- const globalPath = import_path.default.join(import_os.default.homedir(), ".node9", "config.json");
1432
- const projectPath = import_path.default.join(process.cwd(), "node9.config.json");
1536
+ const globalPath = import_path2.default.join(import_os.default.homedir(), ".node9", "config.json");
1537
+ const projectPath = import_path2.default.join(process.cwd(), "node9.config.json");
1433
1538
  const globalConfig = tryLoadConfig(globalPath);
1434
1539
  const projectConfig = tryLoadConfig(projectPath);
1435
1540
  const mergedSettings = {
@@ -1442,7 +1547,12 @@ function getConfig() {
1442
1547
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
1443
1548
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
1444
1549
  rules: [...DEFAULT_CONFIG.policy.rules],
1445
- smartRules: [...DEFAULT_CONFIG.policy.smartRules]
1550
+ smartRules: [...DEFAULT_CONFIG.policy.smartRules],
1551
+ snapshot: {
1552
+ tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
1553
+ onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
1554
+ ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
1555
+ }
1446
1556
  };
1447
1557
  const applyLayer = (source) => {
1448
1558
  if (!source) return;
@@ -1454,6 +1564,7 @@ function getConfig() {
1454
1564
  if (s.enableHookLogDebug !== void 0)
1455
1565
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
1456
1566
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
1567
+ if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
1457
1568
  if (s.environment !== void 0) mergedSettings.environment = s.environment;
1458
1569
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
1459
1570
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -1462,6 +1573,12 @@ function getConfig() {
1462
1573
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
1463
1574
  if (p.rules) mergedPolicy.rules.push(...p.rules);
1464
1575
  if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
1576
+ if (p.snapshot) {
1577
+ const s2 = p.snapshot;
1578
+ if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
1579
+ if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
1580
+ if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
1581
+ }
1465
1582
  };
1466
1583
  applyLayer(globalConfig);
1467
1584
  applyLayer(projectConfig);
@@ -1469,6 +1586,9 @@ function getConfig() {
1469
1586
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
1470
1587
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
1471
1588
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
1589
+ mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
1590
+ mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
1591
+ mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
1472
1592
  cachedConfig = {
1473
1593
  settings: mergedSettings,
1474
1594
  policy: mergedPolicy,
@@ -1497,7 +1617,7 @@ function getCredentials() {
1497
1617
  };
1498
1618
  }
1499
1619
  try {
1500
- const credPath = import_path.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1620
+ const credPath = import_path2.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1501
1621
  if (import_fs.default.existsSync(credPath)) {
1502
1622
  const creds = JSON.parse(import_fs.default.readFileSync(credPath, "utf-8"));
1503
1623
  const profileName = process.env.NODE9_PROFILE || "default";
@@ -1615,7 +1735,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
1615
1735
 
1616
1736
  // src/setup.ts
1617
1737
  var import_fs2 = __toESM(require("fs"));
1618
- var import_path2 = __toESM(require("path"));
1738
+ var import_path3 = __toESM(require("path"));
1619
1739
  var import_os2 = __toESM(require("os"));
1620
1740
  var import_chalk3 = __toESM(require("chalk"));
1621
1741
  var import_prompts2 = require("@inquirer/prompts");
@@ -1640,14 +1760,14 @@ function readJson(filePath) {
1640
1760
  return null;
1641
1761
  }
1642
1762
  function writeJson(filePath, data) {
1643
- const dir = import_path2.default.dirname(filePath);
1763
+ const dir = import_path3.default.dirname(filePath);
1644
1764
  if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
1645
1765
  import_fs2.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
1646
1766
  }
1647
1767
  async function setupClaude() {
1648
1768
  const homeDir2 = import_os2.default.homedir();
1649
- const mcpPath = import_path2.default.join(homeDir2, ".claude.json");
1650
- const hooksPath = import_path2.default.join(homeDir2, ".claude", "settings.json");
1769
+ const mcpPath = import_path3.default.join(homeDir2, ".claude.json");
1770
+ const hooksPath = import_path3.default.join(homeDir2, ".claude", "settings.json");
1651
1771
  const claudeConfig = readJson(mcpPath) ?? {};
1652
1772
  const settings = readJson(hooksPath) ?? {};
1653
1773
  const servers = claudeConfig.mcpServers ?? {};
@@ -1722,7 +1842,7 @@ async function setupClaude() {
1722
1842
  }
1723
1843
  async function setupGemini() {
1724
1844
  const homeDir2 = import_os2.default.homedir();
1725
- const settingsPath = import_path2.default.join(homeDir2, ".gemini", "settings.json");
1845
+ const settingsPath = import_path3.default.join(homeDir2, ".gemini", "settings.json");
1726
1846
  const settings = readJson(settingsPath) ?? {};
1727
1847
  const servers = settings.mcpServers ?? {};
1728
1848
  let anythingChanged = false;
@@ -1805,8 +1925,8 @@ async function setupGemini() {
1805
1925
  }
1806
1926
  async function setupCursor() {
1807
1927
  const homeDir2 = import_os2.default.homedir();
1808
- const mcpPath = import_path2.default.join(homeDir2, ".cursor", "mcp.json");
1809
- const hooksPath = import_path2.default.join(homeDir2, ".cursor", "hooks.json");
1928
+ const mcpPath = import_path3.default.join(homeDir2, ".cursor", "mcp.json");
1929
+ const hooksPath = import_path3.default.join(homeDir2, ".cursor", "hooks.json");
1810
1930
  const mcpConfig = readJson(mcpPath) ?? {};
1811
1931
  const hooksFile = readJson(hooksPath) ?? { version: 1 };
1812
1932
  const servers = mcpConfig.mcpServers ?? {};
@@ -2844,7 +2964,7 @@ var UI_HTML_TEMPLATE = ui_default;
2844
2964
  // src/daemon/index.ts
2845
2965
  var import_http = __toESM(require("http"));
2846
2966
  var import_fs3 = __toESM(require("fs"));
2847
- var import_path3 = __toESM(require("path"));
2967
+ var import_path4 = __toESM(require("path"));
2848
2968
  var import_os3 = __toESM(require("os"));
2849
2969
  var import_child_process2 = require("child_process");
2850
2970
  var import_crypto = require("crypto");
@@ -2852,14 +2972,14 @@ var import_chalk4 = __toESM(require("chalk"));
2852
2972
  var DAEMON_PORT2 = 7391;
2853
2973
  var DAEMON_HOST2 = "127.0.0.1";
2854
2974
  var homeDir = import_os3.default.homedir();
2855
- var DAEMON_PID_FILE = import_path3.default.join(homeDir, ".node9", "daemon.pid");
2856
- var DECISIONS_FILE = import_path3.default.join(homeDir, ".node9", "decisions.json");
2857
- var GLOBAL_CONFIG_FILE = import_path3.default.join(homeDir, ".node9", "config.json");
2858
- var CREDENTIALS_FILE = import_path3.default.join(homeDir, ".node9", "credentials.json");
2859
- var AUDIT_LOG_FILE = import_path3.default.join(homeDir, ".node9", "audit.log");
2860
- var TRUST_FILE2 = import_path3.default.join(homeDir, ".node9", "trust.json");
2975
+ var DAEMON_PID_FILE = import_path4.default.join(homeDir, ".node9", "daemon.pid");
2976
+ var DECISIONS_FILE = import_path4.default.join(homeDir, ".node9", "decisions.json");
2977
+ var GLOBAL_CONFIG_FILE = import_path4.default.join(homeDir, ".node9", "config.json");
2978
+ var CREDENTIALS_FILE = import_path4.default.join(homeDir, ".node9", "credentials.json");
2979
+ var AUDIT_LOG_FILE = import_path4.default.join(homeDir, ".node9", "audit.log");
2980
+ var TRUST_FILE2 = import_path4.default.join(homeDir, ".node9", "trust.json");
2861
2981
  function atomicWriteSync2(filePath, data, options) {
2862
- const dir = import_path3.default.dirname(filePath);
2982
+ const dir = import_path4.default.dirname(filePath);
2863
2983
  if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
2864
2984
  const tmpPath = `${filePath}.${(0, import_crypto.randomUUID)()}.tmp`;
2865
2985
  import_fs3.default.writeFileSync(tmpPath, data, options);
@@ -2903,7 +3023,7 @@ function appendAuditLog(data) {
2903
3023
  decision: data.decision,
2904
3024
  source: "daemon"
2905
3025
  };
2906
- const dir = import_path3.default.dirname(AUDIT_LOG_FILE);
3026
+ const dir = import_path4.default.dirname(AUDIT_LOG_FILE);
2907
3027
  if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
2908
3028
  import_fs3.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
2909
3029
  } catch {
@@ -3108,25 +3228,70 @@ data: ${JSON.stringify(readPersistentDecisions())}
3108
3228
  args: e.args,
3109
3229
  decision: "auto-deny"
3110
3230
  });
3111
- if (e.waiter) e.waiter("deny");
3112
- else e.earlyDecision = "deny";
3231
+ if (e.waiter) e.waiter("deny", "No response \u2014 auto-denied after timeout");
3232
+ else {
3233
+ e.earlyDecision = "deny";
3234
+ e.earlyReason = "No response \u2014 auto-denied after timeout";
3235
+ }
3113
3236
  pending.delete(id);
3114
3237
  broadcast("remove", { id });
3115
3238
  }
3116
3239
  }, AUTO_DENY_MS)
3117
3240
  };
3118
3241
  pending.set(id, entry);
3119
- broadcast("add", {
3120
- id,
3242
+ const browserEnabled = getConfig().settings.approvers?.browser !== false;
3243
+ if (browserEnabled) {
3244
+ broadcast("add", {
3245
+ id,
3246
+ toolName,
3247
+ args,
3248
+ slackDelegated: entry.slackDelegated,
3249
+ agent: entry.agent,
3250
+ mcpServer: entry.mcpServer
3251
+ });
3252
+ if (sseClients.size === 0 && !autoStarted)
3253
+ openBrowser(`http://127.0.0.1:${DAEMON_PORT2}/`);
3254
+ }
3255
+ res.writeHead(200, { "Content-Type": "application/json" });
3256
+ res.end(JSON.stringify({ id }));
3257
+ authorizeHeadless(
3121
3258
  toolName,
3122
3259
  args,
3123
- slackDelegated: entry.slackDelegated,
3124
- agent: entry.agent,
3125
- mcpServer: entry.mcpServer
3260
+ false,
3261
+ {
3262
+ agent: typeof agent === "string" ? agent : void 0,
3263
+ mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
3264
+ },
3265
+ { calledFromDaemon: true }
3266
+ ).then((result) => {
3267
+ const e = pending.get(id);
3268
+ if (!e) return;
3269
+ if (result.noApprovalMechanism) return;
3270
+ clearTimeout(e.timer);
3271
+ const decision = result.approved ? "allow" : "deny";
3272
+ appendAuditLog({ toolName: e.toolName, args: e.args, decision });
3273
+ if (e.waiter) {
3274
+ e.waiter(decision, result.reason);
3275
+ pending.delete(id);
3276
+ broadcast("remove", { id });
3277
+ } else {
3278
+ e.earlyDecision = decision;
3279
+ e.earlyReason = result.reason;
3280
+ }
3281
+ }).catch((err) => {
3282
+ const e = pending.get(id);
3283
+ if (!e) return;
3284
+ clearTimeout(e.timer);
3285
+ const reason = err?.reason || "No response \u2014 request timed out";
3286
+ if (e.waiter) e.waiter("deny", reason);
3287
+ else {
3288
+ e.earlyDecision = "deny";
3289
+ e.earlyReason = reason;
3290
+ }
3291
+ pending.delete(id);
3292
+ broadcast("remove", { id });
3126
3293
  });
3127
- if (sseClients.size === 0 && !autoStarted) openBrowser(`http://127.0.0.1:${DAEMON_PORT2}/`);
3128
- res.writeHead(200, { "Content-Type": "application/json" });
3129
- return res.end(JSON.stringify({ id }));
3294
+ return;
3130
3295
  } catch {
3131
3296
  res.writeHead(400).end();
3132
3297
  }
@@ -3136,12 +3301,18 @@ data: ${JSON.stringify(readPersistentDecisions())}
3136
3301
  const entry = pending.get(id);
3137
3302
  if (!entry) return res.writeHead(404).end();
3138
3303
  if (entry.earlyDecision) {
3304
+ pending.delete(id);
3305
+ broadcast("remove", { id });
3139
3306
  res.writeHead(200, { "Content-Type": "application/json" });
3140
- return res.end(JSON.stringify({ decision: entry.earlyDecision }));
3307
+ const body = { decision: entry.earlyDecision };
3308
+ if (entry.earlyReason) body.reason = entry.earlyReason;
3309
+ return res.end(JSON.stringify(body));
3141
3310
  }
3142
- entry.waiter = (d) => {
3311
+ entry.waiter = (d, reason) => {
3143
3312
  res.writeHead(200, { "Content-Type": "application/json" });
3144
- res.end(JSON.stringify({ decision: d }));
3313
+ const body = { decision: d };
3314
+ if (reason) body.reason = reason;
3315
+ res.end(JSON.stringify(body));
3145
3316
  };
3146
3317
  return;
3147
3318
  }
@@ -3151,7 +3322,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
3151
3322
  const id = pathname.split("/").pop();
3152
3323
  const entry = pending.get(id);
3153
3324
  if (!entry) return res.writeHead(404).end();
3154
- const { decision, persist, trustDuration } = JSON.parse(await readBody(req));
3325
+ const { decision, persist, trustDuration, reason } = JSON.parse(await readBody(req));
3155
3326
  if (decision === "trust" && trustDuration) {
3156
3327
  const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
3157
3328
  writeTrustEntry(entry.toolName, ms);
@@ -3176,8 +3347,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
3176
3347
  decision: resolvedDecision
3177
3348
  });
3178
3349
  clearTimeout(entry.timer);
3179
- if (entry.waiter) entry.waiter(resolvedDecision);
3180
- else entry.earlyDecision = resolvedDecision;
3350
+ if (entry.waiter) entry.waiter(resolvedDecision, reason);
3351
+ else {
3352
+ entry.earlyDecision = resolvedDecision;
3353
+ entry.earlyReason = reason;
3354
+ }
3181
3355
  pending.delete(id);
3182
3356
  broadcast("remove", { id });
3183
3357
  res.writeHead(200);
@@ -3340,16 +3514,16 @@ var import_execa2 = require("execa");
3340
3514
  var import_chalk5 = __toESM(require("chalk"));
3341
3515
  var import_readline = __toESM(require("readline"));
3342
3516
  var import_fs5 = __toESM(require("fs"));
3343
- var import_path5 = __toESM(require("path"));
3517
+ var import_path6 = __toESM(require("path"));
3344
3518
  var import_os5 = __toESM(require("os"));
3345
3519
 
3346
3520
  // src/undo.ts
3347
3521
  var import_child_process3 = require("child_process");
3348
3522
  var import_fs4 = __toESM(require("fs"));
3349
- var import_path4 = __toESM(require("path"));
3523
+ var import_path5 = __toESM(require("path"));
3350
3524
  var import_os4 = __toESM(require("os"));
3351
- var SNAPSHOT_STACK_PATH = import_path4.default.join(import_os4.default.homedir(), ".node9", "snapshots.json");
3352
- var UNDO_LATEST_PATH = import_path4.default.join(import_os4.default.homedir(), ".node9", "undo_latest.txt");
3525
+ var SNAPSHOT_STACK_PATH = import_path5.default.join(import_os4.default.homedir(), ".node9", "snapshots.json");
3526
+ var UNDO_LATEST_PATH = import_path5.default.join(import_os4.default.homedir(), ".node9", "undo_latest.txt");
3353
3527
  var MAX_SNAPSHOTS = 10;
3354
3528
  function readStack() {
3355
3529
  try {
@@ -3360,7 +3534,7 @@ function readStack() {
3360
3534
  return [];
3361
3535
  }
3362
3536
  function writeStack(stack) {
3363
- const dir = import_path4.default.dirname(SNAPSHOT_STACK_PATH);
3537
+ const dir = import_path5.default.dirname(SNAPSHOT_STACK_PATH);
3364
3538
  if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
3365
3539
  import_fs4.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
3366
3540
  }
@@ -3378,8 +3552,8 @@ function buildArgsSummary(tool, args) {
3378
3552
  async function createShadowSnapshot(tool = "unknown", args = {}) {
3379
3553
  try {
3380
3554
  const cwd = process.cwd();
3381
- if (!import_fs4.default.existsSync(import_path4.default.join(cwd, ".git"))) return null;
3382
- const tempIndex = import_path4.default.join(cwd, ".git", `node9_index_${Date.now()}`);
3555
+ if (!import_fs4.default.existsSync(import_path5.default.join(cwd, ".git"))) return null;
3556
+ const tempIndex = import_path5.default.join(cwd, ".git", `node9_index_${Date.now()}`);
3383
3557
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
3384
3558
  (0, import_child_process3.spawnSync)("git", ["add", "-A"], { env });
3385
3559
  const treeRes = (0, import_child_process3.spawnSync)("git", ["write-tree"], { env });
@@ -3443,7 +3617,7 @@ function applyUndo(hash, cwd) {
3443
3617
  const tracked = (0, import_child_process3.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
3444
3618
  const untracked = (0, import_child_process3.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
3445
3619
  for (const file of [...tracked, ...untracked]) {
3446
- const fullPath = import_path4.default.join(dir, file);
3620
+ const fullPath = import_path5.default.join(dir, file);
3447
3621
  if (!snapshotFiles.has(file) && import_fs4.default.existsSync(fullPath)) {
3448
3622
  import_fs4.default.unlinkSync(fullPath);
3449
3623
  }
@@ -3457,7 +3631,7 @@ function applyUndo(hash, cwd) {
3457
3631
  // src/cli.ts
3458
3632
  var import_prompts3 = require("@inquirer/prompts");
3459
3633
  var { version } = JSON.parse(
3460
- import_fs5.default.readFileSync(import_path5.default.join(__dirname, "../package.json"), "utf-8")
3634
+ import_fs5.default.readFileSync(import_path6.default.join(__dirname, "../package.json"), "utf-8")
3461
3635
  );
3462
3636
  function parseDuration(str) {
3463
3637
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -3650,9 +3824,9 @@ async function runProxy(targetCommand) {
3650
3824
  }
3651
3825
  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) => {
3652
3826
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
3653
- const credPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
3654
- if (!import_fs5.default.existsSync(import_path5.default.dirname(credPath)))
3655
- import_fs5.default.mkdirSync(import_path5.default.dirname(credPath), { recursive: true });
3827
+ const credPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
3828
+ if (!import_fs5.default.existsSync(import_path6.default.dirname(credPath)))
3829
+ import_fs5.default.mkdirSync(import_path6.default.dirname(credPath), { recursive: true });
3656
3830
  const profileName = options.profile || "default";
3657
3831
  let existingCreds = {};
3658
3832
  try {
@@ -3671,7 +3845,7 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
3671
3845
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
3672
3846
  import_fs5.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
3673
3847
  if (profileName === "default") {
3674
- const configPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "config.json");
3848
+ const configPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "config.json");
3675
3849
  let config = {};
3676
3850
  try {
3677
3851
  if (import_fs5.default.existsSync(configPath))
@@ -3688,8 +3862,8 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
3688
3862
  };
3689
3863
  approvers.cloud = !options.local;
3690
3864
  s.approvers = approvers;
3691
- if (!import_fs5.default.existsSync(import_path5.default.dirname(configPath)))
3692
- import_fs5.default.mkdirSync(import_path5.default.dirname(configPath), { recursive: true });
3865
+ if (!import_fs5.default.existsSync(import_path6.default.dirname(configPath)))
3866
+ import_fs5.default.mkdirSync(import_path6.default.dirname(configPath), { recursive: true });
3693
3867
  import_fs5.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
3694
3868
  }
3695
3869
  if (options.profile && profileName !== "default") {
@@ -3775,7 +3949,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3775
3949
  );
3776
3950
  }
3777
3951
  section("Configuration");
3778
- const globalConfigPath = import_path5.default.join(homeDir2, ".node9", "config.json");
3952
+ const globalConfigPath = import_path6.default.join(homeDir2, ".node9", "config.json");
3779
3953
  if (import_fs5.default.existsSync(globalConfigPath)) {
3780
3954
  try {
3781
3955
  JSON.parse(import_fs5.default.readFileSync(globalConfigPath, "utf-8"));
@@ -3786,7 +3960,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3786
3960
  } else {
3787
3961
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
3788
3962
  }
3789
- const projectConfigPath = import_path5.default.join(process.cwd(), "node9.config.json");
3963
+ const projectConfigPath = import_path6.default.join(process.cwd(), "node9.config.json");
3790
3964
  if (import_fs5.default.existsSync(projectConfigPath)) {
3791
3965
  try {
3792
3966
  JSON.parse(import_fs5.default.readFileSync(projectConfigPath, "utf-8"));
@@ -3795,7 +3969,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3795
3969
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
3796
3970
  }
3797
3971
  }
3798
- const credsPath = import_path5.default.join(homeDir2, ".node9", "credentials.json");
3972
+ const credsPath = import_path6.default.join(homeDir2, ".node9", "credentials.json");
3799
3973
  if (import_fs5.default.existsSync(credsPath)) {
3800
3974
  pass("Cloud credentials found (~/.node9/credentials.json)");
3801
3975
  } else {
@@ -3805,7 +3979,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3805
3979
  );
3806
3980
  }
3807
3981
  section("Agent Hooks");
3808
- const claudeSettingsPath = import_path5.default.join(homeDir2, ".claude", "settings.json");
3982
+ const claudeSettingsPath = import_path6.default.join(homeDir2, ".claude", "settings.json");
3809
3983
  if (import_fs5.default.existsSync(claudeSettingsPath)) {
3810
3984
  try {
3811
3985
  const cs = JSON.parse(import_fs5.default.readFileSync(claudeSettingsPath, "utf-8"));
@@ -3821,7 +3995,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3821
3995
  } else {
3822
3996
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
3823
3997
  }
3824
- const geminiSettingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
3998
+ const geminiSettingsPath = import_path6.default.join(homeDir2, ".gemini", "settings.json");
3825
3999
  if (import_fs5.default.existsSync(geminiSettingsPath)) {
3826
4000
  try {
3827
4001
  const gs = JSON.parse(import_fs5.default.readFileSync(geminiSettingsPath, "utf-8"));
@@ -3837,7 +4011,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3837
4011
  } else {
3838
4012
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
3839
4013
  }
3840
- const cursorHooksPath = import_path5.default.join(homeDir2, ".cursor", "hooks.json");
4014
+ const cursorHooksPath = import_path6.default.join(homeDir2, ".cursor", "hooks.json");
3841
4015
  if (import_fs5.default.existsSync(cursorHooksPath)) {
3842
4016
  try {
3843
4017
  const cur = JSON.parse(import_fs5.default.readFileSync(cursorHooksPath, "utf-8"));
@@ -3942,7 +4116,7 @@ program.command("explain").description(
3942
4116
  console.log("");
3943
4117
  });
3944
4118
  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) => {
3945
- const configPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "config.json");
4119
+ const configPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "config.json");
3946
4120
  if (import_fs5.default.existsSync(configPath) && !options.force) {
3947
4121
  console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
3948
4122
  console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
@@ -3957,7 +4131,7 @@ program.command("init").description("Create ~/.node9/config.json with default po
3957
4131
  mode: safeMode
3958
4132
  }
3959
4133
  };
3960
- const dir = import_path5.default.dirname(configPath);
4134
+ const dir = import_path6.default.dirname(configPath);
3961
4135
  if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
3962
4136
  import_fs5.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
3963
4137
  console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
@@ -3977,7 +4151,7 @@ function formatRelativeTime(timestamp) {
3977
4151
  return new Date(timestamp).toLocaleDateString();
3978
4152
  }
3979
4153
  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) => {
3980
- const logPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "audit.log");
4154
+ const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "audit.log");
3981
4155
  if (!import_fs5.default.existsSync(logPath)) {
3982
4156
  console.log(
3983
4157
  import_chalk5.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
@@ -4067,8 +4241,8 @@ program.command("status").description("Show current Node9 mode, policy source, a
4067
4241
  console.log("");
4068
4242
  const modeLabel = settings.mode === "audit" ? import_chalk5.default.blue("audit") : settings.mode === "strict" ? import_chalk5.default.red("strict") : import_chalk5.default.white("standard");
4069
4243
  console.log(` Mode: ${modeLabel}`);
4070
- const projectConfig = import_path5.default.join(process.cwd(), "node9.config.json");
4071
- const globalConfig = import_path5.default.join(import_os5.default.homedir(), ".node9", "config.json");
4244
+ const projectConfig = import_path6.default.join(process.cwd(), "node9.config.json");
4245
+ const globalConfig = import_path6.default.join(import_os5.default.homedir(), ".node9", "config.json");
4072
4246
  console.log(
4073
4247
  ` Local: ${import_fs5.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
4074
4248
  );
@@ -4136,7 +4310,7 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
4136
4310
  } catch (err) {
4137
4311
  const tempConfig = getConfig();
4138
4312
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
4139
- const logPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4313
+ const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4140
4314
  const errMsg = err instanceof Error ? err.message : String(err);
4141
4315
  import_fs5.default.appendFileSync(
4142
4316
  logPath,
@@ -4156,9 +4330,9 @@ RAW: ${raw}
4156
4330
  }
4157
4331
  const config = getConfig();
4158
4332
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
4159
- const logPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4160
- if (!import_fs5.default.existsSync(import_path5.default.dirname(logPath)))
4161
- import_fs5.default.mkdirSync(import_path5.default.dirname(logPath), { recursive: true });
4333
+ const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4334
+ if (!import_fs5.default.existsSync(import_path6.default.dirname(logPath)))
4335
+ import_fs5.default.mkdirSync(import_path6.default.dirname(logPath), { recursive: true });
4162
4336
  import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
4163
4337
  `);
4164
4338
  }
@@ -4196,16 +4370,7 @@ RAW: ${raw}
4196
4370
  return;
4197
4371
  }
4198
4372
  const meta = { agent, mcpServer };
4199
- const STATE_CHANGING_TOOLS_PRE = [
4200
- "write_file",
4201
- "edit_file",
4202
- "edit",
4203
- "replace",
4204
- "terminal.execute",
4205
- "str_replace_based_edit_tool",
4206
- "create_file"
4207
- ];
4208
- if (config.settings.enableUndo && STATE_CHANGING_TOOLS_PRE.includes(toolName.toLowerCase())) {
4373
+ if (shouldSnapshot(toolName, toolInput, config)) {
4209
4374
  await createShadowSnapshot(toolName, toolInput);
4210
4375
  }
4211
4376
  const result = await authorizeHeadless(toolName, toolInput, false, meta);
@@ -4239,7 +4404,7 @@ RAW: ${raw}
4239
4404
  });
4240
4405
  } catch (err) {
4241
4406
  if (process.env.NODE9_DEBUG === "1") {
4242
- const logPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4407
+ const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4243
4408
  const errMsg = err instanceof Error ? err.message : String(err);
4244
4409
  import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
4245
4410
  `);
@@ -4286,20 +4451,12 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
4286
4451
  decision: "allowed",
4287
4452
  source: "post-hook"
4288
4453
  };
4289
- const logPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "audit.log");
4290
- if (!import_fs5.default.existsSync(import_path5.default.dirname(logPath)))
4291
- import_fs5.default.mkdirSync(import_path5.default.dirname(logPath), { recursive: true });
4454
+ const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "audit.log");
4455
+ if (!import_fs5.default.existsSync(import_path6.default.dirname(logPath)))
4456
+ import_fs5.default.mkdirSync(import_path6.default.dirname(logPath), { recursive: true });
4292
4457
  import_fs5.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
4293
4458
  const config = getConfig();
4294
- const STATE_CHANGING_TOOLS = [
4295
- "bash",
4296
- "shell",
4297
- "write_file",
4298
- "edit_file",
4299
- "replace",
4300
- "terminal.execute"
4301
- ];
4302
- if (config.settings.enableUndo && STATE_CHANGING_TOOLS.includes(tool.toLowerCase())) {
4459
+ if (shouldSnapshot(tool, {}, config)) {
4303
4460
  await createShadowSnapshot();
4304
4461
  }
4305
4462
  } catch {
@@ -4471,7 +4628,7 @@ process.on("unhandledRejection", (reason) => {
4471
4628
  const isCheckHook = process.argv[2] === "check";
4472
4629
  if (isCheckHook) {
4473
4630
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
4474
- const logPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4631
+ const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4475
4632
  const msg = reason instanceof Error ? reason.message : String(reason);
4476
4633
  import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
4477
4634
  `);