@node9/proxy 1.0.8 → 1.0.10

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 };
70
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
+ };
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\\s+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 = {
@@ -1554,6 +1855,7 @@ function getConfig() {
1554
1855
  ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
1555
1856
  }
1556
1857
  };
1858
+ const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
1557
1859
  const applyLayer = (source) => {
1558
1860
  if (!source) return;
1559
1861
  const s = source.settings || {};
@@ -1579,6 +1881,17 @@ function getConfig() {
1579
1881
  if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
1580
1882
  if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
1581
1883
  }
1884
+ const envs = source.environments || {};
1885
+ for (const [envName, envConfig] of Object.entries(envs)) {
1886
+ if (envConfig && typeof envConfig === "object") {
1887
+ const ec = envConfig;
1888
+ mergedEnvironments[envName] = {
1889
+ ...mergedEnvironments[envName],
1890
+ // Validate field types before merging — do not blindly spread user input
1891
+ ...typeof ec.requireApproval === "boolean" ? { requireApproval: ec.requireApproval } : {}
1892
+ };
1893
+ }
1894
+ }
1582
1895
  };
1583
1896
  applyLayer(globalConfig);
1584
1897
  applyLayer(projectConfig);
@@ -1592,17 +1905,62 @@ function getConfig() {
1592
1905
  cachedConfig = {
1593
1906
  settings: mergedSettings,
1594
1907
  policy: mergedPolicy,
1595
- environments: {}
1908
+ environments: mergedEnvironments
1596
1909
  };
1597
1910
  return cachedConfig;
1598
1911
  }
1599
1912
  function tryLoadConfig(filePath) {
1600
1913
  if (!import_fs.default.existsSync(filePath)) return null;
1914
+ let raw;
1601
1915
  try {
1602
- return JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
1603
- } catch {
1916
+ raw = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
1917
+ } catch (err) {
1918
+ const msg = err instanceof Error ? err.message : String(err);
1919
+ process.stderr.write(
1920
+ `
1921
+ \u26A0\uFE0F Node9: Failed to parse ${filePath}
1922
+ ${msg}
1923
+ \u2192 Using default config
1924
+
1925
+ `
1926
+ );
1604
1927
  return null;
1605
1928
  }
1929
+ const SUPPORTED_VERSION = "1.0";
1930
+ const SUPPORTED_MAJOR = SUPPORTED_VERSION.split(".")[0];
1931
+ const fileVersion = raw?.version;
1932
+ if (fileVersion !== void 0) {
1933
+ const vStr = String(fileVersion);
1934
+ const fileMajor = vStr.split(".")[0];
1935
+ if (fileMajor !== SUPPORTED_MAJOR) {
1936
+ process.stderr.write(
1937
+ `
1938
+ \u274C Node9: Config at ${filePath} has version "${vStr}" \u2014 major version is incompatible with this release (expected "${SUPPORTED_VERSION}"). Config will not be loaded.
1939
+
1940
+ `
1941
+ );
1942
+ return null;
1943
+ } else if (vStr !== SUPPORTED_VERSION) {
1944
+ process.stderr.write(
1945
+ `
1946
+ \u26A0\uFE0F Node9: Config at ${filePath} declares version "${vStr}" \u2014 expected "${SUPPORTED_VERSION}". Continuing with best-effort parsing.
1947
+
1948
+ `
1949
+ );
1950
+ }
1951
+ }
1952
+ const { sanitized, error } = sanitizeConfig(raw);
1953
+ if (error) {
1954
+ process.stderr.write(
1955
+ `
1956
+ \u26A0\uFE0F Node9: Invalid config at ${filePath}:
1957
+ ${error.replace("Invalid config:\n", "")}
1958
+ \u2192 Invalid fields ignored, using defaults for those keys
1959
+
1960
+ `
1961
+ );
1962
+ }
1963
+ return sanitized;
1606
1964
  }
1607
1965
  function getActiveEnvironment(config) {
1608
1966
  const env = config.settings.environment || process.env.NODE_ENV || "development";
@@ -1617,7 +1975,7 @@ function getCredentials() {
1617
1975
  };
1618
1976
  }
