@node9/proxy 1.0.8 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -30,18 +30,18 @@ var import_commander = require("commander");
30
30
  var import_chalk2 = __toESM(require("chalk"));
31
31
  var import_prompts = require("@inquirer/prompts");
32
32
  var import_fs = __toESM(require("fs"));
33
- var import_path2 = __toESM(require("path"));
33
+ var import_path3 = __toESM(require("path"));
34
34
  var import_os = __toESM(require("os"));
35
35
  var import_picomatch = __toESM(require("picomatch"));
36
36
  var import_sh_syntax = require("sh-syntax");
37
37
 
38
38
  // src/ui/native.ts
39
39
  var import_child_process = require("child_process");
40
- var import_path = __toESM(require("path"));
40
+ var import_path2 = __toESM(require("path"));
41
41
  var import_chalk = __toESM(require("chalk"));
42
- var isTestEnv = () => {
43
- return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
44
- };
42
+
43
+ // src/context-sniper.ts
44
+ var import_path = __toESM(require("path"));
45
45
  function smartTruncate(str, maxLen = 500) {
46
46
  if (str.length <= maxLen) return str;
47
47
  const edge = Math.floor(maxLen / 2) - 3;
@@ -49,11 +49,13 @@ function smartTruncate(str, maxLen = 500) {
49
49
  }
50
50
  function extractContext(text, matchedWord) {
51
51
  const lines = text.split("\n");
52
- if (lines.length <= 7 || !matchedWord) return smartTruncate(text, 500);
52
+ if (lines.length <= 7 || !matchedWord) {
53
+ return { snippet: smartTruncate(text, 500), lineIndex: -1 };
54
+ }
53
55
  const escaped = matchedWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
54
56
  const pattern = new RegExp(`\\b${escaped}\\b`, "i");
55
57
  const allHits = lines.map((line, i) => ({ i, line })).filter(({ line }) => pattern.test(line));
56
- if (allHits.length === 0) return smartTruncate(text, 500);
58
+ if (allHits.length === 0) return { snippet: smartTruncate(text, 500), lineIndex: -1 };
57
59
  const nonComment = allHits.find(({ line }) => {
58
60
  const trimmed = line.trim();
59
61
  return !trimmed.startsWith("//") && !trimmed.startsWith("#");
@@ -61,13 +63,89 @@ function extractContext(text, matchedWord) {
61
63
  const hitIndex = (nonComment ?? allHits[0]).i;
62
64
  const start = Math.max(0, hitIndex - 3);
63
65
  const end = Math.min(lines.length, hitIndex + 4);
66
+ const lineIndex = hitIndex - start;
64
67
  const snippet = lines.slice(start, end).map((line, i) => `${start + i === hitIndex ? "\u{1F6D1} " : " "}${line}`).join("\n");
65
68
  const head = start > 0 ? `... [${start} lines hidden] ...
66
69
  ` : "";
67
70
  const tail = end < lines.length ? `
68
71
  ... [${lines.length - end} lines hidden] ...` : "";
69
- return `${head}${snippet}${tail}`;
72
+ return { snippet: `${head}${snippet}${tail}`, lineIndex };
73
+ }
74
+ var CODE_KEYS = [
75
+ "command",
76
+ "cmd",
77
+ "shell_command",
78
+ "bash_command",
79
+ "script",
80
+ "code",
81
+ "input",
82
+ "sql",
83
+ "query",
84
+ "arguments",
85
+ "args",
86
+ "param",
87
+ "params",
88
+ "text"
89
+ ];
90
+ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
91
+ let intent = "EXEC";
92
+ let contextSnippet;
93
+ let contextLineIndex;
94
+ let editFileName;
95
+ let editFilePath;
96
+ let parsed = args;
97
+ if (typeof args === "string") {
98
+ const trimmed = args.trim();
99
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
100
+ try {
101
+ parsed = JSON.parse(trimmed);
102
+ } catch {
103
+ }
104
+ }
105
+ }
106
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
107
+ const obj = parsed;
108
+ if (obj.old_string !== void 0 && obj.new_string !== void 0) {
109
+ intent = "EDIT";
110
+ if (obj.file_path) {
111
+ editFilePath = String(obj.file_path);
112
+ editFileName = import_path.default.basename(editFilePath);
113
+ }
114
+ const result = extractContext(String(obj.new_string), matchedWord);
115
+ contextSnippet = result.snippet;
116
+ if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
117
+ } else if (matchedField && obj[matchedField] !== void 0) {
118
+ const result = extractContext(String(obj[matchedField]), matchedWord);
119
+ contextSnippet = result.snippet;
120
+ if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
121
+ } else {
122
+ const foundKey = Object.keys(obj).find((k) => CODE_KEYS.includes(k.toLowerCase()));
123
+ if (foundKey) {
124
+ const val = obj[foundKey];
125
+ contextSnippet = smartTruncate(typeof val === "string" ? val : JSON.stringify(val), 500);
126
+ }
127
+ }
128
+ } else if (typeof parsed === "string") {
129
+ contextSnippet = smartTruncate(parsed, 500);
130
+ }
131
+ return {
132
+ intent,
133
+ tier,
134
+ blockedByLabel,
135
+ ...matchedWord && { matchedWord },
136
+ ...matchedField && { matchedField },
137
+ ...contextSnippet !== void 0 && { contextSnippet },
138
+ ...contextLineIndex !== void 0 && { contextLineIndex },
139
+ ...editFileName && { editFileName },
140
+ ...editFilePath && { editFilePath },
141
+ ...ruleName && { ruleName }
142
+ };
70
143
  }
144
+
145
+ // src/ui/native.ts
146
+ var isTestEnv = () => {
147
+ return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
148
+ };
71
149
  function formatArgs(args, matchedField, matchedWord) {
72
150
  if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
73
151
  let parsed = args;
@@ -86,9 +164,9 @@ function formatArgs(args, matchedField, matchedWord) {
86
164
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
87
165
  const obj = parsed;
88
166
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
89
- const file = obj.file_path ? import_path.default.basename(String(obj.file_path)) : "file";
167
+ const file = obj.file_path ? import_path2.default.basename(String(obj.file_path)) : "file";
90
168
  const oldPreview = smartTruncate(String(obj.old_string), 120);
91
- const newPreview = extractContext(String(obj.new_string), matchedWord);
169
+ const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
92
170
  return {
93
171
  intent: "EDIT",
94
172
  message: `\u{1F4DD} EDITING: ${file}
@@ -106,7 +184,7 @@ ${newPreview}`
106
184
  const context = otherKeys.length > 0 ? `\u2699\uFE0F Context: ${otherKeys.map((k) => `${k}=${smartTruncate(typeof obj[k] === "object" ? JSON.stringify(obj[k]) : String(obj[k]), 30)}`).join(", ")}
107
185
 
108
186
  ` : "";
109
- const content = extractContext(String(obj[matchedField]), matchedWord);
187
+ const content = extractContext(String(obj[matchedField]), matchedWord).snippet;
110
188
  return {
111
189
  intent: "EXEC",
112
190
  message: `${context}\u{1F6D1} [${matchedField.toUpperCase()}]:
@@ -278,11 +356,113 @@ end run`;
278
356
  });
279
357
  }
280
358
 
359
+ // src/config-schema.ts
360
+ var import_zod = require("zod");
361
+ var noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
362
+ message: "Value must not contain literal newline characters (use \\n instead)"
363
+ });
364
+ var validRegex = noNewlines.refine(
365
+ (s) => {
366
+ try {
367
+ new RegExp(s);
368
+ return true;
369
+ } catch {
370
+ return false;
371
+ }
372
+ },
373
+ { message: "Value must be a valid regular expression" }
374
+ );
375
+ var SmartConditionSchema = import_zod.z.object({
376
+ field: import_zod.z.string().min(1, "Condition field must not be empty"),
377
+ op: import_zod.z.enum(["matches", "notMatches", "contains", "notContains", "exists", "notExists"], {
378
+ errorMap: () => ({
379
+ message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists"
380
+ })
381
+ }),
382
+ value: validRegex.optional(),
383
+ flags: import_zod.z.string().optional()
384
+ });
385
+ var SmartRuleSchema = import_zod.z.object({
386
+ name: import_zod.z.string().optional(),
387
+ tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
388
+ conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
389
+ conditionMode: import_zod.z.enum(["all", "any"]).optional(),
390
+ verdict: import_zod.z.enum(["allow", "review", "block"], {
391
+ errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
392
+ }),
393
+ reason: import_zod.z.string().optional()
394
+ });
395
+ var PolicyRuleSchema = import_zod.z.object({
396
+ action: import_zod.z.string().min(1),
397
+ allowPaths: import_zod.z.array(import_zod.z.string()).optional(),
398
+ blockPaths: import_zod.z.array(import_zod.z.string()).optional()
399
+ });
400
+ var ConfigFileSchema = import_zod.z.object({
401
+ version: import_zod.z.string().optional(),
402
+ settings: import_zod.z.object({
403
+ mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
404
+ autoStartDaemon: import_zod.z.boolean().optional(),
405
+ enableUndo: import_zod.z.boolean().optional(),
406
+ enableHookLogDebug: import_zod.z.boolean().optional(),
407
+ approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
408
+ approvers: import_zod.z.object({
409
+ native: import_zod.z.boolean().optional(),
410
+ browser: import_zod.z.boolean().optional(),
411
+ cloud: import_zod.z.boolean().optional(),
412
+ terminal: import_zod.z.boolean().optional()
413
+ }).optional(),
414
+ environment: import_zod.z.string().optional(),
415
+ slackEnabled: import_zod.z.boolean().optional(),
416
+ enableTrustSessions: import_zod.z.boolean().optional(),
417
+ allowGlobalPause: import_zod.z.boolean().optional()
418
+ }).optional(),
419
+ policy: import_zod.z.object({
420
+ sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
421
+ dangerousWords: import_zod.z.array(noNewlines).optional(),
422
+ ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
423
+ toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
424
+ rules: import_zod.z.array(PolicyRuleSchema).optional(),
425
+ smartRules: import_zod.z.array(SmartRuleSchema).optional(),
426
+ snapshot: import_zod.z.object({
427
+ tools: import_zod.z.array(import_zod.z.string()).optional(),
428
+ onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
429
+ ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
430
+ }).optional()
431
+ }).optional(),
432
+ environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
433
+ }).strict({ message: "Config contains unknown top-level keys" });
434
+ function sanitizeConfig(raw) {
435
+ const result = ConfigFileSchema.safeParse(raw);
436
+ if (result.success) {
437
+ return { sanitized: result.data, error: null };
438
+ }
439
+ const invalidTopLevelKeys = new Set(
440
+ result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
441
+ );
442
+ const sanitized = {};
443
+ if (typeof raw === "object" && raw !== null) {
444
+ for (const [key, value] of Object.entries(raw)) {
445
+ if (!invalidTopLevelKeys.has(key)) {
446
+ sanitized[key] = value;
447
+ }
448
+ }
449
+ }
450
+ const lines = result.error.issues.map((issue) => {
451
+ const path8 = issue.path.length > 0 ? issue.path.join(".") : "root";
452
+ return ` \u2022 ${path8}: ${issue.message}`;
453
+ });
454
+ return {
455
+ sanitized,
456
+ error: `Invalid config:
457
+ ${lines.join("\n")}`
458
+ };
459
+ }
460
+
281
461
  // src/core.ts
