@node9/proxy 1.63.0 → 1.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -347,6 +347,7 @@ var init_config_schema = __esm({
347
347
  // nudge-only (default), and the scan cadence in minutes.
348
348
  mcpAutoWrap: z.boolean().optional(),
349
349
  mcpReconcileIntervalMinutes: z.number().positive().optional(),
350
+ mcpStaleAfterDays: z.number().min(0).optional(),
350
351
  cloudSyncIntervalHours: z.number().positive().optional(),
351
352
  // Seconds-granular override for the cloud policy sync cadence. Wins over
352
353
  // cloudSyncIntervalHours when set. Lets you opt into fast apply (e.g. 20)
@@ -3981,6 +3982,7 @@ var init_dist = __esm({
3981
3982
  name: "redis",
3982
3983
  description: "Protects Redis instances from destructive AI operations",
3983
3984
  aliases: [],
3985
+ _scopeNote: "Every rule requires Redis to actually be involved: a client in the command (redis-cli / valkey-cli, in either order \u2014 `redis-cli FLUSHALL` and `echo FLUSHALL | redis-cli` are both real), OR the command field holding the bare keyword, which is what the MCP redis_command tool sends ({ command: 'FLUSHALL', args: [] }). Without the client requirement these rules matched the WORD anywhere \u2014 including prose in a heredoc writing a file \u2014 which produced two observed false positives: a test fixture describing the shield, and a commit message naming the rule. Every other builtin shield already scopes this way (aws s3, docker system prune, kubectl delete, gh repo delete). See redis-scope.spec.ts.",
3984
3986
  smartRules: [
3985
3987
  {
3986
3988
  name: "shield:redis:block-flushall",
@@ -3989,7 +3991,7 @@ var init_dist = __esm({
3989
3991
  {
3990
3992
  field: "command",
3991
3993
  op: "matches",
3992
- value: "\\bFLUSHALL\\b",
3994
+ value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
3993
3995
  flags: "i"
3994
3996
  }
3995
3997
  ],
@@ -4003,7 +4005,7 @@ var init_dist = __esm({
4003
4005
  {
4004
4006
  field: "command",
4005
4007
  op: "matches",
4006
- value: "\\bFLUSHDB\\b",
4008
+ value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
4007
4009
  flags: "i"
4008
4010
  }
4009
4011
  ],
@@ -4017,7 +4019,7 @@ var init_dist = __esm({
4017
4019
  {
4018
4020
  field: "command",
4019
4021
  op: "matches",
4020
- value: "\\bCONFIG\\s+RESETSTAT\\b",
4022
+ value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
4021
4023
  flags: "i"
4022
4024
  }
4023
4025
  ],
@@ -4031,7 +4033,7 @@ var init_dist = __esm({
4031
4033
  {
4032
4034
  field: "command",
4033
4035
  op: "matches",
4034
- value: "\\bCONFIG\\s+SET\\b",
4036
+ value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
4035
4037
  flags: "i"
4036
4038
  }
4037
4039
  ],
@@ -4045,7 +4047,7 @@ var init_dist = __esm({
4045
4047
  {
4046
4048
  field: "command",
4047
4049
  op: "matches",
4048
- value: "\\bDEL\\b.*[*?\\[]|redis-cli.*--scan.*\\|.*xargs.*del",
4050
+ value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
4049
4051
  flags: "i"
4050
4052
  }
4051
4053
  ],
@@ -4615,6 +4617,48 @@ function getActiveEnvironment(config) {
4615
4617
  const env = config.settings.environment || process.env.NODE_ENV || "development";
4616
4618
  return config.environments[env] ?? null;
4617
4619
  }
4620
+ function readRulesCacheResilient(cacheFile) {
4621
+ let existed = false;
4622
+ for (let attempt = 0; attempt < 3; attempt++) {
4623
+ let content;
4624
+ try {
4625
+ content = fs4.readFileSync(cacheFile, "utf-8");
4626
+ existed = true;
4627
+ } catch (err2) {
4628
+ if (err2.code === "ENOENT") return {};
4629
+ continue;
4630
+ }
4631
+ try {
4632
+ return JSON.parse(content);
4633
+ } catch {
4634
+ }
4635
+ }
4636
+ if (existed) {
4637
+ const backup = path4.join(path4.dirname(cacheFile), "rules-cache.last-good.json");
4638
+ if (backup !== cacheFile) {
4639
+ try {
4640
+ const raw = JSON.parse(fs4.readFileSync(backup, "utf-8"));
4641
+ logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
4642
+ return raw;
4643
+ } catch {
4644
+ }
4645
+ }
4646
+ logCacheReadIssue(cacheFile, "RULES_CACHE_UNREADABLE");
4647
+ }
4648
+ return {};
4649
+ }
4650
+ function logCacheReadIssue(cacheFile, kind) {
4651
+ if (cacheReadFailureLogged) return;
4652
+ cacheReadFailureLogged = true;
4653
+ try {
4654
+ fs4.appendFileSync(
4655
+ path4.join(os4.homedir(), ".node9", "hook-debug.log"),
4656
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
4657
+ `
4658
+ );
4659
+ } catch {
4660
+ }
4661
+ }
4618
4662
  function getConfig(cwd) {
4619
4663
  if (!cwd && cachedConfig) return cachedConfig;
4620
4664
  const globalPath = path4.join(os4.homedir(), ".node9", "config.json");
@@ -4682,6 +4726,7 @@ function getConfig(cwd) {
4682
4726
  if (s.mcpAutoWrap !== void 0) mergedSettings.mcpAutoWrap = s.mcpAutoWrap === true;
4683
4727
  if (s.mcpReconcileIntervalMinutes !== void 0)
4684
4728
  mergedSettings.mcpReconcileIntervalMinutes = s.mcpReconcileIntervalMinutes;
4729
+ if (s.mcpStaleAfterDays !== void 0) mergedSettings.mcpStaleAfterDays = s.mcpStaleAfterDays;
4685
4730
  if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
4686
4731
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
4687
4732
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -4691,12 +4736,13 @@ function getConfig(cwd) {
4691
4736
  if (p.smartRules) {
4692
4737
  const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
4693
4738
  const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
4694
- const userRuleNames = new Set(p.smartRules.filter((r) => r.name).map((r) => r.name));
4739
+ const localRules = p.smartRules.map(({ pinned: _pinned, ...r }) => r);
4740
+ const userRuleNames = new Set(localRules.filter((r) => r.name).map((r) => r.name));
4695
4741
  const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
4696
4742
  const filteredNonBlocks = defaultNonBlocks.filter(
4697
4743
  (r) => !r.name || !userRuleNames.has(r.name)
4698
4744
  );
4699
- mergedPolicy.smartRules = [...filteredBlocks, ...p.smartRules, ...filteredNonBlocks];
4745
+ mergedPolicy.smartRules = [...filteredBlocks, ...localRules, ...filteredNonBlocks];
4700
4746
  }
4701
4747
  if (p.snapshot) {
4702
4748
  const s2 = p.snapshot;
@@ -4761,10 +4807,12 @@ function getConfig(cwd) {
4761
4807
  applyLayer(globalConfig);
4762
4808
  applyLayer(projectConfig);
4763
4809
  let cloudManagedShields = [];
4810
+ let modeCloudControlled = false;
4811
+ let modeCloudStaged = false;
4764
4812
  {
4765
4813
  const cacheFile = path4.join(os4.homedir(), ".node9", "rules-cache.json");
4766
4814
  try {
4767
- const raw = JSON.parse(fs4.readFileSync(cacheFile, "utf-8"));
4815
+ const raw = readRulesCacheResilient(cacheFile);
4768
4816
  if (Array.isArray(raw.rules) && raw.rules.length > 0) {
4769
4817
  applyLayer({ policy: { smartRules: raw.rules } });
4770
4818
  }
@@ -4781,6 +4829,9 @@ function getConfig(cwd) {
4781
4829
  locked.includes("mode")
4782
4830
  );
4783
4831
  }
4832
+ if (typeof mc.mode === "string" || locked.includes("mode")) {
4833
+ modeCloudControlled = true;
4834
+ }
4784
4835
  if (mc.egress && typeof mc.egress === "object") {
4785
4836
  const hosts = (v) => Array.isArray(v) ? v.filter((h) => typeof h === "string") : void 0;
4786
4837
  mergedPolicy.egress = applyManagedEgress(
@@ -4879,19 +4930,28 @@ function getConfig(cwd) {
4879
4930
  }
4880
4931
  if (raw.shadowMode === true) {
4881
4932
  mergedSettings.mode = "observe";
4933
+ modeCloudStaged = true;
4882
4934
  }
4883
4935
  } catch {
4884
4936
  }
4885
4937
  }
4886
4938
  const shieldOverrides = readShieldOverrides();
4887
4939
  const activeShieldNames = [.../* @__PURE__ */ new Set([...readActiveShields(), ...cloudManagedShields])];
4940
+ const cloudManagedSet = new Set(cloudManagedShields);
4888
4941
  for (const shieldName of activeShieldNames) {
4889
- const shield = getShield(shieldName);
4942
+ const isCloudMandated = cloudManagedSet.has(shieldName);
4943
+ const shield = isCloudMandated ? BUILTIN_SHIELDS[shieldName] : getShield(shieldName);
4890
4944
  if (!shield) continue;
4891
4945
  const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4892
- const ruleOverrides = shieldOverrides[shieldName] ?? {};
4946
+ const ruleOverrides = isCloudMandated ? {} : shieldOverrides[shieldName] ?? {};
4893
4947
  for (const rule of shield.smartRules) {
4894
- if (!existingRuleNames.has(rule.name)) {
4948
+ const collides = rule.name ? existingRuleNames.has(rule.name) : false;
4949
+ if (isCloudMandated) {
4950
+ if (collides) {
4951
+ mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
4952
+ }
4953
+ mergedPolicy.smartRules.push({ ...rule, pinned: true });
4954
+ } else if (!collides) {
4895
4955
  const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
4896
4956
  mergedPolicy.smartRules.push(
4897
4957
  overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
@@ -4907,7 +4967,27 @@ function getConfig(cwd) {
4907
4967
  for (const rule of ADVISORY_SMART_RULES) {
4908
4968
  if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
4909
4969
  }
4910
- if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
4970
+ const envMode = process.env.NODE9_MODE;
4971
+ if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
4972
+ mergedSettings.mode = envMode;
4973
+ }
4974
+ if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
4975
+ mergedSettings.mode = "standard";
4976
+ }
4977
+ const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
4978
+ if (modeCloudControlled && mergedSettings.mode === "strict") {
4979
+ for (const name of Object.keys(mergedEnvironments)) {
4980
+ if (mergedEnvironments[name]?.requireApproval === false) {
4981
+ const cleaned = { ...mergedEnvironments[name] };
4982
+ delete cleaned.requireApproval;
4983
+ mergedEnvironments[name] = cleaned;
4984
+ }
4985
+ }
4986
+ }
4987
+ if (managedFloorActive) {
4988
+ mergedPolicy.ignoredTools = [...DEFAULT_CONFIG.policy.ignoredTools];
4989
+ mergedPolicy.sandboxPaths = [...DEFAULT_CONFIG.policy.sandboxPaths];
4990
+ }
4911
4991
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
4912
4992
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
4913
4993
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
@@ -4976,7 +5056,7 @@ ${error.replace("Invalid config:\n", "")}
4976
5056
  }
4977
5057
  return sanitized;
4978
5058
  }
4979
- var DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig;
5059
+ var DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, cacheReadFailureLogged;
4980
5060
  var init_config = __esm({
4981
5061
  "src/config/index.ts"() {
4982
5062
  "use strict";
@@ -4985,6 +5065,7 @@ var init_config = __esm({
4985
5065
  init_managed();
4986
5066
  init_build();
4987
5067
  init_trusted_hosts();
5068
+ init_dist();
4988
5069
  DANGEROUS_WORDS = [
4989
5070
  "mkfs",
4990
5071
  // formats/wipes a filesystem partition
@@ -5262,6 +5343,7 @@ var init_config = __esm({
5262
5343
  }
5263
5344
  ];
5264
5345
  cachedConfig = null;
5346
+ cacheReadFailureLogged = false;
5265
5347
  }
5266
5348
  });
5267
5349
 
@@ -5982,6 +6064,18 @@ async function isDaemonReachable(timeoutMs = 500) {
5982
6064
  return false;
5983
6065
  }
5984
6066
  }
6067
+ async function daemonHasInteractiveApprover(timeoutMs = 400) {
6068
+ try {
6069
+ const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/approver`, {
6070
+ signal: AbortSignal.timeout(timeoutMs)
6071
+ });
6072
+ if (!res.ok) return false;
6073
+ const body = await res.json();
6074
+ return body.interactive === true;
6075
+ } catch {
6076
+ return false;
6077
+ }
6078
+ }
5985
6079
  async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent) {
5986
6080
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
5987
6081
  const ctrl = new AbortController();
@@ -6862,6 +6956,12 @@ function isNetworkTool(toolName, args) {
6862
6956
  function notifyActivity(data) {
6863
6957
  return notifyActivitySocket(data);
6864
6958
  }
6959
+ async function hasReachableHumanApprover(opts) {
6960
+ const hasDisplay = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
6961
+ const nativeReachable = !opts.calledFromDaemon && opts.approvers.native !== false && hasDisplay;
6962
+ if (nativeReachable) return true;
6963
+ return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
6964
+ }
6865
6965
  async function authorizeHeadless(toolName, args, meta, options) {
6866
6966
  if (!options?.calledFromDaemon) {
6867
6967
  const actId = randomUUID();
@@ -7176,6 +7276,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7176
7276
  return { approved: true, checkedBy: "local-policy" };
7177
7277
  }
7178
7278
  if (policyResult.decision === "block") {
7279
+ const daemonUp = isDaemonRunning();
7280
+ let humanApproverReachable = false;
7281
+ if (!policyResult.dependsOnStatePredicates?.length && daemonUp && !isTestEnv2) {
7282
+ humanApproverReachable = await hasReachableHumanApprover({
7283
+ approvers,
7284
+ calledFromDaemon: options?.calledFromDaemon
7285
+ });
7286
+ }
7287
+ const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
7288
+ const hardBlock = () => {
7289
+ if (!isManual)
7290
+ appendLocalAudit(
7291
+ toolName,
7292
+ args,
7293
+ "deny",
7294
+ "smart-rule-block",
7295
+ { ...meta, ruleName: policyResult.ruleName },
7296
+ hashAuditArgs
7297
+ );
7298
+ return {
7299
+ approved: false,
7300
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
7301
+ blockedBy: "local-config",
7302
+ blockedByLabel: policyResult.blockedByLabel,
7303
+ ruleHit: policyResult.ruleName,
7304
+ ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
7305
+ ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
7306
+ };
7307
+ };
7179
7308
  if (policyResult.dependsOnStatePredicates?.length) {
7180
7309
  const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
7181
7310
  const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
@@ -7192,7 +7321,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7192
7321
  if (predicatesMet && policyResult.recoveryCommand) {
7193
7322
  statefulRecoveryCommand = policyResult.recoveryCommand;
7194
7323
  }
7195
- } else if (isDaemonRunning() && !isTestEnv2) {
7324
+ } else if (mayDowngrade) {
7196
7325
  if (!isManual)
7197
7326
  appendLocalAudit(
7198
7327
  toolName,
@@ -7214,36 +7343,13 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7214
7343
  };
7215
7344
  }
7216
7345
  } else {
7217
- if (!isManual)
7218
- appendLocalAudit(
7219
- toolName,
7220
- args,
7221
- "deny",
7222
- "smart-rule-block",
7223
- // Include policyResult.ruleName so the [2] Report SHIELDS
7224
- // panel can attribute this block to its specific shield
7225
- // (e.g. `shield:project-jail:block-read-ssh`) via the
7226
- // rule→shield map. checkedBy stays as the generic
7227
- // `smart-rule-block` for backward compat with existing
7228
- // log readers.
7229
- { ...meta, ruleName: policyResult.ruleName },
7230
- hashAuditArgs
7231
- );
7232
- return {
7233
- approved: false,
7234
- reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
7235
- blockedBy: "local-config",
7236
- blockedByLabel: policyResult.blockedByLabel,
7237
- ruleHit: policyResult.ruleName,
7238
- ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
7239
- ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
7240
- };
7346
+ return hardBlock();
7241
7347
  }
7242
7348
  }
7243
7349
  explainableLabel = policyResult.blockedByLabel || "Local Config";
7244
7350
  policyMatchedField = policyResult.matchedField;
7245
7351
  policyMatchedWord = policyResult.matchedWord;
7246
- if (policyResult.ruleName) localSmartRuleMatched = true;
7352
+ if (policyResult.ruleName || policyResult.tier === 7) localSmartRuleMatched = true;
7247
7353
  if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
7248
7354
  else if (policyResult.reason) policyRuleDescription = policyResult.reason;
7249
7355
  riskMetadata = computeRiskMetadata(
@@ -7255,7 +7361,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7255
7361
  policyResult.ruleName
7256
7362
  );
7257
7363
  if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
7258
- const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
7364
+ const persistent = policyResult.ruleName || policyResult.tier === 7 ? null : getPersistentDecision(toolName);
7259
7365
  if (persistent === "allow" && !appPermReview) {
7260
7366
  if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
7261
7367
  return { approved: true, checkedBy: "persistent" };
@@ -15924,6 +16030,9 @@ data: ${JSON.stringify(data)}
15924
16030
  }
15925
16031
  });
15926
16032
  }
16033
+ function hasInteractiveClient() {
16034
+ return [...sseClients].some((c) => c.capabilities.includes("input"));
16035
+ }
15927
16036
  function broadcastForensic(finding) {
15928
16037
  const severity = CRITICAL_FORENSIC_CATEGORIES.has(finding.type) ? "critical" : "warning";
15929
16038
  const event = {
@@ -17154,18 +17263,27 @@ var init_score = __esm({
17154
17263
 
17155
17264
  // src/posture/headline.ts
17156
17265
  function worstFinding(findings) {
17157
- return [...findings].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0];
17266
+ const cmp = (x, y) => x < y ? -1 : x > y ? 1 : 0;
17267
+ return [...findings].sort(
17268
+ (a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || cmp(a.category, b.category) || cmp(a.title, b.title)
17269
+ )[0];
17270
+ }
17271
+ function actionFromFinding(f) {
17272
+ if (!f?.fix) return null;
17273
+ const fix = f.fix.replace(/^fix it now:\s*/i, "");
17274
+ const where = f.detail.length === 0 ? "" : f.detail.length === 1 ? ` \u2014 found: ${f.detail[0]}` : ` \u2014 found: ${f.detail[0]} and ${f.detail.length - 1} more`;
17275
+ return fix + where;
17158
17276
  }
17159
17277
  function deriveHeadline(allFindings) {
17160
17278
  const findings = allFindings.filter(
17161
17279
  (f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix"
17162
17280
  );
17163
17281
  if (findings.length === 0 || findings.every((f) => f.severity === "advisory")) return null;
17164
- const has = (category) => findings.some((f) => f.category === category);
17165
- const secrets = has("Secrets");
17166
- const egressOpen = has("Egress");
17167
- const noIsolation = has("Isolation");
17168
- const gateWeak = has("Approval gate");
17282
+ const has2 = (category) => findings.some((f) => f.category === category);
17283
+ const secrets = has2("Secrets");
17284
+ const egressOpen = has2("Egress");
17285
+ const noIsolation = has2("Isolation");
17286
+ const gateWeak = has2("Approval gate");
17169
17287
  const notWired = findings.some((f) => f.category === "Coverage" && f.severity === "critical");
17170
17288
  const observeOnly = findings.some((f) => f.category === "Coverage" && f.severity === "high");
17171
17289
  let risk;
@@ -17188,13 +17306,16 @@ function deriveHeadline(allFindings) {
17188
17306
  } else if (observeOnly) {
17189
17307
  action = "Switch node9 to enforcing mode \u2014 right now it is only watching, not blocking.";
17190
17308
  } else if (egressOpen) {
17191
- action = "lock egress to an allowlist (node9 can enforce it) \u2014 it closes the exit the exfiltration needs.";
17309
+ const egressFix = actionFromFinding(
17310
+ worstFinding(findings.filter((f) => f.category === "Egress"))
17311
+ );
17312
+ action = egressFix ? `${egressFix} Closing the exit breaks the exfiltration chain.` : "lock egress to an allowlist (node9 can enforce it) \u2014 it closes the exit the exfiltration needs.";
17192
17313
  } else if (secrets) {
17193
- action = "node9 can block reads of sensitive paths (~/.ssh, ~/.aws) in-path.";
17314
+ action = actionFromFinding(worstFinding(findings.filter((f) => f.category === "Secrets"))) ?? "node9 can block reads of sensitive credential files in-path (`node9 shield enable project-jail`).";
17194
17315
  } else if (gateWeak) {
17195
- action = "node9 can enforce destructive-command blocking in-path.";
17316
+ action = actionFromFinding(worstFinding(findings.filter((f) => f.category === "Approval gate"))) ?? "node9 can enforce destructive-command blocking in-path (`node9 shield enable bash-safe`).";
17196
17317
  } else {
17197
- action = worstFinding(findings)?.fix ?? "Review the findings below.";
17318
+ action = actionFromFinding(worstFinding(findings)) ?? "Review the findings below.";
17198
17319
  }
17199
17320
  return { risk, action };
17200
17321
  }
@@ -17733,6 +17854,21 @@ function inventoryMcp(home = os32.homedir()) {
17733
17854
  }
17734
17855
  return out;
17735
17856
  }
17857
+ function inventoryServerKeys(inv) {
17858
+ const keys = /* @__PURE__ */ new Set();
17859
+ for (const e of inv) {
17860
+ if (e.state === "gatewayed") {
17861
+ const i = e.args.indexOf("--upstream");
17862
+ if (i >= 0 && e.args[i + 1]) {
17863
+ keys.add(getServerKey(e.args[i + 1]));
17864
+ }
17865
+ } else if (e.state === "ungoverned") {
17866
+ const cmd = [e.command, ...e.args].map(quoteArg).join(" ");
17867
+ keys.add(getServerKey(cmd));
17868
+ }
17869
+ }
17870
+ return keys;
17871
+ }
17736
17872
  function writeMcpEntry(mcpFile, format, name, entry) {
17737
17873
  const key = format === "toml" ? "mcp_servers" : "mcpServers";
17738
17874
  let root = {};
@@ -17756,6 +17892,7 @@ var init_mcp_wrap = __esm({
17756
17892
  "use strict";
17757
17893
  init_agent_wiring();
17758
17894
  init_mcp_cmd();
17895
+ init_mcp_pin();
17759
17896
  init_mcp_cmd();
17760
17897
  }
17761
17898
  });
@@ -18187,9 +18324,12 @@ function extractManagedConfig(body) {
18187
18324
  return out.mode !== void 0 || out.egress !== void 0 || out.dlp !== void 0 || out.approvers !== void 0 || out.reviewChannel !== void 0 || out.approvalTimeoutMs !== void 0 || out.injectionScan !== void 0 || out.loopDetection !== void 0 || out.skillPinning !== void 0 || out.jailPaths !== void 0 || out.trustedHosts !== void 0 || out.appPermissions !== void 0 ? out : void 0;
18188
18325
  }
18189
18326
  function writeCache2(cache) {
18190
- const dir = path35.dirname(rulesCacheFile());
18191
- if (!fs36.existsSync(dir)) fs36.mkdirSync(dir, { recursive: true });
18192
- fs36.writeFileSync(rulesCacheFile(), JSON.stringify(cache, null, 2) + "\n", "utf-8");
18327
+ const data = JSON.stringify(cache, null, 2) + "\n";
18328
+ atomicWriteSync2(rulesCacheFile(), data, "utf-8");
18329
+ try {
18330
+ atomicWriteSync2(rulesCacheBackupFile(), data, "utf-8");
18331
+ } catch {
18332
+ }
18193
18333
  }
18194
18334
  async function syncOnce() {
18195
18335
  const creds = readCredentials();
@@ -18483,7 +18623,7 @@ function startForensicBroadcast() {
18483
18623
  const recurring = setInterval(() => void tick(), FORENSIC_BROADCAST_INTERVAL_MS);
18484
18624
  recurring.unref();
18485
18625
  }
18486
- var FINDING_TO_SIGNAL3, rulesCacheFile, DEFAULT_API_URL2, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS, syncHealthFile, STALE_MIN_MS, STALE_MAX_MS, STALE_FACTOR, FORENSIC_BROADCAST_INTERVAL_MS, FORENSIC_INITIAL_DELAY_MS, forensicBroadcastOffsets;
18626
+ var FINDING_TO_SIGNAL3, rulesCacheFile, rulesCacheBackupFile, DEFAULT_API_URL2, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS, syncHealthFile, STALE_MIN_MS, STALE_MAX_MS, STALE_FACTOR, FORENSIC_BROADCAST_INTERVAL_MS, FORENSIC_INITIAL_DELAY_MS, forensicBroadcastOffsets;
18487
18627
  var init_sync = __esm({
18488
18628
  "src/daemon/sync.ts"() {
18489
18629
  "use strict";
@@ -18493,6 +18633,7 @@ var init_sync = __esm({
18493
18633
  init_ship();
18494
18634
  init_build2();
18495
18635
  init_mcp_tools();
18636
+ init_state2();
18496
18637
  init_mcp_status();
18497
18638
  init_ship2();
18498
18639
  init_shields();
@@ -18513,6 +18654,7 @@ var init_sync = __esm({
18513
18654
  "long-output-redacted": "longOutputRedactions"
18514
18655
  };
18515
18656
  rulesCacheFile = () => path35.join(os33.homedir(), ".node9", "rules-cache.json");
18657
+ rulesCacheBackupFile = () => path35.join(os33.homedir(), ".node9", "rules-cache.last-good.json");
18516
18658
  DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept/policies/sync";
18517
18659
  DEFAULT_INTERVAL_HOURS = 5;
18518
18660
  MIN_INTERVAL_SECONDS = 15;
@@ -18754,6 +18896,53 @@ var init_audit_shipper = __esm({
18754
18896
  }
18755
18897
  });
18756
18898
 
18899
+ // src/audit/decision.ts
18900
+ function classifyDecision(a, b) {
18901
+ const isRow = !!a && typeof a === "object";
18902
+ const decision = isRow ? a.decision : a;
18903
+ const attribution = isRow ? a.checkedBy ?? a.source : b;
18904
+ const raw = typeof decision === "string" ? decision : String(decision ?? "");
18905
+ const src = typeof attribution === "string" ? attribution.toLowerCase() : "";
18906
+ const d = raw.toLowerCase();
18907
+ if (src && has(src, "observe-mode")) {
18908
+ return { outcome: "observe", label: "Would block", raw };
18909
+ }
18910
+ if (d === "dlp") return { outcome: "info", label: "Finding", raw };
18911
+ if (d === "mcp-discovered") return { outcome: "info", label: "Info", raw };
18912
+ if (d === "allow" || d === "allowed") {
18913
+ if (src === "post-hook") return { outcome: "allow", label: "Ran", raw };
18914
+ if (HUMAN_SOURCES.has(src)) {
18915
+ return { outcome: "allow", label: "Approved", raw };
18916
+ }
18917
+ return { outcome: "allow", label: "Auto-allowed", raw };
18918
+ }
18919
+ if (d === "deny" || d === "auto-deny" || d === "block") {
18920
+ if (TIMEOUT_SOURCES.has(src)) {
18921
+ return { outcome: "deny", label: "Timed out", raw };
18922
+ }
18923
+ if (HUMAN_SOURCES.has(src)) {
18924
+ return { outcome: "deny", label: "Denied", raw };
18925
+ }
18926
+ return { outcome: "deny", label: "Blocked", raw };
18927
+ }
18928
+ if (d === "review" || d === "pending") {
18929
+ return { outcome: "info", label: "Pending", raw };
18930
+ }
18931
+ return { outcome: "unknown", label: raw ? `? ${raw}` : "? (none)", raw };
18932
+ }
18933
+ function decisionTag(view) {
18934
+ return `[${view.label}]`.padEnd(14);
18935
+ }
18936
+ var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
18937
+ var init_decision = __esm({
18938
+ "src/audit/decision.ts"() {
18939
+ "use strict";
18940
+ HUMAN_SOURCES = /* @__PURE__ */ new Set(["daemon", "cloud", "local-decision", "inline-review-approved"]);
18941
+ TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
18942
+ has = (s, needle) => s.includes(needle);
18943
+ }
18944
+ });
18945
+
18757
18946
  // src/daemon/dlp-scanner.ts
18758
18947
  import fs38 from "fs";
18759
18948
  import path37 from "path";
@@ -18993,6 +19182,7 @@ function runMcpReconcile() {
18993
19182
  }
18994
19183
  const baseline = loadBaseline();
18995
19184
  const creds = getCredentials();
19185
+ reconcileStale(inv, creds);
18996
19186
  const fresh = inv.filter((e) => e.state === "ungoverned" && !baseline.has(idKey(e)));
18997
19187
  if (fresh.length === 0) return;
18998
19188
  const wrappedAgents = /* @__PURE__ */ new Set();
@@ -19032,6 +19222,77 @@ function runMcpReconcile() {
19032
19222
  }
19033
19223
  saveBaseline(baseline);
19034
19224
  }
19225
+ function reconcileStale(inv, creds) {
19226
+ let pins;
19227
+ try {
19228
+ pins = readMcpPins();
19229
+ } catch {
19230
+ return;
19231
+ }
19232
+ const serverKeys = Object.keys(pins.servers);
19233
+ if (serverKeys.length === 0) return;
19234
+ const liveKeys = inventoryServerKeys(inv);
19235
+ const now = (/* @__PURE__ */ new Date()).toISOString();
19236
+ let dirty = false;
19237
+ for (const sk of serverKeys) {
19238
+ const pin = pins.servers[sk];
19239
+ if (liveKeys.has(sk)) {
19240
+ if (pin.lastSeen !== now) {
19241
+ pin.lastSeen = now;
19242
+ dirty = true;
19243
+ }
19244
+ } else if (!pin.lastSeen) {
19245
+ pin.lastSeen = pin.pinnedAt;
19246
+ dirty = true;
19247
+ }
19248
+ }
19249
+ const staleDays = getConfig().settings.mcpStaleAfterDays ?? DEFAULT_STALE_DAYS;
19250
+ if (staleDays > 0 && liveKeys.size > 0) {
19251
+ const staleMs = staleDays * 864e5;
19252
+ for (const sk of serverKeys) {
19253
+ const pin = pins.servers[sk];
19254
+ if (liveKeys.has(sk)) continue;
19255
+ const age = Date.now() - Date.parse(pin.lastSeen ?? pin.pinnedAt);
19256
+ if (age >= staleMs) {
19257
+ appendToLog(HOOK_DEBUG_LOG, {
19258
+ event: "mcp-pin-auto-removed",
19259
+ serverKey: sk,
19260
+ label: pin.label,
19261
+ lastSeen: pin.lastSeen,
19262
+ pinnedAt: pin.pinnedAt
19263
+ });
19264
+ if (creds) {
19265
+ try {
19266
+ void auditLocalAllow(
19267
+ `mcp-server:${sk}`,
19268
+ {
19269
+ serverKey: sk,
19270
+ label: pin.label,
19271
+ lastSeen: pin.lastSeen,
19272
+ reason: "stale",
19273
+ staleDays
19274
+ },
19275
+ "mcp-server-removed",
19276
+ creds,
19277
+ { mcpServer: pin.label },
19278
+ void 0,
19279
+ false
19280
+ );
19281
+ } catch {
19282
+ }
19283
+ }
19284
+ delete pins.servers[sk];
19285
+ dirty = true;
19286
+ }
19287
+ }
19288
+ }
19289
+ if (dirty) {
19290
+ try {
19291
+ writeMcpPins(pins);
19292
+ } catch {
19293
+ }
19294
+ }
19295
+ }
19035
19296
  function startMcpReconciler() {
19036
19297
  setImmediate(() => {
19037
19298
  try {
@@ -19053,7 +19314,7 @@ function startMcpReconciler() {
19053
19314
  };
19054
19315
  schedule();
19055
19316
  }
19056
- var BASELINE_FILE2, BASELINE_CAP, DEFAULT_INTERVAL_MIN;
19317
+ var BASELINE_FILE2, BASELINE_CAP, DEFAULT_INTERVAL_MIN, DEFAULT_STALE_DAYS;
19057
19318
  var init_mcp_reconciler = __esm({
19058
19319
  "src/daemon/mcp-reconciler.ts"() {
19059
19320
  "use strict";
@@ -19062,9 +19323,11 @@ var init_mcp_reconciler = __esm({
19062
19323
  init_config();
19063
19324
  init_cloud();
19064
19325
  init_audit();
19326
+ init_mcp_pin();
19065
19327
  BASELINE_FILE2 = path38.join(os36.homedir(), ".node9", "mcp-baseline.json");
19066
19328
  BASELINE_CAP = 500;
19067
19329
  DEFAULT_INTERVAL_MIN = 60;
19330
+ DEFAULT_STALE_DAYS = 7;
19068
19331
  }
19069
19332
  });
19070
19333
 
@@ -19139,20 +19402,87 @@ var init_hook_heal = __esm({
19139
19402
  import fs40 from "fs";
19140
19403
  import path39 from "path";
19141
19404
  import os37 from "os";
19405
+ function capStartupLog(file) {
19406
+ try {
19407
+ if (fs40.statSync(file).size > MAX_STARTUP_LOG_BYTES) fs40.truncateSync(file);
19408
+ } catch {
19409
+ }
19410
+ }
19142
19411
  function openStartupLogFd() {
19143
19412
  try {
19144
19413
  const file = DAEMON_STARTUP_LOG();
19145
19414
  const dir = path39.dirname(file);
19146
19415
  if (!fs40.existsSync(dir)) fs40.mkdirSync(dir, { recursive: true });
19147
- try {
19148
- if (fs40.statSync(file).size > MAX_STARTUP_LOG_BYTES) fs40.truncateSync(file);
19149
- } catch {
19150
- }
19416
+ capStartupLog(file);
19151
19417
  return fs40.openSync(file, "a");
19152
19418
  } catch {
19153
19419
  return void 0;
19154
19420
  }
19155
19421
  }
19422
+ function recordStartupState(outcome, kind, detail) {
19423
+ try {
19424
+ if (outcome === "starting") {
19425
+ const prev = readStartupState();
19426
+ if (prev?.outcome === "starting") {
19427
+ const prevAt = new Date(prev.at).getTime();
19428
+ const age = Math.abs(Date.now() - prevAt);
19429
+ if (!isNaN(prevAt) && age < 24 * 60 * 60 * 1e3) return;
19430
+ }
19431
+ }
19432
+ const file = DAEMON_STARTUP_STATE();
19433
+ const dir = path39.dirname(file);
19434
+ if (!fs40.existsSync(dir)) fs40.mkdirSync(dir, { recursive: true });
19435
+ const state = { outcome, at: (/* @__PURE__ */ new Date()).toISOString() };
19436
+ if (kind) state.kind = kind;
19437
+ if (detail) state.detail = detail.slice(0, MAX_DETAIL);
19438
+ const tmp = `${file}.${process.pid}.tmp`;
19439
+ try {
19440
+ fs40.writeFileSync(tmp, JSON.stringify(state), "utf-8");
19441
+ fs40.renameSync(tmp, file);
19442
+ } catch (err2) {
19443
+ try {
19444
+ fs40.unlinkSync(tmp);
19445
+ } catch {
19446
+ }
19447
+ throw err2;
19448
+ }
19449
+ } catch {
19450
+ }
19451
+ }
19452
+ function readStartupState() {
19453
+ try {
19454
+ const raw = fs40.readFileSync(DAEMON_STARTUP_STATE(), "utf-8");
19455
+ const s = JSON.parse(raw);
19456
+ if (!s || typeof s.outcome !== "string" || typeof s.at !== "string") return null;
19457
+ return s;
19458
+ } catch {
19459
+ return null;
19460
+ }
19461
+ }
19462
+ function readStartupCause(maxAgeMs = 24 * 60 * 60 * 1e3) {
19463
+ const s = readStartupState();
19464
+ if (!s) return null;
19465
+ const at = new Date(s.at);
19466
+ if (isNaN(at.getTime()) || Date.now() - at.getTime() > maxAgeMs) return null;
19467
+ switch (s.outcome) {
19468
+ case "ok":
19469
+ case "ok-elsewhere":
19470
+ return null;
19471
+ case "starting":
19472
+ if (Date.now() - at.getTime() < STARTING_GRACE_MS) return null;
19473
+ return {
19474
+ kind: "did-not-start",
19475
+ detail: "the daemon did not come up \u2014 see ~/.node9/daemon-startup.log and hook-debug.log",
19476
+ at,
19477
+ // `at` is the FIRST attempt of the streak, not the most recent one.
19478
+ label: "start attempts failing since"
19479
+ };
19480
+ case "failed":
19481
+ return { kind: s.kind || "failed", detail: s.detail || "", at, label: "last start attempt" };
19482
+ default:
19483
+ return null;
19484
+ }
19485
+ }
19156
19486
  function logDaemonStartup(kind, detail) {
19157
19487
  try {
19158
19488
  const file = DAEMON_STARTUP_LOG();
@@ -19164,12 +19494,15 @@ function logDaemonStartup(kind, detail) {
19164
19494
  } catch {
19165
19495
  }
19166
19496
  }
19167
- var DAEMON_STARTUP_LOG, MAX_STARTUP_LOG_BYTES;
19497
+ var DAEMON_STARTUP_LOG, MAX_STARTUP_LOG_BYTES, DAEMON_STARTUP_STATE, MAX_DETAIL, STARTING_GRACE_MS;
19168
19498
  var init_startup_log = __esm({
19169
19499
  "src/daemon/startup-log.ts"() {
19170
19500
  "use strict";
19171
19501
  DAEMON_STARTUP_LOG = () => path39.join(os37.homedir(), ".node9", "daemon-startup.log");
19172
19502
  MAX_STARTUP_LOG_BYTES = 256 * 1024;
19503
+ DAEMON_STARTUP_STATE = () => path39.join(os37.homedir(), ".node9", "daemon-startup-state.json");
19504
+ MAX_DETAIL = 200;
19505
+ STARTING_GRACE_MS = 90 * 1e3;
19173
19506
  }
19174
19507
  });
19175
19508
 
@@ -19181,6 +19514,76 @@ import os38 from "os";
19181
19514
  import { randomUUID as randomUUID4 } from "crypto";
19182
19515
  import { spawnSync } from "child_process";
19183
19516
  import chalk6 from "chalk";
19517
+ function buildDaemonReport(allEntries, period, now) {
19518
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
19519
+ let start = new Date(todayStart);
19520
+ if (period === "7d") start.setDate(start.getDate() - 6);
19521
+ else if (period === "30d") start.setDate(start.getDate() - 29);
19522
+ else if (period === "month") start = new Date(now.getFullYear(), now.getMonth(), 1);
19523
+ const entries = allEntries.filter((e) => {
19524
+ if (e.source === "post-hook" || e.source === "response-dlp") return false;
19525
+ return new Date(e.ts) >= start;
19526
+ });
19527
+ const isBlocked = (e) => classifyDecision(e).outcome === "deny";
19528
+ const checkedBy = (e) => typeof e.checkedBy === "string" ? e.checkedBy : void 0;
19529
+ const summary = {
19530
+ total: entries.length,
19531
+ // The inline `startsWith('allow')` this replaces counted every non-allow
19532
+ // row as "blocked" — so DLP findings, MCP-discovery events and, worst, all
19533
+ // the observe-mode "would have blocked" rows inflated the blocked count
19534
+ // with things that were never refusals.
19535
+ allowed: entries.filter((e) => classifyDecision(e).outcome === "allow").length,
19536
+ blocked: entries.filter(isBlocked).length,
19537
+ dlp: entries.filter((e) => checkedBy(e)?.includes("dlp")).length,
19538
+ loops: entries.filter((e) => checkedBy(e) === "loop-detected").length
19539
+ };
19540
+ const dailyMap = /* @__PURE__ */ new Map();
19541
+ if (period === "today") {
19542
+ for (let h = 0; h < 24; h++) {
19543
+ const key = String(h).padStart(2, "0") + ":00";
19544
+ dailyMap.set(key, { date: key, calls: 0, blocked: 0 });
19545
+ }
19546
+ for (const e of entries) {
19547
+ const hour = new Date(e.ts).getHours();
19548
+ const key = String(hour).padStart(2, "0") + ":00";
19549
+ const d = dailyMap.get(key);
19550
+ d.calls++;
19551
+ if (isBlocked(e)) d.blocked++;
19552
+ }
19553
+ } else {
19554
+ for (const e of entries) {
19555
+ const date = e.ts.slice(0, 10);
19556
+ const d = dailyMap.get(date) || { date, calls: 0, blocked: 0 };
19557
+ d.calls++;
19558
+ if (isBlocked(e)) d.blocked++;
19559
+ dailyMap.set(date, d);
19560
+ }
19561
+ }
19562
+ const topToolsMap = /* @__PURE__ */ new Map();
19563
+ const topBlockedMap = /* @__PURE__ */ new Map();
19564
+ for (const e of entries) {
19565
+ const tool = String(e.tool ?? "");
19566
+ topToolsMap.set(tool, (topToolsMap.get(tool) || 0) + 1);
19567
+ if (isBlocked(e)) topBlockedMap.set(tool, (topBlockedMap.get(tool) || 0) + 1);
19568
+ }
19569
+ const top5 = (m) => [...m.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, value]) => ({ name, value }));
19570
+ const agentMap = /* @__PURE__ */ new Map();
19571
+ for (const e of entries) {
19572
+ const key = e.agent || "unknown";
19573
+ const a = agentMap.get(key) ?? { agent: key, total: 0, blocked: 0, dlp: 0 };
19574
+ a.total++;
19575
+ if (isBlocked(e)) a.blocked++;
19576
+ if (checkedBy(e)?.includes("dlp")) a.dlp++;
19577
+ agentMap.set(key, a);
19578
+ }
19579
+ return {
19580
+ summary,
19581
+ daily: [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date)),
19582
+ topTools: top5(topToolsMap),
19583
+ topBlockedTools: top5(topBlockedMap),
19584
+ byAgent: [...agentMap.values()].sort((a, b) => b.total - a.total)
19585
+ };
19586
+ }
19184
19587
  function startDaemon() {
19185
19588
  try {
19186
19589
  startCostSync();
@@ -19195,6 +19598,7 @@ function startDaemon() {
19195
19598
  const stack = err2 instanceof Error ? err2.stack ?? err2.message : String(err2);
19196
19599
  console.error("\n\u{1F6D1} Node9 daemon startup failed:\n" + stack);
19197
19600
  logDaemonStartup("startup-throw", err2 instanceof Error ? err2.message : String(err2));
19601
+ recordStartupState("failed", "startup-throw", err2 instanceof Error ? err2.message : String(err2));
19198
19602
  process.exit(1);
19199
19603
  }
19200
19604
  const internalToken = randomUUID4();
@@ -19567,6 +19971,10 @@ data: ${JSON.stringify(item.data)}
19567
19971
  return res.end(JSON.stringify({ error: "internal" }));
19568
19972
  }
19569
19973
  }
19974
+ if (req.method === "GET" && pathname === "/approver") {
19975
+ res.writeHead(200, { "Content-Type": "application/json" });
19976
+ return res.end(JSON.stringify({ interactive: hasInteractiveClient() }));
19977
+ }
19570
19978
  if (req.method === "GET" && pathname === "/state/check") {
19571
19979
  const predicatesParam = reqUrl.searchParams.get("predicates") ?? "";
19572
19980
  const predicates = predicatesParam.split(",").filter(Boolean);
@@ -19668,75 +20076,8 @@ data: ${JSON.stringify(item.data)}
19668
20076
  return [];
19669
20077
  }
19670
20078
  });
19671
- const now = /* @__PURE__ */ new Date();
19672
- const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
19673
- let start = new Date(todayStart);
19674
- if (period === "7d") start.setDate(start.getDate() - 6);
19675
- else if (period === "30d") start.setDate(start.getDate() - 29);
19676
- else if (period === "month") start = new Date(now.getFullYear(), now.getMonth(), 1);
19677
- const entries = allEntries.filter((e) => {
19678
- if (e.source === "post-hook" || e.source === "response-dlp") return false;
19679
- return new Date(e.ts) >= start;
19680
- });
19681
- const summary = {
19682
- total: entries.length,
19683
- allowed: entries.filter((e) => e.decision && e.decision.startsWith("allow")).length,
19684
- blocked: entries.filter((e) => e.decision && !e.decision.startsWith("allow")).length,
19685
- dlp: entries.filter((e) => e.checkedBy && e.checkedBy.includes("dlp")).length,
19686
- loops: entries.filter((e) => e.checkedBy === "loop-detected").length
19687
- };
19688
- const dailyMap = /* @__PURE__ */ new Map();
19689
- if (period === "today") {
19690
- for (let h = 0; h < 24; h++) {
19691
- const key = String(h).padStart(2, "0") + ":00";
19692
- dailyMap.set(key, { date: key, calls: 0, blocked: 0 });
19693
- }
19694
- for (const e of entries) {
19695
- const hour = new Date(e.ts).getHours();
19696
- const key = String(hour).padStart(2, "0") + ":00";
19697
- const d = dailyMap.get(key);
19698
- d.calls++;
19699
- if (e.decision && !e.decision.startsWith("allow")) d.blocked++;
19700
- }
19701
- } else {
19702
- for (const e of entries) {
19703
- const date = e.ts.slice(0, 10);
19704
- const d = dailyMap.get(date) || { date, calls: 0, blocked: 0 };
19705
- d.calls++;
19706
- if (e.decision && !e.decision.startsWith("allow")) d.blocked++;
19707
- dailyMap.set(date, d);
19708
- }
19709
- }
19710
- const topToolsMap = /* @__PURE__ */ new Map();
19711
- const topBlockedMap = /* @__PURE__ */ new Map();
19712
- for (const e of entries) {
19713
- topToolsMap.set(e.tool, (topToolsMap.get(e.tool) || 0) + 1);
19714
- if (e.decision && !e.decision.startsWith("allow")) {
19715
- topBlockedMap.set(e.tool, (topBlockedMap.get(e.tool) || 0) + 1);
19716
- }
19717
- }
19718
- const topTools = [...topToolsMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, value]) => ({ name, value }));
19719
- const topBlockedTools = [...topBlockedMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, value]) => ({ name, value }));
19720
- const agentMap = /* @__PURE__ */ new Map();
19721
- for (const e of entries) {
19722
- const key = e.agent || "unknown";
19723
- const a = agentMap.get(key) ?? { agent: key, total: 0, blocked: 0, dlp: 0 };
19724
- a.total++;
19725
- if (e.decision && !e.decision.startsWith("allow")) a.blocked++;
19726
- if (e.checkedBy?.includes("dlp")) a.dlp++;
19727
- agentMap.set(key, a);
19728
- }
19729
- const byAgent = [...agentMap.values()].sort((a, b) => b.total - a.total);
19730
20079
  res.writeHead(200, { "Content-Type": "application/json" });
19731
- return res.end(
19732
- JSON.stringify({
19733
- summary,
19734
- daily: [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date)),
19735
- topTools,
19736
- topBlockedTools,
19737
- byAgent
19738
- })
19739
- );
20080
+ return res.end(JSON.stringify(buildDaemonReport(allEntries, period, /* @__PURE__ */ new Date())));
19740
20081
  } catch {
19741
20082
  res.writeHead(500, { "Content-Type": "application/json" });
19742
20083
  return res.end(JSON.stringify({ error: "Failed to parse report" }));
@@ -20039,6 +20380,23 @@ data: ${JSON.stringify(item.data)}
20039
20380
  res.writeHead(404).end();
20040
20381
  });
20041
20382
  setDaemonServer(server);
20383
+ let bindAttempts = 0;
20384
+ const MAX_BIND_ATTEMPTS = 3;
20385
+ function retryListen() {
20386
+ if (++bindAttempts >= MAX_BIND_ATTEMPTS) {
20387
+ logDaemonStartup(
20388
+ "port-unavailable",
20389
+ `:${DAEMON_PORT} is held by something that is not a node9 daemon`
20390
+ );
20391
+ recordStartupState(
20392
+ "failed",
20393
+ "port-unavailable",
20394
+ `:${DAEMON_PORT} is held by another process that is not a node9 daemon \u2014 free the port, then: node9 daemon --background`
20395
+ );
20396
+ return process.exit(0);
20397
+ }
20398
+ server.listen(DAEMON_PORT, DAEMON_HOST);
20399
+ }
20042
20400
  server.on("error", (e) => {
20043
20401
  if (e.code === "EADDRINUSE") {
20044
20402
  try {
@@ -20046,6 +20404,7 @@ data: ${JSON.stringify(item.data)}
20046
20404
  const { pid } = JSON.parse(fs41.readFileSync(DAEMON_PID_FILE, "utf-8"));
20047
20405
  process.kill(pid, 0);
20048
20406
  logDaemonStartup("port-in-use", `another daemon (pid ${pid}) owns :${DAEMON_PORT}`);
20407
+ recordStartupState("ok-elsewhere");
20049
20408
  return process.exit(0);
20050
20409
  }
20051
20410
  } catch {
@@ -20053,13 +20412,14 @@ data: ${JSON.stringify(item.data)}
20053
20412
  fs41.unlinkSync(DAEMON_PID_FILE);
20054
20413
  } catch {
20055
20414
  }
20056
- server.listen(DAEMON_PORT, DAEMON_HOST);
20415
+ retryListen();
20057
20416
  return;
20058
20417
  }
20059
20418
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/settings`, {
20060
20419
  signal: AbortSignal.timeout(1e3)
20061
20420
  }).then((res) => {
20062
20421
  if (res.ok) {
20422
+ let adopted = false;
20063
20423
  try {
20064
20424
  let orphanPid = null;
20065
20425
  const ss = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
@@ -20087,19 +20447,38 @@ data: ${JSON.stringify(item.data)}
20087
20447
  JSON.stringify({ pid: orphanPid, port: DAEMON_PORT, internalToken, autoStarted }),
20088
20448
  { mode: 384 }
20089
20449
  );
20450
+ adopted = true;
20090
20451
  }
20091
20452
  } catch {
20092
20453
  }
20454
+ if (adopted) {
20455
+ logDaemonStartup(
20456
+ "port-in-use-orphan",
20457
+ `adopted the daemon already on :${DAEMON_PORT}`
20458
+ );
20459
+ recordStartupState("ok-elsewhere");
20460
+ } else {
20461
+ logDaemonStartup(
20462
+ "orphan-unidentified",
20463
+ `healthy daemon on :${DAEMON_PORT} could not be identified \u2014 no pid file written`
20464
+ );
20465
+ recordStartupState(
20466
+ "failed",
20467
+ "orphan-unidentified",
20468
+ `a healthy daemon is running on :${DAEMON_PORT} but its process could not be identified, so node9 cannot track it \u2014 install \`ss\` or \`lsof\`, or restart it with: node9 daemon --background`
20469
+ );
20470
+ }
20093
20471
  process.exit(0);
20094
20472
  } else {
20095
- server.listen(DAEMON_PORT, DAEMON_HOST);
20473
+ retryListen();
20096
20474
  }
20097
20475
  }).catch(() => {
20098
- server.listen(DAEMON_PORT, DAEMON_HOST);
20476
+ retryListen();
20099
20477
  });
20100
20478
  return;
20101
20479
  }
20102
20480
  logDaemonStartup("bind-failed", e.message);
20481
+ recordStartupState("failed", "bind-failed", e.message);
20103
20482
  console.error(chalk6.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
20104
20483
  process.exit(1);
20105
20484
  });
@@ -20117,6 +20496,8 @@ data: ${JSON.stringify(item.data)}
20117
20496
  { mode: 384 }
20118
20497
  );
20119
20498
  console.error(chalk6.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE on 127.0.0.1:${DAEMON_PORT}`));
20499
+ logDaemonStartup("ok", `listening on ${DAEMON_HOST}:${DAEMON_PORT}`);
20500
+ recordStartupState("ok");
20120
20501
  });
20121
20502
  if (watchMode) {
20122
20503
  console.error(chalk6.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
@@ -20134,6 +20515,7 @@ var init_server = __esm({
20134
20515
  init_costSync();
20135
20516
  init_sync();
20136
20517
  init_audit_shipper();
20518
+ init_decision();
20137
20519
  init_dlp_scanner();
20138
20520
  init_mcp_reconciler();
20139
20521
  init_hook_heal();
@@ -45316,7 +45698,7 @@ __export(tail_exports, {
45316
45698
  });
45317
45699
  import http5 from "http";
45318
45700
  import chalk40 from "chalk";
45319
- import fs71 from "fs";
45701
+ import fs72 from "fs";
45320
45702
  import os61 from "os";
45321
45703
  import path68 from "path";
45322
45704
  import readline6 from "readline";
@@ -45343,19 +45725,19 @@ function getModelContextLimit(model) {
45343
45725
  }
45344
45726
  function readSessionUsage() {
45345
45727
  const projectsDir = path68.join(os61.homedir(), ".claude", "projects");
45346
- if (!fs71.existsSync(projectsDir)) return null;
45728
+ if (!fs72.existsSync(projectsDir)) return null;
45347
45729
  let latestFile = null;
45348
45730
  let latestMtime = 0;
45349
45731
  try {
45350
- for (const dir of fs71.readdirSync(projectsDir)) {
45732
+ for (const dir of fs72.readdirSync(projectsDir)) {
45351
45733
  const dirPath = path68.join(projectsDir, dir);
45352
45734
  try {
45353
- if (!fs71.statSync(dirPath).isDirectory()) continue;
45354
- for (const file of fs71.readdirSync(dirPath)) {
45735
+ if (!fs72.statSync(dirPath).isDirectory()) continue;
45736
+ for (const file of fs72.readdirSync(dirPath)) {
45355
45737
  if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
45356
45738
  const filePath = path68.join(dirPath, file);
45357
45739
  try {
45358
- const mtime = fs71.statSync(filePath).mtimeMs;
45740
+ const mtime = fs72.statSync(filePath).mtimeMs;
45359
45741
  if (mtime > latestMtime) {
45360
45742
  latestMtime = mtime;
45361
45743
  latestFile = filePath;
@@ -45370,7 +45752,7 @@ function readSessionUsage() {
45370
45752
  }
45371
45753
  if (!latestFile) return null;
45372
45754
  try {
45373
- const lines = fs71.readFileSync(latestFile, "utf-8").split("\n");
45755
+ const lines = fs72.readFileSync(latestFile, "utf-8").split("\n");
45374
45756
  let lastModel = "";
45375
45757
  let lastInput = 0;
45376
45758
  let lastOutput = 0;
@@ -45470,9 +45852,9 @@ function renderPending(activity) {
45470
45852
  }
45471
45853
  async function ensureDaemon() {
45472
45854
  let pidPort = null;
45473
- if (fs71.existsSync(PID_FILE)) {
45855
+ if (fs72.existsSync(PID_FILE)) {
45474
45856
  try {
45475
- const { port } = JSON.parse(fs71.readFileSync(PID_FILE, "utf-8"));
45857
+ const { port } = JSON.parse(fs72.readFileSync(PID_FILE, "utf-8"));
45476
45858
  pidPort = port;
45477
45859
  } catch {
45478
45860
  console.error(chalk40.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
@@ -45487,12 +45869,21 @@ async function ensureDaemon() {
45487
45869
  } catch {
45488
45870
  }
45489
45871
  console.log(chalk40.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
45872
+ const startupFd = openStartupLogFd();
45873
+ recordStartupState("starting");
45490
45874
  const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
45491
45875
  detached: true,
45492
- stdio: "ignore",
45876
+ stdio: ["ignore", "ignore", startupFd ?? "ignore"],
45493
45877
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
45494
45878
  });
45879
+ child.on("error", (err2) => recordStartupState("failed", "spawn-failed", err2.message));
45495
45880
  child.unref();
45881
+ if (startupFd !== void 0) {
45882
+ try {
45883
+ fs72.closeSync(startupFd);
45884
+ } catch {
45885
+ }
45886
+ }
45496
45887
  for (let i = 0; i < 20; i++) {
45497
45888
  await new Promise((r) => setTimeout(r, 250));
45498
45889
  try {
@@ -45630,7 +46021,7 @@ function buildRecoveryCardLines(req) {
45630
46021
  function readApproversFromDisk() {
45631
46022
  const configPath = path68.join(os61.homedir(), ".node9", "config.json");
45632
46023
  try {
45633
- const raw = JSON.parse(fs71.readFileSync(configPath, "utf-8"));
46024
+ const raw = JSON.parse(fs72.readFileSync(configPath, "utf-8"));
45634
46025
  const settings = raw.settings ?? {};
45635
46026
  return settings.approvers ?? {};
45636
46027
  } catch {
@@ -45648,13 +46039,13 @@ function approverStatusLine() {
45648
46039
  function toggleApprover(channel) {
45649
46040
  const configPath = path68.join(os61.homedir(), ".node9", "config.json");
45650
46041
  try {
45651
- const raw = JSON.parse(fs71.readFileSync(configPath, "utf-8"));
46042
+ const raw = JSON.parse(fs72.readFileSync(configPath, "utf-8"));
45652
46043
  const settings = raw.settings ?? {};
45653
46044
  const approvers = settings.approvers ?? {};
45654
46045
  approvers[channel] = approvers[channel] === false;
45655
46046
  settings.approvers = approvers;
45656
46047
  raw.settings = settings;
45657
- fs71.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
46048
+ fs72.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
45658
46049
  } catch (err2) {
45659
46050
  process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
45660
46051
  `);
@@ -45826,7 +46217,7 @@ async function startTail(options = {}) {
45826
46217
  }
45827
46218
  postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).catch((err2) => {
45828
46219
  try {
45829
- fs71.appendFileSync(
46220
+ fs72.appendFileSync(
45830
46221
  path68.join(os61.homedir(), ".node9", "hook-debug.log"),
45831
46222
  `[tail] POST /decision failed: ${String(err2)}
45832
46223
  `
@@ -45893,7 +46284,7 @@ async function startTail(options = {}) {
45893
46284
  }
45894
46285
  const auditLog = path68.join(os61.homedir(), ".node9", "audit.log");
45895
46286
  try {
45896
- const unackedDlp = fs71.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
46287
+ const unackedDlp = fs72.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
45897
46288
  if (unackedDlp > 0) {
45898
46289
  console.log("");
45899
46290
  console.log(
@@ -45933,7 +46324,7 @@ async function startTail(options = {}) {
45933
46324
  if (stallWarned) return;
45934
46325
  if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
45935
46326
  try {
45936
- const auditMtime = fs71.statSync(auditLog).mtimeMs;
46327
+ const auditMtime = fs72.statSync(auditLog).mtimeMs;
45937
46328
  if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
45938
46329
  console.log("");
45939
46330
  console.log(
@@ -46122,6 +46513,7 @@ var PID_FILE, ICONS, MODEL_CONTEXT_LIMITS, RESET2, BOLD2, RED, YELLOW, CYAN, GRA
46122
46513
  var init_tail = __esm({
46123
46514
  "src/tui/tail.ts"() {
46124
46515
  "use strict";
46516
+ init_startup_log();
46125
46517
  init_daemon2();
46126
46518
  init_daemon();
46127
46519
  PID_FILE = path68.join(os61.homedir(), ".node9", "daemon.pid");
@@ -46172,7 +46564,7 @@ __export(hud_exports, {
46172
46564
  main: () => main,
46173
46565
  renderEnvironmentLine: () => renderEnvironmentLine
46174
46566
  });
46175
- import fs72 from "fs";
46567
+ import fs73 from "fs";
46176
46568
  import path69 from "path";
46177
46569
  import os62 from "os";
46178
46570
  import http6 from "http";
@@ -46250,9 +46642,9 @@ function formatTimeLeft(resetsAt) {
46250
46642
  return ` (${m}m left)`;
46251
46643
  }
46252
46644
  function safeReadJson(filePath) {
46253
- if (!fs72.existsSync(filePath)) return null;
46645
+ if (!fs73.existsSync(filePath)) return null;
46254
46646
  try {
46255
- return JSON.parse(fs72.readFileSync(filePath, "utf-8"));
46647
+ return JSON.parse(fs73.readFileSync(filePath, "utf-8"));
46256
46648
  } catch {
46257
46649
  return null;
46258
46650
  }
@@ -46273,10 +46665,10 @@ function countHooksInFile(filePath) {
46273
46665
  return Object.keys(cfg.hooks).length;
46274
46666
  }
46275
46667
  function countRulesInDir(rulesDir) {
46276
- if (!fs72.existsSync(rulesDir)) return 0;
46668
+ if (!fs73.existsSync(rulesDir)) return 0;
46277
46669
  let count = 0;
46278
46670
  try {
46279
- for (const entry of fs72.readdirSync(rulesDir, { withFileTypes: true })) {
46671
+ for (const entry of fs73.readdirSync(rulesDir, { withFileTypes: true })) {
46280
46672
  if (entry.isDirectory()) {
46281
46673
  count += countRulesInDir(path69.join(rulesDir, entry.name));
46282
46674
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -46302,7 +46694,7 @@ function countConfigs(cwd) {
46302
46694
  let hooksCount = 0;
46303
46695
  const userMcpServers = /* @__PURE__ */ new Set();
46304
46696
  const projectMcpServers = /* @__PURE__ */ new Set();
46305
- if (fs72.existsSync(path69.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
46697
+ if (fs73.existsSync(path69.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
46306
46698
  rulesCount += countRulesInDir(path69.join(claudeDir, "rules"));
46307
46699
  const userSettings = path69.join(claudeDir, "settings.json");
46308
46700
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
@@ -46313,18 +46705,18 @@ function countConfigs(cwd) {
46313
46705
  userMcpServers.delete(name);
46314
46706
  }
46315
46707
  if (cwd) {
46316
- if (fs72.existsSync(path69.join(cwd, "CLAUDE.md"))) claudeMdCount++;
46317
- if (fs72.existsSync(path69.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
46708
+ if (fs73.existsSync(path69.join(cwd, "CLAUDE.md"))) claudeMdCount++;
46709
+ if (fs73.existsSync(path69.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
46318
46710
  const projectClaudeDir = path69.join(cwd, ".claude");
46319
46711
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
46320
46712
  if (!overlapsUserScope) {
46321
- if (fs72.existsSync(path69.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
46713
+ if (fs73.existsSync(path69.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
46322
46714
  rulesCount += countRulesInDir(path69.join(projectClaudeDir, "rules"));
46323
46715
  const projSettings = path69.join(projectClaudeDir, "settings.json");
46324
46716
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
46325
46717
  hooksCount += countHooksInFile(projSettings);
46326
46718
  }
46327
- if (fs72.existsSync(path69.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
46719
+ if (fs73.existsSync(path69.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
46328
46720
  const localSettings = path69.join(projectClaudeDir, "settings.local.json");
46329
46721
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
46330
46722
  hooksCount += countHooksInFile(localSettings);
@@ -46362,11 +46754,11 @@ function readActiveShieldsHud() {
46362
46754
  }
46363
46755
  try {
46364
46756
  const shieldsPath = path69.join(os62.homedir(), ".node9", "shields.json");
46365
- if (!fs72.existsSync(shieldsPath)) {
46757
+ if (!fs73.existsSync(shieldsPath)) {
46366
46758
  shieldsCache = { value: [], ts: now };
46367
46759
  return [];
46368
46760
  }
46369
- const parsed = JSON.parse(fs72.readFileSync(shieldsPath, "utf-8"));
46761
+ const parsed = JSON.parse(fs73.readFileSync(shieldsPath, "utf-8"));
46370
46762
  if (!Array.isArray(parsed.active)) {
46371
46763
  shieldsCache = { value: [], ts: now };
46372
46764
  return [];
@@ -46468,17 +46860,17 @@ function renderContextLine(stdin) {
46468
46860
  async function main() {
46469
46861
  try {
46470
46862
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
46471
- if (fs72.existsSync(path69.join(os62.homedir(), ".node9", "hud-debug"))) {
46863
+ if (fs73.existsSync(path69.join(os62.homedir(), ".node9", "hud-debug"))) {
46472
46864
  try {
46473
46865
  const logPath = path69.join(os62.homedir(), ".node9", "hud-debug.log");
46474
46866
  const MAX_LOG_SIZE = 10 * 1024 * 1024;
46475
46867
  let size = 0;
46476
46868
  try {
46477
- size = fs72.statSync(logPath).size;
46869
+ size = fs73.statSync(logPath).size;
46478
46870
  } catch {
46479
46871
  }
46480
46872
  if (size < MAX_LOG_SIZE) {
46481
- fs72.appendFileSync(
46873
+ fs73.appendFileSync(
46482
46874
  logPath,
46483
46875
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
46484
46876
  );
@@ -46502,8 +46894,8 @@ async function main() {
46502
46894
  path69.join(cwd, "node9.config.json"),
46503
46895
  path69.join(os62.homedir(), ".node9", "config.json")
46504
46896
  ]) {
46505
- if (!fs72.existsSync(configPath)) continue;
46506
- const cfg = JSON.parse(fs72.readFileSync(configPath, "utf-8"));
46897
+ if (!fs73.existsSync(configPath)) continue;
46898
+ const cfg = JSON.parse(fs73.readFileSync(configPath, "utf-8"));
46507
46899
  const hud = cfg.settings?.hud;
46508
46900
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
46509
46901
  }
@@ -46645,7 +47037,7 @@ function writeCredentialsAndConfig(apiKey, opts = {}) {
46645
47037
  // src/cli.ts
46646
47038
  init_daemon2();
46647
47039
  import chalk41 from "chalk";
46648
- import fs73 from "fs";
47040
+ import fs74 from "fs";
46649
47041
  import path70 from "path";
46650
47042
  import os63 from "os";
46651
47043
  import { spawn as spawn9 } from "child_process";
@@ -46851,9 +47243,11 @@ function logAutostartSkipThrottled(reason) {
46851
47243
  if (Date.now() - fs44.statSync(stamp).mtimeMs < SKIP_THROTTLE_MS) return;
46852
47244
  } catch {
46853
47245
  }
47246
+ const dir = path42.join(os40.homedir(), ".node9");
47247
+ if (!fs44.existsSync(dir)) fs44.mkdirSync(dir, { recursive: true });
46854
47248
  fs44.writeFileSync(stamp, "", "utf-8");
46855
47249
  fs44.appendFileSync(
46856
- path42.join(os40.homedir(), ".node9", "hook-debug.log"),
47250
+ path42.join(dir, "hook-debug.log"),
46857
47251
  `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-skip: ${reason}
46858
47252
  `,
46859
47253
  "utf-8"
@@ -46872,6 +47266,8 @@ async function autoStartDaemonAndWait() {
46872
47266
  }
46873
47267
  if (!resolvedArgv1.endsWith(".js")) return false;
46874
47268
  const startupFd = openStartupLogFd();
47269
+ recordStartupState("starting");
47270
+ let spawned = false;
46875
47271
  try {
46876
47272
  const child = spawn3(process.execPath, [resolvedArgv1, "daemon"], {
46877
47273
  detached: true,
@@ -46881,13 +47277,24 @@ async function autoStartDaemonAndWait() {
46881
47277
  NODE9_AUTO_STARTED: "1"
46882
47278
  }
46883
47279
  });
47280
+ child.on("error", (err2) => {
47281
+ if (readStartupState()?.outcome !== "starting") return;
47282
+ recordStartupState("failed", "spawn-failed", err2.message);
47283
+ });
46884
47284
  child.unref();
47285
+ spawned = true;
46885
47286
  for (let i = 0; i < 20; i++) {
46886
47287
  await new Promise((r) => setTimeout(r, 250));
46887
47288
  if (!isDaemonRunning()) continue;
46888
47289
  if (await isDaemonReachable()) return true;
46889
47290
  }
46890
- } catch {
47291
+ } catch (err2) {
47292
+ if (!spawned)
47293
+ recordStartupState(
47294
+ "failed",
47295
+ "spawn-failed",
47296
+ err2 instanceof Error ? err2.message : String(err2)
47297
+ );
46891
47298
  } finally {
46892
47299
  if (startupFd !== void 0) {
46893
47300
  try {
@@ -47700,12 +48107,16 @@ RAW: ${raw}
47700
48107
  delete safeEnv[key];
47701
48108
  }
47702
48109
  const startupFd = openStartupLogFd();
48110
+ recordStartupState("starting");
47703
48111
  try {
47704
48112
  const d = spawn5(process.execPath, [scriptPath, "daemon"], {
47705
48113
  detached: true,
47706
48114
  stdio: ["ignore", "ignore", startupFd ?? "ignore"],
47707
48115
  env: { ...safeEnv, NODE9_AUTO_STARTED: "1" }
47708
48116
  });
48117
+ d.on("error", (err2) => {
48118
+ recordStartupState("failed", "spawn-failed", err2.message);
48119
+ });
47709
48120
  d.unref();
47710
48121
  } finally {
47711
48122
  if (startupFd !== void 0) {
@@ -47718,6 +48129,7 @@ RAW: ${raw}
47718
48129
  } catch (spawnErr) {
47719
48130
  const logPath = path46.join(os44.homedir(), ".node9", "hook-debug.log");
47720
48131
  const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
48132
+ recordStartupState("failed", "spawn-aborted", msg);
47721
48133
  try {
47722
48134
  fs48.appendFileSync(
47723
48135
  logPath,
@@ -48882,6 +49294,7 @@ function agoLabel(iso, now = Date.now()) {
48882
49294
  }
48883
49295
 
48884
49296
  // src/cli/commands/doctor.ts
49297
+ init_startup_log();
48885
49298
  function registerDoctorCommand(program2, version2) {
48886
49299
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(async () => {
48887
49300
  const homeDir2 = os47.homedir();
@@ -48993,6 +49406,15 @@ function registerDoctorCommand(program2, version2) {
48993
49406
  "Daemon not running \u2014 terminal & native approvals unavailable",
48994
49407
  "Run: node9 daemon --background"
48995
49408
  );
49409
+ const cause = readStartupCause();
49410
+ if (cause) {
49411
+ const suffix = cause.detail ? ` \u2014 ${cause.detail}` : "";
49412
+ console.log(
49413
+ chalk11.gray(
49414
+ ` ${cause.label} ${agoLabel(cause.at.toISOString())}: ${cause.kind}${suffix}`
49415
+ )
49416
+ );
49417
+ }
48996
49418
  }
48997
49419
  const autostart = autostartAdvice({
48998
49420
  installed: isDaemonServiceInstalled(),
@@ -49067,6 +49489,7 @@ function registerDoctorCommand(program2, version2) {
49067
49489
  }
49068
49490
 
49069
49491
  // src/cli/commands/audit.ts
49492
+ init_decision();
49070
49493
  import chalk12 from "chalk";
49071
49494
  import fs53 from "fs";
49072
49495
  import path51 from "path";
@@ -49101,10 +49524,16 @@ function registerAuditCommand(program2) {
49101
49524
  });
49102
49525
  entries = entries.map((e) => ({
49103
49526
  ...e,
49104
- decision: String(e.decision).startsWith("allow") ? "allow" : "deny"
49527
+ // classifyDecision is the ONE mapper (audit/decision.ts). The inline
49528
+ // `startsWith('allow') ? allow : deny` this replaces bucketed `dlp` and
49529
+ // `mcp-discovered` — findings, not verdicts — as DENY, inventing
49530
+ // refusals that never happened.
49531
+ // Pass the ROW, never a field pair — the attribution key differs by
49532
+ // producer (`checkedBy` from the gate, `source` from the hook/daemon).
49533
+ view: classifyDecision(e)
49105
49534
  }));
49106
49535
  if (options.tool) entries = entries.filter((e) => String(e.tool).includes(options.tool));
49107
- if (options.deny) entries = entries.filter((e) => e.decision === "deny");
49536
+ if (options.deny) entries = entries.filter((e) => e.view.outcome === "deny");
49108
49537
  const limit = Math.max(1, parseInt(options.tail, 10) || 20);
49109
49538
  entries = entries.slice(-limit);
49110
49539
  if (options.json) {
@@ -49127,13 +49556,13 @@ function registerAuditCommand(program2) {
49127
49556
  for (const e of entries) {
49128
49557
  const time = formatRelativeTime(String(e.ts)).padEnd(12);
49129
49558
  const tool = String(e.tool).slice(0, 17).padEnd(18);
49130
- const result = e.decision === "allow" ? chalk12.green("ALLOW".padEnd(10)) : chalk12.red("DENY".padEnd(10));
49559
+ const result = e.view.outcome === "allow" ? chalk12.green(e.view.label.padEnd(14)) : e.view.outcome === "deny" ? chalk12.red(e.view.label.padEnd(14)) : e.view.outcome === "observe" ? chalk12.yellow(e.view.label.padEnd(14)) : chalk12.gray(e.view.label.padEnd(14));
49131
49560
  const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
49132
49561
  const agent = String(e.agent || "unknown");
49133
49562
  console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
49134
49563
  }
49135
- const allowed = entries.filter((e) => e.decision === "allow").length;
49136
- const denied = entries.filter((e) => e.decision === "deny").length;
49564
+ const allowed = entries.filter((e) => e.view.outcome === "allow").length;
49565
+ const denied = entries.filter((e) => e.view.outcome === "deny").length;
49137
49566
  console.log(chalk12.dim(" " + "\u2500".repeat(65)));
49138
49567
  console.log(
49139
49568
  ` ${entries.length} entries | ${chalk12.green(allowed + " allowed")} | ${chalk12.red(denied + " denied")}
@@ -49149,6 +49578,7 @@ import chalk13 from "chalk";
49149
49578
  init_costSync();
49150
49579
  init_litellm();
49151
49580
  init_cost_codex();
49581
+ init_decision();
49152
49582
  import fs54 from "fs";
49153
49583
  import os49 from "os";
49154
49584
  import path52 from "path";
@@ -49242,8 +49672,15 @@ function parseAuditLog(logPath) {
49242
49672
  }
49243
49673
  });
49244
49674
  }
49245
- function isAllow(decision) {
49246
- return decision.startsWith("allow");
49675
+ function viewOf(e) {
49676
+ const outcome = classifyDecision(e).outcome;
49677
+ const observed = outcome === "observe";
49678
+ return {
49679
+ observed,
49680
+ ran: outcome === "allow" || observed,
49681
+ blocked: outcome === "deny" || outcome === "unknown",
49682
+ info: outcome === "info"
49683
+ };
49247
49684
  }
49248
49685
  function isDlp(checkedBy) {
49249
49686
  return !!checkedBy?.includes("dlp");
@@ -49656,12 +50093,12 @@ function aggregateReportFromAudit(period, opts = {}) {
49656
50093
  const priorStart = new Date(start.getTime() - periodMs);
49657
50094
  const priorEntries = allEntries.filter((e) => {
49658
50095
  if (e.source === "post-hook") return false;
50096
+ if (e.source === "response-dlp") return false;
50097
+ if (typeof e.decision !== "string") return false;
49659
50098
  const ts = new Date(e.ts);
49660
50099
  return ts >= priorStart && ts <= priorEnd;
49661
50100
  });
49662
- const priorBlocked = priorEntries.filter(
49663
- (e) => typeof e.decision === "string" && !isAllow(e.decision)
49664
- ).length;
50101
+ const priorBlocked = priorEntries.filter((e) => viewOf(e).blocked).length;
49665
50102
  const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
49666
50103
  const excludeTests = opts.excludeTests === true;
49667
50104
  const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
@@ -49704,15 +50141,17 @@ function aggregateReportFromAudit(period, opts = {}) {
49704
50141
  let dimInjectionBlocked = 0;
49705
50142
  for (const e of entries) {
49706
50143
  if (superseded.has(supersedeKey(e))) continue;
49707
- const allow = isAllow(e.decision);
50144
+ const view = viewOf(e);
49708
50145
  const dateKey = e.ts.slice(0, 10);
49709
50146
  const userInteracted = e.source === "daemon";
49710
- if (userInteracted) {
49711
- if (allow) userApproved++;
50147
+ if (view.observed) {
50148
+ if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
50149
+ } else if (view.info) {
50150
+ } else if (userInteracted) {
50151
+ if (view.ran) userApproved++;
49712
50152
  else userDenied++;
49713
- } else if (!allow) {
50153
+ } else if (view.blocked) {
49714
50154
  if (e.checkedBy === "timeout") timedOut++;
49715
- else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
49716
50155
  else if (isDlp(e.checkedBy)) dlpBlocked++;
49717
50156
  else if (e.checkedBy === "local-decision") userDenied++;
49718
50157
  else if (e.checkedBy !== "loop-detected") hardBlocked++;
@@ -49724,7 +50163,7 @@ function aggregateReportFromAudit(period, opts = {}) {
49724
50163
  if (cb.includes("would-block") && (cb.includes("pii") || cb.includes("dlp"))) {
49725
50164
  dimDataObserved++;
49726
50165
  }
49727
- if (!allow && !userInteracted) {
50166
+ if (view.blocked && !userInteracted) {
49728
50167
  switch (dimensionOfBlock(cb, e.ruleName ?? "")) {
49729
50168
  case "network":
49730
50169
  dimNetworkBlocked++;
@@ -49742,15 +50181,15 @@ function aggregateReportFromAudit(period, opts = {}) {
49742
50181
  }
49743
50182
  const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
49744
50183
  t.calls++;
49745
- if (!allow) t.blocked++;
50184
+ if (view.blocked) t.blocked++;
49746
50185
  toolMap.set(e.tool, t);
49747
- if (!allow) {
50186
+ if (view.blocked) {
49748
50187
  const key = e.checkedBy ?? (e.source === "daemon" ? "local-decision" : null);
49749
50188
  if (key) {
49750
50189
  blockMap.set(key, (blockMap.get(key) ?? 0) + 1);
49751
50190
  }
49752
50191
  }
49753
- if (!allow && e.ruleName) {
50192
+ if ((view.blocked || view.observed) && e.ruleName) {
49754
50193
  ruleMap.set(e.ruleName, (ruleMap.get(e.ruleName) ?? 0) + 1);
49755
50194
  }
49756
50195
  if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
@@ -49759,7 +50198,7 @@ function aggregateReportFromAudit(period, opts = {}) {
49759
50198
  hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
49760
50199
  const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
49761
50200
  d.calls++;
49762
- if (!allow) d.blocked++;
50201
+ if (view.blocked) d.blocked++;
49763
50202
  dailyMap.set(dateKey, d);
49764
50203
  }
49765
50204
  for (const e of allEntries) {
@@ -50311,9 +50750,11 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
50311
50750
  }
50312
50751
 
50313
50752
  // src/cli/commands/daemon-cmd.ts
50753
+ init_startup_log();
50314
50754
  init_daemon2();
50315
50755
  import chalk14 from "chalk";
50316
50756
  import { spawn as spawn6 } from "child_process";
50757
+ import fs55 from "fs";
50317
50758
  var VALID_ACTIONS = "start | stop | restart | status | install | uninstall";
50318
50759
  function registerDaemonCommand(program2) {
50319
50760
  program2.command("daemon").description("Manage the local approval daemon").argument("[action]", `${VALID_ACTIONS} (default: start)`).option("-b, --background", "Start the daemon in the background (detached)").option(
@@ -50351,14 +50792,27 @@ function registerDaemonCommand(program2) {
50351
50792
  if (cmd === "restart") {
50352
50793
  stopDaemon();
50353
50794
  await new Promise((r) => setTimeout(r, 500));
50795
+ const restartFd = openStartupLogFd();
50796
+ recordStartupState("starting");
50354
50797
  const child = spawn6(process.execPath, [process.argv[1], "daemon"], {
50355
50798
  detached: true,
50356
- stdio: "ignore",
50799
+ stdio: ["ignore", "ignore", restartFd ?? "ignore"],
50357
50800
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
50358
50801
  });
50802
+ child.on(
50803
+ "error",
50804
+ (err2) => recordStartupState("failed", "spawn-failed", err2.message)
50805
+ );
50359
50806
  child.unref();
50807
+ if (restartFd !== void 0) {
50808
+ try {
50809
+ fs55.closeSync(restartFd);
50810
+ } catch {
50811
+ }
50812
+ }
50360
50813
  if (child.pid) {
50361
- console.log(chalk14.green(`\u2713 Daemon restarted (PID ${child.pid})`));
50814
+ console.log(chalk14.green(`\u2713 Daemon relaunching (PID ${child.pid})`));
50815
+ console.log(chalk14.gray(" Confirm with: node9 status"));
50362
50816
  } else {
50363
50817
  console.error(chalk14.red("\u2717 Failed to restart daemon \u2014 spawn returned no PID"));
50364
50818
  process.exit(1);
@@ -50381,13 +50835,33 @@ function registerDaemonCommand(program2) {
50381
50835
  return;
50382
50836
  }
50383
50837
  if (options.background) {
50384
- const child = spawn6(process.execPath, [process.argv[1], "daemon"], {
50385
- detached: true,
50386
- stdio: "ignore"
50387
- });
50388
- child.unref();
50389
- console.log(chalk14.green(`
50390
- \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
50838
+ const startupFd = openStartupLogFd();
50839
+ try {
50840
+ recordStartupState("starting");
50841
+ const child = spawn6(process.execPath, [process.argv[1], "daemon"], {
50842
+ detached: true,
50843
+ // Capture the child's stderr: a module-load crash prints its stack there
50844
+ // and dies before it can record anything itself.
50845
+ stdio: ["ignore", "ignore", startupFd ?? "ignore"]
50846
+ });
50847
+ child.on(
50848
+ "error",
50849
+ (err2) => recordStartupState("failed", "spawn-failed", err2.message)
50850
+ );
50851
+ child.unref();
50852
+ console.log(
50853
+ chalk14.green(`
50854
+ \u{1F6E1}\uFE0F Node9 daemon launching in background (PID ${child.pid})`)
50855
+ );
50856
+ console.log(chalk14.gray(" Confirm with: node9 status"));
50857
+ } finally {
50858
+ if (startupFd !== void 0) {
50859
+ try {
50860
+ fs55.closeSync(startupFd);
50861
+ } catch {
50862
+ }
50863
+ }
50864
+ }
50391
50865
  process.exit(0);
50392
50866
  }
50393
50867
  startDaemon();
@@ -50402,7 +50876,7 @@ init_agent_wiring();
50402
50876
  init_sync();
50403
50877
  init_service();
50404
50878
  import chalk15 from "chalk";
50405
- import fs55 from "fs";
50879
+ import fs56 from "fs";
50406
50880
  import path53 from "path";
50407
50881
  import os50 from "os";
50408
50882
  function printAgentSection(label2, hookPairs, wrapped) {
@@ -50480,10 +50954,10 @@ function registerStatusCommand(program2) {
50480
50954
  const projectConfig = path53.join(process.cwd(), "node9.config.json");
50481
50955
  const globalConfig = path53.join(os50.homedir(), ".node9", "config.json");
50482
50956
  console.log(
50483
- ` Local: ${fs55.existsSync(projectConfig) ? chalk15.green("Active (node9.config.json)") : chalk15.gray("Not present")}`
50957
+ ` Local: ${fs56.existsSync(projectConfig) ? chalk15.green("Active (node9.config.json)") : chalk15.gray("Not present")}`
50484
50958
  );
50485
50959
  console.log(
50486
- ` Global: ${fs55.existsSync(globalConfig) ? chalk15.green("Active (~/.node9/config.json)") : chalk15.gray("Not present")}`
50960
+ ` Global: ${fs56.existsSync(globalConfig) ? chalk15.green("Active (~/.node9/config.json)") : chalk15.gray("Not present")}`
50487
50961
  );
50488
50962
  if (mergedConfig.policy.sandboxPaths.length > 0) {
50489
50963
  console.log(
@@ -50530,7 +51004,7 @@ init_shields();
50530
51004
  init_service();
50531
51005
  init_core();
50532
51006
  import chalk16 from "chalk";
50533
- import fs56 from "fs";
51007
+ import fs57 from "fs";
50534
51008
  import path54 from "path";
50535
51009
  import os51 from "os";
50536
51010
  import https6 from "https";
@@ -50619,15 +51093,15 @@ function registerInitCommand(program2) {
50619
51093
  console.log("");
50620
51094
  }
50621
51095
  const configPath = path54.join(os51.homedir(), ".node9", "config.json");
50622
- const isFirstInstall = !fs56.existsSync(configPath);
50623
- if (fs56.existsSync(configPath) && !options.force) {
51096
+ const isFirstInstall = !fs57.existsSync(configPath);
51097
+ if (fs57.existsSync(configPath) && !options.force) {
50624
51098
  try {
50625
- const existing = JSON.parse(fs56.readFileSync(configPath, "utf-8"));
51099
+ const existing = JSON.parse(fs57.readFileSync(configPath, "utf-8"));
50626
51100
  const settings = existing.settings ?? {};
50627
51101
  if (settings.mode !== chosenMode) {
50628
51102
  settings.mode = chosenMode;
50629
51103
  existing.settings = settings;
50630
- fs56.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
51104
+ fs57.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
50631
51105
  console.log(chalk16.green(`\u2705 Mode updated: ${chosenMode}`));
50632
51106
  } else {
50633
51107
  console.log(chalk16.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
@@ -50641,8 +51115,8 @@ function registerInitCommand(program2) {
50641
51115
  settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
50642
51116
  };
50643
51117
  const dir = path54.dirname(configPath);
50644
- if (!fs56.existsSync(dir)) fs56.mkdirSync(dir, { recursive: true });
50645
- fs56.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
51118
+ if (!fs57.existsSync(dir)) fs57.mkdirSync(dir, { recursive: true });
51119
+ fs57.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
50646
51120
  console.log(chalk16.green(`\u2705 Config created: ${configPath}`));
50647
51121
  console.log(chalk16.gray(` Mode: ${chosenMode}`));
50648
51122
  }
@@ -50745,11 +51219,11 @@ init_agent_wiring();
50745
51219
  init_setup();
50746
51220
  init_hook_baseline();
50747
51221
  import chalk17 from "chalk";
50748
- import fs57 from "fs";
51222
+ import fs58 from "fs";
50749
51223
  var hasHookSurface = (a) => a.hooks.length > 0;
50750
51224
  function backupForHeal(file) {
50751
51225
  try {
50752
- if (file && fs57.existsSync(file)) fs57.copyFileSync(file, `${file}.node9-heal-bak`);
51226
+ if (file && fs58.existsSync(file)) fs58.copyFileSync(file, `${file}.node9-heal-bak`);
50753
51227
  } catch {
50754
51228
  }
50755
51229
  }
@@ -51709,16 +52183,17 @@ function registerMcpGatewayCommand(program2) {
51709
52183
 
51710
52184
  // src/mcp-server/index.ts
51711
52185
  import readline5 from "readline";
51712
- import fs59 from "fs";
52186
+ import fs60 from "fs";
51713
52187
  import os53 from "os";
51714
52188
  import path57 from "path";
51715
52189
  import { spawnSync as spawnSync4 } from "child_process";
52190
+ init_decision();
51716
52191
  init_core();
51717
52192
  init_daemon();
51718
52193
  init_shields();
51719
52194
 
51720
52195
  // src/auth/egress-config.ts
51721
- import fs58 from "fs";
52196
+ import fs59 from "fs";
51722
52197
  import os52 from "os";
51723
52198
  import path56 from "path";
51724
52199
  var DEFAULT_EGRESS = {
@@ -51734,7 +52209,7 @@ function egressConfigPath() {
51734
52209
  function readEgressRawConfig() {
51735
52210
  let text;
51736
52211
  try {
51737
- text = fs58.readFileSync(egressConfigPath(), "utf8");
52212
+ text = fs59.readFileSync(egressConfigPath(), "utf8");
51738
52213
  } catch (err2) {
51739
52214
  if (err2.code === "ENOENT") return {};
51740
52215
  throw err2;
@@ -51749,8 +52224,8 @@ function readEgressRawConfig() {
51749
52224
  }
51750
52225
  function writeEgressRawConfig(config) {
51751
52226
  const p = egressConfigPath();
51752
- fs58.mkdirSync(path56.dirname(p), { recursive: true });
51753
- fs58.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
52227
+ fs59.mkdirSync(path56.dirname(p), { recursive: true });
52228
+ fs59.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
51754
52229
  }
51755
52230
  function applyEgress(config, change) {
51756
52231
  const policy = config.policy = config.policy ?? {};
@@ -51945,7 +52420,7 @@ var TOOLS = [
51945
52420
  },
51946
52421
  {
51947
52422
  name: "node9_audit_get",
51948
- description: "Read recent entries from the node9 audit log (~/.node9/audit.log). Each entry shows timestamp, tool name, decision (allow/block/review), command/args, and agent. Use this to review what AI actions have been taken recently, especially blocked or reviewed ops.",
52423
+ description: "Read recent entries from the node9 audit log (~/.node9/audit.log). Each entry shows timestamp, outcome, tool name, the command, and the rule that fired. Outcomes use the same words as the dashboard: Auto-allowed, Approved, Ran, Blocked, Denied (a human refused), Timed out (nobody answered), Would block (shadow mode let it through), Finding, Info. Use this to review what AI actions were taken, especially refused ones.",
51949
52424
  inputSchema: {
51950
52425
  type: "object",
51951
52426
  properties: {
@@ -51955,8 +52430,8 @@ var TOOLS = [
51955
52430
  },
51956
52431
  filter: {
51957
52432
  type: "string",
51958
- enum: ["all", "block", "review"],
51959
- description: 'Filter by decision. Omit or use "all" to show every entry.'
52433
+ enum: ["all", "allow", "deny", "observe", "info", "block"],
52434
+ description: 'Filter by outcome. "deny" covers every refusal (rule block, human denial, timeout); "observe" is shadow-mode would-blocks; "block" is an alias for "deny". Omit or use "all" for every entry.'
51960
52435
  }
51961
52436
  },
51962
52437
  required: []
@@ -52138,10 +52613,10 @@ function handleStatus() {
52138
52613
  const projectConfig = path57.join(process.cwd(), "node9.config.json");
52139
52614
  const globalConfig = path57.join(os53.homedir(), ".node9", "config.json");
52140
52615
  lines.push(
52141
- `Project config (node9.config.json): ${fs59.existsSync(projectConfig) ? "present" : "not found"}`
52616
+ `Project config (node9.config.json): ${fs60.existsSync(projectConfig) ? "present" : "not found"}`
52142
52617
  );
52143
52618
  lines.push(
52144
- `Global config (~/.node9/config.json): ${fs59.existsSync(globalConfig) ? "present" : "not found"}`
52619
+ `Global config (~/.node9/config.json): ${fs60.existsSync(globalConfig) ? "present" : "not found"}`
52145
52620
  );
52146
52621
  return lines.join("\n");
52147
52622
  }
@@ -52251,8 +52726,8 @@ var GLOBAL_CONFIG_PATH = path57.join(os53.homedir(), ".node9", "config.json");
52251
52726
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
52252
52727
  function readGlobalConfigRaw() {
52253
52728
  try {
52254
- if (fs59.existsSync(GLOBAL_CONFIG_PATH)) {
52255
- return JSON.parse(fs59.readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
52729
+ if (fs60.existsSync(GLOBAL_CONFIG_PATH)) {
52730
+ return JSON.parse(fs60.readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
52256
52731
  }
52257
52732
  } catch {
52258
52733
  }
@@ -52260,8 +52735,8 @@ function readGlobalConfigRaw() {
52260
52735
  }
52261
52736
  function writeGlobalConfigRaw(data) {
52262
52737
  const dir = path57.dirname(GLOBAL_CONFIG_PATH);
52263
- if (!fs59.existsSync(dir)) fs59.mkdirSync(dir, { recursive: true });
52264
- fs59.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(data, null, 2) + "\n");
52738
+ if (!fs60.existsSync(dir)) fs60.mkdirSync(dir, { recursive: true });
52739
+ fs60.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(data, null, 2) + "\n");
52265
52740
  }
52266
52741
  function handleApproverList() {
52267
52742
  const config = getConfig();
@@ -52306,35 +52781,37 @@ function handleAuditGet(args) {
52306
52781
  const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
52307
52782
  const filter = typeof args.filter === "string" && args.filter !== "all" ? args.filter : null;
52308
52783
  const auditPath = path57.join(os53.homedir(), ".node9", "audit.log");
52309
- if (!fs59.existsSync(auditPath)) return "No audit log found.";
52310
- const rawLines = fs59.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
52784
+ if (!fs60.existsSync(auditPath)) return "No audit log found.";
52785
+ const rawLines = fs60.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
52786
+ const wanted = filter === "block" ? "deny" : filter;
52311
52787
  const parsed = [];
52312
52788
  for (const line of rawLines) {
52313
52789
  try {
52314
52790
  const e = JSON.parse(line);
52315
- const decision = String(e.decision ?? "allow");
52316
- if (filter && decision !== filter) continue;
52791
+ const view = classifyDecision(e);
52792
+ if (wanted && view.outcome !== wanted) continue;
52317
52793
  const argsObj = e.args;
52318
52794
  let detail = "";
52319
52795
  if (argsObj) {
52320
52796
  const cmd = argsObj.command ?? argsObj.file_path ?? argsObj.path ?? argsObj.sql;
52321
- if (typeof cmd === "string" && cmd) {
52322
- detail = cmd.length > 80 ? cmd.slice(0, 80) + "\u2026" : cmd;
52323
- }
52797
+ if (typeof cmd === "string" && cmd) detail = cmd;
52324
52798
  }
52325
- const decisionPad = decision === "block" ? "[BLOCK] " : decision === "review" ? "[review]" : "[allow] ";
52799
+ if (!detail && typeof e.argsPreview === "string") detail = e.argsPreview;
52800
+ detail = detail.replace(/\s+/g, " ").trim();
52801
+ if (detail.length > 80) detail = detail.slice(0, 80) + "\u2026";
52802
+ const why = typeof e.ruleName === "string" && e.ruleName ? ` (${e.ruleName})` : "";
52326
52803
  const toolPad = String(e.tool ?? "").padEnd(20);
52327
- const line2 = `${e.ts} ${decisionPad} ${toolPad} ${detail}`;
52328
- parsed.push({ raw: line, decision, formatted: line2 });
52804
+ const line2 = `${e.ts} ${decisionTag(view)} ${toolPad} ${detail}${why}`;
52805
+ parsed.push({ raw: line, outcome: view.outcome, formatted: line2 });
52329
52806
  } catch {
52330
- parsed.push({ raw: line, decision: "allow", formatted: line });
52807
+ parsed.push({ raw: line, outcome: "unknown", formatted: `[? unparseable] ${line}` });
52331
52808
  }
52332
52809
  }
52333
52810
  const recent = parsed.slice(-limit);
52334
52811
  if (recent.length === 0) {
52335
- return filter ? `No ${filter} entries found in audit log.` : "Audit log is empty.";
52812
+ return filter ? `No ${wanted} entries found in audit log.` : "Audit log is empty.";
52336
52813
  }
52337
- const header = filter ? `Last ${recent.length} ${filter.toUpperCase()} entries:` : `Last ${recent.length} audit entries:`;
52814
+ const header = filter ? `Last ${recent.length} ${String(wanted).toUpperCase()} entries:` : `Last ${recent.length} audit entries:`;
52338
52815
  return `${header}
52339
52816
 
52340
52817
  ${recent.map((e) => e.formatted).join("\n")}`;
@@ -52681,7 +53158,7 @@ function registerTrustCommand(program2) {
52681
53158
  // src/cli/commands/mcp-pin.ts
52682
53159
  init_mcp_pin();
52683
53160
  import chalk24 from "chalk";
52684
- import fs60 from "fs";
53161
+ import fs61 from "fs";
52685
53162
 
52686
53163
  // src/cli/commands/mcp-gateway-cmd.ts
52687
53164
  init_mcp_wrap();
@@ -52877,6 +53354,7 @@ Restart ${[...agents].join(", ")}`) + chalk23.gray(" to activate. Undo any serve
52877
53354
  }
52878
53355
 
52879
53356
  // src/cli/commands/mcp-pin.ts
53357
+ init_mcp_wrap();
52880
53358
  function registerMcpPinCommand(program2) {
52881
53359
  const pinCmd = program2.command("mcp").description("Manage MCP servers \u2014 governance (gateway) + tool-definition pinning");
52882
53360
  registerMcpGatewayCommand2(pinCmd);
@@ -52888,7 +53366,7 @@ function registerMcpPinCommand(program2) {
52888
53366
  let repoCorrupt = false;
52889
53367
  if (found.source === "repo") {
52890
53368
  try {
52891
- const raw = fs60.readFileSync(found.path, "utf-8");
53369
+ const raw = fs61.readFileSync(found.path, "utf-8");
52892
53370
  const parsed = JSON.parse(raw);
52893
53371
  repoEntries = parsed.servers ?? {};
52894
53372
  } catch {
@@ -53002,6 +53480,88 @@ function registerMcpPinCommand(program2) {
53002
53480
  \u{1F513} Cleared ${count} MCP pin(s).`));
53003
53481
  console.log(chalk24.gray(" Next connection to each server will re-pin.\n"));
53004
53482
  });
53483
+ pinCmd.command("forget [serverKey]").option("--stale", "Remove all stale (orphaned) servers at once").description("Remove a server pin that is no longer configured in any agent").action((serverKey, opts) => {
53484
+ if (opts.stale) {
53485
+ forgetAllStale();
53486
+ return;
53487
+ }
53488
+ if (!serverKey) {
53489
+ console.error(
53490
+ chalk24.red("\n\u274C Please provide a server key, or use --stale to remove all orphans.\n")
53491
+ );
53492
+ process.exit(1);
53493
+ }
53494
+ let pins;
53495
+ try {
53496
+ pins = readMcpPins();
53497
+ } catch {
53498
+ console.error(chalk24.red("\n\u274C Pin file is corrupt."));
53499
+ console.error(chalk24.yellow(" Run: node9 mcp pin reset\n"));
53500
+ process.exit(1);
53501
+ }
53502
+ if (!pins.servers[serverKey]) {
53503
+ console.error(chalk24.red(`
53504
+ \u274C No pin found for server key "${serverKey}"
53505
+ `));
53506
+ console.error(`Run ${chalk24.cyan("node9 mcp pin list")} to see pinned servers.
53507
+ `);
53508
+ process.exit(1);
53509
+ }
53510
+ const inv = inventoryMcp();
53511
+ const liveKeys = inventoryServerKeys(inv);
53512
+ if (liveKeys.has(serverKey)) {
53513
+ const agent = "an agent config";
53514
+ console.error(chalk24.red(`
53515
+ \u274C Server "${serverKey}" is still configured in ${agent}.`));
53516
+ console.error(
53517
+ chalk24.yellow(
53518
+ ` Remove it from the agent config first, then run: node9 mcp forget ${serverKey}
53519
+ `
53520
+ )
53521
+ );
53522
+ process.exit(1);
53523
+ }
53524
+ const label2 = pins.servers[serverKey].label;
53525
+ removePin(serverKey);
53526
+ console.log(chalk24.green(`
53527
+ \u2713 Forgot server ${chalk24.cyan(serverKey)}`));
53528
+ console.log(chalk24.gray(` Was: ${label2}`));
53529
+ console.log(chalk24.gray(" Pin removed \u2014 server will no longer appear in the dashboard.\n"));
53530
+ });
53531
+ }
53532
+ function forgetAllStale() {
53533
+ let pins;
53534
+ try {
53535
+ pins = readMcpPins();
53536
+ } catch {
53537
+ console.error(chalk24.red("\n\u274C Pin file is corrupt."));
53538
+ console.error(chalk24.yellow(" Run: node9 mcp pin reset\n"));
53539
+ process.exit(1);
53540
+ }
53541
+ const inv = inventoryMcp();
53542
+ const liveKeys = inventoryServerKeys(inv);
53543
+ if (liveKeys.size === 0) {
53544
+ console.error(chalk24.red("\n\u274C No live MCP servers detected \u2014 refusing to remove every pin."));
53545
+ console.error(
53546
+ chalk24.yellow(
53547
+ " This usually means an agent config is missing or unreadable, not that\n every server is gone. Check your agent configs, or remove one at a\n time: node9 mcp forget <key>\n"
53548
+ )
53549
+ );
53550
+ process.exit(1);
53551
+ }
53552
+ const stale = Object.entries(pins.servers).filter(([sk]) => !liveKeys.has(sk));
53553
+ if (stale.length === 0) {
53554
+ console.log(chalk24.gray("\nNo stale servers to remove.\n"));
53555
+ return;
53556
+ }
53557
+ for (const [sk, pin] of stale) {
53558
+ delete pins.servers[sk];
53559
+ console.log(chalk24.green(` \u2713 ${chalk24.cyan(sk)} ${chalk24.gray(pin.label)}`));
53560
+ }
53561
+ writeMcpPins(pins);
53562
+ console.log(chalk24.green(`
53563
+ Removed ${stale.length} stale server(s).
53564
+ `));
53005
53565
  }
53006
53566
 
53007
53567
  // src/cli/commands/sync.ts
@@ -53246,10 +53806,16 @@ var LABEL_WIDTH = 14;
53246
53806
  function label(category) {
53247
53807
  return chalk27.bold(category.padEnd(Math.max(LABEL_WIDTH, category.length + 1)));
53248
53808
  }
53249
- function renderFinding(f, showWeight = false) {
53809
+ function guardedWhat(f) {
53810
+ if (f.detail.length === 0) return "this";
53811
+ const reads = f.coverageProbe?.kind === "fileRead" ? "reads of " : "";
53812
+ const more = f.detail.length > 1 ? ` and ${f.detail.length - 1} more` : "";
53813
+ return `${reads}${f.detail[0]}${more}`;
53814
+ }
53815
+ function renderFinding(f, showWeight = false, displayLabel = f.category) {
53250
53816
  const lines = [];
53251
53817
  const wt = showWeight && f.scoreWeight ? chalk27.cyan.bold(`+${f.scoreWeight} `) : "";
53252
- lines.push(` ${ICON[f.severity]} ${label(f.category)}${wt}${f.title}`);
53818
+ lines.push(` ${ICON[f.severity]} ${label(displayLabel)}${wt}${f.title}`);
53253
53819
  const indent = " ".repeat(2 + 3 + LABEL_WIDTH);
53254
53820
  const width = 80 - indent.length;
53255
53821
  for (const s of [f.what, f.why, f.who]) {
@@ -53286,11 +53852,11 @@ function renderPosture(result) {
53286
53852
  );
53287
53853
  const headroom = openHeadroom(result.findings);
53288
53854
  if (headroom > 0) {
53289
- lines.push(
53290
- " " + chalk27.gray(
53291
- `${headroom} pts of headroom below \u2014 each layer adds security at some cost to flexibility. You choose which to close.`
53292
- )
53293
- );
53855
+ const openExposures = result.findings.filter(
53856
+ (f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix" && !f.scoreWeight && f.severity !== "advisory"
53857
+ ).length;
53858
+ const note = openExposures > 0 ? `Of the gap to 100: ${headroom} pts is optional hardening you can choose to turn on (the \u{1F512} tier below, each at some cost to flexibility); the rest is the open findings \u2014 the flagged rows below. Fix those first.` : `${headroom} pts of headroom \u2014 optional hardening you can choose to turn on (the \u{1F512} tier below), each at some cost to flexibility.`;
53859
+ for (const l of wrap(note, 76)) lines.push(" " + chalk27.gray(l));
53294
53860
  }
53295
53861
  lines.push("");
53296
53862
  if (result.headline) {
@@ -53303,13 +53869,18 @@ function renderPosture(result) {
53303
53869
  }
53304
53870
  const covered = result.findings.filter((f) => f.coverage?.state === "covered");
53305
53871
  const open = result.findings.filter((f) => f.coverage?.state !== "covered");
53872
+ const collision = new Set(
53873
+ covered.map((f) => f.category).filter((c) => open.some((o) => o.category === c))
53874
+ );
53875
+ const openLabel = (f) => collision.has(f.category) ? `${f.category} (exposed)` : f.category;
53306
53876
  if (covered.length > 0) {
53307
53877
  lines.push(" " + chalk27.green("\u{1F7E2} ON NOW \u2014 node9 is enforcing these (your floor)"));
53308
53878
  for (const f of covered) {
53309
53879
  const gated = f.coverage?.level === "review" ? "approval-gating" : "blocking";
53310
53880
  const via = f.coverage?.via ?? "node9";
53881
+ const lbl = collision.has(f.category) ? `${f.category} (guarded)` : f.category;
53311
53882
  lines.push(
53312
- ` ${chalk27.green("\u2705")} ${label(f.category)}${chalk27.gray(`${via} is ${gated} this`)}`
53883
+ ` ${chalk27.green("\u2705")} ${label(lbl)}${chalk27.gray(`${via} is ${gated} ${guardedWhat(f)}`)}`
53313
53884
  );
53314
53885
  }
53315
53886
  lines.push("");
@@ -53319,17 +53890,17 @@ function renderPosture(result) {
53319
53890
  const osOpen = open.filter((f) => f.owner !== "node9" && !f.node9Reduces);
53320
53891
  if (node9Open.length > 0) {
53321
53892
  lines.push(" " + chalk27.cyan.bold("\u{1F527} node9 can fix these \u2014 run the command"));
53322
- for (const f of node9Open) lines.push(...renderFinding(f, true));
53893
+ for (const f of node9Open) lines.push(...renderFinding(f, true, openLabel(f)));
53323
53894
  }
53324
53895
  if (reduceOpen.length > 0) {
53325
53896
  if (node9Open.length > 0) lines.push("");
53326
53897
  lines.push(" " + chalk27.yellow.bold("\u{1F512} AVAILABLE \u2014 turn on to harden (each has a tradeoff)"));
53327
- for (const f of reduceOpen) lines.push(...renderFinding(f, true));
53898
+ for (const f of reduceOpen) lines.push(...renderFinding(f, true, openLabel(f)));
53328
53899
  }
53329
53900
  if (osOpen.length > 0) {
53330
53901
  if (node9Open.length > 0 || reduceOpen.length > 0) lines.push("");
53331
53902
  lines.push(" " + chalk27.bold("\u{1F9F1} YOUR PART \u2014 node9 can't fix these (OS-level)"));
53332
- for (const f of osOpen) lines.push(...renderFinding(f));
53903
+ for (const f of osOpen) lines.push(...renderFinding(f, false, openLabel(f)));
53333
53904
  }
53334
53905
  for (const cat of result.passedCategories) {
53335
53906
  lines.push(` ${chalk27.green("\u2705")} ${label(cat)}${chalk27.gray("no issues found")}`);
@@ -53389,7 +53960,7 @@ import chalk30 from "chalk";
53389
53960
 
53390
53961
  // src/ci-check/fetch.ts
53391
53962
  var import_undici = __toESM(require_undici());
53392
- import fs61 from "fs";
53963
+ import fs62 from "fs";
53393
53964
  import path58 from "path";
53394
53965
  import { execFileSync as execFileSync2 } from "child_process";
53395
53966
  var cachedGhToken;
@@ -53473,7 +54044,7 @@ function parseRepoUrl(input) {
53473
54044
  function isLocalPath(input) {
53474
54045
  if (input.startsWith(".") || input.startsWith("/") || input.startsWith("~")) return true;
53475
54046
  try {
53476
- return fs61.existsSync(input) && fs61.statSync(input).isDirectory();
54047
+ return fs62.existsSync(input) && fs62.statSync(input).isDirectory();
53477
54048
  } catch {
53478
54049
  return false;
53479
54050
  }
@@ -53590,8 +54161,8 @@ function readLocalTree(dir) {
53590
54161
  const add = (rel) => {
53591
54162
  const abs = path58.join(root, rel);
53592
54163
  try {
53593
- if (fs61.existsSync(abs) && fs61.statSync(abs).isFile()) {
53594
- files.push({ path: rel, content: fs61.readFileSync(abs, "utf8") });
54164
+ if (fs62.existsSync(abs) && fs62.statSync(abs).isFile()) {
54165
+ files.push({ path: rel, content: fs62.readFileSync(abs, "utf8") });
53595
54166
  }
53596
54167
  } catch {
53597
54168
  }
@@ -53611,7 +54182,7 @@ function readLocalTree(dir) {
53611
54182
  dirsVisited++;
53612
54183
  let entries;
53613
54184
  try {
53614
- entries = fs61.readdirSync(path58.join(root, relDir), { withFileTypes: true });
54185
+ entries = fs62.readdirSync(path58.join(root, relDir), { withFileTypes: true });
53615
54186
  } catch {
53616
54187
  return;
53617
54188
  }
@@ -53634,8 +54205,8 @@ function readLocalTree(dir) {
53634
54205
  for (const rel of matches) collect(rel);
53635
54206
  const wfDir = path58.join(root, WORKFLOW_DIR);
53636
54207
  try {
53637
- if (fs61.existsSync(wfDir)) {
53638
- for (const name of fs61.readdirSync(wfDir)) {
54208
+ if (fs62.existsSync(wfDir)) {
54209
+ for (const name of fs62.readdirSync(wfDir)) {
53639
54210
  if (/\.ya?ml$/.test(name)) add(path58.join(WORKFLOW_DIR, name));
53640
54211
  }
53641
54212
  }
@@ -54776,7 +55347,7 @@ import chalk32 from "chalk";
54776
55347
  // src/shields/jail.ts
54777
55348
  init_build();
54778
55349
  init_shields();
54779
- import fs62 from "fs";
55350
+ import fs63 from "fs";
54780
55351
  import os54 from "os";
54781
55352
  import path59 from "path";
54782
55353
  var USER_JAIL_SHIELD = "user-jail";
@@ -54786,7 +55357,7 @@ function jailStorePath() {
54786
55357
  function readJailPaths() {
54787
55358
  let text;
54788
55359
  try {
54789
- text = fs62.readFileSync(jailStorePath(), "utf8");
55360
+ text = fs63.readFileSync(jailStorePath(), "utf8");
54790
55361
  } catch (err2) {
54791
55362
  if (err2.code === "ENOENT") return [];
54792
55363
  throw err2;
@@ -54804,8 +55375,8 @@ function readJailPaths() {
54804
55375
  }
54805
55376
  function writeJailPaths(paths) {
54806
55377
  const p = jailStorePath();
54807
- fs62.mkdirSync(path59.dirname(p), { recursive: true });
54808
- fs62.writeFileSync(p, JSON.stringify({ paths }, null, 2) + "\n", { mode: 384 });
55378
+ fs63.mkdirSync(path59.dirname(p), { recursive: true });
55379
+ fs63.writeFileSync(p, JSON.stringify({ paths }, null, 2) + "\n", { mode: 384 });
54809
55380
  }
54810
55381
  function addJailPath(rawPath, verdict) {
54811
55382
  const norm = rawPath.trim();
@@ -54834,7 +55405,7 @@ function regenerateUserJail(paths) {
54834
55405
  writeActiveShields(active2.filter((s) => s !== USER_JAIL_SHIELD));
54835
55406
  }
54836
55407
  try {
54837
- fs62.rmSync(file, { force: true });
55408
+ fs63.rmSync(file, { force: true });
54838
55409
  } catch {
54839
55410
  }
54840
55411
  return;
@@ -54949,12 +55520,12 @@ function registerJailCommand(program2) {
54949
55520
  // src/cli/commands/sandbox.ts
54950
55521
  init_config();
54951
55522
  import chalk33 from "chalk";
54952
- import fs65 from "fs";
55523
+ import fs66 from "fs";
54953
55524
  import path62 from "path";
54954
55525
  import { spawnSync as spawnSync6 } from "child_process";
54955
55526
 
54956
55527
  // src/sandbox/config.ts
54957
- import fs63 from "fs";
55528
+ import fs64 from "fs";
54958
55529
  import path60 from "path";
54959
55530
  import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
54960
55531
  var SANDBOX_CONFIG_FILE = "node9.sandbox.yaml";
@@ -55032,12 +55603,12 @@ function sandboxConfigPath(cwd = process.cwd()) {
55032
55603
  }
55033
55604
  function loadSandboxConfig(cwd = process.cwd(), fallbackAgent = "claude") {
55034
55605
  const p = sandboxConfigPath(cwd);
55035
- if (!fs63.existsSync(p)) {
55606
+ if (!fs64.existsSync(p)) {
55036
55607
  throw new Error(`sandbox: ${SANDBOX_CONFIG_FILE} not found \u2014 run \`node9 sandbox new\` first.`);
55037
55608
  }
55038
55609
  let raw;
55039
55610
  try {
55040
- raw = parseYaml2(fs63.readFileSync(p, "utf-8"));
55611
+ raw = parseYaml2(fs64.readFileSync(p, "utf-8"));
55041
55612
  } catch (err2) {
55042
55613
  throw new Error(
55043
55614
  `sandbox: ${SANDBOX_CONFIG_FILE} is not valid YAML \u2014 ${err2.message}`
@@ -55096,7 +55667,7 @@ init_templates();
55096
55667
 
55097
55668
  // src/sandbox/runtime.ts
55098
55669
  init_templates();
55099
- import fs64 from "fs";
55670
+ import fs65 from "fs";
55100
55671
  import os55 from "os";
55101
55672
  import path61 from "path";
55102
55673
  import crypto9 from "crypto";
@@ -55123,7 +55694,7 @@ function buildRunArgs(opts) {
55123
55694
  args.push("-v", `${allowlistHostPath}:${ALLOWED_DOMAINS_PATH}:ro`);
55124
55695
  if (config.node9.mountAgentCredentials) {
55125
55696
  const creds = agentCredentialsMount(config.agent);
55126
- if (fs64.existsSync(creds.hostPath)) {
55697
+ if (fs65.existsSync(creds.hostPath)) {
55127
55698
  args.push("-v", `${creds.hostPath}:${creds.target}`);
55128
55699
  }
55129
55700
  }
@@ -55145,16 +55716,16 @@ function sandboxBuildDir(cwd = process.cwd()) {
55145
55716
  }
55146
55717
  function writeBuildContext(cwd, dockerfile, entrypoint) {
55147
55718
  const dir = sandboxBuildDir(cwd);
55148
- fs64.mkdirSync(dir, { recursive: true });
55149
- fs64.writeFileSync(path61.join(dir, "Dockerfile"), dockerfile);
55150
- fs64.writeFileSync(path61.join(dir, "entrypoint.sh"), entrypoint);
55719
+ fs65.mkdirSync(dir, { recursive: true });
55720
+ fs65.writeFileSync(path61.join(dir, "Dockerfile"), dockerfile);
55721
+ fs65.writeFileSync(path61.join(dir, "entrypoint.sh"), entrypoint);
55151
55722
  return dir;
55152
55723
  }
55153
55724
  function writeAllowlist(cwd, hosts) {
55154
55725
  const dir = path61.join(cwd, ".node9", "sandbox");
55155
- fs64.mkdirSync(dir, { recursive: true });
55726
+ fs65.mkdirSync(dir, { recursive: true });
55156
55727
  const p = path61.join(dir, "allowed-domains.txt");
55157
- fs64.writeFileSync(p, hosts.join("\n") + "\n");
55728
+ fs65.writeFileSync(p, hosts.join("\n") + "\n");
55158
55729
  return p;
55159
55730
  }
55160
55731
  function resolveHomePath(p) {
@@ -55163,7 +55734,7 @@ function resolveHomePath(p) {
55163
55734
 
55164
55735
  // src/cli/commands/sandbox.ts
55165
55736
  function seedDataDirConfig(dataDir, sandbox) {
55166
- fs65.mkdirSync(dataDir, { recursive: true });
55737
+ fs66.mkdirSync(dataDir, { recursive: true });
55167
55738
  const configPath = path62.join(dataDir, "config.json");
55168
55739
  const seed = {
55169
55740
  settings: {
@@ -55175,7 +55746,7 @@ function seedDataDirConfig(dataDir, sandbox) {
55175
55746
  }
55176
55747
  }
55177
55748
  };
55178
- fs65.writeFileSync(configPath, JSON.stringify(seed, null, 2), { mode: 384 });
55749
+ fs66.writeFileSync(configPath, JSON.stringify(seed, null, 2), { mode: 384 });
55179
55750
  }
55180
55751
  function registerSandboxCommand(program2, version2) {
55181
55752
  const node9Version2 = pinnedNode9Version(version2);
@@ -55183,13 +55754,13 @@ function registerSandboxCommand(program2, version2) {
55183
55754
  cmd.command("new").description(`Scaffold ${SANDBOX_CONFIG_FILE} in this project`).option("--agent <agent>", "claude (default) or codex", "claude").action((opts) => {
55184
55755
  const agent = opts.agent === "codex" ? "codex" : "claude";
55185
55756
  const p = sandboxConfigPath();
55186
- if (fs65.existsSync(p)) {
55757
+ if (fs66.existsSync(p)) {
55187
55758
  console.log(
55188
55759
  chalk33.yellow(` ${SANDBOX_CONFIG_FILE} already exists \u2014 leaving it untouched.`)
55189
55760
  );
55190
55761
  return;
55191
55762
  }
55192
- fs65.writeFileSync(p, scaffoldSandboxYaml(agent));
55763
+ fs66.writeFileSync(p, scaffoldSandboxYaml(agent));
55193
55764
  console.log(
55194
55765
  chalk33.green(` \u2713 wrote ${SANDBOX_CONFIG_FILE}`) + chalk33.dim(` (agent: ${agent})`)
55195
55766
  );
@@ -55230,7 +55801,7 @@ function registerSandboxCommand(program2, version2) {
55230
55801
  const hash = imageContentHash(dockerfile, entrypoint);
55231
55802
  const image = sandbox.runtime.image;
55232
55803
  const hashFile = path62.join(sandboxBuildDir(cwd), ".image-hash");
55233
- const lastHash = fs65.existsSync(hashFile) ? fs65.readFileSync(hashFile, "utf-8").trim() : "";
55804
+ const lastHash = fs66.existsSync(hashFile) ? fs66.readFileSync(hashFile, "utf-8").trim() : "";
55234
55805
  const imageExists = spawnSync6(sandbox.runtime.engine, ["image", "inspect", image], { stdio: "ignore" }).status === 0;
55235
55806
  const needBuild = sandbox.runtime.rebuild === "always" || !imageExists || sandbox.runtime.rebuild !== "never" && lastHash !== hash;
55236
55807
  if (needBuild) {
@@ -55242,7 +55813,7 @@ function registerSandboxCommand(program2, version2) {
55242
55813
  console.error(chalk33.red(" build failed."));
55243
55814
  process.exit(b.status ?? 1);
55244
55815
  }
55245
- fs65.writeFileSync(hashFile, hash);
55816
+ fs66.writeFileSync(hashFile, hash);
55246
55817
  }
55247
55818
  const dataDir = sandboxDataDir(cwd);
55248
55819
  seedDataDirConfig(dataDir, sandbox);
@@ -55256,7 +55827,7 @@ function registerSandboxCommand(program2, version2) {
55256
55827
  });
55257
55828
  if (sandbox.node9.mountAgentCredentials) {
55258
55829
  const creds = agentCredentialsMount(sandbox.agent);
55259
- if (fs65.existsSync(creds.hostPath)) {
55830
+ if (fs66.existsSync(creds.hostPath)) {
55260
55831
  console.log(chalk33.dim(` mounting ${creds.hostPath} (agent credentials, rw)`));
55261
55832
  } else {
55262
55833
  console.log(
@@ -55273,7 +55844,7 @@ function registerSandboxCommand(program2, version2) {
55273
55844
  });
55274
55845
  cmd.command("tail").description("Stream the sandbox's audit log (host-side)").action(() => {
55275
55846
  const auditPath = path62.join(sandboxDataDir(), "audit.log");
55276
- if (!fs65.existsSync(auditPath)) {
55847
+ if (!fs66.existsSync(auditPath)) {
55277
55848
  console.log(chalk33.dim(" no sandbox audit yet."));
55278
55849
  return;
55279
55850
  }
@@ -55281,11 +55852,11 @@ function registerSandboxCommand(program2, version2) {
55281
55852
  });
55282
55853
  cmd.command("logs").description("Dump the sandbox's audit log").action(() => {
55283
55854
  const auditPath = path62.join(sandboxDataDir(), "audit.log");
55284
- if (!fs65.existsSync(auditPath)) {
55855
+ if (!fs66.existsSync(auditPath)) {
55285
55856
  console.log(chalk33.dim(" no sandbox audit yet."));
55286
55857
  return;
55287
55858
  }
55288
- process.stdout.write(fs65.readFileSync(auditPath, "utf-8"));
55859
+ process.stdout.write(fs66.readFileSync(auditPath, "utf-8"));
55289
55860
  });
55290
55861
  cmd.command("clean").description("Remove the sandbox image, build context, and data").action(() => {
55291
55862
  const cwd = process.cwd();
@@ -55299,18 +55870,19 @@ function registerSandboxCommand(program2, version2) {
55299
55870
  stdio: "ignore"
55300
55871
  });
55301
55872
  }
55302
- fs65.rmSync(path62.join(cwd, ".node9", "sandbox"), { recursive: true, force: true });
55873
+ fs66.rmSync(path62.join(cwd, ".node9", "sandbox"), { recursive: true, force: true });
55303
55874
  console.log(chalk33.green(" \u2713 sandbox image + build + data removed."));
55304
55875
  });
55305
55876
  }
55306
55877
 
55307
55878
  // src/cli/commands/sessions.ts
55879
+ init_decision();
55308
55880
  init_scan_summary();
55309
55881
  init_litellm();
55310
55882
  init_cost_gemini();
55311
55883
  init_cost_codex();
55312
55884
  import chalk34 from "chalk";
55313
- import fs66 from "fs";
55885
+ import fs67 from "fs";
55314
55886
  import path63 from "path";
55315
55887
  import os56 from "os";
55316
55888
  function modelPrice(model) {
@@ -55404,7 +55976,7 @@ function loadAuditEntries(auditPath) {
55404
55976
  const aPath = auditPath ?? path63.join(os56.homedir(), ".node9", "audit.log");
55405
55977
  let raw;
55406
55978
  try {
55407
- raw = fs66.readFileSync(aPath, "utf-8");
55979
+ raw = fs67.readFileSync(aPath, "utf-8");
55408
55980
  } catch {
55409
55981
  return [];
55410
55982
  }
@@ -55414,7 +55986,7 @@ function loadAuditEntries(auditPath) {
55414
55986
  try {
55415
55987
  const e = JSON.parse(line);
55416
55988
  if (!e.ts || !e.tool || !e.decision) continue;
55417
- if (e.decision === "allow" || e.decision === "allowed") continue;
55989
+ if (classifyDecision(e).outcome === "allow") continue;
55418
55990
  entries.push(e);
55419
55991
  } catch {
55420
55992
  }
@@ -55441,7 +56013,7 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
55441
56013
  }
55442
56014
  function buildGeminiSessions(days, allAuditEntries) {
55443
56015
  const tmpDir = path63.join(os56.homedir(), ".gemini", "tmp");
55444
- if (!fs66.existsSync(tmpDir)) return [];
56016
+ if (!fs67.existsSync(tmpDir)) return [];
55445
56017
  const cutoff = days !== null ? (() => {
55446
56018
  const d = /* @__PURE__ */ new Date();
55447
56019
  d.setDate(d.getDate() - days);
@@ -55450,7 +56022,7 @@ function buildGeminiSessions(days, allAuditEntries) {
55450
56022
  })() : null;
55451
56023
  let slugDirs;
55452
56024
  try {
55453
- slugDirs = fs66.readdirSync(tmpDir);
56025
+ slugDirs = fs67.readdirSync(tmpDir);
55454
56026
  } catch {
55455
56027
  return [];
55456
56028
  }
@@ -55458,27 +56030,27 @@ function buildGeminiSessions(days, allAuditEntries) {
55458
56030
  for (const slug2 of slugDirs) {
55459
56031
  const slugPath = path63.join(tmpDir, slug2);
55460
56032
  try {
55461
- if (!fs66.statSync(slugPath).isDirectory()) continue;
56033
+ if (!fs67.statSync(slugPath).isDirectory()) continue;
55462
56034
  } catch {
55463
56035
  continue;
55464
56036
  }
55465
56037
  let projectRoot = path63.join(os56.homedir(), slug2);
55466
56038
  try {
55467
- projectRoot = fs66.readFileSync(path63.join(slugPath, ".project_root"), "utf-8").trim();
56039
+ projectRoot = fs67.readFileSync(path63.join(slugPath, ".project_root"), "utf-8").trim();
55468
56040
  } catch {
55469
56041
  }
55470
56042
  const chatsDir = path63.join(slugPath, "chats");
55471
- if (!fs66.existsSync(chatsDir)) continue;
56043
+ if (!fs67.existsSync(chatsDir)) continue;
55472
56044
  let chatFiles;
55473
56045
  try {
55474
- chatFiles = fs66.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
56046
+ chatFiles = fs67.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
55475
56047
  } catch {
55476
56048
  continue;
55477
56049
  }
55478
56050
  for (const chatFile of chatFiles) {
55479
56051
  let raw;
55480
56052
  try {
55481
- raw = fs66.readFileSync(path63.join(chatsDir, chatFile), "utf-8");
56053
+ raw = fs67.readFileSync(path63.join(chatsDir, chatFile), "utf-8");
55482
56054
  } catch {
55483
56055
  continue;
55484
56056
  }
@@ -55559,7 +56131,7 @@ function buildGeminiSessions(days, allAuditEntries) {
55559
56131
  }
55560
56132
  function buildCodexSessions(days, allAuditEntries) {
55561
56133
  const sessionsBase = path63.join(os56.homedir(), ".codex", "sessions");
55562
- if (!fs66.existsSync(sessionsBase)) return [];
56134
+ if (!fs67.existsSync(sessionsBase)) return [];
55563
56135
  const cutoff = days !== null ? (() => {
55564
56136
  const d = /* @__PURE__ */ new Date();
55565
56137
  d.setDate(d.getDate() - days);
@@ -55568,28 +56140,28 @@ function buildCodexSessions(days, allAuditEntries) {
55568
56140
  })() : null;
55569
56141
  const jsonlFiles = [];
55570
56142
  try {
55571
- for (const year of fs66.readdirSync(sessionsBase)) {
56143
+ for (const year of fs67.readdirSync(sessionsBase)) {
55572
56144
  const yearPath = path63.join(sessionsBase, year);
55573
56145
  try {
55574
- if (!fs66.statSync(yearPath).isDirectory()) continue;
56146
+ if (!fs67.statSync(yearPath).isDirectory()) continue;
55575
56147
  } catch {
55576
56148
  continue;
55577
56149
  }
55578
- for (const month of fs66.readdirSync(yearPath)) {
56150
+ for (const month of fs67.readdirSync(yearPath)) {
55579
56151
  const monthPath = path63.join(yearPath, month);
55580
56152
  try {
55581
- if (!fs66.statSync(monthPath).isDirectory()) continue;
56153
+ if (!fs67.statSync(monthPath).isDirectory()) continue;
55582
56154
  } catch {
55583
56155
  continue;
55584
56156
  }
55585
- for (const day of fs66.readdirSync(monthPath)) {
56157
+ for (const day of fs67.readdirSync(monthPath)) {
55586
56158
  const dayPath = path63.join(monthPath, day);
55587
56159
  try {
55588
- if (!fs66.statSync(dayPath).isDirectory()) continue;
56160
+ if (!fs67.statSync(dayPath).isDirectory()) continue;
55589
56161
  } catch {
55590
56162
  continue;
55591
56163
  }
55592
- for (const file of fs66.readdirSync(dayPath)) {
56164
+ for (const file of fs67.readdirSync(dayPath)) {
55593
56165
  if (file.endsWith(".jsonl")) jsonlFiles.push(path63.join(dayPath, file));
55594
56166
  }
55595
56167
  }
@@ -55602,7 +56174,7 @@ function buildCodexSessions(days, allAuditEntries) {
55602
56174
  for (const filePath of jsonlFiles) {
55603
56175
  let lines;
55604
56176
  try {
55605
- lines = fs66.readFileSync(filePath, "utf-8").split("\n");
56177
+ lines = fs67.readFileSync(filePath, "utf-8").split("\n");
55606
56178
  } catch {
55607
56179
  continue;
55608
56180
  }
@@ -55691,7 +56263,7 @@ function buildSessions(days, historyPath) {
55691
56263
  const hPath = historyPath ?? path63.join(os56.homedir(), ".claude", "history.jsonl");
55692
56264
  let historyRaw = "";
55693
56265
  try {
55694
- historyRaw = fs66.readFileSync(hPath, "utf-8");
56266
+ historyRaw = fs67.readFileSync(hPath, "utf-8");
55695
56267
  } catch {
55696
56268
  }
55697
56269
  const cutoff = days !== null ? (() => {
@@ -55715,7 +56287,7 @@ function buildSessions(days, historyPath) {
55715
56287
  const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
55716
56288
  let sessionLines = [];
55717
56289
  try {
55718
- sessionLines = fs66.readFileSync(jsonlFile, "utf-8").split("\n");
56290
+ sessionLines = fs67.readFileSync(jsonlFile, "utf-8").split("\n");
55719
56291
  } catch {
55720
56292
  }
55721
56293
  const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
@@ -56109,12 +56681,12 @@ function registerSessionTaintCommand(program2) {
56109
56681
 
56110
56682
  // src/cli/commands/skill-pin.ts
56111
56683
  import chalk36 from "chalk";
56112
- import fs67 from "fs";
56684
+ import fs68 from "fs";
56113
56685
  import os57 from "os";
56114
56686
  import path64 from "path";
56115
56687
  function wipeSkillSessions() {
56116
56688
  try {
56117
- fs67.rmSync(path64.join(os57.homedir(), ".node9", "skill-sessions"), {
56689
+ fs68.rmSync(path64.join(os57.homedir(), ".node9", "skill-sessions"), {
56118
56690
  recursive: true,
56119
56691
  force: true
56120
56692
  });
@@ -56196,15 +56768,15 @@ function registerSkillPinCommand(program2) {
56196
56768
  }
56197
56769
 
56198
56770
  // src/cli/commands/decisions.ts
56199
- import fs68 from "fs";
56771
+ import fs69 from "fs";
56200
56772
  import os58 from "os";
56201
56773
  import path65 from "path";
56202
56774
  import chalk37 from "chalk";
56203
56775
  var DECISIONS_FILE2 = path65.join(os58.homedir(), ".node9", "decisions.json");
56204
56776
  function readDecisions() {
56205
56777
  try {
56206
- if (!fs68.existsSync(DECISIONS_FILE2)) return {};
56207
- const raw = fs68.readFileSync(DECISIONS_FILE2, "utf-8");
56778
+ if (!fs69.existsSync(DECISIONS_FILE2)) return {};
56779
+ const raw = fs69.readFileSync(DECISIONS_FILE2, "utf-8");
56208
56780
  const parsed = JSON.parse(raw);
56209
56781
  const out = {};
56210
56782
  for (const [k, v] of Object.entries(parsed)) {
@@ -56217,10 +56789,10 @@ function readDecisions() {
56217
56789
  }
56218
56790
  function writeDecisions(d) {
56219
56791
  const dir = path65.dirname(DECISIONS_FILE2);
56220
- if (!fs68.existsSync(dir)) fs68.mkdirSync(dir, { recursive: true });
56792
+ if (!fs69.existsSync(dir)) fs69.mkdirSync(dir, { recursive: true });
56221
56793
  const tmp = `${DECISIONS_FILE2}.${process.pid}.tmp`;
56222
- fs68.writeFileSync(tmp, JSON.stringify(d, null, 2));
56223
- fs68.renameSync(tmp, DECISIONS_FILE2);
56794
+ fs69.writeFileSync(tmp, JSON.stringify(d, null, 2));
56795
+ fs69.renameSync(tmp, DECISIONS_FILE2);
56224
56796
  }
56225
56797
  function registerDecisionsCommand(program2) {
56226
56798
  const cmd = program2.command("decisions").description('Manage persistent "Always Allow" / "Always Deny" tool decisions');
@@ -56277,7 +56849,7 @@ Persistent decisions (${entries.length})
56277
56849
 
56278
56850
  // src/cli/commands/dlp.ts
56279
56851
  import chalk38 from "chalk";
56280
- import fs69 from "fs";
56852
+ import fs70 from "fs";
56281
56853
  import path66 from "path";
56282
56854
  import os59 from "os";
56283
56855
  var AUDIT_LOG = path66.join(os59.homedir(), ".node9", "audit.log");
@@ -56288,7 +56860,7 @@ function stripAnsi(s) {
56288
56860
  }
56289
56861
  function loadResolved() {
56290
56862
  try {
56291
- const raw = JSON.parse(fs69.readFileSync(RESOLVED_FILE, "utf-8"));
56863
+ const raw = JSON.parse(fs70.readFileSync(RESOLVED_FILE, "utf-8"));
56292
56864
  return new Set(raw);
56293
56865
  } catch {
56294
56866
  return /* @__PURE__ */ new Set();
@@ -56296,13 +56868,13 @@ function loadResolved() {
56296
56868
  }
56297
56869
  function saveResolved(resolved) {
56298
56870
  try {
56299
- fs69.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
56871
+ fs70.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
56300
56872
  } catch {
56301
56873
  }
56302
56874
  }
56303
56875
  function loadDlpFindings() {
56304
- if (!fs69.existsSync(AUDIT_LOG)) return [];
56305
- return fs69.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
56876
+ if (!fs70.existsSync(AUDIT_LOG)) return [];
56877
+ return fs70.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
56306
56878
  if (!line.trim()) return [];
56307
56879
  try {
56308
56880
  const e = JSON.parse(line);
@@ -56401,13 +56973,13 @@ function registerDlpCommand(program2) {
56401
56973
  // src/cli/commands/mask.ts
56402
56974
  init_dlp();
56403
56975
  import chalk39 from "chalk";
56404
- import fs70 from "fs";
56976
+ import fs71 from "fs";
56405
56977
  import path67 from "path";
56406
56978
  import os60 from "os";
56407
56979
  function findJsonlFiles(dir) {
56408
56980
  const results = [];
56409
- if (!fs70.existsSync(dir)) return results;
56410
- for (const entry of fs70.readdirSync(dir, { withFileTypes: true })) {
56981
+ if (!fs71.existsSync(dir)) return results;
56982
+ for (const entry of fs71.readdirSync(dir, { withFileTypes: true })) {
56411
56983
  const full = path67.join(dir, entry.name);
56412
56984
  if (entry.isDirectory()) results.push(...findJsonlFiles(full));
56413
56985
  else if (entry.isFile() && entry.name.endsWith(".jsonl")) results.push(full);
@@ -56451,7 +57023,7 @@ function redactJson(obj) {
56451
57023
  function processFile(filePath, dryRun) {
56452
57024
  let raw;
56453
57025
  try {
56454
- raw = fs70.readFileSync(filePath, "utf-8");
57026
+ raw = fs71.readFileSync(filePath, "utf-8");
56455
57027
  } catch {
56456
57028
  return { redactedLines: 0, patterns: [] };
56457
57029
  }
@@ -56483,14 +57055,14 @@ function processFile(filePath, dryRun) {
56483
57055
  }
56484
57056
  }
56485
57057
  if (!dryRun && redactedLines > 0) {
56486
- fs70.writeFileSync(filePath, newLines.join("\n"), "utf-8");
57058
+ fs71.writeFileSync(filePath, newLines.join("\n"), "utf-8");
56487
57059
  }
56488
57060
  return { redactedLines, patterns };
56489
57061
  }
56490
57062
  function processJsonFile(filePath, dryRun) {
56491
57063
  let raw;
56492
57064
  try {
56493
- raw = fs70.readFileSync(filePath, "utf-8");
57065
+ raw = fs71.readFileSync(filePath, "utf-8");
56494
57066
  } catch {
56495
57067
  return { redactedLines: 0, patterns: [] };
56496
57068
  }
@@ -56503,14 +57075,14 @@ function processJsonFile(filePath, dryRun) {
56503
57075
  const { value, modified, found } = redactJson(parsed);
56504
57076
  if (!modified) return { redactedLines: 0, patterns: [] };
56505
57077
  if (!dryRun) {
56506
- fs70.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
57078
+ fs71.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
56507
57079
  }
56508
57080
  return { redactedLines: 1, patterns: found };
56509
57081
  }
56510
57082
  function findJsonFiles(dir) {
56511
57083
  const results = [];
56512
- if (!fs70.existsSync(dir)) return results;
56513
- for (const entry of fs70.readdirSync(dir, { withFileTypes: true })) {
57084
+ if (!fs71.existsSync(dir)) return results;
57085
+ for (const entry of fs71.readdirSync(dir, { withFileTypes: true })) {
56514
57086
  const full = path67.join(dir, entry.name);
56515
57087
  if (entry.isDirectory()) results.push(...findJsonFiles(full));
56516
57088
  else if (entry.isFile() && entry.name.endsWith(".json")) results.push(full);
@@ -56530,7 +57102,7 @@ function registerMaskCommand(program2) {
56530
57102
  const cutoff = options.all ? null : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
56531
57103
  const filtered = cutoff ? allFiles.filter((f) => {
56532
57104
  try {
56533
- return fs70.statSync(f.path).mtime >= cutoff;
57105
+ return fs71.statSync(f.path).mtime >= cutoff;
56534
57106
  } catch {
56535
57107
  return false;
56536
57108
  }
@@ -56586,7 +57158,7 @@ function registerMaskCommand(program2) {
56586
57158
  // src/cli.ts
56587
57159
  init_blast();
56588
57160
  var { version } = JSON.parse(
56589
- fs73.readFileSync(path70.join(__dirname, "../package.json"), "utf-8")
57161
+ fs74.readFileSync(path70.join(__dirname, "../package.json"), "utf-8")
56590
57162
  );
56591
57163
  var program = new Command();
56592
57164
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
@@ -56766,14 +57338,14 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
56766
57338
  }
56767
57339
  if (options.purge) {
56768
57340
  const node9Dir = path70.join(os63.homedir(), ".node9");
56769
- if (fs73.existsSync(node9Dir)) {
57341
+ if (fs74.existsSync(node9Dir)) {
56770
57342
  const confirmed = await confirm2({
56771
57343
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
56772
57344
  default: false
56773
57345
  });
56774
57346
  if (confirmed) {
56775
- fs73.rmSync(node9Dir, { recursive: true });
56776
- if (fs73.existsSync(node9Dir)) {
57347
+ fs74.rmSync(node9Dir, { recursive: true });
57348
+ if (fs74.existsSync(node9Dir)) {
56777
57349
  console.error(
56778
57350
  chalk41.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
56779
57351
  );
@@ -56938,12 +57510,12 @@ Run "node9 addto claude" to register it as the statusLine.`
56938
57510
  if (subcommand === "debug") {
56939
57511
  const flagFile = path70.join(os63.homedir(), ".node9", "hud-debug");
56940
57512
  if (state === "on") {
56941
- fs73.mkdirSync(path70.dirname(flagFile), { recursive: true });
56942
- fs73.writeFileSync(flagFile, "");
57513
+ fs74.mkdirSync(path70.dirname(flagFile), { recursive: true });
57514
+ fs74.writeFileSync(flagFile, "");
56943
57515
  console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
56944
57516
  console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
56945
57517
  } else if (state === "off") {
56946
- if (fs73.existsSync(flagFile)) fs73.unlinkSync(flagFile);
57518
+ if (fs74.existsSync(flagFile)) fs74.unlinkSync(flagFile);
56947
57519
  console.log("HUD debug logging disabled.");
56948
57520
  } else {
56949
57521
  console.error("Usage: node9 hud debug on|off");
@@ -57068,7 +57640,7 @@ if (process.argv[2] !== "daemon") {
57068
57640
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
57069
57641
  const logPath = path70.join(os63.homedir(), ".node9", "hook-debug.log");
57070
57642
  const msg = reason instanceof Error ? reason.message : String(reason);
57071
- fs73.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
57643
+ fs74.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
57072
57644
  `);
57073
57645
  }
57074
57646
  process.exit(0);