@node9/proxy 1.0.0 → 1.0.2

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.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/core.ts
7
- import chalk from "chalk";
7
+ import chalk2 from "chalk";
8
8
  import { confirm } from "@inquirer/prompts";
9
9
  import fs from "fs";
10
10
  import path from "path";
@@ -14,19 +14,69 @@ import { parse } from "sh-syntax";
14
14
 
15
15
  // src/ui/native.ts
16
16
  import { spawn } from "child_process";
17
+ import chalk from "chalk";
17
18
  var isTestEnv = () => {
18
19
  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";
19
20
  };
21
+ function smartTruncate(str, maxLen = 500) {
22
+ if (str.length <= maxLen) return str;
23
+ const edge = Math.floor(maxLen / 2) - 3;
24
+ return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
25
+ }
26
+ function formatArgs(args) {
27
+ if (args === null || args === void 0) return "(none)";
28
+ let parsed = args;
29
+ if (typeof args === "string") {
30
+ const trimmed = args.trim();
31
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
32
+ try {
33
+ parsed = JSON.parse(trimmed);
34
+ } catch {
35
+ parsed = args;
36
+ }
37
+ } else {
38
+ return smartTruncate(args, 600);
39
+ }
40
+ }
41
+ if (typeof parsed === "object" && !Array.isArray(parsed)) {
42
+ const obj = parsed;
43
+ const codeKeys = [
44
+ "command",
45
+ "cmd",
46
+ "shell_command",
47
+ "bash_command",
48
+ "script",
49
+ "code",
50
+ "input",
51
+ "sql",
52
+ "query",
53
+ "arguments",
54
+ "args",
55
+ "param",
56
+ "params",
57
+ "text"
58
+ ];
59
+ const foundKey = Object.keys(obj).find((k) => codeKeys.includes(k.toLowerCase()));
60
+ if (foundKey) {
61
+ const val = obj[foundKey];
62
+ const str = typeof val === "string" ? val : JSON.stringify(val);
63
+ return `[${foundKey.toUpperCase()}]:
64
+ ${smartTruncate(str, 500)}`;
65
+ }
66
+ return Object.entries(obj).slice(0, 5).map(
67
+ ([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
68
+ ).join("\n");
69
+ }
70
+ return smartTruncate(JSON.stringify(parsed), 200);
71
+ }
20
72
  function sendDesktopNotification(title, body) {
21
73
  if (isTestEnv()) return;
22
74
  try {
23
- const safeTitle = title.replace(/"/g, '\\"');
24
- const safeBody = body.replace(/"/g, '\\"');
25
75
  if (process.platform === "darwin") {
26
- const script = `display notification "${safeBody}" with title "${safeTitle}"`;
76
+ const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
27
77
  spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
28
78
  } else if (process.platform === "linux") {
29
- spawn("notify-send", [safeTitle, safeBody, "--icon=dialog-warning"], {
79
+ spawn("notify-send", [title, body, "--icon=dialog-warning"], {
30
80
  detached: true,
31
81
  stdio: "ignore"
32
82
  }).unref();
@@ -34,69 +84,54 @@ function sendDesktopNotification(title, body) {
34
84
  } catch {
35
85
  }
36
86
  }
37
- function formatArgs(args) {
38
- if (args === null || args === void 0) return "(none)";
39
- if (typeof args !== "object" || Array.isArray(args)) {
40
- const str = typeof args === "string" ? args : JSON.stringify(args);
41
- return str.length > 200 ? str.slice(0, 200) + "\u2026" : str;
42
- }
43
- const entries = Object.entries(args).filter(
44
- ([, v]) => v !== null && v !== void 0 && v !== ""
45
- );
46
- if (entries.length === 0) return "(none)";
47
- const MAX_FIELDS = 5;
48
- const MAX_VALUE_LEN = 120;
49
- const lines = entries.slice(0, MAX_FIELDS).map(([key, val]) => {
50
- const str = typeof val === "string" ? val : JSON.stringify(val);
51
- const truncated = str.length > MAX_VALUE_LEN ? str.slice(0, MAX_VALUE_LEN) + "\u2026" : str;
52
- return ` ${key}: ${truncated}`;
53
- });
54
- if (entries.length > MAX_FIELDS) {
55
- lines.push(` \u2026 and ${entries.length - MAX_FIELDS} more field(s)`);
87
+ function escapePango(text) {
88
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
89
+ }
90
+ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
91
+ const lines = [];
92
+ if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
93
+ lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
94
+ lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
95
+ lines.push("");
96
+ lines.push(formattedArgs);
97
+ if (!locked) {
98
+ lines.push("");
99
+ lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
56
100
  }
57
101
  return lines.join("\n");
58
102
  }
59
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
60
- if (isTestEnv()) return "deny";
61
- if (process.env.NODE9_DEBUG === "1" || process.env.VITEST) {
62
- console.log(`[DEBUG Native] askNativePopup called for: ${toolName}`);
63
- console.log(`[DEBUG Native] isTestEnv check:`, {
64
- VITEST: process.env.VITEST,
65
- NODE_ENV: process.env.NODE_ENV,
66
- CI: process.env.CI,
67
- isTest: isTestEnv()
68
- });
69
- }
70
- const title = locked ? `\u26A1 Node9 \u2014 Locked by Admin Policy` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Requires Approval`;
71
- let message = "";
103
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
104
+ const lines = [];
72
105
  if (locked) {
73
- message += `\u26A1 Awaiting remote approval via Slack. Local override is disabled.
74
- `;
75
- message += `\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
76
- `;
77
- }
78
- message += `Tool: ${toolName}
79
- `;
80
- message += `Agent: ${agent || "AI Agent"}
81
- `;
82
- if (explainableLabel) {
83
- message += `Reason: ${explainableLabel}
84
- `;
106
+ lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
107
+ lines.push("");
85
108
  }
86
- message += `
87
- Arguments:
88
- ${formatArgs(args)}`;
109
+ lines.push(
110
+ `<b>\u{1F916} ${escapePango(agent || "AI Agent")}</b> | <b>\u{1F527} <tt>${escapePango(toolName)}</tt></b>`
111
+ );
112
+ lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
113
+ lines.push("");
114
+ lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
89
115
  if (!locked) {
90
- message += `
91
-
92
- Enter = Allow | Click "Block" to deny`;
116
+ lines.push("");
117
+ lines.push(
118
+ '<small>\u21B5 Enter = <b>Allow \u21B5</b> | \u238B Esc = <b>Block \u238B</b> | "Always Allow" = never ask again</small>'
119
+ );
93
120
  }
94
- const safeMessage = message.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, "'");
95
- const safeTitle = title.replace(/"/g, '\\"');
121
+ return lines.join("\n");
122
+ }
123
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
124
+ if (isTestEnv()) return "deny";
125
+ const formattedArgs = formatArgs(args);
126
+ const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
127
+ const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
128
+ process.stderr.write(chalk.yellow(`
129
+ \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
130
+ `));
96
131
  return new Promise((resolve) => {
97
132
  let childProcess = null;
98
133
  const onAbort = () => {
99
- if (childProcess) {
134
+ if (childProcess && childProcess.pid) {
100
135
  try {
101
136
  process.kill(childProcess.pid, "SIGKILL");
102
137
  } catch {
@@ -108,83 +143,58 @@ Enter = Allow | Click "Block" to deny`;
108
143
  if (signal.aborted) return resolve("deny");
109
144
  signal.addEventListener("abort", onAbort);
110
145
  }
111
- const cleanup = () => {
112
- if (signal) signal.removeEventListener("abort", onAbort);
113
- };
114
146
  try {
115
147
  if (process.platform === "darwin") {
116
- const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
117
- const script = `
118
- tell application "System Events"
119
- activate
120
- display dialog "${safeMessage}" with title "${safeTitle}" ${buttons}
121
- end tell`;
122
- childProcess = spawn("osascript", ["-e", script]);
123
- let output = "";
124
- childProcess.stdout?.on("data", (d) => output += d.toString());
125
- childProcess.on("close", (code) => {
126
- cleanup();
127
- if (locked) return resolve("deny");
128
- if (code === 0) {
129
- if (output.includes("Always Allow")) return resolve("always_allow");
130
- if (output.includes("Allow")) return resolve("allow");
131
- }
132
- resolve("deny");
133
- });
148
+ const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block \u238B", "Always Allow", "Allow \u21B5"} default button "Allow \u21B5" cancel button "Block \u238B"`;
149
+ const script = `on run argv
150
+ tell application "System Events"
151
+ activate
152
+ display dialog (item 1 of argv) with title (item 2 of argv) ${buttons}
153
+ end tell
154
+ end run`;
155
+ childProcess = spawn("osascript", ["-e", script, "--", message, title]);
134
156
  } else if (process.platform === "linux") {
135
- const argsList = locked ? [
136
- "--info",
137
- "--title",
138
- title,
139
- "--text",
140
- safeMessage,
141
- "--ok-label",
142
- "Waiting for Slack\u2026",
143
- "--timeout",
144
- "300"
145
- ] : [
146
- "--question",
157
+ const pangoMessage = buildPangoMessage(
158
+ toolName,
159
+ formattedArgs,
160
+ agent,
161
+ explainableLabel,
162
+ locked
163
+ );
164
+ const argsList = [
165
+ locked ? "--info" : "--question",
166
+ "--modal",
167
+ "--width=480",
147
168
  "--title",
148
169
  title,
149
170
  "--text",
150
- safeMessage,
171
+ pangoMessage,
151
172
  "--ok-label",
152
- "Allow",
153
- "--cancel-label",
154
- "Block",
155
- "--extra-button",
156
- "Always Allow",
173
+ locked ? "Waiting..." : "Allow \u21B5",
157
174
  "--timeout",
158
175
  "300"
159
176
  ];
177
+ if (!locked) {
178
+ argsList.push("--cancel-label", "Block \u238B");
179
+ argsList.push("--extra-button", "Always Allow");
180
+ }
160
181
  childProcess = spawn("zenity", argsList);
161
- let output = "";
162
- childProcess.stdout?.on("data", (d) => output += d.toString());
163
- childProcess.on("close", (code) => {
164
- cleanup();
165
- if (locked) return resolve("deny");
166
- if (output.trim() === "Always Allow") return resolve("always_allow");
167
- if (code === 0) return resolve("allow");
168
- resolve("deny");
169
- });
170
182
  } else if (process.platform === "win32") {
171
- const buttonType = locked ? "OK" : "YesNo";
172
- const ps = `
173
- Add-Type -AssemblyName PresentationFramework;
174
- $res = [System.Windows.MessageBox]::Show("${safeMessage}", "${safeTitle}", "${buttonType}", "Warning", "Button2", "DefaultDesktopOnly");
175
- if ($res -eq "Yes") { exit 0 } else { exit 1 }`;
183
+ const b64Msg = Buffer.from(message).toString("base64");
184
+ const b64Title = Buffer.from(title).toString("base64");
185
+ const ps = `Add-Type -AssemblyName PresentationFramework; $msg = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("${b64Msg}")); $title = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("${b64Title}")); $res = [System.Windows.MessageBox]::Show($msg, $title, "${locked ? "OK" : "YesNo"}", "Warning", "Button2", "DefaultDesktopOnly"); if ($res -eq "Yes") { exit 0 } else { exit 1 }`;
176
186
  childProcess = spawn("powershell", ["-Command", ps]);
177
- childProcess.on("close", (code) => {
178
- cleanup();
179
- if (locked) return resolve("deny");
180
- resolve(code === 0 ? "allow" : "deny");
181
- });
182
- } else {
183
- cleanup();
184
- resolve("deny");
185
187
  }
188
+ let output = "";
189
+ childProcess?.stdout?.on("data", (d) => output += d.toString());
190
+ childProcess?.on("close", (code) => {
191
+ if (signal) signal.removeEventListener("abort", onAbort);
192
+ if (locked) return resolve("deny");
193
+ if (output.includes("Always Allow")) return resolve("always_allow");
194
+ if (code === 0) return resolve("allow");
195
+ resolve("deny");
196
+ });
186
197
  } catch {
187
- cleanup();
188
198
  resolve("deny");
189
199
  }
190
200
  });
@@ -193,6 +203,8 @@ Enter = Allow | Click "Block" to deny`;
193
203
  // src/core.ts
194
204
  var PAUSED_FILE = path.join(os.homedir(), ".node9", "PAUSED");
195
205
  var TRUST_FILE = path.join(os.homedir(), ".node9", "trust.json");
206
+ var LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
207
+ var HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
196
208
  function checkPause() {
197
209
  try {
198
210
  if (!fs.existsSync(PAUSED_FILE)) return { paused: false };
@@ -259,36 +271,39 @@ function writeTrustSession(toolName, durationMs) {
259
271
  }
260
272
  }
261
273
  }
262
- function appendAuditModeEntry(toolName, args) {
274
+ function appendToLog(logPath, entry) {
263
275
  try {
264
- const entry = JSON.stringify({
265
- ts: (/* @__PURE__ */ new Date()).toISOString(),
266
- tool: toolName,
267
- args,
268
- decision: "would-have-blocked",
269
- source: "audit-mode"
270
- });
271
- const logPath = path.join(os.homedir(), ".node9", "audit.log");
272
276
  const dir = path.dirname(logPath);
273
277
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
274
- fs.appendFileSync(logPath, entry + "\n");
278
+ fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
275
279
  } catch {
276
280
  }
277
281
  }
278
- var DANGEROUS_WORDS = [
279
- "delete",
280
- "drop",
281
- "remove",
282
- "terminate",
283
- "refund",
284
- "write",
285
- "update",
286
- "destroy",
287
- "rm",
288
- "rmdir",
289
- "purge",
290
- "format"
291
- ];
282
+ function appendHookDebug(toolName, args, meta) {
283
+ const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
284
+ appendToLog(HOOK_DEBUG_LOG, {
285
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
286
+ tool: toolName,
287
+ args: safeArgs,
288
+ agent: meta?.agent,
289
+ mcpServer: meta?.mcpServer,
290
+ hostname: os.hostname(),
291
+ cwd: process.cwd()
292
+ });
293
+ }
294
+ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
295
+ const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
296
+ appendToLog(LOCAL_AUDIT_LOG, {
297
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
298
+ tool: toolName,
299
+ args: safeArgs,
300
+ decision,
301
+ checkedBy,
302
+ agent: meta?.agent,
303
+ mcpServer: meta?.mcpServer,
304
+ hostname: os.hostname()
305
+ });
306
+ }
292
307
  function tokenize(toolName) {
293
308
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
294
309
  }
@@ -395,16 +410,28 @@ function redactSecrets(text) {
395
410
  );
396
411
  return redacted;
397
412
  }
413
+ var DANGEROUS_WORDS = [
414
+ "drop",
415
+ "truncate",
416
+ "purge",
417
+ "format",
418
+ "destroy",
419
+ "terminate",
420
+ "revoke",
421
+ "docker",
422
+ "psql"
423
+ ];
398
424
  var DEFAULT_CONFIG = {
399
425
  settings: {
400
426
  mode: "standard",
401
427
  autoStartDaemon: true,
402
- enableUndo: false,
428
+ enableUndo: true,
429
+ // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
403
430
  enableHookLogDebug: false,
404
431
  approvers: { native: true, browser: true, cloud: true, terminal: true }
405
432
  },
406
433
  policy: {
407
- sandboxPaths: [],
434
+ sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
408
435
  dangerousWords: DANGEROUS_WORDS,
409
436
  ignoredTools: [
410
437
  "list_*",
@@ -412,12 +439,44 @@ var DEFAULT_CONFIG = {
412
439
  "read_*",
413
440
  "describe_*",
414
441
  "read",
442
+ "glob",
415
443
  "grep",
416
444
  "ls",
417
- "askuserquestion"
445
+ "notebookread",
446
+ "notebookedit",
447
+ "webfetch",
448
+ "websearch",
449
+ "exitplanmode",
450
+ "askuserquestion",
451
+ "agent",
452
+ "task*",
453
+ "toolsearch",
454
+ "mcp__ide__*",
455
+ "getDiagnostics"
418
456
  ],
419
- toolInspection: { bash: "command", shell: "command" },
420
- rules: [{ action: "rm", allowPaths: ["**/node_modules/**", "dist/**", ".DS_Store"] }]
457
+ toolInspection: {
458
+ bash: "command",
459
+ shell: "command",
460
+ run_shell_command: "command",
461
+ "terminal.execute": "command",
462
+ "postgres:query": "sql"
463
+ },
464
+ rules: [
465
+ {
466
+ action: "rm",
467
+ allowPaths: [
468
+ "**/node_modules/**",
469
+ "dist/**",
470
+ "build/**",
471
+ ".next/**",
472
+ "coverage/**",
473
+ ".cache/**",
474
+ "tmp/**",
475
+ "temp/**",
476
+ ".DS_Store"
477
+ ]
478
+ }
479
+ ]
421
480
  },
422
481
  environments: {}
423
482
  };
@@ -482,20 +541,15 @@ async function evaluatePolicy(toolName, args, agent) {
482
541
  }
483
542
  const isManual = agent === "Terminal";
484
543
  if (isManual) {
485
- const NUCLEAR_COMMANDS = [
486
- "drop",
487
- "destroy",
488
- "purge",
489
- "rmdir",
490
- "format",
491
- "truncate",
492
- "alter",
493
- "grant",
494
- "revoke",
495
- "docker"
496
- ];
497
- const hasNuclear = allTokens.some((t) => NUCLEAR_COMMANDS.includes(t.toLowerCase()));
498
- if (!hasNuclear) return { decision: "allow" };
544
+ const SYSTEM_DISASTER_COMMANDS = ["mkfs", "shred", "dd", "drop", "truncate", "purge"];
545
+ const hasSystemDisaster = allTokens.some(
546
+ (t) => SYSTEM_DISASTER_COMMANDS.includes(t.toLowerCase())
547
+ );
548
+ const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
549
+ if (hasSystemDisaster || isRootWipe) {
550
+ return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
551
+ }
552
+ return { decision: "allow" };
499
553
  }
500
554
  if (pathTokens.length > 0 && config.policy.sandboxPaths.length > 0) {
501
555
  const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
@@ -509,27 +563,39 @@ async function evaluatePolicy(toolName, args, agent) {
509
563
  if (pathTokens.length > 0) {
510
564
  const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
511
565
  if (anyBlocked)
512
- return { decision: "review", blockedByLabel: "Project/Global Config (Rule Block)" };
566
+ return {
567
+ decision: "review",
568
+ blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
569
+ };
513
570
  const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
514
571
  if (allAllowed) return { decision: "allow" };
515
572
  }
516
- return { decision: "review", blockedByLabel: "Project/Global Config (Rule Default Block)" };
573
+ return {
574
+ decision: "review",
575
+ blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
576
+ };
517
577
  }
518
578
  }
579
+ let matchedDangerousWord;
519
580
  const isDangerous = allTokens.some(
520
581
  (token) => config.policy.dangerousWords.some((word) => {
521
582
  const w = word.toLowerCase();
522
- if (token === w) return true;
523
- try {
524
- return new RegExp(`\\b${w}\\b`, "i").test(token);
525
- } catch {
526
- return false;
527
- }
583
+ const hit = token === w || (() => {
584
+ try {
585
+ return new RegExp(`\\b${w}\\b`, "i").test(token);
586
+ } catch {
587
+ return false;
588
+ }
589
+ })();
590
+ if (hit && !matchedDangerousWord) matchedDangerousWord = word;
591
+ return hit;
528
592
  })
529
593
  );
530
594
  if (isDangerous) {
531
- const label = isManual ? "Manual Nuclear Protection" : "Project/Global Config (Dangerous Word)";
532
- return { decision: "review", blockedByLabel: label };
595
+ return {
596
+ decision: "review",
597
+ blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`
598
+ };
533
599
  }
534
600
  if (config.settings.mode === "strict") {
535
601
  const envConfig = getActiveEnvironment(config);
@@ -644,13 +710,16 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
644
710
  approvers.browser = false;
645
711
  approvers.terminal = false;
646
712
  }
713
+ if (config.settings.enableHookLogDebug && !isTestEnv2) {
714
+ appendHookDebug(toolName, args, meta);
715
+ }
647
716
  const isManual = meta?.agent === "Terminal";
648
717
  let explainableLabel = "Local Config";
649
718
  if (config.settings.mode === "audit") {
650
719
  if (!isIgnoredTool(toolName)) {
651
720
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
652
721
  if (policyResult.decision === "review") {
653
- appendAuditModeEntry(toolName, args);
722
+ appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
654
723
  sendDesktopNotification(
655
724
  "Node9 Audit Mode",
656
725
  `Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
@@ -662,20 +731,24 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
662
731
  if (!isIgnoredTool(toolName)) {
663
732
  if (getActiveTrustSession(toolName)) {
664
733
  if (creds?.apiKey) auditLocalAllow(toolName, args, "trust", creds, meta);
734
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
665
735
  return { approved: true, checkedBy: "trust" };
666
736
  }
667
737
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
668
738
  if (policyResult.decision === "allow") {
669
739
  if (creds?.apiKey) auditLocalAllow(toolName, args, "local-policy", creds, meta);
740
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
670
741
  return { approved: true, checkedBy: "local-policy" };
671
742
  }
672
743
  explainableLabel = policyResult.blockedByLabel || "Local Config";
673
744
  const persistent = getPersistentDecision(toolName);
674
745
  if (persistent === "allow") {
675
746
  if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
747
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
676
748
  return { approved: true, checkedBy: "persistent" };
677
749
  }
678
750
  if (persistent === "deny") {
751
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
679
752
  return {
680
753
  approved: false,
681
754
  reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
@@ -685,6 +758,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
685
758
  }
686
759
  } else {
687
760
  if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
761
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
688
762
  return { approved: true };
689
763
  }
690
764
  let cloudRequestId = null;
@@ -712,8 +786,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
712
786
  const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
713
787
  const reason = isAuthError ? "Invalid or missing API key. Run `node9 login` to generate a key (must start with n9_live_)." : isNetworkError ? "Could not reach the Node9 cloud. Check your network or API URL." : error.message;
714
788
  console.error(
715
- chalk.yellow(`
716
- \u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + chalk.dim(`
789
+ chalk2.yellow(`
790
+ \u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + chalk2.dim(`
717
791
  Falling back to local rules...
718
792
  `)
719
793
  );
@@ -721,13 +795,13 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
721
795
  }
722
796
  if (cloudEnforced && cloudRequestId) {
723
797
  console.error(
724
- chalk.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
798
+ chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
725
799
  );
726
- console.error(chalk.cyan(" Dashboard \u2192 ") + chalk.bold("Mission Control > Activity Feed\n"));
800
+ console.error(chalk2.cyan(" Dashboard \u2192 ") + chalk2.bold("Mission Control > Activity Feed\n"));
727
801
  } else if (!cloudEnforced) {
728
802
  const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
729
803
  console.error(
730
- chalk.dim(`
804
+ chalk2.dim(`
731
805
  \u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
732
806
  `)
733
807
  );
@@ -792,9 +866,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
792
866
  try {
793
867
  if (!approvers.native && !cloudEnforced) {
794
868
  console.error(
795
- chalk.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
869
+ chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
796
870
  );
797
- console.error(chalk.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
871
+ console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
798
872
  `));
799
873
  }
800
874
  const daemonDecision = await askDaemon(toolName, args, meta, signal);
@@ -817,11 +891,11 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
817
891
  racePromises.push(
818
892
  (async () => {
819
893
  try {
820
- console.log(chalk.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
821
- console.log(`${chalk.bold("Action:")} ${chalk.red(toolName)}`);
822
- console.log(`${chalk.bold("Flagged By:")} ${chalk.yellow(explainableLabel)}`);
894
+ console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
895
+ console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
896
+ console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
823
897
  if (isRemoteLocked) {
824
- console.log(chalk.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
898
+ console.log(chalk2.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
825
899
  `));
826
900
  await new Promise((_, reject) => {
827
901
  signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
@@ -909,6 +983,15 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
909
983
  if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
910
984
  await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
911
985
  }
986
+ if (!isManual) {
987
+ appendLocalAudit(
988
+ toolName,
989
+ args,
990
+ finalResult.approved ? "allow" : "deny",
991
+ finalResult.checkedBy || finalResult.blockedBy || "unknown",
992
+ meta
993
+ );
994
+ }
912
995
  return finalResult;
913
996
  }
914
997
  function getConfig() {
@@ -939,8 +1022,8 @@ function getConfig() {
939
1022
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
940
1023
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
941
1024
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
942
- if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
943
1025
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
1026
+ if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
944
1027
  if (p.toolInspection)
945
1028
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
946
1029
  if (p.rules) mergedPolicy.rules.push(...p.rules);
@@ -1068,11 +1151,11 @@ async function pollNode9SaaS(requestId, creds, signal) {
1068
1151
  if (!statusRes.ok) continue;
1069
1152
  const { status, reason } = await statusRes.json();
1070
1153
  if (status === "APPROVED") {
1071
- console.error(chalk.green("\u2705 Approved via Cloud.\n"));
1154
+ console.error(chalk2.green("\u2705 Approved via Cloud.\n"));
1072
1155
  return { approved: true, reason };
1073
1156
  }
1074
1157
  if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
1075
- console.error(chalk.red("\u274C Denied via Cloud.\n"));
1158
+ console.error(chalk2.red("\u274C Denied via Cloud.\n"));
1076
1159
  return { approved: false, reason };
1077
1160
  }
1078
1161
  } catch {
@@ -1100,11 +1183,11 @@ async function resolveNode9SaaS(requestId, creds, approved) {
1100
1183
  import fs2 from "fs";
1101
1184
  import path2 from "path";
1102
1185
  import os2 from "os";
1103
- import chalk2 from "chalk";
1186
+ import chalk3 from "chalk";
1104
1187
  import { confirm as confirm2 } from "@inquirer/prompts";
1105
1188
  function printDaemonTip() {
1106
1189
  console.log(
1107
- chalk2.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk2.white("\n To view your history or manage persistent rules, run:") + chalk2.green("\n node9 daemon --openui")
1190
+ chalk3.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk3.white("\n To view your history or manage persistent rules, run:") + chalk3.green("\n node9 daemon --openui")
1108
1191
  );
1109
1192
  }
1110
1193
  function fullPathCommand(subcommand) {
@@ -1145,7 +1228,7 @@ async function setupClaude() {
1145
1228
  matcher: ".*",
1146
1229
  hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
1147
1230
  });
1148
- console.log(chalk2.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
1231
+ console.log(chalk3.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
1149
1232
  anythingChanged = true;
1150
1233
  }
1151
1234
  const hasPostHook = settings.hooks.PostToolUse?.some(
@@ -1157,7 +1240,7 @@ async function setupClaude() {
1157
1240
  matcher: ".*",
1158
1241
  hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
1159
1242
  });
1160
- console.log(chalk2.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
1243
+ console.log(chalk3.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
1161
1244
  anythingChanged = true;
1162
1245
  }
1163
1246
  if (anythingChanged) {
@@ -1171,10 +1254,10 @@ async function setupClaude() {
1171
1254
  serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
1172
1255
  }
1173
1256
  if (serversToWrap.length > 0) {
1174
- console.log(chalk2.bold("The following existing entries will be modified:\n"));
1175
- console.log(chalk2.white(` ${mcpPath}`));
1257
+ console.log(chalk3.bold("The following existing entries will be modified:\n"));
1258
+ console.log(chalk3.white(` ${mcpPath}`));
1176
1259
  for (const { name, originalCmd } of serversToWrap) {
1177
- console.log(chalk2.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
1260
+ console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
1178
1261
  }
1179
1262
  console.log("");
1180
1263
  const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
@@ -1184,22 +1267,22 @@ async function setupClaude() {
1184
1267
  }
1185
1268
  claudeConfig.mcpServers = servers;
1186
1269
  writeJson(mcpPath, claudeConfig);
1187
- console.log(chalk2.green(`
1270
+ console.log(chalk3.green(`
1188
1271
  \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
1189
1272
  anythingChanged = true;
1190
1273
  } else {
1191
- console.log(chalk2.yellow(" Skipped MCP server wrapping."));
1274
+ console.log(chalk3.yellow(" Skipped MCP server wrapping."));
1192
1275
  }
1193
1276
  console.log("");
1194
1277
  }
1195
1278
  if (!anythingChanged && serversToWrap.length === 0) {
1196
- console.log(chalk2.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
1279
+ console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
1197
1280
  printDaemonTip();
1198
1281
  return;
1199
1282
  }
1200
1283
  if (anythingChanged) {
1201
- console.log(chalk2.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
1202
- console.log(chalk2.gray(" Restart Claude Code for changes to take effect."));
1284
+ console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
1285
+ console.log(chalk3.gray(" Restart Claude Code for changes to take effect."));
1203
1286
  printDaemonTip();
1204
1287
  }
1205
1288
  }
@@ -1227,7 +1310,7 @@ async function setupGemini() {
1227
1310
  }
1228
1311
  ]
1229
1312
  });
1230
- console.log(chalk2.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
1313
+ console.log(chalk3.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
1231
1314
  anythingChanged = true;
1232
1315
  }
1233
1316
  const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
@@ -1240,7 +1323,7 @@ async function setupGemini() {
1240
1323
  matcher: ".*",
1241
1324
  hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
1242
1325
  });
1243
- console.log(chalk2.green(" \u2705 AfterTool hook added \u2192 node9 log"));
1326
+ console.log(chalk3.green(" \u2705 AfterTool hook added \u2192 node9 log"));
1244
1327
  anythingChanged = true;
1245
1328
  }
1246
1329
  if (anythingChanged) {
@@ -1254,10 +1337,10 @@ async function setupGemini() {
1254
1337
  serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
1255
1338
  }
1256
1339
  if (serversToWrap.length > 0) {
1257
- console.log(chalk2.bold("The following existing entries will be modified:\n"));
1258
- console.log(chalk2.white(` ${settingsPath} (mcpServers)`));
1340
+ console.log(chalk3.bold("The following existing entries will be modified:\n"));
1341
+ console.log(chalk3.white(` ${settingsPath} (mcpServers)`));
1259
1342
  for (const { name, originalCmd } of serversToWrap) {
1260
- console.log(chalk2.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
1343
+ console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
1261
1344
  }
1262
1345
  console.log("");
1263
1346
  const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
@@ -1267,22 +1350,22 @@ async function setupGemini() {
1267
1350
  }
1268
1351
  settings.mcpServers = servers;
1269
1352
  writeJson(settingsPath, settings);
1270
- console.log(chalk2.green(`
1353
+ console.log(chalk3.green(`
1271
1354
  \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
1272
1355
  anythingChanged = true;
1273
1356
  } else {
1274
- console.log(chalk2.yellow(" Skipped MCP server wrapping."));
1357
+ console.log(chalk3.yellow(" Skipped MCP server wrapping."));
1275
1358
  }
1276
1359
  console.log("");
1277
1360
  }
1278
1361
  if (!anythingChanged && serversToWrap.length === 0) {
1279
- console.log(chalk2.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
1362
+ console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
1280
1363
  printDaemonTip();
1281
1364
  return;
1282
1365
  }
1283
1366
  if (anythingChanged) {
1284
- console.log(chalk2.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
1285
- console.log(chalk2.gray(" Restart Gemini CLI for changes to take effect."));
1367
+ console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
1368
+ console.log(chalk3.gray(" Restart Gemini CLI for changes to take effect."));
1286
1369
  printDaemonTip();
1287
1370
  }
1288
1371
  }
@@ -1301,7 +1384,7 @@ async function setupCursor() {
1301
1384
  if (!hasPreHook) {
1302
1385
  if (!hooksFile.hooks.preToolUse) hooksFile.hooks.preToolUse = [];
1303
1386
  hooksFile.hooks.preToolUse.push({ command: fullPathCommand("check") });
1304
- console.log(chalk2.green(" \u2705 preToolUse hook added \u2192 node9 check"));
1387
+ console.log(chalk3.green(" \u2705 preToolUse hook added \u2192 node9 check"));
1305
1388
  anythingChanged = true;
1306
1389
  }
1307
1390
  const hasPostHook = hooksFile.hooks.postToolUse?.some(
@@ -1310,7 +1393,7 @@ async function setupCursor() {
1310
1393
  if (!hasPostHook) {
1311
1394
  if (!hooksFile.hooks.postToolUse) hooksFile.hooks.postToolUse = [];
1312
1395
  hooksFile.hooks.postToolUse.push({ command: fullPathCommand("log") });
1313
- console.log(chalk2.green(" \u2705 postToolUse hook added \u2192 node9 log"));
1396
+ console.log(chalk3.green(" \u2705 postToolUse hook added \u2192 node9 log"));
1314
1397
  anythingChanged = true;
1315
1398
  }
1316
1399
  if (anythingChanged) {
@@ -1324,10 +1407,10 @@ async function setupCursor() {
1324
1407
  serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
1325
1408
  }
1326
1409
  if (serversToWrap.length > 0) {
1327
- console.log(chalk2.bold("The following existing entries will be modified:\n"));
1328
- console.log(chalk2.white(` ${mcpPath}`));
1410
+ console.log(chalk3.bold("The following existing entries will be modified:\n"));
1411
+ console.log(chalk3.white(` ${mcpPath}`));
1329
1412
  for (const { name, originalCmd } of serversToWrap) {
1330
- console.log(chalk2.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
1413
+ console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
1331
1414
  }
1332
1415
  console.log("");
1333
1416
  const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
@@ -1337,22 +1420,22 @@ async function setupCursor() {
1337
1420
  }
1338
1421
  mcpConfig.mcpServers = servers;
1339
1422
  writeJson(mcpPath, mcpConfig);
1340
- console.log(chalk2.green(`
1423
+ console.log(chalk3.green(`
1341
1424
  \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
1342
1425
  anythingChanged = true;
1343
1426
  } else {
1344
- console.log(chalk2.yellow(" Skipped MCP server wrapping."));
1427
+ console.log(chalk3.yellow(" Skipped MCP server wrapping."));
1345
1428
  }
1346
1429
  console.log("");
1347
1430
  }
1348
1431
  if (!anythingChanged && serversToWrap.length === 0) {
1349
- console.log(chalk2.blue("\u2139\uFE0F Node9 is already fully configured for Cursor."));
1432
+ console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Cursor."));
1350
1433
  printDaemonTip();
1351
1434
  return;
1352
1435
  }
1353
1436
  if (anythingChanged) {
1354
- console.log(chalk2.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor!"));
1355
- console.log(chalk2.gray(" Restart Cursor for changes to take effect."));
1437
+ console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor!"));
1438
+ console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
1356
1439
  printDaemonTip();
1357
1440
  }
1358
1441
  }
@@ -2331,7 +2414,7 @@ import path3 from "path";
2331
2414
  import os3 from "os";
2332
2415
  import { spawn as spawn2 } from "child_process";
2333
2416
  import { randomUUID } from "crypto";
2334
- import chalk3 from "chalk";
2417
+ import chalk4 from "chalk";
2335
2418
  var DAEMON_PORT2 = 7391;
2336
2419
  var DAEMON_HOST2 = "127.0.0.1";
2337
2420
  var homeDir = os3.homedir();
@@ -2777,7 +2860,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
2777
2860
  return;
2778
2861
  }
2779
2862
  }
2780
- console.error(chalk3.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
2863
+ console.error(chalk4.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
2781
2864
  process.exit(1);
2782
2865
  });
2783
2866
  server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
@@ -2786,17 +2869,17 @@ data: ${JSON.stringify(readPersistentDecisions())}
2786
2869
  JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
2787
2870
  { mode: 384 }
2788
2871
  );
2789
- console.log(chalk3.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
2872
+ console.log(chalk4.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
2790
2873
  });
2791
2874
  }
2792
2875
  function stopDaemon() {
2793
- if (!fs3.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
2876
+ if (!fs3.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
2794
2877
  try {
2795
2878
  const { pid } = JSON.parse(fs3.readFileSync(DAEMON_PID_FILE, "utf-8"));
2796
2879
  process.kill(pid, "SIGTERM");
2797
- console.log(chalk3.green("\u2705 Stopped."));
2880
+ console.log(chalk4.green("\u2705 Stopped."));
2798
2881
  } catch {
2799
- console.log(chalk3.gray("Cleaned up stale PID file."));
2882
+ console.log(chalk4.gray("Cleaned up stale PID file."));
2800
2883
  } finally {
2801
2884
  try {
2802
2885
  fs3.unlinkSync(DAEMON_PID_FILE);
@@ -2806,13 +2889,13 @@ function stopDaemon() {
2806
2889
  }
2807
2890
  function daemonStatus() {
2808
2891
  if (!fs3.existsSync(DAEMON_PID_FILE))
2809
- return console.log(chalk3.yellow("Node9 daemon: not running"));
2892
+ return console.log(chalk4.yellow("Node9 daemon: not running"));
2810
2893
  try {
2811
2894
  const { pid } = JSON.parse(fs3.readFileSync(DAEMON_PID_FILE, "utf-8"));
2812
2895
  process.kill(pid, 0);
2813
- console.log(chalk3.green("Node9 daemon: running"));
2896
+ console.log(chalk4.green("Node9 daemon: running"));
2814
2897
  } catch {
2815
- console.log(chalk3.yellow("Node9 daemon: not running (stale PID)"));
2898
+ console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
2816
2899
  }
2817
2900
  }
2818
2901
 
@@ -2820,7 +2903,7 @@ function daemonStatus() {
2820
2903
  import { spawn as spawn3, execSync } from "child_process";
2821
2904
  import { parseCommandString } from "execa";
2822
2905
  import { execa } from "execa";
2823
- import chalk4 from "chalk";
2906
+ import chalk5 from "chalk";
2824
2907
  import readline from "readline";
2825
2908
  import fs5 from "fs";
2826
2909
  import path5 from "path";
@@ -2959,7 +3042,7 @@ async function runProxy(targetCommand) {
2959
3042
  if (stdout) executable = stdout.trim();
2960
3043
  } catch {
2961
3044
  }
2962
- console.log(chalk4.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
3045
+ console.log(chalk5.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
2963
3046
  const child = spawn3(executable, args, {
2964
3047
  stdio: ["pipe", "pipe", "inherit"],
2965
3048
  // We control STDIN and STDOUT
@@ -3060,92 +3143,47 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
3060
3143
  fs5.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
3061
3144
  }
3062
3145
  if (options.profile && profileName !== "default") {
3063
- console.log(chalk4.green(`\u2705 Profile "${profileName}" saved`));
3064
- console.log(chalk4.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
3146
+ console.log(chalk5.green(`\u2705 Profile "${profileName}" saved`));
3147
+ console.log(chalk5.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
3065
3148
  } else if (options.local) {
3066
- console.log(chalk4.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
3067
- console.log(chalk4.gray(` All decisions stay on this machine.`));
3149
+ console.log(chalk5.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
3150
+ console.log(chalk5.gray(` All decisions stay on this machine.`));
3068
3151
  } else {
3069
- console.log(chalk4.green(`\u2705 Logged in \u2014 agent mode`));
3070
- console.log(chalk4.gray(` Team policy enforced for all calls via Node9 cloud.`));
3152
+ console.log(chalk5.green(`\u2705 Logged in \u2014 agent mode`));
3153
+ console.log(chalk5.gray(` Team policy enforced for all calls via Node9 cloud.`));
3071
3154
  }
3072
3155
  });
3073
3156
  program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
3074
3157
  if (target === "gemini") return await setupGemini();
3075
3158
  if (target === "claude") return await setupClaude();
3076
3159
  if (target === "cursor") return await setupCursor();
3077
- console.error(chalk4.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
3160
+ console.error(chalk5.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
3078
3161
  process.exit(1);
3079
3162
  });
3080
- program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").action((options) => {
3163
+ 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) => {
3081
3164
  const configPath = path5.join(os5.homedir(), ".node9", "config.json");
3082
3165
  if (fs5.existsSync(configPath) && !options.force) {
3083
- console.log(chalk4.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
3084
- console.log(chalk4.gray(` Run with --force to overwrite.`));
3166
+ console.log(chalk5.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
3167
+ console.log(chalk5.gray(` Run with --force to overwrite.`));
3085
3168
  return;
3086
3169
  }
3087
- const defaultConfig = {
3088
- version: "1.0",
3170
+ const requestedMode = options.mode.toLowerCase();
3171
+ const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
3172
+ const configToSave = {
3173
+ ...DEFAULT_CONFIG,
3089
3174
  settings: {
3090
- mode: "standard",
3091
- autoStartDaemon: true,
3092
- enableUndo: true,
3093
- enableHookLogDebug: false,
3094
- approvers: { native: true, browser: true, cloud: true, terminal: true }
3095
- },
3096
- policy: {
3097
- sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
3098
- dangerousWords: DANGEROUS_WORDS,
3099
- ignoredTools: [
3100
- "list_*",
3101
- "get_*",
3102
- "read_*",
3103
- "describe_*",
3104
- "read",
3105
- "write",
3106
- "edit",
3107
- "glob",
3108
- "grep",
3109
- "ls",
3110
- "notebookread",
3111
- "notebookedit",
3112
- "webfetch",
3113
- "websearch",
3114
- "exitplanmode",
3115
- "askuserquestion",
3116
- "agent",
3117
- "task*"
3118
- ],
3119
- toolInspection: {
3120
- bash: "command",
3121
- shell: "command",
3122
- run_shell_command: "command",
3123
- "terminal.execute": "command",
3124
- "postgres:query": "sql"
3125
- },
3126
- rules: [
3127
- {
3128
- action: "rm",
3129
- allowPaths: [
3130
- "**/node_modules/**",
3131
- "dist/**",
3132
- "build/**",
3133
- ".next/**",
3134
- "coverage/**",
3135
- ".cache/**",
3136
- "tmp/**",
3137
- "temp/**",
3138
- ".DS_Store"
3139
- ]
3140
- }
3141
- ]
3175
+ ...DEFAULT_CONFIG.settings,
3176
+ mode: safeMode
3142
3177
  }
3143
3178
  };
3144
- if (!fs5.existsSync(path5.dirname(configPath)))
3145
- fs5.mkdirSync(path5.dirname(configPath), { recursive: true });
3146
- fs5.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
3147
- console.log(chalk4.green(`\u2705 Global config created: ${configPath}`));
3148
- console.log(chalk4.gray(` Edit this file to add custom tool inspection or security rules.`));
3179
+ const dir = path5.dirname(configPath);
3180
+ if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
3181
+ fs5.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
3182
+ console.log(chalk5.green(`\u2705 Global config created: ${configPath}`));
3183
+ console.log(chalk5.cyan(` Mode set to: ${safeMode}`));
3184
+ console.log(
3185
+ chalk5.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
3186
+ );
3149
3187
  });
3150
3188
  program.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
3151
3189
  const creds = getCredentials();
@@ -3154,43 +3192,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
3154
3192
  const settings = mergedConfig.settings;
3155
3193
  console.log("");
3156
3194
  if (creds && settings.approvers.cloud) {
3157
- console.log(chalk4.green(" \u25CF Agent mode") + chalk4.gray(" \u2014 cloud team policy enforced"));
3195
+ console.log(chalk5.green(" \u25CF Agent mode") + chalk5.gray(" \u2014 cloud team policy enforced"));
3158
3196
  } else if (creds && !settings.approvers.cloud) {
3159
3197
  console.log(
3160
- chalk4.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk4.gray(" \u2014 all decisions stay on this machine")
3198
+ chalk5.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk5.gray(" \u2014 all decisions stay on this machine")
3161
3199
  );
3162
3200
  } else {
3163
3201
  console.log(
3164
- chalk4.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk4.gray(" \u2014 no API key (Local rules only)")
3202
+ chalk5.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk5.gray(" \u2014 no API key (Local rules only)")
3165
3203
  );
3166
3204
  }
3167
3205
  console.log("");
3168
3206
  if (daemonRunning) {
3169
3207
  console.log(
3170
- chalk4.green(" \u25CF Daemon running") + chalk4.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
3208
+ chalk5.green(" \u25CF Daemon running") + chalk5.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
3171
3209
  );
3172
3210
  } else {
3173
- console.log(chalk4.gray(" \u25CB Daemon stopped"));
3211
+ console.log(chalk5.gray(" \u25CB Daemon stopped"));
3174
3212
  }
3175
3213
  if (settings.enableUndo) {
3176
3214
  console.log(
3177
- chalk4.magenta(" \u25CF Undo Engine") + chalk4.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
3215
+ chalk5.magenta(" \u25CF Undo Engine") + chalk5.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
3178
3216
  );
3179
3217
  }
3180
3218
  console.log("");
3181
- const modeLabel = settings.mode === "audit" ? chalk4.blue("audit") : settings.mode === "strict" ? chalk4.red("strict") : chalk4.white("standard");
3219
+ const modeLabel = settings.mode === "audit" ? chalk5.blue("audit") : settings.mode === "strict" ? chalk5.red("strict") : chalk5.white("standard");
3182
3220
  console.log(` Mode: ${modeLabel}`);
3183
3221
  const projectConfig = path5.join(process.cwd(), "node9.config.json");
3184
3222
  const globalConfig = path5.join(os5.homedir(), ".node9", "config.json");
3185
3223
  console.log(
3186
- ` Local: ${fs5.existsSync(projectConfig) ? chalk4.green("Active (node9.config.json)") : chalk4.gray("Not present")}`
3224
+ ` Local: ${fs5.existsSync(projectConfig) ? chalk5.green("Active (node9.config.json)") : chalk5.gray("Not present")}`
3187
3225
  );
3188
3226
  console.log(
3189
- ` Global: ${fs5.existsSync(globalConfig) ? chalk4.green("Active (~/.node9/config.json)") : chalk4.gray("Not present")}`
3227
+ ` Global: ${fs5.existsSync(globalConfig) ? chalk5.green("Active (~/.node9/config.json)") : chalk5.gray("Not present")}`
3190
3228
  );
3191
3229
  if (mergedConfig.policy.sandboxPaths.length > 0) {
3192
3230
  console.log(
3193
- ` Sandbox: ${chalk4.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
3231
+ ` Sandbox: ${chalk5.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
3194
3232
  );
3195
3233
  }
3196
3234
  const pauseState = checkPause();
@@ -3198,7 +3236,7 @@ program.command("status").description("Show current Node9 mode, policy source, a
3198
3236
  const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
3199
3237
  console.log("");
3200
3238
  console.log(
3201
- chalk4.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk4.gray(" \u2014 all tool calls allowed")
3239
+ chalk5.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk5.gray(" \u2014 all tool calls allowed")
3202
3240
  );
3203
3241
  }
3204
3242
  console.log("");
@@ -3209,13 +3247,13 @@ program.command("daemon").description("Run the local approval server").argument(
3209
3247
  if (cmd === "stop") return stopDaemon();
3210
3248
  if (cmd === "status") return daemonStatus();
3211
3249
  if (cmd !== "start" && action !== void 0) {
3212
- console.error(chalk4.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
3250
+ console.error(chalk5.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
3213
3251
  process.exit(1);
3214
3252
  }
3215
3253
  if (options.openui) {
3216
3254
  if (isDaemonRunning()) {
3217
3255
  openBrowserLocal();
3218
- console.log(chalk4.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
3256
+ console.log(chalk5.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
3219
3257
  process.exit(0);
3220
3258
  }
3221
3259
  const child = spawn3("node9", ["daemon"], { detached: true, stdio: "ignore" });
@@ -3225,14 +3263,14 @@ program.command("daemon").description("Run the local approval server").argument(
3225
3263
  if (isDaemonRunning()) break;
3226
3264
  }
3227
3265
  openBrowserLocal();
3228
- console.log(chalk4.green(`
3266
+ console.log(chalk5.green(`
3229
3267
  \u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
3230
3268
  process.exit(0);
3231
3269
  }
3232
3270
  if (options.background) {
3233
3271
  const child = spawn3("node9", ["daemon"], { detached: true, stdio: "ignore" });
3234
3272
  child.unref();
3235
- console.log(chalk4.green(`
3273
+ console.log(chalk5.green(`
3236
3274
  \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
3237
3275
  process.exit(0);
3238
3276
  }
@@ -3284,31 +3322,32 @@ RAW: ${raw}
3284
3322
  const sendBlock = (msg, result2) => {
3285
3323
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
3286
3324
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
3287
- console.error(chalk4.red(`
3325
+ console.error(chalk5.red(`
3288
3326
  \u{1F6D1} Node9 blocked "${toolName}"`));
3289
- console.error(chalk4.gray(` Triggered by: ${blockedByContext}`));
3290
- if (result2?.changeHint) console.error(chalk4.cyan(` To change: ${result2.changeHint}`));
3327
+ console.error(chalk5.gray(` Triggered by: ${blockedByContext}`));
3328
+ if (result2?.changeHint) console.error(chalk5.cyan(` To change: ${result2.changeHint}`));
3291
3329
  console.error("");
3292
3330
  let aiFeedbackMessage = "";
3293
3331
  if (isHumanDecision) {
3294
3332
  aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: The human user specifically REJECTED this action.
3295
- REASON: ${msg || "No specific reason provided by user."}
3333
+ REASON: ${msg || "No specific reason provided by user."}
3296
3334
 
3297
- INSTRUCTIONS FOR AI AGENT:
3298
- - Do NOT retry this exact command immediately.
3299
- - Explain to the user that you understand they blocked the action.
3300
- - Ask the user if there is an alternative approach they would prefer, or if they intended to block this action entirely.
3301
- - If you believe this action is critical, explain your reasoning to the user and ask them to run 'node9 pause 15m' to allow you to proceed.`;
3335
+ INSTRUCTIONS FOR AI AGENT:
3336
+ - Do NOT retry this exact command immediately.
3337
+ - Explain to the user that you understand they blocked the action.
3338
+ - Ask the user if there is an alternative approach they would prefer, or if they intended to block this action entirely.
3339
+ - If you believe this action is critical, explain your reasoning to the user and ask them to run 'node9 pause 15m' to allow you to proceed.`;
3302
3340
  } else {
3303
3341
  aiFeedbackMessage = `NODE9 SECURITY INTERVENTION: Action blocked by automated policy [${blockedByContext}].
3304
- REASON: ${msg}
3342
+ REASON: ${msg}
3305
3343
 
3306
- INSTRUCTIONS FOR AI AGENT:
3307
- - This command violates the current security configuration.
3308
- - Do NOT attempt to bypass this rule with bash syntax tricks; it will be blocked again.
3309
- - Pivot to a non-destructive or read-only alternative.
3310
- - Inform the user which security rule was triggered.`;
3344
+ INSTRUCTIONS FOR AI AGENT:
3345
+ - This command violates the current security configuration.
3346
+ - Do NOT attempt to bypass this rule with bash syntax tricks; it will be blocked again.
3347
+ - Pivot to a non-destructive or read-only alternative.
3348
+ - Inform the user which security rule was triggered.`;
3311
3349
  }
3350
+ console.error(chalk5.dim(` (Detailed instructions sent to AI agent)`));
3312
3351
  process.stdout.write(
3313
3352
  JSON.stringify({
3314
3353
  decision: "block",
@@ -3349,7 +3388,7 @@ RAW: ${raw}
3349
3388
  process.exit(0);
3350
3389
  }
3351
3390
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
3352
- console.error(chalk4.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
3391
+ console.error(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
3353
3392
  const daemonReady = await autoStartDaemonAndWait();
3354
3393
  if (daemonReady) {
3355
3394
  const retry = await authorizeHeadless(toolName, toolInput, false, meta);
@@ -3457,7 +3496,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
3457
3496
  const ms = parseDuration(options.duration);
3458
3497
  if (ms === null) {
3459
3498
  console.error(
3460
- chalk4.red(`
3499
+ chalk5.red(`
3461
3500
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
3462
3501
  `)
3463
3502
  );
@@ -3465,20 +3504,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
3465
3504
  }
3466
3505
  pauseNode9(ms, options.duration);
3467
3506
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
3468
- console.log(chalk4.yellow(`
3507
+ console.log(chalk5.yellow(`
3469
3508
  \u23F8 Node9 paused until ${expiresAt}`));
3470
- console.log(chalk4.gray(` All tool calls will be allowed without review.`));
3471
- console.log(chalk4.gray(` Run "node9 resume" to re-enable early.
3509
+ console.log(chalk5.gray(` All tool calls will be allowed without review.`));
3510
+ console.log(chalk5.gray(` Run "node9 resume" to re-enable early.
3472
3511
  `));
3473
3512
  });
3474
3513
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
3475
3514
  const { paused } = checkPause();
3476
3515
  if (!paused) {
3477
- console.log(chalk4.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
3516
+ console.log(chalk5.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
3478
3517
  return;
3479
3518
  }
3480
3519
  resumeNode9();
3481
- console.log(chalk4.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
3520
+ console.log(chalk5.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
3482
3521
  });
3483
3522
  var HOOK_BASED_AGENTS = {
3484
3523
  claude: "claude",
@@ -3491,21 +3530,23 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
3491
3530
  if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
3492
3531
  const target = HOOK_BASED_AGENTS[firstArg];
3493
3532
  console.error(
3494
- chalk4.yellow(`
3533
+ chalk5.yellow(`
3495
3534
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
3496
3535
  );
3497
- console.error(chalk4.white(`
3536
+ console.error(chalk5.white(`
3498
3537
  "${target}" uses its own hook system. Use:`));
3499
3538
  console.error(
3500
- chalk4.green(` node9 addto ${target} `) + chalk4.gray("# one-time setup")
3539
+ chalk5.green(` node9 addto ${target} `) + chalk5.gray("# one-time setup")
3501
3540
  );
3502
- console.error(chalk4.green(` ${target} `) + chalk4.gray("# run normally"));
3541
+ console.error(chalk5.green(` ${target} `) + chalk5.gray("# run normally"));
3503
3542
  process.exit(1);
3504
3543
  }
3505
3544
  const fullCommand = commandArgs.join(" ");
3506
- let result = await authorizeHeadless("shell", { command: fullCommand });
3545
+ let result = await authorizeHeadless("shell", { command: fullCommand }, true, {
3546
+ agent: "Terminal"
3547
+ });
3507
3548
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
3508
- console.error(chalk4.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
3549
+ console.error(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
3509
3550
  const daemonReady = await autoStartDaemonAndWait();
3510
3551
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
3511
3552
  }
@@ -3514,12 +3555,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
3514
3555
  }
3515
3556
  if (!result.approved) {
3516
3557
  console.error(
3517
- chalk4.red(`
3558
+ chalk5.red(`
3518
3559
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
3519
3560
  );
3520
3561
  process.exit(1);
3521
3562
  }
3522
- console.error(chalk4.green("\n\u2705 Approved \u2014 running command...\n"));
3563
+ console.error(chalk5.green("\n\u2705 Approved \u2014 running command...\n"));
3523
3564
  await runProxy(fullCommand);
3524
3565
  } else {
3525
3566
  program.help();
@@ -3528,20 +3569,20 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
3528
3569
  program.command("undo").description("Revert the project to the state before the last AI action").action(async () => {
3529
3570
  const hash = getLatestSnapshotHash();
3530
3571
  if (!hash) {
3531
- console.log(chalk4.yellow("\n\u2139\uFE0F No Undo snapshot found for this machine.\n"));
3572
+ console.log(chalk5.yellow("\n\u2139\uFE0F No Undo snapshot found for this machine.\n"));
3532
3573
  return;
3533
3574
  }
3534
- console.log(chalk4.magenta.bold("\n\u23EA NODE9 UNDO ENGINE"));
3535
- console.log(chalk4.white(`Target Snapshot: ${chalk4.gray(hash.slice(0, 7))}`));
3575
+ console.log(chalk5.magenta.bold("\n\u23EA NODE9 UNDO ENGINE"));
3576
+ console.log(chalk5.white(`Target Snapshot: ${chalk5.gray(hash.slice(0, 7))}`));
3536
3577
  const proceed = await confirm3({
3537
3578
  message: "Revert all files to the state before the last AI action?",
3538
3579
  default: false
3539
3580
  });
3540
3581
  if (proceed) {
3541
3582
  if (applyUndo(hash)) {
3542
- console.log(chalk4.green("\u2705 Project reverted successfully.\n"));
3583
+ console.log(chalk5.green("\u2705 Project reverted successfully.\n"));
3543
3584
  } else {
3544
- console.error(chalk4.red("\u274C Undo failed. Ensure you are in a Git repository.\n"));
3585
+ console.error(chalk5.red("\u274C Undo failed. Ensure you are in a Git repository.\n"));
3545
3586
  }
3546
3587
  }
3547
3588
  });