@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,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,215 @@
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 (
57
+ url: string,
58
+ timeout: number,
59
+ signal?: AbortSignal,
60
+ ): Promise<RenderResult | null> => {
61
+ try {
62
+ const parsed = new URL(url);
63
+ if (parsed.hostname !== "artifacthub.io" && parsed.hostname !== "www.artifacthub.io") return null;
64
+
65
+ // Extract kind, repo, and package name from /packages/{kind}/{repo}/{name}
66
+ const match = parsed.pathname.match(/^\/packages\/([^/]+)\/([^/]+)\/([^/]+)/);
67
+ if (!match) return null;
68
+
69
+ const [, kind, repo, name] = match;
70
+ const fetchedAt = new Date().toISOString();
71
+
72
+ // Fetch from Artifact Hub API
73
+ const apiUrl = `https://artifacthub.io/api/v1/packages/${kind}/${repo}/${name}`;
74
+ const result = await loadPage(apiUrl, {
75
+ timeout,
76
+ headers: { Accept: "application/json" },
77
+ signal,
78
+ });
79
+
80
+ if (!result.ok) return null;
81
+
82
+ let pkg: ArtifactHubPackage;
83
+ try {
84
+ pkg = JSON.parse(result.content);
85
+ } catch {
86
+ return null;
87
+ }
88
+
89
+ const displayName = pkg.display_name || pkg.name;
90
+ const kindLabel = formatKindLabel(kind);
91
+
92
+ let md = `# ${displayName}\n\n`;
93
+ if (pkg.description) md += `${pkg.description}\n\n`;
94
+
95
+ // Basic info line
96
+ md += `**Type:** ${kindLabel}`;
97
+ md += ` · **Version:** ${pkg.version}`;
98
+ if (pkg.app_version) md += ` · **App Version:** ${pkg.app_version}`;
99
+ if (pkg.license) md += ` · **License:** ${pkg.license}`;
100
+ md += "\n";
101
+
102
+ // Stats and badges
103
+ const badges: string[] = [];
104
+ if (pkg.official) badges.push("Official");
105
+ if (pkg.signed) badges.push("Signed");
106
+ if (pkg.stars) badges.push(`${formatCount(pkg.stars)} stars`);
107
+ if (badges.length > 0) md += `**${badges.join(" · ")}**\n`;
108
+ md += "\n";
109
+
110
+ // Repository info
111
+ const repoDisplay =
112
+ pkg.repository.organization_display_name || pkg.repository.display_name || pkg.repository.name;
113
+ md += `**Repository:** ${repoDisplay}`;
114
+ if (pkg.repository.url) md += ` ([${pkg.repository.url}](${pkg.repository.url}))`;
115
+ md += "\n";
116
+
117
+ if (pkg.home_url) md += `**Homepage:** ${pkg.home_url}\n`;
118
+ if (pkg.keywords?.length) md += `**Keywords:** ${pkg.keywords.join(", ")}\n`;
119
+
120
+ // Maintainers
121
+ if (pkg.maintainers?.length) {
122
+ const maintainerNames = pkg.maintainers.map((m) => m.name).join(", ");
123
+ md += `**Maintainers:** ${maintainerNames}\n`;
124
+ }
125
+
126
+ // Security report summary
127
+ if (pkg.security_report_summary) {
128
+ const sec = pkg.security_report_summary;
129
+ const parts: string[] = [];
130
+ if (sec.critical) parts.push(`${sec.critical} critical`);
131
+ if (sec.high) parts.push(`${sec.high} high`);
132
+ if (sec.medium) parts.push(`${sec.medium} medium`);
133
+ if (sec.low) parts.push(`${sec.low} low`);
134
+ if (parts.length > 0) {
135
+ md += `**Security:** ${parts.join(", ")}\n`;
136
+ }
137
+ }
138
+
139
+ // Links
140
+ if (pkg.links?.length) {
141
+ md += `\n## Links\n\n`;
142
+ for (const link of pkg.links) {
143
+ md += `- [${link.name}](${link.url})\n`;
144
+ }
145
+ }
146
+
147
+ // Install instructions
148
+ if (pkg.install) {
149
+ md += `\n## Installation\n\n\`\`\`bash\n${pkg.install.trim()}\n\`\`\`\n`;
150
+ }
151
+
152
+ // Recent versions
153
+ if (pkg.available_versions?.length) {
154
+ md += `\n## Recent Versions\n\n`;
155
+ for (const ver of pkg.available_versions.slice(0, 5)) {
156
+ const date = new Date(ver.ts * 1000).toISOString().split("T")[0];
157
+ md += `- **${ver.version}** (${date})\n`;
158
+ }
159
+ }
160
+
161
+ // README
162
+ if (pkg.readme) {
163
+ md += `\n---\n\n## README\n\n${pkg.readme}\n`;
164
+ }
165
+
166
+ const output = finalizeOutput(md);
167
+ return {
168
+ url,
169
+ finalUrl: url,
170
+ contentType: "text/markdown",
171
+ method: "artifacthub",
172
+ content: output.content,
173
+ fetchedAt,
174
+ truncated: output.truncated,
175
+ notes: [`Fetched via Artifact Hub API (${kindLabel})`],
176
+ };
177
+ } catch {}
178
+
179
+ return null;
180
+ };
181
+
182
+ /**
183
+ * Convert kind slug to display label
184
+ */
185
+ function formatKindLabel(kind: string): string {
186
+ const labels: Record<string, string> = {
187
+ helm: "Helm Chart",
188
+ "helm-plugin": "Helm Plugin",
189
+ falco: "Falco Rules",
190
+ opa: "OPA Policy",
191
+ olm: "OLM Operator",
192
+ tbaction: "Tinkerbell Action",
193
+ krew: "Krew Plugin",
194
+ tekton: "Tekton Task",
195
+ "tekton-pipeline": "Tekton Pipeline",
196
+ keda: "KEDA Scaler",
197
+ coredns: "CoreDNS Plugin",
198
+ keptn: "Keptn Integration",
199
+ container: "Container Image",
200
+ kubewarden: "Kubewarden Policy",
201
+ gatekeeper: "Gatekeeper Policy",
202
+ kyverno: "Kyverno Policy",
203
+ "knative-client": "Knative Client Plugin",
204
+ backstage: "Backstage Plugin",
205
+ argo: "Argo Template",
206
+ kubearmor: "KubeArmor Policy",
207
+ kcl: "KCL Module",
208
+ headlamp: "Headlamp Plugin",
209
+ inspektor: "Inspektor Gadget",
210
+ "meshery-design": "Meshery Design",
211
+ "opencost-plugin": "OpenCost Plugin",
212
+ radius: "Radius Recipe",
213
+ };
214
+ return labels[kind] || kind.charAt(0).toUpperCase() + kind.slice(1);
215
+ }
@@ -0,0 +1,88 @@
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 (
10
+ url: string,
11
+ timeout: number,
12
+ signal?: AbortSignal,
13
+ ): Promise<RenderResult | null> => {
14
+ try {
15
+ const parsed = new URL(url);
16
+ if (parsed.hostname !== "arxiv.org") return null;
17
+
18
+ // Extract paper ID from various URL formats
19
+ // /abs/1234.56789, /pdf/1234.56789, /abs/cs/0123456
20
+ const match = parsed.pathname.match(/\/(abs|pdf)\/(.+?)(?:\.pdf)?$/);
21
+ if (!match) return null;
22
+
23
+ const paperId = match[2];
24
+ const fetchedAt = new Date().toISOString();
25
+ const notes: string[] = [];
26
+
27
+ // Fetch metadata via arXiv API
28
+ const apiUrl = `https://export.arxiv.org/api/query?id_list=${paperId}`;
29
+ const result = await loadPage(apiUrl, { timeout, signal });
30
+
31
+ if (!result.ok) return null;
32
+
33
+ // Parse the Atom feed response
34
+ const doc = parseHtml(result.content, { parseNoneClosedTags: true });
35
+ const entry = doc.querySelector("entry");
36
+
37
+ if (!entry) return null;
38
+
39
+ const title = entry.querySelector("title")?.text?.trim()?.replace(/\s+/g, " ");
40
+ const summary = entry.querySelector("summary")?.text?.trim();
41
+ const authors = entry
42
+ .querySelectorAll("author name")
43
+ .map((n) => n.text?.trim())
44
+ .filter(Boolean);
45
+ const published = entry.querySelector("published")?.text?.trim()?.split("T")[0];
46
+ const categories = entry
47
+ .querySelectorAll("category")
48
+ .map((c) => c.getAttribute("term"))
49
+ .filter(Boolean);
50
+ const pdfLink = entry.querySelector('link[title="pdf"]')?.getAttribute("href");
51
+
52
+ let md = `# ${title || "arXiv Paper"}\n\n`;
53
+ if (authors.length) md += `**Authors:** ${authors.join(", ")}\n`;
54
+ if (published) md += `**Published:** ${published}\n`;
55
+ if (categories.length) md += `**Categories:** ${categories.join(", ")}\n`;
56
+ md += `**arXiv:** ${paperId}\n\n`;
57
+ md += `---\n\n## Abstract\n\n${summary || "No abstract available."}\n\n`;
58
+
59
+ // If it was a PDF link or we want full content, try to fetch and convert PDF
60
+ if (match[1] === "pdf" || parsed.pathname.includes(".pdf")) {
61
+ if (pdfLink) {
62
+ notes.push("Fetching PDF for full content...");
63
+ const pdfResult = await fetchBinary(pdfLink, timeout, signal);
64
+ if (pdfResult.ok) {
65
+ const converted = await convertWithMarkitdown(pdfResult.buffer, ".pdf", timeout, signal);
66
+ if (converted.ok && converted.content.length > 500) {
67
+ md += `---\n\n## Full Paper\n\n${converted.content}\n`;
68
+ notes.push("PDF converted via markitdown");
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ const output = finalizeOutput(md);
75
+ return {
76
+ url,
77
+ finalUrl: url,
78
+ contentType: "text/markdown",
79
+ method: "arxiv",
80
+ content: output.content,
81
+ fetchedAt,
82
+ truncated: output.truncated,
83
+ notes: notes.length ? notes : ["Fetched via arXiv API"],
84
+ };
85
+ } catch {}
86
+
87
+ return null;
88
+ };