@node9/proxy 1.10.3 → 1.11.1

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
@@ -147,8 +147,8 @@ function sanitizeConfig(raw) {
147
147
  }
148
148
  }
149
149
  const lines = result.error.issues.map((issue) => {
150
- const path39 = issue.path.length > 0 ? issue.path.join(".") : "root";
151
- return ` \u2022 ${path39}: ${issue.message}`;
150
+ const path43 = issue.path.length > 0 ? issue.path.join(".") : "root";
151
+ return ` \u2022 ${path43}: ${issue.message}`;
152
152
  });
153
153
  return {
154
154
  sanitized,
@@ -254,6 +254,11 @@ var init_config_schema = __esm({
254
254
  enabled: z.boolean().optional(),
255
255
  threshold: z.number().min(2).optional(),
256
256
  windowSeconds: z.number().min(10).optional()
257
+ }).optional(),
258
+ skillPinning: z.object({
259
+ enabled: z.boolean().optional(),
260
+ mode: z.enum(["warn", "block"]).optional(),
261
+ roots: z.array(z.string()).optional()
257
262
  }).optional()
258
263
  }).optional(),
259
264
  environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
@@ -552,7 +557,11 @@ function getConfig(cwd) {
552
557
  ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
553
558
  },
554
559
  dlp: { ...DEFAULT_CONFIG.policy.dlp },
555
- loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection }
560
+ loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
561
+ skillPinning: {
562
+ ...DEFAULT_CONFIG.policy.skillPinning,
563
+ roots: [...DEFAULT_CONFIG.policy.skillPinning.roots]
564
+ }
556
565
  };
