@node9/proxy 1.0.13 → 1.0.15

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.
Files changed (6) hide show
  1. package/README.md +188 -119
  2. package/dist/cli.js +2335 -1097
  3. package/dist/cli.mjs +2315 -1075
  4. package/dist/index.js +500 -125
  5. package/dist/index.mjs +500 -125
  6. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
9
16
  var __copyProps = (to, from, except, desc) => {
10
17
  if (from && typeof from === "object" || typeof from === "function") {
11
18
  for (let key of __getOwnPropNames(from))
@@ -23,25 +30,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
30
  mod
24
31
  ));
25
32
 
26
- // src/cli.ts
27
- var import_commander = require("commander");
28
-
29
- // src/core.ts
30
- var import_chalk2 = __toESM(require("chalk"));
31
- var import_prompts = require("@inquirer/prompts");
32
- var import_fs = __toESM(require("fs"));
33
- var import_path3 = __toESM(require("path"));
34
- var import_os = __toESM(require("os"));
35
- var import_picomatch = __toESM(require("picomatch"));
36
- var import_sh_syntax = require("sh-syntax");
37
-
38
- // src/ui/native.ts
39
- var import_child_process = require("child_process");
40
- var import_path2 = __toESM(require("path"));
41
- var import_chalk = __toESM(require("chalk"));
42
-
43
33
  // src/context-sniper.ts
44
- var import_path = __toESM(require("path"));
45
34
  function smartTruncate(str, maxLen = 500) {
46
35
  if (str.length <= maxLen) return str;
47
36
  const edge = Math.floor(maxLen / 2) - 3;
@@ -71,22 +60,6 @@ function extractContext(text, matchedWord) {
71
60
  ... [${lines.length - end} lines hidden] ...` : "";
72
61
  return { snippet: `${head}${snippet}${tail}`, lineIndex };
73
62
  }
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
63
  function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
91
64
  let intent = "EXEC";
92
65
  let contextSnippet;
@@ -141,11 +114,31 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
141
114
  ...ruleName && { ruleName }
142
115
  };
143
116
  }
117
+ var import_path, CODE_KEYS;
118
+ var init_context_sniper = __esm({
119
+ "src/context-sniper.ts"() {
120
+ "use strict";
121
+ import_path = __toESM(require("path"));
122
+ CODE_KEYS = [
123
+ "command",
124
+ "cmd",
125
+ "shell_command",
126
+ "bash_command",
127
+ "script",
128
+ "code",
129
+ "input",
130
+ "sql",
131
+ "query",
132
+ "arguments",
133
+ "args",
134
+ "param",
135
+ "params",
136
+ "text"
137
+ ];
138
+ }
139
+ });
144
140
 
145
141
  // 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
- };
149
142
  function formatArgs(args, matchedField, matchedWord) {
150
143
  if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
151
144
  let parsed = args;
@@ -355,82 +348,21 @@ end run`;
355
348
  }
356
349
  });
357
350
  }
351
+ var import_child_process, import_path2, import_chalk, isTestEnv;
352
+ var init_native = __esm({
353
+ "src/ui/native.ts"() {
354
+ "use strict";
355
+ import_child_process = require("child_process");
356
+ import_path2 = __toESM(require("path"));
357
+ import_chalk = __toESM(require("chalk"));
358
+ init_context_sniper();
359
+ isTestEnv = () => {
360
+ 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";
361
+ };
362
+ }
363
+ });
358
364
 
359
365
  // 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
