@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.
- package/CHANGELOG.md +19 -0
- package/package.json +4 -4
- package/src/core/tools/complete.ts +2 -4
- package/src/core/tools/jtd-to-json-schema.ts +174 -196
- package/src/core/tools/read.ts +4 -4
- package/src/core/tools/task/executor.ts +146 -20
- package/src/core/tools/task/name-generator.ts +1544 -214
- package/src/core/tools/task/types.ts +19 -5
- package/src/core/tools/task/worker.ts +103 -13
- package/src/core/tools/web-fetch-handlers/academic.test.ts +239 -0
- package/src/core/tools/web-fetch-handlers/artifacthub.ts +210 -0
- package/src/core/tools/web-fetch-handlers/arxiv.ts +84 -0
- package/src/core/tools/web-fetch-handlers/aur.ts +171 -0
- package/src/core/tools/web-fetch-handlers/biorxiv.ts +136 -0
- package/src/core/tools/web-fetch-handlers/bluesky.ts +277 -0
- package/src/core/tools/web-fetch-handlers/brew.ts +173 -0
- package/src/core/tools/web-fetch-handlers/business.test.ts +82 -0
- package/src/core/tools/web-fetch-handlers/cheatsh.ts +73 -0
- package/src/core/tools/web-fetch-handlers/chocolatey.ts +153 -0
- package/src/core/tools/web-fetch-handlers/coingecko.ts +179 -0
- package/src/core/tools/web-fetch-handlers/crates-io.ts +123 -0
- package/src/core/tools/web-fetch-handlers/dev-platforms.test.ts +254 -0
- package/src/core/tools/web-fetch-handlers/devto.ts +173 -0
- package/src/core/tools/web-fetch-handlers/discogs.ts +303 -0
- package/src/core/tools/web-fetch-handlers/dockerhub.ts +156 -0
- package/src/core/tools/web-fetch-handlers/documentation.test.ts +85 -0
- package/src/core/tools/web-fetch-handlers/finance-media.test.ts +144 -0
- package/src/core/tools/web-fetch-handlers/git-hosting.test.ts +272 -0
- package/src/core/tools/web-fetch-handlers/github-gist.ts +64 -0
- package/src/core/tools/web-fetch-handlers/github.ts +424 -0
- package/src/core/tools/web-fetch-handlers/gitlab.ts +444 -0
- package/src/core/tools/web-fetch-handlers/go-pkg.ts +271 -0
- package/src/core/tools/web-fetch-handlers/hackage.ts +89 -0
- package/src/core/tools/web-fetch-handlers/hackernews.ts +208 -0
- package/src/core/tools/web-fetch-handlers/hex.ts +121 -0
- package/src/core/tools/web-fetch-handlers/huggingface.ts +385 -0
- package/src/core/tools/web-fetch-handlers/iacr.ts +82 -0
- package/src/core/tools/web-fetch-handlers/index.ts +69 -0
- package/src/core/tools/web-fetch-handlers/lobsters.ts +186 -0
- package/src/core/tools/web-fetch-handlers/mastodon.ts +302 -0
- package/src/core/tools/web-fetch-handlers/maven.ts +147 -0
- package/src/core/tools/web-fetch-handlers/mdn.ts +174 -0
- package/src/core/tools/web-fetch-handlers/media.test.ts +138 -0
- package/src/core/tools/web-fetch-handlers/metacpan.ts +247 -0
- package/src/core/tools/web-fetch-handlers/npm.ts +107 -0
- package/src/core/tools/web-fetch-handlers/nuget.ts +201 -0
- package/src/core/tools/web-fetch-handlers/nvd.ts +238 -0
- package/src/core/tools/web-fetch-handlers/opencorporates.ts +273 -0
- package/src/core/tools/web-fetch-handlers/openlibrary.ts +313 -0
- package/src/core/tools/web-fetch-handlers/osv.ts +184 -0
- package/src/core/tools/web-fetch-handlers/package-managers-2.test.ts +199 -0
- package/src/core/tools/web-fetch-handlers/package-managers.test.ts +171 -0
- package/src/core/tools/web-fetch-handlers/package-registries.test.ts +259 -0
- package/src/core/tools/web-fetch-handlers/packagist.ts +170 -0
- package/src/core/tools/web-fetch-handlers/pub-dev.ts +185 -0
- package/src/core/tools/web-fetch-handlers/pubmed.ts +174 -0
- package/src/core/tools/web-fetch-handlers/pypi.ts +125 -0
- package/src/core/tools/web-fetch-handlers/readthedocs.ts +122 -0
- package/src/core/tools/web-fetch-handlers/reddit.ts +100 -0
- package/src/core/tools/web-fetch-handlers/repology.ts +257 -0
- package/src/core/tools/web-fetch-handlers/research.test.ts +107 -0
- package/src/core/tools/web-fetch-handlers/rfc.ts +205 -0
- package/src/core/tools/web-fetch-handlers/rubygems.ts +112 -0
- package/src/core/tools/web-fetch-handlers/sec-edgar.ts +269 -0
- package/src/core/tools/web-fetch-handlers/security.test.ts +103 -0
- package/src/core/tools/web-fetch-handlers/semantic-scholar.ts +190 -0
- package/src/core/tools/web-fetch-handlers/social-extended.test.ts +192 -0
- package/src/core/tools/web-fetch-handlers/social.test.ts +259 -0
- package/src/core/tools/web-fetch-handlers/spotify.ts +218 -0
- package/src/core/tools/web-fetch-handlers/stackexchange.test.ts +120 -0
- package/src/core/tools/web-fetch-handlers/stackoverflow.ts +123 -0
- package/src/core/tools/web-fetch-handlers/standards.test.ts +122 -0
- package/src/core/tools/web-fetch-handlers/terraform.ts +296 -0
- package/src/core/tools/web-fetch-handlers/tldr.ts +47 -0
- package/src/core/tools/web-fetch-handlers/twitter.ts +84 -0
- package/src/core/tools/web-fetch-handlers/types.ts +163 -0
- package/src/core/tools/web-fetch-handlers/utils.ts +91 -0
- package/src/core/tools/web-fetch-handlers/vimeo.ts +152 -0
- package/src/core/tools/web-fetch-handlers/wikidata.ts +349 -0
- package/src/core/tools/web-fetch-handlers/wikipedia.test.ts +73 -0
- package/src/core/tools/web-fetch-handlers/wikipedia.ts +91 -0
- package/src/core/tools/web-fetch-handlers/youtube.test.ts +198 -0
- package/src/core/tools/web-fetch-handlers/youtube.ts +319 -0
- package/src/core/tools/web-fetch.ts +152 -1324
- 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
|
+
};
|