@wrongstack/cli 0.3.3 → 0.3.4

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,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { color, allServers, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, SlashCommandRegistry, loadPlugins, createDelegateTool, FLEET_ROSTER, DefaultLogger, DefaultModelsRegistry, DefaultSessionStore, DefaultSkillLoader, ProviderRegistry, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, Agent, makeDirectorSessionFactory, Director, DefaultMultiAgentCoordinator, makeAgentSubagentRunner, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, atomicWrite, AutoApprovePermissionPolicy, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, removePlanItem, formatPlan, setPlanItemStatus, addPlanItem, InputBuilder, decryptConfigSecrets, encryptConfigSecrets, DefaultPluginAPI } from '@wrongstack/core';
3
- import * as fs3 from 'fs/promises';
3
+ import * as crypto from 'crypto';
4
+ import { randomUUID } from 'crypto';
5
+ import * as fs14 from 'fs/promises';
4
6
  import { WebSocketServer, WebSocket } from 'ws';
5
7
  import { writeFileSync } from 'fs';
6
8
  import { createRequire } from 'module';
@@ -8,11 +10,9 @@ import * as path14 from 'path';
8
10
  import { MCPRegistry } from '@wrongstack/mcp';
9
11
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig, capabilitiesFor } from '@wrongstack/providers';
10
12
  import { createDefaultContainer, routeImagesForModel, readClipboardImage } from '@wrongstack/runtime';
11
- import { rememberTool, forgetTool } from '@wrongstack/tools';
12
- import { builtinToolsPack } from '@wrongstack/tools/pack';
13
+ import { builtinToolsPack, rememberTool, forgetTool } from '@wrongstack/tools';
13
14
  import * as os3 from 'os';
14
15
  import * as readline from 'readline';
15
- import { randomUUID } from 'crypto';
16
16
  import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
17
17
 
18
18
  var __defProp = Object.defineProperty;
