@node9/proxy 1.0.19 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -360,8 +360,8 @@ function sanitizeConfig(raw) {
360
360
  }
361
361
  }
362
362
  const lines = result.error.issues.map((issue) => {
363
- const path10 = issue.path.length > 0 ? issue.path.join(".") : "root";
364
- return ` \u2022 ${path10}: ${issue.message}`;
363
+ const path11 = issue.path.length > 0 ? issue.path.join(".") : "root";
364
+ return ` \u2022 ${path11}: ${issue.message}`;
365
365
  });
366
366
  return {
367
367
  sanitized,
@@ -675,6 +675,41 @@ var init_shields = __esm({
675
675
  });
676
676
 
677
677
  // src/dlp.ts
678
+ import fs2 from "fs";
679
+ import path4 from "path";
680
+ function scanFilePath(filePath, cwd = process.cwd()) {
681
+ if (!filePath) return null;
682
+ let resolved;
683
+ try {
684
+ const absolute = path4.resolve(cwd, filePath);
685
+ resolved = fs2.realpathSync.native(absolute);
686
+ } catch (err) {
687
+ const code = err.code;
688
+ if (code === "ENOENT" || code === "ENOTDIR") {
689
+ resolved = path4.resolve(cwd, filePath);
690
+ } else {
691
+ return {
692
+ patternName: "Sensitive File Path",
693
+ fieldPath: "file_path",
694
+ redactedSample: filePath,
695
+ severity: "block"
696
+ };
697
+ }
698
+ }
699
+ const normalised = resolved.replace(/\\/g, "/");
700
+ for (const pattern of SENSITIVE_PATH_PATTERNS) {
701
+ if (pattern.test(normalised)) {
702
+ return {
703
+ patternName: "Sensitive File Path",
704
+ fieldPath: "file_path",
705
+ redactedSample: filePath,
706
+ // show original path in alert, not resolved
707
+ severity: "block"
708
+ };
709
+ }
710
+ }
711
+ return null;
712
+ }
678
713
  function maskSecret(raw, pattern) {
679
714
  const match = raw.match(pattern);
680
715
  if (!match) return "****";
@@ -727,7 +762,7 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
727
762
  }
728
763
  return null;
729
764
  }