1619
1977
  try {
1620
- const credPath = import_path2.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1978
+ const credPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1621
1979
  if (import_fs.default.existsSync(credPath)) {
1622
1980
  const creds = JSON.parse(import_fs.default.readFileSync(credPath, "utf-8"));
1623
1981
  const profileName = process.env.NODE9_PROFILE || "default";
@@ -1640,9 +1998,7 @@ function getCredentials() {
1640
1998
  return null;
1641
1999
  }
1642
2000
  function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1643
- const controller = new AbortController();
1644
- setTimeout(() => controller.abort(), 5e3);
1645
- fetch(`${creds.apiUrl}/audit`, {
2001
+ return fetch(`${creds.apiUrl}/audit`, {
1646
2002
  method: "POST",
1647
2003
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
1648
2004
  body: JSON.stringify({
@@ -1657,11 +2013,12 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1657
2013
  platform: import_os.default.platform()
1658
2014
  }
1659
2015
  }),
1660
- signal: controller.signal
2016
+ signal: AbortSignal.timeout(5e3)
2017
+ }).then(() => {
1661
2018
  }).catch(() => {
1662
2019
  });
1663
2020
  }
1664
- async function initNode9SaaS(toolName, args, creds, meta) {
2021
+ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
1665
2022
  const controller = new AbortController();
1666
2023
  const timeout = setTimeout(() => controller.abort(), 1e4);
1667
2024
  try {
@@ -1677,7 +2034,8 @@ async function initNode9SaaS(toolName, args, creds, meta) {
1677
2034
  hostname: import_os.default.hostname(),
1678
2035
  cwd: process.cwd(),
1679
2036
  platform: import_os.default.platform()
1680
- }
2037
+ },
2038
+ ...riskMetadata && { riskMetadata }
1681
2039
  }),
1682
2040
  signal: controller.signal
1683
2041
  });
@@ -1735,7 +2093,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
1735
2093
 
1736
2094
  // src/setup.ts
1737
2095
  var import_fs2 = __toESM(require("fs"));
1738
- var import_path3 = __toESM(require("path"));
2096
+ var import_path4 = __toESM(require("path"));
1739
2097
  var import_os2 = __toESM(require("os"));
1740
2098
  var import_chalk3 = __toESM(require("chalk"));
1741
2099
  var import_prompts2 = require("@inquirer/prompts");
@@ -1760,14 +2118,14 @@ function readJson(filePath) {
1760
2118
  return null;
1761
2119
  }
1762
2120
  function writeJson(filePath, data) {
1763
- const dir = import_path3.default.dirname(filePath);
2121
+ const dir = import_path4.default.dirname(filePath);
1764
2122
  if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
1765
2123
  import_fs2.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
1766
2124
  }