@@ -75,8 +75,10 @@ async function runWebUI(opts) {
75
75
  const port = opts.port ?? 3457;
76
76
  const clients = /* @__PURE__ */ new Map();
77
77
  let abortController = null;
78
+ const authToken = crypto.randomBytes(16).toString("hex");
78
79
  const wss = new WebSocketServer({ port, host: "127.0.0.1" });
79
80
  console.log(`[WebUI] WebSocket server starting on ws://localhost:${port}`);
81
+ console.log(`[WebUI] Auth token: ${authToken}`);
80
82
  const eventUnsubscribers = [];
81
83
  function setupEvents() {
82
84
  for (const unsub of eventUnsubscribers) unsub();
@@ -176,7 +178,32 @@ async function runWebUI(opts) {
176
178
  console.log(`[WebUI] WebSocket server running on ws://localhost:${port}`);
177
179
  setupEvents();
178
180
  });
179
- wss.on("connection", (ws) => {
181
+ wss.on("connection", (ws, req2) => {
182
+ const isLoopback = (hostname) => hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
183
+ try {
184
+ const url = new URL(req2.url ?? "/", `http://localhost:${port}`);
185
+ const token = url.searchParams.get("token");
186
+ const tokenOk = token === authToken;
187
+ const origin = req2.headers.origin;
188
+ if (origin) {
189
+ try {
190
+ const { hostname } = new URL(origin);
191
+ if (!isLoopback(hostname) && !tokenOk) {
192
+ ws.close(4003, "Forbidden: non-loopback origin requires auth token");
193
+ return;
194
+ }
195
+ } catch {
196
+ ws.close(4003, "Forbidden: invalid origin");
197
+ return;
198
+ }
199
+ } else {
200
+ if (!tokenOk) {
201
+ }
202
+ }
203
+ } catch {
204
+ ws.close(4001, "Unauthorized: malformed request");
205
+ return;
206
+ }
180
207
  const client = { ws, sessionId: opts.session.id };
181
208
  clients.set(ws, client);
182
209
  console.log("[WebUI] Client connected");
@@ -197,7 +224,8 @@ async function runWebUI(opts) {
197
224
  payload: {
198
225
  sessionId: opts.session.id,
199
226
  model: opts.agent.ctx.model,
200
- provider: opts.agent.ctx.provider.id
227
+ provider: opts.agent.ctx.provider.id,
228
+ wsToken: authToken
201
229
  }
202
230
  });
203
231
  });
@@ -519,7 +547,7 @@ async function runWebUI(opts) {
519
547
  if (!opts.globalConfigPath) return {};
520
548
  let raw;
521
549
  try {
522
- raw = await fs3.readFile(opts.globalConfigPath, "utf8");
550
+ raw = await fs14.readFile(opts.globalConfigPath, "utf8");
523
551
  } catch {
524
552
  return {};
525
553
  }
@@ -535,7 +563,7 @@ async function runWebUI(opts) {
535
563
  if (!opts.globalConfigPath) return;
536
564
  let raw;
537
565
  try {
538
- raw = await fs3.readFile(opts.globalConfigPath, "utf8");
566
+ raw = await fs14.readFile(opts.globalConfigPath, "utf8");
539
567
  } catch {
540
568
  raw = "{}";
541
569
  }
@@ -570,216 +598,6 @@ var init_plugin_api_factory = __esm({
570
598
  "src/plugin-api-factory.ts"() {
571
599
  }
572
600
  });
573
- async function setupProvider(params) {
574
- const { config, modelsRegistry, logger } = params;
575
- const savedProviderCfg = config.providers?.[config.provider];
576
- let resolvedProvider = await modelsRegistry.getProvider(config.provider).catch(() => void 0);
577
- if (!resolvedProvider && savedProviderCfg?.type && savedProviderCfg.type !== config.provider) {
578
- resolvedProvider = await modelsRegistry.getProvider(savedProviderCfg.type).catch(() => void 0);
579
- }
580
- if (!resolvedProvider) {
581
- if (!savedProviderCfg?.family) {
582
- logger.warn(
583
- `Provider "${config.provider}" not found in models.dev. Continuing with raw config.`
584
- );
585
- }
586
- } else if (resolvedProvider.family === "unsupported" && !savedProviderCfg?.family) {
587
- throw Object.assign(
588
- new Error(
589
- `Provider "${config.provider}" uses an unsupported wire family (${resolvedProvider.npm}). Install a plugin to enable it, or pick a different provider.`
590
- ),
591
- { code: "UNSUPPORTED_PROVIDER" }
592
- );
593
- }
594
- const providerRegistry = new ProviderRegistry();
595
- if (config.features.modelsRegistry) {
596
- try {
597
- const factories = await buildProviderFactoriesFromRegistry({
598
- registry: modelsRegistry,
599
- log: logger
600
- });
601
- for (const f of factories) providerRegistry.register(f);
602
- } catch (err) {
603
- throw new Error(
604
- `Failed to load models.dev registry: ${err instanceof Error ? err.message : err}
605
- Try \`wstack models refresh\` once you have network access, or run with --no-features.`
606
- );
607
- }
608
- }
609
- const providerConfig = config.providers?.[config.provider] ?? {
610
- type: config.provider,
611
- apiKey: config.apiKey,
612
- baseUrl: config.baseUrl
613
- };
614
- let provider;
615
- try {
616
- const cfgWithType = { ...providerConfig, type: config.provider };
617
- if (config.features.modelsRegistry && providerRegistry.has(config.provider)) {
618
- provider = providerRegistry.create(cfgWithType);
619
- } else {
620
- provider = makeProviderFromConfig(config.provider, cfgWithType);
621
- }
622
- } catch (err) {
623
- throw new Error(
624
- `Failed to create provider: ${err instanceof Error ? err.message : err}`
625
- );
626
- }
627
- return { resolvedProvider, provider, providerRegistry };
628
- }
629
- async function setupSession(params) {
630
- const { config, wpaths, projectRoot, cwd, sessionStore, systemPrompt, provider, tokenCounter, renderer, flags, onRecovery } = params;
631
- let resumeId = typeof flags["resume"] === "string" ? flags["resume"] : void 0;
632
- const recoveryLock = new RecoveryLock({ dir: wpaths.projectSessions, sessionStore });
633
- if (!resumeId && !flags["no-recovery"]) {
634
- const abandoned = await recoveryLock.checkAbandoned();
635
- if (abandoned && abandoned.messageCount > 0) {
636
- const choice = await onRecovery(abandoned, !!flags["recover"]);
637
- if (choice === "resume") resumeId = abandoned.sessionId;
638
- else if (choice === "delete") {
639
- await sessionStore.delete(abandoned.sessionId).catch(() => void 0);
640
- await recoveryLock.clear();
641
- } else await recoveryLock.clear();
642
- } else if (abandoned) {
643
- await sessionStore.delete(abandoned.sessionId).catch(() => void 0);
644
- await recoveryLock.clear();
645
- }
646
- }
647
- let session;
648
- let restoredMessages = [];
649
- if (resumeId) {
650
- try {
651
- const resumed = await sessionStore.resume(resumeId);
652
- session = resumed.writer;
653
- restoredMessages = resumed.data.messages;
654
- renderer.writeInfo(`Resumed session ${resumed.data.metadata.id} \u2014 ${restoredMessages.length} messages, ${resumed.data.usage.input + resumed.data.usage.output} tokens used previously.`);
655
- } catch (err) {
656
- renderer.writeError(`Resume failed: ${err instanceof Error ? err.message : String(err)}`);
657
- throw Object.assign(new Error("RESUME_FAILED"), { exitCode: 2 });
658
- }
659
- } else {
660
- session = await sessionStore.create({ id: "", title: "", model: config.model, provider: config.provider });
661
- }
662
- const sessionRef = { current: session };
663
- await recoveryLock.write(session.id).catch(() => void 0);
664
- const attachments = new DefaultAttachmentStore({ spoolDir: path14.join(wpaths.projectSessions, session.id, "attachments") });
665
- const queueStore = new QueueStore({ dir: path14.join(wpaths.projectSessions, session.id) });
666
- const ctxSignal = new AbortController().signal;
667
- const context = new Context({ systemPrompt, provider, session, signal: ctxSignal, tokenCounter, cwd, projectRoot, model: config.model });
668
- if (restoredMessages.length > 0) context.state.replaceMessages(restoredMessages);
669
- const todosCheckpointPath = path14.join(wpaths.projectSessions, `${session.id}.todos.json`);
670
- if (resumeId) {
671
- try {
672
- const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
673
- if (restoredTodos && restoredTodos.length > 0) {
674
- context.state.replaceTodos(restoredTodos);
675
- renderer.writeInfo(`Restored ${restoredTodos.length} todo${restoredTodos.length === 1 ? "" : "s"} from previous run.`);
676
- }
677
- } catch {
678
- }
679
- }
680
- const detachTodosCheckpoint = attachTodosCheckpoint(context.state, todosCheckpointPath, session.id);
681
- const planPath = path14.join(wpaths.projectSessions, `${session.id}.plan.json`);
682
- context.state.setMeta("plan.path", planPath);
683
- if (resumeId) {
684
- try {
685
- const fleetRoot = path14.join(wpaths.projectSessions, session.id);
686
- const dirState = await loadDirectorState(path14.join(fleetRoot, "director-state.json"));
687
- if (dirState) {
688
- const tCounts = {};
689
- for (const t of dirState.tasks) tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
690
- const summary = Object.entries(tCounts).map(([k, v]) => `${v} ${k}`).join(", ");
691
- renderer.writeInfo(`Prior fleet state: ${dirState.subagents.length} subagent${dirState.subagents.length === 1 ? "" : "s"}, tasks ${summary || "(none)"}.`);
692
- }
693
- } catch {
694
- }
695
- try {
696
- const plan = await loadPlan(planPath);
697
- if (plan && plan.items.length > 0) {
698
- const open = plan.items.filter((p) => p.status !== "done").length;
699
- const done = plan.items.length - open;
700
- renderer.writeInfo(`Plan: ${plan.items.length} item${plan.items.length === 1 ? "" : "s"} (${open} open, ${done} done). Use /plan to review.`);
701
- }
702
- } catch {
703
- }
704
- }
705
- return { session, sessionRef, context, restoredMessages, attachments, recoveryLock, queueStore, planPath, detachTodosCheckpoint };
706
- }
707
- function setupPipelines(params) {
708
- const { events, logger } = params;
709
- const pipelines = createDefaultPipelines();
710
- const installBoundary = (p) => {
711
- p.setErrorHandler((ev) => {
712
- const fromPlugin = !!ev.owner && ev.owner !== "core";
713
- logger.error(
714
- `Pipeline middleware "${ev.middleware}" crashed (owner=${ev.owner ?? "unknown"}); ${fromPlugin ? "swallowed" : "rethrown"}`,
715
- ev.err
716
- );
717
- events.emit("error", {
718
- err: ev.err instanceof Error ? ev.err : new Error(String(ev.err)),
719
- phase: `pipeline:${ev.middleware}`
720
- });
721
- return fromPlugin ? "swallow" : "rethrow";
722
- });
723
- };
724
- installBoundary(pipelines.request);
725
- installBoundary(pipelines.response);
726
- installBoundary(pipelines.toolCall);
727
- installBoundary(pipelines.userInput);
728
- installBoundary(pipelines.assistantOutput);
729
- installBoundary(pipelines.contextWindow);
730
- return pipelines;
731
- }
732
- async function setupCompaction(params) {
733
- const { compactor, events, modelsRegistry, context, config, provider, pipelines } = params;
734
- const resolvedCaps = await capabilitiesFor(modelsRegistry, provider.id, context.model).catch(() => void 0);
735
- const effectiveMaxContext = config.context.effectiveMaxContext ?? resolvedCaps?.maxContext ?? provider.capabilities.maxContext;
736
- if (config.context.autoCompact !== false) {
737
- const autoCompactor = new AutoCompactionMiddleware(
738
- compactor,
739
- effectiveMaxContext,
740
- (ctx) => {
741
- let total = 0;
742
- for (const m of ctx.messages) {
743
- if (typeof m.content === "string") {
744
- total += Math.ceil(m.content.length / 4);
745
- } else if (Array.isArray(m.content)) {
746
- for (const b of m.content) {
747
- if (b.type === "text") {
748
- total += Math.ceil(b.text.length / 4);
749
- } else if (b.type === "tool_use" || b.type === "tool_result") {
750
- total += Math.ceil(JSON.stringify(b).length / 4);
751
- }
752
- }
753
- }
754
- }
755
- return total;
756
- },
757
- {
758
- warn: config.context.warnThreshold,
759
- soft: config.context.softThreshold,
760
- hard: config.context.hardThreshold
761
- },
762
- { aggressiveOn: "soft", failureMode: "throw_on_hard", events }
763
- );
764
- pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
765
- }
766
- return effectiveMaxContext;
767
- }
768
- function createAgent(params) {
769
- return new Agent({
770
- container: params.container,
771
- tools: params.tools,
772
- providers: params.providers,
773
- events: params.events,
774
- pipelines: params.pipelines,
775
- context: params.context,
776
- maxIterations: params.config.tools.maxIterations,
777
- iterationTimeoutMs: params.config.tools.iterationTimeoutMs,
778
- executionStrategy: params.config.tools.defaultExecutionStrategy,
779
- perIterationOutputCapBytes: params.config.tools.perIterationOutputCapBytes,
780
- confirmAwaiter: params.confirmAwaiter
781
- });
782
- }
783
601
 
784
602
  // src/arg-parser.ts
785
603
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
@@ -956,13 +774,13 @@ function flagsToConfigPatch(flags) {
956
774
  }
957
775
  async function ensureProjectMeta(paths, projectRoot) {
958
776
  try {
959
- await fs3.mkdir(paths.projectDir, { recursive: true });
777
+ await fs14.mkdir(paths.projectDir, { recursive: true });
960
778
  const meta = {
961
779
  hash: paths.projectHash,
962
780
  root: projectRoot,
963
781
  lastSeen: (/* @__PURE__ */ new Date()).toISOString()
964
782
  };
965
- await fs3.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
783
+ await fs14.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
966
784
  } catch {
967
785
  }
968
786
  }
@@ -976,7 +794,7 @@ var ReadlineInputReader = class {
976
794
  }
977
795
  async loadHistory() {
978
796
  try {
979
- const raw = await fs3.readFile(this.historyFile, "utf8");
797
+ const raw = await fs14.readFile(this.historyFile, "utf8");
980
798
  this.history = raw.split("\n").filter(Boolean).slice(-1e3);
981
799
  } catch {
982
800
  this.history = [];
@@ -984,8 +802,8 @@ var ReadlineInputReader = class {
984
802
  }
985
803
  async saveHistory() {
986
804
  try {
987
- await fs3.mkdir(path14.dirname(this.historyFile), { recursive: true });
988
- await fs3.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
805
+ await fs14.mkdir(path14.dirname(this.historyFile), { recursive: true });
806
+ await fs14.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
989
807
  } catch {
990
808
  }
991
809
  }
@@ -1421,61 +1239,104 @@ async function resolveModelSelection(answer, models, provider, _registry, render
1421
1239
  var theme = { primary: color.amber };
1422
1240
  async function saveToGlobalConfig(configPath, provider, model) {
1423
1241
  try {
1424
- const { atomicWrite: atomicWrite5 } = await import('@wrongstack/core');
1425
- const fs14 = await import('fs/promises');
1242
+ const { atomicWrite: atomicWrite6 } = await import('@wrongstack/core');
1243
+ const fs15 = await import('fs/promises');
1426
1244
  let existing = {};
1427
1245
  try {
1428
- const raw = await fs14.readFile(configPath, "utf8");
1246
+ const raw = await fs15.readFile(configPath, "utf8");
1429
1247
  existing = JSON.parse(raw);
1430
1248
  } catch {
1431
1249
  }
1432
1250
  existing.provider = provider;
1433
1251
  existing.model = model;
1434
- await atomicWrite5(configPath, JSON.stringify(existing, null, 2));
1252
+ await atomicWrite6(configPath, JSON.stringify(existing, null, 2));
1253
+ return true;
1254
+ } catch {
1255
+ return false;
1256
+ }
1257
+ }
1258
+ async function pathExists(file) {
1259
+ try {
1260
+ await fs14.access(file);
1435
1261
  return true;
1436
1262
  } catch {
1437
1263
  return false;
1438
1264
  }
1439
1265
  }
1266
+ async function detectPackageManager(root, declared) {
1267
+ if (declared) {
1268
+ const name = declared.split("@")[0];
1269
+ if (name) return name;
1270
+ }
1271
+ if (await pathExists(path14.join(root, "pnpm-lock.yaml"))) return "pnpm";
1272
+ if (await pathExists(path14.join(root, "bun.lockb"))) return "bun";
1273
+ if (await pathExists(path14.join(root, "bun.lock"))) return "bun";
1274
+ if (await pathExists(path14.join(root, "yarn.lock"))) return "yarn";
1275
+ return "npm";
1276
+ }
1277
+ function hasUsableScript(scripts, name) {
1278
+ const script = scripts[name];
1279
+ if (typeof script !== "string" || script.trim() === "") return false;
1280
+ if (name === "test" && /no test specified/i.test(script)) return false;
1281
+ return true;
1282
+ }
1283
+ function parseMakeTargets(makefile) {
1284
+ const targets = /* @__PURE__ */ new Set();
1285
+ for (const line of makefile.split(/\r?\n/)) {
1286
+ if (line.startsWith(" ") || line.trimStart().startsWith("#")) continue;
1287
+ const match = /^([A-Za-z0-9_.-]+)\s*:(?![=])/.exec(line);
1288
+ if (match?.[1]) targets.add(match[1]);
1289
+ }
1290
+ return targets;
1291
+ }
1440
1292
  async function detectProjectFacts(root) {
1441
1293
  const facts = { hints: [] };
1442
1294
  try {
1443
- const pkg = JSON.parse(await fs3.readFile(path14.join(root, "package.json"), "utf8"));
1295
+ const pkg = JSON.parse(await fs14.readFile(path14.join(root, "package.json"), "utf8"));
1444
1296
  const scripts = pkg.scripts ?? {};
1445
- const pm = (pkg.packageManager ?? "npm").split("@")[0] ?? "npm";
1446
- if (scripts["build"]) facts.build = `${pm} run build`;
1447
- if (scripts["test"]) facts.test = `${pm} test`;
1448
- if (scripts["lint"]) facts.lint = `${pm} run lint`;
1449
- if (scripts["dev"] ?? scripts["start"])
1450
- facts.run = `${pm} run ${scripts["dev"] ? "dev" : "start"}`;
1451
- facts.hints.push("package.json scripts");
1297
+ const pm = await detectPackageManager(root, pkg.packageManager);
1298
+ if (hasUsableScript(scripts, "build")) facts.build = `${pm} run build`;
1299
+ if (hasUsableScript(scripts, "test")) facts.test = `${pm} test`;
1300
+ if (hasUsableScript(scripts, "lint")) facts.lint = `${pm} run lint`;
1301
+ const runScript = ["dev", "start", "serve", "preview"].find(
1302
+ (name) => hasUsableScript(scripts, name)
1303
+ );
1304
+ if (runScript) facts.run = `${pm} run ${runScript}`;
1305
+ facts.hints.push(Object.keys(scripts).length > 0 ? "package.json scripts" : "package.json");
1452
1306
  } catch {
1453
1307
  }
1454
1308
  try {
1455
- await fs3.access(path14.join(root, "pyproject.toml"));
1309
+ if (!await pathExists(path14.join(root, "pyproject.toml"))) throw new Error("not python");
1456
1310
  facts.test ??= "pytest";
1457
1311
  facts.lint ??= "ruff check .";
1458
1312
  facts.hints.push("pyproject.toml");
1459
1313
  } catch {
1460
1314
  }
1461
1315
  try {
1462
- await fs3.access(path14.join(root, "go.mod"));
1316
+ if (!await pathExists(path14.join(root, "go.mod"))) throw new Error("not go");
1463
1317
  facts.build ??= "go build ./...";
1464
1318
  facts.test ??= "go test ./...";
1319
+ facts.run ??= "go run .";
1465
1320
  facts.hints.push("go.mod");
1466
1321
  } catch {
1467
1322
  }
1468
1323
  try {
1469
- await fs3.access(path14.join(root, "Cargo.toml"));
1324
+ if (!await pathExists(path14.join(root, "Cargo.toml"))) throw new Error("not rust");
1470
1325
  facts.build ??= "cargo build";
1471
1326
  facts.test ??= "cargo test";
1327
+ facts.lint ??= "cargo clippy";
1328
+ facts.run ??= "cargo run";
1472
1329
  facts.hints.push("Cargo.toml");
1473
1330
  } catch {
1474
1331
  }
1475
1332
  try {
1476
- await fs3.access(path14.join(root, "Makefile"));
1477
- facts.build ??= "make";
1478
- facts.test ??= "make test";
1333
+ const makefile = await fs14.readFile(path14.join(root, "Makefile"), "utf8");
1334
+ const targets = parseMakeTargets(makefile);
1335
+ facts.build ??= targets.has("build") ? "make build" : "make";
1336
+ if (targets.has("test")) facts.test ??= "make test";
1337
+ if (targets.has("lint")) facts.lint ??= "make lint";
1338
+ const runTarget = ["run", "dev", "start", "serve"].find((name) => targets.has(name));
1339
+ if (runTarget) facts.run ??= `make ${runTarget}`;
1479
1340
  facts.hints.push("Makefile");
1480
1341
  } catch {
1481
1342
  }
@@ -1485,35 +1346,49 @@ function renderAgentsTemplate(f) {
1485
1346
  const cmd = (s) => s ? `\`${s}\`` : "_TODO_";
1486
1347
  return `# AGENTS.md
1487
1348
 
1488
- Project notes for WrongStack. Committed to the repo so every contributor
1489
- (human or agent) starts with the same context. Edit freely.
1349
+ This file is loaded into WrongStack's system prompt as project context.
1350
+ Keep it concise, factual, and durable: write the information future agents
1351
+ need before they touch this codebase.
1352
+
1353
+ ## Project brief
1354
+
1355
+ - **Purpose:** _What does this project do, and why does it exist?_
1356
+ - **Primary users:** _Who uses it: developers, operators, customers, internal systems?_
1357
+ - **Runtime/deployment:** _Where does it run: CLI, server, browser, worker, library, package?_
1358
+ - **Main entry points:** _Which files or commands should an agent inspect first?_
1490
1359
 
1491
- ## What this project is
1360
+ ## How to work safely
1492
1361
 
1493
- _One paragraph: what does this codebase do, who runs it, what's the
1494
- deployment target?_
1362
+ - _Project-specific rules the agent should always follow._
1363
+ - _Files, generated artifacts, migrations, or config the agent should not edit without asking._
1364
+ - _Preferred style or architecture choices that are not obvious from the code._
1495
1365
 
1496
- ## How to work on it
1366
+ ## Commands
1497
1367
 
1498
1368
  - **Build:** ${cmd(f.build)}
1499
1369
  - **Test:** ${cmd(f.test)}
1500
1370
  - **Lint:** ${cmd(f.lint)}
1501
1371
  - **Run locally:** ${cmd(f.run)}
1502
1372
 
1503
- ## Conventions
1373
+ ## Architecture notes
1504
1374
 
1505
- _What style choices matter here? Filenames, module layout, naming, error
1506
- handling, log format. Anything a stranger would get wrong._
1375
+ _Summarize the important modules, data flow, boundaries, and ownership rules.
1376
+ Mention anything a newcomer might misread._
1507
1377
 
1508
1378
  ## Domain knowledge
1509
1379
 
1510
- _Acronyms, business rules, foot-guns, "this looks weird but it's
1511
- intentional because\u2026"._
1380
+ _Business rules, acronyms, invariants, external services, and notes where the
1381
+ code looks unusual but is intentional._
1512
1382
 
1513
- ## Pointers
1383
+ ## Verification checklist
1514
1384
 
1515
- _Where to look for: routing, database migrations, feature flags,
1516
- on-call runbooks, dashboards._
1385
+ - _What should be run after code changes?_
1386
+ - _What manual smoke test proves the common path still works?_
1387
+ - _What failure modes deserve extra attention?_
1388
+
1389
+ ## Useful pointers
1390
+
1391
+ - _Docs, dashboards, runbooks, issue trackers, design notes, or owner contacts._
1517
1392
  `;
1518
1393
  }
1519
1394
  function countTurnPairs(messages) {
@@ -1915,13 +1790,13 @@ function buildHelpCommand(opts) {
1915
1790
  function buildInitCommand(opts) {
1916
1791
  return {
1917
1792
  name: "init",
1918
- description: "Scaffold .wrongstack/AGENTS.md in the current project.",
1793
+ description: "Create .wrongstack/AGENTS.md project context for the system prompt.",
1919
1794
  async run(args, ctx) {
1920
1795
  const force = args.trim() === "--force";
1921
1796
  const dir = path14.join(ctx.projectRoot, ".wrongstack");
1922
1797
  const file = path14.join(dir, "AGENTS.md");
1923
1798
  try {
1924
- await fs3.access(file);
1799
+ await fs14.access(file);
1925
1800
  if (!force) {
1926
1801
  const msg2 = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
1927
1802
  opts.renderer.writeWarning(msg2);
@@ -1931,19 +1806,19 @@ function buildInitCommand(opts) {
1931
1806
  }
1932
1807
  const detected = await detectProjectFacts(ctx.projectRoot);
1933
1808
  const body = renderAgentsTemplate(detected);
1934
- await fs3.mkdir(dir, { recursive: true });
1935
- await fs3.writeFile(file, body, "utf8");
1809
+ await fs14.mkdir(dir, { recursive: true });
1810
+ await fs14.writeFile(file, body, "utf8");
1936
1811
  if (detected.hints.length > 0) {
1937
1812
  const msg2 = `Wrote ${file}
1938
- Pre-filled: ${detected.hints.join(", ")}. Edit the file to add anything else worth remembering.`;
1813
+ Pre-filled: ${detected.hints.join(", ")}. Edit the file with project context and instructions the system prompt should carry.`;
1939
1814
  opts.renderer.writeInfo(`Wrote ${file}`);
1940
1815
  opts.renderer.writeInfo(
1941
- `Pre-filled: ${detected.hints.join(", ")}. Edit the file to add anything else worth remembering.`
1816
+ `Pre-filled: ${detected.hints.join(", ")}. Edit the file with project context and instructions the system prompt should carry.`
1942
1817
  );
1943
1818
  return { message: msg2 };
1944
1819
  }
1945
1820
  const msg = `Wrote ${file}
1946
- No project type auto-detected. Edit the file to add build/test commands and conventions.`;
1821
+ No project type auto-detected. Edit the file with project context and instructions the system prompt should carry.`;
1947
1822
  opts.renderer.writeInfo(`Wrote ${file}`);
1948
1823
  return { message: msg };
1949
1824
  }
@@ -2087,6 +1962,38 @@ ${formatPlan(updated)}` };
2087
1962
  }
2088
1963
  };
2089
1964
  }
1965
+
1966
+ // src/slash-commands/plugin.ts
1967
+ function buildPluginCommand(opts) {
1968
+ return {
1969
+ name: "plugin",
1970
+ aliases: ["plugins"],
1971
+ description: "Manage plugins: /plugin [list|status|official|install <alias>|enable <name>|disable <name>|remove <name>]",
1972
+ argsHint: "[list|status|official|install <alias>|enable <name>|disable <name>|remove <name>]",
1973
+ help: [
1974
+ "Usage:",
1975
+ " /plugin List configured plugins.",
1976
+ " /plugin status Alias for list.",
1977
+ " /plugin official List official bundled plugins and aliases.",
1978
+ " /plugin install <alias|package> Add and enable a plugin.",
1979
+ " /plugin add <alias|package> Alias for install.",
1980
+ " /plugin enable <alias|package> Enable a configured plugin.",
1981
+ " /plugin disable <alias|package> Disable a configured plugin.",
1982
+ " /plugin remove <alias|package> Remove a plugin from config.",
1983
+ "",
1984
+ "Examples:",
1985
+ " /plugin official",
1986
+ " /plugin install telegram",
1987
+ " /plugin disable lsp"
1988
+ ].join("\n"),
1989
+ async run(args) {
1990
+ if (!opts.onPlugin) {
1991
+ return { message: "Plugin management is not available in this session." };
1992
+ }
1993
+ return { message: await opts.onPlugin(args.trim()) };
1994
+ }
1995
+ };
1996
+ }
2090
1997
  function buildSaveCommand(opts) {
2091
1998
  return {
2092
1999
  name: "save",
@@ -2286,6 +2193,7 @@ function buildBuiltinSlashCommands(opts) {
2286
2193
  buildContextCommand(opts),
2287
2194
  buildToolsCommand(opts),
2288
2195
  buildSkillCommand(opts),
2196
+ buildPluginCommand(opts),
2289
2197
  buildDiagCommand(opts),
2290
2198
  buildStatsCommand(opts),
2291
2199
  buildSpawnCommand(opts),
@@ -2318,13 +2226,13 @@ var MANIFESTS = [
2318
2226
  ];
2319
2227
  async function detectProjectKind(projectRoot) {
2320
2228
  try {
2321
- await fs3.access(path14.join(projectRoot, ".wrongstack", "AGENTS.md"));
2229
+ await fs14.access(path14.join(projectRoot, ".wrongstack", "AGENTS.md"));
2322
2230
  return "initialized";
2323
2231
  } catch {
2324
2232
  }
2325
2233
  for (const m of MANIFESTS) {
2326
2234
  try {
2327
- await fs3.access(path14.join(projectRoot, m));
2235
+ await fs14.access(path14.join(projectRoot, m));
2328
2236
  return "project";
2329
2237
  } catch {
2330
2238
  }
@@ -2336,8 +2244,8 @@ async function scaffoldAgentsMd(projectRoot) {
2336
2244
  const file = path14.join(dir, "AGENTS.md");
2337
2245
  const facts = await detectProjectFacts(projectRoot);
2338
2246
  const body = renderAgentsTemplate(facts);
2339
- await fs3.mkdir(dir, { recursive: true });
2340
- await fs3.writeFile(file, body, "utf8");
2247
+ await fs14.mkdir(dir, { recursive: true });
2248
+ await fs14.writeFile(file, body, "utf8");
2341
2249
  return file;
2342
2250
  }
2343
2251
  async function runProjectCheck(opts) {
@@ -3245,7 +3153,7 @@ async function readKeyInput(deps, intent) {
3245
3153
  async function loadProviders(deps) {
3246
3154
  let raw;
3247
3155
  try {
3248
- raw = await fs3.readFile(deps.globalConfigPath, "utf8");
3156
+ raw = await fs14.readFile(deps.globalConfigPath, "utf8");
3249
3157
  } catch {
3250
3158
  return {};
3251
3159
  }
@@ -3261,7 +3169,7 @@ async function loadProviders(deps) {
3261
3169
  async function mutateProviders(deps, mutator) {
3262
3170
  let raw;
3263
3171
  try {
3264
- raw = await fs3.readFile(deps.globalConfigPath, "utf8");
3172
+ raw = await fs14.readFile(deps.globalConfigPath, "utf8");
3265
3173
  } catch {
3266
3174
  raw = "{}";
3267
3175
  }
@@ -3401,7 +3309,7 @@ var doctorCmd = async (_args, deps) => {
3401
3309
  });
3402
3310
  }
3403
3311
  try {
3404
- await fs3.access(deps.paths.secretsKey);
3312
+ await fs14.access(deps.paths.secretsKey);
3405
3313
  checks.push({ name: "secret vault", status: "ok", detail: deps.paths.secretsKey });
3406
3314
  } catch {
3407
3315
  checks.push({
@@ -3411,10 +3319,10 @@ var doctorCmd = async (_args, deps) => {
3411
3319
  });
3412
3320
  }
3413
3321
  try {
3414
- await fs3.mkdir(deps.paths.projectSessions, { recursive: true });
3322
+ await fs14.mkdir(deps.paths.projectSessions, { recursive: true });
3415
3323
  const probe = path14.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
3416
- await fs3.writeFile(probe, "");
3417
- await fs3.unlink(probe);
3324
+ await fs14.writeFile(probe, "");
3325
+ await fs14.unlink(probe);
3418
3326
  checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
3419
3327
  } catch (err) {
3420
3328
  checks.push({
@@ -3515,8 +3423,8 @@ var exportCmd = async (args, deps) => {
3515
3423
  return 1;
3516
3424
  }
3517
3425
  if (output) {
3518
- await fs3.mkdir(path14.dirname(path14.resolve(deps.cwd, output)), { recursive: true });
3519
- await fs3.writeFile(path14.resolve(deps.cwd, output), rendered, "utf8");
3426
+ await fs14.mkdir(path14.dirname(path14.resolve(deps.cwd, output)), { recursive: true });
3427
+ await fs14.writeFile(path14.resolve(deps.cwd, output), rendered, "utf8");
3520
3428
  deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
3521
3429
  `);
3522
3430
  } else {
@@ -3573,19 +3481,17 @@ var initCmd = async (_args, deps) => {
3573
3481
  } else {
3574
3482
  deps.renderer.writeInfo(`Found API key in env (${provider.envVars.join(" / ")}).`);
3575
3483
  }
3576
- await fs3.mkdir(deps.paths.globalRoot, { recursive: true });
3484
+ await fs14.mkdir(deps.paths.globalRoot, { recursive: true });
3577
3485
  const config = { version: 1, provider: providerId, model: modelId };
3578
3486
  if (apiKey) config.apiKey = apiKey;
3579
3487
  await atomicWrite(deps.paths.globalConfig, JSON.stringify(config, null, 2));
3580
- await fs3.mkdir(path14.join(deps.projectRoot, ".wrongstack"), { recursive: true });
3488
+ await fs14.mkdir(path14.join(deps.projectRoot, ".wrongstack"), { recursive: true });
3581
3489
  const agentsFile = path14.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
3582
3490
  try {
3583
- await fs3.access(agentsFile);
3491
+ await fs14.access(agentsFile);
3584
3492
  } catch {
3585
- await atomicWrite(
3586
- agentsFile,
3587
- "# Project notes for WrongStack\n\nWrite project-specific conventions, build commands,\nand domain knowledge here. This file is committed to git.\n"
3588
- );
3493
+ const detected2 = await detectProjectFacts(deps.projectRoot);
3494
+ await atomicWrite(agentsFile, renderAgentsTemplate(detected2));
3589
3495
  }
3590
3496
  deps.renderer.writeInfo(`Wrote ${deps.paths.globalConfig}`);
3591
3497
  deps.renderer.writeInfo(`Project state lives in ${deps.paths.projectDir}`);
@@ -3658,7 +3564,7 @@ async function addMcpServer(args, deps) {
3658
3564
  serverCfg.enabled = enable;
3659
3565
  let existing = {};
3660
3566
  try {
3661
- existing = JSON.parse(await fs3.readFile(deps.paths.globalConfig, "utf8"));
3567
+ existing = JSON.parse(await fs14.readFile(deps.paths.globalConfig, "utf8"));
3662
3568
  } catch {
3663
3569
  }
3664
3570
  const mcpServers = existing.mcpServers ?? {};
@@ -3678,7 +3584,7 @@ async function addMcpServer(args, deps) {
3678
3584
  async function removeMcpServer(name, deps) {
3679
3585
  let existing = {};
3680
3586
  try {
3681
- existing = JSON.parse(await fs3.readFile(deps.paths.globalConfig, "utf8"));
3587
+ existing = JSON.parse(await fs14.readFile(deps.paths.globalConfig, "utf8"));
3682
3588
  } catch {
3683
3589
  deps.renderer.writeError("No config file found.\n");
3684
3590
  return 1;
@@ -3696,26 +3602,189 @@ async function removeMcpServer(name, deps) {
3696
3602
  `);
3697
3603
  return 0;
3698
3604
  }
3699
-
3700
- // src/subcommands/handlers/plugin-usage.ts
3701
- var pluginCmd = async (args, deps) => {
3605
+ var OFFICIAL_PLUGINS = [
3606
+ {
3607
+ alias: "telegram",
3608
+ specifier: "@wrongstack/telegram",
3609
+ description: "Telegram bridge for prompts, notifications, and slash commands."
3610
+ },
3611
+ {
3612
+ alias: "lsp",
3613
+ specifier: "@wrongstack/plug-lsp",
3614
+ description: "Language Server Protocol tools for code intelligence."
3615
+ }
3616
+ ];
3617
+ var OFFICIAL_ALIASES = new Map(
3618
+ OFFICIAL_PLUGINS.flatMap((p) => [
3619
+ [p.alias, p.specifier],
3620
+ [p.specifier, p.specifier]
3621
+ ])
3622
+ );
3623
+ async function runPluginManagementCommand(args, deps) {
3702
3624
  const sub = args[0];
3703
- if (!sub || sub === "list") {
3704
- const plugins = deps.config.plugins ?? [];
3705
- if (plugins.length === 0) {
3706
- deps.renderer.write("No plugins configured.\n");
3707
- return 0;
3625
+ if (!sub || sub === "list" || sub === "status") {
3626
+ return {
3627
+ code: 0,
3628
+ level: "output",
3629
+ message: renderConfiguredPlugins(deps.config)
3630
+ };
3631
+ }
3632
+ if (sub === "official" || sub === "officials") {
3633
+ return {
3634
+ code: 0,
3635
+ level: "output",
3636
+ message: renderOfficialPlugins(deps.config)
3637
+ };
3638
+ }
3639
+ if (sub === "add" || sub === "install") {
3640
+ const spec = args[1];
3641
+ if (!spec) {
3642
+ return errorResult("Usage: wstack plugin add <specifier|official-alias> [--disabled]");
3708
3643
  }
3709
- for (const p of plugins) {
3710
- const name = typeof p === "string" ? p : p.name;
3711
- const enabled = typeof p === "object" && p.enabled === false ? "disabled" : "enabled";
3712
- deps.renderer.write(` ${name} ${enabled}
3713
- `);
3644
+ return upsertPlugin(
3645
+ resolvePluginSpecifier(spec),
3646
+ { enabled: !args.includes("--disabled") },
3647
+ deps,
3648
+ "Added"
3649
+ );
3650
+ }
3651
+ if (sub === "remove" || sub === "rm" || sub === "uninstall") {
3652
+ const spec = args[1];
3653
+ if (!spec) {
3654
+ return errorResult("Usage: wstack plugin remove <specifier|official-alias>");
3714
3655
  }
3715
- return 0;
3656
+ return removePlugin(resolvePluginSpecifier(spec), deps);
3716
3657
  }
3717
- deps.renderer.writeWarning(`plugin ${sub} not implemented (edit config.plugins manually).`);
3718
- return 0;
3658
+ if (sub === "enable" || sub === "disable") {
3659
+ const spec = args[1];
3660
+ if (!spec) {
3661
+ return errorResult(`Usage: wstack plugin ${sub} <specifier|official-alias>`);
3662
+ }
3663
+ return upsertPlugin(
3664
+ resolvePluginSpecifier(spec),
3665
+ { enabled: sub === "enable" },
3666
+ deps,
3667
+ sub === "enable" ? "Enabled" : "Disabled"
3668
+ );
3669
+ }
3670
+ return errorResult(
3671
+ `Unknown plugin subcommand: ${sub}
3672
+ Usage: wstack plugin [list|status|official|add|install|remove|enable|disable]`
3673
+ );
3674
+ }
3675
+ function resolvePluginSpecifier(input) {
3676
+ return OFFICIAL_ALIASES.get(input.toLowerCase()) ?? input;
3677
+ }
3678
+ function renderOfficialPlugins(config) {
3679
+ return [
3680
+ "Official plugins:",
3681
+ ...OFFICIAL_PLUGINS.map((p) => {
3682
+ const state = config ? officialPluginState(config, p.specifier) : "";
3683
+ const status = state ? `${state.padEnd(14)} ` : "";
3684
+ return ` ${p.alias.padEnd(12)} ${status}${p.specifier.padEnd(24)} ${p.description}`;
3685
+ }),
3686
+ "",
3687
+ "Use `wstack plugin add <alias>` or `/plugin install <alias>`."
3688
+ ].join("\n");
3689
+ }
3690
+ function renderConfiguredPlugins(config) {
3691
+ const plugins = config.plugins ?? [];
3692
+ if (plugins.length === 0) {
3693
+ return [
3694
+ "No plugins configured.",
3695
+ "Use `wstack plugin add <specifier>` or `/plugin install <official-alias>`."
3696
+ ].join("\n");
3697
+ }
3698
+ return plugins.map((p) => {
3699
+ const name = pluginName(p);
3700
+ const enabled = typeof p === "object" && p.enabled === false ? "disabled" : "enabled";
3701
+ const official = OFFICIAL_PLUGINS.find((entry) => entry.specifier === name);
3702
+ const suffix = official ? ` (${official.alias})` : "";
3703
+ return ` ${`${name}${suffix}`.padEnd(44)} ${enabled}`;
3704
+ }).join("\n");
3705
+ }
3706
+ async function readConfig(file) {
3707
+ try {
3708
+ return JSON.parse(await fs14.readFile(file, "utf8"));
3709
+ } catch {
3710
+ return {};
3711
+ }
3712
+ }
3713
+ function pluginName(p) {
3714
+ return typeof p === "string" ? p : p.name;
3715
+ }
3716
+ function pluginEntry(spec, enabled) {
3717
+ return enabled ? spec : { name: spec, enabled: false };
3718
+ }
3719
+ function officialPluginState(config, spec) {
3720
+ const match = (config.plugins ?? []).find((p) => pluginName(p) === spec);
3721
+ if (!match) return "not configured";
3722
+ return typeof match === "object" && match.enabled === false ? "disabled" : "enabled";
3723
+ }
3724
+ async function upsertPlugin(spec, opts, deps, verb) {
3725
+ const existing = await readConfig(deps.configPath);
3726
+ const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
3727
+ const idx = plugins.findIndex((p) => pluginName(p) === spec);
3728
+ const nextEntry = pluginEntry(spec, opts.enabled);
3729
+ if (idx >= 0) plugins[idx] = nextEntry;
3730
+ else plugins.push(nextEntry);
3731
+ const features = {
3732
+ ...isRecord(deps.config.features) ? deps.config.features : {},
3733
+ ...isRecord(existing.features) ? existing.features : {},
3734
+ plugins: true
3735
+ };
3736
+ existing.plugins = plugins;
3737
+ existing.features = features;
3738
+ await atomicWrite(deps.configPath, JSON.stringify(existing, null, 2));
3739
+ return {
3740
+ code: 0,
3741
+ level: "info",
3742
+ message: `${verb} "${spec}" (${opts.enabled ? "enabled" : "disabled"}). Config written to ${deps.configPath}.`,
3743
+ patch: { plugins, features },
3744
+ restartRequired: true
3745
+ };
3746
+ }
3747
+ async function removePlugin(spec, deps) {
3748
+ const existing = await readConfig(deps.configPath);
3749
+ const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
3750
+ const next = plugins.filter((p) => pluginName(p) !== spec);
3751
+ if (next.length === plugins.length) {
3752
+ return errorResult(`Plugin "${spec}" not in config.`);
3753
+ }
3754
+ existing.plugins = next;
3755
+ await atomicWrite(deps.configPath, JSON.stringify(existing, null, 2));
3756
+ return {
3757
+ code: 0,
3758
+ level: "info",
3759
+ message: `Removed "${spec}" from config.`,
3760
+ patch: { plugins: next },
3761
+ restartRequired: true
3762
+ };
3763
+ }
3764
+ function errorResult(message) {
3765
+ return { code: 1, level: "error", message };
3766
+ }
3767
+ function isRecord(value) {
3768
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3769
+ }
3770
+
3771
+ // src/subcommands/handlers/plugin-usage.ts
3772
+ var pluginCmd = async (args, deps) => {
3773
+ const result = await runPluginManagementCommand(args, {
3774
+ config: deps.config,
3775
+ configPath: deps.paths.globalConfig
3776
+ });
3777
+ if (result.level === "error") {
3778
+ deps.renderer.writeError(`${result.message}
3779
+ `);
3780
+ } else if (result.level === "info") {
3781
+ deps.renderer.writeInfo(`${result.message}
3782
+ `);
3783
+ } else {
3784
+ deps.renderer.write(`${result.message}
3785
+ `);
3786
+ }
3787
+ return result.code;
3719
3788
  };
3720
3789
  var usageCmd = async (_args, deps) => {
3721
3790
  if (!deps.sessionStore) return 0;
@@ -3729,7 +3798,7 @@ var usageCmd = async (_args, deps) => {
3729
3798
  var projectsCmd = async (_args, deps) => {
3730
3799
  const projectsRoot = path14.join(deps.paths.globalRoot, "projects");
3731
3800
  try {
3732
- const entries = await fs3.readdir(projectsRoot);
3801
+ const entries = await fs14.readdir(projectsRoot);
3733
3802
  if (entries.length === 0) {
3734
3803
  deps.renderer.write("No projects tracked.\n");
3735
3804
  return 0;
@@ -3737,7 +3806,7 @@ var projectsCmd = async (_args, deps) => {
3737
3806
  for (const hash of entries) {
3738
3807
  try {
3739
3808
  const meta = JSON.parse(
3740
- await fs3.readFile(path14.join(projectsRoot, hash, "meta.json"), "utf8")
3809
+ await fs14.readFile(path14.join(projectsRoot, hash, "meta.json"), "utf8")
3741
3810
  );
3742
3811
  deps.renderer.write(
3743
3812
  ` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
@@ -3958,7 +4027,7 @@ var helpCmd = async (_args, deps) => {
3958
4027
  " wstack models [<provider>] List models",
3959
4028
  " wstack models refresh Force-refresh cache",
3960
4029
  " wstack mcp [list] List MCP servers",
3961
- " wstack plugin [list] List plugins",
4030
+ " wstack plugin [list|status|official|install|add|remove|enable|disable] Manage plugins",
3962
4031
  " wstack projects List tracked projects",
3963
4032
  " wstack diag Full diagnostics",
3964
4033
  " wstack doctor Health checks",
@@ -3982,6 +4051,7 @@ var subcommands = {
3982
4051
  models: modelsCmd,
3983
4052
  mcp: mcpCmd,
3984
4053
  plugin: pluginCmd,
4054
+ plugins: pluginCmd,
3985
4055
  diag: diagCmd,
3986
4056
  doctor: doctorCmd,
3987
4057
  export: exportCmd,
@@ -4696,7 +4766,7 @@ var MultiAgentHost = class {
4696
4766
  });
4697
4767
  let subSession;
4698
4768
  if (this.sessionFactory) {
4699
- const subagentName = subCfg.name ?? subCfg.id ?? `sub_${randomUUID().slice(0, 8)}`;
4769
+ const subagentName = subCfg.id ?? subCfg.name ?? `sub_${randomUUID().slice(0, 8)}`;
4700
4770
  subSession = await this.sessionFactory.createSubagentSession({
4701
4771
  subagentId: subagentName,
4702
4772
  provider: subCfg.provider ?? config.provider,
@@ -5270,6 +5340,216 @@ function renderProgress2(ratio, width) {
5270
5340
  const capped = Math.min(width, filled);
5271
5341
  return FILLED2.repeat(capped) + EMPTY2.repeat(width - capped);
5272
5342
  }
5343
+ function setupPipelines(params) {
5344
+ const { events, logger } = params;
5345
+ const pipelines = createDefaultPipelines();
5346
+ const installBoundary = (p) => {
5347
+ p.setErrorHandler((ev) => {
5348
+ const fromPlugin = !!ev.owner && ev.owner !== "core";
5349
+ logger.error(
5350
+ `Pipeline middleware "${ev.middleware}" crashed (owner=${ev.owner ?? "unknown"}); ${fromPlugin ? "swallowed" : "rethrown"}`,
5351
+ ev.err
5352
+ );
5353
+ events.emit("error", {
5354
+ err: ev.err instanceof Error ? ev.err : new Error(String(ev.err)),
5355
+ phase: `pipeline:${ev.middleware}`
5356
+ });
5357
+ return fromPlugin ? "swallow" : "rethrow";
5358
+ });
5359
+ };
5360
+ installBoundary(pipelines.request);
5361
+ installBoundary(pipelines.response);
5362
+ installBoundary(pipelines.toolCall);
5363
+ installBoundary(pipelines.userInput);
5364
+ installBoundary(pipelines.assistantOutput);
5365
+ installBoundary(pipelines.contextWindow);
5366
+ return pipelines;
5367
+ }
5368
+ async function setupCompaction(params) {
5369
+ const { compactor, events, modelsRegistry, context, config, provider, pipelines } = params;
5370
+ const resolvedCaps = await capabilitiesFor(modelsRegistry, provider.id, context.model).catch(() => void 0);
5371
+ const effectiveMaxContext = config.context.effectiveMaxContext ?? resolvedCaps?.maxContext ?? provider.capabilities.maxContext;
5372
+ if (config.context.autoCompact !== false) {
5373
+ const autoCompactor = new AutoCompactionMiddleware(
5374
+ compactor,
5375
+ effectiveMaxContext,
5376
+ (ctx) => {
5377
+ let total = 0;
5378
+ for (const m of ctx.messages) {
5379
+ if (typeof m.content === "string") {
5380
+ total += Math.ceil(m.content.length / 4);
5381
+ } else if (Array.isArray(m.content)) {
5382
+ for (const b of m.content) {
5383
+ if (b.type === "text") {
5384
+ total += Math.ceil(b.text.length / 4);
5385
+ } else if (b.type === "tool_use" || b.type === "tool_result") {
5386
+ total += Math.ceil(JSON.stringify(b).length / 4);
5387
+ }
5388
+ }
5389
+ }
5390
+ }
5391
+ return total;
5392
+ },
5393
+ {
5394
+ warn: config.context.warnThreshold,
5395
+ soft: config.context.softThreshold,
5396
+ hard: config.context.hardThreshold
5397
+ },
5398
+ { aggressiveOn: "soft", failureMode: "throw_on_hard", events }
5399
+ );
5400
+ pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
5401
+ }
5402
+ return effectiveMaxContext;
5403
+ }
5404
+ function createAgent(params) {
5405
+ return new Agent({
5406
+ container: params.container,
5407
+ tools: params.tools,
5408
+ providers: params.providers,
5409
+ events: params.events,
5410
+ pipelines: params.pipelines,
5411
+ context: params.context,
5412
+ maxIterations: params.config.tools.maxIterations,
5413
+ iterationTimeoutMs: params.config.tools.iterationTimeoutMs,
5414
+ executionStrategy: params.config.tools.defaultExecutionStrategy,
5415
+ perIterationOutputCapBytes: params.config.tools.perIterationOutputCapBytes,
5416
+ confirmAwaiter: params.confirmAwaiter
5417
+ });
5418
+ }
5419
+ async function setupProvider(params) {
5420
+ const { config, modelsRegistry, logger } = params;
5421
+ const savedProviderCfg = config.providers?.[config.provider];
5422
+ let resolvedProvider = await modelsRegistry.getProvider(config.provider).catch(() => void 0);
5423
+ if (!resolvedProvider && savedProviderCfg?.type && savedProviderCfg.type !== config.provider) {
5424
+ resolvedProvider = await modelsRegistry.getProvider(savedProviderCfg.type).catch(() => void 0);
5425
+ }
5426
+ if (!resolvedProvider) {
5427
+ if (!savedProviderCfg?.family) {
5428
+ logger.warn(
5429
+ `Provider "${config.provider}" not found in models.dev. Continuing with raw config.`
5430
+ );
5431
+ }
5432
+ } else if (resolvedProvider.family === "unsupported" && !savedProviderCfg?.family) {
5433
+ throw Object.assign(
5434
+ new Error(
5435
+ `Provider "${config.provider}" uses an unsupported wire family (${resolvedProvider.npm}). Install a plugin to enable it, or pick a different provider.`
5436
+ ),
5437
+ { code: "UNSUPPORTED_PROVIDER" }
5438
+ );
5439
+ }
5440
+ const providerRegistry = new ProviderRegistry();
5441
+ if (config.features.modelsRegistry) {
5442
+ try {
5443
+ const factories = await buildProviderFactoriesFromRegistry({
5444
+ registry: modelsRegistry,
5445
+ log: logger
5446
+ });
5447
+ for (const f of factories) providerRegistry.register(f);
5448
+ } catch (err) {
5449
+ throw new Error(
5450
+ `Failed to load models.dev registry: ${err instanceof Error ? err.message : err}
5451
+ Try \`wstack models refresh\` once you have network access, or run with --no-features.`
5452
+ );
5453
+ }
5454
+ }
5455
+ const providerConfig = config.providers?.[config.provider] ?? {
5456
+ type: config.provider,
5457
+ apiKey: config.apiKey,
5458
+ baseUrl: config.baseUrl
5459
+ };
5460
+ let provider;
5461
+ try {
5462
+ const cfgWithType = { ...providerConfig, type: config.provider };
5463
+ if (config.features.modelsRegistry && providerRegistry.has(config.provider)) {
5464
+ provider = providerRegistry.create(cfgWithType);
5465
+ } else {
5466
+ provider = makeProviderFromConfig(config.provider, cfgWithType);
5467
+ }
5468
+ } catch (err) {
5469
+ throw new Error(
5470
+ `Failed to create provider: ${err instanceof Error ? err.message : err}`
5471
+ );
5472
+ }
5473
+ return { resolvedProvider, provider, providerRegistry };
5474
+ }
5475
+ async function setupSession(params) {
5476
+ const { config, wpaths, projectRoot, cwd, sessionStore, systemPrompt, provider, tokenCounter, renderer, flags, onRecovery } = params;
5477
+ let resumeId = typeof flags["resume"] === "string" ? flags["resume"] : void 0;
5478
+ const recoveryLock = new RecoveryLock({ dir: wpaths.projectSessions, sessionStore });
5479
+ if (!resumeId && !flags["no-recovery"]) {
5480
+ const abandoned = await recoveryLock.checkAbandoned();
5481
+ if (abandoned && abandoned.messageCount > 0) {
5482
+ const choice = await onRecovery(abandoned, !!flags["recover"]);
5483
+ if (choice === "resume") resumeId = abandoned.sessionId;
5484
+ else if (choice === "delete") {
5485
+ await sessionStore.delete(abandoned.sessionId).catch(() => void 0);
5486
+ await recoveryLock.clear();
5487
+ } else await recoveryLock.clear();
5488
+ } else if (abandoned) {
5489
+ await sessionStore.delete(abandoned.sessionId).catch(() => void 0);
5490
+ await recoveryLock.clear();
5491
+ }
5492
+ }
5493
+ let session;
5494
+ let restoredMessages = [];
5495
+ if (resumeId) {
5496
+ try {
5497
+ const resumed = await sessionStore.resume(resumeId);
5498
+ session = resumed.writer;
5499
+ restoredMessages = resumed.data.messages;
5500
+ renderer.writeInfo(`Resumed session ${resumed.data.metadata.id} \u2014 ${restoredMessages.length} messages, ${resumed.data.usage.input + resumed.data.usage.output} tokens used previously.`);
5501
+ } catch (err) {
5502
+ renderer.writeError(`Resume failed: ${err instanceof Error ? err.message : String(err)}`);
5503
+ throw Object.assign(new Error("RESUME_FAILED"), { exitCode: 2 });
5504
+ }
5505
+ } else {
5506
+ session = await sessionStore.create({ id: "", title: "", model: config.model, provider: config.provider });
5507
+ }
5508
+ const sessionRef = { current: session };
5509
+ await recoveryLock.write(session.id).catch(() => void 0);
5510
+ const attachments = new DefaultAttachmentStore({ spoolDir: path14.join(wpaths.projectSessions, session.id, "attachments") });
5511
+ const queueStore = new QueueStore({ dir: path14.join(wpaths.projectSessions, session.id) });
5512
+ const ctxSignal = new AbortController().signal;
5513
+ const context = new Context({ systemPrompt, provider, session, signal: ctxSignal, tokenCounter, cwd, projectRoot, model: config.model });
5514
+ if (restoredMessages.length > 0) context.state.replaceMessages(restoredMessages);
5515
+ const todosCheckpointPath = path14.join(wpaths.projectSessions, `${session.id}.todos.json`);
5516
+ if (resumeId) {
5517
+ try {
5518
+ const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
5519
+ if (restoredTodos && restoredTodos.length > 0) {
5520
+ context.state.replaceTodos(restoredTodos);
5521
+ renderer.writeInfo(`Restored ${restoredTodos.length} todo${restoredTodos.length === 1 ? "" : "s"} from previous run.`);
5522
+ }
5523
+ } catch {
5524
+ }
5525
+ }
5526
+ const detachTodosCheckpoint = attachTodosCheckpoint(context.state, todosCheckpointPath, session.id);
5527
+ const planPath = path14.join(wpaths.projectSessions, `${session.id}.plan.json`);
5528
+ context.state.setMeta("plan.path", planPath);
5529
+ if (resumeId) {
5530
+ try {
5531
+ const fleetRoot = path14.join(wpaths.projectSessions, session.id);
5532
+ const dirState = await loadDirectorState(path14.join(fleetRoot, "director-state.json"));
5533
+ if (dirState) {
5534
+ const tCounts = {};
5535
+ for (const t of dirState.tasks) tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
5536
+ const summary = Object.entries(tCounts).map(([k, v]) => `${v} ${k}`).join(", ");
5537
+ renderer.writeInfo(`Prior fleet state: ${dirState.subagents.length} subagent${dirState.subagents.length === 1 ? "" : "s"}, tasks ${summary || "(none)"}.`);
5538
+ }
5539
+ } catch {
5540
+ }
5541
+ try {
5542
+ const plan = await loadPlan(planPath);
5543
+ if (plan && plan.items.length > 0) {
5544
+ const open = plan.items.filter((p) => p.status !== "done").length;
5545
+ const done = plan.items.length - open;
5546
+ renderer.writeInfo(`Plan: ${plan.items.length} item${plan.items.length === 1 ? "" : "s"} (${open} open, ${done} done). Use /plan to review.`);
5547
+ }
5548
+ } catch {
5549
+ }
5550
+ }
5551
+ return { session, sessionRef, context, restoredMessages, attachments, recoveryLock, queueStore, planPath, detachTodosCheckpoint };
5552
+ }
5273
5553
 
5274
5554
  // src/index.ts
5275
5555
  function resolveBundledSkillsDir2() {
@@ -5281,6 +5561,17 @@ function resolveBundledSkillsDir2() {
5281
5561
  return void 0;
5282
5562
  }
5283
5563
  }
5564
+ function buildPluginOptions(config) {
5565
+ const options = {};
5566
+ for (const entry of config.plugins ?? []) {
5567
+ if (typeof entry !== "object") continue;
5568
+ if (entry.options) options[entry.name] = { ...entry.options };
5569
+ }
5570
+ for (const [name, value] of Object.entries(config.extensions ?? {})) {
5571
+ options[name] = { ...options[name] ?? {}, ...value };
5572
+ }
5573
+ return options;
5574
+ }
5284
5575
  async function main(argv) {
5285
5576
  const ctx = await boot(argv);
5286
5577
  if (typeof ctx === "number") return ctx;
@@ -5304,7 +5595,10 @@ async function main(argv) {
5304
5595
  wpaths,
5305
5596
  logger,
5306
5597
  modelsRegistry,
5307
- permission: { yolo: config.yolo, promptDelegate: makePromptDelegate(reader) },
5598
+ permission: {
5599
+ yolo: config.yolo,
5600
+ promptDelegate: makePromptDelegate(reader)
5601
+ },
5308
5602
  compactor: { preserveK: config.context.preserveK, eliseThreshold: config.context.eliseThreshold },
5309
5603
  bundledSkillsDir: config.features.skills ? resolveBundledSkillsDir2() : void 0
5310
5604
  });
@@ -5377,7 +5671,7 @@ async function main(argv) {
5377
5671
  name: "session-store",
5378
5672
  check: async () => {
5379
5673
  try {
5380
- await fs3.access(wpaths.projectSessions);
5674
+ await fs14.access(wpaths.projectSessions);
5381
5675
  return { status: "healthy" };
5382
5676
  } catch (e) {
5383
5677
  return { status: "unhealthy", detail: e instanceof Error ? e.message : "access denied" };
@@ -5543,6 +5837,7 @@ async function main(argv) {
5543
5837
  if (config.features.plugins && config.plugins && config.plugins.length > 0) {
5544
5838
  const resolvedPlugins = [];
5545
5839
  for (const p of config.plugins) {
5840
+ if (typeof p === "object" && p.enabled === false) continue;
5546
5841
  const spec = typeof p === "string" ? p : p.name;
5547
5842
  try {
5548
5843
  const mod = await import(spec);
@@ -5553,13 +5848,14 @@ async function main(argv) {
5553
5848
  }
5554
5849
  if (resolvedPlugins.length > 0) {
5555
5850
  const { default: createApi2 } = await Promise.resolve().then(() => (init_plugin_api_factory(), plugin_api_factory_exports));
5851
+ const pluginOptions = buildPluginOptions(config);
5852
+ const pluginConfig = Object.keys(pluginOptions).length > 0 ? patchConfig(config, { extensions: pluginOptions }) : config;
5556
5853
  await loadPlugins(resolvedPlugins, {
5557
5854
  log: logger,
5558
- // Each plugin's `configSchema` is validated against the matching
5559
- // `Config.extensions[name]` subtree before its `setup()` runs.
5560
- // The plugin then reads the same data through `api.config.extensions`
5561
- // (or, once L1-B lands, via `ConfigStore.getExtension(name)`).
5562
- pluginOptions: config.extensions ?? {},
5855
+ // Each plugin's `configSchema` is validated against merged
5856
+ // options from `plugins[].options` and `extensions[name]`.
5857
+ // The merged view is also exposed as `api.config.extensions`.
5858
+ pluginOptions,
5563
5859
  apiFactory: (plugin) => createApi2(plugin.name, {
5564
5860
  container,
5565
5861
  events,
@@ -5568,7 +5864,7 @@ async function main(argv) {
5568
5864
  providerRegistry,
5569
5865
  slashCommandRegistry: slashRegistry,
5570
5866
  mcpRegistry,
5571
- config,
5867
+ config: pluginConfig,
5572
5868
  log: logger,
5573
5869
  extensions: agent.extensions,
5574
5870
  sessionWriter: {
@@ -5761,7 +6057,7 @@ async function main(argv) {
5761
6057
  const subagentsRoot = path14.join(fleetRootForPromotion, "subagents");
5762
6058
  let runDirs;
5763
6059
  try {
5764
- runDirs = await fs3.readdir(subagentsRoot);
6060
+ runDirs = await fs14.readdir(subagentsRoot);
5765
6061
  } catch {
5766
6062
  return "No fleet transcripts on disk \u2014 no subagents have been spawned for this session.";
5767
6063
  }
@@ -5770,7 +6066,7 @@ async function main(argv) {
5770
6066
  const runDir = path14.join(subagentsRoot, runId);
5771
6067
  let files;
5772
6068
  try {
5773
- files = await fs3.readdir(runDir);
6069
+ files = await fs14.readdir(runDir);
5774
6070
  } catch {
5775
6071
  continue;
5776
6072
  }
@@ -5778,7 +6074,7 @@ async function main(argv) {
5778
6074
  if (!f.endsWith(".jsonl")) continue;
5779
6075
  const full = path14.join(runDir, f);
5780
6076
  try {
5781
- const stat2 = await fs3.stat(full);
6077
+ const stat2 = await fs14.stat(full);
5782
6078
  found.push({
5783
6079
  runId,
5784
6080
  subagentId: f.replace(/\.jsonl$/, ""),
@@ -5817,7 +6113,7 @@ async function main(argv) {
5817
6113
  ].join("\n");
5818
6114
  }
5819
6115
  const t = matches[0];
5820
- const raw = await fs3.readFile(t.file, "utf8");
6116
+ const raw = await fs14.readFile(t.file, "utf8");
5821
6117
  if (mode === "raw") return raw;
5822
6118
  const lines = raw.split("\n").filter((l) => l.trim());
5823
6119
  const counts = {};
@@ -5956,6 +6252,23 @@ async function main(argv) {
5956
6252
  ];
5957
6253
  return lines.join("\n");
5958
6254
  },
6255
+ onPlugin: async (args) => {
6256
+ const parsed = args.length === 0 ? [] : args.split(/\s+/).filter(Boolean);
6257
+ const result = await runPluginManagementCommand(parsed, {
6258
+ config,
6259
+ configPath: wpaths.globalConfig
6260
+ });
6261
+ if (result.patch) {
6262
+ const patch = result.patch;
6263
+ config = patchConfig(config, patch);
6264
+ configStore.update(patch);
6265
+ }
6266
+ if (result.restartRequired && result.code === 0) {
6267
+ return `${result.message}
6268
+ Restart WrongStack to load or unload plugin code in this session.`;
6269
+ }
6270
+ return result.message;
6271
+ },
5959
6272
  onExit: () => {
5960
6273
  void mcpRegistry.stopAll();
5961
6274
  },