@node9/proxy 1.10.3 → 1.11.0

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 path41 = issue.path.length > 0 ? issue.path.join(".") : "root";
151
+ return ` \u2022 ${path41}: ${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)];
@@ -920,7 +940,8 @@ var init_config = __esm({
920
940
  }
921
941
  ],
922
942
  dlp: { enabled: true, scanIgnoredTools: true },
923
- loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 }
943
+ loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
944
+ skillPinning: { enabled: false, mode: "warn", roots: [] }
924
945
  },
925
946
  environments: {}
926
947
  };
@@ -1729,9 +1750,9 @@ function matchesPattern(text, patterns) {
1729
1750
  const withoutDotSlash = text.replace(/^\.\//, "");
1730
1751
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1731
1752
  }
1732
- function getNestedValue(obj, path39) {
1753
+ function getNestedValue(obj, path41) {
1733
1754
  if (!obj || typeof obj !== "object") return null;
1734
- return path39.split(".").reduce((prev, curr) => prev?.[curr], obj);
1755
+ return path41.split(".").reduce((prev, curr) => prev?.[curr], obj);
1735
1756
  }
1736
1757
  function shouldSnapshot(toolName, args, config) {
1737
1758
  if (!config.settings.enableUndo) return false;
@@ -2324,6 +2345,15 @@ var init_policy = __esm({
2324
2345
  import fs8 from "fs";
2325
2346
  import path9 from "path";
2326
2347
  import os7 from "os";
2348
+ function extractCommandPattern(toolName, args) {
2349
+ const lower = toolName.toLowerCase();
2350
+ if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
2351
+ const a = args;
2352
+ const cmd = typeof a?.["command"] === "string" ? a["command"].trim() : "";
2353
+ if (!cmd) return void 0;
2354
+ const words = cmd.split(/\s+/);
2355
+ return words.slice(0, 2).join(" ");
2356
+ }
2327
2357
  function checkPause() {
2328
2358
  try {
2329
2359
  if (!fs8.existsSync(PAUSED_FILE)) return { paused: false };
@@ -2357,7 +2387,7 @@ function resumeNode9() {
2357
2387
  } catch {
2358
2388
  }
2359
2389
  }
2360
- function getActiveTrustSession(toolName) {
2390
+ function getActiveTrustSession(toolName, args) {
2361
2391
  try {
2362
2392
  if (!fs8.existsSync(TRUST_FILE)) return false;
2363
2393
  const trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
@@ -2366,12 +2396,20 @@ function getActiveTrustSession(toolName) {
2366
2396
  if (active.length !== trust.entries.length) {
2367
2397
  fs8.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
2368
2398
  }
2369
- return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
2399
+ return active.some((e) => {
2400
+ if (!(e.tool === toolName || matchesPattern(toolName, e.tool))) return false;
2401
+ if (e.commandPattern) {
2402
+ const actual = extractCommandPattern(toolName, args) ?? "";
2403
+ return actual === e.commandPattern || actual.startsWith(e.commandPattern + " ");
2404
+ }
2405
+ return true;
2406
+ });
2370
2407
  } catch {
2371
2408
  return false;
2372
2409
  }
2373
2410
  }
2374
- function writeTrustSession(toolName, durationMs) {
2411
+ function writeTrustSession(toolName, durationMs, args) {
2412
+ const commandPattern = extractCommandPattern(toolName, args);
2375
2413
  try {
2376
2414
  let trust = { entries: [] };
2377
2415
  try {
@@ -2381,8 +2419,14 @@ function writeTrustSession(toolName, durationMs) {
2381
2419
  } catch {
2382
2420
  }
2383
2421
  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 });
2422
+ trust.entries = trust.entries.filter(
2423
+ (e) => !(e.tool === toolName && e.commandPattern === commandPattern) && e.expiry > now
2424
+ );
2425
+ trust.entries.push({
2426
+ tool: toolName,
2427
+ ...commandPattern && { commandPattern },
2428
+ expiry: now + durationMs
2429
+ });
2386
2430
  atomicWriteSync(TRUST_FILE, JSON.stringify(trust, null, 2));
2387
2431
  } catch (err2) {
2388
2432
  if (process.env.NODE9_DEBUG === "1") {
@@ -3378,12 +3422,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3378
3422
  };
3379
3423
  }
3380
3424
  }
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
3425
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
3388
3426
  if (policyResult.decision === "allow") {
3389
3427
  if (approvers.cloud && creds?.apiKey)
@@ -3465,6 +3503,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3465
3503
  if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
3466
3504
  return { approved: true };
3467
3505
  }
3506
+ if (!taintWarning && getActiveTrustSession(toolName, args)) {
3507
+ if (approvers.cloud && creds?.apiKey)
3508
+ await auditLocalAllow(toolName, args, "trust", creds, meta);
3509
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
3510
+ return { approved: true, checkedBy: "trust" };
3511
+ }
3468
3512
  if (taintWarning) {
3469
3513
  explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
3470
3514
  riskMetadata = computeRiskMetadata(
@@ -3597,7 +3641,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3597
3641
  riskMetadata?.ruleDescription
3598
3642
  );
3599
3643
  if (decision === "always_allow") {
3600
- writeTrustSession(toolName, 36e5);
3644
+ writeTrustSession(toolName, 36e5, args);
3601
3645
  return { approved: true, checkedBy: "trust" };
3602
3646
  }
3603
3647
  const isApproved = decision === "allow";
@@ -5775,7 +5819,7 @@ function writeGlobalSetting(key, value) {
5775
5819
  config.settings[key] = value;
5776
5820
  atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
5777
5821
  }
5778
- function writeTrustEntry(toolName, durationMs) {
5822
+ function writeTrustEntry(toolName, durationMs, commandPattern) {
5779
5823
  try {
5780
5824
  let trust = { entries: [] };
5781
5825
  try {
@@ -5783,8 +5827,14 @@ function writeTrustEntry(toolName, durationMs) {
5783
5827
  trust = JSON.parse(fs14.readFileSync(TRUST_FILE2, "utf-8"));
5784
5828
  } catch {
5785
5829
  }
5786
- trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
5787
- trust.entries.push({ tool: toolName, expiry: Date.now() + durationMs });
5830
+ trust.entries = trust.entries.filter(
5831
+ (e) => !(e.tool === toolName && e.commandPattern === commandPattern) && e.expiry > Date.now()
5832
+ );
5833
+ trust.entries.push({
5834
+ tool: toolName,
5835
+ ...commandPattern && { commandPattern },
5836
+ expiry: Date.now() + durationMs
5837
+ });
5788
5838
  atomicWriteSync2(TRUST_FILE2, JSON.stringify(trust, null, 2));
5789
5839
  } catch {
5790
5840
  }
@@ -6713,7 +6763,8 @@ data: ${JSON.stringify(item.data)}
6713
6763
  );
6714
6764
  if (decision === "trust" && trustDuration) {
6715
6765
  const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
6716
- writeTrustEntry(entry.toolName, ms);
6766
+ const commandPattern = extractCommandPattern(entry.toolName, entry.args);
6767
+ writeTrustEntry(entry.toolName, ms, commandPattern);
6717
6768
  appendAuditLog({
6718
6769
  toolName: entry.toolName,
6719
6770
  args: entry.args,
@@ -7161,6 +7212,7 @@ var init_server = __esm({
7161
7212
  init_shields();
7162
7213
  init_ui2();
7163
7214
  init_state2();
7215
+ init_state();
7164
7216
  init_patch();
7165
7217
  init_config_schema();
7166
7218
  init_costSync();
@@ -7494,10 +7546,10 @@ __export(tail_exports, {
7494
7546
  startTail: () => startTail
7495
7547
  });
7496
7548
  import http2 from "http";
7497
- import chalk23 from "chalk";
7498
- import fs33 from "fs";
7499
- import os29 from "os";
7500
- import path36 from "path";
7549
+ import chalk24 from "chalk";
7550
+ import fs35 from "fs";
7551
+ import os31 from "os";
7552
+ import path38 from "path";
7501
7553
  import readline5 from "readline";
7502
7554
  import { spawn as spawn10, execSync as execSync3 } from "child_process";
7503
7555
  function getIcon(tool) {
@@ -7520,22 +7572,22 @@ function formatBase(activity) {
7520
7572
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
7521
7573
  const icon = getIcon(activity.tool);
7522
7574
  const toolName = activity.tool.slice(0, 16).padEnd(16);
7523
- const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os29.homedir(), "~");
7575
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os31.homedir(), "~");
7524
7576
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
7525
- return `${chalk23.gray(time)} ${icon} ${chalk23.white.bold(toolName)} ${chalk23.dim(argsPreview)}`;
7577
+ return `${chalk24.gray(time)} ${icon} ${chalk24.white.bold(toolName)} ${chalk24.dim(argsPreview)}`;
7526
7578
  }
7527
7579
  function renderResult(activity, result) {
7528
7580
  const base = formatBase(activity);
7529
7581
  let status;
7530
7582
  if (result.status === "allow") {
7531
- status = chalk23.green("\u2713 ALLOW");
7583
+ status = chalk24.green("\u2713 ALLOW");
7532
7584
  } else if (result.status === "dlp") {
7533
- status = chalk23.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
7585
+ status = chalk24.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
7534
7586
  } else {
7535
- status = chalk23.red("\u2717 BLOCK");
7587
+ status = chalk24.red("\u2717 BLOCK");
7536
7588
  }
7537
7589
  const cost = result.costEstimate ?? activity.costEstimate;
7538
- const costSuffix = cost == null ? "" : chalk23.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
7590
+ const costSuffix = cost == null ? "" : chalk24.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
7539
7591
  if (process.stdout.isTTY) {
7540
7592
  if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
7541
7593
  readline5.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
@@ -7552,19 +7604,19 @@ function renderResult(activity, result) {
7552
7604
  }
7553
7605
  function renderPending(activity) {
7554
7606
  if (!process.stdout.isTTY) return;
7555
- const line = `${formatBase(activity)} ${chalk23.yellow("\u25CF \u2026")}`;
7607
+ const line = `${formatBase(activity)} ${chalk24.yellow("\u25CF \u2026")}`;
7556
7608
  pendingShownForId = activity.id;
7557
7609
  pendingWrappedLines = wrappedLineCount(line);
7558
7610
  process.stdout.write(`${line}\r`);
7559
7611
  }
7560
7612
  async function ensureDaemon() {
7561
7613
  let pidPort = null;
7562
- if (fs33.existsSync(PID_FILE)) {
7614
+ if (fs35.existsSync(PID_FILE)) {
7563
7615
  try {
7564
- const { port } = JSON.parse(fs33.readFileSync(PID_FILE, "utf-8"));
7616
+ const { port } = JSON.parse(fs35.readFileSync(PID_FILE, "utf-8"));
7565
7617
  pidPort = port;
7566
7618
  } catch {
7567
- console.error(chalk23.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
7619
+ console.error(chalk24.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
7568
7620
  }
7569
7621
  }
7570
7622
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -7575,7 +7627,7 @@ async function ensureDaemon() {
7575
7627
  if (res.ok) return checkPort;
7576
7628
  } catch {
7577
7629
  }
7578
- console.log(chalk23.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
7630
+ console.log(chalk24.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
7579
7631
  const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
7580
7632
  detached: true,
7581
7633
  stdio: "ignore",
@@ -7592,7 +7644,7 @@ async function ensureDaemon() {
7592
7644
  } catch {
7593
7645
  }
7594
7646
  }
7595
- console.error(chalk23.red("\u274C Daemon failed to start. Try: node9 daemon start"));
7647
+ console.error(chalk24.red("\u274C Daemon failed to start. Try: node9 daemon start"));
7596
7648
  process.exit(1);
7597
7649
  }
7598
7650
  function postDecisionHttp(id, decision, csrfToken, port, opts) {
@@ -7713,9 +7765,9 @@ function buildRecoveryCardLines(req) {
7713
7765
  ];
7714
7766
  }
7715
7767
  function readApproversFromDisk() {
7716
- const configPath = path36.join(os29.homedir(), ".node9", "config.json");
7768
+ const configPath = path38.join(os31.homedir(), ".node9", "config.json");
7717
7769
  try {
7718
- const raw = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
7770
+ const raw = JSON.parse(fs35.readFileSync(configPath, "utf-8"));
7719
7771
  const settings = raw.settings ?? {};
7720
7772
  return settings.approvers ?? {};
7721
7773
  } catch {
@@ -7726,20 +7778,20 @@ function approverStatusLine() {
7726
7778
  const a = readApproversFromDisk();
7727
7779
  const fmt = (label, key) => {
7728
7780
  const on = a[key] !== false;
7729
- return `[${key[0]}]${label.slice(1)} ${on ? chalk23.green("\u2713") : chalk23.dim("\u2717")}`;
7781
+ return `[${key[0]}]${label.slice(1)} ${on ? chalk24.green("\u2713") : chalk24.dim("\u2717")}`;
7730
7782
  };
7731
7783
  return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
7732
7784
  }
7733
7785
  function toggleApprover(channel) {
7734
- const configPath = path36.join(os29.homedir(), ".node9", "config.json");
7786
+ const configPath = path38.join(os31.homedir(), ".node9", "config.json");
7735
7787
  try {
7736
- const raw = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
7788
+ const raw = JSON.parse(fs35.readFileSync(configPath, "utf-8"));
7737
7789
  const settings = raw.settings ?? {};
7738
7790
  const approvers = settings.approvers ?? {};
7739
7791
  approvers[channel] = approvers[channel] === false;
7740
7792
  settings.approvers = approvers;
7741
7793
  raw.settings = settings;
7742
- fs33.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
7794
+ fs35.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
7743
7795
  } catch (err2) {
7744
7796
  process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
7745
7797
  `);
