@oh-my-pi/pi-coding-agent 3.30.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 +71 -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/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/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 +150 -74
- 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 +94 -83
- package/src/core/tools/task/index.ts +129 -92
- 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 +15 -6
- package/src/core/tools/task/worker.ts +124 -89
- package/src/core/tools/web-fetch.ts +112 -377
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
- 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-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
- package/src/core/tools/web-scrapers/fdroid.ts +158 -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-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
- 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-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
- 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-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -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-fetch-handlers → web-scrapers}/spotify.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
- 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-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
- 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/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/web-fetch-handlers/index.ts +0 -69
- package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
|
@@ -51,11 +51,12 @@ interface MastodonStatus {
|
|
|
51
51
|
/**
|
|
52
52
|
* Check if a domain is a Mastodon instance by probing the API
|
|
53
53
|
*/
|
|
54
|
-
async function isMastodonInstance(hostname: string, timeout: number): Promise<boolean> {
|
|
54
|
+
async function isMastodonInstance(hostname: string, timeout: number, signal?: AbortSignal): Promise<boolean> {
|
|
55
55
|
try {
|
|
56
56
|
const result = await loadPage(`https://${hostname}/api/v1/instance`, {
|
|
57
57
|
timeout: Math.min(timeout, 5),
|
|
58
58
|
headers: { Accept: "application/json" },
|
|
59
|
+
signal,
|
|
59
60
|
});
|
|
60
61
|
if (!result.ok) return false;
|
|
61
62
|
const data = JSON.parse(result.content);
|
|
@@ -188,7 +189,11 @@ function formatAccount(account: MastodonAccount): string {
|
|
|
188
189
|
/**
|
|
189
190
|
* Handle Mastodon/Fediverse URLs
|
|
190
191
|
*/
|
|
191
|
-
export const handleMastodon: SpecialHandler = async (
|
|
192
|
+
export const handleMastodon: SpecialHandler = async (
|
|
193
|
+
url: string,
|
|
194
|
+
timeout: number,
|
|
195
|
+
signal?: AbortSignal,
|
|
196
|
+
): Promise<RenderResult | null> => {
|
|
192
197
|
try {
|
|
193
198
|
const parsed = new URL(url);
|
|
194
199
|
|
|
@@ -199,7 +204,7 @@ export const handleMastodon: SpecialHandler = async (url: string, timeout: numbe
|
|
|
199
204
|
if (!postMatch && !profileMatch) return null;
|
|
200
205
|
|
|
201
206
|
// Verify this is a Mastodon instance
|
|
202
|
-
if (!(await isMastodonInstance(parsed.hostname, timeout))) {
|
|
207
|
+
if (!(await isMastodonInstance(parsed.hostname, timeout, signal))) {
|
|
203
208
|
return null;
|
|
204
209
|
}
|
|
205
210
|
|
|
@@ -214,6 +219,7 @@ export const handleMastodon: SpecialHandler = async (url: string, timeout: numbe
|
|
|
214
219
|
const result = await loadPage(apiUrl, {
|
|
215
220
|
timeout,
|
|
216
221
|
headers: { Accept: "application/json" },
|
|
222
|
+
signal,
|
|
217
223
|
});
|
|
218
224
|
|
|
219
225
|
if (!result.ok) return null;
|
|
@@ -248,6 +254,7 @@ export const handleMastodon: SpecialHandler = async (url: string, timeout: numbe
|
|
|
248
254
|
const result = await loadPage(lookupUrl, {
|
|
249
255
|
timeout,
|
|
250
256
|
headers: { Accept: "application/json" },
|
|
257
|
+
signal,
|
|
251
258
|
});
|
|
252
259
|
|
|
253
260
|
if (!result.ok) return null;
|
|
@@ -264,6 +271,7 @@ export const handleMastodon: SpecialHandler = async (url: string, timeout: numbe
|
|
|
264
271
|
const statusesResult = await loadPage(statusesUrl, {
|
|
265
272
|
timeout,
|
|
266
273
|
headers: { Accept: "application/json" },
|
|
274
|
+
signal,
|
|
267
275
|
});
|
|
268
276
|
|
|
269
277
|
let md = formatAccount(account);
|
|
@@ -25,7 +25,11 @@ interface MavenResponse {
|
|
|
25
25
|
* Handle Maven Central URLs via Solr API
|
|
26
26
|
* Supports: search.maven.org/artifact/... and mvnrepository.com/artifact/...
|
|
27
27
|
*/
|
|
28
|
-
export const handleMaven: SpecialHandler = async (
|
|
28
|
+
export const handleMaven: SpecialHandler = async (
|
|
29
|
+
url: string,
|
|
30
|
+
timeout: number,
|
|
31
|
+
signal?: AbortSignal,
|
|
32
|
+
): Promise<RenderResult | null> => {
|
|
29
33
|
try {
|
|
30
34
|
const parsed = new URL(url);
|
|
31
35
|
const hostname = parsed.hostname;
|
|
@@ -65,6 +69,7 @@ export const handleMaven: SpecialHandler = async (url: string, timeout: number):
|
|
|
65
69
|
const result = await loadPage(apiUrl, {
|
|
66
70
|
timeout,
|
|
67
71
|
headers: { Accept: "application/json" },
|
|
72
|
+
signal,
|
|
68
73
|
});
|
|
69
74
|
|
|
70
75
|
if (!result.ok) return null;
|
|
@@ -105,7 +105,7 @@ function convertMDNBody(sections: MDNSection[]): string {
|
|
|
105
105
|
return parts.join("\n\n");
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export const handleMDN: SpecialHandler = async (url: string, timeout: number) => {
|
|
108
|
+
export const handleMDN: SpecialHandler = async (url: string, timeout: number, signal?: AbortSignal) => {
|
|
109
109
|
const urlObj = new URL(url);
|
|
110
110
|
|
|
111
111
|
// Only handle developer.mozilla.org
|
|
@@ -124,7 +124,7 @@ export const handleMDN: SpecialHandler = async (url: string, timeout: number) =>
|
|
|
124
124
|
const jsonUrl = url.replace(/\/?$/, "/index.json");
|
|
125
125
|
|
|
126
126
|
try {
|
|
127
|
-
const result = await loadPage(jsonUrl, { timeout, headers: { Accept: "application/json" } });
|
|
127
|
+
const result = await loadPage(jsonUrl, { timeout, signal, headers: { Accept: "application/json" } });
|
|
128
128
|
|
|
129
129
|
if (!result.ok) {
|
|
130
130
|
notes.push(`Failed to fetch MDN JSON API (status ${result.status || "unknown"})`);
|
|
@@ -39,7 +39,11 @@ interface ReleaseResponse {
|
|
|
39
39
|
/**
|
|
40
40
|
* Handle MetaCPAN URLs via fastapi.metacpan.org
|
|
41
41
|
*/
|
|
42
|
-
export const handleMetaCPAN: SpecialHandler = async (
|
|
42
|
+
export const handleMetaCPAN: SpecialHandler = async (
|
|
43
|
+
url: string,
|
|
44
|
+
timeout: number,
|
|
45
|
+
signal?: AbortSignal,
|
|
46
|
+
): Promise<RenderResult | null> => {
|
|
43
47
|
try {
|
|
44
48
|
const parsed = new URL(url);
|
|
45
49
|
if (parsed.hostname !== "metacpan.org" && parsed.hostname !== "www.metacpan.org") return null;
|
|
@@ -50,21 +54,21 @@ export const handleMetaCPAN: SpecialHandler = async (url: string, timeout: numbe
|
|
|
50
54
|
const podMatch = parsed.pathname.match(/^\/pod\/(.+?)(?:\/|$)/);
|
|
51
55
|
if (podMatch) {
|
|
52
56
|
const moduleName = decodeURIComponent(podMatch[1]);
|
|
53
|
-
return await fetchModule(url, moduleName, timeout, fetchedAt);
|
|
57
|
+
return await fetchModule(url, moduleName, timeout, fetchedAt, signal);
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
// Match /release/AUTHOR/Distribution pattern
|
|
57
61
|
const releaseMatch = parsed.pathname.match(/^\/release\/([^/]+)\/([^/]+)/);
|
|
58
62
|
if (releaseMatch) {
|
|
59
63
|
const distribution = decodeURIComponent(releaseMatch[2]);
|
|
60
|
-
return await fetchRelease(url, distribution, timeout, fetchedAt);
|
|
64
|
+
return await fetchRelease(url, distribution, timeout, fetchedAt, signal);
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
// Match /release/Distribution pattern (without author)
|
|
64
68
|
const simpleReleaseMatch = parsed.pathname.match(/^\/release\/([^/]+)$/);
|
|
65
69
|
if (simpleReleaseMatch) {
|
|
66
70
|
const distribution = decodeURIComponent(simpleReleaseMatch[1]);
|
|
67
|
-
return await fetchRelease(url, distribution, timeout, fetchedAt);
|
|
71
|
+
return await fetchRelease(url, distribution, timeout, fetchedAt, signal);
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
return null;
|
|
@@ -78,9 +82,10 @@ async function fetchModule(
|
|
|
78
82
|
moduleName: string,
|
|
79
83
|
timeout: number,
|
|
80
84
|
fetchedAt: string,
|
|
85
|
+
signal?: AbortSignal,
|
|
81
86
|
): Promise<RenderResult | null> {
|
|
82
87
|
const apiUrl = `https://fastapi.metacpan.org/v1/module/${moduleName}`;
|
|
83
|
-
const result = await loadPage(apiUrl, { timeout });
|
|
88
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
84
89
|
|
|
85
90
|
if (!result.ok) return null;
|
|
86
91
|
|
|
@@ -93,7 +98,7 @@ async function fetchModule(
|
|
|
93
98
|
|
|
94
99
|
// Fetch additional release info for dependencies and metadata
|
|
95
100
|
const releaseUrl = `https://fastapi.metacpan.org/v1/release/${module.distribution}`;
|
|
96
|
-
const releaseResult = await loadPage(releaseUrl, { timeout: Math.min(timeout, 5) });
|
|
101
|
+
const releaseResult = await loadPage(releaseUrl, { timeout: Math.min(timeout, 5), signal });
|
|
97
102
|
|
|
98
103
|
let release: ReleaseResponse | null = null;
|
|
99
104
|
if (releaseResult.ok) {
|
|
@@ -122,9 +127,10 @@ async function fetchRelease(
|
|
|
122
127
|
distribution: string,
|
|
123
128
|
timeout: number,
|
|
124
129
|
fetchedAt: string,
|
|
130
|
+
signal?: AbortSignal,
|
|
125
131
|
): Promise<RenderResult | null> {
|
|
126
132
|
const apiUrl = `https://fastapi.metacpan.org/v1/release/${distribution}`;
|
|
127
|
-
const result = await loadPage(apiUrl, { timeout });
|
|
133
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
128
134
|
|
|
129
135
|
if (!result.ok) return null;
|
|
130
136
|
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MusicBrainz URL handler for artists, releases, and recordings
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
6
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
7
|
+
|
|
8
|
+
type MusicBrainzEntity = "artist" | "release" | "recording";
|
|
9
|
+
|
|
10
|
+
interface MusicBrainzLifeSpan {
|
|
11
|
+
begin?: string;
|
|
12
|
+
end?: string;
|
|
13
|
+
ended?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface MusicBrainzArtist {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
type?: string;
|
|
20
|
+
country?: string;
|
|
21
|
+
"life-span"?: MusicBrainzLifeSpan;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface MusicBrainzArtistCredit {
|
|
25
|
+
name?: string;
|
|
26
|
+
artist?: {
|
|
27
|
+
id?: string;
|
|
28
|
+
name: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface MusicBrainzRecording {
|
|
33
|
+
id: string;
|
|
34
|
+
title: string;
|
|
35
|
+
length?: number;
|
|
36
|
+
"artist-credit"?: MusicBrainzArtistCredit[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface MusicBrainzTrack {
|
|
40
|
+
id?: string;
|
|
41
|
+
title?: string;
|
|
42
|
+
number?: string;
|
|
43
|
+
position?: number;
|
|
44
|
+
length?: number;
|
|
45
|
+
recording?: {
|
|
46
|
+
title?: string;
|
|
47
|
+
length?: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface MusicBrainzMedium {
|
|
52
|
+
position?: number;
|
|
53
|
+
format?: string;
|
|
54
|
+
"track-count"?: number;
|
|
55
|
+
tracks?: MusicBrainzTrack[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface MusicBrainzRelease {
|
|
59
|
+
id: string;
|
|
60
|
+
title: string;
|
|
61
|
+
"track-count"?: number;
|
|
62
|
+
media?: MusicBrainzMedium[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const MUSICBRAINZ_HOSTS = new Set(["musicbrainz.org", "www.musicbrainz.org"]);
|
|
66
|
+
const USER_AGENT = "omp-web-fetch/1.0 (https://github.com/anthropics)";
|
|
67
|
+
const MAX_TRACKS = 50;
|
|
68
|
+
|
|
69
|
+
function parseEntity(url: URL): { entity: MusicBrainzEntity; mbid: string } | null {
|
|
70
|
+
if (!MUSICBRAINZ_HOSTS.has(url.hostname)) return null;
|
|
71
|
+
|
|
72
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
73
|
+
if (parts.length < 2) return null;
|
|
74
|
+
|
|
75
|
+
const entity = parts[0] as MusicBrainzEntity;
|
|
76
|
+
if (entity !== "artist" && entity !== "release" && entity !== "recording") return null;
|
|
77
|
+
|
|
78
|
+
const mbid = parts[1];
|
|
79
|
+
if (!/^[0-9a-fA-F-]{36}$/.test(mbid)) return null;
|
|
80
|
+
|
|
81
|
+
return { entity, mbid };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function fetchJson<T>(apiUrl: string, timeout: number, signal?: AbortSignal): Promise<T | null> {
|
|
85
|
+
const result = await loadPage(apiUrl, {
|
|
86
|
+
timeout,
|
|
87
|
+
signal,
|
|
88
|
+
headers: {
|
|
89
|
+
"User-Agent": USER_AGENT,
|
|
90
|
+
Accept: "application/json",
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!result.ok) return null;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(result.content) as T;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatLifeSpan(life: MusicBrainzLifeSpan | undefined): string | null {
|
|
104
|
+
if (!life) return null;
|
|
105
|
+
|
|
106
|
+
const begin = life.begin?.trim();
|
|
107
|
+
const end = life.end?.trim();
|
|
108
|
+
|
|
109
|
+
if (begin && end) return `${begin} - ${end}`;
|
|
110
|
+
if (begin && !end) return `${begin} - ${life.ended ? "ended" : "present"}`;
|
|
111
|
+
if (!begin && end) return `? - ${end}`;
|
|
112
|
+
if (life.ended !== undefined) return life.ended ? "ended" : "present";
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatDurationMs(lengthMs: number | undefined): string | null {
|
|
118
|
+
if (!lengthMs || lengthMs <= 0) return null;
|
|
119
|
+
|
|
120
|
+
const totalSeconds = Math.round(lengthMs / 1000);
|
|
121
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
122
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
123
|
+
const seconds = totalSeconds % 60;
|
|
124
|
+
|
|
125
|
+
if (hours > 0) {
|
|
126
|
+
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatArtistCredits(credits: MusicBrainzArtistCredit[] | undefined): string | null {
|
|
133
|
+
if (!credits?.length) return null;
|
|
134
|
+
|
|
135
|
+
const names = credits
|
|
136
|
+
.map((credit) => credit.name || credit.artist?.name)
|
|
137
|
+
.filter((name): name is string => Boolean(name));
|
|
138
|
+
|
|
139
|
+
if (!names.length) return null;
|
|
140
|
+
return names.join(", ");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function formatTrack(track: MusicBrainzTrack): string {
|
|
144
|
+
const title = track.title || track.recording?.title || "Untitled";
|
|
145
|
+
const duration = formatDurationMs(track.length ?? track.recording?.length);
|
|
146
|
+
const number = track.number || (track.position ? String(track.position) : null);
|
|
147
|
+
|
|
148
|
+
const prefix = number ? `${number}. ` : "- ";
|
|
149
|
+
let line = `${prefix}${title}`;
|
|
150
|
+
if (duration) line += ` (${duration})`;
|
|
151
|
+
return line;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildMediumLabel(medium: MusicBrainzMedium, includePosition: boolean): string | null {
|
|
155
|
+
const parts: string[] = [];
|
|
156
|
+
if (includePosition && medium.position) parts.push(`Disc ${medium.position}`);
|
|
157
|
+
if (medium.format) parts.push(medium.format);
|
|
158
|
+
return parts.length ? parts.join(" - ") : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildArtistMarkdown(artist: MusicBrainzArtist): string {
|
|
162
|
+
let md = `# ${artist.name}\n\n`;
|
|
163
|
+
const meta: string[] = [];
|
|
164
|
+
|
|
165
|
+
if (artist.type) meta.push(`**Type**: ${artist.type}`);
|
|
166
|
+
if (artist.country) meta.push(`**Country**: ${artist.country}`);
|
|
167
|
+
|
|
168
|
+
const lifeSpan = formatLifeSpan(artist["life-span"]);
|
|
169
|
+
if (lifeSpan) meta.push(`**Life Span**: ${lifeSpan}`);
|
|
170
|
+
|
|
171
|
+
if (meta.length) md += `${meta.join("\n")}\n`;
|
|
172
|
+
|
|
173
|
+
return md;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildReleaseMarkdown(release: MusicBrainzRelease): string {
|
|
177
|
+
let md = `# ${release.title}\n\n`;
|
|
178
|
+
|
|
179
|
+
const media = release.media ?? [];
|
|
180
|
+
const totalTracks =
|
|
181
|
+
release["track-count"] ??
|
|
182
|
+
media.reduce((sum, medium) => sum + (medium["track-count"] ?? medium.tracks?.length ?? 0), 0);
|
|
183
|
+
|
|
184
|
+
if (totalTracks) {
|
|
185
|
+
md += `**Tracks**: ${totalTracks}\n\n`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (media.length) {
|
|
189
|
+
md += "## Tracks\n\n";
|
|
190
|
+
const includePosition = media.length > 1;
|
|
191
|
+
|
|
192
|
+
for (const medium of media) {
|
|
193
|
+
const label = buildMediumLabel(medium, includePosition);
|
|
194
|
+
if (label) md += `### ${label}\n\n`;
|
|
195
|
+
|
|
196
|
+
const tracks = medium.tracks ?? [];
|
|
197
|
+
if (tracks.length) {
|
|
198
|
+
const lines = tracks.slice(0, MAX_TRACKS).map(formatTrack).join("\n");
|
|
199
|
+
md += `${lines}\n\n`;
|
|
200
|
+
|
|
201
|
+
if (tracks.length > MAX_TRACKS) {
|
|
202
|
+
md += `_Showing first ${MAX_TRACKS} of ${tracks.length} tracks._\n\n`;
|
|
203
|
+
}
|
|
204
|
+
} else if (medium["track-count"]) {
|
|
205
|
+
md += `- ${medium["track-count"]} tracks (details unavailable)\n\n`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return md;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function buildRecordingMarkdown(recording: MusicBrainzRecording): string {
|
|
214
|
+
let md = `# ${recording.title}\n\n`;
|
|
215
|
+
const meta: string[] = [];
|
|
216
|
+
|
|
217
|
+
const artists = formatArtistCredits(recording["artist-credit"]);
|
|
218
|
+
if (artists) meta.push(`**Artists**: ${artists}`);
|
|
219
|
+
|
|
220
|
+
const length = formatDurationMs(recording.length);
|
|
221
|
+
if (length) meta.push(`**Length**: ${length}`);
|
|
222
|
+
|
|
223
|
+
if (meta.length) md += `${meta.join("\n")}\n`;
|
|
224
|
+
|
|
225
|
+
return md;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export const handleMusicBrainz: SpecialHandler = async (
|
|
229
|
+
url: string,
|
|
230
|
+
timeout: number,
|
|
231
|
+
signal?: AbortSignal,
|
|
232
|
+
): Promise<RenderResult | null> => {
|
|
233
|
+
try {
|
|
234
|
+
const parsed = new URL(url);
|
|
235
|
+
const parsedEntity = parseEntity(parsed);
|
|
236
|
+
if (!parsedEntity) return null;
|
|
237
|
+
|
|
238
|
+
const { entity, mbid } = parsedEntity;
|
|
239
|
+
const fetchedAt = new Date().toISOString();
|
|
240
|
+
let md = "";
|
|
241
|
+
|
|
242
|
+
if (entity === "artist") {
|
|
243
|
+
const apiUrl = `https://musicbrainz.org/ws/2/artist/${mbid}?fmt=json&inc=url-rels`;
|
|
244
|
+
const artist = await fetchJson<MusicBrainzArtist>(apiUrl, timeout, signal);
|
|
245
|
+
if (!artist) return null;
|
|
246
|
+
md = buildArtistMarkdown(artist);
|
|
247
|
+
} else if (entity === "release") {
|
|
248
|
+
const apiUrl = `https://musicbrainz.org/ws/2/release/${mbid}?fmt=json&inc=recordings`;
|
|
249
|
+
const release = await fetchJson<MusicBrainzRelease>(apiUrl, timeout, signal);
|
|
250
|
+
if (!release) return null;
|
|
251
|
+
md = buildReleaseMarkdown(release);
|
|
252
|
+
} else {
|
|
253
|
+
const apiUrl = `https://musicbrainz.org/ws/2/recording/${mbid}?fmt=json`;
|
|
254
|
+
const recording = await fetchJson<MusicBrainzRecording>(apiUrl, timeout, signal);
|
|
255
|
+
if (!recording) return null;
|
|
256
|
+
md = buildRecordingMarkdown(recording);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const output = finalizeOutput(md);
|
|
260
|
+
return {
|
|
261
|
+
url,
|
|
262
|
+
finalUrl: url,
|
|
263
|
+
contentType: "text/markdown",
|
|
264
|
+
method: "musicbrainz-api",
|
|
265
|
+
content: output.content,
|
|
266
|
+
fetchedAt,
|
|
267
|
+
truncated: output.truncated,
|
|
268
|
+
notes: ["Fetched via MusicBrainz API"],
|
|
269
|
+
};
|
|
270
|
+
} catch {}
|
|
271
|
+
|
|
272
|
+
return null;
|
|
273
|
+
};
|
|
@@ -4,7 +4,11 @@ import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
|
4
4
|
/**
|
|
5
5
|
* Handle npm URLs via registry API
|
|
6
6
|
*/
|
|
7
|
-
export const handleNpm: SpecialHandler = async (
|
|
7
|
+
export const handleNpm: SpecialHandler = async (
|
|
8
|
+
url: string,
|
|
9
|
+
timeout: number,
|
|
10
|
+
signal?: AbortSignal,
|
|
11
|
+
): Promise<RenderResult | null> => {
|
|
8
12
|
try {
|
|
9
13
|
const parsed = new URL(url);
|
|
10
14
|
if (parsed.hostname !== "www.npmjs.com" && parsed.hostname !== "npmjs.com") return null;
|
|
@@ -28,8 +32,8 @@ export const handleNpm: SpecialHandler = async (url: string, timeout: number): P
|
|
|
28
32
|
|
|
29
33
|
// Fetch package info and download stats in parallel
|
|
30
34
|
const [result, downloadsResult] = await Promise.all([
|
|
31
|
-
loadPage(latestUrl, { timeout }),
|
|
32
|
-
loadPage(downloadsUrl, { timeout: Math.min(timeout, 5) }),
|
|
35
|
+
loadPage(latestUrl, { timeout, signal }),
|
|
36
|
+
loadPage(downloadsUrl, { timeout: Math.min(timeout, 5), signal }),
|
|
33
37
|
]);
|
|
34
38
|
|
|
35
39
|
if (!result.ok) return null;
|
|
@@ -47,7 +51,7 @@ export const handleNpm: SpecialHandler = async (url: string, timeout: number): P
|
|
|
47
51
|
name: string;
|
|
48
52
|
version: string;
|
|
49
53
|
description?: string;
|
|
50
|
-
license?: string;
|
|
54
|
+
license?: string | { type: string };
|
|
51
55
|
homepage?: string;
|
|
52
56
|
repository?: { url: string } | string;
|
|
53
57
|
keywords?: string[];
|
|
@@ -66,7 +70,10 @@ export const handleNpm: SpecialHandler = async (url: string, timeout: number): P
|
|
|
66
70
|
if (pkg.description) md += `${pkg.description}\n\n`;
|
|
67
71
|
|
|
68
72
|
md += `**Latest:** ${pkg.version || "unknown"}`;
|
|
69
|
-
if (pkg.license)
|
|
73
|
+
if (pkg.license) {
|
|
74
|
+
const license = typeof pkg.license === "string" ? pkg.license : (pkg.license.type ?? String(pkg.license));
|
|
75
|
+
md += ` · **License:** ${license}`;
|
|
76
|
+
}
|
|
70
77
|
md += "\n";
|
|
71
78
|
if (weeklyDownloads !== null) {
|
|
72
79
|
md += `**Weekly Downloads:** ${formatCount(weeklyDownloads)}\n`;
|
|
@@ -37,7 +37,11 @@ interface NuGetRegistrationIndex {
|
|
|
37
37
|
/**
|
|
38
38
|
* Handle NuGet URLs via API
|
|
39
39
|
*/
|
|
40
|
-
export const handleNuGet: SpecialHandler = async (
|
|
40
|
+
export const handleNuGet: SpecialHandler = async (
|
|
41
|
+
url: string,
|
|
42
|
+
timeout: number,
|
|
43
|
+
signal?: AbortSignal,
|
|
44
|
+
): Promise<RenderResult | null> => {
|
|
41
45
|
try {
|
|
42
46
|
const parsed = new URL(url);
|
|
43
47
|
if (parsed.hostname !== "www.nuget.org" && parsed.hostname !== "nuget.org") return null;
|
|
@@ -52,7 +56,7 @@ export const handleNuGet: SpecialHandler = async (url: string, timeout: number):
|
|
|
52
56
|
|
|
53
57
|
// Fetch from NuGet registration API (package name must be lowercase)
|
|
54
58
|
const apiUrl = `https://api.nuget.org/v3/registration5-gz-semver2/${packageName.toLowerCase()}/index.json`;
|
|
55
|
-
const result = await loadPage(apiUrl, { timeout });
|
|
59
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
56
60
|
|
|
57
61
|
if (!result.ok) return null;
|
|
58
62
|
|
|
@@ -70,7 +74,7 @@ export const handleNuGet: SpecialHandler = async (url: string, timeout: number):
|
|
|
70
74
|
|
|
71
75
|
// If items are not inlined, fetch the page
|
|
72
76
|
if (!latestPage.items && latestPage["@id"]) {
|
|
73
|
-
const pageResult = await loadPage(latestPage["@id"], { timeout });
|
|
77
|
+
const pageResult = await loadPage(latestPage["@id"], { timeout, signal });
|
|
74
78
|
if (!pageResult.ok) return null;
|
|
75
79
|
try {
|
|
76
80
|
latestPage = JSON.parse(pageResult.content);
|
|
@@ -91,7 +95,7 @@ export const handleNuGet: SpecialHandler = async (url: string, timeout: number):
|
|
|
91
95
|
|
|
92
96
|
// Fetch page if items not inlined
|
|
93
97
|
if (!pageItems && page["@id"]) {
|
|
94
|
-
const pageResult = await loadPage(page["@id"], { timeout: Math.min(timeout, 5) });
|
|
98
|
+
const pageResult = await loadPage(page["@id"], { timeout: Math.min(timeout, 5), signal });
|
|
95
99
|
if (pageResult.ok) {
|
|
96
100
|
try {
|
|
97
101
|
const fetchedPage = JSON.parse(pageResult.content) as NuGetRegistrationPage;
|
|
@@ -121,7 +125,7 @@ export const handleNuGet: SpecialHandler = async (url: string, timeout: number):
|
|
|
121
125
|
// Fetch download stats via search API
|
|
122
126
|
let totalDownloads: number | null = null;
|
|
123
127
|
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) });
|
|
128
|
+
const searchResult = await loadPage(searchUrl, { timeout: Math.min(timeout, 5), signal });
|
|
125
129
|
|
|
126
130
|
if (searchResult.ok) {
|
|
127
131
|
try {
|
|
@@ -73,7 +73,11 @@ interface NvdResponse {
|
|
|
73
73
|
/**
|
|
74
74
|
* Handle NVD (National Vulnerability Database) CVE URLs
|
|
75
75
|
*/
|
|
76
|
-
export const handleNvd: SpecialHandler = async (
|
|
76
|
+
export const handleNvd: SpecialHandler = async (
|
|
77
|
+
url: string,
|
|
78
|
+
timeout: number,
|
|
79
|
+
signal?: AbortSignal,
|
|
80
|
+
): Promise<RenderResult | null> => {
|
|
77
81
|
try {
|
|
78
82
|
const parsed = new URL(url);
|
|
79
83
|
if (!parsed.hostname.includes("nvd.nist.gov")) return null;
|
|
@@ -90,6 +94,7 @@ export const handleNvd: SpecialHandler = async (url: string, timeout: number): P
|
|
|
90
94
|
const result = await loadPage(apiUrl, {
|
|
91
95
|
timeout,
|
|
92
96
|
headers: { Accept: "application/json" },
|
|
97
|
+
signal,
|
|
93
98
|
});
|
|
94
99
|
|
|
95
100
|
if (!result.ok) return null;
|