@oh-my-pi/pi-coding-agent 3.25.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 (85) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +4 -4
  3. package/src/core/tools/complete.ts +2 -4
  4. package/src/core/tools/jtd-to-json-schema.ts +174 -196
  5. package/src/core/tools/read.ts +4 -4
  6. package/src/core/tools/task/executor.ts +146 -20
  7. package/src/core/tools/task/name-generator.ts +1544 -214
  8. package/src/core/tools/task/types.ts +19 -5
  9. package/src/core/tools/task/worker.ts +103 -13
  10. package/src/core/tools/web-fetch-handlers/academic.test.ts +239 -0
  11. package/src/core/tools/web-fetch-handlers/artifacthub.ts +210 -0
  12. package/src/core/tools/web-fetch-handlers/arxiv.ts +84 -0
  13. package/src/core/tools/web-fetch-handlers/aur.ts +171 -0
  14. package/src/core/tools/web-fetch-handlers/biorxiv.ts +136 -0
  15. package/src/core/tools/web-fetch-handlers/bluesky.ts +277 -0
  16. package/src/core/tools/web-fetch-handlers/brew.ts +173 -0
  17. package/src/core/tools/web-fetch-handlers/business.test.ts +82 -0
  18. package/src/core/tools/web-fetch-handlers/cheatsh.ts +73 -0
  19. package/src/core/tools/web-fetch-handlers/chocolatey.ts +153 -0
  20. package/src/core/tools/web-fetch-handlers/coingecko.ts +179 -0
  21. package/src/core/tools/web-fetch-handlers/crates-io.ts +123 -0
  22. package/src/core/tools/web-fetch-handlers/dev-platforms.test.ts +254 -0
  23. package/src/core/tools/web-fetch-handlers/devto.ts +173 -0
  24. package/src/core/tools/web-fetch-handlers/discogs.ts +303 -0
  25. package/src/core/tools/web-fetch-handlers/dockerhub.ts +156 -0
  26. package/src/core/tools/web-fetch-handlers/documentation.test.ts +85 -0
  27. package/src/core/tools/web-fetch-handlers/finance-media.test.ts +144 -0
  28. package/src/core/tools/web-fetch-handlers/git-hosting.test.ts +272 -0
  29. package/src/core/tools/web-fetch-handlers/github-gist.ts +64 -0
  30. package/src/core/tools/web-fetch-handlers/github.ts +424 -0
  31. package/src/core/tools/web-fetch-handlers/gitlab.ts +444 -0
  32. package/src/core/tools/web-fetch-handlers/go-pkg.ts +271 -0
  33. package/src/core/tools/web-fetch-handlers/hackage.ts +89 -0
  34. package/src/core/tools/web-fetch-handlers/hackernews.ts +208 -0
  35. package/src/core/tools/web-fetch-handlers/hex.ts +121 -0
  36. package/src/core/tools/web-fetch-handlers/huggingface.ts +385 -0
  37. package/src/core/tools/web-fetch-handlers/iacr.ts +82 -0
  38. package/src/core/tools/web-fetch-handlers/index.ts +69 -0
  39. package/src/core/tools/web-fetch-handlers/lobsters.ts +186 -0
  40. package/src/core/tools/web-fetch-handlers/mastodon.ts +302 -0
  41. package/src/core/tools/web-fetch-handlers/maven.ts +147 -0
  42. package/src/core/tools/web-fetch-handlers/mdn.ts +174 -0
  43. package/src/core/tools/web-fetch-handlers/media.test.ts +138 -0
  44. package/src/core/tools/web-fetch-handlers/metacpan.ts +247 -0
  45. package/src/core/tools/web-fetch-handlers/npm.ts +107 -0
  46. package/src/core/tools/web-fetch-handlers/nuget.ts +201 -0
  47. package/src/core/tools/web-fetch-handlers/nvd.ts +238 -0
  48. package/src/core/tools/web-fetch-handlers/opencorporates.ts +273 -0
  49. package/src/core/tools/web-fetch-handlers/openlibrary.ts +313 -0
  50. package/src/core/tools/web-fetch-handlers/osv.ts +184 -0
  51. package/src/core/tools/web-fetch-handlers/package-managers-2.test.ts +199 -0
  52. package/src/core/tools/web-fetch-handlers/package-managers.test.ts +171 -0
  53. package/src/core/tools/web-fetch-handlers/package-registries.test.ts +259 -0
  54. package/src/core/tools/web-fetch-handlers/packagist.ts +170 -0
  55. package/src/core/tools/web-fetch-handlers/pub-dev.ts +185 -0
  56. package/src/core/tools/web-fetch-handlers/pubmed.ts +174 -0
  57. package/src/core/tools/web-fetch-handlers/pypi.ts +125 -0
  58. package/src/core/tools/web-fetch-handlers/readthedocs.ts +122 -0
  59. package/src/core/tools/web-fetch-handlers/reddit.ts +100 -0
  60. package/src/core/tools/web-fetch-handlers/repology.ts +257 -0
  61. package/src/core/tools/web-fetch-handlers/research.test.ts +107 -0
  62. package/src/core/tools/web-fetch-handlers/rfc.ts +205 -0
  63. package/src/core/tools/web-fetch-handlers/rubygems.ts +112 -0
  64. package/src/core/tools/web-fetch-handlers/sec-edgar.ts +269 -0
  65. package/src/core/tools/web-fetch-handlers/security.test.ts +103 -0
  66. package/src/core/tools/web-fetch-handlers/semantic-scholar.ts +190 -0
  67. package/src/core/tools/web-fetch-handlers/social-extended.test.ts +192 -0
  68. package/src/core/tools/web-fetch-handlers/social.test.ts +259 -0
  69. package/src/core/tools/web-fetch-handlers/spotify.ts +218 -0
  70. package/src/core/tools/web-fetch-handlers/stackexchange.test.ts +120 -0
  71. package/src/core/tools/web-fetch-handlers/stackoverflow.ts +123 -0
  72. package/src/core/tools/web-fetch-handlers/standards.test.ts +122 -0
  73. package/src/core/tools/web-fetch-handlers/terraform.ts +296 -0
  74. package/src/core/tools/web-fetch-handlers/tldr.ts +47 -0
  75. package/src/core/tools/web-fetch-handlers/twitter.ts +84 -0
  76. package/src/core/tools/web-fetch-handlers/types.ts +163 -0
  77. package/src/core/tools/web-fetch-handlers/utils.ts +91 -0
  78. package/src/core/tools/web-fetch-handlers/vimeo.ts +152 -0
  79. package/src/core/tools/web-fetch-handlers/wikidata.ts +349 -0
  80. package/src/core/tools/web-fetch-handlers/wikipedia.test.ts +73 -0
  81. package/src/core/tools/web-fetch-handlers/wikipedia.ts +91 -0
  82. package/src/core/tools/web-fetch-handlers/youtube.test.ts +198 -0
  83. package/src/core/tools/web-fetch-handlers/youtube.ts +319 -0
  84. package/src/core/tools/web-fetch.ts +152 -1324
  85. package/src/utils/tools-manager.ts +110 -8
