@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,214 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, htmlToBasicMarkdown, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
type LocalizedText = string | Record<string, string | null | undefined> | null | undefined;
|
|
5
|
+
|
|
6
|
+
type AddonFile = {
|
|
7
|
+
permissions?: string[];
|
|
8
|
+
host_permissions?: string[];
|
|
9
|
+
optional_permissions?: string[];
|
|
10
|
+
optional_host_permissions?: string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type AddonLicense = {
|
|
14
|
+
name?: LocalizedText;
|
|
15
|
+
slug?: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type AddonVersion = {
|
|
20
|
+
version?: string;
|
|
21
|
+
license?: AddonLicense;
|
|
22
|
+
file?: AddonFile;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type AddonHomepage = {
|
|
26
|
+
url?: LocalizedText;
|
|
27
|
+
outgoing?: LocalizedText;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type AddonData = {
|
|
31
|
+
name?: LocalizedText;
|
|
32
|
+
summary?: LocalizedText;
|
|
33
|
+
description?: LocalizedText;
|
|
34
|
+
default_locale?: string;
|
|
35
|
+
authors?: Array<{ name?: string | null }>;
|
|
36
|
+
average_daily_users?: number;
|
|
37
|
+
weekly_downloads?: number;
|
|
38
|
+
ratings?: { average?: number; count?: number };
|
|
39
|
+
current_version?: AddonVersion;
|
|
40
|
+
categories?: string[] | Record<string, string[]>;
|
|
41
|
+
homepage?: AddonHomepage;
|
|
42
|
+
url?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function getLocalizedText(value: LocalizedText, defaultLocale?: string): string | undefined {
|
|
46
|
+
if (!value) return undefined;
|
|
47
|
+
if (typeof value === "string") return value;
|
|
48
|
+
|
|
49
|
+
const localized = value as Record<string, string | null | undefined>;
|
|
50
|
+
if (defaultLocale && localized[defaultLocale]) return localized[defaultLocale] ?? undefined;
|
|
51
|
+
if (localized["en-US"]) return localized["en-US"] ?? undefined;
|
|
52
|
+
|
|
53
|
+
for (const entry of Object.values(localized)) {
|
|
54
|
+
if (entry) return entry;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeCategories(categories?: string[] | Record<string, string[]>): string[] {
|
|
61
|
+
if (!categories) return [];
|
|
62
|
+
if (Array.isArray(categories)) return categories.filter(Boolean);
|
|
63
|
+
|
|
64
|
+
const values: string[] = [];
|
|
65
|
+
for (const list of Object.values(categories)) {
|
|
66
|
+
if (Array.isArray(list)) {
|
|
67
|
+
for (const item of list) {
|
|
68
|
+
if (item) values.push(item);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const seen = new Set<string>();
|
|
74
|
+
return values.filter((item) => {
|
|
75
|
+
if (seen.has(item)) return false;
|
|
76
|
+
seen.add(item);
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function collectPermissions(file?: AddonFile): string[] {
|
|
82
|
+
if (!file) return [];
|
|
83
|
+
const permissions: string[] = [];
|
|
84
|
+
const seen = new Set<string>();
|
|
85
|
+
|
|
86
|
+
const add = (items?: string[]) => {
|
|
87
|
+
for (const item of items ?? []) {
|
|
88
|
+
if (!item || seen.has(item)) continue;
|
|
89
|
+
seen.add(item);
|
|
90
|
+
permissions.push(item);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
add(file.permissions);
|
|
95
|
+
add(file.host_permissions);
|
|
96
|
+
add(file.optional_permissions);
|
|
97
|
+
add(file.optional_host_permissions);
|
|
98
|
+
|
|
99
|
+
return permissions;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const handleFirefoxAddons: SpecialHandler = async (
|
|
103
|
+
url: string,
|
|
104
|
+
timeout: number,
|
|
105
|
+
signal?: AbortSignal,
|
|
106
|
+
): Promise<RenderResult | null> => {
|
|
107
|
+
try {
|
|
108
|
+
const parsed = new URL(url);
|
|
109
|
+
if (parsed.hostname !== "addons.mozilla.org") return null;
|
|
110
|
+
|
|
111
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
112
|
+
const addonIndex = segments.indexOf("addon");
|
|
113
|
+
if (addonIndex === -1) return null;
|
|
114
|
+
|
|
115
|
+
const slug = segments[addonIndex + 1] ? decodeURIComponent(segments[addonIndex + 1]) : "";
|
|
116
|
+
if (!slug) return null;
|
|
117
|
+
|
|
118
|
+
const apiUrl = `https://addons.mozilla.org/api/v5/addons/addon/${encodeURIComponent(slug)}/`;
|
|
119
|
+
const result = await loadPage(apiUrl, { timeout, headers: { Accept: "application/json" }, signal });
|
|
120
|
+
if (!result.ok) return null;
|
|
121
|
+
|
|
122
|
+
let data: AddonData;
|
|
123
|
+
try {
|
|
124
|
+
data = JSON.parse(result.content) as AddonData;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const fetchedAt = new Date().toISOString();
|
|
130
|
+
const defaultLocale = data.default_locale || "en-US";
|
|
131
|
+
|
|
132
|
+
const name = getLocalizedText(data.name, defaultLocale) ?? slug;
|
|
133
|
+
const summary = getLocalizedText(data.summary, defaultLocale);
|
|
134
|
+
const descriptionRaw = getLocalizedText(data.description, defaultLocale);
|
|
135
|
+
const description = descriptionRaw ? htmlToBasicMarkdown(descriptionRaw) : undefined;
|
|
136
|
+
|
|
137
|
+
const authors = (data.authors ?? [])
|
|
138
|
+
.map((author) => author.name ?? "")
|
|
139
|
+
.map((author) => author.trim())
|
|
140
|
+
.filter(Boolean);
|
|
141
|
+
|
|
142
|
+
const ratingAverage = data.ratings?.average;
|
|
143
|
+
const ratingCount = data.ratings?.count;
|
|
144
|
+
const users = data.average_daily_users ?? data.weekly_downloads;
|
|
145
|
+
const version = data.current_version?.version;
|
|
146
|
+
const categories = normalizeCategories(data.categories);
|
|
147
|
+
|
|
148
|
+
const licenseName =
|
|
149
|
+
getLocalizedText(data.current_version?.license?.name, defaultLocale) ?? data.current_version?.license?.slug;
|
|
150
|
+
const licenseUrl = data.current_version?.license?.url;
|
|
151
|
+
|
|
152
|
+
const homepage =
|
|
153
|
+
getLocalizedText(data.homepage?.url, defaultLocale) ??
|
|
154
|
+
getLocalizedText(data.homepage?.outgoing, defaultLocale);
|
|
155
|
+
|
|
156
|
+
const permissions = collectPermissions(data.current_version?.file);
|
|
157
|
+
|
|
158
|
+
let md = `# ${name}\n\n`;
|
|
159
|
+
if (summary) md += `${summary}\n\n`;
|
|
160
|
+
|
|
161
|
+
if (authors.length > 0) {
|
|
162
|
+
md += `**Author${authors.length > 1 ? "s" : ""}:** ${authors.join(", ")}\n`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (ratingAverage !== undefined) {
|
|
166
|
+
md += `**Rating:** ${ratingAverage.toFixed(2)}`;
|
|
167
|
+
if (ratingCount !== undefined) md += ` (${formatCount(ratingCount)} reviews)`;
|
|
168
|
+
md += "\n";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (users !== undefined) md += `**Users:** ${formatCount(users)}\n`;
|
|
172
|
+
if (version) md += `**Version:** ${version}\n`;
|
|
173
|
+
if (categories.length > 0) md += `**Categories:** ${categories.join(", ")}\n`;
|
|
174
|
+
|
|
175
|
+
if (licenseName && licenseUrl) {
|
|
176
|
+
md += `**License:** [${licenseName}](${licenseUrl})\n`;
|
|
177
|
+
} else if (licenseName) {
|
|
178
|
+
md += `**License:** ${licenseName}\n`;
|
|
179
|
+
} else if (licenseUrl) {
|
|
180
|
+
md += `**License:** ${licenseUrl}\n`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (homepage) md += `**Homepage:** ${homepage}\n`;
|
|
184
|
+
|
|
185
|
+
if (description) {
|
|
186
|
+
md += `\n## Description\n\n${description}\n`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (permissions.length > 0) {
|
|
190
|
+
const preview = permissions.slice(0, 40);
|
|
191
|
+
md += `\n## Permissions (${permissions.length})\n\n`;
|
|
192
|
+
for (const permission of preview) {
|
|
193
|
+
md += `- ${permission}\n`;
|
|
194
|
+
}
|
|
195
|
+
if (permissions.length > preview.length) {
|
|
196
|
+
md += `\n*...and ${permissions.length - preview.length} more*\n`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const output = finalizeOutput(md);
|
|
201
|
+
return {
|
|
202
|
+
url,
|
|
203
|
+
finalUrl: data.url ?? result.finalUrl ?? url,
|
|
204
|
+
contentType: "text/markdown",
|
|
205
|
+
method: "firefox-addons",
|
|
206
|
+
content: output.content,
|
|
207
|
+
fetchedAt,
|
|
208
|
+
truncated: output.truncated,
|
|
209
|
+
notes: ["Fetched via Firefox Add-ons API"],
|
|
210
|
+
};
|
|
211
|
+
} catch {}
|
|
212
|
+
|
|
213
|
+
return null;
|
|
214
|
+
};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, htmlToBasicMarkdown, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface FlathubScreenshotSize {
|
|
5
|
+
src?: string;
|
|
6
|
+
width?: string;
|
|
7
|
+
height?: string;
|
|
8
|
+
scale?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface FlathubScreenshot {
|
|
12
|
+
caption?: string | null;
|
|
13
|
+
sizes?: FlathubScreenshotSize[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface FlathubRelease {
|
|
17
|
+
version?: string;
|
|
18
|
+
timestamp?: string;
|
|
19
|
+
description?: string | null;
|
|
20
|
+
url?: string | null;
|
|
21
|
+
type?: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FlathubAppStream {
|
|
25
|
+
id?: string;
|
|
26
|
+
name?: string;
|
|
27
|
+
summary?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
developer_name?: string;
|
|
30
|
+
categories?: string[];
|
|
31
|
+
screenshots?: FlathubScreenshot[];
|
|
32
|
+
releases?: FlathubRelease[];
|
|
33
|
+
metadata?: Record<string, unknown>;
|
|
34
|
+
installs?: number | string;
|
|
35
|
+
permissions?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function extractAppId(pathname: string): string | null {
|
|
39
|
+
const detailsMatch = pathname.match(/^\/apps\/details\/([^/]+)\/?$/);
|
|
40
|
+
if (detailsMatch) return decodeURIComponent(detailsMatch[1]);
|
|
41
|
+
|
|
42
|
+
const appMatch = pathname.match(/^\/apps\/([^/]+)\/?$/);
|
|
43
|
+
if (appMatch) return decodeURIComponent(appMatch[1]);
|
|
44
|
+
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseNumber(value: unknown): number | null {
|
|
49
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
50
|
+
if (typeof value === "string") {
|
|
51
|
+
const cleaned = value.replace(/[^0-9.]/g, "");
|
|
52
|
+
if (!cleaned) return null;
|
|
53
|
+
const parsed = Number(cleaned);
|
|
54
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeStringList(value: unknown): string[] {
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
return value.filter((item): item is string => typeof item === "string" && item.trim().length > 0);
|
|
62
|
+
}
|
|
63
|
+
if (typeof value === "string") {
|
|
64
|
+
return value
|
|
65
|
+
.split(/[,;\n]+/)
|
|
66
|
+
.map((item) => item.trim())
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractInstalls(app: FlathubAppStream): number | null {
|
|
73
|
+
const direct = parseNumber(app.installs);
|
|
74
|
+
if (direct !== null) return direct;
|
|
75
|
+
|
|
76
|
+
if (!app.metadata) return null;
|
|
77
|
+
for (const [key, value] of Object.entries(app.metadata)) {
|
|
78
|
+
if (!key.toLowerCase().includes("install")) continue;
|
|
79
|
+
const parsed = parseNumber(value);
|
|
80
|
+
if (parsed !== null) return parsed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extractPermissions(app: FlathubAppStream): string[] {
|
|
87
|
+
const permissions: string[] = [];
|
|
88
|
+
permissions.push(...normalizeStringList(app.permissions));
|
|
89
|
+
|
|
90
|
+
if (app.metadata) {
|
|
91
|
+
for (const [key, value] of Object.entries(app.metadata)) {
|
|
92
|
+
if (!key.toLowerCase().includes("permission")) continue;
|
|
93
|
+
const list = normalizeStringList(value);
|
|
94
|
+
if (list.length) {
|
|
95
|
+
permissions.push(...list);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
99
|
+
permissions.push(`${key}: ${String(value)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Array.from(new Set(permissions));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function screenshotArea(size?: FlathubScreenshotSize): number {
|
|
108
|
+
if (!size) return 0;
|
|
109
|
+
const width = Number(size.width);
|
|
110
|
+
const height = Number(size.height);
|
|
111
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return 0;
|
|
112
|
+
return width * height;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function bestScreenshotUrl(sizes?: FlathubScreenshotSize[]): string | null {
|
|
116
|
+
if (!sizes || sizes.length === 0) return null;
|
|
117
|
+
|
|
118
|
+
let best = sizes[0];
|
|
119
|
+
let bestArea = screenshotArea(best);
|
|
120
|
+
|
|
121
|
+
for (const size of sizes) {
|
|
122
|
+
const area = screenshotArea(size);
|
|
123
|
+
if (area > bestArea) {
|
|
124
|
+
best = size;
|
|
125
|
+
bestArea = area;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return best.src ?? sizes[0].src ?? null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatReleaseDate(timestamp?: string | null): string | null {
|
|
133
|
+
if (!timestamp) return null;
|
|
134
|
+
const seconds = Number(timestamp);
|
|
135
|
+
if (!Number.isFinite(seconds)) return null;
|
|
136
|
+
const date = new Date(seconds * 1000);
|
|
137
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
138
|
+
return date.toISOString().split("T")[0] ?? null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const handleFlathub: SpecialHandler = async (
|
|
142
|
+
url: string,
|
|
143
|
+
timeout: number,
|
|
144
|
+
signal?: AbortSignal,
|
|
145
|
+
): Promise<RenderResult | null> => {
|
|
146
|
+
try {
|
|
147
|
+
const parsed = new URL(url);
|
|
148
|
+
if (parsed.hostname !== "flathub.org" && parsed.hostname !== "www.flathub.org") return null;
|
|
149
|
+
|
|
150
|
+
const appId = extractAppId(parsed.pathname);
|
|
151
|
+
if (!appId) return null;
|
|
152
|
+
|
|
153
|
+
const apiUrl = `https://flathub.org/api/v2/appstream/${encodeURIComponent(appId)}`;
|
|
154
|
+
const result = await loadPage(apiUrl, { timeout, signal, headers: { Accept: "application/json" } });
|
|
155
|
+
if (!result.ok) return null;
|
|
156
|
+
|
|
157
|
+
let app: FlathubAppStream;
|
|
158
|
+
try {
|
|
159
|
+
app = JSON.parse(result.content) as FlathubAppStream;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const fetchedAt = new Date().toISOString();
|
|
165
|
+
const name = app.name ?? app.id ?? appId;
|
|
166
|
+
|
|
167
|
+
let md = `# ${name}\n\n`;
|
|
168
|
+
if (app.summary) md += `${app.summary}\n\n`;
|
|
169
|
+
|
|
170
|
+
md += "## Metadata\n\n";
|
|
171
|
+
md += `**App ID:** ${app.id ?? appId}\n`;
|
|
172
|
+
if (app.developer_name) md += `**Developer:** ${app.developer_name}\n`;
|
|
173
|
+
|
|
174
|
+
const installs = extractInstalls(app);
|
|
175
|
+
if (installs !== null) md += `**Installs:** ${formatCount(installs)}\n`;
|
|
176
|
+
|
|
177
|
+
if (app.categories?.length) {
|
|
178
|
+
md += "\n## Categories\n\n";
|
|
179
|
+
for (const category of app.categories) {
|
|
180
|
+
md += `- ${category}\n`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (app.description) {
|
|
185
|
+
const description = htmlToBasicMarkdown(app.description);
|
|
186
|
+
if (description) md += `\n## Description\n\n${description}\n`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const permissions = extractPermissions(app);
|
|
190
|
+
if (permissions.length) {
|
|
191
|
+
md += "\n## Permissions\n\n";
|
|
192
|
+
for (const permission of permissions) {
|
|
193
|
+
md += `- ${permission}\n`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (app.screenshots?.length) {
|
|
198
|
+
md += "\n## Screenshots\n\n";
|
|
199
|
+
for (const screenshot of app.screenshots.slice(0, 5)) {
|
|
200
|
+
const screenshotUrl = bestScreenshotUrl(screenshot.sizes);
|
|
201
|
+
if (!screenshotUrl) continue;
|
|
202
|
+
const caption = screenshot.caption ? ` - ${screenshot.caption}` : "";
|
|
203
|
+
md += `- ${screenshotUrl}${caption}\n`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (app.releases?.length) {
|
|
208
|
+
md += "\n## Releases\n\n";
|
|
209
|
+
for (const release of app.releases.slice(0, 5)) {
|
|
210
|
+
const version = release.version ?? "unknown";
|
|
211
|
+
let line = `- **${version}**`;
|
|
212
|
+
const date = formatReleaseDate(release.timestamp);
|
|
213
|
+
if (date) line += ` (${date})`;
|
|
214
|
+
if (release.type) line += ` · ${release.type}`;
|
|
215
|
+
if (release.url) line += ` · ${release.url}`;
|
|
216
|
+
md += `${line}\n`;
|
|
217
|
+
|
|
218
|
+
if (release.description) {
|
|
219
|
+
const releaseDesc = htmlToBasicMarkdown(release.description).replace(/\n+/g, " ").trim();
|
|
220
|
+
if (releaseDesc) md += ` - ${releaseDesc}\n`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const output = finalizeOutput(md);
|
|
226
|
+
return {
|
|
227
|
+
url,
|
|
228
|
+
finalUrl: result.finalUrl,
|
|
229
|
+
contentType: "text/markdown",
|
|
230
|
+
method: "flathub-appstream",
|
|
231
|
+
content: output.content,
|
|
232
|
+
fetchedAt,
|
|
233
|
+
truncated: output.truncated,
|
|
234
|
+
notes: ["Fetched via Flathub Appstream API"],
|
|
235
|
+
};
|
|
236
|
+
} catch {}
|
|
237
|
+
|
|
238
|
+
return null;
|
|
239
|
+
};
|
|
@@ -5,7 +5,11 @@ import { finalizeOutput } from "./types";
|
|
|
5
5
|
/**
|
|
6
6
|
* Handle GitHub Gist URLs via GitHub API
|
|
7
7
|
*/
|
|
8
|
-
export const handleGitHubGist: SpecialHandler = async (
|
|
8
|
+
export const handleGitHubGist: SpecialHandler = async (
|
|
9
|
+
url: string,
|
|
10
|
+
timeout: number,
|
|
11
|
+
signal?: AbortSignal,
|
|
12
|
+
): Promise<RenderResult | null> => {
|
|
9
13
|
try {
|
|
10
14
|
const parsed = new URL(url);
|
|
11
15
|
if (parsed.hostname !== "gist.github.com") return null;
|
|
@@ -21,7 +25,7 @@ export const handleGitHubGist: SpecialHandler = async (url: string, timeout: num
|
|
|
21
25
|
const fetchedAt = new Date().toISOString();
|
|
22
26
|
|
|
23
27
|
// Fetch via GitHub API
|
|
24
|
-
const result = await fetchGitHubApi(`/gists/${gistId}`, timeout);
|
|
28
|
+
const result = await fetchGitHubApi(`/gists/${gistId}`, timeout, signal);
|
|
25
29
|
if (!result.ok || !result.data) return null;
|
|
26
30
|
|
|
27
31
|
const gist = result.data as {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
-
import { finalizeOutput, loadPage } from "./types";
|
|
2
|
+
import { createRequestSignal, finalizeOutput, loadPage } from "./types";
|
|
3
3
|
|
|
4
4
|
interface GitHubUrl {
|
|
5
5
|
type: "blob" | "tree" | "repo" | "issue" | "issues" | "pull" | "pulls" | "discussion" | "discussions" | "other";
|
|
@@ -64,16 +64,19 @@ function parseGitHubUrl(url: string): GitHubUrl | null {
|
|
|
64
64
|
* Convert GitHub blob URL to raw URL
|
|
65
65
|
*/
|
|
66
66
|
function toRawGitHubUrl(gh: GitHubUrl): string {
|
|
67
|
-
return `https://raw.githubusercontent.com/${gh.owner}/${gh.repo}
|
|
67
|
+
return `https://raw.githubusercontent.com/${gh.owner}/${gh.repo}/${gh.ref}/${gh.path}`;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Fetch from GitHub API
|
|
72
72
|
*/
|
|
73
|
-
export async function fetchGitHubApi(
|
|
73
|
+
export async function fetchGitHubApi(
|
|
74
|
+
endpoint: string,
|
|
75
|
+
timeout: number,
|
|
76
|
+
signal?: AbortSignal,
|
|
77
|
+
): Promise<{ data: unknown; ok: boolean }> {
|
|
74
78
|
try {
|
|
75
|
-
const
|
|
76
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout * 1000);
|
|
79
|
+
const { signal: requestSignal, cleanup } = createRequestSignal(timeout * 1000, signal);
|
|
77
80
|
|
|
78
81
|
const headers: Record<string, string> = {
|
|
79
82
|
Accept: "application/vnd.github.v3+json",
|
|
@@ -86,18 +89,20 @@ export async function fetchGitHubApi(endpoint: string, timeout: number): Promise
|
|
|
86
89
|
headers.Authorization = `Bearer ${token}`;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(`https://api.github.com${endpoint}`, {
|
|
94
|
+
signal: requestSignal,
|
|
95
|
+
headers,
|
|
96
|
+
});
|
|
93
97
|
|
|
94
|
-
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
return { data: null, ok: false };
|
|
100
|
+
}
|
|
95
101
|
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
return { data: await response.json(), ok: true };
|
|
103
|
+
} finally {
|
|
104
|
+
cleanup();
|
|
98
105
|
}
|
|
99
|
-
|
|
100
|
-
return { data: await response.json(), ok: true };
|
|
101
106
|
} catch {
|
|
102
107
|
return { data: null, ok: false };
|
|
103
108
|
}
|
|
@@ -106,13 +111,17 @@ export async function fetchGitHubApi(endpoint: string, timeout: number): Promise
|
|
|
106
111
|
/**
|
|
107
112
|
* Render GitHub issue/PR to markdown
|
|
108
113
|
*/
|
|
109
|
-
async function renderGitHubIssue(
|
|
114
|
+
async function renderGitHubIssue(
|
|
115
|
+
gh: GitHubUrl,
|
|
116
|
+
timeout: number,
|
|
117
|
+
signal?: AbortSignal,
|
|
118
|
+
): Promise<{ content: string; ok: boolean }> {
|
|
110
119
|
const endpoint =
|
|
111
120
|
gh.type === "pull"
|
|
112
121
|
? `/repos/${gh.owner}/${gh.repo}/pulls/${gh.number}`
|
|
113
122
|
: `/repos/${gh.owner}/${gh.repo}/issues/${gh.number}`;
|
|
114
123
|
|
|
115
|
-
const result = await fetchGitHubApi(endpoint, timeout);
|
|
124
|
+
const result = await fetchGitHubApi(endpoint, timeout, signal);
|
|
116
125
|
if (!result.ok || !result.data) return { content: "", ok: false };
|
|
117
126
|
|
|
118
127
|
const issue = result.data as {
|
|
@@ -143,6 +152,7 @@ async function renderGitHubIssue(gh: GitHubUrl, timeout: number): Promise<{ cont
|
|
|
143
152
|
const commentsResult = await fetchGitHubApi(
|
|
144
153
|
`/repos/${gh.owner}/${gh.repo}/issues/${gh.number}/comments?per_page=50`,
|
|
145
154
|
timeout,
|
|
155
|
+
signal,
|
|
146
156
|
);
|
|
147
157
|
if (commentsResult.ok && Array.isArray(commentsResult.data)) {
|
|
148
158
|
md += `## Comments (${issue.comments})\n\n`;
|
|
@@ -163,8 +173,12 @@ async function renderGitHubIssue(gh: GitHubUrl, timeout: number): Promise<{ cont
|
|
|
163
173
|
/**
|
|
164
174
|
* Render GitHub issues list to markdown
|
|
165
175
|
*/
|
|
166
|
-
async function renderGitHubIssuesList(
|
|
167
|
-
|
|
176
|
+
async function renderGitHubIssuesList(
|
|
177
|
+
gh: GitHubUrl,
|
|
178
|
+
timeout: number,
|
|
179
|
+
signal?: AbortSignal,
|
|
180
|
+
): Promise<{ content: string; ok: boolean }> {
|
|
181
|
+
const result = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}/issues?state=open&per_page=30`, timeout, signal);
|
|
168
182
|
if (!result.ok || !Array.isArray(result.data)) return { content: "", ok: false };
|
|
169
183
|
|
|
170
184
|
const issues = result.data as Array<{
|
|
@@ -193,9 +207,13 @@ async function renderGitHubIssuesList(gh: GitHubUrl, timeout: number): Promise<{
|
|
|
193
207
|
/**
|
|
194
208
|
* Render GitHub tree (directory) to markdown
|
|
195
209
|
*/
|
|
196
|
-
async function renderGitHubTree(
|
|
210
|
+
async function renderGitHubTree(
|
|
211
|
+
gh: GitHubUrl,
|
|
212
|
+
timeout: number,
|
|
213
|
+
signal?: AbortSignal,
|
|
214
|
+
): Promise<{ content: string; ok: boolean }> {
|
|
197
215
|
// Fetch repo info first to get default branch if ref not specified
|
|
198
|
-
const repoResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}`, timeout);
|
|
216
|
+
const repoResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}`, timeout, signal);
|
|
199
217
|
if (!repoResult.ok) return { content: "", ok: false };
|
|
200
218
|
|
|
201
219
|
const repo = repoResult.data as {
|
|
@@ -210,7 +228,11 @@ async function renderGitHubTree(gh: GitHubUrl, timeout: number): Promise<{ conte
|
|
|
210
228
|
md += `**Branch:** ${ref}\n\n`;
|
|
211
229
|
|
|
212
230
|
// Fetch directory contents
|
|
213
|
-
const contentsResult = await fetchGitHubApi(
|
|
231
|
+
const contentsResult = await fetchGitHubApi(
|
|
232
|
+
`/repos/${gh.owner}/${gh.repo}/contents/${dirPath}?ref=${ref}`,
|
|
233
|
+
timeout,
|
|
234
|
+
signal,
|
|
235
|
+
);
|
|
214
236
|
|
|
215
237
|
if (contentsResult.ok && Array.isArray(contentsResult.data)) {
|
|
216
238
|
const items = contentsResult.data as Array<{
|
|
@@ -240,8 +262,8 @@ async function renderGitHubTree(gh: GitHubUrl, timeout: number): Promise<{ conte
|
|
|
240
262
|
const readmeFile = items.find((item) => item.type === "file" && /^readme\.md$/i.test(item.name));
|
|
241
263
|
if (readmeFile) {
|
|
242
264
|
const readmePath = dirPath ? `${dirPath}/${readmeFile.name}` : readmeFile.name;
|
|
243
|
-
const rawUrl = `https://raw.githubusercontent.com/${gh.owner}/${gh.repo}
|
|
244
|
-
const readmeResult = await loadPage(rawUrl, { timeout });
|
|
265
|
+
const rawUrl = `https://raw.githubusercontent.com/${gh.owner}/${gh.repo}/${ref}/${readmePath}`;
|
|
266
|
+
const readmeResult = await loadPage(rawUrl, { timeout, signal });
|
|
245
267
|
if (readmeResult.ok) {
|
|
246
268
|
md += `---\n\n## README\n\n${readmeResult.content}`;
|
|
247
269
|
}
|
|
@@ -254,9 +276,13 @@ async function renderGitHubTree(gh: GitHubUrl, timeout: number): Promise<{ conte
|
|
|
254
276
|
/**
|
|
255
277
|
* Render GitHub repo to markdown (file list + README)
|
|
256
278
|
*/
|
|
257
|
-
async function renderGitHubRepo(
|
|
279
|
+
async function renderGitHubRepo(
|
|
280
|
+
gh: GitHubUrl,
|
|
281
|
+
timeout: number,
|
|
282
|
+
signal?: AbortSignal,
|
|
283
|
+
): Promise<{ content: string; ok: boolean }> {
|
|
258
284
|
// Fetch repo info
|
|
259
|
-
const repoResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}`, timeout);
|
|
285
|
+
const repoResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}`, timeout, signal);
|
|
260
286
|
if (!repoResult.ok) return { content: "", ok: false };
|
|
261
287
|
|
|
262
288
|
const repo = repoResult.data as {
|
|
@@ -281,6 +307,7 @@ async function renderGitHubRepo(gh: GitHubUrl, timeout: number): Promise<{ conte
|
|
|
281
307
|
const treeResult = await fetchGitHubApi(
|
|
282
308
|
`/repos/${gh.owner}/${gh.repo}/git/trees/${repo.default_branch}?recursive=1`,
|
|
283
309
|
timeout,
|
|
310
|
+
signal,
|
|
284
311
|
);
|
|
285
312
|
if (treeResult.ok && treeResult.data) {
|
|
286
313
|
const tree = (treeResult.data as { tree: Array<{ path: string; type: string }> }).tree;
|
|
@@ -297,7 +324,7 @@ async function renderGitHubRepo(gh: GitHubUrl, timeout: number): Promise<{ conte
|
|
|
297
324
|
}
|
|
298
325
|
|
|
299
326
|
// Fetch README
|
|
300
|
-
const readmeResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}/readme`, timeout);
|
|
327
|
+
const readmeResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}/readme`, timeout, signal);
|
|
301
328
|
if (readmeResult.ok && readmeResult.data) {
|
|
302
329
|
const readme = readmeResult.data as { content: string; encoding: string };
|
|
303
330
|
if (readme.encoding === "base64") {
|
|
@@ -312,7 +339,11 @@ async function renderGitHubRepo(gh: GitHubUrl, timeout: number): Promise<{ conte
|
|
|
312
339
|
/**
|
|
313
340
|
* Handle GitHub URLs specially
|
|
314
341
|
*/
|
|
315
|
-
export const handleGitHub: SpecialHandler = async (
|
|
342
|
+
export const handleGitHub: SpecialHandler = async (
|
|
343
|
+
url: string,
|
|
344
|
+
timeout: number,
|
|
345
|
+
signal?: AbortSignal,
|
|
346
|
+
): Promise<RenderResult | null> => {
|
|
316
347
|
const gh = parseGitHubUrl(url);
|
|
317
348
|
if (!gh) return null;
|
|
318
349
|
|
|
@@ -324,7 +355,7 @@ export const handleGitHub: SpecialHandler = async (url: string, timeout: number)
|
|
|
324
355
|
// Convert to raw URL and fetch
|
|
325
356
|
const rawUrl = toRawGitHubUrl(gh);
|
|
326
357
|
notes.push(`Fetched raw: ${rawUrl}`);
|
|
327
|
-
const result = await loadPage(rawUrl, { timeout });
|
|
358
|
+
const result = await loadPage(rawUrl, { timeout, signal });
|
|
328
359
|
if (result.ok) {
|
|
329
360
|
const output = finalizeOutput(result.content);
|
|
330
361
|
return {
|
|
@@ -343,7 +374,7 @@ export const handleGitHub: SpecialHandler = async (url: string, timeout: number)
|
|
|
343
374
|
|
|
344
375
|
case "tree": {
|
|
345
376
|
notes.push(`Fetched via GitHub API`);
|
|
346
|
-
const result = await renderGitHubTree(gh, timeout);
|
|
377
|
+
const result = await renderGitHubTree(gh, timeout, signal);
|
|
347
378
|
if (result.ok) {
|
|
348
379
|
const output = finalizeOutput(result.content);
|
|
349
380
|
return {
|
|
@@ -363,7 +394,7 @@ export const handleGitHub: SpecialHandler = async (url: string, timeout: number)
|
|
|
363
394
|
case "issue":
|
|
364
395
|
case "pull": {
|
|
365
396
|
notes.push(`Fetched via GitHub API`);
|
|
366
|
-
const result = await renderGitHubIssue(gh, timeout);
|
|
397
|
+
const result = await renderGitHubIssue(gh, timeout, signal);
|
|
367
398
|
if (result.ok) {
|
|
368
399
|
const output = finalizeOutput(result.content);
|
|
369
400
|
return {
|
|
@@ -382,7 +413,7 @@ export const handleGitHub: SpecialHandler = async (url: string, timeout: number)
|
|
|
382
413
|
|
|
383
414
|
case "issues": {
|
|
384
415
|
notes.push(`Fetched via GitHub API`);
|
|
385
|
-
const result = await renderGitHubIssuesList(gh, timeout);
|
|
416
|
+
const result = await renderGitHubIssuesList(gh, timeout, signal);
|
|
386
417
|
if (result.ok) {
|
|
387
418
|
const output = finalizeOutput(result.content);
|
|
388
419
|
return {
|
|
@@ -401,7 +432,7 @@ export const handleGitHub: SpecialHandler = async (url: string, timeout: number)
|
|
|
401
432
|
|
|
402
433
|
case "repo": {
|
|
403
434
|
notes.push(`Fetched via GitHub API`);
|
|
404
|
-
const result = await renderGitHubRepo(gh, timeout);
|
|
435
|
+
const result = await renderGitHubRepo(gh, timeout, signal);
|
|
405
436
|
if (result.ok) {
|
|
406
437
|
const output = finalizeOutput(result.content);
|
|
407
438
|
return {
|