@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.js CHANGED
@@ -343,6 +343,7 @@ var init_config_schema = __esm({
343
343
  // nudge-only (default), and the scan cadence in minutes.
344
344
  mcpAutoWrap: import_zod.z.boolean().optional(),
345
345
  mcpReconcileIntervalMinutes: import_zod.z.number().positive().optional(),
346
+ mcpStaleAfterDays: import_zod.z.number().min(0).optional(),
346
347
  cloudSyncIntervalHours: import_zod.z.number().positive().optional(),
347
348
  // Seconds-granular override for the cloud policy sync cadence. Wins over
348
349
  // cloudSyncIntervalHours when set. Lets you opt into fast apply (e.g. 20)
@@ -3977,6 +3978,7 @@ var init_dist = __esm({
3977
3978
  name: "redis",
3978
3979
  description: "Protects Redis instances from destructive AI operations",
3979
3980
  aliases: [],
3981
+ _scopeNote: "Every rule requires Redis to actually be involved: a client in the command (redis-cli / valkey-cli, in either order \u2014 `redis-cli FLUSHALL` and `echo FLUSHALL | redis-cli` are both real), OR the command field holding the bare keyword, which is what the MCP redis_command tool sends ({ command: 'FLUSHALL', args: [] }). Without the client requirement these rules matched the WORD anywhere \u2014 including prose in a heredoc writing a file \u2014 which produced two observed false positives: a test fixture describing the shield, and a commit message naming the rule. Every other builtin shield already scopes this way (aws s3, docker system prune, kubectl delete, gh repo delete). See redis-scope.spec.ts.",
3980
3982
  smartRules: [
3981
3983
  {
3982
3984
  name: "shield:redis:block-flushall",
@@ -3985,7 +3987,7 @@ var init_dist = __esm({
3985
3987
  {
3986
3988
  field: "command",
3987
3989
  op: "matches",
3988
- value: "\\bFLUSHALL\\b",
3990
+ value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
3989
3991
  flags: "i"
3990
3992
  }
3991
3993
  ],
@@ -3999,7 +4001,7 @@ var init_dist = __esm({
3999
4001
  {
4000
4002
  field: "command",
4001
4003
  op: "matches",
4002
- value: "\\bFLUSHDB\\b",
4004
+ value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
4003
4005
  flags: "i"
4004
4006
  }
4005
4007
  ],
@@ -4013,7 +4015,7 @@ var init_dist = __esm({
4013
4015
  {
4014
4016
  field: "command",
4015
4017
  op: "matches",
4016
- value: "\\bCONFIG\\s+RESETSTAT\\b",
4018
+ value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
4017
4019
  flags: "i"
4018
4020
  }
4019
4021
  ],
@@ -4027,7 +4029,7 @@ var init_dist = __esm({
4027
4029
  {
4028
4030
  field: "command",
4029
4031
  op: "matches",
4030
- value: "\\bCONFIG\\s+SET\\b",
4032
+ value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
4031
4033
  flags: "i"
4032
4034
  }
4033
4035
  ],
@@ -4041,7 +4043,7 @@ var init_dist = __esm({
4041
4043
  {
4042
4044
  field: "command",
4043
4045
  op: "matches",
4044
- value: "\\bDEL\\b.*[*?\\[]|redis-cli.*--scan.*\\|.*xargs.*del",
4046
+ value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
4045
4047
  flags: "i"
4046
4048
  }
4047
4049
  ],
@@ -4608,6 +4610,48 @@ function getActiveEnvironment(config) {
4608
4610
  const env = config.settings.environment || process.env.NODE_ENV || "development";
4609
4611
  return config.environments[env] ?? null;
4610
4612
  }
4613
+ function readRulesCacheResilient(cacheFile) {
4614
+ let existed = false;
4615
+ for (let attempt = 0; attempt < 3; attempt++) {
4616
+ let content;
4617
+ try {
4618
+ content = import_fs4.default.readFileSync(cacheFile, "utf-8");
4619
+ existed = true;
4620
+ } catch (err2) {
4621
+ if (err2.code === "ENOENT") return {};
4622
+ continue;
4623
+ }
4624
+ try {
4625
+ return JSON.parse(content);
4626
+ } catch {
4627
+ }
4628
+ }
4629
+ if (existed) {
4630
+ const backup = import_path4.default.join(import_path4.default.dirname(cacheFile), "rules-cache.last-good.json");
4631
+ if (backup !== cacheFile) {
4632
+ try {
4633
+ const raw = JSON.parse(import_fs4.default.readFileSync(backup, "utf-8"));
4634
+ logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
4635
+ return raw;
4636
+ } catch {
4637
+ }
4638
+ }
4639
+ logCacheReadIssue(cacheFile, "RULES_CACHE_UNREADABLE");
4640
+ }
4641
+ return {};
4642
+ }
4643
+ function logCacheReadIssue(cacheFile, kind) {
4644
+ if (cacheReadFailureLogged) return;
4645
+ cacheReadFailureLogged = true;
4646
+ try {
4647
+ import_fs4.default.appendFileSync(
4648
+ import_path4.default.join(import_os4.default.homedir(), ".node9", "hook-debug.log"),
4649
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
4650
+ `
4651
+ );
4652
+ } catch {
4653
+ }
4654
+ }
4611
4655
  function getConfig(cwd) {
4612
4656
  if (!cwd && cachedConfig) return cachedConfig;
4613
4657
  const globalPath = import_path4.default.join(import_os4.default.homedir(), ".node9", "config.json");
@@ -4675,6 +4719,7 @@ function getConfig(cwd) {
4675
4719
  if (s.mcpAutoWrap !== void 0) mergedSettings.mcpAutoWrap = s.mcpAutoWrap === true;
4676
4720
  if (s.mcpReconcileIntervalMinutes !== void 0)
4677
4721
  mergedSettings.mcpReconcileIntervalMinutes = s.mcpReconcileIntervalMinutes;
4722
+ if (s.mcpStaleAfterDays !== void 0) mergedSettings.mcpStaleAfterDays = s.mcpStaleAfterDays;
4678
4723
  if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
4679
4724
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
4680
4725
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -4684,12 +4729,13 @@ function getConfig(cwd) {
4684
4729
  if (p.smartRules) {
4685
4730
  const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
4686
4731
  const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
4687
- const userRuleNames = new Set(p.smartRules.filter((r) => r.name).map((r) => r.name));
4732
+ const localRules = p.smartRules.map(({ pinned: _pinned, ...r }) => r);
4733
+ const userRuleNames = new Set(localRules.filter((r) => r.name).map((r) => r.name));
4688
4734
  const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
4689
4735
  const filteredNonBlocks = defaultNonBlocks.filter(
4690
4736
  (r) => !r.name || !userRuleNames.has(r.name)
4691
4737
  );
4692
- mergedPolicy.smartRules = [...filteredBlocks, ...p.smartRules, ...filteredNonBlocks];
4738
+ mergedPolicy.smartRules = [...filteredBlocks, ...localRules, ...filteredNonBlocks];
4693
4739
  }
4694
4740
  if (p.snapshot) {
4695
4741
  const s2 = p.snapshot;
@@ -4754,10 +4800,12 @@ function getConfig(cwd) {
4754
4800
  applyLayer(globalConfig);
4755
4801
  applyLayer(projectConfig);
4756
4802
  let cloudManagedShields = [];
4803
+ let modeCloudControlled = false;
4804
+ let modeCloudStaged = false;
4757
4805
  {
4758
4806
  const cacheFile = import_path4.default.join(import_os4.default.homedir(), ".node9", "rules-cache.json");
4759
4807
  try {
4760
- const raw = JSON.parse(import_fs4.default.readFileSync(cacheFile, "utf-8"));
4808
+ const raw = readRulesCacheResilient(cacheFile);
4761
4809
  if (Array.isArray(raw.rules) && raw.rules.length > 0) {
4762
4810
  applyLayer({ policy: { smartRules: raw.rules } });
4763
4811
  }
@@ -4774,6 +4822,9 @@ function getConfig(cwd) {
4774
4822
  locked.includes("mode")
4775
4823
  );
4776
4824
  }
4825
+ if (typeof mc.mode === "string" || locked.includes("mode")) {
4826
+ modeCloudControlled = true;
4827
+ }
4777
4828
  if (mc.egress && typeof mc.egress === "object") {
4778
4829
  const hosts = (v) => Array.isArray(v) ? v.filter((h) => typeof h === "string") : void 0;
4779
4830
  mergedPolicy.egress = applyManagedEgress(
@@ -4872,19 +4923,28 @@ function getConfig(cwd) {
4872
4923
  }
4873
4924
  if (raw.shadowMode === true) {
4874
4925
  mergedSettings.mode = "observe";
4926
+ modeCloudStaged = true;
4875
4927
  }
4876
4928
  } catch {
4877
4929
  }
4878
4930
  }
4879
4931
  const shieldOverrides = readShieldOverrides();
4880
4932
  const activeShieldNames = [.../* @__PURE__ */ new Set([...readActiveShields(), ...cloudManagedShields])];
4933
+ const cloudManagedSet = new Set(cloudManagedShields);
4881
4934
  for (const shieldName of activeShieldNames) {
4882
- const shield = getShield(shieldName);
4935
+ const isCloudMandated = cloudManagedSet.has(shieldName);
4936
+ const shield = isCloudMandated ? BUILTIN_SHIELDS[shieldName] : getShield(shieldName);
4883
4937
  if (!shield) continue;
4884
4938
  const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4885
- const ruleOverrides = shieldOverrides[shieldName] ?? {};
4939
+ const ruleOverrides = isCloudMandated ? {} : shieldOverrides[shieldName] ?? {};
4886
4940
  for (const rule of shield.smartRules) {
4887
- if (!existingRuleNames.has(rule.name)) {
4941
+ const collides = rule.name ? existingRuleNames.has(rule.name) : false;
4942
+ if (isCloudMandated) {
4943
+ if (collides) {
4944
+ mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
4945
+ }
4946
+ mergedPolicy.smartRules.push({ ...rule, pinned: true });
4947
+ } else if (!collides) {
4888
4948
  const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
4889
4949
  mergedPolicy.smartRules.push(
4890
4950
  overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
@@ -4900,7 +4960,27 @@ function getConfig(cwd) {
4900
4960
  for (const rule of ADVISORY_SMART_RULES) {
4901
4961
  if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
4902
4962
  }
4903
- if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
4963
+ const envMode = process.env.NODE9_MODE;
4964
+ if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
4965
+ mergedSettings.mode = envMode;
4966
+ }
4967
+ if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
4968
+ mergedSettings.mode = "standard";
4969
+ }
4970
+ const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
4971
+ if (modeCloudControlled && mergedSettings.mode === "strict") {
4972
+ for (const name of Object.keys(mergedEnvironments)) {
4973
+ if (mergedEnvironments[name]?.requireApproval === false) {
4974
+ const cleaned = { ...mergedEnvironments[name] };
4975
+ delete cleaned.requireApproval;
4976
+ mergedEnvironments[name] = cleaned;
4977
+ }
4978
+ }
4979
+ }
4980
+ if (managedFloorActive) {
4981
+ mergedPolicy.ignoredTools = [...DEFAULT_CONFIG.policy.ignoredTools];
4982
+ mergedPolicy.sandboxPaths = [...DEFAULT_CONFIG.policy.sandboxPaths];
4983
+ }
4904
4984
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
4905
4985
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
4906
4986
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
@@ -4969,7 +5049,7 @@ ${error.replace("Invalid config:\n", "")}
4969
5049
  }
4970
5050
  return sanitized;
4971
5051
  }
4972
- var import_fs4, import_path4, import_os4, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig;
5052
+ var import_fs4, import_path4, import_os4, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, cacheReadFailureLogged;
4973
5053
  var init_config = __esm({
4974
5054
  "src/config/index.ts"() {
4975
5055
  "use strict";
@@ -4981,6 +5061,7 @@ var init_config = __esm({
4981
5061
  init_managed();
4982
5062
  init_build();
4983
5063
  init_trusted_hosts();
5064
+ init_dist();
4984
5065
  DANGEROUS_WORDS = [
4985
5066
  "mkfs",
4986
5067
  // formats/wipes a filesystem partition
@@ -5258,6 +5339,7 @@ var init_config = __esm({
5258
5339
  }
5259
5340
  ];
5260
5341
  cachedConfig = null;
5342
+ cacheReadFailureLogged = false;
5261
5343
  }
5262
5344
  });
5263
5345
 
@@ -5975,6 +6057,18 @@ async function isDaemonReachable(timeoutMs = 500) {
5975
6057
  return false;
5976
6058
  }
5977
6059
  }
6060
+ async function daemonHasInteractiveApprover(timeoutMs = 400) {
6061
+ try {
6062
+ const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/approver`, {
6063
+ signal: AbortSignal.timeout(timeoutMs)
6064
+ });
6065
+ if (!res.ok) return false;
6066
+ const body = await res.json();
6067
+ return body.interactive === true;
6068
+ } catch {
6069
+ return false;
6070
+ }
6071
+ }
5978
6072
  async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent) {
5979
6073
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
5980
6074
  const ctrl = new AbortController();
@@ -6859,6 +6953,12 @@ function isNetworkTool(toolName, args) {
6859
6953
  function notifyActivity(data) {
6860
6954
  return notifyActivitySocket(data);
6861
6955
  }
6956
+ async function hasReachableHumanApprover(opts) {
6957
+ const hasDisplay = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
6958
+ const nativeReachable = !opts.calledFromDaemon && opts.approvers.native !== false && hasDisplay;
6959
+ if (nativeReachable) return true;
6960
+ return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
6961
+ }
6862
6962
  async function authorizeHeadless(toolName, args, meta, options) {
6863
6963
  if (!options?.calledFromDaemon) {
6864
6964
  const actId = (0, import_crypto5.randomUUID)();
@@ -7173,6 +7273,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7173
7273
  return { approved: true, checkedBy: "local-policy" };
7174
7274
  }
7175
7275
  if (policyResult.decision === "block") {
7276
+ const daemonUp = isDaemonRunning();
7277
+ let humanApproverReachable = false;
7278
+ if (!policyResult.dependsOnStatePredicates?.length && daemonUp && !isTestEnv2) {
7279
+ humanApproverReachable = await hasReachableHumanApprover({
7280
+ approvers,
7281
+ calledFromDaemon: options?.calledFromDaemon
7282
+ });
7283
+ }
7284
+ const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
7285
+ const hardBlock = () => {
7286
+ if (!isManual)
7287
+ appendLocalAudit(
7288
+ toolName,
7289
+ args,
7290
+ "deny",
7291
+ "smart-rule-block",
7292
+ { ...meta, ruleName: policyResult.ruleName },
7293
+ hashAuditArgs
7294
+ );
7295
+ return {
7296
+ approved: false,
7297
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
7298
+ blockedBy: "local-config",
7299
+ blockedByLabel: policyResult.blockedByLabel,
7300
+ ruleHit: policyResult.ruleName,
7301
+ ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
7302
+ ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
7303
+ };
7304
+ };
7176
7305
  if (policyResult.dependsOnStatePredicates?.length) {
7177
7306
  const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
7178
7307
  const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
@@ -7189,7 +7318,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7189
7318
  if (predicatesMet && policyResult.recoveryCommand) {
7190
7319
  statefulRecoveryCommand = policyResult.recoveryCommand;
7191
7320
  }
7192
- } else if (isDaemonRunning() && !isTestEnv2) {
7321
+ } else if (mayDowngrade) {
7193
7322
  if (!isManual)
7194
7323
  appendLocalAudit(
7195
7324
  toolName,
@@ -7211,36 +7340,13 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7211
7340
  };
7212
7341
  }
7213
7342
  } else {
7214
- if (!isManual)
7215
- appendLocalAudit(
7216
- toolName,
7217
- args,
7218
- "deny",
7219
- "smart-rule-block",
7220
- // Include policyResult.ruleName so the [2] Report SHIELDS
7221
- // panel can attribute this block to its specific shield
7222
- // (e.g. `shield:project-jail:block-read-ssh`) via the
7223
- // rule→shield map. checkedBy stays as the generic
7224
- // `smart-rule-block` for backward compat with existing
7225
- // log readers.
7226
- { ...meta, ruleName: policyResult.ruleName },
7227
- hashAuditArgs
7228
- );
7229
- return {
7230
- approved: false,
7231
- reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
7232
- blockedBy: "local-config",
7233
- blockedByLabel: policyResult.blockedByLabel,
7234
- ruleHit: policyResult.ruleName,
7235
- ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
7236
- ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
7237
- };
7343
+ return hardBlock();
7238
7344
  }
7239
7345
  }
7240
7346
  explainableLabel = policyResult.blockedByLabel || "Local Config";
7241
7347
  policyMatchedField = policyResult.matchedField;
7242
7348
  policyMatchedWord = policyResult.matchedWord;
7243
- if (policyResult.ruleName) localSmartRuleMatched = true;
7349
+ if (policyResult.ruleName || policyResult.tier === 7) localSmartRuleMatched = true;
7244
7350
  if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
7245
7351
  else if (policyResult.reason) policyRuleDescription = policyResult.reason;
7246
7352
  riskMetadata = computeRiskMetadata(
@@ -7252,7 +7358,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7252
7358
  policyResult.ruleName
7253
7359
  );
7254
7360
  if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
7255
- const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
7361
+ const persistent = policyResult.ruleName || policyResult.tier === 7 ? null : getPersistentDecision(toolName);
7256
7362
  if (persistent === "allow" && !appPermReview) {
7257
7363
  if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
7258
7364
  return { approved: true, checkedBy: "persistent" };
@@ -15919,6 +16025,9 @@ data: ${JSON.stringify(data)}
15919
16025
  }
15920
16026
  });
15921
16027
  }
16028
+ function hasInteractiveClient() {
16029
+ return [...sseClients].some((c) => c.capabilities.includes("input"));
16030
+ }
15922
16031
  function broadcastForensic(finding) {
15923
16032
  const severity = CRITICAL_FORENSIC_CATEGORIES.has(finding.type) ? "critical" : "warning";
15924
16033
  const event = {
@@ -17156,18 +17265,27 @@ var init_score = __esm({
17156
17265
 
17157
17266
  // src/posture/headline.ts
17158
17267
  function worstFinding(findings) {
17159
- return [...findings].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0];
17268
+ const cmp = (x, y) => x < y ? -1 : x > y ? 1 : 0;
17269
+ return [...findings].sort(
17270
+ (a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || cmp(a.category, b.category) || cmp(a.title, b.title)
17271
+ )[0];
17272
+ }
17273
+ function actionFromFinding(f) {
17274
+ if (!f?.fix) return null;
17275
+ const fix = f.fix.replace(/^fix it now:\s*/i, "");
17276
+ const where = f.detail.length === 0 ? "" : f.detail.length === 1 ? ` \u2014 found: ${f.detail[0]}` : ` \u2014 found: ${f.detail[0]} and ${f.detail.length - 1} more`;
17277
+ return fix + where;
17160
17278
  }
17161
17279
  function deriveHeadline(allFindings) {
17162
17280
  const findings = allFindings.filter(
17163
17281
  (f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix"
17164
17282
  );
17165
17283
  if (findings.length === 0 || findings.every((f) => f.severity === "advisory")) return null;
17166
- const has = (category) => findings.some((f) => f.category === category);
17167
- const secrets = has("Secrets");
17168
- const egressOpen = has("Egress");
17169
- const noIsolation = has("Isolation");
17170
- const gateWeak = has("Approval gate");
17284
+ const has2 = (category) => findings.some((f) => f.category === category);
17285
+ const secrets = has2("Secrets");
17286
+ const egressOpen = has2("Egress");
17287
+ const noIsolation = has2("Isolation");
17288
+ const gateWeak = has2("Approval gate");
17171
17289
  const notWired = findings.some((f) => f.category === "Coverage" && f.severity === "critical");
17172
17290
  const observeOnly = findings.some((f) => f.category === "Coverage" && f.severity === "high");
17173
17291
  let risk;
@@ -17190,13 +17308,16 @@ function deriveHeadline(allFindings) {
17190
17308
  } else if (observeOnly) {
17191
17309
  action = "Switch node9 to enforcing mode \u2014 right now it is only watching, not blocking.";
17192
17310
  } else if (egressOpen) {
17193
- action = "lock egress to an allowlist (node9 can enforce it) \u2014 it closes the exit the exfiltration needs.";
17311
+ const egressFix = actionFromFinding(
17312
+ worstFinding(findings.filter((f) => f.category === "Egress"))
17313
+ );
17314
+ action = egressFix ? `${egressFix} Closing the exit breaks the exfiltration chain.` : "lock egress to an allowlist (node9 can enforce it) \u2014 it closes the exit the exfiltration needs.";
17194
17315
  } else if (secrets) {
17195
- action = "node9 can block reads of sensitive paths (~/.ssh, ~/.aws) in-path.";
17316
+ action = actionFromFinding(worstFinding(findings.filter((f) => f.category === "Secrets"))) ?? "node9 can block reads of sensitive credential files in-path (`node9 shield enable project-jail`).";
17196
17317
  } else if (gateWeak) {
17197
- action = "node9 can enforce destructive-command blocking in-path.";
17318
+ action = actionFromFinding(worstFinding(findings.filter((f) => f.category === "Approval gate"))) ?? "node9 can enforce destructive-command blocking in-path (`node9 shield enable bash-safe`).";
17198
17319
  } else {
17199
- action = worstFinding(findings)?.fix ?? "Review the findings below.";
17320
+ action = actionFromFinding(worstFinding(findings)) ?? "Review the findings below.";
17200
17321
  }
17201
17322
  return { risk, action };
17202
17323
  }
@@ -17734,6 +17855,21 @@ function inventoryMcp(home = import_os31.default.homedir()) {
17734
17855
  }
17735
17856
  return out;
17736
17857
  }
17858
+ function inventoryServerKeys(inv) {
17859
+ const keys = /* @__PURE__ */ new Set();
17860
+ for (const e of inv) {
17861
+ if (e.state === "gatewayed") {
17862
+ const i = e.args.indexOf("--upstream");
17863
+ if (i >= 0 && e.args[i + 1]) {
17864
+ keys.add(getServerKey(e.args[i + 1]));
17865
+ }
17866
+ } else if (e.state === "ungoverned") {
17867
+ const cmd = [e.command, ...e.args].map(quoteArg).join(" ");
17868
+ keys.add(getServerKey(cmd));
17869
+ }
17870
+ }
17871
+ return keys;
17872
+ }
17737
17873
  function writeMcpEntry(mcpFile, format, name, entry) {
17738
17874
  const key = format === "toml" ? "mcp_servers" : "mcpServers";
17739
17875
  let root = {};
@@ -17761,6 +17897,7 @@ var init_mcp_wrap = __esm({
17761
17897
  import_smol_toml4 = require("smol-toml");
17762
17898
  init_agent_wiring();
17763
17899
  init_mcp_cmd();
17900
+ init_mcp_pin();
17764
17901
  init_mcp_cmd();
17765
17902
  }
17766
17903
  });
@@ -18189,9 +18326,12 @@ function extractManagedConfig(body) {
18189
18326
  return out.mode !== void 0 || out.egress !== void 0 || out.dlp !== void 0 || out.approvers !== void 0 || out.reviewChannel !== void 0 || out.approvalTimeoutMs !== void 0 || out.injectionScan !== void 0 || out.loopDetection !== void 0 || out.skillPinning !== void 0 || out.jailPaths !== void 0 || out.trustedHosts !== void 0 || out.appPermissions !== void 0 ? out : void 0;
18190
18327
  }
18191
18328
  function writeCache2(cache) {
18192
- const dir = import_path34.default.dirname(rulesCacheFile());
18193
- if (!import_fs35.default.existsSync(dir)) import_fs35.default.mkdirSync(dir, { recursive: true });
18194
- import_fs35.default.writeFileSync(rulesCacheFile(), JSON.stringify(cache, null, 2) + "\n", "utf-8");
18329
+ const data = JSON.stringify(cache, null, 2) + "\n";
18330
+ atomicWriteSync2(rulesCacheFile(), data, "utf-8");
18331
+ try {
18332
+ atomicWriteSync2(rulesCacheBackupFile(), data, "utf-8");
18333
+ } catch {
18334
+ }
18195
18335
  }
18196
18336
  async function syncOnce() {
18197
18337
  const creds = readCredentials();
@@ -18485,7 +18625,7 @@ function startForensicBroadcast() {
18485
18625
  const recurring = setInterval(() => void tick(), FORENSIC_BROADCAST_INTERVAL_MS);
18486
18626
  recurring.unref();
18487
18627
  }
18488
- var import_fs35, import_https4, import_os32, import_path34, FINDING_TO_SIGNAL3, rulesCacheFile, DEFAULT_API_URL2, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS, syncHealthFile, STALE_MIN_MS, STALE_MAX_MS, STALE_FACTOR, FORENSIC_BROADCAST_INTERVAL_MS, FORENSIC_INITIAL_DELAY_MS, forensicBroadcastOffsets;
18628
+ var import_fs35, import_https4, import_os32, import_path34, FINDING_TO_SIGNAL3, rulesCacheFile, rulesCacheBackupFile, DEFAULT_API_URL2, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS, syncHealthFile, STALE_MIN_MS, STALE_MAX_MS, STALE_FACTOR, FORENSIC_BROADCAST_INTERVAL_MS, FORENSIC_INITIAL_DELAY_MS, forensicBroadcastOffsets;
18489
18629
  var init_sync = __esm({
18490
18630
  "src/daemon/sync.ts"() {
18491
18631
  "use strict";
@@ -18499,6 +18639,7 @@ var init_sync = __esm({
18499
18639
  init_ship();
18500
18640
  init_build2();
18501
18641
  init_mcp_tools();
18642
+ init_state2();
18502
18643
  init_mcp_status();
18503
18644
  init_ship2();
18504
18645
  init_shields();
@@ -18519,6 +18660,7 @@ var init_sync = __esm({
18519
18660
  "long-output-redacted": "longOutputRedactions"
18520
18661
  };
18521
18662
  rulesCacheFile = () => import_path34.default.join(import_os32.default.homedir(), ".node9", "rules-cache.json");
18663
+ rulesCacheBackupFile = () => import_path34.default.join(import_os32.default.homedir(), ".node9", "rules-cache.last-good.json");
18522
18664
  DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept/policies/sync";
18523
18665
  DEFAULT_INTERVAL_HOURS = 5;
18524
18666
  MIN_INTERVAL_SECONDS = 15;
@@ -18760,6 +18902,53 @@ var init_audit_shipper = __esm({
18760
18902
  }
18761
18903
  });
18762
18904
 
18905
+ // src/audit/decision.ts
18906
+ function classifyDecision(a, b) {
18907
+ const isRow = !!a && typeof a === "object";
18908
+ const decision = isRow ? a.decision : a;
18909
+ const attribution = isRow ? a.checkedBy ?? a.source : b;
18910
+ const raw = typeof decision === "string" ? decision : String(decision ?? "");
18911
+ const src = typeof attribution === "string" ? attribution.toLowerCase() : "";
18912
+ const d = raw.toLowerCase();
18913
+ if (src && has(src, "observe-mode")) {
18914
+ return { outcome: "observe", label: "Would block", raw };
18915
+ }
18916
+ if (d === "dlp") return { outcome: "info", label: "Finding", raw };
18917
+ if (d === "mcp-discovered") return { outcome: "info", label: "Info", raw };
18918
+ if (d === "allow" || d === "allowed") {
18919
+ if (src === "post-hook") return { outcome: "allow", label: "Ran", raw };
18920
+ if (HUMAN_SOURCES.has(src)) {
18921
+ return { outcome: "allow", label: "Approved", raw };
18922
+ }
18923
+ return { outcome: "allow", label: "Auto-allowed", raw };
18924
+ }
18925
+ if (d === "deny" || d === "auto-deny" || d === "block") {
18926
+ if (TIMEOUT_SOURCES.has(src)) {
18927
+ return { outcome: "deny", label: "Timed out", raw };
18928
+ }
18929
+ if (HUMAN_SOURCES.has(src)) {
18930
+ return { outcome: "deny", label: "Denied", raw };
18931
+ }
18932
+ return { outcome: "deny", label: "Blocked", raw };
18933
+ }
18934
+ if (d === "review" || d === "pending") {
18935
+ return { outcome: "info", label: "Pending", raw };
18936
+ }
18937
+ return { outcome: "unknown", label: raw ? `? ${raw}` : "? (none)", raw };
18938
+ }
18939
+ function decisionTag(view) {
18940
+ return `[${view.label}]`.padEnd(14);
18941
+ }
18942
+ var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
18943
+ var init_decision = __esm({
18944
+ "src/audit/decision.ts"() {
18945
+ "use strict";
18946
+ HUMAN_SOURCES = /* @__PURE__ */ new Set(["daemon", "cloud", "local-decision", "inline-review-approved"]);
18947
+ TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
18948
+ has = (s, needle) => s.includes(needle);
18949
+ }
18950
+ });
18951
+
18763
18952
  // src/daemon/dlp-scanner.ts
18764
18953
  function loadIndex() {
18765
18954
  try {
@@ -18995,6 +19184,7 @@ function runMcpReconcile() {
18995
19184
  }
18996
19185
  const baseline = loadBaseline();
18997
19186
  const creds = getCredentials();
19187
+ reconcileStale(inv, creds);
18998
19188
  const fresh = inv.filter((e) => e.state === "ungoverned" && !baseline.has(idKey(e)));
18999
19189
  if (fresh.length === 0) return;
19000
19190
  const wrappedAgents = /* @__PURE__ */ new Set();
@@ -19034,6 +19224,77 @@ function runMcpReconcile() {
19034
19224
  }
19035
19225
  saveBaseline(baseline);
19036
19226
  }
19227
+ function reconcileStale(inv, creds) {
19228
+ let pins;
19229
+ try {
19230
+ pins = readMcpPins();
19231
+ } catch {
19232
+ return;
19233
+ }
19234
+ const serverKeys = Object.keys(pins.servers);
19235
+ if (serverKeys.length === 0) return;
19236
+ const liveKeys = inventoryServerKeys(inv);
19237
+ const now = (/* @__PURE__ */ new Date()).toISOString();
19238
+ let dirty = false;
19239
+ for (const sk of serverKeys) {
19240
+ const pin = pins.servers[sk];
19241
+ if (liveKeys.has(sk)) {
19242
+ if (pin.lastSeen !== now) {
19243
+ pin.lastSeen = now;
19244
+ dirty = true;
19245
+ }
19246
+ } else if (!pin.lastSeen) {
19247
+ pin.lastSeen = pin.pinnedAt;
19248
+ dirty = true;
19249
+ }
19250
+ }
19251
+ const staleDays = getConfig().settings.mcpStaleAfterDays ?? DEFAULT_STALE_DAYS;
19252
+ if (staleDays > 0 && liveKeys.size > 0) {
19253
+ const staleMs = staleDays * 864e5;
19254
+ for (const sk of serverKeys) {
19255
+ const pin = pins.servers[sk];
19256
+ if (liveKeys.has(sk)) continue;
19257
+ const age = Date.now() - Date.parse(pin.lastSeen ?? pin.pinnedAt);
19258
+ if (age >= staleMs) {
19259
+ appendToLog(HOOK_DEBUG_LOG, {
19260
+ event: "mcp-pin-auto-removed",
19261
+ serverKey: sk,
19262
+ label: pin.label,
19263
+ lastSeen: pin.lastSeen,
19264
+ pinnedAt: pin.pinnedAt
19265
+ });
19266
+ if (creds) {
19267
+ try {
19268
+ void auditLocalAllow(
19269
+ `mcp-server:${sk}`,
19270
+ {
19271
+ serverKey: sk,
19272
+ label: pin.label,
19273
+ lastSeen: pin.lastSeen,
19274
+ reason: "stale",
19275
+ staleDays
19276
+ },
19277
+ "mcp-server-removed",
19278
+ creds,
19279
+ { mcpServer: pin.label },
19280
+ void 0,
19281
+ false
19282
+ );
19283
+ } catch {
19284
+ }
19285
+ }
19286
+ delete pins.servers[sk];
19287
+ dirty = true;
19288
+ }
19289
+ }
19290
+ }
19291
+ if (dirty) {
19292
+ try {
19293
+ writeMcpPins(pins);
19294
+ } catch {
19295
+ }
19296
+ }
19297
+ }
19037
19298
  function startMcpReconciler() {
19038
19299
  setImmediate(() => {
19039
19300
  try {
@@ -19055,7 +19316,7 @@ function startMcpReconciler() {
19055
19316
  };
19056
19317
  schedule();
19057
19318
  }
19058
- var import_fs38, import_path37, import_os35, import_crypto10, BASELINE_FILE2, BASELINE_CAP, DEFAULT_INTERVAL_MIN;
19319
+ var import_fs38, import_path37, import_os35, import_crypto10, BASELINE_FILE2, BASELINE_CAP, DEFAULT_INTERVAL_MIN, DEFAULT_STALE_DAYS;
19059
19320
  var init_mcp_reconciler = __esm({
19060
19321
  "src/daemon/mcp-reconciler.ts"() {
19061
19322
  "use strict";
@@ -19068,9 +19329,11 @@ var init_mcp_reconciler = __esm({
19068
19329
  init_config();
19069
19330
  init_cloud();
19070
19331
  init_audit();
19332
+ init_mcp_pin();
19071
19333
  BASELINE_FILE2 = import_path37.default.join(import_os35.default.homedir(), ".node9", "mcp-baseline.json");
19072
19334
  BASELINE_CAP = 500;
19073
19335
  DEFAULT_INTERVAL_MIN = 60;
19336
+ DEFAULT_STALE_DAYS = 7;
19074
19337
  }
19075
19338
  });
19076
19339
 
@@ -19142,20 +19405,87 @@ var init_hook_heal = __esm({
19142
19405
  });
19143
19406
 
19144
19407
  // src/daemon/startup-log.ts
19408
+ function capStartupLog(file) {
19409
+ try {
19410
+ if (import_fs39.default.statSync(file).size > MAX_STARTUP_LOG_BYTES) import_fs39.default.truncateSync(file);
19411
+ } catch {
19412
+ }
19413
+ }
19145
19414
  function openStartupLogFd() {
19146
19415
  try {
19147
19416
  const file = DAEMON_STARTUP_LOG();
19148
19417
  const dir = import_path38.default.dirname(file);
19149
19418
  if (!import_fs39.default.existsSync(dir)) import_fs39.default.mkdirSync(dir, { recursive: true });
19150
- try {
19151
- if (import_fs39.default.statSync(file).size > MAX_STARTUP_LOG_BYTES) import_fs39.default.truncateSync(file);
19152
- } catch {
19153
- }
19419
+ capStartupLog(file);
19154
19420
  return import_fs39.default.openSync(file, "a");
19155
19421
  } catch {
19156
19422
  return void 0;
19157
19423
  }
19158
19424
  }
19425
+ function recordStartupState(outcome, kind, detail) {
19426
+ try {
19427
+ if (outcome === "starting") {
19428
+ const prev = readStartupState();
19429
+ if (prev?.outcome === "starting") {
19430
+ const prevAt = new Date(prev.at).getTime();
19431
+ const age = Math.abs(Date.now() - prevAt);
19432
+ if (!isNaN(prevAt) && age < 24 * 60 * 60 * 1e3) return;
19433
+ }
19434
+ }
19435
+ const file = DAEMON_STARTUP_STATE();
19436
+ const dir = import_path38.default.dirname(file);
19437
+ if (!import_fs39.default.existsSync(dir)) import_fs39.default.mkdirSync(dir, { recursive: true });
19438
+ const state = { outcome, at: (/* @__PURE__ */ new Date()).toISOString() };
19439
+ if (kind) state.kind = kind;
19440
+ if (detail) state.detail = detail.slice(0, MAX_DETAIL);
19441
+ const tmp = `${file}.${process.pid}.tmp`;
19442
+ try {
19443
+ import_fs39.default.writeFileSync(tmp, JSON.stringify(state), "utf-8");
19444
+ import_fs39.default.renameSync(tmp, file);
19445
+ } catch (err2) {
19446
+ try {
19447
+ import_fs39.default.unlinkSync(tmp);
19448
+ } catch {
19449
+ }
19450
+ throw err2;
19451
+ }
19452
+ } catch {
19453
+ }
19454
+ }
19455
+ function readStartupState() {
19456
+ try {
19457
+ const raw = import_fs39.default.readFileSync(DAEMON_STARTUP_STATE(), "utf-8");
19458
+ const s = JSON.parse(raw);
19459
+ if (!s || typeof s.outcome !== "string" || typeof s.at !== "string") return null;
19460
+ return s;
19461
+ } catch {
19462
+ return null;
19463
+ }
19464
+ }
19465
+ function readStartupCause(maxAgeMs = 24 * 60 * 60 * 1e3) {
19466
+ const s = readStartupState();
19467
+ if (!s) return null;
19468
+ const at = new Date(s.at);
19469
+ if (isNaN(at.getTime()) || Date.now() - at.getTime() > maxAgeMs) return null;
19470
+ switch (s.outcome) {
19471
+ case "ok":
19472
+ case "ok-elsewhere":
19473
+ return null;
19474
+ case "starting":
19475
+ if (Date.now() - at.getTime() < STARTING_GRACE_MS) return null;
19476
+ return {
19477
+ kind: "did-not-start",
19478
+ detail: "the daemon did not come up \u2014 see ~/.node9/daemon-startup.log and hook-debug.log",
19479
+ at,
19480
+ // `at` is the FIRST attempt of the streak, not the most recent one.
19481
+ label: "start attempts failing since"
19482
+ };
19483
+ case "failed":
19484
+ return { kind: s.kind || "failed", detail: s.detail || "", at, label: "last start attempt" };
19485
+ default:
19486
+ return null;
19487
+ }
19488
+ }
19159
19489
  function logDaemonStartup(kind, detail) {
19160
19490
  try {
19161
19491
  const file = DAEMON_STARTUP_LOG();
@@ -19167,7 +19497,7 @@ function logDaemonStartup(kind, detail) {
19167
19497
  } catch {
19168
19498
  }
19169
19499
  }
19170
- var import_fs39, import_path38, import_os36, DAEMON_STARTUP_LOG, MAX_STARTUP_LOG_BYTES;
19500
+ var import_fs39, import_path38, import_os36, DAEMON_STARTUP_LOG, MAX_STARTUP_LOG_BYTES, DAEMON_STARTUP_STATE, MAX_DETAIL, STARTING_GRACE_MS;
19171
19501
  var init_startup_log = __esm({
19172
19502
  "src/daemon/startup-log.ts"() {
19173
19503
  "use strict";
@@ -19176,10 +19506,83 @@ var init_startup_log = __esm({
19176
19506
  import_os36 = __toESM(require("os"));
19177
19507
  DAEMON_STARTUP_LOG = () => import_path38.default.join(import_os36.default.homedir(), ".node9", "daemon-startup.log");
19178
19508
  MAX_STARTUP_LOG_BYTES = 256 * 1024;
19509
+ DAEMON_STARTUP_STATE = () => import_path38.default.join(import_os36.default.homedir(), ".node9", "daemon-startup-state.json");
19510
+ MAX_DETAIL = 200;
19511
+ STARTING_GRACE_MS = 90 * 1e3;
19179
19512
  }
19180
19513
  });
19181
19514
 
19182
19515
  // src/daemon/server.ts
19516
+ function buildDaemonReport(allEntries, period, now) {
19517
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
19518
+ let start = new Date(todayStart);
19519
+ if (period === "7d") start.setDate(start.getDate() - 6);
19520
+ else if (period === "30d") start.setDate(start.getDate() - 29);
19521
+ else if (period === "month") start = new Date(now.getFullYear(), now.getMonth(), 1);
19522
+ const entries = allEntries.filter((e) => {
19523
+ if (e.source === "post-hook" || e.source === "response-dlp") return false;
19524
+ return new Date(e.ts) >= start;
19525
+ });
19526
+ const isBlocked = (e) => classifyDecision(e).outcome === "deny";
19527
+ const checkedBy = (e) => typeof e.checkedBy === "string" ? e.checkedBy : void 0;
19528
+ const summary = {
19529
+ total: entries.length,
19530
+ // The inline `startsWith('allow')` this replaces counted every non-allow
19531
+ // row as "blocked" — so DLP findings, MCP-discovery events and, worst, all
19532
+ // the observe-mode "would have blocked" rows inflated the blocked count
19533
+ // with things that were never refusals.
19534
+ allowed: entries.filter((e) => classifyDecision(e).outcome === "allow").length,
19535
+ blocked: entries.filter(isBlocked).length,
19536
+ dlp: entries.filter((e) => checkedBy(e)?.includes("dlp")).length,
19537
+ loops: entries.filter((e) => checkedBy(e) === "loop-detected").length
19538
+ };
19539
+ const dailyMap = /* @__PURE__ */ new Map();
19540
+ if (period === "today") {
19541
+ for (let h = 0; h < 24; h++) {
19542
+ const key = String(h).padStart(2, "0") + ":00";
19543
+ dailyMap.set(key, { date: key, calls: 0, blocked: 0 });
19544
+ }
19545
+ for (const e of entries) {
19546
+ const hour = new Date(e.ts).getHours();
19547
+ const key = String(hour).padStart(2, "0") + ":00";
19548
+ const d = dailyMap.get(key);
19549
+ d.calls++;
19550
+ if (isBlocked(e)) d.blocked++;
19551
+ }
19552
+ } else {
19553
+ for (const e of entries) {
19554
+ const date = e.ts.slice(0, 10);
19555
+ const d = dailyMap.get(date) || { date, calls: 0, blocked: 0 };
19556
+ d.calls++;
19557
+ if (isBlocked(e)) d.blocked++;
19558
+ dailyMap.set(date, d);
19559
+ }
19560
+ }
19561
+ const topToolsMap = /* @__PURE__ */ new Map();
19562
+ const topBlockedMap = /* @__PURE__ */ new Map();
19563
+ for (const e of entries) {
19564
+ const tool = String(e.tool ?? "");
19565
+ topToolsMap.set(tool, (topToolsMap.get(tool) || 0) + 1);
19566
+ if (isBlocked(e)) topBlockedMap.set(tool, (topBlockedMap.get(tool) || 0) + 1);
19567
+ }
19568
+ const top5 = (m) => [...m.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, value]) => ({ name, value }));
19569
+ const agentMap = /* @__PURE__ */ new Map();
19570
+ for (const e of entries) {
19571
+ const key = e.agent || "unknown";
19572
+ const a = agentMap.get(key) ?? { agent: key, total: 0, blocked: 0, dlp: 0 };
19573
+ a.total++;
19574
+ if (isBlocked(e)) a.blocked++;
19575
+ if (checkedBy(e)?.includes("dlp")) a.dlp++;
19576
+ agentMap.set(key, a);
19577
+ }
19578
+ return {
19579
+ summary,
19580
+ daily: [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date)),
19581
+ topTools: top5(topToolsMap),
19582
+ topBlockedTools: top5(topBlockedMap),
19583
+ byAgent: [...agentMap.values()].sort((a, b) => b.total - a.total)
19584
+ };
19585
+ }
19183
19586
  function startDaemon() {
19184
19587
  try {
19185
19588
  startCostSync();
@@ -19194,6 +19597,7 @@ function startDaemon() {
19194
19597
  const stack = err2 instanceof Error ? err2.stack ?? err2.message : String(err2);
19195
19598
  console.error("\n\u{1F6D1} Node9 daemon startup failed:\n" + stack);
19196
19599
  logDaemonStartup("startup-throw", err2 instanceof Error ? err2.message : String(err2));
19600
+ recordStartupState("failed", "startup-throw", err2 instanceof Error ? err2.message : String(err2));
19197
19601
  process.exit(1);
19198
19602
  }
19199
19603
  const internalToken = (0, import_crypto11.randomUUID)();
@@ -19566,6 +19970,10 @@ data: ${JSON.stringify(item.data)}
19566
19970
  return res.end(JSON.stringify({ error: "internal" }));
19567
19971
  }
19568
19972
  }
19973
+ if (req.method === "GET" && pathname === "/approver") {
19974
+ res.writeHead(200, { "Content-Type": "application/json" });
19975
+ return res.end(JSON.stringify({ interactive: hasInteractiveClient() }));
19976
+ }
19569
19977
  if (req.method === "GET" && pathname === "/state/check") {
19570
19978
  const predicatesParam = reqUrl.searchParams.get("predicates") ?? "";
19571
19979
  const predicates = predicatesParam.split(",").filter(Boolean);
@@ -19667,75 +20075,8 @@ data: ${JSON.stringify(item.data)}
19667
20075
  return [];
19668
20076
  }
19669
20077
  });
19670
- const now = /* @__PURE__ */ new Date();
19671
- const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
19672
- let start = new Date(todayStart);
19673
- if (period === "7d") start.setDate(start.getDate() - 6);
19674
- else if (period === "30d") start.setDate(start.getDate() - 29);
19675
- else if (period === "month") start = new Date(now.getFullYear(), now.getMonth(), 1);
19676
- const entries = allEntries.filter((e) => {
19677
- if (e.source === "post-hook" || e.source === "response-dlp") return false;
19678
- return new Date(e.ts) >= start;
19679
- });
19680
- const summary = {
19681
- total: entries.length,
19682
- allowed: entries.filter((e) => e.decision && e.decision.startsWith("allow")).length,
19683
- blocked: entries.filter((e) => e.decision && !e.decision.startsWith("allow")).length,
19684
- dlp: entries.filter((e) => e.checkedBy && e.checkedBy.includes("dlp")).length,
19685
- loops: entries.filter((e) => e.checkedBy === "loop-detected").length
19686
- };
19687
- const dailyMap = /* @__PURE__ */ new Map();
19688
- if (period === "today") {
19689
- for (let h = 0; h < 24; h++) {
19690
- const key = String(h).padStart(2, "0") + ":00";
19691
- dailyMap.set(key, { date: key, calls: 0, blocked: 0 });
19692
- }
19693
- for (const e of entries) {
19694
- const hour = new Date(e.ts).getHours();
19695
- const key = String(hour).padStart(2, "0") + ":00";
19696
- const d = dailyMap.get(key);
19697
- d.calls++;
19698
- if (e.decision && !e.decision.startsWith("allow")) d.blocked++;
19699
- }
19700
- } else {
19701
- for (const e of entries) {
19702
- const date = e.ts.slice(0, 10);
19703
- const d = dailyMap.get(date) || { date, calls: 0, blocked: 0 };
19704
- d.calls++;
19705
- if (e.decision && !e.decision.startsWith("allow")) d.blocked++;
19706
- dailyMap.set(date, d);
19707
- }
19708
- }
19709
- const topToolsMap = /* @__PURE__ */ new Map();
19710
- const topBlockedMap = /* @__PURE__ */ new Map();
19711
- for (const e of entries) {
19712
- topToolsMap.set(e.tool, (topToolsMap.get(e.tool) || 0) + 1);
19713
- if (e.decision && !e.decision.startsWith("allow")) {
19714
- topBlockedMap.set(e.tool, (topBlockedMap.get(e.tool) || 0) + 1);
19715
- }
19716
- }
19717
- const topTools = [...topToolsMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, value]) => ({ name, value }));
19718
- const topBlockedTools = [...topBlockedMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, value]) => ({ name, value }));
19719
- const agentMap = /* @__PURE__ */ new Map();
19720
- for (const e of entries) {
19721
- const key = e.agent || "unknown";
19722
- const a = agentMap.get(key) ?? { agent: key, total: 0, blocked: 0, dlp: 0 };
19723
- a.total++;
19724
- if (e.decision && !e.decision.startsWith("allow")) a.blocked++;
19725
- if (e.checkedBy?.includes("dlp")) a.dlp++;
19726
- agentMap.set(key, a);
19727
- }
19728
- const byAgent = [...agentMap.values()].sort((a, b) => b.total - a.total);
19729
20078
  res.writeHead(200, { "Content-Type": "application/json" });
19730
- return res.end(
19731
- JSON.stringify({
19732
- summary,
19733
- daily: [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date)),
19734
- topTools,
19735
- topBlockedTools,
19736
- byAgent
19737
- })
19738
- );
20079
+ return res.end(JSON.stringify(buildDaemonReport(allEntries, period, /* @__PURE__ */ new Date())));
19739
20080
  } catch {
19740
20081
  res.writeHead(500, { "Content-Type": "application/json" });
19741
20082
  return res.end(JSON.stringify({ error: "Failed to parse report" }));
@@ -20038,6 +20379,23 @@ data: ${JSON.stringify(item.data)}
20038
20379
  res.writeHead(404).end();
20039
20380
  });
20040
20381
  setDaemonServer(server);
20382
+ let bindAttempts = 0;
20383
+ const MAX_BIND_ATTEMPTS = 3;
20384
+ function retryListen() {
20385
+ if (++bindAttempts >= MAX_BIND_ATTEMPTS) {
20386
+ logDaemonStartup(
20387
+ "port-unavailable",
20388
+ `:${DAEMON_PORT} is held by something that is not a node9 daemon`
20389
+ );
20390
+ recordStartupState(
20391
+ "failed",
20392
+ "port-unavailable",
20393
+ `:${DAEMON_PORT} is held by another process that is not a node9 daemon \u2014 free the port, then: node9 daemon --background`
20394
+ );
20395
+ return process.exit(0);
20396
+ }
20397
+ server.listen(DAEMON_PORT, DAEMON_HOST);
20398
+ }
20041
20399
  server.on("error", (e) => {
20042
20400
  if (e.code === "EADDRINUSE") {
20043
20401
  try {
@@ -20045,6 +20403,7 @@ data: ${JSON.stringify(item.data)}
20045
20403
  const { pid } = JSON.parse(import_fs40.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
20046
20404
  process.kill(pid, 0);
20047
20405
  logDaemonStartup("port-in-use", `another daemon (pid ${pid}) owns :${DAEMON_PORT}`);
20406
+ recordStartupState("ok-elsewhere");
20048
20407
  return process.exit(0);
20049
20408
  }
20050
20409
  } catch {
@@ -20052,13 +20411,14 @@ data: ${JSON.stringify(item.data)}
20052
20411
  import_fs40.default.unlinkSync(DAEMON_PID_FILE);
20053
20412
  } catch {
20054
20413
  }
20055
- server.listen(DAEMON_PORT, DAEMON_HOST);
20414
+ retryListen();
20056
20415
  return;
20057
20416
  }
20058
20417
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/settings`, {
20059
20418
  signal: AbortSignal.timeout(1e3)
20060
20419
  }).then((res) => {
20061
20420
  if (res.ok) {
20421
+ let adopted = false;
20062
20422
  try {
20063
20423
  let orphanPid = null;
20064
20424
  const ss = (0, import_child_process2.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
@@ -20086,19 +20446,38 @@ data: ${JSON.stringify(item.data)}
20086
20446
  JSON.stringify({ pid: orphanPid, port: DAEMON_PORT, internalToken, autoStarted }),
20087
20447
  { mode: 384 }
20088
20448
  );
20449
+ adopted = true;
20089
20450
  }
20090
20451
  } catch {
20091
20452
  }
20453
+ if (adopted) {
20454
+ logDaemonStartup(
20455
+ "port-in-use-orphan",
20456
+ `adopted the daemon already on :${DAEMON_PORT}`
20457
+ );
20458
+ recordStartupState("ok-elsewhere");
20459
+ } else {
20460
+ logDaemonStartup(
20461
+ "orphan-unidentified",
20462
+ `healthy daemon on :${DAEMON_PORT} could not be identified \u2014 no pid file written`
20463
+ );
20464
+ recordStartupState(
20465
+ "failed",
20466
+ "orphan-unidentified",
20467
+ `a healthy daemon is running on :${DAEMON_PORT} but its process could not be identified, so node9 cannot track it \u2014 install \`ss\` or \`lsof\`, or restart it with: node9 daemon --background`
20468
+ );
20469
+ }
20092
20470
  process.exit(0);
20093
20471
  } else {
20094
- server.listen(DAEMON_PORT, DAEMON_HOST);
20472
+ retryListen();
20095
20473
  }
20096
20474
  }).catch(() => {
20097
- server.listen(DAEMON_PORT, DAEMON_HOST);
20475
+ retryListen();
20098
20476
  });
20099
20477
  return;
20100
20478
  }
20101
20479
  logDaemonStartup("bind-failed", e.message);
20480
+ recordStartupState("failed", "bind-failed", e.message);
20102
20481
  console.error(import_chalk6.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
20103
20482
  process.exit(1);
20104
20483
  });
@@ -20116,6 +20495,8 @@ data: ${JSON.stringify(item.data)}
20116
20495
  { mode: 384 }
20117
20496
  );
20118
20497
  console.error(import_chalk6.default.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE on 127.0.0.1:${DAEMON_PORT}`));
20498
+ logDaemonStartup("ok", `listening on ${DAEMON_HOST}:${DAEMON_PORT}`);
20499
+ recordStartupState("ok");
20119
20500
  });
20120
20501
  if (watchMode) {
20121
20502
  console.error(import_chalk6.default.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
@@ -20141,6 +20522,7 @@ var init_server = __esm({
20141
20522
  init_costSync();
20142
20523
  init_sync();
20143
20524
  init_audit_shipper();
20525
+ init_decision();
20144
20526
  init_dlp_scanner();
20145
20527
  init_mcp_reconciler();
20146
20528
  init_hook_heal();
@@ -45343,19 +45725,19 @@ function getModelContextLimit(model) {
45343
45725
  }
45344
45726
  function readSessionUsage() {
45345
45727
  const projectsDir = import_path67.default.join(import_os60.default.homedir(), ".claude", "projects");
45346
- if (!import_fs70.default.existsSync(projectsDir)) return null;
45728
+ if (!import_fs71.default.existsSync(projectsDir)) return null;
45347
45729
  let latestFile = null;
45348
45730
  let latestMtime = 0;
45349
45731
  try {
45350
- for (const dir of import_fs70.default.readdirSync(projectsDir)) {
45732
+ for (const dir of import_fs71.default.readdirSync(projectsDir)) {
45351
45733
  const dirPath = import_path67.default.join(projectsDir, dir);
45352
45734
  try {
45353
- if (!import_fs70.default.statSync(dirPath).isDirectory()) continue;
45354
- for (const file of import_fs70.default.readdirSync(dirPath)) {
45735
+ if (!import_fs71.default.statSync(dirPath).isDirectory()) continue;
45736
+ for (const file of import_fs71.default.readdirSync(dirPath)) {
45355
45737
  if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
45356
45738
  const filePath = import_path67.default.join(dirPath, file);
45357
45739
  try {
45358
- const mtime = import_fs70.default.statSync(filePath).mtimeMs;
45740
+ const mtime = import_fs71.default.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 = import_fs70.default.readFileSync(latestFile, "utf-8").split("\n");
45755
+ const lines = import_fs71.default.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 (import_fs70.default.existsSync(PID_FILE)) {
45855
+ if (import_fs71.default.existsSync(PID_FILE)) {
45474
45856
  try {
45475
- const { port } = JSON.parse(import_fs70.default.readFileSync(PID_FILE, "utf-8"));
45857
+ const { port } = JSON.parse(import_fs71.default.readFileSync(PID_FILE, "utf-8"));
45476
45858
  pidPort = port;
45477
45859
  } catch {
45478
45860
  console.error(import_chalk40.default.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(import_chalk40.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
45872
+ const startupFd = openStartupLogFd();
45873
+ recordStartupState("starting");
45490
45874
  const child = (0, import_child_process14.spawn)(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
+ import_fs71.default.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 = import_path67.default.join(import_os60.default.homedir(), ".node9", "config.json");
45632
46023
  try {
45633
- const raw = JSON.parse(import_fs70.default.readFileSync(configPath, "utf-8"));
46024
+ const raw = JSON.parse(import_fs71.default.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 = import_path67.default.join(import_os60.default.homedir(), ".node9", "config.json");
45650
46041
  try {
45651
- const raw = JSON.parse(import_fs70.default.readFileSync(configPath, "utf-8"));
46042
+ const raw = JSON.parse(import_fs71.default.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
- import_fs70.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
46048
+ import_fs71.default.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
- import_fs70.default.appendFileSync(
46220
+ import_fs71.default.appendFileSync(
45830
46221
  import_path67.default.join(import_os60.default.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 = import_path67.default.join(import_os60.default.homedir(), ".node9", "audit.log");
45895
46286
  try {
45896
- const unackedDlp = import_fs70.default.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
46287
+ const unackedDlp = import_fs71.default.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
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 = import_fs70.default.statSync(auditLog).mtimeMs;
46327
+ const auditMtime = import_fs71.default.statSync(auditLog).mtimeMs;
45937
46328
  if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
45938
46329
  console.log("");
45939
46330
  console.log(
@@ -46118,17 +46509,18 @@ async function startTail(options = {}) {
46118
46509
  process.exit(1);
46119
46510
  });
46120
46511
  }
46121
- var import_http5, import_chalk40, import_fs70, import_os60, import_path67, import_readline6, import_child_process14, PID_FILE, ICONS, MODEL_CONTEXT_LIMITS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
46512
+ var import_http5, import_chalk40, import_fs71, import_os60, import_path67, import_readline6, import_child_process14, PID_FILE, ICONS, MODEL_CONTEXT_LIMITS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
46122
46513
  var init_tail = __esm({
46123
46514
  "src/tui/tail.ts"() {
46124
46515
  "use strict";
46125
46516
  import_http5 = __toESM(require("http"));
46126
46517
  import_chalk40 = __toESM(require("chalk"));
46127
- import_fs70 = __toESM(require("fs"));
46518
+ import_fs71 = __toESM(require("fs"));
46128
46519
  import_os60 = __toESM(require("os"));
46129
46520
  import_path67 = __toESM(require("path"));
46130
46521
  import_readline6 = __toESM(require("readline"));
46131
46522
  import_child_process14 = require("child_process");
46523
+ init_startup_log();
46132
46524
  init_daemon2();
46133
46525
  init_daemon();
46134
46526
  PID_FILE = import_path67.default.join(import_os60.default.homedir(), ".node9", "daemon.pid");
@@ -46253,9 +46645,9 @@ function formatTimeLeft(resetsAt) {
46253
46645
  return ` (${m}m left)`;
46254
46646
  }
46255
46647
  function safeReadJson(filePath) {
46256
- if (!import_fs71.default.existsSync(filePath)) return null;
46648
+ if (!import_fs72.default.existsSync(filePath)) return null;
46257
46649
  try {
46258
- return JSON.parse(import_fs71.default.readFileSync(filePath, "utf-8"));
46650
+ return JSON.parse(import_fs72.default.readFileSync(filePath, "utf-8"));
46259
46651
  } catch {
46260
46652
  return null;
46261
46653
  }
@@ -46276,10 +46668,10 @@ function countHooksInFile(filePath) {
46276
46668
  return Object.keys(cfg.hooks).length;
46277
46669
  }
46278
46670
  function countRulesInDir(rulesDir) {
46279
- if (!import_fs71.default.existsSync(rulesDir)) return 0;
46671
+ if (!import_fs72.default.existsSync(rulesDir)) return 0;
46280
46672
  let count = 0;
46281
46673
  try {
46282
- for (const entry of import_fs71.default.readdirSync(rulesDir, { withFileTypes: true })) {
46674
+ for (const entry of import_fs72.default.readdirSync(rulesDir, { withFileTypes: true })) {
46283
46675
  if (entry.isDirectory()) {
46284
46676
  count += countRulesInDir(import_path68.default.join(rulesDir, entry.name));
46285
46677
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -46305,7 +46697,7 @@ function countConfigs(cwd) {
46305
46697
  let hooksCount = 0;
46306
46698
  const userMcpServers = /* @__PURE__ */ new Set();
46307
46699
  const projectMcpServers = /* @__PURE__ */ new Set();
46308
- if (import_fs71.default.existsSync(import_path68.default.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
46700
+ if (import_fs72.default.existsSync(import_path68.default.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
46309
46701
  rulesCount += countRulesInDir(import_path68.default.join(claudeDir, "rules"));
46310
46702
  const userSettings = import_path68.default.join(claudeDir, "settings.json");
46311
46703
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
@@ -46316,18 +46708,18 @@ function countConfigs(cwd) {
46316
46708
  userMcpServers.delete(name);
46317
46709
  }
46318
46710
  if (cwd) {
46319
- if (import_fs71.default.existsSync(import_path68.default.join(cwd, "CLAUDE.md"))) claudeMdCount++;
46320
- if (import_fs71.default.existsSync(import_path68.default.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
46711
+ if (import_fs72.default.existsSync(import_path68.default.join(cwd, "CLAUDE.md"))) claudeMdCount++;
46712
+ if (import_fs72.default.existsSync(import_path68.default.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
46321
46713
  const projectClaudeDir = import_path68.default.join(cwd, ".claude");
46322
46714
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
46323
46715
  if (!overlapsUserScope) {
46324
- if (import_fs71.default.existsSync(import_path68.default.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
46716
+ if (import_fs72.default.existsSync(import_path68.default.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
46325
46717
  rulesCount += countRulesInDir(import_path68.default.join(projectClaudeDir, "rules"));
46326
46718
  const projSettings = import_path68.default.join(projectClaudeDir, "settings.json");
46327
46719
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
46328
46720
  hooksCount += countHooksInFile(projSettings);
46329
46721
  }
46330
- if (import_fs71.default.existsSync(import_path68.default.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
46722
+ if (import_fs72.default.existsSync(import_path68.default.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
46331
46723
  const localSettings = import_path68.default.join(projectClaudeDir, "settings.local.json");
46332
46724
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
46333
46725
  hooksCount += countHooksInFile(localSettings);
@@ -46365,11 +46757,11 @@ function readActiveShieldsHud() {
46365
46757
  }
46366
46758
  try {
46367
46759
  const shieldsPath = import_path68.default.join(import_os61.default.homedir(), ".node9", "shields.json");
46368
- if (!import_fs71.default.existsSync(shieldsPath)) {
46760
+ if (!import_fs72.default.existsSync(shieldsPath)) {
46369
46761
  shieldsCache = { value: [], ts: now };
46370
46762
  return [];
46371
46763
  }
46372
- const parsed = JSON.parse(import_fs71.default.readFileSync(shieldsPath, "utf-8"));
46764
+ const parsed = JSON.parse(import_fs72.default.readFileSync(shieldsPath, "utf-8"));
46373
46765
  if (!Array.isArray(parsed.active)) {
46374
46766
  shieldsCache = { value: [], ts: now };
46375
46767
  return [];
@@ -46471,17 +46863,17 @@ function renderContextLine(stdin) {
46471
46863
  async function main() {
46472
46864
  try {
46473
46865
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
46474
- if (import_fs71.default.existsSync(import_path68.default.join(import_os61.default.homedir(), ".node9", "hud-debug"))) {
46866
+ if (import_fs72.default.existsSync(import_path68.default.join(import_os61.default.homedir(), ".node9", "hud-debug"))) {
46475
46867
  try {
46476
46868
  const logPath = import_path68.default.join(import_os61.default.homedir(), ".node9", "hud-debug.log");
46477
46869
  const MAX_LOG_SIZE = 10 * 1024 * 1024;
46478
46870
  let size = 0;
46479
46871
  try {
46480
- size = import_fs71.default.statSync(logPath).size;
46872
+ size = import_fs72.default.statSync(logPath).size;
46481
46873
  } catch {
46482
46874
  }
46483
46875
  if (size < MAX_LOG_SIZE) {
46484
- import_fs71.default.appendFileSync(
46876
+ import_fs72.default.appendFileSync(
46485
46877
  logPath,
46486
46878
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
46487
46879
  );
@@ -46505,8 +46897,8 @@ async function main() {
46505
46897
  import_path68.default.join(cwd, "node9.config.json"),
46506
46898
  import_path68.default.join(import_os61.default.homedir(), ".node9", "config.json")
46507
46899
  ]) {
46508
- if (!import_fs71.default.existsSync(configPath)) continue;
46509
- const cfg = JSON.parse(import_fs71.default.readFileSync(configPath, "utf-8"));
46900
+ if (!import_fs72.default.existsSync(configPath)) continue;
46901
+ const cfg = JSON.parse(import_fs72.default.readFileSync(configPath, "utf-8"));
46510
46902
  const hud = cfg.settings?.hud;
46511
46903
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
46512
46904
  }
@@ -46524,11 +46916,11 @@ async function main() {
46524
46916
  renderOffline();
46525
46917
  }
46526
46918
  }
46527
- var import_fs71, import_path68, import_os61, import_http6, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
46919
+ var import_fs72, import_path68, import_os61, import_http6, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
46528
46920
  var init_hud = __esm({
46529
46921
  "src/cli/hud.ts"() {
46530
46922
  "use strict";
46531
- import_fs71 = __toESM(require("fs"));
46923
+ import_fs72 = __toESM(require("fs"));
46532
46924
  import_path68 = __toESM(require("path"));
46533
46925
  import_os61 = __toESM(require("os"));
46534
46926
  import_http6 = __toESM(require("http"));
@@ -46652,7 +47044,7 @@ function writeCredentialsAndConfig(apiKey, opts = {}) {
46652
47044
  // src/cli.ts
46653
47045
  init_daemon2();
46654
47046
  var import_chalk41 = __toESM(require("chalk"));
46655
- var import_fs72 = __toESM(require("fs"));
47047
+ var import_fs73 = __toESM(require("fs"));
46656
47048
  var import_path69 = __toESM(require("path"));
46657
47049
  var import_os62 = __toESM(require("os"));
46658
47050
  var import_child_process15 = require("child_process");
@@ -46858,9 +47250,11 @@ function logAutostartSkipThrottled(reason) {
46858
47250
  if (Date.now() - import_fs43.default.statSync(stamp).mtimeMs < SKIP_THROTTLE_MS) return;
46859
47251
  } catch {
46860
47252
  }
47253
+ const dir = import_path41.default.join(import_os39.default.homedir(), ".node9");
47254
+ if (!import_fs43.default.existsSync(dir)) import_fs43.default.mkdirSync(dir, { recursive: true });
46861
47255
  import_fs43.default.writeFileSync(stamp, "", "utf-8");
46862
47256
  import_fs43.default.appendFileSync(
46863
- import_path41.default.join(import_os39.default.homedir(), ".node9", "hook-debug.log"),
47257
+ import_path41.default.join(dir, "hook-debug.log"),
46864
47258
  `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-skip: ${reason}
46865
47259
  `,
46866
47260
  "utf-8"
@@ -46879,6 +47273,8 @@ async function autoStartDaemonAndWait() {
46879
47273
  }
46880
47274
  if (!resolvedArgv1.endsWith(".js")) return false;
46881
47275
  const startupFd = openStartupLogFd();
47276
+ recordStartupState("starting");
47277
+ let spawned = false;
46882
47278
  try {
46883
47279
  const child = (0, import_child_process5.spawn)(process.execPath, [resolvedArgv1, "daemon"], {
46884
47280
  detached: true,
@@ -46888,13 +47284,24 @@ async function autoStartDaemonAndWait() {
46888
47284
  NODE9_AUTO_STARTED: "1"
46889
47285
  }
46890
47286
  });
47287
+ child.on("error", (err2) => {
47288
+ if (readStartupState()?.outcome !== "starting") return;
47289
+ recordStartupState("failed", "spawn-failed", err2.message);
47290
+ });
46891
47291
  child.unref();
47292
+ spawned = true;
46892
47293
  for (let i = 0; i < 20; i++) {
46893
47294
  await new Promise((r) => setTimeout(r, 250));
46894
47295
  if (!isDaemonRunning()) continue;
46895
47296
  if (await isDaemonReachable()) return true;
46896
47297
  }
46897
- } catch {
47298
+ } catch (err2) {
47299
+ if (!spawned)
47300
+ recordStartupState(
47301
+ "failed",
47302
+ "spawn-failed",
47303
+ err2 instanceof Error ? err2.message : String(err2)
47304
+ );
46898
47305
  } finally {
46899
47306
  if (startupFd !== void 0) {
46900
47307
  try {
@@ -47707,12 +48114,16 @@ RAW: ${raw}
47707
48114
  delete safeEnv[key];
47708
48115
  }
47709
48116
  const startupFd = openStartupLogFd();
48117
+ recordStartupState("starting");
47710
48118
  try {
47711
48119
  const d = (0, import_child_process7.spawn)(process.execPath, [scriptPath, "daemon"], {
47712
48120
  detached: true,
47713
48121
  stdio: ["ignore", "ignore", startupFd ?? "ignore"],
47714
48122
  env: { ...safeEnv, NODE9_AUTO_STARTED: "1" }
47715
48123
  });
48124
+ d.on("error", (err2) => {
48125
+ recordStartupState("failed", "spawn-failed", err2.message);
48126
+ });
47716
48127
  d.unref();
47717
48128
  } finally {
47718
48129
  if (startupFd !== void 0) {
@@ -47725,6 +48136,7 @@ RAW: ${raw}
47725
48136
  } catch (spawnErr) {
47726
48137
  const logPath = import_path45.default.join(import_os43.default.homedir(), ".node9", "hook-debug.log");
47727
48138
  const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
48139
+ recordStartupState("failed", "spawn-aborted", msg);
47728
48140
  try {
47729
48141
  import_fs47.default.appendFileSync(
47730
48142
  logPath,
@@ -48889,6 +49301,7 @@ function agoLabel(iso, now = Date.now()) {
48889
49301
  }
48890
49302
 
48891
49303
  // src/cli/commands/doctor.ts
49304
+ init_startup_log();
48892
49305
  function registerDoctorCommand(program2, version2) {
48893
49306
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(async () => {
48894
49307
  const homeDir2 = import_os46.default.homedir();
@@ -49000,6 +49413,15 @@ function registerDoctorCommand(program2, version2) {
49000
49413
  "Daemon not running \u2014 terminal & native approvals unavailable",
49001
49414
  "Run: node9 daemon --background"
49002
49415
  );
49416
+ const cause = readStartupCause();
49417
+ if (cause) {
49418
+ const suffix = cause.detail ? ` \u2014 ${cause.detail}` : "";
49419
+ console.log(
49420
+ import_chalk11.default.gray(
49421
+ ` ${cause.label} ${agoLabel(cause.at.toISOString())}: ${cause.kind}${suffix}`
49422
+ )
49423
+ );
49424
+ }
49003
49425
  }
49004
49426
  const autostart = autostartAdvice({
49005
49427
  installed: isDaemonServiceInstalled(),
@@ -49077,6 +49499,7 @@ function registerDoctorCommand(program2, version2) {
49077
49499
  var import_chalk12 = __toESM(require("chalk"));
49078
49500
  var import_fs52 = __toESM(require("fs"));
49079
49501
  var import_path50 = __toESM(require("path"));
49502
+ init_decision();
49080
49503
  var import_os47 = __toESM(require("os"));
49081
49504
  function formatRelativeTime(timestamp) {
49082
49505
  const diff = Date.now() - new Date(timestamp).getTime();
@@ -49108,10 +49531,16 @@ function registerAuditCommand(program2) {
49108
49531
  });
49109
49532
  entries = entries.map((e) => ({
49110
49533
  ...e,
49111
- decision: String(e.decision).startsWith("allow") ? "allow" : "deny"
49534
+ // classifyDecision is the ONE mapper (audit/decision.ts). The inline
49535
+ // `startsWith('allow') ? allow : deny` this replaces bucketed `dlp` and
49536
+ // `mcp-discovered` — findings, not verdicts — as DENY, inventing
49537
+ // refusals that never happened.
49538
+ // Pass the ROW, never a field pair — the attribution key differs by
49539
+ // producer (`checkedBy` from the gate, `source` from the hook/daemon).
49540
+ view: classifyDecision(e)
49112
49541
  }));
49113
49542
  if (options.tool) entries = entries.filter((e) => String(e.tool).includes(options.tool));
49114
- if (options.deny) entries = entries.filter((e) => e.decision === "deny");
49543
+ if (options.deny) entries = entries.filter((e) => e.view.outcome === "deny");
49115
49544
  const limit = Math.max(1, parseInt(options.tail, 10) || 20);
49116
49545
  entries = entries.slice(-limit);
49117
49546
  if (options.json) {
@@ -49134,13 +49563,13 @@ function registerAuditCommand(program2) {
49134
49563
  for (const e of entries) {
49135
49564
  const time = formatRelativeTime(String(e.ts)).padEnd(12);
49136
49565
  const tool = String(e.tool).slice(0, 17).padEnd(18);
49137
- const result = e.decision === "allow" ? import_chalk12.default.green("ALLOW".padEnd(10)) : import_chalk12.default.red("DENY".padEnd(10));
49566
+ const result = e.view.outcome === "allow" ? import_chalk12.default.green(e.view.label.padEnd(14)) : e.view.outcome === "deny" ? import_chalk12.default.red(e.view.label.padEnd(14)) : e.view.outcome === "observe" ? import_chalk12.default.yellow(e.view.label.padEnd(14)) : import_chalk12.default.gray(e.view.label.padEnd(14));
49138
49567
  const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
49139
49568
  const agent = String(e.agent || "unknown");
49140
49569
  console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
49141
49570
  }
49142
- const allowed = entries.filter((e) => e.decision === "allow").length;
49143
- const denied = entries.filter((e) => e.decision === "deny").length;
49571
+ const allowed = entries.filter((e) => e.view.outcome === "allow").length;
49572
+ const denied = entries.filter((e) => e.view.outcome === "deny").length;
49144
49573
  console.log(import_chalk12.default.dim(" " + "\u2500".repeat(65)));
49145
49574
  console.log(
49146
49575
  ` ${entries.length} entries | ${import_chalk12.default.green(allowed + " allowed")} | ${import_chalk12.default.red(denied + " denied")}
@@ -49159,6 +49588,7 @@ var import_path51 = __toESM(require("path"));
49159
49588
  init_costSync();
49160
49589
  init_litellm();
49161
49590
  init_cost_codex();
49591
+ init_decision();
49162
49592
  var TEST_COMMAND_RE3 = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
49163
49593
  function buildTestTimestamps(allEntries) {
49164
49594
  const testTs = /* @__PURE__ */ new Set();
@@ -49249,8 +49679,15 @@ function parseAuditLog(logPath) {
49249
49679
  }
49250
49680
  });
49251
49681
  }
49252
- function isAllow(decision) {
49253
- return decision.startsWith("allow");
49682
+ function viewOf(e) {
49683
+ const outcome = classifyDecision(e).outcome;
49684
+ const observed = outcome === "observe";
49685
+ return {
49686
+ observed,
49687
+ ran: outcome === "allow" || observed,
49688
+ blocked: outcome === "deny" || outcome === "unknown",
49689
+ info: outcome === "info"
49690
+ };
49254
49691
  }
49255
49692
  function isDlp(checkedBy) {
49256
49693
  return !!checkedBy?.includes("dlp");
@@ -49663,12 +50100,12 @@ function aggregateReportFromAudit(period, opts = {}) {
49663
50100
  const priorStart = new Date(start.getTime() - periodMs);
49664
50101
  const priorEntries = allEntries.filter((e) => {
49665
50102
  if (e.source === "post-hook") return false;
50103
+ if (e.source === "response-dlp") return false;
50104
+ if (typeof e.decision !== "string") return false;
49666
50105
  const ts = new Date(e.ts);
49667
50106
  return ts >= priorStart && ts <= priorEnd;
49668
50107
  });
49669
- const priorBlocked = priorEntries.filter(
49670
- (e) => typeof e.decision === "string" && !isAllow(e.decision)
49671
- ).length;
50108
+ const priorBlocked = priorEntries.filter((e) => viewOf(e).blocked).length;
49672
50109
  const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
49673
50110
  const excludeTests = opts.excludeTests === true;
49674
50111
  const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
@@ -49711,15 +50148,17 @@ function aggregateReportFromAudit(period, opts = {}) {
49711
50148
  let dimInjectionBlocked = 0;
49712
50149
  for (const e of entries) {
49713
50150
  if (superseded.has(supersedeKey(e))) continue;
49714
- const allow = isAllow(e.decision);
50151
+ const view = viewOf(e);
49715
50152
  const dateKey = e.ts.slice(0, 10);
49716
50153
  const userInteracted = e.source === "daemon";
49717
- if (userInteracted) {
49718
- if (allow) userApproved++;
50154
+ if (view.observed) {
50155
+ if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
50156
+ } else if (view.info) {
50157
+ } else if (userInteracted) {
50158
+ if (view.ran) userApproved++;
49719
50159
  else userDenied++;
49720
- } else if (!allow) {
50160
+ } else if (view.blocked) {
49721
50161
  if (e.checkedBy === "timeout") timedOut++;
49722
- else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
49723
50162
  else if (isDlp(e.checkedBy)) dlpBlocked++;
49724
50163
  else if (e.checkedBy === "local-decision") userDenied++;
49725
50164
  else if (e.checkedBy !== "loop-detected") hardBlocked++;
@@ -49731,7 +50170,7 @@ function aggregateReportFromAudit(period, opts = {}) {
49731
50170
  if (cb.includes("would-block") && (cb.includes("pii") || cb.includes("dlp"))) {
49732
50171
  dimDataObserved++;
49733
50172
  }
49734
- if (!allow && !userInteracted) {
50173
+ if (view.blocked && !userInteracted) {
49735
50174
  switch (dimensionOfBlock(cb, e.ruleName ?? "")) {
49736
50175
  case "network":
49737
50176
  dimNetworkBlocked++;
@@ -49749,15 +50188,15 @@ function aggregateReportFromAudit(period, opts = {}) {
49749
50188
  }
49750
50189
  const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
49751
50190
  t.calls++;
49752
- if (!allow) t.blocked++;
50191
+ if (view.blocked) t.blocked++;
49753
50192
  toolMap.set(e.tool, t);
49754
- if (!allow) {
50193
+ if (view.blocked) {
49755
50194
  const key = e.checkedBy ?? (e.source === "daemon" ? "local-decision" : null);
49756
50195
  if (key) {
49757
50196
  blockMap.set(key, (blockMap.get(key) ?? 0) + 1);
49758
50197
  }
49759
50198
  }
49760
- if (!allow && e.ruleName) {
50199
+ if ((view.blocked || view.observed) && e.ruleName) {
49761
50200
  ruleMap.set(e.ruleName, (ruleMap.get(e.ruleName) ?? 0) + 1);
49762
50201
  }
49763
50202
  if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
@@ -49766,7 +50205,7 @@ function aggregateReportFromAudit(period, opts = {}) {
49766
50205
  hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
49767
50206
  const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
49768
50207
  d.calls++;
49769
- if (!allow) d.blocked++;
50208
+ if (view.blocked) d.blocked++;
49770
50209
  dailyMap.set(dateKey, d);
49771
50210
  }
49772
50211
  for (const e of allEntries) {
@@ -50320,6 +50759,8 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
50320
50759
  // src/cli/commands/daemon-cmd.ts
50321
50760
  var import_chalk14 = __toESM(require("chalk"));
50322
50761
  var import_child_process9 = require("child_process");
50762
+ var import_fs54 = __toESM(require("fs"));
50763
+ init_startup_log();
50323
50764
  init_daemon2();
50324
50765
  var VALID_ACTIONS = "start | stop | restart | status | install | uninstall";
50325
50766
  function registerDaemonCommand(program2) {
@@ -50358,14 +50799,27 @@ function registerDaemonCommand(program2) {
50358
50799
  if (cmd === "restart") {
50359
50800
  stopDaemon();
50360
50801
  await new Promise((r) => setTimeout(r, 500));
50802
+ const restartFd = openStartupLogFd();
50803
+ recordStartupState("starting");
50361
50804
  const child = (0, import_child_process9.spawn)(process.execPath, [process.argv[1], "daemon"], {
50362
50805
  detached: true,
50363
- stdio: "ignore",
50806
+ stdio: ["ignore", "ignore", restartFd ?? "ignore"],
50364
50807
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
50365
50808
  });
50809
+ child.on(
50810
+ "error",
50811
+ (err2) => recordStartupState("failed", "spawn-failed", err2.message)
50812
+ );
50366
50813
  child.unref();
50814
+ if (restartFd !== void 0) {
50815
+ try {
50816
+ import_fs54.default.closeSync(restartFd);
50817
+ } catch {
50818
+ }
50819
+ }
50367
50820
  if (child.pid) {
50368
- console.log(import_chalk14.default.green(`\u2713 Daemon restarted (PID ${child.pid})`));
50821
+ console.log(import_chalk14.default.green(`\u2713 Daemon relaunching (PID ${child.pid})`));
50822
+ console.log(import_chalk14.default.gray(" Confirm with: node9 status"));
50369
50823
  } else {
50370
50824
  console.error(import_chalk14.default.red("\u2717 Failed to restart daemon \u2014 spawn returned no PID"));
50371
50825
  process.exit(1);
@@ -50388,13 +50842,33 @@ function registerDaemonCommand(program2) {
50388
50842
  return;
50389
50843
  }
50390
50844
  if (options.background) {
50391
- const child = (0, import_child_process9.spawn)(process.execPath, [process.argv[1], "daemon"], {
50392
- detached: true,
50393
- stdio: "ignore"
50394
- });
50395
- child.unref();
50396
- console.log(import_chalk14.default.green(`
50397
- \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
50845
+ const startupFd = openStartupLogFd();
50846
+ try {
50847
+ recordStartupState("starting");
50848
+ const child = (0, import_child_process9.spawn)(process.execPath, [process.argv[1], "daemon"], {
50849
+ detached: true,
50850
+ // Capture the child's stderr: a module-load crash prints its stack there
50851
+ // and dies before it can record anything itself.
50852
+ stdio: ["ignore", "ignore", startupFd ?? "ignore"]
50853
+ });
50854
+ child.on(
50855
+ "error",
50856
+ (err2) => recordStartupState("failed", "spawn-failed", err2.message)
50857
+ );
50858
+ child.unref();
50859
+ console.log(
50860
+ import_chalk14.default.green(`
50861
+ \u{1F6E1}\uFE0F Node9 daemon launching in background (PID ${child.pid})`)
50862
+ );
50863
+ console.log(import_chalk14.default.gray(" Confirm with: node9 status"));
50864
+ } finally {
50865
+ if (startupFd !== void 0) {
50866
+ try {
50867
+ import_fs54.default.closeSync(startupFd);
50868
+ } catch {
50869
+ }
50870
+ }
50871
+ }
50398
50872
  process.exit(0);
50399
50873
  }
50400
50874
  startDaemon();
@@ -50404,7 +50878,7 @@ function registerDaemonCommand(program2) {
50404
50878
 
50405
50879
  // src/cli/commands/status.ts
50406
50880
  var import_chalk15 = __toESM(require("chalk"));
50407
- var import_fs54 = __toESM(require("fs"));
50881
+ var import_fs55 = __toESM(require("fs"));
50408
50882
  var import_path52 = __toESM(require("path"));
50409
50883
  var import_os49 = __toESM(require("os"));
50410
50884
  init_core();
@@ -50487,10 +50961,10 @@ function registerStatusCommand(program2) {
50487
50961
  const projectConfig = import_path52.default.join(process.cwd(), "node9.config.json");
50488
50962
  const globalConfig = import_path52.default.join(import_os49.default.homedir(), ".node9", "config.json");
50489
50963
  console.log(
50490
- ` Local: ${import_fs54.default.existsSync(projectConfig) ? import_chalk15.default.green("Active (node9.config.json)") : import_chalk15.default.gray("Not present")}`
50964
+ ` Local: ${import_fs55.default.existsSync(projectConfig) ? import_chalk15.default.green("Active (node9.config.json)") : import_chalk15.default.gray("Not present")}`
50491
50965
  );
50492
50966
  console.log(
50493
- ` Global: ${import_fs54.default.existsSync(globalConfig) ? import_chalk15.default.green("Active (~/.node9/config.json)") : import_chalk15.default.gray("Not present")}`
50967
+ ` Global: ${import_fs55.default.existsSync(globalConfig) ? import_chalk15.default.green("Active (~/.node9/config.json)") : import_chalk15.default.gray("Not present")}`
50494
50968
  );
50495
50969
  if (mergedConfig.policy.sandboxPaths.length > 0) {
50496
50970
  console.log(
@@ -50532,7 +51006,7 @@ function registerStatusCommand(program2) {
50532
51006
 
50533
51007
  // src/cli/commands/init.ts
50534
51008
  var import_chalk16 = __toESM(require("chalk"));
50535
- var import_fs55 = __toESM(require("fs"));
51009
+ var import_fs56 = __toESM(require("fs"));
50536
51010
  var import_path53 = __toESM(require("path"));
50537
51011
  var import_os50 = __toESM(require("os"));
50538
51012
  var import_https6 = __toESM(require("https"));
@@ -50626,15 +51100,15 @@ function registerInitCommand(program2) {
50626
51100
  console.log("");
50627
51101
  }
50628
51102
  const configPath = import_path53.default.join(import_os50.default.homedir(), ".node9", "config.json");
50629
- const isFirstInstall = !import_fs55.default.existsSync(configPath);
50630
- if (import_fs55.default.existsSync(configPath) && !options.force) {
51103
+ const isFirstInstall = !import_fs56.default.existsSync(configPath);
51104
+ if (import_fs56.default.existsSync(configPath) && !options.force) {
50631
51105
  try {
50632
- const existing = JSON.parse(import_fs55.default.readFileSync(configPath, "utf-8"));
51106
+ const existing = JSON.parse(import_fs56.default.readFileSync(configPath, "utf-8"));
50633
51107
  const settings = existing.settings ?? {};
50634
51108
  if (settings.mode !== chosenMode) {
50635
51109
  settings.mode = chosenMode;
50636
51110
  existing.settings = settings;
50637
- import_fs55.default.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
51111
+ import_fs56.default.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
50638
51112
  console.log(import_chalk16.default.green(`\u2705 Mode updated: ${chosenMode}`));
50639
51113
  } else {
50640
51114
  console.log(import_chalk16.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
@@ -50648,8 +51122,8 @@ function registerInitCommand(program2) {
50648
51122
  settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
50649
51123
  };
50650
51124
  const dir = import_path53.default.dirname(configPath);
50651
- if (!import_fs55.default.existsSync(dir)) import_fs55.default.mkdirSync(dir, { recursive: true });
50652
- import_fs55.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
51125
+ if (!import_fs56.default.existsSync(dir)) import_fs56.default.mkdirSync(dir, { recursive: true });
51126
+ import_fs56.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
50653
51127
  console.log(import_chalk16.default.green(`\u2705 Config created: ${configPath}`));
50654
51128
  console.log(import_chalk16.default.gray(` Mode: ${chosenMode}`));
50655
51129
  }
@@ -50749,14 +51223,14 @@ function registerInitCommand(program2) {
50749
51223
 
50750
51224
  // src/cli/commands/heal.ts
50751
51225
  var import_chalk17 = __toESM(require("chalk"));
50752
- var import_fs56 = __toESM(require("fs"));
51226
+ var import_fs57 = __toESM(require("fs"));
50753
51227
  init_agent_wiring();
50754
51228
  init_setup();
50755
51229
  init_hook_baseline();
50756
51230
  var hasHookSurface = (a) => a.hooks.length > 0;
50757
51231
  function backupForHeal(file) {
50758
51232
  try {
50759
- if (file && import_fs56.default.existsSync(file)) import_fs56.default.copyFileSync(file, `${file}.node9-heal-bak`);
51233
+ if (file && import_fs57.default.existsSync(file)) import_fs57.default.copyFileSync(file, `${file}.node9-heal-bak`);
50760
51234
  } catch {
50761
51235
  }
50762
51236
  }
@@ -51716,16 +52190,17 @@ function registerMcpGatewayCommand(program2) {
51716
52190
 
51717
52191
  // src/mcp-server/index.ts
51718
52192
  var import_readline5 = __toESM(require("readline"));
51719
- var import_fs58 = __toESM(require("fs"));
52193
+ var import_fs59 = __toESM(require("fs"));
51720
52194
  var import_os52 = __toESM(require("os"));
51721
52195
  var import_path56 = __toESM(require("path"));
51722
52196
  var import_child_process11 = require("child_process");
52197
+ init_decision();
51723
52198
  init_core();
51724
52199
  init_daemon();
51725
52200
  init_shields();
51726
52201
 
51727
52202
  // src/auth/egress-config.ts
51728
- var import_fs57 = __toESM(require("fs"));
52203
+ var import_fs58 = __toESM(require("fs"));
51729
52204
  var import_os51 = __toESM(require("os"));
51730
52205
  var import_path55 = __toESM(require("path"));
51731
52206
  var DEFAULT_EGRESS = {
@@ -51741,7 +52216,7 @@ function egressConfigPath() {
51741
52216
  function readEgressRawConfig() {
51742
52217
  let text;
51743
52218
  try {
51744
- text = import_fs57.default.readFileSync(egressConfigPath(), "utf8");
52219
+ text = import_fs58.default.readFileSync(egressConfigPath(), "utf8");
51745
52220
  } catch (err2) {
51746
52221
  if (err2.code === "ENOENT") return {};
51747
52222
  throw err2;
@@ -51756,8 +52231,8 @@ function readEgressRawConfig() {
51756
52231
  }
51757
52232
  function writeEgressRawConfig(config) {
51758
52233
  const p = egressConfigPath();
51759
- import_fs57.default.mkdirSync(import_path55.default.dirname(p), { recursive: true });
51760
- import_fs57.default.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
52234
+ import_fs58.default.mkdirSync(import_path55.default.dirname(p), { recursive: true });
52235
+ import_fs58.default.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
51761
52236
  }
51762
52237
  function applyEgress(config, change) {
51763
52238
  const policy = config.policy = config.policy ?? {};
@@ -51952,7 +52427,7 @@ var TOOLS = [
51952
52427
  },
51953
52428
  {
51954
52429
  name: "node9_audit_get",
51955
- description: "Read recent entries from the node9 audit log (~/.node9/audit.log). Each entry shows timestamp, tool name, decision (allow/block/review), command/args, and agent. Use this to review what AI actions have been taken recently, especially blocked or reviewed ops.",
52430
+ description: "Read recent entries from the node9 audit log (~/.node9/audit.log). Each entry shows timestamp, outcome, tool name, the command, and the rule that fired. Outcomes use the same words as the dashboard: Auto-allowed, Approved, Ran, Blocked, Denied (a human refused), Timed out (nobody answered), Would block (shadow mode let it through), Finding, Info. Use this to review what AI actions were taken, especially refused ones.",
51956
52431
  inputSchema: {
51957
52432
  type: "object",
51958
52433
  properties: {
@@ -51962,8 +52437,8 @@ var TOOLS = [
51962
52437
  },
51963
52438
  filter: {
51964
52439
  type: "string",
51965
- enum: ["all", "block", "review"],
51966
- description: 'Filter by decision. Omit or use "all" to show every entry.'
52440
+ enum: ["all", "allow", "deny", "observe", "info", "block"],
52441
+ description: 'Filter by outcome. "deny" covers every refusal (rule block, human denial, timeout); "observe" is shadow-mode would-blocks; "block" is an alias for "deny". Omit or use "all" for every entry.'
51967
52442
  }
51968
52443
  },
51969
52444
  required: []
@@ -52145,10 +52620,10 @@ function handleStatus() {
52145
52620
  const projectConfig = import_path56.default.join(process.cwd(), "node9.config.json");
52146
52621
  const globalConfig = import_path56.default.join(import_os52.default.homedir(), ".node9", "config.json");
52147
52622
  lines.push(
52148
- `Project config (node9.config.json): ${import_fs58.default.existsSync(projectConfig) ? "present" : "not found"}`
52623
+ `Project config (node9.config.json): ${import_fs59.default.existsSync(projectConfig) ? "present" : "not found"}`
52149
52624
  );
52150
52625
  lines.push(
52151
- `Global config (~/.node9/config.json): ${import_fs58.default.existsSync(globalConfig) ? "present" : "not found"}`
52626
+ `Global config (~/.node9/config.json): ${import_fs59.default.existsSync(globalConfig) ? "present" : "not found"}`
52152
52627
  );
52153
52628
  return lines.join("\n");
52154
52629
  }
@@ -52258,8 +52733,8 @@ var GLOBAL_CONFIG_PATH = import_path56.default.join(import_os52.default.homedir(
52258
52733
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
52259
52734
  function readGlobalConfigRaw() {
52260
52735
  try {
52261
- if (import_fs58.default.existsSync(GLOBAL_CONFIG_PATH)) {
52262
- return JSON.parse(import_fs58.default.readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
52736
+ if (import_fs59.default.existsSync(GLOBAL_CONFIG_PATH)) {
52737
+ return JSON.parse(import_fs59.default.readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
52263
52738
  }
52264
52739
  } catch {
52265
52740
  }
@@ -52267,8 +52742,8 @@ function readGlobalConfigRaw() {
52267
52742
  }
52268
52743
  function writeGlobalConfigRaw(data) {
52269
52744
  const dir = import_path56.default.dirname(GLOBAL_CONFIG_PATH);
52270
- if (!import_fs58.default.existsSync(dir)) import_fs58.default.mkdirSync(dir, { recursive: true });
52271
- import_fs58.default.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(data, null, 2) + "\n");
52745
+ if (!import_fs59.default.existsSync(dir)) import_fs59.default.mkdirSync(dir, { recursive: true });
52746
+ import_fs59.default.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(data, null, 2) + "\n");
52272
52747
  }
52273
52748
  function handleApproverList() {
52274
52749
  const config = getConfig();
@@ -52313,35 +52788,37 @@ function handleAuditGet(args) {
52313
52788
  const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
52314
52789
  const filter = typeof args.filter === "string" && args.filter !== "all" ? args.filter : null;
52315
52790
  const auditPath = import_path56.default.join(import_os52.default.homedir(), ".node9", "audit.log");
52316
- if (!import_fs58.default.existsSync(auditPath)) return "No audit log found.";
52317
- const rawLines = import_fs58.default.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
52791
+ if (!import_fs59.default.existsSync(auditPath)) return "No audit log found.";
52792
+ const rawLines = import_fs59.default.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
52793
+ const wanted = filter === "block" ? "deny" : filter;
52318
52794
  const parsed = [];
52319
52795
  for (const line of rawLines) {
52320
52796
  try {
52321
52797
  const e = JSON.parse(line);
52322
- const decision = String(e.decision ?? "allow");
52323
- if (filter && decision !== filter) continue;
52798
+ const view = classifyDecision(e);
52799
+ if (wanted && view.outcome !== wanted) continue;
52324
52800
  const argsObj = e.args;
52325
52801
  let detail = "";
52326
52802
  if (argsObj) {
52327
52803
  const cmd = argsObj.command ?? argsObj.file_path ?? argsObj.path ?? argsObj.sql;
52328
- if (typeof cmd === "string" && cmd) {
52329
- detail = cmd.length > 80 ? cmd.slice(0, 80) + "\u2026" : cmd;
52330
- }
52804
+ if (typeof cmd === "string" && cmd) detail = cmd;
52331
52805
  }
52332
- const decisionPad = decision === "block" ? "[BLOCK] " : decision === "review" ? "[review]" : "[allow] ";
52806
+ if (!detail && typeof e.argsPreview === "string") detail = e.argsPreview;
52807
+ detail = detail.replace(/\s+/g, " ").trim();
52808
+ if (detail.length > 80) detail = detail.slice(0, 80) + "\u2026";
52809
+ const why = typeof e.ruleName === "string" && e.ruleName ? ` (${e.ruleName})` : "";
52333
52810
  const toolPad = String(e.tool ?? "").padEnd(20);
52334
- const line2 = `${e.ts} ${decisionPad} ${toolPad} ${detail}`;
52335
- parsed.push({ raw: line, decision, formatted: line2 });
52811
+ const line2 = `${e.ts} ${decisionTag(view)} ${toolPad} ${detail}${why}`;
52812
+ parsed.push({ raw: line, outcome: view.outcome, formatted: line2 });
52336
52813
  } catch {
52337
- parsed.push({ raw: line, decision: "allow", formatted: line });
52814
+ parsed.push({ raw: line, outcome: "unknown", formatted: `[? unparseable] ${line}` });
52338
52815
  }
52339
52816
  }
52340
52817
  const recent = parsed.slice(-limit);
52341
52818
  if (recent.length === 0) {
52342
- return filter ? `No ${filter} entries found in audit log.` : "Audit log is empty.";
52819
+ return filter ? `No ${wanted} entries found in audit log.` : "Audit log is empty.";
52343
52820
  }
52344
- const header = filter ? `Last ${recent.length} ${filter.toUpperCase()} entries:` : `Last ${recent.length} audit entries:`;
52821
+ const header = filter ? `Last ${recent.length} ${String(wanted).toUpperCase()} entries:` : `Last ${recent.length} audit entries:`;
52345
52822
  return `${header}
52346
52823
 
52347
52824
  ${recent.map((e) => e.formatted).join("\n")}`;
@@ -52688,7 +53165,7 @@ function registerTrustCommand(program2) {
52688
53165
  // src/cli/commands/mcp-pin.ts
52689
53166
  var import_chalk24 = __toESM(require("chalk"));
52690
53167
  init_mcp_pin();
52691
- var import_fs59 = __toESM(require("fs"));
53168
+ var import_fs60 = __toESM(require("fs"));
52692
53169
 
52693
53170
  // src/cli/commands/mcp-gateway-cmd.ts
52694
53171
  var import_chalk23 = __toESM(require("chalk"));
@@ -52884,6 +53361,7 @@ Restart ${[...agents].join(", ")}`) + import_chalk23.default.gray(" to activate.
52884
53361
  }
52885
53362
 
52886
53363
  // src/cli/commands/mcp-pin.ts
53364
+ init_mcp_wrap();
52887
53365
  function registerMcpPinCommand(program2) {
52888
53366
  const pinCmd = program2.command("mcp").description("Manage MCP servers \u2014 governance (gateway) + tool-definition pinning");
52889
53367
  registerMcpGatewayCommand2(pinCmd);
@@ -52895,7 +53373,7 @@ function registerMcpPinCommand(program2) {
52895
53373
  let repoCorrupt = false;
52896
53374
  if (found.source === "repo") {
52897
53375
  try {
52898
- const raw = import_fs59.default.readFileSync(found.path, "utf-8");
53376
+ const raw = import_fs60.default.readFileSync(found.path, "utf-8");
52899
53377
  const parsed = JSON.parse(raw);
52900
53378
  repoEntries = parsed.servers ?? {};
52901
53379
  } catch {
@@ -53009,6 +53487,88 @@ function registerMcpPinCommand(program2) {
53009
53487
  \u{1F513} Cleared ${count} MCP pin(s).`));
53010
53488
  console.log(import_chalk24.default.gray(" Next connection to each server will re-pin.\n"));
53011
53489
  });
53490
+ pinCmd.command("forget [serverKey]").option("--stale", "Remove all stale (orphaned) servers at once").description("Remove a server pin that is no longer configured in any agent").action((serverKey, opts) => {
53491
+ if (opts.stale) {
53492
+ forgetAllStale();
53493
+ return;
53494
+ }
53495
+ if (!serverKey) {
53496
+ console.error(
53497
+ import_chalk24.default.red("\n\u274C Please provide a server key, or use --stale to remove all orphans.\n")
53498
+ );
53499
+ process.exit(1);
53500
+ }
53501
+ let pins;
53502
+ try {
53503
+ pins = readMcpPins();
53504
+ } catch {
53505
+ console.error(import_chalk24.default.red("\n\u274C Pin file is corrupt."));
53506
+ console.error(import_chalk24.default.yellow(" Run: node9 mcp pin reset\n"));
53507
+ process.exit(1);
53508
+ }
53509
+ if (!pins.servers[serverKey]) {
53510
+ console.error(import_chalk24.default.red(`
53511
+ \u274C No pin found for server key "${serverKey}"
53512
+ `));
53513
+ console.error(`Run ${import_chalk24.default.cyan("node9 mcp pin list")} to see pinned servers.
53514
+ `);
53515
+ process.exit(1);
53516
+ }
53517
+ const inv = inventoryMcp();
53518
+ const liveKeys = inventoryServerKeys(inv);
53519
+ if (liveKeys.has(serverKey)) {
53520
+ const agent = "an agent config";
53521
+ console.error(import_chalk24.default.red(`
53522
+ \u274C Server "${serverKey}" is still configured in ${agent}.`));
53523
+ console.error(
53524
+ import_chalk24.default.yellow(
53525
+ ` Remove it from the agent config first, then run: node9 mcp forget ${serverKey}
53526
+ `
53527
+ )
53528
+ );
53529
+ process.exit(1);
53530
+ }
53531
+ const label2 = pins.servers[serverKey].label;
53532
+ removePin(serverKey);
53533
+ console.log(import_chalk24.default.green(`
53534
+ \u2713 Forgot server ${import_chalk24.default.cyan(serverKey)}`));
53535
+ console.log(import_chalk24.default.gray(` Was: ${label2}`));
53536
+ console.log(import_chalk24.default.gray(" Pin removed \u2014 server will no longer appear in the dashboard.\n"));
53537
+ });
53538
+ }
53539
+ function forgetAllStale() {
53540
+ let pins;
53541
+ try {
53542
+ pins = readMcpPins();
53543
+ } catch {
53544
+ console.error(import_chalk24.default.red("\n\u274C Pin file is corrupt."));
53545
+ console.error(import_chalk24.default.yellow(" Run: node9 mcp pin reset\n"));
53546
+ process.exit(1);
53547
+ }
53548
+ const inv = inventoryMcp();
53549
+ const liveKeys = inventoryServerKeys(inv);
53550
+ if (liveKeys.size === 0) {
53551
+ console.error(import_chalk24.default.red("\n\u274C No live MCP servers detected \u2014 refusing to remove every pin."));
53552
+ console.error(
53553
+ import_chalk24.default.yellow(
53554
+ " This usually means an agent config is missing or unreadable, not that\n every server is gone. Check your agent configs, or remove one at a\n time: node9 mcp forget <key>\n"
53555
+ )
53556
+ );
53557
+ process.exit(1);
53558
+ }
53559
+ const stale = Object.entries(pins.servers).filter(([sk]) => !liveKeys.has(sk));
53560
+ if (stale.length === 0) {
53561
+ console.log(import_chalk24.default.gray("\nNo stale servers to remove.\n"));
53562
+ return;
53563
+ }
53564
+ for (const [sk, pin] of stale) {
53565
+ delete pins.servers[sk];
53566
+ console.log(import_chalk24.default.green(` \u2713 ${import_chalk24.default.cyan(sk)} ${import_chalk24.default.gray(pin.label)}`));
53567
+ }
53568
+ writeMcpPins(pins);
53569
+ console.log(import_chalk24.default.green(`
53570
+ Removed ${stale.length} stale server(s).
53571
+ `));
53012
53572
  }
53013
53573
 
53014
53574
  // src/cli/commands/sync.ts
@@ -53253,10 +53813,16 @@ var LABEL_WIDTH = 14;
53253
53813
  function label(category) {
53254
53814
  return import_chalk27.default.bold(category.padEnd(Math.max(LABEL_WIDTH, category.length + 1)));
53255
53815
  }
53256
- function renderFinding(f, showWeight = false) {
53816
+ function guardedWhat(f) {
53817
+ if (f.detail.length === 0) return "this";
53818
+ const reads = f.coverageProbe?.kind === "fileRead" ? "reads of " : "";
53819
+ const more = f.detail.length > 1 ? ` and ${f.detail.length - 1} more` : "";
53820
+ return `${reads}${f.detail[0]}${more}`;
53821
+ }
53822
+ function renderFinding(f, showWeight = false, displayLabel = f.category) {
53257
53823
  const lines = [];
53258
53824
  const wt = showWeight && f.scoreWeight ? import_chalk27.default.cyan.bold(`+${f.scoreWeight} `) : "";
53259
- lines.push(` ${ICON[f.severity]} ${label(f.category)}${wt}${f.title}`);
53825
+ lines.push(` ${ICON[f.severity]} ${label(displayLabel)}${wt}${f.title}`);
53260
53826
  const indent = " ".repeat(2 + 3 + LABEL_WIDTH);
53261
53827
  const width = 80 - indent.length;
53262
53828
  for (const s of [f.what, f.why, f.who]) {
@@ -53293,11 +53859,11 @@ function renderPosture(result) {
53293
53859
  );
53294
53860
  const headroom = openHeadroom(result.findings);
53295
53861
  if (headroom > 0) {
53296
- lines.push(
53297
- " " + import_chalk27.default.gray(
53298
- `${headroom} pts of headroom below \u2014 each layer adds security at some cost to flexibility. You choose which to close.`
53299
- )
53300
- );
53862
+ const openExposures = result.findings.filter(
53863
+ (f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix" && !f.scoreWeight && f.severity !== "advisory"
53864
+ ).length;
53865
+ const note = openExposures > 0 ? `Of the gap to 100: ${headroom} pts is optional hardening you can choose to turn on (the \u{1F512} tier below, each at some cost to flexibility); the rest is the open findings \u2014 the flagged rows below. Fix those first.` : `${headroom} pts of headroom \u2014 optional hardening you can choose to turn on (the \u{1F512} tier below), each at some cost to flexibility.`;
53866
+ for (const l of wrap(note, 76)) lines.push(" " + import_chalk27.default.gray(l));
53301
53867
  }
53302
53868
  lines.push("");
53303
53869
  if (result.headline) {
@@ -53310,13 +53876,18 @@ function renderPosture(result) {
53310
53876
  }
53311
53877
  const covered = result.findings.filter((f) => f.coverage?.state === "covered");
53312
53878
  const open = result.findings.filter((f) => f.coverage?.state !== "covered");
53879
+ const collision = new Set(
53880
+ covered.map((f) => f.category).filter((c) => open.some((o) => o.category === c))
53881
+ );
53882
+ const openLabel = (f) => collision.has(f.category) ? `${f.category} (exposed)` : f.category;
53313
53883
  if (covered.length > 0) {
53314
53884
  lines.push(" " + import_chalk27.default.green("\u{1F7E2} ON NOW \u2014 node9 is enforcing these (your floor)"));
53315
53885
  for (const f of covered) {
53316
53886
  const gated = f.coverage?.level === "review" ? "approval-gating" : "blocking";
53317
53887
  const via = f.coverage?.via ?? "node9";
53888
+ const lbl = collision.has(f.category) ? `${f.category} (guarded)` : f.category;
53318
53889
  lines.push(
53319
- ` ${import_chalk27.default.green("\u2705")} ${label(f.category)}${import_chalk27.default.gray(`${via} is ${gated} this`)}`
53890
+ ` ${import_chalk27.default.green("\u2705")} ${label(lbl)}${import_chalk27.default.gray(`${via} is ${gated} ${guardedWhat(f)}`)}`
53320
53891
  );
53321
53892
  }
53322
53893
  lines.push("");
@@ -53326,17 +53897,17 @@ function renderPosture(result) {
53326
53897
  const osOpen = open.filter((f) => f.owner !== "node9" && !f.node9Reduces);
53327
53898
  if (node9Open.length > 0) {
53328
53899
  lines.push(" " + import_chalk27.default.cyan.bold("\u{1F527} node9 can fix these \u2014 run the command"));
53329
- for (const f of node9Open) lines.push(...renderFinding(f, true));
53900
+ for (const f of node9Open) lines.push(...renderFinding(f, true, openLabel(f)));
53330
53901
  }
53331
53902
  if (reduceOpen.length > 0) {
53332
53903
  if (node9Open.length > 0) lines.push("");
53333
53904
  lines.push(" " + import_chalk27.default.yellow.bold("\u{1F512} AVAILABLE \u2014 turn on to harden (each has a tradeoff)"));
53334
- for (const f of reduceOpen) lines.push(...renderFinding(f, true));
53905
+ for (const f of reduceOpen) lines.push(...renderFinding(f, true, openLabel(f)));
53335
53906
  }
53336
53907
  if (osOpen.length > 0) {
53337
53908
  if (node9Open.length > 0 || reduceOpen.length > 0) lines.push("");
53338
53909
  lines.push(" " + import_chalk27.default.bold("\u{1F9F1} YOUR PART \u2014 node9 can't fix these (OS-level)"));
53339
- for (const f of osOpen) lines.push(...renderFinding(f));
53910
+ for (const f of osOpen) lines.push(...renderFinding(f, false, openLabel(f)));
53340
53911
  }
53341
53912
  for (const cat of result.passedCategories) {
53342
53913
  lines.push(` ${import_chalk27.default.green("\u2705")} ${label(cat)}${import_chalk27.default.gray("no issues found")}`);
@@ -53395,7 +53966,7 @@ function registerPostureCommand(program2) {
53395
53966
  var import_chalk30 = __toESM(require("chalk"));
53396
53967
 
53397
53968
  // src/ci-check/fetch.ts
53398
- var import_fs60 = __toESM(require("fs"));
53969
+ var import_fs61 = __toESM(require("fs"));
53399
53970
  var import_path57 = __toESM(require("path"));
53400
53971
  var import_node_child_process = require("child_process");
53401
53972
  var import_undici = __toESM(require_undici());
@@ -53480,7 +54051,7 @@ function parseRepoUrl(input) {
53480
54051
  function isLocalPath(input) {
53481
54052
  if (input.startsWith(".") || input.startsWith("/") || input.startsWith("~")) return true;
53482
54053
  try {
53483
- return import_fs60.default.existsSync(input) && import_fs60.default.statSync(input).isDirectory();
54054
+ return import_fs61.default.existsSync(input) && import_fs61.default.statSync(input).isDirectory();
53484
54055
  } catch {
53485
54056
  return false;
53486
54057
  }
@@ -53597,8 +54168,8 @@ function readLocalTree(dir) {
53597
54168
  const add = (rel) => {
53598
54169
  const abs = import_path57.default.join(root, rel);
53599
54170
  try {
53600
- if (import_fs60.default.existsSync(abs) && import_fs60.default.statSync(abs).isFile()) {
53601
- files.push({ path: rel, content: import_fs60.default.readFileSync(abs, "utf8") });
54171
+ if (import_fs61.default.existsSync(abs) && import_fs61.default.statSync(abs).isFile()) {
54172
+ files.push({ path: rel, content: import_fs61.default.readFileSync(abs, "utf8") });
53602
54173
  }
53603
54174
  } catch {
53604
54175
  }
@@ -53618,7 +54189,7 @@ function readLocalTree(dir) {
53618
54189
  dirsVisited++;
53619
54190
  let entries;
53620
54191
  try {
53621
- entries = import_fs60.default.readdirSync(import_path57.default.join(root, relDir), { withFileTypes: true });
54192
+ entries = import_fs61.default.readdirSync(import_path57.default.join(root, relDir), { withFileTypes: true });
53622
54193
  } catch {
53623
54194
  return;
53624
54195
  }
@@ -53641,8 +54212,8 @@ function readLocalTree(dir) {
53641
54212
  for (const rel of matches) collect(rel);
53642
54213
  const wfDir = import_path57.default.join(root, WORKFLOW_DIR);
53643
54214
  try {
53644
- if (import_fs60.default.existsSync(wfDir)) {
53645
- for (const name of import_fs60.default.readdirSync(wfDir)) {
54215
+ if (import_fs61.default.existsSync(wfDir)) {
54216
+ for (const name of import_fs61.default.readdirSync(wfDir)) {
53646
54217
  if (/\.ya?ml$/.test(name)) add(import_path57.default.join(WORKFLOW_DIR, name));
53647
54218
  }
53648
54219
  }
@@ -54781,7 +55352,7 @@ function registerEgressCommand(program2) {
54781
55352
  var import_chalk32 = __toESM(require("chalk"));
54782
55353
 
54783
55354
  // src/shields/jail.ts
54784
- var import_fs61 = __toESM(require("fs"));
55355
+ var import_fs62 = __toESM(require("fs"));
54785
55356
  var import_os53 = __toESM(require("os"));
54786
55357
  var import_path58 = __toESM(require("path"));
54787
55358
  init_build();
@@ -54793,7 +55364,7 @@ function jailStorePath() {
54793
55364
  function readJailPaths() {
54794
55365
  let text;
54795
55366
  try {
54796
- text = import_fs61.default.readFileSync(jailStorePath(), "utf8");
55367
+ text = import_fs62.default.readFileSync(jailStorePath(), "utf8");
54797
55368
  } catch (err2) {
54798
55369
  if (err2.code === "ENOENT") return [];
54799
55370
  throw err2;
@@ -54811,8 +55382,8 @@ function readJailPaths() {
54811
55382
  }
54812
55383
  function writeJailPaths(paths) {
54813
55384
  const p = jailStorePath();
54814
- import_fs61.default.mkdirSync(import_path58.default.dirname(p), { recursive: true });
54815
- import_fs61.default.writeFileSync(p, JSON.stringify({ paths }, null, 2) + "\n", { mode: 384 });
55385
+ import_fs62.default.mkdirSync(import_path58.default.dirname(p), { recursive: true });
55386
+ import_fs62.default.writeFileSync(p, JSON.stringify({ paths }, null, 2) + "\n", { mode: 384 });
54816
55387
  }
54817
55388
  function addJailPath(rawPath, verdict) {
54818
55389
  const norm = rawPath.trim();
@@ -54841,7 +55412,7 @@ function regenerateUserJail(paths) {
54841
55412
  writeActiveShields(active2.filter((s) => s !== USER_JAIL_SHIELD));
54842
55413
  }
54843
55414
  try {
54844
- import_fs61.default.rmSync(file, { force: true });
55415
+ import_fs62.default.rmSync(file, { force: true });
54845
55416
  } catch {
54846
55417
  }
54847
55418
  return;
@@ -54955,13 +55526,13 @@ function registerJailCommand(program2) {
54955
55526
 
54956
55527
  // src/cli/commands/sandbox.ts
54957
55528
  var import_chalk33 = __toESM(require("chalk"));
54958
- var import_fs64 = __toESM(require("fs"));
55529
+ var import_fs65 = __toESM(require("fs"));
54959
55530
  var import_path61 = __toESM(require("path"));
54960
55531
  var import_child_process13 = require("child_process");
54961
55532
  init_config();
54962
55533
 
54963
55534
  // src/sandbox/config.ts
54964
- var import_fs62 = __toESM(require("fs"));
55535
+ var import_fs63 = __toESM(require("fs"));
54965
55536
  var import_path59 = __toESM(require("path"));
54966
55537
  var import_yaml2 = require("yaml");
54967
55538
  var SANDBOX_CONFIG_FILE = "node9.sandbox.yaml";
@@ -55039,12 +55610,12 @@ function sandboxConfigPath(cwd = process.cwd()) {
55039
55610
  }
55040
55611
  function loadSandboxConfig(cwd = process.cwd(), fallbackAgent = "claude") {
55041
55612
  const p = sandboxConfigPath(cwd);
55042
- if (!import_fs62.default.existsSync(p)) {
55613
+ if (!import_fs63.default.existsSync(p)) {
55043
55614
  throw new Error(`sandbox: ${SANDBOX_CONFIG_FILE} not found \u2014 run \`node9 sandbox new\` first.`);
55044
55615
  }
55045
55616
  let raw;
55046
55617
  try {
55047
- raw = (0, import_yaml2.parse)(import_fs62.default.readFileSync(p, "utf-8"));
55618
+ raw = (0, import_yaml2.parse)(import_fs63.default.readFileSync(p, "utf-8"));
55048
55619
  } catch (err2) {
55049
55620
  throw new Error(
55050
55621
  `sandbox: ${SANDBOX_CONFIG_FILE} is not valid YAML \u2014 ${err2.message}`
@@ -55102,7 +55673,7 @@ function compileAllowlist(input) {
55102
55673
  init_templates();
55103
55674
 
55104
55675
  // src/sandbox/runtime.ts
55105
- var import_fs63 = __toESM(require("fs"));
55676
+ var import_fs64 = __toESM(require("fs"));
55106
55677
  var import_os54 = __toESM(require("os"));
55107
55678
  var import_path60 = __toESM(require("path"));
55108
55679
  var import_crypto14 = __toESM(require("crypto"));
@@ -55130,7 +55701,7 @@ function buildRunArgs(opts) {
55130
55701
  args.push("-v", `${allowlistHostPath}:${ALLOWED_DOMAINS_PATH}:ro`);
55131
55702
  if (config.node9.mountAgentCredentials) {
55132
55703
  const creds = agentCredentialsMount(config.agent);
55133
- if (import_fs63.default.existsSync(creds.hostPath)) {
55704
+ if (import_fs64.default.existsSync(creds.hostPath)) {
55134
55705
  args.push("-v", `${creds.hostPath}:${creds.target}`);
55135
55706
  }
55136
55707
  }
@@ -55152,16 +55723,16 @@ function sandboxBuildDir(cwd = process.cwd()) {
55152
55723
  }
55153
55724
  function writeBuildContext(cwd, dockerfile, entrypoint) {
55154
55725
  const dir = sandboxBuildDir(cwd);
55155
- import_fs63.default.mkdirSync(dir, { recursive: true });
55156
- import_fs63.default.writeFileSync(import_path60.default.join(dir, "Dockerfile"), dockerfile);
55157
- import_fs63.default.writeFileSync(import_path60.default.join(dir, "entrypoint.sh"), entrypoint);
55726
+ import_fs64.default.mkdirSync(dir, { recursive: true });
55727
+ import_fs64.default.writeFileSync(import_path60.default.join(dir, "Dockerfile"), dockerfile);
55728
+ import_fs64.default.writeFileSync(import_path60.default.join(dir, "entrypoint.sh"), entrypoint);
55158
55729
  return dir;
55159
55730
  }
55160
55731
  function writeAllowlist(cwd, hosts) {
55161
55732
  const dir = import_path60.default.join(cwd, ".node9", "sandbox");
55162
- import_fs63.default.mkdirSync(dir, { recursive: true });
55733
+ import_fs64.default.mkdirSync(dir, { recursive: true });
55163
55734
  const p = import_path60.default.join(dir, "allowed-domains.txt");
55164
- import_fs63.default.writeFileSync(p, hosts.join("\n") + "\n");
55735
+ import_fs64.default.writeFileSync(p, hosts.join("\n") + "\n");
55165
55736
  return p;
55166
55737
  }
55167
55738
  function resolveHomePath(p) {
@@ -55170,7 +55741,7 @@ function resolveHomePath(p) {
55170
55741
 
55171
55742
  // src/cli/commands/sandbox.ts
55172
55743
  function seedDataDirConfig(dataDir, sandbox) {
55173
- import_fs64.default.mkdirSync(dataDir, { recursive: true });
55744
+ import_fs65.default.mkdirSync(dataDir, { recursive: true });
55174
55745
  const configPath = import_path61.default.join(dataDir, "config.json");
55175
55746
  const seed = {
55176
55747
  settings: {
@@ -55182,7 +55753,7 @@ function seedDataDirConfig(dataDir, sandbox) {
55182
55753
  }
55183
55754
  }
55184
55755
  };
55185
- import_fs64.default.writeFileSync(configPath, JSON.stringify(seed, null, 2), { mode: 384 });
55756
+ import_fs65.default.writeFileSync(configPath, JSON.stringify(seed, null, 2), { mode: 384 });
55186
55757
  }
55187
55758
  function registerSandboxCommand(program2, version2) {
55188
55759
  const node9Version2 = pinnedNode9Version(version2);
@@ -55190,13 +55761,13 @@ function registerSandboxCommand(program2, version2) {
55190
55761
  cmd.command("new").description(`Scaffold ${SANDBOX_CONFIG_FILE} in this project`).option("--agent <agent>", "claude (default) or codex", "claude").action((opts) => {
55191
55762
  const agent = opts.agent === "codex" ? "codex" : "claude";
55192
55763
  const p = sandboxConfigPath();
55193
- if (import_fs64.default.existsSync(p)) {
55764
+ if (import_fs65.default.existsSync(p)) {
55194
55765
  console.log(
55195
55766
  import_chalk33.default.yellow(` ${SANDBOX_CONFIG_FILE} already exists \u2014 leaving it untouched.`)
55196
55767
  );
55197
55768
  return;
55198
55769
  }
55199
- import_fs64.default.writeFileSync(p, scaffoldSandboxYaml(agent));
55770
+ import_fs65.default.writeFileSync(p, scaffoldSandboxYaml(agent));
55200
55771
  console.log(
55201
55772
  import_chalk33.default.green(` \u2713 wrote ${SANDBOX_CONFIG_FILE}`) + import_chalk33.default.dim(` (agent: ${agent})`)
55202
55773
  );
@@ -55237,7 +55808,7 @@ function registerSandboxCommand(program2, version2) {
55237
55808
  const hash = imageContentHash(dockerfile, entrypoint);
55238
55809
  const image = sandbox.runtime.image;
55239
55810
  const hashFile = import_path61.default.join(sandboxBuildDir(cwd), ".image-hash");
55240
- const lastHash = import_fs64.default.existsSync(hashFile) ? import_fs64.default.readFileSync(hashFile, "utf-8").trim() : "";
55811
+ const lastHash = import_fs65.default.existsSync(hashFile) ? import_fs65.default.readFileSync(hashFile, "utf-8").trim() : "";
55241
55812
  const imageExists = (0, import_child_process13.spawnSync)(sandbox.runtime.engine, ["image", "inspect", image], { stdio: "ignore" }).status === 0;
55242
55813
  const needBuild = sandbox.runtime.rebuild === "always" || !imageExists || sandbox.runtime.rebuild !== "never" && lastHash !== hash;
55243
55814
  if (needBuild) {
@@ -55249,7 +55820,7 @@ function registerSandboxCommand(program2, version2) {
55249
55820
  console.error(import_chalk33.default.red(" build failed."));
55250
55821
  process.exit(b.status ?? 1);
55251
55822
  }
55252
- import_fs64.default.writeFileSync(hashFile, hash);
55823
+ import_fs65.default.writeFileSync(hashFile, hash);
55253
55824
  }
55254
55825
  const dataDir = sandboxDataDir(cwd);
55255
55826
  seedDataDirConfig(dataDir, sandbox);
@@ -55263,7 +55834,7 @@ function registerSandboxCommand(program2, version2) {
55263
55834
  });
55264
55835
  if (sandbox.node9.mountAgentCredentials) {
55265
55836
  const creds = agentCredentialsMount(sandbox.agent);
55266
- if (import_fs64.default.existsSync(creds.hostPath)) {
55837
+ if (import_fs65.default.existsSync(creds.hostPath)) {
55267
55838
  console.log(import_chalk33.default.dim(` mounting ${creds.hostPath} (agent credentials, rw)`));
55268
55839
  } else {
55269
55840
  console.log(
@@ -55280,7 +55851,7 @@ function registerSandboxCommand(program2, version2) {
55280
55851
  });
55281
55852
  cmd.command("tail").description("Stream the sandbox's audit log (host-side)").action(() => {
55282
55853
  const auditPath = import_path61.default.join(sandboxDataDir(), "audit.log");
55283
- if (!import_fs64.default.existsSync(auditPath)) {
55854
+ if (!import_fs65.default.existsSync(auditPath)) {
55284
55855
  console.log(import_chalk33.default.dim(" no sandbox audit yet."));
55285
55856
  return;
55286
55857
  }
@@ -55288,11 +55859,11 @@ function registerSandboxCommand(program2, version2) {
55288
55859
  });
55289
55860
  cmd.command("logs").description("Dump the sandbox's audit log").action(() => {
55290
55861
  const auditPath = import_path61.default.join(sandboxDataDir(), "audit.log");
55291
- if (!import_fs64.default.existsSync(auditPath)) {
55862
+ if (!import_fs65.default.existsSync(auditPath)) {
55292
55863
  console.log(import_chalk33.default.dim(" no sandbox audit yet."));
55293
55864
  return;
55294
55865
  }
55295
- process.stdout.write(import_fs64.default.readFileSync(auditPath, "utf-8"));
55866
+ process.stdout.write(import_fs65.default.readFileSync(auditPath, "utf-8"));
55296
55867
  });
55297
55868
  cmd.command("clean").description("Remove the sandbox image, build context, and data").action(() => {
55298
55869
  const cwd = process.cwd();
@@ -55306,15 +55877,16 @@ function registerSandboxCommand(program2, version2) {
55306
55877
  stdio: "ignore"
55307
55878
  });
55308
55879
  }
55309
- import_fs64.default.rmSync(import_path61.default.join(cwd, ".node9", "sandbox"), { recursive: true, force: true });
55880
+ import_fs65.default.rmSync(import_path61.default.join(cwd, ".node9", "sandbox"), { recursive: true, force: true });
55310
55881
  console.log(import_chalk33.default.green(" \u2713 sandbox image + build + data removed."));
55311
55882
  });
55312
55883
  }
55313
55884
 
55314
55885
  // src/cli/commands/sessions.ts
55315
55886
  var import_chalk34 = __toESM(require("chalk"));
55316
- var import_fs65 = __toESM(require("fs"));
55887
+ var import_fs66 = __toESM(require("fs"));
55317
55888
  var import_path62 = __toESM(require("path"));
55889
+ init_decision();
55318
55890
  var import_os55 = __toESM(require("os"));
55319
55891
  init_scan_summary();
55320
55892
  init_litellm();
@@ -55411,7 +55983,7 @@ function loadAuditEntries(auditPath) {
55411
55983
  const aPath = auditPath ?? import_path62.default.join(import_os55.default.homedir(), ".node9", "audit.log");
55412
55984
  let raw;
55413
55985
  try {
55414
- raw = import_fs65.default.readFileSync(aPath, "utf-8");
55986
+ raw = import_fs66.default.readFileSync(aPath, "utf-8");
55415
55987
  } catch {
55416
55988
  return [];
55417
55989
  }
@@ -55421,7 +55993,7 @@ function loadAuditEntries(auditPath) {
55421
55993
  try {
55422
55994
  const e = JSON.parse(line);
55423
55995
  if (!e.ts || !e.tool || !e.decision) continue;
55424
- if (e.decision === "allow" || e.decision === "allowed") continue;
55996
+ if (classifyDecision(e).outcome === "allow") continue;
55425
55997
  entries.push(e);
55426
55998
  } catch {
55427
55999
  }
@@ -55448,7 +56020,7 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
55448
56020
  }
55449
56021
  function buildGeminiSessions(days, allAuditEntries) {
55450
56022
  const tmpDir = import_path62.default.join(import_os55.default.homedir(), ".gemini", "tmp");
55451
- if (!import_fs65.default.existsSync(tmpDir)) return [];
56023
+ if (!import_fs66.default.existsSync(tmpDir)) return [];
55452
56024
  const cutoff = days !== null ? (() => {
55453
56025
  const d = /* @__PURE__ */ new Date();
55454
56026
  d.setDate(d.getDate() - days);
@@ -55457,7 +56029,7 @@ function buildGeminiSessions(days, allAuditEntries) {
55457
56029
  })() : null;
55458
56030
  let slugDirs;
55459
56031
  try {
55460
- slugDirs = import_fs65.default.readdirSync(tmpDir);
56032
+ slugDirs = import_fs66.default.readdirSync(tmpDir);
55461
56033
  } catch {
55462
56034
  return [];
55463
56035
  }
@@ -55465,27 +56037,27 @@ function buildGeminiSessions(days, allAuditEntries) {
55465
56037
  for (const slug2 of slugDirs) {
55466
56038
  const slugPath = import_path62.default.join(tmpDir, slug2);
55467
56039
  try {
55468
- if (!import_fs65.default.statSync(slugPath).isDirectory()) continue;
56040
+ if (!import_fs66.default.statSync(slugPath).isDirectory()) continue;
55469
56041
  } catch {
55470
56042
  continue;
55471
56043
  }
55472
56044
  let projectRoot = import_path62.default.join(import_os55.default.homedir(), slug2);
55473
56045
  try {
55474
- projectRoot = import_fs65.default.readFileSync(import_path62.default.join(slugPath, ".project_root"), "utf-8").trim();
56046
+ projectRoot = import_fs66.default.readFileSync(import_path62.default.join(slugPath, ".project_root"), "utf-8").trim();
55475
56047
  } catch {
55476
56048
  }
55477
56049
  const chatsDir = import_path62.default.join(slugPath, "chats");
55478
- if (!import_fs65.default.existsSync(chatsDir)) continue;
56050
+ if (!import_fs66.default.existsSync(chatsDir)) continue;
55479
56051
  let chatFiles;
55480
56052
  try {
55481
- chatFiles = import_fs65.default.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
56053
+ chatFiles = import_fs66.default.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
55482
56054
  } catch {
55483
56055
  continue;
55484
56056
  }
55485
56057
  for (const chatFile of chatFiles) {
55486
56058
  let raw;
55487
56059
  try {
55488
- raw = import_fs65.default.readFileSync(import_path62.default.join(chatsDir, chatFile), "utf-8");
56060
+ raw = import_fs66.default.readFileSync(import_path62.default.join(chatsDir, chatFile), "utf-8");
55489
56061
  } catch {
55490
56062
  continue;
55491
56063
  }
@@ -55566,7 +56138,7 @@ function buildGeminiSessions(days, allAuditEntries) {
55566
56138
  }
55567
56139
  function buildCodexSessions(days, allAuditEntries) {
55568
56140
  const sessionsBase = import_path62.default.join(import_os55.default.homedir(), ".codex", "sessions");
55569
- if (!import_fs65.default.existsSync(sessionsBase)) return [];
56141
+ if (!import_fs66.default.existsSync(sessionsBase)) return [];
55570
56142
  const cutoff = days !== null ? (() => {
55571
56143
  const d = /* @__PURE__ */ new Date();
55572
56144
  d.setDate(d.getDate() - days);
@@ -55575,28 +56147,28 @@ function buildCodexSessions(days, allAuditEntries) {
55575
56147
  })() : null;
55576
56148
  const jsonlFiles = [];
55577
56149
  try {
55578
- for (const year of import_fs65.default.readdirSync(sessionsBase)) {
56150
+ for (const year of import_fs66.default.readdirSync(sessionsBase)) {
55579
56151
  const yearPath = import_path62.default.join(sessionsBase, year);
55580
56152
  try {
55581
- if (!import_fs65.default.statSync(yearPath).isDirectory()) continue;
56153
+ if (!import_fs66.default.statSync(yearPath).isDirectory()) continue;
55582
56154
  } catch {
55583
56155
  continue;
55584
56156
  }
55585
- for (const month of import_fs65.default.readdirSync(yearPath)) {
56157
+ for (const month of import_fs66.default.readdirSync(yearPath)) {
55586
56158
  const monthPath = import_path62.default.join(yearPath, month);
55587
56159
  try {
55588
- if (!import_fs65.default.statSync(monthPath).isDirectory()) continue;
56160
+ if (!import_fs66.default.statSync(monthPath).isDirectory()) continue;
55589
56161
  } catch {
55590
56162
  continue;
55591
56163
  }
55592
- for (const day of import_fs65.default.readdirSync(monthPath)) {
56164
+ for (const day of import_fs66.default.readdirSync(monthPath)) {
55593
56165
  const dayPath = import_path62.default.join(monthPath, day);
55594
56166
  try {
55595
- if (!import_fs65.default.statSync(dayPath).isDirectory()) continue;
56167
+ if (!import_fs66.default.statSync(dayPath).isDirectory()) continue;
55596
56168
  } catch {
55597
56169
  continue;
55598
56170
  }
55599
- for (const file of import_fs65.default.readdirSync(dayPath)) {
56171
+ for (const file of import_fs66.default.readdirSync(dayPath)) {
55600
56172
  if (file.endsWith(".jsonl")) jsonlFiles.push(import_path62.default.join(dayPath, file));
55601
56173
  }
55602
56174
  }
@@ -55609,7 +56181,7 @@ function buildCodexSessions(days, allAuditEntries) {
55609
56181
  for (const filePath of jsonlFiles) {
55610
56182
  let lines;
55611
56183
  try {
55612
- lines = import_fs65.default.readFileSync(filePath, "utf-8").split("\n");
56184
+ lines = import_fs66.default.readFileSync(filePath, "utf-8").split("\n");
55613
56185
  } catch {
55614
56186
  continue;
55615
56187
  }
@@ -55698,7 +56270,7 @@ function buildSessions(days, historyPath) {
55698
56270
  const hPath = historyPath ?? import_path62.default.join(import_os55.default.homedir(), ".claude", "history.jsonl");
55699
56271
  let historyRaw = "";
55700
56272
  try {
55701
- historyRaw = import_fs65.default.readFileSync(hPath, "utf-8");
56273
+ historyRaw = import_fs66.default.readFileSync(hPath, "utf-8");
55702
56274
  } catch {
55703
56275
  }
55704
56276
  const cutoff = days !== null ? (() => {
@@ -55722,7 +56294,7 @@ function buildSessions(days, historyPath) {
55722
56294
  const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
55723
56295
  let sessionLines = [];
55724
56296
  try {
55725
- sessionLines = import_fs65.default.readFileSync(jsonlFile, "utf-8").split("\n");
56297
+ sessionLines = import_fs66.default.readFileSync(jsonlFile, "utf-8").split("\n");
55726
56298
  } catch {
55727
56299
  }
55728
56300
  const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
@@ -56116,12 +56688,12 @@ function registerSessionTaintCommand(program2) {
56116
56688
 
56117
56689
  // src/cli/commands/skill-pin.ts
56118
56690
  var import_chalk36 = __toESM(require("chalk"));
56119
- var import_fs66 = __toESM(require("fs"));
56691
+ var import_fs67 = __toESM(require("fs"));
56120
56692
  var import_os56 = __toESM(require("os"));
56121
56693
  var import_path63 = __toESM(require("path"));
56122
56694
  function wipeSkillSessions() {
56123
56695
  try {
56124
- import_fs66.default.rmSync(import_path63.default.join(import_os56.default.homedir(), ".node9", "skill-sessions"), {
56696
+ import_fs67.default.rmSync(import_path63.default.join(import_os56.default.homedir(), ".node9", "skill-sessions"), {
56125
56697
  recursive: true,
56126
56698
  force: true
56127
56699
  });
@@ -56203,15 +56775,15 @@ function registerSkillPinCommand(program2) {
56203
56775
  }
56204
56776
 
56205
56777
  // src/cli/commands/decisions.ts
56206
- var import_fs67 = __toESM(require("fs"));
56778
+ var import_fs68 = __toESM(require("fs"));
56207
56779
  var import_os57 = __toESM(require("os"));
56208
56780
  var import_path64 = __toESM(require("path"));
56209
56781
  var import_chalk37 = __toESM(require("chalk"));
56210
56782
  var DECISIONS_FILE2 = import_path64.default.join(import_os57.default.homedir(), ".node9", "decisions.json");
56211
56783
  function readDecisions() {
56212
56784
  try {
56213
- if (!import_fs67.default.existsSync(DECISIONS_FILE2)) return {};
56214
- const raw = import_fs67.default.readFileSync(DECISIONS_FILE2, "utf-8");
56785
+ if (!import_fs68.default.existsSync(DECISIONS_FILE2)) return {};
56786
+ const raw = import_fs68.default.readFileSync(DECISIONS_FILE2, "utf-8");
56215
56787
  const parsed = JSON.parse(raw);
56216
56788
  const out = {};
56217
56789
  for (const [k, v] of Object.entries(parsed)) {
@@ -56224,10 +56796,10 @@ function readDecisions() {
56224
56796
  }
56225
56797
  function writeDecisions(d) {
56226
56798
  const dir = import_path64.default.dirname(DECISIONS_FILE2);
56227
- if (!import_fs67.default.existsSync(dir)) import_fs67.default.mkdirSync(dir, { recursive: true });
56799
+ if (!import_fs68.default.existsSync(dir)) import_fs68.default.mkdirSync(dir, { recursive: true });
56228
56800
  const tmp = `${DECISIONS_FILE2}.${process.pid}.tmp`;
56229
- import_fs67.default.writeFileSync(tmp, JSON.stringify(d, null, 2));
56230
- import_fs67.default.renameSync(tmp, DECISIONS_FILE2);
56801
+ import_fs68.default.writeFileSync(tmp, JSON.stringify(d, null, 2));
56802
+ import_fs68.default.renameSync(tmp, DECISIONS_FILE2);
56231
56803
  }
56232
56804
  function registerDecisionsCommand(program2) {
56233
56805
  const cmd = program2.command("decisions").description('Manage persistent "Always Allow" / "Always Deny" tool decisions');
@@ -56284,7 +56856,7 @@ Persistent decisions (${entries.length})
56284
56856
 
56285
56857
  // src/cli/commands/dlp.ts
56286
56858
  var import_chalk38 = __toESM(require("chalk"));
56287
- var import_fs68 = __toESM(require("fs"));
56859
+ var import_fs69 = __toESM(require("fs"));
56288
56860
  var import_path65 = __toESM(require("path"));
56289
56861
  var import_os58 = __toESM(require("os"));
56290
56862
  var AUDIT_LOG = import_path65.default.join(import_os58.default.homedir(), ".node9", "audit.log");
@@ -56295,7 +56867,7 @@ function stripAnsi(s) {
56295
56867
  }
56296
56868
  function loadResolved() {
56297
56869
  try {
56298
- const raw = JSON.parse(import_fs68.default.readFileSync(RESOLVED_FILE, "utf-8"));
56870
+ const raw = JSON.parse(import_fs69.default.readFileSync(RESOLVED_FILE, "utf-8"));
56299
56871
  return new Set(raw);
56300
56872
  } catch {
56301
56873
  return /* @__PURE__ */ new Set();
@@ -56303,13 +56875,13 @@ function loadResolved() {
56303
56875
  }
56304
56876
  function saveResolved(resolved) {
56305
56877
  try {
56306
- import_fs68.default.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
56878
+ import_fs69.default.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
56307
56879
  } catch {
56308
56880
  }
56309
56881
  }
56310
56882
  function loadDlpFindings() {
56311
- if (!import_fs68.default.existsSync(AUDIT_LOG)) return [];
56312
- return import_fs68.default.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
56883
+ if (!import_fs69.default.existsSync(AUDIT_LOG)) return [];
56884
+ return import_fs69.default.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
56313
56885
  if (!line.trim()) return [];
56314
56886
  try {
56315
56887
  const e = JSON.parse(line);
@@ -56407,14 +56979,14 @@ function registerDlpCommand(program2) {
56407
56979
 
56408
56980
  // src/cli/commands/mask.ts
56409
56981
  var import_chalk39 = __toESM(require("chalk"));
56410
- var import_fs69 = __toESM(require("fs"));
56982
+ var import_fs70 = __toESM(require("fs"));
56411
56983
  var import_path66 = __toESM(require("path"));
56412
56984
  var import_os59 = __toESM(require("os"));
56413
56985
  init_dlp();
56414
56986
  function findJsonlFiles(dir) {
56415
56987
  const results = [];
56416
- if (!import_fs69.default.existsSync(dir)) return results;
56417
- for (const entry of import_fs69.default.readdirSync(dir, { withFileTypes: true })) {
56988
+ if (!import_fs70.default.existsSync(dir)) return results;
56989
+ for (const entry of import_fs70.default.readdirSync(dir, { withFileTypes: true })) {
56418
56990
  const full = import_path66.default.join(dir, entry.name);
56419
56991
  if (entry.isDirectory()) results.push(...findJsonlFiles(full));
56420
56992
  else if (entry.isFile() && entry.name.endsWith(".jsonl")) results.push(full);
@@ -56458,7 +57030,7 @@ function redactJson(obj) {
56458
57030
  function processFile(filePath, dryRun) {
56459
57031
  let raw;
56460
57032
  try {
56461
- raw = import_fs69.default.readFileSync(filePath, "utf-8");
57033
+ raw = import_fs70.default.readFileSync(filePath, "utf-8");
56462
57034
  } catch {
56463
57035
  return { redactedLines: 0, patterns: [] };
56464
57036
  }
@@ -56490,14 +57062,14 @@ function processFile(filePath, dryRun) {
56490
57062
  }
56491
57063
  }
56492
57064
  if (!dryRun && redactedLines > 0) {
56493
- import_fs69.default.writeFileSync(filePath, newLines.join("\n"), "utf-8");
57065
+ import_fs70.default.writeFileSync(filePath, newLines.join("\n"), "utf-8");
56494
57066
  }
56495
57067
  return { redactedLines, patterns };
56496
57068
  }
56497
57069
  function processJsonFile(filePath, dryRun) {
56498
57070
  let raw;
56499
57071
  try {
56500
- raw = import_fs69.default.readFileSync(filePath, "utf-8");
57072
+ raw = import_fs70.default.readFileSync(filePath, "utf-8");
56501
57073
  } catch {
56502
57074
  return { redactedLines: 0, patterns: [] };
56503
57075
  }
@@ -56510,14 +57082,14 @@ function processJsonFile(filePath, dryRun) {
56510
57082
  const { value, modified, found } = redactJson(parsed);
56511
57083
  if (!modified) return { redactedLines: 0, patterns: [] };
56512
57084
  if (!dryRun) {
56513
- import_fs69.default.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
57085
+ import_fs70.default.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
56514
57086
  }
56515
57087
  return { redactedLines: 1, patterns: found };
56516
57088
  }
56517
57089
  function findJsonFiles(dir) {
56518
57090
  const results = [];
56519
- if (!import_fs69.default.existsSync(dir)) return results;
56520
- for (const entry of import_fs69.default.readdirSync(dir, { withFileTypes: true })) {
57091
+ if (!import_fs70.default.existsSync(dir)) return results;
57092
+ for (const entry of import_fs70.default.readdirSync(dir, { withFileTypes: true })) {
56521
57093
  const full = import_path66.default.join(dir, entry.name);
56522
57094
  if (entry.isDirectory()) results.push(...findJsonFiles(full));
56523
57095
  else if (entry.isFile() && entry.name.endsWith(".json")) results.push(full);
@@ -56537,7 +57109,7 @@ function registerMaskCommand(program2) {
56537
57109
  const cutoff = options.all ? null : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
56538
57110
  const filtered = cutoff ? allFiles.filter((f) => {
56539
57111
  try {
56540
- return import_fs69.default.statSync(f.path).mtime >= cutoff;
57112
+ return import_fs70.default.statSync(f.path).mtime >= cutoff;
56541
57113
  } catch {
56542
57114
  return false;
56543
57115
  }
@@ -56593,7 +57165,7 @@ function registerMaskCommand(program2) {
56593
57165
  // src/cli.ts
56594
57166
  init_blast();
56595
57167
  var { version } = JSON.parse(
56596
- import_fs72.default.readFileSync(import_path69.default.join(__dirname, "../package.json"), "utf-8")
57168
+ import_fs73.default.readFileSync(import_path69.default.join(__dirname, "../package.json"), "utf-8")
56597
57169
  );
56598
57170
  var program = new import_commander.Command();
56599
57171
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
@@ -56773,14 +57345,14 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
56773
57345
  }
56774
57346
  if (options.purge) {
56775
57347
  const node9Dir = import_path69.default.join(import_os62.default.homedir(), ".node9");
56776
- if (import_fs72.default.existsSync(node9Dir)) {
57348
+ if (import_fs73.default.existsSync(node9Dir)) {
56777
57349
  const confirmed = await (0, import_prompts2.confirm)({
56778
57350
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
56779
57351
  default: false
56780
57352
  });
56781
57353
  if (confirmed) {
56782
- import_fs72.default.rmSync(node9Dir, { recursive: true });
56783
- if (import_fs72.default.existsSync(node9Dir)) {
57354
+ import_fs73.default.rmSync(node9Dir, { recursive: true });
57355
+ if (import_fs73.default.existsSync(node9Dir)) {
56784
57356
  console.error(
56785
57357
  import_chalk41.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
56786
57358
  );
@@ -56945,12 +57517,12 @@ Run "node9 addto claude" to register it as the statusLine.`
56945
57517
  if (subcommand === "debug") {
56946
57518
  const flagFile = import_path69.default.join(import_os62.default.homedir(), ".node9", "hud-debug");
56947
57519
  if (state === "on") {
56948
- import_fs72.default.mkdirSync(import_path69.default.dirname(flagFile), { recursive: true });
56949
- import_fs72.default.writeFileSync(flagFile, "");
57520
+ import_fs73.default.mkdirSync(import_path69.default.dirname(flagFile), { recursive: true });
57521
+ import_fs73.default.writeFileSync(flagFile, "");
56950
57522
  console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
56951
57523
  console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
56952
57524
  } else if (state === "off") {
56953
- if (import_fs72.default.existsSync(flagFile)) import_fs72.default.unlinkSync(flagFile);
57525
+ if (import_fs73.default.existsSync(flagFile)) import_fs73.default.unlinkSync(flagFile);
56954
57526
  console.log("HUD debug logging disabled.");
56955
57527
  } else {
56956
57528
  console.error("Usage: node9 hud debug on|off");
@@ -57075,7 +57647,7 @@ if (process.argv[2] !== "daemon") {
57075
57647
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
57076
57648
  const logPath = import_path69.default.join(import_os62.default.homedir(), ".node9", "hook-debug.log");
57077
57649
  const msg = reason instanceof Error ? reason.message : String(reason);
57078
- import_fs72.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
57650
+ import_fs73.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
57079
57651
  `);
57080
57652
  }
57081
57653
  process.exit(0);