@node9/proxy 1.5.2 → 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.mjs CHANGED
@@ -191,12 +191,21 @@ var init_config_schema = __esm({
191
191
  verdict: z.enum(["allow", "review", "block"], {
192
192
  errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
193
193
  }),
194
- reason: z.string().optional()
194
+ reason: z.string().optional(),
195
+ // Unknown predicate names are filtered out rather than failing the whole rule.
196
+ // Failing the whole z.array() would cause sanitizeConfig to drop the entire
197
+ // `policy` top-level key, silently disabling ALL smart rules in the config.
198
+ dependsOnState: z.array(z.string()).transform(
199
+ (arr) => arr.filter(
200
+ (p) => p === "no_test_passed_since_last_edit"
201
+ )
202
+ ).optional(),
203
+ recoveryCommand: z.string().optional()
195
204
  });
196
205
  ConfigFileSchema = z.object({
197
206
  version: z.string().optional(),
198
207
  settings: z.object({
199
- mode: z.enum(["standard", "strict", "audit"]).optional(),
208
+ mode: z.enum(["standard", "strict", "audit", "observe"]).optional(),
200
209
  autoStartDaemon: z.boolean().optional(),
201
210
  enableUndo: z.boolean().optional(),
202
211
  enableHookLogDebug: z.boolean().optional(),
@@ -626,12 +635,17 @@ function getConfig(cwd) {
626
635
  if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
627
636
  mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
628
637
  if (s.environment !== void 0) mergedSettings.environment = s.environment;
638
+ if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
629
639
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
630
640
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
631
641
  if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
632
642
  if (p.toolInspection)
633
643
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
634
- if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
644
+ if (p.smartRules) {
645
+ const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
646
+ const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
647
+ mergedPolicy.smartRules = [...defaultBlocks, ...p.smartRules, ...defaultNonBlocks];
648
+ }
635
649
  if (p.snapshot) {
636
650
  const s2 = p.snapshot;
637
651
  if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
@@ -1893,7 +1907,13 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1893
1907
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
1894
1908
  reason: matchedRule.reason,
1895
1909
  tier: 2,
1896
- ruleName: matchedRule.name ?? matchedRule.tool
1910
+ ruleName: matchedRule.name ?? matchedRule.tool,
1911
+ ...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
1912
+ dependsOnStatePredicates: matchedRule.dependsOnState
1913
+ },
1914
+ ...matchedRule.verdict === "block" && matchedRule.recoveryCommand && {
1915
+ recoveryCommand: matchedRule.recoveryCommand
1916
+ }
1897
1917
  };
1898
1918
  }
1899
1919
  }
@@ -2416,9 +2436,38 @@ var init_state = __esm({
2416
2436
 
2417
2437
  // src/auth/daemon.ts
2418
2438
  import fs9 from "fs";
2439
+ import net from "net";
2419
2440
  import path10 from "path";
2420
2441
  import os8 from "os";
2421
2442
  import { spawnSync } from "child_process";
2443
+ function notifyActivitySocket(data) {
2444
+ return new Promise((resolve) => {
2445
+ try {
2446
+ const payload = JSON.stringify(data);
2447
+ const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
2448
+ sock.on("connect", () => {
2449
+ sock.on("close", resolve);
2450
+ sock.end(payload);
2451
+ });
2452
+ sock.on("error", resolve);
2453
+ } catch {
2454
+ resolve();
2455
+ }
2456
+ });
2457
+ }
2458
+ async function checkStatePredicates(predicates) {
2459
+ if (predicates.length === 0) return {};
2460
+ try {
2461
+ const qs = predicates.map(encodeURIComponent).join(",");
2462
+ const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/state/check?predicates=${qs}`, {
2463
+ signal: AbortSignal.timeout(100)
2464
+ });
2465
+ if (!res.ok) return null;
2466
+ return await res.json();
2467
+ } catch {
2468
+ return null;
2469
+ }
2470
+ }
2422
2471
  function getInternalToken() {
2423
2472
  try {
2424
2473
  const pidFile = path10.join(os8.homedir(), ".node9", "daemon.pid");
@@ -2452,7 +2501,7 @@ function isDaemonRunning() {
2452
2501
  return false;
2453
2502
  }
2454
2503
  }
2455
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
2504
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
2456
2505
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2457
2506
  const ctrl = new AbortController();
2458
2507
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -2470,7 +2519,10 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2470
2519
  // activity-result as the CLI used for the pending activity event.
2471
2520
  activityId,
2472
2521
  ...riskMetadata && { riskMetadata },
2473
- ...cwd && { cwd }
2522
+ ...cwd && { cwd },
2523
+ ...recoveryCommand && { recoveryCommand },
2524
+ ...skipBackgroundAuth && { skipBackgroundAuth: true },
2525
+ ...viewOnly && { viewOnly: true }
2474
2526
  }),
2475
2527
  signal: ctrl.signal
2476
2528
  });
@@ -2490,10 +2542,10 @@ async function waitForDaemonDecision(id, signal) {
2490
2542
  try {
2491
2543
  const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
2492
2544
  if (!waitRes.ok) return { decision: "deny" };
2493
- const { decision, source } = await waitRes.json();
2545
+ const { decision, source, reason } = await waitRes.json();
2494
2546
  if (decision === "allow") return { decision: "allow", source };
2495
2547
  if (decision === "abandoned") return { decision: "abandoned", source };
2496
- return { decision: "deny", source };
2548
+ return { decision: "deny", source, reason };
2497
2549
  } finally {
2498
2550
  clearTimeout(waitTimer);
2499
2551
  if (signal) signal.removeEventListener("abort", onAbort);
@@ -2579,10 +2631,11 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
2579
2631
  signal: AbortSignal.timeout(3e3)
2580
2632
  });
2581
2633
  }
2582
- var DAEMON_PORT, DAEMON_HOST;
2634
+ var ACTIVITY_SOCKET_PATH, DAEMON_PORT, DAEMON_HOST;
2583
2635
  var init_daemon = __esm({
2584
2636
  "src/auth/daemon.ts"() {
2585
2637
  "use strict";
2638
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path10.join(os8.tmpdir(), "node9-activity.sock");
2586
2639
  DAEMON_PORT = 7391;
2587
2640
  DAEMON_HOST = "127.0.0.1";
2588
2641
  }
@@ -3036,9 +3089,6 @@ var init_cloud = __esm({
3036
3089
  });
3037
3090
 
3038
3091
  // src/auth/orchestrator.ts
3039
- import net from "net";
3040
- import path13 from "path";
3041
- import os10 from "os";
3042
3092
  import { randomUUID } from "crypto";
3043
3093
  function isWriteTool(toolName) {
3044
3094
  const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
@@ -3075,19 +3125,7 @@ function isNetworkTool(toolName, args) {
3075
3125
  return false;
3076
3126
  }
3077
3127
  function notifyActivity(data) {
3078
- return new Promise((resolve) => {
3079
- try {
3080
- const payload = JSON.stringify(data);
3081
- const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
3082
- sock.on("connect", () => {
3083
- sock.on("close", resolve);
3084
- sock.end(payload);
3085
- });
3086
- sock.on("error", resolve);
3087
- } catch {
3088
- resolve();
3089
- }
3090
- });
3128
+ return notifyActivitySocket(data);
3091
3129
  }
3092
3130
  async function authorizeHeadless(toolName, args, meta, options) {
3093
3131
  if (!options?.calledFromDaemon) {
@@ -3104,7 +3142,9 @@ async function authorizeHeadless(toolName, args, meta, options) {
3104
3142
  tool: toolName,
3105
3143
  ts: actTs,
3106
3144
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
3107
- label: result.blockedByLabel
3145
+ label: result.blockedByLabel,
3146
+ ruleHit: result.ruleHit,
3147
+ observeWouldBlock: result.observeWouldBlock
3108
3148
  });
3109
3149
  }
3110
3150
  return result;
@@ -3131,10 +3171,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3131
3171
  appendHookDebug(toolName, args, meta, hashAuditArgs);
3132
3172
  }
3133
3173
  const isManual = meta?.agent === "Terminal";
3174
+ const isObserveMode = config.settings.mode === "observe";
3134
3175
  let explainableLabel = "Local Config";
3135
3176
  let policyMatchedField;
3136
3177
  let policyMatchedWord;
3137
3178
  let riskMetadata;
3179
+ let statefulRecoveryCommand;
3138
3180
  let taintWarning = null;
3139
3181
  if (isNetworkTool(toolName, args)) {
3140
3182
  const filePaths = extractFilePaths(toolName, args);
@@ -3155,10 +3197,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3155
3197
  if (dlpMatch) {
3156
3198
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
3157
3199
  if (dlpMatch.severity === "block") {
3158
- if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta, true);
3200
+ if (!isManual)
3201
+ appendLocalAudit(
3202
+ toolName,
3203
+ args,
3204
+ "deny",
3205
+ isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
3206
+ meta,
3207
+ true
3208
+ );
3159
3209
  if (isWriteTool(toolName) && filePath) {
3160
3210
  await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
3161
3211
  }
3212
+ if (isObserveMode) {
3213
+ return {
3214
+ approved: true,
3215
+ checkedBy: "audit",
3216
+ observeWouldBlock: true,
3217
+ blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
3218
+ };
3219
+ }
3162
3220
  return {
3163
3221
  approved: false,
3164
3222
  reason: dlpReason,
@@ -3171,6 +3229,31 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3171
3229
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
3172
3230
  }
3173
3231
  }
3232
+ if (isObserveMode) {
3233
+ if (!isIgnoredTool(toolName)) {
3234
+ const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
3235
+ const wouldBlock = policyResult.decision === "block";
3236
+ if (!isManual)
3237
+ appendLocalAudit(
3238
+ toolName,
3239
+ args,
3240
+ "allow",
3241
+ wouldBlock ? "observe-mode-would-block" : "observe-mode",
3242
+ meta,
3243
+ hashAuditArgs
3244
+ );
3245
+ return {
3246
+ approved: true,
3247
+ checkedBy: "audit",
3248
+ ...wouldBlock && {
3249
+ observeWouldBlock: true,
3250
+ blockedByLabel: policyResult.blockedByLabel,
3251
+ ruleHit: policyResult.ruleName
3252
+ }
3253
+ };
3254
+ }
3255
+ return { approved: true, checkedBy: "audit" };
3256
+ }
3174
3257
  if (config.settings.mode === "audit") {
3175
3258
  if (!isIgnoredTool(toolName)) {
3176
3259
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
@@ -3198,14 +3281,34 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3198
3281
  return { approved: true, checkedBy: "local-policy" };
3199
3282
  }
3200
3283
  if (policyResult.decision === "block") {
3201
- if (!isManual)
3202
- appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
3203
- return {
3204
- approved: false,
3205
- reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
3206
- blockedBy: "local-config",
3207
- blockedByLabel: policyResult.blockedByLabel
3208
- };
3284
+ if (policyResult.dependsOnStatePredicates?.length) {
3285
+ const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
3286
+ const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
3287
+ if (stateResults === null && !isManual) {
3288
+ appendToLog(HOOK_DEBUG_LOG, {
3289
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3290
+ event: "state-check-fail-open",
3291
+ tool: toolName,
3292
+ rule: policyResult.ruleName,
3293
+ predicates: policyResult.dependsOnStatePredicates,
3294
+ reason: "daemon unreachable or /state/check timed out \u2014 block rule downgraded to review"
3295
+ });
3296
+ }
3297
+ if (predicatesMet && policyResult.recoveryCommand) {
3298
+ statefulRecoveryCommand = policyResult.recoveryCommand;
3299
+ }
3300
+ } else {
3301
+ if (!isManual)
3302
+ appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
3303
+ return {
3304
+ approved: false,
3305
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
3306
+ blockedBy: "local-config",
3307
+ blockedByLabel: policyResult.blockedByLabel,
3308
+ ruleHit: policyResult.ruleName,
3309
+ ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
3310
+ };
3311
+ }
3209
3312
  }
3210
3313
  explainableLabel = policyResult.blockedByLabel || "Local Config";
3211
3314
  policyMatchedField = policyResult.matchedField;
@@ -3312,7 +3415,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3312
3415
  meta,
3313
3416
  riskMetadata,
3314
3417
  options?.activityId,
3315
- options?.cwd
3418
+ options?.cwd,
3419
+ statefulRecoveryCommand
3316
3420
  );
3317
3421
  daemonEntryId = entry.id;
3318
3422
  daemonAllowCount = entry.allowCount;
@@ -3373,20 +3477,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3373
3477
  if (daemonEntryId && (approvers.browser || approvers.terminal)) {
3374
3478
  racePromises.push(
3375
3479
  (async () => {
3376
- const { decision: daemonDecision, source: decisionSource } = await waitForDaemonDecision(
3377
- daemonEntryId,
3378
- signal
3379
- );
3480
+ const {
3481
+ decision: daemonDecision,
3482
+ source: decisionSource,
3483
+ reason: daemonReason
3484
+ } = await waitForDaemonDecision(daemonEntryId, signal);
3380
3485
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
3381
3486
  const isApproved = daemonDecision === "allow";
3382
- const src = decisionSource === "terminal" || decisionSource === "browser" ? decisionSource : approvers.browser ? "browser" : "terminal";
3487
+ const isRedirect = decisionSource === "terminal-redirect";
3488
+ const src = decisionSource === "terminal" || decisionSource === "terminal-redirect" || decisionSource === "browser" ? decisionSource === "browser" ? "browser" : "terminal" : approvers.browser ? "browser" : "terminal";
3383
3489
  const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
3384
3490
  return {
3385
3491
  approved: isApproved,
3386
- reason: isApproved ? void 0 : `The human user rejected this action via the Node9 ${via}.`,
3492
+ reason: isApproved ? void 0 : (
3493
+ // Use the redirect reason from the tail when choice [2] was selected;
3494
+ // otherwise fall back to the generic rejection message.
3495
+ isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`
3496
+ ),
3387
3497
  checkedBy: isApproved ? "daemon" : void 0,
3388
3498
  blockedBy: isApproved ? void 0 : "local-decision",
3389
- blockedByLabel: `User Decision (${via})`,
3499
+ blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
3390
3500
  decisionSource: src
3391
3501
  };
3392
3502
  })()
@@ -3461,7 +3571,7 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3461
3571
  }
3462
3572
  return finalResult;
3463
3573
  }