282
- var PAUSED_FILE = import_path2.default.join(import_os.default.homedir(), ".node9", "PAUSED");
283
- var TRUST_FILE = import_path2.default.join(import_os.default.homedir(), ".node9", "trust.json");
284
- var LOCAL_AUDIT_LOG = import_path2.default.join(import_os.default.homedir(), ".node9", "audit.log");
285
- var HOOK_DEBUG_LOG = import_path2.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
462
+ var PAUSED_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "PAUSED");
463
+ var TRUST_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "trust.json");
464
+ var LOCAL_AUDIT_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "audit.log");
465
+ var HOOK_DEBUG_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
286
466
  function checkPause() {
287
467
  try {
288
468
  if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
@@ -300,7 +480,7 @@ function checkPause() {
300
480
  }
301
481
  }
302
482
  function atomicWriteSync(filePath, data, options) {
303
- const dir = import_path2.default.dirname(filePath);
483
+ const dir = import_path3.default.dirname(filePath);
304
484
  if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
305
485
  const tmpPath = `${filePath}.${import_os.default.hostname()}.${process.pid}.tmp`;
306
486
  import_fs.default.writeFileSync(tmpPath, data, options);
@@ -351,7 +531,7 @@ function writeTrustSession(toolName, durationMs) {
351
531
  }
352
532
  function appendToLog(logPath, entry) {
353
533
  try {
354
- const dir = import_path2.default.dirname(logPath);
534
+ const dir = import_path3.default.dirname(logPath);
355
535
  if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
356
536
  import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
357
537
  } catch {
@@ -395,9 +575,9 @@ function matchesPattern(text, patterns) {
395
575
  const withoutDotSlash = text.replace(/^\.\//, "");
396
576
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
397
577
  }
398
- function getNestedValue(obj, path7) {
578
+ function getNestedValue(obj, path8) {
399
579
  if (!obj || typeof obj !== "object") return null;
400
- return path7.split(".").reduce((prev, curr) => prev?.[curr], obj);
580
+ return path8.split(".").reduce((prev, curr) => prev?.[curr], obj);
401
581
  }
402
582
  function shouldSnapshot(toolName, args, config) {
403
583
  if (!config.settings.enableUndo) return false;
@@ -546,15 +726,10 @@ function redactSecrets(text) {
546
726
  return redacted;
547
727
  }
548
728
  var DANGEROUS_WORDS = [
549
- "drop",
550
- "truncate",
551
- "purge",
552
- "format",
553
- "destroy",
554
- "terminate",
555
- "revoke",
556
- "docker",
557
- "psql"
729
+ "mkfs",
730
+ // formats/wipes a filesystem partition
731
+ "shred"
732
+ // permanently overwrites file contents (unrecoverable)
558
733
  ];
559
734
  var DEFAULT_CONFIG = {
560
735
  settings: {
@@ -611,6 +786,8 @@ var DEFAULT_CONFIG = {
611
786
  ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
612
787
  },
613
788
  rules: [
789
+ // Only use the legacy rules format for simple path-based rm control.
790
+ // All other command-level enforcement lives in smartRules below.
614
791
  {
615
792
  action: "rm",
616
793
  allowPaths: [
@@ -627,6 +804,7 @@ var DEFAULT_CONFIG = {
627
804
  }
628
805
  ],
629
806
  smartRules: [
807
+ // ── SQL safety ────────────────────────────────────────────────────────
630
808
  {
631
809
  name: "no-delete-without-where",
632
810
  tool: "*",
@@ -637,6 +815,84 @@ var DEFAULT_CONFIG = {
637
815
  conditionMode: "all",
638
816
  verdict: "review",
639
817
  reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
818
+ },
819
+ {
820
+ name: "review-drop-truncate-shell",
821
+ tool: "bash",
822
+ conditions: [
823
+ {
824
+ field: "command",
825
+ op: "matches",
826
+ value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
827
+ flags: "i"
828
+ }
829
+ ],
830
+ conditionMode: "all",
831
+ verdict: "review",
832
+ reason: "SQL DDL destructive statement inside a shell command"
833
+ },
834
+ // ── Git safety ────────────────────────────────────────────────────────
835
+ {
836
+ name: "block-force-push",
837
+ tool: "bash",
838
+ conditions: [
839
+ {
840
+ field: "command",
841
+ op: "matches",
842
+ value: "git push.*(--force|--force-with-lease|-f\\b)",
843
+ flags: "i"
844
+ }
845
+ ],
846
+ conditionMode: "all",
847
+ verdict: "block",
848
+ reason: "Force push overwrites remote history and cannot be undone"
849
+ },
850
+ {
851
+ name: "review-git-push",
852
+ tool: "bash",
853
+ conditions: [{ field: "command", op: "matches", value: "^\\s*git push\\b", flags: "i" }],
854
+ conditionMode: "all",
855
+ verdict: "review",
856
+ reason: "git push sends changes to a shared remote"
857
+ },
858
+ {
859
+ name: "review-git-destructive",
860
+ tool: "bash",
861
+ conditions: [
862
+ {
863
+ field: "command",
864
+ op: "matches",
865
+ value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
866
+ flags: "i"
867
+ }
868
+ ],
869
+ conditionMode: "all",
870
+ verdict: "review",
871
+ reason: "Destructive git operation \u2014 discards history or working-tree changes"
872
+ },
873
+ // ── Shell safety ──────────────────────────────────────────────────────
874
+ {
875
+ name: "review-sudo",
876
+ tool: "bash",
877
+ conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
878
+ conditionMode: "all",
879
+ verdict: "review",
880
+ reason: "Command requires elevated privileges"
881
+ },
882
+ {
883
+ name: "review-curl-pipe-shell",
884
+ tool: "bash",
885
+ conditions: [
886
+ {
887
+ field: "command",
888
+ op: "matches",
889
+ value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
890
+ flags: "i"
891
+ }
892
+ ],
893
+ conditionMode: "all",
894
+ verdict: "block",
895
+ reason: "Piping remote script into a shell is a supply-chain attack vector"
640
896
  }
641
897
  ]
642
898
  },
@@ -648,7 +904,7 @@ function _resetConfigCache() {
648
904
  }
649
905
  function getGlobalSettings() {
650
906
  try {
651
- const globalConfigPath = import_path2.default.join(import_os.default.homedir(), ".node9", "config.json");
907
+ const globalConfigPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
652
908
  if (import_fs.default.existsSync(globalConfigPath)) {
653
909
  const parsed = JSON.parse(import_fs.default.readFileSync(globalConfigPath, "utf-8"));
654
910
  const settings = parsed.settings || {};
@@ -672,7 +928,7 @@ function getGlobalSettings() {
672
928
  }
673
929
  function getInternalToken() {
674
930
  try {
675
- const pidFile = import_path2.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
931
+ const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
676
932
  if (!import_fs.default.existsSync(pidFile)) return null;
677
933
  const data = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
678
934
  process.kill(data.pid, 0);
@@ -693,7 +949,9 @@ async function evaluatePolicy(toolName, args, agent) {
693
949
  return {
694
950
  decision: matchedRule.verdict,
695
951
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
696
- reason: matchedRule.reason
952
+ reason: matchedRule.reason,
953
+ tier: 2,
954
+ ruleName: matchedRule.name ?? matchedRule.tool
697
955
  };
698
956
  }
699
957
  }
@@ -708,7 +966,7 @@ async function evaluatePolicy(toolName, args, agent) {
708
966
  pathTokens = analyzed.paths;
709
967
  const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
710
968
  if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
711
- return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)" };
969
+ return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
712
970
  }
713
971
  if (isSqlTool(toolName, config.policy.toolInspection)) {
714
972
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
@@ -731,7 +989,7 @@ async function evaluatePolicy(toolName, args, agent) {
731
989
  );
732
990
  const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
733
991
  if (hasSystemDisaster || isRootWipe) {
734
- return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
992
+ return { decision: "review", blockedByLabel: "Manual Nuclear Protection", tier: 3 };
735
993
  }
736
994
  return { decision: "allow" };
737
995
  }
@@ -749,14 +1007,16 @@ async function evaluatePolicy(toolName, args, agent) {
749
1007
  if (anyBlocked)
750
1008
  return {
751
1009
  decision: "review",
752
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
1010
+ blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
1011
+ tier: 5
753
1012
  };
754
1013
  const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
755
1014
  if (allAllowed) return { decision: "allow" };
756
1015
  }
757
1016
  return {
758
1017
  decision: "review",
759
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
1018
+ blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
1019
+ tier: 5
760
1020
  };
761
1021
  }
762
1022
  }
@@ -798,21 +1058,22 @@ async function evaluatePolicy(toolName, args, agent) {
798
1058
  decision: "review",
799
1059
  blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
800
1060
  matchedWord: matchedDangerousWord,
801
- matchedField
1061
+ matchedField,
1062
+ tier: 6
802
1063
  };
803
1064
  }
804
1065
  if (config.settings.mode === "strict") {
805
1066
  const envConfig = getActiveEnvironment(config);
806
1067
  if (envConfig?.requireApproval === false) return { decision: "allow" };
807
- return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)" };
1068
+ return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)", tier: 7 };
808
1069
  }
809
1070
  return { decision: "allow" };
810
1071
  }
811
1072
  async function explainPolicy(toolName, args) {
812
1073
  const steps = [];
813
- const globalPath = import_path2.default.join(import_os.default.homedir(), ".node9", "config.json");
814
- const projectPath = import_path2.default.join(process.cwd(), "node9.config.json");
815
- const credsPath = import_path2.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1074
+ const globalPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
1075
+ const projectPath = import_path3.default.join(process.cwd(), "node9.config.json");
1076
+ const credsPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
816
1077
  const waterfall = [
817
1078
  {
818
1079
  tier: 1,
@@ -1116,7 +1377,7 @@ var DAEMON_PORT = 7391;
1116
1377
  var DAEMON_HOST = "127.0.0.1";
1117
1378
  function isDaemonRunning() {
1118
1379
  try {
1119
- const pidFile = import_path2.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
1380
+ const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
1120
1381
  if (!import_fs.default.existsSync(pidFile)) return false;
1121
1382
  const { pid, port } = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
1122
1383
  if (port !== DAEMON_PORT) return false;
@@ -1128,7 +1389,7 @@ function isDaemonRunning() {
1128
1389
  }
1129
1390
  function getPersistentDecision(toolName) {
1130
1391
  try {
1131
- const file = import_path2.default.join(import_os.default.homedir(), ".node9", "decisions.json");
1392
+ const file = import_path3.default.join(import_os.default.homedir(), ".node9", "decisions.json");
1132
1393
  if (!import_fs.default.existsSync(file)) return null;
1133
1394
  const decisions = JSON.parse(import_fs.default.readFileSync(file, "utf-8"));
1134
1395
  const d = decisions[toolName];
@@ -1137,7 +1398,7 @@ function getPersistentDecision(toolName) {
1137
1398
  }
1138
1399
  return null;
1139
1400
  }
1140
- async function askDaemon(toolName, args, meta, signal) {
1401
+ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1141
1402
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1142
1403
  const checkCtrl = new AbortController();
1143
1404
  const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
@@ -1147,7 +1408,13 @@ async function askDaemon(toolName, args, meta, signal) {
1147
1408
  const checkRes = await fetch(`${base}/check`, {
1148
1409
  method: "POST",
1149
1410
  headers: { "Content-Type": "application/json" },
1150
- body: JSON.stringify({ toolName, args, agent: meta?.agent, mcpServer: meta?.mcpServer }),
1411
+ body: JSON.stringify({
1412
+ toolName,
1413
+ args,
1414
+ agent: meta?.agent,
1415
+ mcpServer: meta?.mcpServer,
1416
+ ...riskMetadata && { riskMetadata }
1417
+ }),
1151
1418
  signal: checkCtrl.signal
1152
1419
  });
1153
1420
  if (!checkRes.ok) throw new Error("Daemon fail");
@@ -1172,7 +1439,7 @@ async function askDaemon(toolName, args, meta, signal) {
1172
1439
  if (signal) signal.removeEventListener("abort", onAbort);
1173
1440
  }
1174
1441
  }
1175
- async function notifyDaemonViewer(toolName, args, meta) {
1442
+ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
1176
1443
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1177
1444
  const res = await fetch(`${base}/check`, {
1178
1445
  method: "POST",
@@ -1182,7 +1449,8 @@ async function notifyDaemonViewer(toolName, args, meta) {
1182
1449
  args,
1183
1450
  slackDelegated: true,
1184
1451
  agent: meta?.agent,
1185
- mcpServer: meta?.mcpServer
1452
+ mcpServer: meta?.mcpServer,
1453
+ ...riskMetadata && { riskMetadata }
1186
1454
  }),
1187
1455
  signal: AbortSignal.timeout(3e3)
1188
1456
  });
@@ -1221,11 +1489,15 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1221
1489
  let explainableLabel = "Local Config";
1222
1490
  let policyMatchedField;
1223
1491
  let policyMatchedWord;
1492
+ let riskMetadata;
1224
1493
  if (config.settings.mode === "audit") {
1225
1494
  if (!isIgnoredTool(toolName)) {
1226
1495
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
1227
1496
  if (policyResult.decision === "review") {
1228
1497
  appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
1498
+ if (approvers.cloud && creds?.apiKey) {
1499
+ await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
1500
+ }
1229
1501
  sendDesktopNotification(
1230
1502
  "Node9 Audit Mode",
1231
1503
  `Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
@@ -1236,13 +1508,15 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1236
1508
  }
1237
1509
  if (!isIgnoredTool(toolName)) {
1238
1510
  if (getActiveTrustSession(toolName)) {
1239
- if (creds?.apiKey) auditLocalAllow(toolName, args, "trust", creds, meta);
1511
+ if (approvers.cloud && creds?.apiKey)
1512
+ await auditLocalAllow(toolName, args, "trust", creds, meta);
1240
1513
  if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
1241
1514
  return { approved: true, checkedBy: "trust" };
1242
1515
  }
1243
1516
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
1244
1517
  if (policyResult.decision === "allow") {
1245
- if (creds?.apiKey) auditLocalAllow(toolName, args, "local-policy", creds, meta);
1518
+ if (approvers.cloud && creds?.apiKey)
1519
+ auditLocalAllow(toolName, args, "local-policy", creds, meta);
1246
1520
  if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
1247
1521
  return { approved: true, checkedBy: "local-policy" };
1248
1522
  }
@@ -1258,9 +1532,18 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1258
1532
  explainableLabel = policyResult.blockedByLabel || "Local Config";
1259
1533
  policyMatchedField = policyResult.matchedField;
1260
1534
  policyMatchedWord = policyResult.matchedWord;
1535
+ riskMetadata = computeRiskMetadata(
1536
+ args,
1537
+ policyResult.tier ?? 6,
1538
+ explainableLabel,
1539
+ policyMatchedField,
1540
+ policyMatchedWord,
1541
+ policyResult.ruleName
1542
+ );
1261
1543
  const persistent = getPersistentDecision(toolName);
1262
1544
  if (persistent === "allow") {
1263
- if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
1545
+ if (approvers.cloud && creds?.apiKey)
1546
+ await auditLocalAllow(toolName, args, "persistent", creds, meta);
1264
1547
  if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
1265
1548
  return { approved: true, checkedBy: "persistent" };
1266
1549
  }
@@ -1274,7 +1557,6 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1274
1557
  };
1275
1558
  }
1276
1559
  } else {
1277
- if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
1278
1560
  if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
1279
1561
  return { approved: true };
1280
1562
  }
@@ -1283,8 +1565,21 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1283
1565
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
1284
1566
  if (cloudEnforced) {
1285
1567
  try {
1286
- const initResult = await initNode9SaaS(toolName, args, creds, meta);
1568
+ const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
1287
1569
  if (!initResult.pending) {
1570
+ if (initResult.shadowMode) {
1571
+ console.error(
1572
+ import_chalk2.default.yellow(
1573
+ `
1574
+ \u26A0\uFE0F Node9 Shadow Mode: Action allowed, but would have been blocked by company policy.`
1575
+ )
1576
+ );
1577
+ if (initResult.shadowReason) {
1578
+ console.error(import_chalk2.default.dim(` Reason: ${initResult.shadowReason}
1579
+ `));
1580
+ }
1581
+ return { approved: true, checkedBy: "cloud" };
1582
+ }
1288
1583
  return {
1289
1584
  approved: !!initResult.approved,
1290
1585
  reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
@@ -1309,18 +1604,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1309
1604
  );
1310
1605
  }
1311
1606
  }
1312
- if (cloudEnforced && cloudRequestId) {
1313
- console.error(
1314
- import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
1315
- );
1316
- console.error(import_chalk2.default.cyan(" Dashboard \u2192 ") + import_chalk2.default.bold("Mission Control > Activity Feed\n"));
1317
- } else if (!cloudEnforced) {
1318
- const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
1319
- console.error(
1320
- import_chalk2.default.dim(`
1607
+ if (!options?.calledFromDaemon) {
1608
+ if (cloudEnforced && cloudRequestId) {
1609
+ console.error(
1610
+ import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
1611
+ );
1612
+ console.error(
1613
+ import_chalk2.default.cyan(" Dashboard \u2192 ") + import_chalk2.default.bold("Mission Control > Activity Feed\n")
1614
+ );
1615
+ } else if (!cloudEnforced) {
1616
+ const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
1617
+ console.error(
1618
+ import_chalk2.default.dim(`
1321
1619
  \u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
1322
1620
  `)
1323
- );
1621
+ );
1622
+ }
1324
1623
  }
1325
1624
  const abortController = new AbortController();
1326
1625
  const { signal } = abortController;
@@ -1351,7 +1650,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1351
1650
  (async () => {
1352
1651
  try {
1353
1652
  if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
1354
- viewerId = await notifyDaemonViewer(toolName, args, meta).catch(() => null);
1653
+ viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
1654
+ () => null
1655
+ );
1355
1656
  }
1356
1657
  const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
1357
1658
  return {
@@ -1369,7 +1670,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1369
1670
  })()
1370
1671
  );
1371
1672
  }
1372
- if (approvers.native && !isManual) {
1673
+ if (approvers.native && !isManual && !options?.calledFromDaemon) {
1373
1674
  racePromises.push(
1374
1675
  (async () => {
1375
1676
  const decision = await askNativePopup(
@@ -1397,7 +1698,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1397
1698
  })()
1398
1699
  );
1399
1700
  }
1400
- if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon) {
1701
+ if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon && !cloudEnforced) {
1401
1702
  racePromises.push(
1402
1703
  (async () => {
1403
1704
  try {
@@ -1408,7 +1709,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1408
1709
  console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
1409
1710
  `));
1410
1711
  }
1411
- const daemonDecision = await askDaemon(toolName, args, meta, signal);
1712
+ const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
1412
1713
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
1413
1714
  const isApproved = daemonDecision === "allow";
1414
1715
  return {
@@ -1533,8 +1834,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1533
1834
  }
1534
1835
  function getConfig() {
1535
1836
  if (cachedConfig) return cachedConfig;
1536
- const globalPath = import_path2.default.join(import_os.default.homedir(), ".node9", "config.json");
1537
- const projectPath = import_path2.default.join(process.cwd(), "node9.config.json");
1837
+ const globalPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
1838
+ const projectPath = import_path3.default.join(process.cwd(), "node9.config.json");
1538
1839
  const globalConfig = tryLoadConfig(globalPath);
1539
1840
  const projectConfig = tryLoadConfig(projectPath);
1540
1841
  const mergedSettings = {
@@ -1598,11 +1899,33 @@ function getConfig() {
1598
1899
  }
1599
1900
  function tryLoadConfig(filePath) {
1600
1901
  if (!import_fs.default.existsSync(filePath)) return null;
1902
+ let raw;
1601
1903
  try {
1602
- return JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
1603
- } catch {
1904
+ raw = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
1905
+ } catch (err) {
1906
+ const msg = err instanceof Error ? err.message : String(err);
1907
+ process.stderr.write(
1908
+ `
1909
+ \u26A0\uFE0F Node9: Failed to parse ${filePath}
1910
+ ${msg}
1911
+ \u2192 Using default config
1912
+
1913
+ `
1914
+ );
1604
1915
  return null;
1605
1916
  }
1917
+ const { sanitized, error } = sanitizeConfig(raw);
1918
+ if (error) {
1919
+ process.stderr.write(
1920
+ `
1921
+ \u26A0\uFE0F Node9: Invalid config at ${filePath}:
1922
+ ${error.replace("Invalid config:\n", "")}
1923
+ \u2192 Invalid fields ignored, using defaults for those keys
1924
+
1925
+ `
1926
+ );
1927
+ }
1928
+ return sanitized;
1606
1929
  }
1607
1930
  function getActiveEnvironment(config) {
1608
1931
  const env = config.settings.environment || process.env.NODE_ENV || "development";
@@ -1617,7 +1940,7 @@ function getCredentials() {
1617
1940
  };
1618
1941
  }
1619
1942
  try {
1620
- const credPath = import_path2.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1943
+ const credPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1621
1944
  if (import_fs.default.existsSync(credPath)) {
1622
1945
  const creds = JSON.parse(import_fs.default.readFileSync(credPath, "utf-8"));
1623
1946
  const profileName = process.env.NODE9_PROFILE || "default";
@@ -1640,9 +1963,7 @@ function getCredentials() {
1640
1963
  return null;
1641
1964
  }
1642
1965
  function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1643
- const controller = new AbortController();
1644
- setTimeout(() => controller.abort(), 5e3);
1645
- fetch(`${creds.apiUrl}/audit`, {
1966
+ return fetch(`${creds.apiUrl}/audit`, {
1646
1967
  method: "POST",
1647
1968
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
1648
1969
  body: JSON.stringify({
@@ -1657,11 +1978,12 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1657
1978
  platform: import_os.default.platform()
1658
1979
  }
1659
1980
  }),
1660
- signal: controller.signal
1981
+ signal: AbortSignal.timeout(5e3)
1982
+ }).then(() => {
1661
1983
  }).catch(() => {
1662
1984
  });
1663
1985
  }
1664
- async function initNode9SaaS(toolName, args, creds, meta) {
1986
+ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
1665
1987
  const controller = new AbortController();
1666
1988
  const timeout = setTimeout(() => controller.abort(), 1e4);
1667
1989
  try {
@@ -1677,7 +1999,8 @@ async function initNode9SaaS(toolName, args, creds, meta) {
1677
1999
  hostname: import_os.default.hostname(),
1678
2000
  cwd: process.cwd(),
1679
2001
  platform: import_os.default.platform()
1680
- }
2002
+ },
2003
+ ...riskMetadata && { riskMetadata }
1681
2004
  }),
1682
2005
  signal: controller.signal
1683
2006
  });
@@ -1735,7 +2058,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
1735
2058
 
1736
2059
  // src/setup.ts
1737
2060
  var import_fs2 = __toESM(require("fs"));
1738
- var import_path3 = __toESM(require("path"));
2061
+ var import_path4 = __toESM(require("path"));
1739
2062
  var import_os2 = __toESM(require("os"));
1740
2063
  var import_chalk3 = __toESM(require("chalk"));
1741
2064
  var import_prompts2 = require("@inquirer/prompts");
@@ -1760,14 +2083,14 @@ function readJson(filePath) {
1760
2083
  return null;
1761
2084
  }
1762
2085
  function writeJson(filePath, data) {
1763
- const dir = import_path3.default.dirname(filePath);
2086
+ const dir = import_path4.default.dirname(filePath);
1764
2087
  if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
1765
2088
  import_fs2.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
1766
2089
  }
1767
2090
  async function setupClaude() {
1768
2091
  const homeDir2 = import_os2.default.homedir();
1769
- const mcpPath = import_path3.default.join(homeDir2, ".claude.json");
1770
- const hooksPath = import_path3.default.join(homeDir2, ".claude", "settings.json");
2092
+ const mcpPath = import_path4.default.join(homeDir2, ".claude.json");
2093
+ const hooksPath = import_path4.default.join(homeDir2, ".claude", "settings.json");
1771
2094
  const claudeConfig = readJson(mcpPath) ?? {};
1772
2095
  const settings = readJson(hooksPath) ?? {};
1773
2096
  const servers = claudeConfig.mcpServers ?? {};
@@ -1842,7 +2165,7 @@ async function setupClaude() {
1842
2165
  }
1843
2166
  async function setupGemini() {
1844
2167
  const homeDir2 = import_os2.default.homedir();
1845
- const settingsPath = import_path3.default.join(homeDir2, ".gemini", "settings.json");
2168
+ const settingsPath = import_path4.default.join(homeDir2, ".gemini", "settings.json");
1846
2169
  const settings = readJson(settingsPath) ?? {};
1847
2170
  const servers = settings.mcpServers ?? {};
1848
2171
  let anythingChanged = false;
@@ -1925,8 +2248,8 @@ async function setupGemini() {
1925
2248
  }
1926
2249
  async function setupCursor() {
1927
2250
  const homeDir2 = import_os2.default.homedir();
1928
- const mcpPath = import_path3.default.join(homeDir2, ".cursor", "mcp.json");
1929
- const hooksPath = import_path3.default.join(homeDir2, ".cursor", "hooks.json");
2251
+ const mcpPath = import_path4.default.join(homeDir2, ".cursor", "mcp.json");
2252
+ const hooksPath = import_path4.default.join(homeDir2, ".cursor", "hooks.json");
1930
2253
  const mcpConfig = readJson(mcpPath) ?? {};
1931
2254
  const hooksFile = readJson(hooksPath) ?? { version: 1 };
1932
2255
  const servers = mcpConfig.mcpServers ?? {};
@@ -2222,6 +2545,55 @@ var ui_default = `<!doctype html>
2222
2545
  white-space: pre-wrap;
2223
2546
  word-break: break-all;
2224
2547
  }
2548
+ /* \u2500\u2500 Context Sniper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2549
+ .sniper-header {
2550
+ display: flex;
2551
+ align-items: center;
2552
+ gap: 8px;
2553
+ flex-wrap: wrap;
2554
+ margin-bottom: 8px;
2555
+ }
2556
+ .sniper-badge {
2557
+ font-size: 11px;
2558
+ font-weight: 600;
2559
+ padding: 3px 8px;
2560
+ border-radius: 5px;
2561
+ letter-spacing: 0.02em;
2562
+ }
2563
+ .sniper-badge-edit {
2564
+ background: rgba(59, 130, 246, 0.15);
2565
+ color: #60a5fa;
2566
+ border: 1px solid rgba(59, 130, 246, 0.3);
2567
+ }
2568
+ .sniper-badge-exec {
2569
+ background: rgba(239, 68, 68, 0.12);
2570
+ color: #f87171;
2571
+ border: 1px solid rgba(239, 68, 68, 0.25);
2572
+ }
2573
+ .sniper-tier {
2574
+ font-size: 10px;
2575
+ color: var(--muted);
2576
+ font-family: 'Fira Code', monospace;
2577
+ }
2578
+ .sniper-filepath {
2579
+ font-size: 11px;
2580
+ color: #a8b3c4;
2581
+ font-family: 'Fira Code', monospace;
2582
+ margin-bottom: 6px;
2583
+ word-break: break-all;
2584
+ }
2585
+ .sniper-match {
2586
+ font-size: 11px;
2587
+ color: #a8b3c4;
2588
+ margin-bottom: 6px;
2589
+ }
2590
+ .sniper-match code {
2591
+ background: rgba(239, 68, 68, 0.15);
2592
+ color: #f87171;
2593
+ padding: 1px 5px;
2594
+ border-radius: 3px;
2595
+ font-family: 'Fira Code', monospace;
2596
+ }
2225
2597
  .actions {
2226
2598
  display: grid;
2227
2599
  grid-template-columns: 1fr 1fr;
@@ -2728,20 +3100,47 @@ var ui_default = `<!doctype html>
2728
3100
  }, 200);
2729
3101
  }
2730
3102
 
3103
+ function renderPayload(req) {
3104
+ const rm = req.riskMetadata;
3105
+ if (!rm) {
3106
+ // Fallback: raw args for requests without context sniper data
3107
+ const cmd = esc(
3108
+ String(
3109
+ req.args &&
3110
+ (req.args.command ||
3111
+ req.args.cmd ||
3112
+ req.args.script ||
3113
+ JSON.stringify(req.args, null, 2))
3114
+ )
3115
+ );
3116
+ return \`<span class="label">Input Payload</span><pre>\${cmd}</pre>\`;
3117
+ }
3118
+ const isEdit = rm.intent === 'EDIT';
3119
+ const badgeClass = isEdit ? 'sniper-badge-edit' : 'sniper-badge-exec';
3120
+ const badgeLabel = isEdit ? '\u{1F4DD} Code Edit' : '\u{1F6D1} Execution';
3121
+ const tierLabel = \`Tier \${rm.tier} \xB7 \${esc(rm.blockedByLabel)}\`;
3122
+ const fileLine =
3123
+ isEdit && rm.editFilePath
3124
+ ? \`<div class="sniper-filepath">\u{1F4C2} \${esc(rm.editFilePath)}</div>\`
3125
+ : !isEdit && rm.matchedWord
3126
+ ? \`<div class="sniper-match">Matched: <code>\${esc(rm.matchedWord)}</code>\${rm.matchedField ? \` in <code>\${esc(rm.matchedField)}</code>\` : ''}</div>\`
3127
+ : '';
3128
+ const snippetHtml = rm.contextSnippet ? \`<pre>\${esc(rm.contextSnippet)}</pre>\` : '';
3129
+ return \`
3130
+ <div class="sniper-header">
3131
+ <span class="sniper-badge \${badgeClass}">\${badgeLabel}</span>
3132
+ <span class="sniper-tier">\${tierLabel}</span>
3133
+ </div>
3134
+ \${fileLine}
3135
+ \${snippetHtml}
3136
+ \`;
3137
+ }
3138
+
2731
3139
  function addCard(req) {
2732
3140
  if (requests.has(req.id)) return;
2733
3141
  requests.add(req.id);
2734
3142
  refresh();
2735
3143
  const isSlack = !!req.slackDelegated;
2736
- const cmd = esc(
2737
- String(
2738
- req.args &&
2739
- (req.args.command ||
2740
- req.args.cmd ||
2741
- req.args.script ||
2742
- JSON.stringify(req.args, null, 2))
2743
- )
2744
- );
2745
3144
  const card = document.createElement('div');
2746
3145
  card.className = 'card' + (isSlack ? ' slack-viewer' : '');
2747
3146
  card.id = 'c-' + req.id;
@@ -2755,8 +3154,7 @@ var ui_default = `<!doctype html>
2755
3154
  </div>
2756
3155
  <div class="tool-chip">\${esc(req.toolName)}</div>
2757
3156
  \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Slack approval \u2014 view only</div>' : ''}
2758
- <span class="label">Input Payload</span>
2759
- <pre>\${cmd}</pre>
3157
+ \${renderPayload(req)}
2760
3158
  <div class="actions" id="act-\${req.id}">
2761
3159
  <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>Approve Execution</button>
2762
3160
  <button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>Block Action</button>
@@ -2964,7 +3362,7 @@ var UI_HTML_TEMPLATE = ui_default;
2964
3362
  // src/daemon/index.ts
2965
3363
  var import_http = __toESM(require("http"));
2966
3364
  var import_fs3 = __toESM(require("fs"));
2967
- var import_path4 = __toESM(require("path"));
3365
+ var import_path5 = __toESM(require("path"));
2968
3366
  var import_os3 = __toESM(require("os"));
2969
3367
  var import_child_process2 = require("child_process");
2970
3368
  var import_crypto = require("crypto");
@@ -2972,14 +3370,14 @@ var import_chalk4 = __toESM(require("chalk"));
2972
3370
  var DAEMON_PORT2 = 7391;
2973
3371
  var DAEMON_HOST2 = "127.0.0.1";
2974
3372
  var homeDir = import_os3.default.homedir();
2975
- var DAEMON_PID_FILE = import_path4.default.join(homeDir, ".node9", "daemon.pid");
2976
- var DECISIONS_FILE = import_path4.default.join(homeDir, ".node9", "decisions.json");
2977
- var GLOBAL_CONFIG_FILE = import_path4.default.join(homeDir, ".node9", "config.json");
2978
- var CREDENTIALS_FILE = import_path4.default.join(homeDir, ".node9", "credentials.json");
2979
- var AUDIT_LOG_FILE = import_path4.default.join(homeDir, ".node9", "audit.log");
2980
- var TRUST_FILE2 = import_path4.default.join(homeDir, ".node9", "trust.json");
3373
+ var DAEMON_PID_FILE = import_path5.default.join(homeDir, ".node9", "daemon.pid");
3374
+ var DECISIONS_FILE = import_path5.default.join(homeDir, ".node9", "decisions.json");
3375
+ var GLOBAL_CONFIG_FILE = import_path5.default.join(homeDir, ".node9", "config.json");
3376
+ var CREDENTIALS_FILE = import_path5.default.join(homeDir, ".node9", "credentials.json");
3377
+ var AUDIT_LOG_FILE = import_path5.default.join(homeDir, ".node9", "audit.log");
3378
+ var TRUST_FILE2 = import_path5.default.join(homeDir, ".node9", "trust.json");
2981
3379
  function atomicWriteSync2(filePath, data, options) {
2982
- const dir = import_path4.default.dirname(filePath);
3380
+ const dir = import_path5.default.dirname(filePath);
2983
3381
  if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
2984
3382
  const tmpPath = `${filePath}.${(0, import_crypto.randomUUID)()}.tmp`;
2985
3383
  import_fs3.default.writeFileSync(tmpPath, data, options);
@@ -3023,7 +3421,7 @@ function appendAuditLog(data) {
3023
3421
  decision: data.decision,
3024
3422
  source: "daemon"
3025
3423
  };
3026
- const dir = import_path4.default.dirname(AUDIT_LOG_FILE);
3424
+ const dir = import_path5.default.dirname(AUDIT_LOG_FILE);
3027
3425
  if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
3028
3426
  import_fs3.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
3029
3427
  } catch {
@@ -3181,6 +3579,7 @@ data: ${JSON.stringify({
3181
3579
  id: e.id,
3182
3580
  toolName: e.toolName,
3183
3581
  args: e.args,
3582
+ riskMetadata: e.riskMetadata,
3184
3583
  slackDelegated: e.slackDelegated,
3185
3584
  timestamp: e.timestamp,
3186
3585
  agent: e.agent,
@@ -3206,14 +3605,23 @@ data: ${JSON.stringify(readPersistentDecisions())}
3206
3605
  if (req.method === "POST" && pathname === "/check") {
3207
3606
  try {
3208
3607
  resetIdleTimer();
3608
+ _resetConfigCache();
3209
3609
  const body = await readBody(req);
3210
3610
  if (body.length > 65536) return res.writeHead(413).end();
3211
- const { toolName, args, slackDelegated = false, agent, mcpServer } = JSON.parse(body);
3611
+ const {
3612
+ toolName,
3613
+ args,
3614
+ slackDelegated = false,
3615
+ agent,
3616
+ mcpServer,
3617
+ riskMetadata
3618
+ } = JSON.parse(body);
3212
3619
  const id = (0, import_crypto.randomUUID)();
3213
3620
  const entry = {
3214
3621
  id,
3215
3622
  toolName,
3216
3623
  args,
3624
+ riskMetadata: riskMetadata ?? void 0,
3217
3625
  agent: typeof agent === "string" ? agent : void 0,
3218
3626
  mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
3219
3627
  slackDelegated: !!slackDelegated,
@@ -3245,6 +3653,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
3245
3653
  id,
3246
3654
  toolName,
3247
3655
  args,
3656
+ riskMetadata: entry.riskMetadata,
3248
3657
  slackDelegated: entry.slackDelegated,
3249
3658
  agent: entry.agent,
3250
3659
  mcpServer: entry.mcpServer
@@ -3514,16 +3923,16 @@ var import_execa2 = require("execa");
3514
3923
  var import_chalk5 = __toESM(require("chalk"));
3515
3924
  var import_readline = __toESM(require("readline"));
3516
3925
  var import_fs5 = __toESM(require("fs"));
3517
- var import_path6 = __toESM(require("path"));
3926
+ var import_path7 = __toESM(require("path"));
3518
3927
  var import_os5 = __toESM(require("os"));
3519
3928
 
3520
3929
  // src/undo.ts
3521
3930
  var import_child_process3 = require("child_process");
3522
3931
  var import_fs4 = __toESM(require("fs"));
3523
- var import_path5 = __toESM(require("path"));
3932
+ var import_path6 = __toESM(require("path"));
3524
3933
  var import_os4 = __toESM(require("os"));
3525
- var SNAPSHOT_STACK_PATH = import_path5.default.join(import_os4.default.homedir(), ".node9", "snapshots.json");
3526
- var UNDO_LATEST_PATH = import_path5.default.join(import_os4.default.homedir(), ".node9", "undo_latest.txt");
3934
+ var SNAPSHOT_STACK_PATH = import_path6.default.join(import_os4.default.homedir(), ".node9", "snapshots.json");
3935
+ var UNDO_LATEST_PATH = import_path6.default.join(import_os4.default.homedir(), ".node9", "undo_latest.txt");
3527
3936
  var MAX_SNAPSHOTS = 10;
3528
3937
  function readStack() {
3529
3938
  try {
@@ -3534,7 +3943,7 @@ function readStack() {
3534
3943
  return [];
3535
3944
  }
3536
3945
  function writeStack(stack) {
3537
- const dir = import_path5.default.dirname(SNAPSHOT_STACK_PATH);
3946
+ const dir = import_path6.default.dirname(SNAPSHOT_STACK_PATH);
3538
3947
  if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
3539
3948
  import_fs4.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
3540
3949
  }
@@ -3552,8 +3961,8 @@ function buildArgsSummary(tool, args) {
3552
3961
  async function createShadowSnapshot(tool = "unknown", args = {}) {
3553
3962
  try {
3554
3963
  const cwd = process.cwd();
3555
- if (!import_fs4.default.existsSync(import_path5.default.join(cwd, ".git"))) return null;
3556
- const tempIndex = import_path5.default.join(cwd, ".git", `node9_index_${Date.now()}`);
3964
+ if (!import_fs4.default.existsSync(import_path6.default.join(cwd, ".git"))) return null;
3965
+ const tempIndex = import_path6.default.join(cwd, ".git", `node9_index_${Date.now()}`);
3557
3966
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
3558
3967
  (0, import_child_process3.spawnSync)("git", ["add", "-A"], { env });
3559
3968
  const treeRes = (0, import_child_process3.spawnSync)("git", ["write-tree"], { env });
@@ -3617,7 +4026,7 @@ function applyUndo(hash, cwd) {
3617
4026
  const tracked = (0, import_child_process3.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
3618
4027
  const untracked = (0, import_child_process3.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
3619
4028
  for (const file of [...tracked, ...untracked]) {
3620
- const fullPath = import_path5.default.join(dir, file);
4029
+ const fullPath = import_path6.default.join(dir, file);
3621
4030
  if (!snapshotFiles.has(file) && import_fs4.default.existsSync(fullPath)) {
3622
4031
  import_fs4.default.unlinkSync(fullPath);
3623
4032
  }
@@ -3631,7 +4040,7 @@ function applyUndo(hash, cwd) {
3631
4040
  // src/cli.ts
3632
4041
  var import_prompts3 = require("@inquirer/prompts");
3633
4042
  var { version } = JSON.parse(
3634
- import_fs5.default.readFileSync(import_path6.default.join(__dirname, "../package.json"), "utf-8")
4043
+ import_fs5.default.readFileSync(import_path7.default.join(__dirname, "../package.json"), "utf-8")
3635
4044
  );
3636
4045
  function parseDuration(str) {
3637
4046
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -3824,9 +4233,9 @@ async function runProxy(targetCommand) {
3824
4233
  }
3825
4234
  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) => {
3826
4235
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
3827
- const credPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
3828
- if (!import_fs5.default.existsSync(import_path6.default.dirname(credPath)))
3829
- import_fs5.default.mkdirSync(import_path6.default.dirname(credPath), { recursive: true });
4236
+ const credPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
4237
+ if (!import_fs5.default.existsSync(import_path7.default.dirname(credPath)))
4238
+ import_fs5.default.mkdirSync(import_path7.default.dirname(credPath), { recursive: true });
3830
4239
  const profileName = options.profile || "default";
3831
4240
  let existingCreds = {};
3832
4241
  try {
@@ -3845,7 +4254,7 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
3845
4254
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
3846
4255
  import_fs5.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
3847
4256
  if (profileName === "default") {
3848
- const configPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "config.json");
4257
+ const configPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
3849
4258
  let config = {};
3850
4259
  try {
3851
4260
  if (import_fs5.default.existsSync(configPath))
@@ -3860,10 +4269,12 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
3860
4269
  cloud: true,
3861
4270
  terminal: true
3862
4271
  };
3863
- approvers.cloud = !options.local;
4272
+ if (options.local) {
4273
+ approvers.cloud = false;
4274
+ }
3864
4275
  s.approvers = approvers;
3865
- if (!import_fs5.default.existsSync(import_path6.default.dirname(configPath)))
3866
- import_fs5.default.mkdirSync(import_path6.default.dirname(configPath), { recursive: true });
4276
+ if (!import_fs5.default.existsSync(import_path7.default.dirname(configPath)))
4277
+ import_fs5.default.mkdirSync(import_path7.default.dirname(configPath), { recursive: true });
3867
4278
  import_fs5.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
3868
4279
  }
3869
4280
  if (options.profile && profileName !== "default") {
@@ -3949,7 +4360,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3949
4360
  );
3950
4361
  }
3951
4362
  section("Configuration");
3952
- const globalConfigPath = import_path6.default.join(homeDir2, ".node9", "config.json");
4363
+ const globalConfigPath = import_path7.default.join(homeDir2, ".node9", "config.json");
3953
4364
  if (import_fs5.default.existsSync(globalConfigPath)) {
3954
4365
  try {
3955
4366
  JSON.parse(import_fs5.default.readFileSync(globalConfigPath, "utf-8"));
@@ -3960,7 +4371,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3960
4371
  } else {
3961
4372
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
3962
4373
  }
3963
- const projectConfigPath = import_path6.default.join(process.cwd(), "node9.config.json");
4374
+ const projectConfigPath = import_path7.default.join(process.cwd(), "node9.config.json");
3964
4375
  if (import_fs5.default.existsSync(projectConfigPath)) {
3965
4376
  try {
3966
4377
  JSON.parse(import_fs5.default.readFileSync(projectConfigPath, "utf-8"));
@@ -3969,7 +4380,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3969
4380
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
3970
4381
  }
3971
4382
  }
3972
- const credsPath = import_path6.default.join(homeDir2, ".node9", "credentials.json");
4383
+ const credsPath = import_path7.default.join(homeDir2, ".node9", "credentials.json");
3973
4384
  if (import_fs5.default.existsSync(credsPath)) {
3974
4385
  pass("Cloud credentials found (~/.node9/credentials.json)");
3975
4386
  } else {
@@ -3979,7 +4390,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3979
4390
  );
3980
4391
  }
3981
4392
  section("Agent Hooks");
3982
- const claudeSettingsPath = import_path6.default.join(homeDir2, ".claude", "settings.json");
4393
+ const claudeSettingsPath = import_path7.default.join(homeDir2, ".claude", "settings.json");
3983
4394
  if (import_fs5.default.existsSync(claudeSettingsPath)) {
3984
4395
  try {
3985
4396
  const cs = JSON.parse(import_fs5.default.readFileSync(claudeSettingsPath, "utf-8"));
@@ -3995,7 +4406,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3995
4406
  } else {
3996
4407
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
3997
4408
  }
3998
- const geminiSettingsPath = import_path6.default.join(homeDir2, ".gemini", "settings.json");
4409
+ const geminiSettingsPath = import_path7.default.join(homeDir2, ".gemini", "settings.json");
3999
4410
  if (import_fs5.default.existsSync(geminiSettingsPath)) {
4000
4411
  try {
4001
4412
  const gs = JSON.parse(import_fs5.default.readFileSync(geminiSettingsPath, "utf-8"));
@@ -4011,7 +4422,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
4011
4422
  } else {
4012
4423
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
4013
4424
  }
4014
- const cursorHooksPath = import_path6.default.join(homeDir2, ".cursor", "hooks.json");
4425
+ const cursorHooksPath = import_path7.default.join(homeDir2, ".cursor", "hooks.json");
4015
4426
  if (import_fs5.default.existsSync(cursorHooksPath)) {
4016
4427
  try {
4017
4428
  const cur = JSON.parse(import_fs5.default.readFileSync(cursorHooksPath, "utf-8"));
@@ -4116,7 +4527,7 @@ program.command("explain").description(
4116
4527
  console.log("");
4117
4528
  });
4118
4529
  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) => {
4119
- const configPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "config.json");
4530
+ const configPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
4120
4531
  if (import_fs5.default.existsSync(configPath) && !options.force) {
4121
4532
  console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
4122
4533
  console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
@@ -4131,7 +4542,7 @@ program.command("init").description("Create ~/.node9/config.json with default po
4131
4542
  mode: safeMode
4132
4543
  }
4133
4544
  };
4134
- const dir = import_path6.default.dirname(configPath);
4545
+ const dir = import_path7.default.dirname(configPath);
4135
4546
  if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
4136
4547
  import_fs5.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
4137
4548
  console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
@@ -4151,7 +4562,7 @@ function formatRelativeTime(timestamp) {
4151
4562
  return new Date(timestamp).toLocaleDateString();
4152
4563
  }
4153
4564
  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) => {
4154
- const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "audit.log");
4565
+ const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "audit.log");
4155
4566
  if (!import_fs5.default.existsSync(logPath)) {
4156
4567
  console.log(
4157
4568
  import_chalk5.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
@@ -4241,8 +4652,8 @@ program.command("status").description("Show current Node9 mode, policy source, a
4241
4652
  console.log("");
4242
4653
  const modeLabel = settings.mode === "audit" ? import_chalk5.default.blue("audit") : settings.mode === "strict" ? import_chalk5.default.red("strict") : import_chalk5.default.white("standard");
4243
4654
  console.log(` Mode: ${modeLabel}`);
4244
- const projectConfig = import_path6.default.join(process.cwd(), "node9.config.json");
4245
- const globalConfig = import_path6.default.join(import_os5.default.homedir(), ".node9", "config.json");
4655
+ const projectConfig = import_path7.default.join(process.cwd(), "node9.config.json");
4656
+ const globalConfig = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
4246
4657
  console.log(
4247
4658
  ` Local: ${import_fs5.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
4248
4659
  );
@@ -4310,7 +4721,7 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
4310
4721
  } catch (err) {
4311
4722
  const tempConfig = getConfig();
4312
4723
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
4313
- const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4724
+ const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4314
4725
  const errMsg = err instanceof Error ? err.message : String(err);
4315
4726
  import_fs5.default.appendFileSync(
4316
4727
  logPath,
@@ -4330,9 +4741,9 @@ RAW: ${raw}
4330
4741
  }
4331
4742
  const config = getConfig();
4332
4743
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
4333
- const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4334
- if (!import_fs5.default.existsSync(import_path6.default.dirname(logPath)))
4335
- import_fs5.default.mkdirSync(import_path6.default.dirname(logPath), { recursive: true });
4744
+ const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4745
+ if (!import_fs5.default.existsSync(import_path7.default.dirname(logPath)))
4746
+ import_fs5.default.mkdirSync(import_path7.default.dirname(logPath), { recursive: true });
4336
4747
  import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
4337
4748
  `);
4338
4749
  }
@@ -4404,7 +4815,7 @@ RAW: ${raw}
4404
4815
  });
4405
4816
  } catch (err) {
4406
4817
  if (process.env.NODE9_DEBUG === "1") {
4407
- const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4818
+ const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4408
4819
  const errMsg = err instanceof Error ? err.message : String(err);
4409
4820
  import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
4410
4821
  `);
@@ -4451,9 +4862,9 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
4451
4862
  decision: "allowed",
4452
4863
  source: "post-hook"
4453
4864
  };
4454
- const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "audit.log");
4455
- if (!import_fs5.default.existsSync(import_path6.default.dirname(logPath)))
4456
- import_fs5.default.mkdirSync(import_path6.default.dirname(logPath), { recursive: true });
4865
+ const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "audit.log");
4866
+ if (!import_fs5.default.existsSync(import_path7.default.dirname(logPath)))
4867
+ import_fs5.default.mkdirSync(import_path7.default.dirname(logPath), { recursive: true });
4457
4868
  import_fs5.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
4458
4869
  const config = getConfig();
4459
4870
  if (shouldSnapshot(tool, {}, config)) {
@@ -4628,7 +5039,7 @@ process.on("unhandledRejection", (reason) => {
4628
5039
  const isCheckHook = process.argv[2] === "check";
4629
5040
  if (isCheckHook) {
4630
5041
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
4631
- const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
5042
+ const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4632
5043
  const msg = reason instanceof Error ? reason.message : String(reason);
4633
5044
  import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
4634
5045
  `);