@gaberrb/polypus 0.4.4 → 0.4.6
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 +271 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -137,6 +137,10 @@ var en = {
|
|
|
137
137
|
"run.autocorrect": "\u21BB tool failed \u2014 auto-correcting with extra context",
|
|
138
138
|
"run.cancelled": "\u25A0 cancelled",
|
|
139
139
|
"run.jsonNeedsTask": "--json requires a task argument (headless mode has no interactive REPL).",
|
|
140
|
+
"review.approveAll": "approve all",
|
|
141
|
+
"review.reject": "reject",
|
|
142
|
+
"review.pickHunks": "pick hunks\u2026",
|
|
143
|
+
"review.selectHunks": "Select the hunks to apply (space to toggle, enter to confirm)",
|
|
140
144
|
// repl
|
|
141
145
|
"repl.welcome": "Polypus interactive session.",
|
|
142
146
|
"repl.welcomeHint": " Type /help for commands, /exit to quit.",
|
|
@@ -291,7 +295,10 @@ var en = {
|
|
|
291
295
|
// @-mentions
|
|
292
296
|
"mentions.injectedHeader": "Referenced files (@-mentions)",
|
|
293
297
|
"mentions.dirHeader": "@{path} (directory listing)",
|
|
294
|
-
"mentions.notFound": "(could not resolve @{path}: not found or outside the allow-list)"
|
|
298
|
+
"mentions.notFound": "(could not resolve @{path}: not found or outside the allow-list)",
|
|
299
|
+
// safety policy
|
|
300
|
+
"policy.blockedCommand": "blocked by safety policy ({reason}) \u2014 refusing in all modes",
|
|
301
|
+
"policy.secretFound": "write blocked: a possible secret ({kind}) was found on line {line}. Remove it or load it from an environment variable instead of hard-coding it."
|
|
295
302
|
};
|
|
296
303
|
var ptBR = {
|
|
297
304
|
"common.default": "padr\xE3o",
|
|
@@ -362,6 +369,10 @@ var ptBR = {
|
|
|
362
369
|
"run.autocorrect": "\u21BB tool falhou \u2014 autocorrigindo com contexto extra",
|
|
363
370
|
"run.cancelled": "\u25A0 cancelado",
|
|
364
371
|
"run.jsonNeedsTask": "--json exige um argumento de tarefa (o modo headless n\xE3o tem REPL interativo).",
|
|
372
|
+
"review.approveAll": "aprovar tudo",
|
|
373
|
+
"review.reject": "rejeitar",
|
|
374
|
+
"review.pickHunks": "escolher hunks\u2026",
|
|
375
|
+
"review.selectHunks": "Selecione os hunks a aplicar (espa\xE7o alterna, enter confirma)",
|
|
365
376
|
"repl.welcome": "Sess\xE3o interativa do Polypus.",
|
|
366
377
|
"repl.welcomeHint": " Digite /help para comandos, /exit para sair.",
|
|
367
378
|
"repl.modeChanged": "modo \u2192 {mode}",
|
|
@@ -490,6 +501,9 @@ var ptBR = {
|
|
|
490
501
|
"mentions.injectedHeader": "Arquivos referenciados (@-mentions)",
|
|
491
502
|
"mentions.dirHeader": "@{path} (conte\xFAdo do diret\xF3rio)",
|
|
492
503
|
"mentions.notFound": "(n\xE3o foi poss\xEDvel resolver @{path}: n\xE3o encontrado ou fora da allow-list)",
|
|
504
|
+
// safety policy
|
|
505
|
+
"policy.blockedCommand": "bloqueado pela pol\xEDtica de seguran\xE7a ({reason}) \u2014 recusado em todos os modos",
|
|
506
|
+
"policy.secretFound": "escrita bloqueada: poss\xEDvel segredo ({kind}) encontrado na linha {line}. Remova-o ou carregue de uma vari\xE1vel de ambiente em vez de fix\xE1-lo no c\xF3digo.",
|
|
493
507
|
"models.fetching": "Buscando modelos do OpenRouter\u2026",
|
|
494
508
|
"models.fetchError": "N\xE3o foi poss\xEDvel buscar modelos: {msg}",
|
|
495
509
|
"models.none": "Nenhum modelo corresponde aos filtros.",
|
|
@@ -957,6 +971,10 @@ function createProvider(agent) {
|
|
|
957
971
|
return { config: agent, provider, toolMode: resolveToolMode(agent) };
|
|
958
972
|
}
|
|
959
973
|
|
|
974
|
+
// src/core/permissions/modes.ts
|
|
975
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
976
|
+
import { resolve as resolve2 } from "path";
|
|
977
|
+
|
|
960
978
|
// src/core/permissions/allowlist.ts
|
|
961
979
|
import { isAbsolute, relative, resolve, sep } from "path";
|
|
962
980
|
function globToRegExp(glob) {
|
|
@@ -1005,6 +1023,158 @@ function isCommandPreApproved(allowedCommands, command) {
|
|
|
1005
1023
|
return allowedCommands.some((prefix) => c === prefix || c.startsWith(prefix.trim() + " "));
|
|
1006
1024
|
}
|
|
1007
1025
|
|
|
1026
|
+
// src/core/permissions/policy.ts
|
|
1027
|
+
var DANGEROUS_COMMANDS = [
|
|
1028
|
+
{ re: /--no-preserve-root/i, reason: "rm --no-preserve-root" },
|
|
1029
|
+
{ re: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, reason: "fork bomb" },
|
|
1030
|
+
{ re: /\bmkfs(\.\w+)?\b/i, reason: "filesystem format (mkfs)" },
|
|
1031
|
+
{ re: /\bdd\b[^\n]*\bof=\/dev\/(sd|nvme|hd|disk)/i, reason: "dd writing to a raw disk device" },
|
|
1032
|
+
{ re: />\s*\/dev\/(sd|nvme|hd|disk)/i, reason: "redirect to a raw disk device" },
|
|
1033
|
+
{ re: /\bchmod\s+(-[a-z]*\s+)*-?R?\s*777\s+\//i, reason: "chmod 777 on /" },
|
|
1034
|
+
{ re: /\b(curl|wget)\b[^\n|]*\|\s*(sudo\s+)?(sh|bash|zsh)\b/i, reason: "piping a downloaded script straight into a shell" }
|
|
1035
|
+
];
|
|
1036
|
+
function isDangerousRm(command) {
|
|
1037
|
+
if (!/\brm\b/i.test(command)) return false;
|
|
1038
|
+
const recursive = /(?:^|\s)-[a-z]*r[a-z]*f/i.test(command) || /(?:^|\s)-[a-z]*f[a-z]*r/i.test(command) || /(?:^|\s)-[a-z]*r\b/i.test(command) && /(?:^|\s)-[a-z]*f\b/i.test(command);
|
|
1039
|
+
if (!recursive) return false;
|
|
1040
|
+
return /(?:\s|^)(?:\/\*|\/|~|\$HOME|\*)(?:\s|$)/.test(command);
|
|
1041
|
+
}
|
|
1042
|
+
function screenCommand(command) {
|
|
1043
|
+
if (isDangerousRm(command)) {
|
|
1044
|
+
return { blocked: true, reason: "recursive force-delete of / ~ or *" };
|
|
1045
|
+
}
|
|
1046
|
+
for (const { re, reason } of DANGEROUS_COMMANDS) {
|
|
1047
|
+
if (re.test(command)) return { blocked: true, reason };
|
|
1048
|
+
}
|
|
1049
|
+
return { blocked: false };
|
|
1050
|
+
}
|
|
1051
|
+
var SECRET_PATTERNS = [
|
|
1052
|
+
{ re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/, kind: "private key block" },
|
|
1053
|
+
{ re: /\bAKIA[0-9A-Z]{16}\b/, kind: "AWS access key id" },
|
|
1054
|
+
{ re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/, kind: "GitHub token" },
|
|
1055
|
+
{ re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, kind: "Slack token" },
|
|
1056
|
+
{ re: /\bsk-[A-Za-z0-9]{32,}\b/, kind: "OpenAI-style secret key" },
|
|
1057
|
+
{ re: /\bAIza[0-9A-Za-z_-]{35}\b/, kind: "Google API key" }
|
|
1058
|
+
];
|
|
1059
|
+
function scanSecrets(text2) {
|
|
1060
|
+
const findings = [];
|
|
1061
|
+
const lines = text2.split("\n");
|
|
1062
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1063
|
+
for (const { re, kind } of SECRET_PATTERNS) {
|
|
1064
|
+
if (re.test(lines[i])) {
|
|
1065
|
+
findings.push({ line: i + 1, kind });
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return findings;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// src/core/permissions/diff.ts
|
|
1074
|
+
var CONTEXT = 3;
|
|
1075
|
+
function splitLines(text2) {
|
|
1076
|
+
return text2 === "" ? [] : text2.split("\n");
|
|
1077
|
+
}
|
|
1078
|
+
function lcsOps(a, b) {
|
|
1079
|
+
const n = a.length;
|
|
1080
|
+
const m = b.length;
|
|
1081
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
1082
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
1083
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
1084
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const out = [];
|
|
1088
|
+
let i = 0;
|
|
1089
|
+
let j = 0;
|
|
1090
|
+
while (i < n && j < m) {
|
|
1091
|
+
if (a[i] === b[j]) {
|
|
1092
|
+
out.push({ type: " ", text: a[i] });
|
|
1093
|
+
i++;
|
|
1094
|
+
j++;
|
|
1095
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
1096
|
+
out.push({ type: "-", text: a[i] });
|
|
1097
|
+
i++;
|
|
1098
|
+
} else {
|
|
1099
|
+
out.push({ type: "+", text: b[j] });
|
|
1100
|
+
j++;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
while (i < n) out.push({ type: "-", text: a[i++] });
|
|
1104
|
+
while (j < m) out.push({ type: "+", text: b[j++] });
|
|
1105
|
+
return out;
|
|
1106
|
+
}
|
|
1107
|
+
function computeHunks(oldText, newText) {
|
|
1108
|
+
const a = splitLines(oldText);
|
|
1109
|
+
const b = splitLines(newText);
|
|
1110
|
+
const ops = lcsOps(a, b);
|
|
1111
|
+
const isChange = ops.map((o) => o.type !== " ");
|
|
1112
|
+
const keep = new Array(ops.length).fill(false);
|
|
1113
|
+
for (let k2 = 0; k2 < ops.length; k2++) {
|
|
1114
|
+
if (isChange[k2]) {
|
|
1115
|
+
for (let c = Math.max(0, k2 - CONTEXT); c <= Math.min(ops.length - 1, k2 + CONTEXT); c++) {
|
|
1116
|
+
keep[c] = true;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
const hunks = [];
|
|
1121
|
+
let oldLine = 0;
|
|
1122
|
+
let newLine = 0;
|
|
1123
|
+
let k = 0;
|
|
1124
|
+
while (k < ops.length) {
|
|
1125
|
+
const op = ops[k];
|
|
1126
|
+
if (!keep[k]) {
|
|
1127
|
+
if (op.type !== "+") oldLine++;
|
|
1128
|
+
if (op.type !== "-") newLine++;
|
|
1129
|
+
k++;
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
const oldStart = oldLine;
|
|
1133
|
+
const newStart = newLine;
|
|
1134
|
+
const lines = [];
|
|
1135
|
+
let oldCount = 0;
|
|
1136
|
+
let newCount = 0;
|
|
1137
|
+
while (k < ops.length && keep[k]) {
|
|
1138
|
+
const cur = ops[k];
|
|
1139
|
+
lines.push(cur);
|
|
1140
|
+
if (cur.type !== "+") {
|
|
1141
|
+
oldLine++;
|
|
1142
|
+
oldCount++;
|
|
1143
|
+
}
|
|
1144
|
+
if (cur.type !== "-") {
|
|
1145
|
+
newLine++;
|
|
1146
|
+
newCount++;
|
|
1147
|
+
}
|
|
1148
|
+
k++;
|
|
1149
|
+
}
|
|
1150
|
+
hunks.push({ oldStart, oldCount, newStart, newCount, lines });
|
|
1151
|
+
}
|
|
1152
|
+
return hunks;
|
|
1153
|
+
}
|
|
1154
|
+
function applyHunks(oldText, hunks, approved) {
|
|
1155
|
+
const a = splitLines(oldText);
|
|
1156
|
+
const out = [];
|
|
1157
|
+
let oldIdx = 0;
|
|
1158
|
+
hunks.forEach((hunk, idx) => {
|
|
1159
|
+
while (oldIdx < hunk.oldStart) out.push(a[oldIdx++]);
|
|
1160
|
+
if (approved.has(idx)) {
|
|
1161
|
+
for (const l of hunk.lines) if (l.type !== "-") out.push(l.text);
|
|
1162
|
+
} else {
|
|
1163
|
+
for (const l of hunk.lines) if (l.type !== "+") out.push(l.text);
|
|
1164
|
+
}
|
|
1165
|
+
oldIdx = hunk.oldStart + hunk.oldCount;
|
|
1166
|
+
});
|
|
1167
|
+
while (oldIdx < a.length) out.push(a[oldIdx++]);
|
|
1168
|
+
return out.join("\n");
|
|
1169
|
+
}
|
|
1170
|
+
function hunkLabel(hunk) {
|
|
1171
|
+
const added = hunk.lines.filter((l) => l.type === "+").length;
|
|
1172
|
+
const removed = hunk.lines.filter((l) => l.type === "-").length;
|
|
1173
|
+
const firstChange = hunk.lines.find((l) => l.type !== " ");
|
|
1174
|
+
const preview = firstChange ? firstChange.text.trim().slice(0, 50) : "";
|
|
1175
|
+
return `@@ -${hunk.oldStart + 1},${hunk.oldCount} +${hunk.newStart + 1},${hunk.newCount} @@ (+${added}/-${removed}) ${preview}`;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1008
1178
|
// src/core/permissions/modes.ts
|
|
1009
1179
|
var PermissionEngine = class {
|
|
1010
1180
|
constructor(opts) {
|
|
@@ -1019,24 +1189,48 @@ var PermissionEngine = class {
|
|
|
1019
1189
|
const d = checkPath(this.opts.policy, target);
|
|
1020
1190
|
return d.allowed ? { allowed: true } : { allowed: false, reason: d.reason };
|
|
1021
1191
|
}
|
|
1022
|
-
async authorizeWrite(target, preview) {
|
|
1192
|
+
async authorizeWrite(target, preview, content) {
|
|
1023
1193
|
const d = checkPath(this.opts.policy, target);
|
|
1024
1194
|
if (!d.allowed) return { allowed: false, reason: d.reason };
|
|
1195
|
+
let oldContent = "";
|
|
1196
|
+
try {
|
|
1197
|
+
oldContent = await readFile2(resolve2(this.opts.policy.workspace, target), "utf8");
|
|
1198
|
+
} catch {
|
|
1199
|
+
}
|
|
1200
|
+
const hunks = content !== void 0 ? computeHunks(oldContent, content) : [];
|
|
1201
|
+
const added = hunks.flatMap((h) => h.lines.filter((l) => l.type === "+").map((l) => l.text)).join("\n");
|
|
1202
|
+
const findings = scanSecrets(hunks.length > 0 ? added : content ?? preview ?? "");
|
|
1203
|
+
if (findings.length > 0) {
|
|
1204
|
+
const first = findings[0];
|
|
1205
|
+
return {
|
|
1206
|
+
allowed: false,
|
|
1207
|
+
reason: t("policy.secretFound", { kind: first.kind, line: first.line })
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1025
1210
|
if (this.opts.mode === "plan") {
|
|
1026
1211
|
return { allowed: false, reason: "plan mode: file modifications are disabled" };
|
|
1027
1212
|
}
|
|
1028
1213
|
if (this.opts.mode === "bypass") return { allowed: true };
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1214
|
+
const res = await this.ask({ kind: "write", summary: `write ${d.rel}`, preview, hunks });
|
|
1215
|
+
if (res === true) return { allowed: true };
|
|
1216
|
+
if (res === false) return { allowed: false, reason: "rejected by user" };
|
|
1217
|
+
const approved = new Set(res);
|
|
1218
|
+
if (approved.size === 0) return { allowed: false, reason: "rejected by user" };
|
|
1219
|
+
if (approved.size === hunks.length) return { allowed: true };
|
|
1220
|
+
return { allowed: true, content: applyHunks(oldContent, hunks, approved) };
|
|
1031
1221
|
}
|
|
1032
1222
|
async authorizeCommand(command) {
|
|
1223
|
+
const screen = screenCommand(command);
|
|
1224
|
+
if (screen.blocked) {
|
|
1225
|
+
return { allowed: false, reason: t("policy.blockedCommand", { reason: screen.reason ?? "" }) };
|
|
1226
|
+
}
|
|
1033
1227
|
if (this.opts.mode === "plan") {
|
|
1034
1228
|
return { allowed: false, reason: "plan mode: running commands is disabled" };
|
|
1035
1229
|
}
|
|
1036
1230
|
if (this.opts.mode === "bypass") return { allowed: true };
|
|
1037
1231
|
if (isCommandPreApproved(this.opts.allowedCommands, command)) return { allowed: true };
|
|
1038
|
-
const
|
|
1039
|
-
return
|
|
1232
|
+
const res = await this.ask({ kind: "command", summary: `run: ${command}` });
|
|
1233
|
+
return res === true ? { allowed: true } : { allowed: false, reason: "rejected by user" };
|
|
1040
1234
|
}
|
|
1041
1235
|
async ask(req) {
|
|
1042
1236
|
if (!this.opts.confirm) return false;
|
|
@@ -1265,8 +1459,8 @@ function makeDriver(kind, tools) {
|
|
|
1265
1459
|
}
|
|
1266
1460
|
|
|
1267
1461
|
// src/core/tools/edit-file.ts
|
|
1268
|
-
import { readFile as
|
|
1269
|
-
import { resolve as
|
|
1462
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
1463
|
+
import { resolve as resolve3 } from "path";
|
|
1270
1464
|
import { z as z2 } from "zod";
|
|
1271
1465
|
var Args = z2.object({
|
|
1272
1466
|
path: z2.string().min(1),
|
|
@@ -1296,10 +1490,10 @@ var editFileTool = {
|
|
|
1296
1490
|
output: "edit_file needs three arguments: 'path', 'search' (exact text to find), and 'replace'. Resend the tool call with all three filled in."
|
|
1297
1491
|
};
|
|
1298
1492
|
}
|
|
1299
|
-
const abs =
|
|
1493
|
+
const abs = resolve3(ctx.workspace, args.data.path);
|
|
1300
1494
|
let content;
|
|
1301
1495
|
try {
|
|
1302
|
-
content = await
|
|
1496
|
+
content = await readFile3(abs, "utf8");
|
|
1303
1497
|
} catch (err) {
|
|
1304
1498
|
return { ok: false, output: `Could not read file to edit: ${err.message}` };
|
|
1305
1499
|
}
|
|
@@ -1317,11 +1511,12 @@ var editFileTool = {
|
|
|
1317
1511
|
const decision = await ctx.permissions.authorizeWrite(
|
|
1318
1512
|
args.data.path,
|
|
1319
1513
|
`- ${firstLine(args.data.search)}
|
|
1320
|
-
+ ${firstLine(args.data.replace)}
|
|
1514
|
+
+ ${firstLine(args.data.replace)}`,
|
|
1515
|
+
updated
|
|
1321
1516
|
);
|
|
1322
1517
|
if (!decision.allowed) return { ok: false, output: `Edit denied: ${decision.reason}` };
|
|
1323
1518
|
try {
|
|
1324
|
-
await writeFile2(abs, updated, "utf8");
|
|
1519
|
+
await writeFile2(abs, decision.content ?? updated, "utf8");
|
|
1325
1520
|
return { ok: true, output: `Edited ${args.data.path}.` };
|
|
1326
1521
|
} catch (err) {
|
|
1327
1522
|
return { ok: false, output: `Could not write edit: ${err.message}` };
|
|
@@ -1334,7 +1529,7 @@ function firstLine(s) {
|
|
|
1334
1529
|
|
|
1335
1530
|
// src/core/tools/list-dir.ts
|
|
1336
1531
|
import { readdir } from "fs/promises";
|
|
1337
|
-
import { resolve as
|
|
1532
|
+
import { resolve as resolve4 } from "path";
|
|
1338
1533
|
import { z as z3 } from "zod";
|
|
1339
1534
|
var Args2 = z3.object({ path: z3.string().default(".") });
|
|
1340
1535
|
var listDirTool = {
|
|
@@ -1355,7 +1550,7 @@ var listDirTool = {
|
|
|
1355
1550
|
return { ok: false, output: `List denied: ${decision.reason}` };
|
|
1356
1551
|
}
|
|
1357
1552
|
try {
|
|
1358
|
-
const entries = await readdir(
|
|
1553
|
+
const entries = await readdir(resolve4(ctx.workspace, path), { withFileTypes: true });
|
|
1359
1554
|
const lines = entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).sort();
|
|
1360
1555
|
return { ok: true, output: lines.length ? lines.join("\n") : "(empty)" };
|
|
1361
1556
|
} catch (err) {
|
|
@@ -1365,8 +1560,8 @@ var listDirTool = {
|
|
|
1365
1560
|
};
|
|
1366
1561
|
|
|
1367
1562
|
// src/core/tools/read-file.ts
|
|
1368
|
-
import { readFile as
|
|
1369
|
-
import { resolve as
|
|
1563
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1564
|
+
import { resolve as resolve5 } from "path";
|
|
1370
1565
|
import { z as z4 } from "zod";
|
|
1371
1566
|
var Args3 = z4.object({ path: z4.string().min(1) });
|
|
1372
1567
|
var MAX_CHARS = 6e4;
|
|
@@ -1387,7 +1582,7 @@ var readFileTool = {
|
|
|
1387
1582
|
const decision = ctx.permissions.authorizeRead(args.data.path);
|
|
1388
1583
|
if (!decision.allowed) return { ok: false, output: `Read denied: ${decision.reason}` };
|
|
1389
1584
|
try {
|
|
1390
|
-
const content = await
|
|
1585
|
+
const content = await readFile4(resolve5(ctx.workspace, args.data.path), "utf8");
|
|
1391
1586
|
const truncated = content.length > MAX_CHARS;
|
|
1392
1587
|
return {
|
|
1393
1588
|
ok: true,
|
|
@@ -1448,8 +1643,8 @@ function clamp(s) {
|
|
|
1448
1643
|
}
|
|
1449
1644
|
|
|
1450
1645
|
// src/core/tools/search-file.ts
|
|
1451
|
-
import { readdir as readdir2, readFile as
|
|
1452
|
-
import { join as join2, resolve as
|
|
1646
|
+
import { readdir as readdir2, readFile as readFile5, stat } from "fs/promises";
|
|
1647
|
+
import { join as join2, resolve as resolve6 } from "path";
|
|
1453
1648
|
import { z as z6 } from "zod";
|
|
1454
1649
|
var Args5 = z6.object({
|
|
1455
1650
|
query: z6.string().min(1),
|
|
@@ -1501,7 +1696,7 @@ var searchTool = {
|
|
|
1501
1696
|
}
|
|
1502
1697
|
const globRe = glob ? globToRegExp(glob) : void 0;
|
|
1503
1698
|
const limit = max_results ?? DEFAULT_MAX_RESULTS;
|
|
1504
|
-
const root =
|
|
1699
|
+
const root = resolve6(ctx.workspace, path);
|
|
1505
1700
|
const matches = [];
|
|
1506
1701
|
let truncated = false;
|
|
1507
1702
|
const walk = async (dir) => {
|
|
@@ -1527,7 +1722,7 @@ var searchTool = {
|
|
|
1527
1722
|
try {
|
|
1528
1723
|
const info = await stat(abs);
|
|
1529
1724
|
if (info.size > MAX_FILE_BYTES) continue;
|
|
1530
|
-
const content = await
|
|
1725
|
+
const content = await readFile5(abs, "utf8");
|
|
1531
1726
|
if (content.includes(NUL)) continue;
|
|
1532
1727
|
const lines = content.split("\n");
|
|
1533
1728
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1572,7 +1767,7 @@ var FINISH_TOOL = {
|
|
|
1572
1767
|
|
|
1573
1768
|
// src/core/tools/write-file.ts
|
|
1574
1769
|
import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
1575
|
-
import { dirname, resolve as
|
|
1770
|
+
import { dirname, resolve as resolve7 } from "path";
|
|
1576
1771
|
import { z as z7 } from "zod";
|
|
1577
1772
|
var Args6 = z7.object({ path: z7.string().min(1), content: z7.string() });
|
|
1578
1773
|
var writeFileTool = {
|
|
@@ -1599,13 +1794,14 @@ var writeFileTool = {
|
|
|
1599
1794
|
};
|
|
1600
1795
|
}
|
|
1601
1796
|
const preview = previewContent(args.data.content);
|
|
1602
|
-
const decision = await ctx.permissions.authorizeWrite(args.data.path, preview);
|
|
1797
|
+
const decision = await ctx.permissions.authorizeWrite(args.data.path, preview, args.data.content);
|
|
1603
1798
|
if (!decision.allowed) return { ok: false, output: `Write denied: ${decision.reason}` };
|
|
1799
|
+
const finalContent = decision.content ?? args.data.content;
|
|
1604
1800
|
try {
|
|
1605
|
-
const abs =
|
|
1801
|
+
const abs = resolve7(ctx.workspace, args.data.path);
|
|
1606
1802
|
await mkdir2(dirname(abs), { recursive: true });
|
|
1607
|
-
await writeFile3(abs,
|
|
1608
|
-
const lines =
|
|
1803
|
+
await writeFile3(abs, finalContent, "utf8");
|
|
1804
|
+
const lines = finalContent.split("\n").length;
|
|
1609
1805
|
return { ok: true, output: `Wrote ${args.data.path} (${lines} lines).` };
|
|
1610
1806
|
} catch (err) {
|
|
1611
1807
|
return { ok: false, output: `Could not write file: ${err.message}` };
|
|
@@ -1634,8 +1830,8 @@ function getTool(name) {
|
|
|
1634
1830
|
}
|
|
1635
1831
|
|
|
1636
1832
|
// src/core/agent/correction.ts
|
|
1637
|
-
import { readFile as
|
|
1638
|
-
import { dirname as dirname2, resolve as
|
|
1833
|
+
import { readFile as readFile6, readdir as readdir3 } from "fs/promises";
|
|
1834
|
+
import { dirname as dirname2, resolve as resolve8 } from "path";
|
|
1639
1835
|
function truncationGuidance(toolName) {
|
|
1640
1836
|
const fileHint = toolName === "write_file" || toolName === "edit_file" ? " Write large files in parts: create the file with the first chunk via write_file, then append the rest with edit_file in the next steps." : "";
|
|
1641
1837
|
return [
|
|
@@ -1735,7 +1931,7 @@ ${text2}` : null;
|
|
|
1735
1931
|
}
|
|
1736
1932
|
async function readWorkspaceFile(workspace, path) {
|
|
1737
1933
|
try {
|
|
1738
|
-
return await
|
|
1934
|
+
return await readFile6(resolve8(workspace, path), "utf8");
|
|
1739
1935
|
} catch {
|
|
1740
1936
|
return null;
|
|
1741
1937
|
}
|
|
@@ -1793,11 +1989,11 @@ async function occurrenceLines(workspace, path, search) {
|
|
|
1793
1989
|
return out;
|
|
1794
1990
|
}
|
|
1795
1991
|
async function listNearest(workspace, path) {
|
|
1796
|
-
let dir = dirname2(
|
|
1992
|
+
let dir = dirname2(resolve8(workspace, path));
|
|
1797
1993
|
for (let i = 0; i < 8; i++) {
|
|
1798
1994
|
try {
|
|
1799
1995
|
const entries = await readdir3(dir, { withFileTypes: true });
|
|
1800
|
-
const rel = dir ===
|
|
1996
|
+
const rel = dir === resolve8(workspace) ? "." : dir;
|
|
1801
1997
|
const names = entries.slice(0, 40).map((e) => e.isDirectory() ? `${e.name}/` : e.name);
|
|
1802
1998
|
return `${rel}:
|
|
1803
1999
|
${names.join(" ") || "(empty)"}`;
|
|
@@ -1819,14 +2015,14 @@ function formatSchema(spec) {
|
|
|
1819
2015
|
}
|
|
1820
2016
|
|
|
1821
2017
|
// src/core/agent/project-context.ts
|
|
1822
|
-
import { readFile as
|
|
2018
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1823
2019
|
import { join as join3 } from "path";
|
|
1824
2020
|
var INSTRUCTION_FILES = [join3(".poly", "agents.md"), "AGENTS.md"];
|
|
1825
2021
|
var MAX_CHARS2 = 8e3;
|
|
1826
2022
|
async function loadProjectInstructions(workspace) {
|
|
1827
2023
|
for (const rel of INSTRUCTION_FILES) {
|
|
1828
2024
|
try {
|
|
1829
|
-
const raw = (await
|
|
2025
|
+
const raw = (await readFile7(join3(workspace, rel), "utf8")).trim();
|
|
1830
2026
|
if (!raw) continue;
|
|
1831
2027
|
return raw.length > MAX_CHARS2 ? raw.slice(0, MAX_CHARS2) + "\n\u2026(truncated)" : raw;
|
|
1832
2028
|
} catch {
|
|
@@ -1987,8 +2183,8 @@ ${guidance}`;
|
|
|
1987
2183
|
}
|
|
1988
2184
|
|
|
1989
2185
|
// src/core/context/mentions.ts
|
|
1990
|
-
import { readdir as readdir4, readFile as
|
|
1991
|
-
import { resolve as
|
|
2186
|
+
import { readdir as readdir4, readFile as readFile8, stat as stat2 } from "fs/promises";
|
|
2187
|
+
import { resolve as resolve9 } from "path";
|
|
1992
2188
|
var MAX_FILE_CHARS = 1e4;
|
|
1993
2189
|
var MENTION_RE = /(?:^|\s)@([\w./-]+)/g;
|
|
1994
2190
|
async function resolveMentions(task, policy) {
|
|
@@ -2004,7 +2200,7 @@ async function resolveMentions(task, policy) {
|
|
|
2004
2200
|
${t("mentions.notFound", { path: token })}`);
|
|
2005
2201
|
continue;
|
|
2006
2202
|
}
|
|
2007
|
-
const abs =
|
|
2203
|
+
const abs = resolve9(policy.workspace, token);
|
|
2008
2204
|
try {
|
|
2009
2205
|
const info = await stat2(abs);
|
|
2010
2206
|
if (info.isDirectory()) {
|
|
@@ -2014,7 +2210,7 @@ ${t("mentions.notFound", { path: token })}`);
|
|
|
2014
2210
|
${listing || "(empty)"}`);
|
|
2015
2211
|
injected.push(decision.rel);
|
|
2016
2212
|
} else {
|
|
2017
|
-
const raw = await
|
|
2213
|
+
const raw = await readFile8(abs, "utf8");
|
|
2018
2214
|
const content = raw.length > MAX_FILE_CHARS ? raw.slice(0, MAX_FILE_CHARS) + "\n\u2026[truncated]" : raw;
|
|
2019
2215
|
blocks.push(`## @${decision.rel}
|
|
2020
2216
|
\`\`\`
|
|
@@ -2728,10 +2924,10 @@ async function readLineTTY(prompt) {
|
|
|
2728
2924
|
stdin.resume();
|
|
2729
2925
|
stdin.on("data", onData);
|
|
2730
2926
|
try {
|
|
2731
|
-
const line = await new Promise((
|
|
2732
|
-
rl.question(prompt).then(
|
|
2733
|
-
rl.on("SIGINT", () =>
|
|
2734
|
-
rl.on("close", () =>
|
|
2927
|
+
const line = await new Promise((resolve11) => {
|
|
2928
|
+
rl.question(prompt).then(resolve11, () => resolve11(null));
|
|
2929
|
+
rl.on("SIGINT", () => resolve11(null));
|
|
2930
|
+
rl.on("close", () => resolve11(null));
|
|
2735
2931
|
});
|
|
2736
2932
|
return line === null ? null : store.expand(line);
|
|
2737
2933
|
} finally {
|
|
@@ -3483,11 +3679,39 @@ function listenForCancel(controller) {
|
|
|
3483
3679
|
return { pause: detach, resume: attach, dispose: detach };
|
|
3484
3680
|
}
|
|
3485
3681
|
async function confirmAction(req) {
|
|
3682
|
+
if (req.kind === "write" && req.hunks && req.hunks.length > 0) {
|
|
3683
|
+
renderDiff(req.hunks);
|
|
3684
|
+
const options = [
|
|
3685
|
+
{ value: "approve", label: t("review.approveAll") },
|
|
3686
|
+
{ value: "reject", label: t("review.reject") },
|
|
3687
|
+
...req.hunks.length > 1 ? [{ value: "hunks", label: t("review.pickHunks") }] : []
|
|
3688
|
+
];
|
|
3689
|
+
const choice = await p2.select({ message: t("run.confirm", { summary: req.summary }), options });
|
|
3690
|
+
if (p2.isCancel(choice) || choice === "reject") return false;
|
|
3691
|
+
if (choice === "approve") return true;
|
|
3692
|
+
const selected = await p2.multiselect({
|
|
3693
|
+
message: t("review.selectHunks"),
|
|
3694
|
+
options: req.hunks.map((h, i) => ({ value: i, label: hunkLabel(h) })),
|
|
3695
|
+
required: false
|
|
3696
|
+
});
|
|
3697
|
+
if (p2.isCancel(selected)) return false;
|
|
3698
|
+
return selected;
|
|
3699
|
+
}
|
|
3486
3700
|
if (req.preview) console.log(pc8.dim(req.preview));
|
|
3487
3701
|
const answer = await p2.confirm({ message: t("run.confirm", { summary: req.summary }) });
|
|
3488
3702
|
if (p2.isCancel(answer)) return false;
|
|
3489
3703
|
return answer === true;
|
|
3490
3704
|
}
|
|
3705
|
+
function renderDiff(hunks) {
|
|
3706
|
+
for (const h of hunks) {
|
|
3707
|
+
console.log(pc8.cyan(`@@ -${h.oldStart + 1},${h.oldCount} +${h.newStart + 1},${h.newCount} @@`));
|
|
3708
|
+
for (const l of h.lines) {
|
|
3709
|
+
if (l.type === "+") console.log(pc8.green(`+${l.text}`));
|
|
3710
|
+
else if (l.type === "-") console.log(pc8.red(`-${l.text}`));
|
|
3711
|
+
else console.log(pc8.dim(` ${l.text}`));
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3491
3715
|
function renderEvents(spinner3) {
|
|
3492
3716
|
return {
|
|
3493
3717
|
onStep() {
|
|
@@ -3876,7 +4100,7 @@ async function resolveOpenRouterKey() {
|
|
|
3876
4100
|
}
|
|
3877
4101
|
|
|
3878
4102
|
// src/cli/commands/prd.ts
|
|
3879
|
-
import { writeFile as writeFile5, readFile as
|
|
4103
|
+
import { writeFile as writeFile5, readFile as readFile9 } from "fs/promises";
|
|
3880
4104
|
import { execFile } from "child_process";
|
|
3881
4105
|
import { promisify as promisify2 } from "util";
|
|
3882
4106
|
import pc11 from "picocolors";
|
|
@@ -3968,13 +4192,13 @@ async function withRetry(fn, opts = {}) {
|
|
|
3968
4192
|
|
|
3969
4193
|
// src/cli/commands/cli-io.ts
|
|
3970
4194
|
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
3971
|
-
import { resolve as
|
|
4195
|
+
import { resolve as resolve10 } from "path";
|
|
3972
4196
|
var GUIDE_MAX = 12e3;
|
|
3973
4197
|
function readProjectGuide(files) {
|
|
3974
4198
|
const parts = [];
|
|
3975
4199
|
for (const file of files) {
|
|
3976
4200
|
try {
|
|
3977
|
-
const path =
|
|
4201
|
+
const path = resolve10(process.cwd(), file);
|
|
3978
4202
|
if (existsSync2(path)) parts.push(`# ${file}
|
|
3979
4203
|
${readFileSync(path, "utf8").trim()}`);
|
|
3980
4204
|
} catch {
|
|
@@ -4015,7 +4239,7 @@ async function prd(issueRef, opts) {
|
|
|
4015
4239
|
}
|
|
4016
4240
|
async function loadIssue(issueRef, input) {
|
|
4017
4241
|
if (input) {
|
|
4018
|
-
const raw = input === "-" ? await readStdin() : await
|
|
4242
|
+
const raw = input === "-" ? await readStdin() : await readFile9(input, "utf8");
|
|
4019
4243
|
return normalize2(JSON.parse(stripBom(raw)));
|
|
4020
4244
|
}
|
|
4021
4245
|
const num = numericRef(issueRef);
|
|
@@ -4034,7 +4258,7 @@ function normalize2(raw) {
|
|
|
4034
4258
|
}
|
|
4035
4259
|
|
|
4036
4260
|
// src/cli/commands/review.ts
|
|
4037
|
-
import { writeFile as writeFile6, readFile as
|
|
4261
|
+
import { writeFile as writeFile6, readFile as readFile10 } from "fs/promises";
|
|
4038
4262
|
import { execFile as execFile2 } from "child_process";
|
|
4039
4263
|
import { promisify as promisify3 } from "util";
|
|
4040
4264
|
import pc12 from "picocolors";
|
|
@@ -4110,7 +4334,7 @@ async function review(prRef, opts) {
|
|
|
4110
4334
|
}
|
|
4111
4335
|
}
|
|
4112
4336
|
async function loadDiff(num, input) {
|
|
4113
|
-
if (input) return input === "-" ? readStdin() :
|
|
4337
|
+
if (input) return input === "-" ? readStdin() : readFile10(input, "utf8");
|
|
4114
4338
|
const { stdout: stdout2 } = await exec3("gh", ["pr", "diff", num]);
|
|
4115
4339
|
return stdout2;
|
|
4116
4340
|
}
|