@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.mjs CHANGED
@@ -1,21 +1,13 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/cli.ts
4
- import { Command } from "commander";
5
-
6
- // src/core.ts
7
- import chalk2 from "chalk";
8
- import { confirm } from "@inquirer/prompts";
9
- import fs2 from "fs";
10
- import path4 from "path";
11
- import os2 from "os";
12
- import pm from "picomatch";
13
- import { parse } from "sh-syntax";
14
-
15
- // src/ui/native.ts
16
- import { spawn } from "child_process";
17
- import path2 from "path";
18
- import chalk from "chalk";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
19
11
 
20
12
  // src/context-sniper.ts
21
13
  import path from "path";
@@ -48,22 +40,6 @@ function extractContext(text, matchedWord) {
48
40
  ... [${lines.length - end} lines hidden] ...` : "";
49
41
  return { snippet: `${head}${snippet}${tail}`, lineIndex };
50
42
  }
51
- var CODE_KEYS = [
52
- "command",
53
- "cmd",
54
- "shell_command",
55
- "bash_command",
56
- "script",
57
- "code",
58
- "input",
59
- "sql",
60
- "query",
61
- "arguments",
62
- "args",
63
- "param",
64
- "params",
65
- "text"
66
- ];
67
43
  function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
68
44
  let intent = "EXEC";
69
45
  let contextSnippet;
@@ -118,11 +94,33 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
118
94
  ...ruleName && { ruleName }
119
95
  };
120
96
  }
97
+ var CODE_KEYS;
98
+ var init_context_sniper = __esm({
99
+ "src/context-sniper.ts"() {
100
+ "use strict";
101
+ CODE_KEYS = [
102
+ "command",
103
+ "cmd",
104
+ "shell_command",
105
+ "bash_command",
106
+ "script",
107
+ "code",
108
+ "input",
109
+ "sql",
110
+ "query",
111
+ "arguments",
112
+ "args",
113
+ "param",
114
+ "params",
115
+ "text"
116
+ ];
117
+ }
118
+ });
121
119
 
122
120
  // src/ui/native.ts
123
- var isTestEnv = () => {
124
- return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
125
- };
121
+ import { spawn } from "child_process";
122
+ import path2 from "path";
123
+ import chalk from "chalk";
126
124
  function formatArgs(args, matchedField, matchedWord) {
127
125
  if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
128
126
  let parsed = args;
@@ -332,81 +330,19 @@ end run`;
332
330
  }
333
331
  });
334
332
  }
333
+ var isTestEnv;
334
+ var init_native = __esm({
335
+ "src/ui/native.ts"() {
336
+ "use strict";
337
+ init_context_sniper();
338
+ isTestEnv = () => {
339
+ return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
340
+ };
341
+ }
342
+ });
335
343
 
336
344
  // src/config-schema.ts
337
345
  import { z } from "zod";
338
- var noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
339
- message: "Value must not contain literal newline characters (use \\n instead)"
340
- });
341
- var SmartConditionSchema = z.object({
342
- field: z.string().min(1, "Condition field must not be empty"),
343
- op: z.enum(
344
- [
345
- "matches",
346
- "notMatches",
347
- "contains",
348
- "notContains",
349
- "exists",
350
- "notExists",
351
- "matchesGlob",
352
- "notMatchesGlob"
353
- ],
354
- {
355
- errorMap: () => ({
356
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
357
- })
358
- }
359
- ),
360
- value: z.string().optional(),
361
- flags: z.string().optional()
362
- });
363
- var SmartRuleSchema = z.object({
364
- name: z.string().optional(),
365
- tool: z.string().min(1, "Smart rule tool must not be empty"),
366
- conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
367
- conditionMode: z.enum(["all", "any"]).optional(),
368
- verdict: z.enum(["allow", "review", "block"], {
369
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
370
- }),
371
- reason: z.string().optional()
372
- });
373
- var ConfigFileSchema = z.object({
374
- version: z.string().optional(),
375
- settings: z.object({
376
- mode: z.enum(["standard", "strict", "audit"]).optional(),
377
- autoStartDaemon: z.boolean().optional(),
378
- enableUndo: z.boolean().optional(),
379
- enableHookLogDebug: z.boolean().optional(),
380
- approvalTimeoutMs: z.number().nonnegative().optional(),
381
- approvers: z.object({
382
- native: z.boolean().optional(),
383
- browser: z.boolean().optional(),
384
- cloud: z.boolean().optional(),
385
- terminal: z.boolean().optional()
386
- }).optional(),
387
- environment: z.string().optional(),
388
- slackEnabled: z.boolean().optional(),
389
- enableTrustSessions: z.boolean().optional(),
390
- allowGlobalPause: z.boolean().optional()
391
- }).optional(),
392
- policy: z.object({
393
- sandboxPaths: z.array(z.string()).optional(),
394
- dangerousWords: z.array(noNewlines).optional(),
395
- ignoredTools: z.array(z.string()).optional(),
396
- toolInspection: z.record(z.string()).optional(),
397
- smartRules: z.array(SmartRuleSchema).optional(),
398
- snapshot: z.object({
399
- tools: z.array(z.string()).optional(),
400
- onlyPaths: z.array(z.string()).optional(),
401
- ignorePaths: z.array(z.string()).optional()
402
- }).optional(),
403
- dlp: z.object({
404
- enabled: z.boolean().optional(),
405
- scanIgnoredTools: z.boolean().optional()
406
- }).optional()
407
- }).optional(),
408
- environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
409
- }).strict({ message: "Config contains unknown top-level keys" });
410
346
  function sanitizeConfig(raw) {
411
347
  const result = ConfigFileSchema.safeParse(raw);
412
348
  if (result.success) {
@@ -424,8 +360,8 @@ function sanitizeConfig(raw) {
424
360
  }
425
361
  }
426
362
  const lines = result.error.issues.map((issue) => {
427
- const path9 = issue.path.length > 0 ? issue.path.join(".") : "root";
428
- return ` \u2022 ${path9}: ${issue.message}`;
363
+ const path10 = issue.path.length > 0 ? issue.path.join(".") : "root";
364
+ return ` \u2022 ${path10}: ${issue.message}`;
429
365
  });
430
366
  return {
431
367
  sanitized,
@@ -433,179 +369,90 @@ function sanitizeConfig(raw) {
433
369
  ${lines.join("\n")}`
434
370
  };
435
371
  }
372
+ var noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
373
+ var init_config_schema = __esm({
374
+ "src/config-schema.ts"() {
375
+ "use strict";
376
+ noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
377
+ message: "Value must not contain literal newline characters (use \\n instead)"
378
+ });
379
+ SmartConditionSchema = z.object({
380
+ field: z.string().min(1, "Condition field must not be empty"),
381
+ op: z.enum(
382
+ [
383
+ "matches",
384
+ "notMatches",
385
+ "contains",
386
+ "notContains",
387
+ "exists",
388
+ "notExists",
389
+ "matchesGlob",
390
+ "notMatchesGlob"
391
+ ],
392
+ {
393
+ errorMap: () => ({
394
+ message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
395
+ })
396
+ }
397
+ ),
398
+ value: z.string().optional(),
399
+ flags: z.string().optional()
400
+ });
401
+ SmartRuleSchema = z.object({
402
+ name: z.string().optional(),
403
+ tool: z.string().min(1, "Smart rule tool must not be empty"),
404
+ conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
405
+ conditionMode: z.enum(["all", "any"]).optional(),
406
+ verdict: z.enum(["allow", "review", "block"], {
407
+ errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
408
+ }),
409
+ reason: z.string().optional()
410
+ });
411
+ ConfigFileSchema = z.object({
412
+ version: z.string().optional(),
413
+ settings: z.object({
414
+ mode: z.enum(["standard", "strict", "audit"]).optional(),
415
+ autoStartDaemon: z.boolean().optional(),
416
+ enableUndo: z.boolean().optional(),
417
+ enableHookLogDebug: z.boolean().optional(),
418
+ approvalTimeoutMs: z.number().nonnegative().optional(),
419
+ approvers: z.object({
420
+ native: z.boolean().optional(),
421
+ browser: z.boolean().optional(),
422
+ cloud: z.boolean().optional(),
423
+ terminal: z.boolean().optional()
424
+ }).optional(),
425
+ environment: z.string().optional(),
426
+ slackEnabled: z.boolean().optional(),
427
+ enableTrustSessions: z.boolean().optional(),
428
+ allowGlobalPause: z.boolean().optional()
429
+ }).optional(),
430
+ policy: z.object({
431
+ sandboxPaths: z.array(z.string()).optional(),
432
+ dangerousWords: z.array(noNewlines).optional(),
433
+ ignoredTools: z.array(z.string()).optional(),
434
+ toolInspection: z.record(z.string()).optional(),
435
+ smartRules: z.array(SmartRuleSchema).optional(),
436
+ snapshot: z.object({
437
+ tools: z.array(z.string()).optional(),
438
+ onlyPaths: z.array(z.string()).optional(),
439
+ ignorePaths: z.array(z.string()).optional()
440
+ }).optional(),
441
+ dlp: z.object({
442
+ enabled: z.boolean().optional(),
443
+ scanIgnoredTools: z.boolean().optional()
444
+ }).optional()
445
+ }).optional(),
446
+ environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
447
+ }).strict({ message: "Config contains unknown top-level keys" });
448
+ }
449
+ });
436
450
 
437
451
  // src/shields.ts
438
452
  import fs from "fs";
439
453
  import path3 from "path";
440
454
  import os from "os";
441
455
  import crypto from "crypto";
442
- var SHIELDS = {
443
- postgres: {
444
- name: "postgres",
445
- description: "Protects PostgreSQL databases from destructive AI operations",
446
- aliases: ["pg", "postgresql"],
447
- smartRules: [
448
- {
449
- name: "shield:postgres:block-drop-table",
450
- tool: "*",
451
- conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
452
- verdict: "block",
453
- reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
454
- },
455
- {
456
- name: "shield:postgres:block-truncate",
457
- tool: "*",
458
- conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
459
- verdict: "block",
460
- reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
461
- },
462
- {
463
- name: "shield:postgres:block-drop-column",
464
- tool: "*",
465
- conditions: [
466
- { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
467
- ],
468
- verdict: "block",
469
- reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
470
- },
471
- {
472
- name: "shield:postgres:review-grant-revoke",
473
- tool: "*",
474
- conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
475
- verdict: "review",
476
- reason: "Permission changes require human approval (Postgres shield)"
477
- }
478
- ],
479
- dangerousWords: ["dropdb", "pg_dropcluster"]
480
- },
481
- github: {
482
- name: "github",
483
- description: "Protects GitHub repositories from destructive AI operations",
484
- aliases: ["git"],
485
- smartRules: [
486
- {
487
- // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
488
- // This rule adds coverage for `git push --delete` which the built-in does not match.
489
- name: "shield:github:review-delete-branch-remote",
490
- tool: "bash",
491
- conditions: [
492
- {
493
- field: "command",
494
- op: "matches",
495
- value: "git\\s+push\\s+.*--delete",
496
- flags: "i"
497
- }
498
- ],
499
- verdict: "review",
500
- reason: "Remote branch deletion requires human approval (GitHub shield)"
501
- },
502
- {
503
- name: "shield:github:block-delete-repo",
504
- tool: "*",
505
- conditions: [
506
- { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
507
- ],
508
- verdict: "block",
509
- reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
510
- }
511
- ],
512
- dangerousWords: []
513
- },
514
- aws: {
515
- name: "aws",
516
- description: "Protects AWS infrastructure from destructive AI operations",
517
- aliases: ["amazon"],
518
- smartRules: [
519
- {
520
- name: "shield:aws:block-delete-s3-bucket",
521
- tool: "*",
522
- conditions: [
523
- {
524
- field: "command",
525
- op: "matches",
526
- value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
527
- flags: "i"
528
- }
529
- ],
530
- verdict: "block",
531
- reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
532
- },
533
- {
534
- name: "shield:aws:review-iam-changes",
535
- tool: "*",
536
- conditions: [
537
- {
538
- field: "command",
539
- op: "matches",
540
- value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
541
- flags: "i"
542
- }
543
- ],
544
- verdict: "review",
545
- reason: "IAM changes require human approval (AWS shield)"
546
- },
547
- {
548
- name: "shield:aws:block-ec2-terminate",
549
- tool: "*",
550
- conditions: [
551
- {
552
- field: "command",
553
- op: "matches",
554
- value: "aws\\s+ec2\\s+terminate-instances",
555
- flags: "i"
556
- }
557
- ],
558
- verdict: "block",
559
- reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
560
- },
561
- {
562
- name: "shield:aws:review-rds-delete",
563
- tool: "*",
564
- conditions: [
565
- { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
566
- ],
567
- verdict: "review",
568
- reason: "RDS deletion requires human approval (AWS shield)"
569
- }
570
- ],
571
- dangerousWords: []
572
- },
573
- filesystem: {
574
- name: "filesystem",
575
- description: "Protects the local filesystem from dangerous AI operations",
576
- aliases: ["fs"],
577
- smartRules: [
578
- {
579
- name: "shield:filesystem:review-chmod-777",
580
- tool: "bash",
581
- conditions: [
582
- { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
583
- ],
584
- verdict: "review",
585
- reason: "chmod 777 requires human approval (filesystem shield)"
586
- },
587
- {
588
- name: "shield:filesystem:review-write-etc",
589
- tool: "bash",
590
- conditions: [
591
- {
592
- field: "command",
593
- // Narrow to write-indicative operations to avoid approval fatigue on reads.
594
- // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
595
- op: "matches",
596
- value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
597
- }
598
- ],
599
- verdict: "review",
600
- reason: "Writing to /etc requires human approval (filesystem shield)"
601
- }
602
- ],
603
- // dd removed: too common as a legitimate tool (disk imaging, file ops).
604
- // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
605
- // wipefs retained: rarely legitimate in an agent context and not in built-ins.
606
- dangerousWords: ["wipefs"]
607
- }
608
- };
609
456
  function resolveShieldName(input) {
610
457
  const lower = input.toLowerCase();
611
458
  if (SHIELDS[lower]) return lower;
@@ -621,7 +468,6 @@ function getShield(name) {
621
468
  function listShields() {
622
469
  return Object.values(SHIELDS);
623
470
  }
624
- var SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
625
471
  function readActiveShields() {
626
472
  try {
627
473
  const raw = fs.readFileSync(SHIELDS_STATE_FILE, "utf-8");
@@ -646,21 +492,182 @@ function writeActiveShields(active) {
646
492
  fs.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
647
493
  fs.renameSync(tmp, SHIELDS_STATE_FILE);
648
494
  }
495
+ var SHIELDS, SHIELDS_STATE_FILE;
496
+ var init_shields = __esm({
497
+ "src/shields.ts"() {
498
+ "use strict";
499
+ SHIELDS = {
500
+ postgres: {
501
+ name: "postgres",
502
+ description: "Protects PostgreSQL databases from destructive AI operations",
503
+ aliases: ["pg", "postgresql"],
504
+ smartRules: [
505
+ {
506
+ name: "shield:postgres:block-drop-table",
507
+ tool: "*",
508
+ conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
509
+ verdict: "block",
510
+ reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
511
+ },
512
+ {
513
+ name: "shield:postgres:block-truncate",
514
+ tool: "*",
515
+ conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
516
+ verdict: "block",
517
+ reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
518
+ },
519
+ {
520
+ name: "shield:postgres:block-drop-column",
521
+ tool: "*",
522
+ conditions: [
523
+ { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
524
+ ],
525
+ verdict: "block",
526
+ reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
527
+ },
528
+ {
529
+ name: "shield:postgres:review-grant-revoke",
530
+ tool: "*",
531
+ conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
532
+ verdict: "review",
533
+ reason: "Permission changes require human approval (Postgres shield)"
534
+ }
535
+ ],
536
+ dangerousWords: ["dropdb", "pg_dropcluster"]
537
+ },
538
+ github: {
539
+ name: "github",
540
+ description: "Protects GitHub repositories from destructive AI operations",
541
+ aliases: ["git"],
542
+ smartRules: [
543
+ {
544
+ // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
545
+ // This rule adds coverage for `git push --delete` which the built-in does not match.
546
+ name: "shield:github:review-delete-branch-remote",
547
+ tool: "bash",
548
+ conditions: [
549
+ {
550
+ field: "command",
551
+ op: "matches",
552
+ value: "git\\s+push\\s+.*--delete",
553
+ flags: "i"
554
+ }
555
+ ],
556
+ verdict: "review",
557
+ reason: "Remote branch deletion requires human approval (GitHub shield)"
558
+ },
559
+ {
560
+ name: "shield:github:block-delete-repo",
561
+ tool: "*",
562
+ conditions: [
563
+ { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
564
+ ],
565
+ verdict: "block",
566
+ reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
567
+ }
568
+ ],
569
+ dangerousWords: []
570
+ },
571
+ aws: {
572
+ name: "aws",
573
+ description: "Protects AWS infrastructure from destructive AI operations",
574
+ aliases: ["amazon"],
575
+ smartRules: [
576
+ {
577
+ name: "shield:aws:block-delete-s3-bucket",
578
+ tool: "*",
579
+ conditions: [
580
+ {
581
+ field: "command",
582
+ op: "matches",
583
+ value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
584
+ flags: "i"
585
+ }
586
+ ],
587
+ verdict: "block",
588
+ reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
589
+ },
590
+ {
591
+ name: "shield:aws:review-iam-changes",
592
+ tool: "*",
593
+ conditions: [
594
+ {
595
+ field: "command",
596
+ op: "matches",
597
+ value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
598
+ flags: "i"
599
+ }
600
+ ],
601
+ verdict: "review",
602
+ reason: "IAM changes require human approval (AWS shield)"
603
+ },
604
+ {
605
+ name: "shield:aws:block-ec2-terminate",
606
+ tool: "*",
607
+ conditions: [
608
+ {
609
+ field: "command",
610
+ op: "matches",
611
+ value: "aws\\s+ec2\\s+terminate-instances",
612
+ flags: "i"
613
+ }
614
+ ],
615
+ verdict: "block",
616
+ reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
617
+ },
618
+ {
619
+ name: "shield:aws:review-rds-delete",
620
+ tool: "*",
621
+ conditions: [
622
+ { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
623
+ ],
624
+ verdict: "review",
625
+ reason: "RDS deletion requires human approval (AWS shield)"
626
+ }
627
+ ],
628
+ dangerousWords: []
629
+ },
630
+ filesystem: {
631
+ name: "filesystem",
632
+ description: "Protects the local filesystem from dangerous AI operations",
633
+ aliases: ["fs"],
634
+ smartRules: [
635
+ {
636
+ name: "shield:filesystem:review-chmod-777",
637
+ tool: "bash",
638
+ conditions: [
639
+ { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
640
+ ],
641
+ verdict: "review",
642
+ reason: "chmod 777 requires human approval (filesystem shield)"
643
+ },
644
+ {
645
+ name: "shield:filesystem:review-write-etc",
646
+ tool: "bash",
647
+ conditions: [
648
+ {
649
+ field: "command",
650
+ // Narrow to write-indicative operations to avoid approval fatigue on reads.
651
+ // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
652
+ op: "matches",
653
+ value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
654
+ }
655
+ ],
656
+ verdict: "review",
657
+ reason: "Writing to /etc requires human approval (filesystem shield)"
658
+ }
659
+ ],
660
+ // dd removed: too common as a legitimate tool (disk imaging, file ops).
661
+ // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
662
+ // wipefs retained: rarely legitimate in an agent context and not in built-ins.
663
+ dangerousWords: ["wipefs"]
664
+ }
665
+ };
666
+ SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
667
+ }
668
+ });
649
669
 
650
670
  // src/dlp.ts
651
- var DLP_PATTERNS = [
652
- { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
653
- { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
654
- { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
655
- { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
656
- { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
657
- {
658
- name: "Private Key (PEM)",
659
- regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
660
- severity: "block"
661
- },
662
- { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
663
- ];
664
671
  function maskSecret(raw, pattern) {
665
672
  const match = raw.match(pattern);
666
673
  if (!match) return "****";
@@ -671,9 +678,6 @@ function maskSecret(raw, pattern) {
671
678
  const stars = "*".repeat(Math.min(secret.length - 8, 12));
672
679
  return `${prefix}${stars}${suffix}`;
673
680
  }
674
- var MAX_DEPTH = 5;
675
- var MAX_STRING_BYTES = 1e5;
676
- var MAX_JSON_PARSE_BYTES = 1e4;
677
681
  function scanArgs(args, depth = 0, fieldPath = "args") {
678
682
  if (depth > MAX_DEPTH || args === null || args === void 0) return null;
679
683
  if (Array.isArray(args)) {
@@ -716,12 +720,39 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
716
720
  }
717
721
  return null;
718
722
  }
723
+ var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
724
+ var init_dlp = __esm({
725
+ "src/dlp.ts"() {
726
+ "use strict";
727
+ DLP_PATTERNS = [
728
+ { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
729
+ { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
730
+ { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
731
+ { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
732
+ { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
733
+ {
734
+ name: "Private Key (PEM)",
735
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
736
+ severity: "block"
737
+ },
738
+ { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
739
+ ];
740
+ MAX_DEPTH = 5;
741
+ MAX_STRING_BYTES = 1e5;
742
+ MAX_JSON_PARSE_BYTES = 1e4;
743
+ }
744
+ });
719
745
 
720
746
  // src/core.ts
721
- var PAUSED_FILE = path4.join(os2.homedir(), ".node9", "PAUSED");
722
- var TRUST_FILE = path4.join(os2.homedir(), ".node9", "trust.json");
723
- var LOCAL_AUDIT_LOG = path4.join(os2.homedir(), ".node9", "audit.log");
724
- var HOOK_DEBUG_LOG = path4.join(os2.homedir(), ".node9", "hook-debug.log");
747
+ import chalk2 from "chalk";
748
+ import { confirm } from "@inquirer/prompts";
749
+ import fs2 from "fs";
750
+ import path4 from "path";
751
+ import os2 from "os";
752
+ import net from "net";
753
+ import { randomUUID } from "crypto";
754
+ import pm from "picomatch";
755
+ import { parse } from "sh-syntax";
725
756
  function checkPause() {
726
757
  try {
727
758
  if (!fs2.existsSync(PAUSED_FILE)) return { paused: false };
@@ -834,9 +865,9 @@ function matchesPattern(text, patterns) {
834
865
  const withoutDotSlash = text.replace(/^\.\//, "");
835
866
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
836
867
  }
837
- function getNestedValue(obj, path9) {
868
+ function getNestedValue(obj, path10) {
838
869
  if (!obj || typeof obj !== "object") return null;
839
- return path9.split(".").reduce((prev, curr) => prev?.[curr], obj);
870
+ return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
840
871
  }
841
872
  function shouldSnapshot(toolName, args, config) {
842
873
  if (!config.settings.enableUndo) return false;
@@ -906,7 +937,6 @@ function isSqlTool(toolName, toolInspection) {
906
937
  const fieldName = toolInspection[matchingPattern];
907
938
  return fieldName === "sql" || fieldName === "query";
908
939
  }
909
- var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
910
940
  async function analyzeShellCommand(command) {
911
941
  const actions = [];
912
942
  const paths = [];
@@ -988,228 +1018,26 @@ function redactSecrets(text) {
988
1018
  );
989
1019
  return redacted;
990
1020
  }
991
- var DANGEROUS_WORDS = [
992
- "mkfs",
993
- // formats/wipes a filesystem partition
994
- "shred"
995
- // permanently overwrites file contents (unrecoverable)
996
- ];
997
- var DEFAULT_CONFIG = {
998
- settings: {
999
- mode: "standard",
1000
- autoStartDaemon: true,
1001
- enableUndo: true,
1002
- // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
1003
- enableHookLogDebug: false,
1004
- approvalTimeoutMs: 0,
1005
- // 0 = disabled; set e.g. 30000 for 30-second auto-deny
1006
- approvers: { native: true, browser: true, cloud: true, terminal: true }
1007
- },
1008
- policy: {
1009
- sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
1010
- dangerousWords: DANGEROUS_WORDS,
1011
- ignoredTools: [
1012
- "list_*",
1013
- "get_*",
1014
- "read_*",
1015
- "describe_*",
1016
- "read",
1017
- "glob",
1018
- "grep",
1019
- "ls",
1020
- "notebookread",
1021
- "notebookedit",
1022
- "webfetch",
1023
- "websearch",
1024
- "exitplanmode",
1025
- "askuserquestion",
1026
- "agent",
1027
- "task*",
1028
- "toolsearch",
1029
- "mcp__ide__*",
1030
- "getDiagnostics"
1031
- ],
1032
- toolInspection: {
1033
- bash: "command",
1034
- shell: "command",
1035
- run_shell_command: "command",
1036
- "terminal.execute": "command",
1037
- "postgres:query": "sql"
1038
- },
1039
- snapshot: {
1040
- tools: [
1041
- "str_replace_based_edit_tool",
1042
- "write_file",
1043
- "edit_file",
1044
- "create_file",
1045
- "edit",
1046
- "replace"
1047
- ],
1048
- onlyPaths: [],
1049
- ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
1050
- },
1051
- smartRules: [
1052
- // ── rm safety (critical — always evaluated first) ──────────────────────
1053
- {
1054
- name: "block-rm-rf-home",
1055
- tool: "bash",
1056
- conditionMode: "all",
1057
- conditions: [
1058
- {
1059
- field: "command",
1060
- op: "matches",
1061
- value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
1062
- },
1063
- {
1064
- field: "command",
1065
- op: "matches",
1066
- value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
1067
- }
1068
- ],
1069
- verdict: "block",
1070
- reason: "Recursive delete of home directory is irreversible"
1071
- },
1072
- // ── SQL safety ────────────────────────────────────────────────────────
1073
- {
1074
- name: "no-delete-without-where",
1075
- tool: "*",
1076
- conditions: [
1077
- { field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
1078
- { field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
1079
- ],
1080
- conditionMode: "all",
1081
- verdict: "review",
1082
- reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
1083
- },
1084
- {
1085
- name: "review-drop-truncate-shell",
1086
- tool: "bash",
1087
- conditions: [
1088
- {
1089
- field: "command",
1090
- op: "matches",
1091
- value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
1092
- flags: "i"
1093
- }
1094
- ],
1095
- conditionMode: "all",
1096
- verdict: "review",
1097
- reason: "SQL DDL destructive statement inside a shell command"
1098
- },
1099
- // ── Git safety ────────────────────────────────────────────────────────
1100
- {
1101
- name: "block-force-push",
1102
- tool: "bash",
1103
- conditions: [
1104
- {
1105
- field: "command",
1106
- op: "matches",
1107
- value: "git push.*(--force|--force-with-lease|-f\\b)",
1108
- flags: "i"
1109
- }
1110
- ],
1111
- conditionMode: "all",
1112
- verdict: "block",
1113
- reason: "Force push overwrites remote history and cannot be undone"
1114
- },
1115
- {
1116
- name: "review-git-push",
1117
- tool: "bash",
1118
- conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
1119
- conditionMode: "all",
1120
- verdict: "review",
1121
- reason: "git push sends changes to a shared remote"
1122
- },
1123
- {
1124
- name: "review-git-destructive",
1125
- tool: "bash",
1126
- conditions: [
1127
- {
1128
- field: "command",
1129
- op: "matches",
1130
- value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
1131
- flags: "i"
1132
- }
1133
- ],
1134
- conditionMode: "all",
1135
- verdict: "review",
1136
- reason: "Destructive git operation \u2014 discards history or working-tree changes"
1137
- },
1138
- // ── Shell safety ──────────────────────────────────────────────────────
1139
- {
1140
- name: "review-sudo",
1141
- tool: "bash",
1142
- conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
1143
- conditionMode: "all",
1144
- verdict: "review",
1145
- reason: "Command requires elevated privileges"
1146
- },
1147
- {
1148
- name: "review-curl-pipe-shell",
1149
- tool: "bash",
1150
- conditions: [
1151
- {
1152
- field: "command",
1153
- op: "matches",
1154
- value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
1155
- flags: "i"
1156
- }
1157
- ],
1158
- conditionMode: "all",
1159
- verdict: "block",
1160
- reason: "Piping remote script into a shell is a supply-chain attack vector"
1161
- }
1162
- ],
1163
- dlp: { enabled: true, scanIgnoredTools: true }
1164
- },
1165
- environments: {}
1166
- };
1167
- var ADVISORY_SMART_RULES = [
1168
- {
1169
- name: "allow-rm-safe-paths",
1170
- tool: "*",
1171
- conditionMode: "all",
1172
- conditions: [
1173
- { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
1174
- {
1175
- field: "command",
1176
- op: "matches",
1177
- // Matches known-safe build artifact paths in the command.
1178
- value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
1179
- }
1180
- ],
1181
- verdict: "allow",
1182
- reason: "Deleting a known-safe build artifact path"
1183
- },
1184
- {
1185
- name: "review-rm",
1186
- tool: "*",
1187
- conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
1188
- verdict: "review",
1189
- reason: "rm can permanently delete files \u2014 confirm the target path"
1190
- }
1191
- ];
1192
- var cachedConfig = null;
1193
- function _resetConfigCache() {
1194
- cachedConfig = null;
1195
- }
1196
- function getGlobalSettings() {
1197
- try {
1198
- const globalConfigPath = path4.join(os2.homedir(), ".node9", "config.json");
1199
- if (fs2.existsSync(globalConfigPath)) {
1200
- const parsed = JSON.parse(fs2.readFileSync(globalConfigPath, "utf-8"));
1201
- const settings = parsed.settings || {};
1202
- return {
1203
- mode: settings.mode || "standard",
1204
- autoStartDaemon: settings.autoStartDaemon !== false,
1205
- slackEnabled: settings.slackEnabled !== false,
1206
- enableTrustSessions: settings.enableTrustSessions === true,
1207
- allowGlobalPause: settings.allowGlobalPause !== false
1208
- };
1209
- }
1210
- } catch {
1211
- }
1212
- return {
1021
+ function _resetConfigCache() {
1022
+ cachedConfig = null;
1023
+ }
1024
+ function getGlobalSettings() {
1025
+ try {
1026
+ const globalConfigPath = path4.join(os2.homedir(), ".node9", "config.json");
1027
+ if (fs2.existsSync(globalConfigPath)) {
1028
+ const parsed = JSON.parse(fs2.readFileSync(globalConfigPath, "utf-8"));
1029
+ const settings = parsed.settings || {};
1030
+ return {
1031
+ mode: settings.mode || "standard",
1032
+ autoStartDaemon: settings.autoStartDaemon !== false,
1033
+ slackEnabled: settings.slackEnabled !== false,
1034
+ enableTrustSessions: settings.enableTrustSessions === true,
1035
+ allowGlobalPause: settings.allowGlobalPause !== false
1036
+ };
1037
+ }
1038
+ } catch {
1039
+ }
1040
+ return {
1213
1041
  mode: "standard",
1214
1042
  autoStartDaemon: true,
1215
1043
  slackEnabled: true,
@@ -1596,8 +1424,6 @@ function isIgnoredTool(toolName) {
1596
1424
  const config = getConfig();
1597
1425
  return matchesPattern(toolName, config.policy.ignoredTools);
1598
1426
  }
1599
- var DAEMON_PORT = 7391;
1600
- var DAEMON_HOST = "127.0.0.1";
1601
1427
  function isDaemonRunning() {
1602
1428
  try {
1603
1429
  const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
@@ -1621,7 +1447,7 @@ function getPersistentDecision(toolName) {
1621
1447
  }
1622
1448
  return null;
1623
1449
  }
1624
- async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1450
+ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
1625
1451
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1626
1452
  const checkCtrl = new AbortController();
1627
1453
  const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
@@ -1636,6 +1462,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1636
1462
  args,
1637
1463
  agent: meta?.agent,
1638
1464
  mcpServer: meta?.mcpServer,
1465
+ fromCLI: true,
1466
+ // Pass the flight-recorder ID so the daemon uses the same UUID for
1467
+ // activity-result as the CLI used for the pending activity event.
1468
+ // Without this, the two UUIDs never match and tail.ts never resolves
1469
+ // the pending item.
1470
+ activityId,
1639
1471
  ...riskMetadata && { riskMetadata }
1640
1472
  }),
1641
1473
  signal: checkCtrl.signal
@@ -1690,7 +1522,44 @@ async function resolveViaDaemon(id, decision, internalToken) {
1690
1522
  signal: AbortSignal.timeout(3e3)
1691
1523
  });
1692
1524
  }
1525
+ function notifyActivity(data) {
1526
+ return new Promise((resolve) => {
1527
+ try {
1528
+ const payload = JSON.stringify(data);
1529
+ const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
1530
+ sock.on("connect", () => {
1531
+ sock.on("close", resolve);
1532
+ sock.end(payload);
1533
+ });
1534
+ sock.on("error", resolve);
1535
+ } catch {
1536
+ resolve();
1537
+ }
1538
+ });
1539
+ }
1693
1540
  async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
1541
+ if (!options?.calledFromDaemon) {
1542
+ const actId = randomUUID();
1543
+ const actTs = Date.now();
1544
+ await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
1545
+ const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
1546
+ ...options,
1547
+ activityId: actId
1548
+ });
1549
+ if (!result.noApprovalMechanism) {
1550
+ await notifyActivity({
1551
+ id: actId,
1552
+ tool: toolName,
1553
+ ts: actTs,
1554
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
1555
+ label: result.blockedByLabel
1556
+ });
1557
+ }
1558
+ return result;
1559
+ }
1560
+ return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
1561
+ }
1562
+ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
1694
1563
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1695
1564
  const pauseState = checkPause();
1696
1565
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
@@ -1726,6 +1595,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1726
1595
  blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
1727
1596
  };
1728
1597
  }
1598
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
1729
1599
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
1730
1600
  }
1731
1601
  }
@@ -1948,7 +1818,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1948
1818
  console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
1949
1819
  `));
1950
1820
  }
