@oh-my-pi/pi-coding-agent 3.25.0 → 3.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/package.json +4 -4
- package/src/core/tools/complete.ts +2 -4
- package/src/core/tools/jtd-to-json-schema.ts +174 -196
- package/src/core/tools/read.ts +4 -4
- package/src/core/tools/task/executor.ts +146 -20
- package/src/core/tools/task/name-generator.ts +1544 -214
- package/src/core/tools/task/types.ts +19 -5
- package/src/core/tools/task/worker.ts +103 -13
- package/src/core/tools/web-fetch-handlers/academic.test.ts +239 -0
- package/src/core/tools/web-fetch-handlers/artifacthub.ts +210 -0
- package/src/core/tools/web-fetch-handlers/arxiv.ts +84 -0
- package/src/core/tools/web-fetch-handlers/aur.ts +171 -0
- package/src/core/tools/web-fetch-handlers/biorxiv.ts +136 -0
- package/src/core/tools/web-fetch-handlers/bluesky.ts +277 -0
- package/src/core/tools/web-fetch-handlers/brew.ts +173 -0
- package/src/core/tools/web-fetch-handlers/business.test.ts +82 -0
- package/src/core/tools/web-fetch-handlers/cheatsh.ts +73 -0
- package/src/core/tools/web-fetch-handlers/chocolatey.ts +153 -0
- package/src/core/tools/web-fetch-handlers/coingecko.ts +179 -0
- package/src/core/tools/web-fetch-handlers/crates-io.ts +123 -0
- package/src/core/tools/web-fetch-handlers/dev-platforms.test.ts +254 -0
- package/src/core/tools/web-fetch-handlers/devto.ts +173 -0
- package/src/core/tools/web-fetch-handlers/discogs.ts +303 -0
- package/src/core/tools/web-fetch-handlers/dockerhub.ts +156 -0
- package/src/core/tools/web-fetch-handlers/documentation.test.ts +85 -0
- package/src/core/tools/web-fetch-handlers/finance-media.test.ts +144 -0
- package/src/core/tools/web-fetch-handlers/git-hosting.test.ts +272 -0
- package/src/core/tools/web-fetch-handlers/github-gist.ts +64 -0
- package/src/core/tools/web-fetch-handlers/github.ts +424 -0
- package/src/core/tools/web-fetch-handlers/gitlab.ts +444 -0
- package/src/core/tools/web-fetch-handlers/go-pkg.ts +271 -0
- package/src/core/tools/web-fetch-handlers/hackage.ts +89 -0
- package/src/core/tools/web-fetch-handlers/hackernews.ts +208 -0
- package/src/core/tools/web-fetch-handlers/hex.ts +121 -0
- package/src/core/tools/web-fetch-handlers/huggingface.ts +385 -0
- package/src/core/tools/web-fetch-handlers/iacr.ts +82 -0
- package/src/core/tools/web-fetch-handlers/index.ts +69 -0
- package/src/core/tools/web-fetch-handlers/lobsters.ts +186 -0
- package/src/core/tools/web-fetch-handlers/mastodon.ts +302 -0
- package/src/core/tools/web-fetch-handlers/maven.ts +147 -0
- package/src/core/tools/web-fetch-handlers/mdn.ts +174 -0
- package/src/core/tools/web-fetch-handlers/media.test.ts +138 -0
- package/src/core/tools/web-fetch-handlers/metacpan.ts +247 -0
- package/src/core/tools/web-fetch-handlers/npm.ts +107 -0
- package/src/core/tools/web-fetch-handlers/nuget.ts +201 -0
- package/src/core/tools/web-fetch-handlers/nvd.ts +238 -0
- package/src/core/tools/web-fetch-handlers/opencorporates.ts +273 -0
- package/src/core/tools/web-fetch-handlers/openlibrary.ts +313 -0
- package/src/core/tools/web-fetch-handlers/osv.ts +184 -0
- package/src/core/tools/web-fetch-handlers/package-managers-2.test.ts +199 -0
- package/src/core/tools/web-fetch-handlers/package-managers.test.ts +171 -0
- package/src/core/tools/web-fetch-handlers/package-registries.test.ts +259 -0
- package/src/core/tools/web-fetch-handlers/packagist.ts +170 -0
- package/src/core/tools/web-fetch-handlers/pub-dev.ts +185 -0
- package/src/core/tools/web-fetch-handlers/pubmed.ts +174 -0
- package/src/core/tools/web-fetch-handlers/pypi.ts +125 -0
- package/src/core/tools/web-fetch-handlers/readthedocs.ts +122 -0
- package/src/core/tools/web-fetch-handlers/reddit.ts +100 -0
- package/src/core/tools/web-fetch-handlers/repology.ts +257 -0
- package/src/core/tools/web-fetch-handlers/research.test.ts +107 -0
- package/src/core/tools/web-fetch-handlers/rfc.ts +205 -0
- package/src/core/tools/web-fetch-handlers/rubygems.ts +112 -0
- package/src/core/tools/web-fetch-handlers/sec-edgar.ts +269 -0
- package/src/core/tools/web-fetch-handlers/security.test.ts +103 -0
- package/src/core/tools/web-fetch-handlers/semantic-scholar.ts +190 -0
- package/src/core/tools/web-fetch-handlers/social-extended.test.ts +192 -0
- package/src/core/tools/web-fetch-handlers/social.test.ts +259 -0
- package/src/core/tools/web-fetch-handlers/spotify.ts +218 -0
- package/src/core/tools/web-fetch-handlers/stackexchange.test.ts +120 -0
- package/src/core/tools/web-fetch-handlers/stackoverflow.ts +123 -0
- package/src/core/tools/web-fetch-handlers/standards.test.ts +122 -0
- package/src/core/tools/web-fetch-handlers/terraform.ts +296 -0
- package/src/core/tools/web-fetch-handlers/tldr.ts +47 -0
- package/src/core/tools/web-fetch-handlers/twitter.ts +84 -0
- package/src/core/tools/web-fetch-handlers/types.ts +163 -0
- package/src/core/tools/web-fetch-handlers/utils.ts +91 -0
- package/src/core/tools/web-fetch-handlers/vimeo.ts +152 -0
- package/src/core/tools/web-fetch-handlers/wikidata.ts +349 -0
- package/src/core/tools/web-fetch-handlers/wikipedia.test.ts +73 -0
- package/src/core/tools/web-fetch-handlers/wikipedia.ts +91 -0
- package/src/core/tools/web-fetch-handlers/youtube.test.ts +198 -0
- package/src/core/tools/web-fetch-handlers/youtube.ts +319 -0
- package/src/core/tools/web-fetch.ts +152 -1324
- package/src/utils/tools-manager.ts +110 -8
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface NuGetCatalogEntry {
|
|
5
|
+
id: string;
|
|
6
|
+
version: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
authors?: string;
|
|
9
|
+
projectUrl?: string;
|
|
10
|
+
licenseUrl?: string;
|
|
11
|
+
licenseExpression?: string;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
dependencyGroups?: Array<{
|
|
14
|
+
targetFramework?: string;
|
|
15
|
+
dependencies?: Array<{
|
|
16
|
+
id: string;
|
|
17
|
+
range: string;
|
|
18
|
+
}>;
|
|
19
|
+
}>;
|
|
20
|
+
published?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface NuGetRegistrationItem {
|
|
24
|
+
catalogEntry: NuGetCatalogEntry;
|
|
25
|
+
packageContent?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface NuGetRegistrationPage {
|
|
29
|
+
items?: NuGetRegistrationItem[];
|
|
30
|
+
"@id"?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface NuGetRegistrationIndex {
|
|
34
|
+
items: NuGetRegistrationPage[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Handle NuGet URLs via API
|
|
39
|
+
*/
|
|
40
|
+
export const handleNuGet: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = new URL(url);
|
|
43
|
+
if (parsed.hostname !== "www.nuget.org" && parsed.hostname !== "nuget.org") return null;
|
|
44
|
+
|
|
45
|
+
// Extract package name and optional version from /packages/name or /packages/name/version
|
|
46
|
+
const match = parsed.pathname.match(/^\/packages\/([^/]+)(?:\/([^/]+))?/i);
|
|
47
|
+
if (!match) return null;
|
|
48
|
+
|
|
49
|
+
const packageName = decodeURIComponent(match[1]);
|
|
50
|
+
const requestedVersion = match[2] ? decodeURIComponent(match[2]) : null;
|
|
51
|
+
const fetchedAt = new Date().toISOString();
|
|
52
|
+
|
|
53
|
+
// Fetch from NuGet registration API (package name must be lowercase)
|
|
54
|
+
const apiUrl = `https://api.nuget.org/v3/registration5-gz-semver2/${packageName.toLowerCase()}/index.json`;
|
|
55
|
+
const result = await loadPage(apiUrl, { timeout });
|
|
56
|
+
|
|
57
|
+
if (!result.ok) return null;
|
|
58
|
+
|
|
59
|
+
let index: NuGetRegistrationIndex;
|
|
60
|
+
try {
|
|
61
|
+
index = JSON.parse(result.content);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!index.items?.length) return null;
|
|
67
|
+
|
|
68
|
+
// Get the latest page (or fetch it if not inlined)
|
|
69
|
+
let latestPage = index.items[index.items.length - 1];
|
|
70
|
+
|
|
71
|
+
// If items are not inlined, fetch the page
|
|
72
|
+
if (!latestPage.items && latestPage["@id"]) {
|
|
73
|
+
const pageResult = await loadPage(latestPage["@id"], { timeout });
|
|
74
|
+
if (!pageResult.ok) return null;
|
|
75
|
+
try {
|
|
76
|
+
latestPage = JSON.parse(pageResult.content);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!latestPage.items?.length) return null;
|
|
83
|
+
|
|
84
|
+
// Find the requested version or get the latest
|
|
85
|
+
let targetEntry: NuGetCatalogEntry | null = null;
|
|
86
|
+
|
|
87
|
+
if (requestedVersion) {
|
|
88
|
+
// Search all pages for the requested version
|
|
89
|
+
for (const page of index.items) {
|
|
90
|
+
let pageItems = page.items;
|
|
91
|
+
|
|
92
|
+
// Fetch page if items not inlined
|
|
93
|
+
if (!pageItems && page["@id"]) {
|
|
94
|
+
const pageResult = await loadPage(page["@id"], { timeout: Math.min(timeout, 5) });
|
|
95
|
+
if (pageResult.ok) {
|
|
96
|
+
try {
|
|
97
|
+
const fetchedPage = JSON.parse(pageResult.content) as NuGetRegistrationPage;
|
|
98
|
+
pageItems = fetchedPage.items;
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (pageItems) {
|
|
104
|
+
const found = pageItems.find(
|
|
105
|
+
(item) => item.catalogEntry.version.toLowerCase() === requestedVersion.toLowerCase(),
|
|
106
|
+
);
|
|
107
|
+
if (found) {
|
|
108
|
+
targetEntry = found.catalogEntry;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// If no specific version requested or not found, use the latest
|
|
116
|
+
if (!targetEntry) {
|
|
117
|
+
const latestItem = latestPage.items[latestPage.items.length - 1];
|
|
118
|
+
targetEntry = latestItem.catalogEntry;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fetch download stats via search API
|
|
122
|
+
let totalDownloads: number | null = null;
|
|
123
|
+
const searchUrl = `https://api.nuget.org/v3/query?q=packageid:${encodeURIComponent(packageName)}&prerelease=true&take=1`;
|
|
124
|
+
const searchResult = await loadPage(searchUrl, { timeout: Math.min(timeout, 5) });
|
|
125
|
+
|
|
126
|
+
if (searchResult.ok) {
|
|
127
|
+
try {
|
|
128
|
+
const searchData = JSON.parse(searchResult.content) as {
|
|
129
|
+
data?: Array<{ totalDownloads?: number }>;
|
|
130
|
+
};
|
|
131
|
+
totalDownloads = searchData.data?.[0]?.totalDownloads ?? null;
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Format markdown output
|
|
136
|
+
let md = `# ${targetEntry.id}\n\n`;
|
|
137
|
+
if (targetEntry.description) md += `${targetEntry.description}\n\n`;
|
|
138
|
+
|
|
139
|
+
md += `**Version:** ${targetEntry.version}`;
|
|
140
|
+
if (targetEntry.licenseExpression) {
|
|
141
|
+
md += ` · **License:** ${targetEntry.licenseExpression}`;
|
|
142
|
+
} else if (targetEntry.licenseUrl) {
|
|
143
|
+
md += ` · **License:** [View](${targetEntry.licenseUrl})`;
|
|
144
|
+
}
|
|
145
|
+
md += "\n";
|
|
146
|
+
|
|
147
|
+
if (totalDownloads !== null) {
|
|
148
|
+
md += `**Total Downloads:** ${formatCount(totalDownloads)}\n`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (targetEntry.authors) md += `**Authors:** ${targetEntry.authors}\n`;
|
|
152
|
+
if (targetEntry.projectUrl) md += `**Project URL:** ${targetEntry.projectUrl}\n`;
|
|
153
|
+
if (targetEntry.tags?.length) md += `**Tags:** ${targetEntry.tags.join(", ")}\n`;
|
|
154
|
+
if (targetEntry.published) {
|
|
155
|
+
const pubDate = targetEntry.published.split("T")[0];
|
|
156
|
+
md += `**Published:** ${pubDate}\n`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Show dependencies by target framework
|
|
160
|
+
if (targetEntry.dependencyGroups?.length) {
|
|
161
|
+
const hasAnyDeps = targetEntry.dependencyGroups.some((g) => g.dependencies?.length);
|
|
162
|
+
if (hasAnyDeps) {
|
|
163
|
+
md += `\n## Dependencies\n\n`;
|
|
164
|
+
for (const group of targetEntry.dependencyGroups) {
|
|
165
|
+
if (!group.dependencies?.length) continue;
|
|
166
|
+
const framework = group.targetFramework || "All Frameworks";
|
|
167
|
+
md += `### ${framework}\n\n`;
|
|
168
|
+
for (const dep of group.dependencies) {
|
|
169
|
+
md += `- ${dep.id} (${dep.range})\n`;
|
|
170
|
+
}
|
|
171
|
+
md += "\n";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Show recent versions from the latest page
|
|
177
|
+
if (latestPage.items && latestPage.items.length > 1) {
|
|
178
|
+
md += `## Recent Versions\n\n`;
|
|
179
|
+
const recentVersions = latestPage.items.slice(-5).reverse();
|
|
180
|
+
for (const item of recentVersions) {
|
|
181
|
+
const entry = item.catalogEntry;
|
|
182
|
+
const pubDate = entry.published?.split("T")[0] || "unknown";
|
|
183
|
+
md += `- **${entry.version}** (${pubDate})\n`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const output = finalizeOutput(md);
|
|
188
|
+
return {
|
|
189
|
+
url,
|
|
190
|
+
finalUrl: url,
|
|
191
|
+
contentType: "text/markdown",
|
|
192
|
+
method: "nuget",
|
|
193
|
+
content: output.content,
|
|
194
|
+
fetchedAt,
|
|
195
|
+
truncated: output.truncated,
|
|
196
|
+
notes: ["Fetched via NuGet API"],
|
|
197
|
+
};
|
|
198
|
+
} catch {}
|
|
199
|
+
|
|
200
|
+
return null;
|
|
201
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface CvssV31 {
|
|
5
|
+
baseScore: number;
|
|
6
|
+
baseSeverity: string;
|
|
7
|
+
vectorString: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface CvssV2 {
|
|
11
|
+
baseScore: number;
|
|
12
|
+
severity?: string;
|
|
13
|
+
vectorString: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface CvssMetric {
|
|
17
|
+
cvssData: CvssV31 | CvssV2;
|
|
18
|
+
exploitabilityScore?: number;
|
|
19
|
+
impactScore?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CpeMatch {
|
|
23
|
+
criteria: string;
|
|
24
|
+
vulnerable: boolean;
|
|
25
|
+
versionStartIncluding?: string;
|
|
26
|
+
versionEndExcluding?: string;
|
|
27
|
+
versionEndIncluding?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Configuration {
|
|
31
|
+
nodes?: Array<{
|
|
32
|
+
operator?: string;
|
|
33
|
+
cpeMatch?: CpeMatch[];
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Reference {
|
|
38
|
+
url: string;
|
|
39
|
+
source?: string;
|
|
40
|
+
tags?: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface Description {
|
|
44
|
+
lang: string;
|
|
45
|
+
value: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Weakness {
|
|
49
|
+
description: Description[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface CveItem {
|
|
53
|
+
id: string;
|
|
54
|
+
sourceIdentifier?: string;
|
|
55
|
+
published: string;
|
|
56
|
+
lastModified: string;
|
|
57
|
+
vulnStatus?: string;
|
|
58
|
+
descriptions: Description[];
|
|
59
|
+
metrics?: {
|
|
60
|
+
cvssMetricV31?: CvssMetric[];
|
|
61
|
+
cvssMetricV30?: CvssMetric[];
|
|
62
|
+
cvssMetricV2?: CvssMetric[];
|
|
63
|
+
};
|
|
64
|
+
weaknesses?: Weakness[];
|
|
65
|
+
configurations?: Configuration[];
|
|
66
|
+
references?: Reference[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface NvdResponse {
|
|
70
|
+
vulnerabilities?: Array<{ cve: CveItem }>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Handle NVD (National Vulnerability Database) CVE URLs
|
|
75
|
+
*/
|
|
76
|
+
export const handleNvd: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
|
|
77
|
+
try {
|
|
78
|
+
const parsed = new URL(url);
|
|
79
|
+
if (!parsed.hostname.includes("nvd.nist.gov")) return null;
|
|
80
|
+
|
|
81
|
+
// Extract CVE ID from /vuln/detail/{CVE-ID}
|
|
82
|
+
const match = parsed.pathname.match(/\/vuln\/detail\/(CVE-\d{4}-\d+)/i);
|
|
83
|
+
if (!match) return null;
|
|
84
|
+
|
|
85
|
+
const cveId = match[1].toUpperCase();
|
|
86
|
+
const fetchedAt = new Date().toISOString();
|
|
87
|
+
|
|
88
|
+
// Fetch from NVD API
|
|
89
|
+
const apiUrl = `https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=${cveId}`;
|
|
90
|
+
const result = await loadPage(apiUrl, {
|
|
91
|
+
timeout,
|
|
92
|
+
headers: { Accept: "application/json" },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!result.ok) return null;
|
|
96
|
+
|
|
97
|
+
let data: NvdResponse;
|
|
98
|
+
try {
|
|
99
|
+
data = JSON.parse(result.content);
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const vuln = data.vulnerabilities?.[0]?.cve;
|
|
105
|
+
if (!vuln) return null;
|
|
106
|
+
|
|
107
|
+
let md = `# ${vuln.id}\n\n`;
|
|
108
|
+
|
|
109
|
+
// Status and dates
|
|
110
|
+
if (vuln.vulnStatus) {
|
|
111
|
+
md += `**Status:** ${vuln.vulnStatus}\n`;
|
|
112
|
+
}
|
|
113
|
+
md += `**Published:** ${formatDate(vuln.published)}`;
|
|
114
|
+
md += ` · **Modified:** ${formatDate(vuln.lastModified)}\n\n`;
|
|
115
|
+
|
|
116
|
+
// Description
|
|
117
|
+
const desc = vuln.descriptions.find((d) => d.lang === "en")?.value;
|
|
118
|
+
if (desc) {
|
|
119
|
+
md += `## Description\n\n${desc}\n\n`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// CVSS Scores
|
|
123
|
+
const cvss31 = vuln.metrics?.cvssMetricV31?.[0];
|
|
124
|
+
const cvss30 = vuln.metrics?.cvssMetricV30?.[0];
|
|
125
|
+
const cvss2 = vuln.metrics?.cvssMetricV2?.[0];
|
|
126
|
+
|
|
127
|
+
if (cvss31 || cvss30 || cvss2) {
|
|
128
|
+
md += `## CVSS Scores\n\n`;
|
|
129
|
+
|
|
130
|
+
if (cvss31) {
|
|
131
|
+
const data = cvss31.cvssData as CvssV31;
|
|
132
|
+
md += `### CVSS 3.1\n\n`;
|
|
133
|
+
md += `- **Base Score:** ${data.baseScore} (${data.baseSeverity})\n`;
|
|
134
|
+
md += `- **Vector:** \`${data.vectorString}\`\n`;
|
|
135
|
+
if (cvss31.exploitabilityScore !== undefined) {
|
|
136
|
+
md += `- **Exploitability:** ${cvss31.exploitabilityScore}\n`;
|
|
137
|
+
}
|
|
138
|
+
if (cvss31.impactScore !== undefined) {
|
|
139
|
+
md += `- **Impact:** ${cvss31.impactScore}\n`;
|
|
140
|
+
}
|
|
141
|
+
md += "\n";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (cvss30 && !cvss31) {
|
|
145
|
+
const data = cvss30.cvssData as CvssV31;
|
|
146
|
+
md += `### CVSS 3.0\n\n`;
|
|
147
|
+
md += `- **Base Score:** ${data.baseScore} (${data.baseSeverity})\n`;
|
|
148
|
+
md += `- **Vector:** \`${data.vectorString}\`\n`;
|
|
149
|
+
md += "\n";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (cvss2) {
|
|
153
|
+
const data = cvss2.cvssData as CvssV2;
|
|
154
|
+
md += `### CVSS 2.0\n\n`;
|
|
155
|
+
md += `- **Base Score:** ${data.baseScore}`;
|
|
156
|
+
if (data.severity) md += ` (${data.severity})`;
|
|
157
|
+
md += `\n- **Vector:** \`${data.vectorString}\`\n\n`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Weaknesses (CWE)
|
|
162
|
+
const cwes = vuln.weaknesses
|
|
163
|
+
?.flatMap((w) => w.description)
|
|
164
|
+
.filter((d) => d.lang === "en" && d.value !== "NVD-CWE-Other" && d.value !== "NVD-CWE-noinfo");
|
|
165
|
+
|
|
166
|
+
if (cwes?.length) {
|
|
167
|
+
md += `## Weaknesses\n\n`;
|
|
168
|
+
for (const cwe of cwes) {
|
|
169
|
+
md += `- ${cwe.value}\n`;
|
|
170
|
+
}
|
|
171
|
+
md += "\n";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Affected Products (CPE)
|
|
175
|
+
const cpes = extractCpes(vuln.configurations);
|
|
176
|
+
if (cpes.length > 0) {
|
|
177
|
+
md += `## Affected Products\n\n`;
|
|
178
|
+
const shown = cpes.slice(0, 20);
|
|
179
|
+
for (const cpe of shown) {
|
|
180
|
+
md += `- \`${cpe}\`\n`;
|
|
181
|
+
}
|
|
182
|
+
if (cpes.length > 20) {
|
|
183
|
+
md += `\n*...and ${cpes.length - 20} more*\n`;
|
|
184
|
+
}
|
|
185
|
+
md += "\n";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// References
|
|
189
|
+
if (vuln.references?.length) {
|
|
190
|
+
md += `## References\n\n`;
|
|
191
|
+
for (const ref of vuln.references.slice(0, 15)) {
|
|
192
|
+
const tags = ref.tags?.length ? ` (${ref.tags.join(", ")})` : "";
|
|
193
|
+
md += `- ${ref.url}${tags}\n`;
|
|
194
|
+
}
|
|
195
|
+
if (vuln.references.length > 15) {
|
|
196
|
+
md += `\n*...and ${vuln.references.length - 15} more references*\n`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const output = finalizeOutput(md);
|
|
201
|
+
return {
|
|
202
|
+
url,
|
|
203
|
+
finalUrl: url,
|
|
204
|
+
contentType: "text/markdown",
|
|
205
|
+
method: "nvd",
|
|
206
|
+
content: output.content,
|
|
207
|
+
fetchedAt,
|
|
208
|
+
truncated: output.truncated,
|
|
209
|
+
notes: ["Fetched via NVD API"],
|
|
210
|
+
};
|
|
211
|
+
} catch {}
|
|
212
|
+
|
|
213
|
+
return null;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
function formatDate(iso: string): string {
|
|
217
|
+
try {
|
|
218
|
+
return new Date(iso).toISOString().split("T")[0];
|
|
219
|
+
} catch {
|
|
220
|
+
return iso;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function extractCpes(configurations?: Configuration[]): string[] {
|
|
225
|
+
if (!configurations) return [];
|
|
226
|
+
|
|
227
|
+
const cpes: string[] = [];
|
|
228
|
+
for (const config of configurations) {
|
|
229
|
+
for (const node of config.nodes ?? []) {
|
|
230
|
+
for (const match of node.cpeMatch ?? []) {
|
|
231
|
+
if (match.vulnerable && match.criteria) {
|
|
232
|
+
cpes.push(match.criteria);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return Array.from(new Set(cpes));
|
|
238
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface Officer {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
position?: string;
|
|
8
|
+
start_date?: string;
|
|
9
|
+
end_date?: string;
|
|
10
|
+
occupation?: string;
|
|
11
|
+
nationality?: string;
|
|
12
|
+
inactive?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Address {
|
|
16
|
+
street_address?: string;
|
|
17
|
+
locality?: string;
|
|
18
|
+
region?: string;
|
|
19
|
+
postal_code?: string;
|
|
20
|
+
country?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CompanyData {
|
|
24
|
+
name: string;
|
|
25
|
+
company_number: string;
|
|
26
|
+
jurisdiction_code: string;
|
|
27
|
+
incorporation_date?: string;
|
|
28
|
+
dissolution_date?: string;
|
|
29
|
+
company_type?: string;
|
|
30
|
+
registry_url?: string;
|
|
31
|
+
branch?: string;
|
|
32
|
+
branch_status?: string;
|
|
33
|
+
inactive?: boolean;
|
|
34
|
+
current_status?: string;
|
|
35
|
+
created_at?: string;
|
|
36
|
+
updated_at?: string;
|
|
37
|
+
retrieved_at?: string;
|
|
38
|
+
opencorporates_url?: string;
|
|
39
|
+
source?: {
|
|
40
|
+
publisher?: string;
|
|
41
|
+
url?: string;
|
|
42
|
+
retrieved_at?: string;
|
|
43
|
+
};
|
|
44
|
+
registered_address?: Address;
|
|
45
|
+
registered_address_in_full?: string;
|
|
46
|
+
industry_codes?: Array<{
|
|
47
|
+
code: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
code_scheme_name?: string;
|
|
50
|
+
}>;
|
|
51
|
+
identifiers?: Array<{
|
|
52
|
+
identifier_system_code: string;
|
|
53
|
+
identifier_system_name?: string;
|
|
54
|
+
identifier_uid: string;
|
|
55
|
+
}>;
|
|
56
|
+
previous_names?: Array<{
|
|
57
|
+
company_name: string;
|
|
58
|
+
con_date?: string;
|
|
59
|
+
}>;
|
|
60
|
+
alternative_names?: Array<{
|
|
61
|
+
company_name: string;
|
|
62
|
+
type?: string;
|
|
63
|
+
}>;
|
|
64
|
+
officers?: Array<{ officer: Officer }>;
|
|
65
|
+
agent_name?: string;
|
|
66
|
+
agent_address?: string;
|
|
67
|
+
number_of_employees?: string;
|
|
68
|
+
native_company_number?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface ApiResponse {
|
|
72
|
+
api_version: string;
|
|
73
|
+
results: {
|
|
74
|
+
company: CompanyData;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Handle OpenCorporates URLs via API
|
|
80
|
+
*/
|
|
81
|
+
export const handleOpenCorporates: SpecialHandler = async (
|
|
82
|
+
url: string,
|
|
83
|
+
timeout: number,
|
|
84
|
+
): Promise<RenderResult | null> => {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = new URL(url);
|
|
87
|
+
if (!parsed.hostname.includes("opencorporates.com")) return null;
|
|
88
|
+
|
|
89
|
+
// Extract jurisdiction and company number from /companies/{jurisdiction}/{number}
|
|
90
|
+
const match = parsed.pathname.match(/^\/companies\/([^/]+)\/([^/]+)/);
|
|
91
|
+
if (!match) return null;
|
|
92
|
+
|
|
93
|
+
const jurisdiction = decodeURIComponent(match[1]);
|
|
94
|
+
const companyNumber = decodeURIComponent(match[2]);
|
|
95
|
+
|
|
96
|
+
const fetchedAt = new Date().toISOString();
|
|
97
|
+
|
|
98
|
+
// Fetch from OpenCorporates API
|
|
99
|
+
const apiUrl = `https://api.opencorporates.com/v0.4/companies/${jurisdiction}/${companyNumber}`;
|
|
100
|
+
const result = await loadPage(apiUrl, {
|
|
101
|
+
timeout,
|
|
102
|
+
headers: { Accept: "application/json" },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!result.ok) return null;
|
|
106
|
+
|
|
107
|
+
let data: ApiResponse;
|
|
108
|
+
try {
|
|
109
|
+
data = JSON.parse(result.content);
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const company = data.results?.company;
|
|
115
|
+
if (!company) return null;
|
|
116
|
+
|
|
117
|
+
let md = `# ${company.name}\n\n`;
|
|
118
|
+
|
|
119
|
+
// Basic info table
|
|
120
|
+
md += "| Field | Value |\n|-------|-------|\n";
|
|
121
|
+
md += `| **Company Number** | ${company.company_number} |\n`;
|
|
122
|
+
md += `| **Jurisdiction** | ${company.jurisdiction_code.toUpperCase()} |\n`;
|
|
123
|
+
if (company.current_status) {
|
|
124
|
+
md += `| **Status** | ${company.current_status} |\n`;
|
|
125
|
+
}
|
|
126
|
+
if (company.company_type) {
|
|
127
|
+
md += `| **Company Type** | ${company.company_type} |\n`;
|
|
128
|
+
}
|
|
129
|
+
if (company.incorporation_date) {
|
|
130
|
+
md += `| **Incorporated** | ${company.incorporation_date} |\n`;
|
|
131
|
+
}
|
|
132
|
+
if (company.dissolution_date) {
|
|
133
|
+
md += `| **Dissolved** | ${company.dissolution_date} |\n`;
|
|
134
|
+
}
|
|
135
|
+
if (company.branch) {
|
|
136
|
+
md += `| **Branch** | ${company.branch}${company.branch_status ? ` (${company.branch_status})` : ""} |\n`;
|
|
137
|
+
}
|
|
138
|
+
if (company.native_company_number && company.native_company_number !== company.company_number) {
|
|
139
|
+
md += `| **Native Number** | ${company.native_company_number} |\n`;
|
|
140
|
+
}
|
|
141
|
+
md += "\n";
|
|
142
|
+
|
|
143
|
+
// Registered address
|
|
144
|
+
if (company.registered_address_in_full) {
|
|
145
|
+
md += `## Registered Address\n\n${company.registered_address_in_full}\n\n`;
|
|
146
|
+
} else if (company.registered_address) {
|
|
147
|
+
const addr = company.registered_address;
|
|
148
|
+
const parts = [addr.street_address, addr.locality, addr.region, addr.postal_code, addr.country].filter(
|
|
149
|
+
Boolean,
|
|
150
|
+
);
|
|
151
|
+
if (parts.length > 0) {
|
|
152
|
+
md += `## Registered Address\n\n${parts.join(", ")}\n\n`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Agent info
|
|
157
|
+
if (company.agent_name) {
|
|
158
|
+
md += `## Registered Agent\n\n**${company.agent_name}**`;
|
|
159
|
+
if (company.agent_address) {
|
|
160
|
+
md += `\n${company.agent_address}`;
|
|
161
|
+
}
|
|
162
|
+
md += "\n\n";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Officers/Directors
|
|
166
|
+
if (company.officers && company.officers.length > 0) {
|
|
167
|
+
const activeOfficers = company.officers.filter((o) => !o.officer.inactive && !o.officer.end_date);
|
|
168
|
+
const inactiveOfficers = company.officers.filter((o) => o.officer.inactive || o.officer.end_date);
|
|
169
|
+
|
|
170
|
+
if (activeOfficers.length > 0) {
|
|
171
|
+
md += `## Current Officers (${activeOfficers.length})\n\n`;
|
|
172
|
+
for (const { officer } of activeOfficers) {
|
|
173
|
+
md += `- **${officer.name}**`;
|
|
174
|
+
if (officer.position) md += ` - ${officer.position}`;
|
|
175
|
+
if (officer.start_date) md += ` (since ${officer.start_date})`;
|
|
176
|
+
if (officer.occupation) md += ` [${officer.occupation}]`;
|
|
177
|
+
if (officer.nationality) md += ` (${officer.nationality})`;
|
|
178
|
+
md += "\n";
|
|
179
|
+
}
|
|
180
|
+
md += "\n";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (inactiveOfficers.length > 0) {
|
|
184
|
+
md += `## Former Officers (${inactiveOfficers.length})\n\n`;
|
|
185
|
+
for (const { officer } of inactiveOfficers.slice(0, 10)) {
|
|
186
|
+
md += `- **${officer.name}**`;
|
|
187
|
+
if (officer.position) md += ` - ${officer.position}`;
|
|
188
|
+
if (officer.start_date && officer.end_date) {
|
|
189
|
+
md += ` (${officer.start_date} to ${officer.end_date})`;
|
|
190
|
+
} else if (officer.end_date) {
|
|
191
|
+
md += ` (until ${officer.end_date})`;
|
|
192
|
+
}
|
|
193
|
+
md += "\n";
|
|
194
|
+
}
|
|
195
|
+
if (inactiveOfficers.length > 10) {
|
|
196
|
+
md += `\n*...and ${inactiveOfficers.length - 10} more former officers*\n`;
|
|
197
|
+
}
|
|
198
|
+
md += "\n";
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Industry codes
|
|
203
|
+
if (company.industry_codes && company.industry_codes.length > 0) {
|
|
204
|
+
md += `## Industry Codes\n\n`;
|
|
205
|
+
for (const ic of company.industry_codes) {
|
|
206
|
+
md += `- **${ic.code}**`;
|
|
207
|
+
if (ic.description) md += `: ${ic.description}`;
|
|
208
|
+
if (ic.code_scheme_name) md += ` (${ic.code_scheme_name})`;
|
|
209
|
+
md += "\n";
|
|
210
|
+
}
|
|
211
|
+
md += "\n";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Identifiers
|
|
215
|
+
if (company.identifiers && company.identifiers.length > 0) {
|
|
216
|
+
md += `## Identifiers\n\n`;
|
|
217
|
+
for (const id of company.identifiers) {
|
|
218
|
+
md += `- **${id.identifier_system_name || id.identifier_system_code}**: ${id.identifier_uid}\n`;
|
|
219
|
+
}
|
|
220
|
+
md += "\n";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Previous names
|
|
224
|
+
if (company.previous_names && company.previous_names.length > 0) {
|
|
225
|
+
md += `## Previous Names\n\n`;
|
|
226
|
+
for (const pn of company.previous_names) {
|
|
227
|
+
md += `- ${pn.company_name}`;
|
|
228
|
+
if (pn.con_date) md += ` (until ${pn.con_date})`;
|
|
229
|
+
md += "\n";
|
|
230
|
+
}
|
|
231
|
+
md += "\n";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Alternative names
|
|
235
|
+
if (company.alternative_names && company.alternative_names.length > 0) {
|
|
236
|
+
md += `## Alternative Names\n\n`;
|
|
237
|
+
for (const an of company.alternative_names) {
|
|
238
|
+
md += `- ${an.company_name}`;
|
|
239
|
+
if (an.type) md += ` (${an.type})`;
|
|
240
|
+
md += "\n";
|
|
241
|
+
}
|
|
242
|
+
md += "\n";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Source info
|
|
246
|
+
md += "---\n\n";
|
|
247
|
+
if (company.source?.publisher) {
|
|
248
|
+
md += `**Source:** ${company.source.publisher}`;
|
|
249
|
+
if (company.source.url) md += ` ([registry](${company.source.url}))`;
|
|
250
|
+
md += "\n";
|
|
251
|
+
}
|
|
252
|
+
if (company.registry_url) {
|
|
253
|
+
md += `**Official Registry:** ${company.registry_url}\n`;
|
|
254
|
+
}
|
|
255
|
+
if (company.retrieved_at) {
|
|
256
|
+
md += `**Data Retrieved:** ${company.retrieved_at}\n`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const output = finalizeOutput(md);
|
|
260
|
+
return {
|
|
261
|
+
url,
|
|
262
|
+
finalUrl: company.opencorporates_url || url,
|
|
263
|
+
contentType: "text/markdown",
|
|
264
|
+
method: "opencorporates",
|
|
265
|
+
content: output.content,
|
|
266
|
+
fetchedAt,
|
|
267
|
+
truncated: output.truncated,
|
|
268
|
+
notes: ["Fetched via OpenCorporates API"],
|
|
269
|
+
};
|
|
270
|
+
} catch {}
|
|
271
|
+
|
|
272
|
+
return null;
|
|
273
|
+
};
|