@wrongstack/cli 1.0.10 → 1.0.11

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 (30) 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-WC5JNIWY.js} +32 -27
  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/package.json +28 -28
@@ -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 {};
@@ -7,9 +7,36 @@
7
7
  import type * as http from 'node:http';
8
8
  import type { HqAlertEngine, HqCommandAuditLog, HqEventEnvelope, HqPersistence } from '@wrongstack/core/hq';
9
9
  import type { WebSocket } from 'ws';
10
+ import type { HqMailboxGatewayHealth } from '../mailbox-gateway-health.js';
10
11
  import type { ConnectedClient } from '../types.js';
11
12
  export declare function handleApiSystemUpdate(res: http.ServerResponse): Promise<void>;
12
13
  export declare function handleApiSystemHealth(res: http.ServerResponse, clients: Map<WebSocket, ConnectedClient>, persistence: HqPersistence, eventLog: HqEventEnvelope[]): Promise<void>;
13
14
  export declare function handleApiCommandsAudit(req: http.IncomingMessage, res: http.ServerResponse, auditLog: HqCommandAuditLog): Promise<void>;
14
15
  export declare function handleApiAlerts(req: http.IncomingMessage, res: http.ServerResponse, alertEngine: HqAlertEngine): Promise<void>;
16
+ /**
17
+ * Structural view of the gateway manager this handler needs. Typing the
18
+ * parameter structurally — rather than importing `MailboxGatewayManager` —
19
+ * keeps this module out of the type-inclusive cycle
20
+ * `mailbox-gateway-manager → routes → system-handlers`, which
21
+ * `check:architecture` reports as unexcepted. The real manager satisfies this
22
+ * shape, so callers and the wire contract are unaffected.
23
+ */
24
+ interface MailboxHealthSource {
25
+ getHealth(): HqMailboxGatewayHealth;
26
+ }
27
+ /**
28
+ * W2 #14 (RFC hq-improvements-2026-09.md): GET /api/health/mailbox —
29
+ * mailbox gateway health snapshot for the cockpit's "Mailbox gateway" card.
30
+ *
31
+ * Surfaces {@link MailboxHealthSource.getHealth} verbatim. The contract
32
+ * (basename as `projectId`, full path as `projectRoot`, `actorAttached: false`
33
+ * on the HQ mount, sorted gateways) is locked by the focused test in
34
+ * `packages/cli/tests/hq-mailbox-gateway-health.test.ts`.
35
+ *
36
+ * No query parameters. No mutation. Same auth contract as
37
+ * `/api/system/health` — gated by `requireBrowserAuth` upstream in the
38
+ * router, not duplicated here.
39
+ */
40
+ export declare function handleApiMailboxHealth(res: http.ServerResponse, mailboxManager: MailboxHealthSource): void;
41
+ export {};
15
42
  //# sourceMappingURL=system-handlers.d.ts.map
