@cryer/star-cli 0.3.12 → 0.3.14

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 (33) hide show
  1. package/README.md +17 -5
  2. package/README.zh-CN.md +15 -5
  3. package/dist/{chunk-LSYGHLRU.js → chunk-2ISBRSBA.js} +479 -296
  4. package/dist/chunk-2ISBRSBA.js.map +1 -0
  5. package/dist/{chunk-F5RWZMQY.js → chunk-4HANB3DJ.js} +458 -221
  6. package/dist/chunk-4HANB3DJ.js.map +1 -0
  7. package/dist/chunk-4IRY44TG.js +0 -0
  8. package/dist/{chunk-Q5FJN4LN.js → chunk-54KH4JSO.js} +213 -125
  9. package/dist/chunk-54KH4JSO.js.map +1 -0
  10. package/dist/{chunk-MYHD3SZB.js → chunk-CECMCDZG.js} +43 -30
  11. package/dist/chunk-CECMCDZG.js.map +1 -0
  12. package/dist/{chunk-MVAMPVTA.js → chunk-KILMI3S5.js} +13 -5
  13. package/dist/{chunk-MVAMPVTA.js.map → chunk-KILMI3S5.js.map} +1 -1
  14. package/dist/{chunk-2GAUAYCU.js → chunk-NRLGGA7L.js} +507 -175
  15. package/dist/chunk-NRLGGA7L.js.map +1 -0
  16. package/dist/loop-M64D5YII.js +17 -0
  17. package/dist/main.js +177 -131
  18. package/dist/main.js.map +1 -1
  19. package/dist/{provider-EWMUPBLH.js → provider-3Z4B7RMF.js} +2 -2
  20. package/dist/{repl-SSCURTAH.js → repl-N2H7JDSH.js} +617 -327
  21. package/dist/repl-N2H7JDSH.js.map +1 -0
  22. package/dist/{tools-BH55BVIS.js → tools-5RKM7ZI3.js} +3 -3
  23. package/package.json +16 -25
  24. package/dist/chunk-2GAUAYCU.js.map +0 -1
  25. package/dist/chunk-F5RWZMQY.js.map +0 -1
  26. package/dist/chunk-LSYGHLRU.js.map +0 -1
  27. package/dist/chunk-MYHD3SZB.js.map +0 -1
  28. package/dist/chunk-Q5FJN4LN.js.map +0 -1
  29. package/dist/loop-BYXBNFFY.js +0 -17
  30. package/dist/repl-SSCURTAH.js.map +0 -1
  31. /package/dist/{loop-BYXBNFFY.js.map → loop-M64D5YII.js.map} +0 -0
  32. /package/dist/{provider-EWMUPBLH.js.map → provider-3Z4B7RMF.js.map} +0 -0
  33. /package/dist/{tools-BH55BVIS.js.map → tools-5RKM7ZI3.js.map} +0 -0
File without changes
@@ -18,11 +18,12 @@ import {
18
18
  nextSnapshotId,
19
19
  probeImageDimensions,
20
20
  pushSnapshot,
21
+ realpathForCheck,
21
22
  sharedTodoStore,
22
23
  starHome,
23
24
  walkFiles,
24
25
  webFetchTool
25
- } from "./chunk-LSYGHLRU.js";
26
+ } from "./chunk-2ISBRSBA.js";
26
27
 
27
28
  // src/tools/fs/edit.ts
28
29
  import { readFile, stat, writeFile } from "fs/promises";
