@wrongstack/tools 0.318.0 → 0.319.1

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.
Files changed (101) hide show
  1. package/dist/_edit-match.d.ts +1 -3
  2. package/dist/_util.d.ts +20 -0
  3. package/dist/audit.js +4 -4
  4. package/dist/bash.js +2 -2
  5. package/dist/builtin.d.ts +10 -0
  6. package/dist/builtin.js +46 -44
  7. package/dist/{chunk-TKA4VEX2.js → chunk-2OXHS3KZ.js} +17 -5
  8. package/dist/{chunk-X5K3U6GI.js → chunk-3M7LLN7W.js} +71 -7
  9. package/dist/{chunk-4AEZIFOW.js → chunk-5EUJF3WC.js} +36 -8
  10. package/dist/{chunk-CVALEVGA.js → chunk-64VIJPFV.js} +34 -13
  11. package/dist/{chunk-ZE6KLBUO.js → chunk-6AAOT4PW.js} +58 -10
  12. package/dist/{chunk-A6BRWIXN.js → chunk-6IEBD27U.js} +20 -5
  13. package/dist/{chunk-HTYEO67J.js → chunk-AKHYMB4V.js} +36 -12
  14. package/dist/{chunk-4E42D7UF.js → chunk-ATSKPZUK.js} +2 -2
  15. package/dist/{chunk-H6AWAZ2Y.js → chunk-B4HTSV6Z.js} +57 -6
  16. package/dist/{chunk-5ZYEGP2P.js → chunk-BK732J7I.js} +72 -27
  17. package/dist/{chunk-WI3VDXHD.js → chunk-BSJSNVGA.js} +108 -19
  18. package/dist/{chunk-UKGMDCXT.js → chunk-C2DQKVLO.js} +61 -28
  19. package/dist/{chunk-CIFAIIHI.js → chunk-CFLJFDYV.js} +100 -58
  20. package/dist/{chunk-JJQB6R7Y.js → chunk-CMOUCKGC.js} +6 -6
  21. package/dist/{chunk-MX25MTDJ.js → chunk-DC4KMPJO.js} +2 -2
  22. package/dist/{chunk-VZLX2JZG.js → chunk-DP6CXMJZ.js} +3 -3
  23. package/dist/{chunk-3QQSBUXR.js → chunk-EADULOZV.js} +36 -12
  24. package/dist/{chunk-6RNIFLG4.js → chunk-ELRRTWNP.js} +38 -6
  25. package/dist/{chunk-CPSAF2AU.js → chunk-FQNXYA6W.js} +45 -6
  26. package/dist/{chunk-ZZT45EHF.js → chunk-GMZYW52L.js} +3 -3
  27. package/dist/{chunk-ZLTFLC25.js → chunk-IIF5HIVF.js} +2 -2
  28. package/dist/{chunk-EMRGN4KM.js → chunk-IU4DV2PA.js} +18 -8
  29. package/dist/{chunk-AGUSOFNI.js → chunk-IWS56OY7.js} +30 -7
  30. package/dist/{chunk-MRZOVQ5D.js → chunk-IYKXK356.js} +98 -11
  31. package/dist/{chunk-6GZPSWP3.js → chunk-JDPQOR7C.js} +14 -7
  32. package/dist/{chunk-BLCSYKNN.js → chunk-KY4PXYSO.js} +10 -15
  33. package/dist/{chunk-WJYXMB2R.js → chunk-LOBRWKCR.js} +63 -14
  34. package/dist/{chunk-J5SKD6LN.js → chunk-MNP3GE7H.js} +38 -27
  35. package/dist/{chunk-S2GBLQ5S.js → chunk-O44IMQCN.js} +31 -7
  36. package/dist/{chunk-P7ZQ2FAJ.js → chunk-OG2BEUBY.js} +69 -34
  37. package/dist/{chunk-K23JNFE3.js → chunk-OTINMOKK.js} +233 -45
  38. package/dist/{chunk-PQTONQ4P.js → chunk-QPNYDH5B.js} +19 -5
  39. package/dist/{chunk-UE3UBMCP.js → chunk-RLMX75EM.js} +19 -8
  40. package/dist/{chunk-3U4H6ZRF.js → chunk-RNWUCZ2U.js} +53 -25
  41. package/dist/{chunk-YF3D5INN.js → chunk-RTZWBNTY.js} +121 -91
  42. package/dist/{chunk-SOG7XIRF.js → chunk-TTZOK6IY.js} +2 -2
  43. package/dist/{chunk-6GVXZCQV.js → chunk-UDVKRX5R.js} +2 -2
  44. package/dist/{chunk-CPVLVQYT.js → chunk-UQQS3VNT.js} +43 -9
  45. package/dist/{chunk-VB3MJGCV.js → chunk-VD2LV5WO.js} +82 -13
  46. package/dist/{chunk-5WNDJHT6.js → chunk-VPALDFYM.js} +2 -2
  47. package/dist/{chunk-FJEQZHZN.js → chunk-W2CECGTG.js} +24 -14
  48. package/dist/{chunk-CISOLTOP.js → chunk-X5FOFGXX.js} +72 -33
  49. package/dist/{chunk-CWALNI6K.js → chunk-X6KXNHUO.js} +1 -1
  50. package/dist/{chunk-EEOFQDT6.js → chunk-XRNN44QY.js} +46 -2
  51. package/dist/{chunk-EOOFNYIT.js → chunk-YOKD4GWJ.js} +67 -27
  52. package/dist/{chunk-D33S4RQG.js → chunk-ZUFHAHTE.js} +18 -6
  53. package/dist/codebase-index/generic-parser.d.ts +8 -1
  54. package/dist/codebase-index/index.js +9 -9
  55. package/dist/codebase-index/parser-worker-script.js +3 -3
  56. package/dist/codebase-index/project-server.js +5 -5
  57. package/dist/codebase-index/worker.js +4 -4
  58. package/dist/codebase-index/writer-refs.d.ts +1 -1
  59. package/dist/design.d.ts +1 -1
  60. package/dist/design.js +1 -1
  61. package/dist/diff.js +3 -2
  62. package/dist/document.js +2 -2
  63. package/dist/e2e.js +2 -2
  64. package/dist/edit.js +8 -8
  65. package/dist/exec.js +2 -2
  66. package/dist/fetch.js +2 -2
  67. package/dist/format.js +4 -4
  68. package/dist/{generic-parser-VUXDIAOZ.js → generic-parser-WFMIRHBX.js} +2 -2
  69. package/dist/git.js +2 -2
  70. package/dist/glob.js +2 -2
  71. package/dist/grep.d.ts +2 -0
  72. package/dist/grep.js +4 -2
  73. package/dist/index.js +46 -46
  74. package/dist/install.js +4 -4
  75. package/dist/json.js +2 -2
  76. package/dist/kanban-tool-types.d.ts +2 -0
  77. package/dist/kanban.js +2 -2
  78. package/dist/languages/index.js +2 -2
  79. package/dist/{legacy-bridge-SFNXIRAP.js → legacy-bridge-3T7SR4SK.js} +4 -4
  80. package/dist/lint.js +4 -4
  81. package/dist/logs.js +2 -2
  82. package/dist/outdated.js +4 -2
  83. package/dist/pack.js +45 -45
  84. package/dist/{parser-dispatch-X62UWXJS.js → parser-dispatch-QGC4HSP5.js} +4 -4
  85. package/dist/patch.js +8 -8
  86. package/dist/plan.js +4 -4
  87. package/dist/{py-parser-TDXSQZBC.js → py-parser-RFWG2HYI.js} +3 -3
  88. package/dist/read.js +8 -8
  89. package/dist/replace.js +9 -8
  90. package/dist/scaffold.js +2 -2
  91. package/dist/search.js +1 -1
  92. package/dist/session-kanban.js +1 -1
  93. package/dist/task.js +4 -4
  94. package/dist/test.js +4 -4
  95. package/dist/todo.js +3 -3
  96. package/dist/tool-tier.js +46 -46
  97. package/dist/{tree-sitter-parser-AIERW3YI.js → tree-sitter-parser-GILYQLS4.js} +2 -2
  98. package/dist/tree.js +2 -2
  99. package/dist/typecheck.js +4 -4
  100. package/dist/write.js +8 -8
  101. package/package.json +5 -5
