@node9/proxy 1.0.14 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
9
16
  var __copyProps = (to, from, except, desc) => {
10
17
  if (from && typeof from === "object" || typeof from === "function") {
11
18
  for (let key of __getOwnPropNames(from))
@@ -23,25 +30,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
30
  mod
24
31
  ));
25
32
 
26
- // src/cli.ts
27
- var import_commander = require("commander");
28
-
29
- // src/core.ts
30
- var import_chalk2 = __toESM(require("chalk"));
31
- var import_prompts = require("@inquirer/prompts");
32
- var import_fs2 = __toESM(require("fs"));
33
- var import_path4 = __toESM(require("path"));
34
- var import_os2 = __toESM(require("os"));
35
- var import_picomatch = __toESM(require("picomatch"));
36
- var import_sh_syntax = require("sh-syntax");
37
-
38
- // src/ui/native.ts
39
- var import_child_process = require("child_process");
40
- var import_path2 = __toESM(require("path"));
41
- var import_chalk = __toESM(require("chalk"));
42
-
43
33
  // src/context-sniper.ts
44
- var import_path = __toESM(require("path"));
45
34
  function smartTruncate(str, maxLen = 500) {
46
35
  if (str.length <= maxLen) return str;
47
36
  const edge = Math.floor(maxLen / 2) - 3;
@@ -71,22 +60,6 @@ function extractContext(text, matchedWord) {
71
60
  ... [${lines.length - end} lines hidden] ...` : "";
72
61
  return { snippet: `${head}${snippet}${tail}`, lineIndex };
73
62
  }
74
- var CODE_KEYS = [
75
- "command",
76
- "cmd",
77
- "shell_command",
78
- "bash_command",
79
- "script",
80
- "code",
81
- "input",
82
- "sql",
83
- "query",
84
- "arguments",
85
- "args",
86
- "param",
87
- "params",
88
- "text"
89
- ];
90
63
  function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
91
64
  let intent = "EXEC";
92
65
  let contextSnippet;
@@ -141,11 +114,31 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
141
114
  ...ruleName && { ruleName }
142
115
  };
143
116
  }
117
+ var import_path, CODE_KEYS;
118
+ var init_context_sniper = __esm({
119
+ "src/context-sniper.ts"() {
120
+ "use strict";
121
+ import_path = __toESM(require("path"));
122
+ CODE_KEYS = [
123
+ "command",
124
+ "cmd",
125
+ "shell_command",
126
+ "bash_command",
127
+ "script",
128
+ "code",
129
+ "input",
130
+ "sql",
131
+ "query",
132
+ "arguments",
133
+ "args",
134
+ "param",
135
+ "params",
136
+ "text"
137
+ ];
138
+ }
139
+ });
144
140
 
145
141
  // src/ui/native.ts
146
- var isTestEnv = () => {
147
- return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
148
- };
149
142
  function formatArgs(args, matchedField, matchedWord) {
150
143
  if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
151
144
  let parsed = args;
@@ -355,81 +348,21 @@ end run`;
355
348
  }
356
349
  });
357
350
  }
351
+ var import_child_process, import_path2, import_chalk, isTestEnv;
352
+ var init_native = __esm({
353
+ "src/ui/native.ts"() {
354
+ "use strict";
355
+ import_child_process = require("child_process");
356
+ import_path2 = __toESM(require("path"));
357
+ import_chalk = __toESM(require("chalk"));
358
+ init_context_sniper();
359
+ isTestEnv = () => {
360
+ return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
361
+ };
362
+ }
363
+ });
358
364
 
359
365
  // src/config-schema.ts
360
- var import_zod = require("zod");
361
- var noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
362
- message: "Value must not contain literal newline characters (use \\n instead)"
363
- });
364
- var SmartConditionSchema = import_zod.z.object({
365
- field: import_zod.z.string().min(1, "Condition field must not be empty"),
366
- op: import_zod.z.enum(
367
- [
368
- "matches",
369
- "notMatches",
370
- "contains",
371
- "notContains",
372
- "exists",
373
- "notExists",
374
- "matchesGlob",
375
- "notMatchesGlob"
376
- ],
377
- {
378
- errorMap: () => ({
379
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
380
- })
381
- }
382
- ),
383
- value: import_zod.z.string().optional(),
384
- flags: import_zod.z.string().optional()
385
- });
386
- var SmartRuleSchema = import_zod.z.object({
387
- name: import_zod.z.string().optional(),
388
- tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
389
- conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
390
- conditionMode: import_zod.z.enum(["all", "any"]).optional(),
391
- verdict: import_zod.z.enum(["allow", "review", "block"], {
392
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
393
- }),
394
- reason: import_zod.z.string().optional()
395
- });
396
- var ConfigFileSchema = import_zod.z.object({
397
- version: import_zod.z.string().optional(),
398
- settings: import_zod.z.object({
399
- mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
400
- autoStartDaemon: import_zod.z.boolean().optional(),
401
- enableUndo: import_zod.z.boolean().optional(),
402
- enableHookLogDebug: import_zod.z.boolean().optional(),
403
- approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
404
- approvers: import_zod.z.object({
405
- native: import_zod.z.boolean().optional(),
406
- browser: import_zod.z.boolean().optional(),
407
- cloud: import_zod.z.boolean().optional(),
408
- terminal: import_zod.z.boolean().optional()
409
- }).optional(),
410
- environment: import_zod.z.string().optional(),
411
- slackEnabled: import_zod.z.boolean().optional(),
412
- enableTrustSessions: import_zod.z.boolean().optional(),
413
- allowGlobalPause: import_zod.z.boolean().optional()
414
- }).optional(),
415
- policy: import_zod.z.object({
416
- sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
417
- dangerousWords: import_zod.z.array(noNewlines).optional(),
418
- ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
419
- toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
420
- smartRules: import_zod.z.array(SmartRuleSchema).optional(),
421
- snapshot: import_zod.z.object({
422
- tools: import_zod.z.array(import_zod.z.string()).optional(),
423
- onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
424
- ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
425
- }).optional(),
426
- dlp: import_zod.z.object({
427
- enabled: import_zod.z.boolean().optional(),
428
- scanIgnoredTools: import_zod.z.boolean().optional()
429
- }).optional()
430
- }).optional(),
431
- environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
432
- }).strict({ message: "Config contains unknown top-level keys" });
433
366
  function sanitizeConfig(raw) {
434
367
  const result = ConfigFileSchema.safeParse(raw);
435
368
  if (result.success) {
@@ -447,8 +380,8 @@ function sanitizeConfig(raw) {
447
380
  }
448
381
  }
449
382
  const lines = result.error.issues.map((issue) => {
450
- const path9 = issue.path.length > 0 ? issue.path.join(".") : "root";
451
- return ` \u2022 ${path9}: ${issue.message}`;
383
+ const path10 = issue.path.length > 0 ? issue.path.join(".") : "root";
384
+ return ` \u2022 ${path10}: ${issue.message}`;
452
385
  });
453
386
  return {
454
387
  sanitized,
@@ -456,179 +389,94 @@ function sanitizeConfig(raw) {
456
389
  ${lines.join("\n")}`
457
390
  };
458
391
  }
459
-
460
- // src/shields.ts
461
- var import_fs = __toESM(require("fs"));
462
- var import_path3 = __toESM(require("path"));
463
- var import_os = __toESM(require("os"));
464
- var import_crypto = __toESM(require("crypto"));
465
- var SHIELDS = {
466
- postgres: {
467
- name: "postgres",
468
- description: "Protects PostgreSQL databases from destructive AI operations",
469
- aliases: ["pg", "postgresql"],
470
- smartRules: [
471
- {
472
- name: "shield:postgres:block-drop-table",
473
- tool: "*",
474
- conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
475
- verdict: "block",
476
- reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
477
- },
478
- {
479
- name: "shield:postgres:block-truncate",
480
- tool: "*",
481
- conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
482
- verdict: "block",
483
- reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
484
- },
485
- {
486
- name: "shield:postgres:block-drop-column",
487
- tool: "*",
488
- conditions: [
489
- { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
490
- ],
491
- verdict: "block",
492
- reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
493
- },
494
- {
495
- name: "shield:postgres:review-grant-revoke",
496
- tool: "*",
497
- conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
498
- verdict: "review",
499
- reason: "Permission changes require human approval (Postgres shield)"
500
- }
501
- ],
502
- dangerousWords: ["dropdb", "pg_dropcluster"]
503
- },
504
- github: {
505
- name: "github",
506
- description: "Protects GitHub repositories from destructive AI operations",
507
- aliases: ["git"],
508
- smartRules: [
509
- {
510
- // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
511
- // This rule adds coverage for `git push --delete` which the built-in does not match.
512
- name: "shield:github:review-delete-branch-remote",
513
- tool: "bash",
514
- conditions: [
515
- {
516
- field: "command",
517
- op: "matches",
518
- value: "git\\s+push\\s+.*--delete",
519
- flags: "i"
520
- }
521
- ],
522
- verdict: "review",
523
- reason: "Remote branch deletion requires human approval (GitHub shield)"
524
- },
525
- {
526
- name: "shield:github:block-delete-repo",
527
- tool: "*",
528
- conditions: [
529
- { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
530
- ],
531
- verdict: "block",
532
- reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
533
- }
534
- ],
535
- dangerousWords: []
536
- },
537
- aws: {
538
- name: "aws",
539
- description: "Protects AWS infrastructure from destructive AI operations",
540
- aliases: ["amazon"],
541
- smartRules: [
542
- {
543
- name: "shield:aws:block-delete-s3-bucket",
544
- tool: "*",
545
- conditions: [
546
- {
547
- field: "command",
548
- op: "matches",
549
- value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
550
- flags: "i"
551
- }
552
- ],
553
- verdict: "block",
554
- reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
555
- },
556
- {
557
- name: "shield:aws:review-iam-changes",
558
- tool: "*",
559
- conditions: [
560
- {
561
- field: "command",
562
- op: "matches",
563
- value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
564
- flags: "i"
565
- }
566
- ],
567
- verdict: "review",
568
- reason: "IAM changes require human approval (AWS shield)"
569
- },
570
- {
571
- name: "shield:aws:block-ec2-terminate",
572
- tool: "*",
573
- conditions: [
574
- {
575
- field: "command",
576
- op: "matches",
577
- value: "aws\\s+ec2\\s+terminate-instances",
578
- flags: "i"
579
- }
580
- ],
581
- verdict: "block",
582
- reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
583
- },
584
- {
585
- name: "shield:aws:review-rds-delete",
586
- tool: "*",
587
- conditions: [
588
- { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
589
- ],
590
- verdict: "review",
591
- reason: "RDS deletion requires human approval (AWS shield)"
592
- }
593
- ],
594
- dangerousWords: []
595
- },
596
- filesystem: {
597
- name: "filesystem",
598
- description: "Protects the local filesystem from dangerous AI operations",
599
- aliases: ["fs"],
600
- smartRules: [
601
- {
602
- name: "shield:filesystem:review-chmod-777",
603
- tool: "bash",
604
- conditions: [
605
- { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
392
+ var import_zod, noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
393
+ var init_config_schema = __esm({
394
+ "src/config-schema.ts"() {
395
+ "use strict";
396
+ import_zod = require("zod");
397
+ noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
398
+ message: "Value must not contain literal newline characters (use \\n instead)"
399
+ });
400
+ SmartConditionSchema = import_zod.z.object({
401
+ field: import_zod.z.string().min(1, "Condition field must not be empty"),
402
+ op: import_zod.z.enum(
403
+ [
404
+ "matches",
405
+ "notMatches",
406
+ "contains",
407
+ "notContains",
408
+ "exists",
409
+ "notExists",
410
+ "matchesGlob",
411
+ "notMatchesGlob"
606
412
  ],
607
- verdict: "review",
608
- reason: "chmod 777 requires human approval (filesystem shield)"
413
+ {
414
+ errorMap: () => ({
415
+ message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
416
+ })
417
+ }
418
+ ),
419
+ value: import_zod.z.string().optional(),
420
+ flags: import_zod.z.string().optional()
421
+ }).refine(
422
+ (c) => {
423
+ if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
424
+ return true;
609
425
  },
610
- {
611
- name: "shield:filesystem:review-write-etc",
612
- tool: "bash",
613
- conditions: [
614
- {
615
- field: "command",
616
- // Narrow to write-indicative operations to avoid approval fatigue on reads.
617
- // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
618
- op: "matches",
619
- value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
620
- }
621
- ],
622
- verdict: "review",
623
- reason: "Writing to /etc requires human approval (filesystem shield)"
624
- }
625
- ],
626
- // dd removed: too common as a legitimate tool (disk imaging, file ops).
627
- // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
628
- // wipefs retained: rarely legitimate in an agent context and not in built-ins.
629
- dangerousWords: ["wipefs"]
426
+ { message: "matchesGlob and notMatchesGlob conditions require a value field" }
427
+ );
428
+ SmartRuleSchema = import_zod.z.object({
429
+ name: import_zod.z.string().optional(),
430
+ tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
431
+ conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
432
+ conditionMode: import_zod.z.enum(["all", "any"]).optional(),
433
+ verdict: import_zod.z.enum(["allow", "review", "block"], {
434
+ errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
435
+ }),
436
+ reason: import_zod.z.string().optional()
437
+ });
438
+ ConfigFileSchema = import_zod.z.object({
439
+ version: import_zod.z.string().optional(),
440
+ settings: import_zod.z.object({
441
+ mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
442
+ autoStartDaemon: import_zod.z.boolean().optional(),
443
+ enableUndo: import_zod.z.boolean().optional(),
444
+ enableHookLogDebug: import_zod.z.boolean().optional(),
445
+ approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
446
+ flightRecorder: import_zod.z.boolean().optional(),
447
+ approvers: import_zod.z.object({
448
+ native: import_zod.z.boolean().optional(),
449
+ browser: import_zod.z.boolean().optional(),
450
+ cloud: import_zod.z.boolean().optional(),
451
+ terminal: import_zod.z.boolean().optional()
452
+ }).optional(),
453
+ environment: import_zod.z.string().optional(),
454
+ slackEnabled: import_zod.z.boolean().optional(),
455
+ enableTrustSessions: import_zod.z.boolean().optional(),
456
+ allowGlobalPause: import_zod.z.boolean().optional()
457
+ }).optional(),
458
+ policy: import_zod.z.object({
459
+ sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
460
+ dangerousWords: import_zod.z.array(noNewlines).optional(),
461
+ ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
462
+ toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
463
+ smartRules: import_zod.z.array(SmartRuleSchema).optional(),
464
+ snapshot: import_zod.z.object({
465
+ tools: import_zod.z.array(import_zod.z.string()).optional(),
466
+ onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
467
+ ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
468
+ }).optional(),
469
+ dlp: import_zod.z.object({
470
+ enabled: import_zod.z.boolean().optional(),
471
+ scanIgnoredTools: import_zod.z.boolean().optional()
472
+ }).optional()
473
+ }).optional(),
474
+ environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
475
+ }).strict({ message: "Config contains unknown top-level keys" });
630
476
  }
631
- };
477
+ });
478
+
479
+ // src/shields.ts
632
480
  function resolveShieldName(input) {
633
481
  const lower = input.toLowerCase();
634
482
  if (SHIELDS[lower]) return lower;
@@ -644,7 +492,6 @@ function getShield(name) {
644
492
  function listShields() {
645
493
  return Object.values(SHIELDS);
646
494
  }
647
- var SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
648
495
  function readActiveShields() {
649
496
  try {
650
497
  const raw = import_fs.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
@@ -669,21 +516,186 @@ function writeActiveShields(active) {
669
516
  import_fs.default.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
670
517
  import_fs.default.renameSync(tmp, SHIELDS_STATE_FILE);
671
518
  }
519
+ var import_fs, import_path3, import_os, import_crypto, SHIELDS, SHIELDS_STATE_FILE;
520
+ var init_shields = __esm({
521
+ "src/shields.ts"() {
522
+ "use strict";
523
+ import_fs = __toESM(require("fs"));
524
+ import_path3 = __toESM(require("path"));
525
+ import_os = __toESM(require("os"));
526
+ import_crypto = __toESM(require("crypto"));
527
+ SHIELDS = {
528
+ postgres: {
529
+ name: "postgres",
530
+ description: "Protects PostgreSQL databases from destructive AI operations",
531
+ aliases: ["pg", "postgresql"],
532
+ smartRules: [
533
+ {
534
+ name: "shield:postgres:block-drop-table",
535
+ tool: "*",
536
+ conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
537
+ verdict: "block",
538
+ reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
539
+ },
540
+ {
541
+ name: "shield:postgres:block-truncate",
542
+ tool: "*",
543
+ conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
544
+ verdict: "block",
545
+ reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
546
+ },
547
+ {
548
+ name: "shield:postgres:block-drop-column",
549
+ tool: "*",
550
+ conditions: [
551
+ { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
552
+ ],
553
+ verdict: "block",
554
+ reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
555
+ },
556
+ {
557
+ name: "shield:postgres:review-grant-revoke",
558
+ tool: "*",
559
+ conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
560
+ verdict: "review",
561
+ reason: "Permission changes require human approval (Postgres shield)"
562
+ }
563
+ ],
564
+ dangerousWords: ["dropdb", "pg_dropcluster"]
565
+ },
566
+ github: {
567
+ name: "github",
568
+ description: "Protects GitHub repositories from destructive AI operations",
569
+ aliases: ["git"],
570
+ smartRules: [
571
+ {
572
+ // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
573
+ // This rule adds coverage for `git push --delete` which the built-in does not match.
574
+ name: "shield:github:review-delete-branch-remote",
575
+ tool: "bash",
576
+ conditions: [
577
+ {
578
+ field: "command",
579
+ op: "matches",
580
+ value: "git\\s+push\\s+.*--delete",
581
+ flags: "i"
582
+ }
583
+ ],
584
+ verdict: "review",
585
+ reason: "Remote branch deletion requires human approval (GitHub shield)"
586
+ },
587
+ {
588
+ name: "shield:github:block-delete-repo",
589
+ tool: "*",
590
+ conditions: [
591
+ { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
592
+ ],
593
+ verdict: "block",
594
+ reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
595
+ }
596
+ ],
597
+ dangerousWords: []
598
+ },
599
+ aws: {
600
+ name: "aws",
601
+ description: "Protects AWS infrastructure from destructive AI operations",
602
+ aliases: ["amazon"],
603
+ smartRules: [
604
+ {
605
+ name: "shield:aws:block-delete-s3-bucket",
606
+ tool: "*",
607
+ conditions: [
608
+ {
609
+ field: "command",
610
+ op: "matches",
611
+ value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
612
+ flags: "i"
613
+ }
614
+ ],
615
+ verdict: "block",
616
+ reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
617
+ },
618
+ {
619
+ name: "shield:aws:review-iam-changes",
620
+ tool: "*",
621
+ conditions: [
622
+ {
623
+ field: "command",
624
+ op: "matches",
625
+ value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
626
+ flags: "i"
627
+ }
628
+ ],
629
+ verdict: "review",
630
+ reason: "IAM changes require human approval (AWS shield)"
631
+ },
632
+ {
633
+ name: "shield:aws:block-ec2-terminate",
634
+ tool: "*",
635
+ conditions: [
636
+ {
637
+ field: "command",
638
+ op: "matches",
639
+ value: "aws\\s+ec2\\s+terminate-instances",
640
+ flags: "i"
641
+ }
642
+ ],
643
+ verdict: "block",
644
+ reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
645
+ },
646
+ {
647
+ name: "shield:aws:review-rds-delete",
648
+ tool: "*",
649
+ conditions: [
650
+ { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
651
+ ],
652
+ verdict: "review",
653
+ reason: "RDS deletion requires human approval (AWS shield)"
654
+ }
655
+ ],
656
+ dangerousWords: []
657
+ },
658
+ filesystem: {
659
+ name: "filesystem",
660
+ description: "Protects the local filesystem from dangerous AI operations",
661
+ aliases: ["fs"],
662
+ smartRules: [
663
+ {
664
+ name: "shield:filesystem:review-chmod-777",
665
+ tool: "bash",
666
+ conditions: [
667
+ { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
668
+ ],
669
+ verdict: "review",
670
+ reason: "chmod 777 requires human approval (filesystem shield)"
671
+ },
672
+ {
673
+ name: "shield:filesystem:review-write-etc",
674
+ tool: "bash",
675
+ conditions: [
676
+ {
677
+ field: "command",
678
+ // Narrow to write-indicative operations to avoid approval fatigue on reads.
679
+ // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
680
+ op: "matches",
681
+ value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
682
+ }
683
+ ],
684
+ verdict: "review",
685
+ reason: "Writing to /etc requires human approval (filesystem shield)"
686
+ }
687
+ ],
688
+ // dd removed: too common as a legitimate tool (disk imaging, file ops).
689
+ // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
690
+ // wipefs retained: rarely legitimate in an agent context and not in built-ins.
691
+ dangerousWords: ["wipefs"]
692
+ }
693
+ };
694
+ SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
695
+ }
696
+ });
672
697
 
673
698
  // src/dlp.ts
674
- var DLP_PATTERNS = [
675
- { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
676
- { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
677
- { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
678
- { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
679
- { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
680
- {
681
- name: "Private Key (PEM)",
682
- regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
683
- severity: "block"
684
- },
685
- { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
686
- ];
687
699
  function maskSecret(raw, pattern) {
688
700
  const match = raw.match(pattern);
689
701
  if (!match) return "****";
@@ -694,9 +706,6 @@ function maskSecret(raw, pattern) {
694
706
  const stars = "*".repeat(Math.min(secret.length - 8, 12));
695
707
  return `${prefix}${stars}${suffix}`;
696
708
  }
697
- var MAX_DEPTH = 5;
698
- var MAX_STRING_BYTES = 1e5;
699
- var MAX_JSON_PARSE_BYTES = 1e4;
700
709
  function scanArgs(args, depth = 0, fieldPath = "args") {
701
710
  if (depth > MAX_DEPTH || args === null || args === void 0) return null;
702
711
  if (Array.isArray(args)) {
@@ -739,12 +748,30 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
739
748
  }
740
749
  return null;
741
750
  }
751
+ var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
752
+ var init_dlp = __esm({
753
+ "src/dlp.ts"() {
754
+ "use strict";
755
+ DLP_PATTERNS = [
756
+ { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
757
+ { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
758
+ { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
759
+ { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
760
+ { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
761
+ {
762
+ name: "Private Key (PEM)",
763
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
764
+ severity: "block"
765
+ },
766
+ { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
767
+ ];
768
+ MAX_DEPTH = 5;
769
+ MAX_STRING_BYTES = 1e5;
770
+ MAX_JSON_PARSE_BYTES = 1e4;
771
+ }
772
+ });
742
773
 
743
774
  // src/core.ts
744
- var PAUSED_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
745
- var TRUST_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "trust.json");
746
- var LOCAL_AUDIT_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "audit.log");
747
- var HOOK_DEBUG_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
748
775
  function checkPause() {
749
776
  try {
750
777
  if (!import_fs2.default.existsSync(PAUSED_FILE)) return { paused: false };
@@ -857,9 +884,9 @@ function matchesPattern(text, patterns) {
857
884
  const withoutDotSlash = text.replace(/^\.\//, "");
858
885
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
859
886
  }
860
- function getNestedValue(obj, path9) {
887
+ function getNestedValue(obj, path10) {
861
888
  if (!obj || typeof obj !== "object") return null;
862
- return path9.split(".").reduce((prev, curr) => prev?.[curr], obj);
889
+ return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
863
890
  }
864
891
  function shouldSnapshot(toolName, args, config) {
865
892
  if (!config.settings.enableUndo) return false;
@@ -907,7 +934,7 @@ function evaluateSmartConditions(args, rule) {
907
934
  case "matchesGlob":
908
935
  return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
909
936
  case "notMatchesGlob":
910
- return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : true;
937
+ return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
911
938
  default:
912
939
  return false;
913
940
  }
@@ -929,7 +956,6 @@ function isSqlTool(toolName, toolInspection) {
929
956
  const fieldName = toolInspection[matchingPattern];
930
957
  return fieldName === "sql" || fieldName === "query";
931
958
  }
932
- var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
933
959
  async function analyzeShellCommand(command) {
934
960
  const actions = [];
935
961
  const paths = [];
@@ -1011,208 +1037,6 @@ function redactSecrets(text) {
1011
1037
  );
1012
1038
  return redacted;
1013
1039
  }
1014
- var DANGEROUS_WORDS = [
1015
- "mkfs",
1016
- // formats/wipes a filesystem partition
1017
- "shred"
1018
- // permanently overwrites file contents (unrecoverable)
1019
- ];
1020
- var DEFAULT_CONFIG = {
1021
- settings: {
1022
- mode: "standard",
1023
- autoStartDaemon: true,
1024
- enableUndo: true,
1025
- // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
1026
- enableHookLogDebug: false,
1027
- approvalTimeoutMs: 0,
1028
- // 0 = disabled; set e.g. 30000 for 30-second auto-deny
1029
- approvers: { native: true, browser: true, cloud: true, terminal: true }
1030
- },
1031
- policy: {
1032
- sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
1033
- dangerousWords: DANGEROUS_WORDS,
1034
- ignoredTools: [
1035
- "list_*",
1036
- "get_*",
1037
- "read_*",
1038
- "describe_*",
1039
- "read",
1040
- "glob",
1041
- "grep",
1042
- "ls",
1043
- "notebookread",
1044
- "notebookedit",
1045
- "webfetch",
1046
- "websearch",
1047
- "exitplanmode",
1048
- "askuserquestion",
1049
- "agent",
1050
- "task*",
1051
- "toolsearch",
1052
- "mcp__ide__*",
1053
- "getDiagnostics"
1054
- ],
1055
- toolInspection: {
1056
- bash: "command",
1057
- shell: "command",
1058
- run_shell_command: "command",
1059
- "terminal.execute": "command",
1060
- "postgres:query": "sql"
1061
- },
1062
- snapshot: {
1063
- tools: [
1064
- "str_replace_based_edit_tool",
1065
- "write_file",
1066
- "edit_file",
1067
- "create_file",
1068
- "edit",
1069
- "replace"
1070
- ],
1071
- onlyPaths: [],
1072
- ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
1073
- },
1074
- smartRules: [
1075
- // ── rm safety (critical — always evaluated first) ──────────────────────
1076
- {
1077
- name: "block-rm-rf-home",
1078
- tool: "bash",
1079
- conditionMode: "all",
1080
- conditions: [
1081
- {
1082
- field: "command",
1083
- op: "matches",
1084
- value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
1085
- },
1086
- {
1087
- field: "command",
1088
- op: "matches",
1089
- value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
1090
- }
1091
- ],
1092
- verdict: "block",
1093
- reason: "Recursive delete of home directory is irreversible"
1094
- },
1095
- // ── SQL safety ────────────────────────────────────────────────────────
1096
- {
1097
- name: "no-delete-without-where",
1098
- tool: "*",
1099
- conditions: [
1100
- { field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
1101
- { field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
1102
- ],
1103
- conditionMode: "all",
1104
- verdict: "review",
1105
- reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
1106
- },
1107
- {
1108
- name: "review-drop-truncate-shell",
1109
- tool: "bash",
1110
- conditions: [
1111
- {
1112
- field: "command",
1113
- op: "matches",
1114
- value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
1115
- flags: "i"
1116
- }
1117
- ],
1118
- conditionMode: "all",
1119
- verdict: "review",
1120
- reason: "SQL DDL destructive statement inside a shell command"
1121
- },
1122
- // ── Git safety ────────────────────────────────────────────────────────
1123
- {
1124
- name: "block-force-push",
1125
- tool: "bash",
1126
- conditions: [
1127
- {
1128
- field: "command",
1129
- op: "matches",
1130
- value: "git push.*(--force|--force-with-lease|-f\\b)",
1131
- flags: "i"
1132
- }
1133
- ],
1134
- conditionMode: "all",
1135
- verdict: "block",
1136
- reason: "Force push overwrites remote history and cannot be undone"
1137
- },
1138
- {
1139
- name: "review-git-push",
1140
- tool: "bash",
1141
- conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
1142
- conditionMode: "all",
1143
- verdict: "review",
1144
- reason: "git push sends changes to a shared remote"
1145
- },
1146
- {
1147
- name: "review-git-destructive",
1148
- tool: "bash",
1149
- conditions: [
1150
- {
1151
- field: "command",
1152
- op: "matches",
1153
- value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
1154
- flags: "i"
1155
- }
1156
- ],
1157
- conditionMode: "all",
1158
- verdict: "review",
1159
- reason: "Destructive git operation \u2014 discards history or working-tree changes"
1160
- },
1161
- // ── Shell safety ──────────────────────────────────────────────────────
1162
- {
1163
- name: "review-sudo",
1164
- tool: "bash",
1165
- conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
1166
- conditionMode: "all",
1167
- verdict: "review",
1168
- reason: "Command requires elevated privileges"
1169
- },
1170
- {
1171
- name: "review-curl-pipe-shell",
1172
- tool: "bash",
1173
- conditions: [
1174
- {
1175
- field: "command",
1176
- op: "matches",
1177
- value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
1178
- flags: "i"
1179
- }
1180
- ],
1181
- conditionMode: "all",
1182
- verdict: "block",
1183
- reason: "Piping remote script into a shell is a supply-chain attack vector"
1184
- }
1185
- ],
1186
- dlp: { enabled: true, scanIgnoredTools: true }
1187
- },
1188
- environments: {}
1189
- };
1190
- var ADVISORY_SMART_RULES = [
1191
- {
1192
- name: "allow-rm-safe-paths",
1193
- tool: "*",
1194
- conditionMode: "all",
1195
- conditions: [
1196
- { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
1197
- {
1198
- field: "command",
1199
- op: "matches",
1200
- // Matches known-safe build artifact paths in the command.
1201
- value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
1202
- }
1203
- ],
1204
- verdict: "allow",
1205
- reason: "Deleting a known-safe build artifact path"
1206
- },
1207
- {
1208
- name: "review-rm",
1209
- tool: "*",
1210
- conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
1211
- verdict: "review",
1212
- reason: "rm can permanently delete files \u2014 confirm the target path"
1213
- }
1214
- ];
1215
- var cachedConfig = null;
1216
1040
  function _resetConfigCache() {
1217
1041
  cachedConfig = null;
1218
1042
  }
@@ -1223,7 +1047,7 @@ function getGlobalSettings() {
1223
1047
  const parsed = JSON.parse(import_fs2.default.readFileSync(globalConfigPath, "utf-8"));
1224
1048
  const settings = parsed.settings || {};
1225
1049
  return {
1226
- mode: settings.mode || "standard",
1050
+ mode: settings.mode || "audit",
1227
1051
  autoStartDaemon: settings.autoStartDaemon !== false,
1228
1052
  slackEnabled: settings.slackEnabled !== false,
1229
1053
  enableTrustSessions: settings.enableTrustSessions === true,
@@ -1233,7 +1057,7 @@ function getGlobalSettings() {
1233
1057
  } catch {
1234
1058
  }
1235
1059
  return {
1236
- mode: "standard",
1060
+ mode: "audit",
1237
1061
  autoStartDaemon: true,
1238
1062
  slackEnabled: true,
1239
1063
  enableTrustSessions: false,
@@ -1619,16 +1443,24 @@ function isIgnoredTool(toolName) {
1619
1443
  const config = getConfig();
1620
1444
  return matchesPattern(toolName, config.policy.ignoredTools);
1621
1445
  }
1622
- var DAEMON_PORT = 7391;
1623
- var DAEMON_HOST = "127.0.0.1";
1624
1446
  function isDaemonRunning() {
1447
+ const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1448
+ if (import_fs2.default.existsSync(pidFile)) {
1449
+ try {
1450
+ const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1451
+ if (port !== DAEMON_PORT) return false;
1452
+ process.kill(pid, 0);
1453
+ return true;
1454
+ } catch {
1455
+ return false;
1456
+ }
1457
+ }
1625
1458
  try {
1626
- const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1627
- if (!import_fs2.default.existsSync(pidFile)) return false;
1628
- const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1629
- if (port !== DAEMON_PORT) return false;
1630
- process.kill(pid, 0);
1631
- return true;
1459
+ const r = (0, import_child_process2.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
1460
+ encoding: "utf8",
1461
+ timeout: 500
1462
+ });
1463
+ return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
1632
1464
  } catch {
1633
1465
  return false;
1634
1466
  }
@@ -1644,7 +1476,7 @@ function getPersistentDecision(toolName) {
1644
1476
  }
1645
1477
  return null;
1646
1478
  }
1647
- async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1479
+ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
1648
1480
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1649
1481
  const checkCtrl = new AbortController();
1650
1482
  const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
@@ -1659,6 +1491,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1659
1491
  args,
1660
1492
  agent: meta?.agent,
1661
1493
  mcpServer: meta?.mcpServer,
1494
+ fromCLI: true,
1495
+ // Pass the flight-recorder ID so the daemon uses the same UUID for
1496
+ // activity-result as the CLI used for the pending activity event.
1497
+ // Without this, the two UUIDs never match and tail.ts never resolves
1498
+ // the pending item.
1499
+ activityId,
1662
1500
  ...riskMetadata && { riskMetadata }
1663
1501
  }),
1664
1502
  signal: checkCtrl.signal
@@ -1713,11 +1551,48 @@ async function resolveViaDaemon(id, decision, internalToken) {
1713
1551
  signal: AbortSignal.timeout(3e3)
1714
1552
  });
1715
1553
  }
1554
+ function notifyActivity(data) {
1555
+ return new Promise((resolve) => {
1556
+ try {
1557
+ const payload = JSON.stringify(data);
1558
+ const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
1559
+ sock.on("connect", () => {
1560
+ sock.on("close", resolve);
1561
+ sock.end(payload);
1562
+ });
1563
+ sock.on("error", resolve);
1564
+ } catch {
1565
+ resolve();
1566
+ }
1567
+ });
1568
+ }
1716
1569
  async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
1717
- if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1718
- const pauseState = checkPause();
1719
- if (pauseState.paused) return { approved: true, checkedBy: "paused" };
1720
- const creds = getCredentials();
1570
+ if (!options?.calledFromDaemon) {
1571
+ const actId = (0, import_crypto2.randomUUID)();
1572
+ const actTs = Date.now();
1573
+ await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
1574
+ const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
1575
+ ...options,
1576
+ activityId: actId
1577
+ });
1578
+ if (!result.noApprovalMechanism) {
1579
+ await notifyActivity({
1580
+ id: actId,
1581
+ tool: toolName,
1582
+ ts: actTs,
1583
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
1584
+ label: result.blockedByLabel
1585
+ });
1586
+ }
1587
+ return result;
1588
+ }
1589
+ return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
1590
+ }
1591
+ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
1592
+ if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1593
+ const pauseState = checkPause();
1594
+ if (pauseState.paused) return { approved: true, checkedBy: "paused" };
1595
+ const creds = getCredentials();
1721
1596
  const config = getConfig();
1722
1597
  const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
1723
1598
  const approvers = {
@@ -1749,6 +1624,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1749
1624
  blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
1750
1625
  };
1751
1626
  }
1627
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
1752
1628
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
1753
1629
  }
1754
1630
  }
@@ -1971,7 +1847,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1971
1847
  console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
1972
1848
  `));
1973
1849
  }
