@gaberrb/polypus 0.4.1 → 0.4.2
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 +142 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1435,6 +1435,116 @@ function clamp(s) {
|
|
|
1435
1435
|
return s.length > MAX_OUTPUT ? s.slice(0, MAX_OUTPUT) + "\n\u2026[truncated]" : s;
|
|
1436
1436
|
}
|
|
1437
1437
|
|
|
1438
|
+
// src/core/tools/search-file.ts
|
|
1439
|
+
import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
|
|
1440
|
+
import { join as join2, resolve as resolve5 } from "path";
|
|
1441
|
+
import { z as z6 } from "zod";
|
|
1442
|
+
var Args5 = z6.object({
|
|
1443
|
+
query: z6.string().min(1),
|
|
1444
|
+
path: z6.string().optional(),
|
|
1445
|
+
glob: z6.string().optional(),
|
|
1446
|
+
max_results: z6.number().int().positive().max(1e3).optional()
|
|
1447
|
+
});
|
|
1448
|
+
var DEFAULT_MAX_RESULTS = 50;
|
|
1449
|
+
var MAX_OUTPUT2 = 2e4;
|
|
1450
|
+
var MAX_FILE_BYTES = 2e6;
|
|
1451
|
+
var SNIPPET_CHARS = 200;
|
|
1452
|
+
var NUL = String.fromCharCode(0);
|
|
1453
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "coverage", ".turbo"]);
|
|
1454
|
+
var searchTool = {
|
|
1455
|
+
mutating: false,
|
|
1456
|
+
spec: {
|
|
1457
|
+
name: "search",
|
|
1458
|
+
description: "Search file contents by regular expression across the workspace (like grep/ripgrep). Returns matches as 'path:line: snippet'. Respects the allow/deny-list and skips node_modules/.git. Use this to find where a symbol is defined or used instead of reading files blindly.",
|
|
1459
|
+
parameters: {
|
|
1460
|
+
type: "object",
|
|
1461
|
+
properties: {
|
|
1462
|
+
query: { type: "string", description: "Regular expression to match against each line" },
|
|
1463
|
+
path: { type: "string", description: "Workspace-relative directory to search in (default '.')" },
|
|
1464
|
+
glob: {
|
|
1465
|
+
type: "string",
|
|
1466
|
+
description: "Optional glob to limit files, e.g. 'src/**/*.ts'"
|
|
1467
|
+
},
|
|
1468
|
+
max_results: {
|
|
1469
|
+
type: "number",
|
|
1470
|
+
description: `Maximum number of matches to return (default ${DEFAULT_MAX_RESULTS})`
|
|
1471
|
+
}
|
|
1472
|
+
},
|
|
1473
|
+
required: ["query"]
|
|
1474
|
+
}
|
|
1475
|
+
},
|
|
1476
|
+
async run(rawArgs, ctx) {
|
|
1477
|
+
const parsed = Args5.safeParse(rawArgs);
|
|
1478
|
+
if (!parsed.success) return { ok: false, output: "Invalid args: 'query' is required." };
|
|
1479
|
+
const { query, path = ".", glob, max_results } = parsed.data;
|
|
1480
|
+
let regex;
|
|
1481
|
+
try {
|
|
1482
|
+
regex = new RegExp(query);
|
|
1483
|
+
} catch (err) {
|
|
1484
|
+
return { ok: false, output: `Invalid regular expression: ${err.message}` };
|
|
1485
|
+
}
|
|
1486
|
+
if (path !== ".") {
|
|
1487
|
+
const decision = ctx.permissions.authorizeRead(path);
|
|
1488
|
+
if (!decision.allowed) return { ok: false, output: `Search denied: ${decision.reason}` };
|
|
1489
|
+
}
|
|
1490
|
+
const globRe = glob ? globToRegExp(glob) : void 0;
|
|
1491
|
+
const limit = max_results ?? DEFAULT_MAX_RESULTS;
|
|
1492
|
+
const root = resolve5(ctx.workspace, path);
|
|
1493
|
+
const matches = [];
|
|
1494
|
+
let truncated = false;
|
|
1495
|
+
const walk = async (dir) => {
|
|
1496
|
+
if (matches.length >= limit) return;
|
|
1497
|
+
let entries;
|
|
1498
|
+
try {
|
|
1499
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
1500
|
+
} catch {
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
for (const entry of entries) {
|
|
1504
|
+
if (matches.length >= limit) return;
|
|
1505
|
+
const abs = join2(dir, entry.name);
|
|
1506
|
+
const rel = toPosix(abs.slice(ctx.workspace.length + 1));
|
|
1507
|
+
if (entry.isDirectory()) {
|
|
1508
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
1509
|
+
await walk(abs);
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
if (!entry.isFile()) continue;
|
|
1513
|
+
if (globRe && !globRe.test(rel)) continue;
|
|
1514
|
+
if (!ctx.permissions.authorizeRead(rel).allowed) continue;
|
|
1515
|
+
try {
|
|
1516
|
+
const info = await stat(abs);
|
|
1517
|
+
if (info.size > MAX_FILE_BYTES) continue;
|
|
1518
|
+
const content = await readFile4(abs, "utf8");
|
|
1519
|
+
if (content.includes(NUL)) continue;
|
|
1520
|
+
const lines = content.split("\n");
|
|
1521
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1522
|
+
if (matches.length >= limit) {
|
|
1523
|
+
truncated = true;
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
if (regex.test(lines[i])) {
|
|
1527
|
+
const snippet = lines[i].trim().slice(0, SNIPPET_CHARS);
|
|
1528
|
+
matches.push(`${rel}:${i + 1}: ${snippet}`);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
} catch {
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
await walk(root);
|
|
1536
|
+
if (matches.length === 0) {
|
|
1537
|
+
return { ok: true, output: `No matches for /${query}/${glob ? ` in ${glob}` : ""}.` };
|
|
1538
|
+
}
|
|
1539
|
+
const header = `${matches.length}${truncated ? "+" : ""} match(es) for /${query}/:`;
|
|
1540
|
+
const body = [header, ...matches].join("\n");
|
|
1541
|
+
return {
|
|
1542
|
+
ok: true,
|
|
1543
|
+
output: body.length > MAX_OUTPUT2 ? body.slice(0, MAX_OUTPUT2) + "\n\u2026[truncated]" : body
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1438
1548
|
// src/core/tools/types.ts
|
|
1439
1549
|
var FINISH_TOOL = {
|
|
1440
1550
|
name: "finish",
|
|
@@ -1450,9 +1560,9 @@ var FINISH_TOOL = {
|
|
|
1450
1560
|
|
|
1451
1561
|
// src/core/tools/write-file.ts
|
|
1452
1562
|
import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
1453
|
-
import { dirname, resolve as
|
|
1454
|
-
import { z as
|
|
1455
|
-
var
|
|
1563
|
+
import { dirname, resolve as resolve6 } from "path";
|
|
1564
|
+
import { z as z7 } from "zod";
|
|
1565
|
+
var Args6 = z7.object({ path: z7.string().min(1), content: z7.string() });
|
|
1456
1566
|
var writeFileTool = {
|
|
1457
1567
|
mutating: true,
|
|
1458
1568
|
spec: {
|
|
@@ -1468,7 +1578,7 @@ var writeFileTool = {
|
|
|
1468
1578
|
}
|
|
1469
1579
|
},
|
|
1470
1580
|
async run(rawArgs, ctx) {
|
|
1471
|
-
const args =
|
|
1581
|
+
const args = Args6.safeParse(rawArgs);
|
|
1472
1582
|
if (!args.success) {
|
|
1473
1583
|
const got = Object.keys(rawArgs ?? {});
|
|
1474
1584
|
return {
|
|
@@ -1480,7 +1590,7 @@ var writeFileTool = {
|
|
|
1480
1590
|
const decision = await ctx.permissions.authorizeWrite(args.data.path, preview);
|
|
1481
1591
|
if (!decision.allowed) return { ok: false, output: `Write denied: ${decision.reason}` };
|
|
1482
1592
|
try {
|
|
1483
|
-
const abs =
|
|
1593
|
+
const abs = resolve6(ctx.workspace, args.data.path);
|
|
1484
1594
|
await mkdir2(dirname(abs), { recursive: true });
|
|
1485
1595
|
await writeFile3(abs, args.data.content, "utf8");
|
|
1486
1596
|
const lines = args.data.content.split("\n").length;
|
|
@@ -1499,6 +1609,7 @@ function previewContent(content) {
|
|
|
1499
1609
|
var TOOLS = {
|
|
1500
1610
|
[readFileTool.spec.name]: readFileTool,
|
|
1501
1611
|
[listDirTool.spec.name]: listDirTool,
|
|
1612
|
+
[searchTool.spec.name]: searchTool,
|
|
1502
1613
|
[writeFileTool.spec.name]: writeFileTool,
|
|
1503
1614
|
[editFileTool.spec.name]: editFileTool,
|
|
1504
1615
|
[runCommandTool.spec.name]: runCommandTool
|
|
@@ -1511,8 +1622,8 @@ function getTool(name) {
|
|
|
1511
1622
|
}
|
|
1512
1623
|
|
|
1513
1624
|
// src/core/agent/correction.ts
|
|
1514
|
-
import { readFile as
|
|
1515
|
-
import { dirname as dirname2, resolve as
|
|
1625
|
+
import { readFile as readFile5, readdir as readdir3 } from "fs/promises";
|
|
1626
|
+
import { dirname as dirname2, resolve as resolve7 } from "path";
|
|
1516
1627
|
function truncationGuidance(toolName) {
|
|
1517
1628
|
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." : "";
|
|
1518
1629
|
return [
|
|
@@ -1612,7 +1723,7 @@ ${text2}` : null;
|
|
|
1612
1723
|
}
|
|
1613
1724
|
async function readWorkspaceFile(workspace, path) {
|
|
1614
1725
|
try {
|
|
1615
|
-
return await
|
|
1726
|
+
return await readFile5(resolve7(workspace, path), "utf8");
|
|
1616
1727
|
} catch {
|
|
1617
1728
|
return null;
|
|
1618
1729
|
}
|
|
@@ -1670,11 +1781,11 @@ async function occurrenceLines(workspace, path, search) {
|
|
|
1670
1781
|
return out;
|
|
1671
1782
|
}
|
|
1672
1783
|
async function listNearest(workspace, path) {
|
|
1673
|
-
let dir = dirname2(
|
|
1784
|
+
let dir = dirname2(resolve7(workspace, path));
|
|
1674
1785
|
for (let i = 0; i < 8; i++) {
|
|
1675
1786
|
try {
|
|
1676
|
-
const entries = await
|
|
1677
|
-
const rel = dir ===
|
|
1787
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
1788
|
+
const rel = dir === resolve7(workspace) ? "." : dir;
|
|
1678
1789
|
const names = entries.slice(0, 40).map((e) => e.isDirectory() ? `${e.name}/` : e.name);
|
|
1679
1790
|
return `${rel}:
|
|
1680
1791
|
${names.join(" ") || "(empty)"}`;
|
|
@@ -1696,14 +1807,14 @@ function formatSchema(spec) {
|
|
|
1696
1807
|
}
|
|
1697
1808
|
|
|
1698
1809
|
// src/core/agent/project-context.ts
|
|
1699
|
-
import { readFile as
|
|
1700
|
-
import { join as
|
|
1701
|
-
var INSTRUCTION_FILES = [
|
|
1810
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1811
|
+
import { join as join3 } from "path";
|
|
1812
|
+
var INSTRUCTION_FILES = [join3(".poly", "agents.md"), "AGENTS.md"];
|
|
1702
1813
|
var MAX_CHARS2 = 8e3;
|
|
1703
1814
|
async function loadProjectInstructions(workspace) {
|
|
1704
1815
|
for (const rel of INSTRUCTION_FILES) {
|
|
1705
1816
|
try {
|
|
1706
|
-
const raw = (await
|
|
1817
|
+
const raw = (await readFile6(join3(workspace, rel), "utf8")).trim();
|
|
1707
1818
|
if (!raw) continue;
|
|
1708
1819
|
return raw.length > MAX_CHARS2 ? raw.slice(0, MAX_CHARS2) + "\n\u2026(truncated)" : raw;
|
|
1709
1820
|
} catch {
|
|
@@ -2501,10 +2612,10 @@ async function readLineTTY(prompt) {
|
|
|
2501
2612
|
stdin.resume();
|
|
2502
2613
|
stdin.on("data", onData);
|
|
2503
2614
|
try {
|
|
2504
|
-
const line = await new Promise((
|
|
2505
|
-
rl.question(prompt).then(
|
|
2506
|
-
rl.on("SIGINT", () =>
|
|
2507
|
-
rl.on("close", () =>
|
|
2615
|
+
const line = await new Promise((resolve9) => {
|
|
2616
|
+
rl.question(prompt).then(resolve9, () => resolve9(null));
|
|
2617
|
+
rl.on("SIGINT", () => resolve9(null));
|
|
2618
|
+
rl.on("close", () => resolve9(null));
|
|
2508
2619
|
});
|
|
2509
2620
|
return line === null ? null : store.expand(line);
|
|
2510
2621
|
} finally {
|
|
@@ -2649,7 +2760,7 @@ import pc7 from "picocolors";
|
|
|
2649
2760
|
// src/core/git/worktree.ts
|
|
2650
2761
|
import { mkdtemp } from "fs/promises";
|
|
2651
2762
|
import { tmpdir } from "os";
|
|
2652
|
-
import { join as
|
|
2763
|
+
import { join as join4 } from "path";
|
|
2653
2764
|
import { simpleGit } from "simple-git";
|
|
2654
2765
|
async function ensureRepo(workspace) {
|
|
2655
2766
|
const git = simpleGit(workspace);
|
|
@@ -2670,7 +2781,7 @@ async function identityArgs(git) {
|
|
|
2670
2781
|
}
|
|
2671
2782
|
async function createWorktree(git, label) {
|
|
2672
2783
|
const branch = `polypus/${label}-${Date.now().toString(36)}`;
|
|
2673
|
-
const path = await mkdtemp(
|
|
2784
|
+
const path = await mkdtemp(join4(tmpdir(), "polypus-wt-"));
|
|
2674
2785
|
await git.raw(["worktree", "add", "-b", branch, path, "HEAD"]);
|
|
2675
2786
|
return { path, branch };
|
|
2676
2787
|
}
|
|
@@ -3288,7 +3399,7 @@ import pc9 from "picocolors";
|
|
|
3288
3399
|
|
|
3289
3400
|
// src/core/scaffold/init.ts
|
|
3290
3401
|
import { mkdir as mkdir3, writeFile as writeFile4, access } from "fs/promises";
|
|
3291
|
-
import { dirname as dirname3, join as
|
|
3402
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
3292
3403
|
|
|
3293
3404
|
// src/core/scaffold/templates.ts
|
|
3294
3405
|
function polyTemplates(locale) {
|
|
@@ -3531,7 +3642,7 @@ async function scaffoldPoly(workspace, opts) {
|
|
|
3531
3642
|
const skipped = [];
|
|
3532
3643
|
for (const [rel, content] of Object.entries(templates)) {
|
|
3533
3644
|
const display = `.poly/${rel}`;
|
|
3534
|
-
const abs =
|
|
3645
|
+
const abs = join5(workspace, ".poly", ...rel.split("/"));
|
|
3535
3646
|
if (!opts.force && await exists(abs)) {
|
|
3536
3647
|
skipped.push(display);
|
|
3537
3648
|
continue;
|
|
@@ -3631,7 +3742,7 @@ async function resolveOpenRouterKey() {
|
|
|
3631
3742
|
}
|
|
3632
3743
|
|
|
3633
3744
|
// src/cli/commands/prd.ts
|
|
3634
|
-
import { writeFile as writeFile5, readFile as
|
|
3745
|
+
import { writeFile as writeFile5, readFile as readFile7 } from "fs/promises";
|
|
3635
3746
|
import { execFile } from "child_process";
|
|
3636
3747
|
import { promisify as promisify2 } from "util";
|
|
3637
3748
|
import pc11 from "picocolors";
|
|
@@ -3723,13 +3834,13 @@ async function withRetry(fn, opts = {}) {
|
|
|
3723
3834
|
|
|
3724
3835
|
// src/cli/commands/cli-io.ts
|
|
3725
3836
|
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
3726
|
-
import { resolve as
|
|
3837
|
+
import { resolve as resolve8 } from "path";
|
|
3727
3838
|
var GUIDE_MAX = 12e3;
|
|
3728
3839
|
function readProjectGuide(files) {
|
|
3729
3840
|
const parts = [];
|
|
3730
3841
|
for (const file of files) {
|
|
3731
3842
|
try {
|
|
3732
|
-
const path =
|
|
3843
|
+
const path = resolve8(process.cwd(), file);
|
|
3733
3844
|
if (existsSync2(path)) parts.push(`# ${file}
|
|
3734
3845
|
${readFileSync(path, "utf8").trim()}`);
|
|
3735
3846
|
} catch {
|
|
@@ -3770,7 +3881,7 @@ async function prd(issueRef, opts) {
|
|
|
3770
3881
|
}
|
|
3771
3882
|
async function loadIssue(issueRef, input) {
|
|
3772
3883
|
if (input) {
|
|
3773
|
-
const raw = input === "-" ? await readStdin() : await
|
|
3884
|
+
const raw = input === "-" ? await readStdin() : await readFile7(input, "utf8");
|
|
3774
3885
|
return normalize2(JSON.parse(stripBom(raw)));
|
|
3775
3886
|
}
|
|
3776
3887
|
const num = numericRef(issueRef);
|
|
@@ -3789,7 +3900,7 @@ function normalize2(raw) {
|
|
|
3789
3900
|
}
|
|
3790
3901
|
|
|
3791
3902
|
// src/cli/commands/review.ts
|
|
3792
|
-
import { writeFile as writeFile6, readFile as
|
|
3903
|
+
import { writeFile as writeFile6, readFile as readFile8 } from "fs/promises";
|
|
3793
3904
|
import { execFile as execFile2 } from "child_process";
|
|
3794
3905
|
import { promisify as promisify3 } from "util";
|
|
3795
3906
|
import pc12 from "picocolors";
|
|
@@ -3865,7 +3976,7 @@ async function review(prRef, opts) {
|
|
|
3865
3976
|
}
|
|
3866
3977
|
}
|
|
3867
3978
|
async function loadDiff(num, input) {
|
|
3868
|
-
if (input) return input === "-" ? readStdin() :
|
|
3979
|
+
if (input) return input === "-" ? readStdin() : readFile8(input, "utf8");
|
|
3869
3980
|
const { stdout: stdout2 } = await exec3("gh", ["pr", "diff", num]);
|
|
3870
3981
|
return stdout2;
|
|
3871
3982
|
}
|
|
@@ -3877,7 +3988,7 @@ async function loadMeta(num, input) {
|
|
|
3877
3988
|
}
|
|
3878
3989
|
|
|
3879
3990
|
// src/cli/index.ts
|
|
3880
|
-
import { join as
|
|
3991
|
+
import { join as join6 } from "path";
|
|
3881
3992
|
|
|
3882
3993
|
// src/core/config/dotenv.ts
|
|
3883
3994
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
@@ -3946,7 +4057,7 @@ function buildProgram() {
|
|
|
3946
4057
|
}
|
|
3947
4058
|
async function main() {
|
|
3948
4059
|
try {
|
|
3949
|
-
loadDotenv([
|
|
4060
|
+
loadDotenv([join6(configDir(), ".env"), join6(process.cwd(), ".env")]);
|
|
3950
4061
|
await resolveLocale();
|
|
3951
4062
|
await buildProgram().parseAsync(process.argv);
|
|
3952
4063
|
} catch (err) {
|