@wrongstack/cli 0.3.3 → 0.3.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,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
  }
@@ -1008,12 +826,12 @@ var ReadlineInputReader = class {
1008
826
  this.pending = true;
1009
827
  try {
1010
828
  const rl = this.ensure();
1011
- if (rl._flushed) {
829
+ if (rl.closed || rl._flushed) {
1012
830
  rl.close();
1013
831
  this.rl = void 0;
1014
832
  }
1015
833
  const fresh = this.ensure();
1016
- return new Promise((resolve3, reject) => {
834
+ return new Promise((resolve3) => {
1017
835
  fresh.question(prompt ?? "> ", (line) => {
1018
836
  if (line.trim()) {
1019
837
  this.history.push(line);
@@ -1021,7 +839,11 @@ var ReadlineInputReader = class {
1021
839
  }
1022
840
  resolve3(line);
1023
841
  });
1024
- fresh.once("close", () => reject(new Error("EOF")));
842
+ fresh.once("close", () => resolve3(""));
843
+ }).then((result) => {
844
+ this.rl?.close();
845
+ this.rl = void 0;
846
+ return result;
1025
847
  });
1026
848
  } finally {
1027
849
  this.pending = false;
@@ -1037,6 +859,12 @@ var ReadlineInputReader = class {
1037
859
  stdin.resume();
1038
860
  const onData = (buf) => {
1039
861
  const key = buf.toString();
862
+ if (key === "") {
863
+ cleanup();
864
+ process.stdout.write("\n");
865
+ resolve3("");
866
+ return;
867
+ }
1040
868
  const opt = options.find(
1041
869
  (o) => o.key.toLowerCase() === key.toLowerCase() || o.value === key
1042
870
  );
@@ -1047,12 +875,18 @@ var ReadlineInputReader = class {
1047
875
  resolve3(opt.value);
1048
876
  }
1049
877
  };
878
+ const onClose = () => {
879
+ cleanup();
880
+ resolve3("");
881
+ };
1050
882
  const cleanup = () => {
1051
883
  stdin.off("data", onData);
884
+ stdin.off("close", onClose);
1052
885
  if (stdin.isTTY) stdin.setRawMode(wasRaw);
1053
886
  if (wasPaused) stdin.pause();
1054
887
  };
1055
888
  stdin.on("data", onData);
889
+ stdin.on("close", onClose);
1056
890
  });
1057
891
  }
1058
892
  /**
@@ -1421,61 +1255,104 @@ async function resolveModelSelection(answer, models, provider, _registry, render
1421
1255
  var theme = { primary: color.amber };
1422
1256
  async function saveToGlobalConfig(configPath, provider, model) {
1423
1257
  try {
1424
- const { atomicWrite: atomicWrite5 } = await import('@wrongstack/core');
1425
- const fs14 = await import('fs/promises');
1258
+ const { atomicWrite: atomicWrite6 } = await import('@wrongstack/core');
1259
+ const fs15 = await import('fs/promises');
1426
1260
  let existing = {};
1427
1261
  try {
1428
- const raw = await fs14.readFile(configPath, "utf8");
1262
+ const raw = await fs15.readFile(configPath, "utf8");
1429
1263
  existing = JSON.parse(raw);
1430
1264
  } catch {
1431
1265
  }
1432
1266
  existing.provider = provider;
1433
1267
  existing.model = model;
1434
- await atomicWrite5(configPath, JSON.stringify(existing, null, 2));
1268
+ await atomicWrite6(configPath, JSON.stringify(existing, null, 2));
1269
+ return true;
1270
+ } catch {
1271
+ return false;
1272
+ }
1273
+ }
1274
+ async function pathExists(file) {
1275
+ try {
1276
+ await fs14.access(file);
1435
1277
  return true;
1436
1278
  } catch {
1437
1279
  return false;
1438
1280
  }
1439
1281
  }
1282
+ async function detectPackageManager(root, declared) {
1283
+ if (declared) {
1284
+ const name = declared.split("@")[0];
1285
+ if (name) return name;
1286
+ }
1287
+ if (await pathExists(path14.join(root, "pnpm-lock.yaml"))) return "pnpm";
1288
+ if (await pathExists(path14.join(root, "bun.lockb"))) return "bun";
1289
+ if (await pathExists(path14.join(root, "bun.lock"))) return "bun";
1290
+ if (await pathExists(path14.join(root, "yarn.lock"))) return "yarn";
1291
+ return "npm";
1292
+ }
1293
+ function hasUsableScript(scripts, name) {
1294
+ const script = scripts[name];
1295
+ if (typeof script !== "string" || script.trim() === "") return false;
1296
+ if (name === "test" && /no test specified/i.test(script)) return false;
1297
+ return true;
1298
+ }
1299
+ function parseMakeTargets(makefile) {
1300
+ const targets = /* @__PURE__ */ new Set();
1301
+ for (const line of makefile.split(/\r?\n/)) {
1302
+ if (line.startsWith(" ") || line.trimStart().startsWith("#")) continue;
1303
+ const match = /^([A-Za-z0-9_.-]+)\s*:(?![=])/.exec(line);
1304
+ if (match?.[1]) targets.add(match[1]);
1305
+ }
1306
+ return targets;
1307
+ }
1440
1308
  async function detectProjectFacts(root) {
1441
1309
  const facts = { hints: [] };
1442
1310
  try {
1443
- const pkg = JSON.parse(await fs3.readFile(path14.join(root, "package.json"), "utf8"));
1311
+ const pkg = JSON.parse(await fs14.readFile(path14.join(root, "package.json"), "utf8"));
1444
1312
  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");
1313
+ const pm = await detectPackageManager(root, pkg.packageManager);
1314
+ if (hasUsableScript(scripts, "build")) facts.build = `${pm} run build`;
1315
+ if (hasUsableScript(scripts, "test")) facts.test = `${pm} test`;
1316
+ if (hasUsableScript(scripts, "lint")) facts.lint = `${pm} run lint`;
1317
+ const runScript = ["dev", "start", "serve", "preview"].find(
1318
+ (name) => hasUsableScript(scripts, name)
1319
+ );
1320
+ if (runScript) facts.run = `${pm} run ${runScript}`;
1321
+ facts.hints.push(Object.keys(scripts).length > 0 ? "package.json scripts" : "package.json");
1452
1322
  } catch {
1453
1323
  }
1454
1324
  try {
1455
- await fs3.access(path14.join(root, "pyproject.toml"));
1325
+ if (!await pathExists(path14.join(root, "pyproject.toml"))) throw new Error("not python");
1456
1326
  facts.test ??= "pytest";
1457
1327
  facts.lint ??= "ruff check .";
1458
1328
  facts.hints.push("pyproject.toml");
1459
1329
  } catch {
1460
1330
  }
1461
1331
  try {
1462
- await fs3.access(path14.join(root, "go.mod"));
1332
+ if (!await pathExists(path14.join(root, "go.mod"))) throw new Error("not go");
1463
1333
  facts.build ??= "go build ./...";
1464
1334
  facts.test ??= "go test ./...";
1335
+ facts.run ??= "go run .";
1465
1336
  facts.hints.push("go.mod");
1466
1337
  } catch {
1467
1338
  }
1468
1339
  try {
1469
- await fs3.access(path14.join(root, "Cargo.toml"));
1340
+ if (!await pathExists(path14.join(root, "Cargo.toml"))) throw new Error("not rust");
1470
1341
  facts.build ??= "cargo build";
1471
1342
  facts.test ??= "cargo test";
1343
+ facts.lint ??= "cargo clippy";
1344
+ facts.run ??= "cargo run";
1472
1345
  facts.hints.push("Cargo.toml");
1473
1346
  } catch {
1474
1347
  }
1475
1348
  try {
1476
- await fs3.access(path14.join(root, "Makefile"));
1477
- facts.build ??= "make";
1478
- facts.test ??= "make test";
1349
+ const makefile = await fs14.readFile(path14.join(root, "Makefile"), "utf8");
1350
+ const targets = parseMakeTargets(makefile);
1351
+ facts.build ??= targets.has("build") ? "make build" : "make";
1352
+ if (targets.has("test")) facts.test ??= "make test";
1353
+ if (targets.has("lint")) facts.lint ??= "make lint";
1354
+ const runTarget = ["run", "dev", "start", "serve"].find((name) => targets.has(name));
1355
+ if (runTarget) facts.run ??= `make ${runTarget}`;
1479
1356
  facts.hints.push("Makefile");
1480
1357
  } catch {
1481
1358
  }
@@ -1485,35 +1362,49 @@ function renderAgentsTemplate(f) {
1485
1362
  const cmd = (s) => s ? `\`${s}\`` : "_TODO_";
1486
1363
  return `# AGENTS.md
1487
1364
 
1488
- Project notes for WrongStack. Committed to the repo so every contributor
1489
- (human or agent) starts with the same context. Edit freely.
1365
+ This file is loaded into WrongStack's system prompt as project context.
1366
+ Keep it concise, factual, and durable: write the information future agents
1367
+ need before they touch this codebase.
1368
+
1369
+ ## Project brief
1370
+
1371
+ - **Purpose:** _What does this project do, and why does it exist?_
1372
+ - **Primary users:** _Who uses it: developers, operators, customers, internal systems?_
1373
+ - **Runtime/deployment:** _Where does it run: CLI, server, browser, worker, library, package?_
1374
+ - **Main entry points:** _Which files or commands should an agent inspect first?_
1490
1375
 
1491
- ## What this project is
1376
+ ## How to work safely
1492
1377
 
1493
- _One paragraph: what does this codebase do, who runs it, what's the
1494
- deployment target?_
1378
+ - _Project-specific rules the agent should always follow._
1379
+ - _Files, generated artifacts, migrations, or config the agent should not edit without asking._
1380
+ - _Preferred style or architecture choices that are not obvious from the code._
1495
1381
 
1496
- ## How to work on it
1382
+ ## Commands
1497
1383
 
1498
1384
  - **Build:** ${cmd(f.build)}
1499
1385
  - **Test:** ${cmd(f.test)}
1500
1386
  - **Lint:** ${cmd(f.lint)}
1501
1387
  - **Run locally:** ${cmd(f.run)}
1502
1388
 
1503
- ## Conventions
1389
+ ## Architecture notes
1504
1390
 
1505
- _What style choices matter here? Filenames, module layout, naming, error
1506
- handling, log format. Anything a stranger would get wrong._
1391
+ _Summarize the important modules, data flow, boundaries, and ownership rules.
1392
+ Mention anything a newcomer might misread._
1507
1393
 
1508
1394
  ## Domain knowledge
1509
1395
 
1510
- _Acronyms, business rules, foot-guns, "this looks weird but it's
1511
- intentional because\u2026"._
1396
+ _Business rules, acronyms, invariants, external services, and notes where the
1397
+ code looks unusual but is intentional._
1512
1398
 
1513
- ## Pointers
1399
+ ## Verification checklist
1514
1400
 
1515
- _Where to look for: routing, database migrations, feature flags,
1516
- on-call runbooks, dashboards._
1401
+ - _What should be run after code changes?_
1402
+ - _What manual smoke test proves the common path still works?_
1403
+ - _What failure modes deserve extra attention?_
1404
+
1405
+ ## Useful pointers
1406
+
1407
+ - _Docs, dashboards, runbooks, issue trackers, design notes, or owner contacts._
1517
1408
  `;
1518
1409
  }
1519
1410
  function countTurnPairs(messages) {
@@ -1915,13 +1806,13 @@ function buildHelpCommand(opts) {
1915
1806
  function buildInitCommand(opts) {
1916
1807
  return {
1917
1808
  name: "init",
1918
- description: "Scaffold .wrongstack/AGENTS.md in the current project.",
1809
+ description: "Create .wrongstack/AGENTS.md project context for the system prompt.",
1919
1810
  async run(args, ctx) {
1920
1811
  const force = args.trim() === "--force";
1921
1812
  const dir = path14.join(ctx.projectRoot, ".wrongstack");
1922
1813
  const file = path14.join(dir, "AGENTS.md");
1923
1814
  try {
1924
- await fs3.access(file);
1815
+ await fs14.access(file);
1925
1816
  if (!force) {
1926
1817
  const msg2 = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
1927
1818
  opts.renderer.writeWarning(msg2);
@@ -1931,19 +1822,19 @@ function buildInitCommand(opts) {
1931
1822
  }
1932
1823
  const detected = await detectProjectFacts(ctx.projectRoot);
1933
1824
  const body = renderAgentsTemplate(detected);
1934
- await fs3.mkdir(dir, { recursive: true });
1935
- await fs3.writeFile(file, body, "utf8");
1825
+ await fs14.mkdir(dir, { recursive: true });
1826
+ await fs14.writeFile(file, body, "utf8");
1936
1827
  if (detected.hints.length > 0) {
1937
1828
  const msg2 = `Wrote ${file}
1938
- Pre-filled: ${detected.hints.join(", ")}. Edit the file to add anything else worth remembering.`;
1829
+ Pre-filled: ${detected.hints.join(", ")}. Edit the file with project context and instructions the system prompt should carry.`;
1939
1830
  opts.renderer.writeInfo(`Wrote ${file}`);
1940
1831
  opts.renderer.writeInfo(
1941
- `Pre-filled: ${detected.hints.join(", ")}. Edit the file to add anything else worth remembering.`
1832
+ `Pre-filled: ${detected.hints.join(", ")}. Edit the file with project context and instructions the system prompt should carry.`
1942
1833
  );
1943
1834
  return { message: msg2 };
1944
1835
  }
1945
1836
  const msg = `Wrote ${file}
1946
- No project type auto-detected. Edit the file to add build/test commands and conventions.`;
1837
+ No project type auto-detected. Edit the file with project context and instructions the system prompt should carry.`;
1947
1838
  opts.renderer.writeInfo(`Wrote ${file}`);
1948
1839
  return { message: msg };
1949
1840
  }
@@ -2087,6 +1978,38 @@ ${formatPlan(updated)}` };
2087
1978
  }
2088
1979
  };
2089
1980
  }
1981
+
1982
+ // src/slash-commands/plugin.ts
1983
+ function buildPluginCommand(opts) {
1984
+ return {
1985
+ name: "plugin",
1986
+ aliases: ["plugins"],
1987
+ description: "Manage plugins: /plugin [list|status|official|install <alias>|enable <name>|disable <name>|remove <name>]",
1988
+ argsHint: "[list|status|official|install <alias>|enable <name>|disable <name>|remove <name>]",
1989
+ help: [
1990
+ "Usage:",
1991
+ " /plugin List configured plugins.",
1992
+ " /plugin status Alias for list.",
1993
+ " /plugin official List official bundled plugins and aliases.",
1994
+ " /plugin install <alias|package> Add and enable a plugin.",
1995
+ " /plugin add <alias|package> Alias for install.",
1996
+ " /plugin enable <alias|package> Enable a configured plugin.",
1997
+ " /plugin disable <alias|package> Disable a configured plugin.",
1998
+ " /plugin remove <alias|package> Remove a plugin from config.",
1999
+ "",
2000
+ "Examples:",
2001
+ " /plugin official",
2002
+ " /plugin install telegram",
2003
+ " /plugin disable lsp"
2004
+ ].join("\n"),
2005
+ async run(args) {
2006
+ if (!opts.onPlugin) {
2007
+ return { message: "Plugin management is not available in this session." };
2008
+ }
2009
+ return { message: await opts.onPlugin(args.trim()) };
2010
+ }
2011
+ };
2012
+ }
2090
2013
  function buildSaveCommand(opts) {
2091
2014
  return {
2092
2015
  name: "save",
@@ -2286,6 +2209,7 @@ function buildBuiltinSlashCommands(opts) {
2286
2209
  buildContextCommand(opts),
2287
2210
  buildToolsCommand(opts),
2288
2211
  buildSkillCommand(opts),
2212
+ buildPluginCommand(opts),
2289
2213
  buildDiagCommand(opts),
2290
2214
  buildStatsCommand(opts),
2291
2215
  buildSpawnCommand(opts),
@@ -2318,13 +2242,13 @@ var MANIFESTS = [
2318
2242
  ];
2319
2243
  async function detectProjectKind(projectRoot) {
2320
2244
  try {
2321
- await fs3.access(path14.join(projectRoot, ".wrongstack", "AGENTS.md"));
2245
+ await fs14.access(path14.join(projectRoot, ".wrongstack", "AGENTS.md"));
2322
2246
  return "initialized";
2323
2247
  } catch {
2324
2248
  }
2325
2249
  for (const m of MANIFESTS) {
2326
2250
  try {
2327
- await fs3.access(path14.join(projectRoot, m));
2251
+ await fs14.access(path14.join(projectRoot, m));
2328
2252
  return "project";
2329
2253
  } catch {
2330
2254
  }
@@ -2336,8 +2260,8 @@ async function scaffoldAgentsMd(projectRoot) {
2336
2260
  const file = path14.join(dir, "AGENTS.md");
2337
2261
  const facts = await detectProjectFacts(projectRoot);
2338
2262
  const body = renderAgentsTemplate(facts);
2339
- await fs3.mkdir(dir, { recursive: true });
2340
- await fs3.writeFile(file, body, "utf8");
2263
+ await fs14.mkdir(dir, { recursive: true });
2264
+ await fs14.writeFile(file, body, "utf8");
2341
2265
  return file;
2342
2266
  }
2343
2267
  async function runProjectCheck(opts) {
@@ -3245,7 +3169,7 @@ async function readKeyInput(deps, intent) {
3245
3169
  async function loadProviders(deps) {
3246
3170
  let raw;
3247
3171
  try {
3248
- raw = await fs3.readFile(deps.globalConfigPath, "utf8");
3172
+ raw = await fs14.readFile(deps.globalConfigPath, "utf8");
3249
3173
  } catch {
3250
3174
  return {};
3251
3175
  }
@@ -3261,7 +3185,7 @@ async function loadProviders(deps) {
3261
3185
  async function mutateProviders(deps, mutator) {
3262
3186
  let raw;
3263
3187
  try {
3264
- raw = await fs3.readFile(deps.globalConfigPath, "utf8");
3188
+ raw = await fs14.readFile(deps.globalConfigPath, "utf8");
3265
3189
  } catch {
3266
3190
  raw = "{}";
3267
3191
  }
@@ -3401,7 +3325,7 @@ var doctorCmd = async (_args, deps) => {
3401
3325
  });
3402
3326
  }
3403
3327
  try {
3404
- await fs3.access(deps.paths.secretsKey);
3328
+ await fs14.access(deps.paths.secretsKey);
3405
3329
  checks.push({ name: "secret vault", status: "ok", detail: deps.paths.secretsKey });
3406
3330
  } catch {
3407
3331
  checks.push({
@@ -3411,10 +3335,10 @@ var doctorCmd = async (_args, deps) => {
3411
3335
  });
3412
3336
  }
3413
3337
  try {
3414
- await fs3.mkdir(deps.paths.projectSessions, { recursive: true });
3338
+ await fs14.mkdir(deps.paths.projectSessions, { recursive: true });
3415
3339
  const probe = path14.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
3416
- await fs3.writeFile(probe, "");
3417
- await fs3.unlink(probe);
3340
+ await fs14.writeFile(probe, "");
3341
+ await fs14.unlink(probe);
3418
3342
  checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
3419
3343
  } catch (err) {
3420
3344
  checks.push({
@@ -3515,8 +3439,8 @@ var exportCmd = async (args, deps) => {
3515
3439
  return 1;
3516
3440
  }
3517
3441
  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");
3442
+ await fs14.mkdir(path14.dirname(path14.resolve(deps.cwd, output)), { recursive: true });
3443
+ await fs14.writeFile(path14.resolve(deps.cwd, output), rendered, "utf8");
3520
3444
  deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
3521
3445
  `);
3522
3446
  } else {
@@ -3573,19 +3497,17 @@ var initCmd = async (_args, deps) => {
3573
3497
  } else {
3574
3498
  deps.renderer.writeInfo(`Found API key in env (${provider.envVars.join(" / ")}).`);
3575
3499
  }
3576
- await fs3.mkdir(deps.paths.globalRoot, { recursive: true });
3500
+ await fs14.mkdir(deps.paths.globalRoot, { recursive: true });
3577
3501
  const config = { version: 1, provider: providerId, model: modelId };
3578
3502
  if (apiKey) config.apiKey = apiKey;
3579
3503
  await atomicWrite(deps.paths.globalConfig, JSON.stringify(config, null, 2));
3580
- await fs3.mkdir(path14.join(deps.projectRoot, ".wrongstack"), { recursive: true });
3504
+ await fs14.mkdir(path14.join(deps.projectRoot, ".wrongstack"), { recursive: true });
3581
3505
  const agentsFile = path14.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
3582
3506
  try {
3583
- await fs3.access(agentsFile);
3507
+ await fs14.access(agentsFile);
3584
3508
  } 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
- );
3509
+ const detected2 = await detectProjectFacts(deps.projectRoot);
3510
+ await atomicWrite(agentsFile, renderAgentsTemplate(detected2));
3589
3511
  }
3590
3512
  deps.renderer.writeInfo(`Wrote ${deps.paths.globalConfig}`);
3591
3513
  deps.renderer.writeInfo(`Project state lives in ${deps.paths.projectDir}`);
@@ -3658,7 +3580,7 @@ async function addMcpServer(args, deps) {
3658
3580
  serverCfg.enabled = enable;
3659
3581
  let existing = {};
3660
3582
  try {
3661
- existing = JSON.parse(await fs3.readFile(deps.paths.globalConfig, "utf8"));
3583
+ existing = JSON.parse(await fs14.readFile(deps.paths.globalConfig, "utf8"));
3662
3584
  } catch {
3663
3585
  }
3664
3586
  const mcpServers = existing.mcpServers ?? {};
@@ -3678,7 +3600,7 @@ async function addMcpServer(args, deps) {
3678
3600
  async function removeMcpServer(name, deps) {
3679
3601
  let existing = {};
3680
3602
  try {
3681
- existing = JSON.parse(await fs3.readFile(deps.paths.globalConfig, "utf8"));
3603
+ existing = JSON.parse(await fs14.readFile(deps.paths.globalConfig, "utf8"));
3682
3604
  } catch {
3683
3605
  deps.renderer.writeError("No config file found.\n");
3684
3606
  return 1;
@@ -3696,26 +3618,189 @@ async function removeMcpServer(name, deps) {
3696
3618
  `);
3697
3619
  return 0;
3698
3620
  }
3699
-
3700
- // src/subcommands/handlers/plugin-usage.ts
3701
- var pluginCmd = async (args, deps) => {
3621
+ var OFFICIAL_PLUGINS = [
3622
+ {
3623
+ alias: "telegram",
3624
+ specifier: "@wrongstack/telegram",
3625
+ description: "Telegram bridge for prompts, notifications, and slash commands."
3626
+ },
3627
+ {
3628
+ alias: "lsp",
3629
+ specifier: "@wrongstack/plug-lsp",
3630
+ description: "Language Server Protocol tools for code intelligence."
3631
+ }
3632
+ ];
3633
+ var OFFICIAL_ALIASES = new Map(
3634
+ OFFICIAL_PLUGINS.flatMap((p) => [
3635
+ [p.alias, p.specifier],
3636
+ [p.specifier, p.specifier]
3637
+ ])
3638
+ );
3639
+ async function runPluginManagementCommand(args, deps) {
3702
3640
  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;
3641
+ if (!sub || sub === "list" || sub === "status") {
3642
+ return {
3643
+ code: 0,
3644
+ level: "output",
3645
+ message: renderConfiguredPlugins(deps.config)
3646
+ };
3647
+ }
3648
+ if (sub === "official" || sub === "officials") {
3649
+ return {
3650
+ code: 0,
3651
+ level: "output",
3652
+ message: renderOfficialPlugins(deps.config)
3653
+ };
3654
+ }
3655
+ if (sub === "add" || sub === "install") {
3656
+ const spec = args[1];
3657
+ if (!spec) {
3658
+ return errorResult("Usage: wstack plugin add <specifier|official-alias> [--disabled]");
3708
3659
  }
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
- `);
3660
+ return upsertPlugin(
3661
+ resolvePluginSpecifier(spec),
3662
+ { enabled: !args.includes("--disabled") },
3663
+ deps,
3664
+ "Added"
3665
+ );
3666
+ }
3667
+ if (sub === "remove" || sub === "rm" || sub === "uninstall") {
3668
+ const spec = args[1];
3669
+ if (!spec) {
3670
+ return errorResult("Usage: wstack plugin remove <specifier|official-alias>");
3714
3671
  }
3715
- return 0;
3672
+ return removePlugin(resolvePluginSpecifier(spec), deps);
3716
3673
  }
3717
- deps.renderer.writeWarning(`plugin ${sub} not implemented (edit config.plugins manually).`);
3718
- return 0;
3674
+ if (sub === "enable" || sub === "disable") {
3675
+ const spec = args[1];
3676
+ if (!spec) {
3677
+ return errorResult(`Usage: wstack plugin ${sub} <specifier|official-alias>`);
3678
+ }
3679
+ return upsertPlugin(
3680
+ resolvePluginSpecifier(spec),
3681
+ { enabled: sub === "enable" },
3682
+ deps,
3683
+ sub === "enable" ? "Enabled" : "Disabled"
3684
+ );
3685
+ }
3686
+ return errorResult(
3687
+ `Unknown plugin subcommand: ${sub}
3688
+ Usage: wstack plugin [list|status|official|add|install|remove|enable|disable]`
3689
+ );
3690
+ }
3691
+ function resolvePluginSpecifier(input) {
3692
+ return OFFICIAL_ALIASES.get(input.toLowerCase()) ?? input;
3693
+ }
3694
+ function renderOfficialPlugins(config) {
3695
+ return [
3696
+ "Official plugins:",
3697
+ ...OFFICIAL_PLUGINS.map((p) => {
3698
+ const state = config ? officialPluginState(config, p.specifier) : "";
3699
+ const status = state ? `${state.padEnd(14)} ` : "";
3700
+ return ` ${p.alias.padEnd(12)} ${status}${p.specifier.padEnd(24)} ${p.description}`;
3701
+ }),
3702
+ "",
3703
+ "Use `wstack plugin add <alias>` or `/plugin install <alias>`."
3704
+ ].join("\n");
3705
+ }
3706
+ function renderConfiguredPlugins(config) {
3707
+ const plugins = config.plugins ?? [];
3708
+ if (plugins.length === 0) {
3709
+ return [
3710
+ "No plugins configured.",
3711
+ "Use `wstack plugin add <specifier>` or `/plugin install <official-alias>`."
3712
+ ].join("\n");
3713
+ }
3714
+ return plugins.map((p) => {
3715
+ const name = pluginName(p);
3716
+ const enabled = typeof p === "object" && p.enabled === false ? "disabled" : "enabled";
3717
+ const official = OFFICIAL_PLUGINS.find((entry) => entry.specifier === name);
3718
+ const suffix = official ? ` (${official.alias})` : "";
3719
+ return ` ${`${name}${suffix}`.padEnd(44)} ${enabled}`;
3720
+ }).join("\n");
3721
+ }
3722
+ async function readConfig(file) {
3723
+ try {
3724
+ return JSON.parse(await fs14.readFile(file, "utf8"));
3725
+ } catch {
3726
+ return {};
3727
+ }
3728
+ }
3729
+ function pluginName(p) {
3730
+ return typeof p === "string" ? p : p.name;
3731
+ }
3732
+ function pluginEntry(spec, enabled) {
3733
+ return enabled ? spec : { name: spec, enabled: false };
3734
+ }
3735
+ function officialPluginState(config, spec) {
3736
+ const match = (config.plugins ?? []).find((p) => pluginName(p) === spec);
3737
+ if (!match) return "not configured";
3738
+ return typeof match === "object" && match.enabled === false ? "disabled" : "enabled";
3739
+ }
3740
+ async function upsertPlugin(spec, opts, deps, verb) {
3741
+ const existing = await readConfig(deps.configPath);
3742
+ const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
3743
+ const idx = plugins.findIndex((p) => pluginName(p) === spec);
3744
+ const nextEntry = pluginEntry(spec, opts.enabled);
3745
+ if (idx >= 0) plugins[idx] = nextEntry;
3746
+ else plugins.push(nextEntry);
3747
+ const features = {
3748
+ ...isRecord(deps.config.features) ? deps.config.features : {},
3749
+ ...isRecord(existing.features) ? existing.features : {},
3750
+ plugins: true
3751
+ };
3752
+ existing.plugins = plugins;
3753
+ existing.features = features;
3754
+ await atomicWrite(deps.configPath, JSON.stringify(existing, null, 2));
3755
+ return {
3756
+ code: 0,
3757
+ level: "info",
3758
+ message: `${verb} "${spec}" (${opts.enabled ? "enabled" : "disabled"}). Config written to ${deps.configPath}.`,
3759
+ patch: { plugins, features },
3760
+ restartRequired: true
3761
+ };
3762
+ }
3763
+ async function removePlugin(spec, deps) {
3764
+ const existing = await readConfig(deps.configPath);
3765
+ const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
3766
+ const next = plugins.filter((p) => pluginName(p) !== spec);
3767
+ if (next.length === plugins.length) {
3768
+ return errorResult(`Plugin "${spec}" not in config.`);
3769
+ }
3770
+ existing.plugins = next;
3771
+ await atomicWrite(deps.configPath, JSON.stringify(existing, null, 2));
3772
+ return {
3773
+ code: 0,
3774
+ level: "info",
3775
+ message: `Removed "${spec}" from config.`,
3776
+ patch: { plugins: next },
3777
+ restartRequired: true
3778
+ };
3779
+ }
3780
+ function errorResult(message) {
3781
+ return { code: 1, level: "error", message };
3782
+ }
3783
+ function isRecord(value) {
3784
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3785
+ }
3786
+
3787
+ // src/subcommands/handlers/plugin-usage.ts
3788
+ var pluginCmd = async (args, deps) => {
3789
+ const result = await runPluginManagementCommand(args, {
3790
+ config: deps.config,
3791
+ configPath: deps.paths.globalConfig
3792
+ });
3793
+ if (result.level === "error") {
3794
+ deps.renderer.writeError(`${result.message}
3795
+ `);
3796
+ } else if (result.level === "info") {
3797
+ deps.renderer.writeInfo(`${result.message}
3798
+ `);
3799
+ } else {
3800
+ deps.renderer.write(`${result.message}
3801
+ `);
3802
+ }
3803
+ return result.code;
3719
3804
  };
3720
3805
  var usageCmd = async (_args, deps) => {
3721
3806
  if (!deps.sessionStore) return 0;
@@ -3729,7 +3814,7 @@ var usageCmd = async (_args, deps) => {
3729
3814
  var projectsCmd = async (_args, deps) => {
3730
3815
  const projectsRoot = path14.join(deps.paths.globalRoot, "projects");
3731
3816
  try {
3732
- const entries = await fs3.readdir(projectsRoot);
3817
+ const entries = await fs14.readdir(projectsRoot);
3733
3818
  if (entries.length === 0) {
3734
3819
  deps.renderer.write("No projects tracked.\n");
3735
3820
  return 0;
@@ -3737,7 +3822,7 @@ var projectsCmd = async (_args, deps) => {
3737
3822
  for (const hash of entries) {
3738
3823
  try {
3739
3824
  const meta = JSON.parse(
3740
- await fs3.readFile(path14.join(projectsRoot, hash, "meta.json"), "utf8")
3825
+ await fs14.readFile(path14.join(projectsRoot, hash, "meta.json"), "utf8")
3741
3826
  );
3742
3827
  deps.renderer.write(
3743
3828
  ` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
@@ -3958,7 +4043,7 @@ var helpCmd = async (_args, deps) => {
3958
4043
  " wstack models [<provider>] List models",
3959
4044
  " wstack models refresh Force-refresh cache",
3960
4045
  " wstack mcp [list] List MCP servers",
3961
- " wstack plugin [list] List plugins",
4046
+ " wstack plugin [list|status|official|install|add|remove|enable|disable] Manage plugins",
3962
4047
  " wstack projects List tracked projects",
3963
4048
  " wstack diag Full diagnostics",
3964
4049
  " wstack doctor Health checks",
@@ -3982,6 +4067,7 @@ var subcommands = {
3982
4067
  models: modelsCmd,
3983
4068
  mcp: mcpCmd,
3984
4069
  plugin: pluginCmd,
4070
+ plugins: pluginCmd,
3985
4071
  diag: diagCmd,
3986
4072
  doctor: doctorCmd,
3987
4073
  export: exportCmd,
@@ -4310,10 +4396,14 @@ async function readPossiblyMultiline(opts) {
4310
4396
  const first = await opts.reader.readLine(firstPrompt);
4311
4397
  if (first.trim() === '"""') {
4312
4398
  const parts = [];
4313
- for (; ; ) {
4314
- const next = await opts.reader.readLine(contPrompt);
4315
- if (next.trim() === '"""') break;
4316
- parts.push(next);
4399
+ try {
4400
+ for (; ; ) {
4401
+ const next = await opts.reader.readLine(contPrompt);
4402
+ if (next.trim() === '"""') break;
4403
+ parts.push(next);
4404
+ }
4405
+ } catch {
4406
+ return parts.join("\n");
4317
4407
  }
4318
4408
  return parts.join("\n");
4319
4409
  }
@@ -4467,6 +4557,7 @@ async function execute(deps) {
4467
4557
  );
4468
4558
  }
4469
4559
  } else if (flags.tui && !flags["no-tui"]) {
4560
+ agent.disableInteractiveConfirmation();
4470
4561
  const { runTui } = await import('@wrongstack/tui');
4471
4562
  renderer.setSilent(true);
4472
4563
  const banneredFamily = savedProviderCfg?.family ?? resolvedProvider?.family;
@@ -4526,19 +4617,22 @@ async function execute(deps) {
4526
4617
  modelsRegistry,
4527
4618
  globalConfigPath: wpaths.globalConfig
4528
4619
  });
4529
- code = await runRepl({
4530
- agent,
4531
- renderer,
4532
- reader,
4533
- slashRegistry,
4534
- tokenCounter,
4535
- visionAdapters,
4536
- supportsVision,
4537
- attachments,
4538
- effectiveMaxContext,
4539
- projectName: path14.basename(projectRoot) || void 0
4540
- });
4541
- await webuiPromise;
4620
+ try {
4621
+ code = await runRepl({
4622
+ agent,
4623
+ renderer,
4624
+ reader,
4625
+ slashRegistry,
4626
+ tokenCounter,
4627
+ visionAdapters,
4628
+ supportsVision,
4629
+ attachments,
4630
+ effectiveMaxContext,
4631
+ projectName: path14.basename(projectRoot) || void 0
4632
+ });
4633
+ } finally {
4634
+ await webuiPromise.catch(() => void 0);
4635
+ }
4542
4636
  } else {
4543
4637
  code = await runRepl({
4544
4638
  agent,
@@ -4554,7 +4648,10 @@ async function execute(deps) {
4554
4648
  });
4555
4649
  }
4556
4650
  } finally {
4557
- stats.render(renderer);
4651
+ try {
4652
+ stats.render(renderer);
4653
+ } catch (err) {
4654
+ }
4558
4655
  await Promise.resolve(detachTodosCheckpoint?.()).catch(() => void 0);
4559
4656
  await mcpRegistry.stopAll();
4560
4657
  await session.append({
@@ -4696,7 +4793,7 @@ var MultiAgentHost = class {
4696
4793
  });
4697
4794
  let subSession;
4698
4795
  if (this.sessionFactory) {
4699
- const subagentName = subCfg.name ?? subCfg.id ?? `sub_${randomUUID().slice(0, 8)}`;
4796
+ const subagentName = subCfg.id ?? subCfg.name ?? `sub_${randomUUID().slice(0, 8)}`;
4700
4797
  subSession = await this.sessionFactory.createSubagentSession({
4701
4798
  subagentId: subagentName,
4702
4799
  provider: subCfg.provider ?? config.provider,
@@ -5011,8 +5108,9 @@ var MultiAgentHost = class {
5011
5108
  };
5012
5109
  function makePromptDelegate(reader) {
5013
5110
  return async (tool, input, suggestedPattern) => {
5111
+ process.stdout.write("\x07");
5014
5112
  process.stdout.write(`
5015
- ${theme2.primary("\u258D")} ${theme2.bold(tool.name)}
5113
+ ${theme2.warn("\u26A0 APPROVAL REQUIRED")} ${theme2.primary("\u2502")} ${theme2.bold(tool.name)}
5016
5114
  `);
5017
5115
  process.stdout.write(`${color.dim(stringifyInput(input))}
5018
5116
  `);
@@ -5270,6 +5368,216 @@ function renderProgress2(ratio, width) {
5270
5368
  const capped = Math.min(width, filled);
5271
5369
  return FILLED2.repeat(capped) + EMPTY2.repeat(width - capped);
5272
5370
  }
5371
+ function setupPipelines(params) {
5372
+ const { events, logger } = params;
5373
+ const pipelines = createDefaultPipelines();
5374
+ const installBoundary = (p) => {
5375
+ p.setErrorHandler((ev) => {
5376
+ const fromPlugin = !!ev.owner && ev.owner !== "core";
5377
+ logger.error(
5378
+ `Pipeline middleware "${ev.middleware}" crashed (owner=${ev.owner ?? "unknown"}); ${fromPlugin ? "swallowed" : "rethrown"}`,
5379
+ ev.err
5380
+ );
5381
+ events.emit("error", {
5382
+ err: ev.err instanceof Error ? ev.err : new Error(String(ev.err)),
5383
+ phase: `pipeline:${ev.middleware}`
5384
+ });
5385
+ return fromPlugin ? "swallow" : "rethrow";
5386
+ });
5387
+ };
5388
+ installBoundary(pipelines.request);
5389
+ installBoundary(pipelines.response);
5390
+ installBoundary(pipelines.toolCall);
5391
+ installBoundary(pipelines.userInput);
5392
+ installBoundary(pipelines.assistantOutput);
5393
+ installBoundary(pipelines.contextWindow);
5394
+ return pipelines;
5395
+ }
5396
+ async function setupCompaction(params) {
5397
+ const { compactor, events, modelsRegistry, context, config, provider, pipelines } = params;
5398
+ const resolvedCaps = await capabilitiesFor(modelsRegistry, provider.id, context.model).catch(() => void 0);
5399
+ const effectiveMaxContext = config.context.effectiveMaxContext ?? resolvedCaps?.maxContext ?? provider.capabilities.maxContext;
5400
+ if (config.context.autoCompact !== false) {
5401
+ const autoCompactor = new AutoCompactionMiddleware(
5402
+ compactor,
5403
+ effectiveMaxContext,
5404
+ (ctx) => {
5405
+ let total = 0;
5406
+ for (const m of ctx.messages) {
5407
+ if (typeof m.content === "string") {
5408
+ total += Math.ceil(m.content.length / 4);
5409
+ } else if (Array.isArray(m.content)) {
5410
+ for (const b of m.content) {
5411
+ if (b.type === "text") {
5412
+ total += Math.ceil(b.text.length / 4);
5413
+ } else if (b.type === "tool_use" || b.type === "tool_result") {
5414
+ total += Math.ceil(JSON.stringify(b).length / 4);
5415
+ }
5416
+ }
5417
+ }
5418
+ }
5419
+ return total;
5420
+ },
5421
+ {
5422
+ warn: config.context.warnThreshold,
5423
+ soft: config.context.softThreshold,
5424
+ hard: config.context.hardThreshold
5425
+ },
5426
+ { aggressiveOn: "soft", failureMode: "throw_on_hard", events }
5427
+ );
5428
+ pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
5429
+ }
5430
+ return effectiveMaxContext;
5431
+ }
5432
+ function createAgent(params) {
5433
+ return new Agent({
5434
+ container: params.container,
5435
+ tools: params.tools,
5436
+ providers: params.providers,
5437
+ events: params.events,
5438
+ pipelines: params.pipelines,
5439
+ context: params.context,
5440
+ maxIterations: params.config.tools.maxIterations,
5441
+ iterationTimeoutMs: params.config.tools.iterationTimeoutMs,
5442
+ executionStrategy: params.config.tools.defaultExecutionStrategy,
5443
+ perIterationOutputCapBytes: params.config.tools.perIterationOutputCapBytes,
5444
+ confirmAwaiter: params.confirmAwaiter
5445
+ });
5446
+ }
5447
+ async function setupProvider(params) {
5448
+ const { config, modelsRegistry, logger } = params;
5449
+ const savedProviderCfg = config.providers?.[config.provider];
5450
+ let resolvedProvider = await modelsRegistry.getProvider(config.provider).catch(() => void 0);
5451
+ if (!resolvedProvider && savedProviderCfg?.type && savedProviderCfg.type !== config.provider) {
5452
+ resolvedProvider = await modelsRegistry.getProvider(savedProviderCfg.type).catch(() => void 0);
5453
+ }
5454
+ if (!resolvedProvider) {
5455
+ if (!savedProviderCfg?.family) {
5456
+ logger.warn(
5457
+ `Provider "${config.provider}" not found in models.dev. Continuing with raw config.`
5458
+ );
5459
+ }
5460
+ } else if (resolvedProvider.family === "unsupported" && !savedProviderCfg?.family) {
5461
+ throw Object.assign(
5462
+ new Error(
5463
+ `Provider "${config.provider}" uses an unsupported wire family (${resolvedProvider.npm}). Install a plugin to enable it, or pick a different provider.`
5464
+ ),
5465
+ { code: "UNSUPPORTED_PROVIDER" }
5466
+ );
5467
+ }
5468
+ const providerRegistry = new ProviderRegistry();
5469
+ if (config.features.modelsRegistry) {
5470
+ try {
5471
+ const factories = await buildProviderFactoriesFromRegistry({
5472
+ registry: modelsRegistry,
5473
+ log: logger
5474
+ });
5475
+ for (const f of factories) providerRegistry.register(f);
5476
+ } catch (err) {
5477
+ throw new Error(
5478
+ `Failed to load models.dev registry: ${err instanceof Error ? err.message : err}
5479
+ Try \`wstack models refresh\` once you have network access, or run with --no-features.`
5480
+ );
5481
+ }
5482
+ }
5483
+ const providerConfig = config.providers?.[config.provider] ?? {
5484
+ type: config.provider,
5485
+ apiKey: config.apiKey,
5486
+ baseUrl: config.baseUrl
5487
+ };
5488
+ let provider;
5489
+ try {
5490
+ const cfgWithType = { ...providerConfig, type: config.provider };
5491
+ if (config.features.modelsRegistry && providerRegistry.has(config.provider)) {
5492
+ provider = providerRegistry.create(cfgWithType);
5493
+ } else {
5494
+ provider = makeProviderFromConfig(config.provider, cfgWithType);
5495
+ }
5496
+ } catch (err) {
5497
+ throw new Error(
5498
+ `Failed to create provider: ${err instanceof Error ? err.message : err}`
5499
+ );
5500
+ }
5501
+ return { resolvedProvider, provider, providerRegistry };
5502
+ }
5503
+ async function setupSession(params) {
5504
+ const { config, wpaths, projectRoot, cwd, sessionStore, systemPrompt, provider, tokenCounter, renderer, flags, onRecovery } = params;
5505
+ let resumeId = typeof flags["resume"] === "string" ? flags["resume"] : void 0;
5506
+ const recoveryLock = new RecoveryLock({ dir: wpaths.projectSessions, sessionStore });
5507
+ if (!resumeId && !flags["no-recovery"]) {
5508
+ const abandoned = await recoveryLock.checkAbandoned();
5509
+ if (abandoned && abandoned.messageCount > 0) {
5510
+ const choice = await onRecovery(abandoned, !!flags["recover"]);
5511
+ if (choice === "resume") resumeId = abandoned.sessionId;
5512
+ else if (choice === "delete") {
5513
+ await sessionStore.delete(abandoned.sessionId).catch(() => void 0);
5514
+ await recoveryLock.clear();
5515
+ } else await recoveryLock.clear();
5516
+ } else if (abandoned) {
5517
+ await sessionStore.delete(abandoned.sessionId).catch(() => void 0);
5518
+ await recoveryLock.clear();
5519
+ }
5520
+ }
5521
+ let session;
5522
+ let restoredMessages = [];
5523
+ if (resumeId) {
5524
+ try {
5525
+ const resumed = await sessionStore.resume(resumeId);
5526
+ session = resumed.writer;
5527
+ restoredMessages = resumed.data.messages;
5528
+ renderer.writeInfo(`Resumed session ${resumed.data.metadata.id} \u2014 ${restoredMessages.length} messages, ${resumed.data.usage.input + resumed.data.usage.output} tokens used previously.`);
5529
+ } catch (err) {
5530
+ renderer.writeError(`Resume failed: ${err instanceof Error ? err.message : String(err)}`);
5531
+ throw Object.assign(new Error("RESUME_FAILED"), { exitCode: 2 });
5532
+ }
5533
+ } else {
5534
+ session = await sessionStore.create({ id: "", title: "", model: config.model, provider: config.provider });
5535
+ }
5536
+ const sessionRef = { current: session };
5537
+ await recoveryLock.write(session.id).catch(() => void 0);
5538
+ const attachments = new DefaultAttachmentStore({ spoolDir: path14.join(wpaths.projectSessions, session.id, "attachments") });
5539
+ const queueStore = new QueueStore({ dir: path14.join(wpaths.projectSessions, session.id) });
5540
+ const ctxSignal = new AbortController().signal;
5541
+ const context = new Context({ systemPrompt, provider, session, signal: ctxSignal, tokenCounter, cwd, projectRoot, model: config.model });
5542
+ if (restoredMessages.length > 0) context.state.replaceMessages(restoredMessages);
5543
+ const todosCheckpointPath = path14.join(wpaths.projectSessions, `${session.id}.todos.json`);
5544
+ if (resumeId) {
5545
+ try {
5546
+ const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
5547
+ if (restoredTodos && restoredTodos.length > 0) {
5548
+ context.state.replaceTodos(restoredTodos);
5549
+ renderer.writeInfo(`Restored ${restoredTodos.length} todo${restoredTodos.length === 1 ? "" : "s"} from previous run.`);
5550
+ }
5551
+ } catch {
5552
+ }
5553
+ }
5554
+ const detachTodosCheckpoint = attachTodosCheckpoint(context.state, todosCheckpointPath, session.id);
5555
+ const planPath = path14.join(wpaths.projectSessions, `${session.id}.plan.json`);
5556
+ context.state.setMeta("plan.path", planPath);
5557
+ if (resumeId) {
5558
+ try {
5559
+ const fleetRoot = path14.join(wpaths.projectSessions, session.id);
5560
+ const dirState = await loadDirectorState(path14.join(fleetRoot, "director-state.json"));
5561
+ if (dirState) {
5562
+ const tCounts = {};
5563
+ for (const t of dirState.tasks) tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
5564
+ const summary = Object.entries(tCounts).map(([k, v]) => `${v} ${k}`).join(", ");
5565
+ renderer.writeInfo(`Prior fleet state: ${dirState.subagents.length} subagent${dirState.subagents.length === 1 ? "" : "s"}, tasks ${summary || "(none)"}.`);
5566
+ }
5567
+ } catch {
5568
+ }
5569
+ try {
5570
+ const plan = await loadPlan(planPath);
5571
+ if (plan && plan.items.length > 0) {
5572
+ const open = plan.items.filter((p) => p.status !== "done").length;
5573
+ const done = plan.items.length - open;
5574
+ renderer.writeInfo(`Plan: ${plan.items.length} item${plan.items.length === 1 ? "" : "s"} (${open} open, ${done} done). Use /plan to review.`);
5575
+ }
5576
+ } catch {
5577
+ }
5578
+ }
5579
+ return { session, sessionRef, context, restoredMessages, attachments, recoveryLock, queueStore, planPath, detachTodosCheckpoint };
5580
+ }
5273
5581
 
5274
5582
  // src/index.ts
5275
5583
  function resolveBundledSkillsDir2() {
@@ -5281,6 +5589,17 @@ function resolveBundledSkillsDir2() {
5281
5589
  return void 0;
5282
5590
  }
5283
5591
  }
5592
+ function buildPluginOptions(config) {
5593
+ const options = {};
5594
+ for (const entry of config.plugins ?? []) {
5595
+ if (typeof entry !== "object") continue;
5596
+ if (entry.options) options[entry.name] = { ...entry.options };
5597
+ }
5598
+ for (const [name, value] of Object.entries(config.extensions ?? {})) {
5599
+ options[name] = { ...options[name] ?? {}, ...value };
5600
+ }
5601
+ return options;
5602
+ }
5284
5603
  async function main(argv) {
5285
5604
  const ctx = await boot(argv);
5286
5605
  if (typeof ctx === "number") return ctx;
@@ -5304,7 +5623,10 @@ async function main(argv) {
5304
5623
  wpaths,
5305
5624
  logger,
5306
5625
  modelsRegistry,
5307
- permission: { yolo: config.yolo, promptDelegate: makePromptDelegate(reader) },
5626
+ permission: {
5627
+ yolo: config.yolo,
5628
+ promptDelegate: makePromptDelegate(reader)
5629
+ },
5308
5630
  compactor: { preserveK: config.context.preserveK, eliseThreshold: config.context.eliseThreshold },
5309
5631
  bundledSkillsDir: config.features.skills ? resolveBundledSkillsDir2() : void 0
5310
5632
  });
@@ -5377,7 +5699,7 @@ async function main(argv) {
5377
5699
  name: "session-store",
5378
5700
  check: async () => {
5379
5701
  try {
5380
- await fs3.access(wpaths.projectSessions);
5702
+ await fs14.access(wpaths.projectSessions);
5381
5703
  return { status: "healthy" };
5382
5704
  } catch (e) {
5383
5705
  return { status: "unhealthy", detail: e instanceof Error ? e.message : "access denied" };
@@ -5543,6 +5865,7 @@ async function main(argv) {
5543
5865
  if (config.features.plugins && config.plugins && config.plugins.length > 0) {
5544
5866
  const resolvedPlugins = [];
5545
5867
  for (const p of config.plugins) {
5868
+ if (typeof p === "object" && p.enabled === false) continue;
5546
5869
  const spec = typeof p === "string" ? p : p.name;
5547
5870
  try {
5548
5871
  const mod = await import(spec);
@@ -5553,13 +5876,14 @@ async function main(argv) {
5553
5876
  }
5554
5877
  if (resolvedPlugins.length > 0) {
5555
5878
  const { default: createApi2 } = await Promise.resolve().then(() => (init_plugin_api_factory(), plugin_api_factory_exports));
5879
+ const pluginOptions = buildPluginOptions(config);
5880
+ const pluginConfig = Object.keys(pluginOptions).length > 0 ? patchConfig(config, { extensions: pluginOptions }) : config;
5556
5881
  await loadPlugins(resolvedPlugins, {
5557
5882
  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 ?? {},
5883
+ // Each plugin's `configSchema` is validated against merged
5884
+ // options from `plugins[].options` and `extensions[name]`.
5885
+ // The merged view is also exposed as `api.config.extensions`.
5886
+ pluginOptions,
5563
5887
  apiFactory: (plugin) => createApi2(plugin.name, {
5564
5888
  container,
5565
5889
  events,
@@ -5568,7 +5892,7 @@ async function main(argv) {
5568
5892
  providerRegistry,
5569
5893
  slashCommandRegistry: slashRegistry,
5570
5894
  mcpRegistry,
5571
- config,
5895
+ config: pluginConfig,
5572
5896
  log: logger,
5573
5897
  extensions: agent.extensions,
5574
5898
  sessionWriter: {
@@ -5761,7 +6085,7 @@ async function main(argv) {
5761
6085
  const subagentsRoot = path14.join(fleetRootForPromotion, "subagents");
5762
6086
  let runDirs;
5763
6087
  try {
5764
- runDirs = await fs3.readdir(subagentsRoot);
6088
+ runDirs = await fs14.readdir(subagentsRoot);
5765
6089
  } catch {
5766
6090
  return "No fleet transcripts on disk \u2014 no subagents have been spawned for this session.";
5767
6091
  }
@@ -5770,7 +6094,7 @@ async function main(argv) {
5770
6094
  const runDir = path14.join(subagentsRoot, runId);
5771
6095
  let files;
5772
6096
  try {
5773
- files = await fs3.readdir(runDir);
6097
+ files = await fs14.readdir(runDir);
5774
6098
  } catch {
5775
6099
  continue;
5776
6100
  }
@@ -5778,7 +6102,7 @@ async function main(argv) {
5778
6102
  if (!f.endsWith(".jsonl")) continue;
5779
6103
  const full = path14.join(runDir, f);
5780
6104
  try {
5781
- const stat2 = await fs3.stat(full);
6105
+ const stat2 = await fs14.stat(full);
5782
6106
  found.push({
5783
6107
  runId,
5784
6108
  subagentId: f.replace(/\.jsonl$/, ""),
@@ -5817,7 +6141,7 @@ async function main(argv) {
5817
6141
  ].join("\n");
5818
6142
  }
5819
6143
  const t = matches[0];
5820
- const raw = await fs3.readFile(t.file, "utf8");
6144
+ const raw = await fs14.readFile(t.file, "utf8");
5821
6145
  if (mode === "raw") return raw;
5822
6146
  const lines = raw.split("\n").filter((l) => l.trim());
5823
6147
  const counts = {};
@@ -5956,6 +6280,23 @@ async function main(argv) {
5956
6280
  ];
5957
6281
  return lines.join("\n");
5958
6282
  },
6283
+ onPlugin: async (args) => {
6284
+ const parsed = args.length === 0 ? [] : args.split(/\s+/).filter(Boolean);
6285
+ const result = await runPluginManagementCommand(parsed, {
6286
+ config,
6287
+ configPath: wpaths.globalConfig
6288
+ });
6289
+ if (result.patch) {
6290
+ const patch = result.patch;
6291
+ config = patchConfig(config, patch);
6292
+ configStore.update(patch);
6293
+ }
6294
+ if (result.restartRequired && result.code === 0) {
6295
+ return `${result.message}
6296
+ Restart WrongStack to load or unload plugin code in this session.`;
6297
+ }
6298
+ return result.message;
6299
+ },
5959
6300
  onExit: () => {
5960
6301
  void mcpRegistry.stopAll();
5961
6302
  },