1974
- const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
1850
+ const daemonDecision = await askDaemon(
1851
+ toolName,
1852
+ args,
1853
+ meta,
1854
+ signal,
1855
+ riskMetadata,
1856
+ options?.activityId
1857
+ );
1975
1858
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
1976
1859
  const isApproved = daemonDecision === "allow";
1977
1860
  return {
@@ -2175,7 +2058,10 @@ function getConfig() {
2175
2058
  for (const rule of shield.smartRules) {
2176
2059
  if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
2177
2060
  }
2178
- for (const word of shield.dangerousWords) mergedPolicy.dangerousWords.push(word);
2061
+ const existingWords = new Set(mergedPolicy.dangerousWords);
2062
+ for (const word of shield.dangerousWords) {
2063
+ if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
2064
+ }
2179
2065
  }
2180
2066
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
2181
2067
  for (const rule of ADVISORY_SMART_RULES) {
@@ -2376,280 +2262,275 @@ async function resolveNode9SaaS(requestId, creds, approved) {
2376
2262
  } catch {
2377
2263
  }
2378
2264
  }
2379
-
2380
- // src/setup.ts
2381
- var import_fs3 = __toESM(require("fs"));
2382
- var import_path5 = __toESM(require("path"));
2383
- var import_os3 = __toESM(require("os"));
2384
- var import_chalk3 = __toESM(require("chalk"));
2385
- var import_prompts2 = require("@inquirer/prompts");
2386
- function printDaemonTip() {
2387
- console.log(
2388
- import_chalk3.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + import_chalk3.default.white("\n To view your history or manage persistent rules, run:") + import_chalk3.default.green("\n node9 daemon --openui")
2389
- );
2390
- }
2391
- function fullPathCommand(subcommand) {
2392
- if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
2393
- const nodeExec = process.execPath;
2394
- const cliScript = process.argv[1];
2395
- return `${nodeExec} ${cliScript} ${subcommand}`;
2396
- }
2397
- function readJson(filePath) {
2398
- try {
2399
- if (import_fs3.default.existsSync(filePath)) {
2400
- return JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
2401
- }
2402
- } catch {
2403
- }
2404
- return null;
2405
- }
2406
- function writeJson(filePath, data) {
2407
- const dir = import_path5.default.dirname(filePath);
2408
- if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
2409
- import_fs3.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
2410
- }
2411
- async function setupClaude() {
2412
- const homeDir2 = import_os3.default.homedir();
2413
- const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
2414
- const hooksPath = import_path5.default.join(homeDir2, ".claude", "settings.json");
2415
- const claudeConfig = readJson(mcpPath) ?? {};
2416
- const settings = readJson(hooksPath) ?? {};
2417
- const servers = claudeConfig.mcpServers ?? {};
2418
- let anythingChanged = false;
2419
- if (!settings.hooks) settings.hooks = {};
2420
- const hasPreHook = settings.hooks.PreToolUse?.some(
2421
- (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
2422
- );
2423
- if (!hasPreHook) {
2424
- if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
2425
- settings.hooks.PreToolUse.push({
2426
- matcher: ".*",
2427
- hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
2428
- });
2429
- console.log(import_chalk3.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
2430
- anythingChanged = true;
2431
- }
2432
- const hasPostHook = settings.hooks.PostToolUse?.some(
2433
- (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
2434
- );
2435
- if (!hasPostHook) {
2436
- if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
2437
- settings.hooks.PostToolUse.push({
2438
- matcher: ".*",
2439
- hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
2440
- });
2441
- console.log(import_chalk3.default.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
2442
- anythingChanged = true;
2443
- }
2444
- if (anythingChanged) {
2445
- writeJson(hooksPath, settings);
2446
- console.log("");
2447
- }
2448
- const serversToWrap = [];
2449
- for (const [name, server] of Object.entries(servers)) {
2450
- if (!server.command || server.command === "node9") continue;
2451
- const parts = [server.command, ...server.args ?? []];
2452
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
2453
- }
2454
- if (serversToWrap.length > 0) {
2455
- console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
2456
- console.log(import_chalk3.default.white(` ${mcpPath}`));
2457
- for (const { name, originalCmd } of serversToWrap) {
2458
- console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
2459
- }
2460
- console.log("");
2461
- const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
2462
- if (proceed) {
2463
- for (const { name, parts } of serversToWrap) {
2464
- servers[name] = { ...servers[name], command: "node9", args: parts };
2265
+ var import_chalk2, import_prompts, import_fs2, import_path4, import_os2, import_net, import_crypto2, import_child_process2, import_picomatch, import_sh_syntax, PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
2266
+ var init_core = __esm({
2267
+ "src/core.ts"() {
2268
+ "use strict";
2269
+ import_chalk2 = __toESM(require("chalk"));
2270
+ import_prompts = require("@inquirer/prompts");
2271
+ import_fs2 = __toESM(require("fs"));
2272
+ import_path4 = __toESM(require("path"));
2273
+ import_os2 = __toESM(require("os"));
2274
+ import_net = __toESM(require("net"));
2275
+ import_crypto2 = require("crypto");
2276
+ import_child_process2 = require("child_process");
2277
+ import_picomatch = __toESM(require("picomatch"));
2278
+ import_sh_syntax = require("sh-syntax");
2279
+ init_native();
2280
+ init_context_sniper();
2281
+ init_config_schema();
2282
+ init_shields();
2283
+ init_dlp();
2284
+ PAUSED_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
2285
+ TRUST_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "trust.json");
2286
+ LOCAL_AUDIT_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "audit.log");
2287
+ HOOK_DEBUG_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
2288
+ SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2289
+ DANGEROUS_WORDS = [
2290
+ "mkfs",
2291
+ // formats/wipes a filesystem partition
2292
+ "shred"
2293
+ // permanently overwrites file contents (unrecoverable)
2294
+ ];
2295
+ DEFAULT_CONFIG = {
2296
+ version: "1.0",
2297
+ settings: {
2298
+ mode: "audit",
2299
+ autoStartDaemon: true,
2300
+ enableUndo: true,
2301
+ // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
2302
+ enableHookLogDebug: true,
2303
+ approvalTimeoutMs: 3e4,
2304
+ // 30-second auto-deny timeout
2305
+ flightRecorder: true,
2306
+ approvers: { native: true, browser: true, cloud: false, terminal: true }
2307
+ },
2308
+ policy: {
2309
+ sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
2310
+ dangerousWords: DANGEROUS_WORDS,
2311
+ ignoredTools: [
2312
+ "list_*",
2313
+ "get_*",
2314
+ "read_*",
2315
+ "describe_*",
2316
+ "read",
2317
+ "glob",
2318
+ "grep",
2319
+ "ls",
2320
+ "notebookread",
2321
+ "notebookedit",
2322
+ "webfetch",
2323
+ "websearch",
2324
+ "exitplanmode",
2325
+ "askuserquestion",
2326
+ "agent",
2327
+ "task*",
2328
+ "toolsearch",
2329
+ "mcp__ide__*",
2330
+ "getDiagnostics"
2331
+ ],
2332
+ toolInspection: {
2333
+ bash: "command",
2334
+ shell: "command",
2335
+ run_shell_command: "command",
2336
+ "terminal.execute": "command",
2337
+ "postgres:query": "sql"
2338
+ },
2339
+ snapshot: {
2340
+ tools: [
2341
+ "str_replace_based_edit_tool",
2342
+ "write_file",
2343
+ "edit_file",
2344
+ "create_file",
2345
+ "edit",
2346
+ "replace"
2347
+ ],
2348
+ onlyPaths: [],
2349
+ ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
2350
+ },
2351
+ smartRules: [
2352
+ // ── rm safety (critical — always evaluated first) ──────────────────────
2353
+ {
2354
+ name: "block-rm-rf-home",
2355
+ tool: "bash",
2356
+ conditionMode: "all",
2357
+ conditions: [
2358
+ {
2359
+ field: "command",
2360
+ op: "matches",
2361
+ value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
2362
+ },
2363
+ {
2364
+ field: "command",
2365
+ op: "matches",
2366
+ value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
2367
+ }
2368
+ ],
2369
+ verdict: "block",
2370
+ reason: "Recursive delete of home directory is irreversible"
2371
+ },
2372
+ // ── SQL safety ────────────────────────────────────────────────────────
2373
+ {
2374
+ name: "no-delete-without-where",
2375
+ tool: "*",
2376
+ conditions: [
2377
+ { field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
2378
+ { field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
2379
+ ],
2380
+ conditionMode: "all",
2381
+ verdict: "review",
2382
+ reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
2383
+ },
2384
+ {
2385
+ name: "review-drop-truncate-shell",
2386
+ tool: "bash",
2387
+ conditions: [
2388
+ {
2389
+ field: "command",
2390
+ op: "matches",
2391
+ value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
2392
+ flags: "i"
2393
+ }
2394
+ ],
2395
+ conditionMode: "all",
2396
+ verdict: "review",
2397
+ reason: "SQL DDL destructive statement inside a shell command"
2398
+ },
2399
+ // ── Git safety ────────────────────────────────────────────────────────
2400
+ {
2401
+ name: "block-force-push",
2402
+ tool: "bash",
2403
+ conditions: [
2404
+ {
2405
+ field: "command",
2406
+ op: "matches",
2407
+ value: "git push.*(--force|--force-with-lease|-f\\b)",
2408
+ flags: "i"
2409
+ }
2410
+ ],
2411
+ conditionMode: "all",
2412
+ verdict: "block",
2413
+ reason: "Force push overwrites remote history and cannot be undone"
2414
+ },
2415
+ {
2416
+ name: "review-git-push",
2417
+ tool: "bash",
2418
+ conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
2419
+ conditionMode: "all",
2420
+ verdict: "review",
2421
+ reason: "git push sends changes to a shared remote"
2422
+ },
2423
+ {
2424
+ name: "review-git-destructive",
2425
+ tool: "bash",
2426
+ conditions: [
2427
+ {
2428
+ field: "command",
2429
+ op: "matches",
2430
+ value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
2431
+ flags: "i"
2432
+ }
2433
+ ],
2434
+ conditionMode: "all",
2435
+ verdict: "review",
2436
+ reason: "Destructive git operation \u2014 discards history or working-tree changes"
2437
+ },
2438
+ // ── Shell safety ──────────────────────────────────────────────────────
2439
+ {
2440
+ name: "review-sudo",
2441
+ tool: "bash",
2442
+ conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
2443
+ conditionMode: "all",
2444
+ verdict: "review",
2445
+ reason: "Command requires elevated privileges"
2446
+ },
2447
+ {
2448
+ name: "review-curl-pipe-shell",
2449
+ tool: "bash",
2450
+ conditions: [
2451
+ {
2452
+ field: "command",
2453
+ op: "matches",
2454
+ value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
2455
+ flags: "i"
2456
+ }
2457
+ ],
2458
+ conditionMode: "all",
2459
+ verdict: "block",
2460
+ reason: "Piping remote script into a shell is a supply-chain attack vector"
2461
+ }
2462
+ ],
2463
+ dlp: { enabled: true, scanIgnoredTools: true }
2464
+ },
2465
+ environments: {}
2466
+ };
2467
+ ADVISORY_SMART_RULES = [
2468
+ {
2469
+ name: "allow-rm-safe-paths",
2470
+ tool: "*",
2471
+ conditionMode: "all",
2472
+ conditions: [
2473
+ { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
2474
+ {
2475
+ field: "command",
2476
+ op: "matches",
2477
+ // Matches known-safe build artifact paths in the command.
2478
+ value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
2479
+ }
2480
+ ],
2481
+ verdict: "allow",
2482
+ reason: "Deleting a known-safe build artifact path"
2483
+ },
2484
+ {
2485
+ name: "review-rm",
2486
+ tool: "*",
2487
+ conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
2488
+ verdict: "review",
2489
+ reason: "rm can permanently delete files \u2014 confirm the target path"
2465
2490
  }
2466
- claudeConfig.mcpServers = servers;
2467
- writeJson(mcpPath, claudeConfig);
2468
- console.log(import_chalk3.default.green(`
2469
- \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
2470
- anythingChanged = true;
2471
- } else {
2472
- console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
2473
- }
2474
- console.log("");
2475
- }
2476
- if (!anythingChanged && serversToWrap.length === 0) {
2477
- console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
2478
- printDaemonTip();
2479
- return;
2480
- }
2481
- if (anythingChanged) {
2482
- console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
2483
- console.log(import_chalk3.default.gray(" Restart Claude Code for changes to take effect."));
2484
- printDaemonTip();
2485
- }
2486
- }
2487
- async function setupGemini() {
2488
- const homeDir2 = import_os3.default.homedir();
2489
- const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
2490
- const settings = readJson(settingsPath) ?? {};
2491
- const servers = settings.mcpServers ?? {};
2492
- let anythingChanged = false;
2493
- if (!settings.hooks) settings.hooks = {};
2494
- const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
2495
- (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
2496
- );
2497
- if (!hasBeforeHook) {
2498
- if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
2499
- if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
2500
- settings.hooks.BeforeTool.push({
2501
- matcher: ".*",
2502
- hooks: [
2503
- {
2504
- name: "node9-check",
2505
- type: "command",
2506
- command: fullPathCommand("check"),
2507
- timeout: 6e5
2508
- }
2509
- ]
2510
- });
2511
- console.log(import_chalk3.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
2512
- anythingChanged = true;
2513
- }
2514
- const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
2515
- (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
2516
- );
2517
- if (!hasAfterHook) {
2518
- if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
2519
- if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
2520
- settings.hooks.AfterTool.push({
2521
- matcher: ".*",
2522
- hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
2523
- });
2524
- console.log(import_chalk3.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
2525
- anythingChanged = true;
2526
- }
2527
- if (anythingChanged) {
2528
- writeJson(settingsPath, settings);
2529
- console.log("");
2530
- }
2531
- const serversToWrap = [];
2532
- for (const [name, server] of Object.entries(servers)) {
2533
- if (!server.command || server.command === "node9") continue;
2534
- const parts = [server.command, ...server.args ?? []];
2535
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
2491
+ ];
2492
+ cachedConfig = null;
2493
+ DAEMON_PORT = 7391;
2494
+ DAEMON_HOST = "127.0.0.1";
2495
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path4.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
2536
2496
  }
2537
- if (serversToWrap.length > 0) {
2538
- console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
2539
- console.log(import_chalk3.default.white(` ${settingsPath} (mcpServers)`));
2540
- for (const { name, originalCmd } of serversToWrap) {
2541
- console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
2542
- }
2543
- console.log("");
2544
- const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
2545
- if (proceed) {
2546
- for (const { name, parts } of serversToWrap) {
2547
- servers[name] = { ...servers[name], command: "node9", args: parts };
2548
- }
2549
- settings.mcpServers = servers;
2550
- writeJson(settingsPath, settings);
2551
- console.log(import_chalk3.default.green(`
2552
- \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
2553
- anythingChanged = true;
2554
- } else {
2555
- console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
2556
- }
2557
- console.log("");
2558
- }
2559
- if (!anythingChanged && serversToWrap.length === 0) {
2560
- console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
2561
- printDaemonTip();
2562
- return;
2563
- }
2564
- if (anythingChanged) {
2565
- console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
2566
- console.log(import_chalk3.default.gray(" Restart Gemini CLI for changes to take effect."));
2567
- printDaemonTip();
2568
- }
2569
- }
2570
- async function setupCursor() {
2571
- const homeDir2 = import_os3.default.homedir();
2572
- const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
2573
- const mcpConfig = readJson(mcpPath) ?? {};
2574
- const servers = mcpConfig.mcpServers ?? {};
2575
- let anythingChanged = false;
2576
- const serversToWrap = [];
2577
- for (const [name, server] of Object.entries(servers)) {
2578
- if (!server.command || server.command === "node9") continue;
2579
- const parts = [server.command, ...server.args ?? []];
2580
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
2581
- }
2582
- if (serversToWrap.length > 0) {
2583
- console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
2584
- console.log(import_chalk3.default.white(` ${mcpPath}`));
2585
- for (const { name, originalCmd } of serversToWrap) {
2586
- console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
2587
- }
2588
- console.log("");
2589
- const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
2590
- if (proceed) {
2591
- for (const { name, parts } of serversToWrap) {
2592
- servers[name] = { ...servers[name], command: "node9", args: parts };
2593
- }
2594
- mcpConfig.mcpServers = servers;
2595
- writeJson(mcpPath, mcpConfig);
2596
- console.log(import_chalk3.default.green(`
2597
- \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
2598
- anythingChanged = true;
2599
- } else {
2600
- console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
2601
- }
2602
- console.log("");
2603
- }
2604
- console.log(
2605
- import_chalk3.default.yellow(
2606
- " \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
2607
- )
2608
- );
2609
- console.log("");
2610
- if (!anythingChanged && serversToWrap.length === 0) {
2611
- console.log(
2612
- import_chalk3.default.blue(
2613
- "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
2614
- )
2615
- );
2616
- printDaemonTip();
2617
- return;
2618
- }
2619
- if (anythingChanged) {
2620
- console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
2621
- console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
2622
- printDaemonTip();
2623
- }
2624
- }
2625
-
2626
- // src/daemon/ui.html
2627
- var ui_default = `<!doctype html>
2628
- <html lang="en">
2629
- <head>
2630
- <meta charset="UTF-8" />
2631
- <meta name="viewport" content="width=device-width, initial-scale=1" />
2632
- <title>Node9 Security Guard</title>
2633
- <style>
2634
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Fira+Code:wght@400;500&display=swap');
2635
- :root {
2636
- --bg: #0a0c10;
2637
- --card: #1c2128;
2638
- --panel: #161b22;
2639
- --border: #30363d;
2640
- --text: #adbac7;
2641
- --text-bright: #cdd9e5;
2642
- --muted: #768390;
2643
- --primary: #f0883e;
2644
- --success: #347d39;
2645
- --danger: #c93c37;
2646
- --accent: #539bf5;
2497
+ });
2498
+
2499
+ // src/daemon/ui.html
2500
+ var ui_default;
2501
+ var init_ui = __esm({
2502
+ "src/daemon/ui.html"() {
2503
+ ui_default = `<!doctype html>
2504
+ <html lang="en">
2505
+ <head>
2506
+ <meta charset="UTF-8" />
2507
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2508
+ <title>Node9 Security Guard</title>
2509
+ <style>
2510
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Fira+Code:wght@400;500&display=swap');
2511
+ :root {
2512
+ --bg: #0a0c10;
2513
+ --card: #1c2128;
2514
+ --panel: #161b22;
2515
+ --border: #30363d;
2516
+ --text: #adbac7;
2517
+ --text-bright: #cdd9e5;
2518
+ --muted: #768390;
2519
+ --primary: #f0883e;
2520
+ --success: #347d39;
2521
+ --danger: #c93c37;
2522
+ --accent: #539bf5;
2647
2523
  }
2648
2524
  * {
2649
2525
  box-sizing: border-box;
2650
2526
  margin: 0;
2651
2527
  padding: 0;
2652
2528
  }
2529
+ html,
2530
+ body {
2531
+ height: 100%;
2532
+ overflow: hidden;
2533
+ }
2653
2534
  body {
2654
2535
  background: var(--bg);
2655
2536
  color: var(--text);
@@ -2657,16 +2538,17 @@ var ui_default = `<!doctype html>
2657
2538
  'Inter',
2658
2539
  -apple-system,
2659
2540
  sans-serif;
2660
- min-height: 100vh;
2661
2541
  }
2662
2542
 
2663
2543
  .shell {
2664
- max-width: 1000px;
2544
+ max-width: 1440px;
2545
+ height: 100vh;
2665
2546
  margin: 0 auto;
2666
- padding: 32px 24px 48px;
2547
+ padding: 16px 20px 16px;
2667
2548
  display: grid;
2668
2549
  grid-template-rows: auto 1fr;
2669
- gap: 24px;
2550
+ gap: 16px;
2551
+ overflow: hidden;
2670
2552
  }
2671
2553
  header {
2672
2554
  display: flex;
@@ -2703,9 +2585,10 @@ var ui_default = `<!doctype html>
2703
2585
 
2704
2586
  .body {
2705
2587
  display: grid;
2706
- grid-template-columns: 1fr 272px;
2707
- gap: 20px;
2708
- align-items: start;
2588
+ grid-template-columns: 360px 1fr 270px;
2589
+ gap: 16px;
2590
+ min-height: 0;
2591
+ overflow: hidden;
2709
2592
  }
2710
2593
 
2711
2594
  .warning-banner {
@@ -2725,6 +2608,10 @@ var ui_default = `<!doctype html>
2725
2608
 
2726
2609
  .main {
2727
2610
  min-width: 0;
2611
+ min-height: 0;
2612
+ overflow-y: auto;
2613
+ scrollbar-width: thin;
2614
+ scrollbar-color: var(--border) transparent;
2728
2615
  }
2729
2616
  .section-title {
2730
2617
  font-size: 11px;
@@ -2755,14 +2642,64 @@ var ui_default = `<!doctype html>
2755
2642
  background: var(--card);
2756
2643
  border: 1px solid var(--border);
2757
2644
  border-radius: 14px;
2758
- padding: 24px;
2759
- margin-bottom: 16px;
2645
+ padding: 20px;
2646
+ margin-bottom: 14px;
2760
2647
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
2761
2648
  animation: pop 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
2762
2649
  }
2763
2650
  .card.slack-viewer {
2764
2651
  border-color: rgba(83, 155, 245, 0.3);
2765
2652
  }
2653
+ .card-header {
2654
+ display: flex;
2655
+ align-items: center;
2656
+ gap: 8px;
2657
+ margin-bottom: 12px;
2658
+ padding-bottom: 12px;
2659
+ border-bottom: 1px solid var(--border);
2660
+ }
2661
+ .card-header-icon {
2662
+ font-size: 16px;
2663
+ }
2664
+ .card-header-title {
2665
+ font-size: 12px;
2666
+ font-weight: 700;
2667
+ color: var(--text-bright);
2668
+ text-transform: uppercase;
2669
+ letter-spacing: 0.5px;
2670
+ }
2671
+ .card-timer {
2672
+ margin-left: auto;
2673
+ font-size: 11px;
2674
+ font-family: 'Fira Code', monospace;
2675
+ color: var(--muted);
2676
+ background: rgba(48, 54, 61, 0.6);
2677
+ padding: 2px 8px;
2678
+ border-radius: 5px;
2679
+ }
2680
+ .card-timer.urgent {
2681
+ color: var(--danger);
2682
+ background: rgba(201, 60, 55, 0.1);
2683
+ }
2684
+ .btn-allow {
2685
+ background: var(--success);
2686
+ color: #fff;
2687
+ grid-column: span 2;
2688
+ font-size: 14px;
2689
+ padding: 13px 14px;
2690
+ }
2691
+ .btn-deny {
2692
+ background: rgba(201, 60, 55, 0.15);
2693
+ color: #e5534b;
2694
+ border: 1px solid rgba(201, 60, 55, 0.3);
2695
+ grid-column: span 2;
2696
+ }
2697
+ .btn-deny:hover:not(:disabled) {
2698
+ background: var(--danger);
2699
+ color: #fff;
2700
+ border-color: transparent;
2701
+ filter: none;
2702
+ }
2766
2703
  @keyframes pop {
2767
2704
  from {
2768
2705
  opacity: 0;
@@ -2970,24 +2907,178 @@ var ui_default = `<!doctype html>
2970
2907
  cursor: not-allowed;
2971
2908
  }
2972
2909
 
2910
+ .flight-col {
2911
+ display: flex;
2912
+ flex-direction: column;
2913
+ min-height: 0;
2914
+ overflow: hidden;
2915
+ }
2916
+ .flight-panel {
2917
+ flex: 1;
2918
+ min-height: 0;
2919
+ display: flex;
2920
+ flex-direction: column;
2921
+ overflow: hidden;
2922
+ }
2973
2923
  .sidebar {
2974
2924
  display: flex;
2975
2925
  flex-direction: column;
2976
2926
  gap: 12px;
2977
- position: sticky;
2978
- top: 24px;
2927
+ min-height: 0;
2928
+ overflow-y: auto;
2929
+ scrollbar-width: thin;
2930
+ scrollbar-color: var(--border) transparent;
2979
2931
  }
2980
2932
  .panel {
2981
2933
  background: var(--panel);
2982
2934
  border: 1px solid var(--border);
2983
2935
  border-radius: 12px;
2984
- padding: 16px;
2936
+ padding: 14px;
2937
+ }
2938
+ /* \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 */
2939
+ #activity-feed {
2940
+ display: flex;
2941
+ flex-direction: column;
2942
+ gap: 4px;
2943
+ margin-top: 4px;
2944
+ flex: 1;
2945
+ min-height: 0;
2946
+ overflow-y: auto;
2947
+ scrollbar-width: thin;
2948
+ scrollbar-color: var(--border) transparent;
2949
+ }
2950
+ .feed-row {
2951
+ display: grid;
2952
+ grid-template-columns: 58px 20px 1fr 48px;
2953
+ align-items: start;
2954
+ gap: 6px;
2955
+ background: rgba(22, 27, 34, 0.6);
2956
+ border: 1px solid var(--border);
2957
+ padding: 7px 10px;
2958
+ border-radius: 7px;
2959
+ font-size: 11px;
2960
+ animation: frSlideIn 0.15s ease-out;
2961
+ transition: background 0.1s;
2962
+ cursor: default;
2963
+ }
2964
+ .feed-row:hover {
2965
+ background: rgba(30, 38, 48, 0.9);
2966
+ border-color: rgba(83, 155, 245, 0.2);
2967
+ }
2968
+ @keyframes frSlideIn {
2969
+ from {
2970
+ opacity: 0;
2971
+ transform: translateX(-4px);
2972
+ }
2973
+ to {
2974
+ opacity: 1;
2975
+ transform: none;
2976
+ }
2977
+ }
2978
+ .feed-ts {
2979
+ color: var(--muted);
2980
+ font-family: monospace;
2981
+ font-size: 9px;
2982
+ }
2983
+ .feed-icon {
2984
+ text-align: center;
2985
+ font-size: 13px;
2986
+ }
2987
+ .feed-content {
2988
+ min-width: 0;
2989
+ color: var(--text-bright);
2990
+ word-break: break-all;
2991
+ }
2992
+ .feed-args {
2993
+ display: block;
2994
+ color: var(--muted);
2995
+ font-family: monospace;
2996
+ margin-top: 2px;
2997
+ font-size: 10px;
2998
+ word-break: break-all;
2999
+ }
3000
+ .feed-badge {
3001
+ text-align: right;
3002
+ font-weight: 700;
3003
+ font-size: 9px;
3004
+ letter-spacing: 0.03em;
3005
+ }
3006
+ .fr-pending {
3007
+ color: var(--muted);
3008
+ }
3009
+ .fr-allow {
3010
+ color: #57ab5a;
3011
+ }
3012
+ .fr-block {
3013
+ color: var(--danger);
3014
+ }
3015
+ .fr-dlp {
3016
+ color: var(--primary);
3017
+ animation: frBlink 1s infinite;
3018
+ }
3019
+ @keyframes frBlink {
3020
+ 50% {
3021
+ opacity: 0.4;
3022
+ }
3023
+ }
3024
+ .fr-dlp-row {
3025
+ border-color: var(--primary) !important;
3026
+ }
3027
+ .feed-clear-btn {
3028
+ background: transparent;
3029
+ border: none;
3030
+ color: var(--muted);
3031
+ font-size: 10px;
3032
+ padding: 0;
3033
+ cursor: pointer;
3034
+ margin-left: auto;
3035
+ font-family: inherit;
3036
+ font-weight: 500;
3037
+ transition: color 0.15s;
3038
+ }
3039
+ .feed-clear-btn:hover {
3040
+ color: var(--text);
3041
+ filter: none;
3042
+ transform: none;
3043
+ }
3044
+ /* \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 */
3045
+ .shield-row {
3046
+ display: flex;
3047
+ align-items: flex-start;
3048
+ gap: 10px;
3049
+ padding: 8px 0;
3050
+ border-bottom: 1px solid var(--border);
3051
+ }
3052
+ .shield-row:last-child {
3053
+ border-bottom: none;
3054
+ padding-bottom: 0;
3055
+ }
3056
+ .shield-row:first-child {
3057
+ padding-top: 0;
3058
+ }
3059
+ .shield-info {
3060
+ flex: 1;
3061
+ min-width: 0;
2985
3062
  }
3063
+ .shield-name {
3064
+ font-size: 12px;
3065
+ color: var(--text-bright);
3066
+ font-weight: 600;
3067
+ font-family: 'Fira Code', monospace;
3068
+ }
3069
+ .shield-desc {
3070
+ font-size: 10px;
3071
+ color: var(--muted);
3072
+ margin-top: 2px;
3073
+ line-height: 1.4;
3074
+ }
3075
+
2986
3076
  .panel-title {
2987
3077
  font-size: 12px;
2988
3078
  font-weight: 700;
2989
3079
  color: var(--text-bright);
2990
3080
  margin-bottom: 12px;
3081
+ flex-shrink: 0;
2991
3082
  display: flex;
2992
3083
  align-items: center;
2993
3084
  gap: 6px;
@@ -2995,8 +3086,8 @@ var ui_default = `<!doctype html>
2995
3086
  .setting-row {
2996
3087
  display: flex;
2997
3088
  align-items: flex-start;
2998
- gap: 12px;
2999
- margin-bottom: 12px;
3089
+ gap: 10px;
3090
+ margin-bottom: 8px;
3000
3091
  }
3001
3092
  .setting-row:last-child {
3002
3093
  margin-bottom: 0;
@@ -3005,20 +3096,21 @@ var ui_default = `<!doctype html>
3005
3096
  flex: 1;
3006
3097
  }
3007
3098
  .setting-label {
3008
- font-size: 12px;
3099
+ font-size: 11px;
3009
3100
  color: var(--text-bright);
3010
- margin-bottom: 3px;
3101
+ margin-bottom: 2px;
3102
+ font-weight: 600;
3011
3103
  }
3012
3104
  .setting-desc {
3013
- font-size: 11px;
3105
+ font-size: 10px;
3014
3106
  color: var(--muted);
3015
- line-height: 1.5;
3107
+ line-height: 1.4;
3016
3108
  }
3017
3109
  .toggle {
3018
3110
  position: relative;
3019
3111
  display: inline-block;
3020
- width: 40px;
3021
- height: 22px;
3112
+ width: 34px;
3113
+ height: 19px;
3022
3114
  flex-shrink: 0;
3023
3115
  margin-top: 1px;
3024
3116
  }
@@ -3038,8 +3130,8 @@ var ui_default = `<!doctype html>
3038
3130
  .slider:before {
3039
3131
  content: '';
3040
3132
  position: absolute;
3041
- width: 16px;
3042
- height: 16px;
3133
+ width: 13px;
3134
+ height: 13px;
3043
3135
  left: 3px;
3044
3136
  bottom: 3px;
3045
3137
  background: #fff;
@@ -3050,7 +3142,7 @@ var ui_default = `<!doctype html>
3050
3142
  background: var(--success);
3051
3143
  }
3052
3144
  input:checked + .slider:before {
3053
- transform: translateX(18px);
3145
+ transform: translateX(15px);
3054
3146
  }
3055
3147
  input:disabled + .slider {
3056
3148
  opacity: 0.4;
@@ -3209,12 +3301,17 @@ var ui_default = `<!doctype html>
3209
3301
  border: 1px solid var(--border);
3210
3302
  }
3211
3303
 
3212
- @media (max-width: 680px) {
3304
+ @media (max-width: 960px) {
3213
3305
  .body {
3214
- grid-template-columns: 1fr;
3306
+ grid-template-columns: 1fr 220px;
3307
+ }
3308
+ .flight-col {
3309
+ display: none;
3215
3310
  }
3216
- .sidebar {
3217
- position: static;
3311
+ }
3312
+ @media (max-width: 640px) {
3313
+ .body {
3314
+ grid-template-columns: 1fr;
3218
3315
  }
3219
3316
  }
3220
3317
  </style>
@@ -3228,6 +3325,19 @@ var ui_default = `<!doctype html>
3228
3325
  </header>
3229
3326
 
3230
3327
  <div class="body">
3328
+ <div class="flight-col">
3329
+ <div class="panel flight-panel">
3330
+ <div class="panel-title">
3331
+ \u{1F6F0}\uFE0F Flight Recorder
3332
+ <span style="font-weight: 400; color: var(--muted); font-size: 11px">live</span>
3333
+ <button class="feed-clear-btn" onclick="clearFeed()">clear</button>
3334
+ </div>
3335
+ <div id="activity-feed">
3336
+ <span class="decisions-empty">Waiting for agent activity\u2026</span>
3337
+ </div>
3338
+ </div>
3339
+ </div>
3340
+
3231
3341
  <div class="main">
3232
3342
  <div id="warnBanner" class="warning-banner">
3233
3343
  \u26A0\uFE0F Auto-start is off \u2014 daemon started manually. Run "node9 daemon stop" to stop it, or
@@ -3308,6 +3418,11 @@ var ui_default = `<!doctype html>
3308
3418
  <div id="slackStatusLine" class="slack-status-line">No key saved</div>
3309
3419
  </div>
3310
3420
 
3421
+ <div class="panel">
3422
+ <div class="panel-title">\u{1F6E1}\uFE0F Active Shields</div>
3423
+ <div id="shieldsList"><span class="decisions-empty">Loading\u2026</span></div>
3424
+ </div>
3425
+
3311
3426
  <div class="panel">
3312
3427
  <div class="panel-title">\u{1F4CB} Persistent Decisions</div>
3313
3428
  <div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
@@ -3353,14 +3468,23 @@ var ui_default = `<!doctype html>
3353
3468
 
3354
3469
  function updateDenyButton(id, timestamp) {
3355
3470
  const btn = document.querySelector('#c-' + id + ' .btn-deny');
3471
+ const timer = document.querySelector('#timer-' + id);
3356
3472
  if (!btn) return;
3357
3473
  const elapsed = Date.now() - timestamp;
3358
3474
  const remaining = Math.max(0, Math.ceil((autoDenyMs - elapsed) / 1000));
3359
3475
  if (remaining <= 0) {
3360
- btn.textContent = 'Auto-Denying...';
3476
+ btn.textContent = '\u23F3 Auto-Denying\u2026';
3361
3477
  btn.disabled = true;
3478
+ if (timer) {
3479
+ timer.textContent = 'auto-deny';
3480
+ timer.className = 'card-timer urgent';
3481
+ }
3362
3482
  } else {
3363
- btn.textContent = 'Block Action (' + remaining + 's)';
3483
+ btn.textContent = '\u{1F6AB} Block this Action';
3484
+ if (timer) {
3485
+ timer.textContent = remaining + 's';
3486
+ timer.className = 'card-timer' + (remaining < 15 ? ' urgent' : '');
3487
+ }
3364
3488
  setTimeout(() => updateDenyButton(id, timestamp), 1000);
3365
3489
  }
3366
3490
  }
@@ -3376,34 +3500,61 @@ var ui_default = `<!doctype html>
3376
3500
  empty.style.display = requests.size === 0 ? 'block' : 'none';
3377
3501
  }
3378
3502
 
3379
- function sendDecision(id, decision, persist) {
3503
+ function setCardBusy(card, busy) {
3504
+ if (!card) return;
3505
+ card.querySelectorAll('button').forEach((b) => (b.disabled = busy));
3506
+ card.style.opacity = busy ? '0.5' : '1';
3507
+ }
3508
+
3509
+ function showCardError(card, msg) {
3510
+ if (!card) return;
3511
+ card.style.outline = '2px solid #f87171';
3512
+ let err = card.querySelector('.card-error');
3513
+ if (!err) {
3514
+ err = document.createElement('p');
3515
+ err.className = 'card-error';
3516
+ err.style.cssText = 'color:#f87171;font-size:11px;margin:6px 0 0;';
3517
+ card.appendChild(err);
3518
+ }
3519
+ err.textContent = '\u26A0 ' + msg + ' \u2014 please try again or refresh.';
3520
+ }
3521
+
3522
+ async function sendDecision(id, decision, persist) {
3380
3523
  const card = document.getElementById('c-' + id);
3381
- if (card) card.style.opacity = '0.5';
3382
- fetch('/decision/' + id, {
3383
- method: 'POST',
3384
- headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3385
- body: JSON.stringify({ decision, persist: !!persist }),
3386
- });
3387
- setTimeout(() => {
3524
+ setCardBusy(card, true);
3525
+ try {
3526
+ const res = await fetch('/decision/' + id, {
3527
+ method: 'POST',
3528
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3529
+ body: JSON.stringify({ decision, persist: !!persist }),
3530
+ });
3531
+ if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3388
3532
  card?.remove();
3389
3533
  requests.delete(id);
3390
3534
  refresh();
3391
- }, 200);
3535
+ } catch (err) {
3536
+ setCardBusy(card, false);
3537
+ showCardError(card, err.message || 'Network error');
3538
+ }
3392
3539
  }
3393
3540
 
3394
- function sendTrust(id, duration) {
3541
+ async function sendTrust(id, duration) {
3395
3542
  const card = document.getElementById('c-' + id);
3396
- if (card) card.style.opacity = '0.5';
3397
- fetch('/decision/' + id, {
3398
- method: 'POST',
3399
- headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3400
- body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
3401
- });
3402
- setTimeout(() => {
3543
+ setCardBusy(card, true);
3544
+ try {
3545
+ const res = await fetch('/decision/' + id, {
3546
+ method: 'POST',
3547
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3548
+ body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
3549
+ });
3550
+ if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3403
3551
  card?.remove();
3404
3552
  requests.delete(id);
3405
3553
  refresh();
3406
- }, 200);
3554
+ } catch (err) {
3555
+ setCardBusy(card, false);
3556
+ showCardError(card, err.message || 'Network error');
3557
+ }
3407
3558
  }
3408
3559
 
3409
3560
  function renderPayload(req) {
@@ -3454,16 +3605,21 @@ var ui_default = `<!doctype html>
3454
3605
  const mcpLabel = req.mcpServer ? esc(req.mcpServer) : null;
3455
3606
  const dis = isSlack ? 'disabled' : '';
3456
3607
  card.innerHTML = \`
3608
+ <div class="card-header">
3609
+ <span class="card-header-icon">\${isSlack ? '\u26A1' : '\u26A0\uFE0F'}</span>
3610
+ <span class="card-header-title">\${isSlack ? 'Awaiting Cloud Approval' : 'Action Required'}</span>
3611
+ <span class="card-timer" id="timer-\${req.id}">\${autoDenyMs > 0 ? Math.ceil(autoDenyMs / 1000) + 's' : ''}</span>
3612
+ </div>
3457
3613
  <div class="source-row">
3458
3614
  <span class="agent-badge">\${agentLabel}</span>
3459
3615
  \${mcpLabel ? \`<span class="source-arrow">\u2192</span><span class="mcp-badge">mcp::\${mcpLabel}</span>\` : ''}
3460
3616
  </div>
3461
3617
  <div class="tool-chip">\${esc(req.toolName)}</div>
3462
- \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Slack approval \u2014 view only</div>' : ''}
3618
+ \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
3463
3619
  \${renderPayload(req)}
3464
3620
  <div class="actions" id="act-\${req.id}">
3465
- <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>Approve Execution</button>
3466
- <button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>Block Action</button>
3621
+ <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
3622
+ <button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>\u{1F6AB} Block this Action</button>
3467
3623
  <div class="trust-row\${trustEnabled ? ' show' : ''}" id="tr-\${req.id}">
3468
3624
  <button class="btn-trust" onclick="sendTrust('\${req.id}','30m')" \${dis}>\u23F1 Trust 30m</button>
3469
3625
  <button class="btn-trust" onclick="sendTrust('\${req.id}','1h')" \${dis}>\u23F1 Trust 1h</button>
@@ -3523,9 +3679,84 @@ var ui_default = `<!doctype html>
3523
3679
  ev.addEventListener('slack-status', (e) => {
3524
3680
  applySlackStatus(JSON.parse(e.data));
3525
3681
  });
3682
+ ev.addEventListener('shields-status', (e) => {
3683
+ renderShields(JSON.parse(e.data).shields);
3684
+ });
3685
+
3686
+ // \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
3687
+ ev.addEventListener('activity', (e) => {
3688
+ const data = JSON.parse(e.data);
3689
+ const feed = document.getElementById('activity-feed');
3690
+ // Remove placeholder on first item
3691
+ const placeholder = feed.querySelector('.decisions-empty');
3692
+ if (placeholder) placeholder.remove();
3693
+
3694
+ const time = new Date(data.ts).toLocaleTimeString([], {
3695
+ hour12: false,
3696
+ hour: '2-digit',
3697
+ minute: '2-digit',
3698
+ second: '2-digit',
3699
+ });
3700
+ const icon = frIcon(data.tool);
3701
+ const argsStr = JSON.stringify(data.args ?? {});
3702
+ const argsPreview = esc(argsStr.length > 120 ? argsStr.slice(0, 120) + '\u2026' : argsStr);
3703
+
3704
+ const row = document.createElement('div');
3705
+ row.className = 'feed-row';
3706
+ row.id = 'fr-' + data.id;
3707
+ row.innerHTML = \`
3708
+ <span class="feed-ts">\${time}</span>
3709
+ <span class="feed-icon">\${icon}</span>
3710
+ <span class="feed-content"><strong>\${esc(data.tool)}</strong><span class="feed-args">\${argsPreview}</span></span>
3711
+ <span class="feed-badge fr-pending">\u25CF</span>
3712
+ \`;
3713
+ feed.prepend(row);
3714
+ if (feed.children.length > 100) feed.lastChild.remove();
3715
+ });
3716
+
3717
+ ev.addEventListener('activity-result', (e) => {
3718
+ const { id, status, label } = JSON.parse(e.data);
3719
+ const row = document.getElementById('fr-' + id);
3720
+ if (!row) return;
3721
+ const badge = row.querySelector('.feed-badge');
3722
+ if (status === 'allow') {
3723
+ badge.textContent = 'ALLOW';
3724
+ badge.className = 'feed-badge fr-allow';
3725
+ } else if (status === 'dlp') {
3726
+ badge.textContent = '\u{1F6E1}\uFE0F DLP';
3727
+ badge.className = 'feed-badge fr-dlp';
3728
+ row.classList.add('fr-dlp-row');
3729
+ } else {
3730
+ badge.textContent = 'BLOCK';
3731
+ badge.className = 'feed-badge fr-block';
3732
+ }
3733
+ });
3526
3734
  }
3527
3735
  connect();
3528
3736
 
3737
+ const FR_ICONS = {
3738
+ bash: '\u{1F4BB}',
3739
+ read: '\u{1F4D6}',
3740
+ edit: '\u270F\uFE0F',
3741
+ write: '\u270F\uFE0F',
3742
+ glob: '\u{1F4C2}',
3743
+ grep: '\u{1F50D}',
3744
+ agent: '\u{1F916}',
3745
+ search: '\u{1F50D}',
3746
+ sql: '\u{1F5C4}\uFE0F',
3747
+ query: '\u{1F5C4}\uFE0F',
3748
+ list: '\u{1F4C2}',
3749
+ delete: '\u{1F5D1}\uFE0F',
3750
+ web: '\u{1F310}',
3751
+ };
3752
+ function frIcon(tool) {
3753
+ const t = (tool || '').toLowerCase();
3754
+ for (const [k, v] of Object.entries(FR_ICONS)) {
3755
+ if (t.includes(k)) return v;
3756
+ }
3757
+ return '\u{1F6E0}\uFE0F';
3758
+ }
3759
+
3529
3760
  function saveSetting(key, value) {
3530
3761
  fetch('/settings', {
3531
3762
  method: 'POST',
@@ -3615,6 +3846,49 @@ var ui_default = `<!doctype html>
3615
3846
  }
3616
3847
  }
3617
3848
 
3849
+ function clearFeed() {
3850
+ const feed = document.getElementById('activity-feed');
3851
+ feed.innerHTML = '<span class="decisions-empty">Feed cleared.</span>';
3852
+ }
3853
+
3854
+ function renderShields(shields) {
3855
+ const list = document.getElementById('shieldsList');
3856
+ if (!shields || shields.length === 0) {
3857
+ list.innerHTML = '<span class="decisions-empty">No shields available.</span>';
3858
+ return;
3859
+ }
3860
+ list.innerHTML = shields
3861
+ .map(
3862
+ (s) => \`
3863
+ <div class="shield-row">
3864
+ <div class="shield-info">
3865
+ <div class="shield-name">\${esc(s.name)}</div>
3866
+ <div class="shield-desc">\${esc(s.description)}</div>
3867
+ </div>
3868
+ <label class="toggle">
3869
+ <input type="checkbox" \${s.active ? 'checked' : ''}
3870
+ onchange="toggleShield('\${esc(s.name)}', this.checked)" />
3871
+ <span class="slider"></span>
3872
+ </label>
3873
+ </div>
3874
+ \`
3875
+ )
3876
+ .join('');
3877
+ }
3878
+
3879
+ function toggleShield(name, active) {
3880
+ fetch('/shields', {
3881
+ method: 'POST',
3882
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3883
+ body: JSON.stringify({ name, active }),
3884
+ }).catch(() => {});
3885
+ }
3886
+
3887
+ fetch('/shields')
3888
+ .then((r) => r.json())
3889
+ .then(({ shields }) => renderShields(shields))
3890
+ .catch(() => {});
3891
+
3618
3892
  function renderDecisions(decisions) {
3619
3893
  const dl = document.getElementById('decisionsList');
3620
3894
  const entries = Object.entries(decisions);
@@ -3661,31 +3935,24 @@ var ui_default = `<!doctype html>
3661
3935
  </body>
3662
3936
  </html>
3663
3937
  `;
3938
+ }
3939
+ });
3664
3940
 
3665
3941
  // src/daemon/ui.ts
3666
- var UI_HTML_TEMPLATE = ui_default;
3942
+ var UI_HTML_TEMPLATE;
3943
+ var init_ui2 = __esm({
3944
+ "src/daemon/ui.ts"() {
3945
+ "use strict";
3946
+ init_ui();
3947
+ UI_HTML_TEMPLATE = ui_default;
3948
+ }
3949
+ });
3667
3950
 
3668
3951
  // src/daemon/index.ts
3669
- var import_http = __toESM(require("http"));
3670
- var import_fs4 = __toESM(require("fs"));
3671
- var import_path6 = __toESM(require("path"));
3672
- var import_os4 = __toESM(require("os"));
3673
- var import_child_process2 = require("child_process");
3674
- var import_crypto2 = require("crypto");
3675
- var import_chalk4 = __toESM(require("chalk"));
3676
- var DAEMON_PORT2 = 7391;
3677
- var DAEMON_HOST2 = "127.0.0.1";
3678
- var homeDir = import_os4.default.homedir();
3679
- var DAEMON_PID_FILE = import_path6.default.join(homeDir, ".node9", "daemon.pid");
3680
- var DECISIONS_FILE = import_path6.default.join(homeDir, ".node9", "decisions.json");
3681
- var GLOBAL_CONFIG_FILE = import_path6.default.join(homeDir, ".node9", "config.json");
3682
- var CREDENTIALS_FILE = import_path6.default.join(homeDir, ".node9", "credentials.json");
3683
- var AUDIT_LOG_FILE = import_path6.default.join(homeDir, ".node9", "audit.log");
3684
- var TRUST_FILE2 = import_path6.default.join(homeDir, ".node9", "trust.json");
3685
3952
  function atomicWriteSync2(filePath, data, options) {
3686
3953
  const dir = import_path6.default.dirname(filePath);
3687
3954
  if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
3688
- const tmpPath = `${filePath}.${(0, import_crypto2.randomUUID)()}.tmp`;
3955
+ const tmpPath = `${filePath}.${(0, import_crypto3.randomUUID)()}.tmp`;
3689
3956
  import_fs4.default.writeFileSync(tmpPath, data, options);
3690
3957
  import_fs4.default.renameSync(tmpPath, filePath);
3691
3958
  }
@@ -3703,12 +3970,6 @@ function writeTrustEntry(toolName, durationMs) {
3703
3970
  } catch {
3704
3971
  }
3705
3972
  }
3706
- var TRUST_DURATIONS = {
3707
- "30m": 30 * 6e4,
3708
- "1h": 60 * 6e4,
3709
- "2h": 2 * 60 * 6e4
3710
- };
3711
- var SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
3712
3973
  function redactArgs(value) {
3713
3974
  if (!value || typeof value !== "object") return value;
3714
3975
  if (Array.isArray(value)) return value.map(redactArgs);
@@ -3743,7 +4004,6 @@ function getAuditHistory(limit = 20) {
3743
4004
  return [];
3744
4005
  }
3745
4006
  }
3746
- var AUTO_DENY_MS = 12e4;
3747
4007
  function getOrgName() {
3748
4008
  try {
3749
4009
  if (import_fs4.default.existsSync(CREDENTIALS_FILE)) {
@@ -3753,7 +4013,6 @@ function getOrgName() {
3753
4013
  }
3754
4014
  return null;
3755
4015
  }
3756
- var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
3757
4016
  function hasStoredSlackKey() {
3758
4017
  return import_fs4.default.existsSync(CREDENTIALS_FILE);
3759
4018
  }
@@ -3769,11 +4028,6 @@ function writeGlobalSetting(key, value) {
3769
4028
  config.settings[key] = value;
3770
4029
  atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
3771
4030
  }
3772
- var pending = /* @__PURE__ */ new Map();
3773
- var sseClients = /* @__PURE__ */ new Set();
3774
- var abandonTimer = null;
3775
- var daemonServer = null;
3776
- var hadBrowserClient = false;
3777
4031
  function abandonPending() {
3778
4032
  abandonTimer = null;
3779
4033
  pending.forEach((entry, id) => {
@@ -3795,6 +4049,18 @@ function abandonPending() {
3795
4049
  }
3796
4050
  }
3797
4051
  function broadcast(event, data) {
4052
+ if (event === "activity") {
4053
+ activityRing.push({ event, data });
4054
+ if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
4055
+ } else if (event === "activity-result") {
4056
+ const { id, status, label } = data;
4057
+ for (let i = activityRing.length - 1; i >= 0; i--) {
4058
+ if (activityRing[i].data.id === id) {
4059
+ Object.assign(activityRing[i].data, { status, label });
4060
+ break;
4061
+ }
4062
+ }
4063
+ }
3798
4064
  const msg = `event: ${event}
3799
4065
  data: ${JSON.stringify(data)}
3800
4066
 
@@ -3810,7 +4076,7 @@ data: ${JSON.stringify(data)}
3810
4076
  function openBrowser(url) {
3811
4077
  try {
3812
4078
  const args = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
3813
- (0, import_child_process2.spawn)(args[0], args.slice(1), { detached: true, stdio: "ignore" }).unref();
4079
+ (0, import_child_process3.spawn)(args[0], args.slice(1), { detached: true, stdio: "ignore" }).unref();
3814
4080
  } catch {
3815
4081
  }
3816
4082
  }
@@ -3840,13 +4106,15 @@ function writePersistentDecision(toolName, decision) {
3840
4106
  }
3841
4107
  }
3842
4108
  function startDaemon() {
3843
- const csrfToken = (0, import_crypto2.randomUUID)();
3844
- const internalToken = (0, import_crypto2.randomUUID)();
4109
+ const csrfToken = (0, import_crypto3.randomUUID)();
4110
+ const internalToken = (0, import_crypto3.randomUUID)();
3845
4111
  const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
3846
4112
  const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
3847
4113
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
4114
+ const watchMode = process.env.NODE9_WATCH_MODE === "1";
3848
4115
  let idleTimer;
3849
4116
  function resetIdleTimer() {
4117
+ if (watchMode) return;
3850
4118
  if (idleTimer) clearTimeout(idleTimer);
3851
4119
  idleTimer = setTimeout(() => {
3852
4120
  if (autoStarted) {
@@ -3901,6 +4169,12 @@ data: ${JSON.stringify({
3901
4169
  data: ${JSON.stringify(readPersistentDecisions())}
3902
4170
 
3903
4171
  `);
4172
+ for (const item of activityRing) {
4173
+ res.write(`event: ${item.event}
4174
+ data: ${JSON.stringify(item.data)}
4175
+
4176
+ `);
4177
+ }
3904
4178
  return req.on("close", () => {
3905
4179
  sseClients.delete(res);
3906
4180
  if (sseClients.size === 0 && pending.size > 0) {
@@ -3920,9 +4194,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
3920
4194
  slackDelegated = false,
3921
4195
  agent,
3922
4196
  mcpServer,
3923
- riskMetadata
4197
+ riskMetadata,
4198
+ fromCLI = false,
4199
+ activityId
3924
4200
  } = JSON.parse(body);
3925
- const id = (0, import_crypto2.randomUUID)();
4201
+ const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto3.randomUUID)();
3926
4202
  const entry = {
3927
4203
  id,
3928
4204
  toolName,
@@ -3953,6 +4229,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
3953
4229
  }, AUTO_DENY_MS)
3954
4230
  };
3955
4231
  pending.set(id, entry);
4232
+ if (!fromCLI) {
4233
+ broadcast("activity", {
4234
+ id,
4235
+ ts: entry.timestamp,
4236
+ tool: toolName,
4237
+ args: redactArgs(args),
4238
+ status: "pending"
4239
+ });
4240
+ }
3956
4241
  const browserEnabled = getConfig().settings.approvers?.browser !== false;
3957
4242
  if (browserEnabled) {
3958
4243
  broadcast("add", {
@@ -3982,6 +4267,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
3982
4267
  const e = pending.get(id);
3983
4268
  if (!e) return;
3984
4269
  if (result.noApprovalMechanism) return;
4270
+ broadcast("activity-result", {
4271
+ id,
4272
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
4273
+ label: result.blockedByLabel
4274
+ });
3985
4275
  clearTimeout(e.timer);
3986
4276
  const decision = result.approved ? "allow" : "deny";
3987
4277
  appendAuditLog({ toolName: e.toolName, args: e.args, decision });
@@ -4016,8 +4306,8 @@ data: ${JSON.stringify(readPersistentDecisions())}
4016
4306
  const entry = pending.get(id);
4017
4307
  if (!entry) return res.writeHead(404).end();
4018
4308
  if (entry.earlyDecision) {
4309
+ clearTimeout(entry.timer);
4019
4310
  pending.delete(id);
4020
- broadcast("remove", { id });
4021
4311
  res.writeHead(200, { "Content-Type": "application/json" });
4022
4312
  const body = { decision: entry.earlyDecision };
4023
4313
  if (entry.earlyReason) body.reason = entry.earlyReason;
@@ -4047,10 +4337,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
4047
4337
  decision: `trust:${trustDuration}`
4048
4338
  });
4049
4339
  clearTimeout(entry.timer);
4050
- if (entry.waiter) entry.waiter("allow");
4051
- else entry.earlyDecision = "allow";
4052
- pending.delete(id);
4053
- broadcast("remove", { id });
4340
+ if (entry.waiter) {
4341
+ entry.waiter("allow");
4342
+ pending.delete(id);
4343
+ broadcast("remove", { id });
4344
+ } else {
4345
+ entry.earlyDecision = "allow";
4346
+ broadcast("remove", { id });
4347
+ entry.timer = setTimeout(() => pending.delete(id), 3e4);
4348
+ }
4054
4349
  res.writeHead(200);
4055
4350
  return res.end(JSON.stringify({ ok: true }));
4056
4351
  }
@@ -4062,13 +4357,16 @@ data: ${JSON.stringify(readPersistentDecisions())}
4062
4357
  decision: resolvedDecision
4063
4358
  });
4064
4359
  clearTimeout(entry.timer);
4065
- if (entry.waiter) entry.waiter(resolvedDecision, reason);
4066
- else {
4360
+ if (entry.waiter) {
4361
+ entry.waiter(resolvedDecision, reason);
4362
+ pending.delete(id);
4363
+ broadcast("remove", { id });
4364
+ } else {
4067
4365
  entry.earlyDecision = resolvedDecision;
4068
4366
  entry.earlyReason = reason;
4367
+ broadcast("remove", { id });
4368
+ entry.timer = setTimeout(() => pending.delete(id), 3e4);
4069
4369
  }
4070
- pending.delete(id);
4071
- broadcast("remove", { id });
4072
4370
  res.writeHead(200);
4073
4371
  return res.end(JSON.stringify({ ok: true }));
4074
4372
  } catch {
@@ -4121,119 +4419,749 @@ data: ${JSON.stringify(readPersistentDecisions())}
4121
4419
  res.writeHead(400).end();
4122
4420
  }
4123
4421
  }
4124
- if (req.method === "DELETE" && pathname.startsWith("/decisions/")) {
4125
- if (!validToken(req)) return res.writeHead(403).end();
4126
- try {
4127
- const toolName = decodeURIComponent(pathname.split("/").pop());
4128
- const decisions = readPersistentDecisions();
4129
- delete decisions[toolName];
4130
- atomicWriteSync2(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
4131
- broadcast("decisions", decisions);
4132
- res.writeHead(200);
4133
- return res.end(JSON.stringify({ ok: true }));
4134
- } catch {
4135
- res.writeHead(400).end();
4422
+ if (req.method === "DELETE" && pathname.startsWith("/decisions/")) {
4423
+ if (!validToken(req)) return res.writeHead(403).end();
4424
+ try {
4425
+ const toolName = decodeURIComponent(pathname.split("/").pop());
4426
+ const decisions = readPersistentDecisions();
4427
+ delete decisions[toolName];
4428
+ atomicWriteSync2(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
4429
+ broadcast("decisions", decisions);
4430
+ res.writeHead(200);
4431
+ return res.end(JSON.stringify({ ok: true }));
4432
+ } catch {
4433
+ res.writeHead(400).end();
4434
+ }
4435
+ }
4436
+ if (req.method === "POST" && pathname.startsWith("/resolve/")) {
4437
+ const internalAuth = req.headers["x-node9-internal"];
4438
+ if (internalAuth !== internalToken) return res.writeHead(403).end();
4439
+ try {
4440
+ const id = pathname.split("/").pop();
4441
+ const entry = pending.get(id);
4442
+ if (!entry) return res.writeHead(404).end();
4443
+ const { decision } = JSON.parse(await readBody(req));
4444
+ appendAuditLog({
4445
+ toolName: entry.toolName,
4446
+ args: entry.args,
4447
+ decision
4448
+ });
4449
+ clearTimeout(entry.timer);
4450
+ if (entry.waiter) entry.waiter(decision);
4451
+ else entry.earlyDecision = decision;
4452
+ pending.delete(id);
4453
+ broadcast("remove", { id });
4454
+ res.writeHead(200);
4455
+ return res.end(JSON.stringify({ ok: true }));
4456
+ } catch {
4457
+ res.writeHead(400).end();
4458
+ }
4459
+ }
4460
+ if (req.method === "POST" && pathname === "/events/clear") {
4461
+ activityRing.length = 0;
4462
+ res.writeHead(200, { "Content-Type": "application/json" });
4463
+ return res.end(JSON.stringify({ ok: true }));
4464
+ }
4465
+ if (req.method === "GET" && pathname === "/audit") {
4466
+ res.writeHead(200, { "Content-Type": "application/json" });
4467
+ return res.end(JSON.stringify(getAuditHistory()));
4468
+ }
4469
+ if (req.method === "GET" && pathname === "/shields") {
4470
+ if (!validToken(req)) return res.writeHead(403).end();
4471
+ const active = readActiveShields();
4472
+ const shields = Object.values(SHIELDS).map((s) => ({
4473
+ name: s.name,
4474
+ description: s.description,
4475
+ active: active.includes(s.name)
4476
+ }));
4477
+ res.writeHead(200, { "Content-Type": "application/json" });
4478
+ return res.end(JSON.stringify({ shields }));
4479
+ }
4480
+ if (req.method === "POST" && pathname === "/shields") {
4481
+ if (!validToken(req)) return res.writeHead(403).end();
4482
+ try {
4483
+ const { name, active } = JSON.parse(await readBody(req));
4484
+ if (!SHIELDS[name]) return res.writeHead(400).end();
4485
+ const current = readActiveShields();
4486
+ const updated = active ? [.../* @__PURE__ */ new Set([...current, name])] : current.filter((n) => n !== name);
4487
+ writeActiveShields(updated);
4488
+ _resetConfigCache();
4489
+ const shieldsPayload = Object.values(SHIELDS).map((s) => ({
4490
+ name: s.name,
4491
+ description: s.description,
4492
+ active: updated.includes(s.name)
4493
+ }));
4494
+ broadcast("shields-status", { shields: shieldsPayload });
4495
+ res.writeHead(200);
4496
+ return res.end(JSON.stringify({ ok: true }));
4497
+ } catch {
4498
+ res.writeHead(400).end();
4499
+ }
4500
+ }
4501
+ res.writeHead(404).end();
4502
+ });
4503
+ daemonServer = server;
4504
+ server.on("error", (e) => {
4505
+ if (e.code === "EADDRINUSE") {
4506
+ try {
4507
+ if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
4508
+ const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4509
+ process.kill(pid, 0);
4510
+ return process.exit(0);
4511
+ }
4512
+ } catch {
4513
+ try {
4514
+ import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4515
+ } catch {
4516
+ }
4517
+ server.listen(DAEMON_PORT2, DAEMON_HOST2);
4518
+ return;
4519
+ }
4520
+ fetch(`http://${DAEMON_HOST2}:${DAEMON_PORT2}/settings`, {
4521
+ signal: AbortSignal.timeout(1e3)
4522
+ }).then((res) => {
4523
+ if (res.ok) {
4524
+ try {
4525
+ const r = (0, import_child_process3.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT2}`], {
4526
+ encoding: "utf8",
4527
+ timeout: 1e3
4528
+ });
4529
+ const match = r.stdout?.match(/pid=(\d+)/);
4530
+ if (match) {
4531
+ const orphanPid = parseInt(match[1], 10);
4532
+ process.kill(orphanPid, 0);
4533
+ atomicWriteSync2(
4534
+ DAEMON_PID_FILE,
4535
+ JSON.stringify({ pid: orphanPid, port: DAEMON_PORT2, internalToken, autoStarted }),
4536
+ { mode: 384 }
4537
+ );
4538
+ }
4539
+ } catch {
4540
+ }
4541
+ process.exit(0);
4542
+ } else {
4543
+ server.listen(DAEMON_PORT2, DAEMON_HOST2);
4544
+ }
4545
+ }).catch(() => {
4546
+ server.listen(DAEMON_PORT2, DAEMON_HOST2);
4547
+ });
4548
+ return;
4549
+ }
4550
+ console.error(import_chalk4.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
4551
+ process.exit(1);
4552
+ });
4553
+ server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
4554
+ atomicWriteSync2(
4555
+ DAEMON_PID_FILE,
4556
+ JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
4557
+ { mode: 384 }
4558
+ );
4559
+ console.log(import_chalk4.default.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
4560
+ });
4561
+ if (watchMode) {
4562
+ console.log(import_chalk4.default.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
4563
+ }
4564
+ try {
4565
+ import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4566
+ } catch {
4567
+ }
4568
+ const ACTIVITY_MAX_BYTES = 1024 * 1024;
4569
+ const unixServer = import_net2.default.createServer((socket) => {
4570
+ const chunks = [];
4571
+ let bytesReceived = 0;
4572
+ socket.on("data", (chunk) => {
4573
+ bytesReceived += chunk.length;
4574
+ if (bytesReceived > ACTIVITY_MAX_BYTES) {
4575
+ socket.destroy();
4576
+ return;
4577
+ }
4578
+ chunks.push(chunk);
4579
+ });
4580
+ socket.on("end", () => {
4581
+ try {
4582
+ const data = JSON.parse(Buffer.concat(chunks).toString());
4583
+ if (data.status === "pending") {
4584
+ broadcast("activity", {
4585
+ id: data.id,
4586
+ ts: data.ts,
4587
+ tool: data.tool,
4588
+ args: redactArgs(data.args),
4589
+ status: "pending"
4590
+ });
4591
+ } else {
4592
+ broadcast("activity-result", {
4593
+ id: data.id,
4594
+ status: data.status,
4595
+ label: data.label
4596
+ });
4597
+ }
4598
+ } catch {
4599
+ }
4600
+ });
4601
+ socket.on("error", () => {
4602
+ });
4603
+ });
4604
+ unixServer.listen(ACTIVITY_SOCKET_PATH2);
4605
+ process.on("exit", () => {
4606
+ try {
4607
+ import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4608
+ } catch {
4609
+ }
4610
+ });
4611
+ }
4612
+ function stopDaemon() {
4613
+ if (!import_fs4.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
4614
+ try {
4615
+ const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4616
+ process.kill(pid, "SIGTERM");
4617
+ console.log(import_chalk4.default.green("\u2705 Stopped."));
4618
+ } catch {
4619
+ console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
4620
+ } finally {
4621
+ try {
4622
+ import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4623
+ } catch {
4624
+ }
4625
+ }
4626
+ }
4627
+ function daemonStatus() {
4628
+ if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
4629
+ try {
4630
+ const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4631
+ process.kill(pid, 0);
4632
+ console.log(import_chalk4.default.green("Node9 daemon: running"));
4633
+ return;
4634
+ } catch {
4635
+ console.log(import_chalk4.default.yellow("Node9 daemon: not running (stale PID)"));
4636
+ return;
4637
+ }
4638
+ }
4639
+ const r = (0, import_child_process3.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT2}`], {
4640
+ encoding: "utf8",
4641
+ timeout: 500
4642
+ });
4643
+ if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT2}`)) {
4644
+ console.log(import_chalk4.default.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
4645
+ } else {
4646
+ console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
4647
+ }
4648
+ }
4649
+ var import_http, import_net2, import_fs4, import_path6, import_os4, import_child_process3, import_crypto3, import_chalk4, ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
4650
+ var init_daemon = __esm({
4651
+ "src/daemon/index.ts"() {
4652
+ "use strict";
4653
+ init_ui2();
4654
+ import_http = __toESM(require("http"));
4655
+ import_net2 = __toESM(require("net"));
4656
+ import_fs4 = __toESM(require("fs"));
4657
+ import_path6 = __toESM(require("path"));
4658
+ import_os4 = __toESM(require("os"));
4659
+ import_child_process3 = require("child_process");
4660
+ import_crypto3 = require("crypto");
4661
+ import_chalk4 = __toESM(require("chalk"));
4662
+ init_core();
4663
+ init_shields();
4664
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path6.default.join(import_os4.default.tmpdir(), "node9-activity.sock");
4665
+ DAEMON_PORT2 = 7391;
4666
+ DAEMON_HOST2 = "127.0.0.1";
4667
+ homeDir = import_os4.default.homedir();
4668
+ DAEMON_PID_FILE = import_path6.default.join(homeDir, ".node9", "daemon.pid");
4669
+ DECISIONS_FILE = import_path6.default.join(homeDir, ".node9", "decisions.json");
4670
+ GLOBAL_CONFIG_FILE = import_path6.default.join(homeDir, ".node9", "config.json");
4671
+ CREDENTIALS_FILE = import_path6.default.join(homeDir, ".node9", "credentials.json");
4672
+ AUDIT_LOG_FILE = import_path6.default.join(homeDir, ".node9", "audit.log");
4673
+ TRUST_FILE2 = import_path6.default.join(homeDir, ".node9", "trust.json");
4674
+ TRUST_DURATIONS = {
4675
+ "30m": 30 * 6e4,
4676
+ "1h": 60 * 6e4,
4677
+ "2h": 2 * 60 * 6e4
4678
+ };
4679
+ SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
4680
+ AUTO_DENY_MS = 12e4;
4681
+ autoStarted = process.env.NODE9_AUTO_STARTED === "1";
4682
+ pending = /* @__PURE__ */ new Map();
4683
+ sseClients = /* @__PURE__ */ new Set();
4684
+ abandonTimer = null;
4685
+ daemonServer = null;
4686
+ hadBrowserClient = false;
4687
+ ACTIVITY_RING_SIZE = 100;
4688
+ activityRing = [];
4689
+ }
4690
+ });
4691
+
4692
+ // src/tui/tail.ts
4693
+ var tail_exports = {};
4694
+ __export(tail_exports, {
4695
+ startTail: () => startTail
4696
+ });
4697
+ function getIcon(tool) {
4698
+ const t = tool.toLowerCase();
4699
+ for (const [k, v] of Object.entries(ICONS)) {
4700
+ if (t.includes(k)) return v;
4701
+ }
4702
+ return "\u{1F6E0}\uFE0F";
4703
+ }
4704
+ function formatBase(activity) {
4705
+ const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
4706
+ const icon = getIcon(activity.tool);
4707
+ const toolName = activity.tool.slice(0, 16).padEnd(16);
4708
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
4709
+ const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
4710
+ return `${import_chalk5.default.gray(time)} ${icon} ${import_chalk5.default.white.bold(toolName)} ${import_chalk5.default.dim(argsPreview)}`;
4711
+ }
4712
+ function renderResult(activity, result) {
4713
+ const base = formatBase(activity);
4714
+ let status;
4715
+ if (result.status === "allow") {
4716
+ status = import_chalk5.default.green("\u2713 ALLOW");
4717
+ } else if (result.status === "dlp") {
4718
+ status = import_chalk5.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
4719
+ } else {
4720
+ status = import_chalk5.default.red("\u2717 BLOCK");
4721
+ }
4722
+ if (process.stdout.isTTY) {
4723
+ import_readline.default.clearLine(process.stdout, 0);
4724
+ import_readline.default.cursorTo(process.stdout, 0);
4725
+ }
4726
+ console.log(`${base} ${status}`);
4727
+ }
4728
+ function renderPending(activity) {
4729
+ if (!process.stdout.isTTY) return;
4730
+ process.stdout.write(`${formatBase(activity)} ${import_chalk5.default.yellow("\u25CF \u2026")}\r`);
4731
+ }
4732
+ async function ensureDaemon() {
4733
+ if (import_fs6.default.existsSync(PID_FILE)) {
4734
+ try {
4735
+ const { port } = JSON.parse(import_fs6.default.readFileSync(PID_FILE, "utf-8"));
4736
+ return port;
4737
+ } catch {
4738
+ }
4739
+ }
4740
+ try {
4741
+ const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
4742
+ signal: AbortSignal.timeout(500)
4743
+ });
4744
+ if (res.ok) return DAEMON_PORT2;
4745
+ } catch {
4746
+ }
4747
+ console.log(import_chalk5.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
4748
+ const child = (0, import_child_process5.spawn)(process.execPath, [process.argv[1], "daemon"], {
4749
+ detached: true,
4750
+ stdio: "ignore",
4751
+ env: { ...process.env, NODE9_AUTO_STARTED: "1" }
4752
+ });
4753
+ child.unref();
4754
+ for (let i = 0; i < 20; i++) {
4755
+ await new Promise((r) => setTimeout(r, 250));
4756
+ try {
4757
+ const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
4758
+ signal: AbortSignal.timeout(500)
4759
+ });
4760
+ if (res.ok) return DAEMON_PORT2;
4761
+ } catch {
4762
+ }
4763
+ }
4764
+ console.error(import_chalk5.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
4765
+ process.exit(1);
4766
+ }
4767
+ async function startTail(options = {}) {
4768
+ const port = await ensureDaemon();
4769
+ if (options.clear) {
4770
+ await new Promise((resolve) => {
4771
+ const req2 = import_http2.default.request(
4772
+ { method: "POST", hostname: "127.0.0.1", port, path: "/events/clear" },
4773
+ (res) => {
4774
+ res.resume();
4775
+ res.on("end", resolve);
4776
+ }
4777
+ );
4778
+ req2.on("error", resolve);
4779
+ req2.end();
4780
+ });
4781
+ }
4782
+ const connectionTime = Date.now();
4783
+ const pending2 = /* @__PURE__ */ new Map();
4784
+ console.log(import_chalk5.default.cyan.bold(`
4785
+ \u{1F6F0}\uFE0F Node9 tail `) + import_chalk5.default.dim(`\u2192 localhost:${port}`));
4786
+ if (options.clear) {
4787
+ console.log(import_chalk5.default.dim("History cleared. Showing live events. Press Ctrl+C to exit.\n"));
4788
+ } else if (options.history) {
4789
+ console.log(import_chalk5.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
4790
+ } else {
4791
+ console.log(
4792
+ import_chalk5.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
4793
+ );
4794
+ }
4795
+ process.on("SIGINT", () => {
4796
+ if (process.stdout.isTTY) {
4797
+ import_readline.default.clearLine(process.stdout, 0);
4798
+ import_readline.default.cursorTo(process.stdout, 0);
4799
+ }
4800
+ console.log(import_chalk5.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
4801
+ process.exit(0);
4802
+ });
4803
+ const req = import_http2.default.get(`http://127.0.0.1:${port}/events`, (res) => {
4804
+ if (res.statusCode !== 200) {
4805
+ console.error(import_chalk5.default.red(`Failed to connect: HTTP ${res.statusCode}`));
4806
+ process.exit(1);
4807
+ }
4808
+ let currentEvent = "";
4809
+ let currentData = "";
4810
+ res.on("error", () => {
4811
+ });
4812
+ const rl = import_readline.default.createInterface({ input: res, crlfDelay: Infinity });
4813
+ rl.on("error", () => {
4814
+ });
4815
+ rl.on("line", (line) => {
4816
+ if (line.startsWith("event:")) {
4817
+ currentEvent = line.slice(6).trim();
4818
+ } else if (line.startsWith("data:")) {
4819
+ currentData = line.slice(5).trim();
4820
+ } else if (line === "") {
4821
+ if (currentEvent && currentData) {
4822
+ handleMessage(currentEvent, currentData);
4823
+ }
4824
+ currentEvent = "";
4825
+ currentData = "";
4826
+ }
4827
+ });
4828
+ rl.on("close", () => {
4829
+ if (process.stdout.isTTY) {
4830
+ import_readline.default.clearLine(process.stdout, 0);
4831
+ import_readline.default.cursorTo(process.stdout, 0);
4832
+ }
4833
+ console.log(import_chalk5.default.red("\n\u274C Daemon disconnected."));
4834
+ process.exit(1);
4835
+ });
4836
+ });
4837
+ function handleMessage(event, rawData) {
4838
+ let data;
4839
+ try {
4840
+ data = JSON.parse(rawData);
4841
+ } catch {
4842
+ return;
4843
+ }
4844
+ if (event === "activity") {
4845
+ if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
4846
+ if (data.status && data.status !== "pending") {
4847
+ renderResult(data, data);
4848
+ return;
4849
+ }
4850
+ pending2.set(data.id, data);
4851
+ const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
4852
+ if (slowTool) renderPending(data);
4853
+ }
4854
+ if (event === "activity-result") {
4855
+ const original = pending2.get(data.id);
4856
+ if (original) {
4857
+ renderResult(original, data);
4858
+ pending2.delete(data.id);
4859
+ }
4860
+ }
4861
+ }
4862
+ req.on("error", (err) => {
4863
+ const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
4864
+ console.error(import_chalk5.default.red(`
4865
+ \u274C ${msg}`));
4866
+ process.exit(1);
4867
+ });
4868
+ }
4869
+ var import_http2, import_chalk5, import_fs6, import_os6, import_path8, import_readline, import_child_process5, PID_FILE, ICONS;
4870
+ var init_tail = __esm({
4871
+ "src/tui/tail.ts"() {
4872
+ "use strict";
4873
+ import_http2 = __toESM(require("http"));
4874
+ import_chalk5 = __toESM(require("chalk"));
4875
+ import_fs6 = __toESM(require("fs"));
4876
+ import_os6 = __toESM(require("os"));
4877
+ import_path8 = __toESM(require("path"));
4878
+ import_readline = __toESM(require("readline"));
4879
+ import_child_process5 = require("child_process");
4880
+ init_daemon();
4881
+ PID_FILE = import_path8.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
4882
+ ICONS = {
4883
+ bash: "\u{1F4BB}",
4884
+ shell: "\u{1F4BB}",
4885
+ terminal: "\u{1F4BB}",
4886
+ read: "\u{1F4D6}",
4887
+ edit: "\u270F\uFE0F",
4888
+ write: "\u270F\uFE0F",
4889
+ glob: "\u{1F4C2}",
4890
+ grep: "\u{1F50D}",
4891
+ agent: "\u{1F916}",
4892
+ search: "\u{1F50D}",
4893
+ sql: "\u{1F5C4}\uFE0F",
4894
+ query: "\u{1F5C4}\uFE0F",
4895
+ list: "\u{1F4C2}",
4896
+ delete: "\u{1F5D1}\uFE0F",
4897
+ web: "\u{1F310}"
4898
+ };
4899
+ }
4900
+ });
4901
+
4902
+ // src/cli.ts
4903
+ var import_commander = require("commander");
4904
+ init_core();
4905
+
4906
+ // src/setup.ts
4907
+ var import_fs3 = __toESM(require("fs"));
4908
+ var import_path5 = __toESM(require("path"));
4909
+ var import_os3 = __toESM(require("os"));
4910
+ var import_chalk3 = __toESM(require("chalk"));
4911
+ var import_prompts2 = require("@inquirer/prompts");
4912
+ function printDaemonTip() {
4913
+ console.log(
4914
+ import_chalk3.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + import_chalk3.default.white("\n To view your history or manage persistent rules, run:") + import_chalk3.default.green("\n node9 daemon --openui")
4915
+ );
4916
+ }
4917
+ function fullPathCommand(subcommand) {
4918
+ if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
4919
+ const nodeExec = process.execPath;
4920
+ const cliScript = process.argv[1];
4921
+ return `${nodeExec} ${cliScript} ${subcommand}`;
4922
+ }
4923
+ function readJson(filePath) {
4924
+ try {
4925
+ if (import_fs3.default.existsSync(filePath)) {
4926
+ return JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
4927
+ }
4928
+ } catch {
4929
+ }
4930
+ return null;
4931
+ }
4932
+ function writeJson(filePath, data) {
4933
+ const dir = import_path5.default.dirname(filePath);
4934
+ if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
4935
+ import_fs3.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
4936
+ }
4937
+ async function setupClaude() {
4938
+ const homeDir2 = import_os3.default.homedir();
4939
+ const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
4940
+ const hooksPath = import_path5.default.join(homeDir2, ".claude", "settings.json");
4941
+ const claudeConfig = readJson(mcpPath) ?? {};
4942
+ const settings = readJson(hooksPath) ?? {};
4943
+ const servers = claudeConfig.mcpServers ?? {};
4944
+ let anythingChanged = false;
4945
+ if (!settings.hooks) settings.hooks = {};
4946
+ const hasPreHook = settings.hooks.PreToolUse?.some(
4947
+ (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
4948
+ );
4949
+ if (!hasPreHook) {
4950
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
4951
+ settings.hooks.PreToolUse.push({
4952
+ matcher: ".*",
4953
+ hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
4954
+ });
4955
+ console.log(import_chalk3.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
4956
+ anythingChanged = true;
4957
+ }
4958
+ const hasPostHook = settings.hooks.PostToolUse?.some(
4959
+ (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
4960
+ );
4961
+ if (!hasPostHook) {
4962
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
4963
+ settings.hooks.PostToolUse.push({
4964
+ matcher: ".*",
4965
+ hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
4966
+ });
4967
+ console.log(import_chalk3.default.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
4968
+ anythingChanged = true;
4969
+ }
4970
+ if (anythingChanged) {
4971
+ writeJson(hooksPath, settings);
4972
+ console.log("");
4973
+ }
4974
+ const serversToWrap = [];
4975
+ for (const [name, server] of Object.entries(servers)) {
4976
+ if (!server.command || server.command === "node9") continue;
4977
+ const parts = [server.command, ...server.args ?? []];
4978
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
4979
+ }
4980
+ if (serversToWrap.length > 0) {
4981
+ console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
4982
+ console.log(import_chalk3.default.white(` ${mcpPath}`));
4983
+ for (const { name, originalCmd } of serversToWrap) {
4984
+ console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
4985
+ }
4986
+ console.log("");
4987
+ const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
4988
+ if (proceed) {
4989
+ for (const { name, parts } of serversToWrap) {
4990
+ servers[name] = { ...servers[name], command: "node9", args: parts };
4136
4991
  }
4992
+ claudeConfig.mcpServers = servers;
4993
+ writeJson(mcpPath, claudeConfig);
4994
+ console.log(import_chalk3.default.green(`
4995
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
4996
+ anythingChanged = true;
4997
+ } else {
4998
+ console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
4137
4999
  }
4138
- if (req.method === "POST" && pathname.startsWith("/resolve/")) {
4139
- const internalAuth = req.headers["x-node9-internal"];
4140
- if (internalAuth !== internalToken) return res.writeHead(403).end();
4141
- try {
4142
- const id = pathname.split("/").pop();
4143
- const entry = pending.get(id);
4144
- if (!entry) return res.writeHead(404).end();
4145
- const { decision } = JSON.parse(await readBody(req));
4146
- appendAuditLog({
4147
- toolName: entry.toolName,
4148
- args: entry.args,
4149
- decision
4150
- });
4151
- clearTimeout(entry.timer);
4152
- if (entry.waiter) entry.waiter(decision);
4153
- else entry.earlyDecision = decision;
4154
- pending.delete(id);
4155
- broadcast("remove", { id });
4156
- res.writeHead(200);
4157
- return res.end(JSON.stringify({ ok: true }));
4158
- } catch {
4159
- res.writeHead(400).end();
5000
+ console.log("");
5001
+ }
5002
+ if (!anythingChanged && serversToWrap.length === 0) {
5003
+ console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
5004
+ printDaemonTip();
5005
+ return;
5006
+ }
5007
+ if (anythingChanged) {
5008
+ console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
5009
+ console.log(import_chalk3.default.gray(" Restart Claude Code for changes to take effect."));
5010
+ printDaemonTip();
5011
+ }
5012
+ }
5013
+ async function setupGemini() {
5014
+ const homeDir2 = import_os3.default.homedir();
5015
+ const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
5016
+ const settings = readJson(settingsPath) ?? {};
5017
+ const servers = settings.mcpServers ?? {};
5018
+ let anythingChanged = false;
5019
+ if (!settings.hooks) settings.hooks = {};
5020
+ const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
5021
+ (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
5022
+ );
5023
+ if (!hasBeforeHook) {
5024
+ if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
5025
+ if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
5026
+ settings.hooks.BeforeTool.push({
5027
+ matcher: ".*",
5028
+ hooks: [
5029
+ {
5030
+ name: "node9-check",
5031
+ type: "command",
5032
+ command: fullPathCommand("check"),
5033
+ timeout: 6e5
5034
+ }
5035
+ ]
5036
+ });
5037
+ console.log(import_chalk3.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
5038
+ anythingChanged = true;
5039
+ }
5040
+ const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
5041
+ (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
5042
+ );
5043
+ if (!hasAfterHook) {
5044
+ if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
5045
+ if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
5046
+ settings.hooks.AfterTool.push({
5047
+ matcher: ".*",
5048
+ hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
5049
+ });
5050
+ console.log(import_chalk3.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
5051
+ anythingChanged = true;
5052
+ }
5053
+ if (anythingChanged) {
5054
+ writeJson(settingsPath, settings);
5055
+ console.log("");
5056
+ }
5057
+ const serversToWrap = [];
5058
+ for (const [name, server] of Object.entries(servers)) {
5059
+ if (!server.command || server.command === "node9") continue;
5060
+ const parts = [server.command, ...server.args ?? []];
5061
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
5062
+ }
5063
+ if (serversToWrap.length > 0) {
5064
+ console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
5065
+ console.log(import_chalk3.default.white(` ${settingsPath} (mcpServers)`));
5066
+ for (const { name, originalCmd } of serversToWrap) {
5067
+ console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5068
+ }
5069
+ console.log("");
5070
+ const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
5071
+ if (proceed) {
5072
+ for (const { name, parts } of serversToWrap) {
5073
+ servers[name] = { ...servers[name], command: "node9", args: parts };
4160
5074
  }
5075
+ settings.mcpServers = servers;
5076
+ writeJson(settingsPath, settings);
5077
+ console.log(import_chalk3.default.green(`
5078
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
5079
+ anythingChanged = true;
5080
+ } else {
5081
+ console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
4161
5082
  }
4162
- if (req.method === "GET" && pathname === "/audit") {
4163
- res.writeHead(200, { "Content-Type": "application/json" });
4164
- return res.end(JSON.stringify(getAuditHistory()));
5083
+ console.log("");
5084
+ }
5085
+ if (!anythingChanged && serversToWrap.length === 0) {
5086
+ console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
5087
+ printDaemonTip();
5088
+ return;
5089
+ }
5090
+ if (anythingChanged) {
5091
+ console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
5092
+ console.log(import_chalk3.default.gray(" Restart Gemini CLI for changes to take effect."));
5093
+ printDaemonTip();
5094
+ }
5095
+ }
5096
+ async function setupCursor() {
5097
+ const homeDir2 = import_os3.default.homedir();
5098
+ const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
5099
+ const mcpConfig = readJson(mcpPath) ?? {};
5100
+ const servers = mcpConfig.mcpServers ?? {};
5101
+ let anythingChanged = false;
5102
+ const serversToWrap = [];
5103
+ for (const [name, server] of Object.entries(servers)) {
5104
+ if (!server.command || server.command === "node9") continue;
5105
+ const parts = [server.command, ...server.args ?? []];
5106
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
5107
+ }
5108
+ if (serversToWrap.length > 0) {
5109
+ console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
5110
+ console.log(import_chalk3.default.white(` ${mcpPath}`));
5111
+ for (const { name, originalCmd } of serversToWrap) {
5112
+ console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
4165
5113
  }
4166
- res.writeHead(404).end();
4167
- });
4168
- daemonServer = server;
4169
- server.on("error", (e) => {
4170
- if (e.code === "EADDRINUSE") {
4171
- try {
4172
- if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
4173
- const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4174
- process.kill(pid, 0);
4175
- return process.exit(0);
4176
- }
4177
- } catch {
4178
- try {
4179
- import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4180
- } catch {
4181
- }
4182
- server.listen(DAEMON_PORT2, DAEMON_HOST2);
4183
- return;
5114
+ console.log("");
5115
+ const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
5116
+ if (proceed) {
5117
+ for (const { name, parts } of serversToWrap) {
5118
+ servers[name] = { ...servers[name], command: "node9", args: parts };
4184
5119
  }
5120
+ mcpConfig.mcpServers = servers;
5121
+ writeJson(mcpPath, mcpConfig);
5122
+ console.log(import_chalk3.default.green(`
5123
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
5124
+ anythingChanged = true;
5125
+ } else {
5126
+ console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
4185
5127
  }
4186
- console.error(import_chalk4.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
4187
- process.exit(1);
4188
- });
4189
- server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
4190
- atomicWriteSync2(
4191
- DAEMON_PID_FILE,
4192
- JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
4193
- { mode: 384 }
5128
+ console.log("");
5129
+ }
5130
+ console.log(
5131
+ import_chalk3.default.yellow(
5132
+ " \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
5133
+ )
5134
+ );
5135
+ console.log("");
5136
+ if (!anythingChanged && serversToWrap.length === 0) {
5137
+ console.log(
5138
+ import_chalk3.default.blue(
5139
+ "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
5140
+ )
4194
5141
  );
4195
- console.log(import_chalk4.default.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
4196
- });
4197
- }
4198
- function stopDaemon() {
4199
- if (!import_fs4.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
4200
- try {
4201
- const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4202
- process.kill(pid, "SIGTERM");
4203
- console.log(import_chalk4.default.green("\u2705 Stopped."));
4204
- } catch {
4205
- console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
4206
- } finally {
4207
- try {
4208
- import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4209
- } catch {
4210
- }
5142
+ printDaemonTip();
5143
+ return;
4211
5144
  }
4212
- }
4213
- function daemonStatus() {
4214
- if (!import_fs4.default.existsSync(DAEMON_PID_FILE))
4215
- return console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
4216
- try {
4217
- const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4218
- process.kill(pid, 0);
4219
- console.log(import_chalk4.default.green("Node9 daemon: running"));
4220
- } catch {
4221
- console.log(import_chalk4.default.yellow("Node9 daemon: not running (stale PID)"));
5145
+ if (anythingChanged) {
5146
+ console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
5147
+ console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
5148
+ printDaemonTip();
4222
5149
  }
4223
5150
  }
4224
5151
 
4225
5152
  // src/cli.ts
4226
- var import_child_process4 = require("child_process");
5153
+ init_daemon();
5154
+ var import_child_process6 = require("child_process");
4227
5155
  var import_execa = require("execa");
4228
5156
  var import_execa2 = require("execa");
4229
- var import_chalk5 = __toESM(require("chalk"));
4230
- var import_readline = __toESM(require("readline"));
4231
- var import_fs6 = __toESM(require("fs"));
4232
- var import_path8 = __toESM(require("path"));
4233
- var import_os6 = __toESM(require("os"));
5157
+ var import_chalk6 = __toESM(require("chalk"));
5158
+ var import_readline2 = __toESM(require("readline"));
5159
+ var import_fs7 = __toESM(require("fs"));
5160
+ var import_path9 = __toESM(require("path"));
5161
+ var import_os7 = __toESM(require("os"));
4234
5162
 
4235
5163
  // src/undo.ts
4236
- var import_child_process3 = require("child_process");
5164
+ var import_child_process4 = require("child_process");
4237
5165
  var import_fs5 = __toESM(require("fs"));
4238
5166
  var import_path7 = __toESM(require("path"));
4239
5167
  var import_os5 = __toESM(require("os"));
@@ -4270,12 +5198,12 @@ async function createShadowSnapshot(tool = "unknown", args = {}) {
4270
5198
  if (!import_fs5.default.existsSync(import_path7.default.join(cwd, ".git"))) return null;
4271
5199
  const tempIndex = import_path7.default.join(cwd, ".git", `node9_index_${Date.now()}`);
4272
5200
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
4273
- (0, import_child_process3.spawnSync)("git", ["add", "-A"], { env });
4274
- const treeRes = (0, import_child_process3.spawnSync)("git", ["write-tree"], { env });
5201
+ (0, import_child_process4.spawnSync)("git", ["add", "-A"], { env });
5202
+ const treeRes = (0, import_child_process4.spawnSync)("git", ["write-tree"], { env });
4275
5203
  const treeHash = treeRes.stdout.toString().trim();
4276
5204
  if (import_fs5.default.existsSync(tempIndex)) import_fs5.default.unlinkSync(tempIndex);
4277
5205
  if (!treeHash || treeRes.status !== 0) return null;
4278
- const commitRes = (0, import_child_process3.spawnSync)("git", [
5206
+ const commitRes = (0, import_child_process4.spawnSync)("git", [
4279
5207
  "commit-tree",
4280
5208
  treeHash,
4281
5209
  "-m",
@@ -4306,10 +5234,10 @@ function getSnapshotHistory() {
4306
5234
  }
4307
5235
  function computeUndoDiff(hash, cwd) {
4308
5236
  try {
4309
- const result = (0, import_child_process3.spawnSync)("git", ["diff", hash, "--stat", "--", "."], { cwd });
5237
+ const result = (0, import_child_process4.spawnSync)("git", ["diff", hash, "--stat", "--", "."], { cwd });
4310
5238
  const stat = result.stdout.toString().trim();
4311
5239
  if (!stat) return null;
4312
- const diff = (0, import_child_process3.spawnSync)("git", ["diff", hash, "--", "."], { cwd });
5240
+ const diff = (0, import_child_process4.spawnSync)("git", ["diff", hash, "--", "."], { cwd });
4313
5241
  const raw = diff.stdout.toString();
4314
5242
  if (!raw) return null;
4315
5243
  const lines = raw.split("\n").filter(
@@ -4323,14 +5251,14 @@ function computeUndoDiff(hash, cwd) {
4323
5251
  function applyUndo(hash, cwd) {
4324
5252
  try {
4325
5253
  const dir = cwd ?? process.cwd();
4326
- const restore = (0, import_child_process3.spawnSync)("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
5254
+ const restore = (0, import_child_process4.spawnSync)("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
4327
5255
  cwd: dir
4328
5256
  });
4329
5257
  if (restore.status !== 0) return false;
4330
- const lsTree = (0, import_child_process3.spawnSync)("git", ["ls-tree", "-r", "--name-only", hash], { cwd: dir });
5258
+ const lsTree = (0, import_child_process4.spawnSync)("git", ["ls-tree", "-r", "--name-only", hash], { cwd: dir });
4331
5259
  const snapshotFiles = new Set(lsTree.stdout.toString().trim().split("\n").filter(Boolean));
4332
- const tracked = (0, import_child_process3.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
4333
- const untracked = (0, import_child_process3.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5260
+ const tracked = (0, import_child_process4.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5261
+ const untracked = (0, import_child_process4.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
4334
5262
  for (const file of [...tracked, ...untracked]) {
4335
5263
  const fullPath = import_path7.default.join(dir, file);
4336
5264
  if (!snapshotFiles.has(file) && import_fs5.default.existsSync(fullPath)) {
@@ -4344,9 +5272,10 @@ function applyUndo(hash, cwd) {
4344
5272
  }
4345
5273
 
4346
5274
  // src/cli.ts
5275
+ init_shields();
4347
5276
  var import_prompts3 = require("@inquirer/prompts");
4348
5277
  var { version } = JSON.parse(
4349
- import_fs6.default.readFileSync(import_path8.default.join(__dirname, "../package.json"), "utf-8")
5278
+ import_fs7.default.readFileSync(import_path9.default.join(__dirname, "../package.json"), "utf-8")
4350
5279
  );
4351
5280
  function parseDuration(str) {
4352
5281
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -4434,15 +5363,15 @@ function openBrowserLocal() {
4434
5363
  const url = `http://${DAEMON_HOST2}:${DAEMON_PORT2}/`;
4435
5364
  try {
4436
5365
  const opts = { stdio: "ignore" };
4437
- if (process.platform === "darwin") (0, import_child_process4.execSync)(`open "${url}"`, opts);
4438
- else if (process.platform === "win32") (0, import_child_process4.execSync)(`cmd /c start "" "${url}"`, opts);
4439
- else (0, import_child_process4.execSync)(`xdg-open "${url}"`, opts);
5366
+ if (process.platform === "darwin") (0, import_child_process6.execSync)(`open "${url}"`, opts);
5367
+ else if (process.platform === "win32") (0, import_child_process6.execSync)(`cmd /c start "" "${url}"`, opts);
5368
+ else (0, import_child_process6.execSync)(`xdg-open "${url}"`, opts);
4440
5369
  } catch {
4441
5370
  }
4442
5371
  }
4443
5372
  async function autoStartDaemonAndWait() {
4444
5373
  try {
4445
- const child = (0, import_child_process4.spawn)("node9", ["daemon"], {
5374
+ const child = (0, import_child_process6.spawn)("node9", ["daemon"], {
4446
5375
  detached: true,
4447
5376
  stdio: "ignore",
4448
5377
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
@@ -4478,14 +5407,14 @@ async function runProxy(targetCommand) {
4478
5407
  if (stdout) executable = stdout.trim();
4479
5408
  } catch {
4480
5409
  }
4481
- console.log(import_chalk5.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
4482
- const child = (0, import_child_process4.spawn)(executable, args, {
5410
+ console.log(import_chalk6.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
5411
+ const child = (0, import_child_process6.spawn)(executable, args, {
4483
5412
  stdio: ["pipe", "pipe", "inherit"],
4484
5413
  // We control STDIN and STDOUT
4485
5414
  shell: false,
4486
5415
  env: { ...process.env, FORCE_COLOR: "1" }
4487
5416
  });
4488
- const agentIn = import_readline.default.createInterface({ input: process.stdin, terminal: false });
5417
+ const agentIn = import_readline2.default.createInterface({ input: process.stdin, terminal: false });
4489
5418
  agentIn.on("line", async (line) => {
4490
5419
  let message;
4491
5420
  try {
@@ -4503,10 +5432,10 @@ async function runProxy(targetCommand) {
4503
5432
  agent: "Proxy/MCP"
4504
5433
  });
4505
5434
  if (!result.approved) {
4506
- console.error(import_chalk5.default.red(`
5435
+ console.error(import_chalk6.default.red(`
4507
5436
  \u{1F6D1} Node9 Sudo: Action Blocked`));
4508
- console.error(import_chalk5.default.gray(` Tool: ${name}`));
4509
- console.error(import_chalk5.default.gray(` Reason: ${result.reason || "Security Policy"}
5437
+ console.error(import_chalk6.default.gray(` Tool: ${name}`));
5438
+ console.error(import_chalk6.default.gray(` Reason: ${result.reason || "Security Policy"}
4510
5439
  `));
4511
5440
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
4512
5441
  const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -4548,14 +5477,14 @@ async function runProxy(targetCommand) {
4548
5477
  }
4549
5478
  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) => {
4550
5479
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
4551
- const credPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "credentials.json");
4552
- if (!import_fs6.default.existsSync(import_path8.default.dirname(credPath)))
4553
- import_fs6.default.mkdirSync(import_path8.default.dirname(credPath), { recursive: true });
5480
+ const credPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "credentials.json");
5481
+ if (!import_fs7.default.existsSync(import_path9.default.dirname(credPath)))
5482
+ import_fs7.default.mkdirSync(import_path9.default.dirname(credPath), { recursive: true });
4554
5483
  const profileName = options.profile || "default";
4555
5484
  let existingCreds = {};
4556
5485
  try {
4557
- if (import_fs6.default.existsSync(credPath)) {
4558
- const raw = JSON.parse(import_fs6.default.readFileSync(credPath, "utf-8"));
5486
+ if (import_fs7.default.existsSync(credPath)) {
5487
+ const raw = JSON.parse(import_fs7.default.readFileSync(credPath, "utf-8"));
4559
5488
  if (raw.apiKey) {
4560
5489
  existingCreds = {
4561
5490
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -4567,13 +5496,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4567
5496
  } catch {
4568
5497
  }
4569
5498
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
4570
- import_fs6.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
5499
+ import_fs7.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
4571
5500
  if (profileName === "default") {
4572
- const configPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "config.json");
5501
+ const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
4573
5502
  let config = {};
4574
5503
  try {
4575
- if (import_fs6.default.existsSync(configPath))
4576
- config = JSON.parse(import_fs6.default.readFileSync(configPath, "utf-8"));
5504
+ if (import_fs7.default.existsSync(configPath))
5505
+ config = JSON.parse(import_fs7.default.readFileSync(configPath, "utf-8"));
4577
5506
  } catch {
4578
5507
  }
4579
5508
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -4588,36 +5517,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4588
5517
  approvers.cloud = false;
4589
5518
  }
4590
5519
  s.approvers = approvers;
4591
- if (!import_fs6.default.existsSync(import_path8.default.dirname(configPath)))
4592
- import_fs6.default.mkdirSync(import_path8.default.dirname(configPath), { recursive: true });
4593
- import_fs6.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
5520
+ if (!import_fs7.default.existsSync(import_path9.default.dirname(configPath)))
5521
+ import_fs7.default.mkdirSync(import_path9.default.dirname(configPath), { recursive: true });
5522
+ import_fs7.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
4594
5523
  }
4595
5524
  if (options.profile && profileName !== "default") {
4596
- console.log(import_chalk5.default.green(`\u2705 Profile "${profileName}" saved`));
4597
- console.log(import_chalk5.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
5525
+ console.log(import_chalk6.default.green(`\u2705 Profile "${profileName}" saved`));
5526
+ console.log(import_chalk6.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
4598
5527
  } else if (options.local) {
4599
- console.log(import_chalk5.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
4600
- console.log(import_chalk5.default.gray(` All decisions stay on this machine.`));
5528
+ console.log(import_chalk6.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
5529
+ console.log(import_chalk6.default.gray(` All decisions stay on this machine.`));
4601
5530
  } else {
4602
- console.log(import_chalk5.default.green(`\u2705 Logged in \u2014 agent mode`));
4603
- console.log(import_chalk5.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
5531
+ console.log(import_chalk6.default.green(`\u2705 Logged in \u2014 agent mode`));
5532
+ console.log(import_chalk6.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
4604
5533
  }
4605
5534
  });
4606
5535
  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) => {
4607
5536
  if (target === "gemini") return await setupGemini();
4608
5537
  if (target === "claude") return await setupClaude();
4609
5538
  if (target === "cursor") return await setupCursor();
4610
- console.error(import_chalk5.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
5539
+ console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
4611
5540
  process.exit(1);
4612
5541
  });
4613
5542
  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) => {
4614
5543
  if (!target) {
4615
- console.log(import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
4616
- console.log(" Usage: " + import_chalk5.default.white("node9 setup <target>") + "\n");
5544
+ console.log(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
5545
+ console.log(" Usage: " + import_chalk6.default.white("node9 setup <target>") + "\n");
4617
5546
  console.log(" Targets:");
4618
- console.log(" " + import_chalk5.default.green("claude") + " \u2014 Claude Code (hook mode)");
4619
- console.log(" " + import_chalk5.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
4620
- console.log(" " + import_chalk5.default.green("cursor") + " \u2014 Cursor (hook mode)");
5547
+ console.log(" " + import_chalk6.default.green("claude") + " \u2014 Claude Code (hook mode)");
5548
+ console.log(" " + import_chalk6.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
5549
+ console.log(" " + import_chalk6.default.green("cursor") + " \u2014 Cursor (hook mode)");
4621
5550
  console.log("");
4622
5551
  return;
4623
5552
  }
@@ -4625,33 +5554,33 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
4625
5554
  if (t === "gemini") return await setupGemini();
4626
5555
  if (t === "claude") return await setupClaude();
4627
5556
  if (t === "cursor") return await setupCursor();
4628
- console.error(import_chalk5.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
5557
+ console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
4629
5558
  process.exit(1);
4630
5559
  });
4631
5560
  program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
4632
- const homeDir2 = import_os6.default.homedir();
5561
+ const homeDir2 = import_os7.default.homedir();
4633
5562
  let failures = 0;
4634
5563
  function pass(msg) {
4635
- console.log(import_chalk5.default.green(" \u2705 ") + msg);
5564
+ console.log(import_chalk6.default.green(" \u2705 ") + msg);
4636
5565
  }
4637
5566
  function fail(msg, hint) {
4638
- console.log(import_chalk5.default.red(" \u274C ") + msg);
4639
- if (hint) console.log(import_chalk5.default.gray(" " + hint));
5567
+ console.log(import_chalk6.default.red(" \u274C ") + msg);
5568
+ if (hint) console.log(import_chalk6.default.gray(" " + hint));
4640
5569
  failures++;
4641
5570
  }
4642
5571
  function warn(msg, hint) {
4643
- console.log(import_chalk5.default.yellow(" \u26A0\uFE0F ") + msg);
4644
- if (hint) console.log(import_chalk5.default.gray(" " + hint));
5572
+ console.log(import_chalk6.default.yellow(" \u26A0\uFE0F ") + msg);
5573
+ if (hint) console.log(import_chalk6.default.gray(" " + hint));
4645
5574
  }
4646
5575
  function section(title) {
4647
- console.log("\n" + import_chalk5.default.bold(title));
5576
+ console.log("\n" + import_chalk6.default.bold(title));
4648
5577
  }
4649
- console.log(import_chalk5.default.cyan.bold(`
5578
+ console.log(import_chalk6.default.cyan.bold(`
4650
5579
  \u{1F6E1}\uFE0F Node9 Doctor v${version}
4651
5580
  `));
4652
5581
  section("Binary");
4653
5582
  try {
4654
- const which = (0, import_child_process4.execSync)("which node9", { encoding: "utf-8" }).trim();
5583
+ const which = (0, import_child_process6.execSync)("which node9", { encoding: "utf-8" }).trim();
4655
5584
  pass(`node9 found at ${which}`);
4656
5585
  } catch {
4657
5586
  warn("node9 not found in $PATH \u2014 hooks may not find it", "Run: npm install -g @node9/proxy");
@@ -4666,7 +5595,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
4666
5595
  );
4667
5596
  }
4668
5597
  try {
4669
- const gitVersion = (0, import_child_process4.execSync)("git --version", { encoding: "utf-8" }).trim();
5598
+ const gitVersion = (0, import_child_process6.execSync)("git --version", { encoding: "utf-8" }).trim();
4670
5599
  pass(gitVersion);
4671
5600
  } catch {
4672
5601
  warn(
@@ -4675,10 +5604,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4675
5604
  );
4676
5605
  }
4677
5606
  section("Configuration");
4678
- const globalConfigPath = import_path8.default.join(homeDir2, ".node9", "config.json");
4679
- if (import_fs6.default.existsSync(globalConfigPath)) {
5607
+ const globalConfigPath = import_path9.default.join(homeDir2, ".node9", "config.json");
5608
+ if (import_fs7.default.existsSync(globalConfigPath)) {
4680
5609
  try {
4681
- JSON.parse(import_fs6.default.readFileSync(globalConfigPath, "utf-8"));
5610
+ JSON.parse(import_fs7.default.readFileSync(globalConfigPath, "utf-8"));
4682
5611
  pass("~/.node9/config.json found and valid");
4683
5612
  } catch {
4684
5613
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -4686,17 +5615,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
4686
5615
  } else {
4687
5616
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
4688
5617
  }
4689
- const projectConfigPath = import_path8.default.join(process.cwd(), "node9.config.json");
4690
- if (import_fs6.default.existsSync(projectConfigPath)) {
5618
+ const projectConfigPath = import_path9.default.join(process.cwd(), "node9.config.json");
5619
+ if (import_fs7.default.existsSync(projectConfigPath)) {
4691
5620
  try {
4692
- JSON.parse(import_fs6.default.readFileSync(projectConfigPath, "utf-8"));
5621
+ JSON.parse(import_fs7.default.readFileSync(projectConfigPath, "utf-8"));
4693
5622
  pass("node9.config.json found and valid (project)");
4694
5623
  } catch {
4695
5624
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
4696
5625
  }
4697
5626
  }
4698
- const credsPath = import_path8.default.join(homeDir2, ".node9", "credentials.json");
4699
- if (import_fs6.default.existsSync(credsPath)) {
5627
+ const credsPath = import_path9.default.join(homeDir2, ".node9", "credentials.json");
5628
+ if (import_fs7.default.existsSync(credsPath)) {
4700
5629
  pass("Cloud credentials found (~/.node9/credentials.json)");
4701
5630
  } else {
4702
5631
  warn(
@@ -4705,10 +5634,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4705
5634
  );
4706
5635
  }
4707
5636
  section("Agent Hooks");
4708
- const claudeSettingsPath = import_path8.default.join(homeDir2, ".claude", "settings.json");
4709
- if (import_fs6.default.existsSync(claudeSettingsPath)) {
5637
+ const claudeSettingsPath = import_path9.default.join(homeDir2, ".claude", "settings.json");
5638
+ if (import_fs7.default.existsSync(claudeSettingsPath)) {
4710
5639
  try {
4711
- const cs = JSON.parse(import_fs6.default.readFileSync(claudeSettingsPath, "utf-8"));
5640
+ const cs = JSON.parse(import_fs7.default.readFileSync(claudeSettingsPath, "utf-8"));
4712
5641
  const hasHook = cs.hooks?.PreToolUse?.some(
4713
5642
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4714
5643
  );
@@ -4721,10 +5650,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4721
5650
  } else {
4722
5651
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
4723
5652
  }
4724
- const geminiSettingsPath = import_path8.default.join(homeDir2, ".gemini", "settings.json");
4725
- if (import_fs6.default.existsSync(geminiSettingsPath)) {
5653
+ const geminiSettingsPath = import_path9.default.join(homeDir2, ".gemini", "settings.json");
5654
+ if (import_fs7.default.existsSync(geminiSettingsPath)) {
4726
5655
  try {
4727
- const gs = JSON.parse(import_fs6.default.readFileSync(geminiSettingsPath, "utf-8"));
5656
+ const gs = JSON.parse(import_fs7.default.readFileSync(geminiSettingsPath, "utf-8"));
4728
5657
  const hasHook = gs.hooks?.BeforeTool?.some(
4729
5658
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4730
5659
  );
@@ -4737,10 +5666,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4737
5666
  } else {
4738
5667
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
4739
5668
  }
4740
- const cursorHooksPath = import_path8.default.join(homeDir2, ".cursor", "hooks.json");
4741
- if (import_fs6.default.existsSync(cursorHooksPath)) {
5669
+ const cursorHooksPath = import_path9.default.join(homeDir2, ".cursor", "hooks.json");
5670
+ if (import_fs7.default.existsSync(cursorHooksPath)) {
4742
5671
  try {
4743
- const cur = JSON.parse(import_fs6.default.readFileSync(cursorHooksPath, "utf-8"));
5672
+ const cur = JSON.parse(import_fs7.default.readFileSync(cursorHooksPath, "utf-8"));
4744
5673
  const hasHook = cur.hooks?.preToolUse?.some(
4745
5674
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
4746
5675
  );
@@ -4761,9 +5690,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
4761
5690
  }
4762
5691
  console.log("");
4763
5692
  if (failures === 0) {
4764
- console.log(import_chalk5.default.green.bold(" All checks passed. Node9 is ready.\n"));
5693
+ console.log(import_chalk6.default.green.bold(" All checks passed. Node9 is ready.\n"));
4765
5694
  } else {
4766
- console.log(import_chalk5.default.red.bold(` ${failures} check(s) failed. See hints above.
5695
+ console.log(import_chalk6.default.red.bold(` ${failures} check(s) failed. See hints above.
4767
5696
  `));
4768
5697
  process.exit(1);
4769
5698
  }
@@ -4778,7 +5707,7 @@ program.command("explain").description(
4778
5707
  try {
4779
5708
  args = JSON.parse(trimmed);
4780
5709
  } catch {
4781
- console.error(import_chalk5.default.red(`
5710
+ console.error(import_chalk6.default.red(`
4782
5711
  \u274C Invalid JSON: ${trimmed}
4783
5712
  `));
4784
5713
  process.exit(1);
@@ -4789,63 +5718,63 @@ program.command("explain").description(
4789
5718
  }
4790
5719
  const result = await explainPolicy(tool, args);
4791
5720
  console.log("");
4792
- console.log(import_chalk5.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
5721
+ console.log(import_chalk6.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
4793
5722
  console.log("");
4794
- console.log(` ${import_chalk5.default.bold("Tool:")} ${import_chalk5.default.white(result.tool)}`);
5723
+ console.log(` ${import_chalk6.default.bold("Tool:")} ${import_chalk6.default.white(result.tool)}`);
4795
5724
  if (argsRaw) {
4796
5725
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
4797
- console.log(` ${import_chalk5.default.bold("Input:")} ${import_chalk5.default.gray(preview)}`);
5726
+ console.log(` ${import_chalk6.default.bold("Input:")} ${import_chalk6.default.gray(preview)}`);
4798
5727
  }
4799
5728
  console.log("");
4800
- console.log(import_chalk5.default.bold("Config Sources (Waterfall):"));
5729
+ console.log(import_chalk6.default.bold("Config Sources (Waterfall):"));
4801
5730
  for (const tier of result.waterfall) {
4802
- const num = import_chalk5.default.gray(` ${tier.tier}.`);
5731
+ const num = import_chalk6.default.gray(` ${tier.tier}.`);
4803
5732
  const label = tier.label.padEnd(16);
4804
5733
  let statusStr;
4805
5734
  if (tier.tier === 1) {
4806
- statusStr = import_chalk5.default.gray(tier.note ?? "");
5735
+ statusStr = import_chalk6.default.gray(tier.note ?? "");
4807
5736
  } else if (tier.status === "active") {
4808
- const loc = tier.path ? import_chalk5.default.gray(tier.path) : "";
4809
- const note = tier.note ? import_chalk5.default.gray(`(${tier.note})`) : "";
4810
- statusStr = import_chalk5.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
5737
+ const loc = tier.path ? import_chalk6.default.gray(tier.path) : "";
5738
+ const note = tier.note ? import_chalk6.default.gray(`(${tier.note})`) : "";
5739
+ statusStr = import_chalk6.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
4811
5740
  } else {
4812
- statusStr = import_chalk5.default.gray("\u25CB " + (tier.note ?? "not found"));
5741
+ statusStr = import_chalk6.default.gray("\u25CB " + (tier.note ?? "not found"));
4813
5742
  }
4814
- console.log(`${num} ${import_chalk5.default.white(label)} ${statusStr}`);
5743
+ console.log(`${num} ${import_chalk6.default.white(label)} ${statusStr}`);
4815
5744
  }
4816
5745
  console.log("");
4817
- console.log(import_chalk5.default.bold("Policy Evaluation:"));
5746
+ console.log(import_chalk6.default.bold("Policy Evaluation:"));
4818
5747
  for (const step of result.steps) {
4819
5748
  const isFinal = step.isFinal;
4820
5749
  let icon;
4821
- if (step.outcome === "allow") icon = import_chalk5.default.green(" \u2705");
4822
- else if (step.outcome === "review") icon = import_chalk5.default.red(" \u{1F534}");
4823
- else if (step.outcome === "skip") icon = import_chalk5.default.gray(" \u2500 ");
4824
- else icon = import_chalk5.default.gray(" \u25CB ");
5750
+ if (step.outcome === "allow") icon = import_chalk6.default.green(" \u2705");
5751
+ else if (step.outcome === "review") icon = import_chalk6.default.red(" \u{1F534}");
5752
+ else if (step.outcome === "skip") icon = import_chalk6.default.gray(" \u2500 ");
5753
+ else icon = import_chalk6.default.gray(" \u25CB ");
4825
5754
  const name = step.name.padEnd(18);
4826
- const nameStr = isFinal ? import_chalk5.default.white.bold(name) : import_chalk5.default.white(name);
4827
- const detail = isFinal ? import_chalk5.default.white(step.detail) : import_chalk5.default.gray(step.detail);
4828
- const arrow = isFinal ? import_chalk5.default.yellow(" \u2190 STOP") : "";
5755
+ const nameStr = isFinal ? import_chalk6.default.white.bold(name) : import_chalk6.default.white(name);
5756
+ const detail = isFinal ? import_chalk6.default.white(step.detail) : import_chalk6.default.gray(step.detail);
5757
+ const arrow = isFinal ? import_chalk6.default.yellow(" \u2190 STOP") : "";
4829
5758
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
4830
5759
  }
4831
5760
  console.log("");
4832
5761
  if (result.decision === "allow") {
4833
- console.log(import_chalk5.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk5.default.gray(" \u2014 no approval needed"));
5762
+ console.log(import_chalk6.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk6.default.gray(" \u2014 no approval needed"));
4834
5763
  } else {
4835
5764
  console.log(
4836
- import_chalk5.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk5.default.gray(" \u2014 human approval required")
5765
+ import_chalk6.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk6.default.gray(" \u2014 human approval required")
4837
5766
  );
4838
5767
  if (result.blockedByLabel) {
4839
- console.log(import_chalk5.default.gray(` Reason: ${result.blockedByLabel}`));
5768
+ console.log(import_chalk6.default.gray(` Reason: ${result.blockedByLabel}`));
4840
5769
  }
4841
5770
  }
4842
5771
  console.log("");
4843
5772
  });
4844
5773
  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) => {
4845
- const configPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "config.json");
4846
- if (import_fs6.default.existsSync(configPath) && !options.force) {
4847
- console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
4848
- console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
5774
+ const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
5775
+ if (import_fs7.default.existsSync(configPath) && !options.force) {
5776
+ console.log(import_chalk6.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
5777
+ console.log(import_chalk6.default.gray(` Run with --force to overwrite.`));
4849
5778
  return;
4850
5779
  }
4851
5780
  const requestedMode = options.mode.toLowerCase();
@@ -4857,13 +5786,13 @@ program.command("init").description("Create ~/.node9/config.json with default po
4857
5786
  mode: safeMode
4858
5787
  }
4859
5788
  };
4860
- const dir = import_path8.default.dirname(configPath);
4861
- if (!import_fs6.default.existsSync(dir)) import_fs6.default.mkdirSync(dir, { recursive: true });
4862
- import_fs6.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
4863
- console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
4864
- console.log(import_chalk5.default.cyan(` Mode set to: ${safeMode}`));
5789
+ const dir = import_path9.default.dirname(configPath);
5790
+ if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
5791
+ import_fs7.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
5792
+ console.log(import_chalk6.default.green(`\u2705 Global config created: ${configPath}`));
5793
+ console.log(import_chalk6.default.cyan(` Mode set to: ${safeMode}`));
4865
5794
  console.log(
4866
- import_chalk5.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
5795
+ import_chalk6.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
4867
5796
  );
4868
5797
  });
4869
5798
  function formatRelativeTime(timestamp) {
@@ -4877,14 +5806,14 @@ function formatRelativeTime(timestamp) {
4877
5806
  return new Date(timestamp).toLocaleDateString();
4878
5807
  }
4879
5808
  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) => {
4880
- const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "audit.log");
4881
- if (!import_fs6.default.existsSync(logPath)) {
5809
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
5810
+ if (!import_fs7.default.existsSync(logPath)) {
4882
5811
  console.log(
4883
- import_chalk5.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
5812
+ import_chalk6.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
4884
5813
  );
4885
5814
  return;
4886
5815
  }
4887
- const raw = import_fs6.default.readFileSync(logPath, "utf-8");
5816
+ const raw = import_fs7.default.readFileSync(logPath, "utf-8");
4888
5817
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
4889
5818
  let entries = lines.flatMap((line) => {
4890
5819
  try {
@@ -4906,31 +5835,31 @@ program.command("audit").description("View local execution audit log").option("-
4906
5835
  return;
4907
5836
  }
4908
5837
  if (entries.length === 0) {
4909
- console.log(import_chalk5.default.yellow("No matching audit entries."));
5838
+ console.log(import_chalk6.default.yellow("No matching audit entries."));
4910
5839
  return;
4911
5840
  }
4912
5841
  console.log(
4913
5842
  `
4914
- ${import_chalk5.default.bold("Node9 Audit Log")} ${import_chalk5.default.dim(`(${entries.length} entries)`)}`
5843
+ ${import_chalk6.default.bold("Node9 Audit Log")} ${import_chalk6.default.dim(`(${entries.length} entries)`)}`
4915
5844
  );
4916
- console.log(import_chalk5.default.dim(" " + "\u2500".repeat(65)));
5845
+ console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
4917
5846
  console.log(
4918
5847
  ` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
4919
5848
  );
4920
- console.log(import_chalk5.default.dim(" " + "\u2500".repeat(65)));
5849
+ console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
4921
5850
  for (const e of entries) {
4922
5851
  const time = formatRelativeTime(String(e.ts)).padEnd(12);
4923
5852
  const tool = String(e.tool).slice(0, 17).padEnd(18);
4924
- const result = e.decision === "allow" ? import_chalk5.default.green("ALLOW".padEnd(10)) : import_chalk5.default.red("DENY".padEnd(10));
5853
+ const result = e.decision === "allow" ? import_chalk6.default.green("ALLOW".padEnd(10)) : import_chalk6.default.red("DENY".padEnd(10));
4925
5854
  const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
4926
5855
  const agent = String(e.agent || "unknown");
4927
5856
  console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
4928
5857
  }
4929
5858
  const allowed = entries.filter((e) => e.decision === "allow").length;
4930
5859
  const denied = entries.filter((e) => e.decision === "deny").length;
4931
- console.log(import_chalk5.default.dim(" " + "\u2500".repeat(65)));
5860
+ console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
4932
5861
  console.log(
4933
- ` ${entries.length} entries | ${import_chalk5.default.green(allowed + " allowed")} | ${import_chalk5.default.red(denied + " denied")}
5862
+ ` ${entries.length} entries | ${import_chalk6.default.green(allowed + " allowed")} | ${import_chalk6.default.red(denied + " denied")}
4934
5863
  `
4935
5864
  );
4936
5865
  });
@@ -4941,43 +5870,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
4941
5870
  const settings = mergedConfig.settings;
4942
5871
  console.log("");
4943
5872
  if (creds && settings.approvers.cloud) {
4944
- console.log(import_chalk5.default.green(" \u25CF Agent mode") + import_chalk5.default.gray(" \u2014 cloud team policy enforced"));
5873
+ console.log(import_chalk6.default.green(" \u25CF Agent mode") + import_chalk6.default.gray(" \u2014 cloud team policy enforced"));
4945
5874
  } else if (creds && !settings.approvers.cloud) {
4946
5875
  console.log(
4947
- import_chalk5.default.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + import_chalk5.default.gray(" \u2014 all decisions stay on this machine")
5876
+ import_chalk6.default.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + import_chalk6.default.gray(" \u2014 all decisions stay on this machine")
4948
5877
  );
4949
5878
  } else {
4950
5879
  console.log(
4951
- import_chalk5.default.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + import_chalk5.default.gray(" \u2014 no API key (Local rules only)")
5880
+ import_chalk6.default.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + import_chalk6.default.gray(" \u2014 no API key (Local rules only)")
4952
5881
  );
4953
5882
  }
4954
5883
  console.log("");
4955
5884
  if (daemonRunning) {
4956
5885
  console.log(
4957
- import_chalk5.default.green(" \u25CF Daemon running") + import_chalk5.default.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
5886
+ import_chalk6.default.green(" \u25CF Daemon running") + import_chalk6.default.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
4958
5887
  );
4959
5888
  } else {
4960
- console.log(import_chalk5.default.gray(" \u25CB Daemon stopped"));
5889
+ console.log(import_chalk6.default.gray(" \u25CB Daemon stopped"));
4961
5890
  }
4962
5891
  if (settings.enableUndo) {
4963
5892
  console.log(
4964
- import_chalk5.default.magenta(" \u25CF Undo Engine") + import_chalk5.default.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
5893
+ import_chalk6.default.magenta(" \u25CF Undo Engine") + import_chalk6.default.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
4965
5894
  );
4966
5895
  }
4967
5896
  console.log("");
4968
- const modeLabel = settings.mode === "audit" ? import_chalk5.default.blue("audit") : settings.mode === "strict" ? import_chalk5.default.red("strict") : import_chalk5.default.white("standard");
5897
+ const modeLabel = settings.mode === "audit" ? import_chalk6.default.blue("audit") : settings.mode === "strict" ? import_chalk6.default.red("strict") : import_chalk6.default.white("standard");
4969
5898
  console.log(` Mode: ${modeLabel}`);
4970
- const projectConfig = import_path8.default.join(process.cwd(), "node9.config.json");
4971
- const globalConfig = import_path8.default.join(import_os6.default.homedir(), ".node9", "config.json");
5899
+ const projectConfig = import_path9.default.join(process.cwd(), "node9.config.json");
5900
+ const globalConfig = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
4972
5901
  console.log(
4973
- ` Local: ${import_fs6.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
5902
+ ` Local: ${import_fs7.default.existsSync(projectConfig) ? import_chalk6.default.green("Active (node9.config.json)") : import_chalk6.default.gray("Not present")}`
4974
5903
  );
4975
5904
  console.log(
4976
- ` Global: ${import_fs6.default.existsSync(globalConfig) ? import_chalk5.default.green("Active (~/.node9/config.json)") : import_chalk5.default.gray("Not present")}`
5905
+ ` Global: ${import_fs7.default.existsSync(globalConfig) ? import_chalk6.default.green("Active (~/.node9/config.json)") : import_chalk6.default.gray("Not present")}`
4977
5906
  );
4978
5907
  if (mergedConfig.policy.sandboxPaths.length > 0) {
4979
5908
  console.log(
4980
- ` Sandbox: ${import_chalk5.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
5909
+ ` Sandbox: ${import_chalk6.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
4981
5910
  );
4982
5911
  }
4983
5912
  const pauseState = checkPause();
@@ -4985,47 +5914,63 @@ program.command("status").description("Show current Node9 mode, policy source, a
4985
5914
  const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
4986
5915
  console.log("");
4987
5916
  console.log(
4988
- import_chalk5.default.yellow(` \u23F8 PAUSED until ${expiresAt}`) + import_chalk5.default.gray(" \u2014 all tool calls allowed")
5917
+ import_chalk6.default.yellow(` \u23F8 PAUSED until ${expiresAt}`) + import_chalk6.default.gray(" \u2014 all tool calls allowed")
4989
5918
  );
4990
5919
  }
4991
5920
  console.log("");
4992
5921
  });
4993
- 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(
5922
+ 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(
5923
+ "-w, --watch",
5924
+ "Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
5925
+ ).action(
4994
5926
  async (action, options) => {
4995
5927
  const cmd = (action ?? "start").toLowerCase();
4996
5928
  if (cmd === "stop") return stopDaemon();
4997
5929
  if (cmd === "status") return daemonStatus();
4998
5930
  if (cmd !== "start" && action !== void 0) {
4999
- console.error(import_chalk5.default.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
5931
+ console.error(import_chalk6.default.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
5000
5932
  process.exit(1);
5001
5933
  }
5934
+ if (options.watch) {
5935
+ process.env.NODE9_WATCH_MODE = "1";
5936
+ setTimeout(() => {
5937
+ openBrowserLocal();
5938
+ console.log(import_chalk6.default.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5939
+ }, 600);
5940
+ startDaemon();
5941
+ return;
5942
+ }
5002
5943
  if (options.openui) {
5003
5944
  if (isDaemonRunning()) {
5004
5945
  openBrowserLocal();
5005
- console.log(import_chalk5.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5946
+ console.log(import_chalk6.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5006
5947
  process.exit(0);
5007
5948
  }
5008
- const child = (0, import_child_process4.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
5949
+ const child = (0, import_child_process6.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
5009
5950
  child.unref();
5010
5951
  for (let i = 0; i < 12; i++) {
5011
5952
  await new Promise((r) => setTimeout(r, 250));
5012
5953
  if (isDaemonRunning()) break;
5013
5954
  }
5014
5955
  openBrowserLocal();
5015
- console.log(import_chalk5.default.green(`
5956
+ console.log(import_chalk6.default.green(`
5016
5957
  \u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
5017
5958
  process.exit(0);
5018
5959
  }
5019
5960
  if (options.background) {
5020
- const child = (0, import_child_process4.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
5961
+ const child = (0, import_child_process6.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
5021
5962
  child.unref();
5022
- console.log(import_chalk5.default.green(`
5963
+ console.log(import_chalk6.default.green(`
5023
5964
  \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
5024
5965
  process.exit(0);
5025
5966
  }
5026
5967
  startDaemon();
5027
5968
  }
5028
5969
  );
5970
+ program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).option("--clear", "Clear history buffer and stream live events fresh", false).action(async (options) => {
5971
+ const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
5972
+ await startTail2(options);
5973
+ });
5029
5974
  program.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
5030
5975
  const processPayload = async (raw) => {
5031
5976
  try {
@@ -5036,9 +5981,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
5036
5981
  } catch (err) {
5037
5982
  const tempConfig = getConfig();
5038
5983
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
5039
- const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
5984
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
5040
5985
  const errMsg = err instanceof Error ? err.message : String(err);
5041
- import_fs6.default.appendFileSync(
5986
+ import_fs7.default.appendFileSync(
5042
5987
  logPath,
5043
5988
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
5044
5989
  RAW: ${raw}
@@ -5056,10 +6001,10 @@ RAW: ${raw}
5056
6001
  }
5057
6002
  const config = getConfig();
5058
6003
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
5059
- const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
5060
- if (!import_fs6.default.existsSync(import_path8.default.dirname(logPath)))
5061
- import_fs6.default.mkdirSync(import_path8.default.dirname(logPath), { recursive: true });
5062
- import_fs6.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
6004
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
6005
+ if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
6006
+ import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
6007
+ import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
5063
6008
  `);
5064
6009
  }
5065
6010
  const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
@@ -5071,18 +6016,18 @@ RAW: ${raw}
5071
6016
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
5072
6017
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
5073
6018
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
5074
- console.error(import_chalk5.default.bgRed.white.bold(`
6019
+ console.error(import_chalk6.default.bgRed.white.bold(`
5075
6020
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
5076
- console.error(import_chalk5.default.red.bold(` A sensitive secret was found in the tool arguments!`));
6021
+ console.error(import_chalk6.default.red.bold(` A sensitive secret was found in the tool arguments!`));
5077
6022
  } else {
5078
- console.error(import_chalk5.default.red(`
6023
+ console.error(import_chalk6.default.red(`
5079
6024
  \u{1F6D1} Node9 blocked "${toolName}"`));
5080
6025
  }
5081
- console.error(import_chalk5.default.gray(` Triggered by: ${blockedByContext}`));
5082
- if (result2?.changeHint) console.error(import_chalk5.default.cyan(` To change: ${result2.changeHint}`));
6026
+ console.error(import_chalk6.default.gray(` Triggered by: ${blockedByContext}`));
6027
+ if (result2?.changeHint) console.error(import_chalk6.default.cyan(` To change: ${result2.changeHint}`));
5083
6028
  console.error("");
5084
6029
  const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
5085
- console.error(import_chalk5.default.dim(` (Detailed instructions sent to AI agent)`));
6030
+ console.error(import_chalk6.default.dim(` (Detailed instructions sent to AI agent)`));
5086
6031
  process.stdout.write(
5087
6032
  JSON.stringify({
5088
6033
  decision: "block",
@@ -5113,7 +6058,7 @@ RAW: ${raw}
5113
6058
  process.exit(0);
5114
6059
  }
5115
6060
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
5116
- console.error(import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
6061
+ console.error(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
5117
6062
  const daemonReady = await autoStartDaemonAndWait();
5118
6063
  if (daemonReady) {
5119
6064
  const retry = await authorizeHeadless(toolName, toolInput, false, meta);
@@ -5136,9 +6081,9 @@ RAW: ${raw}
5136
6081
  });
5137
6082
  } catch (err) {
5138
6083
  if (process.env.NODE9_DEBUG === "1") {
5139
- const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
6084
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
5140
6085
  const errMsg = err instanceof Error ? err.message : String(err);
5141
- import_fs6.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
6086
+ import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
5142
6087
  `);
5143
6088
  }
5144
6089
  process.exit(0);
@@ -5183,10 +6128,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
5183
6128
  decision: "allowed",
5184
6129
  source: "post-hook"
5185
6130
  };
5186
- const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "audit.log");
5187
- if (!import_fs6.default.existsSync(import_path8.default.dirname(logPath)))
5188
- import_fs6.default.mkdirSync(import_path8.default.dirname(logPath), { recursive: true });
5189
- import_fs6.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
6131
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
6132
+ if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
6133
+ import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
6134
+ import_fs7.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
5190
6135
  const config = getConfig();
5191
6136
  if (shouldSnapshot(tool, {}, config)) {
5192
6137
  await createShadowSnapshot();
@@ -5213,7 +6158,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
5213
6158
  const ms = parseDuration(options.duration);
5214
6159
  if (ms === null) {
5215
6160
  console.error(
5216
- import_chalk5.default.red(`
6161
+ import_chalk6.default.red(`
5217
6162
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
5218
6163
  `)
5219
6164
  );
@@ -5221,20 +6166,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
5221
6166
  }
5222
6167
  pauseNode9(ms, options.duration);
5223
6168
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
5224
- console.log(import_chalk5.default.yellow(`
6169
+ console.log(import_chalk6.default.yellow(`
5225
6170
  \u23F8 Node9 paused until ${expiresAt}`));
5226
- console.log(import_chalk5.default.gray(` All tool calls will be allowed without review.`));
5227
- console.log(import_chalk5.default.gray(` Run "node9 resume" to re-enable early.
6171
+ console.log(import_chalk6.default.gray(` All tool calls will be allowed without review.`));
6172
+ console.log(import_chalk6.default.gray(` Run "node9 resume" to re-enable early.
5228
6173
  `));
5229
6174
  });
5230
6175
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
5231
6176
  const { paused } = checkPause();
5232
6177
  if (!paused) {
5233
- console.log(import_chalk5.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
6178
+ console.log(import_chalk6.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
5234
6179
  return;
5235
6180
  }
5236
6181
  resumeNode9();
5237
- console.log(import_chalk5.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
6182
+ console.log(import_chalk6.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
5238
6183
  });
5239
6184
  var HOOK_BASED_AGENTS = {
5240
6185
  claude: "claude",
@@ -5247,15 +6192,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
5247
6192
  if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
5248
6193
  const target = HOOK_BASED_AGENTS[firstArg];
5249
6194
  console.error(
5250
- import_chalk5.default.yellow(`
6195
+ import_chalk6.default.yellow(`
5251
6196
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
5252
6197
  );
5253
- console.error(import_chalk5.default.white(`
6198
+ console.error(import_chalk6.default.white(`
5254
6199
  "${target}" uses its own hook system. Use:`));
5255
6200
  console.error(
5256
- import_chalk5.default.green(` node9 addto ${target} `) + import_chalk5.default.gray("# one-time setup")
6201
+ import_chalk6.default.green(` node9 addto ${target} `) + import_chalk6.default.gray("# one-time setup")
5257
6202
  );
5258
- console.error(import_chalk5.default.green(` ${target} `) + import_chalk5.default.gray("# run normally"));
6203
+ console.error(import_chalk6.default.green(` ${target} `) + import_chalk6.default.gray("# run normally"));
5259
6204
  process.exit(1);
5260
6205
  }
5261
6206
  const fullCommand = commandArgs.join(" ");
@@ -5263,7 +6208,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
5263
6208
  agent: "Terminal"
5264
6209
  });
5265
6210
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
5266
- console.error(import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
6211
+ console.error(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
5267
6212
  const daemonReady = await autoStartDaemonAndWait();
5268
6213
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
5269
6214
  }
@@ -5272,12 +6217,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
5272
6217
  }
5273
6218
  if (!result.approved) {
5274
6219
  console.error(
5275
- import_chalk5.default.red(`
6220
+ import_chalk6.default.red(`
5276
6221
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
5277
6222
  );
5278
6223
  process.exit(1);
5279
6224
  }
5280
- console.error(import_chalk5.default.green("\n\u2705 Approved \u2014 running command...\n"));
6225
+ console.error(import_chalk6.default.green("\n\u2705 Approved \u2014 running command...\n"));
5281
6226
  await runProxy(fullCommand);
5282
6227
  } else {
5283
6228
  program.help();
@@ -5292,22 +6237,22 @@ program.command("undo").description(
5292
6237
  if (history.length === 0) {
5293
6238
  if (!options.all && allHistory.length > 0) {
5294
6239
  console.log(
5295
- import_chalk5.default.yellow(
6240
+ import_chalk6.default.yellow(
5296
6241
  `
5297
6242
  \u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
5298
- Run ${import_chalk5.default.cyan("node9 undo --all")} to see snapshots from all projects.
6243
+ Run ${import_chalk6.default.cyan("node9 undo --all")} to see snapshots from all projects.
5299
6244
  `
5300
6245
  )
5301
6246
  );
5302
6247
  } else {
5303
- console.log(import_chalk5.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
6248
+ console.log(import_chalk6.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
5304
6249
  }
5305
6250
  return;
5306
6251
  }
5307
6252
  const idx = history.length - steps;
5308
6253
  if (idx < 0) {
5309
6254
  console.log(
5310
- import_chalk5.default.yellow(
6255
+ import_chalk6.default.yellow(
5311
6256
  `
5312
6257
  \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
5313
6258
  `
@@ -5318,18 +6263,18 @@ program.command("undo").description(
5318
6263
  const snapshot = history[idx];
5319
6264
  const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
5320
6265
  const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
5321
- console.log(import_chalk5.default.magenta.bold(`
6266
+ console.log(import_chalk6.default.magenta.bold(`
5322
6267
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
5323
6268
  console.log(
5324
- import_chalk5.default.white(
5325
- ` Tool: ${import_chalk5.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk5.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
6269
+ import_chalk6.default.white(
6270
+ ` Tool: ${import_chalk6.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk6.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
5326
6271
  )
5327
6272
  );
5328
- console.log(import_chalk5.default.white(` When: ${import_chalk5.default.gray(ageStr)}`));
5329
- console.log(import_chalk5.default.white(` Dir: ${import_chalk5.default.gray(snapshot.cwd)}`));
6273
+ console.log(import_chalk6.default.white(` When: ${import_chalk6.default.gray(ageStr)}`));
6274
+ console.log(import_chalk6.default.white(` Dir: ${import_chalk6.default.gray(snapshot.cwd)}`));
5330
6275
  if (steps > 1)
5331
6276
  console.log(
5332
- import_chalk5.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
6277
+ import_chalk6.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
5333
6278
  );
5334
6279
  console.log("");
5335
6280
  const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
@@ -5337,21 +6282,21 @@ program.command("undo").description(
5337
6282
  const lines = diff.split("\n");
5338
6283
  for (const line of lines) {
5339
6284
  if (line.startsWith("+++") || line.startsWith("---")) {
5340
- console.log(import_chalk5.default.bold(line));
6285
+ console.log(import_chalk6.default.bold(line));
5341
6286
  } else if (line.startsWith("+")) {
5342
- console.log(import_chalk5.default.green(line));
6287
+ console.log(import_chalk6.default.green(line));
5343
6288
  } else if (line.startsWith("-")) {
5344
- console.log(import_chalk5.default.red(line));
6289
+ console.log(import_chalk6.default.red(line));
5345
6290
  } else if (line.startsWith("@@")) {
5346
- console.log(import_chalk5.default.cyan(line));
6291
+ console.log(import_chalk6.default.cyan(line));
5347
6292
  } else {
5348
- console.log(import_chalk5.default.gray(line));
6293
+ console.log(import_chalk6.default.gray(line));
5349
6294
  }
5350
6295
  }
5351
6296
  console.log("");
5352
6297
  } else {
5353
6298
  console.log(
5354
- import_chalk5.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
6299
+ import_chalk6.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
5355
6300
  );
5356
6301
  }
5357
6302
  const proceed = await (0, import_prompts3.confirm)({
@@ -5360,42 +6305,42 @@ program.command("undo").description(
5360
6305
  });
5361
6306
  if (proceed) {
5362
6307
  if (applyUndo(snapshot.hash, snapshot.cwd)) {
5363
- console.log(import_chalk5.default.green("\n\u2705 Reverted successfully.\n"));
6308
+ console.log(import_chalk6.default.green("\n\u2705 Reverted successfully.\n"));
5364
6309
  } else {
5365
- console.error(import_chalk5.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
6310
+ console.error(import_chalk6.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
5366
6311
  }
5367
6312
  } else {
5368
- console.log(import_chalk5.default.gray("\nCancelled.\n"));
6313
+ console.log(import_chalk6.default.gray("\nCancelled.\n"));
5369
6314
  }
5370
6315
  });
5371
6316
  var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
5372
6317
  shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
5373
6318
  const name = resolveShieldName(service);
5374
6319
  if (!name) {
5375
- console.error(import_chalk5.default.red(`
6320
+ console.error(import_chalk6.default.red(`
5376
6321
  \u274C Unknown shield: "${service}"
5377
6322
  `));
5378
- console.log(`Run ${import_chalk5.default.cyan("node9 shield list")} to see available shields.
6323
+ console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
5379
6324
  `);
5380
6325
  process.exit(1);
5381
6326
  }
5382
6327
  const shield = getShield(name);
5383
6328
  const active = readActiveShields();
5384
6329
  if (active.includes(name)) {
5385
- console.log(import_chalk5.default.yellow(`
6330
+ console.log(import_chalk6.default.yellow(`
5386
6331
  \u2139\uFE0F Shield "${name}" is already active.
5387
6332
  `));
5388
6333
  return;
5389
6334
  }
5390
6335
  writeActiveShields([...active, name]);
5391
- console.log(import_chalk5.default.green(`
6336
+ console.log(import_chalk6.default.green(`
5392
6337
  \u{1F6E1}\uFE0F Shield "${name}" enabled.`));
5393
- console.log(import_chalk5.default.gray(` ${shield.smartRules.length} smart rules now active.`));
6338
+ console.log(import_chalk6.default.gray(` ${shield.smartRules.length} smart rules now active.`));
5394
6339
  if (shield.dangerousWords.length > 0)
5395
- console.log(import_chalk5.default.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
6340
+ console.log(import_chalk6.default.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
5396
6341
  if (name === "filesystem") {
5397
6342
  console.log(
5398
- import_chalk5.default.yellow(
6343
+ import_chalk6.default.yellow(
5399
6344
  `
5400
6345
  \u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
5401
6346
  Tools like unlink, find -delete, or language-level file ops are not intercepted.`
@@ -5407,51 +6352,51 @@ shieldCmd.command("enable <service>").description("Enable a security shield for
5407
6352
  shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
5408
6353
  const name = resolveShieldName(service);
5409
6354
  if (!name) {
5410
- console.error(import_chalk5.default.red(`
6355
+ console.error(import_chalk6.default.red(`
5411
6356
  \u274C Unknown shield: "${service}"
5412
6357
  `));
5413
- console.log(`Run ${import_chalk5.default.cyan("node9 shield list")} to see available shields.
6358
+ console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
5414
6359
  `);
5415
6360
  process.exit(1);
5416
6361
  }
5417
6362
  const active = readActiveShields();
5418
6363
  if (!active.includes(name)) {
5419
- console.log(import_chalk5.default.yellow(`
6364
+ console.log(import_chalk6.default.yellow(`
5420
6365
  \u2139\uFE0F Shield "${name}" is not active.
5421
6366
  `));
5422
6367
  return;
5423
6368
  }
5424
6369
  writeActiveShields(active.filter((s) => s !== name));
5425
- console.log(import_chalk5.default.green(`
6370
+ console.log(import_chalk6.default.green(`
5426
6371
  \u{1F6E1}\uFE0F Shield "${name}" disabled.
5427
6372
  `));
5428
6373
  });
5429
6374
  shieldCmd.command("list").description("Show all available shields").action(() => {
5430
6375
  const active = new Set(readActiveShields());
5431
- console.log(import_chalk5.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
6376
+ console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
5432
6377
  for (const shield of listShields()) {
5433
- const status = active.has(shield.name) ? import_chalk5.default.green("\u25CF enabled") : import_chalk5.default.gray("\u25CB disabled");
5434
- console.log(` ${status} ${import_chalk5.default.cyan(shield.name.padEnd(12))} ${shield.description}`);
6378
+ const status = active.has(shield.name) ? import_chalk6.default.green("\u25CF enabled") : import_chalk6.default.gray("\u25CB disabled");
6379
+ console.log(` ${status} ${import_chalk6.default.cyan(shield.name.padEnd(12))} ${shield.description}`);
5435
6380
  if (shield.aliases.length > 0)
5436
- console.log(import_chalk5.default.gray(` aliases: ${shield.aliases.join(", ")}`));
6381
+ console.log(import_chalk6.default.gray(` aliases: ${shield.aliases.join(", ")}`));
5437
6382
  }
5438
6383
  console.log("");
5439
6384
  });
5440
6385
  shieldCmd.command("status").description("Show which shields are currently active").action(() => {
5441
6386
  const active = readActiveShields();
5442
6387
  if (active.length === 0) {
5443
- console.log(import_chalk5.default.yellow("\n\u2139\uFE0F No shields are active.\n"));
5444
- console.log(`Run ${import_chalk5.default.cyan("node9 shield list")} to see available shields.
6388
+ console.log(import_chalk6.default.yellow("\n\u2139\uFE0F No shields are active.\n"));
6389
+ console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
5445
6390
  `);
5446
6391
  return;
5447
6392
  }
5448
- console.log(import_chalk5.default.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
6393
+ console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
5449
6394
  for (const name of active) {
5450
6395
  const shield = getShield(name);
5451
6396
  if (!shield) continue;
5452
- console.log(` ${import_chalk5.default.green("\u25CF")} ${import_chalk5.default.cyan(name)}`);
6397
+ console.log(` ${import_chalk6.default.green("\u25CF")} ${import_chalk6.default.cyan(name)}`);
5453
6398
  console.log(
5454
- import_chalk5.default.gray(
6399
+ import_chalk6.default.gray(
5455
6400
  ` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
5456
6401
  )
5457
6402
  );
@@ -5462,9 +6407,9 @@ process.on("unhandledRejection", (reason) => {
5462
6407
  const isCheckHook = process.argv[2] === "check";
5463
6408
  if (isCheckHook) {
5464
6409
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
5465
- const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
6410
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
5466
6411
  const msg = reason instanceof Error ? reason.message : String(reason);
5467
- import_fs6.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
6412
+ import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
5468
6413
  `);
5469
6414
  }
5470
6415
  process.exit(0);