@node9/proxy 1.0.14 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.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,97 @@ 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
+ }).refine(
401
+ (c) => {
402
+ if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
403
+ return true;
404
+ },
405
+ { message: "matchesGlob and notMatchesGlob conditions require a value field" }
406
+ );
407
+ SmartRuleSchema = z.object({
408
+ name: z.string().optional(),
409
+ tool: z.string().min(1, "Smart rule tool must not be empty"),
410
+ conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
411
+ conditionMode: z.enum(["all", "any"]).optional(),
412
+ verdict: z.enum(["allow", "review", "block"], {
413
+ errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
414
+ }),
415
+ reason: z.string().optional()
416
+ });
417
+ ConfigFileSchema = z.object({
418
+ version: z.string().optional(),
419
+ settings: z.object({
420
+ mode: z.enum(["standard", "strict", "audit"]).optional(),
421
+ autoStartDaemon: z.boolean().optional(),
422
+ enableUndo: z.boolean().optional(),
423
+ enableHookLogDebug: z.boolean().optional(),
424
+ approvalTimeoutMs: z.number().nonnegative().optional(),
425
+ flightRecorder: z.boolean().optional(),
426
+ approvers: z.object({
427
+ native: z.boolean().optional(),
428
+ browser: z.boolean().optional(),
429
+ cloud: z.boolean().optional(),
430
+ terminal: z.boolean().optional()
431
+ }).optional(),
432
+ environment: z.string().optional(),
433
+ slackEnabled: z.boolean().optional(),
434
+ enableTrustSessions: z.boolean().optional(),
435
+ allowGlobalPause: z.boolean().optional()
436
+ }).optional(),
437
+ policy: z.object({
438
+ sandboxPaths: z.array(z.string()).optional(),
439
+ dangerousWords: z.array(noNewlines).optional(),
440
+ ignoredTools: z.array(z.string()).optional(),
441
+ toolInspection: z.record(z.string()).optional(),
442
+ smartRules: z.array(SmartRuleSchema).optional(),
443
+ snapshot: z.object({
444
+ tools: z.array(z.string()).optional(),
445
+ onlyPaths: z.array(z.string()).optional(),
446
+ ignorePaths: z.array(z.string()).optional()
447
+ }).optional(),
448
+ dlp: z.object({
449
+ enabled: z.boolean().optional(),
450
+ scanIgnoredTools: z.boolean().optional()
451
+ }).optional()
452
+ }).optional(),
453
+ environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
454
+ }).strict({ message: "Config contains unknown top-level keys" });
455
+ }
456
+ });
436
457
 
437
458
  // src/shields.ts
438
459
  import fs from "fs";
439
460
  import path3 from "path";
440
461
  import os from "os";
441
462
  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
