@wrongstack/webui 0.2.0 → 0.3.2

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.
@@ -1,25 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  // src/server/index.ts
3
- import * as fs from "fs/promises";
4
- import * as os from "os";
3
+ import * as fs2 from "fs/promises";
5
4
  import * as path from "path";
6
5
  import {
7
6
  Agent,
8
7
  AutoCompactionMiddleware,
9
- Container,
10
8
  Context,
11
- DefaultConfigLoader,
12
- DefaultConfigStore,
13
- DefaultErrorHandler,
14
- DefaultLogger,
15
9
  DefaultMemoryStore,
16
10
  DefaultModeStore,
17
11
  DefaultModelsRegistry,
18
- DefaultPathResolver,
19
- DefaultPermissionPolicy,
20
- DefaultRetryPolicy,
21
- DefaultSecretScrubber,
22
- DefaultSecretVault,
23
12
  DefaultSessionStore,
24
13
  DefaultSkillLoader,
25
14
  DefaultSystemPromptBuilder,
@@ -31,13 +20,29 @@ import {
31
20
  ToolRegistry,
32
21
  atomicWrite,
33
22
  createDefaultPipelines,
34
- migratePlaintextSecrets,
35
- resolveWstackPaths
23
+ DEFAULT_CONTEXT_WINDOW_MODE_ID,
24
+ listContextWindowModes,
25
+ repairToolUseAdjacency,
26
+ resolveContextWindowPolicy
36
27
  } from "@wrongstack/core";
37
28
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
38
29
  import { forgetTool, rememberTool } from "@wrongstack/tools";
39
- import { builtinTools } from "@wrongstack/tools/builtin";
30
+ import { builtinToolsPack } from "@wrongstack/tools/pack";
40
31
  import { WebSocket, WebSocketServer } from "ws";
32
+ import { randomBytes } from "crypto";
33
+ import { createDefaultContainer } from "@wrongstack/runtime";
34
+
35
+ // src/server/boot.ts
36
+ import * as fs from "fs/promises";
37
+ import * as os from "os";
38
+ import {
39
+ DefaultConfigLoader,
40
+ DefaultLogger,
41
+ DefaultPathResolver,
42
+ DefaultSecretVault,
43
+ migratePlaintextSecrets,
44
+ resolveWstackPaths
45
+ } from "@wrongstack/core";
41
46
  async function bootConfig() {
42
47
  const cwd = process.cwd();
43
48
  const pathResolver = new DefaultPathResolver(cwd);
@@ -64,18 +69,13 @@ async function bootConfig() {
64
69
  level: config.log?.level ?? "info",
65
70
  file: wpaths.logFile
66
71
  });
67
- return {
68
- config,
69
- vault,
70
- globalConfigPath: wpaths.globalConfig,
71
- projectRoot,
72
- wpaths,
73
- logger
74
- };
72
+ return { config, vault, globalConfigPath: wpaths.globalConfig, projectRoot, wpaths, logger };
75
73
  }
76
74
  function patchConfig(config, updates) {
77
75
  return Object.freeze({ ...config, ...updates });
78
76
  }
77
+
78
+ // src/server/index.ts
79
79
  async function startWebUI(opts = {}) {
80
80
  const wsPort2 = opts.wsPort ?? 3457;
81
81
  const wsHost2 = opts.wsHost ?? "127.0.0.1";
@@ -88,22 +88,8 @@ async function startWebUI(opts = {}) {
88
88
  cacheFile: wpaths.modelsCache,
89
89
  ttlSeconds: 24 * 3600
90
90
  });
91
- const container = new Container();
92
- const configStore = new DefaultConfigStore(config);
93
- container.bind(TOKENS.ConfigStore, () => configStore);
94
- container.bind(TOKENS.Logger, () => logger);
95
- container.bind(TOKENS.SecretScrubber, () => new DefaultSecretScrubber());
96
- container.bind(TOKENS.RetryPolicy, () => new DefaultRetryPolicy());
97
- container.bind(TOKENS.ErrorHandler, () => new DefaultErrorHandler());
98
- container.bind(TOKENS.ModelsRegistry, () => modelsRegistry);
99
- container.bind(
100
- TOKENS.PermissionPolicy,
101
- () => new DefaultPermissionPolicy({
102
- trustFile: wpaths.projectTrust,
103
- yolo: false,
104
- promptDelegate: void 0
105
- })
106
- );
91
+ const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry });
92
+ const configStore = container.resolve(TOKENS.ConfigStore);
107
93
  const providerRegistry = new ProviderRegistry();
