@node9/proxy 1.62.1 → 1.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -347,6 +347,7 @@ var init_config_schema = __esm({
347
347
  // nudge-only (default), and the scan cadence in minutes.
348
348
  mcpAutoWrap: z.boolean().optional(),
349
349
  mcpReconcileIntervalMinutes: z.number().positive().optional(),
350
+ mcpStaleAfterDays: z.number().min(0).optional(),
350
351
  cloudSyncIntervalHours: z.number().positive().optional(),
351
352
  // Seconds-granular override for the cloud policy sync cadence. Wins over
352
353
  // cloudSyncIntervalHours when set. Lets you opt into fast apply (e.g. 20)
@@ -3981,6 +3982,7 @@ var init_dist = __esm({
3981
3982
  name: "redis",
3982
3983
  description: "Protects Redis instances from destructive AI operations",
3983
3984
  aliases: [],
3985
+ _scopeNote: "Every rule requires Redis to actually be involved: a client in the command (redis-cli / valkey-cli, in either order \u2014 `redis-cli FLUSHALL` and `echo FLUSHALL | redis-cli` are both real), OR the command field holding the bare keyword, which is what the MCP redis_command tool sends ({ command: 'FLUSHALL', args: [] }). Without the client requirement these rules matched the WORD anywhere \u2014 including prose in a heredoc writing a file \u2014 which produced two observed false positives: a test fixture describing the shield, and a commit message naming the rule. Every other builtin shield already scopes this way (aws s3, docker system prune, kubectl delete, gh repo delete). See redis-scope.spec.ts.",
3984
3986
  smartRules: [
3985
3987
  {
3986
3988
  name: "shield:redis:block-flushall",
@@ -3989,7 +3991,7 @@ var init_dist = __esm({
3989
3991
  {
3990
3992
  field: "command",
3991
3993
  op: "matches",
3992
- value: "\\bFLUSHALL\\b",
3994
+ value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
3993
3995
  flags: "i"
3994
3996
  }
3995
3997
  ],
@@ -4003,7 +4005,7 @@ var init_dist = __esm({
4003
4005
  {
4004
4006
  field: "command",
4005
4007
  op: "matches",
4006
- value: "\\bFLUSHDB\\b",
4008
+ value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
4007
4009
  flags: "i"
4008
4010
  }
4009
4011
  ],
@@ -4017,7 +4019,7 @@ var init_dist = __esm({
4017
4019
  {
4018
4020
  field: "command",
4019
4021
  op: "matches",
4020
- value: "\\bCONFIG\\s+RESETSTAT\\b",
4022
+ value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
4021
4023
  flags: "i"
4022
4024
  }
4023
4025
  ],
@@ -4031,7 +4033,7 @@ var init_dist = __esm({
4031
4033
  {
4032
4034
  field: "command",
4033
4035
  op: "matches",
4034
- value: "\\bCONFIG\\s+SET\\b",
4036
+ value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
4035
4037
  flags: "i"
4036
4038
  }
4037
4039
  ],
@@ -4045,7 +4047,7 @@ var init_dist = __esm({
4045
4047
  {
4046
4048
  field: "command",
4047
4049
  op: "matches",
4048
- value: "\\bDEL\\b.*[*?\\[]|redis-cli.*--scan.*\\|.*xargs.*del",
4050
+ value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
4049
4051
  flags: "i"
4050
4052
  }
4051
4053
  ],
@@ -4615,6 +4617,48 @@ function getActiveEnvironment(config) {
4615
4617
  const env = config.settings.environment || process.env.NODE_ENV || "development";
4616
4618
  return config.environments[env] ?? null;
4617
4619
  }
4620
+ function readRulesCacheResilient(cacheFile) {
4621
+ let existed = false;
4622
+ for (let attempt = 0; attempt < 3; attempt++) {
4623
+ let content;
4624
+ try {
4625
+ content = fs4.readFileSync(cacheFile, "utf-8");
4626
+ existed = true;
4627
+ } catch (err2) {
4628
+ if (err2.code === "ENOENT") return {};
4629
+ continue;
4630
+ }
4631
+ try {
4632
+ return JSON.parse(content);
4633
+ } catch {
4634
+ }
4635
+ }
4636
+ if (existed) {
4637
+ const backup = path4.join(path4.dirname(cacheFile), "rules-cache.last-good.json");
4638
+ if (backup !== cacheFile) {
4639
+ try {
4640
+ const raw = JSON.parse(fs4.readFileSync(backup, "utf-8"));
4641
+ logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
4642
+ return raw;
4643
+ } catch {
4644
+ }
4645
+ }
4646
+ logCacheReadIssue(cacheFile, "RULES_CACHE_UNREADABLE");
4647
+ }
4648
+ return {};
4649
+ }
4650
+ function logCacheReadIssue(cacheFile, kind) {
4651
+ if (cacheReadFailureLogged) return;
4652
+ cacheReadFailureLogged = true;
4653
+ try {
4654
+ fs4.appendFileSync(
4655
+ path4.join(os4.homedir(), ".node9", "hook-debug.log"),
4656
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
4657
+ `
4658
+ );
4659
+ } catch {
4660
+ }
4661
+ }
4618
4662
  function getConfig(cwd) {
4619
4663
  if (!cwd && cachedConfig) return cachedConfig;
4620
4664
  const globalPath = path4.join(os4.homedir(), ".node9", "config.json");
@@ -4682,6 +4726,7 @@ function getConfig(cwd) {
4682
4726
  if (s.mcpAutoWrap !== void 0) mergedSettings.mcpAutoWrap = s.mcpAutoWrap === true;
4683
4727
  if (s.mcpReconcileIntervalMinutes !== void 0)
4684
4728
  mergedSettings.mcpReconcileIntervalMinutes = s.mcpReconcileIntervalMinutes;
4729
+ if (s.mcpStaleAfterDays !== void 0) mergedSettings.mcpStaleAfterDays = s.mcpStaleAfterDays;
4685
4730
  if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
4686
4731
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
4687
4732
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -4691,12 +4736,13 @@ function getConfig(cwd) {
4691
4736
  if (p.smartRules) {
4692
4737
  const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
4693
4738
  const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
4694
- const userRuleNames = new Set(p.smartRules.filter((r) => r.name).map((r) => r.name));
4739
+ const localRules = p.smartRules.map(({ pinned: _pinned, ...r }) => r);
4740
+ const userRuleNames = new Set(localRules.filter((r) => r.name).map((r) => r.name));
4695
4741
  const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
4696
4742
  const filteredNonBlocks = defaultNonBlocks.filter(
4697
4743
  (r) => !r.name || !userRuleNames.has(r.name)
4698
4744
  );
4699
- mergedPolicy.smartRules = [...filteredBlocks, ...p.smartRules, ...filteredNonBlocks];
4745
+ mergedPolicy.smartRules = [...filteredBlocks, ...localRules, ...filteredNonBlocks];
4700
4746
  }
4701
4747
  if (p.snapshot) {
4702
4748
  const s2 = p.snapshot;
@@ -4761,10 +4807,12 @@ function getConfig(cwd) {
4761
4807
  applyLayer(globalConfig);
4762
4808
  applyLayer(projectConfig);
4763
4809
  let cloudManagedShields = [];
4810
+ let modeCloudControlled = false;
4811
+ let modeCloudStaged = false;
4764
4812
  {
4765
4813
  const cacheFile = path4.join(os4.homedir(), ".node9", "rules-cache.json");
4766
4814
  try {
4767
- const raw = JSON.parse(fs4.readFileSync(cacheFile, "utf-8"));
4815
+ const raw = readRulesCacheResilient(cacheFile);
4768
4816
  if (Array.isArray(raw.rules) && raw.rules.length > 0) {
4769
4817
  applyLayer({ policy: { smartRules: raw.rules } });
4770
4818
  }
@@ -4781,6 +4829,9 @@ function getConfig(cwd) {
4781
4829
  locked.includes("mode")
4782
4830
  );
4783
4831
  }
4832
+ if (typeof mc.mode === "string" || locked.includes("mode")) {
4833
+ modeCloudControlled = true;
4834
+ }
4784
4835
  if (mc.egress && typeof mc.egress === "object") {
4785
4836
  const hosts = (v) => Array.isArray(v) ? v.filter((h) => typeof h === "string") : void 0;
4786
4837
  mergedPolicy.egress = applyManagedEgress(
@@ -4879,19 +4930,28 @@ function getConfig(cwd) {
4879
4930
  }
4880
4931
  if (raw.shadowMode === true) {
4881
4932
  mergedSettings.mode = "observe";
4933
+ modeCloudStaged = true;
4882
4934
  }
4883
4935
  } catch {
4884
4936
  }
4885
4937
  }
4886
4938
  const shieldOverrides = readShieldOverrides();
4887
4939
  const activeShieldNames = [.../* @__PURE__ */ new Set([...readActiveShields(), ...cloudManagedShields])];
4940
+ const cloudManagedSet = new Set(cloudManagedShields);
4888
4941
  for (const shieldName of activeShieldNames) {
4889
- const shield = getShield(shieldName);
4942
+ const isCloudMandated = cloudManagedSet.has(shieldName);
4943
+ const shield = isCloudMandated ? BUILTIN_SHIELDS[shieldName] : getShield(shieldName);
4890
4944
  if (!shield) continue;
4891
4945
  const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4892
- const ruleOverrides = shieldOverrides[shieldName] ?? {};
4946
+ const ruleOverrides = isCloudMandated ? {} : shieldOverrides[shieldName] ?? {};
4893
4947
  for (const rule of shield.smartRules) {
4894
- if (!existingRuleNames.has(rule.name)) {
4948
+ const collides = rule.name ? existingRuleNames.has(rule.name) : false;
4949
+ if (isCloudMandated) {
4950
+ if (collides) {
4951
+ mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
4952
+ }
4953
+ mergedPolicy.smartRules.push({ ...rule, pinned: true });
4954
+ } else if (!collides) {
4895
4955
  const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
4896
4956
  mergedPolicy.smartRules.push(
4897
4957
  overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
@@ -4907,7 +4967,27 @@ function getConfig(cwd) {
4907
4967
  for (const rule of ADVISORY_SMART_RULES) {
4908
4968
  if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
4909
4969
  }
4910
- if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
4970
+ const envMode = process.env.NODE9_MODE;
4971
+ if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
4972
+ mergedSettings.mode = envMode;
4973
+ }
4974
+ if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
4975
+ mergedSettings.mode = "standard";
4976
+ }
4977
+ const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
4978
+ if (modeCloudControlled && mergedSettings.mode === "strict") {
4979
+ for (const name of Object.keys(mergedEnvironments)) {
4980
+ if (mergedEnvironments[name]?.requireApproval === false) {
4981
+ const cleaned = { ...mergedEnvironments[name] };
4982
+ delete cleaned.requireApproval;
4983
+ mergedEnvironments[name] = cleaned;
4984
+ }
4985
+ }
4986
+ }
4987
+ if (managedFloorActive) {
4988
+ mergedPolicy.ignoredTools = [...DEFAULT_CONFIG.policy.ignoredTools];
4989
+ mergedPolicy.sandboxPaths = [...DEFAULT_CONFIG.policy.sandboxPaths];
4990
+ }
4911
4991
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
4912
4992
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
4913
4993
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
@@ -4976,7 +5056,7 @@ ${error.replace("Invalid config:\n", "")}
4976
5056
  }
4977
5057
  return sanitized;
4978
5058
  }
4979
- var DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig;
5059
+ var DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, cacheReadFailureLogged;
4980
5060
  var init_config = __esm({
4981
5061
  "src/config/index.ts"() {
4982
5062
  "use strict";
@@ -4985,6 +5065,7 @@ var init_config = __esm({
4985
5065
  init_managed();
4986
5066
  init_build();
4987
5067
  init_trusted_hosts();
5068
+ init_dist();
4988
5069
  DANGEROUS_WORDS = [
4989
5070
  "mkfs",
4990
5071
  // formats/wipes a filesystem partition
@@ -5262,6 +5343,7 @@ var init_config = __esm({
5262
5343
  }
5263
5344
  ];
5264
5345
  cachedConfig = null;
5346
+ cacheReadFailureLogged = false;
5265
5347
  }
5266
5348
  });
5267
5349
 
@@ -5982,6 +6064,18 @@ async function isDaemonReachable(timeoutMs = 500) {
5982
6064
  return false;
5983
6065
  }
5984
6066
  }
6067
+ async function daemonHasInteractiveApprover(timeoutMs = 400) {
6068
+ try {
6069
+ const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/approver`, {
6070
+ signal: AbortSignal.timeout(timeoutMs)
6071
+ });
6072
+ if (!res.ok) return false;
6073
+ const body = await res.json();
6074
+ return body.interactive === true;
6075
+ } catch {
6076
+ return false;
6077
+ }
6078
+ }
5985
6079
  async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent) {
5986
6080
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
5987
6081
  const ctrl = new AbortController();
@@ -6862,6 +6956,12 @@ function isNetworkTool(toolName, args) {
6862
6956
  function notifyActivity(data) {
6863
6957
  return notifyActivitySocket(data);
6864
6958
  }
6959
+ async function hasReachableHumanApprover(opts) {
6960
+ const hasDisplay = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
6961
+ const nativeReachable = !opts.calledFromDaemon && opts.approvers.native !== false && hasDisplay;
6962
+ if (nativeReachable) return true;
6963
+ return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
6964
+ }
6865
6965
  async function authorizeHeadless(toolName, args, meta, options) {
6866
6966
  if (!options?.calledFromDaemon) {
6867
6967
  const actId = randomUUID();
@@ -7176,6 +7276,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7176
7276
  return { approved: true, checkedBy: "local-policy" };
7177
7277
  }
7178
7278
  if (policyResult.decision === "block") {
7279
+ const daemonUp = isDaemonRunning();
7280
+ let humanApproverReachable = false;
7281
+ if (!policyResult.dependsOnStatePredicates?.length && daemonUp && !isTestEnv2) {
7282
+ humanApproverReachable = await hasReachableHumanApprover({
7283
+ approvers,
7284
+ calledFromDaemon: options?.calledFromDaemon
7285
+ });
7286
+ }
7287
+ const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
7288
+ const hardBlock = () => {
7289
+ if (!isManual)
7290
+ appendLocalAudit(
7291
+ toolName,
7292
+ args,
7293
+ "deny",
7294
+ "smart-rule-block",
7295
+ { ...meta, ruleName: policyResult.ruleName },
7296
+ hashAuditArgs
7297
+ );
7298
+ return {
7299
+ approved: false,
7300
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
7301
+ blockedBy: "local-config",
7302
+ blockedByLabel: policyResult.blockedByLabel,
7303
+ ruleHit: policyResult.ruleName,
7304
+ ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
7305
+ ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
7306
+ };
7307
+ };
7179
7308
  if (policyResult.dependsOnStatePredicates?.length) {
7180
7309
  const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
7181
7310
  const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
@@ -7192,7 +7321,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7192
7321
  if (predicatesMet && policyResult.recoveryCommand) {
7193
7322
  statefulRecoveryCommand = policyResult.recoveryCommand;
7194
7323
  }
7195
- } else if (isDaemonRunning() && !isTestEnv2) {
7324
+ } else if (mayDowngrade) {
7196
7325
  if (!isManual)
7197
7326
  appendLocalAudit(
7198
7327
  toolName,
@@ -7214,36 +7343,13 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7214
7343
  };
7215
7344
  }
7216
7345
  } else {
7217
- if (!isManual)
7218
- appendLocalAudit(
7219
- toolName,
7220
- args,
7221
- "deny",
7222
- "smart-rule-block",
7223
- // Include policyResult.ruleName so the [2] Report SHIELDS
7224
- // panel can attribute this block to its specific shield
7225
- // (e.g. `shield:project-jail:block-read-ssh`) via the
7226
- // rule→shield map. checkedBy stays as the generic
7227
- // `smart-rule-block` for backward compat with existing
7228
- // log readers.
7229
- { ...meta, ruleName: policyResult.ruleName },
7230
- hashAuditArgs
7231
- );
7232
- return {
7233
- approved: false,
7234
- reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
7235
- blockedBy: "local-config",
7236
- blockedByLabel: policyResult.blockedByLabel,
7237
- ruleHit: policyResult.ruleName,
7238
- ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
7239
- ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
7240
- };
7346
+ return hardBlock();
7241
7347
  }
7242
7348
  }
7243
7349
  explainableLabel = policyResult.blockedByLabel || "Local Config";
7244
7350
  policyMatchedField = policyResult.matchedField;
7245
7351
  policyMatchedWord = policyResult.matchedWord;
7246
- if (policyResult.ruleName) localSmartRuleMatched = true;
7352
+ if (policyResult.ruleName || policyResult.tier === 7) localSmartRuleMatched = true;
7247
7353
  if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
7248
7354
  else if (policyResult.reason) policyRuleDescription = policyResult.reason;
7249
7355
  riskMetadata = computeRiskMetadata(
@@ -7255,7 +7361,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7255
7361
  policyResult.ruleName
7256
7362
  );
7257
7363
  if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
7258
- const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
7364
+ const persistent = policyResult.ruleName || policyResult.tier === 7 ? null : getPersistentDecision(toolName);
7259
7365
  if (persistent === "allow" && !appPermReview) {
7260
7366
  if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
7261
7367
  return { approved: true, checkedBy: "persistent" };
@@ -15924,6 +16030,9 @@ data: ${JSON.stringify(data)}
15924
16030
  }
15925
16031
  });
15926
16032
  }
16033
+ function hasInteractiveClient() {
16034
+ return [...sseClients].some((c) => c.capabilities.includes("input"));
16035
+ }
15927
16036
  function broadcastForensic(finding) {
15928
16037
  const severity = CRITICAL_FORENSIC_CATEGORIES.has(finding.type) ? "critical" : "warning";
15929
16038
  const event = {
@@ -17154,18 +17263,27 @@ var init_score = __esm({
17154
17263
 
17155
17264
  // src/posture/headline.ts
17156
17265
  function worstFinding(findings) {
17157
- return [...findings].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0];
17266
+ const cmp = (x, y) => x < y ? -1 : x > y ? 1 : 0;
17267
+ return [...findings].sort(
17268
+ (a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || cmp(a.category, b.category) || cmp(a.title, b.title)
17269
+ )[0];
17270
+ }
17271
+ function actionFromFinding(f) {
17272
+ if (!f?.fix) return null;
17273
+ const fix = f.fix.replace(/^fix it now:\s*/i, "");
17274
+ const where = f.detail.length === 0 ? "" : f.detail.length === 1 ? ` \u2014 found: ${f.detail[0]}` : ` \u2014 found: ${f.detail[0]} and ${f.detail.length - 1} more`;
17275
+ return fix + where;
17158
17276
  }
17159
17277
  function deriveHeadline(allFindings) {
17160
17278
  const findings = allFindings.filter(
17161
17279
  (f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix"
17162
17280
  );
17163
17281
  if (findings.length === 0 || findings.every((f) => f.severity === "advisory")) return null;
17164
- const has = (category) => findings.some((f) => f.category === category);
17165
- const secrets = has("Secrets");
17166
- const egressOpen = has("Egress");
17167
- const noIsolation = has("Isolation");
17168
- const gateWeak = has("Approval gate");
17282
+ const has2 = (category) => findings.some((f) => f.category === category);
17283
+ const secrets = has2("Secrets");
17284
+ const egressOpen = has2("Egress");
17285
+ const noIsolation = has2("Isolation");
17286
+ const gateWeak = has2("Approval gate");
17169
17287
  const notWired = findings.some((f) => f.category === "Coverage" && f.severity === "critical");
17170
17288
  const observeOnly = findings.some((f) => f.category === "Coverage" && f.severity === "high");
17171
17289
  let risk;
@@ -17188,13 +17306,16 @@ function deriveHeadline(allFindings) {
17188
17306
  } else if (observeOnly) {
17189
17307
  action = "Switch node9 to enforcing mode \u2014 right now it is only watching, not blocking.";
17190
17308
  } else if (egressOpen) {
17191
- action = "lock egress to an allowlist (node9 can enforce it) \u2014 it closes the exit the exfiltration needs.";
17309
+ const egressFix = actionFromFinding(
17310
+ worstFinding(findings.filter((f) => f.category === "Egress"))
17311
+ );
17312
+ action = egressFix ? `${egressFix} Closing the exit breaks the exfiltration chain.` : "lock egress to an allowlist (node9 can enforce it) \u2014 it closes the exit the exfiltration needs.";
17192
17313
  } else if (secrets) {
17193
- action = "node9 can block reads of sensitive paths (~/.ssh, ~/.aws) in-path.";
17314
+ action = actionFromFinding(worstFinding(findings.filter((f) => f.category === "Secrets"))) ?? "node9 can block reads of sensitive credential files in-path (`node9 shield enable project-jail`).";
17194
17315
  } else if (gateWeak) {
17195
- action = "node9 can enforce destructive-command blocking in-path.";
17316
+ action = actionFromFinding(worstFinding(findings.filter((f) => f.category === "Approval gate"))) ?? "node9 can enforce destructive-command blocking in-path (`node9 shield enable bash-safe`).";
17196
17317
  } else {
17197
- action = worstFinding(findings)?.fix ?? "Review the findings below.";
17318
+ action = actionFromFinding(worstFinding(findings)) ?? "Review the findings below.";
17198
17319
  }
17199
17320
  return { risk, action };
17200
17321
  }
@@ -17448,7 +17569,7 @@ var init_ship = __esm({
17448
17569
  });
17449
17570
 
17450
17571
  // src/policy-snapshot/build.ts
17451
- function buildPolicySnapshot(config, activeShields, overrides, mcpTools = {}, statusEntries = []) {
17572
+ function buildPolicySnapshot(config, activeShields, overrides, mcpTools = {}, statusEntries = [], syncHealth) {
17452
17573
  const p = config.policy;
17453
17574
  const statusByKey = /* @__PURE__ */ new Map();
17454
17575
  for (const s of statusEntries) {
@@ -17507,7 +17628,17 @@ function buildPolicySnapshot(config, activeShields, overrides, mcpTools = {}, st
17507
17628
  dlpEnabled: p.dlp.enabled,
17508
17629
  engineVersion: ENGINE_VERSION,
17509
17630
  // Connected rows first so they win the cap; non-connected SEE rows fill the rest.
17510
- mcpServers: [...connectedRows, ...extraRows].slice(0, MAX_MCP_SERVERS)
17631
+ mcpServers: [...connectedRows, ...extraRows].slice(0, MAX_MCP_SERVERS),
17632
+ // fleet-ship Step 2: ship the proxy's own sync health so the dashboard can
17633
+ // derive a "sync failing" badge. Only included when the caller passes it.
17634
+ ...syncHealth && {
17635
+ syncHealth: {
17636
+ lastCheckedAt: syncHealth.lastCheckedAt,
17637
+ lastChangedAt: syncHealth.lastChangedAt,
17638
+ lastError: syncHealth.lastError,
17639
+ consecutiveFailures: syncHealth.consecutiveFailures
17640
+ }
17641
+ }
17511
17642
  };
17512
17643
  }
17513
17644
  var MAX_RULES, MAX_EGRESS, MAX_MCP_SERVERS, MAX_MCP_TOOLS;
@@ -17723,6 +17854,21 @@ function inventoryMcp(home = os32.homedir()) {
17723
17854
  }
17724
17855
  return out;
17725
17856
  }
17857
+ function inventoryServerKeys(inv) {
17858
+ const keys = /* @__PURE__ */ new Set();
17859
+ for (const e of inv) {
17860
+ if (e.state === "gatewayed") {
17861
+ const i = e.args.indexOf("--upstream");
17862
+ if (i >= 0 && e.args[i + 1]) {
17863
+ keys.add(getServerKey(e.args[i + 1]));
17864
+ }
17865
+ } else if (e.state === "ungoverned") {
17866
+ const cmd = [e.command, ...e.args].map(quoteArg).join(" ");
17867
+ keys.add(getServerKey(cmd));
17868
+ }
17869
+ }
17870
+ return keys;
17871
+ }
17726
17872
  function writeMcpEntry(mcpFile, format, name, entry) {
17727
17873
  const key = format === "toml" ? "mcp_servers" : "mcpServers";
17728
17874
  let root = {};
@@ -17746,6 +17892,7 @@ var init_mcp_wrap = __esm({
17746
17892
  "use strict";
17747
17893
  init_agent_wiring();
17748
17894
  init_mcp_cmd();
17895
+ init_mcp_pin();
17749
17896
  init_mcp_cmd();
17750
17897
  }
17751
17898
  });
@@ -18177,9 +18324,12 @@ function extractManagedConfig(body) {
18177
18324
  return out.mode !== void 0 || out.egress !== void 0 || out.dlp !== void 0 || out.approvers !== void 0 || out.reviewChannel !== void 0 || out.approvalTimeoutMs !== void 0 || out.injectionScan !== void 0 || out.loopDetection !== void 0 || out.skillPinning !== void 0 || out.jailPaths !== void 0 || out.trustedHosts !== void 0 || out.appPermissions !== void 0 ? out : void 0;
18178
18325
  }
18179
18326
  function writeCache2(cache) {
18180
- const dir = path35.dirname(rulesCacheFile());
18181
- if (!fs36.existsSync(dir)) fs36.mkdirSync(dir, { recursive: true });
18182
- fs36.writeFileSync(rulesCacheFile(), JSON.stringify(cache, null, 2) + "\n", "utf-8");
18327
+ const data = JSON.stringify(cache, null, 2) + "\n";
18328
+ atomicWriteSync2(rulesCacheFile(), data, "utf-8");
18329
+ try {
18330
+ atomicWriteSync2(rulesCacheBackupFile(), data, "utf-8");
18331
+ } catch {
18332
+ }
18183
18333
  }
18184
18334
  async function syncOnce() {
18185
18335
  const creds = readCredentials();
@@ -18283,7 +18433,10 @@ async function pushPolicySnapshot(creds) {
18283
18433
  // Merged config-vs-connected status. resolveMcpStatus reads process.env to
18284
18434
  // resolve ${VAR} placeholders — so this push reflects THIS process's env
18285
18435
  // (daemon here, user shell in runPolicyPush); see the P2 design's two-env note.
18286
- resolveMcpStatus()
18436
+ resolveMcpStatus(),
18437
+ // fleet-ship Step 2: ship this machine's sync health so the dashboard can
18438
+ // show "sync failing" badges. readSyncHealth is local (same module, no circular).
18439
+ readSyncHealth()
18287
18440
  );
18288
18441
  await shipPolicySnapshot(body, creds);
18289
18442
  } catch {
@@ -18306,7 +18459,8 @@ async function runPolicyPush() {
18306
18459
  // Merged config-vs-connected status. resolveMcpStatus reads process.env to
18307
18460
  // resolve ${VAR} placeholders — so this push reflects THIS process's env
18308
18461
  // (daemon here, user shell in runPolicyPush); see the P2 design's two-env note.
18309
- resolveMcpStatus()
18462
+ resolveMcpStatus(),
18463
+ readSyncHealth()
18310
18464
  );
18311
18465
  const sent = await shipPolicySnapshot(body, creds);
18312
18466
  return sent ? { ok: true } : { ok: false, reason: "Push failed (network or server error)" };
@@ -18469,7 +18623,7 @@ function startForensicBroadcast() {
18469
18623
  const recurring = setInterval(() => void tick(), FORENSIC_BROADCAST_INTERVAL_MS);
18470
18624
  recurring.unref();
18471
18625
  }
18472
- var FINDING_TO_SIGNAL3, rulesCacheFile, DEFAULT_API_URL2, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS, syncHealthFile, STALE_MIN_MS, STALE_MAX_MS, STALE_FACTOR, FORENSIC_BROADCAST_INTERVAL_MS, FORENSIC_INITIAL_DELAY_MS, forensicBroadcastOffsets;
18626
+ var FINDING_TO_SIGNAL3, rulesCacheFile, rulesCacheBackupFile, DEFAULT_API_URL2, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS, syncHealthFile, STALE_MIN_MS, STALE_MAX_MS, STALE_FACTOR, FORENSIC_BROADCAST_INTERVAL_MS, FORENSIC_INITIAL_DELAY_MS, forensicBroadcastOffsets;
18473
18627
  var init_sync = __esm({
18474
18628
  "src/daemon/sync.ts"() {
18475
18629
  "use strict";
@@ -18479,6 +18633,7 @@ var init_sync = __esm({
18479
18633
  init_ship();
18480
18634
  init_build2();
18481
18635
  init_mcp_tools();
18636
+ init_state2();
18482
18637
  init_mcp_status();
18483
18638
  init_ship2();
18484
18639
  init_shields();
@@ -18499,6 +18654,7 @@ var init_sync = __esm({
18499
18654
  "long-output-redacted": "longOutputRedactions"
18500
18655
  };
18501
18656
  rulesCacheFile = () => path35.join(os33.homedir(), ".node9", "rules-cache.json");
18657
+ rulesCacheBackupFile = () => path35.join(os33.homedir(), ".node9", "rules-cache.last-good.json");
18502
18658
  DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept/policies/sync";
18503
18659
  DEFAULT_INTERVAL_HOURS = 5;
18504
18660
  MIN_INTERVAL_SECONDS = 15;
@@ -18740,6 +18896,53 @@ var init_audit_shipper = __esm({
18740
18896
  }
18741
18897
  });
18742
18898
 
18899
+ // src/audit/decision.ts
18900
+ function classifyDecision(a, b) {
18901
+ const isRow = !!a && typeof a === "object";
18902
+ const decision = isRow ? a.decision : a;
18903
+ const attribution = isRow ? a.checkedBy ?? a.source : b;
18904
+ const raw = typeof decision === "string" ? decision : String(decision ?? "");
18905
+ const src = typeof attribution === "string" ? attribution.toLowerCase() : "";
18906
+ const d = raw.toLowerCase();
18907
+ if (src && has(src, "observe-mode")) {
18908
+ return { outcome: "observe", label: "Would block", raw };
18909
+ }
18910
+ if (d === "dlp") return { outcome: "info", label: "Finding", raw };
18911
+ if (d === "mcp-discovered") return { outcome: "info", label: "Info", raw };
18912
+ if (d === "allow" || d === "allowed") {
18913
+ if (src === "post-hook") return { outcome: "allow", label: "Ran", raw };
18914
+ if (HUMAN_SOURCES.has(src)) {
18915
+ return { outcome: "allow", label: "Approved", raw };
18916
+ }
18917
+ return { outcome: "allow", label: "Auto-allowed", raw };
18918
+ }
18919
+ if (d === "deny" || d === "auto-deny" || d === "block") {
18920
+ if (TIMEOUT_SOURCES.has(src)) {
18921
+ return { outcome: "deny", label: "Timed out", raw };
18922
+ }
18923
+ if (HUMAN_SOURCES.has(src)) {
18924
+ return { outcome: "deny", label: "Denied", raw };
18925
+ }
18926
+ return { outcome: "deny", label: "Blocked", raw };
18927
+ }
18928
+ if (d === "review" || d === "pending") {
18929
+ return { outcome: "info", label: "Pending", raw };
18930
+ }
18931
+ return { outcome: "unknown", label: raw ? `? ${raw}` : "? (none)", raw };
18932
+ }
18933
+ function decisionTag(view) {
18934
+ return `[${view.label}]`.padEnd(14);
18935
+ }
18936
+ var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
18937
+ var init_decision = __esm({
18938
+ "src/audit/decision.ts"() {
18939
+ "use strict";
18940
+ HUMAN_SOURCES = /* @__PURE__ */ new Set(["daemon", "cloud", "local-decision", "inline-review-approved"]);
18941
+ TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
18942
+ has = (s, needle) => s.includes(needle);
18943
+ }
18944
+ });
18945
+
18743
18946
  // src/daemon/dlp-scanner.ts
18744
18947
  import fs38 from "fs";
18745
18948
  import path37 from "path";
@@ -18979,6 +19182,7 @@ function runMcpReconcile() {
18979
19182
  }
18980
19183
  const baseline = loadBaseline();
18981
19184
  const creds = getCredentials();
19185
+ reconcileStale(inv, creds);
18982
19186
  const fresh = inv.filter((e) => e.state === "ungoverned" && !baseline.has(idKey(e)));
18983
19187
  if (fresh.length === 0) return;
18984
19188
  const wrappedAgents = /* @__PURE__ */ new Set();
@@ -19018,6 +19222,77 @@ function runMcpReconcile() {
19018
19222
  }
19019
19223
  saveBaseline(baseline);
19020
19224
  }
19225
+ function reconcileStale(inv, creds) {
19226
+ let pins;
19227
+ try {
19228
+ pins = readMcpPins();
19229
+ } catch {
19230
+ return;
19231
+ }
19232
+ const serverKeys = Object.keys(pins.servers);
19233
+ if (serverKeys.length === 0) return;
19234
+ const liveKeys = inventoryServerKeys(inv);
19235
+ const now = (/* @__PURE__ */ new Date()).toISOString();
19236
+ let dirty = false;
19237
+ for (const sk of serverKeys) {
19238
+ const pin = pins.servers[sk];
19239
+ if (liveKeys.has(sk)) {
19240
+ if (pin.lastSeen !== now) {
19241
+ pin.lastSeen = now;
19242
+ dirty = true;
19243
+ }
19244
+ } else if (!pin.lastSeen) {
19245
+ pin.lastSeen = pin.pinnedAt;
19246
+ dirty = true;
19247
+ }
19248
+ }
19249
+ const staleDays = getConfig().settings.mcpStaleAfterDays ?? DEFAULT_STALE_DAYS;
19250
+ if (staleDays > 0 && liveKeys.size > 0) {
19251
+ const staleMs = staleDays * 864e5;
19252
+ for (const sk of serverKeys) {
19253
+ const pin = pins.servers[sk];
19254
+ if (liveKeys.has(sk)) continue;
19255
+ const age = Date.now() - Date.parse(pin.lastSeen ?? pin.pinnedAt);
19256
+ if (age >= staleMs) {
19257
+ appendToLog(HOOK_DEBUG_LOG, {
19258
+ event: "mcp-pin-auto-removed",
19259
+ serverKey: sk,
19260
+ label: pin.label,
19261
+ lastSeen: pin.lastSeen,
19262
+ pinnedAt: pin.pinnedAt
19263
+ });
19264
+ if (creds) {
19265
+ try {
19266
+ void auditLocalAllow(
19267
+ `mcp-server:${sk}`,
19268
+ {
19269
+ serverKey: sk,
19270
+ label: pin.label,
19271
+ lastSeen: pin.lastSeen,
19272
+ reason: "stale",
19273
+ staleDays
19274
+ },
19275
+ "mcp-server-removed",
19276
+ creds,
19277
+ { mcpServer: pin.label },
19278
+ void 0,
19279
+ false
19280
+ );
19281
+ } catch {
19282
+ }
19283
+ }
19284
+ delete pins.servers[sk];
19285
+ dirty = true;
19286
+ }
19287
+ }
19288
+ }
19289
+ if (dirty) {
19290
+ try {
19291
+ writeMcpPins(pins);
19292
+ } catch {
19293
+ }
19294
+ }
19295
+ }
19021
19296
  function startMcpReconciler() {
19022
19297
  setImmediate(() => {
19023
19298
  try {
@@ -19039,7 +19314,7 @@ function startMcpReconciler() {
19039
19314
  };
19040
19315
  schedule();
19041
19316
  }
19042
- var BASELINE_FILE2, BASELINE_CAP, DEFAULT_INTERVAL_MIN;
19317
+ var BASELINE_FILE2, BASELINE_CAP, DEFAULT_INTERVAL_MIN, DEFAULT_STALE_DAYS;
19043
19318
  var init_mcp_reconciler = __esm({
19044
19319
  "src/daemon/mcp-reconciler.ts"() {
19045
19320
  "use strict";
@@ -19048,9 +19323,11 @@ var init_mcp_reconciler = __esm({
19048
19323
  init_config();
19049
19324
  init_cloud();
19050
19325
  init_audit();
19326
+ init_mcp_pin();
19051
19327
  BASELINE_FILE2 = path38.join(os36.homedir(), ".node9", "mcp-baseline.json");
19052
19328
  BASELINE_CAP = 500;
19053
19329
  DEFAULT_INTERVAL_MIN = 60;
19330
+ DEFAULT_STALE_DAYS = 7;
19054
19331
  }
19055
19332
  });
19056
19333
 
@@ -19125,20 +19402,87 @@ var init_hook_heal = __esm({
19125
19402
  import fs40 from "fs";
19126
19403
  import path39 from "path";
19127
19404
  import os37 from "os";
19405
+ function capStartupLog(file) {
19406
+ try {
19407
+ if (fs40.statSync(file).size > MAX_STARTUP_LOG_BYTES) fs40.truncateSync(file);
19408
+ } catch {
19409
+ }
19410
+ }
19128
19411
  function openStartupLogFd() {
19129
19412
  try {
19130
19413
  const file = DAEMON_STARTUP_LOG();
19131
19414
  const dir = path39.dirname(file);
19132
19415
  if (!fs40.existsSync(dir)) fs40.mkdirSync(dir, { recursive: true });
19133
- try {
19134
- if (fs40.statSync(file).size > MAX_STARTUP_LOG_BYTES) fs40.truncateSync(file);
19135
- } catch {
19136
- }
19416
+ capStartupLog(file);
19137
19417
  return fs40.openSync(file, "a");
19138
19418
  } catch {
19139
19419
  return void 0;
19140
19420
  }
19141
19421
  }
19422
+ function recordStartupState(outcome, kind, detail) {
19423
+ try {
19424
+ if (outcome === "starting") {
19425
+ const prev = readStartupState();
19426
+ if (prev?.outcome === "starting") {
19427
+ const prevAt = new Date(prev.at).getTime();
19428
+ const age = Math.abs(Date.now() - prevAt);
19429
+ if (!isNaN(prevAt) && age < 24 * 60 * 60 * 1e3) return;
19430
+ }
19431
+ }
19432
+ const file = DAEMON_STARTUP_STATE();
19433
+ const dir = path39.dirname(file);
19434
+ if (!fs40.existsSync(dir)) fs40.mkdirSync(dir, { recursive: true });
19435
+ const state = { outcome, at: (/* @__PURE__ */ new Date()).toISOString() };
19436
+ if (kind) state.kind = kind;
19437
+ if (detail) state.detail = detail.slice(0, MAX_DETAIL);
19438
+ const tmp = `${file}.${process.pid}.tmp`;
19439
+ try {
19440
+ fs40.writeFileSync(tmp, JSON.stringify(state), "utf-8");
19441
+ fs40.renameSync(tmp, file);
19442
+ } catch (err2) {
19443
+ try {
19444
+ fs40.unlinkSync(tmp);
19445
+ } catch {
19446
+ }
19447
+ throw err2;
19448
+ }
19449
+ } catch {
19450
+ }
19451
+ }
19452
+ function readStartupState() {
19453
+ try {
19454
+ const raw = fs40.readFileSync(DAEMON_STARTUP_STATE(), "utf-8");
19455
+ const s = JSON.parse(raw);
19456
+ if (!s || typeof s.outcome !== "string" || typeof s.at !== "string") return null;
19457
+ return s;
19458
+ } catch {
19459
+ return null;
19460
+ }
19461
+ }
19462
+ function readStartupCause(maxAgeMs = 24 * 60 * 60 * 1e3) {
19463
+ const s = readStartupState();
19464
+ if (!s) return null;
19465
+ const at = new Date(s.at);
19466
+ if (isNaN(at.getTime()) || Date.now() - at.getTime() > maxAgeMs) return null;
19467
+ switch (s.outcome) {
19468
+ case "ok":
19469
+ case "ok-elsewhere":
19470
+ return null;
19471
+ case "starting":
19472
+ if (Date.now() - at.getTime() < STARTING_GRACE_MS) return null;
19473
+ return {
19474
+ kind: "did-not-start",
19475
+ detail: "the daemon did not come up \u2014 see ~/.node9/daemon-startup.log and hook-debug.log",
19476
+ at,
19477
+ // `at` is the FIRST attempt of the streak, not the most recent one.
19478
+ label: "start attempts failing since"
19479
+ };
19480
+ case "failed":
19481
+ return { kind: s.kind || "failed", detail: s.detail || "", at, label: "last start attempt" };
19482
+ default:
19483
+ return null;
19484
+ }
19485
+ }
19142
19486
  function logDaemonStartup(kind, detail) {
19143
19487
  try {
19144
19488
  const file = DAEMON_STARTUP_LOG();
@@ -19150,12 +19494,15 @@ function logDaemonStartup(kind, detail) {
19150
19494
  } catch {
19151
19495
  }
19152
19496
  }
19153
- var DAEMON_STARTUP_LOG, MAX_STARTUP_LOG_BYTES;
19497
+ var DAEMON_STARTUP_LOG, MAX_STARTUP_LOG_BYTES, DAEMON_STARTUP_STATE, MAX_DETAIL, STARTING_GRACE_MS;
19154
19498
  var init_startup_log = __esm({
19155
19499
  "src/daemon/startup-log.ts"() {
19156
19500
  "use strict";
19157
19501
  DAEMON_STARTUP_LOG = () => path39.join(os37.homedir(), ".node9", "daemon-startup.log");
19158
19502
  MAX_STARTUP_LOG_BYTES = 256 * 1024;
19503
+ DAEMON_STARTUP_STATE = () => path39.join(os37.homedir(), ".node9", "daemon-startup-state.json");
19504
+ MAX_DETAIL = 200;
19505
+ STARTING_GRACE_MS = 90 * 1e3;
19159
19506
  }
19160
19507
  });
19161
19508
 
@@ -19167,6 +19514,76 @@ import os38 from "os";
19167
19514
  import { randomUUID as randomUUID4 } from "crypto";
19168
19515
  import { spawnSync } from "child_process";
19169
19516
  import chalk6 from "chalk";
19517
+ function buildDaemonReport(allEntries, period, now) {
19518
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
19519
+ let start = new Date(todayStart);
19520
+ if (period === "7d") start.setDate(start.getDate() - 6);
19521
+ else if (period === "30d") start.setDate(start.getDate() - 29);
19522
+ else if (period === "month") start = new Date(now.getFullYear(), now.getMonth(), 1);
19523
+ const entries = allEntries.filter((e) => {
19524
+ if (e.source === "post-hook" || e.source === "response-dlp") return false;
19525
+ return new Date(e.ts) >= start;
19526
+ });
19527
+ const isBlocked = (e) => classifyDecision(e).outcome === "deny";
19528
+ const checkedBy = (e) => typeof e.checkedBy === "string" ? e.checkedBy : void 0;
19529
+ const summary = {
19530
+ total: entries.length,
19531
+ // The inline `startsWith('allow')` this replaces counted every non-allow
19532
+ // row as "blocked" — so DLP findings, MCP-discovery events and, worst, all
19533
+ // the observe-mode "would have blocked" rows inflated the blocked count
19534
+ // with things that were never refusals.
19535
+ allowed: entries.filter((e) => classifyDecision(e).outcome === "allow").length,
19536
+ blocked: entries.filter(isBlocked).length,
19537
+ dlp: entries.filter((e) => checkedBy(e)?.includes("dlp")).length,
19538
+ loops: entries.filter((e) => checkedBy(e) === "loop-detected").length
19539
+ };
19540
+ const dailyMap = /* @__PURE__ */ new Map();
19541
+ if (period === "today") {
19542
+ for (let h = 0; h < 24; h++) {
19543
+ const key = String(h).padStart(2, "0") + ":00";
19544
+ dailyMap.set(key, { date: key, calls: 0, blocked: 0 });
19545
+ }
19546
+ for (const e of entries) {
19547
+ const hour = new Date(e.ts).getHours();
19548
+ const key = String(hour).padStart(2, "0") + ":00";
19549
+ const d = dailyMap.get(key);
19550
+ d.calls++;
19551
+ if (isBlocked(e)) d.blocked++;
19552
+ }
19553
+ } else {
19554
+ for (const e of entries) {
19555
+ const date = e.ts.slice(0, 10);
19556
+ const d = dailyMap.get(date) || { date, calls: 0, blocked: 0 };
19557
+ d.calls++;
19558
+ if (isBlocked(e)) d.blocked++;
19559
+ dailyMap.set(date, d);
19560
+ }
19561
+ }
19562
+ const topToolsMap = /* @__PURE__ */ new Map();
19563
+ const topBlockedMap = /* @__PURE__ */ new Map();
19564
+ for (const e of entries) {
19565
+ const tool = String(e.tool ?? "");
19566
+ topToolsMap.set(tool, (topToolsMap.get(tool) || 0) + 1);
19567
+ if (isBlocked(e)) topBlockedMap.set(tool, (topBlockedMap.get(tool) || 0) + 1);
19568
+ }
19569
+ const top5 = (m) => [...m.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, value]) => ({ name, value }));
19570
+ const agentMap = /* @__PURE__ */ new Map();
19571
+ for (const e of entries) {
19572
+ const key = e.agent || "unknown";
19573
+ const a = agentMap.get(key) ?? { agent: key, total: 0, blocked: 0, dlp: 0 };
19574
+ a.total++;
19575
+ if (isBlocked(e)) a.blocked++;
19576
+ if (checkedBy(e)?.includes("dlp")) a.dlp++;
19577
+ agentMap.set(key, a);
19578
+ }
19579
+ return {
19580
+ summary,
19581
+ daily: [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date)),
19582
+ topTools: top5(topToolsMap),
19583
+ topBlockedTools: top5(topBlockedMap),
19584
+ byAgent: [...agentMap.values()].sort((a, b) => b.total - a.total)
19585
+ };
19586
+ }
19170
19587
  function startDaemon() {
19171
19588
  try {
19172
19589
  startCostSync();
@@ -19181,6 +19598,7 @@ function startDaemon() {
19181
19598
  const stack = err2 instanceof Error ? err2.stack ?? err2.message : String(err2);
19182
19599
  console.error("\n\u{1F6D1} Node9 daemon startup failed:\n" + stack);
19183
19600
  logDaemonStartup("startup-throw", err2 instanceof Error ? err2.message : String(err2));
19601
+ recordStartupState("failed", "startup-throw", err2 instanceof Error ? err2.message : String(err2));
19184
19602
  process.exit(1);
19185
19603
  }
19186
19604
  const internalToken = randomUUID4();
@@ -19553,6 +19971,10 @@ data: ${JSON.stringify(item.data)}
19553
19971
  return res.end(JSON.stringify({ error: "internal" }));
19554
19972
  }
19555
19973
  }
19974
+ if (req.method === "GET" && pathname === "/approver") {
19975
+ res.writeHead(200, { "Content-Type": "application/json" });
19976
+ return res.end(JSON.stringify({ interactive: hasInteractiveClient() }));
19977
+ }
19556
19978
  if (req.method === "GET" && pathname === "/state/check") {
19557
19979
  const predicatesParam = reqUrl.searchParams.get("predicates") ?? "";
19558
19980
  const predicates = predicatesParam.split(",").filter(Boolean);
@@ -19654,75 +20076,8 @@ data: ${JSON.stringify(item.data)}
19654
20076
  return [];
19655
20077
  }
19656
20078
  });
19657
- const now = /* @__PURE__ */ new Date();
19658
- const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
19659
- let start = new Date(todayStart);
19660
- if (period === "7d") start.setDate(start.getDate() - 6);
19661
- else if (period === "30d") start.setDate(start.getDate() - 29);
19662
- else if (period === "month") start = new Date(now.getFullYear(), now.getMonth(), 1);
19663
- const entries = allEntries.filter((e) => {
19664
- if (e.source === "post-hook" || e.source === "response-dlp") return false;
19665
- return new Date(e.ts) >= start;
19666
- });
19667
- const summary = {
19668
- total: entries.length,
19669
- allowed: entries.filter((e) => e.decision && e.decision.startsWith("allow")).length,
19670
- blocked: entries.filter((e) => e.decision && !e.decision.startsWith("allow")).length,
19671
- dlp: entries.filter((e) => e.checkedBy && e.checkedBy.includes("dlp")).length,
19672
- loops: entries.filter((e) => e.checkedBy === "loop-detected").length
19673
- };
19674
- const dailyMap = /* @__PURE__ */ new Map();
19675
- if (period === "today") {
19676
- for (let h = 0; h < 24; h++) {
19677
- const key = String(h).padStart(2, "0") + ":00";
19678
- dailyMap.set(key, { date: key, calls: 0, blocked: 0 });
19679
- }
19680
- for (const e of entries) {
19681
- const hour = new Date(e.ts).getHours();
19682
- const key = String(hour).padStart(2, "0") + ":00";
19683
- const d = dailyMap.get(key);
19684
- d.calls++;
19685
- if (e.decision && !e.decision.startsWith("allow")) d.blocked++;
19686
- }
19687
- } else {
19688
- for (const e of entries) {
19689
- const date = e.ts.slice(0, 10);
19690
- const d = dailyMap.get(date) || { date, calls: 0, blocked: 0 };
19691
- d.calls++;
19692
- if (e.decision && !e.decision.startsWith("allow")) d.blocked++;
19693
- dailyMap.set(date, d);
19694
- }
19695
- }
19696
- const topToolsMap = /* @__PURE__ */ new Map();
19697
- const topBlockedMap = /* @__PURE__ */ new Map();
19698
- for (const e of entries) {
19699
- topToolsMap.set(e.tool, (topToolsMap.get(e.tool) || 0) + 1);
19700
- if (e.decision && !e.decision.startsWith("allow")) {
19701
- topBlockedMap.set(e.tool, (topBlockedMap.get(e.tool) || 0) + 1);
19702
- }
19703
- }
19704
- const topTools = [...topToolsMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, value]) => ({ name, value }));
19705
- const topBlockedTools = [...topBlockedMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, value]) => ({ name, value }));
19706
- const agentMap = /* @__PURE__ */ new Map();
19707
- for (const e of entries) {
19708
- const key = e.agent || "unknown";
19709
- const a = agentMap.get(key) ?? { agent: key, total: 0, blocked: 0, dlp: 0 };
19710
- a.total++;
19711
- if (e.decision && !e.decision.startsWith("allow")) a.blocked++;
19712
- if (e.checkedBy?.includes("dlp")) a.dlp++;
19713
- agentMap.set(key, a);
19714
- }
19715
- const byAgent = [...agentMap.values()].sort((a, b) => b.total - a.total);
19716
20079
  res.writeHead(200, { "Content-Type": "application/json" });
19717
- return res.end(
19718
- JSON.stringify({
19719
- summary,
19720
- daily: [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date)),
19721
- topTools,
19722
- topBlockedTools,
19723
- byAgent
19724
- })
19725
- );
20080
+ return res.end(JSON.stringify(buildDaemonReport(allEntries, period, /* @__PURE__ */ new Date())));
19726
20081
  } catch {
19727
20082
  res.writeHead(500, { "Content-Type": "application/json" });
19728
20083
  return res.end(JSON.stringify({ error: "Failed to parse report" }));
@@ -20025,6 +20380,23 @@ data: ${JSON.stringify(item.data)}
20025
20380
  res.writeHead(404).end();
20026
20381
  });
20027
20382
  setDaemonServer(server);
20383
+ let bindAttempts = 0;
20384
+ const MAX_BIND_ATTEMPTS = 3;
20385
+ function retryListen() {
20386
+ if (++bindAttempts >= MAX_BIND_ATTEMPTS) {
20387
+ logDaemonStartup(
20388
+ "port-unavailable",
20389
+ `:${DAEMON_PORT} is held by something that is not a node9 daemon`
20390
+ );
20391
+ recordStartupState(
20392
+ "failed",
20393
+ "port-unavailable",
20394
+ `:${DAEMON_PORT} is held by another process that is not a node9 daemon \u2014 free the port, then: node9 daemon --background`
20395
+ );
20396
+ return process.exit(0);
20397
+ }
20398
+ server.listen(DAEMON_PORT, DAEMON_HOST);
20399
+ }
20028
20400
  server.on("error", (e) => {
20029
20401
  if (e.code === "EADDRINUSE") {
20030
20402
  try {
@@ -20032,6 +20404,7 @@ data: ${JSON.stringify(item.data)}
20032
20404
  const { pid } = JSON.parse(fs41.readFileSync(DAEMON_PID_FILE, "utf-8"));
20033
20405
  process.kill(pid, 0);
20034
20406
  logDaemonStartup("port-in-use", `another daemon (pid ${pid}) owns :${DAEMON_PORT}`);
20407
+ recordStartupState("ok-elsewhere");
20035
20408
  return process.exit(0);
20036
20409
  }
20037
20410
  } catch {
@@ -20039,13 +20412,14 @@ data: ${JSON.stringify(item.data)}
20039
20412
  fs41.unlinkSync(DAEMON_PID_FILE);
20040
20413
  } catch {
20041
20414
  }
20042
- server.listen(DAEMON_PORT, DAEMON_HOST);
20415
+ retryListen();
20043
20416
  return;
20044
20417
  }
20045
20418
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/settings`, {
20046
20419
  signal: AbortSignal.timeout(1e3)
20047
20420
  }).then((res) => {
20048
20421
  if (res.ok) {
20422
+ let adopted = false;
20049
20423
  try {
20050
20424
  let orphanPid = null;
20051
20425
  const ss = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
@@ -20073,19 +20447,38 @@ data: ${JSON.stringify(item.data)}
20073
20447
  JSON.stringify({ pid: orphanPid, port: DAEMON_PORT, internalToken, autoStarted }),
20074
20448
  { mode: 384 }
20075
20449
  );
20450
+ adopted = true;
20076
20451
  }
20077
20452
  } catch {
20078
20453
  }
20454
+ if (adopted) {
20455
+ logDaemonStartup(
20456
+ "port-in-use-orphan",
20457
+ `adopted the daemon already on :${DAEMON_PORT}`
20458
+ );
20459
+ recordStartupState("ok-elsewhere");
20460
+ } else {
20461
+ logDaemonStartup(
20462
+ "orphan-unidentified",
20463
+ `healthy daemon on :${DAEMON_PORT} could not be identified \u2014 no pid file written`
20464
+ );
20465
+ recordStartupState(
20466
+ "failed",
20467
+ "orphan-unidentified",
20468
+ `a healthy daemon is running on :${DAEMON_PORT} but its process could not be identified, so node9 cannot track it \u2014 install \`ss\` or \`lsof\`, or restart it with: node9 daemon --background`
20469
+ );
20470
+ }
20079
20471
  process.exit(0);
20080
20472
  } else {
20081
- server.listen(DAEMON_PORT, DAEMON_HOST);
20473
+ retryListen();
20082
20474
  }
20083
20475
  }).catch(() => {
20084
- server.listen(DAEMON_PORT, DAEMON_HOST);
20476
+ retryListen();
20085
20477
  });
20086
20478
  return;
20087
20479
  }
20088
20480
  logDaemonStartup("bind-failed", e.message);
20481
+ recordStartupState("failed", "bind-failed", e.message);
20089
20482
  console.error(chalk6.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
20090
20483
  process.exit(1);
20091
20484
  });
@@ -20103,6 +20496,8 @@ data: ${JSON.stringify(item.data)}
20103
20496
  { mode: 384 }
20104
20497
  );
20105
20498
  console.error(chalk6.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE on 127.0.0.1:${DAEMON_PORT}`));
20499
+ logDaemonStartup("ok", `listening on ${DAEMON_HOST}:${DAEMON_PORT}`);
20500
+ recordStartupState("ok");
20106
20501
  });
20107
20502
  if (watchMode) {
20108
20503
  console.error(chalk6.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
@@ -20120,6 +20515,7 @@ var init_server = __esm({
20120
20515
  init_costSync();
20121
20516
  init_sync();
20122
20517
  init_audit_shipper();
20518
+ init_decision();
20123
20519
  init_dlp_scanner();
20124
20520
  init_mcp_reconciler();
20125
20521
  init_hook_heal();
@@ -45302,7 +45698,7 @@ __export(tail_exports, {
45302
45698
  });
45303
45699
  import http5 from "http";
45304
45700
  import chalk40 from "chalk";
45305
- import fs71 from "fs";
45701
+ import fs72 from "fs";
45306
45702
  import os61 from "os";
45307
45703
  import path68 from "path";
45308
45704
  import readline6 from "readline";
@@ -45329,19 +45725,19 @@ function getModelContextLimit(model) {
45329
45725
  }
45330
45726
  function readSessionUsage() {
45331
45727
  const projectsDir = path68.join(os61.homedir(), ".claude", "projects");
45332
- if (!fs71.existsSync(projectsDir)) return null;
45728
+ if (!fs72.existsSync(projectsDir)) return null;
45333
45729
  let latestFile = null;
45334
45730
  let latestMtime = 0;
45335
45731
  try {
45336
- for (const dir of fs71.readdirSync(projectsDir)) {
45732
+ for (const dir of fs72.readdirSync(projectsDir)) {
45337
45733
  const dirPath = path68.join(projectsDir, dir);
45338
45734
  try {
45339
- if (!fs71.statSync(dirPath).isDirectory()) continue;
45340
- for (const file of fs71.readdirSync(dirPath)) {
45735
+ if (!fs72.statSync(dirPath).isDirectory()) continue;
45736
+ for (const file of fs72.readdirSync(dirPath)) {
45341
45737
  if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
45342
45738
  const filePath = path68.join(dirPath, file);
45343
45739
  try {
45344
- const mtime = fs71.statSync(filePath).mtimeMs;
45740
+ const mtime = fs72.statSync(filePath).mtimeMs;
45345
45741
  if (mtime > latestMtime) {
45346
45742
  latestMtime = mtime;
45347
45743
  latestFile = filePath;
@@ -45356,7 +45752,7 @@ function readSessionUsage() {
45356
45752
  }
45357
45753
  if (!latestFile) return null;
45358
45754
  try {
45359
- const lines = fs71.readFileSync(latestFile, "utf-8").split("\n");
45755
+ const lines = fs72.readFileSync(latestFile, "utf-8").split("\n");
45360
45756
  let lastModel = "";
45361
45757
  let lastInput = 0;
45362
45758
  let lastOutput = 0;
@@ -45456,9 +45852,9 @@ function renderPending(activity) {
45456
45852
  }
45457
45853
  async function ensureDaemon() {
45458
45854
  let pidPort = null;
45459
- if (fs71.existsSync(PID_FILE)) {
45855
+ if (fs72.existsSync(PID_FILE)) {
45460
45856
  try {
45461
- const { port } = JSON.parse(fs71.readFileSync(PID_FILE, "utf-8"));
45857
+ const { port } = JSON.parse(fs72.readFileSync(PID_FILE, "utf-8"));
45462
45858
  pidPort = port;
45463
45859
  } catch {
45464
45860
  console.error(chalk40.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
@@ -45473,12 +45869,21 @@ async function ensureDaemon() {
45473
45869
  } catch {
45474
45870
  }
45475
45871
  console.log(chalk40.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
45872
+ const startupFd = openStartupLogFd();
45873
+ recordStartupState("starting");
45476
45874
  const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
45477
45875
  detached: true,
45478
- stdio: "ignore",
45876
+ stdio: ["ignore", "ignore", startupFd ?? "ignore"],
45479
45877
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
45480
45878
  });
45879
+ child.on("error", (err2) => recordStartupState("failed", "spawn-failed", err2.message));
45481
45880
  child.unref();
45881
+ if (startupFd !== void 0) {
45882
+ try {
45883
+ fs72.closeSync(startupFd);
45884
+ } catch {
45885
+ }
45886
+ }
45482
45887
  for (let i = 0; i < 20; i++) {
45483
45888
  await new Promise((r) => setTimeout(r, 250));
45484
45889
  try {
@@ -45616,7 +46021,7 @@ function buildRecoveryCardLines(req) {
45616
46021
  function readApproversFromDisk() {
45617
46022
  const configPath = path68.join(os61.homedir(), ".node9", "config.json");
45618
46023
  try {
45619
- const raw = JSON.parse(fs71.readFileSync(configPath, "utf-8"));
46024
+ const raw = JSON.parse(fs72.readFileSync(configPath, "utf-8"));
45620
46025
  const settings = raw.settings ?? {};
45621
46026
  return settings.approvers ?? {};
45622
46027
  } catch {
@@ -45634,13 +46039,13 @@ function approverStatusLine() {
45634
46039
  function toggleApprover(channel) {
45635
46040
  const configPath = path68.join(os61.homedir(), ".node9", "config.json");
45636
46041
  try {
45637
- const raw = JSON.parse(fs71.readFileSync(configPath, "utf-8"));
46042
+ const raw = JSON.parse(fs72.readFileSync(configPath, "utf-8"));
45638
46043
  const settings = raw.settings ?? {};
45639
46044
  const approvers = settings.approvers ?? {};
45640
46045
  approvers[channel] = approvers[channel] === false;
45641
46046
  settings.approvers = approvers;
45642
46047
  raw.settings = settings;
45643
- fs71.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
46048
+ fs72.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
45644
46049
  } catch (err2) {
45645
46050
  process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
45646
46051
  `);
@@ -45812,7 +46217,7 @@ async function startTail(options = {}) {
45812
46217
  }
45813
46218
  postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).catch((err2) => {
45814
46219
  try {
45815
- fs71.appendFileSync(
46220
+ fs72.appendFileSync(
45816
46221
  path68.join(os61.homedir(), ".node9", "hook-debug.log"),
45817
46222
  `[tail] POST /decision failed: ${String(err2)}
45818
46223
  `
@@ -45879,7 +46284,7 @@ async function startTail(options = {}) {
45879
46284
  }
45880
46285
  const auditLog = path68.join(os61.homedir(), ".node9", "audit.log");
45881
46286
  try {
45882
- const unackedDlp = fs71.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
46287
+ const unackedDlp = fs72.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
45883
46288
  if (unackedDlp > 0) {
45884
46289
  console.log("");
45885
46290
  console.log(
@@ -45919,7 +46324,7 @@ async function startTail(options = {}) {
45919
46324
  if (stallWarned) return;
45920
46325
  if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
45921
46326
  try {
45922
- const auditMtime = fs71.statSync(auditLog).mtimeMs;
46327
+ const auditMtime = fs72.statSync(auditLog).mtimeMs;
45923
46328
  if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
45924
46329
  console.log("");
45925
46330
  console.log(
@@ -46108,6 +46513,7 @@ var PID_FILE, ICONS, MODEL_CONTEXT_LIMITS, RESET2, BOLD2, RED, YELLOW, CYAN, GRA
46108
46513
  var init_tail = __esm({
46109
46514
  "src/tui/tail.ts"() {
46110
46515
  "use strict";
46516
+ init_startup_log();
46111
46517
  init_daemon2();
46112
46518
  init_daemon();
46113
46519
  PID_FILE = path68.join(os61.homedir(), ".node9", "daemon.pid");
@@ -46158,7 +46564,7 @@ __export(hud_exports, {
46158
46564
  main: () => main,
46159
46565
  renderEnvironmentLine: () => renderEnvironmentLine
46160
46566
  });
46161
- import fs72 from "fs";
46567
+ import fs73 from "fs";
46162
46568
  import path69 from "path";
46163
46569
  import os62 from "os";
46164
46570
  import http6 from "http";
@@ -46236,9 +46642,9 @@ function formatTimeLeft(resetsAt) {
46236
46642
  return ` (${m}m left)`;
46237
46643
  }
46238
46644
  function safeReadJson(filePath) {
46239
- if (!fs72.existsSync(filePath)) return null;
46645
+ if (!fs73.existsSync(filePath)) return null;
46240
46646
  try {
46241
- return JSON.parse(fs72.readFileSync(filePath, "utf-8"));
46647
+ return JSON.parse(fs73.readFileSync(filePath, "utf-8"));
46242
46648
  } catch {
46243
46649
  return null;
46244
46650
  }
@@ -46259,10 +46665,10 @@ function countHooksInFile(filePath) {
46259
46665
  return Object.keys(cfg.hooks).length;
46260
46666
  }
46261
46667
  function countRulesInDir(rulesDir) {
46262
- if (!fs72.existsSync(rulesDir)) return 0;
46668
+ if (!fs73.existsSync(rulesDir)) return 0;
46263
46669
  let count = 0;
46264
46670
  try {
46265
- for (const entry of fs72.readdirSync(rulesDir, { withFileTypes: true })) {
46671
+ for (const entry of fs73.readdirSync(rulesDir, { withFileTypes: true })) {
46266
46672
  if (entry.isDirectory()) {
46267
46673
  count += countRulesInDir(path69.join(rulesDir, entry.name));
46268
46674
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -46288,7 +46694,7 @@ function countConfigs(cwd) {
46288
46694
  let hooksCount = 0;
46289
46695
  const userMcpServers = /* @__PURE__ */ new Set();
46290
46696
  const projectMcpServers = /* @__PURE__ */ new Set();
46291
- if (fs72.existsSync(path69.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
46697
+ if (fs73.existsSync(path69.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
46292
46698
  rulesCount += countRulesInDir(path69.join(claudeDir, "rules"));
46293
46699
  const userSettings = path69.join(claudeDir, "settings.json");
46294
46700
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
@@ -46299,18 +46705,18 @@ function countConfigs(cwd) {
46299
46705
  userMcpServers.delete(name);
46300
46706
  }
46301
46707
  if (cwd) {
46302
- if (fs72.existsSync(path69.join(cwd, "CLAUDE.md"))) claudeMdCount++;
46303
- if (fs72.existsSync(path69.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
46708
+ if (fs73.existsSync(path69.join(cwd, "CLAUDE.md"))) claudeMdCount++;
46709
+ if (fs73.existsSync(path69.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
46304
46710
  const projectClaudeDir = path69.join(cwd, ".claude");
46305
46711
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
46306
46712
  if (!overlapsUserScope) {
46307
- if (fs72.existsSync(path69.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
46713
+ if (fs73.existsSync(path69.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
46308
46714
  rulesCount += countRulesInDir(path69.join(projectClaudeDir, "rules"));
46309
46715
  const projSettings = path69.join(projectClaudeDir, "settings.json");
46310
46716
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
46311
46717
  hooksCount += countHooksInFile(projSettings);
46312
46718
  }
46313
- if (fs72.existsSync(path69.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
46719
+ if (fs73.existsSync(path69.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
46314
46720
  const localSettings = path69.join(projectClaudeDir, "settings.local.json");
46315
46721
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
46316
46722
  hooksCount += countHooksInFile(localSettings);
@@ -46348,11 +46754,11 @@ function readActiveShieldsHud() {
46348
46754
  }
46349
46755
  try {
46350
46756
  const shieldsPath = path69.join(os62.homedir(), ".node9", "shields.json");
46351
- if (!fs72.existsSync(shieldsPath)) {
46757
+ if (!fs73.existsSync(shieldsPath)) {
46352
46758
  shieldsCache = { value: [], ts: now };
46353
46759
  return [];
46354
46760
  }
46355
- const parsed = JSON.parse(fs72.readFileSync(shieldsPath, "utf-8"));
46761
+ const parsed = JSON.parse(fs73.readFileSync(shieldsPath, "utf-8"));
46356
46762
  if (!Array.isArray(parsed.active)) {
46357
46763
  shieldsCache = { value: [], ts: now };
46358
46764
  return [];
@@ -46454,17 +46860,17 @@ function renderContextLine(stdin) {
46454
46860
  async function main() {
46455
46861
  try {
46456
46862
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
46457
- if (fs72.existsSync(path69.join(os62.homedir(), ".node9", "hud-debug"))) {
46863
+ if (fs73.existsSync(path69.join(os62.homedir(), ".node9", "hud-debug"))) {
46458
46864
  try {
46459
46865
  const logPath = path69.join(os62.homedir(), ".node9", "hud-debug.log");
46460
46866
  const MAX_LOG_SIZE = 10 * 1024 * 1024;
46461
46867
  let size = 0;
46462
46868
  try {
46463
- size = fs72.statSync(logPath).size;
46869
+ size = fs73.statSync(logPath).size;
46464
46870
  } catch {
46465
46871
  }
46466
46872
  if (size < MAX_LOG_SIZE) {
46467
- fs72.appendFileSync(
46873
+ fs73.appendFileSync(
46468
46874
  logPath,
46469
46875
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
46470
46876
  );
@@ -46488,8 +46894,8 @@ async function main() {
46488
46894
  path69.join(cwd, "node9.config.json"),
46489
46895
  path69.join(os62.homedir(), ".node9", "config.json")
46490
46896
  ]) {
46491
- if (!fs72.existsSync(configPath)) continue;
46492
- const cfg = JSON.parse(fs72.readFileSync(configPath, "utf-8"));
46897
+ if (!fs73.existsSync(configPath)) continue;
46898
+ const cfg = JSON.parse(fs73.readFileSync(configPath, "utf-8"));
46493
46899
  const hud = cfg.settings?.hud;
46494
46900
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
46495
46901
  }
@@ -46631,7 +47037,7 @@ function writeCredentialsAndConfig(apiKey, opts = {}) {
46631
47037
  // src/cli.ts
46632
47038
  init_daemon2();
46633
47039
  import chalk41 from "chalk";
46634
- import fs73 from "fs";
47040
+ import fs74 from "fs";
46635
47041
  import path70 from "path";
46636
47042
  import os63 from "os";
46637
47043
  import { spawn as spawn9 } from "child_process";
@@ -46837,9 +47243,11 @@ function logAutostartSkipThrottled(reason) {
46837
47243
  if (Date.now() - fs44.statSync(stamp).mtimeMs < SKIP_THROTTLE_MS) return;
46838
47244
  } catch {
46839
47245
  }
47246
+ const dir = path42.join(os40.homedir(), ".node9");
47247
+ if (!fs44.existsSync(dir)) fs44.mkdirSync(dir, { recursive: true });
46840
47248
  fs44.writeFileSync(stamp, "", "utf-8");
46841
47249
  fs44.appendFileSync(
46842
- path42.join(os40.homedir(), ".node9", "hook-debug.log"),
47250
+ path42.join(dir, "hook-debug.log"),
46843
47251
  `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-skip: ${reason}
46844
47252
  `,
46845
47253
  "utf-8"
@@ -46858,6 +47266,8 @@ async function autoStartDaemonAndWait() {
46858
47266
  }
46859
47267
  if (!resolvedArgv1.endsWith(".js")) return false;
46860
47268
  const startupFd = openStartupLogFd();
47269
+ recordStartupState("starting");
47270
+ let spawned = false;
46861
47271
  try {
46862
47272
  const child = spawn3(process.execPath, [resolvedArgv1, "daemon"], {
46863
47273
  detached: true,
@@ -46867,13 +47277,24 @@ async function autoStartDaemonAndWait() {
46867
47277
  NODE9_AUTO_STARTED: "1"
46868
47278
  }
46869
47279
  });
47280
+ child.on("error", (err2) => {
47281
+ if (readStartupState()?.outcome !== "starting") return;
47282
+ recordStartupState("failed", "spawn-failed", err2.message);
47283
+ });
46870
47284
  child.unref();
47285
+ spawned = true;
46871
47286
  for (let i = 0; i < 20; i++) {
46872
47287
  await new Promise((r) => setTimeout(r, 250));
46873
47288
  if (!isDaemonRunning()) continue;
46874
47289
  if (await isDaemonReachable()) return true;
46875
47290
  }
46876
- } catch {
47291
+ } catch (err2) {
47292
+ if (!spawned)
47293
+ recordStartupState(
47294
+ "failed",
47295
+ "spawn-failed",
47296
+ err2 instanceof Error ? err2.message : String(err2)
47297
+ );
46877
47298
  } finally {
46878
47299
  if (startupFd !== void 0) {
46879
47300
  try {
@@ -47686,12 +48107,16 @@ RAW: ${raw}
47686
48107
  delete safeEnv[key];
47687
48108
  }
47688
48109
  const startupFd = openStartupLogFd();
48110
+ recordStartupState("starting");
47689
48111
  try {
47690
48112
  const d = spawn5(process.execPath, [scriptPath, "daemon"], {
47691
48113
  detached: true,
47692
48114
  stdio: ["ignore", "ignore", startupFd ?? "ignore"],
47693
48115
  env: { ...safeEnv, NODE9_AUTO_STARTED: "1" }
47694
48116
  });
48117
+ d.on("error", (err2) => {
48118
+ recordStartupState("failed", "spawn-failed", err2.message);
48119
+ });
47695
48120
  d.unref();
47696
48121
  } finally {
47697
48122
  if (startupFd !== void 0) {
@@ -47704,6 +48129,7 @@ RAW: ${raw}
47704
48129
  } catch (spawnErr) {
47705
48130
  const logPath = path46.join(os44.homedir(), ".node9", "hook-debug.log");
47706
48131
  const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
48132
+ recordStartupState("failed", "spawn-aborted", msg);
47707
48133
  try {
47708
48134
  fs48.appendFileSync(
47709
48135
  logPath,
@@ -48868,6 +49294,7 @@ function agoLabel(iso, now = Date.now()) {
48868
49294
  }
48869
49295
 
48870
49296
  // src/cli/commands/doctor.ts
49297
+ init_startup_log();
48871
49298
  function registerDoctorCommand(program2, version2) {
48872
49299
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(async () => {
48873
49300
  const homeDir2 = os47.homedir();
@@ -48979,6 +49406,15 @@ function registerDoctorCommand(program2, version2) {
48979
49406
  "Daemon not running \u2014 terminal & native approvals unavailable",
48980
49407
  "Run: node9 daemon --background"
48981
49408
  );
49409
+ const cause = readStartupCause();
49410
+ if (cause) {
49411
+ const suffix = cause.detail ? ` \u2014 ${cause.detail}` : "";
49412
+ console.log(
49413
+ chalk11.gray(
49414
+ ` ${cause.label} ${agoLabel(cause.at.toISOString())}: ${cause.kind}${suffix}`
49415
+ )
49416
+ );
49417
+ }
48982
49418
  }
48983
49419
  const autostart = autostartAdvice({
48984
49420
  installed: isDaemonServiceInstalled(),
@@ -49053,6 +49489,7 @@ function registerDoctorCommand(program2, version2) {
49053
49489
  }
49054
49490
 
49055
49491
  // src/cli/commands/audit.ts
49492
+ init_decision();
49056
49493
  import chalk12 from "chalk";
49057
49494
  import fs53 from "fs";
49058
49495
  import path51 from "path";
@@ -49087,10 +49524,16 @@ function registerAuditCommand(program2) {
49087
49524
  });
49088
49525
  entries = entries.map((e) => ({
49089
49526
  ...e,
49090
- decision: String(e.decision).startsWith("allow") ? "allow" : "deny"
49527
+ // classifyDecision is the ONE mapper (audit/decision.ts). The inline
49528
+ // `startsWith('allow') ? allow : deny` this replaces bucketed `dlp` and
49529
+ // `mcp-discovered` — findings, not verdicts — as DENY, inventing
49530
+ // refusals that never happened.
49531
+ // Pass the ROW, never a field pair — the attribution key differs by
49532
+ // producer (`checkedBy` from the gate, `source` from the hook/daemon).
49533
+ view: classifyDecision(e)
49091
49534
  }));
49092
49535
  if (options.tool) entries = entries.filter((e) => String(e.tool).includes(options.tool));
49093
- if (options.deny) entries = entries.filter((e) => e.decision === "deny");
49536
+ if (options.deny) entries = entries.filter((e) => e.view.outcome === "deny");
49094
49537
  const limit = Math.max(1, parseInt(options.tail, 10) || 20);
49095
49538
  entries = entries.slice(-limit);
49096
49539
  if (options.json) {
@@ -49113,13 +49556,13 @@ function registerAuditCommand(program2) {
49113
49556
  for (const e of entries) {
49114
49557
  const time = formatRelativeTime(String(e.ts)).padEnd(12);
49115
49558
  const tool = String(e.tool).slice(0, 17).padEnd(18);
49116
- const result = e.decision === "allow" ? chalk12.green("ALLOW".padEnd(10)) : chalk12.red("DENY".padEnd(10));
49559
+ const result = e.view.outcome === "allow" ? chalk12.green(e.view.label.padEnd(14)) : e.view.outcome === "deny" ? chalk12.red(e.view.label.padEnd(14)) : e.view.outcome === "observe" ? chalk12.yellow(e.view.label.padEnd(14)) : chalk12.gray(e.view.label.padEnd(14));
49117
49560
  const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
49118
49561
  const agent = String(e.agent || "unknown");
49119
49562
  console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
49120
49563
  }
49121
- const allowed = entries.filter((e) => e.decision === "allow").length;
49122
- const denied = entries.filter((e) => e.decision === "deny").length;
49564
+ const allowed = entries.filter((e) => e.view.outcome === "allow").length;
49565
+ const denied = entries.filter((e) => e.view.outcome === "deny").length;
49123
49566
  console.log(chalk12.dim(" " + "\u2500".repeat(65)));
49124
49567
  console.log(
49125
49568
  ` ${entries.length} entries | ${chalk12.green(allowed + " allowed")} | ${chalk12.red(denied + " denied")}
@@ -49135,6 +49578,7 @@ import chalk13 from "chalk";
49135
49578
  init_costSync();
49136
49579
  init_litellm();
49137
49580
  init_cost_codex();
49581
+ init_decision();
49138
49582
  import fs54 from "fs";
49139
49583
  import os49 from "os";
49140
49584
  import path52 from "path";
@@ -49228,8 +49672,15 @@ function parseAuditLog(logPath) {
49228
49672
  }
49229
49673
  });
49230
49674
  }
49231
- function isAllow(decision) {
49232
- return decision.startsWith("allow");
49675
+ function viewOf(e) {
49676
+ const outcome = classifyDecision(e).outcome;
49677
+ const observed = outcome === "observe";
49678
+ return {
49679
+ observed,
49680
+ ran: outcome === "allow" || observed,
49681
+ blocked: outcome === "deny" || outcome === "unknown",
49682
+ info: outcome === "info"
49683
+ };
49233
49684
  }
49234
49685
  function isDlp(checkedBy) {
49235
49686
  return !!checkedBy?.includes("dlp");
@@ -49642,12 +50093,12 @@ function aggregateReportFromAudit(period, opts = {}) {
49642
50093
  const priorStart = new Date(start.getTime() - periodMs);
49643
50094
  const priorEntries = allEntries.filter((e) => {
49644
50095
  if (e.source === "post-hook") return false;
50096
+ if (e.source === "response-dlp") return false;
50097
+ if (typeof e.decision !== "string") return false;
49645
50098
  const ts = new Date(e.ts);
49646
50099
  return ts >= priorStart && ts <= priorEnd;
49647
50100
  });
49648
- const priorBlocked = priorEntries.filter(
49649
- (e) => typeof e.decision === "string" && !isAllow(e.decision)
49650
- ).length;
50101
+ const priorBlocked = priorEntries.filter((e) => viewOf(e).blocked).length;
49651
50102
  const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
49652
50103
  const excludeTests = opts.excludeTests === true;
49653
50104
  const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
@@ -49690,15 +50141,17 @@ function aggregateReportFromAudit(period, opts = {}) {
49690
50141
  let dimInjectionBlocked = 0;
49691
50142
  for (const e of entries) {
49692
50143
  if (superseded.has(supersedeKey(e))) continue;
49693
- const allow = isAllow(e.decision);
50144
+ const view = viewOf(e);
49694
50145
  const dateKey = e.ts.slice(0, 10);
49695
50146
  const userInteracted = e.source === "daemon";
49696
- if (userInteracted) {
49697
- if (allow) userApproved++;
50147
+ if (view.observed) {
50148
+ if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
50149
+ } else if (view.info) {
50150
+ } else if (userInteracted) {
50151
+ if (view.ran) userApproved++;
49698
50152
  else userDenied++;
49699
- } else if (!allow) {
50153
+ } else if (view.blocked) {
49700
50154
  if (e.checkedBy === "timeout") timedOut++;
49701
- else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
49702
50155
  else if (isDlp(e.checkedBy)) dlpBlocked++;
49703
50156
  else if (e.checkedBy === "local-decision") userDenied++;
49704
50157
  else if (e.checkedBy !== "loop-detected") hardBlocked++;
@@ -49710,7 +50163,7 @@ function aggregateReportFromAudit(period, opts = {}) {
49710
50163
  if (cb.includes("would-block") && (cb.includes("pii") || cb.includes("dlp"))) {
49711
50164
  dimDataObserved++;
49712
50165
  }
49713
- if (!allow && !userInteracted) {
50166
+ if (view.blocked && !userInteracted) {
49714
50167
  switch (dimensionOfBlock(cb, e.ruleName ?? "")) {
49715
50168
  case "network":
49716
50169
  dimNetworkBlocked++;
@@ -49728,15 +50181,15 @@ function aggregateReportFromAudit(period, opts = {}) {
49728
50181
  }
49729
50182
  const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
49730
50183
  t.calls++;
49731
- if (!allow) t.blocked++;
50184
+ if (view.blocked) t.blocked++;
49732
50185
  toolMap.set(e.tool, t);
49733
- if (!allow) {
50186
+ if (view.blocked) {
49734
50187
  const key = e.checkedBy ?? (e.source === "daemon" ? "local-decision" : null);
49735
50188
  if (key) {
49736
50189
  blockMap.set(key, (blockMap.get(key) ?? 0) + 1);
49737
50190
  }
49738
50191
  }
49739
- if (!allow && e.ruleName) {
50192
+ if ((view.blocked || view.observed) && e.ruleName) {
49740
50193
  ruleMap.set(e.ruleName, (ruleMap.get(e.ruleName) ?? 0) + 1);
49741
50194
  }
49742
50195
  if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
@@ -49745,7 +50198,7 @@ function aggregateReportFromAudit(period, opts = {}) {
49745
50198
  hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
49746
50199
  const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
49747
50200
  d.calls++;
49748
- if (!allow) d.blocked++;
50201
+ if (view.blocked) d.blocked++;
49749
50202
  dailyMap.set(dateKey, d);
49750
50203
  }
49751
50204
  for (const e of allEntries) {
@@ -50297,9 +50750,11 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
50297
50750
  }
50298
50751
 
50299
50752
  // src/cli/commands/daemon-cmd.ts
50753
+ init_startup_log();
50300
50754
  init_daemon2();
50301
50755
  import chalk14 from "chalk";
50302
50756
  import { spawn as spawn6 } from "child_process";
50757
+ import fs55 from "fs";
50303
50758
  var VALID_ACTIONS = "start | stop | restart | status | install | uninstall";
50304
50759
  function registerDaemonCommand(program2) {
50305
50760
  program2.command("daemon").description("Manage the local approval daemon").argument("[action]", `${VALID_ACTIONS} (default: start)`).option("-b, --background", "Start the daemon in the background (detached)").option(
@@ -50337,14 +50792,27 @@ function registerDaemonCommand(program2) {
50337
50792
  if (cmd === "restart") {
50338
50793
  stopDaemon();
50339
50794
  await new Promise((r) => setTimeout(r, 500));
50795
+ const restartFd = openStartupLogFd();
50796
+ recordStartupState("starting");
50340
50797
  const child = spawn6(process.execPath, [process.argv[1], "daemon"], {
50341
50798
  detached: true,
50342
- stdio: "ignore",
50799
+ stdio: ["ignore", "ignore", restartFd ?? "ignore"],
50343
50800
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
50344
50801
  });
50802
+ child.on(
50803
+ "error",
50804
+ (err2) => recordStartupState("failed", "spawn-failed", err2.message)
50805
+ );
50345
50806
  child.unref();
50807
+ if (restartFd !== void 0) {
50808
+ try {
50809
+ fs55.closeSync(restartFd);
50810
+ } catch {
50811
+ }
50812
+ }
50346
50813
  if (child.pid) {
50347
- console.log(chalk14.green(`\u2713 Daemon restarted (PID ${child.pid})`));
50814
+ console.log(chalk14.green(`\u2713 Daemon relaunching (PID ${child.pid})`));
50815
+ console.log(chalk14.gray(" Confirm with: node9 status"));
50348
50816
  } else {
50349
50817
  console.error(chalk14.red("\u2717 Failed to restart daemon \u2014 spawn returned no PID"));
50350
50818
  process.exit(1);
@@ -50367,13 +50835,33 @@ function registerDaemonCommand(program2) {
50367
50835
  return;
50368
50836
  }
50369
50837
  if (options.background) {
50370
- const child = spawn6(process.execPath, [process.argv[1], "daemon"], {
50371
- detached: true,
50372
- stdio: "ignore"
50373
- });
50374
- child.unref();
50375
- console.log(chalk14.green(`
50376
- \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
50838
+ const startupFd = openStartupLogFd();
50839
+ try {
50840
+ recordStartupState("starting");
50841
+ const child = spawn6(process.execPath, [process.argv[1], "daemon"], {
50842
+ detached: true,
50843
+ // Capture the child's stderr: a module-load crash prints its stack there
50844
+ // and dies before it can record anything itself.
50845
+ stdio: ["ignore", "ignore", startupFd ?? "ignore"]
50846
+ });
50847
+ child.on(
50848
+ "error",
50849
+ (err2) => recordStartupState("failed", "spawn-failed", err2.message)
50850
+ );
50851
+ child.unref();
50852
+ console.log(
50853
+ chalk14.green(`
50854
+ \u{1F6E1}\uFE0F Node9 daemon launching in background (PID ${child.pid})`)
50855
+ );
50856
+ console.log(chalk14.gray(" Confirm with: node9 status"));
50857
+ } finally {
50858
+ if (startupFd !== void 0) {
50859
+ try {
50860
+ fs55.closeSync(startupFd);
50861
+ } catch {
50862
+ }
50863
+ }
50864
+ }
50377
50865
  process.exit(0);
50378
50866
  }
50379
50867
  startDaemon();
@@ -50388,7 +50876,7 @@ init_agent_wiring();
50388
50876
  init_sync();
50389
50877
  init_service();
50390
50878
  import chalk15 from "chalk";
50391
- import fs55 from "fs";
50879
+ import fs56 from "fs";
50392
50880
  import path53 from "path";
50393
50881
  import os50 from "os";
50394
50882
  function printAgentSection(label2, hookPairs, wrapped) {
@@ -50466,10 +50954,10 @@ function registerStatusCommand(program2) {
50466
50954
  const projectConfig = path53.join(process.cwd(), "node9.config.json");
50467
50955
  const globalConfig = path53.join(os50.homedir(), ".node9", "config.json");
50468
50956
  console.log(
50469
- ` Local: ${fs55.existsSync(projectConfig) ? chalk15.green("Active (node9.config.json)") : chalk15.gray("Not present")}`
50957
+ ` Local: ${fs56.existsSync(projectConfig) ? chalk15.green("Active (node9.config.json)") : chalk15.gray("Not present")}`
50470
50958
  );
50471
50959
  console.log(
50472
- ` Global: ${fs55.existsSync(globalConfig) ? chalk15.green("Active (~/.node9/config.json)") : chalk15.gray("Not present")}`
50960
+ ` Global: ${fs56.existsSync(globalConfig) ? chalk15.green("Active (~/.node9/config.json)") : chalk15.gray("Not present")}`
50473
50961
  );
50474
50962
  if (mergedConfig.policy.sandboxPaths.length > 0) {
50475
50963
  console.log(
@@ -50516,7 +51004,7 @@ init_shields();
50516
51004
  init_service();
50517
51005
  init_core();
50518
51006
  import chalk16 from "chalk";
50519
- import fs56 from "fs";
51007
+ import fs57 from "fs";
50520
51008
  import path54 from "path";
50521
51009
  import os51 from "os";
50522
51010
  import https6 from "https";
@@ -50605,15 +51093,15 @@ function registerInitCommand(program2) {
50605
51093
  console.log("");
50606
51094
  }
50607
51095
  const configPath = path54.join(os51.homedir(), ".node9", "config.json");
50608
- const isFirstInstall = !fs56.existsSync(configPath);
50609
- if (fs56.existsSync(configPath) && !options.force) {
51096
+ const isFirstInstall = !fs57.existsSync(configPath);
51097
+ if (fs57.existsSync(configPath) && !options.force) {
50610
51098
  try {
50611
- const existing = JSON.parse(fs56.readFileSync(configPath, "utf-8"));
51099
+ const existing = JSON.parse(fs57.readFileSync(configPath, "utf-8"));
50612
51100
  const settings = existing.settings ?? {};
50613
51101
  if (settings.mode !== chosenMode) {
50614
51102
  settings.mode = chosenMode;
50615
51103
  existing.settings = settings;
50616
- fs56.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
51104
+ fs57.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
50617
51105
  console.log(chalk16.green(`\u2705 Mode updated: ${chosenMode}`));
50618
51106
  } else {
50619
51107
  console.log(chalk16.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
@@ -50627,8 +51115,8 @@ function registerInitCommand(program2) {
50627
51115
  settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
50628
51116
  };
50629
51117
  const dir = path54.dirname(configPath);
50630
- if (!fs56.existsSync(dir)) fs56.mkdirSync(dir, { recursive: true });
50631
- fs56.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
51118
+ if (!fs57.existsSync(dir)) fs57.mkdirSync(dir, { recursive: true });
51119
+ fs57.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
50632
51120
  console.log(chalk16.green(`\u2705 Config created: ${configPath}`));
50633
51121
  console.log(chalk16.gray(` Mode: ${chosenMode}`));
50634
51122
  }
@@ -50731,11 +51219,11 @@ init_agent_wiring();
50731
51219
  init_setup();
50732
51220
  init_hook_baseline();
50733
51221
  import chalk17 from "chalk";
50734
- import fs57 from "fs";
51222
+ import fs58 from "fs";
50735
51223
  var hasHookSurface = (a) => a.hooks.length > 0;
50736
51224
  function backupForHeal(file) {
50737
51225
  try {
50738
- if (file && fs57.existsSync(file)) fs57.copyFileSync(file, `${file}.node9-heal-bak`);
51226
+ if (file && fs58.existsSync(file)) fs58.copyFileSync(file, `${file}.node9-heal-bak`);
50739
51227
  } catch {
50740
51228
  }
50741
51229
  }
@@ -51695,16 +52183,17 @@ function registerMcpGatewayCommand(program2) {
51695
52183
 
51696
52184
  // src/mcp-server/index.ts
51697
52185
  import readline5 from "readline";
51698
- import fs59 from "fs";
52186
+ import fs60 from "fs";
51699
52187
  import os53 from "os";
51700
52188
  import path57 from "path";
51701
52189
  import { spawnSync as spawnSync4 } from "child_process";
52190
+ init_decision();
51702
52191
  init_core();
51703
52192
  init_daemon();
51704
52193
  init_shields();
51705
52194
 
51706
52195
  // src/auth/egress-config.ts
51707
- import fs58 from "fs";
52196
+ import fs59 from "fs";
51708
52197
  import os52 from "os";
51709
52198
  import path56 from "path";
51710
52199
  var DEFAULT_EGRESS = {
@@ -51720,7 +52209,7 @@ function egressConfigPath() {
51720
52209
  function readEgressRawConfig() {
51721
52210
  let text;
51722
52211
  try {
51723
- text = fs58.readFileSync(egressConfigPath(), "utf8");
52212
+ text = fs59.readFileSync(egressConfigPath(), "utf8");
51724
52213
  } catch (err2) {
51725
52214
  if (err2.code === "ENOENT") return {};
51726
52215
  throw err2;
@@ -51735,8 +52224,8 @@ function readEgressRawConfig() {
51735
52224
  }
51736
52225
  function writeEgressRawConfig(config) {
51737
52226
  const p = egressConfigPath();
51738
- fs58.mkdirSync(path56.dirname(p), { recursive: true });
51739
- fs58.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
52227
+ fs59.mkdirSync(path56.dirname(p), { recursive: true });
52228
+ fs59.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
51740
52229
  }
51741
52230
  function applyEgress(config, change) {
51742
52231
  const policy = config.policy = config.policy ?? {};
@@ -51931,7 +52420,7 @@ var TOOLS = [
51931
52420
  },
51932
52421
  {
51933
52422
  name: "node9_audit_get",
51934
- description: "Read recent entries from the node9 audit log (~/.node9/audit.log). Each entry shows timestamp, tool name, decision (allow/block/review), command/args, and agent. Use this to review what AI actions have been taken recently, especially blocked or reviewed ops.",
52423
+ description: "Read recent entries from the node9 audit log (~/.node9/audit.log). Each entry shows timestamp, outcome, tool name, the command, and the rule that fired. Outcomes use the same words as the dashboard: Auto-allowed, Approved, Ran, Blocked, Denied (a human refused), Timed out (nobody answered), Would block (shadow mode let it through), Finding, Info. Use this to review what AI actions were taken, especially refused ones.",
51935
52424
  inputSchema: {
51936
52425
  type: "object",
51937
52426
  properties: {
@@ -51941,8 +52430,8 @@ var TOOLS = [
51941
52430
  },
51942
52431
  filter: {
51943
52432
  type: "string",
51944
- enum: ["all", "block", "review"],
51945
- description: 'Filter by decision. Omit or use "all" to show every entry.'
52433
+ enum: ["all", "allow", "deny", "observe", "info", "block"],
52434
+ description: 'Filter by outcome. "deny" covers every refusal (rule block, human denial, timeout); "observe" is shadow-mode would-blocks; "block" is an alias for "deny". Omit or use "all" for every entry.'
51946
52435
  }
51947
52436
  },
51948
52437
  required: []
@@ -52124,10 +52613,10 @@ function handleStatus() {
52124
52613
  const projectConfig = path57.join(process.cwd(), "node9.config.json");
52125
52614
  const globalConfig = path57.join(os53.homedir(), ".node9", "config.json");
52126
52615
  lines.push(
52127
- `Project config (node9.config.json): ${fs59.existsSync(projectConfig) ? "present" : "not found"}`
52616
+ `Project config (node9.config.json): ${fs60.existsSync(projectConfig) ? "present" : "not found"}`
52128
52617
  );
52129
52618
  lines.push(
52130
- `Global config (~/.node9/config.json): ${fs59.existsSync(globalConfig) ? "present" : "not found"}`
52619
+ `Global config (~/.node9/config.json): ${fs60.existsSync(globalConfig) ? "present" : "not found"}`
52131
52620
  );
52132
52621
  return lines.join("\n");
52133
52622
  }
@@ -52237,8 +52726,8 @@ var GLOBAL_CONFIG_PATH = path57.join(os53.homedir(), ".node9", "config.json");
52237
52726
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
52238
52727
  function readGlobalConfigRaw() {
52239
52728
  try {
52240
- if (fs59.existsSync(GLOBAL_CONFIG_PATH)) {
52241
- return JSON.parse(fs59.readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
52729
+ if (fs60.existsSync(GLOBAL_CONFIG_PATH)) {
52730
+ return JSON.parse(fs60.readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
52242
52731
  }
52243
52732
  } catch {
52244
52733
  }
@@ -52246,8 +52735,8 @@ function readGlobalConfigRaw() {
52246
52735
  }
52247
52736
  function writeGlobalConfigRaw(data) {
52248
52737
  const dir = path57.dirname(GLOBAL_CONFIG_PATH);
52249
- if (!fs59.existsSync(dir)) fs59.mkdirSync(dir, { recursive: true });
52250
- fs59.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(data, null, 2) + "\n");
52738
+ if (!fs60.existsSync(dir)) fs60.mkdirSync(dir, { recursive: true });
52739
+ fs60.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(data, null, 2) + "\n");
52251
52740
  }
52252
52741
  function handleApproverList() {
52253
52742
  const config = getConfig();
@@ -52292,35 +52781,37 @@ function handleAuditGet(args) {
52292
52781
  const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
52293
52782
  const filter = typeof args.filter === "string" && args.filter !== "all" ? args.filter : null;
52294
52783
  const auditPath = path57.join(os53.homedir(), ".node9", "audit.log");
52295
- if (!fs59.existsSync(auditPath)) return "No audit log found.";
52296
- const rawLines = fs59.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
52784
+ if (!fs60.existsSync(auditPath)) return "No audit log found.";
52785
+ const rawLines = fs60.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
52786
+ const wanted = filter === "block" ? "deny" : filter;
52297
52787
  const parsed = [];
52298
52788
  for (const line of rawLines) {
52299
52789
  try {
52300
52790
  const e = JSON.parse(line);
52301
- const decision = String(e.decision ?? "allow");
52302
- if (filter && decision !== filter) continue;
52791
+ const view = classifyDecision(e);
52792
+ if (wanted && view.outcome !== wanted) continue;
52303
52793
  const argsObj = e.args;
52304
52794
  let detail = "";
52305
52795
  if (argsObj) {
52306
52796
  const cmd = argsObj.command ?? argsObj.file_path ?? argsObj.path ?? argsObj.sql;
52307
- if (typeof cmd === "string" && cmd) {
52308
- detail = cmd.length > 80 ? cmd.slice(0, 80) + "\u2026" : cmd;
52309
- }
52797
+ if (typeof cmd === "string" && cmd) detail = cmd;
52310
52798
  }
52311
- const decisionPad = decision === "block" ? "[BLOCK] " : decision === "review" ? "[review]" : "[allow] ";
52799
+ if (!detail && typeof e.argsPreview === "string") detail = e.argsPreview;
52800
+ detail = detail.replace(/\s+/g, " ").trim();
52801
+ if (detail.length > 80) detail = detail.slice(0, 80) + "\u2026";
52802
+ const why = typeof e.ruleName === "string" && e.ruleName ? ` (${e.ruleName})` : "";
52312
52803
  const toolPad = String(e.tool ?? "").padEnd(20);
52313
- const line2 = `${e.ts} ${decisionPad} ${toolPad} ${detail}`;
52314
- parsed.push({ raw: line, decision, formatted: line2 });
52804
+ const line2 = `${e.ts} ${decisionTag(view)} ${toolPad} ${detail}${why}`;
52805
+ parsed.push({ raw: line, outcome: view.outcome, formatted: line2 });
52315
52806
  } catch {
52316
- parsed.push({ raw: line, decision: "allow", formatted: line });
52807
+ parsed.push({ raw: line, outcome: "unknown", formatted: `[? unparseable] ${line}` });
52317
52808
  }
52318
52809
  }
52319
52810
  const recent = parsed.slice(-limit);
52320
52811
  if (recent.length === 0) {
52321
- return filter ? `No ${filter} entries found in audit log.` : "Audit log is empty.";
52812
+ return filter ? `No ${wanted} entries found in audit log.` : "Audit log is empty.";
52322
52813
  }
52323
- const header = filter ? `Last ${recent.length} ${filter.toUpperCase()} entries:` : `Last ${recent.length} audit entries:`;
52814
+ const header = filter ? `Last ${recent.length} ${String(wanted).toUpperCase()} entries:` : `Last ${recent.length} audit entries:`;
52324
52815
  return `${header}
52325
52816
 
52326
52817
  ${recent.map((e) => e.formatted).join("\n")}`;
@@ -52667,7 +53158,7 @@ function registerTrustCommand(program2) {
52667
53158
  // src/cli/commands/mcp-pin.ts
52668
53159
  init_mcp_pin();
52669
53160
  import chalk24 from "chalk";
52670
- import fs60 from "fs";
53161
+ import fs61 from "fs";
52671
53162
 
52672
53163
  // src/cli/commands/mcp-gateway-cmd.ts
52673
53164
  init_mcp_wrap();
@@ -52863,6 +53354,7 @@ Restart ${[...agents].join(", ")}`) + chalk23.gray(" to activate. Undo any serve
52863
53354
  }
52864
53355
 
52865
53356
  // src/cli/commands/mcp-pin.ts
53357
+ init_mcp_wrap();
52866
53358
  function registerMcpPinCommand(program2) {
52867
53359
  const pinCmd = program2.command("mcp").description("Manage MCP servers \u2014 governance (gateway) + tool-definition pinning");
52868
53360
  registerMcpGatewayCommand2(pinCmd);
@@ -52874,7 +53366,7 @@ function registerMcpPinCommand(program2) {
52874
53366
  let repoCorrupt = false;
52875
53367
  if (found.source === "repo") {
52876
53368
  try {
52877
- const raw = fs60.readFileSync(found.path, "utf-8");
53369
+ const raw = fs61.readFileSync(found.path, "utf-8");
52878
53370
  const parsed = JSON.parse(raw);
52879
53371
  repoEntries = parsed.servers ?? {};
52880
53372
  } catch {
@@ -52988,6 +53480,88 @@ function registerMcpPinCommand(program2) {
52988
53480
  \u{1F513} Cleared ${count} MCP pin(s).`));
52989
53481
  console.log(chalk24.gray(" Next connection to each server will re-pin.\n"));
52990
53482
  });
53483
+ pinCmd.command("forget [serverKey]").option("--stale", "Remove all stale (orphaned) servers at once").description("Remove a server pin that is no longer configured in any agent").action((serverKey, opts) => {
53484
+ if (opts.stale) {
53485
+ forgetAllStale();
53486
+ return;
53487
+ }
53488
+ if (!serverKey) {
53489
+ console.error(
53490
+ chalk24.red("\n\u274C Please provide a server key, or use --stale to remove all orphans.\n")
53491
+ );
53492
+ process.exit(1);
53493
+ }
53494
+ let pins;
53495
+ try {
53496
+ pins = readMcpPins();
53497
+ } catch {
53498
+ console.error(chalk24.red("\n\u274C Pin file is corrupt."));
53499
+ console.error(chalk24.yellow(" Run: node9 mcp pin reset\n"));
53500
+ process.exit(1);
53501
+ }
53502
+ if (!pins.servers[serverKey]) {
53503
+ console.error(chalk24.red(`
53504
+ \u274C No pin found for server key "${serverKey}"
53505
+ `));
53506
+ console.error(`Run ${chalk24.cyan("node9 mcp pin list")} to see pinned servers.
53507
+ `);
53508
+ process.exit(1);
53509
+ }
53510
+ const inv = inventoryMcp();
53511
+ const liveKeys = inventoryServerKeys(inv);
53512
+ if (liveKeys.has(serverKey)) {
53513
+ const agent = "an agent config";
53514
+ console.error(chalk24.red(`
53515
+ \u274C Server "${serverKey}" is still configured in ${agent}.`));
53516
+ console.error(
53517
+ chalk24.yellow(
53518
+ ` Remove it from the agent config first, then run: node9 mcp forget ${serverKey}
53519
+ `
53520
+ )
53521
+ );
53522
+ process.exit(1);
53523
+ }
53524
+ const label2 = pins.servers[serverKey].label;
53525
+ removePin(serverKey);
53526
+ console.log(chalk24.green(`
53527
+ \u2713 Forgot server ${chalk24.cyan(serverKey)}`));
53528
+ console.log(chalk24.gray(` Was: ${label2}`));
53529
+ console.log(chalk24.gray(" Pin removed \u2014 server will no longer appear in the dashboard.\n"));
53530
+ });
53531
+ }
53532
+ function forgetAllStale() {
53533
+ let pins;
53534
+ try {
53535
+ pins = readMcpPins();
53536
+ } catch {
53537
+ console.error(chalk24.red("\n\u274C Pin file is corrupt."));
53538
+ console.error(chalk24.yellow(" Run: node9 mcp pin reset\n"));
53539
+ process.exit(1);
53540
+ }
53541
+ const inv = inventoryMcp();
53542
+ const liveKeys = inventoryServerKeys(inv);
53543
+ if (liveKeys.size === 0) {
53544
+ console.error(chalk24.red("\n\u274C No live MCP servers detected \u2014 refusing to remove every pin."));
53545
+ console.error(
53546
+ chalk24.yellow(
53547
+ " This usually means an agent config is missing or unreadable, not that\n every server is gone. Check your agent configs, or remove one at a\n time: node9 mcp forget <key>\n"
53548
+ )
53549
+ );
53550
+ process.exit(1);
53551
+ }
53552
+ const stale = Object.entries(pins.servers).filter(([sk]) => !liveKeys.has(sk));
53553
+ if (stale.length === 0) {
53554
+ console.log(chalk24.gray("\nNo stale servers to remove.\n"));
53555
+ return;
53556
+ }
53557
+ for (const [sk, pin] of stale) {
53558
+ delete pins.servers[sk];
53559
+ console.log(chalk24.green(` \u2713 ${chalk24.cyan(sk)} ${chalk24.gray(pin.label)}`));
53560
+ }
53561
+ writeMcpPins(pins);
53562
+ console.log(chalk24.green(`
53563
+ Removed ${stale.length} stale server(s).
53564
+ `));
52991
53565
  }
52992
53566
 
52993
53567
  // src/cli/commands/sync.ts
@@ -53232,10 +53806,16 @@ var LABEL_WIDTH = 14;
53232
53806
  function label(category) {
53233
53807
  return chalk27.bold(category.padEnd(Math.max(LABEL_WIDTH, category.length + 1)));
53234
53808
  }
53235
- function renderFinding(f, showWeight = false) {
53809
+ function guardedWhat(f) {
53810
+ if (f.detail.length === 0) return "this";
53811
+ const reads = f.coverageProbe?.kind === "fileRead" ? "reads of " : "";
53812
+ const more = f.detail.length > 1 ? ` and ${f.detail.length - 1} more` : "";
53813
+ return `${reads}${f.detail[0]}${more}`;
53814
+ }
53815
+ function renderFinding(f, showWeight = false, displayLabel = f.category) {
53236
53816
  const lines = [];
53237
53817
  const wt = showWeight && f.scoreWeight ? chalk27.cyan.bold(`+${f.scoreWeight} `) : "";
53238
- lines.push(` ${ICON[f.severity]} ${label(f.category)}${wt}${f.title}`);
53818
+ lines.push(` ${ICON[f.severity]} ${label(displayLabel)}${wt}${f.title}`);
53239
53819
  const indent = " ".repeat(2 + 3 + LABEL_WIDTH);
53240
53820
  const width = 80 - indent.length;
53241
53821
  for (const s of [f.what, f.why, f.who]) {
@@ -53272,11 +53852,11 @@ function renderPosture(result) {
53272
53852
  );
53273
53853
  const headroom = openHeadroom(result.findings);
53274
53854
  if (headroom > 0) {
53275
- lines.push(
53276
- " " + chalk27.gray(
53277
- `${headroom} pts of headroom below \u2014 each layer adds security at some cost to flexibility. You choose which to close.`
53278
- )
53279
- );
53855
+ const openExposures = result.findings.filter(
53856
+ (f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix" && !f.scoreWeight && f.severity !== "advisory"
53857
+ ).length;
53858
+ const note = openExposures > 0 ? `Of the gap to 100: ${headroom} pts is optional hardening you can choose to turn on (the \u{1F512} tier below, each at some cost to flexibility); the rest is the open findings \u2014 the flagged rows below. Fix those first.` : `${headroom} pts of headroom \u2014 optional hardening you can choose to turn on (the \u{1F512} tier below), each at some cost to flexibility.`;
53859
+ for (const l of wrap(note, 76)) lines.push(" " + chalk27.gray(l));
53280
53860
  }
53281
53861
  lines.push("");
53282
53862
  if (result.headline) {
@@ -53289,13 +53869,18 @@ function renderPosture(result) {
53289
53869
  }
53290
53870
  const covered = result.findings.filter((f) => f.coverage?.state === "covered");
53291
53871
  const open = result.findings.filter((f) => f.coverage?.state !== "covered");
53872
+ const collision = new Set(
53873
+ covered.map((f) => f.category).filter((c) => open.some((o) => o.category === c))
53874
+ );
53875
+ const openLabel = (f) => collision.has(f.category) ? `${f.category} (exposed)` : f.category;
53292
53876
  if (covered.length > 0) {
53293
53877
  lines.push(" " + chalk27.green("\u{1F7E2} ON NOW \u2014 node9 is enforcing these (your floor)"));
53294
53878
  for (const f of covered) {
53295
53879
  const gated = f.coverage?.level === "review" ? "approval-gating" : "blocking";
53296
53880
  const via = f.coverage?.via ?? "node9";
53881
+ const lbl = collision.has(f.category) ? `${f.category} (guarded)` : f.category;
53297
53882
  lines.push(
53298
- ` ${chalk27.green("\u2705")} ${label(f.category)}${chalk27.gray(`${via} is ${gated} this`)}`
53883
+ ` ${chalk27.green("\u2705")} ${label(lbl)}${chalk27.gray(`${via} is ${gated} ${guardedWhat(f)}`)}`
53299
53884
  );
53300
53885
  }
53301
53886
  lines.push("");
@@ -53305,17 +53890,17 @@ function renderPosture(result) {
53305
53890
  const osOpen = open.filter((f) => f.owner !== "node9" && !f.node9Reduces);
53306
53891
  if (node9Open.length > 0) {
53307
53892
  lines.push(" " + chalk27.cyan.bold("\u{1F527} node9 can fix these \u2014 run the command"));
53308
- for (const f of node9Open) lines.push(...renderFinding(f, true));
53893
+ for (const f of node9Open) lines.push(...renderFinding(f, true, openLabel(f)));
53309
53894
  }
53310
53895
  if (reduceOpen.length > 0) {
53311
53896
  if (node9Open.length > 0) lines.push("");
53312
53897
  lines.push(" " + chalk27.yellow.bold("\u{1F512} AVAILABLE \u2014 turn on to harden (each has a tradeoff)"));
53313
- for (const f of reduceOpen) lines.push(...renderFinding(f, true));
53898
+ for (const f of reduceOpen) lines.push(...renderFinding(f, true, openLabel(f)));
53314
53899
  }
53315
53900
  if (osOpen.length > 0) {
53316
53901
  if (node9Open.length > 0 || reduceOpen.length > 0) lines.push("");
53317
53902
  lines.push(" " + chalk27.bold("\u{1F9F1} YOUR PART \u2014 node9 can't fix these (OS-level)"));
53318
- for (const f of osOpen) lines.push(...renderFinding(f));
53903
+ for (const f of osOpen) lines.push(...renderFinding(f, false, openLabel(f)));
53319
53904
  }
53320
53905
  for (const cat of result.passedCategories) {
53321
53906
  lines.push(` ${chalk27.green("\u2705")} ${label(cat)}${chalk27.gray("no issues found")}`);
@@ -53375,7 +53960,7 @@ import chalk30 from "chalk";
53375
53960
 
53376
53961
  // src/ci-check/fetch.ts
53377
53962
  var import_undici = __toESM(require_undici());
53378
- import fs61 from "fs";
53963
+ import fs62 from "fs";
53379
53964
  import path58 from "path";
53380
53965
  import { execFileSync as execFileSync2 } from "child_process";
53381
53966
  var cachedGhToken;
@@ -53459,7 +54044,7 @@ function parseRepoUrl(input) {
53459
54044
  function isLocalPath(input) {
53460
54045
  if (input.startsWith(".") || input.startsWith("/") || input.startsWith("~")) return true;
53461
54046
  try {
53462
- return fs61.existsSync(input) && fs61.statSync(input).isDirectory();
54047
+ return fs62.existsSync(input) && fs62.statSync(input).isDirectory();
53463
54048
  } catch {
53464
54049
  return false;
53465
54050
  }
@@ -53576,8 +54161,8 @@ function readLocalTree(dir) {
53576
54161
  const add = (rel) => {
53577
54162
  const abs = path58.join(root, rel);
53578
54163
  try {
53579
- if (fs61.existsSync(abs) && fs61.statSync(abs).isFile()) {
53580
- files.push({ path: rel, content: fs61.readFileSync(abs, "utf8") });
54164
+ if (fs62.existsSync(abs) && fs62.statSync(abs).isFile()) {
54165
+ files.push({ path: rel, content: fs62.readFileSync(abs, "utf8") });
53581
54166
  }
53582
54167
  } catch {
53583
54168
  }
@@ -53597,7 +54182,7 @@ function readLocalTree(dir) {
53597
54182
  dirsVisited++;
53598
54183
  let entries;
53599
54184
  try {
53600
- entries = fs61.readdirSync(path58.join(root, relDir), { withFileTypes: true });
54185
+ entries = fs62.readdirSync(path58.join(root, relDir), { withFileTypes: true });
53601
54186
  } catch {
53602
54187
  return;
53603
54188
  }
@@ -53620,8 +54205,8 @@ function readLocalTree(dir) {
53620
54205
  for (const rel of matches) collect(rel);
53621
54206
  const wfDir = path58.join(root, WORKFLOW_DIR);
53622
54207
  try {
53623
- if (fs61.existsSync(wfDir)) {
53624
- for (const name of fs61.readdirSync(wfDir)) {
54208
+ if (fs62.existsSync(wfDir)) {
54209
+ for (const name of fs62.readdirSync(wfDir)) {
53625
54210
  if (/\.ya?ml$/.test(name)) add(path58.join(WORKFLOW_DIR, name));
53626
54211
  }
53627
54212
  }
@@ -54762,7 +55347,7 @@ import chalk32 from "chalk";
54762
55347
  // src/shields/jail.ts
54763
55348
  init_build();
54764
55349
  init_shields();
54765
- import fs62 from "fs";
55350
+ import fs63 from "fs";
54766
55351
  import os54 from "os";
54767
55352
  import path59 from "path";
54768
55353
  var USER_JAIL_SHIELD = "user-jail";
@@ -54772,7 +55357,7 @@ function jailStorePath() {
54772
55357
  function readJailPaths() {
54773
55358
  let text;
54774
55359
  try {
54775
- text = fs62.readFileSync(jailStorePath(), "utf8");
55360
+ text = fs63.readFileSync(jailStorePath(), "utf8");
54776
55361
  } catch (err2) {
54777
55362
  if (err2.code === "ENOENT") return [];
54778
55363
  throw err2;
@@ -54790,8 +55375,8 @@ function readJailPaths() {
54790
55375
  }
54791
55376
  function writeJailPaths(paths) {
54792
55377
  const p = jailStorePath();
54793
- fs62.mkdirSync(path59.dirname(p), { recursive: true });
54794
- fs62.writeFileSync(p, JSON.stringify({ paths }, null, 2) + "\n", { mode: 384 });
55378
+ fs63.mkdirSync(path59.dirname(p), { recursive: true });
55379
+ fs63.writeFileSync(p, JSON.stringify({ paths }, null, 2) + "\n", { mode: 384 });
54795
55380
  }
54796
55381
  function addJailPath(rawPath, verdict) {
54797
55382
  const norm = rawPath.trim();
@@ -54820,7 +55405,7 @@ function regenerateUserJail(paths) {
54820
55405
  writeActiveShields(active2.filter((s) => s !== USER_JAIL_SHIELD));
54821
55406
  }
54822
55407
  try {
54823
- fs62.rmSync(file, { force: true });
55408
+ fs63.rmSync(file, { force: true });
54824
55409
  } catch {
54825
55410
  }
54826
55411
  return;
@@ -54935,12 +55520,12 @@ function registerJailCommand(program2) {
54935
55520
  // src/cli/commands/sandbox.ts
54936
55521
  init_config();
54937
55522
  import chalk33 from "chalk";
54938
- import fs65 from "fs";
55523
+ import fs66 from "fs";
54939
55524
  import path62 from "path";
54940
55525
  import { spawnSync as spawnSync6 } from "child_process";
54941
55526
 
54942
55527
  // src/sandbox/config.ts
54943
- import fs63 from "fs";
55528
+ import fs64 from "fs";
54944
55529
  import path60 from "path";
54945
55530
  import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
54946
55531
  var SANDBOX_CONFIG_FILE = "node9.sandbox.yaml";
@@ -55018,12 +55603,12 @@ function sandboxConfigPath(cwd = process.cwd()) {
55018
55603
  }
55019
55604
  function loadSandboxConfig(cwd = process.cwd(), fallbackAgent = "claude") {
55020
55605
  const p = sandboxConfigPath(cwd);
55021
- if (!fs63.existsSync(p)) {
55606
+ if (!fs64.existsSync(p)) {
55022
55607
  throw new Error(`sandbox: ${SANDBOX_CONFIG_FILE} not found \u2014 run \`node9 sandbox new\` first.`);
55023
55608
  }
55024
55609
  let raw;
55025
55610
  try {
55026
- raw = parseYaml2(fs63.readFileSync(p, "utf-8"));
55611
+ raw = parseYaml2(fs64.readFileSync(p, "utf-8"));
55027
55612
  } catch (err2) {
55028
55613
  throw new Error(
55029
55614
  `sandbox: ${SANDBOX_CONFIG_FILE} is not valid YAML \u2014 ${err2.message}`
@@ -55082,7 +55667,7 @@ init_templates();
55082
55667
 
55083
55668
  // src/sandbox/runtime.ts
55084
55669
  init_templates();
55085
- import fs64 from "fs";
55670
+ import fs65 from "fs";
55086
55671
  import os55 from "os";
55087
55672
  import path61 from "path";
55088
55673
  import crypto9 from "crypto";
@@ -55109,7 +55694,7 @@ function buildRunArgs(opts) {
55109
55694
  args.push("-v", `${allowlistHostPath}:${ALLOWED_DOMAINS_PATH}:ro`);
55110
55695
  if (config.node9.mountAgentCredentials) {
55111
55696
  const creds = agentCredentialsMount(config.agent);
55112
- if (fs64.existsSync(creds.hostPath)) {
55697
+ if (fs65.existsSync(creds.hostPath)) {
55113
55698
  args.push("-v", `${creds.hostPath}:${creds.target}`);
55114
55699
  }
55115
55700
  }
@@ -55131,16 +55716,16 @@ function sandboxBuildDir(cwd = process.cwd()) {
55131
55716
  }
55132
55717
  function writeBuildContext(cwd, dockerfile, entrypoint) {
55133
55718
  const dir = sandboxBuildDir(cwd);
55134
- fs64.mkdirSync(dir, { recursive: true });
55135
- fs64.writeFileSync(path61.join(dir, "Dockerfile"), dockerfile);
55136
- fs64.writeFileSync(path61.join(dir, "entrypoint.sh"), entrypoint);
55719
+ fs65.mkdirSync(dir, { recursive: true });
55720
+ fs65.writeFileSync(path61.join(dir, "Dockerfile"), dockerfile);
55721
+ fs65.writeFileSync(path61.join(dir, "entrypoint.sh"), entrypoint);
55137
55722
  return dir;
55138
55723
  }
55139
55724
  function writeAllowlist(cwd, hosts) {
55140
55725
  const dir = path61.join(cwd, ".node9", "sandbox");
55141
- fs64.mkdirSync(dir, { recursive: true });
55726
+ fs65.mkdirSync(dir, { recursive: true });
55142
55727
  const p = path61.join(dir, "allowed-domains.txt");
55143
- fs64.writeFileSync(p, hosts.join("\n") + "\n");
55728
+ fs65.writeFileSync(p, hosts.join("\n") + "\n");
55144
55729
  return p;
55145
55730
  }
55146
55731
  function resolveHomePath(p) {
@@ -55149,7 +55734,7 @@ function resolveHomePath(p) {
55149
55734
 
55150
55735
  // src/cli/commands/sandbox.ts
55151
55736
  function seedDataDirConfig(dataDir, sandbox) {
55152
- fs65.mkdirSync(dataDir, { recursive: true });
55737
+ fs66.mkdirSync(dataDir, { recursive: true });
55153
55738
  const configPath = path62.join(dataDir, "config.json");
55154
55739
  const seed = {
55155
55740
  settings: {
@@ -55161,7 +55746,7 @@ function seedDataDirConfig(dataDir, sandbox) {
55161
55746
  }
55162
55747
  }
55163
55748
  };
55164
- fs65.writeFileSync(configPath, JSON.stringify(seed, null, 2), { mode: 384 });
55749
+ fs66.writeFileSync(configPath, JSON.stringify(seed, null, 2), { mode: 384 });
55165
55750
  }
55166
55751
  function registerSandboxCommand(program2, version2) {
55167
55752
  const node9Version2 = pinnedNode9Version(version2);
@@ -55169,13 +55754,13 @@ function registerSandboxCommand(program2, version2) {
55169
55754
  cmd.command("new").description(`Scaffold ${SANDBOX_CONFIG_FILE} in this project`).option("--agent <agent>", "claude (default) or codex", "claude").action((opts) => {
55170
55755
  const agent = opts.agent === "codex" ? "codex" : "claude";
55171
55756
  const p = sandboxConfigPath();
55172
- if (fs65.existsSync(p)) {
55757
+ if (fs66.existsSync(p)) {
55173
55758
  console.log(
55174
55759
  chalk33.yellow(` ${SANDBOX_CONFIG_FILE} already exists \u2014 leaving it untouched.`)
55175
55760
  );
55176
55761
  return;
55177
55762
  }
55178
- fs65.writeFileSync(p, scaffoldSandboxYaml(agent));
55763
+ fs66.writeFileSync(p, scaffoldSandboxYaml(agent));
55179
55764
  console.log(
55180
55765
  chalk33.green(` \u2713 wrote ${SANDBOX_CONFIG_FILE}`) + chalk33.dim(` (agent: ${agent})`)
55181
55766
  );
@@ -55216,7 +55801,7 @@ function registerSandboxCommand(program2, version2) {
55216
55801
  const hash = imageContentHash(dockerfile, entrypoint);
55217
55802
  const image = sandbox.runtime.image;
55218
55803
  const hashFile = path62.join(sandboxBuildDir(cwd), ".image-hash");
55219
- const lastHash = fs65.existsSync(hashFile) ? fs65.readFileSync(hashFile, "utf-8").trim() : "";
55804
+ const lastHash = fs66.existsSync(hashFile) ? fs66.readFileSync(hashFile, "utf-8").trim() : "";
55220
55805
  const imageExists = spawnSync6(sandbox.runtime.engine, ["image", "inspect", image], { stdio: "ignore" }).status === 0;
55221
55806
  const needBuild = sandbox.runtime.rebuild === "always" || !imageExists || sandbox.runtime.rebuild !== "never" && lastHash !== hash;
55222
55807
  if (needBuild) {
@@ -55228,7 +55813,7 @@ function registerSandboxCommand(program2, version2) {
55228
55813
  console.error(chalk33.red(" build failed."));
55229
55814
  process.exit(b.status ?? 1);
55230
55815
  }
55231
- fs65.writeFileSync(hashFile, hash);
55816
+ fs66.writeFileSync(hashFile, hash);
55232
55817
  }
55233
55818
  const dataDir = sandboxDataDir(cwd);
55234
55819
  seedDataDirConfig(dataDir, sandbox);
@@ -55242,7 +55827,7 @@ function registerSandboxCommand(program2, version2) {
55242
55827
  });
55243
55828
  if (sandbox.node9.mountAgentCredentials) {
55244
55829
  const creds = agentCredentialsMount(sandbox.agent);
55245
- if (fs65.existsSync(creds.hostPath)) {
55830
+ if (fs66.existsSync(creds.hostPath)) {
55246
55831
  console.log(chalk33.dim(` mounting ${creds.hostPath} (agent credentials, rw)`));
55247
55832
  } else {
55248
55833
  console.log(
@@ -55259,7 +55844,7 @@ function registerSandboxCommand(program2, version2) {
55259
55844
  });
55260
55845
  cmd.command("tail").description("Stream the sandbox's audit log (host-side)").action(() => {
55261
55846
  const auditPath = path62.join(sandboxDataDir(), "audit.log");
55262
- if (!fs65.existsSync(auditPath)) {
55847
+ if (!fs66.existsSync(auditPath)) {
55263
55848
  console.log(chalk33.dim(" no sandbox audit yet."));
55264
55849
  return;
55265
55850
  }
@@ -55267,11 +55852,11 @@ function registerSandboxCommand(program2, version2) {
55267
55852
  });
55268
55853
  cmd.command("logs").description("Dump the sandbox's audit log").action(() => {
55269
55854
  const auditPath = path62.join(sandboxDataDir(), "audit.log");
55270
- if (!fs65.existsSync(auditPath)) {
55855
+ if (!fs66.existsSync(auditPath)) {
55271
55856
  console.log(chalk33.dim(" no sandbox audit yet."));
55272
55857
  return;
55273
55858
  }
55274
- process.stdout.write(fs65.readFileSync(auditPath, "utf-8"));
55859
+ process.stdout.write(fs66.readFileSync(auditPath, "utf-8"));
55275
55860
  });
55276
55861
  cmd.command("clean").description("Remove the sandbox image, build context, and data").action(() => {
55277
55862
  const cwd = process.cwd();
@@ -55285,18 +55870,19 @@ function registerSandboxCommand(program2, version2) {
55285
55870
  stdio: "ignore"
55286
55871
  });
55287
55872
  }
55288
- fs65.rmSync(path62.join(cwd, ".node9", "sandbox"), { recursive: true, force: true });
55873
+ fs66.rmSync(path62.join(cwd, ".node9", "sandbox"), { recursive: true, force: true });
55289
55874
  console.log(chalk33.green(" \u2713 sandbox image + build + data removed."));
55290
55875
  });
55291
55876
  }
55292
55877
 
55293
55878
  // src/cli/commands/sessions.ts
55879
+ init_decision();
55294
55880
  init_scan_summary();
55295
55881
  init_litellm();
55296
55882
  init_cost_gemini();
55297
55883
  init_cost_codex();
55298
55884
  import chalk34 from "chalk";
55299
- import fs66 from "fs";
55885
+ import fs67 from "fs";
55300
55886
  import path63 from "path";
55301
55887
  import os56 from "os";
55302
55888
  function modelPrice(model) {
@@ -55390,7 +55976,7 @@ function loadAuditEntries(auditPath) {
55390
55976
  const aPath = auditPath ?? path63.join(os56.homedir(), ".node9", "audit.log");
55391
55977
  let raw;
55392
55978
  try {
55393
- raw = fs66.readFileSync(aPath, "utf-8");
55979
+ raw = fs67.readFileSync(aPath, "utf-8");
55394
55980
  } catch {
55395
55981
  return [];
55396
55982
  }
@@ -55400,7 +55986,7 @@ function loadAuditEntries(auditPath) {
55400
55986
  try {
55401
55987
  const e = JSON.parse(line);
55402
55988
  if (!e.ts || !e.tool || !e.decision) continue;
55403
- if (e.decision === "allow" || e.decision === "allowed") continue;
55989
+ if (classifyDecision(e).outcome === "allow") continue;
55404
55990
  entries.push(e);
55405
55991
  } catch {
55406
55992
  }
@@ -55427,7 +56013,7 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
55427
56013
  }
55428
56014
  function buildGeminiSessions(days, allAuditEntries) {
55429
56015
  const tmpDir = path63.join(os56.homedir(), ".gemini", "tmp");
55430
- if (!fs66.existsSync(tmpDir)) return [];
56016
+ if (!fs67.existsSync(tmpDir)) return [];
55431
56017
  const cutoff = days !== null ? (() => {
55432
56018
  const d = /* @__PURE__ */ new Date();
55433
56019
  d.setDate(d.getDate() - days);
@@ -55436,7 +56022,7 @@ function buildGeminiSessions(days, allAuditEntries) {
55436
56022
  })() : null;
55437
56023
  let slugDirs;
55438
56024
  try {
55439
- slugDirs = fs66.readdirSync(tmpDir);
56025
+ slugDirs = fs67.readdirSync(tmpDir);
55440
56026
  } catch {
55441
56027
  return [];
55442
56028
  }
@@ -55444,27 +56030,27 @@ function buildGeminiSessions(days, allAuditEntries) {
55444
56030
  for (const slug2 of slugDirs) {
55445
56031
  const slugPath = path63.join(tmpDir, slug2);
55446
56032
  try {
55447
- if (!fs66.statSync(slugPath).isDirectory()) continue;
56033
+ if (!fs67.statSync(slugPath).isDirectory()) continue;
55448
56034
  } catch {
55449
56035
  continue;
55450
56036
  }
55451
56037
  let projectRoot = path63.join(os56.homedir(), slug2);
55452
56038
  try {
55453
- projectRoot = fs66.readFileSync(path63.join(slugPath, ".project_root"), "utf-8").trim();
56039
+ projectRoot = fs67.readFileSync(path63.join(slugPath, ".project_root"), "utf-8").trim();
55454
56040
  } catch {
55455
56041
  }
55456
56042
  const chatsDir = path63.join(slugPath, "chats");
55457
- if (!fs66.existsSync(chatsDir)) continue;
56043
+ if (!fs67.existsSync(chatsDir)) continue;
55458
56044
  let chatFiles;
55459
56045
  try {
55460
- chatFiles = fs66.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
56046
+ chatFiles = fs67.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
55461
56047
  } catch {
55462
56048
  continue;
55463
56049
  }
55464
56050
  for (const chatFile of chatFiles) {
55465
56051
  let raw;
55466
56052
  try {
55467
- raw = fs66.readFileSync(path63.join(chatsDir, chatFile), "utf-8");
56053
+ raw = fs67.readFileSync(path63.join(chatsDir, chatFile), "utf-8");
55468
56054
  } catch {
55469
56055
  continue;
55470
56056
  }
@@ -55545,7 +56131,7 @@ function buildGeminiSessions(days, allAuditEntries) {
55545
56131
  }
55546
56132
  function buildCodexSessions(days, allAuditEntries) {
55547
56133
  const sessionsBase = path63.join(os56.homedir(), ".codex", "sessions");
55548
- if (!fs66.existsSync(sessionsBase)) return [];
56134
+ if (!fs67.existsSync(sessionsBase)) return [];
55549
56135
  const cutoff = days !== null ? (() => {
55550
56136
  const d = /* @__PURE__ */ new Date();
55551
56137
  d.setDate(d.getDate() - days);
@@ -55554,28 +56140,28 @@ function buildCodexSessions(days, allAuditEntries) {
55554
56140
  })() : null;
55555
56141
  const jsonlFiles = [];
55556
56142
  try {
55557
- for (const year of fs66.readdirSync(sessionsBase)) {
56143
+ for (const year of fs67.readdirSync(sessionsBase)) {
55558
56144
  const yearPath = path63.join(sessionsBase, year);
55559
56145
  try {
55560
- if (!fs66.statSync(yearPath).isDirectory()) continue;
56146
+ if (!fs67.statSync(yearPath).isDirectory()) continue;
55561
56147
  } catch {
55562
56148
  continue;
55563
56149
  }
55564
- for (const month of fs66.readdirSync(yearPath)) {
56150
+ for (const month of fs67.readdirSync(yearPath)) {
55565
56151
  const monthPath = path63.join(yearPath, month);
55566
56152
  try {
55567
- if (!fs66.statSync(monthPath).isDirectory()) continue;
56153
+ if (!fs67.statSync(monthPath).isDirectory()) continue;
55568
56154
  } catch {
55569
56155
  continue;
55570
56156
  }
55571
- for (const day of fs66.readdirSync(monthPath)) {
56157
+ for (const day of fs67.readdirSync(monthPath)) {
55572
56158
  const dayPath = path63.join(monthPath, day);
55573
56159
  try {
55574
- if (!fs66.statSync(dayPath).isDirectory()) continue;
56160
+ if (!fs67.statSync(dayPath).isDirectory()) continue;
55575
56161
  } catch {
55576
56162
  continue;
55577
56163
  }
55578
- for (const file of fs66.readdirSync(dayPath)) {
56164
+ for (const file of fs67.readdirSync(dayPath)) {
55579
56165
  if (file.endsWith(".jsonl")) jsonlFiles.push(path63.join(dayPath, file));
55580
56166
  }
55581
56167
  }
@@ -55588,7 +56174,7 @@ function buildCodexSessions(days, allAuditEntries) {
55588
56174
  for (const filePath of jsonlFiles) {
55589
56175
  let lines;
55590
56176
  try {
55591
- lines = fs66.readFileSync(filePath, "utf-8").split("\n");
56177
+ lines = fs67.readFileSync(filePath, "utf-8").split("\n");
55592
56178
  } catch {
55593
56179
  continue;
55594
56180
  }
@@ -55677,7 +56263,7 @@ function buildSessions(days, historyPath) {
55677
56263
  const hPath = historyPath ?? path63.join(os56.homedir(), ".claude", "history.jsonl");
55678
56264
  let historyRaw = "";
55679
56265
  try {
55680
- historyRaw = fs66.readFileSync(hPath, "utf-8");
56266
+ historyRaw = fs67.readFileSync(hPath, "utf-8");
55681
56267
  } catch {
55682
56268
  }
55683
56269
  const cutoff = days !== null ? (() => {
@@ -55701,7 +56287,7 @@ function buildSessions(days, historyPath) {
55701
56287
  const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
55702
56288
  let sessionLines = [];
55703
56289
  try {
55704
- sessionLines = fs66.readFileSync(jsonlFile, "utf-8").split("\n");
56290
+ sessionLines = fs67.readFileSync(jsonlFile, "utf-8").split("\n");
55705
56291
  } catch {
55706
56292
  }
55707
56293
  const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
@@ -56095,12 +56681,12 @@ function registerSessionTaintCommand(program2) {
56095
56681
 
56096
56682
  // src/cli/commands/skill-pin.ts
56097
56683
  import chalk36 from "chalk";
56098
- import fs67 from "fs";
56684
+ import fs68 from "fs";
56099
56685
  import os57 from "os";
56100
56686
  import path64 from "path";
56101
56687
  function wipeSkillSessions() {
56102
56688
  try {
56103
- fs67.rmSync(path64.join(os57.homedir(), ".node9", "skill-sessions"), {
56689
+ fs68.rmSync(path64.join(os57.homedir(), ".node9", "skill-sessions"), {
56104
56690
  recursive: true,
56105
56691
  force: true
56106
56692
  });
@@ -56182,15 +56768,15 @@ function registerSkillPinCommand(program2) {
56182
56768
  }
56183
56769
 
56184
56770
  // src/cli/commands/decisions.ts
56185
- import fs68 from "fs";
56771
+ import fs69 from "fs";
56186
56772
  import os58 from "os";
56187
56773
  import path65 from "path";
56188
56774
  import chalk37 from "chalk";
56189
56775
  var DECISIONS_FILE2 = path65.join(os58.homedir(), ".node9", "decisions.json");
56190
56776
  function readDecisions() {
56191
56777
  try {
56192
- if (!fs68.existsSync(DECISIONS_FILE2)) return {};
56193
- const raw = fs68.readFileSync(DECISIONS_FILE2, "utf-8");
56778
+ if (!fs69.existsSync(DECISIONS_FILE2)) return {};
56779
+ const raw = fs69.readFileSync(DECISIONS_FILE2, "utf-8");
56194
56780
  const parsed = JSON.parse(raw);
56195
56781
  const out = {};
56196
56782
  for (const [k, v] of Object.entries(parsed)) {
@@ -56203,10 +56789,10 @@ function readDecisions() {
56203
56789
  }
56204
56790
  function writeDecisions(d) {
56205
56791
  const dir = path65.dirname(DECISIONS_FILE2);
56206
- if (!fs68.existsSync(dir)) fs68.mkdirSync(dir, { recursive: true });
56792
+ if (!fs69.existsSync(dir)) fs69.mkdirSync(dir, { recursive: true });
56207
56793
  const tmp = `${DECISIONS_FILE2}.${process.pid}.tmp`;
56208
- fs68.writeFileSync(tmp, JSON.stringify(d, null, 2));
56209
- fs68.renameSync(tmp, DECISIONS_FILE2);
56794
+ fs69.writeFileSync(tmp, JSON.stringify(d, null, 2));
56795
+ fs69.renameSync(tmp, DECISIONS_FILE2);
56210
56796
  }
56211
56797
  function registerDecisionsCommand(program2) {
56212
56798
  const cmd = program2.command("decisions").description('Manage persistent "Always Allow" / "Always Deny" tool decisions');
@@ -56263,7 +56849,7 @@ Persistent decisions (${entries.length})
56263
56849
 
56264
56850
  // src/cli/commands/dlp.ts
56265
56851
  import chalk38 from "chalk";
56266
- import fs69 from "fs";
56852
+ import fs70 from "fs";
56267
56853
  import path66 from "path";
56268
56854
  import os59 from "os";
56269
56855
  var AUDIT_LOG = path66.join(os59.homedir(), ".node9", "audit.log");
@@ -56274,7 +56860,7 @@ function stripAnsi(s) {
56274
56860
  }
56275
56861
  function loadResolved() {
56276
56862
  try {
56277
- const raw = JSON.parse(fs69.readFileSync(RESOLVED_FILE, "utf-8"));
56863
+ const raw = JSON.parse(fs70.readFileSync(RESOLVED_FILE, "utf-8"));
56278
56864
  return new Set(raw);
56279
56865
  } catch {
56280
56866
  return /* @__PURE__ */ new Set();
@@ -56282,13 +56868,13 @@ function loadResolved() {
56282
56868
  }
56283
56869
  function saveResolved(resolved) {
56284
56870
  try {
56285
- fs69.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
56871
+ fs70.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
56286
56872
  } catch {
56287
56873
  }
56288
56874
  }
56289
56875
  function loadDlpFindings() {
56290
- if (!fs69.existsSync(AUDIT_LOG)) return [];
56291
- return fs69.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
56876
+ if (!fs70.existsSync(AUDIT_LOG)) return [];
56877
+ return fs70.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
56292
56878
  if (!line.trim()) return [];
56293
56879
  try {
56294
56880
  const e = JSON.parse(line);
@@ -56387,13 +56973,13 @@ function registerDlpCommand(program2) {
56387
56973
  // src/cli/commands/mask.ts
56388
56974
  init_dlp();
56389
56975
  import chalk39 from "chalk";
56390
- import fs70 from "fs";
56976
+ import fs71 from "fs";
56391
56977
  import path67 from "path";
56392
56978
  import os60 from "os";
56393
56979
  function findJsonlFiles(dir) {
56394
56980
  const results = [];
56395
- if (!fs70.existsSync(dir)) return results;
56396
- for (const entry of fs70.readdirSync(dir, { withFileTypes: true })) {
56981
+ if (!fs71.existsSync(dir)) return results;
56982
+ for (const entry of fs71.readdirSync(dir, { withFileTypes: true })) {
56397
56983
  const full = path67.join(dir, entry.name);
56398
56984
  if (entry.isDirectory()) results.push(...findJsonlFiles(full));
56399
56985
  else if (entry.isFile() && entry.name.endsWith(".jsonl")) results.push(full);
@@ -56437,7 +57023,7 @@ function redactJson(obj) {
56437
57023
  function processFile(filePath, dryRun) {
56438
57024
  let raw;
56439
57025
  try {
56440
- raw = fs70.readFileSync(filePath, "utf-8");
57026
+ raw = fs71.readFileSync(filePath, "utf-8");
56441
57027
  } catch {
56442
57028
  return { redactedLines: 0, patterns: [] };
56443
57029
  }
@@ -56469,14 +57055,14 @@ function processFile(filePath, dryRun) {
56469
57055
  }
56470
57056
  }
56471
57057
  if (!dryRun && redactedLines > 0) {
56472
- fs70.writeFileSync(filePath, newLines.join("\n"), "utf-8");
57058
+ fs71.writeFileSync(filePath, newLines.join("\n"), "utf-8");
56473
57059
  }
56474
57060
  return { redactedLines, patterns };
56475
57061
  }
56476
57062
  function processJsonFile(filePath, dryRun) {
56477
57063
  let raw;
56478
57064
  try {
56479
- raw = fs70.readFileSync(filePath, "utf-8");
57065
+ raw = fs71.readFileSync(filePath, "utf-8");
56480
57066
  } catch {
56481
57067
  return { redactedLines: 0, patterns: [] };
56482
57068
  }
@@ -56489,14 +57075,14 @@ function processJsonFile(filePath, dryRun) {
56489
57075
  const { value, modified, found } = redactJson(parsed);
56490
57076
  if (!modified) return { redactedLines: 0, patterns: [] };
56491
57077
  if (!dryRun) {
56492
- fs70.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
57078
+ fs71.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
56493
57079
  }
56494
57080
  return { redactedLines: 1, patterns: found };
56495
57081
  }
56496
57082
  function findJsonFiles(dir) {
56497
57083
  const results = [];
56498
- if (!fs70.existsSync(dir)) return results;
56499
- for (const entry of fs70.readdirSync(dir, { withFileTypes: true })) {
57084
+ if (!fs71.existsSync(dir)) return results;
57085
+ for (const entry of fs71.readdirSync(dir, { withFileTypes: true })) {
56500
57086
  const full = path67.join(dir, entry.name);
56501
57087
  if (entry.isDirectory()) results.push(...findJsonFiles(full));
56502
57088
  else if (entry.isFile() && entry.name.endsWith(".json")) results.push(full);
@@ -56516,7 +57102,7 @@ function registerMaskCommand(program2) {
56516
57102
  const cutoff = options.all ? null : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
56517
57103
  const filtered = cutoff ? allFiles.filter((f) => {
56518
57104
  try {
56519
- return fs70.statSync(f.path).mtime >= cutoff;
57105
+ return fs71.statSync(f.path).mtime >= cutoff;
56520
57106
  } catch {
56521
57107
  return false;
56522
57108
  }
@@ -56572,7 +57158,7 @@ function registerMaskCommand(program2) {
56572
57158
  // src/cli.ts
56573
57159
  init_blast();
56574
57160
  var { version } = JSON.parse(
56575
- fs73.readFileSync(path70.join(__dirname, "../package.json"), "utf-8")
57161
+ fs74.readFileSync(path70.join(__dirname, "../package.json"), "utf-8")
56576
57162
  );
56577
57163
  var program = new Command();
56578
57164
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
@@ -56752,14 +57338,14 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
56752
57338
  }
56753
57339
  if (options.purge) {
56754
57340
  const node9Dir = path70.join(os63.homedir(), ".node9");
56755
- if (fs73.existsSync(node9Dir)) {
57341
+ if (fs74.existsSync(node9Dir)) {
56756
57342
  const confirmed = await confirm2({
56757
57343
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
56758
57344
  default: false
56759
57345
  });
56760
57346
  if (confirmed) {
56761
- fs73.rmSync(node9Dir, { recursive: true });
56762
- if (fs73.existsSync(node9Dir)) {
57347
+ fs74.rmSync(node9Dir, { recursive: true });
57348
+ if (fs74.existsSync(node9Dir)) {
56763
57349
  console.error(
56764
57350
  chalk41.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
56765
57351
  );
@@ -56924,12 +57510,12 @@ Run "node9 addto claude" to register it as the statusLine.`
56924
57510
  if (subcommand === "debug") {
56925
57511
  const flagFile = path70.join(os63.homedir(), ".node9", "hud-debug");
56926
57512
  if (state === "on") {
56927
- fs73.mkdirSync(path70.dirname(flagFile), { recursive: true });
56928
- fs73.writeFileSync(flagFile, "");
57513
+ fs74.mkdirSync(path70.dirname(flagFile), { recursive: true });
57514
+ fs74.writeFileSync(flagFile, "");
56929
57515
  console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
56930
57516
  console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
56931
57517
  } else if (state === "off") {
56932
- if (fs73.existsSync(flagFile)) fs73.unlinkSync(flagFile);
57518
+ if (fs74.existsSync(flagFile)) fs74.unlinkSync(flagFile);
56933
57519
  console.log("HUD debug logging disabled.");
56934
57520
  } else {
56935
57521
  console.error("Usage: node9 hud debug on|off");
@@ -57054,7 +57640,7 @@ if (process.argv[2] !== "daemon") {
57054
57640
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
57055
57641
  const logPath = path70.join(os63.homedir(), ".node9", "hook-debug.log");
57056
57642
  const msg = reason instanceof Error ? reason.message : String(reason);
57057
- fs73.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
57643
+ fs74.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
57058
57644
  `);
57059
57645
  }
57060
57646
  process.exit(0);