@gaberrb/polypus 0.4.7 → 0.4.8

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { Command } from "commander";
5
- import pc13 from "picocolors";
5
+ import pc14 from "picocolors";
6
6
 
7
7
  // src/cli/commands/add-agent.ts
8
8
  import pc from "picocolors";
@@ -111,6 +111,8 @@ var en = {
111
111
  "cli.opt.maxSteps": "maximum agent steps",
112
112
  "cli.opt.json": "headless mode: emit a single JSON object (steps, tool calls, files changed, usage) instead of the TUI \u2014 use with --mode bypass",
113
113
  "cli.opt.verify": "after the agent finishes, run project checks (typecheck/build/test) and iterate until they pass",
114
+ "cli.opt.budget": "stop the run when the estimated session cost reaches this USD amount (OpenRouter pricing)",
115
+ "cli.cmd.usage": "Show token/cost analytics aggregated per day",
114
116
  "cli.arg.swarmTask": "high-level task to split across agents",
115
117
  "cli.opt.agents": "comma-separated agent names (default: all configured)",
116
118
  "cli.opt.maxSubtasks": "maximum number of parallel subtasks",
@@ -147,6 +149,13 @@ var en = {
147
149
  "verify.passed": "verification passed",
148
150
  "verify.failed": "{n} check(s) failed \u2014 handing the output back to the agent (attempt {attempt})",
149
151
  "verify.giveUp": "{n} check(s) still failing after the retry budget \u2014 stopping",
152
+ "budget.session": "session spend: {spent} / budget {budget}",
153
+ "budget.hit": "\u25A0 stopped: estimated cost reached the budget of {budget}",
154
+ // usage analytics
155
+ "usage.header": "Usage (tokens / estimated cost) per day:",
156
+ "usage.empty": "No usage recorded yet. Run a task to start tracking.",
157
+ "usage.total": "total",
158
+ "usage.runs": "runs",
150
159
  // repl
151
160
  "repl.welcome": "Polypus interactive session.",
152
161
  "repl.welcomeHint": " Type /help for commands, /exit to quit.",
@@ -351,6 +360,8 @@ var ptBR = {
351
360
  "cli.opt.maxSteps": "n\xFAmero m\xE1ximo de passos do agente",
352
361
  "cli.opt.json": "modo headless: emite um \xFAnico objeto JSON (passos, tool calls, arquivos alterados, uso) em vez da TUI \u2014 use com --mode bypass",
353
362
  "cli.opt.verify": "ap\xF3s o agente terminar, roda as checagens do projeto (typecheck/build/test) e itera at\xE9 passar",
363
+ "cli.opt.budget": "interrompe a execu\xE7\xE3o quando o custo estimado da sess\xE3o atingir este valor em USD (pre\xE7os do OpenRouter)",
364
+ "cli.cmd.usage": "Mostra analytics de tokens/custo agregados por dia",
354
365
  "cli.arg.swarmTask": "tarefa de alto n\xEDvel para dividir entre os agentes",
355
366
  "cli.opt.agents": "nomes de agentes separados por v\xEDrgula (padr\xE3o: todos)",
356
367
  "cli.opt.maxSubtasks": "n\xFAmero m\xE1ximo de subtarefas paralelas",
@@ -385,6 +396,13 @@ var ptBR = {
385
396
  "verify.passed": "verifica\xE7\xE3o passou",
386
397
  "verify.failed": "{n} checagem(ns) falharam \u2014 devolvendo a sa\xEDda ao agente (tentativa {attempt})",
387
398
  "verify.giveUp": "{n} checagem(ns) ainda falhando ap\xF3s o limite de tentativas \u2014 parando",
399
+ "budget.session": "gasto da sess\xE3o: {spent} / or\xE7amento {budget}",
400
+ "budget.hit": "\u25A0 interrompido: o custo estimado atingiu o or\xE7amento de {budget}",
401
+ // usage analytics
402
+ "usage.header": "Uso (tokens / custo estimado) por dia:",
403
+ "usage.empty": "Nenhum uso registrado ainda. Rode uma tarefa para come\xE7ar a medir.",
404
+ "usage.total": "total",
405
+ "usage.runs": "execu\xE7\xF5es",
388
406
  "repl.welcome": "Sess\xE3o interativa do Polypus.",
389
407
  "repl.welcomeHint": " Digite /help para comandos, /exit para sair.",
390
408
  "repl.modeChanged": "modo \u2192 {mode}",
@@ -2096,9 +2114,9 @@ async function runAgent(opts) {
2096
2114
  let failStreak = 0;
2097
2115
  const maxToolRetries = opts.maxToolRetries ?? 3;
2098
2116
  const autoCorrect = opts.autoCorrect ?? true;
2099
- const usage = { promptTokens: 0, completionTokens: 0 };
2117
+ const usage2 = { promptTokens: 0, completionTokens: 0 };
2100
2118
  for (let step = 1; step <= maxSteps; step++) {
2101
- if (opts.signal?.aborted) return { finished: false, reason: "cancelled", steps: step - 1, messages, usage };
2119
+ if (opts.signal?.aborted) return { finished: false, reason: "cancelled", steps: step - 1, messages, usage: usage2 };
2102
2120
  events?.onStep?.(step);
2103
2121
  let response;
2104
2122
  try {
@@ -2109,12 +2127,12 @@ async function runAgent(opts) {
2109
2127
  signal: opts.signal
2110
2128
  });
2111
2129
  } catch (err) {
2112
- if (opts.signal?.aborted) return { finished: false, reason: "cancelled", steps: step, messages, usage };
2130
+ if (opts.signal?.aborted) return { finished: false, reason: "cancelled", steps: step, messages, usage: usage2 };
2113
2131
  throw err;
2114
2132
  }
2115
- usage.promptTokens += response.usage?.promptTokens ?? 0;
2116
- usage.completionTokens += response.usage?.completionTokens ?? 0;
2117
- events?.onUsage?.(usage);
2133
+ usage2.promptTokens += response.usage?.promptTokens ?? 0;
2134
+ usage2.completionTokens += response.usage?.completionTokens ?? 0;
2135
+ events?.onUsage?.(usage2);
2118
2136
  const { toolCalls, text: text2 } = driver.parse(response);
2119
2137
  messages.push(driver.assistantMessage(response, toolCalls));
2120
2138
  if (text2) events?.onAssistantText?.(text2);
@@ -2128,7 +2146,7 @@ async function runAgent(opts) {
2128
2146
  messages.push({ role: "user", content: guidance });
2129
2147
  continue;
2130
2148
  }
2131
- return { finished: false, reason: "stalled", steps: step, messages, usage };
2149
+ return { finished: false, reason: "stalled", steps: step, messages, usage: usage2 };
2132
2150
  }
2133
2151
  const stalled = text2.trim().length === 0 || looksLikeStall(text2);
2134
2152
  if (stalled) {
@@ -2138,17 +2156,17 @@ async function runAgent(opts) {
2138
2156
  messages.push(driver.repromptMessage());
2139
2157
  continue;
2140
2158
  }
2141
- return { finished: false, reason: "stalled", steps: step, messages, usage };
2159
+ return { finished: false, reason: "stalled", steps: step, messages, usage: usage2 };
2142
2160
  }
2143
- return { finished: false, reason: "reply", steps: step, messages, usage };
2161
+ return { finished: false, reason: "reply", steps: step, messages, usage: usage2 };
2144
2162
  }
2145
2163
  consecutiveNoTool = 0;
2146
2164
  for (const call of toolCalls) {
2147
- if (opts.signal?.aborted) return { finished: false, reason: "cancelled", steps: step, messages, usage };
2165
+ if (opts.signal?.aborted) return { finished: false, reason: "cancelled", steps: step, messages, usage: usage2 };
2148
2166
  events?.onToolCall?.(call);
2149
2167
  if (call.name === "finish") {
2150
2168
  const summary = String(call.arguments.summary ?? "").trim();
2151
- return { finished: true, reason: "finished", summary, steps: step, messages, usage };
2169
+ return { finished: true, reason: "finished", summary, steps: step, messages, usage: usage2 };
2152
2170
  }
2153
2171
  const tool = getTool(call.name);
2154
2172
  const result = tool ? await tool.run(call.arguments, ctx) : { ok: false, output: `Unknown tool "${call.name}". Available: ${toolSpecs().map((t2) => t2.name).join(", ")}` };
@@ -2186,12 +2204,12 @@ ${guidance}`;
2186
2204
  failStreak = sig === lastFailSig ? failStreak + 1 : 1;
2187
2205
  lastFailSig = sig;
2188
2206
  if (failStreak >= maxToolRetries) {
2189
- return { finished: false, reason: "stalled", steps: step, messages, usage };
2207
+ return { finished: false, reason: "stalled", steps: step, messages, usage: usage2 };
2190
2208
  }
2191
2209
  }
2192
2210
  }
2193
2211
  }
2194
- return { finished: false, reason: "maxsteps", steps: maxSteps, messages, usage };
2212
+ return { finished: false, reason: "maxsteps", steps: maxSteps, messages, usage: usage2 };
2195
2213
  }
2196
2214
 
2197
2215
  // src/core/context/mentions.ts
@@ -2296,6 +2314,153 @@ function clamp2(s) {
2296
2314
  return s.length > MAX_OUTPUT3 ? s.slice(-MAX_OUTPUT3) : s;
2297
2315
  }
2298
2316
 
2317
+ // src/core/agent/usage.ts
2318
+ import { appendFile, mkdir as mkdir3, readFile as readFile10 } from "fs/promises";
2319
+ import { join as join4 } from "path";
2320
+
2321
+ // src/core/providers/openrouter.ts
2322
+ var MODELS_URL = "https://openrouter.ai/api/v1/models";
2323
+ async function listOpenRouterModels(apiKey, timeoutMs = 8e3) {
2324
+ const controller = new AbortController();
2325
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2326
+ try {
2327
+ const res = await fetch(MODELS_URL, {
2328
+ signal: controller.signal,
2329
+ headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {}
2330
+ });
2331
+ if (!res.ok) throw new Error(`OpenRouter ${res.status}: ${await res.text().catch(() => "")}`);
2332
+ const data = await res.json();
2333
+ return (data.data ?? []).map(normalize).filter((m) => m.id.length > 0);
2334
+ } finally {
2335
+ clearTimeout(timer);
2336
+ }
2337
+ }
2338
+ function normalize(m) {
2339
+ const promptPrice = toPerMillion(m.pricing?.prompt);
2340
+ const completionPrice = toPerMillion(m.pricing?.completion);
2341
+ return {
2342
+ id: m.id ?? "",
2343
+ name: m.name ?? m.id ?? "",
2344
+ promptPrice,
2345
+ completionPrice,
2346
+ contextLength: m.context_length ?? 0,
2347
+ supportsTools: (m.supported_parameters ?? []).includes("tools"),
2348
+ free: promptPrice === 0 && completionPrice === 0
2349
+ };
2350
+ }
2351
+ function toPerMillion(price) {
2352
+ const n = Number(price ?? "0");
2353
+ return Number.isFinite(n) ? n * 1e6 : 0;
2354
+ }
2355
+ function filterModels(models2, f) {
2356
+ const term = f.search?.trim().toLowerCase();
2357
+ let out = models2.filter((m) => {
2358
+ if (term && !m.id.toLowerCase().includes(term) && !m.name.toLowerCase().includes(term)) {
2359
+ return false;
2360
+ }
2361
+ if (f.tools === "tools" && !m.supportsTools) return false;
2362
+ if (f.tools === "no-tools" && m.supportsTools) return false;
2363
+ if (f.freeOnly && !m.free) return false;
2364
+ if (f.maxPrice !== void 0 && (m.promptPrice < 0 || m.promptPrice > f.maxPrice)) return false;
2365
+ return true;
2366
+ });
2367
+ const key = (m) => m.promptPrice < 0 ? Number.POSITIVE_INFINITY : m.promptPrice;
2368
+ const sort = f.sort ?? "price";
2369
+ out = out.sort((a, b) => {
2370
+ switch (sort) {
2371
+ case "price-desc":
2372
+ return key(b) - key(a);
2373
+ case "context":
2374
+ return b.contextLength - a.contextLength;
2375
+ case "name":
2376
+ return a.id.localeCompare(b.id);
2377
+ default:
2378
+ return key(a) - key(b);
2379
+ }
2380
+ });
2381
+ return out;
2382
+ }
2383
+ function fmtPrice(perMillion) {
2384
+ if (perMillion < 0) return "var";
2385
+ if (perMillion === 0) return "free";
2386
+ if (perMillion < 1) return `$${perMillion.toFixed(2)}`;
2387
+ if (perMillion < 100) return `$${perMillion.toFixed(perMillion % 1 ? 1 : 0)}`;
2388
+ return `$${Math.round(perMillion)}`;
2389
+ }
2390
+ function fmtContext(n) {
2391
+ if (n >= 1e6) return `${Math.round(n / 1e5) / 10}M`;
2392
+ if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
2393
+ return String(n);
2394
+ }
2395
+
2396
+ // src/core/agent/usage.ts
2397
+ var catalogCache;
2398
+ async function resolveModelPricing(agent) {
2399
+ if (agent.provider !== "openrouter") return void 0;
2400
+ try {
2401
+ catalogCache ??= listOpenRouterModels(resolveSecret(agent.apiKey));
2402
+ const models2 = await catalogCache;
2403
+ const m = models2.find((x) => x.id === agent.model);
2404
+ if (!m || m.promptPrice < 0 || m.completionPrice < 0) return void 0;
2405
+ return { promptPrice: m.promptPrice, completionPrice: m.completionPrice };
2406
+ } catch {
2407
+ return void 0;
2408
+ }
2409
+ }
2410
+ function estimateCost(usage2, pricing) {
2411
+ return usage2.promptTokens / 1e6 * pricing.promptPrice + usage2.completionTokens / 1e6 * pricing.completionPrice;
2412
+ }
2413
+ function fmtUsd(n) {
2414
+ if (n <= 0) return "US$0.00";
2415
+ if (n < 0.01) return `US$${n.toFixed(4)}`;
2416
+ return `US$${n.toFixed(2)}`;
2417
+ }
2418
+ function usagePath() {
2419
+ return join4(configDir(), "usage.jsonl");
2420
+ }
2421
+ async function recordUsage(entry) {
2422
+ try {
2423
+ await mkdir3(configDir(), { recursive: true });
2424
+ await appendFile(usagePath(), JSON.stringify(entry) + "\n", "utf8");
2425
+ } catch {
2426
+ }
2427
+ }
2428
+ async function aggregateUsage() {
2429
+ let text2 = "";
2430
+ try {
2431
+ text2 = await readFile10(usagePath(), "utf8");
2432
+ } catch {
2433
+ return { days: [], total: emptyBucket("total") };
2434
+ }
2435
+ const byDay = /* @__PURE__ */ new Map();
2436
+ const total = emptyBucket("total");
2437
+ for (const line of text2.split("\n")) {
2438
+ if (!line.trim()) continue;
2439
+ let e;
2440
+ try {
2441
+ e = JSON.parse(line);
2442
+ } catch {
2443
+ continue;
2444
+ }
2445
+ const date = (e.ts ?? "").slice(0, 10) || "unknown";
2446
+ const bucket = byDay.get(date) ?? emptyBucket(date);
2447
+ accumulate(bucket, e);
2448
+ byDay.set(date, bucket);
2449
+ accumulate(total, e);
2450
+ }
2451
+ const days = [...byDay.values()].sort((a, b) => a.date.localeCompare(b.date));
2452
+ return { days, total };
2453
+ }
2454
+ function emptyBucket(date) {
2455
+ return { date, promptTokens: 0, completionTokens: 0, costUsd: 0, runs: 0 };
2456
+ }
2457
+ function accumulate(bucket, e) {
2458
+ bucket.promptTokens += e.promptTokens ?? 0;
2459
+ bucket.completionTokens += e.completionTokens ?? 0;
2460
+ bucket.costUsd += e.costUsd ?? 0;
2461
+ bucket.runs += 1;
2462
+ }
2463
+
2299
2464
  // src/cli/commands/json-output.ts
2300
2465
  var OUTPUT_PREVIEW = 500;
2301
2466
  function createJsonCollector() {
@@ -2381,81 +2546,6 @@ async function listOllamaModels(host = ollamaHost(), timeoutMs = 2e3) {
2381
2546
  }
2382
2547
  }
2383
2548
 
2384
- // src/core/providers/openrouter.ts
2385
- var MODELS_URL = "https://openrouter.ai/api/v1/models";
2386
- async function listOpenRouterModels(apiKey, timeoutMs = 8e3) {
2387
- const controller = new AbortController();
2388
- const timer = setTimeout(() => controller.abort(), timeoutMs);
2389
- try {
2390
- const res = await fetch(MODELS_URL, {
2391
- signal: controller.signal,
2392
- headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {}
2393
- });
2394
- if (!res.ok) throw new Error(`OpenRouter ${res.status}: ${await res.text().catch(() => "")}`);
2395
- const data = await res.json();
2396
- return (data.data ?? []).map(normalize).filter((m) => m.id.length > 0);
2397
- } finally {
2398
- clearTimeout(timer);
2399
- }
2400
- }
2401
- function normalize(m) {
2402
- const promptPrice = toPerMillion(m.pricing?.prompt);
2403
- const completionPrice = toPerMillion(m.pricing?.completion);
2404
- return {
2405
- id: m.id ?? "",
2406
- name: m.name ?? m.id ?? "",
2407
- promptPrice,
2408
- completionPrice,
2409
- contextLength: m.context_length ?? 0,
2410
- supportsTools: (m.supported_parameters ?? []).includes("tools"),
2411
- free: promptPrice === 0 && completionPrice === 0
2412
- };
2413
- }
2414
- function toPerMillion(price) {
2415
- const n = Number(price ?? "0");
2416
- return Number.isFinite(n) ? n * 1e6 : 0;
2417
- }
2418
- function filterModels(models2, f) {
2419
- const term = f.search?.trim().toLowerCase();
2420
- let out = models2.filter((m) => {
2421
- if (term && !m.id.toLowerCase().includes(term) && !m.name.toLowerCase().includes(term)) {
2422
- return false;
2423
- }
2424
- if (f.tools === "tools" && !m.supportsTools) return false;
2425
- if (f.tools === "no-tools" && m.supportsTools) return false;
2426
- if (f.freeOnly && !m.free) return false;
2427
- if (f.maxPrice !== void 0 && (m.promptPrice < 0 || m.promptPrice > f.maxPrice)) return false;
2428
- return true;
2429
- });
2430
- const key = (m) => m.promptPrice < 0 ? Number.POSITIVE_INFINITY : m.promptPrice;
2431
- const sort = f.sort ?? "price";
2432
- out = out.sort((a, b) => {
2433
- switch (sort) {
2434
- case "price-desc":
2435
- return key(b) - key(a);
2436
- case "context":
2437
- return b.contextLength - a.contextLength;
2438
- case "name":
2439
- return a.id.localeCompare(b.id);
2440
- default:
2441
- return key(a) - key(b);
2442
- }
2443
- });
2444
- return out;
2445
- }
2446
- function fmtPrice(perMillion) {
2447
- if (perMillion < 0) return "var";
2448
- if (perMillion === 0) return "free";
2449
- if (perMillion < 1) return `$${perMillion.toFixed(2)}`;
2450
- if (perMillion < 100) return `$${perMillion.toFixed(perMillion % 1 ? 1 : 0)}`;
2451
- return `$${Math.round(perMillion)}`;
2452
- }
2453
- function fmtContext(n) {
2454
- if (n >= 1e6) return `${Math.round(n / 1e5) / 10}M`;
2455
- if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
2456
- return String(n);
2457
- }
2458
-
2459
2549
  // src/ui/wizard.ts
2460
2550
  function bail(value) {
2461
2551
  if (p.isCancel(value)) {
@@ -3136,7 +3226,7 @@ import pc7 from "picocolors";
3136
3226
  // src/core/git/worktree.ts
3137
3227
  import { mkdtemp } from "fs/promises";
3138
3228
  import { tmpdir } from "os";
3139
- import { join as join4 } from "path";
3229
+ import { join as join5 } from "path";
3140
3230
  import { simpleGit } from "simple-git";
3141
3231
  async function ensureRepo(workspace) {
3142
3232
  const git = simpleGit(workspace);
@@ -3157,7 +3247,7 @@ async function identityArgs(git) {
3157
3247
  }
3158
3248
  async function createWorktree(git, label) {
3159
3249
  const branch = `polypus/${label}-${Date.now().toString(36)}`;
3160
- const path = await mkdtemp(join4(tmpdir(), "polypus-wt-"));
3250
+ const path = await mkdtemp(join5(tmpdir(), "polypus-wt-"));
3161
3251
  await git.raw(["worktree", "add", "-b", branch, path, "HEAD"]);
3162
3252
  return { path, branch };
3163
3253
  }
@@ -3597,7 +3687,9 @@ async function run(task, opts) {
3597
3687
  deny: config.permissions.deny,
3598
3688
  allowedCommands: config.permissions.allowedCommands,
3599
3689
  maxSteps: opts.maxSteps ? Number(opts.maxSteps) : void 0,
3600
- history: []
3690
+ history: [],
3691
+ budget: opts.budget ? Number(opts.budget) : void 0,
3692
+ costUsd: 0
3601
3693
  };
3602
3694
  const runTask = async (taskText) => {
3603
3695
  const active = resolveAgent(config, session.agentName);
@@ -3621,6 +3713,9 @@ async function run(task, opts) {
3621
3713
  );
3622
3714
  }
3623
3715
  await executeTask(task, resolved2, workspace, session, opts.json ?? false, opts.verify ?? false);
3716
+ if (session.budget !== void 0 && !opts.json) {
3717
+ console.log(pc8.dim(t("budget.session", { spent: fmtUsd(session.costUsd), budget: fmtUsd(session.budget) })));
3718
+ }
3624
3719
  return;
3625
3720
  }
3626
3721
  const resolved = createProvider(agentConfig);
@@ -3657,6 +3752,21 @@ async function executeTask(task, resolved, workspace, session, json = false, ver
3657
3752
  const controller = new AbortController();
3658
3753
  const cancel2 = listenForCancel(controller);
3659
3754
  const collector = json ? createJsonCollector() : void 0;
3755
+ const pricing = await resolveModelPricing(resolved.config);
3756
+ let budgetHit = false;
3757
+ const baseEvents = collector ? collector.events : renderEvents(spinner3);
3758
+ const events = {
3759
+ ...baseEvents,
3760
+ onUsage(u) {
3761
+ baseEvents.onUsage?.(u);
3762
+ if (session.budget !== void 0 && pricing && !controller.signal.aborted) {
3763
+ if (session.costUsd + estimateCost(u, pricing) >= session.budget) {
3764
+ budgetHit = true;
3765
+ controller.abort();
3766
+ }
3767
+ }
3768
+ }
3769
+ };
3660
3770
  const permissions = new PermissionEngine({
3661
3771
  mode: session.mode,
3662
3772
  policy: { workspace, allow: session.allow, deny: session.deny },
@@ -3679,7 +3789,7 @@ async function executeTask(task, resolved, workspace, session, json = false, ver
3679
3789
  history: session.history,
3680
3790
  maxSteps: session.maxSteps,
3681
3791
  signal: controller.signal,
3682
- events: collector ? collector.events : renderEvents(spinner3)
3792
+ events
3683
3793
  });
3684
3794
  if (!json) spinner3.start(t("ui.thinking"));
3685
3795
  let result;
@@ -3693,10 +3803,24 @@ async function executeTask(task, resolved, workspace, session, json = false, ver
3693
3803
  spinner3.stop();
3694
3804
  cancel2.dispose();
3695
3805
  }
3806
+ const runCost = pricing ? estimateCost(result.usage, pricing) : 0;
3807
+ session.costUsd += runCost;
3808
+ await recordUsage({
3809
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3810
+ agent: resolved.config.name,
3811
+ provider: resolved.config.provider,
3812
+ model: resolved.config.model,
3813
+ promptTokens: result.usage.promptTokens,
3814
+ completionTokens: result.usage.completionTokens,
3815
+ costUsd: runCost
3816
+ });
3696
3817
  if (collector) {
3697
3818
  process.stdout.write(JSON.stringify(collector.build(result)) + "\n");
3698
3819
  return;
3699
3820
  }
3821
+ if (budgetHit) {
3822
+ console.log(pc8.yellow("\n" + t("budget.hit", { budget: fmtUsd(session.budget ?? 0) })));
3823
+ }
3700
3824
  if (result.reason === "finished") {
3701
3825
  console.log(pc8.green("\n" + t("run.done", { steps: result.steps })) + (result.summary ? ` ${result.summary}` : ""));
3702
3826
  } else if (result.reason === "cancelled") {
@@ -3706,15 +3830,13 @@ async function executeTask(task, resolved, workspace, session, json = false, ver
3706
3830
  }
3707
3831
  if (result.usage.promptTokens || result.usage.completionTokens) {
3708
3832
  const total = result.usage.promptTokens + result.usage.completionTokens;
3709
- console.log(
3710
- pc8.dim(
3711
- "\u21B3 " + t("ui.tokens", {
3712
- total: fmtTokens(total),
3713
- in: fmtTokens(result.usage.promptTokens),
3714
- out: fmtTokens(result.usage.completionTokens)
3715
- })
3716
- )
3717
- );
3833
+ const tokensLine = t("ui.tokens", {
3834
+ total: fmtTokens(total),
3835
+ in: fmtTokens(result.usage.promptTokens),
3836
+ out: fmtTokens(result.usage.completionTokens)
3837
+ });
3838
+ const cost = pricing ? ` \xB7 ~${fmtUsd(runCost)}` : "";
3839
+ console.log(pc8.dim("\u21B3 " + tokensLine + cost));
3718
3840
  }
3719
3841
  }
3720
3842
  function fmtTokens(n) {
@@ -3816,8 +3938,8 @@ function renderEvents(spinner3) {
3816
3938
  onStep() {
3817
3939
  spinner3.start(t("ui.thinking"));
3818
3940
  },
3819
- onUsage(usage) {
3820
- const total = usage.promptTokens + usage.completionTokens;
3941
+ onUsage(usage2) {
3942
+ const total = usage2.promptTokens + usage2.completionTokens;
3821
3943
  if (total > 0) spinner3.setSuffix(t("ui.tokensShort", { total: fmtTokens(total) }));
3822
3944
  },
3823
3945
  onAssistantText(text2) {
@@ -3855,8 +3977,8 @@ async function setup() {
3855
3977
  import pc9 from "picocolors";
3856
3978
 
3857
3979
  // src/core/scaffold/init.ts
3858
- import { mkdir as mkdir3, writeFile as writeFile4, access } from "fs/promises";
3859
- import { dirname as dirname3, join as join5 } from "path";
3980
+ import { mkdir as mkdir4, writeFile as writeFile4, access } from "fs/promises";
3981
+ import { dirname as dirname3, join as join6 } from "path";
3860
3982
 
3861
3983
  // src/core/scaffold/templates.ts
3862
3984
  function polyTemplates(locale) {
@@ -4099,12 +4221,12 @@ async function scaffoldPoly(workspace, opts) {
4099
4221
  const skipped = [];
4100
4222
  for (const [rel, content] of Object.entries(templates)) {
4101
4223
  const display = `.poly/${rel}`;
4102
- const abs = join5(workspace, ".poly", ...rel.split("/"));
4224
+ const abs = join6(workspace, ".poly", ...rel.split("/"));
4103
4225
  if (!opts.force && await exists(abs)) {
4104
4226
  skipped.push(display);
4105
4227
  continue;
4106
4228
  }
4107
- await mkdir3(dirname3(abs), { recursive: true });
4229
+ await mkdir4(dirname3(abs), { recursive: true });
4108
4230
  await writeFile4(abs, content, "utf8");
4109
4231
  created.push(display);
4110
4232
  }
@@ -4198,11 +4320,32 @@ async function resolveOpenRouterKey() {
4198
4320
  }
4199
4321
  }
4200
4322
 
4323
+ // src/cli/commands/usage.ts
4324
+ import pc11 from "picocolors";
4325
+ async function usage() {
4326
+ const { days, total } = await aggregateUsage();
4327
+ if (days.length === 0) {
4328
+ console.log(pc11.yellow(t("usage.empty")));
4329
+ return;
4330
+ }
4331
+ console.log(pc11.bold(t("usage.header")));
4332
+ for (const d of days) console.log(" " + formatRow(d));
4333
+ console.log(pc11.dim(" " + "\u2500".repeat(40)));
4334
+ console.log(" " + pc11.bold(formatRow({ ...total, date: t("usage.total") })));
4335
+ }
4336
+ function formatRow(b) {
4337
+ const tokens2 = fmtTokens2(b.promptTokens + b.completionTokens);
4338
+ return `${b.date.padEnd(12)} ${tokens2.padStart(7)} tok ${fmtUsd(b.costUsd).padStart(10)} (${b.runs} ${t("usage.runs")})`;
4339
+ }
4340
+ function fmtTokens2(n) {
4341
+ return n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
4342
+ }
4343
+
4201
4344
  // src/cli/commands/prd.ts
4202
- import { writeFile as writeFile5, readFile as readFile10 } from "fs/promises";
4345
+ import { writeFile as writeFile5, readFile as readFile11 } from "fs/promises";
4203
4346
  import { execFile } from "child_process";
4204
4347
  import { promisify as promisify3 } from "util";
4205
- import pc11 from "picocolors";
4348
+ import pc12 from "picocolors";
4206
4349
 
4207
4350
  // src/core/agent/prd.ts
4208
4351
  var SYSTEM = [
@@ -4331,14 +4474,14 @@ async function prd(issueRef, opts) {
4331
4474
  const markdown = await withRetry(() => generatePrd(issue, provider, guide));
4332
4475
  if (opts.out) {
4333
4476
  await writeFile5(opts.out, markdown + "\n", "utf8");
4334
- console.error(pc11.green(t("prd.wrote", { path: opts.out })));
4477
+ console.error(pc12.green(t("prd.wrote", { path: opts.out })));
4335
4478
  } else {
4336
4479
  process.stdout.write(markdown + "\n");
4337
4480
  }
4338
4481
  }
4339
4482
  async function loadIssue(issueRef, input) {
4340
4483
  if (input) {
4341
- const raw = input === "-" ? await readStdin() : await readFile10(input, "utf8");
4484
+ const raw = input === "-" ? await readStdin() : await readFile11(input, "utf8");
4342
4485
  return normalize2(JSON.parse(stripBom(raw)));
4343
4486
  }
4344
4487
  const num = numericRef(issueRef);
@@ -4357,10 +4500,10 @@ function normalize2(raw) {
4357
4500
  }
4358
4501
 
4359
4502
  // src/cli/commands/review.ts
4360
- import { writeFile as writeFile6, readFile as readFile11 } from "fs/promises";
4503
+ import { writeFile as writeFile6, readFile as readFile12 } from "fs/promises";
4361
4504
  import { execFile as execFile2 } from "child_process";
4362
4505
  import { promisify as promisify4 } from "util";
4363
- import pc12 from "picocolors";
4506
+ import pc13 from "picocolors";
4364
4507
 
4365
4508
  // src/core/agent/review.ts
4366
4509
  var MAX_DIFF_CHARS = Number(process.env.POLYPUS_MAX_DIFF_CHARS) || 6e4;
@@ -4427,13 +4570,13 @@ async function review(prRef, opts) {
4427
4570
  const markdown = await withRetry(() => reviewDiff(diff, meta, provider, guide));
4428
4571
  if (opts.out) {
4429
4572
  await writeFile6(opts.out, markdown + "\n", "utf8");
4430
- console.error(pc12.green(t("review.wrote", { path: opts.out })));
4573
+ console.error(pc13.green(t("review.wrote", { path: opts.out })));
4431
4574
  } else {
4432
4575
  process.stdout.write(markdown + "\n");
4433
4576
  }
4434
4577
  }
4435
4578
  async function loadDiff(num, input) {
4436
- if (input) return input === "-" ? readStdin() : readFile11(input, "utf8");
4579
+ if (input) return input === "-" ? readStdin() : readFile12(input, "utf8");
4437
4580
  const { stdout: stdout2 } = await exec4("gh", ["pr", "diff", num]);
4438
4581
  return stdout2;
4439
4582
  }
@@ -4445,7 +4588,7 @@ async function loadMeta(num, input) {
4445
4588
  }
4446
4589
 
4447
4590
  // src/cli/index.ts
4448
- import { join as join6 } from "path";
4591
+ import { join as join7 } from "path";
4449
4592
 
4450
4593
  // src/core/config/dotenv.ts
4451
4594
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
@@ -4478,7 +4621,7 @@ async function launchInteractive() {
4478
4621
  const config = await loadConfig();
4479
4622
  if (config.agents.length === 0) {
4480
4623
  console.log(banner());
4481
- console.log(" " + pc13.yellow(t("welcome.firstRun")) + "\n");
4624
+ console.log(" " + pc14.yellow(t("welcome.firstRun")) + "\n");
4482
4625
  await setup();
4483
4626
  }
4484
4627
  await run(void 0, {});
@@ -4505,20 +4648,21 @@ function buildProgram() {
4505
4648
  program.command("add-agent").argument("<name>", t("cli.arg.addAgentName")).requiredOption("--provider <provider>", t("cli.opt.provider")).requiredOption("--model <model>", t("cli.opt.model")).option("--api-key <key>", t("cli.opt.apiKey")).option("--base-url <url>", t("cli.opt.baseUrl")).option("--tool-mode <mode>", t("cli.opt.toolMode"), "auto").option("--set-default", t("cli.opt.setDefault")).description(t("cli.cmd.addAgent")).action((name, opts) => addAgent(name, opts));
4506
4649
  program.command("remove-agent").argument("<name>", t("cli.arg.removeAgentName")).description(t("cli.cmd.removeAgent")).action((name) => removeAgent(name));
4507
4650
  program.command("list-agents").alias("agents").description(t("cli.cmd.listAgents")).action(() => listAgents());
4508
- program.command("run").argument("[task]", t("cli.arg.runTask")).option("--agent <name>", t("cli.opt.agent")).option("--mode <mode>", t("cli.opt.mode")).option("--max-steps <n>", t("cli.opt.maxSteps")).option("--json", t("cli.opt.json")).option("--verify", t("cli.opt.verify")).description(t("cli.cmd.run")).action((task, opts) => run(task, opts));
4651
+ program.command("run").argument("[task]", t("cli.arg.runTask")).option("--agent <name>", t("cli.opt.agent")).option("--mode <mode>", t("cli.opt.mode")).option("--max-steps <n>", t("cli.opt.maxSteps")).option("--json", t("cli.opt.json")).option("--verify", t("cli.opt.verify")).option("--budget <usd>", t("cli.opt.budget")).description(t("cli.cmd.run")).action((task, opts) => run(task, opts));
4509
4652
  program.command("swarm").argument("<task>", t("cli.arg.swarmTask")).option("--agents <names>", t("cli.opt.agents")).option("--max-subtasks <n>", t("cli.opt.maxSubtasks")).description(t("cli.cmd.swarm")).action((task, opts) => swarm(task, opts));
4510
4653
  program.command("models").option("--search <text>", t("cli.opt.search")).option("--tools", t("cli.opt.toolsOnly")).option("--free", t("cli.opt.free")).option("--max-price <usd>", t("cli.opt.maxPrice")).option("--sort <order>", t("cli.opt.sort")).option("--limit <n>", t("cli.opt.limit")).description(t("cli.cmd.models")).action((opts) => models(opts));
4654
+ program.command("usage").description(t("cli.cmd.usage")).action(() => usage());
4511
4655
  program.command("prd").argument("<issue>", t("cli.arg.prdIssue")).option("--out <file>", t("cli.opt.out")).option("--model <model>", t("cli.opt.model")).option("--input <file>", t("cli.opt.input")).description(t("cli.cmd.prd")).action((issue, opts) => prd(issue, opts));
4512
4656
  program.command("review").argument("<pr>", t("cli.arg.reviewPr")).option("--out <file>", t("cli.opt.out")).option("--model <model>", t("cli.opt.model")).option("--input <file>", t("cli.opt.input")).description(t("cli.cmd.review")).action((pr, opts) => review(pr, opts));
4513
4657
  return program;
4514
4658
  }
4515
4659
  async function main() {
4516
4660
  try {
4517
- loadDotenv([join6(configDir(), ".env"), join6(process.cwd(), ".env")]);
4661
+ loadDotenv([join7(configDir(), ".env"), join7(process.cwd(), ".env")]);
4518
4662
  await resolveLocale();
4519
4663
  await buildProgram().parseAsync(process.argv);
4520
4664
  } catch (err) {
4521
- console.error(pc13.red(`\u2717 ${err.message}`));
4665
+ console.error(pc14.red(`\u2717 ${err.message}`));
4522
4666
  process.exitCode = 1;
4523
4667
  }
4524
4668
  }