@@ -7771,7 +7823,7 @@ async function startTail(options = {}) {
7771
7823
  req2.end();
7772
7824
  });
7773
7825
  if (result.ok) {
7774
- console.log(chalk23.green("\u2713 Flight Recorder buffer cleared."));
7826
+ console.log(chalk24.green("\u2713 Flight Recorder buffer cleared."));
7775
7827
  } else if (result.code === "ECONNREFUSED") {
7776
7828
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
7777
7829
  } else if (result.code === "ETIMEDOUT") {
@@ -7815,7 +7867,7 @@ async function startTail(options = {}) {
7815
7867
  const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
7816
7868
  if (channel) {
7817
7869
  toggleApprover(channel);
7818
- console.log(chalk23.dim(` Approvers: ${approverStatusLine()}`));
7870
+ console.log(chalk24.dim(` Approvers: ${approverStatusLine()}`));
7819
7871
  }
7820
7872
  };
7821
7873
  process.stdin.on("keypress", idleKeypressHandler);
@@ -7881,7 +7933,7 @@ async function startTail(options = {}) {
7881
7933
  localAllowCounts.get(req2.toolName) ?? 0
7882
7934
  )
7883
7935
  );
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");
7936
+ const decisionStamp = action === "always-allow" ? chalk24.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk24.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk24.green("\u2713 ALLOWED") : action === "redirect" ? chalk24.yellow("\u21A9 REDIRECT AI") : chalk24.red("\u2717 DENIED");
7885
7937
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
7886
7938
  for (const line of stampedLines) process.stdout.write(line + "\n");
7887
7939
  process.stdout.write(SHOW_CURSOR);
@@ -7909,8 +7961,8 @@ async function startTail(options = {}) {
7909
7961
  }
7910
7962
  postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
7911
7963
  try {
7912
- fs33.appendFileSync(
7913
- path36.join(os29.homedir(), ".node9", "hook-debug.log"),
7964
+ fs35.appendFileSync(
7965
+ path38.join(os31.homedir(), ".node9", "hook-debug.log"),
7914
7966
  `[tail] POST /decision failed: ${String(err2)}
7915
7967
  `
7916
7968
  );
@@ -7932,7 +7984,7 @@ async function startTail(options = {}) {
7932
7984
  );
7933
7985
  const stampedLines = buildCardLines(req2, priorCount);
7934
7986
  if (externalDecision) {
7935
- const source = externalDecision === "allow" ? chalk23.green("\u2713 ALLOWED") : chalk23.red("\u2717 DENIED");
7987
+ const source = externalDecision === "allow" ? chalk24.green("\u2713 ALLOWED") : chalk24.red("\u2717 DENIED");
7936
7988
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
7937
7989
  }
7938
7990
  for (const line of stampedLines) process.stdout.write(line + "\n");
@@ -7991,16 +8043,16 @@ async function startTail(options = {}) {
7991
8043
  }
7992
8044
  } catch {
7993
8045
  }
7994
- console.log(chalk23.cyan.bold(`
7995
- \u{1F6F0}\uFE0F Node9 tail `) + chalk23.dim(`\u2192 ${dashboardUrl}`));
8046
+ console.log(chalk24.cyan.bold(`
8047
+ \u{1F6F0}\uFE0F Node9 tail `) + chalk24.dim(`\u2192 ${dashboardUrl}`));
7996
8048
  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`));
8049
+ console.log(chalk24.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
8050
+ console.log(chalk24.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
7999
8051
  }
8000
8052
  if (options.history) {
8001
- console.log(chalk23.dim("Showing history + live events.\n"));
8053
+ console.log(chalk24.dim("Showing history + live events.\n"));
8002
8054
  } else {
8003
- console.log(chalk23.dim("Showing live events only. Use --history to include past.\n"));
8055
+ console.log(chalk24.dim("Showing live events only. Use --history to include past.\n"));
8004
8056
  }
8005
8057
  process.on("SIGINT", () => {
8006
8058
  exitIdleMode();
@@ -8010,13 +8062,13 @@ async function startTail(options = {}) {
8010
8062
  readline5.clearLine(process.stdout, 0);
8011
8063
  readline5.cursorTo(process.stdout, 0);
8012
8064
  }
8013
- console.log(chalk23.dim("\n\u{1F6F0}\uFE0F Disconnected."));
8065
+ console.log(chalk24.dim("\n\u{1F6F0}\uFE0F Disconnected."));
8014
8066
  process.exit(0);
8015
8067
  });
8016
8068
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
8017
8069
  const req = http2.get(sseUrl, (res) => {
8018
8070
  if (res.statusCode !== 200) {
8019
- console.error(chalk23.red(`Failed to connect: HTTP ${res.statusCode}`));
8071
+ console.error(chalk24.red(`Failed to connect: HTTP ${res.statusCode}`));
8020
8072
  process.exit(1);
8021
8073
  }
8022
8074
  if (canApprove) enterIdleMode();
@@ -8047,7 +8099,7 @@ async function startTail(options = {}) {
8047
8099
  readline5.clearLine(process.stdout, 0);
8048
8100
  readline5.cursorTo(process.stdout, 0);
8049
8101
  }
8050
- console.log(chalk23.red("\n\u274C Daemon disconnected."));
8102
+ console.log(chalk24.red("\n\u274C Daemon disconnected."));
8051
8103
  process.exit(1);
8052
8104
  });
8053
8105
  });
@@ -8139,9 +8191,9 @@ async function startTail(options = {}) {
8139
8191
  const hash = data.hash ?? "";
8140
8192
  const summary = data.argsSummary ?? data.tool;
8141
8193
  const fileCount = data.fileCount ?? 0;
8142
- const files = fileCount > 0 ? chalk23.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
8194
+ const files = fileCount > 0 ? chalk24.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
8143
8195
  process.stdout.write(
8144
- `${chalk23.dim(time)} ${chalk23.cyan("\u{1F4F8} snapshot")} ${chalk23.dim(hash)} ${summary}${files}
8196
+ `${chalk24.dim(time)} ${chalk24.cyan("\u{1F4F8} snapshot")} ${chalk24.dim(hash)} ${summary}${files}
8145
8197
  `
8146
8198
  );
8147
8199
  return;
@@ -8158,7 +8210,7 @@ async function startTail(options = {}) {
8158
8210
  }