557
566
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
558
567
  const applyLayer = (source) => {
@@ -605,6 +614,16 @@ function getConfig(cwd) {
605
614
  if (ld.windowSeconds !== void 0)
606
615
  mergedPolicy.loopDetection.windowSeconds = ld.windowSeconds;
607
616
  }
617
+ if (p.skillPinning && typeof p.skillPinning === "object") {
618
+ const sp = p.skillPinning;
619
+ if (sp.enabled !== void 0) mergedPolicy.skillPinning.enabled = sp.enabled;
620
+ if (sp.mode !== void 0) mergedPolicy.skillPinning.mode = sp.mode;
621
+ if (Array.isArray(sp.roots)) {
622
+ for (const r of sp.roots) {
623
+ if (typeof r === "string" && r.length > 0) mergedPolicy.skillPinning.roots.push(r);
624
+ }
625
+ }
626
+ }
608
627
  const envs = source.environments || {};
609
628
  for (const [envName, envConfig] of Object.entries(envs)) {
610
629
  if (envConfig && typeof envConfig === "object") {
@@ -656,6 +675,7 @@ function getConfig(cwd) {
656
675
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
657
676
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
658
677
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
678
+ mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
659
679
  mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
660
680
  mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
661
681
  mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
@@ -800,9 +820,8 @@ var init_config = __esm({
800
820
  {
801
821
  field: "command",
802
822
  op: "matches",
803
- // Require the recursive flag to be preceded by whitespace so that
804
- // filenames containing "-r" (e.g. "ai-review.yml") don't false-positive.
805
- value: "rm\\b.*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
823
+ // Anchor rm as a shell command (not inside a string arg like a git commit message).
824
+ value: "(^|&&|\\|\\||;)\\s*rm\\b[^;&|]*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
806
825
  },
807
826
  {
808
827
  field: "command",
@@ -831,6 +850,13 @@ var init_config = __esm({
831
850
  name: "review-drop-truncate-shell",
832
851
  tool: "bash",
833
852
  conditions: [
853
+ {
854
+ field: "command",
855
+ op: "matches",
856
+ // Require a DB CLI in the command so grep/cat/echo of SQL strings don't trigger.
857
+ value: "(^|&&|\\|\\||;|\\|)\\s*(psql|mysql|sqlite3|sqlplus|cockroach|clickhouse-client|mongo)\\b",
858
+ flags: "i"
859
+ },
834
860
  {
835
861
  field: "command",
836
862
  op: "matches",
@@ -851,7 +877,9 @@ var init_config = __esm({
851
877
  {
852
878
  field: "command",
853
879
  op: "matches",
854
- value: "\\bgit\\b.*\\bpush\\b.*(--force|--force-with-lease|-f\\b)",
880
+ // Anchor git as a shell command so node -e / python -c scripts containing
881
+ // "git push --force" as a string don't false-positive.
882
+ value: "(^|&&|\\|\\||;)\\s*git\\s+push[^;&|]*(--force|--force-with-lease|-f\\b)",
855
883
  flags: "i"
856
884
  }
857
885
  ],
@@ -861,29 +889,20 @@ var init_config = __esm({
861
889
  description: "The AI wants to force push to a remote git branch. This rewrites shared history and can permanently destroy commits that teammates have already pulled."
862
890
  },
863
891
  {
864
- name: "review-git-push",
892
+ name: "review-git-destructive",
865
893
  tool: "bash",
866
894
  conditions: [
867
895
  {
868
896
  field: "command",
869
897
  op: "matches",
870
- value: "\\bgit\\b.*\\bpush\\b(?!.*(-f\\b|--force|--force-with-lease))",
898
+ value: "\\bgit\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
871
899
  flags: "i"
872
- }
873
- ],
874
- conditionMode: "all",
875
- verdict: "review",
876
- reason: "git push sends changes to a shared remote",
877
- description: "The AI wants to push commits to a remote repository. Once pushed, those changes are visible to everyone with access."
878
- },
879
- {
880
- name: "review-git-destructive",
881
- tool: "bash",
882
- conditions: [
900
+ },
883
901
  {
884
902
  field: "command",
885
- op: "matches",
886
- value: "\\bgit\\b.*(reset\\s+--hard|clean\\s+-[fdxX]|\\brebase\\b|tag\\s+-d|branch\\s+-[dD])",
903
+ op: "notMatches",
904
+ // Exclude recovery ops — these resolve a conflict, not start a destructive action.
905
+ value: "\\bgit\\s+rebase\\s+--(abort|continue|skip)\\b",
887
906
  flags: "i"
888
907
  }
889
908
  ],
@@ -909,7 +928,9 @@ var init_config = __esm({
909
928
  {
910
929
  field: "command",
911
930
  op: "matches",
912
- value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
931
+ // Anchor curl/wget as a shell command so node -e scripts testing this
932
+ // regex pattern don't self-match as a false positive.
933
+ value: "(^|&&|\\|\\||;)\\s*(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
913
934
  flags: "i"
914
935
  }
915
936
  ],
@@ -920,7 +941,8 @@ var init_config = __esm({
920
941
  }
921
942
  ],
922
943
  dlp: { enabled: true, scanIgnoredTools: true },
923
- loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 }
944
+ loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
945
+ skillPinning: { enabled: false, mode: "warn", roots: [] }
924
946
  },
925
947
  environments: {}
926
948
  };
@@ -1132,6 +1154,20 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
1132
1154
  }
1133
1155
  return null;
1134
1156
  }
1157
+ function scanText(text) {
1158
+ const t = text.length > MAX_STRING_BYTES ? text.slice(0, MAX_STRING_BYTES) : text;
1159
+ for (const pattern of DLP_PATTERNS) {
1160
+ if (pattern.regex.test(t)) {
1161
+ return {
1162
+ patternName: pattern.name,
1163
+ fieldPath: "response-text",
1164
+ redactedSample: maskSecret(t, pattern.regex),
1165
+ severity: pattern.severity
1166
+ };
1167
+ }
1168
+ }
1169
+ return null;
1170
+ }
1135
1171
  var DLP_PATTERNS, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
1136
1172
  var init_dlp = __esm({
1137
1173
  "src/dlp.ts"() {
@@ -1161,7 +1197,7 @@ var init_dlp = __esm({
1161
1197
  regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
1162
1198
  severity: "block"
1163
1199
  },
1164
- { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
1200
+ { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i, severity: "review" }
1165
1201
  ];
1166
1202
  SENSITIVE_PATH_PATTERNS = [
1167
1203
  /[/\\]\.ssh[/\\]/i,
@@ -1729,9 +1765,21 @@ function matchesPattern(text, patterns) {
1729
1765
  const withoutDotSlash = text.replace(/^\.\//, "");
1730
1766
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1731
1767
  }
1732
- function getNestedValue(obj, path39) {
1768
+ function getNestedValue(obj, path43) {
1733
1769
  if (!obj || typeof obj !== "object") return null;
1734
- return path39.split(".").reduce((prev, curr) => prev?.[curr], obj);
1770
+ return path43.split(".").reduce((prev, curr) => prev?.[curr], obj);
1771
+ }
1772
+ function stripStringArguments(cmd) {
1773
+ let result = cmd;
1774
+ result = result.replace(
1775
+ /\b(node|python3?|ruby|perl|php|deno)\s+(-[ecr]|eval)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/gi,
1776
+ '$1 $2 ""'
1777
+ );
1778
+ result = result.replace(
1779
+ /\s(-m|--message|--body|--title|--description)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,
1780
+ ' $1 ""'
1781
+ );
1782
+ return result;
1735
1783
  }
1736
1784
  function shouldSnapshot(toolName, args, config) {
1737
1785
  if (!config.settings.enableUndo) return false;
@@ -1750,7 +1798,8 @@ function evaluateSmartConditions(args, rule) {
1750
1798
  const mode = rule.conditionMode ?? "all";
1751
1799
  const results = rule.conditions.map((cond) => {
1752
1800
  const rawVal = getNestedValue(args, cond.field);
1753
- const val = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
1801
+ const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
1802
+ const val = cond.field === "command" && normalized !== null ? stripStringArguments(normalized) : normalized;
1754
1803
  switch (cond.op) {
1755
1804
  case "exists":
1756
1805
  return val !== null && val !== "";
@@ -2324,6 +2373,15 @@ var init_policy = __esm({
2324
2373
  import fs8 from "fs";
2325
2374
  import path9 from "path";
2326
2375
  import os7 from "os";
2376
+ function extractCommandPattern(toolName, args) {
2377
+ const lower = toolName.toLowerCase();
2378
+ if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
2379
+ const a = args;
2380
+ const cmd = typeof a?.["command"] === "string" ? a["command"].trim() : "";
2381
+ if (!cmd) return void 0;
2382
+ const words = cmd.split(/\s+/);
2383
+ return words.slice(0, 2).join(" ");
2384
+ }
2327
2385
  function checkPause() {
2328
2386
  try {
2329
2387
  if (!fs8.existsSync(PAUSED_FILE)) return { paused: false };
@@ -2357,7 +2415,7 @@ function resumeNode9() {
2357
2415
  } catch {
2358
2416
  }
2359
2417
  }
2360
- function getActiveTrustSession(toolName) {
2418
+ function getActiveTrustSession(toolName, args) {
2361
2419
  try {
2362
2420
  if (!fs8.existsSync(TRUST_FILE)) return false;
2363
2421
  const trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
@@ -2366,12 +2424,20 @@ function getActiveTrustSession(toolName) {
2366
2424
  if (active.length !== trust.entries.length) {
2367
2425
  fs8.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
2368
2426
  }
2369
- return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
2427
+ return active.some((e) => {
2428
+ if (!(e.tool === toolName || matchesPattern(toolName, e.tool))) return false;
2429
+ if (e.commandPattern) {
2430
+ const actual = extractCommandPattern(toolName, args) ?? "";
2431
+ return actual === e.commandPattern || actual.startsWith(e.commandPattern + " ");
2432
+ }
2433
+ return true;
2434
+ });
2370
2435
  } catch {
2371
2436
  return false;
2372
2437
  }
2373
2438
  }
2374
- function writeTrustSession(toolName, durationMs) {
2439
+ function writeTrustSession(toolName, durationMs, args) {
2440
+ const commandPattern = extractCommandPattern(toolName, args);
2375
2441
  try {
2376
2442
  let trust = { entries: [] };
2377
2443
  try {
@@ -2381,8 +2447,14 @@ function writeTrustSession(toolName, durationMs) {
2381
2447
  } catch {
2382
2448
  }
2383
2449
  const now = Date.now();
2384
- trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > now);
2385
- trust.entries.push({ tool: toolName, expiry: now + durationMs });
2450
+ trust.entries = trust.entries.filter(
2451
+ (e) => !(e.tool === toolName && e.commandPattern === commandPattern) && e.expiry > now
2452
+ );
2453
+ trust.entries.push({
2454
+ tool: toolName,
2455
+ ...commandPattern && { commandPattern },
2456
+ expiry: now + durationMs
2457
+ });
2386
2458
  atomicWriteSync(TRUST_FILE, JSON.stringify(trust, null, 2));
2387
2459
  } catch (err2) {
2388
2460
  if (process.env.NODE9_DEBUG === "1") {
@@ -2833,13 +2905,30 @@ ${smartTruncate(str, 500)}`
2833
2905
  }
2834
2906
  return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
2835
2907
  }
2908
+ function sendDesktopNotification(title, body) {
2909
+ if (isTestEnv()) return;
2910
+ try {
2911
+ if (process.platform === "darwin") {
2912
+ const esc = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2913
+ const script = `display notification "${esc(body)}" with title "${esc(title)}"`;
2914
+ spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
2915
+ } else if (process.platform === "linux") {
2916
+ spawn("notify-send", [title, body, "--icon=dialog-warning"], {
2917
+ detached: true,
2918
+ stdio: "ignore"
2919
+ }).unref();
2920
+ }
2921
+ } catch {
2922
+ }
2923
+ }
2836
2924
  function escapePango(text) {
2837
2925
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2838
2926
  }
2839
2927
  function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
2840
2928
  const lines = [];
2841
2929
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
2842
- lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
2930
+ const safeAgent = (agent ?? "AI Agent").replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80);
2931
+ lines.push(`\u{1F916} ${safeAgent} | \u{1F527} ${toolName}`);
2843
2932
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
2844
2933
  if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
2845
2934
  lines.push("");
@@ -3224,7 +3313,16 @@ async function authorizeHeadless(toolName, args, meta, options) {
3224
3313
  if (!options?.calledFromDaemon) {
3225
3314
  const actId = randomUUID();
3226
3315
  const actTs = Date.now();
3227
- await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
3316
+ await notifyActivity({
3317
+ id: actId,
3318
+ ts: actTs,
3319
+ tool: toolName,
3320
+ args,
3321
+ status: "pending",
3322
+ // Strip ANSI escape sequences — agent name comes from caller-supplied metadata
3323
+ // and may be displayed in a terminal (node9 tail/watch), enabling injection.
3324
+ agent: meta?.agent ? meta.agent.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80) : void 0
3325
+ });
3228
3326
  const result = await _authorizeHeadlessCore(toolName, args, meta, {
3229
3327
  ...options,
3230
3328
  activityId: actId
@@ -3378,12 +3476,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3378
3476
  };
3379
3477
  }
3380
3478
  }
3381
- if (getActiveTrustSession(toolName)) {
3382
- if (approvers.cloud && creds?.apiKey)
3383
- await auditLocalAllow(toolName, args, "trust", creds, meta);
3384
- if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
3385
- return { approved: true, checkedBy: "trust" };
3386
- }
3387
3479
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
3388
3480
  if (policyResult.decision === "allow") {
3389
3481
  if (approvers.cloud && creds?.apiKey)
@@ -3465,6 +3557,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3465
3557
  if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
3466
3558
  return { approved: true };
3467
3559
  }
3560
+ if (!taintWarning && getActiveTrustSession(toolName, args)) {
3561
+ if (approvers.cloud && creds?.apiKey)
3562
+ await auditLocalAllow(toolName, args, "trust", creds, meta);
3563
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
3564
+ return { approved: true, checkedBy: "trust" };
3565
+ }
3468
3566
  if (taintWarning) {
3469
3567
  explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
3470
3568
  riskMetadata = computeRiskMetadata(
@@ -3597,7 +3695,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3597
3695
  riskMetadata?.ruleDescription
3598
3696
  );
3599
3697
  if (decision === "always_allow") {
3600
- writeTrustSession(toolName, 36e5);
3698
+ writeTrustSession(toolName, 36e5, args);
3601
3699
  return { approved: true, checkedBy: "trust" };
3602
3700
  }
3603
3701
  const isApproved = decision === "allow";
@@ -5775,7 +5873,7 @@ function writeGlobalSetting(key, value) {
5775
5873
  config.settings[key] = value;
5776
5874
  atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
5777
5875
  }
5778
- function writeTrustEntry(toolName, durationMs) {
5876
+ function writeTrustEntry(toolName, durationMs, commandPattern) {
5779
5877
  try {
5780
5878
  let trust = { entries: [] };
5781
5879
  try {
@@ -5783,8 +5881,14 @@ function writeTrustEntry(toolName, durationMs) {
5783
5881
  trust = JSON.parse(fs14.readFileSync(TRUST_FILE2, "utf-8"));
5784
5882
  } catch {
5785
5883
  }
5786
- trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
5787
- trust.entries.push({ tool: toolName, expiry: Date.now() + durationMs });
5884
+ trust.entries = trust.entries.filter(
5885
+ (e) => !(e.tool === toolName && e.commandPattern === commandPattern) && e.expiry > Date.now()
5886
+ );
5887
+ trust.entries.push({
5888
+ tool: toolName,
5889
+ ...commandPattern && { commandPattern },
5890
+ expiry: Date.now() + durationMs
5891
+ });
5788
5892
  atomicWriteSync2(TRUST_FILE2, JSON.stringify(trust, null, 2));
5789
5893
  } catch {
5790
5894
  }
@@ -5940,7 +6044,8 @@ function startActivitySocket() {
5940
6044
  ts: data.ts,
5941
6045
  tool: data.tool,
5942
6046
  args: redactArgs(data.args),
5943
- status: "pending"
6047
+ status: "pending",
6048
+ agent: data.agent
5944
6049
  });
5945
6050
  } else {
5946
6051
  if (data.status === "allow") {
@@ -6398,16 +6503,167 @@ var init_sync = __esm({
6398
6503
  }
6399
6504
  });
6400
6505
 
6401
- // src/daemon/server.ts
6402
- import http from "http";
6506
+ // src/daemon/dlp-scanner.ts
6403
6507
  import fs18 from "fs";
6404
6508
  import path21 from "path";
6509
+ import os16 from "os";
6510
+ function loadIndex() {
6511
+ try {
6512
+ return JSON.parse(fs18.readFileSync(INDEX_FILE, "utf-8"));
6513
+ } catch {
6514
+ return {};
6515
+ }
6516
+ }
6517
+ function saveIndex(index) {
6518
+ try {
6519
+ fs18.writeFileSync(INDEX_FILE, JSON.stringify(index), { encoding: "utf-8", mode: 384 });
6520
+ } catch {
6521
+ }
6522
+ }
6523
+ function appendAuditEntry(entry) {
6524
+ try {
6525
+ fs18.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
6526
+ } catch {
6527
+ }
6528
+ }
6529
+ function runDlpScan() {
6530
+ if (!fs18.existsSync(PROJECTS_DIR)) return;
6531
+ const index = loadIndex();
6532
+ let updated = false;
6533
+ let projDirs;
6534
+ try {
6535
+ projDirs = fs18.readdirSync(PROJECTS_DIR);
6536
+ } catch {
6537
+ return;
6538
+ }
6539
+ for (const proj of projDirs) {
6540
+ const projPath = path21.join(PROJECTS_DIR, proj);
6541
+ try {
6542
+ if (!fs18.lstatSync(projPath).isDirectory()) continue;
6543
+ const real = fs18.realpathSync(projPath);
6544
+ if (!real.startsWith(PROJECTS_DIR + path21.sep) && real !== PROJECTS_DIR) continue;
6545
+ } catch {
6546
+ continue;
6547
+ }
6548
+ let files;
6549
+ try {
6550
+ files = fs18.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
6551
+ } catch {
6552
+ continue;
6553
+ }
6554
+ for (const file of files) {
6555
+ const filePath = path21.join(projPath, file);
6556
+ const lastOffset = index[filePath] ?? 0;
6557
+ let size;
6558
+ try {
6559
+ size = fs18.statSync(filePath).size;
6560
+ } catch {
6561
+ continue;
6562
+ }
6563
+ if (size <= lastOffset) continue;
6564
+ let fd;
6565
+ try {
6566
+ fd = fs18.openSync(filePath, "r");
6567
+ } catch {
6568
+ continue;
6569
+ }
6570
+ try {
6571
+ const chunkSize = size - lastOffset;
6572
+ const buf = Buffer.alloc(chunkSize);
6573
+ fs18.readSync(fd, buf, 0, chunkSize, lastOffset);
6574
+ const chunk = buf.toString("utf-8");
6575
+ for (const line of chunk.split("\n")) {
6576
+ if (!line.trim()) continue;
6577
+ let entry;
6578
+ try {
6579
+ entry = JSON.parse(line);
6580
+ } catch {
6581
+ continue;
6582
+ }
6583
+ if (entry.type !== "assistant") continue;
6584
+ const content = entry.message?.content;
6585
+ if (!Array.isArray(content)) continue;
6586
+ for (const block of content) {
6587
+ if (typeof block !== "object" || block === null || block.type !== "text")
6588
+ continue;
6589
+ const text = block.text;
6590
+ if (typeof text !== "string") continue;
6591
+ const match = scanText(text);
6592
+ if (!match) continue;
6593
+ const projLabel = decodeURIComponent(proj).replace(os16.homedir(), "~").slice(0, 40);
6594
+ const ts = entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
6595
+ appendAuditEntry({
6596
+ ts,
6597
+ tool: "response-text",
6598
+ decision: "dlp",
6599
+ checkedBy: "response-dlp",
6600
+ source: "response-dlp",
6601
+ dlpPattern: match.patternName,
6602
+ dlpSample: match.redactedSample,
6603
+ project: projLabel
6604
+ });
6605
+ sendDesktopNotification(
6606
+ "\u26A0\uFE0F node9 DLP Alert",
6607
+ `${match.patternName} found in Claude response
6608
+ Sample: ${match.redactedSample}
6609
+ Project: ${projLabel}
6610
+ Run: node9 report --period 30d`
6611
+ );
6612
+ }
6613
+ }
6614
+ index[filePath] = size;
6615
+ updated = true;
6616
+ } finally {
6617
+ try {
6618
+ fs18.closeSync(fd);
6619
+ } catch {
6620
+ }
6621
+ }
6622
+ }
6623
+ }
6624
+ if (updated) saveIndex(index);
6625
+ }
6626
+ function startDlpScanner() {
6627
+ setImmediate(() => {
6628
+ try {
6629
+ runDlpScan();
6630
+ } catch {
6631
+ }
6632
+ });
6633
+ const timer = setInterval(
6634
+ () => {
6635
+ try {
6636
+ runDlpScan();
6637
+ } catch {
6638
+ }
6639
+ },
6640
+ 60 * 60 * 1e3
6641
+ );
6642
+ timer.unref();
6643
+ }
6644
+ var INDEX_FILE, PROJECTS_DIR;
6645
+ var init_dlp_scanner = __esm({
6646
+ "src/daemon/dlp-scanner.ts"() {
6647
+ "use strict";
6648
+ init_dlp();
6649
+ init_native();
6650
+ init_state2();
6651
+ INDEX_FILE = path21.join(os16.homedir(), ".node9", "dlp-index.json");
6652
+ PROJECTS_DIR = path21.join(os16.homedir(), ".claude", "projects");
6653
+ }
6654
+ });
6655
+
6656
+ // src/daemon/server.ts
6657
+ import http from "http";
6658
+ import fs19 from "fs";
6659
+ import path22 from "path";
6405
6660
  import { randomUUID as randomUUID4 } from "crypto";
6406
6661
  import { spawnSync as spawnSync2 } from "child_process";
6407
6662
  import chalk2 from "chalk";
6408
6663
  function startDaemon() {
6409
6664
  startCostSync();
6410
6665
  startCloudSync();
6666
+ startDlpScanner();
6411
6667
  loadInsightCounts();
6412
6668
  const csrfToken = randomUUID4();
6413
6669
  const internalToken = randomUUID4();
@@ -6423,7 +6679,7 @@ function startDaemon() {
6423
6679
  idleTimer = setTimeout(() => {
6424
6680
  if (autoStarted) {
6425
6681
  try {
6426
- fs18.unlinkSync(DAEMON_PID_FILE);
6682
+ fs19.unlinkSync(DAEMON_PID_FILE);
6427
6683
  } catch {
6428
6684
  }
6429
6685
  }
@@ -6586,7 +6842,7 @@ data: ${JSON.stringify(item.data)}
6586
6842
  status: "pending"
6587
6843
  });
6588
6844
  }
6589
- const projectCwd = typeof cwd === "string" && path21.isAbsolute(cwd) ? cwd : void 0;
6845
+ const projectCwd = typeof cwd === "string" && path22.isAbsolute(cwd) ? cwd : void 0;
6590
6846
  const projectConfig = getConfig(projectCwd);
6591
6847
  const browserEnabled = projectConfig.settings.approvers?.browser !== false;
6592
6848
  const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
@@ -6713,7 +6969,8 @@ data: ${JSON.stringify(item.data)}
6713
6969
  );
6714
6970
  if (decision === "trust" && trustDuration) {
6715
6971
  const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
6716
- writeTrustEntry(entry.toolName, ms);
6972
+ const commandPattern = extractCommandPattern(entry.toolName, entry.args);
6973
+ writeTrustEntry(entry.toolName, ms, commandPattern);
6717
6974
  appendAuditLog({
6718
6975
  toolName: entry.toolName,
6719
6976
  args: entry.args,
@@ -6976,8 +7233,8 @@ data: ${JSON.stringify(item.data)}
6976
7233
  const body = await readBody(req);
6977
7234
  const data = body ? JSON.parse(body) : {};
6978
7235
  const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
6979
- const node9Dir = path21.dirname(GLOBAL_CONFIG_PATH);
6980
- if (!path21.resolve(configPath).startsWith(node9Dir + path21.sep)) {
7236
+ const node9Dir = path22.dirname(GLOBAL_CONFIG_PATH);
7237
+ if (!path22.resolve(configPath).startsWith(node9Dir + path22.sep)) {
6981
7238
  res.writeHead(400, { "Content-Type": "application/json" });
6982
7239
  return res.end(
6983
7240
  JSON.stringify({ error: "configPath must be within the node9 config directory" })
@@ -7088,14 +7345,14 @@ data: ${JSON.stringify(item.data)}
7088
7345
  server.on("error", (e) => {
7089
7346
  if (e.code === "EADDRINUSE") {
7090
7347
  try {
7091
- if (fs18.existsSync(DAEMON_PID_FILE)) {
7092
- const { pid } = JSON.parse(fs18.readFileSync(DAEMON_PID_FILE, "utf-8"));
7348
+ if (fs19.existsSync(DAEMON_PID_FILE)) {
7349
+ const { pid } = JSON.parse(fs19.readFileSync(DAEMON_PID_FILE, "utf-8"));
7093
7350
  process.kill(pid, 0);
7094
7351
  return process.exit(0);
7095
7352
  }
7096
7353
  } catch {
7097
7354
  try {
7098
- fs18.unlinkSync(DAEMON_PID_FILE);
7355
+ fs19.unlinkSync(DAEMON_PID_FILE);
7099
7356
  } catch {
7100
7357
  }
7101
7358
  server.listen(DAEMON_PORT, DAEMON_HOST);
@@ -7161,23 +7418,25 @@ var init_server = __esm({
7161
7418
  init_shields();
7162
7419
  init_ui2();
7163
7420
  init_state2();
7421
+ init_state();
7164
7422
  init_patch();
7165
7423
  init_config_schema();
7166
7424
  init_costSync();
7167
7425
  init_sync();
7426
+ init_dlp_scanner();
7168
7427
  }
7169
7428
  });
7170
7429
 
7171
7430
  // src/daemon/service.ts
7172
- import fs19 from "fs";
7173
- import path22 from "path";
7174
- import os16 from "os";
7431
+ import fs20 from "fs";
7432
+ import path23 from "path";
7433
+ import os17 from "os";
7175
7434
  import { spawnSync as spawnSync3, execFileSync } from "child_process";
7176
7435
  function resolveNode9Binary() {
7177
7436
  try {
7178
7437
  const script = process.argv[1];
7179
- if (typeof script === "string" && path22.isAbsolute(script) && fs19.existsSync(script)) {
7180
- return fs19.realpathSync(script);
7438
+ if (typeof script === "string" && path23.isAbsolute(script) && fs20.existsSync(script)) {
7439
+ return fs20.realpathSync(script);
7181
7440
  }
7182
7441
  } catch {
7183
7442
  }
@@ -7195,11 +7454,11 @@ function xmlEscape(s) {
7195
7454
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
7196
7455
  }
7197
7456
  function launchdPlist(binaryPath) {
7198
- const logDir = path22.join(os16.homedir(), ".node9");
7457
+ const logDir = path23.join(os17.homedir(), ".node9");
7199
7458
  const nodePath = xmlEscape(process.execPath);
7200
7459
  const scriptPath = xmlEscape(binaryPath);
7201
- const outLog = xmlEscape(path22.join(logDir, "daemon.log"));
7202
- const errLog = xmlEscape(path22.join(logDir, "daemon-error.log"));
7460
+ const outLog = xmlEscape(path23.join(logDir, "daemon.log"));
7461
+ const errLog = xmlEscape(path23.join(logDir, "daemon-error.log"));
7203
7462
  return `<?xml version="1.0" encoding="UTF-8"?>
7204
7463
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
7205
7464
  <plist version="1.0">
@@ -7234,9 +7493,9 @@ function launchdPlist(binaryPath) {
7234
7493
  `;
7235
7494
  }
7236
7495
  function installLaunchd(binaryPath) {
7237
- const dir = path22.dirname(LAUNCHD_PLIST);
7238
- if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
7239
- fs19.writeFileSync(LAUNCHD_PLIST, launchdPlist(binaryPath), "utf-8");
7496
+ const dir = path23.dirname(LAUNCHD_PLIST);
7497
+ if (!fs20.existsSync(dir)) fs20.mkdirSync(dir, { recursive: true });
7498
+ fs20.writeFileSync(LAUNCHD_PLIST, launchdPlist(binaryPath), "utf-8");
7240
7499
  spawnSync3("launchctl", ["unload", LAUNCHD_PLIST], { encoding: "utf8" });
7241
7500
  const r = spawnSync3("launchctl", ["load", "-w", LAUNCHD_PLIST], {
7242
7501
  encoding: "utf8",
@@ -7247,13 +7506,13 @@ function installLaunchd(binaryPath) {
7247
7506
  }
7248
7507
  }
7249
7508
  function uninstallLaunchd() {
7250
- if (fs19.existsSync(LAUNCHD_PLIST)) {
7509
+ if (fs20.existsSync(LAUNCHD_PLIST)) {
7251
7510
  spawnSync3("launchctl", ["unload", "-w", LAUNCHD_PLIST], { encoding: "utf8", timeout: 5e3 });
7252
- fs19.unlinkSync(LAUNCHD_PLIST);
7511
+ fs20.unlinkSync(LAUNCHD_PLIST);
7253
7512
  }
7254
7513
  }
7255
7514
  function isLaunchdInstalled() {
7256
- return fs19.existsSync(LAUNCHD_PLIST);
7515
+ return fs20.existsSync(LAUNCHD_PLIST);
7257
7516
  }
7258
7517
  function systemdUnit(binaryPath) {
7259
7518
  return `[Unit]
@@ -7273,12 +7532,12 @@ WantedBy=default.target
7273
7532
  `;
7274
7533
  }
7275
7534
  function installSystemd(binaryPath) {
7276
- if (!fs19.existsSync(SYSTEMD_UNIT_DIR)) {
7277
- fs19.mkdirSync(SYSTEMD_UNIT_DIR, { recursive: true });
7535
+ if (!fs20.existsSync(SYSTEMD_UNIT_DIR)) {
7536
+ fs20.mkdirSync(SYSTEMD_UNIT_DIR, { recursive: true });
7278
7537
  }
7279
- fs19.writeFileSync(SYSTEMD_UNIT, systemdUnit(binaryPath), "utf-8");
7538
+ fs20.writeFileSync(SYSTEMD_UNIT, systemdUnit(binaryPath), "utf-8");
7280
7539
  try {
7281
- execFileSync("loginctl", ["enable-linger", os16.userInfo().username], { timeout: 3e3 });
7540
+ execFileSync("loginctl", ["enable-linger", os17.userInfo().username], { timeout: 3e3 });
7282
7541
  } catch {
7283
7542
  }
7284
7543
  const reload = spawnSync3("systemctl", ["--user", "daemon-reload"], {
@@ -7298,23 +7557,23 @@ function installSystemd(binaryPath) {
7298
7557
  }
7299
7558
  }
7300
7559
  function uninstallSystemd() {
7301
- if (fs19.existsSync(SYSTEMD_UNIT)) {
7560
+ if (fs20.existsSync(SYSTEMD_UNIT)) {
7302
7561
  spawnSync3("systemctl", ["--user", "disable", "--now", "node9-daemon"], {
7303
7562
  encoding: "utf8",
7304
7563
  timeout: 5e3
7305
7564
  });
7306
7565
  spawnSync3("systemctl", ["--user", "daemon-reload"], { encoding: "utf8", timeout: 5e3 });
7307
- fs19.unlinkSync(SYSTEMD_UNIT);
7566
+ fs20.unlinkSync(SYSTEMD_UNIT);
7308
7567
  }
7309
7568
  }
7310
7569
  function isSystemdInstalled() {
7311
- return fs19.existsSync(SYSTEMD_UNIT);
7570
+ return fs20.existsSync(SYSTEMD_UNIT);
7312
7571
  }
7313
7572
  function stopRunningDaemon() {
7314
- const pidFile = path22.join(os16.homedir(), ".node9", "daemon.pid");
7315
- if (!fs19.existsSync(pidFile)) return;
7573
+ const pidFile = path23.join(os17.homedir(), ".node9", "daemon.pid");
7574
+ if (!fs20.existsSync(pidFile)) return;
7316
7575
  try {
7317
- const data = JSON.parse(fs19.readFileSync(pidFile, "utf-8"));
7576
+ const data = JSON.parse(fs20.readFileSync(pidFile, "utf-8"));
7318
7577
  const pid = data.pid;
7319
7578
  const MAX_PID2 = 4194304;
7320
7579
  if (typeof pid === "number" && Number.isInteger(pid) && pid > 0 && pid <= MAX_PID2) {
@@ -7334,7 +7593,7 @@ function stopRunningDaemon() {
7334
7593
  }
7335
7594
  }
7336
7595
  try {
7337
- fs19.unlinkSync(pidFile);
7596
+ fs20.unlinkSync(pidFile);
7338
7597
  } catch {
7339
7598
  }
7340
7599
  } catch {
@@ -7409,20 +7668,20 @@ var init_service = __esm({
7409
7668
  "src/daemon/service.ts"() {
7410
7669
  "use strict";
7411
7670
  LAUNCHD_LABEL = "ai.node9.daemon";
7412
- LAUNCHD_PLIST = path22.join(os16.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
7413
- SYSTEMD_UNIT_DIR = path22.join(os16.homedir(), ".config", "systemd", "user");
7414
- SYSTEMD_UNIT = path22.join(SYSTEMD_UNIT_DIR, "node9-daemon.service");
7671
+ LAUNCHD_PLIST = path23.join(os17.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
7672
+ SYSTEMD_UNIT_DIR = path23.join(os17.homedir(), ".config", "systemd", "user");
7673
+ SYSTEMD_UNIT = path23.join(SYSTEMD_UNIT_DIR, "node9-daemon.service");
7415
7674
  }
7416
7675
  });
7417
7676
 
7418
7677
  // src/daemon/index.ts
7419
- import fs20 from "fs";
7678
+ import fs21 from "fs";
7420
7679
  import chalk3 from "chalk";
7421
7680
  import { spawnSync as spawnSync4 } from "child_process";
7422
7681
  function stopDaemon() {
7423
- if (!fs20.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
7682
+ if (!fs21.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
7424
7683
  try {
7425
- const data = JSON.parse(fs20.readFileSync(DAEMON_PID_FILE, "utf-8"));
7684
+ const data = JSON.parse(fs21.readFileSync(DAEMON_PID_FILE, "utf-8"));
7426
7685
  const pid = data.pid;
7427
7686
  if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
7428
7687
  console.log(chalk3.gray("Cleaned up invalid PID file."));
@@ -7434,7 +7693,7 @@ function stopDaemon() {
7434
7693
  console.log(chalk3.gray("Cleaned up stale PID file."));
7435
7694
  } finally {
7436
7695
  try {
7437
- fs20.unlinkSync(DAEMON_PID_FILE);
7696
+ fs21.unlinkSync(DAEMON_PID_FILE);
7438
7697
  } catch {
7439
7698
  }
7440
7699
  }
@@ -7443,9 +7702,9 @@ function daemonStatus() {
7443
7702
  const serviceInstalled = isDaemonServiceInstalled();
7444
7703
  const serviceLabel = serviceInstalled ? chalk3.green("installed (starts on login)") : chalk3.yellow("not installed \u2014 run: node9 daemon install");
7445
7704
  let processStatus;
7446
- if (fs20.existsSync(DAEMON_PID_FILE)) {
7705
+ if (fs21.existsSync(DAEMON_PID_FILE)) {
7447
7706
  try {
7448
- const data = JSON.parse(fs20.readFileSync(DAEMON_PID_FILE, "utf-8"));
7707
+ const data = JSON.parse(fs21.readFileSync(DAEMON_PID_FILE, "utf-8"));
7449
7708
  const pid = data.pid;
7450
7709
  const port = data.port;
7451
7710
  if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
@@ -7494,10 +7753,10 @@ __export(tail_exports, {
7494
7753
  startTail: () => startTail
7495
7754
  });
7496
7755
  import http2 from "http";
7497
- import chalk23 from "chalk";
7498
- import fs33 from "fs";
7499
- import os29 from "os";
7500
- import path36 from "path";
7756
+ import chalk25 from "chalk";
7757
+ import fs37 from "fs";
7758
+ import os33 from "os";
7759
+ import path40 from "path";
7501
7760
  import readline5 from "readline";
7502
7761
  import { spawn as spawn10, execSync as execSync3 } from "child_process";
7503
7762
  function getIcon(tool) {
@@ -7507,6 +7766,74 @@ function getIcon(tool) {
7507
7766
  }
7508
7767
  return "\u{1F6E0}\uFE0F";
7509
7768
  }
7769
+ function getModelContextLimit(model) {
7770
+ const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
7771
+ for (const [key, limit] of Object.entries(MODEL_CONTEXT_LIMITS)) {
7772
+ if (base.startsWith(key)) return limit;
7773
+ }
7774
+ return 2e5;
7775
+ }
7776
+ function readSessionUsage() {
7777
+ const projectsDir = path40.join(os33.homedir(), ".claude", "projects");
7778
+ if (!fs37.existsSync(projectsDir)) return null;
7779
+ let latestFile = null;
7780
+ let latestMtime = 0;
7781
+ try {
7782
+ for (const dir of fs37.readdirSync(projectsDir)) {
7783
+ const dirPath = path40.join(projectsDir, dir);
7784
+ try {
7785
+ if (!fs37.statSync(dirPath).isDirectory()) continue;
7786
+ for (const file of fs37.readdirSync(dirPath)) {
7787
+ if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
7788
+ const filePath = path40.join(dirPath, file);
7789
+ try {
7790
+ const mtime = fs37.statSync(filePath).mtimeMs;
7791
+ if (mtime > latestMtime) {
7792
+ latestMtime = mtime;
7793
+ latestFile = filePath;
7794
+ }
7795
+ } catch {
7796
+ }
7797
+ }
7798
+ } catch {
7799
+ }
7800
+ }
7801
+ } catch {
7802
+ }
7803
+ if (!latestFile) return null;
7804
+ try {
7805
+ const lines = fs37.readFileSync(latestFile, "utf-8").split("\n");
7806
+ let lastModel = "";
7807
+ let lastInput = 0;
7808
+ let lastOutput = 0;
7809
+ for (const line of lines) {
7810
+ if (!line.trim()) continue;
7811
+ try {
7812
+ const entry = JSON.parse(line);
7813
+ if (entry.type !== "assistant" || !entry.message?.usage) continue;
7814
+ const u = entry.message.usage;
7815
+ lastInput = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
7816
+ lastOutput = u.output_tokens ?? 0;
7817
+ if (entry.message.model) lastModel = entry.message.model;
7818
+ } catch {
7819
+ }
7820
+ }
7821
+ if (!lastModel || lastInput === 0) return null;
7822
+ const limit = getModelContextLimit(lastModel);
7823
+ const fillPct = Math.round(lastInput / limit * 100);
7824
+ return { inputTokens: lastInput, outputTokens: lastOutput, model: lastModel, fillPct };
7825
+ } catch {
7826
+ return null;
7827
+ }
7828
+ }
7829
+ function formatContextStat(stat) {
7830
+ const pctColor = stat.fillPct >= 80 ? chalk25.red : stat.fillPct >= 50 ? chalk25.yellow : chalk25.cyan;
7831
+ const k = (n) => `${Math.round(n / 1e3)}k`;
7832
+ const modelShort = stat.model.replace(/@.*$/, "").replace(/-\d{8}$/, "").replace(/^claude-/, "");
7833
+ return chalk25.dim("ctx: ") + pctColor(`${stat.fillPct}%`) + chalk25.dim(
7834
+ ` (${k(stat.inputTokens)}/${k(getModelContextLimit(stat.model))} out ${k(stat.outputTokens)} \xB7 ${modelShort})`
7835
+ );
7836
+ }
7510
7837
  function visibleLength(s) {
7511
7838
  return s.replace(/\x1B\[[0-9;]*m/g, "").length;
7512
7839
  }
@@ -7516,26 +7843,31 @@ function wrappedLineCount(text) {
7516
7843
  const len = visibleLength(text);
7517
7844
  return Math.max(1, Math.ceil(len / cols));
7518
7845
  }
7846
+ function agentLabel(agent) {
7847
+ if (!agent || agent === "Terminal") return "";
7848
+ const short = agent === "Claude Code" ? "Claude" : agent === "Gemini CLI" ? "Gemini" : agent === "Unknown Agent" ? "" : agent.split(" ")[0];
7849
+ return short ? chalk25.dim(`[${short}] `) : "";
7850
+ }
7519
7851
  function formatBase(activity) {
7520
7852
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
7521
7853
  const icon = getIcon(activity.tool);
7522
7854
  const toolName = activity.tool.slice(0, 16).padEnd(16);
7523
- const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os29.homedir(), "~");
7855
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os33.homedir(), "~");
7524
7856
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
7525
- return `${chalk23.gray(time)} ${icon} ${chalk23.white.bold(toolName)} ${chalk23.dim(argsPreview)}`;
7857
+ return `${chalk25.gray(time)} ${icon} ${agentLabel(activity.agent)}${chalk25.white.bold(toolName)} ${chalk25.dim(argsPreview)}`;
7526
7858
  }
7527
7859
  function renderResult(activity, result) {
7528
7860
  const base = formatBase(activity);
7529
7861
  let status;
7530
7862
  if (result.status === "allow") {
7531
- status = chalk23.green("\u2713 ALLOW");
7863
+ status = chalk25.green("\u2713 ALLOW");
7532
7864
  } else if (result.status === "dlp") {
7533
- status = chalk23.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
7865
+ status = chalk25.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
7534
7866
  } else {
7535
- status = chalk23.red("\u2717 BLOCK");
7867
+ status = chalk25.red("\u2717 BLOCK");
7536
7868
  }
7537
7869
  const cost = result.costEstimate ?? activity.costEstimate;
7538
- const costSuffix = cost == null ? "" : chalk23.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
7870
+ const costSuffix = cost == null ? "" : chalk25.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
7539
7871
  if (process.stdout.isTTY) {
7540
7872
  if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
7541
7873
  readline5.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
@@ -7552,19 +7884,19 @@ function renderResult(activity, result) {
7552
7884
  }
7553
7885
  function renderPending(activity) {
7554
7886
  if (!process.stdout.isTTY) return;
7555
- const line = `${formatBase(activity)} ${chalk23.yellow("\u25CF \u2026")}`;
7887
+ const line = `${formatBase(activity)} ${chalk25.yellow("\u25CF \u2026")}`;
7556
7888
  pendingShownForId = activity.id;
7557
7889
  pendingWrappedLines = wrappedLineCount(line);
7558
7890
  process.stdout.write(`${line}\r`);
7559
7891
  }
7560
7892
  async function ensureDaemon() {
7561
7893
  let pidPort = null;
7562
- if (fs33.existsSync(PID_FILE)) {
7894
+ if (fs37.existsSync(PID_FILE)) {
7563
7895
  try {
7564
- const { port } = JSON.parse(fs33.readFileSync(PID_FILE, "utf-8"));
7896
+ const { port } = JSON.parse(fs37.readFileSync(PID_FILE, "utf-8"));
7565
7897
  pidPort = port;
7566
7898
  } catch {
7567
- console.error(chalk23.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
7899
+ console.error(chalk25.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
7568
7900
  }
7569
7901
  }
7570
7902
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -7575,7 +7907,7 @@ async function ensureDaemon() {
7575
7907
  if (res.ok) return checkPort;
7576
7908
  } catch {
7577
7909
  }
7578
- console.log(chalk23.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
7910
+ console.log(chalk25.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
7579
7911
  const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
7580
7912
  detached: true,
7581
7913
  stdio: "ignore",
@@ -7592,7 +7924,7 @@ async function ensureDaemon() {
7592
7924
  } catch {
7593
7925
  }
7594
7926
  }
7595
- console.error(chalk23.red("\u274C Daemon failed to start. Try: node9 daemon start"));
7927
+ console.error(chalk25.red("\u274C Daemon failed to start. Try: node9 daemon start"));
7596
7928
  process.exit(1);
7597
7929
  }
7598
7930
  function postDecisionHttp(id, decision, csrfToken, port, opts) {
@@ -7658,10 +7990,11 @@ function buildCardLines(req, localCount = 0) {
7658
7990
  const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
7659
7991
  const rawDesc = req.riskMetadata?.ruleDescription ?? "";
7660
7992
  const description = rawDesc ? cleanReason(rawDesc) : "";
7993
+ const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${chalk25.dim(`(${req.agent})`)}` : "";
7661
7994
  const lines = [
7662
7995
  ``,
7663
7996
  `${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
7664
- `${CYAN}\u2551${RESET2} Tool: ${BOLD2}${req.toolName}${RESET2}`,
7997
+ `${CYAN}\u2551${RESET2} Tool: ${BOLD2}${req.toolName}${RESET2}${agentSuffix}`,
7665
7998
  `${CYAN}\u2551${RESET2} Policy: ${severityIcon} ${blockedBy}${RESET2}`
7666
7999
  ];
7667
8000
  if (description) {
@@ -7713,9 +8046,9 @@ function buildRecoveryCardLines(req) {
7713
8046
  ];
7714
8047
  }
7715
8048
  function readApproversFromDisk() {
7716
- const configPath = path36.join(os29.homedir(), ".node9", "config.json");
8049
+ const configPath = path40.join(os33.homedir(), ".node9", "config.json");
7717
8050
  try {
7718
- const raw = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
8051
+ const raw = JSON.parse(fs37.readFileSync(configPath, "utf-8"));
7719
8052
  const settings = raw.settings ?? {};
7720
8053
  return settings.approvers ?? {};
7721
8054
  } catch {
@@ -7726,20 +8059,20 @@ function approverStatusLine() {
7726
8059
  const a = readApproversFromDisk();
7727
8060
  const fmt = (label, key) => {
7728
8061
  const on = a[key] !== false;
7729
- return `[${key[0]}]${label.slice(1)} ${on ? chalk23.green("\u2713") : chalk23.dim("\u2717")}`;
8062
+ return `[${key[0]}]${label.slice(1)} ${on ? chalk25.green("\u2713") : chalk25.dim("\u2717")}`;
7730
8063
  };
7731
8064
  return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
7732
8065
  }
7733
8066
  function toggleApprover(channel) {
7734
- const configPath = path36.join(os29.homedir(), ".node9", "config.json");
8067
+ const configPath = path40.join(os33.homedir(), ".node9", "config.json");
7735
8068
  try {
7736
- const raw = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
8069
+ const raw = JSON.parse(fs37.readFileSync(configPath, "utf-8"));
7737
8070
  const settings = raw.settings ?? {};
7738
8071
  const approvers = settings.approvers ?? {};
7739
8072
  approvers[channel] = approvers[channel] === false;
7740
8073
  settings.approvers = approvers;
7741
8074
  raw.settings = settings;
7742
- fs33.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
8075
+ fs37.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
7743
8076
  } catch (err2) {
7744
8077
  process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
7745
8078
  `);
@@ -7771,7 +8104,7 @@ async function startTail(options = {}) {
7771
8104
  req2.end();
7772
8105
  });
7773
8106
  if (result.ok) {
7774
- console.log(chalk23.green("\u2713 Flight Recorder buffer cleared."));
8107
+ console.log(chalk25.green("\u2713 Flight Recorder buffer cleared."));
7775
8108
  } else if (result.code === "ECONNREFUSED") {
7776
8109
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
7777
8110
  } else if (result.code === "ETIMEDOUT") {
@@ -7815,7 +8148,7 @@ async function startTail(options = {}) {
7815
8148
  const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
7816
8149
  if (channel) {
7817
8150
  toggleApprover(channel);
7818
- console.log(chalk23.dim(` Approvers: ${approverStatusLine()}`));
8151
+ console.log(chalk25.dim(` Approvers: ${approverStatusLine()}`));
7819
8152
  }
7820
8153
  };
7821
8154
  process.stdin.on("keypress", idleKeypressHandler);
@@ -7881,7 +8214,7 @@ async function startTail(options = {}) {
7881
8214
  localAllowCounts.get(req2.toolName) ?? 0
7882
8215
  )
7883
8216
  );
7884
- const decisionStamp = action === "always-allow" ? chalk23.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk23.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk23.green("\u2713 ALLOWED") : action === "redirect" ? chalk23.yellow("\u21A9 REDIRECT AI") : chalk23.red("\u2717 DENIED");
8217
+ const decisionStamp = action === "always-allow" ? chalk25.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk25.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk25.green("\u2713 ALLOWED") : action === "redirect" ? chalk25.yellow("\u21A9 REDIRECT AI") : chalk25.red("\u2717 DENIED");
7885
8218
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
7886
8219
  for (const line of stampedLines) process.stdout.write(line + "\n");
7887
8220
  process.stdout.write(SHOW_CURSOR);
@@ -7909,8 +8242,8 @@ async function startTail(options = {}) {
7909
8242
  }
7910
8243
  postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
7911
8244
  try {
7912
- fs33.appendFileSync(
7913
- path36.join(os29.homedir(), ".node9", "hook-debug.log"),
8245
+ fs37.appendFileSync(
8246
+ path40.join(os33.homedir(), ".node9", "hook-debug.log"),
7914
8247
  `[tail] POST /decision failed: ${String(err2)}
7915
8248
  `
7916
8249
  );
@@ -7932,7 +8265,7 @@ async function startTail(options = {}) {
7932
8265
  );
7933
8266
  const stampedLines = buildCardLines(req2, priorCount);
7934
8267
  if (externalDecision) {
7935
- const source = externalDecision === "allow" ? chalk23.green("\u2713 ALLOWED") : chalk23.red("\u2717 DENIED");
8268
+ const source = externalDecision === "allow" ? chalk25.green("\u2713 ALLOWED") : chalk25.red("\u2717 DENIED");
7936
8269
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
7937
8270
  }
7938
8271
  for (const line of stampedLines) process.stdout.write(line + "\n");
@@ -7991,16 +8324,31 @@ async function startTail(options = {}) {
7991
8324
  }
7992
8325
  } catch {
7993
8326
  }
7994
- console.log(chalk23.cyan.bold(`
7995
- \u{1F6F0}\uFE0F Node9 tail `) + chalk23.dim(`\u2192 ${dashboardUrl}`));
8327
+ const auditLog = path40.join(os33.homedir(), ".node9", "audit.log");
8328
+ try {
8329
+ const unackedDlp = fs37.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
8330
+ if (unackedDlp > 0) {
8331
+ console.log("");
8332
+ console.log(
8333
+ chalk25.bgRed.white.bold(
8334
+ ` \u26A0\uFE0F DLP ALERT: ${unackedDlp} secret${unackedDlp !== 1 ? "s" : ""} found in Claude response text \u2014 run: node9 dlp `
8335
+ )
8336
+ );
8337
+ }
8338
+ } catch {
8339
+ }
8340
+ console.log(chalk25.cyan.bold(`
8341
+ \u{1F6F0}\uFE0F Node9 tail `) + chalk25.dim(`\u2192 ${dashboardUrl}`));
7996
8342
  if (canApprove) {
7997
- console.log(chalk23.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
7998
- console.log(chalk23.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
8343
+ console.log(chalk25.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
8344
+ console.log(chalk25.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
7999
8345
  }
8346
+ const ctxStat = readSessionUsage();
8347
+ if (ctxStat) console.log(" " + formatContextStat(ctxStat));
8000
8348
  if (options.history) {
8001
- console.log(chalk23.dim("Showing history + live events.\n"));
8349
+ console.log(chalk25.dim("Showing history + live events.\n"));
8002
8350
  } else {
8003
- console.log(chalk23.dim("Showing live events only. Use --history to include past.\n"));
8351
+ console.log(chalk25.dim("Showing live events only. Use --history to include past.\n"));
8004
8352
  }
8005
8353
  process.on("SIGINT", () => {
8006
8354
  exitIdleMode();
@@ -8010,13 +8358,13 @@ async function startTail(options = {}) {
8010
8358
  readline5.clearLine(process.stdout, 0);
8011
8359
  readline5.cursorTo(process.stdout, 0);
8012
8360
  }
8013
- console.log(chalk23.dim("\n\u{1F6F0}\uFE0F Disconnected."));
8361
+ console.log(chalk25.dim("\n\u{1F6F0}\uFE0F Disconnected."));
8014
8362
  process.exit(0);
8015
8363
  });
8016
8364
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
8017
8365
  const req = http2.get(sseUrl, (res) => {
8018
8366
  if (res.statusCode !== 200) {
8019
- console.error(chalk23.red(`Failed to connect: HTTP ${res.statusCode}`));
8367
+ console.error(chalk25.red(`Failed to connect: HTTP ${res.statusCode}`));
8020
8368
  process.exit(1);
8021
8369
  }
8022
8370
  if (canApprove) enterIdleMode();
@@ -8047,7 +8395,7 @@ async function startTail(options = {}) {
8047
8395
  readline5.clearLine(process.stdout, 0);
8048
8396
  readline5.cursorTo(process.stdout, 0);
8049
8397
  }
8050
- console.log(chalk23.red("\n\u274C Daemon disconnected."));
8398
+ console.log(chalk25.red("\n\u274C Daemon disconnected."));
8051
8399
  process.exit(1);
8052
8400
  });
8053
8401
  });
@@ -8139,9 +8487,9 @@ async function startTail(options = {}) {
8139
8487
  const hash = data.hash ?? "";
8140
8488
  const summary = data.argsSummary ?? data.tool;
8141
8489
  const fileCount = data.fileCount ?? 0;
8142
- const files = fileCount > 0 ? chalk23.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
8490
+ const files = fileCount > 0 ? chalk25.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
8143
8491
  process.stdout.write(
8144
- `${chalk23.dim(time)} ${chalk23.cyan("\u{1F4F8} snapshot")} ${chalk23.dim(hash)} ${summary}${files}
8492
+ `${chalk25.dim(time)} ${chalk25.cyan("\u{1F4F8} snapshot")} ${chalk25.dim(hash)} ${summary}${files}
8145
8493
  `
8146
8494
  );
8147
8495
  return;
@@ -8158,19 +8506,19 @@ async function startTail(options = {}) {
8158
8506
  }
8159
8507
  req.on("error", (err2) => {
8160
8508
  const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
8161
- console.error(chalk23.red(`
8509
+ console.error(chalk25.red(`
8162
8510
  \u274C ${msg}`));
8163
8511
  process.exit(1);
8164
8512
  });
8165
8513
  }
8166
- var PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
8514
+ var PID_FILE, ICONS, MODEL_CONTEXT_LIMITS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
8167
8515
  var init_tail = __esm({
8168
8516
  "src/tui/tail.ts"() {
8169
8517
  "use strict";
8170
8518
  init_daemon2();
8171
8519
  init_daemon();
8172
8520
  init_core();
8173
- PID_FILE = path36.join(os29.homedir(), ".node9", "daemon.pid");
8521
+ PID_FILE = path40.join(os33.homedir(), ".node9", "daemon.pid");
8174
8522
  ICONS = {
8175
8523
  bash: "\u{1F4BB}",
8176
8524
  shell: "\u{1F4BB}",
@@ -8188,6 +8536,13 @@ var init_tail = __esm({
8188
8536
  delete: "\u{1F5D1}\uFE0F",
8189
8537
  web: "\u{1F310}"
8190
8538
  };
8539
+ MODEL_CONTEXT_LIMITS = {
8540
+ "claude-opus-4": 2e5,
8541
+ "claude-sonnet-4": 2e5,
8542
+ "claude-haiku-4": 2e5,
8543
+ "claude-3-7": 2e5,
8544
+ "claude-3-5": 2e5
8545
+ };
8191
8546
  RESET2 = "\x1B[0m";
8192
8547
  BOLD2 = "\x1B[1m";
8193
8548
  RED = "\x1B[31m";
@@ -8211,9 +8566,9 @@ __export(hud_exports, {
8211
8566
  main: () => main,
8212
8567
  renderEnvironmentLine: () => renderEnvironmentLine
8213
8568
  });
8214
- import fs34 from "fs";
8215
- import path37 from "path";
8216
- import os30 from "os";
8569
+ import fs38 from "fs";
8570
+ import path41 from "path";
8571
+ import os34 from "os";
8217
8572
  import http3 from "http";
8218
8573
  async function readStdin() {
8219
8574
  const chunks = [];
@@ -8289,9 +8644,9 @@ function formatTimeLeft(resetsAt) {
8289
8644
  return ` (${m}m left)`;
8290
8645
  }
8291
8646
  function safeReadJson(filePath) {
8292
- if (!fs34.existsSync(filePath)) return null;
8647
+ if (!fs38.existsSync(filePath)) return null;
8293
8648
  try {
8294
- return JSON.parse(fs34.readFileSync(filePath, "utf-8"));
8649
+ return JSON.parse(fs38.readFileSync(filePath, "utf-8"));
8295
8650
  } catch {
8296
8651
  return null;
8297
8652
  }
@@ -8312,12 +8667,12 @@ function countHooksInFile(filePath) {
8312
8667
  return Object.keys(cfg.hooks).length;
8313
8668
  }
8314
8669
  function countRulesInDir(rulesDir) {
8315
- if (!fs34.existsSync(rulesDir)) return 0;
8670
+ if (!fs38.existsSync(rulesDir)) return 0;
8316
8671
  let count = 0;
8317
8672
  try {
8318
- for (const entry of fs34.readdirSync(rulesDir, { withFileTypes: true })) {
8673
+ for (const entry of fs38.readdirSync(rulesDir, { withFileTypes: true })) {
8319
8674
  if (entry.isDirectory()) {
8320
- count += countRulesInDir(path37.join(rulesDir, entry.name));
8675
+ count += countRulesInDir(path41.join(rulesDir, entry.name));
8321
8676
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
8322
8677
  count++;
8323
8678
  }
@@ -8328,46 +8683,46 @@ function countRulesInDir(rulesDir) {
8328
8683
  }
8329
8684
  function isSamePath(a, b) {
8330
8685
  try {
8331
- return path37.resolve(a) === path37.resolve(b);
8686
+ return path41.resolve(a) === path41.resolve(b);
8332
8687
  } catch {
8333
8688
  return false;
8334
8689
  }
8335
8690
  }
8336
8691
  function countConfigs(cwd) {
8337
- const homeDir2 = os30.homedir();
8338
- const claudeDir = path37.join(homeDir2, ".claude");
8692
+ const homeDir2 = os34.homedir();
8693
+ const claudeDir = path41.join(homeDir2, ".claude");
8339
8694
  let claudeMdCount = 0;
8340
8695
  let rulesCount = 0;
8341
8696
  let hooksCount = 0;
8342
8697
  const userMcpServers = /* @__PURE__ */ new Set();
8343
8698
  const projectMcpServers = /* @__PURE__ */ new Set();
8344
- if (fs34.existsSync(path37.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
8345
- rulesCount += countRulesInDir(path37.join(claudeDir, "rules"));
8346
- const userSettings = path37.join(claudeDir, "settings.json");
8699
+ if (fs38.existsSync(path41.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
8700
+ rulesCount += countRulesInDir(path41.join(claudeDir, "rules"));
8701
+ const userSettings = path41.join(claudeDir, "settings.json");
8347
8702
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
8348
8703
  hooksCount += countHooksInFile(userSettings);
8349
- const userClaudeJson = path37.join(homeDir2, ".claude.json");
8704
+ const userClaudeJson = path41.join(homeDir2, ".claude.json");
8350
8705
  for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
8351
8706
  for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
8352
8707
  userMcpServers.delete(name);
8353
8708
  }
8354
8709
  if (cwd) {
8355
- if (fs34.existsSync(path37.join(cwd, "CLAUDE.md"))) claudeMdCount++;
8356
- if (fs34.existsSync(path37.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
8357
- const projectClaudeDir = path37.join(cwd, ".claude");
8710
+ if (fs38.existsSync(path41.join(cwd, "CLAUDE.md"))) claudeMdCount++;
8711
+ if (fs38.existsSync(path41.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
8712
+ const projectClaudeDir = path41.join(cwd, ".claude");
8358
8713
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
8359
8714
  if (!overlapsUserScope) {
8360
- if (fs34.existsSync(path37.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
8361
- rulesCount += countRulesInDir(path37.join(projectClaudeDir, "rules"));
8362
- const projSettings = path37.join(projectClaudeDir, "settings.json");
8715
+ if (fs38.existsSync(path41.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
8716
+ rulesCount += countRulesInDir(path41.join(projectClaudeDir, "rules"));
8717
+ const projSettings = path41.join(projectClaudeDir, "settings.json");
8363
8718
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
8364
8719
  hooksCount += countHooksInFile(projSettings);
8365
8720
  }
8366
- if (fs34.existsSync(path37.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
8367
- const localSettings = path37.join(projectClaudeDir, "settings.local.json");
8721
+ if (fs38.existsSync(path41.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
8722
+ const localSettings = path41.join(projectClaudeDir, "settings.local.json");
8368
8723
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
8369
8724
  hooksCount += countHooksInFile(localSettings);
8370
- const mcpJsonServers = getMcpServerNames(path37.join(cwd, ".mcp.json"));
8725
+ const mcpJsonServers = getMcpServerNames(path41.join(cwd, ".mcp.json"));
8371
8726
  const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
8372
8727
  for (const name of disabledMcpJson) mcpJsonServers.delete(name);
8373
8728
  for (const name of mcpJsonServers) projectMcpServers.add(name);
@@ -8400,12 +8755,12 @@ function readActiveShieldsHud() {
8400
8755
  return shieldsCache.value;
8401
8756
  }
8402
8757
  try {
8403
- const shieldsPath = path37.join(os30.homedir(), ".node9", "shields.json");
8404
- if (!fs34.existsSync(shieldsPath)) {
8758
+ const shieldsPath = path41.join(os34.homedir(), ".node9", "shields.json");
8759
+ if (!fs38.existsSync(shieldsPath)) {
8405
8760
  shieldsCache = { value: [], ts: now };
8406
8761
  return [];
8407
8762
  }
8408
- const parsed = JSON.parse(fs34.readFileSync(shieldsPath, "utf-8"));
8763
+ const parsed = JSON.parse(fs38.readFileSync(shieldsPath, "utf-8"));
8409
8764
  if (!Array.isArray(parsed.active)) {
8410
8765
  shieldsCache = { value: [], ts: now };
8411
8766
  return [];
@@ -8507,17 +8862,17 @@ function renderContextLine(stdin) {
8507
8862
  async function main() {
8508
8863
  try {
8509
8864
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
8510
- if (fs34.existsSync(path37.join(os30.homedir(), ".node9", "hud-debug"))) {
8865
+ if (fs38.existsSync(path41.join(os34.homedir(), ".node9", "hud-debug"))) {
8511
8866
  try {
8512
- const logPath = path37.join(os30.homedir(), ".node9", "hud-debug.log");
8867
+ const logPath = path41.join(os34.homedir(), ".node9", "hud-debug.log");
8513
8868
  const MAX_LOG_SIZE = 10 * 1024 * 1024;
8514
8869
  let size = 0;
8515
8870
  try {
8516
- size = fs34.statSync(logPath).size;
8871
+ size = fs38.statSync(logPath).size;
8517
8872
  } catch {
8518
8873
  }
8519
8874
  if (size < MAX_LOG_SIZE) {
8520
- fs34.appendFileSync(
8875
+ fs38.appendFileSync(
8521
8876
  logPath,
8522
8877
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
8523
8878
  );
@@ -8538,11 +8893,11 @@ async function main() {
8538
8893
  try {
8539
8894
  const cwd = stdin.cwd ?? process.cwd();
8540
8895
  for (const configPath of [
8541
- path37.join(cwd, "node9.config.json"),
8542
- path37.join(os30.homedir(), ".node9", "config.json")
8896
+ path41.join(cwd, "node9.config.json"),
8897
+ path41.join(os34.homedir(), ".node9", "config.json")
8543
8898
  ]) {
8544
- if (!fs34.existsSync(configPath)) continue;
8545
- const cfg = JSON.parse(fs34.readFileSync(configPath, "utf-8"));
8899
+ if (!fs38.existsSync(configPath)) continue;
8900
+ const cfg = JSON.parse(fs38.readFileSync(configPath, "utf-8"));
8546
8901
  const hud = cfg.settings?.hud;
8547
8902
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
8548
8903
  }
@@ -9477,10 +9832,10 @@ function getAgentsStatus(homeDir2 = os11.homedir()) {
9477
9832
 
9478
9833
  // src/cli.ts
9479
9834
  init_daemon2();
9480
- import chalk24 from "chalk";
9481
- import fs35 from "fs";
9482
- import path38 from "path";
9483
- import os31 from "os";
9835
+ import chalk26 from "chalk";
9836
+ import fs39 from "fs";
9837
+ import path42 from "path";
9838
+ import os35 from "os";
9484
9839
  import { confirm as confirm2 } from "@inquirer/prompts";
9485
9840
 
9486
9841
  // src/utils/duration.ts
@@ -9709,19 +10064,19 @@ init_daemon();
9709
10064
  init_config();
9710
10065
  init_policy();
9711
10066
  import chalk5 from "chalk";
9712
- import fs22 from "fs";
10067
+ import fs24 from "fs";
9713
10068
  import { spawn as spawn6 } from "child_process";
9714
- import path24 from "path";
9715
- import os18 from "os";
10069
+ import path26 from "path";
10070
+ import os20 from "os";
9716
10071
 
9717
10072
  // src/undo.ts
9718
10073
  import { spawnSync as spawnSync5, spawn as spawn5 } from "child_process";
9719
10074
  import crypto3 from "crypto";
9720
- import fs21 from "fs";
10075
+ import fs22 from "fs";
9721
10076
  import net3 from "net";
9722
- import path23 from "path";
9723
- import os17 from "os";
9724
- var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path23.join(os17.tmpdir(), "node9-activity.sock");
10077
+ import path24 from "path";
10078
+ import os18 from "os";
10079
+ var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path24.join(os18.tmpdir(), "node9-activity.sock");
9725
10080
  function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
9726
10081
  try {
9727
10082
  const payload = JSON.stringify({
@@ -9741,22 +10096,22 @@ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
9741
10096
  } catch {
9742
10097
  }
9743
10098
  }
9744
- var SNAPSHOT_STACK_PATH = path23.join(os17.homedir(), ".node9", "snapshots.json");
9745
- var UNDO_LATEST_PATH = path23.join(os17.homedir(), ".node9", "undo_latest.txt");
10099
+ var SNAPSHOT_STACK_PATH = path24.join(os18.homedir(), ".node9", "snapshots.json");
10100
+ var UNDO_LATEST_PATH = path24.join(os18.homedir(), ".node9", "undo_latest.txt");
9746
10101
  var MAX_SNAPSHOTS = 10;
9747
10102
  var GIT_TIMEOUT = 15e3;
9748
10103
  function readStack() {
9749
10104
  try {
9750
- if (fs21.existsSync(SNAPSHOT_STACK_PATH))
9751
- return JSON.parse(fs21.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
10105
+ if (fs22.existsSync(SNAPSHOT_STACK_PATH))
10106
+ return JSON.parse(fs22.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
9752
10107
  } catch {
9753
10108
  }
9754
10109
  return [];
9755
10110
  }
9756
10111
  function writeStack(stack) {
9757
- const dir = path23.dirname(SNAPSHOT_STACK_PATH);
9758
- if (!fs21.existsSync(dir)) fs21.mkdirSync(dir, { recursive: true });
9759
- fs21.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
10112
+ const dir = path24.dirname(SNAPSHOT_STACK_PATH);
10113
+ if (!fs22.existsSync(dir)) fs22.mkdirSync(dir, { recursive: true });
10114
+ fs22.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
9760
10115
  }
9761
10116
  function extractFilePath(args) {
9762
10117
  if (!args || typeof args !== "object") return null;
@@ -9776,12 +10131,12 @@ function buildArgsSummary(tool, args) {
9776
10131
  return "";
9777
10132
  }
9778
10133
  function findProjectRoot(filePath) {
9779
- let dir = path23.dirname(filePath);
10134
+ let dir = path24.dirname(filePath);
9780
10135
  while (true) {
9781
- if (fs21.existsSync(path23.join(dir, ".git")) || fs21.existsSync(path23.join(dir, "package.json"))) {
10136
+ if (fs22.existsSync(path24.join(dir, ".git")) || fs22.existsSync(path24.join(dir, "package.json"))) {
9782
10137
  return dir;
9783
10138
  }
9784
- const parent = path23.dirname(dir);
10139
+ const parent = path24.dirname(dir);
9785
10140
  if (parent === dir) return process.cwd();
9786
10141
  dir = parent;
9787
10142
  }
@@ -9789,7 +10144,7 @@ function findProjectRoot(filePath) {
9789
10144
  function normalizeCwdForHash(cwd) {
9790
10145
  let normalized;
9791
10146
  try {
9792
- normalized = fs21.realpathSync(cwd);
10147
+ normalized = fs22.realpathSync(cwd);
9793
10148
  } catch {
9794
10149
  normalized = cwd;
9795
10150
  }
@@ -9799,16 +10154,16 @@ function normalizeCwdForHash(cwd) {
9799
10154
  }
9800
10155
  function getShadowRepoDir(cwd) {
9801
10156
  const hash = crypto3.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
9802
- return path23.join(os17.homedir(), ".node9", "snapshots", hash);
10157
+ return path24.join(os18.homedir(), ".node9", "snapshots", hash);
9803
10158
  }
9804
10159
  function cleanOrphanedIndexFiles(shadowDir) {
9805
10160
  try {
9806
10161
  const cutoff = Date.now() - 6e4;
9807
- for (const f of fs21.readdirSync(shadowDir)) {
10162
+ for (const f of fs22.readdirSync(shadowDir)) {
9808
10163
  if (f.startsWith("index_")) {
9809
- const fp = path23.join(shadowDir, f);
10164
+ const fp = path24.join(shadowDir, f);
9810
10165
  try {
9811
- if (fs21.statSync(fp).mtimeMs < cutoff) fs21.unlinkSync(fp);
10166
+ if (fs22.statSync(fp).mtimeMs < cutoff) fs22.unlinkSync(fp);
9812
10167
  } catch {
9813
10168
  }
9814
10169
  }
@@ -9820,7 +10175,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
9820
10175
  const hardcoded = [".git", ".node9"];
9821
10176
  const lines = [...hardcoded, ...ignorePaths].join("\n");
9822
10177
  try {
9823
- fs21.writeFileSync(path23.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
10178
+ fs22.writeFileSync(path24.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
9824
10179
  } catch {
9825
10180
  }
9826
10181
  }
@@ -9833,25 +10188,25 @@ function ensureShadowRepo(shadowDir, cwd) {
9833
10188
  timeout: 3e3
9834
10189
  });
9835
10190
  if (check.status === 0) {
9836
- const ptPath = path23.join(shadowDir, "project-path.txt");
10191
+ const ptPath = path24.join(shadowDir, "project-path.txt");
9837
10192
  try {
9838
- const stored = fs21.readFileSync(ptPath, "utf8").trim();
10193
+ const stored = fs22.readFileSync(ptPath, "utf8").trim();
9839
10194
  if (stored === normalizedCwd) return true;
9840
10195
  if (process.env.NODE9_DEBUG === "1")
9841
10196
  console.error(
9842
10197
  `[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
9843
10198
  );
9844
- fs21.rmSync(shadowDir, { recursive: true, force: true });
10199
+ fs22.rmSync(shadowDir, { recursive: true, force: true });
9845
10200
  } catch {
9846
10201
  try {
9847
- fs21.writeFileSync(ptPath, normalizedCwd, "utf8");
10202
+ fs22.writeFileSync(ptPath, normalizedCwd, "utf8");
9848
10203
  } catch {
9849
10204
  }
9850
10205
  return true;
9851
10206
  }
9852
10207
  }
9853
10208
  try {
9854
- fs21.mkdirSync(shadowDir, { recursive: true });
10209
+ fs22.mkdirSync(shadowDir, { recursive: true });
9855
10210
  } catch {
9856
10211
  }
9857
10212
  const init = spawnSync5("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
@@ -9860,7 +10215,7 @@ function ensureShadowRepo(shadowDir, cwd) {
9860
10215
  if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
9861
10216
  return false;
9862
10217
  }
9863
- const configFile = path23.join(shadowDir, "config");
10218
+ const configFile = path24.join(shadowDir, "config");
9864
10219
  spawnSync5("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
9865
10220
  timeout: 3e3
9866
10221
  });
@@ -9868,7 +10223,7 @@ function ensureShadowRepo(shadowDir, cwd) {
9868
10223
  timeout: 3e3
9869
10224
  });
9870
10225
  try {
9871
- fs21.writeFileSync(path23.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
10226
+ fs22.writeFileSync(path24.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
9872
10227
  } catch {
9873
10228
  }
9874
10229
  return true;
@@ -9888,12 +10243,12 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9888
10243
  let indexFile = null;
9889
10244
  try {
9890
10245
  const rawFilePath = extractFilePath(args);
9891
- const absFilePath = rawFilePath && path23.isAbsolute(rawFilePath) ? rawFilePath : null;
10246
+ const absFilePath = rawFilePath && path24.isAbsolute(rawFilePath) ? rawFilePath : null;
9892
10247
  const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
9893
10248
  const shadowDir = getShadowRepoDir(cwd);
9894
10249
  if (!ensureShadowRepo(shadowDir, cwd)) return null;
9895
10250
  writeShadowExcludes(shadowDir, ignorePaths);
9896
- indexFile = path23.join(shadowDir, `index_${process.pid}_${Date.now()}`);
10251
+ indexFile = path24.join(shadowDir, `index_${process.pid}_${Date.now()}`);
9897
10252
  const shadowEnv = {
9898
10253
  ...process.env,
9899
10254
  GIT_DIR: shadowDir,
@@ -9965,7 +10320,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9965
10320
  writeStack(stack);
9966
10321
  const entry = stack[stack.length - 1];
9967
10322
  notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
9968
- fs21.writeFileSync(UNDO_LATEST_PATH, commitHash);
10323
+ fs22.writeFileSync(UNDO_LATEST_PATH, commitHash);
9969
10324
  if (shouldGc) {
9970
10325
  spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
9971
10326
  }
@@ -9976,7 +10331,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9976
10331
  } finally {
9977
10332
  if (indexFile) {
9978
10333
  try {
9979
- fs21.unlinkSync(indexFile);
10334
+ fs22.unlinkSync(indexFile);
9980
10335
  } catch {
9981
10336
  }
9982
10337
  }
@@ -10052,9 +10407,9 @@ function applyUndo(hash, cwd) {
10052
10407
  timeout: GIT_TIMEOUT
10053
10408
  }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
10054
10409
  for (const file of [...tracked, ...untracked]) {
10055
- const fullPath = path23.join(dir, file);
10056
- if (!snapshotFiles.has(file) && fs21.existsSync(fullPath)) {
10057
- fs21.unlinkSync(fullPath);
10410
+ const fullPath = path24.join(dir, file);
10411
+ if (!snapshotFiles.has(file) && fs22.existsSync(fullPath)) {
10412
+ fs22.unlinkSync(fullPath);
10058
10413
  }
10059
10414
  }
10060
10415
  return true;
@@ -10063,6 +10418,187 @@ function applyUndo(hash, cwd) {
10063
10418
  }
10064
10419
  }
10065
10420
 
10421
+ // src/skill-pin.ts
10422
+ import fs23 from "fs";
10423
+ import path25 from "path";
10424
+ import os19 from "os";
10425
+ import crypto4 from "crypto";
10426
+ function getPinsFilePath() {
10427
+ return path25.join(os19.homedir(), ".node9", "skill-pins.json");
10428
+ }
10429
+ var MAX_FILES = 5e3;
10430
+ var MAX_TOTAL_BYTES = 50 * 1024 * 1024;
10431
+ function sha256Bytes(buf) {
10432
+ return crypto4.createHash("sha256").update(buf).digest("hex");
10433
+ }
10434
+ function walkDir(root) {
10435
+ const out = [];
10436
+ let totalBytes = 0;
10437
+ const visit = (dir, relDir) => {
10438
+ if (out.length >= MAX_FILES) return;
10439
+ let entries;
10440
+ try {
10441
+ entries = fs23.readdirSync(dir, { withFileTypes: true });
10442
+ } catch {
10443
+ return;
10444
+ }
10445
+ entries.sort((a, b) => a.name.localeCompare(b.name));
10446
+ for (const entry of entries) {
10447
+ if (out.length >= MAX_FILES) return;
10448
+ const full = path25.join(dir, entry.name);
10449
+ const rel = relDir ? path25.posix.join(relDir, entry.name) : entry.name;
10450
+ let lst;
10451
+ try {
10452
+ lst = fs23.lstatSync(full);
10453
+ } catch {
10454
+ continue;
10455
+ }
10456
+ if (lst.isSymbolicLink()) continue;
10457
+ if (lst.isDirectory()) {
10458
+ visit(full, rel);
10459
+ continue;
10460
+ }
10461
+ if (!lst.isFile()) continue;
10462
+ if (totalBytes + lst.size > MAX_TOTAL_BYTES) continue;
10463
+ try {
10464
+ const buf = fs23.readFileSync(full);
10465
+ totalBytes += buf.length;
10466
+ out.push({ rel, hash: sha256Bytes(buf) });
10467
+ } catch {
10468
+ }
10469
+ }
10470
+ };
10471
+ visit(root, "");
10472
+ out.sort((a, b) => a.rel.localeCompare(b.rel));
10473
+ return out.map((e) => `${e.rel}\0${e.hash}`);
10474
+ }
10475
+ function hashSkillRoot(absPath) {
10476
+ let lst;
10477
+ try {
10478
+ lst = fs23.lstatSync(absPath);
10479
+ } catch {
10480
+ return { exists: false, contentHash: "", fileCount: 0 };
10481
+ }
10482
+ if (lst.isSymbolicLink()) return { exists: false, contentHash: "", fileCount: 0 };
10483
+ if (lst.isFile()) {
10484
+ try {
10485
+ return { exists: true, contentHash: sha256Bytes(fs23.readFileSync(absPath)), fileCount: 1 };
10486
+ } catch {
10487
+ return { exists: false, contentHash: "", fileCount: 0 };
10488
+ }
10489
+ }
10490
+ if (lst.isDirectory()) {
10491
+ const entries = walkDir(absPath);
10492
+ const contentHash = crypto4.createHash("sha256").update(entries.join("\n")).digest("hex");
10493
+ return { exists: true, contentHash, fileCount: entries.length };
10494
+ }
10495
+ return { exists: false, contentHash: "", fileCount: 0 };
10496
+ }
10497
+ function getRootKey(absPath) {
10498
+ return crypto4.createHash("sha256").update(absPath).digest("hex").slice(0, 16);
10499
+ }
10500
+ function readSkillPinsSafe() {
10501
+ const filePath = getPinsFilePath();
10502
+ try {
10503
+ const raw = fs23.readFileSync(filePath, "utf-8");
10504
+ if (!raw.trim()) return { ok: false, reason: "corrupt", detail: "empty file" };
10505
+ const parsed = JSON.parse(raw);
10506
+ if (!parsed.roots || typeof parsed.roots !== "object" || Array.isArray(parsed.roots)) {
10507
+ return { ok: false, reason: "corrupt", detail: "invalid structure: missing roots object" };
10508
+ }
10509
+ return { ok: true, pins: { roots: parsed.roots } };
10510
+ } catch (err2) {
10511
+ if (err2.code === "ENOENT") return { ok: false, reason: "missing" };
10512
+ return { ok: false, reason: "corrupt", detail: String(err2) };
10513
+ }
10514
+ }
10515
+ function readSkillPins() {
10516
+ const result = readSkillPinsSafe();
10517
+ if (result.ok) return result.pins;
10518
+ if (result.reason === "missing") return { roots: {} };
10519
+ throw new Error(`[node9] skill pin file is corrupt: ${result.detail}`);
10520
+ }
10521
+ function writeSkillPins(data) {
10522
+ const filePath = getPinsFilePath();
10523
+ fs23.mkdirSync(path25.dirname(filePath), { recursive: true });
10524
+ const tmp = `${filePath}.${crypto4.randomBytes(6).toString("hex")}.tmp`;
10525
+ fs23.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
10526
+ fs23.renameSync(tmp, filePath);
10527
+ }
10528
+ function removePin(rootKey) {
10529
+ const pins = readSkillPins();
10530
+ delete pins.roots[rootKey];
10531
+ writeSkillPins(pins);
10532
+ }
10533
+ function clearAllPins() {
10534
+ writeSkillPins({ roots: {} });
10535
+ }
10536
+ function verifyAndPinRoots(roots) {
10537
+ const pinsRead = readSkillPinsSafe();
10538
+ if (!pinsRead.ok && pinsRead.reason === "corrupt") {
10539
+ return { kind: "corrupt", detail: pinsRead.detail };
10540
+ }
10541
+ const pins = pinsRead.ok ? pinsRead.pins : { roots: {} };
10542
+ let mutated = false;
10543
+ for (const rootPath of new Set(roots)) {
10544
+ const rootKey = getRootKey(rootPath);
10545
+ const current = hashSkillRoot(rootPath);
10546
+ const existing = pins.roots[rootKey];
10547
+ if (!existing) {
10548
+ pins.roots[rootKey] = {
10549
+ rootPath,
10550
+ exists: current.exists,
10551
+ contentHash: current.contentHash,
10552
+ fileCount: current.fileCount,
10553
+ pinnedAt: (/* @__PURE__ */ new Date()).toISOString()
10554
+ };
10555
+ mutated = true;
10556
+ continue;
10557
+ }
10558
+ if (existing.exists !== current.exists || existing.contentHash !== current.contentHash) {
10559
+ let summary;
10560
+ if (existing.exists && !current.exists) summary = `vanished: ${rootPath}`;
10561
+ else if (!existing.exists && current.exists) summary = `appeared: ${rootPath}`;
10562
+ else summary = `changed: ${rootPath}`;
10563
+ return { kind: "drift", changedRootKey: rootKey, changedRootPath: rootPath, summary };
10564
+ }
10565
+ }
10566
+ if (mutated) writeSkillPins(pins);
10567
+ return { kind: "verified" };
10568
+ }
10569
+ function defaultSkillRoots(_cwd) {
10570
+ const marketplaces = path25.join(os19.homedir(), ".claude", "plugins", "marketplaces");
10571
+ const roots = [];
10572
+ let registries;
10573
+ try {
10574
+ registries = fs23.readdirSync(marketplaces, { withFileTypes: true });
10575
+ } catch {
10576
+ return [];
10577
+ }
10578
+ for (const registry of registries) {
10579
+ if (!registry.isDirectory()) continue;
10580
+ const pluginsDir = path25.join(marketplaces, registry.name, "plugins");
10581
+ let plugins;
10582
+ try {
10583
+ plugins = fs23.readdirSync(pluginsDir, { withFileTypes: true });
10584
+ } catch {
10585
+ continue;
10586
+ }
10587
+ for (const plugin of plugins) {
10588
+ if (!plugin.isDirectory()) continue;
10589
+ roots.push(path25.join(pluginsDir, plugin.name));
10590
+ }
10591
+ }
10592
+ return roots;
10593
+ }
10594
+ function resolveUserSkillRoot(entry, cwd) {
10595
+ if (!entry) return null;
10596
+ if (entry.startsWith("~/") || entry === "~") return path25.join(os19.homedir(), entry.slice(1));
10597
+ if (path25.isAbsolute(entry)) return entry;
10598
+ if (!cwd || !path25.isAbsolute(cwd)) return null;
10599
+ return path25.join(cwd, entry);
10600
+ }
10601
+
10066
10602
  // src/cli/commands/check.ts
10067
10603
  function sanitize2(value) {
10068
10604
  return value.replace(/[\x00-\x1F\x7F]/g, "");
@@ -10078,9 +10614,9 @@ function registerCheckCommand(program2) {
10078
10614
  } catch (err2) {
10079
10615
  const tempConfig = getConfig();
10080
10616
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
10081
- const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
10617
+ const logPath = path26.join(os20.homedir(), ".node9", "hook-debug.log");
10082
10618
  const errMsg = err2 instanceof Error ? err2.message : String(err2);
10083
- fs22.appendFileSync(
10619
+ fs24.appendFileSync(
10084
10620
  logPath,
10085
10621
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
10086
10622
  RAW: ${raw}
@@ -10093,11 +10629,11 @@ RAW: ${raw}
10093
10629
  if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
10094
10630
  try {
10095
10631
  const scriptPath = process.argv[1];
10096
- if (typeof scriptPath !== "string" || !path24.isAbsolute(scriptPath))
10632
+ if (typeof scriptPath !== "string" || !path26.isAbsolute(scriptPath))
10097
10633
  throw new Error("node9: argv[1] is not an absolute path");
10098
- const resolvedScript = fs22.realpathSync(scriptPath);
10099
- const packageDist = fs22.realpathSync(path24.resolve(__dirname, "../.."));
10100
- if (!resolvedScript.startsWith(packageDist + path24.sep) && resolvedScript !== packageDist)
10634
+ const resolvedScript = fs24.realpathSync(scriptPath);
10635
+ const packageDist = fs24.realpathSync(path26.resolve(__dirname, "../.."));
10636
+ if (!resolvedScript.startsWith(packageDist + path26.sep) && resolvedScript !== packageDist)
10101
10637
  throw new Error(
10102
10638
  `node9: daemon spawn aborted \u2014 argv[1] (${resolvedScript}) is outside package dist (${packageDist})`
10103
10639
  );
@@ -10119,10 +10655,10 @@ RAW: ${raw}
10119
10655
  });
10120
10656
  d.unref();
10121
10657
  } catch (spawnErr) {
10122
- const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
10658
+ const logPath = path26.join(os20.homedir(), ".node9", "hook-debug.log");
10123
10659
  const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
10124
10660
  try {
10125
- fs22.appendFileSync(
10661
+ fs24.appendFileSync(
10126
10662
  logPath,
10127
10663
  `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-failed: ${msg}
10128
10664
  `
@@ -10132,10 +10668,10 @@ RAW: ${raw}
10132
10668
  }
10133
10669
  }
10134
10670
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
10135
- const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
10136
- if (!fs22.existsSync(path24.dirname(logPath)))
10137
- fs22.mkdirSync(path24.dirname(logPath), { recursive: true });
10138
- fs22.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
10671
+ const logPath = path26.join(os20.homedir(), ".node9", "hook-debug.log");
10672
+ if (!fs24.existsSync(path26.dirname(logPath)))
10673
+ fs24.mkdirSync(path26.dirname(logPath), { recursive: true });
10674
+ fs24.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
10139
10675
  `);
10140
10676
  }
10141
10677
  const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
@@ -10148,8 +10684,8 @@ RAW: ${raw}
10148
10684
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
10149
10685
  let ttyFd = null;
10150
10686
  try {
10151
- ttyFd = fs22.openSync("/dev/tty", "w");
10152
- const writeTty = (line) => fs22.writeSync(ttyFd, line + "\n");
10687
+ ttyFd = fs24.openSync("/dev/tty", "w");
10688
+ const writeTty = (line) => fs24.writeSync(ttyFd, line + "\n");
10153
10689
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
10154
10690
  writeTty(chalk5.bgRed.white.bold(`
10155
10691
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
@@ -10168,7 +10704,7 @@ RAW: ${raw}
10168
10704
  } finally {
10169
10705
  if (ttyFd !== null)
10170
10706
  try {
10171
- fs22.closeSync(ttyFd);
10707
+ fs24.closeSync(ttyFd);
10172
10708
  } catch {
10173
10709
  }
10174
10710
  }
@@ -10197,10 +10733,131 @@ RAW: ${raw}
10197
10733
  return;
10198
10734
  }
10199
10735
  const meta = { agent, mcpServer };
10736
+ const skillPinCfg = config.policy.skillPinning;
10737
+ const rawSessionId = typeof payload.session_id === "string" ? payload.session_id : "";
10738
+ const safeSessionId = /^[A-Za-z0-9_\-]{1,128}$/.test(rawSessionId) ? rawSessionId : "";
10739
+ if (skillPinCfg.enabled && safeSessionId) {
10740
+ try {
10741
+ const sessionsDir = path26.join(os20.homedir(), ".node9", "skill-sessions");
10742
+ const flagPath = path26.join(sessionsDir, `${safeSessionId}.json`);
10743
+ let flag = null;
10744
+ try {
10745
+ flag = JSON.parse(fs24.readFileSync(flagPath, "utf-8"));
10746
+ } catch {
10747
+ }
10748
+ const writeFlag = (data2) => {
10749
+ try {
10750
+ fs24.mkdirSync(sessionsDir, { recursive: true });
10751
+ fs24.writeFileSync(
10752
+ flagPath,
10753
+ JSON.stringify({ ...data2, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
10754
+ { mode: 384 }
10755
+ );
10756
+ } catch {
10757
+ }
10758
+ };
10759
+ const sendSkillWarn = (detail, recoveryCmd) => {
10760
+ let ttyFd = null;
10761
+ try {
10762
+ ttyFd = fs24.openSync("/dev/tty", "w");
10763
+ const w = (line) => fs24.writeSync(ttyFd, line + "\n");
10764
+ w(chalk5.yellow(`
10765
+ \u26A0\uFE0F Node9: installed skill drift detected`));
10766
+ w(chalk5.gray(` ${detail}`));
10767
+ w(
10768
+ chalk5.gray(
10769
+ ` If you updated a plugin, acknowledge the change to clear this warning.`
10770
+ )
10771
+ );
10772
+ if (recoveryCmd) w(chalk5.green(` \u{1F4A1} Run: ${recoveryCmd}`));
10773
+ w("");
10774
+ } catch {
10775
+ } finally {
10776
+ if (ttyFd !== null)
10777
+ try {
10778
+ fs24.closeSync(ttyFd);
10779
+ } catch {
10780
+ }
10781
+ }
10782
+ };
10783
+ if (flag && flag.state === "quarantined" && skillPinCfg.mode === "block") {
10784
+ sendBlock(
10785
+ `Node9: session quarantined \u2014 installed skill changed. Open a separate terminal and run: node9 skill pin list (to see what changed) then: node9 skill pin update <rootKey> (to acknowledge). If you updated a plugin intentionally, this is expected.`,
10786
+ {
10787
+ blockedByLabel: "Skill Pin Quarantine",
10788
+ recoveryCommand: "node9 skill pin list"
10789
+ }
10790
+ );
10791
+ return;
10792
+ }
10793
+ if (!flag || flag.state !== "verified" && flag.state !== "warned") {
10794
+ const absoluteCwd = typeof payload.cwd === "string" && path26.isAbsolute(payload.cwd) ? payload.cwd : void 0;
10795
+ const extraRoots = skillPinCfg.roots;
10796
+ const resolvedExtra = extraRoots.map((r) => resolveUserSkillRoot(r, absoluteCwd)).filter((r) => typeof r === "string");
10797
+ const roots = [...defaultSkillRoots(absoluteCwd), ...resolvedExtra];
10798
+ const result2 = verifyAndPinRoots(roots);
10799
+ if (result2.kind === "corrupt") {
10800
+ if (skillPinCfg.mode === "block") {
10801
+ writeFlag({
10802
+ state: "quarantined",
10803
+ detail: `pin file corrupt: ${result2.detail}`
10804
+ });
10805
+ sendBlock("Node9: skill pin file is corrupt \u2014 fail-closed.", {
10806
+ blockedByLabel: "Skill Pin Quarantine",
10807
+ recoveryCommand: "node9 skill pin reset"
10808
+ });
10809
+ return;
10810
+ }
10811
+ writeFlag({ state: "warned", detail: `pin file corrupt: ${result2.detail}` });
10812
+ sendSkillWarn(
10813
+ `Skill pin file is corrupt: ${result2.detail}`,
10814
+ "node9 skill pin reset"
10815
+ );
10816
+ } else if (result2.kind === "drift") {
10817
+ if (skillPinCfg.mode === "block") {
10818
+ writeFlag({ state: "quarantined", detail: result2.summary });
10819
+ sendBlock(
10820
+ `Node9: installed skill changed \u2014 ${result2.summary}. If you updated a plugin, open a separate terminal and run: node9 skill pin update ${result2.changedRootKey}`,
10821
+ {
10822
+ blockedByLabel: "Skill Pin Quarantine",
10823
+ recoveryCommand: `node9 skill pin update ${result2.changedRootKey}`
10824
+ }
10825
+ );
10826
+ return;
10827
+ }
10828
+ writeFlag({ state: "warned", detail: result2.summary });
10829
+ sendSkillWarn(result2.summary, `node9 skill pin update ${result2.changedRootKey}`);
10830
+ } else {
10831
+ writeFlag({ state: "verified" });
10832
+ }
10833
+ try {
10834
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
10835
+ for (const name of fs24.readdirSync(sessionsDir)) {
10836
+ const p = path26.join(sessionsDir, name);
10837
+ try {
10838
+ if (fs24.statSync(p).mtimeMs < cutoff) fs24.unlinkSync(p);
10839
+ } catch {
10840
+ }
10841
+ }
10842
+ } catch {
10843
+ }
10844
+ }
10845
+ } catch (err2) {
10846
+ if (process.env.NODE9_DEBUG === "1") {
10847
+ try {
10848
+ const dbg = path26.join(os20.homedir(), ".node9", "hook-debug.log");
10849
+ const msg = err2 instanceof Error ? err2.message : String(err2);
10850
+ fs24.appendFileSync(dbg, `[${(/* @__PURE__ */ new Date()).toISOString()}] SKILL_PIN_ERROR: ${msg}
10851
+ `);
10852
+ } catch {
10853
+ }
10854
+ }
10855
+ }
10856
+ }
10200
10857
  if (shouldSnapshot(toolName, toolInput, config)) {
10201
10858
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
10202
10859
  }
10203
- const safeCwdForAuth = typeof payload.cwd === "string" && path24.isAbsolute(payload.cwd) ? payload.cwd : void 0;
10860
+ const safeCwdForAuth = typeof payload.cwd === "string" && path26.isAbsolute(payload.cwd) ? payload.cwd : void 0;
10204
10861
  const result = await authorizeHeadless(toolName, toolInput, meta, {
10205
10862
  cwd: safeCwdForAuth
10206
10863
  });
@@ -10212,12 +10869,12 @@ RAW: ${raw}
10212
10869
  }
10213
10870
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
10214
10871
  try {
10215
- const tty = fs22.openSync("/dev/tty", "w");
10216
- fs22.writeSync(
10872
+ const tty = fs24.openSync("/dev/tty", "w");
10873
+ fs24.writeSync(
10217
10874
  tty,
10218
10875
  chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
10219
10876
  );
10220
- fs22.closeSync(tty);
10877
+ fs24.closeSync(tty);
10221
10878
  } catch {
10222
10879
  }
10223
10880
  const daemonReady = await autoStartDaemonAndWait();
@@ -10244,9 +10901,9 @@ RAW: ${raw}
10244
10901
  });
10245
10902
  } catch (err2) {
10246
10903
  if (process.env.NODE9_DEBUG === "1") {
10247
- const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
10904
+ const logPath = path26.join(os20.homedir(), ".node9", "hook-debug.log");
10248
10905
  const errMsg = err2 instanceof Error ? err2.message : String(err2);
10249
- fs22.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
10906
+ fs24.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
10250
10907
  `);
10251
10908
  }
10252
10909
  process.exit(0);
@@ -10283,9 +10940,9 @@ RAW: ${raw}
10283
10940
  init_audit();
10284
10941
  init_config();
10285
10942
  init_policy();
10286
- import fs23 from "fs";
10287
- import path25 from "path";
10288
- import os19 from "os";
10943
+ import fs25 from "fs";
10944
+ import path27 from "path";
10945
+ import os21 from "os";
10289
10946
  init_daemon();
10290
10947
 
10291
10948
  // src/utils/cp-mv-parser.ts
@@ -10358,10 +11015,10 @@ function registerLogCommand(program2) {
10358
11015
  decision: "allowed",
10359
11016
  source: "post-hook"
10360
11017
  };
10361
- const logPath = path25.join(os19.homedir(), ".node9", "audit.log");
10362
- if (!fs23.existsSync(path25.dirname(logPath)))
10363
- fs23.mkdirSync(path25.dirname(logPath), { recursive: true });
10364
- fs23.appendFileSync(logPath, JSON.stringify(entry) + "\n");
11018
+ const logPath = path27.join(os21.homedir(), ".node9", "audit.log");
11019
+ if (!fs25.existsSync(path27.dirname(logPath)))
11020
+ fs25.mkdirSync(path27.dirname(logPath), { recursive: true });
11021
+ fs25.appendFileSync(logPath, JSON.stringify(entry) + "\n");
10365
11022
  if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
10366
11023
  const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
10367
11024
  if (command) {
@@ -10394,7 +11051,7 @@ function registerLogCommand(program2) {
10394
11051
  }
10395
11052
  }
10396
11053
  }
10397
- const safeCwd = typeof payload.cwd === "string" && path25.isAbsolute(payload.cwd) ? payload.cwd : void 0;
11054
+ const safeCwd = typeof payload.cwd === "string" && path27.isAbsolute(payload.cwd) ? payload.cwd : void 0;
10398
11055
  const config = getConfig(safeCwd);
10399
11056
  if (shouldSnapshot(tool, {}, config)) {
10400
11057
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
@@ -10403,9 +11060,9 @@ function registerLogCommand(program2) {
10403
11060
  const msg = err2 instanceof Error ? err2.message : String(err2);
10404
11061
  process.stderr.write(`[Node9] audit log error: ${msg}
10405
11062
  `);
10406
- const debugPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
11063
+ const debugPath = path27.join(os21.homedir(), ".node9", "hook-debug.log");
10407
11064
  try {
10408
- fs23.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
11065
+ fs25.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
10409
11066
  `);
10410
11067
  } catch {
10411
11068
  }
@@ -10806,13 +11463,13 @@ function registerConfigShowCommand(program2) {
10806
11463
  // src/cli/commands/doctor.ts
10807
11464
  init_daemon();
10808
11465
  import chalk7 from "chalk";
10809
- import fs24 from "fs";
10810
- import path26 from "path";
10811
- import os20 from "os";
11466
+ import fs26 from "fs";
11467
+ import path28 from "path";
11468
+ import os22 from "os";
10812
11469
  import { execSync as execSync2 } from "child_process";
10813
11470
  function registerDoctorCommand(program2, version2) {
10814
11471
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
10815
- const homeDir2 = os20.homedir();
11472
+ const homeDir2 = os22.homedir();
10816
11473
  let failures = 0;
10817
11474
  function pass(msg) {
10818
11475
  console.log(chalk7.green(" \u2705 ") + msg);
@@ -10861,10 +11518,10 @@ function registerDoctorCommand(program2, version2) {
10861
11518
  );
10862
11519
  }
10863
11520
  section("Configuration");
10864
- const globalConfigPath = path26.join(homeDir2, ".node9", "config.json");
10865
- if (fs24.existsSync(globalConfigPath)) {
11521
+ const globalConfigPath = path28.join(homeDir2, ".node9", "config.json");
11522
+ if (fs26.existsSync(globalConfigPath)) {
10866
11523
  try {
10867
- JSON.parse(fs24.readFileSync(globalConfigPath, "utf-8"));
11524
+ JSON.parse(fs26.readFileSync(globalConfigPath, "utf-8"));
10868
11525
  pass("~/.node9/config.json found and valid");
10869
11526
  } catch {
10870
11527
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -10872,10 +11529,10 @@ function registerDoctorCommand(program2, version2) {
10872
11529
  } else {
10873
11530
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
10874
11531
  }
10875
- const projectConfigPath = path26.join(process.cwd(), "node9.config.json");
10876
- if (fs24.existsSync(projectConfigPath)) {
11532
+ const projectConfigPath = path28.join(process.cwd(), "node9.config.json");
11533
+ if (fs26.existsSync(projectConfigPath)) {
10877
11534
  try {
10878
- JSON.parse(fs24.readFileSync(projectConfigPath, "utf-8"));
11535
+ JSON.parse(fs26.readFileSync(projectConfigPath, "utf-8"));
10879
11536
  pass("node9.config.json found and valid (project)");
10880
11537
  } catch {
10881
11538
  fail(
@@ -10884,8 +11541,8 @@ function registerDoctorCommand(program2, version2) {
10884
11541
  );
10885
11542
  }
10886
11543
  }
10887
- const credsPath = path26.join(homeDir2, ".node9", "credentials.json");
10888
- if (fs24.existsSync(credsPath)) {
11544
+ const credsPath = path28.join(homeDir2, ".node9", "credentials.json");
11545
+ if (fs26.existsSync(credsPath)) {
10889
11546
  pass("Cloud credentials found (~/.node9/credentials.json)");
10890
11547
  } else {
10891
11548
  warn(
@@ -10894,10 +11551,10 @@ function registerDoctorCommand(program2, version2) {
10894
11551
  );
10895
11552
  }
10896
11553
  section("Agent Hooks");
10897
- const claudeSettingsPath = path26.join(homeDir2, ".claude", "settings.json");
10898
- if (fs24.existsSync(claudeSettingsPath)) {
11554
+ const claudeSettingsPath = path28.join(homeDir2, ".claude", "settings.json");
11555
+ if (fs26.existsSync(claudeSettingsPath)) {
10899
11556
  try {
10900
- const cs = JSON.parse(fs24.readFileSync(claudeSettingsPath, "utf-8"));
11557
+ const cs = JSON.parse(fs26.readFileSync(claudeSettingsPath, "utf-8"));
10901
11558
  const hasHook = cs.hooks?.PreToolUse?.some(
10902
11559
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
10903
11560
  );
@@ -10913,10 +11570,10 @@ function registerDoctorCommand(program2, version2) {
10913
11570
  } else {
10914
11571
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
10915
11572
  }
10916
- const geminiSettingsPath = path26.join(homeDir2, ".gemini", "settings.json");
10917
- if (fs24.existsSync(geminiSettingsPath)) {
11573
+ const geminiSettingsPath = path28.join(homeDir2, ".gemini", "settings.json");
11574
+ if (fs26.existsSync(geminiSettingsPath)) {
10918
11575
  try {
10919
- const gs = JSON.parse(fs24.readFileSync(geminiSettingsPath, "utf-8"));
11576
+ const gs = JSON.parse(fs26.readFileSync(geminiSettingsPath, "utf-8"));
10920
11577
  const hasHook = gs.hooks?.BeforeTool?.some(
10921
11578
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
10922
11579
  );
@@ -10932,10 +11589,10 @@ function registerDoctorCommand(program2, version2) {
10932
11589
  } else {
10933
11590
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
10934
11591
  }
10935
- const cursorHooksPath = path26.join(homeDir2, ".cursor", "hooks.json");
10936
- if (fs24.existsSync(cursorHooksPath)) {
11592
+ const cursorHooksPath = path28.join(homeDir2, ".cursor", "hooks.json");
11593
+ if (fs26.existsSync(cursorHooksPath)) {
10937
11594
  try {
10938
- const cur = JSON.parse(fs24.readFileSync(cursorHooksPath, "utf-8"));
11595
+ const cur = JSON.parse(fs26.readFileSync(cursorHooksPath, "utf-8"));
10939
11596
  const hasHook = cur.hooks?.preToolUse?.some(
10940
11597
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
10941
11598
  );
@@ -10973,9 +11630,9 @@ function registerDoctorCommand(program2, version2) {
10973
11630
 
10974
11631
  // src/cli/commands/audit.ts
10975
11632
  import chalk8 from "chalk";
10976
- import fs25 from "fs";
10977
- import path27 from "path";
10978
- import os21 from "os";
11633
+ import fs27 from "fs";
11634
+ import path29 from "path";
11635
+ import os23 from "os";
10979
11636
  function formatRelativeTime(timestamp) {
10980
11637
  const diff = Date.now() - new Date(timestamp).getTime();
10981
11638
  const sec = Math.floor(diff / 1e3);
@@ -10988,14 +11645,14 @@ function formatRelativeTime(timestamp) {
10988
11645
  }
10989
11646
  function registerAuditCommand(program2) {
10990
11647
  program2.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) => {
10991
- const logPath = path27.join(os21.homedir(), ".node9", "audit.log");
10992
- if (!fs25.existsSync(logPath)) {
11648
+ const logPath = path29.join(os23.homedir(), ".node9", "audit.log");
11649
+ if (!fs27.existsSync(logPath)) {
10993
11650
  console.log(
10994
11651
  chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
10995
11652
  );
10996
11653
  return;
10997
11654
  }
10998
- const raw = fs25.readFileSync(logPath, "utf-8");
11655
+ const raw = fs27.readFileSync(logPath, "utf-8");
10999
11656
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
11000
11657
  let entries = lines.flatMap((line) => {
11001
11658
  try {
@@ -11049,9 +11706,9 @@ function registerAuditCommand(program2) {
11049
11706
 
11050
11707
  // src/cli/commands/report.ts
11051
11708
  import chalk9 from "chalk";
11052
- import fs26 from "fs";
11053
- import path28 from "path";
11054
- import os22 from "os";
11709
+ import fs28 from "fs";
11710
+ import path30 from "path";
11711
+ import os24 from "os";
11055
11712
  var TEST_COMMAND_RE3 = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
11056
11713
  function buildTestTimestamps(allEntries) {
11057
11714
  const testTs = /* @__PURE__ */ new Set();
@@ -11098,8 +11755,8 @@ function getDateRange(period) {
11098
11755
  }
11099
11756
  }
11100
11757
  function parseAuditLog(logPath) {
11101
- if (!fs26.existsSync(logPath)) return [];
11102
- const raw = fs26.readFileSync(logPath, "utf-8");
11758
+ if (!fs28.existsSync(logPath)) return [];
11759
+ const raw = fs28.readFileSync(logPath, "utf-8");
11103
11760
  return raw.split("\n").flatMap((line) => {
11104
11761
  if (!line.trim()) return [];
11105
11762
  try {
@@ -11166,34 +11823,38 @@ function loadClaudeCost(start, end) {
11166
11823
  byDay: /* @__PURE__ */ new Map(),
11167
11824
  byModel: /* @__PURE__ */ new Map(),
11168
11825
  inputTokens: 0,
11826
+ outputTokens: 0,
11827
+ cacheWriteTokens: 0,
11169
11828
  cacheReadTokens: 0
11170
11829
  };
11171
- const projectsDir = path28.join(os22.homedir(), ".claude", "projects");
11172
- if (!fs26.existsSync(projectsDir)) return empty;
11830
+ const projectsDir = path30.join(os24.homedir(), ".claude", "projects");
11831
+ if (!fs28.existsSync(projectsDir)) return empty;
11173
11832
  let dirs;
11174
11833
  try {
11175
- dirs = fs26.readdirSync(projectsDir);
11834
+ dirs = fs28.readdirSync(projectsDir);
11176
11835
  } catch {
11177
11836
  return empty;
11178
11837
  }
11179
11838
  let total = 0;
11180
11839
  let inputTokens = 0;
11840
+ let outputTokens = 0;
11841
+ let cacheWriteTokens = 0;
11181
11842
  let cacheReadTokens = 0;
11182
11843
  const byDay = /* @__PURE__ */ new Map();
11183
11844
  const byModel = /* @__PURE__ */ new Map();
11184
11845
  for (const proj of dirs) {
11185
- const projPath = path28.join(projectsDir, proj);
11846
+ const projPath = path30.join(projectsDir, proj);
11186
11847
  let files;
11187
11848
  try {
11188
- const stat = fs26.statSync(projPath);
11849
+ const stat = fs28.statSync(projPath);
11189
11850
  if (!stat.isDirectory()) continue;
11190
- files = fs26.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
11851
+ files = fs28.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
11191
11852
  } catch {
11192
11853
  continue;
11193
11854
  }
11194
11855
  for (const file of files) {
11195
11856
  try {
11196
- const raw = fs26.readFileSync(path28.join(projPath, file), "utf-8");
11857
+ const raw = fs28.readFileSync(path30.join(projPath, file), "utf-8");
11197
11858
  for (const line of raw.split("\n")) {
11198
11859
  if (!line.trim()) continue;
11199
11860
  let entry;
@@ -11218,6 +11879,8 @@ function loadClaudeCost(start, end) {
11218
11879
  const cost = inp * p.i + out * p.o + cw * p.cw + cr * p.cr;
11219
11880
  total += cost;
11220
11881
  inputTokens += inp;
11882
+ outputTokens += out;
11883
+ cacheWriteTokens += cw;
11221
11884
  cacheReadTokens += cr;
11222
11885
  const dateKey = entry.timestamp.slice(0, 10);
11223
11886
  byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
@@ -11229,15 +11892,24 @@ function loadClaudeCost(start, end) {
11229
11892
  }
11230
11893
  }
11231
11894
  }
11232
- return { total, byDay, byModel, inputTokens, cacheReadTokens };
11895
+ return { total, byDay, byModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens };
11233
11896
  }
11234
11897
  function registerReportCommand(program2) {
11235
11898
  program2.command("report").description("Activity and security report \u2014 what Claude did, what was blocked").option("--period <period>", "today | 7d | 30d | month", "7d").option("--no-tests", "exclude test runner calls (npm test, vitest, pytest\u2026) from stats").action((options) => {
11236
11899
  const period = ["today", "7d", "30d", "month"].includes(
11237
11900
  options.period
11238
11901
  ) ? options.period : "7d";
11239
- const logPath = path28.join(os22.homedir(), ".node9", "audit.log");
11902
+ const logPath = path30.join(os24.homedir(), ".node9", "audit.log");
11240
11903
  const allEntries = parseAuditLog(logPath);
11904
+ const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
11905
+ if (unackedDlp.length > 0) {
11906
+ console.log("");
11907
+ console.log(
11908
+ chalk9.bgRed.white.bold(
11909
+ ` \u26A0\uFE0F DLP ALERT: ${unackedDlp.length} secret${unackedDlp.length !== 1 ? "s" : ""} found in Claude response text `
11910
+ ) + " " + chalk9.yellow("\u2192 run: node9 dlp")
11911
+ );
11912
+ }
11241
11913
  if (allEntries.length === 0) {
11242
11914
  console.log(
11243
11915
  chalk9.yellow("\n No audit data found. Run node9 with Claude Code to generate entries.\n")
@@ -11250,6 +11922,8 @@ function registerReportCommand(program2) {
11250
11922
  byDay: costByDay,
11251
11923
  byModel: costByModel,
11252
11924
  inputTokens: costInputTokens,
11925
+ outputTokens: costOutputTokens,
11926
+ cacheWriteTokens: costCacheWrite,
11253
11927
  cacheReadTokens: costCacheRead
11254
11928
  } = loadClaudeCost(start, end);
11255
11929
  const periodMs = end.getTime() - start.getTime();
@@ -11267,6 +11941,7 @@ function registerReportCommand(program2) {
11267
11941
  let filteredTestCount = 0;
11268
11942
  const entries = allEntries.filter((e) => {
11269
11943
  if (e.source === "post-hook") return false;
11944
+ if (e.source === "response-dlp") return false;
11270
11945
  const ts = new Date(e.ts);
11271
11946
  if (ts < start || ts > end) return false;
11272
11947
  if (excludeTests && isTestEntry(e, testTs)) {
@@ -11400,7 +12075,7 @@ function registerReportCommand(program2) {
11400
12075
  if (topBlocks.length === 0) {
11401
12076
  console.log(" " + " ".repeat(COL) + " " + chalk9.dim("nothing blocked \u2713"));
11402
12077
  }
11403
- if (agentMap.size > 1) {
12078
+ if (agentMap.size >= 1) {
11404
12079
  console.log("");
11405
12080
  console.log(" " + chalk9.bold("Agents"));
11406
12081
  console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
@@ -11450,6 +12125,40 @@ function registerReportCommand(program2) {
11450
12125
  );
11451
12126
  }
11452
12127
  }
12128
+ const totalTokens = costInputTokens + costOutputTokens + costCacheWrite + costCacheRead;
12129
+ if (totalTokens > 0) {
12130
+ const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
12131
+ console.log("");
12132
+ console.log(" " + chalk9.bold("Tokens") + " " + chalk9.dim(`${num(totalTokens)} total`));
12133
+ console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
12134
+ const tokenRows = [
12135
+ ["Input", costInputTokens, chalk9.cyan(num(costInputTokens))],
12136
+ ["Output", costOutputTokens, chalk9.white(num(costOutputTokens))],
12137
+ ["Cache write", costCacheWrite, chalk9.yellow(num(costCacheWrite))],
12138
+ ["Cache read", costCacheRead, chalk9.green(num(costCacheRead))]
12139
+ ];
12140
+ const maxTok = Math.max(
12141
+ costInputTokens,
12142
+ costOutputTokens,
12143
+ costCacheWrite,
12144
+ costCacheRead,
12145
+ 1
12146
+ );
12147
+ const TOK_BAR = Math.max(6, Math.min(20, W - 30));
12148
+ const TOK_LABEL = 14;
12149
+ for (const [label, count, colored] of tokenRows) {
12150
+ if (count === 0) continue;
12151
+ const b = colorBar(count, maxTok, TOK_BAR);
12152
+ console.log(" " + chalk9.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
12153
+ }
12154
+ if (cacheHitPct > 0) {
12155
+ console.log(
12156
+ " " + chalk9.dim(
12157
+ `Cache hit rate: ${cacheHitPct}% (saves ~${fmtCost(costCacheRead * 27e-7)} vs fresh input)`
12158
+ )
12159
+ );
12160
+ }
12161
+ }
11453
12162
  if (costUSD > 0) {
11454
12163
  const periodDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 864e5));
11455
12164
  const avgPerDay = costUSD / periodDays;
@@ -11474,6 +12183,33 @@ function registerReportCommand(program2) {
11474
12183
  );
11475
12184
  }
11476
12185
  }
12186
+ const responseDlpEntries = allEntries.filter((e) => {
12187
+ if (e.source !== "response-dlp") return false;
12188
+ const ts = new Date(e.ts);
12189
+ return ts >= start && ts <= end;
12190
+ });
12191
+ if (responseDlpEntries.length > 0) {
12192
+ console.log("");
12193
+ console.log(
12194
+ " " + chalk9.red.bold("\u26A0\uFE0F Response DLP") + chalk9.dim(" \xB7 ") + chalk9.red(
12195
+ `${responseDlpEntries.length} secret${responseDlpEntries.length !== 1 ? "s" : ""} found in Claude response text`
12196
+ )
12197
+ );
12198
+ console.log(" " + chalk9.dim("\u2500".repeat(Math.min(60, W - 4))));
12199
+ console.log(
12200
+ " " + chalk9.yellow("These were NOT blocked \u2014 Claude included them in response prose.")
12201
+ );
12202
+ console.log(" " + chalk9.yellow("Rotate affected keys immediately."));
12203
+ for (const e of responseDlpEntries.slice(0, 5)) {
12204
+ const ts = chalk9.dim(fmtDate(e.ts) + " ");
12205
+ const pattern = chalk9.red(e.dlpPattern ?? "DLP");
12206
+ const sample = chalk9.gray(e.dlpSample ?? "");
12207
+ console.log(` ${ts}${pattern} ${sample}`);
12208
+ }
12209
+ if (responseDlpEntries.length > 5) {
12210
+ console.log(chalk9.dim(` \u2026 and ${responseDlpEntries.length - 5} more`));
12211
+ }
12212
+ }
11477
12213
  console.log("");
11478
12214
  console.log(
11479
12215
  " " + chalk9.dim("node9 audit --deny") + chalk9.dim(" \xB7 ") + chalk9.dim("node9 report --period today|7d|30d|month --no-tests")
@@ -11591,12 +12327,12 @@ function registerDaemonCommand(program2) {
11591
12327
  init_core();
11592
12328
  init_daemon();
11593
12329
  import chalk11 from "chalk";
11594
- import fs27 from "fs";
11595
- import path29 from "path";
11596
- import os23 from "os";
12330
+ import fs29 from "fs";
12331
+ import path31 from "path";
12332
+ import os25 from "os";
11597
12333
  function readJson2(filePath) {
11598
12334
  try {
11599
- if (fs27.existsSync(filePath)) return JSON.parse(fs27.readFileSync(filePath, "utf-8"));
12335
+ if (fs29.existsSync(filePath)) return JSON.parse(fs29.readFileSync(filePath, "utf-8"));
11600
12336
  } catch {
11601
12337
  }
11602
12338
  return null;
@@ -11661,28 +12397,28 @@ function registerStatusCommand(program2) {
11661
12397
  console.log("");
11662
12398
  const modeLabel = settings.mode === "audit" ? chalk11.blue("audit") : settings.mode === "strict" ? chalk11.red("strict") : chalk11.white("standard");
11663
12399
  console.log(` Mode: ${modeLabel}`);
11664
- const projectConfig = path29.join(process.cwd(), "node9.config.json");
11665
- const globalConfig = path29.join(os23.homedir(), ".node9", "config.json");
12400
+ const projectConfig = path31.join(process.cwd(), "node9.config.json");
12401
+ const globalConfig = path31.join(os25.homedir(), ".node9", "config.json");
11666
12402
  console.log(
11667
- ` Local: ${fs27.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
12403
+ ` Local: ${fs29.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
11668
12404
  );
11669
12405
  console.log(
11670
- ` Global: ${fs27.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
12406
+ ` Global: ${fs29.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
11671
12407
  );
11672
12408
  if (mergedConfig.policy.sandboxPaths.length > 0) {
11673
12409
  console.log(
11674
12410
  ` Sandbox: ${chalk11.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
11675
12411
  );
11676
12412
  }
11677
- const homeDir2 = os23.homedir();
12413
+ const homeDir2 = os25.homedir();
11678
12414
  const claudeSettings = readJson2(
11679
- path29.join(homeDir2, ".claude", "settings.json")
12415
+ path31.join(homeDir2, ".claude", "settings.json")
11680
12416
  );
11681
- const claudeConfig = readJson2(path29.join(homeDir2, ".claude.json"));
12417
+ const claudeConfig = readJson2(path31.join(homeDir2, ".claude.json"));
11682
12418
  const geminiSettings = readJson2(
11683
- path29.join(homeDir2, ".gemini", "settings.json")
12419
+ path31.join(homeDir2, ".gemini", "settings.json")
11684
12420
  );
11685
- const cursorConfig = readJson2(path29.join(homeDir2, ".cursor", "mcp.json"));
12421
+ const cursorConfig = readJson2(path31.join(homeDir2, ".cursor", "mcp.json"));
11686
12422
  const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
11687
12423
  if (agentFound) {
11688
12424
  console.log("");
@@ -11742,9 +12478,9 @@ function registerStatusCommand(program2) {
11742
12478
  // src/cli/commands/init.ts
11743
12479
  init_core();
11744
12480
  import chalk12 from "chalk";
11745
- import fs28 from "fs";
11746
- import path30 from "path";
11747
- import os24 from "os";
12481
+ import fs30 from "fs";
12482
+ import path32 from "path";
12483
+ import os26 from "os";
11748
12484
  import https3 from "https";
11749
12485
  init_shields();
11750
12486
  init_service();
@@ -11804,15 +12540,15 @@ function registerInitCommand(program2) {
11804
12540
  }
11805
12541
  console.log("");
11806
12542
  }
11807
- const configPath = path30.join(os24.homedir(), ".node9", "config.json");
11808
- if (fs28.existsSync(configPath) && !options.force) {
12543
+ const configPath = path32.join(os26.homedir(), ".node9", "config.json");
12544
+ if (fs30.existsSync(configPath) && !options.force) {
11809
12545
  try {
11810
- const existing = JSON.parse(fs28.readFileSync(configPath, "utf-8"));
12546
+ const existing = JSON.parse(fs30.readFileSync(configPath, "utf-8"));
11811
12547
  const settings = existing.settings ?? {};
11812
12548
  if (settings.mode !== chosenMode) {
11813
12549
  settings.mode = chosenMode;
11814
12550
  existing.settings = settings;
11815
- fs28.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
12551
+ fs30.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
11816
12552
  console.log(chalk12.green(`\u2705 Mode updated: ${chosenMode}`));
11817
12553
  } else {
11818
12554
  console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
@@ -11825,9 +12561,9 @@ function registerInitCommand(program2) {
11825
12561
  ...DEFAULT_CONFIG,
11826
12562
  settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
11827
12563
  };
11828
- const dir = path30.dirname(configPath);
11829
- if (!fs28.existsSync(dir)) fs28.mkdirSync(dir, { recursive: true });
11830
- fs28.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
12564
+ const dir = path32.dirname(configPath);
12565
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
12566
+ fs30.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
11831
12567
  console.log(chalk12.green(`\u2705 Config created: ${configPath}`));
11832
12568
  console.log(chalk12.gray(` Mode: ${chosenMode}`));
11833
12569
  }
@@ -11911,7 +12647,7 @@ function registerInitCommand(program2) {
11911
12647
  }
11912
12648
 
11913
12649
  // src/cli/commands/undo.ts
11914
- import path31 from "path";
12650
+ import path33 from "path";
11915
12651
  import chalk14 from "chalk";
11916
12652
 
11917
12653
  // src/tui/undo-navigator.ts
@@ -12070,7 +12806,7 @@ function findMatchingCwd(startDir, history) {
12070
12806
  let dir = startDir;
12071
12807
  while (true) {
12072
12808
  if (cwds.has(dir)) return dir;
12073
- const parent = path31.dirname(dir);
12809
+ const parent = path33.dirname(dir);
12074
12810
  if (parent === dir) return null;
12075
12811
  dir = parent;
12076
12812
  }
@@ -12266,12 +13002,12 @@ import { execa as execa2 } from "execa";
12266
13002
  init_provenance();
12267
13003
 
12268
13004
  // src/mcp-pin.ts
12269
- import fs29 from "fs";
12270
- import path32 from "path";
12271
- import os25 from "os";
12272
- import crypto4 from "crypto";
12273
- function getPinsFilePath() {
12274
- return path32.join(os25.homedir(), ".node9", "mcp-pins.json");
13005
+ import fs31 from "fs";
13006
+ import path34 from "path";
13007
+ import os27 from "os";
13008
+ import crypto5 from "crypto";
13009
+ function getPinsFilePath2() {
13010
+ return path34.join(os27.homedir(), ".node9", "mcp-pins.json");
12275
13011
  }
12276
13012
  function hashToolDefinitions(tools) {
12277
13013
  const sorted = [...tools].sort((a, b) => {
@@ -12280,15 +13016,15 @@ function hashToolDefinitions(tools) {
12280
13016
  return nameA.localeCompare(nameB);
12281
13017
  });
12282
13018
  const canonical = JSON.stringify(sorted);
12283
- return crypto4.createHash("sha256").update(canonical).digest("hex");
13019
+ return crypto5.createHash("sha256").update(canonical).digest("hex");
12284
13020
  }
12285
13021
  function getServerKey(upstreamCommand) {
12286
- return crypto4.createHash("sha256").update(upstreamCommand).digest("hex").slice(0, 16);
13022
+ return crypto5.createHash("sha256").update(upstreamCommand).digest("hex").slice(0, 16);
12287
13023
  }
12288
13024
  function readMcpPinsSafe() {
12289
- const filePath = getPinsFilePath();
13025
+ const filePath = getPinsFilePath2();
12290
13026
  try {
12291
- const raw = fs29.readFileSync(filePath, "utf-8");
13027
+ const raw = fs31.readFileSync(filePath, "utf-8");
12292
13028
  if (!raw.trim()) {
12293
13029
  return { ok: false, reason: "corrupt", detail: "empty file" };
12294
13030
  }
@@ -12311,11 +13047,11 @@ function readMcpPins() {
12311
13047
  throw new Error(`[node9] MCP pin file is corrupt: ${result.detail}`);
12312
13048
  }
12313
13049
  function writeMcpPins(data) {
12314
- const filePath = getPinsFilePath();
12315
- fs29.mkdirSync(path32.dirname(filePath), { recursive: true });
12316
- const tmp = `${filePath}.${crypto4.randomBytes(6).toString("hex")}.tmp`;
12317
- fs29.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
12318
- fs29.renameSync(tmp, filePath);
13050
+ const filePath = getPinsFilePath2();
13051
+ fs31.mkdirSync(path34.dirname(filePath), { recursive: true });
13052
+ const tmp = `${filePath}.${crypto5.randomBytes(6).toString("hex")}.tmp`;
13053
+ fs31.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
13054
+ fs31.renameSync(tmp, filePath);
12319
13055
  }
12320
13056
  function checkPin(serverKey, currentHash) {
12321
13057
  const result = readMcpPinsSafe();
@@ -12338,12 +13074,12 @@ function updatePin(serverKey, label, toolsHash, toolNames) {
12338
13074
  };
12339
13075
  writeMcpPins(pins);
12340
13076
  }
12341
- function removePin(serverKey) {
13077
+ function removePin2(serverKey) {
12342
13078
  const pins = readMcpPins();
12343
13079
  delete pins.servers[serverKey];
12344
13080
  writeMcpPins(pins);
12345
13081
  }
12346
- function clearAllPins() {
13082
+ function clearAllPins2() {
12347
13083
  writeMcpPins({ servers: {} });
12348
13084
  }
12349
13085
 
@@ -12687,9 +13423,9 @@ function registerMcpGatewayCommand(program2) {
12687
13423
 
12688
13424
  // src/mcp-server/index.ts
12689
13425
  import readline4 from "readline";
12690
- import fs30 from "fs";
12691
- import os26 from "os";
12692
- import path33 from "path";
13426
+ import fs32 from "fs";
13427
+ import os28 from "os";
13428
+ import path35 from "path";
12693
13429
  init_core();
12694
13430
  init_daemon();
12695
13431
  init_shields();
@@ -12864,13 +13600,13 @@ function handleStatus() {
12864
13600
  lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
12865
13601
  lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
12866
13602
  lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
12867
- const projectConfig = path33.join(process.cwd(), "node9.config.json");
12868
- const globalConfig = path33.join(os26.homedir(), ".node9", "config.json");
13603
+ const projectConfig = path35.join(process.cwd(), "node9.config.json");
13604
+ const globalConfig = path35.join(os28.homedir(), ".node9", "config.json");
12869
13605
  lines.push(
12870
- `Project config (node9.config.json): ${fs30.existsSync(projectConfig) ? "present" : "not found"}`
13606
+ `Project config (node9.config.json): ${fs32.existsSync(projectConfig) ? "present" : "not found"}`
12871
13607
  );
12872
13608
  lines.push(
12873
- `Global config (~/.node9/config.json): ${fs30.existsSync(globalConfig) ? "present" : "not found"}`
13609
+ `Global config (~/.node9/config.json): ${fs32.existsSync(globalConfig) ? "present" : "not found"}`
12874
13610
  );
12875
13611
  return lines.join("\n");
12876
13612
  }
@@ -12944,21 +13680,21 @@ function handleShieldDisable(args) {
12944
13680
  writeActiveShields(active.filter((s) => s !== name));
12945
13681
  return `Shield "${name}" disabled.`;
12946
13682
  }
12947
- var GLOBAL_CONFIG_PATH2 = path33.join(os26.homedir(), ".node9", "config.json");
13683
+ var GLOBAL_CONFIG_PATH2 = path35.join(os28.homedir(), ".node9", "config.json");
12948
13684
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
12949
13685
  function readGlobalConfigRaw() {
12950
13686
  try {
12951
- if (fs30.existsSync(GLOBAL_CONFIG_PATH2)) {
12952
- return JSON.parse(fs30.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
13687
+ if (fs32.existsSync(GLOBAL_CONFIG_PATH2)) {
13688
+ return JSON.parse(fs32.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
12953
13689
  }
12954
13690
  } catch {
12955
13691
  }
12956
13692
  return {};
12957
13693
  }
12958
13694
  function writeGlobalConfigRaw(data) {
12959
- const dir = path33.dirname(GLOBAL_CONFIG_PATH2);
12960
- if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
12961
- fs30.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
13695
+ const dir = path35.dirname(GLOBAL_CONFIG_PATH2);
13696
+ if (!fs32.existsSync(dir)) fs32.mkdirSync(dir, { recursive: true });
13697
+ fs32.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
12962
13698
  }
12963
13699
  function handleApproverList() {
12964
13700
  const config = getConfig();
@@ -13001,9 +13737,9 @@ function handleApproverSet(args) {
13001
13737
  }
13002
13738
  function handleAuditGet(args) {
13003
13739
  const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
13004
- const auditPath = path33.join(os26.homedir(), ".node9", "audit.log");
13005
- if (!fs30.existsSync(auditPath)) return "No audit log found.";
13006
- const lines = fs30.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
13740
+ const auditPath = path35.join(os28.homedir(), ".node9", "audit.log");
13741
+ if (!fs32.existsSync(auditPath)) return "No audit log found.";
13742
+ const lines = fs32.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
13007
13743
  const recent = lines.slice(-limit);
13008
13744
  const entries = recent.map((line) => {
13009
13745
  try {
@@ -13301,7 +14037,7 @@ function registerMcpPinCommand(program2) {
13301
14037
  process.exit(1);
13302
14038
  }
13303
14039
  const label = pins.servers[serverKey].label;
13304
- removePin(serverKey);
14040
+ removePin2(serverKey);
13305
14041
  console.log(chalk18.green(`
13306
14042
  \u{1F513} Pin removed for ${chalk18.cyan(serverKey)}`));
13307
14043
  console.log(chalk18.gray(` Server: ${label}`));
@@ -13314,7 +14050,7 @@ function registerMcpPinCommand(program2) {
13314
14050
  return;
13315
14051
  }
13316
14052
  const count = result.ok ? Object.keys(result.pins.servers).length : "?";
13317
- clearAllPins();
14053
+ clearAllPins2();
13318
14054
  console.log(chalk18.green(`
13319
14055
  \u{1F513} Cleared ${count} MCP pin(s).`));
13320
14056
  console.log(chalk18.gray(" Next connection to each server will re-pin.\n"));
@@ -13465,9 +14201,9 @@ init_config();
13465
14201
  init_policy();
13466
14202
  init_dlp();
13467
14203
  import chalk21 from "chalk";
13468
- import fs31 from "fs";
13469
- import path34 from "path";
13470
- import os27 from "os";
14204
+ import fs33 from "fs";
14205
+ import path36 from "path";
14206
+ import os29 from "os";
13471
14207
  var CLAUDE_PRICING2 = {
13472
14208
  "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13473
14209
  "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
@@ -13535,7 +14271,7 @@ function buildRuleSources() {
13535
14271
  return sources;
13536
14272
  }
13537
14273
  function scanClaudeHistory(startDate) {
13538
- const projectsDir = path34.join(os27.homedir(), ".claude", "projects");
14274
+ const projectsDir = path36.join(os29.homedir(), ".claude", "projects");
13539
14275
  const result = {
13540
14276
  filesScanned: 0,
13541
14277
  sessions: 0,
@@ -13547,25 +14283,25 @@ function scanClaudeHistory(startDate) {
13547
14283
  firstDate: null,
13548
14284
  lastDate: null
13549
14285
  };
13550
- if (!fs31.existsSync(projectsDir)) return result;
14286
+ if (!fs33.existsSync(projectsDir)) return result;
13551
14287
  let projDirs;
13552
14288
  try {
13553
- projDirs = fs31.readdirSync(projectsDir);
14289
+ projDirs = fs33.readdirSync(projectsDir);
13554
14290
  } catch {
13555
14291
  return result;
13556
14292
  }
13557
14293
  const ruleSources = buildRuleSources();
13558
14294
  for (const proj of projDirs) {
13559
- const projPath = path34.join(projectsDir, proj);
14295
+ const projPath = path36.join(projectsDir, proj);
13560
14296
  try {
13561
- if (!fs31.statSync(projPath).isDirectory()) continue;
14297
+ if (!fs33.statSync(projPath).isDirectory()) continue;
13562
14298
  } catch {
13563
14299
  continue;
13564
14300
  }
13565
- const projLabel = decodeURIComponent(proj).replace(os27.homedir(), "~").slice(0, 40);
14301
+ const projLabel = decodeURIComponent(proj).replace(os29.homedir(), "~").slice(0, 40);
13566
14302
  let files;
13567
14303
  try {
13568
- files = fs31.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
14304
+ files = fs33.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
13569
14305
  } catch {
13570
14306
  continue;
13571
14307
  }
@@ -13574,7 +14310,7 @@ function scanClaudeHistory(startDate) {
13574
14310
  result.sessions++;
13575
14311
  let raw;
13576
14312
  try {
13577
- raw = fs31.readFileSync(path34.join(projPath, file), "utf-8");
14313
+ raw = fs33.readFileSync(path36.join(projPath, file), "utf-8");
13578
14314
  } catch {
13579
14315
  continue;
13580
14316
  }
@@ -13615,6 +14351,9 @@ function scanClaudeHistory(startDate) {
13615
14351
  if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
13616
14352
  result.bashCalls++;
13617
14353
  }
14354
+ const rawCmd = String(input.command ?? "").trimStart();
14355
+ if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd))
14356
+ continue;
13618
14357
  const dlpMatch = scanArgs(input);
13619
14358
  if (dlpMatch) {
13620
14359
  const isDupe = result.dlpFindings.some(
@@ -13632,6 +14371,7 @@ function scanClaudeHistory(startDate) {
13632
14371
  }
13633
14372
  for (const source of ruleSources) {
13634
14373
  const { rule } = source;
14374
+ if (rule.verdict === "allow") continue;
13635
14375
  if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
13636
14376
  if (!evaluateSmartConditions(input, rule)) continue;
13637
14377
  const inputPreview = preview(input, 120);
@@ -13667,8 +14407,8 @@ function registerScanCommand(program2) {
13667
14407
  console.log("");
13668
14408
  console.log(chalk21.cyan.bold("\u{1F50D} node9 scan") + chalk21.dim(" \u2014 what would node9 catch?"));
13669
14409
  console.log("");
13670
- const projectsDir = path34.join(os27.homedir(), ".claude", "projects");
13671
- if (!fs31.existsSync(projectsDir)) {
14410
+ const projectsDir = path36.join(os29.homedir(), ".claude", "projects");
14411
+ if (!fs33.existsSync(projectsDir)) {
13672
14412
  console.log(chalk21.yellow(" No Claude history found at ~/.claude/projects/"));
13673
14413
  console.log(chalk21.gray(" Install Claude Code, run a few sessions, then try again.\n"));
13674
14414
  return;
@@ -13780,8 +14520,8 @@ function registerScanCommand(program2) {
13780
14520
  );
13781
14521
  console.log("");
13782
14522
  }
13783
- const auditLog = path34.join(os27.homedir(), ".node9", "audit.log");
13784
- if (fs31.existsSync(auditLog)) {
14523
+ const auditLog = path36.join(os29.homedir(), ".node9", "audit.log");
14524
+ if (fs33.existsSync(auditLog)) {
13785
14525
  console.log(chalk21.green(" \u2705 node9 is active \u2014 future sessions are protected."));
13786
14526
  console.log(
13787
14527
  chalk21.dim(" Run ") + chalk21.cyan("node9 report") + chalk21.dim(" to see live stats.")
@@ -13798,9 +14538,9 @@ function registerScanCommand(program2) {
13798
14538
 
13799
14539
  // src/cli/commands/sessions.ts
13800
14540
  import chalk22 from "chalk";
13801
- import fs32 from "fs";
13802
- import path35 from "path";
13803
- import os28 from "os";
14541
+ import fs34 from "fs";
14542
+ import path37 from "path";
14543
+ import os30 from "os";
13804
14544
  var CLAUDE_PRICING3 = {
13805
14545
  "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13806
14546
  "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
@@ -13825,10 +14565,10 @@ function encodeProjectPath(projectPath) {
13825
14565
  }
13826
14566
  function sessionJsonlPath(projectPath, sessionId) {
13827
14567
  const encoded = encodeProjectPath(projectPath);
13828
- return path35.join(os28.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
14568
+ return path37.join(os30.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
13829
14569
  }
13830
14570
  function projectLabel(projectPath) {
13831
- return projectPath.replace(os28.homedir(), "~");
14571
+ return projectPath.replace(os30.homedir(), "~");
13832
14572
  }
13833
14573
  function parseHistoryLines(lines) {
13834
14574
  const entries = [];
@@ -13897,10 +14637,10 @@ function parseSessionLines(lines) {
13897
14637
  return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
13898
14638
  }
13899
14639
  function loadAuditEntries(auditPath) {
13900
- const aPath = auditPath ?? path35.join(os28.homedir(), ".node9", "audit.log");
14640
+ const aPath = auditPath ?? path37.join(os30.homedir(), ".node9", "audit.log");
13901
14641
  let raw;
13902
14642
  try {
13903
- raw = fs32.readFileSync(aPath, "utf-8");
14643
+ raw = fs34.readFileSync(aPath, "utf-8");
13904
14644
  } catch {
13905
14645
  return [];
13906
14646
  }
@@ -13936,10 +14676,10 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
13936
14676
  return result;
13937
14677
  }
13938
14678
  function buildSessions(days, historyPath) {
13939
- const hPath = historyPath ?? path35.join(os28.homedir(), ".claude", "history.jsonl");
14679
+ const hPath = historyPath ?? path37.join(os30.homedir(), ".claude", "history.jsonl");
13940
14680
  let historyRaw;
13941
14681
  try {
13942
- historyRaw = fs32.readFileSync(hPath, "utf-8");
14682
+ historyRaw = fs34.readFileSync(hPath, "utf-8");
13943
14683
  } catch {
13944
14684
  return [];
13945
14685
  }
@@ -13964,7 +14704,7 @@ function buildSessions(days, historyPath) {
13964
14704
  const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
13965
14705
  let sessionLines = [];
13966
14706
  try {
13967
- sessionLines = fs32.readFileSync(jsonlFile, "utf-8").split("\n");
14707
+ sessionLines = fs34.readFileSync(jsonlFile, "utf-8").split("\n");
13968
14708
  } catch {
13969
14709
  }
13970
14710
  const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
@@ -14215,8 +14955,8 @@ function registerSessionsCommand(program2) {
14215
14955
  console.log("");
14216
14956
  console.log(chalk22.cyan.bold("\u{1F4CB} node9 sessions") + chalk22.dim(" \u2014 what your AI agent did"));
14217
14957
  console.log("");
14218
- const historyPath = path35.join(os28.homedir(), ".claude", "history.jsonl");
14219
- if (!fs32.existsSync(historyPath)) {
14958
+ const historyPath = path37.join(os30.homedir(), ".claude", "history.jsonl");
14959
+ if (!fs34.existsSync(historyPath)) {
14220
14960
  console.log(chalk22.yellow(" No Claude session history found at ~/.claude/history.jsonl"));
14221
14961
  console.log(chalk22.gray(" Install Claude Code, run a few sessions, then try again.\n"));
14222
14962
  return;
@@ -14246,22 +14986,233 @@ function registerSessionsCommand(program2) {
14246
14986
  });
14247
14987
  }
14248
14988
 
14989
+ // src/cli/commands/skill-pin.ts
14990
+ import chalk23 from "chalk";
14991
+ import fs35 from "fs";
14992
+ import os31 from "os";
14993
+ import path38 from "path";
14994
+ function wipeSkillSessions() {
14995
+ try {
14996
+ fs35.rmSync(path38.join(os31.homedir(), ".node9", "skill-sessions"), {
14997
+ recursive: true,
14998
+ force: true
14999
+ });
15000
+ } catch {
15001
+ }
15002
+ }
15003
+ function registerSkillPinCommand(program2) {
15004
+ const skillCmd = program2.command("skill").description("Manage skill pinning (supply chain & update drift defense, AST 02 + AST 07)");
15005
+ const pinSubCmd = skillCmd.command("pin").description("Manage pinned skill roots");
15006
+ pinSubCmd.command("list").description("Show all pinned skill roots and their content hashes").action(() => {
15007
+ const result = readSkillPinsSafe();
15008
+ if (!result.ok) {
15009
+ if (result.reason === "missing") {
15010
+ console.log(chalk23.gray("\nNo skill roots are pinned yet."));
15011
+ console.log(
15012
+ chalk23.gray("Pins are created automatically on the first tool call of each session.\n")
15013
+ );
15014
+ return;
15015
+ }
15016
+ console.error(chalk23.red(`
15017
+ \u274C Pin file is corrupt: ${result.detail}`));
15018
+ console.error(chalk23.yellow(" Run: node9 skill pin reset\n"));
15019
+ process.exit(1);
15020
+ }
15021
+ const entries = Object.entries(result.pins.roots);
15022
+ if (entries.length === 0) {
15023
+ console.log(chalk23.gray("\nNo skill roots are pinned yet.\n"));
15024
+ return;
15025
+ }
15026
+ console.log(chalk23.bold("\n\u{1F512} Pinned Skill Roots\n"));
15027
+ for (const [key, entry] of entries) {
15028
+ const missing = entry.exists ? "" : chalk23.yellow(" (not present at pin time)");
15029
+ console.log(` ${chalk23.cyan(key)} ${chalk23.gray(entry.rootPath)}${missing}`);
15030
+ console.log(` Files (${entry.fileCount})`);
15031
+ console.log(` Hash: ${chalk23.gray(entry.contentHash.slice(0, 16))}...`);
15032
+ console.log(` Pinned: ${chalk23.gray(entry.pinnedAt)}
15033
+ `);
15034
+ }
15035
+ });
15036
+ pinSubCmd.command("update <rootKey>").description("Remove a pin so the next session re-pins with current state").action((rootKey) => {
15037
+ let pins;
15038
+ try {
15039
+ pins = readSkillPins();
15040
+ } catch {
15041
+ console.error(chalk23.red("\n\u274C Pin file is corrupt."));
15042
+ console.error(chalk23.yellow(" Run: node9 skill pin reset\n"));
15043
+ process.exit(1);
15044
+ }
15045
+ if (!pins.roots[rootKey]) {
15046
+ console.error(chalk23.red(`
15047
+ \u274C No pin found for root key "${rootKey}"
15048
+ `));
15049
+ console.error(`Run ${chalk23.cyan("node9 skill pin list")} to see pinned roots.
15050
+ `);
15051
+ process.exit(1);
15052
+ }
15053
+ const rootPath = pins.roots[rootKey].rootPath;
15054
+ removePin(rootKey);
15055
+ wipeSkillSessions();
15056
+ console.log(chalk23.green(`
15057
+ \u{1F513} Pin removed for ${chalk23.cyan(rootKey)}`));
15058
+ console.log(chalk23.gray(` ${rootPath}`));
15059
+ console.log(chalk23.gray(" Next session will re-pin with current state.\n"));
15060
+ });
15061
+ pinSubCmd.command("reset").description("Clear all skill pins and wipe session verification flags").action(() => {
15062
+ const result = readSkillPinsSafe();
15063
+ if (!result.ok && result.reason === "missing") {
15064
+ wipeSkillSessions();
15065
+ console.log(chalk23.gray("\nNo pins to clear.\n"));
15066
+ return;
15067
+ }
15068
+ const count = result.ok ? Object.keys(result.pins.roots).length : "?";
15069
+ clearAllPins();
15070
+ wipeSkillSessions();
15071
+ console.log(chalk23.green(`
15072
+ \u{1F513} Cleared ${count} skill pin(s).`));
15073
+ console.log(chalk23.gray(" Next session will re-pin with current state.\n"));
15074
+ });
15075
+ }
15076
+
15077
+ // src/cli/commands/dlp.ts
15078
+ import chalk24 from "chalk";
15079
+ import fs36 from "fs";
15080
+ import path39 from "path";
15081
+ import os32 from "os";
15082
+ var AUDIT_LOG = path39.join(os32.homedir(), ".node9", "audit.log");
15083
+ var RESOLVED_FILE = path39.join(os32.homedir(), ".node9", "dlp-resolved.json");
15084
+ var ANSI_RE = /\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g;
15085
+ function stripAnsi(s) {
15086
+ return s.replace(ANSI_RE, "");
15087
+ }
15088
+ function loadResolved() {
15089
+ try {
15090
+ const raw = JSON.parse(fs36.readFileSync(RESOLVED_FILE, "utf-8"));
15091
+ return new Set(raw);
15092
+ } catch {
15093
+ return /* @__PURE__ */ new Set();
15094
+ }
15095
+ }
15096
+ function saveResolved(resolved) {
15097
+ try {
15098
+ fs36.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
15099
+ } catch {
15100
+ }
15101
+ }
15102
+ function loadDlpFindings() {
15103
+ if (!fs36.existsSync(AUDIT_LOG)) return [];
15104
+ return fs36.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
15105
+ if (!line.trim()) return [];
15106
+ try {
15107
+ const e = JSON.parse(line);
15108
+ return e.source === "response-dlp" ? [e] : [];
15109
+ } catch {
15110
+ return [];
15111
+ }
15112
+ });
15113
+ }
15114
+ function entryKey(e) {
15115
+ return `${e.ts}:${e.dlpPattern}:${e.dlpSample}`;
15116
+ }
15117
+ function fmtDate3(ts) {
15118
+ try {
15119
+ return new Date(ts).toLocaleDateString("en-US", {
15120
+ month: "short",
15121
+ day: "numeric",
15122
+ year: "numeric"
15123
+ });
15124
+ } catch {
15125
+ return ts.slice(0, 10);
15126
+ }
15127
+ }
15128
+ function registerDlpCommand(program2) {
15129
+ const cmd = program2.command("dlp").description("Show secrets detected in Claude response text and mark them resolved");
15130
+ cmd.command("resolve").description("Mark all current DLP findings as resolved").action(() => {
15131
+ const findings = loadDlpFindings();
15132
+ if (findings.length === 0) {
15133
+ console.log(chalk24.green("\n \u2705 No response-DLP findings to resolve.\n"));
15134
+ return;
15135
+ }
15136
+ const resolved = loadResolved();
15137
+ for (const e of findings) resolved.add(entryKey(e));
15138
+ saveResolved(resolved);
15139
+ console.log(
15140
+ chalk24.green(
15141
+ `
15142
+ \u2705 ${findings.length} finding${findings.length !== 1 ? "s" : ""} marked as resolved.
15143
+ `
15144
+ )
15145
+ );
15146
+ });
15147
+ cmd.action(() => {
15148
+ const findings = loadDlpFindings();
15149
+ const resolved = loadResolved();
15150
+ const open = findings.filter((e) => !resolved.has(entryKey(e)));
15151
+ const resolvedCount = findings.length - open.length;
15152
+ console.log("");
15153
+ console.log(
15154
+ chalk24.bold.cyan("\u{1F510} node9 dlp") + chalk24.dim(" \u2014 secrets found in Claude response text")
15155
+ );
15156
+ console.log("");
15157
+ if (open.length === 0) {
15158
+ if (resolvedCount > 0) {
15159
+ console.log(chalk24.green(` \u2705 No open findings \xB7 ${resolvedCount} previously resolved`));
15160
+ } else {
15161
+ console.log(
15162
+ chalk24.green(" \u2705 No findings \u2014 Claude has not leaked secrets in response text")
15163
+ );
15164
+ }
15165
+ console.log("");
15166
+ return;
15167
+ }
15168
+ console.log(
15169
+ chalk24.bgRed.white.bold(` \u26A0\uFE0F ${open.length} open finding${open.length !== 1 ? "s" : ""} `) + chalk24.dim(resolvedCount > 0 ? ` (${resolvedCount} resolved)` : "")
15170
+ );
15171
+ console.log("");
15172
+ console.log(
15173
+ chalk24.dim(" These secrets were included in Claude's response text \u2014 NOT blocked.")
15174
+ );
15175
+ console.log(chalk24.dim(" Rotate each affected key immediately.\n"));
15176
+ for (const e of open) {
15177
+ console.log(
15178
+ " " + chalk24.red("\u25CF") + " " + chalk24.white(e.dlpPattern ?? "Secret") + chalk24.dim(" " + fmtDate3(e.ts))
15179
+ );
15180
+ if (e.dlpSample) {
15181
+ console.log(" " + chalk24.dim("Sample: ") + chalk24.yellow(stripAnsi(e.dlpSample)));
15182
+ }
15183
+ if (e.project) {
15184
+ console.log(" " + chalk24.dim("Project: ") + chalk24.dim(stripAnsi(e.project)));
15185
+ }
15186
+ console.log("");
15187
+ }
15188
+ console.log(" " + chalk24.bold("Next steps:"));
15189
+ console.log(" " + chalk24.cyan("1.") + " Rotate any exposed keys shown above");
15190
+ console.log(
15191
+ " " + chalk24.cyan("2.") + " Run " + chalk24.white("node9 dlp resolve") + " to acknowledge"
15192
+ );
15193
+ console.log(
15194
+ " " + chalk24.cyan("3.") + " Run " + chalk24.white("node9 report") + " for full audit history"
15195
+ );
15196
+ console.log("");
15197
+ });
15198
+ }
15199
+
14249
15200
  // src/cli.ts
14250
15201
  var { version } = JSON.parse(
14251
- fs35.readFileSync(path38.join(__dirname, "../package.json"), "utf-8")
15202
+ fs39.readFileSync(path42.join(__dirname, "../package.json"), "utf-8")
14252
15203
  );
14253
15204
  var program = new Command();
14254
15205
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
14255
15206
  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) => {
14256
15207
  const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
14257
- const credPath = path38.join(os31.homedir(), ".node9", "credentials.json");
14258
- if (!fs35.existsSync(path38.dirname(credPath)))
14259
- fs35.mkdirSync(path38.dirname(credPath), { recursive: true });
15208
+ const credPath = path42.join(os35.homedir(), ".node9", "credentials.json");
15209
+ if (!fs39.existsSync(path42.dirname(credPath)))
15210
+ fs39.mkdirSync(path42.dirname(credPath), { recursive: true });
14260
15211
  const profileName = options.profile || "default";
14261
15212
  let existingCreds = {};
14262
15213
  try {
14263
- if (fs35.existsSync(credPath)) {
14264
- const raw = JSON.parse(fs35.readFileSync(credPath, "utf-8"));
15214
+ if (fs39.existsSync(credPath)) {
15215
+ const raw = JSON.parse(fs39.readFileSync(credPath, "utf-8"));
14265
15216
  if (raw.apiKey) {
14266
15217
  existingCreds = {
14267
15218
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
@@ -14273,13 +15224,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
14273
15224
  } catch {
14274
15225
  }
14275
15226
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
14276
- fs35.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
15227
+ fs39.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
14277
15228
  if (profileName === "default") {
14278
- const configPath = path38.join(os31.homedir(), ".node9", "config.json");
15229
+ const configPath = path42.join(os35.homedir(), ".node9", "config.json");
14279
15230
  let config = {};
14280
15231
  try {
14281
- if (fs35.existsSync(configPath))
14282
- config = JSON.parse(fs35.readFileSync(configPath, "utf-8"));
15232
+ if (fs39.existsSync(configPath))
15233
+ config = JSON.parse(fs39.readFileSync(configPath, "utf-8"));
14283
15234
  } catch {
14284
15235
  }
14285
15236
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -14294,47 +15245,61 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
14294
15245
  approvers.cloud = false;
14295
15246
  }
14296
15247
  s.approvers = approvers;
14297
- if (!fs35.existsSync(path38.dirname(configPath)))
14298
- fs35.mkdirSync(path38.dirname(configPath), { recursive: true });
14299
- fs35.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
15248
+ if (!fs39.existsSync(path42.dirname(configPath)))
15249
+ fs39.mkdirSync(path42.dirname(configPath), { recursive: true });
15250
+ fs39.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
14300
15251
  }
14301
15252
  if (options.profile && profileName !== "default") {
14302
- console.log(chalk24.green(`\u2705 Profile "${profileName}" saved`));
14303
- console.log(chalk24.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
15253
+ console.log(chalk26.green(`\u2705 Profile "${profileName}" saved`));
15254
+ console.log(chalk26.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
14304
15255
  } else if (options.local) {
14305
- console.log(chalk24.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
14306
- console.log(chalk24.gray(` All decisions stay on this machine.`));
15256
+ console.log(chalk26.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
15257
+ console.log(chalk26.gray(` All decisions stay on this machine.`));
14307
15258
  } else {
14308
- console.log(chalk24.green(`\u2705 Logged in \u2014 agent mode`));
14309
- console.log(chalk24.gray(` Team policy enforced for all calls via Node9 cloud.`));
15259
+ console.log(chalk26.green(`\u2705 Logged in \u2014 agent mode`));
15260
+ console.log(chalk26.gray(` Team policy enforced for all calls via Node9 cloud.`));
14310
15261
  }
14311
15262
  });
14312
- program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor windsurf vscode hud").argument("<target>", "The agent to protect: claude | gemini | cursor | windsurf | vscode | hud").action(async (target) => {
15263
+ program.command("addto").description("Integrate Node9 with an AI agent").addHelpText(
15264
+ "after",
15265
+ "\n Supported targets: claude gemini cursor codex windsurf vscode hud"
15266
+ ).argument(
15267
+ "<target>",
15268
+ "The agent to protect: claude | gemini | cursor | codex | windsurf | vscode | hud"
15269
+ ).action(async (target) => {
14313
15270
  if (target === "gemini") return await setupGemini();
14314
15271
  if (target === "claude") return await setupClaude();
14315
15272
  if (target === "cursor") return await setupCursor();
15273
+ if (target === "codex") return await setupCodex();
14316
15274
  if (target === "windsurf") return await setupWindsurf();
14317
15275
  if (target === "vscode") return await setupVSCode();
14318
15276
  if (target === "hud") return setupHud();
14319
15277
  console.error(
14320
- chalk24.red(
14321
- `Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
15278
+ chalk26.red(
15279
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
14322
15280
  )
14323
15281
  );
14324
15282
  process.exit(1);
14325
15283
  });
14326
- program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor windsurf vscode hud").argument("[target]", "The agent to protect: claude | gemini | cursor | windsurf | vscode | hud").action(async (target) => {
15284
+ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText(
15285
+ "after",
15286
+ "\n Supported targets: claude gemini cursor codex windsurf vscode hud"
15287
+ ).argument(
15288
+ "[target]",
15289
+ "The agent to protect: claude | gemini | cursor | codex | windsurf | vscode | hud"
15290
+ ).action(async (target) => {
14327
15291
  if (!target) {
14328
- console.log(chalk24.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
14329
- console.log(" Usage: " + chalk24.white("node9 setup <target>") + "\n");
15292
+ console.log(chalk26.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
15293
+ console.log(" Usage: " + chalk26.white("node9 setup <target>") + "\n");
14330
15294
  console.log(" Targets:");
14331
- console.log(" " + chalk24.green("claude") + " \u2014 Claude Code (hook mode)");
14332
- console.log(" " + chalk24.green("gemini") + " \u2014 Gemini CLI (hook mode)");
14333
- console.log(" " + chalk24.green("cursor") + " \u2014 Cursor (MCP proxy)");
14334
- console.log(" " + chalk24.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
14335
- console.log(" " + chalk24.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
15295
+ console.log(" " + chalk26.green("claude") + " \u2014 Claude Code (hook mode)");
15296
+ console.log(" " + chalk26.green("gemini") + " \u2014 Gemini CLI (hook mode)");
15297
+ console.log(" " + chalk26.green("cursor") + " \u2014 Cursor (MCP proxy)");
15298
+ console.log(" " + chalk26.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
15299
+ console.log(" " + chalk26.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
15300
+ console.log(" " + chalk26.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
14336
15301
  process.stdout.write(
14337
- " " + chalk24.green("hud") + " \u2014 Claude Code security statusline\n"
15302
+ " " + chalk26.green("hud") + " \u2014 Claude Code security statusline\n"
14338
15303
  );
14339
15304
  console.log("");
14340
15305
  return;
@@ -14343,61 +15308,67 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
14343
15308
  if (t === "gemini") return await setupGemini();
14344
15309
  if (t === "claude") return await setupClaude();
14345
15310
  if (t === "cursor") return await setupCursor();
15311
+ if (t === "codex") return await setupCodex();
14346
15312
  if (t === "windsurf") return await setupWindsurf();
14347
15313
  if (t === "vscode") return await setupVSCode();
14348
15314
  if (t === "hud") return setupHud();
14349
15315
  console.error(
14350
- chalk24.red(
14351
- `Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
15316
+ chalk26.red(
15317
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
14352
15318
  )
14353
15319
  );
14354
15320
  process.exit(1);
14355
15321
  });
14356
- program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor windsurf vscode hud").argument(
15322
+ program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText(
15323
+ "after",
15324
+ "\n Supported targets: claude gemini cursor codex windsurf vscode hud"
15325
+ ).argument(
14357
15326
  "<target>",
14358
- "The agent to remove from: claude | gemini | cursor | windsurf | vscode | hud"
15327
+ "The agent to remove from: claude | gemini | cursor | codex | windsurf | vscode | hud"
14359
15328
  ).action((target) => {
14360
15329
  let fn;
14361
15330
  if (target === "claude") fn = teardownClaude;
14362
15331
  else if (target === "gemini") fn = teardownGemini;
14363
15332
  else if (target === "cursor") fn = teardownCursor;
15333
+ else if (target === "codex") fn = teardownCodex;
14364
15334
  else if (target === "windsurf") fn = teardownWindsurf;
14365
15335
  else if (target === "vscode") fn = teardownVSCode;
14366
15336
  else if (target === "hud") fn = teardownHud;
14367
15337
  else {
14368
15338
  console.error(
14369
- chalk24.red(
14370
- `Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
15339
+ chalk26.red(
15340
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
14371
15341
  )
14372
15342
  );
14373
15343
  process.exit(1);
14374
15344
  }
14375
- console.log(chalk24.cyan(`
15345
+ console.log(chalk26.cyan(`
14376
15346
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
14377
15347
  `));
14378
15348
  try {
14379
15349
  fn();
14380
15350
  } catch (err2) {
14381
- console.error(chalk24.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
15351
+ console.error(chalk26.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
14382
15352
  process.exit(1);
14383
15353
  }
14384
- console.log(chalk24.gray("\n Restart the agent for changes to take effect."));
15354
+ console.log(chalk26.gray("\n Restart the agent for changes to take effect."));
14385
15355
  });
14386
15356
  program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
14387
- console.log(chalk24.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
14388
- console.log(chalk24.bold("Stopping daemon..."));
15357
+ console.log(chalk26.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
15358
+ console.log(chalk26.bold("Stopping daemon..."));
14389
15359
  try {
14390
15360
  stopDaemon();
14391
- console.log(chalk24.green(" \u2705 Daemon stopped"));
15361
+ console.log(chalk26.green(" \u2705 Daemon stopped"));
14392
15362
  } catch {
14393
- console.log(chalk24.blue(" \u2139\uFE0F Daemon was not running"));
15363
+ console.log(chalk26.blue(" \u2139\uFE0F Daemon was not running"));
14394
15364
  }
14395
- console.log(chalk24.bold("\nRemoving hooks..."));
15365
+ console.log(chalk26.bold("\nRemoving hooks..."));
14396
15366
  let teardownFailed = false;
14397
15367
  for (const [label, fn] of [
14398
15368
  ["Claude", teardownClaude],
14399
15369
  ["Gemini", teardownGemini],
14400
15370
  ["Cursor", teardownCursor],
15371
+ ["Codex", teardownCodex],
14401
15372
  ["Windsurf", teardownWindsurf],
14402
15373
  ["VSCode", teardownVSCode]
14403
15374
  ]) {
@@ -14406,45 +15377,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
14406
15377
  } catch (err2) {
14407
15378
  teardownFailed = true;
14408
15379
  console.error(
14409
- chalk24.red(
15380
+ chalk26.red(
14410
15381
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
14411
15382
  )
14412
15383
  );
14413
15384
  }
14414
15385
  }
14415
15386
  if (options.purge) {
14416
- const node9Dir = path38.join(os31.homedir(), ".node9");
14417
- if (fs35.existsSync(node9Dir)) {
15387
+ const node9Dir = path42.join(os35.homedir(), ".node9");
15388
+ if (fs39.existsSync(node9Dir)) {
14418
15389
  const confirmed = await confirm2({
14419
15390
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
14420
15391
  default: false
14421
15392
  });
14422
15393
  if (confirmed) {
14423
- fs35.rmSync(node9Dir, { recursive: true });
14424
- if (fs35.existsSync(node9Dir)) {
15394
+ fs39.rmSync(node9Dir, { recursive: true });
15395
+ if (fs39.existsSync(node9Dir)) {
14425
15396
  console.error(
14426
- chalk24.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
15397
+ chalk26.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
14427
15398
  );
14428
15399
  } else {
14429
- console.log(chalk24.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
15400
+ console.log(chalk26.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
14430
15401
  }
14431
15402
  } else {
14432
- console.log(chalk24.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
15403
+ console.log(chalk26.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
14433
15404
  }
14434
15405
  } else {
14435
- console.log(chalk24.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
15406
+ console.log(chalk26.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
14436
15407
  }
14437
15408
  } else {
14438
15409
  console.log(
14439
- chalk24.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
15410
+ chalk26.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
14440
15411
  );
14441
15412
  }
14442
15413
  if (teardownFailed) {
14443
- console.error(chalk24.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
15414
+ console.error(chalk26.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
14444
15415
  process.exit(1);
14445
15416
  }
14446
- console.log(chalk24.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
14447
- console.log(chalk24.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
15417
+ console.log(chalk26.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
15418
+ console.log(chalk26.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
14448
15419
  });
14449
15420
  registerDoctorCommand(program, version);
14450
15421
  program.command("explain").description(
@@ -14457,7 +15428,7 @@ program.command("explain").description(
14457
15428
  try {
14458
15429
  args = JSON.parse(trimmed);
14459
15430
  } catch {
14460
- console.error(chalk24.red(`
15431
+ console.error(chalk26.red(`
14461
15432
  \u274C Invalid JSON: ${trimmed}
14462
15433
  `));
14463
15434
  process.exit(1);
@@ -14468,54 +15439,54 @@ program.command("explain").description(
14468
15439
  }
14469
15440
  const result = await explainPolicy(tool, args);
14470
15441
  console.log("");
14471
- console.log(chalk24.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
15442
+ console.log(chalk26.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
14472
15443
  console.log("");
14473
- console.log(` ${chalk24.bold("Tool:")} ${chalk24.white(result.tool)}`);
15444
+ console.log(` ${chalk26.bold("Tool:")} ${chalk26.white(result.tool)}`);
14474
15445
  if (argsRaw) {
14475
15446
  const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
14476
- console.log(` ${chalk24.bold("Input:")} ${chalk24.gray(preview2)}`);
15447
+ console.log(` ${chalk26.bold("Input:")} ${chalk26.gray(preview2)}`);
14477
15448
  }
14478
15449
  console.log("");
14479
- console.log(chalk24.bold("Config Sources (Waterfall):"));
15450
+ console.log(chalk26.bold("Config Sources (Waterfall):"));
14480
15451
  for (const tier of result.waterfall) {
14481
- const num3 = chalk24.gray(` ${tier.tier}.`);
15452
+ const num3 = chalk26.gray(` ${tier.tier}.`);
14482
15453
  const label = tier.label.padEnd(16);
14483
15454
  let statusStr;
14484
15455
  if (tier.tier === 1) {
14485
- statusStr = chalk24.gray(tier.note ?? "");
15456
+ statusStr = chalk26.gray(tier.note ?? "");
14486
15457
  } else if (tier.status === "active") {
14487
- const loc = tier.path ? chalk24.gray(tier.path) : "";
14488
- const note = tier.note ? chalk24.gray(`(${tier.note})`) : "";
14489
- statusStr = chalk24.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
15458
+ const loc = tier.path ? chalk26.gray(tier.path) : "";
15459
+ const note = tier.note ? chalk26.gray(`(${tier.note})`) : "";
15460
+ statusStr = chalk26.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
14490
15461
  } else {
14491
- statusStr = chalk24.gray("\u25CB " + (tier.note ?? "not found"));
15462
+ statusStr = chalk26.gray("\u25CB " + (tier.note ?? "not found"));
14492
15463
  }
14493
- console.log(`${num3} ${chalk24.white(label)} ${statusStr}`);
15464
+ console.log(`${num3} ${chalk26.white(label)} ${statusStr}`);
14494
15465
  }
14495
15466
  console.log("");
14496
- console.log(chalk24.bold("Policy Evaluation:"));
15467
+ console.log(chalk26.bold("Policy Evaluation:"));
14497
15468
  for (const step of result.steps) {
14498
15469
  const isFinal = step.isFinal;
14499
15470
  let icon;
14500
- if (step.outcome === "allow") icon = chalk24.green(" \u2705");
14501
- else if (step.outcome === "review") icon = chalk24.red(" \u{1F534}");
14502
- else if (step.outcome === "skip") icon = chalk24.gray(" \u2500 ");
14503
- else icon = chalk24.gray(" \u25CB ");
15471
+ if (step.outcome === "allow") icon = chalk26.green(" \u2705");
15472
+ else if (step.outcome === "review") icon = chalk26.red(" \u{1F534}");
15473
+ else if (step.outcome === "skip") icon = chalk26.gray(" \u2500 ");
15474
+ else icon = chalk26.gray(" \u25CB ");
14504
15475
  const name = step.name.padEnd(18);
14505
- const nameStr = isFinal ? chalk24.white.bold(name) : chalk24.white(name);
14506
- const detail = isFinal ? chalk24.white(step.detail) : chalk24.gray(step.detail);
14507
- const arrow = isFinal ? chalk24.yellow(" \u2190 STOP") : "";
15476
+ const nameStr = isFinal ? chalk26.white.bold(name) : chalk26.white(name);
15477
+ const detail = isFinal ? chalk26.white(step.detail) : chalk26.gray(step.detail);
15478
+ const arrow = isFinal ? chalk26.yellow(" \u2190 STOP") : "";
14508
15479
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
14509
15480
  }
14510
15481
  console.log("");
14511
15482
  if (result.decision === "allow") {
14512
- console.log(chalk24.green.bold(" Decision: \u2705 ALLOW") + chalk24.gray(" \u2014 no approval needed"));
15483
+ console.log(chalk26.green.bold(" Decision: \u2705 ALLOW") + chalk26.gray(" \u2014 no approval needed"));
14513
15484
  } else {
14514
15485
  console.log(
14515
- chalk24.red.bold(" Decision: \u{1F534} REVIEW") + chalk24.gray(" \u2014 human approval required")
15486
+ chalk26.red.bold(" Decision: \u{1F534} REVIEW") + chalk26.gray(" \u2014 human approval required")
14516
15487
  );
14517
15488
  if (result.blockedByLabel) {
14518
- console.log(chalk24.gray(` Reason: ${result.blockedByLabel}`));
15489
+ console.log(chalk26.gray(` Reason: ${result.blockedByLabel}`));
14519
15490
  }
14520
15491
  }
14521
15492
  console.log("");
@@ -14530,7 +15501,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
14530
15501
  try {
14531
15502
  await startTail2(options);
14532
15503
  } catch (err2) {
14533
- console.error(chalk24.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
15504
+ console.error(chalk26.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
14534
15505
  process.exit(1);
14535
15506
  }
14536
15507
  });
@@ -14538,6 +15509,7 @@ registerWatchCommand(program);
14538
15509
  registerMcpGatewayCommand(program);
14539
15510
  registerMcpServerCommand(program);
14540
15511
  registerMcpPinCommand(program);
15512
+ registerSkillPinCommand(program);
14541
15513
  registerCheckCommand(program);
14542
15514
  registerLogCommand(program);
14543
15515
  program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").addHelpText(
@@ -14562,14 +15534,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
14562
15534
  Run "node9 addto claude" to register it as the statusLine.`
14563
15535
  ).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
14564
15536
  if (subcommand === "debug") {
14565
- const flagFile = path38.join(os31.homedir(), ".node9", "hud-debug");
15537
+ const flagFile = path42.join(os35.homedir(), ".node9", "hud-debug");
14566
15538
  if (state === "on") {
14567
- fs35.mkdirSync(path38.dirname(flagFile), { recursive: true });
14568
- fs35.writeFileSync(flagFile, "");
15539
+ fs39.mkdirSync(path42.dirname(flagFile), { recursive: true });
15540
+ fs39.writeFileSync(flagFile, "");
14569
15541
  console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
14570
15542
  console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
14571
15543
  } else if (state === "off") {
14572
- if (fs35.existsSync(flagFile)) fs35.unlinkSync(flagFile);
15544
+ if (fs39.existsSync(flagFile)) fs39.unlinkSync(flagFile);
14573
15545
  console.log("HUD debug logging disabled.");
14574
15546
  } else {
14575
15547
  console.error("Usage: node9 hud debug on|off");
@@ -14584,7 +15556,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
14584
15556
  const ms = parseDuration(options.duration);
14585
15557
  if (ms === null) {
14586
15558
  console.error(
14587
- chalk24.red(`
15559
+ chalk26.red(`
14588
15560
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
14589
15561
  `)
14590
15562
  );
@@ -14592,20 +15564,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
14592
15564
  }
14593
15565
  pauseNode9(ms, options.duration);
14594
15566
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
14595
- console.log(chalk24.yellow(`
15567
+ console.log(chalk26.yellow(`
14596
15568
  \u23F8 Node9 paused until ${expiresAt}`));
14597
- console.log(chalk24.gray(` All tool calls will be allowed without review.`));
14598
- console.log(chalk24.gray(` Run "node9 resume" to re-enable early.
15569
+ console.log(chalk26.gray(` All tool calls will be allowed without review.`));
15570
+ console.log(chalk26.gray(` Run "node9 resume" to re-enable early.
14599
15571
  `));
14600
15572
  });
14601
15573
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
14602
15574
  const { paused } = checkPause();
14603
15575
  if (!paused) {
14604
- console.log(chalk24.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
15576
+ console.log(chalk26.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
14605
15577
  return;
14606
15578
  }
14607
15579
  resumeNode9();
14608
- console.log(chalk24.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
15580
+ console.log(chalk26.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
14609
15581
  });
14610
15582
  var HOOK_BASED_AGENTS = {
14611
15583
  claude: "claude",
@@ -14618,15 +15590,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
14618
15590
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
14619
15591
  const target = HOOK_BASED_AGENTS[firstArg2];
14620
15592
  console.error(
14621
- chalk24.yellow(`
15593
+ chalk26.yellow(`
14622
15594
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
14623
15595
  );
14624
- console.error(chalk24.white(`
15596
+ console.error(chalk26.white(`
14625
15597
  "${target}" uses its own hook system. Use:`));
14626
15598
  console.error(
14627
- chalk24.green(` node9 addto ${target} `) + chalk24.gray("# one-time setup")
15599
+ chalk26.green(` node9 addto ${target} `) + chalk26.gray("# one-time setup")
14628
15600
  );
14629
- console.error(chalk24.green(` ${target} `) + chalk24.gray("# run normally"));
15601
+ console.error(chalk26.green(` ${target} `) + chalk26.gray("# run normally"));
14630
15602
  process.exit(1);
14631
15603
  }
14632
15604
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -14643,7 +15615,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
14643
15615
  }
14644
15616
  );
14645
15617
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
14646
- console.error(chalk24.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
15618
+ console.error(chalk26.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
14647
15619
  const daemonReady = await autoStartDaemonAndWait();
14648
15620
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
14649
15621
  }
@@ -14656,12 +15628,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
14656
15628
  }
14657
15629
  if (!result.approved) {
14658
15630
  console.error(
14659
- chalk24.red(`
15631
+ chalk26.red(`
14660
15632
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
14661
15633
  );
14662
15634
  process.exit(1);
14663
15635
  }
14664
- console.error(chalk24.green("\n\u2705 Approved \u2014 running command...\n"));
15636
+ console.error(chalk26.green("\n\u2705 Approved \u2014 running command...\n"));
14665
15637
  await runProxy(fullCommand);
14666
15638
  } else {
14667
15639
  program.help();
@@ -14675,14 +15647,15 @@ registerSyncCommand(program);
14675
15647
  registerAgentsCommand(program);
14676
15648
  registerScanCommand(program);
14677
15649
  registerSessionsCommand(program);
15650
+ registerDlpCommand(program);
14678
15651
  if (process.argv[2] !== "daemon") {
14679
15652
  process.on("unhandledRejection", (reason) => {
14680
15653
  const isCheckHook = process.argv[2] === "check";
14681
15654
  if (isCheckHook) {
14682
15655
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
14683
- const logPath = path38.join(os31.homedir(), ".node9", "hook-debug.log");
15656
+ const logPath = path42.join(os35.homedir(), ".node9", "hook-debug.log");
14684
15657
  const msg = reason instanceof Error ? reason.message : String(reason);
14685
- fs35.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
15658
+ fs39.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
14686
15659
  `);
14687
15660
  }
14688
15661
  process.exit(0);