@@ -0,0 +1,444 @@
1
+ import {
2
+ finalizeOutput,
3
+ formatCount,
4
+ htmlToBasicMarkdown,
5
+ loadPage,
6
+ type RenderResult,
7
+ type SpecialHandler,
8
+ } from "./types";
9
+
10
+ interface GitLabUrl {
11
+ namespace: string;
12
+ project: string;
13
+ type: "repo" | "blob" | "tree" | "issue" | "merge_request";
14
+ ref?: string;
15
+ path?: string;
16
+ id?: number;
17
+ }
18
+
19
+ /**
20
+ * Parse GitLab URL into structured data
21
+ */
22
+ function parseGitLabUrl(url: string): GitLabUrl | null {
23
+ try {
24
+ const parsed = new URL(url);
25
+ if (parsed.hostname !== "gitlab.com") return null;
26
+
27
+ const segments = parsed.pathname.split("/").filter(Boolean);
28
+ if (segments.length < 2) return null;
29
+
30
+ const [namespace, project, ...rest] = segments;
31
+
32
+ // Repo root
33
+ if (rest.length === 0) {
34
+ return { namespace, project, type: "repo" };
35
+ }
36
+
37
+ // Skip - prefix
38
+ if (rest[0] !== "-") return null;
39
+
40
+ const [, type, ...remaining] = rest;
41
+
42
+ // File: gitlab.com/{ns}/{proj}/-/blob/{ref}/{path}
43
+ if (type === "blob" && remaining.length >= 2) {
44
+ const [ref, ...pathParts] = remaining;
45
+ return {
46
+ namespace,
47
+ project,
48
+ type: "blob",
49
+ ref,
50
+ path: pathParts.join("/"),
51
+ };
52
+ }
53
+
54
+ // Directory: gitlab.com/{ns}/{proj}/-/tree/{ref}/{path}
55
+ if (type === "tree" && remaining.length >= 1) {
56
+ const [ref, ...pathParts] = remaining;
57
+ return {
58
+ namespace,
59
+ project,
60
+ type: "tree",
61
+ ref,
62
+ path: pathParts.length > 0 ? pathParts.join("/") : undefined,
63
+ };
64
+ }
65
+
66
+ // Issue: gitlab.com/{ns}/{proj}/-/issues/{id}
67
+ if (type === "issues" && remaining.length === 1) {
68
+ const id = parseInt(remaining[0], 10);
69
+ if (Number.isNaN(id)) return null;
70
+ return { namespace, project, type: "issue", id };
71
+ }
72
+
73
+ // MR: gitlab.com/{ns}/{proj}/-/merge_requests/{id}
74
+ if (type === "merge_requests" && remaining.length === 1) {
75
+ const id = parseInt(remaining[0], 10);
76
+ if (Number.isNaN(id)) return null;
77
+ return { namespace, project, type: "merge_request", id };
78
+ }
79
+
80
+ return null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Get project ID from namespace/project path
88
+ */
89
+ async function getProjectId(gl: GitLabUrl, timeout: number): Promise<number | null> {
90
+ const encodedPath = encodeURIComponent(`${gl.namespace}/${gl.project}`);
91
+ const apiUrl = `https://gitlab.com/api/v4/projects/${encodedPath}`;
92
+
93
+ const result = await loadPage(apiUrl, { timeout });
94
+ if (!result.ok) return null;
95
+
96
+ try {
97
+ const data = JSON.parse(result.content) as { id: number };
98
+ return data.id;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Render GitLab repository
106
+ */
107
+ async function renderGitLabRepo(gl: GitLabUrl, timeout: number): Promise<{ content: string; ok: boolean }> {
108
+ const encodedPath = encodeURIComponent(`${gl.namespace}/${gl.project}`);
109
+ const apiUrl = `https://gitlab.com/api/v4/projects/${encodedPath}`;
110
+
111
+ const result = await loadPage(apiUrl, { timeout });
112
+ if (!result.ok) return { content: "", ok: false };
113
+
114
+ try {
115
+ const repo = JSON.parse(result.content) as {
116
+ name: string;
117
+ description?: string;
118
+ star_count: number;
119
+ forks_count: number;
120
+ open_issues_count: number;
121
+ default_branch: string;
122
+ visibility: string;
123
+ created_at: string;
124
+ last_activity_at: string;
125
+ topics?: string[];
126
+ readme_url?: string;
127
+ };
128
+
129
+ let md = `# ${repo.name}\n\n`;
130
+ if (repo.description) md += `${repo.description}\n\n`;
131
+ md += `**Stars:** ${formatCount(repo.star_count)} · **Forks:** ${formatCount(repo.forks_count)} · **Issues:** ${formatCount(repo.open_issues_count)}\n`;
132
+ md += `**Visibility:** ${repo.visibility} · **Default Branch:** ${repo.default_branch}\n`;
133
+ if (repo.topics && repo.topics.length > 0) {
134
+ md += `**Topics:** ${repo.topics.join(", ")}\n`;
135
+ }
136
+ md += `**Created:** ${new Date(repo.created_at).toISOString().split("T")[0]} · **Last Activity:** ${new Date(repo.last_activity_at).toISOString().split("T")[0]}\n\n`;
137
+
138
+ // Try to fetch README
139
+ if (repo.readme_url) {
140
+ const readmeResult = await loadPage(repo.readme_url, { timeout });
141
+ if (readmeResult.ok && readmeResult.content.trim().length > 0) {
142
+ md += `---\n\n## README\n\n${readmeResult.content}\n`;
143
+ }
144
+ }
145
+
146
+ return { content: md, ok: true };
147
+ } catch {
148
+ return { content: "", ok: false };
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Render GitLab file
154
+ */
155
+ async function renderGitLabFile(
156
+ gl: GitLabUrl,
157
+ projectId: number,
158
+ timeout: number,
159
+ ): Promise<{ content: string; ok: boolean }> {
160
+ const encodedPath = encodeURIComponent(gl.path!);
161
+ const apiUrl = `https://gitlab.com/api/v4/projects/${projectId}/repository/files/${encodedPath}/raw?ref=${gl.ref}`;
162
+
163
+ const result = await loadPage(apiUrl, { timeout });
164
+ if (!result.ok) return { content: "", ok: false };
165
+
166
+ return { content: result.content, ok: true };
167
+ }
168
+
169
+ /**
170
+ * Render GitLab directory tree
171
+ */
172
+ async function renderGitLabTree(
173
+ gl: GitLabUrl,
174
+ projectId: number,
175
+ timeout: number,
176
+ ): Promise<{ content: string; ok: boolean }> {
177
+ const apiUrl = `https://gitlab.com/api/v4/projects/${projectId}/repository/tree?ref=${gl.ref}&path=${gl.path || ""}&per_page=100`;
178
+
179
+ const result = await loadPage(apiUrl, { timeout });
180
+ if (!result.ok) return { content: "", ok: false };
181
+
182
+ try {
183
+ const tree = JSON.parse(result.content) as Array<{
184
+ name: string;
185
+ type: "tree" | "blob";
186
+ path: string;
187
+ mode: string;
188
+ }>;
189
+
190
+ let md = `# Directory: ${gl.path || "/"}\n\n`;
191
+ md += `**Ref:** ${gl.ref}\n\n`;
192
+
193
+ // Separate directories and files
194
+ const dirs = tree.filter((item) => item.type === "tree");
195
+ const files = tree.filter((item) => item.type === "blob");
196
+
197
+ if (dirs.length > 0) {
198
+ md += `## Directories (${dirs.length})\n\n`;
199
+ for (const dir of dirs) {
200
+ md += `- 📁 ${dir.name}/\n`;
201
+ }
202
+ md += `\n`;
203
+ }
204
+
205
+ if (files.length > 0) {
206
+ md += `## Files (${files.length})\n\n`;
207
+ for (const file of files) {
208
+ md += `- 📄 ${file.name}\n`;
209
+ }
210
+ }
211
+
212
+ return { content: md, ok: true };
213
+ } catch {
214
+ return { content: "", ok: false };
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Render GitLab issue
220
+ */
221
+ async function renderGitLabIssue(
222
+ gl: GitLabUrl,
223
+ projectId: number,
224
+ timeout: number,
225
+ ): Promise<{ content: string; ok: boolean }> {
226
+ const apiUrl = `https://gitlab.com/api/v4/projects/${projectId}/issues/${gl.id}`;
227
+
228
+ const result = await loadPage(apiUrl, { timeout });
229
+ if (!result.ok) return { content: "", ok: false };
230
+
231
+ try {
232
+ const issue = JSON.parse(result.content) as {
233
+ title: string;
234
+ description?: string;
235
+ state: string;
236
+ author: { name: string; username: string };
237
+ created_at: string;
238
+ updated_at: string;
239
+ labels: string[];
240
+ upvotes: number;
241
+ downvotes: number;
242
+ user_notes_count: number;
243
+ assignees?: Array<{ name: string }>;
244
+ };
245
+
246
+ let md = `# Issue #${gl.id}: ${issue.title}\n\n`;
247
+ md += `**State:** ${issue.state.toUpperCase()} · **Author:** ${issue.author.name} (@${issue.author.username})\n`;
248
+ md += `**Created:** ${new Date(issue.created_at).toISOString().split("T")[0]} · **Updated:** ${new Date(issue.updated_at).toISOString().split("T")[0]}\n`;
249
+ md += `**Upvotes:** ${issue.upvotes} · **Downvotes:** ${issue.downvotes} · **Comments:** ${issue.user_notes_count}\n`;
250
+
251
+ if (issue.labels.length > 0) {
252
+ md += `**Labels:** ${issue.labels.join(", ")}\n`;
253
+ }
254
+
255
+ if (issue.assignees && issue.assignees.length > 0) {
256
+ md += `**Assignees:** ${issue.assignees.map((a) => a.name).join(", ")}\n`;
257
+ }
258
+
259
+ md += `\n---\n\n## Description\n\n`;
260
+ md += issue.description ? htmlToBasicMarkdown(issue.description) : "*No description*";
261
+
262
+ return { content: md, ok: true };
263
+ } catch {
264
+ return { content: "", ok: false };
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Render GitLab merge request
270
+ */
271
+ async function renderGitLabMR(
272
+ gl: GitLabUrl,
273
+ projectId: number,
274
+ timeout: number,
275
+ ): Promise<{ content: string; ok: boolean }> {
276
+ const apiUrl = `https://gitlab.com/api/v4/projects/${projectId}/merge_requests/${gl.id}`;
277
+
278
+ const result = await loadPage(apiUrl, { timeout });
279
+ if (!result.ok) return { content: "", ok: false };
280
+
281
+ try {
282
+ const mr = JSON.parse(result.content) as {
283
+ title: string;
284
+ description?: string;
285
+ state: string;
286
+ author: { name: string; username: string };
287
+ created_at: string;
288
+ updated_at: string;
289
+ source_branch: string;
290
+ target_branch: string;
291
+ labels: string[];
292
+ upvotes: number;
293
+ downvotes: number;
294
+ user_notes_count: number;
295
+ assignees?: Array<{ name: string }>;
296
+ draft: boolean;
297
+ merge_status: string;
298
+ };
299
+
300
+ let md = `# MR !${gl.id}: ${mr.title}\n\n`;
301
+ if (mr.draft) md += `**[DRAFT]** `;
302
+ md += `**State:** ${mr.state.toUpperCase()} · **Author:** ${mr.author.name} (@${mr.author.username})\n`;
303
+ md += `**Branch:** ${mr.source_branch} → ${mr.target_branch}\n`;
304
+ md += `**Created:** ${new Date(mr.created_at).toISOString().split("T")[0]} · **Updated:** ${new Date(mr.updated_at).toISOString().split("T")[0]}\n`;
305
+ md += `**Merge Status:** ${mr.merge_status} · **Upvotes:** ${mr.upvotes} · **Downvotes:** ${mr.downvotes} · **Comments:** ${mr.user_notes_count}\n`;
306
+
307
+ if (mr.labels.length > 0) {
308
+ md += `**Labels:** ${mr.labels.join(", ")}\n`;
309
+ }
310
+
311
+ if (mr.assignees && mr.assignees.length > 0) {
312
+ md += `**Assignees:** ${mr.assignees.map((a) => a.name).join(", ")}\n`;
313
+ }
314
+
315
+ md += `\n---\n\n## Description\n\n`;
316
+ md += mr.description ? htmlToBasicMarkdown(mr.description) : "*No description*";
317
+
318
+ return { content: md, ok: true };
319
+ } catch {
320
+ return { content: "", ok: false };
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Handle GitLab URLs specially
326
+ */
327
+ export const handleGitLab: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
328
+ const gl = parseGitLabUrl(url);
329
+ if (!gl) return null;
330
+
331
+ const fetchedAt = new Date().toISOString();
332
+ const notes: string[] = [];
333
+
334
+ switch (gl.type) {
335
+ case "blob": {
336
+ const projectId = await getProjectId(gl, timeout);
337
+ if (!projectId) break;
338
+
339
+ notes.push(`Fetched raw file via GitLab API`);
340
+ const result = await renderGitLabFile(gl, projectId, timeout);
341
+ if (result.ok) {
342
+ const output = finalizeOutput(result.content);
343
+ return {
344
+ url,
345
+ finalUrl: url,
346
+ contentType: "text/plain",
347
+ method: "gitlab-raw",
348
+ content: output.content,
349
+ fetchedAt,
350
+ truncated: output.truncated,
351
+ notes,
352
+ };
353
+ }
354
+ break;
355
+ }
356
+
357
+ case "tree": {
358
+ const projectId = await getProjectId(gl, timeout);
359
+ if (!projectId) break;
360
+
361
+ notes.push(`Fetched directory tree via GitLab API`);
362
+ const result = await renderGitLabTree(gl, projectId, timeout);
363
+ if (result.ok) {
364
+ const output = finalizeOutput(result.content);
365
+ return {
366
+ url,
367
+ finalUrl: url,
368
+ contentType: "text/markdown",
369
+ method: "gitlab-tree",
370
+ content: output.content,
371
+ fetchedAt,
372
+ truncated: output.truncated,
373
+ notes,
374
+ };
375
+ }
376
+ break;
377
+ }
378
+
379
+ case "issue": {
380
+ const projectId = await getProjectId(gl, timeout);
381
+ if (!projectId) break;
382
+
383
+ notes.push(`Fetched issue via GitLab API`);
384
+ const result = await renderGitLabIssue(gl, projectId, timeout);
385
+ if (result.ok) {
386
+ const output = finalizeOutput(result.content);
387
+ return {
388
+ url,
389
+ finalUrl: url,
390
+ contentType: "text/markdown",
391
+ method: "gitlab-issue",
392
+ content: output.content,
393
+ fetchedAt,
394
+ truncated: output.truncated,
395
+ notes,
396
+ };
397
+ }
398
+ break;
399
+ }
400
+
401
+ case "merge_request": {
402
+ const projectId = await getProjectId(gl, timeout);
403
+ if (!projectId) break;
404
+
405
+ notes.push(`Fetched merge request via GitLab API`);
406
+ const result = await renderGitLabMR(gl, projectId, timeout);
407
+ if (result.ok) {
408
+ const output = finalizeOutput(result.content);
409
+ return {
410
+ url,
411
+ finalUrl: url,
412
+ contentType: "text/markdown",
413
+ method: "gitlab-mr",
414
+ content: output.content,
415
+ fetchedAt,
416
+ truncated: output.truncated,
417
+ notes,
418
+ };
419
+ }
420
+ break;
421
+ }
422
+
423
+ case "repo": {
424
+ notes.push(`Fetched repository via GitLab API`);
425
+ const result = await renderGitLabRepo(gl, timeout);
426
+ if (result.ok) {
427
+ const output = finalizeOutput(result.content);
428
+ return {
429
+ url,
430
+ finalUrl: url,
431
+ contentType: "text/markdown",
432
+ method: "gitlab-repo",
433
+ content: output.content,
434
+ fetchedAt,
435
+ truncated: output.truncated,
436
+ notes,
437
+ };
438
+ }
439
+ break;
440
+ }
441
+ }
442
+
443
+ return null;
444
+ };
@@ -0,0 +1,271 @@
1
+ import { parse as parseHtml } from "node-html-parser";
2
+ import type { RenderResult, SpecialHandler } from "./types";
3
+ import { finalizeOutput, htmlToBasicMarkdown, loadPage } from "./types";
4
+
5
+ interface GoModuleInfo {
6
+ Version: string;
7
+ Time: string;
8
+ }
9
+
10
+ /**
11
+ * Handle pkg.go.dev URLs via proxy API and page parsing
12
+ */
13
+ export const handleGoPkg: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
14
+ try {
15
+ const parsed = new URL(url);
16
+ if (parsed.hostname !== "pkg.go.dev") return null;
17
+
18
+ // Extract module path and version from URL
19
+ // Patterns: /module, /module@version, /module/subpackage
20
+ const pathname = parsed.pathname.slice(1); // remove leading /
21
+ if (!pathname) return null;
22
+
23
+ let modulePath: string;
24
+ let version = "latest";
25
+ let _subpackage = "";
26
+
27
+ // Parse @version if present
28
+ const atIndex = pathname.indexOf("@");
29
+ if (atIndex !== -1) {
30
+ const beforeAt = pathname.slice(0, atIndex);
31
+ const afterAt = pathname.slice(atIndex + 1);
32
+
33
+ // Check if there's a subpackage after version
34
+ const slashIndex = afterAt.indexOf("/");
35
+ if (slashIndex !== -1) {
36
+ version = afterAt.slice(0, slashIndex);
37
+ const remainder = afterAt.slice(slashIndex + 1);
38
+ modulePath = beforeAt;
39
+ _subpackage = remainder;
40
+ } else {
41
+ version = afterAt;
42
+ modulePath = beforeAt;
43
+ }
44
+ } else {
45
+ // No version specified, check for subpackage
46
+ // Need to determine where module ends and subpackage begins
47
+ // For now, treat the whole path as module path (we'll refine from proxy response)
48
+ modulePath = pathname;
49
+ }
50
+
51
+ const notes: string[] = [];
52
+ const sections: string[] = [];
53
+
54
+ // Fetch module info from proxy
55
+ let moduleInfo: GoModuleInfo | null = null;
56
+ let actualModulePath = modulePath;
57
+
58
+ if (version === "latest") {
59
+ try {
60
+ const proxyUrl = `https://proxy.golang.org/${encodeURIComponent(modulePath)}/@latest`;
61
+ const proxyResult = await loadPage(proxyUrl, { timeout });
62
+
63
+ if (proxyResult.ok) {
64
+ moduleInfo = JSON.parse(proxyResult.content) as GoModuleInfo;
65
+ version = moduleInfo.Version;
66
+ }
67
+ } catch {
68
+ // If @latest fails, might be a subpackage - will extract from page
69
+ }
70
+ } else {
71
+ try {
72
+ const proxyUrl = `https://proxy.golang.org/${encodeURIComponent(modulePath)}/@v/${encodeURIComponent(version)}.info`;
73
+ const proxyResult = await loadPage(proxyUrl, { timeout });
74
+
75
+ if (proxyResult.ok) {
76
+ moduleInfo = JSON.parse(proxyResult.content) as GoModuleInfo;
77
+ }
78
+ } catch {
79
+ // Proxy lookup failed, will rely on page data
80
+ }
81
+ }
82
+
83
+ // Fetch the pkg.go.dev page
84
+ const pageResult = await loadPage(url, { timeout });
85
+ if (!pageResult.ok) {
86
+ return {
87
+ url,
88
+ finalUrl: pageResult.finalUrl,
89
+ contentType: "text/plain",
90
+ method: "go-pkg",
91
+ content: `Failed to fetch pkg.go.dev page (status: ${pageResult.status ?? "unknown"})`,
92
+ fetchedAt: new Date().toISOString(),
93
+ truncated: false,
94
+ notes: ["error"],
95
+ };
96
+ }
97
+
98
+ const doc = parseHtml(pageResult.content);
99
+
100
+ // Extract module/package information
101
+ const breadcrumb = doc.querySelector(".go-Breadcrumb");
102
+ const _headerDiv = doc.querySelector(".go-Main-header");
103
+
104
+ // Extract actual module path from breadcrumb or header
105
+ if (breadcrumb) {
106
+ const moduleLink = breadcrumb.querySelector("a[href^='/']");
107
+ if (moduleLink) {
108
+ const href = moduleLink.getAttribute("href");
109
+ if (href) {
110
+ actualModulePath = href.slice(1).split("@")[0];
111
+ }
112
+ }
113
+ }
114
+
115
+ // Extract version if not from proxy
116
+ if (!moduleInfo) {
117
+ const versionBadge = doc.querySelector(".go-Chip");
118
+ if (versionBadge) {
119
+ const versionText = versionBadge.textContent?.trim();
120
+ if (versionText?.startsWith("v")) {
121
+ version = versionText;
122
+ }
123
+ }
124
+ }
125
+
126
+ // Extract license
127
+ const licenseLink = doc.querySelector("a[data-test-id='UnitHeader-license']");
128
+ const license = licenseLink?.textContent?.trim() || "Unknown";
129
+
130
+ // Extract import path
131
+ const importPathInput = doc.querySelector("input[data-test-id='UnitHeader-importPath']");
132
+ const importPath = importPathInput?.getAttribute("value") || actualModulePath;
133
+
134
+ // Build header
135
+ sections.push(`# ${importPath}`);
136
+ sections.push("");
137
+ sections.push(`**Module:** ${actualModulePath}`);
138
+ sections.push(`**Version:** ${version}`);
139
+ sections.push(`**License:** ${license}`);
140
+ sections.push("");
141
+
142
+ // Extract package synopsis
143
+ const synopsis = doc.querySelector(".go-Main-headerContent p");
144
+ if (synopsis) {
145
+ const synopsisText = synopsis.textContent?.trim();
146
+ if (synopsisText) {
147
+ sections.push(`## Synopsis`);
148
+ sections.push("");
149
+ sections.push(synopsisText);
150
+ sections.push("");
151
+ }
152
+ }
153
+
154
+ // Extract documentation overview
155
+ const docSection = doc.querySelector("#section-documentation");
156
+ if (docSection) {
157
+ sections.push("## Documentation");
158
+ sections.push("");
159
+
160
+ // Get overview paragraph
161
+ const overview = docSection.querySelector(".go-Message");
162
+ if (overview) {
163
+ const overviewMd = htmlToBasicMarkdown(overview.innerHTML);
164
+ sections.push(overviewMd);
165
+ sections.push("");
166
+ }
167
+
168
+ // Get package-level documentation
169
+ const docContent = docSection.querySelector(".Documentation-content");
170
+ if (docContent) {
171
+ // Extract first few paragraphs
172
+ const paragraphs = docContent.querySelectorAll("p");
173
+ const docParts: string[] = [];
174
+ for (let i = 0; i < Math.min(3, paragraphs.length); i++) {
175
+ const p = paragraphs[i];
176
+ const text = htmlToBasicMarkdown(p.innerHTML).trim();
177
+ if (text) {
178
+ docParts.push(text);
179
+ }
180
+ }
181
+
182
+ if (docParts.length > 0) {
183
+ sections.push(docParts.join("\n\n"));
184
+ sections.push("");
185
+ }
186
+ }
187
+ }
188
+
189
+ // Extract index of exported identifiers
190
+ const indexSection = doc.querySelector("#section-index");
191
+ if (indexSection) {
192
+ const indexList = indexSection.querySelector(".Documentation-indexList");
193
+ if (indexList) {
194
+ sections.push("## Index");
195
+ sections.push("");
196
+
197
+ const items = indexList.querySelectorAll("li");
198
+ const exported: string[] = [];
199
+
200
+ for (const item of items) {
201
+ const link = item.querySelector("a");
202
+ if (link) {
203
+ const name = link.textContent?.trim();
204
+ if (name) {
205
+ exported.push(`- ${name}`);
206
+ }
207
+ }
208
+ }
209
+
210
+ if (exported.length > 0) {
211
+ // Limit to first 50 exports
212
+ sections.push(exported.slice(0, 50).join("\n"));
213
+ if (exported.length > 50) {
214
+ notes.push(`showing 50 of ${exported.length} exports`);
215
+ sections.push(`\n... and ${exported.length - 50} more`);
216
+ }
217
+ sections.push("");
218
+ }
219
+ }
220
+ }
221
+
222
+ // Extract dependencies/imports
223
+ const importsSection = doc.querySelector("#section-imports");
224
+ if (importsSection) {
225
+ const importsList = importsSection.querySelector(".go-Message");
226
+ if (importsList) {
227
+ sections.push("## Imports");
228
+ sections.push("");
229
+
230
+ const links = importsList.querySelectorAll("a");
231
+ const imports: string[] = [];
232
+
233
+ for (const link of links) {
234
+ const imp = link.textContent?.trim();
235
+ if (imp) {
236
+ imports.push(`- ${imp}`);
237
+ }
238
+ }
239
+
240
+ if (imports.length > 0) {
241
+ sections.push(imports.slice(0, 20).join("\n"));
242
+ if (imports.length > 20) {
243
+ notes.push(`showing 20 of ${imports.length} imports`);
244
+ sections.push(`\n... and ${imports.length - 20} more`);
245
+ }
246
+ sections.push("");
247
+ }
248
+ }
249
+ }
250
+
251
+ if (moduleInfo) {
252
+ notes.push(`published ${moduleInfo.Time}`);
253
+ }
254
+
255
+ const content = sections.join("\n");
256
+ const { content: finalContent, truncated } = finalizeOutput(content);
257
+
258
+ return {
259
+ url,
260
+ finalUrl: pageResult.finalUrl,
261
+ contentType: "text/markdown",
262
+ method: "go-pkg",
263
+ content: finalContent,
264
+ fetchedAt: new Date().toISOString(),
265
+ truncated,
266
+ notes,
267
+ };
268
+ } catch {
269
+ return null;
270
+ }
271
+ };