463
  function resolveShieldName(input) {
610
464
  const lower = input.toLowerCase();
611
465
  if (SHIELDS[lower]) return lower;
@@ -621,7 +475,6 @@ function getShield(name) {
621
475
  function listShields() {
622
476
  return Object.values(SHIELDS);
623
477
  }
624
- var SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
625
478
  function readActiveShields() {
626
479
  try {
627
480
  const raw = fs.readFileSync(SHIELDS_STATE_FILE, "utf-8");
@@ -646,21 +499,182 @@ function writeActiveShields(active) {
646
499
  fs.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
647
500
  fs.renameSync(tmp, SHIELDS_STATE_FILE);
648
501
  }
502
+ var SHIELDS, SHIELDS_STATE_FILE;
503
+ var init_shields = __esm({
504
+ "src/shields.ts"() {
505
+ "use strict";
506
+ SHIELDS = {
507
+ postgres: {
508
+ name: "postgres",
509
+ description: "Protects PostgreSQL databases from destructive AI operations",
510
+ aliases: ["pg", "postgresql"],
511
+ smartRules: [
512
+ {
513
+ name: "shield:postgres:block-drop-table",
514
+ tool: "*",
515
+ conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
516
+ verdict: "block",
517
+ reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
518
+ },
519
+ {
520
+ name: "shield:postgres:block-truncate",
521
+ tool: "*",
522
+ conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
523
+ verdict: "block",
524
+ reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
525
+ },
526
+ {
527
+ name: "shield:postgres:block-drop-column",
528
+ tool: "*",
529
+ conditions: [
530
+ { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
531
+ ],
532
+ verdict: "block",
533
+ reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
534
+ },
535
+ {
536
+ name: "shield:postgres:review-grant-revoke",
537
+ tool: "*",
538
+ conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
539
+ verdict: "review",
540
+ reason: "Permission changes require human approval (Postgres shield)"
541
+ }
542
+ ],
543
+ dangerousWords: ["dropdb", "pg_dropcluster"]
544
+ },
545
+ github: {
546
+ name: "github",
547
+ description: "Protects GitHub repositories from destructive AI operations",
548
+ aliases: ["git"],
549
+ smartRules: [
550
+ {
551
+ // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
552
+ // This rule adds coverage for `git push --delete` which the built-in does not match.
553
+ name: "shield:github:review-delete-branch-remote",
554
+ tool: "bash",
555
+ conditions: [
556
+ {
557
+ field: "command",
558
+ op: "matches",
559
+ value: "git\\s+push\\s+.*--delete",
560
+ flags: "i"
561
+ }
562
+ ],
563
+ verdict: "review",
564
+ reason: "Remote branch deletion requires human approval (GitHub shield)"
565
+ },
566
+ {
567
+ name: "shield:github:block-delete-repo",
568
+ tool: "*",
569
+ conditions: [
570
+ { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
571
+ ],
572
+ verdict: "block",
573
+ reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
574
+ }
575
+ ],
576
+ dangerousWords: []
577
+ },
578
+ aws: {
579
+ name: "aws",
580
+ description: "Protects AWS infrastructure from destructive AI operations",
581
+ aliases: ["amazon"],
582
+ smartRules: [
583
+ {
584
+ name: "shield:aws:block-delete-s3-bucket",
585
+ tool: "*",
586
+ conditions: [
587
+ {
588
+ field: "command",
589
+ op: "matches",
590
+ value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
591
+ flags: "i"
592
+ }
593
+ ],
594
+ verdict: "block",
595
+ reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
596
+ },
597
+ {
598
+ name: "shield:aws:review-iam-changes",
599
+ tool: "*",
600
+ conditions: [
601
+ {
602
+ field: "command",
603
+ op: "matches",
604
+ value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
605
+ flags: "i"
606
+ }
607
+ ],
608
+ verdict: "review",
609
+ reason: "IAM changes require human approval (AWS shield)"
610
+ },
611
+ {
612
+ name: "shield:aws:block-ec2-terminate",
613
+ tool: "*",
614
+ conditions: [
615
+ {
616
+ field: "command",
617
+ op: "matches",
618
+ value: "aws\\s+ec2\\s+terminate-instances",
619
+ flags: "i"
620
+ }
621
+ ],
622
+ verdict: "block",
623
+ reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
624
+ },
625
+ {
626
+ name: "shield:aws:review-rds-delete",
627
+ tool: "*",
628
+ conditions: [
629
+ { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
630
+ ],
631
+ verdict: "review",
632
+ reason: "RDS deletion requires human approval (AWS shield)"
633
+ }
634
+ ],
635
+ dangerousWords: []
636
+ },
637
+ filesystem: {
638
+ name: "filesystem",
639
+ description: "Protects the local filesystem from dangerous AI operations",
640
+ aliases: ["fs"],
641
+ smartRules: [
642
+ {
643
+ name: "shield:filesystem:review-chmod-777",
644
+ tool: "bash",
645
+ conditions: [
646
+ { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
647
+ ],
648
+ verdict: "review",
649
+ reason: "chmod 777 requires human approval (filesystem shield)"
650
+ },
651
+ {
652
+ name: "shield:filesystem:review-write-etc",
653
+ tool: "bash",
654
+ conditions: [
655
+ {
656
+ field: "command",
657
+ // Narrow to write-indicative operations to avoid approval fatigue on reads.
658
+ // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
659
+ op: "matches",
660
+ value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
661
+ }
662
+ ],
663
+ verdict: "review",
664
+ reason: "Writing to /etc requires human approval (filesystem shield)"
665
+ }
666
+ ],
667
+ // dd removed: too common as a legitimate tool (disk imaging, file ops).
668
+ // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
669
+ // wipefs retained: rarely legitimate in an agent context and not in built-ins.
670
+ dangerousWords: ["wipefs"]
671
+ }
672
+ };
673
+ SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
674
+ }
675
+ });
649
676
 
650
677
  // 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
678
  function maskSecret(raw, pattern) {
665
679
  const match = raw.match(pattern);
666
680
  if (!match) return "****";
@@ -671,9 +685,6 @@ function maskSecret(raw, pattern) {
671
685
  const stars = "*".repeat(Math.min(secret.length - 8, 12));
672
686
  return `${prefix}${stars}${suffix}`;
673
687
  }
674
- var MAX_DEPTH = 5;
675
- var MAX_STRING_BYTES = 1e5;
676
- var MAX_JSON_PARSE_BYTES = 1e4;
677
688
  function scanArgs(args, depth = 0, fieldPath = "args") {
678
689
  if (depth > MAX_DEPTH || args === null || args === void 0) return null;
679
690
  if (Array.isArray(args)) {
@@ -716,12 +727,40 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
716
727
  }
717
728
  return null;
718
729
  }
730
+ var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
731
+ var init_dlp = __esm({
732
+ "src/dlp.ts"() {
733
+ "use strict";
734
+ DLP_PATTERNS = [
735
+ { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
736
+ { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
737
+ { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
738
+ { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
739
+ { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
740
+ {
741
+ name: "Private Key (PEM)",
742
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
743
+ severity: "block"
744
+ },
745
+ { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
746
+ ];
747
+ MAX_DEPTH = 5;
748
+ MAX_STRING_BYTES = 1e5;
749
+ MAX_JSON_PARSE_BYTES = 1e4;
750
+ }
751
+ });
719
752
 
720
753
  // 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");
754
+ import chalk2 from "chalk";
755
+ import { confirm } from "@inquirer/prompts";
756
+ import fs2 from "fs";
757
+ import path4 from "path";
758
+ import os2 from "os";
759
+ import net from "net";
760
+ import { randomUUID } from "crypto";
761
+ import { spawnSync } from "child_process";
762
+ import pm from "picomatch";
763
+ import { parse } from "sh-syntax";
725
764
  function checkPause() {
726
765
  try {
727
766
  if (!fs2.existsSync(PAUSED_FILE)) return { paused: false };
@@ -834,9 +873,9 @@ function matchesPattern(text, patterns) {
834
873
  const withoutDotSlash = text.replace(/^\.\//, "");
835
874
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
836
875
  }
837
- function getNestedValue(obj, path9) {
876
+ function getNestedValue(obj, path10) {
838
877
  if (!obj || typeof obj !== "object") return null;
839
- return path9.split(".").reduce((prev, curr) => prev?.[curr], obj);
878
+ return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
840
879
  }
841
880
  function shouldSnapshot(toolName, args, config) {
842
881
  if (!config.settings.enableUndo) return false;
@@ -884,7 +923,7 @@ function evaluateSmartConditions(args, rule) {
884
923
  case "matchesGlob":
885
924
  return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
886
925
  case "notMatchesGlob":
887
- return val !== null && cond.value ? !pm.isMatch(val, cond.value) : true;
926
+ return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
888
927
  default:
889
928
  return false;
890
929
  }
@@ -906,7 +945,6 @@ function isSqlTool(toolName, toolInspection) {
906
945
  const fieldName = toolInspection[matchingPattern];
907
946
  return fieldName === "sql" || fieldName === "query";
908
947
  }
909
- var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
910
948
  async function analyzeShellCommand(command) {
911
949
  const actions = [];
912
950
  const paths = [];
@@ -988,229 +1026,27 @@ function redactSecrets(text) {
988
1026
  );
989
1027
  return redacted;
990
1028
  }
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 {
1213
- mode: "standard",
1029
+ function _resetConfigCache() {
1030
+ cachedConfig = null;
1031
+ }
1032
+ function getGlobalSettings() {
1033
+ try {
1034
+ const globalConfigPath = path4.join(os2.homedir(), ".node9", "config.json");
1035
+ if (fs2.existsSync(globalConfigPath)) {
1036
+ const parsed = JSON.parse(fs2.readFileSync(globalConfigPath, "utf-8"));
1037
+ const settings = parsed.settings || {};
1038
+ return {
1039
+ mode: settings.mode || "audit",
1040
+ autoStartDaemon: settings.autoStartDaemon !== false,
1041
+ slackEnabled: settings.slackEnabled !== false,
1042
+ enableTrustSessions: settings.enableTrustSessions === true,
1043
+ allowGlobalPause: settings.allowGlobalPause !== false
1044
+ };
1045
+ }
1046
+ } catch {
1047
+ }
1048
+ return {
1049
+ mode: "audit",
1214
1050
  autoStartDaemon: true,
1215
1051
  slackEnabled: true,
1216
1052
  enableTrustSessions: false,
@@ -1596,16 +1432,24 @@ function isIgnoredTool(toolName) {
1596
1432
  const config = getConfig();
1597
1433
  return matchesPattern(toolName, config.policy.ignoredTools);
1598
1434
  }
1599
- var DAEMON_PORT = 7391;
1600
- var DAEMON_HOST = "127.0.0.1";
1601
1435
  function isDaemonRunning() {
1436
+ const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1437
+ if (fs2.existsSync(pidFile)) {
1438
+ try {
1439
+ const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1440
+ if (port !== DAEMON_PORT) return false;
1441
+ process.kill(pid, 0);
1442
+ return true;
1443
+ } catch {
1444
+ return false;
1445
+ }
1446
+ }
1602
1447
  try {
1603
- const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1604
- if (!fs2.existsSync(pidFile)) return false;
1605
- const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1606
- if (port !== DAEMON_PORT) return false;
1607
- process.kill(pid, 0);
1608
- return true;
1448
+ const r = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
1449
+ encoding: "utf8",
1450
+ timeout: 500
1451
+ });
1452
+ return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
1609
1453
  } catch {
1610
1454
  return false;
1611
1455
  }
@@ -1621,7 +1465,7 @@ function getPersistentDecision(toolName) {
1621
1465
  }
1622
1466
  return null;
1623
1467
  }
1624
- async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1468
+ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
1625
1469
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1626
1470
  const checkCtrl = new AbortController();
1627
1471
  const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
@@ -1636,6 +1480,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1636
1480
  args,
1637
1481
  agent: meta?.agent,
1638
1482
  mcpServer: meta?.mcpServer,
1483
+ fromCLI: true,
1484
+ // Pass the flight-recorder ID so the daemon uses the same UUID for
1485
+ // activity-result as the CLI used for the pending activity event.
1486
+ // Without this, the two UUIDs never match and tail.ts never resolves
1487
+ // the pending item.
1488
+ activityId,
1639
1489
  ...riskMetadata && { riskMetadata }
1640
1490
  }),
1641
1491
  signal: checkCtrl.signal
@@ -1690,7 +1540,44 @@ async function resolveViaDaemon(id, decision, internalToken) {
1690
1540
  signal: AbortSignal.timeout(3e3)
1691
1541
  });
1692
1542
  }
1543
+ function notifyActivity(data) {
1544
+ return new Promise((resolve) => {
1545
+ try {
1546
+ const payload = JSON.stringify(data);
1547
+ const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
1548
+ sock.on("connect", () => {
1549
+ sock.on("close", resolve);
1550
+ sock.end(payload);
1551
+ });
1552
+ sock.on("error", resolve);
1553
+ } catch {
1554
+ resolve();
1555
+ }
1556
+ });
1557
+ }
1693
1558
  async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
1559
+ if (!options?.calledFromDaemon) {
1560
+ const actId = randomUUID();
1561
+ const actTs = Date.now();
1562
+ await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
1563
+ const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
1564
+ ...options,
1565
+ activityId: actId
1566
+ });
1567
+ if (!result.noApprovalMechanism) {
1568
+ await notifyActivity({
1569
+ id: actId,
1570
+ tool: toolName,
1571
+ ts: actTs,
1572
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
1573
+ label: result.blockedByLabel
1574
+ });
1575
+ }
1576
+ return result;
1577
+ }
1578
+ return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
1579
+ }
1580
+ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
1694
1581
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1695
1582
  const pauseState = checkPause();
1696
1583
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
@@ -1726,6 +1613,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1726
1613
  blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
1727
1614
  };
1728
1615
  }
1616
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
1729
1617
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
1730
1618
  }
1731
1619
  }
@@ -1948,7 +1836,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1948
1836
  console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
1949
1837
  `));
1950
1838
  }
1951
- const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
1839
+ const daemonDecision = await askDaemon(
1840
+ toolName,
1841
+ args,
1842
+ meta,
1843
+ signal,
1844
+ riskMetadata,
1845
+ options?.activityId
1846
+ );
1952
1847
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
1953
1848
  const isApproved = daemonDecision === "allow";
1954
1849
  return {
@@ -2152,7 +2047,10 @@ function getConfig() {
2152
2047
  for (const rule of shield.smartRules) {
2153
2048
  if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
2154
2049
  }
2155
- for (const word of shield.dangerousWords) mergedPolicy.dangerousWords.push(word);
2050
+ const existingWords = new Set(mergedPolicy.dangerousWords);
2051
+ for (const word of shield.dangerousWords) {
2052
+ if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
2053
+ }
2156
2054
  }
2157
2055
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
2158
2056
  for (const rule of ADVISORY_SMART_RULES) {
@@ -2353,255 +2251,235 @@ async function resolveNode9SaaS(requestId, creds, approved) {
2353
2251
  } catch {
2354
2252
  }
2355
2253
  }
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 };
2254
+ 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;
2255
+ var init_core = __esm({
2256
+ "src/core.ts"() {
2257
+ "use strict";
2258
+ init_native();
2259
+ init_context_sniper();
2260
+ init_config_schema();
2261
+ init_shields();
2262
+ init_dlp();
2263
+ PAUSED_FILE = path4.join(os2.homedir(), ".node9", "PAUSED");
2264
+ TRUST_FILE = path4.join(os2.homedir(), ".node9", "trust.json");
2265
+ LOCAL_AUDIT_LOG = path4.join(os2.homedir(), ".node9", "audit.log");
2266
+ HOOK_DEBUG_LOG = path4.join(os2.homedir(), ".node9", "hook-debug.log");
2267
+ SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2268
+ DANGEROUS_WORDS = [
2269
+ "mkfs",
2270
+ // formats/wipes a filesystem partition
2271
+ "shred"
2272
+ // permanently overwrites file contents (unrecoverable)
2273
+ ];
2274
+ DEFAULT_CONFIG = {
2275
+ version: "1.0",
2276
+ settings: {
2277
+ mode: "audit",
2278
+ autoStartDaemon: true,
2279
+ enableUndo: true,
2280
+ // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
2281
+ enableHookLogDebug: true,
2282
+ approvalTimeoutMs: 3e4,
2283
+ // 30-second auto-deny timeout
2284
+ flightRecorder: true,
2285
+ approvers: { native: true, browser: true, cloud: false, terminal: true }
2286
+ },
2287
+ policy: {
2288
+ sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
2289
+ dangerousWords: DANGEROUS_WORDS,
2290
+ ignoredTools: [
2291
+ "list_*",
2292
+ "get_*",
2293
+ "read_*",
2294
+ "describe_*",
2295
+ "read",
2296
+ "glob",
2297
+ "grep",
2298
+ "ls",
2299
+ "notebookread",
2300
+ "notebookedit",
2301
+ "webfetch",
2302
+ "websearch",
2303
+ "exitplanmode",
2304
+ "askuserquestion",
2305
+ "agent",
2306
+ "task*",
2307
+ "toolsearch",
2308
+ "mcp__ide__*",
2309
+ "getDiagnostics"
2310
+ ],
2311
+ toolInspection: {
2312
+ bash: "command",
2313
+ shell: "command",
2314
+ run_shell_command: "command",
2315
+ "terminal.execute": "command",
2316
+ "postgres:query": "sql"
2317
+ },
2318
+ snapshot: {
2319
+ tools: [
2320
+ "str_replace_based_edit_tool",
2321
+ "write_file",
2322
+ "edit_file",
2323
+ "create_file",
2324
+ "edit",
2325
+ "replace"
2326
+ ],
2327
+ onlyPaths: [],
2328
+ ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
2329
+ },
2330
+ smartRules: [
2331
+ // ── rm safety (critical — always evaluated first) ──────────────────────
2332
+ {
2333
+ name: "block-rm-rf-home",
2334
+ tool: "bash",
2335
+ conditionMode: "all",
2336
+ conditions: [
2337
+ {
2338
+ field: "command",
2339
+ op: "matches",
2340
+ value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
2341
+ },
2342
+ {
2343
+ field: "command",
2344
+ op: "matches",
2345
+ value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
2346
+ }
2347
+ ],
2348
+ verdict: "block",
2349
+ reason: "Recursive delete of home directory is irreversible"
2350
+ },
2351
+ // ── SQL safety ────────────────────────────────────────────────────────
2352
+ {
2353
+ name: "no-delete-without-where",
2354
+ tool: "*",
2355
+ conditions: [
2356
+ { field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
2357
+ { field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
2358
+ ],
2359
+ conditionMode: "all",
2360
+ verdict: "review",
2361
+ reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
2362
+ },
2363
+ {
2364
+ name: "review-drop-truncate-shell",
2365
+ tool: "bash",
2366
+ conditions: [
2367
+ {
2368
+ field: "command",
2369
+ op: "matches",
2370
+ value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
2371
+ flags: "i"
2372
+ }
2373
+ ],
2374
+ conditionMode: "all",
2375
+ verdict: "review",
2376
+ reason: "SQL DDL destructive statement inside a shell command"
2377
+ },
2378
+ // ── Git safety ────────────────────────────────────────────────────────
2379
+ {
2380
+ name: "block-force-push",
2381
+ tool: "bash",
2382
+ conditions: [
2383
+ {
2384
+ field: "command",
2385
+ op: "matches",
2386
+ value: "git push.*(--force|--force-with-lease|-f\\b)",
2387
+ flags: "i"
2388
+ }
2389
+ ],
2390
+ conditionMode: "all",
2391
+ verdict: "block",
2392
+ reason: "Force push overwrites remote history and cannot be undone"
2393
+ },
2394
+ {
2395
+ name: "review-git-push",
2396
+ tool: "bash",
2397
+ conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
2398
+ conditionMode: "all",
2399
+ verdict: "review",
2400
+ reason: "git push sends changes to a shared remote"
2401
+ },
2402
+ {
2403
+ name: "review-git-destructive",
2404
+ tool: "bash",
2405
+ conditions: [
2406
+ {
2407
+ field: "command",
2408
+ op: "matches",
2409
+ value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
2410
+ flags: "i"
2411
+ }
2412
+ ],
2413
+ conditionMode: "all",
2414
+ verdict: "review",
2415
+ reason: "Destructive git operation \u2014 discards history or working-tree changes"
2416
+ },
2417
+ // ── Shell safety ──────────────────────────────────────────────────────
2418
+ {
2419
+ name: "review-sudo",
2420
+ tool: "bash",
2421
+ conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
2422
+ conditionMode: "all",
2423
+ verdict: "review",
2424
+ reason: "Command requires elevated privileges"
2425
+ },
2426
+ {
2427
+ name: "review-curl-pipe-shell",
2428
+ tool: "bash",
2429
+ conditions: [
2430
+ {
2431
+ field: "command",
2432
+ op: "matches",
2433
+ value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
2434
+ flags: "i"
2435
+ }
2436
+ ],
2437
+ conditionMode: "all",
2438
+ verdict: "block",
2439
+ reason: "Piping remote script into a shell is a supply-chain attack vector"
2440
+ }
2441
+ ],
2442
+ dlp: { enabled: true, scanIgnoredTools: true }
2443
+ },
2444
+ environments: {}
2445
+ };
2446
+ ADVISORY_SMART_RULES = [
2447
+ {
2448
+ name: "allow-rm-safe-paths",
2449
+ tool: "*",
2450
+ conditionMode: "all",
2451
+ conditions: [
2452
+ { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
2453
+ {
2454
+ field: "command",
2455
+ op: "matches",
2456
+ // Matches known-safe build artifact paths in the command.
2457
+ value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
2458
+ }
2459
+ ],
2460
+ verdict: "allow",
2461
+ reason: "Deleting a known-safe build artifact path"
2462
+ },
2463
+ {
2464
+ name: "review-rm",
2465
+ tool: "*",
2466
+ conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
2467
+ verdict: "review",
2468
+ reason: "rm can permanently delete files \u2014 confirm the target path"
2442
2469
  }
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;
2470
+ ];
2471
+ cachedConfig = null;
2472
+ DAEMON_PORT = 7391;
2473
+ DAEMON_HOST = "127.0.0.1";
2474
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path4.join(os2.tmpdir(), "node9-activity.sock");
2490
2475
  }
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("");
2507
- }
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
- }
2476
+ });
2602
2477
 
2603
2478
  // src/daemon/ui.html
2604
- var ui_default = `<!doctype html>
2479
+ var ui_default;
2480
+ var init_ui = __esm({
2481
+ "src/daemon/ui.html"() {
2482
+ ui_default = `<!doctype html>
2605
2483
  <html lang="en">
2606
2484
  <head>
2607
2485
  <meta charset="UTF-8" />
@@ -2627,6 +2505,11 @@ var ui_default = `<!doctype html>
2627
2505
  margin: 0;
2628
2506
  padding: 0;
2629
2507
  }
2508
+ html,
2509
+ body {
2510
+ height: 100%;
2511
+ overflow: hidden;
2512
+ }
2630
2513
  body {
2631
2514
  background: var(--bg);
2632
2515
  color: var(--text);
@@ -2634,16 +2517,17 @@ var ui_default = `<!doctype html>
2634
2517
  'Inter',
2635
2518
  -apple-system,
2636
2519
  sans-serif;
2637
- min-height: 100vh;
2638
2520
  }
2639
2521
 
2640
2522
  .shell {
2641
- max-width: 1000px;
2523
+ max-width: 1440px;
2524
+ height: 100vh;
2642
2525
  margin: 0 auto;
2643
- padding: 32px 24px 48px;
2526
+ padding: 16px 20px 16px;
2644
2527
  display: grid;
2645
2528
  grid-template-rows: auto 1fr;
2646
- gap: 24px;
2529
+ gap: 16px;
2530
+ overflow: hidden;
2647
2531
  }
2648
2532
  header {
2649
2533
  display: flex;
@@ -2680,9 +2564,10 @@ var ui_default = `<!doctype html>
2680
2564
 
2681
2565
  .body {
2682
2566
  display: grid;
2683
- grid-template-columns: 1fr 272px;
2684
- gap: 20px;
2685
- align-items: start;
2567
+ grid-template-columns: 360px 1fr 270px;
2568
+ gap: 16px;
2569
+ min-height: 0;
2570
+ overflow: hidden;
2686
2571
  }
2687
2572
 
2688
2573
  .warning-banner {
@@ -2702,6 +2587,10 @@ var ui_default = `<!doctype html>
2702
2587
 
2703
2588
  .main {
2704
2589
  min-width: 0;
2590
+ min-height: 0;
2591
+ overflow-y: auto;
2592
+ scrollbar-width: thin;
2593
+ scrollbar-color: var(--border) transparent;
2705
2594
  }
2706
2595
  .section-title {
2707
2596
  font-size: 11px;
@@ -2732,14 +2621,64 @@ var ui_default = `<!doctype html>
2732
2621
  background: var(--card);
2733
2622
  border: 1px solid var(--border);
2734
2623
  border-radius: 14px;
2735
- padding: 24px;
2736
- margin-bottom: 16px;
2624
+ padding: 20px;
2625
+ margin-bottom: 14px;
2737
2626
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
2738
2627
  animation: pop 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
2739
2628
  }
2740
2629
  .card.slack-viewer {
2741
2630
  border-color: rgba(83, 155, 245, 0.3);
2742
2631
  }
2632
+ .card-header {
2633
+ display: flex;
2634
+ align-items: center;
2635
+ gap: 8px;
2636
+ margin-bottom: 12px;
2637
+ padding-bottom: 12px;
2638
+ border-bottom: 1px solid var(--border);
2639
+ }
2640
+ .card-header-icon {
2641
+ font-size: 16px;
2642
+ }
2643
+ .card-header-title {
2644
+ font-size: 12px;
2645
+ font-weight: 700;
2646
+ color: var(--text-bright);
2647
+ text-transform: uppercase;
2648
+ letter-spacing: 0.5px;
2649
+ }
2650
+ .card-timer {
2651
+ margin-left: auto;
2652
+ font-size: 11px;
2653
+ font-family: 'Fira Code', monospace;
2654
+ color: var(--muted);
2655
+ background: rgba(48, 54, 61, 0.6);
2656
+ padding: 2px 8px;
2657
+ border-radius: 5px;
2658
+ }
2659
+ .card-timer.urgent {
2660
+ color: var(--danger);
2661
+ background: rgba(201, 60, 55, 0.1);
2662
+ }
2663
+ .btn-allow {
2664
+ background: var(--success);
2665
+ color: #fff;
2666
+ grid-column: span 2;
2667
+ font-size: 14px;
2668
+ padding: 13px 14px;
2669
+ }
2670
+ .btn-deny {
2671
+ background: rgba(201, 60, 55, 0.15);
2672
+ color: #e5534b;
2673
+ border: 1px solid rgba(201, 60, 55, 0.3);
2674
+ grid-column: span 2;
2675
+ }
2676
+ .btn-deny:hover:not(:disabled) {
2677
+ background: var(--danger);
2678
+ color: #fff;
2679
+ border-color: transparent;
2680
+ filter: none;
2681
+ }
2743
2682
  @keyframes pop {
2744
2683
  from {
2745
2684
  opacity: 0;
@@ -2947,24 +2886,178 @@ var ui_default = `<!doctype html>
2947
2886
  cursor: not-allowed;
2948
2887
  }
2949
2888
 
2889
+ .flight-col {
2890
+ display: flex;
2891
+ flex-direction: column;
2892
+ min-height: 0;
2893
+ overflow: hidden;
2894
+ }
2895
+ .flight-panel {
2896
+ flex: 1;
2897
+ min-height: 0;
2898
+ display: flex;
2899
+ flex-direction: column;
2900
+ overflow: hidden;
2901
+ }
2950
2902
  .sidebar {
2951
2903
  display: flex;
2952
2904
  flex-direction: column;
2953
2905
  gap: 12px;
2954
- position: sticky;
2955
- top: 24px;
2906
+ min-height: 0;
2907
+ overflow-y: auto;
2908
+ scrollbar-width: thin;
2909
+ scrollbar-color: var(--border) transparent;
2956
2910
  }
2957
2911
  .panel {
2958
2912
  background: var(--panel);
2959
2913
  border: 1px solid var(--border);
2960
2914
  border-radius: 12px;
2961
- padding: 16px;
2915
+ padding: 14px;
2916
+ }
2917
+ /* \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 */
2918
+ #activity-feed {
2919
+ display: flex;
2920
+ flex-direction: column;
2921
+ gap: 4px;
2922
+ margin-top: 4px;
2923
+ flex: 1;
2924
+ min-height: 0;
2925
+ overflow-y: auto;
2926
+ scrollbar-width: thin;
2927
+ scrollbar-color: var(--border) transparent;
2928
+ }
2929
+ .feed-row {
2930
+ display: grid;
2931
+ grid-template-columns: 58px 20px 1fr 48px;
2932
+ align-items: start;
2933
+ gap: 6px;
2934
+ background: rgba(22, 27, 34, 0.6);
2935
+ border: 1px solid var(--border);
2936
+ padding: 7px 10px;
2937
+ border-radius: 7px;
2938
+ font-size: 11px;
2939
+ animation: frSlideIn 0.15s ease-out;
2940
+ transition: background 0.1s;
2941
+ cursor: default;
2942
+ }
2943
+ .feed-row:hover {
2944
+ background: rgba(30, 38, 48, 0.9);
2945
+ border-color: rgba(83, 155, 245, 0.2);
2946
+ }
2947
+ @keyframes frSlideIn {
2948
+ from {
2949
+ opacity: 0;
2950
+ transform: translateX(-4px);
2951
+ }
2952
+ to {
2953
+ opacity: 1;
2954
+ transform: none;
2955
+ }
2956
+ }
2957
+ .feed-ts {
2958
+ color: var(--muted);
2959
+ font-family: monospace;
2960
+ font-size: 9px;
2961
+ }
2962
+ .feed-icon {
2963
+ text-align: center;
2964
+ font-size: 13px;
2965
+ }
2966
+ .feed-content {
2967
+ min-width: 0;
2968
+ color: var(--text-bright);
2969
+ word-break: break-all;
2970
+ }
2971
+ .feed-args {
2972
+ display: block;
2973
+ color: var(--muted);
2974
+ font-family: monospace;
2975
+ margin-top: 2px;
2976
+ font-size: 10px;
2977
+ word-break: break-all;
2978
+ }
2979
+ .feed-badge {
2980
+ text-align: right;
2981
+ font-weight: 700;
2982
+ font-size: 9px;
2983
+ letter-spacing: 0.03em;
2984
+ }
2985
+ .fr-pending {
2986
+ color: var(--muted);
2987
+ }
2988
+ .fr-allow {
2989
+ color: #57ab5a;
2990
+ }
2991
+ .fr-block {
2992
+ color: var(--danger);
2993
+ }
2994
+ .fr-dlp {
2995
+ color: var(--primary);
2996
+ animation: frBlink 1s infinite;
2962
2997
  }
2998
+ @keyframes frBlink {
2999
+ 50% {
3000
+ opacity: 0.4;
3001
+ }
3002
+ }
3003
+ .fr-dlp-row {
3004
+ border-color: var(--primary) !important;
3005
+ }
3006
+ .feed-clear-btn {
3007
+ background: transparent;
3008
+ border: none;
3009
+ color: var(--muted);
3010
+ font-size: 10px;
3011
+ padding: 0;
3012
+ cursor: pointer;
3013
+ margin-left: auto;
3014
+ font-family: inherit;
3015
+ font-weight: 500;
3016
+ transition: color 0.15s;
3017
+ }
3018
+ .feed-clear-btn:hover {
3019
+ color: var(--text);
3020
+ filter: none;
3021
+ transform: none;
3022
+ }
3023
+ /* \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 */
3024
+ .shield-row {
3025
+ display: flex;
3026
+ align-items: flex-start;
3027
+ gap: 10px;
3028
+ padding: 8px 0;
3029
+ border-bottom: 1px solid var(--border);
3030
+ }
3031
+ .shield-row:last-child {
3032
+ border-bottom: none;
3033
+ padding-bottom: 0;
3034
+ }
3035
+ .shield-row:first-child {
3036
+ padding-top: 0;
3037
+ }
3038
+ .shield-info {
3039
+ flex: 1;
3040
+ min-width: 0;
3041
+ }
3042
+ .shield-name {
3043
+ font-size: 12px;
3044
+ color: var(--text-bright);
3045
+ font-weight: 600;
3046
+ font-family: 'Fira Code', monospace;
3047
+ }
3048
+ .shield-desc {
3049
+ font-size: 10px;
3050
+ color: var(--muted);
3051
+ margin-top: 2px;
3052
+ line-height: 1.4;
3053
+ }
3054
+
2963
3055
  .panel-title {
2964
3056
  font-size: 12px;
2965
3057
  font-weight: 700;
2966
3058
  color: var(--text-bright);
2967
3059
  margin-bottom: 12px;
3060
+ flex-shrink: 0;
2968
3061
  display: flex;
2969
3062
  align-items: center;
2970
3063
  gap: 6px;
@@ -2972,8 +3065,8 @@ var ui_default = `<!doctype html>
2972
3065
  .setting-row {
2973
3066
  display: flex;
2974
3067
  align-items: flex-start;
2975
- gap: 12px;
2976
- margin-bottom: 12px;
3068
+ gap: 10px;
3069
+ margin-bottom: 8px;
2977
3070
  }
2978
3071
  .setting-row:last-child {
2979
3072
  margin-bottom: 0;
@@ -2982,20 +3075,21 @@ var ui_default = `<!doctype html>
2982
3075
  flex: 1;
2983
3076
  }
2984
3077
  .setting-label {
2985
- font-size: 12px;
3078
+ font-size: 11px;
2986
3079
  color: var(--text-bright);
2987
- margin-bottom: 3px;
3080
+ margin-bottom: 2px;
3081
+ font-weight: 600;
2988
3082
  }
2989
3083
  .setting-desc {
2990
- font-size: 11px;
3084
+ font-size: 10px;
2991
3085
  color: var(--muted);
2992
- line-height: 1.5;
3086
+ line-height: 1.4;
2993
3087
  }
2994
3088
  .toggle {
2995
3089
  position: relative;
2996
3090
  display: inline-block;
2997
- width: 40px;
2998
- height: 22px;
3091
+ width: 34px;
3092
+ height: 19px;
2999
3093
  flex-shrink: 0;
3000
3094
  margin-top: 1px;
3001
3095
  }
@@ -3015,8 +3109,8 @@ var ui_default = `<!doctype html>
3015
3109
  .slider:before {
3016
3110
  content: '';
3017
3111
  position: absolute;
3018
- width: 16px;
3019
- height: 16px;
3112
+ width: 13px;
3113
+ height: 13px;
3020
3114
  left: 3px;
3021
3115
  bottom: 3px;
3022
3116
  background: #fff;
@@ -3027,7 +3121,7 @@ var ui_default = `<!doctype html>
3027
3121
  background: var(--success);
3028
3122
  }
3029
3123
  input:checked + .slider:before {
3030
- transform: translateX(18px);
3124
+ transform: translateX(15px);
3031
3125
  }
3032
3126
  input:disabled + .slider {
3033
3127
  opacity: 0.4;
@@ -3186,12 +3280,17 @@ var ui_default = `<!doctype html>
3186
3280
  border: 1px solid var(--border);
3187
3281
  }
3188
3282
 
3189
- @media (max-width: 680px) {
3283
+ @media (max-width: 960px) {
3190
3284
  .body {
3191
- grid-template-columns: 1fr;
3285
+ grid-template-columns: 1fr 220px;
3286
+ }
3287
+ .flight-col {
3288
+ display: none;
3192
3289
  }
3193
- .sidebar {
3194
- position: static;
3290
+ }
3291
+ @media (max-width: 640px) {
3292
+ .body {
3293
+ grid-template-columns: 1fr;
3195
3294
  }
3196
3295
  }
3197
3296
  </style>
@@ -3205,6 +3304,19 @@ var ui_default = `<!doctype html>
3205
3304
  </header>
3206
3305
 
3207
3306
  <div class="body">
3307
+ <div class="flight-col">
3308
+ <div class="panel flight-panel">
3309
+ <div class="panel-title">
3310
+ \u{1F6F0}\uFE0F Flight Recorder
3311
+ <span style="font-weight: 400; color: var(--muted); font-size: 11px">live</span>
3312
+ <button class="feed-clear-btn" onclick="clearFeed()">clear</button>
3313
+ </div>
3314
+ <div id="activity-feed">
3315
+ <span class="decisions-empty">Waiting for agent activity\u2026</span>
3316
+ </div>
3317
+ </div>
3318
+ </div>
3319
+
3208
3320
  <div class="main">
3209
3321
  <div id="warnBanner" class="warning-banner">
3210
3322
  \u26A0\uFE0F Auto-start is off \u2014 daemon started manually. Run "node9 daemon stop" to stop it, or
@@ -3285,6 +3397,11 @@ var ui_default = `<!doctype html>
3285
3397
  <div id="slackStatusLine" class="slack-status-line">No key saved</div>
3286
3398
  </div>
3287
3399
 
3400
+ <div class="panel">
3401
+ <div class="panel-title">\u{1F6E1}\uFE0F Active Shields</div>
3402
+ <div id="shieldsList"><span class="decisions-empty">Loading\u2026</span></div>
3403
+ </div>
3404
+
3288
3405
  <div class="panel">
3289
3406
  <div class="panel-title">\u{1F4CB} Persistent Decisions</div>
3290
3407
  <div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
@@ -3330,14 +3447,23 @@ var ui_default = `<!doctype html>
3330
3447
 
3331
3448
  function updateDenyButton(id, timestamp) {
3332
3449
  const btn = document.querySelector('#c-' + id + ' .btn-deny');
3450
+ const timer = document.querySelector('#timer-' + id);
3333
3451
  if (!btn) return;
3334
3452
  const elapsed = Date.now() - timestamp;
3335
3453
  const remaining = Math.max(0, Math.ceil((autoDenyMs - elapsed) / 1000));
3336
3454
  if (remaining <= 0) {
3337
- btn.textContent = 'Auto-Denying...';
3455
+ btn.textContent = '\u23F3 Auto-Denying\u2026';
3338
3456
  btn.disabled = true;
3457
+ if (timer) {
3458
+ timer.textContent = 'auto-deny';
3459
+ timer.className = 'card-timer urgent';
3460
+ }
3339
3461
  } else {
3340
- btn.textContent = 'Block Action (' + remaining + 's)';
3462
+ btn.textContent = '\u{1F6AB} Block this Action';
3463
+ if (timer) {
3464
+ timer.textContent = remaining + 's';
3465
+ timer.className = 'card-timer' + (remaining < 15 ? ' urgent' : '');
3466
+ }
3341
3467
  setTimeout(() => updateDenyButton(id, timestamp), 1000);
3342
3468
  }
3343
3469
  }
@@ -3353,34 +3479,61 @@ var ui_default = `<!doctype html>
3353
3479
  empty.style.display = requests.size === 0 ? 'block' : 'none';
3354
3480
  }
3355
3481
 
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(() => {
3482
+ function setCardBusy(card, busy) {
3483
+ if (!card) return;
3484
+ card.querySelectorAll('button').forEach((b) => (b.disabled = busy));
3485
+ card.style.opacity = busy ? '0.5' : '1';
3486
+ }
3487
+
3488
+ function showCardError(card, msg) {
3489
+ if (!card) return;
3490
+ card.style.outline = '2px solid #f87171';
3491
+ let err = card.querySelector('.card-error');
3492
+ if (!err) {
3493
+ err = document.createElement('p');
3494
+ err.className = 'card-error';
3495
+ err.style.cssText = 'color:#f87171;font-size:11px;margin:6px 0 0;';
3496
+ card.appendChild(err);
3497
+ }
3498
+ err.textContent = '\u26A0 ' + msg + ' \u2014 please try again or refresh.';
3499
+ }
3500
+
3501
+ async function sendDecision(id, decision, persist) {
3502
+ const card = document.getElementById('c-' + id);
3503
+ setCardBusy(card, true);
3504
+ try {
3505
+ const res = await fetch('/decision/' + id, {
3506
+ method: 'POST',
3507
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3508
+ body: JSON.stringify({ decision, persist: !!persist }),
3509
+ });
3510
+ if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3365
3511
  card?.remove();
3366
3512
  requests.delete(id);
3367
3513
  refresh();
3368
- }, 200);
3514
+ } catch (err) {
3515
+ setCardBusy(card, false);
3516
+ showCardError(card, err.message || 'Network error');
3517
+ }
3369
3518
  }
3370
3519
 
3371
- function sendTrust(id, duration) {
3520
+ async function sendTrust(id, duration) {
3372
3521
  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(() => {
3522
+ setCardBusy(card, true);
3523
+ try {
3524
+ const res = await fetch('/decision/' + id, {
3525
+ method: 'POST',
3526
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3527
+ body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
3528
+ });
3529
+ if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3380
3530
  card?.remove();
3381
3531
  requests.delete(id);
3382
3532
  refresh();
3383
- }, 200);
3533
+ } catch (err) {
3534
+ setCardBusy(card, false);
3535
+ showCardError(card, err.message || 'Network error');
3536
+ }
3384
3537
  }
3385
3538
 
3386
3539
  function renderPayload(req) {
@@ -3431,16 +3584,21 @@ var ui_default = `<!doctype html>
3431
3584
  const mcpLabel = req.mcpServer ? esc(req.mcpServer) : null;
3432
3585
  const dis = isSlack ? 'disabled' : '';
3433
3586
  card.innerHTML = \`
3587
+ <div class="card-header">
3588
+ <span class="card-header-icon">\${isSlack ? '\u26A1' : '\u26A0\uFE0F'}</span>
3589
+ <span class="card-header-title">\${isSlack ? 'Awaiting Cloud Approval' : 'Action Required'}</span>
3590
+ <span class="card-timer" id="timer-\${req.id}">\${autoDenyMs > 0 ? Math.ceil(autoDenyMs / 1000) + 's' : ''}</span>
3591
+ </div>
3434
3592
  <div class="source-row">
3435
3593
  <span class="agent-badge">\${agentLabel}</span>
3436
3594
  \${mcpLabel ? \`<span class="source-arrow">\u2192</span><span class="mcp-badge">mcp::\${mcpLabel}</span>\` : ''}
3437
3595
  </div>
3438
3596
  <div class="tool-chip">\${esc(req.toolName)}</div>
3439
- \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Slack approval \u2014 view only</div>' : ''}
3597
+ \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
3440
3598
  \${renderPayload(req)}
3441
3599
  <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>
3600
+ <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
3601
+ <button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>\u{1F6AB} Block this Action</button>
3444
3602
  <div class="trust-row\${trustEnabled ? ' show' : ''}" id="tr-\${req.id}">
3445
3603
  <button class="btn-trust" onclick="sendTrust('\${req.id}','30m')" \${dis}>\u23F1 Trust 30m</button>
3446
3604
  <button class="btn-trust" onclick="sendTrust('\${req.id}','1h')" \${dis}>\u23F1 Trust 1h</button>
@@ -3500,9 +3658,84 @@ var ui_default = `<!doctype html>
3500
3658
  ev.addEventListener('slack-status', (e) => {
3501
3659
  applySlackStatus(JSON.parse(e.data));
3502
3660
  });
3661
+ ev.addEventListener('shields-status', (e) => {
3662
+ renderShields(JSON.parse(e.data).shields);
3663
+ });
3664
+
3665
+ // \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
3666
+ ev.addEventListener('activity', (e) => {
3667
+ const data = JSON.parse(e.data);
3668
+ const feed = document.getElementById('activity-feed');
3669
+ // Remove placeholder on first item
3670
+ const placeholder = feed.querySelector('.decisions-empty');
3671
+ if (placeholder) placeholder.remove();
3672
+
3673
+ const time = new Date(data.ts).toLocaleTimeString([], {
3674
+ hour12: false,
3675
+ hour: '2-digit',
3676
+ minute: '2-digit',
3677
+ second: '2-digit',
3678
+ });
3679
+ const icon = frIcon(data.tool);
3680
+ const argsStr = JSON.stringify(data.args ?? {});
3681
+ const argsPreview = esc(argsStr.length > 120 ? argsStr.slice(0, 120) + '\u2026' : argsStr);
3682
+
3683
+ const row = document.createElement('div');
3684
+ row.className = 'feed-row';
3685
+ row.id = 'fr-' + data.id;
3686
+ row.innerHTML = \`
3687
+ <span class="feed-ts">\${time}</span>
3688
+ <span class="feed-icon">\${icon}</span>
3689
+ <span class="feed-content"><strong>\${esc(data.tool)}</strong><span class="feed-args">\${argsPreview}</span></span>
3690
+ <span class="feed-badge fr-pending">\u25CF</span>
3691
+ \`;
3692
+ feed.prepend(row);
3693
+ if (feed.children.length > 100) feed.lastChild.remove();
3694
+ });
3695
+
3696
+ ev.addEventListener('activity-result', (e) => {
3697
+ const { id, status, label } = JSON.parse(e.data);
3698
+ const row = document.getElementById('fr-' + id);
3699
+ if (!row) return;
3700
+ const badge = row.querySelector('.feed-badge');
3701
+ if (status === 'allow') {
3702
+ badge.textContent = 'ALLOW';
3703
+ badge.className = 'feed-badge fr-allow';
3704
+ } else if (status === 'dlp') {
3705
+ badge.textContent = '\u{1F6E1}\uFE0F DLP';
3706
+ badge.className = 'feed-badge fr-dlp';
3707
+ row.classList.add('fr-dlp-row');
3708
+ } else {
3709
+ badge.textContent = 'BLOCK';
3710
+ badge.className = 'feed-badge fr-block';
3711
+ }
3712
+ });
3503
3713
  }
3504
3714
  connect();
3505
3715
 
3716
+ const FR_ICONS = {
3717
+ bash: '\u{1F4BB}',
3718
+ read: '\u{1F4D6}',
3719
+ edit: '\u270F\uFE0F',
3720
+ write: '\u270F\uFE0F',
3721
+ glob: '\u{1F4C2}',
3722
+ grep: '\u{1F50D}',
3723
+ agent: '\u{1F916}',
3724
+ search: '\u{1F50D}',
3725
+ sql: '\u{1F5C4}\uFE0F',
3726
+ query: '\u{1F5C4}\uFE0F',
3727
+ list: '\u{1F4C2}',
3728
+ delete: '\u{1F5D1}\uFE0F',
3729
+ web: '\u{1F310}',
3730
+ };
3731
+ function frIcon(tool) {
3732
+ const t = (tool || '').toLowerCase();
3733
+ for (const [k, v] of Object.entries(FR_ICONS)) {
3734
+ if (t.includes(k)) return v;
3735
+ }
3736
+ return '\u{1F6E0}\uFE0F';
3737
+ }
3738
+
3506
3739
  function saveSetting(key, value) {
3507
3740
  fetch('/settings', {
3508
3741
  method: 'POST',
@@ -3592,6 +3825,49 @@ var ui_default = `<!doctype html>
3592
3825
  }
3593
3826
  }
3594
3827
 
3828
+ function clearFeed() {
3829
+ const feed = document.getElementById('activity-feed');
3830
+ feed.innerHTML = '<span class="decisions-empty">Feed cleared.</span>';
3831
+ }
3832
+
3833
+ function renderShields(shields) {
3834
+ const list = document.getElementById('shieldsList');
3835
+ if (!shields || shields.length === 0) {
3836
+ list.innerHTML = '<span class="decisions-empty">No shields available.</span>';
3837
+ return;
3838
+ }
3839
+ list.innerHTML = shields
3840
+ .map(
3841
+ (s) => \`
3842
+ <div class="shield-row">
3843
+ <div class="shield-info">
3844
+ <div class="shield-name">\${esc(s.name)}</div>
3845
+ <div class="shield-desc">\${esc(s.description)}</div>
3846
+ </div>
3847
+ <label class="toggle">
3848
+ <input type="checkbox" \${s.active ? 'checked' : ''}
3849
+ onchange="toggleShield('\${esc(s.name)}', this.checked)" />
3850
+ <span class="slider"></span>
3851
+ </label>
3852
+ </div>
3853
+ \`
3854
+ )
3855
+ .join('');
3856
+ }
3857
+
3858
+ function toggleShield(name, active) {
3859
+ fetch('/shields', {
3860
+ method: 'POST',
3861
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3862
+ body: JSON.stringify({ name, active }),
3863
+ }).catch(() => {});
3864
+ }
3865
+
3866
+ fetch('/shields')
3867
+ .then((r) => r.json())
3868
+ .then(({ shields }) => renderShields(shields))
3869
+ .catch(() => {});
3870
+
3595
3871
  function renderDecisions(decisions) {
3596
3872
  const dl = document.getElementById('decisionsList');
3597
3873
  const entries = Object.entries(decisions);
@@ -3638,31 +3914,32 @@ var ui_default = `<!doctype html>
3638
3914
  </body>
3639
3915
  </html>
3640
3916
  `;
3917
+ }
3918
+ });
3641
3919
 
3642
3920
  // src/daemon/ui.ts
3643
- var UI_HTML_TEMPLATE = ui_default;
3921
+ var UI_HTML_TEMPLATE;
3922
+ var init_ui2 = __esm({
3923
+ "src/daemon/ui.ts"() {
3924
+ "use strict";
3925
+ init_ui();
3926
+ UI_HTML_TEMPLATE = ui_default;
3927
+ }
3928
+ });
3644
3929
 
3645
3930
  // src/daemon/index.ts
3646
3931
  import http from "http";
3932
+ import net2 from "net";
3647
3933
  import fs4 from "fs";
3648
3934
  import path6 from "path";
3649
3935
  import os4 from "os";
3650
- import { spawn as spawn2 } from "child_process";
3651
- import { randomUUID } from "crypto";
3936
+ import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
3937
+ import { randomUUID as randomUUID2 } from "crypto";
3652
3938
  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
3939
  function atomicWriteSync2(filePath, data, options) {
3663
3940
  const dir = path6.dirname(filePath);
3664
3941
  if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
3665
- const tmpPath = `${filePath}.${randomUUID()}.tmp`;
3942
+ const tmpPath = `${filePath}.${randomUUID2()}.tmp`;
3666
3943
  fs4.writeFileSync(tmpPath, data, options);
3667
3944
  fs4.renameSync(tmpPath, filePath);
3668
3945
  }
@@ -3680,12 +3957,6 @@ function writeTrustEntry(toolName, durationMs) {
3680
3957
  } catch {
3681
3958
  }
3682
3959
  }
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
3960
  function redactArgs(value) {
3690
3961
  if (!value || typeof value !== "object") return value;
3691
3962
  if (Array.isArray(value)) return value.map(redactArgs);
@@ -3720,7 +3991,6 @@ function getAuditHistory(limit = 20) {
3720
3991
  return [];
3721
3992
  }
3722
3993
  }
3723
- var AUTO_DENY_MS = 12e4;
3724
3994
  function getOrgName() {
3725
3995
  try {
3726
3996
  if (fs4.existsSync(CREDENTIALS_FILE)) {
@@ -3730,7 +4000,6 @@ function getOrgName() {
3730
4000
  }
3731
4001
  return null;
3732
4002
  }
3733
- var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
3734
4003
  function hasStoredSlackKey() {
3735
4004
  return fs4.existsSync(CREDENTIALS_FILE);
3736
4005
  }
@@ -3746,11 +4015,6 @@ function writeGlobalSetting(key, value) {
3746
4015
  config.settings[key] = value;
3747
4016
  atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
3748
4017
  }
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
4018
  function abandonPending() {
3755
4019
  abandonTimer = null;
3756
4020
  pending.forEach((entry, id) => {
@@ -3772,6 +4036,18 @@ function abandonPending() {
3772
4036
  }
3773
4037
  }
3774
4038
  function broadcast(event, data) {
4039
+ if (event === "activity") {
4040
+ activityRing.push({ event, data });
4041
+ if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
4042
+ } else if (event === "activity-result") {
4043
+ const { id, status, label } = data;
4044
+ for (let i = activityRing.length - 1; i >= 0; i--) {
4045
+ if (activityRing[i].data.id === id) {
4046
+ Object.assign(activityRing[i].data, { status, label });
4047
+ break;
4048
+ }
4049
+ }
4050
+ }
3775
4051
  const msg = `event: ${event}
3776
4052
  data: ${JSON.stringify(data)}
3777
4053
 
@@ -3817,13 +4093,15 @@ function writePersistentDecision(toolName, decision) {
3817
4093
  }
3818
4094
  }
3819
4095
  function startDaemon() {
3820
- const csrfToken = randomUUID();
3821
- const internalToken = randomUUID();
4096
+ const csrfToken = randomUUID2();
4097
+ const internalToken = randomUUID2();
3822
4098
  const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
3823
4099
  const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
3824
4100
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
4101
+ const watchMode = process.env.NODE9_WATCH_MODE === "1";
3825
4102
  let idleTimer;
3826
4103
  function resetIdleTimer() {
4104
+ if (watchMode) return;
3827
4105
  if (idleTimer) clearTimeout(idleTimer);
3828
4106
  idleTimer = setTimeout(() => {
3829
4107
  if (autoStarted) {
@@ -3878,6 +4156,12 @@ data: ${JSON.stringify({
3878
4156
  data: ${JSON.stringify(readPersistentDecisions())}
3879
4157
 
3880
4158
  `);
4159
+ for (const item of activityRing) {
4160
+ res.write(`event: ${item.event}
4161
+ data: ${JSON.stringify(item.data)}
4162
+
4163
+ `);
4164
+ }
3881
4165
  return req.on("close", () => {
3882
4166
  sseClients.delete(res);
3883
4167
  if (sseClients.size === 0 && pending.size > 0) {
@@ -3897,9 +4181,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
3897
4181
  slackDelegated = false,
3898
4182
  agent,
3899
4183
  mcpServer,
3900
- riskMetadata
4184
+ riskMetadata,
4185
+ fromCLI = false,
4186
+ activityId
3901
4187
  } = JSON.parse(body);
3902
- const id = randomUUID();
4188
+ const id = fromCLI && typeof activityId === "string" && activityId || randomUUID2();
3903
4189
  const entry = {
3904
4190
  id,
3905
4191
  toolName,
@@ -3930,6 +4216,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
3930
4216
  }, AUTO_DENY_MS)
3931
4217
  };
3932
4218
  pending.set(id, entry);
4219
+ if (!fromCLI) {
4220
+ broadcast("activity", {
4221
+ id,
4222
+ ts: entry.timestamp,
4223
+ tool: toolName,
4224
+ args: redactArgs(args),
4225
+ status: "pending"
4226
+ });
4227
+ }
3933
4228
  const browserEnabled = getConfig().settings.approvers?.browser !== false;
3934
4229
  if (browserEnabled) {
3935
4230
  broadcast("add", {
@@ -3959,6 +4254,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
3959
4254
  const e = pending.get(id);
3960
4255
  if (!e) return;
3961
4256
  if (result.noApprovalMechanism) return;
4257
+ broadcast("activity-result", {
4258
+ id,
4259
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
4260
+ label: result.blockedByLabel
4261
+ });
3962
4262
  clearTimeout(e.timer);
3963
4263
  const decision = result.approved ? "allow" : "deny";
3964
4264
  appendAuditLog({ toolName: e.toolName, args: e.args, decision });
@@ -3993,8 +4293,8 @@ data: ${JSON.stringify(readPersistentDecisions())}
3993
4293
  const entry = pending.get(id);
3994
4294
  if (!entry) return res.writeHead(404).end();
3995
4295
  if (entry.earlyDecision) {
4296
+ clearTimeout(entry.timer);
3996
4297
  pending.delete(id);
3997
- broadcast("remove", { id });
3998
4298
  res.writeHead(200, { "Content-Type": "application/json" });
3999
4299
  const body = { decision: entry.earlyDecision };
4000
4300
  if (entry.earlyReason) body.reason = entry.earlyReason;
@@ -4024,10 +4324,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
4024
4324
  decision: `trust:${trustDuration}`
4025
4325
  });
4026
4326
  clearTimeout(entry.timer);
4027
- if (entry.waiter) entry.waiter("allow");
4028
- else entry.earlyDecision = "allow";
4029
- pending.delete(id);
4030
- broadcast("remove", { id });
4327
+ if (entry.waiter) {
4328
+ entry.waiter("allow");
4329
+ pending.delete(id);
4330
+ broadcast("remove", { id });
4331
+ } else {
4332
+ entry.earlyDecision = "allow";
4333
+ broadcast("remove", { id });
4334
+ entry.timer = setTimeout(() => pending.delete(id), 3e4);
4335
+ }
4031
4336
  res.writeHead(200);
4032
4337
  return res.end(JSON.stringify({ ok: true }));
4033
4338
  }
@@ -4039,13 +4344,16 @@ data: ${JSON.stringify(readPersistentDecisions())}
4039
4344
  decision: resolvedDecision
4040
4345
  });
4041
4346
  clearTimeout(entry.timer);
4042
- if (entry.waiter) entry.waiter(resolvedDecision, reason);
4043
- else {
4347
+ if (entry.waiter) {
4348
+ entry.waiter(resolvedDecision, reason);
4349
+ pending.delete(id);
4350
+ broadcast("remove", { id });
4351
+ } else {
4044
4352
  entry.earlyDecision = resolvedDecision;
4045
4353
  entry.earlyReason = reason;
4354
+ broadcast("remove", { id });
4355
+ entry.timer = setTimeout(() => pending.delete(id), 3e4);
4046
4356
  }
4047
- pending.delete(id);
4048
- broadcast("remove", { id });
4049
4357
  res.writeHead(200);
4050
4358
  return res.end(JSON.stringify({ ok: true }));
4051
4359
  } catch {
@@ -4098,119 +4406,741 @@ data: ${JSON.stringify(readPersistentDecisions())}
4098
4406
  res.writeHead(400).end();
4099
4407
  }
4100
4408
  }
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();
4409
+ if (req.method === "DELETE" && pathname.startsWith("/decisions/")) {
4410
+ if (!validToken(req)) return res.writeHead(403).end();
4411
+ try {
4412
+ const toolName = decodeURIComponent(pathname.split("/").pop());
4413
+ const decisions = readPersistentDecisions();
4414
+ delete decisions[toolName];
4415
+ atomicWriteSync2(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
4416
+ broadcast("decisions", decisions);
4417
+ res.writeHead(200);
4418
+ return res.end(JSON.stringify({ ok: true }));
4419
+ } catch {
4420
+ res.writeHead(400).end();
4421
+ }
4422
+ }
4423
+ if (req.method === "POST" && pathname.startsWith("/resolve/")) {
4424
+ const internalAuth = req.headers["x-node9-internal"];
4425
+ if (internalAuth !== internalToken) return res.writeHead(403).end();
4426
+ try {
4427
+ const id = pathname.split("/").pop();
4428
+ const entry = pending.get(id);
4429
+ if (!entry) return res.writeHead(404).end();
4430
+ const { decision } = JSON.parse(await readBody(req));
4431
+ appendAuditLog({
4432
+ toolName: entry.toolName,
4433
+ args: entry.args,
4434
+ decision
4435
+ });
4436
+ clearTimeout(entry.timer);
4437
+ if (entry.waiter) entry.waiter(decision);
4438
+ else entry.earlyDecision = decision;
4439
+ pending.delete(id);
4440
+ broadcast("remove", { id });
4441
+ res.writeHead(200);
4442
+ return res.end(JSON.stringify({ ok: true }));
4443
+ } catch {
4444
+ res.writeHead(400).end();
4445
+ }
4446
+ }
4447
+ if (req.method === "POST" && pathname === "/events/clear") {
4448
+ activityRing.length = 0;
4449
+ res.writeHead(200, { "Content-Type": "application/json" });
4450
+ return res.end(JSON.stringify({ ok: true }));
4451
+ }
4452
+ if (req.method === "GET" && pathname === "/audit") {
4453
+ res.writeHead(200, { "Content-Type": "application/json" });
4454
+ return res.end(JSON.stringify(getAuditHistory()));
4455
+ }
4456
+ if (req.method === "GET" && pathname === "/shields") {
4457
+ if (!validToken(req)) return res.writeHead(403).end();
4458
+ const active = readActiveShields();
4459
+ const shields = Object.values(SHIELDS).map((s) => ({
4460
+ name: s.name,
4461
+ description: s.description,
4462
+ active: active.includes(s.name)
4463
+ }));
4464
+ res.writeHead(200, { "Content-Type": "application/json" });
4465
+ return res.end(JSON.stringify({ shields }));
4466
+ }
4467
+ if (req.method === "POST" && pathname === "/shields") {
4468
+ if (!validToken(req)) return res.writeHead(403).end();
4469
+ try {
4470
+ const { name, active } = JSON.parse(await readBody(req));
4471
+ if (!SHIELDS[name]) return res.writeHead(400).end();
4472
+ const current = readActiveShields();
4473
+ const updated = active ? [.../* @__PURE__ */ new Set([...current, name])] : current.filter((n) => n !== name);
4474
+ writeActiveShields(updated);
4475
+ _resetConfigCache();
4476
+ const shieldsPayload = Object.values(SHIELDS).map((s) => ({
4477
+ name: s.name,
4478
+ description: s.description,
4479
+ active: updated.includes(s.name)
4480
+ }));
4481
+ broadcast("shields-status", { shields: shieldsPayload });
4482
+ res.writeHead(200);
4483
+ return res.end(JSON.stringify({ ok: true }));
4484
+ } catch {
4485
+ res.writeHead(400).end();
4486
+ }
4487
+ }
4488
+ res.writeHead(404).end();
4489
+ });
4490
+ daemonServer = server;
4491
+ server.on("error", (e) => {
4492
+ if (e.code === "EADDRINUSE") {
4493
+ try {
4494
+ if (fs4.existsSync(DAEMON_PID_FILE)) {
4495
+ const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4496
+ process.kill(pid, 0);
4497
+ return process.exit(0);
4498
+ }
4499
+ } catch {
4500
+ try {
4501
+ fs4.unlinkSync(DAEMON_PID_FILE);
4502
+ } catch {
4503
+ }
4504
+ server.listen(DAEMON_PORT2, DAEMON_HOST2);
4505
+ return;
4506
+ }
4507
+ fetch(`http://${DAEMON_HOST2}:${DAEMON_PORT2}/settings`, {
4508
+ signal: AbortSignal.timeout(1e3)
4509
+ }).then((res) => {
4510
+ if (res.ok) {
4511
+ try {
4512
+ const r = spawnSync2("ss", ["-Htnp", `sport = :${DAEMON_PORT2}`], {
4513
+ encoding: "utf8",
4514
+ timeout: 1e3
4515
+ });
4516
+ const match = r.stdout?.match(/pid=(\d+)/);
4517
+ if (match) {
4518
+ const orphanPid = parseInt(match[1], 10);
4519
+ process.kill(orphanPid, 0);
4520
+ atomicWriteSync2(
4521
+ DAEMON_PID_FILE,
4522
+ JSON.stringify({ pid: orphanPid, port: DAEMON_PORT2, internalToken, autoStarted }),
4523
+ { mode: 384 }
4524
+ );
4525
+ }
4526
+ } catch {
4527
+ }
4528
+ process.exit(0);
4529
+ } else {
4530
+ server.listen(DAEMON_PORT2, DAEMON_HOST2);
4531
+ }
4532
+ }).catch(() => {
4533
+ server.listen(DAEMON_PORT2, DAEMON_HOST2);
4534
+ });
4535
+ return;
4536
+ }
4537
+ console.error(chalk4.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
4538
+ process.exit(1);
4539
+ });
4540
+ server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
4541
+ atomicWriteSync2(
4542
+ DAEMON_PID_FILE,
4543
+ JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
4544
+ { mode: 384 }
4545
+ );
4546
+ console.log(chalk4.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
4547
+ });
4548
+ if (watchMode) {
4549
+ console.log(chalk4.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
4550
+ }
4551
+ try {
4552
+ fs4.unlinkSync(ACTIVITY_SOCKET_PATH2);
4553
+ } catch {
4554
+ }
4555
+ const ACTIVITY_MAX_BYTES = 1024 * 1024;
4556
+ const unixServer = net2.createServer((socket) => {
4557
+ const chunks = [];
4558
+ let bytesReceived = 0;
4559
+ socket.on("data", (chunk) => {
4560
+ bytesReceived += chunk.length;
4561
+ if (bytesReceived > ACTIVITY_MAX_BYTES) {
4562
+ socket.destroy();
4563
+ return;
4564
+ }
4565
+ chunks.push(chunk);
4566
+ });
4567
+ socket.on("end", () => {
4568
+ try {
4569
+ const data = JSON.parse(Buffer.concat(chunks).toString());
4570
+ if (data.status === "pending") {
4571
+ broadcast("activity", {
4572
+ id: data.id,
4573
+ ts: data.ts,
4574
+ tool: data.tool,
4575
+ args: redactArgs(data.args),
4576
+ status: "pending"
4577
+ });
4578
+ } else {
4579
+ broadcast("activity-result", {
4580
+ id: data.id,
4581
+ status: data.status,
4582
+ label: data.label
4583
+ });
4584
+ }
4585
+ } catch {
4586
+ }
4587
+ });
4588
+ socket.on("error", () => {
4589
+ });
4590
+ });
4591
+ unixServer.listen(ACTIVITY_SOCKET_PATH2);
4592
+ process.on("exit", () => {
4593
+ try {
4594
+ fs4.unlinkSync(ACTIVITY_SOCKET_PATH2);
4595
+ } catch {
4596
+ }
4597
+ });
4598
+ }
4599
+ function stopDaemon() {
4600
+ if (!fs4.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
4601
+ try {
4602
+ const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4603
+ process.kill(pid, "SIGTERM");
4604
+ console.log(chalk4.green("\u2705 Stopped."));
4605
+ } catch {
4606
+ console.log(chalk4.gray("Cleaned up stale PID file."));
4607
+ } finally {
4608
+ try {
4609
+ fs4.unlinkSync(DAEMON_PID_FILE);
4610
+ } catch {
4611
+ }
4612
+ }
4613
+ }
4614
+ function daemonStatus() {
4615
+ if (fs4.existsSync(DAEMON_PID_FILE)) {
4616
+ try {
4617
+ const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4618
+ process.kill(pid, 0);
4619
+ console.log(chalk4.green("Node9 daemon: running"));
4620
+ return;
4621
+ } catch {
4622
+ console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
4623
+ return;
4624
+ }
4625
+ }
4626
+ const r = spawnSync2("ss", ["-Htnp", `sport = :${DAEMON_PORT2}`], {
4627
+ encoding: "utf8",
4628
+ timeout: 500
4629
+ });
4630
+ if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT2}`)) {
4631
+ console.log(chalk4.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
4632
+ } else {
4633
+ console.log(chalk4.yellow("Node9 daemon: not running"));
4634
+ }
4635
+ }
4636
+ 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;
4637
+ var init_daemon = __esm({
4638
+ "src/daemon/index.ts"() {
4639
+ "use strict";
4640
+ init_ui2();
4641
+ init_core();
4642
+ init_shields();
4643
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path6.join(os4.tmpdir(), "node9-activity.sock");
4644
+ DAEMON_PORT2 = 7391;
4645
+ DAEMON_HOST2 = "127.0.0.1";
4646
+ homeDir = os4.homedir();
4647
+ DAEMON_PID_FILE = path6.join(homeDir, ".node9", "daemon.pid");
4648
+ DECISIONS_FILE = path6.join(homeDir, ".node9", "decisions.json");
4649
+ GLOBAL_CONFIG_FILE = path6.join(homeDir, ".node9", "config.json");
4650
+ CREDENTIALS_FILE = path6.join(homeDir, ".node9", "credentials.json");
4651
+ AUDIT_LOG_FILE = path6.join(homeDir, ".node9", "audit.log");
4652
+ TRUST_FILE2 = path6.join(homeDir, ".node9", "trust.json");
4653
+ TRUST_DURATIONS = {
4654
+ "30m": 30 * 6e4,
4655
+ "1h": 60 * 6e4,
4656
+ "2h": 2 * 60 * 6e4
4657
+ };
4658
+ SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
4659
+ AUTO_DENY_MS = 12e4;
4660
+ autoStarted = process.env.NODE9_AUTO_STARTED === "1";
4661
+ pending = /* @__PURE__ */ new Map();
4662
+ sseClients = /* @__PURE__ */ new Set();
4663
+ abandonTimer = null;
4664
+ daemonServer = null;
4665
+ hadBrowserClient = false;
4666
+ ACTIVITY_RING_SIZE = 100;
4667
+ activityRing = [];
4668
+ }
4669
+ });
4670
+
4671
+ // src/tui/tail.ts
4672
+ var tail_exports = {};
4673
+ __export(tail_exports, {
4674
+ startTail: () => startTail
4675
+ });
4676
+ import http2 from "http";
4677
+ import chalk5 from "chalk";
4678
+ import fs6 from "fs";
4679
+ import os6 from "os";
4680
+ import path8 from "path";
4681
+ import readline from "readline";
4682
+ import { spawn as spawn3 } from "child_process";
4683
+ function getIcon(tool) {
4684
+ const t = tool.toLowerCase();
4685
+ for (const [k, v] of Object.entries(ICONS)) {
4686
+ if (t.includes(k)) return v;
4687
+ }
4688
+ return "\u{1F6E0}\uFE0F";
4689
+ }
4690
+ function formatBase(activity) {
4691
+ const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
4692
+ const icon = getIcon(activity.tool);
4693
+ const toolName = activity.tool.slice(0, 16).padEnd(16);
4694
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
4695
+ const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
4696
+ return `${chalk5.gray(time)} ${icon} ${chalk5.white.bold(toolName)} ${chalk5.dim(argsPreview)}`;
4697
+ }
4698
+ function renderResult(activity, result) {
4699
+ const base = formatBase(activity);
4700
+ let status;
4701
+ if (result.status === "allow") {
4702
+ status = chalk5.green("\u2713 ALLOW");
4703
+ } else if (result.status === "dlp") {
4704
+ status = chalk5.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
4705
+ } else {
4706
+ status = chalk5.red("\u2717 BLOCK");
4707
+ }
4708
+ if (process.stdout.isTTY) {
4709
+ readline.clearLine(process.stdout, 0);
4710
+ readline.cursorTo(process.stdout, 0);
4711
+ }
4712
+ console.log(`${base} ${status}`);
4713
+ }
4714
+ function renderPending(activity) {
4715
+ if (!process.stdout.isTTY) return;
4716
+ process.stdout.write(`${formatBase(activity)} ${chalk5.yellow("\u25CF \u2026")}\r`);
4717
+ }
4718
+ async function ensureDaemon() {
4719
+ if (fs6.existsSync(PID_FILE)) {
4720
+ try {
4721
+ const { port } = JSON.parse(fs6.readFileSync(PID_FILE, "utf-8"));
4722
+ return port;
4723
+ } catch {
4724
+ }
4725
+ }
4726
+ try {
4727
+ const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
4728
+ signal: AbortSignal.timeout(500)
4729
+ });
4730
+ if (res.ok) return DAEMON_PORT2;
4731
+ } catch {
4732
+ }
4733
+ console.log(chalk5.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
4734
+ const child = spawn3(process.execPath, [process.argv[1], "daemon"], {
4735
+ detached: true,
4736
+ stdio: "ignore",
4737
+ env: { ...process.env, NODE9_AUTO_STARTED: "1" }
4738
+ });
4739
+ child.unref();
4740
+ for (let i = 0; i < 20; i++) {
4741
+ await new Promise((r) => setTimeout(r, 250));
4742
+ try {
4743
+ const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
4744
+ signal: AbortSignal.timeout(500)
4745
+ });
4746
+ if (res.ok) return DAEMON_PORT2;
4747
+ } catch {
4748
+ }
4749
+ }
4750
+ console.error(chalk5.red("\u274C Daemon failed to start. Try: node9 daemon start"));
4751
+ process.exit(1);
4752
+ }
4753
+ async function startTail(options = {}) {
4754
+ const port = await ensureDaemon();
4755
+ if (options.clear) {
4756
+ await new Promise((resolve) => {
4757
+ const req2 = http2.request(
4758
+ { method: "POST", hostname: "127.0.0.1", port, path: "/events/clear" },
4759
+ (res) => {
4760
+ res.resume();
4761
+ res.on("end", resolve);
4762
+ }
4763
+ );
4764
+ req2.on("error", resolve);
4765
+ req2.end();
4766
+ });
4767
+ }
4768
+ const connectionTime = Date.now();
4769
+ const pending2 = /* @__PURE__ */ new Map();
4770
+ console.log(chalk5.cyan.bold(`
4771
+ \u{1F6F0}\uFE0F Node9 tail `) + chalk5.dim(`\u2192 localhost:${port}`));
4772
+ if (options.clear) {
4773
+ console.log(chalk5.dim("History cleared. Showing live events. Press Ctrl+C to exit.\n"));
4774
+ } else if (options.history) {
4775
+ console.log(chalk5.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
4776
+ } else {
4777
+ console.log(
4778
+ chalk5.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
4779
+ );
4780
+ }
4781
+ process.on("SIGINT", () => {
4782
+ if (process.stdout.isTTY) {
4783
+ readline.clearLine(process.stdout, 0);
4784
+ readline.cursorTo(process.stdout, 0);
4785
+ }
4786
+ console.log(chalk5.dim("\n\u{1F6F0}\uFE0F Disconnected."));
4787
+ process.exit(0);
4788
+ });
4789
+ const req = http2.get(`http://127.0.0.1:${port}/events`, (res) => {
4790
+ if (res.statusCode !== 200) {
4791
+ console.error(chalk5.red(`Failed to connect: HTTP ${res.statusCode}`));
4792
+ process.exit(1);
4793
+ }
4794
+ let currentEvent = "";
4795
+ let currentData = "";
4796
+ res.on("error", () => {
4797
+ });
4798
+ const rl = readline.createInterface({ input: res, crlfDelay: Infinity });
4799
+ rl.on("error", () => {
4800
+ });
4801
+ rl.on("line", (line) => {
4802
+ if (line.startsWith("event:")) {
4803
+ currentEvent = line.slice(6).trim();
4804
+ } else if (line.startsWith("data:")) {
4805
+ currentData = line.slice(5).trim();
4806
+ } else if (line === "") {
4807
+ if (currentEvent && currentData) {
4808
+ handleMessage(currentEvent, currentData);
4809
+ }
4810
+ currentEvent = "";
4811
+ currentData = "";
4812
+ }
4813
+ });
4814
+ rl.on("close", () => {
4815
+ if (process.stdout.isTTY) {
4816
+ readline.clearLine(process.stdout, 0);
4817
+ readline.cursorTo(process.stdout, 0);
4818
+ }
4819
+ console.log(chalk5.red("\n\u274C Daemon disconnected."));
4820
+ process.exit(1);
4821
+ });
4822
+ });
4823
+ function handleMessage(event, rawData) {
4824
+ let data;
4825
+ try {
4826
+ data = JSON.parse(rawData);
4827
+ } catch {
4828
+ return;
4829
+ }
4830
+ if (event === "activity") {
4831
+ if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
4832
+ if (data.status && data.status !== "pending") {
4833
+ renderResult(data, data);
4834
+ return;
4835
+ }
4836
+ pending2.set(data.id, data);
4837
+ const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
4838
+ if (slowTool) renderPending(data);
4839
+ }
4840
+ if (event === "activity-result") {
4841
+ const original = pending2.get(data.id);
4842
+ if (original) {
4843
+ renderResult(original, data);
4844
+ pending2.delete(data.id);
4845
+ }
4846
+ }
4847
+ }
4848
+ req.on("error", (err) => {
4849
+ const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
4850
+ console.error(chalk5.red(`
4851
+ \u274C ${msg}`));
4852
+ process.exit(1);
4853
+ });
4854
+ }
4855
+ var PID_FILE, ICONS;
4856
+ var init_tail = __esm({
4857
+ "src/tui/tail.ts"() {
4858
+ "use strict";
4859
+ init_daemon();
4860
+ PID_FILE = path8.join(os6.homedir(), ".node9", "daemon.pid");
4861
+ ICONS = {
4862
+ bash: "\u{1F4BB}",
4863
+ shell: "\u{1F4BB}",
4864
+ terminal: "\u{1F4BB}",
4865
+ read: "\u{1F4D6}",
4866
+ edit: "\u270F\uFE0F",
4867
+ write: "\u270F\uFE0F",
4868
+ glob: "\u{1F4C2}",
4869
+ grep: "\u{1F50D}",
4870
+ agent: "\u{1F916}",
4871
+ search: "\u{1F50D}",
4872
+ sql: "\u{1F5C4}\uFE0F",
4873
+ query: "\u{1F5C4}\uFE0F",
4874
+ list: "\u{1F4C2}",
4875
+ delete: "\u{1F5D1}\uFE0F",
4876
+ web: "\u{1F310}"
4877
+ };
4878
+ }
4879
+ });
4880
+
4881
+ // src/cli.ts
4882
+ init_core();
4883
+ import { Command } from "commander";
4884
+
4885
+ // src/setup.ts
4886
+ import fs3 from "fs";
4887
+ import path5 from "path";
4888
+ import os3 from "os";
4889
+ import chalk3 from "chalk";
4890
+ import { confirm as confirm2 } from "@inquirer/prompts";
4891
+ function printDaemonTip() {
4892
+ console.log(
4893
+ 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")
4894
+ );
4895
+ }
4896
+ function fullPathCommand(subcommand) {
4897
+ if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
4898
+ const nodeExec = process.execPath;
4899
+ const cliScript = process.argv[1];
4900
+ return `${nodeExec} ${cliScript} ${subcommand}`;
4901
+ }
4902
+ function readJson(filePath) {
4903
+ try {
4904
+ if (fs3.existsSync(filePath)) {
4905
+ return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
4906
+ }
4907
+ } catch {
4908
+ }
4909
+ return null;
4910
+ }
4911
+ function writeJson(filePath, data) {
4912
+ const dir = path5.dirname(filePath);
4913
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
4914
+ fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
4915
+ }
4916
+ async function setupClaude() {
4917
+ const homeDir2 = os3.homedir();
4918
+ const mcpPath = path5.join(homeDir2, ".claude.json");
4919
+ const hooksPath = path5.join(homeDir2, ".claude", "settings.json");
4920
+ const claudeConfig = readJson(mcpPath) ?? {};
4921
+ const settings = readJson(hooksPath) ?? {};
4922
+ const servers = claudeConfig.mcpServers ?? {};
4923
+ let anythingChanged = false;
4924
+ if (!settings.hooks) settings.hooks = {};
4925
+ const hasPreHook = settings.hooks.PreToolUse?.some(
4926
+ (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
4927
+ );
4928
+ if (!hasPreHook) {
4929
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
4930
+ settings.hooks.PreToolUse.push({
4931
+ matcher: ".*",
4932
+ hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
4933
+ });
4934
+ console.log(chalk3.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
4935
+ anythingChanged = true;
4936
+ }
4937
+ const hasPostHook = settings.hooks.PostToolUse?.some(
4938
+ (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
4939
+ );
4940
+ if (!hasPostHook) {
4941
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
4942
+ settings.hooks.PostToolUse.push({
4943
+ matcher: ".*",
4944
+ hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
4945
+ });
4946
+ console.log(chalk3.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
4947
+ anythingChanged = true;
4948
+ }
4949
+ if (anythingChanged) {
4950
+ writeJson(hooksPath, 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(` ${mcpPath}`));
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 };
4113
4970
  }
4971
+ claudeConfig.mcpServers = servers;
4972
+ writeJson(mcpPath, claudeConfig);
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."));
4114
4978
  }
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();
4979
+ console.log("");
4980
+ }
4981
+ if (!anythingChanged && serversToWrap.length === 0) {
4982
+ console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
4983
+ printDaemonTip();
4984
+ return;
4985
+ }
4986
+ if (anythingChanged) {
4987
+ console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
4988
+ console.log(chalk3.gray(" Restart Claude Code for changes to take effect."));
4989
+ printDaemonTip();
4990
+ }
4991
+ }
4992
+ async function setupGemini() {
4993
+ const homeDir2 = os3.homedir();
4994
+ const settingsPath = path5.join(homeDir2, ".gemini", "settings.json");
4995
+ const settings = readJson(settingsPath) ?? {};
4996
+ const servers = settings.mcpServers ?? {};
4997
+ let anythingChanged = false;
4998
+ if (!settings.hooks) settings.hooks = {};
4999
+ const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
5000
+ (m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
5001
+ );
5002
+ if (!hasBeforeHook) {
5003
+ if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
5004
+ if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
5005
+ settings.hooks.BeforeTool.push({
5006
+ matcher: ".*",
5007
+ hooks: [
5008
+ {
5009
+ name: "node9-check",
5010
+ type: "command",
5011
+ command: fullPathCommand("check"),
5012
+ timeout: 6e5
5013
+ }
5014
+ ]
5015
+ });
5016
+ console.log(chalk3.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
5017
+ anythingChanged = true;
5018
+ }
5019
+ const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
5020
+ (m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
5021
+ );
5022
+ if (!hasAfterHook) {
5023
+ if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
5024
+ if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
5025
+ settings.hooks.AfterTool.push({
5026
+ matcher: ".*",
5027
+ hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
5028
+ });
5029
+ console.log(chalk3.green(" \u2705 AfterTool hook added \u2192 node9 log"));
5030
+ anythingChanged = true;
5031
+ }
5032
+ if (anythingChanged) {
5033
+ writeJson(settingsPath, settings);
5034
+ console.log("");
5035
+ }
5036
+ const serversToWrap = [];
5037
+ for (const [name, server] of Object.entries(servers)) {
5038
+ if (!server.command || server.command === "node9") continue;
5039
+ const parts = [server.command, ...server.args ?? []];
5040
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
5041
+ }
5042
+ if (serversToWrap.length > 0) {
5043
+ console.log(chalk3.bold("The following existing entries will be modified:\n"));
5044
+ console.log(chalk3.white(` ${settingsPath} (mcpServers)`));
5045
+ for (const { name, originalCmd } of serversToWrap) {
5046
+ console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5047
+ }
5048
+ console.log("");
5049
+ const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
5050
+ if (proceed) {
5051
+ for (const { name, parts } of serversToWrap) {
5052
+ servers[name] = { ...servers[name], command: "node9", args: parts };
4137
5053
  }
5054
+ settings.mcpServers = servers;
5055
+ writeJson(settingsPath, settings);
5056
+ console.log(chalk3.green(`
5057
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
5058
+ anythingChanged = true;
5059
+ } else {
5060
+ console.log(chalk3.yellow(" Skipped MCP server wrapping."));
4138
5061
  }
4139
- if (req.method === "GET" && pathname === "/audit") {
4140
- res.writeHead(200, { "Content-Type": "application/json" });
4141
- return res.end(JSON.stringify(getAuditHistory()));
5062
+ console.log("");
5063
+ }
5064
+ if (!anythingChanged && serversToWrap.length === 0) {
5065
+ console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
5066
+ printDaemonTip();
5067
+ return;
5068
+ }
5069
+ if (anythingChanged) {
5070
+ console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
5071
+ console.log(chalk3.gray(" Restart Gemini CLI for changes to take effect."));
5072
+ printDaemonTip();
5073
+ }
5074
+ }
5075
+ async function setupCursor() {
5076
+ const homeDir2 = os3.homedir();
5077
+ const mcpPath = path5.join(homeDir2, ".cursor", "mcp.json");
5078
+ const mcpConfig = readJson(mcpPath) ?? {};
5079
+ const servers = mcpConfig.mcpServers ?? {};
5080
+ let anythingChanged = false;
5081
+ const serversToWrap = [];
5082
+ for (const [name, server] of Object.entries(servers)) {
5083
+ if (!server.command || server.command === "node9") continue;
5084
+ const parts = [server.command, ...server.args ?? []];
5085
+ serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
5086
+ }
5087
+ if (serversToWrap.length > 0) {
5088
+ console.log(chalk3.bold("The following existing entries will be modified:\n"));
5089
+ console.log(chalk3.white(` ${mcpPath}`));
5090
+ for (const { name, originalCmd } of serversToWrap) {
5091
+ console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
4142
5092
  }
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;
5093
+ console.log("");
5094
+ const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
5095
+ if (proceed) {
5096
+ for (const { name, parts } of serversToWrap) {
5097
+ servers[name] = { ...servers[name], command: "node9", args: parts };
4161
5098
  }
5099
+ mcpConfig.mcpServers = servers;
5100
+ writeJson(mcpPath, mcpConfig);
5101
+ console.log(chalk3.green(`
5102
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
5103
+ anythingChanged = true;
5104
+ } else {
5105
+ console.log(chalk3.yellow(" Skipped MCP server wrapping."));
4162
5106
  }
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 }
5107
+ console.log("");
5108
+ }
5109
+ console.log(
5110
+ chalk3.yellow(
5111
+ " \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
5112
+ )
5113
+ );
5114
+ console.log("");
5115
+ if (!anythingChanged && serversToWrap.length === 0) {
5116
+ console.log(
5117
+ chalk3.blue(
5118
+ "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
5119
+ )
4171
5120
  );
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
- }
5121
+ printDaemonTip();
5122
+ return;
4188
5123
  }
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)"));
5124
+ if (anythingChanged) {
5125
+ console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
5126
+ console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
5127
+ printDaemonTip();
4199
5128
  }
4200
5129
  }
4201
5130
 
4202
5131
  // src/cli.ts
4203
- import { spawn as spawn3, execSync } from "child_process";
5132
+ init_daemon();
5133
+ import { spawn as spawn4, execSync } from "child_process";
4204
5134
  import { parseCommandString } from "execa";
4205
5135
  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";
5136
+ import chalk6 from "chalk";
5137
+ import readline2 from "readline";
5138
+ import fs7 from "fs";
5139
+ import path9 from "path";
5140
+ import os7 from "os";
4211
5141
 
4212
5142
  // src/undo.ts
4213
- import { spawnSync } from "child_process";
5143
+ import { spawnSync as spawnSync3 } from "child_process";
4214
5144
  import fs5 from "fs";
4215
5145
  import path7 from "path";
4216
5146
  import os5 from "os";
@@ -4247,12 +5177,12 @@ async function createShadowSnapshot(tool = "unknown", args = {}) {
4247
5177
  if (!fs5.existsSync(path7.join(cwd, ".git"))) return null;
4248
5178
  const tempIndex = path7.join(cwd, ".git", `node9_index_${Date.now()}`);
4249
5179
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
4250
- spawnSync("git", ["add", "-A"], { env });
4251
- const treeRes = spawnSync("git", ["write-tree"], { env });
5180
+ spawnSync3("git", ["add", "-A"], { env });
5181
+ const treeRes = spawnSync3("git", ["write-tree"], { env });
4252
5182
  const treeHash = treeRes.stdout.toString().trim();
4253
5183
  if (fs5.existsSync(tempIndex)) fs5.unlinkSync(tempIndex);
4254
5184
  if (!treeHash || treeRes.status !== 0) return null;
4255
- const commitRes = spawnSync("git", [
5185
+ const commitRes = spawnSync3("git", [
4256
5186
  "commit-tree",
4257
5187
  treeHash,
4258
5188
  "-m",
@@ -4283,10 +5213,10 @@ function getSnapshotHistory() {
4283
5213
  }
4284
5214
  function computeUndoDiff(hash, cwd) {
4285
5215
  try {
4286
- const result = spawnSync("git", ["diff", hash, "--stat", "--", "."], { cwd });
5216
+ const result = spawnSync3("git", ["diff", hash, "--stat", "--", "."], { cwd });
4287
5217
  const stat = result.stdout.toString().trim();
4288
5218
  if (!stat) return null;
4289
- const diff = spawnSync("git", ["diff", hash, "--", "."], { cwd });
5219
+ const diff = spawnSync3("git", ["diff", hash, "--", "."], { cwd });
4290
5220
  const raw = diff.stdout.toString();
4291
5221
  if (!raw) return null;
4292
5222
  const lines = raw.split("\n").filter(
@@ -4300,14 +5230,14 @@ function computeUndoDiff(hash, cwd) {
4300
5230
  function applyUndo(hash, cwd) {
4301
5231
  try {
4302
5232
  const dir = cwd ?? process.cwd();
4303
- const restore = spawnSync("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
5233
+ const restore = spawnSync3("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
4304
5234
  cwd: dir
4305
5235
  });
4306
5236
  if (restore.status !== 0) return false;
4307
- const lsTree = spawnSync("git", ["ls-tree", "-r", "--name-only", hash], { cwd: dir });
5237
+ const lsTree = spawnSync3("git", ["ls-tree", "-r", "--name-only", hash], { cwd: dir });
4308
5238
  const snapshotFiles = new Set(lsTree.stdout.toString().trim().split("\n").filter(Boolean));
4309
- const tracked = spawnSync("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
4310
- const untracked = spawnSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5239
+ const tracked = spawnSync3("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5240
+ const untracked = spawnSync3("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
4311
5241
  for (const file of [...tracked, ...untracked]) {
4312
5242
  const fullPath = path7.join(dir, file);
4313
5243
  if (!snapshotFiles.has(file) && fs5.existsSync(fullPath)) {
@@ -4321,9 +5251,10 @@ function applyUndo(hash, cwd) {
4321
5251
  }
4322
5252
 
4323
5253
  // src/cli.ts
5254
+ init_shields();
4324
5255
  import { confirm as confirm3 } from "@inquirer/prompts";
4325
5256
  var { version } = JSON.parse(
4326
- fs6.readFileSync(path8.join(__dirname, "../package.json"), "utf-8")
5257
+ fs7.readFileSync(path9.join(__dirname, "../package.json"), "utf-8")
4327
5258
  );
4328
5259
  function parseDuration(str) {
4329
5260
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -4419,7 +5350,7 @@ function openBrowserLocal() {
4419
5350
  }
4420
5351
  async function autoStartDaemonAndWait() {
4421
5352
  try {
4422
- const child = spawn3("node9", ["daemon"], {
5353
+ const child = spawn4("node9", ["daemon"], {
4423
5354
  detached: true,
4424
5355
  stdio: "ignore",
4425
5356
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
@@ -4455,14 +5386,14 @@ async function runProxy(targetCommand) {
4455
5386
  if (stdout) executable = stdout.trim();
4456
5387
  } catch {
4457
5388
  }
4458
- console.log(chalk5.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
4459
- const child = spawn3(executable, args, {
5389
+ console.log(chalk6.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
5390
+ const child = spawn4(executable, args, {
4460
5391
  stdio: ["pipe", "pipe", "inherit"],
4461
5392
  // We control STDIN and STDOUT
4462
5393
  shell: false,
4463
5394
  env: { ...process.env, FORCE_COLOR: "1" }
4464
5395
  });
4465
- const agentIn = readline.createInterface({ input: process.stdin, terminal: false });
5396
+ const agentIn = readline2.createInterface({ input: process.stdin, terminal: false });
4466
5397
  agentIn.on("line", async (line) => {
4467
5398
  let message;
4468
5399
  try {
@@ -4480,10 +5411,10 @@ async function runProxy(targetCommand) {
4480
5411
  agent: "Proxy/MCP"
4481
5412
  });
4482
5413
  if (!result.approved) {
4483
- console.error(chalk5.red(`
5414
+ console.error(chalk6.red(`
4484
5415
  \u{1F6D1} Node9 Sudo: Action Blocked`));
4485
- console.error(chalk5.gray(` Tool: ${name}`));
4486
- console.error(chalk5.gray(` Reason: ${result.reason || "Security Policy"}
5416
+ console.error(chalk6.gray(` Tool: ${name}`));
5417
+ console.error(chalk6.gray(` Reason: ${result.reason || "Security Policy"}
4487
5418
  `));
4488
5419
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
4489
5420
  const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -4525,14 +5456,14 @@ async function runProxy(targetCommand) {
4525
5456
  }
4526
5457
  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
5458
  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 });
5459
+ const credPath = path9.join(os7.homedir(), ".node9", "credentials.json");
5460
+ if (!fs7.existsSync(path9.dirname(credPath)))
5461
+ fs7.mkdirSync(path9.dirname(credPath), { recursive: true });
4531
5462
  const profileName = options.profile || "default";
4532
5463
  let existingCreds = {};
4533
5464
  try {
4534
- if (fs6.existsSync(credPath)) {
4535
- const raw = JSON.parse(fs6.readFileSync(credPath, "utf-8"));
5465
+ if (fs7.existsSync(credPath)) {
5466
+ const raw = JSON.parse(fs7.readFileSync(credPath, "utf-8"));
4536
5467
  if (raw.apiKey) {
4537
5468
  existingCreds = {
4538
5469
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -4544,13 +5475,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4544
5475
  } catch {
4545
5476
  }
4546
5477
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
4547
- fs6.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
5478
+ fs7.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
4548
5479
  if (profileName === "default") {
4549
- const configPath = path8.join(os6.homedir(), ".node9", "config.json");
5480
+ const configPath = path9.join(os7.homedir(), ".node9", "config.json");
4550
5481
  let config = {};
4551
5482
  try {
4552
- if (fs6.existsSync(configPath))
4553
- config = JSON.parse(fs6.readFileSync(configPath, "utf-8"));
5483
+ if (fs7.existsSync(configPath))
5484
+ config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
4554
5485
  } catch {
4555
5486
  }
4556
5487
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -4565,36 +5496,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4565
5496
  approvers.cloud = false;
4566
5497
  }
4567
5498
  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 });
5499
+ if (!fs7.existsSync(path9.dirname(configPath)))
5500
+ fs7.mkdirSync(path9.dirname(configPath), { recursive: true });
5501
+ fs7.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
4571
5502
  }
4572
5503
  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`));
5504
+ console.log(chalk6.green(`\u2705 Profile "${profileName}" saved`));
5505
+ console.log(chalk6.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
4575
5506
  } 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.`));
5507
+ console.log(chalk6.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
5508
+ console.log(chalk6.gray(` All decisions stay on this machine.`));
4578
5509
  } 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.`));
5510
+ console.log(chalk6.green(`\u2705 Logged in \u2014 agent mode`));
5511
+ console.log(chalk6.gray(` Team policy enforced for all calls via Node9 cloud.`));
4581
5512
  }
4582
5513
  });
4583
5514
  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
5515
  if (target === "gemini") return await setupGemini();
4585
5516
  if (target === "claude") return await setupClaude();
4586
5517
  if (target === "cursor") return await setupCursor();
4587
- console.error(chalk5.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
5518
+ console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
4588
5519
  process.exit(1);
4589
5520
  });
4590
5521
  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
5522
  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");
5523
+ console.log(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
5524
+ console.log(" Usage: " + chalk6.white("node9 setup <target>") + "\n");
4594
5525
  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)");
5526
+ console.log(" " + chalk6.green("claude") + " \u2014 Claude Code (hook mode)");
5527
+ console.log(" " + chalk6.green("gemini") + " \u2014 Gemini CLI (hook mode)");
5528
+ console.log(" " + chalk6.green("cursor") + " \u2014 Cursor (hook mode)");
4598
5529
  console.log("");
4599
5530
  return;
4600
5531
  }
@@ -4602,28 +5533,28 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
4602
5533
  if (t === "gemini") return await setupGemini();
4603
5534
  if (t === "claude") return await setupClaude();
4604
5535
  if (t === "cursor") return await setupCursor();
4605
- console.error(chalk5.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
5536
+ console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
4606
5537
  process.exit(1);
4607
5538
  });
4608
5539
  program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
4609
- const homeDir2 = os6.homedir();
5540
+ const homeDir2 = os7.homedir();
4610
5541
  let failures = 0;
4611
5542
  function pass(msg) {
4612
- console.log(chalk5.green(" \u2705 ") + msg);
5543
+ console.log(chalk6.green(" \u2705 ") + msg);
4613
5544
  }
4614
5545
  function fail(msg, hint) {
4615
- console.log(chalk5.red(" \u274C ") + msg);
4616
- if (hint) console.log(chalk5.gray(" " + hint));
5546
+ console.log(chalk6.red(" \u274C ") + msg);
5547
+ if (hint) console.log(chalk6.gray(" " + hint));
4617
5548
  failures++;
4618
5549
  }
4619
5550
  function warn(msg, hint) {
4620
- console.log(chalk5.yellow(" \u26A0\uFE0F ") + msg);
4621
- if (hint) console.log(chalk5.gray(" " + hint));
5551
+ console.log(chalk6.yellow(" \u26A0\uFE0F ") + msg);
5552
+ if (hint) console.log(chalk6.gray(" " + hint));
4622
5553
  }
4623
5554
  function section(title) {
4624
- console.log("\n" + chalk5.bold(title));
5555
+ console.log("\n" + chalk6.bold(title));
4625
5556
  }
4626
- console.log(chalk5.cyan.bold(`
5557
+ console.log(chalk6.cyan.bold(`
4627
5558
  \u{1F6E1}\uFE0F Node9 Doctor v${version}
4628
5559
  `));
4629
5560
  section("Binary");
@@ -4652,10 +5583,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4652
5583
  );
4653
5584
  }
4654
5585
  section("Configuration");
4655
- const globalConfigPath = path8.join(homeDir2, ".node9", "config.json");
4656
- if (fs6.existsSync(globalConfigPath)) {
5586
+ const globalConfigPath = path9.join(homeDir2, ".node9", "config.json");
5587
+ if (fs7.existsSync(globalConfigPath)) {
4657
5588
  try {
4658
- JSON.parse(fs6.readFileSync(globalConfigPath, "utf-8"));
5589
+ JSON.parse(fs7.readFileSync(globalConfigPath, "utf-8"));
4659
5590
  pass("~/.node9/config.json found and valid");
4660
5591
  } catch {
4661
5592
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -4663,17 +5594,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
4663
5594
  } else {
4664
5595
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
4665
5596
  }
4666
- const projectConfigPath = path8.join(process.cwd(), "node9.config.json");
4667
- if (fs6.existsSync(projectConfigPath)) {
5597
+ const projectConfigPath = path9.join(process.cwd(), "node9.config.json");
5598
+ if (fs7.existsSync(projectConfigPath)) {
4668
5599
  try {
4669
- JSON.parse(fs6.readFileSync(projectConfigPath, "utf-8"));
5600
+ JSON.parse(fs7.readFileSync(projectConfigPath, "utf-8"));
4670
5601
  pass("node9.config.json found and valid (project)");
4671
5602
  } catch {
4672
5603
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
4673
5604
  }
4674
5605
  }
4675
- const credsPath = path8.join(homeDir2, ".node9", "credentials.json");
4676
- if (fs6.existsSync(credsPath)) {
5606
+ const credsPath = path9.join(homeDir2, ".node9", "credentials.json");
5607
+ if (fs7.existsSync(credsPath)) {
4677
5608
  pass("Cloud credentials found (~/.node9/credentials.json)");
4678
5609
  } else {
4679
5610
  warn(
@@ -4682,10 +5613,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4682
5613
  );
4683
5614
  }
4684
5615
  section("Agent Hooks");
4685
- const claudeSettingsPath = path8.join(homeDir2, ".claude", "settings.json");
4686
- if (fs6.existsSync(claudeSettingsPath)) {
5616
+ const claudeSettingsPath = path9.join(homeDir2, ".claude", "settings.json");
5617
+ if (fs7.existsSync(claudeSettingsPath)) {
4687
5618
  try {
4688
- const cs = JSON.parse(fs6.readFileSync(claudeSettingsPath, "utf-8"));
5619
+ const cs = JSON.parse(fs7.readFileSync(claudeSettingsPath, "utf-8"));
4689
5620
  const hasHook = cs.hooks?.PreToolUse?.some(
4690
5621
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4691
5622
  );
@@ -4698,10 +5629,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4698
5629
  } else {
4699
5630
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
4700
5631
  }
4701
- const geminiSettingsPath = path8.join(homeDir2, ".gemini", "settings.json");
4702
- if (fs6.existsSync(geminiSettingsPath)) {
5632
+ const geminiSettingsPath = path9.join(homeDir2, ".gemini", "settings.json");
5633
+ if (fs7.existsSync(geminiSettingsPath)) {
4703
5634
  try {
4704
- const gs = JSON.parse(fs6.readFileSync(geminiSettingsPath, "utf-8"));
5635
+ const gs = JSON.parse(fs7.readFileSync(geminiSettingsPath, "utf-8"));
4705
5636
  const hasHook = gs.hooks?.BeforeTool?.some(
4706
5637
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4707
5638
  );
@@ -4714,10 +5645,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4714
5645
  } else {
4715
5646
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
4716
5647
  }
4717
- const cursorHooksPath = path8.join(homeDir2, ".cursor", "hooks.json");
4718
- if (fs6.existsSync(cursorHooksPath)) {
5648
+ const cursorHooksPath = path9.join(homeDir2, ".cursor", "hooks.json");
5649
+ if (fs7.existsSync(cursorHooksPath)) {
4719
5650
  try {
4720
- const cur = JSON.parse(fs6.readFileSync(cursorHooksPath, "utf-8"));
5651
+ const cur = JSON.parse(fs7.readFileSync(cursorHooksPath, "utf-8"));
4721
5652
  const hasHook = cur.hooks?.preToolUse?.some(
4722
5653
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
4723
5654
  );
@@ -4738,9 +5669,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
4738
5669
  }
4739
5670
  console.log("");
4740
5671
  if (failures === 0) {
4741
- console.log(chalk5.green.bold(" All checks passed. Node9 is ready.\n"));
5672
+ console.log(chalk6.green.bold(" All checks passed. Node9 is ready.\n"));
4742
5673
  } else {
4743
- console.log(chalk5.red.bold(` ${failures} check(s) failed. See hints above.
5674
+ console.log(chalk6.red.bold(` ${failures} check(s) failed. See hints above.
4744
5675
  `));
4745
5676
  process.exit(1);
4746
5677
  }
@@ -4755,7 +5686,7 @@ program.command("explain").description(
4755
5686
  try {
4756
5687
  args = JSON.parse(trimmed);
4757
5688
  } catch {
4758
- console.error(chalk5.red(`
5689
+ console.error(chalk6.red(`
4759
5690
  \u274C Invalid JSON: ${trimmed}
4760
5691
  `));
4761
5692
  process.exit(1);
@@ -4766,63 +5697,63 @@ program.command("explain").description(
4766
5697
  }
4767
5698
  const result = await explainPolicy(tool, args);
4768
5699
  console.log("");
4769
- console.log(chalk5.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
5700
+ console.log(chalk6.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
4770
5701
  console.log("");
4771
- console.log(` ${chalk5.bold("Tool:")} ${chalk5.white(result.tool)}`);
5702
+ console.log(` ${chalk6.bold("Tool:")} ${chalk6.white(result.tool)}`);
4772
5703
  if (argsRaw) {
4773
5704
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
4774
- console.log(` ${chalk5.bold("Input:")} ${chalk5.gray(preview)}`);
5705
+ console.log(` ${chalk6.bold("Input:")} ${chalk6.gray(preview)}`);
4775
5706
  }
4776
5707
  console.log("");
4777
- console.log(chalk5.bold("Config Sources (Waterfall):"));
5708
+ console.log(chalk6.bold("Config Sources (Waterfall):"));
4778
5709
  for (const tier of result.waterfall) {
4779
- const num = chalk5.gray(` ${tier.tier}.`);
5710
+ const num = chalk6.gray(` ${tier.tier}.`);
4780
5711
  const label = tier.label.padEnd(16);
4781
5712
  let statusStr;
4782
5713
  if (tier.tier === 1) {
4783
- statusStr = chalk5.gray(tier.note ?? "");
5714
+ statusStr = chalk6.gray(tier.note ?? "");
4784
5715
  } 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 : "");
5716
+ const loc = tier.path ? chalk6.gray(tier.path) : "";
5717
+ const note = tier.note ? chalk6.gray(`(${tier.note})`) : "";
5718
+ statusStr = chalk6.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
4788
5719
  } else {
4789
- statusStr = chalk5.gray("\u25CB " + (tier.note ?? "not found"));
5720
+ statusStr = chalk6.gray("\u25CB " + (tier.note ?? "not found"));
4790
5721
  }
4791
- console.log(`${num} ${chalk5.white(label)} ${statusStr}`);
5722
+ console.log(`${num} ${chalk6.white(label)} ${statusStr}`);
4792
5723
  }
4793
5724
  console.log("");
4794
- console.log(chalk5.bold("Policy Evaluation:"));
5725
+ console.log(chalk6.bold("Policy Evaluation:"));
4795
5726
  for (const step of result.steps) {
4796
5727
  const isFinal = step.isFinal;
4797
5728
  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 ");
5729
+ if (step.outcome === "allow") icon = chalk6.green(" \u2705");
5730
+ else if (step.outcome === "review") icon = chalk6.red(" \u{1F534}");
5731
+ else if (step.outcome === "skip") icon = chalk6.gray(" \u2500 ");
5732
+ else icon = chalk6.gray(" \u25CB ");
4802
5733
  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") : "";
5734
+ const nameStr = isFinal ? chalk6.white.bold(name) : chalk6.white(name);
5735
+ const detail = isFinal ? chalk6.white(step.detail) : chalk6.gray(step.detail);
5736
+ const arrow = isFinal ? chalk6.yellow(" \u2190 STOP") : "";
4806
5737
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
4807
5738
  }
4808
5739
  console.log("");
4809
5740
  if (result.decision === "allow") {
4810
- console.log(chalk5.green.bold(" Decision: \u2705 ALLOW") + chalk5.gray(" \u2014 no approval needed"));
5741
+ console.log(chalk6.green.bold(" Decision: \u2705 ALLOW") + chalk6.gray(" \u2014 no approval needed"));
4811
5742
  } else {
4812
5743
  console.log(
4813
- chalk5.red.bold(" Decision: \u{1F534} REVIEW") + chalk5.gray(" \u2014 human approval required")
5744
+ chalk6.red.bold(" Decision: \u{1F534} REVIEW") + chalk6.gray(" \u2014 human approval required")
4814
5745
  );
4815
5746
  if (result.blockedByLabel) {
4816
- console.log(chalk5.gray(` Reason: ${result.blockedByLabel}`));
5747
+ console.log(chalk6.gray(` Reason: ${result.blockedByLabel}`));
4817
5748
  }
4818
5749
  }
4819
5750
  console.log("");
4820
5751
  });
4821
5752
  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.`));
5753
+ const configPath = path9.join(os7.homedir(), ".node9", "config.json");
5754
+ if (fs7.existsSync(configPath) && !options.force) {
5755
+ console.log(chalk6.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
5756
+ console.log(chalk6.gray(` Run with --force to overwrite.`));
4826
5757
  return;
4827
5758
  }
4828
5759
  const requestedMode = options.mode.toLowerCase();
@@ -4834,13 +5765,13 @@ program.command("init").description("Create ~/.node9/config.json with default po
4834
5765
  mode: safeMode
4835
5766
  }
4836
5767
  };
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}`));
5768
+ const dir = path9.dirname(configPath);
5769
+ if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
5770
+ fs7.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
5771
+ console.log(chalk6.green(`\u2705 Global config created: ${configPath}`));
5772
+ console.log(chalk6.cyan(` Mode set to: ${safeMode}`));
4842
5773
  console.log(
4843
- chalk5.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
5774
+ chalk6.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
4844
5775
  );
4845
5776
  });
4846
5777
  function formatRelativeTime(timestamp) {
@@ -4854,14 +5785,14 @@ function formatRelativeTime(timestamp) {
4854
5785
  return new Date(timestamp).toLocaleDateString();
4855
5786
  }
4856
5787
  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)) {
5788
+ const logPath = path9.join(os7.homedir(), ".node9", "audit.log");
5789
+ if (!fs7.existsSync(logPath)) {
4859
5790
  console.log(
4860
- chalk5.yellow("No audit logs found. Run node9 with an agent to generate entries.")
5791
+ chalk6.yellow("No audit logs found. Run node9 with an agent to generate entries.")
4861
5792
  );
4862
5793
  return;
4863
5794
  }
4864
- const raw = fs6.readFileSync(logPath, "utf-8");
5795
+ const raw = fs7.readFileSync(logPath, "utf-8");
4865
5796
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
4866
5797
  let entries = lines.flatMap((line) => {
4867
5798
  try {
@@ -4883,31 +5814,31 @@ program.command("audit").description("View local execution audit log").option("-
4883
5814
  return;
4884
5815
  }
4885
5816
  if (entries.length === 0) {
4886
- console.log(chalk5.yellow("No matching audit entries."));
5817
+ console.log(chalk6.yellow("No matching audit entries."));
4887
5818
  return;
4888
5819
  }
4889
5820
  console.log(
4890
5821
  `
4891
- ${chalk5.bold("Node9 Audit Log")} ${chalk5.dim(`(${entries.length} entries)`)}`
5822
+ ${chalk6.bold("Node9 Audit Log")} ${chalk6.dim(`(${entries.length} entries)`)}`
4892
5823
  );
4893
- console.log(chalk5.dim(" " + "\u2500".repeat(65)));
5824
+ console.log(chalk6.dim(" " + "\u2500".repeat(65)));
4894
5825
  console.log(
4895
5826
  ` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
4896
5827
  );
4897
- console.log(chalk5.dim(" " + "\u2500".repeat(65)));
5828
+ console.log(chalk6.dim(" " + "\u2500".repeat(65)));
4898
5829
  for (const e of entries) {
4899
5830
  const time = formatRelativeTime(String(e.ts)).padEnd(12);
4900
5831
  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));
5832
+ const result = e.decision === "allow" ? chalk6.green("ALLOW".padEnd(10)) : chalk6.red("DENY".padEnd(10));
4902
5833
  const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
4903
5834
  const agent = String(e.agent || "unknown");
4904
5835
  console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
4905
5836
  }
4906
5837
  const allowed = entries.filter((e) => e.decision === "allow").length;
4907
5838
  const denied = entries.filter((e) => e.decision === "deny").length;
4908
- console.log(chalk5.dim(" " + "\u2500".repeat(65)));
5839
+ console.log(chalk6.dim(" " + "\u2500".repeat(65)));
4909
5840
  console.log(
4910
- ` ${entries.length} entries | ${chalk5.green(allowed + " allowed")} | ${chalk5.red(denied + " denied")}
5841
+ ` ${entries.length} entries | ${chalk6.green(allowed + " allowed")} | ${chalk6.red(denied + " denied")}
4911
5842
  `
4912
5843
  );
4913
5844
  });
@@ -4918,43 +5849,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
4918
5849
  const settings = mergedConfig.settings;
4919
5850
  console.log("");
4920
5851
  if (creds && settings.approvers.cloud) {
4921
- console.log(chalk5.green(" \u25CF Agent mode") + chalk5.gray(" \u2014 cloud team policy enforced"));
5852
+ console.log(chalk6.green(" \u25CF Agent mode") + chalk6.gray(" \u2014 cloud team policy enforced"));
4922
5853
  } else if (creds && !settings.approvers.cloud) {
4923
5854
  console.log(
4924
- chalk5.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk5.gray(" \u2014 all decisions stay on this machine")
5855
+ chalk6.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk6.gray(" \u2014 all decisions stay on this machine")
4925
5856
  );
4926
5857
  } else {
4927
5858
  console.log(
4928
- chalk5.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk5.gray(" \u2014 no API key (Local rules only)")
5859
+ chalk6.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk6.gray(" \u2014 no API key (Local rules only)")
4929
5860
  );
4930
5861
  }
4931
5862
  console.log("");
4932
5863
  if (daemonRunning) {
4933
5864
  console.log(
4934
- chalk5.green(" \u25CF Daemon running") + chalk5.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
5865
+ chalk6.green(" \u25CF Daemon running") + chalk6.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
4935
5866
  );
4936
5867
  } else {
4937
- console.log(chalk5.gray(" \u25CB Daemon stopped"));
5868
+ console.log(chalk6.gray(" \u25CB Daemon stopped"));
4938
5869
  }
4939
5870
  if (settings.enableUndo) {
4940
5871
  console.log(
4941
- chalk5.magenta(" \u25CF Undo Engine") + chalk5.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
5872
+ chalk6.magenta(" \u25CF Undo Engine") + chalk6.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
4942
5873
  );
4943
5874
  }
4944
5875
  console.log("");
4945
- const modeLabel = settings.mode === "audit" ? chalk5.blue("audit") : settings.mode === "strict" ? chalk5.red("strict") : chalk5.white("standard");
5876
+ const modeLabel = settings.mode === "audit" ? chalk6.blue("audit") : settings.mode === "strict" ? chalk6.red("strict") : chalk6.white("standard");
4946
5877
  console.log(` Mode: ${modeLabel}`);
4947
- const projectConfig = path8.join(process.cwd(), "node9.config.json");
4948
- const globalConfig = path8.join(os6.homedir(), ".node9", "config.json");
5878
+ const projectConfig = path9.join(process.cwd(), "node9.config.json");
5879
+ const globalConfig = path9.join(os7.homedir(), ".node9", "config.json");
4949
5880
  console.log(
4950
- ` Local: ${fs6.existsSync(projectConfig) ? chalk5.green("Active (node9.config.json)") : chalk5.gray("Not present")}`
5881
+ ` Local: ${fs7.existsSync(projectConfig) ? chalk6.green("Active (node9.config.json)") : chalk6.gray("Not present")}`
4951
5882
  );
4952
5883
  console.log(
4953
- ` Global: ${fs6.existsSync(globalConfig) ? chalk5.green("Active (~/.node9/config.json)") : chalk5.gray("Not present")}`
5884
+ ` Global: ${fs7.existsSync(globalConfig) ? chalk6.green("Active (~/.node9/config.json)") : chalk6.gray("Not present")}`
4954
5885
  );
4955
5886
  if (mergedConfig.policy.sandboxPaths.length > 0) {
4956
5887
  console.log(
4957
- ` Sandbox: ${chalk5.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
5888
+ ` Sandbox: ${chalk6.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
4958
5889
  );
4959
5890
  }
4960
5891
  const pauseState = checkPause();
@@ -4962,47 +5893,63 @@ program.command("status").description("Show current Node9 mode, policy source, a
4962
5893
  const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
4963
5894
  console.log("");
4964
5895
  console.log(
4965
- chalk5.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk5.gray(" \u2014 all tool calls allowed")
5896
+ chalk6.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk6.gray(" \u2014 all tool calls allowed")
4966
5897
  );
4967
5898
  }
4968
5899
  console.log("");
4969
5900
  });
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(
5901
+ 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(
5902
+ "-w, --watch",
5903
+ "Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
5904
+ ).action(
4971
5905
  async (action, options) => {
4972
5906
  const cmd = (action ?? "start").toLowerCase();
4973
5907
  if (cmd === "stop") return stopDaemon();
4974
5908
  if (cmd === "status") return daemonStatus();
4975
5909
  if (cmd !== "start" && action !== void 0) {
4976
- console.error(chalk5.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
5910
+ console.error(chalk6.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
4977
5911
  process.exit(1);
4978
5912
  }
5913
+ if (options.watch) {
5914
+ process.env.NODE9_WATCH_MODE = "1";
5915
+ setTimeout(() => {
5916
+ openBrowserLocal();
5917
+ console.log(chalk6.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5918
+ }, 600);
5919
+ startDaemon();
5920
+ return;
5921
+ }
4979
5922
  if (options.openui) {
4980
5923
  if (isDaemonRunning()) {
4981
5924
  openBrowserLocal();
4982
- console.log(chalk5.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5925
+ console.log(chalk6.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
4983
5926
  process.exit(0);
4984
5927
  }
4985
- const child = spawn3("node9", ["daemon"], { detached: true, stdio: "ignore" });
5928
+ const child = spawn4("node9", ["daemon"], { detached: true, stdio: "ignore" });
4986
5929
  child.unref();
4987
5930
  for (let i = 0; i < 12; i++) {
4988
5931
  await new Promise((r) => setTimeout(r, 250));
4989
5932
  if (isDaemonRunning()) break;
4990
5933
  }
4991
5934
  openBrowserLocal();
4992
- console.log(chalk5.green(`
5935
+ console.log(chalk6.green(`
4993
5936
  \u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
4994
5937
  process.exit(0);
4995
5938
  }
4996
5939
  if (options.background) {
4997
- const child = spawn3("node9", ["daemon"], { detached: true, stdio: "ignore" });
5940
+ const child = spawn4("node9", ["daemon"], { detached: true, stdio: "ignore" });
4998
5941
  child.unref();
4999
- console.log(chalk5.green(`
5942
+ console.log(chalk6.green(`
5000
5943
  \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
5001
5944
  process.exit(0);
5002
5945
  }
5003
5946
  startDaemon();
5004
5947
  }
5005
5948
  );
5949
+ program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).option("--clear", "Clear history buffer and stream live events fresh", false).action(async (options) => {
5950
+ const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
5951
+ await startTail2(options);
5952
+ });
5006
5953
  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
5954
  const processPayload = async (raw) => {
5008
5955
  try {
@@ -5013,9 +5960,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
5013
5960
  } catch (err) {
5014
5961
  const tempConfig = getConfig();
5015
5962
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
5016
- const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
5963
+ const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
5017
5964
  const errMsg = err instanceof Error ? err.message : String(err);
5018
- fs6.appendFileSync(
5965
+ fs7.appendFileSync(
5019
5966
  logPath,
5020
5967
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
5021
5968
  RAW: ${raw}
@@ -5033,10 +5980,10 @@ RAW: ${raw}
5033
5980
  }
5034
5981
  const config = getConfig();
5035
5982
  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}
5983
+ const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
5984
+ if (!fs7.existsSync(path9.dirname(logPath)))
5985
+ fs7.mkdirSync(path9.dirname(logPath), { recursive: true });
5986
+ fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
5040
5987
  `);
5041
5988
  }
5042
5989
  const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
@@ -5048,18 +5995,18 @@ RAW: ${raw}
5048
5995
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
5049
5996
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
5050
5997
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
5051
- console.error(chalk5.bgRed.white.bold(`
5998
+ console.error(chalk6.bgRed.white.bold(`
5052
5999
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
5053
- console.error(chalk5.red.bold(` A sensitive secret was found in the tool arguments!`));
6000
+ console.error(chalk6.red.bold(` A sensitive secret was found in the tool arguments!`));
5054
6001
  } else {
5055
- console.error(chalk5.red(`
6002
+ console.error(chalk6.red(`
5056
6003
  \u{1F6D1} Node9 blocked "${toolName}"`));
5057
6004
  }
5058
- console.error(chalk5.gray(` Triggered by: ${blockedByContext}`));
5059
- if (result2?.changeHint) console.error(chalk5.cyan(` To change: ${result2.changeHint}`));
6005
+ console.error(chalk6.gray(` Triggered by: ${blockedByContext}`));
6006
+ if (result2?.changeHint) console.error(chalk6.cyan(` To change: ${result2.changeHint}`));
5060
6007
  console.error("");
5061
6008
  const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
5062
- console.error(chalk5.dim(` (Detailed instructions sent to AI agent)`));
6009
+ console.error(chalk6.dim(` (Detailed instructions sent to AI agent)`));
5063
6010
  process.stdout.write(
5064
6011
  JSON.stringify({
5065
6012
  decision: "block",
@@ -5090,7 +6037,7 @@ RAW: ${raw}
5090
6037
  process.exit(0);
5091
6038
  }
5092
6039
  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..."));
6040
+ console.error(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
5094
6041
  const daemonReady = await autoStartDaemonAndWait();
5095
6042
  if (daemonReady) {
5096
6043
  const retry = await authorizeHeadless(toolName, toolInput, false, meta);
@@ -5113,9 +6060,9 @@ RAW: ${raw}
5113
6060
  });
5114
6061
  } catch (err) {
5115
6062
  if (process.env.NODE9_DEBUG === "1") {
5116
- const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
6063
+ const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
5117
6064
  const errMsg = err instanceof Error ? err.message : String(err);
5118
- fs6.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
6065
+ fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
5119
6066
  `);
5120
6067
  }
5121
6068
  process.exit(0);
@@ -5160,10 +6107,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
5160
6107
  decision: "allowed",
5161
6108
  source: "post-hook"
5162
6109
  };
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");
6110
+ const logPath = path9.join(os7.homedir(), ".node9", "audit.log");
6111
+ if (!fs7.existsSync(path9.dirname(logPath)))
6112
+ fs7.mkdirSync(path9.dirname(logPath), { recursive: true });
6113
+ fs7.appendFileSync(logPath, JSON.stringify(entry) + "\n");
5167
6114
  const config = getConfig();
5168
6115
  if (shouldSnapshot(tool, {}, config)) {
5169
6116
  await createShadowSnapshot();
@@ -5190,7 +6137,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
5190
6137
  const ms = parseDuration(options.duration);
5191
6138
  if (ms === null) {
5192
6139
  console.error(
5193
- chalk5.red(`
6140
+ chalk6.red(`
5194
6141
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
5195
6142
  `)
5196
6143
  );
@@ -5198,20 +6145,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
5198
6145
  }
5199
6146
  pauseNode9(ms, options.duration);
5200
6147
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
5201
- console.log(chalk5.yellow(`
6148
+ console.log(chalk6.yellow(`
5202
6149
  \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.
6150
+ console.log(chalk6.gray(` All tool calls will be allowed without review.`));
6151
+ console.log(chalk6.gray(` Run "node9 resume" to re-enable early.
5205
6152
  `));
5206
6153
  });
5207
6154
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
5208
6155
  const { paused } = checkPause();
5209
6156
  if (!paused) {
5210
- console.log(chalk5.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
6157
+ console.log(chalk6.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
5211
6158
  return;
5212
6159
  }
5213
6160
  resumeNode9();
5214
- console.log(chalk5.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
6161
+ console.log(chalk6.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
5215
6162
  });
5216
6163
  var HOOK_BASED_AGENTS = {
5217
6164
  claude: "claude",
@@ -5224,15 +6171,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
5224
6171
  if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
5225
6172
  const target = HOOK_BASED_AGENTS[firstArg];
5226
6173
  console.error(
5227
- chalk5.yellow(`
6174
+ chalk6.yellow(`
5228
6175
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
5229
6176
  );
5230
- console.error(chalk5.white(`
6177
+ console.error(chalk6.white(`
5231
6178
  "${target}" uses its own hook system. Use:`));
5232
6179
  console.error(
5233
- chalk5.green(` node9 addto ${target} `) + chalk5.gray("# one-time setup")
6180
+ chalk6.green(` node9 addto ${target} `) + chalk6.gray("# one-time setup")
5234
6181
  );
5235
- console.error(chalk5.green(` ${target} `) + chalk5.gray("# run normally"));
6182
+ console.error(chalk6.green(` ${target} `) + chalk6.gray("# run normally"));
5236
6183
  process.exit(1);
5237
6184
  }
5238
6185
  const fullCommand = commandArgs.join(" ");
@@ -5240,7 +6187,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
5240
6187
  agent: "Terminal"
5241
6188
  });
5242
6189
  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..."));
6190
+ console.error(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
5244
6191
  const daemonReady = await autoStartDaemonAndWait();
5245
6192
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
5246
6193
  }
@@ -5249,12 +6196,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
5249
6196
  }
5250
6197
  if (!result.approved) {
5251
6198
  console.error(
5252
- chalk5.red(`
6199
+ chalk6.red(`
5253
6200
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
5254
6201
  );
5255
6202
  process.exit(1);
5256
6203
  }
5257
- console.error(chalk5.green("\n\u2705 Approved \u2014 running command...\n"));
6204
+ console.error(chalk6.green("\n\u2705 Approved \u2014 running command...\n"));
5258
6205
  await runProxy(fullCommand);
5259
6206
  } else {
5260
6207
  program.help();
@@ -5269,22 +6216,22 @@ program.command("undo").description(
5269
6216
  if (history.length === 0) {
5270
6217
  if (!options.all && allHistory.length > 0) {
5271
6218
  console.log(
5272
- chalk5.yellow(
6219
+ chalk6.yellow(
5273
6220
  `
5274
6221
  \u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
5275
- Run ${chalk5.cyan("node9 undo --all")} to see snapshots from all projects.
6222
+ Run ${chalk6.cyan("node9 undo --all")} to see snapshots from all projects.
5276
6223
  `
5277
6224
  )
5278
6225
  );
5279
6226
  } else {
5280
- console.log(chalk5.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
6227
+ console.log(chalk6.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
5281
6228
  }
5282
6229
  return;
5283
6230
  }
5284
6231
  const idx = history.length - steps;
5285
6232
  if (idx < 0) {
5286
6233
  console.log(
5287
- chalk5.yellow(
6234
+ chalk6.yellow(
5288
6235
  `
5289
6236
  \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
5290
6237
  `
@@ -5295,18 +6242,18 @@ program.command("undo").description(
5295
6242
  const snapshot = history[idx];
5296
6243
  const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
5297
6244
  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(`
6245
+ console.log(chalk6.magenta.bold(`
5299
6246
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
5300
6247
  console.log(
5301
- chalk5.white(
5302
- ` Tool: ${chalk5.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk5.gray(" \u2192 " + snapshot.argsSummary) : ""}`
6248
+ chalk6.white(
6249
+ ` Tool: ${chalk6.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk6.gray(" \u2192 " + snapshot.argsSummary) : ""}`
5303
6250
  )
5304
6251
  );
5305
- console.log(chalk5.white(` When: ${chalk5.gray(ageStr)}`));
5306
- console.log(chalk5.white(` Dir: ${chalk5.gray(snapshot.cwd)}`));
6252
+ console.log(chalk6.white(` When: ${chalk6.gray(ageStr)}`));
6253
+ console.log(chalk6.white(` Dir: ${chalk6.gray(snapshot.cwd)}`));
5307
6254
  if (steps > 1)
5308
6255
  console.log(
5309
- chalk5.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
6256
+ chalk6.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
5310
6257
  );
5311
6258
  console.log("");
5312
6259
  const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
@@ -5314,21 +6261,21 @@ program.command("undo").description(
5314
6261
  const lines = diff.split("\n");
5315
6262
  for (const line of lines) {
5316
6263
  if (line.startsWith("+++") || line.startsWith("---")) {
5317
- console.log(chalk5.bold(line));
6264
+ console.log(chalk6.bold(line));
5318
6265
  } else if (line.startsWith("+")) {
5319
- console.log(chalk5.green(line));
6266
+ console.log(chalk6.green(line));
5320
6267
  } else if (line.startsWith("-")) {
5321
- console.log(chalk5.red(line));
6268
+ console.log(chalk6.red(line));
5322
6269
  } else if (line.startsWith("@@")) {
5323
- console.log(chalk5.cyan(line));
6270
+ console.log(chalk6.cyan(line));
5324
6271
  } else {
5325
- console.log(chalk5.gray(line));
6272
+ console.log(chalk6.gray(line));
5326
6273
  }
5327
6274
  }
5328
6275
  console.log("");
5329
6276
  } else {
5330
6277
  console.log(
5331
- chalk5.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
6278
+ chalk6.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
5332
6279
  );
5333
6280
  }
5334
6281
  const proceed = await confirm3({
@@ -5337,42 +6284,42 @@ program.command("undo").description(
5337
6284
  });
5338
6285
  if (proceed) {
5339
6286
  if (applyUndo(snapshot.hash, snapshot.cwd)) {
5340
- console.log(chalk5.green("\n\u2705 Reverted successfully.\n"));
6287
+ console.log(chalk6.green("\n\u2705 Reverted successfully.\n"));
5341
6288
  } else {
5342
- console.error(chalk5.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
6289
+ console.error(chalk6.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
5343
6290
  }
5344
6291
  } else {
5345
- console.log(chalk5.gray("\nCancelled.\n"));
6292
+ console.log(chalk6.gray("\nCancelled.\n"));
5346
6293
  }
5347
6294
  });
5348
6295
  var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
5349
6296
  shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
5350
6297
  const name = resolveShieldName(service);
5351
6298
  if (!name) {
5352
- console.error(chalk5.red(`
6299
+ console.error(chalk6.red(`
5353
6300
  \u274C Unknown shield: "${service}"
5354
6301
  `));
5355
- console.log(`Run ${chalk5.cyan("node9 shield list")} to see available shields.
6302
+ console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
5356
6303
  `);
5357
6304
  process.exit(1);
5358
6305
  }
5359
6306
  const shield = getShield(name);
5360
6307
  const active = readActiveShields();
5361
6308
  if (active.includes(name)) {
5362
- console.log(chalk5.yellow(`
6309
+ console.log(chalk6.yellow(`
5363
6310
  \u2139\uFE0F Shield "${name}" is already active.
5364
6311
  `));
5365
6312
  return;
5366
6313
  }
5367
6314
  writeActiveShields([...active, name]);
5368
- console.log(chalk5.green(`
6315
+ console.log(chalk6.green(`
5369
6316
  \u{1F6E1}\uFE0F Shield "${name}" enabled.`));
5370
- console.log(chalk5.gray(` ${shield.smartRules.length} smart rules now active.`));
6317
+ console.log(chalk6.gray(` ${shield.smartRules.length} smart rules now active.`));
5371
6318
  if (shield.dangerousWords.length > 0)
5372
- console.log(chalk5.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
6319
+ console.log(chalk6.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
5373
6320
  if (name === "filesystem") {
5374
6321
  console.log(
5375
- chalk5.yellow(
6322
+ chalk6.yellow(
5376
6323
  `
5377
6324
  \u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
5378
6325
  Tools like unlink, find -delete, or language-level file ops are not intercepted.`
@@ -5384,51 +6331,51 @@ shieldCmd.command("enable <service>").description("Enable a security shield for
5384
6331
  shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
5385
6332
  const name = resolveShieldName(service);
5386
6333
  if (!name) {
5387
- console.error(chalk5.red(`
6334
+ console.error(chalk6.red(`
5388
6335
  \u274C Unknown shield: "${service}"
5389
6336
  `));
5390
- console.log(`Run ${chalk5.cyan("node9 shield list")} to see available shields.
6337
+ console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
5391
6338
  `);
5392
6339
  process.exit(1);
5393
6340
  }
5394
6341
  const active = readActiveShields();
5395
6342
  if (!active.includes(name)) {
5396
- console.log(chalk5.yellow(`
6343
+ console.log(chalk6.yellow(`
5397
6344
  \u2139\uFE0F Shield "${name}" is not active.
5398
6345
  `));
5399
6346
  return;
5400
6347
  }
5401
6348
  writeActiveShields(active.filter((s) => s !== name));
5402
- console.log(chalk5.green(`
6349
+ console.log(chalk6.green(`
5403
6350
  \u{1F6E1}\uFE0F Shield "${name}" disabled.
5404
6351
  `));
5405
6352
  });
5406
6353
  shieldCmd.command("list").description("Show all available shields").action(() => {
5407
6354
  const active = new Set(readActiveShields());
5408
- console.log(chalk5.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
6355
+ console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
5409
6356
  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}`);
6357
+ const status = active.has(shield.name) ? chalk6.green("\u25CF enabled") : chalk6.gray("\u25CB disabled");
6358
+ console.log(` ${status} ${chalk6.cyan(shield.name.padEnd(12))} ${shield.description}`);
5412
6359
  if (shield.aliases.length > 0)
5413
- console.log(chalk5.gray(` aliases: ${shield.aliases.join(", ")}`));
6360
+ console.log(chalk6.gray(` aliases: ${shield.aliases.join(", ")}`));
5414
6361
  }
5415
6362
  console.log("");
5416
6363
  });
5417
6364
  shieldCmd.command("status").description("Show which shields are currently active").action(() => {
5418
6365
  const active = readActiveShields();
5419
6366
  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.
6367
+ console.log(chalk6.yellow("\n\u2139\uFE0F No shields are active.\n"));
6368
+ console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
5422
6369
  `);
5423
6370
  return;
5424
6371
  }
5425
- console.log(chalk5.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
6372
+ console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
5426
6373
  for (const name of active) {
5427
6374
  const shield = getShield(name);
5428
6375
  if (!shield) continue;
5429
- console.log(` ${chalk5.green("\u25CF")} ${chalk5.cyan(name)}`);
6376
+ console.log(` ${chalk6.green("\u25CF")} ${chalk6.cyan(name)}`);
5430
6377
  console.log(
5431
- chalk5.gray(
6378
+ chalk6.gray(
5432
6379
  ` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
5433
6380
  )
5434
6381
  );
@@ -5439,9 +6386,9 @@ process.on("unhandledRejection", (reason) => {
5439
6386
  const isCheckHook = process.argv[2] === "check";
5440
6387
  if (isCheckHook) {
5441
6388
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
5442
- const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
6389
+ const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
5443
6390
  const msg = reason instanceof Error ? reason.message : String(reason);
5444
- fs6.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
6391
+ fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
5445
6392
  `);
5446
6393
  }
5447
6394
  process.exit(0);