@oh-my-pi/pi-coding-agent 3.25.0 → 3.31.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 (157) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/package.json +5 -5
  3. package/src/cli/args.ts +4 -0
  4. package/src/core/agent-session.ts +29 -2
  5. package/src/core/bash-executor.ts +2 -1
  6. package/src/core/custom-commands/bundled/review/index.ts +369 -14
  7. package/src/core/custom-commands/bundled/wt/index.ts +1 -1
  8. package/src/core/session-manager.ts +158 -246
  9. package/src/core/session-storage.ts +379 -0
  10. package/src/core/settings-manager.ts +155 -4
  11. package/src/core/system-prompt.ts +62 -64
  12. package/src/core/tools/ask.ts +5 -4
  13. package/src/core/tools/bash-interceptor.ts +26 -61
  14. package/src/core/tools/bash.ts +13 -8
  15. package/src/core/tools/complete.ts +2 -4
  16. package/src/core/tools/edit-diff.ts +11 -4
  17. package/src/core/tools/edit.ts +7 -13
  18. package/src/core/tools/find.ts +111 -50
  19. package/src/core/tools/gemini-image.ts +128 -147
  20. package/src/core/tools/grep.ts +397 -415
  21. package/src/core/tools/index.test.ts +5 -1
  22. package/src/core/tools/index.ts +6 -8
  23. package/src/core/tools/jtd-to-json-schema.ts +174 -196
  24. package/src/core/tools/ls.ts +12 -10
  25. package/src/core/tools/lsp/client.ts +58 -9
  26. package/src/core/tools/lsp/config.ts +205 -656
  27. package/src/core/tools/lsp/defaults.json +465 -0
  28. package/src/core/tools/lsp/index.ts +55 -32
  29. package/src/core/tools/lsp/rust-analyzer.ts +49 -10
  30. package/src/core/tools/lsp/types.ts +1 -0
  31. package/src/core/tools/lsp/utils.ts +1 -1
  32. package/src/core/tools/read.ts +152 -76
  33. package/src/core/tools/render-utils.ts +70 -10
  34. package/src/core/tools/review.ts +38 -126
  35. package/src/core/tools/task/artifacts.ts +5 -4
  36. package/src/core/tools/task/executor.ts +204 -67
  37. package/src/core/tools/task/index.ts +129 -92
  38. package/src/core/tools/task/name-generator.ts +1544 -214
  39. package/src/core/tools/task/parallel.ts +30 -3
  40. package/src/core/tools/task/render.ts +85 -39
  41. package/src/core/tools/task/types.ts +34 -11
  42. package/src/core/tools/task/worker.ts +152 -27
  43. package/src/core/tools/web-fetch.ts +220 -1657
  44. package/src/core/tools/web-scrapers/academic.test.ts +239 -0
  45. package/src/core/tools/web-scrapers/artifacthub.ts +215 -0
  46. package/src/core/tools/web-scrapers/arxiv.ts +88 -0
  47. package/src/core/tools/web-scrapers/aur.ts +175 -0
  48. package/src/core/tools/web-scrapers/biorxiv.ts +141 -0
  49. package/src/core/tools/web-scrapers/bluesky.ts +284 -0
  50. package/src/core/tools/web-scrapers/brew.ts +177 -0
  51. package/src/core/tools/web-scrapers/business.test.ts +82 -0
  52. package/src/core/tools/web-scrapers/cheatsh.ts +78 -0
  53. package/src/core/tools/web-scrapers/chocolatey.ts +158 -0
  54. package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
  55. package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
  56. package/src/core/tools/web-scrapers/clojars.ts +180 -0
  57. package/src/core/tools/web-scrapers/coingecko.ts +184 -0
  58. package/src/core/tools/web-scrapers/crates-io.ts +128 -0
  59. package/src/core/tools/web-scrapers/crossref.ts +149 -0
  60. package/src/core/tools/web-scrapers/dev-platforms.test.ts +254 -0
  61. package/src/core/tools/web-scrapers/devto.ts +177 -0
  62. package/src/core/tools/web-scrapers/discogs.ts +308 -0
  63. package/src/core/tools/web-scrapers/discourse.ts +221 -0
  64. package/src/core/tools/web-scrapers/dockerhub.ts +160 -0
  65. package/src/core/tools/web-scrapers/documentation.test.ts +85 -0
  66. package/src/core/tools/web-scrapers/fdroid.ts +158 -0
  67. package/src/core/tools/web-scrapers/finance-media.test.ts +144 -0
  68. package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
  69. package/src/core/tools/web-scrapers/flathub.ts +239 -0
  70. package/src/core/tools/web-scrapers/git-hosting.test.ts +272 -0
  71. package/src/core/tools/web-scrapers/github-gist.ts +68 -0
  72. package/src/core/tools/web-scrapers/github.ts +455 -0
  73. package/src/core/tools/web-scrapers/gitlab.ts +456 -0
  74. package/src/core/tools/web-scrapers/go-pkg.ts +275 -0
  75. package/src/core/tools/web-scrapers/hackage.ts +94 -0
  76. package/src/core/tools/web-scrapers/hackernews.ts +208 -0
  77. package/src/core/tools/web-scrapers/hex.ts +121 -0
  78. package/src/core/tools/web-scrapers/huggingface.ts +385 -0
  79. package/src/core/tools/web-scrapers/iacr.ts +86 -0
  80. package/src/core/tools/web-scrapers/index.ts +250 -0
  81. package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
  82. package/src/core/tools/web-scrapers/lemmy.ts +220 -0
  83. package/src/core/tools/web-scrapers/lobsters.ts +186 -0
  84. package/src/core/tools/web-scrapers/mastodon.ts +310 -0
  85. package/src/core/tools/web-scrapers/maven.ts +152 -0
  86. package/src/core/tools/web-scrapers/mdn.ts +174 -0
  87. package/src/core/tools/web-scrapers/media.test.ts +138 -0
  88. package/src/core/tools/web-scrapers/metacpan.ts +253 -0
  89. package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
  90. package/src/core/tools/web-scrapers/npm.ts +114 -0
  91. package/src/core/tools/web-scrapers/nuget.ts +205 -0
  92. package/src/core/tools/web-scrapers/nvd.ts +243 -0
  93. package/src/core/tools/web-scrapers/ollama.ts +267 -0
  94. package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
  95. package/src/core/tools/web-scrapers/opencorporates.ts +275 -0
  96. package/src/core/tools/web-scrapers/openlibrary.ts +319 -0
  97. package/src/core/tools/web-scrapers/orcid.ts +299 -0
  98. package/src/core/tools/web-scrapers/osv.ts +189 -0
  99. package/src/core/tools/web-scrapers/package-managers-2.test.ts +199 -0
  100. package/src/core/tools/web-scrapers/package-managers.test.ts +171 -0
  101. package/src/core/tools/web-scrapers/package-registries.test.ts +259 -0
  102. package/src/core/tools/web-scrapers/packagist.ts +174 -0
  103. package/src/core/tools/web-scrapers/pub-dev.ts +185 -0
  104. package/src/core/tools/web-scrapers/pubmed.ts +178 -0
  105. package/src/core/tools/web-scrapers/pypi.ts +129 -0
  106. package/src/core/tools/web-scrapers/rawg.ts +124 -0
  107. package/src/core/tools/web-scrapers/readthedocs.ts +126 -0
  108. package/src/core/tools/web-scrapers/reddit.ts +104 -0
  109. package/src/core/tools/web-scrapers/repology.ts +262 -0
  110. package/src/core/tools/web-scrapers/research.test.ts +107 -0
  111. package/src/core/tools/web-scrapers/rfc.ts +209 -0
  112. package/src/core/tools/web-scrapers/rubygems.ts +117 -0
  113. package/src/core/tools/web-scrapers/searchcode.ts +217 -0
  114. package/src/core/tools/web-scrapers/sec-edgar.ts +274 -0
  115. package/src/core/tools/web-scrapers/security.test.ts +103 -0
  116. package/src/core/tools/web-scrapers/semantic-scholar.ts +190 -0
  117. package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
  118. package/src/core/tools/web-scrapers/social-extended.test.ts +192 -0
  119. package/src/core/tools/web-scrapers/social.test.ts +259 -0
  120. package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
  121. package/src/core/tools/web-scrapers/spdx.ts +121 -0
  122. package/src/core/tools/web-scrapers/spotify.ts +218 -0
  123. package/src/core/tools/web-scrapers/stackexchange.test.ts +120 -0
  124. package/src/core/tools/web-scrapers/stackoverflow.ts +124 -0
  125. package/src/core/tools/web-scrapers/standards.test.ts +122 -0
  126. package/src/core/tools/web-scrapers/terraform.ts +304 -0
  127. package/src/core/tools/web-scrapers/tldr.ts +51 -0
  128. package/src/core/tools/web-scrapers/twitter.ts +96 -0
  129. package/src/core/tools/web-scrapers/types.ts +234 -0
  130. package/src/core/tools/web-scrapers/utils.ts +162 -0
  131. package/src/core/tools/web-scrapers/vimeo.ts +152 -0
  132. package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
  133. package/src/core/tools/web-scrapers/w3c.ts +163 -0
  134. package/src/core/tools/web-scrapers/wikidata.ts +357 -0
  135. package/src/core/tools/web-scrapers/wikipedia.test.ts +73 -0
  136. package/src/core/tools/web-scrapers/wikipedia.ts +95 -0
  137. package/src/core/tools/web-scrapers/youtube.test.ts +198 -0
  138. package/src/core/tools/web-scrapers/youtube.ts +371 -0
  139. package/src/core/tools/write.ts +21 -18
  140. package/src/core/voice.ts +3 -2
  141. package/src/lib/worktree/collapse.ts +2 -1
  142. package/src/lib/worktree/git.ts +2 -18
  143. package/src/main.ts +59 -3
  144. package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
  145. package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
  146. package/src/modes/interactive/components/hook-editor.ts +2 -1
  147. package/src/modes/interactive/components/model-selector.ts +19 -4
  148. package/src/modes/interactive/interactive-mode.ts +41 -38
  149. package/src/modes/interactive/theme/theme.ts +58 -58
  150. package/src/modes/rpc/rpc-mode.ts +10 -9
  151. package/src/prompts/review-request.md +27 -0
  152. package/src/prompts/reviewer.md +64 -68
  153. package/src/prompts/tools/output.md +22 -3
  154. package/src/prompts/tools/task.md +32 -33
  155. package/src/utils/clipboard.ts +2 -1
  156. package/src/utils/tools-manager.ts +110 -8
  157. package/examples/extensions/subagent/agents/reviewer.md +0 -35
