@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.mjs CHANGED
@@ -1,21 +1,13 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/cli.ts
4
- import { Command } from "commander";
5
-
6
- // src/core.ts
7
- import chalk2 from "chalk";
8
- import { confirm } from "@inquirer/prompts";
9
- import fs from "fs";
10
- import path3 from "path";
11
- import os from "os";
12
- import pm from "picomatch";
13
- import { parse } from "sh-syntax";
14
-
15
- // src/ui/native.ts
16
- import { spawn } from "child_process";
17
- import path2 from "path";
18
- import chalk from "chalk";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
19
11
 
20
12
  // src/context-sniper.ts
21
13
  import path from "path";
@@ -48,22 +40,6 @@ function extractContext(text, matchedWord) {
48
40
  ... [${lines.length - end} lines hidden] ...` : "";
49
41
  return { snippet: `${head}${snippet}${tail}`, lineIndex };
50
42
  }
51
- var CODE_KEYS = [
52
- "command",
53
- "cmd",
54
- "shell_command",
55
- "bash_command",
56
- "script",
57
- "code",
58
- "input",
59
- "sql",
60
- "query",
61
- "arguments",
62
- "args",
63
- "param",
64
- "params",
65
- "text"
66
- ];
67
43
  function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
68
44
  let intent = "EXEC";
69
45
  let contextSnippet;
@@ -118,11 +94,33 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
118
94
  ...ruleName && { ruleName }
119
95
  };
120
96
  }
97
+ var CODE_KEYS;
98
+ var init_context_sniper = __esm({
99
+ "src/context-sniper.ts"() {
100
+ "use strict";
101
+ CODE_KEYS = [
102
+ "command",
103
+ "cmd",
104
+ "shell_command",
105
+ "bash_command",
106
+ "script",
107
+ "code",
108
+ "input",
109
+ "sql",
110
+ "query",
111
+ "arguments",
112
+ "args",
113
+ "param",
114
+ "params",
115
+ "text"
116
+ ];
117
+ }
118
+ });
121
119
 
122
120
  // src/ui/native.ts
123
- var isTestEnv = () => {
124
- 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";
125
- };
121
+ import { spawn } from "child_process";
122
+ import path2 from "path";
123
+ import chalk from "chalk";
126
124
  function formatArgs(args, matchedField, matchedWord) {
127
125
  if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
128
126
  let parsed = args;
@@ -332,82 +330,19 @@ end run`;
332
330
  }
333
331
  });
334
332
  }
333
+ var isTestEnv;
334
+ var init_native = __esm({
335
+ "src/ui/native.ts"() {
336
+ "use strict";
337
+ init_context_sniper();
338
+ isTestEnv = () => {
339
+ 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";
340
+ };
341
+ }
342
+ });
335
343
 
336
344
  // src/config-schema.ts
337
345
  import { z } from "zod";
