@wrongstack/webui-server 0.301.0 → 0.302.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6463,28 +6463,28 @@ async function restartMailboxServer(projectRoot) {
6463
6463
  var RESTART_POLL_INTERVAL_MS = 250;
6464
6464
  var RESTART_DEADLINE_MS = 3e3;
6465
6465
  function isEndpointAlive(endpoint) {
6466
- return new Promise((resolve16) => {
6466
+ return new Promise((resolve17) => {
6467
6467
  const sock = net.createConnection(endpoint);
6468
6468
  const timer = setTimeout(() => {
6469
6469
  sock.destroy();
6470
- resolve16(false);
6470
+ resolve17(false);
6471
6471
  }, 500);
6472
6472
  timer.unref?.();
6473
6473
  sock.once("connect", () => {
6474
6474
  clearTimeout(timer);
6475
6475
  sock.destroy();
6476
- resolve16(true);
6476
+ resolve17(true);
6477
6477
  });
6478
6478
  sock.once("error", () => {
6479
6479
  clearTimeout(timer);
6480
6480
  sock.destroy();
6481
- resolve16(false);
6481
+ resolve17(false);
6482
6482
  });
6483
6483
  });
6484
6484
  }
6485
6485
  async function waitForShutdown(probe) {
6486
6486
  if (!probe) {
6487
- await new Promise((resolve16) => setTimeout(resolve16, RESTART_POLL_INTERVAL_MS));
6487
+ await new Promise((resolve17) => setTimeout(resolve17, RESTART_POLL_INTERVAL_MS));
6488
6488
  return;
6489
6489
  }
6490
6490
  const deadline = Date.now() + RESTART_DEADLINE_MS;
@@ -6495,7 +6495,7 @@ async function waitForShutdown(probe) {
6495
6495
  } catch {
6496
6496
  return;
6497
6497
  }
6498
- await new Promise((resolve16) => setTimeout(resolve16, RESTART_POLL_INTERVAL_MS));
6498
+ await new Promise((resolve17) => setTimeout(resolve17, RESTART_POLL_INTERVAL_MS));
6499
6499
  }
6500
6500
  }
6501
6501
  function failureService(id, label, required, mode, error2, latencyMs) {
@@ -6658,9 +6658,9 @@ async function handleGitInfo(ws, projectRoot) {
6658
6658
  const cwd = projectRoot || void 0;
6659
6659
  try {
6660
6660
  const { execFile: ef } = await import("node:child_process");
6661
- const git = (args) => new Promise((resolve16) => {
6661
+ const git = (args) => new Promise((resolve17) => {
6662
6662
  ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
6663
- resolve16(err ? "" : stdout.trim());
6663
+ resolve17(err ? "" : stdout.trim());
6664
6664
  });
6665
6665
  });
6666
6666
  const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
@@ -6686,12 +6686,12 @@ async function handleGitInfo(ws, projectRoot) {
6686
6686
  function makeGit(cwd) {
6687
6687
  return async (args) => {
6688
6688
  const { execFile: ef } = await import("node:child_process");
6689
- return new Promise((resolve16) => {
6689
+ return new Promise((resolve17) => {
6690
6690
  ef(
6691
6691
  "git",
6692
6692
  args,
6693
6693
  { cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
6694
- (err, stdout) => resolve16(err ? "" : stdout)
6694
+ (err, stdout) => resolve17(err ? "" : stdout)
6695
6695
  );
6696
6696
  });
6697
6697
  };
@@ -6856,7 +6856,7 @@ import { execFile } from "node:child_process";
6856
6856
  var GIT_TIMEOUT_MS = 1e4;
6857
6857
  var GIT_MAX_OUTPUT_BYTES = 1024 * 1024;
6858
6858
  function gitStdout(cwd, args) {
6859
- return new Promise((resolve16) => {
6859
+ return new Promise((resolve17) => {
6860
6860
  execFile(
6861
6861
  "git",
6862
6862
  [...args],
@@ -6867,7 +6867,7 @@ function gitStdout(cwd, args) {
6867
6867
  timeout: GIT_TIMEOUT_MS,
6868
6868
  maxBuffer: GIT_MAX_OUTPUT_BYTES
6869
6869
  },
6870
- (error2, stdout) => resolve16(error2 ? null : stdout)
6870
+ (error2, stdout) => resolve17(error2 ? null : stdout)
6871
6871
  );
6872
6872
  });
6873
6873
  }
@@ -7158,14 +7158,14 @@ var GoalWebSocketHandler = class {
7158
7158
  const cwd = env?.cwd ?? this.projectRoot;
7159
7159
  try {
7160
7160
  const { execFile: execFile2 } = await import("node:child_process");
7161
- const result = await new Promise((resolve16) => {
7161
+ const result = await new Promise((resolve17) => {
7162
7162
  const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
7163
7163
  execFile2(npxCommand, ["tsc", "--noEmit"], { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
7164
7164
  if (err && err.code === "ENOENT") {
7165
- resolve16("[verify] tsc not found \u2014 skipping");
7165
+ resolve17("[verify] tsc not found \u2014 skipping");
7166
7166
  return;
7167
7167
  }
7168
- resolve16(stdout + stderr);
7168
+ resolve17(stdout + stderr);
7169
7169
  });
7170
7170
  });
7171
7171
  if (result.includes("[verify]") || result.trim().length === 0) {
@@ -7903,7 +7903,7 @@ function pushEvent(event) {
7903
7903
  }
7904
7904
  }
7905
7905
  function parseBody(req) {
7906
- return new Promise((resolve16, reject) => {
7906
+ return new Promise((resolve17, reject) => {
7907
7907
  let body = "";
7908
7908
  let bodyBytes = 0;
7909
7909
  let tooLarge = false;
@@ -7923,7 +7923,7 @@ function parseBody(req) {
7923
7923
  return;
7924
7924
  }
7925
7925
  try {
7926
- resolve16(JSON.parse(body));
7926
+ resolve17(JSON.parse(body));
7927
7927
  } catch {
7928
7928
  reject(new Error("Invalid JSON"));
7929
7929
  }
@@ -8018,7 +8018,7 @@ import * as path10 from "node:path";
8018
8018
  import { runDeadCodeScan } from "@wrongstack/tools/codebase-index";
8019
8019
  var MAX_BODY_BYTES = 10 * 1024 * 1024;
8020
8020
  function readJsonBody(req) {
8021
- return new Promise((resolve16, reject) => {
8021
+ return new Promise((resolve17, reject) => {
8022
8022
  const chunks = [];
8023
8023
  let total = 0;
8024
8024
  req.on("data", (chunk) => {
@@ -8030,7 +8030,7 @@ function readJsonBody(req) {
8030
8030
  }
8031
8031
  chunks.push(chunk);
8032
8032
  });
8033
- req.on("end", () => resolve16(Buffer.concat(chunks).toString("utf8")));
8033
+ req.on("end", () => resolve17(Buffer.concat(chunks).toString("utf8")));
8034
8034
  req.on("error", (err) => reject(err));
8035
8035
  });
8036
8036
  }
@@ -8476,7 +8476,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
8476
8476
  }
8477
8477
  }
8478
8478
  function readJsonBody2(req) {
8479
- return new Promise((resolve16, reject) => {
8479
+ return new Promise((resolve17, reject) => {
8480
8480
  const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
8481
8481
  if (contentType !== "application/json") {
8482
8482
  reject(new Error(`Unsupported Content-Type: ${contentType || "(absent)"}`));
@@ -8492,7 +8492,7 @@ function readJsonBody2(req) {
8492
8492
  });
8493
8493
  req.on("end", () => {
8494
8494
  try {
8495
- resolve16(data ? JSON.parse(data) : {});
8495
+ resolve17(data ? JSON.parse(data) : {});
8496
8496
  } catch (err) {
8497
8497
  reject(err instanceof Error ? err : new Error(String(err)));
8498
8498
  }
@@ -8768,14 +8768,14 @@ async function readJsonBody3(res, req) {
8768
8768
  });
8769
8769
  return null;
8770
8770
  }
8771
- return new Promise((resolve16) => {
8771
+ return new Promise((resolve17) => {
8772
8772
  let data = "";
8773
8773
  let failed = false;
8774
8774
  const fail2 = (message) => {
8775
8775
  if (failed) return;
8776
8776
  failed = true;
8777
8777
  sendJson2(res, 400, { error: { code: "INVALID_BODY", message } });
8778
- resolve16(null);
8778
+ resolve17(null);
8779
8779
  };
8780
8780
  req.on("data", (chunk) => {
8781
8781
  if (failed) return;
@@ -8788,7 +8788,7 @@ async function readJsonBody3(res, req) {
8788
8788
  req.on("end", () => {
8789
8789
  if (failed) return;
8790
8790
  try {
8791
- resolve16(data.trim().length === 0 ? {} : JSON.parse(data));
8791
+ resolve17(data.trim().length === 0 ? {} : JSON.parse(data));
8792
8792
  } catch {
8793
8793
  fail2("Request body is not valid JSON");
8794
8794
  }
@@ -9627,7 +9627,7 @@ function strictDecodeParam(segment, res) {
9627
9627
  function createHttpServer(opts) {
9628
9628
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
9629
9629
  const distDir = path13.resolve(opts.distDir);
9630
- const requireAccessToken = true;
9630
+ const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
9631
9631
  const secureCookies = opts.secureCookies ?? (opts.publicWsUrl?.trim().toLowerCase().startsWith("wss:") ?? false);
9632
9632
  const trustedHostnames = (() => {
9633
9633
  const names = [...opts.allowedHostnames ?? []];
@@ -10173,10 +10173,78 @@ function createHttpServer(opts) {
10173
10173
  }
10174
10174
 
10175
10175
  // src/server/instance-registry.ts
10176
+ import * as fs11 from "node:fs/promises";
10176
10177
  import * as os from "node:os";
10177
10178
  import * as path14 from "node:path";
10178
- import * as fs11 from "node:fs/promises";
10179
10179
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
10180
+ function normalizeRoot(root) {
10181
+ const resolved = path14.resolve(root);
10182
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
10183
+ }
10184
+ function isLiveSessionStatus(status) {
10185
+ return status === "active" || status === "idle";
10186
+ }
10187
+ function instanceRole(instance) {
10188
+ return instance.role ?? "standalone";
10189
+ }
10190
+ function resolveAttachability(input) {
10191
+ const { session, instance } = input;
10192
+ if (!isLiveSessionStatus(session.status)) {
10193
+ return { attachable: false, degradedReason: "session-not-live" };
10194
+ }
10195
+ if (!instance) {
10196
+ return { attachable: false, degradedReason: "live-session-no-webui-endpoint" };
10197
+ }
10198
+ if (instance.pid !== session.pid) {
10199
+ return { attachable: false, degradedReason: "endpoint-owner-mismatch" };
10200
+ }
10201
+ if (!instance.sessionId) {
10202
+ return { attachable: false, instance, degradedReason: "endpoint-missing-session-id" };
10203
+ }
10204
+ if (instance.sessionId !== session.sessionId) {
10205
+ return { attachable: false, instance, degradedReason: "endpoint-session-mismatch" };
10206
+ }
10207
+ if (instanceRole(instance) !== "session-child") {
10208
+ return { attachable: false, instance, degradedReason: "endpoint-not-session-child" };
10209
+ }
10210
+ if (instance.attachable === false) {
10211
+ return { attachable: false, instance, degradedReason: "endpoint-not-attachable" };
10212
+ }
10213
+ return {
10214
+ attachable: true,
10215
+ endpoint: {
10216
+ host: instance.host,
10217
+ httpPort: instance.httpPort,
10218
+ url: instance.url,
10219
+ ...instance.authToken ? { authToken: instance.authToken } : {}
10220
+ }
10221
+ };
10222
+ }
10223
+ function joinSessionRegistryWithWebUIInstances(input) {
10224
+ const targetRoot = input.projectRoot ? normalizeRoot(input.projectRoot) : void 0;
10225
+ const sessions = input.sessions.filter((session) => {
10226
+ if (input.projectSlug && session.projectSlug !== input.projectSlug) return false;
10227
+ if (targetRoot && normalizeRoot(session.projectRoot) !== targetRoot) return false;
10228
+ return true;
10229
+ });
10230
+ return sessions.map((session) => {
10231
+ const instance = input.instances.find((candidate) => candidate.sessionId === session.sessionId) ?? input.instances.find(
10232
+ (candidate) => candidate.pid === session.pid && normalizeRoot(candidate.projectRoot) === normalizeRoot(session.projectRoot)
10233
+ );
10234
+ const resolved = resolveAttachability({ session, instance });
10235
+ return {
10236
+ sessionId: session.sessionId,
10237
+ projectRoot: session.projectRoot,
10238
+ workingDir: session.workingDir,
10239
+ sessionPid: session.pid,
10240
+ status: session.status,
10241
+ ...instance ? { instance } : {},
10242
+ ...resolved.endpoint ? { endpoint: resolved.endpoint } : {},
10243
+ attachable: resolved.attachable,
10244
+ ...resolved.degradedReason ? { degradedReason: resolved.degradedReason } : {}
10245
+ };
10246
+ });
10247
+ }
10180
10248
  function defaultBaseDir() {
10181
10249
  return path14.join(os.homedir(), ".wrongstack");
10182
10250
  }
@@ -13723,16 +13791,16 @@ function getSurfaceDefaultPorts(surface) {
13723
13791
  return { http: SURFACE_DEFAULT_PORTS[surface].http };
13724
13792
  }
13725
13793
  function isPortFree(host, port) {
13726
- return new Promise((resolve16) => {
13794
+ return new Promise((resolve17) => {
13727
13795
  const srv = net2.createServer();
13728
- srv.once("error", () => resolve16(false));
13796
+ srv.once("error", () => resolve17(false));
13729
13797
  srv.once("listening", () => {
13730
- srv.close(() => resolve16(true));
13798
+ srv.close(() => resolve17(true));
13731
13799
  });
13732
13800
  try {
13733
13801
  srv.listen(port, host);
13734
13802
  } catch {
13735
- resolve16(false);
13803
+ resolve17(false);
13736
13804
  }
13737
13805
  });
13738
13806
  }
@@ -13888,7 +13956,7 @@ async function startStaticServe(opts, deps2 = {}) {
13888
13956
  return { server, port: opts.httpPort };
13889
13957
  }
13890
13958
  function runPnpmBuild(cwd, workspace, timeoutMs) {
13891
- return new Promise((resolve16, reject) => {
13959
+ return new Promise((resolve17, reject) => {
13892
13960
  const child = spawn2("pnpm", ["--filter", workspace, "build"], {
13893
13961
  cwd,
13894
13962
  shell: process.platform === "win32",
@@ -13906,7 +13974,7 @@ function runPnpmBuild(cwd, workspace, timeoutMs) {
13906
13974
  });
13907
13975
  child.once("close", (code) => {
13908
13976
  clearTimeout(timer);
13909
- if (code === 0) resolve16();
13977
+ if (code === 0) resolve17();
13910
13978
  else reject(new Error(`pnpm build exited with code ${String(code)}`));
13911
13979
  });
13912
13980
  });
@@ -13970,27 +14038,41 @@ function formatExternalAccessUrls(opts) {
13970
14038
  }
13971
14039
 
13972
14040
  // src/server/embedded-lifecycle.ts
13973
- function registerWebuiInstance(p, deps2 = {}) {
14041
+ async function registerWebuiInstance(p, deps2 = {}) {
13974
14042
  const register = deps2.registerFn ?? registerInstance;
13975
- void register(
13976
- {
13977
- pid: p.pid,
13978
- surface: p.surface,
13979
- httpPort: p.httpPort,
13980
- host: p.host,
13981
- projectRoot: p.projectRoot,
13982
- projectName: path16.basename(p.projectRoot) || p.projectRoot,
13983
- startedAt: p.startedAt,
13984
- url: buildWebUIAccessUrl({
14043
+ try {
14044
+ await register(
14045
+ {
14046
+ pid: p.pid,
14047
+ surface: p.surface,
14048
+ httpPort: p.httpPort,
13985
14049
  host: p.host,
13986
- port: p.httpPort,
13987
- publicUrl: p.publicUrl
13988
- }),
13989
- ...p.authToken ? { authToken: p.authToken } : {}
13990
- },
13991
- p.registryBaseDir
13992
- ).catch(() => {
13993
- });
14050
+ projectRoot: p.projectRoot,
14051
+ projectName: path16.basename(p.projectRoot) || p.projectRoot,
14052
+ startedAt: p.startedAt,
14053
+ url: buildWebUIAccessUrl({
14054
+ host: p.host,
14055
+ port: p.httpPort,
14056
+ publicUrl: p.publicUrl
14057
+ }),
14058
+ ...p.authToken ? { authToken: p.authToken } : {},
14059
+ ...p.role ? { role: p.role } : {},
14060
+ ...p.sessionId ? { sessionId: p.sessionId } : {},
14061
+ ...p.parentPid !== void 0 ? { parentPid: p.parentPid } : {},
14062
+ ...p.parentShellId ? { parentShellId: p.parentShellId } : {},
14063
+ ...p.runtimeId ? { runtimeId: p.runtimeId } : {},
14064
+ ...p.attachable !== void 0 ? { attachable: p.attachable } : {},
14065
+ ...p.authToken ? { auth: { scheme: "registry-token", tokenPresent: true } } : {},
14066
+ ...p.lastReadyAt ? { lastReadyAt: p.lastReadyAt } : {},
14067
+ ...p.protocolVersion !== void 0 ? { protocolVersion: p.protocolVersion } : {},
14068
+ ...p.capabilities ? { capabilities: p.capabilities } : {}
14069
+ },
14070
+ p.registryBaseDir
14071
+ );
14072
+ return true;
14073
+ } catch {
14074
+ return false;
14075
+ }
13994
14076
  }
13995
14077
  function announceWebuiReady(p) {
13996
14078
  const log = p.log ?? ((m) => console.log(m));
@@ -14028,10 +14110,10 @@ async function runBounded(work, timeoutMs, label, debug) {
14028
14110
  Promise.resolve().then(() => work()).catch((err) => {
14029
14111
  debug(`[webui-server] ${label} failed: ${err}`);
14030
14112
  }),
14031
- new Promise((resolve16) => {
14113
+ new Promise((resolve17) => {
14032
14114
  timer = setTimeout(() => {
14033
14115
  debug(`[webui-server] ${label} timed out after ${timeoutMs}ms`);
14034
- resolve16();
14116
+ resolve17();
14035
14117
  }, timeoutMs);
14036
14118
  timer.unref?.();
14037
14119
  })
@@ -14064,8 +14146,8 @@ function createWebuiShutdown(res) {
14064
14146
  const unregistered = unregister(res.pid, res.registryBaseDir).catch(
14065
14147
  (err) => debug(`[webui-server] unregister failed: ${err}`)
14066
14148
  );
14067
- await new Promise((resolve16) => {
14068
- res.wss.close(() => resolve16());
14149
+ await new Promise((resolve17) => {
14150
+ res.wss.close(() => resolve17());
14069
14151
  });
14070
14152
  await unregistered;
14071
14153
  log("[WebUI] Server stopped");
@@ -20458,8 +20540,8 @@ function createConfigWriteLock() {
20458
20540
  acquire() {
20459
20541
  const prev = lock;
20460
20542
  let release = () => void 0;
20461
- lock = new Promise((resolve16) => {
20462
- release = resolve16;
20543
+ lock = new Promise((resolve17) => {
20544
+ release = resolve17;
20463
20545
  });
20464
20546
  return { prev, release };
20465
20547
  }
@@ -23045,7 +23127,7 @@ function findWorkspaceCliEntry(projectRoot) {
23045
23127
  return null;
23046
23128
  }
23047
23129
  function sleep(ms) {
23048
- return new Promise((resolve16) => setTimeout(resolve16, ms));
23130
+ return new Promise((resolve17) => setTimeout(resolve17, ms));
23049
23131
  }
23050
23132
 
23051
23133
  // src/server/terminal-ws-handler.ts
@@ -23288,7 +23370,7 @@ function clampDim(value, fallback) {
23288
23370
  }
23289
23371
 
23290
23372
  // src/server/worktree-ws-handler.ts
23291
- import { join as join14, resolve as resolve13, sep as sep5 } from "node:path";
23373
+ import { join as join14, resolve as resolve14, sep as sep5 } from "node:path";
23292
23374
  import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
23293
23375
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
23294
23376
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
@@ -23373,11 +23455,11 @@ var WorktreeWebSocketHandler = class {
23373
23455
  // ── orphan management ─────────────────────────────────────────────────────
23374
23456
  /** Absolute managed-worktrees root for this project. */
23375
23457
  worktreesRoot() {
23376
- return resolve13(join14(this.management.projectRoot, ".wrongstack", "worktrees"));
23458
+ return resolve14(join14(this.management.projectRoot, ".wrongstack", "worktrees"));
23377
23459
  }
23378
23460
  /** True iff `dir` resolves strictly inside the managed worktrees root. */
23379
23461
  underRoot(dir) {
23380
- const abs = resolve13(dir);
23462
+ const abs = resolve14(dir);
23381
23463
  const root = this.worktreesRoot();
23382
23464
  return abs !== root && abs.startsWith(root + sep5);
23383
23465
  }
@@ -23601,7 +23683,7 @@ var WorktreeWebSocketHandler = class {
23601
23683
  }
23602
23684
  const base = baseBranch && MANAGED_BRANCH_RE.test(baseBranch) ? baseBranch : void 0;
23603
23685
  const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
23604
- const summary = await wt.diffSummary(resolve13(dir), base);
23686
+ const summary = await wt.diffSummary(resolve14(dir), base);
23605
23687
  this.broadcast({ type: "worktree.diff_result", payload: { dir, summary } });
23606
23688
  }
23607
23689
  // ── internals ───────────────────────────────────────────────────────────
@@ -26231,7 +26313,7 @@ async function startWebUI(opts = {}) {
26231
26313
  if (events.listenerCount("tool.confirm_needed") === 0) {
26232
26314
  throw new Error("No permission confirmation surface is connected");
26233
26315
  }
26234
- const decision = await new Promise((resolve16) => {
26316
+ const decision = await new Promise((resolve17) => {
26235
26317
  events.emit("tool.confirm_needed", {
26236
26318
  sessionId: context.session.id,
26237
26319
  tool: confirmTool,
@@ -26241,7 +26323,7 @@ async function startWebUI(opts = {}) {
26241
26323
  decisionSource: pending.decisionSource,
26242
26324
  riskTier: pending.riskTier,
26243
26325
  boundaryReason: pending.boundaryReason,
26244
- resolve: resolve16
26326
+ resolve: resolve17
26245
26327
  });
26246
26328
  });
26247
26329
  const rule = { tool: "language_package", pattern: pending.suggestedPattern };
@@ -26883,6 +26965,7 @@ export {
26883
26965
  isPortFree,
26884
26966
  isRegisteredMessageType,
26885
26967
  isWildcardBind,
26968
+ joinSessionRegistryWithWebUIInstances,
26886
26969
  listInstances,
26887
26970
  loadManifest,
26888
26971
  loadSavedProviders,
@@ -1,4 +1,4 @@
1
- import { type WebUIInstanceRecord } from './instance-registry.js';
1
+ import { type WebUIInstanceRecord, type WebUIInstanceRole } from './instance-registry.js';
2
2
  import type { SurfaceKind } from './port-utils.js';
3
3
  /**
4
4
  * PR 7 of Issue #30 (webui-server 8-PR refactor): process lifecycle.
@@ -38,6 +38,21 @@ export interface RegisterWebuiInstanceParams {
38
38
  * authenticate `POST /api/fleet/ping` (H3). See `WebUIInstanceRecord`.
39
39
  */
40
40
  authToken?: string | undefined;
41
+ /** Runtime role. Missing means the legacy standalone WebUI/SimpleUI role. */
42
+ role?: WebUIInstanceRole | undefined;
43
+ /** Live session owned by this endpoint when `role === 'session-child'`. */
44
+ sessionId?: string | undefined;
45
+ /** Parent shell process identity, when this endpoint was spawned by a parent. */
46
+ parentPid?: number | undefined;
47
+ parentShellId?: string | undefined;
48
+ /** Stable child runtime id, distinct from the session id. */
49
+ runtimeId?: string | undefined;
50
+ /** Whether a parent shell should treat this endpoint as attachable. */
51
+ attachable?: boolean | undefined;
52
+ /** Health/protocol hints for future parent shells. */
53
+ lastReadyAt?: string | undefined;
54
+ protocolVersion?: number | undefined;
55
+ capabilities?: string[] | undefined;
41
56
  }
42
57
  export interface RegisterWebuiInstanceDeps {
43
58
  registerFn?: (record: WebUIInstanceRecord, baseDir?: string) => Promise<void>;
@@ -48,7 +63,7 @@ export interface RegisterWebuiInstanceDeps {
48
63
  * is swallowed (the registry is a convenience index, not a source of
49
64
  * truth). Caller guards on `projectRoot` being known.
50
65
  */
51
- export declare function registerWebuiInstance(p: RegisterWebuiInstanceParams, deps?: RegisterWebuiInstanceDeps): void;
66
+ export declare function registerWebuiInstance(p: RegisterWebuiInstanceParams, deps?: RegisterWebuiInstanceDeps): Promise<boolean>;
52
67
  export interface AnnounceWebuiReadyParams {
53
68
  /** Surface kind — 'webui' or 'simpleui'. */
54
69
  surface: SurfaceKind;
@@ -6377,28 +6377,28 @@ async function restartMailboxServer(projectRoot) {
6377
6377
  var RESTART_POLL_INTERVAL_MS = 250;
6378
6378
  var RESTART_DEADLINE_MS = 3e3;
6379
6379
  function isEndpointAlive(endpoint) {
6380
- return new Promise((resolve15) => {
6380
+ return new Promise((resolve16) => {
6381
6381
  const sock = net.createConnection(endpoint);
6382
6382
  const timer = setTimeout(() => {
6383
6383
  sock.destroy();
6384
- resolve15(false);
6384
+ resolve16(false);
6385
6385
  }, 500);
6386
6386
  timer.unref?.();
6387
6387
  sock.once("connect", () => {
6388
6388
  clearTimeout(timer);
6389
6389
  sock.destroy();
6390
- resolve15(true);
6390
+ resolve16(true);
6391
6391
  });
6392
6392
  sock.once("error", () => {
6393
6393
  clearTimeout(timer);
6394
6394
  sock.destroy();
6395
- resolve15(false);
6395
+ resolve16(false);
6396
6396
  });
6397
6397
  });
6398
6398
  }
6399
6399
  async function waitForShutdown(probe) {
6400
6400
  if (!probe) {
6401
- await new Promise((resolve15) => setTimeout(resolve15, RESTART_POLL_INTERVAL_MS));
6401
+ await new Promise((resolve16) => setTimeout(resolve16, RESTART_POLL_INTERVAL_MS));
6402
6402
  return;
6403
6403
  }
6404
6404
  const deadline = Date.now() + RESTART_DEADLINE_MS;
@@ -6409,7 +6409,7 @@ async function waitForShutdown(probe) {
6409
6409
  } catch {
6410
6410
  return;
6411
6411
  }
6412
- await new Promise((resolve15) => setTimeout(resolve15, RESTART_POLL_INTERVAL_MS));
6412
+ await new Promise((resolve16) => setTimeout(resolve16, RESTART_POLL_INTERVAL_MS));
6413
6413
  }
6414
6414
  }
6415
6415
  function failureService(id, label, required, mode, error2, latencyMs) {
@@ -6572,9 +6572,9 @@ async function handleGitInfo(ws, projectRoot) {
6572
6572
  const cwd = projectRoot || void 0;
6573
6573
  try {
6574
6574
  const { execFile: ef } = await import("node:child_process");
6575
- const git = (args) => new Promise((resolve15) => {
6575
+ const git = (args) => new Promise((resolve16) => {
6576
6576
  ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
6577
- resolve15(err ? "" : stdout.trim());
6577
+ resolve16(err ? "" : stdout.trim());
6578
6578
  });
6579
6579
  });
6580
6580
  const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
@@ -6600,12 +6600,12 @@ async function handleGitInfo(ws, projectRoot) {
6600
6600
  function makeGit(cwd) {
6601
6601
  return async (args) => {
6602
6602
  const { execFile: ef } = await import("node:child_process");
6603
- return new Promise((resolve15) => {
6603
+ return new Promise((resolve16) => {
6604
6604
  ef(
6605
6605
  "git",
6606
6606
  args,
6607
6607
  { cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
6608
- (err, stdout) => resolve15(err ? "" : stdout)
6608
+ (err, stdout) => resolve16(err ? "" : stdout)
6609
6609
  );
6610
6610
  });
6611
6611
  };
@@ -6770,7 +6770,7 @@ import { execFile } from "node:child_process";
6770
6770
  var GIT_TIMEOUT_MS = 1e4;
6771
6771
  var GIT_MAX_OUTPUT_BYTES = 1024 * 1024;
6772
6772
  function gitStdout(cwd, args) {
6773
- return new Promise((resolve15) => {
6773
+ return new Promise((resolve16) => {
6774
6774
  execFile(
6775
6775
  "git",
6776
6776
  [...args],
@@ -6781,7 +6781,7 @@ function gitStdout(cwd, args) {
6781
6781
  timeout: GIT_TIMEOUT_MS,
6782
6782
  maxBuffer: GIT_MAX_OUTPUT_BYTES
6783
6783
  },
6784
- (error2, stdout) => resolve15(error2 ? null : stdout)
6784
+ (error2, stdout) => resolve16(error2 ? null : stdout)
6785
6785
  );
6786
6786
  });
6787
6787
  }
@@ -7072,14 +7072,14 @@ var GoalWebSocketHandler = class {
7072
7072
  const cwd = env?.cwd ?? this.projectRoot;
7073
7073
  try {
7074
7074
  const { execFile: execFile2 } = await import("node:child_process");
7075
- const result = await new Promise((resolve15) => {
7075
+ const result = await new Promise((resolve16) => {
7076
7076
  const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
7077
7077
  execFile2(npxCommand, ["tsc", "--noEmit"], { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
7078
7078
  if (err && err.code === "ENOENT") {
7079
- resolve15("[verify] tsc not found \u2014 skipping");
7079
+ resolve16("[verify] tsc not found \u2014 skipping");
7080
7080
  return;
7081
7081
  }
7082
- resolve15(stdout + stderr);
7082
+ resolve16(stdout + stderr);
7083
7083
  });
7084
7084
  });
7085
7085
  if (result.includes("[verify]") || result.trim().length === 0) {
@@ -7817,7 +7817,7 @@ function pushEvent(event) {
7817
7817
  }
7818
7818
  }
7819
7819
  function parseBody(req) {
7820
- return new Promise((resolve15, reject) => {
7820
+ return new Promise((resolve16, reject) => {
7821
7821
  let body = "";
7822
7822
  let bodyBytes = 0;
7823
7823
  let tooLarge = false;
@@ -7837,7 +7837,7 @@ function parseBody(req) {
7837
7837
  return;
7838
7838
  }
7839
7839
  try {
7840
- resolve15(JSON.parse(body));
7840
+ resolve16(JSON.parse(body));
7841
7841
  } catch {
7842
7842
  reject(new Error("Invalid JSON"));
7843
7843
  }
@@ -7924,7 +7924,7 @@ import * as path10 from "node:path";
7924
7924
  import { runDeadCodeScan } from "@wrongstack/tools/codebase-index";
7925
7925
  var MAX_BODY_BYTES = 10 * 1024 * 1024;
7926
7926
  function readJsonBody(req) {
7927
- return new Promise((resolve15, reject) => {
7927
+ return new Promise((resolve16, reject) => {
7928
7928
  const chunks = [];
7929
7929
  let total = 0;
7930
7930
  req.on("data", (chunk) => {
@@ -7936,7 +7936,7 @@ function readJsonBody(req) {
7936
7936
  }
7937
7937
  chunks.push(chunk);
7938
7938
  });
7939
- req.on("end", () => resolve15(Buffer.concat(chunks).toString("utf8")));
7939
+ req.on("end", () => resolve16(Buffer.concat(chunks).toString("utf8")));
7940
7940
  req.on("error", (err) => reject(err));
7941
7941
  });
7942
7942
  }
@@ -8382,7 +8382,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
8382
8382
  }
8383
8383
  }
8384
8384
  function readJsonBody2(req) {
8385
- return new Promise((resolve15, reject) => {
8385
+ return new Promise((resolve16, reject) => {
8386
8386
  const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
8387
8387
  if (contentType !== "application/json") {
8388
8388
  reject(new Error(`Unsupported Content-Type: ${contentType || "(absent)"}`));
@@ -8398,7 +8398,7 @@ function readJsonBody2(req) {
8398
8398
  });
8399
8399
  req.on("end", () => {
8400
8400
  try {
8401
- resolve15(data ? JSON.parse(data) : {});
8401
+ resolve16(data ? JSON.parse(data) : {});
8402
8402
  } catch (err) {
8403
8403
  reject(err instanceof Error ? err : new Error(String(err)));
8404
8404
  }
@@ -8674,14 +8674,14 @@ async function readJsonBody3(res, req) {
8674
8674
  });
8675
8675
  return null;
8676
8676
  }
8677
- return new Promise((resolve15) => {
8677
+ return new Promise((resolve16) => {
8678
8678
  let data = "";
8679
8679
  let failed = false;
8680
8680
  const fail2 = (message) => {
8681
8681
  if (failed) return;
8682
8682
  failed = true;
8683
8683
  sendJson2(res, 400, { error: { code: "INVALID_BODY", message } });
8684
- resolve15(null);
8684
+ resolve16(null);
8685
8685
  };
8686
8686
  req.on("data", (chunk) => {
8687
8687
  if (failed) return;
@@ -8694,7 +8694,7 @@ async function readJsonBody3(res, req) {
8694
8694
  req.on("end", () => {
8695
8695
  if (failed) return;
8696
8696
  try {
8697
- resolve15(data.trim().length === 0 ? {} : JSON.parse(data));
8697
+ resolve16(data.trim().length === 0 ? {} : JSON.parse(data));
8698
8698
  } catch {
8699
8699
  fail2("Request body is not valid JSON");
8700
8700
  }
@@ -9533,7 +9533,7 @@ function strictDecodeParam(segment, res) {
9533
9533
  function createHttpServer(opts) {
9534
9534
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
9535
9535
  const distDir = path13.resolve(opts.distDir);
9536
- const requireAccessToken = true;
9536
+ const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
9537
9537
  const secureCookies = opts.secureCookies ?? (opts.publicWsUrl?.trim().toLowerCase().startsWith("wss:") ?? false);
9538
9538
  const trustedHostnames = (() => {
9539
9539
  const names = [...opts.allowedHostnames ?? []];
@@ -10079,9 +10079,9 @@ function createHttpServer(opts) {
10079
10079
  }
10080
10080
 
10081
10081
  // src/server/instance-registry.ts
10082
+ import * as fs11 from "node:fs/promises";
10082
10083
  import * as os from "node:os";
10083
10084
  import * as path14 from "node:path";
10084
- import * as fs11 from "node:fs/promises";
10085
10085
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
10086
10086
  function defaultBaseDir() {
10087
10087
  return path14.join(os.homedir(), ".wrongstack");
@@ -13503,16 +13503,16 @@ function createModelOperations(context) {
13503
13503
  import * as net2 from "node:net";
13504
13504
  import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
13505
13505
  function isPortFree(host, port) {
13506
- return new Promise((resolve15) => {
13506
+ return new Promise((resolve16) => {
13507
13507
  const srv = net2.createServer();
13508
- srv.once("error", () => resolve15(false));
13508
+ srv.once("error", () => resolve16(false));
13509
13509
  srv.once("listening", () => {
13510
- srv.close(() => resolve15(true));
13510
+ srv.close(() => resolve16(true));
13511
13511
  });
13512
13512
  try {
13513
13513
  srv.listen(port, host);
13514
13514
  } catch {
13515
- resolve15(false);
13515
+ resolve16(false);
13516
13516
  }
13517
13517
  });
13518
13518
  }
@@ -20724,7 +20724,7 @@ function findWorkspaceCliEntry(projectRoot) {
20724
20724
  return null;
20725
20725
  }
20726
20726
  function sleep(ms) {
20727
- return new Promise((resolve15) => setTimeout(resolve15, ms));
20727
+ return new Promise((resolve16) => setTimeout(resolve16, ms));
20728
20728
  }
20729
20729
 
20730
20730
  // src/server/terminal-ws-handler.ts
@@ -20967,7 +20967,7 @@ function clampDim(value, fallback) {
20967
20967
  }
20968
20968
 
20969
20969
  // src/server/worktree-ws-handler.ts
20970
- import { join as join11, resolve as resolve12, sep as sep5 } from "node:path";
20970
+ import { join as join11, resolve as resolve13, sep as sep5 } from "node:path";
20971
20971
  import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
20972
20972
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
20973
20973
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
@@ -21052,11 +21052,11 @@ var WorktreeWebSocketHandler = class {
21052
21052
  // ── orphan management ─────────────────────────────────────────────────────
21053
21053
  /** Absolute managed-worktrees root for this project. */
21054
21054
  worktreesRoot() {
21055
- return resolve12(join11(this.management.projectRoot, ".wrongstack", "worktrees"));
21055
+ return resolve13(join11(this.management.projectRoot, ".wrongstack", "worktrees"));
21056
21056
  }
21057
21057
  /** True iff `dir` resolves strictly inside the managed worktrees root. */
21058
21058
  underRoot(dir) {
21059
- const abs = resolve12(dir);
21059
+ const abs = resolve13(dir);
21060
21060
  const root = this.worktreesRoot();
21061
21061
  return abs !== root && abs.startsWith(root + sep5);
21062
21062
  }
@@ -21280,7 +21280,7 @@ var WorktreeWebSocketHandler = class {
21280
21280
  }
21281
21281
  const base = baseBranch && MANAGED_BRANCH_RE.test(baseBranch) ? baseBranch : void 0;
21282
21282
  const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
21283
- const summary = await wt.diffSummary(resolve12(dir), base);
21283
+ const summary = await wt.diffSummary(resolve13(dir), base);
21284
21284
  this.broadcast({ type: "worktree.diff_result", payload: { dir, summary } });
21285
21285
  }
21286
21286
  // ── internals ───────────────────────────────────────────────────────────
@@ -23910,7 +23910,7 @@ async function startWebUI(opts = {}) {
23910
23910
  if (events.listenerCount("tool.confirm_needed") === 0) {
23911
23911
  throw new Error("No permission confirmation surface is connected");
23912
23912
  }
23913
- const decision = await new Promise((resolve15) => {
23913
+ const decision = await new Promise((resolve16) => {
23914
23914
  events.emit("tool.confirm_needed", {
23915
23915
  sessionId: context.session.id,
23916
23916
  tool: confirmTool,
@@ -23920,7 +23920,7 @@ async function startWebUI(opts = {}) {
23920
23920
  decisionSource: pending.decisionSource,
23921
23921
  riskTier: pending.riskTier,
23922
23922
  boundaryReason: pending.boundaryReason,
23923
- resolve: resolve15
23923
+ resolve: resolve16
23924
23924
  });
23925
23925
  });
23926
23926
  const rule = { tool: "language_package", pattern: pending.suggestedPattern };
@@ -42,7 +42,7 @@ export { type HostRouteHandlers, handleHostRoute } from './host-routes.js';
42
42
  export { clearAnalyticsBuffer, getAnalyticsBuffer, handleApiAnalyticsGet, handleApiAnalyticsPost, handleApiAnalyticsSummary, } from './http-server/analytics-handler.js';
43
43
  export type { CreateHttpServerOptions } from './http-server.js';
44
44
  export { buildCspHeader, createHttpServer, decodeSessionId, injectWsConfig, isInsideDist, } from './http-server.js';
45
- export { defaultBaseDir, formatInstances, isPidAlive, listInstances, registerInstance, registryPath, unregisterInstance, type WebUIInstanceRecord, } from './instance-registry.js';
45
+ export { defaultBaseDir, formatInstances, isPidAlive, joinSessionRegistryWithWebUIInstances, listInstances, registerInstance, registryPath, unregisterInstance, type WebUIInstanceAuthInfo, type WebUIInstanceRecord, type WebUIInstanceRole, type WebUISessionAttachCandidate, type WebUISessionAttachDegradedReason, type WebUISessionAttachEndpoint, } from './instance-registry.js';
46
46
  export { handleIntrospectionRoute, type IntrospectionRouteContext, } from './introspection-routes.js';
47
47
  export { handleKanbanTaskDispatch, type KanbanDispatchContext, type KanbanDispatchResult, type KanbanTaskDispatcher, parseResolvedDispatchRoute, type ResolvedDispatchRoute, } from './kanban-dispatch.js';
48
48
  export { handleKanbanHostRoute, type KanbanHostRouteHandlers } from './kanban-host-routes.js';
@@ -18,12 +18,20 @@
18
18
  * - **Best-effort**: a failure to read/write the registry must NEVER take the
19
19
  * server down. Callers wrap these in `.catch()`.
20
20
  */
21
+ import type { SessionRegistryEntry, SessionLiveStatus } from '@wrongstack/core/storage';
22
+ export type WebUIInstanceRole = 'standalone' | 'parent-shell' | 'session-child';
23
+ export interface WebUIInstanceAuthInfo {
24
+ /** How a same-user parent/sibling process can authenticate to this endpoint. */
25
+ scheme: 'registry-token' | 'cookie-bootstrap' | 'none';
26
+ /** Whether the record has a usable token in `authToken`. */
27
+ tokenPresent: boolean;
28
+ }
21
29
  /** One running WebUI / SimpleUI process. */
22
30
  export interface WebUIInstanceRecord {
23
31
  /** OS process id — also the liveness key. */
24
32
  pid: number;
25
- /** Surface kind — 'webui' or 'simpleui'. */
26
- surface: 'webui' | 'simpleui';
33
+ /** Surface kind — 'webui' or 'simpleui'. Additional strings are tolerated for new surfaces. */
34
+ surface: 'webui' | 'simpleui' | string;
27
35
  /** Port serving both HTTP and WebSocket. */
28
36
  httpPort: number;
29
37
  /** Bind host (e.g. 127.0.0.1 or 0.0.0.0). */
@@ -57,7 +65,48 @@ export interface WebUIInstanceRecord {
57
65
  * Optional so an older record (or a surface that has no token) still parses.
58
66
  */
59
67
  authToken?: string | undefined;
68
+ /** Runtime role. Missing means the legacy standalone WebUI/SimpleUI role. */
69
+ role?: WebUIInstanceRole | undefined;
70
+ /** Live session owned by this endpoint when `role === 'session-child'`. */
71
+ sessionId?: string | undefined;
72
+ /** Parent shell process identity, when this endpoint was spawned by a parent. */
73
+ parentPid?: number | undefined;
74
+ parentShellId?: string | undefined;
75
+ /** Stable child runtime id, distinct from the session id. */
76
+ runtimeId?: string | undefined;
77
+ /** Whether a parent shell should treat this endpoint as attachable. */
78
+ attachable?: boolean | undefined;
79
+ /** Descriptive auth metadata; the same-user token remains in `authToken`. */
80
+ auth?: WebUIInstanceAuthInfo | undefined;
81
+ /** Health/protocol hints for future parent shells. */
82
+ lastReadyAt?: string | undefined;
83
+ protocolVersion?: number | undefined;
84
+ capabilities?: string[] | undefined;
85
+ }
86
+ export interface WebUISessionAttachEndpoint {
87
+ host: string;
88
+ httpPort: number;
89
+ url: string;
90
+ authToken?: string | undefined;
91
+ }
92
+ export type WebUISessionAttachDegradedReason = 'live-session-no-webui-endpoint' | 'endpoint-owner-mismatch' | 'endpoint-missing-session-id' | 'endpoint-session-mismatch' | 'endpoint-not-session-child' | 'endpoint-not-attachable' | 'session-not-live';
93
+ export interface WebUISessionAttachCandidate {
94
+ sessionId: string;
95
+ projectRoot: string;
96
+ workingDir: string;
97
+ sessionPid: number;
98
+ status: SessionLiveStatus;
99
+ instance?: WebUIInstanceRecord | undefined;
100
+ endpoint?: WebUISessionAttachEndpoint | undefined;
101
+ attachable: boolean;
102
+ degradedReason?: WebUISessionAttachDegradedReason | undefined;
60
103
  }
104
+ export declare function joinSessionRegistryWithWebUIInstances(input: {
105
+ sessions: SessionRegistryEntry[];
106
+ instances: WebUIInstanceRecord[];
107
+ projectRoot?: string | undefined;
108
+ projectSlug?: string | undefined;
109
+ }): WebUISessionAttachCandidate[];
61
110
  /** Default wstack home dir (`~/.wrongstack`). Callers may override the base. */
62
111
  export declare function defaultBaseDir(): string;
63
112
  /** Resolve the registry file path for a given base dir. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/webui-server",
3
- "version": "0.301.0",
3
+ "version": "0.302.0",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack WebUI HTTP/WebSocket server module — extracted from @wrongstack/webui in PR #243/244 to remove the CLI -> @wrongstack/webui/server cross-package edge (audit §3.1.1). Pure backend: HTTP routes, WebSocket handlers, MCP tool wrappers, HTML serving. The web frontend lives in @wrongstack/webui; this package is the standalone server it can run on.",
6
6
  "keywords": [
@@ -40,16 +40,16 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "ws": "^8.21.1",
43
- "@wrongstack/core": "0.301.0",
44
- "@wrongstack/kanban": "0.301.0",
45
- "@wrongstack/mcp": "0.301.0",
46
- "@wrongstack/requirement-intake": "0.301.0",
47
- "@wrongstack/techstack": "0.301.0",
48
- "@wrongstack/sage": "0.301.0",
49
- "@wrongstack/providers": "0.301.0",
50
- "@wrongstack/runtime": "0.301.0",
51
- "@wrongstack/sdd": "0.301.0",
52
- "@wrongstack/tools": "0.301.0"
43
+ "@wrongstack/core": "0.302.0",
44
+ "@wrongstack/mcp": "0.302.0",
45
+ "@wrongstack/techstack": "0.302.0",
46
+ "@wrongstack/kanban": "0.302.0",
47
+ "@wrongstack/runtime": "0.302.0",
48
+ "@wrongstack/sage": "0.302.0",
49
+ "@wrongstack/providers": "0.302.0",
50
+ "@wrongstack/requirement-intake": "0.302.0",
51
+ "@wrongstack/tools": "0.302.0",
52
+ "@wrongstack/sdd": "0.302.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/node": "^26.1.2",