@wrongstack/cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1219 -830
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -234,933 +234,1261 @@ 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
|
-
function
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1232
|
+
async function runRepl(opts) {
|
|
1233
|
+
if (opts.banner !== false) printBanner(opts.renderer);
|
|
1234
|
+
let activeCtrl;
|
|
1235
|
+
let interrupts = 0;
|
|
1236
|
+
const onSigint = () => {
|
|
1237
|
+
interrupts++;
|
|
1238
|
+
if (interrupts >= 2) {
|
|
1239
|
+
opts.renderer.writeWarning("Exiting.");
|
|
1240
|
+
process.exit(130);
|
|
1241
|
+
}
|
|
1242
|
+
if (activeCtrl) {
|
|
1243
|
+
activeCtrl.abort();
|
|
1244
|
+
opts.renderer.writeWarning("Iteration cancelled. Press Ctrl+C again to exit.");
|
|
1245
|
+
} else {
|
|
1246
|
+
opts.renderer.writeWarning("Press Ctrl+C again to exit.");
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
process.on("SIGINT", onSigint);
|
|
1250
|
+
const builder = new InputBuilder({ store: opts.attachments });
|
|
1251
|
+
for (; ; ) {
|
|
1252
|
+
let raw;
|
|
1253
|
+
try {
|
|
1254
|
+
raw = await readPossiblyMultiline(opts);
|
|
1255
|
+
} catch {
|
|
1256
|
+
break;
|
|
1257
|
+
}
|
|
1258
|
+
const trimmed = raw.trim();
|
|
1259
|
+
if (!trimmed) {
|
|
1260
|
+
interrupts = 0;
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
interrupts = 0;
|
|
1264
|
+
if (trimmed.startsWith("/")) {
|
|
1265
|
+
try {
|
|
1266
|
+
const res = await opts.slashRegistry.dispatch(trimmed, opts.agent.ctx);
|
|
1267
|
+
if (res?.message) opts.renderer.write(`${res.message}
|
|
1268
|
+
`);
|
|
1269
|
+
if (res?.exit) break;
|
|
1270
|
+
} catch (err) {
|
|
1271
|
+
opts.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
1272
|
+
}
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
const ph = await builder.appendPaste(raw);
|
|
1276
|
+
if (ph) {
|
|
1277
|
+
const lineCount = raw.split("\n").length;
|
|
1278
|
+
opts.renderer.write(color.dim(` \u21B3 ${ph} (${lineCount} lines)
|
|
1279
|
+
`));
|
|
1280
|
+
}
|
|
1281
|
+
const blocks = await builder.submit();
|
|
1282
|
+
const runCtrl = new AbortController();
|
|
1283
|
+
activeCtrl = runCtrl;
|
|
1284
|
+
try {
|
|
1285
|
+
const startedAt = Date.now();
|
|
1286
|
+
const before = opts.tokenCounter?.total();
|
|
1287
|
+
const costBefore = opts.tokenCounter?.estimateCost().total ?? 0;
|
|
1288
|
+
const result = await opts.agent.run(blocks, { signal: runCtrl.signal });
|
|
1289
|
+
if (result.status === "aborted") {
|
|
1290
|
+
opts.renderer.writeWarning("Aborted.");
|
|
1291
|
+
} else if (result.status === "failed") {
|
|
1292
|
+
opts.renderer.writeError(
|
|
1293
|
+
`Failed: ${result.error instanceof Error ? result.error.message : String(result.error)}`
|
|
1294
|
+
);
|
|
1295
|
+
} else if (result.status === "max_iterations") {
|
|
1296
|
+
opts.renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
|
|
1297
|
+
}
|
|
1298
|
+
if (opts.tokenCounter && before) {
|
|
1299
|
+
const after = opts.tokenCounter.total();
|
|
1300
|
+
const costAfter = opts.tokenCounter.estimateCost().total;
|
|
1301
|
+
const ctxChip = opts.effectiveMaxContext && opts.effectiveMaxContext > 0 ? ` ctx: ${renderContextChip(after.input, opts.effectiveMaxContext)}` : "";
|
|
1302
|
+
opts.renderer.write(
|
|
1303
|
+
`
|
|
1304
|
+
${color.dim(
|
|
1305
|
+
`[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}`
|
|
1306
|
+
)}
|
|
1307
|
+
`
|
|
1308
|
+
);
|
|
987
1309
|
}
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
opts.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
1312
|
+
} finally {
|
|
1313
|
+
activeCtrl = void 0;
|
|
988
1314
|
}
|
|
989
1315
|
}
|
|
990
|
-
|
|
1316
|
+
process.off("SIGINT", onSigint);
|
|
1317
|
+
await opts.reader.close();
|
|
1318
|
+
return 0;
|
|
991
1319
|
}
|
|
992
|
-
function
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1320
|
+
async function readPossiblyMultiline(opts) {
|
|
1321
|
+
const firstPrompt = theme.primary("\u203A ");
|
|
1322
|
+
const contPrompt = color.dim("\xB7 ");
|
|
1323
|
+
const first = await opts.reader.readLine(firstPrompt);
|
|
1324
|
+
if (first.trim() === '"""') {
|
|
1325
|
+
const parts = [];
|
|
1326
|
+
for (; ; ) {
|
|
1327
|
+
const next = await opts.reader.readLine(contPrompt);
|
|
1328
|
+
if (next.trim() === '"""') break;
|
|
1329
|
+
parts.push(next);
|
|
1330
|
+
}
|
|
1331
|
+
return parts.join("\n");
|
|
1332
|
+
}
|
|
1333
|
+
let buf = first;
|
|
1334
|
+
while (buf.endsWith("\\")) {
|
|
1335
|
+
buf = buf.slice(0, -1);
|
|
1336
|
+
const cont = await opts.reader.readLine(contPrompt);
|
|
1337
|
+
buf += "\n" + cont;
|
|
1338
|
+
}
|
|
1339
|
+
return buf;
|
|
1340
|
+
}
|
|
1341
|
+
function fmtTok(n) {
|
|
1342
|
+
if (n < 1e3) return String(n);
|
|
1343
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
1344
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
1345
|
+
}
|
|
1346
|
+
var FILLED = "\u2588";
|
|
1347
|
+
var EMPTY = "\u2591";
|
|
1348
|
+
function renderContextChip(used, max) {
|
|
1349
|
+
const ratio = Math.max(0, Math.min(1, used / max));
|
|
1350
|
+
const pct = Math.round(ratio * 100);
|
|
1351
|
+
const bar = renderProgress(ratio, 6);
|
|
1352
|
+
return `${bar} ${pct}% (${fmtTok(used)}/${fmtTok(max)})`;
|
|
1353
|
+
}
|
|
1354
|
+
function renderProgress(ratio, width) {
|
|
1355
|
+
const clamped = Math.max(0, Math.min(1, ratio));
|
|
1356
|
+
const filled = clamped === 0 ? 0 : Math.max(1, Math.round(clamped * width));
|
|
1357
|
+
const capped = Math.min(width, filled);
|
|
1358
|
+
return FILLED.repeat(capped) + EMPTY.repeat(width - capped);
|
|
1359
|
+
}
|
|
1360
|
+
function printBanner(renderer) {
|
|
1361
|
+
const lines = [
|
|
1362
|
+
theme.primary(theme.bold("WrongStack")) + color.dim(" v0.0.1"),
|
|
1363
|
+
color.dim("Built on the wrong stack. Shipped anyway."),
|
|
1364
|
+
color.dim("Type /help for commands, /exit to quit."),
|
|
1365
|
+
""
|
|
1366
|
+
];
|
|
1367
|
+
renderer.write(`${lines.join("\n")}
|
|
1368
|
+
`);
|
|
1369
|
+
}
|
|
1370
|
+
var SessionStats = class {
|
|
1371
|
+
tokenCounter;
|
|
1372
|
+
startedAt = Date.now();
|
|
1373
|
+
apiRequests = 0;
|
|
1374
|
+
iterations = 0;
|
|
1375
|
+
errors = 0;
|
|
1376
|
+
toolStats = /* @__PURE__ */ new Map();
|
|
1377
|
+
readPaths = /* @__PURE__ */ new Set();
|
|
1378
|
+
editedPaths = /* @__PURE__ */ new Set();
|
|
1379
|
+
writtenPaths = /* @__PURE__ */ new Set();
|
|
1380
|
+
bytesWritten = 0;
|
|
1381
|
+
bashCommands = 0;
|
|
1382
|
+
fetches = 0;
|
|
1383
|
+
constructor(events, tokenCounter) {
|
|
1384
|
+
this.tokenCounter = tokenCounter;
|
|
1385
|
+
events.on("provider.response", () => {
|
|
1386
|
+
this.apiRequests++;
|
|
1387
|
+
});
|
|
1388
|
+
events.on("iteration.completed", () => {
|
|
1389
|
+
this.iterations++;
|
|
1390
|
+
});
|
|
1391
|
+
events.on("error", () => {
|
|
1392
|
+
this.errors++;
|
|
1393
|
+
});
|
|
1394
|
+
events.on("tool.executed", (e) => {
|
|
1395
|
+
const slot = this.toolStats.get(e.name) ?? { ok: 0, fail: 0, totalMs: 0 };
|
|
1396
|
+
if (e.ok) slot.ok++;
|
|
1397
|
+
else slot.fail++;
|
|
1398
|
+
slot.totalMs += e.durationMs;
|
|
1399
|
+
this.toolStats.set(e.name, slot);
|
|
1400
|
+
const input = e.input;
|
|
1401
|
+
if (e.name === "bash") this.bashCommands++;
|
|
1402
|
+
else if (e.name === "fetch") this.fetches++;
|
|
1403
|
+
if (!e.ok) return;
|
|
1404
|
+
const path6 = typeof input?.path === "string" ? input.path : void 0;
|
|
1405
|
+
if (e.name === "read" && path6) this.readPaths.add(path6);
|
|
1406
|
+
else if (e.name === "edit" && path6) this.editedPaths.add(path6);
|
|
1407
|
+
else if (e.name === "write" && path6) {
|
|
1408
|
+
this.writtenPaths.add(path6);
|
|
1409
|
+
const content = typeof input?.content === "string" ? input.content : "";
|
|
1410
|
+
this.bytesWritten += Buffer.byteLength(content, "utf8");
|
|
1001
1411
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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 };
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
hasActivity() {
|
|
1415
|
+
return this.apiRequests > 0 || this.iterations > 0 || this.toolStats.size > 0 || this.tokenCounter.total().input > 0;
|
|
1416
|
+
}
|
|
1417
|
+
render(renderer) {
|
|
1418
|
+
if (!this.hasActivity()) return;
|
|
1419
|
+
const u = this.tokenCounter.total();
|
|
1420
|
+
const cost = this.tokenCounter.estimateCost();
|
|
1421
|
+
const elapsedSec = ((Date.now() - this.startedAt) / 1e3).toFixed(1);
|
|
1422
|
+
const lines = [];
|
|
1423
|
+
lines.push("");
|
|
1424
|
+
lines.push(color.bold("Session report"));
|
|
1425
|
+
lines.push(color.dim("\u2500".repeat(40)));
|
|
1426
|
+
lines.push(` Elapsed: ${elapsedSec}s`);
|
|
1427
|
+
lines.push(` Iterations: ${this.iterations}`);
|
|
1428
|
+
lines.push(` API requests: ${this.apiRequests}`);
|
|
1429
|
+
if (this.errors > 0) {
|
|
1430
|
+
lines.push(` Errors: ${color.yellow(String(this.errors))}`);
|
|
1027
1431
|
}
|
|
1028
|
-
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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 };
|
|
1432
|
+
lines.push("");
|
|
1433
|
+
lines.push(` Tokens: in ${fmtTok2(u.input)} out ${fmtTok2(u.output)}${u.cacheRead ? ` cacheR ${fmtTok2(u.cacheRead)}` : ""}${u.cacheWrite ? ` cacheW ${fmtTok2(u.cacheWrite)}` : ""}`);
|
|
1434
|
+
const cache = this.tokenCounter.cacheStats();
|
|
1435
|
+
if (cache.readTokens > 0 || cache.writeTokens > 0) {
|
|
1436
|
+
const pct = (cache.hitRatio * 100).toFixed(1);
|
|
1437
|
+
lines.push(
|
|
1438
|
+
` Prompt cache: ${pct}% hit ${color.dim(`(${fmtTok2(cache.readTokens)} read / ${fmtTok2(cache.writeTokens)} write)`)}`
|
|
1439
|
+
);
|
|
1044
1440
|
}
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
-
}
|
|
1441
|
+
if (cost.total > 0) {
|
|
1442
|
+
lines.push(` Cost: $${cost.total.toFixed(4)}${color.dim(` (in $${cost.input.toFixed(4)} / out $${cost.output.toFixed(4)})`)}`);
|
|
1443
|
+
} else {
|
|
1444
|
+
lines.push(` Cost: ${color.dim("$0 (no pricing on this plan)")}`);
|
|
1076
1445
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1446
|
+
if (this.toolStats.size > 0) {
|
|
1447
|
+
lines.push("");
|
|
1448
|
+
lines.push(` ${color.bold("Tool calls")}`);
|
|
1449
|
+
const sorted = [...this.toolStats.entries()].sort(
|
|
1450
|
+
(a, b) => b[1].ok + b[1].fail - (a[1].ok + a[1].fail)
|
|
1451
|
+
);
|
|
1452
|
+
for (const [name, s] of sorted) {
|
|
1453
|
+
const total = s.ok + s.fail;
|
|
1454
|
+
const failPart = s.fail > 0 ? color.yellow(` (${s.fail} failed)`) : "";
|
|
1455
|
+
const avgMs = total > 0 ? Math.round(s.totalMs / total) : 0;
|
|
1456
|
+
lines.push(` ${name.padEnd(12)} ${String(total).padStart(3)}\xD7 ${color.dim(`avg ${avgMs}ms`)}${failPart}`);
|
|
1088
1457
|
}
|
|
1089
|
-
opts.onSwitchProvider?.(name);
|
|
1090
|
-
const msg = `Switched provider to "${name}".`;
|
|
1091
|
-
return { message: msg };
|
|
1092
1458
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
if (
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1459
|
+
const fileActivity = this.readPaths.size > 0 || this.editedPaths.size > 0 || this.writtenPaths.size > 0 || this.bytesWritten > 0;
|
|
1460
|
+
if (fileActivity) {
|
|
1461
|
+
lines.push("");
|
|
1462
|
+
lines.push(` ${color.bold("Files")}`);
|
|
1463
|
+
if (this.readPaths.size > 0)
|
|
1464
|
+
lines.push(` read: ${this.readPaths.size} ${color.dim(samplePaths(this.readPaths))}`);
|
|
1465
|
+
if (this.editedPaths.size > 0)
|
|
1466
|
+
lines.push(` edited: ${this.editedPaths.size} ${color.dim(samplePaths(this.editedPaths))}`);
|
|
1467
|
+
if (this.writtenPaths.size > 0) {
|
|
1468
|
+
const bytes = this.bytesWritten;
|
|
1469
|
+
const byteStr = bytes > 1024 ? `${(bytes / 1024).toFixed(1)}KB` : `${bytes}B`;
|
|
1470
|
+
lines.push(` written: ${this.writtenPaths.size} (${byteStr}) ${color.dim(samplePaths(this.writtenPaths))}`);
|
|
1104
1471
|
}
|
|
1105
|
-
opts.onSwitchModel?.(name);
|
|
1106
|
-
const msg = `Switched model to "${name}".`;
|
|
1107
|
-
return { message: msg };
|
|
1108
1472
|
}
|
|
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 };
|
|
1473
|
+
if (this.bashCommands > 0 || this.fetches > 0) {
|
|
1474
|
+
lines.push("");
|
|
1475
|
+
if (this.bashCommands > 0) lines.push(` Shell commands: ${this.bashCommands}`);
|
|
1476
|
+
if (this.fetches > 0) lines.push(` Web fetches: ${this.fetches}`);
|
|
1123
1477
|
}
|
|
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>"}
|
|
1478
|
+
lines.push("");
|
|
1479
|
+
renderer.write(`${lines.join("\n")}
|
|
1148
1480
|
`);
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
function fmtTok2(n) {
|
|
1484
|
+
if (n < 1e3) return String(n);
|
|
1485
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
1486
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
1153
1487
|
}
|
|
1154
|
-
function
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
description: "Exit the REPL.",
|
|
1159
|
-
async run() {
|
|
1160
|
-
opts.onExit?.();
|
|
1161
|
-
return { exit: true };
|
|
1162
|
-
}
|
|
1163
|
-
};
|
|
1488
|
+
function samplePaths(set) {
|
|
1489
|
+
const arr = [...set];
|
|
1490
|
+
if (arr.length <= 2) return arr.join(", ");
|
|
1491
|
+
return `${arr[0]}, \u2026 (+${arr.length - 1} more)`;
|
|
1164
1492
|
}
|
|
1165
1493
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1166
1494
|
var FILLED2 = "\u2588";
|
|
@@ -1893,12 +2221,73 @@ async function main(argv) {
|
|
|
1893
2221
|
await reader.close();
|
|
1894
2222
|
return code2;
|
|
1895
2223
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
);
|
|
1900
|
-
|
|
1901
|
-
|
|
2224
|
+
const isSingleShot = positional.length > 0 || typeof flags["prompt"] === "string";
|
|
2225
|
+
const isInteractiveTTY = !!process.stdin.isTTY && !isSingleShot;
|
|
2226
|
+
if (isInteractiveTTY) {
|
|
2227
|
+
const cont = await runProjectCheck({ projectRoot, renderer, reader });
|
|
2228
|
+
if (!cont) {
|
|
2229
|
+
await reader.close();
|
|
2230
|
+
return 0;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
const providerFlag = typeof flags["provider"] === "string" ? flags["provider"] : void 0;
|
|
2234
|
+
const modelFlag = typeof flags["model"] === "string" ? flags["model"] : void 0;
|
|
2235
|
+
const bothFlagsPinned = !!providerFlag && !!modelFlag;
|
|
2236
|
+
if (!bothFlagsPinned) {
|
|
2237
|
+
if (process.stdin.isTTY) {
|
|
2238
|
+
const picked = await runPicker({
|
|
2239
|
+
modelsRegistry,
|
|
2240
|
+
renderer,
|
|
2241
|
+
reader,
|
|
2242
|
+
config,
|
|
2243
|
+
defaultProvider: providerFlag ?? config.provider,
|
|
2244
|
+
defaultModel: modelFlag ?? config.model
|
|
2245
|
+
});
|
|
2246
|
+
if (!picked) {
|
|
2247
|
+
if (!config.provider || !config.model) {
|
|
2248
|
+
await reader.close();
|
|
2249
|
+
return 2;
|
|
2250
|
+
}
|
|
2251
|
+
} else {
|
|
2252
|
+
const prevProvider = config.provider;
|
|
2253
|
+
const prevModel = config.model;
|
|
2254
|
+
config = Object.freeze({ ...config, provider: picked.provider, model: picked.model });
|
|
2255
|
+
if (picked.provider !== prevProvider || picked.model !== prevModel) {
|
|
2256
|
+
const saved = await saveToGlobalConfig(
|
|
2257
|
+
wpaths.globalConfig,
|
|
2258
|
+
picked.provider,
|
|
2259
|
+
picked.model
|
|
2260
|
+
);
|
|
2261
|
+
if (saved) {
|
|
2262
|
+
renderer.writeInfo(`Saved ${picked.provider}/${picked.model} as default.
|
|
2263
|
+
`);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
} else if (!config.provider || !config.model) {
|
|
2268
|
+
process.stderr.write(
|
|
2269
|
+
"No provider or model configured. Run `wrongstack init` first, or pass --provider <id> --model <id>.\n"
|
|
2270
|
+
);
|
|
2271
|
+
await reader.close();
|
|
2272
|
+
return 2;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
if (isInteractiveTTY) {
|
|
2276
|
+
let modePinned;
|
|
2277
|
+
if (flags["no-tui"]) modePinned = "repl";
|
|
2278
|
+
else if (flags["tui"]) modePinned = "tui";
|
|
2279
|
+
const yoloPinned = flags["yolo"] === true ? true : void 0;
|
|
2280
|
+
const choices = await runLaunchPrompts({ renderer, reader, modePinned, yoloPinned });
|
|
2281
|
+
if (choices.mode === "tui") {
|
|
2282
|
+
flags["tui"] = true;
|
|
2283
|
+
flags["no-tui"] = false;
|
|
2284
|
+
} else {
|
|
2285
|
+
flags["tui"] = false;
|
|
2286
|
+
flags["no-tui"] = true;
|
|
2287
|
+
}
|
|
2288
|
+
if (choices.yolo !== config.yolo) {
|
|
2289
|
+
config = Object.freeze({ ...config, yolo: choices.yolo });
|
|
2290
|
+
}
|
|
1902
2291
|
}
|
|
1903
2292
|
const resolvedProvider = await modelsRegistry.getProvider(config.provider).catch(() => void 0);
|
|
1904
2293
|
if (!resolvedProvider) {
|
|
@@ -2265,7 +2654,7 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
2265
2654
|
};
|
|
2266
2655
|
const newProvider = providerRegistry.create({ ...newCfg, type: name });
|
|
2267
2656
|
context.provider = newProvider;
|
|
2268
|
-
config.provider
|
|
2657
|
+
config = Object.freeze({ ...config, provider: name });
|
|
2269
2658
|
} catch (err) {
|
|
2270
2659
|
renderer.writeError(
|
|
2271
2660
|
`Cannot switch to "${name}": ${err instanceof Error ? err.message : err}`
|