@wrongstack/cli 0.1.2 → 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.js +1849 -375
- 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"}`;
|
|
@@ -1249,6 +1486,16 @@ try {
|
|
|
1249
1486
|
} catch {
|
|
1250
1487
|
}
|
|
1251
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
|
+
|
|
1252
1499
|
// src/repl.ts
|
|
1253
1500
|
async function runRepl(opts) {
|
|
1254
1501
|
if (opts.banner !== false) printBanner(opts.renderer);
|
|
@@ -1310,9 +1557,13 @@ async function runRepl(opts) {
|
|
|
1310
1557
|
if (result.status === "aborted") {
|
|
1311
1558
|
opts.renderer.writeWarning("Aborted.");
|
|
1312
1559
|
} else if (result.status === "failed") {
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
+
}
|
|
1316
1567
|
} else if (result.status === "max_iterations") {
|
|
1317
1568
|
opts.renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
|
|
1318
1569
|
}
|
|
@@ -1359,11 +1610,6 @@ async function readPossiblyMultiline(opts) {
|
|
|
1359
1610
|
}
|
|
1360
1611
|
return buf;
|
|
1361
1612
|
}
|
|
1362
|
-
function fmtTok(n) {
|
|
1363
|
-
if (n < 1e3) return String(n);
|
|
1364
|
-
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
1365
|
-
return `${(n / 1e6).toFixed(1)}M`;
|
|
1366
|
-
}
|
|
1367
1613
|
var FILLED = "\u2588";
|
|
1368
1614
|
var EMPTY = "\u2591";
|
|
1369
1615
|
function renderContextChip(used, max) {
|
|
@@ -1422,11 +1668,11 @@ var SessionStats = class {
|
|
|
1422
1668
|
if (e.name === "bash") this.bashCommands++;
|
|
1423
1669
|
else if (e.name === "fetch") this.fetches++;
|
|
1424
1670
|
if (!e.ok) return;
|
|
1425
|
-
const
|
|
1426
|
-
if (e.name === "read" &&
|
|
1427
|
-
else if (e.name === "edit" &&
|
|
1428
|
-
else if (e.name === "write" &&
|
|
1429
|
-
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);
|
|
1430
1676
|
const content = typeof input?.content === "string" ? input.content : "";
|
|
1431
1677
|
this.bytesWritten += Buffer.byteLength(content, "utf8");
|
|
1432
1678
|
}
|
|
@@ -1435,8 +1681,15 @@ var SessionStats = class {
|
|
|
1435
1681
|
hasActivity() {
|
|
1436
1682
|
return this.apiRequests > 0 || this.iterations > 0 || this.toolStats.size > 0 || this.tokenCounter.total().input > 0;
|
|
1437
1683
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
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;
|
|
1440
1693
|
const u = this.tokenCounter.total();
|
|
1441
1694
|
const cost = this.tokenCounter.estimateCost();
|
|
1442
1695
|
const elapsedSec = ((Date.now() - this.startedAt) / 1e3).toFixed(1);
|
|
@@ -1451,12 +1704,12 @@ var SessionStats = class {
|
|
|
1451
1704
|
lines.push(` Errors: ${color.yellow(String(this.errors))}`);
|
|
1452
1705
|
}
|
|
1453
1706
|
lines.push("");
|
|
1454
|
-
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)}` : ""}`);
|
|
1455
1708
|
const cache = this.tokenCounter.cacheStats();
|
|
1456
1709
|
if (cache.readTokens > 0 || cache.writeTokens > 0) {
|
|
1457
1710
|
const pct = (cache.hitRatio * 100).toFixed(1);
|
|
1458
1711
|
lines.push(
|
|
1459
|
-
` Prompt cache: ${pct}% hit ${color.dim(`(${
|
|
1712
|
+
` Prompt cache: ${pct}% hit ${color.dim(`(${fmtTok(cache.readTokens)} read / ${fmtTok(cache.writeTokens)} write)`)}`
|
|
1460
1713
|
);
|
|
1461
1714
|
}
|
|
1462
1715
|
if (cost.total > 0) {
|
|
@@ -1497,15 +1750,15 @@ var SessionStats = class {
|
|
|
1497
1750
|
if (this.fetches > 0) lines.push(` Web fetches: ${this.fetches}`);
|
|
1498
1751
|
}
|
|
1499
1752
|
lines.push("");
|
|
1500
|
-
|
|
1753
|
+
return lines.join("\n");
|
|
1754
|
+
}
|
|
1755
|
+
render(renderer) {
|
|
1756
|
+
const text = this.format();
|
|
1757
|
+
if (text === null) return;
|
|
1758
|
+
renderer.write(`${text}
|
|
1501
1759
|
`);
|
|
1502
1760
|
}
|
|
1503
1761
|
};
|
|
1504
|
-
function fmtTok2(n) {
|
|
1505
|
-
if (n < 1e3) return String(n);
|
|
1506
|
-
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
1507
|
-
return `${(n / 1e6).toFixed(1)}M`;
|
|
1508
|
-
}
|
|
1509
1762
|
function samplePaths(set) {
|
|
1510
1763
|
const arr = [...set];
|
|
1511
1764
|
if (arr.length <= 2) return arr.join(", ");
|
|
@@ -1576,7 +1829,7 @@ function renderContextChip2(ctx) {
|
|
|
1576
1829
|
const pct = Math.round(ratio * 100);
|
|
1577
1830
|
const chipColor = ratio >= 0.85 ? color.red : ratio >= 0.65 ? color.yellow : color.cyan;
|
|
1578
1831
|
const bar = renderProgress2(ratio, 8);
|
|
1579
|
-
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)})`);
|
|
1580
1833
|
}
|
|
1581
1834
|
function renderProgress2(ratio, width) {
|
|
1582
1835
|
const clamped = Math.max(0, Math.min(1, ratio));
|
|
@@ -1584,92 +1837,888 @@ function renderProgress2(ratio, width) {
|
|
|
1584
1837
|
const capped = Math.min(width, filled);
|
|
1585
1838
|
return FILLED2.repeat(capped) + EMPTY2.repeat(width - capped);
|
|
1586
1839
|
}
|
|
1587
|
-
function
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
+
};
|
|
1591
1865
|
}
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
models: modelsCmd,
|
|
1604
|
-
mcp: mcpCmd,
|
|
1605
|
-
plugin: pluginCmd,
|
|
1606
|
-
diag: diagCmd,
|
|
1607
|
-
usage: usageCmd,
|
|
1608
|
-
version: versionCmd,
|
|
1609
|
-
help: helpCmd,
|
|
1610
|
-
projects: projectsCmd
|
|
1611
|
-
};
|
|
1612
|
-
async function authCmd(args, deps) {
|
|
1613
|
-
const flags = parseAuthFlags(args);
|
|
1614
|
-
let providerId = flags.positional[0];
|
|
1615
|
-
if (!providerId) {
|
|
1616
|
-
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" };
|
|
1617
1877
|
}
|
|
1618
|
-
if (
|
|
1619
|
-
|
|
1620
|
-
|
|
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
|
+
};
|
|
1621
1887
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1888
|
+
return patch;
|
|
1889
|
+
}
|
|
1890
|
+
async function ensureProjectMeta(paths, projectRoot) {
|
|
1625
1891
|
try {
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
}
|
|
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));
|
|
1632
1899
|
} catch {
|
|
1633
1900
|
}
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
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 }
|
|
1637
1958
|
);
|
|
1638
|
-
|
|
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;
|
|
1639
1967
|
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
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
|
+
});
|
|
1646
1978
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
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();
|
|
1656
2024
|
}
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
2025
|
+
}
|
|
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}"`);
|
|
1666
2058
|
}
|
|
1667
2059
|
}
|
|
1668
|
-
function
|
|
1669
|
-
|
|
1670
|
-
|
|
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) {
|
|
2715
|
+
const out = { positional: [] };
|
|
2716
|
+
for (let i = 0; i < args.length; i++) {
|
|
1671
2717
|
const a = args[i];
|
|
1672
|
-
if (a === "--
|
|
2718
|
+
if (a === "--label") {
|
|
2719
|
+
const v = args[++i];
|
|
2720
|
+
if (v) out.label = v;
|
|
2721
|
+
} else if (a === "--family") {
|
|
1673
2722
|
const v = args[++i];
|
|
1674
2723
|
if (v) out.family = v;
|
|
1675
2724
|
} else if (a === "--base-url") {
|
|
@@ -1731,7 +2780,7 @@ async function initCmd(_args, deps) {
|
|
|
1731
2780
|
} else {
|
|
1732
2781
|
deps.renderer.writeInfo(`Found API key in env (${provider.envVars.join(" / ")}).`);
|
|
1733
2782
|
}
|
|
1734
|
-
await
|
|
2783
|
+
await fs6.mkdir(deps.paths.globalRoot, { recursive: true });
|
|
1735
2784
|
const config = {
|
|
1736
2785
|
version: 1,
|
|
1737
2786
|
provider: providerId,
|
|
@@ -1739,10 +2788,10 @@ async function initCmd(_args, deps) {
|
|
|
1739
2788
|
};
|
|
1740
2789
|
if (apiKey) config.apiKey = apiKey;
|
|
1741
2790
|
await atomicWrite(deps.paths.globalConfig, JSON.stringify(config, null, 2));
|
|
1742
|
-
await
|
|
1743
|
-
const agentsFile =
|
|
2791
|
+
await fs6.mkdir(path5.join(deps.projectRoot, ".wrongstack"), { recursive: true });
|
|
2792
|
+
const agentsFile = path5.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
|
|
1744
2793
|
try {
|
|
1745
|
-
await
|
|
2794
|
+
await fs6.access(agentsFile);
|
|
1746
2795
|
} catch {
|
|
1747
2796
|
await atomicWrite(
|
|
1748
2797
|
agentsFile,
|
|
@@ -1874,25 +2923,42 @@ async function modelsCmd(args, deps) {
|
|
|
1874
2923
|
deps.renderer.writeError("Usage: wstack models <provider> | refresh");
|
|
1875
2924
|
return 1;
|
|
1876
2925
|
}
|
|
1877
|
-
|
|
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);
|
|
1878
2932
|
if (!provider) {
|
|
1879
|
-
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
|
+
);
|
|
1880
2936
|
return 1;
|
|
1881
2937
|
}
|
|
2938
|
+
if (lookupId !== providerId) {
|
|
2939
|
+
deps.renderer.write(color.dim(`(showing catalog models for "${lookupId}" via alias "${providerId}")
|
|
2940
|
+
`));
|
|
2941
|
+
}
|
|
1882
2942
|
deps.renderer.write(`${color.bold(provider.name)} ${color.dim(`(${provider.id})`)}
|
|
1883
2943
|
`);
|
|
1884
2944
|
if (provider.doc) deps.renderer.write(color.dim(`Docs: ${provider.doc}
|
|
1885
2945
|
`));
|
|
1886
|
-
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(
|
|
1887
2949
|
(a, b) => (b.release_date ?? "").localeCompare(a.release_date ?? "")
|
|
1888
2950
|
);
|
|
2951
|
+
if (userModels && userModels.length > 0) {
|
|
2952
|
+
deps.renderer.write(color.dim(`(${userModels.length} model(s) from your saved config)
|
|
2953
|
+
`));
|
|
2954
|
+
}
|
|
1889
2955
|
for (const m of sorted) {
|
|
1890
2956
|
const caps = [];
|
|
1891
|
-
if (m.tool_call) caps.push("tools");
|
|
1892
|
-
if (m.reasoning) caps.push("reasoning");
|
|
1893
|
-
if (m.modalities?.input?.includes("image")) caps.push("vision");
|
|
1894
|
-
const ctx = m.limit?.context ? `${(m.limit.context / 1e3).toFixed(0)}k` : "?";
|
|
1895
|
-
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 ?? "?"}` : "";
|
|
1896
2962
|
deps.renderer.write(
|
|
1897
2963
|
` ${m.id.padEnd(40)} ${color.dim(ctx.padStart(6))} ${color.dim(cost.padEnd(14))} ${color.dim(caps.join(","))}
|
|
1898
2964
|
`
|
|
@@ -1914,16 +2980,46 @@ async function mcpCmd(args, deps) {
|
|
|
1914
2980
|
const servers = deps.config.mcpServers ?? {};
|
|
1915
2981
|
if (Object.keys(servers).length === 0) {
|
|
1916
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");
|
|
1917
2984
|
return 0;
|
|
1918
2985
|
}
|
|
1919
2986
|
for (const [name, cfg] of Object.entries(servers)) {
|
|
2987
|
+
const status = cfg.enabled === false ? "disabled" : "enabled";
|
|
2988
|
+
const desc = cfg.description ? ` # ${cfg.description}` : "";
|
|
1920
2989
|
deps.renderer.write(
|
|
1921
|
-
` ${name.padEnd(20)} ${cfg.transport}
|
|
2990
|
+
` ${name.padEnd(20)} ${cfg.transport.padEnd(16)} ${status}${desc}
|
|
1922
2991
|
`
|
|
1923
2992
|
);
|
|
1924
2993
|
}
|
|
1925
2994
|
return 0;
|
|
1926
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
|
+
}
|
|
1927
3023
|
if (sub === "restart") {
|
|
1928
3024
|
deps.renderer.writeWarning("mcp restart is only available in REPL mode.");
|
|
1929
3025
|
return 0;
|
|
@@ -1931,6 +3027,70 @@ async function mcpCmd(args, deps) {
|
|
|
1931
3027
|
deps.renderer.writeError(`Unknown mcp subcommand: ${sub}`);
|
|
1932
3028
|
return 1;
|
|
1933
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
|
+
}
|
|
1934
3094
|
async function pluginCmd(args, deps) {
|
|
1935
3095
|
const sub = args[0];
|
|
1936
3096
|
if (!sub || sub === "list") {
|
|
@@ -1964,7 +3124,7 @@ async function diagCmd(_args, deps) {
|
|
|
1964
3124
|
` modelsCache: ${deps.paths.modelsCache}`,
|
|
1965
3125
|
` cacheAge: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never"}`,
|
|
1966
3126
|
` node: ${process.version}`,
|
|
1967
|
-
` os: ${
|
|
3127
|
+
` os: ${os3.platform()} ${os3.release()}`,
|
|
1968
3128
|
` provider: ${cfg.provider ?? "<unset>"}`,
|
|
1969
3129
|
` model: ${cfg.model ?? "<unset>"}`,
|
|
1970
3130
|
` tools: ${deps.toolRegistry?.list().length ?? 0}`,
|
|
@@ -1974,6 +3134,182 @@ async function diagCmd(_args, deps) {
|
|
|
1974
3134
|
deps.renderer.write(lines.join("\n") + "\n");
|
|
1975
3135
|
return 0;
|
|
1976
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
|
+
}
|
|
1977
3313
|
async function usageCmd(_args, deps) {
|
|
1978
3314
|
if (!deps.sessionStore) return 0;
|
|
1979
3315
|
const list = await deps.sessionStore.list(100);
|
|
@@ -1985,7 +3321,7 @@ async function usageCmd(_args, deps) {
|
|
|
1985
3321
|
}
|
|
1986
3322
|
async function versionCmd(_args, deps) {
|
|
1987
3323
|
deps.renderer.write(
|
|
1988
|
-
`WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${
|
|
3324
|
+
`WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${os3.platform()})
|
|
1989
3325
|
`
|
|
1990
3326
|
);
|
|
1991
3327
|
return 0;
|
|
@@ -1999,7 +3335,8 @@ async function helpCmd(_args, deps) {
|
|
|
1999
3335
|
" wstack resume [<id>] Resume a session",
|
|
2000
3336
|
" wstack sessions List recent sessions",
|
|
2001
3337
|
" wstack init Pick provider + model from models.dev",
|
|
2002
|
-
" 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)",
|
|
2003
3340
|
" wstack resume <id> Resume a session (loads transcript + appends)",
|
|
2004
3341
|
" wstack config [show|edit] Show or edit effective config",
|
|
2005
3342
|
" wstack tools List registered tools",
|
|
@@ -2011,6 +3348,8 @@ async function helpCmd(_args, deps) {
|
|
|
2011
3348
|
" wstack plugin [list] List plugins",
|
|
2012
3349
|
" wstack projects List projects tracked in ~/.wrongstack/projects/",
|
|
2013
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>)",
|
|
2014
3353
|
" wstack usage Token + cost summary",
|
|
2015
3354
|
" wstack version Print version",
|
|
2016
3355
|
"",
|
|
@@ -2021,9 +3360,9 @@ async function helpCmd(_args, deps) {
|
|
|
2021
3360
|
return 0;
|
|
2022
3361
|
}
|
|
2023
3362
|
async function projectsCmd(_args, deps) {
|
|
2024
|
-
const projectsRoot =
|
|
3363
|
+
const projectsRoot = path5.join(deps.paths.globalRoot, "projects");
|
|
2025
3364
|
try {
|
|
2026
|
-
const entries = await
|
|
3365
|
+
const entries = await fs6.readdir(projectsRoot);
|
|
2027
3366
|
if (entries.length === 0) {
|
|
2028
3367
|
deps.renderer.write("No projects tracked.\n");
|
|
2029
3368
|
return 0;
|
|
@@ -2031,7 +3370,7 @@ async function projectsCmd(_args, deps) {
|
|
|
2031
3370
|
for (const hash of entries) {
|
|
2032
3371
|
try {
|
|
2033
3372
|
const meta = JSON.parse(
|
|
2034
|
-
await
|
|
3373
|
+
await fs6.readFile(path5.join(projectsRoot, hash, "meta.json"), "utf8")
|
|
2035
3374
|
);
|
|
2036
3375
|
deps.renderer.write(
|
|
2037
3376
|
` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
|
|
@@ -2078,7 +3417,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
2078
3417
|
"no-alt-screen",
|
|
2079
3418
|
"alt-screen",
|
|
2080
3419
|
"output-json",
|
|
2081
|
-
"prompt"
|
|
3420
|
+
"prompt",
|
|
3421
|
+
"metrics"
|
|
2082
3422
|
]);
|
|
2083
3423
|
function parseArgs(argv) {
|
|
2084
3424
|
const flags = {};
|
|
@@ -2116,83 +3456,32 @@ function parseArgs(argv) {
|
|
|
2116
3456
|
}
|
|
2117
3457
|
return { flags, positional };
|
|
2118
3458
|
}
|
|
2119
|
-
function flagsToConfigPatch(flags) {
|
|
2120
|
-
const patch = {};
|
|
2121
|
-
if (typeof flags["provider"] === "string") patch.provider = flags["provider"];
|
|
2122
|
-
if (typeof flags["model"] === "string") patch.model = flags["model"];
|
|
2123
|
-
if (typeof flags["cwd"] === "string") patch.cwd = flags["cwd"];
|
|
2124
|
-
if (typeof flags["log-level"] === "string") {
|
|
2125
|
-
patch.log = { level: flags["log-level"] };
|
|
2126
|
-
} else if (flags["verbose"]) {
|
|
2127
|
-
patch.log = { level: "debug" };
|
|
2128
|
-
} else if (flags["trace"]) {
|
|
2129
|
-
patch.log = { level: "trace" };
|
|
2130
|
-
}
|
|
2131
|
-
if (flags["yolo"]) patch.yolo = true;
|
|
2132
|
-
if (flags["no-features"]) {
|
|
2133
|
-
patch.features = {
|
|
2134
|
-
mcp: false,
|
|
2135
|
-
plugins: false,
|
|
2136
|
-
memory: false,
|
|
2137
|
-
modelsRegistry: false,
|
|
2138
|
-
skills: false
|
|
2139
|
-
};
|
|
2140
|
-
}
|
|
2141
|
-
return patch;
|
|
2142
|
-
}
|
|
2143
3459
|
function resolveBundledSkillsDir() {
|
|
2144
3460
|
try {
|
|
2145
3461
|
const req2 = createRequire(import.meta.url);
|
|
2146
3462
|
const corePkg = req2.resolve("@wrongstack/core/package.json");
|
|
2147
|
-
return
|
|
3463
|
+
return path5.join(path5.dirname(corePkg), "skills");
|
|
2148
3464
|
} catch {
|
|
2149
3465
|
return void 0;
|
|
2150
3466
|
}
|
|
2151
3467
|
}
|
|
2152
|
-
async function ensureProjectMeta(paths, projectRoot) {
|
|
2153
|
-
try {
|
|
2154
|
-
await fs2.mkdir(paths.projectDir, { recursive: true });
|
|
2155
|
-
const meta = {
|
|
2156
|
-
hash: paths.projectHash,
|
|
2157
|
-
root: projectRoot,
|
|
2158
|
-
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
2159
|
-
};
|
|
2160
|
-
await fs2.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
|
|
2161
|
-
} catch {
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
3468
|
async function main(argv) {
|
|
2165
3469
|
const { flags, positional } = parseArgs(argv);
|
|
2166
|
-
const cwd = typeof flags["cwd"] === "string" ? path2.resolve(flags["cwd"]) : process.cwd();
|
|
2167
|
-
const pathResolver = new DefaultPathResolver(cwd);
|
|
2168
|
-
const projectRoot = pathResolver.projectRoot;
|
|
2169
|
-
const userHome = os2.homedir();
|
|
2170
|
-
const wpaths = resolveWstackPaths({ projectRoot, userHome });
|
|
2171
|
-
await ensureProjectMeta(wpaths, projectRoot);
|
|
2172
3470
|
if (positional[0] === "resume" && positional[1] && !subcommands["__noop_resume_marker"]) {
|
|
2173
3471
|
flags["resume"] = positional[1];
|
|
2174
3472
|
positional.splice(0, 2);
|
|
2175
3473
|
}
|
|
2176
|
-
|
|
2177
|
-
for (const file of [wpaths.globalConfig, wpaths.projectLocalConfig]) {
|
|
2178
|
-
try {
|
|
2179
|
-
const { migrated } = await migratePlaintextSecrets(file, vault);
|
|
2180
|
-
if (migrated > 0) {
|
|
2181
|
-
process.stderr.write(`[wstack] Encrypted ${migrated} plaintext secret(s) in ${file}
|
|
2182
|
-
`);
|
|
2183
|
-
}
|
|
2184
|
-
} catch {
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
const configLoader = new DefaultConfigLoader({ paths: wpaths, vault });
|
|
2188
|
-
let config;
|
|
3474
|
+
let boot;
|
|
2189
3475
|
try {
|
|
2190
|
-
|
|
3476
|
+
boot = await bootConfig(flags);
|
|
2191
3477
|
} catch (err) {
|
|
2192
3478
|
process.stderr.write(`Config error: ${err instanceof Error ? err.message : String(err)}
|
|
2193
3479
|
`);
|
|
2194
3480
|
return 2;
|
|
2195
3481
|
}
|
|
3482
|
+
const { paths, config: _config, vault } = boot;
|
|
3483
|
+
let config = _config;
|
|
3484
|
+
const { cwd, projectRoot, userHome, wpaths, pathResolver } = paths;
|
|
2196
3485
|
const logger = new DefaultLogger({
|
|
2197
3486
|
level: config.log.level,
|
|
2198
3487
|
file: wpaths.logFile
|
|
@@ -2259,7 +3548,7 @@ async function main(argv) {
|
|
|
2259
3548
|
} else {
|
|
2260
3549
|
const prevProvider = config.provider;
|
|
2261
3550
|
const prevModel = config.model;
|
|
2262
|
-
config =
|
|
3551
|
+
config = patchConfig(config, { provider: picked.provider, model: picked.model });
|
|
2263
3552
|
if (picked.provider !== prevProvider || picked.model !== prevModel) {
|
|
2264
3553
|
const saved = await saveToGlobalConfig(
|
|
2265
3554
|
wpaths.globalConfig,
|
|
@@ -2294,15 +3583,21 @@ async function main(argv) {
|
|
|
2294
3583
|
flags["no-tui"] = true;
|
|
2295
3584
|
}
|
|
2296
3585
|
if (choices.yolo !== config.yolo) {
|
|
2297
|
-
config =
|
|
3586
|
+
config = patchConfig(config, { yolo: choices.yolo });
|
|
2298
3587
|
}
|
|
2299
3588
|
}
|
|
2300
|
-
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
|
+
}
|
|
2301
3594
|
if (!resolvedProvider) {
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
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) {
|
|
2306
3601
|
process.stderr.write(
|
|
2307
3602
|
`Provider "${config.provider}" uses an unsupported wire family (${resolvedProvider.npm}). Install a plugin to enable it, or pick a different provider.
|
|
2308
3603
|
`
|
|
@@ -2311,6 +3606,8 @@ async function main(argv) {
|
|
|
2311
3606
|
return 2;
|
|
2312
3607
|
}
|
|
2313
3608
|
const container = new Container();
|
|
3609
|
+
const configStore = new DefaultConfigStore(config);
|
|
3610
|
+
container.bind(TOKENS.ConfigStore, () => configStore);
|
|
2314
3611
|
container.bind(TOKENS.Logger, () => logger);
|
|
2315
3612
|
container.bind(TOKENS.PathResolver, () => pathResolver);
|
|
2316
3613
|
container.bind(TOKENS.SecretScrubber, () => new DefaultSecretScrubber());
|
|
@@ -2385,6 +3682,68 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2385
3682
|
}
|
|
2386
3683
|
const events = new EventBus();
|
|
2387
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
|
+
}
|
|
2388
3747
|
const spinner = new Spinner();
|
|
2389
3748
|
let lastInputTokens = 0;
|
|
2390
3749
|
events.on("provider.response", (e) => {
|
|
@@ -2442,13 +3801,11 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2442
3801
|
};
|
|
2443
3802
|
let provider;
|
|
2444
3803
|
try {
|
|
2445
|
-
|
|
2446
|
-
|
|
3804
|
+
const cfgWithType = { ...providerConfig, type: config.provider };
|
|
3805
|
+
if (config.features.modelsRegistry && providerRegistry.has(config.provider)) {
|
|
3806
|
+
provider = providerRegistry.create(cfgWithType);
|
|
2447
3807
|
} else {
|
|
2448
|
-
provider = makeProviderFromConfig(config.provider,
|
|
2449
|
-
...providerConfig,
|
|
2450
|
-
type: config.provider
|
|
2451
|
-
});
|
|
3808
|
+
provider = makeProviderFromConfig(config.provider, cfgWithType);
|
|
2452
3809
|
}
|
|
2453
3810
|
} catch (err) {
|
|
2454
3811
|
process.stderr.write(
|
|
@@ -2513,13 +3870,21 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2513
3870
|
}
|
|
2514
3871
|
await recoveryLock.write(session.id).catch(() => void 0);
|
|
2515
3872
|
const attachments = new DefaultAttachmentStore({
|
|
2516
|
-
spoolDir:
|
|
3873
|
+
spoolDir: path5.join(wpaths.projectSessions, session.id, "attachments")
|
|
2517
3874
|
});
|
|
2518
3875
|
const queueStore = new QueueStore({
|
|
2519
|
-
dir:
|
|
3876
|
+
dir: path5.join(wpaths.projectSessions, session.id)
|
|
2520
3877
|
});
|
|
2521
3878
|
const tokenCounter = container.resolve(TOKENS.TokenCounter);
|
|
2522
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
|
+
});
|
|
2523
3888
|
const ctxSignal = new AbortController().signal;
|
|
2524
3889
|
const context = new Context({
|
|
2525
3890
|
systemPrompt,
|
|
@@ -2535,6 +3900,26 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2535
3900
|
context.messages.push(...restoredMessages);
|
|
2536
3901
|
}
|
|
2537
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);
|
|
2538
3923
|
const compactor = container.resolve(TOKENS.Compactor);
|
|
2539
3924
|
const resolvedCaps = await capabilitiesFor(modelsRegistry, config.provider, context.model).catch(
|
|
2540
3925
|
() => void 0
|
|
@@ -2589,7 +3974,8 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2589
3974
|
maxIterations: config.tools.maxIterations,
|
|
2590
3975
|
iterationTimeoutMs: config.tools.iterationTimeoutMs,
|
|
2591
3976
|
executionStrategy: config.tools.defaultExecutionStrategy,
|
|
2592
|
-
perIterationOutputCapBytes: config.tools.perIterationOutputCapBytes
|
|
3977
|
+
perIterationOutputCapBytes: config.tools.perIterationOutputCapBytes,
|
|
3978
|
+
confirmAwaiter: makeConfirmAwaiter(reader)
|
|
2593
3979
|
});
|
|
2594
3980
|
const mcpRegistry = new MCPRegistry({ toolRegistry, events, log: logger });
|
|
2595
3981
|
if (config.features.mcp) {
|
|
@@ -2617,6 +4003,11 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2617
4003
|
const { default: createApi2 } = await Promise.resolve().then(() => (init_plugin_api_factory(), plugin_api_factory_exports));
|
|
2618
4004
|
await loadPlugins(resolvedPlugins, {
|
|
2619
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 ?? {},
|
|
2620
4011
|
apiFactory: (plugin) => createApi2(plugin.name, {
|
|
2621
4012
|
container,
|
|
2622
4013
|
events,
|
|
@@ -2631,6 +4022,73 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2631
4022
|
});
|
|
2632
4023
|
}
|
|
2633
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
|
+
});
|
|
2634
4092
|
const slashCmds = buildBuiltinSlashCommands({
|
|
2635
4093
|
registry: slashRegistry,
|
|
2636
4094
|
toolRegistry,
|
|
@@ -2641,53 +4099,56 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2641
4099
|
renderer,
|
|
2642
4100
|
memoryStore,
|
|
2643
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
|
+
},
|
|
2644
4121
|
onExit: () => {
|
|
2645
4122
|
void mcpRegistry.stopAll();
|
|
2646
4123
|
},
|
|
2647
4124
|
onClear: () => {
|
|
4125
|
+
if (flags.tui && !flags["no-tui"]) return;
|
|
2648
4126
|
try {
|
|
2649
4127
|
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
2650
4128
|
} catch {
|
|
2651
4129
|
}
|
|
2652
4130
|
},
|
|
2653
|
-
onSwitchModel: (name) => {
|
|
2654
|
-
context.model = name;
|
|
2655
|
-
},
|
|
2656
|
-
onSwitchProvider: (name) => {
|
|
2657
|
-
try {
|
|
2658
|
-
const newCfg = config.providers?.[name] ?? {
|
|
2659
|
-
type: name,
|
|
2660
|
-
apiKey: config.apiKey,
|
|
2661
|
-
baseUrl: config.baseUrl
|
|
2662
|
-
};
|
|
2663
|
-
const newProvider = providerRegistry.create({ ...newCfg, type: name });
|
|
2664
|
-
context.provider = newProvider;
|
|
2665
|
-
config = Object.freeze({ ...config, provider: name });
|
|
2666
|
-
} catch (err) {
|
|
2667
|
-
renderer.writeError(
|
|
2668
|
-
`Cannot switch to "${name}": ${err instanceof Error ? err.message : err}`
|
|
2669
|
-
);
|
|
2670
|
-
}
|
|
2671
|
-
},
|
|
2672
4131
|
onDiag: () => {
|
|
2673
4132
|
const u = tokenCounter.total();
|
|
2674
4133
|
const cost = tokenCounter.estimateCost();
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
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");
|
|
2687
4150
|
},
|
|
2688
|
-
onStats: () =>
|
|
2689
|
-
stats.render(renderer);
|
|
2690
|
-
}
|
|
4151
|
+
onStats: () => stats.format()
|
|
2691
4152
|
});
|
|
2692
4153
|
for (const cmd of slashCmds) slashRegistry.register(cmd);
|
|
2693
4154
|
let code = 0;
|
|
@@ -2723,16 +4184,27 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2723
4184
|
const json = JSON.stringify({
|
|
2724
4185
|
status: result.status,
|
|
2725
4186
|
finalText: result.finalText ?? null,
|
|
2726
|
-
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,
|
|
2727
4195
|
usage
|
|
2728
4196
|
});
|
|
2729
4197
|
process.stdout.write(json + "\n");
|
|
2730
4198
|
} else {
|
|
2731
4199
|
if (result.status === "failed") {
|
|
2732
4200
|
code = 1;
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
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
|
+
}
|
|
2736
4208
|
} else if (result.status === "aborted") {
|
|
2737
4209
|
code = 130;
|
|
2738
4210
|
renderer.writeWarning("Aborted.");
|
|
@@ -2743,13 +4215,16 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2743
4215
|
if (result.finalText) renderer.write("\n" + result.finalText + "\n");
|
|
2744
4216
|
renderer.write(
|
|
2745
4217
|
"\n" + color.dim(
|
|
2746
|
-
`[in: ${
|
|
4218
|
+
`[in: ${fmtTok(usage.input)} out: ${fmtTok(usage.output)} iters: ${usage.iterations} cost: ${usage.cost.toFixed(4)} ${(usage.elapsedMs / 1e3).toFixed(1)}s]`
|
|
2747
4219
|
) + "\n"
|
|
2748
4220
|
);
|
|
2749
4221
|
}
|
|
2750
4222
|
} else if (flags.tui && !flags["no-tui"]) {
|
|
2751
4223
|
const { runTui } = await import('@wrongstack/tui');
|
|
2752
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;
|
|
2753
4228
|
try {
|
|
2754
4229
|
code = await runTui({
|
|
2755
4230
|
agent,
|
|
@@ -2763,6 +4238,10 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2763
4238
|
yolo: !!config.yolo,
|
|
2764
4239
|
appVersion: CLI_VERSION,
|
|
2765
4240
|
provider: config.provider,
|
|
4241
|
+
family: banneredFamily,
|
|
4242
|
+
keyTail: banneredKeyTail,
|
|
4243
|
+
getPickableProviders: buildPickableProviders,
|
|
4244
|
+
switchProviderAndModel,
|
|
2766
4245
|
effectiveMaxContext,
|
|
2767
4246
|
// Opt-in: alt-screen disables the terminal's native scrollback,
|
|
2768
4247
|
// so we default to false. `--no-alt-screen` is kept as a no-op
|
|
@@ -2836,11 +4315,6 @@ async function promptRecovery(reader, renderer, abandoned, autoRecover) {
|
|
|
2836
4315
|
);
|
|
2837
4316
|
return answer;
|
|
2838
4317
|
}
|
|
2839
|
-
function fmtTok4(n) {
|
|
2840
|
-
if (n < 1e3) return String(n);
|
|
2841
|
-
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
2842
|
-
return `${(n / 1e6).toFixed(1)}M`;
|
|
2843
|
-
}
|
|
2844
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");
|
|
2845
4319
|
if (isMain) {
|
|
2846
4320
|
main(process.argv.slice(2)).then(
|