@gaberrb/polypus 0.4.17 → 0.4.19

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/README.md CHANGED
@@ -196,6 +196,17 @@ agent is also instructed to talk back to you in the configured language.
196
196
  All file access is restricted to the workspace and the configured **allow-list**
197
197
  globs; the **deny-list** (e.g. `.git/**`, `**/.env`) always wins.
198
198
 
199
+ ### Timeout Configuration
200
+
201
+ You can control the maximum duration of a swarm session using the environment variable `POLYPUS_SWARM_OVERALL_TIMEOUT_MS` (default: 1 hour). This prevents the session from hanging indefinitely if an agent stalls:
202
+
203
+ ```bash
204
+ export POLYPUS_SWARM_OVERALL_TIMEOUT_MS=1800000 # 30 minutes
205
+ polypus swarm "your task"
206
+ ```
207
+
208
+ Similarly, `POLYPUS_SWARM_IDLE_TIMEOUT_MS` controls the idle timeout for individual workers (default: 5 minutes).
209
+
199
210
  ## Swarm (parallel agents)
200
211
 
201
212
  ```bash
@@ -207,6 +218,27 @@ worktree (in `bypass` mode, since the worktree is throwaway), and the branches
207
218
  are merged back sequentially. Conflicting branches are kept for manual
208
219
  inspection rather than force-merged.
209
220
 
221
+ ## MCP (external tool servers)
222
+
223
+ Connect [Model Context Protocol](https://modelcontextprotocol.io) servers to give
224
+ the agent extra tools. Declare them in `.poly/mcp.json`:
225
+
226
+ ```json
227
+ {
228
+ "mcpServers": {
229
+ "filesystem": {
230
+ "command": "npx",
231
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
232
+ }
233
+ }
234
+ }
235
+ ```
236
+
237
+ On each run Polypus spawns the servers (stdio transport), lists their tools and
238
+ exposes them to the agent as `mcp__<server>__<tool>` — for both native and
239
+ emulated models. Servers that fail to start are skipped, the processes are shut
240
+ down when the run ends, and external MCP tools are disabled in `plan` mode.
241
+
210
242
  ## Autonomous agent — the tool self-improving 🤖
211
243
 
212
244
  Polypus can run **itself** in CI to implement its own issues. Label an issue
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 pc15 from "picocolors";
5
+ import pc16 from "picocolors";
6
6
 
7
7
  // src/cli/commands/add-agent.ts
8
8
  import pc from "picocolors";
@@ -113,6 +113,8 @@ var en = {
113
113
  "cli.opt.verify": "after the agent finishes, run project checks (typecheck/build/test) and iterate until they pass",
114
114
  "cli.opt.budget": "stop the run when the estimated session cost reaches this USD amount (OpenRouter pricing)",
115
115
  "cli.cmd.usage": "Show token/cost analytics aggregated per day",
116
+ "cli.cmd.estimate": "Estimate the effort/cost to implement a task (no changes made)",
117
+ "cli.arg.estimateTask": "task to estimate",
116
118
  "cli.cmd.sessions": "List saved sessions that can be resumed",
117
119
  "cli.opt.continue": "resume the most recent saved session",
118
120
  "cli.opt.resume": "resume a specific saved session by id",
@@ -144,6 +146,7 @@ var en = {
144
146
  "run.cancelled": "\u25A0 cancelled",
145
147
  "compaction.done": "context compacted: ~{before} \u2192 ~{after} tokens",
146
148
  "tools.customLoaded": "loaded custom tool(s): {names}",
149
+ "mcp.connected": "connected MCP server(s): {servers} ({n} tool(s))",
147
150
  "run.jsonNeedsTask": "--json requires a task argument (headless mode has no interactive REPL).",
148
151
  "review.approveAll": "approve all",
149
152
  "review.reject": "reject",
@@ -161,6 +164,12 @@ var en = {
161
164
  "usage.empty": "No usage recorded yet. Run a task to start tracking.",
162
165
  "usage.total": "total",
163
166
  "usage.runs": "runs",
167
+ // estimate
168
+ "estimate.header": "Effort estimate:",
169
+ "estimate.complexity": "complexity",
170
+ "estimate.steps": "steps",
171
+ "estimate.tokens": "tokens",
172
+ "estimate.cost": "estimated cost",
164
173
  // sessions
165
174
  "sessions.header": "Saved sessions (most recent first):",
166
175
  "sessions.empty": "No saved sessions yet.",
@@ -229,6 +238,7 @@ var en = {
229
238
  "swarm.conflictsHeader": "\u26A0 {n} branch(es) had merge conflicts (kept for inspection):",
230
239
  "swarm.statusDone": "done",
231
240
  "swarm.statusIncomplete": "incomplete",
241
+ "swarm.timeout": "\u26A0 Session timeout reached. Operation aborted.",
232
242
  // init
233
243
  "init.created": "\u2713 .poly scaffolded:",
234
244
  "init.skipped": "Kept (already existed):",
@@ -377,6 +387,8 @@ var ptBR = {
377
387
  "cli.opt.verify": "ap\xF3s o agente terminar, roda as checagens do projeto (typecheck/build/test) e itera at\xE9 passar",
378
388
  "cli.opt.budget": "interrompe a execu\xE7\xE3o quando o custo estimado da sess\xE3o atingir este valor em USD (pre\xE7os do OpenRouter)",
379
389
  "cli.cmd.usage": "Mostra analytics de tokens/custo agregados por dia",
390
+ "cli.cmd.estimate": "Estima o esfor\xE7o/custo para implementar uma tarefa (sem alterar nada)",
391
+ "cli.arg.estimateTask": "tarefa a estimar",
380
392
  "cli.cmd.sessions": "Lista as sess\xF5es salvas que podem ser retomadas",
381
393
  "cli.opt.continue": "retoma a sess\xE3o salva mais recente",
382
394
  "cli.opt.resume": "retoma uma sess\xE3o salva espec\xEDfica pelo id",
@@ -406,6 +418,7 @@ var ptBR = {
406
418
  "run.cancelled": "\u25A0 cancelado",
407
419
  "compaction.done": "contexto compactado: ~{before} \u2192 ~{after} tokens",
408
420
  "tools.customLoaded": "tool(s) customizada(s) carregada(s): {names}",
421
+ "mcp.connected": "servidor(es) MCP conectado(s): {servers} ({n} tool(s))",
409
422
  "run.jsonNeedsTask": "--json exige um argumento de tarefa (o modo headless n\xE3o tem REPL interativo).",
410
423
  "review.approveAll": "aprovar tudo",
411
424
  "review.reject": "rejeitar",
@@ -423,6 +436,12 @@ var ptBR = {
423
436
  "usage.empty": "Nenhum uso registrado ainda. Rode uma tarefa para come\xE7ar a medir.",
424
437
  "usage.total": "total",
425
438
  "usage.runs": "execu\xE7\xF5es",
439
+ // estimate
440
+ "estimate.header": "Estimativa de esfor\xE7o:",
441
+ "estimate.complexity": "complexidade",
442
+ "estimate.steps": "passos",
443
+ "estimate.tokens": "tokens",
444
+ "estimate.cost": "custo estimado",
426
445
  // sessions
427
446
  "sessions.header": "Sess\xF5es salvas (mais recentes primeiro):",
428
447
  "sessions.empty": "Nenhuma sess\xE3o salva ainda.",
@@ -489,6 +508,7 @@ var ptBR = {
489
508
  "swarm.conflictsHeader": "\u26A0 {n} branch(es) tiveram conflitos de merge (mantidos para inspe\xE7\xE3o):",
490
509
  "swarm.statusDone": "ok",
491
510
  "swarm.statusIncomplete": "incompleta",
511
+ "swarm.timeout": "\u26A0 O tempo m\xE1ximo da sess\xE3o foi atingido. A opera\xE7\xE3o foi interrompida.",
492
512
  // init
493
513
  "init.created": "\u2713 .poly criado:",
494
514
  "init.skipped": "Mantidos (j\xE1 existiam):",
@@ -2829,6 +2849,195 @@ function clamp3(s) {
2829
2849
  return s.length > MAX_OUTPUT4 ? s.slice(0, MAX_OUTPUT4) + "\n\u2026[truncated]" : s;
2830
2850
  }
2831
2851
 
2852
+ // src/core/mcp/index.ts
2853
+ import { readFile as readFile14 } from "fs/promises";
2854
+ import { join as join8 } from "path";
2855
+ import { z as z10 } from "zod";
2856
+
2857
+ // src/core/mcp/client.ts
2858
+ import { spawn } from "child_process";
2859
+ var PROTOCOL_VERSION = "2024-11-05";
2860
+ var McpClient = class {
2861
+ constructor(command, args = [], env = {}) {
2862
+ this.command = command;
2863
+ this.args = args;
2864
+ this.env = env;
2865
+ }
2866
+ command;
2867
+ args;
2868
+ env;
2869
+ proc;
2870
+ nextId = 1;
2871
+ pending = /* @__PURE__ */ new Map();
2872
+ buffer = "";
2873
+ closed = false;
2874
+ /** Spawn the server and perform the initialize handshake. */
2875
+ async initialize(timeoutMs = 2e4) {
2876
+ this.proc = spawn(this.command, this.args, {
2877
+ stdio: ["pipe", "pipe", "pipe"],
2878
+ env: { ...process.env, ...this.env },
2879
+ windowsHide: true
2880
+ });
2881
+ this.proc.stdout.setEncoding("utf8");
2882
+ this.proc.stdout.on("data", (chunk) => this.onData(chunk));
2883
+ this.proc.on("exit", () => this.failAll(new Error("MCP server process exited")));
2884
+ this.proc.on("error", (err) => this.failAll(err));
2885
+ await this.request(
2886
+ "initialize",
2887
+ {
2888
+ protocolVersion: PROTOCOL_VERSION,
2889
+ capabilities: {},
2890
+ clientInfo: { name: "polypus", version: "1" }
2891
+ },
2892
+ timeoutMs
2893
+ );
2894
+ this.notify("notifications/initialized");
2895
+ }
2896
+ /** List the tools the server exposes. */
2897
+ async listTools() {
2898
+ const res = await this.request("tools/list", {});
2899
+ return res.tools ?? [];
2900
+ }
2901
+ /** Call a tool and return its textual output. */
2902
+ async callTool(name, args) {
2903
+ try {
2904
+ const res = await this.request("tools/call", { name, arguments: args });
2905
+ const text2 = (res.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n").trim();
2906
+ return { ok: !res.isError, text: text2 || "(no output)" };
2907
+ } catch (err) {
2908
+ return { ok: false, text: `MCP call failed: ${err.message}` };
2909
+ }
2910
+ }
2911
+ /** Terminate the server process. */
2912
+ async close() {
2913
+ if (this.closed) return;
2914
+ this.closed = true;
2915
+ this.failAll(new Error("MCP client closed"));
2916
+ this.proc?.kill();
2917
+ }
2918
+ onData(chunk) {
2919
+ this.buffer += chunk;
2920
+ let nl;
2921
+ while ((nl = this.buffer.indexOf("\n")) !== -1) {
2922
+ const line = this.buffer.slice(0, nl).trim();
2923
+ this.buffer = this.buffer.slice(nl + 1);
2924
+ if (!line) continue;
2925
+ let msg;
2926
+ try {
2927
+ msg = JSON.parse(line);
2928
+ } catch {
2929
+ continue;
2930
+ }
2931
+ if (typeof msg.id !== "number") continue;
2932
+ const p4 = this.pending.get(msg.id);
2933
+ if (!p4) continue;
2934
+ this.pending.delete(msg.id);
2935
+ if (msg.error) p4.reject(new Error(msg.error.message ?? "MCP error"));
2936
+ else p4.resolve(msg.result);
2937
+ }
2938
+ }
2939
+ request(method, params, timeoutMs = 2e4) {
2940
+ if (!this.proc) return Promise.reject(new Error("MCP client not initialized"));
2941
+ const id = this.nextId++;
2942
+ const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
2943
+ return new Promise((resolve12, reject) => {
2944
+ const timer = setTimeout(() => {
2945
+ this.pending.delete(id);
2946
+ reject(new Error(`MCP request "${method}" timed out`));
2947
+ }, timeoutMs);
2948
+ this.pending.set(id, {
2949
+ resolve: (v) => {
2950
+ clearTimeout(timer);
2951
+ resolve12(v);
2952
+ },
2953
+ reject: (e) => {
2954
+ clearTimeout(timer);
2955
+ reject(e);
2956
+ }
2957
+ });
2958
+ this.proc.stdin.write(payload);
2959
+ });
2960
+ }
2961
+ notify(method, params = {}) {
2962
+ this.proc?.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
2963
+ }
2964
+ failAll(err) {
2965
+ for (const [, p4] of this.pending) p4.reject(err);
2966
+ this.pending.clear();
2967
+ }
2968
+ };
2969
+
2970
+ // src/core/mcp/index.ts
2971
+ var ServerSchema = z10.object({
2972
+ command: z10.string().min(1),
2973
+ args: z10.array(z10.string()).default([]),
2974
+ env: z10.record(z10.string()).default({})
2975
+ });
2976
+ var McpConfigSchema = z10.object({
2977
+ mcpServers: z10.record(ServerSchema).default({})
2978
+ });
2979
+ var MAX_OUTPUT5 = 2e4;
2980
+ async function loadMcpTools(workspace) {
2981
+ let raw;
2982
+ try {
2983
+ raw = await readFile14(join8(workspace, ".poly", "mcp.json"), "utf8");
2984
+ } catch {
2985
+ return { tools: [], servers: [], close: async () => {
2986
+ } };
2987
+ }
2988
+ const parsed = McpConfigSchema.safeParse(safeJson(raw));
2989
+ if (!parsed.success) return { tools: [], servers: [], close: async () => {
2990
+ } };
2991
+ const clients = [];
2992
+ const tools = [];
2993
+ const servers = [];
2994
+ for (const [name, cfg] of Object.entries(parsed.data.mcpServers)) {
2995
+ const client = new McpClient(cfg.command, cfg.args, cfg.env);
2996
+ try {
2997
+ await client.initialize();
2998
+ const defs = await client.listTools();
2999
+ for (const def of defs) tools.push(wrapTool(name, client, def.name, def.description, def.inputSchema));
3000
+ clients.push(client);
3001
+ servers.push(name);
3002
+ } catch {
3003
+ await client.close().catch(() => {
3004
+ });
3005
+ }
3006
+ }
3007
+ return {
3008
+ tools,
3009
+ servers,
3010
+ close: async () => {
3011
+ await Promise.all(clients.map((c) => c.close().catch(() => {
3012
+ })));
3013
+ }
3014
+ };
3015
+ }
3016
+ function wrapTool(server, client, toolName, description, inputSchema) {
3017
+ return {
3018
+ mutating: true,
3019
+ spec: {
3020
+ name: `mcp__${server}__${toolName}`,
3021
+ description: `[MCP:${server}] ${description ?? toolName}`,
3022
+ parameters: inputSchema ?? { type: "object", properties: {} }
3023
+ },
3024
+ async run(args, ctx) {
3025
+ if (ctx.permissions.mode === "plan") {
3026
+ return { ok: false, output: "plan mode: external MCP tools are disabled" };
3027
+ }
3028
+ const { ok, text: text2 } = await client.callTool(toolName, args);
3029
+ return { ok, output: text2.length > MAX_OUTPUT5 ? text2.slice(0, MAX_OUTPUT5) + "\n\u2026[truncated]" : text2 };
3030
+ }
3031
+ };
3032
+ }
3033
+ function safeJson(raw) {
3034
+ try {
3035
+ return JSON.parse(raw);
3036
+ } catch {
3037
+ return {};
3038
+ }
3039
+ }
3040
+
2832
3041
  // src/cli/commands/json-output.ts
2833
3042
  var OUTPUT_PREVIEW = 500;
2834
3043
  function createJsonCollector() {
@@ -3627,7 +3836,7 @@ import pc7 from "picocolors";
3627
3836
  // src/core/git/worktree.ts
3628
3837
  import { mkdtemp } from "fs/promises";
3629
3838
  import { tmpdir } from "os";
3630
- import { join as join8 } from "path";
3839
+ import { join as join9 } from "path";
3631
3840
  import { simpleGit } from "simple-git";
3632
3841
  async function ensureRepo(workspace) {
3633
3842
  const git = simpleGit(workspace);
@@ -3648,7 +3857,7 @@ async function identityArgs(git) {
3648
3857
  }
3649
3858
  async function createWorktree(git, label) {
3650
3859
  const branch = `polypus/${label}-${Date.now().toString(36)}`;
3651
- const path = await mkdtemp(join8(tmpdir(), "polypus-wt-"));
3860
+ const path = await mkdtemp(join9(tmpdir(), "polypus-wt-"));
3652
3861
  await git.raw(["worktree", "add", "-b", branch, path, "HEAD"]);
3653
3862
  return { path, branch };
3654
3863
  }
@@ -3868,6 +4077,10 @@ function idleTimeoutMs() {
3868
4077
  const raw = Number(process.env.POLYPUS_SWARM_IDLE_TIMEOUT_MS);
3869
4078
  return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_IDLE_TIMEOUT_MS;
3870
4079
  }
4080
+ function overallTimeoutMs() {
4081
+ const raw = Number(process.env.POLYPUS_SWARM_OVERALL_TIMEOUT_MS);
4082
+ return Number.isFinite(raw) && raw > 0 ? raw : 36e5;
4083
+ }
3871
4084
 
3872
4085
  // src/ui/swarm-view.ts
3873
4086
  var RESET2 = "\x1B[0m";
@@ -4092,6 +4305,10 @@ async function runSwarmSession(task, config, opts = {}) {
4092
4305
  const view = new SwarmView(resolved[0].config.name);
4093
4306
  view.start();
4094
4307
  let result;
4308
+ const sessionTimeout = setTimeout(() => {
4309
+ controller.abort();
4310
+ console.log(pc7.red(t("swarm.timeout")));
4311
+ }, overallTimeoutMs());
4095
4312
  try {
4096
4313
  result = await runSwarm({
4097
4314
  task,
@@ -4115,6 +4332,7 @@ async function runSwarmSession(task, config, opts = {}) {
4115
4332
  }
4116
4333
  });
4117
4334
  } finally {
4335
+ clearTimeout(sessionTimeout);
4118
4336
  view.stop();
4119
4337
  cancel2.dispose();
4120
4338
  }
@@ -4310,9 +4528,17 @@ async function executeTask(task, resolved, workspace, session, json = false, ver
4310
4528
  return ok;
4311
4529
  }
4312
4530
  });
4313
- const [extraTools, hooks] = await Promise.all([loadCustomTools(workspace), loadHooks(workspace)]);
4314
- if (!json && extraTools.length > 0) {
4315
- console.log(pc8.dim(t("tools.customLoaded", { names: extraTools.map((tl) => tl.spec.name).join(", ") })));
4531
+ const [customTools, hooks, mcp] = await Promise.all([
4532
+ loadCustomTools(workspace),
4533
+ loadHooks(workspace),
4534
+ loadMcpTools(workspace)
4535
+ ]);
4536
+ const extraTools = [...customTools, ...mcp.tools];
4537
+ if (!json && customTools.length > 0) {
4538
+ console.log(pc8.dim(t("tools.customLoaded", { names: customTools.map((tl) => tl.spec.name).join(", ") })));
4539
+ }
4540
+ if (!json && mcp.servers.length > 0) {
4541
+ console.log(pc8.dim(t("mcp.connected", { servers: mcp.servers.join(", "), n: mcp.tools.length })));
4316
4542
  }
4317
4543
  const runOnce = (taskText) => runAgent({
4318
4544
  task: taskText,
@@ -4339,6 +4565,7 @@ async function executeTask(task, resolved, workspace, session, json = false, ver
4339
4565
  } finally {
4340
4566
  spinner3.stop();
4341
4567
  cancel2.dispose();
4568
+ await mcp.close();
4342
4569
  }
4343
4570
  if (!session.title) session.title = deriveTitle(session.history);
4344
4571
  await saveSession({
@@ -4513,7 +4740,7 @@ import pc9 from "picocolors";
4513
4740
 
4514
4741
  // src/core/scaffold/init.ts
4515
4742
  import { mkdir as mkdir5, writeFile as writeFile5, access } from "fs/promises";
4516
- import { dirname as dirname3, join as join9 } from "path";
4743
+ import { dirname as dirname3, join as join10 } from "path";
4517
4744
 
4518
4745
  // src/core/scaffold/templates.ts
4519
4746
  function polyTemplates(locale) {
@@ -4756,7 +4983,7 @@ async function scaffoldPoly(workspace, opts) {
4756
4983
  const skipped = [];
4757
4984
  for (const [rel, content] of Object.entries(templates)) {
4758
4985
  const display = `.poly/${rel}`;
4759
- const abs = join9(workspace, ".poly", ...rel.split("/"));
4986
+ const abs = join10(workspace, ".poly", ...rel.split("/"));
4760
4987
  if (!opts.force && await exists(abs)) {
4761
4988
  skipped.push(display);
4762
4989
  continue;
@@ -4895,14 +5122,98 @@ async function sessions() {
4895
5122
  console.log(pc12.dim("\n" + t("sessions.hint")));
4896
5123
  }
4897
5124
 
5125
+ // src/cli/commands/estimate.ts
5126
+ import pc13 from "picocolors";
5127
+
5128
+ // src/core/agent/estimate.ts
5129
+ var SYSTEM = [
5130
+ "You estimate the effort for an autonomous coding agent (a ReAct loop that reads/edits files",
5131
+ "and runs commands over several steps) to implement a software task in an existing repo.",
5132
+ "Account for the loop re-sending growing context each step. Be realistic, not optimistic.",
5133
+ "Return ONLY a JSON object, no prose, with exactly these keys:",
5134
+ '{"complexity":"low|medium|high","estimatedSteps":<int>,"estimatedTokens":<int total across all steps>,',
5135
+ '"rationale":"<one sentence>","risks":"<one sentence>"}'
5136
+ ].join(" ");
5137
+ function extractJsonObject(text2) {
5138
+ const start = text2.indexOf("{");
5139
+ if (start === -1) return void 0;
5140
+ let depth = 0;
5141
+ for (let i = start; i < text2.length; i++) {
5142
+ if (text2[i] === "{") depth++;
5143
+ else if (text2[i] === "}" && --depth === 0) {
5144
+ try {
5145
+ return JSON.parse(text2.slice(start, i + 1));
5146
+ } catch {
5147
+ return void 0;
5148
+ }
5149
+ }
5150
+ }
5151
+ return void 0;
5152
+ }
5153
+ function clampInt(value, min, max, fallback) {
5154
+ const n = Math.round(Number(value));
5155
+ if (!Number.isFinite(n)) return fallback;
5156
+ return Math.min(max, Math.max(min, n));
5157
+ }
5158
+ async function estimateTask(task, agent, pricing) {
5159
+ const res = await agent.provider.chat({
5160
+ messages: [
5161
+ { role: "system", content: SYSTEM },
5162
+ { role: "user", content: `Task:
5163
+ ${task}` }
5164
+ ],
5165
+ params: { temperature: 0 }
5166
+ });
5167
+ const parsed = extractJsonObject(res.content) ?? {};
5168
+ const complexity = ["low", "medium", "high"].includes(parsed.complexity) ? parsed.complexity : "medium";
5169
+ const estimatedSteps = clampInt(parsed.estimatedSteps, 1, 300, 30);
5170
+ const estimatedTokens = clampInt(parsed.estimatedTokens, 1e3, 2e7, 8e4);
5171
+ const rationale = typeof parsed.rationale === "string" ? parsed.rationale : "";
5172
+ const risks = typeof parsed.risks === "string" ? parsed.risks : "";
5173
+ let costUsd;
5174
+ let costLabel = "unknown (no pricing for this model)";
5175
+ if (pricing) {
5176
+ costUsd = estimateCost(
5177
+ { promptTokens: Math.round(estimatedTokens * 0.8), completionTokens: Math.round(estimatedTokens * 0.2) },
5178
+ pricing
5179
+ );
5180
+ costLabel = fmtUsd(costUsd);
5181
+ }
5182
+ return { complexity, estimatedSteps, estimatedTokens, rationale, risks, costUsd, costLabel };
5183
+ }
5184
+
5185
+ // src/cli/commands/estimate.ts
5186
+ async function estimate(task, opts) {
5187
+ const config = await loadConfig();
5188
+ const agentConfig = resolveAgent(config, opts.agent);
5189
+ const resolved = createProvider(agentConfig);
5190
+ const pricing = await resolveModelPricing(resolved.config);
5191
+ const est = await estimateTask(task, resolved, pricing);
5192
+ if (opts.json) {
5193
+ process.stdout.write(JSON.stringify({ estimate: est }) + "\n");
5194
+ return;
5195
+ }
5196
+ console.log(pc13.bold(t("estimate.header")));
5197
+ console.log(` ${t("estimate.complexity")}: ${pc13.cyan(est.complexity)}`);
5198
+ console.log(` ${t("estimate.steps")}: ~${est.estimatedSteps}`);
5199
+ console.log(` ${t("estimate.tokens")}: ~${fmtTokens3(est.estimatedTokens)}`);
5200
+ console.log(` ${t("estimate.cost")}: ${pc13.green(est.costLabel)}`);
5201
+ if (est.rationale) console.log(pc13.dim(` ${est.rationale}`));
5202
+ if (est.risks) console.log(pc13.dim(` \u26A0 ${est.risks}`));
5203
+ }
5204
+ function fmtTokens3(n) {
5205
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
5206
+ return n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
5207
+ }
5208
+
4898
5209
  // src/cli/commands/prd.ts
4899
- import { writeFile as writeFile6, readFile as readFile14 } from "fs/promises";
5210
+ import { writeFile as writeFile6, readFile as readFile15 } from "fs/promises";
4900
5211
  import { execFile } from "child_process";
4901
5212
  import { promisify as promisify5 } from "util";
4902
- import pc13 from "picocolors";
5213
+ import pc14 from "picocolors";
4903
5214
 
4904
5215
  // src/core/agent/prd.ts
4905
- var SYSTEM = [
5216
+ var SYSTEM2 = [
4906
5217
  "You are a product analyst. You turn a GitHub issue into a concise, structured PRD",
4907
5218
  "(Product Requirements Document) in Markdown.",
4908
5219
  "Rules:",
@@ -4932,7 +5243,7 @@ ${comments}` : "",
4932
5243
  ].join("\n");
