@wrongstack/cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1242 -845
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -234,933 +234,1282 @@ function hasDiff(input) {
|
|
|
234
234
|
input && typeof input === "object" && "diff" in input
|
|
235
235
|
);
|
|
236
236
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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}
|
|
237
|
+
function hasApiKey(provider, config) {
|
|
238
|
+
if (provider.envVars.some((v) => !!process.env[v])) return true;
|
|
239
|
+
const stored = config?.providers?.[provider.id]?.apiKey;
|
|
240
|
+
if (typeof stored === "string" && stored.length > 0) return true;
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
async function runPicker(deps) {
|
|
244
|
+
const { modelsRegistry, renderer, reader, config, defaultProvider, defaultModel } = deps;
|
|
245
|
+
renderer.write(`
|
|
246
|
+
${color.bold(theme2.primary("WrongStack") + color.dim(" \u2014 Provider & Model Selection"))}
|
|
277
247
|
`);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
}
|
|
248
|
+
renderer.write(color.dim("Loading provider catalog\u2026\n"));
|
|
249
|
+
let providers;
|
|
250
|
+
try {
|
|
251
|
+
providers = await modelsRegistry.listProviders();
|
|
252
|
+
} catch {
|
|
253
|
+
renderer.writeError("Failed to load provider catalog. Pass --provider and --model to skip the picker.");
|
|
254
|
+
return void 0;
|
|
291
255
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const display = formatInputSummary(input);
|
|
297
|
-
this.out.write(`${arrow} ${theme.bold(name)}${display ? color.dim(` ${display}`) : ""}
|
|
298
|
-
`);
|
|
299
|
-
this.lineStart = true;
|
|
256
|
+
const supported = providers.filter((p) => p.family !== "unsupported");
|
|
257
|
+
if (supported.length === 0) {
|
|
258
|
+
renderer.writeError("No supported providers found in catalog.");
|
|
259
|
+
return void 0;
|
|
300
260
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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"}`)}
|
|
261
|
+
const keyed = supported.filter((p) => hasApiKey(p, config));
|
|
262
|
+
let displayList = keyed;
|
|
263
|
+
let showingFallback = false;
|
|
264
|
+
if (keyed.length === 0) {
|
|
265
|
+
displayList = supported;
|
|
266
|
+
showingFallback = true;
|
|
267
|
+
}
|
|
268
|
+
const families = /* @__PURE__ */ new Map();
|
|
269
|
+
for (const p of displayList) {
|
|
270
|
+
const list = families.get(p.family) ?? [];
|
|
271
|
+
list.push(p);
|
|
272
|
+
families.set(p.family, list);
|
|
273
|
+
}
|
|
274
|
+
const ordered = [];
|
|
275
|
+
const familyOrder = ["anthropic", "openai", "google", "openai-compatible"];
|
|
276
|
+
let idx = 1;
|
|
277
|
+
let defaultIdx;
|
|
278
|
+
renderer.write("\n");
|
|
279
|
+
for (const fam of familyOrder) {
|
|
280
|
+
const list = families.get(fam);
|
|
281
|
+
if (!list || list.length === 0) continue;
|
|
282
|
+
renderer.write(` ${color.bold(fam)}
|
|
338
283
|
`);
|
|
284
|
+
for (const p of list) {
|
|
285
|
+
const envFound = p.envVars.some((v) => !!process.env[v]);
|
|
286
|
+
const configKey = typeof config?.providers?.[p.id]?.apiKey === "string" && config.providers[p.id].apiKey.length > 0;
|
|
287
|
+
const marker = envFound ? color.green("\u25CF") : configKey ? color.cyan("\u25C9") : color.dim("\u25CB");
|
|
288
|
+
const isDefault = p.id === defaultProvider;
|
|
289
|
+
if (isDefault) defaultIdx = idx;
|
|
290
|
+
const idLabel = isDefault ? color.bold(p.id) : p.id;
|
|
291
|
+
const suffix = isDefault ? color.dim(" (default)") : "";
|
|
292
|
+
renderer.write(
|
|
293
|
+
` ${color.dim(`${idx}.`.padStart(4))} ${marker} ${idLabel.padEnd(22)} ${color.dim(p.name)}${suffix}
|
|
294
|
+
`
|
|
295
|
+
);
|
|
296
|
+
ordered.push({ provider: p, index: idx });
|
|
297
|
+
idx++;
|
|
339
298
|
}
|
|
340
|
-
this.lineStart = true;
|
|
341
299
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
`
|
|
347
|
-
|
|
300
|
+
if (showingFallback) {
|
|
301
|
+
renderer.write(
|
|
302
|
+
`
|
|
303
|
+
${color.yellow("\u26A0 No API keys detected.")} ${color.dim("Pick a provider, then run `wstack auth <provider>` to add one.")}
|
|
304
|
+
`
|
|
305
|
+
);
|
|
306
|
+
} else {
|
|
307
|
+
renderer.write(
|
|
308
|
+
`
|
|
309
|
+
${color.dim("\u25CF = env key \u25C9 = stored in config \u25CB = no key")}
|
|
310
|
+
`
|
|
311
|
+
);
|
|
348
312
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
`);
|
|
313
|
+
const defaultHint = defaultIdx !== void 0 && defaultProvider ? ` ${color.dim(`[Enter = ${defaultProvider}]`)}` : "";
|
|
314
|
+
const providerAnswer = (await reader.readLine(`
|
|
315
|
+
${color.amber("?")} Select provider (1-${ordered.length})${defaultHint}: `)).trim();
|
|
316
|
+
if (!providerAnswer) {
|
|
317
|
+
if (defaultIdx !== void 0) {
|
|
318
|
+
const def = ordered[defaultIdx - 1];
|
|
319
|
+
if (def) return pickModel(def.provider, modelsRegistry, renderer, reader, defaultModel);
|
|
320
|
+
}
|
|
321
|
+
renderer.write(color.dim("Cancelled.\n"));
|
|
322
|
+
return void 0;
|
|
352
323
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
324
|
+
const providerIdx = parseInt(providerAnswer, 10);
|
|
325
|
+
if (Number.isNaN(providerIdx) || providerIdx < 1 || providerIdx > ordered.length) {
|
|
326
|
+
const byId = ordered.find((o) => o.provider.id.toLowerCase() === providerAnswer.toLowerCase());
|
|
327
|
+
if (!byId) {
|
|
328
|
+
renderer.writeError(`Invalid selection: "${providerAnswer}"`);
|
|
329
|
+
return void 0;
|
|
330
|
+
}
|
|
331
|
+
return pickModel(byId.provider, modelsRegistry, renderer, reader, defaultModel);
|
|
356
332
|
}
|
|
357
|
-
|
|
358
|
-
|
|
333
|
+
const chosen = ordered[providerIdx - 1];
|
|
334
|
+
if (!chosen) return void 0;
|
|
335
|
+
const modelHint = chosen.provider.id === defaultProvider ? defaultModel : void 0;
|
|
336
|
+
return pickModel(chosen.provider, modelsRegistry, renderer, reader, modelHint);
|
|
337
|
+
}
|
|
338
|
+
async function pickModel(provider, registry, renderer, reader, defaultModel) {
|
|
339
|
+
renderer.write(`
|
|
340
|
+
${color.bold(provider.name)} ${color.dim(`(${provider.id})`)} models:
|
|
341
|
+
|
|
359
342
|
`);
|
|
343
|
+
const models = [...provider.models].sort(
|
|
344
|
+
(a, b) => (b.release_date ?? "").localeCompare(a.release_date ?? "")
|
|
345
|
+
);
|
|
346
|
+
if (models.length === 0) {
|
|
347
|
+
renderer.writeError(" No models listed for this provider in the catalog.");
|
|
348
|
+
return void 0;
|
|
360
349
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
350
|
+
const defaultIdxInModels = defaultModel !== void 0 ? models.findIndex((m) => m.id === defaultModel) : -1;
|
|
351
|
+
const pageSize = 30;
|
|
352
|
+
let offset = 0;
|
|
353
|
+
while (offset < models.length) {
|
|
354
|
+
const page = models.slice(offset, offset + pageSize);
|
|
355
|
+
for (let i = 0; i < page.length; i++) {
|
|
356
|
+
const m = page[i];
|
|
357
|
+
const num = offset + i + 1;
|
|
358
|
+
const ctx = m.limit?.context ? `${(m.limit.context / 1e3).toFixed(0)}k`.padStart(6) : " ?";
|
|
359
|
+
const cost = m.cost?.input !== void 0 ? `$${m.cost.input}/$${m.cost.output ?? "?"}` : "";
|
|
360
|
+
const caps = [];
|
|
361
|
+
if (m.tool_call) caps.push("tools");
|
|
362
|
+
if (m.reasoning) caps.push("reason");
|
|
363
|
+
if (m.modalities?.input?.includes("image")) caps.push("vision");
|
|
364
|
+
const capStr = caps.length > 0 ? color.dim(caps.join(",")) : "";
|
|
365
|
+
const isDefault = m.id === defaultModel;
|
|
366
|
+
const idLabel = isDefault ? color.bold(m.id) : m.id;
|
|
367
|
+
const suffix = isDefault ? color.dim(" (default)") : "";
|
|
368
|
+
renderer.write(
|
|
369
|
+
` ${color.dim(`${num}.`.padStart(5))} ${idLabel.padEnd(44)} ${color.dim(ctx)} ${color.dim(cost.padEnd(14))} ${capStr}${suffix}
|
|
370
|
+
`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
offset += pageSize;
|
|
374
|
+
if (offset < models.length) {
|
|
375
|
+
const more = (await reader.readLine(
|
|
376
|
+
`
|
|
377
|
+
${color.amber("?")} Showing ${Math.min(offset, models.length)}/${models.length} \u2014 Enter number or ${color.dim("Enter")} for more: `
|
|
378
|
+
)).trim();
|
|
379
|
+
if (!more) continue;
|
|
380
|
+
return resolveModelSelection(more, models, provider, registry, renderer);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const defaultHint = defaultIdxInModels >= 0 && defaultModel ? ` ${color.dim(`[Enter = ${defaultModel}]`)}` : "";
|
|
384
|
+
const answer = (await reader.readLine(`
|
|
385
|
+
${color.amber("?")} Select model (1-${models.length})${defaultHint}: `)).trim();
|
|
386
|
+
if (!answer) {
|
|
387
|
+
if (defaultIdxInModels >= 0 && defaultModel) {
|
|
388
|
+
renderer.write(
|
|
389
|
+
`
|
|
390
|
+
${color.green("\u2713")} ${color.bold(provider.id)} / ${color.bold(defaultModel)}
|
|
391
|
+
|
|
392
|
+
`
|
|
393
|
+
);
|
|
394
|
+
return { provider: provider.id, model: defaultModel };
|
|
395
|
+
}
|
|
396
|
+
renderer.write(color.dim("Cancelled.\n"));
|
|
397
|
+
return void 0;
|
|
365
398
|
}
|
|
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;
|
|
399
|
+
return resolveModelSelection(answer, models, provider, registry, renderer);
|
|
381
400
|
}
|
|
382
|
-
function
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
401
|
+
async function resolveModelSelection(answer, models, provider, _registry, renderer, _reader) {
|
|
402
|
+
const idx = parseInt(answer, 10);
|
|
403
|
+
let modelId;
|
|
404
|
+
if (!Number.isNaN(idx) && idx >= 1 && idx <= models.length) {
|
|
405
|
+
modelId = models[idx - 1].id;
|
|
406
|
+
} else {
|
|
407
|
+
const lower = answer.toLowerCase();
|
|
408
|
+
const match = models.find((m) => m.id.toLowerCase() === lower);
|
|
409
|
+
if (match) {
|
|
410
|
+
modelId = match.id;
|
|
411
|
+
} else {
|
|
412
|
+
const partial = models.filter((m) => m.id.toLowerCase().includes(lower));
|
|
413
|
+
if (partial.length === 1) {
|
|
414
|
+
modelId = partial[0].id;
|
|
415
|
+
} else if (partial.length > 1) {
|
|
416
|
+
renderer.writeError(`"${answer}" matches multiple models. Be more specific.`);
|
|
417
|
+
return void 0;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
390
420
|
}
|
|
391
|
-
if (
|
|
392
|
-
|
|
421
|
+
if (!modelId) {
|
|
422
|
+
modelId = answer;
|
|
423
|
+
}
|
|
424
|
+
renderer.write(
|
|
425
|
+
`
|
|
426
|
+
${color.green("\u2713")} ${color.bold(provider.id)} / ${color.bold(modelId)}
|
|
427
|
+
|
|
428
|
+
`
|
|
429
|
+
);
|
|
430
|
+
return { provider: provider.id, model: modelId };
|
|
393
431
|
}
|
|
394
|
-
|
|
395
|
-
|
|
432
|
+
var theme2 = { primary: color.amber };
|
|
433
|
+
async function saveToGlobalConfig(configPath, provider, model) {
|
|
396
434
|
try {
|
|
397
|
-
|
|
435
|
+
const { atomicWrite: atomicWrite2 } = await import('@wrongstack/core');
|
|
436
|
+
const fs6 = await import('fs/promises');
|
|
437
|
+
let existing = {};
|
|
438
|
+
try {
|
|
439
|
+
const raw = await fs6.readFile(configPath, "utf8");
|
|
440
|
+
existing = JSON.parse(raw);
|
|
441
|
+
} catch {
|
|
442
|
+
}
|
|
443
|
+
existing.provider = provider;
|
|
444
|
+
existing.model = model;
|
|
445
|
+
await atomicWrite2(configPath, JSON.stringify(existing, null, 2));
|
|
446
|
+
return true;
|
|
398
447
|
} catch {
|
|
399
|
-
return
|
|
448
|
+
return false;
|
|
400
449
|
}
|
|
401
450
|
}
|
|
402
|
-
function
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
451
|
+
function buildBuiltinSlashCommands(opts) {
|
|
452
|
+
return [
|
|
453
|
+
helpCommand(opts),
|
|
454
|
+
initCommand(opts),
|
|
455
|
+
clearCommand(opts),
|
|
456
|
+
compactCommand(opts),
|
|
457
|
+
contextCommand(opts),
|
|
458
|
+
usageCommand(opts),
|
|
459
|
+
toolsCommand(opts),
|
|
460
|
+
skillCommand(opts),
|
|
461
|
+
useCommand(opts),
|
|
462
|
+
modelCommand(opts),
|
|
463
|
+
diagCommand(opts),
|
|
464
|
+
statsCommand(opts),
|
|
465
|
+
saveCommand(opts),
|
|
466
|
+
loadCommand(opts),
|
|
467
|
+
exitCommand(opts)
|
|
468
|
+
];
|
|
469
|
+
}
|
|
470
|
+
function initCommand(opts) {
|
|
471
|
+
return {
|
|
472
|
+
name: "init",
|
|
473
|
+
description: "Scaffold .wrongstack/AGENTS.md in the current project.",
|
|
474
|
+
async run(args, ctx) {
|
|
475
|
+
const force = args.trim() === "--force";
|
|
476
|
+
const dir = path2.join(ctx.projectRoot, ".wrongstack");
|
|
477
|
+
const file = path2.join(dir, "AGENTS.md");
|
|
410
478
|
try {
|
|
411
|
-
|
|
412
|
-
if (
|
|
413
|
-
|
|
479
|
+
await fs2.access(file);
|
|
480
|
+
if (!force) {
|
|
481
|
+
const msg = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
|
|
482
|
+
opts.renderer.writeWarning(msg);
|
|
483
|
+
return { message: msg };
|
|
414
484
|
}
|
|
415
485
|
} catch {
|
|
416
486
|
}
|
|
487
|
+
const detected = await detectProjectFacts(ctx.projectRoot);
|
|
488
|
+
const body = renderAgentsTemplate(detected);
|
|
489
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
490
|
+
await fs2.writeFile(file, body, "utf8");
|
|
491
|
+
if (detected.hints.length > 0) {
|
|
492
|
+
const msg = `Wrote ${file}
|
|
493
|
+
Pre-filled: ${detected.hints.join(", ")}. Edit the file to add anything else worth remembering.`;
|
|
494
|
+
opts.renderer.writeInfo(`Wrote ${file}`);
|
|
495
|
+
opts.renderer.writeInfo(`Pre-filled: ${detected.hints.join(", ")}. Edit the file to add anything else worth remembering.`);
|
|
496
|
+
return { message: msg };
|
|
497
|
+
} else {
|
|
498
|
+
const msg = `Wrote ${file}
|
|
499
|
+
No project type auto-detected. Edit the file to add build/test commands and conventions.`;
|
|
500
|
+
opts.renderer.writeInfo(`Wrote ${file}`);
|
|
501
|
+
return { message: msg };
|
|
502
|
+
}
|
|
417
503
|
}
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
return null;
|
|
504
|
+
};
|
|
421
505
|
}
|
|
422
|
-
function
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
506
|
+
async function detectProjectFacts(root) {
|
|
507
|
+
const facts = { hints: [] };
|
|
508
|
+
try {
|
|
509
|
+
const pkg = JSON.parse(await fs2.readFile(path2.join(root, "package.json"), "utf8"));
|
|
510
|
+
const scripts = pkg.scripts ?? {};
|
|
511
|
+
const pm = (pkg.packageManager ?? "npm").split("@")[0] ?? "npm";
|
|
512
|
+
if (scripts["build"]) facts.build = `${pm} run build`;
|
|
513
|
+
if (scripts["test"]) facts.test = `${pm} test`;
|
|
514
|
+
if (scripts["lint"]) facts.lint = `${pm} run lint`;
|
|
515
|
+
if (scripts["dev"] ?? scripts["start"]) facts.run = `${pm} run ${scripts["dev"] ? "dev" : "start"}`;
|
|
516
|
+
facts.hints.push("package.json scripts");
|
|
517
|
+
} catch {
|
|
432
518
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
}
|
|
519
|
+
try {
|
|
520
|
+
await fs2.access(path2.join(root, "pyproject.toml"));
|
|
521
|
+
facts.test ??= "pytest";
|
|
522
|
+
facts.lint ??= "ruff check .";
|
|
523
|
+
facts.hints.push("pyproject.toml");
|
|
524
|
+
} catch {
|
|
448
525
|
}
|
|
449
|
-
|
|
526
|
+
try {
|
|
527
|
+
await fs2.access(path2.join(root, "go.mod"));
|
|
528
|
+
facts.build ??= "go build ./...";
|
|
529
|
+
facts.test ??= "go test ./...";
|
|
530
|
+
facts.hints.push("go.mod");
|
|
531
|
+
} catch {
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
await fs2.access(path2.join(root, "Cargo.toml"));
|
|
535
|
+
facts.build ??= "cargo build";
|
|
536
|
+
facts.test ??= "cargo test";
|
|
537
|
+
facts.hints.push("Cargo.toml");
|
|
538
|
+
} catch {
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
await fs2.access(path2.join(root, "Makefile"));
|
|
542
|
+
facts.build ??= "make";
|
|
543
|
+
facts.test ??= "make test";
|
|
544
|
+
facts.hints.push("Makefile");
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
return facts;
|
|
450
548
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
549
|
+
function renderAgentsTemplate(f) {
|
|
550
|
+
const cmd = (s) => s ? `\`${s}\`` : "_TODO_";
|
|
551
|
+
return `# AGENTS.md
|
|
552
|
+
|
|
553
|
+
Project notes for WrongStack. Committed to the repo so every contributor
|
|
554
|
+
(human or agent) starts with the same context. Edit freely.
|
|
555
|
+
|
|
556
|
+
## What this project is
|
|
557
|
+
|
|
558
|
+
_One paragraph: what does this codebase do, who runs it, what's the
|
|
559
|
+
deployment target?_
|
|
560
|
+
|
|
561
|
+
## How to work on it
|
|
562
|
+
|
|
563
|
+
- **Build:** ${cmd(f.build)}
|
|
564
|
+
- **Test:** ${cmd(f.test)}
|
|
565
|
+
- **Lint:** ${cmd(f.lint)}
|
|
566
|
+
- **Run locally:** ${cmd(f.run)}
|
|
567
|
+
|
|
568
|
+
## Conventions
|
|
569
|
+
|
|
570
|
+
_What style choices matter here? Filenames, module layout, naming, error
|
|
571
|
+
handling, log format. Anything a stranger would get wrong._
|
|
572
|
+
|
|
573
|
+
## Domain knowledge
|
|
574
|
+
|
|
575
|
+
_Acronyms, business rules, foot-guns, "this looks weird but it's
|
|
576
|
+
intentional because\u2026"._
|
|
577
|
+
|
|
578
|
+
## Pointers
|
|
579
|
+
|
|
580
|
+
_Where to look for: routing, database migrations, feature flags,
|
|
581
|
+
on-call runbooks, dashboards._
|
|
582
|
+
`;
|
|
583
|
+
}
|
|
584
|
+
function diagCommand(opts) {
|
|
585
|
+
return {
|
|
586
|
+
name: "diag",
|
|
587
|
+
description: "Show runtime diagnostics (provider, tokens, tools, MCP).",
|
|
588
|
+
async run() {
|
|
589
|
+
if (opts.onDiag) {
|
|
590
|
+
opts.onDiag();
|
|
591
|
+
return { message: "diag" };
|
|
592
|
+
} else {
|
|
593
|
+
return { message: "Diag not available in this context." };
|
|
594
|
+
}
|
|
466
595
|
}
|
|
467
596
|
};
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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));
|
|
597
|
+
}
|
|
598
|
+
function statsCommand(opts) {
|
|
599
|
+
return {
|
|
600
|
+
name: "stats",
|
|
601
|
+
description: "Show session report: tokens, requests, tools, files, cost.",
|
|
602
|
+
async run() {
|
|
603
|
+
if (opts.onStats) {
|
|
604
|
+
opts.onStats();
|
|
605
|
+
return { message: "stats" };
|
|
606
|
+
} else {
|
|
607
|
+
return { message: "Stats not available in this context." };
|
|
491
608
|
}
|
|
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
609
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
);
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function helpCommand(opts) {
|
|
613
|
+
return {
|
|
614
|
+
name: "help",
|
|
615
|
+
description: "Show available slash commands.",
|
|
616
|
+
async run() {
|
|
617
|
+
const lines = ["Available slash commands:"];
|
|
618
|
+
for (const { cmd, owner, fullName } of opts.registry.listWithOwner()) {
|
|
619
|
+
const isBuiltin = owner === "core";
|
|
620
|
+
const prefix = isBuiltin ? "" : `${owner}:`;
|
|
621
|
+
const aliases = cmd.aliases ? cmd.aliases.map((a) => `/${prefix}${a}`).join(", ") : "";
|
|
622
|
+
const aliasStr = aliases ? ` (${aliases})` : "";
|
|
623
|
+
lines.push(` /${prefix}${cmd.name}${aliasStr} \u2014 ${cmd.description}`);
|
|
528
624
|
}
|
|
529
|
-
|
|
530
|
-
opts.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
531
|
-
} finally {
|
|
532
|
-
activeCtrl = void 0;
|
|
625
|
+
return { message: lines.join("\n") };
|
|
533
626
|
}
|
|
534
|
-
}
|
|
535
|
-
process.off("SIGINT", onSigint);
|
|
536
|
-
await opts.reader.close();
|
|
537
|
-
return 0;
|
|
627
|
+
};
|
|
538
628
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
629
|
+
function clearCommand(opts) {
|
|
630
|
+
return {
|
|
631
|
+
name: "clear",
|
|
632
|
+
description: "Reset the session and start a new one.",
|
|
633
|
+
async run(_args, ctx) {
|
|
634
|
+
if (ctx) {
|
|
635
|
+
ctx.messages = [];
|
|
636
|
+
ctx.todos = [];
|
|
637
|
+
ctx.readFiles.clear();
|
|
638
|
+
ctx.fileMtimes.clear();
|
|
639
|
+
ctx.meta = {};
|
|
640
|
+
}
|
|
641
|
+
await opts.memoryStore?.clear();
|
|
642
|
+
opts.onClear?.();
|
|
643
|
+
opts.renderer.clear();
|
|
644
|
+
const msg = "Session cleared (context, memory, and history reset).";
|
|
645
|
+
opts.renderer.writeInfo(msg);
|
|
646
|
+
return { message: msg };
|
|
549
647
|
}
|
|
550
|
-
|
|
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;
|
|
648
|
+
};
|
|
559
649
|
}
|
|
560
|
-
function
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
650
|
+
function contextCommand(opts) {
|
|
651
|
+
return {
|
|
652
|
+
name: "context",
|
|
653
|
+
aliases: ["ctx"],
|
|
654
|
+
description: "Show context window summary.",
|
|
655
|
+
async run(args, ctx) {
|
|
656
|
+
const messages = ctx.messages;
|
|
657
|
+
const detailed = args.trim() === "detail";
|
|
658
|
+
const pairCount = countTurnPairs(messages);
|
|
659
|
+
const estimatedTokens = estimateTokens(messages);
|
|
660
|
+
const toolUseCount = countToolUses(messages);
|
|
661
|
+
const toolResultCount = countToolResults(messages);
|
|
662
|
+
const lines = [
|
|
663
|
+
`${color.bold("Context Window")}`,
|
|
664
|
+
` messages: ${messages.length} total (${pairCount} user+assistant pairs)`,
|
|
665
|
+
` tokens (\u2248): ${estimatedTokens.toLocaleString()} (chars \xF7 4 estimate)`,
|
|
666
|
+
` system prompt: ${ctx.systemPrompt.length} block${ctx.systemPrompt.length !== 1 ? "s" : ""}`,
|
|
667
|
+
` tools: ${toolUseCount} calls made, ${toolResultCount} results in history`,
|
|
668
|
+
` read files: ${ctx.readFiles.size} files`,
|
|
669
|
+
` 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`
|
|
670
|
+
];
|
|
671
|
+
if (detailed) {
|
|
672
|
+
lines.push(` model: ${ctx.model}`);
|
|
673
|
+
lines.push(` cwd: ${ctx.cwd}`);
|
|
674
|
+
lines.push(` projectRoot: ${ctx.projectRoot}`);
|
|
675
|
+
lines.push(` file mtimes: ${ctx.fileMtimes.size} tracked`);
|
|
676
|
+
if (ctx.readFiles.size > 0) {
|
|
677
|
+
lines.push(` file list: ${[...ctx.readFiles].join(", ")}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const msg = lines.join("\n");
|
|
681
|
+
opts.renderer.write(`${msg}
|
|
587
682
|
`);
|
|
683
|
+
return { message: msg };
|
|
684
|
+
}
|
|
685
|
+
};
|
|
588
686
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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;
|
|
687
|
+
function countTurnPairs(messages) {
|
|
688
|
+
let count = 0;
|
|
689
|
+
for (const m of messages) {
|
|
690
|
+
if (m.role === "user" || m.role === "assistant") count++;
|
|
635
691
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
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)")}`);
|
|
692
|
+
return Math.floor(count / 2);
|
|
693
|
+
}
|
|
694
|
+
function countToolUses(messages) {
|
|
695
|
+
let count = 0;
|
|
696
|
+
for (const m of messages) {
|
|
697
|
+
const content = m.content;
|
|
698
|
+
if (Array.isArray(content)) {
|
|
699
|
+
count += content.filter((b) => b.type === "tool_use").length;
|
|
664
700
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
}
|
|
701
|
+
}
|
|
702
|
+
return count;
|
|
703
|
+
}
|
|
704
|
+
function countToolResults(messages) {
|
|
705
|
+
let count = 0;
|
|
706
|
+
for (const m of messages) {
|
|
707
|
+
const content = m.content;
|
|
708
|
+
if (Array.isArray(content)) {
|
|
709
|
+
count += content.filter((b) => b.type === "tool_result").length;
|
|
677
710
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
711
|
+
}
|
|
712
|
+
return count;
|
|
713
|
+
}
|
|
714
|
+
function estimateTokens(messages) {
|
|
715
|
+
let total = 0;
|
|
716
|
+
for (const m of messages) {
|
|
717
|
+
const content = m.content;
|
|
718
|
+
if (typeof content === "string") {
|
|
719
|
+
total += Math.ceil(content.length / 4);
|
|
720
|
+
} else if (Array.isArray(content)) {
|
|
721
|
+
for (const b of content) {
|
|
722
|
+
if (b.type === "text") total += Math.ceil(b.text.length / 4);
|
|
723
|
+
else if (b.type === "tool_use" || b.type === "tool_result") {
|
|
724
|
+
total += Math.ceil(JSON.stringify(b).length / 4);
|
|
725
|
+
}
|
|
690
726
|
}
|
|
691
727
|
}
|
|
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
728
|
}
|
|
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
|
-
];
|
|
729
|
+
return total;
|
|
730
730
|
}
|
|
731
|
-
function
|
|
731
|
+
function compactCommand(opts) {
|
|
732
732
|
return {
|
|
733
|
-
name: "
|
|
734
|
-
description: "
|
|
733
|
+
name: "compact",
|
|
734
|
+
description: "Compact the context window.",
|
|
735
735
|
async run(args, ctx) {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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 };
|
|
736
|
+
if (!opts.compactor) {
|
|
737
|
+
const msg2 = "No compactor configured.";
|
|
738
|
+
opts.renderer.writeWarning(msg2);
|
|
739
|
+
return { message: msg2 };
|
|
763
740
|
}
|
|
741
|
+
const aggressive = args.trim() === "aggressive";
|
|
742
|
+
const report = await opts.compactor.compact(ctx, { aggressive });
|
|
743
|
+
const msg = `Compaction: ${report.before} \u2192 ${report.after} tokens (${report.reductions.map((r) => `${r.phase}: ${r.saved}`).join(", ")})`;
|
|
744
|
+
opts.renderer.writeInfo(msg);
|
|
745
|
+
return { message: msg };
|
|
764
746
|
}
|
|
765
747
|
};
|
|
766
748
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}
|
|
780
|
-
|
|
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._
|
|
749
|
+
function usageCommand(opts) {
|
|
750
|
+
return {
|
|
751
|
+
name: "usage",
|
|
752
|
+
aliases: ["cost"],
|
|
753
|
+
description: "Show token usage and estimated cost.",
|
|
754
|
+
async run() {
|
|
755
|
+
const total = opts.tokenCounter.total();
|
|
756
|
+
const cost = opts.tokenCounter.estimateCost();
|
|
757
|
+
const msg = `${color.bold("Usage")}
|
|
758
|
+
input: ${total.input}
|
|
759
|
+
output: ${total.output}
|
|
760
|
+
cache read: ${total.cacheRead ?? 0}
|
|
761
|
+
cache write: ${total.cacheWrite ?? 0}
|
|
762
|
+
cost: $${cost.total.toFixed(4)} (input $${cost.input.toFixed(4)} / output $${cost.output.toFixed(4)})
|
|
843
763
|
`;
|
|
764
|
+
opts.renderer.write(msg);
|
|
765
|
+
return { message: msg };
|
|
766
|
+
}
|
|
767
|
+
};
|
|
844
768
|
}
|
|
845
|
-
function
|
|
769
|
+
function toolsCommand(opts) {
|
|
846
770
|
return {
|
|
847
|
-
name: "
|
|
848
|
-
description: "
|
|
771
|
+
name: "tools",
|
|
772
|
+
description: "List registered tools.",
|
|
849
773
|
async run() {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
return {
|
|
774
|
+
const all = opts.toolRegistry.listWithOwner();
|
|
775
|
+
const lines = all.map(({ tool, owner }) => {
|
|
776
|
+
return ` ${tool.name.padEnd(28)} ${color.dim(`[${owner}]`)} ${tool.mutating ? color.yellow("mut") : color.cyan("ro")} ${color.dim(tool.permission)}`;
|
|
777
|
+
});
|
|
778
|
+
const msg = `${color.bold("Tools")} (${all.length}):
|
|
779
|
+
${lines.join("\n")}
|
|
780
|
+
`;
|
|
781
|
+
opts.renderer.write(msg);
|
|
782
|
+
return { message: msg };
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
function skillCommand(opts) {
|
|
787
|
+
return {
|
|
788
|
+
name: "skill",
|
|
789
|
+
description: "Show a skill manifest or list skills.",
|
|
790
|
+
async run(args) {
|
|
791
|
+
if (!opts.skillLoader) {
|
|
792
|
+
const msg = "No skill loader configured.";
|
|
793
|
+
return { message: msg };
|
|
794
|
+
}
|
|
795
|
+
if (!args.trim()) {
|
|
796
|
+
const list = await opts.skillLoader.list();
|
|
797
|
+
if (list.length === 0) {
|
|
798
|
+
const msg2 = "No skills found.";
|
|
799
|
+
return { message: msg2 };
|
|
800
|
+
}
|
|
801
|
+
const lines = list.map((s) => ` ${s.name.padEnd(24)} ${color.dim(`[${s.source}]`)} ${s.description.split("\n")[0]}`);
|
|
802
|
+
const msg = `Skills:
|
|
803
|
+
${lines.join("\n")}
|
|
804
|
+
`;
|
|
805
|
+
return { message: msg };
|
|
853
806
|
} else {
|
|
854
|
-
|
|
807
|
+
const skill = await opts.skillLoader.find(args.trim());
|
|
808
|
+
if (!skill) {
|
|
809
|
+
const msg = `Skill "${args.trim()}" not found.`;
|
|
810
|
+
return { message: msg };
|
|
811
|
+
}
|
|
812
|
+
const body = await opts.skillLoader.readBody(skill.name);
|
|
813
|
+
return { message: body };
|
|
855
814
|
}
|
|
856
815
|
}
|
|
857
816
|
};
|
|
858
817
|
}
|
|
859
|
-
function
|
|
818
|
+
function useCommand(opts) {
|
|
860
819
|
return {
|
|
861
|
-
name: "
|
|
862
|
-
description: "
|
|
863
|
-
async run() {
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
return { message: "Stats not available in this context." };
|
|
820
|
+
name: "use",
|
|
821
|
+
description: "Switch provider mid-session: /use <provider>",
|
|
822
|
+
async run(args) {
|
|
823
|
+
const name = args.trim();
|
|
824
|
+
if (!name) {
|
|
825
|
+
const msg2 = "Usage: /use <provider-name>";
|
|
826
|
+
return { message: msg2 };
|
|
869
827
|
}
|
|
828
|
+
opts.onSwitchProvider?.(name);
|
|
829
|
+
const msg = `Switched provider to "${name}".`;
|
|
830
|
+
return { message: msg };
|
|
870
831
|
}
|
|
871
832
|
};
|
|
872
833
|
}
|
|
873
|
-
function
|
|
834
|
+
function modelCommand(opts) {
|
|
874
835
|
return {
|
|
875
|
-
name: "
|
|
876
|
-
description: "
|
|
877
|
-
async run() {
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
const
|
|
881
|
-
|
|
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}`);
|
|
836
|
+
name: "model",
|
|
837
|
+
description: "Switch model mid-session: /model <model>",
|
|
838
|
+
async run(args) {
|
|
839
|
+
const name = args.trim();
|
|
840
|
+
if (!name) {
|
|
841
|
+
const msg2 = "Usage: /model <model-name>";
|
|
842
|
+
return { message: msg2 };
|
|
885
843
|
}
|
|
886
|
-
|
|
844
|
+
opts.onSwitchModel?.(name);
|
|
845
|
+
const msg = `Switched model to "${name}".`;
|
|
846
|
+
return { message: msg };
|
|
887
847
|
}
|
|
888
848
|
};
|
|
889
849
|
}
|
|
890
|
-
function
|
|
850
|
+
function saveCommand(opts) {
|
|
891
851
|
return {
|
|
892
|
-
name: "
|
|
893
|
-
description: "
|
|
852
|
+
name: "save",
|
|
853
|
+
description: "Save current session (auto by default; this forces flush).",
|
|
894
854
|
async run(_args, ctx) {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
855
|
+
await ctx.session.append({
|
|
856
|
+
type: "session_end",
|
|
857
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
858
|
+
usage: opts.tokenCounter.total()
|
|
859
|
+
});
|
|
860
|
+
const msg = `Session ${ctx.session.id} flushed.`;
|
|
861
|
+
return { message: msg };
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function loadCommand(opts) {
|
|
866
|
+
return {
|
|
867
|
+
name: "resume",
|
|
868
|
+
aliases: ["load", "sessions"],
|
|
869
|
+
description: "List recent sessions. To actually resume, exit and run `wstack resume <id>`.",
|
|
870
|
+
async run() {
|
|
871
|
+
if (!opts.sessionStore) {
|
|
872
|
+
const msg2 = "No session store configured.";
|
|
873
|
+
return { message: msg2 };
|
|
901
874
|
}
|
|
902
|
-
await opts.
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
875
|
+
const list = await opts.sessionStore.list(10);
|
|
876
|
+
if (list.length === 0) {
|
|
877
|
+
const msg2 = "No saved sessions.";
|
|
878
|
+
return { message: msg2 };
|
|
879
|
+
}
|
|
880
|
+
const lines = list.map(
|
|
881
|
+
(s) => ` ${s.id} ${color.dim(s.startedAt)} ${color.dim(`${s.tokenTotal} tok`)} ${s.title}`
|
|
882
|
+
);
|
|
883
|
+
const msg = `Recent sessions:
|
|
884
|
+
${lines.join("\n")}
|
|
885
|
+
|
|
886
|
+
` + color.dim(`Resume one with: wstack resume ${list[0]?.id ?? "<id>"}
|
|
887
|
+
`);
|
|
888
|
+
opts.renderer.write(msg);
|
|
907
889
|
return { message: msg };
|
|
908
890
|
}
|
|
909
891
|
};
|
|
910
892
|
}
|
|
911
|
-
function
|
|
893
|
+
function exitCommand(opts) {
|
|
912
894
|
return {
|
|
913
|
-
name: "
|
|
914
|
-
aliases: ["
|
|
915
|
-
description: "
|
|
916
|
-
async run(
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
895
|
+
name: "exit",
|
|
896
|
+
aliases: ["quit", "q"],
|
|
897
|
+
description: "Exit the REPL.",
|
|
898
|
+
async run() {
|
|
899
|
+
opts.onExit?.();
|
|
900
|
+
return { exit: true };
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/pre-launch.ts
|
|
906
|
+
var MANIFESTS = [
|
|
907
|
+
"package.json",
|
|
908
|
+
"pyproject.toml",
|
|
909
|
+
"Cargo.toml",
|
|
910
|
+
"go.mod",
|
|
911
|
+
"Makefile",
|
|
912
|
+
"pom.xml",
|
|
913
|
+
"build.gradle",
|
|
914
|
+
"build.gradle.kts",
|
|
915
|
+
"composer.json",
|
|
916
|
+
"Gemfile"
|
|
917
|
+
];
|
|
918
|
+
async function detectProjectKind(projectRoot) {
|
|
919
|
+
try {
|
|
920
|
+
await fs2.access(path2.join(projectRoot, ".wrongstack", "AGENTS.md"));
|
|
921
|
+
return "initialized";
|
|
922
|
+
} catch {
|
|
923
|
+
}
|
|
924
|
+
for (const m of MANIFESTS) {
|
|
925
|
+
try {
|
|
926
|
+
await fs2.access(path2.join(projectRoot, m));
|
|
927
|
+
return "project";
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return "empty";
|
|
932
|
+
}
|
|
933
|
+
async function scaffoldAgentsMd(projectRoot) {
|
|
934
|
+
const dir = path2.join(projectRoot, ".wrongstack");
|
|
935
|
+
const file = path2.join(dir, "AGENTS.md");
|
|
936
|
+
const facts = await detectProjectFacts(projectRoot);
|
|
937
|
+
const body = renderAgentsTemplate(facts);
|
|
938
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
939
|
+
await fs2.writeFile(file, body, "utf8");
|
|
940
|
+
return file;
|
|
941
|
+
}
|
|
942
|
+
async function runProjectCheck(opts) {
|
|
943
|
+
const { projectRoot, renderer, reader } = opts;
|
|
944
|
+
const kind = await detectProjectKind(projectRoot);
|
|
945
|
+
if (kind === "initialized") {
|
|
946
|
+
renderer.write(
|
|
947
|
+
`
|
|
948
|
+
${color.green("\u2713")} Project initialized ${color.dim(`(${path2.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
|
|
949
|
+
`
|
|
950
|
+
);
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
953
|
+
if (kind === "project") {
|
|
954
|
+
renderer.write(
|
|
955
|
+
`
|
|
956
|
+
${color.amber("\u25CF")} Project detected ${color.dim(`(${projectRoot})`)} but ${color.bold(".wrongstack/AGENTS.md")} is missing.
|
|
957
|
+
`
|
|
958
|
+
);
|
|
959
|
+
const answer2 = (await reader.readLine(
|
|
960
|
+
` ${color.amber("?")} Scaffold ${color.bold("AGENTS.md")} now? ${color.dim("[y/N]")} `
|
|
961
|
+
)).trim().toLowerCase();
|
|
962
|
+
if (answer2 === "y" || answer2 === "yes") {
|
|
963
|
+
try {
|
|
964
|
+
const file = await scaffoldAgentsMd(projectRoot);
|
|
965
|
+
renderer.write(` ${color.green("\u2713")} Wrote ${color.dim(file)}
|
|
966
|
+
`);
|
|
967
|
+
} catch (err) {
|
|
968
|
+
renderer.writeError(
|
|
969
|
+
`Failed to scaffold AGENTS.md: ${err instanceof Error ? err.message : String(err)}`
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return true;
|
|
974
|
+
}
|
|
975
|
+
renderer.write(
|
|
976
|
+
`
|
|
977
|
+
${color.dim("\u25CB")} ${color.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
|
|
978
|
+
`
|
|
979
|
+
);
|
|
980
|
+
const answer = (await reader.readLine(
|
|
981
|
+
` ${color.amber("?")} Continue anyway? ${color.dim("[Y/n]")} `
|
|
982
|
+
)).trim().toLowerCase();
|
|
983
|
+
if (answer === "n" || answer === "no") {
|
|
984
|
+
renderer.write(color.dim(" Cancelled.\n"));
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
async function runLaunchPrompts(opts) {
|
|
990
|
+
const { renderer, reader, modePinned, yoloPinned } = opts;
|
|
991
|
+
let mode;
|
|
992
|
+
if (modePinned) {
|
|
993
|
+
mode = modePinned;
|
|
994
|
+
} else {
|
|
995
|
+
const answer = (await reader.readLine(
|
|
996
|
+
`
|
|
997
|
+
${color.amber("?")} Interactive mode: ${color.bold("T")}UI / ${color.bold("R")}EPL ${color.dim("[T/r]")} `
|
|
998
|
+
)).trim().toLowerCase();
|
|
999
|
+
mode = answer === "r" || answer === "repl" ? "repl" : "tui";
|
|
1000
|
+
}
|
|
1001
|
+
let yolo;
|
|
1002
|
+
if (yoloPinned !== void 0) {
|
|
1003
|
+
yolo = yoloPinned;
|
|
1004
|
+
} else {
|
|
1005
|
+
const answer = (await reader.readLine(
|
|
1006
|
+
` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve every tool call)")} ${color.dim("[y/N]")} `
|
|
1007
|
+
)).trim().toLowerCase();
|
|
1008
|
+
yolo = answer === "y" || answer === "yes";
|
|
1009
|
+
}
|
|
1010
|
+
renderer.write(
|
|
1011
|
+
`
|
|
1012
|
+
${color.green("\u25B6")} Launching in ${color.bold(mode.toUpperCase())} mode${yolo ? color.yellow(" (YOLO)") : ""}
|
|
1013
|
+
|
|
1014
|
+
`
|
|
1015
|
+
);
|
|
1016
|
+
return { mode, yolo };
|
|
1017
|
+
}
|
|
1018
|
+
var TerminalRenderer = class {
|
|
1019
|
+
out;
|
|
1020
|
+
err;
|
|
1021
|
+
lineStart = true;
|
|
1022
|
+
/**
|
|
1023
|
+
* When true, every stdout-bound method is a no-op. This is the only
|
|
1024
|
+
* safe state to be in while Ink owns the terminal (TUI mode):
|
|
1025
|
+
* raw writes to stdout interleave with Ink's cursor math and cause
|
|
1026
|
+
* the input + status bar to be reprinted as scrollback junk.
|
|
1027
|
+
* Stderr-bound methods (writeInfo/Warning/Error) still flow — they
|
|
1028
|
+
* go to a different stream Ink does not manage.
|
|
1029
|
+
*/
|
|
1030
|
+
silent = false;
|
|
1031
|
+
constructor(opts = {}) {
|
|
1032
|
+
this.out = opts.out ?? process.stdout;
|
|
1033
|
+
this.err = opts.err ?? process.stderr;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Toggle stdout suppression. Call `setSilent(true)` right before
|
|
1037
|
+
* handing the terminal to Ink, and `setSilent(false)` after Ink
|
|
1038
|
+
* exits. Idempotent.
|
|
1039
|
+
*/
|
|
1040
|
+
setSilent(silent) {
|
|
1041
|
+
this.silent = silent;
|
|
1042
|
+
}
|
|
1043
|
+
isSilent() {
|
|
1044
|
+
return this.silent;
|
|
1045
|
+
}
|
|
1046
|
+
write(input) {
|
|
1047
|
+
if (this.silent) return;
|
|
1048
|
+
const text = typeof input === "string" ? input : input.text;
|
|
1049
|
+
if (!text) return;
|
|
1050
|
+
const rendered = renderMarkdown(text);
|
|
1051
|
+
this.out.write(rendered);
|
|
1052
|
+
this.lineStart = rendered.endsWith("\n");
|
|
1053
|
+
}
|
|
1054
|
+
writeLine(text = "") {
|
|
1055
|
+
if (this.silent) return;
|
|
1056
|
+
if (!this.lineStart) this.out.write("\n");
|
|
1057
|
+
if (text) this.out.write(`${text}
|
|
1058
|
+
`);
|
|
1059
|
+
else this.out.write("\n");
|
|
1060
|
+
this.lineStart = true;
|
|
1061
|
+
}
|
|
1062
|
+
writeBlock(block) {
|
|
1063
|
+
if (this.silent) return;
|
|
1064
|
+
if (block.type === "text") {
|
|
1065
|
+
this.write(block);
|
|
1066
|
+
} else if (block.type === "tool_use") {
|
|
1067
|
+
this.writeToolCall(block.name, block.input);
|
|
1068
|
+
} else if (block.type === "tool_result") {
|
|
1069
|
+
const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
|
|
1070
|
+
this.writeToolResult("result", text, !!block.is_error);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
writeToolCall(name, input) {
|
|
1074
|
+
if (this.silent) return;
|
|
1075
|
+
if (!this.lineStart) this.out.write("\n");
|
|
1076
|
+
const arrow = theme.primary("\u2192");
|
|
1077
|
+
const display = formatInputSummary(input);
|
|
1078
|
+
this.out.write(`${arrow} ${theme.bold(name)}${display ? color.dim(` ${display}`) : ""}
|
|
1079
|
+
`);
|
|
1080
|
+
this.lineStart = true;
|
|
1081
|
+
}
|
|
1082
|
+
writeToolResult(name, content, isError) {
|
|
1083
|
+
if (this.silent) return;
|
|
1084
|
+
const txt = typeof content === "string" ? content : safeStringify(content);
|
|
1085
|
+
const prefix = isError ? theme.error("\u2718") : theme.success("\u2713");
|
|
1086
|
+
if (isError) {
|
|
1087
|
+
const firstLine = txt.split("\n")[0] ?? "";
|
|
1088
|
+
const truncated = firstLine.length > 200 ? `${firstLine.slice(0, 197)}\u2026` : firstLine;
|
|
1089
|
+
this.out.write(` ${prefix} ${color.dim(truncated)}
|
|
1090
|
+
`);
|
|
1091
|
+
this.lineStart = true;
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
const isEditLike = name === "edit" || name === "write";
|
|
1095
|
+
const isReadLike = name === "read" || name === "grep" || name === "glob" || name === "bash";
|
|
1096
|
+
const previewLines = isEditLike ? 0 : isReadLike ? 6 : 2;
|
|
1097
|
+
const diff = extractDiff(content);
|
|
1098
|
+
if (isEditLike && diff) {
|
|
1099
|
+
this.out.write(` ${prefix} ${color.dim(summarize(content, name))}
|
|
1100
|
+
`);
|
|
1101
|
+
const rendered = renderDiff(diff).split("\n").map((l) => ` ${l}`).join("\n");
|
|
1102
|
+
this.out.write(`${rendered}
|
|
1103
|
+
`);
|
|
1104
|
+
this.lineStart = true;
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const lines = txt.split("\n");
|
|
1108
|
+
const head = lines.slice(0, previewLines).map((l) => l.replace(/\s+$/, ""));
|
|
1109
|
+
const moreCount = Math.max(0, lines.length - head.length);
|
|
1110
|
+
this.out.write(` ${prefix} ${color.dim(summarize(content, name))}
|
|
1111
|
+
`);
|
|
1112
|
+
for (const l of head) {
|
|
1113
|
+
const capped = l.length > 200 ? `${l.slice(0, 197)}\u2026` : l;
|
|
1114
|
+
this.out.write(` ${color.dim(capped)}
|
|
1115
|
+
`);
|
|
1116
|
+
}
|
|
1117
|
+
if (moreCount > 0) {
|
|
1118
|
+
this.out.write(` ${color.dim(`+${moreCount} more line${moreCount === 1 ? "" : "s"}`)}
|
|
1119
|
+
`);
|
|
1120
|
+
}
|
|
1121
|
+
this.lineStart = true;
|
|
1122
|
+
}
|
|
1123
|
+
writeDiff(diff) {
|
|
1124
|
+
if (this.silent) return;
|
|
1125
|
+
if (!this.lineStart) this.out.write("\n");
|
|
1126
|
+
this.out.write(`${renderDiff(diff)}
|
|
1127
|
+
`);
|
|
1128
|
+
this.lineStart = true;
|
|
1129
|
+
}
|
|
1130
|
+
writeWarning(text) {
|
|
1131
|
+
this.err.write(`${theme.warn("\u26A0")} ${text}
|
|
1132
|
+
`);
|
|
1133
|
+
}
|
|
1134
|
+
writeError(text) {
|
|
1135
|
+
this.err.write(`${theme.error("\u2718")} ${text}
|
|
1136
|
+
`);
|
|
1137
|
+
}
|
|
1138
|
+
writeInfo(text) {
|
|
1139
|
+
this.err.write(`${theme.info("\u2139")} ${text}
|
|
1140
|
+
`);
|
|
1141
|
+
}
|
|
1142
|
+
clear() {
|
|
1143
|
+
if (this.silent) return;
|
|
1144
|
+
this.out.write("\x1B[2J\x1B[H");
|
|
1145
|
+
this.lineStart = true;
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
function renderMarkdown(s) {
|
|
1149
|
+
let out = s;
|
|
1150
|
+
out = out.replace(/^(#{1,6}) (.+)$/gm, (_m, hashes, text) => {
|
|
1151
|
+
return theme.primary(theme.bold(`${hashes} ${text}`));
|
|
1152
|
+
});
|
|
1153
|
+
out = out.replace(/```([a-zA-Z0-9_+-]*)\n([\s\S]*?)```/g, (_m, _lang, code) => {
|
|
1154
|
+
return color.gray(`
|
|
1155
|
+
\u250C\u2500
|
|
1156
|
+
${code.replace(/^/gm, "\u2502 ")}\u2514\u2500`);
|
|
1157
|
+
});
|
|
1158
|
+
out = out.replace(/`([^`\n]+)`/g, (_m, code) => theme.accent(code));
|
|
1159
|
+
out = out.replace(/\*\*([^*]+)\*\*/g, (_m, text) => theme.bold(text));
|
|
1160
|
+
out = out.replace(/(^|[^*])\*([^*\n]+)\*([^*]|$)/g, (_m, l, t, r) => `${l}${color.italic(t)}${r}`);
|
|
1161
|
+
return out;
|
|
1162
|
+
}
|
|
1163
|
+
function formatInputSummary(input) {
|
|
1164
|
+
if (!input || typeof input !== "object") return "";
|
|
1165
|
+
const obj = input;
|
|
1166
|
+
if (typeof obj["path"] === "string") return obj["path"];
|
|
1167
|
+
if (typeof obj["url"] === "string") return obj["url"];
|
|
1168
|
+
if (typeof obj["command"] === "string") {
|
|
1169
|
+
const cmd = obj["command"];
|
|
1170
|
+
return cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
1171
|
+
}
|
|
1172
|
+
if (typeof obj["pattern"] === "string") return obj["pattern"];
|
|
1173
|
+
return "";
|
|
1174
|
+
}
|
|
1175
|
+
function safeStringify(value) {
|
|
1176
|
+
if (typeof value === "string") return value;
|
|
1177
|
+
try {
|
|
1178
|
+
return JSON.stringify(value);
|
|
1179
|
+
} catch {
|
|
1180
|
+
return String(value);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function extractDiff(value) {
|
|
1184
|
+
if (typeof value === "object" && value !== null) {
|
|
1185
|
+
const d = value.diff;
|
|
1186
|
+
if (typeof d === "string" && d.length > 0) return d;
|
|
1187
|
+
}
|
|
1188
|
+
if (typeof value === "string") {
|
|
1189
|
+
const trimmed = value.trimStart();
|
|
1190
|
+
if (trimmed.startsWith("{")) {
|
|
1191
|
+
try {
|
|
1192
|
+
const parsed = JSON.parse(value);
|
|
1193
|
+
if (typeof parsed.diff === "string" && parsed.diff.length > 0) {
|
|
1194
|
+
return parsed.diff;
|
|
939
1195
|
}
|
|
1196
|
+
} catch {
|
|
940
1197
|
}
|
|
941
|
-
const msg = lines.join("\n");
|
|
942
|
-
opts.renderer.write(`${msg}
|
|
943
|
-
`);
|
|
944
|
-
return { message: msg };
|
|
945
1198
|
}
|
|
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++;
|
|
1199
|
+
if (/^---[^\n]*\n\+\+\+/m.test(value)) return value;
|
|
952
1200
|
}
|
|
953
|
-
return
|
|
1201
|
+
return null;
|
|
954
1202
|
}
|
|
955
|
-
function
|
|
956
|
-
let
|
|
957
|
-
|
|
958
|
-
const
|
|
959
|
-
if (
|
|
960
|
-
|
|
1203
|
+
function summarize(value, name) {
|
|
1204
|
+
let v = value;
|
|
1205
|
+
if (typeof value === "string") {
|
|
1206
|
+
const trimmed = value.trimStart();
|
|
1207
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
1208
|
+
try {
|
|
1209
|
+
v = JSON.parse(value);
|
|
1210
|
+
} catch {
|
|
1211
|
+
}
|
|
961
1212
|
}
|
|
962
1213
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1214
|
+
if (typeof v === "object" && v !== null) {
|
|
1215
|
+
const o = v;
|
|
1216
|
+
if (name === "edit") {
|
|
1217
|
+
const path6 = typeof o["path"] === "string" ? o["path"] : "";
|
|
1218
|
+
const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
|
|
1219
|
+
return `${path6} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
|
|
1220
|
+
}
|
|
1221
|
+
if (name === "write") {
|
|
1222
|
+
const path6 = typeof o["path"] === "string" ? o["path"] : "";
|
|
1223
|
+
const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
|
|
1224
|
+
return bytes !== void 0 ? `${path6} ${bytes}B` : path6;
|
|
1225
|
+
}
|
|
1226
|
+
if (typeof o["count"] === "number") {
|
|
1227
|
+
return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
|
|
971
1228
|
}
|
|
972
1229
|
}
|
|
973
|
-
return
|
|
1230
|
+
return "";
|
|
974
1231
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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
|
-
}
|
|
1232
|
+
var req = createRequire(import.meta.url);
|
|
1233
|
+
function readOwnVersion() {
|
|
1234
|
+
const candidates = ["../package.json", "../../package.json"];
|
|
1235
|
+
for (const rel of candidates) {
|
|
1236
|
+
try {
|
|
1237
|
+
const pkg = req(rel);
|
|
1238
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
|
|
1239
|
+
} catch {
|
|
988
1240
|
}
|
|
989
1241
|
}
|
|
990
|
-
return
|
|
1242
|
+
return "dev";
|
|
991
1243
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
-
};
|
|
1244
|
+
var CLI_VERSION = readOwnVersion();
|
|
1245
|
+
var API_VERSION = "0.0.0";
|
|
1246
|
+
try {
|
|
1247
|
+
const corePkg = req("@wrongstack/core/package.json");
|
|
1248
|
+
if (corePkg.wrongstackApiVersion) API_VERSION = corePkg.wrongstackApiVersion;
|
|
1249
|
+
} catch {
|
|
1009
1250
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1251
|
+
|
|
1252
|
+
// src/repl.ts
|
|
1253
|
+
async function runRepl(opts) {
|
|
1254
|
+
if (opts.banner !== false) printBanner(opts.renderer);
|
|
1255
|
+
let activeCtrl;
|
|
1256
|
+
let interrupts = 0;
|
|
1257
|
+
const onSigint = () => {
|
|
1258
|
+
interrupts++;
|
|
1259
|
+
if (interrupts >= 2) {
|
|
1260
|
+
opts.renderer.writeWarning("Exiting.");
|
|
1261
|
+
process.exit(130);
|
|
1262
|
+
}
|
|
1263
|
+
if (activeCtrl) {
|
|
1264
|
+
activeCtrl.abort();
|
|
1265
|
+
opts.renderer.writeWarning("Iteration cancelled. Press Ctrl+C again to exit.");
|
|
1266
|
+
} else {
|
|
1267
|
+
opts.renderer.writeWarning("Press Ctrl+C again to exit.");
|
|
1027
1268
|
}
|
|
1028
1269
|
};
|
|
1270
|
+
process.on("SIGINT", onSigint);
|
|
1271
|
+
const builder = new InputBuilder({ store: opts.attachments });
|
|
1272
|
+
for (; ; ) {
|
|
1273
|
+
let raw;
|
|
1274
|
+
try {
|
|
1275
|
+
raw = await readPossiblyMultiline(opts);
|
|
1276
|
+
} catch {
|
|
1277
|
+
break;
|
|
1278
|
+
}
|
|
1279
|
+
const trimmed = raw.trim();
|
|
1280
|
+
if (!trimmed) {
|
|
1281
|
+
interrupts = 0;
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
interrupts = 0;
|
|
1285
|
+
if (trimmed.startsWith("/")) {
|
|
1286
|
+
try {
|
|
1287
|
+
const res = await opts.slashRegistry.dispatch(trimmed, opts.agent.ctx);
|
|
1288
|
+
if (res?.message) opts.renderer.write(`${res.message}
|
|
1289
|
+
`);
|
|
1290
|
+
if (res?.exit) break;
|
|
1291
|
+
} catch (err) {
|
|
1292
|
+
opts.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
1293
|
+
}
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
const ph = await builder.appendPaste(raw);
|
|
1297
|
+
if (ph) {
|
|
1298
|
+
const lineCount = raw.split("\n").length;
|
|
1299
|
+
opts.renderer.write(color.dim(` \u21B3 ${ph} (${lineCount} lines)
|
|
1300
|
+
`));
|
|
1301
|
+
}
|
|
1302
|
+
const blocks = await builder.submit();
|
|
1303
|
+
const runCtrl = new AbortController();
|
|
1304
|
+
activeCtrl = runCtrl;
|
|
1305
|
+
try {
|
|
1306
|
+
const startedAt = Date.now();
|
|
1307
|
+
const before = opts.tokenCounter?.total();
|
|
1308
|
+
const costBefore = opts.tokenCounter?.estimateCost().total ?? 0;
|
|
1309
|
+
const result = await opts.agent.run(blocks, { signal: runCtrl.signal });
|
|
1310
|
+
if (result.status === "aborted") {
|
|
1311
|
+
opts.renderer.writeWarning("Aborted.");
|
|
1312
|
+
} else if (result.status === "failed") {
|
|
1313
|
+
opts.renderer.writeError(
|
|
1314
|
+
`Failed: ${result.error instanceof Error ? result.error.message : String(result.error)}`
|
|
1315
|
+
);
|
|
1316
|
+
} else if (result.status === "max_iterations") {
|
|
1317
|
+
opts.renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
|
|
1318
|
+
}
|
|
1319
|
+
if (opts.tokenCounter && before) {
|
|
1320
|
+
const after = opts.tokenCounter.total();
|
|
1321
|
+
const costAfter = opts.tokenCounter.estimateCost().total;
|
|
1322
|
+
const ctxChip = opts.effectiveMaxContext && opts.effectiveMaxContext > 0 ? ` ctx: ${renderContextChip(after.input, opts.effectiveMaxContext)}` : "";
|
|
1323
|
+
opts.renderer.write(
|
|
1324
|
+
`
|
|
1325
|
+
${color.dim(
|
|
1326
|
+
`[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}`
|
|
1327
|
+
)}
|
|
1328
|
+
`
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
} catch (err) {
|
|
1332
|
+
opts.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
1333
|
+
} finally {
|
|
1334
|
+
activeCtrl = void 0;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
process.off("SIGINT", onSigint);
|
|
1338
|
+
await opts.reader.close();
|
|
1339
|
+
return 0;
|
|
1029
1340
|
}
|
|
1030
|
-
function
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
${lines.join("\n")}
|
|
1041
|
-
`;
|
|
1042
|
-
opts.renderer.write(msg);
|
|
1043
|
-
return { message: msg };
|
|
1341
|
+
async function readPossiblyMultiline(opts) {
|
|
1342
|
+
const firstPrompt = theme.primary("\u203A ");
|
|
1343
|
+
const contPrompt = color.dim("\xB7 ");
|
|
1344
|
+
const first = await opts.reader.readLine(firstPrompt);
|
|
1345
|
+
if (first.trim() === '"""') {
|
|
1346
|
+
const parts = [];
|
|
1347
|
+
for (; ; ) {
|
|
1348
|
+
const next = await opts.reader.readLine(contPrompt);
|
|
1349
|
+
if (next.trim() === '"""') break;
|
|
1350
|
+
parts.push(next);
|
|
1044
1351
|
}
|
|
1045
|
-
|
|
1352
|
+
return parts.join("\n");
|
|
1353
|
+
}
|
|
1354
|
+
let buf = first;
|
|
1355
|
+
while (buf.endsWith("\\")) {
|
|
1356
|
+
buf = buf.slice(0, -1);
|
|
1357
|
+
const cont = await opts.reader.readLine(contPrompt);
|
|
1358
|
+
buf += "\n" + cont;
|
|
1359
|
+
}
|
|
1360
|
+
return buf;
|
|
1361
|
+
}
|
|
1362
|
+
function fmtTok(n) {
|
|
1363
|
+
if (n < 1e3) return String(n);
|
|
1364
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
1365
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
1366
|
+
}
|
|
1367
|
+
var FILLED = "\u2588";
|
|
1368
|
+
var EMPTY = "\u2591";
|
|
1369
|
+
function renderContextChip(used, max) {
|
|
1370
|
+
const ratio = Math.max(0, Math.min(1, used / max));
|
|
1371
|
+
const pct = Math.round(ratio * 100);
|
|
1372
|
+
const bar = renderProgress(ratio, 6);
|
|
1373
|
+
return `${bar} ${pct}% (${fmtTok(used)}/${fmtTok(max)})`;
|
|
1374
|
+
}
|
|
1375
|
+
function renderProgress(ratio, width) {
|
|
1376
|
+
const clamped = Math.max(0, Math.min(1, ratio));
|
|
1377
|
+
const filled = clamped === 0 ? 0 : Math.max(1, Math.round(clamped * width));
|
|
1378
|
+
const capped = Math.min(width, filled);
|
|
1379
|
+
return FILLED.repeat(capped) + EMPTY.repeat(width - capped);
|
|
1380
|
+
}
|
|
1381
|
+
function printBanner(renderer) {
|
|
1382
|
+
const lines = [
|
|
1383
|
+
theme.primary(theme.bold("WrongStack")) + color.dim(` v${CLI_VERSION}`),
|
|
1384
|
+
color.dim("Built on the wrong stack. Shipped anyway."),
|
|
1385
|
+
color.dim("Type /help for commands, /exit to quit."),
|
|
1386
|
+
""
|
|
1387
|
+
];
|
|
1388
|
+
renderer.write(`${lines.join("\n")}
|
|
1389
|
+
`);
|
|
1046
1390
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1391
|
+
var SessionStats = class {
|
|
1392
|
+
tokenCounter;
|
|
1393
|
+
startedAt = Date.now();
|
|
1394
|
+
apiRequests = 0;
|
|
1395
|
+
iterations = 0;
|
|
1396
|
+
errors = 0;
|
|
1397
|
+
toolStats = /* @__PURE__ */ new Map();
|
|
1398
|
+
readPaths = /* @__PURE__ */ new Set();
|
|
1399
|
+
editedPaths = /* @__PURE__ */ new Set();
|
|
1400
|
+
writtenPaths = /* @__PURE__ */ new Set();
|
|
1401
|
+
bytesWritten = 0;
|
|
1402
|
+
bashCommands = 0;
|
|
1403
|
+
fetches = 0;
|
|
1404
|
+
constructor(events, tokenCounter) {
|
|
1405
|
+
this.tokenCounter = tokenCounter;
|
|
1406
|
+
events.on("provider.response", () => {
|
|
1407
|
+
this.apiRequests++;
|
|
1408
|
+
});
|
|
1409
|
+
events.on("iteration.completed", () => {
|
|
1410
|
+
this.iterations++;
|
|
1411
|
+
});
|
|
1412
|
+
events.on("error", () => {
|
|
1413
|
+
this.errors++;
|
|
1414
|
+
});
|
|
1415
|
+
events.on("tool.executed", (e) => {
|
|
1416
|
+
const slot = this.toolStats.get(e.name) ?? { ok: 0, fail: 0, totalMs: 0 };
|
|
1417
|
+
if (e.ok) slot.ok++;
|
|
1418
|
+
else slot.fail++;
|
|
1419
|
+
slot.totalMs += e.durationMs;
|
|
1420
|
+
this.toolStats.set(e.name, slot);
|
|
1421
|
+
const input = e.input;
|
|
1422
|
+
if (e.name === "bash") this.bashCommands++;
|
|
1423
|
+
else if (e.name === "fetch") this.fetches++;
|
|
1424
|
+
if (!e.ok) return;
|
|
1425
|
+
const path6 = typeof input?.path === "string" ? input.path : void 0;
|
|
1426
|
+
if (e.name === "read" && path6) this.readPaths.add(path6);
|
|
1427
|
+
else if (e.name === "edit" && path6) this.editedPaths.add(path6);
|
|
1428
|
+
else if (e.name === "write" && path6) {
|
|
1429
|
+
this.writtenPaths.add(path6);
|
|
1430
|
+
const content = typeof input?.content === "string" ? input.content : "";
|
|
1431
|
+
this.bytesWritten += Buffer.byteLength(content, "utf8");
|
|
1075
1432
|
}
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
hasActivity() {
|
|
1436
|
+
return this.apiRequests > 0 || this.iterations > 0 || this.toolStats.size > 0 || this.tokenCounter.total().input > 0;
|
|
1437
|
+
}
|
|
1438
|
+
render(renderer) {
|
|
1439
|
+
if (!this.hasActivity()) return;
|
|
1440
|
+
const u = this.tokenCounter.total();
|
|
1441
|
+
const cost = this.tokenCounter.estimateCost();
|
|
1442
|
+
const elapsedSec = ((Date.now() - this.startedAt) / 1e3).toFixed(1);
|
|
1443
|
+
const lines = [];
|
|
1444
|
+
lines.push("");
|
|
1445
|
+
lines.push(color.bold("Session report"));
|
|
1446
|
+
lines.push(color.dim("\u2500".repeat(40)));
|
|
1447
|
+
lines.push(` Elapsed: ${elapsedSec}s`);
|
|
1448
|
+
lines.push(` Iterations: ${this.iterations}`);
|
|
1449
|
+
lines.push(` API requests: ${this.apiRequests}`);
|
|
1450
|
+
if (this.errors > 0) {
|
|
1451
|
+
lines.push(` Errors: ${color.yellow(String(this.errors))}`);
|
|
1076
1452
|
}
|
|
1077
|
-
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1453
|
+
lines.push("");
|
|
1454
|
+
lines.push(` Tokens: in ${fmtTok2(u.input)} out ${fmtTok2(u.output)}${u.cacheRead ? ` cacheR ${fmtTok2(u.cacheRead)}` : ""}${u.cacheWrite ? ` cacheW ${fmtTok2(u.cacheWrite)}` : ""}`);
|
|
1455
|
+
const cache = this.tokenCounter.cacheStats();
|
|
1456
|
+
if (cache.readTokens > 0 || cache.writeTokens > 0) {
|
|
1457
|
+
const pct = (cache.hitRatio * 100).toFixed(1);
|
|
1458
|
+
lines.push(
|
|
1459
|
+
` Prompt cache: ${pct}% hit ${color.dim(`(${fmtTok2(cache.readTokens)} read / ${fmtTok2(cache.writeTokens)} write)`)}`
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
if (cost.total > 0) {
|
|
1463
|
+
lines.push(` Cost: $${cost.total.toFixed(4)}${color.dim(` (in $${cost.input.toFixed(4)} / out $${cost.output.toFixed(4)})`)}`);
|
|
1464
|
+
} else {
|
|
1465
|
+
lines.push(` Cost: ${color.dim("$0 (no pricing on this plan)")}`);
|
|
1466
|
+
}
|
|
1467
|
+
if (this.toolStats.size > 0) {
|
|
1468
|
+
lines.push("");
|
|
1469
|
+
lines.push(` ${color.bold("Tool calls")}`);
|
|
1470
|
+
const sorted = [...this.toolStats.entries()].sort(
|
|
1471
|
+
(a, b) => b[1].ok + b[1].fail - (a[1].ok + a[1].fail)
|
|
1472
|
+
);
|
|
1473
|
+
for (const [name, s] of sorted) {
|
|
1474
|
+
const total = s.ok + s.fail;
|
|
1475
|
+
const failPart = s.fail > 0 ? color.yellow(` (${s.fail} failed)`) : "";
|
|
1476
|
+
const avgMs = total > 0 ? Math.round(s.totalMs / total) : 0;
|
|
1477
|
+
lines.push(` ${name.padEnd(12)} ${String(total).padStart(3)}\xD7 ${color.dim(`avg ${avgMs}ms`)}${failPart}`);
|
|
1088
1478
|
}
|
|
1089
|
-
opts.onSwitchProvider?.(name);
|
|
1090
|
-
const msg = `Switched provider to "${name}".`;
|
|
1091
|
-
return { message: msg };
|
|
1092
1479
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
if (
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1480
|
+
const fileActivity = this.readPaths.size > 0 || this.editedPaths.size > 0 || this.writtenPaths.size > 0 || this.bytesWritten > 0;
|
|
1481
|
+
if (fileActivity) {
|
|
1482
|
+
lines.push("");
|
|
1483
|
+
lines.push(` ${color.bold("Files")}`);
|
|
1484
|
+
if (this.readPaths.size > 0)
|
|
1485
|
+
lines.push(` read: ${this.readPaths.size} ${color.dim(samplePaths(this.readPaths))}`);
|
|
1486
|
+
if (this.editedPaths.size > 0)
|
|
1487
|
+
lines.push(` edited: ${this.editedPaths.size} ${color.dim(samplePaths(this.editedPaths))}`);
|
|
1488
|
+
if (this.writtenPaths.size > 0) {
|
|
1489
|
+
const bytes = this.bytesWritten;
|
|
1490
|
+
const byteStr = bytes > 1024 ? `${(bytes / 1024).toFixed(1)}KB` : `${bytes}B`;
|
|
1491
|
+
lines.push(` written: ${this.writtenPaths.size} (${byteStr}) ${color.dim(samplePaths(this.writtenPaths))}`);
|
|
1104
1492
|
}
|
|
1105
|
-
opts.onSwitchModel?.(name);
|
|
1106
|
-
const msg = `Switched model to "${name}".`;
|
|
1107
|
-
return { message: msg };
|
|
1108
1493
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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 };
|
|
1494
|
+
if (this.bashCommands > 0 || this.fetches > 0) {
|
|
1495
|
+
lines.push("");
|
|
1496
|
+
if (this.bashCommands > 0) lines.push(` Shell commands: ${this.bashCommands}`);
|
|
1497
|
+
if (this.fetches > 0) lines.push(` Web fetches: ${this.fetches}`);
|
|
1123
1498
|
}
|
|
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>"}
|
|
1499
|
+
lines.push("");
|
|
1500
|
+
renderer.write(`${lines.join("\n")}
|
|
1148
1501
|
`);
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
function fmtTok2(n) {
|
|
1505
|
+
if (n < 1e3) return String(n);
|
|
1506
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
1507
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
1153
1508
|
}
|
|
1154
|
-
function
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
description: "Exit the REPL.",
|
|
1159
|
-
async run() {
|
|
1160
|
-
opts.onExit?.();
|
|
1161
|
-
return { exit: true };
|
|
1162
|
-
}
|
|
1163
|
-
};
|
|
1509
|
+
function samplePaths(set) {
|
|
1510
|
+
const arr = [...set];
|
|
1511
|
+
if (arr.length <= 2) return arr.join(", ");
|
|
1512
|
+
return `${arr[0]}, \u2026 (+${arr.length - 1} more)`;
|
|
1164
1513
|
}
|
|
1165
1514
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1166
1515
|
var FILLED2 = "\u2588";
|
|
@@ -1606,7 +1955,7 @@ async function diagCmd(_args, deps) {
|
|
|
1606
1955
|
const age = await deps.modelsRegistry.ageSeconds();
|
|
1607
1956
|
const lines = [
|
|
1608
1957
|
color.bold("WrongStack diagnostics"),
|
|
1609
|
-
` apiVersion:
|
|
1958
|
+
` apiVersion: ${API_VERSION}`,
|
|
1610
1959
|
` cwd: ${deps.cwd}`,
|
|
1611
1960
|
` projectRoot: ${deps.projectRoot}`,
|
|
1612
1961
|
` projectHash: ${deps.paths.projectHash}`,
|
|
@@ -1636,7 +1985,7 @@ async function usageCmd(_args, deps) {
|
|
|
1636
1985
|
}
|
|
1637
1986
|
async function versionCmd(_args, deps) {
|
|
1638
1987
|
deps.renderer.write(
|
|
1639
|
-
`WrongStack
|
|
1988
|
+
`WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${os2.platform()})
|
|
1640
1989
|
`
|
|
1641
1990
|
);
|
|
1642
1991
|
return 0;
|
|
@@ -1793,26 +2142,13 @@ function flagsToConfigPatch(flags) {
|
|
|
1793
2142
|
}
|
|
1794
2143
|
function resolveBundledSkillsDir() {
|
|
1795
2144
|
try {
|
|
1796
|
-
const
|
|
1797
|
-
const corePkg =
|
|
2145
|
+
const req2 = createRequire(import.meta.url);
|
|
2146
|
+
const corePkg = req2.resolve("@wrongstack/core/package.json");
|
|
1798
2147
|
return path2.join(path2.dirname(corePkg), "skills");
|
|
1799
2148
|
} catch {
|
|
1800
2149
|
return void 0;
|
|
1801
2150
|
}
|
|
1802
2151
|
}
|
|
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
2152
|
async function ensureProjectMeta(paths, projectRoot) {
|
|
1817
2153
|
try {
|
|
1818
2154
|
await fs2.mkdir(paths.projectDir, { recursive: true });
|
|
@@ -1893,12 +2229,73 @@ async function main(argv) {
|
|
|
1893
2229
|
await reader.close();
|
|
1894
2230
|
return code2;
|
|
1895
2231
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
);
|
|
1900
|
-
|
|
1901
|
-
|
|
2232
|
+
const isSingleShot = positional.length > 0 || typeof flags["prompt"] === "string";
|
|
2233
|
+
const isInteractiveTTY = !!process.stdin.isTTY && !isSingleShot;
|
|
2234
|
+
if (isInteractiveTTY) {
|
|
2235
|
+
const cont = await runProjectCheck({ projectRoot, renderer, reader });
|
|
2236
|
+
if (!cont) {
|
|
2237
|
+
await reader.close();
|
|
2238
|
+
return 0;
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
const providerFlag = typeof flags["provider"] === "string" ? flags["provider"] : void 0;
|
|
2242
|
+
const modelFlag = typeof flags["model"] === "string" ? flags["model"] : void 0;
|
|
2243
|
+
const bothFlagsPinned = !!providerFlag && !!modelFlag;
|
|
2244
|
+
if (!bothFlagsPinned) {
|
|
2245
|
+
if (process.stdin.isTTY) {
|
|
2246
|
+
const picked = await runPicker({
|
|
2247
|
+
modelsRegistry,
|
|
2248
|
+
renderer,
|
|
2249
|
+
reader,
|
|
2250
|
+
config,
|
|
2251
|
+
defaultProvider: providerFlag ?? config.provider,
|
|
2252
|
+
defaultModel: modelFlag ?? config.model
|
|
2253
|
+
});
|
|
2254
|
+
if (!picked) {
|
|
2255
|
+
if (!config.provider || !config.model) {
|
|
2256
|
+
await reader.close();
|
|
2257
|
+
return 2;
|
|
2258
|
+
}
|
|
2259
|
+
} else {
|
|
2260
|
+
const prevProvider = config.provider;
|
|
2261
|
+
const prevModel = config.model;
|
|
2262
|
+
config = Object.freeze({ ...config, provider: picked.provider, model: picked.model });
|
|
2263
|
+
if (picked.provider !== prevProvider || picked.model !== prevModel) {
|
|
2264
|
+
const saved = await saveToGlobalConfig(
|
|
2265
|
+
wpaths.globalConfig,
|
|
2266
|
+
picked.provider,
|
|
2267
|
+
picked.model
|
|
2268
|
+
);
|
|
2269
|
+
if (saved) {
|
|
2270
|
+
renderer.writeInfo(`Saved ${picked.provider}/${picked.model} as default.
|
|
2271
|
+
`);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
} else if (!config.provider || !config.model) {
|
|
2276
|
+
process.stderr.write(
|
|
2277
|
+
"No provider or model configured. Run `wrongstack init` first, or pass --provider <id> --model <id>.\n"
|
|
2278
|
+
);
|
|
2279
|
+
await reader.close();
|
|
2280
|
+
return 2;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
if (isInteractiveTTY) {
|
|
2284
|
+
let modePinned;
|
|
2285
|
+
if (flags["no-tui"]) modePinned = "repl";
|
|
2286
|
+
else if (flags["tui"]) modePinned = "tui";
|
|
2287
|
+
const yoloPinned = flags["yolo"] === true ? true : void 0;
|
|
2288
|
+
const choices = await runLaunchPrompts({ renderer, reader, modePinned, yoloPinned });
|
|
2289
|
+
if (choices.mode === "tui") {
|
|
2290
|
+
flags["tui"] = true;
|
|
2291
|
+
flags["no-tui"] = false;
|
|
2292
|
+
} else {
|
|
2293
|
+
flags["tui"] = false;
|
|
2294
|
+
flags["no-tui"] = true;
|
|
2295
|
+
}
|
|
2296
|
+
if (choices.yolo !== config.yolo) {
|
|
2297
|
+
config = Object.freeze({ ...config, yolo: choices.yolo });
|
|
2298
|
+
}
|
|
1902
2299
|
}
|
|
1903
2300
|
const resolvedProvider = await modelsRegistry.getProvider(config.provider).catch(() => void 0);
|
|
1904
2301
|
if (!resolvedProvider) {
|
|
@@ -2265,7 +2662,7 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2265
2662
|
};
|
|
2266
2663
|
const newProvider = providerRegistry.create({ ...newCfg, type: name });
|
|
2267
2664
|
context.provider = newProvider;
|
|
2268
|
-
config.provider
|
|
2665
|
+
config = Object.freeze({ ...config, provider: name });
|
|
2269
2666
|
} catch (err) {
|
|
2270
2667
|
renderer.writeError(
|
|
2271
2668
|
`Cannot switch to "${name}": ${err instanceof Error ? err.message : err}`
|
|
@@ -2459,6 +2856,6 @@ if (isMain) {
|
|
|
2459
2856
|
);
|
|
2460
2857
|
}
|
|
2461
2858
|
|
|
2462
|
-
export { main };
|
|
2859
|
+
export { CLI_VERSION, main };
|
|
2463
2860
|
//# sourceMappingURL=index.js.map
|
|
2464
2861
|
//# sourceMappingURL=index.js.map
|