1767
2125
  async function setupClaude() {
1768
2126
  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");
2127
+ const mcpPath = import_path4.default.join(homeDir2, ".claude.json");
2128
+ const hooksPath = import_path4.default.join(homeDir2, ".claude", "settings.json");
1771
2129
  const claudeConfig = readJson(mcpPath) ?? {};
1772
2130
  const settings = readJson(hooksPath) ?? {};
1773
2131
  const servers = claudeConfig.mcpServers ?? {};
@@ -1842,7 +2200,7 @@ async function setupClaude() {
1842
2200
  }
1843
2201
  async function setupGemini() {
1844
2202
  const homeDir2 = import_os2.default.homedir();
1845
- const settingsPath = import_path3.default.join(homeDir2, ".gemini", "settings.json");
2203
+ const settingsPath = import_path4.default.join(homeDir2, ".gemini", "settings.json");
1846
2204
  const settings = readJson(settingsPath) ?? {};
1847
2205
  const servers = settings.mcpServers ?? {};
1848
2206
  let anythingChanged = false;
@@ -1925,8 +2283,8 @@ async function setupGemini() {
1925
2283
  }
1926
2284
  async function setupCursor() {
1927
2285
  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");
2286
+ const mcpPath = import_path4.default.join(homeDir2, ".cursor", "mcp.json");
2287
+ const hooksPath = import_path4.default.join(homeDir2, ".cursor", "hooks.json");
1930
2288
  const mcpConfig = readJson(mcpPath) ?? {};
1931
2289
  const hooksFile = readJson(hooksPath) ?? { version: 1 };
1932
2290
  const servers = mcpConfig.mcpServers ?? {};
@@ -2222,6 +2580,55 @@ var ui_default = `<!doctype html>
2222
2580
  white-space: pre-wrap;
2223
2581
  word-break: break-all;
2224
2582
  }
2583
+ /* \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 */
2584
+ .sniper-header {
2585
+ display: flex;
2586
+ align-items: center;
2587
+ gap: 8px;
2588
+ flex-wrap: wrap;
2589
+ margin-bottom: 8px;
2590
+ }
2591
+ .sniper-badge {
2592
+ font-size: 11px;
2593
+ font-weight: 600;
2594
+ padding: 3px 8px;
2595
+ border-radius: 5px;
2596
+ letter-spacing: 0.02em;
2597
+ }
2598
+ .sniper-badge-edit {
2599
+ background: rgba(59, 130, 246, 0.15);
2600
+ color: #60a5fa;
2601
+ border: 1px solid rgba(59, 130, 246, 0.3);
2602
+ }
2603
+ .sniper-badge-exec {
2604
+ background: rgba(239, 68, 68, 0.12);
2605
+ color: #f87171;
2606
+ border: 1px solid rgba(239, 68, 68, 0.25);
2607
+ }
2608
+ .sniper-tier {
2609
+ font-size: 10px;
2610
+ color: var(--muted);
2611
+ font-family: 'Fira Code', monospace;
2612
+ }
2613
+ .sniper-filepath {
2614
+ font-size: 11px;
2615
+ color: #a8b3c4;
2616
+ font-family: 'Fira Code', monospace;
2617
+ margin-bottom: 6px;
2618
+ word-break: break-all;
2619
+ }
2620
+ .sniper-match {
2621
+ font-size: 11px;
2622
+ color: #a8b3c4;
2623
+ margin-bottom: 6px;
2624
+ }
2625
+ .sniper-match code {
2626
+ background: rgba(239, 68, 68, 0.15);
2627
+ color: #f87171;
2628
+ padding: 1px 5px;
2629
+ border-radius: 3px;
2630
+ font-family: 'Fira Code', monospace;
2631
+ }
2225
2632
  .actions {
2226
2633
  display: grid;
2227
2634
  grid-template-columns: 1fr 1fr;
@@ -2728,20 +3135,47 @@ var ui_default = `<!doctype html>
2728
3135
  }, 200);
2729
3136
  }
2730
3137
 
