@node9/proxy 1.0.14 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,87 @@ 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" }
606
- ],
607
- verdict: "review",
608
- reason: "chmod 777 requires human approval (filesystem shield)"
609
- },
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
- }
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"
621
412
  ],
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"]
413
+ {
414
+ errorMap: () => ({
415
+ message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
416
+ })
417
+ }
418
+ ),
419
+ value: import_zod.z.string().optional(),
420
+ flags: import_zod.z.string().optional()
421
+ });
422
+ SmartRuleSchema = import_zod.z.object({
423
+ name: import_zod.z.string().optional(),
424
+ tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
425
+ conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
426
+ conditionMode: import_zod.z.enum(["all", "any"]).optional(),
427
+ verdict: import_zod.z.enum(["allow", "review", "block"], {
428
+ errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
429
+ }),
430
+ reason: import_zod.z.string().optional()
431
+ });
432
+ ConfigFileSchema = import_zod.z.object({
433
+ version: import_zod.z.string().optional(),
434
+ settings: import_zod.z.object({
435
+ mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
436
+ autoStartDaemon: import_zod.z.boolean().optional(),
437
+ enableUndo: import_zod.z.boolean().optional(),
438
+ enableHookLogDebug: import_zod.z.boolean().optional(),
439
+ approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
440
+ approvers: import_zod.z.object({
441
+ native: import_zod.z.boolean().optional(),
442
+ browser: import_zod.z.boolean().optional(),
443
+ cloud: import_zod.z.boolean().optional(),
444
+ terminal: import_zod.z.boolean().optional()
445
+ }).optional(),
446
+ environment: import_zod.z.string().optional(),
447
+ slackEnabled: import_zod.z.boolean().optional(),
448
+ enableTrustSessions: import_zod.z.boolean().optional(),
449
+ allowGlobalPause: import_zod.z.boolean().optional()
450
+ }).optional(),
451
+ policy: import_zod.z.object({
452
+ sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
453
+ dangerousWords: import_zod.z.array(noNewlines).optional(),
454
+ ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
455
+ toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
456
+ smartRules: import_zod.z.array(SmartRuleSchema).optional(),
457
+ snapshot: import_zod.z.object({
458
+ tools: import_zod.z.array(import_zod.z.string()).optional(),
459
+ onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
460
+ ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
461
+ }).optional(),
462
+ dlp: import_zod.z.object({
463
+ enabled: import_zod.z.boolean().optional(),
464
+ scanIgnoredTools: import_zod.z.boolean().optional()
465
+ }).optional()
466
+ }).optional(),
467
+ environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
468
+ }).strict({ message: "Config contains unknown top-level keys" });
630
469
  }