108
94
  try {
109
95
  const factories = await buildProviderFactoriesFromRegistry({
@@ -116,7 +102,7 @@ async function startWebUI(opts = {}) {
116
102
  console.warn("[WebUI] Failed to load provider registry:", err);
117
103
  }
118
104
  const toolRegistry = new ToolRegistry();
119
- for (const t of builtinTools) toolRegistry.register(t);
105
+ toolRegistry.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
120
106
  const memoryStore = new DefaultMemoryStore({ paths: wpaths });
121
107
  if (config.features.memory) {
122
108
  toolRegistry.register(rememberTool(memoryStore));
@@ -192,24 +178,42 @@ async function startWebUI(opts = {}) {
192
178
  projectRoot,
193
179
  model: config.model
194
180
  });
181
+ const initialContextPolicy = resolveContextWindowPolicy(config.context);
182
+ context.meta["contextWindowMode"] = initialContextPolicy.id;
183
+ context.meta["contextWindowPolicy"] = initialContextPolicy;
195
184
  const pipelines = createDefaultPipelines();
196
185
  const compactor = new HybridCompactor({
197
186
  preserveK: config.context?.preserveK ?? 20,
198
187
  eliseThreshold: config.context?.eliseThreshold ?? 0.7
199
188
  });
200
189
  if (config.context?.autoCompact !== false) {
190
+ const effectiveMaxContext = config.context?.effectiveMaxContext ?? provider.capabilities.maxContext;
201
191
  const autoCompactor = new AutoCompactionMiddleware(
202
192
  compactor,
203
- 2e5,
193
+ effectiveMaxContext,
204
194
  (ctx) => {
205
195
  let total = 0;
206
196
  for (const m of ctx.messages) {
207
197
  if (typeof m.content === "string") total += Math.ceil(m.content.length / 4);
198
+ else if (Array.isArray(m.content)) {
199
+ for (const b of m.content) total += Math.ceil(JSON.stringify(b).length / 4);
200
+ }
208
201
  }
209
202
  return total;
210
203
  },
211
- { warn: 0.7, soft: 0.85, hard: 0.95 },
212
- { events }
204
+ {
205
+ warn: initialContextPolicy.thresholds.warn,
206
+ soft: initialContextPolicy.thresholds.soft,
207
+ hard: initialContextPolicy.thresholds.hard
208
+ },
209
+ {
210
+ events,
211
+ aggressiveOn: initialContextPolicy.aggressiveOn,
212
+ policyProvider: (ctx) => {
213
+ const policy = ctx.meta["contextWindowPolicy"];
214
+ return policy && typeof policy === "object" ? policy : initialContextPolicy;
215
+ }
216
+ }
213
217
  );
214
218
  pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
215
219
  }
@@ -251,15 +255,27 @@ async function startWebUI(opts = {}) {
251
255
  cacheReadCost,
252
256
  projectName: path.basename(projectRoot) || projectRoot,
253
257
  cwd: projectRoot,
254
- mode: modeId
258
+ mode: modeId,
259
+ contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
260
+ wsToken
255
261
  };
256
262
  }
263
+ const wsToken = randomBytes(16).toString("hex");
264
+ console.log(`[WebUI] WS auth token: ${wsToken}`);
265
+ const isLoopback = (hostname) => hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
257
266
  const verifyClient = (info) => {
258
267
  const origin = info.origin;
259
- if (!origin) return true;
268
+ const url = info.req.url ?? "";
269
+ const tokenMatch = url.match(/[?&]token=([^&]+)/);
270
+ const providedToken = tokenMatch ? tokenMatch[1] : void 0;
271
+ const tokenOk = providedToken === wsToken;
272
+ if (!origin) {
273
+ return tokenOk || wsHost2 === "127.0.0.1" || wsHost2 === "::1" || wsHost2 === "localhost";
274
+ }
260
275
  try {
261
276
  const { hostname } = new URL(origin);
262
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
277
+ if (isLoopback(hostname)) return true;
278
+ return tokenOk;
263
279
  } catch {
264
280
  return false;
265
281
  }
@@ -336,6 +352,16 @@ async function startWebUI(opts = {}) {
336
352
  }
337
353
  });
338
354
  });
