@wrongstack/cli 0.1.3 → 0.1.7

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
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { color, DefaultLogger, DefaultModelsRegistry, Container, DefaultConfigStore, TOKENS, DefaultSecretScrubber, DefaultRetryPolicy, DefaultErrorHandler, DefaultTokenCounter, DefaultSessionStore, DefaultMemoryStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultPermissionPolicy, HybridCompactor, ProviderRegistry, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, createDefaultPipelines, AutoCompactionMiddleware, Agent, SlashCommandRegistry, loadPlugins, DefaultPathResolver, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, InputBuilder, DefaultPluginAPI, DefaultSessionReader, atomicWrite, makeAgentSubagentRunner, DefaultMultiAgentCoordinator, decryptConfigSecrets, encryptConfigSecrets } from '@wrongstack/core';
2
+ import { color, DefaultLogger, DefaultModelsRegistry, Container, DefaultConfigStore, TOKENS, DefaultSecretScrubber, DefaultRetryPolicy, DefaultErrorHandler, DefaultTokenCounter, DefaultModeStore, DefaultSessionStore, DefaultMemoryStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultPermissionPolicy, HybridCompactor, ProviderRegistry, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, createDefaultPipelines, AutoCompactionMiddleware, Agent, SlashCommandRegistry, loadPlugins, DefaultPathResolver, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, InputBuilder, DefaultPluginAPI, atomicWrite, DefaultSessionReader, makeAgentSubagentRunner, DefaultMultiAgentCoordinator, decryptConfigSecrets, encryptConfigSecrets } from '@wrongstack/core';
3
+ import { WebSocketServer, WebSocket } from 'ws';
3
4
  import * as fs6 from 'fs/promises';
4
5
  import { writeFileSync } from 'fs';
5
6
  import { createRequire } from 'module';
@@ -34,6 +35,506 @@ var init_plugin_api_factory = __esm({
34
35
  "src/plugin-api-factory.ts"() {
35
36
  }
36
37
  });
