@node9/proxy 1.5.1 → 1.5.3

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
@@ -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);
@@ -1910,7 +1924,13 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1910
1924
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
1911
1925
  reason: matchedRule.reason,
1912
1926
  tier: 2,
1913
- ruleName: matchedRule.name ?? matchedRule.tool
1927
+ ruleName: matchedRule.name ?? matchedRule.tool,
1928
+ ...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
1929
+ dependsOnStatePredicates: matchedRule.dependsOnState
1930
+ },
1931
+ ...matchedRule.verdict === "block" && matchedRule.recoveryCommand && {
1932
+ recoveryCommand: matchedRule.recoveryCommand
1933
+ }
1914
1934
  };
1915
1935
  }
1916
1936
  }
@@ -2437,6 +2457,34 @@ var init_state = __esm({
2437
2457
  });
2438
2458
 
2439
2459
  // src/auth/daemon.ts
2460
+ function notifyActivitySocket(data) {
2461
+ return new Promise((resolve) => {
2462
+ try {
2463
+ const payload = JSON.stringify(data);
2464
+ const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
2465
+ sock.on("connect", () => {
2466
+ sock.on("close", resolve);
2467
+ sock.end(payload);
2468
+ });
2469
+ sock.on("error", resolve);
2470
+ } catch {
2471
+ resolve();
2472
+ }
2473
+ });
2474
+ }
2475
+ async function checkStatePredicates(predicates) {
2476
+ if (predicates.length === 0) return {};
2477
+ try {
2478
+ const qs = predicates.map(encodeURIComponent).join(",");
2479
+ const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/state/check?predicates=${qs}`, {
2480
+ signal: AbortSignal.timeout(100)
2481
+ });
2482
+ if (!res.ok) return null;
2483
+ return await res.json();
2484
+ } catch {
2485
+ return null;
2486
+ }
2487
+ }
2440
2488
  function getInternalToken() {
2441
2489
  try {
2442
2490
  const pidFile = import_path10.default.join(import_os8.default.homedir(), ".node9", "daemon.pid");
@@ -2470,7 +2518,7 @@ function isDaemonRunning() {
2470
2518
  return false;
2471
2519
  }
2472
2520
  }
2473
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
2521
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
2474
2522
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2475
2523
  const ctrl = new AbortController();
2476
2524
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -2488,7 +2536,10 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2488
2536
  // activity-result as the CLI used for the pending activity event.
2489
2537
  activityId,
2490
2538
  ...riskMetadata && { riskMetadata },
2491
- ...cwd && { cwd }
2539
+ ...cwd && { cwd },
2540
+ ...recoveryCommand && { recoveryCommand },
2541
+ ...skipBackgroundAuth && { skipBackgroundAuth: true },
2542
+ ...viewOnly && { viewOnly: true }
2492
2543
  }),
2493
2544
  signal: ctrl.signal
2494
2545
  });
@@ -2508,10 +2559,10 @@ async function waitForDaemonDecision(id, signal) {
2508
2559
  try {
2509
2560
  const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
2510
2561
  if (!waitRes.ok) return { decision: "deny" };
2511
- const { decision, source } = await waitRes.json();
2562
+ const { decision, source, reason } = await waitRes.json();
2512
2563
  if (decision === "allow") return { decision: "allow", source };
2513
2564
  if (decision === "abandoned") return { decision: "abandoned", source };
2514
- return { decision: "deny", source };
2565
+ return { decision: "deny", source, reason };
2515
2566
  } finally {
2516
2567
  clearTimeout(waitTimer);
2517
2568
  if (signal) signal.removeEventListener("abort", onAbort);
@@ -2597,14 +2648,16 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
2597
2648
  signal: AbortSignal.timeout(3e3)
2598
2649
  });
2599
2650
  }
2600
- var import_fs9, import_path10, import_os8, import_child_process, DAEMON_PORT, DAEMON_HOST;
2651
+ var import_fs9, import_net, import_path10, import_os8, import_child_process, ACTIVITY_SOCKET_PATH, DAEMON_PORT, DAEMON_HOST;
2601
2652
  var init_daemon = __esm({
2602
2653
  "src/auth/daemon.ts"() {
2603
2654
  "use strict";
2604
2655
  import_fs9 = __toESM(require("fs"));
2656
+ import_net = __toESM(require("net"));
2605
2657
  import_path10 = __toESM(require("path"));
2606
2658
  import_os8 = __toESM(require("os"));
2607
2659
  import_child_process = require("child_process");
2660
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path10.default.join(import_os8.default.tmpdir(), "node9-activity.sock");
2608
2661
  DAEMON_PORT = 7391;
2609
2662
  DAEMON_HOST = "127.0.0.1";
2610
2663
  }
@@ -3094,19 +3147,7 @@ function isNetworkTool(toolName, args) {
3094
3147
  return false;
3095
3148
  }
3096
3149
  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
- });
3150
+ return notifyActivitySocket(data);
3110
3151
  }
3111
3152
  async function authorizeHeadless(toolName, args, meta, options) {
3112
3153
  if (!options?.calledFromDaemon) {
@@ -3123,7 +3164,9 @@ async function authorizeHeadless(toolName, args, meta, options) {
3123
3164
  tool: toolName,
3124
3165
  ts: actTs,
3125
3166
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
3126
- label: result.blockedByLabel
3167
+ label: result.blockedByLabel,
3168
+ ruleHit: result.ruleHit,
3169
+ observeWouldBlock: result.observeWouldBlock
3127
3170
  });
3128
3171
  }
3129
3172
  return result;
@@ -3150,10 +3193,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3150
3193
  appendHookDebug(toolName, args, meta, hashAuditArgs);
3151
3194
  }
3152
3195
  const isManual = meta?.agent === "Terminal";
3196
+ const isObserveMode = config.settings.mode === "observe";
3153
3197
  let explainableLabel = "Local Config";
3154
3198
  let policyMatchedField;
3155
3199
  let policyMatchedWord;
3156
3200
  let riskMetadata;
3201
+ let statefulRecoveryCommand;
3157
3202
  let taintWarning = null;
3158
3203
  if (isNetworkTool(toolName, args)) {
3159
3204
  const filePaths = extractFilePaths(toolName, args);
@@ -3174,10 +3219,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3174
3219
  if (dlpMatch) {
3175
3220
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
3176
3221
  if (dlpMatch.severity === "block") {
3177
- if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta, true);
3222
+ if (!isManual)
3223
+ appendLocalAudit(
3224
+ toolName,
3225
+ args,
3226
+ "deny",
3227
+ isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
3228
+ meta,
3229
+ true
3230
+ );
3178
3231
  if (isWriteTool(toolName) && filePath) {
3179
3232
  await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
3180
3233
  }
3234
+ if (isObserveMode) {
3235
+ return {
3236
+ approved: true,
3237
+ checkedBy: "audit",
3238
+ observeWouldBlock: true,
3239
+ blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
3240
+ };
3241
+ }
3181
3242
  return {
3182
3243
  approved: false,
3183
3244
  reason: dlpReason,
@@ -3190,6 +3251,31 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3190
3251
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
3191
3252
  }
3192
3253
  }
3254
+ if (isObserveMode) {
3255
+ if (!isIgnoredTool(toolName)) {
3256
+ const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
3257
+ const wouldBlock = policyResult.decision === "block";
3258
+ if (!isManual)
3259
+ appendLocalAudit(
3260
+ toolName,
3261
+ args,
3262
+ "allow",
3263
+ wouldBlock ? "observe-mode-would-block" : "observe-mode",
3264
+ meta,
3265
+ hashAuditArgs
3266
+ );
3267
+ return {
3268
+ approved: true,
3269
+ checkedBy: "audit",
3270
+ ...wouldBlock && {
3271
+ observeWouldBlock: true,
3272
+ blockedByLabel: policyResult.blockedByLabel,
3273
+ ruleHit: policyResult.ruleName
3274
+ }
3275
+ };
3276
+ }
3277
+ return { approved: true, checkedBy: "audit" };
3278
+ }
3193
3279
  if (config.settings.mode === "audit") {
3194
3280
  if (!isIgnoredTool(toolName)) {
3195
3281
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
@@ -3217,14 +3303,34 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3217
3303
  return { approved: true, checkedBy: "local-policy" };
3218
3304
  }
3219
3305
  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
- };
3306
+ if (policyResult.dependsOnStatePredicates?.length) {
3307
+ const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
3308
+ const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
3309
+ if (stateResults === null && !isManual) {
3310
+ appendToLog(HOOK_DEBUG_LOG, {
3311
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3312
+ event: "state-check-fail-open",
3313
+ tool: toolName,
3314
+ rule: policyResult.ruleName,
3315
+ predicates: policyResult.dependsOnStatePredicates,
3316
+ reason: "daemon unreachable or /state/check timed out \u2014 block rule downgraded to review"
3317
+ });
3318
+ }
3319
+ if (predicatesMet && policyResult.recoveryCommand) {
3320
+ statefulRecoveryCommand = policyResult.recoveryCommand;
3321
+ }
3322
+ } else {
3323
+ if (!isManual)
3324
+ appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
3325
+ return {
3326
+ approved: false,
3327
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
3328
+ blockedBy: "local-config",
3329
+ blockedByLabel: policyResult.blockedByLabel,
3330
+ ruleHit: policyResult.ruleName,
3331
+ ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
3332
+ };
3333
+ }
3228
3334
  }
3229
3335
  explainableLabel = policyResult.blockedByLabel || "Local Config";
3230
3336
  policyMatchedField = policyResult.matchedField;
@@ -3331,7 +3437,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3331
3437
  meta,
3332
3438
  riskMetadata,
3333
3439
  options?.activityId,
3334
- options?.cwd
3440
+ options?.cwd,
3441
+ statefulRecoveryCommand
3335
3442
  );
3336
3443
  daemonEntryId = entry.id;
3337
3444
  daemonAllowCount = entry.allowCount;
@@ -3392,20 +3499,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3392
3499
  if (daemonEntryId && (approvers.browser || approvers.terminal)) {
3393
3500
  racePromises.push(
3394
3501
  (async () => {
3395
- const { decision: daemonDecision, source: decisionSource } = await waitForDaemonDecision(
3396
- daemonEntryId,
3397
- signal
3398
- );
3502
+ const {
3503
+ decision: daemonDecision,
3504
+ source: decisionSource,
3505
+ reason: daemonReason
3506
+ } = await waitForDaemonDecision(daemonEntryId, signal);
3399
3507
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
3400
3508
  const isApproved = daemonDecision === "allow";
3401
- const src = decisionSource === "terminal" || decisionSource === "browser" ? decisionSource : approvers.browser ? "browser" : "terminal";
3509
+ const isRedirect = decisionSource === "terminal-redirect";
3510
+ const src = decisionSource === "terminal" || decisionSource === "terminal-redirect" || decisionSource === "browser" ? decisionSource === "browser" ? "browser" : "terminal" : approvers.browser ? "browser" : "terminal";
3402
3511
  const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
3403
3512
  return {
3404
3513
  approved: isApproved,
3405
- reason: isApproved ? void 0 : `The human user rejected this action via the Node9 ${via}.`,
3514
+ reason: isApproved ? void 0 : (
3515
+ // Use the redirect reason from the tail when choice [2] was selected;
3516
+ // otherwise fall back to the generic rejection message.
3517
+ isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`
3518
+ ),
3406
3519
  checkedBy: isApproved ? "daemon" : void 0,
3407
3520
  blockedBy: isApproved ? void 0 : "local-decision",
3408
- blockedByLabel: `User Decision (${via})`,
3521
+ blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
3409
3522
  decisionSource: src
3410
3523
  };
3411
3524
  })()
@@ -3480,13 +3593,10 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3480
3593
  }
3481
3594
  return finalResult;
3482
3595
  }