4933
5244
  }
4934
5245
  async function generatePrd(issue, provider, projectContext) {
4935
- const messages = [{ role: "system", content: SYSTEM }];
5246
+ const messages = [{ role: "system", content: SYSTEM2 }];
4936
5247
  if (projectContext) {
4937
5248
  messages.push({
4938
5249
  role: "system",
@@ -5028,14 +5339,14 @@ async function prd(issueRef, opts) {
5028
5339
  const markdown = await withRetry(() => generatePrd(issue, provider, guide));
5029
5340
  if (opts.out) {
5030
5341
  await writeFile6(opts.out, markdown + "\n", "utf8");
5031
- console.error(pc13.green(t("prd.wrote", { path: opts.out })));
5342
+ console.error(pc14.green(t("prd.wrote", { path: opts.out })));
5032
5343
  } else {
5033
5344
  process.stdout.write(markdown + "\n");
5034
5345
  }
5035
5346
  }
5036
5347
  async function loadIssue(issueRef, input) {
5037
5348
  if (input) {
5038
- const raw = input === "-" ? await readStdin() : await readFile14(input, "utf8");
5349
+ const raw = input === "-" ? await readStdin() : await readFile15(input, "utf8");
5039
5350
  return normalize2(JSON.parse(stripBom(raw)));
5040
5351
  }
5041
5352
  const num = numericRef(issueRef);
@@ -5054,14 +5365,14 @@ function normalize2(raw) {
5054
5365
  }
5055
5366
 
5056
5367
  // src/cli/commands/review.ts
5057
- import { writeFile as writeFile7, readFile as readFile15 } from "fs/promises";
5368
+ import { writeFile as writeFile7, readFile as readFile16 } from "fs/promises";
5058
5369
  import { execFile as execFile2 } from "child_process";
5059
5370
  import { promisify as promisify6 } from "util";
5060
- import pc14 from "picocolors";
5371
+ import pc15 from "picocolors";
5061
5372
 
5062
5373
  // src/core/agent/review.ts
5063
5374
  var MAX_DIFF_CHARS = Number(process.env.POLYPUS_MAX_DIFF_CHARS) || 6e4;
5064
- var SYSTEM2 = [
5375
+ var SYSTEM3 = [
5065
5376
  "You are a senior code reviewer. Review the pull request diff below and report",
5066
5377
  "concrete findings in Markdown.",
5067
5378
  "Rules:",
@@ -5095,7 +5406,7 @@ function buildReviewPrompt(diff, meta) {
5095
5406
  }
5096
5407
  async function reviewDiff(diff, meta, provider, projectGuide) {
5097
5408
  if (!diff.trim()) return "_Sem altera\xE7\xF5es no diff para revisar._";
5098
- const messages = [{ role: "system", content: SYSTEM2 }];
5409
+ const messages = [{ role: "system", content: SYSTEM3 }];
5099
5410
  if (projectGuide) {
5100
5411
  messages.push({
5101
5412
  role: "system",
@@ -5124,13 +5435,13 @@ async function review(prRef, opts) {
5124
5435
  const markdown = await withRetry(() => reviewDiff(diff, meta, provider, guide));
5125
5436
  if (opts.out) {
5126
5437
  await writeFile7(opts.out, markdown + "\n", "utf8");
5127
- console.error(pc14.green(t("review.wrote", { path: opts.out })));
5438
+ console.error(pc15.green(t("review.wrote", { path: opts.out })));
5128
5439
  } else {
5129
5440
  process.stdout.write(markdown + "\n");
5130
5441
  }
5131
5442
  }
5132
5443
  async function loadDiff(num, input) {
5133
- if (input) return input === "-" ? readStdin() : readFile15(input, "utf8");
5444
+ if (input) return input === "-" ? readStdin() : readFile16(input, "utf8");
5134
5445
  const { stdout: stdout2 } = await exec6("gh", ["pr", "diff", num]);
5135
5446
  return stdout2;
5136
5447
  }
@@ -5142,7 +5453,7 @@ async function loadMeta(num, input) {
5142
5453
  }
5143
5454
 
5144
5455
  // src/cli/index.ts
5145
- import { join as join10 } from "path";
5456
+ import { join as join11 } from "path";
5146
5457
 
5147
5458
  // src/core/config/dotenv.ts
5148
5459
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
@@ -5175,7 +5486,7 @@ async function launchInteractive() {
5175
5486
  const config = await loadConfig();
5176
5487
  if (config.agents.length === 0) {
5177
5488
  console.log(banner());
5178
- console.log(" " + pc15.yellow(t("welcome.firstRun")) + "\n");
5489
+ console.log(" " + pc16.yellow(t("welcome.firstRun")) + "\n");
5179
5490
  await setup();
5180
5491
  }
5181
5492
  await run(void 0, {});
@@ -5206,6 +5517,7 @@ function buildProgram() {
5206
5517
  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));
5207
5518
  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));
5208
5519
  program.command("usage").description(t("cli.cmd.usage")).action(() => usage());
5520
+ program.command("estimate").argument("<task>", t("cli.arg.estimateTask")).option("--agent <name>", t("cli.opt.agent")).option("--json", t("cli.opt.json")).description(t("cli.cmd.estimate")).action((task, opts) => estimate(task, opts));
5209
5521
  program.command("sessions").description(t("cli.cmd.sessions")).action(() => sessions());
5210
5522
  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));
5211
5523
  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));
@@ -5213,11 +5525,11 @@ function buildProgram() {
5213
5525
  }
5214
5526
  async function main() {
5215
5527
  try {
5216
- loadDotenv([join10(configDir(), ".env"), join10(process.cwd(), ".env")]);
5528
+ loadDotenv([join11(configDir(), ".env"), join11(process.cwd(), ".env")]);
5217
5529
  await resolveLocale();
5218
5530
  await buildProgram().parseAsync(process.argv);
5219
5531
  } catch (err) {
5220
- console.error(pc15.red(`\u2717 ${err.message}`));
5532
+ console.error(pc16.red(`\u2717 ${err.message}`));
5221
5533
  process.exitCode = 1;
5222
5534
  }
5223
5535
  }