@wrongstack/cli 0.1.0
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/LICENSE +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2464 -0
- package/dist/index.js.map +1 -0
- package/package.json +37 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2464 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { color, DefaultPathResolver, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultLogger, DefaultModelsRegistry, Container, TOKENS, DefaultSecretScrubber, DefaultRetryPolicy, DefaultErrorHandler, DefaultTokenCounter, DefaultSessionStore, DefaultMemoryStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultPermissionPolicy, HybridCompactor, ProviderRegistry, ToolRegistry, createContextManagerTool, EventBus, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, createDefaultPipelines, AutoCompactionMiddleware, Agent, SlashCommandRegistry, loadPlugins, InputBuilder, DefaultPluginAPI, rewriteConfigEncrypted, atomicWrite } from '@wrongstack/core';
|
|
3
|
+
import * as fs2 from 'fs/promises';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import * as os2 from 'os';
|
|
6
|
+
import * as path2 from 'path';
|
|
7
|
+
import { MCPRegistry } from '@wrongstack/mcp';
|
|
8
|
+
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig, capabilitiesFor } from '@wrongstack/providers';
|
|
9
|
+
import { builtinTools, rememberTool, forgetTool } from '@wrongstack/tools';
|
|
10
|
+
import * as readline from 'readline';
|
|
11
|
+
|
|
12
|
+
var __defProp = Object.defineProperty;
|
|
13
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __esm = (fn, res) => function __init() {
|
|
15
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
16
|
+
};
|
|
17
|
+
var __export = (target, all) => {
|
|
18
|
+
for (var name in all)
|
|
19
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/plugin-api-factory.ts
|
|
23
|
+
var plugin_api_factory_exports = {};
|
|
24
|
+
__export(plugin_api_factory_exports, {
|
|
25
|
+
default: () => createApi
|
|
26
|
+
});
|
|
27
|
+
function createApi(ownerName, base) {
|
|
28
|
+
return new DefaultPluginAPI({ ownerName, ...base });
|
|
29
|
+
}
|
|
30
|
+
var init_plugin_api_factory = __esm({
|
|
31
|
+
"src/plugin-api-factory.ts"() {
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
var ReadlineInputReader = class {
|
|
35
|
+
rl;
|
|
36
|
+
historyFile;
|
|
37
|
+
history = [];
|
|
38
|
+
pending = false;
|
|
39
|
+
constructor(opts = {}) {
|
|
40
|
+
this.historyFile = opts.historyFile ?? path2.join(os2.homedir(), ".wrongstack", "history");
|
|
41
|
+
}
|
|
42
|
+
async loadHistory() {
|
|
43
|
+
try {
|
|
44
|
+
const raw = await fs2.readFile(this.historyFile, "utf8");
|
|
45
|
+
this.history = raw.split("\n").filter(Boolean).slice(-1e3);
|
|
46
|
+
} catch {
|
|
47
|
+
this.history = [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async saveHistory() {
|
|
51
|
+
try {
|
|
52
|
+
await fs2.mkdir(path2.dirname(this.historyFile), { recursive: true });
|
|
53
|
+
await fs2.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
ensure() {
|
|
58
|
+
if (!this.rl) {
|
|
59
|
+
this.rl = readline.createInterface({
|
|
60
|
+
input: process.stdin,
|
|
61
|
+
output: process.stdout,
|
|
62
|
+
history: this.history,
|
|
63
|
+
terminal: process.stdin.isTTY
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return this.rl;
|
|
67
|
+
}
|
|
68
|
+
async readLine(prompt) {
|
|
69
|
+
if (this.history.length === 0) await this.loadHistory();
|
|
70
|
+
while (this.pending) {
|
|
71
|
+
await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
72
|
+
}
|
|
73
|
+
this.pending = true;
|
|
74
|
+
try {
|
|
75
|
+
const rl = this.ensure();
|
|
76
|
+
if (rl._flushed) {
|
|
77
|
+
rl.close();
|
|
78
|
+
this.rl = void 0;
|
|
79
|
+
}
|
|
80
|
+
const fresh = this.ensure();
|
|
81
|
+
return new Promise((resolve2, reject) => {
|
|
82
|
+
fresh.question(prompt ?? "> ", (line) => {
|
|
83
|
+
if (line.trim()) {
|
|
84
|
+
this.history.push(line);
|
|
85
|
+
void this.saveHistory();
|
|
86
|
+
}
|
|
87
|
+
resolve2(line);
|
|
88
|
+
});
|
|
89
|
+
fresh.once("close", () => reject(new Error("EOF")));
|
|
90
|
+
});
|
|
91
|
+
} finally {
|
|
92
|
+
this.pending = false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async readKey(prompt, options) {
|
|
96
|
+
process.stdout.write(prompt);
|
|
97
|
+
return new Promise((resolve2) => {
|
|
98
|
+
const stdin = process.stdin;
|
|
99
|
+
const wasRaw = stdin.isRaw;
|
|
100
|
+
const wasPaused = stdin.isPaused();
|
|
101
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
102
|
+
stdin.resume();
|
|
103
|
+
const onData = (buf) => {
|
|
104
|
+
const key = buf.toString();
|
|
105
|
+
const opt = options.find(
|
|
106
|
+
(o) => o.key.toLowerCase() === key.toLowerCase() || o.value === key
|
|
107
|
+
);
|
|
108
|
+
if (opt) {
|
|
109
|
+
cleanup();
|
|
110
|
+
process.stdout.write(`${opt.key}
|
|
111
|
+
`);
|
|
112
|
+
resolve2(opt.value);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const cleanup = () => {
|
|
116
|
+
stdin.off("data", onData);
|
|
117
|
+
if (stdin.isTTY) stdin.setRawMode(wasRaw);
|
|
118
|
+
if (wasPaused) stdin.pause();
|
|
119
|
+
};
|
|
120
|
+
stdin.on("data", onData);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Read a single line of input without echoing it to the terminal. Used
|
|
125
|
+
* for API keys / passwords. Non-TTY input is read normally — there's
|
|
126
|
+
* nothing to hide when piped.
|
|
127
|
+
*/
|
|
128
|
+
async readSecret(prompt) {
|
|
129
|
+
const stdin = process.stdin;
|
|
130
|
+
if (!stdin.isTTY) return this.readLine(prompt);
|
|
131
|
+
this.rl?.close();
|
|
132
|
+
this.rl = void 0;
|
|
133
|
+
process.stdout.write(prompt);
|
|
134
|
+
return new Promise((resolve2) => {
|
|
135
|
+
let buf = "";
|
|
136
|
+
const wasRaw = stdin.isRaw;
|
|
137
|
+
stdin.setRawMode(true);
|
|
138
|
+
stdin.resume();
|
|
139
|
+
stdin.setEncoding("utf8");
|
|
140
|
+
const onData = (chunk) => {
|
|
141
|
+
for (const ch of chunk) {
|
|
142
|
+
if (ch === "\r" || ch === "\n") {
|
|
143
|
+
cleanup();
|
|
144
|
+
process.stdout.write("\n");
|
|
145
|
+
resolve2(buf);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (ch === "") {
|
|
149
|
+
cleanup();
|
|
150
|
+
process.stdout.write("\n");
|
|
151
|
+
process.exit(130);
|
|
152
|
+
}
|
|
153
|
+
if (ch === "\x7F" || ch === "\b") {
|
|
154
|
+
buf = buf.slice(0, -1);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
buf += ch;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const cleanup = () => {
|
|
161
|
+
stdin.off("data", onData);
|
|
162
|
+
stdin.setRawMode(wasRaw);
|
|
163
|
+
stdin.pause();
|
|
164
|
+
};
|
|
165
|
+
stdin.on("data", onData);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async close() {
|
|
169
|
+
await this.saveHistory();
|
|
170
|
+
this.rl?.close();
|
|
171
|
+
this.rl = void 0;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
function renderDiff(diff) {
|
|
175
|
+
if (!diff) return "";
|
|
176
|
+
const lines = diff.split("\n");
|
|
177
|
+
return lines.map((line) => {
|
|
178
|
+
if (line.startsWith("+++") || line.startsWith("---")) return color.bold(line);
|
|
179
|
+
if (line.startsWith("@@")) return color.cyan(line);
|
|
180
|
+
if (line.startsWith("+")) return color.green(line);
|
|
181
|
+
if (line.startsWith("-")) return color.red(line);
|
|
182
|
+
return color.dim(line);
|
|
183
|
+
}).join("\n");
|
|
184
|
+
}
|
|
185
|
+
var theme = {
|
|
186
|
+
primary: color.amber,
|
|
187
|
+
accent: color.pink,
|
|
188
|
+
muted: color.dim,
|
|
189
|
+
success: color.green,
|
|
190
|
+
warn: color.yellow,
|
|
191
|
+
error: color.red,
|
|
192
|
+
info: color.cyan,
|
|
193
|
+
bold: color.bold,
|
|
194
|
+
underline: color.underline
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// src/permission-prompt.ts
|
|
198
|
+
function makePromptDelegate(reader) {
|
|
199
|
+
return async (tool, input, suggestedPattern) => {
|
|
200
|
+
process.stdout.write(`
|
|
201
|
+
${theme.primary("\u258D")} ${theme.bold(tool.name)}
|
|
202
|
+
`);
|
|
203
|
+
process.stdout.write(`${color.dim(stringifyInput(input))}
|
|
204
|
+
`);
|
|
205
|
+
if (tool.name === "edit" && hasDiff(input)) {
|
|
206
|
+
const inp = input;
|
|
207
|
+
const diff = typeof inp.diff === "string" ? inp.diff : "";
|
|
208
|
+
if (diff) process.stdout.write(`${renderDiff(diff)}
|
|
209
|
+
`);
|
|
210
|
+
}
|
|
211
|
+
process.stdout.write(color.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
212
|
+
const answer = await reader.readKey(
|
|
213
|
+
`${theme.bold("[y]")}es ${theme.bold("[n]")}o ${theme.bold("[a]")}lways allow (${suggestedPattern}) ${theme.bold("[d]")}eny: `,
|
|
214
|
+
[
|
|
215
|
+
{ key: "y", label: "yes", value: "yes" },
|
|
216
|
+
{ key: "n", label: "no", value: "no" },
|
|
217
|
+
{ key: "a", label: "always", value: "always" },
|
|
218
|
+
{ key: "d", label: "deny", value: "deny" }
|
|
219
|
+
]
|
|
220
|
+
);
|
|
221
|
+
return answer;
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function stringifyInput(input) {
|
|
225
|
+
if (!input || typeof input !== "object") return "";
|
|
226
|
+
const obj = input;
|
|
227
|
+
return Object.entries(obj).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${truncate(JSON.stringify(v), 80)}`).join(" ");
|
|
228
|
+
}
|
|
229
|
+
function truncate(s, max) {
|
|
230
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
231
|
+
}
|
|
232
|
+
function hasDiff(input) {
|
|
233
|
+
return Boolean(
|
|
234
|
+
input && typeof input === "object" && "diff" in input
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
var TerminalRenderer = class {
|
|
238
|
+
out;
|
|
239
|
+
err;
|
|
240
|
+
lineStart = true;
|
|
241
|
+
/**
|
|
242
|
+
* When true, every stdout-bound method is a no-op. This is the only
|
|
243
|
+
* safe state to be in while Ink owns the terminal (TUI mode):
|
|
244
|
+
* raw writes to stdout interleave with Ink's cursor math and cause
|
|
245
|
+
* the input + status bar to be reprinted as scrollback junk.
|
|
246
|
+
* Stderr-bound methods (writeInfo/Warning/Error) still flow — they
|
|
247
|
+
* go to a different stream Ink does not manage.
|
|
248
|
+
*/
|
|
249
|
+
silent = false;
|
|
250
|
+
constructor(opts = {}) {
|
|
251
|
+
this.out = opts.out ?? process.stdout;
|
|
252
|
+
this.err = opts.err ?? process.stderr;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Toggle stdout suppression. Call `setSilent(true)` right before
|
|
256
|
+
* handing the terminal to Ink, and `setSilent(false)` after Ink
|
|
257
|
+
* exits. Idempotent.
|
|
258
|
+
*/
|
|
259
|
+
setSilent(silent) {
|
|
260
|
+
this.silent = silent;
|
|
261
|
+
}
|
|
262
|
+
isSilent() {
|
|
263
|
+
return this.silent;
|
|
264
|
+
}
|
|
265
|
+
write(input) {
|
|
266
|
+
if (this.silent) return;
|
|
267
|
+
const text = typeof input === "string" ? input : input.text;
|
|
268
|
+
if (!text) return;
|
|
269
|
+
const rendered = renderMarkdown(text);
|
|
270
|
+
this.out.write(rendered);
|
|
271
|
+
this.lineStart = rendered.endsWith("\n");
|
|
272
|
+
}
|
|
273
|
+
writeLine(text = "") {
|
|
274
|
+
if (this.silent) return;
|
|
275
|
+
if (!this.lineStart) this.out.write("\n");
|
|
276
|
+
if (text) this.out.write(`${text}
|
|
277
|
+
`);
|
|
278
|
+
else this.out.write("\n");
|
|
279
|
+
this.lineStart = true;
|
|
280
|
+
}
|
|
281
|
+
writeBlock(block) {
|
|
282
|
+
if (this.silent) return;
|
|
283
|
+
if (block.type === "text") {
|
|
284
|
+
this.write(block);
|
|
285
|
+
} else if (block.type === "tool_use") {
|
|
286
|
+
this.writeToolCall(block.name, block.input);
|
|
287
|
+
} else if (block.type === "tool_result") {
|
|
288
|
+
const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
|
|
289
|
+
this.writeToolResult("result", text, !!block.is_error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
writeToolCall(name, input) {
|
|
293
|
+
if (this.silent) return;
|
|
294
|
+
if (!this.lineStart) this.out.write("\n");
|
|
295
|
+
const arrow = theme.primary("\u2192");
|
|
296
|
+
const display = formatInputSummary(input);
|
|
297
|
+
this.out.write(`${arrow} ${theme.bold(name)}${display ? color.dim(` ${display}`) : ""}
|
|
298
|
+
`);
|
|
299
|
+
this.lineStart = true;
|
|
300
|
+
}
|
|
301
|
+
writeToolResult(name, content, isError) {
|
|
302
|
+
if (this.silent) return;
|
|
303
|
+
const txt = typeof content === "string" ? content : safeStringify(content);
|
|
304
|
+
const prefix = isError ? theme.error("\u2718") : theme.success("\u2713");
|
|
305
|
+
if (isError) {
|
|
306
|
+
const firstLine = txt.split("\n")[0] ?? "";
|
|
307
|
+
const truncated = firstLine.length > 200 ? `${firstLine.slice(0, 197)}\u2026` : firstLine;
|
|
308
|
+
this.out.write(` ${prefix} ${color.dim(truncated)}
|
|
309
|
+
`);
|
|
310
|
+
this.lineStart = true;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const isEditLike = name === "edit" || name === "write";
|
|
314
|
+
const isReadLike = name === "read" || name === "grep" || name === "glob" || name === "bash";
|
|
315
|
+
const previewLines = isEditLike ? 0 : isReadLike ? 6 : 2;
|
|
316
|
+
const diff = extractDiff(content);
|
|
317
|
+
if (isEditLike && diff) {
|
|
318
|
+
this.out.write(` ${prefix} ${color.dim(summarize(content, name))}
|
|
319
|
+
`);
|
|
320
|
+
const rendered = renderDiff(diff).split("\n").map((l) => ` ${l}`).join("\n");
|
|
321
|
+
this.out.write(`${rendered}
|
|
322
|
+
`);
|
|
323
|
+
this.lineStart = true;
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const lines = txt.split("\n");
|
|
327
|
+
const head = lines.slice(0, previewLines).map((l) => l.replace(/\s+$/, ""));
|
|
328
|
+
const moreCount = Math.max(0, lines.length - head.length);
|
|
329
|
+
this.out.write(` ${prefix} ${color.dim(summarize(content, name))}
|
|
330
|
+
`);
|
|
331
|
+
for (const l of head) {
|
|
332
|
+
const capped = l.length > 200 ? `${l.slice(0, 197)}\u2026` : l;
|
|
333
|
+
this.out.write(` ${color.dim(capped)}
|
|
334
|
+
`);
|
|
335
|
+
}
|
|
336
|
+
if (moreCount > 0) {
|
|
337
|
+
this.out.write(` ${color.dim(`+${moreCount} more line${moreCount === 1 ? "" : "s"}`)}
|
|
338
|
+
`);
|
|
339
|
+
}
|
|
340
|
+
this.lineStart = true;
|
|
341
|
+
}
|
|
342
|
+
writeDiff(diff) {
|
|
343
|
+
if (this.silent) return;
|
|
344
|
+
if (!this.lineStart) this.out.write("\n");
|
|
345
|
+
this.out.write(`${renderDiff(diff)}
|
|
346
|
+
`);
|
|
347
|
+
this.lineStart = true;
|
|
348
|
+
}
|
|
349
|
+
writeWarning(text) {
|
|
350
|
+
this.err.write(`${theme.warn("\u26A0")} ${text}
|
|
351
|
+
`);
|
|
352
|
+
}
|
|
353
|
+
writeError(text) {
|
|
354
|
+
this.err.write(`${theme.error("\u2718")} ${text}
|
|
355
|
+
`);
|
|
356
|
+
}
|
|
357
|
+
writeInfo(text) {
|
|
358
|
+
this.err.write(`${theme.info("\u2139")} ${text}
|
|
359
|
+
`);
|
|
360
|
+
}
|
|
361
|
+
clear() {
|
|
362
|
+
if (this.silent) return;
|
|
363
|
+
this.out.write("\x1B[2J\x1B[H");
|
|
364
|
+
this.lineStart = true;
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
function renderMarkdown(s) {
|
|
368
|
+
let out = s;
|
|
369
|
+
out = out.replace(/^(#{1,6}) (.+)$/gm, (_m, hashes, text) => {
|
|
370
|
+
return theme.primary(theme.bold(`${hashes} ${text}`));
|
|
371
|
+
});
|
|
372
|
+
out = out.replace(/```([a-zA-Z0-9_+-]*)\n([\s\S]*?)```/g, (_m, _lang, code) => {
|
|
373
|
+
return color.gray(`
|
|
374
|
+
\u250C\u2500
|
|
375
|
+
${code.replace(/^/gm, "\u2502 ")}\u2514\u2500`);
|
|
376
|
+
});
|
|
377
|
+
out = out.replace(/`([^`\n]+)`/g, (_m, code) => theme.accent(code));
|
|
378
|
+
out = out.replace(/\*\*([^*]+)\*\*/g, (_m, text) => theme.bold(text));
|
|
379
|
+
out = out.replace(/(^|[^*])\*([^*\n]+)\*([^*]|$)/g, (_m, l, t, r) => `${l}${color.italic(t)}${r}`);
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
function formatInputSummary(input) {
|
|
383
|
+
if (!input || typeof input !== "object") return "";
|
|
384
|
+
const obj = input;
|
|
385
|
+
if (typeof obj["path"] === "string") return obj["path"];
|
|
386
|
+
if (typeof obj["url"] === "string") return obj["url"];
|
|
387
|
+
if (typeof obj["command"] === "string") {
|
|
388
|
+
const cmd = obj["command"];
|
|
389
|
+
return cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
390
|
+
}
|
|
391
|
+
if (typeof obj["pattern"] === "string") return obj["pattern"];
|
|
392
|
+
return "";
|
|
393
|
+
}
|
|
394
|
+
function safeStringify(value) {
|
|
395
|
+
if (typeof value === "string") return value;
|
|
396
|
+
try {
|
|
397
|
+
return JSON.stringify(value);
|
|
398
|
+
} catch {
|
|
399
|
+
return String(value);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function extractDiff(value) {
|
|
403
|
+
if (typeof value === "object" && value !== null) {
|
|
404
|
+
const d = value.diff;
|
|
405
|
+
if (typeof d === "string" && d.length > 0) return d;
|
|
406
|
+
}
|
|
407
|
+
if (typeof value === "string") {
|
|
408
|
+
const trimmed = value.trimStart();
|
|
409
|
+
if (trimmed.startsWith("{")) {
|
|
410
|
+
try {
|
|
411
|
+
const parsed = JSON.parse(value);
|
|
412
|
+
if (typeof parsed.diff === "string" && parsed.diff.length > 0) {
|
|
413
|
+
return parsed.diff;
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (/^---[^\n]*\n\+\+\+/m.test(value)) return value;
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
function summarize(value, name) {
|
|
423
|
+
let v = value;
|
|
424
|
+
if (typeof value === "string") {
|
|
425
|
+
const trimmed = value.trimStart();
|
|
426
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
427
|
+
try {
|
|
428
|
+
v = JSON.parse(value);
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (typeof v === "object" && v !== null) {
|
|
434
|
+
const o = v;
|
|
435
|
+
if (name === "edit") {
|
|
436
|
+
const path5 = typeof o["path"] === "string" ? o["path"] : "";
|
|
437
|
+
const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
|
|
438
|
+
return `${path5} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
|
|
439
|
+
}
|
|
440
|
+
if (name === "write") {
|
|
441
|
+
const path5 = typeof o["path"] === "string" ? o["path"] : "";
|
|
442
|
+
const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
|
|
443
|
+
return bytes !== void 0 ? `${path5} ${bytes}B` : path5;
|
|
444
|
+
}
|
|
445
|
+
if (typeof o["count"] === "number") {
|
|
446
|
+
return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return "";
|
|
450
|
+
}
|
|
451
|
+
async function runRepl(opts) {
|
|
452
|
+
if (opts.banner !== false) printBanner(opts.renderer);
|
|
453
|
+
let activeCtrl;
|
|
454
|
+
let interrupts = 0;
|
|
455
|
+
const onSigint = () => {
|
|
456
|
+
interrupts++;
|
|
457
|
+
if (interrupts >= 2) {
|
|
458
|
+
opts.renderer.writeWarning("Exiting.");
|
|
459
|
+
process.exit(130);
|
|
460
|
+
}
|
|
461
|
+
if (activeCtrl) {
|
|
462
|
+
activeCtrl.abort();
|
|
463
|
+
opts.renderer.writeWarning("Iteration cancelled. Press Ctrl+C again to exit.");
|
|
464
|
+
} else {
|
|
465
|
+
opts.renderer.writeWarning("Press Ctrl+C again to exit.");
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
process.on("SIGINT", onSigint);
|
|
469
|
+
const builder = new InputBuilder({ store: opts.attachments });
|
|
470
|
+
for (; ; ) {
|
|
471
|
+
let raw;
|
|
472
|
+
try {
|
|
473
|
+
raw = await readPossiblyMultiline(opts);
|
|
474
|
+
} catch {
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
const trimmed = raw.trim();
|
|
478
|
+
if (!trimmed) {
|
|
479
|
+
interrupts = 0;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
interrupts = 0;
|
|
483
|
+
if (trimmed.startsWith("/")) {
|
|
484
|
+
try {
|
|
485
|
+
const res = await opts.slashRegistry.dispatch(trimmed, opts.agent.ctx);
|
|
486
|
+
if (res?.message) opts.renderer.write(`${res.message}
|
|
487
|
+
`);
|
|
488
|
+
if (res?.exit) break;
|
|
489
|
+
} catch (err) {
|
|
490
|
+
opts.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
491
|
+
}
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
const ph = await builder.appendPaste(raw);
|
|
495
|
+
if (ph) {
|
|
496
|
+
const lineCount = raw.split("\n").length;
|
|
497
|
+
opts.renderer.write(color.dim(` \u21B3 ${ph} (${lineCount} lines)
|
|
498
|
+
`));
|
|
499
|
+
}
|
|
500
|
+
const blocks = await builder.submit();
|
|
501
|
+
const runCtrl = new AbortController();
|
|
502
|
+
activeCtrl = runCtrl;
|
|
503
|
+
try {
|
|
504
|
+
const startedAt = Date.now();
|
|
505
|
+
const before = opts.tokenCounter?.total();
|
|
506
|
+
const costBefore = opts.tokenCounter?.estimateCost().total ?? 0;
|
|
507
|
+
const result = await opts.agent.run(blocks, { signal: runCtrl.signal });
|
|
508
|
+
if (result.status === "aborted") {
|
|
509
|
+
opts.renderer.writeWarning("Aborted.");
|
|
510
|
+
} else if (result.status === "failed") {
|
|
511
|
+
opts.renderer.writeError(
|
|
512
|
+
`Failed: ${result.error instanceof Error ? result.error.message : String(result.error)}`
|
|
513
|
+
);
|
|
514
|
+
} else if (result.status === "max_iterations") {
|
|
515
|
+
opts.renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
|
|
516
|
+
}
|
|
517
|
+
if (opts.tokenCounter && before) {
|
|
518
|
+
const after = opts.tokenCounter.total();
|
|
519
|
+
const costAfter = opts.tokenCounter.estimateCost().total;
|
|
520
|
+
const ctxChip = opts.effectiveMaxContext && opts.effectiveMaxContext > 0 ? ` ctx: ${renderContextChip(after.input, opts.effectiveMaxContext)}` : "";
|
|
521
|
+
opts.renderer.write(
|
|
522
|
+
`
|
|
523
|
+
${color.dim(
|
|
524
|
+
`[in: ${fmtTok(after.input - before.input)} out: ${fmtTok(after.output - before.output)} iters: ${result.iterations} cost: ${(costAfter - costBefore).toFixed(4)} ${((Date.now() - startedAt) / 1e3).toFixed(1)}s]${ctxChip}`
|
|
525
|
+
)}
|
|
526
|
+
`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
} catch (err) {
|
|
530
|
+
opts.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
531
|
+
} finally {
|
|
532
|
+
activeCtrl = void 0;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
process.off("SIGINT", onSigint);
|
|
536
|
+
await opts.reader.close();
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
async function readPossiblyMultiline(opts) {
|
|
540
|
+
const firstPrompt = theme.primary("\u203A ");
|
|
541
|
+
const contPrompt = color.dim("\xB7 ");
|
|
542
|
+
const first = await opts.reader.readLine(firstPrompt);
|
|
543
|
+
if (first.trim() === '"""') {
|
|
544
|
+
const parts = [];
|
|
545
|
+
for (; ; ) {
|
|
546
|
+
const next = await opts.reader.readLine(contPrompt);
|
|
547
|
+
if (next.trim() === '"""') break;
|
|
548
|
+
parts.push(next);
|
|
549
|
+
}
|
|
550
|
+
return parts.join("\n");
|
|
551
|
+
}
|
|
552
|
+
let buf = first;
|
|
553
|
+
while (buf.endsWith("\\")) {
|
|
554
|
+
buf = buf.slice(0, -1);
|
|
555
|
+
const cont = await opts.reader.readLine(contPrompt);
|
|
556
|
+
buf += "\n" + cont;
|
|
557
|
+
}
|
|
558
|
+
return buf;
|
|
559
|
+
}
|
|
560
|
+
function fmtTok(n) {
|
|
561
|
+
if (n < 1e3) return String(n);
|
|
562
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
563
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
564
|
+
}
|
|
565
|
+
var FILLED = "\u2588";
|
|
566
|
+
var EMPTY = "\u2591";
|
|
567
|
+
function renderContextChip(used, max) {
|
|
568
|
+
const ratio = Math.max(0, Math.min(1, used / max));
|
|
569
|
+
const pct = Math.round(ratio * 100);
|
|
570
|
+
const bar = renderProgress(ratio, 6);
|
|
571
|
+
return `${bar} ${pct}% (${fmtTok(used)}/${fmtTok(max)})`;
|
|
572
|
+
}
|
|
573
|
+
function renderProgress(ratio, width) {
|
|
574
|
+
const clamped = Math.max(0, Math.min(1, ratio));
|
|
575
|
+
const filled = clamped === 0 ? 0 : Math.max(1, Math.round(clamped * width));
|
|
576
|
+
const capped = Math.min(width, filled);
|
|
577
|
+
return FILLED.repeat(capped) + EMPTY.repeat(width - capped);
|
|
578
|
+
}
|
|
579
|
+
function printBanner(renderer) {
|
|
580
|
+
const lines = [
|
|
581
|
+
theme.primary(theme.bold("WrongStack")) + color.dim(" v0.0.1"),
|
|
582
|
+
color.dim("Built on the wrong stack. Shipped anyway."),
|
|
583
|
+
color.dim("Type /help for commands, /exit to quit."),
|
|
584
|
+
""
|
|
585
|
+
];
|
|
586
|
+
renderer.write(`${lines.join("\n")}
|
|
587
|
+
`);
|
|
588
|
+
}
|
|
589
|
+
var SessionStats = class {
|
|
590
|
+
tokenCounter;
|
|
591
|
+
startedAt = Date.now();
|
|
592
|
+
apiRequests = 0;
|
|
593
|
+
iterations = 0;
|
|
594
|
+
errors = 0;
|
|
595
|
+
toolStats = /* @__PURE__ */ new Map();
|
|
596
|
+
readPaths = /* @__PURE__ */ new Set();
|
|
597
|
+
editedPaths = /* @__PURE__ */ new Set();
|
|
598
|
+
writtenPaths = /* @__PURE__ */ new Set();
|
|
599
|
+
bytesWritten = 0;
|
|
600
|
+
bashCommands = 0;
|
|
601
|
+
fetches = 0;
|
|
602
|
+
constructor(events, tokenCounter) {
|
|
603
|
+
this.tokenCounter = tokenCounter;
|
|
604
|
+
events.on("provider.response", () => {
|
|
605
|
+
this.apiRequests++;
|
|
606
|
+
});
|
|
607
|
+
events.on("iteration.completed", () => {
|
|
608
|
+
this.iterations++;
|
|
609
|
+
});
|
|
610
|
+
events.on("error", () => {
|
|
611
|
+
this.errors++;
|
|
612
|
+
});
|
|
613
|
+
events.on("tool.executed", (e) => {
|
|
614
|
+
const slot = this.toolStats.get(e.name) ?? { ok: 0, fail: 0, totalMs: 0 };
|
|
615
|
+
if (e.ok) slot.ok++;
|
|
616
|
+
else slot.fail++;
|
|
617
|
+
slot.totalMs += e.durationMs;
|
|
618
|
+
this.toolStats.set(e.name, slot);
|
|
619
|
+
const input = e.input;
|
|
620
|
+
if (e.name === "bash") this.bashCommands++;
|
|
621
|
+
else if (e.name === "fetch") this.fetches++;
|
|
622
|
+
if (!e.ok) return;
|
|
623
|
+
const path5 = typeof input?.path === "string" ? input.path : void 0;
|
|
624
|
+
if (e.name === "read" && path5) this.readPaths.add(path5);
|
|
625
|
+
else if (e.name === "edit" && path5) this.editedPaths.add(path5);
|
|
626
|
+
else if (e.name === "write" && path5) {
|
|
627
|
+
this.writtenPaths.add(path5);
|
|
628
|
+
const content = typeof input?.content === "string" ? input.content : "";
|
|
629
|
+
this.bytesWritten += Buffer.byteLength(content, "utf8");
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
hasActivity() {
|
|
634
|
+
return this.apiRequests > 0 || this.iterations > 0 || this.toolStats.size > 0 || this.tokenCounter.total().input > 0;
|
|
635
|
+
}
|
|
636
|
+
render(renderer) {
|
|
637
|
+
if (!this.hasActivity()) return;
|
|
638
|
+
const u = this.tokenCounter.total();
|
|
639
|
+
const cost = this.tokenCounter.estimateCost();
|
|
640
|
+
const elapsedSec = ((Date.now() - this.startedAt) / 1e3).toFixed(1);
|
|
641
|
+
const lines = [];
|
|
642
|
+
lines.push("");
|
|
643
|
+
lines.push(color.bold("Session report"));
|
|
644
|
+
lines.push(color.dim("\u2500".repeat(40)));
|
|
645
|
+
lines.push(` Elapsed: ${elapsedSec}s`);
|
|
646
|
+
lines.push(` Iterations: ${this.iterations}`);
|
|
647
|
+
lines.push(` API requests: ${this.apiRequests}`);
|
|
648
|
+
if (this.errors > 0) {
|
|
649
|
+
lines.push(` Errors: ${color.yellow(String(this.errors))}`);
|
|
650
|
+
}
|
|
651
|
+
lines.push("");
|
|
652
|
+
lines.push(` Tokens: in ${fmtTok2(u.input)} out ${fmtTok2(u.output)}${u.cacheRead ? ` cacheR ${fmtTok2(u.cacheRead)}` : ""}${u.cacheWrite ? ` cacheW ${fmtTok2(u.cacheWrite)}` : ""}`);
|
|
653
|
+
const cache = this.tokenCounter.cacheStats();
|
|
654
|
+
if (cache.readTokens > 0 || cache.writeTokens > 0) {
|
|
655
|
+
const pct = (cache.hitRatio * 100).toFixed(1);
|
|
656
|
+
lines.push(
|
|
657
|
+
` Prompt cache: ${pct}% hit ${color.dim(`(${fmtTok2(cache.readTokens)} read / ${fmtTok2(cache.writeTokens)} write)`)}`
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
if (cost.total > 0) {
|
|
661
|
+
lines.push(` Cost: $${cost.total.toFixed(4)}${color.dim(` (in $${cost.input.toFixed(4)} / out $${cost.output.toFixed(4)})`)}`);
|
|
662
|
+
} else {
|
|
663
|
+
lines.push(` Cost: ${color.dim("$0 (no pricing on this plan)")}`);
|
|
664
|
+
}
|
|
665
|
+
if (this.toolStats.size > 0) {
|
|
666
|
+
lines.push("");
|
|
667
|
+
lines.push(` ${color.bold("Tool calls")}`);
|
|
668
|
+
const sorted = [...this.toolStats.entries()].sort(
|
|
669
|
+
(a, b) => b[1].ok + b[1].fail - (a[1].ok + a[1].fail)
|
|
670
|
+
);
|
|
671
|
+
for (const [name, s] of sorted) {
|
|
672
|
+
const total = s.ok + s.fail;
|
|
673
|
+
const failPart = s.fail > 0 ? color.yellow(` (${s.fail} failed)`) : "";
|
|
674
|
+
const avgMs = total > 0 ? Math.round(s.totalMs / total) : 0;
|
|
675
|
+
lines.push(` ${name.padEnd(12)} ${String(total).padStart(3)}\xD7 ${color.dim(`avg ${avgMs}ms`)}${failPart}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const fileActivity = this.readPaths.size > 0 || this.editedPaths.size > 0 || this.writtenPaths.size > 0 || this.bytesWritten > 0;
|
|
679
|
+
if (fileActivity) {
|
|
680
|
+
lines.push("");
|
|
681
|
+
lines.push(` ${color.bold("Files")}`);
|
|
682
|
+
if (this.readPaths.size > 0)
|
|
683
|
+
lines.push(` read: ${this.readPaths.size} ${color.dim(samplePaths(this.readPaths))}`);
|
|
684
|
+
if (this.editedPaths.size > 0)
|
|
685
|
+
lines.push(` edited: ${this.editedPaths.size} ${color.dim(samplePaths(this.editedPaths))}`);
|
|
686
|
+
if (this.writtenPaths.size > 0) {
|
|
687
|
+
const bytes = this.bytesWritten;
|
|
688
|
+
const byteStr = bytes > 1024 ? `${(bytes / 1024).toFixed(1)}KB` : `${bytes}B`;
|
|
689
|
+
lines.push(` written: ${this.writtenPaths.size} (${byteStr}) ${color.dim(samplePaths(this.writtenPaths))}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (this.bashCommands > 0 || this.fetches > 0) {
|
|
693
|
+
lines.push("");
|
|
694
|
+
if (this.bashCommands > 0) lines.push(` Shell commands: ${this.bashCommands}`);
|
|
695
|
+
if (this.fetches > 0) lines.push(` Web fetches: ${this.fetches}`);
|
|
696
|
+
}
|
|
697
|
+
lines.push("");
|
|
698
|
+
renderer.write(`${lines.join("\n")}
|
|
699
|
+
`);
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
function fmtTok2(n) {
|
|
703
|
+
if (n < 1e3) return String(n);
|
|
704
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
705
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
706
|
+
}
|
|
707
|
+
function samplePaths(set) {
|
|
708
|
+
const arr = [...set];
|
|
709
|
+
if (arr.length <= 2) return arr.join(", ");
|
|
710
|
+
return `${arr[0]}, \u2026 (+${arr.length - 1} more)`;
|
|
711
|
+
}
|
|
712
|
+
function buildBuiltinSlashCommands(opts) {
|
|
713
|
+
return [
|
|
714
|
+
helpCommand(opts),
|
|
715
|
+
initCommand(opts),
|
|
716
|
+
clearCommand(opts),
|
|
717
|
+
compactCommand(opts),
|
|
718
|
+
contextCommand(opts),
|
|
719
|
+
usageCommand(opts),
|
|
720
|
+
toolsCommand(opts),
|
|
721
|
+
skillCommand(opts),
|
|
722
|
+
useCommand(opts),
|
|
723
|
+
modelCommand(opts),
|
|
724
|
+
diagCommand(opts),
|
|
725
|
+
statsCommand(opts),
|
|
726
|
+
saveCommand(opts),
|
|
727
|
+
loadCommand(opts),
|
|
728
|
+
exitCommand(opts)
|
|
729
|
+
];
|
|
730
|
+
}
|
|
731
|
+
function initCommand(opts) {
|
|
732
|
+
return {
|
|
733
|
+
name: "init",
|
|
734
|
+
description: "Scaffold .wrongstack/AGENTS.md in the current project.",
|
|
735
|
+
async run(args, ctx) {
|
|
736
|
+
const force = args.trim() === "--force";
|
|
737
|
+
const dir = path2.join(ctx.projectRoot, ".wrongstack");
|
|
738
|
+
const file = path2.join(dir, "AGENTS.md");
|
|
739
|
+
try {
|
|
740
|
+
await fs2.access(file);
|
|
741
|
+
if (!force) {
|
|
742
|
+
const msg = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
|
|
743
|
+
opts.renderer.writeWarning(msg);
|
|
744
|
+
return { message: msg };
|
|
745
|
+
}
|
|
746
|
+
} catch {
|
|
747
|
+
}
|
|
748
|
+
const detected = await detectProjectFacts(ctx.projectRoot);
|
|
749
|
+
const body = renderAgentsTemplate(detected);
|
|
750
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
751
|
+
await fs2.writeFile(file, body, "utf8");
|
|
752
|
+
if (detected.hints.length > 0) {
|
|
753
|
+
const msg = `Wrote ${file}
|
|
754
|
+
Pre-filled: ${detected.hints.join(", ")}. Edit the file to add anything else worth remembering.`;
|
|
755
|
+
opts.renderer.writeInfo(`Wrote ${file}`);
|
|
756
|
+
opts.renderer.writeInfo(`Pre-filled: ${detected.hints.join(", ")}. Edit the file to add anything else worth remembering.`);
|
|
757
|
+
return { message: msg };
|
|
758
|
+
} else {
|
|
759
|
+
const msg = `Wrote ${file}
|
|
760
|
+
No project type auto-detected. Edit the file to add build/test commands and conventions.`;
|
|
761
|
+
opts.renderer.writeInfo(`Wrote ${file}`);
|
|
762
|
+
return { message: msg };
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
async function detectProjectFacts(root) {
|
|
768
|
+
const facts = { hints: [] };
|
|
769
|
+
try {
|
|
770
|
+
const pkg = JSON.parse(await fs2.readFile(path2.join(root, "package.json"), "utf8"));
|
|
771
|
+
const scripts = pkg.scripts ?? {};
|
|
772
|
+
const pm = (pkg.packageManager ?? "npm").split("@")[0] ?? "npm";
|
|
773
|
+
if (scripts["build"]) facts.build = `${pm} run build`;
|
|
774
|
+
if (scripts["test"]) facts.test = `${pm} test`;
|
|
775
|
+
if (scripts["lint"]) facts.lint = `${pm} run lint`;
|
|
776
|
+
if (scripts["dev"] ?? scripts["start"]) facts.run = `${pm} run ${scripts["dev"] ? "dev" : "start"}`;
|
|
777
|
+
facts.hints.push("package.json scripts");
|
|
778
|
+
} catch {
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
await fs2.access(path2.join(root, "pyproject.toml"));
|
|
782
|
+
facts.test ??= "pytest";
|
|
783
|
+
facts.lint ??= "ruff check .";
|
|
784
|
+
facts.hints.push("pyproject.toml");
|
|
785
|
+
} catch {
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
await fs2.access(path2.join(root, "go.mod"));
|
|
789
|
+
facts.build ??= "go build ./...";
|
|
790
|
+
facts.test ??= "go test ./...";
|
|
791
|
+
facts.hints.push("go.mod");
|
|
792
|
+
} catch {
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
await fs2.access(path2.join(root, "Cargo.toml"));
|
|
796
|
+
facts.build ??= "cargo build";
|
|
797
|
+
facts.test ??= "cargo test";
|
|
798
|
+
facts.hints.push("Cargo.toml");
|
|
799
|
+
} catch {
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
await fs2.access(path2.join(root, "Makefile"));
|
|
803
|
+
facts.build ??= "make";
|
|
804
|
+
facts.test ??= "make test";
|
|
805
|
+
facts.hints.push("Makefile");
|
|
806
|
+
} catch {
|
|
807
|
+
}
|
|
808
|
+
return facts;
|
|
809
|
+
}
|
|
810
|
+
function renderAgentsTemplate(f) {
|
|
811
|
+
const cmd = (s) => s ? `\`${s}\`` : "_TODO_";
|
|
812
|
+
return `# AGENTS.md
|
|
813
|
+
|
|
814
|
+
Project notes for WrongStack. Committed to the repo so every contributor
|
|
815
|
+
(human or agent) starts with the same context. Edit freely.
|
|
816
|
+
|
|
817
|
+
## What this project is
|
|
818
|
+
|
|
819
|
+
_One paragraph: what does this codebase do, who runs it, what's the
|
|
820
|
+
deployment target?_
|
|
821
|
+
|
|
822
|
+
## How to work on it
|
|
823
|
+
|
|
824
|
+
- **Build:** ${cmd(f.build)}
|
|
825
|
+
- **Test:** ${cmd(f.test)}
|
|
826
|
+
- **Lint:** ${cmd(f.lint)}
|
|
827
|
+
- **Run locally:** ${cmd(f.run)}
|
|
828
|
+
|
|
829
|
+
## Conventions
|
|
830
|
+
|
|
831
|
+
_What style choices matter here? Filenames, module layout, naming, error
|
|
832
|
+
handling, log format. Anything a stranger would get wrong._
|
|
833
|
+
|
|
834
|
+
## Domain knowledge
|
|
835
|
+
|
|
836
|
+
_Acronyms, business rules, foot-guns, "this looks weird but it's
|
|
837
|
+
intentional because\u2026"._
|
|
838
|
+
|
|
839
|
+
## Pointers
|
|
840
|
+
|
|
841
|
+
_Where to look for: routing, database migrations, feature flags,
|
|
842
|
+
on-call runbooks, dashboards._
|
|
843
|
+
`;
|
|
844
|
+
}
|
|
845
|
+
function diagCommand(opts) {
|
|
846
|
+
return {
|
|
847
|
+
name: "diag",
|
|
848
|
+
description: "Show runtime diagnostics (provider, tokens, tools, MCP).",
|
|
849
|
+
async run() {
|
|
850
|
+
if (opts.onDiag) {
|
|
851
|
+
opts.onDiag();
|
|
852
|
+
return { message: "diag" };
|
|
853
|
+
} else {
|
|
854
|
+
return { message: "Diag not available in this context." };
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function statsCommand(opts) {
|
|
860
|
+
return {
|
|
861
|
+
name: "stats",
|
|
862
|
+
description: "Show session report: tokens, requests, tools, files, cost.",
|
|
863
|
+
async run() {
|
|
864
|
+
if (opts.onStats) {
|
|
865
|
+
opts.onStats();
|
|
866
|
+
return { message: "stats" };
|
|
867
|
+
} else {
|
|
868
|
+
return { message: "Stats not available in this context." };
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function helpCommand(opts) {
|
|
874
|
+
return {
|
|
875
|
+
name: "help",
|
|
876
|
+
description: "Show available slash commands.",
|
|
877
|
+
async run() {
|
|
878
|
+
const lines = ["Available slash commands:"];
|
|
879
|
+
for (const { cmd, owner, fullName } of opts.registry.listWithOwner()) {
|
|
880
|
+
const isBuiltin = owner === "core";
|
|
881
|
+
const prefix = isBuiltin ? "" : `${owner}:`;
|
|
882
|
+
const aliases = cmd.aliases ? cmd.aliases.map((a) => `/${prefix}${a}`).join(", ") : "";
|
|
883
|
+
const aliasStr = aliases ? ` (${aliases})` : "";
|
|
884
|
+
lines.push(` /${prefix}${cmd.name}${aliasStr} \u2014 ${cmd.description}`);
|
|
885
|
+
}
|
|
886
|
+
return { message: lines.join("\n") };
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
function clearCommand(opts) {
|
|
891
|
+
return {
|
|
892
|
+
name: "clear",
|
|
893
|
+
description: "Reset the session and start a new one.",
|
|
894
|
+
async run(_args, ctx) {
|
|
895
|
+
if (ctx) {
|
|
896
|
+
ctx.messages = [];
|
|
897
|
+
ctx.todos = [];
|
|
898
|
+
ctx.readFiles.clear();
|
|
899
|
+
ctx.fileMtimes.clear();
|
|
900
|
+
ctx.meta = {};
|
|
901
|
+
}
|
|
902
|
+
await opts.memoryStore?.clear();
|
|
903
|
+
opts.onClear?.();
|
|
904
|
+
opts.renderer.clear();
|
|
905
|
+
const msg = "Session cleared (context, memory, and history reset).";
|
|
906
|
+
opts.renderer.writeInfo(msg);
|
|
907
|
+
return { message: msg };
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function contextCommand(opts) {
|
|
912
|
+
return {
|
|
913
|
+
name: "context",
|
|
914
|
+
aliases: ["ctx"],
|
|
915
|
+
description: "Show context window summary.",
|
|
916
|
+
async run(args, ctx) {
|
|
917
|
+
const messages = ctx.messages;
|
|
918
|
+
const detailed = args.trim() === "detail";
|
|
919
|
+
const pairCount = countTurnPairs(messages);
|
|
920
|
+
const estimatedTokens = estimateTokens(messages);
|
|
921
|
+
const toolUseCount = countToolUses(messages);
|
|
922
|
+
const toolResultCount = countToolResults(messages);
|
|
923
|
+
const lines = [
|
|
924
|
+
`${color.bold("Context Window")}`,
|
|
925
|
+
` messages: ${messages.length} total (${pairCount} user+assistant pairs)`,
|
|
926
|
+
` tokens (\u2248): ${estimatedTokens.toLocaleString()} (chars \xF7 4 estimate)`,
|
|
927
|
+
` system prompt: ${ctx.systemPrompt.length} block${ctx.systemPrompt.length !== 1 ? "s" : ""}`,
|
|
928
|
+
` tools: ${toolUseCount} calls made, ${toolResultCount} results in history`,
|
|
929
|
+
` read files: ${ctx.readFiles.size} files`,
|
|
930
|
+
` todos: ${ctx.todos.filter((t) => t.status === "in_progress").length} in_progress / ${ctx.todos.filter((t) => t.status === "pending").length} pending / ${ctx.todos.filter((t) => t.status === "completed").length} completed`
|
|
931
|
+
];
|
|
932
|
+
if (detailed) {
|
|
933
|
+
lines.push(` model: ${ctx.model}`);
|
|
934
|
+
lines.push(` cwd: ${ctx.cwd}`);
|
|
935
|
+
lines.push(` projectRoot: ${ctx.projectRoot}`);
|
|
936
|
+
lines.push(` file mtimes: ${ctx.fileMtimes.size} tracked`);
|
|
937
|
+
if (ctx.readFiles.size > 0) {
|
|
938
|
+
lines.push(` file list: ${[...ctx.readFiles].join(", ")}`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const msg = lines.join("\n");
|
|
942
|
+
opts.renderer.write(`${msg}
|
|
943
|
+
`);
|
|
944
|
+
return { message: msg };
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function countTurnPairs(messages) {
|
|
949
|
+
let count = 0;
|
|
950
|
+
for (const m of messages) {
|
|
951
|
+
if (m.role === "user" || m.role === "assistant") count++;
|
|
952
|
+
}
|
|
953
|
+
return Math.floor(count / 2);
|
|
954
|
+
}
|
|
955
|
+
function countToolUses(messages) {
|
|
956
|
+
let count = 0;
|
|
957
|
+
for (const m of messages) {
|
|
958
|
+
const content = m.content;
|
|
959
|
+
if (Array.isArray(content)) {
|
|
960
|
+
count += content.filter((b) => b.type === "tool_use").length;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return count;
|
|
964
|
+
}
|
|
965
|
+
function countToolResults(messages) {
|
|
966
|
+
let count = 0;
|
|
967
|
+
for (const m of messages) {
|
|
968
|
+
const content = m.content;
|
|
969
|
+
if (Array.isArray(content)) {
|
|
970
|
+
count += content.filter((b) => b.type === "tool_result").length;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return count;
|
|
974
|
+
}
|
|
975
|
+
function estimateTokens(messages) {
|
|
976
|
+
let total = 0;
|
|
977
|
+
for (const m of messages) {
|
|
978
|
+
const content = m.content;
|
|
979
|
+
if (typeof content === "string") {
|
|
980
|
+
total += Math.ceil(content.length / 4);
|
|
981
|
+
} else if (Array.isArray(content)) {
|
|
982
|
+
for (const b of content) {
|
|
983
|
+
if (b.type === "text") total += Math.ceil(b.text.length / 4);
|
|
984
|
+
else if (b.type === "tool_use" || b.type === "tool_result") {
|
|
985
|
+
total += Math.ceil(JSON.stringify(b).length / 4);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return total;
|
|
991
|
+
}
|
|
992
|
+
function compactCommand(opts) {
|
|
993
|
+
return {
|
|
994
|
+
name: "compact",
|
|
995
|
+
description: "Compact the context window.",
|
|
996
|
+
async run(args, ctx) {
|
|
997
|
+
if (!opts.compactor) {
|
|
998
|
+
const msg2 = "No compactor configured.";
|
|
999
|
+
opts.renderer.writeWarning(msg2);
|
|
1000
|
+
return { message: msg2 };
|
|
1001
|
+
}
|
|
1002
|
+
const aggressive = args.trim() === "aggressive";
|
|
1003
|
+
const report = await opts.compactor.compact(ctx, { aggressive });
|
|
1004
|
+
const msg = `Compaction: ${report.before} \u2192 ${report.after} tokens (${report.reductions.map((r) => `${r.phase}: ${r.saved}`).join(", ")})`;
|
|
1005
|
+
opts.renderer.writeInfo(msg);
|
|
1006
|
+
return { message: msg };
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function usageCommand(opts) {
|
|
1011
|
+
return {
|
|
1012
|
+
name: "usage",
|
|
1013
|
+
aliases: ["cost"],
|
|
1014
|
+
description: "Show token usage and estimated cost.",
|
|
1015
|
+
async run() {
|
|
1016
|
+
const total = opts.tokenCounter.total();
|
|
1017
|
+
const cost = opts.tokenCounter.estimateCost();
|
|
1018
|
+
const msg = `${color.bold("Usage")}
|
|
1019
|
+
input: ${total.input}
|
|
1020
|
+
output: ${total.output}
|
|
1021
|
+
cache read: ${total.cacheRead ?? 0}
|
|
1022
|
+
cache write: ${total.cacheWrite ?? 0}
|
|
1023
|
+
cost: $${cost.total.toFixed(4)} (input $${cost.input.toFixed(4)} / output $${cost.output.toFixed(4)})
|
|
1024
|
+
`;
|
|
1025
|
+
opts.renderer.write(msg);
|
|
1026
|
+
return { message: msg };
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function toolsCommand(opts) {
|
|
1031
|
+
return {
|
|
1032
|
+
name: "tools",
|
|
1033
|
+
description: "List registered tools.",
|
|
1034
|
+
async run() {
|
|
1035
|
+
const all = opts.toolRegistry.listWithOwner();
|
|
1036
|
+
const lines = all.map(({ tool, owner }) => {
|
|
1037
|
+
return ` ${tool.name.padEnd(28)} ${color.dim(`[${owner}]`)} ${tool.mutating ? color.yellow("mut") : color.cyan("ro")} ${color.dim(tool.permission)}`;
|
|
1038
|
+
});
|
|
1039
|
+
const msg = `${color.bold("Tools")} (${all.length}):
|
|
1040
|
+
${lines.join("\n")}
|
|
1041
|
+
`;
|
|
1042
|
+
opts.renderer.write(msg);
|
|
1043
|
+
return { message: msg };
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
function skillCommand(opts) {
|
|
1048
|
+
return {
|
|
1049
|
+
name: "skill",
|
|
1050
|
+
description: "Show a skill manifest or list skills.",
|
|
1051
|
+
async run(args) {
|
|
1052
|
+
if (!opts.skillLoader) {
|
|
1053
|
+
const msg = "No skill loader configured.";
|
|
1054
|
+
return { message: msg };
|
|
1055
|
+
}
|
|
1056
|
+
if (!args.trim()) {
|
|
1057
|
+
const list = await opts.skillLoader.list();
|
|
1058
|
+
if (list.length === 0) {
|
|
1059
|
+
const msg2 = "No skills found.";
|
|
1060
|
+
return { message: msg2 };
|
|
1061
|
+
}
|
|
1062
|
+
const lines = list.map((s) => ` ${s.name.padEnd(24)} ${color.dim(`[${s.source}]`)} ${s.description.split("\n")[0]}`);
|
|
1063
|
+
const msg = `Skills:
|
|
1064
|
+
${lines.join("\n")}
|
|
1065
|
+
`;
|
|
1066
|
+
return { message: msg };
|
|
1067
|
+
} else {
|
|
1068
|
+
const skill = await opts.skillLoader.find(args.trim());
|
|
1069
|
+
if (!skill) {
|
|
1070
|
+
const msg = `Skill "${args.trim()}" not found.`;
|
|
1071
|
+
return { message: msg };
|
|
1072
|
+
}
|
|
1073
|
+
const body = await opts.skillLoader.readBody(skill.name);
|
|
1074
|
+
return { message: body };
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function useCommand(opts) {
|
|
1080
|
+
return {
|
|
1081
|
+
name: "use",
|
|
1082
|
+
description: "Switch provider mid-session: /use <provider>",
|
|
1083
|
+
async run(args) {
|
|
1084
|
+
const name = args.trim();
|
|
1085
|
+
if (!name) {
|
|
1086
|
+
const msg2 = "Usage: /use <provider-name>";
|
|
1087
|
+
return { message: msg2 };
|
|
1088
|
+
}
|
|
1089
|
+
opts.onSwitchProvider?.(name);
|
|
1090
|
+
const msg = `Switched provider to "${name}".`;
|
|
1091
|
+
return { message: msg };
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
function modelCommand(opts) {
|
|
1096
|
+
return {
|
|
1097
|
+
name: "model",
|
|
1098
|
+
description: "Switch model mid-session: /model <model>",
|
|
1099
|
+
async run(args) {
|
|
1100
|
+
const name = args.trim();
|
|
1101
|
+
if (!name) {
|
|
1102
|
+
const msg2 = "Usage: /model <model-name>";
|
|
1103
|
+
return { message: msg2 };
|
|
1104
|
+
}
|
|
1105
|
+
opts.onSwitchModel?.(name);
|
|
1106
|
+
const msg = `Switched model to "${name}".`;
|
|
1107
|
+
return { message: msg };
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
function saveCommand(opts) {
|
|
1112
|
+
return {
|
|
1113
|
+
name: "save",
|
|
1114
|
+
description: "Save current session (auto by default; this forces flush).",
|
|
1115
|
+
async run(_args, ctx) {
|
|
1116
|
+
await ctx.session.append({
|
|
1117
|
+
type: "session_end",
|
|
1118
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1119
|
+
usage: opts.tokenCounter.total()
|
|
1120
|
+
});
|
|
1121
|
+
const msg = `Session ${ctx.session.id} flushed.`;
|
|
1122
|
+
return { message: msg };
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function loadCommand(opts) {
|
|
1127
|
+
return {
|
|
1128
|
+
name: "resume",
|
|
1129
|
+
aliases: ["load", "sessions"],
|
|
1130
|
+
description: "List recent sessions. To actually resume, exit and run `wstack resume <id>`.",
|
|
1131
|
+
async run() {
|
|
1132
|
+
if (!opts.sessionStore) {
|
|
1133
|
+
const msg2 = "No session store configured.";
|
|
1134
|
+
return { message: msg2 };
|
|
1135
|
+
}
|
|
1136
|
+
const list = await opts.sessionStore.list(10);
|
|
1137
|
+
if (list.length === 0) {
|
|
1138
|
+
const msg2 = "No saved sessions.";
|
|
1139
|
+
return { message: msg2 };
|
|
1140
|
+
}
|
|
1141
|
+
const lines = list.map(
|
|
1142
|
+
(s) => ` ${s.id} ${color.dim(s.startedAt)} ${color.dim(`${s.tokenTotal} tok`)} ${s.title}`
|
|
1143
|
+
);
|
|
1144
|
+
const msg = `Recent sessions:
|
|
1145
|
+
${lines.join("\n")}
|
|
1146
|
+
|
|
1147
|
+
` + color.dim(`Resume one with: wstack resume ${list[0]?.id ?? "<id>"}
|
|
1148
|
+
`);
|
|
1149
|
+
opts.renderer.write(msg);
|
|
1150
|
+
return { message: msg };
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function exitCommand(opts) {
|
|
1155
|
+
return {
|
|
1156
|
+
name: "exit",
|
|
1157
|
+
aliases: ["quit", "q"],
|
|
1158
|
+
description: "Exit the REPL.",
|
|
1159
|
+
async run() {
|
|
1160
|
+
opts.onExit?.();
|
|
1161
|
+
return { exit: true };
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1166
|
+
var FILLED2 = "\u2588";
|
|
1167
|
+
var EMPTY2 = "\u2591";
|
|
1168
|
+
var Spinner = class {
|
|
1169
|
+
timer;
|
|
1170
|
+
frame = 0;
|
|
1171
|
+
active = false;
|
|
1172
|
+
label = "";
|
|
1173
|
+
startedAt = 0;
|
|
1174
|
+
context;
|
|
1175
|
+
out;
|
|
1176
|
+
enabled;
|
|
1177
|
+
constructor(out = process.stderr) {
|
|
1178
|
+
this.out = out;
|
|
1179
|
+
this.enabled = Boolean(out.isTTY) && !process.env.NO_COLOR;
|
|
1180
|
+
}
|
|
1181
|
+
start(label) {
|
|
1182
|
+
if (!this.enabled || this.active) return;
|
|
1183
|
+
this.label = label;
|
|
1184
|
+
this.frame = 0;
|
|
1185
|
+
this.active = true;
|
|
1186
|
+
this.startedAt = Date.now();
|
|
1187
|
+
this.render();
|
|
1188
|
+
this.timer = setInterval(() => {
|
|
1189
|
+
this.frame = (this.frame + 1) % FRAMES.length;
|
|
1190
|
+
this.render();
|
|
1191
|
+
}, 80);
|
|
1192
|
+
this.timer.unref?.();
|
|
1193
|
+
}
|
|
1194
|
+
stop() {
|
|
1195
|
+
if (!this.active) return;
|
|
1196
|
+
this.active = false;
|
|
1197
|
+
if (this.timer) clearInterval(this.timer);
|
|
1198
|
+
this.timer = void 0;
|
|
1199
|
+
this.clearLine();
|
|
1200
|
+
}
|
|
1201
|
+
/** Stop and persist a one-line note where the spinner was (e.g. "✓ done in 1.4s"). */
|
|
1202
|
+
stopWith(note) {
|
|
1203
|
+
this.stop();
|
|
1204
|
+
this.out.write(`${note}
|
|
1205
|
+
`);
|
|
1206
|
+
}
|
|
1207
|
+
/** Update the live context-window chip shown on the spinner line. */
|
|
1208
|
+
setContext(ctx) {
|
|
1209
|
+
this.context = ctx;
|
|
1210
|
+
}
|
|
1211
|
+
render() {
|
|
1212
|
+
const elapsed = ((Date.now() - this.startedAt) / 1e3).toFixed(1);
|
|
1213
|
+
let line = `${color.amber(FRAMES[this.frame] ?? "")} ${this.label} ${color.dim(`${elapsed}s`)}`;
|
|
1214
|
+
if (this.context && this.context.max > 0) {
|
|
1215
|
+
line += " " + renderContextChip2(this.context);
|
|
1216
|
+
}
|
|
1217
|
+
this.clearLine();
|
|
1218
|
+
this.out.write(line);
|
|
1219
|
+
}
|
|
1220
|
+
clearLine() {
|
|
1221
|
+
if (!this.enabled) return;
|
|
1222
|
+
this.out.write("\r\x1B[2K");
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
function renderContextChip2(ctx) {
|
|
1226
|
+
const ratio = Math.max(0, Math.min(1, ctx.used / ctx.max));
|
|
1227
|
+
const pct = Math.round(ratio * 100);
|
|
1228
|
+
const chipColor = ratio >= 0.85 ? color.red : ratio >= 0.65 ? color.yellow : color.cyan;
|
|
1229
|
+
const bar = renderProgress2(ratio, 8);
|
|
1230
|
+
return color.dim("ctx ") + chipColor(bar) + chipColor(` ${pct}%`) + color.dim(` (${fmtTok3(ctx.used)}/${fmtTok3(ctx.max)})`);
|
|
1231
|
+
}
|
|
1232
|
+
function renderProgress2(ratio, width) {
|
|
1233
|
+
const clamped = Math.max(0, Math.min(1, ratio));
|
|
1234
|
+
const filled = clamped === 0 ? 0 : Math.max(1, Math.round(clamped * width));
|
|
1235
|
+
const capped = Math.min(width, filled);
|
|
1236
|
+
return FILLED2.repeat(capped) + EMPTY2.repeat(width - capped);
|
|
1237
|
+
}
|
|
1238
|
+
function fmtTok3(n) {
|
|
1239
|
+
if (n < 1e3) return String(n);
|
|
1240
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
1241
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
1242
|
+
}
|
|
1243
|
+
var subcommands = {
|
|
1244
|
+
init: initCmd,
|
|
1245
|
+
auth: authCmd,
|
|
1246
|
+
// `resume <id>` is special-cased in src/index.ts: it's lifted into
|
|
1247
|
+
// `--resume <id>` so the normal REPL bootstrap runs with a pre-loaded
|
|
1248
|
+
// session. There is no standalone subcommand handler.
|
|
1249
|
+
sessions: sessionsCmd,
|
|
1250
|
+
config: configCmd,
|
|
1251
|
+
tools: toolsCmd,
|
|
1252
|
+
skills: skillsCmd,
|
|
1253
|
+
providers: providersCmd,
|
|
1254
|
+
models: modelsCmd,
|
|
1255
|
+
mcp: mcpCmd,
|
|
1256
|
+
plugin: pluginCmd,
|
|
1257
|
+
diag: diagCmd,
|
|
1258
|
+
usage: usageCmd,
|
|
1259
|
+
version: versionCmd,
|
|
1260
|
+
help: helpCmd,
|
|
1261
|
+
projects: projectsCmd
|
|
1262
|
+
};
|
|
1263
|
+
async function authCmd(args, deps) {
|
|
1264
|
+
const flags = parseAuthFlags(args);
|
|
1265
|
+
let providerId = flags.positional[0];
|
|
1266
|
+
if (!providerId) {
|
|
1267
|
+
providerId = (await deps.reader.readLine("Provider id: ")).trim();
|
|
1268
|
+
}
|
|
1269
|
+
if (!providerId) {
|
|
1270
|
+
deps.renderer.writeError("Provider id is required.");
|
|
1271
|
+
return 1;
|
|
1272
|
+
}
|
|
1273
|
+
let family = flags.family;
|
|
1274
|
+
let baseUrl = flags.baseUrl;
|
|
1275
|
+
let envVars = flags.envVars;
|
|
1276
|
+
try {
|
|
1277
|
+
const known = await deps.modelsRegistry.getProvider(providerId);
|
|
1278
|
+
if (known) {
|
|
1279
|
+
if (!family) family = known.family;
|
|
1280
|
+
if (!baseUrl) baseUrl = known.apiBase;
|
|
1281
|
+
if (!envVars) envVars = known.envVars;
|
|
1282
|
+
}
|
|
1283
|
+
} catch {
|
|
1284
|
+
}
|
|
1285
|
+
if (!family) {
|
|
1286
|
+
deps.renderer.writeError(
|
|
1287
|
+
`Provider "${providerId}" not in catalog. Pass --family <anthropic|openai|openai-compatible|google> to register it manually.`
|
|
1288
|
+
);
|
|
1289
|
+
return 1;
|
|
1290
|
+
}
|
|
1291
|
+
const apiKey = (await deps.reader.readSecret(
|
|
1292
|
+
`API key for ${providerId} (hidden, stored encrypted in ${deps.paths.globalConfig}): `
|
|
1293
|
+
)).trim();
|
|
1294
|
+
if (!apiKey) {
|
|
1295
|
+
deps.renderer.writeError("No key entered. Nothing saved.");
|
|
1296
|
+
return 1;
|
|
1297
|
+
}
|
|
1298
|
+
const patch = {
|
|
1299
|
+
providers: {
|
|
1300
|
+
[providerId]: {
|
|
1301
|
+
type: providerId,
|
|
1302
|
+
apiKey,
|
|
1303
|
+
family,
|
|
1304
|
+
...baseUrl ? { baseUrl } : {},
|
|
1305
|
+
...envVars && envVars.length > 0 ? { envVars } : {}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
try {
|
|
1310
|
+
await rewriteConfigEncrypted(deps.paths.globalConfig, deps.vault, patch);
|
|
1311
|
+
deps.renderer.writeInfo(`Stored encrypted key for ${providerId}.`);
|
|
1312
|
+
deps.renderer.writeInfo(`Use: wstack --provider ${providerId} "<task>"`);
|
|
1313
|
+
return 0;
|
|
1314
|
+
} catch (err) {
|
|
1315
|
+
deps.renderer.writeError(`auth: ${err instanceof Error ? err.message : String(err)}`);
|
|
1316
|
+
return 1;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
function parseAuthFlags(args) {
|
|
1320
|
+
const out = { positional: [] };
|
|
1321
|
+
for (let i = 0; i < args.length; i++) {
|
|
1322
|
+
const a = args[i];
|
|
1323
|
+
if (a === "--family") {
|
|
1324
|
+
const v = args[++i];
|
|
1325
|
+
if (v) out.family = v;
|
|
1326
|
+
} else if (a === "--base-url") {
|
|
1327
|
+
const v = args[++i];
|
|
1328
|
+
if (v) out.baseUrl = v;
|
|
1329
|
+
} else if (a === "--env") {
|
|
1330
|
+
const v = args[++i];
|
|
1331
|
+
if (v) out.envVars = v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1332
|
+
} else if (a && !a.startsWith("--")) {
|
|
1333
|
+
out.positional.push(a);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return out;
|
|
1337
|
+
}
|
|
1338
|
+
async function initCmd(_args, deps) {
|
|
1339
|
+
deps.renderer.write(color.bold("WrongStack init\n"));
|
|
1340
|
+
deps.renderer.writeInfo("Loading provider catalog from models.dev (cached locally)\u2026");
|
|
1341
|
+
let providers;
|
|
1342
|
+
try {
|
|
1343
|
+
providers = await deps.modelsRegistry.listProviders();
|
|
1344
|
+
} catch (err) {
|
|
1345
|
+
deps.renderer.writeError(
|
|
1346
|
+
`Failed to load provider catalog: ${err instanceof Error ? err.message : err}`
|
|
1347
|
+
);
|
|
1348
|
+
return 1;
|
|
1349
|
+
}
|
|
1350
|
+
const detected = providers.filter((p) => p.family !== "unsupported").filter((p) => p.envVars.some((v) => process.env[v]));
|
|
1351
|
+
const ranked = detected.length > 0 ? detected : providers.filter((p) => ["anthropic", "openai", "google"].includes(p.id));
|
|
1352
|
+
if (detected.length > 0) {
|
|
1353
|
+
deps.renderer.write(`Detected API keys for: ${detected.map((p) => p.name).join(", ")}
|
|
1354
|
+
`);
|
|
1355
|
+
}
|
|
1356
|
+
const defaultId = ranked[0]?.id ?? "anthropic";
|
|
1357
|
+
const providerId = (await deps.reader.readLine(`Provider [${defaultId}]: `)).trim() || defaultId;
|
|
1358
|
+
const provider = await deps.modelsRegistry.getProvider(providerId);
|
|
1359
|
+
if (!provider) {
|
|
1360
|
+
deps.renderer.writeError(`Provider "${providerId}" not found in models.dev catalog.`);
|
|
1361
|
+
return 1;
|
|
1362
|
+
}
|
|
1363
|
+
if (provider.family === "unsupported") {
|
|
1364
|
+
deps.renderer.writeError(
|
|
1365
|
+
`Provider "${providerId}" uses ${provider.npm} which has no built-in transport. Install a plugin to enable it.`
|
|
1366
|
+
);
|
|
1367
|
+
return 1;
|
|
1368
|
+
}
|
|
1369
|
+
const suggestedModel = await deps.modelsRegistry.suggestModel(providerId) ?? "";
|
|
1370
|
+
const modelHint = suggestedModel ? ` [${suggestedModel}]` : "";
|
|
1371
|
+
const modelId = (await deps.reader.readLine(`Model${modelHint}: `)).trim() || suggestedModel;
|
|
1372
|
+
if (!modelId) {
|
|
1373
|
+
deps.renderer.writeError("No model selected. Aborting.");
|
|
1374
|
+
return 1;
|
|
1375
|
+
}
|
|
1376
|
+
const envHit = provider.envVars.map((v) => process.env[v]).find(Boolean);
|
|
1377
|
+
let apiKey = "";
|
|
1378
|
+
if (!envHit) {
|
|
1379
|
+
apiKey = (await deps.reader.readLine(
|
|
1380
|
+
`API key (stored in ${deps.paths.globalConfig}; empty = expect ${provider.envVars[0] ?? "env var"}): `
|
|
1381
|
+
)).trim();
|
|
1382
|
+
} else {
|
|
1383
|
+
deps.renderer.writeInfo(`Found API key in env (${provider.envVars.join(" / ")}).`);
|
|
1384
|
+
}
|
|
1385
|
+
await fs2.mkdir(deps.paths.globalRoot, { recursive: true });
|
|
1386
|
+
const config = {
|
|
1387
|
+
version: 1,
|
|
1388
|
+
provider: providerId,
|
|
1389
|
+
model: modelId
|
|
1390
|
+
};
|
|
1391
|
+
if (apiKey) config.apiKey = apiKey;
|
|
1392
|
+
await atomicWrite(deps.paths.globalConfig, JSON.stringify(config, null, 2));
|
|
1393
|
+
await fs2.mkdir(path2.join(deps.projectRoot, ".wrongstack"), { recursive: true });
|
|
1394
|
+
const agentsFile = path2.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
|
|
1395
|
+
try {
|
|
1396
|
+
await fs2.access(agentsFile);
|
|
1397
|
+
} catch {
|
|
1398
|
+
await atomicWrite(
|
|
1399
|
+
agentsFile,
|
|
1400
|
+
"# Project notes for WrongStack\n\nWrite project-specific conventions, build commands,\nand domain knowledge here. This file is committed to git.\n"
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
deps.renderer.writeInfo(`Wrote ${deps.paths.globalConfig}`);
|
|
1404
|
+
deps.renderer.writeInfo(`Project state lives in ${deps.paths.projectDir}`);
|
|
1405
|
+
deps.renderer.writeInfo('Try: wstack "<task>" or wstack');
|
|
1406
|
+
return 0;
|
|
1407
|
+
}
|
|
1408
|
+
async function sessionsCmd(_args, deps) {
|
|
1409
|
+
if (!deps.sessionStore) {
|
|
1410
|
+
deps.renderer.writeError("No session store available.");
|
|
1411
|
+
return 1;
|
|
1412
|
+
}
|
|
1413
|
+
const list = await deps.sessionStore.list(20);
|
|
1414
|
+
if (list.length === 0) {
|
|
1415
|
+
deps.renderer.write("No sessions found.\n");
|
|
1416
|
+
return 0;
|
|
1417
|
+
}
|
|
1418
|
+
for (const s of list) {
|
|
1419
|
+
deps.renderer.write(
|
|
1420
|
+
` ${s.id} ${color.dim(s.startedAt)} ${color.dim(`${s.tokenTotal} tok`)} ${s.title}
|
|
1421
|
+
`
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
return 0;
|
|
1425
|
+
}
|
|
1426
|
+
async function configCmd(args, deps) {
|
|
1427
|
+
const sub = args[0];
|
|
1428
|
+
if (!sub || sub === "show") {
|
|
1429
|
+
const redacted = redactKeys(deps.config);
|
|
1430
|
+
deps.renderer.write(JSON.stringify(redacted, null, 2) + "\n");
|
|
1431
|
+
return 0;
|
|
1432
|
+
}
|
|
1433
|
+
if (sub === "edit") {
|
|
1434
|
+
const editor = process.env["EDITOR"] ?? "vi";
|
|
1435
|
+
deps.renderer.write(`Run: ${editor} ${deps.paths.globalConfig}
|
|
1436
|
+
`);
|
|
1437
|
+
return 0;
|
|
1438
|
+
}
|
|
1439
|
+
deps.renderer.writeError(`Unknown config subcommand: ${sub}`);
|
|
1440
|
+
return 1;
|
|
1441
|
+
}
|
|
1442
|
+
async function toolsCmd(_args, deps) {
|
|
1443
|
+
const reg = deps.toolRegistry;
|
|
1444
|
+
if (!reg) return 0;
|
|
1445
|
+
for (const { tool, owner } of reg.listWithOwner()) {
|
|
1446
|
+
deps.renderer.write(
|
|
1447
|
+
` ${tool.name.padEnd(28)} ${color.dim(`[${owner}]`)} ${tool.permission}
|
|
1448
|
+
`
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
return 0;
|
|
1452
|
+
}
|
|
1453
|
+
async function skillsCmd(_args, deps) {
|
|
1454
|
+
if (!deps.skillLoader) return 0;
|
|
1455
|
+
const list = await deps.skillLoader.list();
|
|
1456
|
+
for (const s of list) {
|
|
1457
|
+
deps.renderer.write(
|
|
1458
|
+
` ${s.name.padEnd(24)} ${color.dim(`[${s.source}]`)} ${s.description.split("\n")[0]}
|
|
1459
|
+
`
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
return 0;
|
|
1463
|
+
}
|
|
1464
|
+
async function providersCmd(args, deps) {
|
|
1465
|
+
const showAll = args.includes("--all");
|
|
1466
|
+
const showUnsupported = args.includes("--unsupported");
|
|
1467
|
+
try {
|
|
1468
|
+
const all = await deps.modelsRegistry.listProviders();
|
|
1469
|
+
const byFamily = {
|
|
1470
|
+
anthropic: [],
|
|
1471
|
+
openai: [],
|
|
1472
|
+
"openai-compatible": [],
|
|
1473
|
+
google: [],
|
|
1474
|
+
unsupported: []
|
|
1475
|
+
};
|
|
1476
|
+
for (const p of all) byFamily[p.family].push(p);
|
|
1477
|
+
const families = showUnsupported ? ["unsupported"] : showAll ? ["anthropic", "openai", "google", "openai-compatible", "unsupported"] : ["anthropic", "openai", "google", "openai-compatible"];
|
|
1478
|
+
for (const family of families) {
|
|
1479
|
+
const list = byFamily[family];
|
|
1480
|
+
if (list.length === 0) continue;
|
|
1481
|
+
deps.renderer.write(`
|
|
1482
|
+
${color.bold(family)} (${list.length}):
|
|
1483
|
+
`);
|
|
1484
|
+
for (const p of list) {
|
|
1485
|
+
const envFound = p.envVars.some((v) => process.env[v]);
|
|
1486
|
+
const marker = envFound ? color.green("\u25CF") : color.dim("\u25CB");
|
|
1487
|
+
const envHint = p.envVars[0] ? color.dim(`[${p.envVars[0]}]`) : "";
|
|
1488
|
+
const note = family === "unsupported" ? color.dim("(needs plugin)") : "";
|
|
1489
|
+
deps.renderer.write(
|
|
1490
|
+
` ${marker} ${p.id.padEnd(20)} ${p.name.padEnd(28)} ${envHint} ${note}
|
|
1491
|
+
`
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
deps.renderer.write(
|
|
1496
|
+
`
|
|
1497
|
+
${color.dim(`Current: ${deps.config.provider ?? "<unset>"} / ${deps.config.model ?? "<unset>"}. Use --all to include unsupported families.`)}
|
|
1498
|
+
`
|
|
1499
|
+
);
|
|
1500
|
+
return 0;
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
deps.renderer.writeError(
|
|
1503
|
+
`Failed to list providers: ${err instanceof Error ? err.message : err}`
|
|
1504
|
+
);
|
|
1505
|
+
return 1;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
async function modelsCmd(args, deps) {
|
|
1509
|
+
const sub = args[0];
|
|
1510
|
+
if (sub === "refresh") {
|
|
1511
|
+
deps.renderer.writeInfo("Refreshing models.dev cache\u2026");
|
|
1512
|
+
try {
|
|
1513
|
+
const payload = await deps.modelsRegistry.refresh();
|
|
1514
|
+
deps.renderer.writeInfo(
|
|
1515
|
+
`Cached ${Object.keys(payload).length} providers to ${deps.paths.modelsCache}`
|
|
1516
|
+
);
|
|
1517
|
+
return 0;
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
deps.renderer.writeError(`Refresh failed: ${err instanceof Error ? err.message : err}`);
|
|
1520
|
+
return 1;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
const providerId = sub ?? deps.config.provider;
|
|
1524
|
+
if (!providerId) {
|
|
1525
|
+
deps.renderer.writeError("Usage: wstack models <provider> | refresh");
|
|
1526
|
+
return 1;
|
|
1527
|
+
}
|
|
1528
|
+
const provider = await deps.modelsRegistry.getProvider(providerId);
|
|
1529
|
+
if (!provider) {
|
|
1530
|
+
deps.renderer.writeError(`Provider "${providerId}" not in catalog.`);
|
|
1531
|
+
return 1;
|
|
1532
|
+
}
|
|
1533
|
+
deps.renderer.write(`${color.bold(provider.name)} ${color.dim(`(${provider.id})`)}
|
|
1534
|
+
`);
|
|
1535
|
+
if (provider.doc) deps.renderer.write(color.dim(`Docs: ${provider.doc}
|
|
1536
|
+
`));
|
|
1537
|
+
const sorted = [...provider.models].sort(
|
|
1538
|
+
(a, b) => (b.release_date ?? "").localeCompare(a.release_date ?? "")
|
|
1539
|
+
);
|
|
1540
|
+
for (const m of sorted) {
|
|
1541
|
+
const caps = [];
|
|
1542
|
+
if (m.tool_call) caps.push("tools");
|
|
1543
|
+
if (m.reasoning) caps.push("reasoning");
|
|
1544
|
+
if (m.modalities?.input?.includes("image")) caps.push("vision");
|
|
1545
|
+
const ctx = m.limit?.context ? `${(m.limit.context / 1e3).toFixed(0)}k` : "?";
|
|
1546
|
+
const cost = m.cost?.input !== void 0 ? `$${m.cost.input}/$${m.cost.output ?? "?"}` : "";
|
|
1547
|
+
deps.renderer.write(
|
|
1548
|
+
` ${m.id.padEnd(40)} ${color.dim(ctx.padStart(6))} ${color.dim(cost.padEnd(14))} ${color.dim(caps.join(","))}
|
|
1549
|
+
`
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
const age = await deps.modelsRegistry.ageSeconds();
|
|
1553
|
+
deps.renderer.write(
|
|
1554
|
+
color.dim(
|
|
1555
|
+
`
|
|
1556
|
+
Cache age: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never fetched"}. Run \`wstack models refresh\` to update.
|
|
1557
|
+
`
|
|
1558
|
+
)
|
|
1559
|
+
);
|
|
1560
|
+
return 0;
|
|
1561
|
+
}
|
|
1562
|
+
async function mcpCmd(args, deps) {
|
|
1563
|
+
const sub = args[0];
|
|
1564
|
+
if (!sub || sub === "list") {
|
|
1565
|
+
const servers = deps.config.mcpServers ?? {};
|
|
1566
|
+
if (Object.keys(servers).length === 0) {
|
|
1567
|
+
deps.renderer.write("No MCP servers configured.\n");
|
|
1568
|
+
return 0;
|
|
1569
|
+
}
|
|
1570
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
1571
|
+
deps.renderer.write(
|
|
1572
|
+
` ${name.padEnd(20)} ${cfg.transport} ${cfg.enabled === false ? "disabled" : "enabled"}
|
|
1573
|
+
`
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
return 0;
|
|
1577
|
+
}
|
|
1578
|
+
if (sub === "restart") {
|
|
1579
|
+
deps.renderer.writeWarning("mcp restart is only available in REPL mode.");
|
|
1580
|
+
return 0;
|
|
1581
|
+
}
|
|
1582
|
+
deps.renderer.writeError(`Unknown mcp subcommand: ${sub}`);
|
|
1583
|
+
return 1;
|
|
1584
|
+
}
|
|
1585
|
+
async function pluginCmd(args, deps) {
|
|
1586
|
+
const sub = args[0];
|
|
1587
|
+
if (!sub || sub === "list") {
|
|
1588
|
+
const plugins = deps.config.plugins ?? [];
|
|
1589
|
+
if (plugins.length === 0) {
|
|
1590
|
+
deps.renderer.write("No plugins configured.\n");
|
|
1591
|
+
return 0;
|
|
1592
|
+
}
|
|
1593
|
+
for (const p of plugins) {
|
|
1594
|
+
const name = typeof p === "string" ? p : p.name;
|
|
1595
|
+
const enabled = typeof p === "object" && p.enabled === false ? "disabled" : "enabled";
|
|
1596
|
+
deps.renderer.write(` ${name} ${enabled}
|
|
1597
|
+
`);
|
|
1598
|
+
}
|
|
1599
|
+
return 0;
|
|
1600
|
+
}
|
|
1601
|
+
deps.renderer.writeWarning(`plugin ${sub} not implemented (edit config.plugins manually).`);
|
|
1602
|
+
return 0;
|
|
1603
|
+
}
|
|
1604
|
+
async function diagCmd(_args, deps) {
|
|
1605
|
+
const cfg = deps.config;
|
|
1606
|
+
const age = await deps.modelsRegistry.ageSeconds();
|
|
1607
|
+
const lines = [
|
|
1608
|
+
color.bold("WrongStack diagnostics"),
|
|
1609
|
+
` apiVersion: 0.0.1`,
|
|
1610
|
+
` cwd: ${deps.cwd}`,
|
|
1611
|
+
` projectRoot: ${deps.projectRoot}`,
|
|
1612
|
+
` projectHash: ${deps.paths.projectHash}`,
|
|
1613
|
+
` projectDir: ${deps.paths.projectDir}`,
|
|
1614
|
+
` globalRoot: ${deps.paths.globalRoot}`,
|
|
1615
|
+
` modelsCache: ${deps.paths.modelsCache}`,
|
|
1616
|
+
` cacheAge: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never"}`,
|
|
1617
|
+
` node: ${process.version}`,
|
|
1618
|
+
` os: ${os2.platform()} ${os2.release()}`,
|
|
1619
|
+
` provider: ${cfg.provider ?? "<unset>"}`,
|
|
1620
|
+
` model: ${cfg.model ?? "<unset>"}`,
|
|
1621
|
+
` tools: ${deps.toolRegistry?.list().length ?? 0}`,
|
|
1622
|
+
` plugins: ${cfg.plugins?.length ?? 0}`,
|
|
1623
|
+
` mcpServers: ${Object.keys(cfg.mcpServers ?? {}).length}`
|
|
1624
|
+
];
|
|
1625
|
+
deps.renderer.write(lines.join("\n") + "\n");
|
|
1626
|
+
return 0;
|
|
1627
|
+
}
|
|
1628
|
+
async function usageCmd(_args, deps) {
|
|
1629
|
+
if (!deps.sessionStore) return 0;
|
|
1630
|
+
const list = await deps.sessionStore.list(100);
|
|
1631
|
+
let totalIn = 0;
|
|
1632
|
+
for (const s of list) totalIn += s.tokenTotal;
|
|
1633
|
+
deps.renderer.write(`Sessions: ${list.length} total tokens: ${totalIn}
|
|
1634
|
+
`);
|
|
1635
|
+
return 0;
|
|
1636
|
+
}
|
|
1637
|
+
async function versionCmd(_args, deps) {
|
|
1638
|
+
deps.renderer.write(
|
|
1639
|
+
`WrongStack 0.0.1 (apiVersion 0.0.1, node ${process.version}, ${os2.platform()})
|
|
1640
|
+
`
|
|
1641
|
+
);
|
|
1642
|
+
return 0;
|
|
1643
|
+
}
|
|
1644
|
+
async function helpCmd(_args, deps) {
|
|
1645
|
+
const lines = [
|
|
1646
|
+
color.bold("WrongStack \u2014 usage"),
|
|
1647
|
+
"",
|
|
1648
|
+
" wstack Start REPL",
|
|
1649
|
+
' wstack "<task>" Run task and exit',
|
|
1650
|
+
" wstack resume [<id>] Resume a session",
|
|
1651
|
+
" wstack sessions List recent sessions",
|
|
1652
|
+
" wstack init Pick provider + model from models.dev",
|
|
1653
|
+
" wstack auth <provider> Store API key (encrypted at rest)",
|
|
1654
|
+
" wstack resume <id> Resume a session (loads transcript + appends)",
|
|
1655
|
+
" wstack config [show|edit] Show or edit effective config",
|
|
1656
|
+
" wstack tools List registered tools",
|
|
1657
|
+
" wstack skills List discovered skills",
|
|
1658
|
+
" wstack providers [--all] List providers from models.dev",
|
|
1659
|
+
" wstack models [<provider>] List models for current/specified provider",
|
|
1660
|
+
" wstack models refresh Force-refresh models.dev cache",
|
|
1661
|
+
" wstack mcp [list] List MCP servers",
|
|
1662
|
+
" wstack plugin [list] List plugins",
|
|
1663
|
+
" wstack projects List projects tracked in ~/.wrongstack/projects/",
|
|
1664
|
+
" wstack diag Full diagnostics",
|
|
1665
|
+
" wstack usage Token + cost summary",
|
|
1666
|
+
" wstack version Print version",
|
|
1667
|
+
"",
|
|
1668
|
+
"Global flags:",
|
|
1669
|
+
" --provider, --model, --cwd, --log-level, --yolo, --verbose, --trace, --config"
|
|
1670
|
+
];
|
|
1671
|
+
deps.renderer.write(lines.join("\n") + "\n");
|
|
1672
|
+
return 0;
|
|
1673
|
+
}
|
|
1674
|
+
async function projectsCmd(_args, deps) {
|
|
1675
|
+
const projectsRoot = path2.join(deps.paths.globalRoot, "projects");
|
|
1676
|
+
try {
|
|
1677
|
+
const entries = await fs2.readdir(projectsRoot);
|
|
1678
|
+
if (entries.length === 0) {
|
|
1679
|
+
deps.renderer.write("No projects tracked.\n");
|
|
1680
|
+
return 0;
|
|
1681
|
+
}
|
|
1682
|
+
for (const hash of entries) {
|
|
1683
|
+
try {
|
|
1684
|
+
const meta = JSON.parse(
|
|
1685
|
+
await fs2.readFile(path2.join(projectsRoot, hash, "meta.json"), "utf8")
|
|
1686
|
+
);
|
|
1687
|
+
deps.renderer.write(
|
|
1688
|
+
` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
|
|
1689
|
+
`
|
|
1690
|
+
);
|
|
1691
|
+
} catch {
|
|
1692
|
+
deps.renderer.write(` ${color.dim(hash)} ${color.dim("(no meta)")}
|
|
1693
|
+
`);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
return 0;
|
|
1697
|
+
} catch {
|
|
1698
|
+
deps.renderer.write("No projects directory.\n");
|
|
1699
|
+
return 0;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
function redactKeys(obj) {
|
|
1703
|
+
if (!obj || typeof obj !== "object") return obj;
|
|
1704
|
+
if (Array.isArray(obj)) return obj.map(redactKeys);
|
|
1705
|
+
const out = {};
|
|
1706
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1707
|
+
if (/api.?key|secret|token|pass/i.test(k) && typeof v === "string" && v.length > 0) {
|
|
1708
|
+
out[k] = "[REDACTED]";
|
|
1709
|
+
} else {
|
|
1710
|
+
out[k] = redactKeys(v);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return out;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/index.ts
|
|
1717
|
+
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
1718
|
+
"yolo",
|
|
1719
|
+
"verbose",
|
|
1720
|
+
"trace",
|
|
1721
|
+
"help",
|
|
1722
|
+
"version",
|
|
1723
|
+
"no-banner",
|
|
1724
|
+
"no-features",
|
|
1725
|
+
"tui",
|
|
1726
|
+
"no-tui",
|
|
1727
|
+
"no-recovery",
|
|
1728
|
+
"recover",
|
|
1729
|
+
"no-alt-screen",
|
|
1730
|
+
"alt-screen",
|
|
1731
|
+
"output-json",
|
|
1732
|
+
"prompt"
|
|
1733
|
+
]);
|
|
1734
|
+
function parseArgs(argv) {
|
|
1735
|
+
const flags = {};
|
|
1736
|
+
const positional = [];
|
|
1737
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1738
|
+
const a = argv[i];
|
|
1739
|
+
if (!a) continue;
|
|
1740
|
+
if (a === "--") {
|
|
1741
|
+
positional.push(...argv.slice(i + 1));
|
|
1742
|
+
break;
|
|
1743
|
+
}
|
|
1744
|
+
if (a.startsWith("--")) {
|
|
1745
|
+
const eq = a.indexOf("=");
|
|
1746
|
+
if (eq !== -1) {
|
|
1747
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
const name = a.slice(2);
|
|
1751
|
+
if (BOOLEAN_FLAGS.has(name)) {
|
|
1752
|
+
flags[name] = true;
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
if (i + 1 < argv.length && !(argv[i + 1] ?? "").startsWith("-")) {
|
|
1756
|
+
flags[name] = argv[++i] ?? "";
|
|
1757
|
+
} else {
|
|
1758
|
+
flags[name] = true;
|
|
1759
|
+
}
|
|
1760
|
+
} else if (a.startsWith("-") && a.length === 2) {
|
|
1761
|
+
const short = a.slice(1);
|
|
1762
|
+
const expand = { v: "verbose" };
|
|
1763
|
+
flags[expand[short] ?? short] = true;
|
|
1764
|
+
} else {
|
|
1765
|
+
positional.push(a);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
return { flags, positional };
|
|
1769
|
+
}
|
|
1770
|
+
function flagsToConfigPatch(flags) {
|
|
1771
|
+
const patch = {};
|
|
1772
|
+
if (typeof flags["provider"] === "string") patch.provider = flags["provider"];
|
|
1773
|
+
if (typeof flags["model"] === "string") patch.model = flags["model"];
|
|
1774
|
+
if (typeof flags["cwd"] === "string") patch.cwd = flags["cwd"];
|
|
1775
|
+
if (typeof flags["log-level"] === "string") {
|
|
1776
|
+
patch.log = { level: flags["log-level"] };
|
|
1777
|
+
} else if (flags["verbose"]) {
|
|
1778
|
+
patch.log = { level: "debug" };
|
|
1779
|
+
} else if (flags["trace"]) {
|
|
1780
|
+
patch.log = { level: "trace" };
|
|
1781
|
+
}
|
|
1782
|
+
if (flags["yolo"]) patch.yolo = true;
|
|
1783
|
+
if (flags["no-features"]) {
|
|
1784
|
+
patch.features = {
|
|
1785
|
+
mcp: false,
|
|
1786
|
+
plugins: false,
|
|
1787
|
+
memory: false,
|
|
1788
|
+
modelsRegistry: false,
|
|
1789
|
+
skills: false
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
return patch;
|
|
1793
|
+
}
|
|
1794
|
+
function resolveBundledSkillsDir() {
|
|
1795
|
+
try {
|
|
1796
|
+
const req = createRequire(import.meta.url);
|
|
1797
|
+
const corePkg = req.resolve("@wrongstack/core/package.json");
|
|
1798
|
+
return path2.join(path2.dirname(corePkg), "skills");
|
|
1799
|
+
} catch {
|
|
1800
|
+
return void 0;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
function readOwnVersion() {
|
|
1804
|
+
const req = createRequire(import.meta.url);
|
|
1805
|
+
const candidates = ["../package.json", "../../package.json"];
|
|
1806
|
+
for (const rel of candidates) {
|
|
1807
|
+
try {
|
|
1808
|
+
const pkg = req(rel);
|
|
1809
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
|
|
1810
|
+
} catch {
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
return "dev";
|
|
1814
|
+
}
|
|
1815
|
+
var CLI_VERSION = readOwnVersion();
|
|
1816
|
+
async function ensureProjectMeta(paths, projectRoot) {
|
|
1817
|
+
try {
|
|
1818
|
+
await fs2.mkdir(paths.projectDir, { recursive: true });
|
|
1819
|
+
const meta = {
|
|
1820
|
+
hash: paths.projectHash,
|
|
1821
|
+
root: projectRoot,
|
|
1822
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
1823
|
+
};
|
|
1824
|
+
await fs2.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
|
|
1825
|
+
} catch {
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
async function main(argv) {
|
|
1829
|
+
const { flags, positional } = parseArgs(argv);
|
|
1830
|
+
const cwd = typeof flags["cwd"] === "string" ? path2.resolve(flags["cwd"]) : process.cwd();
|
|
1831
|
+
const pathResolver = new DefaultPathResolver(cwd);
|
|
1832
|
+
const projectRoot = pathResolver.projectRoot;
|
|
1833
|
+
const userHome = os2.homedir();
|
|
1834
|
+
const wpaths = resolveWstackPaths({ projectRoot, userHome });
|
|
1835
|
+
await ensureProjectMeta(wpaths, projectRoot);
|
|
1836
|
+
if (positional[0] === "resume" && positional[1] && !subcommands["__noop_resume_marker"]) {
|
|
1837
|
+
flags["resume"] = positional[1];
|
|
1838
|
+
positional.splice(0, 2);
|
|
1839
|
+
}
|
|
1840
|
+
const vault = new DefaultSecretVault({ keyFile: wpaths.secretsKey });
|
|
1841
|
+
for (const file of [wpaths.globalConfig, wpaths.projectLocalConfig]) {
|
|
1842
|
+
try {
|
|
1843
|
+
const { migrated } = await migratePlaintextSecrets(file, vault);
|
|
1844
|
+
if (migrated > 0) {
|
|
1845
|
+
process.stderr.write(`[wstack] Encrypted ${migrated} plaintext secret(s) in ${file}
|
|
1846
|
+
`);
|
|
1847
|
+
}
|
|
1848
|
+
} catch {
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
const configLoader = new DefaultConfigLoader({ paths: wpaths, vault });
|
|
1852
|
+
let config;
|
|
1853
|
+
try {
|
|
1854
|
+
config = await configLoader.load({ cliFlags: flagsToConfigPatch(flags) });
|
|
1855
|
+
} catch (err) {
|
|
1856
|
+
process.stderr.write(`Config error: ${err instanceof Error ? err.message : String(err)}
|
|
1857
|
+
`);
|
|
1858
|
+
return 2;
|
|
1859
|
+
}
|
|
1860
|
+
const logger = new DefaultLogger({
|
|
1861
|
+
level: config.log.level,
|
|
1862
|
+
file: wpaths.logFile
|
|
1863
|
+
});
|
|
1864
|
+
const renderer = new TerminalRenderer();
|
|
1865
|
+
const reader = new ReadlineInputReader({ historyFile: wpaths.historyFile });
|
|
1866
|
+
const modelsRegistry = new DefaultModelsRegistry({
|
|
1867
|
+
cacheFile: wpaths.modelsCache,
|
|
1868
|
+
ttlSeconds: 24 * 3600
|
|
1869
|
+
});
|
|
1870
|
+
const first = positional[0];
|
|
1871
|
+
if (first && subcommands[first]) {
|
|
1872
|
+
const sessionStore2 = new DefaultSessionStore({ dir: wpaths.projectSessions });
|
|
1873
|
+
const skillLoader2 = new DefaultSkillLoader({
|
|
1874
|
+
paths: wpaths,
|
|
1875
|
+
bundledDir: resolveBundledSkillsDir()
|
|
1876
|
+
});
|
|
1877
|
+
const toolRegistryForSubcmd = new ToolRegistry();
|
|
1878
|
+
for (const t of builtinTools) toolRegistryForSubcmd.register(t);
|
|
1879
|
+
const code2 = await subcommands[first](positional.slice(1), {
|
|
1880
|
+
config,
|
|
1881
|
+
renderer,
|
|
1882
|
+
reader,
|
|
1883
|
+
sessionStore: sessionStore2,
|
|
1884
|
+
skillLoader: skillLoader2,
|
|
1885
|
+
toolRegistry: toolRegistryForSubcmd,
|
|
1886
|
+
modelsRegistry,
|
|
1887
|
+
paths: wpaths,
|
|
1888
|
+
vault,
|
|
1889
|
+
cwd,
|
|
1890
|
+
projectRoot,
|
|
1891
|
+
userHome
|
|
1892
|
+
});
|
|
1893
|
+
await reader.close();
|
|
1894
|
+
return code2;
|
|
1895
|
+
}
|
|
1896
|
+
if (!config.provider || !config.model) {
|
|
1897
|
+
process.stderr.write(
|
|
1898
|
+
"No provider or model configured. Run `wstack init` first, or set WRONGSTACK_PROVIDER + WRONGSTACK_MODEL.\n"
|
|
1899
|
+
);
|
|
1900
|
+
await reader.close();
|
|
1901
|
+
return 2;
|
|
1902
|
+
}
|
|
1903
|
+
const resolvedProvider = await modelsRegistry.getProvider(config.provider).catch(() => void 0);
|
|
1904
|
+
if (!resolvedProvider) {
|
|
1905
|
+
logger.warn(
|
|
1906
|
+
`Provider "${config.provider}" not found in models.dev. Continuing with raw config.`
|
|
1907
|
+
);
|
|
1908
|
+
} else if (resolvedProvider.family === "unsupported") {
|
|
1909
|
+
process.stderr.write(
|
|
1910
|
+
`Provider "${config.provider}" uses an unsupported wire family (${resolvedProvider.npm}). Install a plugin to enable it, or pick a different provider.
|
|
1911
|
+
`
|
|
1912
|
+
);
|
|
1913
|
+
await reader.close();
|
|
1914
|
+
return 2;
|
|
1915
|
+
}
|
|
1916
|
+
const container = new Container();
|
|
1917
|
+
container.bind(TOKENS.Logger, () => logger);
|
|
1918
|
+
container.bind(TOKENS.PathResolver, () => pathResolver);
|
|
1919
|
+
container.bind(TOKENS.SecretScrubber, () => new DefaultSecretScrubber());
|
|
1920
|
+
container.bind(TOKENS.RetryPolicy, () => new DefaultRetryPolicy());
|
|
1921
|
+
container.bind(TOKENS.ErrorHandler, () => new DefaultErrorHandler());
|
|
1922
|
+
container.bind(TOKENS.ModelsRegistry, () => modelsRegistry);
|
|
1923
|
+
container.bind(
|
|
1924
|
+
TOKENS.TokenCounter,
|
|
1925
|
+
() => new DefaultTokenCounter({ registry: modelsRegistry, providerId: config.provider })
|
|
1926
|
+
);
|
|
1927
|
+
container.bind(
|
|
1928
|
+
TOKENS.SessionStore,
|
|
1929
|
+
() => new DefaultSessionStore({ dir: wpaths.projectSessions })
|
|
1930
|
+
);
|
|
1931
|
+
const memoryStore = new DefaultMemoryStore({ paths: wpaths });
|
|
1932
|
+
container.bind(TOKENS.MemoryStore, () => memoryStore);
|
|
1933
|
+
const skillLoader = new DefaultSkillLoader({
|
|
1934
|
+
paths: wpaths,
|
|
1935
|
+
bundledDir: config.features.skills ? resolveBundledSkillsDir() : void 0
|
|
1936
|
+
});
|
|
1937
|
+
container.bind(TOKENS.SkillLoader, () => skillLoader);
|
|
1938
|
+
container.bind(
|
|
1939
|
+
TOKENS.SystemPromptBuilder,
|
|
1940
|
+
() => new DefaultSystemPromptBuilder({
|
|
1941
|
+
memoryStore,
|
|
1942
|
+
skillLoader: config.features.skills ? skillLoader : void 0
|
|
1943
|
+
})
|
|
1944
|
+
);
|
|
1945
|
+
container.bind(TOKENS.Renderer, () => renderer);
|
|
1946
|
+
container.bind(TOKENS.InputReader, () => reader);
|
|
1947
|
+
container.bind(
|
|
1948
|
+
TOKENS.PermissionPolicy,
|
|
1949
|
+
() => new DefaultPermissionPolicy({
|
|
1950
|
+
trustFile: wpaths.projectTrust,
|
|
1951
|
+
yolo: config.yolo,
|
|
1952
|
+
promptDelegate: makePromptDelegate(reader)
|
|
1953
|
+
})
|
|
1954
|
+
);
|
|
1955
|
+
container.bind(
|
|
1956
|
+
TOKENS.Compactor,
|
|
1957
|
+
() => new HybridCompactor({
|
|
1958
|
+
preserveK: config.context.preserveK,
|
|
1959
|
+
eliseThreshold: config.context.eliseThreshold
|
|
1960
|
+
})
|
|
1961
|
+
);
|
|
1962
|
+
const providerRegistry = new ProviderRegistry();
|
|
1963
|
+
if (config.features.modelsRegistry) {
|
|
1964
|
+
try {
|
|
1965
|
+
const factories = await buildProviderFactoriesFromRegistry({
|
|
1966
|
+
registry: modelsRegistry,
|
|
1967
|
+
log: logger
|
|
1968
|
+
});
|
|
1969
|
+
for (const f of factories) providerRegistry.register(f);
|
|
1970
|
+
} catch (err) {
|
|
1971
|
+
process.stderr.write(
|
|
1972
|
+
`Failed to load models.dev registry: ${err instanceof Error ? err.message : err}
|
|
1973
|
+
Try \`wstack models refresh\` once you have network access, or run with --no-features.
|
|
1974
|
+
`
|
|
1975
|
+
);
|
|
1976
|
+
await reader.close();
|
|
1977
|
+
return 2;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
const toolRegistry = new ToolRegistry();
|
|
1981
|
+
for (const t of builtinTools) toolRegistry.register(t);
|
|
1982
|
+
toolRegistry.registerDefault(
|
|
1983
|
+
createContextManagerTool({ compactor: container.resolve(TOKENS.Compactor) })
|
|
1984
|
+
);
|
|
1985
|
+
if (config.features.memory) {
|
|
1986
|
+
toolRegistry.register(rememberTool(memoryStore));
|
|
1987
|
+
toolRegistry.register(forgetTool(memoryStore));
|
|
1988
|
+
}
|
|
1989
|
+
const events = new EventBus();
|
|
1990
|
+
events.setLogger(logger);
|
|
1991
|
+
const spinner = new Spinner();
|
|
1992
|
+
let lastInputTokens = 0;
|
|
1993
|
+
events.on("provider.response", (e) => {
|
|
1994
|
+
lastInputTokens = e.usage?.input ?? 0;
|
|
1995
|
+
updateSpinnerContext();
|
|
1996
|
+
});
|
|
1997
|
+
events.on("iteration.started", () => {
|
|
1998
|
+
updateSpinnerContext();
|
|
1999
|
+
spinner.start(color.dim(`${config.provider}/${config.model} thinking\u2026`));
|
|
2000
|
+
});
|
|
2001
|
+
events.on("provider.response", () => {
|
|
2002
|
+
spinner.stop();
|
|
2003
|
+
});
|
|
2004
|
+
events.on("error", () => {
|
|
2005
|
+
spinner.stop();
|
|
2006
|
+
});
|
|
2007
|
+
let streamingActive = false;
|
|
2008
|
+
events.on("provider.text_delta", (p) => {
|
|
2009
|
+
if (!streamingActive) {
|
|
2010
|
+
spinner.stop();
|
|
2011
|
+
streamingActive = true;
|
|
2012
|
+
}
|
|
2013
|
+
renderer.write(p.text);
|
|
2014
|
+
});
|
|
2015
|
+
events.on("iteration.completed", () => {
|
|
2016
|
+
if (streamingActive) {
|
|
2017
|
+
renderer.write("\n");
|
|
2018
|
+
streamingActive = false;
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
events.on("provider.retry", (p) => {
|
|
2022
|
+
spinner.stop();
|
|
2023
|
+
if (streamingActive) {
|
|
2024
|
+
renderer.write("\n");
|
|
2025
|
+
streamingActive = false;
|
|
2026
|
+
}
|
|
2027
|
+
const secs = (p.delayMs / 1e3).toFixed(p.delayMs >= 1e3 ? 1 : 2);
|
|
2028
|
+
process.stderr.write(color.yellow(` \u27F3 retry ${p.attempt} in ${secs}s \u2014 ${p.description}
|
|
2029
|
+
`));
|
|
2030
|
+
spinner.start(color.dim(`${config.provider}/${config.model} thinking\u2026`));
|
|
2031
|
+
});
|
|
2032
|
+
events.on("provider.error", (p) => {
|
|
2033
|
+
spinner.stop();
|
|
2034
|
+
if (streamingActive) {
|
|
2035
|
+
renderer.write("\n");
|
|
2036
|
+
streamingActive = false;
|
|
2037
|
+
}
|
|
2038
|
+
process.stderr.write(color.red(` \u2717 ${p.description}
|
|
2039
|
+
`));
|
|
2040
|
+
});
|
|
2041
|
+
const providerConfig = config.providers?.[config.provider] ?? {
|
|
2042
|
+
type: config.provider,
|
|
2043
|
+
apiKey: config.apiKey,
|
|
2044
|
+
baseUrl: config.baseUrl
|
|
2045
|
+
};
|
|
2046
|
+
let provider;
|
|
2047
|
+
try {
|
|
2048
|
+
if (config.features.modelsRegistry) {
|
|
2049
|
+
provider = providerRegistry.create({ ...providerConfig, type: config.provider });
|
|
2050
|
+
} else {
|
|
2051
|
+
provider = makeProviderFromConfig(config.provider, {
|
|
2052
|
+
...providerConfig,
|
|
2053
|
+
type: config.provider
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
process.stderr.write(
|
|
2058
|
+
`Failed to create provider: ${err instanceof Error ? err.message : err}
|
|
2059
|
+
`
|
|
2060
|
+
);
|
|
2061
|
+
await reader.close();
|
|
2062
|
+
return 2;
|
|
2063
|
+
}
|
|
2064
|
+
const promptBuilder = container.resolve(TOKENS.SystemPromptBuilder);
|
|
2065
|
+
const systemPrompt = await promptBuilder.build({
|
|
2066
|
+
cwd,
|
|
2067
|
+
projectRoot,
|
|
2068
|
+
tools: toolRegistry.list(),
|
|
2069
|
+
provider: config.provider,
|
|
2070
|
+
model: config.model
|
|
2071
|
+
});
|
|
2072
|
+
const sessionStore = container.resolve(TOKENS.SessionStore);
|
|
2073
|
+
let resumeId = typeof flags["resume"] === "string" ? flags["resume"] : void 0;
|
|
2074
|
+
const recoveryLock = new RecoveryLock({
|
|
2075
|
+
dir: wpaths.projectSessions,
|
|
2076
|
+
sessionStore
|
|
2077
|
+
});
|
|
2078
|
+
if (!resumeId && !flags["no-recovery"]) {
|
|
2079
|
+
const abandoned = await recoveryLock.checkAbandoned();
|
|
2080
|
+
if (abandoned && abandoned.messageCount > 0) {
|
|
2081
|
+
const choice = await promptRecovery(reader, renderer, abandoned, !!flags["recover"]);
|
|
2082
|
+
if (choice === "resume") {
|
|
2083
|
+
resumeId = abandoned.sessionId;
|
|
2084
|
+
} else if (choice === "delete") {
|
|
2085
|
+
await sessionStore.delete(abandoned.sessionId).catch(() => void 0);
|
|
2086
|
+
await recoveryLock.clear();
|
|
2087
|
+
} else {
|
|
2088
|
+
await recoveryLock.clear();
|
|
2089
|
+
}
|
|
2090
|
+
} else if (abandoned) {
|
|
2091
|
+
await sessionStore.delete(abandoned.sessionId).catch(() => void 0);
|
|
2092
|
+
await recoveryLock.clear();
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
let session;
|
|
2096
|
+
let restoredMessages = [];
|
|
2097
|
+
if (resumeId) {
|
|
2098
|
+
try {
|
|
2099
|
+
const resumed = await sessionStore.resume(resumeId);
|
|
2100
|
+
session = resumed.writer;
|
|
2101
|
+
restoredMessages = resumed.data.messages;
|
|
2102
|
+
renderer.writeInfo(
|
|
2103
|
+
`Resumed session ${resumed.data.metadata.id} \u2014 ${restoredMessages.length} messages, ${resumed.data.usage.input + resumed.data.usage.output} tokens used previously.`
|
|
2104
|
+
);
|
|
2105
|
+
} catch (err) {
|
|
2106
|
+
renderer.writeError(`Resume failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2107
|
+
return 2;
|
|
2108
|
+
}
|
|
2109
|
+
} else {
|
|
2110
|
+
session = await sessionStore.create({
|
|
2111
|
+
id: "",
|
|
2112
|
+
title: "",
|
|
2113
|
+
model: config.model,
|
|
2114
|
+
provider: config.provider
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
await recoveryLock.write(session.id).catch(() => void 0);
|
|
2118
|
+
const attachments = new DefaultAttachmentStore({
|
|
2119
|
+
spoolDir: path2.join(wpaths.projectSessions, session.id, "attachments")
|
|
2120
|
+
});
|
|
2121
|
+
const queueStore = new QueueStore({
|
|
2122
|
+
dir: path2.join(wpaths.projectSessions, session.id)
|
|
2123
|
+
});
|
|
2124
|
+
const tokenCounter = container.resolve(TOKENS.TokenCounter);
|
|
2125
|
+
const stats = new SessionStats(events, tokenCounter);
|
|
2126
|
+
const ctxSignal = new AbortController().signal;
|
|
2127
|
+
const context = new Context({
|
|
2128
|
+
systemPrompt,
|
|
2129
|
+
provider,
|
|
2130
|
+
session,
|
|
2131
|
+
signal: ctxSignal,
|
|
2132
|
+
tokenCounter,
|
|
2133
|
+
cwd,
|
|
2134
|
+
projectRoot,
|
|
2135
|
+
model: config.model
|
|
2136
|
+
});
|
|
2137
|
+
if (restoredMessages.length > 0) {
|
|
2138
|
+
context.messages.push(...restoredMessages);
|
|
2139
|
+
}
|
|
2140
|
+
const pipelines = createDefaultPipelines();
|
|
2141
|
+
const compactor = container.resolve(TOKENS.Compactor);
|
|
2142
|
+
const resolvedCaps = await capabilitiesFor(modelsRegistry, config.provider, context.model).catch(
|
|
2143
|
+
() => void 0
|
|
2144
|
+
);
|
|
2145
|
+
const effectiveMaxContext = config.context.effectiveMaxContext ?? resolvedCaps?.maxContext ?? provider.capabilities.maxContext;
|
|
2146
|
+
const updateSpinnerContext = () => {
|
|
2147
|
+
if (effectiveMaxContext > 0 && lastInputTokens > 0) {
|
|
2148
|
+
spinner.setContext({ used: lastInputTokens, max: effectiveMaxContext });
|
|
2149
|
+
} else {
|
|
2150
|
+
spinner.setContext(void 0);
|
|
2151
|
+
}
|
|
2152
|
+
};
|
|
2153
|
+
if (config.context.autoCompact !== false) {
|
|
2154
|
+
const autoCompactor = new AutoCompactionMiddleware(
|
|
2155
|
+
compactor,
|
|
2156
|
+
effectiveMaxContext,
|
|
2157
|
+
(ctx) => {
|
|
2158
|
+
const msgs = ctx.messages;
|
|
2159
|
+
let total = 0;
|
|
2160
|
+
for (const m of msgs) {
|
|
2161
|
+
if (typeof m.content === "string") total += Math.ceil(m.content.length / 4);
|
|
2162
|
+
else if (Array.isArray(m.content)) {
|
|
2163
|
+
for (const b of m.content) {
|
|
2164
|
+
if (b.type === "text") total += Math.ceil(b.text.length / 4);
|
|
2165
|
+
else if (b.type === "tool_use" || b.type === "tool_result") {
|
|
2166
|
+
total += Math.ceil(JSON.stringify(b).length / 4);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
return total;
|
|
2172
|
+
},
|
|
2173
|
+
{
|
|
2174
|
+
warn: config.context.warnThreshold,
|
|
2175
|
+
soft: config.context.softThreshold,
|
|
2176
|
+
hard: config.context.hardThreshold
|
|
2177
|
+
},
|
|
2178
|
+
"soft"
|
|
2179
|
+
);
|
|
2180
|
+
pipelines.contextWindow.use({
|
|
2181
|
+
name: "AutoCompaction",
|
|
2182
|
+
handler: autoCompactor.handler()
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
const agent = new Agent({
|
|
2186
|
+
container,
|
|
2187
|
+
tools: toolRegistry,
|
|
2188
|
+
providers: providerRegistry,
|
|
2189
|
+
events,
|
|
2190
|
+
pipelines,
|
|
2191
|
+
context,
|
|
2192
|
+
maxIterations: config.tools.maxIterations,
|
|
2193
|
+
iterationTimeoutMs: config.tools.iterationTimeoutMs,
|
|
2194
|
+
executionStrategy: config.tools.defaultExecutionStrategy,
|
|
2195
|
+
perIterationOutputCapBytes: config.tools.perIterationOutputCapBytes
|
|
2196
|
+
});
|
|
2197
|
+
const mcpRegistry = new MCPRegistry({ toolRegistry, events, log: logger });
|
|
2198
|
+
if (config.features.mcp) {
|
|
2199
|
+
for (const cfg of Object.values(config.mcpServers ?? {})) {
|
|
2200
|
+
try {
|
|
2201
|
+
await mcpRegistry.start(cfg);
|
|
2202
|
+
} catch (err) {
|
|
2203
|
+
logger.warn(`MCP server "${cfg.name}" failed to start`, err);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
const slashRegistry = new SlashCommandRegistry();
|
|
2208
|
+
if (config.features.plugins && config.plugins && config.plugins.length > 0) {
|
|
2209
|
+
const resolvedPlugins = [];
|
|
2210
|
+
for (const p of config.plugins) {
|
|
2211
|
+
const spec = typeof p === "string" ? p : p.name;
|
|
2212
|
+
try {
|
|
2213
|
+
const mod = await import(spec);
|
|
2214
|
+
if (mod.default) resolvedPlugins.push(mod.default);
|
|
2215
|
+
} catch (err) {
|
|
2216
|
+
logger.warn(`Plugin "${spec}" failed to load`, err);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
if (resolvedPlugins.length > 0) {
|
|
2220
|
+
const { default: createApi2 } = await Promise.resolve().then(() => (init_plugin_api_factory(), plugin_api_factory_exports));
|
|
2221
|
+
await loadPlugins(resolvedPlugins, {
|
|
2222
|
+
log: logger,
|
|
2223
|
+
apiFactory: (plugin) => createApi2(plugin.name, {
|
|
2224
|
+
container,
|
|
2225
|
+
events,
|
|
2226
|
+
pipelines,
|
|
2227
|
+
toolRegistry,
|
|
2228
|
+
providerRegistry,
|
|
2229
|
+
slashCommandRegistry: slashRegistry,
|
|
2230
|
+
mcpRegistry,
|
|
2231
|
+
config,
|
|
2232
|
+
log: logger
|
|
2233
|
+
})
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
const slashCmds = buildBuiltinSlashCommands({
|
|
2238
|
+
registry: slashRegistry,
|
|
2239
|
+
toolRegistry,
|
|
2240
|
+
compactor: container.resolve(TOKENS.Compactor),
|
|
2241
|
+
sessionStore,
|
|
2242
|
+
skillLoader,
|
|
2243
|
+
tokenCounter,
|
|
2244
|
+
renderer,
|
|
2245
|
+
memoryStore,
|
|
2246
|
+
context,
|
|
2247
|
+
onExit: () => {
|
|
2248
|
+
void mcpRegistry.stopAll();
|
|
2249
|
+
},
|
|
2250
|
+
onClear: () => {
|
|
2251
|
+
try {
|
|
2252
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
2253
|
+
} catch {
|
|
2254
|
+
}
|
|
2255
|
+
},
|
|
2256
|
+
onSwitchModel: (name) => {
|
|
2257
|
+
context.model = name;
|
|
2258
|
+
},
|
|
2259
|
+
onSwitchProvider: (name) => {
|
|
2260
|
+
try {
|
|
2261
|
+
const newCfg = config.providers?.[name] ?? {
|
|
2262
|
+
type: name,
|
|
2263
|
+
apiKey: config.apiKey,
|
|
2264
|
+
baseUrl: config.baseUrl
|
|
2265
|
+
};
|
|
2266
|
+
const newProvider = providerRegistry.create({ ...newCfg, type: name });
|
|
2267
|
+
context.provider = newProvider;
|
|
2268
|
+
config.provider = name;
|
|
2269
|
+
} catch (err) {
|
|
2270
|
+
renderer.writeError(
|
|
2271
|
+
`Cannot switch to "${name}": ${err instanceof Error ? err.message : err}`
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
},
|
|
2275
|
+
onDiag: () => {
|
|
2276
|
+
const u = tokenCounter.total();
|
|
2277
|
+
const cost = tokenCounter.estimateCost();
|
|
2278
|
+
renderer.write(
|
|
2279
|
+
[
|
|
2280
|
+
`${color.bold("WrongStack diag")}`,
|
|
2281
|
+
` provider: ${config.provider} / ${context.model}`,
|
|
2282
|
+
` projectRoot: ${projectRoot}`,
|
|
2283
|
+
` tokens: in ${u.input} out ${u.output} cacheR ${u.cacheRead ?? 0}`,
|
|
2284
|
+
` cost: $${cost.total.toFixed(4)}`,
|
|
2285
|
+
` tools: ${toolRegistry.list().length}`,
|
|
2286
|
+
` mcpServers: ${mcpRegistry.list().length}`,
|
|
2287
|
+
""
|
|
2288
|
+
].join("\n")
|
|
2289
|
+
);
|
|
2290
|
+
},
|
|
2291
|
+
onStats: () => {
|
|
2292
|
+
stats.render(renderer);
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
for (const cmd of slashCmds) slashRegistry.register(cmd);
|
|
2296
|
+
let code = 0;
|
|
2297
|
+
try {
|
|
2298
|
+
const promptFlag = typeof flags["prompt"] === "string" ? flags["prompt"] : void 0;
|
|
2299
|
+
if (promptFlag) {
|
|
2300
|
+
positional.unshift(promptFlag);
|
|
2301
|
+
}
|
|
2302
|
+
if (positional.length > 0 || promptFlag) {
|
|
2303
|
+
const query = positional.join(" ");
|
|
2304
|
+
const ctrl = new AbortController();
|
|
2305
|
+
const onSigint = () => ctrl.abort();
|
|
2306
|
+
process.on("SIGINT", onSigint);
|
|
2307
|
+
const startedAt = Date.now();
|
|
2308
|
+
const before = tokenCounter.total();
|
|
2309
|
+
const costBefore = tokenCounter.estimateCost().total;
|
|
2310
|
+
let result;
|
|
2311
|
+
try {
|
|
2312
|
+
result = await agent.run(query, { signal: ctrl.signal });
|
|
2313
|
+
} finally {
|
|
2314
|
+
process.off("SIGINT", onSigint);
|
|
2315
|
+
}
|
|
2316
|
+
const after = tokenCounter.total();
|
|
2317
|
+
const costAfter = tokenCounter.estimateCost().total;
|
|
2318
|
+
const usage = {
|
|
2319
|
+
input: after.input - before.input,
|
|
2320
|
+
output: after.output - before.output,
|
|
2321
|
+
iterations: result.iterations,
|
|
2322
|
+
cost: costAfter - costBefore,
|
|
2323
|
+
elapsedMs: Date.now() - startedAt
|
|
2324
|
+
};
|
|
2325
|
+
if (flags["output-json"]) {
|
|
2326
|
+
const json = JSON.stringify({
|
|
2327
|
+
status: result.status,
|
|
2328
|
+
finalText: result.finalText ?? null,
|
|
2329
|
+
error: result.error instanceof Error ? result.error.message : result.error ?? null,
|
|
2330
|
+
usage
|
|
2331
|
+
});
|
|
2332
|
+
process.stdout.write(json + "\n");
|
|
2333
|
+
} else {
|
|
2334
|
+
if (result.status === "failed") {
|
|
2335
|
+
code = 1;
|
|
2336
|
+
renderer.writeError(
|
|
2337
|
+
"Failed: " + (result.error instanceof Error ? result.error.message : String(result.error))
|
|
2338
|
+
);
|
|
2339
|
+
} else if (result.status === "aborted") {
|
|
2340
|
+
code = 130;
|
|
2341
|
+
renderer.writeWarning("Aborted.");
|
|
2342
|
+
} else if (result.status === "max_iterations") {
|
|
2343
|
+
code = 1;
|
|
2344
|
+
renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
|
|
2345
|
+
}
|
|
2346
|
+
if (result.finalText) renderer.write("\n" + result.finalText + "\n");
|
|
2347
|
+
renderer.write(
|
|
2348
|
+
"\n" + color.dim(
|
|
2349
|
+
`[in: ${fmtTok4(usage.input)} out: ${fmtTok4(usage.output)} iters: ${usage.iterations} cost: ${usage.cost.toFixed(4)} ${(usage.elapsedMs / 1e3).toFixed(1)}s]`
|
|
2350
|
+
) + "\n"
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2353
|
+
} else if (flags.tui && !flags["no-tui"]) {
|
|
2354
|
+
const { runTui } = await import('@wrongstack/tui');
|
|
2355
|
+
renderer.setSilent(true);
|
|
2356
|
+
try {
|
|
2357
|
+
code = await runTui({
|
|
2358
|
+
agent,
|
|
2359
|
+
events,
|
|
2360
|
+
slashRegistry,
|
|
2361
|
+
attachments,
|
|
2362
|
+
tokenCounter,
|
|
2363
|
+
model: context.model,
|
|
2364
|
+
banner: !flags["no-banner"],
|
|
2365
|
+
queueStore,
|
|
2366
|
+
yolo: !!config.yolo,
|
|
2367
|
+
appVersion: CLI_VERSION,
|
|
2368
|
+
provider: config.provider,
|
|
2369
|
+
effectiveMaxContext,
|
|
2370
|
+
// Opt-in: alt-screen disables the terminal's native scrollback,
|
|
2371
|
+
// so we default to false. `--no-alt-screen` is kept as a no-op
|
|
2372
|
+
// for backward compatibility with old invocation scripts.
|
|
2373
|
+
altScreen: flags["alt-screen"] === true,
|
|
2374
|
+
// Alt-screen exit erases the TUI view. Print a one-line hint
|
|
2375
|
+
// into the user's normal terminal so they know the session is
|
|
2376
|
+
// preserved and can resume it. Skipped automatically when
|
|
2377
|
+
// alt-screen is off — runTui only fires onAfterExit then.
|
|
2378
|
+
onAfterExit: () => {
|
|
2379
|
+
process.stdout.write(
|
|
2380
|
+
color.dim(`Session saved: ${session.id} \u2014 resume with `) + color.cyan(`wstack resume ${session.id}`) + "\n"
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
});
|
|
2384
|
+
} finally {
|
|
2385
|
+
renderer.setSilent(false);
|
|
2386
|
+
}
|
|
2387
|
+
} else {
|
|
2388
|
+
code = await runRepl({
|
|
2389
|
+
agent,
|
|
2390
|
+
renderer,
|
|
2391
|
+
reader,
|
|
2392
|
+
slashRegistry,
|
|
2393
|
+
tokenCounter,
|
|
2394
|
+
attachments,
|
|
2395
|
+
effectiveMaxContext
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
} finally {
|
|
2399
|
+
stats.render(renderer);
|
|
2400
|
+
await mcpRegistry.stopAll();
|
|
2401
|
+
await session.append({
|
|
2402
|
+
type: "session_end",
|
|
2403
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2404
|
+
usage: tokenCounter.total()
|
|
2405
|
+
});
|
|
2406
|
+
await session.close();
|
|
2407
|
+
await recoveryLock.clear().catch(() => void 0);
|
|
2408
|
+
await reader.close();
|
|
2409
|
+
}
|
|
2410
|
+
return code;
|
|
2411
|
+
}
|
|
2412
|
+
async function promptRecovery(reader, renderer, abandoned, autoRecover) {
|
|
2413
|
+
const minutes = Math.round(abandoned.ageMs / 6e4);
|
|
2414
|
+
const ageLabel = minutes < 1 ? `${Math.round(abandoned.ageMs / 1e3)}s ago` : minutes < 60 ? `${minutes} min ago` : `${Math.round(minutes / 60)}h ago`;
|
|
2415
|
+
const summary = `Previous session was killed mid-run: ${abandoned.sessionId} (${abandoned.messageCount} messages, ${ageLabel}).`;
|
|
2416
|
+
if (autoRecover) {
|
|
2417
|
+
renderer.writeInfo(`${summary} Auto-resuming (--recover).`);
|
|
2418
|
+
return "resume";
|
|
2419
|
+
}
|
|
2420
|
+
if (!process.stdin.isTTY) {
|
|
2421
|
+
renderer.writeInfo(
|
|
2422
|
+
`${summary} Non-interactive \u2014 leaving as-is. Use \`wstack resume ${abandoned.sessionId}\` or pass \`--recover\` to auto-resume.`
|
|
2423
|
+
);
|
|
2424
|
+
return "skip";
|
|
2425
|
+
}
|
|
2426
|
+
renderer.writeInfo(summary);
|
|
2427
|
+
const answer = await reader.readKey(
|
|
2428
|
+
`${color.amber("?")} Recover it? ${color.dim("[")}${color.bold("Y")}es / ${color.bold("n")}o / ${color.bold("d")}elete${color.dim("]")} `,
|
|
2429
|
+
[
|
|
2430
|
+
{ key: "y", label: "yes", value: "resume" },
|
|
2431
|
+
{ key: "Y", label: "yes", value: "resume" },
|
|
2432
|
+
{ key: "\r", label: "yes", value: "resume" },
|
|
2433
|
+
{ key: "\n", label: "yes", value: "resume" },
|
|
2434
|
+
{ key: "n", label: "no", value: "skip" },
|
|
2435
|
+
{ key: "N", label: "no", value: "skip" },
|
|
2436
|
+
{ key: "d", label: "delete", value: "delete" },
|
|
2437
|
+
{ key: "D", label: "delete", value: "delete" }
|
|
2438
|
+
]
|
|
2439
|
+
);
|
|
2440
|
+
return answer;
|
|
2441
|
+
}
|
|
2442
|
+
function fmtTok4(n) {
|
|
2443
|
+
if (n < 1e3) return String(n);
|
|
2444
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
2445
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
2446
|
+
}
|
|
2447
|
+
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");
|
|
2448
|
+
if (isMain) {
|
|
2449
|
+
main(process.argv.slice(2)).then(
|
|
2450
|
+
(c) => {
|
|
2451
|
+
process.exitCode = c;
|
|
2452
|
+
setTimeout(() => process.exit(c), 200).unref();
|
|
2453
|
+
},
|
|
2454
|
+
(err) => {
|
|
2455
|
+
process.stderr.write((err instanceof Error ? err.stack : String(err)) + "\n");
|
|
2456
|
+
process.exitCode = 1;
|
|
2457
|
+
setTimeout(() => process.exit(1), 200).unref();
|
|
2458
|
+
}
|
|
2459
|
+
);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
export { main };
|
|
2463
|
+
//# sourceMappingURL=index.js.map
|
|
2464
|
+
//# sourceMappingURL=index.js.map
|