@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.js CHANGED
@@ -380,8 +380,8 @@ function sanitizeConfig(raw) {
380
380
  }
381
381
  }
382
382
  const lines = result.error.issues.map((issue) => {
383
- const path10 = issue.path.length > 0 ? issue.path.join(".") : "root";
384
- return ` \u2022 ${path10}: ${issue.message}`;
383
+ const path11 = issue.path.length > 0 ? issue.path.join(".") : "root";
384
+ return ` \u2022 ${path11}: ${issue.message}`;
385
385
  });
386
386
  return {
387
387
  sanitized,
@@ -696,6 +696,39 @@ var init_shields = __esm({
696
696
  });
697
697
 
698
698
  // src/dlp.ts
699
+ function scanFilePath(filePath, cwd = process.cwd()) {
700
+ if (!filePath) return null;
701
+ let resolved;
702
+ try {
703
+ const absolute = import_path4.default.resolve(cwd, filePath);
704
+ resolved = import_fs2.default.realpathSync.native(absolute);
705
+ } catch (err) {
706
+ const code = err.code;
707
+ if (code === "ENOENT" || code === "ENOTDIR") {
708
+ resolved = import_path4.default.resolve(cwd, filePath);
709
+ } else {
710
+ return {
711
+ patternName: "Sensitive File Path",
712
+ fieldPath: "file_path",
713
+ redactedSample: filePath,
714
+ severity: "block"
715
+ };
716
+ }
717
+ }
718
+ const normalised = resolved.replace(/\\/g, "/");
719
+ for (const pattern of SENSITIVE_PATH_PATTERNS) {
720
+ if (pattern.test(normalised)) {
721
+ return {
722
+ patternName: "Sensitive File Path",
723
+ fieldPath: "file_path",
724
+ redactedSample: filePath,
725
+ // show original path in alert, not resolved
726
+ severity: "block"
727
+ };
728
+ }
729
+ }
730
+ return null;
731
+ }
699
732
  function maskSecret(raw, pattern) {
700
733
  const match = raw.match(pattern);
701
734
  if (!match) return "****";
@@ -748,10 +781,12 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
748
781
  }
749
782
  return null;
750
783
  }
