@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 +265 -121
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
2116
|
-
|
|
2117
|
-
events?.onUsage?.(
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
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(
|
|
3820
|
-
const total =
|
|
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
|
|
3859
|
-
import { dirname as dirname3, join as
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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() :
|
|
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
|
|
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(" " +
|
|
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([
|
|
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(
|
|
4665
|
+
console.error(pc14.red(`\u2717 ${err.message}`));
|
|
4522
4666
|
process.exitCode = 1;
|
|
4523
4667
|
}
|
|
4524
4668
|
}
|