@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/README.md +120 -114
- package/dist/index.js +678 -337
- 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
|
}
|
|
@@ -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
|
|
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", () =>
|
|
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:
|
|
1425
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1446
|
-
if (scripts
|
|
1447
|
-
if (scripts
|
|
1448
|
-
if (scripts
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1477
|
-
|
|
1478
|
-
facts.
|
|
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
|
-
|
|
1489
|
-
|
|
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
|
-
##
|
|
1376
|
+
## How to work safely
|
|
1492
1377
|
|
|
1493
|
-
|
|
1494
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
1389
|
+
## Architecture notes
|
|
1504
1390
|
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
-
|
|
1511
|
-
intentional
|
|
1396
|
+
_Business rules, acronyms, invariants, external services, and notes where the
|
|
1397
|
+
code looks unusual but is intentional._
|
|
1512
1398
|
|
|
1513
|
-
##
|
|
1399
|
+
## Verification checklist
|
|
1514
1400
|
|
|
1515
|
-
|
|
1516
|
-
|
|
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: "
|
|
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
|
|
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
|
|
1935
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2340
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3338
|
+
await fs14.mkdir(deps.paths.projectSessions, { recursive: true });
|
|
3415
3339
|
const probe = path14.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
|
|
3416
|
-
await
|
|
3417
|
-
await
|
|
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
|
|
3519
|
-
await
|
|
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
|
|
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
|
|
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
|
|
3507
|
+
await fs14.access(agentsFile);
|
|
3584
3508
|
} 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
|
-
);
|
|
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
|
|
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
|
|
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
|
-
|
|
3701
|
-
|
|
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
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
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
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
deps
|
|
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
|
|
3672
|
+
return removePlugin(resolvePluginSpecifier(spec), deps);
|
|
3716
3673
|
}
|
|
3717
|
-
|
|
3718
|
-
|
|
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
|
|
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
|
|
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]
|
|
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
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
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
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
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
|
-
|
|
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.
|
|
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("\
|
|
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: {
|
|
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
|
|
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
|
|
5559
|
-
// `
|
|
5560
|
-
// The
|
|
5561
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
},
|