3138
+ function renderPayload(req) {
3139
+ const rm = req.riskMetadata;
3140
+ if (!rm) {
3141
+ // Fallback: raw args for requests without context sniper data
3142
+ const cmd = esc(
3143
+ String(
3144
+ req.args &&
3145
+ (req.args.command ||
3146
+ req.args.cmd ||
3147
+ req.args.script ||
3148
+ JSON.stringify(req.args, null, 2))
3149
+ )
3150
+ );
3151
+ return \`<span class="label">Input Payload</span><pre>\${cmd}</pre>\`;
3152
+ }
3153
+ const isEdit = rm.intent === 'EDIT';
3154
+ const badgeClass = isEdit ? 'sniper-badge-edit' : 'sniper-badge-exec';
3155
+ const badgeLabel = isEdit ? '\u{1F4DD} Code Edit' : '\u{1F6D1} Execution';
3156
+ const tierLabel = \`Tier \${rm.tier} \xB7 \${esc(rm.blockedByLabel)}\`;
3157
+ const fileLine =
3158
+ isEdit && rm.editFilePath
3159
+ ? \`<div class="sniper-filepath">\u{1F4C2} \${esc(rm.editFilePath)}</div>\`
3160
+ : !isEdit && rm.matchedWord
3161
+ ? \`<div class="sniper-match">Matched: <code>\${esc(rm.matchedWord)}</code>\${rm.matchedField ? \` in <code>\${esc(rm.matchedField)}</code>\` : ''}</div>\`
3162
+ : '';
3163
+ const snippetHtml = rm.contextSnippet ? \`<pre>\${esc(rm.contextSnippet)}</pre>\` : '';
3164
+ return \`
3165
+ <div class="sniper-header">
3166
+ <span class="sniper-badge \${badgeClass}">\${badgeLabel}</span>
3167
+ <span class="sniper-tier">\${tierLabel}</span>
3168
+ </div>
3169
+ \${fileLine}
3170
+ \${snippetHtml}
3171
+ \`;
3172
+ }
3173
+
2731
3174
  function addCard(req) {
2732
3175
  if (requests.has(req.id)) return;
2733
3176
  requests.add(req.id);
2734
3177
  refresh();
2735
3178
  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
3179
  const card = document.createElement('div');
2746
3180
  card.className = 'card' + (isSlack ? ' slack-viewer' : '');
2747
3181
  card.id = 'c-' + req.id;
@@ -2755,8 +3189,7 @@ var ui_default = `<!doctype html>
2755
3189
  </div>
2756
3190
  <div class="tool-chip">\${esc(req.toolName)}</div>
2757
3191
  \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Slack approval \u2014 view only</div>' : ''}
2758
- <span class="label">Input Payload</span>
2759
- <pre>\${cmd}</pre>
3192
+ \${renderPayload(req)}
2760
3193
  <div class="actions" id="act-\${req.id}">
2761
3194
  <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>Approve Execution</button>
2762
3195
  <button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>Block Action</button>
@@ -2964,7 +3397,7 @@ var UI_HTML_TEMPLATE = ui_default;
2964
3397
  // src/daemon/index.ts
2965
3398
  var import_http = __toESM(require("http"));
2966
3399
  var import_fs3 = __toESM(require("fs"));
2967
- var import_path4 = __toESM(require("path"));
3400
+ var import_path5 = __toESM(require("path"));
2968
3401
  var import_os3 = __toESM(require("os"));
2969
3402
  var import_child_process2 = require("child_process");
2970
3403
  var import_crypto = require("crypto");
@@ -2972,14 +3405,14 @@ var import_chalk4 = __toESM(require("chalk"));
2972
3405
  var DAEMON_PORT2 = 7391;
2973
3406
  var DAEMON_HOST2 = "127.0.0.1";
2974
3407
  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");
3408
+ var DAEMON_PID_FILE = import_path5.default.join(homeDir, ".node9", "daemon.pid");
3409
+ var DECISIONS_FILE = import_path5.default.join(homeDir, ".node9", "decisions.json");
3410
+ var GLOBAL_CONFIG_FILE = import_path5.default.join(homeDir, ".node9", "config.json");
3411
+ var CREDENTIALS_FILE = import_path5.default.join(homeDir, ".node9", "credentials.json");
3412
+ var AUDIT_LOG_FILE = import_path5.default.join(homeDir, ".node9", "audit.log");
3413
+ var TRUST_FILE2 = import_path5.default.join(homeDir, ".node9", "trust.json");
2981
3414
  function atomicWriteSync2(filePath, data, options) {
2982
- const dir = import_path4.default.dirname(filePath);
3415
+ const dir = import_path5.default.dirname(filePath);
2983
3416
  if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
2984
3417
  const tmpPath = `${filePath}.${(0, import_crypto.randomUUID)()}.tmp`;
2985
3418
  import_fs3.default.writeFileSync(tmpPath, data, options);
@@ -3023,7 +3456,7 @@ function appendAuditLog(data) {
3023
3456
  decision: data.decision,
3024
3457
  source: "daemon"
3025
3458
  };
3026
- const dir = import_path4.default.dirname(AUDIT_LOG_FILE);
3459
+ const dir = import_path5.default.dirname(AUDIT_LOG_FILE);
3027
3460
  if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
3028
3461
  import_fs3.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
3029
3462
  } catch {
@@ -3181,6 +3614,7 @@ data: ${JSON.stringify({
3181
3614
  id: e.id,
3182
3615
  toolName: e.toolName,
3183
3616
  args: e.args,
3617
+ riskMetadata: e.riskMetadata,
3184
3618
  slackDelegated: e.slackDelegated,
3185
3619
  timestamp: e.timestamp,
3186
3620
  agent: e.agent,
@@ -3206,14 +3640,23 @@ data: ${JSON.stringify(readPersistentDecisions())}
3206
3640
  if (req.method === "POST" && pathname === "/check") {
3207
3641
  try {
3208
3642
  resetIdleTimer();
3643
+ _resetConfigCache();
3209
3644
  const body = await readBody(req);
3210
3645
  if (body.length > 65536) return res.writeHead(413).end();
3211
- const { toolName, args, slackDelegated = false, agent, mcpServer } = JSON.parse(body);
3646
+ const {
3647
+ toolName,
3648
+ args,
3649
+ slackDelegated = false,
3650
+ agent,
3651
+ mcpServer,
3652
+ riskMetadata
3653
+ } = JSON.parse(body);
3212
3654
  const id = (0, import_crypto.randomUUID)();
3213
3655
  const entry = {
3214
3656
  id,
3215
3657
  toolName,
3216
3658
  args,
3659
+ riskMetadata: riskMetadata ?? void 0,
3217
3660
  agent: typeof agent === "string" ? agent : void 0,
3218
3661
  mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
3219
3662
  slackDelegated: !!slackDelegated,
@@ -3245,6 +3688,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
3245
3688
  id,
3246
3689
  toolName,
3247
3690
  args,
3691
+ riskMetadata: entry.riskMetadata,
3248
3692
  slackDelegated: entry.slackDelegated,
3249
3693
  agent: entry.agent,
3250
3694
  mcpServer: entry.mcpServer
@@ -3514,16 +3958,16 @@ var import_execa2 = require("execa");
3514
3958
  var import_chalk5 = __toESM(require("chalk"));
3515
3959
  var import_readline = __toESM(require("readline"));
3516
3960
  var import_fs5 = __toESM(require("fs"));
3517
- var import_path6 = __toESM(require("path"));
3961
+ var import_path7 = __toESM(require("path"));
3518
3962
  var import_os5 = __toESM(require("os"));
3519
3963
 
3520
3964
  // src/undo.ts
3521
3965
  var import_child_process3 = require("child_process");
3522
3966
  var import_fs4 = __toESM(require("fs"));
3523
- var import_path5 = __toESM(require("path"));
3967
+ var import_path6 = __toESM(require("path"));
3524
3968
  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");
3969
+ var SNAPSHOT_STACK_PATH = import_path6.default.join(import_os4.default.homedir(), ".node9", "snapshots.json");
3970
+ var UNDO_LATEST_PATH = import_path6.default.join(import_os4.default.homedir(), ".node9", "undo_latest.txt");
3527
3971
  var MAX_SNAPSHOTS = 10;
3528
3972
  function readStack() {
3529
3973
  try {
@@ -3534,7 +3978,7 @@ function readStack() {
3534
3978
  return [];
3535
3979
  }
3536
3980
  function writeStack(stack) {
3537
- const dir = import_path5.default.dirname(SNAPSHOT_STACK_PATH);
3981
+ const dir = import_path6.default.dirname(SNAPSHOT_STACK_PATH);
3538
3982
  if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
3539
3983
  import_fs4.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
3540
3984
  }
@@ -3552,8 +3996,8 @@ function buildArgsSummary(tool, args) {
3552
3996
  async function createShadowSnapshot(tool = "unknown", args = {}) {
3553
3997
  try {
3554
3998
  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()}`);
3999
+ if (!import_fs4.default.existsSync(import_path6.default.join(cwd, ".git"))) return null;
4000
+ const tempIndex = import_path6.default.join(cwd, ".git", `node9_index_${Date.now()}`);
3557
4001
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
3558
4002
  (0, import_child_process3.spawnSync)("git", ["add", "-A"], { env });
3559
4003
  const treeRes = (0, import_child_process3.spawnSync)("git", ["write-tree"], { env });
@@ -3617,7 +4061,7 @@ function applyUndo(hash, cwd) {
3617
4061
  const tracked = (0, import_child_process3.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
3618
4062
  const untracked = (0, import_child_process3.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
3619
4063
  for (const file of [...tracked, ...untracked]) {
3620
- const fullPath = import_path5.default.join(dir, file);
4064
+ const fullPath = import_path6.default.join(dir, file);
3621
4065
  if (!snapshotFiles.has(file) && import_fs4.default.existsSync(fullPath)) {
3622
4066
  import_fs4.default.unlinkSync(fullPath);
3623
4067
  }
@@ -3631,7 +4075,7 @@ function applyUndo(hash, cwd) {
3631
4075
  // src/cli.ts
3632
4076
  var import_prompts3 = require("@inquirer/prompts");
3633
4077
  var { version } = JSON.parse(
3634
- import_fs5.default.readFileSync(import_path6.default.join(__dirname, "../package.json"), "utf-8")
4078
+ import_fs5.default.readFileSync(import_path7.default.join(__dirname, "../package.json"), "utf-8")
3635
4079
  );
3636
4080
  function parseDuration(str) {
3637
4081
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -3824,9 +4268,9 @@ async function runProxy(targetCommand) {
3824
4268
  }
3825
4269
  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
4270
  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 });
4271
+ const credPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
4272
+ if (!import_fs5.default.existsSync(import_path7.default.dirname(credPath)))
4273
+ import_fs5.default.mkdirSync(import_path7.default.dirname(credPath), { recursive: true });
3830
4274
  const profileName = options.profile || "default";
3831
4275
  let existingCreds = {};
3832
4276
  try {
@@ -3845,7 +4289,7 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
3845
4289
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
3846
4290
  import_fs5.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
3847
4291
  if (profileName === "default") {
3848
- const configPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "config.json");
4292
+ const configPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
3849
4293
  let config = {};
3850
4294
  try {
3851
4295
  if (import_fs5.default.existsSync(configPath))
@@ -3860,10 +4304,12 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
3860
4304
  cloud: true,
3861
4305
  terminal: true
3862
4306
  };
3863
- approvers.cloud = !options.local;
4307
+ if (options.local) {
4308
+ approvers.cloud = false;
4309
+ }
3864
4310
  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 });
4311
+ if (!import_fs5.default.existsSync(import_path7.default.dirname(configPath)))
4312
+ import_fs5.default.mkdirSync(import_path7.default.dirname(configPath), { recursive: true });
3867
4313
  import_fs5.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
3868
4314
  }
3869
4315
  if (options.profile && profileName !== "default") {
@@ -3949,7 +4395,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3949
4395
  );
3950
4396
  }
3951
4397
  section("Configuration");
3952
- const globalConfigPath = import_path6.default.join(homeDir2, ".node9", "config.json");
4398
+ const globalConfigPath = import_path7.default.join(homeDir2, ".node9", "config.json");
3953
4399
  if (import_fs5.default.existsSync(globalConfigPath)) {
3954
4400
  try {
3955
4401
  JSON.parse(import_fs5.default.readFileSync(globalConfigPath, "utf-8"));
@@ -3960,7 +4406,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3960
4406
  } else {
3961
4407
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
3962
4408
  }
3963
- const projectConfigPath = import_path6.default.join(process.cwd(), "node9.config.json");
4409
+ const projectConfigPath = import_path7.default.join(process.cwd(), "node9.config.json");
3964
4410
  if (import_fs5.default.existsSync(projectConfigPath)) {
3965
4411
  try {
3966
4412
  JSON.parse(import_fs5.default.readFileSync(projectConfigPath, "utf-8"));
@@ -3969,7 +4415,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3969
4415
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
3970
4416
  }
3971
4417
  }
3972
- const credsPath = import_path6.default.join(homeDir2, ".node9", "credentials.json");
4418
+ const credsPath = import_path7.default.join(homeDir2, ".node9", "credentials.json");
3973
4419
  if (import_fs5.default.existsSync(credsPath)) {
3974
4420
  pass("Cloud credentials found (~/.node9/credentials.json)");
3975
4421
  } else {
@@ -3979,7 +4425,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3979
4425
  );
3980
4426
  }
3981
4427
  section("Agent Hooks");
3982
- const claudeSettingsPath = import_path6.default.join(homeDir2, ".claude", "settings.json");
4428
+ const claudeSettingsPath = import_path7.default.join(homeDir2, ".claude", "settings.json");
3983
4429
  if (import_fs5.default.existsSync(claudeSettingsPath)) {
3984
4430
  try {
3985
4431
  const cs = JSON.parse(import_fs5.default.readFileSync(claudeSettingsPath, "utf-8"));
@@ -3995,7 +4441,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
3995
4441
  } else {
3996
4442
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
3997
4443
  }
3998
- const geminiSettingsPath = import_path6.default.join(homeDir2, ".gemini", "settings.json");
4444
+ const geminiSettingsPath = import_path7.default.join(homeDir2, ".gemini", "settings.json");
3999
4445
  if (import_fs5.default.existsSync(geminiSettingsPath)) {
4000
4446
  try {
4001
4447
  const gs = JSON.parse(import_fs5.default.readFileSync(geminiSettingsPath, "utf-8"));
@@ -4011,7 +4457,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
4011
4457
  } else {
4012
4458
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
4013
4459
  }
4014
- const cursorHooksPath = import_path6.default.join(homeDir2, ".cursor", "hooks.json");
4460
+ const cursorHooksPath = import_path7.default.join(homeDir2, ".cursor", "hooks.json");
4015
4461
  if (import_fs5.default.existsSync(cursorHooksPath)) {
4016
4462
  try {
4017
4463
  const cur = JSON.parse(import_fs5.default.readFileSync(cursorHooksPath, "utf-8"));
@@ -4116,7 +4562,7 @@ program.command("explain").description(
4116
4562
  console.log("");
4117
4563
  });
4118
4564
  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");
4565
+ const configPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
4120
4566
  if (import_fs5.default.existsSync(configPath) && !options.force) {
4121
4567
  console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
4122
4568
  console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
@@ -4131,7 +4577,7 @@ program.command("init").description("Create ~/.node9/config.json with default po
4131
4577
  mode: safeMode
4132
4578
  }
4133
4579
  };
4134
- const dir = import_path6.default.dirname(configPath);
4580
+ const dir = import_path7.default.dirname(configPath);
4135
4581
  if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
4136
4582
  import_fs5.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
4137
4583
  console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
@@ -4151,7 +4597,7 @@ function formatRelativeTime(timestamp) {
4151
4597
  return new Date(timestamp).toLocaleDateString();
4152
4598
  }
4153
4599
  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");
4600
+ const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "audit.log");
4155
4601
  if (!import_fs5.default.existsSync(logPath)) {
4156
4602
  console.log(
4157
4603
  import_chalk5.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
@@ -4241,8 +4687,8 @@ program.command("status").description("Show current Node9 mode, policy source, a
4241
4687
  console.log("");
4242
4688
  const modeLabel = settings.mode === "audit" ? import_chalk5.default.blue("audit") : settings.mode === "strict" ? import_chalk5.default.red("strict") : import_chalk5.default.white("standard");
4243
4689
  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");
4690
+ const projectConfig = import_path7.default.join(process.cwd(), "node9.config.json");
4691
+ const globalConfig = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
4246
4692
  console.log(
4247
4693
  ` Local: ${import_fs5.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
4248
4694
  );
@@ -4310,7 +4756,7 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
4310
4756
  } catch (err) {
4311
4757
  const tempConfig = getConfig();
4312
4758
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
4313
- const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4759
+ const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4314
4760
  const errMsg = err instanceof Error ? err.message : String(err);
4315
4761
  import_fs5.default.appendFileSync(
4316
4762
  logPath,
@@ -4330,9 +4776,9 @@ RAW: ${raw}
4330
4776
  }
4331
4777
  const config = getConfig();
4332
4778
  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 });
4779
+ const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4780
+ if (!import_fs5.default.existsSync(import_path7.default.dirname(logPath)))
4781
+ import_fs5.default.mkdirSync(import_path7.default.dirname(logPath), { recursive: true });
4336
4782
  import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
4337
4783
  `);
4338
4784
  }
@@ -4404,7 +4850,7 @@ RAW: ${raw}
4404
4850
  });
4405
4851
  } catch (err) {
4406
4852
  if (process.env.NODE9_DEBUG === "1") {
4407
- const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4853
+ const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4408
4854
  const errMsg = err instanceof Error ? err.message : String(err);
4409
4855
  import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
4410
4856
  `);
@@ -4451,9 +4897,9 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
4451
4897
  decision: "allowed",
4452
4898
  source: "post-hook"
4453
4899
  };
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 });
4900
+ const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "audit.log");
4901
+ if (!import_fs5.default.existsSync(import_path7.default.dirname(logPath)))
4902
+ import_fs5.default.mkdirSync(import_path7.default.dirname(logPath), { recursive: true });
4457
4903
  import_fs5.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
4458
4904
  const config = getConfig();
4459
4905
  if (shouldSnapshot(tool, {}, config)) {
@@ -4628,7 +5074,7 @@ process.on("unhandledRejection", (reason) => {
4628
5074
  const isCheckHook = process.argv[2] === "check";
4629
5075
  if (isCheckHook) {
4630
5076
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
4631
- const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
5077
+ const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
4632
5078
  const msg = reason instanceof Error ? reason.message : String(reason);
4633
5079
  import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
4634
5080
  `);