@gaberrb/polypus 0.4.18 → 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 +21 -0
- package/dist/index.js +213 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -218,6 +218,27 @@ worktree (in `bypass` mode, since the worktree is throwaway), and the branches
|
|
|
218
218
|
are merged back sequentially. Conflicting branches are kept for manual
|
|
219
219
|
inspection rather than force-merged.
|
|
220
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
|
+
|
|
221
242
|
## Autonomous agent — the tool self-improving 🤖
|
|
222
243
|
|
|
223
244
|
Polypus can run **itself** in CI to implement its own issues. Label an issue
|
package/dist/index.js
CHANGED
|
@@ -146,6 +146,7 @@ var en = {
|
|
|
146
146
|
"run.cancelled": "\u25A0 cancelled",
|
|
147
147
|
"compaction.done": "context compacted: ~{before} \u2192 ~{after} tokens",
|
|
148
148
|
"tools.customLoaded": "loaded custom tool(s): {names}",
|
|
149
|
+
"mcp.connected": "connected MCP server(s): {servers} ({n} tool(s))",
|
|
149
150
|
"run.jsonNeedsTask": "--json requires a task argument (headless mode has no interactive REPL).",
|
|
150
151
|
"review.approveAll": "approve all",
|
|
151
152
|
"review.reject": "reject",
|
|
@@ -417,6 +418,7 @@ var ptBR = {
|
|
|
417
418
|
"run.cancelled": "\u25A0 cancelado",
|
|
418
419
|
"compaction.done": "contexto compactado: ~{before} \u2192 ~{after} tokens",
|
|
419
420
|
"tools.customLoaded": "tool(s) customizada(s) carregada(s): {names}",
|
|
421
|
+
"mcp.connected": "servidor(es) MCP conectado(s): {servers} ({n} tool(s))",
|
|
420
422
|
"run.jsonNeedsTask": "--json exige um argumento de tarefa (o modo headless n\xE3o tem REPL interativo).",
|
|
421
423
|
"review.approveAll": "aprovar tudo",
|
|
422
424
|
"review.reject": "rejeitar",
|
|
@@ -2847,6 +2849,195 @@ function clamp3(s) {
|
|
|
2847
2849
|
return s.length > MAX_OUTPUT4 ? s.slice(0, MAX_OUTPUT4) + "\n\u2026[truncated]" : s;
|
|
2848
2850
|
}
|
|
2849
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
|
+
|
|
2850
3041
|
// src/cli/commands/json-output.ts
|
|
2851
3042
|
var OUTPUT_PREVIEW = 500;
|
|
2852
3043
|
function createJsonCollector() {
|
|
@@ -3645,7 +3836,7 @@ import pc7 from "picocolors";
|
|
|
3645
3836
|
// src/core/git/worktree.ts
|
|
3646
3837
|
import { mkdtemp } from "fs/promises";
|
|
3647
3838
|
import { tmpdir } from "os";
|
|
3648
|
-
import { join as
|
|
3839
|
+
import { join as join9 } from "path";
|
|
3649
3840
|
import { simpleGit } from "simple-git";
|
|
3650
3841
|
async function ensureRepo(workspace) {
|
|
3651
3842
|
const git = simpleGit(workspace);
|
|
@@ -3666,7 +3857,7 @@ async function identityArgs(git) {
|
|
|
3666
3857
|
}
|
|
3667
3858
|
async function createWorktree(git, label) {
|
|
3668
3859
|
const branch = `polypus/${label}-${Date.now().toString(36)}`;
|
|
3669
|
-
const path = await mkdtemp(
|
|
3860
|
+
const path = await mkdtemp(join9(tmpdir(), "polypus-wt-"));
|
|
3670
3861
|
await git.raw(["worktree", "add", "-b", branch, path, "HEAD"]);
|
|
3671
3862
|
return { path, branch };
|
|
3672
3863
|
}
|
|
@@ -4337,9 +4528,17 @@ async function executeTask(task, resolved, workspace, session, json = false, ver
|
|
|
4337
4528
|
return ok;
|
|
4338
4529
|
}
|
|
4339
4530
|
});
|
|
4340
|
-
const [
|
|
4341
|
-
|
|
4342
|
-
|
|
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 })));
|
|
4343
4542
|
}
|
|
4344
4543
|
const runOnce = (taskText) => runAgent({
|
|
4345
4544
|
task: taskText,
|
|
@@ -4366,6 +4565,7 @@ async function executeTask(task, resolved, workspace, session, json = false, ver
|
|
|
4366
4565
|
} finally {
|
|
4367
4566
|
spinner3.stop();
|
|
4368
4567
|
cancel2.dispose();
|
|
4568
|
+
await mcp.close();
|
|
4369
4569
|
}
|
|
4370
4570
|
if (!session.title) session.title = deriveTitle(session.history);
|
|
4371
4571
|
await saveSession({
|
|
@@ -4540,7 +4740,7 @@ import pc9 from "picocolors";
|
|
|
4540
4740
|
|
|
4541
4741
|
// src/core/scaffold/init.ts
|
|
4542
4742
|
import { mkdir as mkdir5, writeFile as writeFile5, access } from "fs/promises";
|
|
4543
|
-
import { dirname as dirname3, join as
|
|
4743
|
+
import { dirname as dirname3, join as join10 } from "path";
|
|
4544
4744
|
|
|
4545
4745
|
// src/core/scaffold/templates.ts
|
|
4546
4746
|
function polyTemplates(locale) {
|
|
@@ -4783,7 +4983,7 @@ async function scaffoldPoly(workspace, opts) {
|
|
|
4783
4983
|
const skipped = [];
|
|
4784
4984
|
for (const [rel, content] of Object.entries(templates)) {
|
|
4785
4985
|
const display = `.poly/${rel}`;
|
|
4786
|
-
const abs =
|
|
4986
|
+
const abs = join10(workspace, ".poly", ...rel.split("/"));
|
|
4787
4987
|
if (!opts.force && await exists(abs)) {
|
|
4788
4988
|
skipped.push(display);
|
|
4789
4989
|
continue;
|
|
@@ -5007,7 +5207,7 @@ function fmtTokens3(n) {
|
|
|
5007
5207
|
}
|
|
5008
5208
|
|
|
5009
5209
|
// src/cli/commands/prd.ts
|
|
5010
|
-
import { writeFile as writeFile6, readFile as
|
|
5210
|
+
import { writeFile as writeFile6, readFile as readFile15 } from "fs/promises";
|
|
5011
5211
|
import { execFile } from "child_process";
|
|
5012
5212
|
import { promisify as promisify5 } from "util";
|
|
5013
5213
|
import pc14 from "picocolors";
|
|
@@ -5146,7 +5346,7 @@ async function prd(issueRef, opts) {
|
|
|
5146
5346
|
}
|
|
5147
5347
|
async function loadIssue(issueRef, input) {
|
|
5148
5348
|
if (input) {
|
|
5149
|
-
const raw = input === "-" ? await readStdin() : await
|
|
5349
|
+
const raw = input === "-" ? await readStdin() : await readFile15(input, "utf8");
|
|
5150
5350
|
return normalize2(JSON.parse(stripBom(raw)));
|
|
5151
5351
|
}
|
|
5152
5352
|
const num = numericRef(issueRef);
|
|
@@ -5165,7 +5365,7 @@ function normalize2(raw) {
|
|
|
5165
5365
|
}
|
|
5166
5366
|
|
|
5167
5367
|
// src/cli/commands/review.ts
|
|
5168
|
-
import { writeFile as writeFile7, readFile as
|
|
5368
|
+
import { writeFile as writeFile7, readFile as readFile16 } from "fs/promises";
|
|
5169
5369
|
import { execFile as execFile2 } from "child_process";
|
|
5170
5370
|
import { promisify as promisify6 } from "util";
|
|
5171
5371
|
import pc15 from "picocolors";
|
|
@@ -5241,7 +5441,7 @@ async function review(prRef, opts) {
|
|
|
5241
5441
|
}
|
|
5242
5442
|
}
|
|
5243
5443
|
async function loadDiff(num, input) {
|
|
5244
|
-
if (input) return input === "-" ? readStdin() :
|
|
5444
|
+
if (input) return input === "-" ? readStdin() : readFile16(input, "utf8");
|
|
5245
5445
|
const { stdout: stdout2 } = await exec6("gh", ["pr", "diff", num]);
|
|
5246
5446
|
return stdout2;
|
|
5247
5447
|
}
|
|
@@ -5253,7 +5453,7 @@ async function loadMeta(num, input) {
|
|
|
5253
5453
|
}
|
|
5254
5454
|
|
|
5255
5455
|
// src/cli/index.ts
|
|
5256
|
-
import { join as
|
|
5456
|
+
import { join as join11 } from "path";
|
|
5257
5457
|
|
|
5258
5458
|
// src/core/config/dotenv.ts
|
|
5259
5459
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
@@ -5325,7 +5525,7 @@ function buildProgram() {
|
|
|
5325
5525
|
}
|
|
5326
5526
|
async function main() {
|
|
5327
5527
|
try {
|
|
5328
|
-
loadDotenv([
|
|
5528
|
+
loadDotenv([join11(configDir(), ".env"), join11(process.cwd(), ".env")]);
|
|
5329
5529
|
await resolveLocale();
|
|
5330
5530
|
await buildProgram().parseAsync(process.argv);
|
|
5331
5531
|
} catch (err) {
|