@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 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 resolve5 } from "path";
1454
- import { z as z6 } from "zod";
1455
- var Args5 = z6.object({ path: z6.string().min(1), content: z6.string() });
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 = Args5.safeParse(rawArgs);
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 = resolve5(ctx.workspace, args.data.path);
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 readFile4, readdir as readdir2 } from "fs/promises";
1515
- import { dirname as dirname2, resolve as resolve6 } from "path";
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 readFile4(resolve6(workspace, path), "utf8");
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(resolve6(workspace, path));
1784
+ let dir = dirname2(resolve7(workspace, path));
1674
1785
  for (let i = 0; i < 8; i++) {
1675
1786
  try {
1676
- const entries = await readdir2(dir, { withFileTypes: true });
1677
- const rel = dir === resolve6(workspace) ? "." : 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 readFile5 } from "fs/promises";
1700
- import { join as join2 } from "path";
1701
- var INSTRUCTION_FILES = [join2(".poly", "agents.md"), "AGENTS.md"];
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 readFile5(join2(workspace, rel), "utf8")).trim();
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((resolve8) => {
2505
- rl.question(prompt).then(resolve8, () => resolve8(null));
2506
- rl.on("SIGINT", () => resolve8(null));
2507
- rl.on("close", () => resolve8(null));
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 join3 } from "path";
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(join3(tmpdir(), "polypus-wt-"));
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 join4 } from "path";
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 = join4(workspace, ".poly", ...rel.split("/"));
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 readFile6 } from "fs/promises";
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 resolve7 } from "path";
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 = resolve7(process.cwd(), file);
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 readFile6(input, "utf8");
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 readFile7 } from "fs/promises";
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() : readFile7(input, "utf8");
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 join5 } from "path";
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([join5(configDir(), ".env"), join5(process.cwd(), ".env")]);
4060
+ loadDotenv([join6(configDir(), ".env"), join6(process.cwd(), ".env")]);
3950
4061
  await resolveLocale();
3951
4062
  await buildProgram().parseAsync(process.argv);
3952
4063
  } catch (err) {