730
- var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
765
+ var DLP_PATTERNS, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
731
766
  var init_dlp = __esm({
732
767
  "src/dlp.ts"() {
733
768
  "use strict";
@@ -742,8 +777,43 @@ var init_dlp = __esm({
742
777
  regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
743
778
  severity: "block"
744
779
  },
780
+ // GCP service account JSON (detects the type field that uniquely identifies it)
781
+ {
782
+ name: "GCP Service Account",
783
+ regex: /"type"\s*:\s*"service_account"/,
784
+ severity: "block"
785
+ },
786
+ // NPM auth token in .npmrc format
787
+ {
788
+ name: "NPM Auth Token",
789
+ regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
790
+ severity: "block"
791
+ },
745
792
  { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
746
793
  ];
794
+ SENSITIVE_PATH_PATTERNS = [
795
+ /[/\\]\.ssh[/\\]/i,
796
+ /[/\\]\.aws[/\\]/i,
797
+ /[/\\]\.config[/\\]gcloud[/\\]/i,
798
+ /[/\\]\.azure[/\\]/i,
799
+ /[/\\]\.kube[/\\]config$/i,
800
+ /[/\\]\.env($|\.)/i,
801
+ // .env, .env.local, .env.production — not .envoy
802
+ /[/\\]\.git-credentials$/i,
803
+ /[/\\]\.npmrc$/i,
804
+ /[/\\]\.docker[/\\]config\.json$/i,
805
+ /[/\\][^/\\]+\.pem$/i,
806
+ /[/\\][^/\\]+\.key$/i,
807
+ /[/\\][^/\\]+\.p12$/i,
808
+ /[/\\][^/\\]+\.pfx$/i,
809
+ /^\/etc\/passwd$/,
810
+ /^\/etc\/shadow$/,
811
+ /^\/etc\/sudoers$/,
812
+ /[/\\]credentials\.json$/i,
813
+ /[/\\]id_rsa$/i,
814
+ /[/\\]id_ed25519$/i,
815
+ /[/\\]id_ecdsa$/i
816
+ ];
747
817
  MAX_DEPTH = 5;
748
818
  MAX_STRING_BYTES = 1e5;
749
819
  MAX_JSON_PARSE_BYTES = 1e4;
@@ -753,21 +823,22 @@ var init_dlp = __esm({
753
823
  // src/core.ts
754
824
  import chalk2 from "chalk";
755
825
  import { confirm } from "@inquirer/prompts";
756
- import fs2 from "fs";
757
- import path4 from "path";
826
+ import fs3 from "fs";
827
+ import path5 from "path";
758
828
  import os2 from "os";
759
829
  import net from "net";
760
830
  import { randomUUID } from "crypto";
761
831
  import { spawnSync } from "child_process";
762
832
  import pm from "picomatch";
833
+ import safeRegex from "safe-regex2";
763
834
  import { parse } from "sh-syntax";
764
835
  function checkPause() {
765
836
  try {
766
- if (!fs2.existsSync(PAUSED_FILE)) return { paused: false };
767
- const state = JSON.parse(fs2.readFileSync(PAUSED_FILE, "utf-8"));
837
+ if (!fs3.existsSync(PAUSED_FILE)) return { paused: false };
838
+ const state = JSON.parse(fs3.readFileSync(PAUSED_FILE, "utf-8"));
768
839
  if (state.expiry > 0 && Date.now() >= state.expiry) {
769
840
  try {
770
- fs2.unlinkSync(PAUSED_FILE);
841
+ fs3.unlinkSync(PAUSED_FILE);
771
842
  } catch {
772
843
  }
773
844
  return { paused: false };
@@ -778,11 +849,11 @@ function checkPause() {
778
849
  }
779
850
  }
780
851
  function atomicWriteSync(filePath, data, options) {
781
- const dir = path4.dirname(filePath);
782
- if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
852
+ const dir = path5.dirname(filePath);
853
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
783
854
  const tmpPath = `${filePath}.${os2.hostname()}.${process.pid}.tmp`;
784
- fs2.writeFileSync(tmpPath, data, options);
785
- fs2.renameSync(tmpPath, filePath);
855
+ fs3.writeFileSync(tmpPath, data, options);
856
+ fs3.renameSync(tmpPath, filePath);
786
857
  }
787
858
  function pauseNode9(durationMs, durationStr) {
788
859
  const state = { expiry: Date.now() + durationMs, duration: durationStr };
@@ -790,18 +861,88 @@ function pauseNode9(durationMs, durationStr) {
790
861
  }
791
862
  function resumeNode9() {
792
863
  try {
793
- if (fs2.existsSync(PAUSED_FILE)) fs2.unlinkSync(PAUSED_FILE);
864
+ if (fs3.existsSync(PAUSED_FILE)) fs3.unlinkSync(PAUSED_FILE);
794
865
  } catch {
795
866
  }
796
867
  }
868
+ function validateRegex(pattern) {
869
+ if (!pattern) return "Pattern is required";
870
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
871
+ let parens = 0, brackets = 0, isEscaped = false, inCharClass = false;
872
+ for (let i = 0; i < pattern.length; i++) {
873
+ const char = pattern[i];
874
+ if (isEscaped) {
875
+ isEscaped = false;
876
+ continue;
877
+ }
878
+ if (char === "\\") {
879
+ isEscaped = true;
880
+ continue;
881
+ }
882
+ if (char === "[" && !inCharClass) {
883
+ inCharClass = true;
884
+ brackets++;
885
+ continue;
886
+ }
887
+ if (char === "]" && inCharClass) {
888
+ inCharClass = false;
889
+ brackets--;
890
+ continue;
891
+ }
892
+ if (inCharClass) continue;
893
+ if (char === "(") parens++;
894
+ else if (char === ")") parens--;
895
+ }
896
+ if (parens !== 0) return "Unbalanced parentheses";
897
+ if (brackets !== 0) return "Unbalanced brackets";
898
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
899
+ if (!safeRegex(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
900
+ try {
901
+ new RegExp(pattern);
902
+ } catch (e) {
903
+ return `Invalid regex syntax: ${e.message}`;
904
+ }
905
+ return null;
906
+ }
907
+ function getCompiledRegex(pattern, flags = "") {
908
+ if (flags && !/^[gimsuy]+$/.test(flags)) {
909
+ if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Invalid regex flags: "${flags}"`);
910
+ return null;
911
+ }
912
+ const key = `${pattern}\0${flags}`;
913
+ if (regexCache.has(key)) {
914
+ const cached = regexCache.get(key);
915
+ regexCache.delete(key);
916
+ regexCache.set(key, cached);
917
+ return cached;
918
+ }
919
+ const err = validateRegex(pattern);
920
+ if (err) {
921
+ if (process.env.NODE9_DEBUG === "1")
922
+ console.error(`[Node9] Regex blocked: ${err} \u2014 pattern: "${pattern}"`);
923
+ return null;
924
+ }
925
+ try {
926
+ const re = new RegExp(pattern, flags);
927
+ if (regexCache.size >= REGEX_CACHE_MAX) {
928
+ const oldest = regexCache.keys().next().value;
929
+ if (oldest) regexCache.delete(oldest);
930
+ }
931
+ regexCache.set(key, re);
932
+ return re;
933
+ } catch (e) {
934
+ if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Regex compile failed:`, e);
935
+ return null;
936
+ }
937
+ }
797
938
  function getActiveTrustSession(toolName) {
798
939
  try {
799
- if (!fs2.existsSync(TRUST_FILE)) return false;
800
- const trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
940
+ if (!fs3.existsSync(TRUST_FILE)) return false;
941
+ const trust = JSON.parse(fs3.readFileSync(TRUST_FILE, "utf-8"));
801
942
  const now = Date.now();
802
943
  const active = trust.entries.filter((e) => e.expiry > now);
803
944
  if (active.length !== trust.entries.length) {
804
- fs2.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
945
+ fs3.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
805
946
  }
806
947
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
807
948
  } catch {
@@ -812,8 +953,8 @@ function writeTrustSession(toolName, durationMs) {
812
953
  try {
813
954
  let trust = { entries: [] };
814
955
  try {
815
- if (fs2.existsSync(TRUST_FILE)) {
816
- trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
956
+ if (fs3.existsSync(TRUST_FILE)) {
957
+ trust = JSON.parse(fs3.readFileSync(TRUST_FILE, "utf-8"));
817
958
  }
818
959
  } catch {
819
960
  }
@@ -829,9 +970,9 @@ function writeTrustSession(toolName, durationMs) {
829
970
  }
830
971
  function appendToLog(logPath, entry) {
831
972
  try {
832
- const dir = path4.dirname(logPath);
833
- if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
834
- fs2.appendFileSync(logPath, JSON.stringify(entry) + "\n");
973
+ const dir = path5.dirname(logPath);
974
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
975
+ fs3.appendFileSync(logPath, JSON.stringify(entry) + "\n");
835
976
  } catch {
836
977
  }
837
978
  }
@@ -873,9 +1014,9 @@ function matchesPattern(text, patterns) {
873
1014
  const withoutDotSlash = text.replace(/^\.\//, "");
874
1015
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
875
1016
  }
876
- function getNestedValue(obj, path10) {
1017
+ function getNestedValue(obj, path11) {
877
1018
  if (!obj || typeof obj !== "object") return null;
878
- return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
1019
+ return path11.split(".").reduce((prev, curr) => prev?.[curr], obj);
879
1020
  }
880
1021
  function shouldSnapshot(toolName, args, config) {
881
1022
  if (!config.settings.enableUndo) return false;
@@ -906,19 +1047,16 @@ function evaluateSmartConditions(args, rule) {
906
1047
  return val !== null && cond.value ? !val.includes(cond.value) : true;
907
1048
  case "matches": {
908
1049
  if (val === null || !cond.value) return false;
909
- try {
910
- return new RegExp(cond.value, cond.flags ?? "").test(val);
911
- } catch {
912
- return false;
913
- }
1050
+ const reM = getCompiledRegex(cond.value, cond.flags ?? "");
1051
+ if (!reM) return false;
1052
+ return reM.test(val);
914
1053
  }
915
1054
  case "notMatches": {
916
- if (val === null || !cond.value) return true;
917
- try {
918
- return !new RegExp(cond.value, cond.flags ?? "").test(val);
919
- } catch {
920
- return true;
921
- }
1055
+ if (!cond.value) return false;
1056
+ if (val === null) return true;
1057
+ const reN = getCompiledRegex(cond.value, cond.flags ?? "");
1058
+ if (!reN) return false;
1059
+ return !reN.test(val);
922
1060
  }
923
1061
  case "matchesGlob":
924
1062
  return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
@@ -1031,9 +1169,9 @@ function _resetConfigCache() {
1031
1169
  }
1032
1170
  function getGlobalSettings() {
1033
1171
  try {
1034
- const globalConfigPath = path4.join(os2.homedir(), ".node9", "config.json");
1035
- if (fs2.existsSync(globalConfigPath)) {
1036
- const parsed = JSON.parse(fs2.readFileSync(globalConfigPath, "utf-8"));
1172
+ const globalConfigPath = path5.join(os2.homedir(), ".node9", "config.json");
1173
+ if (fs3.existsSync(globalConfigPath)) {
1174
+ const parsed = JSON.parse(fs3.readFileSync(globalConfigPath, "utf-8"));
1037
1175
  const settings = parsed.settings || {};
1038
1176
  return {
1039
1177
  mode: settings.mode || "audit",
@@ -1055,9 +1193,9 @@ function getGlobalSettings() {
1055
1193
  }
1056
1194
  function getInternalToken() {
1057
1195
  try {
1058
- const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1059
- if (!fs2.existsSync(pidFile)) return null;
1060
- const data = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1196
+ const pidFile = path5.join(os2.homedir(), ".node9", "daemon.pid");
1197
+ if (!fs3.existsSync(pidFile)) return null;
1198
+ const data = JSON.parse(fs3.readFileSync(pidFile, "utf-8"));
1061
1199
  process.kill(data.pid, 0);
1062
1200
  return data.internalToken ?? null;
1063
1201
  } catch {
@@ -1172,9 +1310,9 @@ async function evaluatePolicy(toolName, args, agent) {
1172
1310
  }
1173
1311
  async function explainPolicy(toolName, args) {
1174
1312
  const steps = [];
1175
- const globalPath = path4.join(os2.homedir(), ".node9", "config.json");
1176
- const projectPath = path4.join(process.cwd(), "node9.config.json");
1177
- const credsPath = path4.join(os2.homedir(), ".node9", "credentials.json");
1313
+ const globalPath = path5.join(os2.homedir(), ".node9", "config.json");
1314
+ const projectPath = path5.join(process.cwd(), "node9.config.json");
1315
+ const credsPath = path5.join(os2.homedir(), ".node9", "credentials.json");
1178
1316
  const waterfall = [
1179
1317
  {
1180
1318
  tier: 1,
@@ -1185,19 +1323,19 @@ async function explainPolicy(toolName, args) {
1185
1323
  {
1186
1324
  tier: 2,
1187
1325
  label: "Cloud policy",
1188
- status: fs2.existsSync(credsPath) ? "active" : "missing",
1189
- note: fs2.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1326
+ status: fs3.existsSync(credsPath) ? "active" : "missing",
1327
+ note: fs3.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1190
1328
  },
1191
1329
  {
1192
1330
  tier: 3,
1193
1331
  label: "Project config",
1194
- status: fs2.existsSync(projectPath) ? "active" : "missing",
1332
+ status: fs3.existsSync(projectPath) ? "active" : "missing",
1195
1333
  path: projectPath
1196
1334
  },
1197
1335
  {
1198
1336
  tier: 4,
1199
1337
  label: "Global config",
1200
- status: fs2.existsSync(globalPath) ? "active" : "missing",
1338
+ status: fs3.existsSync(globalPath) ? "active" : "missing",
1201
1339
  path: globalPath
1202
1340
  },
1203
1341
  {
@@ -1210,7 +1348,9 @@ async function explainPolicy(toolName, args) {
1210
1348
  const config = getConfig();
1211
1349
  const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
1212
1350
  if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
1213
- const dlpMatch = args !== void 0 ? scanArgs(args) : null;
1351
+ const argsObjE = args && typeof args === "object" && !Array.isArray(args) ? args : {};
1352
+ const filePathE = String(argsObjE.file_path ?? argsObjE.path ?? argsObjE.filename ?? "");
1353
+ const dlpMatch = (filePathE ? scanFilePath(filePathE) : null) ?? (args !== void 0 ? scanArgs(args) : null);
1214
1354
  if (dlpMatch) {
1215
1355
  steps.push({
1216
1356
  name: "DLP Content Scanner",
@@ -1433,10 +1573,10 @@ function isIgnoredTool(toolName) {
1433
1573
  return matchesPattern(toolName, config.policy.ignoredTools);
1434
1574
  }
1435
1575
  function isDaemonRunning() {
1436
- const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1437
- if (fs2.existsSync(pidFile)) {
1576
+ const pidFile = path5.join(os2.homedir(), ".node9", "daemon.pid");
1577
+ if (fs3.existsSync(pidFile)) {
1438
1578
  try {
1439
- const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1579
+ const { pid, port } = JSON.parse(fs3.readFileSync(pidFile, "utf-8"));
1440
1580
  if (port !== DAEMON_PORT) return false;
1441
1581
  process.kill(pid, 0);
1442
1582
  return true;
@@ -1456,9 +1596,9 @@ function isDaemonRunning() {
1456
1596
  }
1457
1597
  function getPersistentDecision(toolName) {
1458
1598
  try {
1459
- const file = path4.join(os2.homedir(), ".node9", "decisions.json");
1460
- if (!fs2.existsSync(file)) return null;
1461
- const decisions = JSON.parse(fs2.readFileSync(file, "utf-8"));
1599
+ const file = path5.join(os2.homedir(), ".node9", "decisions.json");
1600
+ if (!fs3.existsSync(file)) return null;
1601
+ const decisions = JSON.parse(fs3.readFileSync(file, "utf-8"));
1462
1602
  const d = decisions[toolName];
1463
1603
  if (d === "allow" || d === "deny") return d;
1464
1604
  } catch {
@@ -1601,7 +1741,9 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1601
1741
  let policyMatchedWord;
1602
1742
  let riskMetadata;
1603
1743
  if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
1604
- const dlpMatch = scanArgs(args);
1744
+ const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
1745
+ const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
1746
+ const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
1605
1747
  if (dlpMatch) {
1606
1748
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
1607
1749
  if (dlpMatch.severity === "block") {
@@ -1975,8 +2117,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1975
2117
  }
1976
2118
  function getConfig() {
1977
2119
  if (cachedConfig) return cachedConfig;
1978
- const globalPath = path4.join(os2.homedir(), ".node9", "config.json");
1979
- const projectPath = path4.join(process.cwd(), "node9.config.json");
2120
+ const globalPath = path5.join(os2.homedir(), ".node9", "config.json");
2121
+ const projectPath = path5.join(process.cwd(), "node9.config.json");
1980
2122
  const globalConfig = tryLoadConfig(globalPath);
1981
2123
  const projectConfig = tryLoadConfig(projectPath);
1982
2124
  const mergedSettings = {
@@ -2071,10 +2213,10 @@ function getConfig() {
2071
2213
  return cachedConfig;
2072
2214
  }
2073
2215
  function tryLoadConfig(filePath) {
2074
- if (!fs2.existsSync(filePath)) return null;
2216
+ if (!fs3.existsSync(filePath)) return null;
2075
2217
  let raw;
2076
2218
  try {
2077
- raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
2219
+ raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
2078
2220
  } catch (err) {
2079
2221
  const msg = err instanceof Error ? err.message : String(err);
2080
2222
  process.stderr.write(
@@ -2136,9 +2278,9 @@ function getCredentials() {
2136
2278
  };
2137
2279
  }
2138
2280
  try {
2139
- const credPath = path4.join(os2.homedir(), ".node9", "credentials.json");
2140
- if (fs2.existsSync(credPath)) {
2141
- const creds = JSON.parse(fs2.readFileSync(credPath, "utf-8"));
2281
+ const credPath = path5.join(os2.homedir(), ".node9", "credentials.json");
2282
+ if (fs3.existsSync(credPath)) {
2283
+ const creds = JSON.parse(fs3.readFileSync(credPath, "utf-8"));
2142
2284
  const profileName = process.env.NODE9_PROFILE || "default";
2143
2285
  const profile = creds[profileName];
2144
2286
  if (profile?.apiKey) {
@@ -2251,7 +2393,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
2251
2393
  } catch {
2252
2394
  }
2253
2395
  }
2254
- var PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
2396
+ var PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
2255
2397
  var init_core = __esm({
2256
2398
  "src/core.ts"() {
2257
2399
  "use strict";
@@ -2260,10 +2402,13 @@ var init_core = __esm({
2260
2402
  init_config_schema();
2261
2403
  init_shields();
2262
2404
  init_dlp();
2263
- PAUSED_FILE = path4.join(os2.homedir(), ".node9", "PAUSED");
2264
- TRUST_FILE = path4.join(os2.homedir(), ".node9", "trust.json");
2265
- LOCAL_AUDIT_LOG = path4.join(os2.homedir(), ".node9", "audit.log");
2266
- HOOK_DEBUG_LOG = path4.join(os2.homedir(), ".node9", "hook-debug.log");
2405
+ PAUSED_FILE = path5.join(os2.homedir(), ".node9", "PAUSED");
2406
+ TRUST_FILE = path5.join(os2.homedir(), ".node9", "trust.json");
2407
+ LOCAL_AUDIT_LOG = path5.join(os2.homedir(), ".node9", "audit.log");
2408
+ HOOK_DEBUG_LOG = path5.join(os2.homedir(), ".node9", "hook-debug.log");
2409
+ MAX_REGEX_LENGTH = 100;
2410
+ REGEX_CACHE_MAX = 500;
2411
+ regexCache = /* @__PURE__ */ new Map();
2267
2412
  SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2268
2413
  DANGEROUS_WORDS = [
2269
2414
  "mkfs",
@@ -2478,7 +2623,7 @@ var init_core = __esm({
2478
2623
  cachedConfig = null;
2479
2624
  DAEMON_PORT = 7391;
2480
2625
  DAEMON_HOST = "127.0.0.1";
2481
- ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path4.join(os2.tmpdir(), "node9-activity.sock");
2626
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path5.join(os2.tmpdir(), "node9-activity.sock");
2482
2627
  }
2483
2628
  });
2484
2629
 
@@ -3937,25 +4082,25 @@ var init_ui2 = __esm({
3937
4082
  // src/daemon/index.ts
3938
4083
  import http from "http";
3939
4084
  import net2 from "net";
3940
- import fs4 from "fs";
3941
- import path6 from "path";
4085
+ import fs5 from "fs";
4086
+ import path7 from "path";
3942
4087
  import os4 from "os";
3943
4088
  import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
3944
4089
  import { randomUUID as randomUUID2 } from "crypto";
3945
4090
  import chalk4 from "chalk";
3946
4091
  function atomicWriteSync2(filePath, data, options) {
3947
- const dir = path6.dirname(filePath);
3948
- if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
4092
+ const dir = path7.dirname(filePath);
4093
+ if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
3949
4094
  const tmpPath = `${filePath}.${randomUUID2()}.tmp`;
3950
- fs4.writeFileSync(tmpPath, data, options);
3951
- fs4.renameSync(tmpPath, filePath);
4095
+ fs5.writeFileSync(tmpPath, data, options);
4096
+ fs5.renameSync(tmpPath, filePath);
3952
4097
  }
3953
4098
  function writeTrustEntry(toolName, durationMs) {
3954
4099
  try {
3955
4100
  let trust = { entries: [] };
3956
4101
  try {
3957
- if (fs4.existsSync(TRUST_FILE2))
3958
- trust = JSON.parse(fs4.readFileSync(TRUST_FILE2, "utf-8"));
4102
+ if (fs5.existsSync(TRUST_FILE2))
4103
+ trust = JSON.parse(fs5.readFileSync(TRUST_FILE2, "utf-8"));
3959
4104
  } catch {
3960
4105
  }
3961
4106
  trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
@@ -3982,16 +4127,16 @@ function appendAuditLog(data) {
3982
4127
  decision: data.decision,
3983
4128
  source: "daemon"
3984
4129
  };
3985
- const dir = path6.dirname(AUDIT_LOG_FILE);
3986
- if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
3987
- fs4.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
4130
+ const dir = path7.dirname(AUDIT_LOG_FILE);
4131
+ if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
4132
+ fs5.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
3988
4133
  } catch {
3989
4134
  }
3990
4135
  }
3991
4136
  function getAuditHistory(limit = 20) {
3992
4137
  try {
3993
- if (!fs4.existsSync(AUDIT_LOG_FILE)) return [];
3994
- const lines = fs4.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
4138
+ if (!fs5.existsSync(AUDIT_LOG_FILE)) return [];
4139
+ const lines = fs5.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
3995
4140
  if (lines.length === 1 && lines[0] === "") return [];
3996
4141
  return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
3997
4142
  } catch {
@@ -4000,7 +4145,7 @@ function getAuditHistory(limit = 20) {
4000
4145
  }
4001
4146
  function getOrgName() {
4002
4147
  try {
4003
- if (fs4.existsSync(CREDENTIALS_FILE)) {
4148
+ if (fs5.existsSync(CREDENTIALS_FILE)) {
4004
4149
  return "Node9 Cloud";
4005
4150
  }
4006
4151
  } catch {
@@ -4008,13 +4153,13 @@ function getOrgName() {
4008
4153
  return null;
4009
4154
  }
4010
4155
  function hasStoredSlackKey() {
4011
- return fs4.existsSync(CREDENTIALS_FILE);
4156
+ return fs5.existsSync(CREDENTIALS_FILE);
4012
4157
  }
4013
4158
  function writeGlobalSetting(key, value) {
4014
4159
  let config = {};
4015
4160
  try {
4016
- if (fs4.existsSync(GLOBAL_CONFIG_FILE)) {
4017
- config = JSON.parse(fs4.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
4161
+ if (fs5.existsSync(GLOBAL_CONFIG_FILE)) {
4162
+ config = JSON.parse(fs5.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
4018
4163
  }
4019
4164
  } catch {
4020
4165
  }
@@ -4033,7 +4178,7 @@ function abandonPending() {
4033
4178
  });
4034
4179
  if (autoStarted) {
4035
4180
  try {
4036
- fs4.unlinkSync(DAEMON_PID_FILE);
4181
+ fs5.unlinkSync(DAEMON_PID_FILE);
4037
4182
  } catch {
4038
4183
  }
4039
4184
  setTimeout(() => {
@@ -4083,8 +4228,8 @@ function readBody(req) {
4083
4228
  }
4084
4229
  function readPersistentDecisions() {
4085
4230
  try {
4086
- if (fs4.existsSync(DECISIONS_FILE)) {
4087
- return JSON.parse(fs4.readFileSync(DECISIONS_FILE, "utf-8"));
4231
+ if (fs5.existsSync(DECISIONS_FILE)) {
4232
+ return JSON.parse(fs5.readFileSync(DECISIONS_FILE, "utf-8"));
4088
4233
  }
4089
4234
  } catch {
4090
4235
  }
@@ -4113,7 +4258,7 @@ function startDaemon() {
4113
4258
  idleTimer = setTimeout(() => {
4114
4259
  if (autoStarted) {
4115
4260
  try {
4116
- fs4.unlinkSync(DAEMON_PID_FILE);
4261
+ fs5.unlinkSync(DAEMON_PID_FILE);
4117
4262
  } catch {
4118
4263
  }
4119
4264
  }
@@ -4498,14 +4643,14 @@ data: ${JSON.stringify(item.data)}
4498
4643
  server.on("error", (e) => {
4499
4644
  if (e.code === "EADDRINUSE") {
4500
4645
  try {
4501
- if (fs4.existsSync(DAEMON_PID_FILE)) {
4502
- const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4646
+ if (fs5.existsSync(DAEMON_PID_FILE)) {
4647
+ const { pid } = JSON.parse(fs5.readFileSync(DAEMON_PID_FILE, "utf-8"));
4503
4648
  process.kill(pid, 0);
4504
4649
  return process.exit(0);
4505
4650
  }
4506
4651
  } catch {
4507
4652
  try {
4508
- fs4.unlinkSync(DAEMON_PID_FILE);
4653
+ fs5.unlinkSync(DAEMON_PID_FILE);
4509
4654
  } catch {
4510
4655
  }
4511
4656
  server.listen(DAEMON_PORT2, DAEMON_HOST2);
@@ -4556,7 +4701,7 @@ data: ${JSON.stringify(item.data)}
4556
4701
  console.log(chalk4.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
4557
4702
  }
4558
4703
  try {
4559
- fs4.unlinkSync(ACTIVITY_SOCKET_PATH2);
4704
+ fs5.unlinkSync(ACTIVITY_SOCKET_PATH2);
4560
4705
  } catch {
4561
4706
  }
4562
4707
  const ACTIVITY_MAX_BYTES = 1024 * 1024;
@@ -4598,30 +4743,30 @@ data: ${JSON.stringify(item.data)}
4598
4743
  unixServer.listen(ACTIVITY_SOCKET_PATH2);
4599
4744
  process.on("exit", () => {
4600
4745
  try {
4601
- fs4.unlinkSync(ACTIVITY_SOCKET_PATH2);
4746
+ fs5.unlinkSync(ACTIVITY_SOCKET_PATH2);
4602
4747
  } catch {
4603
4748
  }
4604
4749
  });
4605
4750
  }
4606
4751
  function stopDaemon() {
4607
- if (!fs4.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
4752
+ if (!fs5.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
4608
4753
  try {
4609
- const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4754
+ const { pid } = JSON.parse(fs5.readFileSync(DAEMON_PID_FILE, "utf-8"));
4610
4755
  process.kill(pid, "SIGTERM");
4611
4756
  console.log(chalk4.green("\u2705 Stopped."));
4612
4757
  } catch {
4613
4758
  console.log(chalk4.gray("Cleaned up stale PID file."));
4614
4759
  } finally {
4615
4760
  try {
4616
- fs4.unlinkSync(DAEMON_PID_FILE);
4761
+ fs5.unlinkSync(DAEMON_PID_FILE);
4617
4762
  } catch {
4618
4763
  }
4619
4764
  }
4620
4765
  }
4621
4766
  function daemonStatus() {
4622
- if (fs4.existsSync(DAEMON_PID_FILE)) {
4767
+ if (fs5.existsSync(DAEMON_PID_FILE)) {
4623
4768
  try {
4624
- const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4769
+ const { pid } = JSON.parse(fs5.readFileSync(DAEMON_PID_FILE, "utf-8"));
4625
4770
  process.kill(pid, 0);
4626
4771
  console.log(chalk4.green("Node9 daemon: running"));
4627
4772
  return;
@@ -4647,16 +4792,16 @@ var init_daemon = __esm({
4647
4792
  init_ui2();
4648
4793
  init_core();
4649
4794
  init_shields();
4650
- ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path6.join(os4.tmpdir(), "node9-activity.sock");
4795
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path7.join(os4.tmpdir(), "node9-activity.sock");
4651
4796
  DAEMON_PORT2 = 7391;
4652
4797
  DAEMON_HOST2 = "127.0.0.1";
4653
4798
  homeDir = os4.homedir();
4654
- DAEMON_PID_FILE = path6.join(homeDir, ".node9", "daemon.pid");
4655
- DECISIONS_FILE = path6.join(homeDir, ".node9", "decisions.json");
4656
- GLOBAL_CONFIG_FILE = path6.join(homeDir, ".node9", "config.json");
4657
- CREDENTIALS_FILE = path6.join(homeDir, ".node9", "credentials.json");
4658
- AUDIT_LOG_FILE = path6.join(homeDir, ".node9", "audit.log");
4659
- TRUST_FILE2 = path6.join(homeDir, ".node9", "trust.json");
4799
+ DAEMON_PID_FILE = path7.join(homeDir, ".node9", "daemon.pid");
4800
+ DECISIONS_FILE = path7.join(homeDir, ".node9", "decisions.json");
4801
+ GLOBAL_CONFIG_FILE = path7.join(homeDir, ".node9", "config.json");
4802
+ CREDENTIALS_FILE = path7.join(homeDir, ".node9", "credentials.json");
4803
+ AUDIT_LOG_FILE = path7.join(homeDir, ".node9", "audit.log");
4804
+ TRUST_FILE2 = path7.join(homeDir, ".node9", "trust.json");
4660
4805
  TRUST_DURATIONS = {
4661
4806
  "30m": 30 * 6e4,
4662
4807
  "1h": 60 * 6e4,
@@ -4682,11 +4827,11 @@ __export(tail_exports, {
4682
4827
  });
4683
4828
  import http2 from "http";
4684
4829
  import chalk5 from "chalk";
4685
- import fs6 from "fs";
4830
+ import fs7 from "fs";
4686
4831
  import os6 from "os";
4687
- import path8 from "path";
4832
+ import path9 from "path";
4688
4833
  import readline from "readline";
4689
- import { spawn as spawn3 } from "child_process";
4834
+ import { spawn as spawn4 } from "child_process";
4690
4835
  function getIcon(tool) {
4691
4836
  const t = tool.toLowerCase();
4692
4837
  for (const [k, v] of Object.entries(ICONS)) {
@@ -4724,9 +4869,9 @@ function renderPending(activity) {
4724
4869
  }
4725
4870
  async function ensureDaemon() {
4726
4871
  let pidPort = null;
4727
- if (fs6.existsSync(PID_FILE)) {
4872
+ if (fs7.existsSync(PID_FILE)) {
4728
4873
  try {
4729
- const { port } = JSON.parse(fs6.readFileSync(PID_FILE, "utf-8"));
4874
+ const { port } = JSON.parse(fs7.readFileSync(PID_FILE, "utf-8"));
4730
4875
  pidPort = port;
4731
4876
  } catch {
4732
4877
  console.error(chalk5.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
@@ -4741,7 +4886,7 @@ async function ensureDaemon() {
4741
4886
  } catch {
4742
4887
  }
4743
4888
  console.log(chalk5.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
4744
- const child = spawn3(process.execPath, [process.argv[1], "daemon"], {
4889
+ const child = spawn4(process.execPath, [process.argv[1], "daemon"], {
4745
4890
  detached: true,
4746
4891
  stdio: "ignore",
4747
4892
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
@@ -4886,7 +5031,7 @@ var init_tail = __esm({
4886
5031
  "src/tui/tail.ts"() {
4887
5032
  "use strict";
4888
5033
  init_daemon();
4889
- PID_FILE = path8.join(os6.homedir(), ".node9", "daemon.pid");
5034
+ PID_FILE = path9.join(os6.homedir(), ".node9", "daemon.pid");
4890
5035
  ICONS = {
4891
5036
  bash: "\u{1F4BB}",
4892
5037
  shell: "\u{1F4BB}",
@@ -4912,8 +5057,8 @@ init_core();
4912
5057
  import { Command } from "commander";
4913
5058
 
4914
5059
  // src/setup.ts
4915
- import fs3 from "fs";
4916
- import path5 from "path";
5060
+ import fs4 from "fs";
5061
+ import path6 from "path";
4917
5062
  import os3 from "os";
4918
5063
  import chalk3 from "chalk";
4919
5064
  import { confirm as confirm2 } from "@inquirer/prompts";
@@ -4931,17 +5076,17 @@ function fullPathCommand(subcommand) {
4931
5076
  }
4932
5077
  function readJson(filePath) {
4933
5078
  try {
4934
- if (fs3.existsSync(filePath)) {
4935
- return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
5079
+ if (fs4.existsSync(filePath)) {
5080
+ return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
4936
5081
  }
4937
5082
  } catch {
4938
5083
  }
4939
5084
  return null;
4940
5085
  }
4941
5086
  function writeJson(filePath, data) {
4942
- const dir = path5.dirname(filePath);
4943
- if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
4944
- fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
5087
+ const dir = path6.dirname(filePath);
5088
+ if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
5089
+ fs4.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
4945
5090
  }
4946
5091
  function isNode9Hook(cmd) {
4947
5092
  if (!cmd) return false;
@@ -4949,8 +5094,8 @@ function isNode9Hook(cmd) {
4949
5094
  }
4950
5095
  function teardownClaude() {
4951
5096
  const homeDir2 = os3.homedir();
4952
- const hooksPath = path5.join(homeDir2, ".claude", "settings.json");
4953
- const mcpPath = path5.join(homeDir2, ".claude.json");
5097
+ const hooksPath = path6.join(homeDir2, ".claude", "settings.json");
5098
+ const mcpPath = path6.join(homeDir2, ".claude.json");
4954
5099
  let changed = false;
4955
5100
  const settings = readJson(hooksPath);
4956
5101
  if (settings?.hooks) {
@@ -4999,7 +5144,7 @@ function teardownClaude() {
4999
5144
  }
5000
5145
  function teardownGemini() {
5001
5146
  const homeDir2 = os3.homedir();
5002
- const settingsPath = path5.join(homeDir2, ".gemini", "settings.json");
5147
+ const settingsPath = path6.join(homeDir2, ".gemini", "settings.json");
5003
5148
  const settings = readJson(settingsPath);
5004
5149
  if (!settings) {
5005
5150
  console.log(chalk3.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
@@ -5038,7 +5183,7 @@ function teardownGemini() {
5038
5183
  }
5039
5184
  function teardownCursor() {
5040
5185
  const homeDir2 = os3.homedir();
5041
- const mcpPath = path5.join(homeDir2, ".cursor", "mcp.json");
5186
+ const mcpPath = path6.join(homeDir2, ".cursor", "mcp.json");
5042
5187
  const mcpConfig = readJson(mcpPath);
5043
5188
  if (!mcpConfig?.mcpServers) {
5044
5189
  console.log(chalk3.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
@@ -5065,8 +5210,8 @@ function teardownCursor() {
5065
5210
  }
5066
5211
  async function setupClaude() {
5067
5212
  const homeDir2 = os3.homedir();
5068
- const mcpPath = path5.join(homeDir2, ".claude.json");
5069
- const hooksPath = path5.join(homeDir2, ".claude", "settings.json");
5213
+ const mcpPath = path6.join(homeDir2, ".claude.json");
5214
+ const hooksPath = path6.join(homeDir2, ".claude", "settings.json");
5070
5215
  const claudeConfig = readJson(mcpPath) ?? {};
5071
5216
  const settings = readJson(hooksPath) ?? {};
5072
5217
  const servers = claudeConfig.mcpServers ?? {};
@@ -5141,7 +5286,7 @@ async function setupClaude() {
5141
5286
  }
5142
5287
  async function setupGemini() {
5143
5288
  const homeDir2 = os3.homedir();
5144
- const settingsPath = path5.join(homeDir2, ".gemini", "settings.json");
5289
+ const settingsPath = path6.join(homeDir2, ".gemini", "settings.json");
5145
5290
  const settings = readJson(settingsPath) ?? {};
5146
5291
  const servers = settings.mcpServers ?? {};
5147
5292
  let anythingChanged = false;
@@ -5224,7 +5369,7 @@ async function setupGemini() {
5224
5369
  }
5225
5370
  async function setupCursor() {
5226
5371
  const homeDir2 = os3.homedir();
5227
- const mcpPath = path5.join(homeDir2, ".cursor", "mcp.json");
5372
+ const mcpPath = path6.join(homeDir2, ".cursor", "mcp.json");
5228
5373
  const mcpConfig = readJson(mcpPath) ?? {};
5229
5374
  const servers = mcpConfig.mcpServers ?? {};
5230
5375
  let anythingChanged = false;
@@ -5280,35 +5425,37 @@ async function setupCursor() {
5280
5425
 
5281
5426
  // src/cli.ts
5282
5427
  init_daemon();
5283
- import { spawn as spawn4, execSync } from "child_process";
5428
+ import { spawn as spawn5, execSync } from "child_process";
5284
5429
  import { parseCommandString } from "execa";
5285
5430
  import { execa } from "execa";
5286
5431
  import chalk6 from "chalk";
5287
5432
  import readline2 from "readline";
5288
- import fs7 from "fs";
5289
- import path9 from "path";
5433
+ import fs8 from "fs";
5434
+ import path10 from "path";
5290
5435
  import os7 from "os";
5291
5436
 
5292
5437
  // src/undo.ts
5293
- import { spawnSync as spawnSync3 } from "child_process";
5294
- import fs5 from "fs";
5295
- import path7 from "path";
5438
+ import { spawnSync as spawnSync3, spawn as spawn3 } from "child_process";
5439
+ import crypto2 from "crypto";
5440
+ import fs6 from "fs";
5441
+ import path8 from "path";
5296
5442
  import os5 from "os";
5297
- var SNAPSHOT_STACK_PATH = path7.join(os5.homedir(), ".node9", "snapshots.json");
5298
- var UNDO_LATEST_PATH = path7.join(os5.homedir(), ".node9", "undo_latest.txt");
5443
+ var SNAPSHOT_STACK_PATH = path8.join(os5.homedir(), ".node9", "snapshots.json");
5444
+ var UNDO_LATEST_PATH = path8.join(os5.homedir(), ".node9", "undo_latest.txt");
5299
5445
  var MAX_SNAPSHOTS = 10;
5446
+ var GIT_TIMEOUT = 15e3;
5300
5447
  function readStack() {
5301
5448
  try {
5302
- if (fs5.existsSync(SNAPSHOT_STACK_PATH))
5303
- return JSON.parse(fs5.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
5449
+ if (fs6.existsSync(SNAPSHOT_STACK_PATH))
5450
+ return JSON.parse(fs6.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
5304
5451
  } catch {
5305
5452
  }
5306
5453
  return [];
5307
5454
  }
5308
5455
  function writeStack(stack) {
5309
- const dir = path7.dirname(SNAPSHOT_STACK_PATH);
5310
- if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
5311
- fs5.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
5456
+ const dir = path8.dirname(SNAPSHOT_STACK_PATH);
5457
+ if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
5458
+ fs6.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
5312
5459
  }
5313
5460
  function buildArgsSummary(tool, args) {
5314
5461
  if (!args || typeof args !== "object") return "";
@@ -5321,54 +5468,177 @@ function buildArgsSummary(tool, args) {
5321
5468
  if (typeof sql === "string") return sql.slice(0, 80);
5322
5469
  return tool;
5323
5470
  }
5324
- async function createShadowSnapshot(tool = "unknown", args = {}) {
5471
+ function normalizeCwdForHash(cwd) {
5472
+ let normalized;
5473
+ try {
5474
+ normalized = fs6.realpathSync(cwd);
5475
+ } catch {
5476
+ normalized = cwd;
5477
+ }
5478
+ normalized = normalized.replace(/\\/g, "/");
5479
+ if (process.platform === "win32") normalized = normalized.toLowerCase();
5480
+ return normalized;
5481
+ }
5482
+ function getShadowRepoDir(cwd) {
5483
+ const hash = crypto2.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
5484
+ return path8.join(os5.homedir(), ".node9", "snapshots", hash);
5485
+ }
5486
+ function cleanOrphanedIndexFiles(shadowDir) {
5487
+ try {
5488
+ const cutoff = Date.now() - 6e4;
5489
+ for (const f of fs6.readdirSync(shadowDir)) {
5490
+ if (f.startsWith("index_")) {
5491
+ const fp = path8.join(shadowDir, f);
5492
+ try {
5493
+ if (fs6.statSync(fp).mtimeMs < cutoff) fs6.unlinkSync(fp);
5494
+ } catch {
5495
+ }
5496
+ }
5497
+ }
5498
+ } catch {
5499
+ }
5500
+ }
5501
+ function writeShadowExcludes(shadowDir, ignorePaths) {
5502
+ const hardcoded = [".git", ".node9"];
5503
+ const lines = [...hardcoded, ...ignorePaths].join("\n");
5504
+ try {
5505
+ fs6.writeFileSync(path8.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
5506
+ } catch {
5507
+ }
5508
+ }
5509
+ function ensureShadowRepo(shadowDir, cwd) {
5510
+ cleanOrphanedIndexFiles(shadowDir);
5511
+ const normalizedCwd = normalizeCwdForHash(cwd);
5512
+ const shadowEnvBase = { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
5513
+ const check = spawnSync3("git", ["rev-parse", "--git-dir"], {
5514
+ env: shadowEnvBase,
5515
+ timeout: 3e3
5516
+ });
5517
+ if (check.status === 0) {
5518
+ const ptPath = path8.join(shadowDir, "project-path.txt");
5519
+ try {
5520
+ const stored = fs6.readFileSync(ptPath, "utf8").trim();
5521
+ if (stored === normalizedCwd) return true;
5522
+ if (process.env.NODE9_DEBUG === "1")
5523
+ console.error(
5524
+ `[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
5525
+ );
5526
+ fs6.rmSync(shadowDir, { recursive: true, force: true });
5527
+ } catch {
5528
+ try {
5529
+ fs6.writeFileSync(ptPath, normalizedCwd, "utf8");
5530
+ } catch {
5531
+ }
5532
+ return true;
5533
+ }
5534
+ }
5535
+ try {
5536
+ fs6.mkdirSync(shadowDir, { recursive: true });
5537
+ } catch {
5538
+ }
5539
+ const init = spawnSync3("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
5540
+ if (init.status !== 0) {
5541
+ if (process.env.NODE9_DEBUG === "1")
5542
+ console.error("[Node9] git init --bare failed:", init.stderr?.toString());
5543
+ return false;
5544
+ }
5545
+ const configFile = path8.join(shadowDir, "config");
5546
+ spawnSync3("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
5547
+ timeout: 3e3
5548
+ });
5549
+ spawnSync3("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
5550
+ timeout: 3e3
5551
+ });
5552
+ try {
5553
+ fs6.writeFileSync(path8.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
5554
+ } catch {
5555
+ }
5556
+ return true;
5557
+ }
5558
+ function buildGitEnv(cwd) {
5559
+ const shadowDir = getShadowRepoDir(cwd);
5560
+ const check = spawnSync3("git", ["rev-parse", "--git-dir"], {
5561
+ env: { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd },
5562
+ timeout: 2e3
5563
+ });
5564
+ if (check.status === 0) {
5565
+ return { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
5566
+ }
5567
+ return { ...process.env };
5568
+ }
5569
+ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = []) {
5570
+ let indexFile = null;
5325
5571
  try {
5326
5572
  const cwd = process.cwd();
5327
- if (!fs5.existsSync(path7.join(cwd, ".git"))) return null;
5328
- const tempIndex = path7.join(cwd, ".git", `node9_index_${Date.now()}`);
5329
- const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
5330
- spawnSync3("git", ["add", "-A"], { env });
5331
- const treeRes = spawnSync3("git", ["write-tree"], { env });
5332
- const treeHash = treeRes.stdout.toString().trim();
5333
- if (fs5.existsSync(tempIndex)) fs5.unlinkSync(tempIndex);
5573
+ const shadowDir = getShadowRepoDir(cwd);
5574
+ if (!ensureShadowRepo(shadowDir, cwd)) return null;
5575
+ writeShadowExcludes(shadowDir, ignorePaths);
5576
+ indexFile = path8.join(shadowDir, `index_${process.pid}_${Date.now()}`);
5577
+ const shadowEnv = {
5578
+ ...process.env,
5579
+ GIT_DIR: shadowDir,
5580
+ GIT_WORK_TREE: cwd,
5581
+ GIT_INDEX_FILE: indexFile
5582
+ };
5583
+ spawnSync3("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
5584
+ const treeRes = spawnSync3("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
5585
+ const treeHash = treeRes.stdout?.toString().trim();
5334
5586
  if (!treeHash || treeRes.status !== 0) return null;
5335
- const commitRes = spawnSync3("git", [
5336
- "commit-tree",
5337
- treeHash,
5338
- "-m",
5339
- `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`
5340
- ]);
5341
- const commitHash = commitRes.stdout.toString().trim();
5587
+ const commitRes = spawnSync3(
5588
+ "git",
5589
+ ["commit-tree", treeHash, "-m", `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`],
5590
+ { env: shadowEnv, timeout: GIT_TIMEOUT }
5591
+ );
5592
+ const commitHash = commitRes.stdout?.toString().trim();
5342
5593
  if (!commitHash || commitRes.status !== 0) return null;
5343
5594
  const stack = readStack();
5344
- const entry = {
5595
+ stack.push({
5345
5596
  hash: commitHash,
5346
5597
  tool,
5347
5598
  argsSummary: buildArgsSummary(tool, args),
5348
5599
  cwd,
5349
5600
  timestamp: Date.now()
5350
- };
5351
- stack.push(entry);
5601
+ });
5602
+ const shouldGc = stack.length % 5 === 0;
5352
5603
  if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
5353
5604
  writeStack(stack);
5354
- fs5.writeFileSync(UNDO_LATEST_PATH, commitHash);
5605
+ fs6.writeFileSync(UNDO_LATEST_PATH, commitHash);
5606
+ if (shouldGc) {
5607
+ spawn3("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
5608
+ }
5355
5609
  return commitHash;
5356
5610
  } catch (err) {
5357
5611
  if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
5612
+ return null;
5613
+ } finally {
5614
+ if (indexFile) {
5615
+ try {
5616
+ fs6.unlinkSync(indexFile);
5617
+ } catch {
5618
+ }
5619
+ }
5358
5620
  }
5359
- return null;
5360
5621
  }
5361
5622
  function getSnapshotHistory() {
5362
5623
  return readStack();
5363
5624
  }
5364
5625
  function computeUndoDiff(hash, cwd) {
5365
5626
  try {
5366
- const result = spawnSync3("git", ["diff", hash, "--stat", "--", "."], { cwd });
5367
- const stat = result.stdout.toString().trim();
5368
- if (!stat) return null;
5369
- const diff = spawnSync3("git", ["diff", hash, "--", "."], { cwd });
5370
- const raw = diff.stdout.toString();
5371
- if (!raw) return null;
5627
+ const env = buildGitEnv(cwd);
5628
+ const statRes = spawnSync3("git", ["diff", hash, "--stat", "--", "."], {
5629
+ cwd,
5630
+ env,
5631
+ timeout: GIT_TIMEOUT
5632
+ });
5633
+ const stat = statRes.stdout?.toString().trim();
5634
+ if (!stat || statRes.status !== 0) return null;
5635
+ const diffRes = spawnSync3("git", ["diff", hash, "--", "."], {
5636
+ cwd,
5637
+ env,
5638
+ timeout: GIT_TIMEOUT
5639
+ });
5640
+ const raw = diffRes.stdout?.toString();
5641
+ if (!raw || diffRes.status !== 0) return null;
5372
5642
  const lines = raw.split("\n").filter(
5373
5643
  (l) => !l.startsWith("diff --git") && !l.startsWith("index ") && !l.startsWith("Binary")
5374
5644
  );
@@ -5380,18 +5650,31 @@ function computeUndoDiff(hash, cwd) {
5380
5650
  function applyUndo(hash, cwd) {
5381
5651
  try {
5382
5652
  const dir = cwd ?? process.cwd();
5653
+ const env = buildGitEnv(dir);
5383
5654
  const restore = spawnSync3("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
5384
- cwd: dir
5655
+ cwd: dir,
5656
+ env,
5657
+ timeout: GIT_TIMEOUT
5385
5658
  });
5386
5659
  if (restore.status !== 0) return false;
5387
- const lsTree = spawnSync3("git", ["ls-tree", "-r", "--name-only", hash], { cwd: dir });
5388
- const snapshotFiles = new Set(lsTree.stdout.toString().trim().split("\n").filter(Boolean));
5389
- const tracked = spawnSync3("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5390
- const untracked = spawnSync3("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5660
+ const lsTree = spawnSync3("git", ["ls-tree", "-r", "--name-only", hash], {
5661
+ cwd: dir,
5662
+ env,
5663
+ timeout: GIT_TIMEOUT
5664
+ });
5665
+ const snapshotFiles = new Set(
5666
+ lsTree.stdout?.toString().trim().split("\n").filter(Boolean) ?? []
5667
+ );
5668
+ const tracked = spawnSync3("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
5669
+ const untracked = spawnSync3("git", ["ls-files", "--others", "--exclude-standard"], {
5670
+ cwd: dir,
5671
+ env,
5672
+ timeout: GIT_TIMEOUT
5673
+ }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
5391
5674
  for (const file of [...tracked, ...untracked]) {
5392
- const fullPath = path7.join(dir, file);
5393
- if (!snapshotFiles.has(file) && fs5.existsSync(fullPath)) {
5394
- fs5.unlinkSync(fullPath);
5675
+ const fullPath = path8.join(dir, file);
5676
+ if (!snapshotFiles.has(file) && fs6.existsSync(fullPath)) {
5677
+ fs6.unlinkSync(fullPath);
5395
5678
  }
5396
5679
  }
5397
5680
  return true;
@@ -5404,7 +5687,7 @@ function applyUndo(hash, cwd) {
5404
5687
  init_shields();
5405
5688
  import { confirm as confirm3 } from "@inquirer/prompts";
5406
5689
  var { version } = JSON.parse(
5407
- fs7.readFileSync(path9.join(__dirname, "../package.json"), "utf-8")
5690
+ fs8.readFileSync(path10.join(__dirname, "../package.json"), "utf-8")
5408
5691
  );
5409
5692
  function parseDuration(str) {
5410
5693
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -5500,7 +5783,7 @@ function openBrowserLocal() {
5500
5783
  }
5501
5784
  async function autoStartDaemonAndWait() {
5502
5785
  try {
5503
- const child = spawn4("node9", ["daemon"], {
5786
+ const child = spawn5("node9", ["daemon"], {
5504
5787
  detached: true,
5505
5788
  stdio: "ignore",
5506
5789
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
@@ -5537,7 +5820,7 @@ async function runProxy(targetCommand) {
5537
5820
  } catch {
5538
5821
  }
5539
5822
  console.log(chalk6.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
5540
- const child = spawn4(executable, args, {
5823
+ const child = spawn5(executable, args, {
5541
5824
  stdio: ["pipe", "pipe", "inherit"],
5542
5825
  // We control STDIN and STDOUT
5543
5826
  shell: false,
@@ -5606,14 +5889,14 @@ async function runProxy(targetCommand) {
5606
5889
  }
5607
5890
  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) => {
5608
5891
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
5609
- const credPath = path9.join(os7.homedir(), ".node9", "credentials.json");
5610
- if (!fs7.existsSync(path9.dirname(credPath)))
5611
- fs7.mkdirSync(path9.dirname(credPath), { recursive: true });
5892
+ const credPath = path10.join(os7.homedir(), ".node9", "credentials.json");
5893
+ if (!fs8.existsSync(path10.dirname(credPath)))
5894
+ fs8.mkdirSync(path10.dirname(credPath), { recursive: true });
5612
5895
  const profileName = options.profile || "default";
5613
5896
  let existingCreds = {};
5614
5897
  try {
5615
- if (fs7.existsSync(credPath)) {
5616
- const raw = JSON.parse(fs7.readFileSync(credPath, "utf-8"));
5898
+ if (fs8.existsSync(credPath)) {
5899
+ const raw = JSON.parse(fs8.readFileSync(credPath, "utf-8"));
5617
5900
  if (raw.apiKey) {
5618
5901
  existingCreds = {
5619
5902
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -5625,13 +5908,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
5625
5908
  } catch {
5626
5909
  }
5627
5910
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
5628
- fs7.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
5911
+ fs8.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
5629
5912
  if (profileName === "default") {
5630
- const configPath = path9.join(os7.homedir(), ".node9", "config.json");
5913
+ const configPath = path10.join(os7.homedir(), ".node9", "config.json");
5631
5914
  let config = {};
5632
5915
  try {
5633
- if (fs7.existsSync(configPath))
5634
- config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
5916
+ if (fs8.existsSync(configPath))
5917
+ config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
5635
5918
  } catch {
5636
5919
  }
5637
5920
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -5646,9 +5929,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
5646
5929
  approvers.cloud = false;
5647
5930
  }
5648
5931
  s.approvers = approvers;
5649
- if (!fs7.existsSync(path9.dirname(configPath)))
5650
- fs7.mkdirSync(path9.dirname(configPath), { recursive: true });
5651
- fs7.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
5932
+ if (!fs8.existsSync(path10.dirname(configPath)))
5933
+ fs8.mkdirSync(path10.dirname(configPath), { recursive: true });
5934
+ fs8.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
5652
5935
  }
5653
5936
  if (options.profile && profileName !== "default") {
5654
5937
  console.log(chalk6.green(`\u2705 Profile "${profileName}" saved`));
@@ -5734,15 +6017,15 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
5734
6017
  }
5735
6018
  }
5736
6019
  if (options.purge) {
5737
- const node9Dir = path9.join(os7.homedir(), ".node9");
5738
- if (fs7.existsSync(node9Dir)) {
6020
+ const node9Dir = path10.join(os7.homedir(), ".node9");
6021
+ if (fs8.existsSync(node9Dir)) {
5739
6022
  const confirmed = await confirm3({
5740
6023
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
5741
6024
  default: false
5742
6025
  });
5743
6026
  if (confirmed) {
5744
- fs7.rmSync(node9Dir, { recursive: true });
5745
- if (fs7.existsSync(node9Dir)) {
6027
+ fs8.rmSync(node9Dir, { recursive: true });
6028
+ if (fs8.existsSync(node9Dir)) {
5746
6029
  console.error(
5747
6030
  chalk6.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
5748
6031
  );
@@ -5814,10 +6097,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5814
6097
  );
5815
6098
  }
5816
6099
  section("Configuration");
5817
- const globalConfigPath = path9.join(homeDir2, ".node9", "config.json");
5818
- if (fs7.existsSync(globalConfigPath)) {
6100
+ const globalConfigPath = path10.join(homeDir2, ".node9", "config.json");
6101
+ if (fs8.existsSync(globalConfigPath)) {
5819
6102
  try {
5820
- JSON.parse(fs7.readFileSync(globalConfigPath, "utf-8"));
6103
+ JSON.parse(fs8.readFileSync(globalConfigPath, "utf-8"));
5821
6104
  pass("~/.node9/config.json found and valid");
5822
6105
  } catch {
5823
6106
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -5825,17 +6108,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
5825
6108
  } else {
5826
6109
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
5827
6110
  }
5828
- const projectConfigPath = path9.join(process.cwd(), "node9.config.json");
5829
- if (fs7.existsSync(projectConfigPath)) {
6111
+ const projectConfigPath = path10.join(process.cwd(), "node9.config.json");
6112
+ if (fs8.existsSync(projectConfigPath)) {
5830
6113
  try {
5831
- JSON.parse(fs7.readFileSync(projectConfigPath, "utf-8"));
6114
+ JSON.parse(fs8.readFileSync(projectConfigPath, "utf-8"));
5832
6115
  pass("node9.config.json found and valid (project)");
5833
6116
  } catch {
5834
6117
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
5835
6118
  }
5836
6119
  }
5837
- const credsPath = path9.join(homeDir2, ".node9", "credentials.json");
5838
- if (fs7.existsSync(credsPath)) {
6120
+ const credsPath = path10.join(homeDir2, ".node9", "credentials.json");
6121
+ if (fs8.existsSync(credsPath)) {
5839
6122
  pass("Cloud credentials found (~/.node9/credentials.json)");
5840
6123
  } else {
5841
6124
  warn(
@@ -5844,10 +6127,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5844
6127
  );
5845
6128
  }
5846
6129
  section("Agent Hooks");
5847
- const claudeSettingsPath = path9.join(homeDir2, ".claude", "settings.json");
5848
- if (fs7.existsSync(claudeSettingsPath)) {
6130
+ const claudeSettingsPath = path10.join(homeDir2, ".claude", "settings.json");
6131
+ if (fs8.existsSync(claudeSettingsPath)) {
5849
6132
  try {
5850
- const cs = JSON.parse(fs7.readFileSync(claudeSettingsPath, "utf-8"));
6133
+ const cs = JSON.parse(fs8.readFileSync(claudeSettingsPath, "utf-8"));
5851
6134
  const hasHook = cs.hooks?.PreToolUse?.some(
5852
6135
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
5853
6136
  );
@@ -5860,10 +6143,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5860
6143
  } else {
5861
6144
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
5862
6145
  }
5863
- const geminiSettingsPath = path9.join(homeDir2, ".gemini", "settings.json");
5864
- if (fs7.existsSync(geminiSettingsPath)) {
6146
+ const geminiSettingsPath = path10.join(homeDir2, ".gemini", "settings.json");
6147
+ if (fs8.existsSync(geminiSettingsPath)) {
5865
6148
  try {
5866
- const gs = JSON.parse(fs7.readFileSync(geminiSettingsPath, "utf-8"));
6149
+ const gs = JSON.parse(fs8.readFileSync(geminiSettingsPath, "utf-8"));
5867
6150
  const hasHook = gs.hooks?.BeforeTool?.some(
5868
6151
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
5869
6152
  );
@@ -5876,10 +6159,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5876
6159
  } else {
5877
6160
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
5878
6161
  }
5879
- const cursorHooksPath = path9.join(homeDir2, ".cursor", "hooks.json");
5880
- if (fs7.existsSync(cursorHooksPath)) {
6162
+ const cursorHooksPath = path10.join(homeDir2, ".cursor", "hooks.json");
6163
+ if (fs8.existsSync(cursorHooksPath)) {
5881
6164
  try {
5882
- const cur = JSON.parse(fs7.readFileSync(cursorHooksPath, "utf-8"));
6165
+ const cur = JSON.parse(fs8.readFileSync(cursorHooksPath, "utf-8"));
5883
6166
  const hasHook = cur.hooks?.preToolUse?.some(
5884
6167
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
5885
6168
  );
@@ -5981,8 +6264,8 @@ program.command("explain").description(
5981
6264
  console.log("");
5982
6265
  });
5983
6266
  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) => {
5984
- const configPath = path9.join(os7.homedir(), ".node9", "config.json");
5985
- if (fs7.existsSync(configPath) && !options.force) {
6267
+ const configPath = path10.join(os7.homedir(), ".node9", "config.json");
6268
+ if (fs8.existsSync(configPath) && !options.force) {
5986
6269
  console.log(chalk6.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
5987
6270
  console.log(chalk6.gray(` Run with --force to overwrite.`));
5988
6271
  return;
@@ -5996,9 +6279,9 @@ program.command("init").description("Create ~/.node9/config.json with default po
5996
6279
  mode: safeMode
5997
6280
  }
5998
6281
  };
5999
- const dir = path9.dirname(configPath);
6000
- if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
6001
- fs7.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
6282
+ const dir = path10.dirname(configPath);
6283
+ if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
6284
+ fs8.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
6002
6285
  console.log(chalk6.green(`\u2705 Global config created: ${configPath}`));
6003
6286
  console.log(chalk6.cyan(` Mode set to: ${safeMode}`));
6004
6287
  console.log(
@@ -6016,14 +6299,14 @@ function formatRelativeTime(timestamp) {
6016
6299
  return new Date(timestamp).toLocaleDateString();
6017
6300
  }
6018
6301
  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) => {
6019
- const logPath = path9.join(os7.homedir(), ".node9", "audit.log");
6020
- if (!fs7.existsSync(logPath)) {
6302
+ const logPath = path10.join(os7.homedir(), ".node9", "audit.log");
6303
+ if (!fs8.existsSync(logPath)) {
6021
6304
  console.log(
6022
6305
  chalk6.yellow("No audit logs found. Run node9 with an agent to generate entries.")
6023
6306
  );
6024
6307
  return;
6025
6308
  }
6026
- const raw = fs7.readFileSync(logPath, "utf-8");
6309
+ const raw = fs8.readFileSync(logPath, "utf-8");
6027
6310
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
6028
6311
  let entries = lines.flatMap((line) => {
6029
6312
  try {
@@ -6106,13 +6389,13 @@ program.command("status").description("Show current Node9 mode, policy source, a
6106
6389
  console.log("");
6107
6390
  const modeLabel = settings.mode === "audit" ? chalk6.blue("audit") : settings.mode === "strict" ? chalk6.red("strict") : chalk6.white("standard");
6108
6391
  console.log(` Mode: ${modeLabel}`);
6109
- const projectConfig = path9.join(process.cwd(), "node9.config.json");
6110
- const globalConfig = path9.join(os7.homedir(), ".node9", "config.json");
6392
+ const projectConfig = path10.join(process.cwd(), "node9.config.json");
6393
+ const globalConfig = path10.join(os7.homedir(), ".node9", "config.json");
6111
6394
  console.log(
6112
- ` Local: ${fs7.existsSync(projectConfig) ? chalk6.green("Active (node9.config.json)") : chalk6.gray("Not present")}`
6395
+ ` Local: ${fs8.existsSync(projectConfig) ? chalk6.green("Active (node9.config.json)") : chalk6.gray("Not present")}`
6113
6396
  );
6114
6397
  console.log(
6115
- ` Global: ${fs7.existsSync(globalConfig) ? chalk6.green("Active (~/.node9/config.json)") : chalk6.gray("Not present")}`
6398
+ ` Global: ${fs8.existsSync(globalConfig) ? chalk6.green("Active (~/.node9/config.json)") : chalk6.gray("Not present")}`
6116
6399
  );
6117
6400
  if (mergedConfig.policy.sandboxPaths.length > 0) {
6118
6401
  console.log(
@@ -6156,7 +6439,7 @@ program.command("daemon").description("Run the local approval server").argument(
6156
6439
  console.log(chalk6.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
6157
6440
  process.exit(0);
6158
6441
  }
6159
- const child = spawn4("node9", ["daemon"], { detached: true, stdio: "ignore" });
6442
+ const child = spawn5("node9", ["daemon"], { detached: true, stdio: "ignore" });
6160
6443
  child.unref();
6161
6444
  for (let i = 0; i < 12; i++) {
6162
6445
  await new Promise((r) => setTimeout(r, 250));
@@ -6168,7 +6451,7 @@ program.command("daemon").description("Run the local approval server").argument(
6168
6451
  process.exit(0);
6169
6452
  }
6170
6453
  if (options.background) {
6171
- const child = spawn4("node9", ["daemon"], { detached: true, stdio: "ignore" });
6454
+ const child = spawn5("node9", ["daemon"], { detached: true, stdio: "ignore" });
6172
6455
  child.unref();
6173
6456
  console.log(chalk6.green(`
6174
6457
  \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
@@ -6196,9 +6479,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
6196
6479
  } catch (err) {
6197
6480
  const tempConfig = getConfig();
6198
6481
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
6199
- const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
6482
+ const logPath = path10.join(os7.homedir(), ".node9", "hook-debug.log");
6200
6483
  const errMsg = err instanceof Error ? err.message : String(err);
6201
- fs7.appendFileSync(
6484
+ fs8.appendFileSync(
6202
6485
  logPath,
6203
6486
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
6204
6487
  RAW: ${raw}
@@ -6216,10 +6499,10 @@ RAW: ${raw}
6216
6499
  }
6217
6500
  const config = getConfig();
6218
6501
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
6219
- const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
6220
- if (!fs7.existsSync(path9.dirname(logPath)))
6221
- fs7.mkdirSync(path9.dirname(logPath), { recursive: true });
6222
- fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
6502
+ const logPath = path10.join(os7.homedir(), ".node9", "hook-debug.log");
6503
+ if (!fs8.existsSync(path10.dirname(logPath)))
6504
+ fs8.mkdirSync(path10.dirname(logPath), { recursive: true });
6505
+ fs8.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
6223
6506
  `);
6224
6507
  }
6225
6508
  const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
@@ -6263,7 +6546,7 @@ RAW: ${raw}
6263
6546
  }
6264
6547
  const meta = { agent, mcpServer };
6265
6548
  if (shouldSnapshot(toolName, toolInput, config)) {
6266
- await createShadowSnapshot(toolName, toolInput);
6549
+ await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
6267
6550
  }
6268
6551
  const result = await authorizeHeadless(toolName, toolInput, false, meta);
6269
6552
  if (result.approved) {
@@ -6296,9 +6579,9 @@ RAW: ${raw}
6296
6579
  });
6297
6580
  } catch (err) {
6298
6581
  if (process.env.NODE9_DEBUG === "1") {
6299
- const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
6582
+ const logPath = path10.join(os7.homedir(), ".node9", "hook-debug.log");
6300
6583
  const errMsg = err instanceof Error ? err.message : String(err);
6301
- fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
6584
+ fs8.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
6302
6585
  `);
6303
6586
  }
6304
6587
  process.exit(0);
@@ -6343,13 +6626,13 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
6343
6626
  decision: "allowed",
6344
6627
  source: "post-hook"
6345
6628
  };
6346
- const logPath = path9.join(os7.homedir(), ".node9", "audit.log");
6347
- if (!fs7.existsSync(path9.dirname(logPath)))
6348
- fs7.mkdirSync(path9.dirname(logPath), { recursive: true });
6349
- fs7.appendFileSync(logPath, JSON.stringify(entry) + "\n");
6629
+ const logPath = path10.join(os7.homedir(), ".node9", "audit.log");
6630
+ if (!fs8.existsSync(path10.dirname(logPath)))
6631
+ fs8.mkdirSync(path10.dirname(logPath), { recursive: true });
6632
+ fs8.appendFileSync(logPath, JSON.stringify(entry) + "\n");
6350
6633
  const config = getConfig();
6351
6634
  if (shouldSnapshot(tool, {}, config)) {
6352
- await createShadowSnapshot();
6635
+ await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
6353
6636
  }
6354
6637
  } catch {
6355
6638
  }
@@ -6622,9 +6905,9 @@ process.on("unhandledRejection", (reason) => {
6622
6905
  const isCheckHook = process.argv[2] === "check";
6623
6906
  if (isCheckHook) {
6624
6907
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
6625
- const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
6908
+ const logPath = path10.join(os7.homedir(), ".node9", "hook-debug.log");
6626
6909
  const msg = reason instanceof Error ? reason.message : String(reason);
6627
- fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
6910
+ fs8.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
6628
6911
  `);
6629
6912
  }
6630
6913
  process.exit(0);