631
- };
470
+ });
471
+
472
+ // src/shields.ts
632
473
  function resolveShieldName(input) {
633
474
  const lower = input.toLowerCase();
634
475
  if (SHIELDS[lower]) return lower;
@@ -644,7 +485,6 @@ function getShield(name) {
644
485
  function listShields() {
645
486
  return Object.values(SHIELDS);
646
487
  }
647
- var SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
648
488
  function readActiveShields() {
649
489
  try {
650
490
  const raw = import_fs.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
@@ -669,21 +509,186 @@ function writeActiveShields(active) {
669
509
  import_fs.default.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
670
510
  import_fs.default.renameSync(tmp, SHIELDS_STATE_FILE);
671
511
  }
512
+ var import_fs, import_path3, import_os, import_crypto, SHIELDS, SHIELDS_STATE_FILE;
513
+ var init_shields = __esm({
514
+ "src/shields.ts"() {
515
+ "use strict";
516
+ import_fs = __toESM(require("fs"));
517
+ import_path3 = __toESM(require("path"));
518
+ import_os = __toESM(require("os"));
519
+ import_crypto = __toESM(require("crypto"));
520
+ SHIELDS = {
521
+ postgres: {
522
+ name: "postgres",
523
+ description: "Protects PostgreSQL databases from destructive AI operations",
524
+ aliases: ["pg", "postgresql"],
525
+ smartRules: [
526
+ {
527
+ name: "shield:postgres:block-drop-table",
528
+ tool: "*",
529
+ conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
530
+ verdict: "block",
531
+ reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
532
+ },
533
+ {
534
+ name: "shield:postgres:block-truncate",
535
+ tool: "*",
536
+ conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
537
+ verdict: "block",
538
+ reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
539
+ },
540
+ {
541
+ name: "shield:postgres:block-drop-column",
542
+ tool: "*",
543
+ conditions: [
544
+ { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
545
+ ],
546
+ verdict: "block",
547
+ reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
548
+ },
549
+ {
550
+ name: "shield:postgres:review-grant-revoke",
551
+ tool: "*",
552
+ conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
553
+ verdict: "review",
554
+ reason: "Permission changes require human approval (Postgres shield)"
555
+ }
556
+ ],
557
+ dangerousWords: ["dropdb", "pg_dropcluster"]
558
+ },
559
+ github: {
560
+ name: "github",
561
+ description: "Protects GitHub repositories from destructive AI operations",
562
+ aliases: ["git"],
563
+ smartRules: [
564
+ {
565
+ // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
566
+ // This rule adds coverage for `git push --delete` which the built-in does not match.
567
+ name: "shield:github:review-delete-branch-remote",
568
+ tool: "bash",
569
+ conditions: [
570
+ {
571
+ field: "command",
572
+ op: "matches",
573
+ value: "git\\s+push\\s+.*--delete",
574
+ flags: "i"
575
+ }
576
+ ],
577
+ verdict: "review",
578
+ reason: "Remote branch deletion requires human approval (GitHub shield)"
579
+ },
580
+ {
581
+ name: "shield:github:block-delete-repo",
582
+ tool: "*",
583
+ conditions: [
584
+ { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
585
+ ],
586
+ verdict: "block",
587
+ reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
588
+ }
589
+ ],
590
+ dangerousWords: []
591
+ },
592
+ aws: {
593
+ name: "aws",
594
+ description: "Protects AWS infrastructure from destructive AI operations",
595
+ aliases: ["amazon"],
596
+ smartRules: [
597
+ {
598
+ name: "shield:aws:block-delete-s3-bucket",
599
+ tool: "*",
600
+ conditions: [
601
+ {
602
+ field: "command",
603
+ op: "matches",
604
+ value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
605
+ flags: "i"
606
+ }
607
+ ],
608
+ verdict: "block",
609
+ reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
610
+ },
611
+ {
612
+ name: "shield:aws:review-iam-changes",
613
+ tool: "*",
614
+ conditions: [
615
+ {
616
+ field: "command",
617
+ op: "matches",
618
+ value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
619
+ flags: "i"
620
+ }
621
+ ],
622
+ verdict: "review",
623
+ reason: "IAM changes require human approval (AWS shield)"
624
+ },
625
+ {
626
+ name: "shield:aws:block-ec2-terminate",
627
+ tool: "*",
628
+ conditions: [
629
+ {
630
+ field: "command",
631
+ op: "matches",
632
+ value: "aws\\s+ec2\\s+terminate-instances",
633
+ flags: "i"
634
+ }
635
+ ],
636
+ verdict: "block",
637
+ reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
638
+ },
639
+ {
640
+ name: "shield:aws:review-rds-delete",
641
+ tool: "*",
642
+ conditions: [
643
+ { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
644
+ ],
645
+ verdict: "review",
646
+ reason: "RDS deletion requires human approval (AWS shield)"
647
+ }
648
+ ],
649
+ dangerousWords: []
650
+ },
651
+ filesystem: {
652
+ name: "filesystem",
653
+ description: "Protects the local filesystem from dangerous AI operations",
654
+ aliases: ["fs"],
655
+ smartRules: [
656
+ {
657
+ name: "shield:filesystem:review-chmod-777",
658
+ tool: "bash",
659
+ conditions: [
660
+ { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
661
+ ],
662
+ verdict: "review",
663
+ reason: "chmod 777 requires human approval (filesystem shield)"
664
+ },
665
+ {
666
+ name: "shield:filesystem:review-write-etc",
667
+ tool: "bash",
668
+ conditions: [
669
+ {
670
+ field: "command",
671
+ // Narrow to write-indicative operations to avoid approval fatigue on reads.
672
+ // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
673
+ op: "matches",
674
+ value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
675
+ }
676
+ ],
677
+ verdict: "review",
678
+ reason: "Writing to /etc requires human approval (filesystem shield)"
679
+ }
680
+ ],
681
+ // dd removed: too common as a legitimate tool (disk imaging, file ops).
682
+ // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
683
+ // wipefs retained: rarely legitimate in an agent context and not in built-ins.
684
+ dangerousWords: ["wipefs"]
685
+ }
686
+ };
687
+ SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
688
+ }
689
+ });
672
690
 
673
691
  // 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
692
  function maskSecret(raw, pattern) {
688
693
  const match = raw.match(pattern);
689
694
  if (!match) return "****";
@@ -694,9 +699,6 @@ function maskSecret(raw, pattern) {
694
699
  const stars = "*".repeat(Math.min(secret.length - 8, 12));
695
700
  return `${prefix}${stars}${suffix}`;
696
701
  }
697
- var MAX_DEPTH = 5;
698
- var MAX_STRING_BYTES = 1e5;
699
- var MAX_JSON_PARSE_BYTES = 1e4;
700
702
  function scanArgs(args, depth = 0, fieldPath = "args") {
701
703
  if (depth > MAX_DEPTH || args === null || args === void 0) return null;
702
704
  if (Array.isArray(args)) {
@@ -739,12 +741,30 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
739
741
  }
740
742
  return null;
741
743
  }
744
+ var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
745
+ var init_dlp = __esm({
746
+ "src/dlp.ts"() {
747
+ "use strict";
748
+ DLP_PATTERNS = [
749
+ { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
750
+ { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
751
+ { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
752
+ { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
753
+ { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
754
+ {
755
+ name: "Private Key (PEM)",
756
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
757
+ severity: "block"
758
+ },
759
+ { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
760
+ ];
761
+ MAX_DEPTH = 5;
762
+ MAX_STRING_BYTES = 1e5;
763
+ MAX_JSON_PARSE_BYTES = 1e4;
764
+ }
765
+ });
742
766
 
743
767
  // 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
768
  function checkPause() {
749
769
  try {
750
770
  if (!import_fs2.default.existsSync(PAUSED_FILE)) return { paused: false };
@@ -857,9 +877,9 @@ function matchesPattern(text, patterns) {
857
877
  const withoutDotSlash = text.replace(/^\.\//, "");
858
878
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
859
879
  }
860
- function getNestedValue(obj, path9) {
880
+ function getNestedValue(obj, path10) {
861
881
  if (!obj || typeof obj !== "object") return null;
862
- return path9.split(".").reduce((prev, curr) => prev?.[curr], obj);
882
+ return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
863
883
  }
864
884
  function shouldSnapshot(toolName, args, config) {
865
885
  if (!config.settings.enableUndo) return false;
@@ -929,7 +949,6 @@ function isSqlTool(toolName, toolInspection) {
929
949
  const fieldName = toolInspection[matchingPattern];
930
950
  return fieldName === "sql" || fieldName === "query";
931
951
  }
932
- var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
933
952
  async function analyzeShellCommand(command) {
934
953
  const actions = [];
935
954
  const paths = [];
@@ -1011,208 +1030,6 @@ function redactSecrets(text) {
1011
1030
  );
1012
1031
  return redacted;
1013
1032
  }
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
1033
  function _resetConfigCache() {
1217
1034
  cachedConfig = null;
1218
1035
  }
@@ -1619,8 +1436,6 @@ function isIgnoredTool(toolName) {
1619
1436
  const config = getConfig();
1620
1437
  return matchesPattern(toolName, config.policy.ignoredTools);
1621
1438
  }
1622
- var DAEMON_PORT = 7391;
1623
- var DAEMON_HOST = "127.0.0.1";
1624
1439
  function isDaemonRunning() {
1625
1440
  try {
1626
1441
  const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
@@ -1644,7 +1459,7 @@ function getPersistentDecision(toolName) {
1644
1459
  }
1645
1460
  return null;
1646
1461
  }
1647
- async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1462
+ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
1648
1463
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1649
1464
  const checkCtrl = new AbortController();
1650
1465
  const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
@@ -1659,6 +1474,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1659
1474
  args,
1660
1475
  agent: meta?.agent,
1661
1476
  mcpServer: meta?.mcpServer,
1477
+ fromCLI: true,
1478
+ // Pass the flight-recorder ID so the daemon uses the same UUID for
1479
+ // activity-result as the CLI used for the pending activity event.
1480
+ // Without this, the two UUIDs never match and tail.ts never resolves
1481
+ // the pending item.
1482
+ activityId,
1662
1483
  ...riskMetadata && { riskMetadata }
1663
1484
  }),
1664
1485
  signal: checkCtrl.signal
@@ -1713,7 +1534,44 @@ async function resolveViaDaemon(id, decision, internalToken) {
1713
1534
  signal: AbortSignal.timeout(3e3)
1714
1535
  });
1715
1536
  }
1537
+ function notifyActivity(data) {
1538
+ return new Promise((resolve) => {
1539
+ try {
1540
+ const payload = JSON.stringify(data);
1541
+ const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
1542
+ sock.on("connect", () => {
1543
+ sock.on("close", resolve);
1544
+ sock.end(payload);
1545
+ });
1546
+ sock.on("error", resolve);
1547
+ } catch {
1548
+ resolve();
1549
+ }
1550
+ });
1551
+ }
1716
1552
  async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
1553
+ if (!options?.calledFromDaemon) {
1554
+ const actId = (0, import_crypto2.randomUUID)();
1555
+ const actTs = Date.now();
1556
+ await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
1557
+ const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
1558
+ ...options,
1559
+ activityId: actId
1560
+ });
1561
+ if (!result.noApprovalMechanism) {
1562
+ await notifyActivity({
1563
+ id: actId,
1564
+ tool: toolName,
1565
+ ts: actTs,
1566
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
1567
+ label: result.blockedByLabel
1568
+ });
1569
+ }
1570
+ return result;
1571
+ }
1572
+ return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
1573
+ }
1574
+ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
1717
1575
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1718
1576
  const pauseState = checkPause();
1719
1577
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
@@ -1749,6 +1607,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1749
1607
  blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
1750
1608
  };
1751
1609
  }
1610
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
1752
1611
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
1753
1612
  }
1754
1613
  }
@@ -1971,9 +1830,16 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1971
1830
  console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
1972
1831
  `));
1973
1832
  }
1974
- const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
1975
- if (daemonDecision === "abandoned") throw new Error("Abandoned");
1976
- const isApproved = daemonDecision === "allow";
1833
+ const daemonDecision = await askDaemon(
1834
+ toolName,
1835
+ args,
1836
+ meta,
1837
+ signal,
1838
+ riskMetadata,
1839
+ options?.activityId
1840
+ );
1841
+ if (daemonDecision === "abandoned") throw new Error("Abandoned");
1842
+ const isApproved = daemonDecision === "allow";
1977
1843
  return {
1978
1844
  approved: isApproved,
1979
1845
  reason: isApproved ? void 0 : "The human user rejected this action via the Node9 Browser Dashboard.",
@@ -2175,7 +2041,10 @@ function getConfig() {
2175
2041
  for (const rule of shield.smartRules) {
2176
2042
  if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
2177
2043
  }
2178
- for (const word of shield.dangerousWords) mergedPolicy.dangerousWords.push(word);
2044
+ const existingWords = new Set(mergedPolicy.dangerousWords);
2045
+ for (const word of shield.dangerousWords) {
2046
+ if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
2047
+ }
2179
2048
  }
2180
2049
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
2181
2050
  for (const rule of ADVISORY_SMART_RULES) {
@@ -2376,280 +2245,272 @@ async function resolveNode9SaaS(requestId, creds, approved) {
2376
2245
  } catch {
2377
2246
  }
2378
2247
  }
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 };
2248
+ var import_chalk2, import_prompts, import_fs2, import_path4, import_os2, import_net, import_crypto2, import_picomatch, import_sh_syntax, PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
2249
+ var init_core = __esm({
2250
+ "src/core.ts"() {
2251
+ "use strict";
2252
+ import_chalk2 = __toESM(require("chalk"));
2253
+ import_prompts = require("@inquirer/prompts");
2254
+ import_fs2 = __toESM(require("fs"));
2255
+ import_path4 = __toESM(require("path"));
2256
+ import_os2 = __toESM(require("os"));
2257
+ import_net = __toESM(require("net"));
2258
+ import_crypto2 = require("crypto");
2259
+ import_picomatch = __toESM(require("picomatch"));
2260
+ import_sh_syntax = require("sh-syntax");
2261
+ init_native();
2262
+ init_context_sniper();
2263
+ init_config_schema();
2264
+ init_shields();
2265
+ init_dlp();
2266
+ PAUSED_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
2267
+ TRUST_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "trust.json");
2268
+ LOCAL_AUDIT_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "audit.log");
2269
+ HOOK_DEBUG_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
2270
+ SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2271
+ DANGEROUS_WORDS = [
2272
+ "mkfs",
2273
+ // formats/wipes a filesystem partition
2274
+ "shred"
2275
+ // permanently overwrites file contents (unrecoverable)
2276
+ ];
2277
+ DEFAULT_CONFIG = {
2278
+ settings: {
2279
+ mode: "standard",
2280
+ autoStartDaemon: true,
2281
+ enableUndo: true,
2282
+ // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
2283
+ enableHookLogDebug: false,
2284
+ approvalTimeoutMs: 0,
2285
+ // 0 = disabled; set e.g. 30000 for 30-second auto-deny
2286
+ approvers: { native: true, browser: true, cloud: true, terminal: true }
2287
+ },
2288
+ policy: {
2289
+ sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
2290
+ dangerousWords: DANGEROUS_WORDS,
2291
+ ignoredTools: [
2292
+ "list_*",
2293
+ "get_*",
2294
+ "read_*",
2295
+ "describe_*",
2296
+ "read",
2297
+ "glob",
2298
+ "grep",
2299
+ "ls",
2300
+ "notebookread",
2301
+ "notebookedit",
2302
+ "webfetch",
2303
+ "websearch",
2304
+ "exitplanmode",
2305
+ "askuserquestion",
2306
+ "agent",
2307
+ "task*",
2308
+ "toolsearch",
2309
+ "mcp__ide__*",
2310
+ "getDiagnostics"
2311
+ ],
2312
+ toolInspection: {
2313
+ bash: "command",
2314
+ shell: "command",
2315
+ run_shell_command: "command",
2316
+ "terminal.execute": "command",
2317
+ "postgres:query": "sql"
2318
+ },
2319
+ snapshot: {
2320
+ tools: [
2321
+ "str_replace_based_edit_tool",
2322
+ "write_file",
2323
+ "edit_file",
2324
+ "create_file",
2325
+ "edit",
2326
+ "replace"
2327
+ ],
2328
+ onlyPaths: [],
2329
+ ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
2330
+ },
2331
+ smartRules: [
2332
+ // ── rm safety (critical always evaluated first) ──────────────────────
2333
+ {
2334
+ name: "block-rm-rf-home",
2335
+ tool: "bash",
2336
+ conditionMode: "all",
2337
+ conditions: [
2338
+ {
2339
+ field: "command",
2340
+ op: "matches",
2341
+ value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
2342
+ },
2343
+ {
2344
+ field: "command",
2345
+ op: "matches",
2346
+ value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
2347
+ }
2348
+ ],
2349
+ verdict: "block",
2350
+ reason: "Recursive delete of home directory is irreversible"
2351
+ },
2352
+ // ── SQL safety ────────────────────────────────────────────────────────
2353
+ {
2354
+ name: "no-delete-without-where",
2355
+ tool: "*",
2356
+ conditions: [
2357
+ { field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
2358
+ { field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
2359
+ ],
2360
+ conditionMode: "all",
2361
+ verdict: "review",
2362
+ reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
2363
+ },
2364
+ {
2365
+ name: "review-drop-truncate-shell",
2366
+ tool: "bash",
2367
+ conditions: [
2368
+ {
2369
+ field: "command",
2370
+ op: "matches",
2371
+ value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
2372
+ flags: "i"
2373
+ }
2374
+ ],
2375
+ conditionMode: "all",
2376
+ verdict: "review",
2377
+ reason: "SQL DDL destructive statement inside a shell command"
2378
+ },
2379
+ // ── Git safety ────────────────────────────────────────────────────────
2380
+ {
2381
+ name: "block-force-push",
2382
+ tool: "bash",
2383
+ conditions: [
2384
+ {
2385
+ field: "command",
2386
+ op: "matches",
2387
+ value: "git push.*(--force|--force-with-lease|-f\\b)",
2388
+ flags: "i"
2389
+ }
2390
+ ],
2391
+ conditionMode: "all",
2392
+ verdict: "block",
2393
+ reason: "Force push overwrites remote history and cannot be undone"
2394
+ },
2395
+ {
2396
+ name: "review-git-push",
2397
+ tool: "bash",
2398
+ conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
2399
+ conditionMode: "all",
2400
+ verdict: "review",
2401
+ reason: "git push sends changes to a shared remote"
2402
+ },
2403
+ {
2404
+ name: "review-git-destructive",
2405
+ tool: "bash",
2406
+ conditions: [
2407
+ {
2408
+ field: "command",
2409
+ op: "matches",
2410
+ value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
2411
+ flags: "i"
2412
+ }
2413
+ ],
2414
+ conditionMode: "all",
2415
+ verdict: "review",
2416
+ reason: "Destructive git operation \u2014 discards history or working-tree changes"
2417
+ },
2418
+ // ── Shell safety ──────────────────────────────────────────────────────
2419
+ {
2420
+ name: "review-sudo",
2421
+ tool: "bash",
2422
+ conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
2423
+ conditionMode: "all",
2424
+ verdict: "review",
2425
+ reason: "Command requires elevated privileges"
2426
+ },
2427
+ {
2428
+ name: "review-curl-pipe-shell",
2429
+ tool: "bash",
2430
+ conditions: [
2431
+ {
2432
+ field: "command",
2433
+ op: "matches",
2434
+ value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
2435
+ flags: "i"
2436
+ }
2437
+ ],
2438
+ conditionMode: "all",
2439
+ verdict: "block",
2440
+ reason: "Piping remote script into a shell is a supply-chain attack vector"
2441
+ }
2442
+ ],
2443
+ dlp: { enabled: true, scanIgnoredTools: true }
2444
+ },
2445
+ environments: {}
2446
+ };
2447
+ ADVISORY_SMART_RULES = [
2448
+ {
2449
+ name: "allow-rm-safe-paths",
2450
+ tool: "*",
2451
+ conditionMode: "all",
2452
+ conditions: [
2453
+ { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
2454
+ {
2455
+ field: "command",
2456
+ op: "matches",
2457
+ // Matches known-safe build artifact paths in the command.
2458
+ value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
2459
+ }
2460
+ ],
2461
+ verdict: "allow",
2462
+ reason: "Deleting a known-safe build artifact path"
2463
+ },
2464
+ {
2465
+ name: "review-rm",
2466
+ tool: "*",
2467
+ conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
2468
+ verdict: "review",
2469
+ reason: "rm can permanently delete files \u2014 confirm the target path"
2465
2470
  }
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 });
2471
+ ];
2472
+ cachedConfig = null;
2473
+ DAEMON_PORT = 7391;
2474
+ DAEMON_HOST = "127.0.0.1";
2475
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path4.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
2536
2476
  }
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;
2477
+ });
2478
+
2479
+ // src/daemon/ui.html
2480
+ var ui_default;
2481
+ var init_ui = __esm({
2482
+ "src/daemon/ui.html"() {
2483
+ ui_default = `<!doctype html>
2484
+ <html lang="en">
2485
+ <head>
2486
+ <meta charset="UTF-8" />
2487
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2488
+ <title>Node9 Security Guard</title>
2489
+ <style>
2490
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Fira+Code:wght@400;500&display=swap');
2491
+ :root {
2492
+ --bg: #0a0c10;
2493
+ --card: #1c2128;
2494
+ --panel: #161b22;
2495
+ --border: #30363d;
2496
+ --text: #adbac7;
2497
+ --text-bright: #cdd9e5;
2498
+ --muted: #768390;
2499
+ --primary: #f0883e;
2500
+ --success: #347d39;
2501
+ --danger: #c93c37;
2502
+ --accent: #539bf5;
2647
2503
  }
2648
2504
  * {
2649
2505
  box-sizing: border-box;
2650
2506
  margin: 0;
2651
2507
  padding: 0;
2652
2508
  }
2509
+ html,
2510
+ body {
2511
+ height: 100%;
2512
+ overflow: hidden;
2513
+ }
2653
2514
  body {
2654
2515
  background: var(--bg);
2655
2516
  color: var(--text);
@@ -2657,16 +2518,17 @@ var ui_default = `<!doctype html>
2657
2518
  'Inter',
2658
2519
  -apple-system,
2659
2520
  sans-serif;
2660
- min-height: 100vh;
2661
2521
  }
2662
2522
 
2663
2523
  .shell {
2664
- max-width: 1000px;
2524
+ max-width: 1440px;
2525
+ height: 100vh;
2665
2526
  margin: 0 auto;
2666
- padding: 32px 24px 48px;
2527
+ padding: 16px 20px 16px;
2667
2528
  display: grid;
2668
2529
  grid-template-rows: auto 1fr;
2669
- gap: 24px;
2530
+ gap: 16px;
2531
+ overflow: hidden;
2670
2532
  }
2671
2533
  header {
2672
2534
  display: flex;
@@ -2703,9 +2565,10 @@ var ui_default = `<!doctype html>
2703
2565
 
2704
2566
  .body {
2705
2567
  display: grid;
2706
- grid-template-columns: 1fr 272px;
2707
- gap: 20px;
2708
- align-items: start;
2568
+ grid-template-columns: 360px 1fr 270px;
2569
+ gap: 16px;
2570
+ min-height: 0;
2571
+ overflow: hidden;
2709
2572
  }
2710
2573
 
2711
2574
  .warning-banner {
@@ -2725,6 +2588,10 @@ var ui_default = `<!doctype html>
2725
2588
 
2726
2589
  .main {
2727
2590
  min-width: 0;
2591
+ min-height: 0;
2592
+ overflow-y: auto;
2593
+ scrollbar-width: thin;
2594
+ scrollbar-color: var(--border) transparent;
2728
2595
  }
2729
2596
  .section-title {
2730
2597
  font-size: 11px;
@@ -2755,14 +2622,64 @@ var ui_default = `<!doctype html>
2755
2622
  background: var(--card);
2756
2623
  border: 1px solid var(--border);
2757
2624
  border-radius: 14px;
2758
- padding: 24px;
2759
- margin-bottom: 16px;
2625
+ padding: 20px;
2626
+ margin-bottom: 14px;
2760
2627
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
2761
2628
  animation: pop 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
2762
2629
  }
2763
2630
  .card.slack-viewer {
2764
2631
  border-color: rgba(83, 155, 245, 0.3);
2765
2632
  }
2633
+ .card-header {
2634
+ display: flex;
2635
+ align-items: center;
2636
+ gap: 8px;
2637
+ margin-bottom: 12px;
2638
+ padding-bottom: 12px;
2639
+ border-bottom: 1px solid var(--border);
2640
+ }
2641
+ .card-header-icon {
2642
+ font-size: 16px;
2643
+ }
2644
+ .card-header-title {
2645
+ font-size: 12px;
2646
+ font-weight: 700;
2647
+ color: var(--text-bright);
2648
+ text-transform: uppercase;
2649
+ letter-spacing: 0.5px;
2650
+ }
2651
+ .card-timer {
2652
+ margin-left: auto;
2653
+ font-size: 11px;
2654
+ font-family: 'Fira Code', monospace;
2655
+ color: var(--muted);
2656
+ background: rgba(48, 54, 61, 0.6);
2657
+ padding: 2px 8px;
2658
+ border-radius: 5px;
2659
+ }
2660
+ .card-timer.urgent {
2661
+ color: var(--danger);
2662
+ background: rgba(201, 60, 55, 0.1);
2663
+ }
2664
+ .btn-allow {
2665
+ background: var(--success);
2666
+ color: #fff;
2667
+ grid-column: span 2;
2668
+ font-size: 14px;
2669
+ padding: 13px 14px;
2670
+ }
2671
+ .btn-deny {
2672
+ background: rgba(201, 60, 55, 0.15);
2673
+ color: #e5534b;
2674
+ border: 1px solid rgba(201, 60, 55, 0.3);
2675
+ grid-column: span 2;
2676
+ }
2677
+ .btn-deny:hover:not(:disabled) {
2678
+ background: var(--danger);
2679
+ color: #fff;
2680
+ border-color: transparent;
2681
+ filter: none;
2682
+ }
2766
2683
  @keyframes pop {
2767
2684
  from {
2768
2685
  opacity: 0;
@@ -2970,24 +2887,178 @@ var ui_default = `<!doctype html>
2970
2887
  cursor: not-allowed;
2971
2888
  }
2972
2889
 
2890
+ .flight-col {
2891
+ display: flex;
2892
+ flex-direction: column;
2893
+ min-height: 0;
2894
+ overflow: hidden;
2895
+ }
2896
+ .flight-panel {
2897
+ flex: 1;
2898
+ min-height: 0;
2899
+ display: flex;
2900
+ flex-direction: column;
2901
+ overflow: hidden;
2902
+ }
2973
2903
  .sidebar {
2974
2904
  display: flex;
2975
2905
  flex-direction: column;
2976
2906
  gap: 12px;
2977
- position: sticky;
2978
- top: 24px;
2907
+ min-height: 0;
2908
+ overflow-y: auto;
2909
+ scrollbar-width: thin;
2910
+ scrollbar-color: var(--border) transparent;
2979
2911
  }
2980
2912
  .panel {
2981
2913
  background: var(--panel);
2982
2914
  border: 1px solid var(--border);
2983
2915
  border-radius: 12px;
2984
- padding: 16px;
2916
+ padding: 14px;
2985
2917
  }
2918
+ /* \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2919
+ #activity-feed {
2920
+ display: flex;
2921
+ flex-direction: column;
2922
+ gap: 4px;
2923
+ margin-top: 4px;
2924
+ flex: 1;
2925
+ min-height: 0;
2926
+ overflow-y: auto;
2927
+ scrollbar-width: thin;
2928
+ scrollbar-color: var(--border) transparent;
2929
+ }
2930
+ .feed-row {
2931
+ display: grid;
2932
+ grid-template-columns: 58px 20px 1fr 48px;
2933
+ align-items: start;
2934
+ gap: 6px;
2935
+ background: rgba(22, 27, 34, 0.6);
2936
+ border: 1px solid var(--border);
2937
+ padding: 7px 10px;
2938
+ border-radius: 7px;
2939
+ font-size: 11px;
2940
+ animation: frSlideIn 0.15s ease-out;
2941
+ transition: background 0.1s;
2942
+ cursor: default;
2943
+ }
2944
+ .feed-row:hover {
2945
+ background: rgba(30, 38, 48, 0.9);
2946
+ border-color: rgba(83, 155, 245, 0.2);
2947
+ }
2948
+ @keyframes frSlideIn {
2949
+ from {
2950
+ opacity: 0;
2951
+ transform: translateX(-4px);
2952
+ }
2953
+ to {
2954
+ opacity: 1;
2955
+ transform: none;
2956
+ }
2957
+ }
2958
+ .feed-ts {
2959
+ color: var(--muted);
2960
+ font-family: monospace;
2961
+ font-size: 9px;
2962
+ }
2963
+ .feed-icon {
2964
+ text-align: center;
2965
+ font-size: 13px;
2966
+ }
2967
+ .feed-content {
2968
+ min-width: 0;
2969
+ color: var(--text-bright);
2970
+ word-break: break-all;
2971
+ }
2972
+ .feed-args {
2973
+ display: block;
2974
+ color: var(--muted);
2975
+ font-family: monospace;
2976
+ margin-top: 2px;
2977
+ font-size: 10px;
2978
+ word-break: break-all;
2979
+ }
2980
+ .feed-badge {
2981
+ text-align: right;
2982
+ font-weight: 700;
2983
+ font-size: 9px;
2984
+ letter-spacing: 0.03em;
2985
+ }
2986
+ .fr-pending {
2987
+ color: var(--muted);
2988
+ }
2989
+ .fr-allow {
2990
+ color: #57ab5a;
2991
+ }
2992
+ .fr-block {
2993
+ color: var(--danger);
2994
+ }
2995
+ .fr-dlp {
2996
+ color: var(--primary);
2997
+ animation: frBlink 1s infinite;
2998
+ }
2999
+ @keyframes frBlink {
3000
+ 50% {
3001
+ opacity: 0.4;
3002
+ }
3003
+ }
3004
+ .fr-dlp-row {
3005
+ border-color: var(--primary) !important;
3006
+ }
3007
+ .feed-clear-btn {
3008
+ background: transparent;
3009
+ border: none;
3010
+ color: var(--muted);
3011
+ font-size: 10px;
3012
+ padding: 0;
3013
+ cursor: pointer;
3014
+ margin-left: auto;
3015
+ font-family: inherit;
3016
+ font-weight: 500;
3017
+ transition: color 0.15s;
3018
+ }
3019
+ .feed-clear-btn:hover {
3020
+ color: var(--text);
3021
+ filter: none;
3022
+ transform: none;
3023
+ }
3024
+ /* \u2500\u2500 Shields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
3025
+ .shield-row {
3026
+ display: flex;
3027
+ align-items: flex-start;
3028
+ gap: 10px;
3029
+ padding: 8px 0;
3030
+ border-bottom: 1px solid var(--border);
3031
+ }
3032
+ .shield-row:last-child {
3033
+ border-bottom: none;
3034
+ padding-bottom: 0;
3035
+ }
3036
+ .shield-row:first-child {
3037
+ padding-top: 0;
3038
+ }
3039
+ .shield-info {
3040
+ flex: 1;
3041
+ min-width: 0;
3042
+ }
3043
+ .shield-name {
3044
+ font-size: 12px;
3045
+ color: var(--text-bright);
3046
+ font-weight: 600;
3047
+ font-family: 'Fira Code', monospace;
3048
+ }
3049
+ .shield-desc {
3050
+ font-size: 10px;
3051
+ color: var(--muted);
3052
+ margin-top: 2px;
3053
+ line-height: 1.4;
3054
+ }
3055
+
2986
3056
  .panel-title {
2987
3057
  font-size: 12px;
2988
3058
  font-weight: 700;
2989
3059
  color: var(--text-bright);
2990
3060
  margin-bottom: 12px;
3061
+ flex-shrink: 0;
2991
3062
  display: flex;
2992
3063
  align-items: center;
2993
3064
  gap: 6px;
@@ -2995,8 +3066,8 @@ var ui_default = `<!doctype html>
2995
3066
  .setting-row {
2996
3067
  display: flex;
2997
3068
  align-items: flex-start;
2998
- gap: 12px;
2999
- margin-bottom: 12px;
3069
+ gap: 10px;
3070
+ margin-bottom: 8px;
3000
3071
  }
3001
3072
  .setting-row:last-child {
3002
3073
  margin-bottom: 0;
@@ -3005,20 +3076,21 @@ var ui_default = `<!doctype html>
3005
3076
  flex: 1;
3006
3077
  }
3007
3078
  .setting-label {
3008
- font-size: 12px;
3079
+ font-size: 11px;
3009
3080
  color: var(--text-bright);
3010
- margin-bottom: 3px;
3081
+ margin-bottom: 2px;
3082
+ font-weight: 600;
3011
3083
  }
3012
3084
  .setting-desc {
3013
- font-size: 11px;
3085
+ font-size: 10px;
3014
3086
  color: var(--muted);
3015
- line-height: 1.5;
3087
+ line-height: 1.4;
3016
3088
  }
3017
3089
  .toggle {
3018
3090
  position: relative;
3019
3091
  display: inline-block;
3020
- width: 40px;
3021
- height: 22px;
3092
+ width: 34px;
3093
+ height: 19px;
3022
3094
  flex-shrink: 0;
3023
3095
  margin-top: 1px;
3024
3096
  }
@@ -3038,8 +3110,8 @@ var ui_default = `<!doctype html>
3038
3110
  .slider:before {
3039
3111
  content: '';
3040
3112
  position: absolute;
3041
- width: 16px;
3042
- height: 16px;
3113
+ width: 13px;
3114
+ height: 13px;
3043
3115
  left: 3px;
3044
3116
  bottom: 3px;
3045
3117
  background: #fff;
@@ -3050,7 +3122,7 @@ var ui_default = `<!doctype html>
3050
3122
  background: var(--success);
3051
3123
  }
3052
3124
  input:checked + .slider:before {
3053
- transform: translateX(18px);
3125
+ transform: translateX(15px);
3054
3126
  }
3055
3127
  input:disabled + .slider {
3056
3128
  opacity: 0.4;
@@ -3209,12 +3281,17 @@ var ui_default = `<!doctype html>
3209
3281
  border: 1px solid var(--border);
3210
3282
  }
3211
3283
 
3212
- @media (max-width: 680px) {
3284
+ @media (max-width: 960px) {
3213
3285
  .body {
3214
- grid-template-columns: 1fr;
3286
+ grid-template-columns: 1fr 220px;
3215
3287
  }
3216
- .sidebar {
3217
- position: static;
3288
+ .flight-col {
3289
+ display: none;
3290
+ }
3291
+ }
3292
+ @media (max-width: 640px) {
3293
+ .body {
3294
+ grid-template-columns: 1fr;
3218
3295
  }
3219
3296
  }
3220
3297
  </style>
@@ -3228,6 +3305,19 @@ var ui_default = `<!doctype html>
3228
3305
  </header>
3229
3306
 
3230
3307
  <div class="body">
3308
+ <div class="flight-col">
3309
+ <div class="panel flight-panel">
3310
+ <div class="panel-title">
3311
+ \u{1F6F0}\uFE0F Flight Recorder
3312
+ <span style="font-weight: 400; color: var(--muted); font-size: 11px">live</span>
3313
+ <button class="feed-clear-btn" onclick="clearFeed()">clear</button>
3314
+ </div>
3315
+ <div id="activity-feed">
3316
+ <span class="decisions-empty">Waiting for agent activity\u2026</span>
3317
+ </div>
3318
+ </div>
3319
+ </div>
3320
+
3231
3321
  <div class="main">
3232
3322
  <div id="warnBanner" class="warning-banner">
3233
3323
  \u26A0\uFE0F Auto-start is off \u2014 daemon started manually. Run "node9 daemon stop" to stop it, or
@@ -3308,6 +3398,11 @@ var ui_default = `<!doctype html>
3308
3398
  <div id="slackStatusLine" class="slack-status-line">No key saved</div>
3309
3399
  </div>
3310
3400
 
3401
+ <div class="panel">
3402
+ <div class="panel-title">\u{1F6E1}\uFE0F Active Shields</div>
3403
+ <div id="shieldsList"><span class="decisions-empty">Loading\u2026</span></div>
3404
+ </div>
3405
+
3311
3406
  <div class="panel">
3312
3407
  <div class="panel-title">\u{1F4CB} Persistent Decisions</div>
3313
3408
  <div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
@@ -3353,14 +3448,23 @@ var ui_default = `<!doctype html>
3353
3448
 
3354
3449
  function updateDenyButton(id, timestamp) {
3355
3450
  const btn = document.querySelector('#c-' + id + ' .btn-deny');
3451
+ const timer = document.querySelector('#timer-' + id);
3356
3452
  if (!btn) return;
3357
3453
  const elapsed = Date.now() - timestamp;
3358
3454
  const remaining = Math.max(0, Math.ceil((autoDenyMs - elapsed) / 1000));
3359
3455
  if (remaining <= 0) {
3360
- btn.textContent = 'Auto-Denying...';
3456
+ btn.textContent = '\u23F3 Auto-Denying\u2026';
3361
3457
  btn.disabled = true;
3458
+ if (timer) {
3459
+ timer.textContent = 'auto-deny';
3460
+ timer.className = 'card-timer urgent';
3461
+ }
3362
3462
  } else {
3363
- btn.textContent = 'Block Action (' + remaining + 's)';
3463
+ btn.textContent = '\u{1F6AB} Block this Action';
3464
+ if (timer) {
3465
+ timer.textContent = remaining + 's';
3466
+ timer.className = 'card-timer' + (remaining < 15 ? ' urgent' : '');
3467
+ }
3364
3468
  setTimeout(() => updateDenyButton(id, timestamp), 1000);
3365
3469
  }
3366
3470
  }
@@ -3376,34 +3480,61 @@ var ui_default = `<!doctype html>
3376
3480
  empty.style.display = requests.size === 0 ? 'block' : 'none';
3377
3481
  }
3378
3482
 
3379
- function sendDecision(id, decision, persist) {
3483
+ function setCardBusy(card, busy) {
3484
+ if (!card) return;
3485
+ card.querySelectorAll('button').forEach((b) => (b.disabled = busy));
3486
+ card.style.opacity = busy ? '0.5' : '1';
3487
+ }
3488
+
3489
+ function showCardError(card, msg) {
3490
+ if (!card) return;
3491
+ card.style.outline = '2px solid #f87171';
3492
+ let err = card.querySelector('.card-error');
3493
+ if (!err) {
3494
+ err = document.createElement('p');
3495
+ err.className = 'card-error';
3496
+ err.style.cssText = 'color:#f87171;font-size:11px;margin:6px 0 0;';
3497
+ card.appendChild(err);
3498
+ }
3499
+ err.textContent = '\u26A0 ' + msg + ' \u2014 please try again or refresh.';
3500
+ }
3501
+
3502
+ async function sendDecision(id, decision, persist) {
3380
3503
  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(() => {
3504
+ setCardBusy(card, true);
3505
+ try {
3506
+ const res = await fetch('/decision/' + id, {
3507
+ method: 'POST',
3508
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3509
+ body: JSON.stringify({ decision, persist: !!persist }),
3510
+ });
3511
+ if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3388
3512
  card?.remove();
3389
3513
  requests.delete(id);
3390
3514
  refresh();
3391
- }, 200);
3515
+ } catch (err) {
3516
+ setCardBusy(card, false);
3517
+ showCardError(card, err.message || 'Network error');
3518
+ }
3392
3519
  }
3393
3520
 
3394
- function sendTrust(id, duration) {
3521
+ async function sendTrust(id, duration) {
3395
3522
  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(() => {
3523
+ setCardBusy(card, true);
3524
+ try {
3525
+ const res = await fetch('/decision/' + id, {
3526
+ method: 'POST',
3527
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3528
+ body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
3529
+ });
3530
+ if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3403
3531
  card?.remove();
3404
3532
  requests.delete(id);
3405
3533
  refresh();
3406
- }, 200);
3534
+ } catch (err) {
3535
+ setCardBusy(card, false);
3536
+ showCardError(card, err.message || 'Network error');
3537
+ }
3407
3538
  }
3408
3539
 
3409
3540
  function renderPayload(req) {
@@ -3454,16 +3585,21 @@ var ui_default = `<!doctype html>
3454
3585
  const mcpLabel = req.mcpServer ? esc(req.mcpServer) : null;
3455
3586
  const dis = isSlack ? 'disabled' : '';
3456
3587
  card.innerHTML = \`
3588
+ <div class="card-header">
3589
+ <span class="card-header-icon">\${isSlack ? '\u26A1' : '\u26A0\uFE0F'}</span>
3590
+ <span class="card-header-title">\${isSlack ? 'Awaiting Cloud Approval' : 'Action Required'}</span>
3591
+ <span class="card-timer" id="timer-\${req.id}">\${autoDenyMs > 0 ? Math.ceil(autoDenyMs / 1000) + 's' : ''}</span>
3592
+ </div>
3457
3593
  <div class="source-row">
3458
3594
  <span class="agent-badge">\${agentLabel}</span>
3459
3595
  \${mcpLabel ? \`<span class="source-arrow">\u2192</span><span class="mcp-badge">mcp::\${mcpLabel}</span>\` : ''}
3460
3596
  </div>
3461
3597
  <div class="tool-chip">\${esc(req.toolName)}</div>
3462
- \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Slack approval \u2014 view only</div>' : ''}
3598
+ \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
3463
3599
  \${renderPayload(req)}
3464
3600
  <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>
3601
+ <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
3602
+ <button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>\u{1F6AB} Block this Action</button>
3467
3603
  <div class="trust-row\${trustEnabled ? ' show' : ''}" id="tr-\${req.id}">
3468
3604
  <button class="btn-trust" onclick="sendTrust('\${req.id}','30m')" \${dis}>\u23F1 Trust 30m</button>
3469
3605
  <button class="btn-trust" onclick="sendTrust('\${req.id}','1h')" \${dis}>\u23F1 Trust 1h</button>
@@ -3523,8 +3659,83 @@ var ui_default = `<!doctype html>
3523
3659
  ev.addEventListener('slack-status', (e) => {
3524
3660
  applySlackStatus(JSON.parse(e.data));
3525
3661
  });
3526
- }
3527
- connect();
3662
+ ev.addEventListener('shields-status', (e) => {
3663
+ renderShields(JSON.parse(e.data).shields);
3664
+ });
3665
+
3666
+ // \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3667
+ ev.addEventListener('activity', (e) => {
3668
+ const data = JSON.parse(e.data);
3669
+ const feed = document.getElementById('activity-feed');
3670
+ // Remove placeholder on first item
3671
+ const placeholder = feed.querySelector('.decisions-empty');
3672
+ if (placeholder) placeholder.remove();
3673
+
3674
+ const time = new Date(data.ts).toLocaleTimeString([], {
3675
+ hour12: false,
3676
+ hour: '2-digit',
3677
+ minute: '2-digit',
3678
+ second: '2-digit',
3679
+ });
3680
+ const icon = frIcon(data.tool);
3681
+ const argsStr = JSON.stringify(data.args ?? {});
3682
+ const argsPreview = esc(argsStr.length > 120 ? argsStr.slice(0, 120) + '\u2026' : argsStr);
3683
+
3684
+ const row = document.createElement('div');
3685
+ row.className = 'feed-row';
3686
+ row.id = 'fr-' + data.id;
3687
+ row.innerHTML = \`
3688
+ <span class="feed-ts">\${time}</span>
3689
+ <span class="feed-icon">\${icon}</span>
3690
+ <span class="feed-content"><strong>\${esc(data.tool)}</strong><span class="feed-args">\${argsPreview}</span></span>
3691
+ <span class="feed-badge fr-pending">\u25CF</span>
3692
+ \`;
3693
+ feed.prepend(row);
3694
+ if (feed.children.length > 100) feed.lastChild.remove();
3695
+ });
3696
+
3697
+ ev.addEventListener('activity-result', (e) => {
3698
+ const { id, status, label } = JSON.parse(e.data);
3699
+ const row = document.getElementById('fr-' + id);
3700
+ if (!row) return;
3701
+ const badge = row.querySelector('.feed-badge');
3702
+ if (status === 'allow') {
3703
+ badge.textContent = 'ALLOW';
3704
+ badge.className = 'feed-badge fr-allow';
3705
+ } else if (status === 'dlp') {
3706
+ badge.textContent = '\u{1F6E1}\uFE0F DLP';
3707
+ badge.className = 'feed-badge fr-dlp';
3708
+ row.classList.add('fr-dlp-row');
3709
+ } else {
3710
+ badge.textContent = 'BLOCK';
3711
+ badge.className = 'feed-badge fr-block';
3712
+ }
3713
+ });
3714
+ }
3715
+ connect();
3716
+
3717
+ const FR_ICONS = {
3718
+ bash: '\u{1F4BB}',
3719
+ read: '\u{1F4D6}',
3720
+ edit: '\u270F\uFE0F',
3721
+ write: '\u270F\uFE0F',
3722
+ glob: '\u{1F4C2}',
3723
+ grep: '\u{1F50D}',
3724
+ agent: '\u{1F916}',
3725
+ search: '\u{1F50D}',
3726
+ sql: '\u{1F5C4}\uFE0F',
3727
+ query: '\u{1F5C4}\uFE0F',
3728
+ list: '\u{1F4C2}',
3729
+ delete: '\u{1F5D1}\uFE0F',
3730
+ web: '\u{1F310}',
3731
+ };
3732
+ function frIcon(tool) {
3733
+ const t = (tool || '').toLowerCase();
3734
+ for (const [k, v] of Object.entries(FR_ICONS)) {
3735
+ if (t.includes(k)) return v;
3736
+ }
3737
+ return '\u{1F6E0}\uFE0F';
3738
+ }
3528
3739
 
3529
3740
  function saveSetting(key, value) {
3530
3741
  fetch('/settings', {
@@ -3615,6 +3826,49 @@ var ui_default = `<!doctype html>
3615
3826
  }
3616
3827
  }
3617
3828
 
3829
+ function clearFeed() {
3830
+ const feed = document.getElementById('activity-feed');
3831
+ feed.innerHTML = '<span class="decisions-empty">Feed cleared.</span>';
3832
+ }
3833
+
3834
+ function renderShields(shields) {
3835
+ const list = document.getElementById('shieldsList');
3836
+ if (!shields || shields.length === 0) {
3837
+ list.innerHTML = '<span class="decisions-empty">No shields available.</span>';
3838
+ return;
3839
+ }
3840
+ list.innerHTML = shields
3841
+ .map(
3842
+ (s) => \`
3843
+ <div class="shield-row">
3844
+ <div class="shield-info">
3845
+ <div class="shield-name">\${esc(s.name)}</div>
3846
+ <div class="shield-desc">\${esc(s.description)}</div>
3847
+ </div>
3848
+ <label class="toggle">
3849
+ <input type="checkbox" \${s.active ? 'checked' : ''}
3850
+ onchange="toggleShield('\${esc(s.name)}', this.checked)" />
3851
+ <span class="slider"></span>
3852
+ </label>
3853
+ </div>
3854
+ \`
3855
+ )
3856
+ .join('');
3857
+ }
3858
+
3859
+ function toggleShield(name, active) {
3860
+ fetch('/shields', {
3861
+ method: 'POST',
3862
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3863
+ body: JSON.stringify({ name, active }),
3864
+ }).catch(() => {});
3865
+ }
3866
+
3867
+ fetch('/shields')
3868
+ .then((r) => r.json())
3869
+ .then(({ shields }) => renderShields(shields))
3870
+ .catch(() => {});
3871
+
3618
3872
  function renderDecisions(decisions) {
3619
3873
  const dl = document.getElementById('decisionsList');
3620
3874
  const entries = Object.entries(decisions);
@@ -3661,31 +3915,24 @@ var ui_default = `<!doctype html>
3661
3915
  </body>
3662
3916
  </html>
3663
3917
  `;
3918
+ }
3919
+ });
3664
3920
 
3665
3921
  // src/daemon/ui.ts
3666
- var UI_HTML_TEMPLATE = ui_default;
3922
+ var UI_HTML_TEMPLATE;
3923
+ var init_ui2 = __esm({
3924
+ "src/daemon/ui.ts"() {
3925
+ "use strict";
3926
+ init_ui();
3927
+ UI_HTML_TEMPLATE = ui_default;
3928
+ }
3929
+ });
3667
3930
 
3668
3931
  // 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
3932
  function atomicWriteSync2(filePath, data, options) {
3686
3933
  const dir = import_path6.default.dirname(filePath);
3687
3934
  if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
3688
- const tmpPath = `${filePath}.${(0, import_crypto2.randomUUID)()}.tmp`;
3935
+ const tmpPath = `${filePath}.${(0, import_crypto3.randomUUID)()}.tmp`;
3689
3936
  import_fs4.default.writeFileSync(tmpPath, data, options);
3690
3937
  import_fs4.default.renameSync(tmpPath, filePath);
3691
3938
  }
@@ -3703,12 +3950,6 @@ function writeTrustEntry(toolName, durationMs) {
3703
3950
  } catch {
3704
3951
  }
3705
3952
  }
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
3953
  function redactArgs(value) {
3713
3954
  if (!value || typeof value !== "object") return value;
3714
3955
  if (Array.isArray(value)) return value.map(redactArgs);
@@ -3743,7 +3984,6 @@ function getAuditHistory(limit = 20) {
3743
3984
  return [];
3744
3985
  }
3745
3986
  }
3746
- var AUTO_DENY_MS = 12e4;
3747
3987
  function getOrgName() {
3748
3988
  try {
3749
3989
  if (import_fs4.default.existsSync(CREDENTIALS_FILE)) {
@@ -3753,7 +3993,6 @@ function getOrgName() {
3753
3993
  }
3754
3994
  return null;
3755
3995
  }
3756
- var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
3757
3996
  function hasStoredSlackKey() {
3758
3997
  return import_fs4.default.existsSync(CREDENTIALS_FILE);
3759
3998
  }
@@ -3769,11 +4008,6 @@ function writeGlobalSetting(key, value) {
3769
4008
  config.settings[key] = value;
3770
4009
  atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
3771
4010
  }
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
4011
  function abandonPending() {
3778
4012
  abandonTimer = null;
3779
4013
  pending.forEach((entry, id) => {
@@ -3795,6 +4029,18 @@ function abandonPending() {
3795
4029
  }
3796
4030
  }
3797
4031
  function broadcast(event, data) {
4032
+ if (event === "activity") {
4033
+ activityRing.push({ event, data });
4034
+ if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
4035
+ } else if (event === "activity-result") {
4036
+ const { id, status, label } = data;
4037
+ for (let i = activityRing.length - 1; i >= 0; i--) {
4038
+ if (activityRing[i].data.id === id) {
4039
+ Object.assign(activityRing[i].data, { status, label });
4040
+ break;
4041
+ }
4042
+ }
4043
+ }
3798
4044
  const msg = `event: ${event}
3799
4045
  data: ${JSON.stringify(data)}
3800
4046
 
@@ -3840,13 +4086,15 @@ function writePersistentDecision(toolName, decision) {
3840
4086
  }
3841
4087
  }
3842
4088
  function startDaemon() {
3843
- const csrfToken = (0, import_crypto2.randomUUID)();
3844
- const internalToken = (0, import_crypto2.randomUUID)();
4089
+ const csrfToken = (0, import_crypto3.randomUUID)();
4090
+ const internalToken = (0, import_crypto3.randomUUID)();
3845
4091
  const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
3846
4092
  const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
3847
4093
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
4094
+ const watchMode = process.env.NODE9_WATCH_MODE === "1";
3848
4095
  let idleTimer;
3849
4096
  function resetIdleTimer() {
4097
+ if (watchMode) return;
3850
4098
  if (idleTimer) clearTimeout(idleTimer);
3851
4099
  idleTimer = setTimeout(() => {
3852
4100
  if (autoStarted) {
@@ -3901,6 +4149,12 @@ data: ${JSON.stringify({
3901
4149
  data: ${JSON.stringify(readPersistentDecisions())}
3902
4150
 
3903
4151
  `);
4152
+ for (const item of activityRing) {
4153
+ res.write(`event: ${item.event}
4154
+ data: ${JSON.stringify(item.data)}
4155
+
4156
+ `);
4157
+ }
3904
4158
  return req.on("close", () => {
3905
4159
  sseClients.delete(res);
3906
4160
  if (sseClients.size === 0 && pending.size > 0) {
@@ -3920,9 +4174,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
3920
4174
  slackDelegated = false,
3921
4175
  agent,
3922
4176
  mcpServer,
3923
- riskMetadata
4177
+ riskMetadata,
4178
+ fromCLI = false,
4179
+ activityId
3924
4180
  } = JSON.parse(body);
3925
- const id = (0, import_crypto2.randomUUID)();
4181
+ const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto3.randomUUID)();
3926
4182
  const entry = {
3927
4183
  id,
3928
4184
  toolName,
@@ -3953,6 +4209,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
3953
4209
  }, AUTO_DENY_MS)
3954
4210
  };
3955
4211
  pending.set(id, entry);
4212
+ if (!fromCLI) {
4213
+ broadcast("activity", {
4214
+ id,
4215
+ ts: entry.timestamp,
4216
+ tool: toolName,
4217
+ args: redactArgs(args),
4218
+ status: "pending"
4219
+ });
4220
+ }
3956
4221
  const browserEnabled = getConfig().settings.approvers?.browser !== false;
3957
4222
  if (browserEnabled) {
3958
4223
  broadcast("add", {
@@ -3982,6 +4247,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
3982
4247
  const e = pending.get(id);
3983
4248
  if (!e) return;
3984
4249
  if (result.noApprovalMechanism) return;
4250
+ broadcast("activity-result", {
4251
+ id,
4252
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
4253
+ label: result.blockedByLabel
4254
+ });
3985
4255
  clearTimeout(e.timer);
3986
4256
  const decision = result.approved ? "allow" : "deny";
3987
4257
  appendAuditLog({ toolName: e.toolName, args: e.args, decision });
@@ -4016,8 +4286,8 @@ data: ${JSON.stringify(readPersistentDecisions())}
4016
4286
  const entry = pending.get(id);
4017
4287
  if (!entry) return res.writeHead(404).end();
4018
4288
  if (entry.earlyDecision) {
4289
+ clearTimeout(entry.timer);
4019
4290
  pending.delete(id);
4020
- broadcast("remove", { id });
4021
4291
  res.writeHead(200, { "Content-Type": "application/json" });
4022
4292
  const body = { decision: entry.earlyDecision };
4023
4293
  if (entry.earlyReason) body.reason = entry.earlyReason;
@@ -4047,10 +4317,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
4047
4317
  decision: `trust:${trustDuration}`
4048
4318
  });
4049
4319
  clearTimeout(entry.timer);
4050
- if (entry.waiter) entry.waiter("allow");
4051
- else entry.earlyDecision = "allow";
4052
- pending.delete(id);
4053
- broadcast("remove", { id });
4320
+ if (entry.waiter) {
4321
+ entry.waiter("allow");
4322
+ pending.delete(id);
4323
+ broadcast("remove", { id });
4324
+ } else {
4325
+ entry.earlyDecision = "allow";
4326
+ broadcast("remove", { id });
4327
+ entry.timer = setTimeout(() => pending.delete(id), 3e4);
4328
+ }
4054
4329
  res.writeHead(200);
4055
4330
  return res.end(JSON.stringify({ ok: true }));
4056
4331
  }
@@ -4062,13 +4337,16 @@ data: ${JSON.stringify(readPersistentDecisions())}
4062
4337
  decision: resolvedDecision
4063
4338
  });
4064
4339
  clearTimeout(entry.timer);
4065
- if (entry.waiter) entry.waiter(resolvedDecision, reason);
4066
- else {
4340
+ if (entry.waiter) {
4341
+ entry.waiter(resolvedDecision, reason);
4342
+ pending.delete(id);
4343
+ broadcast("remove", { id });
4344
+ } else {
4067
4345
  entry.earlyDecision = resolvedDecision;
4068
4346
  entry.earlyReason = reason;
4347
+ broadcast("remove", { id });
4348
+ entry.timer = setTimeout(() => pending.delete(id), 3e4);
4069
4349
  }
4070
- pending.delete(id);
4071
- broadcast("remove", { id });
4072
4350
  res.writeHead(200);
4073
4351
  return res.end(JSON.stringify({ ok: true }));
4074
4352
  } catch {
@@ -4121,116 +4399,683 @@ data: ${JSON.stringify(readPersistentDecisions())}
4121
4399
  res.writeHead(400).end();
4122
4400
  }
4123
4401
  }
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();
4402
+ if (req.method === "DELETE" && pathname.startsWith("/decisions/")) {
4403
+ if (!validToken(req)) return res.writeHead(403).end();
4404
+ try {
4405
+ const toolName = decodeURIComponent(pathname.split("/").pop());
4406
+ const decisions = readPersistentDecisions();
4407
+ delete decisions[toolName];
4408
+ atomicWriteSync2(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
4409
+ broadcast("decisions", decisions);
4410
+ res.writeHead(200);
4411
+ return res.end(JSON.stringify({ ok: true }));
4412
+ } catch {
4413
+ res.writeHead(400).end();
4414
+ }
4415
+ }
4416
+ if (req.method === "POST" && pathname.startsWith("/resolve/")) {
4417
+ const internalAuth = req.headers["x-node9-internal"];
4418
+ if (internalAuth !== internalToken) return res.writeHead(403).end();
4419
+ try {
4420
+ const id = pathname.split("/").pop();
4421
+ const entry = pending.get(id);
4422
+ if (!entry) return res.writeHead(404).end();
4423
+ const { decision } = JSON.parse(await readBody(req));
4424
+ appendAuditLog({
4425
+ toolName: entry.toolName,
4426
+ args: entry.args,
4427
+ decision
4428
+ });
4429
+ clearTimeout(entry.timer);
4430
+ if (entry.waiter) entry.waiter(decision);
4431
+ else entry.earlyDecision = decision;
4432
+ pending.delete(id);
4433
+ broadcast("remove", { id });
4434
+ res.writeHead(200);
4435
+ return res.end(JSON.stringify({ ok: true }));
4436
+ } catch {
4437
+ res.writeHead(400).end();
4438
+ }
4439
+ }
4440
+ if (req.method === "GET" && pathname === "/audit") {
4441
+ res.writeHead(200, { "Content-Type": "application/json" });
4442
+ return res.end(JSON.stringify(getAuditHistory()));
4443
+ }
4444
+ if (req.method === "GET" && pathname === "/shields") {
4445
+ if (!validToken(req)) return res.writeHead(403).end();
4446
+ const active = readActiveShields();
4447
+ const shields = Object.values(SHIELDS).map((s) => ({
4448
+ name: s.name,
4449
+ description: s.description,
4450
+ active: active.includes(s.name)
4451
+ }));
4452
+ res.writeHead(200, { "Content-Type": "application/json" });
4453
+ return res.end(JSON.stringify({ shields }));
4454
+ }
4455
+ if (req.method === "POST" && pathname === "/shields") {
4456
+ if (!validToken(req)) return res.writeHead(403).end();
4457
+ try {
4458
+ const { name, active } = JSON.parse(await readBody(req));
4459
+ if (!SHIELDS[name]) return res.writeHead(400).end();
4460
+ const current = readActiveShields();
4461
+ const updated = active ? [.../* @__PURE__ */ new Set([...current, name])] : current.filter((n) => n !== name);
4462
+ writeActiveShields(updated);
4463
+ _resetConfigCache();
4464
+ const shieldsPayload = Object.values(SHIELDS).map((s) => ({
4465
+ name: s.name,
4466
+ description: s.description,
4467
+ active: updated.includes(s.name)
4468
+ }));
4469
+ broadcast("shields-status", { shields: shieldsPayload });
4470
+ res.writeHead(200);
4471
+ return res.end(JSON.stringify({ ok: true }));
4472
+ } catch {
4473
+ res.writeHead(400).end();
4474
+ }
4475
+ }
4476
+ res.writeHead(404).end();
4477
+ });
4478
+ daemonServer = server;
4479
+ server.on("error", (e) => {
4480
+ if (e.code === "EADDRINUSE") {
4481
+ try {
4482
+ if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
4483
+ const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4484
+ process.kill(pid, 0);
4485
+ return process.exit(0);
4486
+ }
4487
+ } catch {
4488
+ try {
4489
+ import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4490
+ } catch {
4491
+ }
4492
+ server.listen(DAEMON_PORT2, DAEMON_HOST2);
4493
+ return;
4494
+ }
4495
+ }
4496
+ console.error(import_chalk4.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
4497
+ process.exit(1);
4498
+ });
4499
+ server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
4500
+ atomicWriteSync2(
4501
+ DAEMON_PID_FILE,
4502
+ JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
4503
+ { mode: 384 }
4504
+ );
4505
+ console.log(import_chalk4.default.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
4506
+ });
4507
+ if (watchMode) {
4508
+ console.log(import_chalk4.default.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
4509
+ }
4510
+ try {
4511
+ import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4512
+ } catch {
4513
+ }
4514
+ const ACTIVITY_MAX_BYTES = 1024 * 1024;
4515
+ const unixServer = import_net2.default.createServer((socket) => {
4516
+ const chunks = [];
4517
+ let bytesReceived = 0;
4518
+ socket.on("data", (chunk) => {
4519
+ bytesReceived += chunk.length;
4520
+ if (bytesReceived > ACTIVITY_MAX_BYTES) {
4521
+ socket.destroy();
4522
+ return;
4523
+ }
4524
+ chunks.push(chunk);
4525
+ });
4526
+ socket.on("end", () => {
4527
+ try {
4528
+ const data = JSON.parse(Buffer.concat(chunks).toString());
4529
+ if (data.status === "pending") {
4530
+ broadcast("activity", {
4531
+ id: data.id,
4532
+ ts: data.ts,
4533
+ tool: data.tool,
4534
+ args: redactArgs(data.args),
4535
+ status: "pending"
4536
+ });
4537
+ } else {
4538
+ broadcast("activity-result", {
4539
+ id: data.id,
4540
+ status: data.status,
4541
+ label: data.label
4542
+ });
4543
+ }
4544
+ } catch {
4545
+ }
4546
+ });
4547
+ socket.on("error", () => {
4548
+ });
4549
+ });
4550
+ unixServer.listen(ACTIVITY_SOCKET_PATH2);
4551
+ process.on("exit", () => {
4552
+ try {
4553
+ import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4554
+ } catch {
4555
+ }
4556
+ });
4557
+ }
4558
+ function stopDaemon() {
4559
+ if (!import_fs4.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
4560
+ try {
4561
+ const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4562
+ process.kill(pid, "SIGTERM");
4563
+ console.log(import_chalk4.default.green("\u2705 Stopped."));
4564
+ } catch {
4565
+ console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
4566
+ } finally {
4567
+ try {
4568
+ import_fs4.default.unlinkSync(DAEMON_PID_FILE);
4569
+ } catch {
4570
+ }
4571
+ }
4572
+ }
4573
+ function daemonStatus() {
4574
+ if (!import_fs4.default.existsSync(DAEMON_PID_FILE))
4575
+ return console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
4576
+ try {
4577
+ const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4578
+ process.kill(pid, 0);
4579
+ console.log(import_chalk4.default.green("Node9 daemon: running"));
4580
+ } catch {
4581
+ console.log(import_chalk4.default.yellow("Node9 daemon: not running (stale PID)"));
4582
+ }
4583
+ }
4584
+ var import_http, import_net2, import_fs4, import_path6, import_os4, import_child_process2, import_crypto3, import_chalk4, ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
4585
+ var init_daemon = __esm({
4586
+ "src/daemon/index.ts"() {
4587
+ "use strict";
4588
+ init_ui2();
4589
+ import_http = __toESM(require("http"));
4590
+ import_net2 = __toESM(require("net"));
4591
+ import_fs4 = __toESM(require("fs"));
4592
+ import_path6 = __toESM(require("path"));
4593
+ import_os4 = __toESM(require("os"));
4594
+ import_child_process2 = require("child_process");
4595
+ import_crypto3 = require("crypto");
4596
+ import_chalk4 = __toESM(require("chalk"));
4597
+ init_core();
4598
+ init_shields();
4599
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path6.default.join(import_os4.default.tmpdir(), "node9-activity.sock");
4600
+ DAEMON_PORT2 = 7391;
4601
+ DAEMON_HOST2 = "127.0.0.1";
4602
+ homeDir = import_os4.default.homedir();
4603
+ DAEMON_PID_FILE = import_path6.default.join(homeDir, ".node9", "daemon.pid");
4604
+ DECISIONS_FILE = import_path6.default.join(homeDir, ".node9", "decisions.json");
4605
+ GLOBAL_CONFIG_FILE = import_path6.default.join(homeDir, ".node9", "config.json");
4606
+ CREDENTIALS_FILE = import_path6.default.join(homeDir, ".node9", "credentials.json");
4607
+ AUDIT_LOG_FILE = import_path6.default.join(homeDir, ".node9", "audit.log");
4608
+ TRUST_FILE2 = import_path6.default.join(homeDir, ".node9", "trust.json");
4609
+ TRUST_DURATIONS = {
4610
+ "30m": 30 * 6e4,
4611
+ "1h": 60 * 6e4,
4612
+ "2h": 2 * 60 * 6e4
4613
+ };
4614
+ SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
4615
+ AUTO_DENY_MS = 12e4;
4616
+ autoStarted = process.env.NODE9_AUTO_STARTED === "1";
4617
+ pending = /* @__PURE__ */ new Map();
4618
+ sseClients = /* @__PURE__ */ new Set();
4619
+ abandonTimer = null;
4620
+ daemonServer = null;
4621
+ hadBrowserClient = false;
4622
+ ACTIVITY_RING_SIZE = 100;
4623
+ activityRing = [];
4624
+ }
4625
+ });
4626
+
4627
+ // src/tui/tail.ts
4628
+ var tail_exports = {};
4629
+ __export(tail_exports, {
4630
+ startTail: () => startTail
4631
+ });
4632
+ function getIcon(tool) {
4633
+ const t = tool.toLowerCase();
4634
+ for (const [k, v] of Object.entries(ICONS)) {
4635
+ if (t.includes(k)) return v;
4636
+ }
4637
+ return "\u{1F6E0}\uFE0F";
4638
+ }
4639
+ function formatBase(activity) {
4640
+ const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
4641
+ const icon = getIcon(activity.tool);
4642
+ const toolName = activity.tool.slice(0, 16).padEnd(16);
4643
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
4644
+ const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
4645
+ return `${import_chalk5.default.gray(time)} ${icon} ${import_chalk5.default.white.bold(toolName)} ${import_chalk5.default.dim(argsPreview)}`;
4646
+ }
4647
+ function renderResult(activity, result) {
4648
+ const base = formatBase(activity);
4649
+ let status;
4650
+ if (result.status === "allow") {
4651
+ status = import_chalk5.default.green("\u2713 ALLOW");
4652
+ } else if (result.status === "dlp") {
4653
+ status = import_chalk5.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
4654
+ } else {
4655
+ status = import_chalk5.default.red("\u2717 BLOCK");
4656
+ }
4657
+ if (process.stdout.isTTY) {
4658
+ import_readline.default.clearLine(process.stdout, 0);
4659
+ import_readline.default.cursorTo(process.stdout, 0);
4660
+ }
4661
+ console.log(`${base} ${status}`);
4662
+ }
4663
+ function renderPending(activity) {
4664
+ if (!process.stdout.isTTY) return;
4665
+ process.stdout.write(`${formatBase(activity)} ${import_chalk5.default.yellow("\u25CF \u2026")}\r`);
4666
+ }
4667
+ async function ensureDaemon() {
4668
+ if (import_fs6.default.existsSync(PID_FILE)) {
4669
+ try {
4670
+ const { port } = JSON.parse(import_fs6.default.readFileSync(PID_FILE, "utf-8"));
4671
+ return port;
4672
+ } catch {
4673
+ }
4674
+ }
4675
+ console.log(import_chalk5.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
4676
+ const child = (0, import_child_process4.spawn)(process.execPath, [process.argv[1], "daemon"], {
4677
+ detached: true,
4678
+ stdio: "ignore",
4679
+ env: { ...process.env, NODE9_AUTO_STARTED: "1" }
4680
+ });
4681
+ child.unref();
4682
+ for (let i = 0; i < 20; i++) {
4683
+ await new Promise((r) => setTimeout(r, 250));
4684
+ if (!import_fs6.default.existsSync(PID_FILE)) continue;
4685
+ try {
4686
+ const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
4687
+ signal: AbortSignal.timeout(500)
4688
+ });
4689
+ if (res.ok) {
4690
+ const { port } = JSON.parse(import_fs6.default.readFileSync(PID_FILE, "utf-8"));
4691
+ return port;
4692
+ }
4693
+ } catch {
4694
+ }
4695
+ }
4696
+ console.error(import_chalk5.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
4697
+ process.exit(1);
4698
+ }
4699
+ async function startTail(options = {}) {
4700
+ const port = await ensureDaemon();
4701
+ const connectionTime = Date.now();
4702
+ const pending2 = /* @__PURE__ */ new Map();
4703
+ console.log(import_chalk5.default.cyan.bold(`
4704
+ \u{1F6F0}\uFE0F Node9 tail `) + import_chalk5.default.dim(`\u2192 localhost:${port}`));
4705
+ if (options.history) {
4706
+ console.log(import_chalk5.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
4707
+ } else {
4708
+ console.log(
4709
+ import_chalk5.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
4710
+ );
4711
+ }
4712
+ process.on("SIGINT", () => {
4713
+ if (process.stdout.isTTY) {
4714
+ import_readline.default.clearLine(process.stdout, 0);
4715
+ import_readline.default.cursorTo(process.stdout, 0);
4716
+ }
4717
+ console.log(import_chalk5.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
4718
+ process.exit(0);
4719
+ });
4720
+ const req = import_http2.default.get(`http://127.0.0.1:${port}/events`, (res) => {
4721
+ if (res.statusCode !== 200) {
4722
+ console.error(import_chalk5.default.red(`Failed to connect: HTTP ${res.statusCode}`));
4723
+ process.exit(1);
4724
+ }
4725
+ let currentEvent = "";
4726
+ let currentData = "";
4727
+ res.on("error", () => {
4728
+ });
4729
+ const rl = import_readline.default.createInterface({ input: res, crlfDelay: Infinity });
4730
+ rl.on("error", () => {
4731
+ });
4732
+ rl.on("line", (line) => {
4733
+ if (line.startsWith("event:")) {
4734
+ currentEvent = line.slice(6).trim();
4735
+ } else if (line.startsWith("data:")) {
4736
+ currentData = line.slice(5).trim();
4737
+ } else if (line === "") {
4738
+ if (currentEvent && currentData) {
4739
+ handleMessage(currentEvent, currentData);
4740
+ }
4741
+ currentEvent = "";
4742
+ currentData = "";
4743
+ }
4744
+ });
4745
+ rl.on("close", () => {
4746
+ if (process.stdout.isTTY) {
4747
+ import_readline.default.clearLine(process.stdout, 0);
4748
+ import_readline.default.cursorTo(process.stdout, 0);
4749
+ }
4750
+ console.log(import_chalk5.default.red("\n\u274C Daemon disconnected."));
4751
+ process.exit(1);
4752
+ });
4753
+ });
4754
+ function handleMessage(event, rawData) {
4755
+ let data;
4756
+ try {
4757
+ data = JSON.parse(rawData);
4758
+ } catch {
4759
+ return;
4760
+ }
4761
+ if (event === "activity") {
4762
+ if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
4763
+ if (data.status && data.status !== "pending") {
4764
+ renderResult(data, data);
4765
+ return;
4766
+ }
4767
+ pending2.set(data.id, data);
4768
+ const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
4769
+ if (slowTool) renderPending(data);
4770
+ }
4771
+ if (event === "activity-result") {
4772
+ const original = pending2.get(data.id);
4773
+ if (original) {
4774
+ renderResult(original, data);
4775
+ pending2.delete(data.id);
4776
+ }
4777
+ }
4778
+ }
4779
+ req.on("error", (err) => {
4780
+ const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
4781
+ console.error(import_chalk5.default.red(`
4782
+ \u274C ${msg}`));
4783
+ process.exit(1);
4784
+ });
4785
+ }
4786
+ var import_http2, import_chalk5, import_fs6, import_os6, import_path8, import_readline, import_child_process4, PID_FILE, ICONS;
4787
+ var init_tail = __esm({
4788
+ "src/tui/tail.ts"() {
4789
+ "use strict";
4790
+ import_http2 = __toESM(require("http"));
4791
+ import_chalk5 = __toESM(require("chalk"));
4792
+ import_fs6 = __toESM(require("fs"));
4793
+ import_os6 = __toESM(require("os"));
4794
+ import_path8 = __toESM(require("path"));
4795
+ import_readline = __toESM(require("readline"));
4796
+ import_child_process4 = require("child_process");
4797
+ init_daemon();
4798
+ PID_FILE = import_path8.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
4799
+ ICONS = {
4800
+ bash: "\u{1F4BB}",
4801
+ shell: "\u{1F4BB}",
4802
+ terminal: "\u{1F4BB}",
4803
+ read: "\u{1F4D6}",
4804
+ edit: "\u270F\uFE0F",
4805
+ write: "\u270F\uFE0F",
4806
+ glob: "\u{1F4C2}",
4807
+ grep: "\u{1F50D}",
4808
+ agent: "\u{1F916}",
4809
+ search: "\u{1F50D}",
4810
+ sql: "\u{1F5C4}\uFE0F",
4811
+ query: "\u{1F5C4}\uFE0F",
4812
+ list: "\u{1F4C2}",
4813
+ delete: "\u{1F5D1}\uFE0F",
4814
+ web: "\u{1F310}"
4815
+ };
4816
+ }
4817
+ });
4818
+
4819
+ // src/cli.ts
4820
+ var import_commander = require("commander");
4821
+ init_core();
4822
+
4823
+ // src/setup.ts
4824
+ var import_fs3 = __toESM(require("fs"));
4825
+ var import_path5 = __toESM(require("path"));
4826
+ var import_os3 = __toESM(require("os"));
4827
+ var import_chalk3 = __toESM(require("chalk"));
4828
+ var import_prompts2 = require("@inquirer/prompts");
4829
+ function printDaemonTip() {
4830
+ console.log(
4831
+ import_chalk3.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + import_chalk3.default.white("\n To view your history or manage persistent rules, run:") + import_chalk3.default.green("\n node9 daemon --openui")
4832
+ );
4833
+ }
4834
+ function fullPathCommand(subcommand) {
4835
+ if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
4836
+ const nodeExec = process.execPath;
4837
+ const cliScript = process.argv[1];
4838
+ return `${nodeExec} ${cliScript} ${subcommand}`;
4839
+ }
4840
+ function readJson(filePath) {
4841
+ try {
4842
+ if (import_fs3.default.existsSync(filePath)) {
4843
+ return JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
4844
+ }
4845
+ } catch {
4846
+ }
4847
+ return null;
4848
+ }
4849
+ function writeJson(filePath, data) {
4850
+ const dir = import_path5.default.dirname(filePath);
4851
+ if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
4852
+ import_fs3.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
4853
+ }
4854
+ async function setupClaude() {
4855
+ const homeDir2 = import_os3.default.homedir();
4856
+ const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
4857
+ const hooksPath = import_path5.default.join(homeDir2, ".claude", "settings.json");
4858
+ const claudeConfig = readJson(mcpPath) ?? {};
4859
+ const settings = readJson(hooksPath) ?? {};
4860
+ const servers = claudeConfig.mcpServers ?? {};
4861
+ let anythingChanged = false;
4862
+ if (!settings.hooks) settings.hooks = {};
4863
+ const hasPreHook = settings.hooks.PreToolUse?.some(
4864
+ (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
4865
+ );
4866
+ if (!hasPreHook) {
4867
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
4868
+ settings.hooks.PreToolUse.push({
4869
+ matcher: ".*",
4870
+ hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
4871
+ });
4872
+ console.log(import_chalk3.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
4873
+ anythingChanged = true;
4874
+ }
4875
+ const hasPostHook = settings.hooks.PostToolUse?.some(
4876
+ (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
4877
+ );
4878
+ if (!hasPostHook) {
4879
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
4880
+ settings.hooks.PostToolUse.push({
4881
+ matcher: ".*",
4882
+ hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
4883
+ });
4884
+ console.log(import_chalk3.default.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
4885
+ anythingChanged = true;
4886
+ }
4887
+ if (anythingChanged) {
4888
+ writeJson(hooksPath, settings);
4889
+ console.log("");
4890
+ }
4891
+ const serversToWrap = [];
4892
+ for (const [name, server] of Object.entries(servers)) {
4893
+ if (!server.command || server.command === "node9") continue;
4894
+ const parts = [server.command, ...server.args ?? []];
4895
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
4896
+ }
4897
+ if (serversToWrap.length > 0) {
4898
+ console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
4899
+ console.log(import_chalk3.default.white(` ${mcpPath}`));
4900
+ for (const { name, originalCmd } of serversToWrap) {
4901
+ console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
4902
+ }
4903
+ console.log("");
4904
+ const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
4905
+ if (proceed) {
4906
+ for (const { name, parts } of serversToWrap) {
4907
+ servers[name] = { ...servers[name], command: "node9", args: parts };
4136
4908
  }
4909
+ claudeConfig.mcpServers = servers;
4910
+ writeJson(mcpPath, claudeConfig);
4911
+ console.log(import_chalk3.default.green(`
4912
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
4913
+ anythingChanged = true;
4914
+ } else {
4915
+ console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
4137
4916
  }
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();
4917
+ console.log("");
4918
+ }
4919
+ if (!anythingChanged && serversToWrap.length === 0) {
4920
+ console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
4921
+ printDaemonTip();
4922
+ return;
4923
+ }
4924
+ if (anythingChanged) {
4925
+ console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
4926
+ console.log(import_chalk3.default.gray(" Restart Claude Code for changes to take effect."));
4927
+ printDaemonTip();
4928
+ }
4929
+ }
4930
+ async function setupGemini() {
4931
+ const homeDir2 = import_os3.default.homedir();
4932
+ const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
4933
+ const settings = readJson(settingsPath) ?? {};
4934
+ const servers = settings.mcpServers ?? {};
4935
+ let anythingChanged = false;
4936
+ if (!settings.hooks) settings.hooks = {};
4937
+ const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
4938
+ (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
4939
+ );
4940
+ if (!hasBeforeHook) {
4941
+ if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
4942
+ if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
4943
+ settings.hooks.BeforeTool.push({
4944
+ matcher: ".*",
4945
+ hooks: [
4946
+ {
4947
+ name: "node9-check",
4948
+ type: "command",
4949
+ command: fullPathCommand("check"),
4950
+ timeout: 6e5
4951
+ }
4952
+ ]
4953
+ });
4954
+ console.log(import_chalk3.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
4955
+ anythingChanged = true;
4956
+ }
4957
+ const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
4958
+ (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
4959
+ );
4960
+ if (!hasAfterHook) {
4961
+ if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
4962
+ if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
4963
+ settings.hooks.AfterTool.push({
4964
+ matcher: ".*",
4965
+ hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
4966
+ });
4967
+ console.log(import_chalk3.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
4968
+ anythingChanged = true;
4969
+ }
4970
+ if (anythingChanged) {
4971
+ writeJson(settingsPath, settings);
4972
+ console.log("");
4973
+ }
4974
+ const serversToWrap = [];
4975
+ for (const [name, server] of Object.entries(servers)) {
4976
+ if (!server.command || server.command === "node9") continue;
4977
+ const parts = [server.command, ...server.args ?? []];
4978
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
4979
+ }
4980
+ if (serversToWrap.length > 0) {
4981
+ console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
4982
+ console.log(import_chalk3.default.white(` ${settingsPath} (mcpServers)`));
4983
+ for (const { name, originalCmd } of serversToWrap) {
4984
+ console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
4985
+ }
4986
+ console.log("");
4987
+ const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
4988
+ if (proceed) {
4989
+ for (const { name, parts } of serversToWrap) {
4990
+ servers[name] = { ...servers[name], command: "node9", args: parts };
4160
4991
  }
4992
+ settings.mcpServers = servers;
4993
+ writeJson(settingsPath, settings);
4994
+ console.log(import_chalk3.default.green(`
4995
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
4996
+ anythingChanged = true;
4997
+ } else {
4998
+ console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
4161
4999
  }
4162
- if (req.method === "GET" && pathname === "/audit") {
4163
- res.writeHead(200, { "Content-Type": "application/json" });
4164
- return res.end(JSON.stringify(getAuditHistory()));
5000
+ console.log("");
5001
+ }
5002
+ if (!anythingChanged && serversToWrap.length === 0) {
5003
+ console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
5004
+ printDaemonTip();
5005
+ return;
5006
+ }
5007
+ if (anythingChanged) {
5008
+ console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
5009
+ console.log(import_chalk3.default.gray(" Restart Gemini CLI for changes to take effect."));
5010
+ printDaemonTip();
5011
+ }
5012
+ }
5013
+ async function setupCursor() {
5014
+ const homeDir2 = import_os3.default.homedir();
5015
+ const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
5016
+ const mcpConfig = readJson(mcpPath) ?? {};
5017
+ const servers = mcpConfig.mcpServers ?? {};
5018
+ let anythingChanged = false;
5019
+ const serversToWrap = [];
5020
+ for (const [name, server] of Object.entries(servers)) {
5021
+ if (!server.command || server.command === "node9") continue;
5022
+ const parts = [server.command, ...server.args ?? []];
5023
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
5024
+ }
5025
+ if (serversToWrap.length > 0) {
5026
+ console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
5027
+ console.log(import_chalk3.default.white(` ${mcpPath}`));
5028
+ for (const { name, originalCmd } of serversToWrap) {
5029
+ console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
4165
5030
  }
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;
5031
+ console.log("");
5032
+ const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
5033
+ if (proceed) {
5034
+ for (const { name, parts } of serversToWrap) {
5035
+ servers[name] = { ...servers[name], command: "node9", args: parts };
4184
5036
  }
5037
+ mcpConfig.mcpServers = servers;
5038
+ writeJson(mcpPath, mcpConfig);
5039
+ console.log(import_chalk3.default.green(`
5040
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
5041
+ anythingChanged = true;
5042
+ } else {
5043
+ console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
4185
5044
  }
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 }
5045
+ console.log("");
5046
+ }
5047
+ console.log(
5048
+ import_chalk3.default.yellow(
5049
+ " \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
5050
+ )
5051
+ );
5052
+ console.log("");
5053
+ if (!anythingChanged && serversToWrap.length === 0) {
5054
+ console.log(
5055
+ import_chalk3.default.blue(
5056
+ "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
5057
+ )
4194
5058
  );
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
- }
5059
+ printDaemonTip();
5060
+ return;
4211
5061
  }
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)"));
5062
+ if (anythingChanged) {
5063
+ console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
5064
+ console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
5065
+ printDaemonTip();
4222
5066
  }
4223
5067
  }
4224
5068
 
4225
5069
  // src/cli.ts
4226
- var import_child_process4 = require("child_process");
5070
+ init_daemon();
5071
+ var import_child_process5 = require("child_process");
4227
5072
  var import_execa = require("execa");
4228
5073
  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"));
5074
+ var import_chalk6 = __toESM(require("chalk"));
5075
+ var import_readline2 = __toESM(require("readline"));
5076
+ var import_fs7 = __toESM(require("fs"));
5077
+ var import_path9 = __toESM(require("path"));
5078
+ var import_os7 = __toESM(require("os"));
4234
5079
 
4235
5080
  // src/undo.ts
4236
5081
  var import_child_process3 = require("child_process");
@@ -4344,9 +5189,10 @@ function applyUndo(hash, cwd) {
4344
5189
  }
4345
5190
 
4346
5191
  // src/cli.ts
5192
+ init_shields();
4347
5193
  var import_prompts3 = require("@inquirer/prompts");
4348
5194
  var { version } = JSON.parse(
4349
- import_fs6.default.readFileSync(import_path8.default.join(__dirname, "../package.json"), "utf-8")
5195
+ import_fs7.default.readFileSync(import_path9.default.join(__dirname, "../package.json"), "utf-8")
4350
5196
  );
4351
5197
  function parseDuration(str) {
4352
5198
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -4434,15 +5280,15 @@ function openBrowserLocal() {
4434
5280
  const url = `http://${DAEMON_HOST2}:${DAEMON_PORT2}/`;
4435
5281
  try {
4436
5282
  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);
5283
+ if (process.platform === "darwin") (0, import_child_process5.execSync)(`open "${url}"`, opts);
5284
+ else if (process.platform === "win32") (0, import_child_process5.execSync)(`cmd /c start "" "${url}"`, opts);
5285
+ else (0, import_child_process5.execSync)(`xdg-open "${url}"`, opts);
4440
5286
  } catch {
4441
5287
  }
4442
5288
  }
4443
5289
  async function autoStartDaemonAndWait() {
4444
5290
  try {
4445
- const child = (0, import_child_process4.spawn)("node9", ["daemon"], {
5291
+ const child = (0, import_child_process5.spawn)("node9", ["daemon"], {
4446
5292
  detached: true,
4447
5293
  stdio: "ignore",
4448
5294
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
@@ -4478,14 +5324,14 @@ async function runProxy(targetCommand) {
4478
5324
  if (stdout) executable = stdout.trim();
4479
5325
  } catch {
4480
5326
  }
4481
- console.log(import_chalk5.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
4482
- const child = (0, import_child_process4.spawn)(executable, args, {
5327
+ console.log(import_chalk6.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
5328
+ const child = (0, import_child_process5.spawn)(executable, args, {
4483
5329
  stdio: ["pipe", "pipe", "inherit"],
4484
5330
  // We control STDIN and STDOUT
4485
5331
  shell: false,
4486
5332
  env: { ...process.env, FORCE_COLOR: "1" }
4487
5333
  });
4488
- const agentIn = import_readline.default.createInterface({ input: process.stdin, terminal: false });
5334
+ const agentIn = import_readline2.default.createInterface({ input: process.stdin, terminal: false });
4489
5335
  agentIn.on("line", async (line) => {
4490
5336
  let message;
4491
5337
  try {
@@ -4503,10 +5349,10 @@ async function runProxy(targetCommand) {
4503
5349
  agent: "Proxy/MCP"
4504
5350
  });
4505
5351
  if (!result.approved) {
4506
- console.error(import_chalk5.default.red(`
5352
+ console.error(import_chalk6.default.red(`
4507
5353
  \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"}
5354
+ console.error(import_chalk6.default.gray(` Tool: ${name}`));
5355
+ console.error(import_chalk6.default.gray(` Reason: ${result.reason || "Security Policy"}
4510
5356
  `));
4511
5357
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
4512
5358
  const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -4548,14 +5394,14 @@ async function runProxy(targetCommand) {
4548
5394
  }
4549
5395
  program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
4550
5396
  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 });
5397
+ const credPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "credentials.json");
5398
+ if (!import_fs7.default.existsSync(import_path9.default.dirname(credPath)))
5399
+ import_fs7.default.mkdirSync(import_path9.default.dirname(credPath), { recursive: true });
4554
5400
  const profileName = options.profile || "default";
4555
5401
  let existingCreds = {};
4556
5402
  try {
4557
- if (import_fs6.default.existsSync(credPath)) {
4558
- const raw = JSON.parse(import_fs6.default.readFileSync(credPath, "utf-8"));
5403
+ if (import_fs7.default.existsSync(credPath)) {
5404
+ const raw = JSON.parse(import_fs7.default.readFileSync(credPath, "utf-8"));
4559
5405
  if (raw.apiKey) {
4560
5406
  existingCreds = {
4561
5407
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -4567,13 +5413,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4567
5413
  } catch {
4568
5414
  }
4569
5415
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
4570
- import_fs6.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
5416
+ import_fs7.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
4571
5417
  if (profileName === "default") {
4572
- const configPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "config.json");
5418
+ const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
4573
5419
  let config = {};
4574
5420
  try {
4575
- if (import_fs6.default.existsSync(configPath))
4576
- config = JSON.parse(import_fs6.default.readFileSync(configPath, "utf-8"));
5421
+ if (import_fs7.default.existsSync(configPath))
5422
+ config = JSON.parse(import_fs7.default.readFileSync(configPath, "utf-8"));
4577
5423
  } catch {
4578
5424
  }
4579
5425
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -4588,36 +5434,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4588
5434
  approvers.cloud = false;
4589
5435
  }
4590
5436
  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 });
5437
+ if (!import_fs7.default.existsSync(import_path9.default.dirname(configPath)))
5438
+ import_fs7.default.mkdirSync(import_path9.default.dirname(configPath), { recursive: true });
5439
+ import_fs7.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
4594
5440
  }
4595
5441
  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`));
5442
+ console.log(import_chalk6.default.green(`\u2705 Profile "${profileName}" saved`));
5443
+ console.log(import_chalk6.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
4598
5444
  } 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.`));
5445
+ console.log(import_chalk6.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
5446
+ console.log(import_chalk6.default.gray(` All decisions stay on this machine.`));
4601
5447
  } 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.`));
5448
+ console.log(import_chalk6.default.green(`\u2705 Logged in \u2014 agent mode`));
5449
+ console.log(import_chalk6.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
4604
5450
  }
4605
5451
  });
4606
5452
  program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
4607
5453
  if (target === "gemini") return await setupGemini();
4608
5454
  if (target === "claude") return await setupClaude();
4609
5455
  if (target === "cursor") return await setupCursor();
4610
- console.error(import_chalk5.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
5456
+ console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
4611
5457
  process.exit(1);
4612
5458
  });
4613
5459
  program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
4614
5460
  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");
5461
+ console.log(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
5462
+ console.log(" Usage: " + import_chalk6.default.white("node9 setup <target>") + "\n");
4617
5463
  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)");
5464
+ console.log(" " + import_chalk6.default.green("claude") + " \u2014 Claude Code (hook mode)");
5465
+ console.log(" " + import_chalk6.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
5466
+ console.log(" " + import_chalk6.default.green("cursor") + " \u2014 Cursor (hook mode)");
4621
5467
  console.log("");
4622
5468
  return;
4623
5469
  }
@@ -4625,33 +5471,33 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
4625
5471
  if (t === "gemini") return await setupGemini();
4626
5472
  if (t === "claude") return await setupClaude();
4627
5473
  if (t === "cursor") return await setupCursor();
4628
- console.error(import_chalk5.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
5474
+ console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
4629
5475
  process.exit(1);
4630
5476
  });
4631
5477
  program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
4632
- const homeDir2 = import_os6.default.homedir();
5478
+ const homeDir2 = import_os7.default.homedir();
4633
5479
  let failures = 0;
4634
5480
  function pass(msg) {
4635
- console.log(import_chalk5.default.green(" \u2705 ") + msg);
5481
+ console.log(import_chalk6.default.green(" \u2705 ") + msg);
4636
5482
  }
4637
5483
  function fail(msg, hint) {
4638
- console.log(import_chalk5.default.red(" \u274C ") + msg);
4639
- if (hint) console.log(import_chalk5.default.gray(" " + hint));
5484
+ console.log(import_chalk6.default.red(" \u274C ") + msg);
5485
+ if (hint) console.log(import_chalk6.default.gray(" " + hint));
4640
5486
  failures++;
4641
5487
  }
4642
5488
  function warn(msg, hint) {
4643
- console.log(import_chalk5.default.yellow(" \u26A0\uFE0F ") + msg);
4644
- if (hint) console.log(import_chalk5.default.gray(" " + hint));
5489
+ console.log(import_chalk6.default.yellow(" \u26A0\uFE0F ") + msg);
5490
+ if (hint) console.log(import_chalk6.default.gray(" " + hint));
4645
5491
  }
4646
5492
  function section(title) {
4647
- console.log("\n" + import_chalk5.default.bold(title));
5493
+ console.log("\n" + import_chalk6.default.bold(title));
4648
5494
  }
4649
- console.log(import_chalk5.default.cyan.bold(`
5495
+ console.log(import_chalk6.default.cyan.bold(`
4650
5496
  \u{1F6E1}\uFE0F Node9 Doctor v${version}
4651
5497
  `));
4652
5498
  section("Binary");
4653
5499
  try {
4654
- const which = (0, import_child_process4.execSync)("which node9", { encoding: "utf-8" }).trim();
5500
+ const which = (0, import_child_process5.execSync)("which node9", { encoding: "utf-8" }).trim();
4655
5501
  pass(`node9 found at ${which}`);
4656
5502
  } catch {
4657
5503
  warn("node9 not found in $PATH \u2014 hooks may not find it", "Run: npm install -g @node9/proxy");
@@ -4666,7 +5512,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
4666
5512
  );
4667
5513
  }
4668
5514
  try {
4669
- const gitVersion = (0, import_child_process4.execSync)("git --version", { encoding: "utf-8" }).trim();
5515
+ const gitVersion = (0, import_child_process5.execSync)("git --version", { encoding: "utf-8" }).trim();
4670
5516
  pass(gitVersion);
4671
5517
  } catch {
4672
5518
  warn(
@@ -4675,10 +5521,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4675
5521
  );
4676
5522
  }
4677
5523
  section("Configuration");
4678
- const globalConfigPath = import_path8.default.join(homeDir2, ".node9", "config.json");
4679
- if (import_fs6.default.existsSync(globalConfigPath)) {
5524
+ const globalConfigPath = import_path9.default.join(homeDir2, ".node9", "config.json");
5525
+ if (import_fs7.default.existsSync(globalConfigPath)) {
4680
5526
  try {
4681
- JSON.parse(import_fs6.default.readFileSync(globalConfigPath, "utf-8"));
5527
+ JSON.parse(import_fs7.default.readFileSync(globalConfigPath, "utf-8"));
4682
5528
  pass("~/.node9/config.json found and valid");
4683
5529
  } catch {
4684
5530
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -4686,17 +5532,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
4686
5532
  } else {
4687
5533
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
4688
5534
  }
4689
- const projectConfigPath = import_path8.default.join(process.cwd(), "node9.config.json");
4690
- if (import_fs6.default.existsSync(projectConfigPath)) {
5535
+ const projectConfigPath = import_path9.default.join(process.cwd(), "node9.config.json");
5536
+ if (import_fs7.default.existsSync(projectConfigPath)) {
4691
5537
  try {
4692
- JSON.parse(import_fs6.default.readFileSync(projectConfigPath, "utf-8"));
5538
+ JSON.parse(import_fs7.default.readFileSync(projectConfigPath, "utf-8"));
4693
5539
  pass("node9.config.json found and valid (project)");
4694
5540
  } catch {
4695
5541
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
4696
5542
  }
4697
5543
  }
4698
- const credsPath = import_path8.default.join(homeDir2, ".node9", "credentials.json");
4699
- if (import_fs6.default.existsSync(credsPath)) {
5544
+ const credsPath = import_path9.default.join(homeDir2, ".node9", "credentials.json");
5545
+ if (import_fs7.default.existsSync(credsPath)) {
4700
5546
  pass("Cloud credentials found (~/.node9/credentials.json)");
4701
5547
  } else {
4702
5548
  warn(
@@ -4705,10 +5551,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4705
5551
  );
4706
5552
  }
4707
5553
  section("Agent Hooks");
4708
- const claudeSettingsPath = import_path8.default.join(homeDir2, ".claude", "settings.json");
4709
- if (import_fs6.default.existsSync(claudeSettingsPath)) {
5554
+ const claudeSettingsPath = import_path9.default.join(homeDir2, ".claude", "settings.json");
5555
+ if (import_fs7.default.existsSync(claudeSettingsPath)) {
4710
5556
  try {
4711
- const cs = JSON.parse(import_fs6.default.readFileSync(claudeSettingsPath, "utf-8"));
5557
+ const cs = JSON.parse(import_fs7.default.readFileSync(claudeSettingsPath, "utf-8"));
4712
5558
  const hasHook = cs.hooks?.PreToolUse?.some(
4713
5559
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4714
5560
  );
@@ -4721,10 +5567,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4721
5567
  } else {
4722
5568
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
4723
5569
  }
4724
- const geminiSettingsPath = import_path8.default.join(homeDir2, ".gemini", "settings.json");
4725
- if (import_fs6.default.existsSync(geminiSettingsPath)) {
5570
+ const geminiSettingsPath = import_path9.default.join(homeDir2, ".gemini", "settings.json");
5571
+ if (import_fs7.default.existsSync(geminiSettingsPath)) {
4726
5572
  try {
4727
- const gs = JSON.parse(import_fs6.default.readFileSync(geminiSettingsPath, "utf-8"));
5573
+ const gs = JSON.parse(import_fs7.default.readFileSync(geminiSettingsPath, "utf-8"));
4728
5574
  const hasHook = gs.hooks?.BeforeTool?.some(
4729
5575
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4730
5576
  );
@@ -4737,10 +5583,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4737
5583
  } else {
4738
5584
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
4739
5585
  }
4740
- const cursorHooksPath = import_path8.default.join(homeDir2, ".cursor", "hooks.json");
4741
- if (import_fs6.default.existsSync(cursorHooksPath)) {
5586
+ const cursorHooksPath = import_path9.default.join(homeDir2, ".cursor", "hooks.json");
5587
+ if (import_fs7.default.existsSync(cursorHooksPath)) {
4742
5588
  try {
4743
- const cur = JSON.parse(import_fs6.default.readFileSync(cursorHooksPath, "utf-8"));
5589
+ const cur = JSON.parse(import_fs7.default.readFileSync(cursorHooksPath, "utf-8"));
4744
5590
  const hasHook = cur.hooks?.preToolUse?.some(
4745
5591
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
4746
5592
  );
@@ -4761,9 +5607,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
4761
5607
  }
4762
5608
  console.log("");
4763
5609
  if (failures === 0) {
4764
- console.log(import_chalk5.default.green.bold(" All checks passed. Node9 is ready.\n"));
5610
+ console.log(import_chalk6.default.green.bold(" All checks passed. Node9 is ready.\n"));
4765
5611
  } else {
4766
- console.log(import_chalk5.default.red.bold(` ${failures} check(s) failed. See hints above.
5612
+ console.log(import_chalk6.default.red.bold(` ${failures} check(s) failed. See hints above.
4767
5613
  `));
4768
5614
  process.exit(1);
4769
5615
  }
@@ -4778,7 +5624,7 @@ program.command("explain").description(
4778
5624
  try {
4779
5625
  args = JSON.parse(trimmed);
4780
5626
  } catch {
4781
- console.error(import_chalk5.default.red(`
5627
+ console.error(import_chalk6.default.red(`
4782
5628
  \u274C Invalid JSON: ${trimmed}
4783
5629
  `));
4784
5630
  process.exit(1);
@@ -4789,63 +5635,63 @@ program.command("explain").description(
4789
5635
  }
4790
5636
  const result = await explainPolicy(tool, args);
4791
5637
  console.log("");
4792
- console.log(import_chalk5.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
5638
+ console.log(import_chalk6.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
4793
5639
  console.log("");
4794
- console.log(` ${import_chalk5.default.bold("Tool:")} ${import_chalk5.default.white(result.tool)}`);
5640
+ console.log(` ${import_chalk6.default.bold("Tool:")} ${import_chalk6.default.white(result.tool)}`);
4795
5641
  if (argsRaw) {
4796
5642
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
4797
- console.log(` ${import_chalk5.default.bold("Input:")} ${import_chalk5.default.gray(preview)}`);
5643
+ console.log(` ${import_chalk6.default.bold("Input:")} ${import_chalk6.default.gray(preview)}`);
4798
5644
  }
4799
5645
  console.log("");
4800
- console.log(import_chalk5.default.bold("Config Sources (Waterfall):"));
5646
+ console.log(import_chalk6.default.bold("Config Sources (Waterfall):"));
4801
5647
  for (const tier of result.waterfall) {
4802
- const num = import_chalk5.default.gray(` ${tier.tier}.`);
5648
+ const num = import_chalk6.default.gray(` ${tier.tier}.`);
4803
5649
  const label = tier.label.padEnd(16);
4804
5650
  let statusStr;
4805
5651
  if (tier.tier === 1) {
4806
- statusStr = import_chalk5.default.gray(tier.note ?? "");
5652
+ statusStr = import_chalk6.default.gray(tier.note ?? "");
4807
5653
  } 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 : "");
5654
+ const loc = tier.path ? import_chalk6.default.gray(tier.path) : "";
5655
+ const note = tier.note ? import_chalk6.default.gray(`(${tier.note})`) : "";
5656
+ statusStr = import_chalk6.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
4811
5657
  } else {
4812
- statusStr = import_chalk5.default.gray("\u25CB " + (tier.note ?? "not found"));
5658
+ statusStr = import_chalk6.default.gray("\u25CB " + (tier.note ?? "not found"));
4813
5659
  }
4814
- console.log(`${num} ${import_chalk5.default.white(label)} ${statusStr}`);
5660
+ console.log(`${num} ${import_chalk6.default.white(label)} ${statusStr}`);
4815
5661
  }
4816
5662
  console.log("");
4817
- console.log(import_chalk5.default.bold("Policy Evaluation:"));
5663
+ console.log(import_chalk6.default.bold("Policy Evaluation:"));
4818
5664
  for (const step of result.steps) {
4819
5665
  const isFinal = step.isFinal;
4820
5666
  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 ");
5667
+ if (step.outcome === "allow") icon = import_chalk6.default.green(" \u2705");
5668
+ else if (step.outcome === "review") icon = import_chalk6.default.red(" \u{1F534}");
5669
+ else if (step.outcome === "skip") icon = import_chalk6.default.gray(" \u2500 ");
5670
+ else icon = import_chalk6.default.gray(" \u25CB ");
4825
5671
  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") : "";
5672
+ const nameStr = isFinal ? import_chalk6.default.white.bold(name) : import_chalk6.default.white(name);
5673
+ const detail = isFinal ? import_chalk6.default.white(step.detail) : import_chalk6.default.gray(step.detail);
5674
+ const arrow = isFinal ? import_chalk6.default.yellow(" \u2190 STOP") : "";
4829
5675
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
4830
5676
  }
4831
5677
  console.log("");
4832
5678
  if (result.decision === "allow") {
4833
- console.log(import_chalk5.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk5.default.gray(" \u2014 no approval needed"));
5679
+ console.log(import_chalk6.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk6.default.gray(" \u2014 no approval needed"));
4834
5680
  } else {
4835
5681
  console.log(
4836
- import_chalk5.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk5.default.gray(" \u2014 human approval required")
5682
+ import_chalk6.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk6.default.gray(" \u2014 human approval required")
4837
5683
  );
4838
5684
  if (result.blockedByLabel) {
4839
- console.log(import_chalk5.default.gray(` Reason: ${result.blockedByLabel}`));
5685
+ console.log(import_chalk6.default.gray(` Reason: ${result.blockedByLabel}`));
4840
5686
  }
4841
5687
  }
4842
5688
  console.log("");
4843
5689
  });
4844
5690
  program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
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.`));
5691
+ const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
5692
+ if (import_fs7.default.existsSync(configPath) && !options.force) {
5693
+ console.log(import_chalk6.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
5694
+ console.log(import_chalk6.default.gray(` Run with --force to overwrite.`));
4849
5695
  return;
4850
5696
  }
4851
5697
  const requestedMode = options.mode.toLowerCase();
@@ -4857,13 +5703,13 @@ program.command("init").description("Create ~/.node9/config.json with default po
4857
5703
  mode: safeMode
4858
5704
  }
4859
5705
  };
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}`));
5706
+ const dir = import_path9.default.dirname(configPath);
5707
+ if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
5708
+ import_fs7.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
5709
+ console.log(import_chalk6.default.green(`\u2705 Global config created: ${configPath}`));
5710
+ console.log(import_chalk6.default.cyan(` Mode set to: ${safeMode}`));
4865
5711
  console.log(
4866
- import_chalk5.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
5712
+ import_chalk6.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
4867
5713
  );
4868
5714
  });
4869
5715
  function formatRelativeTime(timestamp) {
@@ -4877,14 +5723,14 @@ function formatRelativeTime(timestamp) {
4877
5723
  return new Date(timestamp).toLocaleDateString();
4878
5724
  }
4879
5725
  program.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
4880
- const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "audit.log");
4881
- if (!import_fs6.default.existsSync(logPath)) {
5726
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
5727
+ if (!import_fs7.default.existsSync(logPath)) {
4882
5728
  console.log(
4883
- import_chalk5.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
5729
+ import_chalk6.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
4884
5730
  );
4885
5731
  return;
4886
5732
  }
4887
- const raw = import_fs6.default.readFileSync(logPath, "utf-8");
5733
+ const raw = import_fs7.default.readFileSync(logPath, "utf-8");
4888
5734
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
4889
5735
  let entries = lines.flatMap((line) => {
4890
5736
  try {
@@ -4906,31 +5752,31 @@ program.command("audit").description("View local execution audit log").option("-
4906
5752
  return;
4907
5753
  }
4908
5754
  if (entries.length === 0) {
4909
- console.log(import_chalk5.default.yellow("No matching audit entries."));
5755
+ console.log(import_chalk6.default.yellow("No matching audit entries."));
4910
5756
  return;
4911
5757
  }
4912
5758
  console.log(
4913
5759
  `
4914
- ${import_chalk5.default.bold("Node9 Audit Log")} ${import_chalk5.default.dim(`(${entries.length} entries)`)}`
5760
+ ${import_chalk6.default.bold("Node9 Audit Log")} ${import_chalk6.default.dim(`(${entries.length} entries)`)}`
4915
5761
  );
4916
- console.log(import_chalk5.default.dim(" " + "\u2500".repeat(65)));
5762
+ console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
4917
5763
  console.log(
4918
5764
  ` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
4919
5765
  );
4920
- console.log(import_chalk5.default.dim(" " + "\u2500".repeat(65)));
5766
+ console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
4921
5767
  for (const e of entries) {
4922
5768
  const time = formatRelativeTime(String(e.ts)).padEnd(12);
4923
5769
  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));
5770
+ const result = e.decision === "allow" ? import_chalk6.default.green("ALLOW".padEnd(10)) : import_chalk6.default.red("DENY".padEnd(10));
4925
5771
  const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
4926
5772
  const agent = String(e.agent || "unknown");
4927
5773
  console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
4928
5774
  }
4929
5775
  const allowed = entries.filter((e) => e.decision === "allow").length;
4930
5776
  const denied = entries.filter((e) => e.decision === "deny").length;
4931
- console.log(import_chalk5.default.dim(" " + "\u2500".repeat(65)));
5777
+ console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
4932
5778
  console.log(
4933
- ` ${entries.length} entries | ${import_chalk5.default.green(allowed + " allowed")} | ${import_chalk5.default.red(denied + " denied")}
5779
+ ` ${entries.length} entries | ${import_chalk6.default.green(allowed + " allowed")} | ${import_chalk6.default.red(denied + " denied")}
4934
5780
  `
4935
5781
  );
4936
5782
  });
@@ -4941,43 +5787,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
4941
5787
  const settings = mergedConfig.settings;
4942
5788
  console.log("");
4943
5789
  if (creds && settings.approvers.cloud) {
4944
- console.log(import_chalk5.default.green(" \u25CF Agent mode") + import_chalk5.default.gray(" \u2014 cloud team policy enforced"));
5790
+ console.log(import_chalk6.default.green(" \u25CF Agent mode") + import_chalk6.default.gray(" \u2014 cloud team policy enforced"));
4945
5791
  } else if (creds && !settings.approvers.cloud) {
4946
5792
  console.log(
4947
- import_chalk5.default.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + import_chalk5.default.gray(" \u2014 all decisions stay on this machine")
5793
+ import_chalk6.default.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + import_chalk6.default.gray(" \u2014 all decisions stay on this machine")
4948
5794
  );
4949
5795
  } else {
4950
5796
  console.log(
4951
- import_chalk5.default.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + import_chalk5.default.gray(" \u2014 no API key (Local rules only)")
5797
+ import_chalk6.default.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + import_chalk6.default.gray(" \u2014 no API key (Local rules only)")
4952
5798
  );
4953
5799
  }
4954
5800
  console.log("");
4955
5801
  if (daemonRunning) {
4956
5802
  console.log(
4957
- import_chalk5.default.green(" \u25CF Daemon running") + import_chalk5.default.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
5803
+ import_chalk6.default.green(" \u25CF Daemon running") + import_chalk6.default.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
4958
5804
  );
4959
5805
  } else {
4960
- console.log(import_chalk5.default.gray(" \u25CB Daemon stopped"));
5806
+ console.log(import_chalk6.default.gray(" \u25CB Daemon stopped"));
4961
5807
  }
4962
5808
  if (settings.enableUndo) {
4963
5809
  console.log(
4964
- import_chalk5.default.magenta(" \u25CF Undo Engine") + import_chalk5.default.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
5810
+ import_chalk6.default.magenta(" \u25CF Undo Engine") + import_chalk6.default.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
4965
5811
  );
4966
5812
  }
4967
5813
  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");
5814
+ const modeLabel = settings.mode === "audit" ? import_chalk6.default.blue("audit") : settings.mode === "strict" ? import_chalk6.default.red("strict") : import_chalk6.default.white("standard");
4969
5815
  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");
5816
+ const projectConfig = import_path9.default.join(process.cwd(), "node9.config.json");
5817
+ const globalConfig = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
4972
5818
  console.log(
4973
- ` Local: ${import_fs6.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
5819
+ ` Local: ${import_fs7.default.existsSync(projectConfig) ? import_chalk6.default.green("Active (node9.config.json)") : import_chalk6.default.gray("Not present")}`
4974
5820
  );
4975
5821
  console.log(
4976
- ` Global: ${import_fs6.default.existsSync(globalConfig) ? import_chalk5.default.green("Active (~/.node9/config.json)") : import_chalk5.default.gray("Not present")}`
5822
+ ` Global: ${import_fs7.default.existsSync(globalConfig) ? import_chalk6.default.green("Active (~/.node9/config.json)") : import_chalk6.default.gray("Not present")}`
4977
5823
  );
4978
5824
  if (mergedConfig.policy.sandboxPaths.length > 0) {
4979
5825
  console.log(
4980
- ` Sandbox: ${import_chalk5.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
5826
+ ` Sandbox: ${import_chalk6.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
4981
5827
  );
4982
5828
  }
4983
5829
  const pauseState = checkPause();
@@ -4985,47 +5831,63 @@ program.command("status").description("Show current Node9 mode, policy source, a
4985
5831
  const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
4986
5832
  console.log("");
4987
5833
  console.log(
4988
- import_chalk5.default.yellow(` \u23F8 PAUSED until ${expiresAt}`) + import_chalk5.default.gray(" \u2014 all tool calls allowed")
5834
+ import_chalk6.default.yellow(` \u23F8 PAUSED until ${expiresAt}`) + import_chalk6.default.gray(" \u2014 all tool calls allowed")
4989
5835
  );
4990
5836
  }
4991
5837
  console.log("");
4992
5838
  });
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(
5839
+ program.command("daemon").description("Run the local approval server").argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").option(
5840
+ "-w, --watch",
5841
+ "Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
5842
+ ).action(
4994
5843
  async (action, options) => {
4995
5844
  const cmd = (action ?? "start").toLowerCase();
4996
5845
  if (cmd === "stop") return stopDaemon();
4997
5846
  if (cmd === "status") return daemonStatus();
4998
5847
  if (cmd !== "start" && action !== void 0) {
4999
- console.error(import_chalk5.default.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
5848
+ console.error(import_chalk6.default.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
5000
5849
  process.exit(1);
5001
5850
  }
5851
+ if (options.watch) {
5852
+ process.env.NODE9_WATCH_MODE = "1";
5853
+ setTimeout(() => {
5854
+ openBrowserLocal();
5855
+ console.log(import_chalk6.default.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5856
+ }, 600);
5857
+ startDaemon();
5858
+ return;
5859
+ }
5002
5860
  if (options.openui) {
5003
5861
  if (isDaemonRunning()) {
5004
5862
  openBrowserLocal();
5005
- console.log(import_chalk5.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5863
+ console.log(import_chalk6.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5006
5864
  process.exit(0);
5007
5865
  }
5008
- const child = (0, import_child_process4.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
5866
+ const child = (0, import_child_process5.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
5009
5867
  child.unref();
5010
5868
  for (let i = 0; i < 12; i++) {
5011
5869
  await new Promise((r) => setTimeout(r, 250));
5012
5870
  if (isDaemonRunning()) break;
5013
5871
  }
5014
5872
  openBrowserLocal();
5015
- console.log(import_chalk5.default.green(`
5873
+ console.log(import_chalk6.default.green(`
5016
5874
  \u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
5017
5875
  process.exit(0);
5018
5876
  }
5019
5877
  if (options.background) {
5020
- const child = (0, import_child_process4.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
5878
+ const child = (0, import_child_process5.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
5021
5879
  child.unref();
5022
- console.log(import_chalk5.default.green(`
5880
+ console.log(import_chalk6.default.green(`
5023
5881
  \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
5024
5882
  process.exit(0);
5025
5883
  }
5026
5884
  startDaemon();
5027
5885
  }
5028
5886
  );
5887
+ program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).action(async (options) => {
5888
+ const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
5889
+ await startTail2(options);
5890
+ });
5029
5891
  program.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
5030
5892
  const processPayload = async (raw) => {
5031
5893
  try {
@@ -5036,9 +5898,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
5036
5898
  } catch (err) {
5037
5899
  const tempConfig = getConfig();
5038
5900
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
5039
- const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
5901
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
5040
5902
  const errMsg = err instanceof Error ? err.message : String(err);
5041
- import_fs6.default.appendFileSync(
5903
+ import_fs7.default.appendFileSync(
5042
5904
  logPath,
5043
5905
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
5044
5906
  RAW: ${raw}
@@ -5056,10 +5918,10 @@ RAW: ${raw}
5056
5918
  }
5057
5919
  const config = getConfig();
5058
5920
  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}
5921
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
5922
+ if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
5923
+ import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
5924
+ import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
5063
5925
  `);
5064
5926
  }
5065
5927
  const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
@@ -5071,18 +5933,18 @@ RAW: ${raw}
5071
5933
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
5072
5934
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
5073
5935
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
5074
- console.error(import_chalk5.default.bgRed.white.bold(`
5936
+ console.error(import_chalk6.default.bgRed.white.bold(`
5075
5937
  \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!`));
5938
+ console.error(import_chalk6.default.red.bold(` A sensitive secret was found in the tool arguments!`));
5077
5939
  } else {
5078
- console.error(import_chalk5.default.red(`
5940
+ console.error(import_chalk6.default.red(`
5079
5941
  \u{1F6D1} Node9 blocked "${toolName}"`));
5080
5942
  }
5081
- console.error(import_chalk5.default.gray(` Triggered by: ${blockedByContext}`));
5082
- if (result2?.changeHint) console.error(import_chalk5.default.cyan(` To change: ${result2.changeHint}`));
5943
+ console.error(import_chalk6.default.gray(` Triggered by: ${blockedByContext}`));
5944
+ if (result2?.changeHint) console.error(import_chalk6.default.cyan(` To change: ${result2.changeHint}`));
5083
5945
  console.error("");
5084
5946
  const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
5085
- console.error(import_chalk5.default.dim(` (Detailed instructions sent to AI agent)`));
5947
+ console.error(import_chalk6.default.dim(` (Detailed instructions sent to AI agent)`));
5086
5948
  process.stdout.write(
5087
5949
  JSON.stringify({
5088
5950
  decision: "block",
@@ -5113,7 +5975,7 @@ RAW: ${raw}
5113
5975
  process.exit(0);
5114
5976
  }
5115
5977
  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..."));
5978
+ console.error(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
5117
5979
  const daemonReady = await autoStartDaemonAndWait();
5118
5980
  if (daemonReady) {
5119
5981
  const retry = await authorizeHeadless(toolName, toolInput, false, meta);
@@ -5136,9 +5998,9 @@ RAW: ${raw}
5136
5998
  });
5137
5999
  } catch (err) {
5138
6000
  if (process.env.NODE9_DEBUG === "1") {
5139
- const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
6001
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
5140
6002
  const errMsg = err instanceof Error ? err.message : String(err);
5141
- import_fs6.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
6003
+ import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
5142
6004
  `);
5143
6005
  }
5144
6006
  process.exit(0);
@@ -5183,10 +6045,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
5183
6045
  decision: "allowed",
5184
6046
  source: "post-hook"
5185
6047
  };
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");
6048
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
6049
+ if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
6050
+ import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
6051
+ import_fs7.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
5190
6052
  const config = getConfig();
5191
6053
  if (shouldSnapshot(tool, {}, config)) {
5192
6054
  await createShadowSnapshot();
@@ -5213,7 +6075,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
5213
6075
  const ms = parseDuration(options.duration);
5214
6076
  if (ms === null) {
5215
6077
  console.error(
5216
- import_chalk5.default.red(`
6078
+ import_chalk6.default.red(`
5217
6079
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
5218
6080
  `)
5219
6081
  );
@@ -5221,20 +6083,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
5221
6083
  }
5222
6084
  pauseNode9(ms, options.duration);
5223
6085
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
5224
- console.log(import_chalk5.default.yellow(`
6086
+ console.log(import_chalk6.default.yellow(`
5225
6087
  \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.
6088
+ console.log(import_chalk6.default.gray(` All tool calls will be allowed without review.`));
6089
+ console.log(import_chalk6.default.gray(` Run "node9 resume" to re-enable early.
5228
6090
  `));
5229
6091
  });
5230
6092
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
5231
6093
  const { paused } = checkPause();
5232
6094
  if (!paused) {
5233
- console.log(import_chalk5.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
6095
+ console.log(import_chalk6.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
5234
6096
  return;
5235
6097
  }
5236
6098
  resumeNode9();
5237
- console.log(import_chalk5.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
6099
+ console.log(import_chalk6.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
5238
6100
  });
5239
6101
  var HOOK_BASED_AGENTS = {
5240
6102
  claude: "claude",
@@ -5247,15 +6109,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
5247
6109
  if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
5248
6110
  const target = HOOK_BASED_AGENTS[firstArg];
5249
6111
  console.error(
5250
- import_chalk5.default.yellow(`
6112
+ import_chalk6.default.yellow(`
5251
6113
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
5252
6114
  );
5253
- console.error(import_chalk5.default.white(`
6115
+ console.error(import_chalk6.default.white(`
5254
6116
  "${target}" uses its own hook system. Use:`));
5255
6117
  console.error(
5256
- import_chalk5.default.green(` node9 addto ${target} `) + import_chalk5.default.gray("# one-time setup")
6118
+ import_chalk6.default.green(` node9 addto ${target} `) + import_chalk6.default.gray("# one-time setup")
5257
6119
  );
5258
- console.error(import_chalk5.default.green(` ${target} `) + import_chalk5.default.gray("# run normally"));
6120
+ console.error(import_chalk6.default.green(` ${target} `) + import_chalk6.default.gray("# run normally"));
5259
6121
  process.exit(1);
5260
6122
  }
5261
6123
  const fullCommand = commandArgs.join(" ");
@@ -5263,7 +6125,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
5263
6125
  agent: "Terminal"
5264
6126
  });
5265
6127
  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..."));
6128
+ console.error(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
5267
6129
  const daemonReady = await autoStartDaemonAndWait();
5268
6130
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
5269
6131
  }
@@ -5272,12 +6134,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
5272
6134
  }
5273
6135
  if (!result.approved) {
5274
6136
  console.error(
5275
- import_chalk5.default.red(`
6137
+ import_chalk6.default.red(`
5276
6138
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
5277
6139
  );
5278
6140
  process.exit(1);
5279
6141
  }
5280
- console.error(import_chalk5.default.green("\n\u2705 Approved \u2014 running command...\n"));
6142
+ console.error(import_chalk6.default.green("\n\u2705 Approved \u2014 running command...\n"));
5281
6143
  await runProxy(fullCommand);
5282
6144
  } else {
5283
6145
  program.help();
@@ -5292,22 +6154,22 @@ program.command("undo").description(
5292
6154
  if (history.length === 0) {
5293
6155
  if (!options.all && allHistory.length > 0) {
5294
6156
  console.log(
5295
- import_chalk5.default.yellow(
6157
+ import_chalk6.default.yellow(
5296
6158
  `
5297
6159
  \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.
6160
+ Run ${import_chalk6.default.cyan("node9 undo --all")} to see snapshots from all projects.
5299
6161
  `
5300
6162
  )
5301
6163
  );
5302
6164
  } else {
5303
- console.log(import_chalk5.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
6165
+ console.log(import_chalk6.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
5304
6166
  }
5305
6167
  return;
5306
6168
  }
5307
6169
  const idx = history.length - steps;
5308
6170
  if (idx < 0) {
5309
6171
  console.log(
5310
- import_chalk5.default.yellow(
6172
+ import_chalk6.default.yellow(
5311
6173
  `
5312
6174
  \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
5313
6175
  `
@@ -5318,18 +6180,18 @@ program.command("undo").description(
5318
6180
  const snapshot = history[idx];
5319
6181
  const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
5320
6182
  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(`
6183
+ console.log(import_chalk6.default.magenta.bold(`
5322
6184
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
5323
6185
  console.log(
5324
- import_chalk5.default.white(
5325
- ` Tool: ${import_chalk5.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk5.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
6186
+ import_chalk6.default.white(
6187
+ ` Tool: ${import_chalk6.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk6.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
5326
6188
  )
5327
6189
  );
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)}`));
6190
+ console.log(import_chalk6.default.white(` When: ${import_chalk6.default.gray(ageStr)}`));
6191
+ console.log(import_chalk6.default.white(` Dir: ${import_chalk6.default.gray(snapshot.cwd)}`));
5330
6192
  if (steps > 1)
5331
6193
  console.log(
5332
- import_chalk5.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
6194
+ import_chalk6.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
5333
6195
  );
5334
6196
  console.log("");
5335
6197
  const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
@@ -5337,21 +6199,21 @@ program.command("undo").description(
5337
6199
  const lines = diff.split("\n");
5338
6200
  for (const line of lines) {
5339
6201
  if (line.startsWith("+++") || line.startsWith("---")) {
5340
- console.log(import_chalk5.default.bold(line));
6202
+ console.log(import_chalk6.default.bold(line));
5341
6203
  } else if (line.startsWith("+")) {
5342
- console.log(import_chalk5.default.green(line));
6204
+ console.log(import_chalk6.default.green(line));
5343
6205
  } else if (line.startsWith("-")) {
5344
- console.log(import_chalk5.default.red(line));
6206
+ console.log(import_chalk6.default.red(line));
5345
6207
  } else if (line.startsWith("@@")) {
5346
- console.log(import_chalk5.default.cyan(line));
6208
+ console.log(import_chalk6.default.cyan(line));
5347
6209
  } else {
5348
- console.log(import_chalk5.default.gray(line));
6210
+ console.log(import_chalk6.default.gray(line));
5349
6211
  }
5350
6212
  }
5351
6213
  console.log("");
5352
6214
  } else {
5353
6215
  console.log(
5354
- import_chalk5.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
6216
+ import_chalk6.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
5355
6217
  );
5356
6218
  }
5357
6219
  const proceed = await (0, import_prompts3.confirm)({
@@ -5360,42 +6222,42 @@ program.command("undo").description(
5360
6222
  });
5361
6223
  if (proceed) {
5362
6224
  if (applyUndo(snapshot.hash, snapshot.cwd)) {
5363
- console.log(import_chalk5.default.green("\n\u2705 Reverted successfully.\n"));
6225
+ console.log(import_chalk6.default.green("\n\u2705 Reverted successfully.\n"));
5364
6226
  } else {
5365
- console.error(import_chalk5.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
6227
+ console.error(import_chalk6.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
5366
6228
  }
5367
6229
  } else {
5368
- console.log(import_chalk5.default.gray("\nCancelled.\n"));
6230
+ console.log(import_chalk6.default.gray("\nCancelled.\n"));
5369
6231
  }
5370
6232
  });
5371
6233
  var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
5372
6234
  shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
5373
6235
  const name = resolveShieldName(service);
5374
6236
  if (!name) {
5375
- console.error(import_chalk5.default.red(`
6237
+ console.error(import_chalk6.default.red(`
5376
6238
  \u274C Unknown shield: "${service}"
5377
6239
  `));
5378
- console.log(`Run ${import_chalk5.default.cyan("node9 shield list")} to see available shields.
6240
+ console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
5379
6241
  `);
5380
6242
  process.exit(1);
5381
6243
  }
5382
6244
  const shield = getShield(name);
5383
6245
  const active = readActiveShields();
5384
6246
  if (active.includes(name)) {
5385
- console.log(import_chalk5.default.yellow(`
6247
+ console.log(import_chalk6.default.yellow(`
5386
6248
  \u2139\uFE0F Shield "${name}" is already active.
5387
6249
  `));
5388
6250
  return;
5389
6251
  }
5390
6252
  writeActiveShields([...active, name]);
5391
- console.log(import_chalk5.default.green(`
6253
+ console.log(import_chalk6.default.green(`
5392
6254
  \u{1F6E1}\uFE0F Shield "${name}" enabled.`));
5393
- console.log(import_chalk5.default.gray(` ${shield.smartRules.length} smart rules now active.`));
6255
+ console.log(import_chalk6.default.gray(` ${shield.smartRules.length} smart rules now active.`));
5394
6256
  if (shield.dangerousWords.length > 0)
5395
- console.log(import_chalk5.default.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
6257
+ console.log(import_chalk6.default.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
5396
6258
  if (name === "filesystem") {
5397
6259
  console.log(
5398
- import_chalk5.default.yellow(
6260
+ import_chalk6.default.yellow(
5399
6261
  `
5400
6262
  \u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
5401
6263
  Tools like unlink, find -delete, or language-level file ops are not intercepted.`
@@ -5407,51 +6269,51 @@ shieldCmd.command("enable <service>").description("Enable a security shield for
5407
6269
  shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
5408
6270
  const name = resolveShieldName(service);
5409
6271
  if (!name) {
5410
- console.error(import_chalk5.default.red(`
6272
+ console.error(import_chalk6.default.red(`
5411
6273
  \u274C Unknown shield: "${service}"
5412
6274
  `));
5413
- console.log(`Run ${import_chalk5.default.cyan("node9 shield list")} to see available shields.
6275
+ console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
5414
6276
  `);
5415
6277
  process.exit(1);
5416
6278
  }
5417
6279
  const active = readActiveShields();
5418
6280
  if (!active.includes(name)) {
5419
- console.log(import_chalk5.default.yellow(`
6281
+ console.log(import_chalk6.default.yellow(`
5420
6282
  \u2139\uFE0F Shield "${name}" is not active.
5421
6283
  `));
5422
6284
  return;
5423
6285
  }
5424
6286
  writeActiveShields(active.filter((s) => s !== name));
5425
- console.log(import_chalk5.default.green(`
6287
+ console.log(import_chalk6.default.green(`
5426
6288
  \u{1F6E1}\uFE0F Shield "${name}" disabled.
5427
6289
  `));
5428
6290
  });
5429
6291
  shieldCmd.command("list").description("Show all available shields").action(() => {
5430
6292
  const active = new Set(readActiveShields());
5431
- console.log(import_chalk5.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
6293
+ console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
5432
6294
  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}`);
6295
+ const status = active.has(shield.name) ? import_chalk6.default.green("\u25CF enabled") : import_chalk6.default.gray("\u25CB disabled");
6296
+ console.log(` ${status} ${import_chalk6.default.cyan(shield.name.padEnd(12))} ${shield.description}`);
5435
6297
  if (shield.aliases.length > 0)
5436
- console.log(import_chalk5.default.gray(` aliases: ${shield.aliases.join(", ")}`));
6298
+ console.log(import_chalk6.default.gray(` aliases: ${shield.aliases.join(", ")}`));
5437
6299
  }
5438
6300
  console.log("");
5439
6301
  });
5440
6302
  shieldCmd.command("status").description("Show which shields are currently active").action(() => {
5441
6303
  const active = readActiveShields();
5442
6304
  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.
6305
+ console.log(import_chalk6.default.yellow("\n\u2139\uFE0F No shields are active.\n"));
6306
+ console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
5445
6307
  `);
5446
6308
  return;
5447
6309
  }
5448
- console.log(import_chalk5.default.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
6310
+ console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
5449
6311
  for (const name of active) {
5450
6312
  const shield = getShield(name);
5451
6313
  if (!shield) continue;
5452
- console.log(` ${import_chalk5.default.green("\u25CF")} ${import_chalk5.default.cyan(name)}`);
6314
+ console.log(` ${import_chalk6.default.green("\u25CF")} ${import_chalk6.default.cyan(name)}`);
5453
6315
  console.log(
5454
- import_chalk5.default.gray(
6316
+ import_chalk6.default.gray(
5455
6317
  ` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
5456
6318
  )
5457
6319
  );
@@ -5462,9 +6324,9 @@ process.on("unhandledRejection", (reason) => {
5462
6324
  const isCheckHook = process.argv[2] === "check";
5463
6325
  if (isCheckHook) {
5464
6326
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
5465
- const logPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log");
6327
+ const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
5466
6328
  const msg = reason instanceof Error ? reason.message : String(reason);
5467
- import_fs6.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
6329
+ import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
5468
6330
  `);
5469
6331
  }
5470
6332
  process.exit(0);