@node9/proxy 1.39.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -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 path62 = issue.path.length > 0 ? issue.path.join(".") : "root";
189
+ return ` \u2022 ${path62}: ${issue.message}`;
190
190
  });
191
191
  return {
192
192
  sanitized,
@@ -271,6 +271,11 @@ var init_config_schema = __esm({
271
271
  allowGlobalPause: z.boolean().optional(),
272
272
  auditHashArgs: z.boolean().optional(),
273
273
  agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
274
+ // Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
275
+ // inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
276
+ // node9's own approver (terminal/native/cloud). Unset → smart default
277
+ // (ask for ask-capable agents unless a cloud approver is configured).
278
+ reviewChannel: z.enum(["ask", "approver"]).optional(),
274
279
  cloudSyncIntervalHours: z.number().positive().optional(),
275
280
  // Outbox shipper (audit.log → SaaS batch ingest). enabled defaults
276
281
  // to true; set false to fall back to local-only auditing.
@@ -1258,9 +1263,9 @@ function matchesPattern(text, patterns) {
1258
1263
  const withoutDotSlash = text.replace(/^\.\//, "");
1259
1264
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1260
1265
  }
1261
- function getNestedValue(obj, path58) {
1266
+ function getNestedValue(obj, path62) {
1262
1267
  if (!obj || typeof obj !== "object") return null;
1263
- const segments = path58.split(".");
1268
+ const segments = path62.split(".");
1264
1269
  for (const seg of segments) {
1265
1270
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
1266
1271
  }
@@ -4178,6 +4183,7 @@ function getConfig(cwd) {
4178
4183
  if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
4179
4184
  mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
4180
4185
  if (s.environment !== void 0) mergedSettings.environment = s.environment;
4186
+ if (s.reviewChannel !== void 0) mergedSettings.reviewChannel = s.reviewChannel;
4181
4187
  if (s.cloudSyncIntervalHours !== void 0)
4182
4188
  mergedSettings.cloudSyncIntervalHours = s.cloudSyncIntervalHours;
4183
4189
  if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
@@ -6330,7 +6336,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
6330
6336
  tool: toolName,
6331
6337
  args,
6332
6338
  ts: actTs,
6333
- status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
6339
+ status: result.review ? "review" : result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
6334
6340
  label: result.blockedByLabel,
6335
6341
  ruleHit: result.ruleHit,
6336
6342
  observeWouldBlock: result.observeWouldBlock,
@@ -6693,6 +6699,16 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
6693
6699
  taintWarning
6694
6700
  );
6695
6701
  }
6702
+ const cloudEnforcedForDefer = approvers.cloud && !!creds?.apiKey;
6703
+ if (options?.deferReview && !taintWarning && !cloudEnforcedForDefer) {
6704
+ return {
6705
+ approved: false,
6706
+ review: true,
6707
+ reason: explainableLabel || "Node9 flagged this action for review.",
6708
+ ruleDescription: policyRuleDescription,
6709
+ blockedByLabel: explainableLabel
6710
+ };
6711
+ }
6696
6712
  let cloudRequestId = null;
6697
6713
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
6698
6714
  const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || void 0;
@@ -7504,6 +7520,18 @@ function removeNode9McpServer(servers) {
7504
7520
  function printDaemonTip() {
7505
7521
  console.log(chalk.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups."));
7506
7522
  }
7523
+ function printInlineAskNotice() {
7524
+ console.log(
7525
+ chalk.cyan(
7526
+ " \u{1F4AC} Review prompts appear inline in your agent (approve/deny in the chat) by default."
7527
+ )
7528
+ );
7529
+ console.log(
7530
+ chalk.gray(
7531
+ ' Prefer node9\u2019s own approver? Set "reviewChannel": "approver" in config, or add --no-ask to the hook.\n (Inline prompts are auto-disabled when a cloud approver is configured.)'
7532
+ )
7533
+ );
7534
+ }
7507
7535
  function fullPathCommand(subcommand) {
7508
7536
  if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
7509
7537
  const nodeExec = toForwardSlashes(process.execPath);
@@ -7828,6 +7856,7 @@ async function setupClaude() {
7828
7856
  console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
7829
7857
  console.log(chalk.gray(" Restart Claude Code for changes to take effect."));
7830
7858
  printDaemonTip();
7859
+ printInlineAskNotice();
7831
7860
  }
7832
7861
  }
7833
7862
  async function setupGemini() {
@@ -8224,6 +8253,7 @@ async function setupCopilot() {
8224
8253
  console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting GitHub Copilot CLI!"));
8225
8254
  console.log(chalk.gray(" Restart Copilot CLI for changes to take effect."));
8226
8255
  printDaemonTip();
8256
+ printInlineAskNotice();
8227
8257
  }
8228
8258
  function teardownCopilot() {
8229
8259
  const homeDir2 = os12.homedir();
@@ -15510,8 +15540,8 @@ function fileSignature(filePath) {
15510
15540
  const fd = fs27.openSync(filePath, "r");
15511
15541
  try {
15512
15542
  const buf = Buffer.alloc(512);
15513
- const read = fs27.readSync(fd, buf, 0, 512, 0);
15514
- const slice = buf.subarray(0, read);
15543
+ const read2 = fs27.readSync(fd, buf, 0, 512, 0);
15544
+ const slice = buf.subarray(0, read2);
15515
15545
  const nl = slice.indexOf(10);
15516
15546
  const firstLine = nl === -1 ? slice : slice.subarray(0, nl);
15517
15547
  return crypto5.createHash("sha256").update(firstLine).digest("hex").slice(0, 16);
@@ -15615,13 +15645,13 @@ async function shipOnce(deps = {}) {
15615
15645
  const toRead = Math.min(size - offset, MAX_CHUNK_BYTES);
15616
15646
  const buf = Buffer.alloc(toRead);
15617
15647
  const fd = fs27.openSync(auditLogPath, "r");
15618
- let read;
15648
+ let read2;
15619
15649
  try {
15620
- read = fs27.readSync(fd, buf, 0, toRead, offset);
15650
+ read2 = fs27.readSync(fd, buf, 0, toRead, offset);
15621
15651
  } finally {
15622
15652
  fs27.closeSync(fd);
15623
15653
  }
15624
- const { rows, consumed } = buildWireRows(buf.subarray(0, read));
15654
+ const { rows, consumed } = buildWireRows(buf.subarray(0, read2));
15625
15655
  if (consumed === 0) break;
15626
15656
  for (let i = 0; i < rows.length; i += MAX_BATCH) {
15627
15657
  const batch = rows.slice(i, i + MAX_BATCH);
@@ -17198,10 +17228,10 @@ __export(tail_exports, {
17198
17228
  startTail: () => startTail
17199
17229
  });
17200
17230
  import http3 from "http";
17201
- import chalk33 from "chalk";
17202
- import fs56 from "fs";
17203
- import os51 from "os";
17204
- import path55 from "path";
17231
+ import chalk34 from "chalk";
17232
+ import fs61 from "fs";
17233
+ import os53 from "os";
17234
+ import path59 from "path";
17205
17235
  import readline6 from "readline";
17206
17236
  import { spawn as spawn8 } from "child_process";
17207
17237
  function shortenPathSummary(s) {
@@ -17225,20 +17255,20 @@ function getModelContextLimit(model) {
17225
17255
  return 2e5;
17226
17256
  }
17227
17257
  function readSessionUsage() {
17228
- const projectsDir = path55.join(os51.homedir(), ".claude", "projects");
17229
- if (!fs56.existsSync(projectsDir)) return null;
17258
+ const projectsDir = path59.join(os53.homedir(), ".claude", "projects");
17259
+ if (!fs61.existsSync(projectsDir)) return null;
17230
17260
  let latestFile = null;
17231
17261
  let latestMtime = 0;
17232
17262
  try {
17233
- for (const dir of fs56.readdirSync(projectsDir)) {
17234
- const dirPath = path55.join(projectsDir, dir);
17263
+ for (const dir of fs61.readdirSync(projectsDir)) {
17264
+ const dirPath = path59.join(projectsDir, dir);
17235
17265
  try {
17236
- if (!fs56.statSync(dirPath).isDirectory()) continue;
17237
- for (const file of fs56.readdirSync(dirPath)) {
17266
+ if (!fs61.statSync(dirPath).isDirectory()) continue;
17267
+ for (const file of fs61.readdirSync(dirPath)) {
17238
17268
  if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
17239
- const filePath = path55.join(dirPath, file);
17269
+ const filePath = path59.join(dirPath, file);
17240
17270
  try {
17241
- const mtime = fs56.statSync(filePath).mtimeMs;
17271
+ const mtime = fs61.statSync(filePath).mtimeMs;
17242
17272
  if (mtime > latestMtime) {
17243
17273
  latestMtime = mtime;
17244
17274
  latestFile = filePath;
@@ -17253,7 +17283,7 @@ function readSessionUsage() {
17253
17283
  }
17254
17284
  if (!latestFile) return null;
17255
17285
  try {
17256
- const lines = fs56.readFileSync(latestFile, "utf-8").split("\n");
17286
+ const lines = fs61.readFileSync(latestFile, "utf-8").split("\n");
17257
17287
  let lastModel = "";
17258
17288
  let lastInput = 0;
17259
17289
  let lastOutput = 0;
@@ -17278,10 +17308,10 @@ function readSessionUsage() {
17278
17308
  }
17279
17309
  }
17280
17310
  function formatContextStat(stat) {
17281
- const pctColor = stat.fillPct >= 80 ? chalk33.red : stat.fillPct >= 50 ? chalk33.yellow : chalk33.cyan;
17311
+ const pctColor = stat.fillPct >= 80 ? chalk34.red : stat.fillPct >= 50 ? chalk34.yellow : chalk34.cyan;
17282
17312
  const k = (n) => `${Math.round(n / 1e3)}k`;
17283
17313
  const modelShort = stat.model.replace(/@.*$/, "").replace(/-\d{8}$/, "").replace(/^claude-/, "");
17284
- return chalk33.dim("ctx: ") + pctColor(`${stat.fillPct}%`) + chalk33.dim(
17314
+ return chalk34.dim("ctx: ") + pctColor(`${stat.fillPct}%`) + chalk34.dim(
17285
17315
  ` (${k(stat.inputTokens)}/${k(getModelContextLimit(stat.model))} out ${k(stat.outputTokens)} \xB7 ${modelShort})`
17286
17316
  );
17287
17317
  }
@@ -17304,32 +17334,32 @@ function agentLabel(agent, mcpServer, sessionId) {
17304
17334
  const tag = sessionTag(sessionId);
17305
17335
  const tagSuffix = tag ? `\xB7${tag}` : "";
17306
17336
  if (!agent || agent === "Terminal") {
17307
- return mcpServer ? chalk33.dim(`[\u2192 ${mcpServer}] `) : "";
17337
+ return mcpServer ? chalk34.dim(`[\u2192 ${mcpServer}] `) : "";
17308
17338
  }
17309
17339
  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}] `);
17340
+ if (!short) return mcpServer ? chalk34.dim(`[\u2192 ${mcpServer}] `) : "";
17341
+ return mcpServer ? chalk34.dim(`[${short}${tagSuffix} \u2192 ${mcpServer}] `) : chalk34.dim(`[${short}${tagSuffix}] `);
17312
17342
  }
17313
17343
  function formatBase(activity) {
17314
17344
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
17315
17345
  const icon = getIcon(activity.tool);
17316
17346
  const toolName = activity.tool.slice(0, 16).padEnd(16);
17317
- const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os51.homedir(), "~");
17347
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os53.homedir(), "~");
17318
17348
  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)}`;
17349
+ return `${chalk34.gray(time)} ${icon} ${agentLabel(activity.agent, activity.mcpServer, activity.sessionId)}${chalk34.white.bold(toolName)} ${chalk34.dim(argsPreview)}`;
17320
17350
  }
17321
17351
  function renderResult(activity, result) {
17322
17352
  const base = formatBase(activity);
17323
17353
  let status;
17324
17354
  if (result.status === "allow") {
17325
- status = chalk33.green("\u2713 ALLOW");
17355
+ status = chalk34.green("\u2713 ALLOW");
17326
17356
  } else if (result.status === "dlp") {
17327
- status = chalk33.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
17357
+ status = chalk34.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
17328
17358
  } else {
17329
- status = chalk33.red("\u2717 BLOCK");
17359
+ status = chalk34.red("\u2717 BLOCK");
17330
17360
  }
17331
17361
  const cost = result.costEstimate ?? activity.costEstimate;
17332
- const costSuffix = cost == null ? "" : chalk33.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
17362
+ const costSuffix = cost == null ? "" : chalk34.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
17333
17363
  if (process.stdout.isTTY) {
17334
17364
  if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
17335
17365
  readline6.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
@@ -17346,19 +17376,19 @@ function renderResult(activity, result) {
17346
17376
  }
17347
17377
  function renderPending(activity) {
17348
17378
  if (!process.stdout.isTTY) return;
17349
- const line = `${formatBase(activity)} ${chalk33.yellow("\u25CF \u2026")}`;
17379
+ const line = `${formatBase(activity)} ${chalk34.yellow("\u25CF \u2026")}`;
17350
17380
  pendingShownForId = activity.id;
17351
17381
  pendingWrappedLines = wrappedLineCount(line);
17352
17382
  process.stdout.write(`${line}\r`);
17353
17383
  }
17354
17384
  async function ensureDaemon() {
17355
17385
  let pidPort = null;
17356
- if (fs56.existsSync(PID_FILE)) {
17386
+ if (fs61.existsSync(PID_FILE)) {
17357
17387
  try {
17358
- const { port } = JSON.parse(fs56.readFileSync(PID_FILE, "utf-8"));
17388
+ const { port } = JSON.parse(fs61.readFileSync(PID_FILE, "utf-8"));
17359
17389
  pidPort = port;
17360
17390
  } catch {
17361
- console.error(chalk33.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
17391
+ console.error(chalk34.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
17362
17392
  }
17363
17393
  }
17364
17394
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -17369,7 +17399,7 @@ async function ensureDaemon() {
17369
17399
  if (res.ok) return checkPort;
17370
17400
  } catch {
17371
17401
  }
17372
- console.log(chalk33.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
17402
+ console.log(chalk34.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
17373
17403
  const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
17374
17404
  detached: true,
17375
17405
  stdio: "ignore",
@@ -17386,7 +17416,7 @@ async function ensureDaemon() {
17386
17416
  } catch {
17387
17417
  }
17388
17418
  }
17389
- console.error(chalk33.red("\u274C Daemon failed to start. Try: node9 daemon start"));
17419
+ console.error(chalk34.red("\u274C Daemon failed to start. Try: node9 daemon start"));
17390
17420
  process.exit(1);
17391
17421
  }
17392
17422
  function postDecisionHttp(id, decision, authToken, port, opts) {
@@ -17455,7 +17485,7 @@ function buildCardLines(req, localCount = 0) {
17455
17485
  const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
17456
17486
  const rawDesc = req.riskMetadata?.ruleDescription ?? "";
17457
17487
  const description = rawDesc ? cleanReason(rawDesc) : "";
17458
- const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${chalk33.dim(`(${req.agent})`)}` : "";
17488
+ const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${chalk34.dim(`(${req.agent})`)}` : "";
17459
17489
  const lines = [
17460
17490
  ``,
17461
17491
  `${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
@@ -17511,9 +17541,9 @@ function buildRecoveryCardLines(req) {
17511
17541
  ];
17512
17542
  }
17513
17543
  function readApproversFromDisk() {
17514
- const configPath2 = path55.join(os51.homedir(), ".node9", "config.json");
17544
+ const configPath2 = path59.join(os53.homedir(), ".node9", "config.json");
17515
17545
  try {
17516
- const raw = JSON.parse(fs56.readFileSync(configPath2, "utf-8"));
17546
+ const raw = JSON.parse(fs61.readFileSync(configPath2, "utf-8"));
17517
17547
  const settings = raw.settings ?? {};
17518
17548
  return settings.approvers ?? {};
17519
17549
  } catch {
@@ -17524,20 +17554,20 @@ function approverStatusLine() {
17524
17554
  const a = readApproversFromDisk();
17525
17555
  const fmt = (label2, key) => {
17526
17556
  const on = a[key] !== false;
17527
- return `[${key[0]}]${label2.slice(1)} ${on ? chalk33.green("\u2713") : chalk33.dim("\u2717")}`;
17557
+ return `[${key[0]}]${label2.slice(1)} ${on ? chalk34.green("\u2713") : chalk34.dim("\u2717")}`;
17528
17558
  };
17529
17559
  return `${fmt("native", "native")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
17530
17560
  }
17531
17561
  function toggleApprover(channel) {
17532
- const configPath2 = path55.join(os51.homedir(), ".node9", "config.json");
17562
+ const configPath2 = path59.join(os53.homedir(), ".node9", "config.json");
17533
17563
  try {
17534
- const raw = JSON.parse(fs56.readFileSync(configPath2, "utf-8"));
17564
+ const raw = JSON.parse(fs61.readFileSync(configPath2, "utf-8"));
17535
17565
  const settings = raw.settings ?? {};
17536
17566
  const approvers = settings.approvers ?? {};
17537
17567
  approvers[channel] = approvers[channel] === false;
17538
17568
  settings.approvers = approvers;
17539
17569
  raw.settings = settings;
17540
- fs56.writeFileSync(configPath2, JSON.stringify(raw, null, 2) + "\n");
17570
+ fs61.writeFileSync(configPath2, JSON.stringify(raw, null, 2) + "\n");
17541
17571
  } catch (err2) {
17542
17572
  process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
17543
17573
  `);
@@ -17569,7 +17599,7 @@ async function startTail(options = {}) {
17569
17599
  req2.end();
17570
17600
  });
17571
17601
  if (result.ok) {
17572
- console.log(chalk33.green("\u2713 Flight Recorder buffer cleared."));
17602
+ console.log(chalk34.green("\u2713 Flight Recorder buffer cleared."));
17573
17603
  } else if (result.code === "ECONNREFUSED") {
17574
17604
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
17575
17605
  } else if (result.code === "ETIMEDOUT") {
@@ -17615,7 +17645,7 @@ async function startTail(options = {}) {
17615
17645
  const channel = name === "n" ? "native" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
17616
17646
  if (channel) {
17617
17647
  toggleApprover(channel);
17618
- console.log(chalk33.dim(` Approvers: ${approverStatusLine()}`));
17648
+ console.log(chalk34.dim(` Approvers: ${approverStatusLine()}`));
17619
17649
  }
17620
17650
  };
17621
17651
  process.stdin.on("keypress", idleKeypressHandler);
@@ -17681,7 +17711,7 @@ async function startTail(options = {}) {
17681
17711
  localAllowCounts.get(req2.toolName) ?? 0
17682
17712
  )
17683
17713
  );
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");
17714
+ 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
17715
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
17686
17716
  for (const line of stampedLines) process.stdout.write(line + "\n");
17687
17717
  process.stdout.write(SHOW_CURSOR);
@@ -17709,8 +17739,8 @@ async function startTail(options = {}) {
17709
17739
  }
17710
17740
  postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).catch((err2) => {
17711
17741
  try {
17712
- fs56.appendFileSync(
17713
- path55.join(os51.homedir(), ".node9", "hook-debug.log"),
17742
+ fs61.appendFileSync(
17743
+ path59.join(os53.homedir(), ".node9", "hook-debug.log"),
17714
17744
  `[tail] POST /decision failed: ${String(err2)}
17715
17745
  `
17716
17746
  );
@@ -17732,7 +17762,7 @@ async function startTail(options = {}) {
17732
17762
  );
17733
17763
  const stampedLines = buildCardLines(req2, priorCount);
17734
17764
  if (externalDecision) {
17735
- const source = externalDecision === "allow" ? chalk33.green("\u2713 ALLOWED") : chalk33.red("\u2717 DENIED");
17765
+ const source = externalDecision === "allow" ? chalk34.green("\u2713 ALLOWED") : chalk34.red("\u2717 DENIED");
17736
17766
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
17737
17767
  }
17738
17768
  for (const line of stampedLines) process.stdout.write(line + "\n");
@@ -17774,31 +17804,31 @@ async function startTail(options = {}) {
17774
17804
  };
17775
17805
  process.stdin.on("keypress", onKeypress);
17776
17806
  }
17777
- const auditLog = path55.join(os51.homedir(), ".node9", "audit.log");
17807
+ const auditLog = path59.join(os53.homedir(), ".node9", "audit.log");
17778
17808
  try {
17779
- const unackedDlp = fs56.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
17809
+ const unackedDlp = fs61.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
17780
17810
  if (unackedDlp > 0) {
17781
17811
  console.log("");
17782
17812
  console.log(
17783
- chalk33.bgRed.white.bold(
17813
+ chalk34.bgRed.white.bold(
17784
17814
  ` \u26A0\uFE0F DLP ALERT: ${unackedDlp} secret${unackedDlp !== 1 ? "s" : ""} found in Claude response text \u2014 run: node9 dlp `
17785
17815
  )
17786
17816
  );
17787
17817
  }
17788
17818
  } catch {
17789
17819
  }
17790
- console.log(chalk33.cyan.bold(`
17820
+ console.log(chalk34.cyan.bold(`
17791
17821
  \u{1F6F0}\uFE0F Node9 tail`));
17792
17822
  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`));
17823
+ console.log(chalk34.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
17824
+ console.log(chalk34.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
17795
17825
  }
17796
17826
  const ctxStat = readSessionUsage();
17797
17827
  if (ctxStat) console.log(" " + formatContextStat(ctxStat));
17798
17828
  if (options.history) {
17799
- console.log(chalk33.dim("Showing history + live events.\n"));
17829
+ console.log(chalk34.dim("Showing history + live events.\n"));
17800
17830
  } else {
17801
- console.log(chalk33.dim("Showing live events only. Use --history to include past.\n"));
17831
+ console.log(chalk34.dim("Showing live events only. Use --history to include past.\n"));
17802
17832
  }
17803
17833
  process.on("SIGINT", () => {
17804
17834
  exitIdleMode();
@@ -17808,7 +17838,7 @@ async function startTail(options = {}) {
17808
17838
  readline6.clearLine(process.stdout, 0);
17809
17839
  readline6.cursorTo(process.stdout, 0);
17810
17840
  }
17811
- console.log(chalk33.dim("\n\u{1F6F0}\uFE0F Disconnected."));
17841
+ console.log(chalk34.dim("\n\u{1F6F0}\uFE0F Disconnected."));
17812
17842
  process.exit(0);
17813
17843
  });
17814
17844
  const STALL_THRESHOLD_MS = 6e4;
@@ -17816,11 +17846,11 @@ async function startTail(options = {}) {
17816
17846
  if (stallWarned) return;
17817
17847
  if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
17818
17848
  try {
17819
- const auditMtime = fs56.statSync(auditLog).mtimeMs;
17849
+ const auditMtime = fs61.statSync(auditLog).mtimeMs;
17820
17850
  if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
17821
17851
  console.log("");
17822
17852
  console.log(
17823
- chalk33.yellow(
17853
+ chalk34.yellow(
17824
17854
  "\u26A0\uFE0F Tail appears stalled \u2014 hooks are firing but no events are arriving. Try: node9 daemon restart"
17825
17855
  )
17826
17856
  );
@@ -17837,7 +17867,7 @@ async function startTail(options = {}) {
17837
17867
  },
17838
17868
  (res) => {
17839
17869
  if (res.statusCode !== 200) {
17840
- console.error(chalk33.red(`Failed to connect: HTTP ${res.statusCode}`));
17870
+ console.error(chalk34.red(`Failed to connect: HTTP ${res.statusCode}`));
17841
17871
  process.exit(1);
17842
17872
  }
17843
17873
  if (canApprove) enterIdleMode();
@@ -17868,7 +17898,7 @@ async function startTail(options = {}) {
17868
17898
  readline6.clearLine(process.stdout, 0);
17869
17899
  readline6.cursorTo(process.stdout, 0);
17870
17900
  }
17871
- console.log(chalk33.red("\n\u274C Daemon disconnected."));
17901
+ console.log(chalk34.red("\n\u274C Daemon disconnected."));
17872
17902
  process.exit(1);
17873
17903
  });
17874
17904
  }
@@ -17881,7 +17911,7 @@ async function startTail(options = {}) {
17881
17911
  const parsed = JSON.parse(rawData);
17882
17912
  const msg = parsed.message ?? "Flight recorder is down \u2014 run: node9 daemon restart";
17883
17913
  console.log("");
17884
- console.log(chalk33.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
17914
+ console.log(chalk34.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
17885
17915
  } catch {
17886
17916
  }
17887
17917
  return;
@@ -17966,9 +17996,9 @@ async function startTail(options = {}) {
17966
17996
  const rawSummary = data.argsSummary ?? data.tool;
17967
17997
  const summary = shortenPathSummary(rawSummary);
17968
17998
  const fileCount = data.fileCount ?? 0;
17969
- const files = fileCount > 0 ? chalk33.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
17999
+ const files = fileCount > 0 ? chalk34.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
17970
18000
  process.stdout.write(
17971
- `${chalk33.dim(time)} ${chalk33.cyan("\u{1F4F8} snapshot")} ${chalk33.dim(hash)} ${summary}${files}
18001
+ `${chalk34.dim(time)} ${chalk34.cyan("\u{1F4F8} snapshot")} ${chalk34.dim(hash)} ${summary}${files}
17972
18002
  `
17973
18003
  );
17974
18004
  return;
@@ -17985,18 +18015,18 @@ async function startTail(options = {}) {
17985
18015
  if (event === "execution-result") {
17986
18016
  const exec = data;
17987
18017
  const time = new Date(Date.now()).toLocaleTimeString([], { hour12: false });
17988
- const arrow = exec.isError ? chalk33.red(" \u21B3 \u2717") : chalk33.green(" \u21B3 \u2713");
18018
+ const arrow = exec.isError ? chalk34.red(" \u21B3 \u2717") : chalk34.green(" \u21B3 \u2713");
17989
18019
  const label2 = agentLabel(exec.agent, exec.mcpServer);
17990
18020
  const tool = (exec.tool ?? "").slice(0, 16);
17991
- const duration = typeof exec.durationMs === "number" ? chalk33.dim(` (${exec.durationMs}ms)`) : "";
18021
+ const duration = typeof exec.durationMs === "number" ? chalk34.dim(` (${exec.durationMs}ms)`) : "";
17992
18022
  console.log(
17993
- `${chalk33.gray(time)} ${arrow} ${label2}${chalk33.dim(tool)}${chalk33.dim(" completed")}${duration}`
18023
+ `${chalk34.gray(time)} ${arrow} ${label2}${chalk34.dim(tool)}${chalk34.dim(" completed")}${duration}`
17994
18024
  );
17995
18025
  }
17996
18026
  }
17997
18027
  req.on("error", (err2) => {
17998
18028
  const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
17999
- console.error(chalk33.red(`
18029
+ console.error(chalk34.red(`
18000
18030
  \u274C ${msg}`));
18001
18031
  process.exit(1);
18002
18032
  });
@@ -18007,7 +18037,7 @@ var init_tail = __esm({
18007
18037
  "use strict";
18008
18038
  init_daemon2();
18009
18039
  init_daemon();
18010
- PID_FILE = path55.join(os51.homedir(), ".node9", "daemon.pid");
18040
+ PID_FILE = path59.join(os53.homedir(), ".node9", "daemon.pid");
18011
18041
  ICONS = {
18012
18042
  bash: "\u{1F4BB}",
18013
18043
  shell: "\u{1F4BB}",
@@ -18055,9 +18085,9 @@ __export(hud_exports, {
18055
18085
  main: () => main,
18056
18086
  renderEnvironmentLine: () => renderEnvironmentLine
18057
18087
  });
18058
- import fs57 from "fs";
18059
- import path56 from "path";
18060
- import os52 from "os";
18088
+ import fs62 from "fs";
18089
+ import path60 from "path";
18090
+ import os54 from "os";
18061
18091
  import http4 from "http";
18062
18092
  async function readStdin() {
18063
18093
  const chunks = [];
@@ -18133,9 +18163,9 @@ function formatTimeLeft(resetsAt) {
18133
18163
  return ` (${m}m left)`;
18134
18164
  }
18135
18165
  function safeReadJson(filePath) {
18136
- if (!fs57.existsSync(filePath)) return null;
18166
+ if (!fs62.existsSync(filePath)) return null;
18137
18167
  try {
18138
- return JSON.parse(fs57.readFileSync(filePath, "utf-8"));
18168
+ return JSON.parse(fs62.readFileSync(filePath, "utf-8"));
18139
18169
  } catch {
18140
18170
  return null;
18141
18171
  }
@@ -18156,12 +18186,12 @@ function countHooksInFile(filePath) {
18156
18186
  return Object.keys(cfg.hooks).length;
18157
18187
  }
18158
18188
  function countRulesInDir(rulesDir) {
18159
- if (!fs57.existsSync(rulesDir)) return 0;
18189
+ if (!fs62.existsSync(rulesDir)) return 0;
18160
18190
  let count = 0;
18161
18191
  try {
18162
- for (const entry of fs57.readdirSync(rulesDir, { withFileTypes: true })) {
18192
+ for (const entry of fs62.readdirSync(rulesDir, { withFileTypes: true })) {
18163
18193
  if (entry.isDirectory()) {
18164
- count += countRulesInDir(path56.join(rulesDir, entry.name));
18194
+ count += countRulesInDir(path60.join(rulesDir, entry.name));
18165
18195
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
18166
18196
  count++;
18167
18197
  }
@@ -18172,46 +18202,46 @@ function countRulesInDir(rulesDir) {
18172
18202
  }
18173
18203
  function isSamePath(a, b) {
18174
18204
  try {
18175
- return path56.resolve(a) === path56.resolve(b);
18205
+ return path60.resolve(a) === path60.resolve(b);
18176
18206
  } catch {
18177
18207
  return false;
18178
18208
  }
18179
18209
  }
18180
18210
  function countConfigs(cwd) {
18181
- const homeDir2 = os52.homedir();
18182
- const claudeDir = path56.join(homeDir2, ".claude");
18211
+ const homeDir2 = os54.homedir();
18212
+ const claudeDir = path60.join(homeDir2, ".claude");
18183
18213
  let claudeMdCount = 0;
18184
18214
  let rulesCount = 0;
18185
18215
  let hooksCount = 0;
18186
18216
  const userMcpServers = /* @__PURE__ */ new Set();
18187
18217
  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");
18218
+ if (fs62.existsSync(path60.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
18219
+ rulesCount += countRulesInDir(path60.join(claudeDir, "rules"));
18220
+ const userSettings = path60.join(claudeDir, "settings.json");
18191
18221
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
18192
18222
  hooksCount += countHooksInFile(userSettings);
18193
- const userClaudeJson = path56.join(homeDir2, ".claude.json");
18223
+ const userClaudeJson = path60.join(homeDir2, ".claude.json");
18194
18224
  for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
18195
18225
  for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
18196
18226
  userMcpServers.delete(name);
18197
18227
  }
18198
18228
  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");
18229
+ if (fs62.existsSync(path60.join(cwd, "CLAUDE.md"))) claudeMdCount++;
18230
+ if (fs62.existsSync(path60.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
18231
+ const projectClaudeDir = path60.join(cwd, ".claude");
18202
18232
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
18203
18233
  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");
18234
+ if (fs62.existsSync(path60.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
18235
+ rulesCount += countRulesInDir(path60.join(projectClaudeDir, "rules"));
18236
+ const projSettings = path60.join(projectClaudeDir, "settings.json");
18207
18237
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
18208
18238
  hooksCount += countHooksInFile(projSettings);
18209
18239
  }
18210
- if (fs57.existsSync(path56.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
18211
- const localSettings = path56.join(projectClaudeDir, "settings.local.json");
18240
+ if (fs62.existsSync(path60.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
18241
+ const localSettings = path60.join(projectClaudeDir, "settings.local.json");
18212
18242
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
18213
18243
  hooksCount += countHooksInFile(localSettings);
18214
- const mcpJsonServers = getMcpServerNames(path56.join(cwd, ".mcp.json"));
18244
+ const mcpJsonServers = getMcpServerNames(path60.join(cwd, ".mcp.json"));
18215
18245
  const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
18216
18246
  for (const name of disabledMcpJson) mcpJsonServers.delete(name);
18217
18247
  for (const name of mcpJsonServers) projectMcpServers.add(name);
@@ -18244,12 +18274,12 @@ function readActiveShieldsHud() {
18244
18274
  return shieldsCache.value;
18245
18275
  }
18246
18276
  try {
18247
- const shieldsPath = path56.join(os52.homedir(), ".node9", "shields.json");
18248
- if (!fs57.existsSync(shieldsPath)) {
18277
+ const shieldsPath = path60.join(os54.homedir(), ".node9", "shields.json");
18278
+ if (!fs62.existsSync(shieldsPath)) {
18249
18279
  shieldsCache = { value: [], ts: now };
18250
18280
  return [];
18251
18281
  }
18252
- const parsed = JSON.parse(fs57.readFileSync(shieldsPath, "utf-8"));
18282
+ const parsed = JSON.parse(fs62.readFileSync(shieldsPath, "utf-8"));
18253
18283
  if (!Array.isArray(parsed.active)) {
18254
18284
  shieldsCache = { value: [], ts: now };
18255
18285
  return [];
@@ -18351,17 +18381,17 @@ function renderContextLine(stdin) {
18351
18381
  async function main() {
18352
18382
  try {
18353
18383
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
18354
- if (fs57.existsSync(path56.join(os52.homedir(), ".node9", "hud-debug"))) {
18384
+ if (fs62.existsSync(path60.join(os54.homedir(), ".node9", "hud-debug"))) {
18355
18385
  try {
18356
- const logPath = path56.join(os52.homedir(), ".node9", "hud-debug.log");
18386
+ const logPath = path60.join(os54.homedir(), ".node9", "hud-debug.log");
18357
18387
  const MAX_LOG_SIZE = 10 * 1024 * 1024;
18358
18388
  let size = 0;
18359
18389
  try {
18360
- size = fs57.statSync(logPath).size;
18390
+ size = fs62.statSync(logPath).size;
18361
18391
  } catch {
18362
18392
  }
18363
18393
  if (size < MAX_LOG_SIZE) {
18364
- fs57.appendFileSync(
18394
+ fs62.appendFileSync(
18365
18395
  logPath,
18366
18396
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
18367
18397
  );
@@ -18382,11 +18412,11 @@ async function main() {
18382
18412
  try {
18383
18413
  const cwd = stdin.cwd ?? process.cwd();
18384
18414
  for (const configPath2 of [
18385
- path56.join(cwd, "node9.config.json"),
18386
- path56.join(os52.homedir(), ".node9", "config.json")
18415
+ path60.join(cwd, "node9.config.json"),
18416
+ path60.join(os54.homedir(), ".node9", "config.json")
18387
18417
  ]) {
18388
- if (!fs57.existsSync(configPath2)) continue;
18389
- const cfg = JSON.parse(fs57.readFileSync(configPath2, "utf-8"));
18418
+ if (!fs62.existsSync(configPath2)) continue;
18419
+ const cfg = JSON.parse(fs62.readFileSync(configPath2, "utf-8"));
18390
18420
  const hud = cfg.settings?.hud;
18391
18421
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
18392
18422
  }
@@ -18432,10 +18462,10 @@ init_core();
18432
18462
  init_setup();
18433
18463
  init_daemon2();
18434
18464
  import { Command } from "commander";
18435
- import chalk34 from "chalk";
18436
- import fs58 from "fs";
18437
- import path57 from "path";
18438
- import os53 from "os";
18465
+ import chalk35 from "chalk";
18466
+ import fs63 from "fs";
18467
+ import path61 from "path";
18468
+ import os55 from "os";
18439
18469
  import { spawn as spawn9 } from "child_process";
18440
18470
  import { confirm as confirm2 } from "@inquirer/prompts";
18441
18471
 
@@ -18530,6 +18560,10 @@ INSTRUCTIONS:
18530
18560
  - Do NOT retry this exact command or attempt to bypass the rule.${recovery}
18531
18561
  - Inform the user which security rule was triggered and ask how to proceed.`;
18532
18562
  }
18563
+ function buildReviewMessage(blockedByLabel, ruleDescription) {
18564
+ const why = ruleDescription || blockedByLabel || "this action needs your review";
18565
+ return `Node9 flagged this for your review: ${why}. Approve to proceed, or deny to cancel.`;
18566
+ }
18533
18567
 
18534
18568
  // src/proxy/index.ts
18535
18569
  function sanitize(value) {
@@ -18660,10 +18694,10 @@ init_daemon();
18660
18694
  init_config();
18661
18695
  init_policy();
18662
18696
  import chalk9 from "chalk";
18663
- import fs36 from "fs";
18697
+ import fs37 from "fs";
18664
18698
  import { spawn as spawn5 } from "child_process";
18665
- import path37 from "path";
18666
- import os32 from "os";
18699
+ import path38 from "path";
18700
+ import os33 from "os";
18667
18701
 
18668
18702
  // src/undo.ts
18669
18703
  import { spawnSync as spawnSync3, spawn as spawn4 } from "child_process";
@@ -19201,6 +19235,78 @@ function resolveUserSkillRoot(entry, cwd) {
19201
19235
  // src/cli/commands/check.ts
19202
19236
  init_dlp();
19203
19237
  init_audit();
19238
+
19239
+ // src/review-pending.ts
19240
+ init_hasher();
19241
+ import fs36 from "fs";
19242
+ import os32 from "os";
19243
+ import path37 from "path";
19244
+ function storePath() {
19245
+ return process.env.NODE9_PENDING_STORE || path37.join(os32.homedir(), ".node9", "pending-reviews.json");
19246
+ }
19247
+ var TTL_MS2 = 6 * 60 * 60 * 1e3;
19248
+ var MAX_ENTRIES = 500;
19249
+ function reviewCorrelationKey(payload) {
19250
+ if (typeof payload.tool_use_id === "string" && payload.tool_use_id) {
19251
+ return `tuid:${payload.tool_use_id}`;
19252
+ }
19253
+ const sid = payload.session_id ?? payload.conversationId;
19254
+ const tool = payload.tool_name;
19255
+ if (typeof sid === "string" && sid && typeof tool === "string" && tool) {
19256
+ return `h:${sid}|${tool}|${hashArgs(payload.tool_input)}`;
19257
+ }
19258
+ return null;
19259
+ }
19260
+ function read() {
19261
+ try {
19262
+ const parsed = JSON.parse(fs36.readFileSync(storePath(), "utf-8"));
19263
+ if (parsed && Array.isArray(parsed.entries)) return parsed;
19264
+ } catch {
19265
+ }
19266
+ return { entries: [] };
19267
+ }
19268
+ function write(store) {
19269
+ try {
19270
+ const p = storePath();
19271
+ const dir = path37.dirname(p);
19272
+ if (!fs36.existsSync(dir)) fs36.mkdirSync(dir, { recursive: true });
19273
+ const tmp = `${p}.${process.pid}.tmp`;
19274
+ fs36.writeFileSync(tmp, JSON.stringify(store));
19275
+ fs36.renameSync(tmp, p);
19276
+ } catch {
19277
+ }
19278
+ }
19279
+ function prune(entries, now) {
19280
+ const fresh = entries.filter((e) => now - e.ts < TTL_MS2);
19281
+ return fresh.length > MAX_ENTRIES ? fresh.slice(fresh.length - MAX_ENTRIES) : fresh;
19282
+ }
19283
+ function recordPendingReview(entry) {
19284
+ try {
19285
+ const store = read();
19286
+ store.entries = prune(store.entries, entry.ts);
19287
+ store.entries.push(entry);
19288
+ write(store);
19289
+ } catch {
19290
+ }
19291
+ }
19292
+ function resolvePendingReview(key, now = Date.now()) {
19293
+ try {
19294
+ const store = read();
19295
+ const idx = store.entries.findIndex((e) => e.key === key);
19296
+ if (idx === -1) {
19297
+ const pruned = prune(store.entries, now);
19298
+ if (pruned.length !== store.entries.length) write({ entries: pruned });
19299
+ return null;
19300
+ }
19301
+ const [match] = store.entries.splice(idx, 1);
19302
+ write({ entries: prune(store.entries, now) });
19303
+ return match;
19304
+ } catch {
19305
+ return null;
19306
+ }
19307
+ }
19308
+
19309
+ // src/cli/commands/check.ts
19204
19310
  init_hook_payload();
19205
19311
  function sanitize2(value) {
19206
19312
  return value.replace(/[\x00-\x1F\x7F]/g, "");
@@ -19249,11 +19355,26 @@ function detectAiAgent(payload) {
19249
19355
  }
19250
19356
  return "Terminal";
19251
19357
  }
19358
+ function agentSupportsAsk(agent) {
19359
+ return agent === "Claude Code" || agent === "GitHub Copilot";
19360
+ }
19361
+ function resolveAskMode(agent, opts, config) {
19362
+ if (!agentSupportsAsk(agent)) return false;
19363
+ if (config.settings.approvers.cloud === true) return false;
19364
+ if (opts.ask === true) return true;
19365
+ if (opts.ask === false) return false;
19366
+ if (config.settings.reviewChannel === "ask") return true;
19367
+ if (config.settings.reviewChannel === "approver") return false;
19368
+ return true;
19369
+ }
19252
19370
  function registerCheckCommand(program2) {
19253
19371
  program2.command("check", { hidden: true }).description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").option(
19254
19372
  "--agent <name>",
19255
19373
  "Agent identity override, set by node9-authored hook registrations (e.g. antigravity)"
19256
- ).action(async (data, opts) => {
19374
+ ).option(
19375
+ "--ask",
19376
+ "Route review verdicts to the agent\u2019s native inline approve/deny prompt (Claude Code / GitHub Copilot only)"
19377
+ ).option("--no-ask", "Force node9\u2019s own approver for review verdicts (override default-on)").action(async (data, opts) => {
19257
19378
  const agentOverride = agentLabelFromFlag(opts?.agent);
19258
19379
  const processPayload = async (raw) => {
19259
19380
  try {
@@ -19264,9 +19385,9 @@ function registerCheckCommand(program2) {
19264
19385
  } catch (err2) {
19265
19386
  const tempConfig = getConfig();
19266
19387
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
19267
- const logPath = path37.join(os32.homedir(), ".node9", "hook-debug.log");
19388
+ const logPath = path38.join(os33.homedir(), ".node9", "hook-debug.log");
19268
19389
  const errMsg = err2 instanceof Error ? err2.message : String(err2);
19269
- fs36.appendFileSync(
19390
+ fs37.appendFileSync(
19270
19391
  logPath,
19271
19392
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
19272
19393
  RAW: ${raw}
@@ -19279,14 +19400,14 @@ RAW: ${raw}
19279
19400
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
19280
19401
  if (process.env.NODE9_DEBUG === "1") {
19281
19402
  try {
19282
- const logPath = path37.join(os32.homedir(), ".node9", "hook-debug.log");
19283
- if (!fs36.existsSync(path37.dirname(logPath)))
19284
- fs36.mkdirSync(path37.dirname(logPath), { recursive: true });
19403
+ const logPath = path38.join(os33.homedir(), ".node9", "hook-debug.log");
19404
+ if (!fs37.existsSync(path38.dirname(logPath)))
19405
+ fs37.mkdirSync(path38.dirname(logPath), { recursive: true });
19285
19406
  const sanitized = JSON.stringify({
19286
19407
  ...payload,
19287
19408
  prompt: `<redacted, ${prompt.length} bytes>`
19288
19409
  });
19289
- fs36.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${sanitized}
19410
+ fs37.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${sanitized}
19290
19411
  `);
19291
19412
  } catch {
19292
19413
  }
@@ -19306,8 +19427,8 @@ RAW: ${raw}
19306
19427
  );
19307
19428
  const reason = `\u{1F6A8} Node9 DLP: ${dlpMatch.patternName} detected in prompt (${dlpMatch.redactedSample}). Prompt was not submitted \u2014 remove the credential and try again.`;
19308
19429
  try {
19309
- const ttyFd = fs36.openSync("/dev/tty", "w");
19310
- fs36.writeSync(
19430
+ const ttyFd = fs37.openSync("/dev/tty", "w");
19431
+ fs37.writeSync(
19311
19432
  ttyFd,
19312
19433
  chalk9.bgRed.white.bold(`
19313
19434
  \u{1F6A8} NODE9 DLP \u2014 PROMPT BLOCKED
@@ -19317,7 +19438,7 @@ RAW: ${raw}
19317
19438
 
19318
19439
  `)
19319
19440
  );
19320
- fs36.closeSync(ttyFd);
19441
+ fs37.closeSync(ttyFd);
19321
19442
  } catch {
19322
19443
  }
19323
19444
  const isCodex = agent2 === "Codex";
@@ -19336,16 +19457,16 @@ RAW: ${raw}
19336
19457
  process.exit(2);
19337
19458
  }
19338
19459
  const payloadCwd = typeof payload.cwd === "string" ? payload.cwd : Array.isArray(payload.workspacePaths) && typeof payload.workspacePaths[0] === "string" ? payload.workspacePaths[0] : void 0;
19339
- const safeCwdForConfig = typeof payloadCwd === "string" && path37.isAbsolute(payloadCwd) ? payloadCwd : void 0;
19460
+ const safeCwdForConfig = typeof payloadCwd === "string" && path38.isAbsolute(payloadCwd) ? payloadCwd : void 0;
19340
19461
  const config = getConfig(safeCwdForConfig);
19341
19462
  if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
19342
19463
  try {
19343
19464
  const scriptPath = process.argv[1];
19344
- if (typeof scriptPath !== "string" || !path37.isAbsolute(scriptPath))
19465
+ if (typeof scriptPath !== "string" || !path38.isAbsolute(scriptPath))
19345
19466
  throw new Error("node9: argv[1] is not an absolute path");
19346
- const resolvedScript = fs36.realpathSync(scriptPath);
19347
- const packageDist = fs36.realpathSync(path37.resolve(__dirname, "../.."));
19348
- if (!resolvedScript.startsWith(packageDist + path37.sep) && resolvedScript !== packageDist)
19467
+ const resolvedScript = fs37.realpathSync(scriptPath);
19468
+ const packageDist = fs37.realpathSync(path38.resolve(__dirname, "../.."));
19469
+ if (!resolvedScript.startsWith(packageDist + path38.sep) && resolvedScript !== packageDist)
19349
19470
  throw new Error(
19350
19471
  `node9: daemon spawn aborted \u2014 argv[1] (${resolvedScript}) is outside package dist (${packageDist})`
19351
19472
  );
@@ -19367,10 +19488,10 @@ RAW: ${raw}
19367
19488
  });
19368
19489
  d.unref();
19369
19490
  } catch (spawnErr) {
19370
- const logPath = path37.join(os32.homedir(), ".node9", "hook-debug.log");
19491
+ const logPath = path38.join(os33.homedir(), ".node9", "hook-debug.log");
19371
19492
  const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
19372
19493
  try {
19373
- fs36.appendFileSync(
19494
+ fs37.appendFileSync(
19374
19495
  logPath,
19375
19496
  `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-failed: ${msg}
19376
19497
  `
@@ -19380,10 +19501,10 @@ RAW: ${raw}
19380
19501
  }
19381
19502
  }
19382
19503
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
19383
- const logPath = path37.join(os32.homedir(), ".node9", "hook-debug.log");
19384
- if (!fs36.existsSync(path37.dirname(logPath)))
19385
- fs36.mkdirSync(path37.dirname(logPath), { recursive: true });
19386
- fs36.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
19504
+ const logPath = path38.join(os33.homedir(), ".node9", "hook-debug.log");
19505
+ if (!fs37.existsSync(path38.dirname(logPath)))
19506
+ fs37.mkdirSync(path38.dirname(logPath), { recursive: true });
19507
+ fs37.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
19387
19508
  `);
19388
19509
  }
19389
19510
  const rawToolName = sanitize2(extractToolName(payload));
@@ -19397,8 +19518,8 @@ RAW: ${raw}
19397
19518
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
19398
19519
  let ttyFd = null;
19399
19520
  try {
19400
- ttyFd = fs36.openSync("/dev/tty", "w");
19401
- const writeTty = (line) => fs36.writeSync(ttyFd, line + "\n");
19521
+ ttyFd = fs37.openSync("/dev/tty", "w");
19522
+ const writeTty = (line) => fs37.writeSync(ttyFd, line + "\n");
19402
19523
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
19403
19524
  writeTty(chalk9.bgRed.white.bold(`
19404
19525
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
@@ -19417,7 +19538,7 @@ RAW: ${raw}
19417
19538
  } finally {
19418
19539
  if (ttyFd !== null)
19419
19540
  try {
19420
- fs36.closeSync(ttyFd);
19541
+ fs37.closeSync(ttyFd);
19421
19542
  } catch {
19422
19543
  }
19423
19544
  }
@@ -19456,6 +19577,53 @@ RAW: ${raw}
19456
19577
  );
19457
19578
  process.exit(2);
19458
19579
  };
19580
+ const sendAsk = (result2) => {
19581
+ const msg = buildReviewMessage(result2.blockedByLabel, result2.ruleDescription);
19582
+ try {
19583
+ const key = reviewCorrelationKey(payload);
19584
+ if (key) {
19585
+ const sid = typeof payload.session_id === "string" ? payload.session_id : typeof payload.conversationId === "string" ? payload.conversationId : void 0;
19586
+ recordPendingReview({
19587
+ key,
19588
+ agent,
19589
+ tool: toolName,
19590
+ sessionId: sid,
19591
+ ts: Date.now(),
19592
+ label: result2.blockedByLabel
19593
+ });
19594
+ }
19595
+ } catch {
19596
+ }
19597
+ try {
19598
+ const ttyFd = fs37.openSync("/dev/tty", "w");
19599
+ fs37.writeSync(
19600
+ ttyFd,
19601
+ chalk9.yellow(
19602
+ `
19603
+ \u26A0\uFE0F Node9: review requested for "${toolName}" \u2014 answer in the prompt.
19604
+ `
19605
+ )
19606
+ );
19607
+ fs37.closeSync(ttyFd);
19608
+ } catch {
19609
+ }
19610
+ if (agent === "GitHub Copilot") {
19611
+ process.stdout.write(
19612
+ JSON.stringify({ permissionDecision: "ask", permissionDecisionReason: msg }) + "\n"
19613
+ );
19614
+ } else {
19615
+ process.stdout.write(
19616
+ JSON.stringify({
19617
+ hookSpecificOutput: {
19618
+ hookEventName: "PreToolUse",
19619
+ permissionDecision: "ask",
19620
+ permissionDecisionReason: msg
19621
+ }
19622
+ }) + "\n"
19623
+ );
19624
+ }
19625
+ process.exit(0);
19626
+ };
19459
19627
  if (!toolName) {
19460
19628
  sendBlock("Node9: unrecognised hook payload \u2014 tool name missing.");
19461
19629
  return;
@@ -19468,17 +19636,17 @@ RAW: ${raw}
19468
19636
  const safeSessionId = /^[A-Za-z0-9_\-]{1,128}$/.test(rawSessionId) ? rawSessionId : "";
19469
19637
  if (skillPinCfg.enabled && safeSessionId) {
19470
19638
  try {
19471
- const sessionsDir = path37.join(os32.homedir(), ".node9", "skill-sessions");
19472
- const flagPath = path37.join(sessionsDir, `${safeSessionId}.json`);
19639
+ const sessionsDir = path38.join(os33.homedir(), ".node9", "skill-sessions");
19640
+ const flagPath = path38.join(sessionsDir, `${safeSessionId}.json`);
19473
19641
  let flag = null;
19474
19642
  try {
19475
- flag = JSON.parse(fs36.readFileSync(flagPath, "utf-8"));
19643
+ flag = JSON.parse(fs37.readFileSync(flagPath, "utf-8"));
19476
19644
  } catch {
19477
19645
  }
19478
19646
  const writeFlag = (data2) => {
19479
19647
  try {
19480
- fs36.mkdirSync(sessionsDir, { recursive: true });
19481
- fs36.writeFileSync(
19648
+ fs37.mkdirSync(sessionsDir, { recursive: true });
19649
+ fs37.writeFileSync(
19482
19650
  flagPath,
19483
19651
  JSON.stringify({ ...data2, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
19484
19652
  { mode: 384 }
@@ -19489,8 +19657,8 @@ RAW: ${raw}
19489
19657
  const sendSkillWarn = (detail, recoveryCmd) => {
19490
19658
  let ttyFd = null;
19491
19659
  try {
19492
- ttyFd = fs36.openSync("/dev/tty", "w");
19493
- const w = (line) => fs36.writeSync(ttyFd, line + "\n");
19660
+ ttyFd = fs37.openSync("/dev/tty", "w");
19661
+ const w = (line) => fs37.writeSync(ttyFd, line + "\n");
19494
19662
  w(chalk9.yellow(`
19495
19663
  \u26A0\uFE0F Node9: installed skill drift detected`));
19496
19664
  w(chalk9.gray(` ${detail}`));
@@ -19505,7 +19673,7 @@ RAW: ${raw}
19505
19673
  } finally {
19506
19674
  if (ttyFd !== null)
19507
19675
  try {
19508
- fs36.closeSync(ttyFd);
19676
+ fs37.closeSync(ttyFd);
19509
19677
  } catch {
19510
19678
  }
19511
19679
  }
@@ -19521,7 +19689,7 @@ RAW: ${raw}
19521
19689
  return;
19522
19690
  }
19523
19691
  if (!flag || flag.state !== "verified" && flag.state !== "warned") {
19524
- const absoluteCwd = typeof payloadCwd === "string" && path37.isAbsolute(payloadCwd) ? payloadCwd : void 0;
19692
+ const absoluteCwd = typeof payloadCwd === "string" && path38.isAbsolute(payloadCwd) ? payloadCwd : void 0;
19525
19693
  const extraRoots = skillPinCfg.roots;
19526
19694
  const resolvedExtra = extraRoots.map((r) => resolveUserSkillRoot(r, absoluteCwd)).filter((r) => typeof r === "string");
19527
19695
  const roots = [...defaultSkillRoots(absoluteCwd), ...resolvedExtra];
@@ -19562,10 +19730,10 @@ RAW: ${raw}
19562
19730
  }
19563
19731
  try {
19564
19732
  const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
19565
- for (const name of fs36.readdirSync(sessionsDir)) {
19566
- const p = path37.join(sessionsDir, name);
19733
+ for (const name of fs37.readdirSync(sessionsDir)) {
19734
+ const p = path38.join(sessionsDir, name);
19567
19735
  try {
19568
- if (fs36.statSync(p).mtimeMs < cutoff) fs36.unlinkSync(p);
19736
+ if (fs37.statSync(p).mtimeMs < cutoff) fs37.unlinkSync(p);
19569
19737
  } catch {
19570
19738
  }
19571
19739
  }
@@ -19575,9 +19743,9 @@ RAW: ${raw}
19575
19743
  } catch (err2) {
19576
19744
  if (process.env.NODE9_DEBUG === "1") {
19577
19745
  try {
19578
- const dbg = path37.join(os32.homedir(), ".node9", "hook-debug.log");
19746
+ const dbg = path38.join(os33.homedir(), ".node9", "hook-debug.log");
19579
19747
  const msg = err2 instanceof Error ? err2.message : String(err2);
19580
- fs36.appendFileSync(dbg, `[${(/* @__PURE__ */ new Date()).toISOString()}] SKILL_PIN_ERROR: ${msg}
19748
+ fs37.appendFileSync(dbg, `[${(/* @__PURE__ */ new Date()).toISOString()}] SKILL_PIN_ERROR: ${msg}
19581
19749
  `);
19582
19750
  } catch {
19583
19751
  }
@@ -19587,9 +19755,11 @@ RAW: ${raw}
19587
19755
  if (shouldSnapshot(toolName, toolInput, config)) {
19588
19756
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
19589
19757
  }
19590
- const safeCwdForAuth = typeof payloadCwd === "string" && path37.isAbsolute(payloadCwd) ? payloadCwd : void 0;
19758
+ const safeCwdForAuth = typeof payloadCwd === "string" && path38.isAbsolute(payloadCwd) ? payloadCwd : void 0;
19759
+ const askMode = resolveAskMode(agent, opts, config);
19591
19760
  const result = await authorizeHeadless(toolName, toolInput, meta, {
19592
- cwd: safeCwdForAuth
19761
+ cwd: safeCwdForAuth,
19762
+ deferReview: askMode
19593
19763
  });
19594
19764
  if (result.approved) {
19595
19765
  if (result.checkedBy && process.env.NODE9_DEBUG === "1")
@@ -19597,14 +19767,18 @@ RAW: ${raw}
19597
19767
  `);
19598
19768
  process.exit(0);
19599
19769
  }
19770
+ if (result.review) {
19771
+ sendAsk(result);
19772
+ return;
19773
+ }
19600
19774
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
19601
19775
  try {
19602
- const tty = fs36.openSync("/dev/tty", "w");
19603
- fs36.writeSync(
19776
+ const tty = fs37.openSync("/dev/tty", "w");
19777
+ fs37.writeSync(
19604
19778
  tty,
19605
19779
  chalk9.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
19606
19780
  );
19607
- fs36.closeSync(tty);
19781
+ fs37.closeSync(tty);
19608
19782
  } catch {
19609
19783
  }
19610
19784
  const daemonReady = await autoStartDaemonAndWait();
@@ -19631,9 +19805,9 @@ RAW: ${raw}
19631
19805
  });
19632
19806
  } catch (err2) {
19633
19807
  if (process.env.NODE9_DEBUG === "1") {
19634
- const logPath = path37.join(os32.homedir(), ".node9", "hook-debug.log");
19808
+ const logPath = path38.join(os33.homedir(), ".node9", "hook-debug.log");
19635
19809
  const errMsg = err2 instanceof Error ? err2.message : String(err2);
19636
- fs36.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
19810
+ fs37.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
19637
19811
  `);
19638
19812
  }
19639
19813
  process.exit(0);
@@ -19669,9 +19843,9 @@ RAW: ${raw}
19669
19843
  // src/cli/commands/log.ts
19670
19844
  init_audit();
19671
19845
  init_config();
19672
- import fs37 from "fs";
19673
- import path38 from "path";
19674
- import os33 from "os";
19846
+ import fs38 from "fs";
19847
+ import path39 from "path";
19848
+ import os34 from "os";
19675
19849
  init_daemon();
19676
19850
  init_dlp();
19677
19851
 
@@ -19762,21 +19936,27 @@ function registerLogCommand(program2) {
19762
19936
  return void 0;
19763
19937
  })();
19764
19938
  const agent = agentOverride !== void 0 ? agentOverride : metaTag !== void 0 ? metaTag : payload.turn_id !== void 0 ? "Codex" : payload.toolCall !== void 0 || payload.conversationId !== void 0 ? "Antigravity" : payload.hook_event_name === "pre_tool_call" || payload.hook_event_name === "post_tool_call" ? "Hermes" : payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0 ? "Claude Code" : payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0 ? "Gemini CLI" : process.env.HERMES_SESSION_ID || process.env.HERMES_HOME || process.env.HERMES_INTERACTIVE ? "Hermes" : process.env.ANTIGRAVITY_CONVERSATION_ID ? "Antigravity" : void 0;
19939
+ let reviewApproved = false;
19940
+ try {
19941
+ const key = reviewCorrelationKey(payload);
19942
+ if (key && resolvePendingReview(key)) reviewApproved = true;
19943
+ } catch {
19944
+ }
19765
19945
  const entry = {
19766
19946
  ts: (/* @__PURE__ */ new Date()).toISOString(),
19767
19947
  tool,
19768
19948
  args: JSON.parse(redactSecrets(JSON.stringify(rawInput))),
19769
19949
  decision: "allowed",
19770
- source: "post-hook"
19950
+ source: reviewApproved ? "inline-review-approved" : "post-hook"
19771
19951
  };
19772
19952
  if (agent) entry.agent = agent;
19773
19953
  if (rawToolName !== tool) entry.agentToolName = rawToolName;
19774
19954
  const payloadSessionId = payload.session_id ?? payload.conversationId;
19775
19955
  if (payloadSessionId) entry.sessionId = payloadSessionId;
19776
- const logPath = path38.join(os33.homedir(), ".node9", "audit.log");
19777
- if (!fs37.existsSync(path38.dirname(logPath)))
19778
- fs37.mkdirSync(path38.dirname(logPath), { recursive: true });
19779
- fs37.appendFileSync(logPath, JSON.stringify(entry) + "\n");
19956
+ const logPath = path39.join(os34.homedir(), ".node9", "audit.log");
19957
+ if (!fs38.existsSync(path39.dirname(logPath)))
19958
+ fs38.mkdirSync(path39.dirname(logPath), { recursive: true });
19959
+ fs38.appendFileSync(logPath, JSON.stringify(entry) + "\n");
19780
19960
  if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
19781
19961
  const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
19782
19962
  if (command) {
@@ -19810,7 +19990,7 @@ function registerLogCommand(program2) {
19810
19990
  }
19811
19991
  }
19812
19992
  const payloadCwd = typeof payload.cwd === "string" ? payload.cwd : Array.isArray(payload.workspacePaths) && typeof payload.workspacePaths[0] === "string" ? payload.workspacePaths[0] : void 0;
19813
- const safeCwd = typeof payloadCwd === "string" && path38.isAbsolute(payloadCwd) ? payloadCwd : void 0;
19993
+ const safeCwd = typeof payloadCwd === "string" && path39.isAbsolute(payloadCwd) ? payloadCwd : void 0;
19814
19994
  const config = getConfig(safeCwd);
19815
19995
  {
19816
19996
  const toolOutput = payload.tool_response?.output;
@@ -19887,9 +20067,9 @@ function registerLogCommand(program2) {
19887
20067
  const msg = err2 instanceof Error ? err2.message : String(err2);
19888
20068
  process.stderr.write(`[Node9] audit log error: ${msg}
19889
20069
  `);
19890
- const debugPath = path38.join(os33.homedir(), ".node9", "hook-debug.log");
20070
+ const debugPath = path39.join(os34.homedir(), ".node9", "hook-debug.log");
19891
20071
  try {
19892
- fs37.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
20072
+ fs38.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
19893
20073
  `);
19894
20074
  } catch {
19895
20075
  }
@@ -20291,22 +20471,22 @@ function registerConfigShowCommand(program2) {
20291
20471
  init_daemon();
20292
20472
  init_config();
20293
20473
  import chalk11 from "chalk";
20294
- import fs39 from "fs";
20295
- import path40 from "path";
20296
- import os35 from "os";
20474
+ import fs40 from "fs";
20475
+ import path41 from "path";
20476
+ import os36 from "os";
20297
20477
  import { execSync } from "child_process";
20298
20478
 
20299
20479
  // src/agent-wiring.ts
20300
20480
  init_setup();
20301
- import fs38 from "fs";
20302
- import path39 from "path";
20303
- import os34 from "os";
20481
+ import fs39 from "fs";
20482
+ import path40 from "path";
20483
+ import os35 from "os";
20304
20484
  import * as yaml2 from "yaml";
20305
20485
  import { parse as parseToml2 } from "smol-toml";
20306
20486
  function readJson2(filePath) {
20307
- if (!fs38.existsSync(filePath)) return null;
20487
+ if (!fs39.existsSync(filePath)) return null;
20308
20488
  try {
20309
- return JSON.parse(fs38.readFileSync(filePath, "utf-8"));
20489
+ return JSON.parse(fs39.readFileSync(filePath, "utf-8"));
20310
20490
  } catch {
20311
20491
  return "invalid";
20312
20492
  }
@@ -20318,10 +20498,10 @@ function flatHaveNode9Hook(entries) {
20318
20498
  return (Array.isArray(entries) ? entries : []).some((h) => isNode9Hook(h.command));
20319
20499
  }
20320
20500
  function readHookRoot(filePath, format) {
20321
- if (!fs38.existsSync(filePath)) return "absent";
20501
+ if (!fs39.existsSync(filePath)) return "absent";
20322
20502
  let raw;
20323
20503
  try {
20324
- raw = fs38.readFileSync(filePath, "utf-8");
20504
+ raw = fs39.readFileSync(filePath, "utf-8");
20325
20505
  } catch {
20326
20506
  return "absent";
20327
20507
  }
@@ -20344,10 +20524,10 @@ function detectMcp(servers) {
20344
20524
  return { wrapped, present };
20345
20525
  }
20346
20526
  function readMcp(filePath, format) {
20347
- if (!fs38.existsSync(filePath)) return { wrapped: [], present: false };
20527
+ if (!fs39.existsSync(filePath)) return { wrapped: [], present: false };
20348
20528
  try {
20349
20529
  if (format === "toml") {
20350
- const parsed2 = parseToml2(fs38.readFileSync(filePath, "utf-8"));
20530
+ const parsed2 = parseToml2(fs39.readFileSync(filePath, "utf-8"));
20351
20531
  return detectMcp(parsed2?.mcp_servers);
20352
20532
  }
20353
20533
  const parsed = readJson2(filePath);
@@ -20359,7 +20539,7 @@ function readMcp(filePath, format) {
20359
20539
  }
20360
20540
  var exists = (p) => {
20361
20541
  try {
20362
- return fs38.existsSync(p);
20542
+ return fs39.existsSync(p);
20363
20543
  } catch {
20364
20544
  return false;
20365
20545
  }
@@ -20373,52 +20553,52 @@ var AGENT_SPECS = [
20373
20553
  id: "claude",
20374
20554
  label: "Claude Code",
20375
20555
  setupCommand: "node9 agents add claude",
20376
- hookFile: (h) => path39.join(h, ".claude", "settings.json"),
20556
+ hookFile: (h) => path40.join(h, ".claude", "settings.json"),
20377
20557
  hookFormat: "matcher",
20378
20558
  hookEvents: [ck("PreToolUse"), lg("PostToolUse")],
20379
- mcpFile: (h) => path39.join(h, ".claude.json"),
20380
- present: (h) => exists(path39.join(h, ".claude", "settings.json")) || exists(path39.join(h, ".claude.json"))
20559
+ mcpFile: (h) => path40.join(h, ".claude.json"),
20560
+ present: (h) => exists(path40.join(h, ".claude", "settings.json")) || exists(path40.join(h, ".claude.json"))
20381
20561
  },
20382
20562
  {
20383
20563
  id: "gemini",
20384
20564
  label: "Gemini CLI",
20385
20565
  setupCommand: "node9 agents add gemini",
20386
- hookFile: (h) => path39.join(h, ".gemini", "settings.json"),
20566
+ hookFile: (h) => path40.join(h, ".gemini", "settings.json"),
20387
20567
  hookFormat: "matcher",
20388
20568
  hookEvents: [ck("BeforeTool"), lg("AfterTool")],
20389
- mcpFile: (h) => path39.join(h, ".gemini", "settings.json"),
20390
- present: (h) => exists(path39.join(h, ".gemini", "settings.json"))
20569
+ mcpFile: (h) => path40.join(h, ".gemini", "settings.json"),
20570
+ present: (h) => exists(path40.join(h, ".gemini", "settings.json"))
20391
20571
  },
20392
20572
  {
20393
20573
  id: "codex",
20394
20574
  label: "Codex",
20395
20575
  setupCommand: "node9 agents add codex",
20396
- hookFile: (h) => path39.join(h, ".codex", "hooks.json"),
20576
+ hookFile: (h) => path40.join(h, ".codex", "hooks.json"),
20397
20577
  hookFormat: "matcher",
20398
20578
  hookEvents: [ck("PreToolUse"), ck("UserPromptSubmit")],
20399
- mcpFile: (h) => path39.join(h, ".codex", "config.toml"),
20579
+ mcpFile: (h) => path40.join(h, ".codex", "config.toml"),
20400
20580
  mcpFormat: "toml",
20401
- present: (h) => exists(path39.join(h, ".codex"))
20581
+ present: (h) => exists(path40.join(h, ".codex"))
20402
20582
  },
20403
20583
  {
20404
20584
  id: "antigravity",
20405
20585
  label: "Antigravity",
20406
20586
  setupCommand: "node9 agents add antigravity",
20407
- hookFile: (h) => path39.join(h, ".gemini", "config", "hooks.json"),
20587
+ hookFile: (h) => path40.join(h, ".gemini", "config", "hooks.json"),
20408
20588
  hookFormat: "matcher",
20409
20589
  hookEvents: [ck("PreToolUse"), lg("PostToolUse")],
20410
- mcpFile: (h) => path39.join(h, ".gemini", "config", "mcp_config.json"),
20411
- present: (h) => exists(path39.join(h, ".gemini", "config", "hooks.json")) || exists(path39.join(h, ".gemini", "antigravity-cli")) || exists(path39.join(h, ".gemini", "antigravity-ide"))
20590
+ mcpFile: (h) => path40.join(h, ".gemini", "config", "mcp_config.json"),
20591
+ present: (h) => exists(path40.join(h, ".gemini", "config", "hooks.json")) || exists(path40.join(h, ".gemini", "antigravity-cli")) || exists(path40.join(h, ".gemini", "antigravity-ide"))
20412
20592
  },
20413
20593
  {
20414
20594
  id: "copilot",
20415
20595
  label: "GitHub Copilot",
20416
20596
  setupCommand: "node9 agents add copilot",
20417
- hookFile: (h) => path39.join(h, ".copilot", "hooks", "node9.json"),
20597
+ hookFile: (h) => path40.join(h, ".copilot", "hooks", "node9.json"),
20418
20598
  hookFormat: "flat",
20419
20599
  hookEvents: [ck("PreToolUse"), lg("PostToolUse"), ck("UserPromptSubmit")],
20420
- mcpFile: (h) => path39.join(h, ".copilot", "mcp-config.json"),
20421
- present: (h) => exists(path39.join(h, ".copilot"))
20600
+ mcpFile: (h) => path40.join(h, ".copilot", "mcp-config.json"),
20601
+ present: (h) => exists(path40.join(h, ".copilot"))
20422
20602
  },
20423
20603
  {
20424
20604
  id: "cursor",
@@ -20427,8 +20607,8 @@ var AGENT_SPECS = [
20427
20607
  // MCP-only — no hook file (see note above).
20428
20608
  hookFormat: "flat",
20429
20609
  hookEvents: [],
20430
- mcpFile: (h) => path39.join(h, ".cursor", "mcp.json"),
20431
- present: (h) => exists(path39.join(h, ".cursor", "mcp.json"))
20610
+ mcpFile: (h) => path40.join(h, ".cursor", "mcp.json"),
20611
+ present: (h) => exists(path40.join(h, ".cursor", "mcp.json"))
20432
20612
  },
20433
20613
  {
20434
20614
  id: "hermes",
@@ -20449,8 +20629,8 @@ var AGENT_SPECS = [
20449
20629
  setupCommand: "node9 agents add opencode",
20450
20630
  hookFormat: "flat",
20451
20631
  hookEvents: [],
20452
- shimFile: (h) => path39.join(h, ".config", "opencode", "plugins", "node9.js"),
20453
- present: (h) => exists(path39.join(h, ".config", "opencode")) || exists(path39.join(h, ".config", "opencode", "plugins", "node9.js"))
20632
+ shimFile: (h) => path40.join(h, ".config", "opencode", "plugins", "node9.js"),
20633
+ present: (h) => exists(path40.join(h, ".config", "opencode")) || exists(path40.join(h, ".config", "opencode", "plugins", "node9.js"))
20454
20634
  },
20455
20635
  {
20456
20636
  id: "pi",
@@ -20458,11 +20638,11 @@ var AGENT_SPECS = [
20458
20638
  setupCommand: "node9 agents add pi",
20459
20639
  hookFormat: "flat",
20460
20640
  hookEvents: [],
20461
- shimFile: (h) => path39.join(h, ".pi", "agent", "extensions", "node9.js"),
20462
- present: (h) => exists(path39.join(h, ".pi", "agent")) || exists(path39.join(h, ".pi", "agent", "extensions", "node9.js"))
20641
+ shimFile: (h) => path40.join(h, ".pi", "agent", "extensions", "node9.js"),
20642
+ present: (h) => exists(path40.join(h, ".pi", "agent")) || exists(path40.join(h, ".pi", "agent", "extensions", "node9.js"))
20463
20643
  }
20464
20644
  ];
20465
- function getAgentWiring(home = os34.homedir()) {
20645
+ function getAgentWiring(home = os35.homedir()) {
20466
20646
  const detected = detectAgents(home);
20467
20647
  return AGENT_SPECS.map((spec) => {
20468
20648
  const present = spec.present(home);
@@ -20514,7 +20694,7 @@ function getAgentWiring(home = os34.homedir()) {
20514
20694
  // src/cli/commands/doctor.ts
20515
20695
  function registerDoctorCommand(program2, version2) {
20516
20696
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(async () => {
20517
- const homeDir2 = os35.homedir();
20697
+ const homeDir2 = os36.homedir();
20518
20698
  let failures = 0;
20519
20699
  function pass(msg) {
20520
20700
  console.log(chalk11.green(" \u2705 ") + msg);
@@ -20560,10 +20740,10 @@ function registerDoctorCommand(program2, version2) {
20560
20740
  );
20561
20741
  }
20562
20742
  section("Configuration");
20563
- const globalConfigPath = path40.join(homeDir2, ".node9", "config.json");
20564
- if (fs39.existsSync(globalConfigPath)) {
20743
+ const globalConfigPath = path41.join(homeDir2, ".node9", "config.json");
20744
+ if (fs40.existsSync(globalConfigPath)) {
20565
20745
  try {
20566
- JSON.parse(fs39.readFileSync(globalConfigPath, "utf-8"));
20746
+ JSON.parse(fs40.readFileSync(globalConfigPath, "utf-8"));
20567
20747
  pass("~/.node9/config.json found and valid");
20568
20748
  } catch {
20569
20749
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -20571,10 +20751,10 @@ function registerDoctorCommand(program2, version2) {
20571
20751
  } else {
20572
20752
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
20573
20753
  }
20574
- const projectConfigPath = path40.join(process.cwd(), "node9.config.json");
20575
- if (fs39.existsSync(projectConfigPath)) {
20754
+ const projectConfigPath = path41.join(process.cwd(), "node9.config.json");
20755
+ if (fs40.existsSync(projectConfigPath)) {
20576
20756
  try {
20577
- JSON.parse(fs39.readFileSync(projectConfigPath, "utf-8"));
20757
+ JSON.parse(fs40.readFileSync(projectConfigPath, "utf-8"));
20578
20758
  pass("node9.config.json found and valid (project)");
20579
20759
  } catch {
20580
20760
  fail(
@@ -20583,8 +20763,8 @@ function registerDoctorCommand(program2, version2) {
20583
20763
  );
20584
20764
  }
20585
20765
  }
20586
- const credsPath = path40.join(homeDir2, ".node9", "credentials.json");
20587
- if (fs39.existsSync(credsPath)) {
20766
+ const credsPath = path41.join(homeDir2, ".node9", "credentials.json");
20767
+ if (fs40.existsSync(credsPath)) {
20588
20768
  pass("Cloud credentials found (~/.node9/credentials.json)");
20589
20769
  } else {
20590
20770
  warn(
@@ -20628,7 +20808,7 @@ function registerDoctorCommand(program2, version2) {
20628
20808
  try {
20629
20809
  const { shipLagBytes: shipLagBytes2, readWatermark: readWatermark2, AUDIT_SHIP_WATERMARK: AUDIT_SHIP_WATERMARK2 } = await Promise.resolve().then(() => (init_audit_shipper(), audit_shipper_exports));
20630
20810
  const cfg = getConfig();
20631
- const creds = fs39.existsSync(path40.join(os35.homedir(), ".node9", "credentials.json"));
20811
+ const creds = fs40.existsSync(path41.join(os36.homedir(), ".node9", "credentials.json"));
20632
20812
  if (!creds) {
20633
20813
  warn("Not logged in \u2014 audit rows stay local", "Run: node9 login <api-key>");
20634
20814
  } else if (!cfg.settings.approvers.cloud) {
@@ -20678,9 +20858,9 @@ function registerDoctorCommand(program2, version2) {
20678
20858
 
20679
20859
  // src/cli/commands/audit.ts
20680
20860
  import chalk12 from "chalk";
20681
- import fs40 from "fs";
20682
- import path41 from "path";
20683
- import os36 from "os";
20861
+ import fs41 from "fs";
20862
+ import path42 from "path";
20863
+ import os37 from "os";
20684
20864
  function formatRelativeTime(timestamp) {
20685
20865
  const diff = Date.now() - new Date(timestamp).getTime();
20686
20866
  const sec = Math.floor(diff / 1e3);
@@ -20693,14 +20873,14 @@ function formatRelativeTime(timestamp) {
20693
20873
  }
20694
20874
  function registerAuditCommand(program2) {
20695
20875
  program2.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
20696
- const logPath = path41.join(os36.homedir(), ".node9", "audit.log");
20697
- if (!fs40.existsSync(logPath)) {
20876
+ const logPath = path42.join(os37.homedir(), ".node9", "audit.log");
20877
+ if (!fs41.existsSync(logPath)) {
20698
20878
  console.log(
20699
20879
  chalk12.yellow("No audit logs found. Run node9 with an agent to generate entries.")
20700
20880
  );
20701
20881
  return;
20702
20882
  }
20703
- const raw = fs40.readFileSync(logPath, "utf-8");
20883
+ const raw = fs41.readFileSync(logPath, "utf-8");
20704
20884
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
20705
20885
  let entries = lines.flatMap((line) => {
20706
20886
  try {
@@ -20759,9 +20939,9 @@ import chalk13 from "chalk";
20759
20939
  init_costSync();
20760
20940
  init_litellm();
20761
20941
  init_cost_codex();
20762
- import fs41 from "fs";
20763
- import os37 from "os";
20764
- import path42 from "path";
20942
+ import fs42 from "fs";
20943
+ import os38 from "os";
20944
+ import path43 from "path";
20765
20945
  var TEST_COMMAND_RE3 = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
20766
20946
  function buildTestTimestamps(allEntries) {
20767
20947
  const testTs = /* @__PURE__ */ new Set();
@@ -20841,8 +21021,8 @@ function getDateRange(period, now) {
20841
21021
  }
20842
21022
  }
20843
21023
  function parseAuditLog(logPath) {
20844
- if (!fs41.existsSync(logPath)) return [];
20845
- const raw = fs41.readFileSync(logPath, "utf-8");
21024
+ if (!fs42.existsSync(logPath)) return [];
21025
+ const raw = fs42.readFileSync(logPath, "utf-8");
20846
21026
  return raw.split("\n").flatMap((line) => {
20847
21027
  if (!line.trim()) return [];
20848
21028
  try {
@@ -20889,25 +21069,25 @@ function freezeClaudeCost(acc) {
20889
21069
  };
20890
21070
  }
20891
21071
  function processClaudeCostProject(proj, projectsDir, start, end, acc) {
20892
- const projPath = path42.join(projectsDir, proj);
21072
+ const projPath = path43.join(projectsDir, proj);
20893
21073
  let files;
20894
21074
  try {
20895
- const stat = fs41.statSync(projPath);
21075
+ const stat = fs42.statSync(projPath);
20896
21076
  if (!stat.isDirectory()) return;
20897
- files = fs41.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
21077
+ files = fs42.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
20898
21078
  } catch {
20899
21079
  return;
20900
21080
  }
20901
21081
  const startMs = start.getTime();
20902
21082
  for (const file of files) {
20903
- const filePath = path42.join(projPath, file);
21083
+ const filePath = path43.join(projPath, file);
20904
21084
  try {
20905
- if (fs41.statSync(filePath).mtimeMs < startMs) continue;
21085
+ if (fs42.statSync(filePath).mtimeMs < startMs) continue;
20906
21086
  } catch {
20907
21087
  continue;
20908
21088
  }
20909
21089
  try {
20910
- const raw = fs41.readFileSync(filePath, "utf-8");
21090
+ const raw = fs42.readFileSync(filePath, "utf-8");
20911
21091
  for (const line of raw.split("\n")) {
20912
21092
  if (!line.trim()) continue;
20913
21093
  let entry;
@@ -20957,10 +21137,10 @@ function processClaudeCostProject(proj, projectsDir, start, end, acc) {
20957
21137
  }
20958
21138
  function loadClaudeCost(start, end, projectsDir) {
20959
21139
  const acc = emptyClaudeCostAccumulator();
20960
- if (!fs41.existsSync(projectsDir)) return freezeClaudeCost(acc);
21140
+ if (!fs42.existsSync(projectsDir)) return freezeClaudeCost(acc);
20961
21141
  let dirs;
20962
21142
  try {
20963
- dirs = fs41.readdirSync(projectsDir);
21143
+ dirs = fs42.readdirSync(projectsDir);
20964
21144
  } catch {
20965
21145
  return freezeClaudeCost(acc);
20966
21146
  }
@@ -20972,7 +21152,7 @@ function loadClaudeCost(start, end, projectsDir) {
20972
21152
  function processCodexCostFile(filePath, start, end, acc) {
20973
21153
  let lines;
20974
21154
  try {
20975
- lines = fs41.readFileSync(filePath, "utf-8").split("\n");
21155
+ lines = fs42.readFileSync(filePath, "utf-8").split("\n");
20976
21156
  } catch {
20977
21157
  return;
20978
21158
  }
@@ -21027,31 +21207,31 @@ function processCodexCostFile(filePath, start, end, acc) {
21027
21207
  }
21028
21208
  function listCodexSessionFiles2(sessionsBase) {
21029
21209
  const jsonlFiles = [];
21030
- if (!fs41.existsSync(sessionsBase)) return jsonlFiles;
21210
+ if (!fs42.existsSync(sessionsBase)) return jsonlFiles;
21031
21211
  try {
21032
- for (const year of fs41.readdirSync(sessionsBase)) {
21033
- const yearPath = path42.join(sessionsBase, year);
21212
+ for (const year of fs42.readdirSync(sessionsBase)) {
21213
+ const yearPath = path43.join(sessionsBase, year);
21034
21214
  try {
21035
- if (!fs41.statSync(yearPath).isDirectory()) continue;
21215
+ if (!fs42.statSync(yearPath).isDirectory()) continue;
21036
21216
  } catch {
21037
21217
  continue;
21038
21218
  }
21039
- for (const month of fs41.readdirSync(yearPath)) {
21040
- const monthPath = path42.join(yearPath, month);
21219
+ for (const month of fs42.readdirSync(yearPath)) {
21220
+ const monthPath = path43.join(yearPath, month);
21041
21221
  try {
21042
- if (!fs41.statSync(monthPath).isDirectory()) continue;
21222
+ if (!fs42.statSync(monthPath).isDirectory()) continue;
21043
21223
  } catch {
21044
21224
  continue;
21045
21225
  }
21046
- for (const day of fs41.readdirSync(monthPath)) {
21047
- const dayPath = path42.join(monthPath, day);
21226
+ for (const day of fs42.readdirSync(monthPath)) {
21227
+ const dayPath = path43.join(monthPath, day);
21048
21228
  try {
21049
- if (!fs41.statSync(dayPath).isDirectory()) continue;
21229
+ if (!fs42.statSync(dayPath).isDirectory()) continue;
21050
21230
  } catch {
21051
21231
  continue;
21052
21232
  }
21053
- for (const file of fs41.readdirSync(dayPath)) {
21054
- if (file.endsWith(".jsonl")) jsonlFiles.push(path42.join(dayPath, file));
21233
+ for (const file of fs42.readdirSync(dayPath)) {
21234
+ if (file.endsWith(".jsonl")) jsonlFiles.push(path43.join(dayPath, file));
21055
21235
  }
21056
21236
  }
21057
21237
  }
@@ -21116,13 +21296,13 @@ function freezeGeminiCost(acc) {
21116
21296
  function processGeminiCostFile(filePath, projectKey, start, end, acc) {
21117
21297
  const startMs = start.getTime();
21118
21298
  try {
21119
- if (fs41.statSync(filePath).mtimeMs < startMs) return;
21299
+ if (fs42.statSync(filePath).mtimeMs < startMs) return;
21120
21300
  } catch {
21121
21301
  return;
21122
21302
  }
21123
21303
  let raw;
21124
21304
  try {
21125
- raw = fs41.readFileSync(filePath, "utf-8");
21305
+ raw = fs42.readFileSync(filePath, "utf-8");
21126
21306
  } catch {
21127
21307
  return;
21128
21308
  }
@@ -21171,30 +21351,30 @@ function listGeminiSessionFiles2(geminiTmpDir2) {
21171
21351
  const out = [];
21172
21352
  let dirs;
21173
21353
  try {
21174
- if (!fs41.statSync(geminiTmpDir2).isDirectory()) return out;
21175
- dirs = fs41.readdirSync(geminiTmpDir2);
21354
+ if (!fs42.statSync(geminiTmpDir2).isDirectory()) return out;
21355
+ dirs = fs42.readdirSync(geminiTmpDir2);
21176
21356
  } catch {
21177
21357
  return out;
21178
21358
  }
21179
21359
  for (const proj of dirs) {
21180
- const chatsDir = path42.join(geminiTmpDir2, proj, "chats");
21360
+ const chatsDir = path43.join(geminiTmpDir2, proj, "chats");
21181
21361
  let files;
21182
21362
  try {
21183
- if (!fs41.statSync(chatsDir).isDirectory()) continue;
21184
- files = fs41.readdirSync(chatsDir);
21363
+ if (!fs42.statSync(chatsDir).isDirectory()) continue;
21364
+ files = fs42.readdirSync(chatsDir);
21185
21365
  } catch {
21186
21366
  continue;
21187
21367
  }
21188
21368
  for (const f of files) {
21189
21369
  if (!f.endsWith(".jsonl")) continue;
21190
- out.push({ projectKey: proj, file: path42.join(chatsDir, f) });
21370
+ out.push({ projectKey: proj, file: path43.join(chatsDir, f) });
21191
21371
  }
21192
21372
  }
21193
21373
  return out;
21194
21374
  }
21195
21375
  function loadGeminiCost(start, end, geminiTmpDir2) {
21196
21376
  const acc = emptyGeminiAccumulator();
21197
- if (!fs41.existsSync(geminiTmpDir2)) return freezeGeminiCost(acc);
21377
+ if (!fs42.existsSync(geminiTmpDir2)) return freezeGeminiCost(acc);
21198
21378
  for (const { projectKey, file } of listGeminiSessionFiles2(geminiTmpDir2)) {
21199
21379
  processGeminiCostFile(file, projectKey, start, end, acc);
21200
21380
  }
@@ -21202,11 +21382,11 @@ function loadGeminiCost(start, end, geminiTmpDir2) {
21202
21382
  }
21203
21383
  function aggregateReportFromAudit(period, opts = {}) {
21204
21384
  const now = opts.now ?? /* @__PURE__ */ new Date();
21205
- const auditLogPath = opts.auditLogPath ?? path42.join(os37.homedir(), ".node9", "audit.log");
21206
- const claudeProjectsDir = opts.claudeProjectsDir ?? path42.join(os37.homedir(), ".claude", "projects");
21207
- const codexSessionsDir2 = opts.codexSessionsDir ?? path42.join(os37.homedir(), ".codex", "sessions");
21208
- const geminiTmpDir2 = opts.geminiTmpDir ?? path42.join(os37.homedir(), ".gemini", "tmp");
21209
- const hasAuditFile = fs41.existsSync(auditLogPath);
21385
+ const auditLogPath = opts.auditLogPath ?? path43.join(os38.homedir(), ".node9", "audit.log");
21386
+ const claudeProjectsDir = opts.claudeProjectsDir ?? path43.join(os38.homedir(), ".claude", "projects");
21387
+ const codexSessionsDir2 = opts.codexSessionsDir ?? path43.join(os38.homedir(), ".codex", "sessions");
21388
+ const geminiTmpDir2 = opts.geminiTmpDir ?? path43.join(os38.homedir(), ".gemini", "tmp");
21389
+ const hasAuditFile = fs42.existsSync(auditLogPath);
21210
21390
  const allEntries = opts.preloadedAuditEntries ?? parseAuditLog(auditLogPath);
21211
21391
  const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
21212
21392
  const { start, end } = getDateRange(period, now);
@@ -21904,9 +22084,9 @@ function registerDaemonCommand(program2) {
21904
22084
  init_core();
21905
22085
  init_daemon();
21906
22086
  import chalk15 from "chalk";
21907
- import fs42 from "fs";
21908
- import path43 from "path";
21909
- import os38 from "os";
22087
+ import fs43 from "fs";
22088
+ import path44 from "path";
22089
+ import os39 from "os";
21910
22090
  function printAgentSection(label2, hookPairs, wrapped) {
21911
22091
  console.log(chalk15.bold(` ${label2}`));
21912
22092
  for (const { name, present } of hookPairs) {
@@ -21960,20 +22140,20 @@ function registerStatusCommand(program2) {
21960
22140
  console.log("");
21961
22141
  const modeLabel = settings.mode === "audit" ? chalk15.blue("audit") : settings.mode === "strict" ? chalk15.red("strict") : chalk15.white("standard");
21962
22142
  console.log(` Mode: ${modeLabel}`);
21963
- const projectConfig = path43.join(process.cwd(), "node9.config.json");
21964
- const globalConfig = path43.join(os38.homedir(), ".node9", "config.json");
22143
+ const projectConfig = path44.join(process.cwd(), "node9.config.json");
22144
+ const globalConfig = path44.join(os39.homedir(), ".node9", "config.json");
21965
22145
  console.log(
21966
- ` Local: ${fs42.existsSync(projectConfig) ? chalk15.green("Active (node9.config.json)") : chalk15.gray("Not present")}`
22146
+ ` Local: ${fs43.existsSync(projectConfig) ? chalk15.green("Active (node9.config.json)") : chalk15.gray("Not present")}`
21967
22147
  );
21968
22148
  console.log(
21969
- ` Global: ${fs42.existsSync(globalConfig) ? chalk15.green("Active (~/.node9/config.json)") : chalk15.gray("Not present")}`
22149
+ ` Global: ${fs43.existsSync(globalConfig) ? chalk15.green("Active (~/.node9/config.json)") : chalk15.gray("Not present")}`
21970
22150
  );
21971
22151
  if (mergedConfig.policy.sandboxPaths.length > 0) {
21972
22152
  console.log(
21973
22153
  ` Sandbox: ${chalk15.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
21974
22154
  );
21975
22155
  }
21976
- const wiring = getAgentWiring(os38.homedir()).filter((a) => a.present);
22156
+ const wiring = getAgentWiring(os39.homedir()).filter((a) => a.present);
21977
22157
  if (wiring.length > 0) {
21978
22158
  console.log("");
21979
22159
  console.log(chalk15.bold(" Agent Wiring:"));
@@ -22012,9 +22192,9 @@ init_setup();
22012
22192
  init_shields();
22013
22193
  init_service();
22014
22194
  import chalk16 from "chalk";
22015
- import fs43 from "fs";
22016
- import path44 from "path";
22017
- import os39 from "os";
22195
+ import fs44 from "fs";
22196
+ import path45 from "path";
22197
+ import os40 from "os";
22018
22198
  import https4 from "https";
22019
22199
  var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "project-jail"];
22020
22200
  function buildTelemetryPayload(agents, firstInstall) {
@@ -22100,16 +22280,16 @@ function registerInitCommand(program2) {
22100
22280
  }
22101
22281
  console.log("");
22102
22282
  }
22103
- const configPath2 = path44.join(os39.homedir(), ".node9", "config.json");
22104
- const isFirstInstall = !fs43.existsSync(configPath2);
22105
- if (fs43.existsSync(configPath2) && !options.force) {
22283
+ const configPath2 = path45.join(os40.homedir(), ".node9", "config.json");
22284
+ const isFirstInstall = !fs44.existsSync(configPath2);
22285
+ if (fs44.existsSync(configPath2) && !options.force) {
22106
22286
  try {
22107
- const existing = JSON.parse(fs43.readFileSync(configPath2, "utf-8"));
22287
+ const existing = JSON.parse(fs44.readFileSync(configPath2, "utf-8"));
22108
22288
  const settings = existing.settings ?? {};
22109
22289
  if (settings.mode !== chosenMode) {
22110
22290
  settings.mode = chosenMode;
22111
22291
  existing.settings = settings;
22112
- fs43.writeFileSync(configPath2, JSON.stringify(existing, null, 2) + "\n");
22292
+ fs44.writeFileSync(configPath2, JSON.stringify(existing, null, 2) + "\n");
22113
22293
  console.log(chalk16.green(`\u2705 Mode updated: ${chosenMode}`));
22114
22294
  } else {
22115
22295
  console.log(chalk16.blue(`\u2139\uFE0F Config already exists: ${configPath2}`));
@@ -22122,9 +22302,9 @@ function registerInitCommand(program2) {
22122
22302
  ...DEFAULT_CONFIG,
22123
22303
  settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
22124
22304
  };
22125
- const dir = path44.dirname(configPath2);
22126
- if (!fs43.existsSync(dir)) fs43.mkdirSync(dir, { recursive: true });
22127
- fs43.writeFileSync(configPath2, JSON.stringify(configToSave, null, 2) + "\n");
22305
+ const dir = path45.dirname(configPath2);
22306
+ if (!fs44.existsSync(dir)) fs44.mkdirSync(dir, { recursive: true });
22307
+ fs44.writeFileSync(configPath2, JSON.stringify(configToSave, null, 2) + "\n");
22128
22308
  console.log(chalk16.green(`\u2705 Config created: ${configPath2}`));
22129
22309
  console.log(chalk16.gray(` Mode: ${chosenMode}`));
22130
22310
  }
@@ -22229,7 +22409,7 @@ function registerInitCommand(program2) {
22229
22409
  }
22230
22410
 
22231
22411
  // src/cli/commands/undo.ts
22232
- import path45 from "path";
22412
+ import path46 from "path";
22233
22413
  import chalk18 from "chalk";
22234
22414
 
22235
22415
  // src/tui/undo-navigator.ts
@@ -22388,7 +22568,7 @@ function findMatchingCwd(startDir, history) {
22388
22568
  let dir = startDir;
22389
22569
  while (true) {
22390
22570
  if (cwds.has(dir)) return dir;
22391
- const parent = path45.dirname(dir);
22571
+ const parent = path46.dirname(dir);
22392
22572
  if (parent === dir) return null;
22393
22573
  dir = parent;
22394
22574
  }
@@ -23023,9 +23203,9 @@ function registerMcpGatewayCommand(program2) {
23023
23203
 
23024
23204
  // src/mcp-server/index.ts
23025
23205
  import readline5 from "readline";
23026
- import fs44 from "fs";
23027
- import os40 from "os";
23028
- import path46 from "path";
23206
+ import fs45 from "fs";
23207
+ import os41 from "os";
23208
+ import path47 from "path";
23029
23209
  import { spawnSync as spawnSync4 } from "child_process";
23030
23210
  init_core();
23031
23211
  init_daemon();
@@ -23276,13 +23456,13 @@ function handleStatus() {
23276
23456
  lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
23277
23457
  lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
23278
23458
  lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
23279
- const projectConfig = path46.join(process.cwd(), "node9.config.json");
23280
- const globalConfig = path46.join(os40.homedir(), ".node9", "config.json");
23459
+ const projectConfig = path47.join(process.cwd(), "node9.config.json");
23460
+ const globalConfig = path47.join(os41.homedir(), ".node9", "config.json");
23281
23461
  lines.push(
23282
- `Project config (node9.config.json): ${fs44.existsSync(projectConfig) ? "present" : "not found"}`
23462
+ `Project config (node9.config.json): ${fs45.existsSync(projectConfig) ? "present" : "not found"}`
23283
23463
  );
23284
23464
  lines.push(
23285
- `Global config (~/.node9/config.json): ${fs44.existsSync(globalConfig) ? "present" : "not found"}`
23465
+ `Global config (~/.node9/config.json): ${fs45.existsSync(globalConfig) ? "present" : "not found"}`
23286
23466
  );
23287
23467
  return lines.join("\n");
23288
23468
  }
@@ -23356,21 +23536,21 @@ function handleShieldDisable(args) {
23356
23536
  writeActiveShields(active.filter((s) => s !== name));
23357
23537
  return `Shield "${name}" disabled.`;
23358
23538
  }
23359
- var GLOBAL_CONFIG_PATH = path46.join(os40.homedir(), ".node9", "config.json");
23539
+ var GLOBAL_CONFIG_PATH = path47.join(os41.homedir(), ".node9", "config.json");
23360
23540
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
23361
23541
  function readGlobalConfigRaw() {
23362
23542
  try {
23363
- if (fs44.existsSync(GLOBAL_CONFIG_PATH)) {
23364
- return JSON.parse(fs44.readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
23543
+ if (fs45.existsSync(GLOBAL_CONFIG_PATH)) {
23544
+ return JSON.parse(fs45.readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
23365
23545
  }
23366
23546
  } catch {
23367
23547
  }
23368
23548
  return {};
23369
23549
  }
23370
23550
  function writeGlobalConfigRaw(data) {
23371
- const dir = path46.dirname(GLOBAL_CONFIG_PATH);
23372
- if (!fs44.existsSync(dir)) fs44.mkdirSync(dir, { recursive: true });
23373
- fs44.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(data, null, 2) + "\n");
23551
+ const dir = path47.dirname(GLOBAL_CONFIG_PATH);
23552
+ if (!fs45.existsSync(dir)) fs45.mkdirSync(dir, { recursive: true });
23553
+ fs45.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(data, null, 2) + "\n");
23374
23554
  }
23375
23555
  function handleApproverList() {
23376
23556
  const config = getConfig();
@@ -23414,9 +23594,9 @@ function handleApproverSet(args) {
23414
23594
  function handleAuditGet(args) {
23415
23595
  const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
23416
23596
  const filter = typeof args.filter === "string" && args.filter !== "all" ? args.filter : null;
23417
- const auditPath = path46.join(os40.homedir(), ".node9", "audit.log");
23418
- if (!fs44.existsSync(auditPath)) return "No audit log found.";
23419
- const rawLines = fs44.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
23597
+ const auditPath = path47.join(os41.homedir(), ".node9", "audit.log");
23598
+ if (!fs45.existsSync(auditPath)) return "No audit log found.";
23599
+ const rawLines = fs45.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
23420
23600
  const parsed = [];
23421
23601
  for (const line of rawLines) {
23422
23602
  try {
@@ -23751,7 +23931,7 @@ function registerTrustCommand(program2) {
23751
23931
  // src/cli/commands/mcp-pin.ts
23752
23932
  init_mcp_pin();
23753
23933
  import chalk21 from "chalk";
23754
- import fs45 from "fs";
23934
+ import fs46 from "fs";
23755
23935
  function registerMcpPinCommand(program2) {
23756
23936
  const pinCmd = program2.command("mcp").description("Manage MCP server tool definition pinning (rug pull defense)");
23757
23937
  const pinSubCmd = pinCmd.command("pin").description("Manage pinned MCP server tool definitions");
@@ -23762,7 +23942,7 @@ function registerMcpPinCommand(program2) {
23762
23942
  let repoCorrupt = false;
23763
23943
  if (found.source === "repo") {
23764
23944
  try {
23765
- const raw = fs45.readFileSync(found.path, "utf-8");
23945
+ const raw = fs46.readFileSync(found.path, "utf-8");
23766
23946
  const parsed = JSON.parse(raw);
23767
23947
  repoEntries = parsed.servers ?? {};
23768
23948
  } catch {
@@ -24077,25 +24257,25 @@ init_scan();
24077
24257
  import chalk25 from "chalk";
24078
24258
 
24079
24259
  // src/posture/index.ts
24080
- import os44 from "os";
24260
+ import os45 from "os";
24081
24261
 
24082
24262
  // src/posture/secrets.ts
24083
24263
  init_dist();
24084
- import fs46 from "fs";
24085
- import path47 from "path";
24086
- import os41 from "os";
24264
+ import fs47 from "fs";
24265
+ import path48 from "path";
24266
+ import os42 from "os";
24087
24267
  var MAX_FILE_BYTES = 256 * 1024;
24088
24268
  function displayPath(p, home) {
24089
24269
  if (p === home) return "~";
24090
- const prefix = home.endsWith(path47.sep) ? home : home + path47.sep;
24091
- if (p.startsWith(prefix)) return "~" + path47.sep + p.slice(prefix.length);
24270
+ const prefix = home.endsWith(path48.sep) ? home : home + path48.sep;
24271
+ if (p.startsWith(prefix)) return "~" + path48.sep + p.slice(prefix.length);
24092
24272
  return p;
24093
24273
  }
24094
24274
  function safeRead(file) {
24095
24275
  try {
24096
- const stat = fs46.statSync(file);
24276
+ const stat = fs47.statSync(file);
24097
24277
  if (!stat.isFile() || stat.size === 0 || stat.size > MAX_FILE_BYTES) return null;
24098
- return fs46.readFileSync(file, "utf8");
24278
+ return fs47.readFileSync(file, "utf8");
24099
24279
  } catch {
24100
24280
  return null;
24101
24281
  }
@@ -24103,8 +24283,8 @@ function safeRead(file) {
24103
24283
  function candidateFiles(home, cwd) {
24104
24284
  const files = /* @__PURE__ */ new Set();
24105
24285
  try {
24106
- for (const name of fs46.readdirSync(cwd)) {
24107
- if (name === ".env" || name.startsWith(".env.")) files.add(path47.join(cwd, name));
24286
+ for (const name of fs47.readdirSync(cwd)) {
24287
+ if (name === ".env" || name.startsWith(".env.")) files.add(path48.join(cwd, name));
24108
24288
  }
24109
24289
  } catch {
24110
24290
  }
@@ -24112,21 +24292,21 @@ function candidateFiles(home, cwd) {
24112
24292
  if (spec.hookFile) files.add(spec.hookFile(home));
24113
24293
  if (spec.mcpFile) files.add(spec.mcpFile(home));
24114
24294
  }
24115
- files.add(path47.join(home, ".env"));
24295
+ files.add(path48.join(home, ".env"));
24116
24296
  return [...files];
24117
24297
  }
24118
24298
  function credentialMaterial(home) {
24119
24299
  return [
24120
- path47.join(home, ".ssh", "id_rsa"),
24121
- path47.join(home, ".ssh", "id_dsa"),
24122
- path47.join(home, ".ssh", "id_ecdsa"),
24123
- path47.join(home, ".ssh", "id_ed25519"),
24124
- path47.join(home, ".aws", "credentials"),
24125
- path47.join(home, ".config", "gcloud", "application_default_credentials.json")
24300
+ path48.join(home, ".ssh", "id_rsa"),
24301
+ path48.join(home, ".ssh", "id_dsa"),
24302
+ path48.join(home, ".ssh", "id_ecdsa"),
24303
+ path48.join(home, ".ssh", "id_ed25519"),
24304
+ path48.join(home, ".aws", "credentials"),
24305
+ path48.join(home, ".config", "gcloud", "application_default_credentials.json")
24126
24306
  ];
24127
24307
  }
24128
24308
  function checkSecrets(ctx) {
24129
- const home = ctx.home || os41.homedir();
24309
+ const home = ctx.home || os42.homedir();
24130
24310
  const findings = [];
24131
24311
  const plaintext = [];
24132
24312
  const plaintextPaths = [];
@@ -24159,7 +24339,7 @@ function checkSecrets(ctx) {
24159
24339
  const credPaths = [];
24160
24340
  for (const file of credentialMaterial(home)) {
24161
24341
  try {
24162
- if (fs46.statSync(file).isFile()) {
24342
+ if (fs47.statSync(file).isFile()) {
24163
24343
  creds.push(displayPath(file, home));
24164
24344
  credPaths.push(file);
24165
24345
  }
@@ -24185,6 +24365,132 @@ function checkSecrets(ctx) {
24185
24365
 
24186
24366
  // src/posture/egress.ts
24187
24367
  init_config();
24368
+ import fs48 from "fs";
24369
+
24370
+ // src/sandbox/templates.ts
24371
+ var AGENT_NPM_PACKAGE = {
24372
+ claude: "@anthropic-ai/claude-code",
24373
+ codex: "@openai/codex"
24374
+ };
24375
+ function pinnedNode9Version(hostVersion) {
24376
+ return hostVersion && /^\d+\.\d+\.\d+$/.test(hostVersion) ? hostVersion : "latest";
24377
+ }
24378
+ var AGENT_BIN = {
24379
+ claude: "claude",
24380
+ codex: "codex"
24381
+ };
24382
+ var RUN_AS_USER = "agent";
24383
+ var ALLOWED_DOMAINS_PATH = "/etc/node9-sandbox/allowed-domains.txt";
24384
+ function renderDockerfile(config, node9Version2) {
24385
+ const agentPkg = AGENT_NPM_PACKAGE[config.agent];
24386
+ return `# Auto-generated by node9 sandbox. Do not edit by hand.
24387
+ FROM node:22-bookworm
24388
+
24389
+ ENV DEBIAN_FRONTEND=noninteractive
24390
+
24391
+ # Wall + base tooling (iptables/ipset/dig/gosu) \u2014 ported from Isag.
24392
+ RUN apt-get update && apt-get install -y --no-install-recommends \\
24393
+ ca-certificates curl git gosu iproute2 ipset iptables dnsutils jq \\
24394
+ && rm -rf /var/lib/apt/lists/*
24395
+
24396
+ # The worker (agent CLI).
24397
+ RUN npm install -g ${agentPkg}
24398
+
24399
+ # The guard (node9), pinned to the host version.
24400
+ RUN npm install -g node9-ai@${node9Version2}
24401
+
24402
+ # Non-root runtime user at uid 1000 (matches the typical single-user host so the
24403
+ # mounted ~/.claude / ~/.codex / project are read/writable). The node base image
24404
+ # already claims uid 1000 for the 'node' user \u2014 free it first (cf. Isag/ubuntu).
24405
+ RUN userdel -r node 2>/dev/null || true; \\
24406
+ userdel -r ubuntu 2>/dev/null || true; \\
24407
+ useradd --create-home --uid 1000 --shell /bin/bash ${RUN_AS_USER}
24408
+
24409
+ # Wire the agent's node9 hooks into the runtime user's home (build-time, static).
24410
+ RUN gosu ${RUN_AS_USER} node9 agents add ${config.agent} || true
24411
+
24412
+ RUN mkdir -p /workspace /etc/node9-sandbox \\
24413
+ && chown ${RUN_AS_USER}:${RUN_AS_USER} /workspace
24414
+
24415
+ COPY entrypoint.sh /usr/local/bin/entrypoint.sh
24416
+ RUN chmod +x /usr/local/bin/entrypoint.sh
24417
+ ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
24418
+ `;
24419
+ }
24420
+ function renderEntrypoint(config) {
24421
+ const agentBin = AGENT_BIN[config.agent];
24422
+ return `#!/usr/bin/env bash
24423
+ # Auto-generated by node9 sandbox. Seals the egress wall (root), then drops to the
24424
+ # non-root agent which starts the node9 daemon + execs the agent.
24425
+ set -Eeuo pipefail
24426
+
24427
+ DOMAINS_FILE="${ALLOWED_DOMAINS_PATH}"
24428
+ RUN_AS_USER="${RUN_AS_USER}"
24429
+
24430
+ [[ -s "$DOMAINS_FILE" ]] || { echo "entrypoint: missing/empty $DOMAINS_FILE" >&2; exit 1; }
24431
+
24432
+ # Own the mounted node9 data dir so the agent user can write audit there.
24433
+ mkdir -p "/home/$RUN_AS_USER/.node9"
24434
+ chown -R "$RUN_AS_USER:$RUN_AS_USER" "/home/$RUN_AS_USER/.node9" || true
24435
+
24436
+ # \u2500\u2500 Resolve the allowlist \u2192 ipset (union of every resolver, like Isag) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
24437
+ mapfile -t LOCAL_DNS < <(awk '/^nameserver / {print $2}' /etc/resolv.conf)
24438
+ [[ \${#LOCAL_DNS[@]} -gt 0 ]] || { echo "entrypoint: no resolvers in /etc/resolv.conf" >&2; exit 1; }
24439
+
24440
+ ipset create node9_allowed hash:ip family inet -exist
24441
+ ipset flush node9_allowed
24442
+
24443
+ while IFS= read -r domain; do
24444
+ [[ -n "$domain" ]] || continue
24445
+ found=0
24446
+ # union the local resolver + each upstream so CDN/anycast IP rotation is covered
24447
+ for ip in $(getent ahostsv4 "$domain" 2>/dev/null | awk '{print $1}' | sort -u); do
24448
+ ipset add node9_allowed "$ip" -exist; found=1
24449
+ done
24450
+ for r in "\${LOCAL_DNS[@]}"; do
24451
+ for ip in $(dig +short +time=2 +tries=1 @"$r" A "$domain" 2>/dev/null | awk '/^[0-9.]+$/'); do
24452
+ ipset add node9_allowed "$ip" -exist; found=1
24453
+ done
24454
+ done
24455
+ [[ $found -eq 1 ]] || { echo "entrypoint: failed to resolve $domain" >&2; exit 1; }
24456
+ echo "entrypoint: allowed $domain"
24457
+ done < "$DOMAINS_FILE"
24458
+
24459
+ # \u2500\u2500 Seal iptables: deny-by-default except lo, established, DNS, the allowlist \u2500\u2500\u2500\u2500
24460
+ echo "entrypoint: sealing firewall..."
24461
+ iptables -F; iptables -X
24462
+ iptables -P INPUT DROP
24463
+ iptables -P FORWARD DROP
24464
+ iptables -P OUTPUT DROP
24465
+ iptables -A INPUT -i lo -j ACCEPT
24466
+ iptables -A OUTPUT -o lo -j ACCEPT
24467
+ iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
24468
+ iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
24469
+ for r in "\${LOCAL_DNS[@]}"; do
24470
+ iptables -A OUTPUT -p udp -d "$r" --dport 53 -j ACCEPT
24471
+ iptables -A OUTPUT -p tcp -d "$r" --dport 53 -j ACCEPT
24472
+ done
24473
+ iptables -A OUTPUT -m set --match-set node9_allowed dst -j ACCEPT
24474
+
24475
+ # \u2500\u2500 Drop to the agent: start the node9 daemon (as the user), then exec the agent \u2500
24476
+ echo "entrypoint: starting node9 + ${agentBin} as $RUN_AS_USER"
24477
+ exec gosu "$RUN_AS_USER" bash -lc '
24478
+ set -e
24479
+ node9 daemon --background >/dev/null 2>&1 || true
24480
+ cd /workspace
24481
+ exec ${agentBin} "$@"
24482
+ ' -- "$@"
24483
+ `;
24484
+ }
24485
+
24486
+ // src/posture/egress.ts
24487
+ function sandboxEgressWallActive() {
24488
+ try {
24489
+ return fs48.existsSync(ALLOWED_DOMAINS_PATH);
24490
+ } catch {
24491
+ return false;
24492
+ }
24493
+ }
24188
24494
  function evaluateEgressConfig(egress) {
24189
24495
  if (egress.enabled && egress.mode === "block") {
24190
24496
  return {
@@ -24234,6 +24540,21 @@ function evaluateEgressConfig(egress) {
24234
24540
  };
24235
24541
  }
24236
24542
  function checkEgress(ctx) {
24543
+ if (sandboxEgressWallActive()) {
24544
+ return [
24545
+ {
24546
+ category: "Egress",
24547
+ severity: "advisory",
24548
+ title: "Egress is hard-blocked by the sandbox kernel wall",
24549
+ what: "Outbound is deny-by-default at the kernel; only the allowlist is reachable.",
24550
+ why: "The sandbox seals egress with an ipset/iptables wall before the agent starts.",
24551
+ who: "Even a compromised agent can only reach the allowlisted hosts.",
24552
+ owner: "node9",
24553
+ detail: [],
24554
+ coverage: { state: "covered", level: "block", via: "sandbox egress wall" }
24555
+ }
24556
+ ];
24557
+ }
24237
24558
  const config = getConfig(ctx.cwd);
24238
24559
  const egress = config.policy.egress;
24239
24560
  return [evaluateEgressConfig({ enabled: egress.enabled, mode: egress.mode })];
@@ -24275,25 +24596,25 @@ async function checkGate(ctx) {
24275
24596
 
24276
24597
  // src/posture/supply-chain.ts
24277
24598
  init_provenance();
24278
- import fs47 from "fs";
24279
- import os42 from "os";
24280
- import path48 from "path";
24599
+ import fs49 from "fs";
24600
+ import os43 from "os";
24601
+ import path49 from "path";
24281
24602
  import { parse as parseToml3 } from "smol-toml";
24282
24603
  var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpx", "bunx", "dlx", "yarn", "pnpm", "bun"]);
24283
24604
  function isNode9Managed(command, args = []) {
24284
24605
  if (!command) return false;
24285
- if (path48.basename(command).toLowerCase() === "node9") return true;
24286
- if (PACKAGE_RUNNERS.has(path48.basename(command).toLowerCase())) {
24287
- return args.some((a) => a === "node9" || path48.basename(a).toLowerCase() === "node9");
24606
+ if (path49.basename(command).toLowerCase() === "node9") return true;
24607
+ if (PACKAGE_RUNNERS.has(path49.basename(command).toLowerCase())) {
24608
+ return args.some((a) => a === "node9" || path49.basename(a).toLowerCase() === "node9");
24288
24609
  }
24289
24610
  return false;
24290
24611
  }
24291
24612
  var MAX_CONFIG_BYTES = 10 * 1024 * 1024;
24292
24613
  function readServers(file, format, agent) {
24293
24614
  try {
24294
- const stat = fs47.statSync(file);
24615
+ const stat = fs49.statSync(file);
24295
24616
  if (!stat.isFile() || stat.size > MAX_CONFIG_BYTES) return [];
24296
- const text = fs47.readFileSync(file, "utf8");
24617
+ const text = fs49.readFileSync(file, "utf8");
24297
24618
  const map = format === "toml" ? parseToml3(text)?.mcp_servers : JSON.parse(text)?.mcpServers;
24298
24619
  if (!map || typeof map !== "object") return [];
24299
24620
  return Object.entries(map).map(([name, v]) => ({
@@ -24307,7 +24628,7 @@ function readServers(file, format, agent) {
24307
24628
  }
24308
24629
  }
24309
24630
  function checkSupplyChain(ctx) {
24310
- const home = ctx.home || os42.homedir();
24631
+ const home = ctx.home || os43.homedir();
24311
24632
  const servers = [];
24312
24633
  for (const spec of AGENT_SPECS) {
24313
24634
  if (!spec.mcpFile) continue;
@@ -24389,11 +24710,12 @@ async function checkPrivilege(ctx) {
24389
24710
  }
24390
24711
 
24391
24712
  // src/posture/containment.ts
24392
- import fs48 from "fs";
24713
+ import fs50 from "fs";
24714
+ var ISOLATION_WEIGHT = 12;
24393
24715
  function inContainer() {
24394
- if (fs48.existsSync("/.dockerenv") || fs48.existsSync("/run/.containerenv")) return true;
24716
+ if (fs50.existsSync("/.dockerenv") || fs50.existsSync("/run/.containerenv")) return true;
24395
24717
  try {
24396
- const cgroup = fs48.readFileSync("/proc/1/cgroup", "utf8");
24718
+ const cgroup = fs50.readFileSync("/proc/1/cgroup", "utf8");
24397
24719
  if (/docker|kubepods|containerd|lxc|libpod/.test(cgroup)) return true;
24398
24720
  } catch {
24399
24721
  }
@@ -24412,14 +24734,28 @@ function checkContainment(_ctx) {
24412
24734
  detail: [],
24413
24735
  owner: "os",
24414
24736
  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" }
24737
+ // The single biggest hardening gap, and node9 now fully remedies it
24738
+ // (`node9 sandbox run`). Deducts while open; closing it is the headline
24739
+ // payoff. No coverageProbe → stays OPEN (scored) until adopted; live
24740
+ // partial-credit for the lighter shield path is a fast-follow.
24741
+ scoreWeight: ISOLATION_WEIGHT,
24742
+ gain: "jailed container \xB7 kernel egress wall \xB7 scoped mounts \xB7 governed inside",
24743
+ cost: "the agent works inside /workspace, not your live host",
24744
+ fix: `Two ways to shrink the blast radius \u2014 pick by how much flexibility you need:
24745
+ Strongest \u2014 jail it (closes this gap, +${ISOLATION_WEIGHT}):
24746
+ \u2022 node9 sandbox run <agent>
24747
+ Lighter \u2014 harden in place, keep full host access (about +${Math.round(
24748
+ ISOLATION_WEIGHT / 2
24749
+ )}):
24750
+ \u2022 node9 shield enable project-jail \u2014 block stray credential reads
24751
+ \u2022 node9 egress lock \u2014 block data exfil`
24417
24752
  }
24418
24753
  ];
24419
24754
  }
24420
24755
 
24421
24756
  // src/posture/inbound.ts
24422
- import fs49 from "fs";
24757
+ import fs51 from "fs";
24758
+ var DB_EXPOSURE_WEIGHT = 4;
24423
24759
  var KNOWN_SERVICE_PORTS = {
24424
24760
  5432: "PostgreSQL",
24425
24761
  6379: "Redis",
@@ -24505,7 +24841,7 @@ function collectListeners() {
24505
24841
  const byPort = /* @__PURE__ */ new Map();
24506
24842
  for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) {
24507
24843
  try {
24508
- for (const l of parseListeners(fs49.readFileSync(file, "utf8"))) {
24844
+ for (const l of parseListeners(fs51.readFileSync(file, "utf8"))) {
24509
24845
  if (!byPort.has(l.port)) byPort.set(l.port, l);
24510
24846
  }
24511
24847
  } catch {
@@ -24517,11 +24853,11 @@ function readProc(pid) {
24517
24853
  let comm = "unknown";
24518
24854
  let cmdline = "";
24519
24855
  try {
24520
- comm = fs49.readFileSync(`/proc/${pid}/comm`, "utf8").trim().replace(/-MainThread$/, "") || "unknown";
24856
+ comm = fs51.readFileSync(`/proc/${pid}/comm`, "utf8").trim().replace(/-MainThread$/, "") || "unknown";
24521
24857
  } catch {
24522
24858
  }
24523
24859
  try {
24524
- cmdline = fs49.readFileSync(`/proc/${pid}/cmdline`).toString().replace(/\0/g, " ").trim();
24860
+ cmdline = fs51.readFileSync(`/proc/${pid}/cmdline`).toString().replace(/\0/g, " ").trim();
24525
24861
  } catch {
24526
24862
  }
24527
24863
  return { comm, cmdline };
@@ -24531,21 +24867,21 @@ function resolveProcesses(inodes) {
24531
24867
  if (inodes.size === 0) return map;
24532
24868
  let pids;
24533
24869
  try {
24534
- pids = fs49.readdirSync("/proc").filter((d) => /^\d+$/.test(d));
24870
+ pids = fs51.readdirSync("/proc").filter((d) => /^\d+$/.test(d));
24535
24871
  } catch {
24536
24872
  return map;
24537
24873
  }
24538
24874
  for (const pid of pids) {
24539
24875
  let fds;
24540
24876
  try {
24541
- fds = fs49.readdirSync(`/proc/${pid}/fd`);
24877
+ fds = fs51.readdirSync(`/proc/${pid}/fd`);
24542
24878
  } catch {
24543
24879
  continue;
24544
24880
  }
24545
24881
  for (const fd of fds) {
24546
24882
  let link;
24547
24883
  try {
24548
- link = fs49.readlinkSync(`/proc/${pid}/fd/${fd}`);
24884
+ link = fs51.readlinkSync(`/proc/${pid}/fd/${fd}`);
24549
24885
  } catch {
24550
24886
  continue;
24551
24887
  }
@@ -24600,8 +24936,15 @@ function checkInbound(ctx) {
24600
24936
  // Only when node9 actually has a shield for an exposed service — otherwise
24601
24937
  // (bare dev servers) it stays purely the user's to rebind.
24602
24938
  node9Reduces: reduces,
24603
- fix,
24604
- coverageProbe: { kind: "cantFix" }
24939
+ // When a db-shield applies this is real, node9-addressable hardening → it
24940
+ // scores (and stays OPEN, no cantFix probe). Bare dev servers node9 can't
24941
+ // touch stay can't-fix / your-part / unscored.
24942
+ ...reduces ? {
24943
+ scoreWeight: DB_EXPOSURE_WEIGHT,
24944
+ gain: "blocks DROP TABLE / TRUNCATE / FLUSHALL on the exposed DB",
24945
+ cost: "you confirm legit destructive migrations"
24946
+ } : { coverageProbe: { kind: "cantFix" } },
24947
+ fix
24605
24948
  });
24606
24949
  }
24607
24950
  return findings;
@@ -24609,9 +24952,9 @@ function checkInbound(ctx) {
24609
24952
 
24610
24953
  // src/posture/coverage.ts
24611
24954
  init_config();
24612
- import os43 from "os";
24955
+ import os44 from "os";
24613
24956
  function checkCoverage(ctx) {
24614
- const home = ctx.home || os43.homedir();
24957
+ const home = ctx.home || os44.homedir();
24615
24958
  const findings = [];
24616
24959
  const protectedAgents = getAgentWiring(home).filter((r) => r.isProtected);
24617
24960
  if (protectedAgents.length === 0) {
@@ -24651,16 +24994,20 @@ function scorePosture(findings, checksRun) {
24651
24994
  const open = findings.filter(
24652
24995
  (f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix"
24653
24996
  );
24654
- const count = (sev) => open.filter((f) => f.severity === sev).length;
24655
- return computeSecurityScore({
24997
+ const count = (sev) => open.filter((f) => f.severity === sev && !f.scoreWeight).length;
24998
+ const base = computeSecurityScore({
24656
24999
  critical: count("critical"),
24657
25000
  high: count("high"),
24658
25001
  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
25002
  total: Math.max(checksRun, 1)
24663
25003
  });
25004
+ const headroom = open.reduce((sum, f) => sum + (f.scoreWeight ?? 0), 0);
25005
+ const score = Math.max(0, base.score - headroom);
25006
+ const tier = score >= 80 ? "good" : score >= 50 ? "at-risk" : "critical";
25007
+ return { score, tier };
25008
+ }
25009
+ function openHeadroom(findings) {
25010
+ return findings.filter((f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix").reduce((sum, f) => sum + (f.scoreWeight ?? 0), 0);
24664
25011
  }
24665
25012
 
24666
25013
  // src/posture/headline.ts
@@ -24815,7 +25162,7 @@ async function runChecks(checks, ctx) {
24815
25162
  }
24816
25163
  async function runPosture(opts = {}) {
24817
25164
  const ctx = {
24818
- home: opts.home ?? os44.homedir(),
25165
+ home: opts.home ?? os45.homedir(),
24819
25166
  cwd: opts.cwd ?? process.cwd(),
24820
25167
  agent: opts.agent
24821
25168
  };
@@ -24870,9 +25217,10 @@ var LABEL_WIDTH = 14;
24870
25217
  function label(category) {
24871
25218
  return chalk24.bold(category.padEnd(Math.max(LABEL_WIDTH, category.length + 1)));
24872
25219
  }
24873
- function renderFinding(f) {
25220
+ function renderFinding(f, showWeight = false) {
24874
25221
  const lines = [];
24875
- lines.push(` ${ICON[f.severity]} ${label(f.category)}${f.title}`);
25222
+ const wt = showWeight && f.scoreWeight ? chalk24.cyan.bold(`+${f.scoreWeight} `) : "";
25223
+ lines.push(` ${ICON[f.severity]} ${label(f.category)}${wt}${f.title}`);
24876
25224
  const indent = " ".repeat(2 + 3 + LABEL_WIDTH);
24877
25225
  const width = 80 - indent.length;
24878
25226
  for (const s of [f.what, f.why, f.who]) {
@@ -24888,6 +25236,16 @@ function renderFinding(f) {
24888
25236
  }
24889
25237
  }
24890
25238
  }
25239
+ const tradeoff = [
25240
+ [f.gain, "gain: ", chalk24.green],
25241
+ [f.cost, "cost: ", chalk24.yellow]
25242
+ ];
25243
+ for (const [text, lbl, color2] of tradeoff) {
25244
+ if (!text) continue;
25245
+ wrap(text, width - 6).forEach((l, i) => {
25246
+ lines.push(indent + (i === 0 ? color2(lbl) : " ") + chalk24.gray(l));
25247
+ });
25248
+ }
24891
25249
  return lines;
24892
25250
  }
24893
25251
  function renderPosture(result) {
@@ -24897,15 +25255,11 @@ function renderPosture(result) {
24897
25255
  lines.push(
24898
25256
  chalk24.cyan.bold(`\u{1F6E1}\uFE0F Node9 Posture`) + chalk24.gray(` \u2014 ${result.agent}`) + ` ${chalk24.bold(`Score: ${result.score}/100`)} (${tier})`
24899
25257
  );
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";
25258
+ const headroom = openHeadroom(result.findings);
25259
+ if (headroom > 0) {
24906
25260
  lines.push(
24907
25261
  " " + chalk24.gray(
24908
- `${advisories} ${word} below ${verb} affect the score \u2014 OS-level exposure node9 can't enforce, yours to weigh.`
25262
+ `${headroom} pts of headroom below \u2014 each layer adds security at some cost to flexibility. You choose which to close.`
24909
25263
  )
24910
25264
  );
24911
25265
  }
@@ -24921,7 +25275,7 @@ function renderPosture(result) {
24921
25275
  const covered = result.findings.filter((f) => f.coverage?.state === "covered");
24922
25276
  const open = result.findings.filter((f) => f.coverage?.state !== "covered");
24923
25277
  if (covered.length > 0) {
24924
- lines.push(" " + chalk24.green("\u{1F7E2} node9 is already protecting you"));
25278
+ lines.push(" " + chalk24.green("\u{1F7E2} ON NOW \u2014 node9 is enforcing these (your floor)"));
24925
25279
  for (const f of covered) {
24926
25280
  const gated = f.coverage?.level === "review" ? "approval-gating" : "blocking";
24927
25281
  const via = f.coverage?.via ?? "node9";
@@ -24936,18 +25290,16 @@ function renderPosture(result) {
24936
25290
  const osOpen = open.filter((f) => f.owner !== "node9" && !f.node9Reduces);
24937
25291
  if (node9Open.length > 0) {
24938
25292
  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));
25293
+ for (const f of node9Open) lines.push(...renderFinding(f, true));
24940
25294
  }
24941
25295
  if (reduceOpen.length > 0) {
24942
25296
  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));
25297
+ lines.push(" " + chalk24.yellow.bold("\u{1F512} AVAILABLE \u2014 turn on to harden (each has a tradeoff)"));
25298
+ for (const f of reduceOpen) lines.push(...renderFinding(f, true));
24947
25299
  }
24948
25300
  if (osOpen.length > 0) {
24949
25301
  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"));
25302
+ lines.push(" " + chalk24.bold("\u{1F9F1} YOUR PART \u2014 node9 can't fix these (OS-level)"));
24951
25303
  for (const f of osOpen) lines.push(...renderFinding(f));
24952
25304
  }
24953
25305
  for (const cat of result.passedCategories) {
@@ -25002,7 +25354,12 @@ function buildShipBody(result) {
25002
25354
  // The runnable fix / OS action — commands + advice, never a path.
25003
25355
  fix: f.fix,
25004
25356
  // Whose job it is. Default 'os' so the SaaS never falsely claims node9 can fix it.
25005
- owner: f.owner ?? "os"
25357
+ owner: f.owner ?? "os",
25358
+ // Hardening weight + the flexibility tradeoff (generic prose / a number —
25359
+ // no values or paths), so the fleet view can show the same headroom story.
25360
+ scoreWeight: f.scoreWeight,
25361
+ gain: f.gain,
25362
+ cost: f.cost
25006
25363
  }))
25007
25364
  };
25008
25365
  }
@@ -25075,9 +25432,9 @@ function registerPostureCommand(program2) {
25075
25432
  init_config();
25076
25433
  init_dist();
25077
25434
  import chalk26 from "chalk";
25078
- import fs50 from "fs";
25079
- import os45 from "os";
25080
- import path49 from "path";
25435
+ import fs52 from "fs";
25436
+ import os46 from "os";
25437
+ import path50 from "path";
25081
25438
  var DEFAULT_EGRESS = {
25082
25439
  enabled: false,
25083
25440
  mode: "review",
@@ -25086,12 +25443,12 @@ var DEFAULT_EGRESS = {
25086
25443
  allowPrivate: true
25087
25444
  };
25088
25445
  function configPath() {
25089
- return path49.join(os45.homedir(), ".node9", "config.json");
25446
+ return path50.join(os46.homedir(), ".node9", "config.json");
25090
25447
  }
25091
25448
  function readRawConfig() {
25092
25449
  let text;
25093
25450
  try {
25094
- text = fs50.readFileSync(configPath(), "utf8");
25451
+ text = fs52.readFileSync(configPath(), "utf8");
25095
25452
  } catch (err2) {
25096
25453
  if (err2.code === "ENOENT") return {};
25097
25454
  throw err2;
@@ -25106,8 +25463,8 @@ function readRawConfig() {
25106
25463
  }
25107
25464
  function writeRawConfig(config) {
25108
25465
  const p = configPath();
25109
- fs50.mkdirSync(path49.dirname(p), { recursive: true });
25110
- fs50.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
25466
+ fs52.mkdirSync(path50.dirname(p), { recursive: true });
25467
+ fs52.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
25111
25468
  }
25112
25469
  function applyEgress(config, change) {
25113
25470
  const policy = config.policy = config.policy ?? {};
@@ -25198,15 +25555,369 @@ function registerEgressCommand(program2) {
25198
25555
  egress.action(showStatus);
25199
25556
  }
25200
25557
 
25558
+ // src/cli/commands/sandbox.ts
25559
+ init_config();
25560
+ import chalk27 from "chalk";
25561
+ import fs55 from "fs";
25562
+ import path53 from "path";
25563
+ import { spawnSync as spawnSync6 } from "child_process";
25564
+
25565
+ // src/sandbox/config.ts
25566
+ import fs53 from "fs";
25567
+ import path51 from "path";
25568
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
25569
+ var SANDBOX_CONFIG_FILE = "node9.sandbox.yaml";
25570
+ var FORBIDDEN_ENV = /* @__PURE__ */ new Set(["NODE9_API_KEY", "NODE9_API_URL"]);
25571
+ function defaultSandboxConfig(agent) {
25572
+ return {
25573
+ agent,
25574
+ workspace: { mount: ".", target: "/workspace", mode: "rw" },
25575
+ runtime: { engine: "docker", image: "node9-sandbox:local", rebuild: "auto" },
25576
+ outbound: {
25577
+ mode: "block",
25578
+ 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"]
25579
+ },
25580
+ inbound: { expose: [] },
25581
+ // Provider key only — NODE9_API_KEY intentionally absent (fix #1).
25582
+ env: { pass: [agent === "codex" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY"] },
25583
+ // Terminal-only approval in the MVP; cloud/native/browser off (fix #1).
25584
+ node9: {
25585
+ approvals: { terminal: true, native: false, browser: false, cloud: false },
25586
+ // Mount the agent's OAuth/creds dir so it can authenticate in the box.
25587
+ mountAgentCredentials: true
25588
+ }
25589
+ };
25590
+ }
25591
+ function asStringArray(v) {
25592
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
25593
+ }
25594
+ function mergeSandboxConfig(raw, fallbackAgent) {
25595
+ const r = raw && typeof raw === "object" ? raw : {};
25596
+ const agent = typeof r.agent === "string" ? r.agent : fallbackAgent;
25597
+ if (agent !== "claude" && agent !== "codex") {
25598
+ throw new Error(`sandbox: unsupported agent "${String(agent)}" (use claude or codex)`);
25599
+ }
25600
+ const d = defaultSandboxConfig(agent);
25601
+ const ws = r.workspace ?? {};
25602
+ const rt = r.runtime ?? {};
25603
+ const out = r.outbound ?? {};
25604
+ const inb = r.inbound ?? {};
25605
+ const env = r.env ?? {};
25606
+ const n9 = r.node9 ?? {};
25607
+ const appr = n9.approvals ?? {};
25608
+ const pass = asStringArray(env.pass).filter((k) => !FORBIDDEN_ENV.has(k));
25609
+ return {
25610
+ agent,
25611
+ workspace: {
25612
+ mount: typeof ws.mount === "string" ? ws.mount : d.workspace.mount,
25613
+ target: typeof ws.target === "string" ? ws.target : d.workspace.target,
25614
+ mode: ws.mode === "ro" ? "ro" : "rw"
25615
+ },
25616
+ runtime: {
25617
+ engine: rt.engine === "podman" ? "podman" : "docker",
25618
+ image: typeof rt.image === "string" ? rt.image : d.runtime.image,
25619
+ rebuild: rt.rebuild === "never" || rt.rebuild === "always" ? rt.rebuild : d.runtime.rebuild
25620
+ },
25621
+ outbound: { mode: "block", allow: out.allow ? asStringArray(out.allow) : d.outbound.allow },
25622
+ inbound: { expose: inb.expose ? asStringArray(inb.expose) : d.inbound.expose },
25623
+ env: { pass: env.pass ? pass : d.env.pass },
25624
+ node9: {
25625
+ approvals: {
25626
+ terminal: appr.terminal !== false,
25627
+ native: appr.native === true,
25628
+ browser: appr.browser === true,
25629
+ cloud: appr.cloud === true
25630
+ },
25631
+ mountAgentCredentials: n9.mountAgentCredentials !== false
25632
+ }
25633
+ };
25634
+ }
25635
+ function scaffoldSandboxYaml(agent) {
25636
+ 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";
25637
+ return header + stringifyYaml(defaultSandboxConfig(agent));
25638
+ }
25639
+ function sandboxConfigPath(cwd = process.cwd()) {
25640
+ return path51.join(cwd, SANDBOX_CONFIG_FILE);
25641
+ }
25642
+ function loadSandboxConfig(cwd = process.cwd(), fallbackAgent = "claude") {
25643
+ const p = sandboxConfigPath(cwd);
25644
+ if (!fs53.existsSync(p)) {
25645
+ throw new Error(`sandbox: ${SANDBOX_CONFIG_FILE} not found \u2014 run \`node9 sandbox new\` first.`);
25646
+ }
25647
+ let raw;
25648
+ try {
25649
+ raw = parseYaml(fs53.readFileSync(p, "utf-8"));
25650
+ } catch (err2) {
25651
+ throw new Error(
25652
+ `sandbox: ${SANDBOX_CONFIG_FILE} is not valid YAML \u2014 ${err2.message}`
25653
+ );
25654
+ }
25655
+ return mergeSandboxConfig(raw, fallbackAgent);
25656
+ }
25657
+
25658
+ // src/sandbox/firewall.ts
25659
+ var AGENT_PROVIDER_HOST = {
25660
+ claude: ["api.anthropic.com"],
25661
+ codex: ["api.openai.com"]
25662
+ };
25663
+ var NODE9_SAAS_HOSTS = ["api.node9.ai", "app.node9.ai", "node9.ai"];
25664
+ function isValidHost2(host) {
25665
+ if (typeof host !== "string") return false;
25666
+ const h = host.trim().toLowerCase();
25667
+ if (!h || h.length > 253) return false;
25668
+ if (/[\s/:@?#\\]/.test(h)) return false;
25669
+ 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(
25670
+ h
25671
+ );
25672
+ }
25673
+ function compileAllowlist(input) {
25674
+ const norm = (h) => h.trim().toLowerCase();
25675
+ const denySet = /* @__PURE__ */ new Set([...input.configDeny.map(norm), ...NODE9_SAAS_HOSTS.map(norm)]);
25676
+ const candidates = [
25677
+ ...AGENT_PROVIDER_HOST[input.agent],
25678
+ ...input.sandboxAllow,
25679
+ ...input.configAllow
25680
+ ].map(norm);
25681
+ const allow = /* @__PURE__ */ new Set();
25682
+ const rejected = [];
25683
+ const denied = [];
25684
+ for (const host of candidates) {
25685
+ if (!host) continue;
25686
+ if (!isValidHost2(host)) {
25687
+ if (!rejected.includes(host)) rejected.push(host);
25688
+ continue;
25689
+ }
25690
+ if (denySet.has(host)) {
25691
+ if (!denied.includes(host)) denied.push(host);
25692
+ continue;
25693
+ }
25694
+ allow.add(host);
25695
+ }
25696
+ return {
25697
+ allow: [...allow].sort(),
25698
+ rejected: rejected.sort(),
25699
+ denied: denied.sort()
25700
+ };
25701
+ }
25702
+
25703
+ // src/sandbox/runtime.ts
25704
+ import fs54 from "fs";
25705
+ import os47 from "os";
25706
+ import path52 from "path";
25707
+ import crypto8 from "crypto";
25708
+ import { spawnSync as spawnSync5 } from "child_process";
25709
+ function sandboxDataDir(cwd = process.cwd()) {
25710
+ return path52.join(cwd, ".node9", "sandbox", "data");
25711
+ }
25712
+ function detectEngine(engine) {
25713
+ const r = spawnSync5(engine, ["--version"], { encoding: "utf-8" });
25714
+ if (r.status === 0 && typeof r.stdout === "string") {
25715
+ return { available: true, version: r.stdout.trim() };
25716
+ }
25717
+ return { available: false };
25718
+ }
25719
+ function agentCredentialsMount(agent) {
25720
+ const rel = agent === "codex" ? ".codex/auth.json" : ".claude/.credentials.json";
25721
+ return { hostPath: path52.join(os47.homedir(), rel), target: `/home/${RUN_AS_USER}/${rel}` };
25722
+ }
25723
+ function buildRunArgs(opts) {
25724
+ const { config, workspaceHostPath, dataHostPath, allowlistHostPath, agentArgs } = opts;
25725
+ const args = ["run", "--rm", "-it", "--cap-add=NET_ADMIN"];
25726
+ args.push("-v", `${workspaceHostPath}:${config.workspace.target}:${config.workspace.mode}`);
25727
+ args.push("-v", `${dataHostPath}:/home/${RUN_AS_USER}/.node9`);
25728
+ args.push("-v", `${allowlistHostPath}:${ALLOWED_DOMAINS_PATH}:ro`);
25729
+ if (config.node9.mountAgentCredentials) {
25730
+ const creds = agentCredentialsMount(config.agent);
25731
+ if (fs54.existsSync(creds.hostPath)) {
25732
+ args.push("-v", `${creds.hostPath}:${creds.target}`);
25733
+ }
25734
+ }
25735
+ for (const key of config.env.pass) {
25736
+ if (process.env[key] !== void 0) args.push("-e", key);
25737
+ }
25738
+ for (const port of config.inbound.expose) {
25739
+ args.push("-p", port);
25740
+ }
25741
+ args.push(config.runtime.image);
25742
+ if (agentArgs.length) args.push(...agentArgs);
25743
+ return args;
25744
+ }
25745
+ function imageContentHash(dockerfile, entrypoint) {
25746
+ return crypto8.createHash("sha256").update(dockerfile).update("\0").update(entrypoint).digest("hex").slice(0, 16);
25747
+ }
25748
+ function sandboxBuildDir(cwd = process.cwd()) {
25749
+ return path52.join(cwd, ".node9", "sandbox", "build");
25750
+ }
25751
+ function writeBuildContext(cwd, dockerfile, entrypoint) {
25752
+ const dir = sandboxBuildDir(cwd);
25753
+ fs54.mkdirSync(dir, { recursive: true });
25754
+ fs54.writeFileSync(path52.join(dir, "Dockerfile"), dockerfile);
25755
+ fs54.writeFileSync(path52.join(dir, "entrypoint.sh"), entrypoint);
25756
+ return dir;
25757
+ }
25758
+ function writeAllowlist(cwd, hosts) {
25759
+ const dir = path52.join(cwd, ".node9", "sandbox");
25760
+ fs54.mkdirSync(dir, { recursive: true });
25761
+ const p = path52.join(dir, "allowed-domains.txt");
25762
+ fs54.writeFileSync(p, hosts.join("\n") + "\n");
25763
+ return p;
25764
+ }
25765
+ function resolveHomePath(p) {
25766
+ return p.startsWith("~") ? path52.join(os47.homedir(), p.slice(1)) : path52.resolve(p);
25767
+ }
25768
+
25769
+ // src/cli/commands/sandbox.ts
25770
+ function seedDataDirConfig(dataDir, sandbox) {
25771
+ fs55.mkdirSync(dataDir, { recursive: true });
25772
+ const configPath2 = path53.join(dataDir, "config.json");
25773
+ const seed = {
25774
+ settings: {
25775
+ approvers: {
25776
+ terminal: sandbox.node9.approvals.terminal,
25777
+ native: false,
25778
+ browser: false,
25779
+ cloud: false
25780
+ }
25781
+ }
25782
+ };
25783
+ fs55.writeFileSync(configPath2, JSON.stringify(seed, null, 2), { mode: 384 });
25784
+ }
25785
+ function registerSandboxCommand(program2, version2) {
25786
+ const node9Version2 = pinnedNode9Version(version2);
25787
+ const cmd = program2.command("sandbox").description("Run an agent in a disposable, jailed container \u2014 governed + audited inside");
25788
+ cmd.command("new").description(`Scaffold ${SANDBOX_CONFIG_FILE} in this project`).option("--agent <agent>", "claude (default) or codex", "claude").action((opts) => {
25789
+ const agent = opts.agent === "codex" ? "codex" : "claude";
25790
+ const p = sandboxConfigPath();
25791
+ if (fs55.existsSync(p)) {
25792
+ console.log(
25793
+ chalk27.yellow(` ${SANDBOX_CONFIG_FILE} already exists \u2014 leaving it untouched.`)
25794
+ );
25795
+ return;
25796
+ }
25797
+ fs55.writeFileSync(p, scaffoldSandboxYaml(agent));
25798
+ console.log(
25799
+ chalk27.green(` \u2713 wrote ${SANDBOX_CONFIG_FILE}`) + chalk27.dim(` (agent: ${agent})`)
25800
+ );
25801
+ console.log(
25802
+ chalk27.dim(" Edit it (mounts / allow / expose), then: ") + chalk27.cyan("node9 sandbox run")
25803
+ );
25804
+ });
25805
+ 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) => {
25806
+ const cwd = process.cwd();
25807
+ const sandbox = loadSandboxConfig(cwd, agentArg || "claude");
25808
+ if (agentArg === "claude" || agentArg === "codex") sandbox.agent = agentArg;
25809
+ const engine = detectEngine(sandbox.runtime.engine);
25810
+ if (!engine.available) {
25811
+ console.error(
25812
+ chalk27.red(` ${sandbox.runtime.engine} not found.`) + chalk27.dim(` Install it first \u2014 node9 sandbox needs a container runtime.`)
25813
+ );
25814
+ process.exit(1);
25815
+ }
25816
+ const node9Config = getConfig(cwd);
25817
+ const compiled = compileAllowlist({
25818
+ agent: sandbox.agent,
25819
+ sandboxAllow: sandbox.outbound.allow,
25820
+ configAllow: node9Config.policy.egress.allow,
25821
+ configDeny: node9Config.policy.egress.deny
25822
+ });
25823
+ if (compiled.rejected.length) {
25824
+ console.log(
25825
+ chalk27.yellow(` \u26A0 ignoring invalid allow hosts: ${compiled.rejected.join(", ")}`)
25826
+ );
25827
+ }
25828
+ if (compiled.denied.length) {
25829
+ console.log(chalk27.dim(` (denied: ${compiled.denied.join(", ")})`));
25830
+ }
25831
+ const allowlistPath = writeAllowlist(cwd, compiled.allow);
25832
+ const dockerfile = renderDockerfile(sandbox, node9Version2);
25833
+ const entrypoint = renderEntrypoint(sandbox);
25834
+ const buildDir = writeBuildContext(cwd, dockerfile, entrypoint);
25835
+ const hash = imageContentHash(dockerfile, entrypoint);
25836
+ const image = sandbox.runtime.image;
25837
+ const hashFile = path53.join(sandboxBuildDir(cwd), ".image-hash");
25838
+ const lastHash = fs55.existsSync(hashFile) ? fs55.readFileSync(hashFile, "utf-8").trim() : "";
25839
+ const imageExists = spawnSync6(sandbox.runtime.engine, ["image", "inspect", image], { stdio: "ignore" }).status === 0;
25840
+ const needBuild = sandbox.runtime.rebuild === "always" || !imageExists || sandbox.runtime.rebuild !== "never" && lastHash !== hash;
25841
+ if (needBuild) {
25842
+ console.log(chalk27.dim(` building ${image} \u2026`));
25843
+ const b = spawnSync6(sandbox.runtime.engine, ["build", "-t", image, buildDir], {
25844
+ stdio: "inherit"
25845
+ });
25846
+ if (b.status !== 0) {
25847
+ console.error(chalk27.red(" build failed."));
25848
+ process.exit(b.status ?? 1);
25849
+ }
25850
+ fs55.writeFileSync(hashFile, hash);
25851
+ }
25852
+ const dataDir = sandboxDataDir(cwd);
25853
+ seedDataDirConfig(dataDir, sandbox);
25854
+ const passthru = command.args.slice(agentArg ? 1 : 0);
25855
+ const runArgs = buildRunArgs({
25856
+ config: sandbox,
25857
+ workspaceHostPath: resolveHomePath(sandbox.workspace.mount),
25858
+ dataHostPath: dataDir,
25859
+ allowlistHostPath: allowlistPath,
25860
+ agentArgs: passthru
25861
+ });
25862
+ if (sandbox.node9.mountAgentCredentials) {
25863
+ const creds = agentCredentialsMount(sandbox.agent);
25864
+ if (fs55.existsSync(creds.hostPath)) {
25865
+ console.log(chalk27.dim(` mounting ${creds.hostPath} (agent credentials, rw)`));
25866
+ } else {
25867
+ console.log(
25868
+ chalk27.yellow(` \u26A0 ${creds.hostPath} not found \u2014 `) + chalk27.dim(`the agent must auth via an env key in env.pass.`)
25869
+ );
25870
+ }
25871
+ }
25872
+ console.log(
25873
+ chalk27.green(` \u{1F6E1}\uFE0F ${sandbox.agent} jailed \u2014 ${compiled.allow.length} hosts allowed
25874
+ `)
25875
+ );
25876
+ const r = spawnSync6(sandbox.runtime.engine, runArgs, { stdio: "inherit" });
25877
+ process.exit(r.status ?? 0);
25878
+ });
25879
+ cmd.command("tail").description("Stream the sandbox's audit log (host-side)").action(() => {
25880
+ const auditPath = path53.join(sandboxDataDir(), "audit.log");
25881
+ if (!fs55.existsSync(auditPath)) {
25882
+ console.log(chalk27.dim(" no sandbox audit yet."));
25883
+ return;
25884
+ }
25885
+ spawnSync6("tail", ["-f", auditPath], { stdio: "inherit" });
25886
+ });
25887
+ cmd.command("logs").description("Dump the sandbox's audit log").action(() => {
25888
+ const auditPath = path53.join(sandboxDataDir(), "audit.log");
25889
+ if (!fs55.existsSync(auditPath)) {
25890
+ console.log(chalk27.dim(" no sandbox audit yet."));
25891
+ return;
25892
+ }
25893
+ process.stdout.write(fs55.readFileSync(auditPath, "utf-8"));
25894
+ });
25895
+ cmd.command("clean").description("Remove the sandbox image, build context, and data").action(() => {
25896
+ const cwd = process.cwd();
25897
+ let sandbox = null;
25898
+ try {
25899
+ sandbox = loadSandboxConfig(cwd);
25900
+ } catch {
25901
+ }
25902
+ if (sandbox) {
25903
+ spawnSync6(sandbox.runtime.engine, ["image", "rm", "-f", sandbox.runtime.image], {
25904
+ stdio: "ignore"
25905
+ });
25906
+ }
25907
+ fs55.rmSync(path53.join(cwd, ".node9", "sandbox"), { recursive: true, force: true });
25908
+ console.log(chalk27.green(" \u2713 sandbox image + build + data removed."));
25909
+ });
25910
+ }
25911
+
25201
25912
  // src/cli/commands/sessions.ts
25202
25913
  init_scan_summary();
25203
25914
  init_litellm();
25204
25915
  init_cost_gemini();
25205
25916
  init_cost_codex();
25206
- import chalk27 from "chalk";
25207
- import fs51 from "fs";
25208
- import path50 from "path";
25209
- import os46 from "os";
25917
+ import chalk28 from "chalk";
25918
+ import fs56 from "fs";
25919
+ import path54 from "path";
25920
+ import os48 from "os";
25210
25921
  function modelPrice(model) {
25211
25922
  const t = pricingFor(model);
25212
25923
  if (!t) return null;
@@ -25223,10 +25934,10 @@ function encodeProjectPath(projectPath) {
25223
25934
  }
25224
25935
  function sessionJsonlPath(projectPath, sessionId) {
25225
25936
  const encoded = encodeProjectPath(projectPath);
25226
- return path50.join(os46.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
25937
+ return path54.join(os48.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
25227
25938
  }
25228
25939
  function projectLabel(projectPath) {
25229
- return projectPath.replace(os46.homedir(), "~");
25940
+ return projectPath.replace(os48.homedir(), "~");
25230
25941
  }
25231
25942
  function parseHistoryLines(lines) {
25232
25943
  const entries = [];
@@ -25295,10 +26006,10 @@ function parseSessionLines(lines) {
25295
26006
  return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
25296
26007
  }
25297
26008
  function loadAuditEntries(auditPath) {
25298
- const aPath = auditPath ?? path50.join(os46.homedir(), ".node9", "audit.log");
26009
+ const aPath = auditPath ?? path54.join(os48.homedir(), ".node9", "audit.log");
25299
26010
  let raw;
25300
26011
  try {
25301
- raw = fs51.readFileSync(aPath, "utf-8");
26012
+ raw = fs56.readFileSync(aPath, "utf-8");
25302
26013
  } catch {
25303
26014
  return [];
25304
26015
  }
@@ -25334,8 +26045,8 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
25334
26045
  return result;
25335
26046
  }
25336
26047
  function buildGeminiSessions(days, allAuditEntries) {
25337
- const tmpDir = path50.join(os46.homedir(), ".gemini", "tmp");
25338
- if (!fs51.existsSync(tmpDir)) return [];
26048
+ const tmpDir = path54.join(os48.homedir(), ".gemini", "tmp");
26049
+ if (!fs56.existsSync(tmpDir)) return [];
25339
26050
  const cutoff = days !== null ? (() => {
25340
26051
  const d = /* @__PURE__ */ new Date();
25341
26052
  d.setDate(d.getDate() - days);
@@ -25344,35 +26055,35 @@ function buildGeminiSessions(days, allAuditEntries) {
25344
26055
  })() : null;
25345
26056
  let slugDirs;
25346
26057
  try {
25347
- slugDirs = fs51.readdirSync(tmpDir);
26058
+ slugDirs = fs56.readdirSync(tmpDir);
25348
26059
  } catch {
25349
26060
  return [];
25350
26061
  }
25351
26062
  const summaries = [];
25352
26063
  for (const slug of slugDirs) {
25353
- const slugPath = path50.join(tmpDir, slug);
26064
+ const slugPath = path54.join(tmpDir, slug);
25354
26065
  try {
25355
- if (!fs51.statSync(slugPath).isDirectory()) continue;
26066
+ if (!fs56.statSync(slugPath).isDirectory()) continue;
25356
26067
  } catch {
25357
26068
  continue;
25358
26069
  }
25359
- let projectRoot = path50.join(os46.homedir(), slug);
26070
+ let projectRoot = path54.join(os48.homedir(), slug);
25360
26071
  try {
25361
- projectRoot = fs51.readFileSync(path50.join(slugPath, ".project_root"), "utf-8").trim();
26072
+ projectRoot = fs56.readFileSync(path54.join(slugPath, ".project_root"), "utf-8").trim();
25362
26073
  } catch {
25363
26074
  }
25364
- const chatsDir = path50.join(slugPath, "chats");
25365
- if (!fs51.existsSync(chatsDir)) continue;
26075
+ const chatsDir = path54.join(slugPath, "chats");
26076
+ if (!fs56.existsSync(chatsDir)) continue;
25366
26077
  let chatFiles;
25367
26078
  try {
25368
- chatFiles = fs51.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
26079
+ chatFiles = fs56.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
25369
26080
  } catch {
25370
26081
  continue;
25371
26082
  }
25372
26083
  for (const chatFile of chatFiles) {
25373
26084
  let raw;
25374
26085
  try {
25375
- raw = fs51.readFileSync(path50.join(chatsDir, chatFile), "utf-8");
26086
+ raw = fs56.readFileSync(path54.join(chatsDir, chatFile), "utf-8");
25376
26087
  } catch {
25377
26088
  continue;
25378
26089
  }
@@ -25452,8 +26163,8 @@ function buildGeminiSessions(days, allAuditEntries) {
25452
26163
  return summaries;
25453
26164
  }
25454
26165
  function buildCodexSessions(days, allAuditEntries) {
25455
- const sessionsBase = path50.join(os46.homedir(), ".codex", "sessions");
25456
- if (!fs51.existsSync(sessionsBase)) return [];
26166
+ const sessionsBase = path54.join(os48.homedir(), ".codex", "sessions");
26167
+ if (!fs56.existsSync(sessionsBase)) return [];
25457
26168
  const cutoff = days !== null ? (() => {
25458
26169
  const d = /* @__PURE__ */ new Date();
25459
26170
  d.setDate(d.getDate() - days);
@@ -25462,29 +26173,29 @@ function buildCodexSessions(days, allAuditEntries) {
25462
26173
  })() : null;
25463
26174
  const jsonlFiles = [];
25464
26175
  try {
25465
- for (const year of fs51.readdirSync(sessionsBase)) {
25466
- const yearPath = path50.join(sessionsBase, year);
26176
+ for (const year of fs56.readdirSync(sessionsBase)) {
26177
+ const yearPath = path54.join(sessionsBase, year);
25467
26178
  try {
25468
- if (!fs51.statSync(yearPath).isDirectory()) continue;
26179
+ if (!fs56.statSync(yearPath).isDirectory()) continue;
25469
26180
  } catch {
25470
26181
  continue;
25471
26182
  }
25472
- for (const month of fs51.readdirSync(yearPath)) {
25473
- const monthPath = path50.join(yearPath, month);
26183
+ for (const month of fs56.readdirSync(yearPath)) {
26184
+ const monthPath = path54.join(yearPath, month);
25474
26185
  try {
25475
- if (!fs51.statSync(monthPath).isDirectory()) continue;
26186
+ if (!fs56.statSync(monthPath).isDirectory()) continue;
25476
26187
  } catch {
25477
26188
  continue;
25478
26189
  }
25479
- for (const day of fs51.readdirSync(monthPath)) {
25480
- const dayPath = path50.join(monthPath, day);
26190
+ for (const day of fs56.readdirSync(monthPath)) {
26191
+ const dayPath = path54.join(monthPath, day);
25481
26192
  try {
25482
- if (!fs51.statSync(dayPath).isDirectory()) continue;
26193
+ if (!fs56.statSync(dayPath).isDirectory()) continue;
25483
26194
  } catch {
25484
26195
  continue;
25485
26196
  }
25486
- for (const file of fs51.readdirSync(dayPath)) {
25487
- if (file.endsWith(".jsonl")) jsonlFiles.push(path50.join(dayPath, file));
26197
+ for (const file of fs56.readdirSync(dayPath)) {
26198
+ if (file.endsWith(".jsonl")) jsonlFiles.push(path54.join(dayPath, file));
25488
26199
  }
25489
26200
  }
25490
26201
  }
@@ -25496,7 +26207,7 @@ function buildCodexSessions(days, allAuditEntries) {
25496
26207
  for (const filePath of jsonlFiles) {
25497
26208
  let lines;
25498
26209
  try {
25499
- lines = fs51.readFileSync(filePath, "utf-8").split("\n");
26210
+ lines = fs56.readFileSync(filePath, "utf-8").split("\n");
25500
26211
  } catch {
25501
26212
  continue;
25502
26213
  }
@@ -25582,10 +26293,10 @@ function buildCodexSessions(days, allAuditEntries) {
25582
26293
  return summaries;
25583
26294
  }
25584
26295
  function buildSessions(days, historyPath) {
25585
- const hPath = historyPath ?? path50.join(os46.homedir(), ".claude", "history.jsonl");
26296
+ const hPath = historyPath ?? path54.join(os48.homedir(), ".claude", "history.jsonl");
25586
26297
  let historyRaw = "";
25587
26298
  try {
25588
- historyRaw = fs51.readFileSync(hPath, "utf-8");
26299
+ historyRaw = fs56.readFileSync(hPath, "utf-8");
25589
26300
  } catch {
25590
26301
  }
25591
26302
  const cutoff = days !== null ? (() => {
@@ -25609,7 +26320,7 @@ function buildSessions(days, historyPath) {
25609
26320
  const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
25610
26321
  let sessionLines = [];
25611
26322
  try {
25612
- sessionLines = fs51.readFileSync(jsonlFile, "utf-8").split("\n");
26323
+ sessionLines = fs56.readFileSync(jsonlFile, "utf-8").split("\n");
25613
26324
  } catch {
25614
26325
  }
25615
26326
  const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
@@ -25695,11 +26406,11 @@ function toolInputSummary(tool, input) {
25695
26406
  }
25696
26407
  function toolColor(tool) {
25697
26408
  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;
26409
+ if (t === "bash" || t === "execute_bash") return chalk28.red;
26410
+ if (t === "write") return chalk28.green;
26411
+ if (t === "edit" || t === "notebookedit") return chalk28.yellow;
26412
+ if (t === "read") return chalk28.cyan;
26413
+ return chalk28.gray;
25703
26414
  }
25704
26415
  function barStr2(value, max, width) {
25705
26416
  if (max === 0 || width <= 0) return "\u2591".repeat(width);
@@ -25709,7 +26420,7 @@ function barStr2(value, max, width) {
25709
26420
  function colorBar2(value, max, width) {
25710
26421
  const s = barStr2(value, max, width);
25711
26422
  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));
26423
+ return chalk28.cyan(s.slice(0, filled)) + chalk28.dim(s.slice(filled));
25713
26424
  }
25714
26425
  function renderSummary(summaries) {
25715
26426
  const totalTools = summaries.reduce((n, s) => n + s.toolCalls.length, 0);
@@ -25739,45 +26450,45 @@ function renderSummary(summaries) {
25739
26450
  }
25740
26451
  const topProjects = [...projCosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
25741
26452
  const W = 20;
25742
- console.log(chalk27.dim(" " + "\u2500".repeat(70)));
26453
+ console.log(chalk28.dim(" " + "\u2500".repeat(70)));
25743
26454
  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") : "")
26455
+ " " + 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
26456
  );
25746
26457
  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`)
26458
+ " " + 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
26459
  );
25749
26460
  console.log("");
25750
- console.log(" " + chalk27.dim("Tool breakdown:"));
26461
+ console.log(" " + chalk28.dim("Tool breakdown:"));
25751
26462
  const maxGroup = Math.max(...Object.values(groups));
25752
26463
  for (const [label2, count] of Object.entries(groups)) {
25753
26464
  if (count === 0) continue;
25754
26465
  const pct = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
25755
26466
  console.log(
25756
- " " + label2.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk27.white(String(count).padStart(4)) + chalk27.dim(` (${String(pct)}%)`)
26467
+ " " + label2.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk28.white(String(count).padStart(4)) + chalk28.dim(` (${String(pct)}%)`)
25757
26468
  );
25758
26469
  }
25759
26470
  console.log("");
25760
26471
  if (topProjects.length > 1) {
25761
- console.log(" " + chalk27.dim("Cost by project:"));
26472
+ console.log(" " + chalk28.dim("Cost by project:"));
25762
26473
  const maxProjCost = topProjects[0][1];
25763
26474
  for (const [proj, cost] of topProjects) {
25764
26475
  console.log(
25765
- " " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " + chalk27.yellow(fmtCost3(cost))
26476
+ " " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " + chalk28.yellow(fmtCost3(cost))
25766
26477
  );
25767
26478
  }
25768
26479
  console.log("");
25769
26480
  }
25770
- console.log(chalk27.dim(" " + "\u2500".repeat(70)));
26481
+ console.log(chalk28.dim(" " + "\u2500".repeat(70)));
25771
26482
  console.log("");
25772
26483
  }
25773
26484
  function renderList(summaries, totalCost) {
25774
26485
  if (summaries.length === 0) {
25775
- console.log(chalk27.yellow(" No sessions found in the requested range.\n"));
26486
+ console.log(chalk28.yellow(" No sessions found in the requested range.\n"));
25776
26487
  return;
25777
26488
  }
25778
- const totalLabel = totalCost > 0 ? chalk27.dim(" ~" + fmtCost3(totalCost) + " total") : "";
26489
+ const totalLabel = totalCost > 0 ? chalk28.dim(" ~" + fmtCost3(totalCost) + " total") : "";
25779
26490
  console.log(
25780
- " " + chalk27.white(String(summaries.length)) + chalk27.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
26491
+ " " + chalk28.white(String(summaries.length)) + chalk28.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
25781
26492
  );
25782
26493
  console.log("");
25783
26494
  let lastGroup = "";
@@ -25785,51 +26496,51 @@ function renderList(summaries, totalCost) {
25785
26496
  const activeDate = fmtDate2(s.lastActiveTime);
25786
26497
  const group = activeDate + " " + s.projectLabel;
25787
26498
  if (group !== lastGroup) {
25788
- console.log(chalk27.dim(" \u2500\u2500\u2500 ") + chalk27.bold(activeDate) + chalk27.dim(" " + s.projectLabel));
26499
+ console.log(chalk28.dim(" \u2500\u2500\u2500 ") + chalk28.bold(activeDate) + chalk28.dim(" " + s.projectLabel));
25789
26500
  lastGroup = group;
25790
26501
  }
25791
26502
  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")](
26503
+ const dateRange = startDate !== activeDate ? chalk28.dim(" (" + startDate + " \u2192 " + activeDate + ")") : "";
26504
+ const timeStr = chalk28.dim(fmtTime(s.startTime));
26505
+ const prompt = chalk28.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
26506
+ const tools = s.toolCalls.length > 0 ? chalk28.dim(String(s.toolCalls.length).padStart(3) + " tools") : chalk28.dim(" 0 tools");
26507
+ const cost = s.costUSD > 0 ? chalk28.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
26508
+ const blocked = s.blockedCalls.length > 0 ? chalk28.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
26509
+ const snap = s.hasSnapshot ? chalk28.green(" \u{1F4F8}") : "";
26510
+ const agentBadge = chalk28[agentColorName(s.agent ?? "claude")](
25800
26511
  " " + agentBadgeText(s.agent ?? "claude", 0)
25801
26512
  );
25802
- const sid = chalk27.dim(" " + s.sessionId.slice(0, 8));
26513
+ const sid = chalk28.dim(" " + s.sessionId.slice(0, 8));
25803
26514
  console.log(
25804
26515
  ` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}${dateRange}`
25805
26516
  );
25806
26517
  }
25807
26518
  console.log("");
25808
26519
  console.log(
25809
- chalk27.dim(" Run") + " " + chalk27.cyan("node9 sessions --detail <session-id>") + chalk27.dim(" for full tool trace.")
26520
+ chalk28.dim(" Run") + " " + chalk28.cyan("node9 sessions --detail <session-id>") + chalk28.dim(" for full tool trace.")
25810
26521
  );
25811
26522
  console.log("");
25812
26523
  }
25813
26524
  function renderDetail(s) {
25814
26525
  console.log("");
25815
- console.log(chalk27.bold(" Session ") + chalk27.dim(s.sessionId));
26526
+ console.log(chalk28.bold(" Session ") + chalk28.dim(s.sessionId));
25816
26527
  console.log(
25817
- chalk27.bold(" Prompt ") + chalk27.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
26528
+ chalk28.bold(" Prompt ") + chalk28.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
25818
26529
  );
25819
- console.log(chalk27.bold(" Project ") + chalk27.white(s.projectLabel));
26530
+ console.log(chalk28.bold(" Project ") + chalk28.white(s.projectLabel));
25820
26531
  if (s.agent) {
25821
- const agentLabel2 = chalk27[agentColorName(s.agent)](agentDisplayName(s.agent));
25822
- console.log(chalk27.bold(" Agent ") + agentLabel2);
26532
+ const agentLabel2 = chalk28[agentColorName(s.agent)](agentDisplayName(s.agent));
26533
+ console.log(chalk28.bold(" Agent ") + agentLabel2);
25823
26534
  }
25824
- console.log(chalk27.bold(" When ") + chalk27.white(fmtDateTime(s.startTime)));
26535
+ console.log(chalk28.bold(" When ") + chalk28.white(fmtDateTime(s.startTime)));
25825
26536
  if (s.costUSD > 0)
25826
- console.log(chalk27.bold(" Cost ") + chalk27.yellow("~" + fmtCost3(s.costUSD)));
26537
+ console.log(chalk28.bold(" Cost ") + chalk28.yellow("~" + fmtCost3(s.costUSD)));
25827
26538
  console.log(
25828
- chalk27.bold(" Snapshot ") + (s.hasSnapshot ? chalk27.green("\u2713 taken") : chalk27.dim("none"))
26539
+ chalk28.bold(" Snapshot ") + (s.hasSnapshot ? chalk28.green("\u2713 taken") : chalk28.dim("none"))
25829
26540
  );
25830
26541
  console.log("");
25831
26542
  if (s.toolCalls.length === 0 && s.blockedCalls.length === 0) {
25832
- console.log(chalk27.dim(" No tool calls recorded.\n"));
26543
+ console.log(chalk28.dim(" No tool calls recorded.\n"));
25833
26544
  return;
25834
26545
  }
25835
26546
  const timeline = [
@@ -25842,32 +26553,32 @@ function renderDetail(s) {
25842
26553
  });
25843
26554
  const headerParts = [`Tool calls (${s.toolCalls.length})`];
25844
26555
  if (s.blockedCalls.length > 0)
25845
- headerParts.push(chalk27.red(`${s.blockedCalls.length} blocked by node9`));
25846
- console.log(chalk27.bold(" " + headerParts.join(" \xB7 ")));
26556
+ headerParts.push(chalk28.red(`${s.blockedCalls.length} blocked by node9`));
26557
+ console.log(chalk28.bold(" " + headerParts.join(" \xB7 ")));
25847
26558
  console.log("");
25848
26559
  for (const entry of timeline) {
25849
26560
  if (entry.kind === "tool") {
25850
26561
  const tc = entry.tc;
25851
26562
  const colorFn = toolColor(tc.tool);
25852
26563
  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) + " ") : " ";
26564
+ const detail = chalk28.gray(truncate(toolInputSummary(tc.tool, tc.input), 70));
26565
+ const ts = tc.timestamp ? chalk28.dim(fmtTime(tc.timestamp) + " ") : " ";
25855
26566
  console.log(` ${ts}${toolPad} ${detail}`);
25856
26567
  } else {
25857
26568
  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) : "";
26569
+ const ts = bc.timestamp ? chalk28.dim(fmtTime(bc.timestamp) + " ") : " ";
26570
+ const label2 = chalk28.red("\u{1F6D1} BLOCKED".padEnd(16));
26571
+ const toolName = chalk28.red(bc.tool.padEnd(10));
26572
+ const argsSummary = bc.args ? chalk28.gray(truncate(toolInputSummary(bc.tool, bc.args), 40)) : chalk28.dim("[args not logged]");
26573
+ const reason = bc.checkedBy ? chalk28.dim(" \u2190 " + bc.checkedBy) : "";
25863
26574
  console.log(` ${ts}${label2} ${toolName} ${argsSummary}${reason}`);
25864
26575
  }
25865
26576
  }
25866
26577
  console.log("");
25867
26578
  if (s.modifiedFiles.length > 0) {
25868
- console.log(chalk27.bold(` Files modified (${s.modifiedFiles.length}):`));
26579
+ console.log(chalk28.bold(` Files modified (${s.modifiedFiles.length}):`));
25869
26580
  for (const f of s.modifiedFiles) {
25870
- console.log(" " + chalk27.yellow(f));
26581
+ console.log(" " + chalk28.yellow(f));
25871
26582
  }
25872
26583
  console.log("");
25873
26584
  }
@@ -25875,13 +26586,13 @@ function renderDetail(s) {
25875
26586
  function registerSessionsCommand(program2) {
25876
26587
  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
26588
  console.log("");
25878
- console.log(chalk27.cyan.bold("\u{1F4CB} node9 sessions") + chalk27.dim(" \u2014 what your AI agent did"));
26589
+ console.log(chalk28.cyan.bold("\u{1F4CB} node9 sessions") + chalk28.dim(" \u2014 what your AI agent did"));
25879
26590
  console.log("");
25880
26591
  const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
25881
26592
  const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
25882
- console.log(chalk27.dim(" " + rangeLabel));
26593
+ console.log(chalk28.dim(" " + rangeLabel));
25883
26594
  console.log("");
25884
- process.stdout.write(chalk27.dim(" Loading\u2026"));
26595
+ process.stdout.write(chalk28.dim(" Loading\u2026"));
25885
26596
  const summaries = buildSessions(days);
25886
26597
  if (process.stdout.isTTY) {
25887
26598
  process.stdout.clearLine(0);
@@ -25894,8 +26605,8 @@ function registerSessionsCommand(program2) {
25894
26605
  (s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
25895
26606
  );
25896
26607
  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"));
26608
+ console.log(chalk28.red(` Session not found: ${options.detail}`));
26609
+ console.log(chalk28.dim(" Run `node9 sessions` to list recent sessions.\n"));
25899
26610
  return;
25900
26611
  }
25901
26612
  renderDetail(target);
@@ -25909,7 +26620,7 @@ function registerSessionsCommand(program2) {
25909
26620
 
25910
26621
  // src/cli/commands/session-taint.ts
25911
26622
  init_daemon();
25912
- import chalk28 from "chalk";
26623
+ import chalk29 from "chalk";
25913
26624
  function resolveSessionId(records, query) {
25914
26625
  const exact = records.find((r) => r.sessionId === query);
25915
26626
  if (exact) return { record: exact };
@@ -25935,22 +26646,22 @@ function registerSessionTaintCommand(program2) {
25935
26646
  const records = await listSessionTaints();
25936
26647
  console.log("");
25937
26648
  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");
26649
+ console.log(chalk29.dim(" No tainted sessions."));
26650
+ console.log(chalk29.dim(" (Taint lives in the daemon \u2014 a stopped daemon has none.)") + "\n");
25940
26651
  return;
25941
26652
  }
25942
26653
  console.log(
25943
- " " + chalk28.bold(String(records.length)) + chalk28.dim(` tainted session${records.length !== 1 ? "s" : ""}`)
26654
+ " " + chalk29.bold(String(records.length)) + chalk29.dim(` tainted session${records.length !== 1 ? "s" : ""}`)
25944
26655
  );
25945
26656
  console.log("");
25946
26657
  for (const r of records) {
25947
26658
  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))
26659
+ " " + chalk29.yellow(r.sessionId.slice(0, 8).padEnd(10)) + chalk29.red(r.source) + sourceGap(r.source) + chalk29.dim("clears in " + fmtRemaining(r.expiresAt))
25949
26660
  );
25950
26661
  }
25951
26662
  console.log("");
25952
26663
  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"
26664
+ 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
26665
  );
25955
26666
  });
25956
26667
  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 +26669,32 @@ function registerSessionTaintCommand(program2) {
25958
26669
  if (opts.all) {
25959
26670
  const res2 = await clearSessionTaint({ all: true });
25960
26671
  if (res2.daemonUnavailable) {
25961
- console.log(chalk28.dim(" node9 daemon not running \u2014 no active taints to clear.") + "\n");
26672
+ console.log(chalk29.dim(" node9 daemon not running \u2014 no active taints to clear.") + "\n");
25962
26673
  return;
25963
26674
  }
25964
26675
  console.log(
25965
- chalk28.green(" \u2713 ") + `Cleared ${chalk28.bold(String(res2.cleared))} session taint${res2.cleared !== 1 ? "s" : ""}.
26676
+ chalk29.green(" \u2713 ") + `Cleared ${chalk29.bold(String(res2.cleared))} session taint${res2.cleared !== 1 ? "s" : ""}.
25966
26677
  `
25967
26678
  );
25968
26679
  return;
25969
26680
  }
25970
26681
  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");
26682
+ console.log(chalk29.red(" Provide a session id or --all."));
26683
+ console.log(chalk29.dim(" Run `node9 session-taint list` to see tainted sessions.") + "\n");
25973
26684
  return;
25974
26685
  }
25975
26686
  const records = await listSessionTaints();
25976
26687
  if (records.length === 0) {
25977
- console.log(chalk28.dim(" No tainted sessions \u2014 nothing to clear.") + "\n");
26688
+ console.log(chalk29.dim(" No tainted sessions \u2014 nothing to clear.") + "\n");
25978
26689
  return;
25979
26690
  }
25980
26691
  const resolved = resolveSessionId(records, sessionId);
25981
26692
  if ("error" in resolved) {
25982
26693
  if (resolved.error === "not-found") {
25983
- console.log(chalk28.red(` No tainted session matches "${sessionId}".`));
26694
+ console.log(chalk29.red(` No tainted session matches "${sessionId}".`));
25984
26695
  } else {
25985
- console.log(chalk28.red(` "${sessionId}" is ambiguous \u2014 matches:`));
25986
- for (const m of resolved.matches) console.log(chalk28.dim(" " + m));
26696
+ console.log(chalk29.red(` "${sessionId}" is ambiguous \u2014 matches:`));
26697
+ for (const m of resolved.matches) console.log(chalk29.dim(" " + m));
25987
26698
  }
25988
26699
  console.log("");
25989
26700
  return;
@@ -25991,24 +26702,24 @@ function registerSessionTaintCommand(program2) {
25991
26702
  const res = await clearSessionTaint({ sessionId: resolved.record.sessionId });
25992
26703
  if (res.cleared > 0) {
25993
26704
  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"
26705
+ chalk29.green(" \u2713 ") + `Cleared taint for ${chalk29.yellow(resolved.record.sessionId.slice(0, 8))} ` + chalk29.dim(`(was flagged by ${resolved.record.source}).`) + "\n"
25995
26706
  );
25996
26707
  } else {
25997
26708
  console.log(
25998
- chalk28.dim(` Session ${resolved.record.sessionId.slice(0, 8)} was already clear.`) + "\n"
26709
+ chalk29.dim(` Session ${resolved.record.sessionId.slice(0, 8)} was already clear.`) + "\n"
25999
26710
  );
26000
26711
  }
26001
26712
  });
26002
26713
  }
26003
26714
 
26004
26715
  // 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";
26716
+ import chalk30 from "chalk";
26717
+ import fs57 from "fs";
26718
+ import os49 from "os";
26719
+ import path55 from "path";
26009
26720
  function wipeSkillSessions() {
26010
26721
  try {
26011
- fs52.rmSync(path51.join(os47.homedir(), ".node9", "skill-sessions"), {
26722
+ fs57.rmSync(path55.join(os49.homedir(), ".node9", "skill-sessions"), {
26012
26723
  recursive: true,
26013
26724
  force: true
26014
26725
  });
@@ -26022,29 +26733,29 @@ function registerSkillPinCommand(program2) {
26022
26733
  const result = readSkillPinsSafe();
26023
26734
  if (!result.ok) {
26024
26735
  if (result.reason === "missing") {
26025
- console.log(chalk29.gray("\nNo skill roots are pinned yet."));
26736
+ console.log(chalk30.gray("\nNo skill roots are pinned yet."));
26026
26737
  console.log(
26027
- chalk29.gray("Pins are created automatically on the first tool call of each session.\n")
26738
+ chalk30.gray("Pins are created automatically on the first tool call of each session.\n")
26028
26739
  );
26029
26740
  return;
26030
26741
  }
26031
- console.error(chalk29.red(`
26742
+ console.error(chalk30.red(`
26032
26743
  \u274C Pin file is corrupt: ${result.detail}`));
26033
- console.error(chalk29.yellow(" Run: node9 skill pin reset\n"));
26744
+ console.error(chalk30.yellow(" Run: node9 skill pin reset\n"));
26034
26745
  process.exit(1);
26035
26746
  }
26036
26747
  const entries = Object.entries(result.pins.roots);
26037
26748
  if (entries.length === 0) {
26038
- console.log(chalk29.gray("\nNo skill roots are pinned yet.\n"));
26749
+ console.log(chalk30.gray("\nNo skill roots are pinned yet.\n"));
26039
26750
  return;
26040
26751
  }
26041
- console.log(chalk29.bold("\n\u{1F512} Pinned Skill Roots\n"));
26752
+ console.log(chalk30.bold("\n\u{1F512} Pinned Skill Roots\n"));
26042
26753
  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}`);
26754
+ const missing = entry.exists ? "" : chalk30.yellow(" (not present at pin time)");
26755
+ console.log(` ${chalk30.cyan(key)} ${chalk30.gray(entry.rootPath)}${missing}`);
26045
26756
  console.log(` Files (${entry.fileCount})`);
26046
- console.log(` Hash: ${chalk29.gray(entry.contentHash.slice(0, 16))}...`);
26047
- console.log(` Pinned: ${chalk29.gray(entry.pinnedAt)}
26757
+ console.log(` Hash: ${chalk30.gray(entry.contentHash.slice(0, 16))}...`);
26758
+ console.log(` Pinned: ${chalk30.gray(entry.pinnedAt)}
26048
26759
  `);
26049
26760
  }
26050
26761
  });
@@ -26053,52 +26764,52 @@ function registerSkillPinCommand(program2) {
26053
26764
  try {
26054
26765
  pins = readSkillPins();
26055
26766
  } catch {
26056
- console.error(chalk29.red("\n\u274C Pin file is corrupt."));
26057
- console.error(chalk29.yellow(" Run: node9 skill pin reset\n"));
26767
+ console.error(chalk30.red("\n\u274C Pin file is corrupt."));
26768
+ console.error(chalk30.yellow(" Run: node9 skill pin reset\n"));
26058
26769
  process.exit(1);
26059
26770
  }
26060
26771
  if (!pins.roots[rootKey]) {
26061
- console.error(chalk29.red(`
26772
+ console.error(chalk30.red(`
26062
26773
  \u274C No pin found for root key "${rootKey}"
26063
26774
  `));
26064
- console.error(`Run ${chalk29.cyan("node9 skill pin list")} to see pinned roots.
26775
+ console.error(`Run ${chalk30.cyan("node9 skill pin list")} to see pinned roots.
26065
26776
  `);
26066
26777
  process.exit(1);
26067
26778
  }
26068
26779
  const rootPath = pins.roots[rootKey].rootPath;
26069
26780
  removePin2(rootKey);
26070
26781
  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"));
26782
+ console.log(chalk30.green(`
26783
+ \u{1F513} Pin removed for ${chalk30.cyan(rootKey)}`));
26784
+ console.log(chalk30.gray(` ${rootPath}`));
26785
+ console.log(chalk30.gray(" Next session will re-pin with current state.\n"));
26075
26786
  });
26076
26787
  pinSubCmd.command("reset").description("Clear all skill pins and wipe session verification flags").action(() => {
26077
26788
  const result = readSkillPinsSafe();
26078
26789
  if (!result.ok && result.reason === "missing") {
26079
26790
  wipeSkillSessions();
26080
- console.log(chalk29.gray("\nNo pins to clear.\n"));
26791
+ console.log(chalk30.gray("\nNo pins to clear.\n"));
26081
26792
  return;
26082
26793
  }
26083
26794
  const count = result.ok ? Object.keys(result.pins.roots).length : "?";
26084
26795
  clearAllPins2();
26085
26796
  wipeSkillSessions();
26086
- console.log(chalk29.green(`
26797
+ console.log(chalk30.green(`
26087
26798
  \u{1F513} Cleared ${count} skill pin(s).`));
26088
- console.log(chalk29.gray(" Next session will re-pin with current state.\n"));
26799
+ console.log(chalk30.gray(" Next session will re-pin with current state.\n"));
26089
26800
  });
26090
26801
  }
26091
26802
 
26092
26803
  // 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");
26804
+ import fs58 from "fs";
26805
+ import os50 from "os";
26806
+ import path56 from "path";
26807
+ import chalk31 from "chalk";
26808
+ var DECISIONS_FILE2 = path56.join(os50.homedir(), ".node9", "decisions.json");
26098
26809
  function readDecisions() {
26099
26810
  try {
26100
- if (!fs53.existsSync(DECISIONS_FILE2)) return {};
26101
- const raw = fs53.readFileSync(DECISIONS_FILE2, "utf-8");
26811
+ if (!fs58.existsSync(DECISIONS_FILE2)) return {};
26812
+ const raw = fs58.readFileSync(DECISIONS_FILE2, "utf-8");
26102
26813
  const parsed = JSON.parse(raw);
26103
26814
  const out = {};
26104
26815
  for (const [k, v] of Object.entries(parsed)) {
@@ -26110,11 +26821,11 @@ function readDecisions() {
26110
26821
  }
26111
26822
  }
26112
26823
  function writeDecisions(d) {
26113
- const dir = path52.dirname(DECISIONS_FILE2);
26114
- if (!fs53.existsSync(dir)) fs53.mkdirSync(dir, { recursive: true });
26824
+ const dir = path56.dirname(DECISIONS_FILE2);
26825
+ if (!fs58.existsSync(dir)) fs58.mkdirSync(dir, { recursive: true });
26115
26826
  const tmp = `${DECISIONS_FILE2}.${process.pid}.tmp`;
26116
- fs53.writeFileSync(tmp, JSON.stringify(d, null, 2));
26117
- fs53.renameSync(tmp, DECISIONS_FILE2);
26827
+ fs58.writeFileSync(tmp, JSON.stringify(d, null, 2));
26828
+ fs58.renameSync(tmp, DECISIONS_FILE2);
26118
26829
  }
26119
26830
  function registerDecisionsCommand(program2) {
26120
26831
  const cmd = program2.command("decisions").description('Manage persistent "Always Allow" / "Always Deny" tool decisions');
@@ -26122,67 +26833,67 @@ function registerDecisionsCommand(program2) {
26122
26833
  const decisions = readDecisions();
26123
26834
  const entries = Object.entries(decisions);
26124
26835
  if (entries.length === 0) {
26125
- console.log(chalk30.gray(" No persistent decisions stored."));
26836
+ console.log(chalk31.gray(" No persistent decisions stored."));
26126
26837
  console.log(
26127
- chalk30.gray(` File: ${DECISIONS_FILE2}
26128
- `) + chalk30.gray(' Decisions are written when you click "Always Allow" or')
26838
+ chalk31.gray(` File: ${DECISIONS_FILE2}
26839
+ `) + chalk31.gray(' Decisions are written when you click "Always Allow" or')
26129
26840
  );
26130
- console.log(chalk30.gray(' "Always Deny" in node9 tail or the native popup.'));
26841
+ console.log(chalk31.gray(' "Always Deny" in node9 tail or the native popup.'));
26131
26842
  return;
26132
26843
  }
26133
- console.log(chalk30.bold(`
26844
+ console.log(chalk31.bold(`
26134
26845
  Persistent decisions (${entries.length})
26135
26846
  `));
26136
26847
  const w = Math.max(...entries.map(([k]) => k.length));
26137
26848
  for (const [tool, verdict] of entries.sort()) {
26138
- const colored = verdict === "allow" ? chalk30.green(verdict) : chalk30.red(verdict);
26849
+ const colored = verdict === "allow" ? chalk31.green(verdict) : chalk31.red(verdict);
26139
26850
  console.log(` ${tool.padEnd(w)} ${colored}`);
26140
26851
  }
26141
26852
  console.log(
26142
- chalk30.gray(`
26853
+ chalk31.gray(`
26143
26854
  Stored in ${DECISIONS_FILE2}
26144
- `) + chalk30.gray(" Run `node9 decisions clear <tool>` to remove an entry.")
26855
+ `) + chalk31.gray(" Run `node9 decisions clear <tool>` to remove an entry.")
26145
26856
  );
26146
26857
  });
26147
26858
  cmd.command("clear <toolName>").description("Remove a persistent decision for one tool").action((toolName) => {
26148
26859
  const decisions = readDecisions();
26149
26860
  if (!(toolName in decisions)) {
26150
- console.log(chalk30.yellow(` No persistent decision for "${toolName}". Nothing to clear.`));
26861
+ console.log(chalk31.yellow(` No persistent decision for "${toolName}". Nothing to clear.`));
26151
26862
  process.exitCode = 1;
26152
26863
  return;
26153
26864
  }
26154
26865
  delete decisions[toolName];
26155
26866
  writeDecisions(decisions);
26156
- console.log(chalk30.green(` \u2713 Cleared persistent decision for "${toolName}".`));
26867
+ console.log(chalk31.green(` \u2713 Cleared persistent decision for "${toolName}".`));
26157
26868
  });
26158
26869
  cmd.command("clear-all").description("Remove every persistent decision (irreversible)").action(() => {
26159
26870
  const decisions = readDecisions();
26160
26871
  const count = Object.keys(decisions).length;
26161
26872
  if (count === 0) {
26162
- console.log(chalk30.gray(" Nothing to clear \u2014 no persistent decisions stored."));
26873
+ console.log(chalk31.gray(" Nothing to clear \u2014 no persistent decisions stored."));
26163
26874
  return;
26164
26875
  }
26165
26876
  writeDecisions({});
26166
26877
  console.log(
26167
- chalk30.green(` \u2713 Cleared ${count} persistent decision${count === 1 ? "" : "s"}.`)
26878
+ chalk31.green(` \u2713 Cleared ${count} persistent decision${count === 1 ? "" : "s"}.`)
26168
26879
  );
26169
26880
  });
26170
26881
  }
26171
26882
 
26172
26883
  // 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");
26884
+ import chalk32 from "chalk";
26885
+ import fs59 from "fs";
26886
+ import path57 from "path";
26887
+ import os51 from "os";
26888
+ var AUDIT_LOG = path57.join(os51.homedir(), ".node9", "audit.log");
26889
+ var RESOLVED_FILE = path57.join(os51.homedir(), ".node9", "dlp-resolved.json");
26179
26890
  var ANSI_RE = /\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g;
26180
26891
  function stripAnsi(s) {
26181
26892
  return s.replace(ANSI_RE, "");
26182
26893
  }
26183
26894
  function loadResolved() {
26184
26895
  try {
26185
- const raw = JSON.parse(fs54.readFileSync(RESOLVED_FILE, "utf-8"));
26896
+ const raw = JSON.parse(fs59.readFileSync(RESOLVED_FILE, "utf-8"));
26186
26897
  return new Set(raw);
26187
26898
  } catch {
26188
26899
  return /* @__PURE__ */ new Set();
@@ -26190,13 +26901,13 @@ function loadResolved() {
26190
26901
  }
26191
26902
  function saveResolved(resolved) {
26192
26903
  try {
26193
- fs54.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
26904
+ fs59.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
26194
26905
  } catch {
26195
26906
  }
26196
26907
  }
26197
26908
  function loadDlpFindings() {
26198
- if (!fs54.existsSync(AUDIT_LOG)) return [];
26199
- return fs54.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
26909
+ if (!fs59.existsSync(AUDIT_LOG)) return [];
26910
+ return fs59.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
26200
26911
  if (!line.trim()) return [];
26201
26912
  try {
26202
26913
  const e = JSON.parse(line);
@@ -26225,14 +26936,14 @@ function registerDlpCommand(program2) {
26225
26936
  cmd.command("resolve").description("Mark all current DLP findings as resolved").action(() => {
26226
26937
  const findings = loadDlpFindings();
26227
26938
  if (findings.length === 0) {
26228
- console.log(chalk31.green("\n \u2705 No response-DLP findings to resolve.\n"));
26939
+ console.log(chalk32.green("\n \u2705 No response-DLP findings to resolve.\n"));
26229
26940
  return;
26230
26941
  }
26231
26942
  const resolved = loadResolved();
26232
26943
  for (const e of findings) resolved.add(entryKey2(e));
26233
26944
  saveResolved(resolved);
26234
26945
  console.log(
26235
- chalk31.green(
26946
+ chalk32.green(
26236
26947
  `
26237
26948
  \u2705 ${findings.length} finding${findings.length !== 1 ? "s" : ""} marked as resolved.
26238
26949
  `
@@ -26246,47 +26957,47 @@ function registerDlpCommand(program2) {
26246
26957
  const resolvedCount = findings.length - open.length;
26247
26958
  console.log("");
26248
26959
  console.log(
26249
- chalk31.bold.cyan("\u{1F510} node9 dlp") + chalk31.dim(" \u2014 secrets found in Claude response text")
26960
+ chalk32.bold.cyan("\u{1F510} node9 dlp") + chalk32.dim(" \u2014 secrets found in Claude response text")
26250
26961
  );
26251
26962
  console.log("");
26252
26963
  if (open.length === 0) {
26253
26964
  if (resolvedCount > 0) {
26254
- console.log(chalk31.green(` \u2705 No open findings \xB7 ${resolvedCount} previously resolved`));
26965
+ console.log(chalk32.green(` \u2705 No open findings \xB7 ${resolvedCount} previously resolved`));
26255
26966
  } else {
26256
26967
  console.log(
26257
- chalk31.green(" \u2705 No findings \u2014 Claude has not leaked secrets in response text")
26968
+ chalk32.green(" \u2705 No findings \u2014 Claude has not leaked secrets in response text")
26258
26969
  );
26259
26970
  }
26260
26971
  console.log("");
26261
26972
  return;
26262
26973
  }
26263
26974
  console.log(
26264
- chalk31.bgRed.white.bold(` \u26A0\uFE0F ${open.length} open finding${open.length !== 1 ? "s" : ""} `) + chalk31.dim(resolvedCount > 0 ? ` (${resolvedCount} resolved)` : "")
26975
+ chalk32.bgRed.white.bold(` \u26A0\uFE0F ${open.length} open finding${open.length !== 1 ? "s" : ""} `) + chalk32.dim(resolvedCount > 0 ? ` (${resolvedCount} resolved)` : "")
26265
26976
  );
26266
26977
  console.log("");
26267
26978
  console.log(
26268
- chalk31.dim(" These secrets were included in Claude's response text \u2014 NOT blocked.")
26979
+ chalk32.dim(" These secrets were included in Claude's response text \u2014 NOT blocked.")
26269
26980
  );
26270
- console.log(chalk31.dim(" Rotate each affected key immediately.\n"));
26981
+ console.log(chalk32.dim(" Rotate each affected key immediately.\n"));
26271
26982
  for (const e of open) {
26272
26983
  console.log(
26273
- " " + chalk31.red("\u25CF") + " " + chalk31.white(e.dlpPattern ?? "Secret") + chalk31.dim(" " + fmtDate3(e.ts))
26984
+ " " + chalk32.red("\u25CF") + " " + chalk32.white(e.dlpPattern ?? "Secret") + chalk32.dim(" " + fmtDate3(e.ts))
26274
26985
  );
26275
26986
  if (e.dlpSample) {
26276
- console.log(" " + chalk31.dim("Sample: ") + chalk31.yellow(stripAnsi(e.dlpSample)));
26987
+ console.log(" " + chalk32.dim("Sample: ") + chalk32.yellow(stripAnsi(e.dlpSample)));
26277
26988
  }
26278
26989
  if (e.project) {
26279
- console.log(" " + chalk31.dim("Project: ") + chalk31.dim(stripAnsi(e.project)));
26990
+ console.log(" " + chalk32.dim("Project: ") + chalk32.dim(stripAnsi(e.project)));
26280
26991
  }
26281
26992
  console.log("");
26282
26993
  }
26283
- console.log(" " + chalk31.bold("Next steps:"));
26284
- console.log(" " + chalk31.cyan("1.") + " Rotate any exposed keys shown above");
26994
+ console.log(" " + chalk32.bold("Next steps:"));
26995
+ console.log(" " + chalk32.cyan("1.") + " Rotate any exposed keys shown above");
26285
26996
  console.log(
26286
- " " + chalk31.cyan("2.") + " Run " + chalk31.white("node9 dlp resolve") + " to acknowledge"
26997
+ " " + chalk32.cyan("2.") + " Run " + chalk32.white("node9 dlp resolve") + " to acknowledge"
26287
26998
  );
26288
26999
  console.log(
26289
- " " + chalk31.cyan("3.") + " Run " + chalk31.white("node9 report") + " for full audit history"
27000
+ " " + chalk32.cyan("3.") + " Run " + chalk32.white("node9 report") + " for full audit history"
26290
27001
  );
26291
27002
  console.log("");
26292
27003
  });
@@ -26294,15 +27005,15 @@ function registerDlpCommand(program2) {
26294
27005
 
26295
27006
  // src/cli/commands/mask.ts
26296
27007
  init_dlp();
26297
- import chalk32 from "chalk";
26298
- import fs55 from "fs";
26299
- import path54 from "path";
26300
- import os50 from "os";
27008
+ import chalk33 from "chalk";
27009
+ import fs60 from "fs";
27010
+ import path58 from "path";
27011
+ import os52 from "os";
26301
27012
  function findJsonlFiles(dir) {
26302
27013
  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);
27014
+ if (!fs60.existsSync(dir)) return results;
27015
+ for (const entry of fs60.readdirSync(dir, { withFileTypes: true })) {
27016
+ const full = path58.join(dir, entry.name);
26306
27017
  if (entry.isDirectory()) results.push(...findJsonlFiles(full));
26307
27018
  else if (entry.isFile() && entry.name.endsWith(".jsonl")) results.push(full);
26308
27019
  }
@@ -26345,7 +27056,7 @@ function redactJson(obj) {
26345
27056
  function processFile(filePath, dryRun) {
26346
27057
  let raw;
26347
27058
  try {
26348
- raw = fs55.readFileSync(filePath, "utf-8");
27059
+ raw = fs60.readFileSync(filePath, "utf-8");
26349
27060
  } catch {
26350
27061
  return { redactedLines: 0, patterns: [] };
26351
27062
  }
@@ -26377,14 +27088,14 @@ function processFile(filePath, dryRun) {
26377
27088
  }
26378
27089
  }
26379
27090
  if (!dryRun && redactedLines > 0) {
26380
- fs55.writeFileSync(filePath, newLines.join("\n"), "utf-8");
27091
+ fs60.writeFileSync(filePath, newLines.join("\n"), "utf-8");
26381
27092
  }
26382
27093
  return { redactedLines, patterns };
26383
27094
  }
26384
27095
  function processJsonFile(filePath, dryRun) {
26385
27096
  let raw;
26386
27097
  try {
26387
- raw = fs55.readFileSync(filePath, "utf-8");
27098
+ raw = fs60.readFileSync(filePath, "utf-8");
26388
27099
  } catch {
26389
27100
  return { redactedLines: 0, patterns: [] };
26390
27101
  }
@@ -26397,15 +27108,15 @@ function processJsonFile(filePath, dryRun) {
26397
27108
  const { value, modified, found } = redactJson(parsed);
26398
27109
  if (!modified) return { redactedLines: 0, patterns: [] };
26399
27110
  if (!dryRun) {
26400
- fs55.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
27111
+ fs60.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
26401
27112
  }
26402
27113
  return { redactedLines: 1, patterns: found };
26403
27114
  }
26404
27115
  function findJsonFiles(dir) {
26405
27116
  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);
27117
+ if (!fs60.existsSync(dir)) return results;
27118
+ for (const entry of fs60.readdirSync(dir, { withFileTypes: true })) {
27119
+ const full = path58.join(dir, entry.name);
26409
27120
  if (entry.isDirectory()) results.push(...findJsonFiles(full));
26410
27121
  else if (entry.isFile() && entry.name.endsWith(".json")) results.push(full);
26411
27122
  }
@@ -26414,9 +27125,9 @@ function findJsonFiles(dir) {
26414
27125
  function registerMaskCommand(program2) {
26415
27126
  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
27127
  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");
27128
+ const home = os52.homedir();
27129
+ const claudeDir = path58.join(home, ".claude", "projects");
27130
+ const geminiDir = path58.join(home, ".gemini", "tmp");
26420
27131
  const allFiles = [
26421
27132
  ...findJsonlFiles(claudeDir).map((p) => ({ path: p, type: "jsonl" })),
26422
27133
  ...findJsonFiles(geminiDir).map((p) => ({ path: p, type: "json" }))
@@ -26424,18 +27135,18 @@ function registerMaskCommand(program2) {
26424
27135
  const cutoff = options.all ? null : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
26425
27136
  const filtered = cutoff ? allFiles.filter((f) => {
26426
27137
  try {
26427
- return fs55.statSync(f.path).mtime >= cutoff;
27138
+ return fs60.statSync(f.path).mtime >= cutoff;
26428
27139
  } catch {
26429
27140
  return false;
26430
27141
  }
26431
27142
  }) : allFiles;
26432
27143
  if (filtered.length === 0) {
26433
- console.log(chalk32.yellow(" No session files found."));
27144
+ console.log(chalk33.yellow(" No session files found."));
26434
27145
  return;
26435
27146
  }
26436
27147
  console.log("");
26437
27148
  if (dryRun) {
26438
- console.log(chalk32.dim(" Dry run \u2014 no files will be modified.\n"));
27149
+ console.log(chalk33.dim(" Dry run \u2014 no files will be modified.\n"));
26439
27150
  }
26440
27151
  let totalFiles = 0;
26441
27152
  let totalLines = 0;
@@ -26451,23 +27162,23 @@ function registerMaskCommand(program2) {
26451
27162
  });
26452
27163
  const verb = dryRun ? "Would redact" : "Redacted";
26453
27164
  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" : ""})`)
27165
+ " " + chalk33.dim(shortPath.slice(0, 60).padEnd(62)) + chalk33.red(`${verb}: `) + chalk33.yellow(patterns.join(", ")) + chalk33.dim(` (${redactedLines} line${redactedLines !== 1 ? "s" : ""})`)
26455
27166
  );
26456
27167
  }
26457
27168
  }
26458
27169
  console.log("");
26459
27170
  if (totalFiles === 0) {
26460
- console.log(chalk32.green(" No secrets found in session history."));
27171
+ console.log(chalk33.green(" No secrets found in session history."));
26461
27172
  } else {
26462
27173
  const verb = dryRun ? "would be modified" : "modified";
26463
27174
  console.log(
26464
- chalk32.bold(` ${totalFiles} file${totalFiles !== 1 ? "s" : ""} ${verb}`) + chalk32.dim(`, ${totalLines} line${totalLines !== 1 ? "s" : ""} redacted`)
27175
+ chalk33.bold(` ${totalFiles} file${totalFiles !== 1 ? "s" : ""} ${verb}`) + chalk33.dim(`, ${totalLines} line${totalLines !== 1 ? "s" : ""} redacted`)
26465
27176
  );
26466
- console.log(" Patterns: " + chalk32.yellow(totalPatterns.join(", ")));
27177
+ console.log(" Patterns: " + chalk33.yellow(totalPatterns.join(", ")));
26467
27178
  if (!dryRun) {
26468
27179
  console.log("");
26469
27180
  console.log(
26470
- chalk32.dim(
27181
+ chalk33.dim(
26471
27182
  " 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
27183
  )
26473
27184
  );
@@ -26480,20 +27191,20 @@ function registerMaskCommand(program2) {
26480
27191
  // src/cli.ts
26481
27192
  init_blast();
26482
27193
  var { version } = JSON.parse(
26483
- fs58.readFileSync(path57.join(__dirname, "../package.json"), "utf-8")
27194
+ fs63.readFileSync(path61.join(__dirname, "../package.json"), "utf-8")
26484
27195
  );
26485
27196
  var program = new Command();
26486
27197
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
26487
27198
  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
27199
  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 });
27200
+ const credPath = path61.join(os55.homedir(), ".node9", "credentials.json");
27201
+ if (!fs63.existsSync(path61.dirname(credPath)))
27202
+ fs63.mkdirSync(path61.dirname(credPath), { recursive: true });
26492
27203
  const profileName = options.profile || "default";
26493
27204
  let existingCreds = {};
26494
27205
  try {
26495
- if (fs58.existsSync(credPath)) {
26496
- const raw = JSON.parse(fs58.readFileSync(credPath, "utf-8"));
27206
+ if (fs63.existsSync(credPath)) {
27207
+ const raw = JSON.parse(fs63.readFileSync(credPath, "utf-8"));
26497
27208
  if (raw.apiKey) {
26498
27209
  existingCreds = {
26499
27210
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
@@ -26505,14 +27216,14 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
26505
27216
  } catch {
26506
27217
  }
26507
27218
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
26508
- fs58.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
27219
+ fs63.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
26509
27220
  let effectiveCloud = null;
26510
27221
  if (profileName === "default") {
26511
- const configPath2 = path57.join(os53.homedir(), ".node9", "config.json");
27222
+ const configPath2 = path61.join(os55.homedir(), ".node9", "config.json");
26512
27223
  let config = {};
26513
27224
  try {
26514
- if (fs58.existsSync(configPath2))
26515
- config = JSON.parse(fs58.readFileSync(configPath2, "utf-8"));
27225
+ if (fs63.existsSync(configPath2))
27226
+ config = JSON.parse(fs63.readFileSync(configPath2, "utf-8"));
26516
27227
  } catch {
26517
27228
  }
26518
27229
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -26527,35 +27238,35 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
26527
27238
  approvers.cloud = false;
26528
27239
  }
26529
27240
  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 });
27241
+ if (!fs63.existsSync(path61.dirname(configPath2)))
27242
+ fs63.mkdirSync(path61.dirname(configPath2), { recursive: true });
27243
+ fs63.writeFileSync(configPath2, JSON.stringify(config, null, 2), { mode: 384 });
26533
27244
  effectiveCloud = approvers.cloud === true;
26534
27245
  }
26535
27246
  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`));
27247
+ console.log(chalk35.green(`\u2705 Profile "${profileName}" saved`));
27248
+ console.log(chalk35.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
26538
27249
  } 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.`));
27250
+ console.log(chalk35.green(`\u2705 Key saved \u2014 Privacy mode \u{1F6E1}\uFE0F`));
27251
+ console.log(chalk35.gray(` All decisions stay on this machine. Nothing syncs to the cloud.`));
26541
27252
  if (!options.local) {
26542
27253
  console.log(
26543
- chalk34.yellow(` Your config has cloud approvals OFF (settings.approvers.cloud).`)
27254
+ chalk35.yellow(` Your config has cloud approvals OFF (settings.approvers.cloud).`)
26544
27255
  );
26545
27256
  console.log(
26546
- chalk34.gray(` To enable team policy + dashboard sync: set it to true, or re-init.`)
27257
+ chalk35.gray(` To enable team policy + dashboard sync: set it to true, or re-init.`)
26547
27258
  );
26548
27259
  }
26549
27260
  } 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.`));
27261
+ console.log(chalk35.green(`\u2705 Logged in \u2014 agent mode`));
27262
+ console.log(chalk35.gray(` Team policy enforced for all calls via Node9 cloud.`));
26552
27263
  }
26553
27264
  });
26554
27265
  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
27266
  const route = options.login ? "auth/login" : "auth/signup";
26556
27267
  const url = `https://node9.ai/${route}?ref=cli_cmd`;
26557
27268
  console.log("");
26558
- console.log(" " + chalk34.dim("Opening ") + chalk34.cyan.underline(url));
27269
+ console.log(" " + chalk35.dim("Opening ") + chalk35.cyan.underline(url));
26559
27270
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
26560
27271
  try {
26561
27272
  const child = spawn9(opener, [url], {
@@ -26588,7 +27299,7 @@ program.command("addto", { hidden: true }).description("Integrate Node9 with an
26588
27299
  if (target === "hermes") return setupHermes();
26589
27300
  if (target === "hud") return setupHud();
26590
27301
  console.error(
26591
- chalk34.red(
27302
+ chalk35.red(
26592
27303
  `Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
26593
27304
  )
26594
27305
  );
@@ -26602,20 +27313,20 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
26602
27313
  "The agent to protect: claude | antigravity | copilot | gemini | cursor | codex | windsurf | vscode | hud"
26603
27314
  ).action(async (target) => {
26604
27315
  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");
27316
+ console.log(chalk35.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
27317
+ console.log(" Usage: " + chalk35.white("node9 setup <target>") + "\n");
26607
27318
  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)");
27319
+ console.log(" " + chalk35.green("claude") + " \u2014 Claude Code (hook mode)");
27320
+ console.log(" " + chalk35.green("gemini") + " \u2014 Gemini CLI (hook mode)");
27321
+ console.log(" " + chalk35.green("antigravity") + " \u2014 Antigravity / agy (hook mode)");
27322
+ console.log(" " + chalk35.green("copilot") + " \u2014 GitHub Copilot CLI (hook mode)");
27323
+ console.log(" " + chalk35.green("cursor") + " \u2014 Cursor (MCP proxy)");
27324
+ console.log(" " + chalk35.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
27325
+ console.log(" " + chalk35.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
27326
+ console.log(" " + chalk35.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
27327
+ console.log(" " + chalk35.green("hermes") + " \u2014 Hermes Agent (hook mode)");
26617
27328
  process.stdout.write(
26618
- " " + chalk34.green("hud") + " \u2014 Claude Code security statusline\n"
27329
+ " " + chalk35.green("hud") + " \u2014 Claude Code security statusline\n"
26619
27330
  );
26620
27331
  console.log("");
26621
27332
  return;
@@ -26632,7 +27343,7 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
26632
27343
  if (t === "hermes") return setupHermes();
26633
27344
  if (t === "hud") return setupHud();
26634
27345
  console.error(
26635
- chalk34.red(
27346
+ chalk35.red(
26636
27347
  `Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
26637
27348
  )
26638
27349
  );
@@ -26658,33 +27369,33 @@ program.command("removefrom", { hidden: true }).description("Remove Node9 hooks
26658
27369
  else if (target === "hud") fn = teardownHud;
26659
27370
  else {
26660
27371
  console.error(
26661
- chalk34.red(
27372
+ chalk35.red(
26662
27373
  `Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
26663
27374
  )
26664
27375
  );
26665
27376
  process.exit(1);
26666
27377
  }
26667
- console.log(chalk34.cyan(`
27378
+ console.log(chalk35.cyan(`
26668
27379
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
26669
27380
  `));
26670
27381
  try {
26671
27382
  fn();
26672
27383
  } catch (err2) {
26673
- console.error(chalk34.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
27384
+ console.error(chalk35.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
26674
27385
  process.exit(1);
26675
27386
  }
26676
- console.log(chalk34.gray("\n Restart the agent for changes to take effect."));
27387
+ console.log(chalk35.gray("\n Restart the agent for changes to take effect."));
26677
27388
  });
26678
27389
  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..."));
27390
+ console.log(chalk35.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
27391
+ console.log(chalk35.bold("Stopping daemon..."));
26681
27392
  try {
26682
27393
  stopDaemon();
26683
- console.log(chalk34.green(" \u2705 Daemon stopped"));
27394
+ console.log(chalk35.green(" \u2705 Daemon stopped"));
26684
27395
  } catch {
26685
- console.log(chalk34.blue(" \u2139\uFE0F Daemon was not running"));
27396
+ console.log(chalk35.blue(" \u2139\uFE0F Daemon was not running"));
26686
27397
  }
26687
- console.log(chalk34.bold("\nRemoving hooks..."));
27398
+ console.log(chalk35.bold("\nRemoving hooks..."));
26688
27399
  let teardownFailed = false;
26689
27400
  for (const [label2, fn] of [
26690
27401
  ["Claude", teardownClaude],
@@ -26700,45 +27411,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
26700
27411
  } catch (err2) {
26701
27412
  teardownFailed = true;
26702
27413
  console.error(
26703
- chalk34.red(
27414
+ chalk35.red(
26704
27415
  ` \u26A0\uFE0F Failed to remove ${label2} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
26705
27416
  )
26706
27417
  );
26707
27418
  }
26708
27419
  }
26709
27420
  if (options.purge) {
26710
- const node9Dir = path57.join(os53.homedir(), ".node9");
26711
- if (fs58.existsSync(node9Dir)) {
27421
+ const node9Dir = path61.join(os55.homedir(), ".node9");
27422
+ if (fs63.existsSync(node9Dir)) {
26712
27423
  const confirmed = await confirm2({
26713
27424
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
26714
27425
  default: false
26715
27426
  });
26716
27427
  if (confirmed) {
26717
- fs58.rmSync(node9Dir, { recursive: true });
26718
- if (fs58.existsSync(node9Dir)) {
27428
+ fs63.rmSync(node9Dir, { recursive: true });
27429
+ if (fs63.existsSync(node9Dir)) {
26719
27430
  console.error(
26720
- chalk34.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
27431
+ chalk35.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
26721
27432
  );
26722
27433
  } else {
26723
- console.log(chalk34.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
27434
+ console.log(chalk35.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
26724
27435
  }
26725
27436
  } else {
26726
- console.log(chalk34.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
27437
+ console.log(chalk35.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
26727
27438
  }
26728
27439
  } else {
26729
- console.log(chalk34.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
27440
+ console.log(chalk35.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
26730
27441
  }
26731
27442
  } else {
26732
27443
  console.log(
26733
- chalk34.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
27444
+ chalk35.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
26734
27445
  );
26735
27446
  }
26736
27447
  if (teardownFailed) {
26737
- console.error(chalk34.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
27448
+ console.error(chalk35.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
26738
27449
  process.exit(1);
26739
27450
  }
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"));
27451
+ console.log(chalk35.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g node9-ai"));
27452
+ console.log(chalk35.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
26742
27453
  });
26743
27454
  registerDoctorCommand(program, version);
26744
27455
  program.command("explain").description(
@@ -26751,7 +27462,7 @@ program.command("explain").description(
26751
27462
  try {
26752
27463
  args = JSON.parse(trimmed);
26753
27464
  } catch {
26754
- console.error(chalk34.red(`
27465
+ console.error(chalk35.red(`
26755
27466
  \u274C Invalid JSON: ${trimmed}
26756
27467
  `));
26757
27468
  process.exit(1);
@@ -26762,54 +27473,54 @@ program.command("explain").description(
26762
27473
  }
26763
27474
  const result = await explainPolicy(tool, args);
26764
27475
  console.log("");
26765
- console.log(chalk34.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
27476
+ console.log(chalk35.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
26766
27477
  console.log("");
26767
- console.log(` ${chalk34.bold("Tool:")} ${chalk34.white(result.tool)}`);
27478
+ console.log(` ${chalk35.bold("Tool:")} ${chalk35.white(result.tool)}`);
26768
27479
  if (argsRaw) {
26769
27480
  const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
26770
- console.log(` ${chalk34.bold("Input:")} ${chalk34.gray(preview2)}`);
27481
+ console.log(` ${chalk35.bold("Input:")} ${chalk35.gray(preview2)}`);
26771
27482
  }
26772
27483
  console.log("");
26773
- console.log(chalk34.bold("Config Sources (Waterfall):"));
27484
+ console.log(chalk35.bold("Config Sources (Waterfall):"));
26774
27485
  for (const tier of result.waterfall) {
26775
- const num3 = chalk34.gray(` ${tier.tier}.`);
27486
+ const num3 = chalk35.gray(` ${tier.tier}.`);
26776
27487
  const label2 = tier.label.padEnd(16);
26777
27488
  let statusStr;
26778
27489
  if (tier.tier === 1) {
26779
- statusStr = chalk34.gray(tier.note ?? "");
27490
+ statusStr = chalk35.gray(tier.note ?? "");
26780
27491
  } 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 : "");
27492
+ const loc = tier.path ? chalk35.gray(tier.path) : "";
27493
+ const note = tier.note ? chalk35.gray(`(${tier.note})`) : "";
27494
+ statusStr = chalk35.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
26784
27495
  } else {
26785
- statusStr = chalk34.gray("\u25CB " + (tier.note ?? "not found"));
27496
+ statusStr = chalk35.gray("\u25CB " + (tier.note ?? "not found"));
26786
27497
  }
26787
- console.log(`${num3} ${chalk34.white(label2)} ${statusStr}`);
27498
+ console.log(`${num3} ${chalk35.white(label2)} ${statusStr}`);
26788
27499
  }
26789
27500
  console.log("");
26790
- console.log(chalk34.bold("Policy Evaluation:"));
27501
+ console.log(chalk35.bold("Policy Evaluation:"));
26791
27502
  for (const step of result.steps) {
26792
27503
  const isFinal = step.isFinal;
26793
27504
  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 ");
27505
+ if (step.outcome === "allow") icon = chalk35.green(" \u2705");
27506
+ else if (step.outcome === "review") icon = chalk35.red(" \u{1F534}");
27507
+ else if (step.outcome === "skip") icon = chalk35.gray(" \u2500 ");
27508
+ else icon = chalk35.gray(" \u25CB ");
26798
27509
  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") : "";
27510
+ const nameStr = isFinal ? chalk35.white.bold(name) : chalk35.white(name);
27511
+ const detail = isFinal ? chalk35.white(step.detail) : chalk35.gray(step.detail);
27512
+ const arrow = isFinal ? chalk35.yellow(" \u2190 STOP") : "";
26802
27513
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
26803
27514
  }
26804
27515
  console.log("");
26805
27516
  if (result.decision === "allow") {
26806
- console.log(chalk34.green.bold(" Decision: \u2705 ALLOW") + chalk34.gray(" \u2014 no approval needed"));
27517
+ console.log(chalk35.green.bold(" Decision: \u2705 ALLOW") + chalk35.gray(" \u2014 no approval needed"));
26807
27518
  } else {
26808
27519
  console.log(
26809
- chalk34.red.bold(" Decision: \u{1F534} REVIEW") + chalk34.gray(" \u2014 human approval required")
27520
+ chalk35.red.bold(" Decision: \u{1F534} REVIEW") + chalk35.gray(" \u2014 human approval required")
26810
27521
  );
26811
27522
  if (result.blockedByLabel) {
26812
- console.log(chalk34.gray(` Reason: ${result.blockedByLabel}`));
27523
+ console.log(chalk35.gray(` Reason: ${result.blockedByLabel}`));
26813
27524
  }
26814
27525
  }
26815
27526
  console.log("");
@@ -26824,18 +27535,18 @@ program.command("tail").description("Stream live agent activity to the terminal"
26824
27535
  try {
26825
27536
  await startTail2(options);
26826
27537
  } catch (err2) {
26827
- console.error(chalk34.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
27538
+ console.error(chalk35.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
26828
27539
  process.exit(1);
26829
27540
  }
26830
27541
  });
26831
27542
  program.command("monitor").description("Live interactive dashboard \u2014 activity feed, approvals, security signals").action(async () => {
26832
27543
  try {
26833
- const dashboardPath = path57.join(__dirname, "dashboard.mjs");
27544
+ const dashboardPath = path61.join(__dirname, "dashboard.mjs");
26834
27545
  const dynamicImport = new Function("id", "return import(id)");
26835
27546
  const mod = await dynamicImport(`file://${dashboardPath}`);
26836
27547
  await mod.startMonitor();
26837
27548
  } catch (err2) {
26838
- console.error(chalk34.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
27549
+ console.error(chalk35.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
26839
27550
  process.exit(1);
26840
27551
  }
26841
27552
  });
@@ -26868,14 +27579,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
26868
27579
  Run "node9 addto claude" to register it as the statusLine.`
26869
27580
  ).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
27581
  if (subcommand === "debug") {
26871
- const flagFile = path57.join(os53.homedir(), ".node9", "hud-debug");
27582
+ const flagFile = path61.join(os55.homedir(), ".node9", "hud-debug");
26872
27583
  if (state === "on") {
26873
- fs58.mkdirSync(path57.dirname(flagFile), { recursive: true });
26874
- fs58.writeFileSync(flagFile, "");
27584
+ fs63.mkdirSync(path61.dirname(flagFile), { recursive: true });
27585
+ fs63.writeFileSync(flagFile, "");
26875
27586
  console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
26876
27587
  console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
26877
27588
  } else if (state === "off") {
26878
- if (fs58.existsSync(flagFile)) fs58.unlinkSync(flagFile);
27589
+ if (fs63.existsSync(flagFile)) fs63.unlinkSync(flagFile);
26879
27590
  console.log("HUD debug logging disabled.");
26880
27591
  } else {
26881
27592
  console.error("Usage: node9 hud debug on|off");
@@ -26890,7 +27601,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
26890
27601
  const ms = parseDuration(options.duration);
26891
27602
  if (ms === null) {
26892
27603
  console.error(
26893
- chalk34.red(`
27604
+ chalk35.red(`
26894
27605
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
26895
27606
  `)
26896
27607
  );
@@ -26898,20 +27609,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
26898
27609
  }
26899
27610
  pauseNode9(ms, options.duration);
26900
27611
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
26901
- console.log(chalk34.yellow(`
27612
+ console.log(chalk35.yellow(`
26902
27613
  \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.
27614
+ console.log(chalk35.gray(` All tool calls will be allowed without review.`));
27615
+ console.log(chalk35.gray(` Run "node9 resume" to re-enable early.
26905
27616
  `));
26906
27617
  });
26907
27618
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
26908
27619
  const { paused } = checkPause();
26909
27620
  if (!paused) {
26910
- console.log(chalk34.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
27621
+ console.log(chalk35.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
26911
27622
  return;
26912
27623
  }
26913
27624
  resumeNode9();
26914
- console.log(chalk34.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
27625
+ console.log(chalk35.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
26915
27626
  });
26916
27627
  var HOOK_BASED_AGENTS = {
26917
27628
  claude: "claude",
@@ -26927,15 +27638,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
26927
27638
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
26928
27639
  const target = HOOK_BASED_AGENTS[firstArg2];
26929
27640
  console.error(
26930
- chalk34.yellow(`
27641
+ chalk35.yellow(`
26931
27642
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
26932
27643
  );
26933
- console.error(chalk34.white(`
27644
+ console.error(chalk35.white(`
26934
27645
  "${target}" uses its own hook system. Use:`));
26935
27646
  console.error(
26936
- chalk34.green(` node9 addto ${target} `) + chalk34.gray("# one-time setup")
27647
+ chalk35.green(` node9 addto ${target} `) + chalk35.gray("# one-time setup")
26937
27648
  );
26938
- console.error(chalk34.green(` ${target} `) + chalk34.gray("# run normally"));
27649
+ console.error(chalk35.green(` ${target} `) + chalk35.gray("# run normally"));
26939
27650
  process.exit(1);
26940
27651
  }
26941
27652
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -26952,7 +27663,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
26952
27663
  }
26953
27664
  );
26954
27665
  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..."));
27666
+ console.error(chalk35.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
26956
27667
  const daemonReady = await autoStartDaemonAndWait();
26957
27668
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
26958
27669
  }
@@ -26965,12 +27676,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
26965
27676
  }
26966
27677
  if (!result.approved) {
26967
27678
  console.error(
26968
- chalk34.red(`
27679
+ chalk35.red(`
26969
27680
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
26970
27681
  );
26971
27682
  process.exit(1);
26972
27683
  }
26973
- console.error(chalk34.green("\n\u2705 Approved \u2014 running command...\n"));
27684
+ console.error(chalk35.green("\n\u2705 Approved \u2014 running command...\n"));
26974
27685
  await runProxy(fullCommand);
26975
27686
  } else {
26976
27687
  program.help();
@@ -26985,6 +27696,7 @@ registerAgentsCommand(program);
26985
27696
  registerScanCommand(program);
26986
27697
  registerPostureCommand(program);
26987
27698
  registerEgressCommand(program);
27699
+ registerSandboxCommand(program, version);
26988
27700
  registerSessionsCommand(program);
26989
27701
  registerSessionTaintCommand(program);
26990
27702
  registerDlpCommand(program);
@@ -26995,9 +27707,9 @@ if (process.argv[2] !== "daemon") {
26995
27707
  const isCheckHook = process.argv[2] === "check";
26996
27708
  if (isCheckHook) {
26997
27709
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
26998
- const logPath = path57.join(os53.homedir(), ".node9", "hook-debug.log");
27710
+ const logPath = path61.join(os55.homedir(), ".node9", "hook-debug.log");
26999
27711
  const msg = reason instanceof Error ? reason.message : String(reason);
27000
- fs58.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
27712
+ fs63.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
27001
27713
  `);
27002
27714
  }
27003
27715
  process.exit(0);