@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,424 @@
1
+ import type { RenderResult, SpecialHandler } from "./types";
2
+ import { finalizeOutput, loadPage } from "./types";
3
+
4
+ interface GitHubUrl {
5
+ type: "blob" | "tree" | "repo" | "issue" | "issues" | "pull" | "pulls" | "discussion" | "discussions" | "other";
6
+ owner: string;
7
+ repo: string;
8
+ ref?: string;
9
+ path?: string;
10
+ number?: number;
11
+ }
12
+
13
+ /**
14
+ * Parse GitHub URL into components
15
+ */
16
+ function parseGitHubUrl(url: string): GitHubUrl | null {
17
+ try {
18
+ const parsed = new URL(url);
19
+ if (parsed.hostname !== "github.com") return null;
20
+
21
+ const parts = parsed.pathname.split("/").filter(Boolean);
22
+ if (parts.length < 2) return null;
23
+
24
+ const [owner, repo, ...rest] = parts;
25
+
26
+ if (rest.length === 0) {
27
+ return { type: "repo", owner, repo };
28
+ }
29
+
30
+ const [section, ...subParts] = rest;
31
+
32
+ switch (section) {
33
+ case "blob":
34
+ case "tree": {
35
+ const [ref, ...pathParts] = subParts;
36
+ return { type: section, owner, repo, ref, path: pathParts.join("/") };
37
+ }
38
+ case "issues":
39
+ if (subParts.length > 0 && /^\d+$/.test(subParts[0])) {
40
+ return { type: "issue", owner, repo, number: parseInt(subParts[0], 10) };
41
+ }
42
+ return { type: "issues", owner, repo };
43
+ case "pull":
44
+ if (subParts.length > 0 && /^\d+$/.test(subParts[0])) {
45
+ return { type: "pull", owner, repo, number: parseInt(subParts[0], 10) };
46
+ }
47
+ return { type: "pulls", owner, repo };
48
+ case "pulls":
49
+ return { type: "pulls", owner, repo };
50
+ case "discussions":
51
+ if (subParts.length > 0 && /^\d+$/.test(subParts[0])) {
52
+ return { type: "discussion", owner, repo, number: parseInt(subParts[0], 10) };
53
+ }
54
+ return { type: "discussions", owner, repo };
55
+ default:
56
+ return { type: "other", owner, repo };
57
+ }
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Convert GitHub blob URL to raw URL
65
+ */
66
+ function toRawGitHubUrl(gh: GitHubUrl): string {
67
+ return `https://raw.githubusercontent.com/${gh.owner}/${gh.repo}/refs/heads/${gh.ref}/${gh.path}`;
68
+ }
69
+
70
+ /**
71
+ * Fetch from GitHub API
72
+ */
73
+ export async function fetchGitHubApi(endpoint: string, timeout: number): Promise<{ data: unknown; ok: boolean }> {
74
+ try {
75
+ const controller = new AbortController();
76
+ const timeoutId = setTimeout(() => controller.abort(), timeout * 1000);
77
+
78
+ const headers: Record<string, string> = {
79
+ Accept: "application/vnd.github.v3+json",
80
+ "User-Agent": "omp-web-fetch/1.0",
81
+ };
82
+
83
+ // Use GITHUB_TOKEN if available
84
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
85
+ if (token) {
86
+ headers.Authorization = `Bearer ${token}`;
87
+ }
88
+
89
+ const response = await fetch(`https://api.github.com${endpoint}`, {
90
+ signal: controller.signal,
91
+ headers,
92
+ });
93
+
94
+ clearTimeout(timeoutId);
95
+
96
+ if (!response.ok) {
97
+ return { data: null, ok: false };
98
+ }
99
+
100
+ return { data: await response.json(), ok: true };
101
+ } catch {
102
+ return { data: null, ok: false };
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Render GitHub issue/PR to markdown
108
+ */
109
+ async function renderGitHubIssue(gh: GitHubUrl, timeout: number): Promise<{ content: string; ok: boolean }> {
110
+ const endpoint =
111
+ gh.type === "pull"
112
+ ? `/repos/${gh.owner}/${gh.repo}/pulls/${gh.number}`
113
+ : `/repos/${gh.owner}/${gh.repo}/issues/${gh.number}`;
114
+
115
+ const result = await fetchGitHubApi(endpoint, timeout);
116
+ if (!result.ok || !result.data) return { content: "", ok: false };
117
+
118
+ const issue = result.data as {
119
+ title: string;
120
+ number: number;
121
+ state: string;
122
+ user: { login: string };
123
+ created_at: string;
124
+ updated_at: string;
125
+ body: string | null;
126
+ labels: Array<{ name: string }>;
127
+ comments: number;
128
+ html_url: string;
129
+ };
130
+
131
+ let md = `# ${issue.title}\n\n`;
132
+ md += `**#${issue.number}** · ${issue.state} · opened by @${issue.user.login}\n`;
133
+ md += `Created: ${issue.created_at} · Updated: ${issue.updated_at}\n`;
134
+ if (issue.labels.length > 0) {
135
+ md += `Labels: ${issue.labels.map((l) => l.name).join(", ")}\n`;
136
+ }
137
+ md += `\n---\n\n`;
138
+ md += issue.body || "*No description provided.*";
139
+ md += `\n\n---\n\n`;
140
+
141
+ // Fetch comments if any
142
+ if (issue.comments > 0) {
143
+ const commentsResult = await fetchGitHubApi(
144
+ `/repos/${gh.owner}/${gh.repo}/issues/${gh.number}/comments?per_page=50`,
145
+ timeout,
146
+ );
147
+ if (commentsResult.ok && Array.isArray(commentsResult.data)) {
148
+ md += `## Comments (${issue.comments})\n\n`;
149
+ for (const comment of commentsResult.data as Array<{
150
+ user: { login: string };
151
+ created_at: string;
152
+ body: string;
153
+ }>) {
154
+ md += `### @${comment.user.login} · ${comment.created_at}\n\n`;
155
+ md += `${comment.body}\n\n---\n\n`;
156
+ }
157
+ }
158
+ }
159
+
160
+ return { content: md, ok: true };
161
+ }
162
+
163
+ /**
164
+ * Render GitHub issues list to markdown
165
+ */
166
+ async function renderGitHubIssuesList(gh: GitHubUrl, timeout: number): Promise<{ content: string; ok: boolean }> {
167
+ const result = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}/issues?state=open&per_page=30`, timeout);
168
+ if (!result.ok || !Array.isArray(result.data)) return { content: "", ok: false };
169
+
170
+ const issues = result.data as Array<{
171
+ number: number;
172
+ title: string;
173
+ state: string;
174
+ user: { login: string };
175
+ created_at: string;
176
+ comments: number;
177
+ labels: Array<{ name: string }>;
178
+ pull_request?: unknown;
179
+ }>;
180
+
181
+ let md = `# ${gh.owner}/${gh.repo} - Open Issues\n\n`;
182
+
183
+ for (const issue of issues) {
184
+ if (issue.pull_request) continue; // Skip PRs in issues list
185
+ const labels = issue.labels.length > 0 ? ` [${issue.labels.map((l) => l.name).join(", ")}]` : "";
186
+ md += `- **#${issue.number}** ${issue.title}${labels}\n`;
187
+ md += ` by @${issue.user.login} · ${issue.comments} comments · ${issue.created_at}\n\n`;
188
+ }
189
+
190
+ return { content: md, ok: true };
191
+ }
192
+
193
+ /**
194
+ * Render GitHub tree (directory) to markdown
195
+ */
196
+ async function renderGitHubTree(gh: GitHubUrl, timeout: number): Promise<{ content: string; ok: boolean }> {
197
+ // Fetch repo info first to get default branch if ref not specified
198
+ const repoResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}`, timeout);
199
+ if (!repoResult.ok) return { content: "", ok: false };
200
+
201
+ const repo = repoResult.data as {
202
+ full_name: string;
203
+ default_branch: string;
204
+ };
205
+
206
+ const ref = gh.ref || repo.default_branch;
207
+ const dirPath = gh.path || "";
208
+
209
+ let md = `# ${repo.full_name}/${dirPath || "(root)"}\n\n`;
210
+ md += `**Branch:** ${ref}\n\n`;
211
+
212
+ // Fetch directory contents
213
+ const contentsResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}/contents/${dirPath}?ref=${ref}`, timeout);
214
+
215
+ if (contentsResult.ok && Array.isArray(contentsResult.data)) {
216
+ const items = contentsResult.data as Array<{
217
+ name: string;
218
+ type: "file" | "dir" | "symlink" | "submodule";
219
+ size?: number;
220
+ path: string;
221
+ }>;
222
+
223
+ // Sort: directories first, then files, alphabetically
224
+ items.sort((a, b) => {
225
+ if (a.type === "dir" && b.type !== "dir") return -1;
226
+ if (a.type !== "dir" && b.type === "dir") return 1;
227
+ return a.name.localeCompare(b.name);
228
+ });
229
+
230
+ md += `## Contents\n\n`;
231
+ md += "```\n";
232
+ for (const item of items) {
233
+ const prefix = item.type === "dir" ? "[dir] " : " ";
234
+ const size = item.size ? ` (${item.size} bytes)` : "";
235
+ md += `${prefix}${item.name}${item.type === "file" ? size : ""}\n`;
236
+ }
237
+ md += "```\n\n";
238
+
239
+ // Look for README in this directory
240
+ const readmeFile = items.find((item) => item.type === "file" && /^readme\.md$/i.test(item.name));
241
+ if (readmeFile) {
242
+ const readmePath = dirPath ? `${dirPath}/${readmeFile.name}` : readmeFile.name;
243
+ const rawUrl = `https://raw.githubusercontent.com/${gh.owner}/${gh.repo}/refs/heads/${ref}/${readmePath}`;
244
+ const readmeResult = await loadPage(rawUrl, { timeout });
245
+ if (readmeResult.ok) {
246
+ md += `---\n\n## README\n\n${readmeResult.content}`;
247
+ }
248
+ }
249
+ }
250
+
251
+ return { content: md, ok: true };
252
+ }
253
+
254
+ /**
255
+ * Render GitHub repo to markdown (file list + README)
256
+ */
257
+ async function renderGitHubRepo(gh: GitHubUrl, timeout: number): Promise<{ content: string; ok: boolean }> {
258
+ // Fetch repo info
259
+ const repoResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}`, timeout);
260
+ if (!repoResult.ok) return { content: "", ok: false };
261
+
262
+ const repo = repoResult.data as {
263
+ full_name: string;
264
+ description: string | null;
265
+ stargazers_count: number;
266
+ forks_count: number;
267
+ open_issues_count: number;
268
+ default_branch: string;
269
+ language: string | null;
270
+ license: { name: string } | null;
271
+ };
272
+
273
+ let md = `# ${repo.full_name}\n\n`;
274
+ if (repo.description) md += `${repo.description}\n\n`;
275
+ md += `Stars: ${repo.stargazers_count} · Forks: ${repo.forks_count} · Issues: ${repo.open_issues_count}\n`;
276
+ if (repo.language) md += `Language: ${repo.language}\n`;
277
+ if (repo.license) md += `License: ${repo.license.name}\n`;
278
+ md += `\n---\n\n`;
279
+
280
+ // Fetch file tree
281
+ const treeResult = await fetchGitHubApi(
282
+ `/repos/${gh.owner}/${gh.repo}/git/trees/${repo.default_branch}?recursive=1`,
283
+ timeout,
284
+ );
285
+ if (treeResult.ok && treeResult.data) {
286
+ const tree = (treeResult.data as { tree: Array<{ path: string; type: string }> }).tree;
287
+ md += `## Files\n\n`;
288
+ md += "```\n";
289
+ for (const item of tree.slice(0, 100)) {
290
+ const prefix = item.type === "tree" ? "[dir] " : " ";
291
+ md += `${prefix}${item.path}\n`;
292
+ }
293
+ if (tree.length > 100) {
294
+ md += `... and ${tree.length - 100} more files\n`;
295
+ }
296
+ md += "```\n\n";
297
+ }
298
+
299
+ // Fetch README
300
+ const readmeResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}/readme`, timeout);
301
+ if (readmeResult.ok && readmeResult.data) {
302
+ const readme = readmeResult.data as { content: string; encoding: string };
303
+ if (readme.encoding === "base64") {
304
+ const decoded = Buffer.from(readme.content, "base64").toString("utf-8");
305
+ md += `## README\n\n${decoded}`;
306
+ }
307
+ }
308
+
309
+ return { content: md, ok: true };
310
+ }
311
+
312
+ /**
313
+ * Handle GitHub URLs specially
314
+ */
315
+ export const handleGitHub: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
316
+ const gh = parseGitHubUrl(url);
317
+ if (!gh) return null;
318
+
319
+ const fetchedAt = new Date().toISOString();
320
+ const notes: string[] = [];
321
+
322
+ switch (gh.type) {
323
+ case "blob": {
324
+ // Convert to raw URL and fetch
325
+ const rawUrl = toRawGitHubUrl(gh);
326
+ notes.push(`Fetched raw: ${rawUrl}`);
327
+ const result = await loadPage(rawUrl, { timeout });
328
+ if (result.ok) {
329
+ const output = finalizeOutput(result.content);
330
+ return {
331
+ url,
332
+ finalUrl: rawUrl,
333
+ contentType: "text/plain",
334
+ method: "github-raw",
335
+ content: output.content,
336
+ fetchedAt,
337
+ truncated: output.truncated,
338
+ notes,
339
+ };
340
+ }
341
+ break;
342
+ }
343
+
344
+ case "tree": {
345
+ notes.push(`Fetched via GitHub API`);
346
+ const result = await renderGitHubTree(gh, timeout);
347
+ if (result.ok) {
348
+ const output = finalizeOutput(result.content);
349
+ return {
350
+ url,
351
+ finalUrl: url,
352
+ contentType: "text/markdown",
353
+ method: "github-tree",
354
+ content: output.content,
355
+ fetchedAt,
356
+ truncated: output.truncated,
357
+ notes,
358
+ };
359
+ }
360
+ break;
361
+ }
362
+
363
+ case "issue":
364
+ case "pull": {
365
+ notes.push(`Fetched via GitHub API`);
366
+ const result = await renderGitHubIssue(gh, timeout);
367
+ if (result.ok) {
368
+ const output = finalizeOutput(result.content);
369
+ return {
370
+ url,
371
+ finalUrl: url,
372
+ contentType: "text/markdown",
373
+ method: gh.type === "pull" ? "github-pr" : "github-issue",
374
+ content: output.content,
375
+ fetchedAt,
376
+ truncated: output.truncated,
377
+ notes,
378
+ };
379
+ }
380
+ break;
381
+ }
382
+
383
+ case "issues": {
384
+ notes.push(`Fetched via GitHub API`);
385
+ const result = await renderGitHubIssuesList(gh, timeout);
386
+ if (result.ok) {
387
+ const output = finalizeOutput(result.content);
388
+ return {
389
+ url,
390
+ finalUrl: url,
391
+ contentType: "text/markdown",
392
+ method: "github-issues",
393
+ content: output.content,
394
+ fetchedAt,
395
+ truncated: output.truncated,
396
+ notes,
397
+ };
398
+ }
399
+ break;
400
+ }
401
+
402
+ case "repo": {
403
+ notes.push(`Fetched via GitHub API`);
404
+ const result = await renderGitHubRepo(gh, timeout);
405
+ if (result.ok) {
406
+ const output = finalizeOutput(result.content);
407
+ return {
408
+ url,
409
+ finalUrl: url,
410
+ contentType: "text/markdown",
411
+ method: "github-repo",
412
+ content: output.content,
413
+ fetchedAt,
414
+ truncated: output.truncated,
415
+ notes,
416
+ };
417
+ }
418
+ break;
419
+ }
420
+ }
421
+
422
+ // Fall back to null (let normal rendering handle it)
423
+ return null;
424
+ };