@@ -13,6 +13,7 @@ import type { createHqPersistence, HqAlertEngine, HqCommandAuditLog, HqEventEnve
13
13
  import type { TrustBoundary } from '@wrongstack/core/security';
14
14
  import type { WebSocket } from 'ws';
15
15
  import * as HqServerAuth from './auth.js';
16
+ import type { HqMailboxGatewayHealth } from './mailbox-gateway-health.js';
16
17
  import { type ApplyHqAuthFile } from './routes/auth-handlers.js';
17
18
  import * as HqServerUtils from './utils.js';
18
19
  export declare const setHqSecurityHeaders: typeof HqServerAuth.setHqSecurityHeaders;
@@ -73,6 +74,16 @@ export interface HqRouterDeps {
73
74
  agentMessages: Map<string, HqTranscriptEntry[]>;
74
75
  mailboxGateways: Map<string, HqRouterMailboxGateway>;
75
76
  mailboxGatewayRateLimiter: MailboxHttpRateLimiter;
77
+ /**
78
+ * W2 #14 (RFC hq-improvements-2026-09.md): mailbox gateway manager used
79
+ * by `/api/health/mailbox` to surface the cockpit "Mailbox gateway"
80
+ * card. Same manager that owns `mailboxGateways` and
81
+ * `mailboxGatewayRateLimiter` — passed explicitly so the handler can
82
+ * call `getHealth()` without reaching into a closure.
83
+ */
84
+ mailboxManager: {
85
+ getHealth(): HqMailboxGatewayHealth;
86
+ };
76
87
  alertEngine: HqAlertEngine;
77
88
  auditLog: HqCommandAuditLog;
78
89
  persistence: ReturnType<typeof createHqPersistence>;
@@ -53,8 +53,21 @@ export declare const HQ_STALE_SNAPSHOT_MS: number;
53
53
  export declare function reapStaleClientState(clients: Map<WebSocket, ConnectedClient>, nowMs?: number): void;
54
54
  export declare function buildSnapshot(clients: Map<WebSocket, ConnectedClient>, options?: {
55
55
  tokenStats?: HqSnapshot['totals']['tokenStats'];
56
+ /**
57
+ * Recent command-audit entries (W4 #7). The caller owns the audit ring, so
58
+ * it hands over the window rather than the builder reaching for a global.
59
+ */
60
+ commandAudit?: readonly HqCommandAuditEntry[];
56
61
  }): HqSnapshot;
57
- export declare function createSnapshotBroadcaster(clients: Map<WebSocket, ConnectedClient>, browsers: Set<WebSocket>, persistence?: HqPersistence): HqSnapshotBroadcaster;
62
+ export declare function createSnapshotBroadcaster(clients: Map<WebSocket, ConnectedClient>, browsers: Set<WebSocket>, persistence?: HqPersistence, options?: {
63
+ /**
64
+ * W4 #7: command-audit window for the latency roll-up. Passed as a
65
+ * provider rather than an array because the broadcaster caches its
66
+ * serialized frame and only rebuilds when marked dirty — reading the ring
67
+ * at serialize time is what keeps the cached frame current.
68
+ */
69
+ commandAudit?: () => readonly HqCommandAuditEntry[];
70
+ }): HqSnapshotBroadcaster;
58
71
  export declare function buildProjectDetail(clients: Map<WebSocket, ConnectedClient>, projectId: string): ProjectDetail | null;
59
72
  export declare function broadcastEvent(event: HqEventEnvelope, browsers: Set<WebSocket>): void;
60
73
  /** Push one control command's lifecycle to every authenticated HQ browser. */
@@ -21,6 +21,13 @@ interface HqUpgradeHandlerDeps {
21
21
  sessions: Map<string, HqSessionEntry>;
22
22
  clients: Map<WebSocket, ConnectedClient>;
23
23
  clientSocketTokens: Map<WebSocket, HqToken | undefined>;
24
+ /**
25
+ * Session id that authorized each open browser socket, so token revocation
26
+ * can close only the affected browsers. Absent for a socket that
27
+ * authenticated with a bare loopback `?token=` rather than a cookie session;
28
+ * the revocation path treats that absence as "affected" (fail closed).
29
+ */
30
+ browserSocketSessions: Map<WebSocket, string>;
24
31
  browsers: Set<WebSocket>;
25
32
  eventLog: import('@wrongstack/core/hq').HqEventEnvelope[];
26
33
  transcripts: Map<string, TranscriptRing>;
@@ -10,7 +10,7 @@ import {
10
10
  readLocalSubagentTranscript,
11
11
  sanitizeApiError,
12
12
  startHqServer
13
- } from "./chunk-7FPN3QTH.js";
13
+ } from "./chunk-YRC3I3LE.js";
14
14
  import "./chunk-HXJYHAR4.js";
15
15
  import "./chunk-5EA6OTLJ.js";
16
16
  import "./chunk-Q7E3BPDU.js";
@@ -27,4 +27,4 @@ export {
27
27
  sanitizeApiError,
28
28
  startHqServer
29
29
  };
30
- //# sourceMappingURL=hq-server-JJFGFWI5.js.map
30
+ //# sourceMappingURL=hq-server-KE5YDNGG.js.map
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-XJXDOF63.js";
9
9
  import {
10
10
  parseArgs
11
- } from "./chunk-EK2P53GL.js";
11
+ } from "./chunk-C3Z4N6A6.js";
12
12
 
13
13
  // src/cli-entry-main.ts
14
14
  async function main(argv) {
@@ -16,14 +16,14 @@ async function main(argv) {
16
16
  applySessionShellDefault();
17
17
  const earlyFlags = parseArgs(argv).flags;
18
18
  if (earlyFlags["help"] === true || earlyFlags["version"] === true) {
19
- const { handleHelpVersionShortCircuit } = await import("./short-circuit-flags-LHUNLCZY.js");
19
+ const { handleHelpVersionShortCircuit } = await import("./short-circuit-flags-72E77TJI.js");
20
20
  const earlyExit = await handleHelpVersionShortCircuit(argv);
21
21
  if (earlyExit !== null) return earlyExit;
22
22
  }
23
- const { initializeCli } = await import("./cli-context-6GMXUOXD.js");
23
+ const { initializeCli } = await import("./cli-context-5IF7ZYEY.js");
24
24
  const cliCtx = await initializeCli(argv);
25
25
  if (typeof cliCtx === "number") return cliCtx;
26
- const { runInteractive } = await import("./cli-main-BU5DEHDO.js");
26
+ const { runInteractive } = await import("./cli-main-WC5JNIWY.js");
27
27
  return runInteractive(cliCtx);
28
28
  }
29
29
 
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-YMXXOOFN.js";
7
7
 
8
8
  // src/subcommands/handlers/mcp.ts
9
- import { allServers } from "@wrongstack/core/infrastructure";
9
+ import { allServers, resolveMcpServerConfig } from "@wrongstack/core/infrastructure";
10
10
  import {
11
11
  expectDefined,
12
12
  jsonObjectFileExists,
@@ -49,6 +49,15 @@ function parseToolsFlag(flags, positional) {
49
49
  );
50
50
  return set.size > 0 ? set : null;
51
51
  }
52
+ function resolveServeHttpPort(flags) {
53
+ const raw = typeof flags["port"] === "string" ? flags["port"] : typeof flags["http"] === "string" ? flags["http"] : void 0;
54
+ if (raw === void 0 || raw.trim() === "") return 0;
55
+ const port = Number(raw);
56
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
57
+ throw new Error(`invalid --port "${raw}" \u2014 expected an integer between 0 and 65535`);
58
+ }
59
+ return port;
60
+ }
52
61
  async function loadSelectedMcpServeContent(flags, cwd) {
53
62
  const resources = [];
54
63
  const prompts = [];
@@ -287,7 +296,13 @@ async function serveMcpStdio(deps, positional) {
287
296
  });
288
297
  const mode = yolo ? "yolo: all tools" : "safe: read-only tools";
289
298
  if (flags["http"] === true || typeof flags["http"] === "string" || flags["port"] || flags["host"]) {
290
- const port = Number(flags["port"] ?? flags["http"] ?? 0) || 0;
299
+ let port;
300
+ try {
301
+ port = resolveServeHttpPort(flags);
302
+ } catch (err) {
303
+ log(`wrongstack MCP server: ${err instanceof Error ? err.message : String(err)}`);
304
+ return 1;
305
+ }
291
306
  const httpHost = typeof flags["host"] === "string" ? flags["host"] : "127.0.0.1";
292
307
  const tokenFromArg = typeof flags["token"] === "string" ? flags["token"] : void 0;
293
308
  const token = tokenFromArg ?? process.env.WRONGSTACK_MCP_TOKEN?.trim() ?? void 0;
@@ -344,10 +359,13 @@ var mcpCmd = async (args, deps) => {
344
359
  deps.renderer.write("Use `wstack mcp add <name>` or set mcpServers in your config.\n");
345
360
  return 0;
346
361
  }
347
- for (const [name, cfg] of Object.entries(servers)) {
348
- const status = cfg.enabled === false ? "disabled" : "enabled";
349
- const desc = cfg.description ? ` # ${cfg.description}` : "";
350
- deps.renderer.write(` ${name.padEnd(20)} ${cfg.transport.padEnd(16)} ${status}${desc}
362
+ for (const [name, entry] of Object.entries(servers)) {
363
+ const cfg = resolveMcpServerConfig(name, entry);
364
+ const status = entry?.enabled === false ? "disabled" : "enabled";
365
+ const description = cfg?.description ?? entry?.description;
366
+ const desc = description ? ` # ${description}` : "";
367
+ const transport = cfg?.transport ?? "invalid (no transport)";
368
+ deps.renderer.write(` ${name.padEnd(20)} ${transport.padEnd(16)} ${status}${desc}
351
369
  `);
352
370
  }
353
371
  return 0;
@@ -391,7 +409,7 @@ async function addMcpServer(args, deps) {
391
409
  deps.renderer.write("\nRun `wstack mcp add <name> --enable` to enable immediately.\n");
392
410
  return 1;
393
411
  }
394
- const factory = BUILT_IN_MCP[name];
412
+ const factory = Object.hasOwn(BUILT_IN_MCP, name) ? BUILT_IN_MCP[name] : void 0;
395
413
  if (!factory) {
396
414
  deps.renderer.writeError(
397
415
  `Unknown server "${name}". Run \`wstack mcp add\` without args to see available servers.
@@ -403,7 +421,7 @@ async function addMcpServer(args, deps) {
403
421
  serverCfg.enabled = enable;
404
422
  const existing = await readJsonObjectFile(configPath);
405
423
  const mcpServers = isRecord(existing.mcpServers) ? existing.mcpServers : {};
406
- if (mcpServers[name])
424
+ if (Object.hasOwn(mcpServers, name))
407
425
  deps.renderer.writeWarning(`Server "${name}" already in config. Updating.
408
426
  `);
409
427
  await updateJsonObjectFile(configPath, (config) => {
@@ -424,7 +442,7 @@ async function removeMcpServer(name, deps) {
424
442
  }
425
443
  const existing = await readJsonObjectFile(configPath);
426
444
  const mcpServers = isRecord(existing.mcpServers) ? existing.mcpServers : {};
427
- if (!mcpServers[name]) {
445
+ if (!Object.hasOwn(mcpServers, name)) {
428
446
  deps.renderer.writeError(`Server "${name}" not in config.
429
447
  `);
430
448
  return 1;
@@ -442,4 +460,4 @@ function isRecord(value) {
442
460
  export {
443
461
  mcpCmd
444
462
  };
445
- //# sourceMappingURL=mcp-FX7TKPTV.js.map
463
+ //# sourceMappingURL=mcp-DC5WSBOO.js.map
@@ -45,6 +45,15 @@ export declare function yoloServePolicy(): AutoApprovePermissionPolicy;
45
45
  * "flag present but no list" as an error, never as "no whitelist".
46
46
  */
47
47
  export declare function parseToolsFlag(flags: Record<string, string | boolean>, positional?: readonly string[]): Set<string> | null;
48
+ /**
49
+ * Resolve the TCP port for `mcp serve --http`.
50
+ *
51
+ * `--http` is also a boolean flag, and the old `Number(flags.port ?? flags.http)`
52
+ * turned a bare `--http` into `Number(true) === 1` — the server tried to bind
53
+ * privileged port 1 instead of an ephemeral one. A bare flag now means "pick a
54
+ * free port" (0); an explicit value must be an integer in 0–65535.
55
+ */
56
+ export declare function resolveServeHttpPort(flags: Record<string, string | boolean>): number;
48
57
  interface SelectedMcpServeContent {
49
58
  resources: MCPServerResource[];
50
59
  prompts: MCPServerPrompt[];
@@ -1,9 +1,3 @@
1
- /**
2
- * Shared MCP management service.
3
- * Contains the argument parser and the actual management logic shared between
4
- * the CLI subcommand handler (packages/cli/src/subcommands/handlers/mcp.ts)
5
- * and the slash-command wiring in index.ts.
6
- */
7
1
  import type { Config, MCPServerConfig } from '@wrongstack/core/types';
8
2
  import type { MCPRegistry } from '@wrongstack/mcp';
9
3
  export interface McpParsedArgs {
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  handleHelpVersionShortCircuit
3
- } from "./chunk-5VPDKQDH.js";
3
+ } from "./chunk-TNZBQG45.js";
4
4
  import "./chunk-6WRKAACA.js";
5
5
  import "./chunk-XJXDOF63.js";
6
- import "./chunk-EK2P53GL.js";
6
+ import "./chunk-C3Z4N6A6.js";
7
7
  export {
8
8
  handleHelpVersionShortCircuit
9
9
  };
10
- //# sourceMappingURL=short-circuit-flags-LHUNLCZY.js.map
10
+ //# sourceMappingURL=short-circuit-flags-72E77TJI.js.map
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  subcommandNames,
3
3
  subcommands
4
- } from "./chunk-7K3IJGWC.js";
4
+ } from "./chunk-OJR7K3OI.js";
5
5
  export {
6
6
  subcommandNames,
7
7
  subcommands
8
8
  };
9
- //# sourceMappingURL=subcommands-UXWY3OPC.js.map
9
+ //# sourceMappingURL=subcommands-EJI7DBQS.js.map