@wrongstack/webui 0.5.3 → 0.5.6

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.
@@ -302,6 +302,20 @@ async function startWebUI(opts = {}) {
302
302
  });
303
303
  const wssSecondary = wsHost === "127.0.0.1" ? new WebSocketServer({ port: wsPort, host: "::1", verifyClient }) : null;
304
304
  const clients = /* @__PURE__ */ new Map();
305
+ const RATE_LIMIT_MESSAGES = 60;
306
+ const RATE_LIMIT_WINDOW_MS = 6e4;
307
+ const rateLimits = /* @__PURE__ */ new Map();
308
+ function checkRateLimit(ws) {
309
+ const now = Date.now();
310
+ const limit = rateLimits.get(ws);
311
+ if (!limit || now > limit.resetAt) {
312
+ rateLimits.set(ws, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
313
+ return true;
314
+ }
315
+ if (limit.count >= RATE_LIMIT_MESSAGES) return false;
316
+ limit.count++;
317
+ return true;
318
+ }
305
319
  let runLock = null;
306
320
  console.log(
307
321
  `[WebUI] WebSocket server running on ws://${wsHost}:${wsPort}` + (wssSecondary ? ` (and ws://[::1]:${wsPort})` : "")
@@ -415,13 +429,23 @@ async function startWebUI(opts = {}) {
415
429
  }
416
430
  }
417
431
  const handleConnection = (ws) => {
418
- const client = { ws, sessionId: session.id };
432
+ const client = { ws, sessionId: session.id, connectedAt: Date.now() };
419
433
  clients.set(ws, client);
420
434
  console.log("[WebUI] Client connected, total:", clients.size);
421
435
  void sessionStartPayload().then((payload) => {
422
436
  send(ws, { type: "session.start", payload });
423
437
  });
424
438
  ws.on("message", async (data) => {
439
+ if (!checkRateLimit(ws)) {
440
+ send(ws, {
441
+ type: "error",
442
+ payload: {
443
+ phase: "rate_limit",
444
+ message: "Too many messages. Please wait before sending more."
445
+ }
446
+ });
447
+ return;
448
+ }
425
449
  try {
426
450
  const msg = JSON.parse(data.toString());
427
451
  await handleMessage(ws, client, msg);
@@ -431,6 +455,7 @@ async function startWebUI(opts = {}) {
431
455
  });
432
456
  ws.on("close", () => {
433
457
  clients.delete(ws);
458
+ rateLimits.delete(ws);
434
459
  console.log("[WebUI] Client disconnected, total:", clients.size);
435
460
  if (pendingConfirms.size > 0) {
436
461
  for (const [id, resolve] of pendingConfirms) {
@@ -1007,11 +1032,64 @@ async function startWebUI(opts = {}) {
1007
1032
  break;
1008
1033
  }
1009
1034
  case "todos.clear": {
1010
- context.todos.length = 0;
1035
+ context.state.replaceTodos([]);
1011
1036
  sendResult(ws, true, "Todos cleared");
1012
1037
  broadcast({ type: "todos.updated", payload: { todos: [] } });
1013
1038
  break;
1014
1039
  }
1040
+ case "plan.get": {
1041
+ const planPath = context.meta["plan.path"];
1042
+ if (typeof planPath === "string" && planPath) {
1043
+ try {
1044
+ const { loadPlan } = await import("@wrongstack/core");
1045
+ const plan = await loadPlan(planPath);
1046
+ send(ws, {
1047
+ type: "plan.updated",
1048
+ payload: { plan: plan ?? { version: 1, sessionId: session.id, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), items: [] } }
1049
+ });
1050
+ } catch {
1051
+ send(ws, {
1052
+ type: "plan.updated",
1053
+ payload: { plan: { version: 1, sessionId: session.id, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), items: [] } }
1054
+ });
1055
+ }
1056
+ } else {
1057
+ send(ws, {
1058
+ type: "plan.updated",
1059
+ payload: { plan: null, error: "Plan storage is not configured for this session." }
1060
+ });
1061
+ }
1062
+ break;
1063
+ }
1064
+ case "plan.template_use": {
1065
+ const { template } = msg.payload;
1066
+ const planPath = context.meta["plan.path"];
1067
+ if (typeof planPath !== "string" || !planPath) {
1068
+ sendResult(ws, false, "Plan storage is not configured for this session.");
1069
+ break;
1070
+ }
1071
+ try {
1072
+ const { getPlanTemplate, loadPlan, savePlan, emptyPlan, addPlanItem, formatPlan } = await import("@wrongstack/core");
1073
+ const tpl = getPlanTemplate(template);
1074
+ if (!tpl) {
1075
+ sendResult(ws, false, `Unknown template "${template}".`);
1076
+ break;
1077
+ }
1078
+ let plan = await loadPlan(planPath) ?? emptyPlan(session.id);
1079
+ for (const item of tpl.items) {
1080
+ ({ plan } = addPlanItem(plan, item.title, item.details));
1081
+ }
1082
+ await savePlan(planPath, plan);
1083
+ sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
1084
+ broadcast({
1085
+ type: "plan.updated",
1086
+ payload: { plan }
1087
+ });
1088
+ } catch (err) {
1089
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
1090
+ }
1091
+ break;
1092
+ }
1015
1093
  case "files.list": {
1016
1094
  const payload = msg.payload ?? {};
1017
1095
  const query = (payload.query ?? "").toLowerCase();