338
- var noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
339
- message: "Value must not contain literal newline characters (use \\n instead)"
340
- });
341
- var validRegex = noNewlines.refine(
342
- (s) => {
343
- try {
344
- new RegExp(s);
345
- return true;
346
- } catch {
347
- return false;
348
- }
349
- },
350
- { message: "Value must be a valid regular expression" }
351
- );
352
- var SmartConditionSchema = z.object({
353
- field: z.string().min(1, "Condition field must not be empty"),
354
- op: z.enum(["matches", "notMatches", "contains", "notContains", "exists", "notExists"], {
355
- errorMap: () => ({
356
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists"
357
- })
358
- }),
359
- value: validRegex.optional(),
360
- flags: z.string().optional()
361
- });
362
- var SmartRuleSchema = z.object({
363
- name: z.string().optional(),
364
- tool: z.string().min(1, "Smart rule tool must not be empty"),
365
- conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
366
- conditionMode: z.enum(["all", "any"]).optional(),
367
- verdict: z.enum(["allow", "review", "block"], {
368
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
369
- }),
370
- reason: z.string().optional()
371
- });
372
- var PolicyRuleSchema = z.object({
373
- action: z.string().min(1),
374
- allowPaths: z.array(z.string()).optional(),
375
- blockPaths: z.array(z.string()).optional()
376
- });
377
- var ConfigFileSchema = z.object({
378
- version: z.string().optional(),
379
- settings: z.object({
380
- mode: z.enum(["standard", "strict", "audit"]).optional(),
381
- autoStartDaemon: z.boolean().optional(),
382
- enableUndo: z.boolean().optional(),
383
- enableHookLogDebug: z.boolean().optional(),
384
- approvalTimeoutMs: z.number().nonnegative().optional(),
385
- approvers: z.object({
386
- native: z.boolean().optional(),
387
- browser: z.boolean().optional(),
388
- cloud: z.boolean().optional(),
389
- terminal: z.boolean().optional()
390
- }).optional(),
391
- environment: z.string().optional(),
392
- slackEnabled: z.boolean().optional(),
393
- enableTrustSessions: z.boolean().optional(),
394
- allowGlobalPause: z.boolean().optional()
395
- }).optional(),
396
- policy: z.object({
397
- sandboxPaths: z.array(z.string()).optional(),
398
- dangerousWords: z.array(noNewlines).optional(),
399
- ignoredTools: z.array(z.string()).optional(),
400
- toolInspection: z.record(z.string()).optional(),
401
- rules: z.array(PolicyRuleSchema).optional(),
402
- smartRules: z.array(SmartRuleSchema).optional(),
403
- snapshot: z.object({
404
- tools: z.array(z.string()).optional(),
405
- onlyPaths: z.array(z.string()).optional(),
406
- ignorePaths: z.array(z.string()).optional()
407
- }).optional()
408
- }).optional(),
409
- environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
410
- }).strict({ message: "Config contains unknown top-level keys" });
411
346
  function sanitizeConfig(raw) {
412
347
  const result = ConfigFileSchema.safeParse(raw);
413
348
  if (result.success) {
@@ -425,8 +360,8 @@ function sanitizeConfig(raw) {
425
360
  }
426
361
  }
427
362
  const lines = result.error.issues.map((issue) => {
428
- const path8 = issue.path.length > 0 ? issue.path.join(".") : "root";
429
- return ` \u2022 ${path8}: ${issue.message}`;
363
+ const path10 = issue.path.length > 0 ? issue.path.join(".") : "root";
364
+ return ` \u2022 ${path10}: ${issue.message}`;
430
365
  });
431
366
  return {
432
367
  sanitized,
@@ -434,19 +369,397 @@ function sanitizeConfig(raw) {
434
369
  ${lines.join("\n")}`
435
370
  };
436
371
  }
372
+ var noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
373
+ var init_config_schema = __esm({
374
+ "src/config-schema.ts"() {
375
+ "use strict";
376
+ noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
377
+ message: "Value must not contain literal newline characters (use \\n instead)"
378
+ });
379
+ SmartConditionSchema = z.object({
380
+ field: z.string().min(1, "Condition field must not be empty"),
381
+ op: z.enum(
382
+ [
383
+ "matches",
384
+ "notMatches",
385
+ "contains",
386
+ "notContains",
387
+ "exists",
388
+ "notExists",
389
+ "matchesGlob",
390
+ "notMatchesGlob"
391
+ ],
392
+ {
393
+ errorMap: () => ({
394
+ message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
395
+ })
396
+ }
397
+ ),
398
+ value: z.string().optional(),
399
+ flags: z.string().optional()
400
+ });
401
+ SmartRuleSchema = z.object({
402
+ name: z.string().optional(),
403
+ tool: z.string().min(1, "Smart rule tool must not be empty"),
404
+ conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
405
+ conditionMode: z.enum(["all", "any"]).optional(),
406
+ verdict: z.enum(["allow", "review", "block"], {
407
+ errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
408
+ }),
409
+ reason: z.string().optional()
410
+ });
411
+ ConfigFileSchema = z.object({
412
+ version: z.string().optional(),
413
+ settings: z.object({
414
+ mode: z.enum(["standard", "strict", "audit"]).optional(),
415
+ autoStartDaemon: z.boolean().optional(),
416
+ enableUndo: z.boolean().optional(),
417
+ enableHookLogDebug: z.boolean().optional(),
418
+ approvalTimeoutMs: z.number().nonnegative().optional(),
419
+ approvers: z.object({
420
+ native: z.boolean().optional(),
421
+ browser: z.boolean().optional(),
422
+ cloud: z.boolean().optional(),
423
+ terminal: z.boolean().optional()
424
+ }).optional(),
425
+ environment: z.string().optional(),
426
+ slackEnabled: z.boolean().optional(),
427
+ enableTrustSessions: z.boolean().optional(),
428
+ allowGlobalPause: z.boolean().optional()
429
+ }).optional(),
430
+ policy: z.object({
431
+ sandboxPaths: z.array(z.string()).optional(),
432
+ dangerousWords: z.array(noNewlines).optional(),
433
+ ignoredTools: z.array(z.string()).optional(),
434
+ toolInspection: z.record(z.string()).optional(),
435
+ smartRules: z.array(SmartRuleSchema).optional(),
436
+ snapshot: z.object({
437
+ tools: z.array(z.string()).optional(),
438
+ onlyPaths: z.array(z.string()).optional(),
439
+ ignorePaths: z.array(z.string()).optional()
440
+ }).optional(),
441
+ dlp: z.object({
442
+ enabled: z.boolean().optional(),
443
+ scanIgnoredTools: z.boolean().optional()
444
+ }).optional()
445
+ }).optional(),
446
+ environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
447
+ }).strict({ message: "Config contains unknown top-level keys" });
448
+ }
449
+ });
450
+
451
+ // src/shields.ts
452
+ import fs from "fs";
453
+ import path3 from "path";
454
+ import os from "os";
455
+ import crypto from "crypto";
456
+ function resolveShieldName(input) {
457
+ const lower = input.toLowerCase();
458
+ if (SHIELDS[lower]) return lower;
459
+ for (const [name, def] of Object.entries(SHIELDS)) {
460
+ if (def.aliases.includes(lower)) return name;
461
+ }
462
+ return null;
463
+ }
464
+ function getShield(name) {
465
+ const resolved = resolveShieldName(name);
466
+ return resolved ? SHIELDS[resolved] : null;
467
+ }
468
+ function listShields() {
469
+ return Object.values(SHIELDS);
470
+ }
471
+ function readActiveShields() {
472
+ try {
473
+ const raw = fs.readFileSync(SHIELDS_STATE_FILE, "utf-8");
474
+ if (!raw.trim()) return [];
475
+ const parsed = JSON.parse(raw);
476
+ if (Array.isArray(parsed.active)) {
477
+ return parsed.active.filter(
478
+ (e) => typeof e === "string" && e.length > 0 && e in SHIELDS
479
+ );
480
+ }
481
+ } catch (err) {
482
+ if (err.code !== "ENOENT") {
483
+ process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
484
+ `);
485
+ }
486
+ }
487
+ return [];
488
+ }
489
+ function writeActiveShields(active) {
490
+ fs.mkdirSync(path3.dirname(SHIELDS_STATE_FILE), { recursive: true });
491
+ const tmp = `${SHIELDS_STATE_FILE}.${crypto.randomBytes(6).toString("hex")}.tmp`;
492
+ fs.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
493
+ fs.renameSync(tmp, SHIELDS_STATE_FILE);
494
+ }
495
+ var SHIELDS, SHIELDS_STATE_FILE;
496
+ var init_shields = __esm({
497
+ "src/shields.ts"() {
498
+ "use strict";
499
+ SHIELDS = {
500
+ postgres: {
501
+ name: "postgres",
502
+ description: "Protects PostgreSQL databases from destructive AI operations",
503
+ aliases: ["pg", "postgresql"],
504
+ smartRules: [
505
+ {
506
+ name: "shield:postgres:block-drop-table",
507
+ tool: "*",
508
+ conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
509
+ verdict: "block",
510
+ reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
511
+ },
512
+ {
513
+ name: "shield:postgres:block-truncate",
514
+ tool: "*",
515
+ conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
516
+ verdict: "block",
517
+ reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
518
+ },
519
+ {
520
+ name: "shield:postgres:block-drop-column",
521
+ tool: "*",
522
+ conditions: [
523
+ { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
524
+ ],
525
+ verdict: "block",
526
+ reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
527
+ },
528
+ {
529
+ name: "shield:postgres:review-grant-revoke",
530
+ tool: "*",
531
+ conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
532
+ verdict: "review",
533
+ reason: "Permission changes require human approval (Postgres shield)"
534
+ }
535
+ ],
536
+ dangerousWords: ["dropdb", "pg_dropcluster"]
537
+ },
538
+ github: {
539
+ name: "github",
540
+ description: "Protects GitHub repositories from destructive AI operations",
541
+ aliases: ["git"],
542
+ smartRules: [
543
+ {
544
+ // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
545
+ // This rule adds coverage for `git push --delete` which the built-in does not match.
546
+ name: "shield:github:review-delete-branch-remote",
547
+ tool: "bash",
548
+ conditions: [
549
+ {
550
+ field: "command",
551
+ op: "matches",
552
+ value: "git\\s+push\\s+.*--delete",
553
+ flags: "i"
554
+ }
555
+ ],
556
+ verdict: "review",
557
+ reason: "Remote branch deletion requires human approval (GitHub shield)"
558
+ },
559
+ {
560
+ name: "shield:github:block-delete-repo",
561
+ tool: "*",
562
+ conditions: [
563
+ { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
564
+ ],
565
+ verdict: "block",
566
+ reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
567
+ }
568
+ ],
569
+ dangerousWords: []
570
+ },
571
+ aws: {
572
+ name: "aws",
573
+ description: "Protects AWS infrastructure from destructive AI operations",
574
+ aliases: ["amazon"],
575
+ smartRules: [
576
+ {
577
+ name: "shield:aws:block-delete-s3-bucket",
578
+ tool: "*",
579
+ conditions: [
580
+ {
581
+ field: "command",
582
+ op: "matches",
583
+ value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
584
+ flags: "i"
585
+ }
586
+ ],
587
+ verdict: "block",
588
+ reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
589
+ },
590
+ {
591
+ name: "shield:aws:review-iam-changes",
592
+ tool: "*",
593
+ conditions: [
594
+ {
595
+ field: "command",
596
+ op: "matches",
597
+ value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
598
+ flags: "i"
599
+ }
600
+ ],
601
+ verdict: "review",
602
+ reason: "IAM changes require human approval (AWS shield)"
603
+ },
604
+ {
605
+ name: "shield:aws:block-ec2-terminate",
606
+ tool: "*",
607
+ conditions: [
608
+ {
609
+ field: "command",
610
+ op: "matches",
611
+ value: "aws\\s+ec2\\s+terminate-instances",
612
+ flags: "i"
613
+ }
614
+ ],
615
+ verdict: "block",
616
+ reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
617
+ },
618
+ {
619
+ name: "shield:aws:review-rds-delete",
620
+ tool: "*",
621
+ conditions: [
622
+ { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
623
+ ],
624
+ verdict: "review",
625
+ reason: "RDS deletion requires human approval (AWS shield)"
626
+ }
627
+ ],
628
+ dangerousWords: []
629
+ },
630
+ filesystem: {
631
+ name: "filesystem",
632
+ description: "Protects the local filesystem from dangerous AI operations",
633
+ aliases: ["fs"],
634
+ smartRules: [
635
+ {
636
+ name: "shield:filesystem:review-chmod-777",
637
+ tool: "bash",
638
+ conditions: [
639
+ { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
640
+ ],
641
+ verdict: "review",
642
+ reason: "chmod 777 requires human approval (filesystem shield)"
643
+ },
644
+ {
645
+ name: "shield:filesystem:review-write-etc",
646
+ tool: "bash",
647
+ conditions: [
648
+ {
649
+ field: "command",
650
+ // Narrow to write-indicative operations to avoid approval fatigue on reads.
651
+ // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
652
+ op: "matches",
653
+ value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
654
+ }
655
+ ],
656
+ verdict: "review",
657
+ reason: "Writing to /etc requires human approval (filesystem shield)"
658
+ }
659
+ ],
660
+ // dd removed: too common as a legitimate tool (disk imaging, file ops).
661
+ // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
662
+ // wipefs retained: rarely legitimate in an agent context and not in built-ins.
663
+ dangerousWords: ["wipefs"]
664
+ }
665
+ };
666
+ SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
667
+ }
668
+ });
669
+
670
+ // src/dlp.ts
671
+ function maskSecret(raw, pattern) {
672
+ const match = raw.match(pattern);
673
+ if (!match) return "****";
674
+ const secret = match[0];
675
+ if (secret.length < 8) return "****";
676
+ const prefix = secret.slice(0, 4);
677
+ const suffix = secret.slice(-4);
678
+ const stars = "*".repeat(Math.min(secret.length - 8, 12));
679
+ return `${prefix}${stars}${suffix}`;
680
+ }
681
+ function scanArgs(args, depth = 0, fieldPath = "args") {
682
+ if (depth > MAX_DEPTH || args === null || args === void 0) return null;
683
+ if (Array.isArray(args)) {
684
+ for (let i = 0; i < args.length; i++) {
685
+ const match = scanArgs(args[i], depth + 1, `${fieldPath}[${i}]`);
686
+ if (match) return match;
687
+ }
688
+ return null;
689
+ }
690
+ if (typeof args === "object") {
691
+ for (const [key, value] of Object.entries(args)) {
692
+ const match = scanArgs(value, depth + 1, `${fieldPath}.${key}`);
693
+ if (match) return match;
694
+ }
695
+ return null;
696
+ }
697
+ if (typeof args === "string") {
698
+ const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
699
+ for (const pattern of DLP_PATTERNS) {
700
+ if (pattern.regex.test(text)) {
701
+ return {
702
+ patternName: pattern.name,
703
+ fieldPath,
704
+ redactedSample: maskSecret(text, pattern.regex),
705
+ severity: pattern.severity
706
+ };
707
+ }
708
+ }
709
+ if (text.length < MAX_JSON_PARSE_BYTES) {
710
+ const trimmed = text.trim();
711
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
712
+ try {
713
+ const parsed = JSON.parse(text);
714
+ const inner = scanArgs(parsed, depth + 1, fieldPath);
715
+ if (inner) return inner;
716
+ } catch {
717
+ }
718
+ }
719
+ }
720
+ }
721
+ return null;
722
+ }
723
+ var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
724
+ var init_dlp = __esm({
725
+ "src/dlp.ts"() {
726
+ "use strict";
727
+ DLP_PATTERNS = [
728
+ { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
729
+ { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
730
+ { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
731
+ { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
732
+ { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
733
+ {
734
+ name: "Private Key (PEM)",
735
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
736
+ severity: "block"
737
+ },
738
+ { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
739
+ ];
740
+ MAX_DEPTH = 5;
741
+ MAX_STRING_BYTES = 1e5;
742
+ MAX_JSON_PARSE_BYTES = 1e4;
743
+ }
744
+ });
437
745
 
438
746
  // src/core.ts
439
- var PAUSED_FILE = path3.join(os.homedir(), ".node9", "PAUSED");
440
- var TRUST_FILE = path3.join(os.homedir(), ".node9", "trust.json");
441
- var LOCAL_AUDIT_LOG = path3.join(os.homedir(), ".node9", "audit.log");
442
- var HOOK_DEBUG_LOG = path3.join(os.homedir(), ".node9", "hook-debug.log");
747
+ import chalk2 from "chalk";
748
+ import { confirm } from "@inquirer/prompts";
749
+ import fs2 from "fs";
750
+ import path4 from "path";
751
+ import os2 from "os";
752
+ import net from "net";
753
+ import { randomUUID } from "crypto";
754
+ import pm from "picomatch";
755
+ import { parse } from "sh-syntax";
443
756
  function checkPause() {
444
757
  try {
445
- if (!fs.existsSync(PAUSED_FILE)) return { paused: false };
446
- const state = JSON.parse(fs.readFileSync(PAUSED_FILE, "utf-8"));
758
+ if (!fs2.existsSync(PAUSED_FILE)) return { paused: false };
759
+ const state = JSON.parse(fs2.readFileSync(PAUSED_FILE, "utf-8"));
447
760
  if (state.expiry > 0 && Date.now() >= state.expiry) {
448
761
  try {
449
- fs.unlinkSync(PAUSED_FILE);
762
+ fs2.unlinkSync(PAUSED_FILE);
450
763
  } catch {
451
764
  }
452
765
  return { paused: false };
@@ -457,11 +770,11 @@ function checkPause() {
457
770
  }
458
771
  }
459
772
  function atomicWriteSync(filePath, data, options) {
460
- const dir = path3.dirname(filePath);
461
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
462
- const tmpPath = `${filePath}.${os.hostname()}.${process.pid}.tmp`;
463
- fs.writeFileSync(tmpPath, data, options);
464
- fs.renameSync(tmpPath, filePath);
773
+ const dir = path4.dirname(filePath);
774
+ if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
775
+ const tmpPath = `${filePath}.${os2.hostname()}.${process.pid}.tmp`;
776
+ fs2.writeFileSync(tmpPath, data, options);
777
+ fs2.renameSync(tmpPath, filePath);
465
778
  }
466
779
  function pauseNode9(durationMs, durationStr) {
467
780
  const state = { expiry: Date.now() + durationMs, duration: durationStr };
@@ -469,18 +782,18 @@ function pauseNode9(durationMs, durationStr) {
469
782
  }
470
783
  function resumeNode9() {
471
784
  try {
472
- if (fs.existsSync(PAUSED_FILE)) fs.unlinkSync(PAUSED_FILE);
785
+ if (fs2.existsSync(PAUSED_FILE)) fs2.unlinkSync(PAUSED_FILE);
473
786
  } catch {
474
787
  }
475
788
  }
476
789
  function getActiveTrustSession(toolName) {
477
790
  try {
478
- if (!fs.existsSync(TRUST_FILE)) return false;
479
- const trust = JSON.parse(fs.readFileSync(TRUST_FILE, "utf-8"));
791
+ if (!fs2.existsSync(TRUST_FILE)) return false;
792
+ const trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
480
793
  const now = Date.now();
481
794
  const active = trust.entries.filter((e) => e.expiry > now);
482
795
  if (active.length !== trust.entries.length) {
483
- fs.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
796
+ fs2.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
484
797
  }
485
798
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
486
799
  } catch {
@@ -491,8 +804,8 @@ function writeTrustSession(toolName, durationMs) {
491
804
  try {
492
805
  let trust = { entries: [] };
493
806
  try {
494
- if (fs.existsSync(TRUST_FILE)) {
495
- trust = JSON.parse(fs.readFileSync(TRUST_FILE, "utf-8"));
807
+ if (fs2.existsSync(TRUST_FILE)) {
808
+ trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
496
809
  }
497
810
  } catch {
498
811
  }
@@ -508,9 +821,9 @@ function writeTrustSession(toolName, durationMs) {
508
821
  }
509
822
  function appendToLog(logPath, entry) {
510
823
  try {
511
- const dir = path3.dirname(logPath);
512
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
513
- fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
824
+ const dir = path4.dirname(logPath);
825
+ if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
826
+ fs2.appendFileSync(logPath, JSON.stringify(entry) + "\n");
514
827
  } catch {
515
828
  }
516
829
  }
@@ -522,7 +835,7 @@ function appendHookDebug(toolName, args, meta) {
522
835
  args: safeArgs,
523
836
  agent: meta?.agent,
524
837
  mcpServer: meta?.mcpServer,
525
- hostname: os.hostname(),
838
+ hostname: os2.hostname(),
526
839
  cwd: process.cwd()
527
840
  });
528
841
  }
@@ -536,7 +849,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
536
849
  checkedBy,
537
850
  agent: meta?.agent,
538
851
  mcpServer: meta?.mcpServer,
539
- hostname: os.hostname()
852
+ hostname: os2.hostname()
540
853
  });
541
854
  }
542
855
  function tokenize(toolName) {
@@ -552,9 +865,9 @@ function matchesPattern(text, patterns) {
552
865
  const withoutDotSlash = text.replace(/^\.\//, "");
553
866
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
554
867
  }
555
- function getNestedValue(obj, path8) {
868
+ function getNestedValue(obj, path10) {
556
869
  if (!obj || typeof obj !== "object") return null;
557
- return path8.split(".").reduce((prev, curr) => prev?.[curr], obj);
870
+ return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
558
871
  }
559
872
  function shouldSnapshot(toolName, args, config) {
560
873
  if (!config.settings.enableUndo) return false;
@@ -599,6 +912,10 @@ function evaluateSmartConditions(args, rule) {
599
912
  return true;
600
913
  }
601
914
  }
915
+ case "matchesGlob":
916
+ return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
917
+ case "notMatchesGlob":
918
+ return val !== null && cond.value ? !pm.isMatch(val, cond.value) : true;
602
919
  default:
603
920
  return false;
604
921
  }
@@ -620,7 +937,6 @@ function isSqlTool(toolName, toolInspection) {
620
937
  const fieldName = toolInspection[matchingPattern];
621
938
  return fieldName === "sql" || fieldName === "query";
622
939
  }
623
- var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
624
940
  async function analyzeShellCommand(command) {
625
941
  const actions = [];
626
942
  const paths = [];
@@ -702,188 +1018,14 @@ function redactSecrets(text) {
702
1018
  );
703
1019
  return redacted;
704
1020
  }
705
- var DANGEROUS_WORDS = [
706
- "mkfs",
707
- // formats/wipes a filesystem partition
708
- "shred"
709
- // permanently overwrites file contents (unrecoverable)
710
- ];
711
- var DEFAULT_CONFIG = {
712
- settings: {
713
- mode: "standard",
714
- autoStartDaemon: true,
715
- enableUndo: true,
716
- // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
717
- enableHookLogDebug: false,
718
- approvalTimeoutMs: 0,
719
- // 0 = disabled; set e.g. 30000 for 30-second auto-deny
720
- approvers: { native: true, browser: true, cloud: true, terminal: true }
721
- },
722
- policy: {
723
- sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
724
- dangerousWords: DANGEROUS_WORDS,
725
- ignoredTools: [
726
- "list_*",
727
- "get_*",
728
- "read_*",
729
- "describe_*",
730
- "read",
731
- "glob",
732
- "grep",
733
- "ls",
734
- "notebookread",
735
- "notebookedit",
736
- "webfetch",
737
- "websearch",
738
- "exitplanmode",
739
- "askuserquestion",
740
- "agent",
741
- "task*",
742
- "toolsearch",
743
- "mcp__ide__*",
744
- "getDiagnostics"
745
- ],
746
- toolInspection: {
747
- bash: "command",
748
- shell: "command",
749
- run_shell_command: "command",
750
- "terminal.execute": "command",
751
- "postgres:query": "sql"
752
- },
753
- snapshot: {
754
- tools: [
755
- "str_replace_based_edit_tool",
756
- "write_file",
757
- "edit_file",
758
- "create_file",
759
- "edit",
760
- "replace"
761
- ],
762
- onlyPaths: [],
763
- ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
764
- },
765
- rules: [
766
- // Only use the legacy rules format for simple path-based rm control.
767
- // All other command-level enforcement lives in smartRules below.
768
- {
769
- action: "rm",
770
- allowPaths: [
771
- "**/node_modules/**",
772
- "dist/**",
773
- "build/**",
774
- ".next/**",
775
- "coverage/**",
776
- ".cache/**",
777
- "tmp/**",
778
- "temp/**",
779
- ".DS_Store"
780
- ]
781
- }
782
- ],
783
- smartRules: [
784
- // ── SQL safety ────────────────────────────────────────────────────────
785
- {
786
- name: "no-delete-without-where",
787
- tool: "*",
788
- conditions: [
789
- { field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
790
- { field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
791
- ],
792
- conditionMode: "all",
793
- verdict: "review",
794
- reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
795
- },
796
- {
797
- name: "review-drop-truncate-shell",
798
- tool: "bash",
799
- conditions: [
800
- {
801
- field: "command",
802
- op: "matches",
803
- value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
804
- flags: "i"
805
- }
806
- ],
807
- conditionMode: "all",
808
- verdict: "review",
809
- reason: "SQL DDL destructive statement inside a shell command"
810
- },
811
- // ── Git safety ────────────────────────────────────────────────────────
812
- {
813
- name: "block-force-push",
814
- tool: "bash",
815
- conditions: [
816
- {
817
- field: "command",
818
- op: "matches",
819
- value: "git push.*(--force|--force-with-lease|-f\\b)",
820
- flags: "i"
821
- }
822
- ],
823
- conditionMode: "all",
824
- verdict: "block",
825
- reason: "Force push overwrites remote history and cannot be undone"
826
- },
827
- {
828
- name: "review-git-push",
829
- tool: "bash",
830
- conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
831
- conditionMode: "all",
832
- verdict: "review",
833
- reason: "git push sends changes to a shared remote"
834
- },
835
- {
836
- name: "review-git-destructive",
837
- tool: "bash",
838
- conditions: [
839
- {
840
- field: "command",
841
- op: "matches",
842
- value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
843
- flags: "i"
844
- }
845
- ],
846
- conditionMode: "all",
847
- verdict: "review",
848
- reason: "Destructive git operation \u2014 discards history or working-tree changes"
849
- },
850
- // ── Shell safety ──────────────────────────────────────────────────────
851
- {
852
- name: "review-sudo",
853
- tool: "bash",
854
- conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
855
- conditionMode: "all",
856
- verdict: "review",
857
- reason: "Command requires elevated privileges"
858
- },
859
- {
860
- name: "review-curl-pipe-shell",
861
- tool: "bash",
862
- conditions: [
863
- {
864
- field: "command",
865
- op: "matches",
866
- value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
867
- flags: "i"
868
- }
869
- ],
870
- conditionMode: "all",
871
- verdict: "block",
872
- reason: "Piping remote script into a shell is a supply-chain attack vector"
873
- }
874
- ]
875
- },
876
- environments: {}
877
- };
878
- var cachedConfig = null;
879
1021
  function _resetConfigCache() {
880
1022
  cachedConfig = null;
881
1023
  }
882
1024
  function getGlobalSettings() {
883
1025
  try {
884
- const globalConfigPath = path3.join(os.homedir(), ".node9", "config.json");
885
- if (fs.existsSync(globalConfigPath)) {
886
- const parsed = JSON.parse(fs.readFileSync(globalConfigPath, "utf-8"));
1026
+ const globalConfigPath = path4.join(os2.homedir(), ".node9", "config.json");
1027
+ if (fs2.existsSync(globalConfigPath)) {
1028
+ const parsed = JSON.parse(fs2.readFileSync(globalConfigPath, "utf-8"));
887
1029
  const settings = parsed.settings || {};
888
1030
  return {
889
1031
  mode: settings.mode || "standard",
@@ -905,9 +1047,9 @@ function getGlobalSettings() {
905
1047
  }
906
1048
  function getInternalToken() {
907
1049
  try {
908
- const pidFile = path3.join(os.homedir(), ".node9", "daemon.pid");
909
- if (!fs.existsSync(pidFile)) return null;
910
- const data = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
1050
+ const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1051
+ if (!fs2.existsSync(pidFile)) return null;
1052
+ const data = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
911
1053
  process.kill(data.pid, 0);
912
1054
  return data.internalToken ?? null;
913
1055
  } catch {
@@ -922,7 +1064,8 @@ async function evaluatePolicy(toolName, args, agent) {
922
1064
  (rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
923
1065
  );
924
1066
  if (matchedRule) {
925
- if (matchedRule.verdict === "allow") return { decision: "allow" };
1067
+ if (matchedRule.verdict === "allow")
1068
+ return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
926
1069
  return {
927
1070
  decision: matchedRule.verdict,
928
1071
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
@@ -933,13 +1076,11 @@ async function evaluatePolicy(toolName, args, agent) {
933
1076
  }
934
1077
  }
935
1078
  let allTokens = [];
936
- let actionTokens = [];
937
1079
  let pathTokens = [];
938
1080
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
939
1081
  if (shellCommand) {
940
1082
  const analyzed = await analyzeShellCommand(shellCommand);
941
1083
  allTokens = analyzed.allTokens;
942
- actionTokens = analyzed.actions;
943
1084
  pathTokens = analyzed.paths;
944
1085
  const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
945
1086
  if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
@@ -947,11 +1088,9 @@ async function evaluatePolicy(toolName, args, agent) {
947
1088
  }
948
1089
  if (isSqlTool(toolName, config.policy.toolInspection)) {
949
1090
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
950
- actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
951
1091
  }
952
1092
  } else {
953
1093
  allTokens = tokenize(toolName);
954
- actionTokens = [toolName];
955
1094
  if (args && typeof args === "object") {
956
1095
  const flattenedArgs = JSON.stringify(args).toLowerCase();
957
1096
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
@@ -974,29 +1113,6 @@ async function evaluatePolicy(toolName, args, agent) {
974
1113
  const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
975
1114
  if (allInSandbox) return { decision: "allow" };
976
1115
  }
977
- for (const action of actionTokens) {
978
- const rule = config.policy.rules.find(
979
- (r) => r.action === action || matchesPattern(action, r.action)
980
- );
981
- if (rule) {
982
- if (pathTokens.length > 0) {
983
- const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
984
- if (anyBlocked)
985
- return {
986
- decision: "review",
987
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
988
- tier: 5
989
- };
990
- const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
991
- if (allAllowed) return { decision: "allow" };
992
- }
993
- return {
994
- decision: "review",
995
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
996
- tier: 5
997
- };
998
- }
999
- }
1000
1116
  let matchedDangerousWord;
1001
1117
  const isDangerous = allTokens.some(
1002
1118
  (token) => config.policy.dangerousWords.some((word) => {
@@ -1048,9 +1164,9 @@ async function evaluatePolicy(toolName, args, agent) {
1048
1164
  }
1049
1165
  async function explainPolicy(toolName, args) {
1050
1166
  const steps = [];
1051
- const globalPath = path3.join(os.homedir(), ".node9", "config.json");
1052
- const projectPath = path3.join(process.cwd(), "node9.config.json");
1053
- const credsPath = path3.join(os.homedir(), ".node9", "credentials.json");
1167
+ const globalPath = path4.join(os2.homedir(), ".node9", "config.json");
1168
+ const projectPath = path4.join(process.cwd(), "node9.config.json");
1169
+ const credsPath = path4.join(os2.homedir(), ".node9", "credentials.json");
1054
1170
  const waterfall = [
1055
1171
  {
1056
1172
  tier: 1,
@@ -1061,19 +1177,19 @@ async function explainPolicy(toolName, args) {
1061
1177
  {
1062
1178
  tier: 2,
1063
1179
  label: "Cloud policy",
1064
- status: fs.existsSync(credsPath) ? "active" : "missing",
1065
- note: fs.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1180
+ status: fs2.existsSync(credsPath) ? "active" : "missing",
1181
+ note: fs2.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1066
1182
  },
1067
1183
  {
1068
1184
  tier: 3,
1069
1185
  label: "Project config",
1070
- status: fs.existsSync(projectPath) ? "active" : "missing",
1186
+ status: fs2.existsSync(projectPath) ? "active" : "missing",
1071
1187
  path: projectPath
1072
1188
  },
1073
1189
  {
1074
1190
  tier: 4,
1075
1191
  label: "Global config",
1076
- status: fs.existsSync(globalPath) ? "active" : "missing",
1192
+ status: fs2.existsSync(globalPath) ? "active" : "missing",
1077
1193
  path: globalPath
1078
1194
  },
1079
1195
  {
@@ -1084,7 +1200,28 @@ async function explainPolicy(toolName, args) {
1084
1200
  }
1085
1201
  ];
1086
1202
  const config = getConfig();
1087
- if (matchesPattern(toolName, config.policy.ignoredTools)) {
1203
+ const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
1204
+ if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
1205
+ const dlpMatch = args !== void 0 ? scanArgs(args) : null;
1206
+ if (dlpMatch) {
1207
+ steps.push({
1208
+ name: "DLP Content Scanner",
1209
+ outcome: dlpMatch.severity === "block" ? "block" : "review",
1210
+ detail: `\u{1F6A8} ${dlpMatch.patternName} detected in ${dlpMatch.fieldPath} \u2014 sample: ${dlpMatch.redactedSample}`,
1211
+ isFinal: dlpMatch.severity === "block"
1212
+ });
1213
+ if (dlpMatch.severity === "block") {
1214
+ return { tool: toolName, args, waterfall, steps, decision: "block" };
1215
+ }
1216
+ } else {
1217
+ steps.push({
1218
+ name: "DLP Content Scanner",
1219
+ outcome: "checked",
1220
+ detail: "No sensitive credentials detected in args"
1221
+ });
1222
+ }
1223
+ }
1224
+ if (wouldBeIgnored) {
1088
1225
  steps.push({
1089
1226
  name: "Ignored tools",
1090
1227
  outcome: "allow",
@@ -1137,13 +1274,11 @@ async function explainPolicy(toolName, args) {
1137
1274
  steps.push({ name: "Smart rules", outcome: "skip", detail: "No smart rules configured" });
1138
1275
  }
1139
1276
  let allTokens = [];
1140
- let actionTokens = [];
1141
1277
  let pathTokens = [];
1142
1278
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
1143
1279
  if (shellCommand) {
1144
1280
  const analyzed = await analyzeShellCommand(shellCommand);
1145
1281
  allTokens = analyzed.allTokens;
1146
- actionTokens = analyzed.actions;
1147
1282
  pathTokens = analyzed.paths;
1148
1283
  const patterns = Object.keys(config.policy.toolInspection);
1149
1284
  const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
@@ -1177,7 +1312,6 @@ async function explainPolicy(toolName, args) {
1177
1312
  });
1178
1313
  if (isSqlTool(toolName, config.policy.toolInspection)) {
1179
1314
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
1180
- actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
1181
1315
  steps.push({
1182
1316
  name: "SQL token stripping",
1183
1317
  outcome: "checked",
@@ -1186,7 +1320,6 @@ async function explainPolicy(toolName, args) {
1186
1320
  }
1187
1321
  } else {
1188
1322
  allTokens = tokenize(toolName);
1189
- actionTokens = [toolName];
1190
1323
  let detail = `No toolInspection match for "${toolName}" \u2014 tokens: [${allTokens.join(", ")}]`;
1191
1324
  if (args && typeof args === "object") {
1192
1325
  const flattenedArgs = JSON.stringify(args).toLowerCase();
@@ -1227,74 +1360,15 @@ async function explainPolicy(toolName, args) {
1227
1360
  detail: pathTokens.length === 0 ? "No path tokens found in input" : "No sandbox paths configured"
1228
1361
  });
1229
1362
  }
1230
- let ruleMatched = false;
1231
- for (const action of actionTokens) {
1232
- const rule = config.policy.rules.find(
1233
- (r) => r.action === action || matchesPattern(action, r.action)
1234
- );
1235
- if (rule) {
1236
- ruleMatched = true;
1237
- if (pathTokens.length > 0) {
1238
- const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
1239
- if (anyBlocked) {
1240
- steps.push({
1241
- name: "Policy rules",
1242
- outcome: "review",
1243
- detail: `Rule "${rule.action}" matched + path is in blockPaths`,
1244
- isFinal: true
1245
- });
1246
- return {
1247
- tool: toolName,
1248
- args,
1249
- waterfall,
1250
- steps,
1251
- decision: "review",
1252
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
1253
- };
1254
- }
1255
- const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
1256
- if (allAllowed) {
1257
- steps.push({
1258
- name: "Policy rules",
1259
- outcome: "allow",
1260
- detail: `Rule "${rule.action}" matched + all paths are in allowPaths`,
1261
- isFinal: true
1262
- });
1263
- return { tool: toolName, args, waterfall, steps, decision: "allow" };
1264
- }
1265
- }
1266
- steps.push({
1267
- name: "Policy rules",
1268
- outcome: "review",
1269
- detail: `Rule "${rule.action}" matched \u2014 default block (no path exception)`,
1270
- isFinal: true
1271
- });
1272
- return {
1273
- tool: toolName,
1274
- args,
1275
- waterfall,
1276
- steps,
1277
- decision: "review",
1278
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
1279
- };
1280
- }
1281
- }
1282
- if (!ruleMatched) {
1283
- steps.push({
1284
- name: "Policy rules",
1285
- outcome: "skip",
1286
- detail: config.policy.rules.length === 0 ? "No rules configured" : `No rule matched [${actionTokens.join(", ")}]`
1287
- });
1288
- }
1289
- let matchedDangerousWord;
1290
- const isDangerous = uniqueTokens.some(
1291
- (token) => config.policy.dangerousWords.some((word) => {
1292
- const w = word.toLowerCase();
1293
- const hit = token === w || (() => {
1294
- try {
1295
- return new RegExp(`\\b${w}\\b`, "i").test(token);
1296
- } catch {
1297
- return false;
1363
+ let matchedDangerousWord;
1364
+ const isDangerous = uniqueTokens.some(
1365
+ (token) => config.policy.dangerousWords.some((word) => {
1366
+ const w = word.toLowerCase();
1367
+ const hit = token === w || (() => {
1368
+ try {
1369
+ return new RegExp(`\\b${w}\\b`, "i").test(token);
1370
+ } catch {
1371
+ return false;
1298
1372
  }
1299
1373
  })();
1300
1374
  if (hit && !matchedDangerousWord) matchedDangerousWord = word;
@@ -1350,13 +1424,11 @@ function isIgnoredTool(toolName) {
1350
1424
  const config = getConfig();
1351
1425
  return matchesPattern(toolName, config.policy.ignoredTools);
1352
1426
  }
1353
- var DAEMON_PORT = 7391;
1354
- var DAEMON_HOST = "127.0.0.1";
1355
1427
  function isDaemonRunning() {
1356
1428
  try {
1357
- const pidFile = path3.join(os.homedir(), ".node9", "daemon.pid");
1358
- if (!fs.existsSync(pidFile)) return false;
1359
- const { pid, port } = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
1429
+ const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1430
+ if (!fs2.existsSync(pidFile)) return false;
1431
+ const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1360
1432
  if (port !== DAEMON_PORT) return false;
1361
1433
  process.kill(pid, 0);
1362
1434
  return true;
@@ -1366,16 +1438,16 @@ function isDaemonRunning() {
1366
1438
  }
1367
1439
  function getPersistentDecision(toolName) {
1368
1440
  try {
1369
- const file = path3.join(os.homedir(), ".node9", "decisions.json");
1370
- if (!fs.existsSync(file)) return null;
1371
- const decisions = JSON.parse(fs.readFileSync(file, "utf-8"));
1441
+ const file = path4.join(os2.homedir(), ".node9", "decisions.json");
1442
+ if (!fs2.existsSync(file)) return null;
1443
+ const decisions = JSON.parse(fs2.readFileSync(file, "utf-8"));
1372
1444
  const d = decisions[toolName];
1373
1445
  if (d === "allow" || d === "deny") return d;
1374
1446
  } catch {
1375
1447
  }
1376
1448
  return null;
1377
1449
  }
1378
- async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1450
+ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
1379
1451
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1380
1452
  const checkCtrl = new AbortController();
1381
1453
  const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
@@ -1390,6 +1462,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1390
1462
  args,
1391
1463
  agent: meta?.agent,
1392
1464
  mcpServer: meta?.mcpServer,
1465
+ fromCLI: true,
1466
+ // Pass the flight-recorder ID so the daemon uses the same UUID for
1467
+ // activity-result as the CLI used for the pending activity event.
1468
+ // Without this, the two UUIDs never match and tail.ts never resolves
1469
+ // the pending item.
1470
+ activityId,
1393
1471
  ...riskMetadata && { riskMetadata }
1394
1472
  }),
1395
1473
  signal: checkCtrl.signal
@@ -1444,7 +1522,44 @@ async function resolveViaDaemon(id, decision, internalToken) {
1444
1522
  signal: AbortSignal.timeout(3e3)
1445
1523
  });
1446
1524
  }
1525
+ function notifyActivity(data) {
1526
+ return new Promise((resolve) => {
1527
+ try {
1528
+ const payload = JSON.stringify(data);
1529
+ const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
1530
+ sock.on("connect", () => {
1531
+ sock.on("close", resolve);
1532
+ sock.end(payload);
1533
+ });
1534
+ sock.on("error", resolve);
1535
+ } catch {
1536
+ resolve();
1537
+ }
1538
+ });
1539
+ }
1447
1540
  async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
1541
+ if (!options?.calledFromDaemon) {
1542
+ const actId = randomUUID();
1543
+ const actTs = Date.now();
1544
+ await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
1545
+ const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
1546
+ ...options,
1547
+ activityId: actId
1548
+ });
1549
+ if (!result.noApprovalMechanism) {
1550
+ await notifyActivity({
1551
+ id: actId,
1552
+ tool: toolName,
1553
+ ts: actTs,
1554
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
1555
+ label: result.blockedByLabel
1556
+ });
1557
+ }
1558
+ return result;
1559
+ }
1560
+ return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
1561
+ }
1562
+ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
1448
1563
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1449
1564
  const pauseState = checkPause();
1450
1565
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
@@ -1467,6 +1582,23 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1467
1582
  let policyMatchedField;
1468
1583
  let policyMatchedWord;
1469
1584
  let riskMetadata;
1585
+ if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
1586
+ const dlpMatch = scanArgs(args);
1587
+ if (dlpMatch) {
1588
+ const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
1589
+ if (dlpMatch.severity === "block") {
1590
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
1591
+ return {
1592
+ approved: false,
1593
+ reason: dlpReason,
1594
+ blockedBy: "local-config",
1595
+ blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
1596
+ };
1597
+ }
1598
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
1599
+ explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
1600
+ }
1601
+ }
1470
1602
  if (config.settings.mode === "audit") {
1471
1603
  if (!isIgnoredTool(toolName)) {
1472
1604
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
@@ -1686,7 +1818,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1686
1818
  console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
1687
1819
  `));
1688
1820
  }
1689
- const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
1821
+ const daemonDecision = await askDaemon(
1822
+ toolName,
1823
+ args,
1824
+ meta,
1825
+ signal,
1826
+ riskMetadata,
1827
+ options?.activityId
1828
+ );
1690
1829
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
1691
1830
  const isApproved = daemonDecision === "allow";
1692
1831
  return {
@@ -1706,7 +1845,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1706
1845
  racePromises.push(
1707
1846
  (async () => {
1708
1847
  try {
1709
- console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
1848
+ if (explainableLabel.includes("DLP")) {
1849
+ console.log(chalk2.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
1850
+ console.log(
1851
+ chalk2.red.bold(` A sensitive secret was detected in the tool arguments!`)
1852
+ );
1853
+ } else {
1854
+ console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
1855
+ }
1710
1856
  console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
1711
1857
  console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
1712
1858
  if (isRemoteLocked) {
@@ -1811,8 +1957,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1811
1957
  }
1812
1958
  function getConfig() {
1813
1959
  if (cachedConfig) return cachedConfig;
1814
- const globalPath = path3.join(os.homedir(), ".node9", "config.json");
1815
- const projectPath = path3.join(process.cwd(), "node9.config.json");
1960
+ const globalPath = path4.join(os2.homedir(), ".node9", "config.json");
1961
+ const projectPath = path4.join(process.cwd(), "node9.config.json");
1816
1962
  const globalConfig = tryLoadConfig(globalPath);
1817
1963
  const projectConfig = tryLoadConfig(projectPath);
1818
1964
  const mergedSettings = {
@@ -1824,13 +1970,13 @@ function getConfig() {
1824
1970
  dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
1825
1971
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
1826
1972
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
1827
- rules: [...DEFAULT_CONFIG.policy.rules],
1828
1973
  smartRules: [...DEFAULT_CONFIG.policy.smartRules],
1829
1974
  snapshot: {
1830
1975
  tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
1831
1976
  onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
1832
1977
  ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
1833
- }
1978
+ },
1979
+ dlp: { ...DEFAULT_CONFIG.policy.dlp }
1834
1980
  };
1835
1981
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
1836
1982
  const applyLayer = (source) => {
@@ -1850,7 +1996,6 @@ function getConfig() {
1850
1996
  if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
1851
1997
  if (p.toolInspection)
1852
1998
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
1853
- if (p.rules) mergedPolicy.rules.push(...p.rules);
1854
1999
  if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
1855
2000
  if (p.snapshot) {
1856
2001
  const s2 = p.snapshot;
@@ -1858,6 +2003,11 @@ function getConfig() {
1858
2003
  if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
1859
2004
  if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
1860
2005
  }
2006
+ if (p.dlp) {
2007
+ const d = p.dlp;
2008
+ if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
2009
+ if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
2010
+ }
1861
2011
  const envs = source.environments || {};
1862
2012
  for (const [envName, envConfig] of Object.entries(envs)) {
1863
2013
  if (envConfig && typeof envConfig === "object") {
@@ -1872,6 +2022,22 @@ function getConfig() {
1872
2022
  };
1873
2023
  applyLayer(globalConfig);
1874
2024
  applyLayer(projectConfig);
2025
+ for (const shieldName of readActiveShields()) {
2026
+ const shield = getShield(shieldName);
2027
+ if (!shield) continue;
2028
+ const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
2029
+ for (const rule of shield.smartRules) {
2030
+ if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
2031
+ }
2032
+ const existingWords = new Set(mergedPolicy.dangerousWords);
2033
+ for (const word of shield.dangerousWords) {
2034
+ if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
2035
+ }
2036
+ }
2037
+ const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
2038
+ for (const rule of ADVISORY_SMART_RULES) {
2039
+ if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
2040
+ }
1875
2041
  if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
1876
2042
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
1877
2043
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
@@ -1887,10 +2053,10 @@ function getConfig() {
1887
2053
  return cachedConfig;
1888
2054
  }
1889
2055
  function tryLoadConfig(filePath) {
1890
- if (!fs.existsSync(filePath)) return null;
2056
+ if (!fs2.existsSync(filePath)) return null;
1891
2057
  let raw;
1892
2058
  try {
1893
- raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
2059
+ raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
1894
2060
  } catch (err) {
1895
2061
  const msg = err instanceof Error ? err.message : String(err);
1896
2062
  process.stderr.write(
@@ -1952,9 +2118,9 @@ function getCredentials() {
1952
2118
  };
1953
2119
  }
1954
2120
  try {
1955
- const credPath = path3.join(os.homedir(), ".node9", "credentials.json");
1956
- if (fs.existsSync(credPath)) {
1957
- const creds = JSON.parse(fs.readFileSync(credPath, "utf-8"));
2121
+ const credPath = path4.join(os2.homedir(), ".node9", "credentials.json");
2122
+ if (fs2.existsSync(credPath)) {
2123
+ const creds = JSON.parse(fs2.readFileSync(credPath, "utf-8"));
1958
2124
  const profileName = process.env.NODE9_PROFILE || "default";
1959
2125
  const profile = creds[profileName];
1960
2126
  if (profile?.apiKey) {
@@ -1985,9 +2151,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1985
2151
  context: {
1986
2152
  agent: meta?.agent,
1987
2153
  mcpServer: meta?.mcpServer,
1988
- hostname: os.hostname(),
2154
+ hostname: os2.hostname(),
1989
2155
  cwd: process.cwd(),
1990
- platform: os.platform()
2156
+ platform: os2.platform()
1991
2157
  }
1992
2158
  }),
1993
2159
  signal: AbortSignal.timeout(5e3)
@@ -2008,9 +2174,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2008
2174
  context: {
2009
2175
  agent: meta?.agent,
2010
2176
  mcpServer: meta?.mcpServer,
2011
- hostname: os.hostname(),
2177
+ hostname: os2.hostname(),
2012
2178
  cwd: process.cwd(),
2013
- platform: os.platform()
2179
+ platform: os2.platform()
2014
2180
  },
2015
2181
  ...riskMetadata && { riskMetadata }
2016
2182
  }),
@@ -2067,270 +2233,233 @@ async function resolveNode9SaaS(requestId, creds, approved) {
2067
2233
  } catch {
2068
2234
  }
2069
2235
  }
2070
-
2071
- // src/setup.ts
2072
- import fs2 from "fs";
2073
- import path4 from "path";
2074
- import os2 from "os";
2075
- import chalk3 from "chalk";
2076
- import { confirm as confirm2 } from "@inquirer/prompts";
2077
- function printDaemonTip() {
2078
- console.log(
2079
- chalk3.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk3.white("\n To view your history or manage persistent rules, run:") + chalk3.green("\n node9 daemon --openui")
2080
- );
2081
- }
2082
- function fullPathCommand(subcommand) {
2083
- if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
2084
- const nodeExec = process.execPath;
2085
- const cliScript = process.argv[1];
2086
- return `${nodeExec} ${cliScript} ${subcommand}`;
2087
- }
2088
- function readJson(filePath) {
2089
- try {
2090
- if (fs2.existsSync(filePath)) {
2091
- return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
2092
- }
2093
- } catch {
2094
- }
2095
- return null;
2096
- }
2097
- function writeJson(filePath, data) {
2098
- const dir = path4.dirname(filePath);
2099
- if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
2100
- fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
2101
- }
2102
- async function setupClaude() {
2103
- const homeDir2 = os2.homedir();
2104
- const mcpPath = path4.join(homeDir2, ".claude.json");
2105
- const hooksPath = path4.join(homeDir2, ".claude", "settings.json");
2106
- const claudeConfig = readJson(mcpPath) ?? {};
2107
- const settings = readJson(hooksPath) ?? {};
2108
- const servers = claudeConfig.mcpServers ?? {};
2109
- let anythingChanged = false;
2110
- if (!settings.hooks) settings.hooks = {};
2111
- const hasPreHook = settings.hooks.PreToolUse?.some(
2112
- (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
2113
- );
2114
- if (!hasPreHook) {
2115
- if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
2116
- settings.hooks.PreToolUse.push({
2117
- matcher: ".*",
2118
- hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
2119
- });
2120
- console.log(chalk3.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
2121
- anythingChanged = true;
2122
- }
2123
- const hasPostHook = settings.hooks.PostToolUse?.some(
2124
- (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
2125
- );
2126
- if (!hasPostHook) {
2127
- if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
2128
- settings.hooks.PostToolUse.push({
2129
- matcher: ".*",
2130
- hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
2131
- });
2132
- console.log(chalk3.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
2133
- anythingChanged = true;
2134
- }
2135
- if (anythingChanged) {
2136
- writeJson(hooksPath, settings);
2137
- console.log("");
2138
- }
2139
- const serversToWrap = [];
2140
- for (const [name, server] of Object.entries(servers)) {
2141
- if (!server.command || server.command === "node9") continue;
2142
- const parts = [server.command, ...server.args ?? []];
2143
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
2144
- }
2145
- if (serversToWrap.length > 0) {
2146
- console.log(chalk3.bold("The following existing entries will be modified:\n"));
2147
- console.log(chalk3.white(` ${mcpPath}`));
2148
- for (const { name, originalCmd } of serversToWrap) {
2149
- console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
2150
- }
2151
- console.log("");
2152
- const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
2153
- if (proceed) {
2154
- for (const { name, parts } of serversToWrap) {
2155
- servers[name] = { ...servers[name], command: "node9", args: parts };
2236
+ var 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;
2237
+ var init_core = __esm({
2238
+ "src/core.ts"() {
2239
+ "use strict";
2240
+ init_native();
2241
+ init_context_sniper();
2242
+ init_config_schema();
2243
+ init_shields();
2244
+ init_dlp();
2245
+ PAUSED_FILE = path4.join(os2.homedir(), ".node9", "PAUSED");
2246
+ TRUST_FILE = path4.join(os2.homedir(), ".node9", "trust.json");
2247
+ LOCAL_AUDIT_LOG = path4.join(os2.homedir(), ".node9", "audit.log");
2248
+ HOOK_DEBUG_LOG = path4.join(os2.homedir(), ".node9", "hook-debug.log");
2249
+ SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2250
+ DANGEROUS_WORDS = [
2251
+ "mkfs",
2252
+ // formats/wipes a filesystem partition
2253
+ "shred"
2254
+ // permanently overwrites file contents (unrecoverable)
2255
+ ];
2256
+ DEFAULT_CONFIG = {
2257
+ settings: {
2258
+ mode: "standard",
2259
+ autoStartDaemon: true,
2260
+ enableUndo: true,
2261
+ // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
2262
+ enableHookLogDebug: false,
2263
+ approvalTimeoutMs: 0,
2264
+ // 0 = disabled; set e.g. 30000 for 30-second auto-deny
2265
+ approvers: { native: true, browser: true, cloud: true, terminal: true }
2266
+ },
2267
+ policy: {
2268
+ sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
2269
+ dangerousWords: DANGEROUS_WORDS,
2270
+ ignoredTools: [
2271
+ "list_*",
2272
+ "get_*",
2273
+ "read_*",
2274
+ "describe_*",
2275
+ "read",
2276
+ "glob",
2277
+ "grep",
2278
+ "ls",
2279
+ "notebookread",
2280
+ "notebookedit",
2281
+ "webfetch",
2282
+ "websearch",
2283
+ "exitplanmode",
2284
+ "askuserquestion",
2285
+ "agent",
2286
+ "task*",
2287
+ "toolsearch",
2288
+ "mcp__ide__*",
2289
+ "getDiagnostics"
2290
+ ],
2291
+ toolInspection: {
2292
+ bash: "command",
2293
+ shell: "command",
2294
+ run_shell_command: "command",
2295
+ "terminal.execute": "command",
2296
+ "postgres:query": "sql"
2297
+ },
2298
+ snapshot: {
2299
+ tools: [
2300
+ "str_replace_based_edit_tool",
2301
+ "write_file",
2302
+ "edit_file",
2303
+ "create_file",
2304
+ "edit",
2305
+ "replace"
2306
+ ],
2307
+ onlyPaths: [],
2308
+ ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
2309
+ },
2310
+ smartRules: [
2311
+ // ── rm safety (critical always evaluated first) ──────────────────────
2312
+ {
2313
+ name: "block-rm-rf-home",
2314
+ tool: "bash",
2315
+ conditionMode: "all",
2316
+ conditions: [
2317
+ {
2318
+ field: "command",
2319
+ op: "matches",
2320
+ value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
2321
+ },
2322
+ {
2323
+ field: "command",
2324
+ op: "matches",
2325
+ value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
2326
+ }
2327
+ ],
2328
+ verdict: "block",
2329
+ reason: "Recursive delete of home directory is irreversible"
2330
+ },
2331
+ // ── SQL safety ────────────────────────────────────────────────────────
2332
+ {
2333
+ name: "no-delete-without-where",
2334
+ tool: "*",
2335
+ conditions: [
2336
+ { field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
2337
+ { field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
2338
+ ],
2339
+ conditionMode: "all",
2340
+ verdict: "review",
2341
+ reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
2342
+ },
2343
+ {
2344
+ name: "review-drop-truncate-shell",
2345
+ tool: "bash",
2346
+ conditions: [
2347
+ {
2348
+ field: "command",
2349
+ op: "matches",
2350
+ value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
2351
+ flags: "i"
2352
+ }
2353
+ ],
2354
+ conditionMode: "all",
2355
+ verdict: "review",
2356
+ reason: "SQL DDL destructive statement inside a shell command"
2357
+ },
2358
+ // ── Git safety ────────────────────────────────────────────────────────
2359
+ {
2360
+ name: "block-force-push",
2361
+ tool: "bash",
2362
+ conditions: [
2363
+ {
2364
+ field: "command",
2365
+ op: "matches",
2366
+ value: "git push.*(--force|--force-with-lease|-f\\b)",
2367
+ flags: "i"
2368
+ }
2369
+ ],
2370
+ conditionMode: "all",
2371
+ verdict: "block",
2372
+ reason: "Force push overwrites remote history and cannot be undone"
2373
+ },
2374
+ {
2375
+ name: "review-git-push",
2376
+ tool: "bash",
2377
+ conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
2378
+ conditionMode: "all",
2379
+ verdict: "review",
2380
+ reason: "git push sends changes to a shared remote"
2381
+ },
2382
+ {
2383
+ name: "review-git-destructive",
2384
+ tool: "bash",
2385
+ conditions: [
2386
+ {
2387
+ field: "command",
2388
+ op: "matches",
2389
+ value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
2390
+ flags: "i"
2391
+ }
2392
+ ],
2393
+ conditionMode: "all",
2394
+ verdict: "review",
2395
+ reason: "Destructive git operation \u2014 discards history or working-tree changes"
2396
+ },
2397
+ // ── Shell safety ──────────────────────────────────────────────────────
2398
+ {
2399
+ name: "review-sudo",
2400
+ tool: "bash",
2401
+ conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
2402
+ conditionMode: "all",
2403
+ verdict: "review",
2404
+ reason: "Command requires elevated privileges"
2405
+ },
2406
+ {
2407
+ name: "review-curl-pipe-shell",
2408
+ tool: "bash",
2409
+ conditions: [
2410
+ {
2411
+ field: "command",
2412
+ op: "matches",
2413
+ value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
2414
+ flags: "i"
2415
+ }
2416
+ ],
2417
+ conditionMode: "all",
2418
+ verdict: "block",
2419
+ reason: "Piping remote script into a shell is a supply-chain attack vector"
2420
+ }
2421
+ ],
2422
+ dlp: { enabled: true, scanIgnoredTools: true }
2423
+ },
2424
+ environments: {}
2425
+ };
2426
+ ADVISORY_SMART_RULES = [
2427
+ {
2428
+ name: "allow-rm-safe-paths",
2429
+ tool: "*",
2430
+ conditionMode: "all",
2431
+ conditions: [
2432
+ { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
2433
+ {
2434
+ field: "command",
2435
+ op: "matches",
2436
+ // Matches known-safe build artifact paths in the command.
2437
+ value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
2438
+ }
2439
+ ],
2440
+ verdict: "allow",
2441
+ reason: "Deleting a known-safe build artifact path"
2442
+ },
2443
+ {
2444
+ name: "review-rm",
2445
+ tool: "*",
2446
+ conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
2447
+ verdict: "review",
2448
+ reason: "rm can permanently delete files \u2014 confirm the target path"
2156
2449
  }
2157
- claudeConfig.mcpServers = servers;
2158
- writeJson(mcpPath, claudeConfig);
2159
- console.log(chalk3.green(`
2160
- \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
2161
- anythingChanged = true;
2162
- } else {
2163
- console.log(chalk3.yellow(" Skipped MCP server wrapping."));
2164
- }
2165
- console.log("");
2166
- }
2167
- if (!anythingChanged && serversToWrap.length === 0) {
2168
- console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
2169
- printDaemonTip();
2170
- return;
2171
- }
2172
- if (anythingChanged) {
2173
- console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
2174
- console.log(chalk3.gray(" Restart Claude Code for changes to take effect."));
2175
- printDaemonTip();
2176
- }
2177
- }
2178
- async function setupGemini() {
2179
- const homeDir2 = os2.homedir();
2180
- const settingsPath = path4.join(homeDir2, ".gemini", "settings.json");
2181
- const settings = readJson(settingsPath) ?? {};
2182
- const servers = settings.mcpServers ?? {};
2183
- let anythingChanged = false;
2184
- if (!settings.hooks) settings.hooks = {};
2185
- const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
2186
- (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
2187
- );
2188
- if (!hasBeforeHook) {
2189
- if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
2190
- if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
2191
- settings.hooks.BeforeTool.push({
2192
- matcher: ".*",
2193
- hooks: [
2194
- {
2195
- name: "node9-check",
2196
- type: "command",
2197
- command: fullPathCommand("check"),
2198
- timeout: 6e5
2199
- }
2200
- ]
2201
- });
2202
- console.log(chalk3.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
2203
- anythingChanged = true;
2204
- }
2205
- const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
2206
- (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
2207
- );
2208
- if (!hasAfterHook) {
2209
- if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
2210
- if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
2211
- settings.hooks.AfterTool.push({
2212
- matcher: ".*",
2213
- hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
2214
- });
2215
- console.log(chalk3.green(" \u2705 AfterTool hook added \u2192 node9 log"));
2216
- anythingChanged = true;
2217
- }
2218
- if (anythingChanged) {
2219
- writeJson(settingsPath, settings);
2220
- console.log("");
2221
- }
2222
- const serversToWrap = [];
2223
- for (const [name, server] of Object.entries(servers)) {
2224
- if (!server.command || server.command === "node9") continue;
2225
- const parts = [server.command, ...server.args ?? []];
2226
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
2450
+ ];
2451
+ cachedConfig = null;
2452
+ DAEMON_PORT = 7391;
2453
+ DAEMON_HOST = "127.0.0.1";
2454
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path4.join(os2.tmpdir(), "node9-activity.sock");
2227
2455
  }
2228
- if (serversToWrap.length > 0) {
2229
- console.log(chalk3.bold("The following existing entries will be modified:\n"));
2230
- console.log(chalk3.white(` ${settingsPath} (mcpServers)`));
2231
- for (const { name, originalCmd } of serversToWrap) {
2232
- console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
2233
- }
2234
- console.log("");
2235
- const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
2236
- if (proceed) {
2237
- for (const { name, parts } of serversToWrap) {
2238
- servers[name] = { ...servers[name], command: "node9", args: parts };
2239
- }
2240
- settings.mcpServers = servers;
2241
- writeJson(settingsPath, settings);
2242
- console.log(chalk3.green(`
2243
- \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
2244
- anythingChanged = true;
2245
- } else {
2246
- console.log(chalk3.yellow(" Skipped MCP server wrapping."));
2247
- }
2248
- console.log("");
2249
- }
2250
- if (!anythingChanged && serversToWrap.length === 0) {
2251
- console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
2252
- printDaemonTip();
2253
- return;
2254
- }
2255
- if (anythingChanged) {
2256
- console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
2257
- console.log(chalk3.gray(" Restart Gemini CLI for changes to take effect."));
2258
- printDaemonTip();
2259
- }
2260
- }
2261
- async function setupCursor() {
2262
- const homeDir2 = os2.homedir();
2263
- const mcpPath = path4.join(homeDir2, ".cursor", "mcp.json");
2264
- const hooksPath = path4.join(homeDir2, ".cursor", "hooks.json");
2265
- const mcpConfig = readJson(mcpPath) ?? {};
2266
- const hooksFile = readJson(hooksPath) ?? { version: 1 };
2267
- const servers = mcpConfig.mcpServers ?? {};
2268
- let anythingChanged = false;
2269
- if (!hooksFile.hooks) hooksFile.hooks = {};
2270
- const hasPreHook = hooksFile.hooks.preToolUse?.some(
2271
- (h) => h.command === "node9" && h.args?.includes("check") || h.command?.includes("cli.js")
2272
- );
2273
- if (!hasPreHook) {
2274
- if (!hooksFile.hooks.preToolUse) hooksFile.hooks.preToolUse = [];
2275
- hooksFile.hooks.preToolUse.push({ command: fullPathCommand("check") });
2276
- console.log(chalk3.green(" \u2705 preToolUse hook added \u2192 node9 check"));
2277
- anythingChanged = true;
2278
- }
2279
- const hasPostHook = hooksFile.hooks.postToolUse?.some(
2280
- (h) => h.command === "node9" && h.args?.includes("log") || h.command?.includes("cli.js")
2281
- );
2282
- if (!hasPostHook) {
2283
- if (!hooksFile.hooks.postToolUse) hooksFile.hooks.postToolUse = [];
2284
- hooksFile.hooks.postToolUse.push({ command: fullPathCommand("log") });
2285
- console.log(chalk3.green(" \u2705 postToolUse hook added \u2192 node9 log"));
2286
- anythingChanged = true;
2287
- }
2288
- if (anythingChanged) {
2289
- writeJson(hooksPath, hooksFile);
2290
- console.log("");
2291
- }
2292
- const serversToWrap = [];
2293
- for (const [name, server] of Object.entries(servers)) {
2294
- if (!server.command || server.command === "node9") continue;
2295
- const parts = [server.command, ...server.args ?? []];
2296
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
2297
- }
2298
- if (serversToWrap.length > 0) {
2299
- console.log(chalk3.bold("The following existing entries will be modified:\n"));
2300
- console.log(chalk3.white(` ${mcpPath}`));
2301
- for (const { name, originalCmd } of serversToWrap) {
2302
- console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
2303
- }
2304
- console.log("");
2305
- const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
2306
- if (proceed) {
2307
- for (const { name, parts } of serversToWrap) {
2308
- servers[name] = { ...servers[name], command: "node9", args: parts };
2309
- }
2310
- mcpConfig.mcpServers = servers;
2311
- writeJson(mcpPath, mcpConfig);
2312
- console.log(chalk3.green(`
2313
- \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
2314
- anythingChanged = true;
2315
- } else {
2316
- console.log(chalk3.yellow(" Skipped MCP server wrapping."));
2317
- }
2318
- console.log("");
2319
- }
2320
- if (!anythingChanged && serversToWrap.length === 0) {
2321
- console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Cursor."));
2322
- printDaemonTip();
2323
- return;
2324
- }
2325
- if (anythingChanged) {
2326
- console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor!"));
2327
- console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
2328
- printDaemonTip();
2329
- }
2330
- }
2456
+ });
2331
2457
 
2332
2458
  // src/daemon/ui.html
2333
- var ui_default = `<!doctype html>
2459
+ var ui_default;
2460
+ var init_ui = __esm({
2461
+ "src/daemon/ui.html"() {
2462
+ ui_default = `<!doctype html>
2334
2463
  <html lang="en">
2335
2464
  <head>
2336
2465
  <meta charset="UTF-8" />
@@ -2356,6 +2485,11 @@ var ui_default = `<!doctype html>
2356
2485
  margin: 0;
2357
2486
  padding: 0;
2358
2487
  }
2488
+ html,
2489
+ body {
2490
+ height: 100%;
2491
+ overflow: hidden;
2492
+ }
2359
2493
  body {
2360
2494
  background: var(--bg);
2361
2495
  color: var(--text);
@@ -2363,16 +2497,17 @@ var ui_default = `<!doctype html>
2363
2497
  'Inter',
2364
2498
  -apple-system,
2365
2499
  sans-serif;
2366
- min-height: 100vh;
2367
2500
  }
2368
2501
 
2369
2502
  .shell {
2370
- max-width: 1000px;
2503
+ max-width: 1440px;
2504
+ height: 100vh;
2371
2505
  margin: 0 auto;
2372
- padding: 32px 24px 48px;
2506
+ padding: 16px 20px 16px;
2373
2507
  display: grid;
2374
2508
  grid-template-rows: auto 1fr;
2375
- gap: 24px;
2509
+ gap: 16px;
2510
+ overflow: hidden;
2376
2511
  }
2377
2512
  header {
2378
2513
  display: flex;
@@ -2409,9 +2544,10 @@ var ui_default = `<!doctype html>
2409
2544
 
2410
2545
  .body {
2411
2546
  display: grid;
2412
- grid-template-columns: 1fr 272px;
2413
- gap: 20px;
2414
- align-items: start;
2547
+ grid-template-columns: 360px 1fr 270px;
2548
+ gap: 16px;
2549
+ min-height: 0;
2550
+ overflow: hidden;
2415
2551
  }
2416
2552
 
2417
2553
  .warning-banner {
@@ -2431,6 +2567,10 @@ var ui_default = `<!doctype html>
2431
2567
 
2432
2568
  .main {
2433
2569
  min-width: 0;
2570
+ min-height: 0;
2571
+ overflow-y: auto;
2572
+ scrollbar-width: thin;
2573
+ scrollbar-color: var(--border) transparent;
2434
2574
  }
2435
2575
  .section-title {
2436
2576
  font-size: 11px;
@@ -2461,14 +2601,64 @@ var ui_default = `<!doctype html>
2461
2601
  background: var(--card);
2462
2602
  border: 1px solid var(--border);
2463
2603
  border-radius: 14px;
2464
- padding: 24px;
2465
- margin-bottom: 16px;
2604
+ padding: 20px;
2605
+ margin-bottom: 14px;
2466
2606
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
2467
2607
  animation: pop 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
2468
2608
  }
2469
2609
  .card.slack-viewer {
2470
2610
  border-color: rgba(83, 155, 245, 0.3);
2471
2611
  }
2612
+ .card-header {
2613
+ display: flex;
2614
+ align-items: center;
2615
+ gap: 8px;
2616
+ margin-bottom: 12px;
2617
+ padding-bottom: 12px;
2618
+ border-bottom: 1px solid var(--border);
2619
+ }
2620
+ .card-header-icon {
2621
+ font-size: 16px;
2622
+ }
2623
+ .card-header-title {
2624
+ font-size: 12px;
2625
+ font-weight: 700;
2626
+ color: var(--text-bright);
2627
+ text-transform: uppercase;
2628
+ letter-spacing: 0.5px;
2629
+ }
2630
+ .card-timer {
2631
+ margin-left: auto;
2632
+ font-size: 11px;
2633
+ font-family: 'Fira Code', monospace;
2634
+ color: var(--muted);
2635
+ background: rgba(48, 54, 61, 0.6);
2636
+ padding: 2px 8px;
2637
+ border-radius: 5px;
2638
+ }
2639
+ .card-timer.urgent {
2640
+ color: var(--danger);
2641
+ background: rgba(201, 60, 55, 0.1);
2642
+ }
2643
+ .btn-allow {
2644
+ background: var(--success);
2645
+ color: #fff;
2646
+ grid-column: span 2;
2647
+ font-size: 14px;
2648
+ padding: 13px 14px;
2649
+ }
2650
+ .btn-deny {
2651
+ background: rgba(201, 60, 55, 0.15);
2652
+ color: #e5534b;
2653
+ border: 1px solid rgba(201, 60, 55, 0.3);
2654
+ grid-column: span 2;
2655
+ }
2656
+ .btn-deny:hover:not(:disabled) {
2657
+ background: var(--danger);
2658
+ color: #fff;
2659
+ border-color: transparent;
2660
+ filter: none;
2661
+ }
2472
2662
  @keyframes pop {
2473
2663
  from {
2474
2664
  opacity: 0;
@@ -2676,24 +2866,178 @@ var ui_default = `<!doctype html>
2676
2866
  cursor: not-allowed;
2677
2867
  }
2678
2868
 
2869
+ .flight-col {
2870
+ display: flex;
2871
+ flex-direction: column;
2872
+ min-height: 0;
2873
+ overflow: hidden;
2874
+ }
2875
+ .flight-panel {
2876
+ flex: 1;
2877
+ min-height: 0;
2878
+ display: flex;
2879
+ flex-direction: column;
2880
+ overflow: hidden;
2881
+ }
2679
2882
  .sidebar {
2680
2883
  display: flex;
2681
2884
  flex-direction: column;
2682
2885
  gap: 12px;
2683
- position: sticky;
2684
- top: 24px;
2886
+ min-height: 0;
2887
+ overflow-y: auto;
2888
+ scrollbar-width: thin;
2889
+ scrollbar-color: var(--border) transparent;
2685
2890
  }
2686
2891
  .panel {
2687
2892
  background: var(--panel);
2688
2893
  border: 1px solid var(--border);
2689
2894
  border-radius: 12px;
2690
- padding: 16px;
2895
+ padding: 14px;
2896
+ }
2897
+ /* \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 */
2898
+ #activity-feed {
2899
+ display: flex;
2900
+ flex-direction: column;
2901
+ gap: 4px;
2902
+ margin-top: 4px;
2903
+ flex: 1;
2904
+ min-height: 0;
2905
+ overflow-y: auto;
2906
+ scrollbar-width: thin;
2907
+ scrollbar-color: var(--border) transparent;
2908
+ }
2909
+ .feed-row {
2910
+ display: grid;
2911
+ grid-template-columns: 58px 20px 1fr 48px;
2912
+ align-items: start;
2913
+ gap: 6px;
2914
+ background: rgba(22, 27, 34, 0.6);
2915
+ border: 1px solid var(--border);
2916
+ padding: 7px 10px;
2917
+ border-radius: 7px;
2918
+ font-size: 11px;
2919
+ animation: frSlideIn 0.15s ease-out;
2920
+ transition: background 0.1s;
2921
+ cursor: default;
2922
+ }
2923
+ .feed-row:hover {
2924
+ background: rgba(30, 38, 48, 0.9);
2925
+ border-color: rgba(83, 155, 245, 0.2);
2926
+ }
2927
+ @keyframes frSlideIn {
2928
+ from {
2929
+ opacity: 0;
2930
+ transform: translateX(-4px);
2931
+ }
2932
+ to {
2933
+ opacity: 1;
2934
+ transform: none;
2935
+ }
2936
+ }
2937
+ .feed-ts {
2938
+ color: var(--muted);
2939
+ font-family: monospace;
2940
+ font-size: 9px;
2941
+ }
2942
+ .feed-icon {
2943
+ text-align: center;
2944
+ font-size: 13px;
2945
+ }
2946
+ .feed-content {
2947
+ min-width: 0;
2948
+ color: var(--text-bright);
2949
+ word-break: break-all;
2950
+ }
2951
+ .feed-args {
2952
+ display: block;
2953
+ color: var(--muted);
2954
+ font-family: monospace;
2955
+ margin-top: 2px;
2956
+ font-size: 10px;
2957
+ word-break: break-all;
2958
+ }
2959
+ .feed-badge {
2960
+ text-align: right;
2961
+ font-weight: 700;
2962
+ font-size: 9px;
2963
+ letter-spacing: 0.03em;
2964
+ }
2965
+ .fr-pending {
2966
+ color: var(--muted);
2967
+ }
2968
+ .fr-allow {
2969
+ color: #57ab5a;
2970
+ }
2971
+ .fr-block {
2972
+ color: var(--danger);
2973
+ }
2974
+ .fr-dlp {
2975
+ color: var(--primary);
2976
+ animation: frBlink 1s infinite;
2977
+ }
2978
+ @keyframes frBlink {
2979
+ 50% {
2980
+ opacity: 0.4;
2981
+ }
2982
+ }
2983
+ .fr-dlp-row {
2984
+ border-color: var(--primary) !important;
2985
+ }
2986
+ .feed-clear-btn {
2987
+ background: transparent;
2988
+ border: none;
2989
+ color: var(--muted);
2990
+ font-size: 10px;
2991
+ padding: 0;
2992
+ cursor: pointer;
2993
+ margin-left: auto;
2994
+ font-family: inherit;
2995
+ font-weight: 500;
2996
+ transition: color 0.15s;
2997
+ }
2998
+ .feed-clear-btn:hover {
2999
+ color: var(--text);
3000
+ filter: none;
3001
+ transform: none;
3002
+ }
3003
+ /* \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 */
3004
+ .shield-row {
3005
+ display: flex;
3006
+ align-items: flex-start;
3007
+ gap: 10px;
3008
+ padding: 8px 0;
3009
+ border-bottom: 1px solid var(--border);
3010
+ }
3011
+ .shield-row:last-child {
3012
+ border-bottom: none;
3013
+ padding-bottom: 0;
2691
3014
  }
3015
+ .shield-row:first-child {
3016
+ padding-top: 0;
3017
+ }
3018
+ .shield-info {
3019
+ flex: 1;
3020
+ min-width: 0;
3021
+ }
3022
+ .shield-name {
3023
+ font-size: 12px;
3024
+ color: var(--text-bright);
3025
+ font-weight: 600;
3026
+ font-family: 'Fira Code', monospace;
3027
+ }
3028
+ .shield-desc {
3029
+ font-size: 10px;
3030
+ color: var(--muted);
3031
+ margin-top: 2px;
3032
+ line-height: 1.4;
3033
+ }
3034
+
2692
3035
  .panel-title {
2693
3036
  font-size: 12px;
2694
3037
  font-weight: 700;
2695
3038
  color: var(--text-bright);
2696
3039
  margin-bottom: 12px;
3040
+ flex-shrink: 0;
2697
3041
  display: flex;
2698
3042
  align-items: center;
2699
3043
  gap: 6px;
@@ -2701,8 +3045,8 @@ var ui_default = `<!doctype html>
2701
3045
  .setting-row {
2702
3046
  display: flex;
2703
3047
  align-items: flex-start;
2704
- gap: 12px;
2705
- margin-bottom: 12px;
3048
+ gap: 10px;
3049
+ margin-bottom: 8px;
2706
3050
  }
2707
3051
  .setting-row:last-child {
2708
3052
  margin-bottom: 0;
@@ -2711,20 +3055,21 @@ var ui_default = `<!doctype html>
2711
3055
  flex: 1;
2712
3056
  }
2713
3057
  .setting-label {
2714
- font-size: 12px;
3058
+ font-size: 11px;
2715
3059
  color: var(--text-bright);
2716
- margin-bottom: 3px;
3060
+ margin-bottom: 2px;
3061
+ font-weight: 600;
2717
3062
  }
2718
3063
  .setting-desc {
2719
- font-size: 11px;
3064
+ font-size: 10px;
2720
3065
  color: var(--muted);
2721
- line-height: 1.5;
3066
+ line-height: 1.4;
2722
3067
  }
2723
3068
  .toggle {
2724
3069
  position: relative;
2725
3070
  display: inline-block;
2726
- width: 40px;
2727
- height: 22px;
3071
+ width: 34px;
3072
+ height: 19px;
2728
3073
  flex-shrink: 0;
2729
3074
  margin-top: 1px;
2730
3075
  }
@@ -2744,8 +3089,8 @@ var ui_default = `<!doctype html>
2744
3089
  .slider:before {
2745
3090
  content: '';
2746
3091
  position: absolute;
2747
- width: 16px;
2748
- height: 16px;
3092
+ width: 13px;
3093
+ height: 13px;
2749
3094
  left: 3px;
2750
3095
  bottom: 3px;
2751
3096
  background: #fff;
@@ -2756,7 +3101,7 @@ var ui_default = `<!doctype html>
2756
3101
  background: var(--success);
2757
3102
  }
2758
3103
  input:checked + .slider:before {
2759
- transform: translateX(18px);
3104
+ transform: translateX(15px);
2760
3105
  }
2761
3106
  input:disabled + .slider {
2762
3107
  opacity: 0.4;
@@ -2915,12 +3260,17 @@ var ui_default = `<!doctype html>
2915
3260
  border: 1px solid var(--border);
2916
3261
  }
2917
3262
 
2918
- @media (max-width: 680px) {
3263
+ @media (max-width: 960px) {
2919
3264
  .body {
2920
- grid-template-columns: 1fr;
3265
+ grid-template-columns: 1fr 220px;
3266
+ }
3267
+ .flight-col {
3268
+ display: none;
2921
3269
  }
2922
- .sidebar {
2923
- position: static;
3270
+ }
3271
+ @media (max-width: 640px) {
3272
+ .body {
3273
+ grid-template-columns: 1fr;
2924
3274
  }
2925
3275
  }
2926
3276
  </style>
@@ -2934,6 +3284,19 @@ var ui_default = `<!doctype html>
2934
3284
  </header>
2935
3285
 
2936
3286
  <div class="body">
3287
+ <div class="flight-col">
3288
+ <div class="panel flight-panel">
3289
+ <div class="panel-title">
3290
+ \u{1F6F0}\uFE0F Flight Recorder
3291
+ <span style="font-weight: 400; color: var(--muted); font-size: 11px">live</span>
3292
+ <button class="feed-clear-btn" onclick="clearFeed()">clear</button>
3293
+ </div>
3294
+ <div id="activity-feed">
3295
+ <span class="decisions-empty">Waiting for agent activity\u2026</span>
3296
+ </div>
3297
+ </div>
3298
+ </div>
3299
+
2937
3300
  <div class="main">
2938
3301
  <div id="warnBanner" class="warning-banner">
2939
3302
  \u26A0\uFE0F Auto-start is off \u2014 daemon started manually. Run "node9 daemon stop" to stop it, or
@@ -3014,6 +3377,11 @@ var ui_default = `<!doctype html>
3014
3377
  <div id="slackStatusLine" class="slack-status-line">No key saved</div>
3015
3378
  </div>
3016
3379
 
3380
+ <div class="panel">
3381
+ <div class="panel-title">\u{1F6E1}\uFE0F Active Shields</div>
3382
+ <div id="shieldsList"><span class="decisions-empty">Loading\u2026</span></div>
3383
+ </div>
3384
+
3017
3385
  <div class="panel">
3018
3386
  <div class="panel-title">\u{1F4CB} Persistent Decisions</div>
3019
3387
  <div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
@@ -3059,14 +3427,23 @@ var ui_default = `<!doctype html>
3059
3427
 
3060
3428
  function updateDenyButton(id, timestamp) {
3061
3429
  const btn = document.querySelector('#c-' + id + ' .btn-deny');
3430
+ const timer = document.querySelector('#timer-' + id);
3062
3431
  if (!btn) return;
3063
3432
  const elapsed = Date.now() - timestamp;
3064
3433
  const remaining = Math.max(0, Math.ceil((autoDenyMs - elapsed) / 1000));
3065
3434
  if (remaining <= 0) {
3066
- btn.textContent = 'Auto-Denying...';
3435
+ btn.textContent = '\u23F3 Auto-Denying\u2026';
3067
3436
  btn.disabled = true;
3437
+ if (timer) {
3438
+ timer.textContent = 'auto-deny';
3439
+ timer.className = 'card-timer urgent';
3440
+ }
3068
3441
  } else {
3069
- btn.textContent = 'Block Action (' + remaining + 's)';
3442
+ btn.textContent = '\u{1F6AB} Block this Action';
3443
+ if (timer) {
3444
+ timer.textContent = remaining + 's';
3445
+ timer.className = 'card-timer' + (remaining < 15 ? ' urgent' : '');
3446
+ }
3070
3447
  setTimeout(() => updateDenyButton(id, timestamp), 1000);
3071
3448
  }
3072
3449
  }
@@ -3082,34 +3459,61 @@ var ui_default = `<!doctype html>
3082
3459
  empty.style.display = requests.size === 0 ? 'block' : 'none';
3083
3460
  }
3084
3461
 
3085
- function sendDecision(id, decision, persist) {
3462
+ function setCardBusy(card, busy) {
3463
+ if (!card) return;
3464
+ card.querySelectorAll('button').forEach((b) => (b.disabled = busy));
3465
+ card.style.opacity = busy ? '0.5' : '1';
3466
+ }
3467
+
3468
+ function showCardError(card, msg) {
3469
+ if (!card) return;
3470
+ card.style.outline = '2px solid #f87171';
3471
+ let err = card.querySelector('.card-error');
3472
+ if (!err) {
3473
+ err = document.createElement('p');
3474
+ err.className = 'card-error';
3475
+ err.style.cssText = 'color:#f87171;font-size:11px;margin:6px 0 0;';
3476
+ card.appendChild(err);
3477
+ }
3478
+ err.textContent = '\u26A0 ' + msg + ' \u2014 please try again or refresh.';
3479
+ }
3480
+
3481
+ async function sendDecision(id, decision, persist) {
3086
3482
  const card = document.getElementById('c-' + id);
3087
- if (card) card.style.opacity = '0.5';
3088
- fetch('/decision/' + id, {
3089
- method: 'POST',
3090
- headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3091
- body: JSON.stringify({ decision, persist: !!persist }),
3092
- });
3093
- setTimeout(() => {
3483
+ setCardBusy(card, true);
3484
+ try {
3485
+ const res = await fetch('/decision/' + id, {
3486
+ method: 'POST',
3487
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3488
+ body: JSON.stringify({ decision, persist: !!persist }),
3489
+ });
3490
+ if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3094
3491
  card?.remove();
3095
3492
  requests.delete(id);
3096
3493
  refresh();
3097
- }, 200);
3494
+ } catch (err) {
3495
+ setCardBusy(card, false);
3496
+ showCardError(card, err.message || 'Network error');
3497
+ }
3098
3498
  }
3099
3499
 
3100
- function sendTrust(id, duration) {
3500
+ async function sendTrust(id, duration) {
3101
3501
  const card = document.getElementById('c-' + id);
3102
- if (card) card.style.opacity = '0.5';
3103
- fetch('/decision/' + id, {
3104
- method: 'POST',
3105
- headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3106
- body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
3107
- });
3108
- setTimeout(() => {
3502
+ setCardBusy(card, true);
3503
+ try {
3504
+ const res = await fetch('/decision/' + id, {
3505
+ method: 'POST',
3506
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3507
+ body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
3508
+ });
3509
+ if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3109
3510
  card?.remove();
3110
3511
  requests.delete(id);
3111
3512
  refresh();
3112
- }, 200);
3513
+ } catch (err) {
3514
+ setCardBusy(card, false);
3515
+ showCardError(card, err.message || 'Network error');
3516
+ }
3113
3517
  }
3114
3518
 
3115
3519
  function renderPayload(req) {
@@ -3160,16 +3564,21 @@ var ui_default = `<!doctype html>
3160
3564
  const mcpLabel = req.mcpServer ? esc(req.mcpServer) : null;
3161
3565
  const dis = isSlack ? 'disabled' : '';
3162
3566
  card.innerHTML = \`
3567
+ <div class="card-header">
3568
+ <span class="card-header-icon">\${isSlack ? '\u26A1' : '\u26A0\uFE0F'}</span>
3569
+ <span class="card-header-title">\${isSlack ? 'Awaiting Cloud Approval' : 'Action Required'}</span>
3570
+ <span class="card-timer" id="timer-\${req.id}">\${autoDenyMs > 0 ? Math.ceil(autoDenyMs / 1000) + 's' : ''}</span>
3571
+ </div>
3163
3572
  <div class="source-row">
3164
3573
  <span class="agent-badge">\${agentLabel}</span>
3165
3574
  \${mcpLabel ? \`<span class="source-arrow">\u2192</span><span class="mcp-badge">mcp::\${mcpLabel}</span>\` : ''}
3166
3575
  </div>
3167
3576
  <div class="tool-chip">\${esc(req.toolName)}</div>
3168
- \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Slack approval \u2014 view only</div>' : ''}
3577
+ \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
3169
3578
  \${renderPayload(req)}
3170
3579
  <div class="actions" id="act-\${req.id}">
3171
- <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>Approve Execution</button>
3172
- <button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>Block Action</button>
3580
+ <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
3581
+ <button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>\u{1F6AB} Block this Action</button>
3173
3582
  <div class="trust-row\${trustEnabled ? ' show' : ''}" id="tr-\${req.id}">
3174
3583
  <button class="btn-trust" onclick="sendTrust('\${req.id}','30m')" \${dis}>\u23F1 Trust 30m</button>
3175
3584
  <button class="btn-trust" onclick="sendTrust('\${req.id}','1h')" \${dis}>\u23F1 Trust 1h</button>
@@ -3229,11 +3638,86 @@ var ui_default = `<!doctype html>
3229
3638
  ev.addEventListener('slack-status', (e) => {
3230
3639
  applySlackStatus(JSON.parse(e.data));
3231
3640
  });
3232
- }
3233
- connect();
3641
+ ev.addEventListener('shields-status', (e) => {
3642
+ renderShields(JSON.parse(e.data).shields);
3643
+ });
3234
3644
 
3235
- function saveSetting(key, value) {
3236
- fetch('/settings', {
3645
+ // \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
3646
+ ev.addEventListener('activity', (e) => {
3647
+ const data = JSON.parse(e.data);
3648
+ const feed = document.getElementById('activity-feed');
3649
+ // Remove placeholder on first item
3650
+ const placeholder = feed.querySelector('.decisions-empty');
3651
+ if (placeholder) placeholder.remove();
3652
+
3653
+ const time = new Date(data.ts).toLocaleTimeString([], {
3654
+ hour12: false,
3655
+ hour: '2-digit',
3656
+ minute: '2-digit',
3657
+ second: '2-digit',
3658
+ });
3659
+ const icon = frIcon(data.tool);
3660
+ const argsStr = JSON.stringify(data.args ?? {});
3661
+ const argsPreview = esc(argsStr.length > 120 ? argsStr.slice(0, 120) + '\u2026' : argsStr);
3662
+
3663
+ const row = document.createElement('div');
3664
+ row.className = 'feed-row';
3665
+ row.id = 'fr-' + data.id;
3666
+ row.innerHTML = \`
3667
+ <span class="feed-ts">\${time}</span>
3668
+ <span class="feed-icon">\${icon}</span>
3669
+ <span class="feed-content"><strong>\${esc(data.tool)}</strong><span class="feed-args">\${argsPreview}</span></span>
3670
+ <span class="feed-badge fr-pending">\u25CF</span>
3671
+ \`;
3672
+ feed.prepend(row);
3673
+ if (feed.children.length > 100) feed.lastChild.remove();
3674
+ });
3675
+
3676
+ ev.addEventListener('activity-result', (e) => {
3677
+ const { id, status, label } = JSON.parse(e.data);
3678
+ const row = document.getElementById('fr-' + id);
3679
+ if (!row) return;
3680
+ const badge = row.querySelector('.feed-badge');
3681
+ if (status === 'allow') {
3682
+ badge.textContent = 'ALLOW';
3683
+ badge.className = 'feed-badge fr-allow';
3684
+ } else if (status === 'dlp') {
3685
+ badge.textContent = '\u{1F6E1}\uFE0F DLP';
3686
+ badge.className = 'feed-badge fr-dlp';
3687
+ row.classList.add('fr-dlp-row');
3688
+ } else {
3689
+ badge.textContent = 'BLOCK';
3690
+ badge.className = 'feed-badge fr-block';
3691
+ }
3692
+ });
3693
+ }
3694
+ connect();
3695
+
3696
+ const FR_ICONS = {
3697
+ bash: '\u{1F4BB}',
3698
+ read: '\u{1F4D6}',
3699
+ edit: '\u270F\uFE0F',
3700
+ write: '\u270F\uFE0F',
3701
+ glob: '\u{1F4C2}',
3702
+ grep: '\u{1F50D}',
3703
+ agent: '\u{1F916}',
3704
+ search: '\u{1F50D}',
3705
+ sql: '\u{1F5C4}\uFE0F',
3706
+ query: '\u{1F5C4}\uFE0F',
3707
+ list: '\u{1F4C2}',
3708
+ delete: '\u{1F5D1}\uFE0F',
3709
+ web: '\u{1F310}',
3710
+ };
3711
+ function frIcon(tool) {
3712
+ const t = (tool || '').toLowerCase();
3713
+ for (const [k, v] of Object.entries(FR_ICONS)) {
3714
+ if (t.includes(k)) return v;
3715
+ }
3716
+ return '\u{1F6E0}\uFE0F';
3717
+ }
3718
+
3719
+ function saveSetting(key, value) {
3720
+ fetch('/settings', {
3237
3721
  method: 'POST',
3238
3722
  headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3239
3723
  body: JSON.stringify({ [key]: value }),
@@ -3321,6 +3805,49 @@ var ui_default = `<!doctype html>
3321
3805
  }
3322
3806
  }
3323
3807
 
3808
+ function clearFeed() {
3809
+ const feed = document.getElementById('activity-feed');
3810
+ feed.innerHTML = '<span class="decisions-empty">Feed cleared.</span>';
3811
+ }
3812
+
3813
+ function renderShields(shields) {
3814
+ const list = document.getElementById('shieldsList');
3815
+ if (!shields || shields.length === 0) {
3816
+ list.innerHTML = '<span class="decisions-empty">No shields available.</span>';
3817
+ return;
3818
+ }
3819
+ list.innerHTML = shields
3820
+ .map(
3821
+ (s) => \`
3822
+ <div class="shield-row">
3823
+ <div class="shield-info">
3824
+ <div class="shield-name">\${esc(s.name)}</div>
3825
+ <div class="shield-desc">\${esc(s.description)}</div>
3826
+ </div>
3827
+ <label class="toggle">
3828
+ <input type="checkbox" \${s.active ? 'checked' : ''}
3829
+ onchange="toggleShield('\${esc(s.name)}', this.checked)" />
3830
+ <span class="slider"></span>
3831
+ </label>
3832
+ </div>
3833
+ \`
3834
+ )
3835
+ .join('');
3836
+ }
3837
+
3838
+ function toggleShield(name, active) {
3839
+ fetch('/shields', {
3840
+ method: 'POST',
3841
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3842
+ body: JSON.stringify({ name, active }),
3843
+ }).catch(() => {});
3844
+ }
3845
+
3846
+ fetch('/shields')
3847
+ .then((r) => r.json())
3848
+ .then(({ shields }) => renderShields(shields))
3849
+ .catch(() => {});
3850
+
3324
3851
  function renderDecisions(decisions) {
3325
3852
  const dl = document.getElementById('decisionsList');
3326
3853
  const entries = Object.entries(decisions);
@@ -3367,40 +3894,41 @@ var ui_default = `<!doctype html>
3367
3894
  </body>
3368
3895
  </html>
3369
3896
  `;
3897
+ }
3898
+ });
3370
3899
 
3371
3900
  // src/daemon/ui.ts
3372
- var UI_HTML_TEMPLATE = ui_default;
3901
+ var UI_HTML_TEMPLATE;
3902
+ var init_ui2 = __esm({
3903
+ "src/daemon/ui.ts"() {
3904
+ "use strict";
3905
+ init_ui();
3906
+ UI_HTML_TEMPLATE = ui_default;
3907
+ }
3908
+ });
3373
3909
 
3374
3910
  // src/daemon/index.ts
3375
3911
  import http from "http";
3376
- import fs3 from "fs";
3377
- import path5 from "path";
3378
- import os3 from "os";
3912
+ import net2 from "net";
3913
+ import fs4 from "fs";
3914
+ import path6 from "path";
3915
+ import os4 from "os";
3379
3916
  import { spawn as spawn2 } from "child_process";
3380
- import { randomUUID } from "crypto";
3917
+ import { randomUUID as randomUUID2 } from "crypto";
3381
3918
  import chalk4 from "chalk";
3382
- var DAEMON_PORT2 = 7391;
3383
- var DAEMON_HOST2 = "127.0.0.1";
3384
- var homeDir = os3.homedir();
3385
- var DAEMON_PID_FILE = path5.join(homeDir, ".node9", "daemon.pid");
3386
- var DECISIONS_FILE = path5.join(homeDir, ".node9", "decisions.json");
3387
- var GLOBAL_CONFIG_FILE = path5.join(homeDir, ".node9", "config.json");
3388
- var CREDENTIALS_FILE = path5.join(homeDir, ".node9", "credentials.json");
3389
- var AUDIT_LOG_FILE = path5.join(homeDir, ".node9", "audit.log");
3390
- var TRUST_FILE2 = path5.join(homeDir, ".node9", "trust.json");
3391
3919
  function atomicWriteSync2(filePath, data, options) {
3392
- const dir = path5.dirname(filePath);
3393
- if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
3394
- const tmpPath = `${filePath}.${randomUUID()}.tmp`;
3395
- fs3.writeFileSync(tmpPath, data, options);
3396
- fs3.renameSync(tmpPath, filePath);
3920
+ const dir = path6.dirname(filePath);
3921
+ if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
3922
+ const tmpPath = `${filePath}.${randomUUID2()}.tmp`;
3923
+ fs4.writeFileSync(tmpPath, data, options);
3924
+ fs4.renameSync(tmpPath, filePath);
3397
3925
  }
3398
3926
  function writeTrustEntry(toolName, durationMs) {
3399
3927
  try {
3400
3928
  let trust = { entries: [] };
3401
3929
  try {
3402
- if (fs3.existsSync(TRUST_FILE2))
3403
- trust = JSON.parse(fs3.readFileSync(TRUST_FILE2, "utf-8"));
3930
+ if (fs4.existsSync(TRUST_FILE2))
3931
+ trust = JSON.parse(fs4.readFileSync(TRUST_FILE2, "utf-8"));
3404
3932
  } catch {
3405
3933
  }
3406
3934
  trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
@@ -3409,12 +3937,6 @@ function writeTrustEntry(toolName, durationMs) {
3409
3937
  } catch {
3410
3938
  }
3411
3939
  }
3412
- var TRUST_DURATIONS = {
3413
- "30m": 30 * 6e4,
3414
- "1h": 60 * 6e4,
3415
- "2h": 2 * 60 * 6e4
3416
- };
3417
- var SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
3418
3940
  function redactArgs(value) {
3419
3941
  if (!value || typeof value !== "object") return value;
3420
3942
  if (Array.isArray(value)) return value.map(redactArgs);
@@ -3433,41 +3955,39 @@ function appendAuditLog(data) {
3433
3955
  decision: data.decision,
3434
3956
  source: "daemon"
3435
3957
  };
3436
- const dir = path5.dirname(AUDIT_LOG_FILE);
3437
- if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
3438
- fs3.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
3958
+ const dir = path6.dirname(AUDIT_LOG_FILE);
3959
+ if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
3960
+ fs4.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
3439
3961
  } catch {
3440
3962
  }
3441
3963
  }
3442
3964
  function getAuditHistory(limit = 20) {
3443
3965
  try {
3444
- if (!fs3.existsSync(AUDIT_LOG_FILE)) return [];
3445
- const lines = fs3.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
3966
+ if (!fs4.existsSync(AUDIT_LOG_FILE)) return [];
3967
+ const lines = fs4.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
3446
3968
  if (lines.length === 1 && lines[0] === "") return [];
3447
3969
  return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
3448
3970
  } catch {
3449
3971
  return [];
3450
3972
  }
3451
3973
  }
3452
- var AUTO_DENY_MS = 12e4;
3453
3974
  function getOrgName() {
3454
3975
  try {
3455
- if (fs3.existsSync(CREDENTIALS_FILE)) {
3976
+ if (fs4.existsSync(CREDENTIALS_FILE)) {
3456
3977
  return "Node9 Cloud";
3457
3978
  }
3458
3979
  } catch {
3459
3980
  }
3460
3981
  return null;
3461
3982
  }
3462
- var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
3463
3983
  function hasStoredSlackKey() {
3464
- return fs3.existsSync(CREDENTIALS_FILE);
3984
+ return fs4.existsSync(CREDENTIALS_FILE);
3465
3985
  }
3466
3986
  function writeGlobalSetting(key, value) {
3467
3987
  let config = {};
3468
3988
  try {
3469
- if (fs3.existsSync(GLOBAL_CONFIG_FILE)) {
3470
- config = JSON.parse(fs3.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
3989
+ if (fs4.existsSync(GLOBAL_CONFIG_FILE)) {
3990
+ config = JSON.parse(fs4.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
3471
3991
  }
3472
3992
  } catch {
3473
3993
  }
@@ -3475,11 +3995,6 @@ function writeGlobalSetting(key, value) {
3475
3995
  config.settings[key] = value;
3476
3996
  atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
3477
3997
  }
3478
- var pending = /* @__PURE__ */ new Map();
3479
- var sseClients = /* @__PURE__ */ new Set();
3480
- var abandonTimer = null;
3481
- var daemonServer = null;
3482
- var hadBrowserClient = false;
3483
3998
  function abandonPending() {
3484
3999
  abandonTimer = null;
3485
4000
  pending.forEach((entry, id) => {
@@ -3491,7 +4006,7 @@ function abandonPending() {
3491
4006
  });
3492
4007
  if (autoStarted) {
3493
4008
  try {
3494
- fs3.unlinkSync(DAEMON_PID_FILE);
4009
+ fs4.unlinkSync(DAEMON_PID_FILE);
3495
4010
  } catch {
3496
4011
  }
3497
4012
  setTimeout(() => {
@@ -3501,6 +4016,18 @@ function abandonPending() {
3501
4016
  }
3502
4017
  }
3503
4018
  function broadcast(event, data) {
4019
+ if (event === "activity") {
4020
+ activityRing.push({ event, data });
4021
+ if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
4022
+ } else if (event === "activity-result") {
4023
+ const { id, status, label } = data;
4024
+ for (let i = activityRing.length - 1; i >= 0; i--) {
4025
+ if (activityRing[i].data.id === id) {
4026
+ Object.assign(activityRing[i].data, { status, label });
4027
+ break;
4028
+ }
4029
+ }
4030
+ }
3504
4031
  const msg = `event: ${event}
3505
4032
  data: ${JSON.stringify(data)}
3506
4033
 
@@ -3529,8 +4056,8 @@ function readBody(req) {
3529
4056
  }
3530
4057
  function readPersistentDecisions() {
3531
4058
  try {
3532
- if (fs3.existsSync(DECISIONS_FILE)) {
3533
- return JSON.parse(fs3.readFileSync(DECISIONS_FILE, "utf-8"));
4059
+ if (fs4.existsSync(DECISIONS_FILE)) {
4060
+ return JSON.parse(fs4.readFileSync(DECISIONS_FILE, "utf-8"));
3534
4061
  }
3535
4062
  } catch {
3536
4063
  }
@@ -3546,18 +4073,20 @@ function writePersistentDecision(toolName, decision) {
3546
4073
  }
3547
4074
  }
3548
4075
  function startDaemon() {
3549
- const csrfToken = randomUUID();
3550
- const internalToken = randomUUID();
4076
+ const csrfToken = randomUUID2();
4077
+ const internalToken = randomUUID2();
3551
4078
  const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
3552
4079
  const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
3553
4080
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
4081
+ const watchMode = process.env.NODE9_WATCH_MODE === "1";
3554
4082
  let idleTimer;
3555
4083
  function resetIdleTimer() {
4084
+ if (watchMode) return;
3556
4085
  if (idleTimer) clearTimeout(idleTimer);
3557
4086
  idleTimer = setTimeout(() => {
3558
4087
  if (autoStarted) {
3559
4088
  try {
3560
- fs3.unlinkSync(DAEMON_PID_FILE);
4089
+ fs4.unlinkSync(DAEMON_PID_FILE);
3561
4090
  } catch {
3562
4091
  }
3563
4092
  }
@@ -3607,6 +4136,12 @@ data: ${JSON.stringify({
3607
4136
  data: ${JSON.stringify(readPersistentDecisions())}
3608
4137
 
3609
4138
  `);
4139
+ for (const item of activityRing) {
4140
+ res.write(`event: ${item.event}
4141
+ data: ${JSON.stringify(item.data)}
4142
+
4143
+ `);
4144
+ }
3610
4145
  return req.on("close", () => {
3611
4146
  sseClients.delete(res);
3612
4147
  if (sseClients.size === 0 && pending.size > 0) {
@@ -3626,9 +4161,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
3626
4161
  slackDelegated = false,
3627
4162
  agent,
3628
4163
  mcpServer,
3629
- riskMetadata
4164
+ riskMetadata,
4165
+ fromCLI = false,
4166
+ activityId
3630
4167
  } = JSON.parse(body);
3631
- const id = randomUUID();
4168
+ const id = fromCLI && typeof activityId === "string" && activityId || randomUUID2();
3632
4169
  const entry = {
3633
4170
  id,
3634
4171
  toolName,
@@ -3659,6 +4196,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
3659
4196
  }, AUTO_DENY_MS)
3660
4197
  };
3661
4198
  pending.set(id, entry);
4199
+ if (!fromCLI) {
4200
+ broadcast("activity", {
4201
+ id,
4202
+ ts: entry.timestamp,
4203
+ tool: toolName,
4204
+ args: redactArgs(args),
4205
+ status: "pending"
4206
+ });
4207
+ }
3662
4208
  const browserEnabled = getConfig().settings.approvers?.browser !== false;
3663
4209
  if (browserEnabled) {
3664
4210
  broadcast("add", {
@@ -3688,6 +4234,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
3688
4234
  const e = pending.get(id);
3689
4235
  if (!e) return;
3690
4236
  if (result.noApprovalMechanism) return;
4237
+ broadcast("activity-result", {
4238
+ id,
4239
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
4240
+ label: result.blockedByLabel
4241
+ });
3691
4242
  clearTimeout(e.timer);
3692
4243
  const decision = result.approved ? "allow" : "deny";
3693
4244
  appendAuditLog({ toolName: e.toolName, args: e.args, decision });
@@ -3722,8 +4273,8 @@ data: ${JSON.stringify(readPersistentDecisions())}
3722
4273
  const entry = pending.get(id);
3723
4274
  if (!entry) return res.writeHead(404).end();
3724
4275
  if (entry.earlyDecision) {
4276
+ clearTimeout(entry.timer);
3725
4277
  pending.delete(id);
3726
- broadcast("remove", { id });
3727
4278
  res.writeHead(200, { "Content-Type": "application/json" });
3728
4279
  const body = { decision: entry.earlyDecision };
3729
4280
  if (entry.earlyReason) body.reason = entry.earlyReason;
@@ -3753,10 +4304,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
3753
4304
  decision: `trust:${trustDuration}`
3754
4305
  });
3755
4306
  clearTimeout(entry.timer);
3756
- if (entry.waiter) entry.waiter("allow");
3757
- else entry.earlyDecision = "allow";
3758
- pending.delete(id);
3759
- broadcast("remove", { id });
4307
+ if (entry.waiter) {
4308
+ entry.waiter("allow");
4309
+ pending.delete(id);
4310
+ broadcast("remove", { id });
4311
+ } else {
4312
+ entry.earlyDecision = "allow";
4313
+ broadcast("remove", { id });
4314
+ entry.timer = setTimeout(() => pending.delete(id), 3e4);
4315
+ }
3760
4316
  res.writeHead(200);
3761
4317
  return res.end(JSON.stringify({ ok: true }));
3762
4318
  }
@@ -3768,13 +4324,16 @@ data: ${JSON.stringify(readPersistentDecisions())}
3768
4324
  decision: resolvedDecision
3769
4325
  });
3770
4326
  clearTimeout(entry.timer);
3771
- if (entry.waiter) entry.waiter(resolvedDecision, reason);
3772
- else {
4327
+ if (entry.waiter) {
4328
+ entry.waiter(resolvedDecision, reason);
4329
+ pending.delete(id);
4330
+ broadcast("remove", { id });
4331
+ } else {
3773
4332
  entry.earlyDecision = resolvedDecision;
3774
4333
  entry.earlyReason = reason;
4334
+ broadcast("remove", { id });
4335
+ entry.timer = setTimeout(() => pending.delete(id), 3e4);
3775
4336
  }
3776
- pending.delete(id);
3777
- broadcast("remove", { id });
3778
4337
  res.writeHead(200);
3779
4338
  return res.end(JSON.stringify({ ok: true }));
3780
4339
  } catch {
@@ -3865,99 +4424,658 @@ data: ${JSON.stringify(readPersistentDecisions())}
3865
4424
  res.writeHead(400).end();
3866
4425
  }
3867
4426
  }
3868
- if (req.method === "GET" && pathname === "/audit") {
3869
- res.writeHead(200, { "Content-Type": "application/json" });
3870
- return res.end(JSON.stringify(getAuditHistory()));
4427
+ if (req.method === "GET" && pathname === "/audit") {
4428
+ res.writeHead(200, { "Content-Type": "application/json" });
4429
+ return res.end(JSON.stringify(getAuditHistory()));
4430
+ }
4431
+ if (req.method === "GET" && pathname === "/shields") {
4432
+ if (!validToken(req)) return res.writeHead(403).end();
4433
+ const active = readActiveShields();
4434
+ const shields = Object.values(SHIELDS).map((s) => ({
4435
+ name: s.name,
4436
+ description: s.description,
4437
+ active: active.includes(s.name)
4438
+ }));
4439
+ res.writeHead(200, { "Content-Type": "application/json" });
4440
+ return res.end(JSON.stringify({ shields }));
4441
+ }
4442
+ if (req.method === "POST" && pathname === "/shields") {
4443
+ if (!validToken(req)) return res.writeHead(403).end();
4444
+ try {
4445
+ const { name, active } = JSON.parse(await readBody(req));
4446
+ if (!SHIELDS[name]) return res.writeHead(400).end();
4447
+ const current = readActiveShields();
4448
+ const updated = active ? [.../* @__PURE__ */ new Set([...current, name])] : current.filter((n) => n !== name);
4449
+ writeActiveShields(updated);
4450
+ _resetConfigCache();
4451
+ const shieldsPayload = Object.values(SHIELDS).map((s) => ({
4452
+ name: s.name,
4453
+ description: s.description,
4454
+ active: updated.includes(s.name)
4455
+ }));
4456
+ broadcast("shields-status", { shields: shieldsPayload });
4457
+ res.writeHead(200);
4458
+ return res.end(JSON.stringify({ ok: true }));
4459
+ } catch {
4460
+ res.writeHead(400).end();
4461
+ }
4462
+ }
4463
+ res.writeHead(404).end();
4464
+ });
4465
+ daemonServer = server;
4466
+ server.on("error", (e) => {
4467
+ if (e.code === "EADDRINUSE") {
4468
+ try {
4469
+ if (fs4.existsSync(DAEMON_PID_FILE)) {
4470
+ const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4471
+ process.kill(pid, 0);
4472
+ return process.exit(0);
4473
+ }
4474
+ } catch {
4475
+ try {
4476
+ fs4.unlinkSync(DAEMON_PID_FILE);
4477
+ } catch {
4478
+ }
4479
+ server.listen(DAEMON_PORT2, DAEMON_HOST2);
4480
+ return;
4481
+ }
4482
+ }
4483
+ console.error(chalk4.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
4484
+ process.exit(1);
4485
+ });
4486
+ server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
4487
+ atomicWriteSync2(
4488
+ DAEMON_PID_FILE,
4489
+ JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
4490
+ { mode: 384 }
4491
+ );
4492
+ console.log(chalk4.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
4493
+ });
4494
+ if (watchMode) {
4495
+ console.log(chalk4.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
4496
+ }
4497
+ try {
4498
+ fs4.unlinkSync(ACTIVITY_SOCKET_PATH2);
4499
+ } catch {
4500
+ }
4501
+ const ACTIVITY_MAX_BYTES = 1024 * 1024;
4502
+ const unixServer = net2.createServer((socket) => {
4503
+ const chunks = [];
4504
+ let bytesReceived = 0;
4505
+ socket.on("data", (chunk) => {
4506
+ bytesReceived += chunk.length;
4507
+ if (bytesReceived > ACTIVITY_MAX_BYTES) {
4508
+ socket.destroy();
4509
+ return;
4510
+ }
4511
+ chunks.push(chunk);
4512
+ });
4513
+ socket.on("end", () => {
4514
+ try {
4515
+ const data = JSON.parse(Buffer.concat(chunks).toString());
4516
+ if (data.status === "pending") {
4517
+ broadcast("activity", {
4518
+ id: data.id,
4519
+ ts: data.ts,
4520
+ tool: data.tool,
4521
+ args: redactArgs(data.args),
4522
+ status: "pending"
4523
+ });
4524
+ } else {
4525
+ broadcast("activity-result", {
4526
+ id: data.id,
4527
+ status: data.status,
4528
+ label: data.label
4529
+ });
4530
+ }
4531
+ } catch {
4532
+ }
4533
+ });
4534
+ socket.on("error", () => {
4535
+ });
4536
+ });
4537
+ unixServer.listen(ACTIVITY_SOCKET_PATH2);
4538
+ process.on("exit", () => {
4539
+ try {
4540
+ fs4.unlinkSync(ACTIVITY_SOCKET_PATH2);
4541
+ } catch {
4542
+ }
4543
+ });
4544
+ }
4545
+ function stopDaemon() {
4546
+ if (!fs4.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
4547
+ try {
4548
+ const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4549
+ process.kill(pid, "SIGTERM");
4550
+ console.log(chalk4.green("\u2705 Stopped."));
4551
+ } catch {
4552
+ console.log(chalk4.gray("Cleaned up stale PID file."));
4553
+ } finally {
4554
+ try {
4555
+ fs4.unlinkSync(DAEMON_PID_FILE);
4556
+ } catch {
4557
+ }
4558
+ }
4559
+ }
4560
+ function daemonStatus() {
4561
+ if (!fs4.existsSync(DAEMON_PID_FILE))
4562
+ return console.log(chalk4.yellow("Node9 daemon: not running"));
4563
+ try {
4564
+ const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4565
+ process.kill(pid, 0);
4566
+ console.log(chalk4.green("Node9 daemon: running"));
4567
+ } catch {
4568
+ console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
4569
+ }
4570
+ }
4571
+ var 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;
4572
+ var init_daemon = __esm({
4573
+ "src/daemon/index.ts"() {
4574
+ "use strict";
4575
+ init_ui2();
4576
+ init_core();
4577
+ init_shields();
4578
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path6.join(os4.tmpdir(), "node9-activity.sock");
4579
+ DAEMON_PORT2 = 7391;
4580
+ DAEMON_HOST2 = "127.0.0.1";
4581
+ homeDir = os4.homedir();
4582
+ DAEMON_PID_FILE = path6.join(homeDir, ".node9", "daemon.pid");
4583
+ DECISIONS_FILE = path6.join(homeDir, ".node9", "decisions.json");
4584
+ GLOBAL_CONFIG_FILE = path6.join(homeDir, ".node9", "config.json");
4585
+ CREDENTIALS_FILE = path6.join(homeDir, ".node9", "credentials.json");
4586
+ AUDIT_LOG_FILE = path6.join(homeDir, ".node9", "audit.log");
4587
+ TRUST_FILE2 = path6.join(homeDir, ".node9", "trust.json");
4588
+ TRUST_DURATIONS = {
4589
+ "30m": 30 * 6e4,
4590
+ "1h": 60 * 6e4,
4591
+ "2h": 2 * 60 * 6e4
4592
+ };
4593
+ SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
4594
+ AUTO_DENY_MS = 12e4;
4595
+ autoStarted = process.env.NODE9_AUTO_STARTED === "1";
4596
+ pending = /* @__PURE__ */ new Map();
4597
+ sseClients = /* @__PURE__ */ new Set();
4598
+ abandonTimer = null;
4599
+ daemonServer = null;
4600
+ hadBrowserClient = false;
4601
+ ACTIVITY_RING_SIZE = 100;
4602
+ activityRing = [];
4603
+ }
4604
+ });
4605
+
4606
+ // src/tui/tail.ts
4607
+ var tail_exports = {};
4608
+ __export(tail_exports, {
4609
+ startTail: () => startTail
4610
+ });
4611
+ import http2 from "http";
4612
+ import chalk5 from "chalk";
4613
+ import fs6 from "fs";
4614
+ import os6 from "os";
4615
+ import path8 from "path";
4616
+ import readline from "readline";
4617
+ import { spawn as spawn3 } from "child_process";
4618
+ function getIcon(tool) {
4619
+ const t = tool.toLowerCase();
4620
+ for (const [k, v] of Object.entries(ICONS)) {
4621
+ if (t.includes(k)) return v;
4622
+ }
4623
+ return "\u{1F6E0}\uFE0F";
4624
+ }
4625
+ function formatBase(activity) {
4626
+ const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
4627
+ const icon = getIcon(activity.tool);
4628
+ const toolName = activity.tool.slice(0, 16).padEnd(16);
4629
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
4630
+ const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
4631
+ return `${chalk5.gray(time)} ${icon} ${chalk5.white.bold(toolName)} ${chalk5.dim(argsPreview)}`;
4632
+ }
4633
+ function renderResult(activity, result) {
4634
+ const base = formatBase(activity);
4635
+ let status;
4636
+ if (result.status === "allow") {
4637
+ status = chalk5.green("\u2713 ALLOW");
4638
+ } else if (result.status === "dlp") {
4639
+ status = chalk5.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
4640
+ } else {
4641
+ status = chalk5.red("\u2717 BLOCK");
4642
+ }
4643
+ if (process.stdout.isTTY) {
4644
+ readline.clearLine(process.stdout, 0);
4645
+ readline.cursorTo(process.stdout, 0);
4646
+ }
4647
+ console.log(`${base} ${status}`);
4648
+ }
4649
+ function renderPending(activity) {
4650
+ if (!process.stdout.isTTY) return;
4651
+ process.stdout.write(`${formatBase(activity)} ${chalk5.yellow("\u25CF \u2026")}\r`);
4652
+ }
4653
+ async function ensureDaemon() {
4654
+ if (fs6.existsSync(PID_FILE)) {
4655
+ try {
4656
+ const { port } = JSON.parse(fs6.readFileSync(PID_FILE, "utf-8"));
4657
+ return port;
4658
+ } catch {
4659
+ }
4660
+ }
4661
+ console.log(chalk5.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
4662
+ const child = spawn3(process.execPath, [process.argv[1], "daemon"], {
4663
+ detached: true,
4664
+ stdio: "ignore",
4665
+ env: { ...process.env, NODE9_AUTO_STARTED: "1" }
4666
+ });
4667
+ child.unref();
4668
+ for (let i = 0; i < 20; i++) {
4669
+ await new Promise((r) => setTimeout(r, 250));
4670
+ if (!fs6.existsSync(PID_FILE)) continue;
4671
+ try {
4672
+ const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
4673
+ signal: AbortSignal.timeout(500)
4674
+ });
4675
+ if (res.ok) {
4676
+ const { port } = JSON.parse(fs6.readFileSync(PID_FILE, "utf-8"));
4677
+ return port;
4678
+ }
4679
+ } catch {
4680
+ }
4681
+ }
4682
+ console.error(chalk5.red("\u274C Daemon failed to start. Try: node9 daemon start"));
4683
+ process.exit(1);
4684
+ }
4685
+ async function startTail(options = {}) {
4686
+ const port = await ensureDaemon();
4687
+ const connectionTime = Date.now();
4688
+ const pending2 = /* @__PURE__ */ new Map();
4689
+ console.log(chalk5.cyan.bold(`
4690
+ \u{1F6F0}\uFE0F Node9 tail `) + chalk5.dim(`\u2192 localhost:${port}`));
4691
+ if (options.history) {
4692
+ console.log(chalk5.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
4693
+ } else {
4694
+ console.log(
4695
+ chalk5.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
4696
+ );
4697
+ }
4698
+ process.on("SIGINT", () => {
4699
+ if (process.stdout.isTTY) {
4700
+ readline.clearLine(process.stdout, 0);
4701
+ readline.cursorTo(process.stdout, 0);
4702
+ }
4703
+ console.log(chalk5.dim("\n\u{1F6F0}\uFE0F Disconnected."));
4704
+ process.exit(0);
4705
+ });
4706
+ const req = http2.get(`http://127.0.0.1:${port}/events`, (res) => {
4707
+ if (res.statusCode !== 200) {
4708
+ console.error(chalk5.red(`Failed to connect: HTTP ${res.statusCode}`));
4709
+ process.exit(1);
4710
+ }
4711
+ let currentEvent = "";
4712
+ let currentData = "";
4713
+ res.on("error", () => {
4714
+ });
4715
+ const rl = readline.createInterface({ input: res, crlfDelay: Infinity });
4716
+ rl.on("error", () => {
4717
+ });
4718
+ rl.on("line", (line) => {
4719
+ if (line.startsWith("event:")) {
4720
+ currentEvent = line.slice(6).trim();
4721
+ } else if (line.startsWith("data:")) {
4722
+ currentData = line.slice(5).trim();
4723
+ } else if (line === "") {
4724
+ if (currentEvent && currentData) {
4725
+ handleMessage(currentEvent, currentData);
4726
+ }
4727
+ currentEvent = "";
4728
+ currentData = "";
4729
+ }
4730
+ });
4731
+ rl.on("close", () => {
4732
+ if (process.stdout.isTTY) {
4733
+ readline.clearLine(process.stdout, 0);
4734
+ readline.cursorTo(process.stdout, 0);
4735
+ }
4736
+ console.log(chalk5.red("\n\u274C Daemon disconnected."));
4737
+ process.exit(1);
4738
+ });
4739
+ });
4740
+ function handleMessage(event, rawData) {
4741
+ let data;
4742
+ try {
4743
+ data = JSON.parse(rawData);
4744
+ } catch {
4745
+ return;
4746
+ }
4747
+ if (event === "activity") {
4748
+ if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
4749
+ if (data.status && data.status !== "pending") {
4750
+ renderResult(data, data);
4751
+ return;
4752
+ }
4753
+ pending2.set(data.id, data);
4754
+ const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
4755
+ if (slowTool) renderPending(data);
4756
+ }
4757
+ if (event === "activity-result") {
4758
+ const original = pending2.get(data.id);
4759
+ if (original) {
4760
+ renderResult(original, data);
4761
+ pending2.delete(data.id);
4762
+ }
4763
+ }
4764
+ }
4765
+ req.on("error", (err) => {
4766
+ const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
4767
+ console.error(chalk5.red(`
4768
+ \u274C ${msg}`));
4769
+ process.exit(1);
4770
+ });
4771
+ }
4772
+ var PID_FILE, ICONS;
4773
+ var init_tail = __esm({
4774
+ "src/tui/tail.ts"() {
4775
+ "use strict";
4776
+ init_daemon();
4777
+ PID_FILE = path8.join(os6.homedir(), ".node9", "daemon.pid");
4778
+ ICONS = {
4779
+ bash: "\u{1F4BB}",
4780
+ shell: "\u{1F4BB}",
4781
+ terminal: "\u{1F4BB}",
4782
+ read: "\u{1F4D6}",
4783
+ edit: "\u270F\uFE0F",
4784
+ write: "\u270F\uFE0F",
4785
+ glob: "\u{1F4C2}",
4786
+ grep: "\u{1F50D}",
4787
+ agent: "\u{1F916}",
4788
+ search: "\u{1F50D}",
4789
+ sql: "\u{1F5C4}\uFE0F",
4790
+ query: "\u{1F5C4}\uFE0F",
4791
+ list: "\u{1F4C2}",
4792
+ delete: "\u{1F5D1}\uFE0F",
4793
+ web: "\u{1F310}"
4794
+ };
4795
+ }
4796
+ });
4797
+
4798
+ // src/cli.ts
4799
+ init_core();
4800
+ import { Command } from "commander";
4801
+
4802
+ // src/setup.ts
4803
+ import fs3 from "fs";
4804
+ import path5 from "path";
4805
+ import os3 from "os";
4806
+ import chalk3 from "chalk";
4807
+ import { confirm as confirm2 } from "@inquirer/prompts";
4808
+ function printDaemonTip() {
4809
+ console.log(
4810
+ chalk3.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk3.white("\n To view your history or manage persistent rules, run:") + chalk3.green("\n node9 daemon --openui")
4811
+ );
4812
+ }
4813
+ function fullPathCommand(subcommand) {
4814
+ if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
4815
+ const nodeExec = process.execPath;
4816
+ const cliScript = process.argv[1];
4817
+ return `${nodeExec} ${cliScript} ${subcommand}`;
4818
+ }
4819
+ function readJson(filePath) {
4820
+ try {
4821
+ if (fs3.existsSync(filePath)) {
4822
+ return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
4823
+ }
4824
+ } catch {
4825
+ }
4826
+ return null;
4827
+ }
4828
+ function writeJson(filePath, data) {
4829
+ const dir = path5.dirname(filePath);
4830
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
4831
+ fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
4832
+ }
4833
+ async function setupClaude() {
4834
+ const homeDir2 = os3.homedir();
4835
+ const mcpPath = path5.join(homeDir2, ".claude.json");
4836
+ const hooksPath = path5.join(homeDir2, ".claude", "settings.json");
4837
+ const claudeConfig = readJson(mcpPath) ?? {};
4838
+ const settings = readJson(hooksPath) ?? {};
4839
+ const servers = claudeConfig.mcpServers ?? {};
4840
+ let anythingChanged = false;
4841
+ if (!settings.hooks) settings.hooks = {};
4842
+ const hasPreHook = settings.hooks.PreToolUse?.some(
4843
+ (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
4844
+ );
4845
+ if (!hasPreHook) {
4846
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
4847
+ settings.hooks.PreToolUse.push({
4848
+ matcher: ".*",
4849
+ hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
4850
+ });
4851
+ console.log(chalk3.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
4852
+ anythingChanged = true;
4853
+ }
4854
+ const hasPostHook = settings.hooks.PostToolUse?.some(
4855
+ (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
4856
+ );
4857
+ if (!hasPostHook) {
4858
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
4859
+ settings.hooks.PostToolUse.push({
4860
+ matcher: ".*",
4861
+ hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
4862
+ });
4863
+ console.log(chalk3.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
4864
+ anythingChanged = true;
4865
+ }
4866
+ if (anythingChanged) {
4867
+ writeJson(hooksPath, settings);
4868
+ console.log("");
4869
+ }
4870
+ const serversToWrap = [];
4871
+ for (const [name, server] of Object.entries(servers)) {
4872
+ if (!server.command || server.command === "node9") continue;
4873
+ const parts = [server.command, ...server.args ?? []];
4874
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
4875
+ }
4876
+ if (serversToWrap.length > 0) {
4877
+ console.log(chalk3.bold("The following existing entries will be modified:\n"));
4878
+ console.log(chalk3.white(` ${mcpPath}`));
4879
+ for (const { name, originalCmd } of serversToWrap) {
4880
+ console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
4881
+ }
4882
+ console.log("");
4883
+ const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
4884
+ if (proceed) {
4885
+ for (const { name, parts } of serversToWrap) {
4886
+ servers[name] = { ...servers[name], command: "node9", args: parts };
4887
+ }
4888
+ claudeConfig.mcpServers = servers;
4889
+ writeJson(mcpPath, claudeConfig);
4890
+ console.log(chalk3.green(`
4891
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
4892
+ anythingChanged = true;
4893
+ } else {
4894
+ console.log(chalk3.yellow(" Skipped MCP server wrapping."));
3871
4895
  }
3872
- res.writeHead(404).end();
3873
- });
3874
- daemonServer = server;
3875
- server.on("error", (e) => {
3876
- if (e.code === "EADDRINUSE") {
3877
- try {
3878
- if (fs3.existsSync(DAEMON_PID_FILE)) {
3879
- const { pid } = JSON.parse(fs3.readFileSync(DAEMON_PID_FILE, "utf-8"));
3880
- process.kill(pid, 0);
3881
- return process.exit(0);
3882
- }
3883
- } catch {
3884
- try {
3885
- fs3.unlinkSync(DAEMON_PID_FILE);
3886
- } catch {
4896
+ console.log("");
4897
+ }
4898
+ if (!anythingChanged && serversToWrap.length === 0) {
4899
+ console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
4900
+ printDaemonTip();
4901
+ return;
4902
+ }
4903
+ if (anythingChanged) {
4904
+ console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
4905
+ console.log(chalk3.gray(" Restart Claude Code for changes to take effect."));
4906
+ printDaemonTip();
4907
+ }
4908
+ }
4909
+ async function setupGemini() {
4910
+ const homeDir2 = os3.homedir();
4911
+ const settingsPath = path5.join(homeDir2, ".gemini", "settings.json");
4912
+ const settings = readJson(settingsPath) ?? {};
4913
+ const servers = settings.mcpServers ?? {};
4914
+ let anythingChanged = false;
4915
+ if (!settings.hooks) settings.hooks = {};
4916
+ const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
4917
+ (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
4918
+ );
4919
+ if (!hasBeforeHook) {
4920
+ if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
4921
+ if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
4922
+ settings.hooks.BeforeTool.push({
4923
+ matcher: ".*",
4924
+ hooks: [
4925
+ {
4926
+ name: "node9-check",
4927
+ type: "command",
4928
+ command: fullPathCommand("check"),
4929
+ timeout: 6e5
3887
4930
  }
3888
- server.listen(DAEMON_PORT2, DAEMON_HOST2);
3889
- return;
4931
+ ]
4932
+ });
4933
+ console.log(chalk3.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
4934
+ anythingChanged = true;
4935
+ }
4936
+ const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
4937
+ (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
4938
+ );
4939
+ if (!hasAfterHook) {
4940
+ if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
4941
+ if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
4942
+ settings.hooks.AfterTool.push({
4943
+ matcher: ".*",
4944
+ hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
4945
+ });
4946
+ console.log(chalk3.green(" \u2705 AfterTool hook added \u2192 node9 log"));
4947
+ anythingChanged = true;
4948
+ }
4949
+ if (anythingChanged) {
4950
+ writeJson(settingsPath, settings);
4951
+ console.log("");
4952
+ }
4953
+ const serversToWrap = [];
4954
+ for (const [name, server] of Object.entries(servers)) {
4955
+ if (!server.command || server.command === "node9") continue;
4956
+ const parts = [server.command, ...server.args ?? []];
4957
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
4958
+ }
4959
+ if (serversToWrap.length > 0) {
4960
+ console.log(chalk3.bold("The following existing entries will be modified:\n"));
4961
+ console.log(chalk3.white(` ${settingsPath} (mcpServers)`));
4962
+ for (const { name, originalCmd } of serversToWrap) {
4963
+ console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
4964
+ }
4965
+ console.log("");
4966
+ const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
4967
+ if (proceed) {
4968
+ for (const { name, parts } of serversToWrap) {
4969
+ servers[name] = { ...servers[name], command: "node9", args: parts };
3890
4970
  }
4971
+ settings.mcpServers = servers;
4972
+ writeJson(settingsPath, settings);
4973
+ console.log(chalk3.green(`
4974
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
4975
+ anythingChanged = true;
4976
+ } else {
4977
+ console.log(chalk3.yellow(" Skipped MCP server wrapping."));
3891
4978
  }
3892
- console.error(chalk4.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
3893
- process.exit(1);
3894
- });
3895
- server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
3896
- atomicWriteSync2(
3897
- DAEMON_PID_FILE,
3898
- JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
3899
- { mode: 384 }
3900
- );
3901
- console.log(chalk4.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
3902
- });
4979
+ console.log("");
4980
+ }
4981
+ if (!anythingChanged && serversToWrap.length === 0) {
4982
+ console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
4983
+ printDaemonTip();
4984
+ return;
4985
+ }
4986
+ if (anythingChanged) {
4987
+ console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
4988
+ console.log(chalk3.gray(" Restart Gemini CLI for changes to take effect."));
4989
+ printDaemonTip();
4990
+ }
3903
4991
  }
3904
- function stopDaemon() {
3905
- if (!fs3.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
3906
- try {
3907
- const { pid } = JSON.parse(fs3.readFileSync(DAEMON_PID_FILE, "utf-8"));
3908
- process.kill(pid, "SIGTERM");
3909
- console.log(chalk4.green("\u2705 Stopped."));
3910
- } catch {
3911
- console.log(chalk4.gray("Cleaned up stale PID file."));
3912
- } finally {
3913
- try {
3914
- fs3.unlinkSync(DAEMON_PID_FILE);
3915
- } catch {
4992
+ async function setupCursor() {
4993
+ const homeDir2 = os3.homedir();
4994
+ const mcpPath = path5.join(homeDir2, ".cursor", "mcp.json");
4995
+ const mcpConfig = readJson(mcpPath) ?? {};
4996
+ const servers = mcpConfig.mcpServers ?? {};
4997
+ let anythingChanged = false;
4998
+ const serversToWrap = [];
4999
+ for (const [name, server] of Object.entries(servers)) {
5000
+ if (!server.command || server.command === "node9") continue;
5001
+ const parts = [server.command, ...server.args ?? []];
5002
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
5003
+ }
5004
+ if (serversToWrap.length > 0) {
5005
+ console.log(chalk3.bold("The following existing entries will be modified:\n"));
5006
+ console.log(chalk3.white(` ${mcpPath}`));
5007
+ for (const { name, originalCmd } of serversToWrap) {
5008
+ console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5009
+ }
5010
+ console.log("");
5011
+ const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
5012
+ if (proceed) {
5013
+ for (const { name, parts } of serversToWrap) {
5014
+ servers[name] = { ...servers[name], command: "node9", args: parts };
5015
+ }
5016
+ mcpConfig.mcpServers = servers;
5017
+ writeJson(mcpPath, mcpConfig);
5018
+ console.log(chalk3.green(`
5019
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
5020
+ anythingChanged = true;
5021
+ } else {
5022
+ console.log(chalk3.yellow(" Skipped MCP server wrapping."));
3916
5023
  }
5024
+ console.log("");
3917
5025
  }
3918
- }
3919
- function daemonStatus() {
3920
- if (!fs3.existsSync(DAEMON_PID_FILE))
3921
- return console.log(chalk4.yellow("Node9 daemon: not running"));
3922
- try {
3923
- const { pid } = JSON.parse(fs3.readFileSync(DAEMON_PID_FILE, "utf-8"));
3924
- process.kill(pid, 0);
3925
- console.log(chalk4.green("Node9 daemon: running"));
3926
- } catch {
3927
- console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
5026
+ console.log(
5027
+ chalk3.yellow(
5028
+ " \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
5029
+ )
5030
+ );
5031
+ console.log("");
5032
+ if (!anythingChanged && serversToWrap.length === 0) {
5033
+ console.log(
5034
+ chalk3.blue(
5035
+ "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
5036
+ )
5037
+ );
5038
+ printDaemonTip();
5039
+ return;
5040
+ }
5041
+ if (anythingChanged) {
5042
+ console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
5043
+ console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
5044
+ printDaemonTip();
3928
5045
  }
3929
5046
  }
3930
5047
 
3931
5048
  // src/cli.ts
3932
- import { spawn as spawn3, execSync } from "child_process";
5049
+ init_daemon();
5050
+ import { spawn as spawn4, execSync } from "child_process";
3933
5051
  import { parseCommandString } from "execa";
3934
5052
  import { execa } from "execa";
3935
- import chalk5 from "chalk";
3936
- import readline from "readline";
3937
- import fs5 from "fs";
3938
- import path7 from "path";
3939
- import os5 from "os";
5053
+ import chalk6 from "chalk";
5054
+ import readline2 from "readline";
5055
+ import fs7 from "fs";
5056
+ import path9 from "path";
5057
+ import os7 from "os";
3940
5058
 
3941
5059
  // src/undo.ts
3942
5060
  import { spawnSync } from "child_process";
3943
- import fs4 from "fs";
3944
- import path6 from "path";
3945
- import os4 from "os";
3946
- var SNAPSHOT_STACK_PATH = path6.join(os4.homedir(), ".node9", "snapshots.json");
3947
- var UNDO_LATEST_PATH = path6.join(os4.homedir(), ".node9", "undo_latest.txt");
5061
+ import fs5 from "fs";
5062
+ import path7 from "path";
5063
+ import os5 from "os";
5064
+ var SNAPSHOT_STACK_PATH = path7.join(os5.homedir(), ".node9", "snapshots.json");
5065
+ var UNDO_LATEST_PATH = path7.join(os5.homedir(), ".node9", "undo_latest.txt");
3948
5066
  var MAX_SNAPSHOTS = 10;
3949
5067
  function readStack() {
3950
5068
  try {
3951
- if (fs4.existsSync(SNAPSHOT_STACK_PATH))
3952
- return JSON.parse(fs4.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
5069
+ if (fs5.existsSync(SNAPSHOT_STACK_PATH))
5070
+ return JSON.parse(fs5.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
3953
5071
  } catch {
3954
5072
  }
3955
5073
  return [];
3956
5074
  }
3957
5075
  function writeStack(stack) {
3958
- const dir = path6.dirname(SNAPSHOT_STACK_PATH);
3959
- if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
3960
- fs4.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
5076
+ const dir = path7.dirname(SNAPSHOT_STACK_PATH);
5077
+ if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
5078
+ fs5.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
3961
5079
  }
3962
5080
  function buildArgsSummary(tool, args) {
3963
5081
  if (!args || typeof args !== "object") return "";
@@ -3973,13 +5091,13 @@ function buildArgsSummary(tool, args) {
3973
5091
  async function createShadowSnapshot(tool = "unknown", args = {}) {
3974
5092
  try {
3975
5093
  const cwd = process.cwd();
3976
- if (!fs4.existsSync(path6.join(cwd, ".git"))) return null;
3977
- const tempIndex = path6.join(cwd, ".git", `node9_index_${Date.now()}`);
5094
+ if (!fs5.existsSync(path7.join(cwd, ".git"))) return null;
5095
+ const tempIndex = path7.join(cwd, ".git", `node9_index_${Date.now()}`);
3978
5096
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
3979
5097
  spawnSync("git", ["add", "-A"], { env });
3980
5098
  const treeRes = spawnSync("git", ["write-tree"], { env });
3981
5099
  const treeHash = treeRes.stdout.toString().trim();
3982
- if (fs4.existsSync(tempIndex)) fs4.unlinkSync(tempIndex);
5100
+ if (fs5.existsSync(tempIndex)) fs5.unlinkSync(tempIndex);
3983
5101
  if (!treeHash || treeRes.status !== 0) return null;
3984
5102
  const commitRes = spawnSync("git", [
3985
5103
  "commit-tree",
@@ -4000,7 +5118,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}) {
4000
5118
  stack.push(entry);
4001
5119
  if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
4002
5120
  writeStack(stack);
4003
- fs4.writeFileSync(UNDO_LATEST_PATH, commitHash);
5121
+ fs5.writeFileSync(UNDO_LATEST_PATH, commitHash);
4004
5122
  return commitHash;
4005
5123
  } catch (err) {
4006
5124
  if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
@@ -4038,9 +5156,9 @@ function applyUndo(hash, cwd) {
4038
5156
  const tracked = spawnSync("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
4039
5157
  const untracked = spawnSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
4040
5158
  for (const file of [...tracked, ...untracked]) {
4041
- const fullPath = path6.join(dir, file);
4042
- if (!snapshotFiles.has(file) && fs4.existsSync(fullPath)) {
4043
- fs4.unlinkSync(fullPath);
5159
+ const fullPath = path7.join(dir, file);
5160
+ if (!snapshotFiles.has(file) && fs5.existsSync(fullPath)) {
5161
+ fs5.unlinkSync(fullPath);
4044
5162
  }
4045
5163
  }
4046
5164
  return true;
@@ -4050,9 +5168,10 @@ function applyUndo(hash, cwd) {
4050
5168
  }
4051
5169
 
4052
5170
  // src/cli.ts
5171
+ init_shields();
4053
5172
  import { confirm as confirm3 } from "@inquirer/prompts";
4054
5173
  var { version } = JSON.parse(
4055
- fs5.readFileSync(path7.join(__dirname, "../package.json"), "utf-8")
5174
+ fs7.readFileSync(path9.join(__dirname, "../package.json"), "utf-8")
4056
5175
  );
4057
5176
  function parseDuration(str) {
4058
5177
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -4084,6 +5203,15 @@ INSTRUCTIONS:
4084
5203
  - If you believe this action is critical, explain your reasoning and ask them to run "node9 pause 15m" to proceed.`;
4085
5204
  }
4086
5205
  const label = blockedByLabel.toLowerCase();
5206
+ if (label.includes("dlp") || label.includes("secret detected") || label.includes("credential review")) {
5207
+ return `NODE9 SECURITY ALERT: A sensitive credential (API key, token, or private key) was found in your tool call arguments.
5208
+ CRITICAL INSTRUCTION: Do NOT retry this action.
5209
+ REQUIRED ACTIONS:
5210
+ 1. Remove the hardcoded credential from your command or code.
5211
+ 2. Use an environment variable or a dedicated secrets manager instead.
5212
+ 3. Treat the leaked credential as compromised and rotate it immediately.
5213
+ Do NOT attempt to bypass this check or pass the credential through another tool.`;
5214
+ }
4087
5215
  if (label.includes("sql safety") && label.includes("delete without where")) {
4088
5216
  return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
4089
5217
  INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
@@ -4139,7 +5267,7 @@ function openBrowserLocal() {
4139
5267
  }
4140
5268
  async function autoStartDaemonAndWait() {
4141
5269
  try {
4142
- const child = spawn3("node9", ["daemon"], {
5270
+ const child = spawn4("node9", ["daemon"], {
4143
5271
  detached: true,
4144
5272
  stdio: "ignore",
4145
5273
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
@@ -4175,14 +5303,14 @@ async function runProxy(targetCommand) {
4175
5303
  if (stdout) executable = stdout.trim();
4176
5304
  } catch {
4177
5305
  }
4178
- console.log(chalk5.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
4179
- const child = spawn3(executable, args, {
5306
+ console.log(chalk6.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
5307
+ const child = spawn4(executable, args, {
4180
5308
  stdio: ["pipe", "pipe", "inherit"],
4181
5309
  // We control STDIN and STDOUT
4182
5310
  shell: false,
4183
5311
  env: { ...process.env, FORCE_COLOR: "1" }
4184
5312
  });
4185
- const agentIn = readline.createInterface({ input: process.stdin, terminal: false });
5313
+ const agentIn = readline2.createInterface({ input: process.stdin, terminal: false });
4186
5314
  agentIn.on("line", async (line) => {
4187
5315
  let message;
4188
5316
  try {
@@ -4200,10 +5328,10 @@ async function runProxy(targetCommand) {
4200
5328
  agent: "Proxy/MCP"
4201
5329
  });
4202
5330
  if (!result.approved) {
4203
- console.error(chalk5.red(`
5331
+ console.error(chalk6.red(`
4204
5332
  \u{1F6D1} Node9 Sudo: Action Blocked`));
4205
- console.error(chalk5.gray(` Tool: ${name}`));
4206
- console.error(chalk5.gray(` Reason: ${result.reason || "Security Policy"}
5333
+ console.error(chalk6.gray(` Tool: ${name}`));
5334
+ console.error(chalk6.gray(` Reason: ${result.reason || "Security Policy"}
4207
5335
  `));
4208
5336
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
4209
5337
  const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -4245,14 +5373,14 @@ async function runProxy(targetCommand) {
4245
5373
  }
4246
5374
  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) => {
4247
5375
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
4248
- const credPath = path7.join(os5.homedir(), ".node9", "credentials.json");
4249
- if (!fs5.existsSync(path7.dirname(credPath)))
4250
- fs5.mkdirSync(path7.dirname(credPath), { recursive: true });
5376
+ const credPath = path9.join(os7.homedir(), ".node9", "credentials.json");
5377
+ if (!fs7.existsSync(path9.dirname(credPath)))
5378
+ fs7.mkdirSync(path9.dirname(credPath), { recursive: true });
4251
5379
  const profileName = options.profile || "default";
4252
5380
  let existingCreds = {};
4253
5381
  try {
4254
- if (fs5.existsSync(credPath)) {
4255
- const raw = JSON.parse(fs5.readFileSync(credPath, "utf-8"));
5382
+ if (fs7.existsSync(credPath)) {
5383
+ const raw = JSON.parse(fs7.readFileSync(credPath, "utf-8"));
4256
5384
  if (raw.apiKey) {
4257
5385
  existingCreds = {
4258
5386
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -4264,13 +5392,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4264
5392
  } catch {
4265
5393
  }
4266
5394
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
4267
- fs5.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
5395
+ fs7.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
4268
5396
  if (profileName === "default") {
4269
- const configPath = path7.join(os5.homedir(), ".node9", "config.json");
5397
+ const configPath = path9.join(os7.homedir(), ".node9", "config.json");
4270
5398
  let config = {};
4271
5399
  try {
4272
- if (fs5.existsSync(configPath))
4273
- config = JSON.parse(fs5.readFileSync(configPath, "utf-8"));
5400
+ if (fs7.existsSync(configPath))
5401
+ config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
4274
5402
  } catch {
4275
5403
  }
4276
5404
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -4285,36 +5413,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4285
5413
  approvers.cloud = false;
4286
5414
  }
4287
5415
  s.approvers = approvers;
4288
- if (!fs5.existsSync(path7.dirname(configPath)))
4289
- fs5.mkdirSync(path7.dirname(configPath), { recursive: true });
4290
- fs5.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
5416
+ if (!fs7.existsSync(path9.dirname(configPath)))
5417
+ fs7.mkdirSync(path9.dirname(configPath), { recursive: true });
5418
+ fs7.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
4291
5419
  }
4292
5420
  if (options.profile && profileName !== "default") {
4293
- console.log(chalk5.green(`\u2705 Profile "${profileName}" saved`));
4294
- console.log(chalk5.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
5421
+ console.log(chalk6.green(`\u2705 Profile "${profileName}" saved`));
5422
+ console.log(chalk6.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
4295
5423
  } else if (options.local) {
4296
- console.log(chalk5.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
4297
- console.log(chalk5.gray(` All decisions stay on this machine.`));
5424
+ console.log(chalk6.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
5425
+ console.log(chalk6.gray(` All decisions stay on this machine.`));
4298
5426
  } else {
4299
- console.log(chalk5.green(`\u2705 Logged in \u2014 agent mode`));
4300
- console.log(chalk5.gray(` Team policy enforced for all calls via Node9 cloud.`));
5427
+ console.log(chalk6.green(`\u2705 Logged in \u2014 agent mode`));
5428
+ console.log(chalk6.gray(` Team policy enforced for all calls via Node9 cloud.`));
4301
5429
  }
4302
5430
  });
4303
5431
  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) => {
4304
5432
  if (target === "gemini") return await setupGemini();
4305
5433
  if (target === "claude") return await setupClaude();
4306
5434
  if (target === "cursor") return await setupCursor();
4307
- console.error(chalk5.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
5435
+ console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
4308
5436
  process.exit(1);
4309
5437
  });
4310
5438
  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) => {
4311
5439
  if (!target) {
4312
- console.log(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
4313
- console.log(" Usage: " + chalk5.white("node9 setup <target>") + "\n");
5440
+ console.log(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
5441
+ console.log(" Usage: " + chalk6.white("node9 setup <target>") + "\n");
4314
5442
  console.log(" Targets:");
4315
- console.log(" " + chalk5.green("claude") + " \u2014 Claude Code (hook mode)");
4316
- console.log(" " + chalk5.green("gemini") + " \u2014 Gemini CLI (hook mode)");
4317
- console.log(" " + chalk5.green("cursor") + " \u2014 Cursor (hook mode)");
5443
+ console.log(" " + chalk6.green("claude") + " \u2014 Claude Code (hook mode)");
5444
+ console.log(" " + chalk6.green("gemini") + " \u2014 Gemini CLI (hook mode)");
5445
+ console.log(" " + chalk6.green("cursor") + " \u2014 Cursor (hook mode)");
4318
5446
  console.log("");
4319
5447
  return;
4320
5448
  }
@@ -4322,28 +5450,28 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
4322
5450
  if (t === "gemini") return await setupGemini();
4323
5451
  if (t === "claude") return await setupClaude();
4324
5452
  if (t === "cursor") return await setupCursor();
4325
- console.error(chalk5.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
5453
+ console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
4326
5454
  process.exit(1);
4327
5455
  });
4328
5456
  program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
4329
- const homeDir2 = os5.homedir();
5457
+ const homeDir2 = os7.homedir();
4330
5458
  let failures = 0;
4331
5459
  function pass(msg) {
4332
- console.log(chalk5.green(" \u2705 ") + msg);
5460
+ console.log(chalk6.green(" \u2705 ") + msg);
4333
5461
  }
4334
5462
  function fail(msg, hint) {
4335
- console.log(chalk5.red(" \u274C ") + msg);
4336
- if (hint) console.log(chalk5.gray(" " + hint));
5463
+ console.log(chalk6.red(" \u274C ") + msg);
5464
+ if (hint) console.log(chalk6.gray(" " + hint));
4337
5465
  failures++;
4338
5466
  }
4339
5467
  function warn(msg, hint) {
4340
- console.log(chalk5.yellow(" \u26A0\uFE0F ") + msg);
4341
- if (hint) console.log(chalk5.gray(" " + hint));
5468
+ console.log(chalk6.yellow(" \u26A0\uFE0F ") + msg);
5469
+ if (hint) console.log(chalk6.gray(" " + hint));
4342
5470
  }
4343
5471
  function section(title) {
4344
- console.log("\n" + chalk5.bold(title));
5472
+ console.log("\n" + chalk6.bold(title));
4345
5473
  }
4346
- console.log(chalk5.cyan.bold(`
5474
+ console.log(chalk6.cyan.bold(`
4347
5475
  \u{1F6E1}\uFE0F Node9 Doctor v${version}
4348
5476
  `));
4349
5477
  section("Binary");
@@ -4372,10 +5500,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4372
5500
  );
4373
5501
  }
4374
5502
  section("Configuration");
4375
- const globalConfigPath = path7.join(homeDir2, ".node9", "config.json");
4376
- if (fs5.existsSync(globalConfigPath)) {
5503
+ const globalConfigPath = path9.join(homeDir2, ".node9", "config.json");
5504
+ if (fs7.existsSync(globalConfigPath)) {
4377
5505
  try {
4378
- JSON.parse(fs5.readFileSync(globalConfigPath, "utf-8"));
5506
+ JSON.parse(fs7.readFileSync(globalConfigPath, "utf-8"));
4379
5507
  pass("~/.node9/config.json found and valid");
4380
5508
  } catch {
4381
5509
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -4383,17 +5511,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
4383
5511
  } else {
4384
5512
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
4385
5513
  }
4386
- const projectConfigPath = path7.join(process.cwd(), "node9.config.json");
4387
- if (fs5.existsSync(projectConfigPath)) {
5514
+ const projectConfigPath = path9.join(process.cwd(), "node9.config.json");
5515
+ if (fs7.existsSync(projectConfigPath)) {
4388
5516
  try {
4389
- JSON.parse(fs5.readFileSync(projectConfigPath, "utf-8"));
5517
+ JSON.parse(fs7.readFileSync(projectConfigPath, "utf-8"));
4390
5518
  pass("node9.config.json found and valid (project)");
4391
5519
  } catch {
4392
5520
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
4393
5521
  }
4394
5522
  }
4395
- const credsPath = path7.join(homeDir2, ".node9", "credentials.json");
4396
- if (fs5.existsSync(credsPath)) {
5523
+ const credsPath = path9.join(homeDir2, ".node9", "credentials.json");
5524
+ if (fs7.existsSync(credsPath)) {
4397
5525
  pass("Cloud credentials found (~/.node9/credentials.json)");
4398
5526
  } else {
4399
5527
  warn(
@@ -4402,10 +5530,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4402
5530
  );
4403
5531
  }
4404
5532
  section("Agent Hooks");
4405
- const claudeSettingsPath = path7.join(homeDir2, ".claude", "settings.json");
4406
- if (fs5.existsSync(claudeSettingsPath)) {
5533
+ const claudeSettingsPath = path9.join(homeDir2, ".claude", "settings.json");
5534
+ if (fs7.existsSync(claudeSettingsPath)) {
4407
5535
  try {
4408
- const cs = JSON.parse(fs5.readFileSync(claudeSettingsPath, "utf-8"));
5536
+ const cs = JSON.parse(fs7.readFileSync(claudeSettingsPath, "utf-8"));
4409
5537
  const hasHook = cs.hooks?.PreToolUse?.some(
4410
5538
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4411
5539
  );
@@ -4418,10 +5546,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4418
5546
  } else {
4419
5547
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
4420
5548
  }
4421
- const geminiSettingsPath = path7.join(homeDir2, ".gemini", "settings.json");
4422
- if (fs5.existsSync(geminiSettingsPath)) {
5549
+ const geminiSettingsPath = path9.join(homeDir2, ".gemini", "settings.json");
5550
+ if (fs7.existsSync(geminiSettingsPath)) {
4423
5551
  try {
4424
- const gs = JSON.parse(fs5.readFileSync(geminiSettingsPath, "utf-8"));
5552
+ const gs = JSON.parse(fs7.readFileSync(geminiSettingsPath, "utf-8"));
4425
5553
  const hasHook = gs.hooks?.BeforeTool?.some(
4426
5554
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4427
5555
  );
@@ -4434,10 +5562,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4434
5562
  } else {
4435
5563
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
4436
5564
  }
4437
- const cursorHooksPath = path7.join(homeDir2, ".cursor", "hooks.json");
4438
- if (fs5.existsSync(cursorHooksPath)) {
5565
+ const cursorHooksPath = path9.join(homeDir2, ".cursor", "hooks.json");
5566
+ if (fs7.existsSync(cursorHooksPath)) {
4439
5567
  try {
4440
- const cur = JSON.parse(fs5.readFileSync(cursorHooksPath, "utf-8"));
5568
+ const cur = JSON.parse(fs7.readFileSync(cursorHooksPath, "utf-8"));
4441
5569
  const hasHook = cur.hooks?.preToolUse?.some(
4442
5570
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
4443
5571
  );
@@ -4458,9 +5586,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
4458
5586
  }
4459
5587
  console.log("");
4460
5588
  if (failures === 0) {
4461
- console.log(chalk5.green.bold(" All checks passed. Node9 is ready.\n"));
5589
+ console.log(chalk6.green.bold(" All checks passed. Node9 is ready.\n"));
4462
5590
  } else {
4463
- console.log(chalk5.red.bold(` ${failures} check(s) failed. See hints above.
5591
+ console.log(chalk6.red.bold(` ${failures} check(s) failed. See hints above.
4464
5592
  `));
4465
5593
  process.exit(1);
4466
5594
  }
@@ -4475,7 +5603,7 @@ program.command("explain").description(
4475
5603
  try {
4476
5604
  args = JSON.parse(trimmed);
4477
5605
  } catch {
4478
- console.error(chalk5.red(`
5606
+ console.error(chalk6.red(`
4479
5607
  \u274C Invalid JSON: ${trimmed}
4480
5608
  `));
4481
5609
  process.exit(1);
@@ -4486,63 +5614,63 @@ program.command("explain").description(
4486
5614
  }
4487
5615
  const result = await explainPolicy(tool, args);
4488
5616
  console.log("");
4489
- console.log(chalk5.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
5617
+ console.log(chalk6.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
4490
5618
  console.log("");
4491
- console.log(` ${chalk5.bold("Tool:")} ${chalk5.white(result.tool)}`);
5619
+ console.log(` ${chalk6.bold("Tool:")} ${chalk6.white(result.tool)}`);
4492
5620
  if (argsRaw) {
4493
5621
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
4494
- console.log(` ${chalk5.bold("Input:")} ${chalk5.gray(preview)}`);
5622
+ console.log(` ${chalk6.bold("Input:")} ${chalk6.gray(preview)}`);
4495
5623
  }
4496
5624
  console.log("");
4497
- console.log(chalk5.bold("Config Sources (Waterfall):"));
5625
+ console.log(chalk6.bold("Config Sources (Waterfall):"));
4498
5626
  for (const tier of result.waterfall) {
4499
- const num = chalk5.gray(` ${tier.tier}.`);
5627
+ const num = chalk6.gray(` ${tier.tier}.`);
4500
5628
  const label = tier.label.padEnd(16);
4501
5629
  let statusStr;
4502
5630
  if (tier.tier === 1) {
4503
- statusStr = chalk5.gray(tier.note ?? "");
5631
+ statusStr = chalk6.gray(tier.note ?? "");
4504
5632
  } else if (tier.status === "active") {
4505
- const loc = tier.path ? chalk5.gray(tier.path) : "";
4506
- const note = tier.note ? chalk5.gray(`(${tier.note})`) : "";
4507
- statusStr = chalk5.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
5633
+ const loc = tier.path ? chalk6.gray(tier.path) : "";
5634
+ const note = tier.note ? chalk6.gray(`(${tier.note})`) : "";
5635
+ statusStr = chalk6.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
4508
5636
  } else {
4509
- statusStr = chalk5.gray("\u25CB " + (tier.note ?? "not found"));
5637
+ statusStr = chalk6.gray("\u25CB " + (tier.note ?? "not found"));
4510
5638
  }
4511
- console.log(`${num} ${chalk5.white(label)} ${statusStr}`);
5639
+ console.log(`${num} ${chalk6.white(label)} ${statusStr}`);
4512
5640
  }
4513
5641
  console.log("");
4514
- console.log(chalk5.bold("Policy Evaluation:"));
5642
+ console.log(chalk6.bold("Policy Evaluation:"));
4515
5643
  for (const step of result.steps) {
4516
5644
  const isFinal = step.isFinal;
4517
5645
  let icon;
4518
- if (step.outcome === "allow") icon = chalk5.green(" \u2705");
4519
- else if (step.outcome === "review") icon = chalk5.red(" \u{1F534}");
4520
- else if (step.outcome === "skip") icon = chalk5.gray(" \u2500 ");
4521
- else icon = chalk5.gray(" \u25CB ");
5646
+ if (step.outcome === "allow") icon = chalk6.green(" \u2705");
5647
+ else if (step.outcome === "review") icon = chalk6.red(" \u{1F534}");
5648
+ else if (step.outcome === "skip") icon = chalk6.gray(" \u2500 ");
5649
+ else icon = chalk6.gray(" \u25CB ");
4522
5650
  const name = step.name.padEnd(18);
4523
- const nameStr = isFinal ? chalk5.white.bold(name) : chalk5.white(name);
4524
- const detail = isFinal ? chalk5.white(step.detail) : chalk5.gray(step.detail);
4525
- const arrow = isFinal ? chalk5.yellow(" \u2190 STOP") : "";
5651
+ const nameStr = isFinal ? chalk6.white.bold(name) : chalk6.white(name);
5652
+ const detail = isFinal ? chalk6.white(step.detail) : chalk6.gray(step.detail);
5653
+ const arrow = isFinal ? chalk6.yellow(" \u2190 STOP") : "";
4526
5654
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
4527
5655
  }
4528
5656
  console.log("");
4529
5657
  if (result.decision === "allow") {
4530
- console.log(chalk5.green.bold(" Decision: \u2705 ALLOW") + chalk5.gray(" \u2014 no approval needed"));
5658
+ console.log(chalk6.green.bold(" Decision: \u2705 ALLOW") + chalk6.gray(" \u2014 no approval needed"));
4531
5659
  } else {
4532
5660
  console.log(
4533
- chalk5.red.bold(" Decision: \u{1F534} REVIEW") + chalk5.gray(" \u2014 human approval required")
5661
+ chalk6.red.bold(" Decision: \u{1F534} REVIEW") + chalk6.gray(" \u2014 human approval required")
4534
5662
  );
4535
5663
  if (result.blockedByLabel) {
4536
- console.log(chalk5.gray(` Reason: ${result.blockedByLabel}`));
5664
+ console.log(chalk6.gray(` Reason: ${result.blockedByLabel}`));
4537
5665
  }
4538
5666
  }
4539
5667
  console.log("");
4540
5668
  });
4541
5669
  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) => {
4542
- const configPath = path7.join(os5.homedir(), ".node9", "config.json");
4543
- if (fs5.existsSync(configPath) && !options.force) {
4544
- console.log(chalk5.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
4545
- console.log(chalk5.gray(` Run with --force to overwrite.`));
5670
+ const configPath = path9.join(os7.homedir(), ".node9", "config.json");
5671
+ if (fs7.existsSync(configPath) && !options.force) {
5672
+ console.log(chalk6.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
5673
+ console.log(chalk6.gray(` Run with --force to overwrite.`));
4546
5674
  return;
4547
5675
  }
4548
5676
  const requestedMode = options.mode.toLowerCase();
@@ -4554,13 +5682,13 @@ program.command("init").description("Create ~/.node9/config.json with default po
4554
5682
  mode: safeMode
4555
5683
  }
4556
5684
  };
4557
- const dir = path7.dirname(configPath);
4558
- if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
4559
- fs5.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
4560
- console.log(chalk5.green(`\u2705 Global config created: ${configPath}`));
4561
- console.log(chalk5.cyan(` Mode set to: ${safeMode}`));
5685
+ const dir = path9.dirname(configPath);
5686
+ if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
5687
+ fs7.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
5688
+ console.log(chalk6.green(`\u2705 Global config created: ${configPath}`));
5689
+ console.log(chalk6.cyan(` Mode set to: ${safeMode}`));
4562
5690
  console.log(
4563
- chalk5.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
5691
+ chalk6.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
4564
5692
  );
4565
5693
  });
4566
5694
  function formatRelativeTime(timestamp) {
@@ -4574,14 +5702,14 @@ function formatRelativeTime(timestamp) {
4574
5702
  return new Date(timestamp).toLocaleDateString();
4575
5703
  }
4576
5704
  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) => {
4577
- const logPath = path7.join(os5.homedir(), ".node9", "audit.log");
4578
- if (!fs5.existsSync(logPath)) {
5705
+ const logPath = path9.join(os7.homedir(), ".node9", "audit.log");
5706
+ if (!fs7.existsSync(logPath)) {
4579
5707
  console.log(
4580
- chalk5.yellow("No audit logs found. Run node9 with an agent to generate entries.")
5708
+ chalk6.yellow("No audit logs found. Run node9 with an agent to generate entries.")
4581
5709
  );
4582
5710
  return;
4583
5711
  }
4584
- const raw = fs5.readFileSync(logPath, "utf-8");
5712
+ const raw = fs7.readFileSync(logPath, "utf-8");
4585
5713
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
4586
5714
  let entries = lines.flatMap((line) => {
4587
5715
  try {
@@ -4603,31 +5731,31 @@ program.command("audit").description("View local execution audit log").option("-
4603
5731
  return;
4604
5732
  }
4605
5733
  if (entries.length === 0) {
4606
- console.log(chalk5.yellow("No matching audit entries."));
5734
+ console.log(chalk6.yellow("No matching audit entries."));
4607
5735
  return;
4608
5736
  }
4609
5737
  console.log(
4610
5738
  `
4611
- ${chalk5.bold("Node9 Audit Log")} ${chalk5.dim(`(${entries.length} entries)`)}`
5739
+ ${chalk6.bold("Node9 Audit Log")} ${chalk6.dim(`(${entries.length} entries)`)}`
4612
5740
  );
4613
- console.log(chalk5.dim(" " + "\u2500".repeat(65)));
5741
+ console.log(chalk6.dim(" " + "\u2500".repeat(65)));
4614
5742
  console.log(
4615
5743
  ` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
4616
5744
  );
4617
- console.log(chalk5.dim(" " + "\u2500".repeat(65)));
5745
+ console.log(chalk6.dim(" " + "\u2500".repeat(65)));
4618
5746
  for (const e of entries) {
4619
5747
  const time = formatRelativeTime(String(e.ts)).padEnd(12);
4620
5748
  const tool = String(e.tool).slice(0, 17).padEnd(18);
4621
- const result = e.decision === "allow" ? chalk5.green("ALLOW".padEnd(10)) : chalk5.red("DENY".padEnd(10));
5749
+ const result = e.decision === "allow" ? chalk6.green("ALLOW".padEnd(10)) : chalk6.red("DENY".padEnd(10));
4622
5750
  const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
4623
5751
  const agent = String(e.agent || "unknown");
4624
5752
  console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
4625
5753
  }
4626
5754
  const allowed = entries.filter((e) => e.decision === "allow").length;
4627
5755
  const denied = entries.filter((e) => e.decision === "deny").length;
4628
- console.log(chalk5.dim(" " + "\u2500".repeat(65)));
5756
+ console.log(chalk6.dim(" " + "\u2500".repeat(65)));
4629
5757
  console.log(
4630
- ` ${entries.length} entries | ${chalk5.green(allowed + " allowed")} | ${chalk5.red(denied + " denied")}
5758
+ ` ${entries.length} entries | ${chalk6.green(allowed + " allowed")} | ${chalk6.red(denied + " denied")}
4631
5759
  `
4632
5760
  );
4633
5761
  });
@@ -4638,43 +5766,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
4638
5766
  const settings = mergedConfig.settings;
4639
5767
  console.log("");
4640
5768
  if (creds && settings.approvers.cloud) {
4641
- console.log(chalk5.green(" \u25CF Agent mode") + chalk5.gray(" \u2014 cloud team policy enforced"));
5769
+ console.log(chalk6.green(" \u25CF Agent mode") + chalk6.gray(" \u2014 cloud team policy enforced"));
4642
5770
  } else if (creds && !settings.approvers.cloud) {
4643
5771
  console.log(
4644
- chalk5.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk5.gray(" \u2014 all decisions stay on this machine")
5772
+ chalk6.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk6.gray(" \u2014 all decisions stay on this machine")
4645
5773
  );
4646
5774
  } else {
4647
5775
  console.log(
4648
- chalk5.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk5.gray(" \u2014 no API key (Local rules only)")
5776
+ chalk6.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk6.gray(" \u2014 no API key (Local rules only)")
4649
5777
  );
4650
5778
  }
4651
5779
  console.log("");
4652
5780
  if (daemonRunning) {
4653
5781
  console.log(
4654
- chalk5.green(" \u25CF Daemon running") + chalk5.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
5782
+ chalk6.green(" \u25CF Daemon running") + chalk6.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
4655
5783
  );
4656
5784
  } else {
4657
- console.log(chalk5.gray(" \u25CB Daemon stopped"));
5785
+ console.log(chalk6.gray(" \u25CB Daemon stopped"));
4658
5786
  }
4659
5787
  if (settings.enableUndo) {
4660
5788
  console.log(
4661
- chalk5.magenta(" \u25CF Undo Engine") + chalk5.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
5789
+ chalk6.magenta(" \u25CF Undo Engine") + chalk6.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
4662
5790
  );
4663
5791
  }
4664
5792
  console.log("");
4665
- const modeLabel = settings.mode === "audit" ? chalk5.blue("audit") : settings.mode === "strict" ? chalk5.red("strict") : chalk5.white("standard");
5793
+ const modeLabel = settings.mode === "audit" ? chalk6.blue("audit") : settings.mode === "strict" ? chalk6.red("strict") : chalk6.white("standard");
4666
5794
  console.log(` Mode: ${modeLabel}`);
4667
- const projectConfig = path7.join(process.cwd(), "node9.config.json");
4668
- const globalConfig = path7.join(os5.homedir(), ".node9", "config.json");
5795
+ const projectConfig = path9.join(process.cwd(), "node9.config.json");
5796
+ const globalConfig = path9.join(os7.homedir(), ".node9", "config.json");
4669
5797
  console.log(
4670
- ` Local: ${fs5.existsSync(projectConfig) ? chalk5.green("Active (node9.config.json)") : chalk5.gray("Not present")}`
5798
+ ` Local: ${fs7.existsSync(projectConfig) ? chalk6.green("Active (node9.config.json)") : chalk6.gray("Not present")}`
4671
5799
  );
4672
5800
  console.log(
4673
- ` Global: ${fs5.existsSync(globalConfig) ? chalk5.green("Active (~/.node9/config.json)") : chalk5.gray("Not present")}`
5801
+ ` Global: ${fs7.existsSync(globalConfig) ? chalk6.green("Active (~/.node9/config.json)") : chalk6.gray("Not present")}`
4674
5802
  );
4675
5803
  if (mergedConfig.policy.sandboxPaths.length > 0) {
4676
5804
  console.log(
4677
- ` Sandbox: ${chalk5.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
5805
+ ` Sandbox: ${chalk6.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
4678
5806
  );
4679
5807
  }
4680
5808
  const pauseState = checkPause();
@@ -4682,47 +5810,63 @@ program.command("status").description("Show current Node9 mode, policy source, a
4682
5810
  const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
4683
5811
  console.log("");
4684
5812
  console.log(
4685
- chalk5.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk5.gray(" \u2014 all tool calls allowed")
5813
+ chalk6.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk6.gray(" \u2014 all tool calls allowed")
4686
5814
  );
4687
5815
  }
4688
5816
  console.log("");
4689
5817
  });
4690
- 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(
5818
+ 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(
5819
+ "-w, --watch",
5820
+ "Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
5821
+ ).action(
4691
5822
  async (action, options) => {
4692
5823
  const cmd = (action ?? "start").toLowerCase();
4693
5824
  if (cmd === "stop") return stopDaemon();
4694
5825
  if (cmd === "status") return daemonStatus();
4695
5826
  if (cmd !== "start" && action !== void 0) {
4696
- console.error(chalk5.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
5827
+ console.error(chalk6.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
4697
5828
  process.exit(1);
4698
5829
  }
5830
+ if (options.watch) {
5831
+ process.env.NODE9_WATCH_MODE = "1";
5832
+ setTimeout(() => {
5833
+ openBrowserLocal();
5834
+ console.log(chalk6.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5835
+ }, 600);
5836
+ startDaemon();
5837
+ return;
5838
+ }
4699
5839
  if (options.openui) {
4700
5840
  if (isDaemonRunning()) {
4701
5841
  openBrowserLocal();
4702
- console.log(chalk5.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5842
+ console.log(chalk6.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
4703
5843
  process.exit(0);
4704
5844
  }
4705
- const child = spawn3("node9", ["daemon"], { detached: true, stdio: "ignore" });
5845
+ const child = spawn4("node9", ["daemon"], { detached: true, stdio: "ignore" });
4706
5846
  child.unref();
4707
5847
  for (let i = 0; i < 12; i++) {
4708
5848
  await new Promise((r) => setTimeout(r, 250));
4709
5849
  if (isDaemonRunning()) break;
4710
5850
  }
4711
5851
  openBrowserLocal();
4712
- console.log(chalk5.green(`
5852
+ console.log(chalk6.green(`
4713
5853
  \u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
4714
5854
  process.exit(0);
4715
5855
  }
4716
5856
  if (options.background) {
4717
- const child = spawn3("node9", ["daemon"], { detached: true, stdio: "ignore" });
5857
+ const child = spawn4("node9", ["daemon"], { detached: true, stdio: "ignore" });
4718
5858
  child.unref();
4719
- console.log(chalk5.green(`
5859
+ console.log(chalk6.green(`
4720
5860
  \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
4721
5861
  process.exit(0);
4722
5862
  }
4723
5863
  startDaemon();
4724
5864
  }
4725
5865
  );
5866
+ program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).action(async (options) => {
5867
+ const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
5868
+ await startTail2(options);
5869
+ });
4726
5870
  program.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
4727
5871
  const processPayload = async (raw) => {
4728
5872
  try {
@@ -4733,9 +5877,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
4733
5877
  } catch (err) {
4734
5878
  const tempConfig = getConfig();
4735
5879
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
4736
- const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
5880
+ const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
4737
5881
  const errMsg = err instanceof Error ? err.message : String(err);
4738
- fs5.appendFileSync(
5882
+ fs7.appendFileSync(
4739
5883
  logPath,
4740
5884
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
4741
5885
  RAW: ${raw}
@@ -4753,10 +5897,10 @@ RAW: ${raw}
4753
5897
  }
4754
5898
  const config = getConfig();
4755
5899
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
4756
- const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
4757
- if (!fs5.existsSync(path7.dirname(logPath)))
4758
- fs5.mkdirSync(path7.dirname(logPath), { recursive: true });
4759
- fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
5900
+ const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
5901
+ if (!fs7.existsSync(path9.dirname(logPath)))
5902
+ fs7.mkdirSync(path9.dirname(logPath), { recursive: true });
5903
+ fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
4760
5904
  `);
4761
5905
  }
4762
5906
  const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
@@ -4767,13 +5911,19 @@ RAW: ${raw}
4767
5911
  const sendBlock = (msg, result2) => {
4768
5912
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
4769
5913
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
4770
- console.error(chalk5.red(`
5914
+ if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
5915
+ console.error(chalk6.bgRed.white.bold(`
5916
+ \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
5917
+ console.error(chalk6.red.bold(` A sensitive secret was found in the tool arguments!`));
5918
+ } else {
5919
+ console.error(chalk6.red(`
4771
5920
  \u{1F6D1} Node9 blocked "${toolName}"`));
4772
- console.error(chalk5.gray(` Triggered by: ${blockedByContext}`));
4773
- if (result2?.changeHint) console.error(chalk5.cyan(` To change: ${result2.changeHint}`));
5921
+ }
5922
+ console.error(chalk6.gray(` Triggered by: ${blockedByContext}`));
5923
+ if (result2?.changeHint) console.error(chalk6.cyan(` To change: ${result2.changeHint}`));
4774
5924
  console.error("");
4775
5925
  const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
4776
- console.error(chalk5.dim(` (Detailed instructions sent to AI agent)`));
5926
+ console.error(chalk6.dim(` (Detailed instructions sent to AI agent)`));
4777
5927
  process.stdout.write(
4778
5928
  JSON.stringify({
4779
5929
  decision: "block",
@@ -4804,7 +5954,7 @@ RAW: ${raw}
4804
5954
  process.exit(0);
4805
5955
  }
4806
5956
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
4807
- console.error(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
5957
+ console.error(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
4808
5958
  const daemonReady = await autoStartDaemonAndWait();
4809
5959
  if (daemonReady) {
4810
5960
  const retry = await authorizeHeadless(toolName, toolInput, false, meta);
@@ -4827,9 +5977,9 @@ RAW: ${raw}
4827
5977
  });
4828
5978
  } catch (err) {
4829
5979
  if (process.env.NODE9_DEBUG === "1") {
4830
- const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
5980
+ const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
4831
5981
  const errMsg = err instanceof Error ? err.message : String(err);
4832
- fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
5982
+ fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
4833
5983
  `);
4834
5984
  }
4835
5985
  process.exit(0);
@@ -4874,10 +6024,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
4874
6024
  decision: "allowed",
4875
6025
  source: "post-hook"
4876
6026
  };
4877
- const logPath = path7.join(os5.homedir(), ".node9", "audit.log");
4878
- if (!fs5.existsSync(path7.dirname(logPath)))
4879
- fs5.mkdirSync(path7.dirname(logPath), { recursive: true });
4880
- fs5.appendFileSync(logPath, JSON.stringify(entry) + "\n");
6027
+ const logPath = path9.join(os7.homedir(), ".node9", "audit.log");
6028
+ if (!fs7.existsSync(path9.dirname(logPath)))
6029
+ fs7.mkdirSync(path9.dirname(logPath), { recursive: true });
6030
+ fs7.appendFileSync(logPath, JSON.stringify(entry) + "\n");
4881
6031
  const config = getConfig();
4882
6032
  if (shouldSnapshot(tool, {}, config)) {
4883
6033
  await createShadowSnapshot();
@@ -4904,7 +6054,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
4904
6054
  const ms = parseDuration(options.duration);
4905
6055
  if (ms === null) {
4906
6056
  console.error(
4907
- chalk5.red(`
6057
+ chalk6.red(`
4908
6058
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
4909
6059
  `)
4910
6060
  );
@@ -4912,20 +6062,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
4912
6062
  }
4913
6063
  pauseNode9(ms, options.duration);
4914
6064
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
4915
- console.log(chalk5.yellow(`
6065
+ console.log(chalk6.yellow(`
4916
6066
  \u23F8 Node9 paused until ${expiresAt}`));
4917
- console.log(chalk5.gray(` All tool calls will be allowed without review.`));
4918
- console.log(chalk5.gray(` Run "node9 resume" to re-enable early.
6067
+ console.log(chalk6.gray(` All tool calls will be allowed without review.`));
6068
+ console.log(chalk6.gray(` Run "node9 resume" to re-enable early.
4919
6069
  `));
4920
6070
  });
4921
6071
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
4922
6072
  const { paused } = checkPause();
4923
6073
  if (!paused) {
4924
- console.log(chalk5.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
6074
+ console.log(chalk6.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
4925
6075
  return;
4926
6076
  }
4927
6077
  resumeNode9();
4928
- console.log(chalk5.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
6078
+ console.log(chalk6.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
4929
6079
  });
4930
6080
  var HOOK_BASED_AGENTS = {
4931
6081
  claude: "claude",
@@ -4938,15 +6088,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
4938
6088
  if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
4939
6089
  const target = HOOK_BASED_AGENTS[firstArg];
4940
6090
  console.error(
4941
- chalk5.yellow(`
6091
+ chalk6.yellow(`
4942
6092
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
4943
6093
  );
4944
- console.error(chalk5.white(`
6094
+ console.error(chalk6.white(`
4945
6095
  "${target}" uses its own hook system. Use:`));
4946
6096
  console.error(
4947
- chalk5.green(` node9 addto ${target} `) + chalk5.gray("# one-time setup")
6097
+ chalk6.green(` node9 addto ${target} `) + chalk6.gray("# one-time setup")
4948
6098
  );
4949
- console.error(chalk5.green(` ${target} `) + chalk5.gray("# run normally"));
6099
+ console.error(chalk6.green(` ${target} `) + chalk6.gray("# run normally"));
4950
6100
  process.exit(1);
4951
6101
  }
4952
6102
  const fullCommand = commandArgs.join(" ");
@@ -4954,7 +6104,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
4954
6104
  agent: "Terminal"
4955
6105
  });
4956
6106
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
4957
- console.error(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
6107
+ console.error(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
4958
6108
  const daemonReady = await autoStartDaemonAndWait();
4959
6109
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
4960
6110
  }
@@ -4963,12 +6113,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
4963
6113
  }
4964
6114
  if (!result.approved) {
4965
6115
  console.error(
4966
- chalk5.red(`
6116
+ chalk6.red(`
4967
6117
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
4968
6118
  );
4969
6119
  process.exit(1);
4970
6120
  }
4971
- console.error(chalk5.green("\n\u2705 Approved \u2014 running command...\n"));
6121
+ console.error(chalk6.green("\n\u2705 Approved \u2014 running command...\n"));
4972
6122
  await runProxy(fullCommand);
4973
6123
  } else {
4974
6124
  program.help();
@@ -4983,22 +6133,22 @@ program.command("undo").description(
4983
6133
  if (history.length === 0) {
4984
6134
  if (!options.all && allHistory.length > 0) {
4985
6135
  console.log(
4986
- chalk5.yellow(
6136
+ chalk6.yellow(
4987
6137
  `
4988
6138
  \u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
4989
- Run ${chalk5.cyan("node9 undo --all")} to see snapshots from all projects.
6139
+ Run ${chalk6.cyan("node9 undo --all")} to see snapshots from all projects.
4990
6140
  `
4991
6141
  )
4992
6142
  );
4993
6143
  } else {
4994
- console.log(chalk5.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
6144
+ console.log(chalk6.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
4995
6145
  }
4996
6146
  return;
4997
6147
  }
4998
6148
  const idx = history.length - steps;
4999
6149
  if (idx < 0) {
5000
6150
  console.log(
5001
- chalk5.yellow(
6151
+ chalk6.yellow(
5002
6152
  `
5003
6153
  \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
5004
6154
  `
@@ -5009,18 +6159,18 @@ program.command("undo").description(
5009
6159
  const snapshot = history[idx];
5010
6160
  const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
5011
6161
  const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
5012
- console.log(chalk5.magenta.bold(`
6162
+ console.log(chalk6.magenta.bold(`
5013
6163
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
5014
6164
  console.log(
5015
- chalk5.white(
5016
- ` Tool: ${chalk5.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk5.gray(" \u2192 " + snapshot.argsSummary) : ""}`
6165
+ chalk6.white(
6166
+ ` Tool: ${chalk6.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk6.gray(" \u2192 " + snapshot.argsSummary) : ""}`
5017
6167
  )
5018
6168
  );
5019
- console.log(chalk5.white(` When: ${chalk5.gray(ageStr)}`));
5020
- console.log(chalk5.white(` Dir: ${chalk5.gray(snapshot.cwd)}`));
6169
+ console.log(chalk6.white(` When: ${chalk6.gray(ageStr)}`));
6170
+ console.log(chalk6.white(` Dir: ${chalk6.gray(snapshot.cwd)}`));
5021
6171
  if (steps > 1)
5022
6172
  console.log(
5023
- chalk5.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
6173
+ chalk6.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
5024
6174
  );
5025
6175
  console.log("");
5026
6176
  const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
@@ -5028,21 +6178,21 @@ program.command("undo").description(
5028
6178
  const lines = diff.split("\n");
5029
6179
  for (const line of lines) {
5030
6180
  if (line.startsWith("+++") || line.startsWith("---")) {
5031
- console.log(chalk5.bold(line));
6181
+ console.log(chalk6.bold(line));
5032
6182
  } else if (line.startsWith("+")) {
5033
- console.log(chalk5.green(line));
6183
+ console.log(chalk6.green(line));
5034
6184
  } else if (line.startsWith("-")) {
5035
- console.log(chalk5.red(line));
6185
+ console.log(chalk6.red(line));
5036
6186
  } else if (line.startsWith("@@")) {
5037
- console.log(chalk5.cyan(line));
6187
+ console.log(chalk6.cyan(line));
5038
6188
  } else {
5039
- console.log(chalk5.gray(line));
6189
+ console.log(chalk6.gray(line));
5040
6190
  }
5041
6191
  }
5042
6192
  console.log("");
5043
6193
  } else {
5044
6194
  console.log(
5045
- chalk5.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
6195
+ chalk6.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
5046
6196
  );
5047
6197
  }
5048
6198
  const proceed = await confirm3({
@@ -5051,21 +6201,111 @@ program.command("undo").description(
5051
6201
  });
5052
6202
  if (proceed) {
5053
6203
  if (applyUndo(snapshot.hash, snapshot.cwd)) {
5054
- console.log(chalk5.green("\n\u2705 Reverted successfully.\n"));
6204
+ console.log(chalk6.green("\n\u2705 Reverted successfully.\n"));
5055
6205
  } else {
5056
- console.error(chalk5.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
6206
+ console.error(chalk6.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
5057
6207
  }
5058
6208
  } else {
5059
- console.log(chalk5.gray("\nCancelled.\n"));
6209
+ console.log(chalk6.gray("\nCancelled.\n"));
6210
+ }
6211
+ });
6212
+ var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
6213
+ shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
6214
+ const name = resolveShieldName(service);
6215
+ if (!name) {
6216
+ console.error(chalk6.red(`
6217
+ \u274C Unknown shield: "${service}"
6218
+ `));
6219
+ console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
6220
+ `);
6221
+ process.exit(1);
6222
+ }
6223
+ const shield = getShield(name);
6224
+ const active = readActiveShields();
6225
+ if (active.includes(name)) {
6226
+ console.log(chalk6.yellow(`
6227
+ \u2139\uFE0F Shield "${name}" is already active.
6228
+ `));
6229
+ return;
6230
+ }
6231
+ writeActiveShields([...active, name]);
6232
+ console.log(chalk6.green(`
6233
+ \u{1F6E1}\uFE0F Shield "${name}" enabled.`));
6234
+ console.log(chalk6.gray(` ${shield.smartRules.length} smart rules now active.`));
6235
+ if (shield.dangerousWords.length > 0)
6236
+ console.log(chalk6.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
6237
+ if (name === "filesystem") {
6238
+ console.log(
6239
+ chalk6.yellow(
6240
+ `
6241
+ \u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
6242
+ Tools like unlink, find -delete, or language-level file ops are not intercepted.`
6243
+ )
6244
+ );
6245
+ }
6246
+ console.log("");
6247
+ });
6248
+ shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
6249
+ const name = resolveShieldName(service);
6250
+ if (!name) {
6251
+ console.error(chalk6.red(`
6252
+ \u274C Unknown shield: "${service}"
6253
+ `));
6254
+ console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
6255
+ `);
6256
+ process.exit(1);
5060
6257
  }
6258
+ const active = readActiveShields();
6259
+ if (!active.includes(name)) {
6260
+ console.log(chalk6.yellow(`
6261
+ \u2139\uFE0F Shield "${name}" is not active.
6262
+ `));
6263
+ return;
6264
+ }
6265
+ writeActiveShields(active.filter((s) => s !== name));
6266
+ console.log(chalk6.green(`
6267
+ \u{1F6E1}\uFE0F Shield "${name}" disabled.
6268
+ `));
6269
+ });
6270
+ shieldCmd.command("list").description("Show all available shields").action(() => {
6271
+ const active = new Set(readActiveShields());
6272
+ console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
6273
+ for (const shield of listShields()) {
6274
+ const status = active.has(shield.name) ? chalk6.green("\u25CF enabled") : chalk6.gray("\u25CB disabled");
6275
+ console.log(` ${status} ${chalk6.cyan(shield.name.padEnd(12))} ${shield.description}`);
6276
+ if (shield.aliases.length > 0)
6277
+ console.log(chalk6.gray(` aliases: ${shield.aliases.join(", ")}`));
6278
+ }
6279
+ console.log("");
6280
+ });
6281
+ shieldCmd.command("status").description("Show which shields are currently active").action(() => {
6282
+ const active = readActiveShields();
6283
+ if (active.length === 0) {
6284
+ console.log(chalk6.yellow("\n\u2139\uFE0F No shields are active.\n"));
6285
+ console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
6286
+ `);
6287
+ return;
6288
+ }
6289
+ console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
6290
+ for (const name of active) {
6291
+ const shield = getShield(name);
6292
+ if (!shield) continue;
6293
+ console.log(` ${chalk6.green("\u25CF")} ${chalk6.cyan(name)}`);
6294
+ console.log(
6295
+ chalk6.gray(
6296
+ ` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
6297
+ )
6298
+ );
6299
+ }
6300
+ console.log("");
5061
6301
  });
5062
6302
  process.on("unhandledRejection", (reason) => {
5063
6303
  const isCheckHook = process.argv[2] === "check";
5064
6304
  if (isCheckHook) {
5065
6305
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
5066
- const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
6306
+ const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
5067
6307
  const msg = reason instanceof Error ? reason.message : String(reason);
5068
- fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
6308
+ fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
5069
6309
  `);
5070
6310
  }
5071
6311
  process.exit(0);