@oh-my-pi/pi-coding-agent 3.24.0 → 3.30.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/package.json +4 -4
  3. package/src/core/custom-commands/bundled/wt/index.ts +3 -0
  4. package/src/core/sdk.ts +7 -0
  5. package/src/core/tools/complete.ts +129 -0
  6. package/src/core/tools/index.test.ts +9 -1
  7. package/src/core/tools/index.ts +18 -5
  8. package/src/core/tools/jtd-to-json-schema.ts +252 -0
  9. package/src/core/tools/output.ts +125 -14
  10. package/src/core/tools/read.ts +4 -4
  11. package/src/core/tools/task/artifacts.ts +6 -9
  12. package/src/core/tools/task/executor.ts +189 -24
  13. package/src/core/tools/task/index.ts +23 -18
  14. package/src/core/tools/task/name-generator.ts +1577 -0
  15. package/src/core/tools/task/render.ts +137 -8
  16. package/src/core/tools/task/types.ts +26 -5
  17. package/src/core/tools/task/worker-protocol.ts +1 -0
  18. package/src/core/tools/task/worker.ts +136 -14
  19. package/src/core/tools/web-fetch-handlers/academic.test.ts +239 -0
  20. package/src/core/tools/web-fetch-handlers/artifacthub.ts +210 -0
  21. package/src/core/tools/web-fetch-handlers/arxiv.ts +84 -0
  22. package/src/core/tools/web-fetch-handlers/aur.ts +171 -0
  23. package/src/core/tools/web-fetch-handlers/biorxiv.ts +136 -0
  24. package/src/core/tools/web-fetch-handlers/bluesky.ts +277 -0
  25. package/src/core/tools/web-fetch-handlers/brew.ts +173 -0
  26. package/src/core/tools/web-fetch-handlers/business.test.ts +82 -0
  27. package/src/core/tools/web-fetch-handlers/cheatsh.ts +73 -0
  28. package/src/core/tools/web-fetch-handlers/chocolatey.ts +153 -0
  29. package/src/core/tools/web-fetch-handlers/coingecko.ts +179 -0
  30. package/src/core/tools/web-fetch-handlers/crates-io.ts +123 -0
  31. package/src/core/tools/web-fetch-handlers/dev-platforms.test.ts +254 -0
  32. package/src/core/tools/web-fetch-handlers/devto.ts +173 -0
  33. package/src/core/tools/web-fetch-handlers/discogs.ts +303 -0
  34. package/src/core/tools/web-fetch-handlers/dockerhub.ts +156 -0
  35. package/src/core/tools/web-fetch-handlers/documentation.test.ts +85 -0
  36. package/src/core/tools/web-fetch-handlers/finance-media.test.ts +144 -0
  37. package/src/core/tools/web-fetch-handlers/git-hosting.test.ts +272 -0
  38. package/src/core/tools/web-fetch-handlers/github-gist.ts +64 -0
  39. package/src/core/tools/web-fetch-handlers/github.ts +424 -0
  40. package/src/core/tools/web-fetch-handlers/gitlab.ts +444 -0
  41. package/src/core/tools/web-fetch-handlers/go-pkg.ts +271 -0
  42. package/src/core/tools/web-fetch-handlers/hackage.ts +89 -0
  43. package/src/core/tools/web-fetch-handlers/hackernews.ts +208 -0
  44. package/src/core/tools/web-fetch-handlers/hex.ts +121 -0
  45. package/src/core/tools/web-fetch-handlers/huggingface.ts +385 -0
  46. package/src/core/tools/web-fetch-handlers/iacr.ts +82 -0
  47. package/src/core/tools/web-fetch-handlers/index.ts +69 -0
  48. package/src/core/tools/web-fetch-handlers/lobsters.ts +186 -0
  49. package/src/core/tools/web-fetch-handlers/mastodon.ts +302 -0
  50. package/src/core/tools/web-fetch-handlers/maven.ts +147 -0
  51. package/src/core/tools/web-fetch-handlers/mdn.ts +174 -0
  52. package/src/core/tools/web-fetch-handlers/media.test.ts +138 -0
  53. package/src/core/tools/web-fetch-handlers/metacpan.ts +247 -0
  54. package/src/core/tools/web-fetch-handlers/npm.ts +107 -0
  55. package/src/core/tools/web-fetch-handlers/nuget.ts +201 -0
  56. package/src/core/tools/web-fetch-handlers/nvd.ts +238 -0
  57. package/src/core/tools/web-fetch-handlers/opencorporates.ts +273 -0
  58. package/src/core/tools/web-fetch-handlers/openlibrary.ts +313 -0
  59. package/src/core/tools/web-fetch-handlers/osv.ts +184 -0
  60. package/src/core/tools/web-fetch-handlers/package-managers-2.test.ts +199 -0
  61. package/src/core/tools/web-fetch-handlers/package-managers.test.ts +171 -0
  62. package/src/core/tools/web-fetch-handlers/package-registries.test.ts +259 -0
  63. package/src/core/tools/web-fetch-handlers/packagist.ts +170 -0
  64. package/src/core/tools/web-fetch-handlers/pub-dev.ts +185 -0
  65. package/src/core/tools/web-fetch-handlers/pubmed.ts +174 -0
  66. package/src/core/tools/web-fetch-handlers/pypi.ts +125 -0
  67. package/src/core/tools/web-fetch-handlers/readthedocs.ts +122 -0
  68. package/src/core/tools/web-fetch-handlers/reddit.ts +100 -0
  69. package/src/core/tools/web-fetch-handlers/repology.ts +257 -0
  70. package/src/core/tools/web-fetch-handlers/research.test.ts +107 -0
  71. package/src/core/tools/web-fetch-handlers/rfc.ts +205 -0
  72. package/src/core/tools/web-fetch-handlers/rubygems.ts +112 -0
  73. package/src/core/tools/web-fetch-handlers/sec-edgar.ts +269 -0
  74. package/src/core/tools/web-fetch-handlers/security.test.ts +103 -0
  75. package/src/core/tools/web-fetch-handlers/semantic-scholar.ts +190 -0
  76. package/src/core/tools/web-fetch-handlers/social-extended.test.ts +192 -0
  77. package/src/core/tools/web-fetch-handlers/social.test.ts +259 -0
  78. package/src/core/tools/web-fetch-handlers/spotify.ts +218 -0
  79. package/src/core/tools/web-fetch-handlers/stackexchange.test.ts +120 -0
  80. package/src/core/tools/web-fetch-handlers/stackoverflow.ts +123 -0
  81. package/src/core/tools/web-fetch-handlers/standards.test.ts +122 -0
  82. package/src/core/tools/web-fetch-handlers/terraform.ts +296 -0
  83. package/src/core/tools/web-fetch-handlers/tldr.ts +47 -0
  84. package/src/core/tools/web-fetch-handlers/twitter.ts +84 -0
  85. package/src/core/tools/web-fetch-handlers/types.ts +163 -0
  86. package/src/core/tools/web-fetch-handlers/utils.ts +91 -0
  87. package/src/core/tools/web-fetch-handlers/vimeo.ts +152 -0
  88. package/src/core/tools/web-fetch-handlers/wikidata.ts +349 -0
  89. package/src/core/tools/web-fetch-handlers/wikipedia.test.ts +73 -0
  90. package/src/core/tools/web-fetch-handlers/wikipedia.ts +91 -0
  91. package/src/core/tools/web-fetch-handlers/youtube.test.ts +198 -0
  92. package/src/core/tools/web-fetch-handlers/youtube.ts +319 -0
  93. package/src/core/tools/web-fetch.ts +152 -1324
  94. package/src/prompts/task.md +14 -50
  95. package/src/prompts/tools/output.md +2 -1
  96. package/src/prompts/tools/task.md +3 -1
  97. package/src/utils/tools-manager.ts +110 -8