38
+
39
+ // src/webui-server.ts
40
+ var webui_server_exports = {};
41
+ __export(webui_server_exports, {
42
+ runWebUI: () => runWebUI
43
+ });
44
+ async function runWebUI(opts) {
45
+ const port = opts.port ?? 3457;
46
+ const clients = /* @__PURE__ */ new Map();
47
+ let abortController = null;
48
+ const wss = new WebSocketServer({ port });
49
+ console.log(`[WebUI] WebSocket server starting on ws://localhost:${port}`);
50
+ const eventUnsubscribers = [];
51
+ function setupEvents() {
52
+ for (const unsub of eventUnsubscribers) unsub();
53
+ eventUnsubscribers.length = 0;
54
+ eventUnsubscribers.push(
55
+ opts.events.on("iteration.started", (e) => {
56
+ broadcast({
57
+ type: "iteration.started",
58
+ payload: { index: e.index }
59
+ });
60
+ })
61
+ );
62
+ eventUnsubscribers.push(
63
+ opts.events.on("provider.text_delta", (e) => {
64
+ broadcast({
65
+ type: "provider.text_delta",
66
+ payload: { text: e.text, messageId: "current" }
67
+ });
68
+ })
69
+ );
70
+ eventUnsubscribers.push(
71
+ opts.events.on("tool.started", (e) => {
72
+ broadcast({
73
+ type: "tool.started",
74
+ payload: {
75
+ id: e.id,
76
+ name: e.name,
77
+ input: e.input,
78
+ messageId: `tool_${e.id}`
79
+ }
80
+ });
81
+ })
82
+ );
83
+ eventUnsubscribers.push(
84
+ opts.events.on("tool.progress", (e) => {
85
+ broadcast({
86
+ type: "tool.progress",
87
+ payload: {
88
+ name: e.name,
89
+ id: e.id,
90
+ event: e.event
91
+ }
92
+ });
93
+ })
94
+ );
95
+ eventUnsubscribers.push(
96
+ opts.events.on("tool.executed", (e) => {
97
+ broadcast({
98
+ type: "tool.executed",
99
+ payload: {
100
+ // Forward the tool_use id so the WebUI can correlate this with
101
+ // the matching tool.started bubble for parallel tool calls.
102
+ id: e.id,
103
+ name: e.name,
104
+ durationMs: e.durationMs,
105
+ ok: e.ok,
106
+ input: e.input,
107
+ output: e.output
108
+ }
109
+ });
110
+ })
111
+ );
112
+ eventUnsubscribers.push(
113
+ opts.events.on("provider.response", (e) => {
114
+ broadcast({
115
+ type: "provider.response",
116
+ payload: {
117
+ usage: e.usage,
118
+ stopReason: e.stopReason,
119
+ messageId: "current"
120
+ }
121
+ });
122
+ })
123
+ );
124
+ eventUnsubscribers.push(
125
+ opts.events.on("error", (e) => {
126
+ broadcast({
127
+ type: "error",
128
+ payload: {
129
+ phase: e.phase,
130
+ message: e.err instanceof Error ? e.err.message : String(e.err)
131
+ }
132
+ });
133
+ })
134
+ );
135
+ }
136
+ return new Promise((resolve3) => {
137
+ wss.on("listening", () => {
138
+ console.log(`[WebUI] WebSocket server running on ws://localhost:${port}`);
139
+ setupEvents();
140
+ });
141
+ wss.on("connection", (ws) => {
142
+ const client = { ws, sessionId: opts.session.id };
143
+ clients.set(ws, client);
144
+ console.log("[WebUI] Client connected");
145
+ ws.on("message", async (data) => {
146
+ try {
147
+ const msg = JSON.parse(data.toString());
148
+ await handleMessage(ws, client, msg);
149
+ } catch (err) {
150
+ console.error("[WebUI] Failed to parse message", err);
151
+ }
152
+ });
153
+ ws.on("close", () => {
154
+ console.log("[WebUI] Client disconnected");
155
+ clients.delete(ws);
156
+ });
157
+ send(ws, {
158
+ type: "session.start",
159
+ payload: {
160
+ sessionId: opts.session.id,
161
+ model: opts.agent.ctx.model,
162
+ provider: opts.agent.ctx.provider.id
163
+ }
164
+ });
165
+ });
166
+ wss.on("error", (err) => {
167
+ console.error("[WebUI] Server error:", err);
168
+ });
169
+ function shutdown() {
170
+ console.log("[WebUI] Shutting down...");
171
+ for (const unsub of eventUnsubscribers) unsub();
172
+ for (const [ws] of clients) {
173
+ ws.close();
174
+ }
175
+ clients.clear();
176
+ wss.close(() => {
177
+ console.log("[WebUI] Server stopped");
178
+ resolve3();
179
+ });
180
+ }
181
+ process.on("SIGINT", shutdown);
182
+ process.on("SIGTERM", shutdown);
183
+ });
184
+ async function handleMessage(ws, client, msg) {
185
+ switch (msg.type) {
186
+ case "user_message":
187
+ await handleUserMessage(ws, client, msg.payload.content);
188
+ break;
189
+ case "abort":
190
+ abortController?.abort();
191
+ broadcast({
192
+ type: "error",
193
+ payload: { phase: "abort", message: "User aborted" }
194
+ });
195
+ break;
196
+ case "ping":
197
+ send(ws, { type: "pong", payload: {} });
198
+ break;
199
+ case "providers.list":
200
+ await handleProvidersList(ws);
201
+ break;
202
+ case "provider.models":
203
+ await handleProviderModels(ws, msg.payload.providerId);
204
+ break;
205
+ case "providers.saved":
206
+ await handleProvidersSaved(ws);
207
+ break;
208
+ case "key.add":
209
+ case "key.update": {
210
+ const m = msg;
211
+ await handleKeyUpsert(ws, m.payload.providerId, m.payload.label, m.payload.apiKey);
212
+ break;
213
+ }
214
+ case "key.delete": {
215
+ const m = msg;
216
+ await handleKeyDelete(ws, m.payload.providerId, m.payload.label);
217
+ break;
218
+ }
219
+ case "key.set_active": {
220
+ const m = msg;
221
+ await handleKeySetActive(ws, m.payload.providerId, m.payload.label);
222
+ break;
223
+ }
224
+ case "provider.add": {
225
+ const m = msg;
226
+ await handleProviderAdd(ws, m.payload);
227
+ break;
228
+ }
229
+ case "provider.remove": {
230
+ const m = msg;
231
+ await handleProviderRemove(ws, m.payload.providerId);
232
+ break;
233
+ }
234
+ }
235
+ }
236
+ async function handleUserMessage(ws, client, content) {
237
+ abortController?.abort();
238
+ abortController = new AbortController();
239
+ try {
240
+ const result = await opts.agent.run(content, {
241
+ signal: abortController.signal
242
+ });
243
+ send(ws, {
244
+ type: "run.result",
245
+ payload: {
246
+ status: result.status,
247
+ iterations: result.iterations,
248
+ finalText: result.finalText,
249
+ error: result.error ? {
250
+ code: result.error.code,
251
+ message: result.error.message,
252
+ recoverable: result.error.recoverable
253
+ } : void 0
254
+ }
255
+ });
256
+ } catch (err) {
257
+ send(ws, {
258
+ type: "error",
259
+ payload: {
260
+ phase: "agent.run",
261
+ message: err instanceof Error ? err.message : String(err)
262
+ }
263
+ });
264
+ } finally {
265
+ abortController = null;
266
+ }
267
+ }
268
+ function send(ws, msg) {
269
+ if (ws.readyState === WebSocket.OPEN) {
270
+ ws.send(JSON.stringify(msg));
271
+ }
272
+ }
273
+ function broadcast(msg) {
274
+ const data = JSON.stringify(msg);
275
+ for (const [ws] of clients) {
276
+ if (ws.readyState === WebSocket.OPEN) {
277
+ ws.send(data);
278
+ }
279
+ }
280
+ }
281
+ async function handleProvidersList(ws) {
282
+ if (!opts.modelsRegistry) {
283
+ sendResult(ws, false, "Models registry not available");
284
+ return;
285
+ }
286
+ try {
287
+ const providers = await opts.modelsRegistry.listProviders();
288
+ const savedProviders = await loadSavedProviders();
289
+ const savedIds = new Set(Object.keys(savedProviders));
290
+ send(ws, {
291
+ type: "provider.catalog",
292
+ payload: {
293
+ providers: providers.map((p) => ({
294
+ id: p.id,
295
+ name: p.name,
296
+ family: p.family,
297
+ apiBase: p.apiBase,
298
+ envVars: p.envVars,
299
+ modelCount: p.models.length,
300
+ hasApiKey: savedIds.has(p.id) || p.envVars.some((v) => !!process.env[v])
301
+ }))
302
+ }
303
+ });
304
+ } catch (err) {
305
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
306
+ }
307
+ }
308
+ async function handleProviderModels(ws, providerId) {
309
+ if (!opts.modelsRegistry) {
310
+ sendResult(ws, false, "Models registry not available");
311
+ return;
312
+ }
313
+ try {
314
+ const provider = await opts.modelsRegistry.getProvider(providerId);
315
+ if (!provider) {
316
+ sendResult(ws, false, `Provider "${providerId}" not found in catalog`);
317
+ return;
318
+ }
319
+ send(ws, {
320
+ type: "provider.models",
321
+ payload: {
322
+ provider: providerId,
323
+ models: provider.models.map((m) => ({
324
+ id: m.id,
325
+ name: m.name,
326
+ releaseDate: m.release_date,
327
+ contextWindow: m.limit?.context,
328
+ inputCost: m.cost?.input,
329
+ outputCost: m.cost?.output,
330
+ capabilities: [
331
+ ...m.tool_call ? ["tools"] : [],
332
+ ...m.reasoning ? ["reasoning"] : [],
333
+ ...m.modalities?.input?.includes("image") ? ["vision"] : [],
334
+ ...m.open_weights ? ["open_weights"] : []
335
+ ]
336
+ }))
337
+ }
338
+ });
339
+ } catch (err) {
340
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
341
+ }
342
+ }
343
+ async function handleProvidersSaved(ws) {
344
+ try {
345
+ const providers = await loadSavedProviders();
346
+ send(ws, {
347
+ type: "providers.saved",
348
+ payload: {
349
+ providers: Object.entries(providers).map(([id, cfg]) => ({
350
+ id,
351
+ family: cfg.family,
352
+ baseUrl: cfg.baseUrl,
353
+ apiKeys: normalizeKeys2(cfg).map((k) => ({
354
+ label: k.label,
355
+ maskedKey: maskedKey2(k.apiKey),
356
+ isActive: k.label === cfg.activeKey,
357
+ createdAt: k.createdAt
358
+ }))
359
+ }))
360
+ }
361
+ });
362
+ } catch (err) {
363
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
364
+ }
365
+ }
366
+ async function handleKeyUpsert(ws, providerId, label, apiKey) {
367
+ try {
368
+ const providers = await loadSavedProviders();
369
+ const existing = providers[providerId] ?? { type: providerId };
370
+ const keys = normalizeKeys2(existing);
371
+ const existingIdx = keys.findIndex((k) => k.label === label);
372
+ if (existingIdx >= 0) {
373
+ keys[existingIdx] = { ...keys[existingIdx], apiKey, createdAt: nowIso2() };
374
+ } else {
375
+ keys.push({ label, apiKey, createdAt: nowIso2() });
376
+ }
377
+ writeKeysBack2(existing, keys);
378
+ if (!existing.activeKey) existing.activeKey = label;
379
+ providers[providerId] = existing;
380
+ await saveProviders(providers);
381
+ sendResult(ws, true, `Key "${label}" saved for ${providerId}`);
382
+ } catch (err) {
383
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
384
+ }
385
+ }
386
+ async function handleKeyDelete(ws, providerId, label) {
387
+ try {
388
+ const providers = await loadSavedProviders();
389
+ const existing = providers[providerId];
390
+ if (!existing) {
391
+ sendResult(ws, false, `Provider "${providerId}" not found`);
392
+ return;
393
+ }
394
+ const keys = normalizeKeys2(existing).filter((k) => k.label !== label);
395
+ if (keys.length === 0) {
396
+ delete providers[providerId];
397
+ } else {
398
+ writeKeysBack2(existing, keys);
399
+ if (existing.activeKey === label) {
400
+ existing.activeKey = keys[0].label;
401
+ }
402
+ providers[providerId] = existing;
403
+ }
404
+ await saveProviders(providers);
405
+ sendResult(ws, true, `Key "${label}" deleted from ${providerId}`);
406
+ } catch (err) {
407
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
408
+ }
409
+ }
410
+ async function handleKeySetActive(ws, providerId, label) {
411
+ try {
412
+ const providers = await loadSavedProviders();
413
+ const existing = providers[providerId];
414
+ if (!existing) {
415
+ sendResult(ws, false, `Provider "${providerId}" not found`);
416
+ return;
417
+ }
418
+ existing.activeKey = label;
419
+ writeKeysBack2(existing, normalizeKeys2(existing));
420
+ providers[providerId] = existing;
421
+ await saveProviders(providers);
422
+ sendResult(ws, true, `Active key for ${providerId} set to "${label}"`);
423
+ } catch (err) {
424
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
425
+ }
426
+ }
427
+ async function handleProviderAdd(ws, payload) {
428
+ try {
429
+ const providers = await loadSavedProviders();
430
+ if (providers[payload.id]) {
431
+ sendResult(ws, false, `Provider "${payload.id}" already exists. Use key.add to add a key.`);
432
+ return;
433
+ }
434
+ const newProv = {
435
+ type: payload.id,
436
+ family: payload.family,
437
+ baseUrl: payload.baseUrl
438
+ };
439
+ if (payload.apiKey) {
440
+ newProv.apiKeys = [{ label: "default", apiKey: payload.apiKey, createdAt: nowIso2() }];
441
+ newProv.activeKey = "default";
442
+ }
443
+ providers[payload.id] = newProv;
444
+ await saveProviders(providers);
445
+ sendResult(ws, true, `Provider "${payload.id}" added`);
446
+ } catch (err) {
447
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
448
+ }
449
+ }
450
+ async function handleProviderRemove(ws, providerId) {
451
+ try {
452
+ const providers = await loadSavedProviders();
453
+ if (!providers[providerId]) {
454
+ sendResult(ws, false, `Provider "${providerId}" not found`);
455
+ return;
456
+ }
457
+ delete providers[providerId];
458
+ await saveProviders(providers);
459
+ sendResult(ws, true, `Provider "${providerId}" removed`);
460
+ } catch (err) {
461
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
462
+ }
463
+ }
464
+ async function loadSavedProviders() {
465
+ if (!opts.globalConfigPath) return {};
466
+ let raw;
467
+ try {
468
+ raw = await fs6.readFile(opts.globalConfigPath, "utf8");
469
+ } catch {
470
+ return {};
471
+ }
472
+ let parsed = {};
473
+ try {
474
+ parsed = JSON.parse(raw);
475
+ } catch {
476
+ return {};
477
+ }
478
+ return parsed.providers ?? {};
479
+ }
480
+ async function saveProviders(providers) {
481
+ if (!opts.globalConfigPath) return;
482
+ let raw;
483
+ try {
484
+ raw = await fs6.readFile(opts.globalConfigPath, "utf8");
485
+ } catch {
486
+ raw = "{}";
487
+ }
488
+ let parsed;
489
+ try {
490
+ parsed = JSON.parse(raw);
491
+ } catch {
492
+ parsed = {};
493
+ }
494
+ parsed.providers = providers;
495
+ await atomicWrite(opts.globalConfigPath, JSON.stringify(parsed, null, 2), { mode: 384 });
496
+ }
497
+ function normalizeKeys2(cfg) {
498
+ if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
499
+ return cfg.apiKeys.map((k) => ({ ...k }));
500
+ }
501
+ if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
502
+ return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
503
+ }
504
+ return [];
505
+ }
506
+ function writeKeysBack2(cfg, keys) {
507
+ if (keys.length === 0) {
508
+ delete cfg.apiKeys;
509
+ delete cfg.apiKey;
510
+ delete cfg.activeKey;
511
+ return;
512
+ }
513
+ cfg.apiKeys = keys;
514
+ const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
515
+ cfg.apiKey = active.apiKey;
516
+ if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
517
+ cfg.activeKey = active.label;
518
+ }
519
+ }
520
+ function maskedKey2(key) {
521
+ if (!key) return "\u2014";
522
+ if (key.length <= 8) return "\u2022".repeat(key.length);
523
+ const head = key.slice(0, 4);
524
+ const tail = key.slice(-4);
525
+ return `${head}\u2026${tail}`;
526
+ }
527
+ function nowIso2() {
528
+ return (/* @__PURE__ */ new Date()).toISOString();
529
+ }
530
+ function sendResult(ws, success, message) {
531
+ send(ws, { type: "key.operation_result", payload: { success, message } });
532
+ }
533
+ }
534
+ var init_webui_server = __esm({
535
+ "src/webui-server.ts"() {
536
+ }
537
+ });
37
538
  var ReadlineInputReader = class {
38
539
  rl;
39
540
  historyFile;
@@ -516,17 +1017,17 @@ async function resolveModelSelection(answer, models, provider, _registry, render
516
1017
  var theme2 = { primary: color.amber };
517
1018
  async function saveToGlobalConfig(configPath, provider, model) {
518
1019
  try {
519
- const { atomicWrite: atomicWrite3 } = await import('@wrongstack/core');
520
- const fs8 = await import('fs/promises');
1020
+ const { atomicWrite: atomicWrite4 } = await import('@wrongstack/core');
1021
+ const fs9 = await import('fs/promises');
521
1022
  let existing = {};
522
1023
  try {
523
- const raw = await fs8.readFile(configPath, "utf8");
1024
+ const raw = await fs9.readFile(configPath, "utf8");
524
1025
  existing = JSON.parse(raw);
525
1026
  } catch {
526
1027
  }
527
1028
  existing.provider = provider;
528
1029
  existing.model = model;
529
- await atomicWrite3(configPath, JSON.stringify(existing, null, 2));
1030
+ await atomicWrite4(configPath, JSON.stringify(existing, null, 2));
530
1031
  return true;
531
1032
  } catch {
532
1033
  return false;
@@ -548,6 +1049,7 @@ function buildBuiltinSlashCommands(opts) {
548
1049
  metricsCommand(opts),
549
1050
  healthCommand(opts),
550
1051
  memoryCommand(opts),
1052
+ todosCommand(opts),
551
1053
  saveCommand(opts),
552
1054
  loadCommand(opts),
553
1055
  exitCommand(opts)
@@ -597,6 +1099,68 @@ function memoryCommand(opts) {
597
1099
  }
598
1100
  };
599
1101
  }
1102
+ function todosCommand(opts) {
1103
+ return {
1104
+ name: "todos",
1105
+ description: "Inspect or edit the live todo list: /todos [show|clear|add <text>|done <id|index>]",
1106
+ async run(args) {
1107
+ const ctx = opts.context;
1108
+ if (!ctx) return { message: "No active context." };
1109
+ const [verb, ...rest] = args.trim().split(/\s+/);
1110
+ const restJoined = rest.join(" ").trim();
1111
+ switch (verb) {
1112
+ case "":
1113
+ case "show":
1114
+ case "list": {
1115
+ const todos = ctx.todos;
1116
+ if (todos.length === 0) {
1117
+ return { message: "No todos. The agent will add some when it plans work." };
1118
+ }
1119
+ const lines = [];
1120
+ const done = todos.filter((t) => t.status === "completed").length;
1121
+ lines.push(color.dim(`Todos (${done}/${todos.length} done):`));
1122
+ todos.forEach((t, i) => {
1123
+ const mark = t.status === "completed" ? color.green("[x]") : t.status === "in_progress" ? color.yellow("[~]") : color.dim("[ ]");
1124
+ const text = t.status === "in_progress" && t.activeForm ? t.activeForm : t.content;
1125
+ const label = t.status === "completed" ? color.dim(text) : text;
1126
+ lines.push(` ${color.dim(String(i + 1).padStart(2))}. ${mark} ${label}`);
1127
+ });
1128
+ return { message: lines.join("\n") };
1129
+ }
1130
+ case "clear": {
1131
+ const n = ctx.todos.length;
1132
+ ctx.todos.length = 0;
1133
+ return { message: n === 0 ? "Todos were already empty." : `Cleared ${n} todo${n === 1 ? "" : "s"}.` };
1134
+ }
1135
+ case "add": {
1136
+ if (!restJoined) return { message: "Usage: /todos add <text>" };
1137
+ ctx.todos.push({
1138
+ id: `todo_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
1139
+ content: restJoined,
1140
+ status: "pending"
1141
+ });
1142
+ return { message: `Added: ${restJoined}` };
1143
+ }
1144
+ case "done":
1145
+ case "complete": {
1146
+ if (!restJoined) return { message: "Usage: /todos done <id|index>" };
1147
+ const asIndex = Number.parseInt(restJoined, 10);
1148
+ let target = !Number.isNaN(asIndex) ? ctx.todos[asIndex - 1] : ctx.todos.find((t) => t.id === restJoined);
1149
+ if (!target) {
1150
+ target = ctx.todos.find((t) => t.content.toLowerCase().includes(restJoined.toLowerCase()));
1151
+ }
1152
+ if (!target) return { message: `No todo matched "${restJoined}".` };
1153
+ target.status = "completed";
1154
+ return { message: `Marked done: ${target.content}` };
1155
+ }
1156
+ default:
1157
+ return {
1158
+ message: `Unknown subcommand "${verb}". Try: show | clear | add <text> | done <id|index>`
1159
+ };
1160
+ }
1161
+ }
1162
+ };
1163
+ }
600
1164
  function initCommand(opts) {
601
1165
  return {
602
1166
  name: "init",
@@ -810,11 +1374,11 @@ function clearCommand(opts) {
810
1374
  ].join("\n"),
811
1375
  async run(_args, ctx) {
812
1376
  if (ctx) {
813
- ctx.messages = [];
814
- ctx.todos = [];
1377
+ ctx.state.replaceMessages([]);
1378
+ ctx.state.replaceTodos([]);
815
1379
  ctx.readFiles.clear();
816
1380
  ctx.fileMtimes.clear();
817
- ctx.meta = {};
1381
+ for (const key of Object.keys(ctx.meta)) ctx.state.deleteMeta(key);
818
1382
  }
819
1383
  await opts.memoryStore?.clear();
820
1384
  opts.onClear?.();
@@ -960,21 +1524,25 @@ ${lines.join("\n")}
960
1524
  function skillCommand(opts) {
961
1525
  return {
962
1526
  name: "skill",
963
- description: "Show a skill manifest or list skills.",
1527
+ description: "Show skill details or list available skills.",
964
1528
  async run(args) {
965
1529
  if (!opts.skillLoader) {
966
1530
  const msg = "No skill loader configured.";
967
1531
  return { message: msg };
968
1532
  }
969
1533
  if (!args.trim()) {
970
- const list = await opts.skillLoader.list();
971
- if (list.length === 0) {
1534
+ const entries = await opts.skillLoader.listEntries();
1535
+ if (entries.length === 0) {
972
1536
  const msg2 = "No skills found.";
973
1537
  return { message: msg2 };
974
1538
  }
975
- const lines = list.map((s) => ` ${s.name.padEnd(24)} ${color.dim(`[${s.source}]`)} ${s.description.split("\n")[0]}`);
976
- const msg = `Skills:
977
- ${lines.join("\n")}
1539
+ const lines = entries.map((e) => {
1540
+ const scopeTag = e.scope.length > 0 ? ` ${color.dim(`(${e.scope.slice(0, 3).join(", ")})`)}` : "";
1541
+ return ` ${color.bold(e.name)}${scopeTag}
1542
+ Use when: ${e.trigger}`;
1543
+ });
1544
+ const msg = `Available skills:
1545
+ ${lines.join("\n\n")}
978
1546
  `;
979
1547
  return { message: msg };
980
1548
  }
@@ -1498,7 +2066,7 @@ function patchConfig(base, patch) {
1498
2066
 
1499
2067
  // src/repl.ts
1500
2068
  async function runRepl(opts) {
1501
- if (opts.banner !== false) printBanner(opts.renderer);
2069
+ if (opts.banner !== false) printBanner(opts.renderer, opts.projectName);
1502
2070
  let activeCtrl;
1503
2071
  let interrupts = 0;
1504
2072
  const onSigint = () => {
@@ -1516,78 +2084,82 @@ async function runRepl(opts) {
1516
2084
  };
1517
2085
  process.on("SIGINT", onSigint);
1518
2086
  const builder = new InputBuilder({ store: opts.attachments });
1519
- for (; ; ) {
1520
- let raw;
1521
- try {
1522
- raw = await readPossiblyMultiline(opts);
1523
- } catch {
1524
- break;
1525
- }
1526
- const trimmed = raw.trim();
1527
- if (!trimmed) {
1528
- interrupts = 0;
1529
- continue;
1530
- }
1531
- interrupts = 0;
1532
- if (trimmed.startsWith("/")) {
2087
+ try {
2088
+ for (; ; ) {
2089
+ let raw;
1533
2090
  try {
1534
- const res = await opts.slashRegistry.dispatch(trimmed, opts.agent.ctx);
1535
- if (res?.message) opts.renderer.write(`${res.message}
2091
+ raw = await readPossiblyMultiline(opts);
2092
+ } catch {
2093
+ break;
2094
+ }
2095
+ const trimmed = raw.trim();
2096
+ if (!trimmed) {
2097
+ interrupts = 0;
2098
+ continue;
2099
+ }
2100
+ interrupts = 0;
2101
+ if (trimmed.startsWith("/")) {
2102
+ try {
2103
+ const res = await opts.slashRegistry.dispatch(trimmed, opts.agent.ctx);
2104
+ if (res?.message) opts.renderer.write(`${res.message}
1536
2105
  `);
1537
- if (res?.exit) break;
1538
- } catch (err) {
1539
- opts.renderer.writeError(err instanceof Error ? err.message : String(err));
2106
+ if (res?.exit) break;
2107
+ } catch (err) {
2108
+ opts.renderer.writeError(err instanceof Error ? err.message : String(err));
2109
+ }
2110
+ continue;
1540
2111
  }
1541
- continue;
1542
- }
1543
- const ph = await builder.appendPaste(raw);
1544
- if (ph) {
1545
- const lineCount = raw.split("\n").length;
1546
- opts.renderer.write(color.dim(` \u21B3 ${ph} (${lineCount} lines)
2112
+ const ph = await builder.appendPaste(raw);
2113
+ if (ph) {
2114
+ const lineCount = raw.split("\n").length;
2115
+ opts.renderer.write(color.dim(` \u21B3 ${ph} (${lineCount} lines)
1547
2116
  `));
1548
- }
1549
- const blocks = await builder.submit();
1550
- const runCtrl = new AbortController();
1551
- activeCtrl = runCtrl;
1552
- try {
1553
- const startedAt = Date.now();
1554
- const before = opts.tokenCounter?.total();
1555
- const costBefore = opts.tokenCounter?.estimateCost().total ?? 0;
1556
- const result = await opts.agent.run(blocks, { signal: runCtrl.signal });
1557
- if (result.status === "aborted") {
1558
- opts.renderer.writeWarning("Aborted.");
1559
- } else if (result.status === "failed") {
1560
- const err = result.error;
1561
- if (err) {
1562
- const tag = err.recoverable ? " (recoverable)" : "";
1563
- opts.renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
1564
- } else {
1565
- opts.renderer.writeError("Failed.");
1566
- }
1567
- } else if (result.status === "max_iterations") {
1568
- opts.renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
1569
2117
  }
1570
- if (opts.tokenCounter && before) {
1571
- const after = opts.tokenCounter.total();
1572
- const costAfter = opts.tokenCounter.estimateCost().total;
1573
- const ctxChip = opts.effectiveMaxContext && opts.effectiveMaxContext > 0 ? ` ctx: ${renderContextChip(after.input, opts.effectiveMaxContext)}` : "";
1574
- opts.renderer.write(
1575
- `
2118
+ const blocks = await builder.submit();
2119
+ const runCtrl = new AbortController();
2120
+ activeCtrl = runCtrl;
2121
+ try {
2122
+ const startedAt = Date.now();
2123
+ const before = opts.tokenCounter?.total();
2124
+ const costBefore = opts.tokenCounter?.estimateCost().total ?? 0;
2125
+ const result = await opts.agent.run(blocks, { signal: runCtrl.signal });
2126
+ if (result.status === "aborted") {
2127
+ opts.renderer.writeWarning("Aborted.");
2128
+ } else if (result.status === "failed") {
2129
+ const err = result.error;
2130
+ if (err) {
2131
+ const tag = err.recoverable ? " (recoverable)" : "";
2132
+ opts.renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
2133
+ } else {
2134
+ opts.renderer.writeError("Failed.");
2135
+ }
2136
+ } else if (result.status === "max_iterations") {
2137
+ opts.renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
2138
+ }
2139
+ if (opts.tokenCounter && before) {
2140
+ const after = opts.tokenCounter.total();
2141
+ const costAfter = opts.tokenCounter.estimateCost().total;
2142
+ const ctxChip = opts.effectiveMaxContext && opts.effectiveMaxContext > 0 ? ` ctx: ${renderContextChip(after.input, opts.effectiveMaxContext)}` : "";
2143
+ opts.renderer.write(
2144
+ `
1576
2145
  ${color.dim(
1577
- `[in: ${fmtTok(after.input - before.input)} out: ${fmtTok(after.output - before.output)} iters: ${result.iterations} cost: ${(costAfter - costBefore).toFixed(4)} ${((Date.now() - startedAt) / 1e3).toFixed(1)}s]${ctxChip}`
1578
- )}
2146
+ `[in: ${fmtTok(after.input - before.input)} out: ${fmtTok(after.output - before.output)} iters: ${result.iterations} cost: ${(costAfter - costBefore).toFixed(4)} ${((Date.now() - startedAt) / 1e3).toFixed(1)}s]${ctxChip}`
2147
+ )}
1579
2148
  `
1580
- );
2149
+ );
2150
+ }
2151
+ } catch (err) {
2152
+ opts.renderer.writeError(err instanceof Error ? err.message : String(err));
2153
+ } finally {
2154
+ activeCtrl = void 0;
1581
2155
  }
1582
- } catch (err) {
1583
- opts.renderer.writeError(err instanceof Error ? err.message : String(err));
1584
- } finally {
1585
- activeCtrl = void 0;
1586
2156
  }
2157
+ return 0;
2158
+ } finally {
2159
+ process.off("SIGINT", onSigint);
2160
+ await opts.reader.close().catch(() => {
2161
+ });
1587
2162
  }
1588
- process.off("SIGINT", onSigint);
1589
- await opts.reader.close();
1590
- return 0;
1591
2163
  }
1592
2164
  async function readPossiblyMultiline(opts) {
1593
2165
  const firstPrompt = theme.primary("\u203A ");
@@ -1624,13 +2196,15 @@ function renderProgress(ratio, width) {
1624
2196
  const capped = Math.min(width, filled);
1625
2197
  return FILLED.repeat(capped) + EMPTY.repeat(width - capped);
1626
2198
  }
1627
- function printBanner(renderer) {
2199
+ function printBanner(renderer, projectName) {
1628
2200
  const lines = [
1629
2201
  theme.primary(theme.bold("WrongStack")) + color.dim(` v${CLI_VERSION}`),
1630
- color.dim("Built on the wrong stack. Shipped anyway."),
1631
- color.dim("Type /help for commands, /exit to quit."),
1632
- ""
2202
+ color.dim("Built on the wrong stack. Shipped anyway.")
1633
2203
  ];
2204
+ if (projectName && projectName.length > 0) {
2205
+ lines.push(color.dim("Project: ") + theme.bold(projectName));
2206
+ }
2207
+ lines.push(color.dim("Type /help for commands, /exit to quit."), "");
1634
2208
  renderer.write(`${lines.join("\n")}
1635
2209
  `);
1636
2210
  }
@@ -1929,6 +2503,11 @@ var MultiAgentHost = class {
1929
2503
  systemPrompt: baseSystem,
1930
2504
  provider,
1931
2505
  session: subSession,
2506
+ // Placeholder — Agent.run() overwrites ctx.signal with the live
2507
+ // per-run signal (see core/agent.ts run()). Tools/middleware that
2508
+ // read ctx.signal after construction will see the runtime signal,
2509
+ // not this one. Kept as `new AbortController().signal` so the
2510
+ // initial value is non-null/non-aborted.
1932
2511
  signal: new AbortController().signal,
1933
2512
  tokenCounter: this.deps.tokenCounter,
1934
2513
  cwd: this.deps.cwd,
@@ -3418,7 +3997,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
3418
3997
  "alt-screen",
3419
3998
  "output-json",
3420
3999
  "prompt",
3421
- "metrics"
4000
+ "metrics",
4001
+ "webui"
3422
4002
  ]);
3423
4003
  function parseArgs(argv) {
3424
4004
  const flags = {};
@@ -3618,6 +4198,8 @@ async function main(argv) {
3618
4198
  TOKENS.TokenCounter,
3619
4199
  () => new DefaultTokenCounter({ registry: modelsRegistry, providerId: config.provider })
3620
4200
  );
4201
+ const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
4202
+ container.bind(TOKENS.ModeStore, () => modeStore);
3621
4203
  container.bind(
3622
4204
  TOKENS.SessionStore,
3623
4205
  () => new DefaultSessionStore({ dir: wpaths.projectSessions })
@@ -3629,11 +4211,25 @@ async function main(argv) {
3629
4211
  bundledDir: config.features.skills ? resolveBundledSkillsDir() : void 0
3630
4212
  });
3631
4213
  container.bind(TOKENS.SkillLoader, () => skillLoader);
4214
+ const activeMode = await modeStore.getActiveMode();
4215
+ const modeId = activeMode?.id ?? "default";
4216
+ const modePrompt = activeMode?.prompt ?? "";
4217
+ const resolvedModel = await modelsRegistry.getModel(config.provider, config.model);
4218
+ const modelCapabilities = resolvedModel?.capabilities ? {
4219
+ maxContextTokens: resolvedModel.capabilities.maxContext,
4220
+ supportsTools: resolvedModel.capabilities.tools,
4221
+ supportsVision: resolvedModel.capabilities.vision,
4222
+ supportsReasoning: resolvedModel.capabilities.reasoning
4223
+ } : void 0;
3632
4224
  container.bind(
3633
4225
  TOKENS.SystemPromptBuilder,
3634
4226
  () => new DefaultSystemPromptBuilder({
3635
4227
  memoryStore,
3636
- skillLoader: config.features.skills ? skillLoader : void 0
4228
+ skillLoader: config.features.skills ? skillLoader : void 0,
4229
+ modeStore,
4230
+ modeId,
4231
+ modePrompt,
4232
+ modelCapabilities
3637
4233
  })
3638
4234
  );
3639
4235
  container.bind(TOKENS.Renderer, () => renderer);
@@ -3897,7 +4493,7 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
3897
4493
  model: config.model
3898
4494
  });
3899
4495
  if (restoredMessages.length > 0) {
3900
- context.messages.push(...restoredMessages);
4496
+ context.state.replaceMessages(restoredMessages);
3901
4497
  }
3902
4498
  const pipelines = createDefaultPipelines();
3903
4499
  const installBoundary = (p) => {
@@ -3957,7 +4553,11 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
3957
4553
  soft: config.context.softThreshold,
3958
4554
  hard: config.context.hardThreshold
3959
4555
  },
3960
- "soft"
4556
+ {
4557
+ aggressiveOn: "soft",
4558
+ failureMode: "throw_on_hard",
4559
+ events
4560
+ }
3961
4561
  );
3962
4562
  pipelines.contextWindow.use({
3963
4563
  name: "AutoCompaction",
@@ -4255,11 +4855,36 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4255
4855
  process.stdout.write(
4256
4856
  color.dim(`Session saved: ${session.id} \u2014 resume with `) + color.cyan(`wstack resume ${session.id}`) + "\n"
4257
4857
  );
4858
+ },
4859
+ onClearHistory: (dispatch) => {
4860
+ dispatch({ type: "clearHistory" });
4861
+ dispatch({ type: "resetContextChip" });
4258
4862
  }
4259
4863
  });
4260
4864
  } finally {
4261
4865
  renderer.setSilent(false);
4262
4866
  }
4867
+ } else if (flags.webui) {
4868
+ const { runWebUI: runWebUI2 } = await Promise.resolve().then(() => (init_webui_server(), webui_server_exports));
4869
+ const webuiPromise = runWebUI2({
4870
+ agent,
4871
+ events,
4872
+ session,
4873
+ port: Number.parseInt(String(flags.port ?? "3457"), 10),
4874
+ modelsRegistry,
4875
+ globalConfigPath: wpaths.globalConfig
4876
+ });
4877
+ code = await runRepl({
4878
+ agent,
4879
+ renderer,
4880
+ reader,
4881
+ slashRegistry,
4882
+ tokenCounter,
4883
+ attachments,
4884
+ effectiveMaxContext,
4885
+ projectName: path5.basename(projectRoot) || void 0
4886
+ });
4887
+ await webuiPromise;
4263
4888
  } else {
4264
4889
  code = await runRepl({
4265
4890
  agent,
@@ -4268,7 +4893,8 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4268
4893
  slashRegistry,
4269
4894
  tokenCounter,
4270
4895
  attachments,
4271
- effectiveMaxContext
4896
+ effectiveMaxContext,
4897
+ projectName: path5.basename(projectRoot) || void 0
4272
4898
  });
4273
4899
  }
4274
4900
  } finally {