@node9/proxy 1.0.18 → 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",
@@ -2404,7 +2549,7 @@ var init_core = __esm({
2404
2549
  {
2405
2550
  field: "command",
2406
2551
  op: "matches",
2407
- value: "git push.*(--force|--force-with-lease|-f\\b)",
2552
+ value: "^\\s*git\\b.*\\bpush\\b.*(--force|--force-with-lease|-f\\b)",
2408
2553
  flags: "i"
2409
2554
  }
2410
2555
  ],
@@ -2415,7 +2560,14 @@ var init_core = __esm({
2415
2560
  {
2416
2561
  name: "review-git-push",
2417
2562
  tool: "bash",
2418
- conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
2563
+ conditions: [
2564
+ {
2565
+ field: "command",
2566
+ op: "matches",
2567
+ value: "^\\s*git\\b.*\\bpush\\b(?!.*(-f\\b|--force|--force-with-lease))",
2568
+ flags: "i"
2569
+ }
2570
+ ],
2419
2571
  conditionMode: "all",
2420
2572
  verdict: "review",
2421
2573
  reason: "git push sends changes to a shared remote"
@@ -2427,7 +2579,7 @@ var init_core = __esm({
2427
2579
  {
2428
2580
  field: "command",
2429
2581
  op: "matches",
2430
- value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
2582
+ value: "^\\s*git\\b.*(reset\\s+--hard|clean\\s+-[fdxX]|\\brebase\\b|tag\\s+-d|branch\\s+-[dD])",
2431
2583
  flags: "i"
2432
2584
  }
2433
2585
  ],
@@ -2492,7 +2644,7 @@ var init_core = __esm({
2492
2644
  cachedConfig = null;
2493
2645
  DAEMON_PORT = 7391;
2494
2646
  DAEMON_HOST = "127.0.0.1";
2495
- 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");
2496
2648
  }
2497
2649
  });
2498
2650
 
@@ -3950,18 +4102,18 @@ var init_ui2 = __esm({
3950
4102
 
3951
4103
  // src/daemon/index.ts
3952
4104
  function atomicWriteSync2(filePath, data, options) {
3953
- const dir = import_path6.default.dirname(filePath);
3954
- 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 });
3955
4107
  const tmpPath = `${filePath}.${(0, import_crypto3.randomUUID)()}.tmp`;
3956
- import_fs4.default.writeFileSync(tmpPath, data, options);
3957
- import_fs4.default.renameSync(tmpPath, filePath);
4108
+ import_fs5.default.writeFileSync(tmpPath, data, options);
4109
+ import_fs5.default.renameSync(tmpPath, filePath);
3958
4110
  }
3959
4111
  function writeTrustEntry(toolName, durationMs) {
3960
4112
  try {
3961
4113
  let trust = { entries: [] };
3962
4114
  try {
3963
- if (import_fs4.default.existsSync(TRUST_FILE2))
3964
- 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"));
3965
4117
  } catch {
3966
4118
  }
3967
4119
  trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
@@ -3988,16 +4140,16 @@ function appendAuditLog(data) {
3988
4140
  decision: data.decision,
3989
4141
  source: "daemon"
3990
4142
  };
3991
- const dir = import_path6.default.dirname(AUDIT_LOG_FILE);
3992
- if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
3993
- 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");
3994
4146
  } catch {
3995
4147
  }
3996
4148
  }
3997
4149
  function getAuditHistory(limit = 20) {
3998
4150
  try {
3999
- if (!import_fs4.default.existsSync(AUDIT_LOG_FILE)) return [];
4000
- 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");
4001
4153
  if (lines.length === 1 && lines[0] === "") return [];
4002
4154
  return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
4003
4155
  } catch {
@@ -4006,7 +4158,7 @@ function getAuditHistory(limit = 20) {
4006
4158
  }
4007
4159
  function getOrgName() {
4008
4160
  try {
4009
- if (import_fs4.default.existsSync(CREDENTIALS_FILE)) {
4161
+ if (import_fs5.default.existsSync(CREDENTIALS_FILE)) {
4010
4162
  return "Node9 Cloud";
4011
4163
  }
4012
4164
  } catch {
@@ -4014,13 +4166,13 @@ function getOrgName() {
4014
4166
  return null;
4015
4167
  }
4016
4168
  function hasStoredSlackKey() {
4017
- return import_fs4.default.existsSync(CREDENTIALS_FILE);
4169
+ return import_fs5.default.existsSync(CREDENTIALS_FILE);
4018
4170
  }
4019
4171
  function writeGlobalSetting(key, value) {
4020
4172
  let config = {};
4021
4173
  try {
4022
- if (import_fs4.default.existsSync(GLOBAL_CONFIG_FILE)) {
4023
- 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"));
4024
4176
  }
4025
4177
  } catch {
4026
4178
  }
@@ -4039,7 +4191,7 @@ function abandonPending() {
4039
4191
  });
4040
4192
  if (autoStarted) {
4041
4193
  try {
4042
- import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4194
+ import_fs5.default.unlinkSync(DAEMON_PID_FILE);
4043
4195
  } catch {
4044
4196
  }
4045
4197
  setTimeout(() => {
@@ -4089,8 +4241,8 @@ function readBody(req) {
4089
4241
  }
4090
4242
  function readPersistentDecisions() {
4091
4243
  try {
4092
- if (import_fs4.default.existsSync(DECISIONS_FILE)) {
4093
- 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"));
4094
4246
  }
4095
4247
  } catch {
4096
4248
  }
@@ -4119,7 +4271,7 @@ function startDaemon() {
4119
4271
  idleTimer = setTimeout(() => {
4120
4272
  if (autoStarted) {
4121
4273
  try {
4122
- import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4274
+ import_fs5.default.unlinkSync(DAEMON_PID_FILE);
4123
4275
  } catch {
4124
4276
  }
4125
4277
  }
@@ -4504,14 +4656,14 @@ data: ${JSON.stringify(item.data)}
4504
4656
  server.on("error", (e) => {
4505
4657
  if (e.code === "EADDRINUSE") {
4506
4658
  try {
4507
- if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
4508
- 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"));
4509
4661
  process.kill(pid, 0);
4510
4662
  return process.exit(0);
4511
4663
  }
4512
4664
  } catch {
4513
4665
  try {
4514
- import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4666
+ import_fs5.default.unlinkSync(DAEMON_PID_FILE);
4515
4667
  } catch {
4516
4668
  }
4517
4669
  server.listen(DAEMON_PORT2, DAEMON_HOST2);
@@ -4562,7 +4714,7 @@ data: ${JSON.stringify(item.data)}
4562
4714
  console.log(import_chalk4.default.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
4563
4715
  }
4564
4716
  try {
4565
- import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4717
+ import_fs5.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4566
4718
  } catch {
4567
4719
  }
4568
4720
  const ACTIVITY_MAX_BYTES = 1024 * 1024;
@@ -4604,30 +4756,30 @@ data: ${JSON.stringify(item.data)}
4604
4756
  unixServer.listen(ACTIVITY_SOCKET_PATH2);
4605
4757
  process.on("exit", () => {
4606
4758
  try {
4607
- import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4759
+ import_fs5.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4608
4760
  } catch {
4609
4761
  }
4610
4762
  });
4611
4763
  }
4612
4764
  function stopDaemon() {
4613
- 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."));
4614
4766
  try {
4615
- 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"));
4616
4768
  process.kill(pid, "SIGTERM");
4617
4769
  console.log(import_chalk4.default.green("\u2705 Stopped."));
4618
4770
  } catch {
4619
4771
  console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
4620
4772
  } finally {
4621
4773
  try {
4622
- import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4774
+ import_fs5.default.unlinkSync(DAEMON_PID_FILE);
4623
4775
  } catch {
4624
4776
  }
4625
4777
  }
4626
4778
  }
4627
4779
  function daemonStatus() {
4628
- if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
4780
+ if (import_fs5.default.existsSync(DAEMON_PID_FILE)) {
4629
4781
  try {
4630
- 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"));
4631
4783
  process.kill(pid, 0);
4632
4784
  console.log(import_chalk4.default.green("Node9 daemon: running"));
4633
4785
  return;
@@ -4646,31 +4798,31 @@ function daemonStatus() {
4646
4798
  console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
4647
4799
  }
4648
4800
  }
4649
- 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;
4650
4802
  var init_daemon = __esm({
4651
4803
  "src/daemon/index.ts"() {
4652
4804
  "use strict";
4653
4805
  init_ui2();
4654
4806
  import_http = __toESM(require("http"));
4655
4807
  import_net2 = __toESM(require("net"));
4656
- import_fs4 = __toESM(require("fs"));
4657
- import_path6 = __toESM(require("path"));
4808
+ import_fs5 = __toESM(require("fs"));
4809
+ import_path7 = __toESM(require("path"));
4658
4810
  import_os4 = __toESM(require("os"));
4659
4811
  import_child_process3 = require("child_process");
4660
4812
  import_crypto3 = require("crypto");
4661
4813
  import_chalk4 = __toESM(require("chalk"));
4662
4814
  init_core();
4663
4815
  init_shields();
4664
- 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");
4665
4817
  DAEMON_PORT2 = 7391;
4666
4818
  DAEMON_HOST2 = "127.0.0.1";
4667
4819
  homeDir = import_os4.default.homedir();
4668
- DAEMON_PID_FILE = import_path6.default.join(homeDir, ".node9", "daemon.pid");
4669
- DECISIONS_FILE = import_path6.default.join(homeDir, ".node9", "decisions.json");
4670
- GLOBAL_CONFIG_FILE = import_path6.default.join(homeDir, ".node9", "config.json");
4671
- CREDENTIALS_FILE = import_path6.default.join(homeDir, ".node9", "credentials.json");
4672
- AUDIT_LOG_FILE = import_path6.default.join(homeDir, ".node9", "audit.log");
4673
- 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");
4674
4826
  TRUST_DURATIONS = {
4675
4827
  "30m": 30 * 6e4,
4676
4828
  "1h": 60 * 6e4,
@@ -4731,9 +4883,9 @@ function renderPending(activity) {
4731
4883
  }
4732
4884
  async function ensureDaemon() {
4733
4885
  let pidPort = null;
4734
- if (import_fs6.default.existsSync(PID_FILE)) {
4886
+ if (import_fs7.default.existsSync(PID_FILE)) {
4735
4887
  try {
4736
- 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"));
4737
4889
  pidPort = port;
4738
4890
  } catch {
4739
4891
  console.error(import_chalk5.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
@@ -4888,19 +5040,19 @@ async function startTail(options = {}) {
4888
5040
  process.exit(1);
4889
5041
  });
4890
5042
  }
4891
- 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;
4892
5044
  var init_tail = __esm({
4893
5045
  "src/tui/tail.ts"() {
4894
5046
  "use strict";
4895
5047
  import_http2 = __toESM(require("http"));
4896
5048
  import_chalk5 = __toESM(require("chalk"));
4897
- import_fs6 = __toESM(require("fs"));
5049
+ import_fs7 = __toESM(require("fs"));
4898
5050
  import_os6 = __toESM(require("os"));
4899
- import_path8 = __toESM(require("path"));
5051
+ import_path9 = __toESM(require("path"));
4900
5052
  import_readline = __toESM(require("readline"));
4901
5053
  import_child_process5 = require("child_process");
4902
5054
  init_daemon();
4903
- 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");
4904
5056
  ICONS = {
4905
5057
  bash: "\u{1F4BB}",
4906
5058
  shell: "\u{1F4BB}",
@@ -4926,8 +5078,8 @@ var import_commander = require("commander");
4926
5078
  init_core();
4927
5079
 
4928
5080
  // src/setup.ts
4929
- var import_fs3 = __toESM(require("fs"));
4930
- var import_path5 = __toESM(require("path"));
5081
+ var import_fs4 = __toESM(require("fs"));
5082
+ var import_path6 = __toESM(require("path"));
4931
5083
  var import_os3 = __toESM(require("os"));
4932
5084
  var import_chalk3 = __toESM(require("chalk"));
4933
5085
  var import_prompts2 = require("@inquirer/prompts");
@@ -4945,17 +5097,17 @@ function fullPathCommand(subcommand) {
4945
5097
  }
4946
5098
  function readJson(filePath) {
4947
5099
  try {
4948
- if (import_fs3.default.existsSync(filePath)) {
4949
- 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"));
4950
5102
  }
4951
5103
  } catch {
4952
5104
  }
4953
5105
  return null;
4954
5106
  }
4955
5107
  function writeJson(filePath, data) {
4956
- const dir = import_path5.default.dirname(filePath);
4957
- if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
4958
- 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");
4959
5111
  }
4960
5112
  function isNode9Hook(cmd) {
4961
5113
  if (!cmd) return false;
@@ -4963,8 +5115,8 @@ function isNode9Hook(cmd) {
4963
5115
  }
4964
5116
  function teardownClaude() {
4965
5117
  const homeDir2 = import_os3.default.homedir();
4966
- const hooksPath = import_path5.default.join(homeDir2, ".claude", "settings.json");
4967
- const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
5118
+ const hooksPath = import_path6.default.join(homeDir2, ".claude", "settings.json");
5119
+ const mcpPath = import_path6.default.join(homeDir2, ".claude.json");
4968
5120
  let changed = false;
4969
5121
  const settings = readJson(hooksPath);
4970
5122
  if (settings?.hooks) {
@@ -5013,7 +5165,7 @@ function teardownClaude() {
5013
5165
  }
5014
5166
  function teardownGemini() {
5015
5167
  const homeDir2 = import_os3.default.homedir();
5016
- const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
5168
+ const settingsPath = import_path6.default.join(homeDir2, ".gemini", "settings.json");
5017
5169
  const settings = readJson(settingsPath);
5018
5170
  if (!settings) {
5019
5171
  console.log(import_chalk3.default.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
@@ -5052,7 +5204,7 @@ function teardownGemini() {
5052
5204
  }
5053
5205
  function teardownCursor() {
5054
5206
  const homeDir2 = import_os3.default.homedir();
5055
- const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
5207
+ const mcpPath = import_path6.default.join(homeDir2, ".cursor", "mcp.json");
5056
5208
  const mcpConfig = readJson(mcpPath);
5057
5209
  if (!mcpConfig?.mcpServers) {
5058
5210
  console.log(import_chalk3.default.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
@@ -5079,8 +5231,8 @@ function teardownCursor() {
5079
5231
  }
5080
5232
  async function setupClaude() {
5081
5233
  const homeDir2 = import_os3.default.homedir();
5082
- const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
5083
- 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");
5084
5236
  const claudeConfig = readJson(mcpPath) ?? {};
5085
5237
  const settings = readJson(hooksPath) ?? {};
5086
5238
  const servers = claudeConfig.mcpServers ?? {};
@@ -5155,7 +5307,7 @@ async function setupClaude() {
5155
5307
  }
5156
5308
  async function setupGemini() {
5157
5309
  const homeDir2 = import_os3.default.homedir();
5158
- const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
5310
+ const settingsPath = import_path6.default.join(homeDir2, ".gemini", "settings.json");
5159
5311
  const settings = readJson(settingsPath) ?? {};
5160
5312
  const servers = settings.mcpServers ?? {};
5161
5313
  let anythingChanged = false;
@@ -5238,7 +5390,7 @@ async function setupGemini() {
5238
5390
  }
5239
5391
  async function setupCursor() {
5240
5392
  const homeDir2 = import_os3.default.homedir();
5241
- const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
5393
+ const mcpPath = import_path6.default.join(homeDir2, ".cursor", "mcp.json");
5242
5394
  const mcpConfig = readJson(mcpPath) ?? {};
5243
5395
  const servers = mcpConfig.mcpServers ?? {};
5244
5396
  let anythingChanged = false;
@@ -5299,30 +5451,32 @@ var import_execa = require("execa");
5299
5451
  var import_execa2 = require("execa");
5300
5452
  var import_chalk6 = __toESM(require("chalk"));
5301
5453
  var import_readline2 = __toESM(require("readline"));
5302
- var import_fs7 = __toESM(require("fs"));
5303
- var import_path9 = __toESM(require("path"));
5454
+ var import_fs8 = __toESM(require("fs"));
5455
+ var import_path10 = __toESM(require("path"));
5304
5456
  var import_os7 = __toESM(require("os"));
5305
5457
 
5306
5458
  // src/undo.ts
5307
5459
  var import_child_process4 = require("child_process");
5308
- var import_fs5 = __toESM(require("fs"));
5309
- 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"));
5310
5463
  var import_os5 = __toESM(require("os"));
5311
- var SNAPSHOT_STACK_PATH = import_path7.default.join(import_os5.default.homedir(), ".node9", "snapshots.json");
5312
- 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");
5313
5466
  var MAX_SNAPSHOTS = 10;
5467
+ var GIT_TIMEOUT = 15e3;
5314
5468
  function readStack() {
5315
5469
  try {
5316
- if (import_fs5.default.existsSync(SNAPSHOT_STACK_PATH))
5317
- 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"));
5318
5472
  } catch {
5319
5473
  }
5320
5474
  return [];
5321
5475
  }
5322
5476
  function writeStack(stack) {
5323
- const dir = import_path7.default.dirname(SNAPSHOT_STACK_PATH);
5324
- if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
5325
- 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));
5326
5480
  }
5327
5481
  function buildArgsSummary(tool, args) {
5328
5482
  if (!args || typeof args !== "object") return "";
@@ -5335,54 +5489,177 @@ function buildArgsSummary(tool, args) {
5335
5489
  if (typeof sql === "string") return sql.slice(0, 80);
5336
5490
  return tool;
5337
5491
  }
5338
- 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;
5339
5592
  try {
5340
5593
  const cwd = process.cwd();
5341
- if (!import_fs5.default.existsSync(import_path7.default.join(cwd, ".git"))) return null;
5342
- const tempIndex = import_path7.default.join(cwd, ".git", `node9_index_${Date.now()}`);
5343
- const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
5344
- (0, import_child_process4.spawnSync)("git", ["add", "-A"], { env });
5345
- const treeRes = (0, import_child_process4.spawnSync)("git", ["write-tree"], { env });
5346
- const treeHash = treeRes.stdout.toString().trim();
5347
- 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();
5348
5607
  if (!treeHash || treeRes.status !== 0) return null;
5349
- const commitRes = (0, import_child_process4.spawnSync)("git", [
5350
- "commit-tree",
5351
- treeHash,
5352
- "-m",
5353
- `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`
5354
- ]);
5355
- 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();
5356
5614
  if (!commitHash || commitRes.status !== 0) return null;
5357
5615
  const stack = readStack();
5358
- const entry = {
5616
+ stack.push({
5359
5617
  hash: commitHash,
5360
5618
  tool,
5361
5619
  argsSummary: buildArgsSummary(tool, args),
5362
5620
  cwd,
5363
5621
  timestamp: Date.now()
5364
- };
5365
- stack.push(entry);
5622
+ });
5623
+ const shouldGc = stack.length % 5 === 0;
5366
5624
  if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
5367
5625
  writeStack(stack);
5368
- 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
+ }
5369
5630
  return commitHash;
5370
5631
  } catch (err) {
5371
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
+ }
5372
5641
  }
5373
- return null;
5374
5642
  }
5375
5643
  function getSnapshotHistory() {
5376
5644
  return readStack();
5377
5645
  }
5378
5646
  function computeUndoDiff(hash, cwd) {
5379
5647
  try {
5380
- const result = (0, import_child_process4.spawnSync)("git", ["diff", hash, "--stat", "--", "."], { cwd });
5381
- const stat = result.stdout.toString().trim();
5382
- if (!stat) return null;
5383
- const diff = (0, import_child_process4.spawnSync)("git", ["diff", hash, "--", "."], { cwd });
5384
- const raw = diff.stdout.toString();
5385
- 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;
5386
5663
  const lines = raw.split("\n").filter(
5387
5664
  (l) => !l.startsWith("diff --git") && !l.startsWith("index ") && !l.startsWith("Binary")
5388
5665
  );
@@ -5394,18 +5671,31 @@ function computeUndoDiff(hash, cwd) {
5394
5671
  function applyUndo(hash, cwd) {
5395
5672
  try {
5396
5673
  const dir = cwd ?? process.cwd();
5674
+ const env = buildGitEnv(dir);
5397
5675
  const restore = (0, import_child_process4.spawnSync)("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
5398
- cwd: dir
5676
+ cwd: dir,
5677
+ env,
5678
+ timeout: GIT_TIMEOUT
5399
5679
  });
5400
5680
  if (restore.status !== 0) return false;
5401
- const lsTree = (0, import_child_process4.spawnSync)("git", ["ls-tree", "-r", "--name-only", hash], { cwd: dir });
5402
- const snapshotFiles = new Set(lsTree.stdout.toString().trim().split("\n").filter(Boolean));
5403
- const tracked = (0, import_child_process4.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5404
- 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) ?? [];
5405
5695
  for (const file of [...tracked, ...untracked]) {
5406
- const fullPath = import_path7.default.join(dir, file);
5407
- if (!snapshotFiles.has(file) && import_fs5.default.existsSync(fullPath)) {
5408
- 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);
5409
5699
  }
5410
5700
  }
5411
5701
  return true;
@@ -5418,7 +5708,7 @@ function applyUndo(hash, cwd) {
5418
5708
  init_shields();
5419
5709
  var import_prompts3 = require("@inquirer/prompts");
5420
5710
  var { version } = JSON.parse(
5421
- 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")
5422
5712
  );
5423
5713
  function parseDuration(str) {
5424
5714
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -5620,14 +5910,14 @@ async function runProxy(targetCommand) {
5620
5910
  }
5621
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) => {
5622
5912
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
5623
- const credPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "credentials.json");
5624
- if (!import_fs7.default.existsSync(import_path9.default.dirname(credPath)))
5625
- 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 });
5626
5916
  const profileName = options.profile || "default";
5627
5917
  let existingCreds = {};
5628
5918
  try {
5629
- if (import_fs7.default.existsSync(credPath)) {
5630
- 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"));
5631
5921
  if (raw.apiKey) {
5632
5922
  existingCreds = {
5633
5923
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -5639,13 +5929,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
5639
5929
  } catch {
5640
5930
  }
5641
5931
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
5642
- 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 });
5643
5933
  if (profileName === "default") {
5644
- 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");
5645
5935
  let config = {};
5646
5936
  try {
5647
- if (import_fs7.default.existsSync(configPath))
5648
- 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"));
5649
5939
  } catch {
5650
5940
  }
5651
5941
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -5660,9 +5950,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
5660
5950
  approvers.cloud = false;
5661
5951
  }
5662
5952
  s.approvers = approvers;
5663
- if (!import_fs7.default.existsSync(import_path9.default.dirname(configPath)))
5664
- import_fs7.default.mkdirSync(import_path9.default.dirname(configPath), { recursive: true });
5665
- 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 });
5666
5956
  }
5667
5957
  if (options.profile && profileName !== "default") {
5668
5958
  console.log(import_chalk6.default.green(`\u2705 Profile "${profileName}" saved`));
@@ -5748,15 +6038,15 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
5748
6038
  }
5749
6039
  }
5750
6040
  if (options.purge) {
5751
- const node9Dir = import_path9.default.join(import_os7.default.homedir(), ".node9");
5752
- 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)) {
5753
6043
  const confirmed = await (0, import_prompts3.confirm)({
5754
6044
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
5755
6045
  default: false
5756
6046
  });
5757
6047
  if (confirmed) {
5758
- import_fs7.default.rmSync(node9Dir, { recursive: true });
5759
- if (import_fs7.default.existsSync(node9Dir)) {
6048
+ import_fs8.default.rmSync(node9Dir, { recursive: true });
6049
+ if (import_fs8.default.existsSync(node9Dir)) {
5760
6050
  console.error(
5761
6051
  import_chalk6.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
5762
6052
  );
@@ -5828,10 +6118,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5828
6118
  );
5829
6119
  }
5830
6120
  section("Configuration");
5831
- const globalConfigPath = import_path9.default.join(homeDir2, ".node9", "config.json");
5832
- if (import_fs7.default.existsSync(globalConfigPath)) {
6121
+ const globalConfigPath = import_path10.default.join(homeDir2, ".node9", "config.json");
6122
+ if (import_fs8.default.existsSync(globalConfigPath)) {
5833
6123
  try {
5834
- JSON.parse(import_fs7.default.readFileSync(globalConfigPath, "utf-8"));
6124
+ JSON.parse(import_fs8.default.readFileSync(globalConfigPath, "utf-8"));
5835
6125
  pass("~/.node9/config.json found and valid");
5836
6126
  } catch {
5837
6127
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -5839,17 +6129,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
5839
6129
  } else {
5840
6130
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
5841
6131
  }
5842
- const projectConfigPath = import_path9.default.join(process.cwd(), "node9.config.json");
5843
- 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)) {
5844
6134
  try {
5845
- JSON.parse(import_fs7.default.readFileSync(projectConfigPath, "utf-8"));
6135
+ JSON.parse(import_fs8.default.readFileSync(projectConfigPath, "utf-8"));
5846
6136
  pass("node9.config.json found and valid (project)");
5847
6137
  } catch {
5848
6138
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
5849
6139
  }
5850
6140
  }
5851
- const credsPath = import_path9.default.join(homeDir2, ".node9", "credentials.json");
5852
- if (import_fs7.default.existsSync(credsPath)) {
6141
+ const credsPath = import_path10.default.join(homeDir2, ".node9", "credentials.json");
6142
+ if (import_fs8.default.existsSync(credsPath)) {
5853
6143
  pass("Cloud credentials found (~/.node9/credentials.json)");
5854
6144
  } else {
5855
6145
  warn(
@@ -5858,10 +6148,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5858
6148
  );
5859
6149
  }
5860
6150
  section("Agent Hooks");
5861
- const claudeSettingsPath = import_path9.default.join(homeDir2, ".claude", "settings.json");
5862
- if (import_fs7.default.existsSync(claudeSettingsPath)) {
6151
+ const claudeSettingsPath = import_path10.default.join(homeDir2, ".claude", "settings.json");
6152
+ if (import_fs8.default.existsSync(claudeSettingsPath)) {
5863
6153
  try {
5864
- const cs = JSON.parse(import_fs7.default.readFileSync(claudeSettingsPath, "utf-8"));
6154
+ const cs = JSON.parse(import_fs8.default.readFileSync(claudeSettingsPath, "utf-8"));
5865
6155
  const hasHook = cs.hooks?.PreToolUse?.some(
5866
6156
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
5867
6157
  );
@@ -5874,10 +6164,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5874
6164
  } else {
5875
6165
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
5876
6166
  }
5877
- const geminiSettingsPath = import_path9.default.join(homeDir2, ".gemini", "settings.json");
5878
- if (import_fs7.default.existsSync(geminiSettingsPath)) {
6167
+ const geminiSettingsPath = import_path10.default.join(homeDir2, ".gemini", "settings.json");
6168
+ if (import_fs8.default.existsSync(geminiSettingsPath)) {
5879
6169
  try {
5880
- const gs = JSON.parse(import_fs7.default.readFileSync(geminiSettingsPath, "utf-8"));
6170
+ const gs = JSON.parse(import_fs8.default.readFileSync(geminiSettingsPath, "utf-8"));
5881
6171
  const hasHook = gs.hooks?.BeforeTool?.some(
5882
6172
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
5883
6173
  );
@@ -5890,10 +6180,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5890
6180
  } else {
5891
6181
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
5892
6182
  }
5893
- const cursorHooksPath = import_path9.default.join(homeDir2, ".cursor", "hooks.json");
5894
- if (import_fs7.default.existsSync(cursorHooksPath)) {
6183
+ const cursorHooksPath = import_path10.default.join(homeDir2, ".cursor", "hooks.json");
6184
+ if (import_fs8.default.existsSync(cursorHooksPath)) {
5895
6185
  try {
5896
- const cur = JSON.parse(import_fs7.default.readFileSync(cursorHooksPath, "utf-8"));
6186
+ const cur = JSON.parse(import_fs8.default.readFileSync(cursorHooksPath, "utf-8"));
5897
6187
  const hasHook = cur.hooks?.preToolUse?.some(
5898
6188
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
5899
6189
  );
@@ -5995,8 +6285,8 @@ program.command("explain").description(
5995
6285
  console.log("");
5996
6286
  });
5997
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) => {
5998
- const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
5999
- 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) {
6000
6290
  console.log(import_chalk6.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
6001
6291
  console.log(import_chalk6.default.gray(` Run with --force to overwrite.`));
6002
6292
  return;
@@ -6010,9 +6300,9 @@ program.command("init").description("Create ~/.node9/config.json with default po
6010
6300
  mode: safeMode
6011
6301
  }
6012
6302
  };
6013
- const dir = import_path9.default.dirname(configPath);
6014
- if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
6015
- 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));
6016
6306
  console.log(import_chalk6.default.green(`\u2705 Global config created: ${configPath}`));
6017
6307
  console.log(import_chalk6.default.cyan(` Mode set to: ${safeMode}`));
6018
6308
  console.log(
@@ -6030,14 +6320,14 @@ function formatRelativeTime(timestamp) {
6030
6320
  return new Date(timestamp).toLocaleDateString();
6031
6321
  }
6032
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) => {
6033
- const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
6034
- 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)) {
6035
6325
  console.log(
6036
6326
  import_chalk6.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
6037
6327
  );
6038
6328
  return;
6039
6329
  }
6040
- const raw = import_fs7.default.readFileSync(logPath, "utf-8");
6330
+ const raw = import_fs8.default.readFileSync(logPath, "utf-8");
6041
6331
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
6042
6332
  let entries = lines.flatMap((line) => {
6043
6333
  try {
@@ -6120,13 +6410,13 @@ program.command("status").description("Show current Node9 mode, policy source, a
6120
6410
  console.log("");
6121
6411
  const modeLabel = settings.mode === "audit" ? import_chalk6.default.blue("audit") : settings.mode === "strict" ? import_chalk6.default.red("strict") : import_chalk6.default.white("standard");
6122
6412
  console.log(` Mode: ${modeLabel}`);
6123
- const projectConfig = import_path9.default.join(process.cwd(), "node9.config.json");
6124
- 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");
6125
6415
  console.log(
6126
- ` 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")}`
6127
6417
  );
6128
6418
  console.log(
6129
- ` 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")}`
6130
6420
  );
6131
6421
  if (mergedConfig.policy.sandboxPaths.length > 0) {
6132
6422
  console.log(
@@ -6210,9 +6500,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
6210
6500
  } catch (err) {
6211
6501
  const tempConfig = getConfig();
6212
6502
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
6213
- 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");
6214
6504
  const errMsg = err instanceof Error ? err.message : String(err);
6215
- import_fs7.default.appendFileSync(
6505
+ import_fs8.default.appendFileSync(
6216
6506
  logPath,
6217
6507
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
6218
6508
  RAW: ${raw}
@@ -6230,10 +6520,10 @@ RAW: ${raw}
6230
6520
  }
6231
6521
  const config = getConfig();
6232
6522
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
6233
- const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6234
- if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
6235
- import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
6236
- 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}
6237
6527
  `);
6238
6528
  }
6239
6529
  const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
@@ -6277,7 +6567,7 @@ RAW: ${raw}
6277
6567
  }
6278
6568
  const meta = { agent, mcpServer };
6279
6569
  if (shouldSnapshot(toolName, toolInput, config)) {
6280
- await createShadowSnapshot(toolName, toolInput);
6570
+ await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
6281
6571
  }
6282
6572
  const result = await authorizeHeadless(toolName, toolInput, false, meta);
6283
6573
  if (result.approved) {
@@ -6310,9 +6600,9 @@ RAW: ${raw}
6310
6600
  });
6311
6601
  } catch (err) {
6312
6602
  if (process.env.NODE9_DEBUG === "1") {
6313
- 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");
6314
6604
  const errMsg = err instanceof Error ? err.message : String(err);
6315
- import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
6605
+ import_fs8.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
6316
6606
  `);
6317
6607
  }
6318
6608
  process.exit(0);
@@ -6357,13 +6647,13 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
6357
6647
  decision: "allowed",
6358
6648
  source: "post-hook"
6359
6649
  };
6360
- const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
6361
- if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
6362
- import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
6363
- 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");
6364
6654
  const config = getConfig();
6365
6655
  if (shouldSnapshot(tool, {}, config)) {
6366
- await createShadowSnapshot();
6656
+ await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
6367
6657
  }
6368
6658
  } catch {
6369
6659
  }
@@ -6636,9 +6926,9 @@ process.on("unhandledRejection", (reason) => {
6636
6926
  const isCheckHook = process.argv[2] === "check";
6637
6927
  if (isCheckHook) {
6638
6928
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
6639
- 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");
6640
6930
  const msg = reason instanceof Error ? reason.message : String(reason);
6641
- import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
6931
+ import_fs8.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
6642
6932
  `);
6643
6933
  }
6644
6934
  process.exit(0);