@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.js CHANGED
@@ -160,8 +160,8 @@ function sanitizeConfig(raw) {
160
160
  }
161
161
  }
162
162
  const lines = result.error.issues.map((issue) => {
163
- const path28 = issue.path.length > 0 ? issue.path.join(".") : "root";
164
- return ` \u2022 ${path28}: ${issue.message}`;
163
+ const path30 = issue.path.length > 0 ? issue.path.join(".") : "root";
164
+ return ` \u2022 ${path30}: ${issue.message}`;
165
165
  });
166
166
  return {
167
167
  sanitized,
@@ -213,12 +213,21 @@ var init_config_schema = __esm({
213
213
  verdict: import_zod.z.enum(["allow", "review", "block"], {
214
214
  errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
215
215
  }),
216
- reason: import_zod.z.string().optional()
216
+ reason: import_zod.z.string().optional(),
217
+ // Unknown predicate names are filtered out rather than failing the whole rule.
218
+ // Failing the whole z.array() would cause sanitizeConfig to drop the entire
219
+ // `policy` top-level key, silently disabling ALL smart rules in the config.
220
+ dependsOnState: import_zod.z.array(import_zod.z.string()).transform(
221
+ (arr) => arr.filter(
222
+ (p) => p === "no_test_passed_since_last_edit"
223
+ )
224
+ ).optional(),
225
+ recoveryCommand: import_zod.z.string().optional()
217
226
  });
218
227
  ConfigFileSchema = import_zod.z.object({
219
228
  version: import_zod.z.string().optional(),
220
229
  settings: import_zod.z.object({
221
- mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
230
+ mode: import_zod.z.enum(["standard", "strict", "audit", "observe"]).optional(),
222
231
  autoStartDaemon: import_zod.z.boolean().optional(),
223
232
  enableUndo: import_zod.z.boolean().optional(),
224
233
  enableHookLogDebug: import_zod.z.boolean().optional(),
@@ -645,12 +654,17 @@ function getConfig(cwd) {
645
654
  if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
646
655
  mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
647
656
  if (s.environment !== void 0) mergedSettings.environment = s.environment;
657
+ if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
648
658
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
649
659
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
650
660
  if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
651
661
  if (p.toolInspection)
652
662
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
653
- if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
663
+ if (p.smartRules) {
664
+ const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
665
+ const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
666
+ mergedPolicy.smartRules = [...defaultBlocks, ...p.smartRules, ...defaultNonBlocks];
667
+ }
654
668
  if (p.snapshot) {
655
669
  const s2 = p.snapshot;
656
670
  if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
@@ -849,7 +863,9 @@ var init_config = __esm({
849
863
  {
850
864
  field: "command",
851
865
  op: "matches",
852
- value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
866
+ // Require the recursive flag to be preceded by whitespace so that
867
+ // filenames containing "-r" (e.g. "ai-review.yml") don't false-positive.
868
+ value: "rm\\b.*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
853
869
  },
854
870
  {
855
871
  field: "command",
@@ -1758,9 +1774,9 @@ function matchesPattern(text, patterns) {
1758
1774
  const withoutDotSlash = text.replace(/^\.\//, "");
1759
1775
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1760
1776
  }
1761
- function getNestedValue(obj, path28) {
1777
+ function getNestedValue(obj, path30) {
1762
1778
  if (!obj || typeof obj !== "object") return null;
1763
- return path28.split(".").reduce((prev, curr) => prev?.[curr], obj);
1779
+ return path30.split(".").reduce((prev, curr) => prev?.[curr], obj);
1764
1780
  }
1765
1781
  function shouldSnapshot(toolName, args, config) {
1766
1782
  if (!config.settings.enableUndo) return false;
@@ -1910,7 +1926,13 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1910
1926
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
1911
1927
  reason: matchedRule.reason,
1912
1928
  tier: 2,
1913
- ruleName: matchedRule.name ?? matchedRule.tool
1929
+ ruleName: matchedRule.name ?? matchedRule.tool,
1930
+ ...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
1931
+ dependsOnStatePredicates: matchedRule.dependsOnState
1932
+ },
1933
+ ...matchedRule.verdict === "block" && matchedRule.recoveryCommand && {
1934
+ recoveryCommand: matchedRule.recoveryCommand
1935
+ }
1914
1936
  };
1915
1937
  }
1916
1938
  }
@@ -2437,6 +2459,34 @@ var init_state = __esm({
2437
2459
  });
2438
2460
 
2439
2461
  // src/auth/daemon.ts
2462
+ function notifyActivitySocket(data) {
2463
+ return new Promise((resolve) => {
2464
+ try {
2465
+ const payload = JSON.stringify(data);
2466
+ const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
2467
+ sock.on("connect", () => {
2468
+ sock.on("close", resolve);
2469
+ sock.end(payload);
2470
+ });
2471
+ sock.on("error", resolve);
2472
+ } catch {
2473
+ resolve();
2474
+ }
2475
+ });
2476
+ }
2477
+ async function checkStatePredicates(predicates) {
2478
+ if (predicates.length === 0) return {};
2479
+ try {
2480
+ const qs = predicates.map(encodeURIComponent).join(",");
2481
+ const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/state/check?predicates=${qs}`, {
2482
+ signal: AbortSignal.timeout(100)
2483
+ });
2484
+ if (!res.ok) return null;
2485
+ return await res.json();
2486
+ } catch {
2487
+ return null;
2488
+ }
2489
+ }
2440
2490
  function getInternalToken() {
2441
2491
  try {
2442
2492
  const pidFile = import_path10.default.join(import_os8.default.homedir(), ".node9", "daemon.pid");
@@ -2470,7 +2520,7 @@ function isDaemonRunning() {
2470
2520
  return false;
2471
2521
  }
2472
2522
  }
2473
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
2523
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
2474
2524
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2475
2525
  const ctrl = new AbortController();
2476
2526
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -2488,7 +2538,10 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2488
2538
  // activity-result as the CLI used for the pending activity event.
2489
2539
  activityId,
2490
2540
  ...riskMetadata && { riskMetadata },
2491
- ...cwd && { cwd }
2541
+ ...cwd && { cwd },
2542
+ ...recoveryCommand && { recoveryCommand },
2543
+ ...skipBackgroundAuth && { skipBackgroundAuth: true },
2544
+ ...viewOnly && { viewOnly: true }
2492
2545
  }),
2493
2546
  signal: ctrl.signal
2494
2547
  });
@@ -2508,10 +2561,10 @@ async function waitForDaemonDecision(id, signal) {
2508
2561
  try {
2509
2562
  const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
2510
2563
  if (!waitRes.ok) return { decision: "deny" };
2511
- const { decision, source } = await waitRes.json();
2564
+ const { decision, source, reason } = await waitRes.json();
2512
2565
  if (decision === "allow") return { decision: "allow", source };
2513
2566
  if (decision === "abandoned") return { decision: "abandoned", source };
2514
- return { decision: "deny", source };
2567
+ return { decision: "deny", source, reason };
2515
2568
  } finally {
2516
2569
  clearTimeout(waitTimer);
2517
2570
  if (signal) signal.removeEventListener("abort", onAbort);
@@ -2597,14 +2650,16 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
2597
2650
  signal: AbortSignal.timeout(3e3)
2598
2651
  });
2599
2652
  }
2600
- var import_fs9, import_path10, import_os8, import_child_process, DAEMON_PORT, DAEMON_HOST;
2653
+ var import_fs9, import_net, import_path10, import_os8, import_child_process, ACTIVITY_SOCKET_PATH, DAEMON_PORT, DAEMON_HOST;
2601
2654
  var init_daemon = __esm({
2602
2655
  "src/auth/daemon.ts"() {
2603
2656
  "use strict";
2604
2657
  import_fs9 = __toESM(require("fs"));
2658
+ import_net = __toESM(require("net"));
2605
2659
  import_path10 = __toESM(require("path"));
2606
2660
  import_os8 = __toESM(require("os"));
2607
2661
  import_child_process = require("child_process");
2662
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path10.default.join(import_os8.default.tmpdir(), "node9-activity.sock");
2608
2663
  DAEMON_PORT = 7391;
2609
2664
  DAEMON_HOST = "127.0.0.1";
2610
2665
  }
@@ -2966,6 +3021,33 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
2966
3021
  async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2967
3022
  const controller = new AbortController();
2968
3023
  const timeout = setTimeout(() => controller.abort(), 1e4);
3024
+ if (!creds.apiKey) throw new Error("Node9 API Key is missing");
3025
+ let ciContext;
3026
+ if (process.env.CI) {
3027
+ try {
3028
+ const ciContextPath = import_path13.default.join(import_os9.default.homedir(), ".node9", "ci-context.json");
3029
+ const stats = import_fs10.default.statSync(ciContextPath);
3030
+ if (stats.size > 1e4) throw new Error("ci-context.json exceeds 10 KB");
3031
+ const raw = import_fs10.default.readFileSync(ciContextPath, "utf8");
3032
+ const parsed = JSON.parse(raw);
3033
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3034
+ throw new Error("ci-context.json is not a plain object");
3035
+ }
3036
+ const p = parsed;
3037
+ ciContext = {
3038
+ tests_after: p["tests_after"],
3039
+ files_changed: p["files_changed"],
3040
+ issues_found: p["issues_found"],
3041
+ issues_fixed: p["issues_fixed"],
3042
+ github_repository: p["github_repository"],
3043
+ github_head_ref: p["github_head_ref"],
3044
+ iteration: p["iteration"],
3045
+ draft_pr_number: p["draft_pr_number"],
3046
+ draft_pr_url: p["draft_pr_url"]
3047
+ };
3048
+ } catch {
3049
+ }
3050
+ }
2969
3051
  try {
2970
3052
  const response = await fetch(creds.apiUrl, {
2971
3053
  method: "POST",
@@ -2980,7 +3062,8 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2980
3062
  cwd: process.cwd(),
2981
3063
  platform: import_os9.default.platform()
2982
3064
  },
2983
- ...riskMetadata && { riskMetadata }
3065
+ ...riskMetadata && { riskMetadata },
3066
+ ...ciContext && { ciContext }
2984
3067
  }),
2985
3068
  signal: controller.signal
2986
3069
  });
@@ -3006,12 +3089,17 @@ async function pollNode9SaaS(requestId, creds, signal) {
3006
3089
  });
3007
3090
  clearTimeout(pollTimer);
3008
3091
  if (!statusRes.ok) continue;
3009
- const { status, reason } = await statusRes.json();
3092
+ const statusBody = await statusRes.json();
3093
+ const { status } = statusBody;
3010
3094
  if (status === "APPROVED") {
3011
- return { approved: true, reason };
3095
+ return { approved: true, reason: statusBody.reason };
3012
3096
  }
3013
3097
  if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
3014
- return { approved: false, reason };
3098
+ return { approved: false, reason: statusBody.reason };
3099
+ }
3100
+ if (status === "FIX") {
3101
+ const feedbackText = statusBody.feedbackText ?? statusBody.reason ?? "Run again with feedback.";
3102
+ return { approved: false, reason: feedbackText };
3015
3103
  }
3016
3104
  } catch {
3017
3105
  }
@@ -3048,12 +3136,13 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
3048
3136
  );
3049
3137
  }
3050
3138
  }
3051
- var import_fs10, import_os9;
3139
+ var import_fs10, import_os9, import_path13;
3052
3140
  var init_cloud = __esm({
3053
3141
  "src/auth/cloud.ts"() {
3054
3142
  "use strict";
3055
3143
  import_fs10 = __toESM(require("fs"));
3056
3144
  import_os9 = __toESM(require("os"));
3145
+ import_path13 = __toESM(require("path"));
3057
3146
  init_audit();
3058
3147
  }
3059
3148
  });
@@ -3094,19 +3183,7 @@ function isNetworkTool(toolName, args) {
3094
3183
  return false;
3095
3184
  }
3096
3185
  function notifyActivity(data) {
3097
- return new Promise((resolve) => {
3098
- try {
3099
- const payload = JSON.stringify(data);
3100
- const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
3101
- sock.on("connect", () => {
3102
- sock.on("close", resolve);
3103
- sock.end(payload);
3104
- });
3105
- sock.on("error", resolve);
3106
- } catch {
3107
- resolve();
3108
- }
3109
- });
3186
+ return notifyActivitySocket(data);
3110
3187
  }
3111
3188
  async function authorizeHeadless(toolName, args, meta, options) {
3112
3189
  if (!options?.calledFromDaemon) {
@@ -3123,7 +3200,9 @@ async function authorizeHeadless(toolName, args, meta, options) {
3123
3200
  tool: toolName,
3124
3201
  ts: actTs,
3125
3202
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
3126
- label: result.blockedByLabel
3203
+ label: result.blockedByLabel,
3204
+ ruleHit: result.ruleHit,
3205
+ observeWouldBlock: result.observeWouldBlock
3127
3206
  });
3128
3207
  }
3129
3208
  return result;
@@ -3150,10 +3229,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3150
3229
  appendHookDebug(toolName, args, meta, hashAuditArgs);
3151
3230
  }
3152
3231
  const isManual = meta?.agent === "Terminal";
3232
+ const isObserveMode = config.settings.mode === "observe";
3153
3233
  let explainableLabel = "Local Config";
3154
3234
  let policyMatchedField;
3155
3235
  let policyMatchedWord;
3156
3236
  let riskMetadata;
3237
+ let statefulRecoveryCommand;
3157
3238
  let taintWarning = null;
3158
3239
  if (isNetworkTool(toolName, args)) {
3159
3240
  const filePaths = extractFilePaths(toolName, args);
@@ -3174,10 +3255,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3174
3255
  if (dlpMatch) {
3175
3256
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
3176
3257
  if (dlpMatch.severity === "block") {
3177
- if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta, true);
3258
+ if (!isManual)
3259
+ appendLocalAudit(
3260
+ toolName,
3261
+ args,
3262
+ "deny",
3263
+ isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
3264
+ meta,
3265
+ true
3266
+ );
3178
3267
  if (isWriteTool(toolName) && filePath) {
3179
3268
  await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
3180
3269
  }
3270
+ if (isObserveMode) {
3271
+ return {
3272
+ approved: true,
3273
+ checkedBy: "audit",
3274
+ observeWouldBlock: true,
3275
+ blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
3276
+ };
3277
+ }
3181
3278
  return {
3182
3279
  approved: false,
3183
3280
  reason: dlpReason,
@@ -3190,6 +3287,31 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3190
3287
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
3191
3288
  }
3192
3289
  }
3290
+ if (isObserveMode) {
3291
+ if (!isIgnoredTool(toolName)) {
3292
+ const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
3293
+ const wouldBlock = policyResult.decision === "block";
3294
+ if (!isManual)
3295
+ appendLocalAudit(
3296
+ toolName,
3297
+ args,
3298
+ "allow",
3299
+ wouldBlock ? "observe-mode-would-block" : "observe-mode",
3300
+ meta,
3301
+ hashAuditArgs
3302
+ );
3303
+ return {
3304
+ approved: true,
3305
+ checkedBy: "audit",
3306
+ ...wouldBlock && {
3307
+ observeWouldBlock: true,
3308
+ blockedByLabel: policyResult.blockedByLabel,
3309
+ ruleHit: policyResult.ruleName
3310
+ }
3311
+ };
3312
+ }
3313
+ return { approved: true, checkedBy: "audit" };
3314
+ }
3193
3315
  if (config.settings.mode === "audit") {
3194
3316
  if (!isIgnoredTool(toolName)) {
3195
3317
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
@@ -3212,19 +3334,46 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3212
3334
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
3213
3335
  if (policyResult.decision === "allow") {
3214
3336
  if (approvers.cloud && creds?.apiKey)
3215
- auditLocalAllow(toolName, args, "local-policy", creds, meta);
3337
+ await auditLocalAllow(toolName, args, "local-policy", creds, meta);
3216
3338
  if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
3217
3339
  return { approved: true, checkedBy: "local-policy" };
3218
3340
  }
3219
3341
  if (policyResult.decision === "block") {
3220
- if (!isManual)
3221
- appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
3222
- return {
3223
- approved: false,
3224
- reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
3225
- blockedBy: "local-config",
3226
- blockedByLabel: policyResult.blockedByLabel
3227
- };
3342
+ if (policyResult.dependsOnStatePredicates?.length) {
3343
+ const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
3344
+ const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
3345
+ if (stateResults === null && !isManual) {
3346
+ appendToLog(HOOK_DEBUG_LOG, {
3347
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3348
+ event: "state-check-fail-open",
3349
+ tool: toolName,
3350
+ rule: policyResult.ruleName,
3351
+ predicates: policyResult.dependsOnStatePredicates,
3352
+ reason: "daemon unreachable or /state/check timed out \u2014 block rule downgraded to review"
3353
+ });
3354
+ }
3355
+ if (predicatesMet && policyResult.recoveryCommand) {
3356
+ statefulRecoveryCommand = policyResult.recoveryCommand;
3357
+ }
3358
+ } else if (isDaemonRunning() && !isTestEnv2) {
3359
+ if (!isManual)
3360
+ appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
3361
+ if (approvers.cloud && creds?.apiKey)
3362
+ auditLocalAllow(toolName, args, "smart-rule-block", creds, meta);
3363
+ } else {
3364
+ if (!isManual)
3365
+ appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
3366
+ if (approvers.cloud && creds?.apiKey)
3367
+ auditLocalAllow(toolName, args, "smart-rule-block", creds, meta);
3368
+ return {
3369
+ approved: false,
3370
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
3371
+ blockedBy: "local-config",
3372
+ blockedByLabel: policyResult.blockedByLabel,
3373
+ ruleHit: policyResult.ruleName,
3374
+ ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
3375
+ };
3376
+ }
3228
3377
  }
3229
3378
  explainableLabel = policyResult.blockedByLabel || "Local Config";
3230
3379
  policyMatchedField = policyResult.matchedField;
@@ -3331,7 +3480,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3331
3480
  meta,
3332
3481
  riskMetadata,
3333
3482
  options?.activityId,
3334
- options?.cwd
3483
+ options?.cwd,
3484
+ statefulRecoveryCommand
3335
3485
  );
3336
3486
  daemonEntryId = entry.id;
3337
3487
  daemonAllowCount = entry.allowCount;
@@ -3392,20 +3542,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3392
3542
  if (daemonEntryId && (approvers.browser || approvers.terminal)) {
3393
3543
  racePromises.push(
3394
3544
  (async () => {
3395
- const { decision: daemonDecision, source: decisionSource } = await waitForDaemonDecision(
3396
- daemonEntryId,
3397
- signal
3398
- );
3545
+ const {
3546
+ decision: daemonDecision,
3547
+ source: decisionSource,
3548
+ reason: daemonReason
3549
+ } = await waitForDaemonDecision(daemonEntryId, signal);
3399
3550
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
3400
3551
  const isApproved = daemonDecision === "allow";
3401
- const src = decisionSource === "terminal" || decisionSource === "browser" ? decisionSource : approvers.browser ? "browser" : "terminal";
3552
+ const isRedirect = decisionSource === "terminal-redirect";
3553
+ const src = decisionSource === "terminal" || decisionSource === "terminal-redirect" || decisionSource === "browser" ? decisionSource === "browser" ? "browser" : "terminal" : approvers.browser ? "browser" : "terminal";
3402
3554
  const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
3403
3555
  return {
3404
3556
  approved: isApproved,
3405
- reason: isApproved ? void 0 : `The human user rejected this action via the Node9 ${via}.`,
3557
+ reason: isApproved ? void 0 : (
3558
+ // Use the redirect reason from the tail when choice [2] was selected;
3559
+ // otherwise fall back to the generic rejection message.
3560
+ isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`
3561
+ ),
3406
3562
  checkedBy: isApproved ? "daemon" : void 0,
3407
3563
  blockedBy: isApproved ? void 0 : "local-decision",
3408
- blockedByLabel: `User Decision (${via})`,
3564
+ blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
3409
3565
  decisionSource: src
3410
3566
  };
3411
3567
  })()
@@ -3480,13 +3636,10 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3480
3636
  }
3481
3637
  return finalResult;
3482
3638
  }
3483
- var import_net, import_path13, import_os10, import_crypto3, WRITE_TOOLS, ACTIVITY_SOCKET_PATH;
3639
+ var import_crypto3, WRITE_TOOLS;
3484
3640
  var init_orchestrator = __esm({
3485
3641
  "src/auth/orchestrator.ts"() {
3486
3642
  "use strict";
3487
- import_net = __toESM(require("net"));
3488
- import_path13 = __toESM(require("path"));
3489
- import_os10 = __toESM(require("os"));
3490
3643
  import_crypto3 = require("crypto");
3491
3644
  init_native();
3492
3645
  init_context_sniper();
@@ -3508,7 +3661,6 @@ var init_orchestrator = __esm({
3508
3661
  "notebook_edit",
3509
3662
  "notebookedit"
3510
3663
  ]);
3511
- ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path13.default.join(import_os10.default.tmpdir(), "node9-activity.sock");
3512
3664
  }
3513
3665
  });
3514
3666
 
@@ -5314,6 +5466,107 @@ var init_taint_store = __esm({
5314
5466
  }
5315
5467
  });
5316
5468
 
5469
+ // src/daemon/session-counters.ts
5470
+ var SessionCounters, sessionCounters;
5471
+ var init_session_counters = __esm({
5472
+ "src/daemon/session-counters.ts"() {
5473
+ "use strict";
5474
+ SessionCounters = class {
5475
+ _allowed = 0;
5476
+ _blocked = 0;
5477
+ _dlpHits = 0;
5478
+ _wouldBlock = 0;
5479
+ _lastRuleHit = null;
5480
+ _lastBlockedTool = null;
5481
+ incrementAllowed() {
5482
+ this._allowed++;
5483
+ }
5484
+ incrementBlocked() {
5485
+ this._blocked++;
5486
+ }
5487
+ incrementDlpHits() {
5488
+ this._dlpHits++;
5489
+ }
5490
+ incrementWouldBlock() {
5491
+ this._wouldBlock++;
5492
+ }
5493
+ recordRuleHit(label) {
5494
+ this._lastRuleHit = label;
5495
+ }
5496
+ recordBlockedTool(toolName) {
5497
+ this._lastBlockedTool = toolName;
5498
+ }
5499
+ get() {
5500
+ return {
5501
+ allowed: this._allowed,
5502
+ blocked: this._blocked,
5503
+ dlpHits: this._dlpHits,
5504
+ wouldBlock: this._wouldBlock,
5505
+ lastRuleHit: this._lastRuleHit,
5506
+ lastBlockedTool: this._lastBlockedTool
5507
+ };
5508
+ }
5509
+ reset() {
5510
+ this._allowed = 0;
5511
+ this._blocked = 0;
5512
+ this._dlpHits = 0;
5513
+ this._wouldBlock = 0;
5514
+ this._lastRuleHit = null;
5515
+ this._lastBlockedTool = null;
5516
+ }
5517
+ };
5518
+ sessionCounters = new SessionCounters();
5519
+ }
5520
+ });
5521
+
5522
+ // src/daemon/session-history.ts
5523
+ var SessionHistory, sessionHistory;
5524
+ var init_session_history = __esm({
5525
+ "src/daemon/session-history.ts"() {
5526
+ "use strict";
5527
+ SessionHistory = class {
5528
+ lastEditAt = null;
5529
+ lastTestPassAt = null;
5530
+ lastTestFailAt = null;
5531
+ recordEdit(ts = Date.now()) {
5532
+ this.lastEditAt = ts;
5533
+ }
5534
+ recordTestPass(ts = Date.now()) {
5535
+ this.lastTestPassAt = ts;
5536
+ }
5537
+ recordTestFail(ts = Date.now()) {
5538
+ this.lastTestFailAt = ts;
5539
+ }
5540
+ /**
5541
+ * Returns true when the named predicate is currently satisfied.
5542
+ * Unknown predicates always return false (fail-open: don't block on unknown state).
5543
+ */
5544
+ checkPredicate(name) {
5545
+ switch (name) {
5546
+ case "no_test_passed_since_last_edit":
5547
+ if (this.lastEditAt === null) return false;
5548
+ return this.lastTestPassAt === null || this.lastTestPassAt < this.lastEditAt;
5549
+ default:
5550
+ return false;
5551
+ }
5552
+ }
5553
+ getSnapshot() {
5554
+ return {
5555
+ lastEditAt: this.lastEditAt,
5556
+ lastTestPassAt: this.lastTestPassAt,
5557
+ lastTestFailAt: this.lastTestFailAt
5558
+ };
5559
+ }
5560
+ reset() {
5561
+ this.lastEditAt = null;
5562
+ this.lastTestPassAt = null;
5563
+ this.lastTestFailAt = null;
5564
+ }
5565
+ };
5566
+ sessionHistory = new SessionHistory();
5567
+ }
5568
+ });
5569
+
5317
5570
  // src/daemon/state.ts
5318
5571
  function loadInsightCounts() {
5319
5572
  try {
@@ -5475,6 +5728,7 @@ function readBody(req) {
5475
5728
  });
5476
5729
  }
5477
5730
  function openBrowser(url) {
5731
+ if (process.env.NODE9_TESTING === "1") return;
5478
5732
  try {
5479
5733
  const args = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
5480
5734
  (0, import_child_process3.spawn)(args[0], args.slice(1), { detached: true, stdio: "ignore" }).unref();
@@ -5546,6 +5800,14 @@ function startActivitySocket() {
5546
5800
  socket.on("end", () => {
5547
5801
  try {
5548
5802
  const data = JSON.parse(Buffer.concat(chunks).toString());
5803
+ if (data.status === "test_pass") {
5804
+ sessionHistory.recordTestPass(data.ts);
5805
+ return;
5806
+ }
5807
+ if (data.status === "test_fail") {
5808
+ sessionHistory.recordTestFail(data.ts);
5809
+ return;
5810
+ }
5549
5811
  if (data.status === "pending") {
5550
5812
  broadcast("activity", {
5551
5813
  id: data.id,
@@ -5555,6 +5817,24 @@ function startActivitySocket() {
5555
5817
  status: "pending"
5556
5818
  });
5557
5819
  } else {
5820
+ if (data.status === "allow") {
5821
+ sessionCounters.incrementAllowed();
5822
+ if (data.observeWouldBlock) sessionCounters.incrementWouldBlock();
5823
+ if (WRITE_TOOL_NAMES.has(data.tool.toLowerCase().replace(/[^a-z_]/g, "_"))) {
5824
+ sessionHistory.recordEdit(data.ts);
5825
+ }
5826
+ } else if (data.status === "block") {
5827
+ sessionCounters.incrementBlocked();
5828
+ sessionCounters.recordBlockedTool(data.tool);
5829
+ if (data.ruleHit) sessionCounters.recordRuleHit(data.ruleHit);
5830
+ } else if (data.status === "dlp") {
5831
+ sessionCounters.incrementBlocked();
5832
+ sessionCounters.incrementDlpHits();
5833
+ sessionCounters.recordBlockedTool(data.tool);
5834
+ } else if (data.status === "taint") {
5835
+ sessionCounters.incrementBlocked();
5836
+ sessionCounters.recordBlockedTool(data.tool);
5837
+ }
5558
5838
  broadcast("activity-result", {
5559
5839
  id: data.id,
5560
5840
  status: data.status,
@@ -5575,20 +5855,22 @@ function startActivitySocket() {
5575
5855
  }
5576
5856
  });
5577
5857
  }
5578
- var import_net2, import_fs13, import_path16, import_os12, import_child_process3, import_crypto5, 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;
5858
+ var import_net2, import_fs13, import_path16, import_os11, import_child_process3, import_crypto5, 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;
5579
5859
  var init_state2 = __esm({
5580
5860
  "src/daemon/state.ts"() {
5581
5861
  "use strict";
5582
5862
  import_net2 = __toESM(require("net"));
5583
5863
  import_fs13 = __toESM(require("fs"));
5584
5864
  import_path16 = __toESM(require("path"));
5585
- import_os12 = __toESM(require("os"));
5865
+ import_os11 = __toESM(require("os"));
5586
5866
  import_child_process3 = require("child_process");
5587
5867
  import_crypto5 = require("crypto");
5588
5868
  init_daemon();
5589
5869
  init_suggestion_tracker();
5590
5870
  init_taint_store();
5591
- homeDir = import_os12.default.homedir();
5871
+ init_session_counters();
5872
+ init_session_history();
5873
+ homeDir = import_os11.default.homedir();
5592
5874
  DAEMON_PID_FILE = import_path16.default.join(homeDir, ".node9", "daemon.pid");
5593
5875
  DECISIONS_FILE = import_path16.default.join(homeDir, ".node9", "decisions.json");
5594
5876
  AUDIT_LOG_FILE = import_path16.default.join(homeDir, ".node9", "audit.log");
@@ -5613,10 +5895,21 @@ var init_state2 = __esm({
5613
5895
  "2h": 2 * 60 * 6e4
5614
5896
  };
5615
5897
  autoStarted = process.env.NODE9_AUTO_STARTED === "1";
5616
- ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path16.default.join(import_os12.default.tmpdir(), "node9-activity.sock");
5898
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path16.default.join(import_os11.default.tmpdir(), "node9-activity.sock");
5617
5899
  ACTIVITY_RING_SIZE = 100;
5618
5900
  activityRing = [];
5619
5901
  SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
5902
+ WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
5903
+ "write",
5904
+ "write_file",
5905
+ "create_file",
5906
+ "edit",
5907
+ "multiedit",
5908
+ "str_replace_based_edit_tool",
5909
+ "replace",
5910
+ "notebook_edit",
5911
+ "notebookedit"
5912
+ ]);
5620
5913
  }
5621
5914
  });
5622
5915
 
@@ -5666,14 +5959,14 @@ function patchConfig(configPath, patch) {
5666
5959
  throw err;
5667
5960
  }
5668
5961
  }
5669
- var import_fs14, import_path17, import_os13, GLOBAL_CONFIG_PATH;
5962
+ var import_fs14, import_path17, import_os12, GLOBAL_CONFIG_PATH;
5670
5963
  var init_patch = __esm({
5671
5964
  "src/config/patch.ts"() {
5672
5965
  "use strict";
5673
5966
  import_fs14 = __toESM(require("fs"));
5674
5967
  import_path17 = __toESM(require("path"));
5675
- import_os13 = __toESM(require("os"));
5676
- GLOBAL_CONFIG_PATH = import_path17.default.join(import_os13.default.homedir(), ".node9", "config.json");
5968
+ import_os12 = __toESM(require("os"));
5969
+ GLOBAL_CONFIG_PATH = import_path17.default.join(import_os12.default.homedir(), ".node9", "config.json");
5677
5970
  }
5678
5971
  });
5679
5972
 
@@ -5740,6 +6033,8 @@ data: ${JSON.stringify({
5740
6033
  toolName: e.toolName,
5741
6034
  args: e.args,
5742
6035
  riskMetadata: e.riskMetadata,
6036
+ ...e.recoveryCommand && { recoveryCommand: e.recoveryCommand },
6037
+ ...e.viewOnly && { viewOnly: true },
5743
6038
  slackDelegated: e.slackDelegated,
5744
6039
  timestamp: e.timestamp,
5745
6040
  agent: e.agent,
@@ -5805,6 +6100,9 @@ data: ${JSON.stringify(item.data)}
5805
6100
  agent,
5806
6101
  mcpServer,
5807
6102
  riskMetadata,
6103
+ recoveryCommand,
6104
+ skipBackgroundAuth = false,
6105
+ viewOnly = false,
5808
6106
  fromCLI = false,
5809
6107
  activityId,
5810
6108
  cwd
@@ -5815,6 +6113,8 @@ data: ${JSON.stringify(item.data)}
5815
6113
  toolName,
5816
6114
  args,
5817
6115
  riskMetadata: riskMetadata ?? void 0,
6116
+ ...typeof recoveryCommand === "string" && recoveryCommand && { recoveryCommand },
6117
+ ...viewOnly && { viewOnly: true },
5818
6118
  agent: typeof agent === "string" ? agent : void 0,
5819
6119
  mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
5820
6120
  slackDelegated: !!slackDelegated,
@@ -5859,6 +6159,8 @@ data: ${JSON.stringify(item.data)}
5859
6159
  toolName,
5860
6160
  args,
5861
6161
  riskMetadata: entry.riskMetadata,
6162
+ ...entry.recoveryCommand && { recoveryCommand: entry.recoveryCommand },
6163
+ ...entry.viewOnly && { viewOnly: true },
5862
6164
  slackDelegated: entry.slackDelegated,
5863
6165
  agent: entry.agent,
5864
6166
  mcpServer: entry.mcpServer,
@@ -5875,7 +6177,7 @@ data: ${JSON.stringify(item.data)}
5875
6177
  }
5876
6178
  res.writeHead(200, { "Content-Type": "application/json" });
5877
6179
  res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
5878
- if (slackDelegated) return;
6180
+ if (slackDelegated || skipBackgroundAuth) return;
5879
6181
  authorizeHeadless(
5880
6182
  toolName,
5881
6183
  args,
@@ -6014,7 +6316,7 @@ data: ${JSON.stringify(item.data)}
6014
6316
  saveInsightCounts();
6015
6317
  suggestionTracker.resetTool(entry.toolName);
6016
6318
  }
6017
- const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
6319
+ const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native", "terminal-redirect"]);
6018
6320
  if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
6019
6321
  if (entry.waiter) {
6020
6322
  entry.waiter(resolvedDecision, reason);
@@ -6043,6 +6345,41 @@ data: ${JSON.stringify(item.data)}
6043
6345
  return res.end(JSON.stringify({ error: "internal" }));
6044
6346
  }
6045
6347
  }
6348
+ if (req.method === "GET" && pathname === "/status") {
6349
+ try {
6350
+ const s = getGlobalSettings();
6351
+ const counters = sessionCounters.get();
6352
+ const mode = s.mode ?? "standard";
6353
+ const status = {
6354
+ mode,
6355
+ session: {
6356
+ allowed: counters.allowed,
6357
+ blocked: counters.blocked,
6358
+ dlpHits: counters.dlpHits,
6359
+ wouldBlock: counters.wouldBlock
6360
+ },
6361
+ taintedCount: taintStore.list().length,
6362
+ lastRuleHit: counters.lastRuleHit,
6363
+ lastBlockedTool: counters.lastBlockedTool
6364
+ };
6365
+ res.writeHead(200, { "Content-Type": "application/json" });
6366
+ return res.end(JSON.stringify(status));
6367
+ } catch (err) {
6368
+ console.error(import_chalk2.default.red("[node9 daemon] GET /status failed:"), err);
6369
+ res.writeHead(500, { "Content-Type": "application/json" });
6370
+ return res.end(JSON.stringify({ error: "internal" }));
6371
+ }
6372
+ }
6373
+ if (req.method === "GET" && pathname === "/state/check") {
6374
+ const predicatesParam = reqUrl.searchParams.get("predicates") ?? "";
6375
+ const predicates = predicatesParam.split(",").filter(Boolean);
6376
+ const results = {};
6377
+ for (const p of predicates) {
6378
+ results[p] = sessionHistory.checkPredicate(p);
6379
+ }
6380
+ res.writeHead(200, { "Content-Type": "application/json" });
6381
+ return res.end(JSON.stringify(results));
6382
+ }
6046
6383
  if (req.method === "POST" && pathname === "/settings") {
6047
6384
  if (!validToken(req)) return res.writeHead(403).end();
6048
6385
  try {
@@ -6466,27 +6803,27 @@ function formatBase(activity) {
6466
6803
  const toolName = activity.tool.slice(0, 16).padEnd(16);
6467
6804
  const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
6468
6805
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
6469
- return `${import_chalk16.default.gray(time)} ${icon} ${import_chalk16.default.white.bold(toolName)} ${import_chalk16.default.dim(argsPreview)}`;
6806
+ return `${import_chalk17.default.gray(time)} ${icon} ${import_chalk17.default.white.bold(toolName)} ${import_chalk17.default.dim(argsPreview)}`;
6470
6807
  }
6471
6808
  function renderResult(activity, result) {
6472
6809
  const base = formatBase(activity);
6473
6810
  let status;
6474
6811
  if (result.status === "allow") {
6475
- status = import_chalk16.default.green("\u2713 ALLOW");
6812
+ status = import_chalk17.default.green("\u2713 ALLOW");
6476
6813
  } else if (result.status === "dlp") {
6477
- status = import_chalk16.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
6814
+ status = import_chalk17.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
6478
6815
  } else {
6479
- status = import_chalk16.default.red("\u2717 BLOCK");
6816
+ status = import_chalk17.default.red("\u2717 BLOCK");
6480
6817
  }
6481
6818
  if (process.stdout.isTTY) {
6482
- import_readline3.default.clearLine(process.stdout, 0);
6483
- import_readline3.default.cursorTo(process.stdout, 0);
6819
+ import_readline4.default.clearLine(process.stdout, 0);
6820
+ import_readline4.default.cursorTo(process.stdout, 0);
6484
6821
  }
6485
6822
  console.log(`${base} ${status}`);
6486
6823
  }
6487
6824
  function renderPending(activity) {
6488
6825
  if (!process.stdout.isTTY) return;
6489
- process.stdout.write(`${formatBase(activity)} ${import_chalk16.default.yellow("\u25CF \u2026")}\r`);
6826
+ process.stdout.write(`${formatBase(activity)} ${import_chalk17.default.yellow("\u25CF \u2026")}\r`);
6490
6827
  }
6491
6828
  async function ensureDaemon() {
6492
6829
  let pidPort = null;
@@ -6495,7 +6832,7 @@ async function ensureDaemon() {
6495
6832
  const { port } = JSON.parse(import_fs24.default.readFileSync(PID_FILE, "utf-8"));
6496
6833
  pidPort = port;
6497
6834
  } catch {
6498
- console.error(import_chalk16.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
6835
+ console.error(import_chalk17.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
6499
6836
  }
6500
6837
  }
6501
6838
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -6506,7 +6843,7 @@ async function ensureDaemon() {
6506
6843
  if (res.ok) return checkPort;
6507
6844
  } catch {
6508
6845
  }
6509
- console.log(import_chalk16.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6846
+ console.log(import_chalk17.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6510
6847
  const child = (0, import_child_process13.spawn)(process.execPath, [process.argv[1], "daemon"], {
6511
6848
  detached: true,
6512
6849
  stdio: "ignore",
@@ -6523,14 +6860,15 @@ async function ensureDaemon() {
6523
6860
  } catch {
6524
6861
  }
6525
6862
  }
6526
- console.error(import_chalk16.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6863
+ console.error(import_chalk17.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6527
6864
  process.exit(1);
6528
6865
  }
6529
6866
  function postDecisionHttp(id, decision, csrfToken, port, opts) {
6530
6867
  return new Promise((resolve, reject) => {
6531
- const bodyObj = { decision, source: "terminal" };
6868
+ const bodyObj = { decision, source: opts?.source ?? "terminal" };
6532
6869
  if (opts?.persist) bodyObj.persist = true;
6533
6870
  if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
6871
+ if (opts?.reason) bodyObj.reason = opts.reason;
6534
6872
  const body = JSON.stringify(bodyObj);
6535
6873
  const req = import_http2.default.request(
6536
6874
  {
@@ -6555,33 +6893,61 @@ function postDecisionHttp(id, decision, csrfToken, port, opts) {
6555
6893
  });
6556
6894
  }
6557
6895
  function buildCardLines(req, localCount = 0) {
6896
+ if (req.recoveryCommand) {
6897
+ return buildRecoveryCardLines(req);
6898
+ }
6558
6899
  const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
6559
6900
  const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
6560
6901
  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`;
6561
6902
  const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
6562
6903
  const lines = [
6563
6904
  ``,
6564
- `${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
6565
- `${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
6566
- `${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`
6905
+ `${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
6906
+ `${CYAN}\u2551${RESET2} Tool: ${BOLD2}${req.toolName}${RESET2}`,
6907
+ `${CYAN}\u2551${RESET2} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET2}`
6567
6908
  ];
6568
6909
  if (req.riskMetadata?.ruleName && blockedBy.includes("Taint")) {
6569
- lines.push(`${CYAN}\u2551${RESET} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET}`);
6910
+ lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET2}`);
6570
6911
  }
6571
- lines.push(`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`);
6912
+ lines.push(`${CYAN}\u2551${RESET2} Args: ${GRAY}${argsPreview}${RESET2}`);
6572
6913
  if (localCount >= 2) {
6573
6914
  lines.push(
6574
- `${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
6915
+ `${CYAN}\u2551${RESET2} ${YELLOW}\u{1F4A1}${RESET2} Approved ${localCount}\xD7 before \u2014 ${BOLD2}[a]${RESET2}${YELLOW} creates a permanent rule${RESET2}`
6575
6916
  );
6576
6917
  }
6577
6918
  lines.push(
6578
- `${CYAN}\u255A${RESET}`,
6919
+ `${CYAN}\u255A${RESET2}`,
6579
6920
  ``,
6580
- ` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${YELLOW}[a]${RESET} Always Allow ${BOLD}${CYAN}[t]${RESET} Trust 30m`,
6921
+ ` ${BOLD2}${GREEN}[\u21B5/y]${RESET2} Allow ${BOLD2}${RED}[n]${RESET2} Deny ${BOLD2}${YELLOW}[a]${RESET2} Always Allow ${BOLD2}${CYAN}[t]${RESET2} Trust 30m`,
6581
6922
  ``
6582
6923
  );
6583
6924
  return lines;
6584
6925
  }
6926
+ function buildRecoveryCardLines(req) {
6927
+ const argsObj = req.args;
6928
+ const command = typeof argsObj?.command === "string" ? argsObj.command : JSON.stringify(req.args ?? {}).replace(/\s+/g, " ").slice(0, 60);
6929
+ const ruleName = req.riskMetadata?.ruleName?.replace(/^Smart Rule:\s*/i, "") ?? "policy rule";
6930
+ const recoveryCommand = req.recoveryCommand;
6931
+ const interactiveLines = req.viewOnly ? [` ${GRAY}\u2192 Awaiting decision from interactive terminal...${RESET2}`] : [
6932
+ ` ${BOLD2}${GREEN}[1]${RESET2} Allow anyway ${GRAY}(override policy)${RESET2}`,
6933
+ ` ${BOLD2}${YELLOW}[2]${RESET2} Redirect AI: "Run '${recoveryCommand}' first, then retry"`,
6934
+ ` ${BOLD2}${RED}[3]${RESET2} Deny & stop ${GRAY}(hard block)${RESET2}`,
6935
+ ``,
6936
+ ` ${GRAY}[Timeout: auto-deny]${RESET2}`,
6937
+ ` Select [1-3]: `
6938
+ ];
6939
+ return [
6940
+ ``,
6941
+ `${BOLD2}${CYAN}${DIVIDER}${RESET2}`,
6942
+ `\u{1F6E1}\uFE0F ${BOLD2}NODE9 STATE GUARD:${RESET2} '${BOLD2}${command}${RESET2}'`,
6943
+ `${YELLOW}\u26A0\uFE0F Rule: ${ruleName}${RESET2}`,
6944
+ `${CYAN}${DIVIDER}${RESET2}`,
6945
+ ...!req.viewOnly ? [`${BOLD2}What would you like to do?${RESET2}`, ``] : [],
6946
+ ...interactiveLines,
6947
+ `${CYAN}${DIVIDER}${RESET2}`,
6948
+ ``
6949
+ ];
6950
+ }
6585
6951
  async function startTail(options = {}) {
6586
6952
  const port = await ensureDaemon();
6587
6953
  if (options.clear) {
@@ -6608,7 +6974,7 @@ async function startTail(options = {}) {
6608
6974
  req2.end();
6609
6975
  });
6610
6976
  if (result.ok) {
6611
- console.log(import_chalk16.default.green("\u2713 Flight Recorder buffer cleared."));
6977
+ console.log(import_chalk17.default.green("\u2713 Flight Recorder buffer cleared."));
6612
6978
  } else if (result.code === "ECONNREFUSED") {
6613
6979
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
6614
6980
  } else if (result.code === "ETIMEDOUT") {
@@ -6627,10 +6993,10 @@ async function startTail(options = {}) {
6627
6993
  let cancelActiveCard = null;
6628
6994
  const localAllowCounts = /* @__PURE__ */ new Map();
6629
6995
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
6630
- if (canApprove) import_readline3.default.emitKeypressEvents(process.stdin);
6996
+ if (canApprove) import_readline4.default.emitKeypressEvents(process.stdin);
6631
6997
  function clearCard() {
6632
6998
  if (cardLineCount > 0) {
6633
- import_readline3.default.moveCursor(process.stdout, 0, -cardLineCount);
6999
+ import_readline4.default.moveCursor(process.stdout, 0, -cardLineCount);
6634
7000
  process.stdout.write(ERASE_DOWN);
6635
7001
  cardLineCount = 0;
6636
7002
  }
@@ -6680,14 +7046,14 @@ async function startTail(options = {}) {
6680
7046
  localAllowCounts.get(req2.toolName) ?? 0
6681
7047
  )
6682
7048
  );
6683
- const decisionStamp = action === "always-allow" ? import_chalk16.default.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? import_chalk16.default.cyan("\u23F1 TRUST 30m") : action === "allow" ? import_chalk16.default.green("\u2713 ALLOWED") : import_chalk16.default.red("\u2717 DENIED");
6684
- stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
7049
+ const decisionStamp = action === "always-allow" ? import_chalk17.default.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? import_chalk17.default.cyan("\u23F1 TRUST 30m") : action === "allow" ? import_chalk17.default.green("\u2713 ALLOWED") : action === "redirect" ? import_chalk17.default.yellow("\u21A9 REDIRECT AI") : import_chalk17.default.red("\u2717 DENIED");
7050
+ stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
6685
7051
  for (const line of stampedLines) process.stdout.write(line + "\n");
6686
7052
  process.stdout.write(SHOW_CURSOR);
6687
7053
  cardLineCount = 0;
6688
7054
  if (action === "allow" || action === "always-allow" || action === "trust") {
6689
7055
  localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
6690
- } else if (action === "deny") {
7056
+ } else if (action === "deny" || action === "redirect") {
6691
7057
  localAllowCounts.delete(req2.toolName);
6692
7058
  }
6693
7059
  let httpDecision;
@@ -6698,13 +7064,18 @@ async function startTail(options = {}) {
6698
7064
  } else if (action === "trust") {
6699
7065
  httpDecision = "trust";
6700
7066
  httpOpts = { trustDuration: "30m" };
7067
+ } else if (action === "redirect") {
7068
+ httpDecision = "deny";
7069
+ const recoveryCommand = req2.recoveryCommand ?? "the required pre-condition";
7070
+ 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}\`.`;
7071
+ httpOpts = { reason: redirectReason, source: "terminal-redirect" };
6701
7072
  } else {
6702
7073
  httpDecision = action;
6703
7074
  }
6704
7075
  postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
6705
7076
  try {
6706
7077
  import_fs24.default.appendFileSync(
6707
- import_path26.default.join(import_os21.default.homedir(), ".node9", "hook-debug.log"),
7078
+ import_path27.default.join(import_os20.default.homedir(), ".node9", "hook-debug.log"),
6708
7079
  `[tail] POST /decision failed: ${String(err)}
6709
7080
  `
6710
7081
  );
@@ -6726,8 +7097,8 @@ async function startTail(options = {}) {
6726
7097
  );
6727
7098
  const stampedLines = buildCardLines(req2, priorCount);
6728
7099
  if (externalDecision) {
6729
- const source = externalDecision === "allow" ? import_chalk16.default.green("\u2713 ALLOWED") : import_chalk16.default.red("\u2717 DENIED");
6730
- stampedLines.push(` ${BOLD}\u2192${RESET} ${source} ${GRAY}(external)${RESET}`, ``);
7100
+ const source = externalDecision === "allow" ? import_chalk17.default.green("\u2713 ALLOWED") : import_chalk17.default.red("\u2717 DENIED");
7101
+ stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
6731
7102
  }
6732
7103
  for (const line of stampedLines) process.stdout.write(line + "\n");
6733
7104
  process.stdout.write(SHOW_CURSOR);
@@ -6736,17 +7107,34 @@ async function startTail(options = {}) {
6736
7107
  cardActive = false;
6737
7108
  showNextCard();
6738
7109
  };
7110
+ if (req2.viewOnly) {
7111
+ process.stdin.resume();
7112
+ onKeypress = () => {
7113
+ };
7114
+ process.stdin.on("keypress", onKeypress);
7115
+ return;
7116
+ }
6739
7117
  process.stdin.resume();
6740
7118
  onKeypress = (_str, key) => {
6741
7119
  const name = key?.name ?? "";
6742
- if (name === "y" || name === "return") {
6743
- settle("allow");
6744
- } else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
6745
- settle("deny");
6746
- } else if (name === "a") {
6747
- settle("always-allow");
6748
- } else if (name === "t") {
6749
- settle("trust");
7120
+ if (req2.recoveryCommand) {
7121
+ if (name === "1") {
7122
+ settle("allow");
7123
+ } else if (name === "2") {
7124
+ settle("redirect");
7125
+ } else if (name === "3" || key?.ctrl && name === "c") {
7126
+ settle("deny");
7127
+ }
7128
+ } else {
7129
+ if (name === "y" || name === "return") {
7130
+ settle("allow");
7131
+ } else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
7132
+ settle("deny");
7133
+ } else if (name === "a") {
7134
+ settle("always-allow");
7135
+ } else if (name === "t") {
7136
+ settle("trust");
7137
+ }
6750
7138
  }
6751
7139
  };
6752
7140
  process.stdin.on("keypress", onKeypress);
@@ -6768,41 +7156,41 @@ async function startTail(options = {}) {
6768
7156
  }
6769
7157
  } catch {
6770
7158
  }
6771
- console.log(import_chalk16.default.cyan.bold(`
6772
- \u{1F6F0}\uFE0F Node9 tail `) + import_chalk16.default.dim(`\u2192 ${dashboardUrl}`));
7159
+ console.log(import_chalk17.default.cyan.bold(`
7160
+ \u{1F6F0}\uFE0F Node9 tail `) + import_chalk17.default.dim(`\u2192 ${dashboardUrl}`));
6773
7161
  if (canApprove) {
6774
7162
  console.log(
6775
- import_chalk16.default.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
7163
+ import_chalk17.default.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
6776
7164
  );
6777
7165
  }
6778
7166
  if (options.history) {
6779
- console.log(import_chalk16.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
7167
+ console.log(import_chalk17.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
6780
7168
  } else {
6781
7169
  console.log(
6782
- import_chalk16.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
7170
+ import_chalk17.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
6783
7171
  );
6784
7172
  }
6785
7173
  process.on("SIGINT", () => {
6786
7174
  clearCard();
6787
7175
  process.stdout.write(SHOW_CURSOR);
6788
7176
  if (process.stdout.isTTY) {
6789
- import_readline3.default.clearLine(process.stdout, 0);
6790
- import_readline3.default.cursorTo(process.stdout, 0);
7177
+ import_readline4.default.clearLine(process.stdout, 0);
7178
+ import_readline4.default.cursorTo(process.stdout, 0);
6791
7179
  }
6792
- console.log(import_chalk16.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
7180
+ console.log(import_chalk17.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
6793
7181
  process.exit(0);
6794
7182
  });
6795
7183
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
6796
7184
  const req = import_http2.default.get(sseUrl, (res) => {
6797
7185
  if (res.statusCode !== 200) {
6798
- console.error(import_chalk16.default.red(`Failed to connect: HTTP ${res.statusCode}`));
7186
+ console.error(import_chalk17.default.red(`Failed to connect: HTTP ${res.statusCode}`));
6799
7187
  process.exit(1);
6800
7188
  }
6801
7189
  let currentEvent = "";
6802
7190
  let currentData = "";
6803
7191
  res.on("error", () => {
6804
7192
  });
6805
- const rl = import_readline3.default.createInterface({ input: res, crlfDelay: Infinity });
7193
+ const rl = import_readline4.default.createInterface({ input: res, crlfDelay: Infinity });
6806
7194
  rl.on("error", () => {
6807
7195
  });
6808
7196
  rl.on("line", (line) => {
@@ -6822,10 +7210,10 @@ async function startTail(options = {}) {
6822
7210
  clearCard();
6823
7211
  process.stdout.write(SHOW_CURSOR);
6824
7212
  if (process.stdout.isTTY) {
6825
- import_readline3.default.clearLine(process.stdout, 0);
6826
- import_readline3.default.cursorTo(process.stdout, 0);
7213
+ import_readline4.default.clearLine(process.stdout, 0);
7214
+ import_readline4.default.cursorTo(process.stdout, 0);
6827
7215
  }
6828
- console.log(import_chalk16.default.red("\n\u274C Daemon disconnected."));
7216
+ console.log(import_chalk17.default.red("\n\u274C Daemon disconnected."));
6829
7217
  process.exit(1);
6830
7218
  });
6831
7219
  });
@@ -6911,26 +7299,26 @@ async function startTail(options = {}) {
6911
7299
  }
6912
7300
  req.on("error", (err) => {
6913
7301
  const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
6914
- console.error(import_chalk16.default.red(`
7302
+ console.error(import_chalk17.default.red(`
6915
7303
  \u274C ${msg}`));
6916
7304
  process.exit(1);
6917
7305
  });
6918
7306
  }
6919
- var import_http2, import_chalk16, import_fs24, import_os21, import_path26, import_readline3, import_child_process13, PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN;
7307
+ var import_http2, import_chalk17, import_fs24, import_os20, import_path27, import_readline4, import_child_process13, PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
6920
7308
  var init_tail = __esm({
6921
7309
  "src/tui/tail.ts"() {
6922
7310
  "use strict";
6923
7311
  import_http2 = __toESM(require("http"));
6924
- import_chalk16 = __toESM(require("chalk"));
7312
+ import_chalk17 = __toESM(require("chalk"));
6925
7313
  import_fs24 = __toESM(require("fs"));
6926
- import_os21 = __toESM(require("os"));
6927
- import_path26 = __toESM(require("path"));
6928
- import_readline3 = __toESM(require("readline"));
7314
+ import_os20 = __toESM(require("os"));
7315
+ import_path27 = __toESM(require("path"));
7316
+ import_readline4 = __toESM(require("readline"));
6929
7317
  import_child_process13 = require("child_process");
6930
7318
  init_daemon2();
6931
7319
  init_daemon();
6932
7320
  init_core();
6933
- PID_FILE = import_path26.default.join(import_os21.default.homedir(), ".node9", "daemon.pid");
7321
+ PID_FILE = import_path27.default.join(import_os20.default.homedir(), ".node9", "daemon.pid");
6934
7322
  ICONS = {
6935
7323
  bash: "\u{1F4BB}",
6936
7324
  shell: "\u{1F4BB}",
@@ -6948,8 +7336,8 @@ var init_tail = __esm({
6948
7336
  delete: "\u{1F5D1}\uFE0F",
6949
7337
  web: "\u{1F310}"
6950
7338
  };
6951
- RESET = "\x1B[0m";
6952
- BOLD = "\x1B[1m";
7339
+ RESET2 = "\x1B[0m";
7340
+ BOLD2 = "\x1B[1m";
6953
7341
  RED = "\x1B[31m";
6954
7342
  YELLOW = "\x1B[33m";
6955
7343
  CYAN = "\x1B[36m";
@@ -6958,6 +7346,326 @@ var init_tail = __esm({
6958
7346
  HIDE_CURSOR = "\x1B[?25l";
6959
7347
  SHOW_CURSOR = "\x1B[?25h";
6960
7348
  ERASE_DOWN = "\x1B[J";
7349
+ DIVIDER = "\u2500".repeat(60);
7350
+ }
7351
+ });
7352
+
7353
+ // src/cli/hud.ts
7354
+ var hud_exports = {};
7355
+ __export(hud_exports, {
7356
+ countConfigs: () => countConfigs,
7357
+ main: () => main,
7358
+ renderEnvironmentLine: () => renderEnvironmentLine
7359
+ });
7360
+ async function readStdin() {
7361
+ const chunks = [];
7362
+ for await (const chunk of process.stdin) {
7363
+ chunks.push(chunk);
7364
+ }
7365
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
7366
+ if (!raw) return {};
7367
+ try {
7368
+ return JSON.parse(raw);
7369
+ } catch {
7370
+ return {};
7371
+ }
7372
+ }
7373
+ function queryDaemon() {
7374
+ return new Promise((resolve) => {
7375
+ const timeout = setTimeout(() => resolve(null), 50);
7376
+ try {
7377
+ const req = import_http3.default.get(
7378
+ `http://${DAEMON_HOST}:${DAEMON_PORT}/status`,
7379
+ { timeout: 50 },
7380
+ (res) => {
7381
+ const chunks = [];
7382
+ res.on("data", (c) => chunks.push(c));
7383
+ res.on("end", () => {
7384
+ clearTimeout(timeout);
7385
+ try {
7386
+ resolve(JSON.parse(Buffer.concat(chunks).toString()));
7387
+ } catch {
7388
+ resolve(null);
7389
+ }
7390
+ });
7391
+ }
7392
+ );
7393
+ req.on("error", () => {
7394
+ clearTimeout(timeout);
7395
+ resolve(null);
7396
+ });
7397
+ req.on("timeout", () => {
7398
+ clearTimeout(timeout);
7399
+ req.destroy();
7400
+ resolve(null);
7401
+ });
7402
+ } catch {
7403
+ clearTimeout(timeout);
7404
+ resolve(null);
7405
+ }
7406
+ });
7407
+ }
7408
+ function dim(s) {
7409
+ return `${DIM}${s}${RESET3}`;
7410
+ }
7411
+ function bold(s) {
7412
+ return `${BOLD3}${s}${RESET3}`;
7413
+ }
7414
+ function color(c, s) {
7415
+ return `${c}${s}${RESET3}`;
7416
+ }
7417
+ function progressBar(pct, warnAt = 70, critAt = 85) {
7418
+ const filled = Math.round(Math.min(pct, 100) / 100 * BAR_WIDTH);
7419
+ const bar = BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
7420
+ const c = pct >= critAt ? RED2 : pct >= warnAt ? YELLOW2 : GREEN2;
7421
+ return `${c}${bar}${RESET3}`;
7422
+ }
7423
+ function formatTimeLeft(resetsAt) {
7424
+ if (!resetsAt) return "";
7425
+ const ms = new Date(resetsAt).getTime() - Date.now();
7426
+ if (ms <= 0) return "";
7427
+ const totalMin = Math.ceil(ms / 6e4);
7428
+ const h = Math.floor(totalMin / 60);
7429
+ const m = totalMin % 60;
7430
+ if (h > 0) return ` (${h}h ${m}m left)`;
7431
+ return ` (${m}m left)`;
7432
+ }
7433
+ function safeReadJson(filePath) {
7434
+ if (!import_fs25.default.existsSync(filePath)) return null;
7435
+ try {
7436
+ return JSON.parse(import_fs25.default.readFileSync(filePath, "utf-8"));
7437
+ } catch {
7438
+ return null;
7439
+ }
7440
+ }
7441
+ function getMcpServerNames(filePath) {
7442
+ const cfg = safeReadJson(filePath);
7443
+ if (!cfg || typeof cfg.mcpServers !== "object" || cfg.mcpServers === null) return /* @__PURE__ */ new Set();
7444
+ return new Set(Object.keys(cfg.mcpServers));
7445
+ }
7446
+ function getDisabledMcpServers(filePath, key) {
7447
+ const cfg = safeReadJson(filePath);
7448
+ if (!cfg || !Array.isArray(cfg[key])) return /* @__PURE__ */ new Set();
7449
+ return new Set(cfg[key].filter((s) => typeof s === "string"));
7450
+ }
7451
+ function countHooksInFile(filePath) {
7452
+ const cfg = safeReadJson(filePath);
7453
+ if (!cfg || typeof cfg.hooks !== "object" || cfg.hooks === null) return 0;
7454
+ return Object.keys(cfg.hooks).length;
7455
+ }
7456
+ function countRulesInDir(rulesDir) {
7457
+ if (!import_fs25.default.existsSync(rulesDir)) return 0;
7458
+ let count = 0;
7459
+ try {
7460
+ for (const entry of import_fs25.default.readdirSync(rulesDir, { withFileTypes: true })) {
7461
+ if (entry.isDirectory()) {
7462
+ count += countRulesInDir(import_path28.default.join(rulesDir, entry.name));
7463
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
7464
+ count++;
7465
+ }
7466
+ }
7467
+ } catch {
7468
+ }
7469
+ return count;
7470
+ }
7471
+ function isSamePath(a, b) {
7472
+ try {
7473
+ return import_path28.default.resolve(a) === import_path28.default.resolve(b);
7474
+ } catch {
7475
+ return false;
7476
+ }
7477
+ }
7478
+ function countConfigs(cwd) {
7479
+ const homeDir2 = import_os21.default.homedir();
7480
+ const claudeDir = import_path28.default.join(homeDir2, ".claude");
7481
+ let claudeMdCount = 0;
7482
+ let rulesCount = 0;
7483
+ let hooksCount = 0;
7484
+ const userMcpServers = /* @__PURE__ */ new Set();
7485
+ const projectMcpServers = /* @__PURE__ */ new Set();
7486
+ if (import_fs25.default.existsSync(import_path28.default.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7487
+ rulesCount += countRulesInDir(import_path28.default.join(claudeDir, "rules"));
7488
+ const userSettings = import_path28.default.join(claudeDir, "settings.json");
7489
+ for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
7490
+ hooksCount += countHooksInFile(userSettings);
7491
+ const userClaudeJson = import_path28.default.join(homeDir2, ".claude.json");
7492
+ for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
7493
+ for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
7494
+ userMcpServers.delete(name);
7495
+ }
7496
+ if (cwd) {
7497
+ if (import_fs25.default.existsSync(import_path28.default.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7498
+ if (import_fs25.default.existsSync(import_path28.default.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7499
+ const projectClaudeDir = import_path28.default.join(cwd, ".claude");
7500
+ const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
7501
+ if (!overlapsUserScope) {
7502
+ if (import_fs25.default.existsSync(import_path28.default.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7503
+ rulesCount += countRulesInDir(import_path28.default.join(projectClaudeDir, "rules"));
7504
+ const projSettings = import_path28.default.join(projectClaudeDir, "settings.json");
7505
+ for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
7506
+ hooksCount += countHooksInFile(projSettings);
7507
+ }
7508
+ if (import_fs25.default.existsSync(import_path28.default.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7509
+ const localSettings = import_path28.default.join(projectClaudeDir, "settings.local.json");
7510
+ for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
7511
+ hooksCount += countHooksInFile(localSettings);
7512
+ const mcpJsonServers = getMcpServerNames(import_path28.default.join(cwd, ".mcp.json"));
7513
+ const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
7514
+ for (const name of disabledMcpJson) mcpJsonServers.delete(name);
7515
+ for (const name of mcpJsonServers) projectMcpServers.add(name);
7516
+ }
7517
+ return {
7518
+ claudeMdCount,
7519
+ rulesCount,
7520
+ mcpCount: userMcpServers.size + projectMcpServers.size,
7521
+ hooksCount
7522
+ };
7523
+ }
7524
+ function renderEnvironmentLine(counts) {
7525
+ const { claudeMdCount, rulesCount, mcpCount, hooksCount } = counts;
7526
+ if (claudeMdCount === 0 && rulesCount === 0 && mcpCount === 0 && hooksCount === 0) return null;
7527
+ const parts = [
7528
+ `${claudeMdCount} CLAUDE.md`,
7529
+ `${rulesCount} rules`,
7530
+ `${mcpCount} MCPs`,
7531
+ `${hooksCount} hooks`
7532
+ ];
7533
+ return color(DIM, parts.join(` ${dim("|")} `));
7534
+ }
7535
+ function renderOffline() {
7536
+ process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
7537
+ `);
7538
+ }
7539
+ function renderSecurityLine(status) {
7540
+ const parts = [];
7541
+ parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
7542
+ const modeColors = {
7543
+ standard: GREEN2,
7544
+ strict: RED2,
7545
+ observe: MAGENTA,
7546
+ audit: YELLOW2
7547
+ };
7548
+ const modeIcon = {
7549
+ standard: "",
7550
+ strict: "",
7551
+ observe: "\u{1F441} ",
7552
+ audit: ""
7553
+ };
7554
+ const mc = modeColors[status.mode] ?? WHITE;
7555
+ parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
7556
+ if (status.mode === "observe") {
7557
+ parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
7558
+ if (status.session.wouldBlock > 0) {
7559
+ parts.push(color(YELLOW2, `\u26A0 ${status.session.wouldBlock} would-block`));
7560
+ }
7561
+ } else {
7562
+ parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} allowed`)}`);
7563
+ if (status.session.blocked > 0) {
7564
+ parts.push(color(RED2, `\u{1F6D1} ${status.session.blocked} blocked`));
7565
+ }
7566
+ if (status.session.dlpHits > 0) {
7567
+ parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
7568
+ }
7569
+ }
7570
+ if (status.taintedCount > 0) {
7571
+ parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
7572
+ }
7573
+ if (status.lastRuleHit) {
7574
+ const ruleName = status.lastRuleHit.replace(/^Smart Rule:\s*/i, "");
7575
+ parts.push(color(CYAN2, `\u26A1 ${ruleName}`));
7576
+ }
7577
+ return parts.join(" ");
7578
+ }
7579
+ function renderContextLine(stdin) {
7580
+ const cw = stdin.context_window;
7581
+ if (!cw) return null;
7582
+ const parts = [];
7583
+ const modelName = typeof stdin.model === "string" ? stdin.model : stdin.model?.display_name ?? "";
7584
+ if (modelName) {
7585
+ parts.push(color(CYAN2, modelName));
7586
+ }
7587
+ const usedPct = cw.used_percentage ?? (cw.current_usage && cw.context_window_size ? Math.round(
7588
+ ((cw.current_usage.input_tokens ?? 0) + (cw.current_usage.output_tokens ?? 0)) / cw.context_window_size * 100
7589
+ ) : null);
7590
+ if (usedPct !== null) {
7591
+ const bar = progressBar(usedPct);
7592
+ parts.push(`${dim("\u2502")} ctx ${bar} ${usedPct}%`);
7593
+ }
7594
+ const rl = stdin.rate_limits;
7595
+ if (rl?.five_hour?.used_percentage !== void 0) {
7596
+ const pct = Math.round(rl.five_hour.used_percentage);
7597
+ const bar = progressBar(pct, 60, 80);
7598
+ const left = formatTimeLeft(rl.five_hour.resets_at);
7599
+ parts.push(`${dim("\u2502")} 5h ${bar} ${pct}%${left}`);
7600
+ }
7601
+ if (rl?.seven_day?.used_percentage !== void 0) {
7602
+ const pct = Math.round(rl.seven_day.used_percentage);
7603
+ const bar = progressBar(pct, 60, 80);
7604
+ parts.push(`${dim("\u2502")} 7d ${bar} ${pct}%`);
7605
+ }
7606
+ if (parts.length === 0) return null;
7607
+ return parts.join(" ");
7608
+ }
7609
+ async function main() {
7610
+ try {
7611
+ const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
7612
+ if (!daemonStatus2) {
7613
+ renderOffline();
7614
+ return;
7615
+ }
7616
+ process.stdout.write(renderSecurityLine(daemonStatus2) + "\n");
7617
+ const ctxLine = renderContextLine(stdin);
7618
+ if (ctxLine) {
7619
+ process.stdout.write(ctxLine + "\n");
7620
+ }
7621
+ const showEnvCounts = (() => {
7622
+ try {
7623
+ const cwd = stdin.cwd ?? process.cwd();
7624
+ for (const configPath of [
7625
+ import_path28.default.join(cwd, "node9.config.json"),
7626
+ import_path28.default.join(import_os21.default.homedir(), ".node9", "config.json")
7627
+ ]) {
7628
+ if (!import_fs25.default.existsSync(configPath)) continue;
7629
+ const cfg = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
7630
+ const hud = cfg.settings?.hud;
7631
+ if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
7632
+ }
7633
+ } catch {
7634
+ }
7635
+ return true;
7636
+ })();
7637
+ if (showEnvCounts) {
7638
+ const envLine = renderEnvironmentLine(countConfigs(stdin.cwd));
7639
+ if (envLine) {
7640
+ process.stdout.write(envLine + "\n");
7641
+ }
7642
+ }
7643
+ } catch {
7644
+ renderOffline();
7645
+ }
7646
+ }
7647
+ var import_fs25, import_path28, import_os21, import_http3, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
7648
+ var init_hud = __esm({
7649
+ "src/cli/hud.ts"() {
7650
+ "use strict";
7651
+ import_fs25 = __toESM(require("fs"));
7652
+ import_path28 = __toESM(require("path"));
7653
+ import_os21 = __toESM(require("os"));
7654
+ import_http3 = __toESM(require("http"));
7655
+ init_daemon();
7656
+ RESET3 = "\x1B[0m";
7657
+ BOLD3 = "\x1B[1m";
7658
+ DIM = "\x1B[2m";
7659
+ RED2 = "\x1B[31m";
7660
+ GREEN2 = "\x1B[32m";
7661
+ YELLOW2 = "\x1B[33m";
7662
+ BLUE = "\x1B[34m";
7663
+ MAGENTA = "\x1B[35m";
7664
+ CYAN2 = "\x1B[36m";
7665
+ WHITE = "\x1B[37m";
7666
+ BAR_FILLED = "\u2588";
7667
+ BAR_EMPTY = "\u2591";
7668
+ BAR_WIDTH = 10;
6961
7669
  }
6962
7670
  });
6963
7671
 
@@ -6968,7 +7676,7 @@ init_core();
6968
7676
  // src/setup.ts
6969
7677
  var import_fs11 = __toESM(require("fs"));
6970
7678
  var import_path14 = __toESM(require("path"));
6971
- var import_os11 = __toESM(require("os"));
7679
+ var import_os10 = __toESM(require("os"));
6972
7680
  var import_chalk = __toESM(require("chalk"));
6973
7681
  var import_prompts = require("@inquirer/prompts");
6974
7682
  function printDaemonTip() {
@@ -7002,7 +7710,7 @@ function isNode9Hook(cmd) {
7002
7710
  return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
7003
7711
  }
7004
7712
  function teardownClaude() {
7005
- const homeDir2 = import_os11.default.homedir();
7713
+ const homeDir2 = import_os10.default.homedir();
7006
7714
  const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
7007
7715
  const mcpPath = import_path14.default.join(homeDir2, ".claude.json");
7008
7716
  let changed = false;
@@ -7052,7 +7760,7 @@ function teardownClaude() {
7052
7760
  }
7053
7761
  }
7054
7762
  function teardownGemini() {
7055
- const homeDir2 = import_os11.default.homedir();
7763
+ const homeDir2 = import_os10.default.homedir();
7056
7764
  const settingsPath = import_path14.default.join(homeDir2, ".gemini", "settings.json");
7057
7765
  const settings = readJson(settingsPath);
7058
7766
  if (!settings) {
@@ -7091,7 +7799,7 @@ function teardownGemini() {
7091
7799
  }
7092
7800
  }
7093
7801
  function teardownCursor() {
7094
- const homeDir2 = import_os11.default.homedir();
7802
+ const homeDir2 = import_os10.default.homedir();
7095
7803
  const mcpPath = import_path14.default.join(homeDir2, ".cursor", "mcp.json");
7096
7804
  const mcpConfig = readJson(mcpPath);
7097
7805
  if (!mcpConfig?.mcpServers) {
@@ -7118,7 +7826,7 @@ function teardownCursor() {
7118
7826
  }
7119
7827
  }
7120
7828
  async function setupClaude() {
7121
- const homeDir2 = import_os11.default.homedir();
7829
+ const homeDir2 = import_os10.default.homedir();
7122
7830
  const mcpPath = import_path14.default.join(homeDir2, ".claude.json");
7123
7831
  const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
7124
7832
  const claudeConfig = readJson(mcpPath) ?? {};
@@ -7194,7 +7902,7 @@ async function setupClaude() {
7194
7902
  }
7195
7903
  }
7196
7904
  async function setupGemini() {
7197
- const homeDir2 = import_os11.default.homedir();
7905
+ const homeDir2 = import_os10.default.homedir();
7198
7906
  const settingsPath = import_path14.default.join(homeDir2, ".gemini", "settings.json");
7199
7907
  const settings = readJson(settingsPath) ?? {};
7200
7908
  const servers = settings.mcpServers ?? {};
@@ -7276,7 +7984,7 @@ async function setupGemini() {
7276
7984
  printDaemonTip();
7277
7985
  }
7278
7986
  }
7279
- function detectAgents(homeDir2 = import_os11.default.homedir()) {
7987
+ function detectAgents(homeDir2 = import_os10.default.homedir()) {
7280
7988
  const exists = (p) => {
7281
7989
  try {
7282
7990
  return import_fs11.default.existsSync(p);
@@ -7296,7 +8004,7 @@ function detectAgents(homeDir2 = import_os11.default.homedir()) {
7296
8004
  };
7297
8005
  }
7298
8006
  async function setupCursor() {
7299
- const homeDir2 = import_os11.default.homedir();
8007
+ const homeDir2 = import_os10.default.homedir();
7300
8008
  const mcpPath = import_path14.default.join(homeDir2, ".cursor", "mcp.json");
7301
8009
  const mcpConfig = readJson(mcpPath) ?? {};
7302
8010
  const servers = mcpConfig.mcpServers ?? {};
@@ -7350,14 +8058,60 @@ async function setupCursor() {
7350
8058
  printDaemonTip();
7351
8059
  }
7352
8060
  }
8061
+ function setupHud() {
8062
+ const homeDir2 = import_os10.default.homedir();
8063
+ const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
8064
+ const settings = readJson(hooksPath) ?? {};
8065
+ const hudCommand = fullPathCommand("hud");
8066
+ const statusLineObj = { type: "command", command: hudCommand };
8067
+ const existing = settings.statusLine;
8068
+ const existingCommand = typeof existing === "object" ? existing?.command : existing;
8069
+ if (existingCommand === hudCommand) {
8070
+ console.log(import_chalk.default.blue("\u2139\uFE0F node9 HUD is already configured in ~/.claude/settings.json"));
8071
+ console.log(import_chalk.default.gray(" Restart Claude Code to activate."));
8072
+ return;
8073
+ }
8074
+ if (existing && existingCommand !== hudCommand) {
8075
+ console.log(
8076
+ import_chalk.default.yellow(
8077
+ ` \u26A0\uFE0F statusLine is already set to: "${existingCommand}"
8078
+ Overwriting with node9 HUD.`
8079
+ )
8080
+ );
8081
+ }
8082
+ settings.statusLine = statusLineObj;
8083
+ writeJson(hooksPath, settings);
8084
+ console.log(import_chalk.default.green.bold("\u2705 node9 HUD added to Claude Code statusline"));
8085
+ console.log(import_chalk.default.gray(" Settings: ~/.claude/settings.json"));
8086
+ console.log(import_chalk.default.gray(" Restart Claude Code to activate."));
8087
+ }
8088
+ function teardownHud() {
8089
+ const homeDir2 = import_os10.default.homedir();
8090
+ const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
8091
+ const settings = readJson(hooksPath);
8092
+ if (!settings) {
8093
+ console.log(import_chalk.default.blue(" \u2139\uFE0F ~/.claude/settings.json not found \u2014 nothing to remove"));
8094
+ return;
8095
+ }
8096
+ const existing = settings.statusLine;
8097
+ const existingCommand = typeof existing === "object" ? existing?.command : existing;
8098
+ if (!existingCommand || !String(existingCommand).includes("node9")) {
8099
+ console.log(import_chalk.default.blue(" \u2139\uFE0F node9 HUD not found in ~/.claude/settings.json"));
8100
+ return;
8101
+ }
8102
+ delete settings.statusLine;
8103
+ writeJson(hooksPath, settings);
8104
+ console.log(import_chalk.default.green(" \u2705 node9 HUD removed from ~/.claude/settings.json"));
8105
+ console.log(import_chalk.default.gray(" Restart Claude Code for changes to take effect."));
8106
+ }
7353
8107
 
7354
8108
  // src/cli.ts
7355
8109
  init_daemon2();
7356
- var import_chalk17 = __toESM(require("chalk"));
7357
- var import_fs25 = __toESM(require("fs"));
7358
- var import_path27 = __toESM(require("path"));
8110
+ var import_chalk18 = __toESM(require("chalk"));
8111
+ var import_fs26 = __toESM(require("fs"));
8112
+ var import_path29 = __toESM(require("path"));
7359
8113
  var import_os22 = __toESM(require("os"));
7360
- var import_prompts3 = require("@inquirer/prompts");
8114
+ var import_prompts2 = require("@inquirer/prompts");
7361
8115
 
7362
8116
  // src/utils/duration.ts
7363
8117
  function parseDuration(str) {
@@ -7387,7 +8141,7 @@ var import_execa2 = require("execa");
7387
8141
  init_orchestrator();
7388
8142
 
7389
8143
  // src/policy/negotiation.ts
7390
- function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason) {
8144
+ function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason, recoveryCommand) {
7391
8145
  if (isHumanDecision) {
7392
8146
  return `NODE9: The human user rejected this action.
7393
8147
  REASON: ${humanReason || "No specific reason provided."}
@@ -7443,10 +8197,11 @@ INSTRUCTION: Inform the user this action is pending approval. Wait for them to a
7443
8197
  INSTRUCTION: Do NOT use "${rule}". Find a read-only or non-destructive alternative.
7444
8198
  Do NOT attempt to bypass this rule.`;
7445
8199
  }
8200
+ const recovery = recoveryCommand ? `
8201
+ REQUIRED ACTION: Run \`${recoveryCommand}\` first, then retry your original command.` : "\n- Pivot to a non-destructive or read-only alternative.";
7446
8202
  return `NODE9: Action blocked by security policy [${blockedByLabel}].
7447
8203
  INSTRUCTIONS:
7448
- - Do NOT retry this exact command or attempt to bypass the rule.
7449
- - Pivot to a non-destructive or read-only alternative.
8204
+ - Do NOT retry this exact command or attempt to bypass the rule.${recovery}
7450
8205
  - Inform the user which security rule was triggered and ask how to proceed.`;
7451
8206
  }
7452
8207
 
@@ -7547,6 +8302,7 @@ function openBrowserLocal() {
7547
8302
  }
7548
8303
  }
7549
8304
  async function autoStartDaemonAndWait() {
8305
+ if (process.env.NODE9_TESTING === "1") return false;
7550
8306
  try {
7551
8307
  const child = (0, import_child_process7.spawn)(process.execPath, [process.argv[1], "daemon"], {
7552
8308
  detached: true,
@@ -7579,7 +8335,7 @@ async function autoStartDaemonAndWait() {
7579
8335
  var import_chalk5 = __toESM(require("chalk"));
7580
8336
  var import_fs18 = __toESM(require("fs"));
7581
8337
  var import_path20 = __toESM(require("path"));
7582
- var import_os15 = __toESM(require("os"));
8338
+ var import_os14 = __toESM(require("os"));
7583
8339
  init_orchestrator();
7584
8340
  init_daemon();
7585
8341
  init_config();
@@ -7590,9 +8346,9 @@ var import_child_process8 = require("child_process");
7590
8346
  var import_crypto7 = __toESM(require("crypto"));
7591
8347
  var import_fs17 = __toESM(require("fs"));
7592
8348
  var import_path19 = __toESM(require("path"));
7593
- var import_os14 = __toESM(require("os"));
7594
- var SNAPSHOT_STACK_PATH = import_path19.default.join(import_os14.default.homedir(), ".node9", "snapshots.json");
7595
- var UNDO_LATEST_PATH = import_path19.default.join(import_os14.default.homedir(), ".node9", "undo_latest.txt");
8349
+ var import_os13 = __toESM(require("os"));
8350
+ var SNAPSHOT_STACK_PATH = import_path19.default.join(import_os13.default.homedir(), ".node9", "snapshots.json");
8351
+ var UNDO_LATEST_PATH = import_path19.default.join(import_os13.default.homedir(), ".node9", "undo_latest.txt");
7596
8352
  var MAX_SNAPSHOTS = 10;
7597
8353
  var GIT_TIMEOUT = 15e3;
7598
8354
  function readStack() {
@@ -7608,16 +8364,33 @@ function writeStack(stack) {
7608
8364
  if (!import_fs17.default.existsSync(dir)) import_fs17.default.mkdirSync(dir, { recursive: true });
7609
8365
  import_fs17.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
7610
8366
  }
8367
+ function extractFilePath(args) {
8368
+ if (!args || typeof args !== "object") return null;
8369
+ const a = args;
8370
+ const fp = a.file_path ?? a.path ?? a.filename;
8371
+ return typeof fp === "string" ? fp : null;
8372
+ }
7611
8373
  function buildArgsSummary(tool, args) {
8374
+ const filePath = extractFilePath(args);
8375
+ if (filePath) return filePath;
7612
8376
  if (!args || typeof args !== "object") return "";
7613
8377
  const a = args;
7614
- const filePath = a.file_path ?? a.path ?? a.filename;
7615
- if (typeof filePath === "string") return filePath;
7616
8378
  const cmd = a.command ?? a.cmd;
7617
8379
  if (typeof cmd === "string") return cmd.slice(0, 80);
7618
8380
  const sql = a.sql ?? a.query;
7619
8381
  if (typeof sql === "string") return sql.slice(0, 80);
7620
- return tool;
8382
+ return "";
8383
+ }
8384
+ function findProjectRoot(filePath) {
8385
+ let dir = import_path19.default.dirname(filePath);
8386
+ while (true) {
8387
+ if (import_fs17.default.existsSync(import_path19.default.join(dir, ".git")) || import_fs17.default.existsSync(import_path19.default.join(dir, "package.json"))) {
8388
+ return dir;
8389
+ }
8390
+ const parent = import_path19.default.dirname(dir);
8391
+ if (parent === dir) return process.cwd();
8392
+ dir = parent;
8393
+ }
7621
8394
  }
7622
8395
  function normalizeCwdForHash(cwd) {
7623
8396
  let normalized;
@@ -7632,7 +8405,7 @@ function normalizeCwdForHash(cwd) {
7632
8405
  }
7633
8406
  function getShadowRepoDir(cwd) {
7634
8407
  const hash = import_crypto7.default.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
7635
- return import_path19.default.join(import_os14.default.homedir(), ".node9", "snapshots", hash);
8408
+ return import_path19.default.join(import_os13.default.homedir(), ".node9", "snapshots", hash);
7636
8409
  }
7637
8410
  function cleanOrphanedIndexFiles(shadowDir) {
7638
8411
  try {
@@ -7688,9 +8461,9 @@ function ensureShadowRepo(shadowDir, cwd) {
7688
8461
  } catch {
7689
8462
  }
7690
8463
  const init = (0, import_child_process8.spawnSync)("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
7691
- if (init.status !== 0) {
7692
- if (process.env.NODE9_DEBUG === "1")
7693
- console.error("[Node9] git init --bare failed:", init.stderr?.toString());
8464
+ if (init.status !== 0 || init.error) {
8465
+ const reason = init.error ? init.error.message : init.stderr?.toString();
8466
+ if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
7694
8467
  return false;
7695
8468
  }
7696
8469
  const configFile = import_path19.default.join(shadowDir, "config");
@@ -7720,7 +8493,9 @@ function buildGitEnv(cwd) {
7720
8493
  async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = []) {
7721
8494
  let indexFile = null;
7722
8495
  try {
7723
- const cwd = process.cwd();
8496
+ const rawFilePath = extractFilePath(args);
8497
+ const absFilePath = rawFilePath && import_path19.default.isAbsolute(rawFilePath) ? rawFilePath : null;
8498
+ const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
7724
8499
  const shadowDir = getShadowRepoDir(cwd);
7725
8500
  if (!ensureShadowRepo(shadowDir, cwd)) return null;
7726
8501
  writeShadowExcludes(shadowDir, ignorePaths);
@@ -7743,15 +8518,53 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
7743
8518
  const commitHash = commitRes.stdout?.toString().trim();
7744
8519
  if (!commitHash || commitRes.status !== 0) return null;
7745
8520
  const stack = readStack();
8521
+ const prevEntry = [...stack].reverse().find((e) => e.cwd === cwd);
8522
+ let capturedFiles = [];
8523
+ let capturedDiff = null;
8524
+ if (prevEntry) {
8525
+ const filesRes = (0, import_child_process8.spawnSync)("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
8526
+ env: shadowEnv,
8527
+ timeout: GIT_TIMEOUT
8528
+ });
8529
+ if (filesRes.status === 0) {
8530
+ capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
8531
+ }
8532
+ const diffRes = (0, import_child_process8.spawnSync)("git", ["diff", prevEntry.hash, commitHash], {
8533
+ env: shadowEnv,
8534
+ timeout: GIT_TIMEOUT
8535
+ });
8536
+ if (diffRes.status === 0) {
8537
+ capturedDiff = diffRes.stdout?.toString() || null;
8538
+ }
8539
+ } else {
8540
+ const filesRes = (0, import_child_process8.spawnSync)("git", ["ls-tree", "-r", "--name-only", commitHash], {
8541
+ env: shadowEnv,
8542
+ timeout: GIT_TIMEOUT
8543
+ });
8544
+ if (filesRes.status === 0) {
8545
+ capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
8546
+ }
8547
+ capturedDiff = null;
8548
+ }
7746
8549
  stack.push({
7747
8550
  hash: commitHash,
7748
8551
  tool,
7749
8552
  argsSummary: buildArgsSummary(tool, args),
8553
+ files: capturedFiles,
8554
+ diff: capturedDiff,
7750
8555
  cwd,
7751
8556
  timestamp: Date.now()
7752
8557
  });
7753
8558
  const shouldGc = stack.length % 5 === 0;
7754
- if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
8559
+ let cwdCount = 0;
8560
+ let oldestCwdIdx = -1;
8561
+ for (let i = 0; i < stack.length; i++) {
8562
+ if (stack[i].cwd === cwd) {
8563
+ if (oldestCwdIdx === -1) oldestCwdIdx = i;
8564
+ cwdCount++;
8565
+ }
8566
+ }
8567
+ if (cwdCount > MAX_SNAPSHOTS) stack.splice(oldestCwdIdx, 1);
7755
8568
  writeStack(stack);
7756
8569
  import_fs17.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
7757
8570
  if (shouldGc) {
@@ -7807,14 +8620,21 @@ function applyUndo(hash, cwd) {
7807
8620
  env,
7808
8621
  timeout: GIT_TIMEOUT
7809
8622
  });
7810
- if (restore.status !== 0) return false;
8623
+ if (restore.status !== 0 || restore.error) {
8624
+ if (process.env.NODE9_DEBUG === "1") {
8625
+ const msg = restore.error ? restore.error.message : restore.stderr?.toString();
8626
+ console.error("[Node9] git restore failed:", msg);
8627
+ }
8628
+ return false;
8629
+ }
7811
8630
  const lsTree = (0, import_child_process8.spawnSync)("git", ["ls-tree", "-r", "--name-only", hash], {
7812
8631
  cwd: dir,
7813
8632
  env,
7814
8633
  timeout: GIT_TIMEOUT
7815
8634
  });
7816
8635
  if (lsTree.status !== 0) {
7817
- process.stderr.write(`[Node9] applyUndo: git ls-tree failed for hash ${hash}
8636
+ const errorMsg = lsTree.stderr?.toString() || "Unknown git error";
8637
+ process.stderr.write(`[Node9] applyUndo: git ls-tree failed for hash ${hash}: ${errorMsg}
7818
8638
  `);
7819
8639
  return false;
7820
8640
  }
@@ -7859,7 +8679,7 @@ function registerCheckCommand(program2) {
7859
8679
  } catch (err) {
7860
8680
  const tempConfig = getConfig();
7861
8681
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
7862
- const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
8682
+ const logPath = import_path20.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
7863
8683
  const errMsg = err instanceof Error ? err.message : String(err);
7864
8684
  import_fs18.default.appendFileSync(
7865
8685
  logPath,
@@ -7872,7 +8692,7 @@ RAW: ${raw}
7872
8692
  }
7873
8693
  const config = getConfig(payload.cwd || void 0);
7874
8694
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
7875
- const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
8695
+ const logPath = import_path20.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
7876
8696
  if (!import_fs18.default.existsSync(import_path20.default.dirname(logPath)))
7877
8697
  import_fs18.default.mkdirSync(import_path20.default.dirname(logPath), { recursive: true });
7878
8698
  import_fs18.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
@@ -7900,6 +8720,8 @@ RAW: ${raw}
7900
8720
  }
7901
8721
  writeTty(import_chalk5.default.gray(` Triggered by: ${blockedByContext}`));
7902
8722
  if (result2?.changeHint) writeTty(import_chalk5.default.cyan(` To change: ${result2.changeHint}`));
8723
+ if (result2?.recoveryCommand)
8724
+ writeTty(import_chalk5.default.green(` \u{1F4A1} Run: ${result2.recoveryCommand}`));
7903
8725
  writeTty("");
7904
8726
  } catch {
7905
8727
  } finally {
@@ -7912,7 +8734,8 @@ RAW: ${raw}
7912
8734
  const aiFeedbackMessage = buildNegotiationMessage(
7913
8735
  blockedByContext,
7914
8736
  isHumanDecision,
7915
- msg
8737
+ msg,
8738
+ result2?.recoveryCommand
7916
8739
  );
7917
8740
  process.stdout.write(
7918
8741
  JSON.stringify({
@@ -7980,7 +8803,7 @@ RAW: ${raw}
7980
8803
  });
7981
8804
  } catch (err) {
7982
8805
  if (process.env.NODE9_DEBUG === "1") {
7983
- const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
8806
+ const logPath = import_path20.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
7984
8807
  const errMsg = err instanceof Error ? err.message : String(err);
7985
8808
  import_fs18.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
7986
8809
  `);
@@ -8018,7 +8841,7 @@ RAW: ${raw}
8018
8841
  // src/cli/commands/log.ts
8019
8842
  var import_fs19 = __toESM(require("fs"));
8020
8843
  var import_path21 = __toESM(require("path"));
8021
- var import_os16 = __toESM(require("os"));
8844
+ var import_os15 = __toESM(require("os"));
8022
8845
  init_audit();
8023
8846
  init_config();
8024
8847
  init_policy();
@@ -8062,6 +8885,20 @@ function containsShellMetachar(token) {
8062
8885
  }
8063
8886
 
8064
8887
  // src/cli/commands/log.ts
8888
+ 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;
8889
+ function detectTestResult(command, output) {
8890
+ if (!TEST_COMMAND_RE.test(command)) return null;
8891
+ const out = output.toLowerCase();
8892
+ if (/\b(tests?\s+passed|all\s+tests?\s+passed|passing|test\s+suites?.*passed|ok\b|\d+\s+passed)/i.test(
8893
+ out
8894
+ ) && !/\b(fail|error|failed)\b/.test(out)) {
8895
+ return "pass";
8896
+ }
8897
+ if (/\b(tests?\s+failed|failing|failed|error|assertion\s+error|\d+\s+failed)\b/i.test(out)) {
8898
+ return "fail";
8899
+ }
8900
+ return null;
8901
+ }
8065
8902
  function sanitize3(value) {
8066
8903
  return value.replace(/[\x00-\x1F\x7F]/g, "");
8067
8904
  }
@@ -8080,7 +8917,7 @@ function registerLogCommand(program2) {
8080
8917
  decision: "allowed",
8081
8918
  source: "post-hook"
8082
8919
  };
8083
- const logPath = import_path21.default.join(import_os16.default.homedir(), ".node9", "audit.log");
8920
+ const logPath = import_path21.default.join(import_os15.default.homedir(), ".node9", "audit.log");
8084
8921
  if (!import_fs19.default.existsSync(import_path21.default.dirname(logPath)))
8085
8922
  import_fs19.default.mkdirSync(import_path21.default.dirname(logPath), { recursive: true });
8086
8923
  import_fs19.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
@@ -8093,6 +8930,21 @@ function registerLogCommand(program2) {
8093
8930
  }
8094
8931
  }
8095
8932
  }
8933
+ if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
8934
+ const bashCommand = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
8935
+ const output = payload.tool_response?.output ?? "";
8936
+ if (bashCommand && output) {
8937
+ const testResult = detectTestResult(bashCommand, output);
8938
+ if (testResult) {
8939
+ await notifyActivitySocket({
8940
+ id: "test-result",
8941
+ ts: Date.now(),
8942
+ tool,
8943
+ status: testResult === "pass" ? "test_pass" : "test_fail"
8944
+ });
8945
+ }
8946
+ }
8947
+ }
8096
8948
  const safeCwd = typeof payload.cwd === "string" && import_path21.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
8097
8949
  const config = getConfig(safeCwd);
8098
8950
  if (shouldSnapshot(tool, {}, config)) {
@@ -8102,7 +8954,7 @@ function registerLogCommand(program2) {
8102
8954
  const msg = err instanceof Error ? err.message : String(err);
8103
8955
  process.stderr.write(`[Node9] audit log error: ${msg}
8104
8956
  `);
8105
- const debugPath = import_path21.default.join(import_os16.default.homedir(), ".node9", "hook-debug.log");
8957
+ const debugPath = import_path21.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
8106
8958
  try {
8107
8959
  import_fs19.default.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
8108
8960
  `);
@@ -8410,12 +9262,12 @@ function registerConfigShowCommand(program2) {
8410
9262
  var import_chalk7 = __toESM(require("chalk"));
8411
9263
  var import_fs20 = __toESM(require("fs"));
8412
9264
  var import_path22 = __toESM(require("path"));
8413
- var import_os17 = __toESM(require("os"));
9265
+ var import_os16 = __toESM(require("os"));
8414
9266
  var import_child_process9 = require("child_process");
8415
9267
  init_daemon();
8416
9268
  function registerDoctorCommand(program2, version2) {
8417
9269
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
8418
- const homeDir2 = import_os17.default.homedir();
9270
+ const homeDir2 = import_os16.default.homedir();
8419
9271
  let failures = 0;
8420
9272
  function pass(msg) {
8421
9273
  console.log(import_chalk7.default.green(" \u2705 ") + msg);
@@ -8578,7 +9430,7 @@ function registerDoctorCommand(program2, version2) {
8578
9430
  var import_chalk8 = __toESM(require("chalk"));
8579
9431
  var import_fs21 = __toESM(require("fs"));
8580
9432
  var import_path23 = __toESM(require("path"));
8581
- var import_os18 = __toESM(require("os"));
9433
+ var import_os17 = __toESM(require("os"));
8582
9434
  function formatRelativeTime(timestamp) {
8583
9435
  const diff = Date.now() - new Date(timestamp).getTime();
8584
9436
  const sec = Math.floor(diff / 1e3);
@@ -8591,7 +9443,7 @@ function formatRelativeTime(timestamp) {
8591
9443
  }
8592
9444
  function registerAuditCommand(program2) {
8593
9445
  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) => {
8594
- const logPath = import_path23.default.join(import_os18.default.homedir(), ".node9", "audit.log");
9446
+ const logPath = import_path23.default.join(import_os17.default.homedir(), ".node9", "audit.log");
8595
9447
  if (!import_fs21.default.existsSync(logPath)) {
8596
9448
  console.log(
8597
9449
  import_chalk8.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
@@ -8718,7 +9570,7 @@ function registerDaemonCommand(program2) {
8718
9570
  var import_chalk10 = __toESM(require("chalk"));
8719
9571
  var import_fs22 = __toESM(require("fs"));
8720
9572
  var import_path24 = __toESM(require("path"));
8721
- var import_os19 = __toESM(require("os"));
9573
+ var import_os18 = __toESM(require("os"));
8722
9574
  init_core();
8723
9575
  init_daemon();
8724
9576
  function readJson2(filePath) {
@@ -8789,7 +9641,7 @@ function registerStatusCommand(program2) {
8789
9641
  const modeLabel = settings.mode === "audit" ? import_chalk10.default.blue("audit") : settings.mode === "strict" ? import_chalk10.default.red("strict") : import_chalk10.default.white("standard");
8790
9642
  console.log(` Mode: ${modeLabel}`);
8791
9643
  const projectConfig = import_path24.default.join(process.cwd(), "node9.config.json");
8792
- const globalConfig = import_path24.default.join(import_os19.default.homedir(), ".node9", "config.json");
9644
+ const globalConfig = import_path24.default.join(import_os18.default.homedir(), ".node9", "config.json");
8793
9645
  console.log(
8794
9646
  ` Local: ${import_fs22.default.existsSync(projectConfig) ? import_chalk10.default.green("Active (node9.config.json)") : import_chalk10.default.gray("Not present")}`
8795
9647
  );
@@ -8801,7 +9653,7 @@ function registerStatusCommand(program2) {
8801
9653
  ` Sandbox: ${import_chalk10.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
8802
9654
  );
8803
9655
  }
8804
- const homeDir2 = import_os19.default.homedir();
9656
+ const homeDir2 = import_os18.default.homedir();
8805
9657
  const claudeSettings = readJson2(
8806
9658
  import_path24.default.join(homeDir2, ".claude", "settings.json")
8807
9659
  );
@@ -8870,12 +9722,12 @@ function registerStatusCommand(program2) {
8870
9722
  var import_chalk11 = __toESM(require("chalk"));
8871
9723
  var import_fs23 = __toESM(require("fs"));
8872
9724
  var import_path25 = __toESM(require("path"));
8873
- var import_os20 = __toESM(require("os"));
9725
+ var import_os19 = __toESM(require("os"));
8874
9726
  init_core();
8875
9727
  function registerInitCommand(program2) {
8876
9728
  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) => {
8877
9729
  console.log(import_chalk11.default.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
8878
- const configPath = import_path25.default.join(import_os20.default.homedir(), ".node9", "config.json");
9730
+ const configPath = import_path25.default.join(import_os19.default.homedir(), ".node9", "config.json");
8879
9731
  if (import_fs23.default.existsSync(configPath) && !options.force) {
8880
9732
  console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
8881
9733
  } else {
@@ -8916,106 +9768,305 @@ function registerInitCommand(program2) {
8916
9768
  else if (agent === "cursor") await setupCursor();
8917
9769
  console.log("");
8918
9770
  }
9771
+ if (detected.claude) {
9772
+ setupHud();
9773
+ console.log(import_chalk11.default.green("\u2705 node9 HUD added to Claude Code statusline"));
9774
+ console.log(import_chalk11.default.gray(" Restart Claude Code to activate the security statusline."));
9775
+ console.log("");
9776
+ }
8919
9777
  console.log(import_chalk11.default.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
8920
9778
  console.log(import_chalk11.default.gray(" Run: node9 daemon start"));
8921
9779
  });
8922
9780
  }
8923
9781
 
8924
9782
  // src/cli/commands/undo.ts
9783
+ var import_path26 = __toESM(require("path"));
9784
+ var import_chalk13 = __toESM(require("chalk"));
9785
+
9786
+ // src/tui/undo-navigator.ts
9787
+ var import_readline2 = __toESM(require("readline"));
8925
9788
  var import_chalk12 = __toESM(require("chalk"));
8926
- var import_prompts2 = require("@inquirer/prompts");
9789
+ var RESET = "\x1B[0m";
9790
+ var BOLD = "\x1B[1m";
9791
+ var CLEAR_SCREEN = "\x1B[2J\x1B[H";
9792
+ var SESSION_GAP_MS = 6e4;
9793
+ function formatAge(timestamp) {
9794
+ const age = Math.round((Date.now() - timestamp) / 1e3);
9795
+ if (age < 60) return `${age}s ago`;
9796
+ if (age < 3600) return `${Math.round(age / 60)}m ago`;
9797
+ if (age < 86400) return `${Math.round(age / 3600)}h ago`;
9798
+ return `${Math.round(age / 86400)}d ago`;
9799
+ }
9800
+ function renderDiff(raw) {
9801
+ const lines = raw.split("\n").filter(
9802
+ (l) => !l.startsWith("diff --git") && !l.startsWith("index ") && !l.startsWith("Binary")
9803
+ );
9804
+ for (const line of lines) {
9805
+ if (line.startsWith("+++") || line.startsWith("---")) {
9806
+ process.stdout.write(import_chalk12.default.bold(line) + "\n");
9807
+ } else if (line.startsWith("+")) {
9808
+ process.stdout.write(import_chalk12.default.green(line) + "\n");
9809
+ } else if (line.startsWith("-")) {
9810
+ process.stdout.write(import_chalk12.default.red(line) + "\n");
9811
+ } else if (line.startsWith("@@")) {
9812
+ process.stdout.write(import_chalk12.default.cyan(line) + "\n");
9813
+ } else {
9814
+ process.stdout.write(import_chalk12.default.gray(line) + "\n");
9815
+ }
9816
+ }
9817
+ }
9818
+ function isSessionBoundary(entries, idx) {
9819
+ if (idx <= 0) return false;
9820
+ return entries[idx - 1].timestamp - entries[idx].timestamp > SESSION_GAP_MS;
9821
+ }
9822
+ function sessionStart(entries, idx) {
9823
+ let i = idx;
9824
+ while (i > 0 && !isSessionBoundary(entries, i)) i--;
9825
+ return i;
9826
+ }
9827
+ function render(entries, idx) {
9828
+ const entry = entries[idx];
9829
+ const total = entries.length;
9830
+ const step = idx + 1;
9831
+ process.stdout.write(CLEAR_SCREEN);
9832
+ process.stdout.write(
9833
+ import_chalk12.default.magenta.bold(`\u23EA Node9 Undo`) + import_chalk12.default.gray(` \u2500\u2500 step ${step} of ${total}`) + (entry.files?.length ? import_chalk12.default.gray(
9834
+ ` \u2500\u2500 ${entry.files.slice(0, 2).join(", ")}${entry.files.length > 2 ? ` +${entry.files.length - 2} more` : ""}`
9835
+ ) : "") + "\n\n"
9836
+ );
9837
+ process.stdout.write(
9838
+ ` ${BOLD}Tool:${RESET} ${import_chalk12.default.cyan(entry.tool)}` + (entry.argsSummary ? import_chalk12.default.gray(" \u2192 " + entry.argsSummary) : "") + "\n"
9839
+ );
9840
+ process.stdout.write(` ${BOLD}When:${RESET} ${import_chalk12.default.gray(formatAge(entry.timestamp))}
9841
+ `);
9842
+ process.stdout.write(` ${BOLD}Dir: ${RESET} ${import_chalk12.default.gray(entry.cwd)}
9843
+ `);
9844
+ if (entry.files && entry.files.length > 0) {
9845
+ process.stdout.write(` ${BOLD}Files:${RESET} ${import_chalk12.default.gray(entry.files.join(", "))}
9846
+ `);
9847
+ }
9848
+ if (idx < total - 1 && isSessionBoundary(entries, idx + 1)) {
9849
+ process.stdout.write(import_chalk12.default.gray("\n \u2500\u2500 session boundary above \u2500\u2500\n"));
9850
+ }
9851
+ process.stdout.write("\n");
9852
+ const diff = entry.diff ?? computeUndoDiff(entry.hash, entry.cwd);
9853
+ if (diff) {
9854
+ renderDiff(diff);
9855
+ } else {
9856
+ process.stdout.write(
9857
+ import_chalk12.default.gray(" (no diff \u2014 working tree may already match this snapshot)\n")
9858
+ );
9859
+ }
9860
+ process.stdout.write("\n");
9861
+ process.stdout.write(
9862
+ import_chalk12.default.gray(" ") + (idx < total - 1 ? import_chalk12.default.white("[\u2190] older") : import_chalk12.default.gray("[\u2190] older")) + import_chalk12.default.gray(" ") + (idx > 0 ? import_chalk12.default.white("[\u2192] newer") : import_chalk12.default.gray("[\u2192] newer")) + import_chalk12.default.gray(" ") + import_chalk12.default.green("[\u21B5] restore here") + import_chalk12.default.gray(" ") + import_chalk12.default.yellow("[s] session start") + import_chalk12.default.gray(" ") + import_chalk12.default.gray("[q] quit") + "\n"
9863
+ );
9864
+ }
9865
+ async function runUndoNavigator(entries) {
9866
+ if (entries.length === 0) return { restored: false };
9867
+ const display = [...entries].reverse();
9868
+ let idx = 0;
9869
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
9870
+ render(display, idx);
9871
+ return { restored: false };
9872
+ }
9873
+ import_readline2.default.emitKeypressEvents(process.stdin);
9874
+ return new Promise((resolve) => {
9875
+ let done = false;
9876
+ render(display, idx);
9877
+ try {
9878
+ process.stdin.setRawMode(true);
9879
+ } catch {
9880
+ resolve({ restored: false });
9881
+ return;
9882
+ }
9883
+ process.stdin.resume();
9884
+ const cleanup = () => {
9885
+ process.stdin.removeListener("keypress", onKeypress);
9886
+ try {
9887
+ process.stdin.setRawMode(false);
9888
+ } catch {
9889
+ }
9890
+ process.stdin.pause();
9891
+ };
9892
+ const onKeypress = (_str, key) => {
9893
+ if (done) return;
9894
+ const name = key?.name ?? "";
9895
+ if (name === "left" || name === "h") {
9896
+ if (idx < display.length - 1) {
9897
+ idx++;
9898
+ render(display, idx);
9899
+ }
9900
+ } else if (name === "right" || name === "l") {
9901
+ if (idx > 0) {
9902
+ idx--;
9903
+ render(display, idx);
9904
+ }
9905
+ } else if (name === "s") {
9906
+ const start = sessionStart(display, idx);
9907
+ if (start !== idx) {
9908
+ idx = start;
9909
+ render(display, idx);
9910
+ }
9911
+ } else if (name === "return" || name === "y") {
9912
+ done = true;
9913
+ cleanup();
9914
+ process.stdout.write(CLEAR_SCREEN);
9915
+ const entry = display[idx];
9916
+ process.stdout.write(import_chalk12.default.magenta.bold("\n\u23EA Restoring snapshot...\n\n"));
9917
+ if (applyUndo(entry.hash, entry.cwd)) {
9918
+ process.stdout.write(import_chalk12.default.green("\u2705 Reverted successfully.\n\n"));
9919
+ resolve({ restored: true });
9920
+ } else {
9921
+ process.stdout.write(import_chalk12.default.red("\u274C Undo failed.\n\n"));
9922
+ resolve({ restored: false });
9923
+ }
9924
+ } else if (name === "q" || key?.ctrl && name === "c") {
9925
+ done = true;
9926
+ cleanup();
9927
+ process.stdout.write(CLEAR_SCREEN);
9928
+ process.stdout.write(import_chalk12.default.gray("\nCancelled.\n\n"));
9929
+ resolve({ restored: false });
9930
+ }
9931
+ };
9932
+ process.stdin.on("keypress", onKeypress);
9933
+ });
9934
+ }
9935
+
9936
+ // src/cli/commands/undo.ts
9937
+ function findMatchingCwd(startDir, history) {
9938
+ const cwds = new Set(history.map((e) => e.cwd));
9939
+ let dir = startDir;
9940
+ while (true) {
9941
+ if (cwds.has(dir)) return dir;
9942
+ const parent = import_path26.default.dirname(dir);
9943
+ if (parent === dir) return null;
9944
+ dir = parent;
9945
+ }
9946
+ }
9947
+ function formatAge2(timestamp) {
9948
+ const age = Math.round((Date.now() - timestamp) / 1e3);
9949
+ if (age < 60) return `${age}s ago`;
9950
+ if (age < 3600) return `${Math.round(age / 60)}m ago`;
9951
+ if (age < 86400) return `${Math.round(age / 3600)}h ago`;
9952
+ return `${Math.round(age / 86400)}d ago`;
9953
+ }
8927
9954
  function registerUndoCommand(program2) {
8928
9955
  program2.command("undo").description(
8929
- "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."
8930
- ).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) => {
8931
- const steps = Math.max(1, parseInt(options.steps, 10) || 1);
9956
+ "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."
9957
+ ).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) => {
8932
9958
  const allHistory = getSnapshotHistory();
8933
- const history = options.all ? allHistory : allHistory.filter((s) => s.cwd === process.cwd());
9959
+ const matchedCwd = options.all ? null : findMatchingCwd(process.cwd(), allHistory);
9960
+ const history = options.all ? allHistory : allHistory.filter((s) => s.cwd === matchedCwd);
8934
9961
  if (history.length === 0) {
8935
9962
  if (!options.all && allHistory.length > 0) {
8936
9963
  console.log(
8937
- import_chalk12.default.yellow(
9964
+ import_chalk13.default.yellow(
8938
9965
  `
8939
9966
  \u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
8940
- Run ${import_chalk12.default.cyan("node9 undo --all")} to see snapshots from all projects.
9967
+ Run ${import_chalk13.default.cyan("node9 undo --all")} to see snapshots from all projects.
8941
9968
  `
8942
9969
  )
8943
9970
  );
8944
9971
  } else {
8945
- console.log(import_chalk12.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
9972
+ console.log(import_chalk13.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
8946
9973
  }
8947
9974
  return;
8948
9975
  }
8949
- const idx = history.length - steps;
8950
- if (idx < 0) {
9976
+ if (options.list) {
9977
+ console.log(import_chalk13.default.magenta.bold("\n\u23EA Snapshot History\n"));
8951
9978
  console.log(
8952
- import_chalk12.default.yellow(
8953
- `
8954
- \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
8955
- `
9979
+ import_chalk13.default.gray(
9980
+ ` ${"#".padEnd(3)} ${"File / Command".padEnd(30)} ${"Tool".padEnd(8)} ${"When".padEnd(10)} Dir`
8956
9981
  )
8957
9982
  );
9983
+ console.log(import_chalk13.default.gray(" " + "\u2500".repeat(80)));
9984
+ const display = [...history].reverse();
9985
+ let prevTs = null;
9986
+ for (let i = 0; i < display.length; i++) {
9987
+ const e = display[i];
9988
+ const isGap = prevTs !== null && prevTs - e.timestamp > 6e4;
9989
+ if (isGap) console.log(import_chalk13.default.gray(" \u2500\u2500 earlier \u2500\u2500"));
9990
+ const label = (e.argsSummary || e.files?.[0] || "\u2014").slice(0, 30).padEnd(30);
9991
+ const tool = e.tool.slice(0, 8).padEnd(8);
9992
+ const when = formatAge2(e.timestamp).padEnd(10);
9993
+ const dir = e.cwd.length > 30 ? "\u2026" + e.cwd.slice(-29) : e.cwd;
9994
+ console.log(
9995
+ import_chalk13.default.white(
9996
+ ` ${String(i + 1).padEnd(3)} ${label} ${import_chalk13.default.cyan(tool)} ${import_chalk13.default.gray(when)} ${import_chalk13.default.gray(dir)}`
9997
+ )
9998
+ );
9999
+ prevTs = e.timestamp;
10000
+ }
10001
+ console.log("");
8958
10002
  return;
8959
10003
  }
8960
- const snapshot = history[idx];
8961
- const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
8962
- const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
8963
- console.log(
8964
- import_chalk12.default.magenta.bold(`
10004
+ if (options.steps !== void 0) {
10005
+ const steps = Math.max(1, parseInt(options.steps, 10) || 1);
10006
+ const idx = history.length - steps;
10007
+ if (idx < 0) {
10008
+ console.log(
10009
+ import_chalk13.default.yellow(
10010
+ `
10011
+ \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
10012
+ `
10013
+ )
10014
+ );
10015
+ return;
10016
+ }
10017
+ const snapshot = history[idx];
10018
+ const ageStr = formatAge2(snapshot.timestamp);
10019
+ console.log(
10020
+ import_chalk13.default.magenta.bold(`
8965
10021
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
8966
- );
8967
- console.log(
8968
- import_chalk12.default.white(
8969
- ` Tool: ${import_chalk12.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk12.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
8970
- )
8971
- );
8972
- console.log(import_chalk12.default.white(` When: ${import_chalk12.default.gray(ageStr)}`));
8973
- console.log(import_chalk12.default.white(` Dir: ${import_chalk12.default.gray(snapshot.cwd)}`));
8974
- if (steps > 1)
10022
+ );
8975
10023
  console.log(
8976
- import_chalk12.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
10024
+ import_chalk13.default.white(
10025
+ ` Tool: ${import_chalk13.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk13.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
10026
+ )
8977
10027
  );
8978
- console.log("");
8979
- const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
8980
- if (diff) {
8981
- const lines = diff.split("\n");
8982
- for (const line of lines) {
8983
- if (line.startsWith("+++") || line.startsWith("---")) {
8984
- console.log(import_chalk12.default.bold(line));
8985
- } else if (line.startsWith("+")) {
8986
- console.log(import_chalk12.default.green(line));
8987
- } else if (line.startsWith("-")) {
8988
- console.log(import_chalk12.default.red(line));
8989
- } else if (line.startsWith("@@")) {
8990
- console.log(import_chalk12.default.cyan(line));
8991
- } else {
8992
- console.log(import_chalk12.default.gray(line));
10028
+ console.log(import_chalk13.default.white(` When: ${import_chalk13.default.gray(ageStr)}`));
10029
+ console.log(import_chalk13.default.white(` Dir: ${import_chalk13.default.gray(snapshot.cwd)}`));
10030
+ if (steps > 1)
10031
+ console.log(
10032
+ import_chalk13.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
10033
+ );
10034
+ console.log("");
10035
+ const diff = snapshot.diff ?? computeUndoDiff(snapshot.hash, snapshot.cwd);
10036
+ if (diff) {
10037
+ const lines = diff.split("\n").filter((l) => !l.startsWith("diff --git") && !l.startsWith("index "));
10038
+ for (const line of lines) {
10039
+ if (line.startsWith("+++") || line.startsWith("---")) console.log(import_chalk13.default.bold(line));
10040
+ else if (line.startsWith("+")) console.log(import_chalk13.default.green(line));
10041
+ else if (line.startsWith("-")) console.log(import_chalk13.default.red(line));
10042
+ else if (line.startsWith("@@")) console.log(import_chalk13.default.cyan(line));
10043
+ else console.log(import_chalk13.default.gray(line));
8993
10044
  }
10045
+ console.log("");
10046
+ } else {
10047
+ console.log(
10048
+ import_chalk13.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
10049
+ );
8994
10050
  }
8995
- console.log("");
8996
- } else {
8997
- console.log(
8998
- import_chalk12.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
8999
- );
9000
- }
9001
- const proceed = await (0, import_prompts2.confirm)({
9002
- message: `Revert to this snapshot?`,
9003
- default: false
9004
- });
9005
- if (proceed) {
9006
- if (applyUndo(snapshot.hash, snapshot.cwd)) {
9007
- console.log(import_chalk12.default.green("\n\u2705 Reverted successfully.\n"));
10051
+ const { confirm: confirm3 } = await import("@inquirer/prompts");
10052
+ const proceed = await confirm3({ message: `Revert to this snapshot?`, default: false });
10053
+ if (proceed) {
10054
+ if (applyUndo(snapshot.hash, snapshot.cwd)) {
10055
+ console.log(import_chalk13.default.green("\n\u2705 Reverted successfully.\n"));
10056
+ } else {
10057
+ console.error(import_chalk13.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
10058
+ }
9008
10059
  } else {
9009
- console.error(import_chalk12.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
10060
+ console.log(import_chalk13.default.gray("\nCancelled.\n"));
9010
10061
  }
9011
- } else {
9012
- console.log(import_chalk12.default.gray("\nCancelled.\n"));
10062
+ return;
9013
10063
  }
10064
+ await runUndoNavigator(history);
9014
10065
  });
9015
10066
  }
9016
10067
 
9017
10068
  // src/cli/commands/watch.ts
9018
- var import_chalk13 = __toESM(require("chalk"));
10069
+ var import_chalk14 = __toESM(require("chalk"));
9019
10070
  var import_child_process11 = require("child_process");
9020
10071
  init_daemon();
9021
10072
  function registerWatchCommand(program2) {
@@ -9032,7 +10083,7 @@ function registerWatchCommand(program2) {
9032
10083
  throw new Error("not running");
9033
10084
  }
9034
10085
  } catch {
9035
- console.error(import_chalk13.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
10086
+ console.error(import_chalk14.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
9036
10087
  const child = (0, import_child_process11.spawn)(process.execPath, [process.argv[1], "daemon"], {
9037
10088
  detached: true,
9038
10089
  stdio: "ignore",
@@ -9054,12 +10105,12 @@ function registerWatchCommand(program2) {
9054
10105
  }
9055
10106
  }
9056
10107
  if (!ready) {
9057
- console.error(import_chalk13.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
10108
+ console.error(import_chalk14.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
9058
10109
  process.exit(1);
9059
10110
  }
9060
10111
  }
9061
10112
  console.error(
9062
- import_chalk13.default.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + import_chalk13.default.dim(` \u2192 localhost:${port}`) + import_chalk13.default.dim(
10113
+ import_chalk14.default.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + import_chalk14.default.dim(` \u2192 localhost:${port}`) + import_chalk14.default.dim(
9063
10114
  "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
9064
10115
  )
9065
10116
  );
@@ -9068,7 +10119,7 @@ function registerWatchCommand(program2) {
9068
10119
  env: { ...process.env, NODE9_WATCH_MODE: "1" }
9069
10120
  });
9070
10121
  if (result.error) {
9071
- console.error(import_chalk13.default.red(`\u274C Failed to run command: ${result.error.message}`));
10122
+ console.error(import_chalk14.default.red(`\u274C Failed to run command: ${result.error.message}`));
9072
10123
  process.exit(1);
9073
10124
  }
9074
10125
  process.exit(result.status ?? 0);
@@ -9076,8 +10127,8 @@ function registerWatchCommand(program2) {
9076
10127
  }
9077
10128
 
9078
10129
  // src/mcp-gateway/index.ts
9079
- var import_readline2 = __toESM(require("readline"));
9080
- var import_chalk14 = __toESM(require("chalk"));
10130
+ var import_readline3 = __toESM(require("readline"));
10131
+ var import_chalk15 = __toESM(require("chalk"));
9081
10132
  var import_child_process12 = require("child_process");
9082
10133
  var import_execa3 = require("execa");
9083
10134
  init_orchestrator();
@@ -9141,13 +10192,13 @@ async function runMcpGateway(upstreamCommand) {
9141
10192
  const prov = checkProvenance(executable);
9142
10193
  if (prov.trustLevel === "suspect") {
9143
10194
  console.error(
9144
- import_chalk14.default.red(
10195
+ import_chalk15.default.red(
9145
10196
  `\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
9146
10197
  )
9147
10198
  );
9148
- console.error(import_chalk14.default.red(" Verify this binary is trusted before proceeding."));
10199
+ console.error(import_chalk15.default.red(" Verify this binary is trusted before proceeding."));
9149
10200
  }
9150
- console.error(import_chalk14.default.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
10201
+ console.error(import_chalk15.default.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
9151
10202
  const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
9152
10203
  "NODE_OPTIONS",
9153
10204
  "NODE_PATH",
@@ -9175,7 +10226,7 @@ async function runMcpGateway(upstreamCommand) {
9175
10226
  let authPending = false;
9176
10227
  let deferredExitCode = null;
9177
10228
  let deferredStdinEnd = false;
9178
- const agentIn = import_readline2.default.createInterface({ input: process.stdin, terminal: false });
10229
+ const agentIn = import_readline3.default.createInterface({ input: process.stdin, terminal: false });
9179
10230
  agentIn.on("line", async (line) => {
9180
10231
  let message;
9181
10232
  try {
@@ -9211,10 +10262,10 @@ async function runMcpGateway(upstreamCommand) {
9211
10262
  mcpServer
9212
10263
  });
9213
10264
  if (!result.approved) {
9214
- console.error(import_chalk14.default.red(`
10265
+ console.error(import_chalk15.default.red(`
9215
10266
  \u{1F6D1} Node9 MCP Gateway: Action Blocked`));
9216
- console.error(import_chalk14.default.gray(` Tool: ${toolName}`));
9217
- console.error(import_chalk14.default.gray(` Reason: ${result.reason ?? "Security Policy"}
10267
+ console.error(import_chalk15.default.gray(` Tool: ${toolName}`));
10268
+ console.error(import_chalk15.default.gray(` Reason: ${result.reason ?? "Security Policy"}
9218
10269
  `));
9219
10270
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
9220
10271
  const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -9287,7 +10338,7 @@ function registerMcpGatewayCommand(program2) {
9287
10338
  }
9288
10339
 
9289
10340
  // src/cli/commands/trust.ts
9290
- var import_chalk15 = __toESM(require("chalk"));
10341
+ var import_chalk16 = __toESM(require("chalk"));
9291
10342
  init_trusted_hosts();
9292
10343
  function isValidHost(host) {
9293
10344
  return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
@@ -9298,44 +10349,44 @@ function registerTrustCommand(program2) {
9298
10349
  const normalized = normalizeHost(host.trim());
9299
10350
  if (!isValidHost(normalized)) {
9300
10351
  console.error(
9301
- import_chalk15.default.red(`
10352
+ import_chalk16.default.red(`
9302
10353
  \u274C Invalid host: "${host}"
9303
- `) + import_chalk15.default.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
10354
+ `) + import_chalk16.default.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
9304
10355
  );
9305
10356
  process.exit(1);
9306
10357
  }
9307
10358
  addTrustedHost(normalized);
9308
- console.log(import_chalk15.default.green(`
10359
+ console.log(import_chalk16.default.green(`
9309
10360
  \u2705 ${normalized} added to trusted hosts.`));
9310
10361
  console.log(
9311
- import_chalk15.default.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
10362
+ import_chalk16.default.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
9312
10363
  );
9313
10364
  });
9314
10365
  trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
9315
10366
  const normalized = normalizeHost(host.trim());
9316
10367
  const removed = removeTrustedHost(normalized);
9317
10368
  if (!removed) {
9318
- console.error(import_chalk15.default.yellow(`
10369
+ console.error(import_chalk16.default.yellow(`
9319
10370
  \u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
9320
10371
  `));
9321
10372
  process.exit(1);
9322
10373
  }
9323
- console.log(import_chalk15.default.green(`
10374
+ console.log(import_chalk16.default.green(`
9324
10375
  \u2705 ${normalized} removed from trusted hosts.
9325
10376
  `));
9326
10377
  });
9327
10378
  trustCmd.command("list").description("Show all trusted hosts").action(() => {
9328
10379
  const hosts = readTrustedHosts();
9329
10380
  if (hosts.length === 0) {
9330
- console.log(import_chalk15.default.gray("\n No trusted hosts configured.\n"));
9331
- console.log(` Add one: ${import_chalk15.default.cyan("node9 trust add api.mycompany.com")}
10381
+ console.log(import_chalk16.default.gray("\n No trusted hosts configured.\n"));
10382
+ console.log(` Add one: ${import_chalk16.default.cyan("node9 trust add api.mycompany.com")}
9332
10383
  `);
9333
10384
  return;
9334
10385
  }
9335
- console.log(import_chalk15.default.bold("\n\u{1F513} Trusted Hosts\n"));
10386
+ console.log(import_chalk16.default.bold("\n\u{1F513} Trusted Hosts\n"));
9336
10387
  for (const entry of hosts) {
9337
10388
  const date = new Date(entry.addedAt).toLocaleDateString();
9338
- console.log(` ${import_chalk15.default.cyan(entry.host.padEnd(40))} ${import_chalk15.default.gray(`added ${date}`)}`);
10389
+ console.log(` ${import_chalk16.default.cyan(entry.host.padEnd(40))} ${import_chalk16.default.gray(`added ${date}`)}`);
9339
10390
  }
9340
10391
  console.log("");
9341
10392
  });
@@ -9343,20 +10394,20 @@ function registerTrustCommand(program2) {
9343
10394
 
9344
10395
  // src/cli.ts
9345
10396
  var { version } = JSON.parse(
9346
- import_fs25.default.readFileSync(import_path27.default.join(__dirname, "../package.json"), "utf-8")
10397
+ import_fs26.default.readFileSync(import_path29.default.join(__dirname, "../package.json"), "utf-8")
9347
10398
  );
9348
10399
  var program = new import_commander.Command();
9349
10400
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
9350
10401
  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) => {
9351
10402
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
9352
- const credPath = import_path27.default.join(import_os22.default.homedir(), ".node9", "credentials.json");
9353
- if (!import_fs25.default.existsSync(import_path27.default.dirname(credPath)))
9354
- import_fs25.default.mkdirSync(import_path27.default.dirname(credPath), { recursive: true });
10403
+ const credPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "credentials.json");
10404
+ if (!import_fs26.default.existsSync(import_path29.default.dirname(credPath)))
10405
+ import_fs26.default.mkdirSync(import_path29.default.dirname(credPath), { recursive: true });
9355
10406
  const profileName = options.profile || "default";
9356
10407
  let existingCreds = {};
9357
10408
  try {
9358
- if (import_fs25.default.existsSync(credPath)) {
9359
- const raw = JSON.parse(import_fs25.default.readFileSync(credPath, "utf-8"));
10409
+ if (import_fs26.default.existsSync(credPath)) {
10410
+ const raw = JSON.parse(import_fs26.default.readFileSync(credPath, "utf-8"));
9360
10411
  if (raw.apiKey) {
9361
10412
  existingCreds = {
9362
10413
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -9368,13 +10419,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
9368
10419
  } catch {
9369
10420
  }
9370
10421
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
9371
- import_fs25.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
10422
+ import_fs26.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
9372
10423
  if (profileName === "default") {
9373
- const configPath = import_path27.default.join(import_os22.default.homedir(), ".node9", "config.json");
10424
+ const configPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "config.json");
9374
10425
  let config = {};
9375
10426
  try {
9376
- if (import_fs25.default.existsSync(configPath))
9377
- config = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
10427
+ if (import_fs26.default.existsSync(configPath))
10428
+ config = JSON.parse(import_fs26.default.readFileSync(configPath, "utf-8"));
9378
10429
  } catch {
9379
10430
  }
9380
10431
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -9389,36 +10440,40 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
9389
10440
  approvers.cloud = false;
9390
10441
  }
9391
10442
  s.approvers = approvers;
9392
- if (!import_fs25.default.existsSync(import_path27.default.dirname(configPath)))
9393
- import_fs25.default.mkdirSync(import_path27.default.dirname(configPath), { recursive: true });
9394
- import_fs25.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
10443
+ if (!import_fs26.default.existsSync(import_path29.default.dirname(configPath)))
10444
+ import_fs26.default.mkdirSync(import_path29.default.dirname(configPath), { recursive: true });
10445
+ import_fs26.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
9395
10446
  }
9396
10447
  if (options.profile && profileName !== "default") {
9397
- console.log(import_chalk17.default.green(`\u2705 Profile "${profileName}" saved`));
9398
- console.log(import_chalk17.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
10448
+ console.log(import_chalk18.default.green(`\u2705 Profile "${profileName}" saved`));
10449
+ console.log(import_chalk18.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
9399
10450
  } else if (options.local) {
9400
- console.log(import_chalk17.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
9401
- console.log(import_chalk17.default.gray(` All decisions stay on this machine.`));
10451
+ console.log(import_chalk18.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
10452
+ console.log(import_chalk18.default.gray(` All decisions stay on this machine.`));
9402
10453
  } else {
9403
- console.log(import_chalk17.default.green(`\u2705 Logged in \u2014 agent mode`));
9404
- console.log(import_chalk17.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
10454
+ console.log(import_chalk18.default.green(`\u2705 Logged in \u2014 agent mode`));
10455
+ console.log(import_chalk18.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
9405
10456
  }
9406
10457
  });
9407
- 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) => {
10458
+ 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) => {
9408
10459
  if (target === "gemini") return await setupGemini();
9409
10460
  if (target === "claude") return await setupClaude();
9410
10461
  if (target === "cursor") return await setupCursor();
9411
- console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
10462
+ if (target === "hud") return setupHud();
10463
+ console.error(import_chalk18.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
9412
10464
  process.exit(1);
9413
10465
  });
9414
- 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) => {
10466
+ 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) => {
9415
10467
  if (!target) {
9416
- console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
9417
- console.log(" Usage: " + import_chalk17.default.white("node9 setup <target>") + "\n");
10468
+ console.log(import_chalk18.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
10469
+ console.log(" Usage: " + import_chalk18.default.white("node9 setup <target>") + "\n");
9418
10470
  console.log(" Targets:");
9419
- console.log(" " + import_chalk17.default.green("claude") + " \u2014 Claude Code (hook mode)");
9420
- console.log(" " + import_chalk17.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
9421
- console.log(" " + import_chalk17.default.green("cursor") + " \u2014 Cursor (hook mode)");
10471
+ console.log(" " + import_chalk18.default.green("claude") + " \u2014 Claude Code (hook mode)");
10472
+ console.log(" " + import_chalk18.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
10473
+ console.log(" " + import_chalk18.default.green("cursor") + " \u2014 Cursor (hook mode)");
10474
+ process.stdout.write(
10475
+ " " + import_chalk18.default.green("hud") + " \u2014 Claude Code security statusline\n"
10476
+ );
9422
10477
  console.log("");
9423
10478
  return;
9424
10479
  }
@@ -9426,7 +10481,8 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
9426
10481
  if (t === "gemini") return await setupGemini();
9427
10482
  if (t === "claude") return await setupClaude();
9428
10483
  if (t === "cursor") return await setupCursor();
9429
- console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
10484
+ if (t === "hud") return setupHud();
10485
+ console.error(import_chalk18.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
9430
10486
  process.exit(1);
9431
10487
  });
9432
10488
  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) => {
@@ -9434,31 +10490,34 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
9434
10490
  if (target === "claude") fn = teardownClaude;
9435
10491
  else if (target === "gemini") fn = teardownGemini;
9436
10492
  else if (target === "cursor") fn = teardownCursor;
10493
+ else if (target === "hud") fn = teardownHud;
9437
10494
  else {
9438
- console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
10495
+ console.error(
10496
+ import_chalk18.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
10497
+ );
9439
10498
  process.exit(1);
9440
10499
  }
9441
- console.log(import_chalk17.default.cyan(`
10500
+ console.log(import_chalk18.default.cyan(`
9442
10501
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
9443
10502
  `));
9444
10503
  try {
9445
10504
  fn();
9446
10505
  } catch (err) {
9447
- console.error(import_chalk17.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
10506
+ console.error(import_chalk18.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
9448
10507
  process.exit(1);
9449
10508
  }
9450
- console.log(import_chalk17.default.gray("\n Restart the agent for changes to take effect."));
10509
+ console.log(import_chalk18.default.gray("\n Restart the agent for changes to take effect."));
9451
10510
  });
9452
10511
  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) => {
9453
- console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
9454
- console.log(import_chalk17.default.bold("Stopping daemon..."));
10512
+ console.log(import_chalk18.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
10513
+ console.log(import_chalk18.default.bold("Stopping daemon..."));
9455
10514
  try {
9456
10515
  stopDaemon();
9457
- console.log(import_chalk17.default.green(" \u2705 Daemon stopped"));
10516
+ console.log(import_chalk18.default.green(" \u2705 Daemon stopped"));
9458
10517
  } catch {
9459
- console.log(import_chalk17.default.blue(" \u2139\uFE0F Daemon was not running"));
10518
+ console.log(import_chalk18.default.blue(" \u2139\uFE0F Daemon was not running"));
9460
10519
  }
9461
- console.log(import_chalk17.default.bold("\nRemoving hooks..."));
10520
+ console.log(import_chalk18.default.bold("\nRemoving hooks..."));
9462
10521
  let teardownFailed = false;
9463
10522
  for (const [label, fn] of [
9464
10523
  ["Claude", teardownClaude],
@@ -9470,45 +10529,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
9470
10529
  } catch (err) {
9471
10530
  teardownFailed = true;
9472
10531
  console.error(
9473
- import_chalk17.default.red(
10532
+ import_chalk18.default.red(
9474
10533
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
9475
10534
  )
9476
10535
  );
9477
10536
  }
9478
10537
  }
9479
10538
  if (options.purge) {
9480
- const node9Dir = import_path27.default.join(import_os22.default.homedir(), ".node9");
9481
- if (import_fs25.default.existsSync(node9Dir)) {
9482
- const confirmed = await (0, import_prompts3.confirm)({
10539
+ const node9Dir = import_path29.default.join(import_os22.default.homedir(), ".node9");
10540
+ if (import_fs26.default.existsSync(node9Dir)) {
10541
+ const confirmed = await (0, import_prompts2.confirm)({
9483
10542
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
9484
10543
  default: false
9485
10544
  });
9486
10545
  if (confirmed) {
9487
- import_fs25.default.rmSync(node9Dir, { recursive: true });
9488
- if (import_fs25.default.existsSync(node9Dir)) {
10546
+ import_fs26.default.rmSync(node9Dir, { recursive: true });
10547
+ if (import_fs26.default.existsSync(node9Dir)) {
9489
10548
  console.error(
9490
- import_chalk17.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
10549
+ import_chalk18.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
9491
10550
  );
9492
10551
  } else {
9493
- console.log(import_chalk17.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
10552
+ console.log(import_chalk18.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
9494
10553
  }
9495
10554
  } else {
9496
- console.log(import_chalk17.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
10555
+ console.log(import_chalk18.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
9497
10556
  }
9498
10557
  } else {
9499
- console.log(import_chalk17.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
10558
+ console.log(import_chalk18.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
9500
10559
  }
9501
10560
  } else {
9502
10561
  console.log(
9503
- import_chalk17.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
10562
+ import_chalk18.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
9504
10563
  );
9505
10564
  }
9506
10565
  if (teardownFailed) {
9507
- console.error(import_chalk17.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
10566
+ console.error(import_chalk18.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
9508
10567
  process.exit(1);
9509
10568
  }
9510
- console.log(import_chalk17.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
9511
- console.log(import_chalk17.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
10569
+ console.log(import_chalk18.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
10570
+ console.log(import_chalk18.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
9512
10571
  });
9513
10572
  registerDoctorCommand(program, version);
9514
10573
  program.command("explain").description(
@@ -9521,7 +10580,7 @@ program.command("explain").description(
9521
10580
  try {
9522
10581
  args = JSON.parse(trimmed);
9523
10582
  } catch {
9524
- console.error(import_chalk17.default.red(`
10583
+ console.error(import_chalk18.default.red(`
9525
10584
  \u274C Invalid JSON: ${trimmed}
9526
10585
  `));
9527
10586
  process.exit(1);
@@ -9532,54 +10591,54 @@ program.command("explain").description(
9532
10591
  }
9533
10592
  const result = await explainPolicy(tool, args);
9534
10593
  console.log("");
9535
- console.log(import_chalk17.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
10594
+ console.log(import_chalk18.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
9536
10595
  console.log("");
9537
- console.log(` ${import_chalk17.default.bold("Tool:")} ${import_chalk17.default.white(result.tool)}`);
10596
+ console.log(` ${import_chalk18.default.bold("Tool:")} ${import_chalk18.default.white(result.tool)}`);
9538
10597
  if (argsRaw) {
9539
10598
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
9540
- console.log(` ${import_chalk17.default.bold("Input:")} ${import_chalk17.default.gray(preview)}`);
10599
+ console.log(` ${import_chalk18.default.bold("Input:")} ${import_chalk18.default.gray(preview)}`);
9541
10600
  }
9542
10601
  console.log("");
9543
- console.log(import_chalk17.default.bold("Config Sources (Waterfall):"));
10602
+ console.log(import_chalk18.default.bold("Config Sources (Waterfall):"));
9544
10603
  for (const tier of result.waterfall) {
9545
- const num = import_chalk17.default.gray(` ${tier.tier}.`);
10604
+ const num = import_chalk18.default.gray(` ${tier.tier}.`);
9546
10605
  const label = tier.label.padEnd(16);
9547
10606
  let statusStr;
9548
10607
  if (tier.tier === 1) {
9549
- statusStr = import_chalk17.default.gray(tier.note ?? "");
10608
+ statusStr = import_chalk18.default.gray(tier.note ?? "");
9550
10609
  } else if (tier.status === "active") {
9551
- const loc = tier.path ? import_chalk17.default.gray(tier.path) : "";
9552
- const note = tier.note ? import_chalk17.default.gray(`(${tier.note})`) : "";
9553
- statusStr = import_chalk17.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
10610
+ const loc = tier.path ? import_chalk18.default.gray(tier.path) : "";
10611
+ const note = tier.note ? import_chalk18.default.gray(`(${tier.note})`) : "";
10612
+ statusStr = import_chalk18.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
9554
10613
  } else {
9555
- statusStr = import_chalk17.default.gray("\u25CB " + (tier.note ?? "not found"));
10614
+ statusStr = import_chalk18.default.gray("\u25CB " + (tier.note ?? "not found"));
9556
10615
  }
9557
- console.log(`${num} ${import_chalk17.default.white(label)} ${statusStr}`);
10616
+ console.log(`${num} ${import_chalk18.default.white(label)} ${statusStr}`);
9558
10617
  }
9559
10618
  console.log("");
9560
- console.log(import_chalk17.default.bold("Policy Evaluation:"));
10619
+ console.log(import_chalk18.default.bold("Policy Evaluation:"));
9561
10620
  for (const step of result.steps) {
9562
10621
  const isFinal = step.isFinal;
9563
10622
  let icon;
9564
- if (step.outcome === "allow") icon = import_chalk17.default.green(" \u2705");
9565
- else if (step.outcome === "review") icon = import_chalk17.default.red(" \u{1F534}");
9566
- else if (step.outcome === "skip") icon = import_chalk17.default.gray(" \u2500 ");
9567
- else icon = import_chalk17.default.gray(" \u25CB ");
10623
+ if (step.outcome === "allow") icon = import_chalk18.default.green(" \u2705");
10624
+ else if (step.outcome === "review") icon = import_chalk18.default.red(" \u{1F534}");
10625
+ else if (step.outcome === "skip") icon = import_chalk18.default.gray(" \u2500 ");
10626
+ else icon = import_chalk18.default.gray(" \u25CB ");
9568
10627
  const name = step.name.padEnd(18);
9569
- const nameStr = isFinal ? import_chalk17.default.white.bold(name) : import_chalk17.default.white(name);
9570
- const detail = isFinal ? import_chalk17.default.white(step.detail) : import_chalk17.default.gray(step.detail);
9571
- const arrow = isFinal ? import_chalk17.default.yellow(" \u2190 STOP") : "";
10628
+ const nameStr = isFinal ? import_chalk18.default.white.bold(name) : import_chalk18.default.white(name);
10629
+ const detail = isFinal ? import_chalk18.default.white(step.detail) : import_chalk18.default.gray(step.detail);
10630
+ const arrow = isFinal ? import_chalk18.default.yellow(" \u2190 STOP") : "";
9572
10631
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
9573
10632
  }
9574
10633
  console.log("");
9575
10634
  if (result.decision === "allow") {
9576
- console.log(import_chalk17.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk17.default.gray(" \u2014 no approval needed"));
10635
+ console.log(import_chalk18.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk18.default.gray(" \u2014 no approval needed"));
9577
10636
  } else {
9578
10637
  console.log(
9579
- import_chalk17.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk17.default.gray(" \u2014 human approval required")
10638
+ import_chalk18.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk18.default.gray(" \u2014 human approval required")
9580
10639
  );
9581
10640
  if (result.blockedByLabel) {
9582
- console.log(import_chalk17.default.gray(` Reason: ${result.blockedByLabel}`));
10641
+ console.log(import_chalk18.default.gray(` Reason: ${result.blockedByLabel}`));
9583
10642
  }
9584
10643
  }
9585
10644
  console.log("");
@@ -9593,7 +10652,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
9593
10652
  try {
9594
10653
  await startTail2(options);
9595
10654
  } catch (err) {
9596
- console.error(import_chalk17.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
10655
+ console.error(import_chalk18.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
9597
10656
  process.exit(1);
9598
10657
  }
9599
10658
  });
@@ -9601,11 +10660,15 @@ registerWatchCommand(program);
9601
10660
  registerMcpGatewayCommand(program);
9602
10661
  registerCheckCommand(program);
9603
10662
  registerLogCommand(program);
10663
+ program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").action(async () => {
10664
+ const { main: main2 } = await Promise.resolve().then(() => (init_hud(), hud_exports));
10665
+ await main2();
10666
+ });
9604
10667
  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) => {
9605
10668
  const ms = parseDuration(options.duration);
9606
10669
  if (ms === null) {
9607
10670
  console.error(
9608
- import_chalk17.default.red(`
10671
+ import_chalk18.default.red(`
9609
10672
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
9610
10673
  `)
9611
10674
  );
@@ -9613,20 +10676,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
9613
10676
  }
9614
10677
  pauseNode9(ms, options.duration);
9615
10678
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
9616
- console.log(import_chalk17.default.yellow(`
10679
+ console.log(import_chalk18.default.yellow(`
9617
10680
  \u23F8 Node9 paused until ${expiresAt}`));
9618
- console.log(import_chalk17.default.gray(` All tool calls will be allowed without review.`));
9619
- console.log(import_chalk17.default.gray(` Run "node9 resume" to re-enable early.
10681
+ console.log(import_chalk18.default.gray(` All tool calls will be allowed without review.`));
10682
+ console.log(import_chalk18.default.gray(` Run "node9 resume" to re-enable early.
9620
10683
  `));
9621
10684
  });
9622
10685
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
9623
10686
  const { paused } = checkPause();
9624
10687
  if (!paused) {
9625
- console.log(import_chalk17.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
10688
+ console.log(import_chalk18.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
9626
10689
  return;
9627
10690
  }
9628
10691
  resumeNode9();
9629
- console.log(import_chalk17.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
10692
+ console.log(import_chalk18.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
9630
10693
  });
9631
10694
  var HOOK_BASED_AGENTS = {
9632
10695
  claude: "claude",
@@ -9639,15 +10702,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
9639
10702
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
9640
10703
  const target = HOOK_BASED_AGENTS[firstArg2];
9641
10704
  console.error(
9642
- import_chalk17.default.yellow(`
10705
+ import_chalk18.default.yellow(`
9643
10706
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
9644
10707
  );
9645
- console.error(import_chalk17.default.white(`
10708
+ console.error(import_chalk18.default.white(`
9646
10709
  "${target}" uses its own hook system. Use:`));
9647
10710
  console.error(
9648
- import_chalk17.default.green(` node9 addto ${target} `) + import_chalk17.default.gray("# one-time setup")
10711
+ import_chalk18.default.green(` node9 addto ${target} `) + import_chalk18.default.gray("# one-time setup")
9649
10712
  );
9650
- console.error(import_chalk17.default.green(` ${target} `) + import_chalk17.default.gray("# run normally"));
10713
+ console.error(import_chalk18.default.green(` ${target} `) + import_chalk18.default.gray("# run normally"));
9651
10714
  process.exit(1);
9652
10715
  }
9653
10716
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -9664,12 +10727,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
9664
10727
  }
9665
10728
  );
9666
10729
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
9667
- console.error(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
10730
+ console.error(import_chalk18.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
9668
10731
  const daemonReady = await autoStartDaemonAndWait();
9669
10732
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
9670
10733
  }
9671
10734
  if (result.noApprovalMechanism && process.stdout.isTTY) {
9672
- const approved = await (0, import_prompts3.confirm)({
10735
+ const approved = await (0, import_prompts2.confirm)({
9673
10736
  message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand}"?`,
9674
10737
  default: false
9675
10738
  });
@@ -9677,12 +10740,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
9677
10740
  }
9678
10741
  if (!result.approved) {
9679
10742
  console.error(
9680
- import_chalk17.default.red(`
10743
+ import_chalk18.default.red(`
9681
10744
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
9682
10745
  );
9683
10746
  process.exit(1);
9684
10747
  }
9685
- console.error(import_chalk17.default.green("\n\u2705 Approved \u2014 running command...\n"));
10748
+ console.error(import_chalk18.default.green("\n\u2705 Approved \u2014 running command...\n"));
9686
10749
  await runProxy(fullCommand);
9687
10750
  } else {
9688
10751
  program.help();
@@ -9697,9 +10760,9 @@ if (process.argv[2] !== "daemon") {
9697
10760
  const isCheckHook = process.argv[2] === "check";
9698
10761
  if (isCheckHook) {
9699
10762
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
9700
- const logPath = import_path27.default.join(import_os22.default.homedir(), ".node9", "hook-debug.log");
10763
+ const logPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "hook-debug.log");
9701
10764
  const msg = reason instanceof Error ? reason.message : String(reason);
9702
- import_fs25.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
10765
+ import_fs26.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
9703
10766
  `);
9704
10767
  }
9705
10768
  process.exit(0);