@wrongstack/cli 1.0.10 → 1.0.12

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.
Files changed (31) hide show
  1. package/dist/{acp-CXLANMEZ.js → acp-IJ55NBMJ.js} +5 -3
  2. package/dist/acp-mcp-servers.d.ts +2 -0
  3. package/dist/arg-parser.d.ts +6 -0
  4. package/dist/{auth-3K6EN5JP.js → auth-YXK3EDUL.js} +9 -3
  5. package/dist/{chunk-EK2P53GL.js → chunk-C3Z4N6A6.js} +23 -3
  6. package/dist/{chunk-7K3IJGWC.js → chunk-OJR7K3OI.js} +8 -8
  7. package/dist/{chunk-SXJTVNWF.js → chunk-OWNC7LMD.js} +2 -2
  8. package/dist/{chunk-FSZ5QU3M.js → chunk-RDHFXOJM.js} +153 -69
  9. package/dist/{chunk-5VPDKQDH.js → chunk-TNZBQG45.js} +3 -3
  10. package/dist/{chunk-7FPN3QTH.js → chunk-YRC3I3LE.js} +172 -15
  11. package/dist/{cli-context-6GMXUOXD.js → cli-context-5IF7ZYEY.js} +7 -7
  12. package/dist/{cli-main-BU5DEHDO.js → cli-main-6LMQSS66.js} +84 -42
  13. package/dist/{diag-doctor-PCER6KHH.js → diag-doctor-ANPRUYFM.js} +13 -3
  14. package/dist/{execution-4RXQLDAF.js → execution-EXDRBS4F.js} +2 -2
  15. package/dist/{hq-DQTUE7RC.js → hq-NO2NWGU4.js} +153 -5
  16. package/dist/hq-server/auth-state.d.ts +11 -0
  17. package/dist/hq-server/mailbox-gateway-health.d.ts +67 -0
  18. package/dist/hq-server/mailbox-gateway-manager.d.ts +38 -0
  19. package/dist/hq-server/routes/system-handlers.d.ts +27 -0
  20. package/dist/hq-server/routes.d.ts +11 -0
  21. package/dist/hq-server/snapshot.d.ts +14 -1
  22. package/dist/hq-server/upgrade-handler.d.ts +7 -0
  23. package/dist/{hq-server-JJFGFWI5.js → hq-server-KE5YDNGG.js} +2 -2
  24. package/dist/index.js +4 -4
  25. package/dist/{mcp-FX7TKPTV.js → mcp-DC5WSBOO.js} +28 -10
  26. package/dist/mcp-serve.d.ts +9 -0
  27. package/dist/services/mcp-management.d.ts +0 -6
  28. package/dist/{short-circuit-flags-LHUNLCZY.js → short-circuit-flags-72E77TJI.js} +3 -3
  29. package/dist/{subcommands-UXWY3OPC.js → subcommands-EJI7DBQS.js} +2 -2
  30. package/dist/wiring/codebase-index.d.ts +8 -0
  31. package/package.json +28 -28
@@ -6,7 +6,7 @@ import {
6
6
  removeSuggestions,
7
7
  runGit,
8
8
  setSuggestions
9
- } from "./chunk-SXJTVNWF.js";
9
+ } from "./chunk-OWNC7LMD.js";
10
10
  import {
11
11
  setupPlugins
12
12
  } from "./chunk-XJ7K67PP.js";
@@ -102,7 +102,7 @@ import {
102
102
  runAuthLocal,
103
103
  runProviderAuthLogin,
104
104
  validateFamily
105
- } from "./chunk-FSZ5QU3M.js";
105
+ } from "./chunk-RDHFXOJM.js";
106
106
  import "./chunk-2VMJ3E5Z.js";
107
107
  import {
108
108
  appendHistory
@@ -116,7 +116,7 @@ import {
116
116
  } from "./chunk-YMXXOOFN.js";
117
117
  import {
118
118
  parseSpawnFlags
119
- } from "./chunk-EK2P53GL.js";
119
+ } from "./chunk-C3Z4N6A6.js";
120
120
  import "./chunk-SIGUWXAA.js";
121
121
  import "./chunk-DAKVBMD4.js";
