@gaberrb/polypus 0.4.9 → 0.4.11
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 +281 -38
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -142,6 +142,8 @@ var en = {
|
|
|
142
142
|
"run.reprompt": "\u21BB no tool call \u2014 reinforcing instructions (attempt {attempt})",
|
|
143
143
|
"run.autocorrect": "\u21BB tool failed \u2014 auto-correcting with extra context",
|
|
144
144
|
"run.cancelled": "\u25A0 cancelled",
|
|
145
|
+
"compaction.done": "context compacted: ~{before} \u2192 ~{after} tokens",
|
|
146
|
+
"tools.customLoaded": "loaded custom tool(s): {names}",
|
|
145
147
|
"run.jsonNeedsTask": "--json requires a task argument (headless mode has no interactive REPL).",
|
|
146
148
|
"review.approveAll": "approve all",
|
|
147
149
|
"review.reject": "reject",
|
|
@@ -401,6 +403,8 @@ var ptBR = {
|
|
|
401
403
|
"run.reprompt": "\u21BB nenhuma chamada de tool \u2014 refor\xE7ando instru\xE7\xF5es (tentativa {attempt})",
|
|
402
404
|
"run.autocorrect": "\u21BB tool falhou \u2014 autocorrigindo com contexto extra",
|
|
403
405
|
"run.cancelled": "\u25A0 cancelado",
|
|
406
|
+
"compaction.done": "contexto compactado: ~{before} \u2192 ~{after} tokens",
|
|
407
|
+
"tools.customLoaded": "tool(s) customizada(s) carregada(s): {names}",
|
|
404
408
|
"run.jsonNeedsTask": "--json exige um argumento de tarefa (o modo headless n\xE3o tem REPL interativo).",
|
|
405
409
|
"review.approveAll": "aprovar tudo",
|
|
406
410
|
"review.reject": "rejeitar",
|
|
@@ -2092,6 +2096,116 @@ async function loadProjectInstructions(workspace) {
|
|
|
2092
2096
|
return void 0;
|
|
2093
2097
|
}
|
|
2094
2098
|
|
|
2099
|
+
// src/core/agent/compaction.ts
|
|
2100
|
+
function estimateTokens(messages) {
|
|
2101
|
+
let chars = 0;
|
|
2102
|
+
for (const m of messages) chars += m.content.length;
|
|
2103
|
+
return Math.ceil(chars / 4);
|
|
2104
|
+
}
|
|
2105
|
+
var RECENT_KEEP = 8;
|
|
2106
|
+
var MIN_TO_COMPACT = 4;
|
|
2107
|
+
var MAX_SUMMARY_INPUT = 4e4;
|
|
2108
|
+
function findSafeCut(messages, desiredKeep = RECENT_KEEP) {
|
|
2109
|
+
let cut = Math.max(1, messages.length - desiredKeep);
|
|
2110
|
+
while (cut < messages.length && (messages[cut].role === "tool" || messages[cut - 1]?.role === "assistant" && (messages[cut - 1].toolCalls?.length ?? 0) > 0)) {
|
|
2111
|
+
cut++;
|
|
2112
|
+
}
|
|
2113
|
+
return cut;
|
|
2114
|
+
}
|
|
2115
|
+
function serialize(messages) {
|
|
2116
|
+
const text2 = messages.map((m) => {
|
|
2117
|
+
const tools = m.toolCalls?.length ? ` [called: ${m.toolCalls.map((c) => c.name).join(", ")}]` : "";
|
|
2118
|
+
return `${m.role}${tools}: ${m.content}`;
|
|
2119
|
+
}).join("\n\n");
|
|
2120
|
+
return text2.length > MAX_SUMMARY_INPUT ? text2.slice(-MAX_SUMMARY_INPUT) : text2;
|
|
2121
|
+
}
|
|
2122
|
+
async function compactHistory(messages, agent, signal) {
|
|
2123
|
+
if (messages.length === 0) return messages;
|
|
2124
|
+
const system = messages[0].role === "system" ? messages[0] : void 0;
|
|
2125
|
+
const startIdx = system ? 1 : 0;
|
|
2126
|
+
const cut = findSafeCut(messages);
|
|
2127
|
+
if (cut >= messages.length) return messages;
|
|
2128
|
+
const middle = messages.slice(startIdx, cut);
|
|
2129
|
+
if (middle.length < MIN_TO_COMPACT) return messages;
|
|
2130
|
+
const tail = messages.slice(cut);
|
|
2131
|
+
const summary = await agent.provider.chat({
|
|
2132
|
+
messages: [
|
|
2133
|
+
{
|
|
2134
|
+
role: "system",
|
|
2135
|
+
content: "You compress a coding agent's conversation so it can continue with less context. Summarize the messages below into a concise but information-dense brief that preserves: the original task and goal, key decisions, files created/edited and why, important command/test outputs, and any remaining TODOs or open problems. Use terse bullet points. Do not invent details."
|
|
2136
|
+
},
|
|
2137
|
+
{ role: "user", content: serialize(middle) }
|
|
2138
|
+
],
|
|
2139
|
+
signal
|
|
2140
|
+
});
|
|
2141
|
+
const summaryMessage = {
|
|
2142
|
+
role: "user",
|
|
2143
|
+
content: `[Summary of earlier conversation, compacted to save context]
|
|
2144
|
+
${summary.content.trim()}`
|
|
2145
|
+
};
|
|
2146
|
+
return system ? [system, summaryMessage, ...tail] : [summaryMessage, ...tail];
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
// src/core/agent/hooks.ts
|
|
2150
|
+
import { exec as exec2 } from "child_process";
|
|
2151
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
2152
|
+
import { join as join4 } from "path";
|
|
2153
|
+
import { promisify as promisify2 } from "util";
|
|
2154
|
+
import { z as z8 } from "zod";
|
|
2155
|
+
var execAsync2 = promisify2(exec2);
|
|
2156
|
+
var HOOK_TIMEOUT = 12e4;
|
|
2157
|
+
var HooksSchema = z8.object({
|
|
2158
|
+
/** Shell command run after a successful write_file. `{path}` is substituted. */
|
|
2159
|
+
afterWrite: z8.string().optional(),
|
|
2160
|
+
/** Shell command run after a successful edit_file. `{path}` is substituted. */
|
|
2161
|
+
afterEdit: z8.string().optional(),
|
|
2162
|
+
/** Shell command run after any successful mutating tool. `{tool}`/`{path}` substituted. */
|
|
2163
|
+
afterTool: z8.string().optional(),
|
|
2164
|
+
/** Block run_command when the command contains any of these substrings. */
|
|
2165
|
+
beforeCommand: z8.object({ deny: z8.array(z8.string()).default([]) }).optional()
|
|
2166
|
+
});
|
|
2167
|
+
async function loadHooks(workspace) {
|
|
2168
|
+
try {
|
|
2169
|
+
const raw = await readFile8(join4(workspace, ".poly", "hooks.json"), "utf8");
|
|
2170
|
+
const parsed = HooksSchema.safeParse(JSON.parse(raw));
|
|
2171
|
+
return parsed.success ? parsed.data : void 0;
|
|
2172
|
+
} catch {
|
|
2173
|
+
return void 0;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
function screenCommandHook(hooks, command) {
|
|
2177
|
+
const deny = hooks?.beforeCommand?.deny ?? [];
|
|
2178
|
+
for (const needle of deny) {
|
|
2179
|
+
if (needle && command.includes(needle)) {
|
|
2180
|
+
return { blocked: true, reason: `matches deny rule "${needle}"` };
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return { blocked: false };
|
|
2184
|
+
}
|
|
2185
|
+
function substitute(template, call) {
|
|
2186
|
+
const path = typeof call.arguments.path === "string" ? call.arguments.path : "";
|
|
2187
|
+
return template.replace(/\{path\}/g, path).replace(/\{tool\}/g, call.name);
|
|
2188
|
+
}
|
|
2189
|
+
async function runAfterHook(hooks, call, workspace) {
|
|
2190
|
+
if (!hooks) return void 0;
|
|
2191
|
+
const commands = [];
|
|
2192
|
+
if (call.name === "write_file" && hooks.afterWrite) commands.push({ label: "afterWrite", cmd: hooks.afterWrite });
|
|
2193
|
+
if (call.name === "edit_file" && hooks.afterEdit) commands.push({ label: "afterEdit", cmd: hooks.afterEdit });
|
|
2194
|
+
if (hooks.afterTool) commands.push({ label: "afterTool", cmd: hooks.afterTool });
|
|
2195
|
+
if (commands.length === 0) return void 0;
|
|
2196
|
+
const notes = [];
|
|
2197
|
+
for (const { label, cmd } of commands) {
|
|
2198
|
+
const resolved = substitute(cmd, call);
|
|
2199
|
+
try {
|
|
2200
|
+
await execAsync2(resolved, { cwd: workspace, timeout: HOOK_TIMEOUT, windowsHide: true });
|
|
2201
|
+
notes.push(`\u21AA hook ${label} ok`);
|
|
2202
|
+
} catch (err) {
|
|
2203
|
+
notes.push(`\u21AA hook ${label} failed: ${err.message.split("\n")[0]}`);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return notes.join("\n");
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2095
2209
|
// src/core/agent/loop.ts
|
|
2096
2210
|
function looksLikeStall(text2) {
|
|
2097
2211
|
const lc = text2.toLowerCase();
|
|
@@ -2132,7 +2246,13 @@ async function runAgent(opts) {
|
|
|
2132
2246
|
const { agent, permissions, events } = opts;
|
|
2133
2247
|
const maxSteps = opts.maxSteps ?? 30;
|
|
2134
2248
|
const maxReprompts = opts.maxReprompts ?? 3;
|
|
2135
|
-
const
|
|
2249
|
+
const extra = opts.extraTools ?? [];
|
|
2250
|
+
const extraByName = new Map(extra.map((tl) => [tl.spec.name, tl]));
|
|
2251
|
+
const baseSpecs = toolSpecs();
|
|
2252
|
+
const finishSpec = baseSpecs[baseSpecs.length - 1];
|
|
2253
|
+
const allSpecs = [...baseSpecs.slice(0, -1), ...extra.map((tl) => tl.spec), finishSpec];
|
|
2254
|
+
const resolveTool = (name) => extraByName.get(name) ?? getTool(name);
|
|
2255
|
+
const driver = makeDriver(agent.toolMode, allSpecs);
|
|
2136
2256
|
const ctx = { workspace: opts.workspace, permissions };
|
|
2137
2257
|
const seeding = !(opts.history && opts.history.length > 0);
|
|
2138
2258
|
const promptContext = seeding && opts.promptContext.projectInstructions === void 0 ? { ...opts.promptContext, projectInstructions: await loadProjectInstructions(opts.workspace) } : opts.promptContext;
|
|
@@ -2146,9 +2266,22 @@ async function runAgent(opts) {
|
|
|
2146
2266
|
const maxToolRetries = opts.maxToolRetries ?? 3;
|
|
2147
2267
|
const autoCorrect = opts.autoCorrect ?? true;
|
|
2148
2268
|
const usage2 = { promptTokens: 0, completionTokens: 0 };
|
|
2269
|
+
const compactThreshold = opts.compactThresholdTokens ?? 0;
|
|
2270
|
+
let lastPromptTokens = 0;
|
|
2149
2271
|
for (let step = 1; step <= maxSteps; step++) {
|
|
2150
2272
|
if (opts.signal?.aborted) return { finished: false, reason: "cancelled", steps: step - 1, messages, usage: usage2 };
|
|
2151
2273
|
events?.onStep?.(step);
|
|
2274
|
+
if (compactThreshold > 0) {
|
|
2275
|
+
const current = lastPromptTokens || estimateTokens(messages);
|
|
2276
|
+
if (current >= compactThreshold) {
|
|
2277
|
+
const compacted = await compactHistory(messages, agent, opts.signal);
|
|
2278
|
+
if (compacted.length < messages.length) {
|
|
2279
|
+
messages.splice(0, messages.length, ...compacted);
|
|
2280
|
+
lastPromptTokens = estimateTokens(messages);
|
|
2281
|
+
events?.onCompaction?.(current, lastPromptTokens);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2152
2285
|
let response;
|
|
2153
2286
|
try {
|
|
2154
2287
|
response = await agent.provider.chat({
|
|
@@ -2163,6 +2296,7 @@ async function runAgent(opts) {
|
|
|
2163
2296
|
}
|
|
2164
2297
|
usage2.promptTokens += response.usage?.promptTokens ?? 0;
|
|
2165
2298
|
usage2.completionTokens += response.usage?.completionTokens ?? 0;
|
|
2299
|
+
lastPromptTokens = response.usage?.promptTokens ?? estimateTokens(messages);
|
|
2166
2300
|
events?.onUsage?.(usage2);
|
|
2167
2301
|
const { toolCalls, text: text2 } = driver.parse(response);
|
|
2168
2302
|
messages.push(driver.assistantMessage(response, toolCalls));
|
|
@@ -2199,8 +2333,21 @@ async function runAgent(opts) {
|
|
|
2199
2333
|
const summary = String(call.arguments.summary ?? "").trim();
|
|
2200
2334
|
return { finished: true, reason: "finished", summary, steps: step, messages, usage: usage2 };
|
|
2201
2335
|
}
|
|
2202
|
-
const tool =
|
|
2203
|
-
const
|
|
2336
|
+
const tool = resolveTool(call.name);
|
|
2337
|
+
const hookScreen = call.name === "run_command" ? screenCommandHook(opts.hooks, String(call.arguments.command ?? "")) : { blocked: false };
|
|
2338
|
+
let result;
|
|
2339
|
+
if (hookScreen.blocked) {
|
|
2340
|
+
result = { ok: false, output: `Command blocked by hook: ${hookScreen.reason}` };
|
|
2341
|
+
} else if (tool) {
|
|
2342
|
+
result = await tool.run(call.arguments, ctx);
|
|
2343
|
+
if (result.ok) {
|
|
2344
|
+
const note2 = await runAfterHook(opts.hooks, call, opts.workspace);
|
|
2345
|
+
if (note2) result = { ...result, output: `${result.output}
|
|
2346
|
+
${note2}` };
|
|
2347
|
+
}
|
|
2348
|
+
} else {
|
|
2349
|
+
result = { ok: false, output: `Unknown tool "${call.name}". Available: ${allSpecs.map((t2) => t2.name).join(", ")}` };
|
|
2350
|
+
}
|
|
2204
2351
|
events?.onToolResult?.(call, result);
|
|
2205
2352
|
const sig = `${call.name}:${JSON.stringify(call.arguments)}`;
|
|
2206
2353
|
let resultText = result.output;
|
|
@@ -2244,7 +2391,7 @@ ${guidance}`;
|
|
|
2244
2391
|
}
|
|
2245
2392
|
|
|
2246
2393
|
// src/core/context/mentions.ts
|
|
2247
|
-
import { readdir as readdir4, readFile as
|
|
2394
|
+
import { readdir as readdir4, readFile as readFile9, stat as stat2 } from "fs/promises";
|
|
2248
2395
|
import { resolve as resolve9 } from "path";
|
|
2249
2396
|
var MAX_FILE_CHARS = 1e4;
|
|
2250
2397
|
var MENTION_RE = /(?:^|\s)@([\w./-]+)/g;
|
|
@@ -2271,7 +2418,7 @@ ${t("mentions.notFound", { path: token })}`);
|
|
|
2271
2418
|
${listing || "(empty)"}`);
|
|
2272
2419
|
injected.push(decision.rel);
|
|
2273
2420
|
} else {
|
|
2274
|
-
const raw = await
|
|
2421
|
+
const raw = await readFile9(abs, "utf8");
|
|
2275
2422
|
const content = raw.length > MAX_FILE_CHARS ? raw.slice(0, MAX_FILE_CHARS) + "\n\u2026[truncated]" : raw;
|
|
2276
2423
|
blocks.push(`## @${decision.rel}
|
|
2277
2424
|
\`\`\`
|
|
@@ -2294,16 +2441,16 @@ ${blocks.join("\n\n")}`;
|
|
|
2294
2441
|
}
|
|
2295
2442
|
|
|
2296
2443
|
// src/core/agent/verify.ts
|
|
2297
|
-
import { exec as
|
|
2298
|
-
import { readFile as
|
|
2444
|
+
import { exec as exec3 } from "child_process";
|
|
2445
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2299
2446
|
import { resolve as resolve10 } from "path";
|
|
2300
|
-
import { promisify as
|
|
2301
|
-
var
|
|
2447
|
+
import { promisify as promisify3 } from "util";
|
|
2448
|
+
var execAsync3 = promisify3(exec3);
|
|
2302
2449
|
var MAX_OUTPUT3 = 8e3;
|
|
2303
2450
|
var CHECK_SCRIPTS = ["typecheck", "build", "test"];
|
|
2304
2451
|
async function detectChecks(workspace) {
|
|
2305
2452
|
try {
|
|
2306
|
-
const raw = await
|
|
2453
|
+
const raw = await readFile10(resolve10(workspace, "package.json"), "utf8");
|
|
2307
2454
|
const scripts = JSON.parse(raw).scripts ?? {};
|
|
2308
2455
|
return CHECK_SCRIPTS.filter((s) => typeof scripts[s] === "string").map((s) => `npm run ${s}`);
|
|
2309
2456
|
} catch {
|
|
@@ -2314,7 +2461,7 @@ async function runChecks(workspace, commands) {
|
|
|
2314
2461
|
const results = [];
|
|
2315
2462
|
for (const command of commands) {
|
|
2316
2463
|
try {
|
|
2317
|
-
const { stdout: stdout2, stderr } = await
|
|
2464
|
+
const { stdout: stdout2, stderr } = await execAsync3(command, {
|
|
2318
2465
|
cwd: workspace,
|
|
2319
2466
|
timeout: 3e5,
|
|
2320
2467
|
maxBuffer: 10 * 1024 * 1024,
|
|
@@ -2346,8 +2493,8 @@ function clamp2(s) {
|
|
|
2346
2493
|
}
|
|
2347
2494
|
|
|
2348
2495
|
// src/core/agent/usage.ts
|
|
2349
|
-
import { appendFile, mkdir as mkdir3, readFile as
|
|
2350
|
-
import { join as
|
|
2496
|
+
import { appendFile, mkdir as mkdir3, readFile as readFile11 } from "fs/promises";
|
|
2497
|
+
import { join as join5 } from "path";
|
|
2351
2498
|
|
|
2352
2499
|
// src/core/providers/openrouter.ts
|
|
2353
2500
|
var MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
@@ -2447,7 +2594,7 @@ function fmtUsd(n) {
|
|
|
2447
2594
|
return `US$${n.toFixed(2)}`;
|
|
2448
2595
|
}
|
|
2449
2596
|
function usagePath() {
|
|
2450
|
-
return
|
|
2597
|
+
return join5(configDir(), "usage.jsonl");
|
|
2451
2598
|
}
|
|
2452
2599
|
async function recordUsage(entry) {
|
|
2453
2600
|
try {
|
|
@@ -2459,7 +2606,7 @@ async function recordUsage(entry) {
|
|
|
2459
2606
|
async function aggregateUsage() {
|
|
2460
2607
|
let text2 = "";
|
|
2461
2608
|
try {
|
|
2462
|
-
text2 = await
|
|
2609
|
+
text2 = await readFile11(usagePath(), "utf8");
|
|
2463
2610
|
} catch {
|
|
2464
2611
|
return { days: [], total: emptyBucket("total") };
|
|
2465
2612
|
}
|
|
@@ -2493,10 +2640,10 @@ function accumulate(bucket, e) {
|
|
|
2493
2640
|
}
|
|
2494
2641
|
|
|
2495
2642
|
// src/core/agent/session-store.ts
|
|
2496
|
-
import { mkdir as mkdir4, readFile as
|
|
2497
|
-
import { join as
|
|
2643
|
+
import { mkdir as mkdir4, readFile as readFile12, readdir as readdir5, writeFile as writeFile4 } from "fs/promises";
|
|
2644
|
+
import { join as join6 } from "path";
|
|
2498
2645
|
function sessionsDir() {
|
|
2499
|
-
return
|
|
2646
|
+
return join6(configDir(), "sessions");
|
|
2500
2647
|
}
|
|
2501
2648
|
function newSessionId() {
|
|
2502
2649
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -2504,7 +2651,7 @@ function newSessionId() {
|
|
|
2504
2651
|
return `${stamp}-${rand}`;
|
|
2505
2652
|
}
|
|
2506
2653
|
function sessionPath(id) {
|
|
2507
|
-
return
|
|
2654
|
+
return join6(sessionsDir(), `${id}.json`);
|
|
2508
2655
|
}
|
|
2509
2656
|
async function saveSession(record) {
|
|
2510
2657
|
await mkdir4(sessionsDir(), { recursive: true });
|
|
@@ -2516,7 +2663,7 @@ async function saveSession(record) {
|
|
|
2516
2663
|
}
|
|
2517
2664
|
async function loadSession(id) {
|
|
2518
2665
|
try {
|
|
2519
|
-
return JSON.parse(await
|
|
2666
|
+
return JSON.parse(await readFile12(sessionPath(id), "utf8"));
|
|
2520
2667
|
} catch {
|
|
2521
2668
|
return void 0;
|
|
2522
2669
|
}
|
|
@@ -2531,7 +2678,7 @@ async function listSessions() {
|
|
|
2531
2678
|
const summaries = [];
|
|
2532
2679
|
for (const f of files) {
|
|
2533
2680
|
try {
|
|
2534
|
-
const r = JSON.parse(await
|
|
2681
|
+
const r = JSON.parse(await readFile12(join6(sessionsDir(), f), "utf8"));
|
|
2535
2682
|
summaries.push({
|
|
2536
2683
|
id: r.id,
|
|
2537
2684
|
updatedAt: r.updatedAt,
|
|
@@ -2555,6 +2702,83 @@ function deriveTitle(messages) {
|
|
|
2555
2702
|
return text2.length > 60 ? text2.slice(0, 60) + "\u2026" : text2 || "(untitled)";
|
|
2556
2703
|
}
|
|
2557
2704
|
|
|
2705
|
+
// src/core/tools/custom.ts
|
|
2706
|
+
import { exec as exec4 } from "child_process";
|
|
2707
|
+
import { readFile as readFile13, readdir as readdir6 } from "fs/promises";
|
|
2708
|
+
import { join as join7 } from "path";
|
|
2709
|
+
import { promisify as promisify4 } from "util";
|
|
2710
|
+
import { z as z9 } from "zod";
|
|
2711
|
+
var execAsync4 = promisify4(exec4);
|
|
2712
|
+
var MAX_OUTPUT4 = 2e4;
|
|
2713
|
+
var CustomToolSchema = z9.object({
|
|
2714
|
+
name: z9.string().min(1).regex(/^[a-z][a-z0-9_]*$/i, "tool name must be alphanumeric/underscore"),
|
|
2715
|
+
description: z9.string().min(1),
|
|
2716
|
+
/** JSON-schema object for the tool parameters (advertised to the model). */
|
|
2717
|
+
parameters: z9.record(z9.unknown()).optional(),
|
|
2718
|
+
/** Shell command template; `{argName}` placeholders are filled from the call arguments. */
|
|
2719
|
+
command: z9.string().min(1)
|
|
2720
|
+
});
|
|
2721
|
+
function makeCommandTool(def) {
|
|
2722
|
+
return {
|
|
2723
|
+
mutating: true,
|
|
2724
|
+
spec: {
|
|
2725
|
+
name: def.name,
|
|
2726
|
+
description: def.description,
|
|
2727
|
+
parameters: def.parameters ?? { type: "object", properties: {} }
|
|
2728
|
+
},
|
|
2729
|
+
async run(rawArgs, ctx) {
|
|
2730
|
+
const command = fillTemplate(def.command, rawArgs);
|
|
2731
|
+
const decision = await ctx.permissions.authorizeCommand(command);
|
|
2732
|
+
if (!decision.allowed) return { ok: false, output: `Command denied: ${decision.reason}` };
|
|
2733
|
+
try {
|
|
2734
|
+
const { stdout: stdout2, stderr } = await execAsync4(command, {
|
|
2735
|
+
cwd: ctx.workspace,
|
|
2736
|
+
timeout: 12e4,
|
|
2737
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2738
|
+
windowsHide: true
|
|
2739
|
+
});
|
|
2740
|
+
return { ok: true, output: clamp3(`${stdout2}${stderr ? `
|
|
2741
|
+
[stderr]
|
|
2742
|
+
${stderr}` : ""}`.trim() || "(no output)") };
|
|
2743
|
+
} catch (err) {
|
|
2744
|
+
const e = err;
|
|
2745
|
+
return {
|
|
2746
|
+
ok: false,
|
|
2747
|
+
output: clamp3(`Command failed (exit ${e.code ?? "?"}): ${e.message}
|
|
2748
|
+
${e.stdout ?? ""}${e.stderr ?? ""}`)
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
async function loadCustomTools(workspace) {
|
|
2755
|
+
let files;
|
|
2756
|
+
try {
|
|
2757
|
+
files = (await readdir6(join7(workspace, ".poly", "tools"))).filter((f) => f.endsWith(".json"));
|
|
2758
|
+
} catch {
|
|
2759
|
+
return [];
|
|
2760
|
+
}
|
|
2761
|
+
const tools = [];
|
|
2762
|
+
for (const f of files) {
|
|
2763
|
+
try {
|
|
2764
|
+
const raw = await readFile13(join7(workspace, ".poly", "tools", f), "utf8");
|
|
2765
|
+
const parsed = CustomToolSchema.safeParse(JSON.parse(raw));
|
|
2766
|
+
if (parsed.success) tools.push(makeCommandTool(parsed.data));
|
|
2767
|
+
} catch {
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
return tools;
|
|
2771
|
+
}
|
|
2772
|
+
function fillTemplate(template, args) {
|
|
2773
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
2774
|
+
const v = args[key];
|
|
2775
|
+
return v === void 0 || v === null ? "" : String(v);
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
function clamp3(s) {
|
|
2779
|
+
return s.length > MAX_OUTPUT4 ? s.slice(0, MAX_OUTPUT4) + "\n\u2026[truncated]" : s;
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2558
2782
|
// src/cli/commands/json-output.ts
|
|
2559
2783
|
var OUTPUT_PREVIEW = 500;
|
|
2560
2784
|
function createJsonCollector() {
|
|
@@ -2588,6 +2812,9 @@ function createJsonCollector() {
|
|
|
2588
2812
|
onReprompt(attempt) {
|
|
2589
2813
|
log.push({ type: "reprompt", attempt });
|
|
2590
2814
|
},
|
|
2815
|
+
onCompaction(before, after) {
|
|
2816
|
+
log.push({ type: "compaction", before, after });
|
|
2817
|
+
},
|
|
2591
2818
|
onUsage() {
|
|
2592
2819
|
}
|
|
2593
2820
|
};
|
|
@@ -3350,7 +3577,7 @@ import pc7 from "picocolors";
|
|
|
3350
3577
|
// src/core/git/worktree.ts
|
|
3351
3578
|
import { mkdtemp } from "fs/promises";
|
|
3352
3579
|
import { tmpdir } from "os";
|
|
3353
|
-
import { join as
|
|
3580
|
+
import { join as join8 } from "path";
|
|
3354
3581
|
import { simpleGit } from "simple-git";
|
|
3355
3582
|
async function ensureRepo(workspace) {
|
|
3356
3583
|
const git = simpleGit(workspace);
|
|
@@ -3371,7 +3598,7 @@ async function identityArgs(git) {
|
|
|
3371
3598
|
}
|
|
3372
3599
|
async function createWorktree(git, label) {
|
|
3373
3600
|
const branch = `polypus/${label}-${Date.now().toString(36)}`;
|
|
3374
|
-
const path = await mkdtemp(
|
|
3601
|
+
const path = await mkdtemp(join8(tmpdir(), "polypus-wt-"));
|
|
3375
3602
|
await git.raw(["worktree", "add", "-b", branch, path, "HEAD"]);
|
|
3376
3603
|
return { path, branch };
|
|
3377
3604
|
}
|
|
@@ -3800,6 +4027,11 @@ var Spinner = class {
|
|
|
3800
4027
|
|
|
3801
4028
|
// src/cli/commands/run.ts
|
|
3802
4029
|
var MAX_VERIFY_FIXES = 3;
|
|
4030
|
+
function compactionThreshold() {
|
|
4031
|
+
if (process.env.POLYPUS_NO_COMPACT) return 0;
|
|
4032
|
+
const v = Number(process.env.POLYPUS_COMPACT_THRESHOLD);
|
|
4033
|
+
return Number.isFinite(v) && v > 0 ? v : 12e4;
|
|
4034
|
+
}
|
|
3803
4035
|
async function run(task, opts) {
|
|
3804
4036
|
let config = await loadConfig();
|
|
3805
4037
|
const workspace = process.cwd();
|
|
@@ -3917,6 +4149,10 @@ async function executeTask(task, resolved, workspace, session, json = false, ver
|
|
|
3917
4149
|
return ok;
|
|
3918
4150
|
}
|
|
3919
4151
|
});
|
|
4152
|
+
const [extraTools, hooks] = await Promise.all([loadCustomTools(workspace), loadHooks(workspace)]);
|
|
4153
|
+
if (!json && extraTools.length > 0) {
|
|
4154
|
+
console.log(pc8.dim(t("tools.customLoaded", { names: extraTools.map((tl) => tl.spec.name).join(", ") })));
|
|
4155
|
+
}
|
|
3920
4156
|
const runOnce = (taskText) => runAgent({
|
|
3921
4157
|
task: taskText,
|
|
3922
4158
|
workspace,
|
|
@@ -3925,6 +4161,9 @@ async function executeTask(task, resolved, workspace, session, json = false, ver
|
|
|
3925
4161
|
promptContext: { workspace, mode: session.mode, allow: session.allow },
|
|
3926
4162
|
history: session.history,
|
|
3927
4163
|
maxSteps: session.maxSteps,
|
|
4164
|
+
compactThresholdTokens: compactionThreshold(),
|
|
4165
|
+
extraTools,
|
|
4166
|
+
hooks,
|
|
3928
4167
|
signal: controller.signal,
|
|
3929
4168
|
events
|
|
3930
4169
|
});
|
|
@@ -4108,6 +4347,10 @@ function renderEvents(spinner3) {
|
|
|
4108
4347
|
spinner3.stop();
|
|
4109
4348
|
console.log(pc8.yellow(" " + t("run.reprompt", { attempt })));
|
|
4110
4349
|
},
|
|
4350
|
+
onCompaction(before, after) {
|
|
4351
|
+
spinner3.stop();
|
|
4352
|
+
console.log(pc8.dim("\u21AF " + t("compaction.done", { before: fmtTokens(before), after: fmtTokens(after) })));
|
|
4353
|
+
},
|
|
4111
4354
|
onCorrection() {
|
|
4112
4355
|
spinner3.stop();
|
|
4113
4356
|
console.log(pc8.yellow(" \u21BB " + t("run.autocorrect")));
|
|
@@ -4125,7 +4368,7 @@ import pc9 from "picocolors";
|
|
|
4125
4368
|
|
|
4126
4369
|
// src/core/scaffold/init.ts
|
|
4127
4370
|
import { mkdir as mkdir5, writeFile as writeFile5, access } from "fs/promises";
|
|
4128
|
-
import { dirname as dirname3, join as
|
|
4371
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
4129
4372
|
|
|
4130
4373
|
// src/core/scaffold/templates.ts
|
|
4131
4374
|
function polyTemplates(locale) {
|
|
@@ -4368,7 +4611,7 @@ async function scaffoldPoly(workspace, opts) {
|
|
|
4368
4611
|
const skipped = [];
|
|
4369
4612
|
for (const [rel, content] of Object.entries(templates)) {
|
|
4370
4613
|
const display = `.poly/${rel}`;
|
|
4371
|
-
const abs =
|
|
4614
|
+
const abs = join9(workspace, ".poly", ...rel.split("/"));
|
|
4372
4615
|
if (!opts.force && await exists(abs)) {
|
|
4373
4616
|
skipped.push(display);
|
|
4374
4617
|
continue;
|
|
@@ -4508,9 +4751,9 @@ async function sessions() {
|
|
|
4508
4751
|
}
|
|
4509
4752
|
|
|
4510
4753
|
// src/cli/commands/prd.ts
|
|
4511
|
-
import { writeFile as writeFile6, readFile as
|
|
4754
|
+
import { writeFile as writeFile6, readFile as readFile14 } from "fs/promises";
|
|
4512
4755
|
import { execFile } from "child_process";
|
|
4513
|
-
import { promisify as
|
|
4756
|
+
import { promisify as promisify5 } from "util";
|
|
4514
4757
|
import pc13 from "picocolors";
|
|
4515
4758
|
|
|
4516
4759
|
// src/core/agent/prd.ts
|
|
@@ -4632,7 +4875,7 @@ function stripBom(s) {
|
|
|
4632
4875
|
}
|
|
4633
4876
|
|
|
4634
4877
|
// src/cli/commands/prd.ts
|
|
4635
|
-
var
|
|
4878
|
+
var exec5 = promisify5(execFile);
|
|
4636
4879
|
async function prd(issueRef, opts) {
|
|
4637
4880
|
const issue = await loadIssue(issueRef, opts.input);
|
|
4638
4881
|
const { provider } = resolveFreeProvider(opts.model ?? DEFAULT_PRD_MODEL);
|
|
@@ -4647,11 +4890,11 @@ async function prd(issueRef, opts) {
|
|
|
4647
4890
|
}
|
|
4648
4891
|
async function loadIssue(issueRef, input) {
|
|
4649
4892
|
if (input) {
|
|
4650
|
-
const raw = input === "-" ? await readStdin() : await
|
|
4893
|
+
const raw = input === "-" ? await readStdin() : await readFile14(input, "utf8");
|
|
4651
4894
|
return normalize2(JSON.parse(stripBom(raw)));
|
|
4652
4895
|
}
|
|
4653
4896
|
const num = numericRef(issueRef);
|
|
4654
|
-
const { stdout: stdout2 } = await
|
|
4897
|
+
const { stdout: stdout2 } = await exec5("gh", ["issue", "view", num, "--json", "number,title,body,comments"]);
|
|
4655
4898
|
const data = normalize2(JSON.parse(stdout2));
|
|
4656
4899
|
data.number ??= Number(num);
|
|
4657
4900
|
return data;
|
|
@@ -4666,9 +4909,9 @@ function normalize2(raw) {
|
|
|
4666
4909
|
}
|
|
4667
4910
|
|
|
4668
4911
|
// src/cli/commands/review.ts
|
|
4669
|
-
import { writeFile as writeFile7, readFile as
|
|
4912
|
+
import { writeFile as writeFile7, readFile as readFile15 } from "fs/promises";
|
|
4670
4913
|
import { execFile as execFile2 } from "child_process";
|
|
4671
|
-
import { promisify as
|
|
4914
|
+
import { promisify as promisify6 } from "util";
|
|
4672
4915
|
import pc14 from "picocolors";
|
|
4673
4916
|
|
|
4674
4917
|
// src/core/agent/review.ts
|
|
@@ -4726,7 +4969,7 @@ ${projectGuide}`
|
|
|
4726
4969
|
}
|
|
4727
4970
|
|
|
4728
4971
|
// src/cli/commands/review.ts
|
|
4729
|
-
var
|
|
4972
|
+
var exec6 = promisify6(execFile2);
|
|
4730
4973
|
async function review(prRef, opts) {
|
|
4731
4974
|
const num = opts.input ? prRef.replace(/^#/, "") : numericRef(prRef);
|
|
4732
4975
|
const diff = await loadDiff(num, opts.input);
|
|
@@ -4742,19 +4985,19 @@ async function review(prRef, opts) {
|
|
|
4742
4985
|
}
|
|
4743
4986
|
}
|
|
4744
4987
|
async function loadDiff(num, input) {
|
|
4745
|
-
if (input) return input === "-" ? readStdin() :
|
|
4746
|
-
const { stdout: stdout2 } = await
|
|
4988
|
+
if (input) return input === "-" ? readStdin() : readFile15(input, "utf8");
|
|
4989
|
+
const { stdout: stdout2 } = await exec6("gh", ["pr", "diff", num]);
|
|
4747
4990
|
return stdout2;
|
|
4748
4991
|
}
|
|
4749
4992
|
async function loadMeta(num, input) {
|
|
4750
4993
|
if (input) return { number: Number(num) || void 0, title: `PR ${num}`, body: "" };
|
|
4751
|
-
const { stdout: stdout2 } = await
|
|
4994
|
+
const { stdout: stdout2 } = await exec6("gh", ["pr", "view", num, "--json", "number,title,body"]);
|
|
4752
4995
|
const raw = JSON.parse(stdout2);
|
|
4753
4996
|
return { number: raw.number, title: raw.title ?? "", body: raw.body ?? "" };
|
|
4754
4997
|
}
|
|
4755
4998
|
|
|
4756
4999
|
// src/cli/index.ts
|
|
4757
|
-
import { join as
|
|
5000
|
+
import { join as join10 } from "path";
|
|
4758
5001
|
|
|
4759
5002
|
// src/core/config/dotenv.ts
|
|
4760
5003
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
@@ -4825,7 +5068,7 @@ function buildProgram() {
|
|
|
4825
5068
|
}
|
|
4826
5069
|
async function main() {
|
|
4827
5070
|
try {
|
|
4828
|
-
loadDotenv([
|
|
5071
|
+
loadDotenv([join10(configDir(), ".env"), join10(process.cwd(), ".env")]);
|
|
4829
5072
|
await resolveLocale();
|
|
4830
5073
|
await buildProgram().parseAsync(process.argv);
|
|
4831
5074
|
} catch (err) {
|