@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.js CHANGED
@@ -343,6 +343,7 @@ var init_config_schema = __esm({
343
343
  // nudge-only (default), and the scan cadence in minutes.
344
344
  mcpAutoWrap: import_zod.z.boolean().optional(),
345
345
  mcpReconcileIntervalMinutes: import_zod.z.number().positive().optional(),
346
+ mcpStaleAfterDays: import_zod.z.number().min(0).optional(),
346
347
  cloudSyncIntervalHours: import_zod.z.number().positive().optional(),
347
348
  // Seconds-granular override for the cloud policy sync cadence. Wins over
348
349
  // cloudSyncIntervalHours when set. Lets you opt into fast apply (e.g. 20)
@@ -3977,6 +3978,7 @@ var init_dist = __esm({
3977
3978
  name: "redis",
3978
3979
  description: "Protects Redis instances from destructive AI operations",
3979
3980
  aliases: [],
3981
+ _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.",
3980
3982
  smartRules: [
3981
3983
  {
3982
3984
  name: "shield:redis:block-flushall",
@@ -3985,7 +3987,7 @@ var init_dist = __esm({
3985
3987
  {
3986
3988
  field: "command",
3987
3989
  op: "matches",
3988
- value: "\\bFLUSHALL\\b",
3990
+ value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
3989
3991
  flags: "i"
3990
3992
  }
3991
3993
  ],
@@ -3999,7 +4001,7 @@ var init_dist = __esm({
3999
4001
  {
4000
4002
  field: "command",
4001
4003
  op: "matches",
4002
- value: "\\bFLUSHDB\\b",
4004
+ value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
4003
4005
  flags: "i"
4004
4006
  }
4005
4007
  ],
@@ -4013,7 +4015,7 @@ var init_dist = __esm({
4013
4015
  {
4014
4016
  field: "command",
4015
4017
  op: "matches",
4016
- value: "\\bCONFIG\\s+RESETSTAT\\b",
4018
+ value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
4017
4019
  flags: "i"
4018
4020
  }
4019
4021
  ],
@@ -4027,7 +4029,7 @@ var init_dist = __esm({
4027
4029
  {
4028
4030
  field: "command",
4029
4031
  op: "matches",
4030
- value: "\\bCONFIG\\s+SET\\b",
4032
+ value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
4031
4033
  flags: "i"
4032
4034
  }
4033
4035
  ],
@@ -4041,7 +4043,7 @@ var init_dist = __esm({
4041
4043
  {
4042
4044
  field: "command",
4043
4045
  op: "matches",
4044
- value: "\\bDEL\\b.*[*?\\[]|redis-cli.*--scan.*\\|.*xargs.*del",
4046
+ value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
4045
4047
  flags: "i"
4046
4048
  }
4047
4049
  ],
@@ -4608,6 +4610,48 @@ function getActiveEnvironment(config) {
4608
4610
  const env = config.settings.environment || process.env.NODE_ENV || "development";
4609
4611
  return config.environments[env] ?? null;
4610
4612
  }
4613
+ function readRulesCacheResilient(cacheFile) {
4614
+ let existed = false;
4615
+ for (let attempt = 0; attempt < 3; attempt++) {
4616
+ let content;
4617
+ try {
4618
+ content = import_fs4.default.readFileSync(cacheFile, "utf-8");
4619
+ existed = true;
4620
+ } catch (err2) {
4621
+ if (err2.code === "ENOENT") return {};
4622
+ continue;
4623
+ }
4624
+ try {
4625
+ return JSON.parse(content);
4626
+ } catch {
4627
+ }
4628
+ }
4629
+ if (existed) {
4630
+ const backup = import_path4.default.join(import_path4.default.dirname(cacheFile), "rules-cache.last-good.json");
4631
+ if (backup !== cacheFile) {
4632
+ try {
4633
+ const raw = JSON.parse(import_fs4.default.readFileSync(backup, "utf-8"));
4634
+ logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
4635
+ return raw;
4636
+ } catch {
4637
+ }
4638
+ }
4639
+ logCacheReadIssue(cacheFile, "RULES_CACHE_UNREADABLE");
4640
+ }
4641
+ return {};
4642
+ }
4643
+ function logCacheReadIssue(cacheFile, kind) {
4644
+ if (cacheReadFailureLogged) return;
4645
+ cacheReadFailureLogged = true;
4646
+ try {
4647
+ import_fs4.default.appendFileSync(
4648
+ import_path4.default.join(import_os4.default.homedir(), ".node9", "hook-debug.log"),
4649
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
4650
+ `
4651
+ );
4652
+ } catch {
4653
+ }
4654
+ }
4611
4655
  function getConfig(cwd) {
4612
4656
  if (!cwd && cachedConfig) return cachedConfig;
4613
4657
  const globalPath = import_path4.default.join(import_os4.default.homedir(), ".node9", "config.json");
@@ -4675,6 +4719,7 @@ function getConfig(cwd) {
4675
4719
  if (s.mcpAutoWrap !== void 0) mergedSettings.mcpAutoWrap = s.mcpAutoWrap === true;
4676
4720
  if (s.mcpReconcileIntervalMinutes !== void 0)
4677
4721
  mergedSettings.mcpReconcileIntervalMinutes = s.mcpReconcileIntervalMinutes;
4722
+ if (s.mcpStaleAfterDays !== void 0) mergedSettings.mcpStaleAfterDays = s.mcpStaleAfterDays;
4678
4723
  if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
4679
4724
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
4680
4725
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -4684,12 +4729,13 @@ function getConfig(cwd) {
4684
4729
  if (p.smartRules) {
4685
4730
  const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
4686
4731
  const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
4687
- const userRuleNames = new Set(p.smartRules.filter((r) => r.name).map((r) => r.name));
4732
+ const localRules = p.smartRules.map(({ pinned: _pinned, ...r }) => r);
4733
+ const userRuleNames = new Set(localRules.filter((r) => r.name).map((r) => r.name));
4688
4734
  const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
4689
4735
  const filteredNonBlocks = defaultNonBlocks.filter(
4690
4736
  (r) => !r.name || !userRuleNames.has(r.name)
4691
4737
  );
4692
- mergedPolicy.smartRules = [...filteredBlocks, ...p.smartRules, ...filteredNonBlocks];
4738
+ mergedPolicy.smartRules = [...filteredBlocks, ...localRules, ...filteredNonBlocks];
4693
4739
  }
4694
4740
  if (p.snapshot) {
4695
4741
  const s2 = p.snapshot;
@@ -4754,10 +4800,12 @@ function getConfig(cwd) {
4754
4800
  applyLayer(globalConfig);
4755
4801
  applyLayer(projectConfig);
4756
4802
  let cloudManagedShields = [];
4803
+ let modeCloudControlled = false;
4804
+ let modeCloudStaged = false;
4757
4805
  {
4758
4806
  const cacheFile = import_path4.default.join(import_os4.default.homedir(), ".node9", "rules-cache.json");
4759
4807
  try {
4760
- const raw = JSON.parse(import_fs4.default.readFileSync(cacheFile, "utf-8"));
4808
+ const raw = readRulesCacheResilient(cacheFile);
4761
4809
  if (Array.isArray(raw.rules) && raw.rules.length > 0) {
4762
4810
  applyLayer({ policy: { smartRules: raw.rules } });
4763
4811
  }
@@ -4774,6 +4822,9 @@ function getConfig(cwd) {
4774
4822
  locked.includes("mode")
4775
4823
  );
4776
4824
  }
4825
+ if (typeof mc.mode === "string" || locked.includes("mode")) {
4826
+ modeCloudControlled = true;
4827
+ }
4777
4828
  if (mc.egress && typeof mc.egress === "object") {
4778
4829
  const hosts = (v) => Array.isArray(v) ? v.filter((h) => typeof h === "string") : void 0;
4779
4830
  mergedPolicy.egress = applyManagedEgress(
@@ -4872,19 +4923,28 @@ function getConfig(cwd) {
4872
4923
  }
4873
4924
  if (raw.shadowMode === true) {
4874
4925
  mergedSettings.mode = "observe";
4926
+ modeCloudStaged = true;
4875
4927
  }
4876
4928
  } catch {
4877
4929
  }
4878
4930
  }
4879
4931
  const shieldOverrides = readShieldOverrides();
4880
4932
  const activeShieldNames = [.../* @__PURE__ */ new Set([...readActiveShields(), ...cloudManagedShields])];
4933
+ const cloudManagedSet = new Set(cloudManagedShields);
4881
4934
  for (const shieldName of activeShieldNames) {
4882
- const shield = getShield(shieldName);
4935
+ const isCloudMandated = cloudManagedSet.has(shieldName);
4936
+ const shield = isCloudMandated ? BUILTIN_SHIELDS[shieldName] : getShield(shieldName);
4883
4937
  if (!shield) continue;
4884
4938
  const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4885
- const ruleOverrides = shieldOverrides[shieldName] ?? {};
4939
+ const ruleOverrides = isCloudMandated ? {} : shieldOverrides[shieldName] ?? {};
4886
4940
  for (const rule of shield.smartRules) {
4887
- if (!existingRuleNames.has(rule.name)) {
4941
+ const collides = rule.name ? existingRuleNames.has(rule.name) : false;
4942
+ if (isCloudMandated) {
4943
+ if (collides) {
4944
+ mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
4945
+ }
4946
+ mergedPolicy.smartRules.push({ ...rule, pinned: true });
4947
+ } else if (!collides) {
4888
4948
  const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
4889
4949
  mergedPolicy.smartRules.push(
4890
4950
  overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
@@ -4900,7 +4960,27 @@ function getConfig(cwd) {
4900
4960
  for (const rule of ADVISORY_SMART_RULES) {
4901
4961
  if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
4902
4962
  }
4903
- if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
4963
+ const envMode = process.env.NODE9_MODE;
4964
+ if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
4965
+ mergedSettings.mode = envMode;
4966
+ }
4967
+ if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
4968
+ mergedSettings.mode = "standard";
4969
+ }
4970
+ const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
4971
+ if (modeCloudControlled && mergedSettings.mode === "strict") {
4972
+ for (const name of Object.keys(mergedEnvironments)) {
4973
+ if (mergedEnvironments[name]?.requireApproval === false) {
4974
+ const cleaned = { ...mergedEnvironments[name] };
4975
+ delete cleaned.requireApproval;
4976
+ mergedEnvironments[name] = cleaned;
4977
+ }
4978
+ }
4979
+ }
4980
+ if (managedFloorActive) {
4981
+ mergedPolicy.ignoredTools = [...DEFAULT_CONFIG.policy.ignoredTools];
4982
+ mergedPolicy.sandboxPaths = [...DEFAULT_CONFIG.policy.sandboxPaths];
4983
+ }
4904
4984
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
4905
4985
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
4906
4986
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
@@ -4969,7 +5049,7 @@ ${error.replace("Invalid config:\n", "")}
4969
5049
  }
4970
5050
  return sanitized;
4971
5051
  }
4972
- var import_fs4, import_path4, import_os4, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig;
5052
+ var import_fs4, import_path4, import_os4, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, cacheReadFailureLogged;
4973
5053
  var init_config = __esm({
4974
5054
  "src/config/index.ts"() {
4975
5055
  "use strict";
@@ -4981,6 +5061,7 @@ var init_config = __esm({
4981
5061
  init_managed();
4982
5062
  init_build();
4983
5063
  init_trusted_hosts();
5064
+ init_dist();
4984
5065
  DANGEROUS_WORDS = [
4985
5066
  "mkfs",
4986
5067
  // formats/wipes a filesystem partition
@@ -5258,6 +5339,7 @@ var init_config = __esm({
5258
5339
  }
5259
5340
  ];
5260
5341
  cachedConfig = null;
5342
+ cacheReadFailureLogged = false;
5261
5343
  }
5262
5344
  });
5263
5345
 
@@ -5975,6 +6057,18 @@ async function isDaemonReachable(timeoutMs = 500) {
5975
6057
  return false;
5976
6058
  }
5977
6059
  }
6060
+ async function daemonHasInteractiveApprover(timeoutMs = 400) {
6061
+ try {
6062
+ const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/approver`, {
6063
+ signal: AbortSignal.timeout(timeoutMs)
6064
+ });
6065
+ if (!res.ok) return false;
6066
+ const body = await res.json();
6067
+ return body.interactive === true;
6068
+ } catch {
6069
+ return false;
6070
+ }
6071
+ }
5978
6072
  async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent) {
5979
6073
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
5980
6074
  const ctrl = new AbortController();
@@ -6859,6 +6953,12 @@ function isNetworkTool(toolName, args) {
6859
6953
  function notifyActivity(data) {
6860
6954
  return notifyActivitySocket(data);
6861
6955
  }
6956
+ async function hasReachableHumanApprover(opts) {
6957
+ const hasDisplay = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
6958
+ const nativeReachable = !opts.calledFromDaemon && opts.approvers.native !== false && hasDisplay;
6959
+ if (nativeReachable) return true;
6960
+ return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
6961
+ }
6862
6962
  async function authorizeHeadless(toolName, args, meta, options) {
6863
6963
  if (!options?.calledFromDaemon) {
6864
6964
  const actId = (0, import_crypto5.randomUUID)();
@@ -7173,6 +7273,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7173
7273
  return { approved: true, checkedBy: "local-policy" };
7174
7274
  }
7175
7275
  if (policyResult.decision === "block") {
7276
+ const daemonUp = isDaemonRunning();
7277
+ let humanApproverReachable = false;
7278
+ if (!policyResult.dependsOnStatePredicates?.length && daemonUp && !isTestEnv2) {
7279
+ humanApproverReachable = await hasReachableHumanApprover({
7280
+ approvers,
7281
+ calledFromDaemon: options?.calledFromDaemon
7282
+ });
7283
+ }
7284
+ const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
7285
+ const hardBlock = () => {
7286
+ if (!isManual)
7287
+ appendLocalAudit(
7288
+ toolName,
7289
+ args,
7290
+ "deny",
7291
+ "smart-rule-block",
7292
+ { ...meta, ruleName: policyResult.ruleName },
7293
+ hashAuditArgs
7294
+ );
7295
+ return {
7296
+ approved: false,
7297
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
7298
+ blockedBy: "local-config",
7299
+ blockedByLabel: policyResult.blockedByLabel,
7300
+ ruleHit: policyResult.ruleName,
7301
+ ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
7302
+ ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
7303
+ };
7304
+ };
7176
7305
  if (policyResult.dependsOnStatePredicates?.length) {
7177
7306
  const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
7178
7307
  const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
@@ -7189,7 +7318,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7189
7318
  if (predicatesMet && policyResult.recoveryCommand) {
7190
7319
  statefulRecoveryCommand = policyResult.recoveryCommand;
7191
7320
  }
7192
- } else if (isDaemonRunning() && !isTestEnv2) {
7321
+ } else if (mayDowngrade) {
7193
7322
  if (!isManual)
7194
7323
  appendLocalAudit(
7195
7324
  toolName,
@@ -7211,36 +7340,13 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7211
7340
  };
7212
7341
  }
7213
7342
  } else {
7214
- if (!isManual)
7215
- appendLocalAudit(
7216
- toolName,
7217
- args,
7218
- "deny",
7219
- "smart-rule-block",
7220
- // Include policyResult.ruleName so the [2] Report SHIELDS
7221
- // panel can attribute this block to its specific shield
7222
- // (e.g. `shield:project-jail:block-read-ssh`) via the
7223
- // rule→shield map. checkedBy stays as the generic
7224
- // `smart-rule-block` for backward compat with existing
7225
- // log readers.
7226
- { ...meta, ruleName: policyResult.ruleName },
7227
- hashAuditArgs
7228
- );
7229
- return {
7230
- approved: false,
7231
- reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
7232
- blockedBy: "local-config",
7233
- blockedByLabel: policyResult.blockedByLabel,
7234
- ruleHit: policyResult.ruleName,
7235
- ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
7236
- ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
7237
- };
7343
+ return hardBlock();
7238
7344
  }
7239
7345
  }
7240
7346
  explainableLabel = policyResult.blockedByLabel || "Local Config";
7241
7347
  policyMatchedField = policyResult.matchedField;
7242
7348
  policyMatchedWord = policyResult.matchedWord;
7243
- if (policyResult.ruleName) localSmartRuleMatched = true;
7349
+ if (policyResult.ruleName || policyResult.tier === 7) localSmartRuleMatched = true;
7244
7350
  if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
7245
7351
  else if (policyResult.reason) policyRuleDescription = policyResult.reason;
7246
7352
  riskMetadata = computeRiskMetadata(
@@ -7252,7 +7358,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7252
7358
  policyResult.ruleName
7253
7359
  );
7254
7360
  if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
7255
- const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
7361
+ const persistent = policyResult.ruleName || policyResult.tier === 7 ? null : getPersistentDecision(toolName);
7256
7362
  if (persistent === "allow" && !appPermReview) {
7257
7363
  if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
7258
7364
  return { approved: true, checkedBy: "persistent" };
@@ -15919,6 +16025,9 @@ data: ${JSON.stringify(data)}
15919
16025
  }
15920
16026
  });
15921
16027
  }
16028
+ function hasInteractiveClient() {
16029
+ return [...sseClients].some((c) => c.capabilities.includes("input"));
16030
+ }
15922
16031
  function broadcastForensic(finding) {
15923
16032
  const severity = CRITICAL_FORENSIC_CATEGORIES.has(finding.type) ? "critical" : "warning";
15924
16033
  const event = {
@@ -17156,18 +17265,27 @@ var init_score = __esm({
17156
17265
 
17157
17266
  // src/posture/headline.ts
17158
17267
  function worstFinding(findings) {
17159
- return [...findings].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0];
17268
+ const cmp = (x, y) => x < y ? -1 : x > y ? 1 : 0;
17269
+ return [...findings].sort(
17270
+ (a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || cmp(a.category, b.category) || cmp(a.title, b.title)
17271
+ )[0];
17272
+ }
17273
+ function actionFromFinding(f) {
17274
+ if (!f?.fix) return null;
17275
+ const fix = f.fix.replace(/^fix it now:\s*/i, "");
17276
+ 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`;
17277
+ return fix + where;
17160
17278
  }
17161
17279
  function deriveHeadline(allFindings) {
17162
17280
  const findings = allFindings.filter(
17163
17281
  (f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix"
17164
17282
  );
17165
17283
  if (findings.length === 0 || findings.every((f) => f.severity === "advisory")) return null;
17166
- const has = (category) => findings.some((f) => f.category === category);
17167
- const secrets = has("Secrets");
17168
- const egressOpen = has("Egress");
17169
- const noIsolation = has("Isolation");
17170
- const gateWeak = has("Approval gate");
17284
+ const has2 = (category) => findings.some((f) => f.category === category);
17285
+ const secrets = has2("Secrets");
17286
+ const egressOpen = has2("Egress");
17287
+ const noIsolation = has2("Isolation");
17288
+ const gateWeak = has2("Approval gate");
17171
17289
  const notWired = findings.some((f) => f.category === "Coverage" && f.severity === "critical");
17172
17290
  const observeOnly = findings.some((f) => f.category === "Coverage" && f.severity === "high");
17173
17291
  let risk;
@@ -17190,13 +17308,16 @@ function deriveHeadline(allFindings) {
17190
17308
  } else if (observeOnly) {
17191
17309
  action = "Switch node9 to enforcing mode \u2014 right now it is only watching, not blocking.";
17192
17310
  } else if (egressOpen) {
17193
- action = "lock egress to an allowlist (node9 can enforce it) \u2014 it closes the exit the exfiltration needs.";
17311
+ const egressFix = actionFromFinding(
17312
+ worstFinding(findings.filter((f) => f.category === "Egress"))
17313
+ );
17314
+ 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.";
17194
17315
  } else if (secrets) {
17195
- action = "node9 can block reads of sensitive paths (~/.ssh, ~/.aws) in-path.";
17316
+ action = actionFromFinding(worstFinding(findings.filter((f) => f.category === "Secrets"))) ?? "node9 can block reads of sensitive credential files in-path (`node9 shield enable project-jail`).";
17196
17317
  } else if (gateWeak) {
17197
- action = "node9 can enforce destructive-command blocking in-path.";
17318
+ action = actionFromFinding(worstFinding(findings.filter((f) => f.category === "Approval gate"))) ?? "node9 can enforce destructive-command blocking in-path (`node9 shield enable bash-safe`).";
17198
17319
  } else {
17199
- action = worstFinding(findings)?.fix ?? "Review the findings below.";
17320
+ action = actionFromFinding(worstFinding(findings)) ?? "Review the findings below.";
17200
17321
  }
17201
17322
  return { risk, action };
17202
17323
  }
@@ -17451,7 +17572,7 @@ var init_ship = __esm({
17451
17572
  });
17452
17573
 
17453
17574
  // src/policy-snapshot/build.ts
17454
- function buildPolicySnapshot(config, activeShields, overrides, mcpTools = {}, statusEntries = []) {
17575
+ function buildPolicySnapshot(config, activeShields, overrides, mcpTools = {}, statusEntries = [], syncHealth) {
17455
17576
  const p = config.policy;
17456
17577
  const statusByKey = /* @__PURE__ */ new Map();
17457
17578
  for (const s of statusEntries) {
@@ -17510,7 +17631,17 @@ function buildPolicySnapshot(config, activeShields, overrides, mcpTools = {}, st
17510
17631
  dlpEnabled: p.dlp.enabled,
17511
17632
  engineVersion: ENGINE_VERSION,
17512
17633
  // Connected rows first so they win the cap; non-connected SEE rows fill the rest.
17513
- mcpServers: [...connectedRows, ...extraRows].slice(0, MAX_MCP_SERVERS)
17634
+ mcpServers: [...connectedRows, ...extraRows].slice(0, MAX_MCP_SERVERS),
17635
+ // fleet-ship Step 2: ship the proxy's own sync health so the dashboard can
17636
+ // derive a "sync failing" badge. Only included when the caller passes it.
17637
+ ...syncHealth && {
17638
+ syncHealth: {
17639
+ lastCheckedAt: syncHealth.lastCheckedAt,
17640
+ lastChangedAt: syncHealth.lastChangedAt,
17641
+ lastError: syncHealth.lastError,
17642
+ consecutiveFailures: syncHealth.consecutiveFailures
17643
+ }
17644
+ }
17514
17645
  };
17515
17646
  }
17516
17647
  var MAX_RULES, MAX_EGRESS, MAX_MCP_SERVERS, MAX_MCP_TOOLS;
@@ -17724,6 +17855,21 @@ function inventoryMcp(home = import_os31.default.homedir()) {
17724
17855
  }
17725
17856
  return out;
17726
17857
  }
17858
+ function inventoryServerKeys(inv) {
17859
+ const keys = /* @__PURE__ */ new Set();
17860
+ for (const e of inv) {
17861
+ if (e.state === "gatewayed") {
17862
+ const i = e.args.indexOf("--upstream");
17863
+ if (i >= 0 && e.args[i + 1]) {
17864
+ keys.add(getServerKey(e.args[i + 1]));
17865
+ }
17866
+ } else if (e.state === "ungoverned") {
17867
+ const cmd = [e.command, ...e.args].map(quoteArg).join(" ");
17868
+ keys.add(getServerKey(cmd));
17869
+ }
17870
+ }
17871
+ return keys;
17872
+ }
17727
17873
  function writeMcpEntry(mcpFile, format, name, entry) {
17728
17874
  const key = format === "toml" ? "mcp_servers" : "mcpServers";
17729
17875
  let root = {};
@@ -17751,6 +17897,7 @@ var init_mcp_wrap = __esm({
17751
17897
  import_smol_toml4 = require("smol-toml");
17752
17898
  init_agent_wiring();
17753
17899
  init_mcp_cmd();
17900
+ init_mcp_pin();
17754
17901
  init_mcp_cmd();
17755
17902
  }
17756
17903
  });
@@ -18179,9 +18326,12 @@ function extractManagedConfig(body) {
18179
18326
  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;
18180
18327
  }
18181
18328
  function writeCache2(cache) {
18182
- const dir = import_path34.default.dirname(rulesCacheFile());
18183
- if (!import_fs35.default.existsSync(dir)) import_fs35.default.mkdirSync(dir, { recursive: true });
18184
- import_fs35.default.writeFileSync(rulesCacheFile(), JSON.stringify(cache, null, 2) + "\n", "utf-8");
18329
+ const data = JSON.stringify(cache, null, 2) + "\n";
18330
+ atomicWriteSync2(rulesCacheFile(), data, "utf-8");
18331
+ try {
18332
+ atomicWriteSync2(rulesCacheBackupFile(), data, "utf-8");
18333
+ } catch {
18334
+ }
18185
18335
  }
18186
18336
  async function syncOnce() {
18187
18337
  const creds = readCredentials();
@@ -18285,7 +18435,10 @@ async function pushPolicySnapshot(creds) {
18285
18435
  // Merged config-vs-connected status. resolveMcpStatus reads process.env to
18286
18436
  // resolve ${VAR} placeholders — so this push reflects THIS process's env
18287
18437
  // (daemon here, user shell in runPolicyPush); see the P2 design's two-env note.
18288
- resolveMcpStatus()
18438
+ resolveMcpStatus(),
18439
+ // fleet-ship Step 2: ship this machine's sync health so the dashboard can
18440
+ // show "sync failing" badges. readSyncHealth is local (same module, no circular).
18441
+ readSyncHealth()
18289
18442
  );
18290
18443
  await shipPolicySnapshot(body, creds);
18291
18444
  } catch {
@@ -18308,7 +18461,8 @@ async function runPolicyPush() {
18308
18461
  // Merged config-vs-connected status. resolveMcpStatus reads process.env to
18309
18462
  // resolve ${VAR} placeholders — so this push reflects THIS process's env
18310
18463
  // (daemon here, user shell in runPolicyPush); see the P2 design's two-env note.
18311
- resolveMcpStatus()
18464
+ resolveMcpStatus(),
18465
+ readSyncHealth()
18312
18466
  );
18313
18467
  const sent = await shipPolicySnapshot(body, creds);
18314
18468
  return sent ? { ok: true } : { ok: false, reason: "Push failed (network or server error)" };
@@ -18471,7 +18625,7 @@ function startForensicBroadcast() {
18471
18625
  const recurring = setInterval(() => void tick(), FORENSIC_BROADCAST_INTERVAL_MS);
18472
18626
  recurring.unref();
18473
18627
  }
18474
- var import_fs35, import_https4, import_os32, import_path34, 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;
18628
+ var import_fs35, import_https4, import_os32, import_path34, 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;
18475
18629
  var init_sync = __esm({
18476
18630
  "src/daemon/sync.ts"() {
18477
18631
  "use strict";
@@ -18485,6 +18639,7 @@ var init_sync = __esm({
18485
18639
  init_ship();
18486
18640
  init_build2();
18487
18641
  init_mcp_tools();
18642
+ init_state2();
18488
18643
  init_mcp_status();
18489
18644
  init_ship2();
18490
18645
  init_shields();
@@ -18505,6 +18660,7 @@ var init_sync = __esm({
18505
18660
  "long-output-redacted": "longOutputRedactions"
18506
18661
  };
18507
18662
  rulesCacheFile = () => import_path34.default.join(import_os32.default.homedir(), ".node9", "rules-cache.json");
18663
+ rulesCacheBackupFile = () => import_path34.default.join(import_os32.default.homedir(), ".node9", "rules-cache.last-good.json");
18508
18664
  DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept/policies/sync";
18509
18665
  DEFAULT_INTERVAL_HOURS = 5;
18510
18666
  MIN_INTERVAL_SECONDS = 15;
@@ -18746,6 +18902,53 @@ var init_audit_shipper = __esm({
18746
18902
  }
18747
18903
  });
18748
18904
 
18905
+ // src/audit/decision.ts
18906
+ function classifyDecision(a, b) {
18907
+ const isRow = !!a && typeof a === "object";
18908
+ const decision = isRow ? a.decision : a;
18909
+ const attribution = isRow ? a.checkedBy ?? a.source : b;
18910
+ const raw = typeof decision === "string" ? decision : String(decision ?? "");
18911
+ const src = typeof attribution === "string" ? attribution.toLowerCase() : "";
18912
+ const d = raw.toLowerCase();
18913
+ if (src && has(src, "observe-mode")) {
18914
+ return { outcome: "observe", label: "Would block", raw };
18915
+ }
18916
+ if (d === "dlp") return { outcome: "info", label: "Finding", raw };
18917
+ if (d === "mcp-discovered") return { outcome: "info", label: "Info", raw };
18918
+ if (d === "allow" || d === "allowed") {
18919
+ if (src === "post-hook") return { outcome: "allow", label: "Ran", raw };
18920
+ if (HUMAN_SOURCES.has(src)) {
18921
+ return { outcome: "allow", label: "Approved", raw };
18922
+ }
18923
+ return { outcome: "allow", label: "Auto-allowed", raw };
18924
+ }
18925
+ if (d === "deny" || d === "auto-deny" || d === "block") {
18926
+ if (TIMEOUT_SOURCES.has(src)) {
18927
+ return { outcome: "deny", label: "Timed out", raw };
18928
+ }
18929
+ if (HUMAN_SOURCES.has(src)) {
18930
+ return { outcome: "deny", label: "Denied", raw };
18931
+ }
18932
+ return { outcome: "deny", label: "Blocked", raw };
18933
+ }
18934
+ if (d === "review" || d === "pending") {
18935
+ return { outcome: "info", label: "Pending", raw };
18936
+ }
18937
+ return { outcome: "unknown", label: raw ? `? ${raw}` : "? (none)", raw };
18938
+ }
18939
+ function decisionTag(view) {
18940
+ return `[${view.label}]`.padEnd(14);
18941
+ }
18942
+ var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
18943
+ var init_decision = __esm({
18944
+ "src/audit/decision.ts"() {
18945
+ "use strict";
18946
+ HUMAN_SOURCES = /* @__PURE__ */ new Set(["daemon", "cloud", "local-decision", "inline-review-approved"]);
18947
+ TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
18948
+ has = (s, needle) => s.includes(needle);
18949
+ }
18950
+ });
18951
+
18749
18952
  // src/daemon/dlp-scanner.ts
18750
18953
  function loadIndex() {
18751
18954
  try {
@@ -18981,6 +19184,7 @@ function runMcpReconcile() {
18981
19184
  }
18982
19185
  const baseline = loadBaseline();
18983
19186
  const creds = getCredentials();
19187
+ reconcileStale(inv, creds);
18984
19188
  const fresh = inv.filter((e) => e.state === "ungoverned" && !baseline.has(idKey(e)));
18985
19189
  if (fresh.length === 0) return;
18986
19190
  const wrappedAgents = /* @__PURE__ */ new Set();
@@ -19020,6 +19224,77 @@ function runMcpReconcile() {
19020
19224
  }
19021
19225
  saveBaseline(baseline);
19022
19226
  }
19227
+ function reconcileStale(inv, creds) {
19228
+ let pins;
19229
+ try {
19230
+ pins = readMcpPins();
19231
+ } catch {
19232
+ return;
19233
+ }
19234
+ const serverKeys = Object.keys(pins.servers);
19235
+ if (serverKeys.length === 0) return;
19236
+ const liveKeys = inventoryServerKeys(inv);
19237
+ const now = (/* @__PURE__ */ new Date()).toISOString();
19238
+ let dirty = false;
19239
+ for (const sk of serverKeys) {
19240
+ const pin = pins.servers[sk];
19241
+ if (liveKeys.has(sk)) {
19242
+ if (pin.lastSeen !== now) {
19243
+ pin.lastSeen = now;
19244
+ dirty = true;
19245
+ }
19246
+ } else if (!pin.lastSeen) {
19247
+ pin.lastSeen = pin.pinnedAt;
19248
+ dirty = true;
19249
+ }
19250
+ }
19251
+ const staleDays = getConfig().settings.mcpStaleAfterDays ?? DEFAULT_STALE_DAYS;
19252
+ if (staleDays > 0 && liveKeys.size > 0) {
19253
+ const staleMs = staleDays * 864e5;
19254
+ for (const sk of serverKeys) {
19255
+ const pin = pins.servers[sk];
19256
+ if (liveKeys.has(sk)) continue;
19257
+ const age = Date.now() - Date.parse(pin.lastSeen ?? pin.pinnedAt);
19258
+ if (age >= staleMs) {
19259
+ appendToLog(HOOK_DEBUG_LOG, {
19260
+ event: "mcp-pin-auto-removed",
19261
+ serverKey: sk,
19262
+ label: pin.label,
19263
+ lastSeen: pin.lastSeen,
19264
+ pinnedAt: pin.pinnedAt
19265
+ });
19266
+ if (creds) {
19267
+ try {
19268
+ void auditLocalAllow(
19269
+ `mcp-server:${sk}`,
19270
+ {
19271
+ serverKey: sk,
19272
+ label: pin.label,
19273
+ lastSeen: pin.lastSeen,
19274
+ reason: "stale",
19275
+ staleDays
19276
+ },
19277
+ "mcp-server-removed",
19278
+ creds,
19279
+ { mcpServer: pin.label },
19280
+ void 0,
19281
+ false
19282
+ );
19283
+ } catch {
19284
+ }
19285
+ }
19286
+ delete pins.servers[sk];
19287
+ dirty = true;
19288
+ }
19289
+ }
19290
+ }
19291
+ if (dirty) {
19292
+ try {
19293
+ writeMcpPins(pins);
19294
+ } catch {
19295
+ }
19296
+ }
19297
+ }
19023
19298
  function startMcpReconciler() {
19024
19299
  setImmediate(() => {
19025
19300
  try {
@@ -19041,7 +19316,7 @@ function startMcpReconciler() {
19041
19316
  };
19042
19317
  schedule();
19043
19318
  }
19044
- var import_fs38, import_path37, import_os35, import_crypto10, BASELINE_FILE2, BASELINE_CAP, DEFAULT_INTERVAL_MIN;
19319
+ var import_fs38, import_path37, import_os35, import_crypto10, BASELINE_FILE2, BASELINE_CAP, DEFAULT_INTERVAL_MIN, DEFAULT_STALE_DAYS;
19045
19320
  var init_mcp_reconciler = __esm({
19046
19321
  "src/daemon/mcp-reconciler.ts"() {
19047
19322
  "use strict";
@@ -19054,9 +19329,11 @@ var init_mcp_reconciler = __esm({
19054
19329
  init_config();
19055
19330
  init_cloud();
19056
19331
  init_audit();
19332
+ init_mcp_pin();
19057
19333
  BASELINE_FILE2 = import_path37.default.join(import_os35.default.homedir(), ".node9", "mcp-baseline.json");
19058
19334
  BASELINE_CAP = 500;
19059
19335
  DEFAULT_INTERVAL_MIN = 60;
19336
+ DEFAULT_STALE_DAYS = 7;
19060
19337
  }
19061
19338
  });
19062
19339
 
@@ -19128,20 +19405,87 @@ var init_hook_heal = __esm({
19128
19405
  });
19129
19406
 
19130
19407
  // src/daemon/startup-log.ts
19408
+ function capStartupLog(file) {
19409
+ try {
19410
+ if (import_fs39.default.statSync(file).size > MAX_STARTUP_LOG_BYTES) import_fs39.default.truncateSync(file);
19411
+ } catch {
19412
+ }
19413
+ }
19131
19414
  function openStartupLogFd() {
19132
19415
  try {
19133
19416
  const file = DAEMON_STARTUP_LOG();
19134
19417
  const dir = import_path38.default.dirname(file);
19135
19418
  if (!import_fs39.default.existsSync(dir)) import_fs39.default.mkdirSync(dir, { recursive: true });
19136
- try {
19137
- if (import_fs39.default.statSync(file).size > MAX_STARTUP_LOG_BYTES) import_fs39.default.truncateSync(file);
19138
- } catch {
19139
- }
19419
+ capStartupLog(file);
19140
19420
  return import_fs39.default.openSync(file, "a");
19141
19421
  } catch {
19142
19422
  return void 0;
19143
19423
  }
19144
19424
  }
19425
+ function recordStartupState(outcome, kind, detail) {
19426
+ try {
19427
+ if (outcome === "starting") {
19428
+ const prev = readStartupState();
19429
+ if (prev?.outcome === "starting") {
19430
+ const prevAt = new Date(prev.at).getTime();
19431
+ const age = Math.abs(Date.now() - prevAt);
19432
+ if (!isNaN(prevAt) && age < 24 * 60 * 60 * 1e3) return;
19433
+ }
19434
+ }
19435
+ const file = DAEMON_STARTUP_STATE();
19436
+ const dir = import_path38.default.dirname(file);
19437
+ if (!import_fs39.default.existsSync(dir)) import_fs39.default.mkdirSync(dir, { recursive: true });
19438
+ const state = { outcome, at: (/* @__PURE__ */ new Date()).toISOString() };
19439
+ if (kind) state.kind = kind;
19440
+ if (detail) state.detail = detail.slice(0, MAX_DETAIL);
19441
+ const tmp = `${file}.${process.pid}.tmp`;
19442
+ try {
19443
+ import_fs39.default.writeFileSync(tmp, JSON.stringify(state), "utf-8");
19444
+ import_fs39.default.renameSync(tmp, file);
19445
+ } catch (err2) {
19446
+ try {
19447
+ import_fs39.default.unlinkSync(tmp);
19448
+ } catch {
19449
+ }
19450
+ throw err2;
19451
+ }
19452
+ } catch {
19453
+ }
19454
+ }
19455
+ function readStartupState() {
19456
+ try {
19457
+ const raw = import_fs39.default.readFileSync(DAEMON_STARTUP_STATE(), "utf-8");
19458
+ const s = JSON.parse(raw);
19459
+ if (!s || typeof s.outcome !== "string" || typeof s.at !== "string") return null;
19460
+ return s;
19461
+ } catch {
19462
+ return null;
19463
+ }
19464
+ }
19465
+ function readStartupCause(maxAgeMs = 24 * 60 * 60 * 1e3) {
19466
+ const s = readStartupState();
19467
+ if (!s) return null;
19468
+ const at = new Date(s.at);
19469
+ if (isNaN(at.getTime()) || Date.now() - at.getTime() > maxAgeMs) return null;
19470
+ switch (s.outcome) {
19471
+ case "ok":
19472
+ case "ok-elsewhere":
19473
+ return null;
19474
+ case "starting":
19475
+ if (Date.now() - at.getTime() < STARTING_GRACE_MS) return null;
19476
+ return {
19477
+ kind: "did-not-start",
19478
+ detail: "the daemon did not come up \u2014 see ~/.node9/daemon-startup.log and hook-debug.log",
19479
+ at,
19480
+ // `at` is the FIRST attempt of the streak, not the most recent one.
19481
+ label: "start attempts failing since"
19482
+ };
19483
+ case "failed":
19484
+ return { kind: s.kind || "failed", detail: s.detail || "", at, label: "last start attempt" };
19485
+ default:
19486
+ return null;
19487
+ }
19488
+ }
19145
19489
  function logDaemonStartup(kind, detail) {
19146
19490
  try {
19147
19491
  const file = DAEMON_STARTUP_LOG();
@@ -19153,7 +19497,7 @@ function logDaemonStartup(kind, detail) {
19153
19497
  } catch {
19154
19498
  }
19155
19499
  }
19156
- var import_fs39, import_path38, import_os36, DAEMON_STARTUP_LOG, MAX_STARTUP_LOG_BYTES;
19500
+ var import_fs39, import_path38, import_os36, DAEMON_STARTUP_LOG, MAX_STARTUP_LOG_BYTES, DAEMON_STARTUP_STATE, MAX_DETAIL, STARTING_GRACE_MS;
19157
19501
  var init_startup_log = __esm({
19158
19502
  "src/daemon/startup-log.ts"() {
19159
19503
  "use strict";
@@ -19162,10 +19506,83 @@ var init_startup_log = __esm({
19162
19506
  import_os36 = __toESM(require("os"));
19163
19507
  DAEMON_STARTUP_LOG = () => import_path38.default.join(import_os36.default.homedir(), ".node9", "daemon-startup.log");
19164
19508
  MAX_STARTUP_LOG_BYTES = 256 * 1024;
19509
+ DAEMON_STARTUP_STATE = () => import_path38.default.join(import_os36.default.homedir(), ".node9", "daemon-startup-state.json");
19510
+ MAX_DETAIL = 200;
19511
+ STARTING_GRACE_MS = 90 * 1e3;
19165
19512
  }
19166
19513
  });
19167
19514
 
19168
19515
  // src/daemon/server.ts
19516
+ function buildDaemonReport(allEntries, period, now) {
19517
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
19518
+ let start = new Date(todayStart);
19519
+ if (period === "7d") start.setDate(start.getDate() - 6);
19520
+ else if (period === "30d") start.setDate(start.getDate() - 29);
19521
+ else if (period === "month") start = new Date(now.getFullYear(), now.getMonth(), 1);
19522
+ const entries = allEntries.filter((e) => {
19523
+ if (e.source === "post-hook" || e.source === "response-dlp") return false;
19524
+ return new Date(e.ts) >= start;
19525
+ });
19526
+ const isBlocked = (e) => classifyDecision(e).outcome === "deny";
19527
+ const checkedBy = (e) => typeof e.checkedBy === "string" ? e.checkedBy : void 0;
19528
+ const summary = {
19529
+ total: entries.length,
19530
+ // The inline `startsWith('allow')` this replaces counted every non-allow
19531
+ // row as "blocked" — so DLP findings, MCP-discovery events and, worst, all
19532
+ // the observe-mode "would have blocked" rows inflated the blocked count
19533
+ // with things that were never refusals.
19534
+ allowed: entries.filter((e) => classifyDecision(e).outcome === "allow").length,
19535
+ blocked: entries.filter(isBlocked).length,
19536
+ dlp: entries.filter((e) => checkedBy(e)?.includes("dlp")).length,
19537
+ loops: entries.filter((e) => checkedBy(e) === "loop-detected").length
19538
+ };
19539
+ const dailyMap = /* @__PURE__ */ new Map();
19540
+ if (period === "today") {
19541
+ for (let h = 0; h < 24; h++) {
19542
+ const key = String(h).padStart(2, "0") + ":00";
19543
+ dailyMap.set(key, { date: key, calls: 0, blocked: 0 });
19544
+ }
19545
+ for (const e of entries) {
19546
+ const hour = new Date(e.ts).getHours();
19547
+ const key = String(hour).padStart(2, "0") + ":00";
19548
+ const d = dailyMap.get(key);
19549
+ d.calls++;
19550
+ if (isBlocked(e)) d.blocked++;
19551
+ }
19552
+ } else {
19553
+ for (const e of entries) {
19554
+ const date = e.ts.slice(0, 10);
19555
+ const d = dailyMap.get(date) || { date, calls: 0, blocked: 0 };
19556
+ d.calls++;
19557
+ if (isBlocked(e)) d.blocked++;
19558
+ dailyMap.set(date, d);
19559
+ }
19560
+ }
19561
+ const topToolsMap = /* @__PURE__ */ new Map();
19562
+ const topBlockedMap = /* @__PURE__ */ new Map();
19563
+ for (const e of entries) {
19564
+ const tool = String(e.tool ?? "");
19565
+ topToolsMap.set(tool, (topToolsMap.get(tool) || 0) + 1);
19566
+ if (isBlocked(e)) topBlockedMap.set(tool, (topBlockedMap.get(tool) || 0) + 1);
19567
+ }
19568
+ const top5 = (m) => [...m.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, value]) => ({ name, value }));
19569
+ const agentMap = /* @__PURE__ */ new Map();
19570
+ for (const e of entries) {
19571
+ const key = e.agent || "unknown";
19572
+ const a = agentMap.get(key) ?? { agent: key, total: 0, blocked: 0, dlp: 0 };
19573
+ a.total++;
19574
+ if (isBlocked(e)) a.blocked++;
19575
+ if (checkedBy(e)?.includes("dlp")) a.dlp++;
19576
+ agentMap.set(key, a);
19577
+ }
19578
+ return {
19579
+ summary,
19580
+ daily: [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date)),
19581
+ topTools: top5(topToolsMap),
19582
+ topBlockedTools: top5(topBlockedMap),
19583
+ byAgent: [...agentMap.values()].sort((a, b) => b.total - a.total)
19584
+ };
19585
+ }
19169
19586
  function startDaemon() {
19170
19587
  try {
19171
19588
  startCostSync();
@@ -19180,6 +19597,7 @@ function startDaemon() {
19180
19597
  const stack = err2 instanceof Error ? err2.stack ?? err2.message : String(err2);
19181
19598
  console.error("\n\u{1F6D1} Node9 daemon startup failed:\n" + stack);
19182
19599
  logDaemonStartup("startup-throw", err2 instanceof Error ? err2.message : String(err2));
19600
+ recordStartupState("failed", "startup-throw", err2 instanceof Error ? err2.message : String(err2));
19183
19601
  process.exit(1);
19184
19602
  }
19185
19603
  const internalToken = (0, import_crypto11.randomUUID)();
@@ -19552,6 +19970,10 @@ data: ${JSON.stringify(item.data)}
19552
19970
  return res.end(JSON.stringify({ error: "internal" }));
19553
19971
  }
19554
19972
  }
19973
+ if (req.method === "GET" && pathname === "/approver") {
19974
+ res.writeHead(200, { "Content-Type": "application/json" });
19975
+ return res.end(JSON.stringify({ interactive: hasInteractiveClient() }));
19976
+ }
19555
19977
  if (req.method === "GET" && pathname === "/state/check") {
19556
19978
  const predicatesParam = reqUrl.searchParams.get("predicates") ?? "";
19557
19979
  const predicates = predicatesParam.split(",").filter(Boolean);
@@ -19653,75 +20075,8 @@ data: ${JSON.stringify(item.data)}
19653
20075
  return [];
19654
20076
  }
19655
20077
  });
19656
- const now = /* @__PURE__ */ new Date();
19657
- const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
19658
- let start = new Date(todayStart);
19659
- if (period === "7d") start.setDate(start.getDate() - 6);
19660
- else if (period === "30d") start.setDate(start.getDate() - 29);
19661
- else if (period === "month") start = new Date(now.getFullYear(), now.getMonth(), 1);
19662
- const entries = allEntries.filter((e) => {
19663
- if (e.source === "post-hook" || e.source === "response-dlp") return false;
19664
- return new Date(e.ts) >= start;
19665
- });
19666
- const summary = {
19667
- total: entries.length,
19668
- allowed: entries.filter((e) => e.decision && e.decision.startsWith("allow")).length,
19669
- blocked: entries.filter((e) => e.decision && !e.decision.startsWith("allow")).length,
19670
- dlp: entries.filter((e) => e.checkedBy && e.checkedBy.includes("dlp")).length,
19671
- loops: entries.filter((e) => e.checkedBy === "loop-detected").length
19672
- };
19673
- const dailyMap = /* @__PURE__ */ new Map();
19674
- if (period === "today") {
19675
- for (let h = 0; h < 24; h++) {
19676
- const key = String(h).padStart(2, "0") + ":00";
19677
- dailyMap.set(key, { date: key, calls: 0, blocked: 0 });
19678
- }
19679
- for (const e of entries) {
19680
- const hour = new Date(e.ts).getHours();
19681
- const key = String(hour).padStart(2, "0") + ":00";
19682
- const d = dailyMap.get(key);
19683
- d.calls++;
19684
- if (e.decision && !e.decision.startsWith("allow")) d.blocked++;
19685
- }
19686
- } else {
19687
- for (const e of entries) {
19688
- const date = e.ts.slice(0, 10);
19689
- const d = dailyMap.get(date) || { date, calls: 0, blocked: 0 };
19690
- d.calls++;
19691
- if (e.decision && !e.decision.startsWith("allow")) d.blocked++;
19692
- dailyMap.set(date, d);
19693
- }
19694
- }
19695
- const topToolsMap = /* @__PURE__ */ new Map();
19696
- const topBlockedMap = /* @__PURE__ */ new Map();
19697
- for (const e of entries) {
19698
- topToolsMap.set(e.tool, (topToolsMap.get(e.tool) || 0) + 1);
19699
- if (e.decision && !e.decision.startsWith("allow")) {
19700
- topBlockedMap.set(e.tool, (topBlockedMap.get(e.tool) || 0) + 1);
19701
- }
19702
- }
19703
- const topTools = [...topToolsMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, value]) => ({ name, value }));
19704
- const topBlockedTools = [...topBlockedMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, value]) => ({ name, value }));
19705
- const agentMap = /* @__PURE__ */ new Map();
19706
- for (const e of entries) {
19707
- const key = e.agent || "unknown";
19708
- const a = agentMap.get(key) ?? { agent: key, total: 0, blocked: 0, dlp: 0 };
19709
- a.total++;
19710
- if (e.decision && !e.decision.startsWith("allow")) a.blocked++;
19711
- if (e.checkedBy?.includes("dlp")) a.dlp++;
19712
- agentMap.set(key, a);
19713
- }
19714
- const byAgent = [...agentMap.values()].sort((a, b) => b.total - a.total);
19715
20078
  res.writeHead(200, { "Content-Type": "application/json" });
19716
- return res.end(
19717
- JSON.stringify({
19718
- summary,
19719
- daily: [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date)),
19720
- topTools,
19721
- topBlockedTools,
19722
- byAgent
19723
- })
19724
- );
20079
+ return res.end(JSON.stringify(buildDaemonReport(allEntries, period, /* @__PURE__ */ new Date())));
19725
20080
  } catch {
19726
20081
  res.writeHead(500, { "Content-Type": "application/json" });
19727
20082
  return res.end(JSON.stringify({ error: "Failed to parse report" }));
@@ -20024,6 +20379,23 @@ data: ${JSON.stringify(item.data)}
20024
20379
  res.writeHead(404).end();
20025
20380
  });
20026
20381
  setDaemonServer(server);
20382
+ let bindAttempts = 0;
20383
+ const MAX_BIND_ATTEMPTS = 3;
20384
+ function retryListen() {
20385
+ if (++bindAttempts >= MAX_BIND_ATTEMPTS) {
20386
+ logDaemonStartup(
20387
+ "port-unavailable",
20388
+ `:${DAEMON_PORT} is held by something that is not a node9 daemon`
20389
+ );
20390
+ recordStartupState(
20391
+ "failed",
20392
+ "port-unavailable",
20393
+ `:${DAEMON_PORT} is held by another process that is not a node9 daemon \u2014 free the port, then: node9 daemon --background`
20394
+ );
20395
+ return process.exit(0);
20396
+ }
20397
+ server.listen(DAEMON_PORT, DAEMON_HOST);
20398
+ }
20027
20399
  server.on("error", (e) => {
20028
20400
  if (e.code === "EADDRINUSE") {
20029
20401
  try {
@@ -20031,6 +20403,7 @@ data: ${JSON.stringify(item.data)}
20031
20403
  const { pid } = JSON.parse(import_fs40.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
20032
20404
  process.kill(pid, 0);
20033
20405
  logDaemonStartup("port-in-use", `another daemon (pid ${pid}) owns :${DAEMON_PORT}`);
20406
+ recordStartupState("ok-elsewhere");
20034
20407
  return process.exit(0);
20035
20408
  }
20036
20409
  } catch {
@@ -20038,13 +20411,14 @@ data: ${JSON.stringify(item.data)}
20038
20411
  import_fs40.default.unlinkSync(DAEMON_PID_FILE);
20039
20412
  } catch {
20040
20413
  }
20041
- server.listen(DAEMON_PORT, DAEMON_HOST);
20414
+ retryListen();
20042
20415
  return;
20043
20416
  }
20044
20417
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/settings`, {
20045
20418
  signal: AbortSignal.timeout(1e3)
20046
20419
  }).then((res) => {
20047
20420
  if (res.ok) {
20421
+ let adopted = false;
20048
20422
  try {
20049
20423
  let orphanPid = null;
20050
20424
  const ss = (0, import_child_process2.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
@@ -20072,19 +20446,38 @@ data: ${JSON.stringify(item.data)}
20072
20446
  JSON.stringify({ pid: orphanPid, port: DAEMON_PORT, internalToken, autoStarted }),
20073
20447
  { mode: 384 }
20074
20448
  );
20449
+ adopted = true;
20075
20450
  }
20076
20451
  } catch {
20077
20452
  }
20453
+ if (adopted) {
20454
+ logDaemonStartup(
20455
+ "port-in-use-orphan",
20456
+ `adopted the daemon already on :${DAEMON_PORT}`
20457
+ );
20458
+ recordStartupState("ok-elsewhere");
20459
+ } else {
20460
+ logDaemonStartup(
20461
+ "orphan-unidentified",
20462
+ `healthy daemon on :${DAEMON_PORT} could not be identified \u2014 no pid file written`
20463
+ );
20464
+ recordStartupState(
20465
+ "failed",
20466
+ "orphan-unidentified",
20467
+ `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`
20468
+ );
20469
+ }
20078
20470
  process.exit(0);
20079
20471
  } else {
20080
- server.listen(DAEMON_PORT, DAEMON_HOST);
20472
+ retryListen();
20081
20473
  }
20082
20474
  }).catch(() => {
20083
- server.listen(DAEMON_PORT, DAEMON_HOST);
20475
+ retryListen();
20084
20476
  });
20085
20477
  return;
20086
20478
  }
20087
20479
  logDaemonStartup("bind-failed", e.message);
20480
+ recordStartupState("failed", "bind-failed", e.message);
20088
20481
  console.error(import_chalk6.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
20089
20482
  process.exit(1);
20090
20483
  });
@@ -20102,6 +20495,8 @@ data: ${JSON.stringify(item.data)}
20102
20495
  { mode: 384 }
20103
20496
  );
20104
20497
  console.error(import_chalk6.default.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE on 127.0.0.1:${DAEMON_PORT}`));
20498
+ logDaemonStartup("ok", `listening on ${DAEMON_HOST}:${DAEMON_PORT}`);
20499
+ recordStartupState("ok");
20105
20500
  });
20106
20501
  if (watchMode) {
20107
20502
  console.error(import_chalk6.default.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
@@ -20127,6 +20522,7 @@ var init_server = __esm({
20127
20522
  init_costSync();
20128
20523
  init_sync();
20129
20524
  init_audit_shipper();
20525
+ init_decision();
20130
20526
  init_dlp_scanner();
20131
20527
  init_mcp_reconciler();
20132
20528
  init_hook_heal();
@@ -45329,19 +45725,19 @@ function getModelContextLimit(model) {
45329
45725
  }
45330
45726
  function readSessionUsage() {
45331
45727
  const projectsDir = import_path67.default.join(import_os60.default.homedir(), ".claude", "projects");
45332
- if (!import_fs70.default.existsSync(projectsDir)) return null;
45728
+ if (!import_fs71.default.existsSync(projectsDir)) return null;
45333
45729
  let latestFile = null;
45334
45730
  let latestMtime = 0;
45335
45731
  try {
45336
- for (const dir of import_fs70.default.readdirSync(projectsDir)) {
45732
+ for (const dir of import_fs71.default.readdirSync(projectsDir)) {
45337
45733
  const dirPath = import_path67.default.join(projectsDir, dir);
45338
45734
  try {
45339
- if (!import_fs70.default.statSync(dirPath).isDirectory()) continue;
45340
- for (const file of import_fs70.default.readdirSync(dirPath)) {
45735
+ if (!import_fs71.default.statSync(dirPath).isDirectory()) continue;
45736
+ for (const file of import_fs71.default.readdirSync(dirPath)) {
45341
45737
  if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
45342
45738
  const filePath = import_path67.default.join(dirPath, file);
45343
45739
  try {
45344
- const mtime = import_fs70.default.statSync(filePath).mtimeMs;
45740
+ const mtime = import_fs71.default.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 = import_fs70.default.readFileSync(latestFile, "utf-8").split("\n");
45755
+ const lines = import_fs71.default.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 (import_fs70.default.existsSync(PID_FILE)) {
45855
+ if (import_fs71.default.existsSync(PID_FILE)) {
45460
45856
  try {
45461
- const { port } = JSON.parse(import_fs70.default.readFileSync(PID_FILE, "utf-8"));
45857
+ const { port } = JSON.parse(import_fs71.default.readFileSync(PID_FILE, "utf-8"));
45462
45858
  pidPort = port;
45463
45859
  } catch {
45464
45860
  console.error(import_chalk40.default.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(import_chalk40.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
45872
+ const startupFd = openStartupLogFd();
45873
+ recordStartupState("starting");
45476
45874
  const child = (0, import_child_process14.spawn)(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
+ import_fs71.default.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 = import_path67.default.join(import_os60.default.homedir(), ".node9", "config.json");
45618
46023
  try {
45619
- const raw = JSON.parse(import_fs70.default.readFileSync(configPath, "utf-8"));
46024
+ const raw = JSON.parse(import_fs71.default.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 = import_path67.default.join(import_os60.default.homedir(), ".node9", "config.json");
45636
46041
  try {
45637
- const raw = JSON.parse(import_fs70.default.readFileSync(configPath, "utf-8"));
46042
+ const raw = JSON.parse(import_fs71.default.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
- import_fs70.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
46048
+ import_fs71.default.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
- import_fs70.default.appendFileSync(
46220
+ import_fs71.default.appendFileSync(
45816
46221
  import_path67.default.join(import_os60.default.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 = import_path67.default.join(import_os60.default.homedir(), ".node9", "audit.log");
45881
46286
  try {
45882
- const unackedDlp = import_fs70.default.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
46287
+ const unackedDlp = import_fs71.default.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 = import_fs70.default.statSync(auditLog).mtimeMs;
46327
+ const auditMtime = import_fs71.default.statSync(auditLog).mtimeMs;
45923
46328
  if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
45924
46329
  console.log("");
45925
46330
  console.log(
@@ -46104,17 +46509,18 @@ async function startTail(options = {}) {
46104
46509
  process.exit(1);
46105
46510
  });
46106
46511
  }
46107
- var import_http5, import_chalk40, import_fs70, import_os60, import_path67, import_readline6, import_child_process14, PID_FILE, ICONS, MODEL_CONTEXT_LIMITS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
46512
+ var import_http5, import_chalk40, import_fs71, import_os60, import_path67, import_readline6, import_child_process14, PID_FILE, ICONS, MODEL_CONTEXT_LIMITS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
46108
46513
  var init_tail = __esm({
46109
46514
  "src/tui/tail.ts"() {
46110
46515
  "use strict";
46111
46516
  import_http5 = __toESM(require("http"));
46112
46517
  import_chalk40 = __toESM(require("chalk"));
46113
- import_fs70 = __toESM(require("fs"));
46518
+ import_fs71 = __toESM(require("fs"));
46114
46519
  import_os60 = __toESM(require("os"));
46115
46520
  import_path67 = __toESM(require("path"));
46116
46521
  import_readline6 = __toESM(require("readline"));
46117
46522
  import_child_process14 = require("child_process");
46523
+ init_startup_log();
46118
46524
  init_daemon2();
46119
46525
  init_daemon();
46120
46526
  PID_FILE = import_path67.default.join(import_os60.default.homedir(), ".node9", "daemon.pid");
@@ -46239,9 +46645,9 @@ function formatTimeLeft(resetsAt) {
46239
46645
  return ` (${m}m left)`;
46240
46646
  }
46241
46647
  function safeReadJson(filePath) {
46242
- if (!import_fs71.default.existsSync(filePath)) return null;
46648
+ if (!import_fs72.default.existsSync(filePath)) return null;
46243
46649
  try {
46244
- return JSON.parse(import_fs71.default.readFileSync(filePath, "utf-8"));
46650
+ return JSON.parse(import_fs72.default.readFileSync(filePath, "utf-8"));
46245
46651
  } catch {
46246
46652
  return null;
46247
46653
  }
@@ -46262,10 +46668,10 @@ function countHooksInFile(filePath) {
46262
46668
  return Object.keys(cfg.hooks).length;
46263
46669
  }
46264
46670
  function countRulesInDir(rulesDir) {
46265
- if (!import_fs71.default.existsSync(rulesDir)) return 0;
46671
+ if (!import_fs72.default.existsSync(rulesDir)) return 0;
46266
46672
  let count = 0;
46267
46673
  try {
46268
- for (const entry of import_fs71.default.readdirSync(rulesDir, { withFileTypes: true })) {
46674
+ for (const entry of import_fs72.default.readdirSync(rulesDir, { withFileTypes: true })) {
46269
46675
  if (entry.isDirectory()) {
46270
46676
  count += countRulesInDir(import_path68.default.join(rulesDir, entry.name));
46271
46677
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -46291,7 +46697,7 @@ function countConfigs(cwd) {
46291
46697
  let hooksCount = 0;
46292
46698
  const userMcpServers = /* @__PURE__ */ new Set();
46293
46699
  const projectMcpServers = /* @__PURE__ */ new Set();
46294
- if (import_fs71.default.existsSync(import_path68.default.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
46700
+ if (import_fs72.default.existsSync(import_path68.default.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
46295
46701
  rulesCount += countRulesInDir(import_path68.default.join(claudeDir, "rules"));
46296
46702
  const userSettings = import_path68.default.join(claudeDir, "settings.json");
46297
46703
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
@@ -46302,18 +46708,18 @@ function countConfigs(cwd) {
46302
46708
  userMcpServers.delete(name);
46303
46709
  }
46304
46710
  if (cwd) {
46305
- if (import_fs71.default.existsSync(import_path68.default.join(cwd, "CLAUDE.md"))) claudeMdCount++;
46306
- if (import_fs71.default.existsSync(import_path68.default.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
46711
+ if (import_fs72.default.existsSync(import_path68.default.join(cwd, "CLAUDE.md"))) claudeMdCount++;
46712
+ if (import_fs72.default.existsSync(import_path68.default.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
46307
46713
  const projectClaudeDir = import_path68.default.join(cwd, ".claude");
46308
46714
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
46309
46715
  if (!overlapsUserScope) {
46310
- if (import_fs71.default.existsSync(import_path68.default.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
46716
+ if (import_fs72.default.existsSync(import_path68.default.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
46311
46717
  rulesCount += countRulesInDir(import_path68.default.join(projectClaudeDir, "rules"));
46312
46718
  const projSettings = import_path68.default.join(projectClaudeDir, "settings.json");
46313
46719
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
46314
46720
  hooksCount += countHooksInFile(projSettings);
46315
46721
  }
46316
- if (import_fs71.default.existsSync(import_path68.default.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
46722
+ if (import_fs72.default.existsSync(import_path68.default.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
46317
46723
  const localSettings = import_path68.default.join(projectClaudeDir, "settings.local.json");
46318
46724
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
46319
46725
  hooksCount += countHooksInFile(localSettings);
@@ -46351,11 +46757,11 @@ function readActiveShieldsHud() {
46351
46757
  }
46352
46758
  try {
46353
46759
  const shieldsPath = import_path68.default.join(import_os61.default.homedir(), ".node9", "shields.json");
46354
- if (!import_fs71.default.existsSync(shieldsPath)) {
46760
+ if (!import_fs72.default.existsSync(shieldsPath)) {
46355
46761
  shieldsCache = { value: [], ts: now };
46356
46762
  return [];
46357
46763
  }
46358
- const parsed = JSON.parse(import_fs71.default.readFileSync(shieldsPath, "utf-8"));
46764
+ const parsed = JSON.parse(import_fs72.default.readFileSync(shieldsPath, "utf-8"));
46359
46765
  if (!Array.isArray(parsed.active)) {
46360
46766
  shieldsCache = { value: [], ts: now };
46361
46767
  return [];
@@ -46457,17 +46863,17 @@ function renderContextLine(stdin) {
46457
46863
  async function main() {
46458
46864
  try {
46459
46865
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
46460
- if (import_fs71.default.existsSync(import_path68.default.join(import_os61.default.homedir(), ".node9", "hud-debug"))) {
46866
+ if (import_fs72.default.existsSync(import_path68.default.join(import_os61.default.homedir(), ".node9", "hud-debug"))) {
46461
46867
  try {
46462
46868
  const logPath = import_path68.default.join(import_os61.default.homedir(), ".node9", "hud-debug.log");
46463
46869
  const MAX_LOG_SIZE = 10 * 1024 * 1024;
46464
46870
  let size = 0;
46465
46871
  try {
46466
- size = import_fs71.default.statSync(logPath).size;
46872
+ size = import_fs72.default.statSync(logPath).size;
46467
46873
  } catch {
46468
46874
  }
46469
46875
  if (size < MAX_LOG_SIZE) {
46470
- import_fs71.default.appendFileSync(
46876
+ import_fs72.default.appendFileSync(
46471
46877
  logPath,
46472
46878
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
46473
46879
  );
@@ -46491,8 +46897,8 @@ async function main() {
46491
46897
  import_path68.default.join(cwd, "node9.config.json"),
46492
46898
  import_path68.default.join(import_os61.default.homedir(), ".node9", "config.json")
46493
46899
  ]) {
46494
- if (!import_fs71.default.existsSync(configPath)) continue;
46495
- const cfg = JSON.parse(import_fs71.default.readFileSync(configPath, "utf-8"));
46900
+ if (!import_fs72.default.existsSync(configPath)) continue;
46901
+ const cfg = JSON.parse(import_fs72.default.readFileSync(configPath, "utf-8"));
46496
46902
  const hud = cfg.settings?.hud;
46497
46903
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
46498
46904
  }
@@ -46510,11 +46916,11 @@ async function main() {
46510
46916
  renderOffline();
46511
46917
  }
46512
46918
  }
46513
- var import_fs71, import_path68, import_os61, import_http6, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
46919
+ var import_fs72, import_path68, import_os61, import_http6, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
46514
46920
  var init_hud = __esm({
46515
46921
  "src/cli/hud.ts"() {
46516
46922
  "use strict";
46517
- import_fs71 = __toESM(require("fs"));
46923
+ import_fs72 = __toESM(require("fs"));
46518
46924
  import_path68 = __toESM(require("path"));
46519
46925
  import_os61 = __toESM(require("os"));
46520
46926
  import_http6 = __toESM(require("http"));
@@ -46638,7 +47044,7 @@ function writeCredentialsAndConfig(apiKey, opts = {}) {
46638
47044
  // src/cli.ts
46639
47045
  init_daemon2();
46640
47046
  var import_chalk41 = __toESM(require("chalk"));
46641
- var import_fs72 = __toESM(require("fs"));
47047
+ var import_fs73 = __toESM(require("fs"));
46642
47048
  var import_path69 = __toESM(require("path"));
46643
47049
  var import_os62 = __toESM(require("os"));
46644
47050
  var import_child_process15 = require("child_process");
@@ -46844,9 +47250,11 @@ function logAutostartSkipThrottled(reason) {
46844
47250
  if (Date.now() - import_fs43.default.statSync(stamp).mtimeMs < SKIP_THROTTLE_MS) return;
46845
47251
  } catch {
46846
47252
  }
47253
+ const dir = import_path41.default.join(import_os39.default.homedir(), ".node9");
47254
+ if (!import_fs43.default.existsSync(dir)) import_fs43.default.mkdirSync(dir, { recursive: true });
46847
47255
  import_fs43.default.writeFileSync(stamp, "", "utf-8");
46848
47256
  import_fs43.default.appendFileSync(
46849
- import_path41.default.join(import_os39.default.homedir(), ".node9", "hook-debug.log"),
47257
+ import_path41.default.join(dir, "hook-debug.log"),
46850
47258
  `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-skip: ${reason}
46851
47259
  `,
46852
47260
  "utf-8"
@@ -46865,6 +47273,8 @@ async function autoStartDaemonAndWait() {
46865
47273
  }
46866
47274
  if (!resolvedArgv1.endsWith(".js")) return false;
46867
47275
  const startupFd = openStartupLogFd();
47276
+ recordStartupState("starting");
47277
+ let spawned = false;
46868
47278
  try {
46869
47279
  const child = (0, import_child_process5.spawn)(process.execPath, [resolvedArgv1, "daemon"], {
46870
47280
  detached: true,
@@ -46874,13 +47284,24 @@ async function autoStartDaemonAndWait() {
46874
47284
  NODE9_AUTO_STARTED: "1"
46875
47285
  }
46876
47286
  });
47287
+ child.on("error", (err2) => {
47288
+ if (readStartupState()?.outcome !== "starting") return;
47289
+ recordStartupState("failed", "spawn-failed", err2.message);
47290
+ });
46877
47291
  child.unref();
47292
+ spawned = true;
46878
47293
  for (let i = 0; i < 20; i++) {
46879
47294
  await new Promise((r) => setTimeout(r, 250));
46880
47295
  if (!isDaemonRunning()) continue;
46881
47296
  if (await isDaemonReachable()) return true;
46882
47297
  }
46883
- } catch {
47298
+ } catch (err2) {
47299
+ if (!spawned)
47300
+ recordStartupState(
47301
+ "failed",
47302
+ "spawn-failed",
47303
+ err2 instanceof Error ? err2.message : String(err2)
47304
+ );
46884
47305
  } finally {
46885
47306
  if (startupFd !== void 0) {
46886
47307
  try {
@@ -47693,12 +48114,16 @@ RAW: ${raw}
47693
48114
  delete safeEnv[key];
47694
48115
  }
47695
48116
  const startupFd = openStartupLogFd();
48117
+ recordStartupState("starting");
47696
48118
  try {
47697
48119
  const d = (0, import_child_process7.spawn)(process.execPath, [scriptPath, "daemon"], {
47698
48120
  detached: true,
47699
48121
  stdio: ["ignore", "ignore", startupFd ?? "ignore"],
47700
48122
  env: { ...safeEnv, NODE9_AUTO_STARTED: "1" }
47701
48123
  });
48124
+ d.on("error", (err2) => {
48125
+ recordStartupState("failed", "spawn-failed", err2.message);
48126
+ });
47702
48127
  d.unref();
47703
48128
  } finally {
47704
48129
  if (startupFd !== void 0) {
@@ -47711,6 +48136,7 @@ RAW: ${raw}
47711
48136
  } catch (spawnErr) {
47712
48137
  const logPath = import_path45.default.join(import_os43.default.homedir(), ".node9", "hook-debug.log");
47713
48138
  const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
48139
+ recordStartupState("failed", "spawn-aborted", msg);
47714
48140
  try {
47715
48141
  import_fs47.default.appendFileSync(
47716
48142
  logPath,
@@ -48875,6 +49301,7 @@ function agoLabel(iso, now = Date.now()) {
48875
49301
  }
48876
49302
 
48877
49303
  // src/cli/commands/doctor.ts
49304
+ init_startup_log();
48878
49305
  function registerDoctorCommand(program2, version2) {
48879
49306
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(async () => {
48880
49307
  const homeDir2 = import_os46.default.homedir();
@@ -48986,6 +49413,15 @@ function registerDoctorCommand(program2, version2) {
48986
49413
  "Daemon not running \u2014 terminal & native approvals unavailable",
48987
49414
  "Run: node9 daemon --background"
48988
49415
  );
49416
+ const cause = readStartupCause();
49417
+ if (cause) {
49418
+ const suffix = cause.detail ? ` \u2014 ${cause.detail}` : "";
49419
+ console.log(
49420
+ import_chalk11.default.gray(
49421
+ ` ${cause.label} ${agoLabel(cause.at.toISOString())}: ${cause.kind}${suffix}`
49422
+ )
49423
+ );
49424
+ }
48989
49425
  }
48990
49426
  const autostart = autostartAdvice({
48991
49427
  installed: isDaemonServiceInstalled(),
@@ -49063,6 +49499,7 @@ function registerDoctorCommand(program2, version2) {
49063
49499
  var import_chalk12 = __toESM(require("chalk"));
49064
49500
  var import_fs52 = __toESM(require("fs"));
49065
49501
  var import_path50 = __toESM(require("path"));
49502
+ init_decision();
49066
49503
  var import_os47 = __toESM(require("os"));
49067
49504
  function formatRelativeTime(timestamp) {
49068
49505
  const diff = Date.now() - new Date(timestamp).getTime();
@@ -49094,10 +49531,16 @@ function registerAuditCommand(program2) {
49094
49531
  });
49095
49532
  entries = entries.map((e) => ({
49096
49533
  ...e,
49097
- decision: String(e.decision).startsWith("allow") ? "allow" : "deny"
49534
+ // classifyDecision is the ONE mapper (audit/decision.ts). The inline
49535
+ // `startsWith('allow') ? allow : deny` this replaces bucketed `dlp` and
49536
+ // `mcp-discovered` — findings, not verdicts — as DENY, inventing
49537
+ // refusals that never happened.
49538
+ // Pass the ROW, never a field pair — the attribution key differs by
49539
+ // producer (`checkedBy` from the gate, `source` from the hook/daemon).
49540
+ view: classifyDecision(e)
49098
49541
  }));
49099
49542
  if (options.tool) entries = entries.filter((e) => String(e.tool).includes(options.tool));
49100
- if (options.deny) entries = entries.filter((e) => e.decision === "deny");
49543
+ if (options.deny) entries = entries.filter((e) => e.view.outcome === "deny");
49101
49544
  const limit = Math.max(1, parseInt(options.tail, 10) || 20);
49102
49545
  entries = entries.slice(-limit);
49103
49546
  if (options.json) {
@@ -49120,13 +49563,13 @@ function registerAuditCommand(program2) {
49120
49563
  for (const e of entries) {
49121
49564
  const time = formatRelativeTime(String(e.ts)).padEnd(12);
49122
49565
  const tool = String(e.tool).slice(0, 17).padEnd(18);
49123
- const result = e.decision === "allow" ? import_chalk12.default.green("ALLOW".padEnd(10)) : import_chalk12.default.red("DENY".padEnd(10));
49566
+ const result = e.view.outcome === "allow" ? import_chalk12.default.green(e.view.label.padEnd(14)) : e.view.outcome === "deny" ? import_chalk12.default.red(e.view.label.padEnd(14)) : e.view.outcome === "observe" ? import_chalk12.default.yellow(e.view.label.padEnd(14)) : import_chalk12.default.gray(e.view.label.padEnd(14));
49124
49567
  const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
49125
49568
  const agent = String(e.agent || "unknown");
49126
49569
  console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
49127
49570
  }
49128
- const allowed = entries.filter((e) => e.decision === "allow").length;
49129
- const denied = entries.filter((e) => e.decision === "deny").length;
49571
+ const allowed = entries.filter((e) => e.view.outcome === "allow").length;
49572
+ const denied = entries.filter((e) => e.view.outcome === "deny").length;
49130
49573
  console.log(import_chalk12.default.dim(" " + "\u2500".repeat(65)));
49131
49574
  console.log(
49132
49575
  ` ${entries.length} entries | ${import_chalk12.default.green(allowed + " allowed")} | ${import_chalk12.default.red(denied + " denied")}
@@ -49145,6 +49588,7 @@ var import_path51 = __toESM(require("path"));
49145
49588
  init_costSync();
49146
49589
  init_litellm();
49147
49590
  init_cost_codex();
49591
+ init_decision();
49148
49592
  var TEST_COMMAND_RE3 = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
49149
49593
  function buildTestTimestamps(allEntries) {
49150
49594
  const testTs = /* @__PURE__ */ new Set();
@@ -49235,8 +49679,15 @@ function parseAuditLog(logPath) {
49235
49679
  }
49236
49680
  });
49237
49681
  }
49238
- function isAllow(decision) {
49239
- return decision.startsWith("allow");
49682
+ function viewOf(e) {
49683
+ const outcome = classifyDecision(e).outcome;
49684
+ const observed = outcome === "observe";
49685
+ return {
49686
+ observed,
49687
+ ran: outcome === "allow" || observed,
49688
+ blocked: outcome === "deny" || outcome === "unknown",
49689
+ info: outcome === "info"
49690
+ };
49240
49691
  }
49241
49692
  function isDlp(checkedBy) {
49242
49693
  return !!checkedBy?.includes("dlp");
@@ -49649,12 +50100,12 @@ function aggregateReportFromAudit(period, opts = {}) {
49649
50100
  const priorStart = new Date(start.getTime() - periodMs);
49650
50101
  const priorEntries = allEntries.filter((e) => {
49651
50102
  if (e.source === "post-hook") return false;
50103
+ if (e.source === "response-dlp") return false;
50104
+ if (typeof e.decision !== "string") return false;
49652
50105
  const ts = new Date(e.ts);
49653
50106
  return ts >= priorStart && ts <= priorEnd;
49654
50107
  });
49655
- const priorBlocked = priorEntries.filter(
49656
- (e) => typeof e.decision === "string" && !isAllow(e.decision)
49657
- ).length;
50108
+ const priorBlocked = priorEntries.filter((e) => viewOf(e).blocked).length;
49658
50109
  const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
49659
50110
  const excludeTests = opts.excludeTests === true;
49660
50111
  const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
@@ -49697,15 +50148,17 @@ function aggregateReportFromAudit(period, opts = {}) {
49697
50148
  let dimInjectionBlocked = 0;
49698
50149
  for (const e of entries) {
49699
50150
  if (superseded.has(supersedeKey(e))) continue;
49700
- const allow = isAllow(e.decision);
50151
+ const view = viewOf(e);
49701
50152
  const dateKey = e.ts.slice(0, 10);
49702
50153
  const userInteracted = e.source === "daemon";
49703
- if (userInteracted) {
49704
- if (allow) userApproved++;
50154
+ if (view.observed) {
50155
+ if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
50156
+ } else if (view.info) {
50157
+ } else if (userInteracted) {
50158
+ if (view.ran) userApproved++;
49705
50159
  else userDenied++;
49706
- } else if (!allow) {
50160
+ } else if (view.blocked) {
49707
50161
  if (e.checkedBy === "timeout") timedOut++;
49708
- else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
49709
50162
  else if (isDlp(e.checkedBy)) dlpBlocked++;
49710
50163
  else if (e.checkedBy === "local-decision") userDenied++;
49711
50164
  else if (e.checkedBy !== "loop-detected") hardBlocked++;
@@ -49717,7 +50170,7 @@ function aggregateReportFromAudit(period, opts = {}) {
49717
50170
  if (cb.includes("would-block") && (cb.includes("pii") || cb.includes("dlp"))) {
49718
50171
  dimDataObserved++;
49719
50172
  }
49720
- if (!allow && !userInteracted) {
50173
+ if (view.blocked && !userInteracted) {
49721
50174
  switch (dimensionOfBlock(cb, e.ruleName ?? "")) {
49722
50175
  case "network":
49723
50176
  dimNetworkBlocked++;
@@ -49735,15 +50188,15 @@ function aggregateReportFromAudit(period, opts = {}) {
49735
50188
  }
49736
50189
  const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
49737
50190
  t.calls++;
49738
- if (!allow) t.blocked++;
50191
+ if (view.blocked) t.blocked++;
49739
50192
  toolMap.set(e.tool, t);
49740
- if (!allow) {
50193
+ if (view.blocked) {
49741
50194
  const key = e.checkedBy ?? (e.source === "daemon" ? "local-decision" : null);
49742
50195
  if (key) {
49743
50196
  blockMap.set(key, (blockMap.get(key) ?? 0) + 1);
49744
50197
  }
49745
50198
  }
49746
- if (!allow && e.ruleName) {
50199
+ if ((view.blocked || view.observed) && e.ruleName) {
49747
50200
  ruleMap.set(e.ruleName, (ruleMap.get(e.ruleName) ?? 0) + 1);
49748
50201
  }
49749
50202
  if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
@@ -49752,7 +50205,7 @@ function aggregateReportFromAudit(period, opts = {}) {
49752
50205
  hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
49753
50206
  const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
49754
50207
  d.calls++;
49755
- if (!allow) d.blocked++;
50208
+ if (view.blocked) d.blocked++;
49756
50209
  dailyMap.set(dateKey, d);
49757
50210
  }
49758
50211
  for (const e of allEntries) {
@@ -50306,6 +50759,8 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
50306
50759
  // src/cli/commands/daemon-cmd.ts
50307
50760
  var import_chalk14 = __toESM(require("chalk"));
50308
50761
  var import_child_process9 = require("child_process");
50762
+ var import_fs54 = __toESM(require("fs"));
50763
+ init_startup_log();
50309
50764
  init_daemon2();
50310
50765
  var VALID_ACTIONS = "start | stop | restart | status | install | uninstall";
50311
50766
  function registerDaemonCommand(program2) {
@@ -50344,14 +50799,27 @@ function registerDaemonCommand(program2) {
50344
50799
  if (cmd === "restart") {
50345
50800
  stopDaemon();
50346
50801
  await new Promise((r) => setTimeout(r, 500));
50802
+ const restartFd = openStartupLogFd();
50803
+ recordStartupState("starting");
50347
50804
  const child = (0, import_child_process9.spawn)(process.execPath, [process.argv[1], "daemon"], {
50348
50805
  detached: true,
50349
- stdio: "ignore",
50806
+ stdio: ["ignore", "ignore", restartFd ?? "ignore"],
50350
50807
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
50351
50808
  });
50809
+ child.on(
50810
+ "error",
50811
+ (err2) => recordStartupState("failed", "spawn-failed", err2.message)
50812
+ );
50352
50813
  child.unref();
50814
+ if (restartFd !== void 0) {
50815
+ try {
50816
+ import_fs54.default.closeSync(restartFd);
50817
+ } catch {
50818
+ }
50819
+ }
50353
50820
  if (child.pid) {
50354
- console.log(import_chalk14.default.green(`\u2713 Daemon restarted (PID ${child.pid})`));
50821
+ console.log(import_chalk14.default.green(`\u2713 Daemon relaunching (PID ${child.pid})`));
50822
+ console.log(import_chalk14.default.gray(" Confirm with: node9 status"));
50355
50823
  } else {
50356
50824
  console.error(import_chalk14.default.red("\u2717 Failed to restart daemon \u2014 spawn returned no PID"));
50357
50825
  process.exit(1);
@@ -50374,13 +50842,33 @@ function registerDaemonCommand(program2) {
50374
50842
  return;
50375
50843
  }
50376
50844
  if (options.background) {
50377
- const child = (0, import_child_process9.spawn)(process.execPath, [process.argv[1], "daemon"], {
50378
- detached: true,
50379
- stdio: "ignore"
50380
- });
50381
- child.unref();
50382
- console.log(import_chalk14.default.green(`
50383
- \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
50845
+ const startupFd = openStartupLogFd();
50846
+ try {
50847
+ recordStartupState("starting");
50848
+ const child = (0, import_child_process9.spawn)(process.execPath, [process.argv[1], "daemon"], {
50849
+ detached: true,
50850
+ // Capture the child's stderr: a module-load crash prints its stack there
50851
+ // and dies before it can record anything itself.
50852
+ stdio: ["ignore", "ignore", startupFd ?? "ignore"]
50853
+ });
50854
+ child.on(
50855
+ "error",
50856
+ (err2) => recordStartupState("failed", "spawn-failed", err2.message)
50857
+ );
50858
+ child.unref();
50859
+ console.log(
50860
+ import_chalk14.default.green(`
50861
+ \u{1F6E1}\uFE0F Node9 daemon launching in background (PID ${child.pid})`)
50862
+ );
50863
+ console.log(import_chalk14.default.gray(" Confirm with: node9 status"));
50864
+ } finally {
50865
+ if (startupFd !== void 0) {
50866
+ try {
50867
+ import_fs54.default.closeSync(startupFd);
50868
+ } catch {
50869
+ }
50870
+ }
50871
+ }
50384
50872
  process.exit(0);
50385
50873
  }
50386
50874
  startDaemon();
@@ -50390,7 +50878,7 @@ function registerDaemonCommand(program2) {
50390
50878
 
50391
50879
  // src/cli/commands/status.ts
50392
50880
  var import_chalk15 = __toESM(require("chalk"));
50393
- var import_fs54 = __toESM(require("fs"));
50881
+ var import_fs55 = __toESM(require("fs"));
50394
50882
  var import_path52 = __toESM(require("path"));
50395
50883
  var import_os49 = __toESM(require("os"));
50396
50884
  init_core();
@@ -50473,10 +50961,10 @@ function registerStatusCommand(program2) {
50473
50961
  const projectConfig = import_path52.default.join(process.cwd(), "node9.config.json");
50474
50962
  const globalConfig = import_path52.default.join(import_os49.default.homedir(), ".node9", "config.json");
50475
50963
  console.log(
50476
- ` Local: ${import_fs54.default.existsSync(projectConfig) ? import_chalk15.default.green("Active (node9.config.json)") : import_chalk15.default.gray("Not present")}`
50964
+ ` Local: ${import_fs55.default.existsSync(projectConfig) ? import_chalk15.default.green("Active (node9.config.json)") : import_chalk15.default.gray("Not present")}`
50477
50965
  );
50478
50966
  console.log(
50479
- ` Global: ${import_fs54.default.existsSync(globalConfig) ? import_chalk15.default.green("Active (~/.node9/config.json)") : import_chalk15.default.gray("Not present")}`
50967
+ ` Global: ${import_fs55.default.existsSync(globalConfig) ? import_chalk15.default.green("Active (~/.node9/config.json)") : import_chalk15.default.gray("Not present")}`
50480
50968
  );
50481
50969
  if (mergedConfig.policy.sandboxPaths.length > 0) {
50482
50970
  console.log(
@@ -50518,7 +51006,7 @@ function registerStatusCommand(program2) {
50518
51006
 
50519
51007
  // src/cli/commands/init.ts
50520
51008
  var import_chalk16 = __toESM(require("chalk"));
50521
- var import_fs55 = __toESM(require("fs"));
51009
+ var import_fs56 = __toESM(require("fs"));
50522
51010
  var import_path53 = __toESM(require("path"));
50523
51011
  var import_os50 = __toESM(require("os"));
50524
51012
  var import_https6 = __toESM(require("https"));
@@ -50612,15 +51100,15 @@ function registerInitCommand(program2) {
50612
51100
  console.log("");
50613
51101
  }
50614
51102
  const configPath = import_path53.default.join(import_os50.default.homedir(), ".node9", "config.json");
50615
- const isFirstInstall = !import_fs55.default.existsSync(configPath);
50616
- if (import_fs55.default.existsSync(configPath) && !options.force) {
51103
+ const isFirstInstall = !import_fs56.default.existsSync(configPath);
51104
+ if (import_fs56.default.existsSync(configPath) && !options.force) {
50617
51105
  try {
50618
- const existing = JSON.parse(import_fs55.default.readFileSync(configPath, "utf-8"));
51106
+ const existing = JSON.parse(import_fs56.default.readFileSync(configPath, "utf-8"));
50619
51107
  const settings = existing.settings ?? {};
50620
51108
  if (settings.mode !== chosenMode) {
50621
51109
  settings.mode = chosenMode;
50622
51110
  existing.settings = settings;
50623
- import_fs55.default.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
51111
+ import_fs56.default.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
50624
51112
  console.log(import_chalk16.default.green(`\u2705 Mode updated: ${chosenMode}`));
50625
51113
  } else {
50626
51114
  console.log(import_chalk16.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
@@ -50634,8 +51122,8 @@ function registerInitCommand(program2) {
50634
51122
  settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
50635
51123
  };
50636
51124
  const dir = import_path53.default.dirname(configPath);
50637
- if (!import_fs55.default.existsSync(dir)) import_fs55.default.mkdirSync(dir, { recursive: true });
50638
- import_fs55.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
51125
+ if (!import_fs56.default.existsSync(dir)) import_fs56.default.mkdirSync(dir, { recursive: true });
51126
+ import_fs56.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
50639
51127
  console.log(import_chalk16.default.green(`\u2705 Config created: ${configPath}`));
50640
51128
  console.log(import_chalk16.default.gray(` Mode: ${chosenMode}`));
50641
51129
  }
@@ -50735,14 +51223,14 @@ function registerInitCommand(program2) {
50735
51223
 
50736
51224
  // src/cli/commands/heal.ts
50737
51225
  var import_chalk17 = __toESM(require("chalk"));
50738
- var import_fs56 = __toESM(require("fs"));
51226
+ var import_fs57 = __toESM(require("fs"));
50739
51227
  init_agent_wiring();
50740
51228
  init_setup();
50741
51229
  init_hook_baseline();
50742
51230
  var hasHookSurface = (a) => a.hooks.length > 0;
50743
51231
  function backupForHeal(file) {
50744
51232
  try {
50745
- if (file && import_fs56.default.existsSync(file)) import_fs56.default.copyFileSync(file, `${file}.node9-heal-bak`);
51233
+ if (file && import_fs57.default.existsSync(file)) import_fs57.default.copyFileSync(file, `${file}.node9-heal-bak`);
50746
51234
  } catch {
50747
51235
  }
50748
51236
  }
@@ -51702,16 +52190,17 @@ function registerMcpGatewayCommand(program2) {
51702
52190
 
51703
52191
  // src/mcp-server/index.ts
51704
52192
  var import_readline5 = __toESM(require("readline"));
51705
- var import_fs58 = __toESM(require("fs"));
52193
+ var import_fs59 = __toESM(require("fs"));
51706
52194
  var import_os52 = __toESM(require("os"));
51707
52195
  var import_path56 = __toESM(require("path"));
51708
52196
  var import_child_process11 = require("child_process");
52197
+ init_decision();
51709
52198
  init_core();
51710
52199
  init_daemon();
51711
52200
  init_shields();
51712
52201
 
51713
52202
  // src/auth/egress-config.ts
51714
- var import_fs57 = __toESM(require("fs"));
52203
+ var import_fs58 = __toESM(require("fs"));
51715
52204
  var import_os51 = __toESM(require("os"));
51716
52205
  var import_path55 = __toESM(require("path"));
51717
52206
  var DEFAULT_EGRESS = {
@@ -51727,7 +52216,7 @@ function egressConfigPath() {
51727
52216
  function readEgressRawConfig() {
51728
52217
  let text;
51729
52218
  try {
51730
- text = import_fs57.default.readFileSync(egressConfigPath(), "utf8");
52219
+ text = import_fs58.default.readFileSync(egressConfigPath(), "utf8");
51731
52220
  } catch (err2) {
51732
52221
  if (err2.code === "ENOENT") return {};
51733
52222
  throw err2;
@@ -51742,8 +52231,8 @@ function readEgressRawConfig() {
51742
52231
  }
51743
52232
  function writeEgressRawConfig(config) {
51744
52233
  const p = egressConfigPath();
51745
- import_fs57.default.mkdirSync(import_path55.default.dirname(p), { recursive: true });
51746
- import_fs57.default.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
52234
+ import_fs58.default.mkdirSync(import_path55.default.dirname(p), { recursive: true });
52235
+ import_fs58.default.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
51747
52236
  }
51748
52237
  function applyEgress(config, change) {
51749
52238
  const policy = config.policy = config.policy ?? {};
@@ -51938,7 +52427,7 @@ var TOOLS = [
51938
52427
  },
51939
52428
  {
51940
52429
  name: "node9_audit_get",
51941
- 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.",
52430
+ 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.",
51942
52431
  inputSchema: {
51943
52432
  type: "object",
51944
52433
  properties: {
@@ -51948,8 +52437,8 @@ var TOOLS = [
51948
52437
  },
51949
52438
  filter: {
51950
52439
  type: "string",
51951
- enum: ["all", "block", "review"],
51952
- description: 'Filter by decision. Omit or use "all" to show every entry.'
52440
+ enum: ["all", "allow", "deny", "observe", "info", "block"],
52441
+ 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.'
51953
52442
  }
51954
52443
  },
51955
52444
  required: []
@@ -52131,10 +52620,10 @@ function handleStatus() {
52131
52620
  const projectConfig = import_path56.default.join(process.cwd(), "node9.config.json");
52132
52621
  const globalConfig = import_path56.default.join(import_os52.default.homedir(), ".node9", "config.json");
52133
52622
  lines.push(
52134
- `Project config (node9.config.json): ${import_fs58.default.existsSync(projectConfig) ? "present" : "not found"}`
52623
+ `Project config (node9.config.json): ${import_fs59.default.existsSync(projectConfig) ? "present" : "not found"}`
52135
52624
  );
52136
52625
  lines.push(
52137
- `Global config (~/.node9/config.json): ${import_fs58.default.existsSync(globalConfig) ? "present" : "not found"}`
52626
+ `Global config (~/.node9/config.json): ${import_fs59.default.existsSync(globalConfig) ? "present" : "not found"}`
52138
52627
  );
52139
52628
  return lines.join("\n");
52140
52629
  }
@@ -52244,8 +52733,8 @@ var GLOBAL_CONFIG_PATH = import_path56.default.join(import_os52.default.homedir(
52244
52733
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
52245
52734
  function readGlobalConfigRaw() {
52246
52735
  try {
52247
- if (import_fs58.default.existsSync(GLOBAL_CONFIG_PATH)) {
52248
- return JSON.parse(import_fs58.default.readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
52736
+ if (import_fs59.default.existsSync(GLOBAL_CONFIG_PATH)) {
52737
+ return JSON.parse(import_fs59.default.readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
52249
52738
  }
52250
52739
  } catch {
52251
52740
  }
@@ -52253,8 +52742,8 @@ function readGlobalConfigRaw() {
52253
52742
  }
52254
52743
  function writeGlobalConfigRaw(data) {
52255
52744
  const dir = import_path56.default.dirname(GLOBAL_CONFIG_PATH);
52256
- if (!import_fs58.default.existsSync(dir)) import_fs58.default.mkdirSync(dir, { recursive: true });
52257
- import_fs58.default.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(data, null, 2) + "\n");
52745
+ if (!import_fs59.default.existsSync(dir)) import_fs59.default.mkdirSync(dir, { recursive: true });
52746
+ import_fs59.default.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(data, null, 2) + "\n");
52258
52747
  }
52259
52748
  function handleApproverList() {
52260
52749
  const config = getConfig();
@@ -52299,35 +52788,37 @@ function handleAuditGet(args) {
52299
52788
  const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
52300
52789
  const filter = typeof args.filter === "string" && args.filter !== "all" ? args.filter : null;
52301
52790
  const auditPath = import_path56.default.join(import_os52.default.homedir(), ".node9", "audit.log");
52302
- if (!import_fs58.default.existsSync(auditPath)) return "No audit log found.";
52303
- const rawLines = import_fs58.default.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
52791
+ if (!import_fs59.default.existsSync(auditPath)) return "No audit log found.";
52792
+ const rawLines = import_fs59.default.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
52793
+ const wanted = filter === "block" ? "deny" : filter;
52304
52794
  const parsed = [];
52305
52795
  for (const line of rawLines) {
52306
52796
  try {
52307
52797
  const e = JSON.parse(line);
52308
- const decision = String(e.decision ?? "allow");
52309
- if (filter && decision !== filter) continue;
52798
+ const view = classifyDecision(e);
52799
+ if (wanted && view.outcome !== wanted) continue;
52310
52800
  const argsObj = e.args;
52311
52801
  let detail = "";
52312
52802
  if (argsObj) {
52313
52803
  const cmd = argsObj.command ?? argsObj.file_path ?? argsObj.path ?? argsObj.sql;
52314
- if (typeof cmd === "string" && cmd) {
52315
- detail = cmd.length > 80 ? cmd.slice(0, 80) + "\u2026" : cmd;
52316
- }
52804
+ if (typeof cmd === "string" && cmd) detail = cmd;
52317
52805
  }
52318
- const decisionPad = decision === "block" ? "[BLOCK] " : decision === "review" ? "[review]" : "[allow] ";
52806
+ if (!detail && typeof e.argsPreview === "string") detail = e.argsPreview;
52807
+ detail = detail.replace(/\s+/g, " ").trim();
52808
+ if (detail.length > 80) detail = detail.slice(0, 80) + "\u2026";
52809
+ const why = typeof e.ruleName === "string" && e.ruleName ? ` (${e.ruleName})` : "";
52319
52810
  const toolPad = String(e.tool ?? "").padEnd(20);
52320
- const line2 = `${e.ts} ${decisionPad} ${toolPad} ${detail}`;
52321
- parsed.push({ raw: line, decision, formatted: line2 });
52811
+ const line2 = `${e.ts} ${decisionTag(view)} ${toolPad} ${detail}${why}`;
52812
+ parsed.push({ raw: line, outcome: view.outcome, formatted: line2 });
52322
52813
  } catch {
52323
- parsed.push({ raw: line, decision: "allow", formatted: line });
52814
+ parsed.push({ raw: line, outcome: "unknown", formatted: `[? unparseable] ${line}` });
52324
52815
  }
52325
52816
  }
52326
52817
  const recent = parsed.slice(-limit);
52327
52818
  if (recent.length === 0) {
52328
- return filter ? `No ${filter} entries found in audit log.` : "Audit log is empty.";
52819
+ return filter ? `No ${wanted} entries found in audit log.` : "Audit log is empty.";
52329
52820
  }
52330
- const header = filter ? `Last ${recent.length} ${filter.toUpperCase()} entries:` : `Last ${recent.length} audit entries:`;
52821
+ const header = filter ? `Last ${recent.length} ${String(wanted).toUpperCase()} entries:` : `Last ${recent.length} audit entries:`;
52331
52822
  return `${header}
52332
52823
 
52333
52824
  ${recent.map((e) => e.formatted).join("\n")}`;
@@ -52674,7 +53165,7 @@ function registerTrustCommand(program2) {
52674
53165
  // src/cli/commands/mcp-pin.ts
52675
53166
  var import_chalk24 = __toESM(require("chalk"));
52676
53167
  init_mcp_pin();
52677
- var import_fs59 = __toESM(require("fs"));
53168
+ var import_fs60 = __toESM(require("fs"));
52678
53169
 
52679
53170
  // src/cli/commands/mcp-gateway-cmd.ts
52680
53171
  var import_chalk23 = __toESM(require("chalk"));
@@ -52870,6 +53361,7 @@ Restart ${[...agents].join(", ")}`) + import_chalk23.default.gray(" to activate.
52870
53361
  }
52871
53362
 
52872
53363
  // src/cli/commands/mcp-pin.ts
53364
+ init_mcp_wrap();
52873
53365
  function registerMcpPinCommand(program2) {
52874
53366
  const pinCmd = program2.command("mcp").description("Manage MCP servers \u2014 governance (gateway) + tool-definition pinning");
52875
53367
  registerMcpGatewayCommand2(pinCmd);
@@ -52881,7 +53373,7 @@ function registerMcpPinCommand(program2) {
52881
53373
  let repoCorrupt = false;
52882
53374
  if (found.source === "repo") {
52883
53375
  try {
52884
- const raw = import_fs59.default.readFileSync(found.path, "utf-8");
53376
+ const raw = import_fs60.default.readFileSync(found.path, "utf-8");
52885
53377
  const parsed = JSON.parse(raw);
52886
53378
  repoEntries = parsed.servers ?? {};
52887
53379
  } catch {
@@ -52995,6 +53487,88 @@ function registerMcpPinCommand(program2) {
52995
53487
  \u{1F513} Cleared ${count} MCP pin(s).`));
52996
53488
  console.log(import_chalk24.default.gray(" Next connection to each server will re-pin.\n"));
52997
53489
  });
53490
+ 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) => {
53491
+ if (opts.stale) {
53492
+ forgetAllStale();
53493
+ return;
53494
+ }
53495
+ if (!serverKey) {
53496
+ console.error(
53497
+ import_chalk24.default.red("\n\u274C Please provide a server key, or use --stale to remove all orphans.\n")
53498
+ );
53499
+ process.exit(1);
53500
+ }
53501
+ let pins;
53502
+ try {
53503
+ pins = readMcpPins();
53504
+ } catch {
53505
+ console.error(import_chalk24.default.red("\n\u274C Pin file is corrupt."));
53506
+ console.error(import_chalk24.default.yellow(" Run: node9 mcp pin reset\n"));
53507
+ process.exit(1);
53508
+ }
53509
+ if (!pins.servers[serverKey]) {
53510
+ console.error(import_chalk24.default.red(`
53511
+ \u274C No pin found for server key "${serverKey}"
53512
+ `));
53513
+ console.error(`Run ${import_chalk24.default.cyan("node9 mcp pin list")} to see pinned servers.
53514
+ `);
53515
+ process.exit(1);
53516
+ }
53517
+ const inv = inventoryMcp();
53518
+ const liveKeys = inventoryServerKeys(inv);
53519
+ if (liveKeys.has(serverKey)) {
53520
+ const agent = "an agent config";
53521
+ console.error(import_chalk24.default.red(`
53522
+ \u274C Server "${serverKey}" is still configured in ${agent}.`));
53523
+ console.error(
53524
+ import_chalk24.default.yellow(
53525
+ ` Remove it from the agent config first, then run: node9 mcp forget ${serverKey}
53526
+ `
53527
+ )
53528
+ );
53529
+ process.exit(1);
53530
+ }
53531
+ const label2 = pins.servers[serverKey].label;
53532
+ removePin(serverKey);
53533
+ console.log(import_chalk24.default.green(`
53534
+ \u2713 Forgot server ${import_chalk24.default.cyan(serverKey)}`));
53535
+ console.log(import_chalk24.default.gray(` Was: ${label2}`));
53536
+ console.log(import_chalk24.default.gray(" Pin removed \u2014 server will no longer appear in the dashboard.\n"));
53537
+ });
53538
+ }
53539
+ function forgetAllStale() {
53540
+ let pins;
53541
+ try {
53542
+ pins = readMcpPins();
53543
+ } catch {
53544
+ console.error(import_chalk24.default.red("\n\u274C Pin file is corrupt."));
53545
+ console.error(import_chalk24.default.yellow(" Run: node9 mcp pin reset\n"));
53546
+ process.exit(1);
53547
+ }
53548
+ const inv = inventoryMcp();
53549
+ const liveKeys = inventoryServerKeys(inv);
53550
+ if (liveKeys.size === 0) {
53551
+ console.error(import_chalk24.default.red("\n\u274C No live MCP servers detected \u2014 refusing to remove every pin."));
53552
+ console.error(
53553
+ import_chalk24.default.yellow(
53554
+ " 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"
53555
+ )
53556
+ );
53557
+ process.exit(1);
53558
+ }
53559
+ const stale = Object.entries(pins.servers).filter(([sk]) => !liveKeys.has(sk));
53560
+ if (stale.length === 0) {
53561
+ console.log(import_chalk24.default.gray("\nNo stale servers to remove.\n"));
53562
+ return;
53563
+ }
53564
+ for (const [sk, pin] of stale) {
53565
+ delete pins.servers[sk];
53566
+ console.log(import_chalk24.default.green(` \u2713 ${import_chalk24.default.cyan(sk)} ${import_chalk24.default.gray(pin.label)}`));
53567
+ }
53568
+ writeMcpPins(pins);
53569
+ console.log(import_chalk24.default.green(`
53570
+ Removed ${stale.length} stale server(s).
53571
+ `));
52998
53572
  }
52999
53573
 
53000
53574
  // src/cli/commands/sync.ts
@@ -53239,10 +53813,16 @@ var LABEL_WIDTH = 14;
53239
53813
  function label(category) {
53240
53814
  return import_chalk27.default.bold(category.padEnd(Math.max(LABEL_WIDTH, category.length + 1)));
53241
53815
  }
53242
- function renderFinding(f, showWeight = false) {
53816
+ function guardedWhat(f) {
53817
+ if (f.detail.length === 0) return "this";
53818
+ const reads = f.coverageProbe?.kind === "fileRead" ? "reads of " : "";
53819
+ const more = f.detail.length > 1 ? ` and ${f.detail.length - 1} more` : "";
53820
+ return `${reads}${f.detail[0]}${more}`;
53821
+ }
53822
+ function renderFinding(f, showWeight = false, displayLabel = f.category) {
53243
53823
  const lines = [];
53244
53824
  const wt = showWeight && f.scoreWeight ? import_chalk27.default.cyan.bold(`+${f.scoreWeight} `) : "";
53245
- lines.push(` ${ICON[f.severity]} ${label(f.category)}${wt}${f.title}`);
53825
+ lines.push(` ${ICON[f.severity]} ${label(displayLabel)}${wt}${f.title}`);
53246
53826
  const indent = " ".repeat(2 + 3 + LABEL_WIDTH);
53247
53827
  const width = 80 - indent.length;
53248
53828
  for (const s of [f.what, f.why, f.who]) {
@@ -53279,11 +53859,11 @@ function renderPosture(result) {
53279
53859
  );
53280
53860
  const headroom = openHeadroom(result.findings);
53281
53861
  if (headroom > 0) {
53282
- lines.push(
53283
- " " + import_chalk27.default.gray(
53284
- `${headroom} pts of headroom below \u2014 each layer adds security at some cost to flexibility. You choose which to close.`
53285
- )
53286
- );
53862
+ const openExposures = result.findings.filter(
53863
+ (f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix" && !f.scoreWeight && f.severity !== "advisory"
53864
+ ).length;
53865
+ 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.`;
53866
+ for (const l of wrap(note, 76)) lines.push(" " + import_chalk27.default.gray(l));
53287
53867
  }
53288
53868
  lines.push("");
53289
53869
  if (result.headline) {
@@ -53296,13 +53876,18 @@ function renderPosture(result) {
53296
53876
  }
53297
53877
  const covered = result.findings.filter((f) => f.coverage?.state === "covered");
53298
53878
  const open = result.findings.filter((f) => f.coverage?.state !== "covered");
53879
+ const collision = new Set(
53880
+ covered.map((f) => f.category).filter((c) => open.some((o) => o.category === c))
53881
+ );
53882
+ const openLabel = (f) => collision.has(f.category) ? `${f.category} (exposed)` : f.category;
53299
53883
  if (covered.length > 0) {
53300
53884
  lines.push(" " + import_chalk27.default.green("\u{1F7E2} ON NOW \u2014 node9 is enforcing these (your floor)"));
53301
53885
  for (const f of covered) {
53302
53886
  const gated = f.coverage?.level === "review" ? "approval-gating" : "blocking";
53303
53887
  const via = f.coverage?.via ?? "node9";
53888
+ const lbl = collision.has(f.category) ? `${f.category} (guarded)` : f.category;
53304
53889
  lines.push(
53305
- ` ${import_chalk27.default.green("\u2705")} ${label(f.category)}${import_chalk27.default.gray(`${via} is ${gated} this`)}`
53890
+ ` ${import_chalk27.default.green("\u2705")} ${label(lbl)}${import_chalk27.default.gray(`${via} is ${gated} ${guardedWhat(f)}`)}`
53306
53891
  );
53307
53892
  }
53308
53893
  lines.push("");
@@ -53312,17 +53897,17 @@ function renderPosture(result) {
53312
53897
  const osOpen = open.filter((f) => f.owner !== "node9" && !f.node9Reduces);
53313
53898
  if (node9Open.length > 0) {
53314
53899
  lines.push(" " + import_chalk27.default.cyan.bold("\u{1F527} node9 can fix these \u2014 run the command"));
53315
- for (const f of node9Open) lines.push(...renderFinding(f, true));
53900
+ for (const f of node9Open) lines.push(...renderFinding(f, true, openLabel(f)));
53316
53901
  }
53317
53902
  if (reduceOpen.length > 0) {
53318
53903
  if (node9Open.length > 0) lines.push("");
53319
53904
  lines.push(" " + import_chalk27.default.yellow.bold("\u{1F512} AVAILABLE \u2014 turn on to harden (each has a tradeoff)"));
53320
- for (const f of reduceOpen) lines.push(...renderFinding(f, true));
53905
+ for (const f of reduceOpen) lines.push(...renderFinding(f, true, openLabel(f)));
53321
53906
  }
53322
53907
  if (osOpen.length > 0) {
53323
53908
  if (node9Open.length > 0 || reduceOpen.length > 0) lines.push("");
53324
53909
  lines.push(" " + import_chalk27.default.bold("\u{1F9F1} YOUR PART \u2014 node9 can't fix these (OS-level)"));
53325
- for (const f of osOpen) lines.push(...renderFinding(f));
53910
+ for (const f of osOpen) lines.push(...renderFinding(f, false, openLabel(f)));
53326
53911
  }
53327
53912
  for (const cat of result.passedCategories) {
53328
53913
  lines.push(` ${import_chalk27.default.green("\u2705")} ${label(cat)}${import_chalk27.default.gray("no issues found")}`);
@@ -53381,7 +53966,7 @@ function registerPostureCommand(program2) {
53381
53966
  var import_chalk30 = __toESM(require("chalk"));
53382
53967
 
53383
53968
  // src/ci-check/fetch.ts
53384
- var import_fs60 = __toESM(require("fs"));
53969
+ var import_fs61 = __toESM(require("fs"));
53385
53970
  var import_path57 = __toESM(require("path"));
53386
53971
  var import_node_child_process = require("child_process");
53387
53972
  var import_undici = __toESM(require_undici());
@@ -53466,7 +54051,7 @@ function parseRepoUrl(input) {
53466
54051
  function isLocalPath(input) {
53467
54052
  if (input.startsWith(".") || input.startsWith("/") || input.startsWith("~")) return true;
53468
54053
  try {
53469
- return import_fs60.default.existsSync(input) && import_fs60.default.statSync(input).isDirectory();
54054
+ return import_fs61.default.existsSync(input) && import_fs61.default.statSync(input).isDirectory();
53470
54055
  } catch {
53471
54056
  return false;
53472
54057
  }
@@ -53583,8 +54168,8 @@ function readLocalTree(dir) {
53583
54168
  const add = (rel) => {
53584
54169
  const abs = import_path57.default.join(root, rel);
53585
54170
  try {
53586
- if (import_fs60.default.existsSync(abs) && import_fs60.default.statSync(abs).isFile()) {
53587
- files.push({ path: rel, content: import_fs60.default.readFileSync(abs, "utf8") });
54171
+ if (import_fs61.default.existsSync(abs) && import_fs61.default.statSync(abs).isFile()) {
54172
+ files.push({ path: rel, content: import_fs61.default.readFileSync(abs, "utf8") });
53588
54173
  }
53589
54174
  } catch {
53590
54175
  }
@@ -53604,7 +54189,7 @@ function readLocalTree(dir) {
53604
54189
  dirsVisited++;
53605
54190
  let entries;
53606
54191
  try {
53607
- entries = import_fs60.default.readdirSync(import_path57.default.join(root, relDir), { withFileTypes: true });
54192
+ entries = import_fs61.default.readdirSync(import_path57.default.join(root, relDir), { withFileTypes: true });
53608
54193
  } catch {
53609
54194
  return;
53610
54195
  }
@@ -53627,8 +54212,8 @@ function readLocalTree(dir) {
53627
54212
  for (const rel of matches) collect(rel);
53628
54213
  const wfDir = import_path57.default.join(root, WORKFLOW_DIR);
53629
54214
  try {
53630
- if (import_fs60.default.existsSync(wfDir)) {
53631
- for (const name of import_fs60.default.readdirSync(wfDir)) {
54215
+ if (import_fs61.default.existsSync(wfDir)) {
54216
+ for (const name of import_fs61.default.readdirSync(wfDir)) {
53632
54217
  if (/\.ya?ml$/.test(name)) add(import_path57.default.join(WORKFLOW_DIR, name));
53633
54218
  }
53634
54219
  }
@@ -54767,7 +55352,7 @@ function registerEgressCommand(program2) {
54767
55352
  var import_chalk32 = __toESM(require("chalk"));
54768
55353
 
54769
55354
  // src/shields/jail.ts
54770
- var import_fs61 = __toESM(require("fs"));
55355
+ var import_fs62 = __toESM(require("fs"));
54771
55356
  var import_os53 = __toESM(require("os"));
54772
55357
  var import_path58 = __toESM(require("path"));
54773
55358
  init_build();
@@ -54779,7 +55364,7 @@ function jailStorePath() {
54779
55364
  function readJailPaths() {
54780
55365
  let text;
54781
55366
  try {
54782
- text = import_fs61.default.readFileSync(jailStorePath(), "utf8");
55367
+ text = import_fs62.default.readFileSync(jailStorePath(), "utf8");
54783
55368
  } catch (err2) {
54784
55369
  if (err2.code === "ENOENT") return [];
54785
55370
  throw err2;
@@ -54797,8 +55382,8 @@ function readJailPaths() {
54797
55382
  }
54798
55383
  function writeJailPaths(paths) {
54799
55384
  const p = jailStorePath();
54800
- import_fs61.default.mkdirSync(import_path58.default.dirname(p), { recursive: true });
54801
- import_fs61.default.writeFileSync(p, JSON.stringify({ paths }, null, 2) + "\n", { mode: 384 });
55385
+ import_fs62.default.mkdirSync(import_path58.default.dirname(p), { recursive: true });
55386
+ import_fs62.default.writeFileSync(p, JSON.stringify({ paths }, null, 2) + "\n", { mode: 384 });
54802
55387
  }
54803
55388
  function addJailPath(rawPath, verdict) {
54804
55389
  const norm = rawPath.trim();
@@ -54827,7 +55412,7 @@ function regenerateUserJail(paths) {
54827
55412
  writeActiveShields(active2.filter((s) => s !== USER_JAIL_SHIELD));
54828
55413
  }
54829
55414
  try {
54830
- import_fs61.default.rmSync(file, { force: true });
55415
+ import_fs62.default.rmSync(file, { force: true });
54831
55416
  } catch {
54832
55417
  }
54833
55418
  return;
@@ -54941,13 +55526,13 @@ function registerJailCommand(program2) {
54941
55526
 
54942
55527
  // src/cli/commands/sandbox.ts
54943
55528
  var import_chalk33 = __toESM(require("chalk"));
54944
- var import_fs64 = __toESM(require("fs"));
55529
+ var import_fs65 = __toESM(require("fs"));
54945
55530
  var import_path61 = __toESM(require("path"));
54946
55531
  var import_child_process13 = require("child_process");
54947
55532
  init_config();
54948
55533
 
54949
55534
  // src/sandbox/config.ts
54950
- var import_fs62 = __toESM(require("fs"));
55535
+ var import_fs63 = __toESM(require("fs"));
54951
55536
  var import_path59 = __toESM(require("path"));
54952
55537
  var import_yaml2 = require("yaml");
54953
55538
  var SANDBOX_CONFIG_FILE = "node9.sandbox.yaml";
@@ -55025,12 +55610,12 @@ function sandboxConfigPath(cwd = process.cwd()) {
55025
55610
  }
55026
55611
  function loadSandboxConfig(cwd = process.cwd(), fallbackAgent = "claude") {
55027
55612
  const p = sandboxConfigPath(cwd);
55028
- if (!import_fs62.default.existsSync(p)) {
55613
+ if (!import_fs63.default.existsSync(p)) {
55029
55614
  throw new Error(`sandbox: ${SANDBOX_CONFIG_FILE} not found \u2014 run \`node9 sandbox new\` first.`);
55030
55615
  }
55031
55616
  let raw;
55032
55617
  try {
55033
- raw = (0, import_yaml2.parse)(import_fs62.default.readFileSync(p, "utf-8"));
55618
+ raw = (0, import_yaml2.parse)(import_fs63.default.readFileSync(p, "utf-8"));
55034
55619
  } catch (err2) {
55035
55620
  throw new Error(
55036
55621
  `sandbox: ${SANDBOX_CONFIG_FILE} is not valid YAML \u2014 ${err2.message}`
@@ -55088,7 +55673,7 @@ function compileAllowlist(input) {
55088
55673
  init_templates();
55089
55674
 
55090
55675
  // src/sandbox/runtime.ts
55091
- var import_fs63 = __toESM(require("fs"));
55676
+ var import_fs64 = __toESM(require("fs"));
55092
55677
  var import_os54 = __toESM(require("os"));
55093
55678
  var import_path60 = __toESM(require("path"));
55094
55679
  var import_crypto14 = __toESM(require("crypto"));
@@ -55116,7 +55701,7 @@ function buildRunArgs(opts) {
55116
55701
  args.push("-v", `${allowlistHostPath}:${ALLOWED_DOMAINS_PATH}:ro`);
55117
55702
  if (config.node9.mountAgentCredentials) {
55118
55703
  const creds = agentCredentialsMount(config.agent);
55119
- if (import_fs63.default.existsSync(creds.hostPath)) {
55704
+ if (import_fs64.default.existsSync(creds.hostPath)) {
55120
55705
  args.push("-v", `${creds.hostPath}:${creds.target}`);
55121
55706
  }
55122
55707
  }
@@ -55138,16 +55723,16 @@ function sandboxBuildDir(cwd = process.cwd()) {
55138
55723
  }
55139
55724
  function writeBuildContext(cwd, dockerfile, entrypoint) {
55140
55725
  const dir = sandboxBuildDir(cwd);
55141
- import_fs63.default.mkdirSync(dir, { recursive: true });
55142
- import_fs63.default.writeFileSync(import_path60.default.join(dir, "Dockerfile"), dockerfile);
55143
- import_fs63.default.writeFileSync(import_path60.default.join(dir, "entrypoint.sh"), entrypoint);
55726
+ import_fs64.default.mkdirSync(dir, { recursive: true });
55727
+ import_fs64.default.writeFileSync(import_path60.default.join(dir, "Dockerfile"), dockerfile);
55728
+ import_fs64.default.writeFileSync(import_path60.default.join(dir, "entrypoint.sh"), entrypoint);
55144
55729
  return dir;
55145
55730
  }
55146
55731
  function writeAllowlist(cwd, hosts) {
55147
55732
  const dir = import_path60.default.join(cwd, ".node9", "sandbox");
55148
- import_fs63.default.mkdirSync(dir, { recursive: true });
55733
+ import_fs64.default.mkdirSync(dir, { recursive: true });
55149
55734
  const p = import_path60.default.join(dir, "allowed-domains.txt");
55150
- import_fs63.default.writeFileSync(p, hosts.join("\n") + "\n");
55735
+ import_fs64.default.writeFileSync(p, hosts.join("\n") + "\n");
55151
55736
  return p;
55152
55737
  }
55153
55738
  function resolveHomePath(p) {
@@ -55156,7 +55741,7 @@ function resolveHomePath(p) {
55156
55741
 
55157
55742
  // src/cli/commands/sandbox.ts
55158
55743
  function seedDataDirConfig(dataDir, sandbox) {
55159
- import_fs64.default.mkdirSync(dataDir, { recursive: true });
55744
+ import_fs65.default.mkdirSync(dataDir, { recursive: true });
55160
55745
  const configPath = import_path61.default.join(dataDir, "config.json");
55161
55746
  const seed = {
55162
55747
  settings: {
@@ -55168,7 +55753,7 @@ function seedDataDirConfig(dataDir, sandbox) {
55168
55753
  }
55169
55754
  }
55170
55755
  };
55171
- import_fs64.default.writeFileSync(configPath, JSON.stringify(seed, null, 2), { mode: 384 });
55756
+ import_fs65.default.writeFileSync(configPath, JSON.stringify(seed, null, 2), { mode: 384 });
55172
55757
  }
55173
55758
  function registerSandboxCommand(program2, version2) {
55174
55759
  const node9Version2 = pinnedNode9Version(version2);
@@ -55176,13 +55761,13 @@ function registerSandboxCommand(program2, version2) {
55176
55761
  cmd.command("new").description(`Scaffold ${SANDBOX_CONFIG_FILE} in this project`).option("--agent <agent>", "claude (default) or codex", "claude").action((opts) => {
55177
55762
  const agent = opts.agent === "codex" ? "codex" : "claude";
55178
55763
  const p = sandboxConfigPath();
55179
- if (import_fs64.default.existsSync(p)) {
55764
+ if (import_fs65.default.existsSync(p)) {
55180
55765
  console.log(
55181
55766
  import_chalk33.default.yellow(` ${SANDBOX_CONFIG_FILE} already exists \u2014 leaving it untouched.`)
55182
55767
  );
55183
55768
  return;
55184
55769
  }
55185
- import_fs64.default.writeFileSync(p, scaffoldSandboxYaml(agent));
55770
+ import_fs65.default.writeFileSync(p, scaffoldSandboxYaml(agent));
55186
55771
  console.log(
55187
55772
  import_chalk33.default.green(` \u2713 wrote ${SANDBOX_CONFIG_FILE}`) + import_chalk33.default.dim(` (agent: ${agent})`)
55188
55773
  );
@@ -55223,7 +55808,7 @@ function registerSandboxCommand(program2, version2) {
55223
55808
  const hash = imageContentHash(dockerfile, entrypoint);
55224
55809
  const image = sandbox.runtime.image;
55225
55810
  const hashFile = import_path61.default.join(sandboxBuildDir(cwd), ".image-hash");
55226
- const lastHash = import_fs64.default.existsSync(hashFile) ? import_fs64.default.readFileSync(hashFile, "utf-8").trim() : "";
55811
+ const lastHash = import_fs65.default.existsSync(hashFile) ? import_fs65.default.readFileSync(hashFile, "utf-8").trim() : "";
55227
55812
  const imageExists = (0, import_child_process13.spawnSync)(sandbox.runtime.engine, ["image", "inspect", image], { stdio: "ignore" }).status === 0;
55228
55813
  const needBuild = sandbox.runtime.rebuild === "always" || !imageExists || sandbox.runtime.rebuild !== "never" && lastHash !== hash;
55229
55814
  if (needBuild) {
@@ -55235,7 +55820,7 @@ function registerSandboxCommand(program2, version2) {
55235
55820
  console.error(import_chalk33.default.red(" build failed."));
55236
55821
  process.exit(b.status ?? 1);
55237
55822
  }
55238
- import_fs64.default.writeFileSync(hashFile, hash);
55823
+ import_fs65.default.writeFileSync(hashFile, hash);
55239
55824
  }
55240
55825
  const dataDir = sandboxDataDir(cwd);
55241
55826
  seedDataDirConfig(dataDir, sandbox);
@@ -55249,7 +55834,7 @@ function registerSandboxCommand(program2, version2) {
55249
55834
  });
55250
55835
  if (sandbox.node9.mountAgentCredentials) {
55251
55836
  const creds = agentCredentialsMount(sandbox.agent);
55252
- if (import_fs64.default.existsSync(creds.hostPath)) {
55837
+ if (import_fs65.default.existsSync(creds.hostPath)) {
55253
55838
  console.log(import_chalk33.default.dim(` mounting ${creds.hostPath} (agent credentials, rw)`));
55254
55839
  } else {
55255
55840
  console.log(
@@ -55266,7 +55851,7 @@ function registerSandboxCommand(program2, version2) {
55266
55851
  });
55267
55852
  cmd.command("tail").description("Stream the sandbox's audit log (host-side)").action(() => {
55268
55853
  const auditPath = import_path61.default.join(sandboxDataDir(), "audit.log");
55269
- if (!import_fs64.default.existsSync(auditPath)) {
55854
+ if (!import_fs65.default.existsSync(auditPath)) {
55270
55855
  console.log(import_chalk33.default.dim(" no sandbox audit yet."));
55271
55856
  return;
55272
55857
  }
@@ -55274,11 +55859,11 @@ function registerSandboxCommand(program2, version2) {
55274
55859
  });
55275
55860
  cmd.command("logs").description("Dump the sandbox's audit log").action(() => {
55276
55861
  const auditPath = import_path61.default.join(sandboxDataDir(), "audit.log");
55277
- if (!import_fs64.default.existsSync(auditPath)) {
55862
+ if (!import_fs65.default.existsSync(auditPath)) {
55278
55863
  console.log(import_chalk33.default.dim(" no sandbox audit yet."));
55279
55864
  return;
55280
55865
  }
55281
- process.stdout.write(import_fs64.default.readFileSync(auditPath, "utf-8"));
55866
+ process.stdout.write(import_fs65.default.readFileSync(auditPath, "utf-8"));
55282
55867
  });
55283
55868
  cmd.command("clean").description("Remove the sandbox image, build context, and data").action(() => {
55284
55869
  const cwd = process.cwd();
@@ -55292,15 +55877,16 @@ function registerSandboxCommand(program2, version2) {
55292
55877
  stdio: "ignore"
55293
55878
  });
55294
55879
  }
55295
- import_fs64.default.rmSync(import_path61.default.join(cwd, ".node9", "sandbox"), { recursive: true, force: true });
55880
+ import_fs65.default.rmSync(import_path61.default.join(cwd, ".node9", "sandbox"), { recursive: true, force: true });
55296
55881
  console.log(import_chalk33.default.green(" \u2713 sandbox image + build + data removed."));
55297
55882
  });
55298
55883
  }
55299
55884
 
55300
55885
  // src/cli/commands/sessions.ts
55301
55886
  var import_chalk34 = __toESM(require("chalk"));
55302
- var import_fs65 = __toESM(require("fs"));
55887
+ var import_fs66 = __toESM(require("fs"));
55303
55888
  var import_path62 = __toESM(require("path"));
55889
+ init_decision();
55304
55890
  var import_os55 = __toESM(require("os"));
55305
55891
  init_scan_summary();
55306
55892
  init_litellm();
@@ -55397,7 +55983,7 @@ function loadAuditEntries(auditPath) {
55397
55983
  const aPath = auditPath ?? import_path62.default.join(import_os55.default.homedir(), ".node9", "audit.log");
55398
55984
  let raw;
55399
55985
  try {
55400
- raw = import_fs65.default.readFileSync(aPath, "utf-8");
55986
+ raw = import_fs66.default.readFileSync(aPath, "utf-8");
55401
55987
  } catch {
55402
55988
  return [];
55403
55989
  }
@@ -55407,7 +55993,7 @@ function loadAuditEntries(auditPath) {
55407
55993
  try {
55408
55994
  const e = JSON.parse(line);
55409
55995
  if (!e.ts || !e.tool || !e.decision) continue;
55410
- if (e.decision === "allow" || e.decision === "allowed") continue;
55996
+ if (classifyDecision(e).outcome === "allow") continue;
55411
55997
  entries.push(e);
55412
55998
  } catch {
55413
55999
  }
@@ -55434,7 +56020,7 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
55434
56020
  }
55435
56021
  function buildGeminiSessions(days, allAuditEntries) {
55436
56022
  const tmpDir = import_path62.default.join(import_os55.default.homedir(), ".gemini", "tmp");
55437
- if (!import_fs65.default.existsSync(tmpDir)) return [];
56023
+ if (!import_fs66.default.existsSync(tmpDir)) return [];
55438
56024
  const cutoff = days !== null ? (() => {
55439
56025
  const d = /* @__PURE__ */ new Date();
55440
56026
  d.setDate(d.getDate() - days);
@@ -55443,7 +56029,7 @@ function buildGeminiSessions(days, allAuditEntries) {
55443
56029
  })() : null;
55444
56030
  let slugDirs;
55445
56031
  try {
55446
- slugDirs = import_fs65.default.readdirSync(tmpDir);
56032
+ slugDirs = import_fs66.default.readdirSync(tmpDir);
55447
56033
  } catch {
55448
56034
  return [];
55449
56035
  }
@@ -55451,27 +56037,27 @@ function buildGeminiSessions(days, allAuditEntries) {
55451
56037
  for (const slug2 of slugDirs) {
55452
56038
  const slugPath = import_path62.default.join(tmpDir, slug2);
55453
56039
  try {
55454
- if (!import_fs65.default.statSync(slugPath).isDirectory()) continue;
56040
+ if (!import_fs66.default.statSync(slugPath).isDirectory()) continue;
55455
56041
  } catch {
55456
56042
  continue;
55457
56043
  }
55458
56044
  let projectRoot = import_path62.default.join(import_os55.default.homedir(), slug2);
55459
56045
  try {
55460
- projectRoot = import_fs65.default.readFileSync(import_path62.default.join(slugPath, ".project_root"), "utf-8").trim();
56046
+ projectRoot = import_fs66.default.readFileSync(import_path62.default.join(slugPath, ".project_root"), "utf-8").trim();
55461
56047
  } catch {
55462
56048
  }
55463
56049
  const chatsDir = import_path62.default.join(slugPath, "chats");
55464
- if (!import_fs65.default.existsSync(chatsDir)) continue;
56050
+ if (!import_fs66.default.existsSync(chatsDir)) continue;
55465
56051
  let chatFiles;
55466
56052
  try {
55467
- chatFiles = import_fs65.default.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
56053
+ chatFiles = import_fs66.default.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
55468
56054
  } catch {
55469
56055
  continue;
55470
56056
  }
55471
56057
  for (const chatFile of chatFiles) {
55472
56058
  let raw;
55473
56059
  try {
55474
- raw = import_fs65.default.readFileSync(import_path62.default.join(chatsDir, chatFile), "utf-8");
56060
+ raw = import_fs66.default.readFileSync(import_path62.default.join(chatsDir, chatFile), "utf-8");
55475
56061
  } catch {
55476
56062
  continue;
55477
56063
  }
@@ -55552,7 +56138,7 @@ function buildGeminiSessions(days, allAuditEntries) {
55552
56138
  }
55553
56139
  function buildCodexSessions(days, allAuditEntries) {
55554
56140
  const sessionsBase = import_path62.default.join(import_os55.default.homedir(), ".codex", "sessions");
55555
- if (!import_fs65.default.existsSync(sessionsBase)) return [];
56141
+ if (!import_fs66.default.existsSync(sessionsBase)) return [];
55556
56142
  const cutoff = days !== null ? (() => {
55557
56143
  const d = /* @__PURE__ */ new Date();
55558
56144
  d.setDate(d.getDate() - days);
@@ -55561,28 +56147,28 @@ function buildCodexSessions(days, allAuditEntries) {
55561
56147
  })() : null;
55562
56148
  const jsonlFiles = [];
55563
56149
  try {
55564
- for (const year of import_fs65.default.readdirSync(sessionsBase)) {
56150
+ for (const year of import_fs66.default.readdirSync(sessionsBase)) {
55565
56151
  const yearPath = import_path62.default.join(sessionsBase, year);
55566
56152
  try {
55567
- if (!import_fs65.default.statSync(yearPath).isDirectory()) continue;
56153
+ if (!import_fs66.default.statSync(yearPath).isDirectory()) continue;
55568
56154
  } catch {
55569
56155
  continue;
55570
56156
  }
55571
- for (const month of import_fs65.default.readdirSync(yearPath)) {
56157
+ for (const month of import_fs66.default.readdirSync(yearPath)) {
55572
56158
  const monthPath = import_path62.default.join(yearPath, month);
55573
56159
  try {
55574
- if (!import_fs65.default.statSync(monthPath).isDirectory()) continue;
56160
+ if (!import_fs66.default.statSync(monthPath).isDirectory()) continue;
55575
56161
  } catch {
55576
56162
  continue;
55577
56163
  }
55578
- for (const day of import_fs65.default.readdirSync(monthPath)) {
56164
+ for (const day of import_fs66.default.readdirSync(monthPath)) {
55579
56165
  const dayPath = import_path62.default.join(monthPath, day);
55580
56166
  try {
55581
- if (!import_fs65.default.statSync(dayPath).isDirectory()) continue;
56167
+ if (!import_fs66.default.statSync(dayPath).isDirectory()) continue;
55582
56168
  } catch {
55583
56169
  continue;
55584
56170
  }
55585
- for (const file of import_fs65.default.readdirSync(dayPath)) {
56171
+ for (const file of import_fs66.default.readdirSync(dayPath)) {
55586
56172
  if (file.endsWith(".jsonl")) jsonlFiles.push(import_path62.default.join(dayPath, file));
55587
56173
  }
55588
56174
  }
@@ -55595,7 +56181,7 @@ function buildCodexSessions(days, allAuditEntries) {
55595
56181
  for (const filePath of jsonlFiles) {
55596
56182
  let lines;
55597
56183
  try {
55598
- lines = import_fs65.default.readFileSync(filePath, "utf-8").split("\n");
56184
+ lines = import_fs66.default.readFileSync(filePath, "utf-8").split("\n");
55599
56185
  } catch {
55600
56186
  continue;
55601
56187
  }
@@ -55684,7 +56270,7 @@ function buildSessions(days, historyPath) {
55684
56270
  const hPath = historyPath ?? import_path62.default.join(import_os55.default.homedir(), ".claude", "history.jsonl");
55685
56271
  let historyRaw = "";
55686
56272
  try {
55687
- historyRaw = import_fs65.default.readFileSync(hPath, "utf-8");
56273
+ historyRaw = import_fs66.default.readFileSync(hPath, "utf-8");
55688
56274
  } catch {
55689
56275
  }
55690
56276
  const cutoff = days !== null ? (() => {
@@ -55708,7 +56294,7 @@ function buildSessions(days, historyPath) {
55708
56294
  const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
55709
56295
  let sessionLines = [];
55710
56296
  try {
55711
- sessionLines = import_fs65.default.readFileSync(jsonlFile, "utf-8").split("\n");
56297
+ sessionLines = import_fs66.default.readFileSync(jsonlFile, "utf-8").split("\n");
55712
56298
  } catch {
55713
56299
  }
55714
56300
  const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
@@ -56102,12 +56688,12 @@ function registerSessionTaintCommand(program2) {
56102
56688
 
56103
56689
  // src/cli/commands/skill-pin.ts
56104
56690
  var import_chalk36 = __toESM(require("chalk"));
56105
- var import_fs66 = __toESM(require("fs"));
56691
+ var import_fs67 = __toESM(require("fs"));
56106
56692
  var import_os56 = __toESM(require("os"));
56107
56693
  var import_path63 = __toESM(require("path"));
56108
56694
  function wipeSkillSessions() {
56109
56695
  try {
56110
- import_fs66.default.rmSync(import_path63.default.join(import_os56.default.homedir(), ".node9", "skill-sessions"), {
56696
+ import_fs67.default.rmSync(import_path63.default.join(import_os56.default.homedir(), ".node9", "skill-sessions"), {
56111
56697
  recursive: true,
56112
56698
  force: true
56113
56699
  });
@@ -56189,15 +56775,15 @@ function registerSkillPinCommand(program2) {
56189
56775
  }
56190
56776
 
56191
56777
  // src/cli/commands/decisions.ts
56192
- var import_fs67 = __toESM(require("fs"));
56778
+ var import_fs68 = __toESM(require("fs"));
56193
56779
  var import_os57 = __toESM(require("os"));
56194
56780
  var import_path64 = __toESM(require("path"));
56195
56781
  var import_chalk37 = __toESM(require("chalk"));
56196
56782
  var DECISIONS_FILE2 = import_path64.default.join(import_os57.default.homedir(), ".node9", "decisions.json");
56197
56783
  function readDecisions() {
56198
56784
  try {
56199
- if (!import_fs67.default.existsSync(DECISIONS_FILE2)) return {};
56200
- const raw = import_fs67.default.readFileSync(DECISIONS_FILE2, "utf-8");
56785
+ if (!import_fs68.default.existsSync(DECISIONS_FILE2)) return {};
56786
+ const raw = import_fs68.default.readFileSync(DECISIONS_FILE2, "utf-8");
56201
56787
  const parsed = JSON.parse(raw);
56202
56788
  const out = {};
56203
56789
  for (const [k, v] of Object.entries(parsed)) {
@@ -56210,10 +56796,10 @@ function readDecisions() {
56210
56796
  }
56211
56797
  function writeDecisions(d) {
56212
56798
  const dir = import_path64.default.dirname(DECISIONS_FILE2);
56213
- if (!import_fs67.default.existsSync(dir)) import_fs67.default.mkdirSync(dir, { recursive: true });
56799
+ if (!import_fs68.default.existsSync(dir)) import_fs68.default.mkdirSync(dir, { recursive: true });
56214
56800
  const tmp = `${DECISIONS_FILE2}.${process.pid}.tmp`;
56215
- import_fs67.default.writeFileSync(tmp, JSON.stringify(d, null, 2));
56216
- import_fs67.default.renameSync(tmp, DECISIONS_FILE2);
56801
+ import_fs68.default.writeFileSync(tmp, JSON.stringify(d, null, 2));
56802
+ import_fs68.default.renameSync(tmp, DECISIONS_FILE2);
56217
56803
  }
56218
56804
  function registerDecisionsCommand(program2) {
56219
56805
  const cmd = program2.command("decisions").description('Manage persistent "Always Allow" / "Always Deny" tool decisions');
@@ -56270,7 +56856,7 @@ Persistent decisions (${entries.length})
56270
56856
 
56271
56857
  // src/cli/commands/dlp.ts
56272
56858
  var import_chalk38 = __toESM(require("chalk"));
56273
- var import_fs68 = __toESM(require("fs"));
56859
+ var import_fs69 = __toESM(require("fs"));
56274
56860
  var import_path65 = __toESM(require("path"));
56275
56861
  var import_os58 = __toESM(require("os"));
56276
56862
  var AUDIT_LOG = import_path65.default.join(import_os58.default.homedir(), ".node9", "audit.log");
@@ -56281,7 +56867,7 @@ function stripAnsi(s) {
56281
56867
  }
56282
56868
  function loadResolved() {
56283
56869
  try {
56284
- const raw = JSON.parse(import_fs68.default.readFileSync(RESOLVED_FILE, "utf-8"));
56870
+ const raw = JSON.parse(import_fs69.default.readFileSync(RESOLVED_FILE, "utf-8"));
56285
56871
  return new Set(raw);
56286
56872
  } catch {
56287
56873
  return /* @__PURE__ */ new Set();
@@ -56289,13 +56875,13 @@ function loadResolved() {
56289
56875
  }
56290
56876
  function saveResolved(resolved) {
56291
56877
  try {
56292
- import_fs68.default.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
56878
+ import_fs69.default.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
56293
56879
  } catch {
56294
56880
  }
56295
56881
  }
56296
56882
  function loadDlpFindings() {
56297
- if (!import_fs68.default.existsSync(AUDIT_LOG)) return [];
56298
- return import_fs68.default.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
56883
+ if (!import_fs69.default.existsSync(AUDIT_LOG)) return [];
56884
+ return import_fs69.default.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
56299
56885
  if (!line.trim()) return [];
56300
56886
  try {
56301
56887
  const e = JSON.parse(line);
@@ -56393,14 +56979,14 @@ function registerDlpCommand(program2) {
56393
56979
 
56394
56980
  // src/cli/commands/mask.ts
56395
56981
  var import_chalk39 = __toESM(require("chalk"));
56396
- var import_fs69 = __toESM(require("fs"));
56982
+ var import_fs70 = __toESM(require("fs"));
56397
56983
  var import_path66 = __toESM(require("path"));
56398
56984
  var import_os59 = __toESM(require("os"));
56399
56985
  init_dlp();
56400
56986
  function findJsonlFiles(dir) {
56401
56987
  const results = [];
56402
- if (!import_fs69.default.existsSync(dir)) return results;
56403
- for (const entry of import_fs69.default.readdirSync(dir, { withFileTypes: true })) {
56988
+ if (!import_fs70.default.existsSync(dir)) return results;
56989
+ for (const entry of import_fs70.default.readdirSync(dir, { withFileTypes: true })) {
56404
56990
  const full = import_path66.default.join(dir, entry.name);
56405
56991
  if (entry.isDirectory()) results.push(...findJsonlFiles(full));
56406
56992
  else if (entry.isFile() && entry.name.endsWith(".jsonl")) results.push(full);
@@ -56444,7 +57030,7 @@ function redactJson(obj) {
56444
57030
  function processFile(filePath, dryRun) {
56445
57031
  let raw;
56446
57032
  try {
56447
- raw = import_fs69.default.readFileSync(filePath, "utf-8");
57033
+ raw = import_fs70.default.readFileSync(filePath, "utf-8");
56448
57034
  } catch {
56449
57035
  return { redactedLines: 0, patterns: [] };
56450
57036
  }
@@ -56476,14 +57062,14 @@ function processFile(filePath, dryRun) {
56476
57062
  }
56477
57063
  }
56478
57064
  if (!dryRun && redactedLines > 0) {
56479
- import_fs69.default.writeFileSync(filePath, newLines.join("\n"), "utf-8");
57065
+ import_fs70.default.writeFileSync(filePath, newLines.join("\n"), "utf-8");
56480
57066
  }
56481
57067
  return { redactedLines, patterns };
56482
57068
  }
56483
57069
  function processJsonFile(filePath, dryRun) {
56484
57070
  let raw;
56485
57071
  try {
56486
- raw = import_fs69.default.readFileSync(filePath, "utf-8");
57072
+ raw = import_fs70.default.readFileSync(filePath, "utf-8");
56487
57073
  } catch {
56488
57074
  return { redactedLines: 0, patterns: [] };
56489
57075
  }
@@ -56496,14 +57082,14 @@ function processJsonFile(filePath, dryRun) {
56496
57082
  const { value, modified, found } = redactJson(parsed);
56497
57083
  if (!modified) return { redactedLines: 0, patterns: [] };
56498
57084
  if (!dryRun) {
56499
- import_fs69.default.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
57085
+ import_fs70.default.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
56500
57086
  }
56501
57087
  return { redactedLines: 1, patterns: found };
56502
57088
  }
56503
57089
  function findJsonFiles(dir) {
56504
57090
  const results = [];
56505
- if (!import_fs69.default.existsSync(dir)) return results;
56506
- for (const entry of import_fs69.default.readdirSync(dir, { withFileTypes: true })) {
57091
+ if (!import_fs70.default.existsSync(dir)) return results;
57092
+ for (const entry of import_fs70.default.readdirSync(dir, { withFileTypes: true })) {
56507
57093
  const full = import_path66.default.join(dir, entry.name);
56508
57094
  if (entry.isDirectory()) results.push(...findJsonFiles(full));
56509
57095
  else if (entry.isFile() && entry.name.endsWith(".json")) results.push(full);
@@ -56523,7 +57109,7 @@ function registerMaskCommand(program2) {
56523
57109
  const cutoff = options.all ? null : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
56524
57110
  const filtered = cutoff ? allFiles.filter((f) => {
56525
57111
  try {
56526
- return import_fs69.default.statSync(f.path).mtime >= cutoff;
57112
+ return import_fs70.default.statSync(f.path).mtime >= cutoff;
56527
57113
  } catch {
56528
57114
  return false;
56529
57115
  }
@@ -56579,7 +57165,7 @@ function registerMaskCommand(program2) {
56579
57165
  // src/cli.ts
56580
57166
  init_blast();
56581
57167
  var { version } = JSON.parse(
56582
- import_fs72.default.readFileSync(import_path69.default.join(__dirname, "../package.json"), "utf-8")
57168
+ import_fs73.default.readFileSync(import_path69.default.join(__dirname, "../package.json"), "utf-8")
56583
57169
  );
56584
57170
  var program = new import_commander.Command();
56585
57171
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
@@ -56759,14 +57345,14 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
56759
57345
  }
56760
57346
  if (options.purge) {
56761
57347
  const node9Dir = import_path69.default.join(import_os62.default.homedir(), ".node9");
56762
- if (import_fs72.default.existsSync(node9Dir)) {
57348
+ if (import_fs73.default.existsSync(node9Dir)) {
56763
57349
  const confirmed = await (0, import_prompts2.confirm)({
56764
57350
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
56765
57351
  default: false
56766
57352
  });
56767
57353
  if (confirmed) {
56768
- import_fs72.default.rmSync(node9Dir, { recursive: true });
56769
- if (import_fs72.default.existsSync(node9Dir)) {
57354
+ import_fs73.default.rmSync(node9Dir, { recursive: true });
57355
+ if (import_fs73.default.existsSync(node9Dir)) {
56770
57356
  console.error(
56771
57357
  import_chalk41.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
56772
57358
  );
@@ -56931,12 +57517,12 @@ Run "node9 addto claude" to register it as the statusLine.`
56931
57517
  if (subcommand === "debug") {
56932
57518
  const flagFile = import_path69.default.join(import_os62.default.homedir(), ".node9", "hud-debug");
56933
57519
  if (state === "on") {
56934
- import_fs72.default.mkdirSync(import_path69.default.dirname(flagFile), { recursive: true });
56935
- import_fs72.default.writeFileSync(flagFile, "");
57520
+ import_fs73.default.mkdirSync(import_path69.default.dirname(flagFile), { recursive: true });
57521
+ import_fs73.default.writeFileSync(flagFile, "");
56936
57522
  console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
56937
57523
  console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
56938
57524
  } else if (state === "off") {
56939
- if (import_fs72.default.existsSync(flagFile)) import_fs72.default.unlinkSync(flagFile);
57525
+ if (import_fs73.default.existsSync(flagFile)) import_fs73.default.unlinkSync(flagFile);
56940
57526
  console.log("HUD debug logging disabled.");
56941
57527
  } else {
56942
57528
  console.error("Usage: node9 hud debug on|off");
@@ -57061,7 +57647,7 @@ if (process.argv[2] !== "daemon") {
57061
57647
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
57062
57648
  const logPath = import_path69.default.join(import_os62.default.homedir(), ".node9", "hook-debug.log");
57063
57649
  const msg = reason instanceof Error ? reason.message : String(reason);
57064
- import_fs72.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
57650
+ import_fs73.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
57065
57651
  `);
57066
57652
  }
57067
57653
  process.exit(0);