751
- var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
784
+ var import_fs2, import_path4, DLP_PATTERNS, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
752
785
  var init_dlp = __esm({
753
786
  "src/dlp.ts"() {
754
787
  "use strict";
788
+ import_fs2 = __toESM(require("fs"));
789
+ import_path4 = __toESM(require("path"));
755
790
  DLP_PATTERNS = [
756
791
  { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
757
792
  { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
@@ -763,8 +798,43 @@ var init_dlp = __esm({
763
798
  regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
764
799
  severity: "block"
765
800
  },
801
+ // GCP service account JSON (detects the type field that uniquely identifies it)
802
+ {
803
+ name: "GCP Service Account",
804
+ regex: /"type"\s*:\s*"service_account"/,
805
+ severity: "block"
806
+ },
807
+ // NPM auth token in .npmrc format
808
+ {
809
+ name: "NPM Auth Token",
810
+ regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
811
+ severity: "block"
812
+ },
766
813
  { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
767
814
  ];
815
+ SENSITIVE_PATH_PATTERNS = [
816
+ /[/\\]\.ssh[/\\]/i,
817
+ /[/\\]\.aws[/\\]/i,
818
+ /[/\\]\.config[/\\]gcloud[/\\]/i,
819
+ /[/\\]\.azure[/\\]/i,
820
+ /[/\\]\.kube[/\\]config$/i,
821
+ /[/\\]\.env($|\.)/i,
822
+ // .env, .env.local, .env.production — not .envoy
823
+ /[/\\]\.git-credentials$/i,
824
+ /[/\\]\.npmrc$/i,
825
+ /[/\\]\.docker[/\\]config\.json$/i,
826
+ /[/\\][^/\\]+\.pem$/i,
827
+ /[/\\][^/\\]+\.key$/i,
828
+ /[/\\][^/\\]+\.p12$/i,
829
+ /[/\\][^/\\]+\.pfx$/i,
830
+ /^\/etc\/passwd$/,
831
+ /^\/etc\/shadow$/,
832
+ /^\/etc\/sudoers$/,
833
+ /[/\\]credentials\.json$/i,
834
+ /[/\\]id_rsa$/i,
835
+ /[/\\]id_ed25519$/i,
836
+ /[/\\]id_ecdsa$/i
837
+ ];
768
838
  MAX_DEPTH = 5;
769
839
  MAX_STRING_BYTES = 1e5;
770
840
  MAX_JSON_PARSE_BYTES = 1e4;
@@ -774,11 +844,11 @@ var init_dlp = __esm({
774
844
  // src/core.ts
775
845
  function checkPause() {
776
846
  try {
777
- if (!import_fs2.default.existsSync(PAUSED_FILE)) return { paused: false };
778
- const state = JSON.parse(import_fs2.default.readFileSync(PAUSED_FILE, "utf-8"));
847
+ if (!import_fs3.default.existsSync(PAUSED_FILE)) return { paused: false };
848
+ const state = JSON.parse(import_fs3.default.readFileSync(PAUSED_FILE, "utf-8"));
779
849
  if (state.expiry > 0 && Date.now() >= state.expiry) {
780
850
  try {
781
- import_fs2.default.unlinkSync(PAUSED_FILE);
851
+ import_fs3.default.unlinkSync(PAUSED_FILE);
782
852
  } catch {
783
853
  }
784
854
  return { paused: false };
@@ -789,11 +859,11 @@ function checkPause() {
789
859
  }
790
860
  }
791
861
  function atomicWriteSync(filePath, data, options) {
792
- const dir = import_path4.default.dirname(filePath);
793
- if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
862
+ const dir = import_path5.default.dirname(filePath);
863
+ if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
794
864
  const tmpPath = `${filePath}.${import_os2.default.hostname()}.${process.pid}.tmp`;
795
- import_fs2.default.writeFileSync(tmpPath, data, options);
796
- import_fs2.default.renameSync(tmpPath, filePath);
865
+ import_fs3.default.writeFileSync(tmpPath, data, options);
866
+ import_fs3.default.renameSync(tmpPath, filePath);
797
867
  }
798
868
  function pauseNode9(durationMs, durationStr) {
799
869
  const state = { expiry: Date.now() + durationMs, duration: durationStr };
@@ -801,18 +871,88 @@ function pauseNode9(durationMs, durationStr) {
801
871
  }
802
872
  function resumeNode9() {
803
873
  try {
804
- if (import_fs2.default.existsSync(PAUSED_FILE)) import_fs2.default.unlinkSync(PAUSED_FILE);
874
+ if (import_fs3.default.existsSync(PAUSED_FILE)) import_fs3.default.unlinkSync(PAUSED_FILE);
805
875
  } catch {
806
876
  }
807
877
  }
878
+ function validateRegex(pattern) {
879
+ if (!pattern) return "Pattern is required";
880
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
881
+ let parens = 0, brackets = 0, isEscaped = false, inCharClass = false;
882
+ for (let i = 0; i < pattern.length; i++) {
883
+ const char = pattern[i];
884
+ if (isEscaped) {
885
+ isEscaped = false;
886
+ continue;
887
+ }
888
+ if (char === "\\") {
889
+ isEscaped = true;
890
+ continue;
891
+ }
892
+ if (char === "[" && !inCharClass) {
893
+ inCharClass = true;
894
+ brackets++;
895
+ continue;
896
+ }
897
+ if (char === "]" && inCharClass) {
898
+ inCharClass = false;
899
+ brackets--;
900
+ continue;
901
+ }
902
+ if (inCharClass) continue;
903
+ if (char === "(") parens++;
904
+ else if (char === ")") parens--;
905
+ }
906
+ if (parens !== 0) return "Unbalanced parentheses";
907
+ if (brackets !== 0) return "Unbalanced brackets";
908
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
909
+ if (!(0, import_safe_regex2.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
910
+ try {
911
+ new RegExp(pattern);
912
+ } catch (e) {
913
+ return `Invalid regex syntax: ${e.message}`;
914
+ }
915
+ return null;
916
+ }
917
+ function getCompiledRegex(pattern, flags = "") {
918
+ if (flags && !/^[gimsuy]+$/.test(flags)) {
919
+ if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Invalid regex flags: "${flags}"`);
920
+ return null;
921
+ }
922
+ const key = `${pattern}\0${flags}`;
923
+ if (regexCache.has(key)) {
924
+ const cached = regexCache.get(key);
925
+ regexCache.delete(key);
926
+ regexCache.set(key, cached);
927
+ return cached;
928
+ }
929
+ const err = validateRegex(pattern);
930
+ if (err) {
931
+ if (process.env.NODE9_DEBUG === "1")
932
+ console.error(`[Node9] Regex blocked: ${err} \u2014 pattern: "${pattern}"`);
933
+ return null;
934
+ }
935
+ try {
936
+ const re = new RegExp(pattern, flags);
937
+ if (regexCache.size >= REGEX_CACHE_MAX) {
938
+ const oldest = regexCache.keys().next().value;
939
+ if (oldest) regexCache.delete(oldest);
940
+ }
941
+ regexCache.set(key, re);
942
+ return re;
943
+ } catch (e) {
944
+ if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Regex compile failed:`, e);
945
+ return null;
946
+ }
947
+ }
808
948
  function getActiveTrustSession(toolName) {
809
949
  try {
810
- if (!import_fs2.default.existsSync(TRUST_FILE)) return false;
811
- const trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
950
+ if (!import_fs3.default.existsSync(TRUST_FILE)) return false;
951
+ const trust = JSON.parse(import_fs3.default.readFileSync(TRUST_FILE, "utf-8"));
812
952
  const now = Date.now();
813
953
  const active = trust.entries.filter((e) => e.expiry > now);
814
954
  if (active.length !== trust.entries.length) {
815
- import_fs2.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
955
+ import_fs3.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
816
956
  }
817
957
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
818
958
  } catch {
@@ -823,8 +963,8 @@ function writeTrustSession(toolName, durationMs) {
823
963
  try {
824
964
  let trust = { entries: [] };
825
965
  try {
826
- if (import_fs2.default.existsSync(TRUST_FILE)) {
827
- trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
966
+ if (import_fs3.default.existsSync(TRUST_FILE)) {
967
+ trust = JSON.parse(import_fs3.default.readFileSync(TRUST_FILE, "utf-8"));
828
968
  }
829
969
  } catch {
830
970
  }
@@ -840,9 +980,9 @@ function writeTrustSession(toolName, durationMs) {
840
980
  }
841
981
  function appendToLog(logPath, entry) {
842
982
  try {
843
- const dir = import_path4.default.dirname(logPath);
844
- if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
845
- import_fs2.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
983
+ const dir = import_path5.default.dirname(logPath);
984
+ if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
985
+ import_fs3.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
846
986
  } catch {
847
987
  }
848
988
  }
@@ -884,9 +1024,9 @@ function matchesPattern(text, patterns) {
884
1024
  const withoutDotSlash = text.replace(/^\.\//, "");
885
1025
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
886
1026
  }
887
- function getNestedValue(obj, path10) {
1027
+ function getNestedValue(obj, path11) {
888
1028
  if (!obj || typeof obj !== "object") return null;
889
- return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
1029
+ return path11.split(".").reduce((prev, curr) => prev?.[curr], obj);
890
1030
  }
891
1031
  function shouldSnapshot(toolName, args, config) {
892
1032
  if (!config.settings.enableUndo) return false;
@@ -917,19 +1057,16 @@ function evaluateSmartConditions(args, rule) {
917
1057
  return val !== null && cond.value ? !val.includes(cond.value) : true;
918
1058
  case "matches": {
919
1059
  if (val === null || !cond.value) return false;
920
- try {
921
- return new RegExp(cond.value, cond.flags ?? "").test(val);
922
- } catch {
923
- return false;
924
- }
1060
+ const reM = getCompiledRegex(cond.value, cond.flags ?? "");
1061
+ if (!reM) return false;
1062
+ return reM.test(val);
925
1063
  }
926
1064
  case "notMatches": {
927
- if (val === null || !cond.value) return true;
928
- try {
929
- return !new RegExp(cond.value, cond.flags ?? "").test(val);
930
- } catch {
931
- return true;
932
- }
1065
+ if (!cond.value) return false;
1066
+ if (val === null) return true;
1067
+ const reN = getCompiledRegex(cond.value, cond.flags ?? "");
1068
+ if (!reN) return false;
1069
+ return !reN.test(val);
933
1070
  }
934
1071
  case "matchesGlob":
935
1072
  return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
@@ -1042,9 +1179,9 @@ function _resetConfigCache() {
1042
1179
  }
1043
1180
  function getGlobalSettings() {
1044
1181
  try {
1045
- const globalConfigPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
1046
- if (import_fs2.default.existsSync(globalConfigPath)) {
1047
- const parsed = JSON.parse(import_fs2.default.readFileSync(globalConfigPath, "utf-8"));
1182
+ const globalConfigPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "config.json");
1183
+ if (import_fs3.default.existsSync(globalConfigPath)) {
1184
+ const parsed = JSON.parse(import_fs3.default.readFileSync(globalConfigPath, "utf-8"));
1048
1185
  const settings = parsed.settings || {};
1049
1186
  return {
1050
1187
  mode: settings.mode || "audit",
@@ -1066,9 +1203,9 @@ function getGlobalSettings() {
1066
1203
  }
1067
1204
  function getInternalToken() {
1068
1205
  try {
1069
- const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1070
- if (!import_fs2.default.existsSync(pidFile)) return null;
1071
- const data = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1206
+ const pidFile = import_path5.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1207
+ if (!import_fs3.default.existsSync(pidFile)) return null;
1208
+ const data = JSON.parse(import_fs3.default.readFileSync(pidFile, "utf-8"));
1072
1209
  process.kill(data.pid, 0);
1073
1210
  return data.internalToken ?? null;
1074
1211
  } catch {
@@ -1183,9 +1320,9 @@ async function evaluatePolicy(toolName, args, agent) {
1183
1320
  }
1184
1321
  async function explainPolicy(toolName, args) {
1185
1322
  const steps = [];
1186
- const globalPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
1187
- const projectPath = import_path4.default.join(process.cwd(), "node9.config.json");
1188
- const credsPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
1323
+ const globalPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "config.json");
1324
+ const projectPath = import_path5.default.join(process.cwd(), "node9.config.json");
1325
+ const credsPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
1189
1326
  const waterfall = [
1190
1327
  {
1191
1328
  tier: 1,
@@ -1196,19 +1333,19 @@ async function explainPolicy(toolName, args) {
1196
1333
  {
1197
1334
  tier: 2,
1198
1335
  label: "Cloud policy",
1199
- status: import_fs2.default.existsSync(credsPath) ? "active" : "missing",
1200
- note: import_fs2.default.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1336
+ status: import_fs3.default.existsSync(credsPath) ? "active" : "missing",
1337
+ note: import_fs3.default.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1201
1338
  },
1202
1339
  {
1203
1340
  tier: 3,
1204
1341
  label: "Project config",
1205
- status: import_fs2.default.existsSync(projectPath) ? "active" : "missing",
1342
+ status: import_fs3.default.existsSync(projectPath) ? "active" : "missing",
1206
1343
  path: projectPath
1207
1344
  },
1208
1345
  {
1209
1346
  tier: 4,
1210
1347
  label: "Global config",
1211
- status: import_fs2.default.existsSync(globalPath) ? "active" : "missing",
1348
+ status: import_fs3.default.existsSync(globalPath) ? "active" : "missing",
1212
1349
  path: globalPath
1213
1350
  },
1214
1351
  {
@@ -1221,7 +1358,9 @@ async function explainPolicy(toolName, args) {
1221
1358
  const config = getConfig();
1222
1359
  const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
1223
1360
  if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
1224
- const dlpMatch = args !== void 0 ? scanArgs(args) : null;
1361
+ const argsObjE = args && typeof args === "object" && !Array.isArray(args) ? args : {};
1362
+ const filePathE = String(argsObjE.file_path ?? argsObjE.path ?? argsObjE.filename ?? "");
1363
+ const dlpMatch = (filePathE ? scanFilePath(filePathE) : null) ?? (args !== void 0 ? scanArgs(args) : null);
1225
1364
  if (dlpMatch) {
1226
1365
  steps.push({
1227
1366
  name: "DLP Content Scanner",
@@ -1444,10 +1583,10 @@ function isIgnoredTool(toolName) {
1444
1583
  return matchesPattern(toolName, config.policy.ignoredTools);
1445
1584
  }
1446
1585
  function isDaemonRunning() {
1447
- const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1448
- if (import_fs2.default.existsSync(pidFile)) {
1586
+ const pidFile = import_path5.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1587
+ if (import_fs3.default.existsSync(pidFile)) {
1449
1588
  try {
1450
- const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1589
+ const { pid, port } = JSON.parse(import_fs3.default.readFileSync(pidFile, "utf-8"));
1451
1590
  if (port !== DAEMON_PORT) return false;
1452
1591
  process.kill(pid, 0);
1453
1592
  return true;
@@ -1467,9 +1606,9 @@ function isDaemonRunning() {
1467
1606
  }
1468
1607
  function getPersistentDecision(toolName) {
1469
1608
  try {
1470
- const file = import_path4.default.join(import_os2.default.homedir(), ".node9", "decisions.json");
1471
- if (!import_fs2.default.existsSync(file)) return null;
1472
- const decisions = JSON.parse(import_fs2.default.readFileSync(file, "utf-8"));
1609
+ const file = import_path5.default.join(import_os2.default.homedir(), ".node9", "decisions.json");
1610
+ if (!import_fs3.default.existsSync(file)) return null;
1611
+ const decisions = JSON.parse(import_fs3.default.readFileSync(file, "utf-8"));
1473
1612
  const d = decisions[toolName];
1474
1613
  if (d === "allow" || d === "deny") return d;
1475
1614
  } catch {
@@ -1612,7 +1751,9 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1612
1751
  let policyMatchedWord;
1613
1752
  let riskMetadata;
1614
1753
  if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
1615
- const dlpMatch = scanArgs(args);
1754
+ const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
1755
+ const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
1756
+ const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
1616
1757
  if (dlpMatch) {
1617
1758
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
1618
1759
  if (dlpMatch.severity === "block") {
@@ -1986,8 +2127,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1986
2127
  }
1987
2128
  function getConfig() {
1988
2129
  if (cachedConfig) return cachedConfig;
1989
- const globalPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
1990
- const projectPath = import_path4.default.join(process.cwd(), "node9.config.json");
2130
+ const globalPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "config.json");
2131
+ const projectPath = import_path5.default.join(process.cwd(), "node9.config.json");
1991
2132
  const globalConfig = tryLoadConfig(globalPath);
1992
2133
  const projectConfig = tryLoadConfig(projectPath);
1993
2134
  const mergedSettings = {
@@ -2082,10 +2223,10 @@ function getConfig() {
2082
2223
  return cachedConfig;
2083
2224
  }
2084
2225
  function tryLoadConfig(filePath) {
2085
- if (!import_fs2.default.existsSync(filePath)) return null;
2226
+ if (!import_fs3.default.existsSync(filePath)) return null;
2086
2227
  let raw;
2087
2228
  try {
2088
- raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
2229
+ raw = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
2089
2230
  } catch (err) {
2090
2231
  const msg = err instanceof Error ? err.message : String(err);
2091
2232
  process.stderr.write(
@@ -2147,9 +2288,9 @@ function getCredentials() {
2147
2288
  };
2148
2289
  }
2149
2290
  try {
2150
- const credPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
2151
- if (import_fs2.default.existsSync(credPath)) {
2152
- const creds = JSON.parse(import_fs2.default.readFileSync(credPath, "utf-8"));
2291
+ const credPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
2292
+ if (import_fs3.default.existsSync(credPath)) {
2293
+ const creds = JSON.parse(import_fs3.default.readFileSync(credPath, "utf-8"));
2153
2294
  const profileName = process.env.NODE9_PROFILE || "default";
2154
2295
  const profile = creds[profileName];
2155
2296
  if (profile?.apiKey) {
@@ -2262,29 +2403,33 @@ async function resolveNode9SaaS(requestId, creds, approved) {
2262
2403
  } catch {
2263
2404
  }
2264
2405
  }
2265
- var import_chalk2, import_prompts, import_fs2, import_path4, import_os2, import_net, import_crypto2, import_child_process2, import_picomatch, import_sh_syntax, 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;
2406
+ var import_chalk2, import_prompts, import_fs3, import_path5, import_os2, import_net, import_crypto2, import_child_process2, import_picomatch, import_safe_regex2, import_sh_syntax, 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;
2266
2407
  var init_core = __esm({
2267
2408
  "src/core.ts"() {
2268
2409
  "use strict";
2269
2410
  import_chalk2 = __toESM(require("chalk"));
2270
2411
  import_prompts = require("@inquirer/prompts");
2271
- import_fs2 = __toESM(require("fs"));
2272
- import_path4 = __toESM(require("path"));
2412
+ import_fs3 = __toESM(require("fs"));
2413
+ import_path5 = __toESM(require("path"));
2273
2414
  import_os2 = __toESM(require("os"));
2274
2415
  import_net = __toESM(require("net"));
2275
2416
  import_crypto2 = require("crypto");
2276
2417
  import_child_process2 = require("child_process");
2277
2418
  import_picomatch = __toESM(require("picomatch"));
2419
+ import_safe_regex2 = __toESM(require("safe-regex2"));
2278
2420
  import_sh_syntax = require("sh-syntax");
2279
2421
  init_native();
2280
2422
  init_context_sniper();
2281
2423
  init_config_schema();
2282
2424
  init_shields();
2283
2425
  init_dlp();
2284
- PAUSED_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
2285
- TRUST_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "trust.json");
2286
- LOCAL_AUDIT_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "audit.log");
2287
- HOOK_DEBUG_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
2426
+ PAUSED_FILE = import_path5.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
2427
+ TRUST_FILE = import_path5.default.join(import_os2.default.homedir(), ".node9", "trust.json");
2428
+ LOCAL_AUDIT_LOG = import_path5.default.join(import_os2.default.homedir(), ".node9", "audit.log");
2429
+ HOOK_DEBUG_LOG = import_path5.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
2430
+ MAX_REGEX_LENGTH = 100;
2431
+ REGEX_CACHE_MAX = 500;
2432
+ regexCache = /* @__PURE__ */ new Map();
2288
2433
  SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2289
2434
  DANGEROUS_WORDS = [
2290
2435
  "mkfs",
@@ -2499,7 +2644,7 @@ var init_core = __esm({
2499
2644
  cachedConfig = null;
2500
2645
  DAEMON_PORT = 7391;
2501
2646
  DAEMON_HOST = "127.0.0.1";
2502
- ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path4.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
2647
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path5.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
2503
2648
  }
2504
2649
  });
2505
2650
 
@@ -3957,18 +4102,18 @@ var init_ui2 = __esm({
3957
4102
 
3958
4103
  // src/daemon/index.ts
3959
4104
  function atomicWriteSync2(filePath, data, options) {
3960
- const dir = import_path6.default.dirname(filePath);
3961
- if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
4105
+ const dir = import_path7.default.dirname(filePath);
4106
+ if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
3962
4107
  const tmpPath = `${filePath}.${(0, import_crypto3.randomUUID)()}.tmp`;
3963
- import_fs4.default.writeFileSync(tmpPath, data, options);
3964
- import_fs4.default.renameSync(tmpPath, filePath);
4108
+ import_fs5.default.writeFileSync(tmpPath, data, options);
4109
+ import_fs5.default.renameSync(tmpPath, filePath);
3965
4110
  }
3966
4111
  function writeTrustEntry(toolName, durationMs) {
3967
4112
  try {
3968
4113
  let trust = { entries: [] };
3969
4114
  try {
3970
- if (import_fs4.default.existsSync(TRUST_FILE2))
3971
- trust = JSON.parse(import_fs4.default.readFileSync(TRUST_FILE2, "utf-8"));
4115
+ if (import_fs5.default.existsSync(TRUST_FILE2))
4116
+ trust = JSON.parse(import_fs5.default.readFileSync(TRUST_FILE2, "utf-8"));
3972
4117
  } catch {
3973
4118
  }
3974
4119
  trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
@@ -3995,16 +4140,16 @@ function appendAuditLog(data) {
3995
4140
  decision: data.decision,
3996
4141
  source: "daemon"
3997
4142
  };
3998
- const dir = import_path6.default.dirname(AUDIT_LOG_FILE);
3999
- if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
4000
- import_fs4.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
4143
+ const dir = import_path7.default.dirname(AUDIT_LOG_FILE);
4144
+ if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
4145
+ import_fs5.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
4001
4146
  } catch {
4002
4147
  }
4003
4148
  }
4004
4149
  function getAuditHistory(limit = 20) {
4005
4150
  try {
4006
- if (!import_fs4.default.existsSync(AUDIT_LOG_FILE)) return [];
4007
- const lines = import_fs4.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
4151
+ if (!import_fs5.default.existsSync(AUDIT_LOG_FILE)) return [];
4152
+ const lines = import_fs5.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
4008
4153
  if (lines.length === 1 && lines[0] === "") return [];
4009
4154
  return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
4010
4155
  } catch {
@@ -4013,7 +4158,7 @@ function getAuditHistory(limit = 20) {
4013
4158
  }
4014
4159
  function getOrgName() {
4015
4160
  try {
4016
- if (import_fs4.default.existsSync(CREDENTIALS_FILE)) {
4161
+ if (import_fs5.default.existsSync(CREDENTIALS_FILE)) {
4017
4162
  return "Node9 Cloud";
4018
4163
  }
4019
4164
  } catch {
@@ -4021,13 +4166,13 @@ function getOrgName() {
4021
4166
  return null;
4022
4167
  }
4023
4168
  function hasStoredSlackKey() {
4024
- return import_fs4.default.existsSync(CREDENTIALS_FILE);
4169
+ return import_fs5.default.existsSync(CREDENTIALS_FILE);
4025
4170
  }
4026
4171
  function writeGlobalSetting(key, value) {
4027
4172
  let config = {};
4028
4173
  try {
4029
- if (import_fs4.default.existsSync(GLOBAL_CONFIG_FILE)) {
4030
- config = JSON.parse(import_fs4.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
4174
+ if (import_fs5.default.existsSync(GLOBAL_CONFIG_FILE)) {
4175
+ config = JSON.parse(import_fs5.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
4031
4176
  }
4032
4177
  } catch {
4033
4178
  }
@@ -4046,7 +4191,7 @@ function abandonPending() {
4046
4191
  });
4047
4192
  if (autoStarted) {
4048
4193
  try {
4049
- import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4194
+ import_fs5.default.unlinkSync(DAEMON_PID_FILE);
4050
4195
  } catch {
4051
4196
  }
4052
4197
  setTimeout(() => {
@@ -4096,8 +4241,8 @@ function readBody(req) {
4096
4241
  }
4097
4242
  function readPersistentDecisions() {
4098
4243
  try {
4099
- if (import_fs4.default.existsSync(DECISIONS_FILE)) {
4100
- return JSON.parse(import_fs4.default.readFileSync(DECISIONS_FILE, "utf-8"));
4244
+ if (import_fs5.default.existsSync(DECISIONS_FILE)) {
4245
+ return JSON.parse(import_fs5.default.readFileSync(DECISIONS_FILE, "utf-8"));
4101
4246
  }
4102
4247
  } catch {
4103
4248
  }
@@ -4126,7 +4271,7 @@ function startDaemon() {
4126
4271
  idleTimer = setTimeout(() => {
4127
4272
  if (autoStarted) {
4128
4273
  try {
4129
- import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4274
+ import_fs5.default.unlinkSync(DAEMON_PID_FILE);
4130
4275
  } catch {
4131
4276
  }
4132
4277
  }
@@ -4511,14 +4656,14 @@ data: ${JSON.stringify(item.data)}
4511
4656
  server.on("error", (e) => {
4512
4657
  if (e.code === "EADDRINUSE") {
4513
4658
  try {
4514
- if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
4515
- const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4659
+ if (import_fs5.default.existsSync(DAEMON_PID_FILE)) {
4660
+ const { pid } = JSON.parse(import_fs5.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4516
4661
  process.kill(pid, 0);
4517
4662
  return process.exit(0);
4518
4663
  }
4519
4664
  } catch {
4520
4665
  try {
4521
- import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4666
+ import_fs5.default.unlinkSync(DAEMON_PID_FILE);
4522
4667
  } catch {
4523
4668
  }
4524
4669
  server.listen(DAEMON_PORT2, DAEMON_HOST2);
@@ -4569,7 +4714,7 @@ data: ${JSON.stringify(item.data)}
4569
4714
  console.log(import_chalk4.default.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
4570
4715
  }
4571
4716
  try {
4572
- import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4717
+ import_fs5.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4573
4718
  } catch {
4574
4719
  }
4575
4720
  const ACTIVITY_MAX_BYTES = 1024 * 1024;
@@ -4611,30 +4756,30 @@ data: ${JSON.stringify(item.data)}
4611
4756
  unixServer.listen(ACTIVITY_SOCKET_PATH2);
4612
4757
  process.on("exit", () => {
4613
4758
  try {
4614
- import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4759
+ import_fs5.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4615
4760
  } catch {
4616
4761
  }
4617
4762
  });
4618
4763
  }
4619
4764
  function stopDaemon() {
4620
- if (!import_fs4.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
4765
+ if (!import_fs5.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
4621
4766
  try {
4622
- const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4767
+ const { pid } = JSON.parse(import_fs5.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4623
4768
  process.kill(pid, "SIGTERM");
4624
4769
  console.log(import_chalk4.default.green("\u2705 Stopped."));
4625
4770
  } catch {
4626
4771
  console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
4627
4772
  } finally {
4628
4773
  try {
4629
- import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4774
+ import_fs5.default.unlinkSync(DAEMON_PID_FILE);
4630
4775
  } catch {
4631
4776
  }
4632
4777
  }
4633
4778
  }
4634
4779
  function daemonStatus() {
4635
- if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
4780
+ if (import_fs5.default.existsSync(DAEMON_PID_FILE)) {
4636
4781
  try {
4637
- const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4782
+ const { pid } = JSON.parse(import_fs5.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4638
4783
  process.kill(pid, 0);
4639
4784
  console.log(import_chalk4.default.green("Node9 daemon: running"));
4640
4785
  return;
@@ -4653,31 +4798,31 @@ function daemonStatus() {
4653
4798
  console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
4654
4799
  }
4655
4800
  }
4656
- var import_http, import_net2, import_fs4, import_path6, import_os4, import_child_process3, import_crypto3, import_chalk4, ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
4801
+ var import_http, import_net2, import_fs5, import_path7, import_os4, import_child_process3, import_crypto3, import_chalk4, ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
4657
4802
  var init_daemon = __esm({
4658
4803
  "src/daemon/index.ts"() {
4659
4804
  "use strict";
4660
4805
  init_ui2();
4661
4806
  import_http = __toESM(require("http"));
4662
4807
  import_net2 = __toESM(require("net"));
4663
- import_fs4 = __toESM(require("fs"));
4664
- import_path6 = __toESM(require("path"));
4808
+ import_fs5 = __toESM(require("fs"));
4809
+ import_path7 = __toESM(require("path"));
4665
4810
  import_os4 = __toESM(require("os"));
4666
4811
  import_child_process3 = require("child_process");
4667
4812
  import_crypto3 = require("crypto");
4668
4813
  import_chalk4 = __toESM(require("chalk"));
4669
4814
  init_core();
4670
4815
  init_shields();
4671
- ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path6.default.join(import_os4.default.tmpdir(), "node9-activity.sock");
4816
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path7.default.join(import_os4.default.tmpdir(), "node9-activity.sock");
4672
4817
  DAEMON_PORT2 = 7391;
4673
4818
  DAEMON_HOST2 = "127.0.0.1";
4674
4819
  homeDir = import_os4.default.homedir();
4675
- DAEMON_PID_FILE = import_path6.default.join(homeDir, ".node9", "daemon.pid");
4676
- DECISIONS_FILE = import_path6.default.join(homeDir, ".node9", "decisions.json");
4677
- GLOBAL_CONFIG_FILE = import_path6.default.join(homeDir, ".node9", "config.json");
4678
- CREDENTIALS_FILE = import_path6.default.join(homeDir, ".node9", "credentials.json");
4679
- AUDIT_LOG_FILE = import_path6.default.join(homeDir, ".node9", "audit.log");
4680
- TRUST_FILE2 = import_path6.default.join(homeDir, ".node9", "trust.json");
4820
+ DAEMON_PID_FILE = import_path7.default.join(homeDir, ".node9", "daemon.pid");
4821
+ DECISIONS_FILE = import_path7.default.join(homeDir, ".node9", "decisions.json");
4822
+ GLOBAL_CONFIG_FILE = import_path7.default.join(homeDir, ".node9", "config.json");
4823
+ CREDENTIALS_FILE = import_path7.default.join(homeDir, ".node9", "credentials.json");
4824
+ AUDIT_LOG_FILE = import_path7.default.join(homeDir, ".node9", "audit.log");
4825
+ TRUST_FILE2 = import_path7.default.join(homeDir, ".node9", "trust.json");
4681
4826
  TRUST_DURATIONS = {
4682
4827
  "30m": 30 * 6e4,
4683
4828
  "1h": 60 * 6e4,
@@ -4738,9 +4883,9 @@ function renderPending(activity) {
4738
4883
  }
4739
4884
  async function ensureDaemon() {
4740
4885
  let pidPort = null;
4741
- if (import_fs6.default.existsSync(PID_FILE)) {
4886
+ if (import_fs7.default.existsSync(PID_FILE)) {
4742
4887
  try {
4743
- const { port } = JSON.parse(import_fs6.default.readFileSync(PID_FILE, "utf-8"));
4888
+ const { port } = JSON.parse(import_fs7.default.readFileSync(PID_FILE, "utf-8"));
4744
4889
  pidPort = port;
4745
4890
  } catch {
4746
4891
  console.error(import_chalk5.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
@@ -4895,19 +5040,19 @@ async function startTail(options = {}) {
4895
5040
  process.exit(1);
4896
5041
  });
4897
5042
  }
4898
- var import_http2, import_chalk5, import_fs6, import_os6, import_path8, import_readline, import_child_process5, PID_FILE, ICONS;
5043
+ var import_http2, import_chalk5, import_fs7, import_os6, import_path9, import_readline, import_child_process5, PID_FILE, ICONS;
4899
5044
  var init_tail = __esm({
4900
5045
  "src/tui/tail.ts"() {
4901
5046
  "use strict";
4902
5047
  import_http2 = __toESM(require("http"));
4903
5048
  import_chalk5 = __toESM(require("chalk"));
4904
- import_fs6 = __toESM(require("fs"));
5049
+ import_fs7 = __toESM(require("fs"));
4905
5050
  import_os6 = __toESM(require("os"));
4906
- import_path8 = __toESM(require("path"));
5051
+ import_path9 = __toESM(require("path"));
4907
5052
  import_readline = __toESM(require("readline"));
4908
5053
  import_child_process5 = require("child_process");
4909
5054
  init_daemon();
4910
- PID_FILE = import_path8.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
5055
+ PID_FILE = import_path9.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
4911
5056
  ICONS = {
4912
5057
  bash: "\u{1F4BB}",
4913
5058
  shell: "\u{1F4BB}",
@@ -4933,8 +5078,8 @@ var import_commander = require("commander");
4933
5078
  init_core();
4934
5079
 
4935
5080
  // src/setup.ts
4936
- var import_fs3 = __toESM(require("fs"));
4937
- var import_path5 = __toESM(require("path"));
5081
+ var import_fs4 = __toESM(require("fs"));
5082
+ var import_path6 = __toESM(require("path"));
4938
5083
  var import_os3 = __toESM(require("os"));
4939
5084
  var import_chalk3 = __toESM(require("chalk"));
4940
5085
  var import_prompts2 = require("@inquirer/prompts");
@@ -4952,17 +5097,17 @@ function fullPathCommand(subcommand) {
4952
5097
  }
4953
5098
  function readJson(filePath) {
4954
5099
  try {
4955
- if (import_fs3.default.existsSync(filePath)) {
4956
- return JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
5100
+ if (import_fs4.default.existsSync(filePath)) {
5101
+ return JSON.parse(import_fs4.default.readFileSync(filePath, "utf-8"));
4957
5102
  }
4958
5103
  } catch {
4959
5104
  }
4960
5105
  return null;
4961
5106
  }
4962
5107
  function writeJson(filePath, data) {
4963
- const dir = import_path5.default.dirname(filePath);
4964
- if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
4965
- import_fs3.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
5108
+ const dir = import_path6.default.dirname(filePath);
5109
+ if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
5110
+ import_fs4.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
4966
5111
  }
4967
5112
  function isNode9Hook(cmd) {
4968
5113
  if (!cmd) return false;
@@ -4970,8 +5115,8 @@ function isNode9Hook(cmd) {
4970
5115
  }
4971
5116
  function teardownClaude() {
4972
5117
  const homeDir2 = import_os3.default.homedir();
4973
- const hooksPath = import_path5.default.join(homeDir2, ".claude", "settings.json");
4974
- const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
5118
+ const hooksPath = import_path6.default.join(homeDir2, ".claude", "settings.json");
5119
+ const mcpPath = import_path6.default.join(homeDir2, ".claude.json");
4975
5120
  let changed = false;
4976
5121
  const settings = readJson(hooksPath);
4977
5122
  if (settings?.hooks) {
@@ -5020,7 +5165,7 @@ function teardownClaude() {
5020
5165
  }
5021
5166
  function teardownGemini() {
5022
5167
  const homeDir2 = import_os3.default.homedir();
5023
- const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
5168
+ const settingsPath = import_path6.default.join(homeDir2, ".gemini", "settings.json");
5024
5169
  const settings = readJson(settingsPath);
5025
5170
  if (!settings) {
5026
5171
  console.log(import_chalk3.default.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
@@ -5059,7 +5204,7 @@ function teardownGemini() {
5059
5204
  }
5060
5205
  function teardownCursor() {
5061
5206
  const homeDir2 = import_os3.default.homedir();
5062
- const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
5207
+ const mcpPath = import_path6.default.join(homeDir2, ".cursor", "mcp.json");
5063
5208
  const mcpConfig = readJson(mcpPath);
5064
5209
  if (!mcpConfig?.mcpServers) {
5065
5210
  console.log(import_chalk3.default.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
@@ -5086,8 +5231,8 @@ function teardownCursor() {
5086
5231
  }
5087
5232
  async function setupClaude() {
5088
5233
  const homeDir2 = import_os3.default.homedir();
5089
- const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
5090
- const hooksPath = import_path5.default.join(homeDir2, ".claude", "settings.json");
5234
+ const mcpPath = import_path6.default.join(homeDir2, ".claude.json");
5235
+ const hooksPath = import_path6.default.join(homeDir2, ".claude", "settings.json");
5091
5236
  const claudeConfig = readJson(mcpPath) ?? {};
5092
5237
  const settings = readJson(hooksPath) ?? {};
5093
5238
  const servers = claudeConfig.mcpServers ?? {};
@@ -5162,7 +5307,7 @@ async function setupClaude() {
5162
5307
  }
5163
5308
  async function setupGemini() {
5164
5309
  const homeDir2 = import_os3.default.homedir();
5165
- const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
5310
+ const settingsPath = import_path6.default.join(homeDir2, ".gemini", "settings.json");
5166
5311
  const settings = readJson(settingsPath) ?? {};
5167
5312
  const servers = settings.mcpServers ?? {};
5168
5313
  let anythingChanged = false;
@@ -5245,7 +5390,7 @@ async function setupGemini() {
5245
5390
  }
5246
5391
  async function setupCursor() {
5247
5392
  const homeDir2 = import_os3.default.homedir();
5248
- const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
5393
+ const mcpPath = import_path6.default.join(homeDir2, ".cursor", "mcp.json");
5249
5394
  const mcpConfig = readJson(mcpPath) ?? {};
5250
5395
  const servers = mcpConfig.mcpServers ?? {};
5251
5396
  let anythingChanged = false;
@@ -5306,30 +5451,32 @@ var import_execa = require("execa");
5306
5451
  var import_execa2 = require("execa");
5307
5452
  var import_chalk6 = __toESM(require("chalk"));
5308
5453
  var import_readline2 = __toESM(require("readline"));
5309
- var import_fs7 = __toESM(require("fs"));
5310
- var import_path9 = __toESM(require("path"));
5454
+ var import_fs8 = __toESM(require("fs"));
5455
+ var import_path10 = __toESM(require("path"));
5311
5456
  var import_os7 = __toESM(require("os"));
5312
5457
 
5313
5458
  // src/undo.ts
5314
5459
  var import_child_process4 = require("child_process");
5315
- var import_fs5 = __toESM(require("fs"));
5316
- var import_path7 = __toESM(require("path"));
5460
+ var import_crypto4 = __toESM(require("crypto"));
5461
+ var import_fs6 = __toESM(require("fs"));
5462
+ var import_path8 = __toESM(require("path"));
5317
5463
  var import_os5 = __toESM(require("os"));
5318
- var SNAPSHOT_STACK_PATH = import_path7.default.join(import_os5.default.homedir(), ".node9", "snapshots.json");
5319
- var UNDO_LATEST_PATH = import_path7.default.join(import_os5.default.homedir(), ".node9", "undo_latest.txt");
5464
+ var SNAPSHOT_STACK_PATH = import_path8.default.join(import_os5.default.homedir(), ".node9", "snapshots.json");
5465
+ var UNDO_LATEST_PATH = import_path8.default.join(import_os5.default.homedir(), ".node9", "undo_latest.txt");
5320
5466
  var MAX_SNAPSHOTS = 10;
5467
+ var GIT_TIMEOUT = 15e3;
5321
5468
  function readStack() {
5322
5469
  try {
5323
- if (import_fs5.default.existsSync(SNAPSHOT_STACK_PATH))
5324
- return JSON.parse(import_fs5.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
5470
+ if (import_fs6.default.existsSync(SNAPSHOT_STACK_PATH))
5471
+ return JSON.parse(import_fs6.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
5325
5472
  } catch {
5326
5473
  }
5327
5474
  return [];
5328
5475
  }
5329
5476
  function writeStack(stack) {
5330
- const dir = import_path7.default.dirname(SNAPSHOT_STACK_PATH);
5331
- if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
5332
- import_fs5.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
5477
+ const dir = import_path8.default.dirname(SNAPSHOT_STACK_PATH);
5478
+ if (!import_fs6.default.existsSync(dir)) import_fs6.default.mkdirSync(dir, { recursive: true });
5479
+ import_fs6.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
5333
5480
  }
5334
5481
  function buildArgsSummary(tool, args) {
5335
5482
  if (!args || typeof args !== "object") return "";
@@ -5342,54 +5489,177 @@ function buildArgsSummary(tool, args) {
5342
5489
  if (typeof sql === "string") return sql.slice(0, 80);
5343
5490
  return tool;
5344
5491
  }
5345
- async function createShadowSnapshot(tool = "unknown", args = {}) {
5492
+ function normalizeCwdForHash(cwd) {
5493
+ let normalized;
5494
+ try {
5495
+ normalized = import_fs6.default.realpathSync(cwd);
5496
+ } catch {
5497
+ normalized = cwd;
5498
+ }
5499
+ normalized = normalized.replace(/\\/g, "/");
5500
+ if (process.platform === "win32") normalized = normalized.toLowerCase();
5501
+ return normalized;
5502
+ }
5503
+ function getShadowRepoDir(cwd) {
5504
+ const hash = import_crypto4.default.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
5505
+ return import_path8.default.join(import_os5.default.homedir(), ".node9", "snapshots", hash);
5506
+ }
5507
+ function cleanOrphanedIndexFiles(shadowDir) {
5508
+ try {
5509
+ const cutoff = Date.now() - 6e4;
5510
+ for (const f of import_fs6.default.readdirSync(shadowDir)) {
5511
+ if (f.startsWith("index_")) {
5512
+ const fp = import_path8.default.join(shadowDir, f);
5513
+ try {
5514
+ if (import_fs6.default.statSync(fp).mtimeMs < cutoff) import_fs6.default.unlinkSync(fp);
5515
+ } catch {
5516
+ }
5517
+ }
5518
+ }
5519
+ } catch {
5520
+ }
5521
+ }
5522
+ function writeShadowExcludes(shadowDir, ignorePaths) {
5523
+ const hardcoded = [".git", ".node9"];
5524
+ const lines = [...hardcoded, ...ignorePaths].join("\n");
5525
+ try {
5526
+ import_fs6.default.writeFileSync(import_path8.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
5527
+ } catch {
5528
+ }
5529
+ }
5530
+ function ensureShadowRepo(shadowDir, cwd) {
5531
+ cleanOrphanedIndexFiles(shadowDir);
5532
+ const normalizedCwd = normalizeCwdForHash(cwd);
5533
+ const shadowEnvBase = { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
5534
+ const check = (0, import_child_process4.spawnSync)("git", ["rev-parse", "--git-dir"], {
5535
+ env: shadowEnvBase,
5536
+ timeout: 3e3
5537
+ });
5538
+ if (check.status === 0) {
5539
+ const ptPath = import_path8.default.join(shadowDir, "project-path.txt");
5540
+ try {
5541
+ const stored = import_fs6.default.readFileSync(ptPath, "utf8").trim();
5542
+ if (stored === normalizedCwd) return true;
5543
+ if (process.env.NODE9_DEBUG === "1")
5544
+ console.error(
5545
+ `[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
5546
+ );
5547
+ import_fs6.default.rmSync(shadowDir, { recursive: true, force: true });
5548
+ } catch {
5549
+ try {
5550
+ import_fs6.default.writeFileSync(ptPath, normalizedCwd, "utf8");
5551
+ } catch {
5552
+ }
5553
+ return true;
5554
+ }
5555
+ }
5556
+ try {
5557
+ import_fs6.default.mkdirSync(shadowDir, { recursive: true });
5558
+ } catch {
5559
+ }
5560
+ const init = (0, import_child_process4.spawnSync)("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
5561
+ if (init.status !== 0) {
5562
+ if (process.env.NODE9_DEBUG === "1")
5563
+ console.error("[Node9] git init --bare failed:", init.stderr?.toString());
5564
+ return false;
5565
+ }
5566
+ const configFile = import_path8.default.join(shadowDir, "config");
5567
+ (0, import_child_process4.spawnSync)("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
5568
+ timeout: 3e3
5569
+ });
5570
+ (0, import_child_process4.spawnSync)("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
5571
+ timeout: 3e3
5572
+ });
5573
+ try {
5574
+ import_fs6.default.writeFileSync(import_path8.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
5575
+ } catch {
5576
+ }
5577
+ return true;
5578
+ }
5579
+ function buildGitEnv(cwd) {
5580
+ const shadowDir = getShadowRepoDir(cwd);
5581
+ const check = (0, import_child_process4.spawnSync)("git", ["rev-parse", "--git-dir"], {
5582
+ env: { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd },
5583
+ timeout: 2e3
5584
+ });
5585
+ if (check.status === 0) {
5586
+ return { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
5587
+ }
5588
+ return { ...process.env };
5589
+ }
5590
+ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = []) {
5591
+ let indexFile = null;
5346
5592
  try {
5347
5593
  const cwd = process.cwd();
5348
- if (!import_fs5.default.existsSync(import_path7.default.join(cwd, ".git"))) return null;
5349
- const tempIndex = import_path7.default.join(cwd, ".git", `node9_index_${Date.now()}`);
5350
- const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
5351
- (0, import_child_process4.spawnSync)("git", ["add", "-A"], { env });
5352
- const treeRes = (0, import_child_process4.spawnSync)("git", ["write-tree"], { env });
5353
- const treeHash = treeRes.stdout.toString().trim();
5354
- if (import_fs5.default.existsSync(tempIndex)) import_fs5.default.unlinkSync(tempIndex);
5594
+ const shadowDir = getShadowRepoDir(cwd);
5595
+ if (!ensureShadowRepo(shadowDir, cwd)) return null;
5596
+ writeShadowExcludes(shadowDir, ignorePaths);
5597
+ indexFile = import_path8.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
5598
+ const shadowEnv = {
5599
+ ...process.env,
5600
+ GIT_DIR: shadowDir,
5601
+ GIT_WORK_TREE: cwd,
5602
+ GIT_INDEX_FILE: indexFile
5603
+ };
5604
+ (0, import_child_process4.spawnSync)("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
5605
+ const treeRes = (0, import_child_process4.spawnSync)("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
5606
+ const treeHash = treeRes.stdout?.toString().trim();
5355
5607
  if (!treeHash || treeRes.status !== 0) return null;
5356
- const commitRes = (0, import_child_process4.spawnSync)("git", [
5357
- "commit-tree",
5358
- treeHash,
5359
- "-m",
5360
- `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`
5361
- ]);
5362
- const commitHash = commitRes.stdout.toString().trim();
5608
+ const commitRes = (0, import_child_process4.spawnSync)(
5609
+ "git",
5610
+ ["commit-tree", treeHash, "-m", `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`],
5611
+ { env: shadowEnv, timeout: GIT_TIMEOUT }
5612
+ );
5613
+ const commitHash = commitRes.stdout?.toString().trim();
5363
5614
  if (!commitHash || commitRes.status !== 0) return null;
5364
5615
  const stack = readStack();
5365
- const entry = {
5616
+ stack.push({
5366
5617
  hash: commitHash,
5367
5618
  tool,
5368
5619
  argsSummary: buildArgsSummary(tool, args),
5369
5620
  cwd,
5370
5621
  timestamp: Date.now()
5371
- };
5372
- stack.push(entry);
5622
+ });
5623
+ const shouldGc = stack.length % 5 === 0;
5373
5624
  if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
5374
5625
  writeStack(stack);
5375
- import_fs5.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
5626
+ import_fs6.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
5627
+ if (shouldGc) {
5628
+ (0, import_child_process4.spawn)("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
5629
+ }
5376
5630
  return commitHash;
5377
5631
  } catch (err) {
5378
5632
  if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
5633
+ return null;
5634
+ } finally {
5635
+ if (indexFile) {
5636
+ try {
5637
+ import_fs6.default.unlinkSync(indexFile);
5638
+ } catch {
5639
+ }
5640
+ }
5379
5641
  }
5380
- return null;
5381
5642
  }
5382
5643
  function getSnapshotHistory() {
5383
5644
  return readStack();
5384
5645
  }
5385
5646
  function computeUndoDiff(hash, cwd) {
5386
5647
  try {
5387
- const result = (0, import_child_process4.spawnSync)("git", ["diff", hash, "--stat", "--", "."], { cwd });
5388
- const stat = result.stdout.toString().trim();
5389
- if (!stat) return null;
5390
- const diff = (0, import_child_process4.spawnSync)("git", ["diff", hash, "--", "."], { cwd });
5391
- const raw = diff.stdout.toString();
5392
- if (!raw) return null;
5648
+ const env = buildGitEnv(cwd);
5649
+ const statRes = (0, import_child_process4.spawnSync)("git", ["diff", hash, "--stat", "--", "."], {
5650
+ cwd,
5651
+ env,
5652
+ timeout: GIT_TIMEOUT
5653
+ });
5654
+ const stat = statRes.stdout?.toString().trim();
5655
+ if (!stat || statRes.status !== 0) return null;
5656
+ const diffRes = (0, import_child_process4.spawnSync)("git", ["diff", hash, "--", "."], {
5657
+ cwd,
5658
+ env,
5659
+ timeout: GIT_TIMEOUT
5660
+ });
5661
+ const raw = diffRes.stdout?.toString();
5662
+ if (!raw || diffRes.status !== 0) return null;
5393
5663
  const lines = raw.split("\n").filter(
5394
5664
  (l) => !l.startsWith("diff --git") && !l.startsWith("index ") && !l.startsWith("Binary")
5395
5665
  );
@@ -5401,18 +5671,31 @@ function computeUndoDiff(hash, cwd) {
5401
5671
  function applyUndo(hash, cwd) {
5402
5672
  try {
5403
5673
  const dir = cwd ?? process.cwd();
5674
+ const env = buildGitEnv(dir);
5404
5675
  const restore = (0, import_child_process4.spawnSync)("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
5405
- cwd: dir
5676
+ cwd: dir,
5677
+ env,
5678
+ timeout: GIT_TIMEOUT
5406
5679
  });
5407
5680
  if (restore.status !== 0) return false;
5408
- const lsTree = (0, import_child_process4.spawnSync)("git", ["ls-tree", "-r", "--name-only", hash], { cwd: dir });
5409
- const snapshotFiles = new Set(lsTree.stdout.toString().trim().split("\n").filter(Boolean));
5410
- const tracked = (0, import_child_process4.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5411
- const untracked = (0, import_child_process4.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5681
+ const lsTree = (0, import_child_process4.spawnSync)("git", ["ls-tree", "-r", "--name-only", hash], {
5682
+ cwd: dir,
5683
+ env,
5684
+ timeout: GIT_TIMEOUT
5685
+ });
5686
+ const snapshotFiles = new Set(
5687
+ lsTree.stdout?.toString().trim().split("\n").filter(Boolean) ?? []
5688
+ );
5689
+ const tracked = (0, import_child_process4.spawnSync)("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
5690
+ const untracked = (0, import_child_process4.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], {
5691
+ cwd: dir,
5692
+ env,
5693
+ timeout: GIT_TIMEOUT
5694
+ }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
5412
5695
  for (const file of [...tracked, ...untracked]) {
5413
- const fullPath = import_path7.default.join(dir, file);
5414
- if (!snapshotFiles.has(file) && import_fs5.default.existsSync(fullPath)) {
5415
- import_fs5.default.unlinkSync(fullPath);
5696
+ const fullPath = import_path8.default.join(dir, file);
5697
+ if (!snapshotFiles.has(file) && import_fs6.default.existsSync(fullPath)) {
5698
+ import_fs6.default.unlinkSync(fullPath);
5416
5699
  }
5417
5700
  }
5418
5701
  return true;
@@ -5425,7 +5708,7 @@ function applyUndo(hash, cwd) {
5425
5708
  init_shields();
5426
5709
  var import_prompts3 = require("@inquirer/prompts");
5427
5710
  var { version } = JSON.parse(
5428
- import_fs7.default.readFileSync(import_path9.default.join(__dirname, "../package.json"), "utf-8")
5711
+ import_fs8.default.readFileSync(import_path10.default.join(__dirname, "../package.json"), "utf-8")
5429
5712
  );
5430
5713
  function parseDuration(str) {
5431
5714
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -5627,14 +5910,14 @@ async function runProxy(targetCommand) {
5627
5910
  }
5628
5911
  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) => {
5629
5912
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
5630
- const credPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "credentials.json");
5631
- if (!import_fs7.default.existsSync(import_path9.default.dirname(credPath)))
5632
- import_fs7.default.mkdirSync(import_path9.default.dirname(credPath), { recursive: true });
5913
+ const credPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "credentials.json");
5914
+ if (!import_fs8.default.existsSync(import_path10.default.dirname(credPath)))
5915
+ import_fs8.default.mkdirSync(import_path10.default.dirname(credPath), { recursive: true });
5633
5916
  const profileName = options.profile || "default";
5634
5917
  let existingCreds = {};
5635
5918
  try {
5636
- if (import_fs7.default.existsSync(credPath)) {
5637
- const raw = JSON.parse(import_fs7.default.readFileSync(credPath, "utf-8"));
5919
+ if (import_fs8.default.existsSync(credPath)) {
5920
+ const raw = JSON.parse(import_fs8.default.readFileSync(credPath, "utf-8"));
5638
5921
  if (raw.apiKey) {
5639
5922
  existingCreds = {
5640
5923
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -5646,13 +5929,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
5646
5929
  } catch {
5647
5930
  }
5648
5931
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
5649
- import_fs7.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
5932
+ import_fs8.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
5650
5933
  if (profileName === "default") {
5651
- const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
5934
+ const configPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "config.json");
5652
5935
  let config = {};
5653
5936
  try {
5654
- if (import_fs7.default.existsSync(configPath))
5655
- config = JSON.parse(import_fs7.default.readFileSync(configPath, "utf-8"));
5937
+ if (import_fs8.default.existsSync(configPath))
5938
+ config = JSON.parse(import_fs8.default.readFileSync(configPath, "utf-8"));
5656
5939
  } catch {
5657
5940
  }
5658
5941
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -5667,9 +5950,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
5667
5950
  approvers.cloud = false;
5668
5951
  }
5669
5952
  s.approvers = approvers;
5670
- if (!import_fs7.default.existsSync(import_path9.default.dirname(configPath)))
5671
- import_fs7.default.mkdirSync(import_path9.default.dirname(configPath), { recursive: true });
5672
- import_fs7.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
5953
+ if (!import_fs8.default.existsSync(import_path10.default.dirname(configPath)))
5954
+ import_fs8.default.mkdirSync(import_path10.default.dirname(configPath), { recursive: true });
5955
+ import_fs8.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
5673
5956
  }
5674
5957
  if (options.profile && profileName !== "default") {
5675
5958
  console.log(import_chalk6.default.green(`\u2705 Profile "${profileName}" saved`));
@@ -5755,15 +6038,15 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
5755
6038
  }
5756
6039
  }
5757
6040
  if (options.purge) {
5758
- const node9Dir = import_path9.default.join(import_os7.default.homedir(), ".node9");
5759
- if (import_fs7.default.existsSync(node9Dir)) {
6041
+ const node9Dir = import_path10.default.join(import_os7.default.homedir(), ".node9");
6042
+ if (import_fs8.default.existsSync(node9Dir)) {
5760
6043
  const confirmed = await (0, import_prompts3.confirm)({
5761
6044
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
5762
6045
  default: false
5763
6046
  });
5764
6047
  if (confirmed) {
5765
- import_fs7.default.rmSync(node9Dir, { recursive: true });
5766
- if (import_fs7.default.existsSync(node9Dir)) {
6048
+ import_fs8.default.rmSync(node9Dir, { recursive: true });
6049
+ if (import_fs8.default.existsSync(node9Dir)) {
5767
6050
  console.error(
5768
6051
  import_chalk6.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
5769
6052
  );
@@ -5835,10 +6118,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5835
6118
  );
5836
6119
  }
5837
6120
  section("Configuration");
5838
- const globalConfigPath = import_path9.default.join(homeDir2, ".node9", "config.json");
5839
- if (import_fs7.default.existsSync(globalConfigPath)) {
6121
+ const globalConfigPath = import_path10.default.join(homeDir2, ".node9", "config.json");
6122
+ if (import_fs8.default.existsSync(globalConfigPath)) {
5840
6123
  try {
5841
- JSON.parse(import_fs7.default.readFileSync(globalConfigPath, "utf-8"));
6124
+ JSON.parse(import_fs8.default.readFileSync(globalConfigPath, "utf-8"));
5842
6125
  pass("~/.node9/config.json found and valid");
5843
6126
  } catch {
5844
6127
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -5846,17 +6129,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
5846
6129
  } else {
5847
6130
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
5848
6131
  }
5849
- const projectConfigPath = import_path9.default.join(process.cwd(), "node9.config.json");
5850
- if (import_fs7.default.existsSync(projectConfigPath)) {
6132
+ const projectConfigPath = import_path10.default.join(process.cwd(), "node9.config.json");
6133
+ if (import_fs8.default.existsSync(projectConfigPath)) {
5851
6134
  try {
5852
- JSON.parse(import_fs7.default.readFileSync(projectConfigPath, "utf-8"));
6135
+ JSON.parse(import_fs8.default.readFileSync(projectConfigPath, "utf-8"));
5853
6136
  pass("node9.config.json found and valid (project)");
5854
6137
  } catch {
5855
6138
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
5856
6139
  }
5857
6140
  }
5858
- const credsPath = import_path9.default.join(homeDir2, ".node9", "credentials.json");
5859
- if (import_fs7.default.existsSync(credsPath)) {
6141
+ const credsPath = import_path10.default.join(homeDir2, ".node9", "credentials.json");
6142
+ if (import_fs8.default.existsSync(credsPath)) {
5860
6143
  pass("Cloud credentials found (~/.node9/credentials.json)");
5861
6144
  } else {
5862
6145
  warn(
@@ -5865,10 +6148,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5865
6148
  );
5866
6149
  }
5867
6150
  section("Agent Hooks");
5868
- const claudeSettingsPath = import_path9.default.join(homeDir2, ".claude", "settings.json");
5869
- if (import_fs7.default.existsSync(claudeSettingsPath)) {
6151
+ const claudeSettingsPath = import_path10.default.join(homeDir2, ".claude", "settings.json");
6152
+ if (import_fs8.default.existsSync(claudeSettingsPath)) {
5870
6153
  try {
5871
- const cs = JSON.parse(import_fs7.default.readFileSync(claudeSettingsPath, "utf-8"));
6154
+ const cs = JSON.parse(import_fs8.default.readFileSync(claudeSettingsPath, "utf-8"));
5872
6155
  const hasHook = cs.hooks?.PreToolUse?.some(
5873
6156
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
5874
6157
  );
@@ -5881,10 +6164,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5881
6164
  } else {
5882
6165
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
5883
6166
  }
5884
- const geminiSettingsPath = import_path9.default.join(homeDir2, ".gemini", "settings.json");
5885
- if (import_fs7.default.existsSync(geminiSettingsPath)) {
6167
+ const geminiSettingsPath = import_path10.default.join(homeDir2, ".gemini", "settings.json");
6168
+ if (import_fs8.default.existsSync(geminiSettingsPath)) {
5886
6169
  try {
5887
- const gs = JSON.parse(import_fs7.default.readFileSync(geminiSettingsPath, "utf-8"));
6170
+ const gs = JSON.parse(import_fs8.default.readFileSync(geminiSettingsPath, "utf-8"));
5888
6171
  const hasHook = gs.hooks?.BeforeTool?.some(
5889
6172
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
5890
6173
  );
@@ -5897,10 +6180,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5897
6180
  } else {
5898
6181
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
5899
6182
  }
5900
- const cursorHooksPath = import_path9.default.join(homeDir2, ".cursor", "hooks.json");
5901
- if (import_fs7.default.existsSync(cursorHooksPath)) {
6183
+ const cursorHooksPath = import_path10.default.join(homeDir2, ".cursor", "hooks.json");
6184
+ if (import_fs8.default.existsSync(cursorHooksPath)) {
5902
6185
  try {
5903
- const cur = JSON.parse(import_fs7.default.readFileSync(cursorHooksPath, "utf-8"));
6186
+ const cur = JSON.parse(import_fs8.default.readFileSync(cursorHooksPath, "utf-8"));
5904
6187
  const hasHook = cur.hooks?.preToolUse?.some(
5905
6188
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
5906
6189
  );
@@ -6002,8 +6285,8 @@ program.command("explain").description(
6002
6285
  console.log("");
6003
6286
  });
6004
6287
  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) => {
6005
- const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
6006
- if (import_fs7.default.existsSync(configPath) && !options.force) {
6288
+ const configPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "config.json");
6289
+ if (import_fs8.default.existsSync(configPath) && !options.force) {
6007
6290
  console.log(import_chalk6.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
6008
6291
  console.log(import_chalk6.default.gray(` Run with --force to overwrite.`));
6009
6292
  return;
@@ -6017,9 +6300,9 @@ program.command("init").description("Create ~/.node9/config.json with default po
6017
6300
  mode: safeMode
6018
6301
  }
6019
6302
  };
6020
- const dir = import_path9.default.dirname(configPath);
6021
- if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
6022
- import_fs7.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
6303
+ const dir = import_path10.default.dirname(configPath);
6304
+ if (!import_fs8.default.existsSync(dir)) import_fs8.default.mkdirSync(dir, { recursive: true });
6305
+ import_fs8.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
6023
6306
  console.log(import_chalk6.default.green(`\u2705 Global config created: ${configPath}`));
6024
6307
  console.log(import_chalk6.default.cyan(` Mode set to: ${safeMode}`));
6025
6308
  console.log(
@@ -6037,14 +6320,14 @@ function formatRelativeTime(timestamp) {
6037
6320
  return new Date(timestamp).toLocaleDateString();
6038
6321
  }
6039
6322
  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) => {
6040
- const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
6041
- if (!import_fs7.default.existsSync(logPath)) {
6323
+ const logPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "audit.log");
6324
+ if (!import_fs8.default.existsSync(logPath)) {
6042
6325
  console.log(
6043
6326
  import_chalk6.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
6044
6327
  );
6045
6328
  return;
6046
6329
  }
6047
- const raw = import_fs7.default.readFileSync(logPath, "utf-8");
6330
+ const raw = import_fs8.default.readFileSync(logPath, "utf-8");
6048
6331
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
6049
6332
  let entries = lines.flatMap((line) => {
6050
6333
  try {
@@ -6127,13 +6410,13 @@ program.command("status").description("Show current Node9 mode, policy source, a
6127
6410
  console.log("");
6128
6411
  const modeLabel = settings.mode === "audit" ? import_chalk6.default.blue("audit") : settings.mode === "strict" ? import_chalk6.default.red("strict") : import_chalk6.default.white("standard");
6129
6412
  console.log(` Mode: ${modeLabel}`);
6130
- const projectConfig = import_path9.default.join(process.cwd(), "node9.config.json");
6131
- const globalConfig = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
6413
+ const projectConfig = import_path10.default.join(process.cwd(), "node9.config.json");
6414
+ const globalConfig = import_path10.default.join(import_os7.default.homedir(), ".node9", "config.json");
6132
6415
  console.log(
6133
- ` Local: ${import_fs7.default.existsSync(projectConfig) ? import_chalk6.default.green("Active (node9.config.json)") : import_chalk6.default.gray("Not present")}`
6416
+ ` Local: ${import_fs8.default.existsSync(projectConfig) ? import_chalk6.default.green("Active (node9.config.json)") : import_chalk6.default.gray("Not present")}`
6134
6417
  );
6135
6418
  console.log(
6136
- ` Global: ${import_fs7.default.existsSync(globalConfig) ? import_chalk6.default.green("Active (~/.node9/config.json)") : import_chalk6.default.gray("Not present")}`
6419
+ ` Global: ${import_fs8.default.existsSync(globalConfig) ? import_chalk6.default.green("Active (~/.node9/config.json)") : import_chalk6.default.gray("Not present")}`
6137
6420
  );
6138
6421
  if (mergedConfig.policy.sandboxPaths.length > 0) {
6139
6422
  console.log(
@@ -6217,9 +6500,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
6217
6500
  } catch (err) {
6218
6501
  const tempConfig = getConfig();
6219
6502
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
6220
- const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6503
+ const logPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6221
6504
  const errMsg = err instanceof Error ? err.message : String(err);
6222
- import_fs7.default.appendFileSync(
6505
+ import_fs8.default.appendFileSync(
6223
6506
  logPath,
6224
6507
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
6225
6508
  RAW: ${raw}
@@ -6237,10 +6520,10 @@ RAW: ${raw}
6237
6520
  }
6238
6521
  const config = getConfig();
6239
6522
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
6240
- const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6241
- if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
6242
- import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
6243
- import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
6523
+ const logPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6524
+ if (!import_fs8.default.existsSync(import_path10.default.dirname(logPath)))
6525
+ import_fs8.default.mkdirSync(import_path10.default.dirname(logPath), { recursive: true });
6526
+ import_fs8.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
6244
6527
  `);
6245
6528
  }
6246
6529
  const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
@@ -6284,7 +6567,7 @@ RAW: ${raw}
6284
6567
  }
6285
6568
  const meta = { agent, mcpServer };
6286
6569
  if (shouldSnapshot(toolName, toolInput, config)) {
6287
- await createShadowSnapshot(toolName, toolInput);
6570
+ await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
6288
6571
  }
6289
6572
  const result = await authorizeHeadless(toolName, toolInput, false, meta);
6290
6573
  if (result.approved) {
@@ -6317,9 +6600,9 @@ RAW: ${raw}
6317
6600
  });
6318
6601
  } catch (err) {
6319
6602
  if (process.env.NODE9_DEBUG === "1") {
6320
- const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6603
+ const logPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6321
6604
  const errMsg = err instanceof Error ? err.message : String(err);
6322
- import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
6605
+ import_fs8.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
6323
6606
  `);
6324
6607
  }
6325
6608
  process.exit(0);
@@ -6364,13 +6647,13 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
6364
6647
  decision: "allowed",
6365
6648
  source: "post-hook"
6366
6649
  };
6367
- const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
6368
- if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
6369
- import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
6370
- import_fs7.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
6650
+ const logPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "audit.log");
6651
+ if (!import_fs8.default.existsSync(import_path10.default.dirname(logPath)))
6652
+ import_fs8.default.mkdirSync(import_path10.default.dirname(logPath), { recursive: true });
6653
+ import_fs8.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
6371
6654
  const config = getConfig();
6372
6655
  if (shouldSnapshot(tool, {}, config)) {
6373
- await createShadowSnapshot();
6656
+ await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
6374
6657
  }
6375
6658
  } catch {
6376
6659
  }
@@ -6643,9 +6926,9 @@ process.on("unhandledRejection", (reason) => {
6643
6926
  const isCheckHook = process.argv[2] === "check";
6644
6927
  if (isCheckHook) {
6645
6928
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
6646
- const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6929
+ const logPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6647
6930
  const msg = reason instanceof Error ? reason.message : String(reason);
6648
- import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
6931
+ import_fs8.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
6649
6932
  `);
6650
6933
  }
6651
6934
  process.exit(0);