@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,89 @@
1
+ import type { RenderResult, SpecialHandler } from "./types";
2
+ import { finalizeOutput, loadPage } from "./types";
3
+
4
+ interface HackagePackage {
5
+ name: string;
6
+ synopsis: string;
7
+ description: string;
8
+ license: string;
9
+ author: string;
10
+ maintainer: string;
11
+ version: string;
12
+ homepage?: string;
13
+ "bug-reports"?: string;
14
+ category?: string;
15
+ stability?: string;
16
+ dependencies?: Record<string, string>;
17
+ }
18
+
19
+ /**
20
+ * Handle Hackage (Haskell package registry) URLs via JSON API
21
+ */
22
+ export const handleHackage: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
23
+ try {
24
+ const parsed = new URL(url);
25
+ if (parsed.hostname !== "hackage.haskell.org") return null;
26
+
27
+ // Match /package/{name} or /package/{name}-{version}
28
+ const match = parsed.pathname.match(/^\/package\/([^/]+)(?:\/|$)/);
29
+ if (!match) return null;
30
+
31
+ const packageId = decodeURIComponent(match[1]);
32
+ const fetchedAt = new Date().toISOString();
33
+
34
+ // Fetch package info with JSON accept header
35
+ const apiUrl = `https://hackage.haskell.org/package/${encodeURIComponent(packageId)}`;
36
+ const result = await loadPage(apiUrl, {
37
+ timeout,
38
+ headers: { Accept: "application/json" },
39
+ });
40
+
41
+ if (!result.ok) return null;
42
+
43
+ let pkg: HackagePackage;
44
+ try {
45
+ pkg = JSON.parse(result.content);
46
+ } catch {
47
+ return null;
48
+ }
49
+
50
+ let md = `# ${pkg.name}\n\n`;
51
+ if (pkg.synopsis) md += `${pkg.synopsis}\n\n`;
52
+
53
+ md += `**Version:** ${pkg.version}`;
54
+ if (pkg.license) md += ` · **License:** ${pkg.license}`;
55
+ md += "\n";
56
+
57
+ if (pkg.author) md += `**Author:** ${pkg.author}\n`;
58
+ if (pkg.maintainer) md += `**Maintainer:** ${pkg.maintainer}\n`;
59
+ if (pkg.category) md += `**Category:** ${pkg.category}\n`;
60
+ if (pkg.stability) md += `**Stability:** ${pkg.stability}\n`;
61
+ if (pkg.homepage) md += `**Homepage:** ${pkg.homepage}\n`;
62
+ if (pkg["bug-reports"]) md += `**Bug Reports:** ${pkg["bug-reports"]}\n`;
63
+
64
+ if (pkg.description) {
65
+ md += `\n## Description\n\n${pkg.description}\n`;
66
+ }
67
+
68
+ if (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) {
69
+ md += `\n## Dependencies\n\n`;
70
+ for (const [dep, version] of Object.entries(pkg.dependencies)) {
71
+ md += `- ${dep}: ${version}\n`;
72
+ }
73
+ }
74
+
75
+ const output = finalizeOutput(md);
76
+ return {
77
+ url,
78
+ finalUrl: url,
79
+ contentType: "text/markdown",
80
+ method: "hackage",
81
+ content: output.content,
82
+ fetchedAt,
83
+ truncated: output.truncated,
84
+ notes: ["Fetched via Hackage API"],
85
+ };
86
+ } catch {}
87
+
88
+ return null;
89
+ };
@@ -0,0 +1,208 @@
1
+ import type { SpecialHandler } from "./types";
2
+ import { finalizeOutput, loadPage } from "./types";
3
+
4
+ interface HNItem {
5
+ id: number;
6
+ deleted?: boolean;
7
+ type?: "job" | "story" | "comment" | "poll" | "pollopt";
8
+ by?: string;
9
+ time?: number;
10
+ text?: string;
11
+ dead?: boolean;
12
+ parent?: number;
13
+ poll?: number;
14
+ kids?: number[];
15
+ url?: string;
16
+ score?: number;
17
+ title?: string;
18
+ parts?: number[];
19
+ descendants?: number;
20
+ }
21
+
22
+ const API_BASE = "https://hacker-news.firebaseio.com/v0";
23
+
24
+ async function fetchItem(id: number, timeout: number): Promise<HNItem | null> {
25
+ const url = `${API_BASE}/item/${id}.json`;
26
+ const { content, ok } = await loadPage(url, { timeout });
27
+ if (!ok) return null;
28
+ return JSON.parse(content) as HNItem;
29
+ }
30
+
31
+ async function fetchItems(ids: number[], timeout: number, limit = 20): Promise<HNItem[]> {
32
+ const promises = ids.slice(0, limit).map((id) => fetchItem(id, timeout));
33
+ const results = await Promise.all(promises);
34
+ return results.filter((item): item is HNItem => item !== null && !item.deleted && !item.dead);
35
+ }
36
+
37
+ function decodeHNText(html: string): string {
38
+ return html
39
+ .replace(/<p>/g, "\n\n")
40
+ .replace(/<\/p>/g, "")
41
+ .replace(/<pre><code>/g, "\n```\n")
42
+ .replace(/<\/code><\/pre>/g, "\n```\n")
43
+ .replace(/<code>/g, "`")
44
+ .replace(/<\/code>/g, "`")
45
+ .replace(/<i>/g, "*")
46
+ .replace(/<\/i>/g, "*")
47
+ .replace(/<a href="([^"]+)"[^>]*>([^<]*)<\/a>/g, "[$2]($1)")
48
+ .replace(/<[^>]+>/g, "")
49
+ .replace(/&quot;/g, '"')
50
+ .replace(/&#x27;/g, "'")
51
+ .replace(/&#x2F;/g, "/")
52
+ .replace(/&lt;/g, "<")
53
+ .replace(/&gt;/g, ">")
54
+ .replace(/&amp;/g, "&")
55
+ .trim();
56
+ }
57
+
58
+ function formatTimestamp(unixTime: number): string {
59
+ const date = new Date(unixTime * 1000);
60
+ const now = Date.now();
61
+ const diff = now - date.getTime();
62
+ const hours = Math.floor(diff / (1000 * 60 * 60));
63
+ const days = Math.floor(hours / 24);
64
+
65
+ if (days > 7) return date.toISOString().split("T")[0];
66
+ if (days > 0) return `${days}d ago`;
67
+ if (hours > 0) return `${hours}h ago`;
68
+ const minutes = Math.floor(diff / (1000 * 60));
69
+ return `${minutes}m ago`;
70
+ }
71
+
72
+ async function renderStory(item: HNItem, timeout: number, depth = 0): Promise<string> {
73
+ let output = "";
74
+
75
+ if (depth === 0) {
76
+ output += `# ${item.title}\n\n`;
77
+ if (item.url) {
78
+ output += `**URL:** ${item.url}\n\n`;
79
+ }
80
+ output += `**Posted by:** ${item.by} | **Score:** ${item.score ?? 0} | **Time:** ${formatTimestamp(item.time ?? 0)}`;
81
+ if (item.descendants) {
82
+ output += ` | **Comments:** ${item.descendants}`;
83
+ }
84
+ output += "\n\n";
85
+ }
86
+
87
+ if (item.text) {
88
+ output += `${decodeHNText(item.text)}\n\n`;
89
+ }
90
+
91
+ if (item.kids && item.kids.length > 0 && depth < 2) {
92
+ const topComments = item.kids.slice(0, depth === 0 ? 20 : 10);
93
+ const comments = await fetchItems(topComments, timeout, topComments.length);
94
+
95
+ if (comments.length > 0) {
96
+ if (depth === 0) output += "---\n\n## Comments\n\n";
97
+
98
+ for (const comment of comments) {
99
+ const indent = " ".repeat(depth);
100
+ output += `${indent}**${comment.by}** (${formatTimestamp(comment.time ?? 0)})`;
101
+ if (comment.score !== undefined) output += ` [${comment.score}]`;
102
+ output += "\n";
103
+ if (comment.text) {
104
+ const text = decodeHNText(comment.text);
105
+ const lines = text.split("\n");
106
+ output += `${lines.map((line) => `${indent}${line}`).join("\n")}\n\n`;
107
+ }
108
+
109
+ if (comment.kids && comment.kids.length > 0 && depth < 1) {
110
+ const childOutput = await renderStory(comment, timeout, depth + 1);
111
+ output += childOutput;
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ return output;
118
+ }
119
+
120
+ async function renderListing(ids: number[], timeout: number, title: string): Promise<string> {
121
+ let output = `# ${title}\n\n`;
122
+ const stories = await fetchItems(ids, timeout, 20);
123
+
124
+ for (let i = 0; i < stories.length; i++) {
125
+ const story = stories[i];
126
+ output += `${i + 1}. **${story.title}**\n`;
127
+ if (story.url) {
128
+ output += ` ${story.url}\n`;
129
+ }
130
+ output += ` ${story.score ?? 0} points by ${story.by} | ${formatTimestamp(story.time ?? 0)}`;
131
+ if (story.descendants) {
132
+ output += ` | ${story.descendants} comments`;
133
+ }
134
+ output += `\n https://news.ycombinator.com/item?id=${story.id}\n\n`;
135
+ }
136
+
137
+ return output;
138
+ }
139
+
140
+ export const handleHackerNews: SpecialHandler = async (url, timeout) => {
141
+ const parsed = new URL(url);
142
+ if (!parsed.hostname.includes("news.ycombinator.com")) return null;
143
+
144
+ const notes: string[] = [];
145
+ let content = "";
146
+ const fetchedAt = new Date().toISOString();
147
+
148
+ try {
149
+ const itemId = parsed.searchParams.get("id");
150
+
151
+ if (itemId) {
152
+ const item = await fetchItem(parseInt(itemId, 10), timeout);
153
+ if (!item) throw new Error(`Failed to fetch item ${itemId}`);
154
+
155
+ content = await renderStory(item, timeout);
156
+ notes.push(`Fetched HN item ${itemId} with top-level comments (depth 2)`);
157
+ } else if (parsed.pathname === "/" || parsed.pathname === "/news") {
158
+ const { content: raw, ok } = await loadPage(`${API_BASE}/topstories.json`, { timeout });
159
+ if (!ok) throw new Error("Failed to fetch top stories");
160
+ const ids = JSON.parse(raw) as number[];
161
+ content = await renderListing(ids, timeout, "Hacker News - Top Stories");
162
+ notes.push("Fetched top 20 stories from HN front page");
163
+ } else if (parsed.pathname === "/newest") {
164
+ const { content: raw, ok } = await loadPage(`${API_BASE}/newstories.json`, { timeout });
165
+ if (!ok) throw new Error("Failed to fetch new stories");
166
+ const ids = JSON.parse(raw) as number[];
167
+ content = await renderListing(ids, timeout, "Hacker News - New Stories");
168
+ notes.push("Fetched top 20 new stories");
169
+ } else if (parsed.pathname === "/best") {
170
+ const { content: raw, ok } = await loadPage(`${API_BASE}/beststories.json`, { timeout });
171
+ if (!ok) throw new Error("Failed to fetch best stories");
172
+ const ids = JSON.parse(raw) as number[];
173
+ content = await renderListing(ids, timeout, "Hacker News - Best Stories");
174
+ notes.push("Fetched top 20 best stories");
175
+ } else {
176
+ return null;
177
+ }
178
+
179
+ const { content: finalContent, truncated } = finalizeOutput(content);
180
+
181
+ return {
182
+ url,
183
+ finalUrl: url,
184
+ contentType: "text/markdown",
185
+ method: "hackernews",
186
+ content: finalContent,
187
+ fetchedAt,
188
+ truncated,
189
+ notes,
190
+ };
191
+ } catch (err) {
192
+ const errorMsg = err instanceof Error ? err.message : String(err);
193
+ notes.push(`Error: ${errorMsg}`);
194
+ const { content: finalContent, truncated } = finalizeOutput(
195
+ `# Error fetching Hacker News content\n\n${errorMsg}`,
196
+ );
197
+ return {
198
+ url,
199
+ finalUrl: url,
200
+ contentType: "text/markdown",
201
+ method: "hackernews",
202
+ content: finalContent,
203
+ fetchedAt,
204
+ truncated,
205
+ notes,
206
+ };
207
+ }
208
+ };
@@ -0,0 +1,121 @@
1
+ import type { SpecialHandler } from "./types";
2
+ import { finalizeOutput, formatCount, loadPage } from "./types";
3
+
4
+ /**
5
+ * Handle Hex.pm (Elixir package registry) URLs via API
6
+ */
7
+ export const handleHex: SpecialHandler = async (url, timeout) => {
8
+ try {
9
+ const parsed = new URL(url);
10
+ if (parsed.hostname !== "hex.pm" && parsed.hostname !== "www.hex.pm") return null;
11
+
12
+ // Extract package name from /packages/name or /packages/name/version
13
+ const match = parsed.pathname.match(/^\/packages\/([^/]+)/);
14
+ if (!match) return null;
15
+
16
+ const packageName = decodeURIComponent(match[1]);
17
+ const fetchedAt = new Date().toISOString();
18
+
19
+ // Fetch from Hex.pm API
20
+ const apiUrl = `https://hex.pm/api/packages/${packageName}`;
21
+ const result = await loadPage(apiUrl, { timeout });
22
+
23
+ if (!result.ok) return null;
24
+
25
+ let data: {
26
+ name: string;
27
+ meta?: {
28
+ description?: string;
29
+ links?: Record<string, string>;
30
+ licenses?: string[];
31
+ };
32
+ releases?: Array<{
33
+ version: string;
34
+ inserted_at: string;
35
+ }>;
36
+ downloads?: {
37
+ all?: number;
38
+ week?: number;
39
+ day?: number;
40
+ };
41
+ latest_version?: string;
42
+ latest_stable_version?: string;
43
+ };
44
+
45
+ try {
46
+ data = JSON.parse(result.content);
47
+ } catch {
48
+ return null;
49
+ }
50
+
51
+ let md = `# ${data.name}\n\n`;
52
+ if (data.meta?.description) md += `${data.meta.description}\n\n`;
53
+
54
+ const version = data.latest_stable_version || data.latest_version || "unknown";
55
+ md += `**Latest:** ${version}`;
56
+ if (data.meta?.licenses?.length) md += ` · **License:** ${data.meta.licenses.join(", ")}`;
57
+ md += "\n";
58
+
59
+ if (data.downloads?.all) {
60
+ md += `**Total Downloads:** ${formatCount(data.downloads.all)}`;
61
+ if (data.downloads.week) md += ` · **This Week:** ${formatCount(data.downloads.week)}`;
62
+ md += "\n";
63
+ }
64
+ md += "\n";
65
+
66
+ if (data.meta?.links && Object.keys(data.meta.links).length > 0) {
67
+ md += `## Links\n\n`;
68
+ for (const [key, value] of Object.entries(data.meta.links)) {
69
+ md += `- **${key}:** ${value}\n`;
70
+ }
71
+ md += "\n";
72
+ }
73
+
74
+ // Fetch releases if available
75
+ if (data.releases?.length) {
76
+ const releasesUrl = `https://hex.pm/api/packages/${packageName}/releases/${version}`;
77
+ const releaseResult = await loadPage(releasesUrl, { timeout: Math.min(timeout, 5) });
78
+
79
+ if (releaseResult.ok) {
80
+ try {
81
+ const releaseData = JSON.parse(releaseResult.content) as {
82
+ requirements?: Record<string, { app?: string; optional: boolean; requirement: string }>;
83
+ };
84
+
85
+ if (releaseData.requirements && Object.keys(releaseData.requirements).length > 0) {
86
+ md += `## Dependencies (${version})\n\n`;
87
+ for (const [dep, info] of Object.entries(releaseData.requirements)) {
88
+ const optional = info.optional ? " (optional)" : "";
89
+ md += `- ${dep}: ${info.requirement}${optional}\n`;
90
+ }
91
+ md += "\n";
92
+ }
93
+ } catch {}
94
+ }
95
+
96
+ // Show recent releases
97
+ const recentReleases = data.releases.slice(0, 10);
98
+ if (recentReleases.length > 0) {
99
+ md += `## Recent Releases\n\n`;
100
+ for (const release of recentReleases) {
101
+ const date = new Date(release.inserted_at).toISOString().split("T")[0];
102
+ md += `- **${release.version}** (${date})\n`;
103
+ }
104
+ }
105
+ }
106
+
107
+ const output = finalizeOutput(md);
108
+ return {
109
+ url,
110
+ finalUrl: url,
111
+ contentType: "text/markdown",
112
+ method: "hex",
113
+ content: output.content,
114
+ fetchedAt,
115
+ truncated: output.truncated,
116
+ notes: ["Fetched via Hex.pm API"],
117
+ };
118
+ } catch {}
119
+
120
+ return null;
121
+ };