1951
- const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
1821
+ const daemonDecision = await askDaemon(
1822
+ toolName,
1823
+ args,
1824
+ meta,
1825
+ signal,
1826
+ riskMetadata,
1827
+ options?.activityId
1828
+ );
1952
1829
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
1953
1830
  const isApproved = daemonDecision === "allow";
1954
1831
  return {
@@ -2152,7 +2029,10 @@ function getConfig() {
2152
2029
  for (const rule of shield.smartRules) {
2153
2030
  if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
2154
2031
  }
2155
- for (const word of shield.dangerousWords) mergedPolicy.dangerousWords.push(word);
2032
+ const existingWords = new Set(mergedPolicy.dangerousWords);
2033
+ for (const word of shield.dangerousWords) {
2034
+ if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
2035
+ }
2156
2036
  }
2157
2037
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
2158
2038
  for (const rule of ADVISORY_SMART_RULES) {
@@ -2353,255 +2233,233 @@ async function resolveNode9SaaS(requestId, creds, approved) {
2353
2233
  } catch {
2354
2234
  }
2355
2235
  }
2356
-
2357
- // src/setup.ts
2358
- import fs3 from "fs";
2359
- import path5 from "path";
2360
- import os3 from "os";
2361
- import chalk3 from "chalk";
2362
- import { confirm as confirm2 } from "@inquirer/prompts";
2363
- function printDaemonTip() {
2364
- console.log(
2365
- chalk3.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk3.white("\n To view your history or manage persistent rules, run:") + chalk3.green("\n node9 daemon --openui")
2366
- );
2367
- }
2368
- function fullPathCommand(subcommand) {
2369
- if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
2370
- const nodeExec = process.execPath;
2371
- const cliScript = process.argv[1];
2372
- return `${nodeExec} ${cliScript} ${subcommand}`;
2373
- }
2374
- function readJson(filePath) {
2375
- try {
2376
- if (fs3.existsSync(filePath)) {
2377
- return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
2378
- }
2379
- } catch {
2380
- }
2381
- return null;
2382
- }
2383
- function writeJson(filePath, data) {
2384
- const dir = path5.dirname(filePath);
2385
- if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
2386
- fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
2387
- }
2388
- async function setupClaude() {
2389
- const homeDir2 = os3.homedir();
2390
- const mcpPath = path5.join(homeDir2, ".claude.json");
2391
- const hooksPath = path5.join(homeDir2, ".claude", "settings.json");
2392
- const claudeConfig = readJson(mcpPath) ?? {};
2393
- const settings = readJson(hooksPath) ?? {};
2394
- const servers = claudeConfig.mcpServers ?? {};
2395
- let anythingChanged = false;
2396
- if (!settings.hooks) settings.hooks = {};
2397
- const hasPreHook = settings.hooks.PreToolUse?.some(
2398
- (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
2399
- );
2400
- if (!hasPreHook) {
2401
- if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
2402
- settings.hooks.PreToolUse.push({
2403
- matcher: ".*",
2404
- hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
2405
- });
2406
- console.log(chalk3.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
2407
- anythingChanged = true;
2408
- }
2409
- const hasPostHook = settings.hooks.PostToolUse?.some(
2410
- (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
2411
- );
2412
- if (!hasPostHook) {
2413
- if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
2414
- settings.hooks.PostToolUse.push({
2415
- matcher: ".*",
2416
- hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
2417
- });
2418
- console.log(chalk3.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
2419
- anythingChanged = true;
2420
- }
2421
- if (anythingChanged) {
2422
- writeJson(hooksPath, settings);
2423
- console.log("");
2424
- }
2425
- const serversToWrap = [];
2426
- for (const [name, server] of Object.entries(servers)) {
2427
- if (!server.command || server.command === "node9") continue;
2428
- const parts = [server.command, ...server.args ?? []];
2429
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
2430
- }
2431
- if (serversToWrap.length > 0) {
2432
- console.log(chalk3.bold("The following existing entries will be modified:\n"));
2433
- console.log(chalk3.white(` ${mcpPath}`));
2434
- for (const { name, originalCmd } of serversToWrap) {
2435
- console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
2436
- }
2437
- console.log("");
2438
- const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
2439
- if (proceed) {
2440
- for (const { name, parts } of serversToWrap) {
2441
- servers[name] = { ...servers[name], command: "node9", args: parts };
2236
+ var PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
2237
+ var init_core = __esm({
2238
+ "src/core.ts"() {
2239
+ "use strict";
2240
+ init_native();
2241
+ init_context_sniper();
2242
+ init_config_schema();
2243
+ init_shields();
2244
+ init_dlp();
2245
+ PAUSED_FILE = path4.join(os2.homedir(), ".node9", "PAUSED");
2246
+ TRUST_FILE = path4.join(os2.homedir(), ".node9", "trust.json");
2247
+ LOCAL_AUDIT_LOG = path4.join(os2.homedir(), ".node9", "audit.log");
2248
+ HOOK_DEBUG_LOG = path4.join(os2.homedir(), ".node9", "hook-debug.log");
2249
+ SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2250
+ DANGEROUS_WORDS = [
2251
+ "mkfs",
2252
+ // formats/wipes a filesystem partition
2253
+ "shred"
2254
+ // permanently overwrites file contents (unrecoverable)
2255
+ ];
2256
+ DEFAULT_CONFIG = {
2257
+ settings: {
2258
+ mode: "standard",
2259
+ autoStartDaemon: true,
2260
+ enableUndo: true,
2261
+ // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
2262
+ enableHookLogDebug: false,
2263
+ approvalTimeoutMs: 0,
2264
+ // 0 = disabled; set e.g. 30000 for 30-second auto-deny
2265
+ approvers: { native: true, browser: true, cloud: true, terminal: true }
2266
+ },
2267
+ policy: {
2268
+ sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
2269
+ dangerousWords: DANGEROUS_WORDS,
2270
+ ignoredTools: [
2271
+ "list_*",
2272
+ "get_*",
2273
+ "read_*",
2274
+ "describe_*",
2275
+ "read",
2276
+ "glob",
2277
+ "grep",
2278
+ "ls",
2279
+ "notebookread",
2280
+ "notebookedit",
2281
+ "webfetch",
2282
+ "websearch",
2283
+ "exitplanmode",
2284
+ "askuserquestion",
2285
+ "agent",
2286
+ "task*",
2287
+ "toolsearch",
2288
+ "mcp__ide__*",
2289
+ "getDiagnostics"
2290
+ ],
2291
+ toolInspection: {
2292
+ bash: "command",
2293
+ shell: "command",
2294
+ run_shell_command: "command",
2295
+ "terminal.execute": "command",
2296
+ "postgres:query": "sql"
2297
+ },
2298
+ snapshot: {
2299
+ tools: [
2300
+ "str_replace_based_edit_tool",
2301
+ "write_file",
2302
+ "edit_file",
2303
+ "create_file",
2304
+ "edit",
2305
+ "replace"
2306
+ ],
2307
+ onlyPaths: [],
2308
+ ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
2309
+ },
2310
+ smartRules: [
2311
+ // ── rm safety (critical always evaluated first) ──────────────────────
2312
+ {
2313
+ name: "block-rm-rf-home",
2314
+ tool: "bash",
2315
+ conditionMode: "all",
2316
+ conditions: [
2317
+ {
2318
+ field: "command",
2319
+ op: "matches",
2320
+ value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
2321
+ },
2322
+ {
2323
+ field: "command",
2324
+ op: "matches",
2325
+ value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
2326
+ }
2327
+ ],
2328
+ verdict: "block",
2329
+ reason: "Recursive delete of home directory is irreversible"
2330
+ },
2331
+ // ── SQL safety ────────────────────────────────────────────────────────
2332
+ {
2333
+ name: "no-delete-without-where",
2334
+ tool: "*",
2335
+ conditions: [
2336
+ { field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
2337
+ { field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
2338
+ ],
2339
+ conditionMode: "all",
2340
+ verdict: "review",
2341
+ reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
2342
+ },
2343
+ {
2344
+ name: "review-drop-truncate-shell",
2345
+ tool: "bash",
2346
+ conditions: [
2347
+ {
2348
+ field: "command",
2349
+ op: "matches",
2350
+ value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
2351
+ flags: "i"
2352
+ }
2353
+ ],
2354
+ conditionMode: "all",
2355
+ verdict: "review",
2356
+ reason: "SQL DDL destructive statement inside a shell command"
2357
+ },
2358
+ // ── Git safety ────────────────────────────────────────────────────────
2359
+ {
2360
+ name: "block-force-push",
2361
+ tool: "bash",
2362
+ conditions: [
2363
+ {
2364
+ field: "command",
2365
+ op: "matches",
2366
+ value: "git push.*(--force|--force-with-lease|-f\\b)",
2367
+ flags: "i"
2368
+ }
2369
+ ],
2370
+ conditionMode: "all",
2371
+ verdict: "block",
2372
+ reason: "Force push overwrites remote history and cannot be undone"
2373
+ },
2374
+ {
2375
+ name: "review-git-push",
2376
+ tool: "bash",
2377
+ conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
2378
+ conditionMode: "all",
2379
+ verdict: "review",
2380
+ reason: "git push sends changes to a shared remote"
2381
+ },
2382
+ {
2383
+ name: "review-git-destructive",
2384
+ tool: "bash",
2385
+ conditions: [
2386
+ {
2387
+ field: "command",
2388
+ op: "matches",
2389
+ value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
2390
+ flags: "i"
2391
+ }
2392
+ ],
2393
+ conditionMode: "all",
2394
+ verdict: "review",
2395
+ reason: "Destructive git operation \u2014 discards history or working-tree changes"
2396
+ },
2397
+ // ── Shell safety ──────────────────────────────────────────────────────
2398
+ {
2399
+ name: "review-sudo",
2400
+ tool: "bash",
2401
+ conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
2402
+ conditionMode: "all",
2403
+ verdict: "review",
2404
+ reason: "Command requires elevated privileges"
2405
+ },
2406
+ {
2407
+ name: "review-curl-pipe-shell",
2408
+ tool: "bash",
2409
+ conditions: [
2410
+ {
2411
+ field: "command",
2412
+ op: "matches",
2413
+ value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
2414
+ flags: "i"
2415
+ }
2416
+ ],
2417
+ conditionMode: "all",
2418
+ verdict: "block",
2419
+ reason: "Piping remote script into a shell is a supply-chain attack vector"
2420
+ }
2421
+ ],
2422
+ dlp: { enabled: true, scanIgnoredTools: true }
2423
+ },
2424
+ environments: {}
2425
+ };
2426
+ ADVISORY_SMART_RULES = [
2427
+ {
2428
+ name: "allow-rm-safe-paths",
2429
+ tool: "*",
2430
+ conditionMode: "all",
2431
+ conditions: [
2432
+ { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
2433
+ {
2434
+ field: "command",
2435
+ op: "matches",
2436
+ // Matches known-safe build artifact paths in the command.
2437
+ value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
2438
+ }
2439
+ ],
2440
+ verdict: "allow",
2441
+ reason: "Deleting a known-safe build artifact path"
2442
+ },
2443
+ {
2444
+ name: "review-rm",
2445
+ tool: "*",
2446
+ conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
2447
+ verdict: "review",
2448
+ reason: "rm can permanently delete files \u2014 confirm the target path"
2442
2449
  }
2443
- claudeConfig.mcpServers = servers;
2444
- writeJson(mcpPath, claudeConfig);
2445
- console.log(chalk3.green(`
2446
- \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
2447
- anythingChanged = true;
2448
- } else {
2449
- console.log(chalk3.yellow(" Skipped MCP server wrapping."));
2450
- }
2451
- console.log("");
2452
- }
2453
- if (!anythingChanged && serversToWrap.length === 0) {
2454
- console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
2455
- printDaemonTip();
2456
- return;
2457
- }
2458
- if (anythingChanged) {
2459
- console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
2460
- console.log(chalk3.gray(" Restart Claude Code for changes to take effect."));
2461
- printDaemonTip();
2462
- }
2463
- }
2464
- async function setupGemini() {
2465
- const homeDir2 = os3.homedir();
2466
- const settingsPath = path5.join(homeDir2, ".gemini", "settings.json");
2467
- const settings = readJson(settingsPath) ?? {};
2468
- const servers = settings.mcpServers ?? {};
2469
- let anythingChanged = false;
2470
- if (!settings.hooks) settings.hooks = {};
2471
- const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
2472
- (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
2473
- );
2474
- if (!hasBeforeHook) {
2475
- if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
2476
- if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
2477
- settings.hooks.BeforeTool.push({
2478
- matcher: ".*",
2479
- hooks: [
2480
- {
2481
- name: "node9-check",
2482
- type: "command",
2483
- command: fullPathCommand("check"),
2484
- timeout: 6e5
2485
- }
2486
- ]
2487
- });
2488
- console.log(chalk3.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
2489
- anythingChanged = true;
2490
- }
2491
- const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
2492
- (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
2493
- );
2494
- if (!hasAfterHook) {
2495
- if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
2496
- if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
2497
- settings.hooks.AfterTool.push({
2498
- matcher: ".*",
2499
- hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
2500
- });
2501
- console.log(chalk3.green(" \u2705 AfterTool hook added \u2192 node9 log"));
2502
- anythingChanged = true;
2503
- }
2504
- if (anythingChanged) {
2505
- writeJson(settingsPath, settings);
2506
- console.log("");
2450
+ ];
2451
+ cachedConfig = null;
2452
+ DAEMON_PORT = 7391;
2453
+ DAEMON_HOST = "127.0.0.1";
2454
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path4.join(os2.tmpdir(), "node9-activity.sock");
2507
2455
  }
2508
- const serversToWrap = [];
2509
- for (const [name, server] of Object.entries(servers)) {
2510
- if (!server.command || server.command === "node9") continue;
2511
- const parts = [server.command, ...server.args ?? []];
2512
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
2513
- }
2514
- if (serversToWrap.length > 0) {
2515
- console.log(chalk3.bold("The following existing entries will be modified:\n"));
2516
- console.log(chalk3.white(` ${settingsPath} (mcpServers)`));
2517
- for (const { name, originalCmd } of serversToWrap) {
2518
- console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
2519
- }
2520
- console.log("");
2521
- const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
2522
- if (proceed) {
2523
- for (const { name, parts } of serversToWrap) {
2524
- servers[name] = { ...servers[name], command: "node9", args: parts };
2525
- }
2526
- settings.mcpServers = servers;
2527
- writeJson(settingsPath, settings);
2528
- console.log(chalk3.green(`
2529
- \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
2530
- anythingChanged = true;
2531
- } else {
2532
- console.log(chalk3.yellow(" Skipped MCP server wrapping."));
2533
- }
2534
- console.log("");
2535
- }
2536
- if (!anythingChanged && serversToWrap.length === 0) {
2537
- console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
2538
- printDaemonTip();
2539
- return;
2540
- }
2541
- if (anythingChanged) {
2542
- console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
2543
- console.log(chalk3.gray(" Restart Gemini CLI for changes to take effect."));
2544
- printDaemonTip();
2545
- }
2546
- }
2547
- async function setupCursor() {
2548
- const homeDir2 = os3.homedir();
2549
- const mcpPath = path5.join(homeDir2, ".cursor", "mcp.json");
2550
- const mcpConfig = readJson(mcpPath) ?? {};
2551
- const servers = mcpConfig.mcpServers ?? {};
2552
- let anythingChanged = false;
2553
- const serversToWrap = [];
2554
- for (const [name, server] of Object.entries(servers)) {
2555
- if (!server.command || server.command === "node9") continue;
2556
- const parts = [server.command, ...server.args ?? []];
2557
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
2558
- }
2559
- if (serversToWrap.length > 0) {
2560
- console.log(chalk3.bold("The following existing entries will be modified:\n"));
2561
- console.log(chalk3.white(` ${mcpPath}`));
2562
- for (const { name, originalCmd } of serversToWrap) {
2563
- console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
2564
- }
2565
- console.log("");
2566
- const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
2567
- if (proceed) {
2568
- for (const { name, parts } of serversToWrap) {
2569
- servers[name] = { ...servers[name], command: "node9", args: parts };
2570
- }
2571
- mcpConfig.mcpServers = servers;
2572
- writeJson(mcpPath, mcpConfig);
2573
- console.log(chalk3.green(`
2574
- \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
2575
- anythingChanged = true;
2576
- } else {
2577
- console.log(chalk3.yellow(" Skipped MCP server wrapping."));
2578
- }
2579
- console.log("");
2580
- }
2581
- console.log(
2582
- chalk3.yellow(
2583
- " \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
2584
- )
2585
- );
2586
- console.log("");
2587
- if (!anythingChanged && serversToWrap.length === 0) {
2588
- console.log(
2589
- chalk3.blue(
2590
- "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
2591
- )
2592
- );
2593
- printDaemonTip();
2594
- return;
2595
- }
2596
- if (anythingChanged) {
2597
- console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
2598
- console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
2599
- printDaemonTip();
2600
- }
2601
- }
2456
+ });
2602
2457
 
2603
2458
  // src/daemon/ui.html
2604
- var ui_default = `<!doctype html>
2459
+ var ui_default;
2460
+ var init_ui = __esm({
2461
+ "src/daemon/ui.html"() {
2462
+ ui_default = `<!doctype html>
2605
2463
  <html lang="en">
2606
2464
  <head>
2607
2465
  <meta charset="UTF-8" />
@@ -2627,6 +2485,11 @@ var ui_default = `<!doctype html>
2627
2485
  margin: 0;
2628
2486
  padding: 0;
2629
2487
  }
2488
+ html,
2489
+ body {
2490
+ height: 100%;
2491
+ overflow: hidden;
2492
+ }
2630
2493
  body {
2631
2494
  background: var(--bg);
2632
2495
  color: var(--text);
@@ -2634,16 +2497,17 @@ var ui_default = `<!doctype html>
2634
2497
  'Inter',
2635
2498
  -apple-system,
2636
2499
  sans-serif;
2637
- min-height: 100vh;
2638
2500
  }
2639
2501
 
2640
2502
  .shell {
2641
- max-width: 1000px;
2503
+ max-width: 1440px;
2504
+ height: 100vh;
2642
2505
  margin: 0 auto;
2643
- padding: 32px 24px 48px;
2506
+ padding: 16px 20px 16px;
2644
2507
  display: grid;
2645
2508
  grid-template-rows: auto 1fr;
2646
- gap: 24px;
2509
+ gap: 16px;
2510
+ overflow: hidden;
2647
2511
  }
2648
2512
  header {
2649
2513
  display: flex;
@@ -2680,9 +2544,10 @@ var ui_default = `<!doctype html>
2680
2544
 
2681
2545
  .body {
2682
2546
  display: grid;
2683
- grid-template-columns: 1fr 272px;
2684
- gap: 20px;
2685
- align-items: start;
2547
+ grid-template-columns: 360px 1fr 270px;
2548
+ gap: 16px;
2549
+ min-height: 0;
2550
+ overflow: hidden;
2686
2551
  }
2687
2552
 
2688
2553
  .warning-banner {
@@ -2702,6 +2567,10 @@ var ui_default = `<!doctype html>
2702
2567
 
2703
2568
  .main {
2704
2569
  min-width: 0;
2570
+ min-height: 0;
2571
+ overflow-y: auto;
2572
+ scrollbar-width: thin;
2573
+ scrollbar-color: var(--border) transparent;
2705
2574
  }
2706
2575
  .section-title {
2707
2576
  font-size: 11px;
@@ -2732,14 +2601,64 @@ var ui_default = `<!doctype html>
2732
2601
  background: var(--card);
2733
2602
  border: 1px solid var(--border);
2734
2603
  border-radius: 14px;
2735
- padding: 24px;
2736
- margin-bottom: 16px;
2604
+ padding: 20px;
2605
+ margin-bottom: 14px;
2737
2606
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
2738
2607
  animation: pop 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
2739
2608
  }
2740
2609
  .card.slack-viewer {
2741
2610
  border-color: rgba(83, 155, 245, 0.3);
2742
2611
  }
2612
+ .card-header {
2613
+ display: flex;
2614
+ align-items: center;
2615
+ gap: 8px;
2616
+ margin-bottom: 12px;
2617
+ padding-bottom: 12px;
2618
+ border-bottom: 1px solid var(--border);
2619
+ }
2620
+ .card-header-icon {
2621
+ font-size: 16px;
2622
+ }
2623
+ .card-header-title {
2624
+ font-size: 12px;
2625
+ font-weight: 700;
2626
+ color: var(--text-bright);
2627
+ text-transform: uppercase;
2628
+ letter-spacing: 0.5px;
2629
+ }
2630
+ .card-timer {
2631
+ margin-left: auto;
2632
+ font-size: 11px;
2633
+ font-family: 'Fira Code', monospace;
2634
+ color: var(--muted);
2635
+ background: rgba(48, 54, 61, 0.6);
2636
+ padding: 2px 8px;
2637
+ border-radius: 5px;
2638
+ }
2639
+ .card-timer.urgent {
2640
+ color: var(--danger);
2641
+ background: rgba(201, 60, 55, 0.1);
2642
+ }
2643
+ .btn-allow {
2644
+ background: var(--success);
2645
+ color: #fff;
2646
+ grid-column: span 2;
2647
+ font-size: 14px;
2648
+ padding: 13px 14px;
2649
+ }
2650
+ .btn-deny {
2651
+ background: rgba(201, 60, 55, 0.15);
2652
+ color: #e5534b;
2653
+ border: 1px solid rgba(201, 60, 55, 0.3);
2654
+ grid-column: span 2;
2655
+ }
2656
+ .btn-deny:hover:not(:disabled) {
2657
+ background: var(--danger);
2658
+ color: #fff;
2659
+ border-color: transparent;
2660
+ filter: none;
2661
+ }
2743
2662
  @keyframes pop {
2744
2663
  from {
2745
2664
  opacity: 0;
@@ -2947,24 +2866,178 @@ var ui_default = `<!doctype html>
2947
2866
  cursor: not-allowed;
2948
2867
  }
2949
2868
 
2869
+ .flight-col {
2870
+ display: flex;
2871
+ flex-direction: column;
2872
+ min-height: 0;
2873
+ overflow: hidden;
2874
+ }
2875
+ .flight-panel {
2876
+ flex: 1;
2877
+ min-height: 0;
2878
+ display: flex;
2879
+ flex-direction: column;
2880
+ overflow: hidden;
2881
+ }
2950
2882
  .sidebar {
2951
2883
  display: flex;
2952
2884
  flex-direction: column;
2953
2885
  gap: 12px;
2954
- position: sticky;
2955
- top: 24px;
2886
+ min-height: 0;
2887
+ overflow-y: auto;
2888
+ scrollbar-width: thin;
2889
+ scrollbar-color: var(--border) transparent;
2956
2890
  }
2957
2891
  .panel {
2958
2892
  background: var(--panel);
2959
2893
  border: 1px solid var(--border);
2960
2894
  border-radius: 12px;
2961
- padding: 16px;
2895
+ padding: 14px;
2896
+ }
2897
+ /* \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2898
+ #activity-feed {
2899
+ display: flex;
2900
+ flex-direction: column;
2901
+ gap: 4px;
2902
+ margin-top: 4px;
2903
+ flex: 1;
2904
+ min-height: 0;
2905
+ overflow-y: auto;
2906
+ scrollbar-width: thin;
2907
+ scrollbar-color: var(--border) transparent;
2908
+ }
2909
+ .feed-row {
2910
+ display: grid;
2911
+ grid-template-columns: 58px 20px 1fr 48px;
2912
+ align-items: start;
2913
+ gap: 6px;
2914
+ background: rgba(22, 27, 34, 0.6);
2915
+ border: 1px solid var(--border);
2916
+ padding: 7px 10px;
2917
+ border-radius: 7px;
2918
+ font-size: 11px;
2919
+ animation: frSlideIn 0.15s ease-out;
2920
+ transition: background 0.1s;
2921
+ cursor: default;
2922
+ }
2923
+ .feed-row:hover {
2924
+ background: rgba(30, 38, 48, 0.9);
2925
+ border-color: rgba(83, 155, 245, 0.2);
2926
+ }
2927
+ @keyframes frSlideIn {
2928
+ from {
2929
+ opacity: 0;
2930
+ transform: translateX(-4px);
2931
+ }
2932
+ to {
2933
+ opacity: 1;
2934
+ transform: none;
2935
+ }
2936
+ }
2937
+ .feed-ts {
2938
+ color: var(--muted);
2939
+ font-family: monospace;
2940
+ font-size: 9px;
2941
+ }
2942
+ .feed-icon {
2943
+ text-align: center;
2944
+ font-size: 13px;
2945
+ }
2946
+ .feed-content {
2947
+ min-width: 0;
2948
+ color: var(--text-bright);
2949
+ word-break: break-all;
2950
+ }
2951
+ .feed-args {
2952
+ display: block;
2953
+ color: var(--muted);
2954
+ font-family: monospace;
2955
+ margin-top: 2px;
2956
+ font-size: 10px;
2957
+ word-break: break-all;
2958
+ }
2959
+ .feed-badge {
2960
+ text-align: right;
2961
+ font-weight: 700;
2962
+ font-size: 9px;
2963
+ letter-spacing: 0.03em;
2964
+ }
2965
+ .fr-pending {
2966
+ color: var(--muted);
2967
+ }
2968
+ .fr-allow {
2969
+ color: #57ab5a;
2970
+ }
2971
+ .fr-block {
2972
+ color: var(--danger);
2973
+ }
2974
+ .fr-dlp {
2975
+ color: var(--primary);
2976
+ animation: frBlink 1s infinite;
2977
+ }
2978
+ @keyframes frBlink {
2979
+ 50% {
2980
+ opacity: 0.4;
2981
+ }
2982
+ }
2983
+ .fr-dlp-row {
2984
+ border-color: var(--primary) !important;
2985
+ }
2986
+ .feed-clear-btn {
2987
+ background: transparent;
2988
+ border: none;
2989
+ color: var(--muted);
2990
+ font-size: 10px;
2991
+ padding: 0;
2992
+ cursor: pointer;
2993
+ margin-left: auto;
2994
+ font-family: inherit;
2995
+ font-weight: 500;
2996
+ transition: color 0.15s;
2997
+ }
2998
+ .feed-clear-btn:hover {
2999
+ color: var(--text);
3000
+ filter: none;
3001
+ transform: none;
3002
+ }
3003
+ /* \u2500\u2500 Shields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
3004
+ .shield-row {
3005
+ display: flex;
3006
+ align-items: flex-start;
3007
+ gap: 10px;
3008
+ padding: 8px 0;
3009
+ border-bottom: 1px solid var(--border);
2962
3010
  }
3011
+ .shield-row:last-child {
3012
+ border-bottom: none;
3013
+ padding-bottom: 0;
3014
+ }
3015
+ .shield-row:first-child {
3016
+ padding-top: 0;
3017
+ }
3018
+ .shield-info {
3019
+ flex: 1;
3020
+ min-width: 0;
3021
+ }
3022
+ .shield-name {
3023
+ font-size: 12px;
3024
+ color: var(--text-bright);
3025
+ font-weight: 600;
3026
+ font-family: 'Fira Code', monospace;
3027
+ }
3028
+ .shield-desc {
3029
+ font-size: 10px;
3030
+ color: var(--muted);
3031
+ margin-top: 2px;
3032
+ line-height: 1.4;
3033
+ }
3034
+
2963
3035
  .panel-title {
2964
3036
  font-size: 12px;
2965
3037
  font-weight: 700;
2966
3038
  color: var(--text-bright);
2967
3039
  margin-bottom: 12px;
3040
+ flex-shrink: 0;
2968
3041
  display: flex;
2969
3042
  align-items: center;
2970
3043
  gap: 6px;
@@ -2972,8 +3045,8 @@ var ui_default = `<!doctype html>
2972
3045
  .setting-row {
2973
3046
  display: flex;
2974
3047
  align-items: flex-start;
2975
- gap: 12px;
2976
- margin-bottom: 12px;
3048
+ gap: 10px;
3049
+ margin-bottom: 8px;
2977
3050
  }
2978
3051
  .setting-row:last-child {
2979
3052
  margin-bottom: 0;
@@ -2982,20 +3055,21 @@ var ui_default = `<!doctype html>
2982
3055
  flex: 1;
2983
3056
  }
2984
3057
  .setting-label {
2985
- font-size: 12px;
3058
+ font-size: 11px;
2986
3059
  color: var(--text-bright);
2987
- margin-bottom: 3px;
3060
+ margin-bottom: 2px;
3061
+ font-weight: 600;
2988
3062
  }
2989
3063
  .setting-desc {
2990
- font-size: 11px;
3064
+ font-size: 10px;
2991
3065
  color: var(--muted);
2992
- line-height: 1.5;
3066
+ line-height: 1.4;
2993
3067
  }
2994
3068
  .toggle {
2995
3069
  position: relative;
2996
3070
  display: inline-block;
2997
- width: 40px;
2998
- height: 22px;
3071
+ width: 34px;
3072
+ height: 19px;
2999
3073
  flex-shrink: 0;
3000
3074
  margin-top: 1px;
3001
3075
  }
@@ -3015,8 +3089,8 @@ var ui_default = `<!doctype html>
3015
3089
  .slider:before {
3016
3090
  content: '';
3017
3091
  position: absolute;
3018
- width: 16px;
3019
- height: 16px;
3092
+ width: 13px;
3093
+ height: 13px;
3020
3094
  left: 3px;
3021
3095
  bottom: 3px;
3022
3096
  background: #fff;
@@ -3027,7 +3101,7 @@ var ui_default = `<!doctype html>
3027
3101
  background: var(--success);
3028
3102
  }
3029
3103
  input:checked + .slider:before {
3030
- transform: translateX(18px);
3104
+ transform: translateX(15px);
3031
3105
  }
3032
3106
  input:disabled + .slider {
3033
3107
  opacity: 0.4;
@@ -3186,12 +3260,17 @@ var ui_default = `<!doctype html>
3186
3260
  border: 1px solid var(--border);
3187
3261
  }
3188
3262
 
3189
- @media (max-width: 680px) {
3263
+ @media (max-width: 960px) {
3190
3264
  .body {
3191
- grid-template-columns: 1fr;
3265
+ grid-template-columns: 1fr 220px;
3266
+ }
3267
+ .flight-col {
3268
+ display: none;
3192
3269
  }
3193
- .sidebar {
3194
- position: static;
3270
+ }
3271
+ @media (max-width: 640px) {
3272
+ .body {
3273
+ grid-template-columns: 1fr;
3195
3274
  }
3196
3275
  }
3197
3276
  </style>
@@ -3205,6 +3284,19 @@ var ui_default = `<!doctype html>
3205
3284
  </header>
3206
3285
 
3207
3286
  <div class="body">
3287
+ <div class="flight-col">
3288
+ <div class="panel flight-panel">
3289
+ <div class="panel-title">
3290
+ \u{1F6F0}\uFE0F Flight Recorder
3291
+ <span style="font-weight: 400; color: var(--muted); font-size: 11px">live</span>
3292
+ <button class="feed-clear-btn" onclick="clearFeed()">clear</button>
3293
+ </div>
3294
+ <div id="activity-feed">
3295
+ <span class="decisions-empty">Waiting for agent activity\u2026</span>
3296
+ </div>
3297
+ </div>
3298
+ </div>
3299
+
3208
3300
  <div class="main">
3209
3301
  <div id="warnBanner" class="warning-banner">
3210
3302
  \u26A0\uFE0F Auto-start is off \u2014 daemon started manually. Run "node9 daemon stop" to stop it, or
@@ -3285,6 +3377,11 @@ var ui_default = `<!doctype html>
3285
3377
  <div id="slackStatusLine" class="slack-status-line">No key saved</div>
3286
3378
  </div>
3287
3379
 
3380
+ <div class="panel">
3381
+ <div class="panel-title">\u{1F6E1}\uFE0F Active Shields</div>
3382
+ <div id="shieldsList"><span class="decisions-empty">Loading\u2026</span></div>
3383
+ </div>
3384
+
3288
3385
  <div class="panel">
3289
3386
  <div class="panel-title">\u{1F4CB} Persistent Decisions</div>
3290
3387
  <div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
@@ -3330,14 +3427,23 @@ var ui_default = `<!doctype html>
3330
3427
 
3331
3428
  function updateDenyButton(id, timestamp) {
3332
3429
  const btn = document.querySelector('#c-' + id + ' .btn-deny');
3430
+ const timer = document.querySelector('#timer-' + id);
3333
3431
  if (!btn) return;
3334
3432
  const elapsed = Date.now() - timestamp;
3335
3433
  const remaining = Math.max(0, Math.ceil((autoDenyMs - elapsed) / 1000));
3336
3434
  if (remaining <= 0) {
3337
- btn.textContent = 'Auto-Denying...';
3435
+ btn.textContent = '\u23F3 Auto-Denying\u2026';
3338
3436
  btn.disabled = true;
3437
+ if (timer) {
3438
+ timer.textContent = 'auto-deny';
3439
+ timer.className = 'card-timer urgent';
3440
+ }
3339
3441
  } else {
3340
- btn.textContent = 'Block Action (' + remaining + 's)';
3442
+ btn.textContent = '\u{1F6AB} Block this Action';
3443
+ if (timer) {
3444
+ timer.textContent = remaining + 's';
3445
+ timer.className = 'card-timer' + (remaining < 15 ? ' urgent' : '');
3446
+ }
3341
3447
  setTimeout(() => updateDenyButton(id, timestamp), 1000);
3342
3448
  }
3343
3449
  }
@@ -3353,34 +3459,61 @@ var ui_default = `<!doctype html>
3353
3459
  empty.style.display = requests.size === 0 ? 'block' : 'none';
3354
3460
  }
3355
3461
 
3356
- function sendDecision(id, decision, persist) {
3357
- const card = document.getElementById('c-' + id);
3358
- if (card) card.style.opacity = '0.5';
3359
- fetch('/decision/' + id, {
3360
- method: 'POST',
3361
- headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3362
- body: JSON.stringify({ decision, persist: !!persist }),
3363
- });
3364
- setTimeout(() => {
3365
- card?.remove();
3366
- requests.delete(id);
3367
- refresh();
3368
- }, 200);
3462
+ function setCardBusy(card, busy) {
3463
+ if (!card) return;
3464
+ card.querySelectorAll('button').forEach((b) => (b.disabled = busy));
3465
+ card.style.opacity = busy ? '0.5' : '1';
3369
3466
  }
3370
3467
 
3371
- function sendTrust(id, duration) {
3468
+ function showCardError(card, msg) {
3469
+ if (!card) return;
3470
+ card.style.outline = '2px solid #f87171';
3471
+ let err = card.querySelector('.card-error');
3472
+ if (!err) {
3473
+ err = document.createElement('p');
3474
+ err.className = 'card-error';
3475
+ err.style.cssText = 'color:#f87171;font-size:11px;margin:6px 0 0;';
3476
+ card.appendChild(err);
3477
+ }
3478
+ err.textContent = '\u26A0 ' + msg + ' \u2014 please try again or refresh.';
3479
+ }
3480
+
3481
+ async function sendDecision(id, decision, persist) {
3372
3482
  const card = document.getElementById('c-' + id);
3373
- if (card) card.style.opacity = '0.5';
3374
- fetch('/decision/' + id, {
3375
- method: 'POST',
3376
- headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3377
- body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
3378
- });
3379
- setTimeout(() => {
3483
+ setCardBusy(card, true);
3484
+ try {
3485
+ const res = await fetch('/decision/' + id, {
3486
+ method: 'POST',
3487
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3488
+ body: JSON.stringify({ decision, persist: !!persist }),
3489
+ });
3490
+ if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3491
+ card?.remove();
3492
+ requests.delete(id);
3493
+ refresh();
3494
+ } catch (err) {
3495
+ setCardBusy(card, false);
3496
+ showCardError(card, err.message || 'Network error');
3497
+ }
3498
+ }
3499
+
3500
+ async function sendTrust(id, duration) {
3501
+ const card = document.getElementById('c-' + id);
3502
+ setCardBusy(card, true);
3503
+ try {
3504
+ const res = await fetch('/decision/' + id, {
3505
+ method: 'POST',
3506
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3507
+ body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
3508
+ });
3509
+ if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3380
3510
  card?.remove();
3381
3511
  requests.delete(id);
3382
3512
  refresh();
3383
- }, 200);
3513
+ } catch (err) {
3514
+ setCardBusy(card, false);
3515
+ showCardError(card, err.message || 'Network error');
3516
+ }
3384
3517
  }
3385
3518
 
3386
3519
  function renderPayload(req) {
@@ -3431,16 +3564,21 @@ var ui_default = `<!doctype html>
3431
3564
  const mcpLabel = req.mcpServer ? esc(req.mcpServer) : null;
3432
3565
  const dis = isSlack ? 'disabled' : '';
3433
3566
  card.innerHTML = \`
3567
+ <div class="card-header">
3568
+ <span class="card-header-icon">\${isSlack ? '\u26A1' : '\u26A0\uFE0F'}</span>
3569
+ <span class="card-header-title">\${isSlack ? 'Awaiting Cloud Approval' : 'Action Required'}</span>
3570
+ <span class="card-timer" id="timer-\${req.id}">\${autoDenyMs > 0 ? Math.ceil(autoDenyMs / 1000) + 's' : ''}</span>
3571
+ </div>
3434
3572
  <div class="source-row">
3435
3573
  <span class="agent-badge">\${agentLabel}</span>
3436
3574
  \${mcpLabel ? \`<span class="source-arrow">\u2192</span><span class="mcp-badge">mcp::\${mcpLabel}</span>\` : ''}
3437
3575
  </div>
3438
3576
  <div class="tool-chip">\${esc(req.toolName)}</div>
3439
- \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Slack approval \u2014 view only</div>' : ''}
3577
+ \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
3440
3578
  \${renderPayload(req)}
3441
3579
  <div class="actions" id="act-\${req.id}">
3442
- <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>Approve Execution</button>
3443
- <button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>Block Action</button>
3580
+ <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
3581
+ <button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>\u{1F6AB} Block this Action</button>
3444
3582
  <div class="trust-row\${trustEnabled ? ' show' : ''}" id="tr-\${req.id}">
3445
3583
  <button class="btn-trust" onclick="sendTrust('\${req.id}','30m')" \${dis}>\u23F1 Trust 30m</button>
3446
3584
  <button class="btn-trust" onclick="sendTrust('\${req.id}','1h')" \${dis}>\u23F1 Trust 1h</button>
@@ -3500,9 +3638,84 @@ var ui_default = `<!doctype html>
3500
3638
  ev.addEventListener('slack-status', (e) => {
3501
3639
  applySlackStatus(JSON.parse(e.data));
3502
3640
  });
3641
+ ev.addEventListener('shields-status', (e) => {
3642
+ renderShields(JSON.parse(e.data).shields);
3643
+ });
3644
+
3645
+ // \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3646
+ ev.addEventListener('activity', (e) => {
3647
+ const data = JSON.parse(e.data);
3648
+ const feed = document.getElementById('activity-feed');
3649
+ // Remove placeholder on first item
3650
+ const placeholder = feed.querySelector('.decisions-empty');
3651
+ if (placeholder) placeholder.remove();
3652
+
3653
+ const time = new Date(data.ts).toLocaleTimeString([], {
3654
+ hour12: false,
3655
+ hour: '2-digit',
3656
+ minute: '2-digit',
3657
+ second: '2-digit',
3658
+ });
3659
+ const icon = frIcon(data.tool);
3660
+ const argsStr = JSON.stringify(data.args ?? {});
3661
+ const argsPreview = esc(argsStr.length > 120 ? argsStr.slice(0, 120) + '\u2026' : argsStr);
3662
+
3663
+ const row = document.createElement('div');
3664
+ row.className = 'feed-row';
3665
+ row.id = 'fr-' + data.id;
3666
+ row.innerHTML = \`
3667
+ <span class="feed-ts">\${time}</span>
3668
+ <span class="feed-icon">\${icon}</span>
3669
+ <span class="feed-content"><strong>\${esc(data.tool)}</strong><span class="feed-args">\${argsPreview}</span></span>
3670
+ <span class="feed-badge fr-pending">\u25CF</span>
3671
+ \`;
3672
+ feed.prepend(row);
3673
+ if (feed.children.length > 100) feed.lastChild.remove();
3674
+ });
3675
+
3676
+ ev.addEventListener('activity-result', (e) => {
3677
+ const { id, status, label } = JSON.parse(e.data);
3678
+ const row = document.getElementById('fr-' + id);
3679
+ if (!row) return;
3680
+ const badge = row.querySelector('.feed-badge');
3681
+ if (status === 'allow') {
3682
+ badge.textContent = 'ALLOW';
3683
+ badge.className = 'feed-badge fr-allow';
3684
+ } else if (status === 'dlp') {
3685
+ badge.textContent = '\u{1F6E1}\uFE0F DLP';
3686
+ badge.className = 'feed-badge fr-dlp';
3687
+ row.classList.add('fr-dlp-row');
3688
+ } else {
3689
+ badge.textContent = 'BLOCK';
3690
+ badge.className = 'feed-badge fr-block';
3691
+ }
3692
+ });
3503
3693
  }
3504
3694
  connect();
3505
3695
 
3696
+ const FR_ICONS = {
3697
+ bash: '\u{1F4BB}',
3698
+ read: '\u{1F4D6}',
3699
+ edit: '\u270F\uFE0F',
3700
+ write: '\u270F\uFE0F',
3701
+ glob: '\u{1F4C2}',
3702
+ grep: '\u{1F50D}',
3703
+ agent: '\u{1F916}',
3704
+ search: '\u{1F50D}',
3705
+ sql: '\u{1F5C4}\uFE0F',
3706
+ query: '\u{1F5C4}\uFE0F',
3707
+ list: '\u{1F4C2}',
3708
+ delete: '\u{1F5D1}\uFE0F',
3709
+ web: '\u{1F310}',
3710
+ };
3711
+ function frIcon(tool) {
3712
+ const t = (tool || '').toLowerCase();
3713
+ for (const [k, v] of Object.entries(FR_ICONS)) {
3714
+ if (t.includes(k)) return v;
3715
+ }
3716
+ return '\u{1F6E0}\uFE0F';
3717
+ }
3718
+
3506
3719
  function saveSetting(key, value) {
3507
3720
  fetch('/settings', {
3508
3721
  method: 'POST',
@@ -3592,6 +3805,49 @@ var ui_default = `<!doctype html>
3592
3805
  }
3593
3806
  }
3594
3807
 
3808
+ function clearFeed() {
3809
+ const feed = document.getElementById('activity-feed');
3810
+ feed.innerHTML = '<span class="decisions-empty">Feed cleared.</span>';
3811
+ }
3812
+
3813
+ function renderShields(shields) {
3814
+ const list = document.getElementById('shieldsList');
3815
+ if (!shields || shields.length === 0) {
3816
+ list.innerHTML = '<span class="decisions-empty">No shields available.</span>';
3817
+ return;
3818
+ }
3819
+ list.innerHTML = shields
3820
+ .map(
3821
+ (s) => \`
3822
+ <div class="shield-row">
3823
+ <div class="shield-info">
3824
+ <div class="shield-name">\${esc(s.name)}</div>
3825
+ <div class="shield-desc">\${esc(s.description)}</div>
3826
+ </div>
3827
+ <label class="toggle">
3828
+ <input type="checkbox" \${s.active ? 'checked' : ''}
3829
+ onchange="toggleShield('\${esc(s.name)}', this.checked)" />
3830
+ <span class="slider"></span>
3831
+ </label>
3832
+ </div>
3833
+ \`
3834
+ )
3835
+ .join('');
3836
+ }
3837
+
3838
+ function toggleShield(name, active) {
3839
+ fetch('/shields', {
3840
+ method: 'POST',
3841
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3842
+ body: JSON.stringify({ name, active }),
3843
+ }).catch(() => {});
3844
+ }
3845
+
3846
+ fetch('/shields')
3847
+ .then((r) => r.json())
3848
+ .then(({ shields }) => renderShields(shields))
3849
+ .catch(() => {});
3850
+
3595
3851
  function renderDecisions(decisions) {
3596
3852
  const dl = document.getElementById('decisionsList');
3597
3853
  const entries = Object.entries(decisions);
@@ -3638,31 +3894,32 @@ var ui_default = `<!doctype html>
3638
3894
  </body>
3639
3895
  </html>
3640
3896
  `;
3897
+ }
3898
+ });
3641
3899
 
3642
3900
  // src/daemon/ui.ts
3643
- var UI_HTML_TEMPLATE = ui_default;
3901
+ var UI_HTML_TEMPLATE;
3902
+ var init_ui2 = __esm({
3903
+ "src/daemon/ui.ts"() {
3904
+ "use strict";
3905
+ init_ui();
3906
+ UI_HTML_TEMPLATE = ui_default;
3907
+ }
3908
+ });
3644
3909
 
3645
3910
  // src/daemon/index.ts
3646
3911
  import http from "http";
3912
+ import net2 from "net";
3647
3913
  import fs4 from "fs";
3648
3914
  import path6 from "path";
3649
3915
  import os4 from "os";
3650
3916
  import { spawn as spawn2 } from "child_process";
3651
- import { randomUUID } from "crypto";
3917
+ import { randomUUID as randomUUID2 } from "crypto";
3652
3918
  import chalk4 from "chalk";
3653
- var DAEMON_PORT2 = 7391;
3654
- var DAEMON_HOST2 = "127.0.0.1";
3655
- var homeDir = os4.homedir();
3656
- var DAEMON_PID_FILE = path6.join(homeDir, ".node9", "daemon.pid");
3657
- var DECISIONS_FILE = path6.join(homeDir, ".node9", "decisions.json");
3658
- var GLOBAL_CONFIG_FILE = path6.join(homeDir, ".node9", "config.json");
3659
- var CREDENTIALS_FILE = path6.join(homeDir, ".node9", "credentials.json");
3660
- var AUDIT_LOG_FILE = path6.join(homeDir, ".node9", "audit.log");
3661
- var TRUST_FILE2 = path6.join(homeDir, ".node9", "trust.json");
3662
3919
  function atomicWriteSync2(filePath, data, options) {
3663
3920
  const dir = path6.dirname(filePath);
3664
3921
  if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
3665
- const tmpPath = `${filePath}.${randomUUID()}.tmp`;
3922
+ const tmpPath = `${filePath}.${randomUUID2()}.tmp`;
3666
3923
  fs4.writeFileSync(tmpPath, data, options);
3667
3924
  fs4.renameSync(tmpPath, filePath);
3668
3925
  }
@@ -3680,12 +3937,6 @@ function writeTrustEntry(toolName, durationMs) {
3680
3937
  } catch {
3681
3938
  }
3682
3939
  }
3683
- var TRUST_DURATIONS = {
3684
- "30m": 30 * 6e4,
3685
- "1h": 60 * 6e4,
3686
- "2h": 2 * 60 * 6e4
3687
- };
3688
- var SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
3689
3940
  function redactArgs(value) {
3690
3941
  if (!value || typeof value !== "object") return value;
3691
3942
  if (Array.isArray(value)) return value.map(redactArgs);
@@ -3720,7 +3971,6 @@ function getAuditHistory(limit = 20) {
3720
3971
  return [];
3721
3972
  }
3722
3973
  }
3723
- var AUTO_DENY_MS = 12e4;
3724
3974
  function getOrgName() {
3725
3975
  try {
3726
3976
  if (fs4.existsSync(CREDENTIALS_FILE)) {
@@ -3730,7 +3980,6 @@ function getOrgName() {
3730
3980
  }
3731
3981
  return null;
3732
3982
  }
3733
- var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
3734
3983
  function hasStoredSlackKey() {
3735
3984
  return fs4.existsSync(CREDENTIALS_FILE);
3736
3985
  }
@@ -3746,11 +3995,6 @@ function writeGlobalSetting(key, value) {
3746
3995
  config.settings[key] = value;
3747
3996
  atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
3748
3997
  }
3749
- var pending = /* @__PURE__ */ new Map();
3750
- var sseClients = /* @__PURE__ */ new Set();
3751
- var abandonTimer = null;
3752
- var daemonServer = null;
3753
- var hadBrowserClient = false;
3754
3998
  function abandonPending() {
3755
3999
  abandonTimer = null;
3756
4000
  pending.forEach((entry, id) => {
@@ -3772,6 +4016,18 @@ function abandonPending() {
3772
4016
  }
3773
4017
  }
3774
4018
  function broadcast(event, data) {
4019
+ if (event === "activity") {
4020
+ activityRing.push({ event, data });
4021
+ if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
4022
+ } else if (event === "activity-result") {
4023
+ const { id, status, label } = data;
4024
+ for (let i = activityRing.length - 1; i >= 0; i--) {
4025
+ if (activityRing[i].data.id === id) {
4026
+ Object.assign(activityRing[i].data, { status, label });
4027
+ break;
4028
+ }
4029
+ }
4030
+ }
3775
4031
  const msg = `event: ${event}
3776
4032
  data: ${JSON.stringify(data)}
3777
4033
 
@@ -3817,13 +4073,15 @@ function writePersistentDecision(toolName, decision) {
3817
4073
  }
3818
4074
  }
3819
4075
  function startDaemon() {
3820
- const csrfToken = randomUUID();
3821
- const internalToken = randomUUID();
4076
+ const csrfToken = randomUUID2();
4077
+ const internalToken = randomUUID2();
3822
4078
  const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
3823
4079
  const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
3824
4080
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
4081
+ const watchMode = process.env.NODE9_WATCH_MODE === "1";
3825
4082
  let idleTimer;
3826
4083
  function resetIdleTimer() {
4084
+ if (watchMode) return;
3827
4085
  if (idleTimer) clearTimeout(idleTimer);
3828
4086
  idleTimer = setTimeout(() => {
3829
4087
  if (autoStarted) {
@@ -3878,6 +4136,12 @@ data: ${JSON.stringify({
3878
4136
  data: ${JSON.stringify(readPersistentDecisions())}
3879
4137
 
3880
4138
  `);
4139
+ for (const item of activityRing) {
4140
+ res.write(`event: ${item.event}
4141
+ data: ${JSON.stringify(item.data)}
4142
+
4143
+ `);
4144
+ }
3881
4145
  return req.on("close", () => {
3882
4146
  sseClients.delete(res);
3883
4147
  if (sseClients.size === 0 && pending.size > 0) {
@@ -3897,9 +4161,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
3897
4161
  slackDelegated = false,
3898
4162
  agent,
3899
4163
  mcpServer,
3900
- riskMetadata
4164
+ riskMetadata,
4165
+ fromCLI = false,
4166
+ activityId
3901
4167
  } = JSON.parse(body);
3902
- const id = randomUUID();
4168
+ const id = fromCLI && typeof activityId === "string" && activityId || randomUUID2();
3903
4169
  const entry = {
3904
4170
  id,
3905
4171
  toolName,
@@ -3930,6 +4196,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
3930
4196
  }, AUTO_DENY_MS)
3931
4197
  };
3932
4198
  pending.set(id, entry);
4199
+ if (!fromCLI) {
4200
+ broadcast("activity", {
4201
+ id,
4202
+ ts: entry.timestamp,
4203
+ tool: toolName,
4204
+ args: redactArgs(args),
4205
+ status: "pending"
4206
+ });
4207
+ }
3933
4208
  const browserEnabled = getConfig().settings.approvers?.browser !== false;
3934
4209
  if (browserEnabled) {
3935
4210
  broadcast("add", {
@@ -3959,6 +4234,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
3959
4234
  const e = pending.get(id);
3960
4235
  if (!e) return;
3961
4236
  if (result.noApprovalMechanism) return;
4237
+ broadcast("activity-result", {
4238
+ id,
4239
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
4240
+ label: result.blockedByLabel
4241
+ });
3962
4242
  clearTimeout(e.timer);
3963
4243
  const decision = result.approved ? "allow" : "deny";
3964
4244
  appendAuditLog({ toolName: e.toolName, args: e.args, decision });
@@ -3993,8 +4273,8 @@ data: ${JSON.stringify(readPersistentDecisions())}
3993
4273
  const entry = pending.get(id);
3994
4274
  if (!entry) return res.writeHead(404).end();
3995
4275
  if (entry.earlyDecision) {
4276
+ clearTimeout(entry.timer);
3996
4277
  pending.delete(id);
3997
- broadcast("remove", { id });
3998
4278
  res.writeHead(200, { "Content-Type": "application/json" });
3999
4279
  const body = { decision: entry.earlyDecision };
4000
4280
  if (entry.earlyReason) body.reason = entry.earlyReason;
@@ -4024,10 +4304,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
4024
4304
  decision: `trust:${trustDuration}`
4025
4305
  });
4026
4306
  clearTimeout(entry.timer);
4027
- if (entry.waiter) entry.waiter("allow");
4028
- else entry.earlyDecision = "allow";
4029
- pending.delete(id);
4030
- broadcast("remove", { id });
4307
+ if (entry.waiter) {
4308
+ entry.waiter("allow");
4309
+ pending.delete(id);
4310
+ broadcast("remove", { id });
4311
+ } else {
4312
+ entry.earlyDecision = "allow";
4313
+ broadcast("remove", { id });
4314
+ entry.timer = setTimeout(() => pending.delete(id), 3e4);
4315
+ }
4031
4316
  res.writeHead(200);
4032
4317
  return res.end(JSON.stringify({ ok: true }));
4033
4318
  }
@@ -4039,13 +4324,16 @@ data: ${JSON.stringify(readPersistentDecisions())}
4039
4324
  decision: resolvedDecision
4040
4325
  });
4041
4326
  clearTimeout(entry.timer);
4042
- if (entry.waiter) entry.waiter(resolvedDecision, reason);
4043
- else {
4327
+ if (entry.waiter) {
4328
+ entry.waiter(resolvedDecision, reason);
4329
+ pending.delete(id);
4330
+ broadcast("remove", { id });
4331
+ } else {
4044
4332
  entry.earlyDecision = resolvedDecision;
4045
4333
  entry.earlyReason = reason;
4334
+ broadcast("remove", { id });
4335
+ entry.timer = setTimeout(() => pending.delete(id), 3e4);
4046
4336
  }
4047
- pending.delete(id);
4048
- broadcast("remove", { id });
4049
4337
  res.writeHead(200);
4050
4338
  return res.end(JSON.stringify({ ok: true }));
4051
4339
  } catch {
@@ -4098,116 +4386,675 @@ data: ${JSON.stringify(readPersistentDecisions())}
4098
4386
  res.writeHead(400).end();
4099
4387
  }
4100
4388
  }
4101
- if (req.method === "DELETE" && pathname.startsWith("/decisions/")) {
4102
- if (!validToken(req)) return res.writeHead(403).end();
4103
- try {
4104
- const toolName = decodeURIComponent(pathname.split("/").pop());
4105
- const decisions = readPersistentDecisions();
4106
- delete decisions[toolName];
4107
- atomicWriteSync2(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
4108
- broadcast("decisions", decisions);
4109
- res.writeHead(200);
4110
- return res.end(JSON.stringify({ ok: true }));
4111
- } catch {
4112
- res.writeHead(400).end();
4389
+ if (req.method === "DELETE" && pathname.startsWith("/decisions/")) {
4390
+ if (!validToken(req)) return res.writeHead(403).end();
4391
+ try {
4392
+ const toolName = decodeURIComponent(pathname.split("/").pop());
4393
+ const decisions = readPersistentDecisions();
4394
+ delete decisions[toolName];
4395
+ atomicWriteSync2(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
4396
+ broadcast("decisions", decisions);
4397
+ res.writeHead(200);
4398
+ return res.end(JSON.stringify({ ok: true }));
4399
+ } catch {
4400
+ res.writeHead(400).end();
4401
+ }
4402
+ }
4403
+ if (req.method === "POST" && pathname.startsWith("/resolve/")) {
4404
+ const internalAuth = req.headers["x-node9-internal"];
4405
+ if (internalAuth !== internalToken) return res.writeHead(403).end();
4406
+ try {
4407
+ const id = pathname.split("/").pop();
4408
+ const entry = pending.get(id);
4409
+ if (!entry) return res.writeHead(404).end();
4410
+ const { decision } = JSON.parse(await readBody(req));
4411
+ appendAuditLog({
4412
+ toolName: entry.toolName,
4413
+ args: entry.args,
4414
+ decision
4415
+ });
4416
+ clearTimeout(entry.timer);
4417
+ if (entry.waiter) entry.waiter(decision);
4418
+ else entry.earlyDecision = decision;
4419
+ pending.delete(id);
4420
+ broadcast("remove", { id });
4421
+ res.writeHead(200);
4422
+ return res.end(JSON.stringify({ ok: true }));
4423
+ } catch {
4424
+ res.writeHead(400).end();
4425
+ }
4426
+ }
4427
+ if (req.method === "GET" && pathname === "/audit") {
4428
+ res.writeHead(200, { "Content-Type": "application/json" });
4429
+ return res.end(JSON.stringify(getAuditHistory()));
4430
+ }
4431
+ if (req.method === "GET" && pathname === "/shields") {
4432
+ if (!validToken(req)) return res.writeHead(403).end();
4433
+ const active = readActiveShields();
4434
+ const shields = Object.values(SHIELDS).map((s) => ({
4435
+ name: s.name,
4436
+ description: s.description,
4437
+ active: active.includes(s.name)
4438
+ }));
4439
+ res.writeHead(200, { "Content-Type": "application/json" });
4440
+ return res.end(JSON.stringify({ shields }));
4441
+ }
4442
+ if (req.method === "POST" && pathname === "/shields") {
4443
+ if (!validToken(req)) return res.writeHead(403).end();
4444
+ try {
4445
+ const { name, active } = JSON.parse(await readBody(req));
4446
+ if (!SHIELDS[name]) return res.writeHead(400).end();
4447
+ const current = readActiveShields();
4448
+ const updated = active ? [.../* @__PURE__ */ new Set([...current, name])] : current.filter((n) => n !== name);
4449
+ writeActiveShields(updated);
4450
+ _resetConfigCache();
4451
+ const shieldsPayload = Object.values(SHIELDS).map((s) => ({
4452
+ name: s.name,
4453
+ description: s.description,
4454
+ active: updated.includes(s.name)
4455
+ }));
4456
+ broadcast("shields-status", { shields: shieldsPayload });
4457
+ res.writeHead(200);
4458
+ return res.end(JSON.stringify({ ok: true }));
4459
+ } catch {
4460
+ res.writeHead(400).end();
4461
+ }
4462
+ }
4463
+ res.writeHead(404).end();
4464
+ });
4465
+ daemonServer = server;
4466
+ server.on("error", (e) => {
4467
+ if (e.code === "EADDRINUSE") {
4468
+ try {
4469
+ if (fs4.existsSync(DAEMON_PID_FILE)) {
4470
+ const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4471
+ process.kill(pid, 0);
4472
+ return process.exit(0);
4473
+ }
4474
+ } catch {
4475
+ try {
4476
+ fs4.unlinkSync(DAEMON_PID_FILE);
4477
+ } catch {
4478
+ }
4479
+ server.listen(DAEMON_PORT2, DAEMON_HOST2);
4480
+ return;
4481
+ }
4482
+ }
4483
+ console.error(chalk4.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
4484
+ process.exit(1);
4485
+ });
4486
+ server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
4487
+ atomicWriteSync2(
4488
+ DAEMON_PID_FILE,
4489
+ JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
4490
+ { mode: 384 }
4491
+ );
4492
+ console.log(chalk4.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
4493
+ });
4494
+ if (watchMode) {
4495
+ console.log(chalk4.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
4496
+ }
4497
+ try {
4498
+ fs4.unlinkSync(ACTIVITY_SOCKET_PATH2);
4499
+ } catch {
4500
+ }
4501
+ const ACTIVITY_MAX_BYTES = 1024 * 1024;
4502
+ const unixServer = net2.createServer((socket) => {
4503
+ const chunks = [];
4504
+ let bytesReceived = 0;
4505
+ socket.on("data", (chunk) => {
4506
+ bytesReceived += chunk.length;
4507
+ if (bytesReceived > ACTIVITY_MAX_BYTES) {
4508
+ socket.destroy();
4509
+ return;
4510
+ }
4511
+ chunks.push(chunk);
4512
+ });
4513
+ socket.on("end", () => {
4514
+ try {
4515
+ const data = JSON.parse(Buffer.concat(chunks).toString());
4516
+ if (data.status === "pending") {
4517
+ broadcast("activity", {
4518
+ id: data.id,
4519
+ ts: data.ts,
4520
+ tool: data.tool,
4521
+ args: redactArgs(data.args),
4522
+ status: "pending"
4523
+ });
4524
+ } else {
4525
+ broadcast("activity-result", {
4526
+ id: data.id,
4527
+ status: data.status,
4528
+ label: data.label
4529
+ });
4530
+ }
4531
+ } catch {
4532
+ }
4533
+ });
4534
+ socket.on("error", () => {
4535
+ });
4536
+ });
4537
+ unixServer.listen(ACTIVITY_SOCKET_PATH2);
4538
+ process.on("exit", () => {
4539
+ try {
4540
+ fs4.unlinkSync(ACTIVITY_SOCKET_PATH2);
4541
+ } catch {
4542
+ }
4543
+ });
4544
+ }
4545
+ function stopDaemon() {
4546
+ if (!fs4.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
4547
+ try {
4548
+ const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4549
+ process.kill(pid, "SIGTERM");
4550
+ console.log(chalk4.green("\u2705 Stopped."));
4551
+ } catch {
4552
+ console.log(chalk4.gray("Cleaned up stale PID file."));
4553
+ } finally {
4554
+ try {
4555
+ fs4.unlinkSync(DAEMON_PID_FILE);
4556
+ } catch {
4557
+ }
4558
+ }
4559
+ }
4560
+ function daemonStatus() {
4561
+ if (!fs4.existsSync(DAEMON_PID_FILE))
4562
+ return console.log(chalk4.yellow("Node9 daemon: not running"));
4563
+ try {
4564
+ const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4565
+ process.kill(pid, 0);
4566
+ console.log(chalk4.green("Node9 daemon: running"));
4567
+ } catch {
4568
+ console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
4569
+ }
4570
+ }
4571
+ var ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
4572
+ var init_daemon = __esm({
4573
+ "src/daemon/index.ts"() {
4574
+ "use strict";
4575
+ init_ui2();
4576
+ init_core();
4577
+ init_shields();
4578
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path6.join(os4.tmpdir(), "node9-activity.sock");
4579
+ DAEMON_PORT2 = 7391;
4580
+ DAEMON_HOST2 = "127.0.0.1";
4581
+ homeDir = os4.homedir();
4582
+ DAEMON_PID_FILE = path6.join(homeDir, ".node9", "daemon.pid");
4583
+ DECISIONS_FILE = path6.join(homeDir, ".node9", "decisions.json");
4584
+ GLOBAL_CONFIG_FILE = path6.join(homeDir, ".node9", "config.json");
4585
+ CREDENTIALS_FILE = path6.join(homeDir, ".node9", "credentials.json");
4586
+ AUDIT_LOG_FILE = path6.join(homeDir, ".node9", "audit.log");
4587
+ TRUST_FILE2 = path6.join(homeDir, ".node9", "trust.json");
4588
+ TRUST_DURATIONS = {
4589
+ "30m": 30 * 6e4,
4590
+ "1h": 60 * 6e4,
4591
+ "2h": 2 * 60 * 6e4
4592
+ };
4593
+ SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
4594
+ AUTO_DENY_MS = 12e4;
4595
+ autoStarted = process.env.NODE9_AUTO_STARTED === "1";
4596
+ pending = /* @__PURE__ */ new Map();
4597
+ sseClients = /* @__PURE__ */ new Set();
4598
+ abandonTimer = null;
4599
+ daemonServer = null;
4600
+ hadBrowserClient = false;
4601
+ ACTIVITY_RING_SIZE = 100;
4602
+ activityRing = [];
4603
+ }
4604
+ });
4605
+
4606
+ // src/tui/tail.ts
4607
+ var tail_exports = {};
4608
+ __export(tail_exports, {
4609
+ startTail: () => startTail
4610
+ });
4611
+ import http2 from "http";
4612
+ import chalk5 from "chalk";
4613
+ import fs6 from "fs";
4614
+ import os6 from "os";
4615
+ import path8 from "path";
4616
+ import readline from "readline";
4617
+ import { spawn as spawn3 } from "child_process";
4618
+ function getIcon(tool) {
4619
+ const t = tool.toLowerCase();
4620
+ for (const [k, v] of Object.entries(ICONS)) {
4621
+ if (t.includes(k)) return v;
4622
+ }
4623
+ return "\u{1F6E0}\uFE0F";
4624
+ }
4625
+ function formatBase(activity) {
4626
+ const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
4627
+ const icon = getIcon(activity.tool);
4628
+ const toolName = activity.tool.slice(0, 16).padEnd(16);
4629
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
4630
+ const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
4631
+ return `${chalk5.gray(time)} ${icon} ${chalk5.white.bold(toolName)} ${chalk5.dim(argsPreview)}`;
4632
+ }
4633
+ function renderResult(activity, result) {
4634
+ const base = formatBase(activity);
4635
+ let status;
4636
+ if (result.status === "allow") {
4637
+ status = chalk5.green("\u2713 ALLOW");
4638
+ } else if (result.status === "dlp") {
4639
+ status = chalk5.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
4640
+ } else {
4641
+ status = chalk5.red("\u2717 BLOCK");
4642
+ }
4643
+ if (process.stdout.isTTY) {
4644
+ readline.clearLine(process.stdout, 0);
4645
+ readline.cursorTo(process.stdout, 0);
4646
+ }
4647
+ console.log(`${base} ${status}`);
4648
+ }
4649
+ function renderPending(activity) {
4650
+ if (!process.stdout.isTTY) return;
4651
+ process.stdout.write(`${formatBase(activity)} ${chalk5.yellow("\u25CF \u2026")}\r`);
4652
+ }
4653
+ async function ensureDaemon() {
4654
+ if (fs6.existsSync(PID_FILE)) {
4655
+ try {
4656
+ const { port } = JSON.parse(fs6.readFileSync(PID_FILE, "utf-8"));
4657
+ return port;
4658
+ } catch {
4659
+ }
4660
+ }
4661
+ console.log(chalk5.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
4662
+ const child = spawn3(process.execPath, [process.argv[1], "daemon"], {
4663
+ detached: true,
4664
+ stdio: "ignore",
4665
+ env: { ...process.env, NODE9_AUTO_STARTED: "1" }
4666
+ });
4667
+ child.unref();
4668
+ for (let i = 0; i < 20; i++) {
4669
+ await new Promise((r) => setTimeout(r, 250));
4670
+ if (!fs6.existsSync(PID_FILE)) continue;
4671
+ try {
4672
+ const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
4673
+ signal: AbortSignal.timeout(500)
4674
+ });
4675
+ if (res.ok) {
4676
+ const { port } = JSON.parse(fs6.readFileSync(PID_FILE, "utf-8"));
4677
+ return port;
4678
+ }
4679
+ } catch {
4680
+ }
4681
+ }
4682
+ console.error(chalk5.red("\u274C Daemon failed to start. Try: node9 daemon start"));
4683
+ process.exit(1);
4684
+ }
4685
+ async function startTail(options = {}) {
4686
+ const port = await ensureDaemon();
4687
+ const connectionTime = Date.now();
4688
+ const pending2 = /* @__PURE__ */ new Map();
4689
+ console.log(chalk5.cyan.bold(`
4690
+ \u{1F6F0}\uFE0F Node9 tail `) + chalk5.dim(`\u2192 localhost:${port}`));
4691
+ if (options.history) {
4692
+ console.log(chalk5.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
4693
+ } else {
4694
+ console.log(
4695
+ chalk5.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
4696
+ );
4697
+ }
4698
+ process.on("SIGINT", () => {
4699
+ if (process.stdout.isTTY) {
4700
+ readline.clearLine(process.stdout, 0);
4701
+ readline.cursorTo(process.stdout, 0);
4702
+ }
4703
+ console.log(chalk5.dim("\n\u{1F6F0}\uFE0F Disconnected."));
4704
+ process.exit(0);
4705
+ });
4706
+ const req = http2.get(`http://127.0.0.1:${port}/events`, (res) => {
4707
+ if (res.statusCode !== 200) {
4708
+ console.error(chalk5.red(`Failed to connect: HTTP ${res.statusCode}`));
4709
+ process.exit(1);
4710
+ }
4711
+ let currentEvent = "";
4712
+ let currentData = "";
4713
+ res.on("error", () => {
4714
+ });
4715
+ const rl = readline.createInterface({ input: res, crlfDelay: Infinity });
4716
+ rl.on("error", () => {
4717
+ });
4718
+ rl.on("line", (line) => {
4719
+ if (line.startsWith("event:")) {
4720
+ currentEvent = line.slice(6).trim();
4721
+ } else if (line.startsWith("data:")) {
4722
+ currentData = line.slice(5).trim();
4723
+ } else if (line === "") {
4724
+ if (currentEvent && currentData) {
4725
+ handleMessage(currentEvent, currentData);
4726
+ }
4727
+ currentEvent = "";
4728
+ currentData = "";
4729
+ }
4730
+ });
4731
+ rl.on("close", () => {
4732
+ if (process.stdout.isTTY) {
4733
+ readline.clearLine(process.stdout, 0);
4734
+ readline.cursorTo(process.stdout, 0);
4735
+ }
4736
+ console.log(chalk5.red("\n\u274C Daemon disconnected."));
4737
+ process.exit(1);
4738
+ });
4739
+ });
4740
+ function handleMessage(event, rawData) {
4741
+ let data;
4742
+ try {
4743
+ data = JSON.parse(rawData);
4744
+ } catch {
4745
+ return;
4746
+ }
4747
+ if (event === "activity") {
4748
+ if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
4749
+ if (data.status && data.status !== "pending") {
4750
+ renderResult(data, data);
4751
+ return;
4752
+ }
4753
+ pending2.set(data.id, data);
4754
+ const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
4755
+ if (slowTool) renderPending(data);
4756
+ }
4757
+ if (event === "activity-result") {
4758
+ const original = pending2.get(data.id);
4759
+ if (original) {
4760
+ renderResult(original, data);
4761
+ pending2.delete(data.id);
4762
+ }
4763
+ }
4764
+ }
4765
+ req.on("error", (err) => {
4766
+ const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
4767
+ console.error(chalk5.red(`
4768
+ \u274C ${msg}`));
4769
+ process.exit(1);
4770
+ });
4771
+ }
4772
+ var PID_FILE, ICONS;
4773
+ var init_tail = __esm({
4774
+ "src/tui/tail.ts"() {
4775
+ "use strict";
4776
+ init_daemon();
4777
+ PID_FILE = path8.join(os6.homedir(), ".node9", "daemon.pid");
4778
+ ICONS = {
4779
+ bash: "\u{1F4BB}",
4780
+ shell: "\u{1F4BB}",
4781
+ terminal: "\u{1F4BB}",
4782
+ read: "\u{1F4D6}",
4783
+ edit: "\u270F\uFE0F",
4784
+ write: "\u270F\uFE0F",
4785
+ glob: "\u{1F4C2}",
4786
+ grep: "\u{1F50D}",
4787
+ agent: "\u{1F916}",
4788
+ search: "\u{1F50D}",
4789
+ sql: "\u{1F5C4}\uFE0F",
4790
+ query: "\u{1F5C4}\uFE0F",
4791
+ list: "\u{1F4C2}",
4792
+ delete: "\u{1F5D1}\uFE0F",
4793
+ web: "\u{1F310}"
4794
+ };
4795
+ }
4796
+ });
4797
+
4798
+ // src/cli.ts
4799
+ init_core();
4800
+ import { Command } from "commander";
4801
+
4802
+ // src/setup.ts
4803
+ import fs3 from "fs";
4804
+ import path5 from "path";
4805
+ import os3 from "os";
4806
+ import chalk3 from "chalk";
4807
+ import { confirm as confirm2 } from "@inquirer/prompts";
4808
+ function printDaemonTip() {
4809
+ console.log(
4810
+ chalk3.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk3.white("\n To view your history or manage persistent rules, run:") + chalk3.green("\n node9 daemon --openui")
4811
+ );
4812
+ }
4813
+ function fullPathCommand(subcommand) {
4814
+ if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
4815
+ const nodeExec = process.execPath;
4816
+ const cliScript = process.argv[1];
4817
+ return `${nodeExec} ${cliScript} ${subcommand}`;
4818
+ }
4819
+ function readJson(filePath) {
4820
+ try {
4821
+ if (fs3.existsSync(filePath)) {
4822
+ return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
4823
+ }
4824
+ } catch {
4825
+ }
4826
+ return null;
4827
+ }
4828
+ function writeJson(filePath, data) {
4829
+ const dir = path5.dirname(filePath);
4830
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
4831
+ fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
4832
+ }
4833
+ async function setupClaude() {
4834
+ const homeDir2 = os3.homedir();
4835
+ const mcpPath = path5.join(homeDir2, ".claude.json");
4836
+ const hooksPath = path5.join(homeDir2, ".claude", "settings.json");
4837
+ const claudeConfig = readJson(mcpPath) ?? {};
4838
+ const settings = readJson(hooksPath) ?? {};
4839
+ const servers = claudeConfig.mcpServers ?? {};
4840
+ let anythingChanged = false;
4841
+ if (!settings.hooks) settings.hooks = {};
4842
+ const hasPreHook = settings.hooks.PreToolUse?.some(
4843
+ (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
4844
+ );
4845
+ if (!hasPreHook) {
4846
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
4847
+ settings.hooks.PreToolUse.push({
4848
+ matcher: ".*",
4849
+ hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
4850
+ });
4851
+ console.log(chalk3.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
4852
+ anythingChanged = true;
4853
+ }
4854
+ const hasPostHook = settings.hooks.PostToolUse?.some(
4855
+ (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
4856
+ );
4857
+ if (!hasPostHook) {
4858
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
4859
+ settings.hooks.PostToolUse.push({
4860
+ matcher: ".*",
4861
+ hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
4862
+ });
4863
+ console.log(chalk3.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
4864
+ anythingChanged = true;
4865
+ }
4866
+ if (anythingChanged) {
4867
+ writeJson(hooksPath, settings);
4868
+ console.log("");
4869
+ }
4870
+ const serversToWrap = [];
4871
+ for (const [name, server] of Object.entries(servers)) {
4872
+ if (!server.command || server.command === "node9") continue;
4873
+ const parts = [server.command, ...server.args ?? []];
4874
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
4875
+ }
4876
+ if (serversToWrap.length > 0) {
4877
+ console.log(chalk3.bold("The following existing entries will be modified:\n"));
4878
+ console.log(chalk3.white(` ${mcpPath}`));
4879
+ for (const { name, originalCmd } of serversToWrap) {
4880
+ console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
4881
+ }
4882
+ console.log("");
4883
+ const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
4884
+ if (proceed) {
4885
+ for (const { name, parts } of serversToWrap) {
4886
+ servers[name] = { ...servers[name], command: "node9", args: parts };
4113
4887
  }
4888
+ claudeConfig.mcpServers = servers;
4889
+ writeJson(mcpPath, claudeConfig);
4890
+ console.log(chalk3.green(`
4891
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
4892
+ anythingChanged = true;
4893
+ } else {
4894
+ console.log(chalk3.yellow(" Skipped MCP server wrapping."));
4114
4895
  }
4115
- if (req.method === "POST" && pathname.startsWith("/resolve/")) {
4116
- const internalAuth = req.headers["x-node9-internal"];
4117
- if (internalAuth !== internalToken) return res.writeHead(403).end();
4118
- try {
4119
- const id = pathname.split("/").pop();
4120
- const entry = pending.get(id);
4121
- if (!entry) return res.writeHead(404).end();
4122
- const { decision } = JSON.parse(await readBody(req));
4123
- appendAuditLog({
4124
- toolName: entry.toolName,
4125
- args: entry.args,
4126
- decision
4127
- });
4128
- clearTimeout(entry.timer);
4129
- if (entry.waiter) entry.waiter(decision);
4130
- else entry.earlyDecision = decision;
4131
- pending.delete(id);
4132
- broadcast("remove", { id });
4133
- res.writeHead(200);
4134
- return res.end(JSON.stringify({ ok: true }));
4135
- } catch {
4136
- res.writeHead(400).end();
4896
+ console.log("");
4897
+ }
4898
+ if (!anythingChanged && serversToWrap.length === 0) {
4899
+ console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
4900
+ printDaemonTip();
4901
+ return;
4902
+ }
4903
+ if (anythingChanged) {
4904
+ console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
4905
+ console.log(chalk3.gray(" Restart Claude Code for changes to take effect."));
4906
+ printDaemonTip();
4907
+ }
4908
+ }
4909
+ async function setupGemini() {
4910
+ const homeDir2 = os3.homedir();
4911
+ const settingsPath = path5.join(homeDir2, ".gemini", "settings.json");
4912
+ const settings = readJson(settingsPath) ?? {};
4913
+ const servers = settings.mcpServers ?? {};
4914
+ let anythingChanged = false;
4915
+ if (!settings.hooks) settings.hooks = {};
4916
+ const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
4917
+ (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
4918
+ );
4919
+ if (!hasBeforeHook) {
4920
+ if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
4921
+ if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
4922
+ settings.hooks.BeforeTool.push({
4923
+ matcher: ".*",
4924
+ hooks: [
4925
+ {
4926
+ name: "node9-check",
4927
+ type: "command",
4928
+ command: fullPathCommand("check"),
4929
+ timeout: 6e5
4930
+ }
4931
+ ]
4932
+ });
4933
+ console.log(chalk3.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
4934
+ anythingChanged = true;
4935
+ }
4936
+ const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
4937
+ (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
4938
+ );
4939
+ if (!hasAfterHook) {
4940
+ if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
4941
+ if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
4942
+ settings.hooks.AfterTool.push({
4943
+ matcher: ".*",
4944
+ hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
4945
+ });
4946
+ console.log(chalk3.green(" \u2705 AfterTool hook added \u2192 node9 log"));
4947
+ anythingChanged = true;
4948
+ }
4949
+ if (anythingChanged) {
4950
+ writeJson(settingsPath, settings);
4951
+ console.log("");
4952
+ }
4953
+ const serversToWrap = [];
4954
+ for (const [name, server] of Object.entries(servers)) {
4955
+ if (!server.command || server.command === "node9") continue;
4956
+ const parts = [server.command, ...server.args ?? []];
4957
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
4958
+ }
4959
+ if (serversToWrap.length > 0) {
4960
+ console.log(chalk3.bold("The following existing entries will be modified:\n"));
4961
+ console.log(chalk3.white(` ${settingsPath} (mcpServers)`));
4962
+ for (const { name, originalCmd } of serversToWrap) {
4963
+ console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
4964
+ }
4965
+ console.log("");
4966
+ const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
4967
+ if (proceed) {
4968
+ for (const { name, parts } of serversToWrap) {
4969
+ servers[name] = { ...servers[name], command: "node9", args: parts };
4137
4970
  }
4971
+ settings.mcpServers = servers;
4972
+ writeJson(settingsPath, settings);
4973
+ console.log(chalk3.green(`
4974
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
4975
+ anythingChanged = true;
4976
+ } else {
4977
+ console.log(chalk3.yellow(" Skipped MCP server wrapping."));
4138
4978
  }
4139
- if (req.method === "GET" && pathname === "/audit") {
4140
- res.writeHead(200, { "Content-Type": "application/json" });
4141
- return res.end(JSON.stringify(getAuditHistory()));
4979
+ console.log("");
4980
+ }
4981
+ if (!anythingChanged && serversToWrap.length === 0) {
4982
+ console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
4983
+ printDaemonTip();
4984
+ return;
4985
+ }
4986
+ if (anythingChanged) {
4987
+ console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
4988
+ console.log(chalk3.gray(" Restart Gemini CLI for changes to take effect."));
4989
+ printDaemonTip();
4990
+ }
4991
+ }
4992
+ async function setupCursor() {
4993
+ const homeDir2 = os3.homedir();
4994
+ const mcpPath = path5.join(homeDir2, ".cursor", "mcp.json");
4995
+ const mcpConfig = readJson(mcpPath) ?? {};
4996
+ const servers = mcpConfig.mcpServers ?? {};
4997
+ let anythingChanged = false;
4998
+ const serversToWrap = [];
4999
+ for (const [name, server] of Object.entries(servers)) {
5000
+ if (!server.command || server.command === "node9") continue;
5001
+ const parts = [server.command, ...server.args ?? []];
5002
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
5003
+ }
5004
+ if (serversToWrap.length > 0) {
5005
+ console.log(chalk3.bold("The following existing entries will be modified:\n"));
5006
+ console.log(chalk3.white(` ${mcpPath}`));
5007
+ for (const { name, originalCmd } of serversToWrap) {
5008
+ console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
4142
5009
  }
4143
- res.writeHead(404).end();
4144
- });
4145
- daemonServer = server;
4146
- server.on("error", (e) => {
4147
- if (e.code === "EADDRINUSE") {
4148
- try {
4149
- if (fs4.existsSync(DAEMON_PID_FILE)) {
4150
- const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4151
- process.kill(pid, 0);
4152
- return process.exit(0);
4153
- }
4154
- } catch {
4155
- try {
4156
- fs4.unlinkSync(DAEMON_PID_FILE);
4157
- } catch {
4158
- }
4159
- server.listen(DAEMON_PORT2, DAEMON_HOST2);
4160
- return;
5010
+ console.log("");
5011
+ const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
5012
+ if (proceed) {
5013
+ for (const { name, parts } of serversToWrap) {
5014
+ servers[name] = { ...servers[name], command: "node9", args: parts };
4161
5015
  }
5016
+ mcpConfig.mcpServers = servers;
5017
+ writeJson(mcpPath, mcpConfig);
5018
+ console.log(chalk3.green(`
5019
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
5020
+ anythingChanged = true;
5021
+ } else {
5022
+ console.log(chalk3.yellow(" Skipped MCP server wrapping."));
4162
5023
  }
4163
- console.error(chalk4.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
4164
- process.exit(1);
4165
- });
4166
- server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
4167
- atomicWriteSync2(
4168
- DAEMON_PID_FILE,
4169
- JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
4170
- { mode: 384 }
5024
+ console.log("");
5025
+ }
5026
+ console.log(
5027
+ chalk3.yellow(
5028
+ " \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
5029
+ )
5030
+ );
5031
+ console.log("");
5032
+ if (!anythingChanged && serversToWrap.length === 0) {
5033
+ console.log(
5034
+ chalk3.blue(
5035
+ "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
5036
+ )
4171
5037
  );
4172
- console.log(chalk4.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
4173
- });
4174
- }
4175
- function stopDaemon() {
4176
- if (!fs4.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
4177
- try {
4178
- const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4179
- process.kill(pid, "SIGTERM");
4180
- console.log(chalk4.green("\u2705 Stopped."));
4181
- } catch {
4182
- console.log(chalk4.gray("Cleaned up stale PID file."));
4183
- } finally {
4184
- try {
4185
- fs4.unlinkSync(DAEMON_PID_FILE);
4186
- } catch {
4187
- }
5038
+ printDaemonTip();
5039
+ return;
4188
5040
  }
4189
- }
4190
- function daemonStatus() {
4191
- if (!fs4.existsSync(DAEMON_PID_FILE))
4192
- return console.log(chalk4.yellow("Node9 daemon: not running"));
4193
- try {
4194
- const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4195
- process.kill(pid, 0);
4196
- console.log(chalk4.green("Node9 daemon: running"));
4197
- } catch {
4198
- console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
5041
+ if (anythingChanged) {
5042
+ console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
5043
+ console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
5044
+ printDaemonTip();
4199
5045
  }
4200
5046
  }
4201
5047
 
4202
5048
  // src/cli.ts
4203
- import { spawn as spawn3, execSync } from "child_process";
5049
+ init_daemon();
5050
+ import { spawn as spawn4, execSync } from "child_process";
4204
5051
  import { parseCommandString } from "execa";
4205
5052
  import { execa } from "execa";
4206
- import chalk5 from "chalk";
4207
- import readline from "readline";
4208
- import fs6 from "fs";
4209
- import path8 from "path";
4210
- import os6 from "os";
5053
+ import chalk6 from "chalk";
5054
+ import readline2 from "readline";
5055
+ import fs7 from "fs";
5056
+ import path9 from "path";
5057
+ import os7 from "os";
4211
5058
 
4212
5059
  // src/undo.ts
4213
5060
  import { spawnSync } from "child_process";
@@ -4321,9 +5168,10 @@ function applyUndo(hash, cwd) {
4321
5168
  }
4322
5169
 
4323
5170
  // src/cli.ts
5171
+ init_shields();
4324
5172
  import { confirm as confirm3 } from "@inquirer/prompts";
4325
5173
  var { version } = JSON.parse(
4326
- fs6.readFileSync(path8.join(__dirname, "../package.json"), "utf-8")
5174
+ fs7.readFileSync(path9.join(__dirname, "../package.json"), "utf-8")
4327
5175
  );
4328
5176
  function parseDuration(str) {
4329
5177
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -4419,7 +5267,7 @@ function openBrowserLocal() {
4419
5267
  }
4420
5268
  async function autoStartDaemonAndWait() {
4421
5269
  try {
4422
- const child = spawn3("node9", ["daemon"], {
5270
+ const child = spawn4("node9", ["daemon"], {
4423
5271
  detached: true,
4424
5272
  stdio: "ignore",
4425
5273
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
@@ -4455,14 +5303,14 @@ async function runProxy(targetCommand) {
4455
5303
  if (stdout) executable = stdout.trim();
4456
5304
  } catch {
4457
5305
  }
4458
- console.log(chalk5.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
4459
- const child = spawn3(executable, args, {
5306
+ console.log(chalk6.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
5307
+ const child = spawn4(executable, args, {
4460
5308
  stdio: ["pipe", "pipe", "inherit"],
4461
5309
  // We control STDIN and STDOUT
4462
5310
  shell: false,
4463
5311
  env: { ...process.env, FORCE_COLOR: "1" }
4464
5312
  });
4465
- const agentIn = readline.createInterface({ input: process.stdin, terminal: false });
5313
+ const agentIn = readline2.createInterface({ input: process.stdin, terminal: false });
4466
5314
  agentIn.on("line", async (line) => {
4467
5315
  let message;
4468
5316
  try {
@@ -4480,10 +5328,10 @@ async function runProxy(targetCommand) {
4480
5328
  agent: "Proxy/MCP"
4481
5329
  });
4482
5330
  if (!result.approved) {
4483
- console.error(chalk5.red(`
5331
+ console.error(chalk6.red(`
4484
5332
  \u{1F6D1} Node9 Sudo: Action Blocked`));
4485
- console.error(chalk5.gray(` Tool: ${name}`));
4486
- console.error(chalk5.gray(` Reason: ${result.reason || "Security Policy"}
5333
+ console.error(chalk6.gray(` Tool: ${name}`));
5334
+ console.error(chalk6.gray(` Reason: ${result.reason || "Security Policy"}
4487
5335
  `));
4488
5336
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
4489
5337
  const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -4525,14 +5373,14 @@ async function runProxy(targetCommand) {
4525
5373
  }
4526
5374
  program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
4527
5375
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
4528
- const credPath = path8.join(os6.homedir(), ".node9", "credentials.json");
4529
- if (!fs6.existsSync(path8.dirname(credPath)))
4530
- fs6.mkdirSync(path8.dirname(credPath), { recursive: true });
5376
+ const credPath = path9.join(os7.homedir(), ".node9", "credentials.json");
5377
+ if (!fs7.existsSync(path9.dirname(credPath)))
5378
+ fs7.mkdirSync(path9.dirname(credPath), { recursive: true });
4531
5379
  const profileName = options.profile || "default";
4532
5380
  let existingCreds = {};
4533
5381
  try {
4534
- if (fs6.existsSync(credPath)) {
4535
- const raw = JSON.parse(fs6.readFileSync(credPath, "utf-8"));
5382
+ if (fs7.existsSync(credPath)) {
5383
+ const raw = JSON.parse(fs7.readFileSync(credPath, "utf-8"));
4536
5384
  if (raw.apiKey) {
4537
5385
  existingCreds = {
4538
5386
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -4544,13 +5392,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4544
5392
  } catch {
4545
5393
  }
4546
5394
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
4547
- fs6.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
5395
+ fs7.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
4548
5396
  if (profileName === "default") {
4549
- const configPath = path8.join(os6.homedir(), ".node9", "config.json");
5397
+ const configPath = path9.join(os7.homedir(), ".node9", "config.json");
4550
5398
  let config = {};
4551
5399
  try {
4552
- if (fs6.existsSync(configPath))
4553
- config = JSON.parse(fs6.readFileSync(configPath, "utf-8"));
5400
+ if (fs7.existsSync(configPath))
5401
+ config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
4554
5402
  } catch {
4555
5403
  }
4556
5404
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -4565,36 +5413,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4565
5413
  approvers.cloud = false;
4566
5414
  }
4567
5415
  s.approvers = approvers;
4568
- if (!fs6.existsSync(path8.dirname(configPath)))
4569
- fs6.mkdirSync(path8.dirname(configPath), { recursive: true });
4570
- fs6.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
5416
+ if (!fs7.existsSync(path9.dirname(configPath)))
5417
+ fs7.mkdirSync(path9.dirname(configPath), { recursive: true });
5418
+ fs7.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
4571
5419
  }
4572
5420
  if (options.profile && profileName !== "default") {
4573
- console.log(chalk5.green(`\u2705 Profile "${profileName}" saved`));
4574
- console.log(chalk5.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
5421
+ console.log(chalk6.green(`\u2705 Profile "${profileName}" saved`));
5422
+ console.log(chalk6.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
4575
5423
  } else if (options.local) {
4576
- console.log(chalk5.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
4577
- console.log(chalk5.gray(` All decisions stay on this machine.`));
5424
+ console.log(chalk6.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
5425
+ console.log(chalk6.gray(` All decisions stay on this machine.`));
4578
5426
  } else {
4579
- console.log(chalk5.green(`\u2705 Logged in \u2014 agent mode`));
4580
- console.log(chalk5.gray(` Team policy enforced for all calls via Node9 cloud.`));
5427
+ console.log(chalk6.green(`\u2705 Logged in \u2014 agent mode`));
5428
+ console.log(chalk6.gray(` Team policy enforced for all calls via Node9 cloud.`));
4581
5429
  }
4582
5430
  });
4583
5431
  program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
4584
5432
  if (target === "gemini") return await setupGemini();
4585
5433
  if (target === "claude") return await setupClaude();
4586
5434
  if (target === "cursor") return await setupCursor();
4587
- console.error(chalk5.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
5435
+ console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
4588
5436
  process.exit(1);
4589
5437
  });
4590
5438
  program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
4591
5439
  if (!target) {
4592
- console.log(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
4593
- console.log(" Usage: " + chalk5.white("node9 setup <target>") + "\n");
5440
+ console.log(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
5441
+ console.log(" Usage: " + chalk6.white("node9 setup <target>") + "\n");
4594
5442
  console.log(" Targets:");
4595
- console.log(" " + chalk5.green("claude") + " \u2014 Claude Code (hook mode)");
4596
- console.log(" " + chalk5.green("gemini") + " \u2014 Gemini CLI (hook mode)");
4597
- console.log(" " + chalk5.green("cursor") + " \u2014 Cursor (hook mode)");
5443
+ console.log(" " + chalk6.green("claude") + " \u2014 Claude Code (hook mode)");
5444
+ console.log(" " + chalk6.green("gemini") + " \u2014 Gemini CLI (hook mode)");
5445
+ console.log(" " + chalk6.green("cursor") + " \u2014 Cursor (hook mode)");
4598
5446
  console.log("");
4599
5447
  return;
4600
5448
  }
@@ -4602,28 +5450,28 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
4602
5450
  if (t === "gemini") return await setupGemini();
4603
5451
  if (t === "claude") return await setupClaude();
4604
5452
  if (t === "cursor") return await setupCursor();
4605
- console.error(chalk5.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
5453
+ console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
4606
5454
  process.exit(1);
4607
5455
  });
4608
5456
  program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
4609
- const homeDir2 = os6.homedir();
5457
+ const homeDir2 = os7.homedir();
4610
5458
  let failures = 0;
4611
5459
  function pass(msg) {
4612
- console.log(chalk5.green(" \u2705 ") + msg);
5460
+ console.log(chalk6.green(" \u2705 ") + msg);
4613
5461
  }
4614
5462
  function fail(msg, hint) {
4615
- console.log(chalk5.red(" \u274C ") + msg);
4616
- if (hint) console.log(chalk5.gray(" " + hint));
5463
+ console.log(chalk6.red(" \u274C ") + msg);
5464
+ if (hint) console.log(chalk6.gray(" " + hint));
4617
5465
  failures++;
4618
5466
  }
4619
5467
  function warn(msg, hint) {
4620
- console.log(chalk5.yellow(" \u26A0\uFE0F ") + msg);
4621
- if (hint) console.log(chalk5.gray(" " + hint));
5468
+ console.log(chalk6.yellow(" \u26A0\uFE0F ") + msg);
5469
+ if (hint) console.log(chalk6.gray(" " + hint));
4622
5470
  }
4623
5471
  function section(title) {
4624
- console.log("\n" + chalk5.bold(title));
5472
+ console.log("\n" + chalk6.bold(title));
4625
5473
  }
4626
- console.log(chalk5.cyan.bold(`
5474
+ console.log(chalk6.cyan.bold(`
4627
5475
  \u{1F6E1}\uFE0F Node9 Doctor v${version}
4628
5476
  `));
4629
5477
  section("Binary");
@@ -4652,10 +5500,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4652
5500
  );
4653
5501
  }
4654
5502
  section("Configuration");
4655
- const globalConfigPath = path8.join(homeDir2, ".node9", "config.json");
4656
- if (fs6.existsSync(globalConfigPath)) {
5503
+ const globalConfigPath = path9.join(homeDir2, ".node9", "config.json");
5504
+ if (fs7.existsSync(globalConfigPath)) {
4657
5505
  try {
4658
- JSON.parse(fs6.readFileSync(globalConfigPath, "utf-8"));
5506
+ JSON.parse(fs7.readFileSync(globalConfigPath, "utf-8"));
4659
5507
  pass("~/.node9/config.json found and valid");
4660
5508
  } catch {
4661
5509
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -4663,17 +5511,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
4663
5511
  } else {
4664
5512
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
4665
5513
  }
4666
- const projectConfigPath = path8.join(process.cwd(), "node9.config.json");
4667
- if (fs6.existsSync(projectConfigPath)) {
5514
+ const projectConfigPath = path9.join(process.cwd(), "node9.config.json");
5515
+ if (fs7.existsSync(projectConfigPath)) {
4668
5516
  try {
4669
- JSON.parse(fs6.readFileSync(projectConfigPath, "utf-8"));
5517
+ JSON.parse(fs7.readFileSync(projectConfigPath, "utf-8"));
4670
5518
  pass("node9.config.json found and valid (project)");
4671
5519
  } catch {
4672
5520
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
4673
5521
  }
4674
5522
  }
4675
- const credsPath = path8.join(homeDir2, ".node9", "credentials.json");
4676
- if (fs6.existsSync(credsPath)) {
5523
+ const credsPath = path9.join(homeDir2, ".node9", "credentials.json");
5524
+ if (fs7.existsSync(credsPath)) {
4677
5525
  pass("Cloud credentials found (~/.node9/credentials.json)");
4678
5526
  } else {
4679
5527
  warn(
@@ -4682,10 +5530,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4682
5530
  );
4683
5531
  }
4684
5532
  section("Agent Hooks");
4685
- const claudeSettingsPath = path8.join(homeDir2, ".claude", "settings.json");
4686
- if (fs6.existsSync(claudeSettingsPath)) {
5533
+ const claudeSettingsPath = path9.join(homeDir2, ".claude", "settings.json");
5534
+ if (fs7.existsSync(claudeSettingsPath)) {
4687
5535
  try {
4688
- const cs = JSON.parse(fs6.readFileSync(claudeSettingsPath, "utf-8"));
5536
+ const cs = JSON.parse(fs7.readFileSync(claudeSettingsPath, "utf-8"));
4689
5537
  const hasHook = cs.hooks?.PreToolUse?.some(
4690
5538
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4691
5539
  );
@@ -4698,10 +5546,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4698
5546
  } else {
4699
5547
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
4700
5548
  }
4701
- const geminiSettingsPath = path8.join(homeDir2, ".gemini", "settings.json");
4702
- if (fs6.existsSync(geminiSettingsPath)) {
5549
+ const geminiSettingsPath = path9.join(homeDir2, ".gemini", "settings.json");
5550
+ if (fs7.existsSync(geminiSettingsPath)) {
4703
5551
  try {
4704
- const gs = JSON.parse(fs6.readFileSync(geminiSettingsPath, "utf-8"));
5552
+ const gs = JSON.parse(fs7.readFileSync(geminiSettingsPath, "utf-8"));
4705
5553
  const hasHook = gs.hooks?.BeforeTool?.some(
4706
5554
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4707
5555
  );
@@ -4714,10 +5562,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4714
5562
  } else {
4715
5563
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
4716
5564
  }
4717
- const cursorHooksPath = path8.join(homeDir2, ".cursor", "hooks.json");
4718
- if (fs6.existsSync(cursorHooksPath)) {
5565
+ const cursorHooksPath = path9.join(homeDir2, ".cursor", "hooks.json");
5566
+ if (fs7.existsSync(cursorHooksPath)) {
4719
5567
  try {
4720
- const cur = JSON.parse(fs6.readFileSync(cursorHooksPath, "utf-8"));
5568
+ const cur = JSON.parse(fs7.readFileSync(cursorHooksPath, "utf-8"));
4721
5569
  const hasHook = cur.hooks?.preToolUse?.some(
4722
5570
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
4723
5571
  );
@@ -4738,9 +5586,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
4738
5586
  }
4739
5587
  console.log("");
4740
5588
  if (failures === 0) {
4741
- console.log(chalk5.green.bold(" All checks passed. Node9 is ready.\n"));
5589
+ console.log(chalk6.green.bold(" All checks passed. Node9 is ready.\n"));
4742
5590
  } else {
4743
- console.log(chalk5.red.bold(` ${failures} check(s) failed. See hints above.
5591
+ console.log(chalk6.red.bold(` ${failures} check(s) failed. See hints above.
4744
5592
  `));
4745
5593
  process.exit(1);
4746
5594
  }
@@ -4755,7 +5603,7 @@ program.command("explain").description(
4755
5603
  try {
4756
5604
  args = JSON.parse(trimmed);
4757
5605
  } catch {
4758
- console.error(chalk5.red(`
5606
+ console.error(chalk6.red(`
4759
5607
  \u274C Invalid JSON: ${trimmed}
4760
5608
  `));
4761
5609
  process.exit(1);
@@ -4766,63 +5614,63 @@ program.command("explain").description(
4766
5614
  }
4767
5615
  const result = await explainPolicy(tool, args);
4768
5616
  console.log("");
4769
- console.log(chalk5.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
5617
+ console.log(chalk6.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
4770
5618
  console.log("");
4771
- console.log(` ${chalk5.bold("Tool:")} ${chalk5.white(result.tool)}`);
5619
+ console.log(` ${chalk6.bold("Tool:")} ${chalk6.white(result.tool)}`);
4772
5620
  if (argsRaw) {
4773
5621
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
4774
- console.log(` ${chalk5.bold("Input:")} ${chalk5.gray(preview)}`);
5622
+ console.log(` ${chalk6.bold("Input:")} ${chalk6.gray(preview)}`);
4775
5623
  }
4776
5624
  console.log("");
4777
- console.log(chalk5.bold("Config Sources (Waterfall):"));
5625
+ console.log(chalk6.bold("Config Sources (Waterfall):"));
4778
5626
  for (const tier of result.waterfall) {
4779
- const num = chalk5.gray(` ${tier.tier}.`);
5627
+ const num = chalk6.gray(` ${tier.tier}.`);
4780
5628
  const label = tier.label.padEnd(16);
4781
5629
  let statusStr;
4782
5630
  if (tier.tier === 1) {
4783
- statusStr = chalk5.gray(tier.note ?? "");
5631
+ statusStr = chalk6.gray(tier.note ?? "");
4784
5632
  } else if (tier.status === "active") {
4785
- const loc = tier.path ? chalk5.gray(tier.path) : "";
4786
- const note = tier.note ? chalk5.gray(`(${tier.note})`) : "";
4787
- statusStr = chalk5.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
5633
+ const loc = tier.path ? chalk6.gray(tier.path) : "";
5634
+ const note = tier.note ? chalk6.gray(`(${tier.note})`) : "";
5635
+ statusStr = chalk6.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
4788
5636
  } else {
4789
- statusStr = chalk5.gray("\u25CB " + (tier.note ?? "not found"));
5637
+ statusStr = chalk6.gray("\u25CB " + (tier.note ?? "not found"));
4790
5638
  }
4791
- console.log(`${num} ${chalk5.white(label)} ${statusStr}`);
5639
+ console.log(`${num} ${chalk6.white(label)} ${statusStr}`);
4792
5640
  }
4793
5641
  console.log("");
4794
- console.log(chalk5.bold("Policy Evaluation:"));
5642
+ console.log(chalk6.bold("Policy Evaluation:"));
4795
5643
  for (const step of result.steps) {
4796
5644
  const isFinal = step.isFinal;
4797
5645
  let icon;
4798
- if (step.outcome === "allow") icon = chalk5.green(" \u2705");
4799
- else if (step.outcome === "review") icon = chalk5.red(" \u{1F534}");
4800
- else if (step.outcome === "skip") icon = chalk5.gray(" \u2500 ");
4801
- else icon = chalk5.gray(" \u25CB ");
5646
+ if (step.outcome === "allow") icon = chalk6.green(" \u2705");
5647
+ else if (step.outcome === "review") icon = chalk6.red(" \u{1F534}");
5648
+ else if (step.outcome === "skip") icon = chalk6.gray(" \u2500 ");
5649
+ else icon = chalk6.gray(" \u25CB ");
4802
5650
  const name = step.name.padEnd(18);
4803
- const nameStr = isFinal ? chalk5.white.bold(name) : chalk5.white(name);
4804
- const detail = isFinal ? chalk5.white(step.detail) : chalk5.gray(step.detail);
4805
- const arrow = isFinal ? chalk5.yellow(" \u2190 STOP") : "";
5651
+ const nameStr = isFinal ? chalk6.white.bold(name) : chalk6.white(name);
5652
+ const detail = isFinal ? chalk6.white(step.detail) : chalk6.gray(step.detail);
5653
+ const arrow = isFinal ? chalk6.yellow(" \u2190 STOP") : "";
4806
5654
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
4807
5655
  }
4808
5656
  console.log("");
4809
5657
  if (result.decision === "allow") {
4810
- console.log(chalk5.green.bold(" Decision: \u2705 ALLOW") + chalk5.gray(" \u2014 no approval needed"));
5658
+ console.log(chalk6.green.bold(" Decision: \u2705 ALLOW") + chalk6.gray(" \u2014 no approval needed"));
4811
5659
  } else {
4812
5660
  console.log(
4813
- chalk5.red.bold(" Decision: \u{1F534} REVIEW") + chalk5.gray(" \u2014 human approval required")
5661
+ chalk6.red.bold(" Decision: \u{1F534} REVIEW") + chalk6.gray(" \u2014 human approval required")
4814
5662
  );
4815
5663
  if (result.blockedByLabel) {
4816
- console.log(chalk5.gray(` Reason: ${result.blockedByLabel}`));
5664
+ console.log(chalk6.gray(` Reason: ${result.blockedByLabel}`));
4817
5665
  }
4818
5666
  }
4819
5667
  console.log("");
4820
5668
  });
4821
5669
  program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
4822
- const configPath = path8.join(os6.homedir(), ".node9", "config.json");
4823
- if (fs6.existsSync(configPath) && !options.force) {
4824
- console.log(chalk5.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
4825
- console.log(chalk5.gray(` Run with --force to overwrite.`));
5670
+ const configPath = path9.join(os7.homedir(), ".node9", "config.json");
5671
+ if (fs7.existsSync(configPath) && !options.force) {
5672
+ console.log(chalk6.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
5673
+ console.log(chalk6.gray(` Run with --force to overwrite.`));
4826
5674
  return;
4827
5675
  }
4828
5676
  const requestedMode = options.mode.toLowerCase();
@@ -4834,13 +5682,13 @@ program.command("init").description("Create ~/.node9/config.json with default po
4834
5682
  mode: safeMode
4835
5683
  }
4836
5684
  };
4837
- const dir = path8.dirname(configPath);
4838
- if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
4839
- fs6.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
4840
- console.log(chalk5.green(`\u2705 Global config created: ${configPath}`));
4841
- console.log(chalk5.cyan(` Mode set to: ${safeMode}`));
5685
+ const dir = path9.dirname(configPath);
5686
+ if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
5687
+ fs7.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
5688
+ console.log(chalk6.green(`\u2705 Global config created: ${configPath}`));
5689
+ console.log(chalk6.cyan(` Mode set to: ${safeMode}`));
4842
5690
  console.log(
4843
- chalk5.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
5691
+ chalk6.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
4844
5692
  );
4845
5693
  });
4846
5694
  function formatRelativeTime(timestamp) {
@@ -4854,14 +5702,14 @@ function formatRelativeTime(timestamp) {
4854
5702
  return new Date(timestamp).toLocaleDateString();
4855
5703
  }
4856
5704
  program.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
4857
- const logPath = path8.join(os6.homedir(), ".node9", "audit.log");
4858
- if (!fs6.existsSync(logPath)) {
5705
+ const logPath = path9.join(os7.homedir(), ".node9", "audit.log");
5706
+ if (!fs7.existsSync(logPath)) {
4859
5707
  console.log(
4860
- chalk5.yellow("No audit logs found. Run node9 with an agent to generate entries.")
5708
+ chalk6.yellow("No audit logs found. Run node9 with an agent to generate entries.")
4861
5709
  );
4862
5710
  return;
4863
5711
  }
4864
- const raw = fs6.readFileSync(logPath, "utf-8");
5712
+ const raw = fs7.readFileSync(logPath, "utf-8");
4865
5713
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
4866
5714
  let entries = lines.flatMap((line) => {
4867
5715
  try {
@@ -4883,31 +5731,31 @@ program.command("audit").description("View local execution audit log").option("-
4883
5731
  return;
4884
5732
  }
4885
5733
  if (entries.length === 0) {
4886
- console.log(chalk5.yellow("No matching audit entries."));
5734
+ console.log(chalk6.yellow("No matching audit entries."));
4887
5735
  return;
4888
5736
  }
4889
5737
  console.log(
4890
5738
  `
4891
- ${chalk5.bold("Node9 Audit Log")} ${chalk5.dim(`(${entries.length} entries)`)}`
5739
+ ${chalk6.bold("Node9 Audit Log")} ${chalk6.dim(`(${entries.length} entries)`)}`
4892
5740
  );
4893
- console.log(chalk5.dim(" " + "\u2500".repeat(65)));
5741
+ console.log(chalk6.dim(" " + "\u2500".repeat(65)));
4894
5742
  console.log(
4895
5743
  ` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
4896
5744
  );
4897
- console.log(chalk5.dim(" " + "\u2500".repeat(65)));
5745
+ console.log(chalk6.dim(" " + "\u2500".repeat(65)));
4898
5746
  for (const e of entries) {
4899
5747
  const time = formatRelativeTime(String(e.ts)).padEnd(12);
4900
5748
  const tool = String(e.tool).slice(0, 17).padEnd(18);
4901
- const result = e.decision === "allow" ? chalk5.green("ALLOW".padEnd(10)) : chalk5.red("DENY".padEnd(10));
5749
+ const result = e.decision === "allow" ? chalk6.green("ALLOW".padEnd(10)) : chalk6.red("DENY".padEnd(10));
4902
5750
  const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
4903
5751
  const agent = String(e.agent || "unknown");
4904
5752
  console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
4905
5753
  }
4906
5754
  const allowed = entries.filter((e) => e.decision === "allow").length;
4907
5755
  const denied = entries.filter((e) => e.decision === "deny").length;
4908
- console.log(chalk5.dim(" " + "\u2500".repeat(65)));
5756
+ console.log(chalk6.dim(" " + "\u2500".repeat(65)));
4909
5757
  console.log(
4910
- ` ${entries.length} entries | ${chalk5.green(allowed + " allowed")} | ${chalk5.red(denied + " denied")}
5758
+ ` ${entries.length} entries | ${chalk6.green(allowed + " allowed")} | ${chalk6.red(denied + " denied")}
4911
5759
  `
4912
5760
  );
4913
5761
  });
@@ -4918,43 +5766,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
4918
5766
  const settings = mergedConfig.settings;
4919
5767
  console.log("");
4920
5768
  if (creds && settings.approvers.cloud) {
4921
- console.log(chalk5.green(" \u25CF Agent mode") + chalk5.gray(" \u2014 cloud team policy enforced"));
5769
+ console.log(chalk6.green(" \u25CF Agent mode") + chalk6.gray(" \u2014 cloud team policy enforced"));
4922
5770
  } else if (creds && !settings.approvers.cloud) {
4923
5771
  console.log(
4924
- chalk5.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk5.gray(" \u2014 all decisions stay on this machine")
5772
+ chalk6.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk6.gray(" \u2014 all decisions stay on this machine")
4925
5773
  );
4926
5774
  } else {
4927
5775
  console.log(
4928
- chalk5.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk5.gray(" \u2014 no API key (Local rules only)")
5776
+ chalk6.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk6.gray(" \u2014 no API key (Local rules only)")
4929
5777
  );
4930
5778
  }
4931
5779
  console.log("");
4932
5780
  if (daemonRunning) {
4933
5781
  console.log(
4934
- chalk5.green(" \u25CF Daemon running") + chalk5.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
5782
+ chalk6.green(" \u25CF Daemon running") + chalk6.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
4935
5783
  );
4936
5784
  } else {
4937
- console.log(chalk5.gray(" \u25CB Daemon stopped"));
5785
+ console.log(chalk6.gray(" \u25CB Daemon stopped"));
4938
5786
  }
4939
5787
  if (settings.enableUndo) {
4940
5788
  console.log(
4941
- chalk5.magenta(" \u25CF Undo Engine") + chalk5.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
5789
+ chalk6.magenta(" \u25CF Undo Engine") + chalk6.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
4942
5790
  );
4943
5791
  }
4944
5792
  console.log("");
4945
- const modeLabel = settings.mode === "audit" ? chalk5.blue("audit") : settings.mode === "strict" ? chalk5.red("strict") : chalk5.white("standard");
5793
+ const modeLabel = settings.mode === "audit" ? chalk6.blue("audit") : settings.mode === "strict" ? chalk6.red("strict") : chalk6.white("standard");
4946
5794
  console.log(` Mode: ${modeLabel}`);
4947
- const projectConfig = path8.join(process.cwd(), "node9.config.json");
4948
- const globalConfig = path8.join(os6.homedir(), ".node9", "config.json");
5795
+ const projectConfig = path9.join(process.cwd(), "node9.config.json");
5796
+ const globalConfig = path9.join(os7.homedir(), ".node9", "config.json");
4949
5797
  console.log(
4950
- ` Local: ${fs6.existsSync(projectConfig) ? chalk5.green("Active (node9.config.json)") : chalk5.gray("Not present")}`
5798
+ ` Local: ${fs7.existsSync(projectConfig) ? chalk6.green("Active (node9.config.json)") : chalk6.gray("Not present")}`
4951
5799
  );
4952
5800
  console.log(
4953
- ` Global: ${fs6.existsSync(globalConfig) ? chalk5.green("Active (~/.node9/config.json)") : chalk5.gray("Not present")}`
5801
+ ` Global: ${fs7.existsSync(globalConfig) ? chalk6.green("Active (~/.node9/config.json)") : chalk6.gray("Not present")}`
4954
5802
  );
4955
5803
  if (mergedConfig.policy.sandboxPaths.length > 0) {
4956
5804
  console.log(
4957
- ` Sandbox: ${chalk5.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
5805
+ ` Sandbox: ${chalk6.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
4958
5806
  );
4959
5807
  }
4960
5808
  const pauseState = checkPause();
@@ -4962,47 +5810,63 @@ program.command("status").description("Show current Node9 mode, policy source, a
4962
5810
  const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
4963
5811
  console.log("");
4964
5812
  console.log(
4965
- chalk5.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk5.gray(" \u2014 all tool calls allowed")
5813
+ chalk6.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk6.gray(" \u2014 all tool calls allowed")
4966
5814
  );
4967
5815
  }
4968
5816
  console.log("");
4969
5817
  });
4970
- program.command("daemon").description("Run the local approval server").argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").action(
5818
+ program.command("daemon").description("Run the local approval server").argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").option(
5819
+ "-w, --watch",
5820
+ "Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
5821
+ ).action(
4971
5822
  async (action, options) => {
4972
5823
  const cmd = (action ?? "start").toLowerCase();
4973
5824
  if (cmd === "stop") return stopDaemon();
4974
5825
  if (cmd === "status") return daemonStatus();
4975
5826
  if (cmd !== "start" && action !== void 0) {
4976
- console.error(chalk5.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
5827
+ console.error(chalk6.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
4977
5828
  process.exit(1);
4978
5829
  }
5830
+ if (options.watch) {
5831
+ process.env.NODE9_WATCH_MODE = "1";
5832
+ setTimeout(() => {
5833
+ openBrowserLocal();
5834
+ console.log(chalk6.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5835
+ }, 600);
5836
+ startDaemon();
5837
+ return;
5838
+ }
4979
5839
  if (options.openui) {
4980
5840
  if (isDaemonRunning()) {
4981
5841
  openBrowserLocal();
4982
- console.log(chalk5.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5842
+ console.log(chalk6.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
4983
5843
  process.exit(0);
4984
5844
  }
4985
- const child = spawn3("node9", ["daemon"], { detached: true, stdio: "ignore" });
5845
+ const child = spawn4("node9", ["daemon"], { detached: true, stdio: "ignore" });
4986
5846
  child.unref();
4987
5847
  for (let i = 0; i < 12; i++) {
4988
5848
  await new Promise((r) => setTimeout(r, 250));
4989
5849
  if (isDaemonRunning()) break;
4990
5850
  }
4991
5851
  openBrowserLocal();
4992
- console.log(chalk5.green(`
5852
+ console.log(chalk6.green(`
4993
5853
  \u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
4994
5854
  process.exit(0);
4995
5855
  }
4996
5856
  if (options.background) {
4997
- const child = spawn3("node9", ["daemon"], { detached: true, stdio: "ignore" });
5857
+ const child = spawn4("node9", ["daemon"], { detached: true, stdio: "ignore" });
4998
5858
  child.unref();
4999
- console.log(chalk5.green(`
5859
+ console.log(chalk6.green(`
5000
5860
  \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
5001
5861
  process.exit(0);
5002
5862
  }
5003
5863
  startDaemon();
5004
5864
  }
5005
5865
  );
5866
+ program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).action(async (options) => {
5867
+ const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
5868
+ await startTail2(options);
5869
+ });
5006
5870
  program.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
5007
5871
  const processPayload = async (raw) => {
5008
5872
  try {
@@ -5013,9 +5877,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
5013
5877
  } catch (err) {
5014
5878
  const tempConfig = getConfig();
5015
5879
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
5016
- const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
5880
+ const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
5017
5881
  const errMsg = err instanceof Error ? err.message : String(err);
5018
- fs6.appendFileSync(
5882
+ fs7.appendFileSync(
5019
5883
  logPath,
5020
5884
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
5021
5885
  RAW: ${raw}
@@ -5033,10 +5897,10 @@ RAW: ${raw}
5033
5897
  }
5034
5898
  const config = getConfig();
5035
5899
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
5036
- const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
5037
- if (!fs6.existsSync(path8.dirname(logPath)))
5038
- fs6.mkdirSync(path8.dirname(logPath), { recursive: true });
5039
- fs6.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
5900
+ const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
5901
+ if (!fs7.existsSync(path9.dirname(logPath)))
5902
+ fs7.mkdirSync(path9.dirname(logPath), { recursive: true });
5903
+ fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
5040
5904
  `);
5041
5905
  }
5042
5906
  const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
@@ -5048,18 +5912,18 @@ RAW: ${raw}
5048
5912
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
5049
5913
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
5050
5914
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
5051
- console.error(chalk5.bgRed.white.bold(`
5915
+ console.error(chalk6.bgRed.white.bold(`
5052
5916
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
5053
- console.error(chalk5.red.bold(` A sensitive secret was found in the tool arguments!`));
5917
+ console.error(chalk6.red.bold(` A sensitive secret was found in the tool arguments!`));
5054
5918
  } else {
5055
- console.error(chalk5.red(`
5919
+ console.error(chalk6.red(`
5056
5920
  \u{1F6D1} Node9 blocked "${toolName}"`));
5057
5921
  }
5058
- console.error(chalk5.gray(` Triggered by: ${blockedByContext}`));
5059
- if (result2?.changeHint) console.error(chalk5.cyan(` To change: ${result2.changeHint}`));
5922
+ console.error(chalk6.gray(` Triggered by: ${blockedByContext}`));
5923
+ if (result2?.changeHint) console.error(chalk6.cyan(` To change: ${result2.changeHint}`));
5060
5924
  console.error("");
5061
5925
  const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
5062
- console.error(chalk5.dim(` (Detailed instructions sent to AI agent)`));
5926
+ console.error(chalk6.dim(` (Detailed instructions sent to AI agent)`));
5063
5927
  process.stdout.write(
5064
5928
  JSON.stringify({
5065
5929
  decision: "block",
@@ -5090,7 +5954,7 @@ RAW: ${raw}
5090
5954
  process.exit(0);
5091
5955
  }
5092
5956
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
5093
- console.error(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
5957
+ console.error(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
5094
5958
  const daemonReady = await autoStartDaemonAndWait();
5095
5959
  if (daemonReady) {
5096
5960
  const retry = await authorizeHeadless(toolName, toolInput, false, meta);
@@ -5113,9 +5977,9 @@ RAW: ${raw}
5113
5977
  });
5114
5978
  } catch (err) {
5115
5979
  if (process.env.NODE9_DEBUG === "1") {
5116
- const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
5980
+ const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
5117
5981
  const errMsg = err instanceof Error ? err.message : String(err);
5118
- fs6.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
5982
+ fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
5119
5983
  `);
5120
5984
  }
5121
5985
  process.exit(0);
@@ -5160,10 +6024,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
5160
6024
  decision: "allowed",
5161
6025
  source: "post-hook"
5162
6026
  };
5163
- const logPath = path8.join(os6.homedir(), ".node9", "audit.log");
5164
- if (!fs6.existsSync(path8.dirname(logPath)))
5165
- fs6.mkdirSync(path8.dirname(logPath), { recursive: true });
5166
- fs6.appendFileSync(logPath, JSON.stringify(entry) + "\n");
6027
+ const logPath = path9.join(os7.homedir(), ".node9", "audit.log");
6028
+ if (!fs7.existsSync(path9.dirname(logPath)))
6029
+ fs7.mkdirSync(path9.dirname(logPath), { recursive: true });
6030
+ fs7.appendFileSync(logPath, JSON.stringify(entry) + "\n");
5167
6031
  const config = getConfig();
5168
6032
  if (shouldSnapshot(tool, {}, config)) {
5169
6033
  await createShadowSnapshot();
@@ -5190,7 +6054,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
5190
6054
  const ms = parseDuration(options.duration);
5191
6055
  if (ms === null) {
5192
6056
  console.error(
5193
- chalk5.red(`
6057
+ chalk6.red(`
5194
6058
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
5195
6059
  `)
5196
6060
  );
@@ -5198,20 +6062,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
5198
6062
  }
5199
6063
  pauseNode9(ms, options.duration);
5200
6064
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
5201
- console.log(chalk5.yellow(`
6065
+ console.log(chalk6.yellow(`
5202
6066
  \u23F8 Node9 paused until ${expiresAt}`));
5203
- console.log(chalk5.gray(` All tool calls will be allowed without review.`));
5204
- console.log(chalk5.gray(` Run "node9 resume" to re-enable early.
6067
+ console.log(chalk6.gray(` All tool calls will be allowed without review.`));
6068
+ console.log(chalk6.gray(` Run "node9 resume" to re-enable early.
5205
6069
  `));
5206
6070
  });
5207
6071
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
5208
6072
  const { paused } = checkPause();
5209
6073
  if (!paused) {
5210
- console.log(chalk5.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
6074
+ console.log(chalk6.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
5211
6075
  return;
5212
6076
  }
5213
6077
  resumeNode9();
5214
- console.log(chalk5.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
6078
+ console.log(chalk6.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
5215
6079
  });
5216
6080
  var HOOK_BASED_AGENTS = {
5217
6081
  claude: "claude",
@@ -5224,15 +6088,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
5224
6088
  if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
5225
6089
  const target = HOOK_BASED_AGENTS[firstArg];
5226
6090
  console.error(
5227
- chalk5.yellow(`
6091
+ chalk6.yellow(`
5228
6092
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
5229
6093
  );
5230
- console.error(chalk5.white(`
6094
+ console.error(chalk6.white(`
5231
6095
  "${target}" uses its own hook system. Use:`));
5232
6096
  console.error(
5233
- chalk5.green(` node9 addto ${target} `) + chalk5.gray("# one-time setup")
6097
+ chalk6.green(` node9 addto ${target} `) + chalk6.gray("# one-time setup")
5234
6098
  );
5235
- console.error(chalk5.green(` ${target} `) + chalk5.gray("# run normally"));
6099
+ console.error(chalk6.green(` ${target} `) + chalk6.gray("# run normally"));
5236
6100
  process.exit(1);
5237
6101
  }
5238
6102
  const fullCommand = commandArgs.join(" ");
@@ -5240,7 +6104,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
5240
6104
  agent: "Terminal"
5241
6105
  });
5242
6106
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
5243
- console.error(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
6107
+ console.error(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
5244
6108
  const daemonReady = await autoStartDaemonAndWait();
5245
6109
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
5246
6110
  }
@@ -5249,12 +6113,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
5249
6113
  }
5250
6114
  if (!result.approved) {
5251
6115
  console.error(
5252
- chalk5.red(`
6116
+ chalk6.red(`
5253
6117
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
5254
6118
  );
5255
6119
  process.exit(1);
5256
6120
  }
5257
- console.error(chalk5.green("\n\u2705 Approved \u2014 running command...\n"));
6121
+ console.error(chalk6.green("\n\u2705 Approved \u2014 running command...\n"));
5258
6122
  await runProxy(fullCommand);
5259
6123
  } else {
5260
6124
  program.help();
@@ -5269,22 +6133,22 @@ program.command("undo").description(
5269
6133
  if (history.length === 0) {
5270
6134
  if (!options.all && allHistory.length > 0) {
5271
6135
  console.log(
5272
- chalk5.yellow(
6136
+ chalk6.yellow(
5273
6137
  `
5274
6138
  \u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
5275
- Run ${chalk5.cyan("node9 undo --all")} to see snapshots from all projects.
6139
+ Run ${chalk6.cyan("node9 undo --all")} to see snapshots from all projects.
5276
6140
  `
5277
6141
  )
5278
6142
  );
5279
6143
  } else {
5280
- console.log(chalk5.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
6144
+ console.log(chalk6.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
5281
6145
  }
5282
6146
  return;
5283
6147
  }
5284
6148
  const idx = history.length - steps;
5285
6149
  if (idx < 0) {
5286
6150
  console.log(
5287
- chalk5.yellow(
6151
+ chalk6.yellow(
5288
6152
  `
5289
6153
  \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
5290
6154
  `
@@ -5295,18 +6159,18 @@ program.command("undo").description(
5295
6159
  const snapshot = history[idx];
5296
6160
  const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
5297
6161
  const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
5298
- console.log(chalk5.magenta.bold(`
6162
+ console.log(chalk6.magenta.bold(`
5299
6163
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
5300
6164
  console.log(
5301
- chalk5.white(
5302
- ` Tool: ${chalk5.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk5.gray(" \u2192 " + snapshot.argsSummary) : ""}`
6165
+ chalk6.white(
6166
+ ` Tool: ${chalk6.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk6.gray(" \u2192 " + snapshot.argsSummary) : ""}`
5303
6167
  )
5304
6168
  );
5305
- console.log(chalk5.white(` When: ${chalk5.gray(ageStr)}`));
5306
- console.log(chalk5.white(` Dir: ${chalk5.gray(snapshot.cwd)}`));
6169
+ console.log(chalk6.white(` When: ${chalk6.gray(ageStr)}`));
6170
+ console.log(chalk6.white(` Dir: ${chalk6.gray(snapshot.cwd)}`));
5307
6171
  if (steps > 1)
5308
6172
  console.log(
5309
- chalk5.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
6173
+ chalk6.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
5310
6174
  );
5311
6175
  console.log("");
5312
6176
  const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
@@ -5314,21 +6178,21 @@ program.command("undo").description(
5314
6178
  const lines = diff.split("\n");
5315
6179
  for (const line of lines) {
5316
6180
  if (line.startsWith("+++") || line.startsWith("---")) {
5317
- console.log(chalk5.bold(line));
6181
+ console.log(chalk6.bold(line));
5318
6182
  } else if (line.startsWith("+")) {
5319
- console.log(chalk5.green(line));
6183
+ console.log(chalk6.green(line));
5320
6184
  } else if (line.startsWith("-")) {
5321
- console.log(chalk5.red(line));
6185
+ console.log(chalk6.red(line));
5322
6186
  } else if (line.startsWith("@@")) {
5323
- console.log(chalk5.cyan(line));
6187
+ console.log(chalk6.cyan(line));
5324
6188
  } else {
5325
- console.log(chalk5.gray(line));
6189
+ console.log(chalk6.gray(line));
5326
6190
  }
5327
6191
  }
5328
6192
  console.log("");
5329
6193
  } else {
5330
6194
  console.log(
5331
- chalk5.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
6195
+ chalk6.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
5332
6196
  );
5333
6197
  }
5334
6198
  const proceed = await confirm3({
@@ -5337,42 +6201,42 @@ program.command("undo").description(
5337
6201
  });
5338
6202
  if (proceed) {
5339
6203
  if (applyUndo(snapshot.hash, snapshot.cwd)) {
5340
- console.log(chalk5.green("\n\u2705 Reverted successfully.\n"));
6204
+ console.log(chalk6.green("\n\u2705 Reverted successfully.\n"));
5341
6205
  } else {
5342
- console.error(chalk5.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
6206
+ console.error(chalk6.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
5343
6207
  }
5344
6208
  } else {
5345
- console.log(chalk5.gray("\nCancelled.\n"));
6209
+ console.log(chalk6.gray("\nCancelled.\n"));
5346
6210
  }
5347
6211
  });
5348
6212
  var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
5349
6213
  shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
5350
6214
  const name = resolveShieldName(service);
5351
6215
  if (!name) {
5352
- console.error(chalk5.red(`
6216
+ console.error(chalk6.red(`
5353
6217
  \u274C Unknown shield: "${service}"
5354
6218
  `));
5355
- console.log(`Run ${chalk5.cyan("node9 shield list")} to see available shields.
6219
+ console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
5356
6220
  `);
5357
6221
  process.exit(1);
5358
6222
  }
5359
6223
  const shield = getShield(name);
5360
6224
  const active = readActiveShields();
5361
6225
  if (active.includes(name)) {
5362
- console.log(chalk5.yellow(`
6226
+ console.log(chalk6.yellow(`
5363
6227
  \u2139\uFE0F Shield "${name}" is already active.
5364
6228
  `));
5365
6229
  return;
5366
6230
  }
5367
6231
  writeActiveShields([...active, name]);
5368
- console.log(chalk5.green(`
6232
+ console.log(chalk6.green(`
5369
6233
  \u{1F6E1}\uFE0F Shield "${name}" enabled.`));
5370
- console.log(chalk5.gray(` ${shield.smartRules.length} smart rules now active.`));
6234
+ console.log(chalk6.gray(` ${shield.smartRules.length} smart rules now active.`));
5371
6235
  if (shield.dangerousWords.length > 0)
5372
- console.log(chalk5.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
6236
+ console.log(chalk6.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
5373
6237
  if (name === "filesystem") {
5374
6238
  console.log(
5375
- chalk5.yellow(
6239
+ chalk6.yellow(
5376
6240
  `
5377
6241
  \u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
5378
6242
  Tools like unlink, find -delete, or language-level file ops are not intercepted.`
@@ -5384,51 +6248,51 @@ shieldCmd.command("enable <service>").description("Enable a security shield for
5384
6248
  shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
5385
6249
  const name = resolveShieldName(service);
5386
6250
  if (!name) {
5387
- console.error(chalk5.red(`
6251
+ console.error(chalk6.red(`
5388
6252
  \u274C Unknown shield: "${service}"
5389
6253
  `));
5390
- console.log(`Run ${chalk5.cyan("node9 shield list")} to see available shields.
6254
+ console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
5391
6255
  `);
5392
6256
  process.exit(1);
5393
6257
  }
5394
6258
  const active = readActiveShields();
5395
6259
  if (!active.includes(name)) {
5396
- console.log(chalk5.yellow(`
6260
+ console.log(chalk6.yellow(`
5397
6261
  \u2139\uFE0F Shield "${name}" is not active.
5398
6262
  `));
5399
6263
  return;
5400
6264
  }
5401
6265
  writeActiveShields(active.filter((s) => s !== name));
5402
- console.log(chalk5.green(`
6266
+ console.log(chalk6.green(`
5403
6267
  \u{1F6E1}\uFE0F Shield "${name}" disabled.
5404
6268
  `));
5405
6269
  });
5406
6270
  shieldCmd.command("list").description("Show all available shields").action(() => {
5407
6271
  const active = new Set(readActiveShields());
5408
- console.log(chalk5.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
6272
+ console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
5409
6273
  for (const shield of listShields()) {
5410
- const status = active.has(shield.name) ? chalk5.green("\u25CF enabled") : chalk5.gray("\u25CB disabled");
5411
- console.log(` ${status} ${chalk5.cyan(shield.name.padEnd(12))} ${shield.description}`);
6274
+ const status = active.has(shield.name) ? chalk6.green("\u25CF enabled") : chalk6.gray("\u25CB disabled");
6275
+ console.log(` ${status} ${chalk6.cyan(shield.name.padEnd(12))} ${shield.description}`);
5412
6276
  if (shield.aliases.length > 0)
5413
- console.log(chalk5.gray(` aliases: ${shield.aliases.join(", ")}`));
6277
+ console.log(chalk6.gray(` aliases: ${shield.aliases.join(", ")}`));
5414
6278
  }
5415
6279
  console.log("");
5416
6280
  });
5417
6281
  shieldCmd.command("status").description("Show which shields are currently active").action(() => {
5418
6282
  const active = readActiveShields();
5419
6283
  if (active.length === 0) {
5420
- console.log(chalk5.yellow("\n\u2139\uFE0F No shields are active.\n"));
5421
- console.log(`Run ${chalk5.cyan("node9 shield list")} to see available shields.
6284
+ console.log(chalk6.yellow("\n\u2139\uFE0F No shields are active.\n"));
6285
+ console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
5422
6286
  `);
5423
6287
  return;
5424
6288
  }
5425
- console.log(chalk5.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
6289
+ console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
5426
6290
  for (const name of active) {
5427
6291
  const shield = getShield(name);
5428
6292
  if (!shield) continue;
5429
- console.log(` ${chalk5.green("\u25CF")} ${chalk5.cyan(name)}`);
6293
+ console.log(` ${chalk6.green("\u25CF")} ${chalk6.cyan(name)}`);
5430
6294
  console.log(
5431
- chalk5.gray(
6295
+ chalk6.gray(
5432
6296
  ` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
5433
6297
  )
5434
6298
  );
@@ -5439,9 +6303,9 @@ process.on("unhandledRejection", (reason) => {
5439
6303
  const isCheckHook = process.argv[2] === "check";
5440
6304
  if (isCheckHook) {
5441
6305
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
5442
- const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
6306
+ const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
5443
6307
  const msg = reason instanceof Error ? reason.message : String(reason);
5444
- fs6.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
6308
+ fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
5445
6309
  `);
5446
6310
  }
5447
6311
  process.exit(0);