3483
- var import_net, import_path13, import_os10, import_crypto3, WRITE_TOOLS, ACTIVITY_SOCKET_PATH;
3596
+ var import_crypto3, WRITE_TOOLS;
3484
3597
  var init_orchestrator = __esm({
3485
3598
  "src/auth/orchestrator.ts"() {
3486
3599
  "use strict";
3487
- import_net = __toESM(require("net"));
3488
- import_path13 = __toESM(require("path"));
3489
- import_os10 = __toESM(require("os"));
3490
3600
  import_crypto3 = require("crypto");
3491
3601
  init_native();
3492
3602
  init_context_sniper();
@@ -3508,7 +3618,6 @@ var init_orchestrator = __esm({
3508
3618
  "notebook_edit",
3509
3619
  "notebookedit"
3510
3620
  ]);
3511
- ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path13.default.join(import_os10.default.tmpdir(), "node9-activity.sock");
3512
3621
  }
3513
3622
  });
3514
3623
 
@@ -5235,12 +5344,12 @@ var init_suggestion_tracker = __esm({
5235
5344
  });
5236
5345
 
5237
5346
  // src/daemon/taint-store.ts
5238
- var import_fs12, import_path15, DEFAULT_TTL_MS, TaintStore;
5347
+ var import_fs12, import_path14, DEFAULT_TTL_MS, TaintStore;
5239
5348
  var init_taint_store = __esm({
5240
5349
  "src/daemon/taint-store.ts"() {
5241
5350
  "use strict";
5242
5351
  import_fs12 = __toESM(require("fs"));
5243
- import_path15 = __toESM(require("path"));
5352
+ import_path14 = __toESM(require("path"));
5244
5353
  DEFAULT_TTL_MS = 60 * 60 * 1e3;
5245
5354
  TaintStore = class {
5246
5355
  records = /* @__PURE__ */ new Map();
@@ -5305,15 +5414,116 @@ var init_taint_store = __esm({
5305
5414
  /** Resolve to absolute path, falling back to path.resolve if file doesn't exist yet. */
5306
5415
  _resolve(filePath) {
5307
5416
  try {
5308
- return import_fs12.default.realpathSync.native(import_path15.default.resolve(filePath));
5417
+ return import_fs12.default.realpathSync.native(import_path14.default.resolve(filePath));
5309
5418
  } catch {
5310
- return import_path15.default.resolve(filePath);
5419
+ return import_path14.default.resolve(filePath);
5311
5420
  }
5312
5421
  }
5313
5422
  };
5314
5423
  }
5315
5424
  });
5316
5425
 
5426
+ // src/daemon/session-counters.ts
5427
+ var SessionCounters, sessionCounters;
5428
+ var init_session_counters = __esm({
5429
+ "src/daemon/session-counters.ts"() {
5430
+ "use strict";
5431
+ SessionCounters = class {
5432
+ _allowed = 0;
5433
+ _blocked = 0;
5434
+ _dlpHits = 0;
5435
+ _wouldBlock = 0;
5436
+ _lastRuleHit = null;
5437
+ _lastBlockedTool = null;
5438
+ incrementAllowed() {
5439
+ this._allowed++;
5440
+ }
5441
+ incrementBlocked() {
5442
+ this._blocked++;
5443
+ }
5444
+ incrementDlpHits() {
5445
+ this._dlpHits++;
5446
+ }
5447
+ incrementWouldBlock() {
5448
+ this._wouldBlock++;
5449
+ }
5450
+ recordRuleHit(label) {
5451
+ this._lastRuleHit = label;
5452
+ }
5453
+ recordBlockedTool(toolName) {
5454
+ this._lastBlockedTool = toolName;
5455
+ }
5456
+ get() {
5457
+ return {
5458
+ allowed: this._allowed,
5459
+ blocked: this._blocked,
5460
+ dlpHits: this._dlpHits,
5461
+ wouldBlock: this._wouldBlock,
5462
+ lastRuleHit: this._lastRuleHit,
5463
+ lastBlockedTool: this._lastBlockedTool
5464
+ };
5465
+ }
5466
+ reset() {
5467
+ this._allowed = 0;
5468
+ this._blocked = 0;
5469
+ this._dlpHits = 0;
5470
+ this._wouldBlock = 0;
5471
+ this._lastRuleHit = null;
5472
+ this._lastBlockedTool = null;
5473
+ }
5474
+ };
5475
+ sessionCounters = new SessionCounters();
5476
+ }
5477
+ });
5478
+
5479
+ // src/daemon/session-history.ts
5480
+ var SessionHistory, sessionHistory;
5481
+ var init_session_history = __esm({
5482
+ "src/daemon/session-history.ts"() {
5483
+ "use strict";
5484
+ SessionHistory = class {
5485
+ lastEditAt = null;
5486
+ lastTestPassAt = null;
5487
+ lastTestFailAt = null;
5488
+ recordEdit(ts = Date.now()) {
5489
+ this.lastEditAt = ts;
5490
+ }
5491
+ recordTestPass(ts = Date.now()) {
5492
+ this.lastTestPassAt = ts;
5493
+ }
5494
+ recordTestFail(ts = Date.now()) {
5495
+ this.lastTestFailAt = ts;
5496
+ }
5497
+ /**
5498
+ * Returns true when the named predicate is currently satisfied.
5499
+ * Unknown predicates always return false (fail-open: don't block on unknown state).
5500
+ */
5501
+ checkPredicate(name) {
5502
+ switch (name) {
5503
+ case "no_test_passed_since_last_edit":
5504
+ if (this.lastEditAt === null) return false;
5505
+ return this.lastTestPassAt === null || this.lastTestPassAt < this.lastEditAt;
5506
+ default:
5507
+ return false;
5508
+ }
5509
+ }
5510
+ getSnapshot() {
5511
+ return {
5512
+ lastEditAt: this.lastEditAt,
5513
+ lastTestPassAt: this.lastTestPassAt,
5514
+ lastTestFailAt: this.lastTestFailAt
5515
+ };
5516
+ }
5517
+ reset() {
5518
+ this.lastEditAt = null;
5519
+ this.lastTestPassAt = null;
5520
+ this.lastTestFailAt = null;
5521
+ }
5522
+ };
5523
+ sessionHistory = new SessionHistory();
5524
+ }
5525
+ });
5526
+
5317
5527
  // src/daemon/state.ts
5318
5528
  function loadInsightCounts() {
5319
5529
  try {
@@ -5357,7 +5567,7 @@ function markRejectionHandlerRegistered() {
5357
5567
  daemonRejectionHandlerRegistered = true;
5358
5568
  }
5359
5569
  function atomicWriteSync2(filePath, data, options) {
5360
- const dir = import_path16.default.dirname(filePath);
5570
+ const dir = import_path15.default.dirname(filePath);
5361
5571
  if (!import_fs13.default.existsSync(dir)) import_fs13.default.mkdirSync(dir, { recursive: true });
5362
5572
  const tmpPath = `${filePath}.${(0, import_crypto5.randomUUID)()}.tmp`;
5363
5573
  try {
@@ -5397,7 +5607,7 @@ function appendAuditLog(data) {
5397
5607
  decision: data.decision,
5398
5608
  source: "daemon"
5399
5609
  };
5400
- const dir = import_path16.default.dirname(AUDIT_LOG_FILE);
5610
+ const dir = import_path15.default.dirname(AUDIT_LOG_FILE);
5401
5611
  if (!import_fs13.default.existsSync(dir)) import_fs13.default.mkdirSync(dir, { recursive: true });
5402
5612
  import_fs13.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
5403
5613
  } catch {
@@ -5546,6 +5756,14 @@ function startActivitySocket() {
5546
5756
  socket.on("end", () => {
5547
5757
  try {
5548
5758
  const data = JSON.parse(Buffer.concat(chunks).toString());
5759
+ if (data.status === "test_pass") {
5760
+ sessionHistory.recordTestPass(data.ts);
5761
+ return;
5762
+ }
5763
+ if (data.status === "test_fail") {
5764
+ sessionHistory.recordTestFail(data.ts);
5765
+ return;
5766
+ }
5549
5767
  if (data.status === "pending") {
5550
5768
  broadcast("activity", {
5551
5769
  id: data.id,
@@ -5555,6 +5773,24 @@ function startActivitySocket() {
5555
5773
  status: "pending"
5556
5774
  });
5557
5775
  } else {
5776
+ if (data.status === "allow") {
5777
+ sessionCounters.incrementAllowed();
5778
+ if (data.observeWouldBlock) sessionCounters.incrementWouldBlock();
5779
+ if (WRITE_TOOL_NAMES.has(data.tool.toLowerCase().replace(/[^a-z_]/g, "_"))) {
5780
+ sessionHistory.recordEdit(data.ts);
5781
+ }
5782
+ } else if (data.status === "block") {
5783
+ sessionCounters.incrementBlocked();
5784
+ sessionCounters.recordBlockedTool(data.tool);
5785
+ if (data.ruleHit) sessionCounters.recordRuleHit(data.ruleHit);
5786
+ } else if (data.status === "dlp") {
5787
+ sessionCounters.incrementBlocked();
5788
+ sessionCounters.incrementDlpHits();
5789
+ sessionCounters.recordBlockedTool(data.tool);
5790
+ } else if (data.status === "taint") {
5791
+ sessionCounters.incrementBlocked();
5792
+ sessionCounters.recordBlockedTool(data.tool);
5793
+ }
5558
5794
  broadcast("activity-result", {
5559
5795
  id: data.id,
5560
5796
  status: data.status,
@@ -5575,27 +5811,29 @@ function startActivitySocket() {
5575
5811
  }
5576
5812
  });
5577
5813
  }
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;
5814
+ var import_net2, import_fs13, import_path15, 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
5815
  var init_state2 = __esm({
5580
5816
  "src/daemon/state.ts"() {
5581
5817
  "use strict";
5582
5818
  import_net2 = __toESM(require("net"));
5583
5819
  import_fs13 = __toESM(require("fs"));
5584
- import_path16 = __toESM(require("path"));
5585
- import_os12 = __toESM(require("os"));
5820
+ import_path15 = __toESM(require("path"));
5821
+ import_os11 = __toESM(require("os"));
5586
5822
  import_child_process3 = require("child_process");
5587
5823
  import_crypto5 = require("crypto");
5588
5824
  init_daemon();
5589
5825
  init_suggestion_tracker();
5590
5826
  init_taint_store();
5591
- homeDir = import_os12.default.homedir();
5592
- DAEMON_PID_FILE = import_path16.default.join(homeDir, ".node9", "daemon.pid");
5593
- DECISIONS_FILE = import_path16.default.join(homeDir, ".node9", "decisions.json");
5594
- AUDIT_LOG_FILE = import_path16.default.join(homeDir, ".node9", "audit.log");
5595
- TRUST_FILE2 = import_path16.default.join(homeDir, ".node9", "trust.json");
5596
- GLOBAL_CONFIG_FILE = import_path16.default.join(homeDir, ".node9", "config.json");
5597
- CREDENTIALS_FILE = import_path16.default.join(homeDir, ".node9", "credentials.json");
5598
- INSIGHT_COUNTS_FILE = import_path16.default.join(homeDir, ".node9", "insight-counts.json");
5827
+ init_session_counters();
5828
+ init_session_history();
5829
+ homeDir = import_os11.default.homedir();
5830
+ DAEMON_PID_FILE = import_path15.default.join(homeDir, ".node9", "daemon.pid");
5831
+ DECISIONS_FILE = import_path15.default.join(homeDir, ".node9", "decisions.json");
5832
+ AUDIT_LOG_FILE = import_path15.default.join(homeDir, ".node9", "audit.log");
5833
+ TRUST_FILE2 = import_path15.default.join(homeDir, ".node9", "trust.json");
5834
+ GLOBAL_CONFIG_FILE = import_path15.default.join(homeDir, ".node9", "config.json");
5835
+ CREDENTIALS_FILE = import_path15.default.join(homeDir, ".node9", "credentials.json");
5836
+ INSIGHT_COUNTS_FILE = import_path15.default.join(homeDir, ".node9", "insight-counts.json");
5599
5837
  pending = /* @__PURE__ */ new Map();
5600
5838
  sseClients = /* @__PURE__ */ new Set();
5601
5839
  suggestionTracker = new SuggestionTracker(3);
@@ -5613,10 +5851,21 @@ var init_state2 = __esm({
5613
5851
  "2h": 2 * 60 * 6e4
5614
5852
  };
5615
5853
  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");
5854
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path15.default.join(import_os11.default.tmpdir(), "node9-activity.sock");
5617
5855
  ACTIVITY_RING_SIZE = 100;
5618
5856
  activityRing = [];
5619
5857
  SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
5858
+ WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
5859
+ "write",
5860
+ "write_file",
5861
+ "create_file",
5862
+ "edit",
5863
+ "multiedit",
5864
+ "str_replace_based_edit_tool",
5865
+ "replace",
5866
+ "notebook_edit",
5867
+ "notebookedit"
5868
+ ]);
5620
5869
  }
5621
5870
  });
5622
5871
 
@@ -5644,7 +5893,7 @@ function patchConfig(configPath, patch) {
5644
5893
  ignored.push(patch.toolName);
5645
5894
  }
5646
5895
  }
5647
- const dir = import_path17.default.dirname(configPath);
5896
+ const dir = import_path16.default.dirname(configPath);
5648
5897
  import_fs14.default.mkdirSync(dir, { recursive: true });
5649
5898
  const tmp = configPath + ".node9-tmp";
5650
5899
  try {
@@ -5666,14 +5915,14 @@ function patchConfig(configPath, patch) {
5666
5915
  throw err;
5667
5916
  }
5668
5917
  }
5669
- var import_fs14, import_path17, import_os13, GLOBAL_CONFIG_PATH;
5918
+ var import_fs14, import_path16, import_os12, GLOBAL_CONFIG_PATH;
5670
5919
  var init_patch = __esm({
5671
5920
  "src/config/patch.ts"() {
5672
5921
  "use strict";
5673
5922
  import_fs14 = __toESM(require("fs"));
5674
- 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");
5923
+ import_path16 = __toESM(require("path"));
5924
+ import_os12 = __toESM(require("os"));
5925
+ GLOBAL_CONFIG_PATH = import_path16.default.join(import_os12.default.homedir(), ".node9", "config.json");
5677
5926
  }
5678
5927
  });
5679
5928
 
@@ -5740,6 +5989,8 @@ data: ${JSON.stringify({
5740
5989
  toolName: e.toolName,
5741
5990
  args: e.args,
5742
5991
  riskMetadata: e.riskMetadata,
5992
+ ...e.recoveryCommand && { recoveryCommand: e.recoveryCommand },
5993
+ ...e.viewOnly && { viewOnly: true },
5743
5994
  slackDelegated: e.slackDelegated,
5744
5995
  timestamp: e.timestamp,
5745
5996
  agent: e.agent,
@@ -5805,6 +6056,9 @@ data: ${JSON.stringify(item.data)}
5805
6056
  agent,
5806
6057
  mcpServer,
5807
6058
  riskMetadata,
6059
+ recoveryCommand,
6060
+ skipBackgroundAuth = false,
6061
+ viewOnly = false,
5808
6062
  fromCLI = false,
5809
6063
  activityId,
5810
6064
  cwd
@@ -5815,6 +6069,8 @@ data: ${JSON.stringify(item.data)}
5815
6069
  toolName,
5816
6070
  args,
5817
6071
  riskMetadata: riskMetadata ?? void 0,
6072
+ ...typeof recoveryCommand === "string" && recoveryCommand && { recoveryCommand },
6073
+ ...viewOnly && { viewOnly: true },
5818
6074
  agent: typeof agent === "string" ? agent : void 0,
5819
6075
  mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
5820
6076
  slackDelegated: !!slackDelegated,
@@ -5849,7 +6105,7 @@ data: ${JSON.stringify(item.data)}
5849
6105
  status: "pending"
5850
6106
  });
5851
6107
  }
5852
- const projectCwd = typeof cwd === "string" && import_path18.default.isAbsolute(cwd) ? cwd : void 0;
6108
+ const projectCwd = typeof cwd === "string" && import_path17.default.isAbsolute(cwd) ? cwd : void 0;
5853
6109
  const projectConfig = getConfig(projectCwd);
5854
6110
  const browserEnabled = projectConfig.settings.approvers?.browser !== false;
5855
6111
  const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
@@ -5859,6 +6115,8 @@ data: ${JSON.stringify(item.data)}
5859
6115
  toolName,
5860
6116
  args,
5861
6117
  riskMetadata: entry.riskMetadata,
6118
+ ...entry.recoveryCommand && { recoveryCommand: entry.recoveryCommand },
6119
+ ...entry.viewOnly && { viewOnly: true },
5862
6120
  slackDelegated: entry.slackDelegated,
5863
6121
  agent: entry.agent,
5864
6122
  mcpServer: entry.mcpServer,
@@ -5875,7 +6133,7 @@ data: ${JSON.stringify(item.data)}
5875
6133
  }
5876
6134
  res.writeHead(200, { "Content-Type": "application/json" });
5877
6135
  res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
5878
- if (slackDelegated) return;
6136
+ if (slackDelegated || skipBackgroundAuth) return;
5879
6137
  authorizeHeadless(
5880
6138
  toolName,
5881
6139
  args,
@@ -6014,7 +6272,7 @@ data: ${JSON.stringify(item.data)}
6014
6272
  saveInsightCounts();
6015
6273
  suggestionTracker.resetTool(entry.toolName);
6016
6274
  }
6017
- const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
6275
+ const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native", "terminal-redirect"]);
6018
6276
  if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
6019
6277
  if (entry.waiter) {
6020
6278
  entry.waiter(resolvedDecision, reason);
@@ -6043,6 +6301,41 @@ data: ${JSON.stringify(item.data)}
6043
6301
  return res.end(JSON.stringify({ error: "internal" }));
6044
6302
  }
6045
6303
  }
6304
+ if (req.method === "GET" && pathname === "/status") {
6305
+ try {
6306
+ const s = getGlobalSettings();
6307
+ const counters = sessionCounters.get();
6308
+ const mode = s.mode ?? "standard";
6309
+ const status = {
6310
+ mode,
6311
+ session: {
6312
+ allowed: counters.allowed,
6313
+ blocked: counters.blocked,
6314
+ dlpHits: counters.dlpHits,
6315
+ wouldBlock: counters.wouldBlock
6316
+ },
6317
+ taintedCount: taintStore.list().length,
6318
+ lastRuleHit: counters.lastRuleHit,
6319
+ lastBlockedTool: counters.lastBlockedTool
6320
+ };
6321
+ res.writeHead(200, { "Content-Type": "application/json" });
6322
+ return res.end(JSON.stringify(status));
6323
+ } catch (err) {
6324
+ console.error(import_chalk2.default.red("[node9 daemon] GET /status failed:"), err);
6325
+ res.writeHead(500, { "Content-Type": "application/json" });
6326
+ return res.end(JSON.stringify({ error: "internal" }));
6327
+ }
6328
+ }
6329
+ if (req.method === "GET" && pathname === "/state/check") {
6330
+ const predicatesParam = reqUrl.searchParams.get("predicates") ?? "";
6331
+ const predicates = predicatesParam.split(",").filter(Boolean);
6332
+ const results = {};
6333
+ for (const p of predicates) {
6334
+ results[p] = sessionHistory.checkPredicate(p);
6335
+ }
6336
+ res.writeHead(200, { "Content-Type": "application/json" });
6337
+ return res.end(JSON.stringify(results));
6338
+ }
6046
6339
  if (req.method === "POST" && pathname === "/settings") {
6047
6340
  if (!validToken(req)) return res.writeHead(403).end();
6048
6341
  try {
@@ -6200,8 +6493,8 @@ data: ${JSON.stringify(item.data)}
6200
6493
  const body = await readBody(req);
6201
6494
  const data = body ? JSON.parse(body) : {};
6202
6495
  const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
6203
- const node9Dir = import_path18.default.dirname(GLOBAL_CONFIG_PATH);
6204
- if (!import_path18.default.resolve(configPath).startsWith(node9Dir + import_path18.default.sep)) {
6496
+ const node9Dir = import_path17.default.dirname(GLOBAL_CONFIG_PATH);
6497
+ if (!import_path17.default.resolve(configPath).startsWith(node9Dir + import_path17.default.sep)) {
6205
6498
  res.writeHead(400, { "Content-Type": "application/json" });
6206
6499
  return res.end(
6207
6500
  JSON.stringify({ error: "configPath must be within the node9 config directory" })
@@ -6378,13 +6671,13 @@ data: ${JSON.stringify(item.data)}
6378
6671
  }
6379
6672
  startActivitySocket();
6380
6673
  }
6381
- var import_http, import_fs15, import_path18, import_crypto6, import_child_process4, import_chalk2;
6674
+ var import_http, import_fs15, import_path17, import_crypto6, import_child_process4, import_chalk2;
6382
6675
  var init_server = __esm({
6383
6676
  "src/daemon/server.ts"() {
6384
6677
  "use strict";
6385
6678
  import_http = __toESM(require("http"));
6386
6679
  import_fs15 = __toESM(require("fs"));
6387
- import_path18 = __toESM(require("path"));
6680
+ import_path17 = __toESM(require("path"));
6388
6681
  import_crypto6 = require("crypto");
6389
6682
  import_child_process4 = require("child_process");
6390
6683
  import_chalk2 = __toESM(require("chalk"));
@@ -6528,9 +6821,10 @@ async function ensureDaemon() {
6528
6821
  }
6529
6822
  function postDecisionHttp(id, decision, csrfToken, port, opts) {
6530
6823
  return new Promise((resolve, reject) => {
6531
- const bodyObj = { decision, source: "terminal" };
6824
+ const bodyObj = { decision, source: opts?.source ?? "terminal" };
6532
6825
  if (opts?.persist) bodyObj.persist = true;
6533
6826
  if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
6827
+ if (opts?.reason) bodyObj.reason = opts.reason;
6534
6828
  const body = JSON.stringify(bodyObj);
6535
6829
  const req = import_http2.default.request(
6536
6830
  {
@@ -6555,6 +6849,9 @@ function postDecisionHttp(id, decision, csrfToken, port, opts) {
6555
6849
  });
6556
6850
  }
6557
6851
  function buildCardLines(req, localCount = 0) {
6852
+ if (req.recoveryCommand) {
6853
+ return buildRecoveryCardLines(req);
6854
+ }
6558
6855
  const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
6559
6856
  const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
6560
6857
  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`;
@@ -6582,6 +6879,31 @@ function buildCardLines(req, localCount = 0) {
6582
6879
  );
6583
6880
  return lines;
6584
6881
  }
6882
+ function buildRecoveryCardLines(req) {
6883
+ const argsObj = req.args;
6884
+ const command = typeof argsObj?.command === "string" ? argsObj.command : JSON.stringify(req.args ?? {}).replace(/\s+/g, " ").slice(0, 60);
6885
+ const ruleName = req.riskMetadata?.ruleName?.replace(/^Smart Rule:\s*/i, "") ?? "policy rule";
6886
+ const recoveryCommand = req.recoveryCommand;
6887
+ const interactiveLines = req.viewOnly ? [` ${GRAY}\u2192 Awaiting decision from interactive terminal...${RESET}`] : [
6888
+ ` ${BOLD}${GREEN}[1]${RESET} Allow anyway ${GRAY}(override policy)${RESET}`,
6889
+ ` ${BOLD}${YELLOW}[2]${RESET} Redirect AI: "Run '${recoveryCommand}' first, then retry"`,
6890
+ ` ${BOLD}${RED}[3]${RESET} Deny & stop ${GRAY}(hard block)${RESET}`,
6891
+ ``,
6892
+ ` ${GRAY}[Timeout: auto-deny]${RESET}`,
6893
+ ` Select [1-3]: `
6894
+ ];
6895
+ return [
6896
+ ``,
6897
+ `${BOLD}${CYAN}${DIVIDER}${RESET}`,
6898
+ `\u{1F6E1}\uFE0F ${BOLD}NODE9 STATE GUARD:${RESET} '${BOLD}${command}${RESET}'`,
6899
+ `${YELLOW}\u26A0\uFE0F Rule: ${ruleName}${RESET}`,
6900
+ `${CYAN}${DIVIDER}${RESET}`,
6901
+ ...!req.viewOnly ? [`${BOLD}What would you like to do?${RESET}`, ``] : [],
6902
+ ...interactiveLines,
6903
+ `${CYAN}${DIVIDER}${RESET}`,
6904
+ ``
6905
+ ];
6906
+ }
6585
6907
  async function startTail(options = {}) {
6586
6908
  const port = await ensureDaemon();
6587
6909
  if (options.clear) {
@@ -6680,14 +7002,14 @@ async function startTail(options = {}) {
6680
7002
  localAllowCounts.get(req2.toolName) ?? 0
6681
7003
  )
6682
7004
  );
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");
7005
+ 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") : action === "redirect" ? import_chalk16.default.yellow("\u21A9 REDIRECT AI") : import_chalk16.default.red("\u2717 DENIED");
6684
7006
  stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
6685
7007
  for (const line of stampedLines) process.stdout.write(line + "\n");
6686
7008
  process.stdout.write(SHOW_CURSOR);
6687
7009
  cardLineCount = 0;
6688
7010
  if (action === "allow" || action === "always-allow" || action === "trust") {
6689
7011
  localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
6690
- } else if (action === "deny") {
7012
+ } else if (action === "deny" || action === "redirect") {
6691
7013
  localAllowCounts.delete(req2.toolName);
6692
7014
  }
6693
7015
  let httpDecision;
@@ -6698,13 +7020,18 @@ async function startTail(options = {}) {
6698
7020
  } else if (action === "trust") {
6699
7021
  httpDecision = "trust";
6700
7022
  httpOpts = { trustDuration: "30m" };
7023
+ } else if (action === "redirect") {
7024
+ httpDecision = "deny";
7025
+ const recoveryCommand = req2.recoveryCommand ?? "the required pre-condition";
7026
+ 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}\`.`;
7027
+ httpOpts = { reason: redirectReason, source: "terminal-redirect" };
6701
7028
  } else {
6702
7029
  httpDecision = action;
6703
7030
  }
6704
7031
  postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
6705
7032
  try {
6706
7033
  import_fs24.default.appendFileSync(
6707
- import_path26.default.join(import_os21.default.homedir(), ".node9", "hook-debug.log"),
7034
+ import_path25.default.join(import_os20.default.homedir(), ".node9", "hook-debug.log"),
6708
7035
  `[tail] POST /decision failed: ${String(err)}
6709
7036
  `
6710
7037
  );
@@ -6736,17 +7063,34 @@ async function startTail(options = {}) {
6736
7063
  cardActive = false;
6737
7064
  showNextCard();
6738
7065
  };
7066
+ if (req2.viewOnly) {
7067
+ process.stdin.resume();
7068
+ onKeypress = () => {
7069
+ };
7070
+ process.stdin.on("keypress", onKeypress);
7071
+ return;
7072
+ }
6739
7073
  process.stdin.resume();
6740
7074
  onKeypress = (_str, key) => {
6741
7075
  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");
7076
+ if (req2.recoveryCommand) {
7077
+ if (name === "1") {
7078
+ settle("allow");
7079
+ } else if (name === "2") {
7080
+ settle("redirect");
7081
+ } else if (name === "3" || key?.ctrl && name === "c") {
7082
+ settle("deny");
7083
+ }
7084
+ } else {
7085
+ if (name === "y" || name === "return") {
7086
+ settle("allow");
7087
+ } else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
7088
+ settle("deny");
7089
+ } else if (name === "a") {
7090
+ settle("always-allow");
7091
+ } else if (name === "t") {
7092
+ settle("trust");
7093
+ }
6750
7094
  }
6751
7095
  };
6752
7096
  process.stdin.on("keypress", onKeypress);
@@ -6916,21 +7260,21 @@ async function startTail(options = {}) {
6916
7260
  process.exit(1);
6917
7261
  });
6918
7262
  }
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;
7263
+ var import_http2, import_chalk16, import_fs24, import_os20, import_path25, import_readline3, import_child_process13, PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
6920
7264
  var init_tail = __esm({
6921
7265
  "src/tui/tail.ts"() {
6922
7266
  "use strict";
6923
7267
  import_http2 = __toESM(require("http"));
6924
7268
  import_chalk16 = __toESM(require("chalk"));
6925
7269
  import_fs24 = __toESM(require("fs"));
6926
- import_os21 = __toESM(require("os"));
6927
- import_path26 = __toESM(require("path"));
7270
+ import_os20 = __toESM(require("os"));
7271
+ import_path25 = __toESM(require("path"));
6928
7272
  import_readline3 = __toESM(require("readline"));
6929
7273
  import_child_process13 = require("child_process");
6930
7274
  init_daemon2();
6931
7275
  init_daemon();
6932
7276
  init_core();
6933
- PID_FILE = import_path26.default.join(import_os21.default.homedir(), ".node9", "daemon.pid");
7277
+ PID_FILE = import_path25.default.join(import_os20.default.homedir(), ".node9", "daemon.pid");
6934
7278
  ICONS = {
6935
7279
  bash: "\u{1F4BB}",
6936
7280
  shell: "\u{1F4BB}",
@@ -6958,6 +7302,326 @@ var init_tail = __esm({
6958
7302
  HIDE_CURSOR = "\x1B[?25l";
6959
7303
  SHOW_CURSOR = "\x1B[?25h";
6960
7304
  ERASE_DOWN = "\x1B[J";
7305
+ DIVIDER = "\u2500".repeat(60);
7306
+ }
7307
+ });
7308
+
7309
+ // src/cli/hud.ts
7310
+ var hud_exports = {};
7311
+ __export(hud_exports, {
7312
+ countConfigs: () => countConfigs,
7313
+ main: () => main,
7314
+ renderEnvironmentLine: () => renderEnvironmentLine
7315
+ });
7316
+ async function readStdin() {
7317
+ const chunks = [];
7318
+ for await (const chunk of process.stdin) {
7319
+ chunks.push(chunk);
7320
+ }
7321
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
7322
+ if (!raw) return {};
7323
+ try {
7324
+ return JSON.parse(raw);
7325
+ } catch {
7326
+ return {};
7327
+ }
7328
+ }
7329
+ function queryDaemon() {
7330
+ return new Promise((resolve) => {
7331
+ const timeout = setTimeout(() => resolve(null), 50);
7332
+ try {
7333
+ const req = import_http3.default.get(
7334
+ `http://${DAEMON_HOST}:${DAEMON_PORT}/status`,
7335
+ { timeout: 50 },
7336
+ (res) => {
7337
+ const chunks = [];
7338
+ res.on("data", (c) => chunks.push(c));
7339
+ res.on("end", () => {
7340
+ clearTimeout(timeout);
7341
+ try {
7342
+ resolve(JSON.parse(Buffer.concat(chunks).toString()));
7343
+ } catch {
7344
+ resolve(null);
7345
+ }
7346
+ });
7347
+ }
7348
+ );
7349
+ req.on("error", () => {
7350
+ clearTimeout(timeout);
7351
+ resolve(null);
7352
+ });
7353
+ req.on("timeout", () => {
7354
+ clearTimeout(timeout);
7355
+ req.destroy();
7356
+ resolve(null);
7357
+ });
7358
+ } catch {
7359
+ clearTimeout(timeout);
7360
+ resolve(null);
7361
+ }
7362
+ });
7363
+ }
7364
+ function dim(s) {
7365
+ return `${DIM}${s}${RESET2}`;
7366
+ }
7367
+ function bold(s) {
7368
+ return `${BOLD2}${s}${RESET2}`;
7369
+ }
7370
+ function color(c, s) {
7371
+ return `${c}${s}${RESET2}`;
7372
+ }
7373
+ function progressBar(pct, warnAt = 70, critAt = 85) {
7374
+ const filled = Math.round(Math.min(pct, 100) / 100 * BAR_WIDTH);
7375
+ const bar = BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
7376
+ const c = pct >= critAt ? RED2 : pct >= warnAt ? YELLOW2 : GREEN2;
7377
+ return `${c}${bar}${RESET2}`;
7378
+ }
7379
+ function formatTimeLeft(resetsAt) {
7380
+ if (!resetsAt) return "";
7381
+ const ms = new Date(resetsAt).getTime() - Date.now();
7382
+ if (ms <= 0) return "";
7383
+ const totalMin = Math.ceil(ms / 6e4);
7384
+ const h = Math.floor(totalMin / 60);
7385
+ const m = totalMin % 60;
7386
+ if (h > 0) return ` (${h}h ${m}m left)`;
7387
+ return ` (${m}m left)`;
7388
+ }
7389
+ function safeReadJson(filePath) {
7390
+ if (!import_fs25.default.existsSync(filePath)) return null;
7391
+ try {
7392
+ return JSON.parse(import_fs25.default.readFileSync(filePath, "utf-8"));
7393
+ } catch {
7394
+ return null;
7395
+ }
7396
+ }
7397
+ function getMcpServerNames(filePath) {
7398
+ const cfg = safeReadJson(filePath);
7399
+ if (!cfg || typeof cfg.mcpServers !== "object" || cfg.mcpServers === null) return /* @__PURE__ */ new Set();
7400
+ return new Set(Object.keys(cfg.mcpServers));
7401
+ }
7402
+ function getDisabledMcpServers(filePath, key) {
7403
+ const cfg = safeReadJson(filePath);
7404
+ if (!cfg || !Array.isArray(cfg[key])) return /* @__PURE__ */ new Set();
7405
+ return new Set(cfg[key].filter((s) => typeof s === "string"));
7406
+ }
7407
+ function countHooksInFile(filePath) {
7408
+ const cfg = safeReadJson(filePath);
7409
+ if (!cfg || typeof cfg.hooks !== "object" || cfg.hooks === null) return 0;
7410
+ return Object.keys(cfg.hooks).length;
7411
+ }
7412
+ function countRulesInDir(rulesDir) {
7413
+ if (!import_fs25.default.existsSync(rulesDir)) return 0;
7414
+ let count = 0;
7415
+ try {
7416
+ for (const entry of import_fs25.default.readdirSync(rulesDir, { withFileTypes: true })) {
7417
+ if (entry.isDirectory()) {
7418
+ count += countRulesInDir(import_path26.default.join(rulesDir, entry.name));
7419
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
7420
+ count++;
7421
+ }
7422
+ }
7423
+ } catch {
7424
+ }
7425
+ return count;
7426
+ }
7427
+ function isSamePath(a, b) {
7428
+ try {
7429
+ return import_path26.default.resolve(a) === import_path26.default.resolve(b);
7430
+ } catch {
7431
+ return false;
7432
+ }
7433
+ }
7434
+ function countConfigs(cwd) {
7435
+ const homeDir2 = import_os21.default.homedir();
7436
+ const claudeDir = import_path26.default.join(homeDir2, ".claude");
7437
+ let claudeMdCount = 0;
7438
+ let rulesCount = 0;
7439
+ let hooksCount = 0;
7440
+ const userMcpServers = /* @__PURE__ */ new Set();
7441
+ const projectMcpServers = /* @__PURE__ */ new Set();
7442
+ if (import_fs25.default.existsSync(import_path26.default.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7443
+ rulesCount += countRulesInDir(import_path26.default.join(claudeDir, "rules"));
7444
+ const userSettings = import_path26.default.join(claudeDir, "settings.json");
7445
+ for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
7446
+ hooksCount += countHooksInFile(userSettings);
7447
+ const userClaudeJson = import_path26.default.join(homeDir2, ".claude.json");
7448
+ for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
7449
+ for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
7450
+ userMcpServers.delete(name);
7451
+ }
7452
+ if (cwd) {
7453
+ if (import_fs25.default.existsSync(import_path26.default.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7454
+ if (import_fs25.default.existsSync(import_path26.default.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7455
+ const projectClaudeDir = import_path26.default.join(cwd, ".claude");
7456
+ const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
7457
+ if (!overlapsUserScope) {
7458
+ if (import_fs25.default.existsSync(import_path26.default.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7459
+ rulesCount += countRulesInDir(import_path26.default.join(projectClaudeDir, "rules"));
7460
+ const projSettings = import_path26.default.join(projectClaudeDir, "settings.json");
7461
+ for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
7462
+ hooksCount += countHooksInFile(projSettings);
7463
+ }
7464
+ if (import_fs25.default.existsSync(import_path26.default.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7465
+ const localSettings = import_path26.default.join(projectClaudeDir, "settings.local.json");
7466
+ for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
7467
+ hooksCount += countHooksInFile(localSettings);
7468
+ const mcpJsonServers = getMcpServerNames(import_path26.default.join(cwd, ".mcp.json"));
7469
+ const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
7470
+ for (const name of disabledMcpJson) mcpJsonServers.delete(name);
7471
+ for (const name of mcpJsonServers) projectMcpServers.add(name);
7472
+ }
7473
+ return {
7474
+ claudeMdCount,
7475
+ rulesCount,
7476
+ mcpCount: userMcpServers.size + projectMcpServers.size,
7477
+ hooksCount
7478
+ };
7479
+ }
7480
+ function renderEnvironmentLine(counts) {
7481
+ const { claudeMdCount, rulesCount, mcpCount, hooksCount } = counts;
7482
+ if (claudeMdCount === 0 && rulesCount === 0 && mcpCount === 0 && hooksCount === 0) return null;
7483
+ const parts = [
7484
+ `${claudeMdCount} CLAUDE.md`,
7485
+ `${rulesCount} rules`,
7486
+ `${mcpCount} MCPs`,
7487
+ `${hooksCount} hooks`
7488
+ ];
7489
+ return color(DIM, parts.join(` ${dim("|")} `));
7490
+ }
7491
+ function renderOffline() {
7492
+ process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
7493
+ `);
7494
+ }
7495
+ function renderSecurityLine(status) {
7496
+ const parts = [];
7497
+ parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
7498
+ const modeColors = {
7499
+ standard: GREEN2,
7500
+ strict: RED2,
7501
+ observe: MAGENTA,
7502
+ audit: YELLOW2
7503
+ };
7504
+ const modeIcon = {
7505
+ standard: "",
7506
+ strict: "",
7507
+ observe: "\u{1F441} ",
7508
+ audit: ""
7509
+ };
7510
+ const mc = modeColors[status.mode] ?? WHITE;
7511
+ parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
7512
+ if (status.mode === "observe") {
7513
+ parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
7514
+ if (status.session.wouldBlock > 0) {
7515
+ parts.push(color(YELLOW2, `\u26A0 ${status.session.wouldBlock} would-block`));
7516
+ }
7517
+ } else {
7518
+ parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} allowed`)}`);
7519
+ if (status.session.blocked > 0) {
7520
+ parts.push(color(RED2, `\u{1F6D1} ${status.session.blocked} blocked`));
7521
+ }
7522
+ if (status.session.dlpHits > 0) {
7523
+ parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
7524
+ }
7525
+ }
7526
+ if (status.taintedCount > 0) {
7527
+ parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
7528
+ }
7529
+ if (status.lastRuleHit) {
7530
+ const ruleName = status.lastRuleHit.replace(/^Smart Rule:\s*/i, "");
7531
+ parts.push(color(CYAN2, `\u26A1 ${ruleName}`));
7532
+ }
7533
+ return parts.join(" ");
7534
+ }
7535
+ function renderContextLine(stdin) {
7536
+ const cw = stdin.context_window;
7537
+ if (!cw) return null;
7538
+ const parts = [];
7539
+ const modelName = typeof stdin.model === "string" ? stdin.model : stdin.model?.display_name ?? "";
7540
+ if (modelName) {
7541
+ parts.push(color(CYAN2, modelName));
7542
+ }
7543
+ const usedPct = cw.used_percentage ?? (cw.current_usage && cw.context_window_size ? Math.round(
7544
+ ((cw.current_usage.input_tokens ?? 0) + (cw.current_usage.output_tokens ?? 0)) / cw.context_window_size * 100
7545
+ ) : null);
7546
+ if (usedPct !== null) {
7547
+ const bar = progressBar(usedPct);
7548
+ parts.push(`${dim("\u2502")} ctx ${bar} ${usedPct}%`);
7549
+ }
7550
+ const rl = stdin.rate_limits;
7551
+ if (rl?.five_hour?.used_percentage !== void 0) {
7552
+ const pct = Math.round(rl.five_hour.used_percentage);
7553
+ const bar = progressBar(pct, 60, 80);
7554
+ const left = formatTimeLeft(rl.five_hour.resets_at);
7555
+ parts.push(`${dim("\u2502")} 5h ${bar} ${pct}%${left}`);
7556
+ }
7557
+ if (rl?.seven_day?.used_percentage !== void 0) {
7558
+ const pct = Math.round(rl.seven_day.used_percentage);
7559
+ const bar = progressBar(pct, 60, 80);
7560
+ parts.push(`${dim("\u2502")} 7d ${bar} ${pct}%`);
7561
+ }
7562
+ if (parts.length === 0) return null;
7563
+ return parts.join(" ");
7564
+ }
7565
+ async function main() {
7566
+ try {
7567
+ const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
7568
+ if (!daemonStatus2) {
7569
+ renderOffline();
7570
+ return;
7571
+ }
7572
+ process.stdout.write(renderSecurityLine(daemonStatus2) + "\n");
7573
+ const ctxLine = renderContextLine(stdin);
7574
+ if (ctxLine) {
7575
+ process.stdout.write(ctxLine + "\n");
7576
+ }
7577
+ const showEnvCounts = (() => {
7578
+ try {
7579
+ const cwd = stdin.cwd ?? process.cwd();
7580
+ for (const configPath of [
7581
+ import_path26.default.join(cwd, "node9.config.json"),
7582
+ import_path26.default.join(import_os21.default.homedir(), ".node9", "config.json")
7583
+ ]) {
7584
+ if (!import_fs25.default.existsSync(configPath)) continue;
7585
+ const cfg = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
7586
+ const hud = cfg.settings?.hud;
7587
+ if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
7588
+ }
7589
+ } catch {
7590
+ }
7591
+ return true;
7592
+ })();
7593
+ if (showEnvCounts) {
7594
+ const envLine = renderEnvironmentLine(countConfigs(stdin.cwd));
7595
+ if (envLine) {
7596
+ process.stdout.write(envLine + "\n");
7597
+ }
7598
+ }
7599
+ } catch {
7600
+ renderOffline();
7601
+ }
7602
+ }
7603
+ var import_fs25, import_path26, import_os21, import_http3, RESET2, BOLD2, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
7604
+ var init_hud = __esm({
7605
+ "src/cli/hud.ts"() {
7606
+ "use strict";
7607
+ import_fs25 = __toESM(require("fs"));
7608
+ import_path26 = __toESM(require("path"));
7609
+ import_os21 = __toESM(require("os"));
7610
+ import_http3 = __toESM(require("http"));
7611
+ init_daemon();
7612
+ RESET2 = "\x1B[0m";
7613
+ BOLD2 = "\x1B[1m";
7614
+ DIM = "\x1B[2m";
7615
+ RED2 = "\x1B[31m";
7616
+ GREEN2 = "\x1B[32m";
7617
+ YELLOW2 = "\x1B[33m";
7618
+ BLUE = "\x1B[34m";
7619
+ MAGENTA = "\x1B[35m";
7620
+ CYAN2 = "\x1B[36m";
7621
+ WHITE = "\x1B[37m";
7622
+ BAR_FILLED = "\u2588";
7623
+ BAR_EMPTY = "\u2591";
7624
+ BAR_WIDTH = 10;
6961
7625
  }
6962
7626
  });
6963
7627
 
@@ -6967,8 +7631,8 @@ init_core();
6967
7631
 
6968
7632
  // src/setup.ts
6969
7633
  var import_fs11 = __toESM(require("fs"));
6970
- var import_path14 = __toESM(require("path"));
6971
- var import_os11 = __toESM(require("os"));
7634
+ var import_path13 = __toESM(require("path"));
7635
+ var import_os10 = __toESM(require("os"));
6972
7636
  var import_chalk = __toESM(require("chalk"));
6973
7637
  var import_prompts = require("@inquirer/prompts");
6974
7638
  function printDaemonTip() {
@@ -6993,7 +7657,7 @@ function readJson(filePath) {
6993
7657
  return null;
6994
7658
  }
6995
7659
  function writeJson(filePath, data) {
6996
- const dir = import_path14.default.dirname(filePath);
7660
+ const dir = import_path13.default.dirname(filePath);
6997
7661
  if (!import_fs11.default.existsSync(dir)) import_fs11.default.mkdirSync(dir, { recursive: true });
6998
7662
  import_fs11.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
6999
7663
  }
@@ -7002,9 +7666,9 @@ function isNode9Hook(cmd) {
7002
7666
  return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
7003
7667
  }
7004
7668
  function teardownClaude() {
7005
- const homeDir2 = import_os11.default.homedir();
7006
- const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
7007
- const mcpPath = import_path14.default.join(homeDir2, ".claude.json");
7669
+ const homeDir2 = import_os10.default.homedir();
7670
+ const hooksPath = import_path13.default.join(homeDir2, ".claude", "settings.json");
7671
+ const mcpPath = import_path13.default.join(homeDir2, ".claude.json");
7008
7672
  let changed = false;
7009
7673
  const settings = readJson(hooksPath);
7010
7674
  if (settings?.hooks) {
@@ -7052,8 +7716,8 @@ function teardownClaude() {
7052
7716
  }
7053
7717
  }
7054
7718
  function teardownGemini() {
7055
- const homeDir2 = import_os11.default.homedir();
7056
- const settingsPath = import_path14.default.join(homeDir2, ".gemini", "settings.json");
7719
+ const homeDir2 = import_os10.default.homedir();
7720
+ const settingsPath = import_path13.default.join(homeDir2, ".gemini", "settings.json");
7057
7721
  const settings = readJson(settingsPath);
7058
7722
  if (!settings) {
7059
7723
  console.log(import_chalk.default.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
@@ -7091,8 +7755,8 @@ function teardownGemini() {
7091
7755
  }
7092
7756
  }
7093
7757
  function teardownCursor() {
7094
- const homeDir2 = import_os11.default.homedir();
7095
- const mcpPath = import_path14.default.join(homeDir2, ".cursor", "mcp.json");
7758
+ const homeDir2 = import_os10.default.homedir();
7759
+ const mcpPath = import_path13.default.join(homeDir2, ".cursor", "mcp.json");
7096
7760
  const mcpConfig = readJson(mcpPath);
7097
7761
  if (!mcpConfig?.mcpServers) {
7098
7762
  console.log(import_chalk.default.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
@@ -7118,9 +7782,9 @@ function teardownCursor() {
7118
7782
  }
7119
7783
  }
7120
7784
  async function setupClaude() {
7121
- const homeDir2 = import_os11.default.homedir();
7122
- const mcpPath = import_path14.default.join(homeDir2, ".claude.json");
7123
- const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
7785
+ const homeDir2 = import_os10.default.homedir();
7786
+ const mcpPath = import_path13.default.join(homeDir2, ".claude.json");
7787
+ const hooksPath = import_path13.default.join(homeDir2, ".claude", "settings.json");
7124
7788
  const claudeConfig = readJson(mcpPath) ?? {};
7125
7789
  const settings = readJson(hooksPath) ?? {};
7126
7790
  const servers = claudeConfig.mcpServers ?? {};
@@ -7194,8 +7858,8 @@ async function setupClaude() {
7194
7858
  }
7195
7859
  }
7196
7860
  async function setupGemini() {
7197
- const homeDir2 = import_os11.default.homedir();
7198
- const settingsPath = import_path14.default.join(homeDir2, ".gemini", "settings.json");
7861
+ const homeDir2 = import_os10.default.homedir();
7862
+ const settingsPath = import_path13.default.join(homeDir2, ".gemini", "settings.json");
7199
7863
  const settings = readJson(settingsPath) ?? {};
7200
7864
  const servers = settings.mcpServers ?? {};
7201
7865
  let anythingChanged = false;
@@ -7276,7 +7940,7 @@ async function setupGemini() {
7276
7940
  printDaemonTip();
7277
7941
  }
7278
7942
  }
7279
- function detectAgents(homeDir2 = import_os11.default.homedir()) {
7943
+ function detectAgents(homeDir2 = import_os10.default.homedir()) {
7280
7944
  const exists = (p) => {
7281
7945
  try {
7282
7946
  return import_fs11.default.existsSync(p);
@@ -7290,14 +7954,14 @@ function detectAgents(homeDir2 = import_os11.default.homedir()) {
7290
7954
  }
7291
7955
  };
7292
7956
  return {
7293
- claude: exists(import_path14.default.join(homeDir2, ".claude")) || exists(import_path14.default.join(homeDir2, ".claude.json")),
7294
- gemini: exists(import_path14.default.join(homeDir2, ".gemini")),
7295
- cursor: exists(import_path14.default.join(homeDir2, ".cursor"))
7957
+ claude: exists(import_path13.default.join(homeDir2, ".claude")) || exists(import_path13.default.join(homeDir2, ".claude.json")),
7958
+ gemini: exists(import_path13.default.join(homeDir2, ".gemini")),
7959
+ cursor: exists(import_path13.default.join(homeDir2, ".cursor"))
7296
7960
  };
7297
7961
  }
7298
7962
  async function setupCursor() {
7299
- const homeDir2 = import_os11.default.homedir();
7300
- const mcpPath = import_path14.default.join(homeDir2, ".cursor", "mcp.json");
7963
+ const homeDir2 = import_os10.default.homedir();
7964
+ const mcpPath = import_path13.default.join(homeDir2, ".cursor", "mcp.json");
7301
7965
  const mcpConfig = readJson(mcpPath) ?? {};
7302
7966
  const servers = mcpConfig.mcpServers ?? {};
7303
7967
  let anythingChanged = false;
@@ -7350,11 +8014,57 @@ async function setupCursor() {
7350
8014
  printDaemonTip();
7351
8015
  }
7352
8016
  }
8017
+ function setupHud() {
8018
+ const homeDir2 = import_os10.default.homedir();
8019
+ const hooksPath = import_path13.default.join(homeDir2, ".claude", "settings.json");
8020
+ const settings = readJson(hooksPath) ?? {};
8021
+ const hudCommand = fullPathCommand("hud");
8022
+ const statusLineObj = { type: "command", command: hudCommand };
8023
+ const existing = settings.statusLine;
8024
+ const existingCommand = typeof existing === "object" ? existing?.command : existing;
8025
+ if (existingCommand === hudCommand) {
8026
+ console.log(import_chalk.default.blue("\u2139\uFE0F node9 HUD is already configured in ~/.claude/settings.json"));
8027
+ console.log(import_chalk.default.gray(" Restart Claude Code to activate."));
8028
+ return;
8029
+ }
8030
+ if (existing && existingCommand !== hudCommand) {
8031
+ console.log(
8032
+ import_chalk.default.yellow(
8033
+ ` \u26A0\uFE0F statusLine is already set to: "${existingCommand}"
8034
+ Overwriting with node9 HUD.`
8035
+ )
8036
+ );
8037
+ }
8038
+ settings.statusLine = statusLineObj;
8039
+ writeJson(hooksPath, settings);
8040
+ console.log(import_chalk.default.green.bold("\u2705 node9 HUD added to Claude Code statusline"));
8041
+ console.log(import_chalk.default.gray(" Settings: ~/.claude/settings.json"));
8042
+ console.log(import_chalk.default.gray(" Restart Claude Code to activate."));
8043
+ }
8044
+ function teardownHud() {
8045
+ const homeDir2 = import_os10.default.homedir();
8046
+ const hooksPath = import_path13.default.join(homeDir2, ".claude", "settings.json");
8047
+ const settings = readJson(hooksPath);
8048
+ if (!settings) {
8049
+ console.log(import_chalk.default.blue(" \u2139\uFE0F ~/.claude/settings.json not found \u2014 nothing to remove"));
8050
+ return;
8051
+ }
8052
+ const existing = settings.statusLine;
8053
+ const existingCommand = typeof existing === "object" ? existing?.command : existing;
8054
+ if (!existingCommand || !String(existingCommand).includes("node9")) {
8055
+ console.log(import_chalk.default.blue(" \u2139\uFE0F node9 HUD not found in ~/.claude/settings.json"));
8056
+ return;
8057
+ }
8058
+ delete settings.statusLine;
8059
+ writeJson(hooksPath, settings);
8060
+ console.log(import_chalk.default.green(" \u2705 node9 HUD removed from ~/.claude/settings.json"));
8061
+ console.log(import_chalk.default.gray(" Restart Claude Code for changes to take effect."));
8062
+ }
7353
8063
 
7354
8064
  // src/cli.ts
7355
8065
  init_daemon2();
7356
8066
  var import_chalk17 = __toESM(require("chalk"));
7357
- var import_fs25 = __toESM(require("fs"));
8067
+ var import_fs26 = __toESM(require("fs"));
7358
8068
  var import_path27 = __toESM(require("path"));
7359
8069
  var import_os22 = __toESM(require("os"));
7360
8070
  var import_prompts3 = require("@inquirer/prompts");
@@ -7387,7 +8097,7 @@ var import_execa2 = require("execa");
7387
8097
  init_orchestrator();
7388
8098
 
7389
8099
  // src/policy/negotiation.ts
7390
- function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason) {
8100
+ function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason, recoveryCommand) {
7391
8101
  if (isHumanDecision) {
7392
8102
  return `NODE9: The human user rejected this action.
7393
8103
  REASON: ${humanReason || "No specific reason provided."}
@@ -7443,10 +8153,11 @@ INSTRUCTION: Inform the user this action is pending approval. Wait for them to a
7443
8153
  INSTRUCTION: Do NOT use "${rule}". Find a read-only or non-destructive alternative.
7444
8154
  Do NOT attempt to bypass this rule.`;
7445
8155
  }
8156
+ const recovery = recoveryCommand ? `
8157
+ REQUIRED ACTION: Run \`${recoveryCommand}\` first, then retry your original command.` : "\n- Pivot to a non-destructive or read-only alternative.";
7446
8158
  return `NODE9: Action blocked by security policy [${blockedByLabel}].
7447
8159
  INSTRUCTIONS:
7448
- - Do NOT retry this exact command or attempt to bypass the rule.
7449
- - Pivot to a non-destructive or read-only alternative.
8160
+ - Do NOT retry this exact command or attempt to bypass the rule.${recovery}
7450
8161
  - Inform the user which security rule was triggered and ask how to proceed.`;
7451
8162
  }
7452
8163
 
@@ -7578,8 +8289,8 @@ async function autoStartDaemonAndWait() {
7578
8289
  // src/cli/commands/check.ts
7579
8290
  var import_chalk5 = __toESM(require("chalk"));
7580
8291
  var import_fs18 = __toESM(require("fs"));
7581
- var import_path20 = __toESM(require("path"));
7582
- var import_os15 = __toESM(require("os"));
8292
+ var import_path19 = __toESM(require("path"));
8293
+ var import_os14 = __toESM(require("os"));
7583
8294
  init_orchestrator();
7584
8295
  init_daemon();
7585
8296
  init_config();
@@ -7589,10 +8300,10 @@ init_policy();
7589
8300
  var import_child_process8 = require("child_process");
7590
8301
  var import_crypto7 = __toESM(require("crypto"));
7591
8302
  var import_fs17 = __toESM(require("fs"));
7592
- 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");
8303
+ var import_path18 = __toESM(require("path"));
8304
+ var import_os13 = __toESM(require("os"));
8305
+ var SNAPSHOT_STACK_PATH = import_path18.default.join(import_os13.default.homedir(), ".node9", "snapshots.json");
8306
+ var UNDO_LATEST_PATH = import_path18.default.join(import_os13.default.homedir(), ".node9", "undo_latest.txt");
7596
8307
  var MAX_SNAPSHOTS = 10;
7597
8308
  var GIT_TIMEOUT = 15e3;
7598
8309
  function readStack() {
@@ -7604,7 +8315,7 @@ function readStack() {
7604
8315
  return [];
7605
8316
  }
7606
8317
  function writeStack(stack) {
7607
- const dir = import_path19.default.dirname(SNAPSHOT_STACK_PATH);
8318
+ const dir = import_path18.default.dirname(SNAPSHOT_STACK_PATH);
7608
8319
  if (!import_fs17.default.existsSync(dir)) import_fs17.default.mkdirSync(dir, { recursive: true });
7609
8320
  import_fs17.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
7610
8321
  }
@@ -7632,14 +8343,14 @@ function normalizeCwdForHash(cwd) {
7632
8343
  }
7633
8344
  function getShadowRepoDir(cwd) {
7634
8345
  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);
8346
+ return import_path18.default.join(import_os13.default.homedir(), ".node9", "snapshots", hash);
7636
8347
  }
7637
8348
  function cleanOrphanedIndexFiles(shadowDir) {
7638
8349
  try {
7639
8350
  const cutoff = Date.now() - 6e4;
7640
8351
  for (const f of import_fs17.default.readdirSync(shadowDir)) {
7641
8352
  if (f.startsWith("index_")) {
7642
- const fp = import_path19.default.join(shadowDir, f);
8353
+ const fp = import_path18.default.join(shadowDir, f);
7643
8354
  try {
7644
8355
  if (import_fs17.default.statSync(fp).mtimeMs < cutoff) import_fs17.default.unlinkSync(fp);
7645
8356
  } catch {
@@ -7653,7 +8364,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
7653
8364
  const hardcoded = [".git", ".node9"];
7654
8365
  const lines = [...hardcoded, ...ignorePaths].join("\n");
7655
8366
  try {
7656
- import_fs17.default.writeFileSync(import_path19.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
8367
+ import_fs17.default.writeFileSync(import_path18.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
7657
8368
  } catch {
7658
8369
  }
7659
8370
  }
@@ -7666,7 +8377,7 @@ function ensureShadowRepo(shadowDir, cwd) {
7666
8377
  timeout: 3e3
7667
8378
  });
7668
8379
  if (check.status === 0) {
7669
- const ptPath = import_path19.default.join(shadowDir, "project-path.txt");
8380
+ const ptPath = import_path18.default.join(shadowDir, "project-path.txt");
7670
8381
  try {
7671
8382
  const stored = import_fs17.default.readFileSync(ptPath, "utf8").trim();
7672
8383
  if (stored === normalizedCwd) return true;
@@ -7693,7 +8404,7 @@ function ensureShadowRepo(shadowDir, cwd) {
7693
8404
  console.error("[Node9] git init --bare failed:", init.stderr?.toString());
7694
8405
  return false;
7695
8406
  }
7696
- const configFile = import_path19.default.join(shadowDir, "config");
8407
+ const configFile = import_path18.default.join(shadowDir, "config");
7697
8408
  (0, import_child_process8.spawnSync)("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
7698
8409
  timeout: 3e3
7699
8410
  });
@@ -7701,7 +8412,7 @@ function ensureShadowRepo(shadowDir, cwd) {
7701
8412
  timeout: 3e3
7702
8413
  });
7703
8414
  try {
7704
- import_fs17.default.writeFileSync(import_path19.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
8415
+ import_fs17.default.writeFileSync(import_path18.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
7705
8416
  } catch {
7706
8417
  }
7707
8418
  return true;
@@ -7724,7 +8435,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
7724
8435
  const shadowDir = getShadowRepoDir(cwd);
7725
8436
  if (!ensureShadowRepo(shadowDir, cwd)) return null;
7726
8437
  writeShadowExcludes(shadowDir, ignorePaths);
7727
- indexFile = import_path19.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
8438
+ indexFile = import_path18.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
7728
8439
  const shadowEnv = {
7729
8440
  ...process.env,
7730
8441
  GIT_DIR: shadowDir,
@@ -7833,7 +8544,7 @@ function applyUndo(hash, cwd) {
7833
8544
  timeout: GIT_TIMEOUT
7834
8545
  }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
7835
8546
  for (const file of [...tracked, ...untracked]) {
7836
- const fullPath = import_path19.default.join(dir, file);
8547
+ const fullPath = import_path18.default.join(dir, file);
7837
8548
  if (!snapshotFiles.has(file) && import_fs17.default.existsSync(fullPath)) {
7838
8549
  import_fs17.default.unlinkSync(fullPath);
7839
8550
  }
@@ -7859,7 +8570,7 @@ function registerCheckCommand(program2) {
7859
8570
  } catch (err) {
7860
8571
  const tempConfig = getConfig();
7861
8572
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
7862
- const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
8573
+ const logPath = import_path19.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
7863
8574
  const errMsg = err instanceof Error ? err.message : String(err);
7864
8575
  import_fs18.default.appendFileSync(
7865
8576
  logPath,
@@ -7872,9 +8583,9 @@ RAW: ${raw}
7872
8583
  }
7873
8584
  const config = getConfig(payload.cwd || void 0);
7874
8585
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
7875
- const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
7876
- if (!import_fs18.default.existsSync(import_path20.default.dirname(logPath)))
7877
- import_fs18.default.mkdirSync(import_path20.default.dirname(logPath), { recursive: true });
8586
+ const logPath = import_path19.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
8587
+ if (!import_fs18.default.existsSync(import_path19.default.dirname(logPath)))
8588
+ import_fs18.default.mkdirSync(import_path19.default.dirname(logPath), { recursive: true });
7878
8589
  import_fs18.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
7879
8590
  `);
7880
8591
  }
@@ -7900,6 +8611,8 @@ RAW: ${raw}
7900
8611
  }
7901
8612
  writeTty(import_chalk5.default.gray(` Triggered by: ${blockedByContext}`));
7902
8613
  if (result2?.changeHint) writeTty(import_chalk5.default.cyan(` To change: ${result2.changeHint}`));
8614
+ if (result2?.recoveryCommand)
8615
+ writeTty(import_chalk5.default.green(` \u{1F4A1} Run: ${result2.recoveryCommand}`));
7903
8616
  writeTty("");
7904
8617
  } catch {
7905
8618
  } finally {
@@ -7912,7 +8625,8 @@ RAW: ${raw}
7912
8625
  const aiFeedbackMessage = buildNegotiationMessage(
7913
8626
  blockedByContext,
7914
8627
  isHumanDecision,
7915
- msg
8628
+ msg,
8629
+ result2?.recoveryCommand
7916
8630
  );
7917
8631
  process.stdout.write(
7918
8632
  JSON.stringify({
@@ -7936,7 +8650,7 @@ RAW: ${raw}
7936
8650
  if (shouldSnapshot(toolName, toolInput, config)) {
7937
8651
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
7938
8652
  }
7939
- const safeCwdForAuth = typeof payload.cwd === "string" && import_path20.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
8653
+ const safeCwdForAuth = typeof payload.cwd === "string" && import_path19.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7940
8654
  const result = await authorizeHeadless(toolName, toolInput, meta, {
7941
8655
  cwd: safeCwdForAuth
7942
8656
  });
@@ -7980,7 +8694,7 @@ RAW: ${raw}
7980
8694
  });
7981
8695
  } catch (err) {
7982
8696
  if (process.env.NODE9_DEBUG === "1") {
7983
- const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
8697
+ const logPath = import_path19.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
7984
8698
  const errMsg = err instanceof Error ? err.message : String(err);
7985
8699
  import_fs18.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
7986
8700
  `);
@@ -8017,8 +8731,8 @@ RAW: ${raw}
8017
8731
 
8018
8732
  // src/cli/commands/log.ts
8019
8733
  var import_fs19 = __toESM(require("fs"));
8020
- var import_path21 = __toESM(require("path"));
8021
- var import_os16 = __toESM(require("os"));
8734
+ var import_path20 = __toESM(require("path"));
8735
+ var import_os15 = __toESM(require("os"));
8022
8736
  init_audit();
8023
8737
  init_config();
8024
8738
  init_policy();
@@ -8062,6 +8776,20 @@ function containsShellMetachar(token) {
8062
8776
  }
8063
8777
 
8064
8778
  // src/cli/commands/log.ts
8779
+ 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;
8780
+ function detectTestResult(command, output) {
8781
+ if (!TEST_COMMAND_RE.test(command)) return null;
8782
+ const out = output.toLowerCase();
8783
+ if (/\b(tests?\s+passed|all\s+tests?\s+passed|passing|test\s+suites?.*passed|ok\b|\d+\s+passed)/i.test(
8784
+ out
8785
+ ) && !/\b(fail|error|failed)\b/.test(out)) {
8786
+ return "pass";
8787
+ }
8788
+ if (/\b(tests?\s+failed|failing|failed|error|assertion\s+error|\d+\s+failed)\b/i.test(out)) {
8789
+ return "fail";
8790
+ }
8791
+ return null;
8792
+ }
8065
8793
  function sanitize3(value) {
8066
8794
  return value.replace(/[\x00-\x1F\x7F]/g, "");
8067
8795
  }
@@ -8080,9 +8808,9 @@ function registerLogCommand(program2) {
8080
8808
  decision: "allowed",
8081
8809
  source: "post-hook"
8082
8810
  };
8083
- const logPath = import_path21.default.join(import_os16.default.homedir(), ".node9", "audit.log");
8084
- if (!import_fs19.default.existsSync(import_path21.default.dirname(logPath)))
8085
- import_fs19.default.mkdirSync(import_path21.default.dirname(logPath), { recursive: true });
8811
+ const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "audit.log");
8812
+ if (!import_fs19.default.existsSync(import_path20.default.dirname(logPath)))
8813
+ import_fs19.default.mkdirSync(import_path20.default.dirname(logPath), { recursive: true });
8086
8814
  import_fs19.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
8087
8815
  if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
8088
8816
  const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
@@ -8093,7 +8821,22 @@ function registerLogCommand(program2) {
8093
8821
  }
8094
8822
  }
8095
8823
  }
8096
- const safeCwd = typeof payload.cwd === "string" && import_path21.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
8824
+ if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
8825
+ const bashCommand = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
8826
+ const output = payload.tool_response?.output ?? "";
8827
+ if (bashCommand && output) {
8828
+ const testResult = detectTestResult(bashCommand, output);
8829
+ if (testResult) {
8830
+ await notifyActivitySocket({
8831
+ id: "test-result",
8832
+ ts: Date.now(),
8833
+ tool,
8834
+ status: testResult === "pass" ? "test_pass" : "test_fail"
8835
+ });
8836
+ }
8837
+ }
8838
+ }
8839
+ const safeCwd = typeof payload.cwd === "string" && import_path20.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
8097
8840
  const config = getConfig(safeCwd);
8098
8841
  if (shouldSnapshot(tool, {}, config)) {
8099
8842
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
@@ -8102,7 +8845,7 @@ function registerLogCommand(program2) {
8102
8845
  const msg = err instanceof Error ? err.message : String(err);
8103
8846
  process.stderr.write(`[Node9] audit log error: ${msg}
8104
8847
  `);
8105
- const debugPath = import_path21.default.join(import_os16.default.homedir(), ".node9", "hook-debug.log");
8848
+ const debugPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
8106
8849
  try {
8107
8850
  import_fs19.default.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
8108
8851
  `);
@@ -8409,13 +9152,13 @@ function registerConfigShowCommand(program2) {
8409
9152
  // src/cli/commands/doctor.ts
8410
9153
  var import_chalk7 = __toESM(require("chalk"));
8411
9154
  var import_fs20 = __toESM(require("fs"));
8412
- var import_path22 = __toESM(require("path"));
8413
- var import_os17 = __toESM(require("os"));
9155
+ var import_path21 = __toESM(require("path"));
9156
+ var import_os16 = __toESM(require("os"));
8414
9157
  var import_child_process9 = require("child_process");
8415
9158
  init_daemon();
8416
9159
  function registerDoctorCommand(program2, version2) {
8417
9160
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
8418
- const homeDir2 = import_os17.default.homedir();
9161
+ const homeDir2 = import_os16.default.homedir();
8419
9162
  let failures = 0;
8420
9163
  function pass(msg) {
8421
9164
  console.log(import_chalk7.default.green(" \u2705 ") + msg);
@@ -8464,7 +9207,7 @@ function registerDoctorCommand(program2, version2) {
8464
9207
  );
8465
9208
  }
8466
9209
  section("Configuration");
8467
- const globalConfigPath = import_path22.default.join(homeDir2, ".node9", "config.json");
9210
+ const globalConfigPath = import_path21.default.join(homeDir2, ".node9", "config.json");
8468
9211
  if (import_fs20.default.existsSync(globalConfigPath)) {
8469
9212
  try {
8470
9213
  JSON.parse(import_fs20.default.readFileSync(globalConfigPath, "utf-8"));
@@ -8475,7 +9218,7 @@ function registerDoctorCommand(program2, version2) {
8475
9218
  } else {
8476
9219
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
8477
9220
  }
8478
- const projectConfigPath = import_path22.default.join(process.cwd(), "node9.config.json");
9221
+ const projectConfigPath = import_path21.default.join(process.cwd(), "node9.config.json");
8479
9222
  if (import_fs20.default.existsSync(projectConfigPath)) {
8480
9223
  try {
8481
9224
  JSON.parse(import_fs20.default.readFileSync(projectConfigPath, "utf-8"));
@@ -8487,7 +9230,7 @@ function registerDoctorCommand(program2, version2) {
8487
9230
  );
8488
9231
  }
8489
9232
  }
8490
- const credsPath = import_path22.default.join(homeDir2, ".node9", "credentials.json");
9233
+ const credsPath = import_path21.default.join(homeDir2, ".node9", "credentials.json");
8491
9234
  if (import_fs20.default.existsSync(credsPath)) {
8492
9235
  pass("Cloud credentials found (~/.node9/credentials.json)");
8493
9236
  } else {
@@ -8497,7 +9240,7 @@ function registerDoctorCommand(program2, version2) {
8497
9240
  );
8498
9241
  }
8499
9242
  section("Agent Hooks");
8500
- const claudeSettingsPath = import_path22.default.join(homeDir2, ".claude", "settings.json");
9243
+ const claudeSettingsPath = import_path21.default.join(homeDir2, ".claude", "settings.json");
8501
9244
  if (import_fs20.default.existsSync(claudeSettingsPath)) {
8502
9245
  try {
8503
9246
  const cs = JSON.parse(import_fs20.default.readFileSync(claudeSettingsPath, "utf-8"));
@@ -8516,7 +9259,7 @@ function registerDoctorCommand(program2, version2) {
8516
9259
  } else {
8517
9260
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
8518
9261
  }
8519
- const geminiSettingsPath = import_path22.default.join(homeDir2, ".gemini", "settings.json");
9262
+ const geminiSettingsPath = import_path21.default.join(homeDir2, ".gemini", "settings.json");
8520
9263
  if (import_fs20.default.existsSync(geminiSettingsPath)) {
8521
9264
  try {
8522
9265
  const gs = JSON.parse(import_fs20.default.readFileSync(geminiSettingsPath, "utf-8"));
@@ -8535,7 +9278,7 @@ function registerDoctorCommand(program2, version2) {
8535
9278
  } else {
8536
9279
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
8537
9280
  }
8538
- const cursorHooksPath = import_path22.default.join(homeDir2, ".cursor", "hooks.json");
9281
+ const cursorHooksPath = import_path21.default.join(homeDir2, ".cursor", "hooks.json");
8539
9282
  if (import_fs20.default.existsSync(cursorHooksPath)) {
8540
9283
  try {
8541
9284
  const cur = JSON.parse(import_fs20.default.readFileSync(cursorHooksPath, "utf-8"));
@@ -8577,8 +9320,8 @@ function registerDoctorCommand(program2, version2) {
8577
9320
  // src/cli/commands/audit.ts
8578
9321
  var import_chalk8 = __toESM(require("chalk"));
8579
9322
  var import_fs21 = __toESM(require("fs"));
8580
- var import_path23 = __toESM(require("path"));
8581
- var import_os18 = __toESM(require("os"));
9323
+ var import_path22 = __toESM(require("path"));
9324
+ var import_os17 = __toESM(require("os"));
8582
9325
  function formatRelativeTime(timestamp) {
8583
9326
  const diff = Date.now() - new Date(timestamp).getTime();
8584
9327
  const sec = Math.floor(diff / 1e3);
@@ -8591,7 +9334,7 @@ function formatRelativeTime(timestamp) {
8591
9334
  }
8592
9335
  function registerAuditCommand(program2) {
8593
9336
  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");
9337
+ const logPath = import_path22.default.join(import_os17.default.homedir(), ".node9", "audit.log");
8595
9338
  if (!import_fs21.default.existsSync(logPath)) {
8596
9339
  console.log(
8597
9340
  import_chalk8.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
@@ -8717,8 +9460,8 @@ function registerDaemonCommand(program2) {
8717
9460
  // src/cli/commands/status.ts
8718
9461
  var import_chalk10 = __toESM(require("chalk"));
8719
9462
  var import_fs22 = __toESM(require("fs"));
8720
- var import_path24 = __toESM(require("path"));
8721
- var import_os19 = __toESM(require("os"));
9463
+ var import_path23 = __toESM(require("path"));
9464
+ var import_os18 = __toESM(require("os"));
8722
9465
  init_core();
8723
9466
  init_daemon();
8724
9467
  function readJson2(filePath) {
@@ -8788,8 +9531,8 @@ function registerStatusCommand(program2) {
8788
9531
  console.log("");
8789
9532
  const modeLabel = settings.mode === "audit" ? import_chalk10.default.blue("audit") : settings.mode === "strict" ? import_chalk10.default.red("strict") : import_chalk10.default.white("standard");
8790
9533
  console.log(` Mode: ${modeLabel}`);
8791
- 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");
9534
+ const projectConfig = import_path23.default.join(process.cwd(), "node9.config.json");
9535
+ const globalConfig = import_path23.default.join(import_os18.default.homedir(), ".node9", "config.json");
8793
9536
  console.log(
8794
9537
  ` Local: ${import_fs22.default.existsSync(projectConfig) ? import_chalk10.default.green("Active (node9.config.json)") : import_chalk10.default.gray("Not present")}`
8795
9538
  );
@@ -8801,15 +9544,15 @@ function registerStatusCommand(program2) {
8801
9544
  ` Sandbox: ${import_chalk10.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
8802
9545
  );
8803
9546
  }
8804
- const homeDir2 = import_os19.default.homedir();
9547
+ const homeDir2 = import_os18.default.homedir();
8805
9548
  const claudeSettings = readJson2(
8806
- import_path24.default.join(homeDir2, ".claude", "settings.json")
9549
+ import_path23.default.join(homeDir2, ".claude", "settings.json")
8807
9550
  );
8808
- const claudeConfig = readJson2(import_path24.default.join(homeDir2, ".claude.json"));
9551
+ const claudeConfig = readJson2(import_path23.default.join(homeDir2, ".claude.json"));
8809
9552
  const geminiSettings = readJson2(
8810
- import_path24.default.join(homeDir2, ".gemini", "settings.json")
9553
+ import_path23.default.join(homeDir2, ".gemini", "settings.json")
8811
9554
  );
8812
- const cursorConfig = readJson2(import_path24.default.join(homeDir2, ".cursor", "mcp.json"));
9555
+ const cursorConfig = readJson2(import_path23.default.join(homeDir2, ".cursor", "mcp.json"));
8813
9556
  const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
8814
9557
  if (agentFound) {
8815
9558
  console.log("");
@@ -8869,13 +9612,13 @@ function registerStatusCommand(program2) {
8869
9612
  // src/cli/commands/init.ts
8870
9613
  var import_chalk11 = __toESM(require("chalk"));
8871
9614
  var import_fs23 = __toESM(require("fs"));
8872
- var import_path25 = __toESM(require("path"));
8873
- var import_os20 = __toESM(require("os"));
9615
+ var import_path24 = __toESM(require("path"));
9616
+ var import_os19 = __toESM(require("os"));
8874
9617
  init_core();
8875
9618
  function registerInitCommand(program2) {
8876
9619
  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
9620
  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");
9621
+ const configPath = import_path24.default.join(import_os19.default.homedir(), ".node9", "config.json");
8879
9622
  if (import_fs23.default.existsSync(configPath) && !options.force) {
8880
9623
  console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
8881
9624
  } else {
@@ -8885,7 +9628,7 @@ function registerInitCommand(program2) {
8885
9628
  ...DEFAULT_CONFIG,
8886
9629
  settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
8887
9630
  };
8888
- const dir = import_path25.default.dirname(configPath);
9631
+ const dir = import_path24.default.dirname(configPath);
8889
9632
  if (!import_fs23.default.existsSync(dir)) import_fs23.default.mkdirSync(dir, { recursive: true });
8890
9633
  import_fs23.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
8891
9634
  console.log(import_chalk11.default.green(`\u2705 Config created: ${configPath}`));
@@ -9343,20 +10086,20 @@ function registerTrustCommand(program2) {
9343
10086
 
9344
10087
  // src/cli.ts
9345
10088
  var { version } = JSON.parse(
9346
- import_fs25.default.readFileSync(import_path27.default.join(__dirname, "../package.json"), "utf-8")
10089
+ import_fs26.default.readFileSync(import_path27.default.join(__dirname, "../package.json"), "utf-8")
9347
10090
  );
9348
10091
  var program = new import_commander.Command();
9349
10092
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
9350
10093
  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
10094
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
9352
10095
  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 });
10096
+ if (!import_fs26.default.existsSync(import_path27.default.dirname(credPath)))
10097
+ import_fs26.default.mkdirSync(import_path27.default.dirname(credPath), { recursive: true });
9355
10098
  const profileName = options.profile || "default";
9356
10099
  let existingCreds = {};
9357
10100
  try {
9358
- if (import_fs25.default.existsSync(credPath)) {
9359
- const raw = JSON.parse(import_fs25.default.readFileSync(credPath, "utf-8"));
10101
+ if (import_fs26.default.existsSync(credPath)) {
10102
+ const raw = JSON.parse(import_fs26.default.readFileSync(credPath, "utf-8"));
9360
10103
  if (raw.apiKey) {
9361
10104
  existingCreds = {
9362
10105
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -9368,13 +10111,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
9368
10111
  } catch {
9369
10112
  }
9370
10113
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
9371
- import_fs25.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
10114
+ import_fs26.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
9372
10115
  if (profileName === "default") {
9373
10116
  const configPath = import_path27.default.join(import_os22.default.homedir(), ".node9", "config.json");
9374
10117
  let config = {};
9375
10118
  try {
9376
- if (import_fs25.default.existsSync(configPath))
9377
- config = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
10119
+ if (import_fs26.default.existsSync(configPath))
10120
+ config = JSON.parse(import_fs26.default.readFileSync(configPath, "utf-8"));
9378
10121
  } catch {
9379
10122
  }
9380
10123
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -9389,9 +10132,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
9389
10132
  approvers.cloud = false;
9390
10133
  }
9391
10134
  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 });
10135
+ if (!import_fs26.default.existsSync(import_path27.default.dirname(configPath)))
10136
+ import_fs26.default.mkdirSync(import_path27.default.dirname(configPath), { recursive: true });
10137
+ import_fs26.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
9395
10138
  }
9396
10139
  if (options.profile && profileName !== "default") {
9397
10140
  console.log(import_chalk17.default.green(`\u2705 Profile "${profileName}" saved`));
@@ -9404,14 +10147,15 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
9404
10147
  console.log(import_chalk17.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
9405
10148
  }
9406
10149
  });
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) => {
10150
+ 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
10151
  if (target === "gemini") return await setupGemini();
9409
10152
  if (target === "claude") return await setupClaude();
9410
10153
  if (target === "cursor") return await setupCursor();
9411
- console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
10154
+ if (target === "hud") return setupHud();
10155
+ console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
9412
10156
  process.exit(1);
9413
10157
  });
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) => {
10158
+ 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
10159
  if (!target) {
9416
10160
  console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
9417
10161
  console.log(" Usage: " + import_chalk17.default.white("node9 setup <target>") + "\n");
@@ -9419,6 +10163,9 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
9419
10163
  console.log(" " + import_chalk17.default.green("claude") + " \u2014 Claude Code (hook mode)");
9420
10164
  console.log(" " + import_chalk17.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
9421
10165
  console.log(" " + import_chalk17.default.green("cursor") + " \u2014 Cursor (hook mode)");
10166
+ process.stdout.write(
10167
+ " " + import_chalk17.default.green("hud") + " \u2014 Claude Code security statusline\n"
10168
+ );
9422
10169
  console.log("");
9423
10170
  return;
9424
10171
  }
@@ -9426,7 +10173,8 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
9426
10173
  if (t === "gemini") return await setupGemini();
9427
10174
  if (t === "claude") return await setupClaude();
9428
10175
  if (t === "cursor") return await setupCursor();
9429
- console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
10176
+ if (t === "hud") return setupHud();
10177
+ console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
9430
10178
  process.exit(1);
9431
10179
  });
9432
10180
  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,8 +10182,11 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
9434
10182
  if (target === "claude") fn = teardownClaude;
9435
10183
  else if (target === "gemini") fn = teardownGemini;
9436
10184
  else if (target === "cursor") fn = teardownCursor;
10185
+ else if (target === "hud") fn = teardownHud;
9437
10186
  else {
9438
- console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
10187
+ console.error(
10188
+ import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
10189
+ );
9439
10190
  process.exit(1);
9440
10191
  }
9441
10192
  console.log(import_chalk17.default.cyan(`
@@ -9478,14 +10229,14 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
9478
10229
  }
9479
10230
  if (options.purge) {
9480
10231
  const node9Dir = import_path27.default.join(import_os22.default.homedir(), ".node9");
9481
- if (import_fs25.default.existsSync(node9Dir)) {
10232
+ if (import_fs26.default.existsSync(node9Dir)) {
9482
10233
  const confirmed = await (0, import_prompts3.confirm)({
9483
10234
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
9484
10235
  default: false
9485
10236
  });
9486
10237
  if (confirmed) {
9487
- import_fs25.default.rmSync(node9Dir, { recursive: true });
9488
- if (import_fs25.default.existsSync(node9Dir)) {
10238
+ import_fs26.default.rmSync(node9Dir, { recursive: true });
10239
+ if (import_fs26.default.existsSync(node9Dir)) {
9489
10240
  console.error(
9490
10241
  import_chalk17.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
9491
10242
  );
@@ -9601,6 +10352,10 @@ registerWatchCommand(program);
9601
10352
  registerMcpGatewayCommand(program);
9602
10353
  registerCheckCommand(program);
9603
10354
  registerLogCommand(program);
10355
+ program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").action(async () => {
10356
+ const { main: main2 } = await Promise.resolve().then(() => (init_hud(), hud_exports));
10357
+ await main2();
10358
+ });
9604
10359
  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
10360
  const ms = parseDuration(options.duration);
9606
10361
  if (ms === null) {
@@ -9699,7 +10454,7 @@ if (process.argv[2] !== "daemon") {
9699
10454
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
9700
10455
  const logPath = import_path27.default.join(import_os22.default.homedir(), ".node9", "hook-debug.log");
9701
10456
  const msg = reason instanceof Error ? reason.message : String(reason);
9702
- import_fs25.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
10457
+ import_fs26.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
9703
10458
  `);
9704
10459
  }
9705
10460
  process.exit(0);