@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.
- package/dist/{acp-CXLANMEZ.js → acp-IJ55NBMJ.js} +5 -3
- package/dist/acp-mcp-servers.d.ts +2 -0
- package/dist/arg-parser.d.ts +6 -0
- package/dist/{auth-3K6EN5JP.js → auth-YXK3EDUL.js} +9 -3
- package/dist/{chunk-EK2P53GL.js → chunk-C3Z4N6A6.js} +23 -3
- package/dist/{chunk-7K3IJGWC.js → chunk-OJR7K3OI.js} +8 -8
- package/dist/{chunk-SXJTVNWF.js → chunk-OWNC7LMD.js} +2 -2
- package/dist/{chunk-FSZ5QU3M.js → chunk-RDHFXOJM.js} +153 -69
- package/dist/{chunk-5VPDKQDH.js → chunk-TNZBQG45.js} +3 -3
- package/dist/{chunk-7FPN3QTH.js → chunk-YRC3I3LE.js} +172 -15
- package/dist/{cli-context-6GMXUOXD.js → cli-context-5IF7ZYEY.js} +7 -7
- package/dist/{cli-main-BU5DEHDO.js → cli-main-6LMQSS66.js} +84 -42
- package/dist/{diag-doctor-PCER6KHH.js → diag-doctor-ANPRUYFM.js} +13 -3
- package/dist/{execution-4RXQLDAF.js → execution-EXDRBS4F.js} +2 -2
- package/dist/{hq-DQTUE7RC.js → hq-NO2NWGU4.js} +153 -5
- package/dist/hq-server/auth-state.d.ts +11 -0
- package/dist/hq-server/mailbox-gateway-health.d.ts +67 -0
- package/dist/hq-server/mailbox-gateway-manager.d.ts +38 -0
- package/dist/hq-server/routes/system-handlers.d.ts +27 -0
- package/dist/hq-server/routes.d.ts +11 -0
- package/dist/hq-server/snapshot.d.ts +14 -1
- package/dist/hq-server/upgrade-handler.d.ts +7 -0
- package/dist/{hq-server-JJFGFWI5.js → hq-server-KE5YDNGG.js} +2 -2
- package/dist/index.js +4 -4
- package/dist/{mcp-FX7TKPTV.js → mcp-DC5WSBOO.js} +28 -10
- package/dist/mcp-serve.d.ts +9 -0
- package/dist/services/mcp-management.d.ts +0 -6
- package/dist/{short-circuit-flags-LHUNLCZY.js → short-circuit-flags-72E77TJI.js} +3 -3
- package/dist/{subcommands-UXWY3OPC.js → subcommands-EJI7DBQS.js} +2 -2
- package/dist/wiring/codebase-index.d.ts +8 -0
- package/package.json +28 -28
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
removeSuggestions,
|
|
7
7
|
runGit,
|
|
8
8
|
setSuggestions
|
|
9
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
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
|
-
|
|
8589
|
-
|
|
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 ??
|
|
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]
|
|
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 =
|
|
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
|
|
11415
|
-
|
|
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 ??
|
|
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 {
|
|
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
|
|
35791
|
-
|
|
35792
|
-
|
|
35793
|
-
|
|
35794
|
-
|
|
35795
|
-
|
|
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
|
-
|
|
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
|
|
36726
|
-
const
|
|
36727
|
-
|
|
36728
|
-
|
|
36729
|
-
|
|
36730
|
-
|
|
36731
|
-
|
|
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-
|
|
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,
|
|
332
|
-
if (
|
|
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-
|
|
395
|
+
//# sourceMappingURL=diag-doctor-ANPRUYFM.js.map
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
runGit,
|
|
5
5
|
setAutoSuggestions,
|
|
6
6
|
setSuggestions
|
|
7
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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((
|
|
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
|
-
|
|
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-
|
|
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 {};
|