8159
8211
  req.on("error", (err2) => {
8160
8212
  const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
8161
- console.error(chalk23.red(`
8213
+ console.error(chalk24.red(`
8162
8214
  \u274C ${msg}`));
8163
8215
  process.exit(1);
8164
8216
  });
@@ -8170,7 +8222,7 @@ var init_tail = __esm({
8170
8222
  init_daemon2();
8171
8223
  init_daemon();
8172
8224
  init_core();
8173
- PID_FILE = path36.join(os29.homedir(), ".node9", "daemon.pid");
8225
+ PID_FILE = path38.join(os31.homedir(), ".node9", "daemon.pid");
8174
8226
  ICONS = {
8175
8227
  bash: "\u{1F4BB}",
8176
8228
  shell: "\u{1F4BB}",
@@ -8211,9 +8263,9 @@ __export(hud_exports, {
8211
8263
  main: () => main,
8212
8264
  renderEnvironmentLine: () => renderEnvironmentLine
8213
8265
  });
8214
- import fs34 from "fs";
8215
- import path37 from "path";
8216
- import os30 from "os";
8266
+ import fs36 from "fs";
8267
+ import path39 from "path";
8268
+ import os32 from "os";
8217
8269
  import http3 from "http";
8218
8270
  async function readStdin() {
8219
8271
  const chunks = [];
@@ -8289,9 +8341,9 @@ function formatTimeLeft(resetsAt) {
8289
8341
  return ` (${m}m left)`;
8290
8342
  }
8291
8343
  function safeReadJson(filePath) {
8292
- if (!fs34.existsSync(filePath)) return null;
8344
+ if (!fs36.existsSync(filePath)) return null;
8293
8345
  try {
8294
- return JSON.parse(fs34.readFileSync(filePath, "utf-8"));
8346
+ return JSON.parse(fs36.readFileSync(filePath, "utf-8"));
8295
8347
  } catch {
8296
8348
  return null;
8297
8349
  }
@@ -8312,12 +8364,12 @@ function countHooksInFile(filePath) {
8312
8364
  return Object.keys(cfg.hooks).length;
8313
8365
  }
8314
8366
  function countRulesInDir(rulesDir) {
8315
- if (!fs34.existsSync(rulesDir)) return 0;
8367
+ if (!fs36.existsSync(rulesDir)) return 0;
8316
8368
  let count = 0;
8317
8369
  try {
8318
- for (const entry of fs34.readdirSync(rulesDir, { withFileTypes: true })) {
8370
+ for (const entry of fs36.readdirSync(rulesDir, { withFileTypes: true })) {
8319
8371
  if (entry.isDirectory()) {
8320
- count += countRulesInDir(path37.join(rulesDir, entry.name));
8372
+ count += countRulesInDir(path39.join(rulesDir, entry.name));
8321
8373
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
8322
8374
  count++;
8323
8375
  }
@@ -8328,46 +8380,46 @@ function countRulesInDir(rulesDir) {
8328
8380
  }
8329
8381
  function isSamePath(a, b) {
8330
8382
  try {
8331
- return path37.resolve(a) === path37.resolve(b);
8383
+ return path39.resolve(a) === path39.resolve(b);
8332
8384
  } catch {
8333
8385
  return false;
8334
8386
  }
8335
8387
  }
8336
8388
  function countConfigs(cwd) {
8337
- const homeDir2 = os30.homedir();
8338
- const claudeDir = path37.join(homeDir2, ".claude");
8389
+ const homeDir2 = os32.homedir();
8390
+ const claudeDir = path39.join(homeDir2, ".claude");
8339
8391
  let claudeMdCount = 0;
8340
8392
  let rulesCount = 0;
8341
8393
  let hooksCount = 0;
8342
8394
  const userMcpServers = /* @__PURE__ */ new Set();
8343
8395
  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");
8396
+ if (fs36.existsSync(path39.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
8397
+ rulesCount += countRulesInDir(path39.join(claudeDir, "rules"));
8398
+ const userSettings = path39.join(claudeDir, "settings.json");
8347
8399
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
8348
8400
  hooksCount += countHooksInFile(userSettings);
8349
- const userClaudeJson = path37.join(homeDir2, ".claude.json");
8401
+ const userClaudeJson = path39.join(homeDir2, ".claude.json");
8350
8402
  for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
8351
8403
  for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
8352
8404
  userMcpServers.delete(name);
8353
8405
  }
8354
8406
  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");
8407
+ if (fs36.existsSync(path39.join(cwd, "CLAUDE.md"))) claudeMdCount++;
8408
+ if (fs36.existsSync(path39.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
8409
+ const projectClaudeDir = path39.join(cwd, ".claude");
8358
8410
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
8359
8411
  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");
8412
+ if (fs36.existsSync(path39.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
8413
+ rulesCount += countRulesInDir(path39.join(projectClaudeDir, "rules"));
8414
+ const projSettings = path39.join(projectClaudeDir, "settings.json");
8363
8415
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
8364
8416
  hooksCount += countHooksInFile(projSettings);
8365
8417
  }
8366
- if (fs34.existsSync(path37.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
8367
- const localSettings = path37.join(projectClaudeDir, "settings.local.json");
8418
+ if (fs36.existsSync(path39.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
8419
+ const localSettings = path39.join(projectClaudeDir, "settings.local.json");
8368
8420
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
8369
8421
  hooksCount += countHooksInFile(localSettings);
8370
- const mcpJsonServers = getMcpServerNames(path37.join(cwd, ".mcp.json"));
8422
+ const mcpJsonServers = getMcpServerNames(path39.join(cwd, ".mcp.json"));
8371
8423
  const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
8372
8424
  for (const name of disabledMcpJson) mcpJsonServers.delete(name);
8373
8425
  for (const name of mcpJsonServers) projectMcpServers.add(name);
@@ -8400,12 +8452,12 @@ function readActiveShieldsHud() {
8400
8452
  return shieldsCache.value;
8401
8453
  }
8402
8454
  try {
8403
- const shieldsPath = path37.join(os30.homedir(), ".node9", "shields.json");
8404
- if (!fs34.existsSync(shieldsPath)) {
8455
+ const shieldsPath = path39.join(os32.homedir(), ".node9", "shields.json");
8456
+ if (!fs36.existsSync(shieldsPath)) {
8405
8457
  shieldsCache = { value: [], ts: now };
8406
8458
  return [];
8407
8459
  }
8408
- const parsed = JSON.parse(fs34.readFileSync(shieldsPath, "utf-8"));
8460
+ const parsed = JSON.parse(fs36.readFileSync(shieldsPath, "utf-8"));
8409
8461
  if (!Array.isArray(parsed.active)) {
8410
8462
  shieldsCache = { value: [], ts: now };
8411
8463
  return [];
@@ -8507,17 +8559,17 @@ function renderContextLine(stdin) {
8507
8559
  async function main() {
8508
8560
  try {
8509
8561
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
8510
- if (fs34.existsSync(path37.join(os30.homedir(), ".node9", "hud-debug"))) {
8562
+ if (fs36.existsSync(path39.join(os32.homedir(), ".node9", "hud-debug"))) {
8511
8563
  try {
8512
- const logPath = path37.join(os30.homedir(), ".node9", "hud-debug.log");
8564
+ const logPath = path39.join(os32.homedir(), ".node9", "hud-debug.log");
8513
8565
  const MAX_LOG_SIZE = 10 * 1024 * 1024;
8514
8566
  let size = 0;
8515
8567
  try {
8516
- size = fs34.statSync(logPath).size;
8568
+ size = fs36.statSync(logPath).size;
8517
8569
  } catch {
8518
8570
  }
8519
8571
  if (size < MAX_LOG_SIZE) {
8520
- fs34.appendFileSync(
8572
+ fs36.appendFileSync(
8521
8573
  logPath,
8522
8574
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
8523
8575
  );
@@ -8538,11 +8590,11 @@ async function main() {
8538
8590
  try {
8539
8591
  const cwd = stdin.cwd ?? process.cwd();
8540
8592
  for (const configPath of [
8541
- path37.join(cwd, "node9.config.json"),
8542
- path37.join(os30.homedir(), ".node9", "config.json")
8593
+ path39.join(cwd, "node9.config.json"),
8594
+ path39.join(os32.homedir(), ".node9", "config.json")
8543
8595
  ]) {
8544
- if (!fs34.existsSync(configPath)) continue;
8545
- const cfg = JSON.parse(fs34.readFileSync(configPath, "utf-8"));
8596
+ if (!fs36.existsSync(configPath)) continue;
8597
+ const cfg = JSON.parse(fs36.readFileSync(configPath, "utf-8"));
8546
8598
  const hud = cfg.settings?.hud;
8547
8599
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
8548
8600
  }
@@ -9477,10 +9529,10 @@ function getAgentsStatus(homeDir2 = os11.homedir()) {
9477
9529
 
9478
9530
  // src/cli.ts
9479
9531
  init_daemon2();
9480
- import chalk24 from "chalk";
9481
- import fs35 from "fs";
9482
- import path38 from "path";
9483
- import os31 from "os";
9532
+ import chalk25 from "chalk";
9533
+ import fs37 from "fs";
9534
+ import path40 from "path";
9535
+ import os33 from "os";
9484
9536
  import { confirm as confirm2 } from "@inquirer/prompts";
9485
9537
 
9486
9538
  // src/utils/duration.ts
@@ -9709,10 +9761,10 @@ init_daemon();
9709
9761
  init_config();
9710
9762
  init_policy();
9711
9763
  import chalk5 from "chalk";
9712
- import fs22 from "fs";
9764
+ import fs23 from "fs";
9713
9765
  import { spawn as spawn6 } from "child_process";
9714
- import path24 from "path";
9715
- import os18 from "os";
9766
+ import path25 from "path";
9767
+ import os19 from "os";
9716
9768
 
9717
9769
  // src/undo.ts
9718
9770
  import { spawnSync as spawnSync5, spawn as spawn5 } from "child_process";
@@ -10063,6 +10115,187 @@ function applyUndo(hash, cwd) {
10063
10115
  }
10064
10116
  }
10065
10117
 
10118
+ // src/skill-pin.ts
10119
+ import fs22 from "fs";
10120
+ import path24 from "path";
10121
+ import os18 from "os";
10122
+ import crypto4 from "crypto";
10123
+ function getPinsFilePath() {
10124
+ return path24.join(os18.homedir(), ".node9", "skill-pins.json");
10125
+ }
10126
+ var MAX_FILES = 5e3;
10127
+ var MAX_TOTAL_BYTES = 50 * 1024 * 1024;
10128
+ function sha256Bytes(buf) {
10129
+ return crypto4.createHash("sha256").update(buf).digest("hex");
10130
+ }
10131
+ function walkDir(root) {
10132
+ const out = [];
10133
+ let totalBytes = 0;
10134
+ const visit = (dir, relDir) => {
10135
+ if (out.length >= MAX_FILES) return;
10136
+ let entries;
10137
+ try {
10138
+ entries = fs22.readdirSync(dir, { withFileTypes: true });
10139
+ } catch {
10140
+ return;
10141
+ }
10142
+ entries.sort((a, b) => a.name.localeCompare(b.name));
10143
+ for (const entry of entries) {
10144
+ if (out.length >= MAX_FILES) return;
10145
+ const full = path24.join(dir, entry.name);
10146
+ const rel = relDir ? path24.posix.join(relDir, entry.name) : entry.name;
10147
+ let lst;
10148
+ try {
10149
+ lst = fs22.lstatSync(full);
10150
+ } catch {
10151
+ continue;
10152
+ }
10153
+ if (lst.isSymbolicLink()) continue;
10154
+ if (lst.isDirectory()) {
10155
+ visit(full, rel);
10156
+ continue;
10157
+ }
10158
+ if (!lst.isFile()) continue;
10159
+ if (totalBytes + lst.size > MAX_TOTAL_BYTES) continue;
10160
+ try {
10161
+ const buf = fs22.readFileSync(full);
10162
+ totalBytes += buf.length;
10163
+ out.push({ rel, hash: sha256Bytes(buf) });
10164
+ } catch {
10165
+ }
10166
+ }
10167
+ };
10168
+ visit(root, "");
10169
+ out.sort((a, b) => a.rel.localeCompare(b.rel));
10170
+ return out.map((e) => `${e.rel}\0${e.hash}`);
10171
+ }
10172
+ function hashSkillRoot(absPath) {
10173
+ let lst;
10174
+ try {
10175
+ lst = fs22.lstatSync(absPath);
10176
+ } catch {
10177
+ return { exists: false, contentHash: "", fileCount: 0 };
10178
+ }
10179
+ if (lst.isSymbolicLink()) return { exists: false, contentHash: "", fileCount: 0 };
10180
+ if (lst.isFile()) {
10181
+ try {
10182
+ return { exists: true, contentHash: sha256Bytes(fs22.readFileSync(absPath)), fileCount: 1 };
10183
+ } catch {
10184
+ return { exists: false, contentHash: "", fileCount: 0 };
10185
+ }
10186
+ }
10187
+ if (lst.isDirectory()) {
10188
+ const entries = walkDir(absPath);
10189
+ const contentHash = crypto4.createHash("sha256").update(entries.join("\n")).digest("hex");
10190
+ return { exists: true, contentHash, fileCount: entries.length };
10191
+ }
10192
+ return { exists: false, contentHash: "", fileCount: 0 };
10193
+ }
10194
+ function getRootKey(absPath) {
10195
+ return crypto4.createHash("sha256").update(absPath).digest("hex").slice(0, 16);
10196
+ }
10197
+ function readSkillPinsSafe() {
10198
+ const filePath = getPinsFilePath();
10199
+ try {
10200
+ const raw = fs22.readFileSync(filePath, "utf-8");
10201
+ if (!raw.trim()) return { ok: false, reason: "corrupt", detail: "empty file" };
10202
+ const parsed = JSON.parse(raw);
10203
+ if (!parsed.roots || typeof parsed.roots !== "object" || Array.isArray(parsed.roots)) {
10204
+ return { ok: false, reason: "corrupt", detail: "invalid structure: missing roots object" };
10205
+ }
10206
+ return { ok: true, pins: { roots: parsed.roots } };
10207
+ } catch (err2) {
10208
+ if (err2.code === "ENOENT") return { ok: false, reason: "missing" };
10209
+ return { ok: false, reason: "corrupt", detail: String(err2) };
10210
+ }
10211
+ }
10212
+ function readSkillPins() {
10213
+ const result = readSkillPinsSafe();
10214
+ if (result.ok) return result.pins;
10215
+ if (result.reason === "missing") return { roots: {} };
10216
+ throw new Error(`[node9] skill pin file is corrupt: ${result.detail}`);
10217
+ }
10218
+ function writeSkillPins(data) {
10219
+ const filePath = getPinsFilePath();
10220
+ fs22.mkdirSync(path24.dirname(filePath), { recursive: true });
10221
+ const tmp = `${filePath}.${crypto4.randomBytes(6).toString("hex")}.tmp`;
10222
+ fs22.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
10223
+ fs22.renameSync(tmp, filePath);
10224
+ }
10225
+ function removePin(rootKey) {
10226
+ const pins = readSkillPins();
10227
+ delete pins.roots[rootKey];
10228
+ writeSkillPins(pins);
10229
+ }
10230
+ function clearAllPins() {
10231
+ writeSkillPins({ roots: {} });
10232
+ }
10233
+ function verifyAndPinRoots(roots) {
10234
+ const pinsRead = readSkillPinsSafe();
10235
+ if (!pinsRead.ok && pinsRead.reason === "corrupt") {
10236
+ return { kind: "corrupt", detail: pinsRead.detail };
10237
+ }
10238
+ const pins = pinsRead.ok ? pinsRead.pins : { roots: {} };
10239
+ let mutated = false;
10240
+ for (const rootPath of new Set(roots)) {
10241
+ const rootKey = getRootKey(rootPath);
10242
+ const current = hashSkillRoot(rootPath);
10243
+ const existing = pins.roots[rootKey];
10244
+ if (!existing) {
10245
+ pins.roots[rootKey] = {
10246
+ rootPath,
10247
+ exists: current.exists,
10248
+ contentHash: current.contentHash,
10249
+ fileCount: current.fileCount,
10250
+ pinnedAt: (/* @__PURE__ */ new Date()).toISOString()
10251
+ };
10252
+ mutated = true;
10253
+ continue;
10254
+ }
10255
+ if (existing.exists !== current.exists || existing.contentHash !== current.contentHash) {
10256
+ let summary;
10257
+ if (existing.exists && !current.exists) summary = `vanished: ${rootPath}`;
10258
+ else if (!existing.exists && current.exists) summary = `appeared: ${rootPath}`;
10259
+ else summary = `changed: ${rootPath}`;
10260
+ return { kind: "drift", changedRootKey: rootKey, changedRootPath: rootPath, summary };
10261
+ }
10262
+ }
10263
+ if (mutated) writeSkillPins(pins);
10264
+ return { kind: "verified" };
10265
+ }
10266
+ function defaultSkillRoots(_cwd) {
10267
+ const marketplaces = path24.join(os18.homedir(), ".claude", "plugins", "marketplaces");
10268
+ const roots = [];
10269
+ let registries;
10270
+ try {
10271
+ registries = fs22.readdirSync(marketplaces, { withFileTypes: true });
10272
+ } catch {
10273
+ return [];
10274
+ }
10275
+ for (const registry of registries) {
10276
+ if (!registry.isDirectory()) continue;
10277
+ const pluginsDir = path24.join(marketplaces, registry.name, "plugins");
10278
+ let plugins;
10279
+ try {
10280
+ plugins = fs22.readdirSync(pluginsDir, { withFileTypes: true });
10281
+ } catch {
10282
+ continue;
10283
+ }
10284
+ for (const plugin of plugins) {
10285
+ if (!plugin.isDirectory()) continue;
10286
+ roots.push(path24.join(pluginsDir, plugin.name));
10287
+ }
10288
+ }
10289
+ return roots;
10290
+ }
10291
+ function resolveUserSkillRoot(entry, cwd) {
10292
+ if (!entry) return null;
10293
+ if (entry.startsWith("~/") || entry === "~") return path24.join(os18.homedir(), entry.slice(1));
10294
+ if (path24.isAbsolute(entry)) return entry;
10295
+ if (!cwd || !path24.isAbsolute(cwd)) return null;
10296
+ return path24.join(cwd, entry);
10297
+ }
10298
+
10066
10299
  // src/cli/commands/check.ts
10067
10300
  function sanitize2(value) {
10068
10301
  return value.replace(/[\x00-\x1F\x7F]/g, "");
@@ -10078,9 +10311,9 @@ function registerCheckCommand(program2) {
10078
10311
  } catch (err2) {
10079
10312
  const tempConfig = getConfig();
10080
10313
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
10081
- const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
10314
+ const logPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
10082
10315
  const errMsg = err2 instanceof Error ? err2.message : String(err2);
10083
- fs22.appendFileSync(
10316
+ fs23.appendFileSync(
10084
10317
  logPath,
10085
10318
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
10086
10319
  RAW: ${raw}
@@ -10093,11 +10326,11 @@ RAW: ${raw}
10093
10326
  if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
10094
10327
  try {
10095
10328
  const scriptPath = process.argv[1];
10096
- if (typeof scriptPath !== "string" || !path24.isAbsolute(scriptPath))
10329
+ if (typeof scriptPath !== "string" || !path25.isAbsolute(scriptPath))
10097
10330
  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)
10331
+ const resolvedScript = fs23.realpathSync(scriptPath);
10332
+ const packageDist = fs23.realpathSync(path25.resolve(__dirname, "../.."));
10333
+ if (!resolvedScript.startsWith(packageDist + path25.sep) && resolvedScript !== packageDist)
10101
10334
  throw new Error(
10102
10335
  `node9: daemon spawn aborted \u2014 argv[1] (${resolvedScript}) is outside package dist (${packageDist})`
10103
10336
  );
@@ -10119,10 +10352,10 @@ RAW: ${raw}
10119
10352
  });
10120
10353
  d.unref();
10121
10354
  } catch (spawnErr) {
10122
- const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
10355
+ const logPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
10123
10356
  const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
10124
10357
  try {
10125
- fs22.appendFileSync(
10358
+ fs23.appendFileSync(
10126
10359
  logPath,
10127
10360
  `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-failed: ${msg}
10128
10361
  `
@@ -10132,10 +10365,10 @@ RAW: ${raw}
10132
10365
  }
10133
10366
  }
10134
10367
  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}
10368
+ const logPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
10369
+ if (!fs23.existsSync(path25.dirname(logPath)))
10370
+ fs23.mkdirSync(path25.dirname(logPath), { recursive: true });
10371
+ fs23.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
10139
10372
  `);
10140
10373
  }
10141
10374
  const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
@@ -10148,8 +10381,8 @@ RAW: ${raw}
10148
10381
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
10149
10382
  let ttyFd = null;
10150
10383
  try {
10151
- ttyFd = fs22.openSync("/dev/tty", "w");
10152
- const writeTty = (line) => fs22.writeSync(ttyFd, line + "\n");
10384
+ ttyFd = fs23.openSync("/dev/tty", "w");
10385
+ const writeTty = (line) => fs23.writeSync(ttyFd, line + "\n");
10153
10386
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
10154
10387
  writeTty(chalk5.bgRed.white.bold(`
10155
10388
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
@@ -10168,7 +10401,7 @@ RAW: ${raw}
10168
10401
  } finally {
10169
10402
  if (ttyFd !== null)
10170
10403
  try {
10171
- fs22.closeSync(ttyFd);
10404
+ fs23.closeSync(ttyFd);
10172
10405
  } catch {
10173
10406
  }
10174
10407
  }
@@ -10197,10 +10430,131 @@ RAW: ${raw}
10197
10430
  return;
10198
10431
  }
10199
10432
  const meta = { agent, mcpServer };
10433
+ const skillPinCfg = config.policy.skillPinning;
10434
+ const rawSessionId = typeof payload.session_id === "string" ? payload.session_id : "";
10435
+ const safeSessionId = /^[A-Za-z0-9_\-]{1,128}$/.test(rawSessionId) ? rawSessionId : "";
10436
+ if (skillPinCfg.enabled && safeSessionId) {
10437
+ try {
10438
+ const sessionsDir = path25.join(os19.homedir(), ".node9", "skill-sessions");
10439
+ const flagPath = path25.join(sessionsDir, `${safeSessionId}.json`);
10440
+ let flag = null;
10441
+ try {
10442
+ flag = JSON.parse(fs23.readFileSync(flagPath, "utf-8"));
10443
+ } catch {
10444
+ }
10445
+ const writeFlag = (data2) => {
10446
+ try {
10447
+ fs23.mkdirSync(sessionsDir, { recursive: true });
10448
+ fs23.writeFileSync(
10449
+ flagPath,
10450
+ JSON.stringify({ ...data2, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
10451
+ { mode: 384 }
10452
+ );
10453
+ } catch {
10454
+ }
10455
+ };
10456
+ const sendSkillWarn = (detail, recoveryCmd) => {
10457
+ let ttyFd = null;
10458
+ try {
10459
+ ttyFd = fs23.openSync("/dev/tty", "w");
10460
+ const w = (line) => fs23.writeSync(ttyFd, line + "\n");
10461
+ w(chalk5.yellow(`
10462
+ \u26A0\uFE0F Node9: installed skill drift detected`));
10463
+ w(chalk5.gray(` ${detail}`));
10464
+ w(
10465
+ chalk5.gray(
10466
+ ` If you updated a plugin, acknowledge the change to clear this warning.`
10467
+ )
10468
+ );
10469
+ if (recoveryCmd) w(chalk5.green(` \u{1F4A1} Run: ${recoveryCmd}`));
10470
+ w("");
10471
+ } catch {
10472
+ } finally {
10473
+ if (ttyFd !== null)
10474
+ try {
10475
+ fs23.closeSync(ttyFd);
10476
+ } catch {
10477
+ }
10478
+ }
10479
+ };
10480
+ if (flag && flag.state === "quarantined" && skillPinCfg.mode === "block") {
10481
+ sendBlock(
10482
+ `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.`,
10483
+ {
10484
+ blockedByLabel: "Skill Pin Quarantine",
10485
+ recoveryCommand: "node9 skill pin list"
10486
+ }
10487
+ );
10488
+ return;
10489
+ }
10490
+ if (!flag || flag.state !== "verified" && flag.state !== "warned") {
10491
+ const absoluteCwd = typeof payload.cwd === "string" && path25.isAbsolute(payload.cwd) ? payload.cwd : void 0;
10492
+ const extraRoots = skillPinCfg.roots;
10493
+ const resolvedExtra = extraRoots.map((r) => resolveUserSkillRoot(r, absoluteCwd)).filter((r) => typeof r === "string");
10494
+ const roots = [...defaultSkillRoots(absoluteCwd), ...resolvedExtra];
10495
+ const result2 = verifyAndPinRoots(roots);
10496
+ if (result2.kind === "corrupt") {
10497
+ if (skillPinCfg.mode === "block") {
10498
+ writeFlag({
10499
+ state: "quarantined",
10500
+ detail: `pin file corrupt: ${result2.detail}`
10501
+ });
10502
+ sendBlock("Node9: skill pin file is corrupt \u2014 fail-closed.", {
10503
+ blockedByLabel: "Skill Pin Quarantine",
10504
+ recoveryCommand: "node9 skill pin reset"
10505
+ });
10506
+ return;
10507
+ }
10508
+ writeFlag({ state: "warned", detail: `pin file corrupt: ${result2.detail}` });
10509
+ sendSkillWarn(
10510
+ `Skill pin file is corrupt: ${result2.detail}`,
10511
+ "node9 skill pin reset"
10512
+ );
10513
+ } else if (result2.kind === "drift") {
10514
+ if (skillPinCfg.mode === "block") {
10515
+ writeFlag({ state: "quarantined", detail: result2.summary });
10516
+ sendBlock(
10517
+ `Node9: installed skill changed \u2014 ${result2.summary}. If you updated a plugin, open a separate terminal and run: node9 skill pin update ${result2.changedRootKey}`,
10518
+ {
10519
+ blockedByLabel: "Skill Pin Quarantine",
10520
+ recoveryCommand: `node9 skill pin update ${result2.changedRootKey}`
10521
+ }
10522
+ );
10523
+ return;
10524
+ }
10525
+ writeFlag({ state: "warned", detail: result2.summary });
10526
+ sendSkillWarn(result2.summary, `node9 skill pin update ${result2.changedRootKey}`);
10527
+ } else {
10528
+ writeFlag({ state: "verified" });
10529
+ }
10530
+ try {
10531
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
10532
+ for (const name of fs23.readdirSync(sessionsDir)) {
10533
+ const p = path25.join(sessionsDir, name);
10534
+ try {
10535
+ if (fs23.statSync(p).mtimeMs < cutoff) fs23.unlinkSync(p);
10536
+ } catch {
10537
+ }
10538
+ }
10539
+ } catch {
10540
+ }
10541
+ }
10542
+ } catch (err2) {
10543
+ if (process.env.NODE9_DEBUG === "1") {
10544
+ try {
10545
+ const dbg = path25.join(os19.homedir(), ".node9", "hook-debug.log");
10546
+ const msg = err2 instanceof Error ? err2.message : String(err2);
10547
+ fs23.appendFileSync(dbg, `[${(/* @__PURE__ */ new Date()).toISOString()}] SKILL_PIN_ERROR: ${msg}
10548
+ `);
10549
+ } catch {
10550
+ }
10551
+ }
10552
+ }
10553
+ }
10200
10554
  if (shouldSnapshot(toolName, toolInput, config)) {
10201
10555
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
10202
10556
  }
10203
- const safeCwdForAuth = typeof payload.cwd === "string" && path24.isAbsolute(payload.cwd) ? payload.cwd : void 0;
10557
+ const safeCwdForAuth = typeof payload.cwd === "string" && path25.isAbsolute(payload.cwd) ? payload.cwd : void 0;
10204
10558
  const result = await authorizeHeadless(toolName, toolInput, meta, {
10205
10559
  cwd: safeCwdForAuth
10206
10560
  });
@@ -10212,12 +10566,12 @@ RAW: ${raw}
10212
10566
  }
10213
10567
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
10214
10568
  try {
10215
- const tty = fs22.openSync("/dev/tty", "w");
10216
- fs22.writeSync(
10569
+ const tty = fs23.openSync("/dev/tty", "w");
10570
+ fs23.writeSync(
10217
10571
  tty,
10218
10572
  chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
10219
10573
  );
10220
- fs22.closeSync(tty);
10574
+ fs23.closeSync(tty);
10221
10575
  } catch {
10222
10576
  }
10223
10577
  const daemonReady = await autoStartDaemonAndWait();
@@ -10244,9 +10598,9 @@ RAW: ${raw}
10244
10598
  });
10245
10599
  } catch (err2) {
10246
10600
  if (process.env.NODE9_DEBUG === "1") {
10247
- const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
10601
+ const logPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
10248
10602
  const errMsg = err2 instanceof Error ? err2.message : String(err2);
10249
- fs22.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
10603
+ fs23.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
10250
10604
  `);
10251
10605
  }
10252
10606
  process.exit(0);
@@ -10283,9 +10637,9 @@ RAW: ${raw}
10283
10637
  init_audit();
10284
10638
  init_config();
10285
10639
  init_policy();
10286
- import fs23 from "fs";
10287
- import path25 from "path";
10288
- import os19 from "os";
10640
+ import fs24 from "fs";
10641
+ import path26 from "path";
10642
+ import os20 from "os";
10289
10643
  init_daemon();
10290
10644
 
10291
10645
  // src/utils/cp-mv-parser.ts
@@ -10358,10 +10712,10 @@ function registerLogCommand(program2) {
10358
10712
  decision: "allowed",
10359
10713
  source: "post-hook"
10360
10714
  };
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");
10715
+ const logPath = path26.join(os20.homedir(), ".node9", "audit.log");
10716
+ if (!fs24.existsSync(path26.dirname(logPath)))
10717
+ fs24.mkdirSync(path26.dirname(logPath), { recursive: true });
10718
+ fs24.appendFileSync(logPath, JSON.stringify(entry) + "\n");
10365
10719
  if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
10366
10720
  const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
10367
10721
  if (command) {
@@ -10394,7 +10748,7 @@ function registerLogCommand(program2) {
10394
10748
  }
10395
10749
  }
10396
10750
  }
10397
- const safeCwd = typeof payload.cwd === "string" && path25.isAbsolute(payload.cwd) ? payload.cwd : void 0;
10751
+ const safeCwd = typeof payload.cwd === "string" && path26.isAbsolute(payload.cwd) ? payload.cwd : void 0;
10398
10752
  const config = getConfig(safeCwd);
10399
10753
  if (shouldSnapshot(tool, {}, config)) {
10400
10754
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
@@ -10403,9 +10757,9 @@ function registerLogCommand(program2) {
10403
10757
  const msg = err2 instanceof Error ? err2.message : String(err2);
10404
10758
  process.stderr.write(`[Node9] audit log error: ${msg}
10405
10759
  `);
10406
- const debugPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
10760
+ const debugPath = path26.join(os20.homedir(), ".node9", "hook-debug.log");
10407
10761
  try {
10408
- fs23.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
10762
+ fs24.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
10409
10763
  `);
10410
10764
  } catch {
10411
10765
  }
@@ -10806,13 +11160,13 @@ function registerConfigShowCommand(program2) {
10806
11160
  // src/cli/commands/doctor.ts
10807
11161
  init_daemon();
10808
11162
  import chalk7 from "chalk";
10809
- import fs24 from "fs";
10810
- import path26 from "path";
10811
- import os20 from "os";
11163
+ import fs25 from "fs";
11164
+ import path27 from "path";
11165
+ import os21 from "os";
10812
11166
  import { execSync as execSync2 } from "child_process";
10813
11167
  function registerDoctorCommand(program2, version2) {
10814
11168
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
10815
- const homeDir2 = os20.homedir();
11169
+ const homeDir2 = os21.homedir();
10816
11170
  let failures = 0;
10817
11171
  function pass(msg) {
10818
11172
  console.log(chalk7.green(" \u2705 ") + msg);
@@ -10861,10 +11215,10 @@ function registerDoctorCommand(program2, version2) {
10861
11215
  );
10862
11216
  }
10863
11217
  section("Configuration");
10864
- const globalConfigPath = path26.join(homeDir2, ".node9", "config.json");
10865
- if (fs24.existsSync(globalConfigPath)) {
11218
+ const globalConfigPath = path27.join(homeDir2, ".node9", "config.json");
11219
+ if (fs25.existsSync(globalConfigPath)) {
10866
11220
  try {
10867
- JSON.parse(fs24.readFileSync(globalConfigPath, "utf-8"));
11221
+ JSON.parse(fs25.readFileSync(globalConfigPath, "utf-8"));
10868
11222
  pass("~/.node9/config.json found and valid");
10869
11223
  } catch {
10870
11224
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -10872,10 +11226,10 @@ function registerDoctorCommand(program2, version2) {
10872
11226
  } else {
10873
11227
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
10874
11228
  }
10875
- const projectConfigPath = path26.join(process.cwd(), "node9.config.json");
10876
- if (fs24.existsSync(projectConfigPath)) {
11229
+ const projectConfigPath = path27.join(process.cwd(), "node9.config.json");
11230
+ if (fs25.existsSync(projectConfigPath)) {
10877
11231
  try {
10878
- JSON.parse(fs24.readFileSync(projectConfigPath, "utf-8"));
11232
+ JSON.parse(fs25.readFileSync(projectConfigPath, "utf-8"));
10879
11233
  pass("node9.config.json found and valid (project)");
10880
11234
  } catch {
10881
11235
  fail(
@@ -10884,8 +11238,8 @@ function registerDoctorCommand(program2, version2) {
10884
11238
  );
10885
11239
  }
10886
11240
  }
10887
- const credsPath = path26.join(homeDir2, ".node9", "credentials.json");
10888
- if (fs24.existsSync(credsPath)) {
11241
+ const credsPath = path27.join(homeDir2, ".node9", "credentials.json");
11242
+ if (fs25.existsSync(credsPath)) {
10889
11243
  pass("Cloud credentials found (~/.node9/credentials.json)");
10890
11244
  } else {
10891
11245
  warn(
@@ -10894,10 +11248,10 @@ function registerDoctorCommand(program2, version2) {
10894
11248
  );
10895
11249
  }
10896
11250
  section("Agent Hooks");
10897
- const claudeSettingsPath = path26.join(homeDir2, ".claude", "settings.json");
10898
- if (fs24.existsSync(claudeSettingsPath)) {
11251
+ const claudeSettingsPath = path27.join(homeDir2, ".claude", "settings.json");
11252
+ if (fs25.existsSync(claudeSettingsPath)) {
10899
11253
  try {
10900
- const cs = JSON.parse(fs24.readFileSync(claudeSettingsPath, "utf-8"));
11254
+ const cs = JSON.parse(fs25.readFileSync(claudeSettingsPath, "utf-8"));
10901
11255
  const hasHook = cs.hooks?.PreToolUse?.some(
10902
11256
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
10903
11257
  );
@@ -10913,10 +11267,10 @@ function registerDoctorCommand(program2, version2) {
10913
11267
  } else {
10914
11268
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
10915
11269
  }
10916
- const geminiSettingsPath = path26.join(homeDir2, ".gemini", "settings.json");
10917
- if (fs24.existsSync(geminiSettingsPath)) {
11270
+ const geminiSettingsPath = path27.join(homeDir2, ".gemini", "settings.json");
11271
+ if (fs25.existsSync(geminiSettingsPath)) {
10918
11272
  try {
10919
- const gs = JSON.parse(fs24.readFileSync(geminiSettingsPath, "utf-8"));
11273
+ const gs = JSON.parse(fs25.readFileSync(geminiSettingsPath, "utf-8"));
10920
11274
  const hasHook = gs.hooks?.BeforeTool?.some(
10921
11275
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
10922
11276
  );
@@ -10932,10 +11286,10 @@ function registerDoctorCommand(program2, version2) {
10932
11286
  } else {
10933
11287
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
10934
11288
  }
10935
- const cursorHooksPath = path26.join(homeDir2, ".cursor", "hooks.json");
10936
- if (fs24.existsSync(cursorHooksPath)) {
11289
+ const cursorHooksPath = path27.join(homeDir2, ".cursor", "hooks.json");
11290
+ if (fs25.existsSync(cursorHooksPath)) {
10937
11291
  try {
10938
- const cur = JSON.parse(fs24.readFileSync(cursorHooksPath, "utf-8"));
11292
+ const cur = JSON.parse(fs25.readFileSync(cursorHooksPath, "utf-8"));
10939
11293
  const hasHook = cur.hooks?.preToolUse?.some(
10940
11294
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
10941
11295
  );
@@ -10973,9 +11327,9 @@ function registerDoctorCommand(program2, version2) {
10973
11327
 
10974
11328
  // src/cli/commands/audit.ts
10975
11329
  import chalk8 from "chalk";
10976
- import fs25 from "fs";
10977
- import path27 from "path";
10978
- import os21 from "os";
11330
+ import fs26 from "fs";
11331
+ import path28 from "path";
11332
+ import os22 from "os";
10979
11333
  function formatRelativeTime(timestamp) {
10980
11334
  const diff = Date.now() - new Date(timestamp).getTime();
10981
11335
  const sec = Math.floor(diff / 1e3);
@@ -10988,14 +11342,14 @@ function formatRelativeTime(timestamp) {
10988
11342
  }
10989
11343
  function registerAuditCommand(program2) {
10990
11344
  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)) {
11345
+ const logPath = path28.join(os22.homedir(), ".node9", "audit.log");
11346
+ if (!fs26.existsSync(logPath)) {
10993
11347
  console.log(
10994
11348
  chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
10995
11349
  );
10996
11350
  return;
10997
11351
  }
10998
- const raw = fs25.readFileSync(logPath, "utf-8");
11352
+ const raw = fs26.readFileSync(logPath, "utf-8");
10999
11353
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
11000
11354
  let entries = lines.flatMap((line) => {
11001
11355
  try {
@@ -11049,9 +11403,9 @@ function registerAuditCommand(program2) {
11049
11403
 
11050
11404
  // src/cli/commands/report.ts
11051
11405
  import chalk9 from "chalk";
11052
- import fs26 from "fs";
11053
- import path28 from "path";
11054
- import os22 from "os";
11406
+ import fs27 from "fs";
11407
+ import path29 from "path";
11408
+ import os23 from "os";
11055
11409
  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
11410
  function buildTestTimestamps(allEntries) {
11057
11411
  const testTs = /* @__PURE__ */ new Set();
@@ -11098,8 +11452,8 @@ function getDateRange(period) {
11098
11452
  }
11099
11453
  }
11100
11454
  function parseAuditLog(logPath) {
11101
- if (!fs26.existsSync(logPath)) return [];
11102
- const raw = fs26.readFileSync(logPath, "utf-8");
11455
+ if (!fs27.existsSync(logPath)) return [];
11456
+ const raw = fs27.readFileSync(logPath, "utf-8");
11103
11457
  return raw.split("\n").flatMap((line) => {
11104
11458
  if (!line.trim()) return [];
11105
11459
  try {
@@ -11168,11 +11522,11 @@ function loadClaudeCost(start, end) {
11168
11522
  inputTokens: 0,
11169
11523
  cacheReadTokens: 0
11170
11524
  };
11171
- const projectsDir = path28.join(os22.homedir(), ".claude", "projects");
11172
- if (!fs26.existsSync(projectsDir)) return empty;
11525
+ const projectsDir = path29.join(os23.homedir(), ".claude", "projects");
11526
+ if (!fs27.existsSync(projectsDir)) return empty;
11173
11527
  let dirs;
11174
11528
  try {
11175
- dirs = fs26.readdirSync(projectsDir);
11529
+ dirs = fs27.readdirSync(projectsDir);
11176
11530
  } catch {
11177
11531
  return empty;
11178
11532
  }
@@ -11182,18 +11536,18 @@ function loadClaudeCost(start, end) {
11182
11536
  const byDay = /* @__PURE__ */ new Map();
11183
11537
  const byModel = /* @__PURE__ */ new Map();
11184
11538
  for (const proj of dirs) {
11185
- const projPath = path28.join(projectsDir, proj);
11539
+ const projPath = path29.join(projectsDir, proj);
11186
11540
  let files;
11187
11541
  try {
11188
- const stat = fs26.statSync(projPath);
11542
+ const stat = fs27.statSync(projPath);
11189
11543
  if (!stat.isDirectory()) continue;
11190
- files = fs26.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
11544
+ files = fs27.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
11191
11545
  } catch {
11192
11546
  continue;
11193
11547
  }
11194
11548
  for (const file of files) {
11195
11549
  try {
11196
- const raw = fs26.readFileSync(path28.join(projPath, file), "utf-8");
11550
+ const raw = fs27.readFileSync(path29.join(projPath, file), "utf-8");
11197
11551
  for (const line of raw.split("\n")) {
11198
11552
  if (!line.trim()) continue;
11199
11553
  let entry;
@@ -11236,7 +11590,7 @@ function registerReportCommand(program2) {
11236
11590
  const period = ["today", "7d", "30d", "month"].includes(
11237
11591
  options.period
11238
11592
  ) ? options.period : "7d";
11239
- const logPath = path28.join(os22.homedir(), ".node9", "audit.log");
11593
+ const logPath = path29.join(os23.homedir(), ".node9", "audit.log");
11240
11594
  const allEntries = parseAuditLog(logPath);
11241
11595
  if (allEntries.length === 0) {
11242
11596
  console.log(
@@ -11591,12 +11945,12 @@ function registerDaemonCommand(program2) {
11591
11945
  init_core();
11592
11946
  init_daemon();
11593
11947
  import chalk11 from "chalk";
11594
- import fs27 from "fs";
11595
- import path29 from "path";
11596
- import os23 from "os";
11948
+ import fs28 from "fs";
11949
+ import path30 from "path";
11950
+ import os24 from "os";
11597
11951
  function readJson2(filePath) {
11598
11952
  try {
11599
- if (fs27.existsSync(filePath)) return JSON.parse(fs27.readFileSync(filePath, "utf-8"));
11953
+ if (fs28.existsSync(filePath)) return JSON.parse(fs28.readFileSync(filePath, "utf-8"));
11600
11954
  } catch {
11601
11955
  }
11602
11956
  return null;
@@ -11661,28 +12015,28 @@ function registerStatusCommand(program2) {
11661
12015
  console.log("");
11662
12016
  const modeLabel = settings.mode === "audit" ? chalk11.blue("audit") : settings.mode === "strict" ? chalk11.red("strict") : chalk11.white("standard");
11663
12017
  console.log(` Mode: ${modeLabel}`);
11664
- const projectConfig = path29.join(process.cwd(), "node9.config.json");
11665
- const globalConfig = path29.join(os23.homedir(), ".node9", "config.json");
12018
+ const projectConfig = path30.join(process.cwd(), "node9.config.json");
12019
+ const globalConfig = path30.join(os24.homedir(), ".node9", "config.json");
11666
12020
  console.log(
11667
- ` Local: ${fs27.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
12021
+ ` Local: ${fs28.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
11668
12022
  );
11669
12023
  console.log(
11670
- ` Global: ${fs27.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
12024
+ ` Global: ${fs28.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
11671
12025
  );
11672
12026
  if (mergedConfig.policy.sandboxPaths.length > 0) {
11673
12027
  console.log(
11674
12028
  ` Sandbox: ${chalk11.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
11675
12029
  );
11676
12030
  }
11677
- const homeDir2 = os23.homedir();
12031
+ const homeDir2 = os24.homedir();
11678
12032
  const claudeSettings = readJson2(
11679
- path29.join(homeDir2, ".claude", "settings.json")
12033
+ path30.join(homeDir2, ".claude", "settings.json")
11680
12034
  );
11681
- const claudeConfig = readJson2(path29.join(homeDir2, ".claude.json"));
12035
+ const claudeConfig = readJson2(path30.join(homeDir2, ".claude.json"));
11682
12036
  const geminiSettings = readJson2(
11683
- path29.join(homeDir2, ".gemini", "settings.json")
12037
+ path30.join(homeDir2, ".gemini", "settings.json")
11684
12038
  );
11685
- const cursorConfig = readJson2(path29.join(homeDir2, ".cursor", "mcp.json"));
12039
+ const cursorConfig = readJson2(path30.join(homeDir2, ".cursor", "mcp.json"));
11686
12040
  const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
11687
12041
  if (agentFound) {
11688
12042
  console.log("");
@@ -11742,9 +12096,9 @@ function registerStatusCommand(program2) {
11742
12096
  // src/cli/commands/init.ts
11743
12097
  init_core();
11744
12098
  import chalk12 from "chalk";
11745
- import fs28 from "fs";
11746
- import path30 from "path";
11747
- import os24 from "os";
12099
+ import fs29 from "fs";
12100
+ import path31 from "path";
12101
+ import os25 from "os";
11748
12102
  import https3 from "https";
11749
12103
  init_shields();
11750
12104
  init_service();
@@ -11804,15 +12158,15 @@ function registerInitCommand(program2) {
11804
12158
  }
11805
12159
  console.log("");
11806
12160
  }
11807
- const configPath = path30.join(os24.homedir(), ".node9", "config.json");
11808
- if (fs28.existsSync(configPath) && !options.force) {
12161
+ const configPath = path31.join(os25.homedir(), ".node9", "config.json");
12162
+ if (fs29.existsSync(configPath) && !options.force) {
11809
12163
  try {
11810
- const existing = JSON.parse(fs28.readFileSync(configPath, "utf-8"));
12164
+ const existing = JSON.parse(fs29.readFileSync(configPath, "utf-8"));
11811
12165
  const settings = existing.settings ?? {};
11812
12166
  if (settings.mode !== chosenMode) {
11813
12167
  settings.mode = chosenMode;
11814
12168
  existing.settings = settings;
11815
- fs28.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
12169
+ fs29.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
11816
12170
  console.log(chalk12.green(`\u2705 Mode updated: ${chosenMode}`));
11817
12171
  } else {
11818
12172
  console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
@@ -11825,9 +12179,9 @@ function registerInitCommand(program2) {
11825
12179
  ...DEFAULT_CONFIG,
11826
12180
  settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
11827
12181
  };
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");
12182
+ const dir = path31.dirname(configPath);
12183
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
12184
+ fs29.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
11831
12185
  console.log(chalk12.green(`\u2705 Config created: ${configPath}`));
11832
12186
  console.log(chalk12.gray(` Mode: ${chosenMode}`));
11833
12187
  }
@@ -11911,7 +12265,7 @@ function registerInitCommand(program2) {
11911
12265
  }
11912
12266
 
11913
12267
  // src/cli/commands/undo.ts
11914
- import path31 from "path";
12268
+ import path32 from "path";
11915
12269
  import chalk14 from "chalk";
11916
12270
 
11917
12271
  // src/tui/undo-navigator.ts
@@ -12070,7 +12424,7 @@ function findMatchingCwd(startDir, history) {
12070
12424
  let dir = startDir;
12071
12425
  while (true) {
12072
12426
  if (cwds.has(dir)) return dir;
12073
- const parent = path31.dirname(dir);
12427
+ const parent = path32.dirname(dir);
12074
12428
  if (parent === dir) return null;
12075
12429
  dir = parent;
12076
12430
  }
@@ -12266,12 +12620,12 @@ import { execa as execa2 } from "execa";
12266
12620
  init_provenance();
12267
12621
 
12268
12622
  // 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");
12623
+ import fs30 from "fs";
12624
+ import path33 from "path";
12625
+ import os26 from "os";
12626
+ import crypto5 from "crypto";
12627
+ function getPinsFilePath2() {
12628
+ return path33.join(os26.homedir(), ".node9", "mcp-pins.json");
12275
12629
  }
12276
12630
  function hashToolDefinitions(tools) {
12277
12631
  const sorted = [...tools].sort((a, b) => {
@@ -12280,15 +12634,15 @@ function hashToolDefinitions(tools) {
12280
12634
  return nameA.localeCompare(nameB);
12281
12635
  });
12282
12636
  const canonical = JSON.stringify(sorted);
12283
- return crypto4.createHash("sha256").update(canonical).digest("hex");
12637
+ return crypto5.createHash("sha256").update(canonical).digest("hex");
12284
12638
  }
12285
12639
  function getServerKey(upstreamCommand) {
12286
- return crypto4.createHash("sha256").update(upstreamCommand).digest("hex").slice(0, 16);
12640
+ return crypto5.createHash("sha256").update(upstreamCommand).digest("hex").slice(0, 16);
12287
12641
  }
12288
12642
  function readMcpPinsSafe() {
12289
- const filePath = getPinsFilePath();
12643
+ const filePath = getPinsFilePath2();
12290
12644
  try {
12291
- const raw = fs29.readFileSync(filePath, "utf-8");
12645
+ const raw = fs30.readFileSync(filePath, "utf-8");
12292
12646
  if (!raw.trim()) {
12293
12647
  return { ok: false, reason: "corrupt", detail: "empty file" };
12294
12648
  }
@@ -12311,11 +12665,11 @@ function readMcpPins() {
12311
12665
  throw new Error(`[node9] MCP pin file is corrupt: ${result.detail}`);
12312
12666
  }
12313
12667
  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);
12668
+ const filePath = getPinsFilePath2();
12669
+ fs30.mkdirSync(path33.dirname(filePath), { recursive: true });
12670
+ const tmp = `${filePath}.${crypto5.randomBytes(6).toString("hex")}.tmp`;
12671
+ fs30.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
12672
+ fs30.renameSync(tmp, filePath);
12319
12673
  }
12320
12674
  function checkPin(serverKey, currentHash) {
12321
12675
  const result = readMcpPinsSafe();
@@ -12338,12 +12692,12 @@ function updatePin(serverKey, label, toolsHash, toolNames) {
12338
12692
  };
12339
12693
  writeMcpPins(pins);
12340
12694
  }
12341
- function removePin(serverKey) {
12695
+ function removePin2(serverKey) {
12342
12696
  const pins = readMcpPins();
12343
12697
  delete pins.servers[serverKey];
12344
12698
  writeMcpPins(pins);
12345
12699
  }
12346
- function clearAllPins() {
12700
+ function clearAllPins2() {
12347
12701
  writeMcpPins({ servers: {} });
12348
12702
  }
12349
12703
 
@@ -12687,9 +13041,9 @@ function registerMcpGatewayCommand(program2) {
12687
13041
 
12688
13042
  // src/mcp-server/index.ts
12689
13043
  import readline4 from "readline";
12690
- import fs30 from "fs";
12691
- import os26 from "os";
12692
- import path33 from "path";
13044
+ import fs31 from "fs";
13045
+ import os27 from "os";
13046
+ import path34 from "path";
12693
13047
  init_core();
12694
13048
  init_daemon();
12695
13049
  init_shields();
@@ -12864,13 +13218,13 @@ function handleStatus() {
12864
13218
  lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
12865
13219
  lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
12866
13220
  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");
13221
+ const projectConfig = path34.join(process.cwd(), "node9.config.json");
13222
+ const globalConfig = path34.join(os27.homedir(), ".node9", "config.json");
12869
13223
  lines.push(
12870
- `Project config (node9.config.json): ${fs30.existsSync(projectConfig) ? "present" : "not found"}`
13224
+ `Project config (node9.config.json): ${fs31.existsSync(projectConfig) ? "present" : "not found"}`
12871
13225
  );
12872
13226
  lines.push(
12873
- `Global config (~/.node9/config.json): ${fs30.existsSync(globalConfig) ? "present" : "not found"}`
13227
+ `Global config (~/.node9/config.json): ${fs31.existsSync(globalConfig) ? "present" : "not found"}`
12874
13228
  );
12875
13229
  return lines.join("\n");
12876
13230
  }
@@ -12944,21 +13298,21 @@ function handleShieldDisable(args) {
12944
13298
  writeActiveShields(active.filter((s) => s !== name));
12945
13299
  return `Shield "${name}" disabled.`;
12946
13300
  }
12947
- var GLOBAL_CONFIG_PATH2 = path33.join(os26.homedir(), ".node9", "config.json");
13301
+ var GLOBAL_CONFIG_PATH2 = path34.join(os27.homedir(), ".node9", "config.json");
12948
13302
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
12949
13303
  function readGlobalConfigRaw() {
12950
13304
  try {
12951
- if (fs30.existsSync(GLOBAL_CONFIG_PATH2)) {
12952
- return JSON.parse(fs30.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
13305
+ if (fs31.existsSync(GLOBAL_CONFIG_PATH2)) {
13306
+ return JSON.parse(fs31.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
12953
13307
  }
12954
13308
  } catch {
12955
13309
  }
12956
13310
  return {};
12957
13311
  }
12958
13312
  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");
13313
+ const dir = path34.dirname(GLOBAL_CONFIG_PATH2);
13314
+ if (!fs31.existsSync(dir)) fs31.mkdirSync(dir, { recursive: true });
13315
+ fs31.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
12962
13316
  }
12963
13317
  function handleApproverList() {
12964
13318
  const config = getConfig();
@@ -13001,9 +13355,9 @@ function handleApproverSet(args) {
13001
13355
  }
13002
13356
  function handleAuditGet(args) {
13003
13357
  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);
13358
+ const auditPath = path34.join(os27.homedir(), ".node9", "audit.log");
13359
+ if (!fs31.existsSync(auditPath)) return "No audit log found.";
13360
+ const lines = fs31.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
13007
13361
  const recent = lines.slice(-limit);
13008
13362
  const entries = recent.map((line) => {
13009
13363
  try {
@@ -13301,7 +13655,7 @@ function registerMcpPinCommand(program2) {
13301
13655
  process.exit(1);
13302
13656
  }
13303
13657
  const label = pins.servers[serverKey].label;
13304
- removePin(serverKey);
13658
+ removePin2(serverKey);
13305
13659
  console.log(chalk18.green(`
13306
13660
  \u{1F513} Pin removed for ${chalk18.cyan(serverKey)}`));
13307
13661
  console.log(chalk18.gray(` Server: ${label}`));
@@ -13314,7 +13668,7 @@ function registerMcpPinCommand(program2) {
13314
13668
  return;
13315
13669
  }
13316
13670
  const count = result.ok ? Object.keys(result.pins.servers).length : "?";
13317
- clearAllPins();
13671
+ clearAllPins2();
13318
13672
  console.log(chalk18.green(`
13319
13673
  \u{1F513} Cleared ${count} MCP pin(s).`));
13320
13674
  console.log(chalk18.gray(" Next connection to each server will re-pin.\n"));
@@ -13465,9 +13819,9 @@ init_config();
13465
13819
  init_policy();
13466
13820
  init_dlp();
13467
13821
  import chalk21 from "chalk";
13468
- import fs31 from "fs";
13469
- import path34 from "path";
13470
- import os27 from "os";
13822
+ import fs32 from "fs";
13823
+ import path35 from "path";
13824
+ import os28 from "os";
13471
13825
  var CLAUDE_PRICING2 = {
13472
13826
  "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13473
13827
  "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
@@ -13535,7 +13889,7 @@ function buildRuleSources() {
13535
13889
  return sources;
13536
13890
  }
13537
13891
  function scanClaudeHistory(startDate) {
13538
- const projectsDir = path34.join(os27.homedir(), ".claude", "projects");
13892
+ const projectsDir = path35.join(os28.homedir(), ".claude", "projects");
13539
13893
  const result = {
13540
13894
  filesScanned: 0,
13541
13895
  sessions: 0,
@@ -13547,25 +13901,25 @@ function scanClaudeHistory(startDate) {
13547
13901
  firstDate: null,
13548
13902
  lastDate: null
13549
13903
  };
13550
- if (!fs31.existsSync(projectsDir)) return result;
13904
+ if (!fs32.existsSync(projectsDir)) return result;
13551
13905
  let projDirs;
13552
13906
  try {
13553
- projDirs = fs31.readdirSync(projectsDir);
13907
+ projDirs = fs32.readdirSync(projectsDir);
13554
13908
  } catch {
13555
13909
  return result;
13556
13910
  }
13557
13911
  const ruleSources = buildRuleSources();
13558
13912
  for (const proj of projDirs) {
13559
- const projPath = path34.join(projectsDir, proj);
13913
+ const projPath = path35.join(projectsDir, proj);
13560
13914
  try {
13561
- if (!fs31.statSync(projPath).isDirectory()) continue;
13915
+ if (!fs32.statSync(projPath).isDirectory()) continue;
13562
13916
  } catch {
13563
13917
  continue;
13564
13918
  }
13565
- const projLabel = decodeURIComponent(proj).replace(os27.homedir(), "~").slice(0, 40);
13919
+ const projLabel = decodeURIComponent(proj).replace(os28.homedir(), "~").slice(0, 40);
13566
13920
  let files;
13567
13921
  try {
13568
- files = fs31.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
13922
+ files = fs32.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
13569
13923
  } catch {
13570
13924
  continue;
13571
13925
  }
@@ -13574,7 +13928,7 @@ function scanClaudeHistory(startDate) {
13574
13928
  result.sessions++;
13575
13929
  let raw;
13576
13930
  try {
13577
- raw = fs31.readFileSync(path34.join(projPath, file), "utf-8");
13931
+ raw = fs32.readFileSync(path35.join(projPath, file), "utf-8");
13578
13932
  } catch {
13579
13933
  continue;
13580
13934
  }
@@ -13667,8 +14021,8 @@ function registerScanCommand(program2) {
13667
14021
  console.log("");
13668
14022
  console.log(chalk21.cyan.bold("\u{1F50D} node9 scan") + chalk21.dim(" \u2014 what would node9 catch?"));
13669
14023
  console.log("");
13670
- const projectsDir = path34.join(os27.homedir(), ".claude", "projects");
13671
- if (!fs31.existsSync(projectsDir)) {
14024
+ const projectsDir = path35.join(os28.homedir(), ".claude", "projects");
14025
+ if (!fs32.existsSync(projectsDir)) {
13672
14026
  console.log(chalk21.yellow(" No Claude history found at ~/.claude/projects/"));
13673
14027
  console.log(chalk21.gray(" Install Claude Code, run a few sessions, then try again.\n"));
13674
14028
  return;
@@ -13780,8 +14134,8 @@ function registerScanCommand(program2) {
13780
14134
  );
13781
14135
  console.log("");
13782
14136
  }
13783
- const auditLog = path34.join(os27.homedir(), ".node9", "audit.log");
13784
- if (fs31.existsSync(auditLog)) {
14137
+ const auditLog = path35.join(os28.homedir(), ".node9", "audit.log");
14138
+ if (fs32.existsSync(auditLog)) {
13785
14139
  console.log(chalk21.green(" \u2705 node9 is active \u2014 future sessions are protected."));
13786
14140
  console.log(
13787
14141
  chalk21.dim(" Run ") + chalk21.cyan("node9 report") + chalk21.dim(" to see live stats.")
@@ -13798,9 +14152,9 @@ function registerScanCommand(program2) {
13798
14152
 
13799
14153
  // src/cli/commands/sessions.ts
13800
14154
  import chalk22 from "chalk";
13801
- import fs32 from "fs";
13802
- import path35 from "path";
13803
- import os28 from "os";
14155
+ import fs33 from "fs";
14156
+ import path36 from "path";
14157
+ import os29 from "os";
13804
14158
  var CLAUDE_PRICING3 = {
13805
14159
  "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13806
14160
  "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
@@ -13825,10 +14179,10 @@ function encodeProjectPath(projectPath) {
13825
14179
  }
13826
14180
  function sessionJsonlPath(projectPath, sessionId) {
13827
14181
  const encoded = encodeProjectPath(projectPath);
13828
- return path35.join(os28.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
14182
+ return path36.join(os29.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
13829
14183
  }
13830
14184
  function projectLabel(projectPath) {
13831
- return projectPath.replace(os28.homedir(), "~");
14185
+ return projectPath.replace(os29.homedir(), "~");
13832
14186
  }
13833
14187
  function parseHistoryLines(lines) {
13834
14188
  const entries = [];
@@ -13897,10 +14251,10 @@ function parseSessionLines(lines) {
13897
14251
  return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
13898
14252
  }
13899
14253
  function loadAuditEntries(auditPath) {
13900
- const aPath = auditPath ?? path35.join(os28.homedir(), ".node9", "audit.log");
14254
+ const aPath = auditPath ?? path36.join(os29.homedir(), ".node9", "audit.log");
13901
14255
  let raw;
13902
14256
  try {
13903
- raw = fs32.readFileSync(aPath, "utf-8");
14257
+ raw = fs33.readFileSync(aPath, "utf-8");
13904
14258
  } catch {
13905
14259
  return [];
13906
14260
  }
@@ -13936,10 +14290,10 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
13936
14290
  return result;
13937
14291
  }
13938
14292
  function buildSessions(days, historyPath) {
13939
- const hPath = historyPath ?? path35.join(os28.homedir(), ".claude", "history.jsonl");
14293
+ const hPath = historyPath ?? path36.join(os29.homedir(), ".claude", "history.jsonl");
13940
14294
  let historyRaw;
13941
14295
  try {
13942
- historyRaw = fs32.readFileSync(hPath, "utf-8");
14296
+ historyRaw = fs33.readFileSync(hPath, "utf-8");
13943
14297
  } catch {
13944
14298
  return [];
13945
14299
  }
@@ -13964,7 +14318,7 @@ function buildSessions(days, historyPath) {
13964
14318
  const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
13965
14319
  let sessionLines = [];
13966
14320
  try {
13967
- sessionLines = fs32.readFileSync(jsonlFile, "utf-8").split("\n");
14321
+ sessionLines = fs33.readFileSync(jsonlFile, "utf-8").split("\n");
13968
14322
  } catch {
13969
14323
  }
13970
14324
  const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
@@ -14215,8 +14569,8 @@ function registerSessionsCommand(program2) {
14215
14569
  console.log("");
14216
14570
  console.log(chalk22.cyan.bold("\u{1F4CB} node9 sessions") + chalk22.dim(" \u2014 what your AI agent did"));
14217
14571
  console.log("");
14218
- const historyPath = path35.join(os28.homedir(), ".claude", "history.jsonl");
14219
- if (!fs32.existsSync(historyPath)) {
14572
+ const historyPath = path36.join(os29.homedir(), ".claude", "history.jsonl");
14573
+ if (!fs33.existsSync(historyPath)) {
14220
14574
  console.log(chalk22.yellow(" No Claude session history found at ~/.claude/history.jsonl"));
14221
14575
  console.log(chalk22.gray(" Install Claude Code, run a few sessions, then try again.\n"));
14222
14576
  return;
@@ -14246,22 +14600,110 @@ function registerSessionsCommand(program2) {
14246
14600
  });
14247
14601
  }
14248
14602
 
14603
+ // src/cli/commands/skill-pin.ts
14604
+ import chalk23 from "chalk";
14605
+ import fs34 from "fs";
14606
+ import os30 from "os";
14607
+ import path37 from "path";
14608
+ function wipeSkillSessions() {
14609
+ try {
14610
+ fs34.rmSync(path37.join(os30.homedir(), ".node9", "skill-sessions"), {
14611
+ recursive: true,
14612
+ force: true
14613
+ });
14614
+ } catch {
14615
+ }
14616
+ }
14617
+ function registerSkillPinCommand(program2) {
14618
+ const skillCmd = program2.command("skill").description("Manage skill pinning (supply chain & update drift defense, AST 02 + AST 07)");
14619
+ const pinSubCmd = skillCmd.command("pin").description("Manage pinned skill roots");
14620
+ pinSubCmd.command("list").description("Show all pinned skill roots and their content hashes").action(() => {
14621
+ const result = readSkillPinsSafe();
14622
+ if (!result.ok) {
14623
+ if (result.reason === "missing") {
14624
+ console.log(chalk23.gray("\nNo skill roots are pinned yet."));
14625
+ console.log(
14626
+ chalk23.gray("Pins are created automatically on the first tool call of each session.\n")
14627
+ );
14628
+ return;
14629
+ }
14630
+ console.error(chalk23.red(`
14631
+ \u274C Pin file is corrupt: ${result.detail}`));
14632
+ console.error(chalk23.yellow(" Run: node9 skill pin reset\n"));
14633
+ process.exit(1);
14634
+ }
14635
+ const entries = Object.entries(result.pins.roots);
14636
+ if (entries.length === 0) {
14637
+ console.log(chalk23.gray("\nNo skill roots are pinned yet.\n"));
14638
+ return;
14639
+ }
14640
+ console.log(chalk23.bold("\n\u{1F512} Pinned Skill Roots\n"));
14641
+ for (const [key, entry] of entries) {
14642
+ const missing = entry.exists ? "" : chalk23.yellow(" (not present at pin time)");
14643
+ console.log(` ${chalk23.cyan(key)} ${chalk23.gray(entry.rootPath)}${missing}`);
14644
+ console.log(` Files (${entry.fileCount})`);
14645
+ console.log(` Hash: ${chalk23.gray(entry.contentHash.slice(0, 16))}...`);
14646
+ console.log(` Pinned: ${chalk23.gray(entry.pinnedAt)}
14647
+ `);
14648
+ }
14649
+ });
14650
+ pinSubCmd.command("update <rootKey>").description("Remove a pin so the next session re-pins with current state").action((rootKey) => {
14651
+ let pins;
14652
+ try {
14653
+ pins = readSkillPins();
14654
+ } catch {
14655
+ console.error(chalk23.red("\n\u274C Pin file is corrupt."));
14656
+ console.error(chalk23.yellow(" Run: node9 skill pin reset\n"));
14657
+ process.exit(1);
14658
+ }
14659
+ if (!pins.roots[rootKey]) {
14660
+ console.error(chalk23.red(`
14661
+ \u274C No pin found for root key "${rootKey}"
14662
+ `));
14663
+ console.error(`Run ${chalk23.cyan("node9 skill pin list")} to see pinned roots.
14664
+ `);
14665
+ process.exit(1);
14666
+ }
14667
+ const rootPath = pins.roots[rootKey].rootPath;
14668
+ removePin(rootKey);
14669
+ wipeSkillSessions();
14670
+ console.log(chalk23.green(`
14671
+ \u{1F513} Pin removed for ${chalk23.cyan(rootKey)}`));
14672
+ console.log(chalk23.gray(` ${rootPath}`));
14673
+ console.log(chalk23.gray(" Next session will re-pin with current state.\n"));
14674
+ });
14675
+ pinSubCmd.command("reset").description("Clear all skill pins and wipe session verification flags").action(() => {
14676
+ const result = readSkillPinsSafe();
14677
+ if (!result.ok && result.reason === "missing") {
14678
+ wipeSkillSessions();
14679
+ console.log(chalk23.gray("\nNo pins to clear.\n"));
14680
+ return;
14681
+ }
14682
+ const count = result.ok ? Object.keys(result.pins.roots).length : "?";
14683
+ clearAllPins();
14684
+ wipeSkillSessions();
14685
+ console.log(chalk23.green(`
14686
+ \u{1F513} Cleared ${count} skill pin(s).`));
14687
+ console.log(chalk23.gray(" Next session will re-pin with current state.\n"));
14688
+ });
14689
+ }
14690
+
14249
14691
  // src/cli.ts
14250
14692
  var { version } = JSON.parse(
14251
- fs35.readFileSync(path38.join(__dirname, "../package.json"), "utf-8")
14693
+ fs37.readFileSync(path40.join(__dirname, "../package.json"), "utf-8")
14252
14694
  );
14253
14695
  var program = new Command();
14254
14696
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
14255
14697
  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
14698
  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 });
14699
+ const credPath = path40.join(os33.homedir(), ".node9", "credentials.json");
14700
+ if (!fs37.existsSync(path40.dirname(credPath)))
14701
+ fs37.mkdirSync(path40.dirname(credPath), { recursive: true });
14260
14702
  const profileName = options.profile || "default";
14261
14703
  let existingCreds = {};
14262
14704
  try {
14263
- if (fs35.existsSync(credPath)) {
14264
- const raw = JSON.parse(fs35.readFileSync(credPath, "utf-8"));
14705
+ if (fs37.existsSync(credPath)) {
14706
+ const raw = JSON.parse(fs37.readFileSync(credPath, "utf-8"));
14265
14707
  if (raw.apiKey) {
14266
14708
  existingCreds = {
14267
14709
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
@@ -14273,13 +14715,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
14273
14715
  } catch {
14274
14716
  }
14275
14717
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
14276
- fs35.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
14718
+ fs37.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
14277
14719
  if (profileName === "default") {
14278
- const configPath = path38.join(os31.homedir(), ".node9", "config.json");
14720
+ const configPath = path40.join(os33.homedir(), ".node9", "config.json");
14279
14721
  let config = {};
14280
14722
  try {
14281
- if (fs35.existsSync(configPath))
14282
- config = JSON.parse(fs35.readFileSync(configPath, "utf-8"));
14723
+ if (fs37.existsSync(configPath))
14724
+ config = JSON.parse(fs37.readFileSync(configPath, "utf-8"));
14283
14725
  } catch {
14284
14726
  }
14285
14727
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -14294,19 +14736,19 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
14294
14736
  approvers.cloud = false;
14295
14737
  }
14296
14738
  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 });
14739
+ if (!fs37.existsSync(path40.dirname(configPath)))
14740
+ fs37.mkdirSync(path40.dirname(configPath), { recursive: true });
14741
+ fs37.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
14300
14742
  }
14301
14743
  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`));
14744
+ console.log(chalk25.green(`\u2705 Profile "${profileName}" saved`));
14745
+ console.log(chalk25.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
14304
14746
  } 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.`));
14747
+ console.log(chalk25.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
14748
+ console.log(chalk25.gray(` All decisions stay on this machine.`));
14307
14749
  } 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.`));
14750
+ console.log(chalk25.green(`\u2705 Logged in \u2014 agent mode`));
14751
+ console.log(chalk25.gray(` Team policy enforced for all calls via Node9 cloud.`));
14310
14752
  }
14311
14753
  });
14312
14754
  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) => {
@@ -14317,7 +14759,7 @@ program.command("addto").description("Integrate Node9 with an AI agent").addHelp
14317
14759
  if (target === "vscode") return await setupVSCode();
14318
14760
  if (target === "hud") return setupHud();
14319
14761
  console.error(
14320
- chalk24.red(
14762
+ chalk25.red(
14321
14763
  `Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
14322
14764
  )
14323
14765
  );
@@ -14325,16 +14767,16 @@ program.command("addto").description("Integrate Node9 with an AI agent").addHelp
14325
14767
  });
14326
14768
  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) => {
14327
14769
  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");
14770
+ console.log(chalk25.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
14771
+ console.log(" Usage: " + chalk25.white("node9 setup <target>") + "\n");
14330
14772
  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)");
14773
+ console.log(" " + chalk25.green("claude") + " \u2014 Claude Code (hook mode)");
14774
+ console.log(" " + chalk25.green("gemini") + " \u2014 Gemini CLI (hook mode)");
14775
+ console.log(" " + chalk25.green("cursor") + " \u2014 Cursor (MCP proxy)");
14776
+ console.log(" " + chalk25.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
14777
+ console.log(" " + chalk25.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
14336
14778
  process.stdout.write(
14337
- " " + chalk24.green("hud") + " \u2014 Claude Code security statusline\n"
14779
+ " " + chalk25.green("hud") + " \u2014 Claude Code security statusline\n"
14338
14780
  );
14339
14781
  console.log("");
14340
14782
  return;
@@ -14347,7 +14789,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
14347
14789
  if (t === "vscode") return await setupVSCode();
14348
14790
  if (t === "hud") return setupHud();
14349
14791
  console.error(
14350
- chalk24.red(
14792
+ chalk25.red(
14351
14793
  `Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
14352
14794
  )
14353
14795
  );
@@ -14366,33 +14808,33 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
14366
14808
  else if (target === "hud") fn = teardownHud;
14367
14809
  else {
14368
14810
  console.error(
14369
- chalk24.red(
14811
+ chalk25.red(
14370
14812
  `Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
14371
14813
  )
14372
14814
  );
14373
14815
  process.exit(1);
14374
14816
  }
14375
- console.log(chalk24.cyan(`
14817
+ console.log(chalk25.cyan(`
14376
14818
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
14377
14819
  `));
14378
14820
  try {
14379
14821
  fn();
14380
14822
  } catch (err2) {
14381
- console.error(chalk24.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
14823
+ console.error(chalk25.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
14382
14824
  process.exit(1);
14383
14825
  }
14384
- console.log(chalk24.gray("\n Restart the agent for changes to take effect."));
14826
+ console.log(chalk25.gray("\n Restart the agent for changes to take effect."));
14385
14827
  });
14386
14828
  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..."));
14829
+ console.log(chalk25.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
14830
+ console.log(chalk25.bold("Stopping daemon..."));
14389
14831
  try {
14390
14832
  stopDaemon();
14391
- console.log(chalk24.green(" \u2705 Daemon stopped"));
14833
+ console.log(chalk25.green(" \u2705 Daemon stopped"));
14392
14834
  } catch {
14393
- console.log(chalk24.blue(" \u2139\uFE0F Daemon was not running"));
14835
+ console.log(chalk25.blue(" \u2139\uFE0F Daemon was not running"));
14394
14836
  }
14395
- console.log(chalk24.bold("\nRemoving hooks..."));
14837
+ console.log(chalk25.bold("\nRemoving hooks..."));
14396
14838
  let teardownFailed = false;
14397
14839
  for (const [label, fn] of [
14398
14840
  ["Claude", teardownClaude],
@@ -14406,45 +14848,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
14406
14848
  } catch (err2) {
14407
14849
  teardownFailed = true;
14408
14850
  console.error(
14409
- chalk24.red(
14851
+ chalk25.red(
14410
14852
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
14411
14853
  )
14412
14854
  );
14413
14855
  }
14414
14856
  }
14415
14857
  if (options.purge) {
14416
- const node9Dir = path38.join(os31.homedir(), ".node9");
14417
- if (fs35.existsSync(node9Dir)) {
14858
+ const node9Dir = path40.join(os33.homedir(), ".node9");
14859
+ if (fs37.existsSync(node9Dir)) {
14418
14860
  const confirmed = await confirm2({
14419
14861
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
14420
14862
  default: false
14421
14863
  });
14422
14864
  if (confirmed) {
14423
- fs35.rmSync(node9Dir, { recursive: true });
14424
- if (fs35.existsSync(node9Dir)) {
14865
+ fs37.rmSync(node9Dir, { recursive: true });
14866
+ if (fs37.existsSync(node9Dir)) {
14425
14867
  console.error(
14426
- chalk24.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
14868
+ chalk25.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
14427
14869
  );
14428
14870
  } else {
14429
- console.log(chalk24.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
14871
+ console.log(chalk25.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
14430
14872
  }
14431
14873
  } else {
14432
- console.log(chalk24.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
14874
+ console.log(chalk25.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
14433
14875
  }
14434
14876
  } else {
14435
- console.log(chalk24.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
14877
+ console.log(chalk25.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
14436
14878
  }
14437
14879
  } else {
14438
14880
  console.log(
14439
- chalk24.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
14881
+ chalk25.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
14440
14882
  );
14441
14883
  }
14442
14884
  if (teardownFailed) {
14443
- console.error(chalk24.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
14885
+ console.error(chalk25.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
14444
14886
  process.exit(1);
14445
14887
  }
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"));
14888
+ console.log(chalk25.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
14889
+ console.log(chalk25.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
14448
14890
  });
14449
14891
  registerDoctorCommand(program, version);
14450
14892
  program.command("explain").description(
@@ -14457,7 +14899,7 @@ program.command("explain").description(
14457
14899
  try {
14458
14900
  args = JSON.parse(trimmed);
14459
14901
  } catch {
14460
- console.error(chalk24.red(`
14902
+ console.error(chalk25.red(`
14461
14903
  \u274C Invalid JSON: ${trimmed}
14462
14904
  `));
14463
14905
  process.exit(1);
@@ -14468,54 +14910,54 @@ program.command("explain").description(
14468
14910
  }
14469
14911
  const result = await explainPolicy(tool, args);
14470
14912
  console.log("");
14471
- console.log(chalk24.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
14913
+ console.log(chalk25.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
14472
14914
  console.log("");
14473
- console.log(` ${chalk24.bold("Tool:")} ${chalk24.white(result.tool)}`);
14915
+ console.log(` ${chalk25.bold("Tool:")} ${chalk25.white(result.tool)}`);
14474
14916
  if (argsRaw) {
14475
14917
  const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
14476
- console.log(` ${chalk24.bold("Input:")} ${chalk24.gray(preview2)}`);
14918
+ console.log(` ${chalk25.bold("Input:")} ${chalk25.gray(preview2)}`);
14477
14919
  }
14478
14920
  console.log("");
14479
- console.log(chalk24.bold("Config Sources (Waterfall):"));
14921
+ console.log(chalk25.bold("Config Sources (Waterfall):"));
14480
14922
  for (const tier of result.waterfall) {
14481
- const num3 = chalk24.gray(` ${tier.tier}.`);
14923
+ const num3 = chalk25.gray(` ${tier.tier}.`);
14482
14924
  const label = tier.label.padEnd(16);
14483
14925
  let statusStr;
14484
14926
  if (tier.tier === 1) {
14485
- statusStr = chalk24.gray(tier.note ?? "");
14927
+ statusStr = chalk25.gray(tier.note ?? "");
14486
14928
  } 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 : "");
14929
+ const loc = tier.path ? chalk25.gray(tier.path) : "";
14930
+ const note = tier.note ? chalk25.gray(`(${tier.note})`) : "";
14931
+ statusStr = chalk25.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
14490
14932
  } else {
14491
- statusStr = chalk24.gray("\u25CB " + (tier.note ?? "not found"));
14933
+ statusStr = chalk25.gray("\u25CB " + (tier.note ?? "not found"));
14492
14934
  }
14493
- console.log(`${num3} ${chalk24.white(label)} ${statusStr}`);
14935
+ console.log(`${num3} ${chalk25.white(label)} ${statusStr}`);
14494
14936
  }
14495
14937
  console.log("");
14496
- console.log(chalk24.bold("Policy Evaluation:"));
14938
+ console.log(chalk25.bold("Policy Evaluation:"));
14497
14939
  for (const step of result.steps) {
14498
14940
  const isFinal = step.isFinal;
14499
14941
  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 ");
14942
+ if (step.outcome === "allow") icon = chalk25.green(" \u2705");
14943
+ else if (step.outcome === "review") icon = chalk25.red(" \u{1F534}");
14944
+ else if (step.outcome === "skip") icon = chalk25.gray(" \u2500 ");
14945
+ else icon = chalk25.gray(" \u25CB ");
14504
14946
  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") : "";
14947
+ const nameStr = isFinal ? chalk25.white.bold(name) : chalk25.white(name);
14948
+ const detail = isFinal ? chalk25.white(step.detail) : chalk25.gray(step.detail);
14949
+ const arrow = isFinal ? chalk25.yellow(" \u2190 STOP") : "";
14508
14950
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
14509
14951
  }
14510
14952
  console.log("");
14511
14953
  if (result.decision === "allow") {
14512
- console.log(chalk24.green.bold(" Decision: \u2705 ALLOW") + chalk24.gray(" \u2014 no approval needed"));
14954
+ console.log(chalk25.green.bold(" Decision: \u2705 ALLOW") + chalk25.gray(" \u2014 no approval needed"));
14513
14955
  } else {
14514
14956
  console.log(
14515
- chalk24.red.bold(" Decision: \u{1F534} REVIEW") + chalk24.gray(" \u2014 human approval required")
14957
+ chalk25.red.bold(" Decision: \u{1F534} REVIEW") + chalk25.gray(" \u2014 human approval required")
14516
14958
  );
14517
14959
  if (result.blockedByLabel) {
14518
- console.log(chalk24.gray(` Reason: ${result.blockedByLabel}`));
14960
+ console.log(chalk25.gray(` Reason: ${result.blockedByLabel}`));
14519
14961
  }
14520
14962
  }
14521
14963
  console.log("");
@@ -14530,7 +14972,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
14530
14972
  try {
14531
14973
  await startTail2(options);
14532
14974
  } catch (err2) {
14533
- console.error(chalk24.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
14975
+ console.error(chalk25.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
14534
14976
  process.exit(1);
14535
14977
  }
14536
14978
  });
@@ -14538,6 +14980,7 @@ registerWatchCommand(program);
14538
14980
  registerMcpGatewayCommand(program);
14539
14981
  registerMcpServerCommand(program);
14540
14982
  registerMcpPinCommand(program);
14983
+ registerSkillPinCommand(program);
14541
14984
  registerCheckCommand(program);
14542
14985
  registerLogCommand(program);
14543
14986
  program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").addHelpText(
@@ -14562,14 +15005,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
14562
15005
  Run "node9 addto claude" to register it as the statusLine.`
14563
15006
  ).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
15007
  if (subcommand === "debug") {
14565
- const flagFile = path38.join(os31.homedir(), ".node9", "hud-debug");
15008
+ const flagFile = path40.join(os33.homedir(), ".node9", "hud-debug");
14566
15009
  if (state === "on") {
14567
- fs35.mkdirSync(path38.dirname(flagFile), { recursive: true });
14568
- fs35.writeFileSync(flagFile, "");
15010
+ fs37.mkdirSync(path40.dirname(flagFile), { recursive: true });
15011
+ fs37.writeFileSync(flagFile, "");
14569
15012
  console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
14570
15013
  console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
14571
15014
  } else if (state === "off") {
14572
- if (fs35.existsSync(flagFile)) fs35.unlinkSync(flagFile);
15015
+ if (fs37.existsSync(flagFile)) fs37.unlinkSync(flagFile);
14573
15016
  console.log("HUD debug logging disabled.");
14574
15017
  } else {
14575
15018
  console.error("Usage: node9 hud debug on|off");
@@ -14584,7 +15027,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
14584
15027
  const ms = parseDuration(options.duration);
14585
15028
  if (ms === null) {
14586
15029
  console.error(
14587
- chalk24.red(`
15030
+ chalk25.red(`
14588
15031
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
14589
15032
  `)
14590
15033
  );
@@ -14592,20 +15035,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
14592
15035
  }
14593
15036
  pauseNode9(ms, options.duration);
14594
15037
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
14595
- console.log(chalk24.yellow(`
15038
+ console.log(chalk25.yellow(`
14596
15039
  \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.
15040
+ console.log(chalk25.gray(` All tool calls will be allowed without review.`));
15041
+ console.log(chalk25.gray(` Run "node9 resume" to re-enable early.
14599
15042
  `));
14600
15043
  });
14601
15044
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
14602
15045
  const { paused } = checkPause();
14603
15046
  if (!paused) {
14604
- console.log(chalk24.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
15047
+ console.log(chalk25.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
14605
15048
  return;
14606
15049
  }
14607
15050
  resumeNode9();
14608
- console.log(chalk24.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
15051
+ console.log(chalk25.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
14609
15052
  });
14610
15053
  var HOOK_BASED_AGENTS = {
14611
15054
  claude: "claude",
@@ -14618,15 +15061,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
14618
15061
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
14619
15062
  const target = HOOK_BASED_AGENTS[firstArg2];
14620
15063
  console.error(
14621
- chalk24.yellow(`
15064
+ chalk25.yellow(`
14622
15065
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
14623
15066
  );
14624
- console.error(chalk24.white(`
15067
+ console.error(chalk25.white(`
14625
15068
  "${target}" uses its own hook system. Use:`));
14626
15069
  console.error(
14627
- chalk24.green(` node9 addto ${target} `) + chalk24.gray("# one-time setup")
15070
+ chalk25.green(` node9 addto ${target} `) + chalk25.gray("# one-time setup")
14628
15071
  );
14629
- console.error(chalk24.green(` ${target} `) + chalk24.gray("# run normally"));
15072
+ console.error(chalk25.green(` ${target} `) + chalk25.gray("# run normally"));
14630
15073
  process.exit(1);
14631
15074
  }
14632
15075
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -14643,7 +15086,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
14643
15086
  }
14644
15087
  );
14645
15088
  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..."));
15089
+ console.error(chalk25.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
14647
15090
  const daemonReady = await autoStartDaemonAndWait();
14648
15091
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
14649
15092
  }
@@ -14656,12 +15099,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
14656
15099
  }
14657
15100
  if (!result.approved) {
14658
15101
  console.error(
14659
- chalk24.red(`
15102
+ chalk25.red(`
14660
15103
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
14661
15104
  );
14662
15105
  process.exit(1);
14663
15106
  }
14664
- console.error(chalk24.green("\n\u2705 Approved \u2014 running command...\n"));
15107
+ console.error(chalk25.green("\n\u2705 Approved \u2014 running command...\n"));
14665
15108
  await runProxy(fullCommand);
14666
15109
  } else {
14667
15110
  program.help();
@@ -14680,9 +15123,9 @@ if (process.argv[2] !== "daemon") {
14680
15123
  const isCheckHook = process.argv[2] === "check";
14681
15124
  if (isCheckHook) {
14682
15125
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
14683
- const logPath = path38.join(os31.homedir(), ".node9", "hook-debug.log");
15126
+ const logPath = path40.join(os33.homedir(), ".node9", "hook-debug.log");
14684
15127
  const msg = reason instanceof Error ? reason.message : String(reason);
14685
- fs35.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
15128
+ fs37.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
14686
15129
  `);
14687
15130
  }
14688
15131
  process.exit(0);