@@ -20,10 +20,11 @@ import {
20
20
  COMMAND_OUTPUT_MAX_BYTES,
21
21
  normalizeCommandOutput,
22
22
  safeResolveReal
23
- } from "./chunk-D33S4RQG.js";
23
+ } from "./chunk-ZUFHAHTE.js";
24
24
 
25
25
  // src/exec.ts
26
26
  import { spawn } from "node:child_process";
27
+ import { StringDecoder } from "node:string_decoder";
27
28
  import {
28
29
  emitProcessCompleted,
29
30
  emitProcessOutput,
@@ -32,13 +33,12 @@ import {
32
33
  import { toErrorMessage } from "@wrongstack/core/utils/error";
33
34
 
34
35
  // src/_danger-detect.ts
35
- var argHas = (args, value) => args.includes(value);
36
36
  var argMatches = (args, re) => args.some((a) => re.test(a));
37
37
  var flagLetters = (args) => {
38
38
  const seen = /* @__PURE__ */ new Set();
39
39
  for (const a of args) {
40
40
  if (/^-[a-zA-Z]+$/.test(a)) {
41
- for (const ch of a.slice(1)) seen.add(ch);
41
+ for (const ch of a.slice(1)) seen.add(ch.toLowerCase());
42
42
  } else if (a === "--recursive") {
43
43
  seen.add("r");
44
44
  } else if (a === "--force") {
@@ -54,13 +54,29 @@ var RULES = [
54
54
  // want to do intentionally. We downgrade it to 'destructive' so the
55
55
  // confirm prompt can approve. Short clusters, GNU long forms
56
56
  // (`--recursive --force`), and mixed shapes all classify identically.
57
+ // The Windows cmd.exe shape classifies identically too: `rmdir /s /q`
58
+ // (and its `rd` alias) is the same operation — /s is the recursive half,
59
+ // /q the quiet (no per-directory prompt, i.e. the "force") half. /s alone
60
+ // still prompts in cmd, so it stays safe, mirroring the PowerShell rule's
61
+ // requirement for both -Recurse and -Force. `del`/`erase` (erase is a cmd
62
+ // alias of del) with /s is classified as well: `del /s` deletes matching
63
+ // files in the whole subtree WITHOUT any per-file prompt, so there /s
64
+ // alone is the recursive-force half (/f only overrides read-only, /q only
65
+ // mutes the global-wildcard "are you sure"). Non-recursive del/erase
66
+ // (single files, or a quiet single-directory wildcard) stays safe.
57
67
  {
58
68
  id: "rm-recursive",
59
69
  level: "destructive",
60
70
  test: (cmd, args) => {
61
- if (cmd !== "rm" && cmd !== "rmdir") return false;
71
+ if (cmd !== "rm" && cmd !== "rmdir" && cmd !== "rd" && cmd !== "del" && cmd !== "erase") {
72
+ return false;
73
+ }
62
74
  const letters = flagLetters(args);
63
- return letters.has("r") && letters.has("f");
75
+ if (letters.has("r") && letters.has("f")) return true;
76
+ const has = (flag) => args.some((a) => a.toLowerCase() === `/${flag}`);
77
+ if (cmd === "rmdir" || cmd === "rd") return has("s") && has("q");
78
+ if (cmd === "del" || cmd === "erase") return has("s");
79
+ return false;
64
80
  },
65
81
  reason: "recursive force-delete"
66
82
  },
@@ -70,9 +86,9 @@ var RULES = [
70
86
  level: "destructive",
71
87
  test: (cmd, args) => {
72
88
  if (cmd !== "powershell" && cmd !== "pwsh") return false;
73
- const hasRecurse = argMatches(args, /^-(?:R|Recurse|Recurse\s)/);
74
- const hasForce = argHas(args, "-Force") || argHas(args, "-F");
75
- if (argHas(args, "-WhatIf")) return false;
89
+ const hasRecurse = argMatches(args, /^-(?:r|recurse)(?::\$true)?$/i);
90
+ const hasForce = argMatches(args, /^-(?:f|force)(?::\$true)?$/i);
91
+ if (argMatches(args, /^-whatif(?::\$true)?$/i)) return false;
76
92
  return hasRecurse && hasForce;
77
93
  },
78
94
  reason: "Remove-Item with -Recurse -Force"
@@ -84,7 +100,9 @@ var RULES = [
84
100
  test: (cmd, args) => {
85
101
  if (cmd !== "powershell" && cmd !== "pwsh") return false;
86
102
  return args.some(
87
- (a) => /^(?:Format-Volume|Clear-Disk|Initialize-Disk|Remove-Partition|Clear-Volume)(?:\s|$)/i.test(a)
103
+ (a) => /^(?:Format-Volume|Clear-Disk|Initialize-Disk|Remove-Partition|Clear-Volume)(?:\s|$)/i.test(
104
+ a
105
+ )
88
106
  );
89
107
  },
90
108
  reason: "PowerShell disk/volume partition destruction"
@@ -191,6 +209,15 @@ var RULES = [
191
209
  // ----- VCS history rewrite (destructive) -----
192
210
  // `git push --force` / `-f` rewrites remote history. `--force-with-lease`
193
211
  // is the safer variant (checks remote hasn't moved) but still rewrites.
212
+ // A refspec prefixed with `+` (`git push origin +main`,
213
+ // `+HEAD:refs/heads/main`) is per-refspec force — documented git shorthand
214
+ // equivalent to `--force` for that ref — so it classifies identically. A
215
+ // `+` anywhere else in a refspec (branch `feature+fix`) is just a
216
+ // character, and a leading `^` EXCLUDES the ref (with --all/--mirror);
217
+ // neither is force syntax. `--dry-run` / `-n` rewrites nothing and is
218
+ // exempt, like the PowerShell -WhatIf carve-out above (combined short
219
+ // clusters like `-nf` count as dry-run too — git push's short flags are
220
+ // only -q -v -f -n -u -o, so an 'n' in a cluster means dry-run).
194
221
  {
195
222
  id: "git-push-force",
196
223
  level: "destructive",
@@ -198,11 +225,18 @@ var RULES = [
198
225
  if (cmd !== "git") return false;
199
226
  const pushIdx = args.indexOf("push");
200
227
  if (pushIdx < 0) return false;
228
+ if (args.slice(pushIdx + 1).some((a) => a === "--dry-run" || a === "-n" || /^-[a-z]*n[a-z]*$/i.test(a))) {
229
+ return false;
230
+ }
201
231
  for (let i = pushIdx + 1; i < args.length; i++) {
202
232
  const a = args[i];
203
233
  if (a === "--force" || a === "-f" || a === "--force-with-lease") return true;
204
- if (!a.startsWith("-") && !a.includes("=")) continue;
234
+ if (!a.startsWith("-") && !a.includes("=")) {
235
+ if (a.startsWith("+")) return true;
236
+ continue;
237
+ }
205
238
  if (a.startsWith("--force")) return true;
239
+ if (/^-[a-z]+$/i.test(a) && a.toLowerCase().includes("f")) return true;
206
240
  }
207
241
  return false;
208
242
  },
@@ -1507,7 +1541,8 @@ var execTool = {
1507
1541
  };
1508
1542
  }
1509
1543
  const args = (input.args ?? []).slice(0, MAX_ARGS);
1510
- const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS));
1544
+ const rawTimeout = typeof input.timeout === "number" && !Number.isNaN(input.timeout) ? input.timeout : DEFAULT_TIMEOUT_MS;
1545
+ const timeout = Math.max(1, Math.min(rawTimeout, MAX_TIMEOUT_MS));
1511
1546
  const danger = detectDanger(cmd, args, dangerBypass);
1512
1547
  const killCheck = await checkExecKillCommand(cmd, args);
1513
1548
  if (killCheck.blocked) {
@@ -1552,7 +1587,30 @@ var execTool = {
1552
1587
  };
1553
1588
  }
1554
1589
  const signal = opts?.signal ?? ctx.signal ?? new AbortController().signal;
1590
+ if (signal.aborted) {
1591
+ return {
1592
+ command: cmd,
1593
+ args,
1594
+ stdout: "",
1595
+ stderr: "Aborted",
1596
+ exitCode: 124,
1597
+ truncated: false,
1598
+ allowed: true,
1599
+ danger
1600
+ };
1601
+ }
1555
1602
  return runCommand(cmd, args, cwd, timeout, signal, ctx.session?.id, danger);
1603
+ },
1604
+ async cleanup(_input, ctx) {
1605
+ const registry = getProcessRegistry();
1606
+ const sessionId = ctx.session?.id;
1607
+ if (!sessionId) return;
1608
+ for (const entry of registry.bySession(sessionId)) {
1609
+ if (entry.name !== "exec") continue;
1610
+ if (entry.child && entry.child.exitCode !== null) continue;
1611
+ if (entry.protected) continue;
1612
+ registry.kill(entry.pid, { force: true });
1613
+ }
1556
1614
  }
1557
1615
  };
1558
1616
  function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
@@ -1572,6 +1630,20 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
1572
1630
  let telemetryCompleted = false;
1573
1631
  let timedOut = false;
1574
1632
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT });
1633
+ if (signal.aborted) {
1634
+ spool.finalize();
1635
+ finish({
1636
+ command: cmd,
1637
+ args,
1638
+ stdout: "",
1639
+ stderr: "Aborted",
1640
+ exitCode: 124,
1641
+ truncated: false,
1642
+ allowed: true,
1643
+ danger
1644
+ });
1645
+ return;
1646
+ }
1575
1647
  const resolved = resolveWin32Command(cmd);
1576
1648
  const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
1577
1649
  const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
@@ -1681,19 +1753,25 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
1681
1753
  if (signal.aborted) onAbort();
1682
1754
  else signal.addEventListener("abort", onAbort, { once: true });
1683
1755
  }
1756
+ const stdoutDecoder = new StringDecoder("utf8");
1757
+ const stderrDecoder = new StringDecoder("utf8");
1684
1758
  child.stdout?.on("data", (chunk) => {
1685
- const text = chunk.toString();
1759
+ const text = stdoutDecoder.write(chunk);
1686
1760
  stdoutBytes += chunk.byteLength;
1687
1761
  emitProcessOutput({ pid, stream: "stdout", chunk });
1688
- if (stdout.length < MAX_OUTPUT) stdout += text;
1689
- spool.write(text);
1762
+ if (text.length > 0) {
1763
+ if (stdout.length < MAX_OUTPUT) stdout += text.slice(0, MAX_OUTPUT - stdout.length);
1764
+ spool.write(text);
1765
+ }
1690
1766
  });
1691
1767
  child.stderr?.on("data", (chunk) => {
1692
- const text = chunk.toString();
1768
+ const text = stderrDecoder.write(chunk);
1693
1769
  stderrBytes += chunk.byteLength;
1694
1770
  emitProcessOutput({ pid, stream: "stderr", chunk });
1695
- if (stderr.length < MAX_OUTPUT) stderr += text;
1696
- spool.write(text);
1771
+ if (text.length > 0) {
1772
+ if (stderr.length < MAX_OUTPUT) stderr += text.slice(0, MAX_OUTPUT - stderr.length);
1773
+ spool.write(text);
1774
+ }
1697
1775
  });
1698
1776
  child.on("close", (code) => {
1699
1777
  clearTimeout(timer);
@@ -1703,14 +1781,25 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
1703
1781
  const exitCode = killed ? 124 : code ?? 1;
1704
1782
  emitCompletedOnce(exitCode, pid);
1705
1783
  registry.afterCall(durationMs, exitCode !== 0);
1784
+ const stdoutTail = stdoutDecoder.end();
1785
+ if (stdoutTail) {
1786
+ if (stdout.length < MAX_OUTPUT) stdout += stdoutTail.slice(0, MAX_OUTPUT - stdout.length);
1787
+ spool.write(stdoutTail);
1788
+ }
1789
+ const stderrTail = stderrDecoder.end();
1790
+ if (stderrTail) {
1791
+ if (stderr.length < MAX_OUTPUT) stderr += stderrTail.slice(0, MAX_OUTPUT - stderr.length);
1792
+ spool.write(stderrTail);
1793
+ }
1706
1794
  const spooled = spool.finalize();
1795
+ const isTruncated = stdoutBytes > MAX_OUTPUT || stderrBytes > MAX_OUTPUT || Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES || Buffer.byteLength(stderr, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
1707
1796
  finish({
1708
1797
  command: cmd,
1709
1798
  args,
1710
1799
  stdout: normalizeCommandOutput(stdout) + (spooled ? spoolNote(spooled) : ""),
1711
1800
  stderr: normalizeCommandOutput(stderr),
1712
1801
  exitCode,
1713
- truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES || Buffer.byteLength(stderr, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
1802
+ truncated: isTruncated,
1714
1803
  allowed: true,
1715
1804
  danger
1716
1805
  });
@@ -1730,4 +1819,4 @@ export {
1730
1819
  getExecAllowlist,
1731
1820
  execTool
1732
1821
  };
1733
- //# sourceMappingURL=chunk-WI3VDXHD.js.map
1822
+ //# sourceMappingURL=chunk-BSJSNVGA.js.map
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-NBDZMMG2.js";
6
6
  import {
7
7
  truncateMiddle
8
- } from "./chunk-D33S4RQG.js";
8
+ } from "./chunk-ZUFHAHTE.js";
9
9
 
10
10
  // src/fetch.ts
11
11
  import { FetchError, ToolError, ToolValidationError } from "@wrongstack/core/types";
@@ -79,13 +79,29 @@ var fetchTool = {
79
79
  return final;
80
80
  },
81
81
  async *executeStream(input, ctx, opts) {
82
- if (!input?.url) {
82
+ if (!input?.url || typeof input.url !== "string" || !input.url.trim()) {
83
83
  throw new ToolValidationError({
84
- message: "fetch: url is required",
84
+ message: "fetch: url is required and cannot be empty",
85
85
  field: "url"
86
86
  });
87
87
  }
88
- const u = new URL(input.url);
88
+ const trimmedUrl = input.url.trim();
89
+ let u;
90
+ try {
91
+ u = new URL(trimmedUrl);
92
+ } catch {
93
+ throw new ToolValidationError({
94
+ message: `fetch: invalid URL "${input.url}"`,
95
+ field: "url"
96
+ });
97
+ }
98
+ const ALLOWED_FORMATS = /* @__PURE__ */ new Set(["markdown", "text", "raw"]);
99
+ if (input.format !== void 0 && !ALLOWED_FORMATS.has(input.format)) {
100
+ throw new ToolValidationError({
101
+ message: `fetch: unsupported format "${input.format}". Allowed formats: markdown, text, raw`,
102
+ field: "format"
103
+ });
104
+ }
89
105
  if (u.username || u.password) {
90
106
  throw new ToolValidationError({
91
107
  message: "fetch: URLs with embedded credentials (user:pass@host) are not allowed",
@@ -105,8 +121,9 @@ var fetchTool = {
105
121
  });
106
122
  }
107
123
  await assertNotPrivate(u.hostname);
108
- yield { type: "log", text: `GET ${input.url}` };
124
+ yield { type: "log", text: `GET ${trimmedUrl}` };
109
125
  const signal = opts?.signal ?? ctx.signal ?? new AbortController().signal;
126
+ signal.throwIfAborted();
110
127
  const ctrl = new AbortController();
111
128
  const timer = setTimeout(
112
129
  () => ctrl.abort(
@@ -122,10 +139,10 @@ var fetchTool = {
122
139
  try {
123
140
  let res;
124
141
  try {
125
- res = await guardedFetch(input.url, 5, combined);
142
+ res = await guardedFetch(trimmedUrl, 5, combined);
126
143
  } catch (err) {
127
144
  if (signal.aborted) throw err;
128
- throw describeFetchError(err, input.url, ctrl.signal.aborted);
145
+ throw describeFetchError(err, trimmedUrl, ctrl.signal.aborted);
129
146
  }
130
147
  const ct = res.headers.get("content-type") ?? "application/octet-stream";
131
148
  if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
@@ -146,31 +163,46 @@ var fetchTool = {
146
163
  let pendingBytes = 0;
147
164
  const FLUSH_AT = 4 * 1024;
148
165
  if (reader) {
149
- for (; ; ) {
150
- const { value, done } = await reader.read();
151
- if (done) break;
152
- if (!value) continue;
153
- received += value.byteLength;
154
- pendingBytes += value.byteLength;
155
- chunks.push(value);
156
- if (pendingBytes >= FLUSH_AT) {
157
- const recent = Buffer.from(value).toString("utf-8");
158
- yield {
159
- type: "partial_output",
160
- text: recent,
161
- data: { received }
162
- };
163
- pendingBytes = 0;
166
+ try {
167
+ for (; ; ) {
168
+ const { value, done } = await reader.read();
169
+ if (done) break;
170
+ if (!value) continue;
171
+ received += value.byteLength;
172
+ pendingBytes += value.byteLength;
173
+ chunks.push(value);
174
+ if (pendingBytes >= FLUSH_AT) {
175
+ const recent = Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString(
176
+ "utf8"
177
+ );
178
+ yield {
179
+ type: "partial_output",
180
+ text: recent,
181
+ data: { received }
182
+ };
183
+ pendingBytes = 0;
184
+ }
185
+ if (received > MAX_BYTES) {
186
+ await reader.cancel().catch(() => {
187
+ });
188
+ break;
189
+ }
164
190
  }
165
- if (received > MAX_BYTES) break;
191
+ } finally {
192
+ reader.releaseLock();
166
193
  }
167
194
  }
168
- const text = Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf8");
169
- const format = input.format ?? (ct.includes("text/html") ? "markdown" : "text");
195
+ const text = Buffer.concat(chunks).toString("utf8");
196
+ const isHtml = ct.includes("text/html");
197
+ const isJson = /[\/+]json(;|$)/i.test(ct);
198
+ const format = input.format ?? (isHtml ? "markdown" : "text");
170
199
  let content;
171
200
  if (format === "raw") content = text;
172
- else if (format === "markdown" && ct.includes("text/html")) content = TD.turndown(text);
173
- else if (ct.includes("application/json")) content = prettyJson(text);
201
+ else if (format === "markdown" && isHtml) content = TD.turndown(text);
202
+ else if (format === "markdown" && isJson) content = `\`\`\`json
203
+ ${prettyJson(text)}
204
+ \`\`\``;
205
+ else if (isJson) content = prettyJson(text);
174
206
  else content = text;
175
207
  yield {
176
208
  type: "final",
@@ -226,6 +258,7 @@ function describeFetchError(err, url, timedOut) {
226
258
  });
227
259
  }
228
260
  function prettyJson(s) {
261
+ if (!s || s.length > 5e5) return s;
229
262
  try {
230
263
  return JSON.stringify(JSON.parse(s), null, 2);
231
264
  } catch {
@@ -236,4 +269,4 @@ function prettyJson(s) {
236
269
  export {
237
270
  fetchTool
238
271
  };
239
- //# sourceMappingURL=chunk-UKGMDCXT.js.map
272
+ //# sourceMappingURL=chunk-C2DQKVLO.js.map
@@ -3,12 +3,12 @@ import {
3
3
  } from "./chunk-FQZOHLG3.js";
4
4
  import {
5
5
  enqueueReindex
6
- } from "./chunk-VZLX2JZG.js";
6
+ } from "./chunk-DP6CXMJZ.js";
7
7
  import {
8
8
  safeResolveReal,
9
9
  sha256hex,
10
10
  truncateDiffPayload
11
- } from "./chunk-D33S4RQG.js";
11
+ } from "./chunk-ZUFHAHTE.js";
12
12
 
13
13
  // src/edit.ts
14
14
  import * as fs from "node:fs/promises";
@@ -41,43 +41,50 @@ var FUZZY_MAX_INTERIOR_CHARS = 2e3;
41
41
  function findLadderMatches(fileLf, oldLf) {
42
42
  const exact = [];
43
43
  let idx = fileLf.indexOf(oldLf);
44
+ let currentLine = 1;
45
+ let lastPos = 0;
44
46
  while (idx !== -1) {
45
- exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt(fileLf, idx) });
47
+ while (lastPos < idx) {
48
+ const nextNewline = fileLf.indexOf("\n", lastPos);
49
+ if (nextNewline === -1 || nextNewline >= idx) break;
50
+ currentLine++;
51
+ lastPos = nextNewline + 1;
52
+ }
53
+ exact.push({ start: idx, end: idx + oldLf.length, startLine: currentLine });
46
54
  idx = fileLf.indexOf(oldLf, idx + 1);
47
55
  }
48
56
  if (exact.length > 0) return { tier: "exact", matches: exact };
49
57
  const fileLines = fileLf.split("\n");
50
- const needleLines = oldLf.split("\n");
51
- if (needleLines.length > fileLines.length) return void 0;
58
+ const needleEndsWithNewline = oldLf.endsWith("\n");
59
+ const rawNeedleLines = oldLf.split("\n");
60
+ const needleLines = needleEndsWithNewline && rawNeedleLines[rawNeedleLines.length - 1] === "" ? rawNeedleLines.slice(0, -1) : rawNeedleLines;
61
+ if (needleLines.length === 0 || needleLines.length > fileLines.length) return void 0;
52
62
  const offsets = lineOffsets(fileLines);
53
63
  const fileTrimEnd = fileLines.map((l) => l.trimEnd());
54
64
  const needleTrimEnd = needleLines.map((l) => l.trimEnd());
55
- const trailing = windowScan(fileTrimEnd, needleTrimEnd, offsets, fileLines, (a, b) => a === b);
65
+ const trailing = windowScan(
66
+ fileTrimEnd,
67
+ needleTrimEnd,
68
+ offsets,
69
+ fileLines,
70
+ (a, b) => a === b,
71
+ needleEndsWithNewline
72
+ );
56
73
  if (trailing.length > 0) return { tier: "trailing-whitespace", matches: trailing };
57
74
  const normalizedLen = needleTrimEnd.reduce((n, l) => n + l.trimStart().length, 0);
58
75
  if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return void 0;
59
76
  const fileTrimmed = fileTrimEnd.map((l) => l.trimStart());
60
77
  const needleTrimmed = needleTrimEnd.map((l) => l.trimStart());
61
- const normalized = windowScan(fileTrimmed, needleTrimmed, offsets, fileLines, (a, b) => a === b);
78
+ const normalized = windowScan(
79
+ fileTrimmed,
80
+ needleTrimmed,
81
+ offsets,
82
+ fileLines,
83
+ (a, b) => a === b,
84
+ needleEndsWithNewline
85
+ );
62
86
  if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
63
- return fuzzyScan(fileLines, needleLines, offsets);
64
- }
65
- function lineAt(text, pos) {
66
- if (pos < 512) {
67
- let line2 = 1;
68
- for (let i = 0; i < pos; i++) {
69
- if (text.charCodeAt(i) === 10) line2++;
70
- }
71
- return line2;
72
- }
73
- let line = 1;
74
- let search = 0;
75
- while (true) {
76
- const idx = text.indexOf("\n", search);
77
- if (idx === -1 || idx >= pos) return line;
78
- search = idx + 1;
79
- line++;
80
- }
87
+ return fuzzyScan(fileLines, needleLines, offsets, needleEndsWithNewline);
81
88
  }
82
89
  function lineOffsets(lines) {
83
90
  const out = new Array(lines.length);
@@ -88,15 +95,17 @@ function lineOffsets(lines) {
88
95
  }
89
96
  return out;
90
97
  }
91
- function windowToMatch(fileLines, offsets, start, windowLen) {
98
+ function windowToMatch(fileLines, offsets, start, windowLen, includeTrailingNewline = false) {
92
99
  const lastLine = start + windowLen - 1;
100
+ const lineEnd = offsets[lastLine] + fileLines[lastLine].length;
101
+ const end = includeTrailingNewline && lastLine < fileLines.length - 1 ? lineEnd + 1 : lineEnd;
93
102
  return {
94
103
  start: offsets[start],
95
- end: offsets[lastLine] + fileLines[lastLine].length,
104
+ end,
96
105
  startLine: start + 1
97
106
  };
98
107
  }
99
- function windowScan(comparisonLines, needleLines, offsets, originalLines, eq) {
108
+ function windowScan(comparisonLines, needleLines, offsets, originalLines, eq, includeTrailingNewline = false) {
100
109
  const n = needleLines.length;
101
110
  const out = [];
102
111
  for (let i = 0; i + n <= comparisonLines.length; i++) {
@@ -108,28 +117,32 @@ function windowScan(comparisonLines, needleLines, offsets, originalLines, eq) {
108
117
  }
109
118
  }
110
119
  if (all) {
111
- out.push(windowToMatch(originalLines, offsets, i, n));
120
+ out.push(windowToMatch(originalLines, offsets, i, n, includeTrailingNewline));
112
121
  i += n - 1;
113
122
  }
114
123
  }
115
124
  return out;
116
125
  }
117
- function fuzzyScan(fileLines, needleLines, offsets) {
126
+ function fuzzyScan(fileLines, needleLines, offsets, includeTrailingNewline = false) {
118
127
  const n = needleLines.length;
119
128
  if (n < 3) return void 0;
120
129
  const firstNeedle = needleLines[0].trim();
121
130
  const lastNeedle = needleLines[n - 1].trim();
122
131
  const needleInterior = needleLines.slice(1, -1).map((l) => l.trim()).join("\n");
123
132
  if (needleInterior.length > FUZZY_MAX_INTERIOR_CHARS) return void 0;
133
+ const fileTrimmed = fileLines.map((l) => l.trim());
124
134
  const candidates = [];
125
135
  for (let i = 0; i + n <= fileLines.length; i++) {
126
- if (fileLines[i].trim() !== firstNeedle) continue;
127
- if (fileLines[i + n - 1].trim() !== lastNeedle) continue;
128
- const windowInterior = fileLines.slice(i + 1, i + n - 1).map((l) => l.trim()).join("\n");
136
+ if (fileTrimmed[i] !== firstNeedle) continue;
137
+ if (fileTrimmed[i + n - 1] !== lastNeedle) continue;
138
+ const windowInterior = fileTrimmed.slice(i + 1, i + n - 1).join("\n");
129
139
  if (windowInterior.length > FUZZY_MAX_INTERIOR_CHARS) continue;
130
140
  const score = similarity(needleInterior, windowInterior);
131
141
  if (score >= FUZZY_MIN_SIMILARITY) {
132
- candidates.push({ match: windowToMatch(fileLines, offsets, i, n), score });
142
+ candidates.push({
143
+ match: windowToMatch(fileLines, offsets, i, n, includeTrailingNewline),
144
+ score
145
+ });
133
146
  }
134
147
  }
135
148
  if (candidates.length === 0) return void 0;
@@ -150,21 +163,29 @@ function similarity(a, b) {
150
163
  if (a === b) return 1;
151
164
  const max = Math.max(a.length, b.length);
152
165
  if (max === 0) return 1;
153
- if (Math.abs(a.length - b.length) / max > 1 - FUZZY_MIN_SIMILARITY + 0.05) {
154
- return 1 - Math.abs(a.length - b.length) / max;
166
+ const lenDiff = Math.abs(a.length - b.length);
167
+ if (lenDiff / max > 1 - FUZZY_MIN_SIMILARITY) {
168
+ return 1 - lenDiff / max;
155
169
  }
156
170
  return 1 - levenshtein(a, b) / max;
157
171
  }
158
172
  function levenshtein(a, b) {
159
173
  if (a.length === 0) return b.length;
160
174
  if (b.length === 0) return a.length;
161
- let prev = new Array(b.length + 1);
162
- let cur = new Array(b.length + 1);
163
- for (let j = 0; j <= b.length; j++) prev[j] = j;
164
- for (let i = 1; i <= a.length; i++) {
175
+ if (a.length < b.length) {
176
+ const tmp = a;
177
+ a = b;
178
+ b = tmp;
179
+ }
180
+ const bLen = b.length;
181
+ const aLen = a.length;
182
+ let prev = new Int32Array(bLen + 1);
183
+ let cur = new Int32Array(bLen + 1);
184
+ for (let j = 0; j <= bLen; j++) prev[j] = j;
185
+ for (let i = 1; i <= aLen; i++) {
165
186
  cur[0] = i;
166
187
  const ca = a.charCodeAt(i - 1);
167
- for (let j = 1; j <= b.length; j++) {
188
+ for (let j = 1; j <= bLen; j++) {
168
189
  const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
169
190
  cur[j] = Math.min(
170
191
  prev[j] + 1,
@@ -172,9 +193,11 @@ function levenshtein(a, b) {
172
193
  prev[j - 1] + cost
173
194
  );
174
195
  }
175
- [prev, cur] = [cur, prev];
196
+ const tmp = prev;
197
+ prev = cur;
198
+ cur = tmp;
176
199
  }
177
- return prev[b.length];
200
+ return prev[bLen];
178
201
  }
179
202
  function firstLineIndent(text) {
180
203
  for (const line of text.split("\n")) {
@@ -189,14 +212,17 @@ function adjustIndent(newLf, fromIndent, toIndent) {
189
212
  if (toIndent.startsWith(fromIndent)) {
190
213
  const extra = toIndent.slice(fromIndent.length);
191
214
  return {
192
- text: lines.map((l) => l.trim() === "" ? l : extra + l).join("\n"),
215
+ text: lines.map((l) => l.trim() === "" ? "" : extra + l).join("\n"),
193
216
  adjusted: true
194
217
  };
195
218
  }
196
219
  if (fromIndent.startsWith(toIndent)) {
197
220
  const remove = fromIndent.slice(toIndent.length);
198
221
  return {
199
- text: lines.map((l) => l.startsWith(remove) ? l.slice(remove.length) : l).join("\n"),
222
+ text: lines.map((l) => {
223
+ if (l.trim() === "") return "";
224
+ return l.startsWith(remove) ? l.slice(remove.length) : l;
225
+ }).join("\n"),
200
226
  adjusted: true
201
227
  };
202
228
  }
@@ -204,34 +230,50 @@ function adjustIndent(newLf, fromIndent, toIndent) {
204
230
  }
205
231
  function nearestMatchHint(fileLf, oldLf) {
206
232
  const fileLines = fileLf.split("\n");
207
- const needleLines = oldLf.split("\n");
208
- if (needleLines.length > fileLines.length) return void 0;
233
+ const rawNeedle = oldLf.split("\n");
234
+ const needleLines = oldLf.endsWith("\n") && rawNeedle[rawNeedle.length - 1] === "" ? rawNeedle.slice(0, -1) : rawNeedle;
235
+ const n = needleLines.length;
236
+ if (n === 0 || n > fileLines.length) return void 0;
209
237
  const needleTrimmed = needleLines.map((l) => l.trim());
238
+ const fileTrimmed = fileLines.map((l) => l.trim());
210
239
  let bestScore = 0;
211
240
  let bestStart = -1;
212
- for (let i = 0; i + needleLines.length <= fileLines.length; i++) {
241
+ const maxI = fileLines.length - n;
242
+ for (let i = 0; i <= maxI; i++) {
213
243
  let sum = 0;
214
- for (let j = 0; j < needleLines.length; j++) {
215
- sum += prefixSimilarity(needleTrimmed[j], fileLines[i + j].trim());
244
+ let possible = true;
245
+ for (let j = 0; j < n; j++) {
246
+ sum += lineSimilarity(needleTrimmed[j], fileTrimmed[i + j]);
247
+ if (sum + (n - 1 - j) <= bestScore * n) {
248
+ possible = false;
249
+ break;
250
+ }
216
251
  }
217
- const score = sum / needleLines.length;
218
- if (score > bestScore) {
219
- bestScore = score;
220
- bestStart = i;
252
+ if (possible) {
253
+ const score = sum / n;
254
+ if (score > bestScore) {
255
+ bestScore = score;
256
+ bestStart = i;
257
+ }
221
258
  }
222
259
  }
223
260
  if (bestStart === -1 || bestScore < 0.5) return void 0;
224
- const snippet = fileLines.slice(bestStart, bestStart + Math.min(3, needleLines.length)).map((l) => l.length > 120 ? `${l.slice(0, 120)}\u2026` : l).join("\n");
261
+ const snippet = fileLines.slice(bestStart, bestStart + Math.min(3, n)).map((l) => l.length > 120 ? `${l.slice(0, 120)}\u2026` : l).join("\n");
225
262
  return { line: bestStart + 1, snippet };
226
263
  }
227
- function prefixSimilarity(a, b) {
264
+ function lineSimilarity(a, b) {
228
265
  if (a === b) return 1;
229
266
  const max = Math.max(a.length, b.length);
230
267
  if (max === 0) return 1;
268
+ if (a.includes(b) || b.includes(a)) {
269
+ return Math.min(a.length, b.length) / max;
270
+ }
231
271
  let p = 0;
232
272
  const lim = Math.min(a.length, b.length);
233
273
  while (p < lim && a.charCodeAt(p) === b.charCodeAt(p)) p++;
234
- return p / max;
274
+ let s = 0;
275
+ while (s < lim - p && a.charCodeAt(a.length - 1 - s) === b.charCodeAt(b.length - 1 - s)) s++;
276
+ return (p + s) / max;
235
277
  }
236
278
 
237
279
  // src/edit.ts
@@ -473,4 +515,4 @@ Compare this against your old_string and retry with the file's actual text.` : "
473
515
  export {
474
516
  editTool
475
517
  };
476
- //# sourceMappingURL=chunk-CIFAIIHI.js.map
518
+ //# sourceMappingURL=chunk-CFLJFDYV.js.map