@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
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORCID handler for web-fetch
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
6
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
7
|
+
|
|
8
|
+
const MAX_WORKS = 50;
|
|
9
|
+
const ORCID_PATTERN = /\/(\d{4}-\d{4}-\d{4}-\d{3}[\dXx])(?:\/|$)/;
|
|
10
|
+
|
|
11
|
+
interface OrcidName {
|
|
12
|
+
"given-names"?: { value?: string };
|
|
13
|
+
"family-name"?: { value?: string };
|
|
14
|
+
"credit-name"?: { value?: string };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface OrcidBiography {
|
|
18
|
+
content?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface OrcidPerson {
|
|
22
|
+
name?: OrcidName;
|
|
23
|
+
biography?: OrcidBiography;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface OrcidSummaryDate {
|
|
27
|
+
year?: { value?: string };
|
|
28
|
+
month?: { value?: string };
|
|
29
|
+
day?: { value?: string };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface OrcidOrganizationAddress {
|
|
33
|
+
city?: string;
|
|
34
|
+
region?: string;
|
|
35
|
+
country?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface OrcidOrganization {
|
|
39
|
+
name?: string;
|
|
40
|
+
address?: OrcidOrganizationAddress;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface OrcidAffiliationSummary {
|
|
44
|
+
organization?: OrcidOrganization;
|
|
45
|
+
"role-title"?: string;
|
|
46
|
+
"department-name"?: string;
|
|
47
|
+
"start-date"?: OrcidSummaryDate;
|
|
48
|
+
"end-date"?: OrcidSummaryDate;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface OrcidAffiliationGroupSummary {
|
|
52
|
+
"employment-summary"?: OrcidAffiliationSummary;
|
|
53
|
+
"education-summary"?: OrcidAffiliationSummary;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface OrcidAffiliationGroup {
|
|
57
|
+
summaries?: OrcidAffiliationGroupSummary[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface OrcidAffiliationsContainer {
|
|
61
|
+
"affiliation-group"?: OrcidAffiliationGroup[];
|
|
62
|
+
"employment-summary"?: OrcidAffiliationSummary[];
|
|
63
|
+
"education-summary"?: OrcidAffiliationSummary[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface OrcidWorkTitle {
|
|
67
|
+
title?: { value?: string };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface OrcidWorkSummary {
|
|
71
|
+
title?: OrcidWorkTitle;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface OrcidWorkGroup {
|
|
75
|
+
"work-summary"?: OrcidWorkSummary[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface OrcidWorksContainer {
|
|
79
|
+
group?: OrcidWorkGroup[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface OrcidActivitiesSummary {
|
|
83
|
+
employments?: OrcidAffiliationsContainer;
|
|
84
|
+
educations?: OrcidAffiliationsContainer;
|
|
85
|
+
works?: OrcidWorksContainer;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface OrcidRecord {
|
|
89
|
+
"orcid-identifier"?: { path?: string; uri?: string };
|
|
90
|
+
person?: OrcidPerson;
|
|
91
|
+
"activities-summary"?: OrcidActivitiesSummary;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isOrcidHost(hostname: string): boolean {
|
|
95
|
+
return hostname === "orcid.org" || hostname === "www.orcid.org";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractOrcidId(pathname: string): string | null {
|
|
99
|
+
const match = pathname.match(ORCID_PATTERN);
|
|
100
|
+
return match?.[1] ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatName(name?: OrcidName): string | null {
|
|
104
|
+
const credit = name?.["credit-name"]?.value?.trim();
|
|
105
|
+
if (credit) return credit;
|
|
106
|
+
|
|
107
|
+
const given = name?.["given-names"]?.value?.trim();
|
|
108
|
+
const family = name?.["family-name"]?.value?.trim();
|
|
109
|
+
if (given && family) return `${given} ${family}`;
|
|
110
|
+
return given || family || null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatDate(date?: OrcidSummaryDate): string | null {
|
|
114
|
+
const year = date?.year?.value;
|
|
115
|
+
if (!year) return null;
|
|
116
|
+
|
|
117
|
+
const month = date?.month?.value;
|
|
118
|
+
const day = date?.day?.value;
|
|
119
|
+
if (month && day) {
|
|
120
|
+
return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
|
|
121
|
+
}
|
|
122
|
+
if (month) return `${year}-${month.padStart(2, "0")}`;
|
|
123
|
+
return year;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function collectAffiliations(
|
|
127
|
+
container: OrcidAffiliationsContainer | undefined,
|
|
128
|
+
key: "employment-summary" | "education-summary",
|
|
129
|
+
): OrcidAffiliationSummary[] {
|
|
130
|
+
const summaries: OrcidAffiliationSummary[] = [];
|
|
131
|
+
|
|
132
|
+
if (!container) return summaries;
|
|
133
|
+
|
|
134
|
+
const direct = container[key];
|
|
135
|
+
if (direct?.length) summaries.push(...direct);
|
|
136
|
+
|
|
137
|
+
const groups = container["affiliation-group"];
|
|
138
|
+
if (groups?.length) {
|
|
139
|
+
for (const group of groups) {
|
|
140
|
+
const groupSummaries = group.summaries || [];
|
|
141
|
+
for (const summary of groupSummaries) {
|
|
142
|
+
const entry = summary[key];
|
|
143
|
+
if (entry) summaries.push(entry);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return summaries;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function formatAffiliation(summary: OrcidAffiliationSummary): string | null {
|
|
152
|
+
const organization = summary.organization?.name?.trim();
|
|
153
|
+
const role = summary["role-title"]?.trim();
|
|
154
|
+
const department = summary["department-name"]?.trim();
|
|
155
|
+
|
|
156
|
+
const address = summary.organization?.address;
|
|
157
|
+
const locationParts = [address?.city, address?.region, address?.country].filter(Boolean) as string[];
|
|
158
|
+
const location = locationParts.length > 0 ? locationParts.join(", ") : null;
|
|
159
|
+
|
|
160
|
+
const start = formatDate(summary["start-date"]);
|
|
161
|
+
const end = formatDate(summary["end-date"]);
|
|
162
|
+
let dates: string | null = null;
|
|
163
|
+
if (start && end) {
|
|
164
|
+
dates = `${start} - ${end}`;
|
|
165
|
+
} else if (start) {
|
|
166
|
+
dates = `${start} - Present`;
|
|
167
|
+
} else if (end) {
|
|
168
|
+
dates = `Until ${end}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const label = organization || role || department;
|
|
172
|
+
if (!label) return null;
|
|
173
|
+
|
|
174
|
+
const details: string[] = [];
|
|
175
|
+
if (organization && role) details.push(role);
|
|
176
|
+
if (!organization && role && department) details.push(department);
|
|
177
|
+
if (organization && department) details.push(`Dept: ${department}`);
|
|
178
|
+
if (location) details.push(`Location: ${location}`);
|
|
179
|
+
if (dates) details.push(`Dates: ${dates}`);
|
|
180
|
+
|
|
181
|
+
if (details.length === 0) return label;
|
|
182
|
+
return `${label} (${details.join("; ")})`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function collectWorkTitles(container: OrcidWorksContainer | undefined): string[] {
|
|
186
|
+
const titles: string[] = [];
|
|
187
|
+
const seen = new Set<string>();
|
|
188
|
+
const groups = container?.group || [];
|
|
189
|
+
|
|
190
|
+
for (const group of groups) {
|
|
191
|
+
const summaries = group["work-summary"] || [];
|
|
192
|
+
for (const summary of summaries) {
|
|
193
|
+
const title = summary.title?.title?.value?.trim();
|
|
194
|
+
if (!title || seen.has(title)) continue;
|
|
195
|
+
seen.add(title);
|
|
196
|
+
titles.push(title);
|
|
197
|
+
if (titles.length >= MAX_WORKS) return titles;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return titles;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export const handleOrcid: SpecialHandler = async (
|
|
205
|
+
url: string,
|
|
206
|
+
timeout: number,
|
|
207
|
+
signal?: AbortSignal,
|
|
208
|
+
): Promise<RenderResult | null> => {
|
|
209
|
+
try {
|
|
210
|
+
const parsed = new URL(url);
|
|
211
|
+
if (!isOrcidHost(parsed.hostname)) return null;
|
|
212
|
+
|
|
213
|
+
const orcid = extractOrcidId(parsed.pathname);
|
|
214
|
+
if (!orcid) return null;
|
|
215
|
+
|
|
216
|
+
const fetchedAt = new Date().toISOString();
|
|
217
|
+
const apiUrl = `https://pub.orcid.org/v3.0/${orcid}/record`;
|
|
218
|
+
|
|
219
|
+
const result = await loadPage(apiUrl, {
|
|
220
|
+
timeout,
|
|
221
|
+
headers: { Accept: "application/json" },
|
|
222
|
+
signal,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (!result.ok || !result.content) return null;
|
|
226
|
+
|
|
227
|
+
let record: OrcidRecord;
|
|
228
|
+
try {
|
|
229
|
+
record = JSON.parse(result.content) as OrcidRecord;
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const personName = formatName(record.person?.name);
|
|
235
|
+
const biography = record.person?.biography?.content?.trim();
|
|
236
|
+
|
|
237
|
+
const activities = record["activities-summary"];
|
|
238
|
+
const employments = collectAffiliations(activities?.employments, "employment-summary");
|
|
239
|
+
const educations = collectAffiliations(activities?.educations, "education-summary");
|
|
240
|
+
const works = collectWorkTitles(activities?.works);
|
|
241
|
+
|
|
242
|
+
let md = `# ${personName || "ORCID Profile"}\n\n`;
|
|
243
|
+
md += `**ORCID:** ${orcid}\n`;
|
|
244
|
+
md += `**ORCID Profile:** https://orcid.org/${orcid}\n\n`;
|
|
245
|
+
|
|
246
|
+
md += "## Biography\n\n";
|
|
247
|
+
md += biography ? `${biography}\n\n` : "No biography available.\n\n";
|
|
248
|
+
|
|
249
|
+
md += "## Affiliations\n\n";
|
|
250
|
+
let hasAffiliations = false;
|
|
251
|
+
|
|
252
|
+
if (employments.length > 0) {
|
|
253
|
+
hasAffiliations = true;
|
|
254
|
+
md += "### Employment\n\n";
|
|
255
|
+
for (const summary of employments) {
|
|
256
|
+
const line = formatAffiliation(summary);
|
|
257
|
+
if (line) md += `- ${line}\n`;
|
|
258
|
+
}
|
|
259
|
+
md += "\n";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (educations.length > 0) {
|
|
263
|
+
hasAffiliations = true;
|
|
264
|
+
md += "### Education\n\n";
|
|
265
|
+
for (const summary of educations) {
|
|
266
|
+
const line = formatAffiliation(summary);
|
|
267
|
+
if (line) md += `- ${line}\n`;
|
|
268
|
+
}
|
|
269
|
+
md += "\n";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!hasAffiliations) {
|
|
273
|
+
md += "No affiliations available.\n\n";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
md += "## Works\n\n";
|
|
277
|
+
if (works.length > 0) {
|
|
278
|
+
for (const title of works) {
|
|
279
|
+
md += `- ${title}\n`;
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
md += "No works available.\n";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const output = finalizeOutput(md);
|
|
286
|
+
return {
|
|
287
|
+
url,
|
|
288
|
+
finalUrl: url,
|
|
289
|
+
contentType: "text/markdown",
|
|
290
|
+
method: "orcid-api",
|
|
291
|
+
content: output.content,
|
|
292
|
+
fetchedAt,
|
|
293
|
+
truncated: output.truncated,
|
|
294
|
+
notes: ["Fetched via ORCID Public API"],
|
|
295
|
+
};
|
|
296
|
+
} catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
@@ -47,7 +47,11 @@ interface OsvVulnerability {
|
|
|
47
47
|
/**
|
|
48
48
|
* Handle OSV (Open Source Vulnerabilities) URLs
|
|
49
49
|
*/
|
|
50
|
-
export const handleOsv: SpecialHandler = async (
|
|
50
|
+
export const handleOsv: SpecialHandler = async (
|
|
51
|
+
url: string,
|
|
52
|
+
timeout: number,
|
|
53
|
+
signal?: AbortSignal,
|
|
54
|
+
): Promise<RenderResult | null> => {
|
|
51
55
|
try {
|
|
52
56
|
const parsed = new URL(url);
|
|
53
57
|
if (parsed.hostname !== "osv.dev") return null;
|
|
@@ -64,6 +68,7 @@ export const handleOsv: SpecialHandler = async (url: string, timeout: number): P
|
|
|
64
68
|
const result = await loadPage(apiUrl, {
|
|
65
69
|
timeout,
|
|
66
70
|
headers: { Accept: "application/json" },
|
|
71
|
+
signal,
|
|
67
72
|
});
|
|
68
73
|
|
|
69
74
|
if (!result.ok) return null;
|
|
@@ -4,7 +4,11 @@ import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
|
4
4
|
/**
|
|
5
5
|
* Handle Packagist URLs via JSON API
|
|
6
6
|
*/
|
|
7
|
-
export const handlePackagist: SpecialHandler = async (
|
|
7
|
+
export const handlePackagist: SpecialHandler = async (
|
|
8
|
+
url: string,
|
|
9
|
+
timeout: number,
|
|
10
|
+
signal?: AbortSignal,
|
|
11
|
+
): Promise<RenderResult | null> => {
|
|
8
12
|
try {
|
|
9
13
|
const parsed = new URL(url);
|
|
10
14
|
if (parsed.hostname !== "packagist.org" && parsed.hostname !== "www.packagist.org") return null;
|
|
@@ -19,7 +23,7 @@ export const handlePackagist: SpecialHandler = async (url: string, timeout: numb
|
|
|
19
23
|
|
|
20
24
|
// Fetch from Packagist JSON API
|
|
21
25
|
const apiUrl = `https://packagist.org/packages/${vendor}/${packageName}.json`;
|
|
22
|
-
const result = await loadPage(apiUrl, { timeout });
|
|
26
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
23
27
|
|
|
24
28
|
if (!result.ok) return null;
|
|
25
29
|
|
|
@@ -3,7 +3,7 @@ import { finalizeOutput, formatCount, loadPage, type SpecialHandler } from "./ty
|
|
|
3
3
|
/**
|
|
4
4
|
* Handle pub.dev URLs via API
|
|
5
5
|
*/
|
|
6
|
-
export const handlePubDev: SpecialHandler = async (url: string, timeout: number) => {
|
|
6
|
+
export const handlePubDev: SpecialHandler = async (url: string, timeout: number, signal?: AbortSignal) => {
|
|
7
7
|
try {
|
|
8
8
|
const parsed = new URL(url);
|
|
9
9
|
if (parsed.hostname !== "pub.dev" && parsed.hostname !== "www.pub.dev") return null;
|
|
@@ -17,7 +17,7 @@ export const handlePubDev: SpecialHandler = async (url: string, timeout: number)
|
|
|
17
17
|
|
|
18
18
|
// Fetch from pub.dev API
|
|
19
19
|
const apiUrl = `https://pub.dev/api/packages/${encodeURIComponent(packageName)}`;
|
|
20
|
-
const result = await loadPage(apiUrl, { timeout });
|
|
20
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
21
21
|
|
|
22
22
|
if (!result.ok) return null;
|
|
23
23
|
|
|
@@ -122,7 +122,7 @@ export const handlePubDev: SpecialHandler = async (url: string, timeout: number)
|
|
|
122
122
|
// Try to fetch README from pub.dev
|
|
123
123
|
const readmeUrl = `https://pub.dev/packages/${encodeURIComponent(packageName)}/versions/${encodeURIComponent(latest.version)}/readme`;
|
|
124
124
|
try {
|
|
125
|
-
const readmeResult = await loadPage(readmeUrl, { timeout: Math.min(timeout, 10) });
|
|
125
|
+
const readmeResult = await loadPage(readmeUrl, { timeout: Math.min(timeout, 10), signal });
|
|
126
126
|
if (readmeResult.ok) {
|
|
127
127
|
// Extract README content from HTML
|
|
128
128
|
const readmeMatch = readmeResult.content.match(
|
|
@@ -8,7 +8,11 @@ import { finalizeOutput, loadPage } from "./types";
|
|
|
8
8
|
/**
|
|
9
9
|
* Handle PubMed URLs - fetch article metadata, abstract, MeSH terms
|
|
10
10
|
*/
|
|
11
|
-
export const handlePubMed: SpecialHandler = async (
|
|
11
|
+
export const handlePubMed: SpecialHandler = async (
|
|
12
|
+
url: string,
|
|
13
|
+
timeout: number,
|
|
14
|
+
signal?: AbortSignal,
|
|
15
|
+
): Promise<RenderResult | null> => {
|
|
12
16
|
try {
|
|
13
17
|
const parsed = new URL(url);
|
|
14
18
|
|
|
@@ -39,7 +43,7 @@ export const handlePubMed: SpecialHandler = async (url: string, timeout: number)
|
|
|
39
43
|
|
|
40
44
|
// Fetch summary metadata
|
|
41
45
|
const summaryUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&id=${pmid}&retmode=json`;
|
|
42
|
-
const summaryResult = await loadPage(summaryUrl, { timeout });
|
|
46
|
+
const summaryResult = await loadPage(summaryUrl, { timeout, signal });
|
|
43
47
|
|
|
44
48
|
if (!summaryResult.ok) return null;
|
|
45
49
|
|
|
@@ -70,7 +74,7 @@ export const handlePubMed: SpecialHandler = async (url: string, timeout: number)
|
|
|
70
74
|
|
|
71
75
|
// Fetch abstract
|
|
72
76
|
const abstractUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=${pmid}&rettype=abstract&retmode=text`;
|
|
73
|
-
const abstractResult = await loadPage(abstractUrl, { timeout });
|
|
77
|
+
const abstractResult = await loadPage(abstractUrl, { timeout, signal });
|
|
74
78
|
|
|
75
79
|
let abstractText = "";
|
|
76
80
|
if (abstractResult.ok) {
|
|
@@ -133,7 +137,7 @@ export const handlePubMed: SpecialHandler = async (url: string, timeout: number)
|
|
|
133
137
|
// Try to fetch MeSH terms
|
|
134
138
|
try {
|
|
135
139
|
const meshUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=${pmid}&rettype=medline&retmode=text`;
|
|
136
|
-
const meshResult = await loadPage(meshUrl, { timeout: Math.min(timeout, 5) });
|
|
140
|
+
const meshResult = await loadPage(meshUrl, { timeout: Math.min(timeout, 5), signal });
|
|
137
141
|
|
|
138
142
|
if (meshResult.ok) {
|
|
139
143
|
const meshTerms: string[] = [];
|
|
@@ -4,7 +4,11 @@ import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
|
4
4
|
/**
|
|
5
5
|
* Handle PyPI URLs via JSON API
|
|
6
6
|
*/
|
|
7
|
-
export const handlePyPI: SpecialHandler = async (
|
|
7
|
+
export const handlePyPI: SpecialHandler = async (
|
|
8
|
+
url: string,
|
|
9
|
+
timeout: number,
|
|
10
|
+
signal?: AbortSignal,
|
|
11
|
+
): Promise<RenderResult | null> => {
|
|
8
12
|
try {
|
|
9
13
|
const parsed = new URL(url);
|
|
10
14
|
if (parsed.hostname !== "pypi.org" && parsed.hostname !== "www.pypi.org") return null;
|
|
@@ -22,8 +26,8 @@ export const handlePyPI: SpecialHandler = async (url: string, timeout: number):
|
|
|
22
26
|
|
|
23
27
|
// Fetch package info and download stats in parallel
|
|
24
28
|
const [result, downloadsResult] = await Promise.all([
|
|
25
|
-
loadPage(apiUrl, { timeout }),
|
|
26
|
-
loadPage(downloadsUrl, { timeout: Math.min(timeout, 5) }),
|
|
29
|
+
loadPage(apiUrl, { timeout, signal }),
|
|
30
|
+
loadPage(downloadsUrl, { timeout: Math.min(timeout, 5), signal }),
|
|
27
31
|
]);
|
|
28
32
|
|
|
29
33
|
if (!result.ok) return null;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, htmlToBasicMarkdown, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface RawgPlatformEntry {
|
|
5
|
+
platform?: {
|
|
6
|
+
name?: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface RawgGenreEntry {
|
|
11
|
+
name?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface RawgGameResponse {
|
|
15
|
+
name?: string;
|
|
16
|
+
released?: string;
|
|
17
|
+
rating?: number;
|
|
18
|
+
platforms?: RawgPlatformEntry[];
|
|
19
|
+
genres?: RawgGenreEntry[];
|
|
20
|
+
description?: string;
|
|
21
|
+
description_raw?: string;
|
|
22
|
+
detail?: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const handleRawg: SpecialHandler = async (
|
|
27
|
+
url: string,
|
|
28
|
+
timeout: number,
|
|
29
|
+
signal?: AbortSignal,
|
|
30
|
+
): Promise<RenderResult | null> => {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = new URL(url);
|
|
33
|
+
if (!isRawgHostname(parsed.hostname)) return null;
|
|
34
|
+
|
|
35
|
+
const slug = extractGameSlug(parsed.pathname);
|
|
36
|
+
if (!slug) return null;
|
|
37
|
+
|
|
38
|
+
const fetchedAt = new Date().toISOString();
|
|
39
|
+
const apiUrl = `https://api.rawg.io/api/games/${encodeURIComponent(slug)}`;
|
|
40
|
+
const result = await loadPage(apiUrl, { timeout, signal, headers: { Accept: "application/json" } });
|
|
41
|
+
|
|
42
|
+
if (!result.ok) return null;
|
|
43
|
+
|
|
44
|
+
let game: RawgGameResponse;
|
|
45
|
+
try {
|
|
46
|
+
game = JSON.parse(result.content);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (requiresApiKey(game)) return null;
|
|
52
|
+
|
|
53
|
+
const title = game.name?.trim() || slug;
|
|
54
|
+
let md = `# ${title}\n\n`;
|
|
55
|
+
|
|
56
|
+
if (game.released) md += `**Released:** ${game.released}\n`;
|
|
57
|
+
if (typeof game.rating === "number" && !Number.isNaN(game.rating)) {
|
|
58
|
+
md += `**Rating:** ${game.rating.toFixed(2)} / 5\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const platforms = collectNames(game.platforms?.map((entry) => entry.platform?.name));
|
|
62
|
+
if (platforms.length) md += `**Platforms:** ${platforms.join(", ")}\n`;
|
|
63
|
+
|
|
64
|
+
const genres = collectNames(game.genres?.map((entry) => entry.name));
|
|
65
|
+
if (genres.length) md += `**Genres:** ${genres.join(", ")}\n`;
|
|
66
|
+
|
|
67
|
+
md += `**RAWG:** https://rawg.io/games/${encodeURIComponent(slug)}\n`;
|
|
68
|
+
md += "\n";
|
|
69
|
+
|
|
70
|
+
const description = extractDescription(game);
|
|
71
|
+
if (description) {
|
|
72
|
+
md += `## Description\n\n${description}\n`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const output = finalizeOutput(md);
|
|
76
|
+
return {
|
|
77
|
+
url,
|
|
78
|
+
finalUrl: url,
|
|
79
|
+
contentType: "text/markdown",
|
|
80
|
+
method: "rawg",
|
|
81
|
+
content: output.content,
|
|
82
|
+
fetchedAt,
|
|
83
|
+
truncated: output.truncated,
|
|
84
|
+
notes: ["Fetched via RAWG API"],
|
|
85
|
+
};
|
|
86
|
+
} catch {}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function isRawgHostname(hostname: string): boolean {
|
|
92
|
+
return hostname === "rawg.io" || hostname === "www.rawg.io";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function extractGameSlug(pathname: string): string | null {
|
|
96
|
+
const match = pathname.match(/^\/games\/([^/?#]+)/);
|
|
97
|
+
if (!match) return null;
|
|
98
|
+
|
|
99
|
+
const slug = decodeURIComponent(match[1]);
|
|
100
|
+
return slug ? slug.trim() : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function requiresApiKey(game: RawgGameResponse): boolean {
|
|
104
|
+
const detail = `${game.detail ?? ""} ${game.error ?? ""}`.toLowerCase();
|
|
105
|
+
return detail.includes("api key") || detail.includes("key is required") || detail.includes("apikey");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractDescription(game: RawgGameResponse): string | null {
|
|
109
|
+
if (game.description_raw) return game.description_raw.trim();
|
|
110
|
+
if (!game.description) return null;
|
|
111
|
+
|
|
112
|
+
const markdown = htmlToBasicMarkdown(game.description).trim();
|
|
113
|
+
return markdown || null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function collectNames(values?: Array<string | undefined>): string[] {
|
|
117
|
+
if (!values?.length) return [];
|
|
118
|
+
const names = new Set<string>();
|
|
119
|
+
for (const value of values) {
|
|
120
|
+
const trimmed = value?.trim();
|
|
121
|
+
if (trimmed) names.add(trimmed);
|
|
122
|
+
}
|
|
123
|
+
return Array.from(names);
|
|
124
|
+
}
|
|
@@ -6,7 +6,11 @@ import { parse as parseHtml } from "node-html-parser";
|
|
|
6
6
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
7
7
|
import { finalizeOutput, htmlToBasicMarkdown, loadPage } from "./types";
|
|
8
8
|
|
|
9
|
-
export const handleReadTheDocs: SpecialHandler = async (
|
|
9
|
+
export const handleReadTheDocs: SpecialHandler = async (
|
|
10
|
+
url: string,
|
|
11
|
+
timeout: number,
|
|
12
|
+
signal?: AbortSignal,
|
|
13
|
+
): Promise<RenderResult | null> => {
|
|
10
14
|
// Check if URL matches Read the Docs patterns
|
|
11
15
|
const urlObj = new URL(url);
|
|
12
16
|
const isReadTheDocs =
|
|
@@ -22,7 +26,7 @@ export const handleReadTheDocs: SpecialHandler = async (url: string, timeout: nu
|
|
|
22
26
|
const fetchedAt = new Date().toISOString();
|
|
23
27
|
|
|
24
28
|
// Fetch the page
|
|
25
|
-
const result = await loadPage(url, { timeout });
|
|
29
|
+
const result = await loadPage(url, { timeout, signal });
|
|
26
30
|
if (!result.ok) {
|
|
27
31
|
return {
|
|
28
32
|
url,
|
|
@@ -86,7 +90,7 @@ export const handleReadTheDocs: SpecialHandler = async (url: string, timeout: nu
|
|
|
86
90
|
// Try to fetch raw source if available
|
|
87
91
|
if (sourceUrl) {
|
|
88
92
|
try {
|
|
89
|
-
const sourceResult = await loadPage(sourceUrl, { timeout: Math.min(timeout, 10) });
|
|
93
|
+
const sourceResult = await loadPage(sourceUrl, { timeout: Math.min(timeout, 10), signal });
|
|
90
94
|
if (sourceResult.ok && sourceResult.content.length > 0 && sourceResult.content.length < 1_000_000) {
|
|
91
95
|
content = sourceResult.content;
|
|
92
96
|
notes.push(`Fetched raw source from ${sourceUrl}`);
|
|
@@ -24,7 +24,11 @@ interface RedditComment {
|
|
|
24
24
|
/**
|
|
25
25
|
* Handle Reddit URLs via JSON API
|
|
26
26
|
*/
|
|
27
|
-
export const handleReddit: SpecialHandler = async (
|
|
27
|
+
export const handleReddit: SpecialHandler = async (
|
|
28
|
+
url: string,
|
|
29
|
+
timeout: number,
|
|
30
|
+
signal?: AbortSignal,
|
|
31
|
+
): Promise<RenderResult | null> => {
|
|
28
32
|
try {
|
|
29
33
|
const parsed = new URL(url);
|
|
30
34
|
if (!parsed.hostname.includes("reddit.com")) return null;
|
|
@@ -37,7 +41,7 @@ export const handleReddit: SpecialHandler = async (url: string, timeout: number)
|
|
|
37
41
|
jsonUrl = `${url.replace(/\/$/, "").replace(parsed.search, "")}.json${parsed.search}`;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
const result = await loadPage(jsonUrl, { timeout });
|
|
44
|
+
const result = await loadPage(jsonUrl, { timeout, signal });
|
|
41
45
|
if (!result.ok) return null;
|
|
42
46
|
|
|
43
47
|
const data = JSON.parse(result.content);
|
|
@@ -102,7 +102,11 @@ function prettifyRepo(repo: string): string {
|
|
|
102
102
|
/**
|
|
103
103
|
* Handle Repology URLs via API
|
|
104
104
|
*/
|
|
105
|
-
export const handleRepology: SpecialHandler = async (
|
|
105
|
+
export const handleRepology: SpecialHandler = async (
|
|
106
|
+
url: string,
|
|
107
|
+
timeout: number,
|
|
108
|
+
signal?: AbortSignal,
|
|
109
|
+
): Promise<RenderResult | null> => {
|
|
106
110
|
try {
|
|
107
111
|
const parsed = new URL(url);
|
|
108
112
|
if (parsed.hostname !== "repology.org" && parsed.hostname !== "www.repology.org") return null;
|
|
@@ -119,6 +123,7 @@ export const handleRepology: SpecialHandler = async (url: string, timeout: numbe
|
|
|
119
123
|
const result = await loadPage(apiUrl, {
|
|
120
124
|
timeout,
|
|
121
125
|
headers: { Accept: "application/json" },
|
|
126
|
+
signal,
|
|
122
127
|
});
|
|
123
128
|
|
|
124
129
|
if (!result.ok) return null;
|
|
@@ -90,7 +90,11 @@ function cleanRfcText(text: string): string {
|
|
|
90
90
|
/**
|
|
91
91
|
* Handle RFC Editor URLs - fetches IETF RFCs
|
|
92
92
|
*/
|
|
93
|
-
export const handleRfc: SpecialHandler = async (
|
|
93
|
+
export const handleRfc: SpecialHandler = async (
|
|
94
|
+
url: string,
|
|
95
|
+
timeout: number,
|
|
96
|
+
signal?: AbortSignal,
|
|
97
|
+
): Promise<RenderResult | null> => {
|
|
94
98
|
try {
|
|
95
99
|
const parsed = new URL(url);
|
|
96
100
|
const rfcNumber = extractRfcNumber(parsed);
|
|
@@ -105,8 +109,8 @@ export const handleRfc: SpecialHandler = async (url: string, timeout: number): P
|
|
|
105
109
|
const textUrl = `https://www.rfc-editor.org/rfc/rfc${rfcNumber}.txt`;
|
|
106
110
|
|
|
107
111
|
const [metaResult, textResult] = await Promise.all([
|
|
108
|
-
loadPage(metadataUrl, { timeout: Math.min(timeout, 10) }),
|
|
109
|
-
loadPage(textUrl, { timeout }),
|
|
112
|
+
loadPage(metadataUrl, { timeout: Math.min(timeout, 10), signal }),
|
|
113
|
+
loadPage(textUrl, { timeout, signal }),
|
|
110
114
|
]);
|
|
111
115
|
|
|
112
116
|
// We need at least the text content
|
|
@@ -30,7 +30,11 @@ interface RubyGemsResponse {
|
|
|
30
30
|
/**
|
|
31
31
|
* Handle RubyGems URLs via API
|
|
32
32
|
*/
|
|
33
|
-
export const handleRubyGems: SpecialHandler = async (
|
|
33
|
+
export const handleRubyGems: SpecialHandler = async (
|
|
34
|
+
url: string,
|
|
35
|
+
timeout: number,
|
|
36
|
+
signal?: AbortSignal,
|
|
37
|
+
): Promise<RenderResult | null> => {
|
|
34
38
|
try {
|
|
35
39
|
const parsed = new URL(url);
|
|
36
40
|
if (parsed.hostname !== "rubygems.org" && parsed.hostname !== "www.rubygems.org") return null;
|
|
@@ -46,6 +50,7 @@ export const handleRubyGems: SpecialHandler = async (url: string, timeout: numbe
|
|
|
46
50
|
const apiUrl = `https://rubygems.org/api/v1/gems/${encodeURIComponent(gemName)}.json`;
|
|
47
51
|
const result = await loadPage(apiUrl, {
|
|
48
52
|
timeout,
|
|
53
|
+
signal,
|
|
49
54
|
headers: { Accept: "application/json" },
|
|
50
55
|
});
|
|
51
56
|
|