@node9/proxy 1.39.0 → 1.40.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.
Files changed (4) hide show
  1. package/README.md +55 -0
  2. package/dist/cli.js +993 -461
  3. package/dist/cli.mjs +989 -457
  4. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -185,8 +185,8 @@ function sanitizeConfig(raw) {
185
185
  }
186
186
  }
187
187
  const lines = result.error.issues.map((issue) => {
188
- const path58 = issue.path.length > 0 ? issue.path.join(".") : "root";
189
- return ` \u2022 ${path58}: ${issue.message}`;
188
+ const path61 = issue.path.length > 0 ? issue.path.join(".") : "root";
189
+ return ` \u2022 ${path61}: ${issue.message}`;
190
190
  });
191
191
  return {
192
192
  sanitized,
@@ -1258,9 +1258,9 @@ function matchesPattern(text, patterns) {
1258
1258
  const withoutDotSlash = text.replace(/^\.\//, "");
1259
1259
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1260
1260
  }
1261
- function getNestedValue(obj, path58) {
1261
+ function getNestedValue(obj, path61) {
1262
1262
  if (!obj || typeof obj !== "object") return null;
1263
- const segments = path58.split(".");
1263
+ const segments = path61.split(".");
1264
1264
  for (const seg of segments) {
1265
1265
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
1266
1266
  }
@@ -17198,10 +17198,10 @@ __export(tail_exports, {
17198
17198
  startTail: () => startTail
17199
17199
  });
17200
17200
  import http3 from "http";
17201
- import chalk33 from "chalk";
17202
- import fs56 from "fs";
17203
- import os51 from "os";
17204
- import path55 from "path";
17201
+ import chalk34 from "chalk";
17202
+ import fs60 from "fs";
17203
+ import os52 from "os";
17204
+ import path58 from "path";
17205
17205
  import readline6 from "readline";
17206
17206
  import { spawn as spawn8 } from "child_process";
17207
17207
  function shortenPathSummary(s) {
@@ -17225,20 +17225,20 @@ function getModelContextLimit(model) {
17225
17225
  return 2e5;
17226
17226
  }
17227
17227
  function readSessionUsage() {
17228
- const projectsDir = path55.join(os51.homedir(), ".claude", "projects");
17229
- if (!fs56.existsSync(projectsDir)) return null;
17228
+ const projectsDir = path58.join(os52.homedir(), ".claude", "projects");
17229
+ if (!fs60.existsSync(projectsDir)) return null;
17230
17230
  let latestFile = null;
17231
17231
  let latestMtime = 0;
17232
17232
  try {
17233
- for (const dir of fs56.readdirSync(projectsDir)) {
17234
- const dirPath = path55.join(projectsDir, dir);
17233
+ for (const dir of fs60.readdirSync(projectsDir)) {
17234
+ const dirPath = path58.join(projectsDir, dir);
17235
17235
  try {
17236
- if (!fs56.statSync(dirPath).isDirectory()) continue;
17237
- for (const file of fs56.readdirSync(dirPath)) {
17236
+ if (!fs60.statSync(dirPath).isDirectory()) continue;
17237
+ for (const file of fs60.readdirSync(dirPath)) {
17238
17238
  if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
17239
- const filePath = path55.join(dirPath, file);
17239
+ const filePath = path58.join(dirPath, file);
17240
17240
  try {
17241
- const mtime = fs56.statSync(filePath).mtimeMs;
17241
+ const mtime = fs60.statSync(filePath).mtimeMs;
17242
17242
  if (mtime > latestMtime) {
17243
17243
  latestMtime = mtime;
17244
17244
  latestFile = filePath;
@@ -17253,7 +17253,7 @@ function readSessionUsage() {
17253
17253
  }
17254
17254
  if (!latestFile) return null;
17255
17255
  try {
17256
- const lines = fs56.readFileSync(latestFile, "utf-8").split("\n");
17256
+ const lines = fs60.readFileSync(latestFile, "utf-8").split("\n");
17257
17257
  let lastModel = "";
17258
17258
  let lastInput = 0;
17259
17259
  let lastOutput = 0;
@@ -17278,10 +17278,10 @@ function readSessionUsage() {
17278
17278
  }
17279
17279
  }
17280
17280
  function formatContextStat(stat) {
17281
- const pctColor = stat.fillPct >= 80 ? chalk33.red : stat.fillPct >= 50 ? chalk33.yellow : chalk33.cyan;
17281
+ const pctColor = stat.fillPct >= 80 ? chalk34.red : stat.fillPct >= 50 ? chalk34.yellow : chalk34.cyan;
17282
17282
  const k = (n) => `${Math.round(n / 1e3)}k`;
17283
17283
  const modelShort = stat.model.replace(/@.*$/, "").replace(/-\d{8}$/, "").replace(/^claude-/, "");
17284
- return chalk33.dim("ctx: ") + pctColor(`${stat.fillPct}%`) + chalk33.dim(
17284
+ return chalk34.dim("ctx: ") + pctColor(`${stat.fillPct}%`) + chalk34.dim(
17285
17285
  ` (${k(stat.inputTokens)}/${k(getModelContextLimit(stat.model))} out ${k(stat.outputTokens)} \xB7 ${modelShort})`
17286
17286
  );
17287
17287
  }
@@ -17304,32 +17304,32 @@ function agentLabel(agent, mcpServer, sessionId) {
17304
17304
  const tag = sessionTag(sessionId);
17305
17305
  const tagSuffix = tag ? `\xB7${tag}` : "";
17306
17306
  if (!agent || agent === "Terminal") {
17307
- return mcpServer ? chalk33.dim(`[\u2192 ${mcpServer}] `) : "";
17307
+ return mcpServer ? chalk34.dim(`[\u2192 ${mcpServer}] `) : "";
17308
17308
  }
17309
17309
  const short = agent === "Claude Code" ? "Claude" : agent === "Gemini CLI" ? "Gemini" : agent === "Unknown Agent" ? "" : agent.split(" ")[0];
17310
- if (!short) return mcpServer ? chalk33.dim(`[\u2192 ${mcpServer}] `) : "";
17311
- return mcpServer ? chalk33.dim(`[${short}${tagSuffix} \u2192 ${mcpServer}] `) : chalk33.dim(`[${short}${tagSuffix}] `);
17310
+ if (!short) return mcpServer ? chalk34.dim(`[\u2192 ${mcpServer}] `) : "";
17311
+ return mcpServer ? chalk34.dim(`[${short}${tagSuffix} \u2192 ${mcpServer}] `) : chalk34.dim(`[${short}${tagSuffix}] `);
17312
17312
  }
17313
17313
  function formatBase(activity) {
17314
17314
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
17315
17315
  const icon = getIcon(activity.tool);
17316
17316
  const toolName = activity.tool.slice(0, 16).padEnd(16);
17317
- const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os51.homedir(), "~");
17317
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os52.homedir(), "~");
17318
17318
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
17319
- return `${chalk33.gray(time)} ${icon} ${agentLabel(activity.agent, activity.mcpServer, activity.sessionId)}${chalk33.white.bold(toolName)} ${chalk33.dim(argsPreview)}`;
17319
+ return `${chalk34.gray(time)} ${icon} ${agentLabel(activity.agent, activity.mcpServer, activity.sessionId)}${chalk34.white.bold(toolName)} ${chalk34.dim(argsPreview)}`;
17320
17320
  }
17321
17321
  function renderResult(activity, result) {
17322
17322
  const base = formatBase(activity);
17323
17323
  let status;
17324
17324
  if (result.status === "allow") {
17325
- status = chalk33.green("\u2713 ALLOW");
17325
+ status = chalk34.green("\u2713 ALLOW");
17326
17326
  } else if (result.status === "dlp") {
17327
- status = chalk33.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
17327
+ status = chalk34.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
17328
17328
  } else {
17329
- status = chalk33.red("\u2717 BLOCK");
17329
+ status = chalk34.red("\u2717 BLOCK");
17330
17330
  }
17331
17331
  const cost = result.costEstimate ?? activity.costEstimate;
17332
- const costSuffix = cost == null ? "" : chalk33.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
17332
+ const costSuffix = cost == null ? "" : chalk34.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
17333
17333
  if (process.stdout.isTTY) {
17334
17334
  if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
17335
17335
  readline6.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
@@ -17346,19 +17346,19 @@ function renderResult(activity, result) {
17346
17346
  }
17347
17347
  function renderPending(activity) {
17348
17348
  if (!process.stdout.isTTY) return;
17349
- const line = `${formatBase(activity)} ${chalk33.yellow("\u25CF \u2026")}`;
17349
+ const line = `${formatBase(activity)} ${chalk34.yellow("\u25CF \u2026")}`;
17350
17350
  pendingShownForId = activity.id;
17351
17351
  pendingWrappedLines = wrappedLineCount(line);
17352
17352
  process.stdout.write(`${line}\r`);
17353
17353
  }
17354
17354
  async function ensureDaemon() {
17355
17355
  let pidPort = null;
17356
- if (fs56.existsSync(PID_FILE)) {
17356
+ if (fs60.existsSync(PID_FILE)) {
17357
17357
  try {
17358
- const { port } = JSON.parse(fs56.readFileSync(PID_FILE, "utf-8"));
17358
+ const { port } = JSON.parse(fs60.readFileSync(PID_FILE, "utf-8"));
17359
17359
  pidPort = port;
17360
17360
  } catch {
17361
- console.error(chalk33.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
17361
+ console.error(chalk34.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
17362
17362
  }
17363
17363
  }
17364
17364
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -17369,7 +17369,7 @@ async function ensureDaemon() {
17369
17369
  if (res.ok) return checkPort;
17370
17370
  } catch {
17371
17371
  }
17372
- console.log(chalk33.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
17372
+ console.log(chalk34.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
17373
17373
  const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
17374
17374
  detached: true,
17375
17375
  stdio: "ignore",
@@ -17386,7 +17386,7 @@ async function ensureDaemon() {
17386
17386
  } catch {
17387
17387
  }
17388
17388
  }
17389
- console.error(chalk33.red("\u274C Daemon failed to start. Try: node9 daemon start"));
17389
+ console.error(chalk34.red("\u274C Daemon failed to start. Try: node9 daemon start"));
17390
17390
  process.exit(1);
17391
17391
  }
17392
17392
  function postDecisionHttp(id, decision, authToken, port, opts) {
@@ -17455,7 +17455,7 @@ function buildCardLines(req, localCount = 0) {
17455
17455
  const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
17456
17456
  const rawDesc = req.riskMetadata?.ruleDescription ?? "";
17457
17457
  const description = rawDesc ? cleanReason(rawDesc) : "";
17458
- const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${chalk33.dim(`(${req.agent})`)}` : "";
17458
+ const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${chalk34.dim(`(${req.agent})`)}` : "";
17459
17459
  const lines = [
17460
17460
  ``,
17461
17461
  `${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
@@ -17511,9 +17511,9 @@ function buildRecoveryCardLines(req) {
17511
17511
  ];
17512
17512
  }
17513
17513
  function readApproversFromDisk() {
17514
- const configPath2 = path55.join(os51.homedir(), ".node9", "config.json");
17514
+ const configPath2 = path58.join(os52.homedir(), ".node9", "config.json");
17515
17515
  try {
17516
- const raw = JSON.parse(fs56.readFileSync(configPath2, "utf-8"));
17516
+ const raw = JSON.parse(fs60.readFileSync(configPath2, "utf-8"));
17517
17517
  const settings = raw.settings ?? {};
17518
17518
  return settings.approvers ?? {};
17519
17519
  } catch {
@@ -17524,20 +17524,20 @@ function approverStatusLine() {
17524
17524
  const a = readApproversFromDisk();
17525
17525
  const fmt = (label2, key) => {
17526
17526
  const on = a[key] !== false;
17527
- return `[${key[0]}]${label2.slice(1)} ${on ? chalk33.green("\u2713") : chalk33.dim("\u2717")}`;
17527
+ return `[${key[0]}]${label2.slice(1)} ${on ? chalk34.green("\u2713") : chalk34.dim("\u2717")}`;
17528
17528
  };
17529
17529
  return `${fmt("native", "native")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
17530
17530
  }
17531
17531
  function toggleApprover(channel) {
17532
- const configPath2 = path55.join(os51.homedir(), ".node9", "config.json");
17532
+ const configPath2 = path58.join(os52.homedir(), ".node9", "config.json");
17533
17533
  try {
17534
- const raw = JSON.parse(fs56.readFileSync(configPath2, "utf-8"));
17534
+ const raw = JSON.parse(fs60.readFileSync(configPath2, "utf-8"));
17535
17535
  const settings = raw.settings ?? {};
17536
17536
  const approvers = settings.approvers ?? {};
17537
17537
  approvers[channel] = approvers[channel] === false;
17538
17538
  settings.approvers = approvers;
17539
17539
  raw.settings = settings;
17540
- fs56.writeFileSync(configPath2, JSON.stringify(raw, null, 2) + "\n");
17540
+ fs60.writeFileSync(configPath2, JSON.stringify(raw, null, 2) + "\n");
17541
17541
  } catch (err2) {
17542
17542
  process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
17543
17543
  `);
@@ -17569,7 +17569,7 @@ async function startTail(options = {}) {
17569
17569
  req2.end();
17570
17570
  });
17571
17571
  if (result.ok) {
17572
- console.log(chalk33.green("\u2713 Flight Recorder buffer cleared."));
17572
+ console.log(chalk34.green("\u2713 Flight Recorder buffer cleared."));
17573
17573
  } else if (result.code === "ECONNREFUSED") {
17574
17574
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
17575
17575
  } else if (result.code === "ETIMEDOUT") {
@@ -17615,7 +17615,7 @@ async function startTail(options = {}) {
17615
17615
  const channel = name === "n" ? "native" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
17616
17616
  if (channel) {
17617
17617
  toggleApprover(channel);
17618
- console.log(chalk33.dim(` Approvers: ${approverStatusLine()}`));
17618
+ console.log(chalk34.dim(` Approvers: ${approverStatusLine()}`));
17619
17619
  }
17620
17620
  };
17621
17621
  process.stdin.on("keypress", idleKeypressHandler);
@@ -17681,7 +17681,7 @@ async function startTail(options = {}) {
17681
17681
  localAllowCounts.get(req2.toolName) ?? 0
17682
17682
  )
17683
17683
  );
17684
- const decisionStamp = action === "always-allow" ? chalk33.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk33.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk33.green("\u2713 ALLOWED") : action === "redirect" ? chalk33.yellow("\u21A9 REDIRECT AI") : chalk33.red("\u2717 DENIED");
17684
+ const decisionStamp = action === "always-allow" ? chalk34.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk34.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk34.green("\u2713 ALLOWED") : action === "redirect" ? chalk34.yellow("\u21A9 REDIRECT AI") : chalk34.red("\u2717 DENIED");
17685
17685
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
17686
17686
  for (const line of stampedLines) process.stdout.write(line + "\n");
17687
17687
  process.stdout.write(SHOW_CURSOR);
@@ -17709,8 +17709,8 @@ async function startTail(options = {}) {
17709
17709
  }
17710
17710
  postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).catch((err2) => {
17711
17711
  try {
17712
- fs56.appendFileSync(
17713
- path55.join(os51.homedir(), ".node9", "hook-debug.log"),
17712
+ fs60.appendFileSync(
17713
+ path58.join(os52.homedir(), ".node9", "hook-debug.log"),
17714
17714
  `[tail] POST /decision failed: ${String(err2)}
17715
17715
  `
17716
17716
  );
@@ -17732,7 +17732,7 @@ async function startTail(options = {}) {
17732
17732
  );
17733
17733
  const stampedLines = buildCardLines(req2, priorCount);
17734
17734
  if (externalDecision) {
17735
- const source = externalDecision === "allow" ? chalk33.green("\u2713 ALLOWED") : chalk33.red("\u2717 DENIED");
17735
+ const source = externalDecision === "allow" ? chalk34.green("\u2713 ALLOWED") : chalk34.red("\u2717 DENIED");
17736
17736
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
17737
17737
  }
17738
17738
  for (const line of stampedLines) process.stdout.write(line + "\n");
@@ -17774,31 +17774,31 @@ async function startTail(options = {}) {
17774
17774
  };
17775
17775
  process.stdin.on("keypress", onKeypress);
17776
17776
  }
17777
- const auditLog = path55.join(os51.homedir(), ".node9", "audit.log");
17777
+ const auditLog = path58.join(os52.homedir(), ".node9", "audit.log");
17778
17778
  try {
17779
- const unackedDlp = fs56.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
17779
+ const unackedDlp = fs60.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
17780
17780
  if (unackedDlp > 0) {
17781
17781
  console.log("");
17782
17782
  console.log(
17783
- chalk33.bgRed.white.bold(
17783
+ chalk34.bgRed.white.bold(
17784
17784
  ` \u26A0\uFE0F DLP ALERT: ${unackedDlp} secret${unackedDlp !== 1 ? "s" : ""} found in Claude response text \u2014 run: node9 dlp `
17785
17785
  )
17786
17786
  );
17787
17787
  }
17788
17788
  } catch {
17789
17789
  }
17790
- console.log(chalk33.cyan.bold(`
17790
+ console.log(chalk34.cyan.bold(`
17791
17791
  \u{1F6F0}\uFE0F Node9 tail`));
17792
17792
  if (canApprove) {
17793
- console.log(chalk33.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
17794
- console.log(chalk33.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
17793
+ console.log(chalk34.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
17794
+ console.log(chalk34.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
17795
17795
  }
17796
17796
  const ctxStat = readSessionUsage();
17797
17797
  if (ctxStat) console.log(" " + formatContextStat(ctxStat));
17798
17798
  if (options.history) {
17799
- console.log(chalk33.dim("Showing history + live events.\n"));
17799
+ console.log(chalk34.dim("Showing history + live events.\n"));
17800
17800
  } else {
17801
- console.log(chalk33.dim("Showing live events only. Use --history to include past.\n"));
17801
+ console.log(chalk34.dim("Showing live events only. Use --history to include past.\n"));
17802
17802
  }
17803
17803
  process.on("SIGINT", () => {
17804
17804
  exitIdleMode();
@@ -17808,7 +17808,7 @@ async function startTail(options = {}) {
17808
17808
  readline6.clearLine(process.stdout, 0);
17809
17809
  readline6.cursorTo(process.stdout, 0);
17810
17810
  }
17811
- console.log(chalk33.dim("\n\u{1F6F0}\uFE0F Disconnected."));
17811
+ console.log(chalk34.dim("\n\u{1F6F0}\uFE0F Disconnected."));
17812
17812
  process.exit(0);
17813
17813
  });
17814
17814
  const STALL_THRESHOLD_MS = 6e4;
@@ -17816,11 +17816,11 @@ async function startTail(options = {}) {
17816
17816
  if (stallWarned) return;
17817
17817
  if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
17818
17818
  try {
17819
- const auditMtime = fs56.statSync(auditLog).mtimeMs;
17819
+ const auditMtime = fs60.statSync(auditLog).mtimeMs;
17820
17820
  if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
17821
17821
  console.log("");
17822
17822
  console.log(
17823
- chalk33.yellow(
17823
+ chalk34.yellow(
17824
17824
  "\u26A0\uFE0F Tail appears stalled \u2014 hooks are firing but no events are arriving. Try: node9 daemon restart"
17825
17825
  )
17826
17826
  );
@@ -17837,7 +17837,7 @@ async function startTail(options = {}) {
17837
17837
  },
17838
17838
  (res) => {
17839
17839
  if (res.statusCode !== 200) {
17840
- console.error(chalk33.red(`Failed to connect: HTTP ${res.statusCode}`));
17840
+ console.error(chalk34.red(`Failed to connect: HTTP ${res.statusCode}`));
17841
17841
  process.exit(1);
17842
17842
  }
17843
17843
  if (canApprove) enterIdleMode();
@@ -17868,7 +17868,7 @@ async function startTail(options = {}) {
17868
17868
  readline6.clearLine(process.stdout, 0);
17869
17869
  readline6.cursorTo(process.stdout, 0);
17870
17870
  }
17871
- console.log(chalk33.red("\n\u274C Daemon disconnected."));
17871
+ console.log(chalk34.red("\n\u274C Daemon disconnected."));
17872
17872
  process.exit(1);
17873
17873
  });
17874
17874
  }
@@ -17881,7 +17881,7 @@ async function startTail(options = {}) {
17881
17881
  const parsed = JSON.parse(rawData);
17882
17882
  const msg = parsed.message ?? "Flight recorder is down \u2014 run: node9 daemon restart";
17883
17883
  console.log("");
17884
- console.log(chalk33.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
17884
+ console.log(chalk34.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
17885
17885
  } catch {
17886
17886
  }
17887
17887
  return;
@@ -17966,9 +17966,9 @@ async function startTail(options = {}) {
17966
17966
  const rawSummary = data.argsSummary ?? data.tool;
17967
17967
  const summary = shortenPathSummary(rawSummary);
17968
17968
  const fileCount = data.fileCount ?? 0;
17969
- const files = fileCount > 0 ? chalk33.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
17969
+ const files = fileCount > 0 ? chalk34.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
17970
17970
  process.stdout.write(
17971
- `${chalk33.dim(time)} ${chalk33.cyan("\u{1F4F8} snapshot")} ${chalk33.dim(hash)} ${summary}${files}
17971
+ `${chalk34.dim(time)} ${chalk34.cyan("\u{1F4F8} snapshot")} ${chalk34.dim(hash)} ${summary}${files}
17972
17972
  `
17973
17973
  );
17974
17974
  return;
@@ -17985,18 +17985,18 @@ async function startTail(options = {}) {
17985
17985
  if (event === "execution-result") {
17986
17986
  const exec = data;
17987
17987
  const time = new Date(Date.now()).toLocaleTimeString([], { hour12: false });
17988
- const arrow = exec.isError ? chalk33.red(" \u21B3 \u2717") : chalk33.green(" \u21B3 \u2713");
17988
+ const arrow = exec.isError ? chalk34.red(" \u21B3 \u2717") : chalk34.green(" \u21B3 \u2713");
17989
17989
  const label2 = agentLabel(exec.agent, exec.mcpServer);
17990
17990
  const tool = (exec.tool ?? "").slice(0, 16);
17991
- const duration = typeof exec.durationMs === "number" ? chalk33.dim(` (${exec.durationMs}ms)`) : "";
17991
+ const duration = typeof exec.durationMs === "number" ? chalk34.dim(` (${exec.durationMs}ms)`) : "";
17992
17992
  console.log(
17993
- `${chalk33.gray(time)} ${arrow} ${label2}${chalk33.dim(tool)}${chalk33.dim(" completed")}${duration}`
17993
+ `${chalk34.gray(time)} ${arrow} ${label2}${chalk34.dim(tool)}${chalk34.dim(" completed")}${duration}`
17994
17994
  );
17995
17995
  }
17996
17996
  }
17997
17997
  req.on("error", (err2) => {
17998
17998
  const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
17999
- console.error(chalk33.red(`
17999
+ console.error(chalk34.red(`
18000
18000
  \u274C ${msg}`));
18001
18001
  process.exit(1);
18002
18002
  });
@@ -18007,7 +18007,7 @@ var init_tail = __esm({
18007
18007
  "use strict";
18008
18008
  init_daemon2();
18009
18009
  init_daemon();
18010
- PID_FILE = path55.join(os51.homedir(), ".node9", "daemon.pid");
18010
+ PID_FILE = path58.join(os52.homedir(), ".node9", "daemon.pid");
18011
18011
  ICONS = {
18012
18012
  bash: "\u{1F4BB}",
18013
18013
  shell: "\u{1F4BB}",
@@ -18055,9 +18055,9 @@ __export(hud_exports, {
18055
18055
  main: () => main,
18056
18056
  renderEnvironmentLine: () => renderEnvironmentLine
18057
18057
  });
18058
- import fs57 from "fs";
18059
- import path56 from "path";
18060
- import os52 from "os";
18058
+ import fs61 from "fs";
18059
+ import path59 from "path";
18060
+ import os53 from "os";
18061
18061
  import http4 from "http";
18062
18062
  async function readStdin() {
18063
18063
  const chunks = [];
@@ -18133,9 +18133,9 @@ function formatTimeLeft(resetsAt) {
18133
18133
  return ` (${m}m left)`;
18134
18134
  }
18135
18135
  function safeReadJson(filePath) {
18136
- if (!fs57.existsSync(filePath)) return null;
18136
+ if (!fs61.existsSync(filePath)) return null;
18137
18137
  try {
18138
- return JSON.parse(fs57.readFileSync(filePath, "utf-8"));
18138
+ return JSON.parse(fs61.readFileSync(filePath, "utf-8"));
18139
18139
  } catch {
18140
18140
  return null;
18141
18141
  }
@@ -18156,12 +18156,12 @@ function countHooksInFile(filePath) {
18156
18156
  return Object.keys(cfg.hooks).length;
18157
18157
  }
18158
18158
  function countRulesInDir(rulesDir) {
18159
- if (!fs57.existsSync(rulesDir)) return 0;
18159
+ if (!fs61.existsSync(rulesDir)) return 0;
18160
18160
  let count = 0;
18161
18161
  try {
18162
- for (const entry of fs57.readdirSync(rulesDir, { withFileTypes: true })) {
18162
+ for (const entry of fs61.readdirSync(rulesDir, { withFileTypes: true })) {
18163
18163
  if (entry.isDirectory()) {
18164
- count += countRulesInDir(path56.join(rulesDir, entry.name));
18164
+ count += countRulesInDir(path59.join(rulesDir, entry.name));
18165
18165
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
18166
18166
  count++;
18167
18167
  }
@@ -18172,46 +18172,46 @@ function countRulesInDir(rulesDir) {
18172
18172
  }
18173
18173
  function isSamePath(a, b) {
18174
18174
  try {
18175
- return path56.resolve(a) === path56.resolve(b);
18175
+ return path59.resolve(a) === path59.resolve(b);
18176
18176
  } catch {
18177
18177
  return false;
18178
18178
  }
18179
18179
  }
18180
18180
  function countConfigs(cwd) {
18181
- const homeDir2 = os52.homedir();
18182
- const claudeDir = path56.join(homeDir2, ".claude");
18181
+ const homeDir2 = os53.homedir();
18182
+ const claudeDir = path59.join(homeDir2, ".claude");
18183
18183
  let claudeMdCount = 0;
18184
18184
  let rulesCount = 0;
18185
18185
  let hooksCount = 0;
18186
18186
  const userMcpServers = /* @__PURE__ */ new Set();
18187
18187
  const projectMcpServers = /* @__PURE__ */ new Set();
18188
- if (fs57.existsSync(path56.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
18189
- rulesCount += countRulesInDir(path56.join(claudeDir, "rules"));
18190
- const userSettings = path56.join(claudeDir, "settings.json");
18188
+ if (fs61.existsSync(path59.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
18189
+ rulesCount += countRulesInDir(path59.join(claudeDir, "rules"));
18190
+ const userSettings = path59.join(claudeDir, "settings.json");
18191
18191
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
18192
18192
  hooksCount += countHooksInFile(userSettings);
18193
- const userClaudeJson = path56.join(homeDir2, ".claude.json");
18193
+ const userClaudeJson = path59.join(homeDir2, ".claude.json");
18194
18194
  for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
18195
18195
  for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
18196
18196
  userMcpServers.delete(name);
18197
18197
  }
18198
18198
  if (cwd) {
18199
- if (fs57.existsSync(path56.join(cwd, "CLAUDE.md"))) claudeMdCount++;
18200
- if (fs57.existsSync(path56.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
18201
- const projectClaudeDir = path56.join(cwd, ".claude");
18199
+ if (fs61.existsSync(path59.join(cwd, "CLAUDE.md"))) claudeMdCount++;
18200
+ if (fs61.existsSync(path59.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
18201
+ const projectClaudeDir = path59.join(cwd, ".claude");
18202
18202
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
18203
18203
  if (!overlapsUserScope) {
18204
- if (fs57.existsSync(path56.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
18205
- rulesCount += countRulesInDir(path56.join(projectClaudeDir, "rules"));
18206
- const projSettings = path56.join(projectClaudeDir, "settings.json");
18204
+ if (fs61.existsSync(path59.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
18205
+ rulesCount += countRulesInDir(path59.join(projectClaudeDir, "rules"));
18206
+ const projSettings = path59.join(projectClaudeDir, "settings.json");
18207
18207
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
18208
18208
  hooksCount += countHooksInFile(projSettings);
18209
18209
  }
18210
- if (fs57.existsSync(path56.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
18211
- const localSettings = path56.join(projectClaudeDir, "settings.local.json");
18210
+ if (fs61.existsSync(path59.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
18211
+ const localSettings = path59.join(projectClaudeDir, "settings.local.json");
18212
18212
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
18213
18213
  hooksCount += countHooksInFile(localSettings);
18214
- const mcpJsonServers = getMcpServerNames(path56.join(cwd, ".mcp.json"));
18214
+ const mcpJsonServers = getMcpServerNames(path59.join(cwd, ".mcp.json"));
18215
18215
  const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
18216
18216
  for (const name of disabledMcpJson) mcpJsonServers.delete(name);
18217
18217
  for (const name of mcpJsonServers) projectMcpServers.add(name);
@@ -18244,12 +18244,12 @@ function readActiveShieldsHud() {
18244
18244
  return shieldsCache.value;
18245
18245
  }
18246
18246
  try {
18247
- const shieldsPath = path56.join(os52.homedir(), ".node9", "shields.json");
18248
- if (!fs57.existsSync(shieldsPath)) {
18247
+ const shieldsPath = path59.join(os53.homedir(), ".node9", "shields.json");
18248
+ if (!fs61.existsSync(shieldsPath)) {
18249
18249
  shieldsCache = { value: [], ts: now };
18250
18250
  return [];
18251
18251
  }
18252
- const parsed = JSON.parse(fs57.readFileSync(shieldsPath, "utf-8"));
18252
+ const parsed = JSON.parse(fs61.readFileSync(shieldsPath, "utf-8"));
18253
18253
  if (!Array.isArray(parsed.active)) {
18254
18254
  shieldsCache = { value: [], ts: now };
18255
18255
  return [];
@@ -18351,17 +18351,17 @@ function renderContextLine(stdin) {
18351
18351
  async function main() {
18352
18352
  try {
18353
18353
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
18354
- if (fs57.existsSync(path56.join(os52.homedir(), ".node9", "hud-debug"))) {
18354
+ if (fs61.existsSync(path59.join(os53.homedir(), ".node9", "hud-debug"))) {
18355
18355
  try {
18356
- const logPath = path56.join(os52.homedir(), ".node9", "hud-debug.log");
18356
+ const logPath = path59.join(os53.homedir(), ".node9", "hud-debug.log");
18357
18357
  const MAX_LOG_SIZE = 10 * 1024 * 1024;
18358
18358
  let size = 0;
18359
18359
  try {
18360
- size = fs57.statSync(logPath).size;
18360
+ size = fs61.statSync(logPath).size;
18361
18361
  } catch {
18362
18362
  }
18363
18363
  if (size < MAX_LOG_SIZE) {
18364
- fs57.appendFileSync(
18364
+ fs61.appendFileSync(
18365
18365
  logPath,
18366
18366
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
18367
18367
  );
@@ -18382,11 +18382,11 @@ async function main() {
18382
18382
  try {
18383
18383
  const cwd = stdin.cwd ?? process.cwd();
18384
18384
  for (const configPath2 of [
18385
- path56.join(cwd, "node9.config.json"),
18386
- path56.join(os52.homedir(), ".node9", "config.json")
18385
+ path59.join(cwd, "node9.config.json"),
18386
+ path59.join(os53.homedir(), ".node9", "config.json")
18387
18387
  ]) {
18388
- if (!fs57.existsSync(configPath2)) continue;
18389
- const cfg = JSON.parse(fs57.readFileSync(configPath2, "utf-8"));
18388
+ if (!fs61.existsSync(configPath2)) continue;
18389
+ const cfg = JSON.parse(fs61.readFileSync(configPath2, "utf-8"));
18390
18390
  const hud = cfg.settings?.hud;
18391
18391
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
18392
18392
  }
@@ -18432,10 +18432,10 @@ init_core();
18432
18432
  init_setup();
18433
18433
  init_daemon2();
18434
18434
  import { Command } from "commander";
18435
- import chalk34 from "chalk";
18436
- import fs58 from "fs";
18437
- import path57 from "path";
18438
- import os53 from "os";
18435
+ import chalk35 from "chalk";
18436
+ import fs62 from "fs";
18437
+ import path60 from "path";
18438
+ import os54 from "os";
18439
18439
  import { spawn as spawn9 } from "child_process";
18440
18440
  import { confirm as confirm2 } from "@inquirer/prompts";
18441
18441
 
@@ -24185,6 +24185,132 @@ function checkSecrets(ctx) {
24185
24185
 
24186
24186
  // src/posture/egress.ts
24187
24187
  init_config();
24188
+ import fs47 from "fs";
24189
+
24190
+ // src/sandbox/templates.ts
24191
+ var AGENT_NPM_PACKAGE = {
24192
+ claude: "@anthropic-ai/claude-code",
24193
+ codex: "@openai/codex"
24194
+ };
24195
+ function pinnedNode9Version(hostVersion) {
24196
+ return hostVersion && /^\d+\.\d+\.\d+$/.test(hostVersion) ? hostVersion : "latest";
24197
+ }
24198
+ var AGENT_BIN = {
24199
+ claude: "claude",
24200
+ codex: "codex"
24201
+ };
24202
+ var RUN_AS_USER = "agent";
24203
+ var ALLOWED_DOMAINS_PATH = "/etc/node9-sandbox/allowed-domains.txt";
24204
+ function renderDockerfile(config, node9Version2) {
24205
+ const agentPkg = AGENT_NPM_PACKAGE[config.agent];
24206
+ return `# Auto-generated by node9 sandbox. Do not edit by hand.
24207
+ FROM node:22-bookworm
24208
+
24209
+ ENV DEBIAN_FRONTEND=noninteractive
24210
+
24211
+ # Wall + base tooling (iptables/ipset/dig/gosu) \u2014 ported from Isag.
24212
+ RUN apt-get update && apt-get install -y --no-install-recommends \\
24213
+ ca-certificates curl git gosu iproute2 ipset iptables dnsutils jq \\
24214
+ && rm -rf /var/lib/apt/lists/*
24215
+
24216
+ # The worker (agent CLI).
24217
+ RUN npm install -g ${agentPkg}
24218
+
24219
+ # The guard (node9), pinned to the host version.
24220
+ RUN npm install -g node9-ai@${node9Version2}
24221
+
24222
+ # Non-root runtime user at uid 1000 (matches the typical single-user host so the
24223
+ # mounted ~/.claude / ~/.codex / project are read/writable). The node base image
24224
+ # already claims uid 1000 for the 'node' user \u2014 free it first (cf. Isag/ubuntu).
24225
+ RUN userdel -r node 2>/dev/null || true; \\
24226
+ userdel -r ubuntu 2>/dev/null || true; \\
24227
+ useradd --create-home --uid 1000 --shell /bin/bash ${RUN_AS_USER}
24228
+
24229
+ # Wire the agent's node9 hooks into the runtime user's home (build-time, static).
24230
+ RUN gosu ${RUN_AS_USER} node9 agents add ${config.agent} || true
24231
+
24232
+ RUN mkdir -p /workspace /etc/node9-sandbox \\
24233
+ && chown ${RUN_AS_USER}:${RUN_AS_USER} /workspace
24234
+
24235
+ COPY entrypoint.sh /usr/local/bin/entrypoint.sh
24236
+ RUN chmod +x /usr/local/bin/entrypoint.sh
24237
+ ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
24238
+ `;
24239
+ }
24240
+ function renderEntrypoint(config) {
24241
+ const agentBin = AGENT_BIN[config.agent];
24242
+ return `#!/usr/bin/env bash
24243
+ # Auto-generated by node9 sandbox. Seals the egress wall (root), then drops to the
24244
+ # non-root agent which starts the node9 daemon + execs the agent.
24245
+ set -Eeuo pipefail
24246
+
24247
+ DOMAINS_FILE="${ALLOWED_DOMAINS_PATH}"
24248
+ RUN_AS_USER="${RUN_AS_USER}"
24249
+
24250
+ [[ -s "$DOMAINS_FILE" ]] || { echo "entrypoint: missing/empty $DOMAINS_FILE" >&2; exit 1; }
24251
+
24252
+ # Own the mounted node9 data dir so the agent user can write audit there.
24253
+ mkdir -p "/home/$RUN_AS_USER/.node9"
24254
+ chown -R "$RUN_AS_USER:$RUN_AS_USER" "/home/$RUN_AS_USER/.node9" || true
24255
+
24256
+ # \u2500\u2500 Resolve the allowlist \u2192 ipset (union of every resolver, like Isag) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
24257
+ mapfile -t LOCAL_DNS < <(awk '/^nameserver / {print $2}' /etc/resolv.conf)
24258
+ [[ \${#LOCAL_DNS[@]} -gt 0 ]] || { echo "entrypoint: no resolvers in /etc/resolv.conf" >&2; exit 1; }
24259
+
24260
+ ipset create node9_allowed hash:ip family inet -exist
24261
+ ipset flush node9_allowed
24262
+
24263
+ while IFS= read -r domain; do
24264
+ [[ -n "$domain" ]] || continue
24265
+ found=0
24266
+ # union the local resolver + each upstream so CDN/anycast IP rotation is covered
24267
+ for ip in $(getent ahostsv4 "$domain" 2>/dev/null | awk '{print $1}' | sort -u); do
24268
+ ipset add node9_allowed "$ip" -exist; found=1
24269
+ done
24270
+ for r in "\${LOCAL_DNS[@]}"; do
24271
+ for ip in $(dig +short +time=2 +tries=1 @"$r" A "$domain" 2>/dev/null | awk '/^[0-9.]+$/'); do
24272
+ ipset add node9_allowed "$ip" -exist; found=1
24273
+ done
24274
+ done
24275
+ [[ $found -eq 1 ]] || { echo "entrypoint: failed to resolve $domain" >&2; exit 1; }
24276
+ echo "entrypoint: allowed $domain"
24277
+ done < "$DOMAINS_FILE"
24278
+
24279
+ # \u2500\u2500 Seal iptables: deny-by-default except lo, established, DNS, the allowlist \u2500\u2500\u2500\u2500
24280
+ echo "entrypoint: sealing firewall..."
24281
+ iptables -F; iptables -X
24282
+ iptables -P INPUT DROP
24283
+ iptables -P FORWARD DROP
24284
+ iptables -P OUTPUT DROP
24285
+ iptables -A INPUT -i lo -j ACCEPT
24286
+ iptables -A OUTPUT -o lo -j ACCEPT
24287
+ iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
24288
+ iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
24289
+ for r in "\${LOCAL_DNS[@]}"; do
24290
+ iptables -A OUTPUT -p udp -d "$r" --dport 53 -j ACCEPT
24291
+ iptables -A OUTPUT -p tcp -d "$r" --dport 53 -j ACCEPT
24292
+ done
24293
+ iptables -A OUTPUT -m set --match-set node9_allowed dst -j ACCEPT
24294
+
24295
+ # \u2500\u2500 Drop to the agent: start the node9 daemon (as the user), then exec the agent \u2500
24296
+ echo "entrypoint: starting node9 + ${agentBin} as $RUN_AS_USER"
24297
+ exec gosu "$RUN_AS_USER" bash -lc '
24298
+ set -e
24299
+ node9 daemon --background >/dev/null 2>&1 || true
24300
+ cd /workspace
24301
+ exec ${agentBin} "$@"
24302
+ ' -- "$@"
24303
+ `;
24304
+ }
24305
+
24306
+ // src/posture/egress.ts
24307
+ function sandboxEgressWallActive() {
24308
+ try {
24309
+ return fs47.existsSync(ALLOWED_DOMAINS_PATH);
24310
+ } catch {
24311
+ return false;
24312
+ }
24313
+ }
24188
24314
  function evaluateEgressConfig(egress) {
24189
24315
  if (egress.enabled && egress.mode === "block") {
24190
24316
  return {
@@ -24234,6 +24360,21 @@ function evaluateEgressConfig(egress) {
24234
24360
  };
24235
24361
  }
24236
24362
  function checkEgress(ctx) {
24363
+ if (sandboxEgressWallActive()) {
24364
+ return [
24365
+ {
24366
+ category: "Egress",
24367
+ severity: "advisory",
24368
+ title: "Egress is hard-blocked by the sandbox kernel wall",
24369
+ what: "Outbound is deny-by-default at the kernel; only the allowlist is reachable.",
24370
+ why: "The sandbox seals egress with an ipset/iptables wall before the agent starts.",
24371
+ who: "Even a compromised agent can only reach the allowlisted hosts.",
24372
+ owner: "node9",
24373
+ detail: [],
24374
+ coverage: { state: "covered", level: "block", via: "sandbox egress wall" }
24375
+ }
24376
+ ];
24377
+ }
24237
24378
  const config = getConfig(ctx.cwd);
24238
24379
  const egress = config.policy.egress;
24239
24380
  return [evaluateEgressConfig({ enabled: egress.enabled, mode: egress.mode })];
@@ -24275,7 +24416,7 @@ async function checkGate(ctx) {
24275
24416
 
24276
24417
  // src/posture/supply-chain.ts
24277
24418
  init_provenance();
24278
- import fs47 from "fs";
24419
+ import fs48 from "fs";
24279
24420
  import os42 from "os";
24280
24421
  import path48 from "path";
24281
24422
  import { parse as parseToml3 } from "smol-toml";
@@ -24291,9 +24432,9 @@ function isNode9Managed(command, args = []) {
24291
24432
  var MAX_CONFIG_BYTES = 10 * 1024 * 1024;
24292
24433
  function readServers(file, format, agent) {
24293
24434
  try {
24294
- const stat = fs47.statSync(file);
24435
+ const stat = fs48.statSync(file);
24295
24436
  if (!stat.isFile() || stat.size > MAX_CONFIG_BYTES) return [];
24296
- const text = fs47.readFileSync(file, "utf8");
24437
+ const text = fs48.readFileSync(file, "utf8");
24297
24438
  const map = format === "toml" ? parseToml3(text)?.mcp_servers : JSON.parse(text)?.mcpServers;
24298
24439
  if (!map || typeof map !== "object") return [];
24299
24440
  return Object.entries(map).map(([name, v]) => ({
@@ -24389,11 +24530,12 @@ async function checkPrivilege(ctx) {
24389
24530
  }
24390
24531
 
24391
24532
  // src/posture/containment.ts
24392
- import fs48 from "fs";
24533
+ import fs49 from "fs";
24534
+ var ISOLATION_WEIGHT = 12;
24393
24535
  function inContainer() {
24394
- if (fs48.existsSync("/.dockerenv") || fs48.existsSync("/run/.containerenv")) return true;
24536
+ if (fs49.existsSync("/.dockerenv") || fs49.existsSync("/run/.containerenv")) return true;
24395
24537
  try {
24396
- const cgroup = fs48.readFileSync("/proc/1/cgroup", "utf8");
24538
+ const cgroup = fs49.readFileSync("/proc/1/cgroup", "utf8");
24397
24539
  if (/docker|kubepods|containerd|lxc|libpod/.test(cgroup)) return true;
24398
24540
  } catch {
24399
24541
  }
@@ -24412,14 +24554,28 @@ function checkContainment(_ctx) {
24412
24554
  detail: [],
24413
24555
  owner: "os",
24414
24556
  node9Reduces: true,
24415
- fix: "node9 can shrink the blast radius without a container \u2014 you keep every tool:\n \u2022 node9 shield enable project-jail \u2014 block credential reads\n \u2022 node9 egress lock \u2014 block data exfil\nA container/VM adds full isolation, but you lose host access.",
24416
- coverageProbe: { kind: "cantFix" }
24557
+ // The single biggest hardening gap, and node9 now fully remedies it
24558
+ // (`node9 sandbox run`). Deducts while open; closing it is the headline
24559
+ // payoff. No coverageProbe → stays OPEN (scored) until adopted; live
24560
+ // partial-credit for the lighter shield path is a fast-follow.
24561
+ scoreWeight: ISOLATION_WEIGHT,
24562
+ gain: "jailed container \xB7 kernel egress wall \xB7 scoped mounts \xB7 governed inside",
24563
+ cost: "the agent works inside /workspace, not your live host",
24564
+ fix: `Two ways to shrink the blast radius \u2014 pick by how much flexibility you need:
24565
+ Strongest \u2014 jail it (closes this gap, +${ISOLATION_WEIGHT}):
24566
+ \u2022 node9 sandbox run <agent>
24567
+ Lighter \u2014 harden in place, keep full host access (about +${Math.round(
24568
+ ISOLATION_WEIGHT / 2
24569
+ )}):
24570
+ \u2022 node9 shield enable project-jail \u2014 block stray credential reads
24571
+ \u2022 node9 egress lock \u2014 block data exfil`
24417
24572
  }
24418
24573
  ];
24419
24574
  }
24420
24575
 
24421
24576
  // src/posture/inbound.ts
24422
- import fs49 from "fs";
24577
+ import fs50 from "fs";
24578
+ var DB_EXPOSURE_WEIGHT = 4;
24423
24579
  var KNOWN_SERVICE_PORTS = {
24424
24580
  5432: "PostgreSQL",
24425
24581
  6379: "Redis",
@@ -24505,7 +24661,7 @@ function collectListeners() {
24505
24661
  const byPort = /* @__PURE__ */ new Map();
24506
24662
  for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) {
24507
24663
  try {
24508
- for (const l of parseListeners(fs49.readFileSync(file, "utf8"))) {
24664
+ for (const l of parseListeners(fs50.readFileSync(file, "utf8"))) {
24509
24665
  if (!byPort.has(l.port)) byPort.set(l.port, l);
24510
24666
  }
24511
24667
  } catch {
@@ -24517,11 +24673,11 @@ function readProc(pid) {
24517
24673
  let comm = "unknown";
24518
24674
  let cmdline = "";
24519
24675
  try {
24520
- comm = fs49.readFileSync(`/proc/${pid}/comm`, "utf8").trim().replace(/-MainThread$/, "") || "unknown";
24676
+ comm = fs50.readFileSync(`/proc/${pid}/comm`, "utf8").trim().replace(/-MainThread$/, "") || "unknown";
24521
24677
  } catch {
24522
24678
  }
24523
24679
  try {
24524
- cmdline = fs49.readFileSync(`/proc/${pid}/cmdline`).toString().replace(/\0/g, " ").trim();
24680
+ cmdline = fs50.readFileSync(`/proc/${pid}/cmdline`).toString().replace(/\0/g, " ").trim();
24525
24681
  } catch {
24526
24682
  }
24527
24683
  return { comm, cmdline };
@@ -24531,21 +24687,21 @@ function resolveProcesses(inodes) {
24531
24687
  if (inodes.size === 0) return map;
24532
24688
  let pids;
24533
24689
  try {
24534
- pids = fs49.readdirSync("/proc").filter((d) => /^\d+$/.test(d));
24690
+ pids = fs50.readdirSync("/proc").filter((d) => /^\d+$/.test(d));
24535
24691
  } catch {
24536
24692
  return map;
24537
24693
  }
24538
24694
  for (const pid of pids) {
24539
24695
  let fds;
24540
24696
  try {
24541
- fds = fs49.readdirSync(`/proc/${pid}/fd`);
24697
+ fds = fs50.readdirSync(`/proc/${pid}/fd`);
24542
24698
  } catch {
24543
24699
  continue;
24544
24700
  }
24545
24701
  for (const fd of fds) {
24546
24702
  let link;
24547
24703
  try {
24548
- link = fs49.readlinkSync(`/proc/${pid}/fd/${fd}`);
24704
+ link = fs50.readlinkSync(`/proc/${pid}/fd/${fd}`);
24549
24705
  } catch {
24550
24706
  continue;
24551
24707
  }
@@ -24600,8 +24756,15 @@ function checkInbound(ctx) {
24600
24756
  // Only when node9 actually has a shield for an exposed service — otherwise
24601
24757
  // (bare dev servers) it stays purely the user's to rebind.
24602
24758
  node9Reduces: reduces,
24603
- fix,
24604
- coverageProbe: { kind: "cantFix" }
24759
+ // When a db-shield applies this is real, node9-addressable hardening → it
24760
+ // scores (and stays OPEN, no cantFix probe). Bare dev servers node9 can't
24761
+ // touch stay can't-fix / your-part / unscored.
24762
+ ...reduces ? {
24763
+ scoreWeight: DB_EXPOSURE_WEIGHT,
24764
+ gain: "blocks DROP TABLE / TRUNCATE / FLUSHALL on the exposed DB",
24765
+ cost: "you confirm legit destructive migrations"
24766
+ } : { coverageProbe: { kind: "cantFix" } },
24767
+ fix
24605
24768
  });
24606
24769
  }
24607
24770
  return findings;
@@ -24651,16 +24814,20 @@ function scorePosture(findings, checksRun) {
24651
24814
  const open = findings.filter(
24652
24815
  (f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix"
24653
24816
  );
24654
- const count = (sev) => open.filter((f) => f.severity === sev).length;
24655
- return computeSecurityScore({
24817
+ const count = (sev) => open.filter((f) => f.severity === sev && !f.scoreWeight).length;
24818
+ const base = computeSecurityScore({
24656
24819
  critical: count("critical"),
24657
24820
  high: count("high"),
24658
24821
  medium: count("medium"),
24659
- // Denominator = number of checks evaluated. With computeSecurityScore's
24660
- // caps this makes any critical → critical tier, any high → at-risk, and a
24661
- // fully clean run (0 findings, checksRun > 0) → 100/good.
24662
24822
  total: Math.max(checksRun, 1)
24663
24823
  });
24824
+ const headroom = open.reduce((sum, f) => sum + (f.scoreWeight ?? 0), 0);
24825
+ const score = Math.max(0, base.score - headroom);
24826
+ const tier = score >= 80 ? "good" : score >= 50 ? "at-risk" : "critical";
24827
+ return { score, tier };
24828
+ }
24829
+ function openHeadroom(findings) {
24830
+ return findings.filter((f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix").reduce((sum, f) => sum + (f.scoreWeight ?? 0), 0);
24664
24831
  }
24665
24832
 
24666
24833
  // src/posture/headline.ts
@@ -24870,9 +25037,10 @@ var LABEL_WIDTH = 14;
24870
25037
  function label(category) {
24871
25038
  return chalk24.bold(category.padEnd(Math.max(LABEL_WIDTH, category.length + 1)));
24872
25039
  }
24873
- function renderFinding(f) {
25040
+ function renderFinding(f, showWeight = false) {
24874
25041
  const lines = [];
24875
- lines.push(` ${ICON[f.severity]} ${label(f.category)}${f.title}`);
25042
+ const wt = showWeight && f.scoreWeight ? chalk24.cyan.bold(`+${f.scoreWeight} `) : "";
25043
+ lines.push(` ${ICON[f.severity]} ${label(f.category)}${wt}${f.title}`);
24876
25044
  const indent = " ".repeat(2 + 3 + LABEL_WIDTH);
24877
25045
  const width = 80 - indent.length;
24878
25046
  for (const s of [f.what, f.why, f.who]) {
@@ -24888,6 +25056,16 @@ function renderFinding(f) {
24888
25056
  }
24889
25057
  }
24890
25058
  }
25059
+ const tradeoff = [
25060
+ [f.gain, "gain: ", chalk24.green],
25061
+ [f.cost, "cost: ", chalk24.yellow]
25062
+ ];
25063
+ for (const [text, lbl, color2] of tradeoff) {
25064
+ if (!text) continue;
25065
+ wrap(text, width - 6).forEach((l, i) => {
25066
+ lines.push(indent + (i === 0 ? color2(lbl) : " ") + chalk24.gray(l));
25067
+ });
25068
+ }
24891
25069
  return lines;
24892
25070
  }
24893
25071
  function renderPosture(result) {
@@ -24897,15 +25075,11 @@ function renderPosture(result) {
24897
25075
  lines.push(
24898
25076
  chalk24.cyan.bold(`\u{1F6E1}\uFE0F Node9 Posture`) + chalk24.gray(` \u2014 ${result.agent}`) + ` ${chalk24.bold(`Score: ${result.score}/100`)} (${tier})`
24899
25077
  );
24900
- const advisories = result.findings.filter(
24901
- (f) => f.severity === "advisory" && f.coverage?.state !== "covered"
24902
- ).length;
24903
- if (advisories > 0) {
24904
- const word = advisories === 1 ? "advisory" : "advisories";
24905
- const verb = advisories === 1 ? "doesn't" : "don't";
25078
+ const headroom = openHeadroom(result.findings);
25079
+ if (headroom > 0) {
24906
25080
  lines.push(
24907
25081
  " " + chalk24.gray(
24908
- `${advisories} ${word} below ${verb} affect the score \u2014 OS-level exposure node9 can't enforce, yours to weigh.`
25082
+ `${headroom} pts of headroom below \u2014 each layer adds security at some cost to flexibility. You choose which to close.`
24909
25083
  )
24910
25084
  );
24911
25085
  }
@@ -24921,7 +25095,7 @@ function renderPosture(result) {
24921
25095
  const covered = result.findings.filter((f) => f.coverage?.state === "covered");
24922
25096
  const open = result.findings.filter((f) => f.coverage?.state !== "covered");
24923
25097
  if (covered.length > 0) {
24924
- lines.push(" " + chalk24.green("\u{1F7E2} node9 is already protecting you"));
25098
+ lines.push(" " + chalk24.green("\u{1F7E2} ON NOW \u2014 node9 is enforcing these (your floor)"));
24925
25099
  for (const f of covered) {
24926
25100
  const gated = f.coverage?.level === "review" ? "approval-gating" : "blocking";
24927
25101
  const via = f.coverage?.via ?? "node9";
@@ -24936,18 +25110,16 @@ function renderPosture(result) {
24936
25110
  const osOpen = open.filter((f) => f.owner !== "node9" && !f.node9Reduces);
24937
25111
  if (node9Open.length > 0) {
24938
25112
  lines.push(" " + chalk24.cyan.bold("\u{1F527} node9 can fix these \u2014 run the command"));
24939
- for (const f of node9Open) lines.push(...renderFinding(f));
25113
+ for (const f of node9Open) lines.push(...renderFinding(f, true));
24940
25114
  }
24941
25115
  if (reduceOpen.length > 0) {
24942
25116
  if (node9Open.length > 0) lines.push("");
24943
- lines.push(
24944
- " " + chalk24.yellow.bold("\u{1F512} node9 reduces these \u2014 run the command, the rest is yours")
24945
- );
24946
- for (const f of reduceOpen) lines.push(...renderFinding(f));
25117
+ lines.push(" " + chalk24.yellow.bold("\u{1F512} AVAILABLE \u2014 turn on to harden (each has a tradeoff)"));
25118
+ for (const f of reduceOpen) lines.push(...renderFinding(f, true));
24947
25119
  }
24948
25120
  if (osOpen.length > 0) {
24949
25121
  if (node9Open.length > 0 || reduceOpen.length > 0) lines.push("");
24950
- lines.push(" " + chalk24.bold("\u{1F9F1} Only you can fix these \u2014 node9 can't"));
25122
+ lines.push(" " + chalk24.bold("\u{1F9F1} YOUR PART \u2014 node9 can't fix these (OS-level)"));
24951
25123
  for (const f of osOpen) lines.push(...renderFinding(f));
24952
25124
  }
24953
25125
  for (const cat of result.passedCategories) {
@@ -25002,7 +25174,12 @@ function buildShipBody(result) {
25002
25174
  // The runnable fix / OS action — commands + advice, never a path.
25003
25175
  fix: f.fix,
25004
25176
  // Whose job it is. Default 'os' so the SaaS never falsely claims node9 can fix it.
25005
- owner: f.owner ?? "os"
25177
+ owner: f.owner ?? "os",
25178
+ // Hardening weight + the flexibility tradeoff (generic prose / a number —
25179
+ // no values or paths), so the fleet view can show the same headroom story.
25180
+ scoreWeight: f.scoreWeight,
25181
+ gain: f.gain,
25182
+ cost: f.cost
25006
25183
  }))
25007
25184
  };
25008
25185
  }
@@ -25075,7 +25252,7 @@ function registerPostureCommand(program2) {
25075
25252
  init_config();
25076
25253
  init_dist();
25077
25254
  import chalk26 from "chalk";
25078
- import fs50 from "fs";
25255
+ import fs51 from "fs";
25079
25256
  import os45 from "os";
25080
25257
  import path49 from "path";
25081
25258
  var DEFAULT_EGRESS = {
@@ -25091,7 +25268,7 @@ function configPath() {
25091
25268
  function readRawConfig() {
25092
25269
  let text;
25093
25270
  try {
25094
- text = fs50.readFileSync(configPath(), "utf8");
25271
+ text = fs51.readFileSync(configPath(), "utf8");
25095
25272
  } catch (err2) {
25096
25273
  if (err2.code === "ENOENT") return {};
25097
25274
  throw err2;
@@ -25106,8 +25283,8 @@ function readRawConfig() {
25106
25283
  }
25107
25284
  function writeRawConfig(config) {
25108
25285
  const p = configPath();
25109
- fs50.mkdirSync(path49.dirname(p), { recursive: true });
25110
- fs50.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
25286
+ fs51.mkdirSync(path49.dirname(p), { recursive: true });
25287
+ fs51.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
25111
25288
  }
25112
25289
  function applyEgress(config, change) {
25113
25290
  const policy = config.policy = config.policy ?? {};
@@ -25198,15 +25375,369 @@ function registerEgressCommand(program2) {
25198
25375
  egress.action(showStatus);
25199
25376
  }
25200
25377
 
25378
+ // src/cli/commands/sandbox.ts
25379
+ init_config();
25380
+ import chalk27 from "chalk";
25381
+ import fs54 from "fs";
25382
+ import path52 from "path";
25383
+ import { spawnSync as spawnSync6 } from "child_process";
25384
+
25385
+ // src/sandbox/config.ts
25386
+ import fs52 from "fs";
25387
+ import path50 from "path";
25388
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
25389
+ var SANDBOX_CONFIG_FILE = "node9.sandbox.yaml";
25390
+ var FORBIDDEN_ENV = /* @__PURE__ */ new Set(["NODE9_API_KEY", "NODE9_API_URL"]);
25391
+ function defaultSandboxConfig(agent) {
25392
+ return {
25393
+ agent,
25394
+ workspace: { mount: ".", target: "/workspace", mode: "rw" },
25395
+ runtime: { engine: "docker", image: "node9-sandbox:local", rebuild: "auto" },
25396
+ outbound: {
25397
+ mode: "block",
25398
+ allow: agent === "codex" ? ["api.openai.com", "api.github.com", "github.com", "registry.npmjs.org"] : ["api.anthropic.com", "api.github.com", "github.com", "registry.npmjs.org"]
25399
+ },
25400
+ inbound: { expose: [] },
25401
+ // Provider key only — NODE9_API_KEY intentionally absent (fix #1).
25402
+ env: { pass: [agent === "codex" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY"] },
25403
+ // Terminal-only approval in the MVP; cloud/native/browser off (fix #1).
25404
+ node9: {
25405
+ approvals: { terminal: true, native: false, browser: false, cloud: false },
25406
+ // Mount the agent's OAuth/creds dir so it can authenticate in the box.
25407
+ mountAgentCredentials: true
25408
+ }
25409
+ };
25410
+ }
25411
+ function asStringArray(v) {
25412
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
25413
+ }
25414
+ function mergeSandboxConfig(raw, fallbackAgent) {
25415
+ const r = raw && typeof raw === "object" ? raw : {};
25416
+ const agent = typeof r.agent === "string" ? r.agent : fallbackAgent;
25417
+ if (agent !== "claude" && agent !== "codex") {
25418
+ throw new Error(`sandbox: unsupported agent "${String(agent)}" (use claude or codex)`);
25419
+ }
25420
+ const d = defaultSandboxConfig(agent);
25421
+ const ws = r.workspace ?? {};
25422
+ const rt = r.runtime ?? {};
25423
+ const out = r.outbound ?? {};
25424
+ const inb = r.inbound ?? {};
25425
+ const env = r.env ?? {};
25426
+ const n9 = r.node9 ?? {};
25427
+ const appr = n9.approvals ?? {};
25428
+ const pass = asStringArray(env.pass).filter((k) => !FORBIDDEN_ENV.has(k));
25429
+ return {
25430
+ agent,
25431
+ workspace: {
25432
+ mount: typeof ws.mount === "string" ? ws.mount : d.workspace.mount,
25433
+ target: typeof ws.target === "string" ? ws.target : d.workspace.target,
25434
+ mode: ws.mode === "ro" ? "ro" : "rw"
25435
+ },
25436
+ runtime: {
25437
+ engine: rt.engine === "podman" ? "podman" : "docker",
25438
+ image: typeof rt.image === "string" ? rt.image : d.runtime.image,
25439
+ rebuild: rt.rebuild === "never" || rt.rebuild === "always" ? rt.rebuild : d.runtime.rebuild
25440
+ },
25441
+ outbound: { mode: "block", allow: out.allow ? asStringArray(out.allow) : d.outbound.allow },
25442
+ inbound: { expose: inb.expose ? asStringArray(inb.expose) : d.inbound.expose },
25443
+ env: { pass: env.pass ? pass : d.env.pass },
25444
+ node9: {
25445
+ approvals: {
25446
+ terminal: appr.terminal !== false,
25447
+ native: appr.native === true,
25448
+ browser: appr.browser === true,
25449
+ cloud: appr.cloud === true
25450
+ },
25451
+ mountAgentCredentials: n9.mountAgentCredentials !== false
25452
+ }
25453
+ };
25454
+ }
25455
+ function scaffoldSandboxYaml(agent) {
25456
+ const header = "# node9.sandbox.yaml \u2014 sandbox TOPOLOGY (what the agent may touch).\n# Security policy (shields / egress rules / approvers) lives in ~/.node9/config.json\n# and applies to both native and sandbox. NODE9_API_KEY is never passed into the box.\n\n";
25457
+ return header + stringifyYaml(defaultSandboxConfig(agent));
25458
+ }
25459
+ function sandboxConfigPath(cwd = process.cwd()) {
25460
+ return path50.join(cwd, SANDBOX_CONFIG_FILE);
25461
+ }
25462
+ function loadSandboxConfig(cwd = process.cwd(), fallbackAgent = "claude") {
25463
+ const p = sandboxConfigPath(cwd);
25464
+ if (!fs52.existsSync(p)) {
25465
+ throw new Error(`sandbox: ${SANDBOX_CONFIG_FILE} not found \u2014 run \`node9 sandbox new\` first.`);
25466
+ }
25467
+ let raw;
25468
+ try {
25469
+ raw = parseYaml(fs52.readFileSync(p, "utf-8"));
25470
+ } catch (err2) {
25471
+ throw new Error(
25472
+ `sandbox: ${SANDBOX_CONFIG_FILE} is not valid YAML \u2014 ${err2.message}`
25473
+ );
25474
+ }
25475
+ return mergeSandboxConfig(raw, fallbackAgent);
25476
+ }
25477
+
25478
+ // src/sandbox/firewall.ts
25479
+ var AGENT_PROVIDER_HOST = {
25480
+ claude: ["api.anthropic.com"],
25481
+ codex: ["api.openai.com"]
25482
+ };
25483
+ var NODE9_SAAS_HOSTS = ["api.node9.ai", "app.node9.ai", "node9.ai"];
25484
+ function isValidHost2(host) {
25485
+ if (typeof host !== "string") return false;
25486
+ const h = host.trim().toLowerCase();
25487
+ if (!h || h.length > 253) return false;
25488
+ if (/[\s/:@?#\\]/.test(h)) return false;
25489
+ return /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(
25490
+ h
25491
+ );
25492
+ }
25493
+ function compileAllowlist(input) {
25494
+ const norm = (h) => h.trim().toLowerCase();
25495
+ const denySet = /* @__PURE__ */ new Set([...input.configDeny.map(norm), ...NODE9_SAAS_HOSTS.map(norm)]);
25496
+ const candidates = [
25497
+ ...AGENT_PROVIDER_HOST[input.agent],
25498
+ ...input.sandboxAllow,
25499
+ ...input.configAllow
25500
+ ].map(norm);
25501
+ const allow = /* @__PURE__ */ new Set();
25502
+ const rejected = [];
25503
+ const denied = [];
25504
+ for (const host of candidates) {
25505
+ if (!host) continue;
25506
+ if (!isValidHost2(host)) {
25507
+ if (!rejected.includes(host)) rejected.push(host);
25508
+ continue;
25509
+ }
25510
+ if (denySet.has(host)) {
25511
+ if (!denied.includes(host)) denied.push(host);
25512
+ continue;
25513
+ }
25514
+ allow.add(host);
25515
+ }
25516
+ return {
25517
+ allow: [...allow].sort(),
25518
+ rejected: rejected.sort(),
25519
+ denied: denied.sort()
25520
+ };
25521
+ }
25522
+
25523
+ // src/sandbox/runtime.ts
25524
+ import fs53 from "fs";
25525
+ import os46 from "os";
25526
+ import path51 from "path";
25527
+ import crypto8 from "crypto";
25528
+ import { spawnSync as spawnSync5 } from "child_process";
25529
+ function sandboxDataDir(cwd = process.cwd()) {
25530
+ return path51.join(cwd, ".node9", "sandbox", "data");
25531
+ }
25532
+ function detectEngine(engine) {
25533
+ const r = spawnSync5(engine, ["--version"], { encoding: "utf-8" });
25534
+ if (r.status === 0 && typeof r.stdout === "string") {
25535
+ return { available: true, version: r.stdout.trim() };
25536
+ }
25537
+ return { available: false };
25538
+ }
25539
+ function agentCredentialsMount(agent) {
25540
+ const rel = agent === "codex" ? ".codex/auth.json" : ".claude/.credentials.json";
25541
+ return { hostPath: path51.join(os46.homedir(), rel), target: `/home/${RUN_AS_USER}/${rel}` };
25542
+ }
25543
+ function buildRunArgs(opts) {
25544
+ const { config, workspaceHostPath, dataHostPath, allowlistHostPath, agentArgs } = opts;
25545
+ const args = ["run", "--rm", "-it", "--cap-add=NET_ADMIN"];
25546
+ args.push("-v", `${workspaceHostPath}:${config.workspace.target}:${config.workspace.mode}`);
25547
+ args.push("-v", `${dataHostPath}:/home/${RUN_AS_USER}/.node9`);
25548
+ args.push("-v", `${allowlistHostPath}:${ALLOWED_DOMAINS_PATH}:ro`);
25549
+ if (config.node9.mountAgentCredentials) {
25550
+ const creds = agentCredentialsMount(config.agent);
25551
+ if (fs53.existsSync(creds.hostPath)) {
25552
+ args.push("-v", `${creds.hostPath}:${creds.target}`);
25553
+ }
25554
+ }
25555
+ for (const key of config.env.pass) {
25556
+ if (process.env[key] !== void 0) args.push("-e", key);
25557
+ }
25558
+ for (const port of config.inbound.expose) {
25559
+ args.push("-p", port);
25560
+ }
25561
+ args.push(config.runtime.image);
25562
+ if (agentArgs.length) args.push(...agentArgs);
25563
+ return args;
25564
+ }
25565
+ function imageContentHash(dockerfile, entrypoint) {
25566
+ return crypto8.createHash("sha256").update(dockerfile).update("\0").update(entrypoint).digest("hex").slice(0, 16);
25567
+ }
25568
+ function sandboxBuildDir(cwd = process.cwd()) {
25569
+ return path51.join(cwd, ".node9", "sandbox", "build");
25570
+ }
25571
+ function writeBuildContext(cwd, dockerfile, entrypoint) {
25572
+ const dir = sandboxBuildDir(cwd);
25573
+ fs53.mkdirSync(dir, { recursive: true });
25574
+ fs53.writeFileSync(path51.join(dir, "Dockerfile"), dockerfile);
25575
+ fs53.writeFileSync(path51.join(dir, "entrypoint.sh"), entrypoint);
25576
+ return dir;
25577
+ }
25578
+ function writeAllowlist(cwd, hosts) {
25579
+ const dir = path51.join(cwd, ".node9", "sandbox");
25580
+ fs53.mkdirSync(dir, { recursive: true });
25581
+ const p = path51.join(dir, "allowed-domains.txt");
25582
+ fs53.writeFileSync(p, hosts.join("\n") + "\n");
25583
+ return p;
25584
+ }
25585
+ function resolveHomePath(p) {
25586
+ return p.startsWith("~") ? path51.join(os46.homedir(), p.slice(1)) : path51.resolve(p);
25587
+ }
25588
+
25589
+ // src/cli/commands/sandbox.ts
25590
+ function seedDataDirConfig(dataDir, sandbox) {
25591
+ fs54.mkdirSync(dataDir, { recursive: true });
25592
+ const configPath2 = path52.join(dataDir, "config.json");
25593
+ const seed = {
25594
+ settings: {
25595
+ approvers: {
25596
+ terminal: sandbox.node9.approvals.terminal,
25597
+ native: false,
25598
+ browser: false,
25599
+ cloud: false
25600
+ }
25601
+ }
25602
+ };
25603
+ fs54.writeFileSync(configPath2, JSON.stringify(seed, null, 2), { mode: 384 });
25604
+ }
25605
+ function registerSandboxCommand(program2, version2) {
25606
+ const node9Version2 = pinnedNode9Version(version2);
25607
+ const cmd = program2.command("sandbox").description("Run an agent in a disposable, jailed container \u2014 governed + audited inside");
25608
+ cmd.command("new").description(`Scaffold ${SANDBOX_CONFIG_FILE} in this project`).option("--agent <agent>", "claude (default) or codex", "claude").action((opts) => {
25609
+ const agent = opts.agent === "codex" ? "codex" : "claude";
25610
+ const p = sandboxConfigPath();
25611
+ if (fs54.existsSync(p)) {
25612
+ console.log(
25613
+ chalk27.yellow(` ${SANDBOX_CONFIG_FILE} already exists \u2014 leaving it untouched.`)
25614
+ );
25615
+ return;
25616
+ }
25617
+ fs54.writeFileSync(p, scaffoldSandboxYaml(agent));
25618
+ console.log(
25619
+ chalk27.green(` \u2713 wrote ${SANDBOX_CONFIG_FILE}`) + chalk27.dim(` (agent: ${agent})`)
25620
+ );
25621
+ console.log(
25622
+ chalk27.dim(" Edit it (mounts / allow / expose), then: ") + chalk27.cyan("node9 sandbox run")
25623
+ );
25624
+ });
25625
+ cmd.command("run [agent]").description("Build (if needed) + run the agent jailed. Extra args after -- go to the agent.").allowUnknownOption(true).allowExcessArguments(true).action((agentArg, _opts, command) => {
25626
+ const cwd = process.cwd();
25627
+ const sandbox = loadSandboxConfig(cwd, agentArg || "claude");
25628
+ if (agentArg === "claude" || agentArg === "codex") sandbox.agent = agentArg;
25629
+ const engine = detectEngine(sandbox.runtime.engine);
25630
+ if (!engine.available) {
25631
+ console.error(
25632
+ chalk27.red(` ${sandbox.runtime.engine} not found.`) + chalk27.dim(` Install it first \u2014 node9 sandbox needs a container runtime.`)
25633
+ );
25634
+ process.exit(1);
25635
+ }
25636
+ const node9Config = getConfig(cwd);
25637
+ const compiled = compileAllowlist({
25638
+ agent: sandbox.agent,
25639
+ sandboxAllow: sandbox.outbound.allow,
25640
+ configAllow: node9Config.policy.egress.allow,
25641
+ configDeny: node9Config.policy.egress.deny
25642
+ });
25643
+ if (compiled.rejected.length) {
25644
+ console.log(
25645
+ chalk27.yellow(` \u26A0 ignoring invalid allow hosts: ${compiled.rejected.join(", ")}`)
25646
+ );
25647
+ }
25648
+ if (compiled.denied.length) {
25649
+ console.log(chalk27.dim(` (denied: ${compiled.denied.join(", ")})`));
25650
+ }
25651
+ const allowlistPath = writeAllowlist(cwd, compiled.allow);
25652
+ const dockerfile = renderDockerfile(sandbox, node9Version2);
25653
+ const entrypoint = renderEntrypoint(sandbox);
25654
+ const buildDir = writeBuildContext(cwd, dockerfile, entrypoint);
25655
+ const hash = imageContentHash(dockerfile, entrypoint);
25656
+ const image = sandbox.runtime.image;
25657
+ const hashFile = path52.join(sandboxBuildDir(cwd), ".image-hash");
25658
+ const lastHash = fs54.existsSync(hashFile) ? fs54.readFileSync(hashFile, "utf-8").trim() : "";
25659
+ const imageExists = spawnSync6(sandbox.runtime.engine, ["image", "inspect", image], { stdio: "ignore" }).status === 0;
25660
+ const needBuild = sandbox.runtime.rebuild === "always" || !imageExists || sandbox.runtime.rebuild !== "never" && lastHash !== hash;
25661
+ if (needBuild) {
25662
+ console.log(chalk27.dim(` building ${image} \u2026`));
25663
+ const b = spawnSync6(sandbox.runtime.engine, ["build", "-t", image, buildDir], {
25664
+ stdio: "inherit"
25665
+ });
25666
+ if (b.status !== 0) {
25667
+ console.error(chalk27.red(" build failed."));
25668
+ process.exit(b.status ?? 1);
25669
+ }
25670
+ fs54.writeFileSync(hashFile, hash);
25671
+ }
25672
+ const dataDir = sandboxDataDir(cwd);
25673
+ seedDataDirConfig(dataDir, sandbox);
25674
+ const passthru = command.args.slice(agentArg ? 1 : 0);
25675
+ const runArgs = buildRunArgs({
25676
+ config: sandbox,
25677
+ workspaceHostPath: resolveHomePath(sandbox.workspace.mount),
25678
+ dataHostPath: dataDir,
25679
+ allowlistHostPath: allowlistPath,
25680
+ agentArgs: passthru
25681
+ });
25682
+ if (sandbox.node9.mountAgentCredentials) {
25683
+ const creds = agentCredentialsMount(sandbox.agent);
25684
+ if (fs54.existsSync(creds.hostPath)) {
25685
+ console.log(chalk27.dim(` mounting ${creds.hostPath} (agent credentials, rw)`));
25686
+ } else {
25687
+ console.log(
25688
+ chalk27.yellow(` \u26A0 ${creds.hostPath} not found \u2014 `) + chalk27.dim(`the agent must auth via an env key in env.pass.`)
25689
+ );
25690
+ }
25691
+ }
25692
+ console.log(
25693
+ chalk27.green(` \u{1F6E1}\uFE0F ${sandbox.agent} jailed \u2014 ${compiled.allow.length} hosts allowed
25694
+ `)
25695
+ );
25696
+ const r = spawnSync6(sandbox.runtime.engine, runArgs, { stdio: "inherit" });
25697
+ process.exit(r.status ?? 0);
25698
+ });
25699
+ cmd.command("tail").description("Stream the sandbox's audit log (host-side)").action(() => {
25700
+ const auditPath = path52.join(sandboxDataDir(), "audit.log");
25701
+ if (!fs54.existsSync(auditPath)) {
25702
+ console.log(chalk27.dim(" no sandbox audit yet."));
25703
+ return;
25704
+ }
25705
+ spawnSync6("tail", ["-f", auditPath], { stdio: "inherit" });
25706
+ });
25707
+ cmd.command("logs").description("Dump the sandbox's audit log").action(() => {
25708
+ const auditPath = path52.join(sandboxDataDir(), "audit.log");
25709
+ if (!fs54.existsSync(auditPath)) {
25710
+ console.log(chalk27.dim(" no sandbox audit yet."));
25711
+ return;
25712
+ }
25713
+ process.stdout.write(fs54.readFileSync(auditPath, "utf-8"));
25714
+ });
25715
+ cmd.command("clean").description("Remove the sandbox image, build context, and data").action(() => {
25716
+ const cwd = process.cwd();
25717
+ let sandbox = null;
25718
+ try {
25719
+ sandbox = loadSandboxConfig(cwd);
25720
+ } catch {
25721
+ }
25722
+ if (sandbox) {
25723
+ spawnSync6(sandbox.runtime.engine, ["image", "rm", "-f", sandbox.runtime.image], {
25724
+ stdio: "ignore"
25725
+ });
25726
+ }
25727
+ fs54.rmSync(path52.join(cwd, ".node9", "sandbox"), { recursive: true, force: true });
25728
+ console.log(chalk27.green(" \u2713 sandbox image + build + data removed."));
25729
+ });
25730
+ }
25731
+
25201
25732
  // src/cli/commands/sessions.ts
25202
25733
  init_scan_summary();
25203
25734
  init_litellm();
25204
25735
  init_cost_gemini();
25205
25736
  init_cost_codex();
25206
- import chalk27 from "chalk";
25207
- import fs51 from "fs";
25208
- import path50 from "path";
25209
- import os46 from "os";
25737
+ import chalk28 from "chalk";
25738
+ import fs55 from "fs";
25739
+ import path53 from "path";
25740
+ import os47 from "os";
25210
25741
  function modelPrice(model) {
25211
25742
  const t = pricingFor(model);
25212
25743
  if (!t) return null;
@@ -25223,10 +25754,10 @@ function encodeProjectPath(projectPath) {
25223
25754
  }
25224
25755
  function sessionJsonlPath(projectPath, sessionId) {
25225
25756
  const encoded = encodeProjectPath(projectPath);
25226
- return path50.join(os46.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
25757
+ return path53.join(os47.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
25227
25758
  }
25228
25759
  function projectLabel(projectPath) {
25229
- return projectPath.replace(os46.homedir(), "~");
25760
+ return projectPath.replace(os47.homedir(), "~");
25230
25761
  }
25231
25762
  function parseHistoryLines(lines) {
25232
25763
  const entries = [];
@@ -25295,10 +25826,10 @@ function parseSessionLines(lines) {
25295
25826
  return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
25296
25827
  }
25297
25828
  function loadAuditEntries(auditPath) {
25298
- const aPath = auditPath ?? path50.join(os46.homedir(), ".node9", "audit.log");
25829
+ const aPath = auditPath ?? path53.join(os47.homedir(), ".node9", "audit.log");
25299
25830
  let raw;
25300
25831
  try {
25301
- raw = fs51.readFileSync(aPath, "utf-8");
25832
+ raw = fs55.readFileSync(aPath, "utf-8");
25302
25833
  } catch {
25303
25834
  return [];
25304
25835
  }
@@ -25334,8 +25865,8 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
25334
25865
  return result;
25335
25866
  }
25336
25867
  function buildGeminiSessions(days, allAuditEntries) {
25337
- const tmpDir = path50.join(os46.homedir(), ".gemini", "tmp");
25338
- if (!fs51.existsSync(tmpDir)) return [];
25868
+ const tmpDir = path53.join(os47.homedir(), ".gemini", "tmp");
25869
+ if (!fs55.existsSync(tmpDir)) return [];
25339
25870
  const cutoff = days !== null ? (() => {
25340
25871
  const d = /* @__PURE__ */ new Date();
25341
25872
  d.setDate(d.getDate() - days);
@@ -25344,35 +25875,35 @@ function buildGeminiSessions(days, allAuditEntries) {
25344
25875
  })() : null;
25345
25876
  let slugDirs;
25346
25877
  try {
25347
- slugDirs = fs51.readdirSync(tmpDir);
25878
+ slugDirs = fs55.readdirSync(tmpDir);
25348
25879
  } catch {
25349
25880
  return [];
25350
25881
  }
25351
25882
  const summaries = [];
25352
25883
  for (const slug of slugDirs) {
25353
- const slugPath = path50.join(tmpDir, slug);
25884
+ const slugPath = path53.join(tmpDir, slug);
25354
25885
  try {
25355
- if (!fs51.statSync(slugPath).isDirectory()) continue;
25886
+ if (!fs55.statSync(slugPath).isDirectory()) continue;
25356
25887
  } catch {
25357
25888
  continue;
25358
25889
  }
25359
- let projectRoot = path50.join(os46.homedir(), slug);
25890
+ let projectRoot = path53.join(os47.homedir(), slug);
25360
25891
  try {
25361
- projectRoot = fs51.readFileSync(path50.join(slugPath, ".project_root"), "utf-8").trim();
25892
+ projectRoot = fs55.readFileSync(path53.join(slugPath, ".project_root"), "utf-8").trim();
25362
25893
  } catch {
25363
25894
  }
25364
- const chatsDir = path50.join(slugPath, "chats");
25365
- if (!fs51.existsSync(chatsDir)) continue;
25895
+ const chatsDir = path53.join(slugPath, "chats");
25896
+ if (!fs55.existsSync(chatsDir)) continue;
25366
25897
  let chatFiles;
25367
25898
  try {
25368
- chatFiles = fs51.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
25899
+ chatFiles = fs55.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
25369
25900
  } catch {
25370
25901
  continue;
25371
25902
  }
25372
25903
  for (const chatFile of chatFiles) {
25373
25904
  let raw;
25374
25905
  try {
25375
- raw = fs51.readFileSync(path50.join(chatsDir, chatFile), "utf-8");
25906
+ raw = fs55.readFileSync(path53.join(chatsDir, chatFile), "utf-8");
25376
25907
  } catch {
25377
25908
  continue;
25378
25909
  }
@@ -25452,8 +25983,8 @@ function buildGeminiSessions(days, allAuditEntries) {
25452
25983
  return summaries;
25453
25984
  }
25454
25985
  function buildCodexSessions(days, allAuditEntries) {
25455
- const sessionsBase = path50.join(os46.homedir(), ".codex", "sessions");
25456
- if (!fs51.existsSync(sessionsBase)) return [];
25986
+ const sessionsBase = path53.join(os47.homedir(), ".codex", "sessions");
25987
+ if (!fs55.existsSync(sessionsBase)) return [];
25457
25988
  const cutoff = days !== null ? (() => {
25458
25989
  const d = /* @__PURE__ */ new Date();
25459
25990
  d.setDate(d.getDate() - days);
@@ -25462,29 +25993,29 @@ function buildCodexSessions(days, allAuditEntries) {
25462
25993
  })() : null;
25463
25994
  const jsonlFiles = [];
25464
25995
  try {
25465
- for (const year of fs51.readdirSync(sessionsBase)) {
25466
- const yearPath = path50.join(sessionsBase, year);
25996
+ for (const year of fs55.readdirSync(sessionsBase)) {
25997
+ const yearPath = path53.join(sessionsBase, year);
25467
25998
  try {
25468
- if (!fs51.statSync(yearPath).isDirectory()) continue;
25999
+ if (!fs55.statSync(yearPath).isDirectory()) continue;
25469
26000
  } catch {
25470
26001
  continue;
25471
26002
  }
25472
- for (const month of fs51.readdirSync(yearPath)) {
25473
- const monthPath = path50.join(yearPath, month);
26003
+ for (const month of fs55.readdirSync(yearPath)) {
26004
+ const monthPath = path53.join(yearPath, month);
25474
26005
  try {
25475
- if (!fs51.statSync(monthPath).isDirectory()) continue;
26006
+ if (!fs55.statSync(monthPath).isDirectory()) continue;
25476
26007
  } catch {
25477
26008
  continue;
25478
26009
  }
25479
- for (const day of fs51.readdirSync(monthPath)) {
25480
- const dayPath = path50.join(monthPath, day);
26010
+ for (const day of fs55.readdirSync(monthPath)) {
26011
+ const dayPath = path53.join(monthPath, day);
25481
26012
  try {
25482
- if (!fs51.statSync(dayPath).isDirectory()) continue;
26013
+ if (!fs55.statSync(dayPath).isDirectory()) continue;
25483
26014
  } catch {
25484
26015
  continue;
25485
26016
  }
25486
- for (const file of fs51.readdirSync(dayPath)) {
25487
- if (file.endsWith(".jsonl")) jsonlFiles.push(path50.join(dayPath, file));
26017
+ for (const file of fs55.readdirSync(dayPath)) {
26018
+ if (file.endsWith(".jsonl")) jsonlFiles.push(path53.join(dayPath, file));
25488
26019
  }
25489
26020
  }
25490
26021
  }
@@ -25496,7 +26027,7 @@ function buildCodexSessions(days, allAuditEntries) {
25496
26027
  for (const filePath of jsonlFiles) {
25497
26028
  let lines;
25498
26029
  try {
25499
- lines = fs51.readFileSync(filePath, "utf-8").split("\n");
26030
+ lines = fs55.readFileSync(filePath, "utf-8").split("\n");
25500
26031
  } catch {
25501
26032
  continue;
25502
26033
  }
@@ -25582,10 +26113,10 @@ function buildCodexSessions(days, allAuditEntries) {
25582
26113
  return summaries;
25583
26114
  }
25584
26115
  function buildSessions(days, historyPath) {
25585
- const hPath = historyPath ?? path50.join(os46.homedir(), ".claude", "history.jsonl");
26116
+ const hPath = historyPath ?? path53.join(os47.homedir(), ".claude", "history.jsonl");
25586
26117
  let historyRaw = "";
25587
26118
  try {
25588
- historyRaw = fs51.readFileSync(hPath, "utf-8");
26119
+ historyRaw = fs55.readFileSync(hPath, "utf-8");
25589
26120
  } catch {
25590
26121
  }
25591
26122
  const cutoff = days !== null ? (() => {
@@ -25609,7 +26140,7 @@ function buildSessions(days, historyPath) {
25609
26140
  const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
25610
26141
  let sessionLines = [];
25611
26142
  try {
25612
- sessionLines = fs51.readFileSync(jsonlFile, "utf-8").split("\n");
26143
+ sessionLines = fs55.readFileSync(jsonlFile, "utf-8").split("\n");
25613
26144
  } catch {
25614
26145
  }
25615
26146
  const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
@@ -25695,11 +26226,11 @@ function toolInputSummary(tool, input) {
25695
26226
  }
25696
26227
  function toolColor(tool) {
25697
26228
  const t = tool.toLowerCase();
25698
- if (t === "bash" || t === "execute_bash") return chalk27.red;
25699
- if (t === "write") return chalk27.green;
25700
- if (t === "edit" || t === "notebookedit") return chalk27.yellow;
25701
- if (t === "read") return chalk27.cyan;
25702
- return chalk27.gray;
26229
+ if (t === "bash" || t === "execute_bash") return chalk28.red;
26230
+ if (t === "write") return chalk28.green;
26231
+ if (t === "edit" || t === "notebookedit") return chalk28.yellow;
26232
+ if (t === "read") return chalk28.cyan;
26233
+ return chalk28.gray;
25703
26234
  }
25704
26235
  function barStr2(value, max, width) {
25705
26236
  if (max === 0 || width <= 0) return "\u2591".repeat(width);
@@ -25709,7 +26240,7 @@ function barStr2(value, max, width) {
25709
26240
  function colorBar2(value, max, width) {
25710
26241
  const s = barStr2(value, max, width);
25711
26242
  const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
25712
- return chalk27.cyan(s.slice(0, filled)) + chalk27.dim(s.slice(filled));
26243
+ return chalk28.cyan(s.slice(0, filled)) + chalk28.dim(s.slice(filled));
25713
26244
  }
25714
26245
  function renderSummary(summaries) {
25715
26246
  const totalTools = summaries.reduce((n, s) => n + s.toolCalls.length, 0);
@@ -25739,45 +26270,45 @@ function renderSummary(summaries) {
25739
26270
  }
25740
26271
  const topProjects = [...projCosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
25741
26272
  const W = 20;
25742
- console.log(chalk27.dim(" " + "\u2500".repeat(70)));
26273
+ console.log(chalk28.dim(" " + "\u2500".repeat(70)));
25743
26274
  console.log(
25744
- " " + chalk27.bold.white(String(summaries.length).padEnd(4)) + chalk27.dim("sessions ") + chalk27.bold.yellow(fmtCost3(totalCost).padEnd(10)) + chalk27.dim("total ") + chalk27.bold.white(String(totalTools).padEnd(6)) + chalk27.dim("tool calls ") + chalk27.bold.white(String(totalFiles)) + chalk27.dim(" files modified") + (totalBlocked > 0 ? chalk27.dim(" ") + chalk27.red.bold(String(totalBlocked)) + chalk27.dim(" blocked by node9") : "")
26275
+ " " + chalk28.bold.white(String(summaries.length).padEnd(4)) + chalk28.dim("sessions ") + chalk28.bold.yellow(fmtCost3(totalCost).padEnd(10)) + chalk28.dim("total ") + chalk28.bold.white(String(totalTools).padEnd(6)) + chalk28.dim("tool calls ") + chalk28.bold.white(String(totalFiles)) + chalk28.dim(" files modified") + (totalBlocked > 0 ? chalk28.dim(" ") + chalk28.red.bold(String(totalBlocked)) + chalk28.dim(" blocked by node9") : "")
25745
26276
  );
25746
26277
  console.log(
25747
- " " + chalk27.dim("avg ") + chalk27.white(fmtCost3(avgCost).padEnd(10)) + chalk27.dim("/session ") + chalk27.green(String(snapshots)) + chalk27.dim(` of ${summaries.length} sessions had snapshots`)
26278
+ " " + chalk28.dim("avg ") + chalk28.white(fmtCost3(avgCost).padEnd(10)) + chalk28.dim("/session ") + chalk28.green(String(snapshots)) + chalk28.dim(` of ${summaries.length} sessions had snapshots`)
25748
26279
  );
25749
26280
  console.log("");
25750
- console.log(" " + chalk27.dim("Tool breakdown:"));
26281
+ console.log(" " + chalk28.dim("Tool breakdown:"));
25751
26282
  const maxGroup = Math.max(...Object.values(groups));
25752
26283
  for (const [label2, count] of Object.entries(groups)) {
25753
26284
  if (count === 0) continue;
25754
26285
  const pct = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
25755
26286
  console.log(
25756
- " " + label2.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk27.white(String(count).padStart(4)) + chalk27.dim(` (${String(pct)}%)`)
26287
+ " " + label2.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk28.white(String(count).padStart(4)) + chalk28.dim(` (${String(pct)}%)`)
25757
26288
  );
25758
26289
  }
25759
26290
  console.log("");
25760
26291
  if (topProjects.length > 1) {
25761
- console.log(" " + chalk27.dim("Cost by project:"));
26292
+ console.log(" " + chalk28.dim("Cost by project:"));
25762
26293
  const maxProjCost = topProjects[0][1];
25763
26294
  for (const [proj, cost] of topProjects) {
25764
26295
  console.log(
25765
- " " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " + chalk27.yellow(fmtCost3(cost))
26296
+ " " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " + chalk28.yellow(fmtCost3(cost))
25766
26297
  );
25767
26298
  }
25768
26299
  console.log("");
25769
26300
  }
25770
- console.log(chalk27.dim(" " + "\u2500".repeat(70)));
26301
+ console.log(chalk28.dim(" " + "\u2500".repeat(70)));
25771
26302
  console.log("");
25772
26303
  }
25773
26304
  function renderList(summaries, totalCost) {
25774
26305
  if (summaries.length === 0) {
25775
- console.log(chalk27.yellow(" No sessions found in the requested range.\n"));
26306
+ console.log(chalk28.yellow(" No sessions found in the requested range.\n"));
25776
26307
  return;
25777
26308
  }
25778
- const totalLabel = totalCost > 0 ? chalk27.dim(" ~" + fmtCost3(totalCost) + " total") : "";
26309
+ const totalLabel = totalCost > 0 ? chalk28.dim(" ~" + fmtCost3(totalCost) + " total") : "";
25779
26310
  console.log(
25780
- " " + chalk27.white(String(summaries.length)) + chalk27.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
26311
+ " " + chalk28.white(String(summaries.length)) + chalk28.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
25781
26312
  );
25782
26313
  console.log("");
25783
26314
  let lastGroup = "";
@@ -25785,51 +26316,51 @@ function renderList(summaries, totalCost) {
25785
26316
  const activeDate = fmtDate2(s.lastActiveTime);
25786
26317
  const group = activeDate + " " + s.projectLabel;
25787
26318
  if (group !== lastGroup) {
25788
- console.log(chalk27.dim(" \u2500\u2500\u2500 ") + chalk27.bold(activeDate) + chalk27.dim(" " + s.projectLabel));
26319
+ console.log(chalk28.dim(" \u2500\u2500\u2500 ") + chalk28.bold(activeDate) + chalk28.dim(" " + s.projectLabel));
25789
26320
  lastGroup = group;
25790
26321
  }
25791
26322
  const startDate = fmtDate2(s.startTime);
25792
- const dateRange = startDate !== activeDate ? chalk27.dim(" (" + startDate + " \u2192 " + activeDate + ")") : "";
25793
- const timeStr = chalk27.dim(fmtTime(s.startTime));
25794
- const prompt = chalk27.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
25795
- const tools = s.toolCalls.length > 0 ? chalk27.dim(String(s.toolCalls.length).padStart(3) + " tools") : chalk27.dim(" 0 tools");
25796
- const cost = s.costUSD > 0 ? chalk27.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
25797
- const blocked = s.blockedCalls.length > 0 ? chalk27.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
25798
- const snap = s.hasSnapshot ? chalk27.green(" \u{1F4F8}") : "";
25799
- const agentBadge = chalk27[agentColorName(s.agent ?? "claude")](
26323
+ const dateRange = startDate !== activeDate ? chalk28.dim(" (" + startDate + " \u2192 " + activeDate + ")") : "";
26324
+ const timeStr = chalk28.dim(fmtTime(s.startTime));
26325
+ const prompt = chalk28.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
26326
+ const tools = s.toolCalls.length > 0 ? chalk28.dim(String(s.toolCalls.length).padStart(3) + " tools") : chalk28.dim(" 0 tools");
26327
+ const cost = s.costUSD > 0 ? chalk28.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
26328
+ const blocked = s.blockedCalls.length > 0 ? chalk28.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
26329
+ const snap = s.hasSnapshot ? chalk28.green(" \u{1F4F8}") : "";
26330
+ const agentBadge = chalk28[agentColorName(s.agent ?? "claude")](
25800
26331
  " " + agentBadgeText(s.agent ?? "claude", 0)
25801
26332
  );
25802
- const sid = chalk27.dim(" " + s.sessionId.slice(0, 8));
26333
+ const sid = chalk28.dim(" " + s.sessionId.slice(0, 8));
25803
26334
  console.log(
25804
26335
  ` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}${dateRange}`
25805
26336
  );
25806
26337
  }
25807
26338
  console.log("");
25808
26339
  console.log(
25809
- chalk27.dim(" Run") + " " + chalk27.cyan("node9 sessions --detail <session-id>") + chalk27.dim(" for full tool trace.")
26340
+ chalk28.dim(" Run") + " " + chalk28.cyan("node9 sessions --detail <session-id>") + chalk28.dim(" for full tool trace.")
25810
26341
  );
25811
26342
  console.log("");
25812
26343
  }
25813
26344
  function renderDetail(s) {
25814
26345
  console.log("");
25815
- console.log(chalk27.bold(" Session ") + chalk27.dim(s.sessionId));
26346
+ console.log(chalk28.bold(" Session ") + chalk28.dim(s.sessionId));
25816
26347
  console.log(
25817
- chalk27.bold(" Prompt ") + chalk27.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
26348
+ chalk28.bold(" Prompt ") + chalk28.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
25818
26349
  );
25819
- console.log(chalk27.bold(" Project ") + chalk27.white(s.projectLabel));
26350
+ console.log(chalk28.bold(" Project ") + chalk28.white(s.projectLabel));
25820
26351
  if (s.agent) {
25821
- const agentLabel2 = chalk27[agentColorName(s.agent)](agentDisplayName(s.agent));
25822
- console.log(chalk27.bold(" Agent ") + agentLabel2);
26352
+ const agentLabel2 = chalk28[agentColorName(s.agent)](agentDisplayName(s.agent));
26353
+ console.log(chalk28.bold(" Agent ") + agentLabel2);
25823
26354
  }
25824
- console.log(chalk27.bold(" When ") + chalk27.white(fmtDateTime(s.startTime)));
26355
+ console.log(chalk28.bold(" When ") + chalk28.white(fmtDateTime(s.startTime)));
25825
26356
  if (s.costUSD > 0)
25826
- console.log(chalk27.bold(" Cost ") + chalk27.yellow("~" + fmtCost3(s.costUSD)));
26357
+ console.log(chalk28.bold(" Cost ") + chalk28.yellow("~" + fmtCost3(s.costUSD)));
25827
26358
  console.log(
25828
- chalk27.bold(" Snapshot ") + (s.hasSnapshot ? chalk27.green("\u2713 taken") : chalk27.dim("none"))
26359
+ chalk28.bold(" Snapshot ") + (s.hasSnapshot ? chalk28.green("\u2713 taken") : chalk28.dim("none"))
25829
26360
  );
25830
26361
  console.log("");
25831
26362
  if (s.toolCalls.length === 0 && s.blockedCalls.length === 0) {
25832
- console.log(chalk27.dim(" No tool calls recorded.\n"));
26363
+ console.log(chalk28.dim(" No tool calls recorded.\n"));
25833
26364
  return;
25834
26365
  }
25835
26366
  const timeline = [
@@ -25842,32 +26373,32 @@ function renderDetail(s) {
25842
26373
  });
25843
26374
  const headerParts = [`Tool calls (${s.toolCalls.length})`];
25844
26375
  if (s.blockedCalls.length > 0)
25845
- headerParts.push(chalk27.red(`${s.blockedCalls.length} blocked by node9`));
25846
- console.log(chalk27.bold(" " + headerParts.join(" \xB7 ")));
26376
+ headerParts.push(chalk28.red(`${s.blockedCalls.length} blocked by node9`));
26377
+ console.log(chalk28.bold(" " + headerParts.join(" \xB7 ")));
25847
26378
  console.log("");
25848
26379
  for (const entry of timeline) {
25849
26380
  if (entry.kind === "tool") {
25850
26381
  const tc = entry.tc;
25851
26382
  const colorFn = toolColor(tc.tool);
25852
26383
  const toolPad = colorFn(tc.tool.padEnd(16));
25853
- const detail = chalk27.gray(truncate(toolInputSummary(tc.tool, tc.input), 70));
25854
- const ts = tc.timestamp ? chalk27.dim(fmtTime(tc.timestamp) + " ") : " ";
26384
+ const detail = chalk28.gray(truncate(toolInputSummary(tc.tool, tc.input), 70));
26385
+ const ts = tc.timestamp ? chalk28.dim(fmtTime(tc.timestamp) + " ") : " ";
25855
26386
  console.log(` ${ts}${toolPad} ${detail}`);
25856
26387
  } else {
25857
26388
  const bc = entry.bc;
25858
- const ts = bc.timestamp ? chalk27.dim(fmtTime(bc.timestamp) + " ") : " ";
25859
- const label2 = chalk27.red("\u{1F6D1} BLOCKED".padEnd(16));
25860
- const toolName = chalk27.red(bc.tool.padEnd(10));
25861
- const argsSummary = bc.args ? chalk27.gray(truncate(toolInputSummary(bc.tool, bc.args), 40)) : chalk27.dim("[args not logged]");
25862
- const reason = bc.checkedBy ? chalk27.dim(" \u2190 " + bc.checkedBy) : "";
26389
+ const ts = bc.timestamp ? chalk28.dim(fmtTime(bc.timestamp) + " ") : " ";
26390
+ const label2 = chalk28.red("\u{1F6D1} BLOCKED".padEnd(16));
26391
+ const toolName = chalk28.red(bc.tool.padEnd(10));
26392
+ const argsSummary = bc.args ? chalk28.gray(truncate(toolInputSummary(bc.tool, bc.args), 40)) : chalk28.dim("[args not logged]");
26393
+ const reason = bc.checkedBy ? chalk28.dim(" \u2190 " + bc.checkedBy) : "";
25863
26394
  console.log(` ${ts}${label2} ${toolName} ${argsSummary}${reason}`);
25864
26395
  }
25865
26396
  }
25866
26397
  console.log("");
25867
26398
  if (s.modifiedFiles.length > 0) {
25868
- console.log(chalk27.bold(` Files modified (${s.modifiedFiles.length}):`));
26399
+ console.log(chalk28.bold(` Files modified (${s.modifiedFiles.length}):`));
25869
26400
  for (const f of s.modifiedFiles) {
25870
- console.log(" " + chalk27.yellow(f));
26401
+ console.log(" " + chalk28.yellow(f));
25871
26402
  }
25872
26403
  console.log("");
25873
26404
  }
@@ -25875,13 +26406,13 @@ function renderDetail(s) {
25875
26406
  function registerSessionsCommand(program2) {
25876
26407
  program2.command("sessions").description("Show what your AI agent did \u2014 sessions, tool calls, cost, and file changes").option("--all", "Show all sessions (default: last 7 days)").option("--days <n>", "Show last N days of sessions", "7").option("--detail <sessionId>", "Show full tool trace for a session").action((options) => {
25877
26408
  console.log("");
25878
- console.log(chalk27.cyan.bold("\u{1F4CB} node9 sessions") + chalk27.dim(" \u2014 what your AI agent did"));
26409
+ console.log(chalk28.cyan.bold("\u{1F4CB} node9 sessions") + chalk28.dim(" \u2014 what your AI agent did"));
25879
26410
  console.log("");
25880
26411
  const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
25881
26412
  const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
25882
- console.log(chalk27.dim(" " + rangeLabel));
26413
+ console.log(chalk28.dim(" " + rangeLabel));
25883
26414
  console.log("");
25884
- process.stdout.write(chalk27.dim(" Loading\u2026"));
26415
+ process.stdout.write(chalk28.dim(" Loading\u2026"));
25885
26416
  const summaries = buildSessions(days);
25886
26417
  if (process.stdout.isTTY) {
25887
26418
  process.stdout.clearLine(0);
@@ -25894,8 +26425,8 @@ function registerSessionsCommand(program2) {
25894
26425
  (s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
25895
26426
  );
25896
26427
  if (!target) {
25897
- console.log(chalk27.red(` Session not found: ${options.detail}`));
25898
- console.log(chalk27.dim(" Run `node9 sessions` to list recent sessions.\n"));
26428
+ console.log(chalk28.red(` Session not found: ${options.detail}`));
26429
+ console.log(chalk28.dim(" Run `node9 sessions` to list recent sessions.\n"));
25899
26430
  return;
25900
26431
  }
25901
26432
  renderDetail(target);
@@ -25909,7 +26440,7 @@ function registerSessionsCommand(program2) {
25909
26440
 
25910
26441
  // src/cli/commands/session-taint.ts
25911
26442
  init_daemon();
25912
- import chalk28 from "chalk";
26443
+ import chalk29 from "chalk";
25913
26444
  function resolveSessionId(records, query) {
25914
26445
  const exact = records.find((r) => r.sessionId === query);
25915
26446
  if (exact) return { record: exact };
@@ -25935,22 +26466,22 @@ function registerSessionTaintCommand(program2) {
25935
26466
  const records = await listSessionTaints();
25936
26467
  console.log("");
25937
26468
  if (records.length === 0) {
25938
- console.log(chalk28.dim(" No tainted sessions."));
25939
- console.log(chalk28.dim(" (Taint lives in the daemon \u2014 a stopped daemon has none.)") + "\n");
26469
+ console.log(chalk29.dim(" No tainted sessions."));
26470
+ console.log(chalk29.dim(" (Taint lives in the daemon \u2014 a stopped daemon has none.)") + "\n");
25940
26471
  return;
25941
26472
  }
25942
26473
  console.log(
25943
- " " + chalk28.bold(String(records.length)) + chalk28.dim(` tainted session${records.length !== 1 ? "s" : ""}`)
26474
+ " " + chalk29.bold(String(records.length)) + chalk29.dim(` tainted session${records.length !== 1 ? "s" : ""}`)
25944
26475
  );
25945
26476
  console.log("");
25946
26477
  for (const r of records) {
25947
26478
  console.log(
25948
- " " + chalk28.yellow(r.sessionId.slice(0, 8).padEnd(10)) + chalk28.red(r.source) + sourceGap(r.source) + chalk28.dim("clears in " + fmtRemaining(r.expiresAt))
26479
+ " " + chalk29.yellow(r.sessionId.slice(0, 8).padEnd(10)) + chalk29.red(r.source) + sourceGap(r.source) + chalk29.dim("clears in " + fmtRemaining(r.expiresAt))
25949
26480
  );
25950
26481
  }
25951
26482
  console.log("");
25952
26483
  console.log(
25953
- chalk28.dim(" Run ") + chalk28.cyan("node9 session-taint clear <id>") + chalk28.dim(" to release one, or ") + chalk28.cyan("--all") + chalk28.dim(" for every session.") + "\n"
26484
+ chalk29.dim(" Run ") + chalk29.cyan("node9 session-taint clear <id>") + chalk29.dim(" to release one, or ") + chalk29.cyan("--all") + chalk29.dim(" for every session.") + "\n"
25954
26485
  );
25955
26486
  });
25956
26487
  cmd.command("clear").description("Clear a session's taint so its next network/write action isn't held for review").argument("[sessionId]", "Session id to clear (the 8-char prefix from `list` is accepted)").option("--all", "Clear every session taint").action(async (sessionId, opts) => {
@@ -25958,32 +26489,32 @@ function registerSessionTaintCommand(program2) {
25958
26489
  if (opts.all) {
25959
26490
  const res2 = await clearSessionTaint({ all: true });
25960
26491
  if (res2.daemonUnavailable) {
25961
- console.log(chalk28.dim(" node9 daemon not running \u2014 no active taints to clear.") + "\n");
26492
+ console.log(chalk29.dim(" node9 daemon not running \u2014 no active taints to clear.") + "\n");
25962
26493
  return;
25963
26494
  }
25964
26495
  console.log(
25965
- chalk28.green(" \u2713 ") + `Cleared ${chalk28.bold(String(res2.cleared))} session taint${res2.cleared !== 1 ? "s" : ""}.
26496
+ chalk29.green(" \u2713 ") + `Cleared ${chalk29.bold(String(res2.cleared))} session taint${res2.cleared !== 1 ? "s" : ""}.
25966
26497
  `
25967
26498
  );
25968
26499
  return;
25969
26500
  }
25970
26501
  if (!sessionId) {
25971
- console.log(chalk28.red(" Provide a session id or --all."));
25972
- console.log(chalk28.dim(" Run `node9 session-taint list` to see tainted sessions.") + "\n");
26502
+ console.log(chalk29.red(" Provide a session id or --all."));
26503
+ console.log(chalk29.dim(" Run `node9 session-taint list` to see tainted sessions.") + "\n");
25973
26504
  return;
25974
26505
  }
25975
26506
  const records = await listSessionTaints();
25976
26507
  if (records.length === 0) {
25977
- console.log(chalk28.dim(" No tainted sessions \u2014 nothing to clear.") + "\n");
26508
+ console.log(chalk29.dim(" No tainted sessions \u2014 nothing to clear.") + "\n");
25978
26509
  return;
25979
26510
  }
25980
26511
  const resolved = resolveSessionId(records, sessionId);
25981
26512
  if ("error" in resolved) {
25982
26513
  if (resolved.error === "not-found") {
25983
- console.log(chalk28.red(` No tainted session matches "${sessionId}".`));
26514
+ console.log(chalk29.red(` No tainted session matches "${sessionId}".`));
25984
26515
  } else {
25985
- console.log(chalk28.red(` "${sessionId}" is ambiguous \u2014 matches:`));
25986
- for (const m of resolved.matches) console.log(chalk28.dim(" " + m));
26516
+ console.log(chalk29.red(` "${sessionId}" is ambiguous \u2014 matches:`));
26517
+ for (const m of resolved.matches) console.log(chalk29.dim(" " + m));
25987
26518
  }
25988
26519
  console.log("");
25989
26520
  return;
@@ -25991,24 +26522,24 @@ function registerSessionTaintCommand(program2) {
25991
26522
  const res = await clearSessionTaint({ sessionId: resolved.record.sessionId });
25992
26523
  if (res.cleared > 0) {
25993
26524
  console.log(
25994
- chalk28.green(" \u2713 ") + `Cleared taint for ${chalk28.yellow(resolved.record.sessionId.slice(0, 8))} ` + chalk28.dim(`(was flagged by ${resolved.record.source}).`) + "\n"
26525
+ chalk29.green(" \u2713 ") + `Cleared taint for ${chalk29.yellow(resolved.record.sessionId.slice(0, 8))} ` + chalk29.dim(`(was flagged by ${resolved.record.source}).`) + "\n"
25995
26526
  );
25996
26527
  } else {
25997
26528
  console.log(
25998
- chalk28.dim(` Session ${resolved.record.sessionId.slice(0, 8)} was already clear.`) + "\n"
26529
+ chalk29.dim(` Session ${resolved.record.sessionId.slice(0, 8)} was already clear.`) + "\n"
25999
26530
  );
26000
26531
  }
26001
26532
  });
26002
26533
  }
26003
26534
 
26004
26535
  // src/cli/commands/skill-pin.ts
26005
- import chalk29 from "chalk";
26006
- import fs52 from "fs";
26007
- import os47 from "os";
26008
- import path51 from "path";
26536
+ import chalk30 from "chalk";
26537
+ import fs56 from "fs";
26538
+ import os48 from "os";
26539
+ import path54 from "path";
26009
26540
  function wipeSkillSessions() {
26010
26541
  try {
26011
- fs52.rmSync(path51.join(os47.homedir(), ".node9", "skill-sessions"), {
26542
+ fs56.rmSync(path54.join(os48.homedir(), ".node9", "skill-sessions"), {
26012
26543
  recursive: true,
26013
26544
  force: true
26014
26545
  });
@@ -26022,29 +26553,29 @@ function registerSkillPinCommand(program2) {
26022
26553
  const result = readSkillPinsSafe();
26023
26554
  if (!result.ok) {
26024
26555
  if (result.reason === "missing") {
26025
- console.log(chalk29.gray("\nNo skill roots are pinned yet."));
26556
+ console.log(chalk30.gray("\nNo skill roots are pinned yet."));
26026
26557
  console.log(
26027
- chalk29.gray("Pins are created automatically on the first tool call of each session.\n")
26558
+ chalk30.gray("Pins are created automatically on the first tool call of each session.\n")
26028
26559
  );
26029
26560
  return;
26030
26561
  }
26031
- console.error(chalk29.red(`
26562
+ console.error(chalk30.red(`
26032
26563
  \u274C Pin file is corrupt: ${result.detail}`));
26033
- console.error(chalk29.yellow(" Run: node9 skill pin reset\n"));
26564
+ console.error(chalk30.yellow(" Run: node9 skill pin reset\n"));
26034
26565
  process.exit(1);
26035
26566
  }
26036
26567
  const entries = Object.entries(result.pins.roots);
26037
26568
  if (entries.length === 0) {
26038
- console.log(chalk29.gray("\nNo skill roots are pinned yet.\n"));
26569
+ console.log(chalk30.gray("\nNo skill roots are pinned yet.\n"));
26039
26570
  return;
26040
26571
  }
26041
- console.log(chalk29.bold("\n\u{1F512} Pinned Skill Roots\n"));
26572
+ console.log(chalk30.bold("\n\u{1F512} Pinned Skill Roots\n"));
26042
26573
  for (const [key, entry] of entries) {
26043
- const missing = entry.exists ? "" : chalk29.yellow(" (not present at pin time)");
26044
- console.log(` ${chalk29.cyan(key)} ${chalk29.gray(entry.rootPath)}${missing}`);
26574
+ const missing = entry.exists ? "" : chalk30.yellow(" (not present at pin time)");
26575
+ console.log(` ${chalk30.cyan(key)} ${chalk30.gray(entry.rootPath)}${missing}`);
26045
26576
  console.log(` Files (${entry.fileCount})`);
26046
- console.log(` Hash: ${chalk29.gray(entry.contentHash.slice(0, 16))}...`);
26047
- console.log(` Pinned: ${chalk29.gray(entry.pinnedAt)}
26577
+ console.log(` Hash: ${chalk30.gray(entry.contentHash.slice(0, 16))}...`);
26578
+ console.log(` Pinned: ${chalk30.gray(entry.pinnedAt)}
26048
26579
  `);
26049
26580
  }
26050
26581
  });
@@ -26053,52 +26584,52 @@ function registerSkillPinCommand(program2) {
26053
26584
  try {
26054
26585
  pins = readSkillPins();
26055
26586
  } catch {
26056
- console.error(chalk29.red("\n\u274C Pin file is corrupt."));
26057
- console.error(chalk29.yellow(" Run: node9 skill pin reset\n"));
26587
+ console.error(chalk30.red("\n\u274C Pin file is corrupt."));
26588
+ console.error(chalk30.yellow(" Run: node9 skill pin reset\n"));
26058
26589
  process.exit(1);
26059
26590
  }
26060
26591
  if (!pins.roots[rootKey]) {
26061
- console.error(chalk29.red(`
26592
+ console.error(chalk30.red(`
26062
26593
  \u274C No pin found for root key "${rootKey}"
26063
26594
  `));
26064
- console.error(`Run ${chalk29.cyan("node9 skill pin list")} to see pinned roots.
26595
+ console.error(`Run ${chalk30.cyan("node9 skill pin list")} to see pinned roots.
26065
26596
  `);
26066
26597
  process.exit(1);
26067
26598
  }
26068
26599
  const rootPath = pins.roots[rootKey].rootPath;
26069
26600
  removePin2(rootKey);
26070
26601
  wipeSkillSessions();
26071
- console.log(chalk29.green(`
26072
- \u{1F513} Pin removed for ${chalk29.cyan(rootKey)}`));
26073
- console.log(chalk29.gray(` ${rootPath}`));
26074
- console.log(chalk29.gray(" Next session will re-pin with current state.\n"));
26602
+ console.log(chalk30.green(`
26603
+ \u{1F513} Pin removed for ${chalk30.cyan(rootKey)}`));
26604
+ console.log(chalk30.gray(` ${rootPath}`));
26605
+ console.log(chalk30.gray(" Next session will re-pin with current state.\n"));
26075
26606
  });
26076
26607
  pinSubCmd.command("reset").description("Clear all skill pins and wipe session verification flags").action(() => {
26077
26608
  const result = readSkillPinsSafe();
26078
26609
  if (!result.ok && result.reason === "missing") {
26079
26610
  wipeSkillSessions();
26080
- console.log(chalk29.gray("\nNo pins to clear.\n"));
26611
+ console.log(chalk30.gray("\nNo pins to clear.\n"));
26081
26612
  return;
26082
26613
  }
26083
26614
  const count = result.ok ? Object.keys(result.pins.roots).length : "?";
26084
26615
  clearAllPins2();
26085
26616
  wipeSkillSessions();
26086
- console.log(chalk29.green(`
26617
+ console.log(chalk30.green(`
26087
26618
  \u{1F513} Cleared ${count} skill pin(s).`));
26088
- console.log(chalk29.gray(" Next session will re-pin with current state.\n"));
26619
+ console.log(chalk30.gray(" Next session will re-pin with current state.\n"));
26089
26620
  });
26090
26621
  }
26091
26622
 
26092
26623
  // src/cli/commands/decisions.ts
26093
- import fs53 from "fs";
26094
- import os48 from "os";
26095
- import path52 from "path";
26096
- import chalk30 from "chalk";
26097
- var DECISIONS_FILE2 = path52.join(os48.homedir(), ".node9", "decisions.json");
26624
+ import fs57 from "fs";
26625
+ import os49 from "os";
26626
+ import path55 from "path";
26627
+ import chalk31 from "chalk";
26628
+ var DECISIONS_FILE2 = path55.join(os49.homedir(), ".node9", "decisions.json");
26098
26629
  function readDecisions() {
26099
26630
  try {
26100
- if (!fs53.existsSync(DECISIONS_FILE2)) return {};
26101
- const raw = fs53.readFileSync(DECISIONS_FILE2, "utf-8");
26631
+ if (!fs57.existsSync(DECISIONS_FILE2)) return {};
26632
+ const raw = fs57.readFileSync(DECISIONS_FILE2, "utf-8");
26102
26633
  const parsed = JSON.parse(raw);
26103
26634
  const out = {};
26104
26635
  for (const [k, v] of Object.entries(parsed)) {
@@ -26110,11 +26641,11 @@ function readDecisions() {
26110
26641
  }
26111
26642
  }
26112
26643
  function writeDecisions(d) {
26113
- const dir = path52.dirname(DECISIONS_FILE2);
26114
- if (!fs53.existsSync(dir)) fs53.mkdirSync(dir, { recursive: true });
26644
+ const dir = path55.dirname(DECISIONS_FILE2);
26645
+ if (!fs57.existsSync(dir)) fs57.mkdirSync(dir, { recursive: true });
26115
26646
  const tmp = `${DECISIONS_FILE2}.${process.pid}.tmp`;
26116
- fs53.writeFileSync(tmp, JSON.stringify(d, null, 2));
26117
- fs53.renameSync(tmp, DECISIONS_FILE2);
26647
+ fs57.writeFileSync(tmp, JSON.stringify(d, null, 2));
26648
+ fs57.renameSync(tmp, DECISIONS_FILE2);
26118
26649
  }
26119
26650
  function registerDecisionsCommand(program2) {
26120
26651
  const cmd = program2.command("decisions").description('Manage persistent "Always Allow" / "Always Deny" tool decisions');
@@ -26122,67 +26653,67 @@ function registerDecisionsCommand(program2) {
26122
26653
  const decisions = readDecisions();
26123
26654
  const entries = Object.entries(decisions);
26124
26655
  if (entries.length === 0) {
26125
- console.log(chalk30.gray(" No persistent decisions stored."));
26656
+ console.log(chalk31.gray(" No persistent decisions stored."));
26126
26657
  console.log(
26127
- chalk30.gray(` File: ${DECISIONS_FILE2}
26128
- `) + chalk30.gray(' Decisions are written when you click "Always Allow" or')
26658
+ chalk31.gray(` File: ${DECISIONS_FILE2}
26659
+ `) + chalk31.gray(' Decisions are written when you click "Always Allow" or')
26129
26660
  );
26130
- console.log(chalk30.gray(' "Always Deny" in node9 tail or the native popup.'));
26661
+ console.log(chalk31.gray(' "Always Deny" in node9 tail or the native popup.'));
26131
26662
  return;
26132
26663
  }
26133
- console.log(chalk30.bold(`
26664
+ console.log(chalk31.bold(`
26134
26665
  Persistent decisions (${entries.length})
26135
26666
  `));
26136
26667
  const w = Math.max(...entries.map(([k]) => k.length));
26137
26668
  for (const [tool, verdict] of entries.sort()) {
26138
- const colored = verdict === "allow" ? chalk30.green(verdict) : chalk30.red(verdict);
26669
+ const colored = verdict === "allow" ? chalk31.green(verdict) : chalk31.red(verdict);
26139
26670
  console.log(` ${tool.padEnd(w)} ${colored}`);
26140
26671
  }
26141
26672
  console.log(
26142
- chalk30.gray(`
26673
+ chalk31.gray(`
26143
26674
  Stored in ${DECISIONS_FILE2}
26144
- `) + chalk30.gray(" Run `node9 decisions clear <tool>` to remove an entry.")
26675
+ `) + chalk31.gray(" Run `node9 decisions clear <tool>` to remove an entry.")
26145
26676
  );
26146
26677
  });
26147
26678
  cmd.command("clear <toolName>").description("Remove a persistent decision for one tool").action((toolName) => {
26148
26679
  const decisions = readDecisions();
26149
26680
  if (!(toolName in decisions)) {
26150
- console.log(chalk30.yellow(` No persistent decision for "${toolName}". Nothing to clear.`));
26681
+ console.log(chalk31.yellow(` No persistent decision for "${toolName}". Nothing to clear.`));
26151
26682
  process.exitCode = 1;
26152
26683
  return;
26153
26684
  }
26154
26685
  delete decisions[toolName];
26155
26686
  writeDecisions(decisions);
26156
- console.log(chalk30.green(` \u2713 Cleared persistent decision for "${toolName}".`));
26687
+ console.log(chalk31.green(` \u2713 Cleared persistent decision for "${toolName}".`));
26157
26688
  });
26158
26689
  cmd.command("clear-all").description("Remove every persistent decision (irreversible)").action(() => {
26159
26690
  const decisions = readDecisions();
26160
26691
  const count = Object.keys(decisions).length;
26161
26692
  if (count === 0) {
26162
- console.log(chalk30.gray(" Nothing to clear \u2014 no persistent decisions stored."));
26693
+ console.log(chalk31.gray(" Nothing to clear \u2014 no persistent decisions stored."));
26163
26694
  return;
26164
26695
  }
26165
26696
  writeDecisions({});
26166
26697
  console.log(
26167
- chalk30.green(` \u2713 Cleared ${count} persistent decision${count === 1 ? "" : "s"}.`)
26698
+ chalk31.green(` \u2713 Cleared ${count} persistent decision${count === 1 ? "" : "s"}.`)
26168
26699
  );
26169
26700
  });
26170
26701
  }
26171
26702
 
26172
26703
  // src/cli/commands/dlp.ts
26173
- import chalk31 from "chalk";
26174
- import fs54 from "fs";
26175
- import path53 from "path";
26176
- import os49 from "os";
26177
- var AUDIT_LOG = path53.join(os49.homedir(), ".node9", "audit.log");
26178
- var RESOLVED_FILE = path53.join(os49.homedir(), ".node9", "dlp-resolved.json");
26704
+ import chalk32 from "chalk";
26705
+ import fs58 from "fs";
26706
+ import path56 from "path";
26707
+ import os50 from "os";
26708
+ var AUDIT_LOG = path56.join(os50.homedir(), ".node9", "audit.log");
26709
+ var RESOLVED_FILE = path56.join(os50.homedir(), ".node9", "dlp-resolved.json");
26179
26710
  var ANSI_RE = /\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g;
26180
26711
  function stripAnsi(s) {
26181
26712
  return s.replace(ANSI_RE, "");
26182
26713
  }
26183
26714
  function loadResolved() {
26184
26715
  try {
26185
- const raw = JSON.parse(fs54.readFileSync(RESOLVED_FILE, "utf-8"));
26716
+ const raw = JSON.parse(fs58.readFileSync(RESOLVED_FILE, "utf-8"));
26186
26717
  return new Set(raw);
26187
26718
  } catch {
26188
26719
  return /* @__PURE__ */ new Set();
@@ -26190,13 +26721,13 @@ function loadResolved() {
26190
26721
  }
26191
26722
  function saveResolved(resolved) {
26192
26723
  try {
26193
- fs54.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
26724
+ fs58.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
26194
26725
  } catch {
26195
26726
  }
26196
26727
  }
26197
26728
  function loadDlpFindings() {
26198
- if (!fs54.existsSync(AUDIT_LOG)) return [];
26199
- return fs54.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
26729
+ if (!fs58.existsSync(AUDIT_LOG)) return [];
26730
+ return fs58.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
26200
26731
  if (!line.trim()) return [];
26201
26732
  try {
26202
26733
  const e = JSON.parse(line);
@@ -26225,14 +26756,14 @@ function registerDlpCommand(program2) {
26225
26756
  cmd.command("resolve").description("Mark all current DLP findings as resolved").action(() => {
26226
26757
  const findings = loadDlpFindings();
26227
26758
  if (findings.length === 0) {
26228
- console.log(chalk31.green("\n \u2705 No response-DLP findings to resolve.\n"));
26759
+ console.log(chalk32.green("\n \u2705 No response-DLP findings to resolve.\n"));
26229
26760
  return;
26230
26761
  }
26231
26762
  const resolved = loadResolved();
26232
26763
  for (const e of findings) resolved.add(entryKey2(e));
26233
26764
  saveResolved(resolved);
26234
26765
  console.log(
26235
- chalk31.green(
26766
+ chalk32.green(
26236
26767
  `
26237
26768
  \u2705 ${findings.length} finding${findings.length !== 1 ? "s" : ""} marked as resolved.
26238
26769
  `
@@ -26246,47 +26777,47 @@ function registerDlpCommand(program2) {
26246
26777
  const resolvedCount = findings.length - open.length;
26247
26778
  console.log("");
26248
26779
  console.log(
26249
- chalk31.bold.cyan("\u{1F510} node9 dlp") + chalk31.dim(" \u2014 secrets found in Claude response text")
26780
+ chalk32.bold.cyan("\u{1F510} node9 dlp") + chalk32.dim(" \u2014 secrets found in Claude response text")
26250
26781
  );
26251
26782
  console.log("");
26252
26783
  if (open.length === 0) {
26253
26784
  if (resolvedCount > 0) {
26254
- console.log(chalk31.green(` \u2705 No open findings \xB7 ${resolvedCount} previously resolved`));
26785
+ console.log(chalk32.green(` \u2705 No open findings \xB7 ${resolvedCount} previously resolved`));
26255
26786
  } else {
26256
26787
  console.log(
26257
- chalk31.green(" \u2705 No findings \u2014 Claude has not leaked secrets in response text")
26788
+ chalk32.green(" \u2705 No findings \u2014 Claude has not leaked secrets in response text")
26258
26789
  );
26259
26790
  }
26260
26791
  console.log("");
26261
26792
  return;
26262
26793
  }
26263
26794
  console.log(
26264
- chalk31.bgRed.white.bold(` \u26A0\uFE0F ${open.length} open finding${open.length !== 1 ? "s" : ""} `) + chalk31.dim(resolvedCount > 0 ? ` (${resolvedCount} resolved)` : "")
26795
+ chalk32.bgRed.white.bold(` \u26A0\uFE0F ${open.length} open finding${open.length !== 1 ? "s" : ""} `) + chalk32.dim(resolvedCount > 0 ? ` (${resolvedCount} resolved)` : "")
26265
26796
  );
26266
26797
  console.log("");
26267
26798
  console.log(
26268
- chalk31.dim(" These secrets were included in Claude's response text \u2014 NOT blocked.")
26799
+ chalk32.dim(" These secrets were included in Claude's response text \u2014 NOT blocked.")
26269
26800
  );
26270
- console.log(chalk31.dim(" Rotate each affected key immediately.\n"));
26801
+ console.log(chalk32.dim(" Rotate each affected key immediately.\n"));
26271
26802
  for (const e of open) {
26272
26803
  console.log(
26273
- " " + chalk31.red("\u25CF") + " " + chalk31.white(e.dlpPattern ?? "Secret") + chalk31.dim(" " + fmtDate3(e.ts))
26804
+ " " + chalk32.red("\u25CF") + " " + chalk32.white(e.dlpPattern ?? "Secret") + chalk32.dim(" " + fmtDate3(e.ts))
26274
26805
  );
26275
26806
  if (e.dlpSample) {
26276
- console.log(" " + chalk31.dim("Sample: ") + chalk31.yellow(stripAnsi(e.dlpSample)));
26807
+ console.log(" " + chalk32.dim("Sample: ") + chalk32.yellow(stripAnsi(e.dlpSample)));
26277
26808
  }
26278
26809
  if (e.project) {
26279
- console.log(" " + chalk31.dim("Project: ") + chalk31.dim(stripAnsi(e.project)));
26810
+ console.log(" " + chalk32.dim("Project: ") + chalk32.dim(stripAnsi(e.project)));
26280
26811
  }
26281
26812
  console.log("");
26282
26813
  }
26283
- console.log(" " + chalk31.bold("Next steps:"));
26284
- console.log(" " + chalk31.cyan("1.") + " Rotate any exposed keys shown above");
26814
+ console.log(" " + chalk32.bold("Next steps:"));
26815
+ console.log(" " + chalk32.cyan("1.") + " Rotate any exposed keys shown above");
26285
26816
  console.log(
26286
- " " + chalk31.cyan("2.") + " Run " + chalk31.white("node9 dlp resolve") + " to acknowledge"
26817
+ " " + chalk32.cyan("2.") + " Run " + chalk32.white("node9 dlp resolve") + " to acknowledge"
26287
26818
  );
26288
26819
  console.log(
26289
- " " + chalk31.cyan("3.") + " Run " + chalk31.white("node9 report") + " for full audit history"
26820
+ " " + chalk32.cyan("3.") + " Run " + chalk32.white("node9 report") + " for full audit history"
26290
26821
  );
26291
26822
  console.log("");
26292
26823
  });
@@ -26294,15 +26825,15 @@ function registerDlpCommand(program2) {
26294
26825
 
26295
26826
  // src/cli/commands/mask.ts
26296
26827
  init_dlp();
26297
- import chalk32 from "chalk";
26298
- import fs55 from "fs";
26299
- import path54 from "path";
26300
- import os50 from "os";
26828
+ import chalk33 from "chalk";
26829
+ import fs59 from "fs";
26830
+ import path57 from "path";
26831
+ import os51 from "os";
26301
26832
  function findJsonlFiles(dir) {
26302
26833
  const results = [];
26303
- if (!fs55.existsSync(dir)) return results;
26304
- for (const entry of fs55.readdirSync(dir, { withFileTypes: true })) {
26305
- const full = path54.join(dir, entry.name);
26834
+ if (!fs59.existsSync(dir)) return results;
26835
+ for (const entry of fs59.readdirSync(dir, { withFileTypes: true })) {
26836
+ const full = path57.join(dir, entry.name);
26306
26837
  if (entry.isDirectory()) results.push(...findJsonlFiles(full));
26307
26838
  else if (entry.isFile() && entry.name.endsWith(".jsonl")) results.push(full);
26308
26839
  }
@@ -26345,7 +26876,7 @@ function redactJson(obj) {
26345
26876
  function processFile(filePath, dryRun) {
26346
26877
  let raw;
26347
26878
  try {
26348
- raw = fs55.readFileSync(filePath, "utf-8");
26879
+ raw = fs59.readFileSync(filePath, "utf-8");
26349
26880
  } catch {
26350
26881
  return { redactedLines: 0, patterns: [] };
26351
26882
  }
@@ -26377,14 +26908,14 @@ function processFile(filePath, dryRun) {
26377
26908
  }
26378
26909
  }
26379
26910
  if (!dryRun && redactedLines > 0) {
26380
- fs55.writeFileSync(filePath, newLines.join("\n"), "utf-8");
26911
+ fs59.writeFileSync(filePath, newLines.join("\n"), "utf-8");
26381
26912
  }
26382
26913
  return { redactedLines, patterns };
26383
26914
  }
26384
26915
  function processJsonFile(filePath, dryRun) {
26385
26916
  let raw;
26386
26917
  try {
26387
- raw = fs55.readFileSync(filePath, "utf-8");
26918
+ raw = fs59.readFileSync(filePath, "utf-8");
26388
26919
  } catch {
26389
26920
  return { redactedLines: 0, patterns: [] };
26390
26921
  }
@@ -26397,15 +26928,15 @@ function processJsonFile(filePath, dryRun) {
26397
26928
  const { value, modified, found } = redactJson(parsed);
26398
26929
  if (!modified) return { redactedLines: 0, patterns: [] };
26399
26930
  if (!dryRun) {
26400
- fs55.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
26931
+ fs59.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
26401
26932
  }
26402
26933
  return { redactedLines: 1, patterns: found };
26403
26934
  }
26404
26935
  function findJsonFiles(dir) {
26405
26936
  const results = [];
26406
- if (!fs55.existsSync(dir)) return results;
26407
- for (const entry of fs55.readdirSync(dir, { withFileTypes: true })) {
26408
- const full = path54.join(dir, entry.name);
26937
+ if (!fs59.existsSync(dir)) return results;
26938
+ for (const entry of fs59.readdirSync(dir, { withFileTypes: true })) {
26939
+ const full = path57.join(dir, entry.name);
26409
26940
  if (entry.isDirectory()) results.push(...findJsonFiles(full));
26410
26941
  else if (entry.isFile() && entry.name.endsWith(".json")) results.push(full);
26411
26942
  }
@@ -26414,9 +26945,9 @@ function findJsonFiles(dir) {
26414
26945
  function registerMaskCommand(program2) {
26415
26946
  program2.command("mask").description("Redact plaintext secrets from local AI session history files").option("--dry-run", "show what would be redacted without making changes").option("--all", "scan all history (default: last 30 days)").action(async (options) => {
26416
26947
  const dryRun = !!options.dryRun;
26417
- const home = os50.homedir();
26418
- const claudeDir = path54.join(home, ".claude", "projects");
26419
- const geminiDir = path54.join(home, ".gemini", "tmp");
26948
+ const home = os51.homedir();
26949
+ const claudeDir = path57.join(home, ".claude", "projects");
26950
+ const geminiDir = path57.join(home, ".gemini", "tmp");
26420
26951
  const allFiles = [
26421
26952
  ...findJsonlFiles(claudeDir).map((p) => ({ path: p, type: "jsonl" })),
26422
26953
  ...findJsonFiles(geminiDir).map((p) => ({ path: p, type: "json" }))
@@ -26424,18 +26955,18 @@ function registerMaskCommand(program2) {
26424
26955
  const cutoff = options.all ? null : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
26425
26956
  const filtered = cutoff ? allFiles.filter((f) => {
26426
26957
  try {
26427
- return fs55.statSync(f.path).mtime >= cutoff;
26958
+ return fs59.statSync(f.path).mtime >= cutoff;
26428
26959
  } catch {
26429
26960
  return false;
26430
26961
  }
26431
26962
  }) : allFiles;
26432
26963
  if (filtered.length === 0) {
26433
- console.log(chalk32.yellow(" No session files found."));
26964
+ console.log(chalk33.yellow(" No session files found."));
26434
26965
  return;
26435
26966
  }
26436
26967
  console.log("");
26437
26968
  if (dryRun) {
26438
- console.log(chalk32.dim(" Dry run \u2014 no files will be modified.\n"));
26969
+ console.log(chalk33.dim(" Dry run \u2014 no files will be modified.\n"));
26439
26970
  }
26440
26971
  let totalFiles = 0;
26441
26972
  let totalLines = 0;
@@ -26451,23 +26982,23 @@ function registerMaskCommand(program2) {
26451
26982
  });
26452
26983
  const verb = dryRun ? "Would redact" : "Redacted";
26453
26984
  console.log(
26454
- " " + chalk32.dim(shortPath.slice(0, 60).padEnd(62)) + chalk32.red(`${verb}: `) + chalk32.yellow(patterns.join(", ")) + chalk32.dim(` (${redactedLines} line${redactedLines !== 1 ? "s" : ""})`)
26985
+ " " + chalk33.dim(shortPath.slice(0, 60).padEnd(62)) + chalk33.red(`${verb}: `) + chalk33.yellow(patterns.join(", ")) + chalk33.dim(` (${redactedLines} line${redactedLines !== 1 ? "s" : ""})`)
26455
26986
  );
26456
26987
  }
26457
26988
  }
26458
26989
  console.log("");
26459
26990
  if (totalFiles === 0) {
26460
- console.log(chalk32.green(" No secrets found in session history."));
26991
+ console.log(chalk33.green(" No secrets found in session history."));
26461
26992
  } else {
26462
26993
  const verb = dryRun ? "would be modified" : "modified";
26463
26994
  console.log(
26464
- chalk32.bold(` ${totalFiles} file${totalFiles !== 1 ? "s" : ""} ${verb}`) + chalk32.dim(`, ${totalLines} line${totalLines !== 1 ? "s" : ""} redacted`)
26995
+ chalk33.bold(` ${totalFiles} file${totalFiles !== 1 ? "s" : ""} ${verb}`) + chalk33.dim(`, ${totalLines} line${totalLines !== 1 ? "s" : ""} redacted`)
26465
26996
  );
26466
- console.log(" Patterns: " + chalk32.yellow(totalPatterns.join(", ")));
26997
+ console.log(" Patterns: " + chalk33.yellow(totalPatterns.join(", ")));
26467
26998
  if (!dryRun) {
26468
26999
  console.log("");
26469
27000
  console.log(
26470
- chalk32.dim(
27001
+ chalk33.dim(
26471
27002
  " Note: secrets were already sent to the AI provider during the active session.\n This cleans your local disk only. Rotate any exposed keys."
26472
27003
  )
26473
27004
  );
@@ -26480,20 +27011,20 @@ function registerMaskCommand(program2) {
26480
27011
  // src/cli.ts
26481
27012
  init_blast();
26482
27013
  var { version } = JSON.parse(
26483
- fs58.readFileSync(path57.join(__dirname, "../package.json"), "utf-8")
27014
+ fs62.readFileSync(path60.join(__dirname, "../package.json"), "utf-8")
26484
27015
  );
26485
27016
  var program = new Command();
26486
27017
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
26487
27018
  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) => {
26488
27019
  const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
26489
- const credPath = path57.join(os53.homedir(), ".node9", "credentials.json");
26490
- if (!fs58.existsSync(path57.dirname(credPath)))
26491
- fs58.mkdirSync(path57.dirname(credPath), { recursive: true });
27020
+ const credPath = path60.join(os54.homedir(), ".node9", "credentials.json");
27021
+ if (!fs62.existsSync(path60.dirname(credPath)))
27022
+ fs62.mkdirSync(path60.dirname(credPath), { recursive: true });
26492
27023
  const profileName = options.profile || "default";
26493
27024
  let existingCreds = {};
26494
27025
  try {
26495
- if (fs58.existsSync(credPath)) {
26496
- const raw = JSON.parse(fs58.readFileSync(credPath, "utf-8"));
27026
+ if (fs62.existsSync(credPath)) {
27027
+ const raw = JSON.parse(fs62.readFileSync(credPath, "utf-8"));
26497
27028
  if (raw.apiKey) {
26498
27029
  existingCreds = {
26499
27030
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
@@ -26505,14 +27036,14 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
26505
27036
  } catch {
26506
27037
  }
26507
27038
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
26508
- fs58.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
27039
+ fs62.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
26509
27040
  let effectiveCloud = null;
26510
27041
  if (profileName === "default") {
26511
- const configPath2 = path57.join(os53.homedir(), ".node9", "config.json");
27042
+ const configPath2 = path60.join(os54.homedir(), ".node9", "config.json");
26512
27043
  let config = {};
26513
27044
  try {
26514
- if (fs58.existsSync(configPath2))
26515
- config = JSON.parse(fs58.readFileSync(configPath2, "utf-8"));
27045
+ if (fs62.existsSync(configPath2))
27046
+ config = JSON.parse(fs62.readFileSync(configPath2, "utf-8"));
26516
27047
  } catch {
26517
27048
  }
26518
27049
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -26527,35 +27058,35 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
26527
27058
  approvers.cloud = false;
26528
27059
  }
26529
27060
  s.approvers = approvers;
26530
- if (!fs58.existsSync(path57.dirname(configPath2)))
26531
- fs58.mkdirSync(path57.dirname(configPath2), { recursive: true });
26532
- fs58.writeFileSync(configPath2, JSON.stringify(config, null, 2), { mode: 384 });
27061
+ if (!fs62.existsSync(path60.dirname(configPath2)))
27062
+ fs62.mkdirSync(path60.dirname(configPath2), { recursive: true });
27063
+ fs62.writeFileSync(configPath2, JSON.stringify(config, null, 2), { mode: 384 });
26533
27064
  effectiveCloud = approvers.cloud === true;
26534
27065
  }
26535
27066
  if (options.profile && profileName !== "default") {
26536
- console.log(chalk34.green(`\u2705 Profile "${profileName}" saved`));
26537
- console.log(chalk34.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
27067
+ console.log(chalk35.green(`\u2705 Profile "${profileName}" saved`));
27068
+ console.log(chalk35.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
26538
27069
  } else if (options.local || effectiveCloud === false) {
26539
- console.log(chalk34.green(`\u2705 Key saved \u2014 Privacy mode \u{1F6E1}\uFE0F`));
26540
- console.log(chalk34.gray(` All decisions stay on this machine. Nothing syncs to the cloud.`));
27070
+ console.log(chalk35.green(`\u2705 Key saved \u2014 Privacy mode \u{1F6E1}\uFE0F`));
27071
+ console.log(chalk35.gray(` All decisions stay on this machine. Nothing syncs to the cloud.`));
26541
27072
  if (!options.local) {
26542
27073
  console.log(
26543
- chalk34.yellow(` Your config has cloud approvals OFF (settings.approvers.cloud).`)
27074
+ chalk35.yellow(` Your config has cloud approvals OFF (settings.approvers.cloud).`)
26544
27075
  );
26545
27076
  console.log(
26546
- chalk34.gray(` To enable team policy + dashboard sync: set it to true, or re-init.`)
27077
+ chalk35.gray(` To enable team policy + dashboard sync: set it to true, or re-init.`)
26547
27078
  );
26548
27079
  }
26549
27080
  } else {
26550
- console.log(chalk34.green(`\u2705 Logged in \u2014 agent mode`));
26551
- console.log(chalk34.gray(` Team policy enforced for all calls via Node9 cloud.`));
27081
+ console.log(chalk35.green(`\u2705 Logged in \u2014 agent mode`));
27082
+ console.log(chalk35.gray(` Team policy enforced for all calls via Node9 cloud.`));
26552
27083
  }
26553
27084
  });
26554
27085
  program.command("signup").description("Create your node9 account / open the dashboard in your browser").option("--login", "Open the login page instead of signup").action((options) => {
26555
27086
  const route = options.login ? "auth/login" : "auth/signup";
26556
27087
  const url = `https://node9.ai/${route}?ref=cli_cmd`;
26557
27088
  console.log("");
26558
- console.log(" " + chalk34.dim("Opening ") + chalk34.cyan.underline(url));
27089
+ console.log(" " + chalk35.dim("Opening ") + chalk35.cyan.underline(url));
26559
27090
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
26560
27091
  try {
26561
27092
  const child = spawn9(opener, [url], {
@@ -26588,7 +27119,7 @@ program.command("addto", { hidden: true }).description("Integrate Node9 with an
26588
27119
  if (target === "hermes") return setupHermes();
26589
27120
  if (target === "hud") return setupHud();
26590
27121
  console.error(
26591
- chalk34.red(
27122
+ chalk35.red(
26592
27123
  `Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
26593
27124
  )
26594
27125
  );
@@ -26602,20 +27133,20 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
26602
27133
  "The agent to protect: claude | antigravity | copilot | gemini | cursor | codex | windsurf | vscode | hud"
26603
27134
  ).action(async (target) => {
26604
27135
  if (!target) {
26605
- console.log(chalk34.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
26606
- console.log(" Usage: " + chalk34.white("node9 setup <target>") + "\n");
27136
+ console.log(chalk35.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
27137
+ console.log(" Usage: " + chalk35.white("node9 setup <target>") + "\n");
26607
27138
  console.log(" Targets:");
26608
- console.log(" " + chalk34.green("claude") + " \u2014 Claude Code (hook mode)");
26609
- console.log(" " + chalk34.green("gemini") + " \u2014 Gemini CLI (hook mode)");
26610
- console.log(" " + chalk34.green("antigravity") + " \u2014 Antigravity / agy (hook mode)");
26611
- console.log(" " + chalk34.green("copilot") + " \u2014 GitHub Copilot CLI (hook mode)");
26612
- console.log(" " + chalk34.green("cursor") + " \u2014 Cursor (MCP proxy)");
26613
- console.log(" " + chalk34.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
26614
- console.log(" " + chalk34.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
26615
- console.log(" " + chalk34.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
26616
- console.log(" " + chalk34.green("hermes") + " \u2014 Hermes Agent (hook mode)");
27139
+ console.log(" " + chalk35.green("claude") + " \u2014 Claude Code (hook mode)");
27140
+ console.log(" " + chalk35.green("gemini") + " \u2014 Gemini CLI (hook mode)");
27141
+ console.log(" " + chalk35.green("antigravity") + " \u2014 Antigravity / agy (hook mode)");
27142
+ console.log(" " + chalk35.green("copilot") + " \u2014 GitHub Copilot CLI (hook mode)");
27143
+ console.log(" " + chalk35.green("cursor") + " \u2014 Cursor (MCP proxy)");
27144
+ console.log(" " + chalk35.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
27145
+ console.log(" " + chalk35.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
27146
+ console.log(" " + chalk35.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
27147
+ console.log(" " + chalk35.green("hermes") + " \u2014 Hermes Agent (hook mode)");
26617
27148
  process.stdout.write(
26618
- " " + chalk34.green("hud") + " \u2014 Claude Code security statusline\n"
27149
+ " " + chalk35.green("hud") + " \u2014 Claude Code security statusline\n"
26619
27150
  );
26620
27151
  console.log("");
26621
27152
  return;
@@ -26632,7 +27163,7 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
26632
27163
  if (t === "hermes") return setupHermes();
26633
27164
  if (t === "hud") return setupHud();
26634
27165
  console.error(
26635
- chalk34.red(
27166
+ chalk35.red(
26636
27167
  `Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
26637
27168
  )
26638
27169
  );
@@ -26658,33 +27189,33 @@ program.command("removefrom", { hidden: true }).description("Remove Node9 hooks
26658
27189
  else if (target === "hud") fn = teardownHud;
26659
27190
  else {
26660
27191
  console.error(
26661
- chalk34.red(
27192
+ chalk35.red(
26662
27193
  `Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
26663
27194
  )
26664
27195
  );
26665
27196
  process.exit(1);
26666
27197
  }
26667
- console.log(chalk34.cyan(`
27198
+ console.log(chalk35.cyan(`
26668
27199
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
26669
27200
  `));
26670
27201
  try {
26671
27202
  fn();
26672
27203
  } catch (err2) {
26673
- console.error(chalk34.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
27204
+ console.error(chalk35.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
26674
27205
  process.exit(1);
26675
27206
  }
26676
- console.log(chalk34.gray("\n Restart the agent for changes to take effect."));
27207
+ console.log(chalk35.gray("\n Restart the agent for changes to take effect."));
26677
27208
  });
26678
27209
  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) => {
26679
- console.log(chalk34.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
26680
- console.log(chalk34.bold("Stopping daemon..."));
27210
+ console.log(chalk35.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
27211
+ console.log(chalk35.bold("Stopping daemon..."));
26681
27212
  try {
26682
27213
  stopDaemon();
26683
- console.log(chalk34.green(" \u2705 Daemon stopped"));
27214
+ console.log(chalk35.green(" \u2705 Daemon stopped"));
26684
27215
  } catch {
26685
- console.log(chalk34.blue(" \u2139\uFE0F Daemon was not running"));
27216
+ console.log(chalk35.blue(" \u2139\uFE0F Daemon was not running"));
26686
27217
  }
26687
- console.log(chalk34.bold("\nRemoving hooks..."));
27218
+ console.log(chalk35.bold("\nRemoving hooks..."));
26688
27219
  let teardownFailed = false;
26689
27220
  for (const [label2, fn] of [
26690
27221
  ["Claude", teardownClaude],
@@ -26700,45 +27231,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
26700
27231
  } catch (err2) {
26701
27232
  teardownFailed = true;
26702
27233
  console.error(
26703
- chalk34.red(
27234
+ chalk35.red(
26704
27235
  ` \u26A0\uFE0F Failed to remove ${label2} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
26705
27236
  )
26706
27237
  );
26707
27238
  }
26708
27239
  }
26709
27240
  if (options.purge) {
26710
- const node9Dir = path57.join(os53.homedir(), ".node9");
26711
- if (fs58.existsSync(node9Dir)) {
27241
+ const node9Dir = path60.join(os54.homedir(), ".node9");
27242
+ if (fs62.existsSync(node9Dir)) {
26712
27243
  const confirmed = await confirm2({
26713
27244
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
26714
27245
  default: false
26715
27246
  });
26716
27247
  if (confirmed) {
26717
- fs58.rmSync(node9Dir, { recursive: true });
26718
- if (fs58.existsSync(node9Dir)) {
27248
+ fs62.rmSync(node9Dir, { recursive: true });
27249
+ if (fs62.existsSync(node9Dir)) {
26719
27250
  console.error(
26720
- chalk34.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
27251
+ chalk35.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
26721
27252
  );
26722
27253
  } else {
26723
- console.log(chalk34.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
27254
+ console.log(chalk35.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
26724
27255
  }
26725
27256
  } else {
26726
- console.log(chalk34.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
27257
+ console.log(chalk35.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
26727
27258
  }
26728
27259
  } else {
26729
- console.log(chalk34.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
27260
+ console.log(chalk35.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
26730
27261
  }
26731
27262
  } else {
26732
27263
  console.log(
26733
- chalk34.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
27264
+ chalk35.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
26734
27265
  );
26735
27266
  }
26736
27267
  if (teardownFailed) {
26737
- console.error(chalk34.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
27268
+ console.error(chalk35.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
26738
27269
  process.exit(1);
26739
27270
  }
26740
- console.log(chalk34.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g node9-ai"));
26741
- console.log(chalk34.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
27271
+ console.log(chalk35.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g node9-ai"));
27272
+ console.log(chalk35.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
26742
27273
  });
26743
27274
  registerDoctorCommand(program, version);
26744
27275
  program.command("explain").description(
@@ -26751,7 +27282,7 @@ program.command("explain").description(
26751
27282
  try {
26752
27283
  args = JSON.parse(trimmed);
26753
27284
  } catch {
26754
- console.error(chalk34.red(`
27285
+ console.error(chalk35.red(`
26755
27286
  \u274C Invalid JSON: ${trimmed}
26756
27287
  `));
26757
27288
  process.exit(1);
@@ -26762,54 +27293,54 @@ program.command("explain").description(
26762
27293
  }
26763
27294
  const result = await explainPolicy(tool, args);
26764
27295
  console.log("");
26765
- console.log(chalk34.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
27296
+ console.log(chalk35.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
26766
27297
  console.log("");
26767
- console.log(` ${chalk34.bold("Tool:")} ${chalk34.white(result.tool)}`);
27298
+ console.log(` ${chalk35.bold("Tool:")} ${chalk35.white(result.tool)}`);
26768
27299
  if (argsRaw) {
26769
27300
  const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
26770
- console.log(` ${chalk34.bold("Input:")} ${chalk34.gray(preview2)}`);
27301
+ console.log(` ${chalk35.bold("Input:")} ${chalk35.gray(preview2)}`);
26771
27302
  }
26772
27303
  console.log("");
26773
- console.log(chalk34.bold("Config Sources (Waterfall):"));
27304
+ console.log(chalk35.bold("Config Sources (Waterfall):"));
26774
27305
  for (const tier of result.waterfall) {
26775
- const num3 = chalk34.gray(` ${tier.tier}.`);
27306
+ const num3 = chalk35.gray(` ${tier.tier}.`);
26776
27307
  const label2 = tier.label.padEnd(16);
26777
27308
  let statusStr;
26778
27309
  if (tier.tier === 1) {
26779
- statusStr = chalk34.gray(tier.note ?? "");
27310
+ statusStr = chalk35.gray(tier.note ?? "");
26780
27311
  } else if (tier.status === "active") {
26781
- const loc = tier.path ? chalk34.gray(tier.path) : "";
26782
- const note = tier.note ? chalk34.gray(`(${tier.note})`) : "";
26783
- statusStr = chalk34.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
27312
+ const loc = tier.path ? chalk35.gray(tier.path) : "";
27313
+ const note = tier.note ? chalk35.gray(`(${tier.note})`) : "";
27314
+ statusStr = chalk35.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
26784
27315
  } else {
26785
- statusStr = chalk34.gray("\u25CB " + (tier.note ?? "not found"));
27316
+ statusStr = chalk35.gray("\u25CB " + (tier.note ?? "not found"));
26786
27317
  }
26787
- console.log(`${num3} ${chalk34.white(label2)} ${statusStr}`);
27318
+ console.log(`${num3} ${chalk35.white(label2)} ${statusStr}`);
26788
27319
  }
26789
27320
  console.log("");
26790
- console.log(chalk34.bold("Policy Evaluation:"));
27321
+ console.log(chalk35.bold("Policy Evaluation:"));
26791
27322
  for (const step of result.steps) {
26792
27323
  const isFinal = step.isFinal;
26793
27324
  let icon;
26794
- if (step.outcome === "allow") icon = chalk34.green(" \u2705");
26795
- else if (step.outcome === "review") icon = chalk34.red(" \u{1F534}");
26796
- else if (step.outcome === "skip") icon = chalk34.gray(" \u2500 ");
26797
- else icon = chalk34.gray(" \u25CB ");
27325
+ if (step.outcome === "allow") icon = chalk35.green(" \u2705");
27326
+ else if (step.outcome === "review") icon = chalk35.red(" \u{1F534}");
27327
+ else if (step.outcome === "skip") icon = chalk35.gray(" \u2500 ");
27328
+ else icon = chalk35.gray(" \u25CB ");
26798
27329
  const name = step.name.padEnd(18);
26799
- const nameStr = isFinal ? chalk34.white.bold(name) : chalk34.white(name);
26800
- const detail = isFinal ? chalk34.white(step.detail) : chalk34.gray(step.detail);
26801
- const arrow = isFinal ? chalk34.yellow(" \u2190 STOP") : "";
27330
+ const nameStr = isFinal ? chalk35.white.bold(name) : chalk35.white(name);
27331
+ const detail = isFinal ? chalk35.white(step.detail) : chalk35.gray(step.detail);
27332
+ const arrow = isFinal ? chalk35.yellow(" \u2190 STOP") : "";
26802
27333
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
26803
27334
  }
26804
27335
  console.log("");
26805
27336
  if (result.decision === "allow") {
26806
- console.log(chalk34.green.bold(" Decision: \u2705 ALLOW") + chalk34.gray(" \u2014 no approval needed"));
27337
+ console.log(chalk35.green.bold(" Decision: \u2705 ALLOW") + chalk35.gray(" \u2014 no approval needed"));
26807
27338
  } else {
26808
27339
  console.log(
26809
- chalk34.red.bold(" Decision: \u{1F534} REVIEW") + chalk34.gray(" \u2014 human approval required")
27340
+ chalk35.red.bold(" Decision: \u{1F534} REVIEW") + chalk35.gray(" \u2014 human approval required")
26810
27341
  );
26811
27342
  if (result.blockedByLabel) {
26812
- console.log(chalk34.gray(` Reason: ${result.blockedByLabel}`));
27343
+ console.log(chalk35.gray(` Reason: ${result.blockedByLabel}`));
26813
27344
  }
26814
27345
  }
26815
27346
  console.log("");
@@ -26824,18 +27355,18 @@ program.command("tail").description("Stream live agent activity to the terminal"
26824
27355
  try {
26825
27356
  await startTail2(options);
26826
27357
  } catch (err2) {
26827
- console.error(chalk34.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
27358
+ console.error(chalk35.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
26828
27359
  process.exit(1);
26829
27360
  }
26830
27361
  });
26831
27362
  program.command("monitor").description("Live interactive dashboard \u2014 activity feed, approvals, security signals").action(async () => {
26832
27363
  try {
26833
- const dashboardPath = path57.join(__dirname, "dashboard.mjs");
27364
+ const dashboardPath = path60.join(__dirname, "dashboard.mjs");
26834
27365
  const dynamicImport = new Function("id", "return import(id)");
26835
27366
  const mod = await dynamicImport(`file://${dashboardPath}`);
26836
27367
  await mod.startMonitor();
26837
27368
  } catch (err2) {
26838
- console.error(chalk34.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
27369
+ console.error(chalk35.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
26839
27370
  process.exit(1);
26840
27371
  }
26841
27372
  });
@@ -26868,14 +27399,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
26868
27399
  Run "node9 addto claude" to register it as the statusLine.`
26869
27400
  ).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
26870
27401
  if (subcommand === "debug") {
26871
- const flagFile = path57.join(os53.homedir(), ".node9", "hud-debug");
27402
+ const flagFile = path60.join(os54.homedir(), ".node9", "hud-debug");
26872
27403
  if (state === "on") {
26873
- fs58.mkdirSync(path57.dirname(flagFile), { recursive: true });
26874
- fs58.writeFileSync(flagFile, "");
27404
+ fs62.mkdirSync(path60.dirname(flagFile), { recursive: true });
27405
+ fs62.writeFileSync(flagFile, "");
26875
27406
  console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
26876
27407
  console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
26877
27408
  } else if (state === "off") {
26878
- if (fs58.existsSync(flagFile)) fs58.unlinkSync(flagFile);
27409
+ if (fs62.existsSync(flagFile)) fs62.unlinkSync(flagFile);
26879
27410
  console.log("HUD debug logging disabled.");
26880
27411
  } else {
26881
27412
  console.error("Usage: node9 hud debug on|off");
@@ -26890,7 +27421,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
26890
27421
  const ms = parseDuration(options.duration);
26891
27422
  if (ms === null) {
26892
27423
  console.error(
26893
- chalk34.red(`
27424
+ chalk35.red(`
26894
27425
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
26895
27426
  `)
26896
27427
  );
@@ -26898,20 +27429,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
26898
27429
  }
26899
27430
  pauseNode9(ms, options.duration);
26900
27431
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
26901
- console.log(chalk34.yellow(`
27432
+ console.log(chalk35.yellow(`
26902
27433
  \u23F8 Node9 paused until ${expiresAt}`));
26903
- console.log(chalk34.gray(` All tool calls will be allowed without review.`));
26904
- console.log(chalk34.gray(` Run "node9 resume" to re-enable early.
27434
+ console.log(chalk35.gray(` All tool calls will be allowed without review.`));
27435
+ console.log(chalk35.gray(` Run "node9 resume" to re-enable early.
26905
27436
  `));
26906
27437
  });
26907
27438
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
26908
27439
  const { paused } = checkPause();
26909
27440
  if (!paused) {
26910
- console.log(chalk34.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
27441
+ console.log(chalk35.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
26911
27442
  return;
26912
27443
  }
26913
27444
  resumeNode9();
26914
- console.log(chalk34.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
27445
+ console.log(chalk35.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
26915
27446
  });
26916
27447
  var HOOK_BASED_AGENTS = {
26917
27448
  claude: "claude",
@@ -26927,15 +27458,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
26927
27458
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
26928
27459
  const target = HOOK_BASED_AGENTS[firstArg2];
26929
27460
  console.error(
26930
- chalk34.yellow(`
27461
+ chalk35.yellow(`
26931
27462
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
26932
27463
  );
26933
- console.error(chalk34.white(`
27464
+ console.error(chalk35.white(`
26934
27465
  "${target}" uses its own hook system. Use:`));
26935
27466
  console.error(
26936
- chalk34.green(` node9 addto ${target} `) + chalk34.gray("# one-time setup")
27467
+ chalk35.green(` node9 addto ${target} `) + chalk35.gray("# one-time setup")
26937
27468
  );
26938
- console.error(chalk34.green(` ${target} `) + chalk34.gray("# run normally"));
27469
+ console.error(chalk35.green(` ${target} `) + chalk35.gray("# run normally"));
26939
27470
  process.exit(1);
26940
27471
  }
26941
27472
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -26952,7 +27483,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
26952
27483
  }
26953
27484
  );
26954
27485
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
26955
- console.error(chalk34.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
27486
+ console.error(chalk35.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
26956
27487
  const daemonReady = await autoStartDaemonAndWait();
26957
27488
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
26958
27489
  }
@@ -26965,12 +27496,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
26965
27496
  }
26966
27497
  if (!result.approved) {
26967
27498
  console.error(
26968
- chalk34.red(`
27499
+ chalk35.red(`
26969
27500
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
26970
27501
  );
26971
27502
  process.exit(1);
26972
27503
  }
26973
- console.error(chalk34.green("\n\u2705 Approved \u2014 running command...\n"));
27504
+ console.error(chalk35.green("\n\u2705 Approved \u2014 running command...\n"));
26974
27505
  await runProxy(fullCommand);
26975
27506
  } else {
26976
27507
  program.help();
@@ -26985,6 +27516,7 @@ registerAgentsCommand(program);
26985
27516
  registerScanCommand(program);
26986
27517
  registerPostureCommand(program);
26987
27518
  registerEgressCommand(program);
27519
+ registerSandboxCommand(program, version);
26988
27520
  registerSessionsCommand(program);
26989
27521
  registerSessionTaintCommand(program);
26990
27522
  registerDlpCommand(program);
@@ -26995,9 +27527,9 @@ if (process.argv[2] !== "daemon") {
26995
27527
  const isCheckHook = process.argv[2] === "check";
26996
27528
  if (isCheckHook) {
26997
27529
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
26998
- const logPath = path57.join(os53.homedir(), ".node9", "hook-debug.log");
27530
+ const logPath = path60.join(os54.homedir(), ".node9", "hook-debug.log");
26999
27531
  const msg = reason instanceof Error ? reason.message : String(reason);
27000
- fs58.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
27532
+ fs62.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
27001
27533
  `);
27002
27534
  }
27003
27535
  process.exit(0);