@oh-my-pi/pi-coding-agent 3.30.0 → 3.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +367 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/sdk.ts +10 -2
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/slash-commands.ts +39 -13
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +8 -4
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +84 -19
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +72 -35
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +150 -74
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/commands.ts +4 -0
- package/src/core/tools/task/executor.ts +94 -83
- package/src/core/tools/task/index.ts +130 -92
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +15 -6
- package/src/core/tools/task/worker.ts +124 -89
- package/src/core/tools/web-fetch.ts +112 -377
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -63
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/web-fetch-handlers/index.ts +0 -69
- package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface OllamaTagDetails {
|
|
5
|
+
parent_model?: string;
|
|
6
|
+
format?: string;
|
|
7
|
+
family?: string;
|
|
8
|
+
families?: string[] | null;
|
|
9
|
+
parameter_size?: string;
|
|
10
|
+
quantization_level?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface OllamaTagModel {
|
|
14
|
+
name?: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
modified_at?: string;
|
|
17
|
+
size?: number;
|
|
18
|
+
digest?: string;
|
|
19
|
+
details?: OllamaTagDetails;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface OllamaTagsResponse {
|
|
23
|
+
models?: OllamaTagModel[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const VALID_HOSTNAMES = new Set(["ollama.com", "www.ollama.com"]);
|
|
27
|
+
const RESERVED_ROOTS = new Set([
|
|
28
|
+
"models",
|
|
29
|
+
"blog",
|
|
30
|
+
"docs",
|
|
31
|
+
"download",
|
|
32
|
+
"cloud",
|
|
33
|
+
"signin",
|
|
34
|
+
"signout",
|
|
35
|
+
"search",
|
|
36
|
+
"api",
|
|
37
|
+
"terms",
|
|
38
|
+
"privacy",
|
|
39
|
+
"license",
|
|
40
|
+
"settings",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
function decodeHtmlEntities(value: string): string {
|
|
44
|
+
return value
|
|
45
|
+
.replace(/&/g, "&")
|
|
46
|
+
.replace(/</g, "<")
|
|
47
|
+
.replace(/>/g, ">")
|
|
48
|
+
.replace(/"/g, '"')
|
|
49
|
+
.replace(/'/g, "'")
|
|
50
|
+
.replace(/ /g, " ");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractMetaDescription(html: string): string | null {
|
|
54
|
+
const patterns = [
|
|
55
|
+
/<meta[^>]+name=["']description["'][^>]*content=["']([^"']+)["']/i,
|
|
56
|
+
/<meta[^>]+property=["']og:description["'][^>]*content=["']([^"']+)["']/i,
|
|
57
|
+
/<meta[^>]+property=["']twitter:description["'][^>]*content=["']([^"']+)["']/i,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
for (const pattern of patterns) {
|
|
61
|
+
const match = html.match(pattern);
|
|
62
|
+
if (match?.[1]) {
|
|
63
|
+
return decodeHtmlEntities(match[1].trim());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extractParameterSizes(html: string): string[] {
|
|
71
|
+
const sizes = new Set<string>();
|
|
72
|
+
const pattern = /x-test-size[^>]*>([^<]+)<\/span>/gi;
|
|
73
|
+
let match = pattern.exec(html);
|
|
74
|
+
while (match) {
|
|
75
|
+
const raw = match[1]?.trim();
|
|
76
|
+
if (raw) {
|
|
77
|
+
sizes.add(raw.toUpperCase());
|
|
78
|
+
}
|
|
79
|
+
match = pattern.exec(html);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return Array.from(sizes);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function extractTagsFromHtml(html: string, baseRef: string): string[] {
|
|
86
|
+
const tags = new Set<string>();
|
|
87
|
+
const pattern = /href=["']\/library\/([^"']+)["']/gi;
|
|
88
|
+
let match = pattern.exec(html);
|
|
89
|
+
while (match) {
|
|
90
|
+
const raw = match[1]?.trim();
|
|
91
|
+
if (raw) {
|
|
92
|
+
const decoded = decodeHtmlEntities(raw);
|
|
93
|
+
if (decoded === baseRef || decoded.startsWith(`${baseRef}:`)) {
|
|
94
|
+
tags.add(decoded);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
match = pattern.exec(html);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Array.from(tags);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatSize(bytes: number): string {
|
|
104
|
+
if (bytes >= 1_000_000_000) return `${(bytes / 1_000_000_000).toFixed(1)}GB`;
|
|
105
|
+
if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)}MB`;
|
|
106
|
+
if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(1)}KB`;
|
|
107
|
+
return `${bytes}B`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildModelPath(parts: string[]): string {
|
|
111
|
+
return parts.map((part) => encodeURIComponent(part)).join("/");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseOllamaUrl(url: string): { modelRef: string; baseRef: string; pageUrl: string } | null {
|
|
115
|
+
try {
|
|
116
|
+
const parsed = new URL(url);
|
|
117
|
+
if (!VALID_HOSTNAMES.has(parsed.hostname)) return null;
|
|
118
|
+
|
|
119
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
120
|
+
if (parts.length === 0) return null;
|
|
121
|
+
|
|
122
|
+
if (parts[0] === "library" && parts.length >= 2) {
|
|
123
|
+
const modelRef = decodeURIComponent(parts[1]);
|
|
124
|
+
const baseRef = modelRef.split(":")[0] ?? modelRef;
|
|
125
|
+
const pageUrl = `${parsed.origin}/${buildModelPath(["library", baseRef])}`;
|
|
126
|
+
return { modelRef, baseRef, pageUrl };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (parts.length >= 2 && !RESERVED_ROOTS.has(parts[0])) {
|
|
130
|
+
const namespace = decodeURIComponent(parts[0]);
|
|
131
|
+
const model = decodeURIComponent(parts[1]);
|
|
132
|
+
const modelBase = model.split(":")[0] ?? model;
|
|
133
|
+
const modelRef = `${namespace}/${model}`;
|
|
134
|
+
const baseRef = `${namespace}/${modelBase}`;
|
|
135
|
+
const pageUrl = `${parsed.origin}/${buildModelPath([namespace, modelBase])}`;
|
|
136
|
+
return { modelRef, baseRef, pageUrl };
|
|
137
|
+
}
|
|
138
|
+
} catch {}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function sortTags(tags: string[]): string[] {
|
|
144
|
+
return tags.sort((a, b) => {
|
|
145
|
+
const aLatest = a.endsWith(":latest");
|
|
146
|
+
const bLatest = b.endsWith(":latest");
|
|
147
|
+
if (aLatest && !bLatest) return -1;
|
|
148
|
+
if (!aLatest && bLatest) return 1;
|
|
149
|
+
return a.localeCompare(b);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function formatTagList(tags: string[], maxItems: number): string {
|
|
154
|
+
const limited = tags.slice(0, maxItems);
|
|
155
|
+
const formatted = limited.map((tag) => `\`${tag}\``).join(", ");
|
|
156
|
+
if (tags.length > maxItems) {
|
|
157
|
+
return `${formatted} (and ${tags.length - maxItems} more)`;
|
|
158
|
+
}
|
|
159
|
+
return formatted;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function collectParameterSizes(models: OllamaTagModel[], htmlSizes: string[]): string[] {
|
|
163
|
+
const sizes = new Set<string>();
|
|
164
|
+
for (const model of models) {
|
|
165
|
+
const param = model.details?.parameter_size?.trim();
|
|
166
|
+
if (param) sizes.add(param.toUpperCase());
|
|
167
|
+
}
|
|
168
|
+
for (const size of htmlSizes) {
|
|
169
|
+
sizes.add(size);
|
|
170
|
+
}
|
|
171
|
+
return Array.from(sizes);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const handleOllama: SpecialHandler = async (
|
|
175
|
+
url: string,
|
|
176
|
+
timeout: number,
|
|
177
|
+
signal?: AbortSignal,
|
|
178
|
+
): Promise<RenderResult | null> => {
|
|
179
|
+
try {
|
|
180
|
+
const parsed = parseOllamaUrl(url);
|
|
181
|
+
if (!parsed) return null;
|
|
182
|
+
|
|
183
|
+
const { modelRef, baseRef, pageUrl } = parsed;
|
|
184
|
+
const fetchedAt = new Date().toISOString();
|
|
185
|
+
|
|
186
|
+
const tagsUrl = "https://ollama.com/api/tags";
|
|
187
|
+
const [tagsResult, pageResult] = await Promise.all([
|
|
188
|
+
loadPage(tagsUrl, { timeout, signal, headers: { Accept: "application/json" } }),
|
|
189
|
+
loadPage(pageUrl, { timeout, signal }),
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
let tagsData: OllamaTagsResponse | null = null;
|
|
193
|
+
if (tagsResult.ok) {
|
|
194
|
+
try {
|
|
195
|
+
tagsData = JSON.parse(tagsResult.content) as OllamaTagsResponse;
|
|
196
|
+
} catch {
|
|
197
|
+
tagsData = null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const html = pageResult.ok ? pageResult.content : "";
|
|
202
|
+
const description = html ? extractMetaDescription(html) : null;
|
|
203
|
+
const htmlParameterSizes = html ? extractParameterSizes(html) : [];
|
|
204
|
+
const htmlTags = html ? extractTagsFromHtml(html, baseRef) : [];
|
|
205
|
+
|
|
206
|
+
const baseLower = baseRef.toLowerCase();
|
|
207
|
+
const models = tagsData?.models ?? [];
|
|
208
|
+
const matchingModels = models.filter((model) => {
|
|
209
|
+
const name = (model.model ?? model.name ?? "").toLowerCase();
|
|
210
|
+
return name === baseLower || name.startsWith(`${baseLower}:`);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const tagRef = modelRef.includes(":") ? modelRef : null;
|
|
214
|
+
const selectedTag = tagRef ? matchingModels.find((model) => (model.model ?? model.name ?? "") === tagRef) : null;
|
|
215
|
+
|
|
216
|
+
const availableTagsRaw = matchingModels
|
|
217
|
+
.map((model) => model.model ?? model.name ?? "")
|
|
218
|
+
.filter((tag) => tag.length > 0);
|
|
219
|
+
const availableTags = sortTags(Array.from(new Set(availableTagsRaw)));
|
|
220
|
+
|
|
221
|
+
const fallbackTags = sortTags(Array.from(new Set(htmlTags)));
|
|
222
|
+
const tagsToUse = availableTags.length > 0 ? availableTags : fallbackTags;
|
|
223
|
+
|
|
224
|
+
const parameterSizes = collectParameterSizes(selectedTag ? [selectedTag] : matchingModels, htmlParameterSizes);
|
|
225
|
+
|
|
226
|
+
const sizes = matchingModels
|
|
227
|
+
.map((model) => model.size)
|
|
228
|
+
.filter((size): size is number => typeof size === "number");
|
|
229
|
+
let sizeLine: string | null = null;
|
|
230
|
+
|
|
231
|
+
if (selectedTag?.size) {
|
|
232
|
+
sizeLine = formatSize(selectedTag.size);
|
|
233
|
+
} else if (sizes.length > 0) {
|
|
234
|
+
const minSize = Math.min(...sizes);
|
|
235
|
+
const maxSize = Math.max(...sizes);
|
|
236
|
+
sizeLine = minSize === maxSize ? formatSize(minSize) : `${formatSize(minSize)} - ${formatSize(maxSize)}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let md = `# ${baseRef}\n\n`;
|
|
240
|
+
if (description) md += `${description}\n\n`;
|
|
241
|
+
|
|
242
|
+
md += `**Model:** ${baseRef}\n`;
|
|
243
|
+
if (tagRef) md += `**Tag:** ${tagRef}\n`;
|
|
244
|
+
if (parameterSizes.length > 0) md += `**Parameters:** ${parameterSizes.join(", ")}\n`;
|
|
245
|
+
if (sizeLine) {
|
|
246
|
+
const label = sizeLine.includes(" - ") ? "Size Range" : "Size";
|
|
247
|
+
md += `**${label}:** ${sizeLine}\n`;
|
|
248
|
+
}
|
|
249
|
+
if (tagsToUse.length > 0) {
|
|
250
|
+
md += `**Available Tags:** ${formatTagList(tagsToUse, 40)}\n`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const output = finalizeOutput(md);
|
|
254
|
+
return {
|
|
255
|
+
url,
|
|
256
|
+
finalUrl: pageResult.ok ? pageResult.finalUrl : url,
|
|
257
|
+
contentType: "text/markdown",
|
|
258
|
+
method: "ollama",
|
|
259
|
+
content: output.content,
|
|
260
|
+
fetchedAt,
|
|
261
|
+
truncated: output.truncated,
|
|
262
|
+
notes: ["Fetched via Ollama API"],
|
|
263
|
+
};
|
|
264
|
+
} catch {}
|
|
265
|
+
|
|
266
|
+
return null;
|
|
267
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface OpenVsxFileLinks {
|
|
5
|
+
readme?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface OpenVsxExtension {
|
|
9
|
+
name: string;
|
|
10
|
+
namespace: string;
|
|
11
|
+
version: string;
|
|
12
|
+
displayName?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
downloadCount?: number;
|
|
15
|
+
averageRating?: number;
|
|
16
|
+
reviewCount?: number;
|
|
17
|
+
repository?: string | { url?: string };
|
|
18
|
+
license?: string;
|
|
19
|
+
categories?: string[];
|
|
20
|
+
homepage?: string;
|
|
21
|
+
files?: OpenVsxFileLinks;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handle Open VSX URLs via their API
|
|
26
|
+
*/
|
|
27
|
+
export const handleOpenVsx: SpecialHandler = async (
|
|
28
|
+
url: string,
|
|
29
|
+
timeout: number,
|
|
30
|
+
signal?: AbortSignal,
|
|
31
|
+
): Promise<RenderResult | null> => {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = new URL(url);
|
|
34
|
+
if (parsed.hostname !== "open-vsx.org" && parsed.hostname !== "www.open-vsx.org") return null;
|
|
35
|
+
|
|
36
|
+
const match = parsed.pathname.match(/^\/extension\/([^/]+)\/([^/]+)(?:\/([^/]+))?\/?$/);
|
|
37
|
+
if (!match) return null;
|
|
38
|
+
|
|
39
|
+
const namespace = decodeURIComponent(match[1]);
|
|
40
|
+
const extension = decodeURIComponent(match[2]);
|
|
41
|
+
const version = match[3] ? decodeURIComponent(match[3]) : null;
|
|
42
|
+
|
|
43
|
+
const fetchedAt = new Date().toISOString();
|
|
44
|
+
const baseUrl = `https://open-vsx.org/api/${encodeURIComponent(namespace)}/${encodeURIComponent(extension)}`;
|
|
45
|
+
const apiUrl = version ? `${baseUrl}/${encodeURIComponent(version)}` : baseUrl;
|
|
46
|
+
|
|
47
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
48
|
+
if (!result.ok) return null;
|
|
49
|
+
|
|
50
|
+
let data: OpenVsxExtension;
|
|
51
|
+
try {
|
|
52
|
+
data = JSON.parse(result.content);
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let readme: string | null = null;
|
|
58
|
+
const readmeUrl = data.files?.readme;
|
|
59
|
+
if (readmeUrl) {
|
|
60
|
+
try {
|
|
61
|
+
const readmeResult = await loadPage(readmeUrl, { timeout: Math.min(timeout, 10), signal });
|
|
62
|
+
if (readmeResult.ok) readme = readmeResult.content;
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const displayName = data.displayName || data.name || `${namespace}/${extension}`;
|
|
67
|
+
const displayNamespace = data.namespace || namespace;
|
|
68
|
+
const displayVersion = data.version || version || "unknown";
|
|
69
|
+
const downloads = typeof data.downloadCount === "number" ? data.downloadCount : null;
|
|
70
|
+
const rating = typeof data.averageRating === "number" ? data.averageRating : null;
|
|
71
|
+
const reviews = typeof data.reviewCount === "number" ? data.reviewCount : null;
|
|
72
|
+
const repository = typeof data.repository === "string" ? data.repository : data.repository?.url || null;
|
|
73
|
+
|
|
74
|
+
let md = `# ${displayName}\n\n`;
|
|
75
|
+
if (data.description) md += `${data.description}\n\n`;
|
|
76
|
+
|
|
77
|
+
md += `**Namespace:** ${displayNamespace}\n`;
|
|
78
|
+
md += `**Extension:** ${data.name || extension}\n`;
|
|
79
|
+
md += `**Version:** ${displayVersion}`;
|
|
80
|
+
if (data.license) md += ` | **License:** ${data.license}`;
|
|
81
|
+
md += "\n";
|
|
82
|
+
|
|
83
|
+
if (downloads !== null) {
|
|
84
|
+
md += `**Downloads:** ${formatCount(downloads)}\n`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (rating !== null) {
|
|
88
|
+
const reviewSuffix = reviews !== null ? ` (${reviews} reviews)` : "";
|
|
89
|
+
md += `**Rating:** ${rating}${reviewSuffix}\n`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (repository) {
|
|
93
|
+
const cleanedRepo = repository.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
94
|
+
md += `**Repository:** ${cleanedRepo}\n`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (data.homepage) md += `**Homepage:** ${data.homepage}\n`;
|
|
98
|
+
if (data.categories?.length) md += `**Categories:** ${data.categories.join(", ")}\n`;
|
|
99
|
+
|
|
100
|
+
if (readme) {
|
|
101
|
+
md += "\n---\n\n## README\n\n";
|
|
102
|
+
md += `${readme}\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const output = finalizeOutput(md);
|
|
106
|
+
return {
|
|
107
|
+
url,
|
|
108
|
+
finalUrl: url,
|
|
109
|
+
contentType: "text/markdown",
|
|
110
|
+
method: "open-vsx",
|
|
111
|
+
content: output.content,
|
|
112
|
+
fetchedAt,
|
|
113
|
+
truncated: output.truncated,
|
|
114
|
+
notes: ["Fetched via Open VSX API"],
|
|
115
|
+
};
|
|
116
|
+
} catch {}
|
|
117
|
+
|
|
118
|
+
return null;
|
|
119
|
+
};
|
|
@@ -81,6 +81,7 @@ interface ApiResponse {
|
|
|
81
81
|
export const handleOpenCorporates: SpecialHandler = async (
|
|
82
82
|
url: string,
|
|
83
83
|
timeout: number,
|
|
84
|
+
signal?: AbortSignal,
|
|
84
85
|
): Promise<RenderResult | null> => {
|
|
85
86
|
try {
|
|
86
87
|
const parsed = new URL(url);
|
|
@@ -100,6 +101,7 @@ export const handleOpenCorporates: SpecialHandler = async (
|
|
|
100
101
|
const result = await loadPage(apiUrl, {
|
|
101
102
|
timeout,
|
|
102
103
|
headers: { Accept: "application/json" },
|
|
104
|
+
signal,
|
|
103
105
|
});
|
|
104
106
|
|
|
105
107
|
if (!result.ok) return null;
|
|
@@ -67,7 +67,11 @@ interface OpenLibraryBooksApiResponse {
|
|
|
67
67
|
/**
|
|
68
68
|
* Handle Open Library URLs via their API
|
|
69
69
|
*/
|
|
70
|
-
export const handleOpenLibrary: SpecialHandler = async (
|
|
70
|
+
export const handleOpenLibrary: SpecialHandler = async (
|
|
71
|
+
url: string,
|
|
72
|
+
timeout: number,
|
|
73
|
+
signal?: AbortSignal,
|
|
74
|
+
): Promise<RenderResult | null> => {
|
|
71
75
|
try {
|
|
72
76
|
const parsed = new URL(url);
|
|
73
77
|
if (!parsed.hostname.includes("openlibrary.org")) return null;
|
|
@@ -83,11 +87,11 @@ export const handleOpenLibrary: SpecialHandler = async (url: string, timeout: nu
|
|
|
83
87
|
let md: string | null = null;
|
|
84
88
|
|
|
85
89
|
if (workMatch) {
|
|
86
|
-
md = await fetchWork(workMatch[1], timeout);
|
|
90
|
+
md = await fetchWork(workMatch[1], timeout, signal);
|
|
87
91
|
} else if (editionMatch) {
|
|
88
|
-
md = await fetchEdition(editionMatch[1], timeout);
|
|
92
|
+
md = await fetchEdition(editionMatch[1], timeout, signal);
|
|
89
93
|
} else if (isbnMatch) {
|
|
90
|
-
md = await fetchByIsbn(isbnMatch[1], timeout);
|
|
94
|
+
md = await fetchByIsbn(isbnMatch[1], timeout, signal);
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
if (!md) return null;
|
|
@@ -108,9 +112,9 @@ export const handleOpenLibrary: SpecialHandler = async (url: string, timeout: nu
|
|
|
108
112
|
return null;
|
|
109
113
|
};
|
|
110
114
|
|
|
111
|
-
async function fetchWork(workId: string, timeout: number): Promise<string | null> {
|
|
115
|
+
async function fetchWork(workId: string, timeout: number, signal?: AbortSignal): Promise<string | null> {
|
|
112
116
|
const apiUrl = `https://openlibrary.org/works/${workId}.json`;
|
|
113
|
-
const result = await loadPage(apiUrl, { timeout });
|
|
117
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
114
118
|
if (!result.ok) return null;
|
|
115
119
|
|
|
116
120
|
let work: OpenLibraryWork;
|
|
@@ -127,6 +131,7 @@ async function fetchWork(workId: string, timeout: number): Promise<string | null
|
|
|
127
131
|
const authorNames = await fetchAuthorNames(
|
|
128
132
|
work.authors.map((a) => a.author.key),
|
|
129
133
|
timeout,
|
|
134
|
+
signal,
|
|
130
135
|
);
|
|
131
136
|
if (authorNames.length) {
|
|
132
137
|
md += `**Authors:** ${authorNames.join(", ")}\n`;
|
|
@@ -157,9 +162,9 @@ async function fetchWork(workId: string, timeout: number): Promise<string | null
|
|
|
157
162
|
return md;
|
|
158
163
|
}
|
|
159
164
|
|
|
160
|
-
async function fetchEdition(editionId: string, timeout: number): Promise<string | null> {
|
|
165
|
+
async function fetchEdition(editionId: string, timeout: number, signal?: AbortSignal): Promise<string | null> {
|
|
161
166
|
const apiUrl = `https://openlibrary.org/books/${editionId}.json`;
|
|
162
|
-
const result = await loadPage(apiUrl, { timeout });
|
|
167
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
163
168
|
if (!result.ok) return null;
|
|
164
169
|
|
|
165
170
|
let edition: OpenLibraryEdition;
|
|
@@ -176,6 +181,7 @@ async function fetchEdition(editionId: string, timeout: number): Promise<string
|
|
|
176
181
|
const authorNames = await fetchAuthorNames(
|
|
177
182
|
edition.authors.map((a) => a.key),
|
|
178
183
|
timeout,
|
|
184
|
+
signal,
|
|
179
185
|
);
|
|
180
186
|
if (authorNames.length) {
|
|
181
187
|
md += `**Authors:** ${authorNames.join(", ")}\n`;
|
|
@@ -225,9 +231,9 @@ async function fetchEdition(editionId: string, timeout: number): Promise<string
|
|
|
225
231
|
return md;
|
|
226
232
|
}
|
|
227
233
|
|
|
228
|
-
async function fetchByIsbn(isbn: string, timeout: number): Promise<string | null> {
|
|
234
|
+
async function fetchByIsbn(isbn: string, timeout: number, signal?: AbortSignal): Promise<string | null> {
|
|
229
235
|
const apiUrl = `https://openlibrary.org/api/books?bibkeys=ISBN:${isbn}&format=json&jscmd=data`;
|
|
230
|
-
const result = await loadPage(apiUrl, { timeout });
|
|
236
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
231
237
|
if (!result.ok) return null;
|
|
232
238
|
|
|
233
239
|
let data: OpenLibraryBooksApiResponse;
|
|
@@ -281,7 +287,7 @@ async function fetchByIsbn(isbn: string, timeout: number): Promise<string | null
|
|
|
281
287
|
return md;
|
|
282
288
|
}
|
|
283
289
|
|
|
284
|
-
async function fetchAuthorNames(authorKeys: string[], timeout: number): Promise<string[]> {
|
|
290
|
+
async function fetchAuthorNames(authorKeys: string[], timeout: number, signal?: AbortSignal): Promise<string[]> {
|
|
285
291
|
const names: string[] = [];
|
|
286
292
|
|
|
287
293
|
// Fetch authors in parallel (limit to first 5)
|
|
@@ -289,7 +295,7 @@ async function fetchAuthorNames(authorKeys: string[], timeout: number): Promise<
|
|
|
289
295
|
const authorKey = key.startsWith("/authors/") ? key : `/authors/${key}`;
|
|
290
296
|
const apiUrl = `https://openlibrary.org${authorKey}.json`;
|
|
291
297
|
try {
|
|
292
|
-
const result = await loadPage(apiUrl, { timeout: Math.min(timeout, 5) });
|
|
298
|
+
const result = await loadPage(apiUrl, { timeout: Math.min(timeout, 5), signal });
|
|
293
299
|
if (result.ok) {
|
|
294
300
|
const author = JSON.parse(result.content) as { name?: string };
|
|
295
301
|
return author.name || null;
|