@@ -0,0 +1,456 @@
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, signal?: AbortSignal): 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, signal });
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(
108
+ gl: GitLabUrl,
109
+ timeout: number,
110
+ signal?: AbortSignal,
111
+ ): Promise<{ content: string; ok: boolean }> {
112
+ const encodedPath = encodeURIComponent(`${gl.namespace}/${gl.project}`);
113
+ const apiUrl = `https://gitlab.com/api/v4/projects/${encodedPath}`;
114
+
115
+ const result = await loadPage(apiUrl, { timeout, signal });
116
+ if (!result.ok) return { content: "", ok: false };
117
+
118
+ try {
119
+ const repo = JSON.parse(result.content) as {
120
+ name: string;
121
+ description?: string;
122
+ star_count: number;
123
+ forks_count: number;
124
+ open_issues_count: number;
125
+ default_branch: string;
126
+ visibility: string;
127
+ created_at: string;
128
+ last_activity_at: string;
129
+ topics?: string[];
130
+ readme_url?: string;
131
+ };
132
+
133
+ let md = `# ${repo.name}\n\n`;
134
+ if (repo.description) md += `${repo.description}\n\n`;
135
+ md += `**Stars:** ${formatCount(repo.star_count)} · **Forks:** ${formatCount(repo.forks_count)} · **Issues:** ${formatCount(repo.open_issues_count)}\n`;
136
+ md += `**Visibility:** ${repo.visibility} · **Default Branch:** ${repo.default_branch}\n`;
137
+ if (repo.topics && repo.topics.length > 0) {
138
+ md += `**Topics:** ${repo.topics.join(", ")}\n`;
139
+ }
140
+ 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`;
141
+
142
+ // Try to fetch README
143
+ if (repo.readme_url) {
144
+ const readmeResult = await loadPage(repo.readme_url, { timeout, signal });
145
+ if (readmeResult.ok && readmeResult.content.trim().length > 0) {
146
+ md += `---\n\n## README\n\n${readmeResult.content}\n`;
147
+ }
148
+ }
149
+
150
+ return { content: md, ok: true };
151
+ } catch {
152
+ return { content: "", ok: false };
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Render GitLab file
158
+ */
159
+ async function renderGitLabFile(
160
+ gl: GitLabUrl,
161
+ projectId: number,
162
+ timeout: number,
163
+ signal?: AbortSignal,
164
+ ): Promise<{ content: string; ok: boolean }> {
165
+ const encodedPath = encodeURIComponent(gl.path!);
166
+ const apiUrl = `https://gitlab.com/api/v4/projects/${projectId}/repository/files/${encodedPath}/raw?ref=${gl.ref}`;
167
+
168
+ const result = await loadPage(apiUrl, { timeout, signal });
169
+ if (!result.ok) return { content: "", ok: false };
170
+
171
+ return { content: result.content, ok: true };
172
+ }
173
+
174
+ /**
175
+ * Render GitLab directory tree
176
+ */
177
+ async function renderGitLabTree(
178
+ gl: GitLabUrl,
179
+ projectId: number,
180
+ timeout: number,
181
+ signal?: AbortSignal,
182
+ ): Promise<{ content: string; ok: boolean }> {
183
+ const apiUrl = `https://gitlab.com/api/v4/projects/${projectId}/repository/tree?ref=${gl.ref}&path=${gl.path || ""}&per_page=100`;
184
+
185
+ const result = await loadPage(apiUrl, { timeout, signal });
186
+ if (!result.ok) return { content: "", ok: false };
187
+
188
+ try {
189
+ const tree = JSON.parse(result.content) as Array<{
190
+ name: string;
191
+ type: "tree" | "blob";
192
+ path: string;
193
+ mode: string;
194
+ }>;
195
+
196
+ let md = `# Directory: ${gl.path || "/"}\n\n`;
197
+ md += `**Ref:** ${gl.ref}\n\n`;
198
+
199
+ // Separate directories and files
200
+ const dirs = tree.filter((item) => item.type === "tree");
201
+ const files = tree.filter((item) => item.type === "blob");
202
+
203
+ if (dirs.length > 0) {
204
+ md += `## Directories (${dirs.length})\n\n`;
205
+ for (const dir of dirs) {
206
+ md += `- 📁 ${dir.name}/\n`;
207
+ }
208
+ md += `\n`;
209
+ }
210
+
211
+ if (files.length > 0) {
212
+ md += `## Files (${files.length})\n\n`;
213
+ for (const file of files) {
214
+ md += `- 📄 ${file.name}\n`;
215
+ }
216
+ }
217
+
218
+ return { content: md, ok: true };
219
+ } catch {
220
+ return { content: "", ok: false };
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Render GitLab issue
226
+ */
227
+ async function renderGitLabIssue(
228
+ gl: GitLabUrl,
229
+ projectId: number,
230
+ timeout: number,
231
+ signal?: AbortSignal,
232
+ ): Promise<{ content: string; ok: boolean }> {
233
+ const apiUrl = `https://gitlab.com/api/v4/projects/${projectId}/issues/${gl.id}`;
234
+
235
+ const result = await loadPage(apiUrl, { timeout, signal });
236
+ if (!result.ok) return { content: "", ok: false };
237
+
238
+ try {
239
+ const issue = JSON.parse(result.content) as {
240
+ title: string;
241
+ description?: string;
242
+ state: string;
243
+ author: { name: string; username: string };
244
+ created_at: string;
245
+ updated_at: string;
246
+ labels: string[];
247
+ upvotes: number;
248
+ downvotes: number;
249
+ user_notes_count: number;
250
+ assignees?: Array<{ name: string }>;
251
+ };
252
+
253
+ let md = `# Issue #${gl.id}: ${issue.title}\n\n`;
254
+ md += `**State:** ${issue.state.toUpperCase()} · **Author:** ${issue.author.name} (@${issue.author.username})\n`;
255
+ md += `**Created:** ${new Date(issue.created_at).toISOString().split("T")[0]} · **Updated:** ${new Date(issue.updated_at).toISOString().split("T")[0]}\n`;
256
+ md += `**Upvotes:** ${issue.upvotes} · **Downvotes:** ${issue.downvotes} · **Comments:** ${issue.user_notes_count}\n`;
257
+
258
+ if (issue.labels.length > 0) {
259
+ md += `**Labels:** ${issue.labels.join(", ")}\n`;
260
+ }
261
+
262
+ if (issue.assignees && issue.assignees.length > 0) {
263
+ md += `**Assignees:** ${issue.assignees.map((a) => a.name).join(", ")}\n`;
264
+ }
265
+
266
+ md += `\n---\n\n## Description\n\n`;
267
+ md += issue.description ? htmlToBasicMarkdown(issue.description) : "*No description*";
268
+
269
+ return { content: md, ok: true };
270
+ } catch {
271
+ return { content: "", ok: false };
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Render GitLab merge request
277
+ */
278
+ async function renderGitLabMR(
279
+ gl: GitLabUrl,
280
+ projectId: number,
281
+ timeout: number,
282
+ signal?: AbortSignal,
283
+ ): Promise<{ content: string; ok: boolean }> {
284
+ const apiUrl = `https://gitlab.com/api/v4/projects/${projectId}/merge_requests/${gl.id}`;
285
+
286
+ const result = await loadPage(apiUrl, { timeout, signal });
287
+ if (!result.ok) return { content: "", ok: false };
288
+
289
+ try {
290
+ const mr = JSON.parse(result.content) as {
291
+ title: string;
292
+ description?: string;
293
+ state: string;
294
+ author: { name: string; username: string };
295
+ created_at: string;
296
+ updated_at: string;
297
+ source_branch: string;
298
+ target_branch: string;
299
+ labels: string[];
300
+ upvotes: number;
301
+ downvotes: number;
302
+ user_notes_count: number;
303
+ assignees?: Array<{ name: string }>;
304
+ draft: boolean;
305
+ merge_status: string;
306
+ };
307
+
308
+ let md = `# MR !${gl.id}: ${mr.title}\n\n`;
309
+ if (mr.draft) md += `**[DRAFT]** `;
310
+ md += `**State:** ${mr.state.toUpperCase()} · **Author:** ${mr.author.name} (@${mr.author.username})\n`;
311
+ md += `**Branch:** ${mr.source_branch} → ${mr.target_branch}\n`;
312
+ md += `**Created:** ${new Date(mr.created_at).toISOString().split("T")[0]} · **Updated:** ${new Date(mr.updated_at).toISOString().split("T")[0]}\n`;
313
+ md += `**Merge Status:** ${mr.merge_status} · **Upvotes:** ${mr.upvotes} · **Downvotes:** ${mr.downvotes} · **Comments:** ${mr.user_notes_count}\n`;
314
+
315
+ if (mr.labels.length > 0) {
316
+ md += `**Labels:** ${mr.labels.join(", ")}\n`;
317
+ }
318
+
319
+ if (mr.assignees && mr.assignees.length > 0) {
320
+ md += `**Assignees:** ${mr.assignees.map((a) => a.name).join(", ")}\n`;
321
+ }
322
+
323
+ md += `\n---\n\n## Description\n\n`;
324
+ md += mr.description ? htmlToBasicMarkdown(mr.description) : "*No description*";
325
+
326
+ return { content: md, ok: true };
327
+ } catch {
328
+ return { content: "", ok: false };
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Handle GitLab URLs specially
334
+ */
335
+ export const handleGitLab: SpecialHandler = async (
336
+ url: string,
337
+ timeout: number,
338
+ signal?: AbortSignal,
339
+ ): Promise<RenderResult | null> => {
340
+ const gl = parseGitLabUrl(url);
341
+ if (!gl) return null;
342
+
343
+ const fetchedAt = new Date().toISOString();
344
+ const notes: string[] = [];
345
+
346
+ switch (gl.type) {
347
+ case "blob": {
348
+ const projectId = await getProjectId(gl, timeout, signal);
349
+ if (!projectId) break;
350
+
351
+ notes.push(`Fetched raw file via GitLab API`);
352
+ const result = await renderGitLabFile(gl, projectId, timeout, signal);
353
+ if (result.ok) {
354
+ const output = finalizeOutput(result.content);
355
+ return {
356
+ url,
357
+ finalUrl: url,
358
+ contentType: "text/plain",
359
+ method: "gitlab-raw",
360
+ content: output.content,
361
+ fetchedAt,
362
+ truncated: output.truncated,
363
+ notes,
364
+ };
365
+ }
366
+ break;
367
+ }
368
+
369
+ case "tree": {
370
+ const projectId = await getProjectId(gl, timeout, signal);
371
+ if (!projectId) break;
372
+
373
+ notes.push(`Fetched directory tree via GitLab API`);
374
+ const result = await renderGitLabTree(gl, projectId, timeout, signal);
375
+ if (result.ok) {
376
+ const output = finalizeOutput(result.content);
377
+ return {
378
+ url,
379
+ finalUrl: url,
380
+ contentType: "text/markdown",
381
+ method: "gitlab-tree",
382
+ content: output.content,
383
+ fetchedAt,
384
+ truncated: output.truncated,
385
+ notes,
386
+ };
387
+ }
388
+ break;
389
+ }
390
+
391
+ case "issue": {
392
+ const projectId = await getProjectId(gl, timeout, signal);
393
+ if (!projectId) break;
394
+
395
+ notes.push(`Fetched issue via GitLab API`);
396
+ const result = await renderGitLabIssue(gl, projectId, timeout, signal);
397
+ if (result.ok) {
398
+ const output = finalizeOutput(result.content);
399
+ return {
400
+ url,
401
+ finalUrl: url,
402
+ contentType: "text/markdown",
403
+ method: "gitlab-issue",
404
+ content: output.content,
405
+ fetchedAt,
406
+ truncated: output.truncated,
407
+ notes,
408
+ };
409
+ }
410
+ break;
411
+ }
412
+
413
+ case "merge_request": {
414
+ const projectId = await getProjectId(gl, timeout, signal);
415
+ if (!projectId) break;
416
+
417
+ notes.push(`Fetched merge request via GitLab API`);
418
+ const result = await renderGitLabMR(gl, projectId, timeout, signal);
419
+ if (result.ok) {
420
+ const output = finalizeOutput(result.content);
421
+ return {
422
+ url,
423
+ finalUrl: url,
424
+ contentType: "text/markdown",
425
+ method: "gitlab-mr",
426
+ content: output.content,
427
+ fetchedAt,
428
+ truncated: output.truncated,
429
+ notes,
430
+ };
431
+ }
432
+ break;
433
+ }
434
+
435
+ case "repo": {
436
+ notes.push(`Fetched repository via GitLab API`);
437
+ const result = await renderGitLabRepo(gl, timeout, signal);
438
+ if (result.ok) {
439
+ const output = finalizeOutput(result.content);
440
+ return {
441
+ url,
442
+ finalUrl: url,
443
+ contentType: "text/markdown",
444
+ method: "gitlab-repo",
445
+ content: output.content,
446
+ fetchedAt,
447
+ truncated: output.truncated,
448
+ notes,
449
+ };
450
+ }
451
+ break;
452
+ }
453
+ }
454
+
455
+ return null;
456
+ };