355
+ events.on("context.repaired", (e) => {
356
+ broadcast({
357
+ type: "context.repaired",
358
+ payload: {
359
+ removedToolUses: e.removedToolUses,
360
+ removedToolResults: e.removedToolResults,
361
+ removedMessages: e.removedMessages
362
+ }
363
+ });
364
+ });
339
365
  events.on("error", (e) => {
340
366
  broadcast({
341
367
  type: "error",
@@ -536,6 +562,8 @@ async function startWebUI(opts = {}) {
536
562
  type: "context.debug",
537
563
  payload: {
538
564
  total,
565
+ mode: context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID,
566
+ policy: context.meta["contextWindowPolicy"],
539
567
  systemPrompt: sysTokens,
540
568
  tools: { total: toolTokens, count: tools.length, breakdown: toolBreakdown },
541
569
  messages: {
@@ -557,7 +585,8 @@ async function startWebUI(opts = {}) {
557
585
  before: report.before,
558
586
  after: report.after,
559
587
  saved: Math.max(0, report.before - report.after),
560
- reductions: report.reductions
588
+ reductions: report.reductions,
589
+ repaired: report.repaired
561
590
  }
562
591
  });
563
592
  sendResult(
@@ -570,6 +599,63 @@ async function startWebUI(opts = {}) {
570
599
  }
571
600
  break;
572
601
  }
602
+ case "context.repair": {
603
+ const beforeMessages = context.messages.length;
604
+ const repaired = repairToolUseAdjacency(context.messages);
605
+ if (repaired.report.changed) {
606
+ context.state.replaceMessages(repaired.messages);
607
+ }
608
+ const payload = {
609
+ removedToolUses: repaired.report.removedToolUses,
610
+ removedToolResults: repaired.report.removedToolResults,
611
+ removedMessages: repaired.report.removedMessages,
612
+ beforeMessages,
613
+ afterMessages: context.messages.length
614
+ };
615
+ broadcast({ type: "context.repaired", payload });
616
+ const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
617
+ sendResult(
618
+ ws,
619
+ true,
620
+ removed > 0 ? `Context repaired: removed ${removed} orphan protocol item(s)` : "Context repair found no orphan protocol blocks"
621
+ );
622
+ break;
623
+ }
624
+ case "context.modes.list": {
625
+ const active = String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID);
626
+ send(ws, {
627
+ type: "context.modes.list",
628
+ payload: {
629
+ activeId: active,
630
+ modes: listContextWindowModes().map((m) => ({
631
+ id: m.id,
632
+ name: m.name,
633
+ description: m.description,
634
+ isActive: m.id === active,
635
+ thresholds: m.thresholds,
636
+ preserveK: m.preserveK,
637
+ eliseThreshold: m.eliseThreshold
638
+ }))
639
+ }
640
+ });
641
+ break;
642
+ }
643
+ case "context.mode.switch": {
644
+ const { id } = msg.payload;
645
+ const policy = resolveContextWindowPolicy({}, id);
646
+ if (policy.id !== id) {
647
+ sendResult(ws, false, `Unknown context mode "${id}"`);
648
+ break;
649
+ }
650
+ context.meta["contextWindowMode"] = policy.id;
651
+ context.meta["contextWindowPolicy"] = policy;
652
+ sendResult(ws, true, `Context mode switched to ${policy.id}`);
653
+ broadcast({
654
+ type: "context.mode.changed",
655
+ payload: { id: policy.id, name: policy.name, policy }
656
+ });
657
+ break;
658
+ }
573
659
  case "providers.list": {
574
660
  const providers = await modelsRegistry.listProviders();
575
661
  const savedIds = new Set(Object.keys(config.providers ?? {}));
@@ -624,7 +710,7 @@ async function startWebUI(opts = {}) {
624
710
  const newProv = providerRegistry.has(newProvider) ? providerRegistry.create({ ...providerCfg, type: newProvider }) : makeProviderFromConfig(newProvider, providerCfg);
625
711
  context.provider = newProv;
626
712
  try {
627
- const raw = await fs.readFile(globalConfigPath, "utf8");
713
+ const raw = await fs2.readFile(globalConfigPath, "utf8");
628
714
  const parsed = JSON.parse(raw);
629
715
  parsed.provider = newProvider;
630
716
  parsed.model = newModel;
@@ -926,7 +1012,7 @@ async function startWebUI(opts = {}) {
926
1012
  if (depth > 8 || results.length >= 600) return;
927
1013
  let entries = [];
928
1014
  try {
929
- entries = await fs.readdir(dir, { withFileTypes: true });
1015
+ entries = await fs2.readdir(dir, { withFileTypes: true });
930
1016
  } catch {
931
1017
  return;
932
1018
  }
@@ -1063,7 +1149,7 @@ async function startWebUI(opts = {}) {
1063
1149
  }
1064
1150
  async function loadSavedProviders() {
1065
1151
  try {
1066
- const raw = await fs.readFile(globalConfigPath, "utf8");
1152
+ const raw = await fs2.readFile(globalConfigPath, "utf8");
1067
1153
  const parsed = JSON.parse(raw);
1068
1154
  return parsed.providers ?? {};
1069
1155
  } catch {
@@ -1073,7 +1159,7 @@ async function startWebUI(opts = {}) {
1073
1159
  async function saveProviders(providers) {
1074
1160
  let parsed;
1075
1161
  try {
1076
- const raw = await fs.readFile(globalConfigPath, "utf8");
1162
+ const raw = await fs2.readFile(globalConfigPath, "utf8");
1077
1163
  parsed = JSON.parse(raw);
1078
1164
  } catch {
1079
1165
  parsed = {};