@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,239 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { handleArxiv } from "./arxiv";
3
+ import { handleIacr } from "./iacr";
4
+ import { handlePubMed } from "./pubmed";
5
+ import { handleSemanticScholar } from "./semantic-scholar";
6
+
7
+ const SKIP = !process.env.WEB_FETCH_INTEGRATION;
8
+
9
+ describe.skipIf(SKIP)("handleSemanticScholar", () => {
10
+ it("returns null for non-S2 URLs", async () => {
11
+ const result = await handleSemanticScholar("https://example.com", 10);
12
+ expect(result).toBeNull();
13
+ });
14
+
15
+ it("fetches a known paper", async () => {
16
+ // "Attention Is All You Need" paper
17
+ const result = await handleSemanticScholar(
18
+ "https://www.semanticscholar.org/paper/Attention-is-All-you-Need-Vaswani-Shazeer/204e3073870fae3d05bcbc2f6a8e263d9b72e776",
19
+ 20,
20
+ );
21
+ expect(result).not.toBeNull();
22
+ expect(result?.method).toBe("semantic-scholar");
23
+ // API may be rate-limited or fail, verify handler structure
24
+ if (
25
+ !result?.content.includes("Too Many Requests") &&
26
+ !result?.content.includes("Failed to fetch") &&
27
+ !result?.content.includes("Failed to parse")
28
+ ) {
29
+ expect(result?.content).toContain("Attention");
30
+ expect(result?.contentType).toBe("text/markdown");
31
+ }
32
+ expect(result?.truncated).toBe(false);
33
+ });
34
+
35
+ it("handles invalid paper ID format", async () => {
36
+ const result = await handleSemanticScholar("https://www.semanticscholar.org/paper/invalid", 20);
37
+ expect(result).not.toBeNull();
38
+ expect(result?.method).toBe("semantic-scholar");
39
+ expect(result?.content).toContain("Failed to extract paper ID");
40
+ expect(result?.notes).toContain("Invalid URL format");
41
+ });
42
+
43
+ it("extracts paper ID from various URL formats", async () => {
44
+ const paperId = "204e3073870fae3d05bcbc2f6a8e263d9b72e776";
45
+ const urls = [
46
+ `https://www.semanticscholar.org/paper/Attention-is-All-you-Need-Vaswani-Shazeer/${paperId}`,
47
+ `https://www.semanticscholar.org/paper/${paperId}`,
48
+ ];
49
+
50
+ for (const url of urls) {
51
+ const result = await handleSemanticScholar(url, 20);
52
+ expect(result).not.toBeNull();
53
+ expect(result?.method).toBe("semantic-scholar");
54
+ // API may be rate-limited or fail
55
+ if (
56
+ !result?.content.includes("Too Many Requests") &&
57
+ !result?.content.includes("Failed to fetch") &&
58
+ !result?.content.includes("Failed to parse")
59
+ ) {
60
+ expect(result?.content).toContain("Attention");
61
+ }
62
+ }
63
+ });
64
+
65
+ it("includes metadata in formatted output", async () => {
66
+ const result = await handleSemanticScholar(
67
+ "https://www.semanticscholar.org/paper/Attention-is-All-you-Need-Vaswani-Shazeer/204e3073870fae3d05bcbc2f6a8e263d9b72e776",
68
+ 20,
69
+ );
70
+ expect(result).not.toBeNull();
71
+ // Only check metadata if API call succeeded (not rate-limited)
72
+ if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
73
+ expect(result?.content).toMatch(/Year:/);
74
+ expect(result?.content).toMatch(/Citations:/);
75
+ expect(result?.content).toMatch(/Authors:/);
76
+ expect(result?.content).toContain("Vaswani");
77
+ }
78
+ });
79
+ });
80
+
81
+ describe.skipIf(SKIP)("handlePubMed", () => {
82
+ it("returns null for non-PubMed URLs", async () => {
83
+ const result = await handlePubMed("https://example.com", 10);
84
+ expect(result).toBeNull();
85
+ });
86
+
87
+ it("fetches a known article from pubmed.ncbi.nlm.nih.gov", async () => {
88
+ // PMID 33782455 - COVID-19 vaccine paper
89
+ const result = await handlePubMed("https://pubmed.ncbi.nlm.nih.gov/33782455/", 20);
90
+ expect(result).not.toBeNull();
91
+ expect(result?.method).toBe("pubmed");
92
+ expect(result?.contentType).toBe("text/markdown");
93
+ expect(result?.truncated).toBe(false);
94
+ });
95
+
96
+ it("fetches from ncbi.nlm.nih.gov/pubmed format", async () => {
97
+ const result = await handlePubMed("https://ncbi.nlm.nih.gov/pubmed/33782455", 20);
98
+ expect(result).not.toBeNull();
99
+ expect(result?.method).toBe("pubmed");
100
+ });
101
+
102
+ it("includes PMID in output", async () => {
103
+ const result = await handlePubMed("https://pubmed.ncbi.nlm.nih.gov/33782455/", 20);
104
+ expect(result).not.toBeNull();
105
+ expect(result?.content).toContain("PMID:");
106
+ expect(result?.content).toContain("33782455");
107
+ });
108
+
109
+ it("includes abstract section", async () => {
110
+ const result = await handlePubMed("https://pubmed.ncbi.nlm.nih.gov/33782455/", 20);
111
+ expect(result).not.toBeNull();
112
+ expect(result?.content).toContain("## Abstract");
113
+ });
114
+
115
+ it("includes metadata fields", async () => {
116
+ const result = await handlePubMed("https://pubmed.ncbi.nlm.nih.gov/33782455/", 20);
117
+ expect(result).not.toBeNull();
118
+ expect(result?.content).toMatch(/Authors:/);
119
+ expect(result?.content).toMatch(/Journal:/);
120
+ });
121
+
122
+ it("returns null for invalid PMID format", async () => {
123
+ const result = await handlePubMed("https://pubmed.ncbi.nlm.nih.gov/invalid/", 20);
124
+ expect(result).toBeNull();
125
+ });
126
+
127
+ it("handles non-existent PMID gracefully", async () => {
128
+ const result = await handlePubMed("https://pubmed.ncbi.nlm.nih.gov/99999999999/", 20);
129
+ // NCBI API returns a response even for non-existent PMIDs with minimal data
130
+ expect(result).not.toBeNull();
131
+ expect(result?.method).toBe("pubmed");
132
+ });
133
+ });
134
+
135
+ describe.skipIf(SKIP)("handleArxiv", () => {
136
+ it("returns null for non-arXiv URLs", async () => {
137
+ const result = await handleArxiv("https://example.com", 10000);
138
+ expect(result).toBeNull();
139
+ });
140
+
141
+ it("fetches a known paper", async () => {
142
+ // "Attention Is All You Need" paper
143
+ const result = await handleArxiv("https://arxiv.org/abs/1706.03762", 30000);
144
+ expect(result).not.toBeNull();
145
+ expect(result?.method).toBe("arxiv");
146
+ expect(result?.contentType).toBe("text/markdown");
147
+ // API may be rate-limited or fail
148
+ if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
149
+ expect(result?.content).toContain("Attention");
150
+ expect(result?.content).toContain("arXiv:");
151
+ expect(result?.content).toContain("1706.03762");
152
+ }
153
+ expect(result?.truncated).toBe(false);
154
+ });
155
+
156
+ it("handles /pdf/ URLs", async () => {
157
+ const result = await handleArxiv("https://arxiv.org/pdf/1706.03762", 30000);
158
+ expect(result).not.toBeNull();
159
+ expect(result?.method).toBe("arxiv");
160
+ if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
161
+ expect(result?.content).toContain("Attention");
162
+ expect(result?.notes?.some((n) => n.includes("PDF"))).toBe(true);
163
+ }
164
+ });
165
+
166
+ it("handles arxiv.org/abs/ format", async () => {
167
+ const result = await handleArxiv("https://arxiv.org/abs/1706.03762", 30000);
168
+ expect(result).not.toBeNull();
169
+ expect(result?.method).toBe("arxiv");
170
+ if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
171
+ expect(result?.content).toContain("1706.03762");
172
+ }
173
+ });
174
+
175
+ it("includes paper metadata", async () => {
176
+ const result = await handleArxiv("https://arxiv.org/abs/1706.03762", 30000);
177
+ expect(result).not.toBeNull();
178
+ if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
179
+ expect(result?.content).toMatch(/Authors:/);
180
+ expect(result?.content).toContain("Vaswani");
181
+ expect(result?.content).toMatch(/Abstract/);
182
+ expect(result?.content).toMatch(/Published:/);
183
+ }
184
+ });
185
+
186
+ it("handles rate limiting gracefully", async () => {
187
+ const result = await handleArxiv("https://arxiv.org/abs/1706.03762", 5000);
188
+ expect(result).not.toBeNull();
189
+ expect(result?.method).toBe("arxiv");
190
+ // Should return something, even if rate limited
191
+ expect(result?.content).toBeTruthy();
192
+ });
193
+ });
194
+
195
+ describe.skipIf(SKIP)("handleIacr", () => {
196
+ it("returns null for non-IACR URLs", async () => {
197
+ const result = await handleIacr("https://example.com", 10000);
198
+ expect(result).toBeNull();
199
+ });
200
+
201
+ it("fetches a known ePrint", async () => {
202
+ // Using a well-known paper
203
+ const result = await handleIacr("https://eprint.iacr.org/2023/123", 30000);
204
+ expect(result).not.toBeNull();
205
+ expect(result?.method).toBe("iacr");
206
+ expect(result?.contentType).toBe("text/markdown");
207
+ if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
208
+ expect(result?.content).toContain("ePrint:");
209
+ expect(result?.content).toContain("2023/123");
210
+ }
211
+ expect(result?.truncated).toBe(false);
212
+ });
213
+
214
+ it("includes paper metadata", async () => {
215
+ const result = await handleIacr("https://eprint.iacr.org/2023/123", 30000);
216
+ expect(result).not.toBeNull();
217
+ if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
218
+ expect(result?.content).toMatch(/Authors:/);
219
+ expect(result?.content).toMatch(/Abstract/);
220
+ }
221
+ });
222
+
223
+ it("handles rate limiting gracefully", async () => {
224
+ const result = await handleIacr("https://eprint.iacr.org/2023/123", 5000);
225
+ expect(result).not.toBeNull();
226
+ expect(result?.method).toBe("iacr");
227
+ // Should return something, even if rate limited
228
+ expect(result?.content).toBeTruthy();
229
+ });
230
+
231
+ it("handles PDF URLs", async () => {
232
+ const result = await handleIacr("https://eprint.iacr.org/2023/123.pdf", 30000);
233
+ expect(result).not.toBeNull();
234
+ expect(result?.method).toBe("iacr");
235
+ if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
236
+ expect(result?.notes?.some((n) => n.includes("PDF"))).toBe(true);
237
+ }
238
+ });
239
+ });
@@ -0,0 +1,210 @@
1
+ import type { RenderResult, SpecialHandler } from "./types";
2
+ import { finalizeOutput, formatCount, loadPage } from "./types";
3
+
4
+ interface ArtifactHubMaintainer {
5
+ name: string;
6
+ email?: string;
7
+ }
8
+
9
+ interface ArtifactHubLink {
10
+ name: string;
11
+ url: string;
12
+ }
13
+
14
+ interface ArtifactHubRepository {
15
+ name: string;
16
+ display_name?: string;
17
+ url: string;
18
+ organization_name?: string;
19
+ organization_display_name?: string;
20
+ }
21
+
22
+ interface ArtifactHubPackage {
23
+ package_id: string;
24
+ name: string;
25
+ normalized_name: string;
26
+ display_name?: string;
27
+ description?: string;
28
+ version: string;
29
+ app_version?: string;
30
+ license?: string;
31
+ home_url?: string;
32
+ readme?: string;
33
+ install?: string;
34
+ keywords?: string[];
35
+ maintainers?: ArtifactHubMaintainer[];
36
+ links?: ArtifactHubLink[];
37
+ repository: ArtifactHubRepository;
38
+ ts: number;
39
+ created_at: number;
40
+ stars?: number;
41
+ official?: boolean;
42
+ signed?: boolean;
43
+ security_report_summary?: {
44
+ low?: number;
45
+ medium?: number;
46
+ high?: number;
47
+ critical?: number;
48
+ };
49
+ available_versions?: Array<{ version: string; ts: number }>;
50
+ }
51
+
52
+ /**
53
+ * Handle Artifact Hub URLs via API
54
+ * Supports Helm charts, OLM operators, Falco rules, OPA policies, etc.
55
+ */
56
+ export const handleArtifactHub: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
57
+ try {
58
+ const parsed = new URL(url);
59
+ if (parsed.hostname !== "artifacthub.io" && parsed.hostname !== "www.artifacthub.io") return null;
60
+
61
+ // Extract kind, repo, and package name from /packages/{kind}/{repo}/{name}
62
+ const match = parsed.pathname.match(/^\/packages\/([^/]+)\/([^/]+)\/([^/]+)/);
63
+ if (!match) return null;
64
+
65
+ const [, kind, repo, name] = match;
66
+ const fetchedAt = new Date().toISOString();
67
+
68
+ // Fetch from Artifact Hub API
69
+ const apiUrl = `https://artifacthub.io/api/v1/packages/${kind}/${repo}/${name}`;
70
+ const result = await loadPage(apiUrl, {
71
+ timeout,
72
+ headers: { Accept: "application/json" },
73
+ });
74
+
75
+ if (!result.ok) return null;
76
+
77
+ let pkg: ArtifactHubPackage;
78
+ try {
79
+ pkg = JSON.parse(result.content);
80
+ } catch {
81
+ return null;
82
+ }
83
+
84
+ const displayName = pkg.display_name || pkg.name;
85
+ const kindLabel = formatKindLabel(kind);
86
+
87
+ let md = `# ${displayName}\n\n`;
88
+ if (pkg.description) md += `${pkg.description}\n\n`;
89
+
90
+ // Basic info line
91
+ md += `**Type:** ${kindLabel}`;
92
+ md += ` · **Version:** ${pkg.version}`;
93
+ if (pkg.app_version) md += ` · **App Version:** ${pkg.app_version}`;
94
+ if (pkg.license) md += ` · **License:** ${pkg.license}`;
95
+ md += "\n";
96
+
97
+ // Stats and badges
98
+ const badges: string[] = [];
99
+ if (pkg.official) badges.push("Official");
100
+ if (pkg.signed) badges.push("Signed");
101
+ if (pkg.stars) badges.push(`${formatCount(pkg.stars)} stars`);
102
+ if (badges.length > 0) md += `**${badges.join(" · ")}**\n`;
103
+ md += "\n";
104
+
105
+ // Repository info
106
+ const repoDisplay =
107
+ pkg.repository.organization_display_name || pkg.repository.display_name || pkg.repository.name;
108
+ md += `**Repository:** ${repoDisplay}`;
109
+ if (pkg.repository.url) md += ` ([${pkg.repository.url}](${pkg.repository.url}))`;
110
+ md += "\n";
111
+
112
+ if (pkg.home_url) md += `**Homepage:** ${pkg.home_url}\n`;
113
+ if (pkg.keywords?.length) md += `**Keywords:** ${pkg.keywords.join(", ")}\n`;
114
+
115
+ // Maintainers
116
+ if (pkg.maintainers?.length) {
117
+ const maintainerNames = pkg.maintainers.map((m) => m.name).join(", ");
118
+ md += `**Maintainers:** ${maintainerNames}\n`;
119
+ }
120
+
121
+ // Security report summary
122
+ if (pkg.security_report_summary) {
123
+ const sec = pkg.security_report_summary;
124
+ const parts: string[] = [];
125
+ if (sec.critical) parts.push(`${sec.critical} critical`);
126
+ if (sec.high) parts.push(`${sec.high} high`);
127
+ if (sec.medium) parts.push(`${sec.medium} medium`);
128
+ if (sec.low) parts.push(`${sec.low} low`);
129
+ if (parts.length > 0) {
130
+ md += `**Security:** ${parts.join(", ")}\n`;
131
+ }
132
+ }
133
+
134
+ // Links
135
+ if (pkg.links?.length) {
136
+ md += `\n## Links\n\n`;
137
+ for (const link of pkg.links) {
138
+ md += `- [${link.name}](${link.url})\n`;
139
+ }
140
+ }
141
+
142
+ // Install instructions
143
+ if (pkg.install) {
144
+ md += `\n## Installation\n\n\`\`\`bash\n${pkg.install.trim()}\n\`\`\`\n`;
145
+ }
146
+
147
+ // Recent versions
148
+ if (pkg.available_versions?.length) {
149
+ md += `\n## Recent Versions\n\n`;
150
+ for (const ver of pkg.available_versions.slice(0, 5)) {
151
+ const date = new Date(ver.ts * 1000).toISOString().split("T")[0];
152
+ md += `- **${ver.version}** (${date})\n`;
153
+ }
154
+ }
155
+
156
+ // README
157
+ if (pkg.readme) {
158
+ md += `\n---\n\n## README\n\n${pkg.readme}\n`;
159
+ }
160
+
161
+ const output = finalizeOutput(md);
162
+ return {
163
+ url,
164
+ finalUrl: url,
165
+ contentType: "text/markdown",
166
+ method: "artifacthub",
167
+ content: output.content,
168
+ fetchedAt,
169
+ truncated: output.truncated,
170
+ notes: [`Fetched via Artifact Hub API (${kindLabel})`],
171
+ };
172
+ } catch {}
173
+
174
+ return null;
175
+ };
176
+
177
+ /**
178
+ * Convert kind slug to display label
179
+ */
180
+ function formatKindLabel(kind: string): string {
181
+ const labels: Record<string, string> = {
182
+ helm: "Helm Chart",
183
+ "helm-plugin": "Helm Plugin",
184
+ falco: "Falco Rules",
185
+ opa: "OPA Policy",
186
+ olm: "OLM Operator",
187
+ tbaction: "Tinkerbell Action",
188
+ krew: "Krew Plugin",
189
+ tekton: "Tekton Task",
190
+ "tekton-pipeline": "Tekton Pipeline",
191
+ keda: "KEDA Scaler",
192
+ coredns: "CoreDNS Plugin",
193
+ keptn: "Keptn Integration",
194
+ container: "Container Image",
195
+ kubewarden: "Kubewarden Policy",
196
+ gatekeeper: "Gatekeeper Policy",
197
+ kyverno: "Kyverno Policy",
198
+ "knative-client": "Knative Client Plugin",
199
+ backstage: "Backstage Plugin",
200
+ argo: "Argo Template",
201
+ kubearmor: "KubeArmor Policy",
202
+ kcl: "KCL Module",
203
+ headlamp: "Headlamp Plugin",
204
+ inspektor: "Inspektor Gadget",
205
+ "meshery-design": "Meshery Design",
206
+ "opencost-plugin": "OpenCost Plugin",
207
+ radius: "Radius Recipe",
208
+ };
209
+ return labels[kind] || kind.charAt(0).toUpperCase() + kind.slice(1);
210
+ }
@@ -0,0 +1,84 @@
1
+ import { parse as parseHtml } from "node-html-parser";
2
+ import type { RenderResult, SpecialHandler } from "./types";
3
+ import { finalizeOutput, loadPage } from "./types";
4
+ import { convertWithMarkitdown, fetchBinary } from "./utils";
5
+
6
+ /**
7
+ * Handle arXiv URLs via arXiv API
8
+ */
9
+ export const handleArxiv: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
10
+ try {
11
+ const parsed = new URL(url);
12
+ if (parsed.hostname !== "arxiv.org") return null;
13
+
14
+ // Extract paper ID from various URL formats
15
+ // /abs/1234.56789, /pdf/1234.56789, /abs/cs/0123456
16
+ const match = parsed.pathname.match(/\/(abs|pdf)\/(.+?)(?:\.pdf)?$/);
17
+ if (!match) return null;
18
+
19
+ const paperId = match[2];
20
+ const fetchedAt = new Date().toISOString();
21
+ const notes: string[] = [];
22
+
23
+ // Fetch metadata via arXiv API
24
+ const apiUrl = `https://export.arxiv.org/api/query?id_list=${paperId}`;
25
+ const result = await loadPage(apiUrl, { timeout });
26
+
27
+ if (!result.ok) return null;
28
+
29
+ // Parse the Atom feed response
30
+ const doc = parseHtml(result.content, { parseNoneClosedTags: true });
31
+ const entry = doc.querySelector("entry");
32
+
33
+ if (!entry) return null;
34
+
35
+ const title = entry.querySelector("title")?.text?.trim()?.replace(/\s+/g, " ");
36
+ const summary = entry.querySelector("summary")?.text?.trim();
37
+ const authors = entry
38
+ .querySelectorAll("author name")
39
+ .map((n) => n.text?.trim())
40
+ .filter(Boolean);
41
+ const published = entry.querySelector("published")?.text?.trim()?.split("T")[0];
42
+ const categories = entry
43
+ .querySelectorAll("category")
44
+ .map((c) => c.getAttribute("term"))
45
+ .filter(Boolean);
46
+ const pdfLink = entry.querySelector('link[title="pdf"]')?.getAttribute("href");
47
+
48
+ let md = `# ${title || "arXiv Paper"}\n\n`;
49
+ if (authors.length) md += `**Authors:** ${authors.join(", ")}\n`;
50
+ if (published) md += `**Published:** ${published}\n`;
51
+ if (categories.length) md += `**Categories:** ${categories.join(", ")}\n`;
52
+ md += `**arXiv:** ${paperId}\n\n`;
53
+ md += `---\n\n## Abstract\n\n${summary || "No abstract available."}\n\n`;
54
+
55
+ // If it was a PDF link or we want full content, try to fetch and convert PDF
56
+ if (match[1] === "pdf" || parsed.pathname.includes(".pdf")) {
57
+ if (pdfLink) {
58
+ notes.push("Fetching PDF for full content...");
59
+ const pdfResult = await fetchBinary(pdfLink, timeout);
60
+ if (pdfResult.ok) {
61
+ const converted = await convertWithMarkitdown(pdfResult.buffer, ".pdf", timeout);
62
+ if (converted.ok && converted.content.length > 500) {
63
+ md += `---\n\n## Full Paper\n\n${converted.content}\n`;
64
+ notes.push("PDF converted via markitdown");
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ const output = finalizeOutput(md);
71
+ return {
72
+ url,
73
+ finalUrl: url,
74
+ contentType: "text/markdown",
75
+ method: "arxiv",
76
+ content: output.content,
77
+ fetchedAt,
78
+ truncated: output.truncated,
79
+ notes: notes.length ? notes : ["Fetched via arXiv API"],
80
+ };
81
+ } catch {}
82
+
83
+ return null;
84
+ };
@@ -0,0 +1,171 @@
1
+ import type { RenderResult, SpecialHandler } from "./types";
2
+ import { finalizeOutput, formatCount, loadPage } from "./types";
3
+
4
+ interface AurPackage {
5
+ Name: string;
6
+ Version: string;
7
+ Description?: string;
8
+ Maintainer?: string;
9
+ NumVotes: number;
10
+ Popularity: number;
11
+ Depends?: string[];
12
+ MakeDepends?: string[];
13
+ OptDepends?: string[];
14
+ CheckDepends?: string[];
15
+ LastModified: number;
16
+ FirstSubmitted: number;
17
+ URL?: string;
18
+ URLPath?: string;
19
+ PackageBase: string;
20
+ OutOfDate?: number | null;
21
+ License?: string[];
22
+ Keywords?: string[];
23
+ Conflicts?: string[];
24
+ Provides?: string[];
25
+ Replaces?: string[];
26
+ }
27
+
28
+ interface AurResponse {
29
+ version: number;
30
+ type: string;
31
+ resultcount: number;
32
+ results: AurPackage[];
33
+ }
34
+
35
+ /**
36
+ * Handle AUR (Arch User Repository) URLs via RPC API
37
+ */
38
+ export const handleAur: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
39
+ try {
40
+ const parsed = new URL(url);
41
+ if (parsed.hostname !== "aur.archlinux.org") return null;
42
+
43
+ // Extract package name from /packages/{name}
44
+ const match = parsed.pathname.match(/^\/packages\/([^/?#]+)/);
45
+ if (!match) return null;
46
+
47
+ const packageName = decodeURIComponent(match[1]);
48
+ const fetchedAt = new Date().toISOString();
49
+
50
+ // Fetch from AUR RPC API
51
+ const apiUrl = `https://aur.archlinux.org/rpc/?v=5&type=info&arg=${encodeURIComponent(packageName)}`;
52
+ const result = await loadPage(apiUrl, { timeout });
53
+
54
+ if (!result.ok) return null;
55
+
56
+ let data: AurResponse;
57
+ try {
58
+ data = JSON.parse(result.content);
59
+ } catch {
60
+ return null;
61
+ }
62
+
63
+ if (data.resultcount === 0 || !data.results[0]) return null;
64
+
65
+ const pkg = data.results[0];
66
+
67
+ let md = `# ${pkg.Name}\n\n`;
68
+ if (pkg.Description) md += `${pkg.Description}\n\n`;
69
+
70
+ // Package info
71
+ md += `**Version:** ${pkg.Version}`;
72
+ if (pkg.OutOfDate) {
73
+ const outOfDateDate = new Date(pkg.OutOfDate * 1000).toISOString().split("T")[0];
74
+ md += ` (flagged out-of-date: ${outOfDateDate})`;
75
+ }
76
+ md += "\n";
77
+
78
+ if (pkg.Maintainer) {
79
+ md += `**Maintainer:** [${pkg.Maintainer}](https://aur.archlinux.org/account/${pkg.Maintainer})\n`;
80
+ } else {
81
+ md += "**Maintainer:** Orphaned\n";
82
+ }
83
+
84
+ md += `**Votes:** ${formatCount(pkg.NumVotes)} · **Popularity:** ${pkg.Popularity.toFixed(2)}\n`;
85
+
86
+ // Timestamps
87
+ const lastModified = new Date(pkg.LastModified * 1000).toISOString().split("T")[0];
88
+ const firstSubmitted = new Date(pkg.FirstSubmitted * 1000).toISOString().split("T")[0];
89
+ md += `**Last Updated:** ${lastModified} · **First Submitted:** ${firstSubmitted}\n`;
90
+
91
+ if (pkg.License?.length) md += `**License:** ${pkg.License.join(", ")}\n`;
92
+ if (pkg.URL) md += `**Upstream:** ${pkg.URL}\n`;
93
+ if (pkg.Keywords?.length) md += `**Keywords:** ${pkg.Keywords.join(", ")}\n`;
94
+
95
+ // Dependencies
96
+ if (pkg.Depends?.length) {
97
+ md += `\n## Dependencies (${pkg.Depends.length})\n\n`;
98
+ for (const dep of pkg.Depends) {
99
+ md += `- ${dep}\n`;
100
+ }
101
+ }
102
+
103
+ if (pkg.MakeDepends?.length) {
104
+ md += `\n## Make Dependencies (${pkg.MakeDepends.length})\n\n`;
105
+ for (const dep of pkg.MakeDepends) {
106
+ md += `- ${dep}\n`;
107
+ }
108
+ }
109
+
110
+ if (pkg.OptDepends?.length) {
111
+ md += `\n## Optional Dependencies\n\n`;
112
+ for (const dep of pkg.OptDepends) {
113
+ md += `- ${dep}\n`;
114
+ }
115
+ }
116
+
117
+ if (pkg.CheckDepends?.length) {
118
+ md += `\n## Check Dependencies\n\n`;
119
+ for (const dep of pkg.CheckDepends) {
120
+ md += `- ${dep}\n`;
121
+ }
122
+ }
123
+
124
+ // Package relationships
125
+ if (pkg.Provides?.length) {
126
+ md += `\n## Provides\n\n`;
127
+ for (const p of pkg.Provides) {
128
+ md += `- ${p}\n`;
129
+ }
130
+ }
131
+
132
+ if (pkg.Conflicts?.length) {
133
+ md += `\n## Conflicts\n\n`;
134
+ for (const c of pkg.Conflicts) {
135
+ md += `- ${c}\n`;
136
+ }
137
+ }
138
+
139
+ if (pkg.Replaces?.length) {
140
+ md += `\n## Replaces\n\n`;
141
+ for (const r of pkg.Replaces) {
142
+ md += `- ${r}\n`;
143
+ }
144
+ }
145
+
146
+ // Installation instructions
147
+ md += `\n---\n\n## Installation\n\n`;
148
+ md += "```bash\n";
149
+ md += `# Using an AUR helper (e.g., yay, paru)\n`;
150
+ md += `yay -S ${pkg.Name}\n\n`;
151
+ md += `# Manual installation\n`;
152
+ md += `git clone https://aur.archlinux.org/${pkg.PackageBase}.git\n`;
153
+ md += `cd ${pkg.PackageBase}\n`;
154
+ md += `makepkg -si\n`;
155
+ md += "```\n";
156
+
157
+ const output = finalizeOutput(md);
158
+ return {
159
+ url,
160
+ finalUrl: url,
161
+ contentType: "text/markdown",
162
+ method: "aur",
163
+ content: output.content,
164
+ fetchedAt,
165
+ truncated: output.truncated,
166
+ notes: ["Fetched via AUR RPC API"],
167
+ };
168
+ } catch {}
169
+
170
+ return null;
171
+ };