@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,303 @@
1
+ /**
2
+ * Discogs URL handler for music releases and masters
3
+ *
4
+ * Uses the Discogs API to extract structured metadata about releases.
5
+ * API docs: https://www.discogs.com/developers
6
+ */
7
+
8
+ import type { RenderResult, SpecialHandler } from "./types";
9
+ import { finalizeOutput, loadPage } from "./types";
10
+
11
+ interface DiscogsArtist {
12
+ name: string;
13
+ anv?: string; // artist name variation
14
+ role?: string;
15
+ join?: string;
16
+ }
17
+
18
+ interface DiscogsTrack {
19
+ position: string;
20
+ title: string;
21
+ duration?: string;
22
+ artists?: DiscogsArtist[];
23
+ extraartists?: DiscogsArtist[];
24
+ }
25
+
26
+ interface DiscogsLabel {
27
+ name: string;
28
+ catno?: string; // catalog number
29
+ }
30
+
31
+ interface DiscogsFormat {
32
+ name: string;
33
+ qty?: string;
34
+ descriptions?: string[];
35
+ }
36
+
37
+ interface DiscogsRelease {
38
+ id: number;
39
+ title: string;
40
+ artists?: DiscogsArtist[];
41
+ year?: number;
42
+ released?: string;
43
+ country?: string;
44
+ genres?: string[];
45
+ styles?: string[];
46
+ labels?: DiscogsLabel[];
47
+ formats?: DiscogsFormat[];
48
+ tracklist?: DiscogsTrack[];
49
+ extraartists?: DiscogsArtist[];
50
+ notes?: string;
51
+ uri?: string;
52
+ master_id?: number;
53
+ master_url?: string;
54
+ }
55
+
56
+ interface DiscogsMaster {
57
+ id: number;
58
+ title: string;
59
+ artists?: DiscogsArtist[];
60
+ year?: number;
61
+ genres?: string[];
62
+ styles?: string[];
63
+ tracklist?: DiscogsTrack[];
64
+ notes?: string;
65
+ uri?: string;
66
+ main_release?: number;
67
+ main_release_url?: string;
68
+ versions_url?: string;
69
+ num_for_sale?: number;
70
+ lowest_price?: number;
71
+ }
72
+
73
+ /**
74
+ * Format artist names, handling name variations
75
+ */
76
+ function formatArtists(artists: DiscogsArtist[] | undefined): string {
77
+ if (!artists?.length) return "Unknown Artist";
78
+ return artists
79
+ .map((a) => {
80
+ const name = a.anv || a.name;
81
+ const join = a.join || ", ";
82
+ return name + (a.join ? ` ${join} ` : "");
83
+ })
84
+ .join("")
85
+ .replace(/[,&]\s*$/, "")
86
+ .trim();
87
+ }
88
+
89
+ /**
90
+ * Format a single track
91
+ */
92
+ function formatTrack(track: DiscogsTrack): string {
93
+ let line = track.position ? `${track.position}. ` : "- ";
94
+ line += track.title;
95
+ if (track.duration) line += ` (${track.duration})`;
96
+ if (track.artists?.length) {
97
+ line += ` - ${formatArtists(track.artists)}`;
98
+ }
99
+ return line;
100
+ }
101
+
102
+ /**
103
+ * Format credits/extraartists grouped by role
104
+ */
105
+ function formatCredits(extraartists: DiscogsArtist[] | undefined): string {
106
+ if (!extraartists?.length) return "";
107
+
108
+ const byRole: Record<string, string[]> = {};
109
+ for (const artist of extraartists) {
110
+ const role = artist.role || "Other";
111
+ if (!byRole[role]) byRole[role] = [];
112
+ byRole[role].push(artist.anv || artist.name);
113
+ }
114
+
115
+ const lines: string[] = [];
116
+ for (const [role, names] of Object.entries(byRole)) {
117
+ lines.push(`- **${role}**: ${names.join(", ")}`);
118
+ }
119
+ return lines.join("\n");
120
+ }
121
+
122
+ /**
123
+ * Format formats (e.g., "2×LP, Album, Reissue")
124
+ */
125
+ function formatFormats(formats: DiscogsFormat[] | undefined): string {
126
+ if (!formats?.length) return "";
127
+
128
+ return formats
129
+ .map((f) => {
130
+ const parts: string[] = [];
131
+ if (f.qty && parseInt(f.qty, 10) > 1) parts.push(`${f.qty}×`);
132
+ parts.push(f.name);
133
+ if (f.descriptions?.length) parts.push(f.descriptions.join(", "));
134
+ return parts.join(" ");
135
+ })
136
+ .join(" + ");
137
+ }
138
+
139
+ /**
140
+ * Format labels with catalog numbers
141
+ */
142
+ function formatLabels(labels: DiscogsLabel[] | undefined): string {
143
+ if (!labels?.length) return "";
144
+ return labels
145
+ .map((l) => {
146
+ if (l.catno && l.catno !== "none") return `${l.name} (${l.catno})`;
147
+ return l.name;
148
+ })
149
+ .join(", ");
150
+ }
151
+
152
+ /**
153
+ * Build markdown for a release
154
+ */
155
+ function buildReleaseMarkdown(release: DiscogsRelease): string {
156
+ const sections: string[] = [];
157
+
158
+ // Title with artist
159
+ const artist = formatArtists(release.artists);
160
+ sections.push(`# ${artist} - ${release.title}\n`);
161
+
162
+ // Metadata
163
+ const meta: string[] = [];
164
+ if (release.year) meta.push(`**Year**: ${release.year}`);
165
+ if (release.country) meta.push(`**Country**: ${release.country}`);
166
+
167
+ const format = formatFormats(release.formats);
168
+ if (format) meta.push(`**Format**: ${format}`);
169
+
170
+ const labels = formatLabels(release.labels);
171
+ if (labels) meta.push(`**Label**: ${labels}`);
172
+
173
+ if (release.genres?.length) meta.push(`**Genre**: ${release.genres.join(", ")}`);
174
+ if (release.styles?.length) meta.push(`**Style**: ${release.styles.join(", ")}`);
175
+
176
+ if (release.master_id) {
177
+ meta.push(`**Master Release**: [${release.master_id}](https://www.discogs.com/master/${release.master_id})`);
178
+ }
179
+
180
+ if (meta.length) sections.push(`${meta.join("\n")}\n`);
181
+
182
+ // Tracklist
183
+ if (release.tracklist?.length) {
184
+ sections.push("## Tracklist\n");
185
+ const tracks = release.tracklist.map(formatTrack);
186
+ sections.push(`${tracks.join("\n")}\n`);
187
+ }
188
+
189
+ // Credits
190
+ const credits = formatCredits(release.extraartists);
191
+ if (credits) {
192
+ sections.push("## Credits\n");
193
+ sections.push(`${credits}\n`);
194
+ }
195
+
196
+ // Notes
197
+ if (release.notes) {
198
+ sections.push("## Notes\n");
199
+ sections.push(`${release.notes}\n`);
200
+ }
201
+
202
+ return sections.join("\n");
203
+ }
204
+
205
+ /**
206
+ * Build markdown for a master release
207
+ */
208
+ function buildMasterMarkdown(master: DiscogsMaster): string {
209
+ const sections: string[] = [];
210
+
211
+ // Title with artist
212
+ const artist = formatArtists(master.artists);
213
+ sections.push(`# ${artist} - ${master.title}\n`);
214
+ sections.push("*Master Release*\n");
215
+
216
+ // Metadata
217
+ const meta: string[] = [];
218
+ if (master.year) meta.push(`**Year**: ${master.year}`);
219
+ if (master.genres?.length) meta.push(`**Genre**: ${master.genres.join(", ")}`);
220
+ if (master.styles?.length) meta.push(`**Style**: ${master.styles.join(", ")}`);
221
+
222
+ if (master.main_release) {
223
+ meta.push(`**Main Release**: [${master.main_release}](https://www.discogs.com/release/${master.main_release})`);
224
+ }
225
+
226
+ if (master.num_for_sale !== undefined && master.num_for_sale > 0) {
227
+ meta.push(`**For Sale**: ${master.num_for_sale} copies`);
228
+ if (master.lowest_price !== undefined) {
229
+ meta.push(`**Lowest Price**: $${master.lowest_price.toFixed(2)}`);
230
+ }
231
+ }
232
+
233
+ if (meta.length) sections.push(`${meta.join("\n")}\n`);
234
+
235
+ // Tracklist
236
+ if (master.tracklist?.length) {
237
+ sections.push("## Tracklist\n");
238
+ const tracks = master.tracklist.map(formatTrack);
239
+ sections.push(`${tracks.join("\n")}\n`);
240
+ }
241
+
242
+ // Notes
243
+ if (master.notes) {
244
+ sections.push("## Notes\n");
245
+ sections.push(`${master.notes}\n`);
246
+ }
247
+
248
+ return sections.join("\n");
249
+ }
250
+
251
+ export const handleDiscogs: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
252
+ try {
253
+ const parsed = new URL(url);
254
+ if (!parsed.hostname.includes("discogs.com")) return null;
255
+
256
+ // Match release or master URLs
257
+ // Patterns: /release/{id}, /master/{id}
258
+ // Also handles: /release/{id}-Artist-Title, /master/{id}-Artist-Title
259
+ const releaseMatch = parsed.pathname.match(/\/release\/(\d+)/);
260
+ const masterMatch = parsed.pathname.match(/\/master\/(\d+)/);
261
+
262
+ if (!releaseMatch && !masterMatch) return null;
263
+
264
+ const fetchedAt = new Date().toISOString();
265
+ const isRelease = !!releaseMatch;
266
+ const id = isRelease ? releaseMatch[1] : masterMatch![1];
267
+
268
+ const apiUrl = isRelease ? `https://api.discogs.com/releases/${id}` : `https://api.discogs.com/masters/${id}`;
269
+
270
+ const result = await loadPage(apiUrl, {
271
+ timeout,
272
+ headers: {
273
+ Accept: "application/json",
274
+ "User-Agent": "CodingAgent/1.0 +https://github.com/can1357/oh-my-pi",
275
+ },
276
+ });
277
+
278
+ if (!result.ok) return null;
279
+
280
+ let md: string;
281
+ if (isRelease) {
282
+ const release = JSON.parse(result.content) as DiscogsRelease;
283
+ md = buildReleaseMarkdown(release);
284
+ } else {
285
+ const master = JSON.parse(result.content) as DiscogsMaster;
286
+ md = buildMasterMarkdown(master);
287
+ }
288
+
289
+ const output = finalizeOutput(md);
290
+ return {
291
+ url,
292
+ finalUrl: url,
293
+ contentType: "text/markdown",
294
+ method: "discogs",
295
+ content: output.content,
296
+ fetchedAt,
297
+ truncated: output.truncated,
298
+ notes: [`Fetched via Discogs API (${isRelease ? "release" : "master"})`],
299
+ };
300
+ } catch {}
301
+
302
+ return null;
303
+ };
@@ -0,0 +1,156 @@
1
+ import type { RenderResult, SpecialHandler } from "./types";
2
+ import { finalizeOutput, formatCount, loadPage } from "./types";
3
+
4
+ interface DockerHubRepo {
5
+ name: string;
6
+ namespace: string;
7
+ description?: string;
8
+ star_count?: number;
9
+ pull_count?: number;
10
+ last_updated?: string;
11
+ is_official?: boolean;
12
+ is_automated?: boolean;
13
+ user?: string;
14
+ }
15
+
16
+ interface DockerHubTag {
17
+ name: string;
18
+ last_updated?: string;
19
+ full_size?: number;
20
+ digest?: string;
21
+ images?: Array<{
22
+ architecture?: string;
23
+ os?: string;
24
+ size?: number;
25
+ }>;
26
+ }
27
+
28
+ interface DockerHubTagsResponse {
29
+ results?: DockerHubTag[];
30
+ }
31
+
32
+ function formatSize(bytes: number): string {
33
+ if (bytes >= 1_000_000_000) return `${(bytes / 1_000_000_000).toFixed(1)}GB`;
34
+ if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)}MB`;
35
+ if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(1)}KB`;
36
+ return `${bytes}B`;
37
+ }
38
+
39
+ /**
40
+ * Handle Docker Hub URLs via API
41
+ */
42
+ export const handleDockerHub: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
43
+ try {
44
+ const parsed = new URL(url);
45
+ if (!parsed.hostname.includes("hub.docker.com")) return null;
46
+
47
+ let namespace: string;
48
+ let repository: string;
49
+
50
+ // Official images: /_ /{image}
51
+ const officialMatch = parsed.pathname.match(/^\/_\/([^/]+)/);
52
+ if (officialMatch) {
53
+ namespace = "library";
54
+ repository = officialMatch[1];
55
+ } else {
56
+ // Regular images: /r/{namespace}/{repository}
57
+ const repoMatch = parsed.pathname.match(/^\/r\/([^/]+)\/([^/]+)/);
58
+ if (!repoMatch) return null;
59
+ namespace = repoMatch[1];
60
+ repository = repoMatch[2];
61
+ }
62
+
63
+ const fetchedAt = new Date().toISOString();
64
+
65
+ // Fetch repository info and tags in parallel
66
+ const repoUrl = `https://hub.docker.com/v2/repositories/${namespace}/${repository}/`;
67
+ const tagsUrl = `https://hub.docker.com/v2/repositories/${namespace}/${repository}/tags/?page_size=10`;
68
+
69
+ const [repoResult, tagsResult] = await Promise.all([
70
+ loadPage(repoUrl, { timeout, headers: { Accept: "application/json" } }),
71
+ loadPage(tagsUrl, { timeout: Math.min(timeout, 10), headers: { Accept: "application/json" } }),
72
+ ]);
73
+
74
+ if (!repoResult.ok) return null;
75
+
76
+ let repo: DockerHubRepo;
77
+ try {
78
+ repo = JSON.parse(repoResult.content);
79
+ } catch {
80
+ return null;
81
+ }
82
+
83
+ // Parse tags
84
+ let tags: DockerHubTag[] = [];
85
+ if (tagsResult.ok) {
86
+ try {
87
+ const tagsData = JSON.parse(tagsResult.content) as DockerHubTagsResponse;
88
+ tags = tagsData.results ?? [];
89
+ } catch {}
90
+ }
91
+
92
+ // Build markdown output
93
+ const fullName = namespace === "library" ? repo.name : `${namespace}/${repo.name}`;
94
+ let md = `# ${fullName}\n\n`;
95
+
96
+ if (repo.description) {
97
+ md += `${repo.description}\n\n`;
98
+ }
99
+
100
+ // Stats line
101
+ const stats: string[] = [];
102
+ if (repo.pull_count !== undefined) stats.push(`**Pulls:** ${formatCount(repo.pull_count)}`);
103
+ if (repo.star_count !== undefined) stats.push(`**Stars:** ${formatCount(repo.star_count)}`);
104
+ if (repo.is_official) stats.push("**Official Image**");
105
+ if (repo.is_automated) stats.push("**Automated Build**");
106
+ if (stats.length > 0) {
107
+ md += `${stats.join(" · ")}\n`;
108
+ }
109
+
110
+ if (repo.last_updated) {
111
+ const date = new Date(repo.last_updated);
112
+ md += `**Last Updated:** ${date.toISOString().split("T")[0]}\n`;
113
+ }
114
+
115
+ md += "\n";
116
+
117
+ // Docker pull command
118
+ md += "## Quick Start\n\n";
119
+ md += "```bash\n";
120
+ md += `docker pull ${fullName}\n`;
121
+ md += "```\n\n";
122
+
123
+ // Tags
124
+ if (tags.length > 0) {
125
+ md += "## Recent Tags\n\n";
126
+ md += "| Tag | Size | Architectures | Updated |\n";
127
+ md += "|-----|------|---------------|--------|\n";
128
+
129
+ for (const tag of tags) {
130
+ const size = tag.full_size ? formatSize(tag.full_size) : "-";
131
+ const archs =
132
+ tag.images
133
+ ?.map((img) => img.architecture)
134
+ .filter(Boolean)
135
+ .join(", ") || "-";
136
+ const updated = tag.last_updated ? new Date(tag.last_updated).toISOString().split("T")[0] : "-";
137
+ md += `| \`${tag.name}\` | ${size} | ${archs} | ${updated} |\n`;
138
+ }
139
+ md += "\n";
140
+ }
141
+
142
+ const output = finalizeOutput(md);
143
+ return {
144
+ url,
145
+ finalUrl: url,
146
+ contentType: "text/markdown",
147
+ method: "dockerhub",
148
+ content: output.content,
149
+ fetchedAt,
150
+ truncated: output.truncated,
151
+ notes: ["Fetched via Docker Hub API"],
152
+ };
153
+ } catch {}
154
+
155
+ return null;
156
+ };
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { handleMDN } from "./mdn";
3
+ import { handleReadTheDocs } from "./readthedocs";
4
+
5
+ const SKIP = !process.env.WEB_FETCH_INTEGRATION;
6
+
7
+ describe.skipIf(SKIP)("handleMDN", () => {
8
+ it("returns null for non-MDN URLs", async () => {
9
+ const result = await handleMDN("https://example.com", 20);
10
+ expect(result).toBeNull();
11
+ });
12
+
13
+ it("returns null for non-docs MDN URLs", async () => {
14
+ const result = await handleMDN("https://developer.mozilla.org/en-US/", 20);
15
+ expect(result).toBeNull();
16
+ });
17
+
18
+ it("returns null for MDN blog URLs", async () => {
19
+ const result = await handleMDN("https://developer.mozilla.org/en-US/blog/", 20);
20
+ expect(result).toBeNull();
21
+ });
22
+
23
+ it("fetches Array.map documentation", async () => {
24
+ const result = await handleMDN(
25
+ "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map",
26
+ 20,
27
+ );
28
+ expect(result).not.toBeNull();
29
+ expect(result?.method).toBe("mdn");
30
+ expect(result?.content).toContain("map");
31
+ expect(result?.contentType).toBe("text/markdown");
32
+ expect(result?.fetchedAt).toBeTruthy();
33
+ });
34
+
35
+ it("fetches Promise documentation", async () => {
36
+ const result = await handleMDN(
37
+ "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise",
38
+ 20,
39
+ );
40
+ expect(result).not.toBeNull();
41
+ expect(result?.method).toBe("mdn");
42
+ expect(result?.content).toContain("Promise");
43
+ expect(result?.truncated).toBeDefined();
44
+ });
45
+
46
+ it("fetches CSS documentation", async () => {
47
+ const result = await handleMDN("https://developer.mozilla.org/en-US/docs/Web/CSS/display", 20);
48
+ expect(result).not.toBeNull();
49
+ expect(result?.method).toBe("mdn");
50
+ expect(result?.content).toContain("display");
51
+ });
52
+ });
53
+
54
+ describe.skipIf(SKIP)("handleReadTheDocs", () => {
55
+ it("returns null for non-RTD URLs", async () => {
56
+ const result = await handleReadTheDocs("https://example.com", 20);
57
+ expect(result).toBeNull();
58
+ });
59
+
60
+ it("returns null for github.com URLs", async () => {
61
+ const result = await handleReadTheDocs("https://github.com/user/repo", 20);
62
+ expect(result).toBeNull();
63
+ });
64
+
65
+ it("fetches requests docs", async () => {
66
+ const result = await handleReadTheDocs("https://requests.readthedocs.io/en/latest/", 20);
67
+ expect(result).not.toBeNull();
68
+ expect(result?.method).toBe("readthedocs");
69
+ expect(result?.fetchedAt).toBeTruthy();
70
+ expect(result?.truncated).toBeDefined();
71
+ });
72
+
73
+ it("returns null for non-readthedocs sites", async () => {
74
+ // These sites use Sphinx/RTD theme but aren't hosted on readthedocs.io
75
+ expect(await handleReadTheDocs("https://www.sphinx-doc.org/en/master/", 20)).toBeNull();
76
+ expect(await handleReadTheDocs("https://docs.pytest.org/en/stable/", 20)).toBeNull();
77
+ expect(await handleReadTheDocs("https://pip.pypa.io/en/stable/", 20)).toBeNull();
78
+ });
79
+
80
+ it("handles readthedocs.io subdomain", async () => {
81
+ const result = await handleReadTheDocs("https://flask.palletsprojects.readthedocs.io/en/latest/", 20);
82
+ expect(result).not.toBeNull();
83
+ expect(result?.method).toBe("readthedocs");
84
+ });
85
+ });
@@ -0,0 +1,144 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { handleArtifactHub } from "./artifacthub";
3
+ import { handleCoinGecko } from "./coingecko";
4
+ import { handleDiscogs } from "./discogs";
5
+
6
+ const SKIP = !process.env.WEB_FETCH_INTEGRATION;
7
+
8
+ describe.skipIf(SKIP)("handleCoinGecko", () => {
9
+ it("returns null for non-CoinGecko URLs", async () => {
10
+ const result = await handleCoinGecko("https://example.com", 20);
11
+ expect(result).toBeNull();
12
+ });
13
+
14
+ it("returns null for CoinGecko homepage", async () => {
15
+ const result = await handleCoinGecko("https://www.coingecko.com/", 20);
16
+ expect(result).toBeNull();
17
+ });
18
+
19
+ it("returns null for CoinGecko categories page", async () => {
20
+ const result = await handleCoinGecko("https://www.coingecko.com/en/categories", 20);
21
+ expect(result).toBeNull();
22
+ });
23
+
24
+ it("fetches Bitcoin data", async () => {
25
+ const result = await handleCoinGecko("https://www.coingecko.com/en/coins/bitcoin", 20);
26
+ expect(result).not.toBeNull();
27
+ expect(result?.method).toBe("coingecko");
28
+ expect(result?.content).toContain("Bitcoin");
29
+ expect(result?.content).toContain("BTC");
30
+ expect(result?.content).toContain("Price");
31
+ expect(result?.contentType).toBe("text/markdown");
32
+ expect(result?.fetchedAt).toBeTruthy();
33
+ expect(result?.truncated).toBeDefined();
34
+ });
35
+
36
+ it("fetches Ethereum data", async () => {
37
+ const result = await handleCoinGecko("https://www.coingecko.com/en/coins/ethereum", 20);
38
+ expect(result).not.toBeNull();
39
+ expect(result?.method).toBe("coingecko");
40
+ expect(result?.content).toContain("Ethereum");
41
+ expect(result?.content).toContain("ETH");
42
+ expect(result?.content).toContain("Market Cap");
43
+ expect(result?.truncated).toBeDefined();
44
+ });
45
+
46
+ it("handles URL without locale prefix", async () => {
47
+ const result = await handleCoinGecko("https://www.coingecko.com/coins/bitcoin", 20);
48
+ expect(result).not.toBeNull();
49
+ expect(result?.method).toBe("coingecko");
50
+ });
51
+ });
52
+
53
+ describe.skipIf(SKIP)("handleDiscogs", () => {
54
+ it("returns null for non-Discogs URLs", async () => {
55
+ const result = await handleDiscogs("https://example.com", 20);
56
+ expect(result).toBeNull();
57
+ });
58
+
59
+ it("returns null for Discogs homepage", async () => {
60
+ const result = await handleDiscogs("https://www.discogs.com/", 20);
61
+ expect(result).toBeNull();
62
+ });
63
+
64
+ it("returns null for Discogs search page", async () => {
65
+ const result = await handleDiscogs("https://www.discogs.com/search/", 20);
66
+ expect(result).toBeNull();
67
+ });
68
+
69
+ it("fetches Daft Punk Discovery release", async () => {
70
+ // Release 249504: Daft Punk - Discovery
71
+ const result = await handleDiscogs("https://www.discogs.com/release/249504-Daft-Punk-Discovery", 20);
72
+ expect(result).not.toBeNull();
73
+ expect(result?.method).toBe("discogs");
74
+ expect(result?.content).toContain("Daft Punk");
75
+ expect(result?.content).toContain("Discovery");
76
+ expect(result?.content).toContain("Tracklist");
77
+ expect(result?.contentType).toBe("text/markdown");
78
+ expect(result?.fetchedAt).toBeTruthy();
79
+ expect(result?.truncated).toBeDefined();
80
+ });
81
+
82
+ it("fetches master release", async () => {
83
+ // Master 33395: Daft Punk - Discovery (master)
84
+ const result = await handleDiscogs("https://www.discogs.com/master/33395-Daft-Punk-Discovery", 20);
85
+ expect(result).not.toBeNull();
86
+ expect(result?.method).toBe("discogs");
87
+ expect(result?.content).toContain("Daft Punk");
88
+ expect(result?.content).toContain("Master Release");
89
+ expect(result?.truncated).toBeDefined();
90
+ });
91
+
92
+ it("handles release URL with just ID", async () => {
93
+ const result = await handleDiscogs("https://www.discogs.com/release/249504", 20);
94
+ expect(result).not.toBeNull();
95
+ expect(result?.method).toBe("discogs");
96
+ });
97
+ });
98
+
99
+ describe.skipIf(SKIP)("handleArtifactHub", () => {
100
+ it("returns null for non-ArtifactHub URLs", async () => {
101
+ const result = await handleArtifactHub("https://example.com", 20);
102
+ expect(result).toBeNull();
103
+ });
104
+
105
+ it("returns null for ArtifactHub homepage", async () => {
106
+ const result = await handleArtifactHub("https://artifacthub.io/", 20);
107
+ expect(result).toBeNull();
108
+ });
109
+
110
+ it("returns null for ArtifactHub search page", async () => {
111
+ const result = await handleArtifactHub("https://artifacthub.io/packages/search", 20);
112
+ expect(result).toBeNull();
113
+ });
114
+
115
+ it("fetches bitnami/nginx helm chart", async () => {
116
+ const result = await handleArtifactHub("https://artifacthub.io/packages/helm/bitnami/nginx", 20);
117
+ expect(result).not.toBeNull();
118
+ expect(result?.method).toBe("artifacthub");
119
+ expect(result?.content).toContain("nginx");
120
+ expect(result?.content).toContain("Helm Chart");
121
+ expect(result?.content).toContain("Version");
122
+ expect(result?.contentType).toBe("text/markdown");
123
+ expect(result?.fetchedAt).toBeTruthy();
124
+ expect(result?.truncated).toBeDefined();
125
+ });
126
+
127
+ it("fetches prometheus-community/prometheus helm chart", async () => {
128
+ const result = await handleArtifactHub(
129
+ "https://artifacthub.io/packages/helm/prometheus-community/prometheus",
130
+ 20,
131
+ );
132
+ expect(result).not.toBeNull();
133
+ expect(result?.method).toBe("artifacthub");
134
+ expect(result?.content).toContain("prometheus");
135
+ expect(result?.content).toContain("Repository");
136
+ expect(result?.truncated).toBeDefined();
137
+ });
138
+
139
+ it("handles www subdomain", async () => {
140
+ const result = await handleArtifactHub("https://www.artifacthub.io/packages/helm/bitnami/nginx", 20);
141
+ expect(result).not.toBeNull();
142
+ expect(result?.method).toBe("artifacthub");
143
+ });
144
+ });