@oh-my-pi/pi-coding-agent 15.5.4 → 15.5.7
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/CHANGELOG.md +48 -2
- package/dist/types/config/settings-schema.d.ts +50 -2
- package/dist/types/edit/hashline/diff.d.ts +6 -1
- package/dist/types/edit/hashline/execute.d.ts +1 -2
- package/dist/types/edit/hashline/params.d.ts +4 -5
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +23 -0
- package/dist/types/lib/xai-http.d.ts +40 -0
- package/dist/types/session/agent-session.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +19 -0
- package/dist/types/tools/find.d.ts +7 -0
- package/dist/types/tools/image-gen.d.ts +6 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/plan-mode-guard.d.ts +5 -6
- package/dist/types/tools/tts.d.ts +18 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +11 -0
- package/src/config/model-registry.ts +41 -9
- package/src/config/settings-schema.ts +43 -2
- package/src/edit/diff.ts +5 -3
- package/src/edit/hashline/diff.ts +11 -4
- package/src/edit/hashline/execute.ts +3 -10
- package/src/edit/hashline/params.ts +10 -3
- package/src/edit/index.ts +9 -12
- package/src/edit/renderer.ts +14 -7
- package/src/edit/streaming.ts +15 -128
- package/src/extensibility/legacy-pi-ai-shim.ts +24 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +47 -3
- package/src/lib/xai-http.ts +124 -0
- package/src/main.ts +2 -1
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/interactive-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +3 -1
- package/src/prompts/tools/find.md +3 -2
- package/src/sdk.ts +15 -9
- package/src/session/agent-session.ts +48 -5
- package/src/tools/fetch.ts +145 -74
- package/src/tools/find.ts +38 -6
- package/src/tools/image-gen.ts +205 -7
- package/src/tools/index.ts +1 -0
- package/src/tools/plan-mode-guard.ts +14 -6
- package/src/tools/read.ts +57 -3
- package/src/tools/search.ts +2 -2
- package/src/tools/tts.ts +133 -0
package/src/tools/fetch.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { htmlToMarkdown } from "@oh-my-pi/pi-natives";
|
|
|
6
6
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { $which, ptree, truncate } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { parseHTML } from "linkedom";
|
|
9
|
+
import { LRUCache } from "lru-cache/raw";
|
|
9
10
|
import type { Settings } from "../config/settings";
|
|
10
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
12
|
import { type Theme, theme } from "../modes/theme/theme";
|
|
@@ -23,6 +24,7 @@ import { finalizeOutput, loadPage, looksLikeHtml, MAX_OUTPUT_CHARS } from "../we
|
|
|
23
24
|
import { convertWithMarkit, fetchBinary } from "../web/scrapers/utils";
|
|
24
25
|
import { applyListLimit } from "./list-limit";
|
|
25
26
|
import { formatStyledArtifactReference, type OutputMeta } from "./output-meta";
|
|
27
|
+
import { type LineRange, parseLineRanges } from "./path-utils";
|
|
26
28
|
import { formatExpandHint, getDomain, replaceTabs } from "./render-utils";
|
|
27
29
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
28
30
|
import { toolResult } from "./tool-result";
|
|
@@ -138,16 +140,31 @@ export function isReadableUrlPath(value: string): boolean {
|
|
|
138
140
|
return /^https?:\/\//i.test(value) || /^www\./i.test(value);
|
|
139
141
|
}
|
|
140
142
|
|
|
141
|
-
// URL line selectors mirror the file form: `:50`, `:50-100`, `:50+150`, `:raw
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
143
|
+
// URL line selectors mirror the file form: `:50`, `:50-100`, `:50+150`, `:5-10,20-30`, `:raw`,
|
|
144
|
+
// or `:raw:N-M` / `:N-M:raw` to combine raw mode with a range. If a URL would otherwise look
|
|
145
|
+
// like `host:port`, add a trailing slash before the selector (e.g. `https://example.com/:80`
|
|
146
|
+
// to read line 80 of the document at `https://example.com/`).
|
|
145
147
|
|
|
146
148
|
export interface ParsedReadUrlTarget {
|
|
147
149
|
path: string;
|
|
148
150
|
raw: boolean;
|
|
149
151
|
offset?: number;
|
|
150
152
|
limit?: number;
|
|
153
|
+
/** Populated only when the selector carries 2+ ranges. Single-range stays on offset/limit. */
|
|
154
|
+
ranges?: readonly LineRange[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Recognize a single selector token (`raw` or one/many line ranges). */
|
|
158
|
+
function isUrlSelectorToken(token: string): boolean {
|
|
159
|
+
if (token === "raw") return true;
|
|
160
|
+
try {
|
|
161
|
+
return parseLineRanges(token) !== null;
|
|
162
|
+
} catch {
|
|
163
|
+
// `parseLineRanges` throws `ToolError` for malformed ranges (e.g. `5+0`). Only treat the
|
|
164
|
+
// token as a selector when it parses cleanly so URL ports like `:80` keep flowing
|
|
165
|
+
// through to the URL path.
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
151
168
|
}
|
|
152
169
|
|
|
153
170
|
export function parseReadUrlTarget(readPath: string): ParsedReadUrlTarget | null {
|
|
@@ -157,62 +174,71 @@ export function parseReadUrlTarget(readPath: string): ParsedReadUrlTarget | null
|
|
|
157
174
|
return null;
|
|
158
175
|
}
|
|
159
176
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
throw new ToolError("URL line selector 0 is invalid; lines are 1-indexed. Use :1.");
|
|
177
|
+
let raw = false;
|
|
178
|
+
let ranges: readonly LineRange[] | undefined;
|
|
179
|
+
for (const sel of embedded?.sels ?? []) {
|
|
180
|
+
if (sel === "raw") {
|
|
181
|
+
raw = true;
|
|
182
|
+
continue;
|
|
167
183
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
throw new ToolError(`Invalid range ${startLine}-${rhs ?? 0}: end must be >= start.`);
|
|
179
|
-
}
|
|
180
|
-
endLine = rhs;
|
|
184
|
+
if (ranges !== undefined) {
|
|
185
|
+
// Two range groups on the same URL (`…:5-10:20-30`) — combine with commas instead.
|
|
186
|
+
throw new ToolError(
|
|
187
|
+
`URL selector has multiple range groups; combine them with commas (e.g. \`:5-10,20-30\`).`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const parsed = parseLineRanges(sel);
|
|
191
|
+
if (parsed === null) {
|
|
192
|
+
// Shouldn't happen — isUrlSelectorToken vetted it. Belt-and-suspenders.
|
|
193
|
+
throw new ToolError(`Invalid URL line selector: ${sel}`);
|
|
181
194
|
}
|
|
195
|
+
ranges = parsed;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!ranges || ranges.length === 0) return { path: urlPath, raw };
|
|
199
|
+
if (ranges.length === 1) {
|
|
200
|
+
const r = ranges[0];
|
|
182
201
|
return {
|
|
183
202
|
path: urlPath,
|
|
184
|
-
raw
|
|
185
|
-
offset: startLine,
|
|
186
|
-
limit: endLine !== undefined ? endLine - startLine + 1 : undefined,
|
|
203
|
+
raw,
|
|
204
|
+
offset: r.startLine,
|
|
205
|
+
limit: r.endLine !== undefined ? r.endLine - r.startLine + 1 : undefined,
|
|
187
206
|
};
|
|
188
207
|
}
|
|
189
|
-
|
|
190
|
-
return { path: urlPath, raw };
|
|
208
|
+
return { path: urlPath, raw, ranges };
|
|
191
209
|
}
|
|
192
210
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Peel one or more selector tokens off the right of a URL string. Walks back through
|
|
213
|
+
* trailing `:tok` segments while each token (a) looks like a selector and (b) leaves
|
|
214
|
+
* behind a string that still parses as a URL. Returns selectors left-to-right so callers
|
|
215
|
+
* can apply them in source order.
|
|
216
|
+
*/
|
|
217
|
+
function tryExtractEmbeddedUrlSelector(readPath: string): { path: string; sels: string[] } | null {
|
|
218
|
+
let basePath = readPath;
|
|
219
|
+
const sels: string[] = [];
|
|
220
|
+
while (true) {
|
|
221
|
+
const lastColonIndex = basePath.lastIndexOf(":");
|
|
222
|
+
if (lastColonIndex <= 0) break;
|
|
223
|
+
|
|
224
|
+
const candidate = basePath.slice(lastColonIndex + 1);
|
|
225
|
+
const remainder = basePath.slice(0, lastColonIndex);
|
|
226
|
+
if (!isReadableUrlPath(remainder)) break;
|
|
227
|
+
if (!isUrlSelectorToken(candidate)) break;
|
|
198
228
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
229
|
+
try {
|
|
230
|
+
new URL(
|
|
231
|
+
remainder.startsWith("http://") || remainder.startsWith("https://") ? remainder : `https://${remainder}`,
|
|
232
|
+
);
|
|
233
|
+
} catch {
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
204
236
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
new URL(basePath.startsWith("http://") || basePath.startsWith("https://") ? basePath : `https://${basePath}`);
|
|
212
|
-
return { path: basePath, sel: candidateSelector };
|
|
213
|
-
} catch {
|
|
214
|
-
return null;
|
|
237
|
+
sels.unshift(candidate);
|
|
238
|
+
basePath = remainder;
|
|
215
239
|
}
|
|
240
|
+
if (sels.length === 0) return null;
|
|
241
|
+
return { path: basePath, sels };
|
|
216
242
|
}
|
|
217
243
|
|
|
218
244
|
/**
|
|
@@ -536,9 +562,22 @@ function parseFeedToMarkdown(content: string, maxItems = 10): string {
|
|
|
536
562
|
}
|
|
537
563
|
|
|
538
564
|
/**
|
|
539
|
-
*
|
|
565
|
+
* Cap on any single remote reader-mode request (Parallel, Jina) so a stalled
|
|
566
|
+
* remote endpoint cannot consume the whole reader-mode budget and starve the
|
|
567
|
+
* local fallback renderers (trafilatura, lynx, native). See #1449.
|
|
568
|
+
*/
|
|
569
|
+
const REMOTE_READER_MAX_MS = 10_000;
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Render HTML to markdown using Parallel, jina, trafilatura, lynx, then the
|
|
573
|
+
* in-process native converter. The overall `timeout` budget bounds the call,
|
|
574
|
+
* but remote reader requests are additionally capped at `REMOTE_READER_MAX_MS`
|
|
575
|
+
* so that a hung remote endpoint cannot prevent local fallbacks from running.
|
|
576
|
+
* Only a real `userSignal` cancellation aborts the chain — remote per-attempt
|
|
577
|
+
* timeouts and the overall reader-mode timeout still allow later renderers
|
|
578
|
+
* (especially the purely-local native converter) to be tried.
|
|
540
579
|
*/
|
|
541
|
-
async function renderHtmlToText(
|
|
580
|
+
export async function renderHtmlToText(
|
|
542
581
|
url: string,
|
|
543
582
|
html: string,
|
|
544
583
|
timeout: number,
|
|
@@ -546,14 +585,15 @@ async function renderHtmlToText(
|
|
|
546
585
|
userSignal: AbortSignal | undefined,
|
|
547
586
|
storage: AgentStorage | null,
|
|
548
587
|
): Promise<{ content: string; ok: boolean; method: string }> {
|
|
549
|
-
const
|
|
588
|
+
const overallSignal = ptree.combineSignals(userSignal, timeout * 1000);
|
|
550
589
|
const execOptions = {
|
|
551
590
|
mode: "group" as const,
|
|
552
591
|
allowNonZero: true,
|
|
553
592
|
allowAbort: true,
|
|
554
593
|
stderr: "full" as const,
|
|
555
|
-
signal,
|
|
594
|
+
signal: overallSignal,
|
|
556
595
|
};
|
|
596
|
+
const remoteBudgetMs = Math.min(timeout * 1000, REMOTE_READER_MAX_MS);
|
|
557
597
|
|
|
558
598
|
// Try Parallel extract first when credentials are configured
|
|
559
599
|
if (settings.get("providers.parallelFetch") && findParallelApiKey(storage)) {
|
|
@@ -564,7 +604,7 @@ async function renderHtmlToText(
|
|
|
564
604
|
objective: "Extract the main content",
|
|
565
605
|
excerpts: true,
|
|
566
606
|
fullContent: false,
|
|
567
|
-
signal,
|
|
607
|
+
signal: ptree.combineSignals(userSignal, remoteBudgetMs),
|
|
568
608
|
},
|
|
569
609
|
storage,
|
|
570
610
|
);
|
|
@@ -576,17 +616,18 @@ async function renderHtmlToText(
|
|
|
576
616
|
}
|
|
577
617
|
}
|
|
578
618
|
} catch {
|
|
579
|
-
// Parallel extract failed
|
|
580
|
-
|
|
619
|
+
// Parallel extract failed or stalled; honour real cancellation only.
|
|
620
|
+
userSignal?.throwIfAborted();
|
|
581
621
|
}
|
|
582
622
|
}
|
|
583
623
|
|
|
584
|
-
// Try jina
|
|
624
|
+
// Try jina reader API with its own sub-budget so a stall cannot starve
|
|
625
|
+
// later fallbacks (#1449).
|
|
585
626
|
try {
|
|
586
627
|
const jinaUrl = `https://r.jina.ai/${url}`;
|
|
587
628
|
const response = await fetch(jinaUrl, {
|
|
588
629
|
headers: { Accept: "text/markdown" },
|
|
589
|
-
signal,
|
|
630
|
+
signal: ptree.combineSignals(userSignal, remoteBudgetMs),
|
|
590
631
|
});
|
|
591
632
|
if (response.ok) {
|
|
592
633
|
const content = await response.text();
|
|
@@ -595,37 +636,50 @@ async function renderHtmlToText(
|
|
|
595
636
|
}
|
|
596
637
|
}
|
|
597
638
|
} catch {
|
|
598
|
-
// Jina failed
|
|
599
|
-
|
|
639
|
+
// Jina failed or stalled; honour real cancellation only.
|
|
640
|
+
userSignal?.throwIfAborted();
|
|
600
641
|
}
|
|
601
642
|
|
|
602
643
|
// Try trafilatura (auto-install via uv/pip)
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
644
|
+
try {
|
|
645
|
+
const trafilatura = await ensureTool("trafilatura", { signal: overallSignal, silent: true });
|
|
646
|
+
if (trafilatura) {
|
|
647
|
+
const result = await ptree.exec([trafilatura, "-u", url, "--output-format", "markdown"], execOptions);
|
|
648
|
+
if (result.ok && result.stdout.trim().length > 100) {
|
|
649
|
+
return { content: result.stdout, ok: true, method: "trafilatura" };
|
|
650
|
+
}
|
|
608
651
|
}
|
|
652
|
+
} catch {
|
|
653
|
+
// trafilatura unavailable or stalled; continue to next method.
|
|
654
|
+
userSignal?.throwIfAborted();
|
|
609
655
|
}
|
|
610
656
|
|
|
611
657
|
// Try lynx (can't auto-install, system package)
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
658
|
+
try {
|
|
659
|
+
const lynx = hasCommand("lynx");
|
|
660
|
+
if (lynx) {
|
|
661
|
+
const result = await ptree.exec(["lynx", "-dump", "-nolist", "-width", "250", url], execOptions);
|
|
662
|
+
if (result.ok) {
|
|
663
|
+
return { content: result.stdout, ok: true, method: "lynx" };
|
|
664
|
+
}
|
|
617
665
|
}
|
|
666
|
+
} catch {
|
|
667
|
+
// lynx failed or stalled; continue to native converter.
|
|
668
|
+
userSignal?.throwIfAborted();
|
|
618
669
|
}
|
|
619
670
|
|
|
620
|
-
// Fall back to native converter (
|
|
671
|
+
// Fall back to native converter (purely local, no network/subprocess).
|
|
672
|
+
// Always attempted: even if remote renderers and subprocesses were aborted
|
|
673
|
+
// by the overall reader-mode timeout, this still works on already-loaded
|
|
674
|
+
// HTML (#1449).
|
|
621
675
|
try {
|
|
622
676
|
const content = await htmlToMarkdown(html, { cleanContent: true });
|
|
623
677
|
if (content.trim().length > 100 && !isLowQualityOutput(content)) {
|
|
624
678
|
return { content, ok: true, method: "native" };
|
|
625
679
|
}
|
|
626
680
|
} catch {
|
|
627
|
-
// Native converter failed
|
|
628
|
-
|
|
681
|
+
// Native converter failed; nothing else to try.
|
|
682
|
+
userSignal?.throwIfAborted();
|
|
629
683
|
}
|
|
630
684
|
return { content: "", ok: false, method: "none" };
|
|
631
685
|
}
|
|
@@ -931,6 +985,22 @@ async function renderUrl(
|
|
|
931
985
|
const isText = mime.includes("text/plain") || mime.includes("text/markdown");
|
|
932
986
|
const isFeed = mime.includes("rss") || mime.includes("atom") || mime.includes("feed");
|
|
933
987
|
|
|
988
|
+
// Raw mode skips every text-shaping branch below (JSON pretty-print, feed-to-markdown,
|
|
989
|
+
// HTML extraction) and returns the response body verbatim. The image/markit branches
|
|
990
|
+
// above already ran because raw isn't useful for binary payloads.
|
|
991
|
+
if (raw) {
|
|
992
|
+
const output = finalizeOutput(rawContent);
|
|
993
|
+
return {
|
|
994
|
+
url,
|
|
995
|
+
finalUrl,
|
|
996
|
+
contentType: mime,
|
|
997
|
+
method: "raw",
|
|
998
|
+
content: output.content,
|
|
999
|
+
fetchedAt,
|
|
1000
|
+
truncated: output.truncated,
|
|
1001
|
+
notes,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
934
1004
|
if (isJson) {
|
|
935
1005
|
const output = finalizeOutput(formatJson(rawContent));
|
|
936
1006
|
return {
|
|
@@ -1174,7 +1244,8 @@ interface ReadUrlCacheEntry {
|
|
|
1174
1244
|
output: string;
|
|
1175
1245
|
}
|
|
1176
1246
|
|
|
1177
|
-
const
|
|
1247
|
+
const READ_URL_CACHE_MAX_ENTRIES = 100;
|
|
1248
|
+
const readUrlCache = new LRUCache<string, ReadUrlCacheEntry>({ max: READ_URL_CACHE_MAX_ENTRIES });
|
|
1178
1249
|
|
|
1179
1250
|
function getReadUrlCacheKey(session: ToolSession, requestedUrl: string, raw: boolean): string {
|
|
1180
1251
|
const scope = session.getSessionFile() ?? session.cwd;
|
package/src/tools/find.ts
CHANGED
|
@@ -39,14 +39,15 @@ const findSchema = z
|
|
|
39
39
|
paths: z.array(z.string().describe("glob including search path")).min(1).describe("globs including search paths"),
|
|
40
40
|
hidden: z.boolean().default(true).describe("include hidden files").optional(),
|
|
41
41
|
gitignore: z.boolean().default(true).describe("respect gitignore").optional(),
|
|
42
|
-
limit: z.number().default(
|
|
42
|
+
limit: z.number().default(200).describe("max results (clamped to 1-200)").optional(),
|
|
43
43
|
timeout: z.number().min(0.5).max(60).default(5).describe("timeout in seconds (0.5–60)").optional(),
|
|
44
44
|
})
|
|
45
45
|
.strict();
|
|
46
46
|
|
|
47
47
|
export type FindToolInput = z.infer<typeof findSchema>;
|
|
48
48
|
|
|
49
|
-
const DEFAULT_LIMIT =
|
|
49
|
+
const DEFAULT_LIMIT = 200;
|
|
50
|
+
const MAX_LIMIT = 200;
|
|
50
51
|
const DEFAULT_GLOB_TIMEOUT_MS = 5000;
|
|
51
52
|
const MIN_GLOB_TIMEOUT_MS = 500;
|
|
52
53
|
const MAX_GLOB_TIMEOUT_MS = 60_000;
|
|
@@ -78,6 +79,37 @@ export function validateFindPathInputs(paths: readonly string[]): void {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Group find matches by their directory so the model doesn't pay repeated
|
|
84
|
+
* tokens for shared path prefixes. Preserves the input order: groups appear in
|
|
85
|
+
* the order their first member was emitted (mtime-desc for native glob), and
|
|
86
|
+
* within a group entries keep their relative order.
|
|
87
|
+
*/
|
|
88
|
+
export function formatFindGroupedOutput(paths: readonly string[]): string {
|
|
89
|
+
if (paths.length === 0) return "";
|
|
90
|
+
const groups = new Map<string, string[]>();
|
|
91
|
+
for (const entry of paths) {
|
|
92
|
+
const hasTrailingSlash = entry.endsWith("/");
|
|
93
|
+
const trimmed = hasTrailingSlash ? entry.slice(0, -1) : entry;
|
|
94
|
+
const slash = trimmed.lastIndexOf("/");
|
|
95
|
+
const dir = slash === -1 ? "" : trimmed.slice(0, slash);
|
|
96
|
+
const base = slash === -1 ? trimmed : trimmed.slice(slash + 1);
|
|
97
|
+
const label = hasTrailingSlash ? `${base}/` : base;
|
|
98
|
+
const list = groups.get(dir);
|
|
99
|
+
if (list) list.push(label);
|
|
100
|
+
else groups.set(dir, [label]);
|
|
101
|
+
}
|
|
102
|
+
const sections: string[] = [];
|
|
103
|
+
for (const [dir, entries] of groups) {
|
|
104
|
+
if (dir === "") {
|
|
105
|
+
sections.push(entries.join("\n"));
|
|
106
|
+
} else {
|
|
107
|
+
sections.push(`# ${dir}/\n${entries.join("\n")}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return sections.join("\n\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
81
113
|
export interface FindToolDetails {
|
|
82
114
|
truncation?: TruncationResult;
|
|
83
115
|
resultLimitReached?: number;
|
|
@@ -195,11 +227,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
195
227
|
if (searchPath === "/") {
|
|
196
228
|
throw new ToolError("Searching from root directory '/' is not allowed");
|
|
197
229
|
}
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
if (!Number.isFinite(effectiveLimit) || effectiveLimit <= 0) {
|
|
230
|
+
const requestedLimit = limit ?? DEFAULT_LIMIT;
|
|
231
|
+
if (!Number.isFinite(requestedLimit) || requestedLimit <= 0) {
|
|
201
232
|
throw new ToolError("Limit must be a positive number");
|
|
202
233
|
}
|
|
234
|
+
const effectiveLimit = Math.min(MAX_LIMIT, Math.max(1, Math.floor(requestedLimit)));
|
|
203
235
|
const includeHidden = hidden ?? true;
|
|
204
236
|
const useGitignore = gitignore ?? true;
|
|
205
237
|
const requestedTimeoutMs = timeout != null ? Math.round(timeout * 1000) : DEFAULT_GLOB_TIMEOUT_MS;
|
|
@@ -241,7 +273,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
241
273
|
const listLimit = applyListLimit(files, { limit: effectiveLimit });
|
|
242
274
|
const limited = listLimit.items;
|
|
243
275
|
const limitMeta = listLimit.meta;
|
|
244
|
-
const baseOutput = limited
|
|
276
|
+
const baseOutput = formatFindGroupedOutput(limited);
|
|
245
277
|
const trailingNotes: string[] = [];
|
|
246
278
|
if (notice) trailingNotes.push(notice);
|
|
247
279
|
if (missingPathsNote) trailingNotes.push(missingPathsNote);
|