366
  function sanitizeConfig(raw) {
435
367
  const result = ConfigFileSchema.safeParse(raw);
436
368
  if (result.success) {
@@ -448,8 +380,8 @@ function sanitizeConfig(raw) {
448
380
  }
449
381
  }
450
382
  const lines = result.error.issues.map((issue) => {
451
- const path8 = issue.path.length > 0 ? issue.path.join(".") : "root";
452
- return ` \u2022 ${path8}: ${issue.message}`;
383
+ const path10 = issue.path.length > 0 ? issue.path.join(".") : "root";
384
+ return ` \u2022 ${path10}: ${issue.message}`;
453
385
  });
454
386
  return {
455
387
  sanitized,
@@ -457,19 +389,389 @@ function sanitizeConfig(raw) {
457
389
  ${lines.join("\n")}`
458
390
  };
459
391
  }
392
+ var import_zod, noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
393
+ var init_config_schema = __esm({
394
+ "src/config-schema.ts"() {
395
+ "use strict";
396
+ import_zod = require("zod");
397
+ noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
398
+ message: "Value must not contain literal newline characters (use \\n instead)"
399
+ });
400
+ SmartConditionSchema = import_zod.z.object({
401
+ field: import_zod.z.string().min(1, "Condition field must not be empty"),
402
+ op: import_zod.z.enum(
403
+ [
404
+ "matches",
405
+ "notMatches",
406
+ "contains",
407
+ "notContains",
408
+ "exists",
409
+ "notExists",
410
+ "matchesGlob",
411
+ "notMatchesGlob"
412
+ ],
413
+ {
414
+ errorMap: () => ({
415
+ message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
416
+ })
417
+ }
418
+ ),
419
+ value: import_zod.z.string().optional(),
420
+ flags: import_zod.z.string().optional()
421
+ });
422
+ SmartRuleSchema = import_zod.z.object({
423
+ name: import_zod.z.string().optional(),
424
+ tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
425
+ conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
426
+ conditionMode: import_zod.z.enum(["all", "any"]).optional(),
427
+ verdict: import_zod.z.enum(["allow", "review", "block"], {
428
+ errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
429
+ }),
430
+ reason: import_zod.z.string().optional()
431
+ });
432
+ ConfigFileSchema = import_zod.z.object({
433
+ version: import_zod.z.string().optional(),
434
+ settings: import_zod.z.object({
435
+ mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
436
+ autoStartDaemon: import_zod.z.boolean().optional(),
437
+ enableUndo: import_zod.z.boolean().optional(),
438
+ enableHookLogDebug: import_zod.z.boolean().optional(),
439
+ approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
440
+ approvers: import_zod.z.object({
441
+ native: import_zod.z.boolean().optional(),
442
+ browser: import_zod.z.boolean().optional(),
443
+ cloud: import_zod.z.boolean().optional(),
444
+ terminal: import_zod.z.boolean().optional()
445
+ }).optional(),
446
+ environment: import_zod.z.string().optional(),
447
+ slackEnabled: import_zod.z.boolean().optional(),
448
+ enableTrustSessions: import_zod.z.boolean().optional(),
449
+ allowGlobalPause: import_zod.z.boolean().optional()
450
+ }).optional(),
451
+ policy: import_zod.z.object({
452
+ sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
453
+ dangerousWords: import_zod.z.array(noNewlines).optional(),
454
+ ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
455
+ toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
456
+ smartRules: import_zod.z.array(SmartRuleSchema).optional(),
457
+ snapshot: import_zod.z.object({
458
+ tools: import_zod.z.array(import_zod.z.string()).optional(),
459
+ onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
460
+ ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
461
+ }).optional(),
462
+ dlp: import_zod.z.object({
463
+ enabled: import_zod.z.boolean().optional(),
464
+ scanIgnoredTools: import_zod.z.boolean().optional()
465
+ }).optional()
466
+ }).optional(),
467
+ environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
468
+ }).strict({ message: "Config contains unknown top-level keys" });
469
+ }
470
+ });
471
+
472
+ // src/shields.ts
473
+ function resolveShieldName(input) {
474
+ const lower = input.toLowerCase();
475
+ if (SHIELDS[lower]) return lower;
476
+ for (const [name, def] of Object.entries(SHIELDS)) {
477
+ if (def.aliases.includes(lower)) return name;
478
+ }
479
+ return null;
480
+ }
481
+ function getShield(name) {
482
+ const resolved = resolveShieldName(name);
483
+ return resolved ? SHIELDS[resolved] : null;
484
+ }
485
+ function listShields() {
486
+ return Object.values(SHIELDS);
487
+ }
488
+ function readActiveShields() {
489
+ try {
490
+ const raw = import_fs.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
491
+ if (!raw.trim()) return [];
492
+ const parsed = JSON.parse(raw);
493
+ if (Array.isArray(parsed.active)) {
494
+ return parsed.active.filter(
495
+ (e) => typeof e === "string" && e.length > 0 && e in SHIELDS
496
+ );
497
+ }
498
+ } catch (err) {
499
+ if (err.code !== "ENOENT") {
500
+ process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
501
+ `);
502
+ }
503
+ }
504
+ return [];
505
+ }
506
+ function writeActiveShields(active) {
507
+ import_fs.default.mkdirSync(import_path3.default.dirname(SHIELDS_STATE_FILE), { recursive: true });
508
+ const tmp = `${SHIELDS_STATE_FILE}.${import_crypto.default.randomBytes(6).toString("hex")}.tmp`;
509
+ import_fs.default.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
510
+ import_fs.default.renameSync(tmp, SHIELDS_STATE_FILE);
511
+ }
512
+ var import_fs, import_path3, import_os, import_crypto, SHIELDS, SHIELDS_STATE_FILE;
513
+ var init_shields = __esm({
514
+ "src/shields.ts"() {
515
+ "use strict";
516
+ import_fs = __toESM(require("fs"));
517
+ import_path3 = __toESM(require("path"));
518
+ import_os = __toESM(require("os"));
519
+ import_crypto = __toESM(require("crypto"));
520
+ SHIELDS = {
521
+ postgres: {
522
+ name: "postgres",
523
+ description: "Protects PostgreSQL databases from destructive AI operations",
524
+ aliases: ["pg", "postgresql"],
525
+ smartRules: [
526
+ {
527
+ name: "shield:postgres:block-drop-table",
528
+ tool: "*",
529
+ conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
530
+ verdict: "block",
531
+ reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
532
+ },
533
+ {
534
+ name: "shield:postgres:block-truncate",
535
+ tool: "*",
536
+ conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
537
+ verdict: "block",
538
+ reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
539
+ },
540
+ {
541
+ name: "shield:postgres:block-drop-column",
542
+ tool: "*",
543
+ conditions: [
544
+ { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
545
+ ],
546
+ verdict: "block",
547
+ reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
548
+ },
549
+ {
550
+ name: "shield:postgres:review-grant-revoke",
551
+ tool: "*",
552
+ conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
553
+ verdict: "review",
554
+ reason: "Permission changes require human approval (Postgres shield)"
555
+ }
556
+ ],
557
+ dangerousWords: ["dropdb", "pg_dropcluster"]
558
+ },
559
+ github: {
560
+ name: "github",
561
+ description: "Protects GitHub repositories from destructive AI operations",
562
+ aliases: ["git"],
563
+ smartRules: [
564
+ {
565
+ // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
566
+ // This rule adds coverage for `git push --delete` which the built-in does not match.
567
+ name: "shield:github:review-delete-branch-remote",
568
+ tool: "bash",
569
+ conditions: [
570
+ {
571
+ field: "command",
572
+ op: "matches",
573
+ value: "git\\s+push\\s+.*--delete",
574
+ flags: "i"
575
+ }
576
+ ],
577
+ verdict: "review",
578
+ reason: "Remote branch deletion requires human approval (GitHub shield)"
579
+ },
580
+ {
581
+ name: "shield:github:block-delete-repo",
582
+ tool: "*",
583
+ conditions: [
584
+ { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
585
+ ],
586
+ verdict: "block",
587
+ reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
588
+ }
589
+ ],
590
+ dangerousWords: []
591
+ },
592
+ aws: {
593
+ name: "aws",
594
+ description: "Protects AWS infrastructure from destructive AI operations",
595
+ aliases: ["amazon"],
596
+ smartRules: [
597
+ {
598
+ name: "shield:aws:block-delete-s3-bucket",
599
+ tool: "*",
600
+ conditions: [
601
+ {
602
+ field: "command",
603
+ op: "matches",
604
+ value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
605
+ flags: "i"
606
+ }
607
+ ],
608
+ verdict: "block",
609
+ reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
610
+ },
611
+ {
612
+ name: "shield:aws:review-iam-changes",
613
+ tool: "*",
614
+ conditions: [
615
+ {
616
+ field: "command",
617
+ op: "matches",
618
+ value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
619
+ flags: "i"
620
+ }
621
+ ],
622
+ verdict: "review",
623
+ reason: "IAM changes require human approval (AWS shield)"
624
+ },
625
+ {
626
+ name: "shield:aws:block-ec2-terminate",
627
+ tool: "*",
628
+ conditions: [
629
+ {
630
+ field: "command",
631
+ op: "matches",
632
+ value: "aws\\s+ec2\\s+terminate-instances",
633
+ flags: "i"
634
+ }
635
+ ],
636
+ verdict: "block",
637
+ reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
638
+ },
639
+ {
640
+ name: "shield:aws:review-rds-delete",
641
+ tool: "*",
642
+ conditions: [
643
+ { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
644
+ ],
645
+ verdict: "review",
646
+ reason: "RDS deletion requires human approval (AWS shield)"
647
+ }
648
+ ],
649
+ dangerousWords: []
650
+ },
651
+ filesystem: {
652
+ name: "filesystem",
653
+ description: "Protects the local filesystem from dangerous AI operations",
654
+ aliases: ["fs"],
655
+ smartRules: [
656
+ {
657
+ name: "shield:filesystem:review-chmod-777",
658
+ tool: "bash",
659
+ conditions: [
660
+ { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
661
+ ],
662
+ verdict: "review",
663
+ reason: "chmod 777 requires human approval (filesystem shield)"
664
+ },
665
+ {
666
+ name: "shield:filesystem:review-write-etc",
667
+ tool: "bash",
668
+ conditions: [
669
+ {
670
+ field: "command",
671
+ // Narrow to write-indicative operations to avoid approval fatigue on reads.
672
+ // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
673
+ op: "matches",
674
+ value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
675
+ }
676
+ ],
677
+ verdict: "review",
678
+ reason: "Writing to /etc requires human approval (filesystem shield)"
679
+ }
680
+ ],
681
+ // dd removed: too common as a legitimate tool (disk imaging, file ops).
682
+ // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
683
+ // wipefs retained: rarely legitimate in an agent context and not in built-ins.
684
+ dangerousWords: ["wipefs"]
685
+ }
686
+ };
687
+ SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
688
+ }
689
+ });
690
+
691
+ // src/dlp.ts
692
+ function maskSecret(raw, pattern) {
693
+ const match = raw.match(pattern);
694
+ if (!match) return "****";
695
+ const secret = match[0];
696
+ if (secret.length < 8) return "****";
697
+ const prefix = secret.slice(0, 4);
698
+ const suffix = secret.slice(-4);
699
+ const stars = "*".repeat(Math.min(secret.length - 8, 12));
700
+ return `${prefix}${stars}${suffix}`;
701
+ }
702
+ function scanArgs(args, depth = 0, fieldPath = "args") {
703
+ if (depth > MAX_DEPTH || args === null || args === void 0) return null;
704
+ if (Array.isArray(args)) {
705
+ for (let i = 0; i < args.length; i++) {
706
+ const match = scanArgs(args[i], depth + 1, `${fieldPath}[${i}]`);
707
+ if (match) return match;
708
+ }
709
+ return null;
710
+ }
711
+ if (typeof args === "object") {
712
+ for (const [key, value] of Object.entries(args)) {
713
+ const match = scanArgs(value, depth + 1, `${fieldPath}.${key}`);
714
+ if (match) return match;
715
+ }
716
+ return null;
717
+ }
718
+ if (typeof args === "string") {
719
+ const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
720
+ for (const pattern of DLP_PATTERNS) {
721
+ if (pattern.regex.test(text)) {
722
+ return {
723
+ patternName: pattern.name,
724
+ fieldPath,
725
+ redactedSample: maskSecret(text, pattern.regex),
726
+ severity: pattern.severity
727
+ };
728
+ }
729
+ }
730
+ if (text.length < MAX_JSON_PARSE_BYTES) {
731
+ const trimmed = text.trim();
732
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
733
+ try {
734
+ const parsed = JSON.parse(text);
735
+ const inner = scanArgs(parsed, depth + 1, fieldPath);
736
+ if (inner) return inner;
737
+ } catch {
738
+ }
739
+ }
740
+ }
741
+ }
742
+ return null;
743
+ }
744
+ var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
745
+ var init_dlp = __esm({
746
+ "src/dlp.ts"() {
747
+ "use strict";
748
+ DLP_PATTERNS = [
749
+ { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
750
+ { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
751
+ { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
752
+ { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
753
+ { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
754
+ {
755
+ name: "Private Key (PEM)",
756
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
757
+ severity: "block"
758
+ },
759
+ { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
760
+ ];
761
+ MAX_DEPTH = 5;
762
+ MAX_STRING_BYTES = 1e5;
763
+ MAX_JSON_PARSE_BYTES = 1e4;
764
+ }
765
+ });
460
766
 
461
767
  // src/core.ts
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");
466
768
  function checkPause() {
467
769
  try {
468
- if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
469
- const state = JSON.parse(import_fs.default.readFileSync(PAUSED_FILE, "utf-8"));
770
+ if (!import_fs2.default.existsSync(PAUSED_FILE)) return { paused: false };
771
+ const state = JSON.parse(import_fs2.default.readFileSync(PAUSED_FILE, "utf-8"));
470
772
  if (state.expiry > 0 && Date.now() >= state.expiry) {
471
773
  try {
472
- import_fs.default.unlinkSync(PAUSED_FILE);
774
+ import_fs2.default.unlinkSync(PAUSED_FILE);
473
775
  } catch {
474
776
  }
475
777
  return { paused: false };
@@ -480,11 +782,11 @@ function checkPause() {
480
782
  }
481
783
  }
482
784
  function atomicWriteSync(filePath, data, options) {
483
- const dir = import_path3.default.dirname(filePath);
484
- if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
485
- const tmpPath = `${filePath}.${import_os.default.hostname()}.${process.pid}.tmp`;
486
- import_fs.default.writeFileSync(tmpPath, data, options);
487
- import_fs.default.renameSync(tmpPath, filePath);
785
+ const dir = import_path4.default.dirname(filePath);
786
+ if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
787
+ const tmpPath = `${filePath}.${import_os2.default.hostname()}.${process.pid}.tmp`;
788
+ import_fs2.default.writeFileSync(tmpPath, data, options);
789
+ import_fs2.default.renameSync(tmpPath, filePath);
488
790
  }
489
791
  function pauseNode9(durationMs, durationStr) {
490
792
  const state = { expiry: Date.now() + durationMs, duration: durationStr };
@@ -492,18 +794,18 @@ function pauseNode9(durationMs, durationStr) {
492
794
  }
493
795
  function resumeNode9() {
494
796
  try {
495
- if (import_fs.default.existsSync(PAUSED_FILE)) import_fs.default.unlinkSync(PAUSED_FILE);
797
+ if (import_fs2.default.existsSync(PAUSED_FILE)) import_fs2.default.unlinkSync(PAUSED_FILE);
496
798
  } catch {
497
799
  }
498
800
  }
499
801
  function getActiveTrustSession(toolName) {
500
802
  try {
501
- if (!import_fs.default.existsSync(TRUST_FILE)) return false;
502
- const trust = JSON.parse(import_fs.default.readFileSync(TRUST_FILE, "utf-8"));
803
+ if (!import_fs2.default.existsSync(TRUST_FILE)) return false;
804
+ const trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
503
805
  const now = Date.now();
504
806
  const active = trust.entries.filter((e) => e.expiry > now);
505
807
  if (active.length !== trust.entries.length) {
506
- import_fs.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
808
+ import_fs2.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
507
809
  }
508
810
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
509
811
  } catch {
@@ -514,8 +816,8 @@ function writeTrustSession(toolName, durationMs) {
514
816
  try {
515
817
  let trust = { entries: [] };
516
818
  try {
517
- if (import_fs.default.existsSync(TRUST_FILE)) {
518
- trust = JSON.parse(import_fs.default.readFileSync(TRUST_FILE, "utf-8"));
819
+ if (import_fs2.default.existsSync(TRUST_FILE)) {
820
+ trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
519
821
  }
520
822
  } catch {
521
823
  }
@@ -531,9 +833,9 @@ function writeTrustSession(toolName, durationMs) {
531
833
  }
532
834
  function appendToLog(logPath, entry) {
533
835
  try {
534
- const dir = import_path3.default.dirname(logPath);
535
- if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
536
- import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
836
+ const dir = import_path4.default.dirname(logPath);
837
+ if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
838
+ import_fs2.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
537
839
  } catch {
538
840
  }
539
841
  }
@@ -545,7 +847,7 @@ function appendHookDebug(toolName, args, meta) {
545
847
  args: safeArgs,
546
848
  agent: meta?.agent,
547
849
  mcpServer: meta?.mcpServer,
548
- hostname: import_os.default.hostname(),
850
+ hostname: import_os2.default.hostname(),
549
851
  cwd: process.cwd()
550
852
  });
551
853
  }
@@ -559,7 +861,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
559
861
  checkedBy,
560
862
  agent: meta?.agent,
561
863
  mcpServer: meta?.mcpServer,
562
- hostname: import_os.default.hostname()
864
+ hostname: import_os2.default.hostname()
563
865
  });
564
866
  }
565
867
  function tokenize(toolName) {
@@ -575,9 +877,9 @@ function matchesPattern(text, patterns) {
575
877
  const withoutDotSlash = text.replace(/^\.\//, "");
576
878
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
577
879
  }
578
- function getNestedValue(obj, path8) {
880
+ function getNestedValue(obj, path10) {
579
881
  if (!obj || typeof obj !== "object") return null;
580
- return path8.split(".").reduce((prev, curr) => prev?.[curr], obj);
882
+ return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
581
883
  }
582
884
  function shouldSnapshot(toolName, args, config) {
583
885
  if (!config.settings.enableUndo) return false;
@@ -622,6 +924,10 @@ function evaluateSmartConditions(args, rule) {
622
924
  return true;
623
925
  }
624
926
  }
927
+ case "matchesGlob":
928
+ return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
929
+ case "notMatchesGlob":
930
+ return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : true;
625
931
  default:
626
932
  return false;
627
933
  }
@@ -643,7 +949,6 @@ function isSqlTool(toolName, toolInspection) {
643
949
  const fieldName = toolInspection[matchingPattern];
644
950
  return fieldName === "sql" || fieldName === "query";
645
951
  }
646
- var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
647
952
  async function analyzeShellCommand(command) {
648
953
  const actions = [];
649
954
  const paths = [];
@@ -725,188 +1030,14 @@ function redactSecrets(text) {
725
1030
  );
726
1031
  return redacted;
727
1032
  }
728
- var DANGEROUS_WORDS = [
729
- "mkfs",
730
- // formats/wipes a filesystem partition
731
- "shred"
732
- // permanently overwrites file contents (unrecoverable)
733
- ];
734
- var DEFAULT_CONFIG = {
735
- settings: {
736
- mode: "standard",
737
- autoStartDaemon: true,
738
- enableUndo: true,
739
- // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
740
- enableHookLogDebug: false,
741
- approvalTimeoutMs: 0,
742
- // 0 = disabled; set e.g. 30000 for 30-second auto-deny
743
- approvers: { native: true, browser: true, cloud: true, terminal: true }
744
- },
745
- policy: {
746
- sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
747
- dangerousWords: DANGEROUS_WORDS,
748
- ignoredTools: [
749
- "list_*",
750
- "get_*",
751
- "read_*",
752
- "describe_*",
753
- "read",
754
- "glob",
755
- "grep",
756
- "ls",
757
- "notebookread",
758
- "notebookedit",
759
- "webfetch",
760
- "websearch",
761
- "exitplanmode",
762
- "askuserquestion",
763
- "agent",
764
- "task*",
765
- "toolsearch",
766
- "mcp__ide__*",
767
- "getDiagnostics"
768
- ],
769
- toolInspection: {
770
- bash: "command",
771
- shell: "command",
772
- run_shell_command: "command",
773
- "terminal.execute": "command",
774
- "postgres:query": "sql"
775
- },
776
- snapshot: {
777
- tools: [
778
- "str_replace_based_edit_tool",
779
- "write_file",
780
- "edit_file",
781
- "create_file",
782
- "edit",
783
- "replace"
784
- ],
785
- onlyPaths: [],
786
- ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
787
- },
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.
791
- {
792
- action: "rm",
793
- allowPaths: [
794
- "**/node_modules/**",
795
- "dist/**",
796
- "build/**",
797
- ".next/**",
798
- "coverage/**",
799
- ".cache/**",
800
- "tmp/**",
801
- "temp/**",
802
- ".DS_Store"
803
- ]
804
- }
805
- ],
806
- smartRules: [
807
- // ── SQL safety ────────────────────────────────────────────────────────
808
- {
809
- name: "no-delete-without-where",
810
- tool: "*",
811
- conditions: [
812
- { field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
813
- { field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
814
- ],
815
- conditionMode: "all",
816
- verdict: "review",
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"
896
- }
897
- ]
898
- },
899
- environments: {}
900
- };
901
- var cachedConfig = null;
902
1033
  function _resetConfigCache() {
903
1034
  cachedConfig = null;
904
1035
  }
905
1036
  function getGlobalSettings() {
906
1037
  try {
907
- const globalConfigPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
908
- if (import_fs.default.existsSync(globalConfigPath)) {
909
- const parsed = JSON.parse(import_fs.default.readFileSync(globalConfigPath, "utf-8"));
1038
+ const globalConfigPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
1039
+ if (import_fs2.default.existsSync(globalConfigPath)) {
1040
+ const parsed = JSON.parse(import_fs2.default.readFileSync(globalConfigPath, "utf-8"));
910
1041
  const settings = parsed.settings || {};
911
1042
  return {
912
1043
  mode: settings.mode || "standard",
@@ -928,9 +1059,9 @@ function getGlobalSettings() {
928
1059
  }
929
1060
  function getInternalToken() {
930
1061
  try {
931
- const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
932
- if (!import_fs.default.existsSync(pidFile)) return null;
933
- const data = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
1062
+ const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1063
+ if (!import_fs2.default.existsSync(pidFile)) return null;
1064
+ const data = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
934
1065
  process.kill(data.pid, 0);
935
1066
  return data.internalToken ?? null;
936
1067
  } catch {
@@ -945,7 +1076,8 @@ async function evaluatePolicy(toolName, args, agent) {
945
1076
  (rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
946
1077
  );
947
1078
  if (matchedRule) {
948
- if (matchedRule.verdict === "allow") return { decision: "allow" };
1079
+ if (matchedRule.verdict === "allow")
1080
+ return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
949
1081
  return {
950
1082
  decision: matchedRule.verdict,
951
1083
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
@@ -956,13 +1088,11 @@ async function evaluatePolicy(toolName, args, agent) {
956
1088
  }
957
1089
  }
958
1090
  let allTokens = [];
959
- let actionTokens = [];
960
1091
  let pathTokens = [];
961
1092
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
962
1093
  if (shellCommand) {
963
1094
  const analyzed = await analyzeShellCommand(shellCommand);
964
1095
  allTokens = analyzed.allTokens;
965
- actionTokens = analyzed.actions;
966
1096
  pathTokens = analyzed.paths;
967
1097
  const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
968
1098
  if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
@@ -970,11 +1100,9 @@ async function evaluatePolicy(toolName, args, agent) {
970
1100
  }
971
1101
  if (isSqlTool(toolName, config.policy.toolInspection)) {
972
1102
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
973
- actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
974
1103
  }
975
1104
  } else {
976
1105
  allTokens = tokenize(toolName);
977
- actionTokens = [toolName];
978
1106
  if (args && typeof args === "object") {
979
1107
  const flattenedArgs = JSON.stringify(args).toLowerCase();
980
1108
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
@@ -997,29 +1125,6 @@ async function evaluatePolicy(toolName, args, agent) {
997
1125
  const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
998
1126
  if (allInSandbox) return { decision: "allow" };
999
1127
  }
1000
- for (const action of actionTokens) {
1001
- const rule = config.policy.rules.find(
1002
- (r) => r.action === action || matchesPattern(action, r.action)
1003
- );
1004
- if (rule) {
1005
- if (pathTokens.length > 0) {
1006
- const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
1007
- if (anyBlocked)
1008
- return {
1009
- decision: "review",
1010
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
1011
- tier: 5
1012
- };
1013
- const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
1014
- if (allAllowed) return { decision: "allow" };
1015
- }
1016
- return {
1017
- decision: "review",
1018
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
1019
- tier: 5
1020
- };
1021
- }
1022
- }
1023
1128
  let matchedDangerousWord;
1024
1129
  const isDangerous = allTokens.some(
1025
1130
  (token) => config.policy.dangerousWords.some((word) => {
@@ -1071,9 +1176,9 @@ async function evaluatePolicy(toolName, args, agent) {
1071
1176
  }
1072
1177
  async function explainPolicy(toolName, args) {
1073
1178
  const steps = [];
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");
1179
+ const globalPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
1180
+ const projectPath = import_path4.default.join(process.cwd(), "node9.config.json");
1181
+ const credsPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
1077
1182
  const waterfall = [
1078
1183
  {
1079
1184
  tier: 1,
@@ -1084,19 +1189,19 @@ async function explainPolicy(toolName, args) {
1084
1189
  {
1085
1190
  tier: 2,
1086
1191
  label: "Cloud policy",
1087
- status: import_fs.default.existsSync(credsPath) ? "active" : "missing",
1088
- note: import_fs.default.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1192
+ status: import_fs2.default.existsSync(credsPath) ? "active" : "missing",
1193
+ note: import_fs2.default.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1089
1194
  },
1090
1195
  {
1091
1196
  tier: 3,
1092
1197
  label: "Project config",
1093
- status: import_fs.default.existsSync(projectPath) ? "active" : "missing",
1198
+ status: import_fs2.default.existsSync(projectPath) ? "active" : "missing",
1094
1199
  path: projectPath
1095
1200
  },
1096
1201
  {
1097
1202
  tier: 4,
1098
1203
  label: "Global config",
1099
- status: import_fs.default.existsSync(globalPath) ? "active" : "missing",
1204
+ status: import_fs2.default.existsSync(globalPath) ? "active" : "missing",
1100
1205
  path: globalPath
1101
1206
  },
1102
1207
  {
@@ -1107,7 +1212,28 @@ async function explainPolicy(toolName, args) {
1107
1212
  }
1108
1213
  ];
1109
1214
  const config = getConfig();
1110
- if (matchesPattern(toolName, config.policy.ignoredTools)) {
1215
+ const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
1216
+ if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
1217
+ const dlpMatch = args !== void 0 ? scanArgs(args) : null;
1218
+ if (dlpMatch) {
1219
+ steps.push({
1220
+ name: "DLP Content Scanner",
1221
+ outcome: dlpMatch.severity === "block" ? "block" : "review",
1222
+ detail: `\u{1F6A8} ${dlpMatch.patternName} detected in ${dlpMatch.fieldPath} \u2014 sample: ${dlpMatch.redactedSample}`,
1223
+ isFinal: dlpMatch.severity === "block"
1224
+ });
1225
+ if (dlpMatch.severity === "block") {
1226
+ return { tool: toolName, args, waterfall, steps, decision: "block" };
1227
+ }
1228
+ } else {
1229
+ steps.push({
1230
+ name: "DLP Content Scanner",
1231
+ outcome: "checked",
1232
+ detail: "No sensitive credentials detected in args"
1233
+ });
1234
+ }
1235
+ }
1236
+ if (wouldBeIgnored) {
1111
1237
  steps.push({
1112
1238
  name: "Ignored tools",
1113
1239
  outcome: "allow",
@@ -1160,13 +1286,11 @@ async function explainPolicy(toolName, args) {
1160
1286
  steps.push({ name: "Smart rules", outcome: "skip", detail: "No smart rules configured" });
1161
1287
  }
1162
1288
  let allTokens = [];
1163
- let actionTokens = [];
1164
1289
  let pathTokens = [];
1165
1290
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
1166
1291
  if (shellCommand) {
1167
1292
  const analyzed = await analyzeShellCommand(shellCommand);
1168
1293
  allTokens = analyzed.allTokens;
1169
- actionTokens = analyzed.actions;
1170
1294
  pathTokens = analyzed.paths;
1171
1295
  const patterns = Object.keys(config.policy.toolInspection);
1172
1296
  const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
@@ -1200,7 +1324,6 @@ async function explainPolicy(toolName, args) {
1200
1324
  });
1201
1325
  if (isSqlTool(toolName, config.policy.toolInspection)) {
1202
1326
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
1203
- actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
1204
1327
  steps.push({
1205
1328
  name: "SQL token stripping",
1206
1329
  outcome: "checked",
@@ -1209,7 +1332,6 @@ async function explainPolicy(toolName, args) {
1209
1332
  }
1210
1333
  } else {
1211
1334
  allTokens = tokenize(toolName);
1212
- actionTokens = [toolName];
1213
1335
  let detail = `No toolInspection match for "${toolName}" \u2014 tokens: [${allTokens.join(", ")}]`;
1214
1336
  if (args && typeof args === "object") {
1215
1337
  const flattenedArgs = JSON.stringify(args).toLowerCase();
@@ -1250,65 +1372,6 @@ async function explainPolicy(toolName, args) {
1250
1372
  detail: pathTokens.length === 0 ? "No path tokens found in input" : "No sandbox paths configured"
1251
1373
  });
1252
1374
  }
1253
- let ruleMatched = false;
1254
- for (const action of actionTokens) {
1255
- const rule = config.policy.rules.find(
1256
- (r) => r.action === action || matchesPattern(action, r.action)
1257
- );
1258
- if (rule) {
1259
- ruleMatched = true;
1260
- if (pathTokens.length > 0) {
1261
- const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
1262
- if (anyBlocked) {
1263
- steps.push({
1264
- name: "Policy rules",
1265
- outcome: "review",
1266
- detail: `Rule "${rule.action}" matched + path is in blockPaths`,
1267
- isFinal: true
1268
- });
1269
- return {
1270
- tool: toolName,
1271
- args,
1272
- waterfall,
1273
- steps,
1274
- decision: "review",
1275
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
1276
- };
1277
- }
1278
- const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
1279
- if (allAllowed) {
1280
- steps.push({
1281
- name: "Policy rules",
1282
- outcome: "allow",
1283
- detail: `Rule "${rule.action}" matched + all paths are in allowPaths`,
1284
- isFinal: true
1285
- });
1286
- return { tool: toolName, args, waterfall, steps, decision: "allow" };
1287
- }
1288
- }
1289
- steps.push({
1290
- name: "Policy rules",
1291
- outcome: "review",
1292
- detail: `Rule "${rule.action}" matched \u2014 default block (no path exception)`,
1293
- isFinal: true
1294
- });
1295
- return {
1296
- tool: toolName,
1297
- args,
1298
- waterfall,
1299
- steps,
1300
- decision: "review",
1301
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
1302
- };
1303
- }
1304
- }
1305
- if (!ruleMatched) {
1306
- steps.push({
1307
- name: "Policy rules",
1308
- outcome: "skip",
1309
- detail: config.policy.rules.length === 0 ? "No rules configured" : `No rule matched [${actionTokens.join(", ")}]`
1310
- });
1311
- }
1312
1375
  let matchedDangerousWord;
1313
1376
  const isDangerous = uniqueTokens.some(
1314
1377
  (token) => config.policy.dangerousWords.some((word) => {
@@ -1373,13 +1436,11 @@ function isIgnoredTool(toolName) {
1373
1436
  const config = getConfig();
1374
1437
  return matchesPattern(toolName, config.policy.ignoredTools);
1375
1438
  }
1376
- var DAEMON_PORT = 7391;
1377
- var DAEMON_HOST = "127.0.0.1";
1378
1439
  function isDaemonRunning() {
1379
1440
  try {
1380
- const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
1381
- if (!import_fs.default.existsSync(pidFile)) return false;
1382
- const { pid, port } = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
1441
+ const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1442
+ if (!import_fs2.default.existsSync(pidFile)) return false;
1443
+ const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1383
1444
  if (port !== DAEMON_PORT) return false;
1384
1445
  process.kill(pid, 0);
1385
1446
  return true;
@@ -1389,16 +1450,16 @@ function isDaemonRunning() {
1389
1450
  }
1390
1451
  function getPersistentDecision(toolName) {
1391
1452
  try {
1392
- const file = import_path3.default.join(import_os.default.homedir(), ".node9", "decisions.json");
1393
- if (!import_fs.default.existsSync(file)) return null;
1394
- const decisions = JSON.parse(import_fs.default.readFileSync(file, "utf-8"));
1453
+ const file = import_path4.default.join(import_os2.default.homedir(), ".node9", "decisions.json");
1454
+ if (!import_fs2.default.existsSync(file)) return null;
1455
+ const decisions = JSON.parse(import_fs2.default.readFileSync(file, "utf-8"));
1395
1456
  const d = decisions[toolName];
1396
1457
  if (d === "allow" || d === "deny") return d;
1397
1458
  } catch {
1398
1459
  }
1399
1460
  return null;
1400
1461
  }
1401
- async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1462
+ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
1402
1463
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1403
1464
  const checkCtrl = new AbortController();
1404
1465
  const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
@@ -1413,6 +1474,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1413
1474
  args,
1414
1475
  agent: meta?.agent,
1415
1476
  mcpServer: meta?.mcpServer,
1477
+ fromCLI: true,
1478
+ // Pass the flight-recorder ID so the daemon uses the same UUID for
1479
+ // activity-result as the CLI used for the pending activity event.
1480
+ // Without this, the two UUIDs never match and tail.ts never resolves
1481
+ // the pending item.
1482
+ activityId,
1416
1483
  ...riskMetadata && { riskMetadata }
1417
1484
  }),
1418
1485
  signal: checkCtrl.signal
@@ -1467,7 +1534,44 @@ async function resolveViaDaemon(id, decision, internalToken) {
1467
1534
  signal: AbortSignal.timeout(3e3)
1468
1535
  });
1469
1536
  }
1537
+ function notifyActivity(data) {
1538
+ return new Promise((resolve) => {
1539
+ try {
1540
+ const payload = JSON.stringify(data);
1541
+ const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
1542
+ sock.on("connect", () => {
1543
+ sock.on("close", resolve);
1544
+ sock.end(payload);
1545
+ });
1546
+ sock.on("error", resolve);
1547
+ } catch {
1548
+ resolve();
1549
+ }
1550
+ });
1551
+ }
1470
1552
  async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
1553
+ if (!options?.calledFromDaemon) {
1554
+ const actId = (0, import_crypto2.randomUUID)();
1555
+ const actTs = Date.now();
1556
+ await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
1557
+ const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
1558
+ ...options,
1559
+ activityId: actId
1560
+ });
1561
+ if (!result.noApprovalMechanism) {
1562
+ await notifyActivity({
1563
+ id: actId,
1564
+ tool: toolName,
1565
+ ts: actTs,
1566
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
1567
+ label: result.blockedByLabel
1568
+ });
1569
+ }
1570
+ return result;
1571
+ }
1572
+ return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
1573
+ }
1574
+ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
1471
1575
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1472
1576
  const pauseState = checkPause();
1473
1577
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
@@ -1490,10 +1594,27 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1490
1594
  let policyMatchedField;
1491
1595
  let policyMatchedWord;
1492
1596
  let riskMetadata;
1493
- if (config.settings.mode === "audit") {
1494
- if (!isIgnoredTool(toolName)) {
1495
- const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
1496
- if (policyResult.decision === "review") {
1597
+ if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
1598
+ const dlpMatch = scanArgs(args);
1599
+ if (dlpMatch) {
1600
+ const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
1601
+ if (dlpMatch.severity === "block") {
1602
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
1603
+ return {
1604
+ approved: false,
1605
+ reason: dlpReason,
1606
+ blockedBy: "local-config",
1607
+ blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
1608
+ };
1609
+ }
1610
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
1611
+ explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
1612
+ }
1613
+ }
1614
+ if (config.settings.mode === "audit") {
1615
+ if (!isIgnoredTool(toolName)) {
1616
+ const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
1617
+ if (policyResult.decision === "review") {
1497
1618
  appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
1498
1619
  if (approvers.cloud && creds?.apiKey) {
1499
1620
  await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
@@ -1709,7 +1830,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1709
1830
  console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
1710
1831
  `));
1711
1832
  }
1712
- const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
1833
+ const daemonDecision = await askDaemon(
1834
+ toolName,
1835
+ args,
1836
+ meta,
1837
+ signal,
1838
+ riskMetadata,
1839
+ options?.activityId
1840
+ );
1713
1841
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
1714
1842
  const isApproved = daemonDecision === "allow";
1715
1843
  return {
@@ -1729,7 +1857,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1729
1857
  racePromises.push(
1730
1858
  (async () => {
1731
1859
  try {
1732
- console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
1860
+ if (explainableLabel.includes("DLP")) {
1861
+ console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
1862
+ console.log(
1863
+ import_chalk2.default.red.bold(` A sensitive secret was detected in the tool arguments!`)
1864
+ );
1865
+ } else {
1866
+ console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
1867
+ }
1733
1868
  console.log(`${import_chalk2.default.bold("Action:")} ${import_chalk2.default.red(toolName)}`);
1734
1869
  console.log(`${import_chalk2.default.bold("Flagged By:")} ${import_chalk2.default.yellow(explainableLabel)}`);
1735
1870
  if (isRemoteLocked) {
@@ -1834,8 +1969,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1834
1969
  }
1835
1970
  function getConfig() {
1836
1971
  if (cachedConfig) return cachedConfig;
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");
1972
+ const globalPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
1973
+ const projectPath = import_path4.default.join(process.cwd(), "node9.config.json");
1839
1974
  const globalConfig = tryLoadConfig(globalPath);
1840
1975
  const projectConfig = tryLoadConfig(projectPath);
1841
1976
  const mergedSettings = {
@@ -1847,13 +1982,13 @@ function getConfig() {
1847
1982
  dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
1848
1983
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
1849
1984
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
1850
- rules: [...DEFAULT_CONFIG.policy.rules],
1851
1985
  smartRules: [...DEFAULT_CONFIG.policy.smartRules],
1852
1986
  snapshot: {
1853
1987
  tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
1854
1988
  onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
1855
1989
  ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
1856
- }
1990
+ },
1991
+ dlp: { ...DEFAULT_CONFIG.policy.dlp }
1857
1992
  };
1858
1993
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
1859
1994
  const applyLayer = (source) => {
@@ -1873,7 +2008,6 @@ function getConfig() {
1873
2008
  if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
1874
2009
  if (p.toolInspection)
1875
2010
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
1876
- if (p.rules) mergedPolicy.rules.push(...p.rules);
1877
2011
  if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
1878
2012
  if (p.snapshot) {
1879
2013
  const s2 = p.snapshot;
@@ -1881,6 +2015,11 @@ function getConfig() {
1881
2015
  if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
1882
2016
  if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
1883
2017
  }
2018
+ if (p.dlp) {
2019
+ const d = p.dlp;
2020
+ if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
2021
+ if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
2022
+ }
1884
2023
  const envs = source.environments || {};
1885
2024
  for (const [envName, envConfig] of Object.entries(envs)) {
1886
2025
  if (envConfig && typeof envConfig === "object") {
@@ -1895,6 +2034,22 @@ function getConfig() {
1895
2034
  };
1896
2035
  applyLayer(globalConfig);
1897
2036
  applyLayer(projectConfig);
2037
+ for (const shieldName of readActiveShields()) {
2038
+ const shield = getShield(shieldName);
2039
+ if (!shield) continue;
2040
+ const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
2041
+ for (const rule of shield.smartRules) {
2042
+ if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
2043
+ }
2044
+ const existingWords = new Set(mergedPolicy.dangerousWords);
2045
+ for (const word of shield.dangerousWords) {
2046
+ if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
2047
+ }
2048
+ }
2049
+ const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
2050
+ for (const rule of ADVISORY_SMART_RULES) {
2051
+ if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
2052
+ }
1898
2053
  if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
1899
2054
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
1900
2055
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
@@ -1910,10 +2065,10 @@ function getConfig() {
1910
2065
  return cachedConfig;
1911
2066
  }
1912
2067
  function tryLoadConfig(filePath) {
1913
- if (!import_fs.default.existsSync(filePath)) return null;
2068
+ if (!import_fs2.default.existsSync(filePath)) return null;
1914
2069
  let raw;
1915
2070
  try {
1916
- raw = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
2071
+ raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
1917
2072
  } catch (err) {
1918
2073
  const msg = err instanceof Error ? err.message : String(err);
1919
2074
  process.stderr.write(
@@ -1975,9 +2130,9 @@ function getCredentials() {
1975
2130
  };
1976
2131
  }
1977
2132
  try {
1978
- const credPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1979
- if (import_fs.default.existsSync(credPath)) {
1980
- const creds = JSON.parse(import_fs.default.readFileSync(credPath, "utf-8"));
2133
+ const credPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
2134
+ if (import_fs2.default.existsSync(credPath)) {
2135
+ const creds = JSON.parse(import_fs2.default.readFileSync(credPath, "utf-8"));
1981
2136
  const profileName = process.env.NODE9_PROFILE || "default";
1982
2137
  const profile = creds[profileName];
1983
2138
  if (profile?.apiKey) {
@@ -2008,9 +2163,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
2008
2163
  context: {
2009
2164
  agent: meta?.agent,
2010
2165
  mcpServer: meta?.mcpServer,
2011
- hostname: import_os.default.hostname(),
2166
+ hostname: import_os2.default.hostname(),
2012
2167
  cwd: process.cwd(),
2013
- platform: import_os.default.platform()
2168
+ platform: import_os2.default.platform()
2014
2169
  }
2015
2170
  }),
2016
2171
  signal: AbortSignal.timeout(5e3)
@@ -2031,9 +2186,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2031
2186
  context: {
2032
2187
  agent: meta?.agent,
2033
2188
  mcpServer: meta?.mcpServer,
2034
- hostname: import_os.default.hostname(),
2189
+ hostname: import_os2.default.hostname(),
2035
2190
  cwd: process.cwd(),
2036
- platform: import_os.default.platform()
2191
+ platform: import_os2.default.platform()
2037
2192
  },
2038
2193
  ...riskMetadata && { riskMetadata }
2039
2194
  }),
@@ -2090,295 +2245,272 @@ async function resolveNode9SaaS(requestId, creds, approved) {
2090
2245
  } catch {
2091
2246
  }
2092
2247
  }
2093
-
2094
- // src/setup.ts
2095
- var import_fs2 = __toESM(require("fs"));
2096
- var import_path4 = __toESM(require("path"));
2097
- var import_os2 = __toESM(require("os"));
2098
- var import_chalk3 = __toESM(require("chalk"));
2099
- var import_prompts2 = require("@inquirer/prompts");
2100
- function printDaemonTip() {
2101
- console.log(
2102
- import_chalk3.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + import_chalk3.default.white("\n To view your history or manage persistent rules, run:") + import_chalk3.default.green("\n node9 daemon --openui")
2103
- );
2104
- }
2105
- function fullPathCommand(subcommand) {
2106
- if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
2107
- const nodeExec = process.execPath;
2108
- const cliScript = process.argv[1];
2109
- return `${nodeExec} ${cliScript} ${subcommand}`;
2110
- }
2111
- function readJson(filePath) {
2112
- try {
2113
- if (import_fs2.default.existsSync(filePath)) {
2114
- return JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
2115
- }
2116
- } catch {
2117
- }
2118
- return null;
2119
- }
2120
- function writeJson(filePath, data) {
2121
- const dir = import_path4.default.dirname(filePath);
2122
- if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
2123
- import_fs2.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
2124
- }
2125
- async function setupClaude() {
2126
- const homeDir2 = import_os2.default.homedir();
2127
- const mcpPath = import_path4.default.join(homeDir2, ".claude.json");
2128
- const hooksPath = import_path4.default.join(homeDir2, ".claude", "settings.json");
2129
- const claudeConfig = readJson(mcpPath) ?? {};
2130
- const settings = readJson(hooksPath) ?? {};
2131
- const servers = claudeConfig.mcpServers ?? {};
2132
- let anythingChanged = false;
2133
- if (!settings.hooks) settings.hooks = {};
2134
- const hasPreHook = settings.hooks.PreToolUse?.some(
2135
- (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
2136
- );
2137
- if (!hasPreHook) {
2138
- if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
2139
- settings.hooks.PreToolUse.push({
2140
- matcher: ".*",
2141
- hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
2142
- });
2143
- console.log(import_chalk3.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
2144
- anythingChanged = true;
2145
- }
2146
- const hasPostHook = settings.hooks.PostToolUse?.some(
2147
- (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
2148
- );
2149
- if (!hasPostHook) {
2150
- if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
2151
- settings.hooks.PostToolUse.push({
2152
- matcher: ".*",
2153
- hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
2154
- });
2155
- console.log(import_chalk3.default.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
2156
- anythingChanged = true;
2157
- }
2158
- if (anythingChanged) {
2159
- writeJson(hooksPath, settings);
2160
- console.log("");
2161
- }
2162
- const serversToWrap = [];
2163
- for (const [name, server] of Object.entries(servers)) {
2164
- if (!server.command || server.command === "node9") continue;
2165
- const parts = [server.command, ...server.args ?? []];
2166
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
2167
- }
2168
- if (serversToWrap.length > 0) {
2169
- console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
2170
- console.log(import_chalk3.default.white(` ${mcpPath}`));
2171
- for (const { name, originalCmd } of serversToWrap) {
2172
- console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
2173
- }
2174
- console.log("");
2175
- const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
2176
- if (proceed) {
2177
- for (const { name, parts } of serversToWrap) {
2178
- servers[name] = { ...servers[name], command: "node9", args: parts };
2248
+ var import_chalk2, import_prompts, import_fs2, import_path4, import_os2, import_net, import_crypto2, import_picomatch, import_sh_syntax, PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
2249
+ var init_core = __esm({
2250
+ "src/core.ts"() {
2251
+ "use strict";
2252
+ import_chalk2 = __toESM(require("chalk"));
2253
+ import_prompts = require("@inquirer/prompts");
2254
+ import_fs2 = __toESM(require("fs"));
2255
+ import_path4 = __toESM(require("path"));
2256
+ import_os2 = __toESM(require("os"));
2257
+ import_net = __toESM(require("net"));
2258
+ import_crypto2 = require("crypto");
2259
+ import_picomatch = __toESM(require("picomatch"));
2260
+ import_sh_syntax = require("sh-syntax");
2261
+ init_native();
2262
+ init_context_sniper();
2263
+ init_config_schema();
2264
+ init_shields();
2265
+ init_dlp();
2266
+ PAUSED_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
2267
+ TRUST_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "trust.json");
2268
+ LOCAL_AUDIT_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "audit.log");
2269
+ HOOK_DEBUG_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
2270
+ SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2271
+ DANGEROUS_WORDS = [
2272
+ "mkfs",
2273
+ // formats/wipes a filesystem partition
2274
+ "shred"
2275
+ // permanently overwrites file contents (unrecoverable)
2276
+ ];
2277
+ DEFAULT_CONFIG = {
2278
+ settings: {
2279
+ mode: "standard",
2280
+ autoStartDaemon: true,
2281
+ enableUndo: true,
2282
+ // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
2283
+ enableHookLogDebug: false,
2284
+ approvalTimeoutMs: 0,
2285
+ // 0 = disabled; set e.g. 30000 for 30-second auto-deny
2286
+ approvers: { native: true, browser: true, cloud: true, terminal: true }
2287
+ },
2288
+ policy: {
2289
+ sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
2290
+ dangerousWords: DANGEROUS_WORDS,
2291
+ ignoredTools: [
2292
+ "list_*",
2293
+ "get_*",
2294
+ "read_*",
2295
+ "describe_*",
2296
+ "read",
2297
+ "glob",
2298
+ "grep",
2299
+ "ls",
2300
+ "notebookread",
2301
+ "notebookedit",
2302
+ "webfetch",
2303
+ "websearch",
2304
+ "exitplanmode",
2305
+ "askuserquestion",
2306
+ "agent",
2307
+ "task*",
2308
+ "toolsearch",
2309
+ "mcp__ide__*",
2310
+ "getDiagnostics"
2311
+ ],
2312
+ toolInspection: {
2313
+ bash: "command",
2314
+ shell: "command",
2315
+ run_shell_command: "command",
2316
+ "terminal.execute": "command",
2317
+ "postgres:query": "sql"
2318
+ },
2319
+ snapshot: {
2320
+ tools: [
2321
+ "str_replace_based_edit_tool",
2322
+ "write_file",
2323
+ "edit_file",
2324
+ "create_file",
2325
+ "edit",
2326
+ "replace"
2327
+ ],
2328
+ onlyPaths: [],
2329
+ ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
2330
+ },
2331
+ smartRules: [
2332
+ // ── rm safety (critical always evaluated first) ──────────────────────
2333
+ {
2334
+ name: "block-rm-rf-home",
2335
+ tool: "bash",
2336
+ conditionMode: "all",
2337
+ conditions: [
2338
+ {
2339
+ field: "command",
2340
+ op: "matches",
2341
+ value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
2342
+ },
2343
+ {
2344
+ field: "command",
2345
+ op: "matches",
2346
+ value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
2347
+ }
2348
+ ],
2349
+ verdict: "block",
2350
+ reason: "Recursive delete of home directory is irreversible"
2351
+ },
2352
+ // ── SQL safety ────────────────────────────────────────────────────────
2353
+ {
2354
+ name: "no-delete-without-where",
2355
+ tool: "*",
2356
+ conditions: [
2357
+ { field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
2358
+ { field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
2359
+ ],
2360
+ conditionMode: "all",
2361
+ verdict: "review",
2362
+ reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
2363
+ },
2364
+ {
2365
+ name: "review-drop-truncate-shell",
2366
+ tool: "bash",
2367
+ conditions: [
2368
+ {
2369
+ field: "command",
2370
+ op: "matches",
2371
+ value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
2372
+ flags: "i"
2373
+ }
2374
+ ],
2375
+ conditionMode: "all",
2376
+ verdict: "review",
2377
+ reason: "SQL DDL destructive statement inside a shell command"
2378
+ },
2379
+ // ── Git safety ────────────────────────────────────────────────────────
2380
+ {
2381
+ name: "block-force-push",
2382
+ tool: "bash",
2383
+ conditions: [
2384
+ {
2385
+ field: "command",
2386
+ op: "matches",
2387
+ value: "git push.*(--force|--force-with-lease|-f\\b)",
2388
+ flags: "i"
2389
+ }
2390
+ ],
2391
+ conditionMode: "all",
2392
+ verdict: "block",
2393
+ reason: "Force push overwrites remote history and cannot be undone"
2394
+ },
2395
+ {
2396
+ name: "review-git-push",
2397
+ tool: "bash",
2398
+ conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
2399
+ conditionMode: "all",
2400
+ verdict: "review",
2401
+ reason: "git push sends changes to a shared remote"
2402
+ },
2403
+ {
2404
+ name: "review-git-destructive",
2405
+ tool: "bash",
2406
+ conditions: [
2407
+ {
2408
+ field: "command",
2409
+ op: "matches",
2410
+ value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
2411
+ flags: "i"
2412
+ }
2413
+ ],
2414
+ conditionMode: "all",
2415
+ verdict: "review",
2416
+ reason: "Destructive git operation \u2014 discards history or working-tree changes"
2417
+ },
2418
+ // ── Shell safety ──────────────────────────────────────────────────────
2419
+ {
2420
+ name: "review-sudo",
2421
+ tool: "bash",
2422
+ conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
2423
+ conditionMode: "all",
2424
+ verdict: "review",
2425
+ reason: "Command requires elevated privileges"
2426
+ },
2427
+ {
2428
+ name: "review-curl-pipe-shell",
2429
+ tool: "bash",
2430
+ conditions: [
2431
+ {
2432
+ field: "command",
2433
+ op: "matches",
2434
+ value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
2435
+ flags: "i"
2436
+ }
2437
+ ],
2438
+ conditionMode: "all",
2439
+ verdict: "block",
2440
+ reason: "Piping remote script into a shell is a supply-chain attack vector"
2441
+ }
2442
+ ],
2443
+ dlp: { enabled: true, scanIgnoredTools: true }
2444
+ },
2445
+ environments: {}
2446
+ };
2447
+ ADVISORY_SMART_RULES = [
2448
+ {
2449
+ name: "allow-rm-safe-paths",
2450
+ tool: "*",
2451
+ conditionMode: "all",
2452
+ conditions: [
2453
+ { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
2454
+ {
2455
+ field: "command",
2456
+ op: "matches",
2457
+ // Matches known-safe build artifact paths in the command.
2458
+ value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
2459
+ }
2460
+ ],
2461
+ verdict: "allow",
2462
+ reason: "Deleting a known-safe build artifact path"
2463
+ },
2464
+ {
2465
+ name: "review-rm",
2466
+ tool: "*",
2467
+ conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
2468
+ verdict: "review",
2469
+ reason: "rm can permanently delete files \u2014 confirm the target path"
2179
2470
  }
2180
- claudeConfig.mcpServers = servers;
2181
- writeJson(mcpPath, claudeConfig);
2182
- console.log(import_chalk3.default.green(`
2183
- \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
2184
- anythingChanged = true;
2185
- } else {
2186
- console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
2187
- }
2188
- console.log("");
2189
- }
2190
- if (!anythingChanged && serversToWrap.length === 0) {
2191
- console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
2192
- printDaemonTip();
2193
- return;
2194
- }
2195
- if (anythingChanged) {
2196
- console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
2197
- console.log(import_chalk3.default.gray(" Restart Claude Code for changes to take effect."));
2198
- printDaemonTip();
2199
- }
2200
- }
2201
- async function setupGemini() {
2202
- const homeDir2 = import_os2.default.homedir();
2203
- const settingsPath = import_path4.default.join(homeDir2, ".gemini", "settings.json");
2204
- const settings = readJson(settingsPath) ?? {};
2205
- const servers = settings.mcpServers ?? {};
2206
- let anythingChanged = false;
2207
- if (!settings.hooks) settings.hooks = {};
2208
- const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
2209
- (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
2210
- );
2211
- if (!hasBeforeHook) {
2212
- if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
2213
- if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
2214
- settings.hooks.BeforeTool.push({
2215
- matcher: ".*",
2216
- hooks: [
2217
- {
2218
- name: "node9-check",
2219
- type: "command",
2220
- command: fullPathCommand("check"),
2221
- timeout: 6e5
2222
- }
2223
- ]
2224
- });
2225
- console.log(import_chalk3.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
2226
- anythingChanged = true;
2227
- }
2228
- const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
2229
- (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
2230
- );
2231
- if (!hasAfterHook) {
2232
- if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
2233
- if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
2234
- settings.hooks.AfterTool.push({
2235
- matcher: ".*",
2236
- hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
2237
- });
2238
- console.log(import_chalk3.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
2239
- anythingChanged = true;
2240
- }
2241
- if (anythingChanged) {
2242
- writeJson(settingsPath, settings);
2243
- console.log("");
2244
- }
2245
- const serversToWrap = [];
2246
- for (const [name, server] of Object.entries(servers)) {
2247
- if (!server.command || server.command === "node9") continue;
2248
- const parts = [server.command, ...server.args ?? []];
2249
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
2471
+ ];
2472
+ cachedConfig = null;
2473
+ DAEMON_PORT = 7391;
2474
+ DAEMON_HOST = "127.0.0.1";
2475
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path4.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
2250
2476
  }
2251
- if (serversToWrap.length > 0) {
2252
- console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
2253
- console.log(import_chalk3.default.white(` ${settingsPath} (mcpServers)`));
2254
- for (const { name, originalCmd } of serversToWrap) {
2255
- console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
2256
- }
2257
- console.log("");
2258
- const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
2259
- if (proceed) {
2260
- for (const { name, parts } of serversToWrap) {
2261
- servers[name] = { ...servers[name], command: "node9", args: parts };
2262
- }
2263
- settings.mcpServers = servers;
2264
- writeJson(settingsPath, settings);
2265
- console.log(import_chalk3.default.green(`
2266
- \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
2267
- anythingChanged = true;
2268
- } else {
2269
- console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
2270
- }
2271
- console.log("");
2272
- }
2273
- if (!anythingChanged && serversToWrap.length === 0) {
2274
- console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
2275
- printDaemonTip();
2276
- return;
2277
- }
2278
- if (anythingChanged) {
2279
- console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
2280
- console.log(import_chalk3.default.gray(" Restart Gemini CLI for changes to take effect."));
2281
- printDaemonTip();
2282
- }
2283
- }
2284
- async function setupCursor() {
2285
- const homeDir2 = import_os2.default.homedir();
2286
- const mcpPath = import_path4.default.join(homeDir2, ".cursor", "mcp.json");
2287
- const hooksPath = import_path4.default.join(homeDir2, ".cursor", "hooks.json");
2288
- const mcpConfig = readJson(mcpPath) ?? {};
2289
- const hooksFile = readJson(hooksPath) ?? { version: 1 };
2290
- const servers = mcpConfig.mcpServers ?? {};
2291
- let anythingChanged = false;
2292
- if (!hooksFile.hooks) hooksFile.hooks = {};
2293
- const hasPreHook = hooksFile.hooks.preToolUse?.some(
2294
- (h) => h.command === "node9" && h.args?.includes("check") || h.command?.includes("cli.js")
2295
- );
2296
- if (!hasPreHook) {
2297
- if (!hooksFile.hooks.preToolUse) hooksFile.hooks.preToolUse = [];
2298
- hooksFile.hooks.preToolUse.push({ command: fullPathCommand("check") });
2299
- console.log(import_chalk3.default.green(" \u2705 preToolUse hook added \u2192 node9 check"));
2300
- anythingChanged = true;
2301
- }
2302
- const hasPostHook = hooksFile.hooks.postToolUse?.some(
2303
- (h) => h.command === "node9" && h.args?.includes("log") || h.command?.includes("cli.js")
2304
- );
2305
- if (!hasPostHook) {
2306
- if (!hooksFile.hooks.postToolUse) hooksFile.hooks.postToolUse = [];
2307
- hooksFile.hooks.postToolUse.push({ command: fullPathCommand("log") });
2308
- console.log(import_chalk3.default.green(" \u2705 postToolUse hook added \u2192 node9 log"));
2309
- anythingChanged = true;
2310
- }
2311
- if (anythingChanged) {
2312
- writeJson(hooksPath, hooksFile);
2313
- console.log("");
2314
- }
2315
- const serversToWrap = [];
2316
- for (const [name, server] of Object.entries(servers)) {
2317
- if (!server.command || server.command === "node9") continue;
2318
- const parts = [server.command, ...server.args ?? []];
2319
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
2320
- }
2321
- if (serversToWrap.length > 0) {
2322
- console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
2323
- console.log(import_chalk3.default.white(` ${mcpPath}`));
2324
- for (const { name, originalCmd } of serversToWrap) {
2325
- console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
2326
- }
2327
- console.log("");
2328
- const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
2329
- if (proceed) {
2330
- for (const { name, parts } of serversToWrap) {
2331
- servers[name] = { ...servers[name], command: "node9", args: parts };
2332
- }
2333
- mcpConfig.mcpServers = servers;
2334
- writeJson(mcpPath, mcpConfig);
2335
- console.log(import_chalk3.default.green(`
2336
- \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
2337
- anythingChanged = true;
2338
- } else {
2339
- console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
2340
- }
2341
- console.log("");
2342
- }
2343
- if (!anythingChanged && serversToWrap.length === 0) {
2344
- console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Cursor."));
2345
- printDaemonTip();
2346
- return;
2347
- }
2348
- if (anythingChanged) {
2349
- console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor!"));
2350
- console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
2351
- printDaemonTip();
2352
- }
2353
- }
2354
-
2355
- // src/daemon/ui.html
2356
- var ui_default = `<!doctype html>
2357
- <html lang="en">
2358
- <head>
2359
- <meta charset="UTF-8" />
2360
- <meta name="viewport" content="width=device-width, initial-scale=1" />
2361
- <title>Node9 Security Guard</title>
2362
- <style>
2363
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Fira+Code:wght@400;500&display=swap');
2364
- :root {
2365
- --bg: #0a0c10;
2366
- --card: #1c2128;
2367
- --panel: #161b22;
2368
- --border: #30363d;
2369
- --text: #adbac7;
2370
- --text-bright: #cdd9e5;
2371
- --muted: #768390;
2372
- --primary: #f0883e;
2373
- --success: #347d39;
2374
- --danger: #c93c37;
2375
- --accent: #539bf5;
2477
+ });
2478
+
2479
+ // src/daemon/ui.html
2480
+ var ui_default;
2481
+ var init_ui = __esm({
2482
+ "src/daemon/ui.html"() {
2483
+ ui_default = `<!doctype html>
2484
+ <html lang="en">
2485
+ <head>
2486
+ <meta charset="UTF-8" />
2487
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2488
+ <title>Node9 Security Guard</title>
2489
+ <style>
2490
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Fira+Code:wght@400;500&display=swap');
2491
+ :root {
2492
+ --bg: #0a0c10;
2493
+ --card: #1c2128;
2494
+ --panel: #161b22;
2495
+ --border: #30363d;
2496
+ --text: #adbac7;
2497
+ --text-bright: #cdd9e5;
2498
+ --muted: #768390;
2499
+ --primary: #f0883e;
2500
+ --success: #347d39;
2501
+ --danger: #c93c37;
2502
+ --accent: #539bf5;
2376
2503
  }
2377
2504
  * {
2378
2505
  box-sizing: border-box;
2379
2506
  margin: 0;
2380
2507
  padding: 0;
2381
2508
  }
2509
+ html,
2510
+ body {
2511
+ height: 100%;
2512
+ overflow: hidden;
2513
+ }
2382
2514
  body {
2383
2515
  background: var(--bg);
2384
2516
  color: var(--text);
@@ -2386,16 +2518,17 @@ var ui_default = `<!doctype html>
2386
2518
  'Inter',
2387
2519
  -apple-system,
2388
2520
  sans-serif;
2389
- min-height: 100vh;
2390
2521
  }
2391
2522
 
2392
2523
  .shell {
2393
- max-width: 1000px;
2524
+ max-width: 1440px;
2525
+ height: 100vh;
2394
2526
  margin: 0 auto;
2395
- padding: 32px 24px 48px;
2527
+ padding: 16px 20px 16px;
2396
2528
  display: grid;
2397
2529
  grid-template-rows: auto 1fr;
2398
- gap: 24px;
2530
+ gap: 16px;
2531
+ overflow: hidden;
2399
2532
  }
2400
2533
  header {
2401
2534
  display: flex;
@@ -2432,9 +2565,10 @@ var ui_default = `<!doctype html>
2432
2565
 
2433
2566
  .body {
2434
2567
  display: grid;
2435
- grid-template-columns: 1fr 272px;
2436
- gap: 20px;
2437
- align-items: start;
2568
+ grid-template-columns: 360px 1fr 270px;
2569
+ gap: 16px;
2570
+ min-height: 0;
2571
+ overflow: hidden;
2438
2572
  }
2439
2573
 
2440
2574
  .warning-banner {
@@ -2454,6 +2588,10 @@ var ui_default = `<!doctype html>
2454
2588
 
2455
2589
  .main {
2456
2590
  min-width: 0;
2591
+ min-height: 0;
2592
+ overflow-y: auto;
2593
+ scrollbar-width: thin;
2594
+ scrollbar-color: var(--border) transparent;
2457
2595
  }
2458
2596
  .section-title {
2459
2597
  font-size: 11px;
@@ -2484,14 +2622,64 @@ var ui_default = `<!doctype html>
2484
2622
  background: var(--card);
2485
2623
  border: 1px solid var(--border);
2486
2624
  border-radius: 14px;
2487
- padding: 24px;
2488
- margin-bottom: 16px;
2625
+ padding: 20px;
2626
+ margin-bottom: 14px;
2489
2627
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
2490
2628
  animation: pop 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
2491
2629
  }
2492
2630
  .card.slack-viewer {
2493
2631
  border-color: rgba(83, 155, 245, 0.3);
2494
2632
  }
2633
+ .card-header {
2634
+ display: flex;
2635
+ align-items: center;
2636
+ gap: 8px;
2637
+ margin-bottom: 12px;
2638
+ padding-bottom: 12px;
2639
+ border-bottom: 1px solid var(--border);
2640
+ }
2641
+ .card-header-icon {
2642
+ font-size: 16px;
2643
+ }
2644
+ .card-header-title {
2645
+ font-size: 12px;
2646
+ font-weight: 700;
2647
+ color: var(--text-bright);
2648
+ text-transform: uppercase;
2649
+ letter-spacing: 0.5px;
2650
+ }
2651
+ .card-timer {
2652
+ margin-left: auto;
2653
+ font-size: 11px;
2654
+ font-family: 'Fira Code', monospace;
2655
+ color: var(--muted);
2656
+ background: rgba(48, 54, 61, 0.6);
2657
+ padding: 2px 8px;
2658
+ border-radius: 5px;
2659
+ }
2660
+ .card-timer.urgent {
2661
+ color: var(--danger);
2662
+ background: rgba(201, 60, 55, 0.1);
2663
+ }
2664
+ .btn-allow {
2665
+ background: var(--success);
2666
+ color: #fff;
2667
+ grid-column: span 2;
2668
+ font-size: 14px;
2669
+ padding: 13px 14px;
2670
+ }
2671
+ .btn-deny {
2672
+ background: rgba(201, 60, 55, 0.15);
2673
+ color: #e5534b;
2674
+ border: 1px solid rgba(201, 60, 55, 0.3);
2675
+ grid-column: span 2;
2676
+ }
2677
+ .btn-deny:hover:not(:disabled) {
2678
+ background: var(--danger);
2679
+ color: #fff;
2680
+ border-color: transparent;
2681
+ filter: none;
2682
+ }
2495
2683
  @keyframes pop {
2496
2684
  from {
2497
2685
  opacity: 0;
@@ -2699,24 +2887,178 @@ var ui_default = `<!doctype html>
2699
2887
  cursor: not-allowed;
2700
2888
  }
2701
2889
 
2890
+ .flight-col {
2891
+ display: flex;
2892
+ flex-direction: column;
2893
+ min-height: 0;
2894
+ overflow: hidden;
2895
+ }
2896
+ .flight-panel {
2897
+ flex: 1;
2898
+ min-height: 0;
2899
+ display: flex;
2900
+ flex-direction: column;
2901
+ overflow: hidden;
2902
+ }
2702
2903
  .sidebar {
2703
2904
  display: flex;
2704
2905
  flex-direction: column;
2705
2906
  gap: 12px;
2706
- position: sticky;
2707
- top: 24px;
2907
+ min-height: 0;
2908
+ overflow-y: auto;
2909
+ scrollbar-width: thin;
2910
+ scrollbar-color: var(--border) transparent;
2708
2911
  }
2709
2912
  .panel {
2710
2913
  background: var(--panel);
2711
2914
  border: 1px solid var(--border);
2712
2915
  border-radius: 12px;
2713
- padding: 16px;
2916
+ padding: 14px;
2917
+ }
2918
+ /* \u2500\u2500 Flight Recorder \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\u2500\u2500\u2500\u2500\u2500\u2500 */
2919
+ #activity-feed {
2920
+ display: flex;
2921
+ flex-direction: column;
2922
+ gap: 4px;
2923
+ margin-top: 4px;
2924
+ flex: 1;
2925
+ min-height: 0;
2926
+ overflow-y: auto;
2927
+ scrollbar-width: thin;
2928
+ scrollbar-color: var(--border) transparent;
2929
+ }
2930
+ .feed-row {
2931
+ display: grid;
2932
+ grid-template-columns: 58px 20px 1fr 48px;
2933
+ align-items: start;
2934
+ gap: 6px;
2935
+ background: rgba(22, 27, 34, 0.6);
2936
+ border: 1px solid var(--border);
2937
+ padding: 7px 10px;
2938
+ border-radius: 7px;
2939
+ font-size: 11px;
2940
+ animation: frSlideIn 0.15s ease-out;
2941
+ transition: background 0.1s;
2942
+ cursor: default;
2943
+ }
2944
+ .feed-row:hover {
2945
+ background: rgba(30, 38, 48, 0.9);
2946
+ border-color: rgba(83, 155, 245, 0.2);
2947
+ }
2948
+ @keyframes frSlideIn {
2949
+ from {
2950
+ opacity: 0;
2951
+ transform: translateX(-4px);
2952
+ }
2953
+ to {
2954
+ opacity: 1;
2955
+ transform: none;
2956
+ }
2957
+ }
2958
+ .feed-ts {
2959
+ color: var(--muted);
2960
+ font-family: monospace;
2961
+ font-size: 9px;
2962
+ }
2963
+ .feed-icon {
2964
+ text-align: center;
2965
+ font-size: 13px;
2966
+ }
2967
+ .feed-content {
2968
+ min-width: 0;
2969
+ color: var(--text-bright);
2970
+ word-break: break-all;
2971
+ }
2972
+ .feed-args {
2973
+ display: block;
2974
+ color: var(--muted);
2975
+ font-family: monospace;
2976
+ margin-top: 2px;
2977
+ font-size: 10px;
2978
+ word-break: break-all;
2979
+ }
2980
+ .feed-badge {
2981
+ text-align: right;
2982
+ font-weight: 700;
2983
+ font-size: 9px;
2984
+ letter-spacing: 0.03em;
2985
+ }
2986
+ .fr-pending {
2987
+ color: var(--muted);
2988
+ }
2989
+ .fr-allow {
2990
+ color: #57ab5a;
2991
+ }
2992
+ .fr-block {
2993
+ color: var(--danger);
2994
+ }
2995
+ .fr-dlp {
2996
+ color: var(--primary);
2997
+ animation: frBlink 1s infinite;
2998
+ }
2999
+ @keyframes frBlink {
3000
+ 50% {
3001
+ opacity: 0.4;
3002
+ }
3003
+ }
3004
+ .fr-dlp-row {
3005
+ border-color: var(--primary) !important;
3006
+ }
3007
+ .feed-clear-btn {
3008
+ background: transparent;
3009
+ border: none;
3010
+ color: var(--muted);
3011
+ font-size: 10px;
3012
+ padding: 0;
3013
+ cursor: pointer;
3014
+ margin-left: auto;
3015
+ font-family: inherit;
3016
+ font-weight: 500;
3017
+ transition: color 0.15s;
3018
+ }
3019
+ .feed-clear-btn:hover {
3020
+ color: var(--text);
3021
+ filter: none;
3022
+ transform: none;
3023
+ }
3024
+ /* \u2500\u2500 Shields \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
3025
+ .shield-row {
3026
+ display: flex;
3027
+ align-items: flex-start;
3028
+ gap: 10px;
3029
+ padding: 8px 0;
3030
+ border-bottom: 1px solid var(--border);
3031
+ }
3032
+ .shield-row:last-child {
3033
+ border-bottom: none;
3034
+ padding-bottom: 0;
3035
+ }
3036
+ .shield-row:first-child {
3037
+ padding-top: 0;
3038
+ }
3039
+ .shield-info {
3040
+ flex: 1;
3041
+ min-width: 0;
3042
+ }
3043
+ .shield-name {
3044
+ font-size: 12px;
3045
+ color: var(--text-bright);
3046
+ font-weight: 600;
3047
+ font-family: 'Fira Code', monospace;
3048
+ }
3049
+ .shield-desc {
3050
+ font-size: 10px;
3051
+ color: var(--muted);
3052
+ margin-top: 2px;
3053
+ line-height: 1.4;
2714
3054
  }
3055
+
2715
3056
  .panel-title {
2716
3057
  font-size: 12px;
2717
3058
  font-weight: 700;
2718
3059
  color: var(--text-bright);
2719
3060
  margin-bottom: 12px;
3061
+ flex-shrink: 0;
2720
3062
  display: flex;
2721
3063
  align-items: center;
2722
3064
  gap: 6px;
@@ -2724,8 +3066,8 @@ var ui_default = `<!doctype html>
2724
3066
  .setting-row {
2725
3067
  display: flex;
2726
3068
  align-items: flex-start;
2727
- gap: 12px;
2728
- margin-bottom: 12px;
3069
+ gap: 10px;
3070
+ margin-bottom: 8px;
2729
3071
  }
2730
3072
  .setting-row:last-child {
2731
3073
  margin-bottom: 0;
@@ -2734,20 +3076,21 @@ var ui_default = `<!doctype html>
2734
3076
  flex: 1;
2735
3077
  }
2736
3078
  .setting-label {
2737
- font-size: 12px;
3079
+ font-size: 11px;
2738
3080
  color: var(--text-bright);
2739
- margin-bottom: 3px;
3081
+ margin-bottom: 2px;
3082
+ font-weight: 600;
2740
3083
  }
2741
3084
  .setting-desc {
2742
- font-size: 11px;
3085
+ font-size: 10px;
2743
3086
  color: var(--muted);
2744
- line-height: 1.5;
3087
+ line-height: 1.4;
2745
3088
  }
2746
3089
  .toggle {
2747
3090
  position: relative;
2748
3091
  display: inline-block;
2749
- width: 40px;
2750
- height: 22px;
3092
+ width: 34px;
3093
+ height: 19px;
2751
3094
  flex-shrink: 0;
2752
3095
  margin-top: 1px;
2753
3096
  }
@@ -2767,8 +3110,8 @@ var ui_default = `<!doctype html>
2767
3110
  .slider:before {
2768
3111
  content: '';
2769
3112
  position: absolute;
2770
- width: 16px;
2771
- height: 16px;
3113
+ width: 13px;
3114
+ height: 13px;
2772
3115
  left: 3px;
2773
3116
  bottom: 3px;
2774
3117
  background: #fff;
@@ -2779,7 +3122,7 @@ var ui_default = `<!doctype html>
2779
3122
  background: var(--success);
2780
3123
  }
2781
3124
  input:checked + .slider:before {
2782
- transform: translateX(18px);
3125
+ transform: translateX(15px);
2783
3126
  }
2784
3127
  input:disabled + .slider {
2785
3128
  opacity: 0.4;
@@ -2938,12 +3281,17 @@ var ui_default = `<!doctype html>
2938
3281
  border: 1px solid var(--border);
2939
3282
  }
2940
3283
 
2941
- @media (max-width: 680px) {
3284
+ @media (max-width: 960px) {
2942
3285
  .body {
2943
- grid-template-columns: 1fr;
3286
+ grid-template-columns: 1fr 220px;
2944
3287
  }
2945
- .sidebar {
2946
- position: static;
3288
+ .flight-col {
3289
+ display: none;
3290
+ }
3291
+ }
3292
+ @media (max-width: 640px) {
3293
+ .body {
3294
+ grid-template-columns: 1fr;
2947
3295
  }
2948
3296
  }
2949
3297
  </style>
@@ -2957,6 +3305,19 @@ var ui_default = `<!doctype html>
2957
3305
  </header>
2958
3306
 
2959
3307
  <div class="body">
3308
+ <div class="flight-col">
3309
+ <div class="panel flight-panel">
3310
+ <div class="panel-title">
3311
+ \u{1F6F0}\uFE0F Flight Recorder
3312
+ <span style="font-weight: 400; color: var(--muted); font-size: 11px">live</span>
3313
+ <button class="feed-clear-btn" onclick="clearFeed()">clear</button>
3314
+ </div>
3315
+ <div id="activity-feed">
3316
+ <span class="decisions-empty">Waiting for agent activity\u2026</span>
3317
+ </div>
3318
+ </div>
3319
+ </div>
3320
+
2960
3321
  <div class="main">
2961
3322
  <div id="warnBanner" class="warning-banner">
2962
3323
  \u26A0\uFE0F Auto-start is off \u2014 daemon started manually. Run "node9 daemon stop" to stop it, or
@@ -3037,6 +3398,11 @@ var ui_default = `<!doctype html>
3037
3398
  <div id="slackStatusLine" class="slack-status-line">No key saved</div>
3038
3399
  </div>
3039
3400
 
3401
+ <div class="panel">
3402
+ <div class="panel-title">\u{1F6E1}\uFE0F Active Shields</div>
3403
+ <div id="shieldsList"><span class="decisions-empty">Loading\u2026</span></div>
3404
+ </div>
3405
+
3040
3406
  <div class="panel">
3041
3407
  <div class="panel-title">\u{1F4CB} Persistent Decisions</div>
3042
3408
  <div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
@@ -3082,14 +3448,23 @@ var ui_default = `<!doctype html>
3082
3448
 
3083
3449
  function updateDenyButton(id, timestamp) {
3084
3450
  const btn = document.querySelector('#c-' + id + ' .btn-deny');
3451
+ const timer = document.querySelector('#timer-' + id);
3085
3452
  if (!btn) return;
3086
3453
  const elapsed = Date.now() - timestamp;
3087
3454
  const remaining = Math.max(0, Math.ceil((autoDenyMs - elapsed) / 1000));
3088
3455
  if (remaining <= 0) {
3089
- btn.textContent = 'Auto-Denying...';
3456
+ btn.textContent = '\u23F3 Auto-Denying\u2026';
3090
3457
  btn.disabled = true;
3458
+ if (timer) {
3459
+ timer.textContent = 'auto-deny';
3460
+ timer.className = 'card-timer urgent';
3461
+ }
3091
3462
  } else {
3092
- btn.textContent = 'Block Action (' + remaining + 's)';
3463
+ btn.textContent = '\u{1F6AB} Block this Action';
3464
+ if (timer) {
3465
+ timer.textContent = remaining + 's';
3466
+ timer.className = 'card-timer' + (remaining < 15 ? ' urgent' : '');
3467
+ }
3093
3468
  setTimeout(() => updateDenyButton(id, timestamp), 1000);
3094
3469
  }
3095
3470
  }
@@ -3105,34 +3480,61 @@ var ui_default = `<!doctype html>
3105
3480
  empty.style.display = requests.size === 0 ? 'block' : 'none';
3106
3481
  }
3107
3482
 
3108
- function sendDecision(id, decision, persist) {
3483
+ function setCardBusy(card, busy) {
3484
+ if (!card) return;
3485
+ card.querySelectorAll('button').forEach((b) => (b.disabled = busy));
3486
+ card.style.opacity = busy ? '0.5' : '1';
3487
+ }
3488
+
3489
+ function showCardError(card, msg) {
3490
+ if (!card) return;
3491
+ card.style.outline = '2px solid #f87171';
3492
+ let err = card.querySelector('.card-error');
3493
+ if (!err) {
3494
+ err = document.createElement('p');
3495
+ err.className = 'card-error';
3496
+ err.style.cssText = 'color:#f87171;font-size:11px;margin:6px 0 0;';
3497
+ card.appendChild(err);
3498
+ }
3499
+ err.textContent = '\u26A0 ' + msg + ' \u2014 please try again or refresh.';
3500
+ }
3501
+
3502
+ async function sendDecision(id, decision, persist) {
3109
3503
  const card = document.getElementById('c-' + id);
3110
- if (card) card.style.opacity = '0.5';
3111
- fetch('/decision/' + id, {
3112
- method: 'POST',
3113
- headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3114
- body: JSON.stringify({ decision, persist: !!persist }),
3115
- });
3116
- setTimeout(() => {
3504
+ setCardBusy(card, true);
3505
+ try {
3506
+ const res = await fetch('/decision/' + id, {
3507
+ method: 'POST',
3508
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3509
+ body: JSON.stringify({ decision, persist: !!persist }),
3510
+ });
3511
+ if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3117
3512
  card?.remove();
3118
3513
  requests.delete(id);
3119
3514
  refresh();
3120
- }, 200);
3515
+ } catch (err) {
3516
+ setCardBusy(card, false);
3517
+ showCardError(card, err.message || 'Network error');
3518
+ }
3121
3519
  }
3122
3520
 
3123
- function sendTrust(id, duration) {
3521
+ async function sendTrust(id, duration) {
3124
3522
  const card = document.getElementById('c-' + id);
3125
- if (card) card.style.opacity = '0.5';
3126
- fetch('/decision/' + id, {
3127
- method: 'POST',
3128
- headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3129
- body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
3130
- });
3131
- setTimeout(() => {
3523
+ setCardBusy(card, true);
3524
+ try {
3525
+ const res = await fetch('/decision/' + id, {
3526
+ method: 'POST',
3527
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3528
+ body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
3529
+ });
3530
+ if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3132
3531
  card?.remove();
3133
3532
  requests.delete(id);
3134
3533
  refresh();
3135
- }, 200);
3534
+ } catch (err) {
3535
+ setCardBusy(card, false);
3536
+ showCardError(card, err.message || 'Network error');
3537
+ }
3136
3538
  }
3137
3539
 
3138
3540
  function renderPayload(req) {
@@ -3183,16 +3585,21 @@ var ui_default = `<!doctype html>
3183
3585
  const mcpLabel = req.mcpServer ? esc(req.mcpServer) : null;
3184
3586
  const dis = isSlack ? 'disabled' : '';
3185
3587
  card.innerHTML = \`
3588
+ <div class="card-header">
3589
+ <span class="card-header-icon">\${isSlack ? '\u26A1' : '\u26A0\uFE0F'}</span>
3590
+ <span class="card-header-title">\${isSlack ? 'Awaiting Cloud Approval' : 'Action Required'}</span>
3591
+ <span class="card-timer" id="timer-\${req.id}">\${autoDenyMs > 0 ? Math.ceil(autoDenyMs / 1000) + 's' : ''}</span>
3592
+ </div>
3186
3593
  <div class="source-row">
3187
3594
  <span class="agent-badge">\${agentLabel}</span>
3188
3595
  \${mcpLabel ? \`<span class="source-arrow">\u2192</span><span class="mcp-badge">mcp::\${mcpLabel}</span>\` : ''}
3189
3596
  </div>
3190
3597
  <div class="tool-chip">\${esc(req.toolName)}</div>
3191
- \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Slack approval \u2014 view only</div>' : ''}
3598
+ \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
3192
3599
  \${renderPayload(req)}
3193
3600
  <div class="actions" id="act-\${req.id}">
3194
- <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>Approve Execution</button>
3195
- <button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>Block Action</button>
3601
+ <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
3602
+ <button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>\u{1F6AB} Block this Action</button>
3196
3603
  <div class="trust-row\${trustEnabled ? ' show' : ''}" id="tr-\${req.id}">
3197
3604
  <button class="btn-trust" onclick="sendTrust('\${req.id}','30m')" \${dis}>\u23F1 Trust 30m</button>
3198
3605
  <button class="btn-trust" onclick="sendTrust('\${req.id}','1h')" \${dis}>\u23F1 Trust 1h</button>
@@ -3252,9 +3659,84 @@ var ui_default = `<!doctype html>
3252
3659
  ev.addEventListener('slack-status', (e) => {
3253
3660
  applySlackStatus(JSON.parse(e.data));
3254
3661
  });
3662
+ ev.addEventListener('shields-status', (e) => {
3663
+ renderShields(JSON.parse(e.data).shields);
3664
+ });
3665
+
3666
+ // \u2500\u2500 Flight Recorder \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\u2500\u2500\u2500\u2500\u2500
3667
+ ev.addEventListener('activity', (e) => {
3668
+ const data = JSON.parse(e.data);
3669
+ const feed = document.getElementById('activity-feed');
3670
+ // Remove placeholder on first item
3671
+ const placeholder = feed.querySelector('.decisions-empty');
3672
+ if (placeholder) placeholder.remove();
3673
+
3674
+ const time = new Date(data.ts).toLocaleTimeString([], {
3675
+ hour12: false,
3676
+ hour: '2-digit',
3677
+ minute: '2-digit',
3678
+ second: '2-digit',
3679
+ });
3680
+ const icon = frIcon(data.tool);
3681
+ const argsStr = JSON.stringify(data.args ?? {});
3682
+ const argsPreview = esc(argsStr.length > 120 ? argsStr.slice(0, 120) + '\u2026' : argsStr);
3683
+
3684
+ const row = document.createElement('div');
3685
+ row.className = 'feed-row';
3686
+ row.id = 'fr-' + data.id;
3687
+ row.innerHTML = \`
3688
+ <span class="feed-ts">\${time}</span>
3689
+ <span class="feed-icon">\${icon}</span>
3690
+ <span class="feed-content"><strong>\${esc(data.tool)}</strong><span class="feed-args">\${argsPreview}</span></span>
3691
+ <span class="feed-badge fr-pending">\u25CF</span>
3692
+ \`;
3693
+ feed.prepend(row);
3694
+ if (feed.children.length > 100) feed.lastChild.remove();
3695
+ });
3696
+
3697
+ ev.addEventListener('activity-result', (e) => {
3698
+ const { id, status, label } = JSON.parse(e.data);
3699
+ const row = document.getElementById('fr-' + id);
3700
+ if (!row) return;
3701
+ const badge = row.querySelector('.feed-badge');
3702
+ if (status === 'allow') {
3703
+ badge.textContent = 'ALLOW';
3704
+ badge.className = 'feed-badge fr-allow';
3705
+ } else if (status === 'dlp') {
3706
+ badge.textContent = '\u{1F6E1}\uFE0F DLP';
3707
+ badge.className = 'feed-badge fr-dlp';
3708
+ row.classList.add('fr-dlp-row');
3709
+ } else {
3710
+ badge.textContent = 'BLOCK';
3711
+ badge.className = 'feed-badge fr-block';
3712
+ }
3713
+ });
3255
3714
  }
3256
3715
  connect();
3257
3716
 
3717
+ const FR_ICONS = {
3718
+ bash: '\u{1F4BB}',
3719
+ read: '\u{1F4D6}',
3720
+ edit: '\u270F\uFE0F',
3721
+ write: '\u270F\uFE0F',
3722
+ glob: '\u{1F4C2}',
3723
+ grep: '\u{1F50D}',
3724
+ agent: '\u{1F916}',
3725
+ search: '\u{1F50D}',
3726
+ sql: '\u{1F5C4}\uFE0F',
3727
+ query: '\u{1F5C4}\uFE0F',
3728
+ list: '\u{1F4C2}',
3729
+ delete: '\u{1F5D1}\uFE0F',
3730
+ web: '\u{1F310}',
3731
+ };
3732
+ function frIcon(tool) {
3733
+ const t = (tool || '').toLowerCase();
3734
+ for (const [k, v] of Object.entries(FR_ICONS)) {
3735
+ if (t.includes(k)) return v;
3736
+ }
3737
+ return '\u{1F6E0}\uFE0F';
3738
+ }
3739
+
3258
3740
  function saveSetting(key, value) {
3259
3741
  fetch('/settings', {
3260
3742
  method: 'POST',
@@ -3344,6 +3826,49 @@ var ui_default = `<!doctype html>
3344
3826
  }
3345
3827
  }
3346
3828
 
3829
+ function clearFeed() {
3830
+ const feed = document.getElementById('activity-feed');
3831
+ feed.innerHTML = '<span class="decisions-empty">Feed cleared.</span>';
3832
+ }
3833
+
3834
+ function renderShields(shields) {
3835
+ const list = document.getElementById('shieldsList');
3836
+ if (!shields || shields.length === 0) {
3837
+ list.innerHTML = '<span class="decisions-empty">No shields available.</span>';
3838
+ return;
3839
+ }
3840
+ list.innerHTML = shields
3841
+ .map(
3842
+ (s) => \`
3843
+ <div class="shield-row">
3844
+ <div class="shield-info">
3845
+ <div class="shield-name">\${esc(s.name)}</div>
3846
+ <div class="shield-desc">\${esc(s.description)}</div>
3847
+ </div>
3848
+ <label class="toggle">
3849
+ <input type="checkbox" \${s.active ? 'checked' : ''}
3850
+ onchange="toggleShield('\${esc(s.name)}', this.checked)" />
3851
+ <span class="slider"></span>
3852
+ </label>
3853
+ </div>
3854
+ \`
3855
+ )
3856
+ .join('');
3857
+ }
3858
+
3859
+ function toggleShield(name, active) {
3860
+ fetch('/shields', {
3861
+ method: 'POST',
3862
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3863
+ body: JSON.stringify({ name, active }),
3864
+ }).catch(() => {});
3865
+ }
3866
+
3867
+ fetch('/shields')
3868
+ .then((r) => r.json())
3869
+ .then(({ shields }) => renderShields(shields))
3870
+ .catch(() => {});
3871
+
3347
3872
  function renderDecisions(decisions) {
3348
3873
  const dl = document.getElementById('decisionsList');
3349
3874
  const entries = Object.entries(decisions);
@@ -3390,40 +3915,33 @@ var ui_default = `<!doctype html>
3390
3915
  </body>
3391
3916
  </html>
3392
3917
  `;
3918
+ }
3919
+ });
3393
3920
 
3394
3921
  // src/daemon/ui.ts
3395
- var UI_HTML_TEMPLATE = ui_default;
3922
+ var UI_HTML_TEMPLATE;
3923
+ var init_ui2 = __esm({
3924
+ "src/daemon/ui.ts"() {
3925
+ "use strict";
3926
+ init_ui();
3927
+ UI_HTML_TEMPLATE = ui_default;
3928
+ }
3929
+ });
3396
3930
 
3397
3931
  // src/daemon/index.ts
3398
- var import_http = __toESM(require("http"));
3399
- var import_fs3 = __toESM(require("fs"));
3400
- var import_path5 = __toESM(require("path"));
3401
- var import_os3 = __toESM(require("os"));
3402
- var import_child_process2 = require("child_process");
3403
- var import_crypto = require("crypto");
3404
- var import_chalk4 = __toESM(require("chalk"));
3405
- var DAEMON_PORT2 = 7391;
3406
- var DAEMON_HOST2 = "127.0.0.1";
3407
- var homeDir = import_os3.default.homedir();
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");
3414
3932
  function atomicWriteSync2(filePath, data, options) {
3415
- const dir = import_path5.default.dirname(filePath);
3416
- if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
3417
- const tmpPath = `${filePath}.${(0, import_crypto.randomUUID)()}.tmp`;
3418
- import_fs3.default.writeFileSync(tmpPath, data, options);
3419
- import_fs3.default.renameSync(tmpPath, filePath);
3933
+ const dir = import_path6.default.dirname(filePath);
3934
+ if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
3935
+ const tmpPath = `${filePath}.${(0, import_crypto3.randomUUID)()}.tmp`;
3936
+ import_fs4.default.writeFileSync(tmpPath, data, options);
3937
+ import_fs4.default.renameSync(tmpPath, filePath);
3420
3938
  }
3421
3939
  function writeTrustEntry(toolName, durationMs) {
3422
3940
  try {
3423
3941
  let trust = { entries: [] };
3424
3942
  try {
3425
- if (import_fs3.default.existsSync(TRUST_FILE2))
3426
- trust = JSON.parse(import_fs3.default.readFileSync(TRUST_FILE2, "utf-8"));
3943
+ if (import_fs4.default.existsSync(TRUST_FILE2))
3944
+ trust = JSON.parse(import_fs4.default.readFileSync(TRUST_FILE2, "utf-8"));
3427
3945
  } catch {
3428
3946
  }
3429
3947
  trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
@@ -3432,12 +3950,6 @@ function writeTrustEntry(toolName, durationMs) {
3432
3950
  } catch {
3433
3951
  }
3434
3952
  }
3435
- var TRUST_DURATIONS = {
3436
- "30m": 30 * 6e4,
3437
- "1h": 60 * 6e4,
3438
- "2h": 2 * 60 * 6e4
3439
- };
3440
- var SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
3441
3953
  function redactArgs(value) {
3442
3954
  if (!value || typeof value !== "object") return value;
3443
3955
  if (Array.isArray(value)) return value.map(redactArgs);
@@ -3456,41 +3968,39 @@ function appendAuditLog(data) {
3456
3968
  decision: data.decision,
3457
3969
  source: "daemon"
3458
3970
  };
3459
- const dir = import_path5.default.dirname(AUDIT_LOG_FILE);
3460
- if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
3461
- import_fs3.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
3971
+ const dir = import_path6.default.dirname(AUDIT_LOG_FILE);
3972
+ if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
3973
+ import_fs4.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
3462
3974
  } catch {
3463
3975
  }
3464
3976
  }
3465
3977
  function getAuditHistory(limit = 20) {
3466
3978
  try {
3467
- if (!import_fs3.default.existsSync(AUDIT_LOG_FILE)) return [];
3468
- const lines = import_fs3.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
3979
+ if (!import_fs4.default.existsSync(AUDIT_LOG_FILE)) return [];
3980
+ const lines = import_fs4.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
3469
3981
  if (lines.length === 1 && lines[0] === "") return [];
3470
3982
  return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
3471
3983
  } catch {
3472
3984
  return [];
3473
3985
  }
3474
3986
  }
3475
- var AUTO_DENY_MS = 12e4;
3476
3987
  function getOrgName() {
3477
3988
  try {
3478
- if (import_fs3.default.existsSync(CREDENTIALS_FILE)) {
3989
+ if (import_fs4.default.existsSync(CREDENTIALS_FILE)) {
3479
3990
  return "Node9 Cloud";
3480
3991
  }
3481
3992
  } catch {
3482
3993
  }
3483
3994
  return null;
3484
3995
  }
3485
- var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
3486
3996
  function hasStoredSlackKey() {
3487
- return import_fs3.default.existsSync(CREDENTIALS_FILE);
3997
+ return import_fs4.default.existsSync(CREDENTIALS_FILE);
3488
3998
  }
3489
3999
  function writeGlobalSetting(key, value) {
3490
4000
  let config = {};
3491
4001
  try {
3492
- if (import_fs3.default.existsSync(GLOBAL_CONFIG_FILE)) {
3493
- config = JSON.parse(import_fs3.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
4002
+ if (import_fs4.default.existsSync(GLOBAL_CONFIG_FILE)) {
4003
+ config = JSON.parse(import_fs4.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
3494
4004
  }
3495
4005
  } catch {
3496
4006
  }
@@ -3498,11 +4008,6 @@ function writeGlobalSetting(key, value) {
3498
4008
  config.settings[key] = value;
3499
4009
  atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
3500
4010
  }
3501
- var pending = /* @__PURE__ */ new Map();
3502
- var sseClients = /* @__PURE__ */ new Set();
3503
- var abandonTimer = null;
3504
- var daemonServer = null;
3505
- var hadBrowserClient = false;
3506
4011
  function abandonPending() {
3507
4012
  abandonTimer = null;
3508
4013
  pending.forEach((entry, id) => {
@@ -3514,7 +4019,7 @@ function abandonPending() {
3514
4019
  });
3515
4020
  if (autoStarted) {
3516
4021
  try {
3517
- import_fs3.default.unlinkSync(DAEMON_PID_FILE);
4022
+ import_fs4.default.unlinkSync(DAEMON_PID_FILE);
3518
4023
  } catch {
3519
4024
  }
3520
4025
  setTimeout(() => {
@@ -3524,6 +4029,18 @@ function abandonPending() {
3524
4029
  }
3525
4030
  }
3526
4031
  function broadcast(event, data) {
4032
+ if (event === "activity") {
4033
+ activityRing.push({ event, data });
4034
+ if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
4035
+ } else if (event === "activity-result") {
4036
+ const { id, status, label } = data;
4037
+ for (let i = activityRing.length - 1; i >= 0; i--) {
4038
+ if (activityRing[i].data.id === id) {
4039
+ Object.assign(activityRing[i].data, { status, label });
4040
+ break;
4041
+ }
4042
+ }
4043
+ }
3527
4044
  const msg = `event: ${event}
3528
4045
  data: ${JSON.stringify(data)}
3529
4046
 
@@ -3552,8 +4069,8 @@ function readBody(req) {
3552
4069
  }
3553
4070
  function readPersistentDecisions() {
3554
4071
  try {
3555
- if (import_fs3.default.existsSync(DECISIONS_FILE)) {
3556
- return JSON.parse(import_fs3.default.readFileSync(DECISIONS_FILE, "utf-8"));
4072
+ if (import_fs4.default.existsSync(DECISIONS_FILE)) {
4073
+ return JSON.parse(import_fs4.default.readFileSync(DECISIONS_FILE, "utf-8"));
3557
4074
  }
3558
4075
  } catch {
3559
4076
  }
@@ -3569,18 +4086,20 @@ function writePersistentDecision(toolName, decision) {
3569
4086
  }
3570
4087
  }
3571
4088
  function startDaemon() {
3572
- const csrfToken = (0, import_crypto.randomUUID)();
3573
- const internalToken = (0, import_crypto.randomUUID)();
4089
+ const csrfToken = (0, import_crypto3.randomUUID)();
4090
+ const internalToken = (0, import_crypto3.randomUUID)();
3574
4091
  const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
3575
4092
  const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
3576
4093
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
4094
+ const watchMode = process.env.NODE9_WATCH_MODE === "1";
3577
4095
  let idleTimer;
3578
4096
  function resetIdleTimer() {
4097
+ if (watchMode) return;
3579
4098
  if (idleTimer) clearTimeout(idleTimer);
3580
4099
  idleTimer = setTimeout(() => {
3581
4100
  if (autoStarted) {
3582
4101
  try {
3583
- import_fs3.default.unlinkSync(DAEMON_PID_FILE);
4102
+ import_fs4.default.unlinkSync(DAEMON_PID_FILE);
3584
4103
  } catch {
3585
4104
  }
3586
4105
  }
@@ -3630,6 +4149,12 @@ data: ${JSON.stringify({
3630
4149
  data: ${JSON.stringify(readPersistentDecisions())}
3631
4150
 
3632
4151
  `);
4152
+ for (const item of activityRing) {
4153
+ res.write(`event: ${item.event}
4154
+ data: ${JSON.stringify(item.data)}
4155
+
4156
+ `);
4157
+ }
3633
4158
  return req.on("close", () => {
3634
4159
  sseClients.delete(res);
3635
4160
  if (sseClients.size === 0 && pending.size > 0) {
@@ -3649,9 +4174,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
3649
4174
  slackDelegated = false,
3650
4175
  agent,
3651
4176
  mcpServer,
3652
- riskMetadata
4177
+ riskMetadata,
4178
+ fromCLI = false,
4179
+ activityId
3653
4180
  } = JSON.parse(body);
3654
- const id = (0, import_crypto.randomUUID)();
4181
+ const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto3.randomUUID)();
3655
4182
  const entry = {
3656
4183
  id,
3657
4184
  toolName,
@@ -3682,6 +4209,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
3682
4209
  }, AUTO_DENY_MS)
3683
4210
  };
3684
4211
  pending.set(id, entry);
4212
+ if (!fromCLI) {
4213
+ broadcast("activity", {
4214
+ id,
4215
+ ts: entry.timestamp,
4216
+ tool: toolName,
4217
+ args: redactArgs(args),
4218
+ status: "pending"
4219
+ });
4220
+ }
3685
4221
  const browserEnabled = getConfig().settings.approvers?.browser !== false;
3686
4222
  if (browserEnabled) {
3687
4223
  broadcast("add", {
@@ -3711,6 +4247,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
3711
4247
  const e = pending.get(id);
3712
4248
  if (!e) return;
3713
4249
  if (result.noApprovalMechanism) return;
4250
+ broadcast("activity-result", {
4251
+ id,
4252
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
4253
+ label: result.blockedByLabel
4254
+ });
3714
4255
  clearTimeout(e.timer);
3715
4256
  const decision = result.approved ? "allow" : "deny";
3716
4257
  appendAuditLog({ toolName: e.toolName, args: e.args, decision });
@@ -3745,8 +4286,8 @@ data: ${JSON.stringify(readPersistentDecisions())}
3745
4286
  const entry = pending.get(id);
3746
4287
  if (!entry) return res.writeHead(404).end();
3747
4288
  if (entry.earlyDecision) {
4289
+ clearTimeout(entry.timer);
3748
4290
  pending.delete(id);
3749
- broadcast("remove", { id });
3750
4291
  res.writeHead(200, { "Content-Type": "application/json" });
3751
4292
  const body = { decision: entry.earlyDecision };
3752
4293
  if (entry.earlyReason) body.reason = entry.earlyReason;
@@ -3776,10 +4317,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
3776
4317
  decision: `trust:${trustDuration}`
3777
4318
  });
3778
4319
  clearTimeout(entry.timer);
3779
- if (entry.waiter) entry.waiter("allow");
3780
- else entry.earlyDecision = "allow";
3781
- pending.delete(id);
3782
- broadcast("remove", { id });
4320
+ if (entry.waiter) {
4321
+ entry.waiter("allow");
4322
+ pending.delete(id);
4323
+ broadcast("remove", { id });
4324
+ } else {
4325
+ entry.earlyDecision = "allow";
4326
+ broadcast("remove", { id });
4327
+ entry.timer = setTimeout(() => pending.delete(id), 3e4);
4328
+ }
3783
4329
  res.writeHead(200);
3784
4330
  return res.end(JSON.stringify({ ok: true }));
3785
4331
  }
@@ -3791,13 +4337,16 @@ data: ${JSON.stringify(readPersistentDecisions())}
3791
4337
  decision: resolvedDecision
3792
4338
  });
3793
4339
  clearTimeout(entry.timer);
3794
- if (entry.waiter) entry.waiter(resolvedDecision, reason);
3795
- else {
4340
+ if (entry.waiter) {
4341
+ entry.waiter(resolvedDecision, reason);
4342
+ pending.delete(id);
4343
+ broadcast("remove", { id });
4344
+ } else {
3796
4345
  entry.earlyDecision = resolvedDecision;
3797
4346
  entry.earlyReason = reason;
4347
+ broadcast("remove", { id });
4348
+ entry.timer = setTimeout(() => pending.delete(id), 3e4);
3798
4349
  }
3799
- pending.delete(id);
3800
- broadcast("remove", { id });
3801
4350
  res.writeHead(200);
3802
4351
  return res.end(JSON.stringify({ ok: true }));
3803
4352
  } catch {
@@ -3888,99 +4437,666 @@ data: ${JSON.stringify(readPersistentDecisions())}
3888
4437
  res.writeHead(400).end();
3889
4438
  }
3890
4439
  }
3891
- if (req.method === "GET" && pathname === "/audit") {
3892
- res.writeHead(200, { "Content-Type": "application/json" });
3893
- return res.end(JSON.stringify(getAuditHistory()));
4440
+ if (req.method === "GET" && pathname === "/audit") {
4441
+ res.writeHead(200, { "Content-Type": "application/json" });
4442
+ return res.end(JSON.stringify(getAuditHistory()));
4443
+ }
4444
+ if (req.method === "GET" && pathname === "/shields") {
4445
+ if (!validToken(req)) return res.writeHead(403).end();
4446
+ const active = readActiveShields();
4447
+ const shields = Object.values(SHIELDS).map((s) => ({
4448
+ name: s.name,
4449
+ description: s.description,
4450
+ active: active.includes(s.name)
4451
+ }));
4452
+ res.writeHead(200, { "Content-Type": "application/json" });
4453
+ return res.end(JSON.stringify({ shields }));
4454
+ }
4455
+ if (req.method === "POST" && pathname === "/shields") {
4456
+ if (!validToken(req)) return res.writeHead(403).end();
4457
+ try {
4458
+ const { name, active } = JSON.parse(await readBody(req));
4459
+ if (!SHIELDS[name]) return res.writeHead(400).end();
4460
+ const current = readActiveShields();
4461
+ const updated = active ? [.../* @__PURE__ */ new Set([...current, name])] : current.filter((n) => n !== name);
4462
+ writeActiveShields(updated);
4463
+ _resetConfigCache();
4464
+ const shieldsPayload = Object.values(SHIELDS).map((s) => ({
4465
+ name: s.name,
4466
+ description: s.description,
4467
+ active: updated.includes(s.name)
4468
+ }));
4469
+ broadcast("shields-status", { shields: shieldsPayload });
4470
+ res.writeHead(200);
4471
+ return res.end(JSON.stringify({ ok: true }));
4472
+ } catch {
4473
+ res.writeHead(400).end();
4474
+ }
4475
+ }
4476
+ res.writeHead(404).end();
4477
+ });
4478
+ daemonServer = server;
4479
+ server.on("error", (e) => {
4480
+ if (e.code === "EADDRINUSE") {
4481
+ try {
4482
+ if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
4483
+ const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4484
+ process.kill(pid, 0);
4485
+ return process.exit(0);
4486
+ }
4487
+ } catch {
4488
+ try {
4489
+ import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4490
+ } catch {
4491
+ }
4492
+ server.listen(DAEMON_PORT2, DAEMON_HOST2);
4493
+ return;
4494
+ }
4495
+ }
4496
+ console.error(import_chalk4.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
4497
+ process.exit(1);
4498
+ });
4499
+ server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
4500
+ atomicWriteSync2(
4501
+ DAEMON_PID_FILE,
4502
+ JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
4503
+ { mode: 384 }
4504
+ );
4505
+ console.log(import_chalk4.default.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
4506
+ });
4507
+ if (watchMode) {
4508
+ console.log(import_chalk4.default.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
4509
+ }
4510
+ try {
4511
+ import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4512
+ } catch {
4513
+ }
4514
+ const ACTIVITY_MAX_BYTES = 1024 * 1024;
4515
+ const unixServer = import_net2.default.createServer((socket) => {
4516
+ const chunks = [];
4517
+ let bytesReceived = 0;
4518
+ socket.on("data", (chunk) => {
4519
+ bytesReceived += chunk.length;
4520
+ if (bytesReceived > ACTIVITY_MAX_BYTES) {
4521
+ socket.destroy();
4522
+ return;
4523
+ }
4524
+ chunks.push(chunk);
4525
+ });
4526
+ socket.on("end", () => {
4527
+ try {
4528
+ const data = JSON.parse(Buffer.concat(chunks).toString());
4529
+ if (data.status === "pending") {
4530
+ broadcast("activity", {
4531
+ id: data.id,
4532
+ ts: data.ts,
4533
+ tool: data.tool,
4534
+ args: redactArgs(data.args),
4535
+ status: "pending"
4536
+ });
4537
+ } else {
4538
+ broadcast("activity-result", {
4539
+ id: data.id,
4540
+ status: data.status,
4541
+ label: data.label
4542
+ });
4543
+ }
4544
+ } catch {
4545
+ }
4546
+ });
4547
+ socket.on("error", () => {
4548
+ });
4549
+ });
4550
+ unixServer.listen(ACTIVITY_SOCKET_PATH2);
4551
+ process.on("exit", () => {
4552
+ try {
4553
+ import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4554
+ } catch {
4555
+ }
4556
+ });
4557
+ }
4558
+ function stopDaemon() {
4559
+ if (!import_fs4.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
4560
+ try {
4561
+ const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4562
+ process.kill(pid, "SIGTERM");
4563
+ console.log(import_chalk4.default.green("\u2705 Stopped."));
4564
+ } catch {
4565
+ console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
4566
+ } finally {
4567
+ try {
4568
+ import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4569
+ } catch {
4570
+ }
4571
+ }
4572
+ }
4573
+ function daemonStatus() {
4574
+ if (!import_fs4.default.existsSync(DAEMON_PID_FILE))
4575
+ return console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
4576
+ try {
4577
+ const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4578
+ process.kill(pid, 0);
4579
+ console.log(import_chalk4.default.green("Node9 daemon: running"));
4580
+ } catch {
4581
+ console.log(import_chalk4.default.yellow("Node9 daemon: not running (stale PID)"));
4582
+ }
4583
+ }
4584
+ var import_http, import_net2, import_fs4, import_path6, import_os4, import_child_process2, import_crypto3, import_chalk4, ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
4585
+ var init_daemon = __esm({
4586
+ "src/daemon/index.ts"() {
4587
+ "use strict";
4588
+ init_ui2();
4589
+ import_http = __toESM(require("http"));
4590
+ import_net2 = __toESM(require("net"));
4591
+ import_fs4 = __toESM(require("fs"));
4592
+ import_path6 = __toESM(require("path"));
4593
+ import_os4 = __toESM(require("os"));
4594
+ import_child_process2 = require("child_process");
4595
+ import_crypto3 = require("crypto");
4596
+ import_chalk4 = __toESM(require("chalk"));
4597
+ init_core();
4598
+ init_shields();
4599
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path6.default.join(import_os4.default.tmpdir(), "node9-activity.sock");
4600
+ DAEMON_PORT2 = 7391;
4601
+ DAEMON_HOST2 = "127.0.0.1";
4602
+ homeDir = import_os4.default.homedir();
4603
+ DAEMON_PID_FILE = import_path6.default.join(homeDir, ".node9", "daemon.pid");
4604
+ DECISIONS_FILE = import_path6.default.join(homeDir, ".node9", "decisions.json");
4605
+ GLOBAL_CONFIG_FILE = import_path6.default.join(homeDir, ".node9", "config.json");
4606
+ CREDENTIALS_FILE = import_path6.default.join(homeDir, ".node9", "credentials.json");
4607
+ AUDIT_LOG_FILE = import_path6.default.join(homeDir, ".node9", "audit.log");
4608
+ TRUST_FILE2 = import_path6.default.join(homeDir, ".node9", "trust.json");
4609
+ TRUST_DURATIONS = {
4610
+ "30m": 30 * 6e4,
4611
+ "1h": 60 * 6e4,
4612
+ "2h": 2 * 60 * 6e4
4613
+ };
4614
+ SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
4615
+ AUTO_DENY_MS = 12e4;
4616
+ autoStarted = process.env.NODE9_AUTO_STARTED === "1";
4617
+ pending = /* @__PURE__ */ new Map();
4618
+ sseClients = /* @__PURE__ */ new Set();
4619
+ abandonTimer = null;
4620
+ daemonServer = null;
4621
+ hadBrowserClient = false;
4622
+ ACTIVITY_RING_SIZE = 100;
4623
+ activityRing = [];
4624
+ }
4625
+ });
4626
+
4627
+ // src/tui/tail.ts
4628
+ var tail_exports = {};
4629
+ __export(tail_exports, {
4630
+ startTail: () => startTail
4631
+ });
4632
+ function getIcon(tool) {
4633
+ const t = tool.toLowerCase();
4634
+ for (const [k, v] of Object.entries(ICONS)) {
4635
+ if (t.includes(k)) return v;
4636
+ }
4637
+ return "\u{1F6E0}\uFE0F";
4638
+ }
4639
+ function formatBase(activity) {
4640
+ const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
4641
+ const icon = getIcon(activity.tool);
4642
+ const toolName = activity.tool.slice(0, 16).padEnd(16);
4643
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
4644
+ const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
4645
+ return `${import_chalk5.default.gray(time)} ${icon} ${import_chalk5.default.white.bold(toolName)} ${import_chalk5.default.dim(argsPreview)}`;
4646
+ }
4647
+ function renderResult(activity, result) {
4648
+ const base = formatBase(activity);
4649
+ let status;
4650
+ if (result.status === "allow") {
4651
+ status = import_chalk5.default.green("\u2713 ALLOW");
4652
+ } else if (result.status === "dlp") {
4653
+ status = import_chalk5.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
4654
+ } else {
4655
+ status = import_chalk5.default.red("\u2717 BLOCK");
4656
+ }
4657
+ if (process.stdout.isTTY) {
4658
+ import_readline.default.clearLine(process.stdout, 0);
4659
+ import_readline.default.cursorTo(process.stdout, 0);
4660
+ }
4661
+ console.log(`${base} ${status}`);
4662
+ }
4663
+ function renderPending(activity) {
4664
+ if (!process.stdout.isTTY) return;
4665
+ process.stdout.write(`${formatBase(activity)} ${import_chalk5.default.yellow("\u25CF \u2026")}\r`);
4666
+ }
4667
+ async function ensureDaemon() {
4668
+ if (import_fs6.default.existsSync(PID_FILE)) {
4669
+ try {
4670
+ const { port } = JSON.parse(import_fs6.default.readFileSync(PID_FILE, "utf-8"));
4671
+ return port;
4672
+ } catch {
4673
+ }
4674
+ }
4675
+ console.log(import_chalk5.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
4676
+ const child = (0, import_child_process4.spawn)(process.execPath, [process.argv[1], "daemon"], {
4677
+ detached: true,
4678
+ stdio: "ignore",
4679
+ env: { ...process.env, NODE9_AUTO_STARTED: "1" }
4680
+ });
4681
+ child.unref();
4682
+ for (let i = 0; i < 20; i++) {
4683
+ await new Promise((r) => setTimeout(r, 250));
4684
+ if (!import_fs6.default.existsSync(PID_FILE)) continue;
4685
+ try {
4686
+ const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
4687
+ signal: AbortSignal.timeout(500)
4688
+ });
4689
+ if (res.ok) {
4690
+ const { port } = JSON.parse(import_fs6.default.readFileSync(PID_FILE, "utf-8"));
4691
+ return port;
4692
+ }
4693
+ } catch {
4694
+ }
4695
+ }
4696
+ console.error(import_chalk5.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
4697
+ process.exit(1);
4698
+ }
4699
+ async function startTail(options = {}) {
4700
+ const port = await ensureDaemon();
4701
+ const connectionTime = Date.now();
4702
+ const pending2 = /* @__PURE__ */ new Map();
4703
+ console.log(import_chalk5.default.cyan.bold(`
4704
+ \u{1F6F0}\uFE0F Node9 tail `) + import_chalk5.default.dim(`\u2192 localhost:${port}`));
4705
+ if (options.history) {
4706
+ console.log(import_chalk5.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
4707
+ } else {
4708
+ console.log(
4709
+ import_chalk5.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
4710
+ );
4711
+ }
4712
+ process.on("SIGINT", () => {
4713
+ if (process.stdout.isTTY) {
4714
+ import_readline.default.clearLine(process.stdout, 0);
4715
+ import_readline.default.cursorTo(process.stdout, 0);
4716
+ }
4717
+ console.log(import_chalk5.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
4718
+ process.exit(0);
4719
+ });
4720
+ const req = import_http2.default.get(`http://127.0.0.1:${port}/events`, (res) => {
4721
+ if (res.statusCode !== 200) {
4722
+ console.error(import_chalk5.default.red(`Failed to connect: HTTP ${res.statusCode}`));
4723
+ process.exit(1);
4724
+ }
4725
+ let currentEvent = "";
4726
+ let currentData = "";
4727
+ res.on("error", () => {
4728
+ });
4729
+ const rl = import_readline.default.createInterface({ input: res, crlfDelay: Infinity });
4730
+ rl.on("error", () => {
4731
+ });
4732
+ rl.on("line", (line) => {
4733
+ if (line.startsWith("event:")) {
4734
+ currentEvent = line.slice(6).trim();
4735
+ } else if (line.startsWith("data:")) {
4736
+ currentData = line.slice(5).trim();
4737
+ } else if (line === "") {
4738
+ if (currentEvent && currentData) {
4739
+ handleMessage(currentEvent, currentData);
4740
+ }
4741
+ currentEvent = "";
4742
+ currentData = "";
4743
+ }
4744
+ });
4745
+ rl.on("close", () => {
4746
+ if (process.stdout.isTTY) {
4747
+ import_readline.default.clearLine(process.stdout, 0);
4748
+ import_readline.default.cursorTo(process.stdout, 0);
4749
+ }
4750
+ console.log(import_chalk5.default.red("\n\u274C Daemon disconnected."));
4751
+ process.exit(1);
4752
+ });
4753
+ });
4754
+ function handleMessage(event, rawData) {
4755
+ let data;
4756
+ try {
4757
+ data = JSON.parse(rawData);
4758
+ } catch {
4759
+ return;
4760
+ }
4761
+ if (event === "activity") {
4762
+ if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
4763
+ if (data.status && data.status !== "pending") {
4764
+ renderResult(data, data);
4765
+ return;
4766
+ }
4767
+ pending2.set(data.id, data);
4768
+ const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
4769
+ if (slowTool) renderPending(data);
4770
+ }
4771
+ if (event === "activity-result") {
4772
+ const original = pending2.get(data.id);
4773
+ if (original) {
4774
+ renderResult(original, data);
4775
+ pending2.delete(data.id);
4776
+ }
4777
+ }
4778
+ }
4779
+ req.on("error", (err) => {
4780
+ const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
4781
+ console.error(import_chalk5.default.red(`
4782
+ \u274C ${msg}`));
4783
+ process.exit(1);
4784
+ });
4785
+ }
4786
+ var import_http2, import_chalk5, import_fs6, import_os6, import_path8, import_readline, import_child_process4, PID_FILE, ICONS;
4787
+ var init_tail = __esm({
4788
+ "src/tui/tail.ts"() {
4789
+ "use strict";
4790
+ import_http2 = __toESM(require("http"));
4791
+ import_chalk5 = __toESM(require("chalk"));
4792
+ import_fs6 = __toESM(require("fs"));
4793
+ import_os6 = __toESM(require("os"));
4794
+ import_path8 = __toESM(require("path"));
4795
+ import_readline = __toESM(require("readline"));
4796
+ import_child_process4 = require("child_process");
4797
+ init_daemon();
4798
+ PID_FILE = import_path8.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
4799
+ ICONS = {
4800
+ bash: "\u{1F4BB}",
4801
+ shell: "\u{1F4BB}",
4802
+ terminal: "\u{1F4BB}",
4803
+ read: "\u{1F4D6}",
4804
+ edit: "\u270F\uFE0F",
4805
+ write: "\u270F\uFE0F",
4806
+ glob: "\u{1F4C2}",
4807
+ grep: "\u{1F50D}",
4808
+ agent: "\u{1F916}",
4809
+ search: "\u{1F50D}",
4810
+ sql: "\u{1F5C4}\uFE0F",
4811
+ query: "\u{1F5C4}\uFE0F",
4812
+ list: "\u{1F4C2}",
4813
+ delete: "\u{1F5D1}\uFE0F",
4814
+ web: "\u{1F310}"
4815
+ };
4816
+ }
4817
+ });
4818
+
4819
+ // src/cli.ts
4820
+ var import_commander = require("commander");
4821
+ init_core();
4822
+
4823
+ // src/setup.ts
4824
+ var import_fs3 = __toESM(require("fs"));
4825
+ var import_path5 = __toESM(require("path"));
4826
+ var import_os3 = __toESM(require("os"));
4827
+ var import_chalk3 = __toESM(require("chalk"));
4828
+ var import_prompts2 = require("@inquirer/prompts");
4829
+ function printDaemonTip() {
4830
+ console.log(
4831
+ import_chalk3.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + import_chalk3.default.white("\n To view your history or manage persistent rules, run:") + import_chalk3.default.green("\n node9 daemon --openui")
4832
+ );
4833
+ }
4834
+ function fullPathCommand(subcommand) {
4835
+ if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
4836
+ const nodeExec = process.execPath;
4837
+ const cliScript = process.argv[1];
4838
+ return `${nodeExec} ${cliScript} ${subcommand}`;
4839
+ }
4840
+ function readJson(filePath) {
4841
+ try {
4842
+ if (import_fs3.default.existsSync(filePath)) {
4843
+ return JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
4844
+ }
4845
+ } catch {
4846
+ }
4847
+ return null;
4848
+ }
4849
+ function writeJson(filePath, data) {
4850
+ const dir = import_path5.default.dirname(filePath);
4851
+ if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
4852
+ import_fs3.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
4853
+ }
4854
+ async function setupClaude() {
4855
+ const homeDir2 = import_os3.default.homedir();
4856
+ const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
4857
+ const hooksPath = import_path5.default.join(homeDir2, ".claude", "settings.json");
4858
+ const claudeConfig = readJson(mcpPath) ?? {};
4859
+ const settings = readJson(hooksPath) ?? {};
4860
+ const servers = claudeConfig.mcpServers ?? {};
4861
+ let anythingChanged = false;
4862
+ if (!settings.hooks) settings.hooks = {};
4863
+ const hasPreHook = settings.hooks.PreToolUse?.some(
4864
+ (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
4865
+ );
4866
+ if (!hasPreHook) {
4867
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
4868
+ settings.hooks.PreToolUse.push({
4869
+ matcher: ".*",
4870
+ hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
4871
+ });
4872
+ console.log(import_chalk3.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
4873
+ anythingChanged = true;
4874
+ }
4875
+ const hasPostHook = settings.hooks.PostToolUse?.some(
4876
+ (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
4877
+ );
4878
+ if (!hasPostHook) {
4879
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
4880
+ settings.hooks.PostToolUse.push({
4881
+ matcher: ".*",
4882
+ hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
4883
+ });
4884
+ console.log(import_chalk3.default.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
4885
+ anythingChanged = true;
4886
+ }
4887
+ if (anythingChanged) {
4888
+ writeJson(hooksPath, settings);
4889
+ console.log("");
4890
+ }
4891
+ const serversToWrap = [];
4892
+ for (const [name, server] of Object.entries(servers)) {
4893
+ if (!server.command || server.command === "node9") continue;
4894
+ const parts = [server.command, ...server.args ?? []];
4895
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
4896
+ }
4897
+ if (serversToWrap.length > 0) {
4898
+ console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
4899
+ console.log(import_chalk3.default.white(` ${mcpPath}`));
4900
+ for (const { name, originalCmd } of serversToWrap) {
4901
+ console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
4902
+ }
4903
+ console.log("");
4904
+ const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
4905
+ if (proceed) {
4906
+ for (const { name, parts } of serversToWrap) {
4907
+ servers[name] = { ...servers[name], command: "node9", args: parts };
4908
+ }
4909
+ claudeConfig.mcpServers = servers;
4910
+ writeJson(mcpPath, claudeConfig);
4911
+ console.log(import_chalk3.default.green(`
4912
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
4913
+ anythingChanged = true;
4914
+ } else {
4915
+ console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
3894
4916
  }
3895
- res.writeHead(404).end();
3896
- });
3897
- daemonServer = server;
3898
- server.on("error", (e) => {
3899
- if (e.code === "EADDRINUSE") {
3900
- try {
3901
- if (import_fs3.default.existsSync(DAEMON_PID_FILE)) {
3902
- const { pid } = JSON.parse(import_fs3.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
3903
- process.kill(pid, 0);
3904
- return process.exit(0);
3905
- }
3906
- } catch {
3907
- try {
3908
- import_fs3.default.unlinkSync(DAEMON_PID_FILE);
3909
- } catch {
4917
+ console.log("");
4918
+ }
4919
+ if (!anythingChanged && serversToWrap.length === 0) {
4920
+ console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
4921
+ printDaemonTip();
4922
+ return;
4923
+ }
4924
+ if (anythingChanged) {
4925
+ console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
4926
+ console.log(import_chalk3.default.gray(" Restart Claude Code for changes to take effect."));
4927
+ printDaemonTip();
4928
+ }
4929
+ }
4930
+ async function setupGemini() {
4931
+ const homeDir2 = import_os3.default.homedir();
4932
+ const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
4933
+ const settings = readJson(settingsPath) ?? {};
4934
+ const servers = settings.mcpServers ?? {};
4935
+ let anythingChanged = false;
4936
+ if (!settings.hooks) settings.hooks = {};
4937
+ const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
4938
+ (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
4939
+ );
4940
+ if (!hasBeforeHook) {
4941
+ if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
4942
+ if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
4943
+ settings.hooks.BeforeTool.push({
4944
+ matcher: ".*",
4945
+ hooks: [
4946
+ {
4947
+ name: "node9-check",
4948
+ type: "command",
4949
+ command: fullPathCommand("check"),
4950
+ timeout: 6e5
3910
4951
  }
3911
- server.listen(DAEMON_PORT2, DAEMON_HOST2);
3912
- return;
4952
+ ]
4953
+ });
4954
+ console.log(import_chalk3.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
4955
+ anythingChanged = true;
4956
+ }
4957
+ const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
4958
+ (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
4959
+ );
4960
+ if (!hasAfterHook) {
4961
+ if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
4962
+ if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
4963
+ settings.hooks.AfterTool.push({
4964
+ matcher: ".*",
4965
+ hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
4966
+ });
4967
+ console.log(import_chalk3.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
4968
+ anythingChanged = true;
4969
+ }
4970
+ if (anythingChanged) {
4971
+ writeJson(settingsPath, settings);
4972
+ console.log("");
4973
+ }
4974
+ const serversToWrap = [];
4975
+ for (const [name, server] of Object.entries(servers)) {
4976
+ if (!server.command || server.command === "node9") continue;
4977
+ const parts = [server.command, ...server.args ?? []];
4978
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
4979
+ }
4980
+ if (serversToWrap.length > 0) {
4981
+ console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
4982
+ console.log(import_chalk3.default.white(` ${settingsPath} (mcpServers)`));
4983
+ for (const { name, originalCmd } of serversToWrap) {
4984
+ console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
4985
+ }
4986
+ console.log("");
4987
+ const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
4988
+ if (proceed) {
4989
+ for (const { name, parts } of serversToWrap) {
4990
+ servers[name] = { ...servers[name], command: "node9", args: parts };
3913
4991
  }
4992
+ settings.mcpServers = servers;
4993
+ writeJson(settingsPath, settings);
4994
+ console.log(import_chalk3.default.green(`
4995
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
4996
+ anythingChanged = true;
4997
+ } else {
4998
+ console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
3914
4999
  }
3915
- console.error(import_chalk4.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
3916
- process.exit(1);
3917
- });
3918
- server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
3919
- atomicWriteSync2(
3920
- DAEMON_PID_FILE,
3921
- JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
3922
- { mode: 384 }
3923
- );
3924
- console.log(import_chalk4.default.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
3925
- });
5000
+ console.log("");
5001
+ }
5002
+ if (!anythingChanged && serversToWrap.length === 0) {
5003
+ console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
5004
+ printDaemonTip();
5005
+ return;
5006
+ }
5007
+ if (anythingChanged) {
5008
+ console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
5009
+ console.log(import_chalk3.default.gray(" Restart Gemini CLI for changes to take effect."));
5010
+ printDaemonTip();
5011
+ }
3926
5012
  }
3927
- function stopDaemon() {
3928
- if (!import_fs3.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
3929
- try {
3930
- const { pid } = JSON.parse(import_fs3.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
3931
- process.kill(pid, "SIGTERM");
3932
- console.log(import_chalk4.default.green("\u2705 Stopped."));
3933
- } catch {
3934
- console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
3935
- } finally {
3936
- try {
3937
- import_fs3.default.unlinkSync(DAEMON_PID_FILE);
3938
- } catch {
5013
+ async function setupCursor() {
5014
+ const homeDir2 = import_os3.default.homedir();
5015
+ const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
5016
+ const mcpConfig = readJson(mcpPath) ?? {};
5017
+ const servers = mcpConfig.mcpServers ?? {};
5018
+ let anythingChanged = false;
5019
+ const serversToWrap = [];
5020
+ for (const [name, server] of Object.entries(servers)) {
5021
+ if (!server.command || server.command === "node9") continue;
5022
+ const parts = [server.command, ...server.args ?? []];
5023
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
5024
+ }
5025
+ if (serversToWrap.length > 0) {
5026
+ console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
5027
+ console.log(import_chalk3.default.white(` ${mcpPath}`));
5028
+ for (const { name, originalCmd } of serversToWrap) {
5029
+ console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5030
+ }
5031
+ console.log("");
5032
+ const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
5033
+ if (proceed) {
5034
+ for (const { name, parts } of serversToWrap) {
5035
+ servers[name] = { ...servers[name], command: "node9", args: parts };
5036
+ }
5037
+ mcpConfig.mcpServers = servers;
5038
+ writeJson(mcpPath, mcpConfig);
5039
+ console.log(import_chalk3.default.green(`
5040
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
5041
+ anythingChanged = true;
5042
+ } else {
5043
+ console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
3939
5044
  }
5045
+ console.log("");
3940
5046
  }
3941
- }
3942
- function daemonStatus() {
3943
- if (!import_fs3.default.existsSync(DAEMON_PID_FILE))
3944
- return console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
3945
- try {
3946
- const { pid } = JSON.parse(import_fs3.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
3947
- process.kill(pid, 0);
3948
- console.log(import_chalk4.default.green("Node9 daemon: running"));
3949
- } catch {
3950
- console.log(import_chalk4.default.yellow("Node9 daemon: not running (stale PID)"));
5047
+ console.log(
5048
+ import_chalk3.default.yellow(
5049
+ " \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
5050
+ )
5051
+ );
5052
+ console.log("");
5053
+ if (!anythingChanged && serversToWrap.length === 0) {
5054
+ console.log(
5055
+ import_chalk3.default.blue(
5056
+ "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
5057
+ )
5058
+ );
5059
+ printDaemonTip();
5060
+ return;
5061
+ }
5062
+ if (anythingChanged) {
5063
+ console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
5064
+ console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
5065
+ printDaemonTip();
3951
5066
  }
3952
5067
  }
3953
5068
 
3954
5069
  // src/cli.ts
3955
- var import_child_process4 = require("child_process");
5070
+ init_daemon();
5071
+ var import_child_process5 = require("child_process");
3956
5072
  var import_execa = require("execa");
3957
5073
  var import_execa2 = require("execa");
3958
- var import_chalk5 = __toESM(require("chalk"));
3959
- var import_readline = __toESM(require("readline"));
3960
- var import_fs5 = __toESM(require("fs"));
3961
- var import_path7 = __toESM(require("path"));
3962
- var import_os5 = __toESM(require("os"));
5074
+ var import_chalk6 = __toESM(require("chalk"));
5075
+ var import_readline2 = __toESM(require("readline"));
5076
+ var import_fs7 = __toESM(require("fs"));
5077
+ var import_path9 = __toESM(require("path"));
5078
+ var import_os7 = __toESM(require("os"));
3963
5079
 
3964
5080
  // src/undo.ts
3965
5081
  var import_child_process3 = require("child_process");
3966
- var import_fs4 = __toESM(require("fs"));
3967
- var import_path6 = __toESM(require("path"));
3968
- var import_os4 = __toESM(require("os"));
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");
5082
+ var import_fs5 = __toESM(require("fs"));
5083
+ var import_path7 = __toESM(require("path"));
5084
+ var import_os5 = __toESM(require("os"));
5085
+ var SNAPSHOT_STACK_PATH = import_path7.default.join(import_os5.default.homedir(), ".node9", "snapshots.json");
5086
+ var UNDO_LATEST_PATH = import_path7.default.join(import_os5.default.homedir(), ".node9", "undo_latest.txt");
3971
5087
  var MAX_SNAPSHOTS = 10;
3972
5088
  function readStack() {
3973
5089
  try {
3974
- if (import_fs4.default.existsSync(SNAPSHOT_STACK_PATH))
3975
- return JSON.parse(import_fs4.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
5090
+ if (import_fs5.default.existsSync(SNAPSHOT_STACK_PATH))
5091
+ return JSON.parse(import_fs5.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
3976
5092
  } catch {
3977
5093
  }
3978
5094
  return [];
3979
5095
  }
3980
5096
  function writeStack(stack) {
3981
- const dir = import_path6.default.dirname(SNAPSHOT_STACK_PATH);
3982
- if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
3983
- import_fs4.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
5097
+ const dir = import_path7.default.dirname(SNAPSHOT_STACK_PATH);
5098
+ if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
5099
+ import_fs5.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
3984
5100
  }
3985
5101
  function buildArgsSummary(tool, args) {
3986
5102
  if (!args || typeof args !== "object") return "";
@@ -3996,13 +5112,13 @@ function buildArgsSummary(tool, args) {
3996
5112
  async function createShadowSnapshot(tool = "unknown", args = {}) {
3997
5113
  try {
3998
5114
  const cwd = process.cwd();
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()}`);
5115
+ if (!import_fs5.default.existsSync(import_path7.default.join(cwd, ".git"))) return null;
5116
+ const tempIndex = import_path7.default.join(cwd, ".git", `node9_index_${Date.now()}`);
4001
5117
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
4002
5118
  (0, import_child_process3.spawnSync)("git", ["add", "-A"], { env });
4003
5119
  const treeRes = (0, import_child_process3.spawnSync)("git", ["write-tree"], { env });
4004
5120
  const treeHash = treeRes.stdout.toString().trim();
4005
- if (import_fs4.default.existsSync(tempIndex)) import_fs4.default.unlinkSync(tempIndex);
5121
+ if (import_fs5.default.existsSync(tempIndex)) import_fs5.default.unlinkSync(tempIndex);
4006
5122
  if (!treeHash || treeRes.status !== 0) return null;
4007
5123
  const commitRes = (0, import_child_process3.spawnSync)("git", [
4008
5124
  "commit-tree",
@@ -4023,7 +5139,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}) {
4023
5139
  stack.push(entry);
4024
5140
  if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
4025
5141
  writeStack(stack);
4026
- import_fs4.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
5142
+ import_fs5.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
4027
5143
  return commitHash;
4028
5144
  } catch (err) {
4029
5145
  if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
@@ -4061,9 +5177,9 @@ function applyUndo(hash, cwd) {
4061
5177
  const tracked = (0, import_child_process3.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
4062
5178
  const untracked = (0, import_child_process3.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
4063
5179
  for (const file of [...tracked, ...untracked]) {
4064
- const fullPath = import_path6.default.join(dir, file);
4065
- if (!snapshotFiles.has(file) && import_fs4.default.existsSync(fullPath)) {
4066
- import_fs4.default.unlinkSync(fullPath);
5180
+ const fullPath = import_path7.default.join(dir, file);
5181
+ if (!snapshotFiles.has(file) && import_fs5.default.existsSync(fullPath)) {
5182
+ import_fs5.default.unlinkSync(fullPath);
4067
5183
  }
4068
5184
  }
4069
5185
  return true;
@@ -4073,9 +5189,10 @@ function applyUndo(hash, cwd) {
4073
5189
  }
4074
5190
 
4075
5191
  // src/cli.ts
5192
+ init_shields();
4076
5193
  var import_prompts3 = require("@inquirer/prompts");
4077
5194
  var { version } = JSON.parse(
4078
- import_fs5.default.readFileSync(import_path7.default.join(__dirname, "../package.json"), "utf-8")
5195
+ import_fs7.default.readFileSync(import_path9.default.join(__dirname, "../package.json"), "utf-8")
4079
5196
  );
4080
5197
  function parseDuration(str) {
4081
5198
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -4107,6 +5224,15 @@ INSTRUCTIONS:
4107
5224
  - If you believe this action is critical, explain your reasoning and ask them to run "node9 pause 15m" to proceed.`;
4108
5225
  }
4109
5226
  const label = blockedByLabel.toLowerCase();
5227
+ if (label.includes("dlp") || label.includes("secret detected") || label.includes("credential review")) {
5228
+ return `NODE9 SECURITY ALERT: A sensitive credential (API key, token, or private key) was found in your tool call arguments.
5229
+ CRITICAL INSTRUCTION: Do NOT retry this action.
5230
+ REQUIRED ACTIONS:
5231
+ 1. Remove the hardcoded credential from your command or code.
5232
+ 2. Use an environment variable or a dedicated secrets manager instead.
5233
+ 3. Treat the leaked credential as compromised and rotate it immediately.
5234
+ Do NOT attempt to bypass this check or pass the credential through another tool.`;
5235
+ }
4110
5236
  if (label.includes("sql safety") && label.includes("delete without where")) {
4111
5237
  return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
4112
5238
  INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
@@ -4154,15 +5280,15 @@ function openBrowserLocal() {
4154
5280
  const url = `http://${DAEMON_HOST2}:${DAEMON_PORT2}/`;
4155
5281
  try {
4156
5282
  const opts = { stdio: "ignore" };
4157
- if (process.platform === "darwin") (0, import_child_process4.execSync)(`open "${url}"`, opts);
4158
- else if (process.platform === "win32") (0, import_child_process4.execSync)(`cmd /c start "" "${url}"`, opts);
4159
- else (0, import_child_process4.execSync)(`xdg-open "${url}"`, opts);
5283
+ if (process.platform === "darwin") (0, import_child_process5.execSync)(`open "${url}"`, opts);
5284
+ else if (process.platform === "win32") (0, import_child_process5.execSync)(`cmd /c start "" "${url}"`, opts);
5285
+ else (0, import_child_process5.execSync)(`xdg-open "${url}"`, opts);
4160
5286
  } catch {
4161
5287
  }
4162
5288
  }
4163
5289
  async function autoStartDaemonAndWait() {
4164
5290
  try {
4165
- const child = (0, import_child_process4.spawn)("node9", ["daemon"], {
5291
+ const child = (0, import_child_process5.spawn)("node9", ["daemon"], {
4166
5292
  detached: true,
4167
5293
  stdio: "ignore",
4168
5294
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
@@ -4198,14 +5324,14 @@ async function runProxy(targetCommand) {
4198
5324
  if (stdout) executable = stdout.trim();
4199
5325
  } catch {
4200
5326
  }
4201
- console.log(import_chalk5.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
4202
- const child = (0, import_child_process4.spawn)(executable, args, {
5327
+ console.log(import_chalk6.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
5328
+ const child = (0, import_child_process5.spawn)(executable, args, {
4203
5329
  stdio: ["pipe", "pipe", "inherit"],
4204
5330
  // We control STDIN and STDOUT
4205
5331
  shell: false,
4206
5332
  env: { ...process.env, FORCE_COLOR: "1" }
4207
5333
  });
4208
- const agentIn = import_readline.default.createInterface({ input: process.stdin, terminal: false });
5334
+ const agentIn = import_readline2.default.createInterface({ input: process.stdin, terminal: false });
4209
5335
  agentIn.on("line", async (line) => {
4210
5336
  let message;
4211
5337
  try {
@@ -4223,10 +5349,10 @@ async function runProxy(targetCommand) {
4223
5349
  agent: "Proxy/MCP"
4224
5350
  });
4225
5351
  if (!result.approved) {
4226
- console.error(import_chalk5.default.red(`
5352
+ console.error(import_chalk6.default.red(`
4227
5353
  \u{1F6D1} Node9 Sudo: Action Blocked`));
4228
- console.error(import_chalk5.default.gray(` Tool: ${name}`));
4229
- console.error(import_chalk5.default.gray(` Reason: ${result.reason || "Security Policy"}
5354
+ console.error(import_chalk6.default.gray(` Tool: ${name}`));
5355
+ console.error(import_chalk6.default.gray(` Reason: ${result.reason || "Security Policy"}
4230
5356
  `));
4231
5357
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
4232
5358
  const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -4268,14 +5394,14 @@ async function runProxy(targetCommand) {
4268
5394
  }
4269
5395
  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) => {
4270
5396
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
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 });
5397
+ const credPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "credentials.json");
5398
+ if (!import_fs7.default.existsSync(import_path9.default.dirname(credPath)))
5399
+ import_fs7.default.mkdirSync(import_path9.default.dirname(credPath), { recursive: true });
4274
5400
  const profileName = options.profile || "default";
4275
5401
  let existingCreds = {};
4276
5402
  try {
4277
- if (import_fs5.default.existsSync(credPath)) {
4278
- const raw = JSON.parse(import_fs5.default.readFileSync(credPath, "utf-8"));
5403
+ if (import_fs7.default.existsSync(credPath)) {
5404
+ const raw = JSON.parse(import_fs7.default.readFileSync(credPath, "utf-8"));
4279
5405
  if (raw.apiKey) {
4280
5406
  existingCreds = {
4281
5407
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -4287,13 +5413,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4287
5413
  } catch {
4288
5414
  }
4289
5415
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
4290
- import_fs5.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
5416
+ import_fs7.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
4291
5417
  if (profileName === "default") {
4292
- const configPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
5418
+ const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
4293
5419
  let config = {};
4294
5420
  try {
4295
- if (import_fs5.default.existsSync(configPath))
4296
- config = JSON.parse(import_fs5.default.readFileSync(configPath, "utf-8"));
5421
+ if (import_fs7.default.existsSync(configPath))
5422
+ config = JSON.parse(import_fs7.default.readFileSync(configPath, "utf-8"));
4297
5423
  } catch {
4298
5424
  }
4299
5425
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -4308,36 +5434,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4308
5434
  approvers.cloud = false;
4309
5435
  }
4310
5436
  s.approvers = approvers;
4311
- if (!import_fs5.default.existsSync(import_path7.default.dirname(configPath)))
4312
- import_fs5.default.mkdirSync(import_path7.default.dirname(configPath), { recursive: true });
4313
- import_fs5.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
5437
+ if (!import_fs7.default.existsSync(import_path9.default.dirname(configPath)))
5438
+ import_fs7.default.mkdirSync(import_path9.default.dirname(configPath), { recursive: true });
5439
+ import_fs7.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
4314
5440
  }
4315
5441
  if (options.profile && profileName !== "default") {
4316
- console.log(import_chalk5.default.green(`\u2705 Profile "${profileName}" saved`));
4317
- console.log(import_chalk5.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
5442
+ console.log(import_chalk6.default.green(`\u2705 Profile "${profileName}" saved`));
5443
+ console.log(import_chalk6.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
4318
5444
  } else if (options.local) {
4319
- console.log(import_chalk5.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
4320
- console.log(import_chalk5.default.gray(` All decisions stay on this machine.`));
5445
+ console.log(import_chalk6.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
5446
+ console.log(import_chalk6.default.gray(` All decisions stay on this machine.`));
4321
5447
  } else {
4322
- console.log(import_chalk5.default.green(`\u2705 Logged in \u2014 agent mode`));
4323
- console.log(import_chalk5.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
5448
+ console.log(import_chalk6.default.green(`\u2705 Logged in \u2014 agent mode`));
5449
+ console.log(import_chalk6.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
4324
5450
  }
4325
5451
  });
4326
5452
  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) => {
4327
5453
  if (target === "gemini") return await setupGemini();
4328
5454
  if (target === "claude") return await setupClaude();
4329
5455
  if (target === "cursor") return await setupCursor();
4330
- console.error(import_chalk5.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
5456
+ console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
4331
5457
  process.exit(1);
4332
5458
  });
4333
5459
  program.command("setup").description('Alias for "addto" \u2014 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) => {
4334
5460
  if (!target) {
4335
- console.log(import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
4336
- console.log(" Usage: " + import_chalk5.default.white("node9 setup <target>") + "\n");
5461
+ console.log(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
5462
+ console.log(" Usage: " + import_chalk6.default.white("node9 setup <target>") + "\n");
4337
5463
  console.log(" Targets:");
4338
- console.log(" " + import_chalk5.default.green("claude") + " \u2014 Claude Code (hook mode)");
4339
- console.log(" " + import_chalk5.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
4340
- console.log(" " + import_chalk5.default.green("cursor") + " \u2014 Cursor (hook mode)");
5464
+ console.log(" " + import_chalk6.default.green("claude") + " \u2014 Claude Code (hook mode)");
5465
+ console.log(" " + import_chalk6.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
5466
+ console.log(" " + import_chalk6.default.green("cursor") + " \u2014 Cursor (hook mode)");
4341
5467
  console.log("");
4342
5468
  return;
4343
5469
  }
@@ -4345,33 +5471,33 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
4345
5471
  if (t === "gemini") return await setupGemini();
4346
5472
  if (t === "claude") return await setupClaude();
4347
5473
  if (t === "cursor") return await setupCursor();
4348
- console.error(import_chalk5.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
5474
+ console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
4349
5475
  process.exit(1);
4350
5476
  });
4351
5477
  program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
4352
- const homeDir2 = import_os5.default.homedir();
5478
+ const homeDir2 = import_os7.default.homedir();
4353
5479
  let failures = 0;
4354
5480
  function pass(msg) {
4355
- console.log(import_chalk5.default.green(" \u2705 ") + msg);
5481
+ console.log(import_chalk6.default.green(" \u2705 ") + msg);
4356
5482
  }
4357
5483
  function fail(msg, hint) {
4358
- console.log(import_chalk5.default.red(" \u274C ") + msg);
4359
- if (hint) console.log(import_chalk5.default.gray(" " + hint));
5484
+ console.log(import_chalk6.default.red(" \u274C ") + msg);
5485
+ if (hint) console.log(import_chalk6.default.gray(" " + hint));
4360
5486
  failures++;
4361
5487
  }
4362
5488
  function warn(msg, hint) {
4363
- console.log(import_chalk5.default.yellow(" \u26A0\uFE0F ") + msg);
4364
- if (hint) console.log(import_chalk5.default.gray(" " + hint));
5489
+ console.log(import_chalk6.default.yellow(" \u26A0\uFE0F ") + msg);
5490
+ if (hint) console.log(import_chalk6.default.gray(" " + hint));
4365
5491
  }
4366
5492
  function section(title) {
4367
- console.log("\n" + import_chalk5.default.bold(title));
5493
+ console.log("\n" + import_chalk6.default.bold(title));
4368
5494
  }
4369
- console.log(import_chalk5.default.cyan.bold(`
5495
+ console.log(import_chalk6.default.cyan.bold(`
4370
5496
  \u{1F6E1}\uFE0F Node9 Doctor v${version}
4371
5497
  `));
4372
5498
  section("Binary");
4373
5499
  try {
4374
- const which = (0, import_child_process4.execSync)("which node9", { encoding: "utf-8" }).trim();
5500
+ const which = (0, import_child_process5.execSync)("which node9", { encoding: "utf-8" }).trim();
4375
5501
  pass(`node9 found at ${which}`);
4376
5502
  } catch {
4377
5503
  warn("node9 not found in $PATH \u2014 hooks may not find it", "Run: npm install -g @node9/proxy");
@@ -4386,7 +5512,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
4386
5512
  );
4387
5513
  }
4388
5514
  try {
4389
- const gitVersion = (0, import_child_process4.execSync)("git --version", { encoding: "utf-8" }).trim();
5515
+ const gitVersion = (0, import_child_process5.execSync)("git --version", { encoding: "utf-8" }).trim();
4390
5516
  pass(gitVersion);
4391
5517
  } catch {
4392
5518
  warn(
@@ -4395,10 +5521,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4395
5521
  );
4396
5522
  }
4397
5523
  section("Configuration");
4398
- const globalConfigPath = import_path7.default.join(homeDir2, ".node9", "config.json");
4399
- if (import_fs5.default.existsSync(globalConfigPath)) {
5524
+ const globalConfigPath = import_path9.default.join(homeDir2, ".node9", "config.json");
5525
+ if (import_fs7.default.existsSync(globalConfigPath)) {
4400
5526
  try {
4401
- JSON.parse(import_fs5.default.readFileSync(globalConfigPath, "utf-8"));
5527
+ JSON.parse(import_fs7.default.readFileSync(globalConfigPath, "utf-8"));
4402
5528
  pass("~/.node9/config.json found and valid");
4403
5529
  } catch {
4404
5530
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -4406,17 +5532,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
4406
5532
  } else {
4407
5533
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
4408
5534
  }
4409
- const projectConfigPath = import_path7.default.join(process.cwd(), "node9.config.json");
4410
- if (import_fs5.default.existsSync(projectConfigPath)) {
5535
+ const projectConfigPath = import_path9.default.join(process.cwd(), "node9.config.json");
5536
+ if (import_fs7.default.existsSync(projectConfigPath)) {
4411
5537
  try {
4412
- JSON.parse(import_fs5.default.readFileSync(projectConfigPath, "utf-8"));
5538
+ JSON.parse(import_fs7.default.readFileSync(projectConfigPath, "utf-8"));
4413
5539
  pass("node9.config.json found and valid (project)");
4414
5540
  } catch {
4415
5541
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
4416
5542
  }
4417
5543
  }
4418
- const credsPath = import_path7.default.join(homeDir2, ".node9", "credentials.json");
4419
- if (import_fs5.default.existsSync(credsPath)) {
5544
+ const credsPath = import_path9.default.join(homeDir2, ".node9", "credentials.json");
5545
+ if (import_fs7.default.existsSync(credsPath)) {
4420
5546
  pass("Cloud credentials found (~/.node9/credentials.json)");
4421
5547
  } else {
4422
5548
  warn(
@@ -4425,10 +5551,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4425
5551
  );
4426
5552
  }
4427
5553
  section("Agent Hooks");
4428
- const claudeSettingsPath = import_path7.default.join(homeDir2, ".claude", "settings.json");
4429
- if (import_fs5.default.existsSync(claudeSettingsPath)) {
5554
+ const claudeSettingsPath = import_path9.default.join(homeDir2, ".claude", "settings.json");
5555
+ if (import_fs7.default.existsSync(claudeSettingsPath)) {
4430
5556
  try {
4431
- const cs = JSON.parse(import_fs5.default.readFileSync(claudeSettingsPath, "utf-8"));
5557
+ const cs = JSON.parse(import_fs7.default.readFileSync(claudeSettingsPath, "utf-8"));
4432
5558
  const hasHook = cs.hooks?.PreToolUse?.some(
4433
5559
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4434
5560
  );
@@ -4441,10 +5567,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4441
5567
  } else {
4442
5568
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
4443
5569
  }
4444
- const geminiSettingsPath = import_path7.default.join(homeDir2, ".gemini", "settings.json");
4445
- if (import_fs5.default.existsSync(geminiSettingsPath)) {
5570
+ const geminiSettingsPath = import_path9.default.join(homeDir2, ".gemini", "settings.json");
5571
+ if (import_fs7.default.existsSync(geminiSettingsPath)) {
4446
5572
  try {
4447
- const gs = JSON.parse(import_fs5.default.readFileSync(geminiSettingsPath, "utf-8"));
5573
+ const gs = JSON.parse(import_fs7.default.readFileSync(geminiSettingsPath, "utf-8"));
4448
5574
  const hasHook = gs.hooks?.BeforeTool?.some(
4449
5575
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4450
5576
  );
@@ -4457,10 +5583,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4457
5583
  } else {
4458
5584
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
4459
5585
  }
4460
- const cursorHooksPath = import_path7.default.join(homeDir2, ".cursor", "hooks.json");
4461
- if (import_fs5.default.existsSync(cursorHooksPath)) {
5586
+ const cursorHooksPath = import_path9.default.join(homeDir2, ".cursor", "hooks.json");
5587
+ if (import_fs7.default.existsSync(cursorHooksPath)) {
4462
5588
  try {
4463
- const cur = JSON.parse(import_fs5.default.readFileSync(cursorHooksPath, "utf-8"));
5589
+ const cur = JSON.parse(import_fs7.default.readFileSync(cursorHooksPath, "utf-8"));
4464
5590
  const hasHook = cur.hooks?.preToolUse?.some(
4465
5591
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
4466
5592
  );
@@ -4481,9 +5607,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
4481
5607
  }
4482
5608
  console.log("");
4483
5609
  if (failures === 0) {
4484
- console.log(import_chalk5.default.green.bold(" All checks passed. Node9 is ready.\n"));
5610
+ console.log(import_chalk6.default.green.bold(" All checks passed. Node9 is ready.\n"));
4485
5611
  } else {
4486
- console.log(import_chalk5.default.red.bold(` ${failures} check(s) failed. See hints above.
5612
+ console.log(import_chalk6.default.red.bold(` ${failures} check(s) failed. See hints above.
4487
5613
  `));
4488
5614
  process.exit(1);
4489
5615
  }
@@ -4498,7 +5624,7 @@ program.command("explain").description(
4498
5624
  try {
4499
5625
  args = JSON.parse(trimmed);
4500
5626
  } catch {
4501
- console.error(import_chalk5.default.red(`
5627
+ console.error(import_chalk6.default.red(`
4502
5628
  \u274C Invalid JSON: ${trimmed}
4503
5629
  `));
4504
5630
  process.exit(1);
@@ -4509,63 +5635,63 @@ program.command("explain").description(
4509
5635
  }
4510
5636
  const result = await explainPolicy(tool, args);
4511
5637
  console.log("");
4512
- console.log(import_chalk5.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
5638
+ console.log(import_chalk6.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
4513
5639
  console.log("");
4514
- console.log(` ${import_chalk5.default.bold("Tool:")} ${import_chalk5.default.white(result.tool)}`);
5640
+ console.log(` ${import_chalk6.default.bold("Tool:")} ${import_chalk6.default.white(result.tool)}`);
4515
5641
  if (argsRaw) {
4516
5642
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
4517
- console.log(` ${import_chalk5.default.bold("Input:")} ${import_chalk5.default.gray(preview)}`);
5643
+ console.log(` ${import_chalk6.default.bold("Input:")} ${import_chalk6.default.gray(preview)}`);
4518
5644
  }
4519
5645
  console.log("");
4520
- console.log(import_chalk5.default.bold("Config Sources (Waterfall):"));
5646
+ console.log(import_chalk6.default.bold("Config Sources (Waterfall):"));
4521
5647
  for (const tier of result.waterfall) {
4522
- const num = import_chalk5.default.gray(` ${tier.tier}.`);
5648
+ const num = import_chalk6.default.gray(` ${tier.tier}.`);
4523
5649
  const label = tier.label.padEnd(16);
4524
5650
  let statusStr;
4525
5651
  if (tier.tier === 1) {
4526
- statusStr = import_chalk5.default.gray(tier.note ?? "");
5652
+ statusStr = import_chalk6.default.gray(tier.note ?? "");
4527
5653
  } else if (tier.status === "active") {
4528
- const loc = tier.path ? import_chalk5.default.gray(tier.path) : "";
4529
- const note = tier.note ? import_chalk5.default.gray(`(${tier.note})`) : "";
4530
- statusStr = import_chalk5.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
5654
+ const loc = tier.path ? import_chalk6.default.gray(tier.path) : "";
5655
+ const note = tier.note ? import_chalk6.default.gray(`(${tier.note})`) : "";
5656
+ statusStr = import_chalk6.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
4531
5657
  } else {
4532
- statusStr = import_chalk5.default.gray("\u25CB " + (tier.note ?? "not found"));
5658
+ statusStr = import_chalk6.default.gray("\u25CB " + (tier.note ?? "not found"));
4533
5659
  }
4534
- console.log(`${num} ${import_chalk5.default.white(label)} ${statusStr}`);
5660
+ console.log(`${num} ${import_chalk6.default.white(label)} ${statusStr}`);
4535
5661
  }
4536
5662
  console.log("");
4537
- console.log(import_chalk5.default.bold("Policy Evaluation:"));
5663
+ console.log(import_chalk6.default.bold("Policy Evaluation:"));
4538
5664
  for (const step of result.steps) {
4539
5665
  const isFinal = step.isFinal;
4540
5666
  let icon;
4541
- if (step.outcome === "allow") icon = import_chalk5.default.green(" \u2705");
4542
- else if (step.outcome === "review") icon = import_chalk5.default.red(" \u{1F534}");
4543
- else if (step.outcome === "skip") icon = import_chalk5.default.gray(" \u2500 ");
4544
- else icon = import_chalk5.default.gray(" \u25CB ");
5667
+ if (step.outcome === "allow") icon = import_chalk6.default.green(" \u2705");
5668
+ else if (step.outcome === "review") icon = import_chalk6.default.red(" \u{1F534}");
5669
+ else if (step.outcome === "skip") icon = import_chalk6.default.gray(" \u2500 ");
5670
+ else icon = import_chalk6.default.gray(" \u25CB ");
4545
5671
  const name = step.name.padEnd(18);
4546
- const nameStr = isFinal ? import_chalk5.default.white.bold(name) : import_chalk5.default.white(name);
4547
- const detail = isFinal ? import_chalk5.default.white(step.detail) : import_chalk5.default.gray(step.detail);
4548
- const arrow = isFinal ? import_chalk5.default.yellow(" \u2190 STOP") : "";
5672
+ const nameStr = isFinal ? import_chalk6.default.white.bold(name) : import_chalk6.default.white(name);
5673
+ const detail = isFinal ? import_chalk6.default.white(step.detail) : import_chalk6.default.gray(step.detail);
5674
+ const arrow = isFinal ? import_chalk6.default.yellow(" \u2190 STOP") : "";
4549
5675
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
4550
5676
  }
4551
5677
  console.log("");
4552
5678
  if (result.decision === "allow") {
4553
- console.log(import_chalk5.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk5.default.gray(" \u2014 no approval needed"));
5679
+ console.log(import_chalk6.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk6.default.gray(" \u2014 no approval needed"));
4554
5680
  } else {
4555
5681
  console.log(
4556
- import_chalk5.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk5.default.gray(" \u2014 human approval required")
5682
+ import_chalk6.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk6.default.gray(" \u2014 human approval required")
4557
5683
  );
4558
5684
  if (result.blockedByLabel) {
4559
- console.log(import_chalk5.default.gray(` Reason: ${result.blockedByLabel}`));
5685
+ console.log(import_chalk6.default.gray(` Reason: ${result.blockedByLabel}`));
4560
5686
  }
4561
5687
  }
4562
5688
  console.log("");
4563
5689
  });
4564
5690
  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) => {
4565
- const configPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
4566
- if (import_fs5.default.existsSync(configPath) && !options.force) {
4567
- console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
4568
- console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
5691
+ const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
5692
+ if (import_fs7.default.existsSync(configPath) && !options.force) {
5693
+ console.log(import_chalk6.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
5694
+ console.log(import_chalk6.default.gray(` Run with --force to overwrite.`));
4569
5695
  return;
4570
5696
  }
4571
5697
  const requestedMode = options.mode.toLowerCase();
@@ -4577,13 +5703,13 @@ program.command("init").description("Create ~/.node9/config.json with default po
4577
5703
  mode: safeMode
4578
5704
  }
4579
5705
  };
4580
- const dir = import_path7.default.dirname(configPath);
4581
- if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
4582
- import_fs5.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
4583
- console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
4584
- console.log(import_chalk5.default.cyan(` Mode set to: ${safeMode}`));
5706
+ const dir = import_path9.default.dirname(configPath);
5707
+ if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
5708
+ import_fs7.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
5709
+ console.log(import_chalk6.default.green(`\u2705 Global config created: ${configPath}`));
5710
+ console.log(import_chalk6.default.cyan(` Mode set to: ${safeMode}`));
4585
5711
  console.log(
4586
- import_chalk5.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
5712
+ import_chalk6.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
4587
5713
  );
4588
5714
  });
4589
5715
  function formatRelativeTime(timestamp) {
@@ -4597,14 +5723,14 @@ function formatRelativeTime(timestamp) {
4597
5723
  return new Date(timestamp).toLocaleDateString();
4598
5724
  }
4599
5725
  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) => {
4600
- const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "audit.log");
4601
- if (!import_fs5.default.existsSync(logPath)) {
5726
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
5727
+ if (!import_fs7.default.existsSync(logPath)) {
4602
5728
  console.log(
4603
- import_chalk5.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
5729
+ import_chalk6.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
4604
5730
  );
4605
5731
  return;
4606
5732
  }
4607
- const raw = import_fs5.default.readFileSync(logPath, "utf-8");
5733
+ const raw = import_fs7.default.readFileSync(logPath, "utf-8");
4608
5734
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
4609
5735
  let entries = lines.flatMap((line) => {
4610
5736
  try {
@@ -4626,31 +5752,31 @@ program.command("audit").description("View local execution audit log").option("-
4626
5752
  return;
4627
5753
  }
4628
5754
  if (entries.length === 0) {
4629
- console.log(import_chalk5.default.yellow("No matching audit entries."));
5755
+ console.log(import_chalk6.default.yellow("No matching audit entries."));
4630
5756
  return;
4631
5757
  }
4632
5758
  console.log(
4633
5759
  `
4634
- ${import_chalk5.default.bold("Node9 Audit Log")} ${import_chalk5.default.dim(`(${entries.length} entries)`)}`
5760
+ ${import_chalk6.default.bold("Node9 Audit Log")} ${import_chalk6.default.dim(`(${entries.length} entries)`)}`
4635
5761
  );
4636
- console.log(import_chalk5.default.dim(" " + "\u2500".repeat(65)));
5762
+ console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
4637
5763
  console.log(
4638
5764
  ` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
4639
5765
  );
4640
- console.log(import_chalk5.default.dim(" " + "\u2500".repeat(65)));
5766
+ console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
4641
5767
  for (const e of entries) {
4642
5768
  const time = formatRelativeTime(String(e.ts)).padEnd(12);
4643
5769
  const tool = String(e.tool).slice(0, 17).padEnd(18);
4644
- const result = e.decision === "allow" ? import_chalk5.default.green("ALLOW".padEnd(10)) : import_chalk5.default.red("DENY".padEnd(10));
5770
+ const result = e.decision === "allow" ? import_chalk6.default.green("ALLOW".padEnd(10)) : import_chalk6.default.red("DENY".padEnd(10));
4645
5771
  const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
4646
5772
  const agent = String(e.agent || "unknown");
4647
5773
  console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
4648
5774
  }
4649
5775
  const allowed = entries.filter((e) => e.decision === "allow").length;
4650
5776
  const denied = entries.filter((e) => e.decision === "deny").length;
4651
- console.log(import_chalk5.default.dim(" " + "\u2500".repeat(65)));
5777
+ console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
4652
5778
  console.log(
4653
- ` ${entries.length} entries | ${import_chalk5.default.green(allowed + " allowed")} | ${import_chalk5.default.red(denied + " denied")}
5779
+ ` ${entries.length} entries | ${import_chalk6.default.green(allowed + " allowed")} | ${import_chalk6.default.red(denied + " denied")}
4654
5780
  `
4655
5781
  );
4656
5782
  });
@@ -4661,43 +5787,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
4661
5787
  const settings = mergedConfig.settings;
4662
5788
  console.log("");
4663
5789
  if (creds && settings.approvers.cloud) {
4664
- console.log(import_chalk5.default.green(" \u25CF Agent mode") + import_chalk5.default.gray(" \u2014 cloud team policy enforced"));
5790
+ console.log(import_chalk6.default.green(" \u25CF Agent mode") + import_chalk6.default.gray(" \u2014 cloud team policy enforced"));
4665
5791
  } else if (creds && !settings.approvers.cloud) {
4666
5792
  console.log(
4667
- import_chalk5.default.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + import_chalk5.default.gray(" \u2014 all decisions stay on this machine")
5793
+ import_chalk6.default.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + import_chalk6.default.gray(" \u2014 all decisions stay on this machine")
4668
5794
  );
4669
5795
  } else {
4670
5796
  console.log(
4671
- import_chalk5.default.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + import_chalk5.default.gray(" \u2014 no API key (Local rules only)")
5797
+ import_chalk6.default.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + import_chalk6.default.gray(" \u2014 no API key (Local rules only)")
4672
5798
  );
4673
5799
  }
4674
5800
  console.log("");
4675
5801
  if (daemonRunning) {
4676
5802
  console.log(
4677
- import_chalk5.default.green(" \u25CF Daemon running") + import_chalk5.default.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
5803
+ import_chalk6.default.green(" \u25CF Daemon running") + import_chalk6.default.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
4678
5804
  );
4679
5805
  } else {
4680
- console.log(import_chalk5.default.gray(" \u25CB Daemon stopped"));
5806
+ console.log(import_chalk6.default.gray(" \u25CB Daemon stopped"));
4681
5807
  }
4682
5808
  if (settings.enableUndo) {
4683
5809
  console.log(
4684
- import_chalk5.default.magenta(" \u25CF Undo Engine") + import_chalk5.default.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
5810
+ import_chalk6.default.magenta(" \u25CF Undo Engine") + import_chalk6.default.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
4685
5811
  );
4686
5812
  }
4687
5813
  console.log("");
4688
- const modeLabel = settings.mode === "audit" ? import_chalk5.default.blue("audit") : settings.mode === "strict" ? import_chalk5.default.red("strict") : import_chalk5.default.white("standard");
5814
+ const modeLabel = settings.mode === "audit" ? import_chalk6.default.blue("audit") : settings.mode === "strict" ? import_chalk6.default.red("strict") : import_chalk6.default.white("standard");
4689
5815
  console.log(` Mode: ${modeLabel}`);
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");
5816
+ const projectConfig = import_path9.default.join(process.cwd(), "node9.config.json");
5817
+ const globalConfig = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
4692
5818
  console.log(
4693
- ` Local: ${import_fs5.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
5819
+ ` Local: ${import_fs7.default.existsSync(projectConfig) ? import_chalk6.default.green("Active (node9.config.json)") : import_chalk6.default.gray("Not present")}`
4694
5820
  );
4695
5821
  console.log(
4696
- ` Global: ${import_fs5.default.existsSync(globalConfig) ? import_chalk5.default.green("Active (~/.node9/config.json)") : import_chalk5.default.gray("Not present")}`
5822
+ ` Global: ${import_fs7.default.existsSync(globalConfig) ? import_chalk6.default.green("Active (~/.node9/config.json)") : import_chalk6.default.gray("Not present")}`
4697
5823
  );
4698
5824
  if (mergedConfig.policy.sandboxPaths.length > 0) {
4699
5825
  console.log(
4700
- ` Sandbox: ${import_chalk5.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
5826
+ ` Sandbox: ${import_chalk6.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
4701
5827
  );
4702
5828
  }
4703
5829
  const pauseState = checkPause();
@@ -4705,47 +5831,63 @@ program.command("status").description("Show current Node9 mode, policy source, a
4705
5831
  const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
4706
5832
  console.log("");
4707
5833
  console.log(
4708
- import_chalk5.default.yellow(` \u23F8 PAUSED until ${expiresAt}`) + import_chalk5.default.gray(" \u2014 all tool calls allowed")
5834
+ import_chalk6.default.yellow(` \u23F8 PAUSED until ${expiresAt}`) + import_chalk6.default.gray(" \u2014 all tool calls allowed")
4709
5835
  );
4710
5836
  }
4711
5837
  console.log("");
4712
5838
  });
4713
- program.command("daemon").description("Run the local approval server").argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").action(
5839
+ program.command("daemon").description("Run the local approval server").argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").option(
5840
+ "-w, --watch",
5841
+ "Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
5842
+ ).action(
4714
5843
  async (action, options) => {
4715
5844
  const cmd = (action ?? "start").toLowerCase();
4716
5845
  if (cmd === "stop") return stopDaemon();
4717
5846
  if (cmd === "status") return daemonStatus();
4718
5847
  if (cmd !== "start" && action !== void 0) {
4719
- console.error(import_chalk5.default.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
5848
+ console.error(import_chalk6.default.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
4720
5849
  process.exit(1);
4721
5850
  }
5851
+ if (options.watch) {
5852
+ process.env.NODE9_WATCH_MODE = "1";
5853
+ setTimeout(() => {
5854
+ openBrowserLocal();
5855
+ console.log(import_chalk6.default.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5856
+ }, 600);
5857
+ startDaemon();
5858
+ return;
5859
+ }
4722
5860
  if (options.openui) {
4723
5861
  if (isDaemonRunning()) {
4724
5862
  openBrowserLocal();
4725
- console.log(import_chalk5.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5863
+ console.log(import_chalk6.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
4726
5864
  process.exit(0);
4727
5865
  }
4728
- const child = (0, import_child_process4.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
5866
+ const child = (0, import_child_process5.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
4729
5867
  child.unref();
4730
5868
  for (let i = 0; i < 12; i++) {
4731
5869
  await new Promise((r) => setTimeout(r, 250));
4732
5870
  if (isDaemonRunning()) break;
4733
5871
  }
4734
5872
  openBrowserLocal();
4735
- console.log(import_chalk5.default.green(`
5873
+ console.log(import_chalk6.default.green(`
4736
5874
  \u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
4737
5875
  process.exit(0);
4738
5876
  }
4739
5877
  if (options.background) {
4740
- const child = (0, import_child_process4.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
5878
+ const child = (0, import_child_process5.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
4741
5879
  child.unref();
4742
- console.log(import_chalk5.default.green(`
5880
+ console.log(import_chalk6.default.green(`
4743
5881
  \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
4744
5882
  process.exit(0);
4745
5883
  }
4746
5884
  startDaemon();
4747
5885
  }
4748
5886
  );
5887
+ program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).action(async (options) => {
5888
+ const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
5889
+ await startTail2(options);
5890
+ });
4749
5891
  program.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
4750
5892
  const processPayload = async (raw) => {
4751
5893
  try {
@@ -4756,9 +5898,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
4756
5898
  } catch (err) {
4757
5899
  const tempConfig = getConfig();
4758
5900
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
4759
- const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
5901
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
4760
5902
  const errMsg = err instanceof Error ? err.message : String(err);
4761
- import_fs5.default.appendFileSync(
5903
+ import_fs7.default.appendFileSync(
4762
5904
  logPath,
4763
5905
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
4764
5906
  RAW: ${raw}
@@ -4776,10 +5918,10 @@ RAW: ${raw}
4776
5918
  }
4777
5919
  const config = getConfig();
4778
5920
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
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 });
4782
- import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
5921
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
5922
+ if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
5923
+ import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
5924
+ import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
4783
5925
  `);
4784
5926
  }
4785
5927
  const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
@@ -4790,13 +5932,19 @@ RAW: ${raw}
4790
5932
  const sendBlock = (msg, result2) => {
4791
5933
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
4792
5934
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
4793
- console.error(import_chalk5.default.red(`
5935
+ if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
5936
+ console.error(import_chalk6.default.bgRed.white.bold(`
5937
+ \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
5938
+ console.error(import_chalk6.default.red.bold(` A sensitive secret was found in the tool arguments!`));
5939
+ } else {
5940
+ console.error(import_chalk6.default.red(`
4794
5941
  \u{1F6D1} Node9 blocked "${toolName}"`));
4795
- console.error(import_chalk5.default.gray(` Triggered by: ${blockedByContext}`));
4796
- if (result2?.changeHint) console.error(import_chalk5.default.cyan(` To change: ${result2.changeHint}`));
5942
+ }
5943
+ console.error(import_chalk6.default.gray(` Triggered by: ${blockedByContext}`));
5944
+ if (result2?.changeHint) console.error(import_chalk6.default.cyan(` To change: ${result2.changeHint}`));
4797
5945
  console.error("");
4798
5946
  const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
4799
- console.error(import_chalk5.default.dim(` (Detailed instructions sent to AI agent)`));
5947
+ console.error(import_chalk6.default.dim(` (Detailed instructions sent to AI agent)`));
4800
5948
  process.stdout.write(
4801
5949
  JSON.stringify({
4802
5950
  decision: "block",
@@ -4827,7 +5975,7 @@ RAW: ${raw}
4827
5975
  process.exit(0);
4828
5976
  }
4829
5977
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
4830
- console.error(import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
5978
+ console.error(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
4831
5979
  const daemonReady = await autoStartDaemonAndWait();
4832
5980
  if (daemonReady) {
4833
5981
  const retry = await authorizeHeadless(toolName, toolInput, false, meta);
@@ -4850,9 +5998,9 @@ RAW: ${raw}
4850
5998
  });
4851
5999
  } catch (err) {
4852
6000
  if (process.env.NODE9_DEBUG === "1") {
4853
- const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
6001
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
4854
6002
  const errMsg = err instanceof Error ? err.message : String(err);
4855
- import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
6003
+ import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
4856
6004
  `);
4857
6005
  }
4858
6006
  process.exit(0);
@@ -4897,10 +6045,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
4897
6045
  decision: "allowed",
4898
6046
  source: "post-hook"
4899
6047
  };
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 });
4903
- import_fs5.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
6048
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
6049
+ if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
6050
+ import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
6051
+ import_fs7.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
4904
6052
  const config = getConfig();
4905
6053
  if (shouldSnapshot(tool, {}, config)) {
4906
6054
  await createShadowSnapshot();
@@ -4927,7 +6075,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
4927
6075
  const ms = parseDuration(options.duration);
4928
6076
  if (ms === null) {
4929
6077
  console.error(
4930
- import_chalk5.default.red(`
6078
+ import_chalk6.default.red(`
4931
6079
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
4932
6080
  `)
4933
6081
  );
@@ -4935,20 +6083,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
4935
6083
  }
4936
6084
  pauseNode9(ms, options.duration);
4937
6085
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
4938
- console.log(import_chalk5.default.yellow(`
6086
+ console.log(import_chalk6.default.yellow(`
4939
6087
  \u23F8 Node9 paused until ${expiresAt}`));
4940
- console.log(import_chalk5.default.gray(` All tool calls will be allowed without review.`));
4941
- console.log(import_chalk5.default.gray(` Run "node9 resume" to re-enable early.
6088
+ console.log(import_chalk6.default.gray(` All tool calls will be allowed without review.`));
6089
+ console.log(import_chalk6.default.gray(` Run "node9 resume" to re-enable early.
4942
6090
  `));
4943
6091
  });
4944
6092
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
4945
6093
  const { paused } = checkPause();
4946
6094
  if (!paused) {
4947
- console.log(import_chalk5.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
6095
+ console.log(import_chalk6.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
4948
6096
  return;
4949
6097
  }
4950
6098
  resumeNode9();
4951
- console.log(import_chalk5.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
6099
+ console.log(import_chalk6.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
4952
6100
  });
4953
6101
  var HOOK_BASED_AGENTS = {
4954
6102
  claude: "claude",
@@ -4961,15 +6109,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
4961
6109
  if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
4962
6110
  const target = HOOK_BASED_AGENTS[firstArg];
4963
6111
  console.error(
4964
- import_chalk5.default.yellow(`
6112
+ import_chalk6.default.yellow(`
4965
6113
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
4966
6114
  );
4967
- console.error(import_chalk5.default.white(`
6115
+ console.error(import_chalk6.default.white(`
4968
6116
  "${target}" uses its own hook system. Use:`));
4969
6117
  console.error(
4970
- import_chalk5.default.green(` node9 addto ${target} `) + import_chalk5.default.gray("# one-time setup")
6118
+ import_chalk6.default.green(` node9 addto ${target} `) + import_chalk6.default.gray("# one-time setup")
4971
6119
  );
4972
- console.error(import_chalk5.default.green(` ${target} `) + import_chalk5.default.gray("# run normally"));
6120
+ console.error(import_chalk6.default.green(` ${target} `) + import_chalk6.default.gray("# run normally"));
4973
6121
  process.exit(1);
4974
6122
  }
4975
6123
  const fullCommand = commandArgs.join(" ");
@@ -4977,7 +6125,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
4977
6125
  agent: "Terminal"
4978
6126
  });
4979
6127
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
4980
- console.error(import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
6128
+ console.error(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
4981
6129
  const daemonReady = await autoStartDaemonAndWait();
4982
6130
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
4983
6131
  }
@@ -4986,12 +6134,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
4986
6134
  }
4987
6135
  if (!result.approved) {
4988
6136
  console.error(
4989
- import_chalk5.default.red(`
6137
+ import_chalk6.default.red(`
4990
6138
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
4991
6139
  );
4992
6140
  process.exit(1);
4993
6141
  }
4994
- console.error(import_chalk5.default.green("\n\u2705 Approved \u2014 running command...\n"));
6142
+ console.error(import_chalk6.default.green("\n\u2705 Approved \u2014 running command...\n"));
4995
6143
  await runProxy(fullCommand);
4996
6144
  } else {
4997
6145
  program.help();
@@ -5006,22 +6154,22 @@ program.command("undo").description(
5006
6154
  if (history.length === 0) {
5007
6155
  if (!options.all && allHistory.length > 0) {
5008
6156
  console.log(
5009
- import_chalk5.default.yellow(
6157
+ import_chalk6.default.yellow(
5010
6158
  `
5011
6159
  \u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
5012
- Run ${import_chalk5.default.cyan("node9 undo --all")} to see snapshots from all projects.
6160
+ Run ${import_chalk6.default.cyan("node9 undo --all")} to see snapshots from all projects.
5013
6161
  `
5014
6162
  )
5015
6163
  );
5016
6164
  } else {
5017
- console.log(import_chalk5.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
6165
+ console.log(import_chalk6.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
5018
6166
  }
5019
6167
  return;
5020
6168
  }
5021
6169
  const idx = history.length - steps;
5022
6170
  if (idx < 0) {
5023
6171
  console.log(
5024
- import_chalk5.default.yellow(
6172
+ import_chalk6.default.yellow(
5025
6173
  `
5026
6174
  \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
5027
6175
  `
@@ -5032,18 +6180,18 @@ program.command("undo").description(
5032
6180
  const snapshot = history[idx];
5033
6181
  const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
5034
6182
  const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
5035
- console.log(import_chalk5.default.magenta.bold(`
6183
+ console.log(import_chalk6.default.magenta.bold(`
5036
6184
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
5037
6185
  console.log(
5038
- import_chalk5.default.white(
5039
- ` Tool: ${import_chalk5.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk5.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
6186
+ import_chalk6.default.white(
6187
+ ` Tool: ${import_chalk6.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk6.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
5040
6188
  )
5041
6189
  );
5042
- console.log(import_chalk5.default.white(` When: ${import_chalk5.default.gray(ageStr)}`));
5043
- console.log(import_chalk5.default.white(` Dir: ${import_chalk5.default.gray(snapshot.cwd)}`));
6190
+ console.log(import_chalk6.default.white(` When: ${import_chalk6.default.gray(ageStr)}`));
6191
+ console.log(import_chalk6.default.white(` Dir: ${import_chalk6.default.gray(snapshot.cwd)}`));
5044
6192
  if (steps > 1)
5045
6193
  console.log(
5046
- import_chalk5.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
6194
+ import_chalk6.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
5047
6195
  );
5048
6196
  console.log("");
5049
6197
  const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
@@ -5051,21 +6199,21 @@ program.command("undo").description(
5051
6199
  const lines = diff.split("\n");
5052
6200
  for (const line of lines) {
5053
6201
  if (line.startsWith("+++") || line.startsWith("---")) {
5054
- console.log(import_chalk5.default.bold(line));
6202
+ console.log(import_chalk6.default.bold(line));
5055
6203
  } else if (line.startsWith("+")) {
5056
- console.log(import_chalk5.default.green(line));
6204
+ console.log(import_chalk6.default.green(line));
5057
6205
  } else if (line.startsWith("-")) {
5058
- console.log(import_chalk5.default.red(line));
6206
+ console.log(import_chalk6.default.red(line));
5059
6207
  } else if (line.startsWith("@@")) {
5060
- console.log(import_chalk5.default.cyan(line));
6208
+ console.log(import_chalk6.default.cyan(line));
5061
6209
  } else {
5062
- console.log(import_chalk5.default.gray(line));
6210
+ console.log(import_chalk6.default.gray(line));
5063
6211
  }
5064
6212
  }
5065
6213
  console.log("");
5066
6214
  } else {
5067
6215
  console.log(
5068
- import_chalk5.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
6216
+ import_chalk6.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
5069
6217
  );
5070
6218
  }
5071
6219
  const proceed = await (0, import_prompts3.confirm)({
@@ -5074,21 +6222,111 @@ program.command("undo").description(
5074
6222
  });
5075
6223
  if (proceed) {
5076
6224
  if (applyUndo(snapshot.hash, snapshot.cwd)) {
5077
- console.log(import_chalk5.default.green("\n\u2705 Reverted successfully.\n"));
6225
+ console.log(import_chalk6.default.green("\n\u2705 Reverted successfully.\n"));
5078
6226
  } else {
5079
- console.error(import_chalk5.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
6227
+ console.error(import_chalk6.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
5080
6228
  }
5081
6229
  } else {
5082
- console.log(import_chalk5.default.gray("\nCancelled.\n"));
6230
+ console.log(import_chalk6.default.gray("\nCancelled.\n"));
6231
+ }
6232
+ });
6233
+ var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
6234
+ shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
6235
+ const name = resolveShieldName(service);
6236
+ if (!name) {
6237
+ console.error(import_chalk6.default.red(`
6238
+ \u274C Unknown shield: "${service}"
6239
+ `));
6240
+ console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
6241
+ `);
6242
+ process.exit(1);
6243
+ }
6244
+ const shield = getShield(name);
6245
+ const active = readActiveShields();
6246
+ if (active.includes(name)) {
6247
+ console.log(import_chalk6.default.yellow(`
6248
+ \u2139\uFE0F Shield "${name}" is already active.
6249
+ `));
6250
+ return;
6251
+ }
6252
+ writeActiveShields([...active, name]);
6253
+ console.log(import_chalk6.default.green(`
6254
+ \u{1F6E1}\uFE0F Shield "${name}" enabled.`));
6255
+ console.log(import_chalk6.default.gray(` ${shield.smartRules.length} smart rules now active.`));
6256
+ if (shield.dangerousWords.length > 0)
6257
+ console.log(import_chalk6.default.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
6258
+ if (name === "filesystem") {
6259
+ console.log(
6260
+ import_chalk6.default.yellow(
6261
+ `
6262
+ \u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
6263
+ Tools like unlink, find -delete, or language-level file ops are not intercepted.`
6264
+ )
6265
+ );
6266
+ }
6267
+ console.log("");
6268
+ });
6269
+ shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
6270
+ const name = resolveShieldName(service);
6271
+ if (!name) {
6272
+ console.error(import_chalk6.default.red(`
6273
+ \u274C Unknown shield: "${service}"
6274
+ `));
6275
+ console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
6276
+ `);
6277
+ process.exit(1);
5083
6278
  }
6279
+ const active = readActiveShields();
6280
+ if (!active.includes(name)) {
6281
+ console.log(import_chalk6.default.yellow(`
6282
+ \u2139\uFE0F Shield "${name}" is not active.
6283
+ `));
6284
+ return;
6285
+ }
6286
+ writeActiveShields(active.filter((s) => s !== name));
6287
+ console.log(import_chalk6.default.green(`
6288
+ \u{1F6E1}\uFE0F Shield "${name}" disabled.
6289
+ `));
6290
+ });
6291
+ shieldCmd.command("list").description("Show all available shields").action(() => {
6292
+ const active = new Set(readActiveShields());
6293
+ console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
6294
+ for (const shield of listShields()) {
6295
+ const status = active.has(shield.name) ? import_chalk6.default.green("\u25CF enabled") : import_chalk6.default.gray("\u25CB disabled");
6296
+ console.log(` ${status} ${import_chalk6.default.cyan(shield.name.padEnd(12))} ${shield.description}`);
6297
+ if (shield.aliases.length > 0)
6298
+ console.log(import_chalk6.default.gray(` aliases: ${shield.aliases.join(", ")}`));
6299
+ }
6300
+ console.log("");
6301
+ });
6302
+ shieldCmd.command("status").description("Show which shields are currently active").action(() => {
6303
+ const active = readActiveShields();
6304
+ if (active.length === 0) {
6305
+ console.log(import_chalk6.default.yellow("\n\u2139\uFE0F No shields are active.\n"));
6306
+ console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
6307
+ `);
6308
+ return;
6309
+ }
6310
+ console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
6311
+ for (const name of active) {
6312
+ const shield = getShield(name);
6313
+ if (!shield) continue;
6314
+ console.log(` ${import_chalk6.default.green("\u25CF")} ${import_chalk6.default.cyan(name)}`);
6315
+ console.log(
6316
+ import_chalk6.default.gray(
6317
+ ` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
6318
+ )
6319
+ );
6320
+ }
6321
+ console.log("");
5084
6322
  });
5085
6323
  process.on("unhandledRejection", (reason) => {
5086
6324
  const isCheckHook = process.argv[2] === "check";
5087
6325
  if (isCheckHook) {
5088
6326
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
5089
- const logPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
6327
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
5090
6328
  const msg = reason instanceof Error ? reason.message : String(reason);
5091
- import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
6329
+ import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
5092
6330
  `);
5093
6331
  }
5094
6332
  process.exit(0);