@@ -0,0 +1,313 @@
1
+ import type { RenderResult, SpecialHandler } from "./types";
2
+ import { finalizeOutput, loadPage } from "./types";
3
+
4
+ interface OpenLibraryAuthor {
5
+ name?: string;
6
+ url?: string;
7
+ }
8
+
9
+ interface OpenLibrarySubject {
10
+ name: string;
11
+ url?: string;
12
+ }
13
+
14
+ interface OpenLibraryPublisher {
15
+ name: string;
16
+ }
17
+
18
+ interface OpenLibraryCover {
19
+ small?: string;
20
+ medium?: string;
21
+ large?: string;
22
+ }
23
+
24
+ interface OpenLibraryWork {
25
+ title: string;
26
+ authors?: Array<{ author: { key: string } }>;
27
+ description?: string | { value: string };
28
+ subjects?: string[];
29
+ subject_places?: string[];
30
+ subject_times?: string[];
31
+ covers?: number[];
32
+ first_publish_date?: string;
33
+ }
34
+
35
+ interface OpenLibraryEdition {
36
+ title: string;
37
+ authors?: Array<{ key: string }>;
38
+ publishers?: string[];
39
+ publish_date?: string;
40
+ number_of_pages?: number;
41
+ isbn_10?: string[];
42
+ isbn_13?: string[];
43
+ covers?: number[];
44
+ description?: string | { value: string };
45
+ subjects?: string[];
46
+ works?: Array<{ key: string }>;
47
+ }
48
+
49
+ interface OpenLibraryBooksApiResponse {
50
+ [key: string]: {
51
+ title: string;
52
+ authors?: OpenLibraryAuthor[];
53
+ publishers?: OpenLibraryPublisher[];
54
+ publish_date?: string;
55
+ number_of_pages?: number;
56
+ subjects?: OpenLibrarySubject[];
57
+ cover?: OpenLibraryCover;
58
+ url?: string;
59
+ identifiers?: {
60
+ isbn_10?: string[];
61
+ isbn_13?: string[];
62
+ openlibrary?: string[];
63
+ };
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Handle Open Library URLs via their API
69
+ */
70
+ export const handleOpenLibrary: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
71
+ try {
72
+ const parsed = new URL(url);
73
+ if (!parsed.hostname.includes("openlibrary.org")) return null;
74
+
75
+ const fetchedAt = new Date().toISOString();
76
+ const path = parsed.pathname;
77
+
78
+ // Match URL patterns
79
+ const workMatch = path.match(/^\/works\/(OL\d+W)/i);
80
+ const editionMatch = path.match(/^\/books\/(OL\d+M)/i);
81
+ const isbnMatch = path.match(/^\/isbn\/(\d{10}|\d{13})/i);
82
+
83
+ let md: string | null = null;
84
+
85
+ if (workMatch) {
86
+ md = await fetchWork(workMatch[1], timeout);
87
+ } else if (editionMatch) {
88
+ md = await fetchEdition(editionMatch[1], timeout);
89
+ } else if (isbnMatch) {
90
+ md = await fetchByIsbn(isbnMatch[1], timeout);
91
+ }
92
+
93
+ if (!md) return null;
94
+
95
+ const output = finalizeOutput(md);
96
+ return {
97
+ url,
98
+ finalUrl: url,
99
+ contentType: "text/markdown",
100
+ method: "openlibrary",
101
+ content: output.content,
102
+ fetchedAt,
103
+ truncated: output.truncated,
104
+ notes: ["Fetched via Open Library API"],
105
+ };
106
+ } catch {}
107
+
108
+ return null;
109
+ };
110
+
111
+ async function fetchWork(workId: string, timeout: number): Promise<string | null> {
112
+ const apiUrl = `https://openlibrary.org/works/${workId}.json`;
113
+ const result = await loadPage(apiUrl, { timeout });
114
+ if (!result.ok) return null;
115
+
116
+ let work: OpenLibraryWork;
117
+ try {
118
+ work = JSON.parse(result.content);
119
+ } catch {
120
+ return null;
121
+ }
122
+
123
+ let md = `# ${work.title}\n\n`;
124
+
125
+ // Fetch author names if we have author keys
126
+ if (work.authors?.length) {
127
+ const authorNames = await fetchAuthorNames(
128
+ work.authors.map((a) => a.author.key),
129
+ timeout,
130
+ );
131
+ if (authorNames.length) {
132
+ md += `**Authors:** ${authorNames.join(", ")}\n`;
133
+ }
134
+ }
135
+
136
+ if (work.first_publish_date) {
137
+ md += `**First Published:** ${work.first_publish_date}\n`;
138
+ }
139
+
140
+ if (work.covers?.length) {
141
+ const coverId = work.covers[0];
142
+ md += `**Cover:** https://covers.openlibrary.org/b/id/${coverId}-L.jpg\n`;
143
+ }
144
+
145
+ md += `**Open Library:** https://openlibrary.org/works/${workId}\n`;
146
+ md += "\n";
147
+
148
+ const description = extractDescription(work.description);
149
+ if (description) {
150
+ md += `## Description\n\n${description}\n\n`;
151
+ }
152
+
153
+ if (work.subjects?.length) {
154
+ md += `## Subjects\n\n${work.subjects.slice(0, 20).join(", ")}\n`;
155
+ }
156
+
157
+ return md;
158
+ }
159
+
160
+ async function fetchEdition(editionId: string, timeout: number): Promise<string | null> {
161
+ const apiUrl = `https://openlibrary.org/books/${editionId}.json`;
162
+ const result = await loadPage(apiUrl, { timeout });
163
+ if (!result.ok) return null;
164
+
165
+ let edition: OpenLibraryEdition;
166
+ try {
167
+ edition = JSON.parse(result.content);
168
+ } catch {
169
+ return null;
170
+ }
171
+
172
+ let md = `# ${edition.title}\n\n`;
173
+
174
+ // Fetch author names
175
+ if (edition.authors?.length) {
176
+ const authorNames = await fetchAuthorNames(
177
+ edition.authors.map((a) => a.key),
178
+ timeout,
179
+ );
180
+ if (authorNames.length) {
181
+ md += `**Authors:** ${authorNames.join(", ")}\n`;
182
+ }
183
+ }
184
+
185
+ if (edition.publishers?.length) {
186
+ md += `**Publishers:** ${edition.publishers.join(", ")}\n`;
187
+ }
188
+
189
+ if (edition.publish_date) {
190
+ md += `**Published:** ${edition.publish_date}\n`;
191
+ }
192
+
193
+ if (edition.number_of_pages) {
194
+ md += `**Pages:** ${edition.number_of_pages}\n`;
195
+ }
196
+
197
+ const isbns = [...(edition.isbn_13 || []), ...(edition.isbn_10 || [])];
198
+ if (isbns.length) {
199
+ md += `**ISBN:** ${isbns[0]}\n`;
200
+ }
201
+
202
+ if (edition.covers?.length) {
203
+ const coverId = edition.covers[0];
204
+ md += `**Cover:** https://covers.openlibrary.org/b/id/${coverId}-L.jpg\n`;
205
+ }
206
+
207
+ md += `**Open Library:** https://openlibrary.org/books/${editionId}\n`;
208
+
209
+ if (edition.works?.length) {
210
+ const workKey = edition.works[0].key.replace("/works/", "");
211
+ md += `**Work:** https://openlibrary.org/works/${workKey}\n`;
212
+ }
213
+
214
+ md += "\n";
215
+
216
+ const description = extractDescription(edition.description);
217
+ if (description) {
218
+ md += `## Description\n\n${description}\n\n`;
219
+ }
220
+
221
+ if (edition.subjects?.length) {
222
+ md += `## Subjects\n\n${edition.subjects.slice(0, 20).join(", ")}\n`;
223
+ }
224
+
225
+ return md;
226
+ }
227
+
228
+ async function fetchByIsbn(isbn: string, timeout: number): Promise<string | null> {
229
+ const apiUrl = `https://openlibrary.org/api/books?bibkeys=ISBN:${isbn}&format=json&jscmd=data`;
230
+ const result = await loadPage(apiUrl, { timeout });
231
+ if (!result.ok) return null;
232
+
233
+ let data: OpenLibraryBooksApiResponse;
234
+ try {
235
+ data = JSON.parse(result.content);
236
+ } catch {
237
+ return null;
238
+ }
239
+
240
+ const key = `ISBN:${isbn}`;
241
+ const book = data[key];
242
+ if (!book) return null;
243
+
244
+ let md = `# ${book.title}\n\n`;
245
+
246
+ if (book.authors?.length) {
247
+ md += `**Authors:** ${book.authors.map((a) => a.name).join(", ")}\n`;
248
+ }
249
+
250
+ if (book.publishers?.length) {
251
+ md += `**Publishers:** ${book.publishers.map((p) => p.name).join(", ")}\n`;
252
+ }
253
+
254
+ if (book.publish_date) {
255
+ md += `**Published:** ${book.publish_date}\n`;
256
+ }
257
+
258
+ if (book.number_of_pages) {
259
+ md += `**Pages:** ${book.number_of_pages}\n`;
260
+ }
261
+
262
+ md += `**ISBN:** ${isbn}\n`;
263
+
264
+ if (book.cover?.large || book.cover?.medium) {
265
+ md += `**Cover:** ${book.cover.large || book.cover.medium}\n`;
266
+ }
267
+
268
+ if (book.url) {
269
+ md += `**Open Library:** ${book.url}\n`;
270
+ }
271
+
272
+ md += "\n";
273
+
274
+ if (book.subjects?.length) {
275
+ md += `## Subjects\n\n${book.subjects
276
+ .slice(0, 20)
277
+ .map((s) => s.name)
278
+ .join(", ")}\n`;
279
+ }
280
+
281
+ return md;
282
+ }
283
+
284
+ async function fetchAuthorNames(authorKeys: string[], timeout: number): Promise<string[]> {
285
+ const names: string[] = [];
286
+
287
+ // Fetch authors in parallel (limit to first 5)
288
+ const promises = authorKeys.slice(0, 5).map(async (key) => {
289
+ const authorKey = key.startsWith("/authors/") ? key : `/authors/${key}`;
290
+ const apiUrl = `https://openlibrary.org${authorKey}.json`;
291
+ try {
292
+ const result = await loadPage(apiUrl, { timeout: Math.min(timeout, 5) });
293
+ if (result.ok) {
294
+ const author = JSON.parse(result.content) as { name?: string };
295
+ return author.name || null;
296
+ }
297
+ } catch {}
298
+ return null;
299
+ });
300
+
301
+ const results = await Promise.all(promises);
302
+ for (const name of results) {
303
+ if (name) names.push(name);
304
+ }
305
+
306
+ return names;
307
+ }
308
+
309
+ function extractDescription(desc: string | { value: string } | undefined): string | null {
310
+ if (!desc) return null;
311
+ if (typeof desc === "string") return desc;
312
+ return desc.value || null;
313
+ }
@@ -0,0 +1,184 @@
1
+ import type { RenderResult, SpecialHandler } from "./types";
2
+ import { finalizeOutput, loadPage } from "./types";
3
+
4
+ interface OsvSeverity {
5
+ type: string;
6
+ score: string;
7
+ }
8
+
9
+ interface OsvAffectedRange {
10
+ type: string;
11
+ events?: Array<{ introduced?: string; fixed?: string; last_affected?: string; limit?: string }>;
12
+ }
13
+
14
+ interface OsvAffected {
15
+ package?: {
16
+ ecosystem: string;
17
+ name: string;
18
+ purl?: string;
19
+ };
20
+ ranges?: OsvAffectedRange[];
21
+ versions?: string[];
22
+ severity?: OsvSeverity[];
23
+ database_specific?: Record<string, unknown>;
24
+ ecosystem_specific?: Record<string, unknown>;
25
+ }
26
+
27
+ interface OsvReference {
28
+ type: string;
29
+ url: string;
30
+ }
31
+
32
+ interface OsvVulnerability {
33
+ id: string;
34
+ summary?: string;
35
+ details?: string;
36
+ aliases?: string[];
37
+ modified?: string;
38
+ published?: string;
39
+ withdrawn?: string;
40
+ severity?: OsvSeverity[];
41
+ affected?: OsvAffected[];
42
+ references?: OsvReference[];
43
+ credits?: Array<{ name: string; contact?: string[]; type?: string }>;
44
+ database_specific?: Record<string, unknown>;
45
+ }
46
+
47
+ /**
48
+ * Handle OSV (Open Source Vulnerabilities) URLs
49
+ */
50
+ export const handleOsv: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
51
+ try {
52
+ const parsed = new URL(url);
53
+ if (parsed.hostname !== "osv.dev") return null;
54
+
55
+ // Extract vulnerability ID from /vulnerability/{id}
56
+ const match = parsed.pathname.match(/^\/vulnerability\/([A-Za-z0-9-]+)$/);
57
+ if (!match) return null;
58
+
59
+ const vulnId = match[1];
60
+ const fetchedAt = new Date().toISOString();
61
+
62
+ // Fetch from OSV API
63
+ const apiUrl = `https://api.osv.dev/v1/vulns/${vulnId}`;
64
+ const result = await loadPage(apiUrl, {
65
+ timeout,
66
+ headers: { Accept: "application/json" },
67
+ });
68
+
69
+ if (!result.ok) return null;
70
+
71
+ let vuln: OsvVulnerability;
72
+ try {
73
+ vuln = JSON.parse(result.content);
74
+ } catch {
75
+ return null;
76
+ }
77
+
78
+ let md = `# ${vuln.id}\n\n`;
79
+
80
+ // Summary
81
+ if (vuln.summary) {
82
+ md += `${vuln.summary}\n\n`;
83
+ }
84
+
85
+ // Metadata section
86
+ md += "## Metadata\n\n";
87
+ if (vuln.aliases?.length) {
88
+ md += `**Aliases:** ${vuln.aliases.join(", ")}\n`;
89
+ }
90
+ if (vuln.published) {
91
+ md += `**Published:** ${vuln.published.split("T")[0]}\n`;
92
+ }
93
+ if (vuln.modified) {
94
+ md += `**Modified:** ${vuln.modified.split("T")[0]}\n`;
95
+ }
96
+ if (vuln.withdrawn) {
97
+ md += `**Withdrawn:** ${vuln.withdrawn.split("T")[0]}\n`;
98
+ }
99
+
100
+ // Severity
101
+ const severities = vuln.severity || vuln.affected?.flatMap((a) => a.severity || []) || [];
102
+ if (severities.length) {
103
+ const formatted = severities.map((s) => `${s.type}: ${s.score}`).join(", ");
104
+ md += `**Severity:** ${formatted}\n`;
105
+ }
106
+ md += "\n";
107
+
108
+ // Details
109
+ if (vuln.details) {
110
+ md += `## Details\n\n${vuln.details}\n\n`;
111
+ }
112
+
113
+ // Affected packages
114
+ if (vuln.affected?.length) {
115
+ md += "## Affected Packages\n\n";
116
+ for (const affected of vuln.affected) {
117
+ const pkg = affected.package;
118
+ if (!pkg) continue;
119
+
120
+ md += `### ${pkg.ecosystem}: ${pkg.name}\n\n`;
121
+
122
+ // Version ranges
123
+ if (affected.ranges?.length) {
124
+ for (const range of affected.ranges) {
125
+ if (!range.events?.length) continue;
126
+ const parts: string[] = [];
127
+ for (const event of range.events) {
128
+ if (event.introduced) parts.push(`introduced: ${event.introduced}`);
129
+ if (event.fixed) parts.push(`fixed: ${event.fixed}`);
130
+ if (event.last_affected) parts.push(`last_affected: ${event.last_affected}`);
131
+ if (event.limit) parts.push(`limit: ${event.limit}`);
132
+ }
133
+ if (parts.length) {
134
+ md += `- **${range.type}:** ${parts.join(" → ")}\n`;
135
+ }
136
+ }
137
+ }
138
+
139
+ // Specific versions
140
+ if (affected.versions?.length) {
141
+ const versions =
142
+ affected.versions.length > 10
143
+ ? `${affected.versions.slice(0, 10).join(", ")}... (${affected.versions.length} total)`
144
+ : affected.versions.join(", ");
145
+ md += `- **Versions:** ${versions}\n`;
146
+ }
147
+
148
+ md += "\n";
149
+ }
150
+ }
151
+
152
+ // References
153
+ if (vuln.references?.length) {
154
+ md += "## References\n\n";
155
+ for (const ref of vuln.references) {
156
+ md += `- [${ref.type}](${ref.url})\n`;
157
+ }
158
+ md += "\n";
159
+ }
160
+
161
+ // Credits
162
+ if (vuln.credits?.length) {
163
+ md += "## Credits\n\n";
164
+ for (const credit of vuln.credits) {
165
+ const type = credit.type ? ` (${credit.type})` : "";
166
+ md += `- ${credit.name}${type}\n`;
167
+ }
168
+ }
169
+
170
+ const output = finalizeOutput(md);
171
+ return {
172
+ url,
173
+ finalUrl: url,
174
+ contentType: "text/markdown",
175
+ method: "osv",
176
+ content: output.content,
177
+ fetchedAt,
178
+ truncated: output.truncated,
179
+ notes: ["Fetched via OSV API"],
180
+ };
181
+ } catch {}
182
+
183
+ return null;
184
+ };
@@ -0,0 +1,199 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { handleChocolatey } from "./chocolatey";
3
+ import { handleDockerHub } from "./dockerhub";
4
+ import { handleHackage } from "./hackage";
5
+ import { handleMetaCPAN } from "./metacpan";
6
+ import { handleRepology } from "./repology";
7
+ import { handleTerraform } from "./terraform";
8
+
9
+ const SKIP = !process.env.WEB_FETCH_INTEGRATION;
10
+
11
+ describe.skipIf(SKIP)("handleMetaCPAN", () => {
12
+ it("returns null for non-MetaCPAN URLs", async () => {
13
+ const result = await handleMetaCPAN("https://example.com", 20);
14
+ expect(result).toBeNull();
15
+ });
16
+
17
+ it("returns null for non-matching MetaCPAN paths", async () => {
18
+ const result = await handleMetaCPAN("https://metacpan.org/about", 20);
19
+ expect(result).toBeNull();
20
+ });
21
+
22
+ it("fetches Moose module", async () => {
23
+ const result = await handleMetaCPAN("https://metacpan.org/pod/Moose", 20);
24
+ expect(result).not.toBeNull();
25
+ expect(result?.method).toBe("metacpan");
26
+ expect(result?.content).toContain("Moose");
27
+ expect(result?.contentType).toBe("text/markdown");
28
+ expect(result?.fetchedAt).toBeTruthy();
29
+ expect(result?.truncated).toBeDefined();
30
+ });
31
+
32
+ it("fetches release by distribution name", async () => {
33
+ const result = await handleMetaCPAN("https://metacpan.org/release/Moose", 20);
34
+ expect(result).not.toBeNull();
35
+ expect(result?.method).toBe("metacpan");
36
+ expect(result?.content).toContain("Moose");
37
+ });
38
+ });
39
+
40
+ describe.skipIf(SKIP)("handleHackage", () => {
41
+ it("returns null for non-Hackage URLs", async () => {
42
+ const result = await handleHackage("https://example.com", 20);
43
+ expect(result).toBeNull();
44
+ });
45
+
46
+ it("returns null for non-package Hackage paths", async () => {
47
+ const result = await handleHackage("https://hackage.haskell.org/", 20);
48
+ expect(result).toBeNull();
49
+ });
50
+
51
+ it("fetches aeson package", async () => {
52
+ const result = await handleHackage("https://hackage.haskell.org/package/aeson", 20);
53
+ expect(result).not.toBeNull();
54
+ expect(result?.method).toBe("hackage");
55
+ expect(result?.content).toContain("aeson");
56
+ expect(result?.content).toContain("JSON");
57
+ expect(result?.contentType).toBe("text/markdown");
58
+ expect(result?.fetchedAt).toBeTruthy();
59
+ expect(result?.truncated).toBeDefined();
60
+ });
61
+
62
+ it("fetches text package", async () => {
63
+ const result = await handleHackage("https://hackage.haskell.org/package/text", 20);
64
+ expect(result).not.toBeNull();
65
+ expect(result?.method).toBe("hackage");
66
+ expect(result?.content).toContain("text");
67
+ });
68
+ });
69
+
70
+ describe.skipIf(SKIP)("handleDockerHub", () => {
71
+ it("returns null for non-DockerHub URLs", async () => {
72
+ const result = await handleDockerHub("https://example.com", 20);
73
+ expect(result).toBeNull();
74
+ });
75
+
76
+ it("returns null for non-matching DockerHub paths", async () => {
77
+ const result = await handleDockerHub("https://hub.docker.com/search", 20);
78
+ expect(result).toBeNull();
79
+ });
80
+
81
+ it("fetches official nginx image", async () => {
82
+ const result = await handleDockerHub("https://hub.docker.com/_/nginx", 20);
83
+ expect(result).not.toBeNull();
84
+ expect(result?.method).toBe("dockerhub");
85
+ expect(result?.content).toContain("nginx");
86
+ expect(result?.content).toContain("docker pull");
87
+ expect(result?.contentType).toBe("text/markdown");
88
+ expect(result?.fetchedAt).toBeTruthy();
89
+ expect(result?.truncated).toBeDefined();
90
+ });
91
+
92
+ it("fetches grafana/grafana image", async () => {
93
+ const result = await handleDockerHub("https://hub.docker.com/r/grafana/grafana", 20);
94
+ expect(result).not.toBeNull();
95
+ expect(result?.method).toBe("dockerhub");
96
+ expect(result?.content).toContain("grafana");
97
+ expect(result?.content).toContain("docker pull");
98
+ });
99
+ });
100
+
101
+ describe.skipIf(SKIP)("handleChocolatey", () => {
102
+ it("returns null for non-Chocolatey URLs", async () => {
103
+ const result = await handleChocolatey("https://example.com", 20);
104
+ expect(result).toBeNull();
105
+ });
106
+
107
+ it("returns null for non-package Chocolatey paths", async () => {
108
+ const result = await handleChocolatey("https://community.chocolatey.org/", 20);
109
+ expect(result).toBeNull();
110
+ });
111
+
112
+ it("fetches git package", async () => {
113
+ const result = await handleChocolatey("https://community.chocolatey.org/packages/git", 20);
114
+ expect(result).not.toBeNull();
115
+ expect(result?.method).toBe("chocolatey");
116
+ expect(result?.content).toContain("Git");
117
+ expect(result?.content).toContain("choco install");
118
+ expect(result?.contentType).toBe("text/markdown");
119
+ expect(result?.fetchedAt).toBeTruthy();
120
+ expect(result?.truncated).toBeDefined();
121
+ });
122
+
123
+ it("fetches nodejs package", async () => {
124
+ const result = await handleChocolatey("https://community.chocolatey.org/packages/nodejs", 20);
125
+ expect(result).not.toBeNull();
126
+ expect(result?.method).toBe("chocolatey");
127
+ expect(result?.content).toContain("Node");
128
+ });
129
+ });
130
+
131
+ describe.skipIf(SKIP)("handleRepology", () => {
132
+ it("returns null for non-Repology URLs", async () => {
133
+ const result = await handleRepology("https://example.com", 20);
134
+ expect(result).toBeNull();
135
+ });
136
+
137
+ it("returns null for non-project Repology paths", async () => {
138
+ const result = await handleRepology("https://repology.org/", 20);
139
+ expect(result).toBeNull();
140
+ });
141
+
142
+ it("fetches firefox project", async () => {
143
+ const result = await handleRepology("https://repology.org/project/firefox", 20);
144
+ expect(result).not.toBeNull();
145
+ expect(result?.method).toBe("repology");
146
+ expect(result?.content).toContain("firefox");
147
+ expect(result?.content).toContain("Repositories");
148
+ expect(result?.contentType).toBe("text/markdown");
149
+ expect(result?.fetchedAt).toBeTruthy();
150
+ expect(result?.truncated).toBeDefined();
151
+ });
152
+
153
+ it("fetches vim project", async () => {
154
+ const result = await handleRepology("https://repology.org/project/vim/versions", 20);
155
+ expect(result).not.toBeNull();
156
+ expect(result?.method).toBe("repology");
157
+ expect(result?.content).toContain("vim");
158
+ });
159
+ });
160
+
161
+ describe.skipIf(SKIP)("handleTerraform", () => {
162
+ it("returns null for non-Terraform URLs", async () => {
163
+ const result = await handleTerraform("https://example.com", 20);
164
+ expect(result).toBeNull();
165
+ });
166
+
167
+ it("returns null for non-matching Terraform paths", async () => {
168
+ const result = await handleTerraform("https://registry.terraform.io/", 20);
169
+ expect(result).toBeNull();
170
+ });
171
+
172
+ it("fetches hashicorp/aws provider", async () => {
173
+ const result = await handleTerraform("https://registry.terraform.io/providers/hashicorp/aws", 20);
174
+ expect(result).not.toBeNull();
175
+ expect(result?.method).toBe("terraform");
176
+ expect(result?.content).toContain("aws");
177
+ expect(result?.content).toContain("hashicorp");
178
+ expect(result?.content).toContain("required_providers");
179
+ expect(result?.contentType).toBe("text/markdown");
180
+ expect(result?.fetchedAt).toBeTruthy();
181
+ expect(result?.truncated).toBeDefined();
182
+ });
183
+
184
+ it("fetches terraform-aws-modules/vpc/aws module", async () => {
185
+ const result = await handleTerraform("https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws", 20);
186
+ expect(result).not.toBeNull();
187
+ expect(result?.method).toBe("terraform");
188
+ expect(result?.content).toContain("vpc");
189
+ expect(result?.content).toContain("terraform-aws-modules");
190
+ expect(result?.content).toContain("module");
191
+ });
192
+
193
+ it("fetches hashicorp/random provider", async () => {
194
+ const result = await handleTerraform("https://registry.terraform.io/providers/hashicorp/random", 20);
195
+ expect(result).not.toBeNull();
196
+ expect(result?.method).toBe("terraform");
197
+ expect(result?.content).toContain("random");
198
+ });
199
+ });