@wrongstack/webui 0.66.13 → 0.68.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.
@@ -1625,6 +1625,60 @@ function computeUsageCost(usage, rates) {
1625
1625
  return (usage.input * rates.input + usage.output * rates.output + (usage.cacheRead ?? 0) * rates.cacheRead) / 1e6;
1626
1626
  }
1627
1627
 
1628
+ // src/server/provider-config-io.ts
1629
+ import * as fs3 from "fs/promises";
1630
+ import * as path3 from "path";
1631
+ import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
1632
+ import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
1633
+ import { DefaultSecretVault } from "@wrongstack/core";
1634
+ async function loadSavedProviders(configPath, vault) {
1635
+ let raw;
1636
+ try {
1637
+ raw = await fs3.readFile(configPath, "utf8");
1638
+ } catch {
1639
+ return {};
1640
+ }
1641
+ let parsed = {};
1642
+ try {
1643
+ parsed = JSON.parse(raw);
1644
+ } catch {
1645
+ return {};
1646
+ }
1647
+ if (!parsed.providers) return {};
1648
+ return decryptConfigSecrets(parsed.providers, vault);
1649
+ }
1650
+ async function saveProviders(configPath, vault, providers) {
1651
+ let raw;
1652
+ let fileExists = true;
1653
+ try {
1654
+ raw = await fs3.readFile(configPath, "utf8");
1655
+ } catch (err) {
1656
+ if (err.code !== "ENOENT") {
1657
+ throw new Error(
1658
+ `Refusing to mutate ${configPath}: ${err.message}`,
1659
+ { cause: err }
1660
+ );
1661
+ }
1662
+ fileExists = false;
1663
+ raw = "{}";
1664
+ }
1665
+ let parsed;
1666
+ try {
1667
+ parsed = JSON.parse(raw);
1668
+ } catch (err) {
1669
+ if (fileExists) {
1670
+ throw new Error(
1671
+ `Refusing to overwrite corrupt config at ${configPath} (${err.message}). Fix or move the file aside before retrying.`,
1672
+ { cause: err }
1673
+ );
1674
+ }
1675
+ parsed = {};
1676
+ }
1677
+ parsed.providers = providers;
1678
+ const encrypted = encryptConfigSecrets(parsed, vault);
1679
+ await atomicWrite2(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
1680
+ }
1681
+
1628
1682
  // src/server/provider-keys.ts
1629
1683
  function normalizeKeys(cfg) {
1630
1684
  if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
@@ -1720,60 +1774,6 @@ function removeProvider(providers, providerId) {
1720
1774
  return { ok: true, message: `Provider "${providerId}" removed` };
1721
1775
  }
1722
1776
 
1723
- // src/server/provider-config-io.ts
1724
- import * as fs3 from "fs/promises";
1725
- import * as path3 from "path";
1726
- import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
1727
- import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
1728
- import { DefaultSecretVault } from "@wrongstack/core";
1729
- async function loadSavedProviders(configPath, vault) {
1730
- let raw;
1731
- try {
1732
- raw = await fs3.readFile(configPath, "utf8");
1733
- } catch {
1734
- return {};
1735
- }
1736
- let parsed = {};
1737
- try {
1738
- parsed = JSON.parse(raw);
1739
- } catch {
1740
- return {};
1741
- }
1742
- if (!parsed.providers) return {};
1743
- return decryptConfigSecrets(parsed.providers, vault);
1744
- }
1745
- async function saveProviders(configPath, vault, providers) {
1746
- let raw;
1747
- let fileExists = true;
1748
- try {
1749
- raw = await fs3.readFile(configPath, "utf8");
1750
- } catch (err) {
1751
- if (err.code !== "ENOENT") {
1752
- throw new Error(
1753
- `Refusing to mutate ${configPath}: ${err.message}`,
1754
- { cause: err }
1755
- );
1756
- }
1757
- fileExists = false;
1758
- raw = "{}";
1759
- }
1760
- let parsed;
1761
- try {
1762
- parsed = JSON.parse(raw);
1763
- } catch (err) {
1764
- if (fileExists) {
1765
- throw new Error(
1766
- `Refusing to overwrite corrupt config at ${configPath} (${err.message}). Fix or move the file aside before retrying.`,
1767
- { cause: err }
1768
- );
1769
- }
1770
- parsed = {};
1771
- }
1772
- parsed.providers = providers;
1773
- const encrypted = encryptConfigSecrets(parsed, vault);
1774
- await atomicWrite2(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
1775
- }
1776
-
1777
1777
  // src/server/ws-utils.ts
1778
1778
  import { randomBytes } from "crypto";
1779
1779
  import { WebSocket } from "ws";
@@ -1803,6 +1803,130 @@ function generateAuthToken() {
1803
1803
  return randomBytes(16).toString("hex");
1804
1804
  }
1805
1805
 
1806
+ // src/server/provider-handlers.ts
1807
+ function createProviderHandlers(deps) {
1808
+ const { globalConfigPath, vault } = deps;
1809
+ let configWriteLock = deps.getConfigWriteLock();
1810
+ async function loadConfigProviders() {
1811
+ return loadSavedProviders(globalConfigPath, vault);
1812
+ }
1813
+ async function saveConfigProviders(providers) {
1814
+ const next = configWriteLock.then(() => saveProviders(globalConfigPath, vault, providers));
1815
+ configWriteLock = next;
1816
+ deps.setConfigWriteLock(next);
1817
+ await next;
1818
+ }
1819
+ async function handleKeyUpsert(ws, providerId, label, apiKey) {
1820
+ try {
1821
+ const providers = await loadConfigProviders();
1822
+ const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
1823
+ if (result.ok) await saveConfigProviders(providers);
1824
+ sendResult(ws, result.ok, result.message);
1825
+ } catch (err) {
1826
+ sendResult(ws, false, errMessage(err));
1827
+ }
1828
+ }
1829
+ async function handleKeyDelete(ws, providerId, label) {
1830
+ try {
1831
+ const providers = await loadConfigProviders();
1832
+ const result = deleteKey(providers, providerId, label);
1833
+ if (result.ok) await saveConfigProviders(providers);
1834
+ sendResult(ws, result.ok, result.message);
1835
+ } catch (err) {
1836
+ sendResult(ws, false, errMessage(err));
1837
+ }
1838
+ }
1839
+ async function handleKeySetActive(ws, providerId, label) {
1840
+ try {
1841
+ const providers = await loadConfigProviders();
1842
+ const result = setActiveKey(providers, providerId, label);
1843
+ if (result.ok) await saveConfigProviders(providers);
1844
+ sendResult(ws, result.ok, result.message);
1845
+ } catch (err) {
1846
+ sendResult(ws, false, errMessage(err));
1847
+ }
1848
+ }
1849
+ async function handleProviderAdd(ws, payload) {
1850
+ try {
1851
+ const providers = await loadConfigProviders();
1852
+ const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
1853
+ if (result.ok) await saveConfigProviders(providers);
1854
+ sendResult(ws, result.ok, result.message);
1855
+ } catch (err) {
1856
+ sendResult(ws, false, errMessage(err));
1857
+ }
1858
+ }
1859
+ async function handleProviderRemove(ws, providerId) {
1860
+ try {
1861
+ const providers = await loadConfigProviders();
1862
+ const result = removeProvider(providers, providerId);
1863
+ if (result.ok) await saveConfigProviders(providers);
1864
+ sendResult(ws, result.ok, result.message);
1865
+ } catch (err) {
1866
+ sendResult(ws, false, errMessage(err));
1867
+ }
1868
+ }
1869
+ return { handleKeyUpsert, handleKeyDelete, handleKeySetActive, handleProviderAdd, handleProviderRemove, loadConfigProviders };
1870
+ }
1871
+
1872
+ // src/server/setup-events.ts
1873
+ function setupEvents(deps) {
1874
+ const { events, broadcast: broadcast2, clients, config, context, pendingConfirms } = deps;
1875
+ events.on("iteration.started", (e) => {
1876
+ broadcast2(clients, {
1877
+ type: "iteration.started",
1878
+ payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
1879
+ });
1880
+ });
1881
+ events.on("provider.text_delta", (e) => {
1882
+ broadcast2(clients, { type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
1883
+ });
1884
+ events.on("provider.thinking_delta", (e) => {
1885
+ broadcast2(clients, { type: "provider.thinking_delta", payload: { text: e.text } });
1886
+ });
1887
+ events.on("tool.started", (e) => {
1888
+ broadcast2(clients, {
1889
+ type: "tool.started",
1890
+ payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
1891
+ });
1892
+ });
1893
+ events.on("tool.progress", (e) => {
1894
+ broadcast2(clients, {
1895
+ type: "tool.progress",
1896
+ payload: { id: e.id, name: e.name, eventType: e.event.type, text: e.event.text }
1897
+ });
1898
+ });
1899
+ events.on("tool.executed", (e) => {
1900
+ broadcast2(clients, {
1901
+ type: "tool.executed",
1902
+ payload: { id: e.id, name: e.name, durationMs: e.durationMs, ok: e.ok, input: e.input, output: e.output }
1903
+ });
1904
+ broadcast2(clients, { type: "todos.updated", payload: { todos: [...context.todos] } });
1905
+ });
1906
+ events.on("provider.response", (e) => {
1907
+ broadcast2(clients, { type: "provider.response", payload: { usage: e.usage, stopReason: e.stopReason, messageId: "current" } });
1908
+ });
1909
+ events.on("context.repaired", (e) => {
1910
+ broadcast2(clients, { type: "context.repaired", payload: { removedToolUses: e.removedToolUses, removedToolResults: e.removedToolResults, removedMessages: e.removedMessages } });
1911
+ });
1912
+ events.on("tool.confirm_needed", (e) => {
1913
+ const id = e.toolUseId ?? `confirm_${Date.now()}`;
1914
+ pendingConfirms.set(id, e.resolve);
1915
+ broadcast2(clients, { type: "tool.confirm_needed", payload: { id, toolName: e.tool?.name ?? "unknown", input: e.input, suggestedPattern: e.suggestedPattern } });
1916
+ });
1917
+ events.on("error", (e) => {
1918
+ broadcast2(clients, { type: "error", payload: { phase: e.phase, message: e.err instanceof Error ? e.err.message : String(e.err) } });
1919
+ });
1920
+ const forwardSubagent = (kind, payload) => broadcast2(clients, { type: "subagent.event", payload: { kind, ...payload } });
1921
+ events.on("subagent.spawned", (e) => forwardSubagent("spawned", { subagentId: e.subagentId, taskId: e.taskId, name: e.name, provider: e.provider, model: e.model, description: e.description }));
1922
+ events.on("subagent.task_started", (e) => forwardSubagent("task_started", { subagentId: e.subagentId, taskId: e.taskId, description: e.description }));
1923
+ events.on("subagent.tool_executed", (e) => forwardSubagent("tool_executed", { subagentId: e.subagentId, toolName: e.name, durationMs: e.durationMs, ok: e.ok }));
1924
+ events.on("subagent.iteration_summary", (e) => forwardSubagent("iteration_summary", { subagentId: e.subagentId, iteration: e.iteration, toolCalls: e.toolCalls, costUsd: e.costUsd, currentTool: e.currentTool }));
1925
+ events.on("subagent.budget_extended", (e) => forwardSubagent("budget_extended", { subagentId: e.subagentId, totalExtensions: e.totalExtensions }));
1926
+ events.on("subagent.ctx_pct", (e) => forwardSubagent("ctx_pct", { subagentId: e.subagentId, load: e.load, tokens: e.tokens, maxContext: e.maxContext }));
1927
+ events.on("subagent.task_completed", (e) => forwardSubagent("task_completed", { subagentId: e.subagentId, status: e.status, iterations: e.iterations, toolCalls: e.toolCalls, error: e.error ? { kind: e.error.kind, message: e.error.message } : void 0 }));
1928
+ }
1929
+
1806
1930
  // src/server/token-estimator.ts
1807
1931
  function estimateTokens(s) {
1808
1932
  return Math.ceil(s.length / 4);
@@ -2183,165 +2307,6 @@ async function startWebUI(opts = {}) {
2183
2307
  `[WebUI] WebSocket server running on ws://${wsHost}:${wsPort}` + (wssSecondary ? ` (and ws://[::1]:${wsPort})` : "")
2184
2308
  );
2185
2309
  const pendingConfirms = /* @__PURE__ */ new Map();
2186
- function setupEvents() {
2187
- events.on("iteration.started", (e) => {
2188
- broadcast(clients, {
2189
- type: "iteration.started",
2190
- payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
2191
- });
2192
- });
2193
- events.on("provider.text_delta", (e) => {
2194
- broadcast(clients, { type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
2195
- });
2196
- events.on("provider.thinking_delta", (e) => {
2197
- broadcast(clients, { type: "provider.thinking_delta", payload: { text: e.text } });
2198
- });
2199
- events.on("tool.started", (e) => {
2200
- broadcast(clients, {
2201
- type: "tool.started",
2202
- payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
2203
- });
2204
- });
2205
- events.on("tool.progress", (e) => {
2206
- broadcast(clients, {
2207
- type: "tool.progress",
2208
- payload: {
2209
- id: e.id,
2210
- name: e.name,
2211
- eventType: e.event.type,
2212
- text: e.event.text
2213
- }
2214
- });
2215
- });
2216
- events.on("tool.executed", (e) => {
2217
- broadcast(clients, {
2218
- type: "tool.executed",
2219
- payload: {
2220
- // Forward the tool_use id so frontend can correlate with the
2221
- // matching tool.started bubble — without this, parallel tool calls
2222
- // all stay stuck on "Running…" because the frontend can't tell
2223
- // which bubble this result belongs to.
2224
- id: e.id,
2225
- name: e.name,
2226
- durationMs: e.durationMs,
2227
- ok: e.ok,
2228
- input: e.input,
2229
- output: e.output
2230
- }
2231
- });
2232
- broadcast(clients, {
2233
- type: "todos.updated",
2234
- payload: { todos: [...context.todos] }
2235
- });
2236
- });
2237
- events.on("provider.response", (e) => {
2238
- broadcast(clients, {
2239
- type: "provider.response",
2240
- payload: {
2241
- usage: e.usage,
2242
- stopReason: e.stopReason,
2243
- messageId: "current"
2244
- }
2245
- });
2246
- });
2247
- events.on("context.repaired", (e) => {
2248
- broadcast(clients, {
2249
- type: "context.repaired",
2250
- payload: {
2251
- removedToolUses: e.removedToolUses,
2252
- removedToolResults: e.removedToolResults,
2253
- removedMessages: e.removedMessages
2254
- }
2255
- });
2256
- });
2257
- events.on("tool.confirm_needed", (e) => {
2258
- const id = e.toolUseId ?? `confirm_${Date.now()}`;
2259
- pendingConfirms.set(id, e.resolve);
2260
- broadcast(clients, {
2261
- type: "tool.confirm_needed",
2262
- payload: {
2263
- id,
2264
- toolName: e.tool?.name ?? "unknown",
2265
- input: e.input,
2266
- suggestedPattern: e.suggestedPattern
2267
- }
2268
- });
2269
- });
2270
- events.on("error", (e) => {
2271
- broadcast(clients, {
2272
- type: "error",
2273
- payload: {
2274
- phase: e.phase,
2275
- message: e.err instanceof Error ? e.err.message : String(e.err)
2276
- }
2277
- });
2278
- });
2279
- const forwardSubagent = (kind, payload) => broadcast(clients, { type: "subagent.event", payload: { kind, ...payload } });
2280
- events.on(
2281
- "subagent.spawned",
2282
- (e) => forwardSubagent("spawned", {
2283
- subagentId: e.subagentId,
2284
- taskId: e.taskId,
2285
- name: e.name,
2286
- provider: e.provider,
2287
- model: e.model,
2288
- description: e.description
2289
- })
2290
- );
2291
- events.on(
2292
- "subagent.task_started",
2293
- (e) => forwardSubagent("task_started", {
2294
- subagentId: e.subagentId,
2295
- taskId: e.taskId,
2296
- description: e.description
2297
- })
2298
- );
2299
- events.on(
2300
- "subagent.tool_executed",
2301
- (e) => forwardSubagent("tool_executed", {
2302
- subagentId: e.subagentId,
2303
- toolName: e.name,
2304
- durationMs: e.durationMs,
2305
- ok: e.ok
2306
- })
2307
- );
2308
- events.on(
2309
- "subagent.iteration_summary",
2310
- (e) => forwardSubagent("iteration_summary", {
2311
- subagentId: e.subagentId,
2312
- iteration: e.iteration,
2313
- toolCalls: e.toolCalls,
2314
- costUsd: e.costUsd,
2315
- currentTool: e.currentTool
2316
- })
2317
- );
2318
- events.on(
2319
- "subagent.budget_extended",
2320
- (e) => forwardSubagent("budget_extended", {
2321
- subagentId: e.subagentId,
2322
- totalExtensions: e.totalExtensions
2323
- })
2324
- );
2325
- events.on(
2326
- "subagent.ctx_pct",
2327
- (e) => forwardSubagent("ctx_pct", {
2328
- subagentId: e.subagentId,
2329
- load: e.load,
2330
- tokens: e.tokens,
2331
- maxContext: e.maxContext
2332
- })
2333
- );
2334
- events.on(
2335
- "subagent.task_completed",
2336
- (e) => forwardSubagent("task_completed", {
2337
- subagentId: e.subagentId,
2338
- status: e.status,
2339
- iterations: e.iterations,
2340
- toolCalls: e.toolCalls,
2341
- error: e.error ? { kind: e.error.kind, message: e.error.message } : void 0
2342
- })
2343
- );
2344
- }
2345
2310
  const handleConnection = (ws) => {
2346
2311
  const client = { ws, sessionId: session.id, connectedAt: Date.now() };
2347
2312
  clients.set(ws, client);
@@ -2399,7 +2364,7 @@ async function startWebUI(opts = {}) {
2399
2364
  if (eventsArmed) return;
2400
2365
  eventsArmed = true;
2401
2366
  console.log(`[WebUI] Backend ready (${label})`);
2402
- setupEvents();
2367
+ setupEvents({ events, broadcast, clients, config, context, pendingConfirms });
2403
2368
  };
2404
2369
  wssPrimary.on("listening", () => armOnce(`${wsHost}:${wsPort}`));
2405
2370
  wssPrimary.on("connection", handleConnection);
@@ -2636,7 +2601,7 @@ async function startWebUI(opts = {}) {
2636
2601
  break;
2637
2602
  }
2638
2603
  case "providers.saved": {
2639
- const saved = await loadConfigProviders();
2604
+ const saved = await providerHandlers.loadConfigProviders();
2640
2605
  send(ws, {
2641
2606
  type: "providers.saved",
2642
2607
  payload: {
@@ -2725,27 +2690,27 @@ async function startWebUI(opts = {}) {
2725
2690
  case "key.add":
2726
2691
  case "key.update": {
2727
2692
  const { providerId, label, apiKey } = msg.payload;
2728
- await handleKeyUpsert(ws, providerId, label, apiKey);
2693
+ await providerHandlers.handleKeyUpsert(ws, providerId, label, apiKey);
2729
2694
  break;
2730
2695
  }
2731
2696
  case "key.delete": {
2732
2697
  const { providerId, label } = msg.payload;
2733
- await handleKeyDelete(ws, providerId, label);
2698
+ await providerHandlers.handleKeyDelete(ws, providerId, label);
2734
2699
  break;
2735
2700
  }
2736
2701
  case "key.set_active": {
2737
2702
  const { providerId, label } = msg.payload;
2738
- await handleKeySetActive(ws, providerId, label);
2703
+ await providerHandlers.handleKeySetActive(ws, providerId, label);
2739
2704
  break;
2740
2705
  }
2741
2706
  case "provider.add": {
2742
2707
  const p = msg.payload;
2743
- await handleProviderAdd(ws, p);
2708
+ await providerHandlers.handleProviderAdd(ws, p);
2744
2709
  break;
2745
2710
  }
2746
2711
  case "provider.remove": {
2747
2712
  const { providerId } = msg.payload;
2748
- await handleProviderRemove(ws, providerId);
2713
+ await providerHandlers.handleProviderRemove(ws, providerId);
2749
2714
  break;
2750
2715
  }
2751
2716
  case "sessions.list": {
@@ -3130,65 +3095,14 @@ async function startWebUI(opts = {}) {
3130
3095
  }
3131
3096
  }
3132
3097
  }
3133
- async function loadConfigProviders() {
3134
- return loadSavedProviders(globalConfigPath, vault);
3135
- }
3136
- async function saveConfigProviders(providers) {
3137
- configWriteLock = configWriteLock.then(
3138
- () => saveProviders(globalConfigPath, vault, providers)
3139
- );
3140
- await configWriteLock;
3141
- }
3142
- async function handleKeyUpsert(ws, providerId, label, apiKey) {
3143
- try {
3144
- const providers = await loadConfigProviders();
3145
- const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
3146
- if (result.ok) await saveConfigProviders(providers);
3147
- sendResult(ws, result.ok, result.message);
3148
- } catch (err) {
3149
- sendResult(ws, false, errMessage(err));
3098
+ const providerHandlers = createProviderHandlers({
3099
+ globalConfigPath,
3100
+ vault,
3101
+ getConfigWriteLock: () => configWriteLock,
3102
+ setConfigWriteLock: (p) => {
3103
+ configWriteLock = p;
3150
3104
  }
3151
- }
3152
- async function handleKeyDelete(ws, providerId, label) {
3153
- try {
3154
- const providers = await loadConfigProviders();
3155
- const result = deleteKey(providers, providerId, label);
3156
- if (result.ok) await saveConfigProviders(providers);
3157
- sendResult(ws, result.ok, result.message);
3158
- } catch (err) {
3159
- sendResult(ws, false, errMessage(err));
3160
- }
3161
- }
3162
- async function handleKeySetActive(ws, providerId, label) {
3163
- try {
3164
- const providers = await loadConfigProviders();
3165
- const result = setActiveKey(providers, providerId, label);
3166
- if (result.ok) await saveConfigProviders(providers);
3167
- sendResult(ws, result.ok, result.message);
3168
- } catch (err) {
3169
- sendResult(ws, false, errMessage(err));
3170
- }
3171
- }
3172
- async function handleProviderAdd(ws, payload) {
3173
- try {
3174
- const providers = await loadConfigProviders();
3175
- const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
3176
- if (result.ok) await saveConfigProviders(providers);
3177
- sendResult(ws, result.ok, result.message);
3178
- } catch (err) {
3179
- sendResult(ws, false, errMessage(err));
3180
- }
3181
- }
3182
- async function handleProviderRemove(ws, providerId) {
3183
- try {
3184
- const providers = await loadConfigProviders();
3185
- const result = removeProvider(providers, providerId);
3186
- if (result.ok) await saveConfigProviders(providers);
3187
- sendResult(ws, result.ok, result.message);
3188
- } catch (err) {
3189
- sendResult(ws, false, errMessage(err));
3190
- }
3191
- }
3105
+ });
3192
3106
  const httpServer = createHttpServer({
3193
3107
  host: wsHost,
3194
3108
  distDir: path4.resolve(import.meta.dirname, "../../dist"),