@oh-my-pi/pi-coding-agent 3.30.0 → 3.31.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 +71 -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 +369 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- 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/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 +6 -8
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +58 -9
- 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 +55 -32
- 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/executor.ts +94 -83
- package/src/core/tools/task/index.ts +129 -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 -38
- 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
|
@@ -53,7 +53,11 @@ interface ArtifactHubPackage {
|
|
|
53
53
|
* Handle Artifact Hub URLs via API
|
|
54
54
|
* Supports Helm charts, OLM operators, Falco rules, OPA policies, etc.
|
|
55
55
|
*/
|
|
56
|
-
export const handleArtifactHub: SpecialHandler = async (
|
|
56
|
+
export const handleArtifactHub: SpecialHandler = async (
|
|
57
|
+
url: string,
|
|
58
|
+
timeout: number,
|
|
59
|
+
signal?: AbortSignal,
|
|
60
|
+
): Promise<RenderResult | null> => {
|
|
57
61
|
try {
|
|
58
62
|
const parsed = new URL(url);
|
|
59
63
|
if (parsed.hostname !== "artifacthub.io" && parsed.hostname !== "www.artifacthub.io") return null;
|
|
@@ -70,6 +74,7 @@ export const handleArtifactHub: SpecialHandler = async (url: string, timeout: nu
|
|
|
70
74
|
const result = await loadPage(apiUrl, {
|
|
71
75
|
timeout,
|
|
72
76
|
headers: { Accept: "application/json" },
|
|
77
|
+
signal,
|
|
73
78
|
});
|
|
74
79
|
|
|
75
80
|
if (!result.ok) return null;
|
|
@@ -6,7 +6,11 @@ import { convertWithMarkitdown, fetchBinary } from "./utils";
|
|
|
6
6
|
/**
|
|
7
7
|
* Handle arXiv URLs via arXiv API
|
|
8
8
|
*/
|
|
9
|
-
export const handleArxiv: SpecialHandler = async (
|
|
9
|
+
export const handleArxiv: SpecialHandler = async (
|
|
10
|
+
url: string,
|
|
11
|
+
timeout: number,
|
|
12
|
+
signal?: AbortSignal,
|
|
13
|
+
): Promise<RenderResult | null> => {
|
|
10
14
|
try {
|
|
11
15
|
const parsed = new URL(url);
|
|
12
16
|
if (parsed.hostname !== "arxiv.org") return null;
|
|
@@ -22,7 +26,7 @@ export const handleArxiv: SpecialHandler = async (url: string, timeout: number):
|
|
|
22
26
|
|
|
23
27
|
// Fetch metadata via arXiv API
|
|
24
28
|
const apiUrl = `https://export.arxiv.org/api/query?id_list=${paperId}`;
|
|
25
|
-
const result = await loadPage(apiUrl, { timeout });
|
|
29
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
26
30
|
|
|
27
31
|
if (!result.ok) return null;
|
|
28
32
|
|
|
@@ -56,9 +60,9 @@ export const handleArxiv: SpecialHandler = async (url: string, timeout: number):
|
|
|
56
60
|
if (match[1] === "pdf" || parsed.pathname.includes(".pdf")) {
|
|
57
61
|
if (pdfLink) {
|
|
58
62
|
notes.push("Fetching PDF for full content...");
|
|
59
|
-
const pdfResult = await fetchBinary(pdfLink, timeout);
|
|
63
|
+
const pdfResult = await fetchBinary(pdfLink, timeout, signal);
|
|
60
64
|
if (pdfResult.ok) {
|
|
61
|
-
const converted = await convertWithMarkitdown(pdfResult.buffer, ".pdf", timeout);
|
|
65
|
+
const converted = await convertWithMarkitdown(pdfResult.buffer, ".pdf", timeout, signal);
|
|
62
66
|
if (converted.ok && converted.content.length > 500) {
|
|
63
67
|
md += `---\n\n## Full Paper\n\n${converted.content}\n`;
|
|
64
68
|
notes.push("PDF converted via markitdown");
|
|
@@ -35,7 +35,11 @@ interface AurResponse {
|
|
|
35
35
|
/**
|
|
36
36
|
* Handle AUR (Arch User Repository) URLs via RPC API
|
|
37
37
|
*/
|
|
38
|
-
export const handleAur: SpecialHandler = async (
|
|
38
|
+
export const handleAur: SpecialHandler = async (
|
|
39
|
+
url: string,
|
|
40
|
+
timeout: number,
|
|
41
|
+
signal?: AbortSignal,
|
|
42
|
+
): Promise<RenderResult | null> => {
|
|
39
43
|
try {
|
|
40
44
|
const parsed = new URL(url);
|
|
41
45
|
if (parsed.hostname !== "aur.archlinux.org") return null;
|
|
@@ -49,7 +53,7 @@ export const handleAur: SpecialHandler = async (url: string, timeout: number): P
|
|
|
49
53
|
|
|
50
54
|
// Fetch from AUR RPC API
|
|
51
55
|
const apiUrl = `https://aur.archlinux.org/rpc/?v=5&type=info&arg=${encodeURIComponent(packageName)}`;
|
|
52
|
-
const result = await loadPage(apiUrl, { timeout });
|
|
56
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
53
57
|
|
|
54
58
|
if (!result.ok) return null;
|
|
55
59
|
|
|
@@ -27,7 +27,11 @@ interface BiorxivResponse {
|
|
|
27
27
|
/**
|
|
28
28
|
* Handle bioRxiv and medRxiv preprint URLs via their API
|
|
29
29
|
*/
|
|
30
|
-
export const handleBiorxiv: SpecialHandler = async (
|
|
30
|
+
export const handleBiorxiv: SpecialHandler = async (
|
|
31
|
+
url: string,
|
|
32
|
+
timeout: number,
|
|
33
|
+
signal?: AbortSignal,
|
|
34
|
+
): Promise<RenderResult | null> => {
|
|
31
35
|
try {
|
|
32
36
|
const parsed = new URL(url);
|
|
33
37
|
const hostname = parsed.hostname.toLowerCase();
|
|
@@ -54,6 +58,7 @@ export const handleBiorxiv: SpecialHandler = async (url: string, timeout: number
|
|
|
54
58
|
const result = await loadPage(apiUrl, {
|
|
55
59
|
timeout,
|
|
56
60
|
headers: { Accept: "application/json" },
|
|
61
|
+
signal,
|
|
57
62
|
});
|
|
58
63
|
|
|
59
64
|
if (!result.ok) return null;
|
|
@@ -54,11 +54,12 @@ interface ThreadViewPost {
|
|
|
54
54
|
/**
|
|
55
55
|
* Resolve a handle to DID using the profile API
|
|
56
56
|
*/
|
|
57
|
-
async function resolveHandle(handle: string, timeout: number): Promise<string | null> {
|
|
57
|
+
async function resolveHandle(handle: string, timeout: number, signal?: AbortSignal): Promise<string | null> {
|
|
58
58
|
const url = `${API_BASE}/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`;
|
|
59
59
|
const result = await loadPage(url, {
|
|
60
60
|
timeout,
|
|
61
61
|
headers: { Accept: "application/json" },
|
|
62
|
+
signal,
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
if (!result.ok) return null;
|
|
@@ -148,7 +149,11 @@ function formatPost(post: BlueskyPost, isQuote = false): string {
|
|
|
148
149
|
/**
|
|
149
150
|
* Handle Bluesky post URLs
|
|
150
151
|
*/
|
|
151
|
-
export const handleBluesky: SpecialHandler = async (
|
|
152
|
+
export const handleBluesky: SpecialHandler = async (
|
|
153
|
+
url: string,
|
|
154
|
+
timeout: number,
|
|
155
|
+
signal?: AbortSignal,
|
|
156
|
+
): Promise<RenderResult | null> => {
|
|
152
157
|
try {
|
|
153
158
|
const parsed = new URL(url);
|
|
154
159
|
if (!["bsky.app", "www.bsky.app"].includes(parsed.hostname)) {
|
|
@@ -167,7 +172,7 @@ export const handleBluesky: SpecialHandler = async (url: string, timeout: number
|
|
|
167
172
|
const rkey = pathParts[3];
|
|
168
173
|
|
|
169
174
|
// First resolve handle to DID
|
|
170
|
-
const did = await resolveHandle(handle, timeout);
|
|
175
|
+
const did = await resolveHandle(handle, timeout, signal);
|
|
171
176
|
if (!did) return null;
|
|
172
177
|
|
|
173
178
|
// Construct AT URI and fetch thread
|
|
@@ -177,6 +182,7 @@ export const handleBluesky: SpecialHandler = async (url: string, timeout: number
|
|
|
177
182
|
const result = await loadPage(threadUrl, {
|
|
178
183
|
timeout,
|
|
179
184
|
headers: { Accept: "application/json" },
|
|
185
|
+
signal,
|
|
180
186
|
});
|
|
181
187
|
|
|
182
188
|
if (!result.ok) return null;
|
|
@@ -230,6 +236,7 @@ export const handleBluesky: SpecialHandler = async (url: string, timeout: number
|
|
|
230
236
|
const result = await loadPage(profileUrl, {
|
|
231
237
|
timeout,
|
|
232
238
|
headers: { Accept: "application/json" },
|
|
239
|
+
signal,
|
|
233
240
|
});
|
|
234
241
|
|
|
235
242
|
if (!result.ok) return null;
|
|
@@ -58,7 +58,11 @@ function getInstallCount(analytics?: { install?: { "30d"?: Record<string, number
|
|
|
58
58
|
/**
|
|
59
59
|
* Handle Homebrew formulae and cask URLs via API
|
|
60
60
|
*/
|
|
61
|
-
export const handleBrew: SpecialHandler = async (
|
|
61
|
+
export const handleBrew: SpecialHandler = async (
|
|
62
|
+
url: string,
|
|
63
|
+
timeout: number,
|
|
64
|
+
signal?: AbortSignal,
|
|
65
|
+
): Promise<RenderResult | null> => {
|
|
62
66
|
try {
|
|
63
67
|
const parsed = new URL(url);
|
|
64
68
|
if (parsed.hostname !== "formulae.brew.sh") return null;
|
|
@@ -76,7 +80,7 @@ export const handleBrew: SpecialHandler = async (url: string, timeout: number):
|
|
|
76
80
|
? `https://formulae.brew.sh/api/formula/${encodeURIComponent(name)}.json`
|
|
77
81
|
: `https://formulae.brew.sh/api/cask/${encodeURIComponent(name)}.json`;
|
|
78
82
|
|
|
79
|
-
const result = await loadPage(apiUrl, { timeout });
|
|
83
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
80
84
|
if (!result.ok) return null;
|
|
81
85
|
|
|
82
86
|
let md: string;
|
|
@@ -7,7 +7,11 @@ import { finalizeOutput, loadPage } from "./types";
|
|
|
7
7
|
* API: Plain text at https://cheat.sh/{topic}?T (T flag removes ANSI colors)
|
|
8
8
|
* Supports: commands, language/topic queries (e.g., python/list, go/slice)
|
|
9
9
|
*/
|
|
10
|
-
export const handleCheatSh: SpecialHandler = async (
|
|
10
|
+
export const handleCheatSh: SpecialHandler = async (
|
|
11
|
+
url: string,
|
|
12
|
+
timeout: number,
|
|
13
|
+
signal?: AbortSignal,
|
|
14
|
+
): Promise<RenderResult | null> => {
|
|
11
15
|
try {
|
|
12
16
|
const parsed = new URL(url);
|
|
13
17
|
if (parsed.hostname !== "cheat.sh" && parsed.hostname !== "cht.sh") return null;
|
|
@@ -22,6 +26,7 @@ export const handleCheatSh: SpecialHandler = async (url: string, timeout: number
|
|
|
22
26
|
const apiUrl = `https://cheat.sh/${encodeURIComponent(topic)}?T`;
|
|
23
27
|
const result = await loadPage(apiUrl, {
|
|
24
28
|
timeout,
|
|
29
|
+
signal,
|
|
25
30
|
headers: {
|
|
26
31
|
Accept: "text/plain",
|
|
27
32
|
},
|
|
@@ -28,7 +28,11 @@ interface NuGetODataResponse {
|
|
|
28
28
|
/**
|
|
29
29
|
* Handle Chocolatey package URLs via NuGet v2 OData API
|
|
30
30
|
*/
|
|
31
|
-
export const handleChocolatey: SpecialHandler = async (
|
|
31
|
+
export const handleChocolatey: SpecialHandler = async (
|
|
32
|
+
url: string,
|
|
33
|
+
timeout: number,
|
|
34
|
+
signal?: AbortSignal,
|
|
35
|
+
): Promise<RenderResult | null> => {
|
|
32
36
|
try {
|
|
33
37
|
const parsed = new URL(url);
|
|
34
38
|
if (!parsed.hostname.includes("chocolatey.org")) return null;
|
|
@@ -53,6 +57,7 @@ export const handleChocolatey: SpecialHandler = async (url: string, timeout: num
|
|
|
53
57
|
|
|
54
58
|
const result = await loadPage(apiUrl, {
|
|
55
59
|
timeout,
|
|
60
|
+
signal,
|
|
56
61
|
headers: {
|
|
57
62
|
Accept: "application/json",
|
|
58
63
|
},
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { parseFrontmatter } from "../../../discovery/helpers";
|
|
2
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
4
|
+
|
|
5
|
+
const ALLOWED_HOSTS = new Set(["choosealicense.com", "www.choosealicense.com"]);
|
|
6
|
+
const LICENSE_PATH = /^\/licenses\/([^/]+)\/?$/i;
|
|
7
|
+
const APPENDIX_PATH = /^\/appendix\/?$/i;
|
|
8
|
+
|
|
9
|
+
function asString(value: unknown): string | undefined {
|
|
10
|
+
if (typeof value !== "string") return undefined;
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeList(value: unknown): string[] {
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return value
|
|
18
|
+
.filter((item): item is string => typeof item === "string")
|
|
19
|
+
.map((item) => item.trim())
|
|
20
|
+
.filter((item) => item.length > 0);
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "string") {
|
|
23
|
+
return value
|
|
24
|
+
.split(",")
|
|
25
|
+
.map((item) => item.trim())
|
|
26
|
+
.filter((item) => item.length > 0);
|
|
27
|
+
}
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatLabel(value: string): string {
|
|
32
|
+
const cleaned = value.replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim();
|
|
33
|
+
if (!cleaned) return value;
|
|
34
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatSection(title: string, items: string[]): string {
|
|
38
|
+
let md = `## ${title}\n\n`;
|
|
39
|
+
if (items.length === 0) {
|
|
40
|
+
md += "- None listed\n\n";
|
|
41
|
+
return md;
|
|
42
|
+
}
|
|
43
|
+
for (const item of items) {
|
|
44
|
+
md += `- ${formatLabel(item)}\n`;
|
|
45
|
+
}
|
|
46
|
+
md += "\n";
|
|
47
|
+
return md;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const handleChooseALicense: SpecialHandler = async (
|
|
51
|
+
url: string,
|
|
52
|
+
timeout: number,
|
|
53
|
+
signal?: AbortSignal,
|
|
54
|
+
): Promise<RenderResult | null> => {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = new URL(url);
|
|
57
|
+
if (!ALLOWED_HOSTS.has(parsed.hostname)) return null;
|
|
58
|
+
|
|
59
|
+
const licenseMatch = parsed.pathname.match(LICENSE_PATH);
|
|
60
|
+
const isAppendix = APPENDIX_PATH.test(parsed.pathname);
|
|
61
|
+
if (!licenseMatch && !isAppendix) return null;
|
|
62
|
+
|
|
63
|
+
const licenseSlug = licenseMatch ? decodeURIComponent(licenseMatch[1]).toLowerCase() : "appendix";
|
|
64
|
+
const rawUrl = licenseMatch
|
|
65
|
+
? `https://raw.githubusercontent.com/github/choosealicense.com/gh-pages/_licenses/${licenseSlug}.txt`
|
|
66
|
+
: "https://raw.githubusercontent.com/github/choosealicense.com/gh-pages/_pages/appendix.md";
|
|
67
|
+
|
|
68
|
+
const fetchedAt = new Date().toISOString();
|
|
69
|
+
const result = await loadPage(rawUrl, { timeout, headers: { Accept: "text/plain" }, signal });
|
|
70
|
+
if (!result.ok) return null;
|
|
71
|
+
|
|
72
|
+
const { frontmatter, body } = parseFrontmatter(result.content);
|
|
73
|
+
|
|
74
|
+
const title = asString(frontmatter.title) ?? formatLabel(licenseSlug);
|
|
75
|
+
const spdxId = asString(frontmatter["spdx-id"]) ?? "Unknown";
|
|
76
|
+
const description = asString(frontmatter.description);
|
|
77
|
+
const permissions = normalizeList(frontmatter.permissions);
|
|
78
|
+
const conditions = normalizeList(frontmatter.conditions);
|
|
79
|
+
const limitations = normalizeList(frontmatter.limitations);
|
|
80
|
+
|
|
81
|
+
let md = `# ${title}\n\n`;
|
|
82
|
+
if (description) md += `${description}\n\n`;
|
|
83
|
+
|
|
84
|
+
md += `**SPDX ID:** ${spdxId}\n`;
|
|
85
|
+
md += `**Source:** https://choosealicense.com${isAppendix ? "/appendix" : `/licenses/${licenseSlug}/`}\n\n`;
|
|
86
|
+
|
|
87
|
+
md += formatSection("Permissions", permissions);
|
|
88
|
+
md += formatSection("Conditions", conditions);
|
|
89
|
+
md += formatSection("Limitations", limitations);
|
|
90
|
+
|
|
91
|
+
const licenseText = body.trim();
|
|
92
|
+
if (licenseText.length > 0) {
|
|
93
|
+
md += `---\n\n## License Text\n\n${licenseText}\n`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const output = finalizeOutput(md);
|
|
97
|
+
return {
|
|
98
|
+
url,
|
|
99
|
+
finalUrl: url,
|
|
100
|
+
contentType: "text/markdown",
|
|
101
|
+
method: "choosealicense",
|
|
102
|
+
content: output.content,
|
|
103
|
+
fetchedAt,
|
|
104
|
+
truncated: output.truncated,
|
|
105
|
+
notes: ["Fetched via Choose a License"],
|
|
106
|
+
};
|
|
107
|
+
} catch {}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface KevEntry {
|
|
5
|
+
cveID: string;
|
|
6
|
+
vendorProject?: string;
|
|
7
|
+
product?: string;
|
|
8
|
+
vulnerabilityName?: string;
|
|
9
|
+
shortDescription?: string;
|
|
10
|
+
requiredAction?: string;
|
|
11
|
+
dateAdded?: string;
|
|
12
|
+
dueDate?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface KevCatalog {
|
|
16
|
+
title?: string;
|
|
17
|
+
catalogVersion?: string;
|
|
18
|
+
dateReleased?: string;
|
|
19
|
+
count?: number;
|
|
20
|
+
vulnerabilities?: KevEntry[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CVE_PATTERN = /CVE-\d{4}-\d{4,7}/i;
|
|
24
|
+
const KEV_FEED_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Handle CISA Known Exploited Vulnerabilities (KEV) URLs
|
|
28
|
+
*/
|
|
29
|
+
export const handleCisaKev: SpecialHandler = async (
|
|
30
|
+
url: string,
|
|
31
|
+
timeout: number,
|
|
32
|
+
signal?: AbortSignal,
|
|
33
|
+
): Promise<RenderResult | null> => {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(url);
|
|
36
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
37
|
+
if (!hostname.endsWith("cisa.gov")) return null;
|
|
38
|
+
|
|
39
|
+
const path = parsed.pathname.toLowerCase();
|
|
40
|
+
if (!path.includes("known-exploited-vulnerabilities")) return null;
|
|
41
|
+
|
|
42
|
+
const cveMatch = parsed.pathname.match(CVE_PATTERN) ?? parsed.search.match(CVE_PATTERN);
|
|
43
|
+
if (!cveMatch) return null;
|
|
44
|
+
|
|
45
|
+
const cveId = cveMatch[0].toUpperCase();
|
|
46
|
+
const fetchedAt = new Date().toISOString();
|
|
47
|
+
|
|
48
|
+
const result = await loadPage(KEV_FEED_URL, {
|
|
49
|
+
timeout,
|
|
50
|
+
headers: { Accept: "application/json" },
|
|
51
|
+
signal,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!result.ok) return null;
|
|
55
|
+
|
|
56
|
+
let data: KevCatalog;
|
|
57
|
+
try {
|
|
58
|
+
data = JSON.parse(result.content) as KevCatalog;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const entry = data.vulnerabilities?.find((item) => item.cveID?.toUpperCase() === cveId);
|
|
64
|
+
if (!entry) return null;
|
|
65
|
+
|
|
66
|
+
let md = `# ${entry.cveID}\n\n`;
|
|
67
|
+
if (entry.vulnerabilityName) {
|
|
68
|
+
md += `${entry.vulnerabilityName}\n\n`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
md += "## Metadata\n\n";
|
|
72
|
+
if (entry.vendorProject) md += `**Vendor:** ${entry.vendorProject}\n`;
|
|
73
|
+
if (entry.product) md += `**Product:** ${entry.product}\n`;
|
|
74
|
+
if (entry.dateAdded) md += `**Date Added:** ${entry.dateAdded}\n`;
|
|
75
|
+
if (entry.dueDate) md += `**Due Date:** ${entry.dueDate}\n`;
|
|
76
|
+
md += "\n";
|
|
77
|
+
|
|
78
|
+
if (entry.shortDescription) {
|
|
79
|
+
md += `## Description\n\n${entry.shortDescription}\n\n`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (entry.requiredAction) {
|
|
83
|
+
md += `## Required Action\n\n${entry.requiredAction}\n\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const output = finalizeOutput(md);
|
|
87
|
+
return {
|
|
88
|
+
url,
|
|
89
|
+
finalUrl: url,
|
|
90
|
+
contentType: "text/markdown",
|
|
91
|
+
method: "cisa-kev",
|
|
92
|
+
content: output.content,
|
|
93
|
+
fetchedAt,
|
|
94
|
+
truncated: output.truncated,
|
|
95
|
+
notes: ["Fetched via CISA KEV feed"],
|
|
96
|
+
};
|
|
97
|
+
} catch {}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
5
|
+
return typeof value === "object" && value !== null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function asString(value: unknown): string | null {
|
|
9
|
+
if (typeof value !== "string") return null;
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function asNumber(value: unknown): number | null {
|
|
15
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatLicenses(licenses: unknown): string[] {
|
|
19
|
+
if (!Array.isArray(licenses)) return [];
|
|
20
|
+
const output: string[] = [];
|
|
21
|
+
for (const license of licenses) {
|
|
22
|
+
if (typeof license === "string") {
|
|
23
|
+
const trimmed = license.trim();
|
|
24
|
+
if (trimmed) output.push(trimmed);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (isRecord(license)) {
|
|
28
|
+
const name = asString(license.name);
|
|
29
|
+
const url = asString(license.url);
|
|
30
|
+
if (name && url) {
|
|
31
|
+
output.push(`${name} (${url})`);
|
|
32
|
+
} else if (name) {
|
|
33
|
+
output.push(name);
|
|
34
|
+
} else if (url) {
|
|
35
|
+
output.push(url);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return output;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatDependencies(deps: unknown): string[] {
|
|
43
|
+
const output: string[] = [];
|
|
44
|
+
if (Array.isArray(deps)) {
|
|
45
|
+
for (const dep of deps) {
|
|
46
|
+
if (typeof dep === "string") {
|
|
47
|
+
const trimmed = dep.trim();
|
|
48
|
+
if (trimmed) output.push(trimmed);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(dep)) {
|
|
52
|
+
const name = asString(dep[0]);
|
|
53
|
+
const version = asString(dep[1]);
|
|
54
|
+
if (name && version) {
|
|
55
|
+
output.push(`${name}: ${version}`);
|
|
56
|
+
} else if (name) {
|
|
57
|
+
output.push(name);
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (isRecord(dep)) {
|
|
62
|
+
const name = asString(dep.name) ?? asString(dep.artifact) ?? asString(dep.jar_name);
|
|
63
|
+
const version = asString(dep.version);
|
|
64
|
+
if (name && version) {
|
|
65
|
+
output.push(`${name}: ${version}`);
|
|
66
|
+
} else if (name) {
|
|
67
|
+
output.push(name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return output;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isRecord(deps)) {
|
|
75
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
76
|
+
const versionText = asString(version);
|
|
77
|
+
if (versionText) {
|
|
78
|
+
output.push(`${name}: ${versionText}`);
|
|
79
|
+
} else if (name.trim()) {
|
|
80
|
+
output.push(name);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return output;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Handle Clojars URLs via API
|
|
90
|
+
*/
|
|
91
|
+
export const handleClojars: SpecialHandler = async (
|
|
92
|
+
url: string,
|
|
93
|
+
timeout: number,
|
|
94
|
+
signal?: AbortSignal,
|
|
95
|
+
): Promise<RenderResult | null> => {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = new URL(url);
|
|
98
|
+
if (parsed.hostname !== "clojars.org" && parsed.hostname !== "www.clojars.org") return null;
|
|
99
|
+
|
|
100
|
+
const path = parsed.pathname.replace(/^\/+|\/+$/g, "");
|
|
101
|
+
if (!path) return null;
|
|
102
|
+
|
|
103
|
+
const segments = path.split("/").filter(Boolean);
|
|
104
|
+
if (segments.length < 1 || segments.length > 2) return null;
|
|
105
|
+
|
|
106
|
+
const groupFromUrl = segments.length === 2 ? decodeURIComponent(segments[0]) : null;
|
|
107
|
+
const artifactFromUrl = decodeURIComponent(segments[segments.length - 1]);
|
|
108
|
+
|
|
109
|
+
const apiUrl =
|
|
110
|
+
segments.length === 2
|
|
111
|
+
? `https://clojars.org/api/artifacts/${encodeURIComponent(groupFromUrl ?? "")}/${encodeURIComponent(artifactFromUrl)}`
|
|
112
|
+
: `https://clojars.org/api/artifacts/${encodeURIComponent(artifactFromUrl)}`;
|
|
113
|
+
|
|
114
|
+
const fetchedAt = new Date().toISOString();
|
|
115
|
+
|
|
116
|
+
const result = await loadPage(apiUrl, {
|
|
117
|
+
timeout,
|
|
118
|
+
headers: { Accept: "application/json" },
|
|
119
|
+
signal,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!result.ok) return null;
|
|
123
|
+
|
|
124
|
+
let payload: unknown;
|
|
125
|
+
try {
|
|
126
|
+
payload = JSON.parse(result.content);
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const data = Array.isArray(payload) ? payload[0] : payload;
|
|
132
|
+
if (!isRecord(data)) return null;
|
|
133
|
+
|
|
134
|
+
const groupName = asString(data.group_name) ?? asString(data.group) ?? groupFromUrl;
|
|
135
|
+
const artifactName = asString(data.jar_name) ?? asString(data.artifact) ?? asString(data.name) ?? artifactFromUrl;
|
|
136
|
+
const version = asString(data.latest_version) ?? asString(data.version);
|
|
137
|
+
const description = asString(data.description) ?? asString(data.summary);
|
|
138
|
+
const downloads =
|
|
139
|
+
asNumber(data.downloads) ?? asNumber(data.downloads_total) ?? asNumber(data.total_downloads) ?? null;
|
|
140
|
+
const homepage = asString(data.homepage) ?? asString(data.url);
|
|
141
|
+
const licenses = formatLicenses(data.licenses);
|
|
142
|
+
const dependencies = formatDependencies(data.dependencies ?? data.deps);
|
|
143
|
+
|
|
144
|
+
const displayName =
|
|
145
|
+
groupName && artifactName && groupName !== artifactName
|
|
146
|
+
? `${groupName}/${artifactName}`
|
|
147
|
+
: (artifactName ?? groupName ?? "Clojars artifact");
|
|
148
|
+
|
|
149
|
+
let md = `# ${displayName}\n\n`;
|
|
150
|
+
if (description) md += `${description}\n\n`;
|
|
151
|
+
|
|
152
|
+
if (groupName) md += `**Group:** ${groupName}\n`;
|
|
153
|
+
if (artifactName) md += `**Artifact:** ${artifactName}\n`;
|
|
154
|
+
if (version) md += `**Latest:** ${version}\n`;
|
|
155
|
+
if (downloads !== null) md += `**Downloads:** ${formatCount(downloads)}\n`;
|
|
156
|
+
if (homepage) md += `**Homepage:** ${homepage}\n`;
|
|
157
|
+
if (licenses.length > 0) md += `**Licenses:** ${licenses.join(", ")}\n`;
|
|
158
|
+
|
|
159
|
+
if (dependencies.length > 0) {
|
|
160
|
+
md += "\n## Dependencies\n\n";
|
|
161
|
+
for (const dep of dependencies) {
|
|
162
|
+
md += `- ${dep}\n`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const output = finalizeOutput(md);
|
|
167
|
+
return {
|
|
168
|
+
url,
|
|
169
|
+
finalUrl: url,
|
|
170
|
+
contentType: "text/markdown",
|
|
171
|
+
method: "clojars",
|
|
172
|
+
content: output.content,
|
|
173
|
+
fetchedAt,
|
|
174
|
+
truncated: output.truncated,
|
|
175
|
+
notes: ["Fetched via Clojars API"],
|
|
176
|
+
};
|
|
177
|
+
} catch {}
|
|
178
|
+
|
|
179
|
+
return null;
|
|
180
|
+
};
|
|
@@ -29,7 +29,11 @@ interface CoinGeckoResponse {
|
|
|
29
29
|
/**
|
|
30
30
|
* Handle CoinGecko cryptocurrency URLs via API
|
|
31
31
|
*/
|
|
32
|
-
export const handleCoinGecko: SpecialHandler = async (
|
|
32
|
+
export const handleCoinGecko: SpecialHandler = async (
|
|
33
|
+
url: string,
|
|
34
|
+
timeout: number,
|
|
35
|
+
signal?: AbortSignal,
|
|
36
|
+
): Promise<RenderResult | null> => {
|
|
33
37
|
try {
|
|
34
38
|
const parsed = new URL(url);
|
|
35
39
|
if (!parsed.hostname.includes("coingecko.com")) return null;
|
|
@@ -46,6 +50,7 @@ export const handleCoinGecko: SpecialHandler = async (url: string, timeout: numb
|
|
|
46
50
|
const result = await loadPage(apiUrl, {
|
|
47
51
|
timeout,
|
|
48
52
|
headers: { Accept: "application/json" },
|
|
53
|
+
signal,
|
|
49
54
|
});
|
|
50
55
|
|
|
51
56
|
if (!result.ok) return null;
|
|
@@ -17,7 +17,11 @@ function looksLikeHtml(content: string): boolean {
|
|
|
17
17
|
/**
|
|
18
18
|
* Handle crates.io URLs via API
|
|
19
19
|
*/
|
|
20
|
-
export const handleCratesIo: SpecialHandler = async (
|
|
20
|
+
export const handleCratesIo: SpecialHandler = async (
|
|
21
|
+
url: string,
|
|
22
|
+
timeout: number,
|
|
23
|
+
signal?: AbortSignal,
|
|
24
|
+
): Promise<RenderResult | null> => {
|
|
21
25
|
try {
|
|
22
26
|
const parsed = new URL(url);
|
|
23
27
|
if (parsed.hostname !== "crates.io" && parsed.hostname !== "www.crates.io") return null;
|
|
@@ -33,6 +37,7 @@ export const handleCratesIo: SpecialHandler = async (url: string, timeout: numbe
|
|
|
33
37
|
const apiUrl = `https://crates.io/api/v1/crates/${crateName}`;
|
|
34
38
|
const result = await loadPage(apiUrl, {
|
|
35
39
|
timeout,
|
|
40
|
+
signal,
|
|
36
41
|
headers: { "User-Agent": "omp-web-fetch/1.0 (https://github.com/anthropics)" },
|
|
37
42
|
});
|
|
38
43
|
|
|
@@ -101,7 +106,7 @@ export const handleCratesIo: SpecialHandler = async (url: string, timeout: numbe
|
|
|
101
106
|
|
|
102
107
|
// Try to fetch README from docs.rs or repository
|
|
103
108
|
const docsRsUrl = `https://docs.rs/crate/${crateName}/${crate.max_version}/source/README.md`;
|
|
104
|
-
const readmeResult = await loadPage(docsRsUrl, { timeout: Math.min(timeout, 5) });
|
|
109
|
+
const readmeResult = await loadPage(docsRsUrl, { timeout: Math.min(timeout, 5), signal });
|
|
105
110
|
if (readmeResult.ok && readmeResult.content.length > 100 && !looksLikeHtml(readmeResult.content)) {
|
|
106
111
|
md += `\n---\n\n## README\n\n${readmeResult.content}\n`;
|
|
107
112
|
}
|