@node9/proxy 1.0.19 → 1.1.1

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,14 +781,18 @@ 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" },
758
- { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
793
+ // Slack bot tokens: xoxb- + variable segment. Real tokens are ~50–80 chars;
794
+ // lower bound 20 avoids false negatives on partial tokens, upper 100 caps scan cost.
795
+ { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/, severity: "block" },
759
796
  { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
760
797
  { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
761
798
  {
@@ -763,8 +800,43 @@ var init_dlp = __esm({
763
800
  regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
764
801
  severity: "block"
765
802
  },
803
+ // GCP service account JSON (detects the type field that uniquely identifies it)
804
+ {
805
+ name: "GCP Service Account",
806
+ regex: /"type"\s*:\s*"service_account"/,
807
+ severity: "block"
808
+ },
809
+ // NPM auth token in .npmrc format
810
+ {
811
+ name: "NPM Auth Token",
812
+ regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
813
+ severity: "block"
814
+ },
766
815
  { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
767
816
  ];
817
+ SENSITIVE_PATH_PATTERNS = [
818
+ /[/\\]\.ssh[/\\]/i,
819
+ /[/\\]\.aws[/\\]/i,
820
+ /[/\\]\.config[/\\]gcloud[/\\]/i,
821
+ /[/\\]\.azure[/\\]/i,
822
+ /[/\\]\.kube[/\\]config$/i,
823
+ /[/\\]\.env($|\.)/i,
824
+ // .env, .env.local, .env.production — not .envoy
825
+ /[/\\]\.git-credentials$/i,
826
+ /[/\\]\.npmrc$/i,
827
+ /[/\\]\.docker[/\\]config\.json$/i,
828
+ /[/\\][^/\\]+\.pem$/i,
829
+ /[/\\][^/\\]+\.key$/i,
830
+ /[/\\][^/\\]+\.p12$/i,
831
+ /[/\\][^/\\]+\.pfx$/i,
832
+ /^\/etc\/passwd$/,
833
+ /^\/etc\/shadow$/,
834
+ /^\/etc\/sudoers$/,
835
+ /[/\\]credentials\.json$/i,
836
+ /[/\\]id_rsa$/i,
837
+ /[/\\]id_ed25519$/i,
838
+ /[/\\]id_ecdsa$/i
839
+ ];
768
840
  MAX_DEPTH = 5;
769
841
  MAX_STRING_BYTES = 1e5;
770
842
  MAX_JSON_PARSE_BYTES = 1e4;
@@ -774,11 +846,11 @@ var init_dlp = __esm({
774
846
  // src/core.ts
775
847
  function checkPause() {
776
848
  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"));
849
+ if (!import_fs3.default.existsSync(PAUSED_FILE)) return { paused: false };
850
+ const state = JSON.parse(import_fs3.default.readFileSync(PAUSED_FILE, "utf-8"));
779
851
  if (state.expiry > 0 && Date.now() >= state.expiry) {
780
852
  try {
781
- import_fs2.default.unlinkSync(PAUSED_FILE);
853
+ import_fs3.default.unlinkSync(PAUSED_FILE);
782
854
  } catch {
783
855
  }
784
856
  return { paused: false };
@@ -789,11 +861,11 @@ function checkPause() {
789
861
  }
790
862
  }
791
863
  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 });
864
+ const dir = import_path5.default.dirname(filePath);
865
+ if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
794
866
  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);
867
+ import_fs3.default.writeFileSync(tmpPath, data, options);
868
+ import_fs3.default.renameSync(tmpPath, filePath);
797
869
  }
798
870
  function pauseNode9(durationMs, durationStr) {
799
871
  const state = { expiry: Date.now() + durationMs, duration: durationStr };
@@ -801,18 +873,61 @@ function pauseNode9(durationMs, durationStr) {
801
873
  }
802
874
  function resumeNode9() {
803
875
  try {
804
- if (import_fs2.default.existsSync(PAUSED_FILE)) import_fs2.default.unlinkSync(PAUSED_FILE);
876
+ if (import_fs3.default.existsSync(PAUSED_FILE)) import_fs3.default.unlinkSync(PAUSED_FILE);
805
877
  } catch {
806
878
  }
807
879
  }
880
+ function validateRegex(pattern) {
881
+ if (!pattern) return "Pattern is required";
882
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
883
+ try {
884
+ new RegExp(pattern);
885
+ } catch (e) {
886
+ return `Invalid regex syntax: ${e.message}`;
887
+ }
888
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
889
+ if (!(0, import_safe_regex2.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
890
+ return null;
891
+ }
892
+ function getCompiledRegex(pattern, flags = "") {
893
+ if (flags && !/^[gimsuy]+$/.test(flags)) {
894
+ if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Invalid regex flags: "${flags}"`);
895
+ return null;
896
+ }
897
+ const key = `${pattern}\0${flags}`;
898
+ if (regexCache.has(key)) {
899
+ const cached = regexCache.get(key);
900
+ regexCache.delete(key);
901
+ regexCache.set(key, cached);
902
+ return cached;
903
+ }
904
+ const err = validateRegex(pattern);
905
+ if (err) {
906
+ if (process.env.NODE9_DEBUG === "1")
907
+ console.error(`[Node9] Regex blocked: ${err} \u2014 pattern: "${pattern}"`);
908
+ return null;
909
+ }
910
+ try {
911
+ const re = new RegExp(pattern, flags);
912
+ if (regexCache.size >= REGEX_CACHE_MAX) {
913
+ const oldest = regexCache.keys().next().value;
914
+ if (oldest) regexCache.delete(oldest);
915
+ }
916
+ regexCache.set(key, re);
917
+ return re;
918
+ } catch (e) {
919
+ if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Regex compile failed:`, e);
920
+ return null;
921
+ }
922
+ }
808
923
  function getActiveTrustSession(toolName) {
809
924
  try {
810
- if (!import_fs2.default.existsSync(TRUST_FILE)) return false;
811
- const trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
925
+ if (!import_fs3.default.existsSync(TRUST_FILE)) return false;
926
+ const trust = JSON.parse(import_fs3.default.readFileSync(TRUST_FILE, "utf-8"));
812
927
  const now = Date.now();
813
928
  const active = trust.entries.filter((e) => e.expiry > now);
814
929
  if (active.length !== trust.entries.length) {
815
- import_fs2.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
930
+ import_fs3.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
816
931
  }
817
932
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
818
933
  } catch {
@@ -823,8 +938,8 @@ function writeTrustSession(toolName, durationMs) {
823
938
  try {
824
939
  let trust = { entries: [] };
825
940
  try {
826
- if (import_fs2.default.existsSync(TRUST_FILE)) {
827
- trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
941
+ if (import_fs3.default.existsSync(TRUST_FILE)) {
942
+ trust = JSON.parse(import_fs3.default.readFileSync(TRUST_FILE, "utf-8"));
828
943
  }
829
944
  } catch {
830
945
  }
@@ -840,9 +955,9 @@ function writeTrustSession(toolName, durationMs) {
840
955
  }
841
956
  function appendToLog(logPath, entry) {
842
957
  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");
958
+ const dir = import_path5.default.dirname(logPath);
959
+ if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
960
+ import_fs3.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
846
961
  } catch {
847
962
  }
848
963
  }
@@ -884,9 +999,9 @@ function matchesPattern(text, patterns) {
884
999
  const withoutDotSlash = text.replace(/^\.\//, "");
885
1000
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
886
1001
  }
887
- function getNestedValue(obj, path10) {
1002
+ function getNestedValue(obj, path11) {
888
1003
  if (!obj || typeof obj !== "object") return null;
889
- return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
1004
+ return path11.split(".").reduce((prev, curr) => prev?.[curr], obj);
890
1005
  }
891
1006
  function shouldSnapshot(toolName, args, config) {
892
1007
  if (!config.settings.enableUndo) return false;
@@ -917,19 +1032,16 @@ function evaluateSmartConditions(args, rule) {
917
1032
  return val !== null && cond.value ? !val.includes(cond.value) : true;
918
1033
  case "matches": {
919
1034
  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
- }
1035
+ const reM = getCompiledRegex(cond.value, cond.flags ?? "");
1036
+ if (!reM) return false;
1037
+ return reM.test(val);
925
1038
  }
926
1039
  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
- }
1040
+ if (!cond.value) return false;
1041
+ if (val === null) return true;
1042
+ const reN = getCompiledRegex(cond.value, cond.flags ?? "");
1043
+ if (!reN) return false;
1044
+ return !reN.test(val);
933
1045
  }
934
1046
  case "matchesGlob":
935
1047
  return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
@@ -1042,9 +1154,9 @@ function _resetConfigCache() {
1042
1154
  }
1043
1155
  function getGlobalSettings() {
1044
1156
  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"));
1157
+ const globalConfigPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "config.json");
1158
+ if (import_fs3.default.existsSync(globalConfigPath)) {
1159
+ const parsed = JSON.parse(import_fs3.default.readFileSync(globalConfigPath, "utf-8"));
1048
1160
  const settings = parsed.settings || {};
1049
1161
  return {
1050
1162
  mode: settings.mode || "audit",
@@ -1066,9 +1178,9 @@ function getGlobalSettings() {
1066
1178
  }
1067
1179
  function getInternalToken() {
1068
1180
  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"));
1181
+ const pidFile = import_path5.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1182
+ if (!import_fs3.default.existsSync(pidFile)) return null;
1183
+ const data = JSON.parse(import_fs3.default.readFileSync(pidFile, "utf-8"));
1072
1184
  process.kill(data.pid, 0);
1073
1185
  return data.internalToken ?? null;
1074
1186
  } catch {
@@ -1183,9 +1295,9 @@ async function evaluatePolicy(toolName, args, agent) {
1183
1295
  }
1184
1296
  async function explainPolicy(toolName, args) {
1185
1297
  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");
1298
+ const globalPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "config.json");
1299
+ const projectPath = import_path5.default.join(process.cwd(), "node9.config.json");
1300
+ const credsPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
1189
1301
  const waterfall = [
1190
1302
  {
1191
1303
  tier: 1,
@@ -1196,19 +1308,19 @@ async function explainPolicy(toolName, args) {
1196
1308
  {
1197
1309
  tier: 2,
1198
1310
  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"
1311
+ status: import_fs3.default.existsSync(credsPath) ? "active" : "missing",
1312
+ note: import_fs3.default.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1201
1313
  },
1202
1314
  {
1203
1315
  tier: 3,
1204
1316
  label: "Project config",
1205
- status: import_fs2.default.existsSync(projectPath) ? "active" : "missing",
1317
+ status: import_fs3.default.existsSync(projectPath) ? "active" : "missing",
1206
1318
  path: projectPath
1207
1319
  },
1208
1320
  {
1209
1321
  tier: 4,
1210
1322
  label: "Global config",
1211
- status: import_fs2.default.existsSync(globalPath) ? "active" : "missing",
1323
+ status: import_fs3.default.existsSync(globalPath) ? "active" : "missing",
1212
1324
  path: globalPath
1213
1325
  },
1214
1326
  {
@@ -1221,7 +1333,9 @@ async function explainPolicy(toolName, args) {
1221
1333
  const config = getConfig();
1222
1334
  const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
1223
1335
  if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
1224
- const dlpMatch = args !== void 0 ? scanArgs(args) : null;
1336
+ const argsObjE = args && typeof args === "object" && !Array.isArray(args) ? args : {};
1337
+ const filePathE = String(argsObjE.file_path ?? argsObjE.path ?? argsObjE.filename ?? "");
1338
+ const dlpMatch = (filePathE ? scanFilePath(filePathE) : null) ?? (args !== void 0 ? scanArgs(args) : null);
1225
1339
  if (dlpMatch) {
1226
1340
  steps.push({
1227
1341
  name: "DLP Content Scanner",
@@ -1444,10 +1558,10 @@ function isIgnoredTool(toolName) {
1444
1558
  return matchesPattern(toolName, config.policy.ignoredTools);
1445
1559
  }
1446
1560
  function isDaemonRunning() {
1447
- const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1448
- if (import_fs2.default.existsSync(pidFile)) {
1561
+ const pidFile = import_path5.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1562
+ if (import_fs3.default.existsSync(pidFile)) {
1449
1563
  try {
1450
- const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1564
+ const { pid, port } = JSON.parse(import_fs3.default.readFileSync(pidFile, "utf-8"));
1451
1565
  if (port !== DAEMON_PORT) return false;
1452
1566
  process.kill(pid, 0);
1453
1567
  return true;
@@ -1467,9 +1581,9 @@ function isDaemonRunning() {
1467
1581
  }
1468
1582
  function getPersistentDecision(toolName) {
1469
1583
  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"));
1584
+ const file = import_path5.default.join(import_os2.default.homedir(), ".node9", "decisions.json");
1585
+ if (!import_fs3.default.existsSync(file)) return null;
1586
+ const decisions = JSON.parse(import_fs3.default.readFileSync(file, "utf-8"));
1473
1587
  const d = decisions[toolName];
1474
1588
  if (d === "allow" || d === "deny") return d;
1475
1589
  } catch {
@@ -1612,7 +1726,9 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1612
1726
  let policyMatchedWord;
1613
1727
  let riskMetadata;
1614
1728
  if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
1615
- const dlpMatch = scanArgs(args);
1729
+ const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
1730
+ const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
1731
+ const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
1616
1732
  if (dlpMatch) {
1617
1733
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
1618
1734
  if (dlpMatch.severity === "block") {
@@ -1984,10 +2100,10 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1984
2100
  }
1985
2101
  return finalResult;
1986
2102
  }
1987
- function getConfig() {
1988
- 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");
2103
+ function getConfig(cwd) {
2104
+ if (!cwd && cachedConfig) return cachedConfig;
2105
+ const globalPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "config.json");
2106
+ const projectPath = import_path5.default.join(cwd ?? process.cwd(), "node9.config.json");
1991
2107
  const globalConfig = tryLoadConfig(globalPath);
1992
2108
  const projectConfig = tryLoadConfig(projectPath);
1993
2109
  const mergedSettings = {
@@ -2074,18 +2190,19 @@ function getConfig() {
2074
2190
  mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
2075
2191
  mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
2076
2192
  mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
2077
- cachedConfig = {
2193
+ const result = {
2078
2194
  settings: mergedSettings,
2079
2195
  policy: mergedPolicy,
2080
2196
  environments: mergedEnvironments
2081
2197
  };
2082
- return cachedConfig;
2198
+ if (!cwd) cachedConfig = result;
2199
+ return result;
2083
2200
  }
2084
2201
  function tryLoadConfig(filePath) {
2085
- if (!import_fs2.default.existsSync(filePath)) return null;
2202
+ if (!import_fs3.default.existsSync(filePath)) return null;
2086
2203
  let raw;
2087
2204
  try {
2088
- raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
2205
+ raw = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
2089
2206
  } catch (err) {
2090
2207
  const msg = err instanceof Error ? err.message : String(err);
2091
2208
  process.stderr.write(
@@ -2147,9 +2264,9 @@ function getCredentials() {
2147
2264
  };
2148
2265
  }
2149
2266
  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"));
2267
+ const credPath = import_path5.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
2268
+ if (import_fs3.default.existsSync(credPath)) {
2269
+ const creds = JSON.parse(import_fs3.default.readFileSync(credPath, "utf-8"));
2153
2270
  const profileName = process.env.NODE9_PROFILE || "default";
2154
2271
  const profile = creds[profileName];
2155
2272
  if (profile?.apiKey) {
@@ -2262,29 +2379,33 @@ async function resolveNode9SaaS(requestId, creds, approved) {
2262
2379
  } catch {
2263
2380
  }
2264
2381
  }
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;
2382
+ 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
2383
  var init_core = __esm({
2267
2384
  "src/core.ts"() {
2268
2385
  "use strict";
2269
2386
  import_chalk2 = __toESM(require("chalk"));
2270
2387
  import_prompts = require("@inquirer/prompts");
2271
- import_fs2 = __toESM(require("fs"));
2272
- import_path4 = __toESM(require("path"));
2388
+ import_fs3 = __toESM(require("fs"));
2389
+ import_path5 = __toESM(require("path"));
2273
2390
  import_os2 = __toESM(require("os"));
2274
2391
  import_net = __toESM(require("net"));
2275
2392
  import_crypto2 = require("crypto");
2276
2393
  import_child_process2 = require("child_process");
2277
2394
  import_picomatch = __toESM(require("picomatch"));
2395
+ import_safe_regex2 = __toESM(require("safe-regex2"));
2278
2396
  import_sh_syntax = require("sh-syntax");
2279
2397
  init_native();
2280
2398
  init_context_sniper();
2281
2399
  init_config_schema();
2282
2400
  init_shields();
2283
2401
  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");
2402
+ PAUSED_FILE = import_path5.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
2403
+ TRUST_FILE = import_path5.default.join(import_os2.default.homedir(), ".node9", "trust.json");
2404
+ LOCAL_AUDIT_LOG = import_path5.default.join(import_os2.default.homedir(), ".node9", "audit.log");
2405
+ HOOK_DEBUG_LOG = import_path5.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
2406
+ MAX_REGEX_LENGTH = 100;
2407
+ REGEX_CACHE_MAX = 500;
2408
+ regexCache = /* @__PURE__ */ new Map();
2288
2409
  SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2289
2410
  DANGEROUS_WORDS = [
2290
2411
  "mkfs",
@@ -2499,7 +2620,7 @@ var init_core = __esm({
2499
2620
  cachedConfig = null;
2500
2621
  DAEMON_PORT = 7391;
2501
2622
  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");
2623
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path5.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
2503
2624
  }
2504
2625
  });
2505
2626
 
@@ -3957,18 +4078,18 @@ var init_ui2 = __esm({
3957
4078
 
3958
4079
  // src/daemon/index.ts
3959
4080
  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 });
4081
+ const dir = import_path7.default.dirname(filePath);
4082
+ if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
3962
4083
  const tmpPath = `${filePath}.${(0, import_crypto3.randomUUID)()}.tmp`;
3963
- import_fs4.default.writeFileSync(tmpPath, data, options);
3964
- import_fs4.default.renameSync(tmpPath, filePath);
4084
+ import_fs5.default.writeFileSync(tmpPath, data, options);
4085
+ import_fs5.default.renameSync(tmpPath, filePath);
3965
4086
  }
3966
4087
  function writeTrustEntry(toolName, durationMs) {
3967
4088
  try {
3968
4089
  let trust = { entries: [] };
3969
4090
  try {
3970
- if (import_fs4.default.existsSync(TRUST_FILE2))
3971
- trust = JSON.parse(import_fs4.default.readFileSync(TRUST_FILE2, "utf-8"));
4091
+ if (import_fs5.default.existsSync(TRUST_FILE2))
4092
+ trust = JSON.parse(import_fs5.default.readFileSync(TRUST_FILE2, "utf-8"));
3972
4093
  } catch {
3973
4094
  }
3974
4095
  trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
@@ -3995,16 +4116,16 @@ function appendAuditLog(data) {
3995
4116
  decision: data.decision,
3996
4117
  source: "daemon"
3997
4118
  };
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");
4119
+ const dir = import_path7.default.dirname(AUDIT_LOG_FILE);
4120
+ if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
4121
+ import_fs5.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
4001
4122
  } catch {
4002
4123
  }
4003
4124
  }
4004
4125
  function getAuditHistory(limit = 20) {
4005
4126
  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");
4127
+ if (!import_fs5.default.existsSync(AUDIT_LOG_FILE)) return [];
4128
+ const lines = import_fs5.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
4008
4129
  if (lines.length === 1 && lines[0] === "") return [];
4009
4130
  return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
4010
4131
  } catch {
@@ -4013,7 +4134,7 @@ function getAuditHistory(limit = 20) {
4013
4134
  }
4014
4135
  function getOrgName() {
4015
4136
  try {
4016
- if (import_fs4.default.existsSync(CREDENTIALS_FILE)) {
4137
+ if (import_fs5.default.existsSync(CREDENTIALS_FILE)) {
4017
4138
  return "Node9 Cloud";
4018
4139
  }
4019
4140
  } catch {
@@ -4021,13 +4142,13 @@ function getOrgName() {
4021
4142
  return null;
4022
4143
  }
4023
4144
  function hasStoredSlackKey() {
4024
- return import_fs4.default.existsSync(CREDENTIALS_FILE);
4145
+ return import_fs5.default.existsSync(CREDENTIALS_FILE);
4025
4146
  }
4026
4147
  function writeGlobalSetting(key, value) {
4027
4148
  let config = {};
4028
4149
  try {
4029
- if (import_fs4.default.existsSync(GLOBAL_CONFIG_FILE)) {
4030
- config = JSON.parse(import_fs4.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
4150
+ if (import_fs5.default.existsSync(GLOBAL_CONFIG_FILE)) {
4151
+ config = JSON.parse(import_fs5.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
4031
4152
  }
4032
4153
  } catch {
4033
4154
  }
@@ -4046,7 +4167,7 @@ function abandonPending() {
4046
4167
  });
4047
4168
  if (autoStarted) {
4048
4169
  try {
4049
- import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4170
+ import_fs5.default.unlinkSync(DAEMON_PID_FILE);
4050
4171
  } catch {
4051
4172
  }
4052
4173
  setTimeout(() => {
@@ -4096,8 +4217,8 @@ function readBody(req) {
4096
4217
  }
4097
4218
  function readPersistentDecisions() {
4098
4219
  try {
4099
- if (import_fs4.default.existsSync(DECISIONS_FILE)) {
4100
- return JSON.parse(import_fs4.default.readFileSync(DECISIONS_FILE, "utf-8"));
4220
+ if (import_fs5.default.existsSync(DECISIONS_FILE)) {
4221
+ return JSON.parse(import_fs5.default.readFileSync(DECISIONS_FILE, "utf-8"));
4101
4222
  }
4102
4223
  } catch {
4103
4224
  }
@@ -4126,7 +4247,7 @@ function startDaemon() {
4126
4247
  idleTimer = setTimeout(() => {
4127
4248
  if (autoStarted) {
4128
4249
  try {
4129
- import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4250
+ import_fs5.default.unlinkSync(DAEMON_PID_FILE);
4130
4251
  } catch {
4131
4252
  }
4132
4253
  }
@@ -4511,14 +4632,14 @@ data: ${JSON.stringify(item.data)}
4511
4632
  server.on("error", (e) => {
4512
4633
  if (e.code === "EADDRINUSE") {
4513
4634
  try {
4514
- if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
4515
- const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4635
+ if (import_fs5.default.existsSync(DAEMON_PID_FILE)) {
4636
+ const { pid } = JSON.parse(import_fs5.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4516
4637
  process.kill(pid, 0);
4517
4638
  return process.exit(0);
4518
4639
  }
4519
4640
  } catch {
4520
4641
  try {
4521
- import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4642
+ import_fs5.default.unlinkSync(DAEMON_PID_FILE);
4522
4643
  } catch {
4523
4644
  }
4524
4645
  server.listen(DAEMON_PORT2, DAEMON_HOST2);
@@ -4569,7 +4690,7 @@ data: ${JSON.stringify(item.data)}
4569
4690
  console.log(import_chalk4.default.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
4570
4691
  }
4571
4692
  try {
4572
- import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4693
+ import_fs5.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4573
4694
  } catch {
4574
4695
  }
4575
4696
  const ACTIVITY_MAX_BYTES = 1024 * 1024;
@@ -4611,30 +4732,30 @@ data: ${JSON.stringify(item.data)}
4611
4732
  unixServer.listen(ACTIVITY_SOCKET_PATH2);
4612
4733
  process.on("exit", () => {
4613
4734
  try {
4614
- import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4735
+ import_fs5.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4615
4736
  } catch {
4616
4737
  }
4617
4738
  });
4618
4739
  }
4619
4740
  function stopDaemon() {
4620
- if (!import_fs4.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
4741
+ if (!import_fs5.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
4621
4742
  try {
4622
- const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4743
+ const { pid } = JSON.parse(import_fs5.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4623
4744
  process.kill(pid, "SIGTERM");
4624
4745
  console.log(import_chalk4.default.green("\u2705 Stopped."));
4625
4746
  } catch {
4626
4747
  console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
4627
4748
  } finally {
4628
4749
  try {
4629
- import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4750
+ import_fs5.default.unlinkSync(DAEMON_PID_FILE);
4630
4751
  } catch {
4631
4752
  }
4632
4753
  }
4633
4754
  }
4634
4755
  function daemonStatus() {
4635
- if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
4756
+ if (import_fs5.default.existsSync(DAEMON_PID_FILE)) {
4636
4757
  try {
4637
- const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4758
+ const { pid } = JSON.parse(import_fs5.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4638
4759
  process.kill(pid, 0);
4639
4760
  console.log(import_chalk4.default.green("Node9 daemon: running"));
4640
4761
  return;
@@ -4653,31 +4774,31 @@ function daemonStatus() {
4653
4774
  console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
4654
4775
  }
4655
4776
  }
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;
4777
+ 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
4778
  var init_daemon = __esm({
4658
4779
  "src/daemon/index.ts"() {
4659
4780
  "use strict";
4660
4781
  init_ui2();
4661
4782
  import_http = __toESM(require("http"));
4662
4783
  import_net2 = __toESM(require("net"));
4663
- import_fs4 = __toESM(require("fs"));
4664
- import_path6 = __toESM(require("path"));
4784
+ import_fs5 = __toESM(require("fs"));
4785
+ import_path7 = __toESM(require("path"));
4665
4786
  import_os4 = __toESM(require("os"));
4666
4787
  import_child_process3 = require("child_process");
4667
4788
  import_crypto3 = require("crypto");
4668
4789
  import_chalk4 = __toESM(require("chalk"));
4669
4790
  init_core();
4670
4791
  init_shields();
4671
- ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path6.default.join(import_os4.default.tmpdir(), "node9-activity.sock");
4792
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path7.default.join(import_os4.default.tmpdir(), "node9-activity.sock");
4672
4793
  DAEMON_PORT2 = 7391;
4673
4794
  DAEMON_HOST2 = "127.0.0.1";
4674
4795
  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");
4796
+ DAEMON_PID_FILE = import_path7.default.join(homeDir, ".node9", "daemon.pid");
4797
+ DECISIONS_FILE = import_path7.default.join(homeDir, ".node9", "decisions.json");
4798
+ GLOBAL_CONFIG_FILE = import_path7.default.join(homeDir, ".node9", "config.json");
4799
+ CREDENTIALS_FILE = import_path7.default.join(homeDir, ".node9", "credentials.json");
4800
+ AUDIT_LOG_FILE = import_path7.default.join(homeDir, ".node9", "audit.log");
4801
+ TRUST_FILE2 = import_path7.default.join(homeDir, ".node9", "trust.json");
4681
4802
  TRUST_DURATIONS = {
4682
4803
  "30m": 30 * 6e4,
4683
4804
  "1h": 60 * 6e4,
@@ -4738,9 +4859,9 @@ function renderPending(activity) {
4738
4859
  }
4739
4860
  async function ensureDaemon() {
4740
4861
  let pidPort = null;
4741
- if (import_fs6.default.existsSync(PID_FILE)) {
4862
+ if (import_fs7.default.existsSync(PID_FILE)) {
4742
4863
  try {
4743
- const { port } = JSON.parse(import_fs6.default.readFileSync(PID_FILE, "utf-8"));
4864
+ const { port } = JSON.parse(import_fs7.default.readFileSync(PID_FILE, "utf-8"));
4744
4865
  pidPort = port;
4745
4866
  } catch {
4746
4867
  console.error(import_chalk5.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
@@ -4895,19 +5016,19 @@ async function startTail(options = {}) {
4895
5016
  process.exit(1);
4896
5017
  });
4897
5018
  }
4898
- var import_http2, import_chalk5, import_fs6, import_os6, import_path8, import_readline, import_child_process5, PID_FILE, ICONS;
5019
+ var import_http2, import_chalk5, import_fs7, import_os6, import_path9, import_readline, import_child_process5, PID_FILE, ICONS;
4899
5020
  var init_tail = __esm({
4900
5021
  "src/tui/tail.ts"() {
4901
5022
  "use strict";
4902
5023
  import_http2 = __toESM(require("http"));
4903
5024
  import_chalk5 = __toESM(require("chalk"));
4904
- import_fs6 = __toESM(require("fs"));
5025
+ import_fs7 = __toESM(require("fs"));
4905
5026
  import_os6 = __toESM(require("os"));
4906
- import_path8 = __toESM(require("path"));
5027
+ import_path9 = __toESM(require("path"));
4907
5028
  import_readline = __toESM(require("readline"));
4908
5029
  import_child_process5 = require("child_process");
4909
5030
  init_daemon();
4910
- PID_FILE = import_path8.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
5031
+ PID_FILE = import_path9.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
4911
5032
  ICONS = {
4912
5033
  bash: "\u{1F4BB}",
4913
5034
  shell: "\u{1F4BB}",
@@ -4933,8 +5054,8 @@ var import_commander = require("commander");
4933
5054
  init_core();
4934
5055
 
4935
5056
  // src/setup.ts
4936
- var import_fs3 = __toESM(require("fs"));
4937
- var import_path5 = __toESM(require("path"));
5057
+ var import_fs4 = __toESM(require("fs"));
5058
+ var import_path6 = __toESM(require("path"));
4938
5059
  var import_os3 = __toESM(require("os"));
4939
5060
  var import_chalk3 = __toESM(require("chalk"));
4940
5061
  var import_prompts2 = require("@inquirer/prompts");
@@ -4952,17 +5073,17 @@ function fullPathCommand(subcommand) {
4952
5073
  }
4953
5074
  function readJson(filePath) {
4954
5075
  try {
4955
- if (import_fs3.default.existsSync(filePath)) {
4956
- return JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
5076
+ if (import_fs4.default.existsSync(filePath)) {
5077
+ return JSON.parse(import_fs4.default.readFileSync(filePath, "utf-8"));
4957
5078
  }
4958
5079
  } catch {
4959
5080
  }
4960
5081
  return null;
4961
5082
  }
4962
5083
  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");
5084
+ const dir = import_path6.default.dirname(filePath);
5085
+ if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
5086
+ import_fs4.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
4966
5087
  }
4967
5088
  function isNode9Hook(cmd) {
4968
5089
  if (!cmd) return false;
@@ -4970,8 +5091,8 @@ function isNode9Hook(cmd) {
4970
5091
  }
4971
5092
  function teardownClaude() {
4972
5093
  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");
5094
+ const hooksPath = import_path6.default.join(homeDir2, ".claude", "settings.json");
5095
+ const mcpPath = import_path6.default.join(homeDir2, ".claude.json");
4975
5096
  let changed = false;
4976
5097
  const settings = readJson(hooksPath);
4977
5098
  if (settings?.hooks) {
@@ -5020,7 +5141,7 @@ function teardownClaude() {
5020
5141
  }
5021
5142
  function teardownGemini() {
5022
5143
  const homeDir2 = import_os3.default.homedir();
5023
- const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
5144
+ const settingsPath = import_path6.default.join(homeDir2, ".gemini", "settings.json");
5024
5145
  const settings = readJson(settingsPath);
5025
5146
  if (!settings) {
5026
5147
  console.log(import_chalk3.default.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
@@ -5059,7 +5180,7 @@ function teardownGemini() {
5059
5180
  }
5060
5181
  function teardownCursor() {
5061
5182
  const homeDir2 = import_os3.default.homedir();
5062
- const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
5183
+ const mcpPath = import_path6.default.join(homeDir2, ".cursor", "mcp.json");
5063
5184
  const mcpConfig = readJson(mcpPath);
5064
5185
  if (!mcpConfig?.mcpServers) {
5065
5186
  console.log(import_chalk3.default.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
@@ -5086,8 +5207,8 @@ function teardownCursor() {
5086
5207
  }
5087
5208
  async function setupClaude() {
5088
5209
  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");
5210
+ const mcpPath = import_path6.default.join(homeDir2, ".claude.json");
5211
+ const hooksPath = import_path6.default.join(homeDir2, ".claude", "settings.json");
5091
5212
  const claudeConfig = readJson(mcpPath) ?? {};
5092
5213
  const settings = readJson(hooksPath) ?? {};
5093
5214
  const servers = claudeConfig.mcpServers ?? {};
@@ -5162,7 +5283,7 @@ async function setupClaude() {
5162
5283
  }
5163
5284
  async function setupGemini() {
5164
5285
  const homeDir2 = import_os3.default.homedir();
5165
- const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
5286
+ const settingsPath = import_path6.default.join(homeDir2, ".gemini", "settings.json");
5166
5287
  const settings = readJson(settingsPath) ?? {};
5167
5288
  const servers = settings.mcpServers ?? {};
5168
5289
  let anythingChanged = false;
@@ -5245,7 +5366,7 @@ async function setupGemini() {
5245
5366
  }
5246
5367
  async function setupCursor() {
5247
5368
  const homeDir2 = import_os3.default.homedir();
5248
- const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
5369
+ const mcpPath = import_path6.default.join(homeDir2, ".cursor", "mcp.json");
5249
5370
  const mcpConfig = readJson(mcpPath) ?? {};
5250
5371
  const servers = mcpConfig.mcpServers ?? {};
5251
5372
  let anythingChanged = false;
@@ -5306,30 +5427,32 @@ var import_execa = require("execa");
5306
5427
  var import_execa2 = require("execa");
5307
5428
  var import_chalk6 = __toESM(require("chalk"));
5308
5429
  var import_readline2 = __toESM(require("readline"));
5309
- var import_fs7 = __toESM(require("fs"));
5310
- var import_path9 = __toESM(require("path"));
5430
+ var import_fs8 = __toESM(require("fs"));
5431
+ var import_path10 = __toESM(require("path"));
5311
5432
  var import_os7 = __toESM(require("os"));
5312
5433
 
5313
5434
  // src/undo.ts
5314
5435
  var import_child_process4 = require("child_process");
5315
- var import_fs5 = __toESM(require("fs"));
5316
- var import_path7 = __toESM(require("path"));
5436
+ var import_crypto4 = __toESM(require("crypto"));
5437
+ var import_fs6 = __toESM(require("fs"));
5438
+ var import_path8 = __toESM(require("path"));
5317
5439
  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");
5440
+ var SNAPSHOT_STACK_PATH = import_path8.default.join(import_os5.default.homedir(), ".node9", "snapshots.json");
5441
+ var UNDO_LATEST_PATH = import_path8.default.join(import_os5.default.homedir(), ".node9", "undo_latest.txt");
5320
5442
  var MAX_SNAPSHOTS = 10;
5443
+ var GIT_TIMEOUT = 15e3;
5321
5444
  function readStack() {
5322
5445
  try {
5323
- if (import_fs5.default.existsSync(SNAPSHOT_STACK_PATH))
5324
- return JSON.parse(import_fs5.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
5446
+ if (import_fs6.default.existsSync(SNAPSHOT_STACK_PATH))
5447
+ return JSON.parse(import_fs6.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
5325
5448
  } catch {
5326
5449
  }
5327
5450
  return [];
5328
5451
  }
5329
5452
  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));
5453
+ const dir = import_path8.default.dirname(SNAPSHOT_STACK_PATH);
5454
+ if (!import_fs6.default.existsSync(dir)) import_fs6.default.mkdirSync(dir, { recursive: true });
5455
+ import_fs6.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
5333
5456
  }
5334
5457
  function buildArgsSummary(tool, args) {
5335
5458
  if (!args || typeof args !== "object") return "";
@@ -5342,54 +5465,177 @@ function buildArgsSummary(tool, args) {
5342
5465
  if (typeof sql === "string") return sql.slice(0, 80);
5343
5466
  return tool;
5344
5467
  }
5345
- async function createShadowSnapshot(tool = "unknown", args = {}) {
5468
+ function normalizeCwdForHash(cwd) {
5469
+ let normalized;
5470
+ try {
5471
+ normalized = import_fs6.default.realpathSync(cwd);
5472
+ } catch {
5473
+ normalized = cwd;
5474
+ }
5475
+ normalized = normalized.replace(/\\/g, "/");
5476
+ if (process.platform === "win32") normalized = normalized.toLowerCase();
5477
+ return normalized;
5478
+ }
5479
+ function getShadowRepoDir(cwd) {
5480
+ const hash = import_crypto4.default.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
5481
+ return import_path8.default.join(import_os5.default.homedir(), ".node9", "snapshots", hash);
5482
+ }
5483
+ function cleanOrphanedIndexFiles(shadowDir) {
5484
+ try {
5485
+ const cutoff = Date.now() - 6e4;
5486
+ for (const f of import_fs6.default.readdirSync(shadowDir)) {
5487
+ if (f.startsWith("index_")) {
5488
+ const fp = import_path8.default.join(shadowDir, f);
5489
+ try {
5490
+ if (import_fs6.default.statSync(fp).mtimeMs < cutoff) import_fs6.default.unlinkSync(fp);
5491
+ } catch {
5492
+ }
5493
+ }
5494
+ }
5495
+ } catch {
5496
+ }
5497
+ }
5498
+ function writeShadowExcludes(shadowDir, ignorePaths) {
5499
+ const hardcoded = [".git", ".node9"];
5500
+ const lines = [...hardcoded, ...ignorePaths].join("\n");
5501
+ try {
5502
+ import_fs6.default.writeFileSync(import_path8.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
5503
+ } catch {
5504
+ }
5505
+ }
5506
+ function ensureShadowRepo(shadowDir, cwd) {
5507
+ cleanOrphanedIndexFiles(shadowDir);
5508
+ const normalizedCwd = normalizeCwdForHash(cwd);
5509
+ const shadowEnvBase = { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
5510
+ const check = (0, import_child_process4.spawnSync)("git", ["rev-parse", "--git-dir"], {
5511
+ env: shadowEnvBase,
5512
+ timeout: 3e3
5513
+ });
5514
+ if (check.status === 0) {
5515
+ const ptPath = import_path8.default.join(shadowDir, "project-path.txt");
5516
+ try {
5517
+ const stored = import_fs6.default.readFileSync(ptPath, "utf8").trim();
5518
+ if (stored === normalizedCwd) return true;
5519
+ if (process.env.NODE9_DEBUG === "1")
5520
+ console.error(
5521
+ `[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
5522
+ );
5523
+ import_fs6.default.rmSync(shadowDir, { recursive: true, force: true });
5524
+ } catch {
5525
+ try {
5526
+ import_fs6.default.writeFileSync(ptPath, normalizedCwd, "utf8");
5527
+ } catch {
5528
+ }
5529
+ return true;
5530
+ }
5531
+ }
5532
+ try {
5533
+ import_fs6.default.mkdirSync(shadowDir, { recursive: true });
5534
+ } catch {
5535
+ }
5536
+ const init = (0, import_child_process4.spawnSync)("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
5537
+ if (init.status !== 0) {
5538
+ if (process.env.NODE9_DEBUG === "1")
5539
+ console.error("[Node9] git init --bare failed:", init.stderr?.toString());
5540
+ return false;
5541
+ }
5542
+ const configFile = import_path8.default.join(shadowDir, "config");
5543
+ (0, import_child_process4.spawnSync)("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
5544
+ timeout: 3e3
5545
+ });
5546
+ (0, import_child_process4.spawnSync)("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
5547
+ timeout: 3e3
5548
+ });
5549
+ try {
5550
+ import_fs6.default.writeFileSync(import_path8.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
5551
+ } catch {
5552
+ }
5553
+ return true;
5554
+ }
5555
+ function buildGitEnv(cwd) {
5556
+ const shadowDir = getShadowRepoDir(cwd);
5557
+ const check = (0, import_child_process4.spawnSync)("git", ["rev-parse", "--git-dir"], {
5558
+ env: { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd },
5559
+ timeout: 2e3
5560
+ });
5561
+ if (check.status === 0) {
5562
+ return { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
5563
+ }
5564
+ return { ...process.env };
5565
+ }
5566
+ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = []) {
5567
+ let indexFile = null;
5346
5568
  try {
5347
5569
  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);
5570
+ const shadowDir = getShadowRepoDir(cwd);
5571
+ if (!ensureShadowRepo(shadowDir, cwd)) return null;
5572
+ writeShadowExcludes(shadowDir, ignorePaths);
5573
+ indexFile = import_path8.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
5574
+ const shadowEnv = {
5575
+ ...process.env,
5576
+ GIT_DIR: shadowDir,
5577
+ GIT_WORK_TREE: cwd,
5578
+ GIT_INDEX_FILE: indexFile
5579
+ };
5580
+ (0, import_child_process4.spawnSync)("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
5581
+ const treeRes = (0, import_child_process4.spawnSync)("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
5582
+ const treeHash = treeRes.stdout?.toString().trim();
5355
5583
  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();
5584
+ const commitRes = (0, import_child_process4.spawnSync)(
5585
+ "git",
5586
+ ["commit-tree", treeHash, "-m", `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`],
5587
+ { env: shadowEnv, timeout: GIT_TIMEOUT }
5588
+ );
5589
+ const commitHash = commitRes.stdout?.toString().trim();
5363
5590
  if (!commitHash || commitRes.status !== 0) return null;
5364
5591
  const stack = readStack();
5365
- const entry = {
5592
+ stack.push({
5366
5593
  hash: commitHash,
5367
5594
  tool,
5368
5595
  argsSummary: buildArgsSummary(tool, args),
5369
5596
  cwd,
5370
5597
  timestamp: Date.now()
5371
- };
5372
- stack.push(entry);
5598
+ });
5599
+ const shouldGc = stack.length % 5 === 0;
5373
5600
  if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
5374
5601
  writeStack(stack);
5375
- import_fs5.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
5602
+ import_fs6.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
5603
+ if (shouldGc) {
5604
+ (0, import_child_process4.spawn)("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
5605
+ }
5376
5606
  return commitHash;
5377
5607
  } catch (err) {
5378
5608
  if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
5609
+ return null;
5610
+ } finally {
5611
+ if (indexFile) {
5612
+ try {
5613
+ import_fs6.default.unlinkSync(indexFile);
5614
+ } catch {
5615
+ }
5616
+ }
5379
5617
  }
5380
- return null;
5381
5618
  }
5382
5619
  function getSnapshotHistory() {
5383
5620
  return readStack();
5384
5621
  }
5385
5622
  function computeUndoDiff(hash, cwd) {
5386
5623
  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;
5624
+ const env = buildGitEnv(cwd);
5625
+ const statRes = (0, import_child_process4.spawnSync)("git", ["diff", hash, "--stat", "--", "."], {
5626
+ cwd,
5627
+ env,
5628
+ timeout: GIT_TIMEOUT
5629
+ });
5630
+ const stat = statRes.stdout?.toString().trim();
5631
+ if (!stat || statRes.status !== 0) return null;
5632
+ const diffRes = (0, import_child_process4.spawnSync)("git", ["diff", hash, "--", "."], {
5633
+ cwd,
5634
+ env,
5635
+ timeout: GIT_TIMEOUT
5636
+ });
5637
+ const raw = diffRes.stdout?.toString();
5638
+ if (!raw || diffRes.status !== 0) return null;
5393
5639
  const lines = raw.split("\n").filter(
5394
5640
  (l) => !l.startsWith("diff --git") && !l.startsWith("index ") && !l.startsWith("Binary")
5395
5641
  );
@@ -5401,18 +5647,41 @@ function computeUndoDiff(hash, cwd) {
5401
5647
  function applyUndo(hash, cwd) {
5402
5648
  try {
5403
5649
  const dir = cwd ?? process.cwd();
5650
+ const env = buildGitEnv(dir);
5404
5651
  const restore = (0, import_child_process4.spawnSync)("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
5405
- cwd: dir
5652
+ cwd: dir,
5653
+ env,
5654
+ timeout: GIT_TIMEOUT
5406
5655
  });
5407
5656
  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);
5657
+ const lsTree = (0, import_child_process4.spawnSync)("git", ["ls-tree", "-r", "--name-only", hash], {
5658
+ cwd: dir,
5659
+ env,
5660
+ timeout: GIT_TIMEOUT
5661
+ });
5662
+ if (lsTree.status !== 0) {
5663
+ process.stderr.write(`[Node9] applyUndo: git ls-tree failed for hash ${hash}
5664
+ `);
5665
+ return false;
5666
+ }
5667
+ const snapshotFiles = new Set(
5668
+ lsTree.stdout?.toString().trim().split("\n").filter(Boolean) ?? []
5669
+ );
5670
+ if (snapshotFiles.size === 0) {
5671
+ process.stderr.write(`[Node9] applyUndo: ls-tree returned no files for hash ${hash}
5672
+ `);
5673
+ return false;
5674
+ }
5675
+ const tracked = (0, import_child_process4.spawnSync)("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
5676
+ const untracked = (0, import_child_process4.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], {
5677
+ cwd: dir,
5678
+ env,
5679
+ timeout: GIT_TIMEOUT
5680
+ }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
5412
5681
  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);
5682
+ const fullPath = import_path8.default.join(dir, file);
5683
+ if (!snapshotFiles.has(file) && import_fs6.default.existsSync(fullPath)) {
5684
+ import_fs6.default.unlinkSync(fullPath);
5416
5685
  }
5417
5686
  }
5418
5687
  return true;
@@ -5425,7 +5694,7 @@ function applyUndo(hash, cwd) {
5425
5694
  init_shields();
5426
5695
  var import_prompts3 = require("@inquirer/prompts");
5427
5696
  var { version } = JSON.parse(
5428
- import_fs7.default.readFileSync(import_path9.default.join(__dirname, "../package.json"), "utf-8")
5697
+ import_fs8.default.readFileSync(import_path10.default.join(__dirname, "../package.json"), "utf-8")
5429
5698
  );
5430
5699
  function parseDuration(str) {
5431
5700
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -5557,7 +5826,7 @@ async function runProxy(targetCommand) {
5557
5826
  if (stdout) executable = stdout.trim();
5558
5827
  } catch {
5559
5828
  }
5560
- console.log(import_chalk6.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
5829
+ console.error(import_chalk6.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
5561
5830
  const child = (0, import_child_process6.spawn)(executable, args, {
5562
5831
  stdio: ["pipe", "pipe", "inherit"],
5563
5832
  // We control STDIN and STDOUT
@@ -5627,14 +5896,14 @@ async function runProxy(targetCommand) {
5627
5896
  }
5628
5897
  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
5898
  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 });
5899
+ const credPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "credentials.json");
5900
+ if (!import_fs8.default.existsSync(import_path10.default.dirname(credPath)))
5901
+ import_fs8.default.mkdirSync(import_path10.default.dirname(credPath), { recursive: true });
5633
5902
  const profileName = options.profile || "default";
5634
5903
  let existingCreds = {};
5635
5904
  try {
5636
- if (import_fs7.default.existsSync(credPath)) {
5637
- const raw = JSON.parse(import_fs7.default.readFileSync(credPath, "utf-8"));
5905
+ if (import_fs8.default.existsSync(credPath)) {
5906
+ const raw = JSON.parse(import_fs8.default.readFileSync(credPath, "utf-8"));
5638
5907
  if (raw.apiKey) {
5639
5908
  existingCreds = {
5640
5909
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -5646,13 +5915,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
5646
5915
  } catch {
5647
5916
  }
5648
5917
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
5649
- import_fs7.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
5918
+ import_fs8.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
5650
5919
  if (profileName === "default") {
5651
- const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
5920
+ const configPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "config.json");
5652
5921
  let config = {};
5653
5922
  try {
5654
- if (import_fs7.default.existsSync(configPath))
5655
- config = JSON.parse(import_fs7.default.readFileSync(configPath, "utf-8"));
5923
+ if (import_fs8.default.existsSync(configPath))
5924
+ config = JSON.parse(import_fs8.default.readFileSync(configPath, "utf-8"));
5656
5925
  } catch {
5657
5926
  }
5658
5927
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -5667,9 +5936,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
5667
5936
  approvers.cloud = false;
5668
5937
  }
5669
5938
  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 });
5939
+ if (!import_fs8.default.existsSync(import_path10.default.dirname(configPath)))
5940
+ import_fs8.default.mkdirSync(import_path10.default.dirname(configPath), { recursive: true });
5941
+ import_fs8.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
5673
5942
  }
5674
5943
  if (options.profile && profileName !== "default") {
5675
5944
  console.log(import_chalk6.default.green(`\u2705 Profile "${profileName}" saved`));
@@ -5755,15 +6024,15 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
5755
6024
  }
5756
6025
  }
5757
6026
  if (options.purge) {
5758
- const node9Dir = import_path9.default.join(import_os7.default.homedir(), ".node9");
5759
- if (import_fs7.default.existsSync(node9Dir)) {
6027
+ const node9Dir = import_path10.default.join(import_os7.default.homedir(), ".node9");
6028
+ if (import_fs8.default.existsSync(node9Dir)) {
5760
6029
  const confirmed = await (0, import_prompts3.confirm)({
5761
6030
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
5762
6031
  default: false
5763
6032
  });
5764
6033
  if (confirmed) {
5765
- import_fs7.default.rmSync(node9Dir, { recursive: true });
5766
- if (import_fs7.default.existsSync(node9Dir)) {
6034
+ import_fs8.default.rmSync(node9Dir, { recursive: true });
6035
+ if (import_fs8.default.existsSync(node9Dir)) {
5767
6036
  console.error(
5768
6037
  import_chalk6.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
5769
6038
  );
@@ -5835,10 +6104,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5835
6104
  );
5836
6105
  }
5837
6106
  section("Configuration");
5838
- const globalConfigPath = import_path9.default.join(homeDir2, ".node9", "config.json");
5839
- if (import_fs7.default.existsSync(globalConfigPath)) {
6107
+ const globalConfigPath = import_path10.default.join(homeDir2, ".node9", "config.json");
6108
+ if (import_fs8.default.existsSync(globalConfigPath)) {
5840
6109
  try {
5841
- JSON.parse(import_fs7.default.readFileSync(globalConfigPath, "utf-8"));
6110
+ JSON.parse(import_fs8.default.readFileSync(globalConfigPath, "utf-8"));
5842
6111
  pass("~/.node9/config.json found and valid");
5843
6112
  } catch {
5844
6113
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -5846,17 +6115,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
5846
6115
  } else {
5847
6116
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
5848
6117
  }
5849
- const projectConfigPath = import_path9.default.join(process.cwd(), "node9.config.json");
5850
- if (import_fs7.default.existsSync(projectConfigPath)) {
6118
+ const projectConfigPath = import_path10.default.join(process.cwd(), "node9.config.json");
6119
+ if (import_fs8.default.existsSync(projectConfigPath)) {
5851
6120
  try {
5852
- JSON.parse(import_fs7.default.readFileSync(projectConfigPath, "utf-8"));
6121
+ JSON.parse(import_fs8.default.readFileSync(projectConfigPath, "utf-8"));
5853
6122
  pass("node9.config.json found and valid (project)");
5854
6123
  } catch {
5855
6124
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
5856
6125
  }
5857
6126
  }
5858
- const credsPath = import_path9.default.join(homeDir2, ".node9", "credentials.json");
5859
- if (import_fs7.default.existsSync(credsPath)) {
6127
+ const credsPath = import_path10.default.join(homeDir2, ".node9", "credentials.json");
6128
+ if (import_fs8.default.existsSync(credsPath)) {
5860
6129
  pass("Cloud credentials found (~/.node9/credentials.json)");
5861
6130
  } else {
5862
6131
  warn(
@@ -5865,10 +6134,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5865
6134
  );
5866
6135
  }
5867
6136
  section("Agent Hooks");
5868
- const claudeSettingsPath = import_path9.default.join(homeDir2, ".claude", "settings.json");
5869
- if (import_fs7.default.existsSync(claudeSettingsPath)) {
6137
+ const claudeSettingsPath = import_path10.default.join(homeDir2, ".claude", "settings.json");
6138
+ if (import_fs8.default.existsSync(claudeSettingsPath)) {
5870
6139
  try {
5871
- const cs = JSON.parse(import_fs7.default.readFileSync(claudeSettingsPath, "utf-8"));
6140
+ const cs = JSON.parse(import_fs8.default.readFileSync(claudeSettingsPath, "utf-8"));
5872
6141
  const hasHook = cs.hooks?.PreToolUse?.some(
5873
6142
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
5874
6143
  );
@@ -5881,10 +6150,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5881
6150
  } else {
5882
6151
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
5883
6152
  }
5884
- const geminiSettingsPath = import_path9.default.join(homeDir2, ".gemini", "settings.json");
5885
- if (import_fs7.default.existsSync(geminiSettingsPath)) {
6153
+ const geminiSettingsPath = import_path10.default.join(homeDir2, ".gemini", "settings.json");
6154
+ if (import_fs8.default.existsSync(geminiSettingsPath)) {
5886
6155
  try {
5887
- const gs = JSON.parse(import_fs7.default.readFileSync(geminiSettingsPath, "utf-8"));
6156
+ const gs = JSON.parse(import_fs8.default.readFileSync(geminiSettingsPath, "utf-8"));
5888
6157
  const hasHook = gs.hooks?.BeforeTool?.some(
5889
6158
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
5890
6159
  );
@@ -5897,10 +6166,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
5897
6166
  } else {
5898
6167
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
5899
6168
  }
5900
- const cursorHooksPath = import_path9.default.join(homeDir2, ".cursor", "hooks.json");
5901
- if (import_fs7.default.existsSync(cursorHooksPath)) {
6169
+ const cursorHooksPath = import_path10.default.join(homeDir2, ".cursor", "hooks.json");
6170
+ if (import_fs8.default.existsSync(cursorHooksPath)) {
5902
6171
  try {
5903
- const cur = JSON.parse(import_fs7.default.readFileSync(cursorHooksPath, "utf-8"));
6172
+ const cur = JSON.parse(import_fs8.default.readFileSync(cursorHooksPath, "utf-8"));
5904
6173
  const hasHook = cur.hooks?.preToolUse?.some(
5905
6174
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
5906
6175
  );
@@ -6002,8 +6271,8 @@ program.command("explain").description(
6002
6271
  console.log("");
6003
6272
  });
6004
6273
  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) {
6274
+ const configPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "config.json");
6275
+ if (import_fs8.default.existsSync(configPath) && !options.force) {
6007
6276
  console.log(import_chalk6.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
6008
6277
  console.log(import_chalk6.default.gray(` Run with --force to overwrite.`));
6009
6278
  return;
@@ -6017,9 +6286,9 @@ program.command("init").description("Create ~/.node9/config.json with default po
6017
6286
  mode: safeMode
6018
6287
  }
6019
6288
  };
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));
6289
+ const dir = import_path10.default.dirname(configPath);
6290
+ if (!import_fs8.default.existsSync(dir)) import_fs8.default.mkdirSync(dir, { recursive: true });
6291
+ import_fs8.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
6023
6292
  console.log(import_chalk6.default.green(`\u2705 Global config created: ${configPath}`));
6024
6293
  console.log(import_chalk6.default.cyan(` Mode set to: ${safeMode}`));
6025
6294
  console.log(
@@ -6037,14 +6306,14 @@ function formatRelativeTime(timestamp) {
6037
6306
  return new Date(timestamp).toLocaleDateString();
6038
6307
  }
6039
6308
  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)) {
6309
+ const logPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "audit.log");
6310
+ if (!import_fs8.default.existsSync(logPath)) {
6042
6311
  console.log(
6043
6312
  import_chalk6.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
6044
6313
  );
6045
6314
  return;
6046
6315
  }
6047
- const raw = import_fs7.default.readFileSync(logPath, "utf-8");
6316
+ const raw = import_fs8.default.readFileSync(logPath, "utf-8");
6048
6317
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
6049
6318
  let entries = lines.flatMap((line) => {
6050
6319
  try {
@@ -6127,13 +6396,13 @@ program.command("status").description("Show current Node9 mode, policy source, a
6127
6396
  console.log("");
6128
6397
  const modeLabel = settings.mode === "audit" ? import_chalk6.default.blue("audit") : settings.mode === "strict" ? import_chalk6.default.red("strict") : import_chalk6.default.white("standard");
6129
6398
  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");
6399
+ const projectConfig = import_path10.default.join(process.cwd(), "node9.config.json");
6400
+ const globalConfig = import_path10.default.join(import_os7.default.homedir(), ".node9", "config.json");
6132
6401
  console.log(
6133
- ` Local: ${import_fs7.default.existsSync(projectConfig) ? import_chalk6.default.green("Active (node9.config.json)") : import_chalk6.default.gray("Not present")}`
6402
+ ` Local: ${import_fs8.default.existsSync(projectConfig) ? import_chalk6.default.green("Active (node9.config.json)") : import_chalk6.default.gray("Not present")}`
6134
6403
  );
6135
6404
  console.log(
6136
- ` Global: ${import_fs7.default.existsSync(globalConfig) ? import_chalk6.default.green("Active (~/.node9/config.json)") : import_chalk6.default.gray("Not present")}`
6405
+ ` Global: ${import_fs8.default.existsSync(globalConfig) ? import_chalk6.default.green("Active (~/.node9/config.json)") : import_chalk6.default.gray("Not present")}`
6137
6406
  );
6138
6407
  if (mergedConfig.policy.sandboxPaths.length > 0) {
6139
6408
  console.log(
@@ -6217,9 +6486,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
6217
6486
  } catch (err) {
6218
6487
  const tempConfig = getConfig();
6219
6488
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
6220
- const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6489
+ const logPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6221
6490
  const errMsg = err instanceof Error ? err.message : String(err);
6222
- import_fs7.default.appendFileSync(
6491
+ import_fs8.default.appendFileSync(
6223
6492
  logPath,
6224
6493
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
6225
6494
  RAW: ${raw}
@@ -6228,19 +6497,12 @@ RAW: ${raw}
6228
6497
  }
6229
6498
  process.exit(0);
6230
6499
  }
6231
- if (payload.cwd) {
6232
- try {
6233
- process.chdir(payload.cwd);
6234
- _resetConfigCache();
6235
- } catch {
6236
- }
6237
- }
6238
- const config = getConfig();
6500
+ const config = getConfig(payload.cwd || void 0);
6239
6501
  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}
6502
+ const logPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6503
+ if (!import_fs8.default.existsSync(import_path10.default.dirname(logPath)))
6504
+ import_fs8.default.mkdirSync(import_path10.default.dirname(logPath), { recursive: true });
6505
+ import_fs8.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
6244
6506
  `);
6245
6507
  }
6246
6508
  const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
@@ -6284,7 +6546,7 @@ RAW: ${raw}
6284
6546
  }
6285
6547
  const meta = { agent, mcpServer };
6286
6548
  if (shouldSnapshot(toolName, toolInput, config)) {
6287
- await createShadowSnapshot(toolName, toolInput);
6549
+ await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
6288
6550
  }
6289
6551
  const result = await authorizeHeadless(toolName, toolInput, false, meta);
6290
6552
  if (result.approved) {
@@ -6317,9 +6579,9 @@ RAW: ${raw}
6317
6579
  });
6318
6580
  } catch (err) {
6319
6581
  if (process.env.NODE9_DEBUG === "1") {
6320
- const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6582
+ const logPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6321
6583
  const errMsg = err instanceof Error ? err.message : String(err);
6322
- import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
6584
+ import_fs8.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
6323
6585
  `);
6324
6586
  }
6325
6587
  process.exit(0);
@@ -6364,15 +6626,25 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
6364
6626
  decision: "allowed",
6365
6627
  source: "post-hook"
6366
6628
  };
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");
6371
- const config = getConfig();
6629
+ const logPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "audit.log");
6630
+ if (!import_fs8.default.existsSync(import_path10.default.dirname(logPath)))
6631
+ import_fs8.default.mkdirSync(import_path10.default.dirname(logPath), { recursive: true });
6632
+ import_fs8.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
6633
+ const safeCwd = typeof payload.cwd === "string" && import_path10.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
6634
+ const config = getConfig(safeCwd);
6372
6635
  if (shouldSnapshot(tool, {}, config)) {
6373
- await createShadowSnapshot();
6636
+ await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
6637
+ }
6638
+ } catch (err) {
6639
+ const msg = err instanceof Error ? err.message : String(err);
6640
+ process.stderr.write(`[Node9] audit log error: ${msg}
6641
+ `);
6642
+ const debugPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6643
+ try {
6644
+ import_fs8.default.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
6645
+ `);
6646
+ } catch {
6374
6647
  }
6375
- } catch {
6376
6648
  }
6377
6649
  process.exit(0);
6378
6650
  };
@@ -6643,9 +6915,9 @@ process.on("unhandledRejection", (reason) => {
6643
6915
  const isCheckHook = process.argv[2] === "check";
6644
6916
  if (isCheckHook) {
6645
6917
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
6646
- const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6918
+ const logPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6647
6919
  const msg = reason instanceof Error ? reason.message : String(reason);
6648
- import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
6920
+ import_fs8.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
6649
6921
  `);
6650
6922
  }
6651
6923
  process.exit(0);