@@ -41,6 +42,31 @@ function countOccurrences(haystack, needle) {
41
42
  function normalizeEol(text) {
42
43
  return text.replace(/\r\n/g, "\n");
43
44
  }
45
+ function normalizedOffsets(text) {
46
+ const offsets = [0];
47
+ for (let i = 0; i < text.length; i++) {
48
+ if (text[i] === "\r" && text[i + 1] === "\n") i++;
49
+ offsets.push(i + 1);
50
+ }
51
+ return offsets;
52
+ }
53
+ function replacePreservingEol(raw, normalized, offsets, oldString, newString, replaceAll, useCrlf) {
54
+ const replacement = useCrlf ? newString.replace(/\n/g, "\r\n") : newString;
55
+ let normalizedFrom = 0;
56
+ let rawFrom = 0;
57
+ let updated = "";
58
+ while (true) {
59
+ const match = normalized.indexOf(oldString, normalizedFrom);
60
+ if (match < 0) break;
61
+ const rawStart = offsets[match] ?? raw.length;
62
+ const rawEnd = offsets[match + oldString.length] ?? raw.length;
63
+ updated += raw.slice(rawFrom, rawStart) + replacement;
64
+ rawFrom = rawEnd;
65
+ normalizedFrom = match + oldString.length;
66
+ if (!replaceAll) break;
67
+ }
68
+ return updated + raw.slice(rawFrom);
69
+ }
44
70
  var schema = z.object({
45
71
  path: z.string().describe("File path, absolute or relative to the working directory"),
46
72
  old_string: z.string().min(1).describe("Exact text to replace"),
@@ -54,6 +80,10 @@ var editFileTool = {
54
80
  parameters: schema,
55
81
  async execute(args, ctx) {
56
82
  const filePath = path.resolve(ctx.cwd, args.path);
83
+ const resolvedPath = await realpathForCheck(filePath);
84
+ if (isSensitivePath(filePath) || resolvedPath !== null && isSensitivePath(resolvedPath)) {
85
+ return { content: `Refused to edit sensitive file: ${args.path}`, isError: true };
86
+ }
57
87
  let content;
58
88
  let readMtimeMs;
59
89
  try {
@@ -76,7 +106,8 @@ var editFileTool = {
76
106
  let working = content;
77
107
  let oldString = args.old_string;
78
108
  let newString = args.new_string;
79
- let restoreEol = null;
109
+ let offsets = null;
110
+ let useCrlf = false;
80
111
  let count = countOccurrences(working, oldString);
81
112
  if (count === 0) {
82
113
  const normalizedContent = normalizeEol(content);
@@ -85,7 +116,8 @@ var editFileTool = {
85
116
  if (normalizedCount > 0) {
86
117
  const crlfCount = countOccurrences(content, "\r\n");
87
118
  const loneLfCount = countOccurrences(content.replace(/\r\n/g, ""), "\n");
88
- restoreEol = crlfCount > 0 && crlfCount >= loneLfCount ? (text) => text.replace(/\n/g, "\r\n") : null;
119
+ useCrlf = crlfCount > 0 && crlfCount >= loneLfCount;
120
+ offsets = normalizedOffsets(content);
89
121
  working = normalizedContent;
90
122
  oldString = normalizedOld;
91
123
  newString = normalizeEol(newString);
@@ -101,10 +133,15 @@ var editFileTool = {
101
133
  isError: true
102
134
  };
103
135
  }
104
- let updated = args.replace_all ? working.split(oldString).join(newString) : working.replace(oldString, () => newString);
105
- if (restoreEol) {
106
- updated = restoreEol(updated);
107
- }
136
+ const updated = offsets ? replacePreservingEol(
137
+ content,
138
+ working,
139
+ offsets,
140
+ oldString,
141
+ newString,
142
+ args.replace_all === true,
143
+ useCrlf
144
+ ) : args.replace_all ? working.split(oldString).join(newString) : working.replace(oldString, () => newString);
108
145
  try {
109
146
  const now = await stat(filePath).catch(() => null);
110
147
  if (!now || now.mtimeMs !== readMtimeMs) {
@@ -175,13 +212,17 @@ var globTool = {
175
212
  const prefixSegments = literalDirPrefix(args.pattern);
176
213
  const walkRoot = prefixSegments.length > 0 ? path2.join(root, ...prefixSegments) : root;
177
214
  const prefix = prefixSegments.length > 0 ? `${prefixSegments.join("/")}/` : "";
178
- const { files, aborted } = await walkFiles(walkRoot, void 0, {
215
+ let matched = [];
216
+ const { aborted } = await walkFiles(walkRoot, void 0, {
179
217
  ignore: createIgnorePredicate(ctx.cwd),
180
218
  signal: ctx.abortSignal,
181
219
  symlinkBoundary: ctx.cwd,
182
- nestedIgnore: true
220
+ nestedIgnore: true,
221
+ collect: false,
222
+ onBatch: (files) => {
223
+ matched = matched.concat(files.filter((f) => matchesGlob(args.pattern, `${prefix}${f.rel}`))).sort((a, b) => b.mtimeMs - a.mtimeMs || (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0)).slice(0, MAX_RESULTS);
224
+ }
183
225
  });
184
- const matched = files.filter((f) => matchesGlob(args.pattern, `${prefix}${f.rel}`)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
185
226
  if (matched.length === 0) {
186
227
  return {
187
228
  content: aborted ? `Search interrupted before any files matched pattern: ${args.pattern}` : `No files matched pattern: ${args.pattern}`
@@ -231,44 +272,13 @@ var grepTool = {
231
272
  return { content: `Invalid regular expression: ${err.message}`, isError: true };
232
273
  }
233
274
  const root = path3.resolve(ctx.cwd, args.path ?? ".");
234
- let files;
235
275
  let interrupted = false;
236
- try {
237
- const st = await stat2(root);
238
- if (st.isFile()) {
239
- const resolved = await realpath(root).catch(() => null);
240
- if (isSensitivePath(root) || resolved !== null && isSensitivePath(resolved)) {
241
- return {
242
- content: `Refused to grep sensitive file: ${args.path ?? "."}`,
243
- isError: true
244
- };
245
- }
246
- files = [{ abs: root, rel: path3.basename(root), mtimeMs: st.mtimeMs }];
247
- } else {
248
- const walked = await walkFiles(root, void 0, {
249
- ignore: createIgnorePredicate(ctx.cwd),
250
- withMtime: false,
251
- signal: ctx.abortSignal,
252
- symlinkBoundary: ctx.cwd,
253
- nestedIgnore: true
254
- });
255
- interrupted = walked.aborted;
256
- files = walked.files.filter(
257
- (f) => !isSensitivePath(f.abs) && (f.resolved === void 0 || !isSensitivePath(f.resolved))
258
- );
259
- }
260
- } catch {
261
- return { content: `Path not found: ${args.path ?? "."}`, isError: true };
262
- }
263
- if (args.glob) {
264
- const glob = args.glob;
265
- files = files.filter((f) => matchesGlob(glob, f.rel));
266
- }
276
+ const out = [];
277
+ let truncated = false;
278
+ let skippedLarge = 0;
267
279
  const scanFile = async (file) => {
268
280
  const st = await stat2(file.abs).catch(() => null);
269
- if (!st) {
270
- return null;
271
- }
281
+ if (!st) return null;
272
282
  if (st.size > MAX_FILE_BYTES) {
273
283
  skippedLarge += 1;
274
284
  return null;
@@ -279,12 +289,10 @@ var grepTool = {
279
289
  } catch {
280
290
  return null;
281
291
  }
282
- if (buf.includes(0)) {
283
- return null;
284
- }
292
+ if (buf.includes(0)) return null;
285
293
  const hits = [];
286
294
  const lines = buf.toString("utf8").split("\n");
287
- for (let i = 0; i < lines.length; i++) {
295
+ for (let i = 0; i < lines.length && hits.length < MAX_MATCHES; i += 1) {
288
296
  const line = lines[i] ?? "";
289
297
  if (re.test(line)) {
290
298
  const shown = line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}\u2026` : line;
@@ -293,29 +301,55 @@ var grepTool = {
293
301
  }
294
302
  return hits;
295
303
  };
296
- const out = [];
297
- let truncated = false;
298
- let skippedLarge = 0;
299
- outer: for (let i = 0; i < files.length; i += WALK_CONCURRENCY) {
300
- if (ctx.abortSignal?.aborted) {
301
- interrupted = true;
302
- break;
303
- }
304
- const chunk = await Promise.all(
305
- files.slice(i, i + WALK_CONCURRENCY).map((file) => scanFile(file))
304
+ const scanBatch = async (batch) => {
305
+ const files = batch.filter(
306
+ (f) => !isSensitivePath(f.abs) && (f.resolved === void 0 || !isSensitivePath(f.resolved)) && (!args.glob || matchesGlob(args.glob, f.rel))
306
307
  );
307
- for (const hits of chunk) {
308
- if (!hits) {
309
- continue;
308
+ outer: for (let i = 0; i < files.length; i += WALK_CONCURRENCY) {
309
+ if (ctx.abortSignal?.aborted) {
310
+ interrupted = true;
311
+ return false;
310
312
  }
311
- for (const hit of hits) {
312
- out.push(hit);
313
- if (out.length >= MAX_MATCHES) {
314
- truncated = true;
315
- break outer;
313
+ const chunk = await Promise.all(
314
+ files.slice(i, i + WALK_CONCURRENCY).map((file) => scanFile(file))
315
+ );
316
+ for (const hits of chunk) {
317
+ for (const hit of hits ?? []) {
318
+ out.push(hit);
319
+ if (out.length >= MAX_MATCHES) {
320
+ truncated = true;
321
+ break outer;
322
+ }
316
323
  }
317
324
  }
318
325
  }
326
+ return !truncated && !ctx.abortSignal?.aborted;
327
+ };
328
+ try {
329
+ const st = await stat2(root);
330
+ if (st.isFile()) {
331
+ const resolved = await realpath(root).catch(() => null);
332
+ if (isSensitivePath(root) || resolved !== null && isSensitivePath(resolved)) {
333
+ return {
334
+ content: `Refused to grep sensitive file: ${args.path ?? "."}`,
335
+ isError: true
336
+ };
337
+ }
338
+ await scanBatch([{ abs: root, rel: path3.basename(root), mtimeMs: st.mtimeMs }]);
339
+ } else {
340
+ const walked = await walkFiles(root, void 0, {
341
+ ignore: createIgnorePredicate(ctx.cwd),
342
+ withMtime: false,
343
+ signal: ctx.abortSignal,
344
+ symlinkBoundary: ctx.cwd,
345
+ nestedIgnore: true,
346
+ collect: false,
347
+ onBatch: scanBatch
348
+ });
349
+ interrupted = walked.aborted;
350
+ }
351
+ } catch {
352
+ return { content: `Path not found: ${args.path ?? "."}`, isError: true };
319
353
  }
320
354
  const skippedNote = skippedLarge > 0 ? `(skipped ${skippedLarge} file(s) larger than 5MB)` : null;
321
355
  if (out.length === 0) {
@@ -578,38 +612,82 @@ var codeOutlineTool = {
578
612
  // src/tools/fs/read.ts
579
613
  import { open, realpath as realpath3, stat as stat4 } from "fs/promises";
580
614
  import path5 from "path";
615
+ import { StringDecoder } from "string_decoder";
581
616
  import { z as z5 } from "zod";
582
617
  var DEFAULT_LIMIT = 2e3;
583
618
  var MAX_CHARS = 100 * 1024;
584
- var MAX_READ_BYTES = 1024 * 1024;
585
619
  var BINARY_PROBE_BYTES2 = 8192;
586
- function decodeUtf16be(buf) {
587
- const len = buf.length - buf.length % 2;
588
- const swapped = Buffer.allocUnsafe(len);
589
- for (let k = 0; k < len; k += 2) {
590
- swapped[k] = buf[k + 1] ?? 0;
591
- swapped[k + 1] = buf[k] ?? 0;
592
- }
593
- return swapped.toString("utf16le");
594
- }
595
- function decodeTextBuffer(buf) {
596
- if (buf.length >= 2 && buf[0] === 255 && buf[1] === 254) {
597
- return { text: buf.subarray(2).toString("utf16le") };
598
- }
599
- if (buf.length >= 2 && buf[0] === 254 && buf[1] === 255) {
600
- return { text: decodeUtf16be(buf.subarray(2)) };
601
- }
602
- if (buf.subarray(0, BINARY_PROBE_BYTES2).includes(0)) {
603
- return { binary: true };
604
- }
605
- return { text: buf.toString("utf8") };
606
- }
620
+ var READ_CHUNK_BYTES = 64 * 1024;
607
621
  var schema5 = z5.object({
608
622
  path: z5.string().describe("File path, absolute or relative to the working directory"),
609
623
  offset: z5.number().int().positive().optional().describe("1-based line number to start reading from"),
610
624
  limit: z5.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
611
625
  });
612
626
  var READ_CACHE_MAX_ENTRIES = 200;
627
+ async function readWindow(handle, size, start, limit) {
628
+ const probe = Buffer.alloc(Math.min(size, BINARY_PROBE_BYTES2));
629
+ const { bytesRead: probeBytes } = await handle.read(probe, 0, probe.length, 0);
630
+ const head = probe.subarray(0, probeBytes);
631
+ const utf16le = head[0] === 255 && head[1] === 254;
632
+ const utf16be = head[0] === 254 && head[1] === 255;
633
+ if (!utf16le && !utf16be && head.includes(0)) return { binary: true };
634
+ const decoder = new StringDecoder(utf16le || utf16be ? "utf16le" : "utf8");
635
+ const requestedEnd = start - 1 + limit;
636
+ const result = {
637
+ totalLines: 1,
638
+ out: [],
639
+ lastShown: start - 1,
640
+ truncatedBySize: false
641
+ };
642
+ let lineNumber = 1;
643
+ let outputSize = 0;
644
+ let pending = "";
645
+ let beRemainder;
646
+ const showLine = (raw) => {
647
+ if (lineNumber >= start && lineNumber <= requestedEnd && !result.truncatedBySize) {
648
+ const numbered = `${lineNumber} ${raw.replace(/\r$/, "")}`;
649
+ if (outputSize + numbered.length > MAX_CHARS) {
650
+ result.truncatedBySize = true;
651
+ } else {
652
+ result.out.push(numbered);
653
+ outputSize += numbered.length;
654
+ result.lastShown = lineNumber;
655
+ }
656
+ }
657
+ };
658
+ const accept = (text) => {
659
+ const parts = `${pending}${text}`.split("\n");
660
+ for (let i = 0; i < parts.length - 1; i += 1) {
661
+ showLine(parts[i] ?? "");
662
+ lineNumber += 1;
663
+ }
664
+ pending = parts[parts.length - 1] ?? "";
665
+ };
666
+ let position = utf16le || utf16be ? 2 : 0;
667
+ while (position < size) {
668
+ const length = Math.min(READ_CHUNK_BYTES, size - position);
669
+ const { bytesRead, buffer } = await handle.read(Buffer.alloc(length), 0, length, position);
670
+ if (bytesRead === 0) break;
671
+ position += bytesRead;
672
+ let chunk = buffer.subarray(0, bytesRead);
673
+ if (utf16be) {
674
+ const combined = beRemainder === void 0 ? chunk : Buffer.concat([Buffer.from([beRemainder]), chunk]);
675
+ const pairedLength = combined.length - combined.length % 2;
676
+ const swapped = Buffer.allocUnsafe(pairedLength);
677
+ for (let i = 0; i < pairedLength; i += 2) {
678
+ swapped[i] = combined[i + 1] ?? 0;
679
+ swapped[i + 1] = combined[i] ?? 0;
680
+ }
681
+ beRemainder = pairedLength < combined.length ? combined[combined.length - 1] : void 0;
682
+ chunk = swapped;
683
+ }
684
+ accept(decoder.write(chunk));
685
+ }
686
+ accept(decoder.end());
687
+ showLine(pending);
688
+ result.totalLines = lineNumber;
689
+ return result;
690
+ }
613
691
  function createReadFileTool() {
614
692
  const cache = /* @__PURE__ */ new Map();
615
693
  return {
@@ -643,55 +721,33 @@ function createReadFileTool() {
643
721
  };
644
722
  }
645
723
  }
646
- const bytesToRead = Math.min(st.size, MAX_READ_BYTES);
647
- const fileCapped = st.size > MAX_READ_BYTES;
648
724
  const handle = await open(filePath, "r").catch(() => null);
649
725
  if (!handle) {
650
726
  return { content: `Failed to read ${args.path}`, isError: true };
651
727
  }
652
- let buf;
728
+ let decoded;
653
729
  try {
654
- const { bytesRead, buffer } = await handle.read(
655
- Buffer.alloc(bytesToRead),
656
- 0,
657
- bytesToRead,
658
- 0
659
- );
660
- buf = buffer.subarray(0, bytesRead);
730
+ decoded = await readWindow(handle, st.size, start, limit);
731
+ } catch {
732
+ return { content: `Failed to read ${args.path}`, isError: true };
661
733
  } finally {
662
734
  await handle.close().catch(() => {
663
735
  });
664
736
  }
665
- const decoded = decodeTextBuffer(buf);
666
737
  if ("binary" in decoded) {
667
738
  return { content: `Cannot read binary file: ${args.path}`, isError: true };
668
739
  }
669
- const lines = decoded.text.split("\n");
670
- const totalDesc = fileCapped ? `at least ${lines.length}` : `${lines.length}`;
671
- if (start > lines.length) {
672
- return { content: `(offset ${start} beyond end of file; file has ${totalDesc} lines)` };
673
- }
674
- const end = Math.min(lines.length, start - 1 + limit);
675
- const out = [];
676
- let size = 0;
677
- let truncatedBySize = false;
678
- for (let i = start - 1; i < end; i++) {
679
- const line = (lines[i] ?? "").replace(/\r$/, "");
680
- const numbered = `${i + 1} ${line}`;
681
- if (size + numbered.length > MAX_CHARS) {
682
- truncatedBySize = true;
683
- break;
684
- }
685
- out.push(numbered);
686
- size += numbered.length;
740
+ const { totalLines, out, lastShown, truncatedBySize } = decoded;
741
+ if (start > totalLines) {
742
+ return { content: `(offset ${start} beyond end of file; file has ${totalLines} lines)` };
687
743
  }
688
- const lastShown = start - 1 + out.length;
744
+ const end = Math.min(totalLines, start - 1 + limit);
689
745
  if (truncatedBySize) {
690
746
  out.push(
691
- `... (truncated: output exceeds 100KB, showing lines ${start}-${lastShown} of ${totalDesc})`
747
+ `... (truncated: output exceeds 100KB, showing lines ${start}-${lastShown} of ${totalLines})`
692
748
  );
693
- } else if (end < lines.length || fileCapped) {
694
- out.push(`... (truncated: showing lines ${start}-${end} of ${totalDesc})`);
749
+ } else if (end < totalLines) {
750
+ out.push(`... (truncated: showing lines ${start}-${end} of ${totalLines})`);
695
751
  }
696
752
  if (out.length > 0) {
697
753
  if (cache.size >= READ_CACHE_MAX_ENTRIES) cache.clear();
@@ -699,7 +755,7 @@ function createReadFileTool() {
699
755
  cache.set(cacheKey, {
700
756
  mtimeMs: st.mtimeMs,
701
757
  size: st.size,
702
- totalLines: lines.length,
758
+ totalLines,
703
759
  // Extend the remembered range when the new read overlaps or abuts
704
760
  // the previous one; disjoint ranges replace it.
705
761
  start: prev && start <= prev.end + 1 ? Math.min(prev.start, start) : start,
@@ -811,6 +867,10 @@ var writeFileTool = {
811
867
  parameters: schema7,
812
868
  async execute(args, ctx) {
813
869
  const filePath = path7.resolve(ctx.cwd, args.path);
870
+ const resolvedPath = await realpathForCheck(filePath);
871
+ if (isSensitivePath(filePath) || resolvedPath !== null && isSensitivePath(resolvedPath)) {
872
+ return { content: `Refused to write sensitive file: ${args.path}`, isError: true };
873
+ }
814
874
  if (args.content.includes("\0")) {
815
875
  return {
816
876
  content: `Refused to write ${args.path}: content contains NUL bytes (write_file is text-only)`,
@@ -825,6 +885,13 @@ var writeFileTool = {
825
885
  };
826
886
  }
827
887
  const snapshot = await captureSnapshot(filePath, "write_file", ctx.snapshotContext);
888
+ if (snapshot.existed && snapshot.content === null) {
889
+ const reason = snapshot.contentTooLarge ? "existing content is over the 5MB snapshot limit" : "existing content could not be read";
890
+ return {
891
+ content: `Refused to overwrite ${args.path}: ${reason}, and no restorable snapshot is available`,
892
+ isError: true
893
+ };
894
+ }
828
895
  try {
829
896
  await mkdir(path7.dirname(filePath), { recursive: true });
830
897
  await writeFile2(filePath, args.content, "utf8");
@@ -1146,6 +1213,7 @@ function debugStreamLog(event, data) {
1146
1213
 
1147
1214
  // src/tools/web/search.ts
1148
1215
  var TIMEOUT_MS = 15e3;
1216
+ var MAX_BODY_BYTES = 2 * 1024 * 1024;
1149
1217
  var DEFAULT_MAX_RESULTS = 5;
1150
1218
  var MAX_RESULTS_LIMIT = 10;
1151
1219
  var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
@@ -1268,6 +1336,26 @@ var SEARCH_SOURCES = [
1268
1336
  parse: parseBingResults
1269
1337
  }
1270
1338
  ];
1339
+ async function readLimitedBody(res) {
1340
+ if (!res.body) return "";
1341
+ const reader = res.body.getReader();
1342
+ const decoder = new TextDecoder();
1343
+ let bytes = 0;
1344
+ let text = "";
1345
+ try {
1346
+ while (bytes < MAX_BODY_BYTES) {
1347
+ const { done, value } = await reader.read();
1348
+ if (done) break;
1349
+ bytes += value.byteLength;
1350
+ text += decoder.decode(value, { stream: true });
1351
+ }
1352
+ text += decoder.decode();
1353
+ } finally {
1354
+ await reader.cancel().catch(() => {
1355
+ });
1356
+ }
1357
+ return text;
1358
+ }
1271
1359
  var webSearchTool = {
1272
1360
  name: "web_search",
1273
1361
  description: "Search the web and return a numbered list of results with title, URL and snippet. Uses DuckDuckGo with automatic fallback to DuckDuckGo Lite and Bing when a source fails or returns nothing. No API key required. Use web_fetch to read a result's full page.",
@@ -1307,7 +1395,7 @@ var webSearchTool = {
1307
1395
  }
1308
1396
  let html;
1309
1397
  try {
1310
- html = await res.text();
1398
+ html = await readLimitedBody(res);
1311
1399
  } catch (err) {
1312
1400
  const message = err instanceof Error ? err.message : String(err);
1313
1401
  failures.push(`${source.name}: failed to read search response: ${message}`);
@@ -1415,4 +1503,4 @@ export {
1415
1503
  ToolRegistry,
1416
1504
  createDefaultRegistry
1417
1505
  };
1418
- //# sourceMappingURL=chunk-Q5FJN4LN.js.map
1506
+ //# sourceMappingURL=chunk-54KH4JSO.js.map