3464
- var WRITE_TOOLS, ACTIVITY_SOCKET_PATH;
3574
+ var WRITE_TOOLS;
3465
3575
  var init_orchestrator = __esm({
3466
3576
  "src/auth/orchestrator.ts"() {
3467
3577
  "use strict";
@@ -3485,7 +3595,6 @@ var init_orchestrator = __esm({
3485
3595
  "notebook_edit",
3486
3596
  "notebookedit"
3487
3597
  ]);
3488
- ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os10.tmpdir(), "node9-activity.sock");
3489
3598
  }
3490
3599
  });
3491
3600
 
@@ -5213,7 +5322,7 @@ var init_suggestion_tracker = __esm({
5213
5322
 
5214
5323
  // src/daemon/taint-store.ts
5215
5324
  import fs12 from "fs";
5216
- import path15 from "path";
5325
+ import path14 from "path";
5217
5326
  var DEFAULT_TTL_MS, TaintStore;
5218
5327
  var init_taint_store = __esm({
5219
5328
  "src/daemon/taint-store.ts"() {
@@ -5282,20 +5391,121 @@ var init_taint_store = __esm({
5282
5391
  /** Resolve to absolute path, falling back to path.resolve if file doesn't exist yet. */
5283
5392
  _resolve(filePath) {
5284
5393
  try {
5285
- return fs12.realpathSync.native(path15.resolve(filePath));
5394
+ return fs12.realpathSync.native(path14.resolve(filePath));
5286
5395
  } catch {
5287
- return path15.resolve(filePath);
5396
+ return path14.resolve(filePath);
5288
5397
  }
5289
5398
  }
5290
5399
  };
5291
5400
  }
5292
5401
  });
5293
5402
 
5403
+ // src/daemon/session-counters.ts
5404
+ var SessionCounters, sessionCounters;
5405
+ var init_session_counters = __esm({
5406
+ "src/daemon/session-counters.ts"() {
5407
+ "use strict";
5408
+ SessionCounters = class {
5409
+ _allowed = 0;
5410
+ _blocked = 0;
5411
+ _dlpHits = 0;
5412
+ _wouldBlock = 0;
5413
+ _lastRuleHit = null;
5414
+ _lastBlockedTool = null;
5415
+ incrementAllowed() {
5416
+ this._allowed++;
5417
+ }
5418
+ incrementBlocked() {
5419
+ this._blocked++;
5420
+ }
5421
+ incrementDlpHits() {
5422
+ this._dlpHits++;
5423
+ }
5424
+ incrementWouldBlock() {
5425
+ this._wouldBlock++;
5426
+ }
5427
+ recordRuleHit(label) {
5428
+ this._lastRuleHit = label;
5429
+ }
5430
+ recordBlockedTool(toolName) {
5431
+ this._lastBlockedTool = toolName;
5432
+ }
5433
+ get() {
5434
+ return {
5435
+ allowed: this._allowed,
5436
+ blocked: this._blocked,
5437
+ dlpHits: this._dlpHits,
5438
+ wouldBlock: this._wouldBlock,
5439
+ lastRuleHit: this._lastRuleHit,
5440
+ lastBlockedTool: this._lastBlockedTool
5441
+ };
5442
+ }
5443
+ reset() {
5444
+ this._allowed = 0;
5445
+ this._blocked = 0;
5446
+ this._dlpHits = 0;
5447
+ this._wouldBlock = 0;
5448
+ this._lastRuleHit = null;
5449
+ this._lastBlockedTool = null;
5450
+ }
5451
+ };
5452
+ sessionCounters = new SessionCounters();
5453
+ }
5454
+ });
5455
+
5456
+ // src/daemon/session-history.ts
5457
+ var SessionHistory, sessionHistory;
5458
+ var init_session_history = __esm({
5459
+ "src/daemon/session-history.ts"() {
5460
+ "use strict";
5461
+ SessionHistory = class {
5462
+ lastEditAt = null;
5463
+ lastTestPassAt = null;
5464
+ lastTestFailAt = null;
5465
+ recordEdit(ts = Date.now()) {
5466
+ this.lastEditAt = ts;
5467
+ }
5468
+ recordTestPass(ts = Date.now()) {
5469
+ this.lastTestPassAt = ts;
5470
+ }
5471
+ recordTestFail(ts = Date.now()) {
5472
+ this.lastTestFailAt = ts;
5473
+ }
5474
+ /**
5475
+ * Returns true when the named predicate is currently satisfied.
5476
+ * Unknown predicates always return false (fail-open: don't block on unknown state).
5477
+ */
5478
+ checkPredicate(name) {
5479
+ switch (name) {
5480
+ case "no_test_passed_since_last_edit":
5481
+ if (this.lastEditAt === null) return false;
5482
+ return this.lastTestPassAt === null || this.lastTestPassAt < this.lastEditAt;
5483
+ default:
5484
+ return false;
5485
+ }
5486
+ }
5487
+ getSnapshot() {
5488
+ return {
5489
+ lastEditAt: this.lastEditAt,
5490
+ lastTestPassAt: this.lastTestPassAt,
5491
+ lastTestFailAt: this.lastTestFailAt
5492
+ };
5493
+ }
5494
+ reset() {
5495
+ this.lastEditAt = null;
5496
+ this.lastTestPassAt = null;
5497
+ this.lastTestFailAt = null;
5498
+ }
5499
+ };
5500
+ sessionHistory = new SessionHistory();
5501
+ }
5502
+ });
5503
+
5294
5504
  // src/daemon/state.ts
5295
5505
  import net2 from "net";
5296
5506
  import fs13 from "fs";
5297
- import path16 from "path";
5298
- import os12 from "os";
5507
+ import path15 from "path";
5508
+ import os11 from "os";
5299
5509
  import { spawn as spawn2 } from "child_process";
5300
5510
  import { randomUUID as randomUUID3 } from "crypto";
5301
5511
  function loadInsightCounts() {
@@ -5340,7 +5550,7 @@ function markRejectionHandlerRegistered() {
5340
5550
  daemonRejectionHandlerRegistered = true;
5341
5551
  }
5342
5552
  function atomicWriteSync2(filePath, data, options) {
5343
- const dir = path16.dirname(filePath);
5553
+ const dir = path15.dirname(filePath);
5344
5554
  if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
5345
5555
  const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
5346
5556
  try {
@@ -5380,7 +5590,7 @@ function appendAuditLog(data) {
5380
5590
  decision: data.decision,
5381
5591
  source: "daemon"
5382
5592
  };
5383
- const dir = path16.dirname(AUDIT_LOG_FILE);
5593
+ const dir = path15.dirname(AUDIT_LOG_FILE);
5384
5594
  if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
5385
5595
  fs13.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
5386
5596
  } catch {
@@ -5529,6 +5739,14 @@ function startActivitySocket() {
5529
5739
  socket.on("end", () => {
5530
5740
  try {
5531
5741
  const data = JSON.parse(Buffer.concat(chunks).toString());
5742
+ if (data.status === "test_pass") {
5743
+ sessionHistory.recordTestPass(data.ts);
5744
+ return;
5745
+ }
5746
+ if (data.status === "test_fail") {
5747
+ sessionHistory.recordTestFail(data.ts);
5748
+ return;
5749
+ }
5532
5750
  if (data.status === "pending") {
5533
5751
  broadcast("activity", {
5534
5752
  id: data.id,
@@ -5538,6 +5756,24 @@ function startActivitySocket() {
5538
5756
  status: "pending"
5539
5757
  });
5540
5758
  } else {
5759
+ if (data.status === "allow") {
5760
+ sessionCounters.incrementAllowed();
5761
+ if (data.observeWouldBlock) sessionCounters.incrementWouldBlock();
5762
+ if (WRITE_TOOL_NAMES.has(data.tool.toLowerCase().replace(/[^a-z_]/g, "_"))) {
5763
+ sessionHistory.recordEdit(data.ts);
5764
+ }
5765
+ } else if (data.status === "block") {
5766
+ sessionCounters.incrementBlocked();
5767
+ sessionCounters.recordBlockedTool(data.tool);
5768
+ if (data.ruleHit) sessionCounters.recordRuleHit(data.ruleHit);
5769
+ } else if (data.status === "dlp") {
5770
+ sessionCounters.incrementBlocked();
5771
+ sessionCounters.incrementDlpHits();
5772
+ sessionCounters.recordBlockedTool(data.tool);
5773
+ } else if (data.status === "taint") {
5774
+ sessionCounters.incrementBlocked();
5775
+ sessionCounters.recordBlockedTool(data.tool);
5776
+ }
5541
5777
  broadcast("activity-result", {
5542
5778
  id: data.id,
5543
5779
  status: data.status,
@@ -5558,21 +5794,23 @@ function startActivitySocket() {
5558
5794
  }
5559
5795
  });
5560
5796
  }
5561
- var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
5797
+ var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, WRITE_TOOL_NAMES;
5562
5798
  var init_state2 = __esm({
5563
5799
  "src/daemon/state.ts"() {
5564
5800
  "use strict";
5565
5801
  init_daemon();
5566
5802
  init_suggestion_tracker();
5567
5803
  init_taint_store();
5568
- homeDir = os12.homedir();
5569
- DAEMON_PID_FILE = path16.join(homeDir, ".node9", "daemon.pid");
5570
- DECISIONS_FILE = path16.join(homeDir, ".node9", "decisions.json");
5571
- AUDIT_LOG_FILE = path16.join(homeDir, ".node9", "audit.log");
5572
- TRUST_FILE2 = path16.join(homeDir, ".node9", "trust.json");
5573
- GLOBAL_CONFIG_FILE = path16.join(homeDir, ".node9", "config.json");
5574
- CREDENTIALS_FILE = path16.join(homeDir, ".node9", "credentials.json");
5575
- INSIGHT_COUNTS_FILE = path16.join(homeDir, ".node9", "insight-counts.json");
5804
+ init_session_counters();
5805
+ init_session_history();
5806
+ homeDir = os11.homedir();
5807
+ DAEMON_PID_FILE = path15.join(homeDir, ".node9", "daemon.pid");
5808
+ DECISIONS_FILE = path15.join(homeDir, ".node9", "decisions.json");
5809
+ AUDIT_LOG_FILE = path15.join(homeDir, ".node9", "audit.log");
5810
+ TRUST_FILE2 = path15.join(homeDir, ".node9", "trust.json");
5811
+ GLOBAL_CONFIG_FILE = path15.join(homeDir, ".node9", "config.json");
5812
+ CREDENTIALS_FILE = path15.join(homeDir, ".node9", "credentials.json");
5813
+ INSIGHT_COUNTS_FILE = path15.join(homeDir, ".node9", "insight-counts.json");
5576
5814
  pending = /* @__PURE__ */ new Map();
5577
5815
  sseClients = /* @__PURE__ */ new Set();
5578
5816
  suggestionTracker = new SuggestionTracker(3);
@@ -5590,17 +5828,28 @@ var init_state2 = __esm({
5590
5828
  "2h": 2 * 60 * 6e4
5591
5829
  };
5592
5830
  autoStarted = process.env.NODE9_AUTO_STARTED === "1";
5593
- ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path16.join(os12.tmpdir(), "node9-activity.sock");
5831
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path15.join(os11.tmpdir(), "node9-activity.sock");
5594
5832
  ACTIVITY_RING_SIZE = 100;
5595
5833
  activityRing = [];
5596
5834
  SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
5835
+ WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
5836
+ "write",
5837
+ "write_file",
5838
+ "create_file",
5839
+ "edit",
5840
+ "multiedit",
5841
+ "str_replace_based_edit_tool",
5842
+ "replace",
5843
+ "notebook_edit",
5844
+ "notebookedit"
5845
+ ]);
5597
5846
  }
5598
5847
  });
5599
5848
 
5600
5849
  // src/config/patch.ts
5601
5850
  import fs14 from "fs";
5602
- import path17 from "path";
5603
- import os13 from "os";
5851
+ import path16 from "path";
5852
+ import os12 from "os";
5604
5853
  function patchConfig(configPath, patch) {
5605
5854
  let config = {};
5606
5855
  try {
@@ -5624,7 +5873,7 @@ function patchConfig(configPath, patch) {
5624
5873
  ignored.push(patch.toolName);
5625
5874
  }
5626
5875
  }
5627
- const dir = path17.dirname(configPath);
5876
+ const dir = path16.dirname(configPath);
5628
5877
  fs14.mkdirSync(dir, { recursive: true });
5629
5878
  const tmp = configPath + ".node9-tmp";
5630
5879
  try {
@@ -5650,14 +5899,14 @@ var GLOBAL_CONFIG_PATH;
5650
5899
  var init_patch = __esm({
5651
5900
  "src/config/patch.ts"() {
5652
5901
  "use strict";
5653
- GLOBAL_CONFIG_PATH = path17.join(os13.homedir(), ".node9", "config.json");
5902
+ GLOBAL_CONFIG_PATH = path16.join(os12.homedir(), ".node9", "config.json");
5654
5903
  }
5655
5904
  });
5656
5905
 
5657
5906
  // src/daemon/server.ts
5658
5907
  import http from "http";
5659
5908
  import fs15 from "fs";
5660
- import path18 from "path";
5909
+ import path17 from "path";
5661
5910
  import { randomUUID as randomUUID4 } from "crypto";
5662
5911
  import { spawnSync as spawnSync2 } from "child_process";
5663
5912
  import chalk2 from "chalk";
@@ -5723,6 +5972,8 @@ data: ${JSON.stringify({
5723
5972
  toolName: e.toolName,
5724
5973
  args: e.args,
5725
5974
  riskMetadata: e.riskMetadata,
5975
+ ...e.recoveryCommand && { recoveryCommand: e.recoveryCommand },
5976
+ ...e.viewOnly && { viewOnly: true },
5726
5977
  slackDelegated: e.slackDelegated,
5727
5978
  timestamp: e.timestamp,
5728
5979
  agent: e.agent,
@@ -5788,6 +6039,9 @@ data: ${JSON.stringify(item.data)}
5788
6039
  agent,
5789
6040
  mcpServer,
5790
6041
  riskMetadata,
6042
+ recoveryCommand,
6043
+ skipBackgroundAuth = false,
6044
+ viewOnly = false,
5791
6045
  fromCLI = false,
5792
6046
  activityId,
5793
6047
  cwd
@@ -5798,6 +6052,8 @@ data: ${JSON.stringify(item.data)}
5798
6052
  toolName,
5799
6053
  args,
5800
6054
  riskMetadata: riskMetadata ?? void 0,
6055
+ ...typeof recoveryCommand === "string" && recoveryCommand && { recoveryCommand },
6056
+ ...viewOnly && { viewOnly: true },
5801
6057
  agent: typeof agent === "string" ? agent : void 0,
5802
6058
  mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
5803
6059
  slackDelegated: !!slackDelegated,
@@ -5832,7 +6088,7 @@ data: ${JSON.stringify(item.data)}
5832
6088
  status: "pending"
5833
6089
  });
5834
6090
  }
5835
- const projectCwd = typeof cwd === "string" && path18.isAbsolute(cwd) ? cwd : void 0;
6091
+ const projectCwd = typeof cwd === "string" && path17.isAbsolute(cwd) ? cwd : void 0;
5836
6092
  const projectConfig = getConfig(projectCwd);
5837
6093
  const browserEnabled = projectConfig.settings.approvers?.browser !== false;
5838
6094
  const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
@@ -5842,6 +6098,8 @@ data: ${JSON.stringify(item.data)}
5842
6098
  toolName,
5843
6099
  args,
5844
6100
  riskMetadata: entry.riskMetadata,
6101
+ ...entry.recoveryCommand && { recoveryCommand: entry.recoveryCommand },
6102
+ ...entry.viewOnly && { viewOnly: true },
5845
6103
  slackDelegated: entry.slackDelegated,
5846
6104
  agent: entry.agent,
5847
6105
  mcpServer: entry.mcpServer,
@@ -5858,7 +6116,7 @@ data: ${JSON.stringify(item.data)}
5858
6116
  }
5859
6117
  res.writeHead(200, { "Content-Type": "application/json" });
5860
6118
  res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
5861
- if (slackDelegated) return;
6119
+ if (slackDelegated || skipBackgroundAuth) return;
5862
6120
  authorizeHeadless(
5863
6121
  toolName,
5864
6122
  args,
@@ -5997,7 +6255,7 @@ data: ${JSON.stringify(item.data)}
5997
6255
  saveInsightCounts();
5998
6256
  suggestionTracker.resetTool(entry.toolName);
5999
6257
  }
6000
- const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
6258
+ const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native", "terminal-redirect"]);
6001
6259
  if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
6002
6260
  if (entry.waiter) {
6003
6261
  entry.waiter(resolvedDecision, reason);
@@ -6026,6 +6284,41 @@ data: ${JSON.stringify(item.data)}
6026
6284
  return res.end(JSON.stringify({ error: "internal" }));
6027
6285
  }
6028
6286
  }
6287
+ if (req.method === "GET" && pathname === "/status") {
6288
+ try {
6289
+ const s = getGlobalSettings();
6290
+ const counters = sessionCounters.get();
6291
+ const mode = s.mode ?? "standard";
6292
+ const status = {
6293
+ mode,
6294
+ session: {
6295
+ allowed: counters.allowed,
6296
+ blocked: counters.blocked,
6297
+ dlpHits: counters.dlpHits,
6298
+ wouldBlock: counters.wouldBlock
6299
+ },
6300
+ taintedCount: taintStore.list().length,
6301
+ lastRuleHit: counters.lastRuleHit,
6302
+ lastBlockedTool: counters.lastBlockedTool
6303
+ };
6304
+ res.writeHead(200, { "Content-Type": "application/json" });
6305
+ return res.end(JSON.stringify(status));
6306
+ } catch (err) {
6307
+ console.error(chalk2.red("[node9 daemon] GET /status failed:"), err);
6308
+ res.writeHead(500, { "Content-Type": "application/json" });
6309
+ return res.end(JSON.stringify({ error: "internal" }));
6310
+ }
6311
+ }
6312
+ if (req.method === "GET" && pathname === "/state/check") {
6313
+ const predicatesParam = reqUrl.searchParams.get("predicates") ?? "";
6314
+ const predicates = predicatesParam.split(",").filter(Boolean);
6315
+ const results = {};
6316
+ for (const p of predicates) {
6317
+ results[p] = sessionHistory.checkPredicate(p);
6318
+ }
6319
+ res.writeHead(200, { "Content-Type": "application/json" });
6320
+ return res.end(JSON.stringify(results));
6321
+ }
6029
6322
  if (req.method === "POST" && pathname === "/settings") {
6030
6323
  if (!validToken(req)) return res.writeHead(403).end();
6031
6324
  try {
@@ -6183,8 +6476,8 @@ data: ${JSON.stringify(item.data)}
6183
6476
  const body = await readBody(req);
6184
6477
  const data = body ? JSON.parse(body) : {};
6185
6478
  const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
6186
- const node9Dir = path18.dirname(GLOBAL_CONFIG_PATH);
6187
- if (!path18.resolve(configPath).startsWith(node9Dir + path18.sep)) {
6479
+ const node9Dir = path17.dirname(GLOBAL_CONFIG_PATH);
6480
+ if (!path17.resolve(configPath).startsWith(node9Dir + path17.sep)) {
6188
6481
  res.writeHead(400, { "Content-Type": "application/json" });
6189
6482
  return res.end(
6190
6483
  JSON.stringify({ error: "configPath must be within the node9 config directory" })
@@ -6431,8 +6724,8 @@ __export(tail_exports, {
6431
6724
  import http2 from "http";
6432
6725
  import chalk16 from "chalk";
6433
6726
  import fs24 from "fs";
6434
- import os21 from "os";
6435
- import path26 from "path";
6727
+ import os20 from "os";
6728
+ import path25 from "path";
6436
6729
  import readline3 from "readline";
6437
6730
  import { spawn as spawn9, execSync as execSync3 } from "child_process";
6438
6731
  function getIcon(tool) {
@@ -6510,9 +6803,10 @@ async function ensureDaemon() {
6510
6803
  }
6511
6804
  function postDecisionHttp(id, decision, csrfToken, port, opts) {
6512
6805
  return new Promise((resolve, reject) => {
6513
- const bodyObj = { decision, source: "terminal" };
6806
+ const bodyObj = { decision, source: opts?.source ?? "terminal" };
6514
6807
  if (opts?.persist) bodyObj.persist = true;
6515
6808
  if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
6809
+ if (opts?.reason) bodyObj.reason = opts.reason;
6516
6810
  const body = JSON.stringify(bodyObj);
6517
6811
  const req = http2.request(
6518
6812
  {
@@ -6537,6 +6831,9 @@ function postDecisionHttp(id, decision, csrfToken, port, opts) {
6537
6831
  });
6538
6832
  }
6539
6833
  function buildCardLines(req, localCount = 0) {
6834
+ if (req.recoveryCommand) {
6835
+ return buildRecoveryCardLines(req);
6836
+ }
6540
6837
  const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
6541
6838
  const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
6542
6839
  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`;
@@ -6564,6 +6861,31 @@ function buildCardLines(req, localCount = 0) {
6564
6861
  );
6565
6862
  return lines;
6566
6863
  }
6864
+ function buildRecoveryCardLines(req) {
6865
+ const argsObj = req.args;
6866
+ const command = typeof argsObj?.command === "string" ? argsObj.command : JSON.stringify(req.args ?? {}).replace(/\s+/g, " ").slice(0, 60);
6867
+ const ruleName = req.riskMetadata?.ruleName?.replace(/^Smart Rule:\s*/i, "") ?? "policy rule";
6868
+ const recoveryCommand = req.recoveryCommand;
6869
+ const interactiveLines = req.viewOnly ? [` ${GRAY}\u2192 Awaiting decision from interactive terminal...${RESET}`] : [
6870
+ ` ${BOLD}${GREEN}[1]${RESET} Allow anyway ${GRAY}(override policy)${RESET}`,
6871
+ ` ${BOLD}${YELLOW}[2]${RESET} Redirect AI: "Run '${recoveryCommand}' first, then retry"`,
6872
+ ` ${BOLD}${RED}[3]${RESET} Deny & stop ${GRAY}(hard block)${RESET}`,
6873
+ ``,
6874
+ ` ${GRAY}[Timeout: auto-deny]${RESET}`,
6875
+ ` Select [1-3]: `
6876
+ ];
6877
+ return [
6878
+ ``,
6879
+ `${BOLD}${CYAN}${DIVIDER}${RESET}`,
6880
+ `\u{1F6E1}\uFE0F ${BOLD}NODE9 STATE GUARD:${RESET} '${BOLD}${command}${RESET}'`,
6881
+ `${YELLOW}\u26A0\uFE0F Rule: ${ruleName}${RESET}`,
6882
+ `${CYAN}${DIVIDER}${RESET}`,
6883
+ ...!req.viewOnly ? [`${BOLD}What would you like to do?${RESET}`, ``] : [],
6884
+ ...interactiveLines,
6885
+ `${CYAN}${DIVIDER}${RESET}`,
6886
+ ``
6887
+ ];
6888
+ }
6567
6889
  async function startTail(options = {}) {
6568
6890
  const port = await ensureDaemon();
6569
6891
  if (options.clear) {
@@ -6662,14 +6984,14 @@ async function startTail(options = {}) {
6662
6984
  localAllowCounts.get(req2.toolName) ?? 0
6663
6985
  )
6664
6986
  );
6665
- const decisionStamp = action === "always-allow" ? chalk16.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk16.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk16.green("\u2713 ALLOWED") : chalk16.red("\u2717 DENIED");
6987
+ const decisionStamp = action === "always-allow" ? chalk16.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk16.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk16.green("\u2713 ALLOWED") : action === "redirect" ? chalk16.yellow("\u21A9 REDIRECT AI") : chalk16.red("\u2717 DENIED");
6666
6988
  stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
6667
6989
  for (const line of stampedLines) process.stdout.write(line + "\n");
6668
6990
  process.stdout.write(SHOW_CURSOR);
6669
6991
  cardLineCount = 0;
6670
6992
  if (action === "allow" || action === "always-allow" || action === "trust") {
6671
6993
  localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
6672
- } else if (action === "deny") {
6994
+ } else if (action === "deny" || action === "redirect") {
6673
6995
  localAllowCounts.delete(req2.toolName);
6674
6996
  }
6675
6997
  let httpDecision;
@@ -6680,13 +7002,18 @@ async function startTail(options = {}) {
6680
7002
  } else if (action === "trust") {
6681
7003
  httpDecision = "trust";
6682
7004
  httpOpts = { trustDuration: "30m" };
7005
+ } else if (action === "redirect") {
7006
+ httpDecision = "deny";
7007
+ const recoveryCommand = req2.recoveryCommand ?? "the required pre-condition";
7008
+ 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}\`.`;
7009
+ httpOpts = { reason: redirectReason, source: "terminal-redirect" };
6683
7010
  } else {
6684
7011
  httpDecision = action;
6685
7012
  }
6686
7013
  postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
6687
7014
  try {
6688
7015
  fs24.appendFileSync(
6689
- path26.join(os21.homedir(), ".node9", "hook-debug.log"),
7016
+ path25.join(os20.homedir(), ".node9", "hook-debug.log"),
6690
7017
  `[tail] POST /decision failed: ${String(err)}
6691
7018
  `
6692
7019
  );
@@ -6718,17 +7045,34 @@ async function startTail(options = {}) {
6718
7045
  cardActive = false;
6719
7046
  showNextCard();
6720
7047
  };
7048
+ if (req2.viewOnly) {
7049
+ process.stdin.resume();
7050
+ onKeypress = () => {
7051
+ };
7052
+ process.stdin.on("keypress", onKeypress);
7053
+ return;
7054
+ }
6721
7055
  process.stdin.resume();
6722
7056
  onKeypress = (_str, key) => {
6723
7057
  const name = key?.name ?? "";
6724
- if (name === "y" || name === "return") {
6725
- settle("allow");
6726
- } else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
6727
- settle("deny");
6728
- } else if (name === "a") {
6729
- settle("always-allow");
6730
- } else if (name === "t") {
6731
- settle("trust");
7058
+ if (req2.recoveryCommand) {
7059
+ if (name === "1") {
7060
+ settle("allow");
7061
+ } else if (name === "2") {
7062
+ settle("redirect");
7063
+ } else if (name === "3" || key?.ctrl && name === "c") {
7064
+ settle("deny");
7065
+ }
7066
+ } else {
7067
+ if (name === "y" || name === "return") {
7068
+ settle("allow");
7069
+ } else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
7070
+ settle("deny");
7071
+ } else if (name === "a") {
7072
+ settle("always-allow");
7073
+ } else if (name === "t") {
7074
+ settle("trust");
7075
+ }
6732
7076
  }
6733
7077
  };
6734
7078
  process.stdin.on("keypress", onKeypress);
@@ -6898,14 +7242,14 @@ async function startTail(options = {}) {
6898
7242
  process.exit(1);
6899
7243
  });
6900
7244
  }
6901
- var PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN;
7245
+ var PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
6902
7246
  var init_tail = __esm({
6903
7247
  "src/tui/tail.ts"() {
6904
7248
  "use strict";
6905
7249
  init_daemon2();
6906
7250
  init_daemon();
6907
7251
  init_core();
6908
- PID_FILE = path26.join(os21.homedir(), ".node9", "daemon.pid");
7252
+ PID_FILE = path25.join(os20.homedir(), ".node9", "daemon.pid");
6909
7253
  ICONS = {
6910
7254
  bash: "\u{1F4BB}",
6911
7255
  shell: "\u{1F4BB}",
@@ -6933,6 +7277,326 @@ var init_tail = __esm({
6933
7277
  HIDE_CURSOR = "\x1B[?25l";
6934
7278
  SHOW_CURSOR = "\x1B[?25h";
6935
7279
  ERASE_DOWN = "\x1B[J";
7280
+ DIVIDER = "\u2500".repeat(60);
7281
+ }
7282
+ });
7283
+
7284
+ // src/cli/hud.ts
7285
+ var hud_exports = {};
7286
+ __export(hud_exports, {
7287
+ countConfigs: () => countConfigs,
7288
+ main: () => main,
7289
+ renderEnvironmentLine: () => renderEnvironmentLine
7290
+ });
7291
+ import fs25 from "fs";
7292
+ import path26 from "path";
7293
+ import os21 from "os";
7294
+ import http3 from "http";
7295
+ async function readStdin() {
7296
+ const chunks = [];
7297
+ for await (const chunk of process.stdin) {
7298
+ chunks.push(chunk);
7299
+ }
7300
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
7301
+ if (!raw) return {};
7302
+ try {
7303
+ return JSON.parse(raw);
7304
+ } catch {
7305
+ return {};
7306
+ }
7307
+ }
7308
+ function queryDaemon() {
7309
+ return new Promise((resolve) => {
7310
+ const timeout = setTimeout(() => resolve(null), 50);
7311
+ try {
7312
+ const req = http3.get(
7313
+ `http://${DAEMON_HOST}:${DAEMON_PORT}/status`,
7314
+ { timeout: 50 },
7315
+ (res) => {
7316
+ const chunks = [];
7317
+ res.on("data", (c) => chunks.push(c));
7318
+ res.on("end", () => {
7319
+ clearTimeout(timeout);
7320
+ try {
7321
+ resolve(JSON.parse(Buffer.concat(chunks).toString()));
7322
+ } catch {
7323
+ resolve(null);
7324
+ }
7325
+ });
7326
+ }
7327
+ );
7328
+ req.on("error", () => {
7329
+ clearTimeout(timeout);
7330
+ resolve(null);
7331
+ });
7332
+ req.on("timeout", () => {
7333
+ clearTimeout(timeout);
7334
+ req.destroy();
7335
+ resolve(null);
7336
+ });
7337
+ } catch {
7338
+ clearTimeout(timeout);
7339
+ resolve(null);
7340
+ }
7341
+ });
7342
+ }
7343
+ function dim(s) {
7344
+ return `${DIM}${s}${RESET2}`;
7345
+ }
7346
+ function bold(s) {
7347
+ return `${BOLD2}${s}${RESET2}`;
7348
+ }
7349
+ function color(c, s) {
7350
+ return `${c}${s}${RESET2}`;
7351
+ }
7352
+ function progressBar(pct, warnAt = 70, critAt = 85) {
7353
+ const filled = Math.round(Math.min(pct, 100) / 100 * BAR_WIDTH);
7354
+ const bar = BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
7355
+ const c = pct >= critAt ? RED2 : pct >= warnAt ? YELLOW2 : GREEN2;
7356
+ return `${c}${bar}${RESET2}`;
7357
+ }
7358
+ function formatTimeLeft(resetsAt) {
7359
+ if (!resetsAt) return "";
7360
+ const ms = new Date(resetsAt).getTime() - Date.now();
7361
+ if (ms <= 0) return "";
7362
+ const totalMin = Math.ceil(ms / 6e4);
7363
+ const h = Math.floor(totalMin / 60);
7364
+ const m = totalMin % 60;
7365
+ if (h > 0) return ` (${h}h ${m}m left)`;
7366
+ return ` (${m}m left)`;
7367
+ }
7368
+ function safeReadJson(filePath) {
7369
+ if (!fs25.existsSync(filePath)) return null;
7370
+ try {
7371
+ return JSON.parse(fs25.readFileSync(filePath, "utf-8"));
7372
+ } catch {
7373
+ return null;
7374
+ }
7375
+ }
7376
+ function getMcpServerNames(filePath) {
7377
+ const cfg = safeReadJson(filePath);
7378
+ if (!cfg || typeof cfg.mcpServers !== "object" || cfg.mcpServers === null) return /* @__PURE__ */ new Set();
7379
+ return new Set(Object.keys(cfg.mcpServers));
7380
+ }
7381
+ function getDisabledMcpServers(filePath, key) {
7382
+ const cfg = safeReadJson(filePath);
7383
+ if (!cfg || !Array.isArray(cfg[key])) return /* @__PURE__ */ new Set();
7384
+ return new Set(cfg[key].filter((s) => typeof s === "string"));
7385
+ }
7386
+ function countHooksInFile(filePath) {
7387
+ const cfg = safeReadJson(filePath);
7388
+ if (!cfg || typeof cfg.hooks !== "object" || cfg.hooks === null) return 0;
7389
+ return Object.keys(cfg.hooks).length;
7390
+ }
7391
+ function countRulesInDir(rulesDir) {
7392
+ if (!fs25.existsSync(rulesDir)) return 0;
7393
+ let count = 0;
7394
+ try {
7395
+ for (const entry of fs25.readdirSync(rulesDir, { withFileTypes: true })) {
7396
+ if (entry.isDirectory()) {
7397
+ count += countRulesInDir(path26.join(rulesDir, entry.name));
7398
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
7399
+ count++;
7400
+ }
7401
+ }
7402
+ } catch {
7403
+ }
7404
+ return count;
7405
+ }
7406
+ function isSamePath(a, b) {
7407
+ try {
7408
+ return path26.resolve(a) === path26.resolve(b);
7409
+ } catch {
7410
+ return false;
7411
+ }
7412
+ }
7413
+ function countConfigs(cwd) {
7414
+ const homeDir2 = os21.homedir();
7415
+ const claudeDir = path26.join(homeDir2, ".claude");
7416
+ let claudeMdCount = 0;
7417
+ let rulesCount = 0;
7418
+ let hooksCount = 0;
7419
+ const userMcpServers = /* @__PURE__ */ new Set();
7420
+ const projectMcpServers = /* @__PURE__ */ new Set();
7421
+ if (fs25.existsSync(path26.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7422
+ rulesCount += countRulesInDir(path26.join(claudeDir, "rules"));
7423
+ const userSettings = path26.join(claudeDir, "settings.json");
7424
+ for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
7425
+ hooksCount += countHooksInFile(userSettings);
7426
+ const userClaudeJson = path26.join(homeDir2, ".claude.json");
7427
+ for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
7428
+ for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
7429
+ userMcpServers.delete(name);
7430
+ }
7431
+ if (cwd) {
7432
+ if (fs25.existsSync(path26.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7433
+ if (fs25.existsSync(path26.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7434
+ const projectClaudeDir = path26.join(cwd, ".claude");
7435
+ const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
7436
+ if (!overlapsUserScope) {
7437
+ if (fs25.existsSync(path26.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7438
+ rulesCount += countRulesInDir(path26.join(projectClaudeDir, "rules"));
7439
+ const projSettings = path26.join(projectClaudeDir, "settings.json");
7440
+ for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
7441
+ hooksCount += countHooksInFile(projSettings);
7442
+ }
7443
+ if (fs25.existsSync(path26.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7444
+ const localSettings = path26.join(projectClaudeDir, "settings.local.json");
7445
+ for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
7446
+ hooksCount += countHooksInFile(localSettings);
7447
+ const mcpJsonServers = getMcpServerNames(path26.join(cwd, ".mcp.json"));
7448
+ const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
7449
+ for (const name of disabledMcpJson) mcpJsonServers.delete(name);
7450
+ for (const name of mcpJsonServers) projectMcpServers.add(name);
7451
+ }
7452
+ return {
7453
+ claudeMdCount,
7454
+ rulesCount,
7455
+ mcpCount: userMcpServers.size + projectMcpServers.size,
7456
+ hooksCount
7457
+ };
7458
+ }
7459
+ function renderEnvironmentLine(counts) {
7460
+ const { claudeMdCount, rulesCount, mcpCount, hooksCount } = counts;
7461
+ if (claudeMdCount === 0 && rulesCount === 0 && mcpCount === 0 && hooksCount === 0) return null;
7462
+ const parts = [
7463
+ `${claudeMdCount} CLAUDE.md`,
7464
+ `${rulesCount} rules`,
7465
+ `${mcpCount} MCPs`,
7466
+ `${hooksCount} hooks`
7467
+ ];
7468
+ return color(DIM, parts.join(` ${dim("|")} `));
7469
+ }
7470
+ function renderOffline() {
7471
+ process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
7472
+ `);
7473
+ }
7474
+ function renderSecurityLine(status) {
7475
+ const parts = [];
7476
+ parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
7477
+ const modeColors = {
7478
+ standard: GREEN2,
7479
+ strict: RED2,
7480
+ observe: MAGENTA,
7481
+ audit: YELLOW2
7482
+ };
7483
+ const modeIcon = {
7484
+ standard: "",
7485
+ strict: "",
7486
+ observe: "\u{1F441} ",
7487
+ audit: ""
7488
+ };
7489
+ const mc = modeColors[status.mode] ?? WHITE;
7490
+ parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
7491
+ if (status.mode === "observe") {
7492
+ parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
7493
+ if (status.session.wouldBlock > 0) {
7494
+ parts.push(color(YELLOW2, `\u26A0 ${status.session.wouldBlock} would-block`));
7495
+ }
7496
+ } else {
7497
+ parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} allowed`)}`);
7498
+ if (status.session.blocked > 0) {
7499
+ parts.push(color(RED2, `\u{1F6D1} ${status.session.blocked} blocked`));
7500
+ }
7501
+ if (status.session.dlpHits > 0) {
7502
+ parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
7503
+ }
7504
+ }
7505
+ if (status.taintedCount > 0) {
7506
+ parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
7507
+ }
7508
+ if (status.lastRuleHit) {
7509
+ const ruleName = status.lastRuleHit.replace(/^Smart Rule:\s*/i, "");
7510
+ parts.push(color(CYAN2, `\u26A1 ${ruleName}`));
7511
+ }
7512
+ return parts.join(" ");
7513
+ }
7514
+ function renderContextLine(stdin) {
7515
+ const cw = stdin.context_window;
7516
+ if (!cw) return null;
7517
+ const parts = [];
7518
+ const modelName = typeof stdin.model === "string" ? stdin.model : stdin.model?.display_name ?? "";
7519
+ if (modelName) {
7520
+ parts.push(color(CYAN2, modelName));
7521
+ }
7522
+ const usedPct = cw.used_percentage ?? (cw.current_usage && cw.context_window_size ? Math.round(
7523
+ ((cw.current_usage.input_tokens ?? 0) + (cw.current_usage.output_tokens ?? 0)) / cw.context_window_size * 100
7524
+ ) : null);
7525
+ if (usedPct !== null) {
7526
+ const bar = progressBar(usedPct);
7527
+ parts.push(`${dim("\u2502")} ctx ${bar} ${usedPct}%`);
7528
+ }
7529
+ const rl = stdin.rate_limits;
7530
+ if (rl?.five_hour?.used_percentage !== void 0) {
7531
+ const pct = Math.round(rl.five_hour.used_percentage);
7532
+ const bar = progressBar(pct, 60, 80);
7533
+ const left = formatTimeLeft(rl.five_hour.resets_at);
7534
+ parts.push(`${dim("\u2502")} 5h ${bar} ${pct}%${left}`);
7535
+ }
7536
+ if (rl?.seven_day?.used_percentage !== void 0) {
7537
+ const pct = Math.round(rl.seven_day.used_percentage);
7538
+ const bar = progressBar(pct, 60, 80);
7539
+ parts.push(`${dim("\u2502")} 7d ${bar} ${pct}%`);
7540
+ }
7541
+ if (parts.length === 0) return null;
7542
+ return parts.join(" ");
7543
+ }
7544
+ async function main() {
7545
+ try {
7546
+ const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
7547
+ if (!daemonStatus2) {
7548
+ renderOffline();
7549
+ return;
7550
+ }
7551
+ process.stdout.write(renderSecurityLine(daemonStatus2) + "\n");
7552
+ const ctxLine = renderContextLine(stdin);
7553
+ if (ctxLine) {
7554
+ process.stdout.write(ctxLine + "\n");
7555
+ }
7556
+ const showEnvCounts = (() => {
7557
+ try {
7558
+ const cwd = stdin.cwd ?? process.cwd();
7559
+ for (const configPath of [
7560
+ path26.join(cwd, "node9.config.json"),
7561
+ path26.join(os21.homedir(), ".node9", "config.json")
7562
+ ]) {
7563
+ if (!fs25.existsSync(configPath)) continue;
7564
+ const cfg = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
7565
+ const hud = cfg.settings?.hud;
7566
+ if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
7567
+ }
7568
+ } catch {
7569
+ }
7570
+ return true;
7571
+ })();
7572
+ if (showEnvCounts) {
7573
+ const envLine = renderEnvironmentLine(countConfigs(stdin.cwd));
7574
+ if (envLine) {
7575
+ process.stdout.write(envLine + "\n");
7576
+ }
7577
+ }
7578
+ } catch {
7579
+ renderOffline();
7580
+ }
7581
+ }
7582
+ var RESET2, BOLD2, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
7583
+ var init_hud = __esm({
7584
+ "src/cli/hud.ts"() {
7585
+ "use strict";
7586
+ init_daemon();
7587
+ RESET2 = "\x1B[0m";
7588
+ BOLD2 = "\x1B[1m";
7589
+ DIM = "\x1B[2m";
7590
+ RED2 = "\x1B[31m";
7591
+ GREEN2 = "\x1B[32m";
7592
+ YELLOW2 = "\x1B[33m";
7593
+ BLUE = "\x1B[34m";
7594
+ MAGENTA = "\x1B[35m";
7595
+ CYAN2 = "\x1B[36m";
7596
+ WHITE = "\x1B[37m";
7597
+ BAR_FILLED = "\u2588";
7598
+ BAR_EMPTY = "\u2591";
7599
+ BAR_WIDTH = 10;
6936
7600
  }
6937
7601
  });
6938
7602
 
@@ -6942,8 +7606,8 @@ import { Command } from "commander";
6942
7606
 
6943
7607
  // src/setup.ts
6944
7608
  import fs11 from "fs";
6945
- import path14 from "path";
6946
- import os11 from "os";
7609
+ import path13 from "path";
7610
+ import os10 from "os";
6947
7611
  import chalk from "chalk";
6948
7612
  import { confirm } from "@inquirer/prompts";
6949
7613
  function printDaemonTip() {
@@ -6968,7 +7632,7 @@ function readJson(filePath) {
6968
7632
  return null;
6969
7633
  }
6970
7634
  function writeJson(filePath, data) {
6971
- const dir = path14.dirname(filePath);
7635
+ const dir = path13.dirname(filePath);
6972
7636
  if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
6973
7637
  fs11.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
6974
7638
  }
@@ -6977,9 +7641,9 @@ function isNode9Hook(cmd) {
6977
7641
  return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
6978
7642
  }
6979
7643
  function teardownClaude() {
6980
- const homeDir2 = os11.homedir();
6981
- const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
6982
- const mcpPath = path14.join(homeDir2, ".claude.json");
7644
+ const homeDir2 = os10.homedir();
7645
+ const hooksPath = path13.join(homeDir2, ".claude", "settings.json");
7646
+ const mcpPath = path13.join(homeDir2, ".claude.json");
6983
7647
  let changed = false;
6984
7648
  const settings = readJson(hooksPath);
6985
7649
  if (settings?.hooks) {
@@ -7027,8 +7691,8 @@ function teardownClaude() {
7027
7691
  }
7028
7692
  }
7029
7693
  function teardownGemini() {
7030
- const homeDir2 = os11.homedir();
7031
- const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
7694
+ const homeDir2 = os10.homedir();
7695
+ const settingsPath = path13.join(homeDir2, ".gemini", "settings.json");
7032
7696
  const settings = readJson(settingsPath);
7033
7697
  if (!settings) {
7034
7698
  console.log(chalk.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
@@ -7066,8 +7730,8 @@ function teardownGemini() {
7066
7730
  }
7067
7731
  }
7068
7732
  function teardownCursor() {
7069
- const homeDir2 = os11.homedir();
7070
- const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
7733
+ const homeDir2 = os10.homedir();
7734
+ const mcpPath = path13.join(homeDir2, ".cursor", "mcp.json");
7071
7735
  const mcpConfig = readJson(mcpPath);
7072
7736
  if (!mcpConfig?.mcpServers) {
7073
7737
  console.log(chalk.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
@@ -7093,9 +7757,9 @@ function teardownCursor() {
7093
7757
  }
7094
7758
  }
7095
7759
  async function setupClaude() {
7096
- const homeDir2 = os11.homedir();
7097
- const mcpPath = path14.join(homeDir2, ".claude.json");
7098
- const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
7760
+ const homeDir2 = os10.homedir();
7761
+ const mcpPath = path13.join(homeDir2, ".claude.json");
7762
+ const hooksPath = path13.join(homeDir2, ".claude", "settings.json");
7099
7763
  const claudeConfig = readJson(mcpPath) ?? {};
7100
7764
  const settings = readJson(hooksPath) ?? {};
7101
7765
  const servers = claudeConfig.mcpServers ?? {};
@@ -7169,8 +7833,8 @@ async function setupClaude() {
7169
7833
  }
7170
7834
  }
7171
7835
  async function setupGemini() {
7172
- const homeDir2 = os11.homedir();
7173
- const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
7836
+ const homeDir2 = os10.homedir();
7837
+ const settingsPath = path13.join(homeDir2, ".gemini", "settings.json");
7174
7838
  const settings = readJson(settingsPath) ?? {};
7175
7839
  const servers = settings.mcpServers ?? {};
7176
7840
  let anythingChanged = false;
@@ -7251,7 +7915,7 @@ async function setupGemini() {
7251
7915
  printDaemonTip();
7252
7916
  }
7253
7917
  }
7254
- function detectAgents(homeDir2 = os11.homedir()) {
7918
+ function detectAgents(homeDir2 = os10.homedir()) {
7255
7919
  const exists = (p) => {
7256
7920
  try {
7257
7921
  return fs11.existsSync(p);
@@ -7265,14 +7929,14 @@ function detectAgents(homeDir2 = os11.homedir()) {
7265
7929
  }
7266
7930
  };
7267
7931
  return {
7268
- claude: exists(path14.join(homeDir2, ".claude")) || exists(path14.join(homeDir2, ".claude.json")),
7269
- gemini: exists(path14.join(homeDir2, ".gemini")),
7270
- cursor: exists(path14.join(homeDir2, ".cursor"))
7932
+ claude: exists(path13.join(homeDir2, ".claude")) || exists(path13.join(homeDir2, ".claude.json")),
7933
+ gemini: exists(path13.join(homeDir2, ".gemini")),
7934
+ cursor: exists(path13.join(homeDir2, ".cursor"))
7271
7935
  };
7272
7936
  }
7273
7937
  async function setupCursor() {
7274
- const homeDir2 = os11.homedir();
7275
- const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
7938
+ const homeDir2 = os10.homedir();
7939
+ const mcpPath = path13.join(homeDir2, ".cursor", "mcp.json");
7276
7940
  const mcpConfig = readJson(mcpPath) ?? {};
7277
7941
  const servers = mcpConfig.mcpServers ?? {};
7278
7942
  let anythingChanged = false;
@@ -7325,11 +7989,57 @@ async function setupCursor() {
7325
7989
  printDaemonTip();
7326
7990
  }
7327
7991
  }
7992
+ function setupHud() {
7993
+ const homeDir2 = os10.homedir();
7994
+ const hooksPath = path13.join(homeDir2, ".claude", "settings.json");
7995
+ const settings = readJson(hooksPath) ?? {};
7996
+ const hudCommand = fullPathCommand("hud");
7997
+ const statusLineObj = { type: "command", command: hudCommand };
7998
+ const existing = settings.statusLine;
7999
+ const existingCommand = typeof existing === "object" ? existing?.command : existing;
8000
+ if (existingCommand === hudCommand) {
8001
+ console.log(chalk.blue("\u2139\uFE0F node9 HUD is already configured in ~/.claude/settings.json"));
8002
+ console.log(chalk.gray(" Restart Claude Code to activate."));
8003
+ return;
8004
+ }
8005
+ if (existing && existingCommand !== hudCommand) {
8006
+ console.log(
8007
+ chalk.yellow(
8008
+ ` \u26A0\uFE0F statusLine is already set to: "${existingCommand}"
8009
+ Overwriting with node9 HUD.`
8010
+ )
8011
+ );
8012
+ }
8013
+ settings.statusLine = statusLineObj;
8014
+ writeJson(hooksPath, settings);
8015
+ console.log(chalk.green.bold("\u2705 node9 HUD added to Claude Code statusline"));
8016
+ console.log(chalk.gray(" Settings: ~/.claude/settings.json"));
8017
+ console.log(chalk.gray(" Restart Claude Code to activate."));
8018
+ }
8019
+ function teardownHud() {
8020
+ const homeDir2 = os10.homedir();
8021
+ const hooksPath = path13.join(homeDir2, ".claude", "settings.json");
8022
+ const settings = readJson(hooksPath);
8023
+ if (!settings) {
8024
+ console.log(chalk.blue(" \u2139\uFE0F ~/.claude/settings.json not found \u2014 nothing to remove"));
8025
+ return;
8026
+ }
8027
+ const existing = settings.statusLine;
8028
+ const existingCommand = typeof existing === "object" ? existing?.command : existing;
8029
+ if (!existingCommand || !String(existingCommand).includes("node9")) {
8030
+ console.log(chalk.blue(" \u2139\uFE0F node9 HUD not found in ~/.claude/settings.json"));
8031
+ return;
8032
+ }
8033
+ delete settings.statusLine;
8034
+ writeJson(hooksPath, settings);
8035
+ console.log(chalk.green(" \u2705 node9 HUD removed from ~/.claude/settings.json"));
8036
+ console.log(chalk.gray(" Restart Claude Code for changes to take effect."));
8037
+ }
7328
8038
 
7329
8039
  // src/cli.ts
7330
8040
  init_daemon2();
7331
8041
  import chalk17 from "chalk";
7332
- import fs25 from "fs";
8042
+ import fs26 from "fs";
7333
8043
  import path27 from "path";
7334
8044
  import os22 from "os";
7335
8045
  import { confirm as confirm3 } from "@inquirer/prompts";
@@ -7362,7 +8072,7 @@ import { execa } from "execa";
7362
8072
  import { parseCommandString } from "execa";
7363
8073
 
7364
8074
  // src/policy/negotiation.ts
7365
- function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason) {
8075
+ function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason, recoveryCommand) {
7366
8076
  if (isHumanDecision) {
7367
8077
  return `NODE9: The human user rejected this action.
7368
8078
  REASON: ${humanReason || "No specific reason provided."}
@@ -7418,10 +8128,11 @@ INSTRUCTION: Inform the user this action is pending approval. Wait for them to a
7418
8128
  INSTRUCTION: Do NOT use "${rule}". Find a read-only or non-destructive alternative.
7419
8129
  Do NOT attempt to bypass this rule.`;
7420
8130
  }
8131
+ const recovery = recoveryCommand ? `
8132
+ REQUIRED ACTION: Run \`${recoveryCommand}\` first, then retry your original command.` : "\n- Pivot to a non-destructive or read-only alternative.";
7421
8133
  return `NODE9: Action blocked by security policy [${blockedByLabel}].
7422
8134
  INSTRUCTIONS:
7423
- - Do NOT retry this exact command or attempt to bypass the rule.
7424
- - Pivot to a non-destructive or read-only alternative.
8135
+ - Do NOT retry this exact command or attempt to bypass the rule.${recovery}
7425
8136
  - Inform the user which security rule was triggered and ask how to proceed.`;
7426
8137
  }
7427
8138
 
@@ -7557,17 +8268,17 @@ init_config();
7557
8268
  init_policy();
7558
8269
  import chalk5 from "chalk";
7559
8270
  import fs18 from "fs";
7560
- import path20 from "path";
7561
- import os15 from "os";
8271
+ import path19 from "path";
8272
+ import os14 from "os";
7562
8273
 
7563
8274
  // src/undo.ts
7564
8275
  import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
7565
8276
  import crypto2 from "crypto";
7566
8277
  import fs17 from "fs";
7567
- import path19 from "path";
7568
- import os14 from "os";
7569
- var SNAPSHOT_STACK_PATH = path19.join(os14.homedir(), ".node9", "snapshots.json");
7570
- var UNDO_LATEST_PATH = path19.join(os14.homedir(), ".node9", "undo_latest.txt");
8278
+ import path18 from "path";
8279
+ import os13 from "os";
8280
+ var SNAPSHOT_STACK_PATH = path18.join(os13.homedir(), ".node9", "snapshots.json");
8281
+ var UNDO_LATEST_PATH = path18.join(os13.homedir(), ".node9", "undo_latest.txt");
7571
8282
  var MAX_SNAPSHOTS = 10;
7572
8283
  var GIT_TIMEOUT = 15e3;
7573
8284
  function readStack() {
@@ -7579,7 +8290,7 @@ function readStack() {
7579
8290
  return [];
7580
8291
  }
7581
8292
  function writeStack(stack) {
7582
- const dir = path19.dirname(SNAPSHOT_STACK_PATH);
8293
+ const dir = path18.dirname(SNAPSHOT_STACK_PATH);
7583
8294
  if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
7584
8295
  fs17.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
7585
8296
  }
@@ -7607,14 +8318,14 @@ function normalizeCwdForHash(cwd) {
7607
8318
  }
7608
8319
  function getShadowRepoDir(cwd) {
7609
8320
  const hash = crypto2.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
7610
- return path19.join(os14.homedir(), ".node9", "snapshots", hash);
8321
+ return path18.join(os13.homedir(), ".node9", "snapshots", hash);
7611
8322
  }
7612
8323
  function cleanOrphanedIndexFiles(shadowDir) {
7613
8324
  try {
7614
8325
  const cutoff = Date.now() - 6e4;
7615
8326
  for (const f of fs17.readdirSync(shadowDir)) {
7616
8327
  if (f.startsWith("index_")) {
7617
- const fp = path19.join(shadowDir, f);
8328
+ const fp = path18.join(shadowDir, f);
7618
8329
  try {
7619
8330
  if (fs17.statSync(fp).mtimeMs < cutoff) fs17.unlinkSync(fp);
7620
8331
  } catch {
@@ -7628,7 +8339,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
7628
8339
  const hardcoded = [".git", ".node9"];
7629
8340
  const lines = [...hardcoded, ...ignorePaths].join("\n");
7630
8341
  try {
7631
- fs17.writeFileSync(path19.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
8342
+ fs17.writeFileSync(path18.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
7632
8343
  } catch {
7633
8344
  }
7634
8345
  }
@@ -7641,7 +8352,7 @@ function ensureShadowRepo(shadowDir, cwd) {
7641
8352
  timeout: 3e3
7642
8353
  });
7643
8354
  if (check.status === 0) {
7644
- const ptPath = path19.join(shadowDir, "project-path.txt");
8355
+ const ptPath = path18.join(shadowDir, "project-path.txt");
7645
8356
  try {
7646
8357
  const stored = fs17.readFileSync(ptPath, "utf8").trim();
7647
8358
  if (stored === normalizedCwd) return true;
@@ -7668,7 +8379,7 @@ function ensureShadowRepo(shadowDir, cwd) {
7668
8379
  console.error("[Node9] git init --bare failed:", init.stderr?.toString());
7669
8380
  return false;
7670
8381
  }
7671
- const configFile = path19.join(shadowDir, "config");
8382
+ const configFile = path18.join(shadowDir, "config");
7672
8383
  spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
7673
8384
  timeout: 3e3
7674
8385
  });
@@ -7676,7 +8387,7 @@ function ensureShadowRepo(shadowDir, cwd) {
7676
8387
  timeout: 3e3
7677
8388
  });
7678
8389
  try {
7679
- fs17.writeFileSync(path19.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
8390
+ fs17.writeFileSync(path18.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
7680
8391
  } catch {
7681
8392
  }
7682
8393
  return true;
@@ -7699,7 +8410,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
7699
8410
  const shadowDir = getShadowRepoDir(cwd);
7700
8411
  if (!ensureShadowRepo(shadowDir, cwd)) return null;
7701
8412
  writeShadowExcludes(shadowDir, ignorePaths);
7702
- indexFile = path19.join(shadowDir, `index_${process.pid}_${Date.now()}`);
8413
+ indexFile = path18.join(shadowDir, `index_${process.pid}_${Date.now()}`);
7703
8414
  const shadowEnv = {
7704
8415
  ...process.env,
7705
8416
  GIT_DIR: shadowDir,
@@ -7808,7 +8519,7 @@ function applyUndo(hash, cwd) {
7808
8519
  timeout: GIT_TIMEOUT
7809
8520
  }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
7810
8521
  for (const file of [...tracked, ...untracked]) {
7811
- const fullPath = path19.join(dir, file);
8522
+ const fullPath = path18.join(dir, file);
7812
8523
  if (!snapshotFiles.has(file) && fs17.existsSync(fullPath)) {
7813
8524
  fs17.unlinkSync(fullPath);
7814
8525
  }
@@ -7834,7 +8545,7 @@ function registerCheckCommand(program2) {
7834
8545
  } catch (err) {
7835
8546
  const tempConfig = getConfig();
7836
8547
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
7837
- const logPath = path20.join(os15.homedir(), ".node9", "hook-debug.log");
8548
+ const logPath = path19.join(os14.homedir(), ".node9", "hook-debug.log");
7838
8549
  const errMsg = err instanceof Error ? err.message : String(err);
7839
8550
  fs18.appendFileSync(
7840
8551
  logPath,
@@ -7847,9 +8558,9 @@ RAW: ${raw}
7847
8558
  }
7848
8559
  const config = getConfig(payload.cwd || void 0);
7849
8560
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
7850
- const logPath = path20.join(os15.homedir(), ".node9", "hook-debug.log");
7851
- if (!fs18.existsSync(path20.dirname(logPath)))
7852
- fs18.mkdirSync(path20.dirname(logPath), { recursive: true });
8561
+ const logPath = path19.join(os14.homedir(), ".node9", "hook-debug.log");
8562
+ if (!fs18.existsSync(path19.dirname(logPath)))
8563
+ fs18.mkdirSync(path19.dirname(logPath), { recursive: true });
7853
8564
  fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
7854
8565
  `);
7855
8566
  }
@@ -7875,6 +8586,8 @@ RAW: ${raw}
7875
8586
  }
7876
8587
  writeTty(chalk5.gray(` Triggered by: ${blockedByContext}`));
7877
8588
  if (result2?.changeHint) writeTty(chalk5.cyan(` To change: ${result2.changeHint}`));
8589
+ if (result2?.recoveryCommand)
8590
+ writeTty(chalk5.green(` \u{1F4A1} Run: ${result2.recoveryCommand}`));
7878
8591
  writeTty("");
7879
8592
  } catch {
7880
8593
  } finally {
@@ -7887,7 +8600,8 @@ RAW: ${raw}
7887
8600
  const aiFeedbackMessage = buildNegotiationMessage(
7888
8601
  blockedByContext,
7889
8602
  isHumanDecision,
7890
- msg
8603
+ msg,
8604
+ result2?.recoveryCommand
7891
8605
  );
7892
8606
  process.stdout.write(
7893
8607
  JSON.stringify({
@@ -7911,7 +8625,7 @@ RAW: ${raw}
7911
8625
  if (shouldSnapshot(toolName, toolInput, config)) {
7912
8626
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
7913
8627
  }
7914
- const safeCwdForAuth = typeof payload.cwd === "string" && path20.isAbsolute(payload.cwd) ? payload.cwd : void 0;
8628
+ const safeCwdForAuth = typeof payload.cwd === "string" && path19.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7915
8629
  const result = await authorizeHeadless(toolName, toolInput, meta, {
7916
8630
  cwd: safeCwdForAuth
7917
8631
  });
@@ -7955,7 +8669,7 @@ RAW: ${raw}
7955
8669
  });
7956
8670
  } catch (err) {
7957
8671
  if (process.env.NODE9_DEBUG === "1") {
7958
- const logPath = path20.join(os15.homedir(), ".node9", "hook-debug.log");
8672
+ const logPath = path19.join(os14.homedir(), ".node9", "hook-debug.log");
7959
8673
  const errMsg = err instanceof Error ? err.message : String(err);
7960
8674
  fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
7961
8675
  `);
@@ -7995,8 +8709,8 @@ init_audit();
7995
8709
  init_config();
7996
8710
  init_policy();
7997
8711
  import fs19 from "fs";
7998
- import path21 from "path";
7999
- import os16 from "os";
8712
+ import path20 from "path";
8713
+ import os15 from "os";
8000
8714
  init_daemon();
8001
8715
 
8002
8716
  // src/utils/cp-mv-parser.ts
@@ -8037,6 +8751,20 @@ function containsShellMetachar(token) {
8037
8751
  }
8038
8752
 
8039
8753
  // src/cli/commands/log.ts
8754
+ 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;
8755
+ function detectTestResult(command, output) {
8756
+ if (!TEST_COMMAND_RE.test(command)) return null;
8757
+ const out = output.toLowerCase();
8758
+ if (/\b(tests?\s+passed|all\s+tests?\s+passed|passing|test\s+suites?.*passed|ok\b|\d+\s+passed)/i.test(
8759
+ out
8760
+ ) && !/\b(fail|error|failed)\b/.test(out)) {
8761
+ return "pass";
8762
+ }
8763
+ if (/\b(tests?\s+failed|failing|failed|error|assertion\s+error|\d+\s+failed)\b/i.test(out)) {
8764
+ return "fail";
8765
+ }
8766
+ return null;
8767
+ }
8040
8768
  function sanitize3(value) {
8041
8769
  return value.replace(/[\x00-\x1F\x7F]/g, "");
8042
8770
  }
@@ -8055,9 +8783,9 @@ function registerLogCommand(program2) {
8055
8783
  decision: "allowed",
8056
8784
  source: "post-hook"
8057
8785
  };
8058
- const logPath = path21.join(os16.homedir(), ".node9", "audit.log");
8059
- if (!fs19.existsSync(path21.dirname(logPath)))
8060
- fs19.mkdirSync(path21.dirname(logPath), { recursive: true });
8786
+ const logPath = path20.join(os15.homedir(), ".node9", "audit.log");
8787
+ if (!fs19.existsSync(path20.dirname(logPath)))
8788
+ fs19.mkdirSync(path20.dirname(logPath), { recursive: true });
8061
8789
  fs19.appendFileSync(logPath, JSON.stringify(entry) + "\n");
8062
8790
  if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
8063
8791
  const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
@@ -8068,7 +8796,22 @@ function registerLogCommand(program2) {
8068
8796
  }
8069
8797
  }
8070
8798
  }
8071
- const safeCwd = typeof payload.cwd === "string" && path21.isAbsolute(payload.cwd) ? payload.cwd : void 0;
8799
+ if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
8800
+ const bashCommand = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
8801
+ const output = payload.tool_response?.output ?? "";
8802
+ if (bashCommand && output) {
8803
+ const testResult = detectTestResult(bashCommand, output);
8804
+ if (testResult) {
8805
+ await notifyActivitySocket({
8806
+ id: "test-result",
8807
+ ts: Date.now(),
8808
+ tool,
8809
+ status: testResult === "pass" ? "test_pass" : "test_fail"
8810
+ });
8811
+ }
8812
+ }
8813
+ }
8814
+ const safeCwd = typeof payload.cwd === "string" && path20.isAbsolute(payload.cwd) ? payload.cwd : void 0;
8072
8815
  const config = getConfig(safeCwd);
8073
8816
  if (shouldSnapshot(tool, {}, config)) {
8074
8817
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
@@ -8077,7 +8820,7 @@ function registerLogCommand(program2) {
8077
8820
  const msg = err instanceof Error ? err.message : String(err);
8078
8821
  process.stderr.write(`[Node9] audit log error: ${msg}
8079
8822
  `);
8080
- const debugPath = path21.join(os16.homedir(), ".node9", "hook-debug.log");
8823
+ const debugPath = path20.join(os15.homedir(), ".node9", "hook-debug.log");
8081
8824
  try {
8082
8825
  fs19.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
8083
8826
  `);
@@ -8385,12 +9128,12 @@ function registerConfigShowCommand(program2) {
8385
9128
  init_daemon();
8386
9129
  import chalk7 from "chalk";
8387
9130
  import fs20 from "fs";
8388
- import path22 from "path";
8389
- import os17 from "os";
9131
+ import path21 from "path";
9132
+ import os16 from "os";
8390
9133
  import { execSync as execSync2 } from "child_process";
8391
9134
  function registerDoctorCommand(program2, version2) {
8392
9135
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
8393
- const homeDir2 = os17.homedir();
9136
+ const homeDir2 = os16.homedir();
8394
9137
  let failures = 0;
8395
9138
  function pass(msg) {
8396
9139
  console.log(chalk7.green(" \u2705 ") + msg);
@@ -8439,7 +9182,7 @@ function registerDoctorCommand(program2, version2) {
8439
9182
  );
8440
9183
  }
8441
9184
  section("Configuration");
8442
- const globalConfigPath = path22.join(homeDir2, ".node9", "config.json");
9185
+ const globalConfigPath = path21.join(homeDir2, ".node9", "config.json");
8443
9186
  if (fs20.existsSync(globalConfigPath)) {
8444
9187
  try {
8445
9188
  JSON.parse(fs20.readFileSync(globalConfigPath, "utf-8"));
@@ -8450,7 +9193,7 @@ function registerDoctorCommand(program2, version2) {
8450
9193
  } else {
8451
9194
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
8452
9195
  }
8453
- const projectConfigPath = path22.join(process.cwd(), "node9.config.json");
9196
+ const projectConfigPath = path21.join(process.cwd(), "node9.config.json");
8454
9197
  if (fs20.existsSync(projectConfigPath)) {
8455
9198
  try {
8456
9199
  JSON.parse(fs20.readFileSync(projectConfigPath, "utf-8"));
@@ -8462,7 +9205,7 @@ function registerDoctorCommand(program2, version2) {
8462
9205
  );
8463
9206
  }
8464
9207
  }
8465
- const credsPath = path22.join(homeDir2, ".node9", "credentials.json");
9208
+ const credsPath = path21.join(homeDir2, ".node9", "credentials.json");
8466
9209
  if (fs20.existsSync(credsPath)) {
8467
9210
  pass("Cloud credentials found (~/.node9/credentials.json)");
8468
9211
  } else {
@@ -8472,7 +9215,7 @@ function registerDoctorCommand(program2, version2) {
8472
9215
  );
8473
9216
  }
8474
9217
  section("Agent Hooks");
8475
- const claudeSettingsPath = path22.join(homeDir2, ".claude", "settings.json");
9218
+ const claudeSettingsPath = path21.join(homeDir2, ".claude", "settings.json");
8476
9219
  if (fs20.existsSync(claudeSettingsPath)) {
8477
9220
  try {
8478
9221
  const cs = JSON.parse(fs20.readFileSync(claudeSettingsPath, "utf-8"));
@@ -8491,7 +9234,7 @@ function registerDoctorCommand(program2, version2) {
8491
9234
  } else {
8492
9235
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
8493
9236
  }
8494
- const geminiSettingsPath = path22.join(homeDir2, ".gemini", "settings.json");
9237
+ const geminiSettingsPath = path21.join(homeDir2, ".gemini", "settings.json");
8495
9238
  if (fs20.existsSync(geminiSettingsPath)) {
8496
9239
  try {
8497
9240
  const gs = JSON.parse(fs20.readFileSync(geminiSettingsPath, "utf-8"));
@@ -8510,7 +9253,7 @@ function registerDoctorCommand(program2, version2) {
8510
9253
  } else {
8511
9254
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
8512
9255
  }
8513
- const cursorHooksPath = path22.join(homeDir2, ".cursor", "hooks.json");
9256
+ const cursorHooksPath = path21.join(homeDir2, ".cursor", "hooks.json");
8514
9257
  if (fs20.existsSync(cursorHooksPath)) {
8515
9258
  try {
8516
9259
  const cur = JSON.parse(fs20.readFileSync(cursorHooksPath, "utf-8"));
@@ -8552,8 +9295,8 @@ function registerDoctorCommand(program2, version2) {
8552
9295
  // src/cli/commands/audit.ts
8553
9296
  import chalk8 from "chalk";
8554
9297
  import fs21 from "fs";
8555
- import path23 from "path";
8556
- import os18 from "os";
9298
+ import path22 from "path";
9299
+ import os17 from "os";
8557
9300
  function formatRelativeTime(timestamp) {
8558
9301
  const diff = Date.now() - new Date(timestamp).getTime();
8559
9302
  const sec = Math.floor(diff / 1e3);
@@ -8566,7 +9309,7 @@ function formatRelativeTime(timestamp) {
8566
9309
  }
8567
9310
  function registerAuditCommand(program2) {
8568
9311
  program2.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
8569
- const logPath = path23.join(os18.homedir(), ".node9", "audit.log");
9312
+ const logPath = path22.join(os17.homedir(), ".node9", "audit.log");
8570
9313
  if (!fs21.existsSync(logPath)) {
8571
9314
  console.log(
8572
9315
  chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
@@ -8694,8 +9437,8 @@ init_core();
8694
9437
  init_daemon();
8695
9438
  import chalk10 from "chalk";
8696
9439
  import fs22 from "fs";
8697
- import path24 from "path";
8698
- import os19 from "os";
9440
+ import path23 from "path";
9441
+ import os18 from "os";
8699
9442
  function readJson2(filePath) {
8700
9443
  try {
8701
9444
  if (fs22.existsSync(filePath)) return JSON.parse(fs22.readFileSync(filePath, "utf-8"));
@@ -8763,8 +9506,8 @@ function registerStatusCommand(program2) {
8763
9506
  console.log("");
8764
9507
  const modeLabel = settings.mode === "audit" ? chalk10.blue("audit") : settings.mode === "strict" ? chalk10.red("strict") : chalk10.white("standard");
8765
9508
  console.log(` Mode: ${modeLabel}`);
8766
- const projectConfig = path24.join(process.cwd(), "node9.config.json");
8767
- const globalConfig = path24.join(os19.homedir(), ".node9", "config.json");
9509
+ const projectConfig = path23.join(process.cwd(), "node9.config.json");
9510
+ const globalConfig = path23.join(os18.homedir(), ".node9", "config.json");
8768
9511
  console.log(
8769
9512
  ` Local: ${fs22.existsSync(projectConfig) ? chalk10.green("Active (node9.config.json)") : chalk10.gray("Not present")}`
8770
9513
  );
@@ -8776,15 +9519,15 @@ function registerStatusCommand(program2) {
8776
9519
  ` Sandbox: ${chalk10.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
8777
9520
  );
8778
9521
  }
8779
- const homeDir2 = os19.homedir();
9522
+ const homeDir2 = os18.homedir();
8780
9523
  const claudeSettings = readJson2(
8781
- path24.join(homeDir2, ".claude", "settings.json")
9524
+ path23.join(homeDir2, ".claude", "settings.json")
8782
9525
  );
8783
- const claudeConfig = readJson2(path24.join(homeDir2, ".claude.json"));
9526
+ const claudeConfig = readJson2(path23.join(homeDir2, ".claude.json"));
8784
9527
  const geminiSettings = readJson2(
8785
- path24.join(homeDir2, ".gemini", "settings.json")
9528
+ path23.join(homeDir2, ".gemini", "settings.json")
8786
9529
  );
8787
- const cursorConfig = readJson2(path24.join(homeDir2, ".cursor", "mcp.json"));
9530
+ const cursorConfig = readJson2(path23.join(homeDir2, ".cursor", "mcp.json"));
8788
9531
  const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
8789
9532
  if (agentFound) {
8790
9533
  console.log("");
@@ -8845,12 +9588,12 @@ function registerStatusCommand(program2) {
8845
9588
  init_core();
8846
9589
  import chalk11 from "chalk";
8847
9590
  import fs23 from "fs";
8848
- import path25 from "path";
8849
- import os20 from "os";
9591
+ import path24 from "path";
9592
+ import os19 from "os";
8850
9593
  function registerInitCommand(program2) {
8851
9594
  program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").action(async (options) => {
8852
9595
  console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
8853
- const configPath = path25.join(os20.homedir(), ".node9", "config.json");
9596
+ const configPath = path24.join(os19.homedir(), ".node9", "config.json");
8854
9597
  if (fs23.existsSync(configPath) && !options.force) {
8855
9598
  console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
8856
9599
  } else {
@@ -8860,7 +9603,7 @@ function registerInitCommand(program2) {
8860
9603
  ...DEFAULT_CONFIG,
8861
9604
  settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
8862
9605
  };
8863
- const dir = path25.dirname(configPath);
9606
+ const dir = path24.dirname(configPath);
8864
9607
  if (!fs23.existsSync(dir)) fs23.mkdirSync(dir, { recursive: true });
8865
9608
  fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
8866
9609
  console.log(chalk11.green(`\u2705 Config created: ${configPath}`));
@@ -9318,20 +10061,20 @@ function registerTrustCommand(program2) {
9318
10061
 
9319
10062
  // src/cli.ts
9320
10063
  var { version } = JSON.parse(
9321
- fs25.readFileSync(path27.join(__dirname, "../package.json"), "utf-8")
10064
+ fs26.readFileSync(path27.join(__dirname, "../package.json"), "utf-8")
9322
10065
  );
9323
10066
  var program = new Command();
9324
10067
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
9325
10068
  program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
9326
10069
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
9327
10070
  const credPath = path27.join(os22.homedir(), ".node9", "credentials.json");
9328
- if (!fs25.existsSync(path27.dirname(credPath)))
9329
- fs25.mkdirSync(path27.dirname(credPath), { recursive: true });
10071
+ if (!fs26.existsSync(path27.dirname(credPath)))
10072
+ fs26.mkdirSync(path27.dirname(credPath), { recursive: true });
9330
10073
  const profileName = options.profile || "default";
9331
10074
  let existingCreds = {};
9332
10075
  try {
9333
- if (fs25.existsSync(credPath)) {
9334
- const raw = JSON.parse(fs25.readFileSync(credPath, "utf-8"));
10076
+ if (fs26.existsSync(credPath)) {
10077
+ const raw = JSON.parse(fs26.readFileSync(credPath, "utf-8"));
9335
10078
  if (raw.apiKey) {
9336
10079
  existingCreds = {
9337
10080
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -9343,13 +10086,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
9343
10086
  } catch {
9344
10087
  }
9345
10088
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
9346
- fs25.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
10089
+ fs26.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
9347
10090
  if (profileName === "default") {
9348
10091
  const configPath = path27.join(os22.homedir(), ".node9", "config.json");
9349
10092
  let config = {};
9350
10093
  try {
9351
- if (fs25.existsSync(configPath))
9352
- config = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
10094
+ if (fs26.existsSync(configPath))
10095
+ config = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
9353
10096
  } catch {
9354
10097
  }
9355
10098
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -9364,9 +10107,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
9364
10107
  approvers.cloud = false;
9365
10108
  }
9366
10109
  s.approvers = approvers;
9367
- if (!fs25.existsSync(path27.dirname(configPath)))
9368
- fs25.mkdirSync(path27.dirname(configPath), { recursive: true });
9369
- fs25.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
10110
+ if (!fs26.existsSync(path27.dirname(configPath)))
10111
+ fs26.mkdirSync(path27.dirname(configPath), { recursive: true });
10112
+ fs26.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
9370
10113
  }
9371
10114
  if (options.profile && profileName !== "default") {
9372
10115
  console.log(chalk17.green(`\u2705 Profile "${profileName}" saved`));
@@ -9379,14 +10122,15 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
9379
10122
  console.log(chalk17.gray(` Team policy enforced for all calls via Node9 cloud.`));
9380
10123
  }
9381
10124
  });
9382
- program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
10125
+ program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("<target>", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
9383
10126
  if (target === "gemini") return await setupGemini();
9384
10127
  if (target === "claude") return await setupClaude();
9385
10128
  if (target === "cursor") return await setupCursor();
9386
- console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
10129
+ if (target === "hud") return setupHud();
10130
+ console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
9387
10131
  process.exit(1);
9388
10132
  });
9389
- program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
10133
+ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("[target]", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
9390
10134
  if (!target) {
9391
10135
  console.log(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
9392
10136
  console.log(" Usage: " + chalk17.white("node9 setup <target>") + "\n");
@@ -9394,6 +10138,9 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
9394
10138
  console.log(" " + chalk17.green("claude") + " \u2014 Claude Code (hook mode)");
9395
10139
  console.log(" " + chalk17.green("gemini") + " \u2014 Gemini CLI (hook mode)");
9396
10140
  console.log(" " + chalk17.green("cursor") + " \u2014 Cursor (hook mode)");
10141
+ process.stdout.write(
10142
+ " " + chalk17.green("hud") + " \u2014 Claude Code security statusline\n"
10143
+ );
9397
10144
  console.log("");
9398
10145
  return;
9399
10146
  }
@@ -9401,7 +10148,8 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
9401
10148
  if (t === "gemini") return await setupGemini();
9402
10149
  if (t === "claude") return await setupClaude();
9403
10150
  if (t === "cursor") return await setupCursor();
9404
- console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
10151
+ if (t === "hud") return setupHud();
10152
+ console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
9405
10153
  process.exit(1);
9406
10154
  });
9407
10155
  program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
@@ -9409,8 +10157,11 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
9409
10157
  if (target === "claude") fn = teardownClaude;
9410
10158
  else if (target === "gemini") fn = teardownGemini;
9411
10159
  else if (target === "cursor") fn = teardownCursor;
10160
+ else if (target === "hud") fn = teardownHud;
9412
10161
  else {
9413
- console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
10162
+ console.error(
10163
+ chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
10164
+ );
9414
10165
  process.exit(1);
9415
10166
  }
9416
10167
  console.log(chalk17.cyan(`
@@ -9453,14 +10204,14 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
9453
10204
  }
9454
10205
  if (options.purge) {
9455
10206
  const node9Dir = path27.join(os22.homedir(), ".node9");
9456
- if (fs25.existsSync(node9Dir)) {
10207
+ if (fs26.existsSync(node9Dir)) {
9457
10208
  const confirmed = await confirm3({
9458
10209
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
9459
10210
  default: false
9460
10211
  });
9461
10212
  if (confirmed) {
9462
- fs25.rmSync(node9Dir, { recursive: true });
9463
- if (fs25.existsSync(node9Dir)) {
10213
+ fs26.rmSync(node9Dir, { recursive: true });
10214
+ if (fs26.existsSync(node9Dir)) {
9464
10215
  console.error(
9465
10216
  chalk17.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
9466
10217
  );
@@ -9576,6 +10327,10 @@ registerWatchCommand(program);
9576
10327
  registerMcpGatewayCommand(program);
9577
10328
  registerCheckCommand(program);
9578
10329
  registerLogCommand(program);
10330
+ program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").action(async () => {
10331
+ const { main: main2 } = await Promise.resolve().then(() => (init_hud(), hud_exports));
10332
+ await main2();
10333
+ });
9579
10334
  program.command("pause").description("Temporarily disable Node9 protection for a set duration").option("-d, --duration <duration>", "How long to pause (e.g. 15m, 1h, 30s)", "15m").action((options) => {
9580
10335
  const ms = parseDuration(options.duration);
9581
10336
  if (ms === null) {
@@ -9674,7 +10429,7 @@ if (process.argv[2] !== "daemon") {
9674
10429
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
9675
10430
  const logPath = path27.join(os22.homedir(), ".node9", "hook-debug.log");
9676
10431
  const msg = reason instanceof Error ? reason.message : String(reason);
9677
- fs25.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
10432
+ fs26.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
9678
10433
  `);
9679
10434
  }
9680
10435
  process.exit(0);