@oh-my-pi/pi-coding-agent 3.30.0 → 3.32.0
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 +85 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +367 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/sdk.ts +10 -2
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/slash-commands.ts +39 -13
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +8 -4
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +84 -19
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +72 -35
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +150 -74
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/commands.ts +4 -0
- package/src/core/tools/task/executor.ts +94 -83
- package/src/core/tools/task/index.ts +130 -92
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +15 -6
- package/src/core/tools/task/worker.ts +124 -89
- package/src/core/tools/web-fetch.ts +112 -377
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -63
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/web-fetch-handlers/index.ts +0 -69
- package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
|
@@ -1,86 +1,26 @@
|
|
|
1
1
|
import { tmpdir } from "node:os";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
4
6
|
import { Type } from "@sinclair/typebox";
|
|
7
|
+
import { nanoid } from "nanoid";
|
|
5
8
|
import { parse as parseHtml } from "node-html-parser";
|
|
9
|
+
import { type Theme, theme } from "../../modes/interactive/theme/theme";
|
|
6
10
|
import webFetchDescription from "../../prompts/tools/web-fetch.md" with { type: "text" };
|
|
7
11
|
import { ensureTool } from "../../utils/tools-manager";
|
|
8
|
-
import {
|
|
12
|
+
import type { RenderResultOptions } from "../custom-tools/types";
|
|
9
13
|
import type { ToolSession } from "./index";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
handleBiorxiv,
|
|
15
|
-
handleBluesky,
|
|
16
|
-
handleBrew,
|
|
17
|
-
handleCheatSh,
|
|
18
|
-
handleChocolatey,
|
|
19
|
-
handleCoinGecko,
|
|
20
|
-
handleCratesIo,
|
|
21
|
-
handleDevTo,
|
|
22
|
-
handleDiscogs,
|
|
23
|
-
handleDockerHub,
|
|
24
|
-
handleGitHub,
|
|
25
|
-
handleGitHubGist,
|
|
26
|
-
handleGitLab,
|
|
27
|
-
handleGoPkg,
|
|
28
|
-
handleHackage,
|
|
29
|
-
handleHackerNews,
|
|
30
|
-
handleHex,
|
|
31
|
-
handleHuggingFace,
|
|
32
|
-
handleIacr,
|
|
33
|
-
handleLobsters,
|
|
34
|
-
handleMastodon,
|
|
35
|
-
handleMaven,
|
|
36
|
-
handleMDN,
|
|
37
|
-
handleMetaCPAN,
|
|
38
|
-
handleNpm,
|
|
39
|
-
handleNuGet,
|
|
40
|
-
handleNvd,
|
|
41
|
-
handleOpenCorporates,
|
|
42
|
-
handleOpenLibrary,
|
|
43
|
-
handleOsv,
|
|
44
|
-
handlePackagist,
|
|
45
|
-
handlePubDev,
|
|
46
|
-
handlePubMed,
|
|
47
|
-
handlePyPI,
|
|
48
|
-
handleReadTheDocs,
|
|
49
|
-
handleReddit,
|
|
50
|
-
handleRepology,
|
|
51
|
-
handleRfc,
|
|
52
|
-
handleRubyGems,
|
|
53
|
-
handleSecEdgar,
|
|
54
|
-
handleSemanticScholar,
|
|
55
|
-
handleSpotify,
|
|
56
|
-
handleStackOverflow,
|
|
57
|
-
handleTerraform,
|
|
58
|
-
handleTldr,
|
|
59
|
-
handleTwitter,
|
|
60
|
-
handleVimeo,
|
|
61
|
-
handleWikidata,
|
|
62
|
-
handleWikipedia,
|
|
63
|
-
handleYouTube,
|
|
64
|
-
} from "./web-fetch-handlers/index";
|
|
14
|
+
import { specialHandlers } from "./web-scrapers/index";
|
|
15
|
+
import type { RenderResult } from "./web-scrapers/types";
|
|
16
|
+
import { finalizeOutput, loadPage } from "./web-scrapers/types";
|
|
17
|
+
import { convertWithMarkitdown, fetchBinary } from "./web-scrapers/utils";
|
|
65
18
|
|
|
66
19
|
// =============================================================================
|
|
67
20
|
// Types and Constants
|
|
68
21
|
// =============================================================================
|
|
69
22
|
|
|
70
|
-
interface RenderResult {
|
|
71
|
-
url: string;
|
|
72
|
-
finalUrl: string;
|
|
73
|
-
contentType: string;
|
|
74
|
-
method: string;
|
|
75
|
-
content: string;
|
|
76
|
-
fetchedAt: string;
|
|
77
|
-
truncated: boolean;
|
|
78
|
-
notes: string[];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
23
|
const DEFAULT_TIMEOUT = 20;
|
|
82
|
-
const MAX_BYTES = 50 * 1024 * 1024; // 50MB for binary files
|
|
83
|
-
const MAX_OUTPUT_CHARS = 500_000;
|
|
84
24
|
|
|
85
25
|
// Convertible document types (markitdown supported)
|
|
86
26
|
const CONVERTIBLE_MIMES = new Set([
|
|
@@ -123,124 +63,11 @@ const CONVERTIBLE_EXTENSIONS = new Set([
|
|
|
123
63
|
".ogg",
|
|
124
64
|
]);
|
|
125
65
|
|
|
126
|
-
const USER_AGENTS = [
|
|
127
|
-
"curl/8.0",
|
|
128
|
-
"Mozilla/5.0 (compatible; TextBot/1.0)",
|
|
129
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
130
|
-
];
|
|
131
|
-
|
|
132
66
|
// =============================================================================
|
|
133
67
|
// Utilities
|
|
134
68
|
// =============================================================================
|
|
135
69
|
|
|
136
|
-
|
|
137
|
-
content: string;
|
|
138
|
-
contentType: string;
|
|
139
|
-
finalUrl: string;
|
|
140
|
-
ok: boolean;
|
|
141
|
-
status?: number;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
interface LoadPageOptions {
|
|
145
|
-
timeout?: number;
|
|
146
|
-
headers?: Record<string, string>;
|
|
147
|
-
maxBytes?: number;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Check if response indicates bot blocking (Cloudflare, etc.)
|
|
152
|
-
*/
|
|
153
|
-
function isBotBlocked(status: number, content: string): boolean {
|
|
154
|
-
if (status === 403 || status === 503) {
|
|
155
|
-
const lower = content.toLowerCase();
|
|
156
|
-
return (
|
|
157
|
-
lower.includes("cloudflare") ||
|
|
158
|
-
lower.includes("captcha") ||
|
|
159
|
-
lower.includes("challenge") ||
|
|
160
|
-
lower.includes("blocked") ||
|
|
161
|
-
lower.includes("access denied") ||
|
|
162
|
-
lower.includes("bot detection")
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Fetch a page with timeout, size limit, and automatic retry with browser UA if blocked
|
|
170
|
-
*/
|
|
171
|
-
async function loadPage(url: string, options: LoadPageOptions = {}): Promise<LoadPageResult> {
|
|
172
|
-
const { timeout = 20, headers = {}, maxBytes = MAX_BYTES } = options;
|
|
173
|
-
|
|
174
|
-
for (let attempt = 0; attempt < USER_AGENTS.length; attempt++) {
|
|
175
|
-
const userAgent = USER_AGENTS[attempt];
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
const controller = new AbortController();
|
|
179
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout * 1000);
|
|
180
|
-
|
|
181
|
-
const response = await fetch(url, {
|
|
182
|
-
signal: controller.signal,
|
|
183
|
-
headers: {
|
|
184
|
-
"User-Agent": userAgent,
|
|
185
|
-
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
186
|
-
"Accept-Language": "en-US,en;q=0.5",
|
|
187
|
-
...headers,
|
|
188
|
-
},
|
|
189
|
-
redirect: "follow",
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
clearTimeout(timeoutId);
|
|
193
|
-
|
|
194
|
-
const contentType = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
195
|
-
const finalUrl = response.url;
|
|
196
|
-
|
|
197
|
-
// Read with size limit
|
|
198
|
-
const reader = response.body?.getReader();
|
|
199
|
-
if (!reader) {
|
|
200
|
-
return { content: "", contentType, finalUrl, ok: false, status: response.status };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const chunks: Uint8Array[] = [];
|
|
204
|
-
let totalSize = 0;
|
|
205
|
-
|
|
206
|
-
while (true) {
|
|
207
|
-
const { done, value } = await reader.read();
|
|
208
|
-
if (done) break;
|
|
209
|
-
|
|
210
|
-
chunks.push(value);
|
|
211
|
-
totalSize += value.length;
|
|
212
|
-
|
|
213
|
-
if (totalSize > maxBytes) {
|
|
214
|
-
reader.cancel();
|
|
215
|
-
break;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const decoder = new TextDecoder();
|
|
220
|
-
const content = decoder.decode(Buffer.concat(chunks));
|
|
221
|
-
|
|
222
|
-
// Check if we got blocked and should retry with browser UA
|
|
223
|
-
if (isBotBlocked(response.status, content) && attempt < USER_AGENTS.length - 1) {
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (!response.ok) {
|
|
228
|
-
return { content, contentType, finalUrl, ok: false, status: response.status };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return { content, contentType, finalUrl, ok: true, status: response.status };
|
|
232
|
-
} catch (err) {
|
|
233
|
-
// On last attempt, return failure
|
|
234
|
-
if (attempt === USER_AGENTS.length - 1) {
|
|
235
|
-
logger.debug("Web fetch failed after retries", { url, error: String(err) });
|
|
236
|
-
return { content: "", contentType: "", finalUrl: url, ok: false };
|
|
237
|
-
}
|
|
238
|
-
// Otherwise retry with next UA
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return { content: "", contentType: "", finalUrl: url, ok: false };
|
|
243
|
-
}
|
|
70
|
+
type SpawnSyncOptions = NonNullable<Parameters<typeof Bun.spawnSync>[1]>;
|
|
244
71
|
|
|
245
72
|
/**
|
|
246
73
|
* Execute a command and return stdout
|
|
@@ -250,8 +77,9 @@ function exec(
|
|
|
250
77
|
args: string[],
|
|
251
78
|
options?: { timeout?: number; input?: string | Buffer },
|
|
252
79
|
): { stdout: string; stderr: string; ok: boolean } {
|
|
80
|
+
const stdin = (options?.input ?? "ignore") as SpawnSyncOptions["stdin"];
|
|
253
81
|
const result = Bun.spawnSync([cmd, ...args], {
|
|
254
|
-
stdin
|
|
82
|
+
stdin,
|
|
255
83
|
stdout: "pipe",
|
|
256
84
|
stderr: "pipe",
|
|
257
85
|
});
|
|
@@ -344,39 +172,10 @@ function looksLikeHtml(content: string): boolean {
|
|
|
344
172
|
);
|
|
345
173
|
}
|
|
346
174
|
|
|
347
|
-
/**
|
|
348
|
-
* Convert binary file to markdown using markitdown
|
|
349
|
-
*/
|
|
350
|
-
async function convertWithMarkitdown(
|
|
351
|
-
content: Buffer,
|
|
352
|
-
extensionHint: string,
|
|
353
|
-
timeout: number,
|
|
354
|
-
): Promise<{ content: string; ok: boolean }> {
|
|
355
|
-
const markitdown = await ensureTool("markitdown", true);
|
|
356
|
-
if (!markitdown) {
|
|
357
|
-
return { content: "", ok: false };
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Write to temp file with extension hint
|
|
361
|
-
const ext = extensionHint || ".bin";
|
|
362
|
-
const tmpDir = tmpdir();
|
|
363
|
-
const tmpFile = path.join(tmpDir, `omp-convert-${Date.now()}${ext}`);
|
|
364
|
-
|
|
365
|
-
try {
|
|
366
|
-
await Bun.write(tmpFile, content);
|
|
367
|
-
const result = exec(markitdown, [tmpFile], { timeout });
|
|
368
|
-
return { content: result.stdout, ok: result.ok };
|
|
369
|
-
} finally {
|
|
370
|
-
try {
|
|
371
|
-
await Bun.$`rm ${tmpFile}`.quiet();
|
|
372
|
-
} catch {}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
175
|
/**
|
|
377
176
|
* Try fetching URL with .md appended (llms.txt convention)
|
|
378
177
|
*/
|
|
379
|
-
async function tryMdSuffix(url: string, timeout: number): Promise<string | null> {
|
|
178
|
+
async function tryMdSuffix(url: string, timeout: number, signal?: AbortSignal): Promise<string | null> {
|
|
380
179
|
const candidates: string[] = [];
|
|
381
180
|
|
|
382
181
|
try {
|
|
@@ -397,8 +196,15 @@ async function tryMdSuffix(url: string, timeout: number): Promise<string | null>
|
|
|
397
196
|
return null;
|
|
398
197
|
}
|
|
399
198
|
|
|
199
|
+
if (signal?.aborted) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
400
203
|
for (const candidate of candidates) {
|
|
401
|
-
|
|
204
|
+
if (signal?.aborted) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const result = await loadPage(candidate, { timeout: Math.min(timeout, 5), signal });
|
|
402
208
|
if (result.ok && result.content.trim().length > 100 && !looksLikeHtml(result.content)) {
|
|
403
209
|
return result.content;
|
|
404
210
|
}
|
|
@@ -410,11 +216,18 @@ async function tryMdSuffix(url: string, timeout: number): Promise<string | null>
|
|
|
410
216
|
/**
|
|
411
217
|
* Try to fetch LLM-friendly endpoints
|
|
412
218
|
*/
|
|
413
|
-
async function tryLlmEndpoints(origin: string, timeout: number): Promise<string | null> {
|
|
219
|
+
async function tryLlmEndpoints(origin: string, timeout: number, signal?: AbortSignal): Promise<string | null> {
|
|
414
220
|
const endpoints = [`${origin}/.well-known/llms.txt`, `${origin}/llms.txt`, `${origin}/llms.md`];
|
|
415
221
|
|
|
222
|
+
if (signal?.aborted) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
416
226
|
for (const endpoint of endpoints) {
|
|
417
|
-
|
|
227
|
+
if (signal?.aborted) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const result = await loadPage(endpoint, { timeout: Math.min(timeout, 5), signal });
|
|
418
231
|
if (result.ok && result.content.trim().length > 100 && !looksLikeHtml(result.content)) {
|
|
419
232
|
return result.content;
|
|
420
233
|
}
|
|
@@ -425,10 +238,19 @@ async function tryLlmEndpoints(origin: string, timeout: number): Promise<string
|
|
|
425
238
|
/**
|
|
426
239
|
* Try content negotiation for markdown/plain
|
|
427
240
|
*/
|
|
428
|
-
async function tryContentNegotiation(
|
|
241
|
+
async function tryContentNegotiation(
|
|
242
|
+
url: string,
|
|
243
|
+
timeout: number,
|
|
244
|
+
signal?: AbortSignal,
|
|
245
|
+
): Promise<{ content: string; type: string } | null> {
|
|
246
|
+
if (signal?.aborted) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
429
250
|
const result = await loadPage(url, {
|
|
430
251
|
timeout,
|
|
431
252
|
headers: { Accept: "text/markdown, text/plain;q=0.9, text/html;q=0.8" },
|
|
253
|
+
signal,
|
|
432
254
|
});
|
|
433
255
|
|
|
434
256
|
if (!result.ok) return null;
|
|
@@ -586,7 +408,7 @@ async function renderHtmlToText(
|
|
|
586
408
|
timeout: number,
|
|
587
409
|
): Promise<{ content: string; ok: boolean; method: string }> {
|
|
588
410
|
const tmpDir = tmpdir();
|
|
589
|
-
const tmpFile = path.join(tmpDir, `omp
|
|
411
|
+
const tmpFile = path.join(tmpDir, `omp-${nanoid()}.html`);
|
|
590
412
|
|
|
591
413
|
try {
|
|
592
414
|
await Bun.write(tmpFile, html);
|
|
@@ -658,64 +480,6 @@ function formatJson(content: string): string {
|
|
|
658
480
|
}
|
|
659
481
|
}
|
|
660
482
|
|
|
661
|
-
/**
|
|
662
|
-
* Truncate and cleanup output
|
|
663
|
-
*/
|
|
664
|
-
function finalizeOutput(content: string): { content: string; truncated: boolean } {
|
|
665
|
-
const cleaned = content.replace(/\n{3,}/g, "\n\n").trim();
|
|
666
|
-
const truncated = cleaned.length > MAX_OUTPUT_CHARS;
|
|
667
|
-
return {
|
|
668
|
-
content: cleaned.slice(0, MAX_OUTPUT_CHARS),
|
|
669
|
-
truncated,
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Fetch page as binary buffer (for convertible files)
|
|
675
|
-
*/
|
|
676
|
-
async function fetchBinary(
|
|
677
|
-
url: string,
|
|
678
|
-
timeout: number,
|
|
679
|
-
): Promise<{ buffer: Buffer; contentType: string; contentDisposition?: string; ok: boolean }> {
|
|
680
|
-
try {
|
|
681
|
-
const controller = new AbortController();
|
|
682
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout * 1000);
|
|
683
|
-
|
|
684
|
-
const response = await fetch(url, {
|
|
685
|
-
signal: controller.signal,
|
|
686
|
-
headers: {
|
|
687
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0.0.0",
|
|
688
|
-
},
|
|
689
|
-
redirect: "follow",
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
clearTimeout(timeoutId);
|
|
693
|
-
|
|
694
|
-
if (!response.ok) {
|
|
695
|
-
return { buffer: Buffer.alloc(0), contentType: "", ok: false };
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
699
|
-
const contentDisposition = response.headers.get("content-disposition") ?? undefined;
|
|
700
|
-
const contentLength = response.headers.get("content-length");
|
|
701
|
-
if (contentLength) {
|
|
702
|
-
const size = Number.parseInt(contentLength, 10);
|
|
703
|
-
if (Number.isFinite(size) && size > MAX_BYTES) {
|
|
704
|
-
return { buffer: Buffer.alloc(0), contentType, contentDisposition, ok: false };
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
709
|
-
if (buffer.length > MAX_BYTES) {
|
|
710
|
-
return { buffer: Buffer.alloc(0), contentType, contentDisposition, ok: false };
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
return { buffer, contentType, contentDisposition, ok: true };
|
|
714
|
-
} catch {
|
|
715
|
-
return { buffer: Buffer.alloc(0), contentType: "", ok: false };
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
483
|
// =============================================================================
|
|
720
484
|
// Unified Special Handler Dispatch
|
|
721
485
|
// =============================================================================
|
|
@@ -723,74 +487,15 @@ async function fetchBinary(
|
|
|
723
487
|
/**
|
|
724
488
|
* Try all special handlers
|
|
725
489
|
*/
|
|
726
|
-
async function handleSpecialUrls(url: string, timeout: number): Promise<RenderResult | null> {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
(await handleVimeo(url, timeout)) ||
|
|
736
|
-
(await handleSpotify(url, timeout)) ||
|
|
737
|
-
(await handleDiscogs(url, timeout)) ||
|
|
738
|
-
// Social/News
|
|
739
|
-
(await handleTwitter(url, timeout)) ||
|
|
740
|
-
(await handleBluesky(url, timeout)) ||
|
|
741
|
-
(await handleMastodon(url, timeout)) ||
|
|
742
|
-
(await handleHackerNews(url, timeout)) ||
|
|
743
|
-
(await handleLobsters(url, timeout)) ||
|
|
744
|
-
(await handleReddit(url, timeout)) ||
|
|
745
|
-
// Developer content
|
|
746
|
-
(await handleStackOverflow(url, timeout)) ||
|
|
747
|
-
(await handleDevTo(url, timeout)) ||
|
|
748
|
-
(await handleMDN(url, timeout)) ||
|
|
749
|
-
(await handleReadTheDocs(url, timeout)) ||
|
|
750
|
-
(await handleTldr(url, timeout)) ||
|
|
751
|
-
(await handleCheatSh(url, timeout)) ||
|
|
752
|
-
// Package registries
|
|
753
|
-
(await handleNpm(url, timeout)) ||
|
|
754
|
-
(await handleNuGet(url, timeout)) ||
|
|
755
|
-
(await handleChocolatey(url, timeout)) ||
|
|
756
|
-
(await handleBrew(url, timeout)) ||
|
|
757
|
-
(await handlePyPI(url, timeout)) ||
|
|
758
|
-
(await handleCratesIo(url, timeout)) ||
|
|
759
|
-
(await handleDockerHub(url, timeout)) ||
|
|
760
|
-
(await handleGoPkg(url, timeout)) ||
|
|
761
|
-
(await handleHex(url, timeout)) ||
|
|
762
|
-
(await handlePackagist(url, timeout)) ||
|
|
763
|
-
(await handlePubDev(url, timeout)) ||
|
|
764
|
-
(await handleMaven(url, timeout)) ||
|
|
765
|
-
(await handleArtifactHub(url, timeout)) ||
|
|
766
|
-
(await handleRubyGems(url, timeout)) ||
|
|
767
|
-
(await handleTerraform(url, timeout)) ||
|
|
768
|
-
(await handleAur(url, timeout)) ||
|
|
769
|
-
(await handleHackage(url, timeout)) ||
|
|
770
|
-
(await handleMetaCPAN(url, timeout)) ||
|
|
771
|
-
(await handleRepology(url, timeout)) ||
|
|
772
|
-
// ML/AI
|
|
773
|
-
(await handleHuggingFace(url, timeout)) ||
|
|
774
|
-
// Academic
|
|
775
|
-
(await handleArxiv(url, timeout)) ||
|
|
776
|
-
(await handleBiorxiv(url, timeout)) ||
|
|
777
|
-
(await handleIacr(url, timeout)) ||
|
|
778
|
-
(await handleSemanticScholar(url, timeout)) ||
|
|
779
|
-
(await handlePubMed(url, timeout)) ||
|
|
780
|
-
(await handleRfc(url, timeout)) ||
|
|
781
|
-
// Security
|
|
782
|
-
(await handleNvd(url, timeout)) ||
|
|
783
|
-
(await handleOsv(url, timeout)) ||
|
|
784
|
-
// Crypto
|
|
785
|
-
(await handleCoinGecko(url, timeout)) ||
|
|
786
|
-
// Business
|
|
787
|
-
(await handleOpenCorporates(url, timeout)) ||
|
|
788
|
-
(await handleSecEdgar(url, timeout)) ||
|
|
789
|
-
// Reference
|
|
790
|
-
(await handleOpenLibrary(url, timeout)) ||
|
|
791
|
-
(await handleWikidata(url, timeout)) ||
|
|
792
|
-
(await handleWikipedia(url, timeout))
|
|
793
|
-
);
|
|
490
|
+
async function handleSpecialUrls(url: string, timeout: number, signal?: AbortSignal): Promise<RenderResult | null> {
|
|
491
|
+
for (const handler of specialHandlers) {
|
|
492
|
+
if (signal?.aborted) {
|
|
493
|
+
throw new Error("Operation aborted");
|
|
494
|
+
}
|
|
495
|
+
const result = await handler(url, timeout, signal);
|
|
496
|
+
if (result) return result;
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
794
499
|
}
|
|
795
500
|
|
|
796
501
|
// =============================================================================
|
|
@@ -800,9 +505,17 @@ async function handleSpecialUrls(url: string, timeout: number): Promise<RenderRe
|
|
|
800
505
|
/**
|
|
801
506
|
* Main render function implementing the full pipeline
|
|
802
507
|
*/
|
|
803
|
-
async function renderUrl(
|
|
508
|
+
async function renderUrl(
|
|
509
|
+
url: string,
|
|
510
|
+
timeout: number,
|
|
511
|
+
raw: boolean = false,
|
|
512
|
+
signal?: AbortSignal,
|
|
513
|
+
): Promise<RenderResult> {
|
|
804
514
|
const notes: string[] = [];
|
|
805
515
|
const fetchedAt = new Date().toISOString();
|
|
516
|
+
if (signal?.aborted) {
|
|
517
|
+
throw new Error("Operation aborted");
|
|
518
|
+
}
|
|
806
519
|
|
|
807
520
|
// Step 0: Normalize URL (ensure scheme for special handlers)
|
|
808
521
|
url = normalizeUrl(url);
|
|
@@ -810,12 +523,15 @@ async function renderUrl(url: string, timeout: number, raw: boolean = false): Pr
|
|
|
810
523
|
|
|
811
524
|
// Step 1: Try special handlers for known sites (unless raw mode)
|
|
812
525
|
if (!raw) {
|
|
813
|
-
const specialResult = await handleSpecialUrls(url, timeout);
|
|
526
|
+
const specialResult = await handleSpecialUrls(url, timeout, signal);
|
|
814
527
|
if (specialResult) return specialResult;
|
|
815
528
|
}
|
|
816
529
|
|
|
817
530
|
// Step 2: Fetch page
|
|
818
|
-
const response = await loadPage(url, { timeout });
|
|
531
|
+
const response = await loadPage(url, { timeout, signal });
|
|
532
|
+
if (signal?.aborted) {
|
|
533
|
+
throw new Error("Operation aborted");
|
|
534
|
+
}
|
|
819
535
|
if (!response.ok) {
|
|
820
536
|
return {
|
|
821
537
|
url,
|
|
@@ -835,26 +551,36 @@ async function renderUrl(url: string, timeout: number, raw: boolean = false): Pr
|
|
|
835
551
|
|
|
836
552
|
// Step 3: Handle convertible binary files (PDF, DOCX, etc.)
|
|
837
553
|
if (isConvertible(mime, extHint)) {
|
|
838
|
-
const binary = await fetchBinary(finalUrl, timeout);
|
|
554
|
+
const binary = await fetchBinary(finalUrl, timeout, signal);
|
|
839
555
|
if (binary.ok) {
|
|
840
556
|
const ext = getExtensionHint(finalUrl, binary.contentDisposition) || extHint;
|
|
841
|
-
const converted = await convertWithMarkitdown(binary.buffer, ext, timeout);
|
|
842
|
-
if (converted.ok
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
557
|
+
const converted = await convertWithMarkitdown(binary.buffer, ext, timeout, signal);
|
|
558
|
+
if (converted.ok) {
|
|
559
|
+
if (converted.content.trim().length > 50) {
|
|
560
|
+
notes.push("Converted with markitdown");
|
|
561
|
+
const output = finalizeOutput(converted.content);
|
|
562
|
+
return {
|
|
563
|
+
url,
|
|
564
|
+
finalUrl,
|
|
565
|
+
contentType: mime,
|
|
566
|
+
method: "markitdown",
|
|
567
|
+
content: output.content,
|
|
568
|
+
fetchedAt,
|
|
569
|
+
truncated: output.truncated,
|
|
570
|
+
notes,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
notes.push("markitdown conversion produced no usable output");
|
|
574
|
+
} else if (converted.error) {
|
|
575
|
+
notes.push(`markitdown conversion failed: ${converted.error}`);
|
|
576
|
+
} else {
|
|
577
|
+
notes.push("markitdown conversion failed");
|
|
855
578
|
}
|
|
579
|
+
} else if (binary.error) {
|
|
580
|
+
notes.push(`Binary fetch failed: ${binary.error}`);
|
|
581
|
+
} else {
|
|
582
|
+
notes.push("Binary fetch failed");
|
|
856
583
|
}
|
|
857
|
-
notes.push("markitdown conversion failed");
|
|
858
584
|
}
|
|
859
585
|
|
|
860
586
|
// Step 4: Handle non-HTML text content
|
|
@@ -914,7 +640,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean = false): Pr
|
|
|
914
640
|
const markdownAlt = alternates.find((alt) => alt.endsWith(".md") || alt.includes("markdown"));
|
|
915
641
|
if (markdownAlt) {
|
|
916
642
|
const resolved = markdownAlt.startsWith("http") ? markdownAlt : new URL(markdownAlt, finalUrl).href;
|
|
917
|
-
const altResult = await loadPage(resolved, { timeout });
|
|
643
|
+
const altResult = await loadPage(resolved, { timeout, signal });
|
|
918
644
|
if (altResult.ok && altResult.content.trim().length > 100 && !looksLikeHtml(altResult.content)) {
|
|
919
645
|
notes.push(`Used markdown alternate: ${resolved}`);
|
|
920
646
|
const output = finalizeOutput(altResult.content);
|
|
@@ -932,7 +658,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean = false): Pr
|
|
|
932
658
|
}
|
|
933
659
|
|
|
934
660
|
// 5B: Try URL.md suffix (llms.txt convention)
|
|
935
|
-
const mdSuffix = await tryMdSuffix(finalUrl, timeout);
|
|
661
|
+
const mdSuffix = await tryMdSuffix(finalUrl, timeout, signal);
|
|
936
662
|
if (mdSuffix) {
|
|
937
663
|
notes.push("Found .md suffix version");
|
|
938
664
|
const output = finalizeOutput(mdSuffix);
|
|
@@ -949,7 +675,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean = false): Pr
|
|
|
949
675
|
}
|
|
950
676
|
|
|
951
677
|
// 5C: LLM-friendly endpoints
|
|
952
|
-
const llmContent = await tryLlmEndpoints(origin, timeout);
|
|
678
|
+
const llmContent = await tryLlmEndpoints(origin, timeout, signal);
|
|
953
679
|
if (llmContent) {
|
|
954
680
|
notes.push("Found llms.txt");
|
|
955
681
|
const output = finalizeOutput(llmContent);
|
|
@@ -966,7 +692,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean = false): Pr
|
|
|
966
692
|
}
|
|
967
693
|
|
|
968
694
|
// 5D: Content negotiation
|
|
969
|
-
const negotiated = await tryContentNegotiation(url, timeout);
|
|
695
|
+
const negotiated = await tryContentNegotiation(url, timeout, signal);
|
|
970
696
|
if (negotiated) {
|
|
971
697
|
notes.push(`Content negotiation returned ${negotiated.type}`);
|
|
972
698
|
const output = finalizeOutput(negotiated.content);
|
|
@@ -986,7 +712,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean = false): Pr
|
|
|
986
712
|
const feedAlternates = alternates.filter((alt) => !alt.endsWith(".md") && !alt.includes("markdown"));
|
|
987
713
|
for (const altUrl of feedAlternates.slice(0, 2)) {
|
|
988
714
|
const resolved = altUrl.startsWith("http") ? altUrl : new URL(altUrl, finalUrl).href;
|
|
989
|
-
const altResult = await loadPage(resolved, { timeout });
|
|
715
|
+
const altResult = await loadPage(resolved, { timeout, signal });
|
|
990
716
|
if (altResult.ok && altResult.content.trim().length > 200) {
|
|
991
717
|
notes.push(`Used feed alternate: ${resolved}`);
|
|
992
718
|
const parsed = parseFeedToMarkdown(altResult.content);
|
|
@@ -1004,6 +730,10 @@ async function renderUrl(url: string, timeout: number, raw: boolean = false): Pr
|
|
|
1004
730
|
}
|
|
1005
731
|
}
|
|
1006
732
|
|
|
733
|
+
if (signal?.aborted) {
|
|
734
|
+
throw new Error("Operation aborted");
|
|
735
|
+
}
|
|
736
|
+
|
|
1007
737
|
// Step 6: Render HTML with lynx or html2text
|
|
1008
738
|
const htmlResult = await renderHtmlToText(rawContent, timeout);
|
|
1009
739
|
if (!htmlResult.ok) {
|
|
@@ -1026,10 +756,10 @@ async function renderUrl(url: string, timeout: number, raw: boolean = false): Pr
|
|
|
1026
756
|
const docLinks = extractDocumentLinks(rawContent, finalUrl);
|
|
1027
757
|
if (docLinks.length > 0) {
|
|
1028
758
|
const docUrl = docLinks[0];
|
|
1029
|
-
const binary = await fetchBinary(docUrl, timeout);
|
|
759
|
+
const binary = await fetchBinary(docUrl, timeout, signal);
|
|
1030
760
|
if (binary.ok) {
|
|
1031
761
|
const ext = getExtensionHint(docUrl, binary.contentDisposition);
|
|
1032
|
-
const converted = await convertWithMarkitdown(binary.buffer, ext, timeout);
|
|
762
|
+
const converted = await convertWithMarkitdown(binary.buffer, ext, timeout, signal);
|
|
1033
763
|
if (converted.ok && converted.content.trim().length > htmlResult.content.length) {
|
|
1034
764
|
notes.push(`Extracted and converted document: ${docUrl}`);
|
|
1035
765
|
const output = finalizeOutput(converted.content);
|
|
@@ -1044,6 +774,11 @@ async function renderUrl(url: string, timeout: number, raw: boolean = false): Pr
|
|
|
1044
774
|
notes,
|
|
1045
775
|
};
|
|
1046
776
|
}
|
|
777
|
+
if (!converted.ok && converted.error) {
|
|
778
|
+
notes.push(`markitdown conversion failed: ${converted.error}`);
|
|
779
|
+
}
|
|
780
|
+
} else if (binary.error) {
|
|
781
|
+
notes.push(`Binary fetch failed: ${binary.error}`);
|
|
1047
782
|
}
|
|
1048
783
|
}
|
|
1049
784
|
notes.push("Page appears to require JavaScript or is mostly navigation");
|
|
@@ -1106,11 +841,16 @@ export function createWebFetchTool(_session: ToolSession): AgentTool<typeof webF
|
|
|
1106
841
|
execute: async (
|
|
1107
842
|
_toolCallId: string,
|
|
1108
843
|
{ url, timeout = DEFAULT_TIMEOUT, raw = false }: { url: string; timeout?: number; raw?: boolean },
|
|
844
|
+
signal?: AbortSignal,
|
|
1109
845
|
) => {
|
|
846
|
+
if (signal?.aborted) {
|
|
847
|
+
throw new Error("Operation aborted");
|
|
848
|
+
}
|
|
849
|
+
|
|
1110
850
|
// Clamp timeout
|
|
1111
851
|
const effectiveTimeout = Math.min(Math.max(timeout, 1), 120);
|
|
1112
852
|
|
|
1113
|
-
const result = await renderUrl(url, effectiveTimeout, raw);
|
|
853
|
+
const result = await renderUrl(url, effectiveTimeout, raw, signal);
|
|
1114
854
|
|
|
1115
855
|
// Format output
|
|
1116
856
|
let output = "";
|
|
@@ -1147,11 +887,6 @@ export function createWebFetchTool(_session: ToolSession): AgentTool<typeof webF
|
|
|
1147
887
|
// TUI Rendering
|
|
1148
888
|
// =============================================================================
|
|
1149
889
|
|
|
1150
|
-
import type { Component } from "@oh-my-pi/pi-tui";
|
|
1151
|
-
import { Text } from "@oh-my-pi/pi-tui";
|
|
1152
|
-
import { type Theme, theme } from "../../modes/interactive/theme/theme";
|
|
1153
|
-
import type { RenderResultOptions } from "../custom-tools/types";
|
|
1154
|
-
|
|
1155
890
|
/** Truncate text to max length with ellipsis */
|
|
1156
891
|
function truncate(text: string, maxLen: number, ellipsis: string): string {
|
|
1157
892
|
if (text.length <= maxLen) return text;
|