@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.
- package/CHANGELOG.md +90 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +369 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/complete.ts +2 -4
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +6 -8
- package/src/core/tools/jtd-to-json-schema.ts +174 -196
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +58 -9
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +55 -32
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +152 -76
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/executor.ts +204 -67
- package/src/core/tools/task/index.ts +129 -92
- package/src/core/tools/task/name-generator.ts +1544 -214
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +34 -11
- package/src/core/tools/task/worker.ts +152 -27
- package/src/core/tools/web-fetch.ts +220 -1657
- package/src/core/tools/web-scrapers/academic.test.ts +239 -0
- package/src/core/tools/web-scrapers/artifacthub.ts +215 -0
- package/src/core/tools/web-scrapers/arxiv.ts +88 -0
- package/src/core/tools/web-scrapers/aur.ts +175 -0
- package/src/core/tools/web-scrapers/biorxiv.ts +141 -0
- package/src/core/tools/web-scrapers/bluesky.ts +284 -0
- package/src/core/tools/web-scrapers/brew.ts +177 -0
- package/src/core/tools/web-scrapers/business.test.ts +82 -0
- package/src/core/tools/web-scrapers/cheatsh.ts +78 -0
- package/src/core/tools/web-scrapers/chocolatey.ts +158 -0
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/web-scrapers/coingecko.ts +184 -0
- package/src/core/tools/web-scrapers/crates-io.ts +128 -0
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/web-scrapers/dev-platforms.test.ts +254 -0
- package/src/core/tools/web-scrapers/devto.ts +177 -0
- package/src/core/tools/web-scrapers/discogs.ts +308 -0
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/web-scrapers/dockerhub.ts +160 -0
- package/src/core/tools/web-scrapers/documentation.test.ts +85 -0
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/finance-media.test.ts +144 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/web-scrapers/git-hosting.test.ts +272 -0
- package/src/core/tools/web-scrapers/github-gist.ts +68 -0
- package/src/core/tools/web-scrapers/github.ts +455 -0
- package/src/core/tools/web-scrapers/gitlab.ts +456 -0
- package/src/core/tools/web-scrapers/go-pkg.ts +275 -0
- package/src/core/tools/web-scrapers/hackage.ts +94 -0
- package/src/core/tools/web-scrapers/hackernews.ts +208 -0
- package/src/core/tools/web-scrapers/hex.ts +121 -0
- package/src/core/tools/web-scrapers/huggingface.ts +385 -0
- package/src/core/tools/web-scrapers/iacr.ts +86 -0
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/web-scrapers/lobsters.ts +186 -0
- package/src/core/tools/web-scrapers/mastodon.ts +310 -0
- package/src/core/tools/web-scrapers/maven.ts +152 -0
- package/src/core/tools/web-scrapers/mdn.ts +174 -0
- package/src/core/tools/web-scrapers/media.test.ts +138 -0
- package/src/core/tools/web-scrapers/metacpan.ts +253 -0
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/web-scrapers/npm.ts +114 -0
- package/src/core/tools/web-scrapers/nuget.ts +205 -0
- package/src/core/tools/web-scrapers/nvd.ts +243 -0
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/web-scrapers/opencorporates.ts +275 -0
- package/src/core/tools/web-scrapers/openlibrary.ts +319 -0
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/web-scrapers/osv.ts +189 -0
- package/src/core/tools/web-scrapers/package-managers-2.test.ts +199 -0
- package/src/core/tools/web-scrapers/package-managers.test.ts +171 -0
- package/src/core/tools/web-scrapers/package-registries.test.ts +259 -0
- package/src/core/tools/web-scrapers/packagist.ts +174 -0
- package/src/core/tools/web-scrapers/pub-dev.ts +185 -0
- package/src/core/tools/web-scrapers/pubmed.ts +178 -0
- package/src/core/tools/web-scrapers/pypi.ts +129 -0
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/web-scrapers/readthedocs.ts +126 -0
- package/src/core/tools/web-scrapers/reddit.ts +104 -0
- package/src/core/tools/web-scrapers/repology.ts +262 -0
- package/src/core/tools/web-scrapers/research.test.ts +107 -0
- package/src/core/tools/web-scrapers/rfc.ts +209 -0
- package/src/core/tools/web-scrapers/rubygems.ts +117 -0
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/web-scrapers/sec-edgar.ts +274 -0
- package/src/core/tools/web-scrapers/security.test.ts +103 -0
- package/src/core/tools/web-scrapers/semantic-scholar.ts +190 -0
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/social-extended.test.ts +192 -0
- package/src/core/tools/web-scrapers/social.test.ts +259 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/web-scrapers/spotify.ts +218 -0
- package/src/core/tools/web-scrapers/stackexchange.test.ts +120 -0
- package/src/core/tools/web-scrapers/stackoverflow.ts +124 -0
- package/src/core/tools/web-scrapers/standards.test.ts +122 -0
- package/src/core/tools/web-scrapers/terraform.ts +304 -0
- package/src/core/tools/web-scrapers/tldr.ts +51 -0
- package/src/core/tools/web-scrapers/twitter.ts +96 -0
- package/src/core/tools/web-scrapers/types.ts +234 -0
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/web-scrapers/vimeo.ts +152 -0
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/web-scrapers/wikidata.ts +357 -0
- package/src/core/tools/web-scrapers/wikipedia.test.ts +73 -0
- package/src/core/tools/web-scrapers/wikipedia.ts +95 -0
- package/src/core/tools/web-scrapers/youtube.test.ts +198 -0
- package/src/core/tools/web-scrapers/youtube.ts +371 -0
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -38
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/src/utils/tools-manager.ts +110 -8
- 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
|
+
};
|