@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 +32 -0
- package/dist/index.js +336 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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(
|
|
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 [
|
|
4314
|
-
|
|
4315
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
5213
|
+
import pc14 from "picocolors";
|
|
4903
5214
|
|
|
4904
5215
|
// src/core/agent/prd.ts
|
|
4905
|
-
var
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
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() :
|
|
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
|
|
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(" " +
|
|
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([
|
|
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(
|
|
5532
|
+
console.error(pc16.red(`\u2717 ${err.message}`));
|
|
5221
5533
|
process.exitCode = 1;
|
|
5222
5534
|
}
|
|
5223
5535
|
}
|