@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,274 @@
1
+ import type { RenderResult, SpecialHandler } from "./types";
2
+ import { finalizeOutput, loadPage } from "./types";
3
+
4
+ interface SecFiling {
5
+ accessionNumber: string;
6
+ filingDate: string;
7
+ reportDate: string;
8
+ acceptanceDateTime: string;
9
+ form: string;
10
+ primaryDocument: string;
11
+ primaryDocDescription: string;
12
+ }
13
+
14
+ interface SecCompany {
15
+ cik: string;
16
+ entityType: string;
17
+ sic: string;
18
+ sicDescription: string;
19
+ name: string;
20
+ tickers: string[];
21
+ exchanges: string[];
22
+ ein: string;
23
+ stateOfIncorporation: string;
24
+ fiscalYearEnd: string;
25
+ addresses: {
26
+ business: {
27
+ street1?: string;
28
+ street2?: string;
29
+ city?: string;
30
+ stateOrCountry?: string;
31
+ zipCode?: string;
32
+ };
33
+ mailing: {
34
+ street1?: string;
35
+ street2?: string;
36
+ city?: string;
37
+ stateOrCountry?: string;
38
+ zipCode?: string;
39
+ };
40
+ };
41
+ filings: {
42
+ recent: {
43
+ accessionNumber: string[];
44
+ filingDate: string[];
45
+ reportDate: string[];
46
+ acceptanceDateTime: string[];
47
+ form: string[];
48
+ primaryDocument: string[];
49
+ primaryDocDescription: string[];
50
+ };
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Extract CIK from various SEC EDGAR URL patterns
56
+ */
57
+ function extractCik(url: URL): string | null {
58
+ const { hostname, pathname, searchParams } = url;
59
+
60
+ // Check hostname
61
+ if (!hostname.includes("sec.gov")) return null;
62
+
63
+ // Pattern: ?CIK=xxx or ?cik=xxx
64
+ const cikParam = searchParams.get("CIK") || searchParams.get("cik");
65
+ if (cikParam) {
66
+ return normalizeCik(cikParam);
67
+ }
68
+
69
+ // Pattern: /cik/XXXXXXXXXX or /cik/XXXXXXXXXX/...
70
+ const cikPathMatch = pathname.match(/\/cik\/(\d+)/i);
71
+ if (cikPathMatch) {
72
+ return normalizeCik(cikPathMatch[1]);
73
+ }
74
+
75
+ // Pattern: /submissions/CIK*.json
76
+ const submissionsMatch = pathname.match(/\/submissions\/CIK(\d+)\.json/i);
77
+ if (submissionsMatch) {
78
+ return normalizeCik(submissionsMatch[1]);
79
+ }
80
+
81
+ // Pattern: /cgi-bin/browse-edgar with company search (no CIK yet)
82
+ if (pathname.includes("/cgi-bin/browse-edgar") && searchParams.get("company")) {
83
+ // Company name search - we'd need to search first, skip for now
84
+ return null;
85
+ }
86
+
87
+ // Pattern: Filing URLs like /Archives/edgar/data/XXXXXXXXXX/...
88
+ const archivesMatch = pathname.match(/\/Archives\/edgar\/data\/(\d+)/);
89
+ if (archivesMatch) {
90
+ return normalizeCik(archivesMatch[1]);
91
+ }
92
+
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * Normalize CIK to 10 digits with leading zeros
98
+ */
99
+ function normalizeCik(cik: string): string {
100
+ const cleaned = cik.replace(/\D/g, "");
101
+ return cleaned.padStart(10, "0");
102
+ }
103
+
104
+ /**
105
+ * Format address for display
106
+ */
107
+ function formatAddress(addr: SecCompany["addresses"]["business"]): string {
108
+ const parts: string[] = [];
109
+ if (addr.street1) parts.push(addr.street1);
110
+ if (addr.street2) parts.push(addr.street2);
111
+
112
+ const cityLine: string[] = [];
113
+ if (addr.city) cityLine.push(addr.city);
114
+ if (addr.stateOrCountry) cityLine.push(addr.stateOrCountry);
115
+ if (addr.zipCode) cityLine.push(addr.zipCode);
116
+ if (cityLine.length) parts.push(cityLine.join(", "));
117
+
118
+ return parts.join("\n");
119
+ }
120
+
121
+ /**
122
+ * Get recent filings of specific types
123
+ */
124
+ function getRecentFilings(company: SecCompany, formTypes: string[], limit = 10): SecFiling[] {
125
+ const { recent } = company.filings;
126
+ const filings: SecFiling[] = [];
127
+
128
+ for (let i = 0; i < recent.form.length && filings.length < limit; i++) {
129
+ if (formTypes.length === 0 || formTypes.includes(recent.form[i])) {
130
+ filings.push({
131
+ accessionNumber: recent.accessionNumber[i],
132
+ filingDate: recent.filingDate[i],
133
+ reportDate: recent.reportDate[i],
134
+ acceptanceDateTime: recent.acceptanceDateTime[i],
135
+ form: recent.form[i],
136
+ primaryDocument: recent.primaryDocument[i],
137
+ primaryDocDescription: recent.primaryDocDescription[i],
138
+ });
139
+ }
140
+ }
141
+
142
+ return filings;
143
+ }
144
+
145
+ /**
146
+ * Build SEC EDGAR filing URL
147
+ */
148
+ function buildFilingUrl(cik: string, accessionNumber: string, document: string): string {
149
+ const accessionNoDashes = accessionNumber.replace(/-/g, "");
150
+ return `https://www.sec.gov/Archives/edgar/data/${parseInt(cik, 10)}/${accessionNoDashes}/${document}`;
151
+ }
152
+
153
+ /**
154
+ * Handle SEC EDGAR URLs via data.sec.gov API
155
+ */
156
+ export const handleSecEdgar: SpecialHandler = async (
157
+ url: string,
158
+ timeout: number,
159
+ signal?: AbortSignal,
160
+ ): Promise<RenderResult | null> => {
161
+ try {
162
+ const parsed = new URL(url);
163
+
164
+ // Check if it's an SEC URL
165
+ if (!parsed.hostname.includes("sec.gov")) return null;
166
+
167
+ // Extract CIK from URL
168
+ const cik = extractCik(parsed);
169
+ if (!cik) return null;
170
+
171
+ const fetchedAt = new Date().toISOString();
172
+
173
+ // Fetch company data from SEC API
174
+ // SEC requires a proper User-Agent with contact info
175
+ const apiUrl = `https://data.sec.gov/submissions/CIK${cik}.json`;
176
+ const result = await loadPage(apiUrl, {
177
+ timeout,
178
+ signal,
179
+ headers: {
180
+ "User-Agent": "CodingAgent/1.0 (research tool)",
181
+ Accept: "application/json",
182
+ },
183
+ });
184
+
185
+ if (!result.ok) return null;
186
+
187
+ let company: SecCompany;
188
+ try {
189
+ company = JSON.parse(result.content);
190
+ } catch {
191
+ return null;
192
+ }
193
+
194
+ // Build markdown output
195
+ let md = `# ${company.name}\n\n`;
196
+
197
+ // Basic info
198
+ md += `**CIK:** ${company.cik}`;
199
+ if (company.tickers?.length) {
200
+ md += ` · **Ticker${company.tickers.length > 1 ? "s" : ""}:** ${company.tickers.join(", ")}`;
201
+ }
202
+ if (company.exchanges?.length) {
203
+ md += ` (${company.exchanges.join(", ")})`;
204
+ }
205
+ md += "\n";
206
+
207
+ if (company.entityType) md += `**Entity Type:** ${company.entityType}\n`;
208
+ if (company.sic) md += `**SIC:** ${company.sic} - ${company.sicDescription}\n`;
209
+ if (company.stateOfIncorporation) md += `**State of Incorporation:** ${company.stateOfIncorporation}\n`;
210
+ if (company.ein) md += `**EIN:** ${company.ein}\n`;
211
+ if (company.fiscalYearEnd) {
212
+ const fy = company.fiscalYearEnd;
213
+ md += `**Fiscal Year End:** ${fy.slice(0, 2)}/${fy.slice(2)}\n`;
214
+ }
215
+ md += "\n";
216
+
217
+ // Business address
218
+ if (company.addresses?.business) {
219
+ const addr = formatAddress(company.addresses.business);
220
+ if (addr) {
221
+ md += `## Business Address\n\n${addr}\n\n`;
222
+ }
223
+ }
224
+
225
+ // Recent key filings (10-K, 10-Q, 8-K)
226
+ const keyFilings = getRecentFilings(company, ["10-K", "10-K/A", "10-Q", "10-Q/A", "8-K", "8-K/A"], 15);
227
+ if (keyFilings.length) {
228
+ md += `## Recent Filings (10-K, 10-Q, 8-K)\n\n`;
229
+ md += "| Date | Form | Description |\n";
230
+ md += "|------|------|-------------|\n";
231
+
232
+ for (const filing of keyFilings) {
233
+ const filingUrl = buildFilingUrl(cik, filing.accessionNumber, filing.primaryDocument);
234
+ const desc = filing.primaryDocDescription || filing.form;
235
+ md += `| ${filing.filingDate} | [${filing.form}](${filingUrl}) | ${desc} |\n`;
236
+ }
237
+ md += "\n";
238
+ }
239
+
240
+ // All recent filings (last 20)
241
+ const allFilings = getRecentFilings(company, [], 20);
242
+ if (allFilings.length) {
243
+ md += `## All Recent Filings\n\n`;
244
+ md += "| Date | Form | Description |\n";
245
+ md += "|------|------|-------------|\n";
246
+
247
+ for (const filing of allFilings) {
248
+ const filingUrl = buildFilingUrl(cik, filing.accessionNumber, filing.primaryDocument);
249
+ const desc = filing.primaryDocDescription || filing.form;
250
+ md += `| ${filing.filingDate} | [${filing.form}](${filingUrl}) | ${desc} |\n`;
251
+ }
252
+ md += "\n";
253
+ }
254
+
255
+ // Links
256
+ md += `## Links\n\n`;
257
+ md += `- [SEC EDGAR Filings](https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=${cik}&type=&dateb=&owner=include&count=40)\n`;
258
+ md += `- [Company Search](https://www.sec.gov/cgi-bin/browse-edgar?company=${encodeURIComponent(company.name)}&CIK=&type=&owner=include&count=40&action=getcompany)\n`;
259
+
260
+ const output = finalizeOutput(md);
261
+ return {
262
+ url,
263
+ finalUrl: url,
264
+ contentType: "text/markdown",
265
+ method: "sec-edgar",
266
+ content: output.content,
267
+ fetchedAt,
268
+ truncated: output.truncated,
269
+ notes: ["Fetched via SEC EDGAR API"],
270
+ };
271
+ } catch {}
272
+
273
+ return null;
274
+ };
@@ -0,0 +1,103 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { handleNvd } from "./nvd";
3
+ import { handleOsv } from "./osv";
4
+
5
+ const SKIP = !process.env.WEB_FETCH_INTEGRATION;
6
+
7
+ describe.skipIf(SKIP)("handleNvd", () => {
8
+ it("returns null for non-NVD URLs", async () => {
9
+ const result = await handleNvd("https://example.com", 20);
10
+ expect(result).toBeNull();
11
+ });
12
+
13
+ it("returns null for NVD URLs without CVE detail path", async () => {
14
+ const result = await handleNvd("https://nvd.nist.gov/", 20);
15
+ expect(result).toBeNull();
16
+ });
17
+
18
+ it("returns null for NVD search URLs", async () => {
19
+ const result = await handleNvd("https://nvd.nist.gov/vuln/search/results?query=log4j", 20);
20
+ expect(result).toBeNull();
21
+ });
22
+
23
+ it("fetches CVE-2021-44228 (Log4Shell)", async () => {
24
+ const result = await handleNvd("https://nvd.nist.gov/vuln/detail/CVE-2021-44228", 20);
25
+ expect(result).not.toBeNull();
26
+ expect(result?.method).toBe("nvd");
27
+ expect(result?.content).toContain("CVE-2021-44228");
28
+ expect(result?.content).toContain("Log4j");
29
+ expect(result?.content).toContain("CVSS");
30
+ expect(result?.contentType).toBe("text/markdown");
31
+ expect(result?.fetchedAt).toBeTruthy();
32
+ expect(result?.truncated).toBeDefined();
33
+ });
34
+
35
+ it("fetches CVE-2014-0160 (Heartbleed)", async () => {
36
+ const result = await handleNvd("https://nvd.nist.gov/vuln/detail/CVE-2014-0160", 20);
37
+ expect(result).not.toBeNull();
38
+ expect(result?.method).toBe("nvd");
39
+ expect(result?.content).toContain("CVE-2014-0160");
40
+ expect(result?.content).toContain("OpenSSL");
41
+ expect(result?.truncated).toBeDefined();
42
+ });
43
+
44
+ it("handles lowercase CVE IDs", async () => {
45
+ const result = await handleNvd("https://nvd.nist.gov/vuln/detail/cve-2021-44228", 20);
46
+ expect(result).not.toBeNull();
47
+ expect(result?.method).toBe("nvd");
48
+ expect(result?.content).toContain("CVE-2021-44228");
49
+ });
50
+ });
51
+
52
+ describe.skipIf(SKIP)("handleOsv", () => {
53
+ it("returns null for non-OSV URLs", async () => {
54
+ const result = await handleOsv("https://example.com", 20);
55
+ expect(result).toBeNull();
56
+ });
57
+
58
+ it("returns null for OSV homepage", async () => {
59
+ const result = await handleOsv("https://osv.dev/", 20);
60
+ expect(result).toBeNull();
61
+ });
62
+
63
+ it("returns null for OSV list URLs", async () => {
64
+ const result = await handleOsv("https://osv.dev/list", 20);
65
+ expect(result).toBeNull();
66
+ });
67
+
68
+ it("fetches GHSA-jfh8-c2jp-5v3q (log4j RCE)", async () => {
69
+ const result = await handleOsv("https://osv.dev/vulnerability/GHSA-jfh8-c2jp-5v3q", 20);
70
+ expect(result).not.toBeNull();
71
+ expect(result?.method).toBe("osv");
72
+ expect(result?.content).toContain("GHSA-jfh8-c2jp-5v3q");
73
+ expect(result?.content).toContain("log4j");
74
+ expect(result?.contentType).toBe("text/markdown");
75
+ expect(result?.fetchedAt).toBeTruthy();
76
+ expect(result?.truncated).toBeDefined();
77
+ });
78
+
79
+ it("fetches CVE-2021-44228 via OSV", async () => {
80
+ const result = await handleOsv("https://osv.dev/vulnerability/CVE-2021-44228", 20);
81
+ expect(result).not.toBeNull();
82
+ expect(result?.method).toBe("osv");
83
+ expect(result?.content).toContain("CVE-2021-44228");
84
+ expect(result?.truncated).toBeDefined();
85
+ });
86
+
87
+ it("fetches PYSEC vulnerability", async () => {
88
+ // PYSEC-2021-19 is a well-known pillow vulnerability
89
+ const result = await handleOsv("https://osv.dev/vulnerability/PYSEC-2021-19", 20);
90
+ expect(result).not.toBeNull();
91
+ expect(result?.method).toBe("osv");
92
+ expect(result?.content).toContain("PYSEC-2021-19");
93
+ expect(result?.content).toContain("Affected Packages");
94
+ });
95
+
96
+ it("fetches RUSTSEC vulnerability", async () => {
97
+ // RUSTSEC-2021-0119 is a well-known actix-web vulnerability
98
+ const result = await handleOsv("https://osv.dev/vulnerability/RUSTSEC-2021-0119", 20);
99
+ expect(result).not.toBeNull();
100
+ expect(result?.method).toBe("osv");
101
+ expect(result?.content).toContain("RUSTSEC-2021-0119");
102
+ });
103
+ });
@@ -0,0 +1,190 @@
1
+ import type { SpecialHandler } from "./types";
2
+ import { finalizeOutput, formatCount, loadPage } from "./types";
3
+
4
+ interface SemanticScholarAuthor {
5
+ name: string;
6
+ authorId?: string;
7
+ }
8
+
9
+ interface SemanticScholarPaper {
10
+ paperId: string;
11
+ title: string;
12
+ abstract?: string;
13
+ authors?: SemanticScholarAuthor[];
14
+ year?: number;
15
+ citationCount?: number;
16
+ referenceCount?: number;
17
+ fieldsOfStudy?: string[];
18
+ publicationTypes?: string[];
19
+ journal?: { name: string; volume?: string; pages?: string };
20
+ externalIds?: {
21
+ DOI?: string;
22
+ ArXiv?: string;
23
+ PubMed?: string;
24
+ MAG?: string;
25
+ CorpusId?: string;
26
+ };
27
+ tldr?: { text: string };
28
+ openAccessPdf?: { url: string };
29
+ }
30
+
31
+ function extractPaperId(url: string): string | null {
32
+ const patterns = [
33
+ /semanticscholar\.org\/paper\/[^/]+\/([a-f0-9]{40})/i,
34
+ /semanticscholar\.org\/paper\/([a-f0-9]{40})/i,
35
+ /api\.semanticscholar\.org\/.*\/paper\/([a-f0-9]{40})/i,
36
+ ];
37
+
38
+ for (const pattern of patterns) {
39
+ const match = url.match(pattern);
40
+ if (match?.[1]) return match[1];
41
+ }
42
+
43
+ return null;
44
+ }
45
+
46
+ export const handleSemanticScholar: SpecialHandler = async (url: string, timeout: number, signal?: AbortSignal) => {
47
+ if (!url.includes("semanticscholar.org")) return null;
48
+
49
+ const paperId = extractPaperId(url);
50
+ if (!paperId) {
51
+ return {
52
+ url,
53
+ finalUrl: url,
54
+ contentType: "text/plain",
55
+ method: "semantic-scholar",
56
+ content: "Failed to extract paper ID from Semantic Scholar URL",
57
+ fetchedAt: new Date().toISOString(),
58
+ truncated: false,
59
+ notes: ["Invalid URL format"],
60
+ };
61
+ }
62
+
63
+ const fields = [
64
+ "title",
65
+ "abstract",
66
+ "authors",
67
+ "year",
68
+ "citationCount",
69
+ "referenceCount",
70
+ "fieldsOfStudy",
71
+ "publicationTypes",
72
+ "journal",
73
+ "externalIds",
74
+ "tldr",
75
+ "openAccessPdf",
76
+ ].join(",");
77
+
78
+ const apiUrl = `https://api.semanticscholar.org/graph/v1/paper/${paperId}?fields=${fields}`;
79
+
80
+ const { content, ok, finalUrl } = await loadPage(apiUrl, { timeout, signal });
81
+
82
+ if (!ok || !content) {
83
+ return {
84
+ url,
85
+ finalUrl: apiUrl,
86
+ contentType: "text/plain",
87
+ method: "semantic-scholar",
88
+ content: "Failed to fetch paper from Semantic Scholar API",
89
+ fetchedAt: new Date().toISOString(),
90
+ truncated: false,
91
+ notes: ["API request failed"],
92
+ };
93
+ }
94
+
95
+ let paper: SemanticScholarPaper;
96
+ try {
97
+ paper = JSON.parse(content);
98
+ } catch {
99
+ return {
100
+ url,
101
+ finalUrl: apiUrl,
102
+ contentType: "text/plain",
103
+ method: "semantic-scholar",
104
+ content: "Failed to parse response from Semantic Scholar API",
105
+ fetchedAt: new Date().toISOString(),
106
+ truncated: false,
107
+ notes: ["JSON parse error"],
108
+ };
109
+ }
110
+
111
+ const sections: string[] = [];
112
+
113
+ sections.push(`# ${paper.title || "Untitled"}`);
114
+ sections.push("");
115
+
116
+ if (paper.authors && paper.authors.length > 0) {
117
+ const authorList = paper.authors.map((a) => a.name).join(", ");
118
+ sections.push(`**Authors:** ${authorList}`);
119
+ sections.push("");
120
+ }
121
+
122
+ const metadata: string[] = [];
123
+ if (paper.year) metadata.push(`Year: ${paper.year}`);
124
+ if (paper.journal?.name) metadata.push(`Venue: ${paper.journal.name}`);
125
+ if (paper.citationCount !== undefined) {
126
+ metadata.push(`Citations: ${formatCount(paper.citationCount)}`);
127
+ }
128
+ if (paper.referenceCount !== undefined) {
129
+ metadata.push(`References: ${formatCount(paper.referenceCount)}`);
130
+ }
131
+ if (metadata.length > 0) {
132
+ sections.push(metadata.join(" • "));
133
+ sections.push("");
134
+ }
135
+
136
+ if (paper.fieldsOfStudy && paper.fieldsOfStudy.length > 0) {
137
+ sections.push(`**Fields:** ${paper.fieldsOfStudy.join(", ")}`);
138
+ sections.push("");
139
+ }
140
+
141
+ if (paper.tldr?.text) {
142
+ sections.push("## TL;DR");
143
+ sections.push("");
144
+ sections.push(paper.tldr.text);
145
+ sections.push("");
146
+ }
147
+
148
+ if (paper.abstract) {
149
+ sections.push("## Abstract");
150
+ sections.push("");
151
+ sections.push(paper.abstract);
152
+ sections.push("");
153
+ }
154
+
155
+ const links: string[] = [];
156
+ if (paper.openAccessPdf?.url) {
157
+ links.push(`[PDF](${paper.openAccessPdf.url})`);
158
+ }
159
+ if (paper.externalIds?.ArXiv) {
160
+ links.push(`[arXiv](https://arxiv.org/abs/${paper.externalIds.ArXiv})`);
161
+ }
162
+ if (paper.externalIds?.DOI) {
163
+ links.push(`[DOI](https://doi.org/${paper.externalIds.DOI})`);
164
+ }
165
+ if (paper.externalIds?.PubMed) {
166
+ links.push(`[PubMed](https://pubmed.ncbi.nlm.nih.gov/${paper.externalIds.PubMed}/)`);
167
+ }
168
+ links.push(`[Semantic Scholar](https://www.semanticscholar.org/paper/${paper.paperId})`);
169
+
170
+ if (links.length > 0) {
171
+ sections.push("## Links");
172
+ sections.push("");
173
+ sections.push(links.join(" • "));
174
+ sections.push("");
175
+ }
176
+
177
+ const fullContent = sections.join("\n");
178
+ const { content: finalContent, truncated } = finalizeOutput(fullContent);
179
+
180
+ return {
181
+ url,
182
+ finalUrl,
183
+ contentType: "text/markdown",
184
+ method: "semantic-scholar",
185
+ content: finalContent,
186
+ fetchedAt: new Date().toISOString(),
187
+ truncated,
188
+ notes: [],
189
+ };
190
+ };