@wrongstack/cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1874 -392
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { color,
|
|
3
|
-
import * as
|
|
2
|
+
import { color, DefaultLogger, DefaultModelsRegistry, Container, DefaultConfigStore, TOKENS, DefaultSecretScrubber, DefaultRetryPolicy, DefaultErrorHandler, DefaultTokenCounter, DefaultSessionStore, DefaultMemoryStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultPermissionPolicy, HybridCompactor, ProviderRegistry, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, createDefaultPipelines, AutoCompactionMiddleware, Agent, SlashCommandRegistry, loadPlugins, DefaultPathResolver, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, InputBuilder, DefaultPluginAPI, DefaultSessionReader, atomicWrite, makeAgentSubagentRunner, DefaultMultiAgentCoordinator, decryptConfigSecrets, encryptConfigSecrets } from '@wrongstack/core';
|
|
3
|
+
import * as fs6 from 'fs/promises';
|
|
4
|
+
import { writeFileSync } from 'fs';
|
|
4
5
|
import { createRequire } from 'module';
|
|
5
|
-
import * as
|
|
6
|
-
import * as path2 from 'path';
|
|
6
|
+
import * as path5 from 'path';
|
|
7
7
|
import { MCPRegistry } from '@wrongstack/mcp';
|
|
8
8
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig, capabilitiesFor } from '@wrongstack/providers';
|
|
9
|
-
import {
|
|
9
|
+
import { rememberTool, forgetTool } from '@wrongstack/tools';
|
|
10
|
+
import { builtinTools } from '@wrongstack/tools/builtin';
|
|
10
11
|
import * as readline from 'readline';
|
|
12
|
+
import * as os3 from 'os';
|
|
13
|
+
import { randomUUID } from 'crypto';
|
|
11
14
|
|
|
12
15
|
var __defProp = Object.defineProperty;
|
|
13
16
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -37,11 +40,11 @@ var ReadlineInputReader = class {
|
|
|
37
40
|
history = [];
|
|
38
41
|
pending = false;
|
|
39
42
|
constructor(opts = {}) {
|
|
40
|
-
this.historyFile = opts.historyFile ??
|
|
43
|
+
this.historyFile = opts.historyFile ?? path5.join(os3.homedir(), ".wrongstack", "history");
|
|
41
44
|
}
|
|
42
45
|
async loadHistory() {
|
|
43
46
|
try {
|
|
44
|
-
const raw = await
|
|
47
|
+
const raw = await fs6.readFile(this.historyFile, "utf8");
|
|
45
48
|
this.history = raw.split("\n").filter(Boolean).slice(-1e3);
|
|
46
49
|
} catch {
|
|
47
50
|
this.history = [];
|
|
@@ -49,8 +52,8 @@ var ReadlineInputReader = class {
|
|
|
49
52
|
}
|
|
50
53
|
async saveHistory() {
|
|
51
54
|
try {
|
|
52
|
-
await
|
|
53
|
-
await
|
|
55
|
+
await fs6.mkdir(path5.dirname(this.historyFile), { recursive: true });
|
|
56
|
+
await fs6.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
|
|
54
57
|
} catch {
|
|
55
58
|
}
|
|
56
59
|
}
|
|
@@ -68,7 +71,7 @@ var ReadlineInputReader = class {
|
|
|
68
71
|
async readLine(prompt) {
|
|
69
72
|
if (this.history.length === 0) await this.loadHistory();
|
|
70
73
|
while (this.pending) {
|
|
71
|
-
await new Promise((
|
|
74
|
+
await new Promise((resolve3) => setTimeout(resolve3, 50));
|
|
72
75
|
}
|
|
73
76
|
this.pending = true;
|
|
74
77
|
try {
|
|
@@ -78,13 +81,13 @@ var ReadlineInputReader = class {
|
|
|
78
81
|
this.rl = void 0;
|
|
79
82
|
}
|
|
80
83
|
const fresh = this.ensure();
|
|
81
|
-
return new Promise((
|
|
84
|
+
return new Promise((resolve3, reject) => {
|
|
82
85
|
fresh.question(prompt ?? "> ", (line) => {
|
|
83
86
|
if (line.trim()) {
|
|
84
87
|
this.history.push(line);
|
|
85
88
|
void this.saveHistory();
|
|
86
89
|
}
|
|
87
|
-
|
|
90
|
+
resolve3(line);
|
|
88
91
|
});
|
|
89
92
|
fresh.once("close", () => reject(new Error("EOF")));
|
|
90
93
|
});
|
|
@@ -94,7 +97,7 @@ var ReadlineInputReader = class {
|
|
|
94
97
|
}
|
|
95
98
|
async readKey(prompt, options) {
|
|
96
99
|
process.stdout.write(prompt);
|
|
97
|
-
return new Promise((
|
|
100
|
+
return new Promise((resolve3) => {
|
|
98
101
|
const stdin = process.stdin;
|
|
99
102
|
const wasRaw = stdin.isRaw;
|
|
100
103
|
const wasPaused = stdin.isPaused();
|
|
@@ -109,7 +112,7 @@ var ReadlineInputReader = class {
|
|
|
109
112
|
cleanup();
|
|
110
113
|
process.stdout.write(`${opt.key}
|
|
111
114
|
`);
|
|
112
|
-
|
|
115
|
+
resolve3(opt.value);
|
|
113
116
|
}
|
|
114
117
|
};
|
|
115
118
|
const cleanup = () => {
|
|
@@ -121,9 +124,14 @@ var ReadlineInputReader = class {
|
|
|
121
124
|
});
|
|
122
125
|
}
|
|
123
126
|
/**
|
|
124
|
-
* Read a
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
+
* Read a line of input while masking each character with a bullet so the
|
|
128
|
+
* user gets visual confirmation that bytes are arriving (especially on
|
|
129
|
+
* paste, which previously felt like nothing happened). Pasted chunks
|
|
130
|
+
* are echoed as a run of bullets, Backspace/DEL erases one bullet, and
|
|
131
|
+
* Ctrl+U / Ctrl+W are honored. Non-TTY input is read normally — there's
|
|
132
|
+
* nothing to hide when piped, and echoing bullets to a file is noise.
|
|
133
|
+
*
|
|
134
|
+
* Returns the raw entered string (no trim — caller decides).
|
|
127
135
|
*/
|
|
128
136
|
async readSecret(prompt) {
|
|
129
137
|
const stdin = process.stdin;
|
|
@@ -131,18 +139,25 @@ var ReadlineInputReader = class {
|
|
|
131
139
|
this.rl?.close();
|
|
132
140
|
this.rl = void 0;
|
|
133
141
|
process.stdout.write(prompt);
|
|
134
|
-
return new Promise((
|
|
142
|
+
return new Promise((resolve3) => {
|
|
135
143
|
let buf = "";
|
|
136
144
|
const wasRaw = stdin.isRaw;
|
|
137
145
|
stdin.setRawMode(true);
|
|
138
146
|
stdin.resume();
|
|
139
147
|
stdin.setEncoding("utf8");
|
|
148
|
+
const eraseChar = () => {
|
|
149
|
+
process.stdout.write("\b \b");
|
|
150
|
+
};
|
|
151
|
+
const eraseAll = () => {
|
|
152
|
+
for (let i = 0; i < buf.length; i++) eraseChar();
|
|
153
|
+
};
|
|
140
154
|
const onData = (chunk) => {
|
|
141
155
|
for (const ch of chunk) {
|
|
142
156
|
if (ch === "\r" || ch === "\n") {
|
|
143
157
|
cleanup();
|
|
144
|
-
process.stdout.write(
|
|
145
|
-
|
|
158
|
+
process.stdout.write(` ${dim(`[${buf.length} chars]`)}
|
|
159
|
+
`);
|
|
160
|
+
resolve3(buf);
|
|
146
161
|
return;
|
|
147
162
|
}
|
|
148
163
|
if (ch === "") {
|
|
@@ -150,11 +165,28 @@ var ReadlineInputReader = class {
|
|
|
150
165
|
process.stdout.write("\n");
|
|
151
166
|
process.exit(130);
|
|
152
167
|
}
|
|
168
|
+
if (ch === "") {
|
|
169
|
+
eraseAll();
|
|
170
|
+
buf = "";
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (ch === "") {
|
|
174
|
+
const m = buf.match(/(\S+\s*)$/);
|
|
175
|
+
const drop = m ? m[0].length : buf.length;
|
|
176
|
+
for (let i = 0; i < drop; i++) eraseChar();
|
|
177
|
+
buf = buf.slice(0, buf.length - drop);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
153
180
|
if (ch === "\x7F" || ch === "\b") {
|
|
154
|
-
buf
|
|
181
|
+
if (buf.length > 0) {
|
|
182
|
+
buf = buf.slice(0, -1);
|
|
183
|
+
eraseChar();
|
|
184
|
+
}
|
|
155
185
|
continue;
|
|
156
186
|
}
|
|
187
|
+
if (ch < " ") continue;
|
|
157
188
|
buf += ch;
|
|
189
|
+
process.stdout.write("\u2022");
|
|
158
190
|
}
|
|
159
191
|
};
|
|
160
192
|
const cleanup = () => {
|
|
@@ -171,6 +203,10 @@ var ReadlineInputReader = class {
|
|
|
171
203
|
this.rl = void 0;
|
|
172
204
|
}
|
|
173
205
|
};
|
|
206
|
+
function dim(s) {
|
|
207
|
+
if (!process.stdout.isTTY) return s;
|
|
208
|
+
return `\x1B[2m${s}\x1B[22m`;
|
|
209
|
+
}
|
|
174
210
|
function renderDiff(diff) {
|
|
175
211
|
if (!diff) return "";
|
|
176
212
|
const lines = diff.split("\n");
|
|
@@ -221,6 +257,13 @@ ${theme.primary("\u258D")} ${theme.bold(tool.name)}
|
|
|
221
257
|
return answer;
|
|
222
258
|
};
|
|
223
259
|
}
|
|
260
|
+
function makeConfirmAwaiter(reader) {
|
|
261
|
+
const delegate = makePromptDelegate(reader);
|
|
262
|
+
return async (tool, input, _toolUseId, suggestedPattern) => {
|
|
263
|
+
const result = await delegate(tool, input, suggestedPattern);
|
|
264
|
+
return result;
|
|
265
|
+
};
|
|
266
|
+
}
|
|
224
267
|
function stringifyInput(input) {
|
|
225
268
|
if (!input || typeof input !== "object") return "";
|
|
226
269
|
const obj = input;
|
|
@@ -236,8 +279,10 @@ function hasDiff(input) {
|
|
|
236
279
|
}
|
|
237
280
|
function hasApiKey(provider, config) {
|
|
238
281
|
if (provider.envVars.some((v) => !!process.env[v])) return true;
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
282
|
+
const entry = config?.providers?.[provider.id];
|
|
283
|
+
if (!entry) return false;
|
|
284
|
+
if (typeof entry.apiKey === "string" && entry.apiKey.length > 0) return true;
|
|
285
|
+
if (Array.isArray(entry.apiKeys) && entry.apiKeys.some((k) => k?.apiKey)) return true;
|
|
241
286
|
return false;
|
|
242
287
|
}
|
|
243
288
|
async function runPicker(deps) {
|
|
@@ -254,15 +299,53 @@ ${color.bold(theme2.primary("WrongStack") + color.dim(" \u2014 Provider & Model
|
|
|
254
299
|
return void 0;
|
|
255
300
|
}
|
|
256
301
|
const supported = providers.filter((p) => p.family !== "unsupported");
|
|
257
|
-
|
|
302
|
+
const catalogById = new Map(supported.map((p) => [p.id, p]));
|
|
303
|
+
const overlay = config?.providers ?? {};
|
|
304
|
+
const seen = /* @__PURE__ */ new Set();
|
|
305
|
+
const merged = [];
|
|
306
|
+
for (const p of supported) {
|
|
307
|
+
const cfg = overlay[p.id];
|
|
308
|
+
seen.add(p.id);
|
|
309
|
+
if (cfg) {
|
|
310
|
+
merged.push({
|
|
311
|
+
...p,
|
|
312
|
+
family: cfg.family ?? p.family,
|
|
313
|
+
apiBase: cfg.baseUrl ?? p.apiBase,
|
|
314
|
+
envVars: cfg.envVars && cfg.envVars.length > 0 ? cfg.envVars : p.envVars,
|
|
315
|
+
// When the user has saved an explicit model list, it wins — they
|
|
316
|
+
// know which models their endpoint actually serves (e.g. LM
|
|
317
|
+
// Studio, vLLM, or a proxy with custom model ids). Otherwise the
|
|
318
|
+
// catalog list keeps providing suggestions.
|
|
319
|
+
models: cfg.models && cfg.models.length > 0 ? cfg.models.map((m) => ({ id: m, name: m })) : p.models
|
|
320
|
+
});
|
|
321
|
+
} else {
|
|
322
|
+
merged.push(p);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
for (const [id, cfg] of Object.entries(overlay)) {
|
|
326
|
+
if (seen.has(id)) continue;
|
|
327
|
+
if (!cfg?.family || cfg.family === "unsupported") continue;
|
|
328
|
+
const catalogType = cfg.type && cfg.type !== id ? cfg.type : void 0;
|
|
329
|
+
const inherited = catalogType ? catalogById.get(catalogType) : void 0;
|
|
330
|
+
merged.push({
|
|
331
|
+
id,
|
|
332
|
+
name: inherited ? `${inherited.name} ${color.dim("(alias)")}` : id,
|
|
333
|
+
family: cfg.family,
|
|
334
|
+
apiBase: cfg.baseUrl ?? inherited?.apiBase,
|
|
335
|
+
envVars: cfg.envVars ?? inherited?.envVars ?? [],
|
|
336
|
+
models: cfg.models && cfg.models.length > 0 ? cfg.models.map((m) => ({ id: m, name: m })) : inherited?.models ?? [],
|
|
337
|
+
npm: inherited?.npm
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
if (merged.length === 0) {
|
|
258
341
|
renderer.writeError("No supported providers found in catalog.");
|
|
259
342
|
return void 0;
|
|
260
343
|
}
|
|
261
|
-
const keyed =
|
|
344
|
+
const keyed = merged.filter((p) => hasApiKey(p, config));
|
|
262
345
|
let displayList = keyed;
|
|
263
346
|
let showingFallback = false;
|
|
264
347
|
if (keyed.length === 0) {
|
|
265
|
-
displayList =
|
|
348
|
+
displayList = merged;
|
|
266
349
|
showingFallback = true;
|
|
267
350
|
}
|
|
268
351
|
const families = /* @__PURE__ */ new Map();
|
|
@@ -283,7 +366,8 @@ ${color.bold(theme2.primary("WrongStack") + color.dim(" \u2014 Provider & Model
|
|
|
283
366
|
`);
|
|
284
367
|
for (const p of list) {
|
|
285
368
|
const envFound = p.envVars.some((v) => !!process.env[v]);
|
|
286
|
-
const
|
|
369
|
+
const entry = config?.providers?.[p.id];
|
|
370
|
+
const configKey = typeof entry?.apiKey === "string" && entry.apiKey.length > 0 || Array.isArray(entry?.apiKeys) && entry.apiKeys.some((k) => k?.apiKey);
|
|
287
371
|
const marker = envFound ? color.green("\u25CF") : configKey ? color.cyan("\u25C9") : color.dim("\u25CB");
|
|
288
372
|
const isDefault = p.id === defaultProvider;
|
|
289
373
|
if (isDefault) defaultIdx = idx;
|
|
@@ -321,7 +405,7 @@ ${color.amber("?")} Select provider (1-${ordered.length})${defaultHint}: `)).tri
|
|
|
321
405
|
renderer.write(color.dim("Cancelled.\n"));
|
|
322
406
|
return void 0;
|
|
323
407
|
}
|
|
324
|
-
const providerIdx = parseInt(providerAnswer, 10);
|
|
408
|
+
const providerIdx = Number.parseInt(providerAnswer, 10);
|
|
325
409
|
if (Number.isNaN(providerIdx) || providerIdx < 1 || providerIdx > ordered.length) {
|
|
326
410
|
const byId = ordered.find((o) => o.provider.id.toLowerCase() === providerAnswer.toLowerCase());
|
|
327
411
|
if (!byId) {
|
|
@@ -399,7 +483,7 @@ ${color.amber("?")} Select model (1-${models.length})${defaultHint}: `)).trim();
|
|
|
399
483
|
return resolveModelSelection(answer, models, provider, registry, renderer);
|
|
400
484
|
}
|
|
401
485
|
async function resolveModelSelection(answer, models, provider, _registry, renderer, _reader) {
|
|
402
|
-
const idx = parseInt(answer, 10);
|
|
486
|
+
const idx = Number.parseInt(answer, 10);
|
|
403
487
|
let modelId;
|
|
404
488
|
if (!Number.isNaN(idx) && idx >= 1 && idx <= models.length) {
|
|
405
489
|
modelId = models[idx - 1].id;
|
|
@@ -432,17 +516,17 @@ async function resolveModelSelection(answer, models, provider, _registry, render
|
|
|
432
516
|
var theme2 = { primary: color.amber };
|
|
433
517
|
async function saveToGlobalConfig(configPath, provider, model) {
|
|
434
518
|
try {
|
|
435
|
-
const { atomicWrite:
|
|
436
|
-
const
|
|
519
|
+
const { atomicWrite: atomicWrite3 } = await import('@wrongstack/core');
|
|
520
|
+
const fs8 = await import('fs/promises');
|
|
437
521
|
let existing = {};
|
|
438
522
|
try {
|
|
439
|
-
const raw = await
|
|
523
|
+
const raw = await fs8.readFile(configPath, "utf8");
|
|
440
524
|
existing = JSON.parse(raw);
|
|
441
525
|
} catch {
|
|
442
526
|
}
|
|
443
527
|
existing.provider = provider;
|
|
444
528
|
existing.model = model;
|
|
445
|
-
await
|
|
529
|
+
await atomicWrite3(configPath, JSON.stringify(existing, null, 2));
|
|
446
530
|
return true;
|
|
447
531
|
} catch {
|
|
448
532
|
return false;
|
|
@@ -455,58 +539,103 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
455
539
|
clearCommand(opts),
|
|
456
540
|
compactCommand(opts),
|
|
457
541
|
contextCommand(opts),
|
|
458
|
-
usageCommand(opts),
|
|
459
542
|
toolsCommand(opts),
|
|
460
543
|
skillCommand(opts),
|
|
461
|
-
useCommand(opts),
|
|
462
|
-
modelCommand(opts),
|
|
463
544
|
diagCommand(opts),
|
|
464
545
|
statsCommand(opts),
|
|
546
|
+
spawnCommand(opts),
|
|
547
|
+
agentsCommand(opts),
|
|
548
|
+
metricsCommand(opts),
|
|
549
|
+
healthCommand(opts),
|
|
550
|
+
memoryCommand(opts),
|
|
465
551
|
saveCommand(opts),
|
|
466
552
|
loadCommand(opts),
|
|
467
553
|
exitCommand(opts)
|
|
468
554
|
];
|
|
469
555
|
}
|
|
556
|
+
function memoryCommand(opts) {
|
|
557
|
+
return {
|
|
558
|
+
name: "memory",
|
|
559
|
+
description: "Inspect or edit persistent memory: /memory [show|remember <text>|forget <query>|clear]",
|
|
560
|
+
async run(args) {
|
|
561
|
+
const store = opts.memoryStore;
|
|
562
|
+
if (!store) return { message: "No memory store configured." };
|
|
563
|
+
const [verb, ...rest] = args.trim().split(/\s+/);
|
|
564
|
+
const restJoined = rest.join(" ").trim();
|
|
565
|
+
switch (verb) {
|
|
566
|
+
case "":
|
|
567
|
+
case "show":
|
|
568
|
+
case "list": {
|
|
569
|
+
const text = await store.readAll();
|
|
570
|
+
return {
|
|
571
|
+
message: text.trim().length === 0 ? "Memory is empty. Add an entry with `/memory remember <text>`." : text
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
case "remember":
|
|
575
|
+
case "add": {
|
|
576
|
+
if (!restJoined) return { message: "Usage: /memory remember <text>" };
|
|
577
|
+
await store.remember(restJoined);
|
|
578
|
+
return { message: `Remembered: ${restJoined}` };
|
|
579
|
+
}
|
|
580
|
+
case "forget":
|
|
581
|
+
case "rm": {
|
|
582
|
+
if (!restJoined) return { message: "Usage: /memory forget <query>" };
|
|
583
|
+
const n = await store.forget(restJoined);
|
|
584
|
+
return {
|
|
585
|
+
message: n === 0 ? `No entries matched "${restJoined}".` : `Forgot ${n} entries.`
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
case "clear": {
|
|
589
|
+
await store.clear();
|
|
590
|
+
return { message: "Cleared all memory scopes." };
|
|
591
|
+
}
|
|
592
|
+
default:
|
|
593
|
+
return {
|
|
594
|
+
message: `Unknown subcommand "${verb}". Try: show | remember <text> | forget <query> | clear`
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
}
|
|
470
600
|
function initCommand(opts) {
|
|
471
601
|
return {
|
|
472
602
|
name: "init",
|
|
473
603
|
description: "Scaffold .wrongstack/AGENTS.md in the current project.",
|
|
474
604
|
async run(args, ctx) {
|
|
475
605
|
const force = args.trim() === "--force";
|
|
476
|
-
const dir =
|
|
477
|
-
const file =
|
|
606
|
+
const dir = path5.join(ctx.projectRoot, ".wrongstack");
|
|
607
|
+
const file = path5.join(dir, "AGENTS.md");
|
|
478
608
|
try {
|
|
479
|
-
await
|
|
609
|
+
await fs6.access(file);
|
|
480
610
|
if (!force) {
|
|
481
|
-
const
|
|
482
|
-
opts.renderer.writeWarning(
|
|
483
|
-
return { message:
|
|
611
|
+
const msg2 = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
|
|
612
|
+
opts.renderer.writeWarning(msg2);
|
|
613
|
+
return { message: msg2 };
|
|
484
614
|
}
|
|
485
615
|
} catch {
|
|
486
616
|
}
|
|
487
617
|
const detected = await detectProjectFacts(ctx.projectRoot);
|
|
488
618
|
const body = renderAgentsTemplate(detected);
|
|
489
|
-
await
|
|
490
|
-
await
|
|
619
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
620
|
+
await fs6.writeFile(file, body, "utf8");
|
|
491
621
|
if (detected.hints.length > 0) {
|
|
492
|
-
const
|
|
622
|
+
const msg2 = `Wrote ${file}
|
|
493
623
|
Pre-filled: ${detected.hints.join(", ")}. Edit the file to add anything else worth remembering.`;
|
|
494
624
|
opts.renderer.writeInfo(`Wrote ${file}`);
|
|
495
625
|
opts.renderer.writeInfo(`Pre-filled: ${detected.hints.join(", ")}. Edit the file to add anything else worth remembering.`);
|
|
496
|
-
return { message:
|
|
497
|
-
} else {
|
|
498
|
-
const msg = `Wrote ${file}
|
|
499
|
-
No project type auto-detected. Edit the file to add build/test commands and conventions.`;
|
|
500
|
-
opts.renderer.writeInfo(`Wrote ${file}`);
|
|
501
|
-
return { message: msg };
|
|
626
|
+
return { message: msg2 };
|
|
502
627
|
}
|
|
628
|
+
const msg = `Wrote ${file}
|
|
629
|
+
No project type auto-detected. Edit the file to add build/test commands and conventions.`;
|
|
630
|
+
opts.renderer.writeInfo(`Wrote ${file}`);
|
|
631
|
+
return { message: msg };
|
|
503
632
|
}
|
|
504
633
|
};
|
|
505
634
|
}
|
|
506
635
|
async function detectProjectFacts(root) {
|
|
507
636
|
const facts = { hints: [] };
|
|
508
637
|
try {
|
|
509
|
-
const pkg = JSON.parse(await
|
|
638
|
+
const pkg = JSON.parse(await fs6.readFile(path5.join(root, "package.json"), "utf8"));
|
|
510
639
|
const scripts = pkg.scripts ?? {};
|
|
511
640
|
const pm = (pkg.packageManager ?? "npm").split("@")[0] ?? "npm";
|
|
512
641
|
if (scripts["build"]) facts.build = `${pm} run build`;
|
|
@@ -517,28 +646,28 @@ async function detectProjectFacts(root) {
|
|
|
517
646
|
} catch {
|
|
518
647
|
}
|
|
519
648
|
try {
|
|
520
|
-
await
|
|
649
|
+
await fs6.access(path5.join(root, "pyproject.toml"));
|
|
521
650
|
facts.test ??= "pytest";
|
|
522
651
|
facts.lint ??= "ruff check .";
|
|
523
652
|
facts.hints.push("pyproject.toml");
|
|
524
653
|
} catch {
|
|
525
654
|
}
|
|
526
655
|
try {
|
|
527
|
-
await
|
|
656
|
+
await fs6.access(path5.join(root, "go.mod"));
|
|
528
657
|
facts.build ??= "go build ./...";
|
|
529
658
|
facts.test ??= "go test ./...";
|
|
530
659
|
facts.hints.push("go.mod");
|
|
531
660
|
} catch {
|
|
532
661
|
}
|
|
533
662
|
try {
|
|
534
|
-
await
|
|
663
|
+
await fs6.access(path5.join(root, "Cargo.toml"));
|
|
535
664
|
facts.build ??= "cargo build";
|
|
536
665
|
facts.test ??= "cargo test";
|
|
537
666
|
facts.hints.push("Cargo.toml");
|
|
538
667
|
} catch {
|
|
539
668
|
}
|
|
540
669
|
try {
|
|
541
|
-
await
|
|
670
|
+
await fs6.access(path5.join(root, "Makefile"));
|
|
542
671
|
facts.build ??= "make";
|
|
543
672
|
facts.test ??= "make test";
|
|
544
673
|
facts.hints.push("Makefile");
|
|
@@ -586,12 +715,8 @@ function diagCommand(opts) {
|
|
|
586
715
|
name: "diag",
|
|
587
716
|
description: "Show runtime diagnostics (provider, tokens, tools, MCP).",
|
|
588
717
|
async run() {
|
|
589
|
-
if (opts.onDiag) {
|
|
590
|
-
|
|
591
|
-
return { message: "diag" };
|
|
592
|
-
} else {
|
|
593
|
-
return { message: "Diag not available in this context." };
|
|
594
|
-
}
|
|
718
|
+
if (!opts.onDiag) return { message: "Diag not available in this context." };
|
|
719
|
+
return { message: opts.onDiag() };
|
|
595
720
|
}
|
|
596
721
|
};
|
|
597
722
|
}
|
|
@@ -600,28 +725,73 @@ function statsCommand(opts) {
|
|
|
600
725
|
name: "stats",
|
|
601
726
|
description: "Show session report: tokens, requests, tools, files, cost.",
|
|
602
727
|
async run() {
|
|
603
|
-
if (opts.onStats) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
} else {
|
|
607
|
-
return { message: "Stats not available in this context." };
|
|
608
|
-
}
|
|
728
|
+
if (!opts.onStats) return { message: "Stats not available in this context." };
|
|
729
|
+
const text = opts.onStats();
|
|
730
|
+
return { message: text ?? "No session activity recorded yet." };
|
|
609
731
|
}
|
|
610
732
|
};
|
|
611
733
|
}
|
|
612
734
|
function helpCommand(opts) {
|
|
613
735
|
return {
|
|
614
736
|
name: "help",
|
|
615
|
-
description: "Show available slash commands.",
|
|
616
|
-
|
|
737
|
+
description: "Show available slash commands. Pass a name for detailed help.",
|
|
738
|
+
help: [
|
|
739
|
+
"Usage:",
|
|
740
|
+
" /help List every command with its one-line description.",
|
|
741
|
+
" /help <name> Show detailed help for one command (falls back to the description).",
|
|
742
|
+
"",
|
|
743
|
+
"Examples:",
|
|
744
|
+
" /help",
|
|
745
|
+
" /help context",
|
|
746
|
+
" /help model"
|
|
747
|
+
].join("\n"),
|
|
748
|
+
async run(args) {
|
|
749
|
+
const query = args.trim();
|
|
750
|
+
if (query) {
|
|
751
|
+
const needle = query.startsWith("/") ? query.slice(1) : query;
|
|
752
|
+
let match;
|
|
753
|
+
for (const entry of opts.registry.listWithOwner()) {
|
|
754
|
+
const aliases = entry.cmd.aliases ?? [];
|
|
755
|
+
const candidates = [
|
|
756
|
+
entry.cmd.name,
|
|
757
|
+
entry.fullName,
|
|
758
|
+
...aliases,
|
|
759
|
+
...aliases.map(
|
|
760
|
+
(a) => entry.owner === "core" ? a : `${entry.owner}:${a}`
|
|
761
|
+
)
|
|
762
|
+
];
|
|
763
|
+
if (candidates.includes(needle)) {
|
|
764
|
+
match = entry;
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (!match) {
|
|
769
|
+
return { message: `Unknown command: /${needle}. Run /help to list commands.` };
|
|
770
|
+
}
|
|
771
|
+
const prefix = match.owner === "core" ? "" : `${match.owner}:`;
|
|
772
|
+
const header = `/${prefix}${match.cmd.name}`;
|
|
773
|
+
const aliasLine = match.cmd.aliases && match.cmd.aliases.length > 0 ? `Aliases: ${match.cmd.aliases.map((a) => `/${prefix}${a}`).join(", ")}
|
|
774
|
+
` : "";
|
|
775
|
+
const body = match.cmd.help ?? match.cmd.description;
|
|
776
|
+
return {
|
|
777
|
+
message: [
|
|
778
|
+
header,
|
|
779
|
+
"\u2500".repeat(header.length),
|
|
780
|
+
aliasLine + (match.cmd.help ? "" : `${match.cmd.description}
|
|
781
|
+
`),
|
|
782
|
+
body
|
|
783
|
+
].filter(Boolean).join("\n")
|
|
784
|
+
};
|
|
785
|
+
}
|
|
617
786
|
const lines = ["Available slash commands:"];
|
|
618
|
-
for (const { cmd, owner
|
|
787
|
+
for (const { cmd, owner } of opts.registry.listWithOwner()) {
|
|
619
788
|
const isBuiltin = owner === "core";
|
|
620
789
|
const prefix = isBuiltin ? "" : `${owner}:`;
|
|
621
790
|
const aliases = cmd.aliases ? cmd.aliases.map((a) => `/${prefix}${a}`).join(", ") : "";
|
|
622
791
|
const aliasStr = aliases ? ` (${aliases})` : "";
|
|
623
792
|
lines.push(` /${prefix}${cmd.name}${aliasStr} \u2014 ${cmd.description}`);
|
|
624
793
|
}
|
|
794
|
+
lines.push("", "Run `/help <name>` for detailed help on a specific command.");
|
|
625
795
|
return { message: lines.join("\n") };
|
|
626
796
|
}
|
|
627
797
|
};
|
|
@@ -630,6 +800,14 @@ function clearCommand(opts) {
|
|
|
630
800
|
return {
|
|
631
801
|
name: "clear",
|
|
632
802
|
description: "Reset the session and start a new one.",
|
|
803
|
+
help: [
|
|
804
|
+
"Usage:",
|
|
805
|
+
" /clear",
|
|
806
|
+
"",
|
|
807
|
+
"Wipes everything in the current REPL state: messages, todos, read-file tracking,",
|
|
808
|
+
"file mtimes, meta. Memory store entries (all scopes) are cleared too. The terminal",
|
|
809
|
+
"is wiped. Use this when you want a fresh conversation without restarting `wstack`."
|
|
810
|
+
].join("\n"),
|
|
633
811
|
async run(_args, ctx) {
|
|
634
812
|
if (ctx) {
|
|
635
813
|
ctx.messages = [];
|
|
@@ -652,6 +830,14 @@ function contextCommand(opts) {
|
|
|
652
830
|
name: "context",
|
|
653
831
|
aliases: ["ctx"],
|
|
654
832
|
description: "Show context window summary.",
|
|
833
|
+
help: [
|
|
834
|
+
"Usage:",
|
|
835
|
+
" /context Show counts: messages, est. tokens, tool calls, todos, read files.",
|
|
836
|
+
" /context detail As above, plus model, cwd, projectRoot, and the file list.",
|
|
837
|
+
"",
|
|
838
|
+
"Token estimate is a `chars \xF7 4` heuristic, not a real tokenizer call \u2014",
|
|
839
|
+
"good enough to spot growth between turns."
|
|
840
|
+
].join("\n"),
|
|
655
841
|
async run(args, ctx) {
|
|
656
842
|
const messages = ctx.messages;
|
|
657
843
|
const detailed = args.trim() === "detail";
|
|
@@ -732,6 +918,14 @@ function compactCommand(opts) {
|
|
|
732
918
|
return {
|
|
733
919
|
name: "compact",
|
|
734
920
|
description: "Compact the context window.",
|
|
921
|
+
help: [
|
|
922
|
+
"Usage:",
|
|
923
|
+
" /compact Run the configured compactor with default settings.",
|
|
924
|
+
" /compact aggressive Compact more aggressively \u2014 keeps fewer recent turns verbatim.",
|
|
925
|
+
"",
|
|
926
|
+
"The compactor summarizes older turns to reclaim tokens. The default keeps the most",
|
|
927
|
+
"recent K message pairs untouched; aggressive halves that window."
|
|
928
|
+
].join("\n"),
|
|
735
929
|
async run(args, ctx) {
|
|
736
930
|
if (!opts.compactor) {
|
|
737
931
|
const msg2 = "No compactor configured.";
|
|
@@ -746,26 +940,6 @@ function compactCommand(opts) {
|
|
|
746
940
|
}
|
|
747
941
|
};
|
|
748
942
|
}
|
|
749
|
-
function usageCommand(opts) {
|
|
750
|
-
return {
|
|
751
|
-
name: "usage",
|
|
752
|
-
aliases: ["cost"],
|
|
753
|
-
description: "Show token usage and estimated cost.",
|
|
754
|
-
async run() {
|
|
755
|
-
const total = opts.tokenCounter.total();
|
|
756
|
-
const cost = opts.tokenCounter.estimateCost();
|
|
757
|
-
const msg = `${color.bold("Usage")}
|
|
758
|
-
input: ${total.input}
|
|
759
|
-
output: ${total.output}
|
|
760
|
-
cache read: ${total.cacheRead ?? 0}
|
|
761
|
-
cache write: ${total.cacheWrite ?? 0}
|
|
762
|
-
cost: $${cost.total.toFixed(4)} (input $${cost.input.toFixed(4)} / output $${cost.output.toFixed(4)})
|
|
763
|
-
`;
|
|
764
|
-
opts.renderer.write(msg);
|
|
765
|
-
return { message: msg };
|
|
766
|
-
}
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
943
|
function toolsCommand(opts) {
|
|
770
944
|
return {
|
|
771
945
|
name: "tools",
|
|
@@ -803,47 +977,14 @@ function skillCommand(opts) {
|
|
|
803
977
|
${lines.join("\n")}
|
|
804
978
|
`;
|
|
805
979
|
return { message: msg };
|
|
806
|
-
} else {
|
|
807
|
-
const skill = await opts.skillLoader.find(args.trim());
|
|
808
|
-
if (!skill) {
|
|
809
|
-
const msg = `Skill "${args.trim()}" not found.`;
|
|
810
|
-
return { message: msg };
|
|
811
|
-
}
|
|
812
|
-
const body = await opts.skillLoader.readBody(skill.name);
|
|
813
|
-
return { message: body };
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
function useCommand(opts) {
|
|
819
|
-
return {
|
|
820
|
-
name: "use",
|
|
821
|
-
description: "Switch provider mid-session: /use <provider>",
|
|
822
|
-
async run(args) {
|
|
823
|
-
const name = args.trim();
|
|
824
|
-
if (!name) {
|
|
825
|
-
const msg2 = "Usage: /use <provider-name>";
|
|
826
|
-
return { message: msg2 };
|
|
827
980
|
}
|
|
828
|
-
opts.
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
function modelCommand(opts) {
|
|
835
|
-
return {
|
|
836
|
-
name: "model",
|
|
837
|
-
description: "Switch model mid-session: /model <model>",
|
|
838
|
-
async run(args) {
|
|
839
|
-
const name = args.trim();
|
|
840
|
-
if (!name) {
|
|
841
|
-
const msg2 = "Usage: /model <model-name>";
|
|
842
|
-
return { message: msg2 };
|
|
981
|
+
const skill = await opts.skillLoader.find(args.trim());
|
|
982
|
+
if (!skill) {
|
|
983
|
+
const msg = `Skill "${args.trim()}" not found.`;
|
|
984
|
+
return { message: msg };
|
|
843
985
|
}
|
|
844
|
-
opts.
|
|
845
|
-
|
|
846
|
-
return { message: msg };
|
|
986
|
+
const body = await opts.skillLoader.readBody(skill.name);
|
|
987
|
+
return { message: body };
|
|
847
988
|
}
|
|
848
989
|
};
|
|
849
990
|
}
|
|
@@ -901,6 +1042,102 @@ function exitCommand(opts) {
|
|
|
901
1042
|
}
|
|
902
1043
|
};
|
|
903
1044
|
}
|
|
1045
|
+
function metricsCommand(opts) {
|
|
1046
|
+
return {
|
|
1047
|
+
name: "metrics",
|
|
1048
|
+
description: "Show metrics snapshot (requires --metrics flag).",
|
|
1049
|
+
async run() {
|
|
1050
|
+
if (!opts.metricsSink) {
|
|
1051
|
+
return { message: "Metrics not enabled. Restart with --metrics to collect." };
|
|
1052
|
+
}
|
|
1053
|
+
const snap = opts.metricsSink.snapshot();
|
|
1054
|
+
if (snap.series.length === 0) {
|
|
1055
|
+
return { message: "No metrics recorded yet." };
|
|
1056
|
+
}
|
|
1057
|
+
const lines = [];
|
|
1058
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1059
|
+
for (const s of snap.series) {
|
|
1060
|
+
const bucket = byName.get(s.name) ?? [];
|
|
1061
|
+
bucket.push(s);
|
|
1062
|
+
byName.set(s.name, bucket);
|
|
1063
|
+
}
|
|
1064
|
+
for (const [name, series] of [...byName.entries()].sort()) {
|
|
1065
|
+
lines.push(color.dim(`# ${name}`));
|
|
1066
|
+
for (const s of series) {
|
|
1067
|
+
const labels = Object.entries(s.labels).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
1068
|
+
const labelStr = labels ? color.dim(` {${labels}}`) : "";
|
|
1069
|
+
if (s.type === "histogram") {
|
|
1070
|
+
lines.push(
|
|
1071
|
+
` count=${s.values.count} sum=${s.values.sum} min=${s.values.min} max=${s.values.max} p50=${s.values.p50} p95=${s.values.p95} p99=${s.values.p99}${labelStr}`
|
|
1072
|
+
);
|
|
1073
|
+
} else {
|
|
1074
|
+
lines.push(` ${s.values.value}${labelStr}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
const msg = lines.join("\n");
|
|
1079
|
+
return { message: msg };
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function healthCommand(opts) {
|
|
1084
|
+
return {
|
|
1085
|
+
name: "health",
|
|
1086
|
+
description: "Run health checks (requires --metrics flag).",
|
|
1087
|
+
async run() {
|
|
1088
|
+
if (!opts.healthRegistry) {
|
|
1089
|
+
return { message: "Health checks not enabled. Restart with --metrics." };
|
|
1090
|
+
}
|
|
1091
|
+
const result = await opts.healthRegistry.run();
|
|
1092
|
+
const lines = [
|
|
1093
|
+
`${statusIcon(result.status)} overall: ${result.status}`,
|
|
1094
|
+
...result.checks.map((c) => {
|
|
1095
|
+
const detail = c.detail ? color.dim(` \u2014 ${c.detail}`) : "";
|
|
1096
|
+
return ` ${statusIcon(c.status)} ${c.name}: ${c.status}${detail}`;
|
|
1097
|
+
})
|
|
1098
|
+
];
|
|
1099
|
+
return { message: lines.join("\n") };
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function statusIcon(status) {
|
|
1104
|
+
if (status === "healthy") return color.green("\u25CF");
|
|
1105
|
+
if (status === "degraded") return color.yellow("\u25CF");
|
|
1106
|
+
return color.red("\u25CF");
|
|
1107
|
+
}
|
|
1108
|
+
function spawnCommand(opts) {
|
|
1109
|
+
return {
|
|
1110
|
+
name: "spawn",
|
|
1111
|
+
description: "Spawn an isolated subagent to handle a task. Usage: /spawn <task description>",
|
|
1112
|
+
async run(args) {
|
|
1113
|
+
const description = args.trim();
|
|
1114
|
+
if (!description) return { message: "Usage: /spawn <task description>" };
|
|
1115
|
+
if (!opts.onSpawn) {
|
|
1116
|
+
return { message: "Multi-agent is not enabled in this session." };
|
|
1117
|
+
}
|
|
1118
|
+
try {
|
|
1119
|
+
const summary = await opts.onSpawn(description);
|
|
1120
|
+
return { message: summary };
|
|
1121
|
+
} catch (err) {
|
|
1122
|
+
return {
|
|
1123
|
+
message: `Spawn failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function agentsCommand(opts) {
|
|
1130
|
+
return {
|
|
1131
|
+
name: "agents",
|
|
1132
|
+
description: "Show status of spawned subagents (pending + completed tasks).",
|
|
1133
|
+
async run() {
|
|
1134
|
+
if (!opts.onAgents) {
|
|
1135
|
+
return { message: "Multi-agent is not enabled in this session." };
|
|
1136
|
+
}
|
|
1137
|
+
return { message: opts.onAgents() };
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
904
1141
|
|
|
905
1142
|
// src/pre-launch.ts
|
|
906
1143
|
var MANIFESTS = [
|
|
@@ -917,13 +1154,13 @@ var MANIFESTS = [
|
|
|
917
1154
|
];
|
|
918
1155
|
async function detectProjectKind(projectRoot) {
|
|
919
1156
|
try {
|
|
920
|
-
await
|
|
1157
|
+
await fs6.access(path5.join(projectRoot, ".wrongstack", "AGENTS.md"));
|
|
921
1158
|
return "initialized";
|
|
922
1159
|
} catch {
|
|
923
1160
|
}
|
|
924
1161
|
for (const m of MANIFESTS) {
|
|
925
1162
|
try {
|
|
926
|
-
await
|
|
1163
|
+
await fs6.access(path5.join(projectRoot, m));
|
|
927
1164
|
return "project";
|
|
928
1165
|
} catch {
|
|
929
1166
|
}
|
|
@@ -931,12 +1168,12 @@ async function detectProjectKind(projectRoot) {
|
|
|
931
1168
|
return "empty";
|
|
932
1169
|
}
|
|
933
1170
|
async function scaffoldAgentsMd(projectRoot) {
|
|
934
|
-
const dir =
|
|
935
|
-
const file =
|
|
1171
|
+
const dir = path5.join(projectRoot, ".wrongstack");
|
|
1172
|
+
const file = path5.join(dir, "AGENTS.md");
|
|
936
1173
|
const facts = await detectProjectFacts(projectRoot);
|
|
937
1174
|
const body = renderAgentsTemplate(facts);
|
|
938
|
-
await
|
|
939
|
-
await
|
|
1175
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
1176
|
+
await fs6.writeFile(file, body, "utf8");
|
|
940
1177
|
return file;
|
|
941
1178
|
}
|
|
942
1179
|
async function runProjectCheck(opts) {
|
|
@@ -945,7 +1182,7 @@ async function runProjectCheck(opts) {
|
|
|
945
1182
|
if (kind === "initialized") {
|
|
946
1183
|
renderer.write(
|
|
947
1184
|
`
|
|
948
|
-
${color.green("\u2713")} Project initialized ${color.dim(`(${
|
|
1185
|
+
${color.green("\u2713")} Project initialized ${color.dim(`(${path5.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
|
|
949
1186
|
`
|
|
950
1187
|
);
|
|
951
1188
|
return true;
|
|
@@ -1214,14 +1451,14 @@ function summarize(value, name) {
|
|
|
1214
1451
|
if (typeof v === "object" && v !== null) {
|
|
1215
1452
|
const o = v;
|
|
1216
1453
|
if (name === "edit") {
|
|
1217
|
-
const
|
|
1454
|
+
const path7 = typeof o["path"] === "string" ? o["path"] : "";
|
|
1218
1455
|
const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
|
|
1219
|
-
return `${
|
|
1456
|
+
return `${path7} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
|
|
1220
1457
|
}
|
|
1221
1458
|
if (name === "write") {
|
|
1222
|
-
const
|
|
1459
|
+
const path7 = typeof o["path"] === "string" ? o["path"] : "";
|
|
1223
1460
|
const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
|
|
1224
|
-
return bytes !== void 0 ? `${
|
|
1461
|
+
return bytes !== void 0 ? `${path7} ${bytes}B` : path7;
|
|
1225
1462
|
}
|
|
1226
1463
|
if (typeof o["count"] === "number") {
|
|
1227
1464
|
return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
|
|
@@ -1229,6 +1466,37 @@ function summarize(value, name) {
|
|
|
1229
1466
|
}
|
|
1230
1467
|
return "";
|
|
1231
1468
|
}
|
|
1469
|
+
var req = createRequire(import.meta.url);
|
|
1470
|
+
function readOwnVersion() {
|
|
1471
|
+
const candidates = ["../package.json", "../../package.json"];
|
|
1472
|
+
for (const rel of candidates) {
|
|
1473
|
+
try {
|
|
1474
|
+
const pkg = req(rel);
|
|
1475
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
|
|
1476
|
+
} catch {
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return "dev";
|
|
1480
|
+
}
|
|
1481
|
+
var CLI_VERSION = readOwnVersion();
|
|
1482
|
+
var API_VERSION = "0.0.0";
|
|
1483
|
+
try {
|
|
1484
|
+
const corePkg = req("@wrongstack/core/package.json");
|
|
1485
|
+
if (corePkg.wrongstackApiVersion) API_VERSION = corePkg.wrongstackApiVersion;
|
|
1486
|
+
} catch {
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// src/utils.ts
|
|
1490
|
+
function fmtTok(n) {
|
|
1491
|
+
if (n < 1e3) return String(n);
|
|
1492
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
1493
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
1494
|
+
}
|
|
1495
|
+
function patchConfig(base, patch) {
|
|
1496
|
+
return Object.freeze({ ...base, ...patch });
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// src/repl.ts
|
|
1232
1500
|
async function runRepl(opts) {
|
|
1233
1501
|
if (opts.banner !== false) printBanner(opts.renderer);
|
|
1234
1502
|
let activeCtrl;
|
|
@@ -1289,9 +1557,13 @@ async function runRepl(opts) {
|
|
|
1289
1557
|
if (result.status === "aborted") {
|
|
1290
1558
|
opts.renderer.writeWarning("Aborted.");
|
|
1291
1559
|
} else if (result.status === "failed") {
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1560
|
+
const err = result.error;
|
|
1561
|
+
if (err) {
|
|
1562
|
+
const tag = err.recoverable ? " (recoverable)" : "";
|
|
1563
|
+
opts.renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
|
|
1564
|
+
} else {
|
|
1565
|
+
opts.renderer.writeError("Failed.");
|
|
1566
|
+
}
|
|
1295
1567
|
} else if (result.status === "max_iterations") {
|
|
1296
1568
|
opts.renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
|
|
1297
1569
|
}
|
|
@@ -1338,11 +1610,6 @@ async function readPossiblyMultiline(opts) {
|
|
|
1338
1610
|
}
|
|
1339
1611
|
return buf;
|
|
1340
1612
|
}
|
|
1341
|
-
function fmtTok(n) {
|
|
1342
|
-
if (n < 1e3) return String(n);
|
|
1343
|
-
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
1344
|
-
return `${(n / 1e6).toFixed(1)}M`;
|
|
1345
|
-
}
|
|
1346
1613
|
var FILLED = "\u2588";
|
|
1347
1614
|
var EMPTY = "\u2591";
|
|
1348
1615
|
function renderContextChip(used, max) {
|
|
@@ -1359,7 +1626,7 @@ function renderProgress(ratio, width) {
|
|
|
1359
1626
|
}
|
|
1360
1627
|
function printBanner(renderer) {
|
|
1361
1628
|
const lines = [
|
|
1362
|
-
theme.primary(theme.bold("WrongStack")) + color.dim(
|
|
1629
|
+
theme.primary(theme.bold("WrongStack")) + color.dim(` v${CLI_VERSION}`),
|
|
1363
1630
|
color.dim("Built on the wrong stack. Shipped anyway."),
|
|
1364
1631
|
color.dim("Type /help for commands, /exit to quit."),
|
|
1365
1632
|
""
|
|
@@ -1401,11 +1668,11 @@ var SessionStats = class {
|
|
|
1401
1668
|
if (e.name === "bash") this.bashCommands++;
|
|
1402
1669
|
else if (e.name === "fetch") this.fetches++;
|
|
1403
1670
|
if (!e.ok) return;
|
|
1404
|
-
const
|
|
1405
|
-
if (e.name === "read" &&
|
|
1406
|
-
else if (e.name === "edit" &&
|
|
1407
|
-
else if (e.name === "write" &&
|
|
1408
|
-
this.writtenPaths.add(
|
|
1671
|
+
const path7 = typeof input?.path === "string" ? input.path : void 0;
|
|
1672
|
+
if (e.name === "read" && path7) this.readPaths.add(path7);
|
|
1673
|
+
else if (e.name === "edit" && path7) this.editedPaths.add(path7);
|
|
1674
|
+
else if (e.name === "write" && path7) {
|
|
1675
|
+
this.writtenPaths.add(path7);
|
|
1409
1676
|
const content = typeof input?.content === "string" ? input.content : "";
|
|
1410
1677
|
this.bytesWritten += Buffer.byteLength(content, "utf8");
|
|
1411
1678
|
}
|
|
@@ -1414,8 +1681,15 @@ var SessionStats = class {
|
|
|
1414
1681
|
hasActivity() {
|
|
1415
1682
|
return this.apiRequests > 0 || this.iterations > 0 || this.toolStats.size > 0 || this.tokenCounter.total().input > 0;
|
|
1416
1683
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1684
|
+
/**
|
|
1685
|
+
* Build the report string. Returns null when there's no recorded
|
|
1686
|
+
* activity yet — caller decides whether to emit a placeholder or stay
|
|
1687
|
+
* silent. Splitting `format()` out of `render()` lets the TUI's slash
|
|
1688
|
+
* dispatcher take the string and turn it into a history entry, while
|
|
1689
|
+
* REPL keeps the old direct-write path.
|
|
1690
|
+
*/
|
|
1691
|
+
format() {
|
|
1692
|
+
if (!this.hasActivity()) return null;
|
|
1419
1693
|
const u = this.tokenCounter.total();
|
|
1420
1694
|
const cost = this.tokenCounter.estimateCost();
|
|
1421
1695
|
const elapsedSec = ((Date.now() - this.startedAt) / 1e3).toFixed(1);
|
|
@@ -1430,12 +1704,12 @@ var SessionStats = class {
|
|
|
1430
1704
|
lines.push(` Errors: ${color.yellow(String(this.errors))}`);
|
|
1431
1705
|
}
|
|
1432
1706
|
lines.push("");
|
|
1433
|
-
lines.push(` Tokens: in ${
|
|
1707
|
+
lines.push(` Tokens: in ${fmtTok(u.input)} out ${fmtTok(u.output)}${u.cacheRead ? ` cacheR ${fmtTok(u.cacheRead)}` : ""}${u.cacheWrite ? ` cacheW ${fmtTok(u.cacheWrite)}` : ""}`);
|
|
1434
1708
|
const cache = this.tokenCounter.cacheStats();
|
|
1435
1709
|
if (cache.readTokens > 0 || cache.writeTokens > 0) {
|
|
1436
1710
|
const pct = (cache.hitRatio * 100).toFixed(1);
|
|
1437
1711
|
lines.push(
|
|
1438
|
-
` Prompt cache: ${pct}% hit ${color.dim(`(${
|
|
1712
|
+
` Prompt cache: ${pct}% hit ${color.dim(`(${fmtTok(cache.readTokens)} read / ${fmtTok(cache.writeTokens)} write)`)}`
|
|
1439
1713
|
);
|
|
1440
1714
|
}
|
|
1441
1715
|
if (cost.total > 0) {
|
|
@@ -1476,15 +1750,15 @@ var SessionStats = class {
|
|
|
1476
1750
|
if (this.fetches > 0) lines.push(` Web fetches: ${this.fetches}`);
|
|
1477
1751
|
}
|
|
1478
1752
|
lines.push("");
|
|
1479
|
-
|
|
1753
|
+
return lines.join("\n");
|
|
1754
|
+
}
|
|
1755
|
+
render(renderer) {
|
|
1756
|
+
const text = this.format();
|
|
1757
|
+
if (text === null) return;
|
|
1758
|
+
renderer.write(`${text}
|
|
1480
1759
|
`);
|
|
1481
1760
|
}
|
|
1482
1761
|
};
|
|
1483
|
-
function fmtTok2(n) {
|
|
1484
|
-
if (n < 1e3) return String(n);
|
|
1485
|
-
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
1486
|
-
return `${(n / 1e6).toFixed(1)}M`;
|
|
1487
|
-
}
|
|
1488
1762
|
function samplePaths(set) {
|
|
1489
1763
|
const arr = [...set];
|
|
1490
1764
|
if (arr.length <= 2) return arr.join(", ");
|
|
@@ -1555,7 +1829,7 @@ function renderContextChip2(ctx) {
|
|
|
1555
1829
|
const pct = Math.round(ratio * 100);
|
|
1556
1830
|
const chipColor = ratio >= 0.85 ? color.red : ratio >= 0.65 ? color.yellow : color.cyan;
|
|
1557
1831
|
const bar = renderProgress2(ratio, 8);
|
|
1558
|
-
return color.dim("ctx ") + chipColor(bar) + chipColor(` ${pct}%`) + color.dim(` (${
|
|
1832
|
+
return color.dim("ctx ") + chipColor(bar) + chipColor(` ${pct}%`) + color.dim(` (${fmtTok(ctx.used)}/${fmtTok(ctx.max)})`);
|
|
1559
1833
|
}
|
|
1560
1834
|
function renderProgress2(ratio, width) {
|
|
1561
1835
|
const clamped = Math.max(0, Math.min(1, ratio));
|
|
@@ -1563,92 +1837,888 @@ function renderProgress2(ratio, width) {
|
|
|
1563
1837
|
const capped = Math.min(width, filled);
|
|
1564
1838
|
return FILLED2.repeat(capped) + EMPTY2.repeat(width - capped);
|
|
1565
1839
|
}
|
|
1566
|
-
function
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1840
|
+
async function bootConfig(flags) {
|
|
1841
|
+
const cwd = typeof flags["cwd"] === "string" ? path5.resolve(flags["cwd"]) : process.cwd();
|
|
1842
|
+
const pathResolver = new DefaultPathResolver(cwd);
|
|
1843
|
+
const projectRoot = pathResolver.projectRoot;
|
|
1844
|
+
const userHome = os3.homedir();
|
|
1845
|
+
const wpaths = resolveWstackPaths({ projectRoot, userHome });
|
|
1846
|
+
await ensureProjectMeta(wpaths, projectRoot);
|
|
1847
|
+
const vault = new DefaultSecretVault({ keyFile: wpaths.secretsKey });
|
|
1848
|
+
for (const file of [wpaths.globalConfig, wpaths.projectLocalConfig]) {
|
|
1849
|
+
try {
|
|
1850
|
+
const { migrated } = await migratePlaintextSecrets(file, vault);
|
|
1851
|
+
if (migrated > 0) {
|
|
1852
|
+
process.stderr.write(`[wstack] Encrypted ${migrated} plaintext secret(s) in ${file}
|
|
1853
|
+
`);
|
|
1854
|
+
}
|
|
1855
|
+
} catch {
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
const configLoader = new DefaultConfigLoader({ paths: wpaths, vault });
|
|
1859
|
+
const config = await configLoader.load({ cliFlags: flagsToConfigPatch(flags) });
|
|
1860
|
+
return {
|
|
1861
|
+
paths: { cwd, projectRoot, userHome, wpaths, pathResolver },
|
|
1862
|
+
config,
|
|
1863
|
+
vault
|
|
1864
|
+
};
|
|
1570
1865
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
models: modelsCmd,
|
|
1583
|
-
mcp: mcpCmd,
|
|
1584
|
-
plugin: pluginCmd,
|
|
1585
|
-
diag: diagCmd,
|
|
1586
|
-
usage: usageCmd,
|
|
1587
|
-
version: versionCmd,
|
|
1588
|
-
help: helpCmd,
|
|
1589
|
-
projects: projectsCmd
|
|
1590
|
-
};
|
|
1591
|
-
async function authCmd(args, deps) {
|
|
1592
|
-
const flags = parseAuthFlags(args);
|
|
1593
|
-
let providerId = flags.positional[0];
|
|
1594
|
-
if (!providerId) {
|
|
1595
|
-
providerId = (await deps.reader.readLine("Provider id: ")).trim();
|
|
1866
|
+
function flagsToConfigPatch(flags) {
|
|
1867
|
+
const patch = {};
|
|
1868
|
+
if (typeof flags["provider"] === "string") patch.provider = flags["provider"];
|
|
1869
|
+
if (typeof flags["model"] === "string") patch.model = flags["model"];
|
|
1870
|
+
if (typeof flags["cwd"] === "string") patch.cwd = flags["cwd"];
|
|
1871
|
+
if (typeof flags["log-level"] === "string") {
|
|
1872
|
+
patch.log = { level: flags["log-level"] };
|
|
1873
|
+
} else if (flags["verbose"]) {
|
|
1874
|
+
patch.log = { level: "debug" };
|
|
1875
|
+
} else if (flags["trace"]) {
|
|
1876
|
+
patch.log = { level: "trace" };
|
|
1596
1877
|
}
|
|
1597
|
-
if (
|
|
1598
|
-
|
|
1599
|
-
|
|
1878
|
+
if (flags["yolo"]) patch.yolo = true;
|
|
1879
|
+
if (flags["no-features"]) {
|
|
1880
|
+
patch.features = {
|
|
1881
|
+
mcp: false,
|
|
1882
|
+
plugins: false,
|
|
1883
|
+
memory: false,
|
|
1884
|
+
modelsRegistry: false,
|
|
1885
|
+
skills: false
|
|
1886
|
+
};
|
|
1600
1887
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1888
|
+
return patch;
|
|
1889
|
+
}
|
|
1890
|
+
async function ensureProjectMeta(paths, projectRoot) {
|
|
1604
1891
|
try {
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
}
|
|
1892
|
+
await fs6.mkdir(paths.projectDir, { recursive: true });
|
|
1893
|
+
const meta = {
|
|
1894
|
+
hash: paths.projectHash,
|
|
1895
|
+
root: projectRoot,
|
|
1896
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
1897
|
+
};
|
|
1898
|
+
await fs6.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
|
|
1611
1899
|
} catch {
|
|
1612
1900
|
}
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1901
|
+
}
|
|
1902
|
+
var MultiAgentHost = class {
|
|
1903
|
+
constructor(deps) {
|
|
1904
|
+
this.deps = deps;
|
|
1905
|
+
}
|
|
1906
|
+
deps;
|
|
1907
|
+
coordinator;
|
|
1908
|
+
pending = /* @__PURE__ */ new Map();
|
|
1909
|
+
results = [];
|
|
1910
|
+
async ensureCoordinator() {
|
|
1911
|
+
if (this.coordinator) return this.coordinator;
|
|
1912
|
+
const config = this.deps.configStore.get();
|
|
1913
|
+
const factory = async (subCfg) => {
|
|
1914
|
+
const events = new EventBus();
|
|
1915
|
+
const provider = await this.buildSubagentProvider(config);
|
|
1916
|
+
const baseSystem = await this.deps.systemPromptBuilder.build({
|
|
1917
|
+
cwd: this.deps.cwd,
|
|
1918
|
+
projectRoot: this.deps.projectRoot,
|
|
1919
|
+
tools: this.filterTools(subCfg.tools),
|
|
1920
|
+
model: subCfg.model ?? config.model,
|
|
1921
|
+
provider: config.provider
|
|
1922
|
+
});
|
|
1923
|
+
const parentSession = this.deps.session;
|
|
1924
|
+
const subSession = {
|
|
1925
|
+
id: parentSession.id,
|
|
1926
|
+
append: (ev) => parentSession.append({ ...ev })
|
|
1927
|
+
};
|
|
1928
|
+
const ctx = new Context({
|
|
1929
|
+
systemPrompt: baseSystem,
|
|
1930
|
+
provider,
|
|
1931
|
+
session: subSession,
|
|
1932
|
+
signal: new AbortController().signal,
|
|
1933
|
+
tokenCounter: this.deps.tokenCounter,
|
|
1934
|
+
cwd: this.deps.cwd,
|
|
1935
|
+
projectRoot: this.deps.projectRoot,
|
|
1936
|
+
model: subCfg.model ?? config.model,
|
|
1937
|
+
tools: this.filterTools(subCfg.tools)
|
|
1938
|
+
});
|
|
1939
|
+
const agent = new Agent({
|
|
1940
|
+
container: this.deps.container,
|
|
1941
|
+
tools: this.subagentToolRegistry(subCfg.tools),
|
|
1942
|
+
providers: this.deps.providerRegistry,
|
|
1943
|
+
events,
|
|
1944
|
+
pipelines: createDefaultPipelines(),
|
|
1945
|
+
context: ctx
|
|
1946
|
+
});
|
|
1947
|
+
return { agent, events };
|
|
1948
|
+
};
|
|
1949
|
+
const runner = makeAgentSubagentRunner({ factory });
|
|
1950
|
+
this.coordinator = new DefaultMultiAgentCoordinator(
|
|
1951
|
+
{
|
|
1952
|
+
coordinatorId: randomUUID(),
|
|
1953
|
+
doneCondition: { type: "all_tasks_done" },
|
|
1954
|
+
maxConcurrent: 2,
|
|
1955
|
+
defaultBudget: { maxToolCalls: 20, maxIterations: 20, timeoutMs: 12e4 }
|
|
1956
|
+
},
|
|
1957
|
+
{ runner }
|
|
1616
1958
|
);
|
|
1617
|
-
|
|
1959
|
+
this.coordinator.on(
|
|
1960
|
+
"task.completed",
|
|
1961
|
+
({ task, result }) => {
|
|
1962
|
+
this.results.push(result);
|
|
1963
|
+
this.pending.delete(task.id);
|
|
1964
|
+
}
|
|
1965
|
+
);
|
|
1966
|
+
return this.coordinator;
|
|
1618
1967
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1968
|
+
async buildSubagentProvider(config) {
|
|
1969
|
+
const newCfg = config.providers?.[config.provider] ?? {
|
|
1970
|
+
type: config.provider,
|
|
1971
|
+
apiKey: config.apiKey,
|
|
1972
|
+
baseUrl: config.baseUrl
|
|
1973
|
+
};
|
|
1974
|
+
return makeProviderFromConfig(config.provider, {
|
|
1975
|
+
...newCfg,
|
|
1976
|
+
type: config.provider
|
|
1977
|
+
});
|
|
1625
1978
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1979
|
+
/** Returns a tool slice for the subagent — full set unless restricted. */
|
|
1980
|
+
filterTools(allow) {
|
|
1981
|
+
const all = this.deps.toolRegistry.list();
|
|
1982
|
+
if (!allow || allow.length === 0) return all;
|
|
1983
|
+
const allowSet = new Set(allow);
|
|
1984
|
+
return all.filter((t) => allowSet.has(t.name));
|
|
1985
|
+
}
|
|
1986
|
+
subagentToolRegistry(allow) {
|
|
1987
|
+
if (!allow || allow.length === 0) return this.deps.toolRegistry;
|
|
1988
|
+
const cloneCtor = this.deps.toolRegistry.constructor;
|
|
1989
|
+
const sub = new cloneCtor();
|
|
1990
|
+
for (const t of this.filterTools(allow)) sub.register(t);
|
|
1991
|
+
return sub;
|
|
1992
|
+
}
|
|
1993
|
+
/** Spawn a fresh subagent and assign a single task. Returns task id. */
|
|
1994
|
+
async spawn(description) {
|
|
1995
|
+
const coord = await this.ensureCoordinator();
|
|
1996
|
+
const spawned = await coord.spawn({
|
|
1997
|
+
name: "adhoc",
|
|
1998
|
+
role: "general",
|
|
1999
|
+
maxToolCalls: 20,
|
|
2000
|
+
maxIterations: 20
|
|
2001
|
+
});
|
|
2002
|
+
const taskId = randomUUID();
|
|
2003
|
+
this.pending.set(taskId, { description, subagentId: spawned.subagentId });
|
|
2004
|
+
await coord.assign({
|
|
2005
|
+
id: taskId,
|
|
2006
|
+
description,
|
|
2007
|
+
subagentId: spawned.subagentId,
|
|
2008
|
+
maxToolCalls: 20
|
|
2009
|
+
});
|
|
2010
|
+
return { subagentId: spawned.subagentId, taskId };
|
|
2011
|
+
}
|
|
2012
|
+
status() {
|
|
2013
|
+
const pending = Array.from(this.pending.entries()).map(([taskId, v]) => ({
|
|
2014
|
+
taskId,
|
|
2015
|
+
description: v.description,
|
|
2016
|
+
subagentId: v.subagentId
|
|
2017
|
+
}));
|
|
2018
|
+
const summary = !this.coordinator ? "No subagents have been spawned." : `${pending.length} pending, ${this.results.length} completed.`;
|
|
2019
|
+
return { pending, completed: this.results, summary };
|
|
2020
|
+
}
|
|
2021
|
+
async stopAll() {
|
|
2022
|
+
if (this.coordinator) {
|
|
2023
|
+
await this.coordinator.stopAll();
|
|
1635
2024
|
}
|
|
1636
|
-
};
|
|
1637
|
-
try {
|
|
1638
|
-
await rewriteConfigEncrypted(deps.paths.globalConfig, deps.vault, patch);
|
|
1639
|
-
deps.renderer.writeInfo(`Stored encrypted key for ${providerId}.`);
|
|
1640
|
-
deps.renderer.writeInfo(`Use: wstack --provider ${providerId} "<task>"`);
|
|
1641
|
-
return 0;
|
|
1642
|
-
} catch (err) {
|
|
1643
|
-
deps.renderer.writeError(`auth: ${err instanceof Error ? err.message : String(err)}`);
|
|
1644
|
-
return 1;
|
|
1645
2025
|
}
|
|
1646
|
-
}
|
|
1647
|
-
function
|
|
2026
|
+
};
|
|
2027
|
+
async function runAuthMenu(deps) {
|
|
2028
|
+
for (; ; ) {
|
|
2029
|
+
const providers = await loadProviders(deps);
|
|
2030
|
+
renderTopMenu(deps.renderer, providers);
|
|
2031
|
+
const ids = Object.keys(providers).sort();
|
|
2032
|
+
const choice = (await deps.reader.readLine(`
|
|
2033
|
+
${color.amber("?")} Pick: `)).trim().toLowerCase();
|
|
2034
|
+
if (!choice || choice === "q" || choice === "quit" || choice === "exit") {
|
|
2035
|
+
deps.renderer.write(color.dim("Done.\n"));
|
|
2036
|
+
return 0;
|
|
2037
|
+
}
|
|
2038
|
+
if (choice === "a" || choice === "add") {
|
|
2039
|
+
await addForNewProvider(deps);
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
if (choice === "c" || choice === "custom") {
|
|
2043
|
+
await addCustomProvider(deps);
|
|
2044
|
+
continue;
|
|
2045
|
+
}
|
|
2046
|
+
const idx = Number.parseInt(choice, 10);
|
|
2047
|
+
if (!Number.isNaN(idx) && idx >= 1 && idx <= ids.length) {
|
|
2048
|
+
const pid = ids[idx - 1];
|
|
2049
|
+
await manageProvider(pid, deps);
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
const byId = ids.find((id) => id.toLowerCase() === choice);
|
|
2053
|
+
if (byId) {
|
|
2054
|
+
await manageProvider(byId, deps);
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
2057
|
+
deps.renderer.writeError(`Unknown selection: "${choice}"`);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
function renderTopMenu(renderer, providers) {
|
|
2061
|
+
renderer.write(`
|
|
2062
|
+
${color.bold("WrongStack")} ${color.dim("\u2014 API keys")}
|
|
2063
|
+
|
|
2064
|
+
`);
|
|
2065
|
+
const ids = Object.keys(providers).sort();
|
|
2066
|
+
if (ids.length === 0) {
|
|
2067
|
+
renderer.write(color.dim(" No providers configured yet.\n"));
|
|
2068
|
+
} else {
|
|
2069
|
+
renderer.write(` ${color.dim("Saved providers:")}
|
|
2070
|
+
`);
|
|
2071
|
+
let idx = 1;
|
|
2072
|
+
for (const id of ids) {
|
|
2073
|
+
const cfg = providers[id];
|
|
2074
|
+
const keys = normalizeKeys(cfg);
|
|
2075
|
+
const active = activeLabel(cfg, keys);
|
|
2076
|
+
const summary = keys.length === 0 ? color.dim("(no keys)") : keys.length === 1 ? maskedKey(keys[0].apiKey) : `${color.dim(`${keys.length} keys`)} ${color.dim("active:")} ${color.bold(active ?? "?")} ${maskedKey(keys.find((k) => k.label === active)?.apiKey ?? keys[0].apiKey)}`;
|
|
2077
|
+
const fam = cfg.family ? color.dim(`[${cfg.family}]`) : "";
|
|
2078
|
+
const aliasHint = cfg.type && cfg.type !== id ? color.dim(`\u2192 ${cfg.type}`) : "";
|
|
2079
|
+
renderer.write(
|
|
2080
|
+
` ${color.dim(`${idx}.`.padStart(4))} ${id.padEnd(22)} ${fam} ${aliasHint} ${summary}
|
|
2081
|
+
`
|
|
2082
|
+
);
|
|
2083
|
+
idx++;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
renderer.write(`
|
|
2087
|
+
${color.dim("Actions:")}
|
|
2088
|
+
`);
|
|
2089
|
+
renderer.write(` ${color.bold("a")} Add key for a new provider (from catalog)
|
|
2090
|
+
`);
|
|
2091
|
+
renderer.write(` ${color.bold("c")} Add custom provider (type + family + baseUrl)
|
|
2092
|
+
`);
|
|
2093
|
+
renderer.write(` ${color.bold("q")} Quit
|
|
2094
|
+
`);
|
|
2095
|
+
if (ids.length > 0) {
|
|
2096
|
+
renderer.write(color.dim(`
|
|
2097
|
+
Pick a number to manage that provider's keys.
|
|
2098
|
+
`));
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
async function manageProvider(providerId, deps) {
|
|
2102
|
+
for (; ; ) {
|
|
2103
|
+
const providers = await loadProviders(deps);
|
|
2104
|
+
const cfg = providers[providerId];
|
|
2105
|
+
if (!cfg) {
|
|
2106
|
+
deps.renderer.writeError(`Provider "${providerId}" no longer in config.`);
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
const keys = normalizeKeys(cfg);
|
|
2110
|
+
const active = activeLabel(cfg, keys);
|
|
2111
|
+
deps.renderer.write(`
|
|
2112
|
+
${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.amber("[no family]")}
|
|
2113
|
+
`);
|
|
2114
|
+
deps.renderer.write(
|
|
2115
|
+
color.dim(` type: ${cfg.type ?? providerId}
|
|
2116
|
+
`) + color.dim(` family: ${cfg.family ?? "(unset \u2192 resolved from models.dev when type matches)"}
|
|
2117
|
+
`) + color.dim(` baseUrl: ${cfg.baseUrl ?? "(unset \u2192 catalog default)"}
|
|
2118
|
+
`)
|
|
2119
|
+
);
|
|
2120
|
+
if (cfg.envVars && cfg.envVars.length > 0) {
|
|
2121
|
+
deps.renderer.write(color.dim(` envVars: ${cfg.envVars.join(", ")}
|
|
2122
|
+
`));
|
|
2123
|
+
}
|
|
2124
|
+
if (cfg.models && cfg.models.length > 0) {
|
|
2125
|
+
deps.renderer.write(color.dim(` models: ${cfg.models.join(", ")}
|
|
2126
|
+
`));
|
|
2127
|
+
}
|
|
2128
|
+
if (keys.length === 0) {
|
|
2129
|
+
deps.renderer.write(color.dim(" (no keys saved)\n"));
|
|
2130
|
+
} else {
|
|
2131
|
+
for (let i = 0; i < keys.length; i++) {
|
|
2132
|
+
const k = keys[i];
|
|
2133
|
+
const marker = k.label === active ? color.green("\u25CF") : color.dim("\u25CB");
|
|
2134
|
+
deps.renderer.write(
|
|
2135
|
+
` ${color.dim(`${i + 1}.`.padStart(4))} ${marker} ${k.label.padEnd(20)} ${maskedKey(k.apiKey)} ${color.dim(k.createdAt)}
|
|
2136
|
+
`
|
|
2137
|
+
);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
deps.renderer.write(`
|
|
2141
|
+
${color.dim("Actions:")}
|
|
2142
|
+
`);
|
|
2143
|
+
deps.renderer.write(` ${color.bold("a")} Add another key
|
|
2144
|
+
`);
|
|
2145
|
+
if (keys.length > 0) {
|
|
2146
|
+
deps.renderer.write(` ${color.bold("u")} <n> Update key <n>
|
|
2147
|
+
`);
|
|
2148
|
+
deps.renderer.write(` ${color.bold("d")} <n> Delete key <n>
|
|
2149
|
+
`);
|
|
2150
|
+
deps.renderer.write(` ${color.bold("s")} <n> Set key <n> as active
|
|
2151
|
+
`);
|
|
2152
|
+
}
|
|
2153
|
+
deps.renderer.write(` ${color.bold("f")} Edit family
|
|
2154
|
+
`);
|
|
2155
|
+
deps.renderer.write(` ${color.bold("B")} Edit baseUrl
|
|
2156
|
+
`);
|
|
2157
|
+
deps.renderer.write(` ${color.bold("m")} Edit visible model list
|
|
2158
|
+
`);
|
|
2159
|
+
deps.renderer.write(` ${color.bold("x")} Remove this provider entirely
|
|
2160
|
+
`);
|
|
2161
|
+
deps.renderer.write(` ${color.bold("b")} Back
|
|
2162
|
+
`);
|
|
2163
|
+
const raw = (await deps.reader.readLine(`
|
|
2164
|
+
${color.amber("?")} ${providerId} > `)).trim();
|
|
2165
|
+
if (!raw || raw === "b" || raw === "back") return;
|
|
2166
|
+
const [verb, argRaw] = raw.split(/\s+/, 2);
|
|
2167
|
+
const arg = argRaw ? Number.parseInt(argRaw, 10) : Number.NaN;
|
|
2168
|
+
if (verb === "a" || verb === "add") {
|
|
2169
|
+
await addKeyForProvider(providerId, deps, cfg);
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
2172
|
+
if (verb === "x" || verb === "remove") {
|
|
2173
|
+
const confirm = (await deps.reader.readLine(
|
|
2174
|
+
` ${color.amber("?")} Remove provider "${providerId}" and ${keys.length} key(s)? ${color.dim("[y/N]")} `
|
|
2175
|
+
)).trim().toLowerCase();
|
|
2176
|
+
if (confirm === "y" || confirm === "yes") {
|
|
2177
|
+
await mutateProviders(deps, (all) => {
|
|
2178
|
+
delete all[providerId];
|
|
2179
|
+
});
|
|
2180
|
+
deps.renderer.write(` ${color.green("\u2713")} Removed ${providerId}.
|
|
2181
|
+
`);
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
2186
|
+
if (verb === "u" || verb === "update") {
|
|
2187
|
+
if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
|
|
2188
|
+
deps.renderer.writeError(`Usage: u <1-${keys.length}>`);
|
|
2189
|
+
continue;
|
|
2190
|
+
}
|
|
2191
|
+
const target = keys[arg - 1];
|
|
2192
|
+
const newKey = await readKeyInput(deps, `Updated key for ${target.label}`);
|
|
2193
|
+
if (!newKey) continue;
|
|
2194
|
+
await mutateProviders(deps, (all) => {
|
|
2195
|
+
const p = all[providerId];
|
|
2196
|
+
if (!p) return;
|
|
2197
|
+
const list = normalizeKeys(p).map(
|
|
2198
|
+
(k) => k.label === target.label ? { ...k, apiKey: newKey, createdAt: nowIso() } : k
|
|
2199
|
+
);
|
|
2200
|
+
writeKeysBack(p, list);
|
|
2201
|
+
});
|
|
2202
|
+
deps.renderer.write(` ${color.green("\u2713")} Updated ${providerId}/${target.label}.
|
|
2203
|
+
`);
|
|
2204
|
+
continue;
|
|
2205
|
+
}
|
|
2206
|
+
if (verb === "d" || verb === "delete" || verb === "rm") {
|
|
2207
|
+
if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
|
|
2208
|
+
deps.renderer.writeError(`Usage: d <1-${keys.length}>`);
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2211
|
+
const target = keys[arg - 1];
|
|
2212
|
+
const confirm = (await deps.reader.readLine(
|
|
2213
|
+
` ${color.amber("?")} Delete key "${target.label}" (${maskedKey(target.apiKey)})? ${color.dim("[y/N]")} `
|
|
2214
|
+
)).trim().toLowerCase();
|
|
2215
|
+
if (confirm !== "y" && confirm !== "yes") continue;
|
|
2216
|
+
await mutateProviders(deps, (all) => {
|
|
2217
|
+
const p = all[providerId];
|
|
2218
|
+
if (!p) return;
|
|
2219
|
+
const list = normalizeKeys(p).filter((k) => k.label !== target.label);
|
|
2220
|
+
writeKeysBack(p, list);
|
|
2221
|
+
if (p.activeKey === target.label) {
|
|
2222
|
+
p.activeKey = list[0]?.label;
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
2225
|
+
deps.renderer.write(` ${color.green("\u2713")} Deleted ${providerId}/${target.label}.
|
|
2226
|
+
`);
|
|
2227
|
+
continue;
|
|
2228
|
+
}
|
|
2229
|
+
if (verb === "f" || verb === "family") {
|
|
2230
|
+
const current = cfg.family ?? "";
|
|
2231
|
+
const ans = (await deps.reader.readLine(
|
|
2232
|
+
` ${color.amber("?")} Family ${color.dim(`(anthropic | openai | openai-compatible | google, empty = unset, current: ${current || "unset"})`)}: `
|
|
2233
|
+
)).trim();
|
|
2234
|
+
if (ans !== "" && !["anthropic", "openai", "openai-compatible", "google"].includes(ans)) {
|
|
2235
|
+
deps.renderer.writeError(`Invalid family: "${ans}"`);
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
await mutateProviders(deps, (all) => {
|
|
2239
|
+
const p = all[providerId];
|
|
2240
|
+
if (!p) return;
|
|
2241
|
+
if (ans === "") delete p.family;
|
|
2242
|
+
else p.family = ans;
|
|
2243
|
+
});
|
|
2244
|
+
deps.renderer.write(` ${color.green("\u2713")} family \u2192 ${ans || "(unset)"}
|
|
2245
|
+
`);
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
if (verb === "B" || verb === "baseurl" || verb === "base-url") {
|
|
2249
|
+
const current = cfg.baseUrl ?? "";
|
|
2250
|
+
const ans = (await deps.reader.readLine(
|
|
2251
|
+
` ${color.amber("?")} Base URL ${color.dim(`(empty = unset, current: ${current || "unset"})`)}: `
|
|
2252
|
+
)).trim();
|
|
2253
|
+
await mutateProviders(deps, (all) => {
|
|
2254
|
+
const p = all[providerId];
|
|
2255
|
+
if (!p) return;
|
|
2256
|
+
if (ans === "") delete p.baseUrl;
|
|
2257
|
+
else p.baseUrl = ans;
|
|
2258
|
+
});
|
|
2259
|
+
deps.renderer.write(` ${color.green("\u2713")} baseUrl \u2192 ${ans || "(unset)"}
|
|
2260
|
+
`);
|
|
2261
|
+
continue;
|
|
2262
|
+
}
|
|
2263
|
+
if (verb === "m" || verb === "models") {
|
|
2264
|
+
const current = (cfg.models ?? []).join(", ");
|
|
2265
|
+
const ans = (await deps.reader.readLine(
|
|
2266
|
+
` ${color.amber("?")} Model ids ${color.dim(`(comma-separated, empty = catalog default, current: ${current || "none"})`)}: `
|
|
2267
|
+
)).trim();
|
|
2268
|
+
const list = ans ? ans.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
2269
|
+
await mutateProviders(deps, (all) => {
|
|
2270
|
+
const p = all[providerId];
|
|
2271
|
+
if (!p) return;
|
|
2272
|
+
if (list.length === 0) delete p.models;
|
|
2273
|
+
else p.models = list;
|
|
2274
|
+
});
|
|
2275
|
+
deps.renderer.write(` ${color.green("\u2713")} models \u2192 ${list.length === 0 ? "(catalog default)" : list.join(", ")}
|
|
2276
|
+
`);
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
if (verb === "s" || verb === "set" || verb === "active") {
|
|
2280
|
+
if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
|
|
2281
|
+
deps.renderer.writeError(`Usage: s <1-${keys.length}>`);
|
|
2282
|
+
continue;
|
|
2283
|
+
}
|
|
2284
|
+
const target = keys[arg - 1];
|
|
2285
|
+
await mutateProviders(deps, (all) => {
|
|
2286
|
+
const p = all[providerId];
|
|
2287
|
+
if (!p) return;
|
|
2288
|
+
const list = normalizeKeys(p);
|
|
2289
|
+
writeKeysBack(p, list);
|
|
2290
|
+
p.activeKey = target.label;
|
|
2291
|
+
});
|
|
2292
|
+
deps.renderer.write(` ${color.green("\u2713")} Active key for ${providerId} \u2192 ${color.bold(target.label)}.
|
|
2293
|
+
`);
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2296
|
+
deps.renderer.writeError(`Unknown action: "${raw}"`);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
async function addForNewProvider(deps) {
|
|
2300
|
+
let catalog = [];
|
|
2301
|
+
try {
|
|
2302
|
+
catalog = (await deps.modelsRegistry.listProviders()).filter((p) => p.family !== "unsupported");
|
|
2303
|
+
} catch {
|
|
2304
|
+
deps.renderer.writeWarning("Catalog unavailable \u2014 falling back to manual entry.");
|
|
2305
|
+
}
|
|
2306
|
+
if (catalog.length === 0) {
|
|
2307
|
+
const pid = (await deps.reader.readLine(` ${color.amber("?")} Provider id: `)).trim();
|
|
2308
|
+
if (!pid) return;
|
|
2309
|
+
const fam = (await deps.reader.readLine(
|
|
2310
|
+
` ${color.amber("?")} Family (anthropic/openai/openai-compatible/google): `
|
|
2311
|
+
)).trim();
|
|
2312
|
+
const baseUrl2 = (await deps.reader.readLine(
|
|
2313
|
+
` ${color.amber("?")} Base URL ${color.dim("(optional)")}: `
|
|
2314
|
+
)).trim();
|
|
2315
|
+
await addKeyForProvider(pid, deps, {
|
|
2316
|
+
type: pid,
|
|
2317
|
+
family: fam || void 0,
|
|
2318
|
+
...baseUrl2 ? { baseUrl: baseUrl2 } : {}
|
|
2319
|
+
});
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
const saved = new Set(Object.keys(await loadProviders(deps)));
|
|
2323
|
+
deps.renderer.write(
|
|
2324
|
+
color.dim(` Catalog has ${catalog.length} providers. Filter by name to narrow, or "s" for unsaved-only.
|
|
2325
|
+
`)
|
|
2326
|
+
);
|
|
2327
|
+
const filterRaw = (await deps.reader.readLine(
|
|
2328
|
+
` ${color.amber("?")} Filter ${color.dim('(substring of id/name, "s" for unsaved-only, empty = all)')}: `
|
|
2329
|
+
)).trim();
|
|
2330
|
+
const filterLc = filterRaw.toLowerCase();
|
|
2331
|
+
const showUnsavedOnly = filterLc === "s" || filterLc === "unsaved";
|
|
2332
|
+
const matches = (p) => {
|
|
2333
|
+
if (showUnsavedOnly) return !saved.has(p.id);
|
|
2334
|
+
if (!filterLc) return true;
|
|
2335
|
+
return p.id.toLowerCase().includes(filterLc) || p.name.toLowerCase().includes(filterLc);
|
|
2336
|
+
};
|
|
2337
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
2338
|
+
let filteredCount = 0;
|
|
2339
|
+
for (const p of catalog) {
|
|
2340
|
+
if (!matches(p)) continue;
|
|
2341
|
+
filteredCount++;
|
|
2342
|
+
const list = byFamily.get(p.family) ?? [];
|
|
2343
|
+
list.push(p);
|
|
2344
|
+
byFamily.set(p.family, list);
|
|
2345
|
+
}
|
|
2346
|
+
if (filteredCount === 0) {
|
|
2347
|
+
deps.renderer.writeError(
|
|
2348
|
+
`No providers match "${filterRaw}". Try a shorter substring or check \`wstack providers\` for valid ids.`
|
|
2349
|
+
);
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
if (filterRaw && !showUnsavedOnly) {
|
|
2353
|
+
deps.renderer.write(
|
|
2354
|
+
color.dim(` ${filteredCount} match${filteredCount === 1 ? "" : "es"} for "${filterRaw}".
|
|
2355
|
+
`)
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
const ordered = [];
|
|
2359
|
+
const familyOrder = ["anthropic", "openai", "google", "openai-compatible"];
|
|
2360
|
+
let idx = 1;
|
|
2361
|
+
deps.renderer.write("\n");
|
|
2362
|
+
for (const fam of familyOrder) {
|
|
2363
|
+
const list = byFamily.get(fam);
|
|
2364
|
+
if (!list || list.length === 0) continue;
|
|
2365
|
+
deps.renderer.write(` ${color.bold(fam)}
|
|
2366
|
+
`);
|
|
2367
|
+
for (const p of list) {
|
|
2368
|
+
const savedMark = saved.has(p.id) ? color.cyan("\u25C9") : color.dim("\u25CB");
|
|
2369
|
+
const env = p.envVars[0] ? color.dim(`[${p.envVars[0]}]`) : "";
|
|
2370
|
+
deps.renderer.write(
|
|
2371
|
+
` ${color.dim(`${idx}.`.padStart(4))} ${savedMark} ${p.id.padEnd(22)} ${color.dim(p.name)} ${env}
|
|
2372
|
+
`
|
|
2373
|
+
);
|
|
2374
|
+
ordered.push(p);
|
|
2375
|
+
idx++;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
deps.renderer.write(`
|
|
2379
|
+
${color.dim("\u25C9 already saved \u25CB no key yet")}
|
|
2380
|
+
`);
|
|
2381
|
+
const answer = (await deps.reader.readLine(
|
|
2382
|
+
`
|
|
2383
|
+
${color.amber("?")} Pick (1-${ordered.length}) or type provider id: `
|
|
2384
|
+
)).trim();
|
|
2385
|
+
if (!answer) return;
|
|
2386
|
+
let chosen;
|
|
2387
|
+
const num = Number.parseInt(answer, 10);
|
|
2388
|
+
if (!Number.isNaN(num) && num >= 1 && num <= ordered.length) {
|
|
2389
|
+
chosen = ordered[num - 1];
|
|
2390
|
+
} else {
|
|
2391
|
+
chosen = ordered.find((p) => p.id.toLowerCase() === answer.toLowerCase()) ?? catalog.find((p) => p.id.toLowerCase() === answer.toLowerCase());
|
|
2392
|
+
}
|
|
2393
|
+
if (!chosen) {
|
|
2394
|
+
deps.renderer.writeError(`No such provider: "${answer}"`);
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
deps.renderer.write(
|
|
2398
|
+
color.dim(`
|
|
2399
|
+
Defaults from models.dev \u2014 press Enter to keep, or type a new value.
|
|
2400
|
+
`)
|
|
2401
|
+
);
|
|
2402
|
+
const famRaw = (await deps.reader.readLine(
|
|
2403
|
+
` ${color.amber("?")} Family ${color.dim(`[${chosen.family}]`)}: `
|
|
2404
|
+
)).trim();
|
|
2405
|
+
let family = chosen.family;
|
|
2406
|
+
if (famRaw) {
|
|
2407
|
+
if (!["anthropic", "openai", "openai-compatible", "google"].includes(famRaw)) {
|
|
2408
|
+
deps.renderer.writeError(`Invalid family: "${famRaw}" (must be anthropic | openai | openai-compatible | google).`);
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
family = famRaw;
|
|
2412
|
+
}
|
|
2413
|
+
const baseRaw = (await deps.reader.readLine(
|
|
2414
|
+
` ${color.amber("?")} Base URL ${color.dim(`[${chosen.apiBase ?? "unset"}]`)}: `
|
|
2415
|
+
)).trim();
|
|
2416
|
+
const baseUrl = baseRaw || chosen.apiBase;
|
|
2417
|
+
const providersNow = await loadProviders(deps);
|
|
2418
|
+
let suggestedAlias = chosen.id;
|
|
2419
|
+
if (family !== chosen.family) {
|
|
2420
|
+
let candidate = `${chosen.id}-${family}`;
|
|
2421
|
+
let n = 2;
|
|
2422
|
+
while (providersNow[candidate]) {
|
|
2423
|
+
candidate = `${chosen.id}-${family}-${n}`;
|
|
2424
|
+
n++;
|
|
2425
|
+
}
|
|
2426
|
+
suggestedAlias = candidate;
|
|
2427
|
+
}
|
|
2428
|
+
const aliasRaw = (await deps.reader.readLine(
|
|
2429
|
+
` ${color.amber("?")} Save under alias ${color.dim(`[${suggestedAlias}]`)} ${color.dim("(used as `--provider <alias>`)")}: `
|
|
2430
|
+
)).trim();
|
|
2431
|
+
const alias = aliasRaw || suggestedAlias;
|
|
2432
|
+
const existing = providersNow[alias];
|
|
2433
|
+
if (existing) {
|
|
2434
|
+
const sameFamily = (existing.family ?? chosen.family) === family;
|
|
2435
|
+
const sameBase = (existing.baseUrl ?? chosen.apiBase) === baseUrl;
|
|
2436
|
+
if (!sameFamily || !sameBase) {
|
|
2437
|
+
deps.renderer.writeError(
|
|
2438
|
+
`Alias "${alias}" already exists with different family/baseUrl.
|
|
2439
|
+
Existing: family=${existing.family ?? "(unset)"}, baseUrl=${existing.baseUrl ?? "(unset)"}
|
|
2440
|
+
New: family=${family}, baseUrl=${baseUrl ?? "(unset)"}
|
|
2441
|
+
Pick a different alias to keep them separate.`
|
|
2442
|
+
);
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
await addKeyForProvider(alias, deps, {
|
|
2447
|
+
type: chosen.id,
|
|
2448
|
+
family,
|
|
2449
|
+
baseUrl,
|
|
2450
|
+
envVars: chosen.envVars
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
async function addCustomProvider(deps) {
|
|
2454
|
+
deps.renderer.write(`
|
|
2455
|
+
${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies not in the models.dev catalog.")}
|
|
2456
|
+
`);
|
|
2457
|
+
const type = (await deps.reader.readLine(
|
|
2458
|
+
` ${color.amber("?")} Provider id ${color.dim('(e.g. "local-llama", "my-proxy")')}: `
|
|
2459
|
+
)).trim();
|
|
2460
|
+
if (!type) return;
|
|
2461
|
+
const existing = (await loadProviders(deps))[type];
|
|
2462
|
+
if (existing) {
|
|
2463
|
+
deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
const familyRaw = (await deps.reader.readLine(
|
|
2467
|
+
` ${color.amber("?")} Wire family ${color.dim("(anthropic | openai | openai-compatible | google)")}: `
|
|
2468
|
+
)).trim();
|
|
2469
|
+
if (!["anthropic", "openai", "openai-compatible", "google"].includes(familyRaw)) {
|
|
2470
|
+
deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
const family = familyRaw;
|
|
2474
|
+
const baseUrl = (await deps.reader.readLine(
|
|
2475
|
+
` ${color.amber("?")} Base URL ${color.dim("(e.g. http://localhost:11434/v1, leave empty if not needed)")}: `
|
|
2476
|
+
)).trim();
|
|
2477
|
+
const modelsRaw = (await deps.reader.readLine(
|
|
2478
|
+
` ${color.amber("?")} Model ids ${color.dim("(comma-separated, optional)")}: `
|
|
2479
|
+
)).trim();
|
|
2480
|
+
const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
2481
|
+
const envVarsRaw = (await deps.reader.readLine(
|
|
2482
|
+
` ${color.amber("?")} Env var names ${color.dim("(comma-separated, optional fallback for the key)")}: `
|
|
2483
|
+
)).trim();
|
|
2484
|
+
const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
2485
|
+
await addKeyForProvider(type, deps, {
|
|
2486
|
+
type,
|
|
2487
|
+
family,
|
|
2488
|
+
...baseUrl ? { baseUrl } : {},
|
|
2489
|
+
...models ? { models } : {},
|
|
2490
|
+
...envVars ? { envVars } : {}
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
async function addKeyForProvider(providerId, deps, template) {
|
|
2494
|
+
const providers = await loadProviders(deps);
|
|
2495
|
+
const existing = providers[providerId];
|
|
2496
|
+
const existingKeys = existing ? normalizeKeys(existing) : [];
|
|
2497
|
+
const usedLabels = new Set(existingKeys.map((k) => k.label));
|
|
2498
|
+
let defaultLabel = "default";
|
|
2499
|
+
if (usedLabels.has(defaultLabel)) {
|
|
2500
|
+
let n = 2;
|
|
2501
|
+
while (usedLabels.has(`key${n}`)) n++;
|
|
2502
|
+
defaultLabel = `key${n}`;
|
|
2503
|
+
}
|
|
2504
|
+
const labelRaw = (await deps.reader.readLine(
|
|
2505
|
+
` ${color.amber("?")} Label for this key ${color.dim(`[${defaultLabel}]`)}: `
|
|
2506
|
+
)).trim();
|
|
2507
|
+
const label = labelRaw || defaultLabel;
|
|
2508
|
+
if (usedLabels.has(label)) {
|
|
2509
|
+
deps.renderer.writeError(`Label "${label}" already used for ${providerId}. Use update (u) instead.`);
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
|
|
2513
|
+
if (!apiKey) {
|
|
2514
|
+
deps.renderer.writeError("No key entered. Nothing saved.");
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
await mutateProviders(deps, (all) => {
|
|
2518
|
+
const existingProv = all[providerId] ?? { type: providerId, ...template };
|
|
2519
|
+
if (!existingProv.type) existingProv.type = providerId;
|
|
2520
|
+
if (!existingProv.family && template.family) existingProv.family = template.family;
|
|
2521
|
+
if (!existingProv.baseUrl && template.baseUrl) existingProv.baseUrl = template.baseUrl;
|
|
2522
|
+
if (!existingProv.envVars && template.envVars) existingProv.envVars = template.envVars;
|
|
2523
|
+
const list = normalizeKeys(existingProv);
|
|
2524
|
+
list.push({ label, apiKey, createdAt: nowIso() });
|
|
2525
|
+
writeKeysBack(existingProv, list);
|
|
2526
|
+
if (!existingProv.activeKey) existingProv.activeKey = label;
|
|
2527
|
+
all[providerId] = existingProv;
|
|
2528
|
+
});
|
|
2529
|
+
deps.renderer.write(
|
|
2530
|
+
` ${color.green("\u2713")} Saved ${color.bold(providerId)}/${color.bold(label)}. ${color.dim("Use `wstack --provider " + providerId + ' "<task>"` to launch.')}
|
|
2531
|
+
`
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
async function runAuthDirect(deps, opts) {
|
|
2535
|
+
const { providerId } = opts;
|
|
2536
|
+
const providers = await loadProviders(deps);
|
|
2537
|
+
const existing = providers[providerId];
|
|
2538
|
+
if (!existing && !opts.family) {
|
|
2539
|
+
let knownFamily;
|
|
2540
|
+
let knownBase;
|
|
2541
|
+
let knownEnv;
|
|
2542
|
+
try {
|
|
2543
|
+
const k = await deps.modelsRegistry.getProvider(providerId);
|
|
2544
|
+
if (k) {
|
|
2545
|
+
knownFamily = k.family;
|
|
2546
|
+
knownBase = k.apiBase;
|
|
2547
|
+
knownEnv = k.envVars;
|
|
2548
|
+
}
|
|
2549
|
+
} catch {
|
|
2550
|
+
}
|
|
2551
|
+
if (!knownFamily || knownFamily === "unsupported") {
|
|
2552
|
+
deps.renderer.writeError(
|
|
2553
|
+
`Provider "${providerId}" not in catalog. Pass --family <anthropic|openai|openai-compatible|google>.`
|
|
2554
|
+
);
|
|
2555
|
+
return 1;
|
|
2556
|
+
}
|
|
2557
|
+
opts.family = knownFamily;
|
|
2558
|
+
opts.baseUrl ??= knownBase;
|
|
2559
|
+
opts.envVars ??= knownEnv;
|
|
2560
|
+
}
|
|
2561
|
+
const usedLabels = new Set(
|
|
2562
|
+
existing ? normalizeKeys(existing).map((k) => k.label) : []
|
|
2563
|
+
);
|
|
2564
|
+
let label = opts.label ?? "default";
|
|
2565
|
+
if (usedLabels.has(label)) {
|
|
2566
|
+
let n = 2;
|
|
2567
|
+
while (usedLabels.has(`${label}-${n}`)) n++;
|
|
2568
|
+
label = `${label}-${n}`;
|
|
2569
|
+
deps.renderer.writeInfo(`Label collided; saving as "${label}".`);
|
|
2570
|
+
}
|
|
2571
|
+
const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
|
|
2572
|
+
if (!apiKey) return 1;
|
|
2573
|
+
await mutateProviders(deps, (all) => {
|
|
2574
|
+
const p = all[providerId] ?? { type: providerId };
|
|
2575
|
+
if (!p.type) p.type = providerId;
|
|
2576
|
+
if (!p.family && opts.family) p.family = opts.family;
|
|
2577
|
+
if (!p.baseUrl && opts.baseUrl) p.baseUrl = opts.baseUrl;
|
|
2578
|
+
if (!p.envVars && opts.envVars) p.envVars = opts.envVars;
|
|
2579
|
+
const list = normalizeKeys(p);
|
|
2580
|
+
list.push({ label, apiKey, createdAt: nowIso() });
|
|
2581
|
+
writeKeysBack(p, list);
|
|
2582
|
+
if (!p.activeKey) p.activeKey = label;
|
|
2583
|
+
all[providerId] = p;
|
|
2584
|
+
});
|
|
2585
|
+
deps.renderer.writeInfo(`Stored encrypted key for ${providerId} (label "${label}").`);
|
|
2586
|
+
deps.renderer.writeInfo(`Use: wstack --provider ${providerId} "<task>"`);
|
|
2587
|
+
return 0;
|
|
2588
|
+
}
|
|
2589
|
+
async function readKeyInput(deps, intent) {
|
|
2590
|
+
const key = (await deps.reader.readSecret(` ${color.amber("?")} ${intent} ${color.dim("(hidden, paste OK)")}: `)).trim();
|
|
2591
|
+
if (!key) {
|
|
2592
|
+
deps.renderer.writeError("No key entered.");
|
|
2593
|
+
return void 0;
|
|
2594
|
+
}
|
|
2595
|
+
return key;
|
|
2596
|
+
}
|
|
2597
|
+
async function loadProviders(deps) {
|
|
2598
|
+
let raw;
|
|
2599
|
+
try {
|
|
2600
|
+
raw = await fs6.readFile(deps.globalConfigPath, "utf8");
|
|
2601
|
+
} catch {
|
|
2602
|
+
return {};
|
|
2603
|
+
}
|
|
2604
|
+
let parsed = {};
|
|
2605
|
+
try {
|
|
2606
|
+
parsed = JSON.parse(raw);
|
|
2607
|
+
} catch {
|
|
2608
|
+
return {};
|
|
2609
|
+
}
|
|
2610
|
+
const decrypted = decryptConfigSecrets(parsed, deps.vault);
|
|
2611
|
+
return decrypted.providers ?? {};
|
|
2612
|
+
}
|
|
2613
|
+
async function mutateProviders(deps, mutator) {
|
|
2614
|
+
let raw;
|
|
2615
|
+
try {
|
|
2616
|
+
raw = await fs6.readFile(deps.globalConfigPath, "utf8");
|
|
2617
|
+
} catch {
|
|
2618
|
+
raw = "{}";
|
|
2619
|
+
}
|
|
2620
|
+
let parsed;
|
|
2621
|
+
try {
|
|
2622
|
+
parsed = JSON.parse(raw);
|
|
2623
|
+
} catch {
|
|
2624
|
+
parsed = {};
|
|
2625
|
+
}
|
|
2626
|
+
const decrypted = decryptConfigSecrets(parsed, deps.vault);
|
|
2627
|
+
const providers = decrypted.providers ?? {};
|
|
2628
|
+
mutator(providers);
|
|
2629
|
+
decrypted.providers = providers;
|
|
2630
|
+
const encrypted = encryptConfigSecrets(decrypted, deps.vault);
|
|
2631
|
+
await atomicWrite(deps.globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
2632
|
+
}
|
|
2633
|
+
function normalizeKeys(cfg) {
|
|
2634
|
+
if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
|
|
2635
|
+
return cfg.apiKeys.map((k) => ({ ...k }));
|
|
2636
|
+
}
|
|
2637
|
+
if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
|
|
2638
|
+
return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
|
|
2639
|
+
}
|
|
2640
|
+
return [];
|
|
2641
|
+
}
|
|
2642
|
+
function writeKeysBack(cfg, keys) {
|
|
2643
|
+
if (keys.length === 0) {
|
|
2644
|
+
delete cfg.apiKeys;
|
|
2645
|
+
delete cfg.apiKey;
|
|
2646
|
+
delete cfg.activeKey;
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
cfg.apiKeys = keys;
|
|
2650
|
+
const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
|
|
2651
|
+
cfg.apiKey = active.apiKey;
|
|
2652
|
+
if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
|
|
2653
|
+
cfg.activeKey = active.label;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
function activeLabel(cfg, keys) {
|
|
2657
|
+
if (cfg.activeKey && keys.some((k) => k.label === cfg.activeKey)) return cfg.activeKey;
|
|
2658
|
+
return keys[0]?.label;
|
|
2659
|
+
}
|
|
2660
|
+
function maskedKey(key) {
|
|
2661
|
+
if (!key) return color.dim("\u2014");
|
|
2662
|
+
if (key.length <= 8) return color.dim("\u2022".repeat(key.length));
|
|
2663
|
+
const head = key.slice(0, 4);
|
|
2664
|
+
const tail = key.slice(-4);
|
|
2665
|
+
return `${color.dim(head + "\u2026")}${tail}`;
|
|
2666
|
+
}
|
|
2667
|
+
function nowIso() {
|
|
2668
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
// src/subcommands/index.ts
|
|
2672
|
+
var subcommands = {
|
|
2673
|
+
init: initCmd,
|
|
2674
|
+
auth: authCmd,
|
|
2675
|
+
// `resume <id>` is special-cased in src/index.ts: it's lifted into
|
|
2676
|
+
// `--resume <id>` so the normal REPL bootstrap runs with a pre-loaded
|
|
2677
|
+
// session. There is no standalone subcommand handler.
|
|
2678
|
+
sessions: sessionsCmd,
|
|
2679
|
+
config: configCmd,
|
|
2680
|
+
tools: toolsCmd,
|
|
2681
|
+
skills: skillsCmd,
|
|
2682
|
+
providers: providersCmd,
|
|
2683
|
+
models: modelsCmd,
|
|
2684
|
+
mcp: mcpCmd,
|
|
2685
|
+
plugin: pluginCmd,
|
|
2686
|
+
diag: diagCmd,
|
|
2687
|
+
doctor: doctorCmd,
|
|
2688
|
+
export: exportCmd,
|
|
2689
|
+
usage: usageCmd,
|
|
2690
|
+
version: versionCmd,
|
|
2691
|
+
help: helpCmd,
|
|
2692
|
+
projects: projectsCmd
|
|
2693
|
+
};
|
|
2694
|
+
async function authCmd(args, deps) {
|
|
2695
|
+
const flags = parseAuthFlags(args);
|
|
2696
|
+
const menuDeps = {
|
|
2697
|
+
renderer: deps.renderer,
|
|
2698
|
+
reader: deps.reader,
|
|
2699
|
+
modelsRegistry: deps.modelsRegistry,
|
|
2700
|
+
vault: deps.vault,
|
|
2701
|
+
globalConfigPath: deps.paths.globalConfig
|
|
2702
|
+
};
|
|
2703
|
+
if (flags.positional.length === 0) {
|
|
2704
|
+
return runAuthMenu(menuDeps);
|
|
2705
|
+
}
|
|
2706
|
+
return runAuthDirect(menuDeps, {
|
|
2707
|
+
providerId: flags.positional[0],
|
|
2708
|
+
label: flags.label,
|
|
2709
|
+
family: flags.family,
|
|
2710
|
+
baseUrl: flags.baseUrl,
|
|
2711
|
+
envVars: flags.envVars
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
function parseAuthFlags(args) {
|
|
1648
2715
|
const out = { positional: [] };
|
|
1649
2716
|
for (let i = 0; i < args.length; i++) {
|
|
1650
2717
|
const a = args[i];
|
|
1651
|
-
if (a === "--
|
|
2718
|
+
if (a === "--label") {
|
|
2719
|
+
const v = args[++i];
|
|
2720
|
+
if (v) out.label = v;
|
|
2721
|
+
} else if (a === "--family") {
|
|
1652
2722
|
const v = args[++i];
|
|
1653
2723
|
if (v) out.family = v;
|
|
1654
2724
|
} else if (a === "--base-url") {
|
|
@@ -1710,7 +2780,7 @@ async function initCmd(_args, deps) {
|
|
|
1710
2780
|
} else {
|
|
1711
2781
|
deps.renderer.writeInfo(`Found API key in env (${provider.envVars.join(" / ")}).`);
|
|
1712
2782
|
}
|
|
1713
|
-
await
|
|
2783
|
+
await fs6.mkdir(deps.paths.globalRoot, { recursive: true });
|
|
1714
2784
|
const config = {
|
|
1715
2785
|
version: 1,
|
|
1716
2786
|
provider: providerId,
|
|
@@ -1718,10 +2788,10 @@ async function initCmd(_args, deps) {
|
|
|
1718
2788
|
};
|
|
1719
2789
|
if (apiKey) config.apiKey = apiKey;
|
|
1720
2790
|
await atomicWrite(deps.paths.globalConfig, JSON.stringify(config, null, 2));
|
|
1721
|
-
await
|
|
1722
|
-
const agentsFile =
|
|
2791
|
+
await fs6.mkdir(path5.join(deps.projectRoot, ".wrongstack"), { recursive: true });
|
|
2792
|
+
const agentsFile = path5.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
|
|
1723
2793
|
try {
|
|
1724
|
-
await
|
|
2794
|
+
await fs6.access(agentsFile);
|
|
1725
2795
|
} catch {
|
|
1726
2796
|
await atomicWrite(
|
|
1727
2797
|
agentsFile,
|
|
@@ -1853,25 +2923,42 @@ async function modelsCmd(args, deps) {
|
|
|
1853
2923
|
deps.renderer.writeError("Usage: wstack models <provider> | refresh");
|
|
1854
2924
|
return 1;
|
|
1855
2925
|
}
|
|
1856
|
-
|
|
2926
|
+
let lookupId = providerId;
|
|
2927
|
+
const savedAlias = deps.config.providers?.[providerId];
|
|
2928
|
+
if (savedAlias?.type && savedAlias.type !== providerId) {
|
|
2929
|
+
lookupId = savedAlias.type;
|
|
2930
|
+
}
|
|
2931
|
+
const provider = await deps.modelsRegistry.getProvider(lookupId);
|
|
1857
2932
|
if (!provider) {
|
|
1858
|
-
deps.renderer.writeError(
|
|
2933
|
+
deps.renderer.writeError(
|
|
2934
|
+
lookupId !== providerId ? `Alias "${providerId}" points at catalog id "${lookupId}" which is not in the cache.` : `Provider "${providerId}" not in catalog.`
|
|
2935
|
+
);
|
|
1859
2936
|
return 1;
|
|
1860
2937
|
}
|
|
2938
|
+
if (lookupId !== providerId) {
|
|
2939
|
+
deps.renderer.write(color.dim(`(showing catalog models for "${lookupId}" via alias "${providerId}")
|
|
2940
|
+
`));
|
|
2941
|
+
}
|
|
1861
2942
|
deps.renderer.write(`${color.bold(provider.name)} ${color.dim(`(${provider.id})`)}
|
|
1862
2943
|
`);
|
|
1863
2944
|
if (provider.doc) deps.renderer.write(color.dim(`Docs: ${provider.doc}
|
|
1864
2945
|
`));
|
|
1865
|
-
const
|
|
2946
|
+
const userModels = deps.config.providers?.[providerId]?.models;
|
|
2947
|
+
const catalogById = new Map(provider.models.map((m) => [m.id, m]));
|
|
2948
|
+
const sorted = userModels && userModels.length > 0 ? userModels.map((id) => catalogById.get(id) ?? { id, name: id }) : [...provider.models].sort(
|
|
1866
2949
|
(a, b) => (b.release_date ?? "").localeCompare(a.release_date ?? "")
|
|
1867
2950
|
);
|
|
2951
|
+
if (userModels && userModels.length > 0) {
|
|
2952
|
+
deps.renderer.write(color.dim(`(${userModels.length} model(s) from your saved config)
|
|
2953
|
+
`));
|
|
2954
|
+
}
|
|
1868
2955
|
for (const m of sorted) {
|
|
1869
2956
|
const caps = [];
|
|
1870
|
-
if (m.tool_call) caps.push("tools");
|
|
1871
|
-
if (m.reasoning) caps.push("reasoning");
|
|
1872
|
-
if (m.modalities?.input?.includes("image")) caps.push("vision");
|
|
1873
|
-
const ctx = m.limit?.context ? `${(m.limit.context / 1e3).toFixed(0)}k` : "?";
|
|
1874
|
-
const cost = m.cost?.input !== void 0 ? `$${m.cost.input}/$${m.cost.output ?? "?"}` : "";
|
|
2957
|
+
if ("tool_call" in m && m.tool_call) caps.push("tools");
|
|
2958
|
+
if ("reasoning" in m && m.reasoning) caps.push("reasoning");
|
|
2959
|
+
if ("modalities" in m && m.modalities?.input?.includes("image")) caps.push("vision");
|
|
2960
|
+
const ctx = "limit" in m && m.limit?.context ? `${(m.limit.context / 1e3).toFixed(0)}k` : "?";
|
|
2961
|
+
const cost = "cost" in m && m.cost?.input !== void 0 ? `$${m.cost.input}/$${m.cost.output ?? "?"}` : "";
|
|
1875
2962
|
deps.renderer.write(
|
|
1876
2963
|
` ${m.id.padEnd(40)} ${color.dim(ctx.padStart(6))} ${color.dim(cost.padEnd(14))} ${color.dim(caps.join(","))}
|
|
1877
2964
|
`
|
|
@@ -1893,16 +2980,46 @@ async function mcpCmd(args, deps) {
|
|
|
1893
2980
|
const servers = deps.config.mcpServers ?? {};
|
|
1894
2981
|
if (Object.keys(servers).length === 0) {
|
|
1895
2982
|
deps.renderer.write("No MCP servers configured.\n");
|
|
2983
|
+
deps.renderer.write("Use `wstack mcp add <name>` or set mcpServers in your config.\n");
|
|
1896
2984
|
return 0;
|
|
1897
2985
|
}
|
|
1898
2986
|
for (const [name, cfg] of Object.entries(servers)) {
|
|
2987
|
+
const status = cfg.enabled === false ? "disabled" : "enabled";
|
|
2988
|
+
const desc = cfg.description ? ` # ${cfg.description}` : "";
|
|
1899
2989
|
deps.renderer.write(
|
|
1900
|
-
` ${name.padEnd(20)} ${cfg.transport}
|
|
2990
|
+
` ${name.padEnd(20)} ${cfg.transport.padEnd(16)} ${status}${desc}
|
|
1901
2991
|
`
|
|
1902
2992
|
);
|
|
1903
2993
|
}
|
|
1904
2994
|
return 0;
|
|
1905
2995
|
}
|
|
2996
|
+
if (sub === "add") {
|
|
2997
|
+
const name = args[1];
|
|
2998
|
+
if (!name) {
|
|
2999
|
+
deps.renderer.writeError("Usage: wstack mcp add <name>\n");
|
|
3000
|
+
deps.renderer.write("Available servers:\n");
|
|
3001
|
+
for (const [sname, scfg] of Object.entries(deps.config.mcpServers ?? {})) {
|
|
3002
|
+
deps.renderer.write(` ${sname.padEnd(20)} ${scfg.description ?? scfg.transport}
|
|
3003
|
+
`);
|
|
3004
|
+
}
|
|
3005
|
+
if (Object.keys(deps.config.mcpServers ?? {}).length === 0) {
|
|
3006
|
+
deps.renderer.write(
|
|
3007
|
+
" filesystem filesystem (read/write/navigate)\n github github (issues, PRs, repos)\n context7 context7 (codebase docs & Q&A)\n brave-search brave search (web search)\n block block (Postgres via SQL)\n everart everart (AI image generation)\n slack slack (messaging & channels)\n aws aws (EC2, S3, Lambda, IAM)\n google-maps google-maps (directions, geocoding)\n sentinel sentinel (security vulnerabilities)\n"
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
deps.renderer.write("\nRun `wstack mcp add <name> --enable` to enable immediately.\n");
|
|
3011
|
+
return 1;
|
|
3012
|
+
}
|
|
3013
|
+
return addMcpServer(args, deps);
|
|
3014
|
+
}
|
|
3015
|
+
if (sub === "remove") {
|
|
3016
|
+
const name = args[1];
|
|
3017
|
+
if (!name) {
|
|
3018
|
+
deps.renderer.writeError("Usage: wstack mcp remove <name>\n");
|
|
3019
|
+
return 1;
|
|
3020
|
+
}
|
|
3021
|
+
return removeMcpServer(name, deps);
|
|
3022
|
+
}
|
|
1906
3023
|
if (sub === "restart") {
|
|
1907
3024
|
deps.renderer.writeWarning("mcp restart is only available in REPL mode.");
|
|
1908
3025
|
return 0;
|
|
@@ -1910,6 +3027,70 @@ async function mcpCmd(args, deps) {
|
|
|
1910
3027
|
deps.renderer.writeError(`Unknown mcp subcommand: ${sub}`);
|
|
1911
3028
|
return 1;
|
|
1912
3029
|
}
|
|
3030
|
+
async function addMcpServer(args, deps) {
|
|
3031
|
+
const name = args[1];
|
|
3032
|
+
const enable = args.includes("--enable") || args.includes("-e");
|
|
3033
|
+
const builtIn = {
|
|
3034
|
+
filesystem: { name: "filesystem", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "."], permission: "confirm", description: "Read, write, and navigate the local filesystem" },
|
|
3035
|
+
github: { name: "github", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"], permission: "confirm", description: "GitHub API \u2014 issues, PRs, repos, search" },
|
|
3036
|
+
"context7": { name: "context7", transport: "streamable-http", url: "https://server.context7.ai/mcp", permission: "confirm", description: "Codebase-aware documentation and Q&A" },
|
|
3037
|
+
"brave-search": { name: "brave-search", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-brave-search"], permission: "confirm", description: "Web search (Brave)" },
|
|
3038
|
+
block: { name: "block", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-block"], permission: "confirm", description: "Postgres database via SQL" },
|
|
3039
|
+
everart: { name: "everart", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-everart"], permission: "confirm", description: "AI image generation" },
|
|
3040
|
+
slack: { name: "slack", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-slack"], permission: "confirm", description: "Slack messaging & channels" },
|
|
3041
|
+
aws: { name: "aws", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-aws"], permission: "confirm", description: "AWS \u2014 EC2, S3, Lambda, IAM" },
|
|
3042
|
+
"google-maps": { name: "google-maps", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-google-maps"], permission: "confirm", description: "Google Maps \u2014 directions, geocoding, places" },
|
|
3043
|
+
sentinel: { name: "sentinel", transport: "streamable-http", url: "https://mcp.sentinel.ai", permission: "deny", description: "Security vulnerability scanning" }
|
|
3044
|
+
};
|
|
3045
|
+
const factory = builtIn[name];
|
|
3046
|
+
if (!factory) {
|
|
3047
|
+
deps.renderer.writeError(`Unknown server "${name}". Run \`wstack mcp add\` without args to see available servers.
|
|
3048
|
+
`);
|
|
3049
|
+
return 1;
|
|
3050
|
+
}
|
|
3051
|
+
const serverCfg = { ...factory };
|
|
3052
|
+
if (!enable) serverCfg.enabled = false;
|
|
3053
|
+
let existing = {};
|
|
3054
|
+
try {
|
|
3055
|
+
const raw = await fs6.readFile(deps.paths.globalConfig, "utf8");
|
|
3056
|
+
existing = JSON.parse(raw);
|
|
3057
|
+
} catch {
|
|
3058
|
+
}
|
|
3059
|
+
const mcpServers = existing.mcpServers ?? {};
|
|
3060
|
+
if (mcpServers[name]) {
|
|
3061
|
+
deps.renderer.writeWarning(`Server "${name}" already in config. Updating.
|
|
3062
|
+
`);
|
|
3063
|
+
}
|
|
3064
|
+
mcpServers[name] = serverCfg;
|
|
3065
|
+
existing.mcpServers = mcpServers;
|
|
3066
|
+
await atomicWrite(deps.paths.globalConfig, JSON.stringify(existing, null, 2));
|
|
3067
|
+
const verb = enable ? "Enabled" : "Added (disabled \u2014 set enabled:true to activate)";
|
|
3068
|
+
deps.renderer.writeInfo(`${verb} "${name}" (${serverCfg.transport}). Config written to ${deps.paths.globalConfig}.
|
|
3069
|
+
`);
|
|
3070
|
+
return 0;
|
|
3071
|
+
}
|
|
3072
|
+
async function removeMcpServer(name, deps) {
|
|
3073
|
+
let existing = {};
|
|
3074
|
+
try {
|
|
3075
|
+
const raw = await fs6.readFile(deps.paths.globalConfig, "utf8");
|
|
3076
|
+
existing = JSON.parse(raw);
|
|
3077
|
+
} catch {
|
|
3078
|
+
deps.renderer.writeError("No config file found.\n");
|
|
3079
|
+
return 1;
|
|
3080
|
+
}
|
|
3081
|
+
const mcpServers = existing.mcpServers ?? {};
|
|
3082
|
+
if (!mcpServers[name]) {
|
|
3083
|
+
deps.renderer.writeError(`Server "${name}" not in config.
|
|
3084
|
+
`);
|
|
3085
|
+
return 1;
|
|
3086
|
+
}
|
|
3087
|
+
delete mcpServers[name];
|
|
3088
|
+
existing.mcpServers = mcpServers;
|
|
3089
|
+
await atomicWrite(deps.paths.globalConfig, JSON.stringify(existing, null, 2));
|
|
3090
|
+
deps.renderer.writeInfo(`Removed "${name}" from config.
|
|
3091
|
+
`);
|
|
3092
|
+
return 0;
|
|
3093
|
+
}
|
|
1913
3094
|
async function pluginCmd(args, deps) {
|
|
1914
3095
|
const sub = args[0];
|
|
1915
3096
|
if (!sub || sub === "list") {
|
|
@@ -1934,7 +3115,7 @@ async function diagCmd(_args, deps) {
|
|
|
1934
3115
|
const age = await deps.modelsRegistry.ageSeconds();
|
|
1935
3116
|
const lines = [
|
|
1936
3117
|
color.bold("WrongStack diagnostics"),
|
|
1937
|
-
` apiVersion:
|
|
3118
|
+
` apiVersion: ${API_VERSION}`,
|
|
1938
3119
|
` cwd: ${deps.cwd}`,
|
|
1939
3120
|
` projectRoot: ${deps.projectRoot}`,
|
|
1940
3121
|
` projectHash: ${deps.paths.projectHash}`,
|
|
@@ -1943,7 +3124,7 @@ async function diagCmd(_args, deps) {
|
|
|
1943
3124
|
` modelsCache: ${deps.paths.modelsCache}`,
|
|
1944
3125
|
` cacheAge: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never"}`,
|
|
1945
3126
|
` node: ${process.version}`,
|
|
1946
|
-
` os: ${
|
|
3127
|
+
` os: ${os3.platform()} ${os3.release()}`,
|
|
1947
3128
|
` provider: ${cfg.provider ?? "<unset>"}`,
|
|
1948
3129
|
` model: ${cfg.model ?? "<unset>"}`,
|
|
1949
3130
|
` tools: ${deps.toolRegistry?.list().length ?? 0}`,
|
|
@@ -1953,6 +3134,182 @@ async function diagCmd(_args, deps) {
|
|
|
1953
3134
|
deps.renderer.write(lines.join("\n") + "\n");
|
|
1954
3135
|
return 0;
|
|
1955
3136
|
}
|
|
3137
|
+
async function doctorCmd(_args, deps) {
|
|
3138
|
+
const checks = [];
|
|
3139
|
+
const cfg = deps.config;
|
|
3140
|
+
if (!cfg.provider) {
|
|
3141
|
+
checks.push({ name: "provider", status: "fail", detail: "no provider configured \u2014 run `wstack init` or `wstack auth`" });
|
|
3142
|
+
} else {
|
|
3143
|
+
checks.push({ name: "provider", status: "ok", detail: cfg.provider });
|
|
3144
|
+
}
|
|
3145
|
+
if (!cfg.model) {
|
|
3146
|
+
checks.push({ name: "model", status: "fail", detail: "no model configured \u2014 run `wstack init`" });
|
|
3147
|
+
} else {
|
|
3148
|
+
checks.push({ name: "model", status: "ok", detail: cfg.model });
|
|
3149
|
+
}
|
|
3150
|
+
if (cfg.provider) {
|
|
3151
|
+
const providerCfg = cfg.providers?.[cfg.provider];
|
|
3152
|
+
const hasVaultKey = typeof providerCfg?.apiKey === "string" && providerCfg.apiKey.length > 0;
|
|
3153
|
+
const envHit = providerCfg?.envVars?.some((v) => process.env[v]) ?? false;
|
|
3154
|
+
if (hasVaultKey || envHit) {
|
|
3155
|
+
checks.push({
|
|
3156
|
+
name: "api key",
|
|
3157
|
+
status: "ok",
|
|
3158
|
+
detail: hasVaultKey ? "found in vault" : "found in env"
|
|
3159
|
+
});
|
|
3160
|
+
} else {
|
|
3161
|
+
checks.push({
|
|
3162
|
+
name: "api key",
|
|
3163
|
+
status: "fail",
|
|
3164
|
+
detail: `no key for "${cfg.provider}" in vault or env \u2014 run \`wstack auth ${cfg.provider}\``
|
|
3165
|
+
});
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
try {
|
|
3169
|
+
const age = await deps.modelsRegistry.ageSeconds();
|
|
3170
|
+
if (!isFinite(age)) {
|
|
3171
|
+
checks.push({ name: "models cache", status: "warn", detail: "never fetched \u2014 run `wstack models refresh`" });
|
|
3172
|
+
} else if (age > 7 * 24 * 3600) {
|
|
3173
|
+
checks.push({
|
|
3174
|
+
name: "models cache",
|
|
3175
|
+
status: "warn",
|
|
3176
|
+
detail: `${Math.round(age / 86400)} days old \u2014 run \`wstack models refresh\``
|
|
3177
|
+
});
|
|
3178
|
+
} else {
|
|
3179
|
+
checks.push({ name: "models cache", status: "ok", detail: `${Math.round(age / 60)}m old` });
|
|
3180
|
+
}
|
|
3181
|
+
} catch (err) {
|
|
3182
|
+
checks.push({
|
|
3183
|
+
name: "models cache",
|
|
3184
|
+
status: "warn",
|
|
3185
|
+
detail: `read failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
try {
|
|
3189
|
+
await fs6.access(deps.paths.secretsKey);
|
|
3190
|
+
checks.push({ name: "secret vault", status: "ok", detail: deps.paths.secretsKey });
|
|
3191
|
+
} catch {
|
|
3192
|
+
checks.push({
|
|
3193
|
+
name: "secret vault",
|
|
3194
|
+
status: "warn",
|
|
3195
|
+
detail: "not yet initialized (created lazily on first encrypt)"
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
3198
|
+
try {
|
|
3199
|
+
await fs6.mkdir(deps.paths.projectSessions, { recursive: true });
|
|
3200
|
+
const probe = path5.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
|
|
3201
|
+
await fs6.writeFile(probe, "");
|
|
3202
|
+
await fs6.unlink(probe);
|
|
3203
|
+
checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
|
|
3204
|
+
} catch (err) {
|
|
3205
|
+
checks.push({
|
|
3206
|
+
name: "sessions writable",
|
|
3207
|
+
status: "fail",
|
|
3208
|
+
detail: `cannot write to ${deps.paths.projectSessions}: ${err instanceof Error ? err.message : String(err)}`
|
|
3209
|
+
});
|
|
3210
|
+
}
|
|
3211
|
+
const mcpEntries = Object.entries(cfg.mcpServers ?? {});
|
|
3212
|
+
for (const [name, srv] of mcpEntries) {
|
|
3213
|
+
if (!srv.enabled) continue;
|
|
3214
|
+
if ((srv.transport === "sse" || srv.transport === "streamable-http") && !srv.url) {
|
|
3215
|
+
checks.push({ name: `mcp:${name}`, status: "fail", detail: "transport requires url" });
|
|
3216
|
+
} else if (srv.transport === "stdio" && !srv.command) {
|
|
3217
|
+
checks.push({ name: `mcp:${name}`, status: "fail", detail: "stdio transport requires command" });
|
|
3218
|
+
} else {
|
|
3219
|
+
checks.push({ name: `mcp:${name}`, status: "ok", detail: `${srv.transport} ${srv.command ?? srv.url ?? ""}`.trim() });
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
const major = Number.parseInt(process.version.replace(/^v/, "").split(".")[0] ?? "0", 10);
|
|
3223
|
+
if (major < 22) {
|
|
3224
|
+
checks.push({ name: "node", status: "fail", detail: `${process.version} (need \u226522)` });
|
|
3225
|
+
} else {
|
|
3226
|
+
checks.push({ name: "node", status: "ok", detail: process.version });
|
|
3227
|
+
}
|
|
3228
|
+
deps.renderer.write(color.bold("WrongStack doctor\n\n"));
|
|
3229
|
+
let failed = 0;
|
|
3230
|
+
let warned = 0;
|
|
3231
|
+
for (const c of checks) {
|
|
3232
|
+
const icon = c.status === "ok" ? color.green("\u2713") : c.status === "warn" ? color.amber("\u25CF") : color.red("\u2717");
|
|
3233
|
+
deps.renderer.write(` ${icon} ${c.name.padEnd(20)} ${color.dim(c.detail)}
|
|
3234
|
+
`);
|
|
3235
|
+
if (c.status === "fail") failed++;
|
|
3236
|
+
if (c.status === "warn") warned++;
|
|
3237
|
+
}
|
|
3238
|
+
deps.renderer.write("\n");
|
|
3239
|
+
if (failed > 0) {
|
|
3240
|
+
deps.renderer.write(color.red(`${failed} failed, ${warned} warning${warned === 1 ? "" : "s"}
|
|
3241
|
+
`));
|
|
3242
|
+
return 1;
|
|
3243
|
+
}
|
|
3244
|
+
if (warned > 0) {
|
|
3245
|
+
deps.renderer.write(color.amber(`All checks passed (${warned} warning${warned === 1 ? "" : "s"})
|
|
3246
|
+
`));
|
|
3247
|
+
return 0;
|
|
3248
|
+
}
|
|
3249
|
+
deps.renderer.write(color.green("All checks passed.\n"));
|
|
3250
|
+
return 0;
|
|
3251
|
+
}
|
|
3252
|
+
async function exportCmd(args, deps) {
|
|
3253
|
+
if (!deps.sessionStore) {
|
|
3254
|
+
deps.renderer.writeError("No session store configured.");
|
|
3255
|
+
return 1;
|
|
3256
|
+
}
|
|
3257
|
+
let format = "markdown";
|
|
3258
|
+
let output;
|
|
3259
|
+
let includeTools = true;
|
|
3260
|
+
let includeDiagnostics = true;
|
|
3261
|
+
let sessionId;
|
|
3262
|
+
for (let i = 0; i < args.length; i++) {
|
|
3263
|
+
const a = args[i];
|
|
3264
|
+
if (a === "--format" || a === "-f") {
|
|
3265
|
+
const v = args[++i];
|
|
3266
|
+
if (v !== "markdown" && v !== "json" && v !== "text") {
|
|
3267
|
+
deps.renderer.writeError(`Unknown --format ${v}. Use markdown, json, or text.`);
|
|
3268
|
+
return 1;
|
|
3269
|
+
}
|
|
3270
|
+
format = v;
|
|
3271
|
+
} else if (a === "--out" || a === "-o") {
|
|
3272
|
+
output = args[++i];
|
|
3273
|
+
} else if (a === "--no-tools") {
|
|
3274
|
+
includeTools = false;
|
|
3275
|
+
} else if (a === "--no-diagnostics") {
|
|
3276
|
+
includeDiagnostics = false;
|
|
3277
|
+
} else if (a.startsWith("-")) {
|
|
3278
|
+
deps.renderer.writeError(`Unknown flag: ${a}`);
|
|
3279
|
+
return 1;
|
|
3280
|
+
} else if (!sessionId) {
|
|
3281
|
+
sessionId = a;
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
if (!sessionId) {
|
|
3285
|
+
deps.renderer.writeError("Usage: wstack export <sessionId> [--format markdown|json|text] [--out <file>] [--no-tools] [--no-diagnostics]");
|
|
3286
|
+
return 1;
|
|
3287
|
+
}
|
|
3288
|
+
const reader = new DefaultSessionReader({ store: deps.sessionStore });
|
|
3289
|
+
let rendered;
|
|
3290
|
+
try {
|
|
3291
|
+
rendered = await reader.export(sessionId, {
|
|
3292
|
+
format,
|
|
3293
|
+
includeTools,
|
|
3294
|
+
includeDiagnostics
|
|
3295
|
+
});
|
|
3296
|
+
} catch (err) {
|
|
3297
|
+
deps.renderer.writeError(
|
|
3298
|
+
`Export failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3299
|
+
);
|
|
3300
|
+
return 1;
|
|
3301
|
+
}
|
|
3302
|
+
if (output) {
|
|
3303
|
+
await fs6.mkdir(path5.dirname(path5.resolve(deps.cwd, output)), { recursive: true });
|
|
3304
|
+
await fs6.writeFile(path5.resolve(deps.cwd, output), rendered, "utf8");
|
|
3305
|
+
deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
|
|
3306
|
+
`);
|
|
3307
|
+
} else {
|
|
3308
|
+
deps.renderer.write(rendered);
|
|
3309
|
+
if (!rendered.endsWith("\n")) deps.renderer.write("\n");
|
|
3310
|
+
}
|
|
3311
|
+
return 0;
|
|
3312
|
+
}
|
|
1956
3313
|
async function usageCmd(_args, deps) {
|
|
1957
3314
|
if (!deps.sessionStore) return 0;
|
|
1958
3315
|
const list = await deps.sessionStore.list(100);
|
|
@@ -1964,7 +3321,7 @@ async function usageCmd(_args, deps) {
|
|
|
1964
3321
|
}
|
|
1965
3322
|
async function versionCmd(_args, deps) {
|
|
1966
3323
|
deps.renderer.write(
|
|
1967
|
-
`WrongStack
|
|
3324
|
+
`WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${os3.platform()})
|
|
1968
3325
|
`
|
|
1969
3326
|
);
|
|
1970
3327
|
return 0;
|
|
@@ -1978,7 +3335,8 @@ async function helpCmd(_args, deps) {
|
|
|
1978
3335
|
" wstack resume [<id>] Resume a session",
|
|
1979
3336
|
" wstack sessions List recent sessions",
|
|
1980
3337
|
" wstack init Pick provider + model from models.dev",
|
|
1981
|
-
" wstack auth
|
|
3338
|
+
" wstack auth Interactive key manager (list/add/update/delete)",
|
|
3339
|
+
" wstack auth <provider> Append one key for a provider (encrypted at rest)",
|
|
1982
3340
|
" wstack resume <id> Resume a session (loads transcript + appends)",
|
|
1983
3341
|
" wstack config [show|edit] Show or edit effective config",
|
|
1984
3342
|
" wstack tools List registered tools",
|
|
@@ -1990,6 +3348,8 @@ async function helpCmd(_args, deps) {
|
|
|
1990
3348
|
" wstack plugin [list] List plugins",
|
|
1991
3349
|
" wstack projects List projects tracked in ~/.wrongstack/projects/",
|
|
1992
3350
|
" wstack diag Full diagnostics",
|
|
3351
|
+
" wstack doctor Health checks (config, keys, MCP, node)",
|
|
3352
|
+
" wstack export <id> [opts] Render a session (--format markdown|json|text, --out <file>)",
|
|
1993
3353
|
" wstack usage Token + cost summary",
|
|
1994
3354
|
" wstack version Print version",
|
|
1995
3355
|
"",
|
|
@@ -2000,9 +3360,9 @@ async function helpCmd(_args, deps) {
|
|
|
2000
3360
|
return 0;
|
|
2001
3361
|
}
|
|
2002
3362
|
async function projectsCmd(_args, deps) {
|
|
2003
|
-
const projectsRoot =
|
|
3363
|
+
const projectsRoot = path5.join(deps.paths.globalRoot, "projects");
|
|
2004
3364
|
try {
|
|
2005
|
-
const entries = await
|
|
3365
|
+
const entries = await fs6.readdir(projectsRoot);
|
|
2006
3366
|
if (entries.length === 0) {
|
|
2007
3367
|
deps.renderer.write("No projects tracked.\n");
|
|
2008
3368
|
return 0;
|
|
@@ -2010,7 +3370,7 @@ async function projectsCmd(_args, deps) {
|
|
|
2010
3370
|
for (const hash of entries) {
|
|
2011
3371
|
try {
|
|
2012
3372
|
const meta = JSON.parse(
|
|
2013
|
-
await
|
|
3373
|
+
await fs6.readFile(path5.join(projectsRoot, hash, "meta.json"), "utf8")
|
|
2014
3374
|
);
|
|
2015
3375
|
deps.renderer.write(
|
|
2016
3376
|
` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
|
|
@@ -2057,7 +3417,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
2057
3417
|
"no-alt-screen",
|
|
2058
3418
|
"alt-screen",
|
|
2059
3419
|
"output-json",
|
|
2060
|
-
"prompt"
|
|
3420
|
+
"prompt",
|
|
3421
|
+
"metrics"
|
|
2061
3422
|
]);
|
|
2062
3423
|
function parseArgs(argv) {
|
|
2063
3424
|
const flags = {};
|
|
@@ -2095,96 +3456,32 @@ function parseArgs(argv) {
|
|
|
2095
3456
|
}
|
|
2096
3457
|
return { flags, positional };
|
|
2097
3458
|
}
|
|
2098
|
-
function flagsToConfigPatch(flags) {
|
|
2099
|
-
const patch = {};
|
|
2100
|
-
if (typeof flags["provider"] === "string") patch.provider = flags["provider"];
|
|
2101
|
-
if (typeof flags["model"] === "string") patch.model = flags["model"];
|
|
2102
|
-
if (typeof flags["cwd"] === "string") patch.cwd = flags["cwd"];
|
|
2103
|
-
if (typeof flags["log-level"] === "string") {
|
|
2104
|
-
patch.log = { level: flags["log-level"] };
|
|
2105
|
-
} else if (flags["verbose"]) {
|
|
2106
|
-
patch.log = { level: "debug" };
|
|
2107
|
-
} else if (flags["trace"]) {
|
|
2108
|
-
patch.log = { level: "trace" };
|
|
2109
|
-
}
|
|
2110
|
-
if (flags["yolo"]) patch.yolo = true;
|
|
2111
|
-
if (flags["no-features"]) {
|
|
2112
|
-
patch.features = {
|
|
2113
|
-
mcp: false,
|
|
2114
|
-
plugins: false,
|
|
2115
|
-
memory: false,
|
|
2116
|
-
modelsRegistry: false,
|
|
2117
|
-
skills: false
|
|
2118
|
-
};
|
|
2119
|
-
}
|
|
2120
|
-
return patch;
|
|
2121
|
-
}
|
|
2122
3459
|
function resolveBundledSkillsDir() {
|
|
2123
3460
|
try {
|
|
2124
|
-
const
|
|
2125
|
-
const corePkg =
|
|
2126
|
-
return
|
|
3461
|
+
const req2 = createRequire(import.meta.url);
|
|
3462
|
+
const corePkg = req2.resolve("@wrongstack/core/package.json");
|
|
3463
|
+
return path5.join(path5.dirname(corePkg), "skills");
|
|
2127
3464
|
} catch {
|
|
2128
3465
|
return void 0;
|
|
2129
3466
|
}
|
|
2130
3467
|
}
|
|
2131
|
-
function readOwnVersion() {
|
|
2132
|
-
const req = createRequire(import.meta.url);
|
|
2133
|
-
const candidates = ["../package.json", "../../package.json"];
|
|
2134
|
-
for (const rel of candidates) {
|
|
2135
|
-
try {
|
|
2136
|
-
const pkg = req(rel);
|
|
2137
|
-
if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
|
|
2138
|
-
} catch {
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
return "dev";
|
|
2142
|
-
}
|
|
2143
|
-
var CLI_VERSION = readOwnVersion();
|
|
2144
|
-
async function ensureProjectMeta(paths, projectRoot) {
|
|
2145
|
-
try {
|
|
2146
|
-
await fs2.mkdir(paths.projectDir, { recursive: true });
|
|
2147
|
-
const meta = {
|
|
2148
|
-
hash: paths.projectHash,
|
|
2149
|
-
root: projectRoot,
|
|
2150
|
-
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
2151
|
-
};
|
|
2152
|
-
await fs2.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
|
|
2153
|
-
} catch {
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
3468
|
async function main(argv) {
|
|
2157
3469
|
const { flags, positional } = parseArgs(argv);
|
|
2158
|
-
const cwd = typeof flags["cwd"] === "string" ? path2.resolve(flags["cwd"]) : process.cwd();
|
|
2159
|
-
const pathResolver = new DefaultPathResolver(cwd);
|
|
2160
|
-
const projectRoot = pathResolver.projectRoot;
|
|
2161
|
-
const userHome = os2.homedir();
|
|
2162
|
-
const wpaths = resolveWstackPaths({ projectRoot, userHome });
|
|
2163
|
-
await ensureProjectMeta(wpaths, projectRoot);
|
|
2164
3470
|
if (positional[0] === "resume" && positional[1] && !subcommands["__noop_resume_marker"]) {
|
|
2165
3471
|
flags["resume"] = positional[1];
|
|
2166
3472
|
positional.splice(0, 2);
|
|
2167
3473
|
}
|
|
2168
|
-
|
|
2169
|
-
for (const file of [wpaths.globalConfig, wpaths.projectLocalConfig]) {
|
|
2170
|
-
try {
|
|
2171
|
-
const { migrated } = await migratePlaintextSecrets(file, vault);
|
|
2172
|
-
if (migrated > 0) {
|
|
2173
|
-
process.stderr.write(`[wstack] Encrypted ${migrated} plaintext secret(s) in ${file}
|
|
2174
|
-
`);
|
|
2175
|
-
}
|
|
2176
|
-
} catch {
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
const configLoader = new DefaultConfigLoader({ paths: wpaths, vault });
|
|
2180
|
-
let config;
|
|
3474
|
+
let boot;
|
|
2181
3475
|
try {
|
|
2182
|
-
|
|
3476
|
+
boot = await bootConfig(flags);
|
|
2183
3477
|
} catch (err) {
|
|
2184
3478
|
process.stderr.write(`Config error: ${err instanceof Error ? err.message : String(err)}
|
|
2185
3479
|
`);
|
|
2186
3480
|
return 2;
|
|
2187
3481
|
}
|
|
3482
|
+
const { paths, config: _config, vault } = boot;
|
|
3483
|
+
let config = _config;
|
|
3484
|
+
const { cwd, projectRoot, userHome, wpaths, pathResolver } = paths;
|
|
2188
3485
|
const logger = new DefaultLogger({
|
|
2189
3486
|
level: config.log.level,
|
|
2190
3487
|
file: wpaths.logFile
|
|
@@ -2251,7 +3548,7 @@ async function main(argv) {
|
|
|
2251
3548
|
} else {
|
|
2252
3549
|
const prevProvider = config.provider;
|
|
2253
3550
|
const prevModel = config.model;
|
|
2254
|
-
config =
|
|
3551
|
+
config = patchConfig(config, { provider: picked.provider, model: picked.model });
|
|
2255
3552
|
if (picked.provider !== prevProvider || picked.model !== prevModel) {
|
|
2256
3553
|
const saved = await saveToGlobalConfig(
|
|
2257
3554
|
wpaths.globalConfig,
|
|
@@ -2286,15 +3583,21 @@ async function main(argv) {
|
|
|
2286
3583
|
flags["no-tui"] = true;
|
|
2287
3584
|
}
|
|
2288
3585
|
if (choices.yolo !== config.yolo) {
|
|
2289
|
-
config =
|
|
3586
|
+
config = patchConfig(config, { yolo: choices.yolo });
|
|
2290
3587
|
}
|
|
2291
3588
|
}
|
|
2292
|
-
const
|
|
3589
|
+
const savedProviderCfg = config.providers?.[config.provider];
|
|
3590
|
+
let resolvedProvider = await modelsRegistry.getProvider(config.provider).catch(() => void 0);
|
|
3591
|
+
if (!resolvedProvider && savedProviderCfg?.type && savedProviderCfg.type !== config.provider) {
|
|
3592
|
+
resolvedProvider = await modelsRegistry.getProvider(savedProviderCfg.type).catch(() => void 0);
|
|
3593
|
+
}
|
|
2293
3594
|
if (!resolvedProvider) {
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
3595
|
+
if (!savedProviderCfg?.family) {
|
|
3596
|
+
logger.warn(
|
|
3597
|
+
`Provider "${config.provider}" not found in models.dev. Continuing with raw config.`
|
|
3598
|
+
);
|
|
3599
|
+
}
|
|
3600
|
+
} else if (resolvedProvider.family === "unsupported" && !savedProviderCfg?.family) {
|
|
2298
3601
|
process.stderr.write(
|
|
2299
3602
|
`Provider "${config.provider}" uses an unsupported wire family (${resolvedProvider.npm}). Install a plugin to enable it, or pick a different provider.
|
|
2300
3603
|
`
|
|
@@ -2303,6 +3606,8 @@ async function main(argv) {
|
|
|
2303
3606
|
return 2;
|
|
2304
3607
|
}
|
|
2305
3608
|
const container = new Container();
|
|
3609
|
+
const configStore = new DefaultConfigStore(config);
|
|
3610
|
+
container.bind(TOKENS.ConfigStore, () => configStore);
|
|
2306
3611
|
container.bind(TOKENS.Logger, () => logger);
|
|
2307
3612
|
container.bind(TOKENS.PathResolver, () => pathResolver);
|
|
2308
3613
|
container.bind(TOKENS.SecretScrubber, () => new DefaultSecretScrubber());
|
|
@@ -2377,6 +3682,68 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2377
3682
|
}
|
|
2378
3683
|
const events = new EventBus();
|
|
2379
3684
|
events.setLogger(logger);
|
|
3685
|
+
let metricsSink;
|
|
3686
|
+
let healthRegistry;
|
|
3687
|
+
let metricsServerHandle;
|
|
3688
|
+
const metricsPortFlag = flags["metrics-port"];
|
|
3689
|
+
const metricsPort = typeof metricsPortFlag === "string" && metricsPortFlag.length > 0 ? Number.parseInt(metricsPortFlag, 10) : void 0;
|
|
3690
|
+
if (metricsPort !== void 0 && !flags.metrics) flags.metrics = true;
|
|
3691
|
+
if (flags.metrics) {
|
|
3692
|
+
metricsSink = new InMemoryMetricsSink();
|
|
3693
|
+
wireMetricsToEvents(events, metricsSink);
|
|
3694
|
+
healthRegistry = new DefaultHealthRegistry();
|
|
3695
|
+
healthRegistry.register({
|
|
3696
|
+
name: "session-store",
|
|
3697
|
+
check: async () => {
|
|
3698
|
+
try {
|
|
3699
|
+
await fs6.access(wpaths.projectSessions);
|
|
3700
|
+
return { status: "healthy" };
|
|
3701
|
+
} catch (e) {
|
|
3702
|
+
return { status: "unhealthy", detail: e instanceof Error ? e.message : "access denied" };
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
});
|
|
3706
|
+
healthRegistry.register({
|
|
3707
|
+
name: "provider",
|
|
3708
|
+
check: async () => ({
|
|
3709
|
+
status: "healthy",
|
|
3710
|
+
data: { id: config.provider, model: config.model }
|
|
3711
|
+
})
|
|
3712
|
+
});
|
|
3713
|
+
const dumpMetrics = () => {
|
|
3714
|
+
if (!metricsSink) return;
|
|
3715
|
+
try {
|
|
3716
|
+
const out = path5.join(wpaths.projectSessions, "metrics.json");
|
|
3717
|
+
const snap = metricsSink.snapshot();
|
|
3718
|
+
writeFileSync(out, JSON.stringify(snap, null, 2));
|
|
3719
|
+
} catch {
|
|
3720
|
+
}
|
|
3721
|
+
};
|
|
3722
|
+
process.on("exit", dumpMetrics);
|
|
3723
|
+
process.on("SIGINT", () => {
|
|
3724
|
+
dumpMetrics();
|
|
3725
|
+
process.exit(130);
|
|
3726
|
+
});
|
|
3727
|
+
if (metricsPort !== void 0 && Number.isFinite(metricsPort)) {
|
|
3728
|
+
try {
|
|
3729
|
+
metricsServerHandle = await startMetricsServer({
|
|
3730
|
+
port: metricsPort,
|
|
3731
|
+
host: process.env.METRICS_HOST ?? "127.0.0.1",
|
|
3732
|
+
sink: metricsSink,
|
|
3733
|
+
// V2-C: mount /healthz on the same listener so k8s probes can
|
|
3734
|
+
// hit one endpoint per pod for both observability and liveness.
|
|
3735
|
+
healthRegistry
|
|
3736
|
+
});
|
|
3737
|
+
logger.info(`metrics endpoint listening on ${metricsServerHandle.url} (healthz on same port)`);
|
|
3738
|
+
process.on("exit", () => {
|
|
3739
|
+
void metricsServerHandle?.close().catch(() => {
|
|
3740
|
+
});
|
|
3741
|
+
});
|
|
3742
|
+
} catch (err) {
|
|
3743
|
+
logger.warn(`metrics endpoint failed to start: ${err instanceof Error ? err.message : String(err)}`);
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
2380
3747
|
const spinner = new Spinner();
|
|
2381
3748
|
let lastInputTokens = 0;
|
|
2382
3749
|
events.on("provider.response", (e) => {
|
|
@@ -2434,13 +3801,11 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2434
3801
|
};
|
|
2435
3802
|
let provider;
|
|
2436
3803
|
try {
|
|
2437
|
-
|
|
2438
|
-
|
|
3804
|
+
const cfgWithType = { ...providerConfig, type: config.provider };
|
|
3805
|
+
if (config.features.modelsRegistry && providerRegistry.has(config.provider)) {
|
|
3806
|
+
provider = providerRegistry.create(cfgWithType);
|
|
2439
3807
|
} else {
|
|
2440
|
-
provider = makeProviderFromConfig(config.provider,
|
|
2441
|
-
...providerConfig,
|
|
2442
|
-
type: config.provider
|
|
2443
|
-
});
|
|
3808
|
+
provider = makeProviderFromConfig(config.provider, cfgWithType);
|
|
2444
3809
|
}
|
|
2445
3810
|
} catch (err) {
|
|
2446
3811
|
process.stderr.write(
|
|
@@ -2505,13 +3870,21 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2505
3870
|
}
|
|
2506
3871
|
await recoveryLock.write(session.id).catch(() => void 0);
|
|
2507
3872
|
const attachments = new DefaultAttachmentStore({
|
|
2508
|
-
spoolDir:
|
|
3873
|
+
spoolDir: path5.join(wpaths.projectSessions, session.id, "attachments")
|
|
2509
3874
|
});
|
|
2510
3875
|
const queueStore = new QueueStore({
|
|
2511
|
-
dir:
|
|
3876
|
+
dir: path5.join(wpaths.projectSessions, session.id)
|
|
2512
3877
|
});
|
|
2513
3878
|
const tokenCounter = container.resolve(TOKENS.TokenCounter);
|
|
2514
3879
|
const stats = new SessionStats(events, tokenCounter);
|
|
3880
|
+
const errorRing = [];
|
|
3881
|
+
events.on("error", (e) => {
|
|
3882
|
+
const err = e.err;
|
|
3883
|
+
const code2 = err && typeof err === "object" && "code" in err && typeof err.code === "string" ? err.code : "UNKNOWN";
|
|
3884
|
+
const message = e.err instanceof Error ? e.err.message : String(e.err);
|
|
3885
|
+
errorRing.push({ ts: (/* @__PURE__ */ new Date()).toISOString(), phase: e.phase, code: code2, message });
|
|
3886
|
+
if (errorRing.length > 5) errorRing.shift();
|
|
3887
|
+
});
|
|
2515
3888
|
const ctxSignal = new AbortController().signal;
|
|
2516
3889
|
const context = new Context({
|
|
2517
3890
|
systemPrompt,
|
|
@@ -2527,6 +3900,26 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2527
3900
|
context.messages.push(...restoredMessages);
|
|
2528
3901
|
}
|
|
2529
3902
|
const pipelines = createDefaultPipelines();
|
|
3903
|
+
const installBoundary = (p) => {
|
|
3904
|
+
p.setErrorHandler((ev) => {
|
|
3905
|
+
const fromPlugin = !!ev.owner && ev.owner !== "core";
|
|
3906
|
+
logger.error(
|
|
3907
|
+
`Pipeline middleware "${ev.middleware}" crashed (owner=${ev.owner ?? "unknown"}); ${fromPlugin ? "swallowed" : "rethrown"}`,
|
|
3908
|
+
ev.err
|
|
3909
|
+
);
|
|
3910
|
+
events.emit("error", {
|
|
3911
|
+
err: ev.err instanceof Error ? ev.err : new Error(String(ev.err)),
|
|
3912
|
+
phase: `pipeline:${ev.middleware}`
|
|
3913
|
+
});
|
|
3914
|
+
return fromPlugin ? "swallow" : "rethrow";
|
|
3915
|
+
});
|
|
3916
|
+
};
|
|
3917
|
+
installBoundary(pipelines.request);
|
|
3918
|
+
installBoundary(pipelines.response);
|
|
3919
|
+
installBoundary(pipelines.toolCall);
|
|
3920
|
+
installBoundary(pipelines.userInput);
|
|
3921
|
+
installBoundary(pipelines.assistantOutput);
|
|
3922
|
+
installBoundary(pipelines.contextWindow);
|
|
2530
3923
|
const compactor = container.resolve(TOKENS.Compactor);
|
|
2531
3924
|
const resolvedCaps = await capabilitiesFor(modelsRegistry, config.provider, context.model).catch(
|
|
2532
3925
|
() => void 0
|
|
@@ -2581,7 +3974,8 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2581
3974
|
maxIterations: config.tools.maxIterations,
|
|
2582
3975
|
iterationTimeoutMs: config.tools.iterationTimeoutMs,
|
|
2583
3976
|
executionStrategy: config.tools.defaultExecutionStrategy,
|
|
2584
|
-
perIterationOutputCapBytes: config.tools.perIterationOutputCapBytes
|
|
3977
|
+
perIterationOutputCapBytes: config.tools.perIterationOutputCapBytes,
|
|
3978
|
+
confirmAwaiter: makeConfirmAwaiter(reader)
|
|
2585
3979
|
});
|
|
2586
3980
|
const mcpRegistry = new MCPRegistry({ toolRegistry, events, log: logger });
|
|
2587
3981
|
if (config.features.mcp) {
|
|
@@ -2609,6 +4003,11 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2609
4003
|
const { default: createApi2 } = await Promise.resolve().then(() => (init_plugin_api_factory(), plugin_api_factory_exports));
|
|
2610
4004
|
await loadPlugins(resolvedPlugins, {
|
|
2611
4005
|
log: logger,
|
|
4006
|
+
// Each plugin's `configSchema` is validated against the matching
|
|
4007
|
+
// `Config.extensions[name]` subtree before its `setup()` runs.
|
|
4008
|
+
// The plugin then reads the same data through `api.config.extensions`
|
|
4009
|
+
// (or, once L1-B lands, via `ConfigStore.getExtension(name)`).
|
|
4010
|
+
pluginOptions: config.extensions ?? {},
|
|
2612
4011
|
apiFactory: (plugin) => createApi2(plugin.name, {
|
|
2613
4012
|
container,
|
|
2614
4013
|
events,
|
|
@@ -2623,6 +4022,73 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2623
4022
|
});
|
|
2624
4023
|
}
|
|
2625
4024
|
}
|
|
4025
|
+
const buildPickableProviders = async () => {
|
|
4026
|
+
const overlay = config.providers ?? {};
|
|
4027
|
+
let catalog = [];
|
|
4028
|
+
try {
|
|
4029
|
+
catalog = await modelsRegistry.listProviders();
|
|
4030
|
+
} catch {
|
|
4031
|
+
}
|
|
4032
|
+
const catalogById = new Map(catalog.map((p) => [p.id, p]));
|
|
4033
|
+
const hasKey = (id) => {
|
|
4034
|
+
const entry = overlay[id];
|
|
4035
|
+
const envHit = catalogById.get(id)?.envVars.some((v) => !!process.env[v]);
|
|
4036
|
+
if (envHit) return true;
|
|
4037
|
+
if (!entry) return false;
|
|
4038
|
+
if (typeof entry.apiKey === "string" && entry.apiKey.length > 0) return true;
|
|
4039
|
+
if (Array.isArray(entry.apiKeys) && entry.apiKeys.some((k) => k?.apiKey)) return true;
|
|
4040
|
+
return false;
|
|
4041
|
+
};
|
|
4042
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4043
|
+
const out = [];
|
|
4044
|
+
for (const [id, cfg] of Object.entries(overlay)) {
|
|
4045
|
+
if (!hasKey(id)) continue;
|
|
4046
|
+
seen.add(id);
|
|
4047
|
+
const catalogType = cfg.type && cfg.type !== id ? cfg.type : id;
|
|
4048
|
+
const inherited = catalogById.get(catalogType);
|
|
4049
|
+
const family = cfg.family ?? inherited?.family ?? "unsupported";
|
|
4050
|
+
if (family === "unsupported") continue;
|
|
4051
|
+
const models = cfg.models && cfg.models.length > 0 ? [...cfg.models] : (inherited?.models ?? []).map((m) => m.id);
|
|
4052
|
+
out.push({ id, family, models });
|
|
4053
|
+
}
|
|
4054
|
+
for (const p of catalog) {
|
|
4055
|
+
if (seen.has(p.id)) continue;
|
|
4056
|
+
if (p.family === "unsupported") continue;
|
|
4057
|
+
if (!hasKey(p.id)) continue;
|
|
4058
|
+
out.push({ id: p.id, family: p.family, models: p.models.map((m) => m.id) });
|
|
4059
|
+
}
|
|
4060
|
+
return out;
|
|
4061
|
+
};
|
|
4062
|
+
const switchProviderAndModel = (providerId, modelId) => {
|
|
4063
|
+
try {
|
|
4064
|
+
const newCfg = config.providers?.[providerId] ?? {
|
|
4065
|
+
type: providerId,
|
|
4066
|
+
apiKey: config.apiKey,
|
|
4067
|
+
baseUrl: config.baseUrl
|
|
4068
|
+
};
|
|
4069
|
+
const cfgWithType = { ...newCfg, type: providerId };
|
|
4070
|
+
const newProvider = config.features.modelsRegistry && providerRegistry.has(providerId) ? providerRegistry.create(cfgWithType) : makeProviderFromConfig(providerId, cfgWithType);
|
|
4071
|
+
context.provider = newProvider;
|
|
4072
|
+
context.model = modelId;
|
|
4073
|
+
config = patchConfig(config, { provider: providerId, model: modelId });
|
|
4074
|
+
configStore.update({ provider: providerId, model: modelId });
|
|
4075
|
+
return null;
|
|
4076
|
+
} catch (err) {
|
|
4077
|
+
return err instanceof Error ? err.message : String(err);
|
|
4078
|
+
}
|
|
4079
|
+
};
|
|
4080
|
+
const multiAgentHost = new MultiAgentHost({
|
|
4081
|
+
container,
|
|
4082
|
+
toolRegistry,
|
|
4083
|
+
providerRegistry,
|
|
4084
|
+
configStore,
|
|
4085
|
+
events,
|
|
4086
|
+
systemPromptBuilder: promptBuilder,
|
|
4087
|
+
session,
|
|
4088
|
+
tokenCounter,
|
|
4089
|
+
projectRoot,
|
|
4090
|
+
cwd
|
|
4091
|
+
});
|
|
2626
4092
|
const slashCmds = buildBuiltinSlashCommands({
|
|
2627
4093
|
registry: slashRegistry,
|
|
2628
4094
|
toolRegistry,
|
|
@@ -2633,53 +4099,56 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2633
4099
|
renderer,
|
|
2634
4100
|
memoryStore,
|
|
2635
4101
|
context,
|
|
4102
|
+
metricsSink,
|
|
4103
|
+
healthRegistry,
|
|
4104
|
+
onSpawn: async (description) => {
|
|
4105
|
+
const { subagentId, taskId } = await multiAgentHost.spawn(description);
|
|
4106
|
+
return `Spawned subagent ${subagentId} for task ${taskId}. Use /agents to track progress.`;
|
|
4107
|
+
},
|
|
4108
|
+
onAgents: () => {
|
|
4109
|
+
const s = multiAgentHost.status();
|
|
4110
|
+
const lines = [s.summary];
|
|
4111
|
+
for (const p of s.pending) {
|
|
4112
|
+
lines.push(` pending ${p.taskId.slice(0, 8)} \u2192 ${p.description.slice(0, 60)}`);
|
|
4113
|
+
}
|
|
4114
|
+
for (const r of s.completed) {
|
|
4115
|
+
lines.push(
|
|
4116
|
+
` ${r.status === "success" ? color.green("\u2713") : color.red("\u2717")} ${r.taskId.slice(0, 8)} ${r.iterations}it ${r.toolCalls}tc ${r.durationMs}ms`
|
|
4117
|
+
);
|
|
4118
|
+
}
|
|
4119
|
+
return lines.join("\n");
|
|
4120
|
+
},
|
|
2636
4121
|
onExit: () => {
|
|
2637
4122
|
void mcpRegistry.stopAll();
|
|
2638
4123
|
},
|
|
2639
4124
|
onClear: () => {
|
|
4125
|
+
if (flags.tui && !flags["no-tui"]) return;
|
|
2640
4126
|
try {
|
|
2641
4127
|
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
2642
4128
|
} catch {
|
|
2643
4129
|
}
|
|
2644
4130
|
},
|
|
2645
|
-
onSwitchModel: (name) => {
|
|
2646
|
-
context.model = name;
|
|
2647
|
-
},
|
|
2648
|
-
onSwitchProvider: (name) => {
|
|
2649
|
-
try {
|
|
2650
|
-
const newCfg = config.providers?.[name] ?? {
|
|
2651
|
-
type: name,
|
|
2652
|
-
apiKey: config.apiKey,
|
|
2653
|
-
baseUrl: config.baseUrl
|
|
2654
|
-
};
|
|
2655
|
-
const newProvider = providerRegistry.create({ ...newCfg, type: name });
|
|
2656
|
-
context.provider = newProvider;
|
|
2657
|
-
config = Object.freeze({ ...config, provider: name });
|
|
2658
|
-
} catch (err) {
|
|
2659
|
-
renderer.writeError(
|
|
2660
|
-
`Cannot switch to "${name}": ${err instanceof Error ? err.message : err}`
|
|
2661
|
-
);
|
|
2662
|
-
}
|
|
2663
|
-
},
|
|
2664
4131
|
onDiag: () => {
|
|
2665
4132
|
const u = tokenCounter.total();
|
|
2666
4133
|
const cost = tokenCounter.estimateCost();
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
4134
|
+
const errSection = errorRing.length === 0 ? [] : [
|
|
4135
|
+
"",
|
|
4136
|
+
`${color.bold("Recent errors")} (last ${errorRing.length}):`,
|
|
4137
|
+
...errorRing.map((e) => ` [${e.ts}] ${e.phase} ${e.code} \u2014 ${e.message}`)
|
|
4138
|
+
];
|
|
4139
|
+
const liveCfg = configStore.get();
|
|
4140
|
+
return [
|
|
4141
|
+
`${color.bold("WrongStack diag")}`,
|
|
4142
|
+
` provider: ${liveCfg.provider} / ${context.model}`,
|
|
4143
|
+
` projectRoot: ${projectRoot}`,
|
|
4144
|
+
` tokens: in ${u.input} out ${u.output} cacheR ${u.cacheRead ?? 0}`,
|
|
4145
|
+
` cost: $${cost.total.toFixed(4)}`,
|
|
4146
|
+
` tools: ${toolRegistry.list().length}`,
|
|
4147
|
+
` mcpServers: ${mcpRegistry.list().length}`,
|
|
4148
|
+
...errSection
|
|
4149
|
+
].join("\n");
|
|
2679
4150
|
},
|
|
2680
|
-
onStats: () =>
|
|
2681
|
-
stats.render(renderer);
|
|
2682
|
-
}
|
|
4151
|
+
onStats: () => stats.format()
|
|
2683
4152
|
});
|
|
2684
4153
|
for (const cmd of slashCmds) slashRegistry.register(cmd);
|
|
2685
4154
|
let code = 0;
|
|
@@ -2715,16 +4184,27 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2715
4184
|
const json = JSON.stringify({
|
|
2716
4185
|
status: result.status,
|
|
2717
4186
|
finalText: result.finalText ?? null,
|
|
2718
|
-
error: result.error
|
|
4187
|
+
error: result.error ? {
|
|
4188
|
+
code: result.error.code,
|
|
4189
|
+
subsystem: result.error.subsystem,
|
|
4190
|
+
severity: result.error.severity,
|
|
4191
|
+
recoverable: result.error.recoverable,
|
|
4192
|
+
message: result.error.message,
|
|
4193
|
+
context: result.error.context ?? null
|
|
4194
|
+
} : null,
|
|
2719
4195
|
usage
|
|
2720
4196
|
});
|
|
2721
4197
|
process.stdout.write(json + "\n");
|
|
2722
4198
|
} else {
|
|
2723
4199
|
if (result.status === "failed") {
|
|
2724
4200
|
code = 1;
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
4201
|
+
const err = result.error;
|
|
4202
|
+
if (err) {
|
|
4203
|
+
const tag = err.recoverable ? " (recoverable)" : "";
|
|
4204
|
+
renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
|
|
4205
|
+
} else {
|
|
4206
|
+
renderer.writeError("Failed.");
|
|
4207
|
+
}
|
|
2728
4208
|
} else if (result.status === "aborted") {
|
|
2729
4209
|
code = 130;
|
|
2730
4210
|
renderer.writeWarning("Aborted.");
|
|
@@ -2735,13 +4215,16 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2735
4215
|
if (result.finalText) renderer.write("\n" + result.finalText + "\n");
|
|
2736
4216
|
renderer.write(
|
|
2737
4217
|
"\n" + color.dim(
|
|
2738
|
-
`[in: ${
|
|
4218
|
+
`[in: ${fmtTok(usage.input)} out: ${fmtTok(usage.output)} iters: ${usage.iterations} cost: ${usage.cost.toFixed(4)} ${(usage.elapsedMs / 1e3).toFixed(1)}s]`
|
|
2739
4219
|
) + "\n"
|
|
2740
4220
|
);
|
|
2741
4221
|
}
|
|
2742
4222
|
} else if (flags.tui && !flags["no-tui"]) {
|
|
2743
4223
|
const { runTui } = await import('@wrongstack/tui');
|
|
2744
4224
|
renderer.setSilent(true);
|
|
4225
|
+
const banneredFamily = savedProviderCfg?.family ?? resolvedProvider?.family;
|
|
4226
|
+
const banneredKey = savedProviderCfg?.apiKey ?? config.apiKey ?? (resolvedProvider?.envVars ?? savedProviderCfg?.envVars ?? []).map((v) => process.env[v]).find((v) => !!v);
|
|
4227
|
+
const banneredKeyTail = banneredKey && banneredKey.length >= 3 ? banneredKey.slice(-3) : void 0;
|
|
2745
4228
|
try {
|
|
2746
4229
|
code = await runTui({
|
|
2747
4230
|
agent,
|
|
@@ -2755,6 +4238,10 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2755
4238
|
yolo: !!config.yolo,
|
|
2756
4239
|
appVersion: CLI_VERSION,
|
|
2757
4240
|
provider: config.provider,
|
|
4241
|
+
family: banneredFamily,
|
|
4242
|
+
keyTail: banneredKeyTail,
|
|
4243
|
+
getPickableProviders: buildPickableProviders,
|
|
4244
|
+
switchProviderAndModel,
|
|
2758
4245
|
effectiveMaxContext,
|
|
2759
4246
|
// Opt-in: alt-screen disables the terminal's native scrollback,
|
|
2760
4247
|
// so we default to false. `--no-alt-screen` is kept as a no-op
|
|
@@ -2828,11 +4315,6 @@ async function promptRecovery(reader, renderer, abandoned, autoRecover) {
|
|
|
2828
4315
|
);
|
|
2829
4316
|
return answer;
|
|
2830
4317
|
}
|
|
2831
|
-
function fmtTok4(n) {
|
|
2832
|
-
if (n < 1e3) return String(n);
|
|
2833
|
-
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
2834
|
-
return `${(n / 1e6).toFixed(1)}M`;
|
|
2835
|
-
}
|
|
2836
4318
|
var isMain = import.meta.url === `file://${process.argv[1]?.replace(/\\/g, "/")}` || process.argv[1]?.endsWith("/cli/dist/index.js") || process.argv[1]?.endsWith("\\cli\\dist\\index.js");
|
|
2837
4319
|
if (isMain) {
|
|
2838
4320
|
main(process.argv.slice(2)).then(
|
|
@@ -2848,6 +4330,6 @@ if (isMain) {
|
|
|
2848
4330
|
);
|
|
2849
4331
|
}
|
|
2850
4332
|
|
|
2851
|
-
export { main };
|
|
4333
|
+
export { CLI_VERSION, main };
|
|
2852
4334
|
//# sourceMappingURL=index.js.map
|
|
2853
4335
|
//# sourceMappingURL=index.js.map
|