122
122
  import {
@@ -6231,7 +6231,7 @@ async function runCliExecution(params) {
6231
6231
  governanceHandle,
6232
6232
  setConfig
6233
6233
  } = params;
6234
- const { execute } = await import("./execution-4RXQLDAF.js");
6234
+ const { execute } = await import("./execution-EXDRBS4F.js");
6235
6235
  return execute(
6236
6236
  toExecuteDeps({
6237
6237
  core: {
@@ -8419,6 +8419,7 @@ ${diff}`;
8419
8419
  }
8420
8420
 
8421
8421
  // src/services/mcp-management.ts
8422
+ import { resolveMcpServerConfig } from "@wrongstack/core/infrastructure";
8422
8423
  import {
8423
8424
  color as color7,
8424
8425
  expectDefined as expectDefined4,
@@ -8534,7 +8535,7 @@ async function runAdd(name, enable, configured, configPath, mcpRegistry, all) {
8534
8535
  }
8535
8536
  try {
8536
8537
  if (mcpRegistry.list().some((server) => server.name === name)) {
8537
- await mcpRegistry.restart(name);
8538
+ await mcpRegistry.restart(name, { ...nextCfg, name });
8538
8539
  } else {
8539
8540
  await mcpRegistry.start(nextCfg);
8540
8541
  }
@@ -8572,24 +8573,21 @@ async function runRemove(name, configured, configPath, mcpRegistry) {
8572
8573
  async function runEnable(name, configured, configPath, mcpRegistry) {
8573
8574
  const cfg = configured[name];
8574
8575
  if (!cfg) return `Server "${name}" is not in config. Run \`/mcp add ${name} --enable\` first.`;
8576
+ const resolved = resolveMcpServerConfig(name, cfg);
8577
+ if (!resolved) return `Server "${name}" has no transport and matches no preset.`;
8578
+ const startCfg = { ...resolved, enabled: true };
8579
+ const registered = mcpRegistry.list().some((server) => server.name === name);
8575
8580
  if (cfg.enabled !== false) {
8576
- try {
8577
- await mcpRegistry.restart(name);
8578
- return `${color7.green("\u25CF")} "${name}" is already enabled and running.`;
8579
- } catch {
8580
- await mcpRegistry.start({ ...cfg, enabled: true });
8581
- return `${color7.green("Enabled")} "${name}" and started.`;
8582
- }
8581
+ if (registered) await mcpRegistry.restart(name, startCfg);
8582
+ else await mcpRegistry.start(startCfg);
8583
+ return `${color7.green("\u25CF")} "${name}" is already enabled and running.`;
8583
8584
  }
8584
8585
  await updateJsonObjectFile(configPath, (full) => {
8585
8586
  const current = isMcpServerRecord(full.mcpServers) ? full.mcpServers : {};
8586
8587
  setJsonPath(full, ["mcpServers", name], { ...cfg, ...current[name], enabled: true });
8587
8588
  });
8588
- try {
8589
- await mcpRegistry.restart(name);
8590
- } catch {
8591
- await mcpRegistry.start({ ...cfg, enabled: true });
8592
- }
8589
+ if (registered) await mcpRegistry.restart(name, startCfg);
8590
+ else await mcpRegistry.start(startCfg);
8593
8591
  return `${color7.green("Enabled")} "${name}" and started.`;
8594
8592
  }
8595
8593
  async function runDisable(name, configured, configPath, mcpRegistry) {
@@ -8642,6 +8640,8 @@ function stateBadge(state) {
8642
8640
  return color7.dim("\u25CB disconnected");
8643
8641
  case "failed":
8644
8642
  return color7.red("\u2717 failed");
8643
+ case "dormant":
8644
+ return color7.dim("\u25CC dormant (spawns on first call)");
8645
8645
  default:
8646
8646
  return color7.dim(state);
8647
8647
  }
@@ -8781,7 +8781,7 @@ async function checkUncommittedChanges(projectRoot) {
8781
8781
  stdout += data;
8782
8782
  });
8783
8783
  child.on("error", reject);
8784
- child.on("close", (code) => resolve9({ stdout, code: code ?? 0 }));
8784
+ child.on("close", (code) => resolve9({ stdout, code: code ?? 1 }));
8785
8785
  });
8786
8786
  if (!result.stdout.trim()) return void 0;
8787
8787
  const count = result.stdout.split("\n").filter(Boolean).length;
@@ -11230,10 +11230,10 @@ function buildCodebaseMapCommand(opts) {
11230
11230
  const check = /(^|\s)--check(\s|$)/.test(text);
11231
11231
  const enrich = /(^|\s)--enrich(\s|$)/.test(text);
11232
11232
  const embed = /(^|\s)--embed(\s|$)/.test(text);
11233
- const exportMatch = /(^|\s)--export(?:[= ]([^\s]+))?(\s|$)/.exec(text);
11233
+ const exportMatch = /(^|\s)--export(?:[= ]([^\s-][^\s]*))?(\s|$)/.exec(text);
11234
11234
  const maxFilesMatch = /--max-files[= ](\d+)/.exec(text);
11235
11235
  const tokensMatch = /--tokens[= ](\d+)/.exec(text);
11236
- const maxTokens = tokensMatch ? Number(tokensMatch[1]) : void 0;
11236
+ const maxTokens = tokensMatch ? Math.min(Math.max(Number(tokensMatch[1]), MIN_MAP_TOKENS), MAX_MAP_TOKENS) : void 0;
11237
11237
  const projectRoot = opts.projectRoot;
11238
11238
  try {
11239
11239
  if (embed) {
@@ -11321,6 +11321,8 @@ function buildCodebaseMapCommand(opts) {
11321
11321
  };
11322
11322
  }
11323
11323
  var DEFAULT_EXPORT_FILE = "atlas.html";
11324
+ var MIN_MAP_TOKENS = 100;
11325
+ var MAX_MAP_TOKENS = 2e4;
11324
11326
  function noIndexMessage() {
11325
11327
  return color17.yellow(
11326
11328
  `No codebase index for this project yet. Run ${color17.bold("/codebase-reindex")} first.`
@@ -11405,14 +11407,17 @@ function buildCodebaseReindexCommand(opts) {
11405
11407
  "refresh \u2014 e.g. after a large branch switch, merge, or external edit."
11406
11408
  ].join("\n"),
11407
11409
  async run(args, _ctx) {
11408
- const force = /\b(force|--force|-f)\b/.test(args.trim());
11410
+ const force = /(^|\s)(force|--force|-f)(\s|$)/.test(args.trim());
11409
11411
  opts.renderer.write(color18.dim(`${force ? "Rebuilding" : "Reindexing"} codebase index\u2026
11410
11412
  `));
11411
11413
  try {
11412
11414
  resetIndexCircuitBreaker();
11413
11415
  const r = await runStartupIndex({ projectRoot: opts.projectRoot, force });
11414
- const summary = `${color18.green("\u2713")} codebase index ${force ? "rebuilt" : "updated"} ` + color18.dim(`\u2014 ${r.symbolsIndexed} symbols \xB7 ${r.filesIndexed} files \xB7 ${r.durationMs}ms`) + (r.errors.length ? `
11415
- ${color18.yellow(` ${r.errors.length} file(s) had errors`)}` : "");
11416
+ const outcomes = r.fileOutcomes;
11417
+ const fileSummary = outcomes ? `${outcomes.parsed} parsed \xB7 ${outcomes.skipped} unchanged` + (outcomes.failed > 0 ? ` \xB7 ${outcomes.failed} failed` : "") : `${r.filesIndexed} files`;
11418
+ const summary = `${color18.green("\u2713")} codebase index ${force || r.autoRecovered ? "rebuilt" : "updated"} ` + color18.dim(`\u2014 ${r.symbolsIndexed} symbols \xB7 ${fileSummary} \xB7 ${r.durationMs}ms`) + (r.autoRecovered ? `
11419
+ ${color18.yellow(" corrupt index detected \u2014 rebuilt from scratch")}` : "") + (r.errors.length ? `
11420
+ ${color18.yellow(` ${r.errors.length} error(s) reported`)}` : "");
11416
11421
  return { message: summary };
11417
11422
  } catch (err) {
11418
11423
  const msg = `${color18.red("Codebase reindex failed:")} ${toErrorMessage7(err)}`;
@@ -28231,7 +28236,7 @@ async function runGit2(args, cwd) {
28231
28236
  stdout += d;
28232
28237
  });
28233
28238
  child.on("error", () => resolve9({ stdout, code: 1 }));
28234
- child.on("close", (code) => resolve9({ stdout, code: code ?? 0 }));
28239
+ child.on("close", (code) => resolve9({ stdout, code: code ?? 1 }));
28235
28240
  });
28236
28241
  }
28237
28242
  async function getChangedFiles(cwd) {
@@ -34711,7 +34716,7 @@ import * as path29 from "node:path";
34711
34716
  import { getSharedProjectMailbox as getSharedProjectMailbox10 } from "@wrongstack/core/coordination";
34712
34717
  import { CouncilOrchestrator, OneShotOrchestrator as OneShotOrchestrator3 } from "@wrongstack/core/execution";
34713
34718
  import { countShellHooks, HookRegistry as HookRegistry2, HookRunner as HookRunner2, shellHooksEqual } from "@wrongstack/core/hooks";
34714
- import { allServers as allServers3 } from "@wrongstack/core/infrastructure";
34719
+ import { resolveMcpServerConfig as resolveMcpServerConfig2 } from "@wrongstack/core/infrastructure";
34715
34720
  import { TOKENS as TOKENS11 } from "@wrongstack/core/kernel";
34716
34721
  import { NotifierImpl } from "@wrongstack/core/notifications";
34717
34722
  import {
@@ -35781,18 +35786,23 @@ async function setupLifecycleAndPlugins(deps) {
35781
35786
  log: logger,
35782
35787
  lazyMode: normalizeTokenSavingTier5(config.features.tokenSavingMode) !== "off",
35783
35788
  cacheDir: wpaths.cacheDir,
35789
+ // Presets say `--project-root .`; resolve it against the project, not
35790
+ // wherever this process happens to be running.
35791
+ cwd: wpaths.projectRoot,
35784
35792
  authorizationProviderFactory: createVaultBackedMcpAuthorizationProviderFactory({
35785
35793
  store: mcpTokenStore
35786
35794
  }),
35787
35795
  authorizationManager: mcpAuthorizationManager
35788
35796
  });
35789
35797
  if (config.features.mcp) {
35790
- const presets = allServers3();
35791
- for (const cfg of Object.values(config.mcpServers ?? {})) {
35792
- const preset = presets[cfg.name];
35793
- const merged = preset ? { ...preset, ...cfg } : cfg;
35794
- void mcpRegistry.start(merged).catch((err) => {
35795
- logger.warn(`MCP server "${cfg.name}" failed to start`, err);
35798
+ for (const [name, entry] of Object.entries(config.mcpServers ?? {})) {
35799
+ const resolved = resolveMcpServerConfig2(name, entry);
35800
+ if (!resolved) {
35801
+ logger.warn(`MCP server "${name}" has no transport and matches no preset \u2014 skipped`);
35802
+ continue;
35803
+ }
35804
+ void mcpRegistry.start(resolved).catch((err) => {
35805
+ logger.warn(`MCP server "${name}" failed to start`, err);
35796
35806
  });
35797
35807
  }
35798
35808
  }
@@ -36683,7 +36693,38 @@ import {
36683
36693
  setContextQueryEmbedder,
36684
36694
  shutdownCodebaseIndexHost
36685
36695
  } from "@wrongstack/tools";
36686
- var FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["write", "edit"]);
36696
+ function editedFilePaths(toolName2, input, cwd) {
36697
+ const args = input ?? {};
36698
+ const resolve9 = (value, base = cwd) => typeof value === "string" && value.length > 0 ? [path30.resolve(base, value)] : [];
36699
+ switch (toolName2) {
36700
+ case "write":
36701
+ case "edit":
36702
+ return resolve9(args["path"]);
36703
+ case "codebase-ast-replace":
36704
+ return resolve9(args["file"]);
36705
+ case "format": {
36706
+ if (args["check"] === true) return [];
36707
+ const files = args["files"];
36708
+ const base = typeof args["cwd"] === "string" ? path30.resolve(cwd, args["cwd"]) : cwd;
36709
+ return (Array.isArray(files) ? files : [files]).flatMap((file) => resolve9(file, base));
36710
+ }
36711
+ case "patch": {
36712
+ if (args["dry_run"] === true || typeof args["patch"] !== "string") return [];
36713
+ const base = typeof args["directory"] === "string" ? path30.resolve(cwd, args["directory"]) : cwd;
36714
+ const strip = typeof args["strip"] === "number" ? args["strip"] : 1;
36715
+ const out = /* @__PURE__ */ new Set();
36716
+ for (const match of args["patch"].matchAll(/^(?:\+\+\+|---) ([^\t\r\n]+)/gm)) {
36717
+ const header = (match[1] ?? "").trim().replace(/^"(.*)"$/, "$1");
36718
+ if (!header || header === "/dev/null") continue;
36719
+ const stripped = header.split("/").slice(strip).join("/");
36720
+ if (stripped) out.add(path30.resolve(base, stripped));
36721
+ }
36722
+ return [...out];
36723
+ }
36724
+ default:
36725
+ return [];
36726
+ }
36727
+ }
36687
36728
  async function setupCodebaseIndexing(deps) {
36688
36729
  const { config, context, pipelines, projectRoot, logger } = deps;
36689
36730
  const idx = config.indexing;
@@ -36722,16 +36763,17 @@ async function setupCodebaseIndexing(deps) {
36722
36763
  handler: async (payload, next) => {
36723
36764
  try {
36724
36765
  const tool = payload.tool;
36725
- if (tool?.mutating && FILE_EDIT_TOOLS.has(tool.name) && !payload.result.is_error) {
36726
- const fp = payload.toolUse.input?.path;
36727
- if (typeof fp === "string" && fp.length > 0) {
36728
- const activeRoot = payload.ctx.projectRoot;
36729
- const abs = path30.resolve(payload.ctx.cwd, fp);
36730
- const rel = path30.relative(activeRoot, abs);
36731
- const inside = rel !== ".." && !rel.startsWith(`..${path30.sep}`) && !path30.isAbsolute(rel);
36732
- if (inside && isIndexableFile(abs)) {
36733
- enqueueReindex({ projectRoot: activeRoot, files: [abs], debounceMs, onError });
36766
+ if (tool && !payload.result.is_error) {
36767
+ const activeRoot = payload.ctx.projectRoot;
36768
+ const files = editedFilePaths(tool.name, payload.toolUse.input, payload.ctx.cwd).filter(
36769
+ (abs) => {
36770
+ const rel = path30.relative(activeRoot, abs);
36771
+ const inside = rel !== ".." && !rel.startsWith(`..${path30.sep}`) && !path30.isAbsolute(rel);
36772
+ return inside && isIndexableFile(abs);
36734
36773
  }
36774
+ );
36775
+ if (files.length > 0) {
36776
+ enqueueReindex({ projectRoot: activeRoot, files, debounceMs, onError });
36735
36777
  }
36736
36778
  }
36737
36779
  } catch {
@@ -38950,4 +38992,4 @@ export {
38950
38992
  CLI_VERSION,
38951
38993
  runInteractive
38952
38994
  };
38953
- //# sourceMappingURL=cli-main-BU5DEHDO.js.map
38995
+ //# sourceMappingURL=cli-main-6LMQSS66.js.map
@@ -13,6 +13,7 @@ import {
13
13
  import * as fs2 from "node:fs/promises";
14
14
  import * as os from "node:os";
15
15
  import * as path2 from "node:path";
16
+ import { resolveMcpServerConfig } from "@wrongstack/core/infrastructure";
16
17
  import { color, toErrorMessage } from "@wrongstack/core/utils";
17
18
  import {
18
19
  applyProxyConfig,
@@ -328,8 +329,17 @@ var doctorCmd = async (args, deps) => {
328
329
  });
329
330
  }
330
331
  const mcpEntries = Object.entries(cfg.mcpServers ?? {});
331
- for (const [name, srv] of mcpEntries) {
332
- if (!srv.enabled) continue;
332
+ for (const [name, entry] of mcpEntries) {
333
+ if (entry.enabled === false) continue;
334
+ const srv = resolveMcpServerConfig(name, entry);
335
+ if (!srv) {
336
+ checks.push({
337
+ name: `mcp:${name}`,
338
+ status: "fail",
339
+ detail: "no transport and no built-in preset of this name"
340
+ });
341
+ continue;
342
+ }
333
343
  if ((srv.transport === "sse" || srv.transport === "streamable-http") && !srv.url)
334
344
  checks.push({ name: `mcp:${name}`, status: "fail", detail: "transport requires url" });
335
345
  else if (srv.transport === "stdio" && !srv.command)
@@ -382,4 +392,4 @@ export {
382
392
  doctorCmd,
383
393
  proxyCmd
384
394
  };
385
- //# sourceMappingURL=diag-doctor-PCER6KHH.js.map
395
+ //# sourceMappingURL=diag-doctor-ANPRUYFM.js.map
@@ -4,7 +4,7 @@ import {
4
4
  runGit,
5
5
  setAutoSuggestions,
6
6
  setSuggestions
7
- } from "./chunk-SXJTVNWF.js";
7
+ } from "./chunk-OWNC7LMD.js";
8
8
  import {
9
9
  deriveFsAccessPair,
10
10
  filterSafeForProject,
@@ -6953,4 +6953,4 @@ export {
6953
6953
  execute,
6954
6954
  resolveReviewerFallbackModels
6955
6955
  };
6956
- //# sourceMappingURL=execution-4RXQLDAF.js.map
6956
+ //# sourceMappingURL=execution-EXDRBS4F.js.map
@@ -14,9 +14,11 @@ import {
14
14
 
15
15
  // src/subcommands/handlers/hq.ts
16
16
  import * as fs from "node:fs/promises";
17
+ import * as path from "node:path";
17
18
  import {
18
19
  HQ_AUTH_FILE_VERSION,
19
20
  HQ_CLI_DEFAULT_HOST,
21
+ HqAlertEngine,
20
22
  HqInsecureExposureError,
21
23
  hqAuthAuditPath,
22
24
  hqAuthContentHash,
@@ -25,6 +27,7 @@ import {
25
27
  logHqAuthAudit,
26
28
  mintHqToken,
27
29
  mutateHqAuthFile,
30
+ readHqAlertsConfig,
28
31
  readHqAuthFile,
29
32
  resolveHqDataDir
30
33
  } from "@wrongstack/core/hq";
@@ -48,6 +51,9 @@ var hqCmd = async (args, deps) => {
48
51
  if (sub === "audit") {
49
52
  return hqAuditCmd(args.slice(1), deps);
50
53
  }
54
+ if (sub === "alerts") {
55
+ return hqAlertsCmd(args.slice(1), deps);
56
+ }
51
57
  if (sub === "service") {
52
58
  const { hqServiceCmd } = await import("./hq-service-L5INQFM4.js");
53
59
  return hqServiceCmd(args.slice(1), deps);
@@ -62,7 +68,7 @@ var hqCmd = async (args, deps) => {
62
68
  return 1;
63
69
  };
64
70
  async function startServer(deps) {
65
- const { startHqServer } = await import("./hq-server-JJFGFWI5.js");
71
+ const { startHqServer } = await import("./hq-server-KE5YDNGG.js");
66
72
  const dataDir = resolveDataDir(deps);
67
73
  const flags = deps.flags ?? {};
68
74
  const rawPublicUrl = typeof flags["hq-public-url"] === "string" ? flags["hq-public-url"] : process.env.WRONGSTACK_HQ_PUBLIC_URL;
@@ -188,7 +194,7 @@ async function startServer(deps) {
188
194
  ipAllowlist === void 0 ? "Network allowlist: disabled (authentication remains required when configured).\n" : `Network allowlist: active (${ipAllowlist.length} configured rule${ipAllowlist.length === 1 ? "" : "s"} + loopback).
189
195
  `
190
196
  );
191
- await new Promise((resolve) => {
197
+ await new Promise((resolve2) => {
192
198
  const shutdown = async () => {
193
199
  try {
194
200
  await handle.close();
@@ -196,7 +202,7 @@ async function startServer(deps) {
196
202
  deps.renderer.write(`HQ server close error: ${String(err)}
197
203
  `);
198
204
  }
199
- resolve();
205
+ resolve2();
200
206
  };
201
207
  process.on("SIGINT", shutdown);
202
208
  process.on("SIGTERM", shutdown);
@@ -807,7 +813,7 @@ function printAuditHelp(deps) {
807
813
  `
808
814
  );
809
815
  deps.renderer.write(
810
- ` auth.json and print it so an operator can compare
816
+ ` on-disk auth.json and print it so an operator can compare
811
817
  `
812
818
  );
813
819
  deps.renderer.write(
@@ -825,8 +831,150 @@ function printAuditHelp(deps) {
825
831
  deps.renderer.write(`Run \`wstack hq --help\` for the full HQ command list.
826
832
  `);
827
833
  }
834
+ async function hqAlertsCmd(args, deps) {
835
+ const action = args[0];
836
+ if (deps.flags?.["help"] === true || action === "help" || action === "--help") {
837
+ printAlertsHelp(deps);
838
+ return 0;
839
+ }
840
+ if (action === "eval" || action === void 0) {
841
+ return hqAlertsEval(args.slice(1), deps);
842
+ }
843
+ deps.renderer.writeError(`Unknown hq alerts subcommand: ${action ?? "(none)"}
844
+ `);
845
+ printAlertsHelp(deps);
846
+ return 1;
847
+ }
848
+ async function hqAlertsEval(_args, deps) {
849
+ const flags = deps.flags ?? {};
850
+ const snapshotArg = typeof flags["snapshot"] === "string" ? flags["snapshot"] : void 0;
851
+ let snapshotPath;
852
+ try {
853
+ if (snapshotArg !== void 0) {
854
+ snapshotPath = path.resolve(snapshotArg);
855
+ } else {
856
+ const dataDir2 = resolveDataDir(deps);
857
+ snapshotPath = path.join(dataDir2, "snapshot.json");
858
+ }
859
+ } catch (err) {
860
+ deps.renderer.writeError(`Failed to resolve snapshot path: ${err.message}
861
+ `);
862
+ return 2;
863
+ }
864
+ let raw;
865
+ try {
866
+ raw = await fs.readFile(snapshotPath, "utf8");
867
+ } catch (err) {
868
+ deps.renderer.writeError(
869
+ `Cannot read snapshot at ${snapshotPath}: ${err.message}
870
+ Pass --snapshot <path> to evaluate against a specific file.
871
+ `
872
+ );
873
+ return 2;
874
+ }
875
+ let snapshot;
876
+ try {
877
+ snapshot = JSON.parse(raw);
878
+ } catch (err) {
879
+ deps.renderer.writeError(
880
+ `Snapshot at ${snapshotPath} is not valid JSON: ${err.message}
881
+ `
882
+ );
883
+ return 2;
884
+ }
885
+ const dataDir = resolveDataDir(deps);
886
+ let thresholds;
887
+ try {
888
+ const config = await readHqAlertsConfig(dataDir);
889
+ thresholds = config.thresholds;
890
+ } catch {
891
+ }
892
+ const engine = new HqAlertEngine({
893
+ onAlert: () => {
894
+ }
895
+ });
896
+ let fired;
897
+ try {
898
+ fired = engine.evaluate(snapshot, thresholds);
899
+ } catch (err) {
900
+ deps.renderer.writeError(`Alert evaluation failed: ${err.message}
901
+ `);
902
+ return 2;
903
+ }
904
+ if (fired.length === 0) {
905
+ deps.renderer.write(`No alert rules fired against ${snapshotPath}.
906
+ `);
907
+ if (thresholds === void 0) {
908
+ deps.renderer.write(
909
+ "(No persisted thresholds; using built-in defaults. Run `wstack hq alerts` with a populated alerts-config.json to override.)\n"
910
+ );
911
+ }
912
+ return 0;
913
+ }
914
+ deps.renderer.write(`${fired.length} alert rule(s) fired against ${snapshotPath}:
915
+ `);
916
+ for (const alert of fired) {
917
+ deps.renderer.write(` [${alert.severity}] ${alert.ruleId}: ${alert.message}
918
+ `);
919
+ }
920
+ return 1;
921
+ }
922
+ function printAlertsHelp(deps) {
923
+ deps.renderer.write(`Usage: wstack hq alerts <eval>
924
+ `);
925
+ deps.renderer.write("\n");
926
+ deps.renderer.write(
927
+ ` wstack hq alerts eval [--snapshot <path>] Run HqAlertEngine.evaluate() against a
928
+ `
929
+ );
930
+ deps.renderer.write(
931
+ ` snapshot file (or the live <dataDir>/
932
+ `
933
+ );
934
+ deps.renderer.write(
935
+ ` snapshot.json). Pure-function dry run;
936
+ `
937
+ );
938
+ deps.renderer.write(
939
+ ` no server required. Exits 0 when no
940
+ `
941
+ );
942
+ deps.renderer.write(
943
+ ` rules fire, 1 when rules fire, 2 on errors.
944
+ `
945
+ );
946
+ deps.renderer.write("\n");
947
+ deps.renderer.write(
948
+ `Persisted thresholds (<dataDir>/alerts-config.json) are loaded automatically when
949
+ `
950
+ );
951
+ deps.renderer.write(
952
+ `present; otherwise the engine's built-in defaults apply. Snoozes are NOT honored
953
+ `
954
+ );
955
+ deps.renderer.write(
956
+ `by this command \u2014 eval is a "what would fire RIGHT NOW if there were no snoozes"
957
+ `
958
+ );
959
+ deps.renderer.write(`probe.
960
+ `);
961
+ deps.renderer.write("\n");
962
+ deps.renderer.write(`Flags:
963
+ `);
964
+ deps.renderer.write(
965
+ ` --snapshot <path> Evaluate against this snapshot file instead of the live one.
966
+ `
967
+ );
968
+ deps.renderer.write(
969
+ ` --data-dir <path> Override HQ data directory (default ~/.wrongstack/hq).
970
+ `
971
+ );
972
+ deps.renderer.write("\n");
973
+ deps.renderer.write(`Run \`wstack hq --help\` for the full HQ command list.
974
+ `);
975
+ }
828
976
  export {
829
977
  hqCmd,
830
978
  resolveAuditActor
831
979
  };
832
- //# sourceMappingURL=hq-DQTUE7RC.js.map
980
+ //# sourceMappingURL=hq-NO2NWGU4.js.map
@@ -26,6 +26,17 @@ interface HqAuthStateOptions {
26
26
  * makes the latch unconditional.
27
27
  */
28
28
  onApplied?: ((mutableAuth: HqRouterMutableAuth) => void) | undefined;
29
+ /**
30
+ * Invoked when browser tokens stopped being live — revoked by hand, or aged
31
+ * out — during an {@link HqAuthState.apply}. `keys` are the stored verifiers,
32
+ * `ids` the token ids of those that had one.
33
+ *
34
+ * W4 #15: an open WebSocket never re-presents its credential, so before this
35
+ * a revoked operator kept a working dashboard until the idle-eviction pass
36
+ * happened to catch them. Reporting the change at the single projection
37
+ * choke point is what lets the server close those sockets immediately.
38
+ */
39
+ onTokensRevoked?: ((keys: readonly string[], ids: readonly string[]) => void) | undefined;
29
40
  }
30
41
  /**
31
42
  * Project an `auth.json` document onto the live `mutableAuth` used by every
@@ -0,0 +1,67 @@
1
+ /**
2
+ * W2 #14 (RFC hq-improvements-2026-09.md): Mailbox gateway health types.
3
+ *
4
+ * These types are the wire contract for `/api/health/mailbox`. They live in
5
+ * their own module so both the gateway manager (producer) and the HTTP
6
+ * handler (consumer) can import them without a circular dependency.
7
+ *
8
+ * Critical contract (from SAGE memory, surfaced multiple times in this work):
9
+ *
10
+ * - Mailbox credential `projectId` is the **basename** of the project
11
+ * directory, not the full path. `mailbox-serve.ts:192` derives it via
12
+ * `path.basename(projectDir)`. The dashboard MUST render `projectId`
13
+ * from this surface so it matches what other mailbox surfaces see.
14
+ *
15
+ * - HQ's `MailboxGatewayManager` binds gateways with the **full filesystem
16
+ * path** (from `resolveHqProjectRoot(...)` in `mailbox-handlers.ts:90`).
17
+ * We surface the full path as `projectRoot` so operators can debug
18
+ * path-mismatch issues without grepping the source.
19
+ *
20
+ * - HQ never attaches a `MailboxActorContext` to its mailbox authorization
21
+ * decisions (see `authorizeMailboxGateway` in `mailbox-gateway-manager.ts:57-96`).
22
+ * The aggregate `actorAttached: false` flag surfaces this truth so the
23
+ * dashboard can render "no actor context" honestly.
24
+ *
25
+ * @module hq-server/mailbox-gateway-health
26
+ */
27
+ /**
28
+ * One mailbox gateway entry — one per bound `projectDir` in the manager.
29
+ */
30
+ export interface HqMailboxGatewayHealthEntry {
31
+ /**
32
+ * Credential contract key: `path.basename(projectRoot)`. Matches the
33
+ * `projectId` used by `mailbox-serve.ts` and other mailbox surfaces.
34
+ */
35
+ projectId: string;
36
+ /** Full filesystem path the gateway is bound to. */
37
+ projectRoot: string;
38
+ /** Whether the gateway has any open HTTP streams right now. */
39
+ hasActiveStreams: boolean;
40
+ /** Epoch ms of the last `getMailboxGateway` / `authorizeMailboxGateway` hit, or null if never used. */
41
+ lastUsedAt: number | null;
42
+ /** Convenience: `Date.now() - lastUsedAt`, or null when `lastUsedAt` is null. */
43
+ idleForMs: number | null;
44
+ }
45
+ /**
46
+ * Aggregate health snapshot for the HQ cockpit "Mailbox gateway" card.
47
+ */
48
+ export interface HqMailboxGatewayHealth {
49
+ /** Total number of bound gateways. */
50
+ gatewayCount: number;
51
+ /** Whether the rate limiter is configured. Always `true` on the HQ mount. */
52
+ rateLimiterConfigured: boolean;
53
+ /**
54
+ * Whether the gateway manager attaches a `MailboxActorContext` to its
55
+ * authorization decisions. HQ is a read-mostly operator dashboard and
56
+ * never attaches one — this flag is `false` on the HQ mount and is
57
+ * surfaced so the dashboard can render the truthful state.
58
+ */
59
+ actorAttached: boolean;
60
+ /** Per-gateway health entries, sorted by `projectId` for stable rendering. */
61
+ gateways: readonly HqMailboxGatewayHealthEntry[];
62
+ /** Idle-eviction sweep interval in ms (mirrors the manager's private constant). */
63
+ sweepIntervalMs: number;
64
+ /** Idle TTL after which a gateway is eligible for eviction. */
65
+ idleTtlMs: number;
66
+ }
67
+ //# sourceMappingURL=mailbox-gateway-health.d.ts.map
@@ -6,6 +6,7 @@
6
6
  import type { IncomingMessage } from 'node:http';
7
7
  import { type MailboxHttpAccessDecision, MailboxHttpRateLimiter } from '@wrongstack/core/coordination';
8
8
  import type { HqAuthState } from './auth-state.js';
9
+ import type { HqMailboxGatewayHealth } from './mailbox-gateway-health.js';
9
10
  import { type HqRouterMailboxGateway } from './routes.js';
10
11
  import type { HqSessionEntry } from './types.js';
11
12
  interface MailboxGatewayManagerDeps {
@@ -27,6 +28,43 @@ export declare class MailboxGatewayManager {
27
28
  authorizeMailboxGateway(req: IncomingMessage, projectDir: string): MailboxHttpAccessDecision;
28
29
  getMailboxGateway(projectDir: string): HqRouterMailboxGateway;
29
30
  private evictIdleGateways;
31
+ /**
32
+ * W2 #14 (RFC hq-improvements-2026-09.md): health snapshot for the cockpit
33
+ * "Mailbox gateway" card.
34
+ *
35
+ * Per-gateway entry surfaces the three diagnostic facts an operator
36
+ * needs:
37
+ *
38
+ * - `projectId`: the **basename** of the project directory, matching
39
+ * the credential contract used by `mailbox-serve.ts:192` (where
40
+ * `path.basename(projectDir)` is the `projectId` key for mailbox
41
+ * data). HQ binds the gateway with the full filesystem path
42
+ * (`resolveHqProjectRoot(...)` from `mailbox-handlers.ts:90`), so
43
+ * reporting the basename here means the dashboard's projectId
44
+ * matches what other mailbox surfaces see.
45
+ * - `projectRoot`: the full filesystem path the manager actually
46
+ * binds against. Surfaced separately so operators can debug
47
+ * path-mismatch issues without grepping the source.
48
+ * - `hasActiveStreams`: whether the gateway has any open HTTP streams
49
+ * right now. A gateway with `false` is a candidate for the next
50
+ * idle eviction; one with `true` is being watched.
51
+ * - `lastUsedAt`: epoch ms of the most recent `getMailboxGateway()`
52
+ * call (or the last `authorizeMailboxGateway` hit that resolved to
53
+ * this gateway). Surfaced so the dashboard can render the same
54
+ * "fresh / quiet / stale" staleness buckets the publisher health
55
+ * tile uses.
56
+ * - `idleForMs`: convenience — `Date.now() - lastUsedAt`. `Infinity`
57
+ * when the gateway was never used (shouldn't happen, since
58
+ * `getMailboxGateway` always stamps `mailboxGatewayLastUsed`).
59
+ *
60
+ * The aggregate `actor` field is **explicitly absent** on the HQ mount:
61
+ * `authorizeMailboxGateway` (L57-96) never attaches a `MailboxActorContext`
62
+ * to its decision, because HQ is a read-mostly operator dashboard, not
63
+ * a producer-side mailbox client. We surface this truth with a stable
64
+ * `actorAttached: false` so the dashboard can render "no actor
65
+ * context" honestly rather than papering over it.
66
+ */
67
+ getHealth(): HqMailboxGatewayHealth;
30
68
  close(): void;
31
69
  }
32
70
  export {};