@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/README.md +120 -114
- package/dist/index.js +628 -315
- package/dist/index.js.map +1 -1
- package/package.json +10 -9
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
988
|
-
await
|
|
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:
|
|
1425
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1446
|
-
if (scripts
|
|
1447
|
-
if (scripts
|
|
1448
|
-
if (scripts
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1477
|
-
|
|
1478
|
-
facts.
|
|
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
|
-
|
|
1489
|
-
|
|
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
|
-
##
|
|
1360
|
+
## How to work safely
|
|
1492
1361
|
|
|
1493
|
-
|
|
1494
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
1373
|
+
## Architecture notes
|
|
1504
1374
|
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
-
|
|
1511
|
-
intentional
|
|
1380
|
+
_Business rules, acronyms, invariants, external services, and notes where the
|
|
1381
|
+
code looks unusual but is intentional._
|
|
1512
1382
|
|
|
1513
|
-
##
|
|
1383
|
+
## Verification checklist
|
|
1514
1384
|
|
|
1515
|
-
|
|
1516
|
-
|
|
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: "
|
|
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
|
|
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
|
|
1935
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2340
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3322
|
+
await fs14.mkdir(deps.paths.projectSessions, { recursive: true });
|
|
3415
3323
|
const probe = path14.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
|
|
3416
|
-
await
|
|
3417
|
-
await
|
|
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
|
|
3519
|
-
await
|
|
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
|
|
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
|
|
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
|
|
3491
|
+
await fs14.access(agentsFile);
|
|
3584
3492
|
} catch {
|
|
3585
|
-
await
|
|
3586
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
3701
|
-
|
|
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
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
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
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
deps
|
|
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
|
|
3656
|
+
return removePlugin(resolvePluginSpecifier(spec), deps);
|
|
3716
3657
|
}
|
|
3717
|
-
|
|
3718
|
-
|
|
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
|
|
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
|
|
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]
|
|
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.
|
|
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: {
|
|
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
|
|
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
|
|
5559
|
-
// `
|
|
5560
|
-
// The
|
|
5561
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
},
|