@node9/proxy 1.5.2 → 1.5.4

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
@@ -139,8 +139,8 @@ function sanitizeConfig(raw) {
139
139
  }
140
140
  }
141
141
  const lines = result.error.issues.map((issue) => {
142
- const path28 = issue.path.length > 0 ? issue.path.join(".") : "root";
143
- return ` \u2022 ${path28}: ${issue.message}`;
142
+ const path30 = issue.path.length > 0 ? issue.path.join(".") : "root";
143
+ return ` \u2022 ${path30}: ${issue.message}`;
144
144
  });
145
145
  return {
146
146
  sanitized,
@@ -191,12 +191,21 @@ var init_config_schema = __esm({
191
191
  verdict: z.enum(["allow", "review", "block"], {
192
192
  errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
193
193
  }),
194
- reason: z.string().optional()
194
+ reason: z.string().optional(),
195
+ // Unknown predicate names are filtered out rather than failing the whole rule.
196
+ // Failing the whole z.array() would cause sanitizeConfig to drop the entire
197
+ // `policy` top-level key, silently disabling ALL smart rules in the config.
198
+ dependsOnState: z.array(z.string()).transform(
199
+ (arr) => arr.filter(
200
+ (p) => p === "no_test_passed_since_last_edit"
201
+ )
202
+ ).optional(),
203
+ recoveryCommand: z.string().optional()
195
204
  });
196
205
  ConfigFileSchema = z.object({
197
206
  version: z.string().optional(),
198
207
  settings: z.object({
199
- mode: z.enum(["standard", "strict", "audit"]).optional(),
208
+ mode: z.enum(["standard", "strict", "audit", "observe"]).optional(),
200
209
  autoStartDaemon: z.boolean().optional(),
201
210
  enableUndo: z.boolean().optional(),
202
211
  enableHookLogDebug: z.boolean().optional(),
@@ -626,12 +635,17 @@ function getConfig(cwd) {
626
635
  if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
627
636
  mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
628
637
  if (s.environment !== void 0) mergedSettings.environment = s.environment;
638
+ if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
629
639
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
630
640
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
631
641
  if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
632
642
  if (p.toolInspection)
633
643
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
634
- if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
644
+ if (p.smartRules) {
645
+ const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
646
+ const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
647
+ mergedPolicy.smartRules = [...defaultBlocks, ...p.smartRules, ...defaultNonBlocks];
648
+ }
635
649
  if (p.snapshot) {
636
650
  const s2 = p.snapshot;
637
651
  if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
@@ -827,7 +841,9 @@ var init_config = __esm({
827
841
  {
828
842
  field: "command",
829
843
  op: "matches",
830
- value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
844
+ // Require the recursive flag to be preceded by whitespace so that
845
+ // filenames containing "-r" (e.g. "ai-review.yml") don't false-positive.
846
+ value: "rm\\b.*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
831
847
  },
832
848
  {
833
849
  field: "command",
@@ -1741,9 +1757,9 @@ function matchesPattern(text, patterns) {
1741
1757
  const withoutDotSlash = text.replace(/^\.\//, "");
1742
1758
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1743
1759
  }
1744
- function getNestedValue(obj, path28) {
1760
+ function getNestedValue(obj, path30) {
1745
1761
  if (!obj || typeof obj !== "object") return null;
1746
- return path28.split(".").reduce((prev, curr) => prev?.[curr], obj);
1762
+ return path30.split(".").reduce((prev, curr) => prev?.[curr], obj);
1747
1763
  }
1748
1764
  function shouldSnapshot(toolName, args, config) {
1749
1765
  if (!config.settings.enableUndo) return false;
@@ -1893,7 +1909,13 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1893
1909
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
1894
1910
  reason: matchedRule.reason,
1895
1911
  tier: 2,
1896
- ruleName: matchedRule.name ?? matchedRule.tool
1912
+ ruleName: matchedRule.name ?? matchedRule.tool,
1913
+ ...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
1914
+ dependsOnStatePredicates: matchedRule.dependsOnState
1915
+ },
1916
+ ...matchedRule.verdict === "block" && matchedRule.recoveryCommand && {
1917
+ recoveryCommand: matchedRule.recoveryCommand
1918
+ }
1897
1919
  };
1898
1920
  }
1899
1921
  }
@@ -2416,9 +2438,38 @@ var init_state = __esm({
2416
2438
 
2417
2439
  // src/auth/daemon.ts
2418
2440
  import fs9 from "fs";
2441
+ import net from "net";
2419
2442
  import path10 from "path";
2420
2443
  import os8 from "os";
2421
2444
  import { spawnSync } from "child_process";
2445
+ function notifyActivitySocket(data) {
2446
+ return new Promise((resolve) => {
2447
+ try {
2448
+ const payload = JSON.stringify(data);
2449
+ const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
2450
+ sock.on("connect", () => {
2451
+ sock.on("close", resolve);
2452
+ sock.end(payload);
2453
+ });
2454
+ sock.on("error", resolve);
2455
+ } catch {
2456
+ resolve();
2457
+ }
2458
+ });
2459
+ }
2460
+ async function checkStatePredicates(predicates) {
2461
+ if (predicates.length === 0) return {};
2462
+ try {
2463
+ const qs = predicates.map(encodeURIComponent).join(",");
2464
+ const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/state/check?predicates=${qs}`, {
2465
+ signal: AbortSignal.timeout(100)
2466
+ });
2467
+ if (!res.ok) return null;
2468
+ return await res.json();
2469
+ } catch {
2470
+ return null;
2471
+ }
2472
+ }
2422
2473
  function getInternalToken() {
2423
2474
  try {
2424
2475
  const pidFile = path10.join(os8.homedir(), ".node9", "daemon.pid");
@@ -2452,7 +2503,7 @@ function isDaemonRunning() {
2452
2503
  return false;
2453
2504
  }
2454
2505
  }
2455
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
2506
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
2456
2507
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2457
2508
  const ctrl = new AbortController();
2458
2509
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -2470,7 +2521,10 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2470
2521
  // activity-result as the CLI used for the pending activity event.
2471
2522
  activityId,
2472
2523
  ...riskMetadata && { riskMetadata },
2473
- ...cwd && { cwd }
2524
+ ...cwd && { cwd },
2525
+ ...recoveryCommand && { recoveryCommand },
2526
+ ...skipBackgroundAuth && { skipBackgroundAuth: true },
2527
+ ...viewOnly && { viewOnly: true }
2474
2528
  }),
2475
2529
  signal: ctrl.signal
2476
2530
  });
@@ -2490,10 +2544,10 @@ async function waitForDaemonDecision(id, signal) {
2490
2544
  try {
2491
2545
  const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
2492
2546
  if (!waitRes.ok) return { decision: "deny" };
2493
- const { decision, source } = await waitRes.json();
2547
+ const { decision, source, reason } = await waitRes.json();
2494
2548
  if (decision === "allow") return { decision: "allow", source };
2495
2549
  if (decision === "abandoned") return { decision: "abandoned", source };
2496
- return { decision: "deny", source };
2550
+ return { decision: "deny", source, reason };
2497
2551
  } finally {
2498
2552
  clearTimeout(waitTimer);
2499
2553
  if (signal) signal.removeEventListener("abort", onAbort);
@@ -2579,10 +2633,11 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
2579
2633
  signal: AbortSignal.timeout(3e3)
2580
2634
  });
2581
2635
  }
2582
- var DAEMON_PORT, DAEMON_HOST;
2636
+ var ACTIVITY_SOCKET_PATH, DAEMON_PORT, DAEMON_HOST;
2583
2637
  var init_daemon = __esm({
2584
2638
  "src/auth/daemon.ts"() {
2585
2639
  "use strict";
2640
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path10.join(os8.tmpdir(), "node9-activity.sock");
2586
2641
  DAEMON_PORT = 7391;
2587
2642
  DAEMON_HOST = "127.0.0.1";
2588
2643
  }
@@ -2922,6 +2977,7 @@ var init_native = __esm({
2922
2977
  // src/auth/cloud.ts
2923
2978
  import fs10 from "fs";
2924
2979
  import os9 from "os";
2980
+ import path13 from "path";
2925
2981
  function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
2926
2982
  return fetch(`${creds.apiUrl}/audit`, {
2927
2983
  method: "POST",
@@ -2946,6 +3002,33 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
2946
3002
  async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2947
3003
  const controller = new AbortController();
2948
3004
  const timeout = setTimeout(() => controller.abort(), 1e4);
3005
+ if (!creds.apiKey) throw new Error("Node9 API Key is missing");
3006
+ let ciContext;
3007
+ if (process.env.CI) {
3008
+ try {
3009
+ const ciContextPath = path13.join(os9.homedir(), ".node9", "ci-context.json");
3010
+ const stats = fs10.statSync(ciContextPath);
3011
+ if (stats.size > 1e4) throw new Error("ci-context.json exceeds 10 KB");
3012
+ const raw = fs10.readFileSync(ciContextPath, "utf8");
3013
+ const parsed = JSON.parse(raw);
3014
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3015
+ throw new Error("ci-context.json is not a plain object");
3016
+ }
3017
+ const p = parsed;
3018
+ ciContext = {
3019
+ tests_after: p["tests_after"],
3020
+ files_changed: p["files_changed"],
3021
+ issues_found: p["issues_found"],
3022
+ issues_fixed: p["issues_fixed"],
3023
+ github_repository: p["github_repository"],
3024
+ github_head_ref: p["github_head_ref"],
3025
+ iteration: p["iteration"],
3026
+ draft_pr_number: p["draft_pr_number"],
3027
+ draft_pr_url: p["draft_pr_url"]
3028
+ };
3029
+ } catch {
3030
+ }
3031
+ }
2949
3032
  try {
2950
3033
  const response = await fetch(creds.apiUrl, {
2951
3034
  method: "POST",
@@ -2960,7 +3043,8 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2960
3043
  cwd: process.cwd(),
2961
3044
  platform: os9.platform()
2962
3045
  },
2963
- ...riskMetadata && { riskMetadata }
3046
+ ...riskMetadata && { riskMetadata },
3047
+ ...ciContext && { ciContext }
2964
3048
  }),
2965
3049
  signal: controller.signal
2966
3050
  });
@@ -2986,12 +3070,17 @@ async function pollNode9SaaS(requestId, creds, signal) {
2986
3070
  });
2987
3071
  clearTimeout(pollTimer);
2988
3072
  if (!statusRes.ok) continue;
2989
- const { status, reason } = await statusRes.json();
3073
+ const statusBody = await statusRes.json();
3074
+ const { status } = statusBody;
2990
3075
  if (status === "APPROVED") {
2991
- return { approved: true, reason };
3076
+ return { approved: true, reason: statusBody.reason };
2992
3077
  }
2993
3078
  if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
2994
- return { approved: false, reason };
3079
+ return { approved: false, reason: statusBody.reason };
3080
+ }
3081
+ if (status === "FIX") {
3082
+ const feedbackText = statusBody.feedbackText ?? statusBody.reason ?? "Run again with feedback.";
3083
+ return { approved: false, reason: feedbackText };
2995
3084
  }
2996
3085
  } catch {
2997
3086
  }
@@ -3036,9 +3125,6 @@ var init_cloud = __esm({
3036
3125
  });
3037
3126
 
3038
3127
  // src/auth/orchestrator.ts
3039
- import net from "net";
3040
- import path13 from "path";
3041
- import os10 from "os";
3042
3128
  import { randomUUID } from "crypto";
3043
3129
  function isWriteTool(toolName) {
3044
3130
  const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
@@ -3075,19 +3161,7 @@ function isNetworkTool(toolName, args) {
3075
3161
  return false;
3076
3162
  }
3077
3163
  function notifyActivity(data) {
3078
- return new Promise((resolve) => {
3079
- try {
3080
- const payload = JSON.stringify(data);
3081
- const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
3082
- sock.on("connect", () => {
3083
- sock.on("close", resolve);
3084
- sock.end(payload);
3085
- });
3086
- sock.on("error", resolve);
3087
- } catch {
3088
- resolve();
3089
- }
3090
- });
3164
+ return notifyActivitySocket(data);
3091
3165
  }
3092
3166
  async function authorizeHeadless(toolName, args, meta, options) {
3093
3167
  if (!options?.calledFromDaemon) {
@@ -3104,7 +3178,9 @@ async function authorizeHeadless(toolName, args, meta, options) {
3104
3178
  tool: toolName,
3105
3179
  ts: actTs,
3106
3180
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
3107
- label: result.blockedByLabel
3181
+ label: result.blockedByLabel,
3182
+ ruleHit: result.ruleHit,
3183
+ observeWouldBlock: result.observeWouldBlock
3108
3184
  });
3109
3185
  }
3110
3186
  return result;
@@ -3131,10 +3207,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3131
3207
  appendHookDebug(toolName, args, meta, hashAuditArgs);
3132
3208
  }
3133
3209
  const isManual = meta?.agent === "Terminal";
3210
+ const isObserveMode = config.settings.mode === "observe";
3134
3211
  let explainableLabel = "Local Config";
3135
3212
  let policyMatchedField;
3136
3213
  let policyMatchedWord;
3137
3214
  let riskMetadata;
3215
+ let statefulRecoveryCommand;
3138
3216
  let taintWarning = null;
3139
3217
  if (isNetworkTool(toolName, args)) {
3140
3218
  const filePaths = extractFilePaths(toolName, args);
@@ -3155,10 +3233,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3155
3233
  if (dlpMatch) {
3156
3234
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
3157
3235
  if (dlpMatch.severity === "block") {
3158
- if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta, true);
3236
+ if (!isManual)
3237
+ appendLocalAudit(
3238
+ toolName,
3239
+ args,
3240
+ "deny",
3241
+ isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
3242
+ meta,
3243
+ true
3244
+ );
3159
3245
  if (isWriteTool(toolName) && filePath) {
3160
3246
  await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
3161
3247
  }
3248
+ if (isObserveMode) {
3249
+ return {
3250
+ approved: true,
3251
+ checkedBy: "audit",
3252
+ observeWouldBlock: true,
3253
+ blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
3254
+ };
3255
+ }
3162
3256
  return {
3163
3257
  approved: false,
3164
3258
  reason: dlpReason,
@@ -3171,6 +3265,31 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3171
3265
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
3172
3266
  }
3173
3267
  }
3268
+ if (isObserveMode) {
3269
+ if (!isIgnoredTool(toolName)) {
3270
+ const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
3271
+ const wouldBlock = policyResult.decision === "block";
3272
+ if (!isManual)
3273
+ appendLocalAudit(
3274
+ toolName,
3275
+ args,
3276
+ "allow",
3277
+ wouldBlock ? "observe-mode-would-block" : "observe-mode",
3278
+ meta,
3279
+ hashAuditArgs
3280
+ );
3281
+ return {
3282
+ approved: true,
3283
+ checkedBy: "audit",
3284
+ ...wouldBlock && {
3285
+ observeWouldBlock: true,
3286
+ blockedByLabel: policyResult.blockedByLabel,
3287
+ ruleHit: policyResult.ruleName
3288
+ }
3289
+ };
3290
+ }
3291
+ return { approved: true, checkedBy: "audit" };
3292
+ }
3174
3293
  if (config.settings.mode === "audit") {
3175
3294
  if (!isIgnoredTool(toolName)) {
3176
3295
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
@@ -3193,19 +3312,46 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3193
3312
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
3194
3313
  if (policyResult.decision === "allow") {
3195
3314
  if (approvers.cloud && creds?.apiKey)
3196
- auditLocalAllow(toolName, args, "local-policy", creds, meta);
3315
+ await auditLocalAllow(toolName, args, "local-policy", creds, meta);
3197
3316
  if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
3198
3317
  return { approved: true, checkedBy: "local-policy" };
3199
3318
  }
3200
3319
  if (policyResult.decision === "block") {
3201
- if (!isManual)
3202
- appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
3203
- return {
3204
- approved: false,
3205
- reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
3206
- blockedBy: "local-config",
3207
- blockedByLabel: policyResult.blockedByLabel
3208
- };
3320
+ if (policyResult.dependsOnStatePredicates?.length) {
3321
+ const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
3322
+ const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
3323
+ if (stateResults === null && !isManual) {
3324
+ appendToLog(HOOK_DEBUG_LOG, {
3325
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3326
+ event: "state-check-fail-open",
3327
+ tool: toolName,
3328
+ rule: policyResult.ruleName,
3329
+ predicates: policyResult.dependsOnStatePredicates,
3330
+ reason: "daemon unreachable or /state/check timed out \u2014 block rule downgraded to review"
3331
+ });
3332
+ }
3333
+ if (predicatesMet && policyResult.recoveryCommand) {
3334
+ statefulRecoveryCommand = policyResult.recoveryCommand;
3335
+ }
3336
+ } else if (isDaemonRunning() && !isTestEnv2) {
3337
+ if (!isManual)
3338
+ appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
3339
+ if (approvers.cloud && creds?.apiKey)
3340
+ auditLocalAllow(toolName, args, "smart-rule-block", creds, meta);
3341
+ } else {
3342
+ if (!isManual)
3343
+ appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
3344
+ if (approvers.cloud && creds?.apiKey)
3345
+ auditLocalAllow(toolName, args, "smart-rule-block", creds, meta);
3346
+ return {
3347
+ approved: false,
3348
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
3349
+ blockedBy: "local-config",
3350
+ blockedByLabel: policyResult.blockedByLabel,
3351
+ ruleHit: policyResult.ruleName,
3352
+ ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
3353
+ };
3354
+ }
3209
3355
  }
3210
3356
  explainableLabel = policyResult.blockedByLabel || "Local Config";
3211
3357
  policyMatchedField = policyResult.matchedField;
@@ -3312,7 +3458,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3312
3458
  meta,
3313
3459
  riskMetadata,
3314
3460
  options?.activityId,
3315
- options?.cwd
3461
+ options?.cwd,
3462
+ statefulRecoveryCommand
3316
3463
  );
3317
3464
  daemonEntryId = entry.id;
3318
3465
  daemonAllowCount = entry.allowCount;
@@ -3373,20 +3520,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3373
3520
  if (daemonEntryId && (approvers.browser || approvers.terminal)) {
3374
3521
  racePromises.push(
3375
3522
  (async () => {
3376
- const { decision: daemonDecision, source: decisionSource } = await waitForDaemonDecision(
3377
- daemonEntryId,
3378
- signal
3379
- );
3523
+ const {
3524
+ decision: daemonDecision,
3525
+ source: decisionSource,
3526
+ reason: daemonReason
3527
+ } = await waitForDaemonDecision(daemonEntryId, signal);
3380
3528
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
3381
3529
  const isApproved = daemonDecision === "allow";
3382
- const src = decisionSource === "terminal" || decisionSource === "browser" ? decisionSource : approvers.browser ? "browser" : "terminal";
3530
+ const isRedirect = decisionSource === "terminal-redirect";
3531
+ const src = decisionSource === "terminal" || decisionSource === "terminal-redirect" || decisionSource === "browser" ? decisionSource === "browser" ? "browser" : "terminal" : approvers.browser ? "browser" : "terminal";
3383
3532
  const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
3384
3533
  return {
3385
3534
  approved: isApproved,
3386
- reason: isApproved ? void 0 : `The human user rejected this action via the Node9 ${via}.`,
3535
+ reason: isApproved ? void 0 : (
3536
+ // Use the redirect reason from the tail when choice [2] was selected;
3537
+ // otherwise fall back to the generic rejection message.
3538
+ isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`
3539
+ ),
3387
3540
  checkedBy: isApproved ? "daemon" : void 0,
3388
3541
  blockedBy: isApproved ? void 0 : "local-decision",
3389
- blockedByLabel: `User Decision (${via})`,
3542
+ blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
3390
3543
  decisionSource: src
3391
3544
  };
3392
3545
  })()
@@ -3461,7 +3614,7 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3461
3614
  }
3462
3615
  return finalResult;
3463
3616
  }
3464
- var WRITE_TOOLS, ACTIVITY_SOCKET_PATH;
3617
+ var WRITE_TOOLS;
3465
3618
  var init_orchestrator = __esm({
3466
3619
  "src/auth/orchestrator.ts"() {
3467
3620
  "use strict";
@@ -3485,7 +3638,6 @@ var init_orchestrator = __esm({
3485
3638
  "notebook_edit",
3486
3639
  "notebookedit"
3487
3640
  ]);
3488
- ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os10.tmpdir(), "node9-activity.sock");
3489
3641
  }
3490
3642
  });
3491
3643
 
@@ -5291,11 +5443,112 @@ var init_taint_store = __esm({
5291
5443
  }
5292
5444
  });
5293
5445
 
5446
+ // src/daemon/session-counters.ts
5447
+ var SessionCounters, sessionCounters;
5448
+ var init_session_counters = __esm({
5449
+ "src/daemon/session-counters.ts"() {
5450
+ "use strict";
5451
+ SessionCounters = class {
5452
+ _allowed = 0;
5453
+ _blocked = 0;
5454
+ _dlpHits = 0;
5455
+ _wouldBlock = 0;
5456
+ _lastRuleHit = null;
5457
+ _lastBlockedTool = null;
5458
+ incrementAllowed() {
5459
+ this._allowed++;
5460
+ }
5461
+ incrementBlocked() {
5462
+ this._blocked++;
5463
+ }
5464
+ incrementDlpHits() {
5465
+ this._dlpHits++;
5466
+ }
5467
+ incrementWouldBlock() {
5468
+ this._wouldBlock++;
5469
+ }
5470
+ recordRuleHit(label) {
5471
+ this._lastRuleHit = label;
5472
+ }
5473
+ recordBlockedTool(toolName) {
5474
+ this._lastBlockedTool = toolName;
5475
+ }
5476
+ get() {
5477
+ return {
5478
+ allowed: this._allowed,
5479
+ blocked: this._blocked,
5480
+ dlpHits: this._dlpHits,
5481
+ wouldBlock: this._wouldBlock,
5482
+ lastRuleHit: this._lastRuleHit,
5483
+ lastBlockedTool: this._lastBlockedTool
5484
+ };
5485
+ }
5486
+ reset() {
5487
+ this._allowed = 0;
5488
+ this._blocked = 0;
5489
+ this._dlpHits = 0;
5490
+ this._wouldBlock = 0;
5491
+ this._lastRuleHit = null;
5492
+ this._lastBlockedTool = null;
5493
+ }
5494
+ };
5495
+ sessionCounters = new SessionCounters();
5496
+ }
5497
+ });
5498
+
5499
+ // src/daemon/session-history.ts
5500
+ var SessionHistory, sessionHistory;
5501
+ var init_session_history = __esm({
5502
+ "src/daemon/session-history.ts"() {
5503
+ "use strict";
5504
+ SessionHistory = class {
5505
+ lastEditAt = null;
5506
+ lastTestPassAt = null;
5507
+ lastTestFailAt = null;
5508
+ recordEdit(ts = Date.now()) {
5509
+ this.lastEditAt = ts;
5510
+ }
5511
+ recordTestPass(ts = Date.now()) {
5512
+ this.lastTestPassAt = ts;
5513
+ }
5514
+ recordTestFail(ts = Date.now()) {
5515
+ this.lastTestFailAt = ts;
5516
+ }
5517
+ /**
5518
+ * Returns true when the named predicate is currently satisfied.
5519
+ * Unknown predicates always return false (fail-open: don't block on unknown state).
5520
+ */
5521
+ checkPredicate(name) {
5522
+ switch (name) {
5523
+ case "no_test_passed_since_last_edit":
5524
+ if (this.lastEditAt === null) return false;
5525
+ return this.lastTestPassAt === null || this.lastTestPassAt < this.lastEditAt;
5526
+ default:
5527
+ return false;
5528
+ }
5529
+ }
5530
+ getSnapshot() {
5531
+ return {
5532
+ lastEditAt: this.lastEditAt,
5533
+ lastTestPassAt: this.lastTestPassAt,
5534
+ lastTestFailAt: this.lastTestFailAt
5535
+ };
5536
+ }
5537
+ reset() {
5538
+ this.lastEditAt = null;
5539
+ this.lastTestPassAt = null;
5540
+ this.lastTestFailAt = null;
5541
+ }
5542
+ };
5543
+ sessionHistory = new SessionHistory();
5544
+ }
5545
+ });
5546
+
5294
5547
  // src/daemon/state.ts
5295
5548
  import net2 from "net";
5296
5549
  import fs13 from "fs";
5297
5550
  import path16 from "path";
5298
- import os12 from "os";
5551
+ import os11 from "os";
5299
5552
  import { spawn as spawn2 } from "child_process";
5300
5553
  import { randomUUID as randomUUID3 } from "crypto";
5301
5554
  function loadInsightCounts() {
@@ -5458,6 +5711,7 @@ function readBody(req) {
5458
5711
  });
5459
5712
  }
5460
5713
  function openBrowser(url) {
5714
+ if (process.env.NODE9_TESTING === "1") return;
5461
5715
  try {
5462
5716
  const args = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
5463
5717
  spawn2(args[0], args.slice(1), { detached: true, stdio: "ignore" }).unref();
@@ -5529,6 +5783,14 @@ function startActivitySocket() {
5529
5783
  socket.on("end", () => {
5530
5784
  try {
5531
5785
  const data = JSON.parse(Buffer.concat(chunks).toString());
5786
+ if (data.status === "test_pass") {
5787
+ sessionHistory.recordTestPass(data.ts);
5788
+ return;
5789
+ }
5790
+ if (data.status === "test_fail") {
5791
+ sessionHistory.recordTestFail(data.ts);
5792
+ return;
5793
+ }
5532
5794
  if (data.status === "pending") {
5533
5795
  broadcast("activity", {
5534
5796
  id: data.id,
@@ -5538,6 +5800,24 @@ function startActivitySocket() {
5538
5800
  status: "pending"
5539
5801
  });
5540
5802
  } else {
5803
+ if (data.status === "allow") {
5804
+ sessionCounters.incrementAllowed();
5805
+ if (data.observeWouldBlock) sessionCounters.incrementWouldBlock();
5806
+ if (WRITE_TOOL_NAMES.has(data.tool.toLowerCase().replace(/[^a-z_]/g, "_"))) {
5807
+ sessionHistory.recordEdit(data.ts);
5808
+ }
5809
+ } else if (data.status === "block") {
5810
+ sessionCounters.incrementBlocked();
5811
+ sessionCounters.recordBlockedTool(data.tool);
5812
+ if (data.ruleHit) sessionCounters.recordRuleHit(data.ruleHit);
5813
+ } else if (data.status === "dlp") {
5814
+ sessionCounters.incrementBlocked();
5815
+ sessionCounters.incrementDlpHits();
5816
+ sessionCounters.recordBlockedTool(data.tool);
5817
+ } else if (data.status === "taint") {
5818
+ sessionCounters.incrementBlocked();
5819
+ sessionCounters.recordBlockedTool(data.tool);
5820
+ }
5541
5821
  broadcast("activity-result", {
5542
5822
  id: data.id,
5543
5823
  status: data.status,
@@ -5558,14 +5838,16 @@ function startActivitySocket() {
5558
5838
  }
5559
5839
  });
5560
5840
  }
5561
- var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
5841
+ var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, WRITE_TOOL_NAMES;
5562
5842
  var init_state2 = __esm({
5563
5843
  "src/daemon/state.ts"() {
5564
5844
  "use strict";
5565
5845
  init_daemon();
5566
5846
  init_suggestion_tracker();
5567
5847
  init_taint_store();
5568
- homeDir = os12.homedir();
5848
+ init_session_counters();
5849
+ init_session_history();
5850
+ homeDir = os11.homedir();
5569
5851
  DAEMON_PID_FILE = path16.join(homeDir, ".node9", "daemon.pid");
5570
5852
  DECISIONS_FILE = path16.join(homeDir, ".node9", "decisions.json");
5571
5853
  AUDIT_LOG_FILE = path16.join(homeDir, ".node9", "audit.log");
@@ -5590,17 +5872,28 @@ var init_state2 = __esm({
5590
5872
  "2h": 2 * 60 * 6e4
5591
5873
  };
5592
5874
  autoStarted = process.env.NODE9_AUTO_STARTED === "1";
5593
- ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path16.join(os12.tmpdir(), "node9-activity.sock");
5875
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path16.join(os11.tmpdir(), "node9-activity.sock");
5594
5876
  ACTIVITY_RING_SIZE = 100;
5595
5877
  activityRing = [];
5596
5878
  SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
5879
+ WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
5880
+ "write",
5881
+ "write_file",
5882
+ "create_file",
5883
+ "edit",
5884
+ "multiedit",
5885
+ "str_replace_based_edit_tool",
5886
+ "replace",
5887
+ "notebook_edit",
5888
+ "notebookedit"
5889
+ ]);
5597
5890
  }
5598
5891
  });
5599
5892
 
5600
5893
  // src/config/patch.ts
5601
5894
  import fs14 from "fs";
5602
5895
  import path17 from "path";
5603
- import os13 from "os";
5896
+ import os12 from "os";
5604
5897
  function patchConfig(configPath, patch) {
5605
5898
  let config = {};
5606
5899
  try {
@@ -5650,7 +5943,7 @@ var GLOBAL_CONFIG_PATH;
5650
5943
  var init_patch = __esm({
5651
5944
  "src/config/patch.ts"() {
5652
5945
  "use strict";
5653
- GLOBAL_CONFIG_PATH = path17.join(os13.homedir(), ".node9", "config.json");
5946
+ GLOBAL_CONFIG_PATH = path17.join(os12.homedir(), ".node9", "config.json");
5654
5947
  }
5655
5948
  });
5656
5949
 
@@ -5723,6 +6016,8 @@ data: ${JSON.stringify({
5723
6016
  toolName: e.toolName,
5724
6017
  args: e.args,
5725
6018
  riskMetadata: e.riskMetadata,
6019
+ ...e.recoveryCommand && { recoveryCommand: e.recoveryCommand },
6020
+ ...e.viewOnly && { viewOnly: true },
5726
6021
  slackDelegated: e.slackDelegated,
5727
6022
  timestamp: e.timestamp,
5728
6023
  agent: e.agent,
@@ -5788,6 +6083,9 @@ data: ${JSON.stringify(item.data)}
5788
6083
  agent,
5789
6084
  mcpServer,
5790
6085
  riskMetadata,
6086
+ recoveryCommand,
6087
+ skipBackgroundAuth = false,
6088
+ viewOnly = false,
5791
6089
  fromCLI = false,
5792
6090
  activityId,
5793
6091
  cwd
@@ -5798,6 +6096,8 @@ data: ${JSON.stringify(item.data)}
5798
6096
  toolName,
5799
6097
  args,
5800
6098
  riskMetadata: riskMetadata ?? void 0,
6099
+ ...typeof recoveryCommand === "string" && recoveryCommand && { recoveryCommand },
6100
+ ...viewOnly && { viewOnly: true },
5801
6101
  agent: typeof agent === "string" ? agent : void 0,
5802
6102
  mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
5803
6103
  slackDelegated: !!slackDelegated,
@@ -5842,6 +6142,8 @@ data: ${JSON.stringify(item.data)}
5842
6142
  toolName,
5843
6143
  args,
5844
6144
  riskMetadata: entry.riskMetadata,
6145
+ ...entry.recoveryCommand && { recoveryCommand: entry.recoveryCommand },
6146
+ ...entry.viewOnly && { viewOnly: true },
5845
6147
  slackDelegated: entry.slackDelegated,
5846
6148
  agent: entry.agent,
5847
6149
  mcpServer: entry.mcpServer,
@@ -5858,7 +6160,7 @@ data: ${JSON.stringify(item.data)}
5858
6160
  }
5859
6161
  res.writeHead(200, { "Content-Type": "application/json" });
5860
6162
  res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
5861
- if (slackDelegated) return;
6163
+ if (slackDelegated || skipBackgroundAuth) return;
5862
6164
  authorizeHeadless(
5863
6165
  toolName,
5864
6166
  args,
@@ -5997,7 +6299,7 @@ data: ${JSON.stringify(item.data)}
5997
6299
  saveInsightCounts();
5998
6300
  suggestionTracker.resetTool(entry.toolName);
5999
6301
  }
6000
- const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
6302
+ const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native", "terminal-redirect"]);
6001
6303
  if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
6002
6304
  if (entry.waiter) {
6003
6305
  entry.waiter(resolvedDecision, reason);
@@ -6026,6 +6328,41 @@ data: ${JSON.stringify(item.data)}
6026
6328
  return res.end(JSON.stringify({ error: "internal" }));
6027
6329
  }
6028
6330
  }
6331
+ if (req.method === "GET" && pathname === "/status") {
6332
+ try {
6333
+ const s = getGlobalSettings();
6334
+ const counters = sessionCounters.get();
6335
+ const mode = s.mode ?? "standard";
6336
+ const status = {
6337
+ mode,
6338
+ session: {
6339
+ allowed: counters.allowed,
6340
+ blocked: counters.blocked,
6341
+ dlpHits: counters.dlpHits,
6342
+ wouldBlock: counters.wouldBlock
6343
+ },
6344
+ taintedCount: taintStore.list().length,
6345
+ lastRuleHit: counters.lastRuleHit,
6346
+ lastBlockedTool: counters.lastBlockedTool
6347
+ };
6348
+ res.writeHead(200, { "Content-Type": "application/json" });
6349
+ return res.end(JSON.stringify(status));
6350
+ } catch (err) {
6351
+ console.error(chalk2.red("[node9 daemon] GET /status failed:"), err);
6352
+ res.writeHead(500, { "Content-Type": "application/json" });
6353
+ return res.end(JSON.stringify({ error: "internal" }));
6354
+ }
6355
+ }
6356
+ if (req.method === "GET" && pathname === "/state/check") {
6357
+ const predicatesParam = reqUrl.searchParams.get("predicates") ?? "";
6358
+ const predicates = predicatesParam.split(",").filter(Boolean);
6359
+ const results = {};
6360
+ for (const p of predicates) {
6361
+ results[p] = sessionHistory.checkPredicate(p);
6362
+ }
6363
+ res.writeHead(200, { "Content-Type": "application/json" });
6364
+ return res.end(JSON.stringify(results));
6365
+ }
6029
6366
  if (req.method === "POST" && pathname === "/settings") {
6030
6367
  if (!validToken(req)) return res.writeHead(403).end();
6031
6368
  try {
@@ -6429,11 +6766,11 @@ __export(tail_exports, {
6429
6766
  startTail: () => startTail
6430
6767
  });
6431
6768
  import http2 from "http";
6432
- import chalk16 from "chalk";
6769
+ import chalk17 from "chalk";
6433
6770
  import fs24 from "fs";
6434
- import os21 from "os";
6435
- import path26 from "path";
6436
- import readline3 from "readline";
6771
+ import os20 from "os";
6772
+ import path27 from "path";
6773
+ import readline4 from "readline";
6437
6774
  import { spawn as spawn9, execSync as execSync3 } from "child_process";
6438
6775
  function getIcon(tool) {
6439
6776
  const t = tool.toLowerCase();
@@ -6448,27 +6785,27 @@ function formatBase(activity) {
6448
6785
  const toolName = activity.tool.slice(0, 16).padEnd(16);
6449
6786
  const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
6450
6787
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
6451
- return `${chalk16.gray(time)} ${icon} ${chalk16.white.bold(toolName)} ${chalk16.dim(argsPreview)}`;
6788
+ return `${chalk17.gray(time)} ${icon} ${chalk17.white.bold(toolName)} ${chalk17.dim(argsPreview)}`;
6452
6789
  }
6453
6790
  function renderResult(activity, result) {
6454
6791
  const base = formatBase(activity);
6455
6792
  let status;
6456
6793
  if (result.status === "allow") {
6457
- status = chalk16.green("\u2713 ALLOW");
6794
+ status = chalk17.green("\u2713 ALLOW");
6458
6795
  } else if (result.status === "dlp") {
6459
- status = chalk16.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
6796
+ status = chalk17.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
6460
6797
  } else {
6461
- status = chalk16.red("\u2717 BLOCK");
6798
+ status = chalk17.red("\u2717 BLOCK");
6462
6799
  }
6463
6800
  if (process.stdout.isTTY) {
6464
- readline3.clearLine(process.stdout, 0);
6465
- readline3.cursorTo(process.stdout, 0);
6801
+ readline4.clearLine(process.stdout, 0);
6802
+ readline4.cursorTo(process.stdout, 0);
6466
6803
  }
6467
6804
  console.log(`${base} ${status}`);
6468
6805
  }
6469
6806
  function renderPending(activity) {
6470
6807
  if (!process.stdout.isTTY) return;
6471
- process.stdout.write(`${formatBase(activity)} ${chalk16.yellow("\u25CF \u2026")}\r`);
6808
+ process.stdout.write(`${formatBase(activity)} ${chalk17.yellow("\u25CF \u2026")}\r`);
6472
6809
  }
6473
6810
  async function ensureDaemon() {
6474
6811
  let pidPort = null;
@@ -6477,7 +6814,7 @@ async function ensureDaemon() {
6477
6814
  const { port } = JSON.parse(fs24.readFileSync(PID_FILE, "utf-8"));
6478
6815
  pidPort = port;
6479
6816
  } catch {
6480
- console.error(chalk16.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
6817
+ console.error(chalk17.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
6481
6818
  }
6482
6819
  }
6483
6820
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -6488,7 +6825,7 @@ async function ensureDaemon() {
6488
6825
  if (res.ok) return checkPort;
6489
6826
  } catch {
6490
6827
  }
6491
- console.log(chalk16.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6828
+ console.log(chalk17.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6492
6829
  const child = spawn9(process.execPath, [process.argv[1], "daemon"], {
6493
6830
  detached: true,
6494
6831
  stdio: "ignore",
@@ -6505,14 +6842,15 @@ async function ensureDaemon() {
6505
6842
  } catch {
6506
6843
  }
6507
6844
  }
6508
- console.error(chalk16.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6845
+ console.error(chalk17.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6509
6846
  process.exit(1);
6510
6847
  }
6511
6848
  function postDecisionHttp(id, decision, csrfToken, port, opts) {
6512
6849
  return new Promise((resolve, reject) => {
6513
- const bodyObj = { decision, source: "terminal" };
6850
+ const bodyObj = { decision, source: opts?.source ?? "terminal" };
6514
6851
  if (opts?.persist) bodyObj.persist = true;
6515
6852
  if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
6853
+ if (opts?.reason) bodyObj.reason = opts.reason;
6516
6854
  const body = JSON.stringify(bodyObj);
6517
6855
  const req = http2.request(
6518
6856
  {
@@ -6537,33 +6875,61 @@ function postDecisionHttp(id, decision, csrfToken, port, opts) {
6537
6875
  });
6538
6876
  }
6539
6877
  function buildCardLines(req, localCount = 0) {
6878
+ if (req.recoveryCommand) {
6879
+ return buildRecoveryCardLines(req);
6880
+ }
6540
6881
  const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
6541
6882
  const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
6542
6883
  const tierLabel = req.riskMetadata?.tier != null ? req.riskMetadata.tier <= 2 ? `${YELLOW}\u26A0 Tier ${req.riskMetadata.tier}` : `${RED}\u{1F6D1} Tier ${req.riskMetadata.tier}` : `${YELLOW}\u26A0 Review`;
6543
6884
  const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
6544
6885
  const lines = [
6545
6886
  ``,
6546
- `${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
6547
- `${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
6548
- `${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`
6887
+ `${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
6888
+ `${CYAN}\u2551${RESET2} Tool: ${BOLD2}${req.toolName}${RESET2}`,
6889
+ `${CYAN}\u2551${RESET2} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET2}`
6549
6890
  ];
6550
6891
  if (req.riskMetadata?.ruleName && blockedBy.includes("Taint")) {
6551
- lines.push(`${CYAN}\u2551${RESET} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET}`);
6892
+ lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET2}`);
6552
6893
  }
6553
- lines.push(`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`);
6894
+ lines.push(`${CYAN}\u2551${RESET2} Args: ${GRAY}${argsPreview}${RESET2}`);
6554
6895
  if (localCount >= 2) {
6555
6896
  lines.push(
6556
- `${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
6897
+ `${CYAN}\u2551${RESET2} ${YELLOW}\u{1F4A1}${RESET2} Approved ${localCount}\xD7 before \u2014 ${BOLD2}[a]${RESET2}${YELLOW} creates a permanent rule${RESET2}`
6557
6898
  );
6558
6899
  }
6559
6900
  lines.push(
6560
- `${CYAN}\u255A${RESET}`,
6901
+ `${CYAN}\u255A${RESET2}`,
6561
6902
  ``,
6562
- ` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${YELLOW}[a]${RESET} Always Allow ${BOLD}${CYAN}[t]${RESET} Trust 30m`,
6903
+ ` ${BOLD2}${GREEN}[\u21B5/y]${RESET2} Allow ${BOLD2}${RED}[n]${RESET2} Deny ${BOLD2}${YELLOW}[a]${RESET2} Always Allow ${BOLD2}${CYAN}[t]${RESET2} Trust 30m`,
6563
6904
  ``
6564
6905
  );
6565
6906
  return lines;
6566
6907
  }
6908
+ function buildRecoveryCardLines(req) {
6909
+ const argsObj = req.args;
6910
+ const command = typeof argsObj?.command === "string" ? argsObj.command : JSON.stringify(req.args ?? {}).replace(/\s+/g, " ").slice(0, 60);
6911
+ const ruleName = req.riskMetadata?.ruleName?.replace(/^Smart Rule:\s*/i, "") ?? "policy rule";
6912
+ const recoveryCommand = req.recoveryCommand;
6913
+ const interactiveLines = req.viewOnly ? [` ${GRAY}\u2192 Awaiting decision from interactive terminal...${RESET2}`] : [
6914
+ ` ${BOLD2}${GREEN}[1]${RESET2} Allow anyway ${GRAY}(override policy)${RESET2}`,
6915
+ ` ${BOLD2}${YELLOW}[2]${RESET2} Redirect AI: "Run '${recoveryCommand}' first, then retry"`,
6916
+ ` ${BOLD2}${RED}[3]${RESET2} Deny & stop ${GRAY}(hard block)${RESET2}`,
6917
+ ``,
6918
+ ` ${GRAY}[Timeout: auto-deny]${RESET2}`,
6919
+ ` Select [1-3]: `
6920
+ ];
6921
+ return [
6922
+ ``,
6923
+ `${BOLD2}${CYAN}${DIVIDER}${RESET2}`,
6924
+ `\u{1F6E1}\uFE0F ${BOLD2}NODE9 STATE GUARD:${RESET2} '${BOLD2}${command}${RESET2}'`,
6925
+ `${YELLOW}\u26A0\uFE0F Rule: ${ruleName}${RESET2}`,
6926
+ `${CYAN}${DIVIDER}${RESET2}`,
6927
+ ...!req.viewOnly ? [`${BOLD2}What would you like to do?${RESET2}`, ``] : [],
6928
+ ...interactiveLines,
6929
+ `${CYAN}${DIVIDER}${RESET2}`,
6930
+ ``
6931
+ ];
6932
+ }
6567
6933
  async function startTail(options = {}) {
6568
6934
  const port = await ensureDaemon();
6569
6935
  if (options.clear) {
@@ -6590,7 +6956,7 @@ async function startTail(options = {}) {
6590
6956
  req2.end();
6591
6957
  });
6592
6958
  if (result.ok) {
6593
- console.log(chalk16.green("\u2713 Flight Recorder buffer cleared."));
6959
+ console.log(chalk17.green("\u2713 Flight Recorder buffer cleared."));
6594
6960
  } else if (result.code === "ECONNREFUSED") {
6595
6961
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
6596
6962
  } else if (result.code === "ETIMEDOUT") {
@@ -6609,10 +6975,10 @@ async function startTail(options = {}) {
6609
6975
  let cancelActiveCard = null;
6610
6976
  const localAllowCounts = /* @__PURE__ */ new Map();
6611
6977
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
6612
- if (canApprove) readline3.emitKeypressEvents(process.stdin);
6978
+ if (canApprove) readline4.emitKeypressEvents(process.stdin);
6613
6979
  function clearCard() {
6614
6980
  if (cardLineCount > 0) {
6615
- readline3.moveCursor(process.stdout, 0, -cardLineCount);
6981
+ readline4.moveCursor(process.stdout, 0, -cardLineCount);
6616
6982
  process.stdout.write(ERASE_DOWN);
6617
6983
  cardLineCount = 0;
6618
6984
  }
@@ -6662,14 +7028,14 @@ async function startTail(options = {}) {
6662
7028
  localAllowCounts.get(req2.toolName) ?? 0
6663
7029
  )
6664
7030
  );
6665
- const decisionStamp = action === "always-allow" ? chalk16.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk16.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk16.green("\u2713 ALLOWED") : chalk16.red("\u2717 DENIED");
6666
- stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
7031
+ const decisionStamp = action === "always-allow" ? chalk17.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk17.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk17.green("\u2713 ALLOWED") : action === "redirect" ? chalk17.yellow("\u21A9 REDIRECT AI") : chalk17.red("\u2717 DENIED");
7032
+ stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
6667
7033
  for (const line of stampedLines) process.stdout.write(line + "\n");
6668
7034
  process.stdout.write(SHOW_CURSOR);
6669
7035
  cardLineCount = 0;
6670
7036
  if (action === "allow" || action === "always-allow" || action === "trust") {
6671
7037
  localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
6672
- } else if (action === "deny") {
7038
+ } else if (action === "deny" || action === "redirect") {
6673
7039
  localAllowCounts.delete(req2.toolName);
6674
7040
  }
6675
7041
  let httpDecision;
@@ -6680,13 +7046,18 @@ async function startTail(options = {}) {
6680
7046
  } else if (action === "trust") {
6681
7047
  httpDecision = "trust";
6682
7048
  httpOpts = { trustDuration: "30m" };
7049
+ } else if (action === "redirect") {
7050
+ httpDecision = "deny";
7051
+ const recoveryCommand = req2.recoveryCommand ?? "the required pre-condition";
7052
+ const redirectReason = `USER INTERVENTION: I am blocking this ${req2.toolName} because the required pre-condition has not been met. Please execute \`${recoveryCommand}\`. If it passes, you are then authorized to run \`${req2.toolName}\`.`;
7053
+ httpOpts = { reason: redirectReason, source: "terminal-redirect" };
6683
7054
  } else {
6684
7055
  httpDecision = action;
6685
7056
  }
6686
7057
  postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
6687
7058
  try {
6688
7059
  fs24.appendFileSync(
6689
- path26.join(os21.homedir(), ".node9", "hook-debug.log"),
7060
+ path27.join(os20.homedir(), ".node9", "hook-debug.log"),
6690
7061
  `[tail] POST /decision failed: ${String(err)}
6691
7062
  `
6692
7063
  );
@@ -6708,8 +7079,8 @@ async function startTail(options = {}) {
6708
7079
  );
6709
7080
  const stampedLines = buildCardLines(req2, priorCount);
6710
7081
  if (externalDecision) {
6711
- const source = externalDecision === "allow" ? chalk16.green("\u2713 ALLOWED") : chalk16.red("\u2717 DENIED");
6712
- stampedLines.push(` ${BOLD}\u2192${RESET} ${source} ${GRAY}(external)${RESET}`, ``);
7082
+ const source = externalDecision === "allow" ? chalk17.green("\u2713 ALLOWED") : chalk17.red("\u2717 DENIED");
7083
+ stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
6713
7084
  }
6714
7085
  for (const line of stampedLines) process.stdout.write(line + "\n");
6715
7086
  process.stdout.write(SHOW_CURSOR);
@@ -6718,17 +7089,34 @@ async function startTail(options = {}) {
6718
7089
  cardActive = false;
6719
7090
  showNextCard();
6720
7091
  };
7092
+ if (req2.viewOnly) {
7093
+ process.stdin.resume();
7094
+ onKeypress = () => {
7095
+ };
7096
+ process.stdin.on("keypress", onKeypress);
7097
+ return;
7098
+ }
6721
7099
  process.stdin.resume();
6722
7100
  onKeypress = (_str, key) => {
6723
7101
  const name = key?.name ?? "";
6724
- if (name === "y" || name === "return") {
6725
- settle("allow");
6726
- } else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
6727
- settle("deny");
6728
- } else if (name === "a") {
6729
- settle("always-allow");
6730
- } else if (name === "t") {
6731
- settle("trust");
7102
+ if (req2.recoveryCommand) {
7103
+ if (name === "1") {
7104
+ settle("allow");
7105
+ } else if (name === "2") {
7106
+ settle("redirect");
7107
+ } else if (name === "3" || key?.ctrl && name === "c") {
7108
+ settle("deny");
7109
+ }
7110
+ } else {
7111
+ if (name === "y" || name === "return") {
7112
+ settle("allow");
7113
+ } else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
7114
+ settle("deny");
7115
+ } else if (name === "a") {
7116
+ settle("always-allow");
7117
+ } else if (name === "t") {
7118
+ settle("trust");
7119
+ }
6732
7120
  }
6733
7121
  };
6734
7122
  process.stdin.on("keypress", onKeypress);
@@ -6750,41 +7138,41 @@ async function startTail(options = {}) {
6750
7138
  }
6751
7139
  } catch {
6752
7140
  }
6753
- console.log(chalk16.cyan.bold(`
6754
- \u{1F6F0}\uFE0F Node9 tail `) + chalk16.dim(`\u2192 ${dashboardUrl}`));
7141
+ console.log(chalk17.cyan.bold(`
7142
+ \u{1F6F0}\uFE0F Node9 tail `) + chalk17.dim(`\u2192 ${dashboardUrl}`));
6755
7143
  if (canApprove) {
6756
7144
  console.log(
6757
- chalk16.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
7145
+ chalk17.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
6758
7146
  );
6759
7147
  }
6760
7148
  if (options.history) {
6761
- console.log(chalk16.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
7149
+ console.log(chalk17.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
6762
7150
  } else {
6763
7151
  console.log(
6764
- chalk16.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
7152
+ chalk17.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
6765
7153
  );
6766
7154
  }
6767
7155
  process.on("SIGINT", () => {
6768
7156
  clearCard();
6769
7157
  process.stdout.write(SHOW_CURSOR);
6770
7158
  if (process.stdout.isTTY) {
6771
- readline3.clearLine(process.stdout, 0);
6772
- readline3.cursorTo(process.stdout, 0);
7159
+ readline4.clearLine(process.stdout, 0);
7160
+ readline4.cursorTo(process.stdout, 0);
6773
7161
  }
6774
- console.log(chalk16.dim("\n\u{1F6F0}\uFE0F Disconnected."));
7162
+ console.log(chalk17.dim("\n\u{1F6F0}\uFE0F Disconnected."));
6775
7163
  process.exit(0);
6776
7164
  });
6777
7165
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
6778
7166
  const req = http2.get(sseUrl, (res) => {
6779
7167
  if (res.statusCode !== 200) {
6780
- console.error(chalk16.red(`Failed to connect: HTTP ${res.statusCode}`));
7168
+ console.error(chalk17.red(`Failed to connect: HTTP ${res.statusCode}`));
6781
7169
  process.exit(1);
6782
7170
  }
6783
7171
  let currentEvent = "";
6784
7172
  let currentData = "";
6785
7173
  res.on("error", () => {
6786
7174
  });
6787
- const rl = readline3.createInterface({ input: res, crlfDelay: Infinity });
7175
+ const rl = readline4.createInterface({ input: res, crlfDelay: Infinity });
6788
7176
  rl.on("error", () => {
6789
7177
  });
6790
7178
  rl.on("line", (line) => {
@@ -6804,10 +7192,10 @@ async function startTail(options = {}) {
6804
7192
  clearCard();
6805
7193
  process.stdout.write(SHOW_CURSOR);
6806
7194
  if (process.stdout.isTTY) {
6807
- readline3.clearLine(process.stdout, 0);
6808
- readline3.cursorTo(process.stdout, 0);
7195
+ readline4.clearLine(process.stdout, 0);
7196
+ readline4.cursorTo(process.stdout, 0);
6809
7197
  }
6810
- console.log(chalk16.red("\n\u274C Daemon disconnected."));
7198
+ console.log(chalk17.red("\n\u274C Daemon disconnected."));
6811
7199
  process.exit(1);
6812
7200
  });
6813
7201
  });
@@ -6893,19 +7281,19 @@ async function startTail(options = {}) {
6893
7281
  }
6894
7282
  req.on("error", (err) => {
6895
7283
  const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
6896
- console.error(chalk16.red(`
7284
+ console.error(chalk17.red(`
6897
7285
  \u274C ${msg}`));
6898
7286
  process.exit(1);
6899
7287
  });
6900
7288
  }
6901
- var PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN;
7289
+ var PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
6902
7290
  var init_tail = __esm({
6903
7291
  "src/tui/tail.ts"() {
6904
7292
  "use strict";
6905
7293
  init_daemon2();
6906
7294
  init_daemon();
6907
7295
  init_core();
6908
- PID_FILE = path26.join(os21.homedir(), ".node9", "daemon.pid");
7296
+ PID_FILE = path27.join(os20.homedir(), ".node9", "daemon.pid");
6909
7297
  ICONS = {
6910
7298
  bash: "\u{1F4BB}",
6911
7299
  shell: "\u{1F4BB}",
@@ -6923,8 +7311,8 @@ var init_tail = __esm({
6923
7311
  delete: "\u{1F5D1}\uFE0F",
6924
7312
  web: "\u{1F310}"
6925
7313
  };
6926
- RESET = "\x1B[0m";
6927
- BOLD = "\x1B[1m";
7314
+ RESET2 = "\x1B[0m";
7315
+ BOLD2 = "\x1B[1m";
6928
7316
  RED = "\x1B[31m";
6929
7317
  YELLOW = "\x1B[33m";
6930
7318
  CYAN = "\x1B[36m";
@@ -6933,6 +7321,326 @@ var init_tail = __esm({
6933
7321
  HIDE_CURSOR = "\x1B[?25l";
6934
7322
  SHOW_CURSOR = "\x1B[?25h";
6935
7323
  ERASE_DOWN = "\x1B[J";
7324
+ DIVIDER = "\u2500".repeat(60);
7325
+ }
7326
+ });
7327
+
7328
+ // src/cli/hud.ts
7329
+ var hud_exports = {};
7330
+ __export(hud_exports, {
7331
+ countConfigs: () => countConfigs,
7332
+ main: () => main,
7333
+ renderEnvironmentLine: () => renderEnvironmentLine
7334
+ });
7335
+ import fs25 from "fs";
7336
+ import path28 from "path";
7337
+ import os21 from "os";
7338
+ import http3 from "http";
7339
+ async function readStdin() {
7340
+ const chunks = [];
7341
+ for await (const chunk of process.stdin) {
7342
+ chunks.push(chunk);
7343
+ }
7344
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
7345
+ if (!raw) return {};
7346
+ try {
7347
+ return JSON.parse(raw);
7348
+ } catch {
7349
+ return {};
7350
+ }
7351
+ }
7352
+ function queryDaemon() {
7353
+ return new Promise((resolve) => {
7354
+ const timeout = setTimeout(() => resolve(null), 50);
7355
+ try {
7356
+ const req = http3.get(
7357
+ `http://${DAEMON_HOST}:${DAEMON_PORT}/status`,
7358
+ { timeout: 50 },
7359
+ (res) => {
7360
+ const chunks = [];
7361
+ res.on("data", (c) => chunks.push(c));
7362
+ res.on("end", () => {
7363
+ clearTimeout(timeout);
7364
+ try {
7365
+ resolve(JSON.parse(Buffer.concat(chunks).toString()));
7366
+ } catch {
7367
+ resolve(null);
7368
+ }
7369
+ });
7370
+ }
7371
+ );
7372
+ req.on("error", () => {
7373
+ clearTimeout(timeout);
7374
+ resolve(null);
7375
+ });
7376
+ req.on("timeout", () => {
7377
+ clearTimeout(timeout);
7378
+ req.destroy();
7379
+ resolve(null);
7380
+ });
7381
+ } catch {
7382
+ clearTimeout(timeout);
7383
+ resolve(null);
7384
+ }
7385
+ });
7386
+ }
7387
+ function dim(s) {
7388
+ return `${DIM}${s}${RESET3}`;
7389
+ }
7390
+ function bold(s) {
7391
+ return `${BOLD3}${s}${RESET3}`;
7392
+ }
7393
+ function color(c, s) {
7394
+ return `${c}${s}${RESET3}`;
7395
+ }
7396
+ function progressBar(pct, warnAt = 70, critAt = 85) {
7397
+ const filled = Math.round(Math.min(pct, 100) / 100 * BAR_WIDTH);
7398
+ const bar = BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
7399
+ const c = pct >= critAt ? RED2 : pct >= warnAt ? YELLOW2 : GREEN2;
7400
+ return `${c}${bar}${RESET3}`;
7401
+ }
7402
+ function formatTimeLeft(resetsAt) {
7403
+ if (!resetsAt) return "";
7404
+ const ms = new Date(resetsAt).getTime() - Date.now();
7405
+ if (ms <= 0) return "";
7406
+ const totalMin = Math.ceil(ms / 6e4);
7407
+ const h = Math.floor(totalMin / 60);
7408
+ const m = totalMin % 60;
7409
+ if (h > 0) return ` (${h}h ${m}m left)`;
7410
+ return ` (${m}m left)`;
7411
+ }
7412
+ function safeReadJson(filePath) {
7413
+ if (!fs25.existsSync(filePath)) return null;
7414
+ try {
7415
+ return JSON.parse(fs25.readFileSync(filePath, "utf-8"));
7416
+ } catch {
7417
+ return null;
7418
+ }
7419
+ }
7420
+ function getMcpServerNames(filePath) {
7421
+ const cfg = safeReadJson(filePath);
7422
+ if (!cfg || typeof cfg.mcpServers !== "object" || cfg.mcpServers === null) return /* @__PURE__ */ new Set();
7423
+ return new Set(Object.keys(cfg.mcpServers));
7424
+ }
7425
+ function getDisabledMcpServers(filePath, key) {
7426
+ const cfg = safeReadJson(filePath);
7427
+ if (!cfg || !Array.isArray(cfg[key])) return /* @__PURE__ */ new Set();
7428
+ return new Set(cfg[key].filter((s) => typeof s === "string"));
7429
+ }
7430
+ function countHooksInFile(filePath) {
7431
+ const cfg = safeReadJson(filePath);
7432
+ if (!cfg || typeof cfg.hooks !== "object" || cfg.hooks === null) return 0;
7433
+ return Object.keys(cfg.hooks).length;
7434
+ }
7435
+ function countRulesInDir(rulesDir) {
7436
+ if (!fs25.existsSync(rulesDir)) return 0;
7437
+ let count = 0;
7438
+ try {
7439
+ for (const entry of fs25.readdirSync(rulesDir, { withFileTypes: true })) {
7440
+ if (entry.isDirectory()) {
7441
+ count += countRulesInDir(path28.join(rulesDir, entry.name));
7442
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
7443
+ count++;
7444
+ }
7445
+ }
7446
+ } catch {
7447
+ }
7448
+ return count;
7449
+ }
7450
+ function isSamePath(a, b) {
7451
+ try {
7452
+ return path28.resolve(a) === path28.resolve(b);
7453
+ } catch {
7454
+ return false;
7455
+ }
7456
+ }
7457
+ function countConfigs(cwd) {
7458
+ const homeDir2 = os21.homedir();
7459
+ const claudeDir = path28.join(homeDir2, ".claude");
7460
+ let claudeMdCount = 0;
7461
+ let rulesCount = 0;
7462
+ let hooksCount = 0;
7463
+ const userMcpServers = /* @__PURE__ */ new Set();
7464
+ const projectMcpServers = /* @__PURE__ */ new Set();
7465
+ if (fs25.existsSync(path28.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7466
+ rulesCount += countRulesInDir(path28.join(claudeDir, "rules"));
7467
+ const userSettings = path28.join(claudeDir, "settings.json");
7468
+ for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
7469
+ hooksCount += countHooksInFile(userSettings);
7470
+ const userClaudeJson = path28.join(homeDir2, ".claude.json");
7471
+ for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
7472
+ for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
7473
+ userMcpServers.delete(name);
7474
+ }
7475
+ if (cwd) {
7476
+ if (fs25.existsSync(path28.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7477
+ if (fs25.existsSync(path28.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7478
+ const projectClaudeDir = path28.join(cwd, ".claude");
7479
+ const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
7480
+ if (!overlapsUserScope) {
7481
+ if (fs25.existsSync(path28.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7482
+ rulesCount += countRulesInDir(path28.join(projectClaudeDir, "rules"));
7483
+ const projSettings = path28.join(projectClaudeDir, "settings.json");
7484
+ for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
7485
+ hooksCount += countHooksInFile(projSettings);
7486
+ }
7487
+ if (fs25.existsSync(path28.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7488
+ const localSettings = path28.join(projectClaudeDir, "settings.local.json");
7489
+ for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
7490
+ hooksCount += countHooksInFile(localSettings);
7491
+ const mcpJsonServers = getMcpServerNames(path28.join(cwd, ".mcp.json"));
7492
+ const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
7493
+ for (const name of disabledMcpJson) mcpJsonServers.delete(name);
7494
+ for (const name of mcpJsonServers) projectMcpServers.add(name);
7495
+ }
7496
+ return {
7497
+ claudeMdCount,
7498
+ rulesCount,
7499
+ mcpCount: userMcpServers.size + projectMcpServers.size,
7500
+ hooksCount
7501
+ };
7502
+ }
7503
+ function renderEnvironmentLine(counts) {
7504
+ const { claudeMdCount, rulesCount, mcpCount, hooksCount } = counts;
7505
+ if (claudeMdCount === 0 && rulesCount === 0 && mcpCount === 0 && hooksCount === 0) return null;
7506
+ const parts = [
7507
+ `${claudeMdCount} CLAUDE.md`,
7508
+ `${rulesCount} rules`,
7509
+ `${mcpCount} MCPs`,
7510
+ `${hooksCount} hooks`
7511
+ ];
7512
+ return color(DIM, parts.join(` ${dim("|")} `));
7513
+ }
7514
+ function renderOffline() {
7515
+ process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
7516
+ `);
7517
+ }
7518
+ function renderSecurityLine(status) {
7519
+ const parts = [];
7520
+ parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
7521
+ const modeColors = {
7522
+ standard: GREEN2,
7523
+ strict: RED2,
7524
+ observe: MAGENTA,
7525
+ audit: YELLOW2
7526
+ };
7527
+ const modeIcon = {
7528
+ standard: "",
7529
+ strict: "",
7530
+ observe: "\u{1F441} ",
7531
+ audit: ""
7532
+ };
7533
+ const mc = modeColors[status.mode] ?? WHITE;
7534
+ parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
7535
+ if (status.mode === "observe") {
7536
+ parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
7537
+ if (status.session.wouldBlock > 0) {
7538
+ parts.push(color(YELLOW2, `\u26A0 ${status.session.wouldBlock} would-block`));
7539
+ }
7540
+ } else {
7541
+ parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} allowed`)}`);
7542
+ if (status.session.blocked > 0) {
7543
+ parts.push(color(RED2, `\u{1F6D1} ${status.session.blocked} blocked`));
7544
+ }
7545
+ if (status.session.dlpHits > 0) {
7546
+ parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
7547
+ }
7548
+ }
7549
+ if (status.taintedCount > 0) {
7550
+ parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
7551
+ }
7552
+ if (status.lastRuleHit) {
7553
+ const ruleName = status.lastRuleHit.replace(/^Smart Rule:\s*/i, "");
7554
+ parts.push(color(CYAN2, `\u26A1 ${ruleName}`));
7555
+ }
7556
+ return parts.join(" ");
7557
+ }
7558
+ function renderContextLine(stdin) {
7559
+ const cw = stdin.context_window;
7560
+ if (!cw) return null;
7561
+ const parts = [];
7562
+ const modelName = typeof stdin.model === "string" ? stdin.model : stdin.model?.display_name ?? "";
7563
+ if (modelName) {
7564
+ parts.push(color(CYAN2, modelName));
7565
+ }
7566
+ const usedPct = cw.used_percentage ?? (cw.current_usage && cw.context_window_size ? Math.round(
7567
+ ((cw.current_usage.input_tokens ?? 0) + (cw.current_usage.output_tokens ?? 0)) / cw.context_window_size * 100
7568
+ ) : null);
7569
+ if (usedPct !== null) {
7570
+ const bar = progressBar(usedPct);
7571
+ parts.push(`${dim("\u2502")} ctx ${bar} ${usedPct}%`);
7572
+ }
7573
+ const rl = stdin.rate_limits;
7574
+ if (rl?.five_hour?.used_percentage !== void 0) {
7575
+ const pct = Math.round(rl.five_hour.used_percentage);
7576
+ const bar = progressBar(pct, 60, 80);
7577
+ const left = formatTimeLeft(rl.five_hour.resets_at);
7578
+ parts.push(`${dim("\u2502")} 5h ${bar} ${pct}%${left}`);
7579
+ }
7580
+ if (rl?.seven_day?.used_percentage !== void 0) {
7581
+ const pct = Math.round(rl.seven_day.used_percentage);
7582
+ const bar = progressBar(pct, 60, 80);
7583
+ parts.push(`${dim("\u2502")} 7d ${bar} ${pct}%`);
7584
+ }
7585
+ if (parts.length === 0) return null;
7586
+ return parts.join(" ");
7587
+ }
7588
+ async function main() {
7589
+ try {
7590
+ const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
7591
+ if (!daemonStatus2) {
7592
+ renderOffline();
7593
+ return;
7594
+ }
7595
+ process.stdout.write(renderSecurityLine(daemonStatus2) + "\n");
7596
+ const ctxLine = renderContextLine(stdin);
7597
+ if (ctxLine) {
7598
+ process.stdout.write(ctxLine + "\n");
7599
+ }
7600
+ const showEnvCounts = (() => {
7601
+ try {
7602
+ const cwd = stdin.cwd ?? process.cwd();
7603
+ for (const configPath of [
7604
+ path28.join(cwd, "node9.config.json"),
7605
+ path28.join(os21.homedir(), ".node9", "config.json")
7606
+ ]) {
7607
+ if (!fs25.existsSync(configPath)) continue;
7608
+ const cfg = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
7609
+ const hud = cfg.settings?.hud;
7610
+ if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
7611
+ }
7612
+ } catch {
7613
+ }
7614
+ return true;
7615
+ })();
7616
+ if (showEnvCounts) {
7617
+ const envLine = renderEnvironmentLine(countConfigs(stdin.cwd));
7618
+ if (envLine) {
7619
+ process.stdout.write(envLine + "\n");
7620
+ }
7621
+ }
7622
+ } catch {
7623
+ renderOffline();
7624
+ }
7625
+ }
7626
+ var RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
7627
+ var init_hud = __esm({
7628
+ "src/cli/hud.ts"() {
7629
+ "use strict";
7630
+ init_daemon();
7631
+ RESET3 = "\x1B[0m";
7632
+ BOLD3 = "\x1B[1m";
7633
+ DIM = "\x1B[2m";
7634
+ RED2 = "\x1B[31m";
7635
+ GREEN2 = "\x1B[32m";
7636
+ YELLOW2 = "\x1B[33m";
7637
+ BLUE = "\x1B[34m";
7638
+ MAGENTA = "\x1B[35m";
7639
+ CYAN2 = "\x1B[36m";
7640
+ WHITE = "\x1B[37m";
7641
+ BAR_FILLED = "\u2588";
7642
+ BAR_EMPTY = "\u2591";
7643
+ BAR_WIDTH = 10;
6936
7644
  }
6937
7645
  });
6938
7646
 
@@ -6943,7 +7651,7 @@ import { Command } from "commander";
6943
7651
  // src/setup.ts
6944
7652
  import fs11 from "fs";
6945
7653
  import path14 from "path";
6946
- import os11 from "os";
7654
+ import os10 from "os";
6947
7655
  import chalk from "chalk";
6948
7656
  import { confirm } from "@inquirer/prompts";
6949
7657
  function printDaemonTip() {
@@ -6977,7 +7685,7 @@ function isNode9Hook(cmd) {
6977
7685
  return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
6978
7686
  }
6979
7687
  function teardownClaude() {
6980
- const homeDir2 = os11.homedir();
7688
+ const homeDir2 = os10.homedir();
6981
7689
  const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
6982
7690
  const mcpPath = path14.join(homeDir2, ".claude.json");
6983
7691
  let changed = false;
@@ -7027,7 +7735,7 @@ function teardownClaude() {
7027
7735
  }
7028
7736
  }
7029
7737
  function teardownGemini() {
7030
- const homeDir2 = os11.homedir();
7738
+ const homeDir2 = os10.homedir();
7031
7739
  const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
7032
7740
  const settings = readJson(settingsPath);
7033
7741
  if (!settings) {
@@ -7066,7 +7774,7 @@ function teardownGemini() {
7066
7774
  }
7067
7775
  }
7068
7776
  function teardownCursor() {
7069
- const homeDir2 = os11.homedir();
7777
+ const homeDir2 = os10.homedir();
7070
7778
  const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
7071
7779
  const mcpConfig = readJson(mcpPath);
7072
7780
  if (!mcpConfig?.mcpServers) {
@@ -7093,7 +7801,7 @@ function teardownCursor() {
7093
7801
  }
7094
7802
  }
7095
7803
  async function setupClaude() {
7096
- const homeDir2 = os11.homedir();
7804
+ const homeDir2 = os10.homedir();
7097
7805
  const mcpPath = path14.join(homeDir2, ".claude.json");
7098
7806
  const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
7099
7807
  const claudeConfig = readJson(mcpPath) ?? {};
@@ -7169,7 +7877,7 @@ async function setupClaude() {
7169
7877
  }
7170
7878
  }
7171
7879
  async function setupGemini() {
7172
- const homeDir2 = os11.homedir();
7880
+ const homeDir2 = os10.homedir();
7173
7881
  const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
7174
7882
  const settings = readJson(settingsPath) ?? {};
7175
7883
  const servers = settings.mcpServers ?? {};
@@ -7251,7 +7959,7 @@ async function setupGemini() {
7251
7959
  printDaemonTip();
7252
7960
  }
7253
7961
  }
7254
- function detectAgents(homeDir2 = os11.homedir()) {
7962
+ function detectAgents(homeDir2 = os10.homedir()) {
7255
7963
  const exists = (p) => {
7256
7964
  try {
7257
7965
  return fs11.existsSync(p);
@@ -7271,7 +7979,7 @@ function detectAgents(homeDir2 = os11.homedir()) {
7271
7979
  };
7272
7980
  }
7273
7981
  async function setupCursor() {
7274
- const homeDir2 = os11.homedir();
7982
+ const homeDir2 = os10.homedir();
7275
7983
  const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
7276
7984
  const mcpConfig = readJson(mcpPath) ?? {};
7277
7985
  const servers = mcpConfig.mcpServers ?? {};
@@ -7325,14 +8033,60 @@ async function setupCursor() {
7325
8033
  printDaemonTip();
7326
8034
  }
7327
8035
  }
8036
+ function setupHud() {
8037
+ const homeDir2 = os10.homedir();
8038
+ const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
8039
+ const settings = readJson(hooksPath) ?? {};
8040
+ const hudCommand = fullPathCommand("hud");
8041
+ const statusLineObj = { type: "command", command: hudCommand };
8042
+ const existing = settings.statusLine;
8043
+ const existingCommand = typeof existing === "object" ? existing?.command : existing;
8044
+ if (existingCommand === hudCommand) {
8045
+ console.log(chalk.blue("\u2139\uFE0F node9 HUD is already configured in ~/.claude/settings.json"));
8046
+ console.log(chalk.gray(" Restart Claude Code to activate."));
8047
+ return;
8048
+ }
8049
+ if (existing && existingCommand !== hudCommand) {
8050
+ console.log(
8051
+ chalk.yellow(
8052
+ ` \u26A0\uFE0F statusLine is already set to: "${existingCommand}"
8053
+ Overwriting with node9 HUD.`
8054
+ )
8055
+ );
8056
+ }
8057
+ settings.statusLine = statusLineObj;
8058
+ writeJson(hooksPath, settings);
8059
+ console.log(chalk.green.bold("\u2705 node9 HUD added to Claude Code statusline"));
8060
+ console.log(chalk.gray(" Settings: ~/.claude/settings.json"));
8061
+ console.log(chalk.gray(" Restart Claude Code to activate."));
8062
+ }
8063
+ function teardownHud() {
8064
+ const homeDir2 = os10.homedir();
8065
+ const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
8066
+ const settings = readJson(hooksPath);
8067
+ if (!settings) {
8068
+ console.log(chalk.blue(" \u2139\uFE0F ~/.claude/settings.json not found \u2014 nothing to remove"));
8069
+ return;
8070
+ }
8071
+ const existing = settings.statusLine;
8072
+ const existingCommand = typeof existing === "object" ? existing?.command : existing;
8073
+ if (!existingCommand || !String(existingCommand).includes("node9")) {
8074
+ console.log(chalk.blue(" \u2139\uFE0F node9 HUD not found in ~/.claude/settings.json"));
8075
+ return;
8076
+ }
8077
+ delete settings.statusLine;
8078
+ writeJson(hooksPath, settings);
8079
+ console.log(chalk.green(" \u2705 node9 HUD removed from ~/.claude/settings.json"));
8080
+ console.log(chalk.gray(" Restart Claude Code for changes to take effect."));
8081
+ }
7328
8082
 
7329
8083
  // src/cli.ts
7330
8084
  init_daemon2();
7331
- import chalk17 from "chalk";
7332
- import fs25 from "fs";
7333
- import path27 from "path";
8085
+ import chalk18 from "chalk";
8086
+ import fs26 from "fs";
8087
+ import path29 from "path";
7334
8088
  import os22 from "os";
7335
- import { confirm as confirm3 } from "@inquirer/prompts";
8089
+ import { confirm as confirm2 } from "@inquirer/prompts";
7336
8090
 
7337
8091
  // src/utils/duration.ts
7338
8092
  function parseDuration(str) {
@@ -7362,7 +8116,7 @@ import { execa } from "execa";
7362
8116
  import { parseCommandString } from "execa";
7363
8117
 
7364
8118
  // src/policy/negotiation.ts
7365
- function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason) {
8119
+ function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason, recoveryCommand) {
7366
8120
  if (isHumanDecision) {
7367
8121
  return `NODE9: The human user rejected this action.
7368
8122
  REASON: ${humanReason || "No specific reason provided."}
@@ -7418,10 +8172,11 @@ INSTRUCTION: Inform the user this action is pending approval. Wait for them to a
7418
8172
  INSTRUCTION: Do NOT use "${rule}". Find a read-only or non-destructive alternative.
7419
8173
  Do NOT attempt to bypass this rule.`;
7420
8174
  }
8175
+ const recovery = recoveryCommand ? `
8176
+ REQUIRED ACTION: Run \`${recoveryCommand}\` first, then retry your original command.` : "\n- Pivot to a non-destructive or read-only alternative.";
7421
8177
  return `NODE9: Action blocked by security policy [${blockedByLabel}].
7422
8178
  INSTRUCTIONS:
7423
- - Do NOT retry this exact command or attempt to bypass the rule.
7424
- - Pivot to a non-destructive or read-only alternative.
8179
+ - Do NOT retry this exact command or attempt to bypass the rule.${recovery}
7425
8180
  - Inform the user which security rule was triggered and ask how to proceed.`;
7426
8181
  }
7427
8182
 
@@ -7522,6 +8277,7 @@ function openBrowserLocal() {
7522
8277
  }
7523
8278
  }
7524
8279
  async function autoStartDaemonAndWait() {
8280
+ if (process.env.NODE9_TESTING === "1") return false;
7525
8281
  try {
7526
8282
  const child = spawn4(process.execPath, [process.argv[1], "daemon"], {
7527
8283
  detached: true,
@@ -7558,16 +8314,16 @@ init_policy();
7558
8314
  import chalk5 from "chalk";
7559
8315
  import fs18 from "fs";
7560
8316
  import path20 from "path";
7561
- import os15 from "os";
8317
+ import os14 from "os";
7562
8318
 
7563
8319
  // src/undo.ts
7564
8320
  import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
7565
8321
  import crypto2 from "crypto";
7566
8322
  import fs17 from "fs";
7567
8323
  import path19 from "path";
7568
- import os14 from "os";
7569
- var SNAPSHOT_STACK_PATH = path19.join(os14.homedir(), ".node9", "snapshots.json");
7570
- var UNDO_LATEST_PATH = path19.join(os14.homedir(), ".node9", "undo_latest.txt");
8324
+ import os13 from "os";
8325
+ var SNAPSHOT_STACK_PATH = path19.join(os13.homedir(), ".node9", "snapshots.json");
8326
+ var UNDO_LATEST_PATH = path19.join(os13.homedir(), ".node9", "undo_latest.txt");
7571
8327
  var MAX_SNAPSHOTS = 10;
7572
8328
  var GIT_TIMEOUT = 15e3;
7573
8329
  function readStack() {
@@ -7583,16 +8339,33 @@ function writeStack(stack) {
7583
8339
  if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
7584
8340
  fs17.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
7585
8341
  }
8342
+ function extractFilePath(args) {
8343
+ if (!args || typeof args !== "object") return null;
8344
+ const a = args;
8345
+ const fp = a.file_path ?? a.path ?? a.filename;
8346
+ return typeof fp === "string" ? fp : null;
8347
+ }
7586
8348
  function buildArgsSummary(tool, args) {
8349
+ const filePath = extractFilePath(args);
8350
+ if (filePath) return filePath;
7587
8351
  if (!args || typeof args !== "object") return "";
7588
8352
  const a = args;
7589
- const filePath = a.file_path ?? a.path ?? a.filename;
7590
- if (typeof filePath === "string") return filePath;
7591
8353
  const cmd = a.command ?? a.cmd;
7592
8354
  if (typeof cmd === "string") return cmd.slice(0, 80);
7593
8355
  const sql = a.sql ?? a.query;
7594
8356
  if (typeof sql === "string") return sql.slice(0, 80);
7595
- return tool;
8357
+ return "";
8358
+ }
8359
+ function findProjectRoot(filePath) {
8360
+ let dir = path19.dirname(filePath);
8361
+ while (true) {
8362
+ if (fs17.existsSync(path19.join(dir, ".git")) || fs17.existsSync(path19.join(dir, "package.json"))) {
8363
+ return dir;
8364
+ }
8365
+ const parent = path19.dirname(dir);
8366
+ if (parent === dir) return process.cwd();
8367
+ dir = parent;
8368
+ }
7596
8369
  }
7597
8370
  function normalizeCwdForHash(cwd) {
7598
8371
  let normalized;
@@ -7607,7 +8380,7 @@ function normalizeCwdForHash(cwd) {
7607
8380
  }
7608
8381
  function getShadowRepoDir(cwd) {
7609
8382
  const hash = crypto2.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
7610
- return path19.join(os14.homedir(), ".node9", "snapshots", hash);
8383
+ return path19.join(os13.homedir(), ".node9", "snapshots", hash);
7611
8384
  }
7612
8385
  function cleanOrphanedIndexFiles(shadowDir) {
7613
8386
  try {
@@ -7663,9 +8436,9 @@ function ensureShadowRepo(shadowDir, cwd) {
7663
8436
  } catch {
7664
8437
  }
7665
8438
  const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
7666
- if (init.status !== 0) {
7667
- if (process.env.NODE9_DEBUG === "1")
7668
- console.error("[Node9] git init --bare failed:", init.stderr?.toString());
8439
+ if (init.status !== 0 || init.error) {
8440
+ const reason = init.error ? init.error.message : init.stderr?.toString();
8441
+ if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
7669
8442
  return false;
7670
8443
  }
7671
8444
  const configFile = path19.join(shadowDir, "config");
@@ -7695,7 +8468,9 @@ function buildGitEnv(cwd) {
7695
8468
  async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = []) {
7696
8469
  let indexFile = null;
7697
8470
  try {
7698
- const cwd = process.cwd();
8471
+ const rawFilePath = extractFilePath(args);
8472
+ const absFilePath = rawFilePath && path19.isAbsolute(rawFilePath) ? rawFilePath : null;
8473
+ const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
7699
8474
  const shadowDir = getShadowRepoDir(cwd);
7700
8475
  if (!ensureShadowRepo(shadowDir, cwd)) return null;
7701
8476
  writeShadowExcludes(shadowDir, ignorePaths);
@@ -7718,15 +8493,53 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
7718
8493
  const commitHash = commitRes.stdout?.toString().trim();
7719
8494
  if (!commitHash || commitRes.status !== 0) return null;
7720
8495
  const stack = readStack();
8496
+ const prevEntry = [...stack].reverse().find((e) => e.cwd === cwd);
8497
+ let capturedFiles = [];
8498
+ let capturedDiff = null;
8499
+ if (prevEntry) {
8500
+ const filesRes = spawnSync4("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
8501
+ env: shadowEnv,
8502
+ timeout: GIT_TIMEOUT
8503
+ });
8504
+ if (filesRes.status === 0) {
8505
+ capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
8506
+ }
8507
+ const diffRes = spawnSync4("git", ["diff", prevEntry.hash, commitHash], {
8508
+ env: shadowEnv,
8509
+ timeout: GIT_TIMEOUT
8510
+ });
8511
+ if (diffRes.status === 0) {
8512
+ capturedDiff = diffRes.stdout?.toString() || null;
8513
+ }
8514
+ } else {
8515
+ const filesRes = spawnSync4("git", ["ls-tree", "-r", "--name-only", commitHash], {
8516
+ env: shadowEnv,
8517
+ timeout: GIT_TIMEOUT
8518
+ });
8519
+ if (filesRes.status === 0) {
8520
+ capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
8521
+ }
8522
+ capturedDiff = null;
8523
+ }
7721
8524
  stack.push({
7722
8525
  hash: commitHash,
7723
8526
  tool,
7724
8527
  argsSummary: buildArgsSummary(tool, args),
8528
+ files: capturedFiles,
8529
+ diff: capturedDiff,
7725
8530
  cwd,
7726
8531
  timestamp: Date.now()
7727
8532
  });
7728
8533
  const shouldGc = stack.length % 5 === 0;
7729
- if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
8534
+ let cwdCount = 0;
8535
+ let oldestCwdIdx = -1;
8536
+ for (let i = 0; i < stack.length; i++) {
8537
+ if (stack[i].cwd === cwd) {
8538
+ if (oldestCwdIdx === -1) oldestCwdIdx = i;
8539
+ cwdCount++;
8540
+ }
8541
+ }
8542
+ if (cwdCount > MAX_SNAPSHOTS) stack.splice(oldestCwdIdx, 1);
7730
8543
  writeStack(stack);
7731
8544
  fs17.writeFileSync(UNDO_LATEST_PATH, commitHash);
7732
8545
  if (shouldGc) {
@@ -7782,14 +8595,21 @@ function applyUndo(hash, cwd) {
7782
8595
  env,
7783
8596
  timeout: GIT_TIMEOUT
7784
8597
  });
7785
- if (restore.status !== 0) return false;
8598
+ if (restore.status !== 0 || restore.error) {
8599
+ if (process.env.NODE9_DEBUG === "1") {
8600
+ const msg = restore.error ? restore.error.message : restore.stderr?.toString();
8601
+ console.error("[Node9] git restore failed:", msg);
8602
+ }
8603
+ return false;
8604
+ }
7786
8605
  const lsTree = spawnSync4("git", ["ls-tree", "-r", "--name-only", hash], {
7787
8606
  cwd: dir,
7788
8607
  env,
7789
8608
  timeout: GIT_TIMEOUT
7790
8609
  });
7791
8610
  if (lsTree.status !== 0) {
7792
- process.stderr.write(`[Node9] applyUndo: git ls-tree failed for hash ${hash}
8611
+ const errorMsg = lsTree.stderr?.toString() || "Unknown git error";
8612
+ process.stderr.write(`[Node9] applyUndo: git ls-tree failed for hash ${hash}: ${errorMsg}
7793
8613
  `);
7794
8614
  return false;
7795
8615
  }
@@ -7834,7 +8654,7 @@ function registerCheckCommand(program2) {
7834
8654
  } catch (err) {
7835
8655
  const tempConfig = getConfig();
7836
8656
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
7837
- const logPath = path20.join(os15.homedir(), ".node9", "hook-debug.log");
8657
+ const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
7838
8658
  const errMsg = err instanceof Error ? err.message : String(err);
7839
8659
  fs18.appendFileSync(
7840
8660
  logPath,
@@ -7847,7 +8667,7 @@ RAW: ${raw}
7847
8667
  }
7848
8668
  const config = getConfig(payload.cwd || void 0);
7849
8669
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
7850
- const logPath = path20.join(os15.homedir(), ".node9", "hook-debug.log");
8670
+ const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
7851
8671
  if (!fs18.existsSync(path20.dirname(logPath)))
7852
8672
  fs18.mkdirSync(path20.dirname(logPath), { recursive: true });
7853
8673
  fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
@@ -7875,6 +8695,8 @@ RAW: ${raw}
7875
8695
  }
7876
8696
  writeTty(chalk5.gray(` Triggered by: ${blockedByContext}`));
7877
8697
  if (result2?.changeHint) writeTty(chalk5.cyan(` To change: ${result2.changeHint}`));
8698
+ if (result2?.recoveryCommand)
8699
+ writeTty(chalk5.green(` \u{1F4A1} Run: ${result2.recoveryCommand}`));
7878
8700
  writeTty("");
7879
8701
  } catch {
7880
8702
  } finally {
@@ -7887,7 +8709,8 @@ RAW: ${raw}
7887
8709
  const aiFeedbackMessage = buildNegotiationMessage(
7888
8710
  blockedByContext,
7889
8711
  isHumanDecision,
7890
- msg
8712
+ msg,
8713
+ result2?.recoveryCommand
7891
8714
  );
7892
8715
  process.stdout.write(
7893
8716
  JSON.stringify({
@@ -7955,7 +8778,7 @@ RAW: ${raw}
7955
8778
  });
7956
8779
  } catch (err) {
7957
8780
  if (process.env.NODE9_DEBUG === "1") {
7958
- const logPath = path20.join(os15.homedir(), ".node9", "hook-debug.log");
8781
+ const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
7959
8782
  const errMsg = err instanceof Error ? err.message : String(err);
7960
8783
  fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
7961
8784
  `);
@@ -7996,7 +8819,7 @@ init_config();
7996
8819
  init_policy();
7997
8820
  import fs19 from "fs";
7998
8821
  import path21 from "path";
7999
- import os16 from "os";
8822
+ import os15 from "os";
8000
8823
  init_daemon();
8001
8824
 
8002
8825
  // src/utils/cp-mv-parser.ts
@@ -8037,6 +8860,20 @@ function containsShellMetachar(token) {
8037
8860
  }
8038
8861
 
8039
8862
  // src/cli/commands/log.ts
8863
+ var TEST_COMMAND_RE = /(?:^|\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;
8864
+ function detectTestResult(command, output) {
8865
+ if (!TEST_COMMAND_RE.test(command)) return null;
8866
+ const out = output.toLowerCase();
8867
+ if (/\b(tests?\s+passed|all\s+tests?\s+passed|passing|test\s+suites?.*passed|ok\b|\d+\s+passed)/i.test(
8868
+ out
8869
+ ) && !/\b(fail|error|failed)\b/.test(out)) {
8870
+ return "pass";
8871
+ }
8872
+ if (/\b(tests?\s+failed|failing|failed|error|assertion\s+error|\d+\s+failed)\b/i.test(out)) {
8873
+ return "fail";
8874
+ }
8875
+ return null;
8876
+ }
8040
8877
  function sanitize3(value) {
8041
8878
  return value.replace(/[\x00-\x1F\x7F]/g, "");
8042
8879
  }
@@ -8055,7 +8892,7 @@ function registerLogCommand(program2) {
8055
8892
  decision: "allowed",
8056
8893
  source: "post-hook"
8057
8894
  };
8058
- const logPath = path21.join(os16.homedir(), ".node9", "audit.log");
8895
+ const logPath = path21.join(os15.homedir(), ".node9", "audit.log");
8059
8896
  if (!fs19.existsSync(path21.dirname(logPath)))
8060
8897
  fs19.mkdirSync(path21.dirname(logPath), { recursive: true });
8061
8898
  fs19.appendFileSync(logPath, JSON.stringify(entry) + "\n");
@@ -8068,6 +8905,21 @@ function registerLogCommand(program2) {
8068
8905
  }
8069
8906
  }
8070
8907
  }
8908
+ if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
8909
+ const bashCommand = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
8910
+ const output = payload.tool_response?.output ?? "";
8911
+ if (bashCommand && output) {
8912
+ const testResult = detectTestResult(bashCommand, output);
8913
+ if (testResult) {
8914
+ await notifyActivitySocket({
8915
+ id: "test-result",
8916
+ ts: Date.now(),
8917
+ tool,
8918
+ status: testResult === "pass" ? "test_pass" : "test_fail"
8919
+ });
8920
+ }
8921
+ }
8922
+ }
8071
8923
  const safeCwd = typeof payload.cwd === "string" && path21.isAbsolute(payload.cwd) ? payload.cwd : void 0;
8072
8924
  const config = getConfig(safeCwd);
8073
8925
  if (shouldSnapshot(tool, {}, config)) {
@@ -8077,7 +8929,7 @@ function registerLogCommand(program2) {
8077
8929
  const msg = err instanceof Error ? err.message : String(err);
8078
8930
  process.stderr.write(`[Node9] audit log error: ${msg}
8079
8931
  `);
8080
- const debugPath = path21.join(os16.homedir(), ".node9", "hook-debug.log");
8932
+ const debugPath = path21.join(os15.homedir(), ".node9", "hook-debug.log");
8081
8933
  try {
8082
8934
  fs19.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
8083
8935
  `);
@@ -8386,11 +9238,11 @@ init_daemon();
8386
9238
  import chalk7 from "chalk";
8387
9239
  import fs20 from "fs";
8388
9240
  import path22 from "path";
8389
- import os17 from "os";
9241
+ import os16 from "os";
8390
9242
  import { execSync as execSync2 } from "child_process";
8391
9243
  function registerDoctorCommand(program2, version2) {
8392
9244
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
8393
- const homeDir2 = os17.homedir();
9245
+ const homeDir2 = os16.homedir();
8394
9246
  let failures = 0;
8395
9247
  function pass(msg) {
8396
9248
  console.log(chalk7.green(" \u2705 ") + msg);
@@ -8553,7 +9405,7 @@ function registerDoctorCommand(program2, version2) {
8553
9405
  import chalk8 from "chalk";
8554
9406
  import fs21 from "fs";
8555
9407
  import path23 from "path";
8556
- import os18 from "os";
9408
+ import os17 from "os";
8557
9409
  function formatRelativeTime(timestamp) {
8558
9410
  const diff = Date.now() - new Date(timestamp).getTime();
8559
9411
  const sec = Math.floor(diff / 1e3);
@@ -8566,7 +9418,7 @@ function formatRelativeTime(timestamp) {
8566
9418
  }
8567
9419
  function registerAuditCommand(program2) {
8568
9420
  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) => {
8569
- const logPath = path23.join(os18.homedir(), ".node9", "audit.log");
9421
+ const logPath = path23.join(os17.homedir(), ".node9", "audit.log");
8570
9422
  if (!fs21.existsSync(logPath)) {
8571
9423
  console.log(
8572
9424
  chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
@@ -8695,7 +9547,7 @@ init_daemon();
8695
9547
  import chalk10 from "chalk";
8696
9548
  import fs22 from "fs";
8697
9549
  import path24 from "path";
8698
- import os19 from "os";
9550
+ import os18 from "os";
8699
9551
  function readJson2(filePath) {
8700
9552
  try {
8701
9553
  if (fs22.existsSync(filePath)) return JSON.parse(fs22.readFileSync(filePath, "utf-8"));
@@ -8764,7 +9616,7 @@ function registerStatusCommand(program2) {
8764
9616
  const modeLabel = settings.mode === "audit" ? chalk10.blue("audit") : settings.mode === "strict" ? chalk10.red("strict") : chalk10.white("standard");
8765
9617
  console.log(` Mode: ${modeLabel}`);
8766
9618
  const projectConfig = path24.join(process.cwd(), "node9.config.json");
8767
- const globalConfig = path24.join(os19.homedir(), ".node9", "config.json");
9619
+ const globalConfig = path24.join(os18.homedir(), ".node9", "config.json");
8768
9620
  console.log(
8769
9621
  ` Local: ${fs22.existsSync(projectConfig) ? chalk10.green("Active (node9.config.json)") : chalk10.gray("Not present")}`
8770
9622
  );
@@ -8776,7 +9628,7 @@ function registerStatusCommand(program2) {
8776
9628
  ` Sandbox: ${chalk10.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
8777
9629
  );
8778
9630
  }
8779
- const homeDir2 = os19.homedir();
9631
+ const homeDir2 = os18.homedir();
8780
9632
  const claudeSettings = readJson2(
8781
9633
  path24.join(homeDir2, ".claude", "settings.json")
8782
9634
  );
@@ -8846,11 +9698,11 @@ init_core();
8846
9698
  import chalk11 from "chalk";
8847
9699
  import fs23 from "fs";
8848
9700
  import path25 from "path";
8849
- import os20 from "os";
9701
+ import os19 from "os";
8850
9702
  function registerInitCommand(program2) {
8851
9703
  program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").action(async (options) => {
8852
9704
  console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
8853
- const configPath = path25.join(os20.homedir(), ".node9", "config.json");
9705
+ const configPath = path25.join(os19.homedir(), ".node9", "config.json");
8854
9706
  if (fs23.existsSync(configPath) && !options.force) {
8855
9707
  console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
8856
9708
  } else {
@@ -8891,107 +9743,306 @@ function registerInitCommand(program2) {
8891
9743
  else if (agent === "cursor") await setupCursor();
8892
9744
  console.log("");
8893
9745
  }
9746
+ if (detected.claude) {
9747
+ setupHud();
9748
+ console.log(chalk11.green("\u2705 node9 HUD added to Claude Code statusline"));
9749
+ console.log(chalk11.gray(" Restart Claude Code to activate the security statusline."));
9750
+ console.log("");
9751
+ }
8894
9752
  console.log(chalk11.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
8895
9753
  console.log(chalk11.gray(" Run: node9 daemon start"));
8896
9754
  });
8897
9755
  }
8898
9756
 
8899
9757
  // src/cli/commands/undo.ts
9758
+ import path26 from "path";
9759
+ import chalk13 from "chalk";
9760
+
9761
+ // src/tui/undo-navigator.ts
9762
+ import readline2 from "readline";
8900
9763
  import chalk12 from "chalk";
8901
- import { confirm as confirm2 } from "@inquirer/prompts";
9764
+ var RESET = "\x1B[0m";
9765
+ var BOLD = "\x1B[1m";
9766
+ var CLEAR_SCREEN = "\x1B[2J\x1B[H";
9767
+ var SESSION_GAP_MS = 6e4;
9768
+ function formatAge(timestamp) {
9769
+ const age = Math.round((Date.now() - timestamp) / 1e3);
9770
+ if (age < 60) return `${age}s ago`;
9771
+ if (age < 3600) return `${Math.round(age / 60)}m ago`;
9772
+ if (age < 86400) return `${Math.round(age / 3600)}h ago`;
9773
+ return `${Math.round(age / 86400)}d ago`;
9774
+ }
9775
+ function renderDiff(raw) {
9776
+ const lines = raw.split("\n").filter(
9777
+ (l) => !l.startsWith("diff --git") && !l.startsWith("index ") && !l.startsWith("Binary")
9778
+ );
9779
+ for (const line of lines) {
9780
+ if (line.startsWith("+++") || line.startsWith("---")) {
9781
+ process.stdout.write(chalk12.bold(line) + "\n");
9782
+ } else if (line.startsWith("+")) {
9783
+ process.stdout.write(chalk12.green(line) + "\n");
9784
+ } else if (line.startsWith("-")) {
9785
+ process.stdout.write(chalk12.red(line) + "\n");
9786
+ } else if (line.startsWith("@@")) {
9787
+ process.stdout.write(chalk12.cyan(line) + "\n");
9788
+ } else {
9789
+ process.stdout.write(chalk12.gray(line) + "\n");
9790
+ }
9791
+ }
9792
+ }
9793
+ function isSessionBoundary(entries, idx) {
9794
+ if (idx <= 0) return false;
9795
+ return entries[idx - 1].timestamp - entries[idx].timestamp > SESSION_GAP_MS;
9796
+ }
9797
+ function sessionStart(entries, idx) {
9798
+ let i = idx;
9799
+ while (i > 0 && !isSessionBoundary(entries, i)) i--;
9800
+ return i;
9801
+ }
9802
+ function render(entries, idx) {
9803
+ const entry = entries[idx];
9804
+ const total = entries.length;
9805
+ const step = idx + 1;
9806
+ process.stdout.write(CLEAR_SCREEN);
9807
+ process.stdout.write(
9808
+ chalk12.magenta.bold(`\u23EA Node9 Undo`) + chalk12.gray(` \u2500\u2500 step ${step} of ${total}`) + (entry.files?.length ? chalk12.gray(
9809
+ ` \u2500\u2500 ${entry.files.slice(0, 2).join(", ")}${entry.files.length > 2 ? ` +${entry.files.length - 2} more` : ""}`
9810
+ ) : "") + "\n\n"
9811
+ );
9812
+ process.stdout.write(
9813
+ ` ${BOLD}Tool:${RESET} ${chalk12.cyan(entry.tool)}` + (entry.argsSummary ? chalk12.gray(" \u2192 " + entry.argsSummary) : "") + "\n"
9814
+ );
9815
+ process.stdout.write(` ${BOLD}When:${RESET} ${chalk12.gray(formatAge(entry.timestamp))}
9816
+ `);
9817
+ process.stdout.write(` ${BOLD}Dir: ${RESET} ${chalk12.gray(entry.cwd)}
9818
+ `);
9819
+ if (entry.files && entry.files.length > 0) {
9820
+ process.stdout.write(` ${BOLD}Files:${RESET} ${chalk12.gray(entry.files.join(", "))}
9821
+ `);
9822
+ }
9823
+ if (idx < total - 1 && isSessionBoundary(entries, idx + 1)) {
9824
+ process.stdout.write(chalk12.gray("\n \u2500\u2500 session boundary above \u2500\u2500\n"));
9825
+ }
9826
+ process.stdout.write("\n");
9827
+ const diff = entry.diff ?? computeUndoDiff(entry.hash, entry.cwd);
9828
+ if (diff) {
9829
+ renderDiff(diff);
9830
+ } else {
9831
+ process.stdout.write(
9832
+ chalk12.gray(" (no diff \u2014 working tree may already match this snapshot)\n")
9833
+ );
9834
+ }
9835
+ process.stdout.write("\n");
9836
+ process.stdout.write(
9837
+ chalk12.gray(" ") + (idx < total - 1 ? chalk12.white("[\u2190] older") : chalk12.gray("[\u2190] older")) + chalk12.gray(" ") + (idx > 0 ? chalk12.white("[\u2192] newer") : chalk12.gray("[\u2192] newer")) + chalk12.gray(" ") + chalk12.green("[\u21B5] restore here") + chalk12.gray(" ") + chalk12.yellow("[s] session start") + chalk12.gray(" ") + chalk12.gray("[q] quit") + "\n"
9838
+ );
9839
+ }
9840
+ async function runUndoNavigator(entries) {
9841
+ if (entries.length === 0) return { restored: false };
9842
+ const display = [...entries].reverse();
9843
+ let idx = 0;
9844
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
9845
+ render(display, idx);
9846
+ return { restored: false };
9847
+ }
9848
+ readline2.emitKeypressEvents(process.stdin);
9849
+ return new Promise((resolve) => {
9850
+ let done = false;
9851
+ render(display, idx);
9852
+ try {
9853
+ process.stdin.setRawMode(true);
9854
+ } catch {
9855
+ resolve({ restored: false });
9856
+ return;
9857
+ }
9858
+ process.stdin.resume();
9859
+ const cleanup = () => {
9860
+ process.stdin.removeListener("keypress", onKeypress);
9861
+ try {
9862
+ process.stdin.setRawMode(false);
9863
+ } catch {
9864
+ }
9865
+ process.stdin.pause();
9866
+ };
9867
+ const onKeypress = (_str, key) => {
9868
+ if (done) return;
9869
+ const name = key?.name ?? "";
9870
+ if (name === "left" || name === "h") {
9871
+ if (idx < display.length - 1) {
9872
+ idx++;
9873
+ render(display, idx);
9874
+ }
9875
+ } else if (name === "right" || name === "l") {
9876
+ if (idx > 0) {
9877
+ idx--;
9878
+ render(display, idx);
9879
+ }
9880
+ } else if (name === "s") {
9881
+ const start = sessionStart(display, idx);
9882
+ if (start !== idx) {
9883
+ idx = start;
9884
+ render(display, idx);
9885
+ }
9886
+ } else if (name === "return" || name === "y") {
9887
+ done = true;
9888
+ cleanup();
9889
+ process.stdout.write(CLEAR_SCREEN);
9890
+ const entry = display[idx];
9891
+ process.stdout.write(chalk12.magenta.bold("\n\u23EA Restoring snapshot...\n\n"));
9892
+ if (applyUndo(entry.hash, entry.cwd)) {
9893
+ process.stdout.write(chalk12.green("\u2705 Reverted successfully.\n\n"));
9894
+ resolve({ restored: true });
9895
+ } else {
9896
+ process.stdout.write(chalk12.red("\u274C Undo failed.\n\n"));
9897
+ resolve({ restored: false });
9898
+ }
9899
+ } else if (name === "q" || key?.ctrl && name === "c") {
9900
+ done = true;
9901
+ cleanup();
9902
+ process.stdout.write(CLEAR_SCREEN);
9903
+ process.stdout.write(chalk12.gray("\nCancelled.\n\n"));
9904
+ resolve({ restored: false });
9905
+ }
9906
+ };
9907
+ process.stdin.on("keypress", onKeypress);
9908
+ });
9909
+ }
9910
+
9911
+ // src/cli/commands/undo.ts
9912
+ function findMatchingCwd(startDir, history) {
9913
+ const cwds = new Set(history.map((e) => e.cwd));
9914
+ let dir = startDir;
9915
+ while (true) {
9916
+ if (cwds.has(dir)) return dir;
9917
+ const parent = path26.dirname(dir);
9918
+ if (parent === dir) return null;
9919
+ dir = parent;
9920
+ }
9921
+ }
9922
+ function formatAge2(timestamp) {
9923
+ const age = Math.round((Date.now() - timestamp) / 1e3);
9924
+ if (age < 60) return `${age}s ago`;
9925
+ if (age < 3600) return `${Math.round(age / 60)}m ago`;
9926
+ if (age < 86400) return `${Math.round(age / 3600)}h ago`;
9927
+ return `${Math.round(age / 86400)}d ago`;
9928
+ }
8902
9929
  function registerUndoCommand(program2) {
8903
9930
  program2.command("undo").description(
8904
- "Revert files to a pre-AI snapshot. Shows a diff and asks for confirmation before reverting. Use --steps N to go back N actions, --all to include snapshots from other directories."
8905
- ).option("--steps <n>", "Number of snapshots to go back (default: 1)", "1").option("--all", "Show snapshots from all directories, not just the current one").action(async (options) => {
8906
- const steps = Math.max(1, parseInt(options.steps, 10) || 1);
9931
+ "Browse and restore pre-AI snapshots. Arrow keys to navigate, Enter to restore. Use --steps N to go back N actions non-interactively, --list to print history."
9932
+ ).option("--steps <n>", "Non-interactive: restore N steps back (default: 1)").option("--list", "Print snapshot history as a table and exit").option("--all", "Include snapshots from all directories, not just the current one").action(async (options) => {
8907
9933
  const allHistory = getSnapshotHistory();
8908
- const history = options.all ? allHistory : allHistory.filter((s) => s.cwd === process.cwd());
9934
+ const matchedCwd = options.all ? null : findMatchingCwd(process.cwd(), allHistory);
9935
+ const history = options.all ? allHistory : allHistory.filter((s) => s.cwd === matchedCwd);
8909
9936
  if (history.length === 0) {
8910
9937
  if (!options.all && allHistory.length > 0) {
8911
9938
  console.log(
8912
- chalk12.yellow(
9939
+ chalk13.yellow(
8913
9940
  `
8914
9941
  \u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
8915
- Run ${chalk12.cyan("node9 undo --all")} to see snapshots from all projects.
9942
+ Run ${chalk13.cyan("node9 undo --all")} to see snapshots from all projects.
8916
9943
  `
8917
9944
  )
8918
9945
  );
8919
9946
  } else {
8920
- console.log(chalk12.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
9947
+ console.log(chalk13.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
8921
9948
  }
8922
9949
  return;
8923
9950
  }
8924
- const idx = history.length - steps;
8925
- if (idx < 0) {
9951
+ if (options.list) {
9952
+ console.log(chalk13.magenta.bold("\n\u23EA Snapshot History\n"));
8926
9953
  console.log(
8927
- chalk12.yellow(
8928
- `
8929
- \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
8930
- `
9954
+ chalk13.gray(
9955
+ ` ${"#".padEnd(3)} ${"File / Command".padEnd(30)} ${"Tool".padEnd(8)} ${"When".padEnd(10)} Dir`
8931
9956
  )
8932
9957
  );
9958
+ console.log(chalk13.gray(" " + "\u2500".repeat(80)));
9959
+ const display = [...history].reverse();
9960
+ let prevTs = null;
9961
+ for (let i = 0; i < display.length; i++) {
9962
+ const e = display[i];
9963
+ const isGap = prevTs !== null && prevTs - e.timestamp > 6e4;
9964
+ if (isGap) console.log(chalk13.gray(" \u2500\u2500 earlier \u2500\u2500"));
9965
+ const label = (e.argsSummary || e.files?.[0] || "\u2014").slice(0, 30).padEnd(30);
9966
+ const tool = e.tool.slice(0, 8).padEnd(8);
9967
+ const when = formatAge2(e.timestamp).padEnd(10);
9968
+ const dir = e.cwd.length > 30 ? "\u2026" + e.cwd.slice(-29) : e.cwd;
9969
+ console.log(
9970
+ chalk13.white(
9971
+ ` ${String(i + 1).padEnd(3)} ${label} ${chalk13.cyan(tool)} ${chalk13.gray(when)} ${chalk13.gray(dir)}`
9972
+ )
9973
+ );
9974
+ prevTs = e.timestamp;
9975
+ }
9976
+ console.log("");
8933
9977
  return;
8934
9978
  }
8935
- const snapshot = history[idx];
8936
- const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
8937
- const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
8938
- console.log(
8939
- chalk12.magenta.bold(`
9979
+ if (options.steps !== void 0) {
9980
+ const steps = Math.max(1, parseInt(options.steps, 10) || 1);
9981
+ const idx = history.length - steps;
9982
+ if (idx < 0) {
9983
+ console.log(
9984
+ chalk13.yellow(
9985
+ `
9986
+ \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
9987
+ `
9988
+ )
9989
+ );
9990
+ return;
9991
+ }
9992
+ const snapshot = history[idx];
9993
+ const ageStr = formatAge2(snapshot.timestamp);
9994
+ console.log(
9995
+ chalk13.magenta.bold(`
8940
9996
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
8941
- );
8942
- console.log(
8943
- chalk12.white(
8944
- ` Tool: ${chalk12.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk12.gray(" \u2192 " + snapshot.argsSummary) : ""}`
8945
- )
8946
- );
8947
- console.log(chalk12.white(` When: ${chalk12.gray(ageStr)}`));
8948
- console.log(chalk12.white(` Dir: ${chalk12.gray(snapshot.cwd)}`));
8949
- if (steps > 1)
9997
+ );
8950
9998
  console.log(
8951
- chalk12.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
9999
+ chalk13.white(
10000
+ ` Tool: ${chalk13.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk13.gray(" \u2192 " + snapshot.argsSummary) : ""}`
10001
+ )
8952
10002
  );
8953
- console.log("");
8954
- const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
8955
- if (diff) {
8956
- const lines = diff.split("\n");
8957
- for (const line of lines) {
8958
- if (line.startsWith("+++") || line.startsWith("---")) {
8959
- console.log(chalk12.bold(line));
8960
- } else if (line.startsWith("+")) {
8961
- console.log(chalk12.green(line));
8962
- } else if (line.startsWith("-")) {
8963
- console.log(chalk12.red(line));
8964
- } else if (line.startsWith("@@")) {
8965
- console.log(chalk12.cyan(line));
8966
- } else {
8967
- console.log(chalk12.gray(line));
10003
+ console.log(chalk13.white(` When: ${chalk13.gray(ageStr)}`));
10004
+ console.log(chalk13.white(` Dir: ${chalk13.gray(snapshot.cwd)}`));
10005
+ if (steps > 1)
10006
+ console.log(
10007
+ chalk13.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
10008
+ );
10009
+ console.log("");
10010
+ const diff = snapshot.diff ?? computeUndoDiff(snapshot.hash, snapshot.cwd);
10011
+ if (diff) {
10012
+ const lines = diff.split("\n").filter((l) => !l.startsWith("diff --git") && !l.startsWith("index "));
10013
+ for (const line of lines) {
10014
+ if (line.startsWith("+++") || line.startsWith("---")) console.log(chalk13.bold(line));
10015
+ else if (line.startsWith("+")) console.log(chalk13.green(line));
10016
+ else if (line.startsWith("-")) console.log(chalk13.red(line));
10017
+ else if (line.startsWith("@@")) console.log(chalk13.cyan(line));
10018
+ else console.log(chalk13.gray(line));
8968
10019
  }
10020
+ console.log("");
10021
+ } else {
10022
+ console.log(
10023
+ chalk13.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
10024
+ );
8969
10025
  }
8970
- console.log("");
8971
- } else {
8972
- console.log(
8973
- chalk12.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
8974
- );
8975
- }
8976
- const proceed = await confirm2({
8977
- message: `Revert to this snapshot?`,
8978
- default: false
8979
- });
8980
- if (proceed) {
8981
- if (applyUndo(snapshot.hash, snapshot.cwd)) {
8982
- console.log(chalk12.green("\n\u2705 Reverted successfully.\n"));
10026
+ const { confirm: confirm3 } = await import("@inquirer/prompts");
10027
+ const proceed = await confirm3({ message: `Revert to this snapshot?`, default: false });
10028
+ if (proceed) {
10029
+ if (applyUndo(snapshot.hash, snapshot.cwd)) {
10030
+ console.log(chalk13.green("\n\u2705 Reverted successfully.\n"));
10031
+ } else {
10032
+ console.error(chalk13.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
10033
+ }
8983
10034
  } else {
8984
- console.error(chalk12.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
10035
+ console.log(chalk13.gray("\nCancelled.\n"));
8985
10036
  }
8986
- } else {
8987
- console.log(chalk12.gray("\nCancelled.\n"));
10037
+ return;
8988
10038
  }
10039
+ await runUndoNavigator(history);
8989
10040
  });
8990
10041
  }
8991
10042
 
8992
10043
  // src/cli/commands/watch.ts
8993
10044
  init_daemon();
8994
- import chalk13 from "chalk";
10045
+ import chalk14 from "chalk";
8995
10046
  import { spawn as spawn7, spawnSync as spawnSync5 } from "child_process";
8996
10047
  function registerWatchCommand(program2) {
8997
10048
  program2.command("watch").description("Run a command under Node9 watch mode (daemon stays alive for the session)").argument("<command>", "Command to run").argument("[args...]", "Arguments for the command").action(async (cmd, args) => {
@@ -9007,7 +10058,7 @@ function registerWatchCommand(program2) {
9007
10058
  throw new Error("not running");
9008
10059
  }
9009
10060
  } catch {
9010
- console.error(chalk13.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
10061
+ console.error(chalk14.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
9011
10062
  const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
9012
10063
  detached: true,
9013
10064
  stdio: "ignore",
@@ -9029,12 +10080,12 @@ function registerWatchCommand(program2) {
9029
10080
  }
9030
10081
  }
9031
10082
  if (!ready) {
9032
- console.error(chalk13.red("\u274C Daemon failed to start. Try: node9 daemon start"));
10083
+ console.error(chalk14.red("\u274C Daemon failed to start. Try: node9 daemon start"));
9033
10084
  process.exit(1);
9034
10085
  }
9035
10086
  }
9036
10087
  console.error(
9037
- chalk13.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk13.dim(` \u2192 localhost:${port}`) + chalk13.dim(
10088
+ chalk14.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk14.dim(` \u2192 localhost:${port}`) + chalk14.dim(
9038
10089
  "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
9039
10090
  )
9040
10091
  );
@@ -9043,7 +10094,7 @@ function registerWatchCommand(program2) {
9043
10094
  env: { ...process.env, NODE9_WATCH_MODE: "1" }
9044
10095
  });
9045
10096
  if (result.error) {
9046
- console.error(chalk13.red(`\u274C Failed to run command: ${result.error.message}`));
10097
+ console.error(chalk14.red(`\u274C Failed to run command: ${result.error.message}`));
9047
10098
  process.exit(1);
9048
10099
  }
9049
10100
  process.exit(result.status ?? 0);
@@ -9052,8 +10103,8 @@ function registerWatchCommand(program2) {
9052
10103
 
9053
10104
  // src/mcp-gateway/index.ts
9054
10105
  init_orchestrator();
9055
- import readline2 from "readline";
9056
- import chalk14 from "chalk";
10106
+ import readline3 from "readline";
10107
+ import chalk15 from "chalk";
9057
10108
  import { spawn as spawn8 } from "child_process";
9058
10109
  import { execa as execa2 } from "execa";
9059
10110
  init_provenance();
@@ -9116,13 +10167,13 @@ async function runMcpGateway(upstreamCommand) {
9116
10167
  const prov = checkProvenance(executable);
9117
10168
  if (prov.trustLevel === "suspect") {
9118
10169
  console.error(
9119
- chalk14.red(
10170
+ chalk15.red(
9120
10171
  `\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
9121
10172
  )
9122
10173
  );
9123
- console.error(chalk14.red(" Verify this binary is trusted before proceeding."));
10174
+ console.error(chalk15.red(" Verify this binary is trusted before proceeding."));
9124
10175
  }
9125
- console.error(chalk14.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
10176
+ console.error(chalk15.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
9126
10177
  const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
9127
10178
  "NODE_OPTIONS",
9128
10179
  "NODE_PATH",
@@ -9150,7 +10201,7 @@ async function runMcpGateway(upstreamCommand) {
9150
10201
  let authPending = false;
9151
10202
  let deferredExitCode = null;
9152
10203
  let deferredStdinEnd = false;
9153
- const agentIn = readline2.createInterface({ input: process.stdin, terminal: false });
10204
+ const agentIn = readline3.createInterface({ input: process.stdin, terminal: false });
9154
10205
  agentIn.on("line", async (line) => {
9155
10206
  let message;
9156
10207
  try {
@@ -9186,10 +10237,10 @@ async function runMcpGateway(upstreamCommand) {
9186
10237
  mcpServer
9187
10238
  });
9188
10239
  if (!result.approved) {
9189
- console.error(chalk14.red(`
10240
+ console.error(chalk15.red(`
9190
10241
  \u{1F6D1} Node9 MCP Gateway: Action Blocked`));
9191
- console.error(chalk14.gray(` Tool: ${toolName}`));
9192
- console.error(chalk14.gray(` Reason: ${result.reason ?? "Security Policy"}
10242
+ console.error(chalk15.gray(` Tool: ${toolName}`));
10243
+ console.error(chalk15.gray(` Reason: ${result.reason ?? "Security Policy"}
9193
10244
  `));
9194
10245
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
9195
10246
  const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -9263,7 +10314,7 @@ function registerMcpGatewayCommand(program2) {
9263
10314
 
9264
10315
  // src/cli/commands/trust.ts
9265
10316
  init_trusted_hosts();
9266
- import chalk15 from "chalk";
10317
+ import chalk16 from "chalk";
9267
10318
  function isValidHost(host) {
9268
10319
  return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
9269
10320
  }
@@ -9273,44 +10324,44 @@ function registerTrustCommand(program2) {
9273
10324
  const normalized = normalizeHost(host.trim());
9274
10325
  if (!isValidHost(normalized)) {
9275
10326
  console.error(
9276
- chalk15.red(`
10327
+ chalk16.red(`
9277
10328
  \u274C Invalid host: "${host}"
9278
- `) + chalk15.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
10329
+ `) + chalk16.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
9279
10330
  );
9280
10331
  process.exit(1);
9281
10332
  }
9282
10333
  addTrustedHost(normalized);
9283
- console.log(chalk15.green(`
10334
+ console.log(chalk16.green(`
9284
10335
  \u2705 ${normalized} added to trusted hosts.`));
9285
10336
  console.log(
9286
- chalk15.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
10337
+ chalk16.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
9287
10338
  );
9288
10339
  });
9289
10340
  trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
9290
10341
  const normalized = normalizeHost(host.trim());
9291
10342
  const removed = removeTrustedHost(normalized);
9292
10343
  if (!removed) {
9293
- console.error(chalk15.yellow(`
10344
+ console.error(chalk16.yellow(`
9294
10345
  \u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
9295
10346
  `));
9296
10347
  process.exit(1);
9297
10348
  }
9298
- console.log(chalk15.green(`
10349
+ console.log(chalk16.green(`
9299
10350
  \u2705 ${normalized} removed from trusted hosts.
9300
10351
  `));
9301
10352
  });
9302
10353
  trustCmd.command("list").description("Show all trusted hosts").action(() => {
9303
10354
  const hosts = readTrustedHosts();
9304
10355
  if (hosts.length === 0) {
9305
- console.log(chalk15.gray("\n No trusted hosts configured.\n"));
9306
- console.log(` Add one: ${chalk15.cyan("node9 trust add api.mycompany.com")}
10356
+ console.log(chalk16.gray("\n No trusted hosts configured.\n"));
10357
+ console.log(` Add one: ${chalk16.cyan("node9 trust add api.mycompany.com")}
9307
10358
  `);
9308
10359
  return;
9309
10360
  }
9310
- console.log(chalk15.bold("\n\u{1F513} Trusted Hosts\n"));
10361
+ console.log(chalk16.bold("\n\u{1F513} Trusted Hosts\n"));
9311
10362
  for (const entry of hosts) {
9312
10363
  const date = new Date(entry.addedAt).toLocaleDateString();
9313
- console.log(` ${chalk15.cyan(entry.host.padEnd(40))} ${chalk15.gray(`added ${date}`)}`);
10364
+ console.log(` ${chalk16.cyan(entry.host.padEnd(40))} ${chalk16.gray(`added ${date}`)}`);
9314
10365
  }
9315
10366
  console.log("");
9316
10367
  });
@@ -9318,20 +10369,20 @@ function registerTrustCommand(program2) {
9318
10369
 
9319
10370
  // src/cli.ts
9320
10371
  var { version } = JSON.parse(
9321
- fs25.readFileSync(path27.join(__dirname, "../package.json"), "utf-8")
10372
+ fs26.readFileSync(path29.join(__dirname, "../package.json"), "utf-8")
9322
10373
  );
9323
10374
  var program = new Command();
9324
10375
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
9325
10376
  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) => {
9326
10377
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
9327
- const credPath = path27.join(os22.homedir(), ".node9", "credentials.json");
9328
- if (!fs25.existsSync(path27.dirname(credPath)))
9329
- fs25.mkdirSync(path27.dirname(credPath), { recursive: true });
10378
+ const credPath = path29.join(os22.homedir(), ".node9", "credentials.json");
10379
+ if (!fs26.existsSync(path29.dirname(credPath)))
10380
+ fs26.mkdirSync(path29.dirname(credPath), { recursive: true });
9330
10381
  const profileName = options.profile || "default";
9331
10382
  let existingCreds = {};
9332
10383
  try {
9333
- if (fs25.existsSync(credPath)) {
9334
- const raw = JSON.parse(fs25.readFileSync(credPath, "utf-8"));
10384
+ if (fs26.existsSync(credPath)) {
10385
+ const raw = JSON.parse(fs26.readFileSync(credPath, "utf-8"));
9335
10386
  if (raw.apiKey) {
9336
10387
  existingCreds = {
9337
10388
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -9343,13 +10394,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
9343
10394
  } catch {
9344
10395
  }
9345
10396
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
9346
- fs25.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
10397
+ fs26.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
9347
10398
  if (profileName === "default") {
9348
- const configPath = path27.join(os22.homedir(), ".node9", "config.json");
10399
+ const configPath = path29.join(os22.homedir(), ".node9", "config.json");
9349
10400
  let config = {};
9350
10401
  try {
9351
- if (fs25.existsSync(configPath))
9352
- config = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
10402
+ if (fs26.existsSync(configPath))
10403
+ config = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
9353
10404
  } catch {
9354
10405
  }
9355
10406
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -9364,36 +10415,40 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
9364
10415
  approvers.cloud = false;
9365
10416
  }
9366
10417
  s.approvers = approvers;
9367
- if (!fs25.existsSync(path27.dirname(configPath)))
9368
- fs25.mkdirSync(path27.dirname(configPath), { recursive: true });
9369
- fs25.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
10418
+ if (!fs26.existsSync(path29.dirname(configPath)))
10419
+ fs26.mkdirSync(path29.dirname(configPath), { recursive: true });
10420
+ fs26.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
9370
10421
  }
9371
10422
  if (options.profile && profileName !== "default") {
9372
- console.log(chalk17.green(`\u2705 Profile "${profileName}" saved`));
9373
- console.log(chalk17.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
10423
+ console.log(chalk18.green(`\u2705 Profile "${profileName}" saved`));
10424
+ console.log(chalk18.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
9374
10425
  } else if (options.local) {
9375
- console.log(chalk17.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
9376
- console.log(chalk17.gray(` All decisions stay on this machine.`));
10426
+ console.log(chalk18.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
10427
+ console.log(chalk18.gray(` All decisions stay on this machine.`));
9377
10428
  } else {
9378
- console.log(chalk17.green(`\u2705 Logged in \u2014 agent mode`));
9379
- console.log(chalk17.gray(` Team policy enforced for all calls via Node9 cloud.`));
10429
+ console.log(chalk18.green(`\u2705 Logged in \u2014 agent mode`));
10430
+ console.log(chalk18.gray(` Team policy enforced for all calls via Node9 cloud.`));
9380
10431
  }
9381
10432
  });
9382
- program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
10433
+ program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("<target>", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
9383
10434
  if (target === "gemini") return await setupGemini();
9384
10435
  if (target === "claude") return await setupClaude();
9385
10436
  if (target === "cursor") return await setupCursor();
9386
- console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
10437
+ if (target === "hud") return setupHud();
10438
+ console.error(chalk18.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
9387
10439
  process.exit(1);
9388
10440
  });
9389
- program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
10441
+ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("[target]", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
9390
10442
  if (!target) {
9391
- console.log(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
9392
- console.log(" Usage: " + chalk17.white("node9 setup <target>") + "\n");
10443
+ console.log(chalk18.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
10444
+ console.log(" Usage: " + chalk18.white("node9 setup <target>") + "\n");
9393
10445
  console.log(" Targets:");
9394
- console.log(" " + chalk17.green("claude") + " \u2014 Claude Code (hook mode)");
9395
- console.log(" " + chalk17.green("gemini") + " \u2014 Gemini CLI (hook mode)");
9396
- console.log(" " + chalk17.green("cursor") + " \u2014 Cursor (hook mode)");
10446
+ console.log(" " + chalk18.green("claude") + " \u2014 Claude Code (hook mode)");
10447
+ console.log(" " + chalk18.green("gemini") + " \u2014 Gemini CLI (hook mode)");
10448
+ console.log(" " + chalk18.green("cursor") + " \u2014 Cursor (hook mode)");
10449
+ process.stdout.write(
10450
+ " " + chalk18.green("hud") + " \u2014 Claude Code security statusline\n"
10451
+ );
9397
10452
  console.log("");
9398
10453
  return;
9399
10454
  }
@@ -9401,7 +10456,8 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
9401
10456
  if (t === "gemini") return await setupGemini();
9402
10457
  if (t === "claude") return await setupClaude();
9403
10458
  if (t === "cursor") return await setupCursor();
9404
- console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
10459
+ if (t === "hud") return setupHud();
10460
+ console.error(chalk18.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
9405
10461
  process.exit(1);
9406
10462
  });
9407
10463
  program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
@@ -9409,31 +10465,34 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
9409
10465
  if (target === "claude") fn = teardownClaude;
9410
10466
  else if (target === "gemini") fn = teardownGemini;
9411
10467
  else if (target === "cursor") fn = teardownCursor;
10468
+ else if (target === "hud") fn = teardownHud;
9412
10469
  else {
9413
- console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
10470
+ console.error(
10471
+ chalk18.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
10472
+ );
9414
10473
  process.exit(1);
9415
10474
  }
9416
- console.log(chalk17.cyan(`
10475
+ console.log(chalk18.cyan(`
9417
10476
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
9418
10477
  `));
9419
10478
  try {
9420
10479
  fn();
9421
10480
  } catch (err) {
9422
- console.error(chalk17.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
10481
+ console.error(chalk18.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
9423
10482
  process.exit(1);
9424
10483
  }
9425
- console.log(chalk17.gray("\n Restart the agent for changes to take effect."));
10484
+ console.log(chalk18.gray("\n Restart the agent for changes to take effect."));
9426
10485
  });
9427
10486
  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) => {
9428
- console.log(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
9429
- console.log(chalk17.bold("Stopping daemon..."));
10487
+ console.log(chalk18.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
10488
+ console.log(chalk18.bold("Stopping daemon..."));
9430
10489
  try {
9431
10490
  stopDaemon();
9432
- console.log(chalk17.green(" \u2705 Daemon stopped"));
10491
+ console.log(chalk18.green(" \u2705 Daemon stopped"));
9433
10492
  } catch {
9434
- console.log(chalk17.blue(" \u2139\uFE0F Daemon was not running"));
10493
+ console.log(chalk18.blue(" \u2139\uFE0F Daemon was not running"));
9435
10494
  }
9436
- console.log(chalk17.bold("\nRemoving hooks..."));
10495
+ console.log(chalk18.bold("\nRemoving hooks..."));
9437
10496
  let teardownFailed = false;
9438
10497
  for (const [label, fn] of [
9439
10498
  ["Claude", teardownClaude],
@@ -9445,45 +10504,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
9445
10504
  } catch (err) {
9446
10505
  teardownFailed = true;
9447
10506
  console.error(
9448
- chalk17.red(
10507
+ chalk18.red(
9449
10508
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
9450
10509
  )
9451
10510
  );
9452
10511
  }
9453
10512
  }
9454
10513
  if (options.purge) {
9455
- const node9Dir = path27.join(os22.homedir(), ".node9");
9456
- if (fs25.existsSync(node9Dir)) {
9457
- const confirmed = await confirm3({
10514
+ const node9Dir = path29.join(os22.homedir(), ".node9");
10515
+ if (fs26.existsSync(node9Dir)) {
10516
+ const confirmed = await confirm2({
9458
10517
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
9459
10518
  default: false
9460
10519
  });
9461
10520
  if (confirmed) {
9462
- fs25.rmSync(node9Dir, { recursive: true });
9463
- if (fs25.existsSync(node9Dir)) {
10521
+ fs26.rmSync(node9Dir, { recursive: true });
10522
+ if (fs26.existsSync(node9Dir)) {
9464
10523
  console.error(
9465
- chalk17.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
10524
+ chalk18.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
9466
10525
  );
9467
10526
  } else {
9468
- console.log(chalk17.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
10527
+ console.log(chalk18.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
9469
10528
  }
9470
10529
  } else {
9471
- console.log(chalk17.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
10530
+ console.log(chalk18.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
9472
10531
  }
9473
10532
  } else {
9474
- console.log(chalk17.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
10533
+ console.log(chalk18.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
9475
10534
  }
9476
10535
  } else {
9477
10536
  console.log(
9478
- chalk17.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
10537
+ chalk18.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
9479
10538
  );
9480
10539
  }
9481
10540
  if (teardownFailed) {
9482
- console.error(chalk17.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
10541
+ console.error(chalk18.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
9483
10542
  process.exit(1);
9484
10543
  }
9485
- console.log(chalk17.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
9486
- console.log(chalk17.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
10544
+ console.log(chalk18.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
10545
+ console.log(chalk18.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
9487
10546
  });
9488
10547
  registerDoctorCommand(program, version);
9489
10548
  program.command("explain").description(
@@ -9496,7 +10555,7 @@ program.command("explain").description(
9496
10555
  try {
9497
10556
  args = JSON.parse(trimmed);
9498
10557
  } catch {
9499
- console.error(chalk17.red(`
10558
+ console.error(chalk18.red(`
9500
10559
  \u274C Invalid JSON: ${trimmed}
9501
10560
  `));
9502
10561
  process.exit(1);
@@ -9507,54 +10566,54 @@ program.command("explain").description(
9507
10566
  }
9508
10567
  const result = await explainPolicy(tool, args);
9509
10568
  console.log("");
9510
- console.log(chalk17.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
10569
+ console.log(chalk18.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
9511
10570
  console.log("");
9512
- console.log(` ${chalk17.bold("Tool:")} ${chalk17.white(result.tool)}`);
10571
+ console.log(` ${chalk18.bold("Tool:")} ${chalk18.white(result.tool)}`);
9513
10572
  if (argsRaw) {
9514
10573
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
9515
- console.log(` ${chalk17.bold("Input:")} ${chalk17.gray(preview)}`);
10574
+ console.log(` ${chalk18.bold("Input:")} ${chalk18.gray(preview)}`);
9516
10575
  }
9517
10576
  console.log("");
9518
- console.log(chalk17.bold("Config Sources (Waterfall):"));
10577
+ console.log(chalk18.bold("Config Sources (Waterfall):"));
9519
10578
  for (const tier of result.waterfall) {
9520
- const num = chalk17.gray(` ${tier.tier}.`);
10579
+ const num = chalk18.gray(` ${tier.tier}.`);
9521
10580
  const label = tier.label.padEnd(16);
9522
10581
  let statusStr;
9523
10582
  if (tier.tier === 1) {
9524
- statusStr = chalk17.gray(tier.note ?? "");
10583
+ statusStr = chalk18.gray(tier.note ?? "");
9525
10584
  } else if (tier.status === "active") {
9526
- const loc = tier.path ? chalk17.gray(tier.path) : "";
9527
- const note = tier.note ? chalk17.gray(`(${tier.note})`) : "";
9528
- statusStr = chalk17.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
10585
+ const loc = tier.path ? chalk18.gray(tier.path) : "";
10586
+ const note = tier.note ? chalk18.gray(`(${tier.note})`) : "";
10587
+ statusStr = chalk18.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
9529
10588
  } else {
9530
- statusStr = chalk17.gray("\u25CB " + (tier.note ?? "not found"));
10589
+ statusStr = chalk18.gray("\u25CB " + (tier.note ?? "not found"));
9531
10590
  }
9532
- console.log(`${num} ${chalk17.white(label)} ${statusStr}`);
10591
+ console.log(`${num} ${chalk18.white(label)} ${statusStr}`);
9533
10592
  }
9534
10593
  console.log("");
9535
- console.log(chalk17.bold("Policy Evaluation:"));
10594
+ console.log(chalk18.bold("Policy Evaluation:"));
9536
10595
  for (const step of result.steps) {
9537
10596
  const isFinal = step.isFinal;
9538
10597
  let icon;
9539
- if (step.outcome === "allow") icon = chalk17.green(" \u2705");
9540
- else if (step.outcome === "review") icon = chalk17.red(" \u{1F534}");
9541
- else if (step.outcome === "skip") icon = chalk17.gray(" \u2500 ");
9542
- else icon = chalk17.gray(" \u25CB ");
10598
+ if (step.outcome === "allow") icon = chalk18.green(" \u2705");
10599
+ else if (step.outcome === "review") icon = chalk18.red(" \u{1F534}");
10600
+ else if (step.outcome === "skip") icon = chalk18.gray(" \u2500 ");
10601
+ else icon = chalk18.gray(" \u25CB ");
9543
10602
  const name = step.name.padEnd(18);
9544
- const nameStr = isFinal ? chalk17.white.bold(name) : chalk17.white(name);
9545
- const detail = isFinal ? chalk17.white(step.detail) : chalk17.gray(step.detail);
9546
- const arrow = isFinal ? chalk17.yellow(" \u2190 STOP") : "";
10603
+ const nameStr = isFinal ? chalk18.white.bold(name) : chalk18.white(name);
10604
+ const detail = isFinal ? chalk18.white(step.detail) : chalk18.gray(step.detail);
10605
+ const arrow = isFinal ? chalk18.yellow(" \u2190 STOP") : "";
9547
10606
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
9548
10607
  }
9549
10608
  console.log("");
9550
10609
  if (result.decision === "allow") {
9551
- console.log(chalk17.green.bold(" Decision: \u2705 ALLOW") + chalk17.gray(" \u2014 no approval needed"));
10610
+ console.log(chalk18.green.bold(" Decision: \u2705 ALLOW") + chalk18.gray(" \u2014 no approval needed"));
9552
10611
  } else {
9553
10612
  console.log(
9554
- chalk17.red.bold(" Decision: \u{1F534} REVIEW") + chalk17.gray(" \u2014 human approval required")
10613
+ chalk18.red.bold(" Decision: \u{1F534} REVIEW") + chalk18.gray(" \u2014 human approval required")
9555
10614
  );
9556
10615
  if (result.blockedByLabel) {
9557
- console.log(chalk17.gray(` Reason: ${result.blockedByLabel}`));
10616
+ console.log(chalk18.gray(` Reason: ${result.blockedByLabel}`));
9558
10617
  }
9559
10618
  }
9560
10619
  console.log("");
@@ -9568,7 +10627,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
9568
10627
  try {
9569
10628
  await startTail2(options);
9570
10629
  } catch (err) {
9571
- console.error(chalk17.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
10630
+ console.error(chalk18.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
9572
10631
  process.exit(1);
9573
10632
  }
9574
10633
  });
@@ -9576,11 +10635,15 @@ registerWatchCommand(program);
9576
10635
  registerMcpGatewayCommand(program);
9577
10636
  registerCheckCommand(program);
9578
10637
  registerLogCommand(program);
10638
+ program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").action(async () => {
10639
+ const { main: main2 } = await Promise.resolve().then(() => (init_hud(), hud_exports));
10640
+ await main2();
10641
+ });
9579
10642
  program.command("pause").description("Temporarily disable Node9 protection for a set duration").option("-d, --duration <duration>", "How long to pause (e.g. 15m, 1h, 30s)", "15m").action((options) => {
9580
10643
  const ms = parseDuration(options.duration);
9581
10644
  if (ms === null) {
9582
10645
  console.error(
9583
- chalk17.red(`
10646
+ chalk18.red(`
9584
10647
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
9585
10648
  `)
9586
10649
  );
@@ -9588,20 +10651,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
9588
10651
  }
9589
10652
  pauseNode9(ms, options.duration);
9590
10653
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
9591
- console.log(chalk17.yellow(`
10654
+ console.log(chalk18.yellow(`
9592
10655
  \u23F8 Node9 paused until ${expiresAt}`));
9593
- console.log(chalk17.gray(` All tool calls will be allowed without review.`));
9594
- console.log(chalk17.gray(` Run "node9 resume" to re-enable early.
10656
+ console.log(chalk18.gray(` All tool calls will be allowed without review.`));
10657
+ console.log(chalk18.gray(` Run "node9 resume" to re-enable early.
9595
10658
  `));
9596
10659
  });
9597
10660
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
9598
10661
  const { paused } = checkPause();
9599
10662
  if (!paused) {
9600
- console.log(chalk17.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
10663
+ console.log(chalk18.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
9601
10664
  return;
9602
10665
  }
9603
10666
  resumeNode9();
9604
- console.log(chalk17.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
10667
+ console.log(chalk18.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
9605
10668
  });
9606
10669
  var HOOK_BASED_AGENTS = {
9607
10670
  claude: "claude",
@@ -9614,15 +10677,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
9614
10677
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
9615
10678
  const target = HOOK_BASED_AGENTS[firstArg2];
9616
10679
  console.error(
9617
- chalk17.yellow(`
10680
+ chalk18.yellow(`
9618
10681
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
9619
10682
  );
9620
- console.error(chalk17.white(`
10683
+ console.error(chalk18.white(`
9621
10684
  "${target}" uses its own hook system. Use:`));
9622
10685
  console.error(
9623
- chalk17.green(` node9 addto ${target} `) + chalk17.gray("# one-time setup")
10686
+ chalk18.green(` node9 addto ${target} `) + chalk18.gray("# one-time setup")
9624
10687
  );
9625
- console.error(chalk17.green(` ${target} `) + chalk17.gray("# run normally"));
10688
+ console.error(chalk18.green(` ${target} `) + chalk18.gray("# run normally"));
9626
10689
  process.exit(1);
9627
10690
  }
9628
10691
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -9639,12 +10702,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
9639
10702
  }
9640
10703
  );
9641
10704
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
9642
- console.error(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
10705
+ console.error(chalk18.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
9643
10706
  const daemonReady = await autoStartDaemonAndWait();
9644
10707
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
9645
10708
  }
9646
10709
  if (result.noApprovalMechanism && process.stdout.isTTY) {
9647
- const approved = await confirm3({
10710
+ const approved = await confirm2({
9648
10711
  message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand}"?`,
9649
10712
  default: false
9650
10713
  });
@@ -9652,12 +10715,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
9652
10715
  }
9653
10716
  if (!result.approved) {
9654
10717
  console.error(
9655
- chalk17.red(`
10718
+ chalk18.red(`
9656
10719
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
9657
10720
  );
9658
10721
  process.exit(1);
9659
10722
  }
9660
- console.error(chalk17.green("\n\u2705 Approved \u2014 running command...\n"));
10723
+ console.error(chalk18.green("\n\u2705 Approved \u2014 running command...\n"));
9661
10724
  await runProxy(fullCommand);
9662
10725
  } else {
9663
10726
  program.help();
@@ -9672,9 +10735,9 @@ if (process.argv[2] !== "daemon") {
9672
10735
  const isCheckHook = process.argv[2] === "check";
9673
10736
  if (isCheckHook) {
9674
10737
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
9675
- const logPath = path27.join(os22.homedir(), ".node9", "hook-debug.log");
10738
+ const logPath = path29.join(os22.homedir(), ".node9", "hook-debug.log");
9676
10739
  const msg = reason instanceof Error ? reason.message : String(reason);
9677
- fs25.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
10740
+ fs26.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
9678
10741
  `);
9679
10742
  }
9680
10743
  process.exit(0);