@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,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
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handle npm URLs via registry API
|
|
6
|
+
*/
|
|
7
|
+
export const handleNpm: SpecialHandler = async (
|
|
8
|
+
url: string,
|
|
9
|
+
timeout: number,
|
|
10
|
+
signal?: AbortSignal,
|
|
11
|
+
): Promise<RenderResult | null> => {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = new URL(url);
|
|
14
|
+
if (parsed.hostname !== "www.npmjs.com" && parsed.hostname !== "npmjs.com") return null;
|
|
15
|
+
|
|
16
|
+
// Extract package name from /package/[scope/]name
|
|
17
|
+
const match = parsed.pathname.match(/^\/package\/(.+?)(?:\/|$)/);
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
|
|
20
|
+
let packageName = decodeURIComponent(match[1]);
|
|
21
|
+
// Handle scoped packages: /package/@scope/name
|
|
22
|
+
if (packageName.startsWith("@")) {
|
|
23
|
+
const scopeMatch = parsed.pathname.match(/^\/package\/(@[^/]+\/[^/]+)/);
|
|
24
|
+
if (scopeMatch) packageName = decodeURIComponent(scopeMatch[1]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const fetchedAt = new Date().toISOString();
|
|
28
|
+
|
|
29
|
+
// Fetch from npm registry - use /latest endpoint for smaller response
|
|
30
|
+
const latestUrl = `https://registry.npmjs.org/${packageName}/latest`;
|
|
31
|
+
const downloadsUrl = `https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`;
|
|
32
|
+
|
|
33
|
+
// Fetch package info and download stats in parallel
|
|
34
|
+
const [result, downloadsResult] = await Promise.all([
|
|
35
|
+
loadPage(latestUrl, { timeout, signal }),
|
|
36
|
+
loadPage(downloadsUrl, { timeout: Math.min(timeout, 5), signal }),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
if (!result.ok) return null;
|
|
40
|
+
|
|
41
|
+
// Parse download stats
|
|
42
|
+
let weeklyDownloads: number | null = null;
|
|
43
|
+
if (downloadsResult.ok) {
|
|
44
|
+
try {
|
|
45
|
+
const dlData = JSON.parse(downloadsResult.content) as { downloads?: number };
|
|
46
|
+
weeklyDownloads = dlData.downloads ?? null;
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let pkg: {
|
|
51
|
+
name: string;
|
|
52
|
+
version: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
license?: string | { type: string };
|
|
55
|
+
homepage?: string;
|
|
56
|
+
repository?: { url: string } | string;
|
|
57
|
+
keywords?: string[];
|
|
58
|
+
maintainers?: Array<{ name: string }>;
|
|
59
|
+
dependencies?: Record<string, string>;
|
|
60
|
+
readme?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
pkg = JSON.parse(result.content);
|
|
65
|
+
} catch {
|
|
66
|
+
return null; // JSON parse failed (truncated response)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let md = `# ${pkg.name}\n\n`;
|
|
70
|
+
if (pkg.description) md += `${pkg.description}\n\n`;
|
|
71
|
+
|
|
72
|
+
md += `**Latest:** ${pkg.version || "unknown"}`;
|
|
73
|
+
if (pkg.license) {
|
|
74
|
+
const license = typeof pkg.license === "string" ? pkg.license : (pkg.license.type ?? String(pkg.license));
|
|
75
|
+
md += ` · **License:** ${license}`;
|
|
76
|
+
}
|
|
77
|
+
md += "\n";
|
|
78
|
+
if (weeklyDownloads !== null) {
|
|
79
|
+
md += `**Weekly Downloads:** ${formatCount(weeklyDownloads)}\n`;
|
|
80
|
+
}
|
|
81
|
+
md += "\n";
|
|
82
|
+
|
|
83
|
+
if (pkg.homepage) md += `**Homepage:** ${pkg.homepage}\n`;
|
|
84
|
+
const repoUrl = typeof pkg.repository === "string" ? pkg.repository : pkg.repository?.url;
|
|
85
|
+
if (repoUrl) md += `**Repository:** ${repoUrl.replace(/^git\+/, "").replace(/\.git$/, "")}\n`;
|
|
86
|
+
if (pkg.keywords?.length) md += `**Keywords:** ${pkg.keywords.join(", ")}\n`;
|
|
87
|
+
if (pkg.maintainers?.length) md += `**Maintainers:** ${pkg.maintainers.map((m) => m.name).join(", ")}\n`;
|
|
88
|
+
|
|
89
|
+
if (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) {
|
|
90
|
+
md += `\n## Dependencies\n\n`;
|
|
91
|
+
for (const [dep, version] of Object.entries(pkg.dependencies)) {
|
|
92
|
+
md += `- ${dep}: ${version}\n`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (pkg.readme) {
|
|
97
|
+
md += `\n---\n\n## README\n\n${pkg.readme}\n`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const output = finalizeOutput(md);
|
|
101
|
+
return {
|
|
102
|
+
url,
|
|
103
|
+
finalUrl: url,
|
|
104
|
+
contentType: "text/markdown",
|
|
105
|
+
method: "npm",
|
|
106
|
+
content: output.content,
|
|
107
|
+
fetchedAt,
|
|
108
|
+
truncated: output.truncated,
|
|
109
|
+
notes: ["Fetched via npm registry"],
|
|
110
|
+
};
|
|
111
|
+
} catch {}
|
|
112
|
+
|
|
113
|
+
return null;
|
|
114
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
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 (
|
|
41
|
+
url: string,
|
|
42
|
+
timeout: number,
|
|
43
|
+
signal?: AbortSignal,
|
|
44
|
+
): Promise<RenderResult | null> => {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = new URL(url);
|
|
47
|
+
if (parsed.hostname !== "www.nuget.org" && parsed.hostname !== "nuget.org") return null;
|
|
48
|
+
|
|
49
|
+
// Extract package name and optional version from /packages/name or /packages/name/version
|
|
50
|
+
const match = parsed.pathname.match(/^\/packages\/([^/]+)(?:\/([^/]+))?/i);
|
|
51
|
+
if (!match) return null;
|
|
52
|
+
|
|
53
|
+
const packageName = decodeURIComponent(match[1]);
|
|
54
|
+
const requestedVersion = match[2] ? decodeURIComponent(match[2]) : null;
|
|
55
|
+
const fetchedAt = new Date().toISOString();
|
|
56
|
+
|
|
57
|
+
// Fetch from NuGet registration API (package name must be lowercase)
|
|
58
|
+
const apiUrl = `https://api.nuget.org/v3/registration5-gz-semver2/${packageName.toLowerCase()}/index.json`;
|
|
59
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
60
|
+
|
|
61
|
+
if (!result.ok) return null;
|
|
62
|
+
|
|
63
|
+
let index: NuGetRegistrationIndex;
|
|
64
|
+
try {
|
|
65
|
+
index = JSON.parse(result.content);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!index.items?.length) return null;
|
|
71
|
+
|
|
72
|
+
// Get the latest page (or fetch it if not inlined)
|
|
73
|
+
let latestPage = index.items[index.items.length - 1];
|
|
74
|
+
|
|
75
|
+
// If items are not inlined, fetch the page
|
|
76
|
+
if (!latestPage.items && latestPage["@id"]) {
|
|
77
|
+
const pageResult = await loadPage(latestPage["@id"], { timeout, signal });
|
|
78
|
+
if (!pageResult.ok) return null;
|
|
79
|
+
try {
|
|
80
|
+
latestPage = JSON.parse(pageResult.content);
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!latestPage.items?.length) return null;
|
|
87
|
+
|
|
88
|
+
// Find the requested version or get the latest
|
|
89
|
+
let targetEntry: NuGetCatalogEntry | null = null;
|
|
90
|
+
|
|
91
|
+
if (requestedVersion) {
|
|
92
|
+
// Search all pages for the requested version
|
|
93
|
+
for (const page of index.items) {
|
|
94
|
+
let pageItems = page.items;
|
|
95
|
+
|
|
96
|
+
// Fetch page if items not inlined
|
|
97
|
+
if (!pageItems && page["@id"]) {
|
|
98
|
+
const pageResult = await loadPage(page["@id"], { timeout: Math.min(timeout, 5), signal });
|
|
99
|
+
if (pageResult.ok) {
|
|
100
|
+
try {
|
|
101
|
+
const fetchedPage = JSON.parse(pageResult.content) as NuGetRegistrationPage;
|
|
102
|
+
pageItems = fetchedPage.items;
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (pageItems) {
|
|
108
|
+
const found = pageItems.find(
|
|
109
|
+
(item) => item.catalogEntry.version.toLowerCase() === requestedVersion.toLowerCase(),
|
|
110
|
+
);
|
|
111
|
+
if (found) {
|
|
112
|
+
targetEntry = found.catalogEntry;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// If no specific version requested or not found, use the latest
|
|
120
|
+
if (!targetEntry) {
|
|
121
|
+
const latestItem = latestPage.items[latestPage.items.length - 1];
|
|
122
|
+
targetEntry = latestItem.catalogEntry;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Fetch download stats via search API
|
|
126
|
+
let totalDownloads: number | null = null;
|
|
127
|
+
const searchUrl = `https://api.nuget.org/v3/query?q=packageid:${encodeURIComponent(packageName)}&prerelease=true&take=1`;
|
|
128
|
+
const searchResult = await loadPage(searchUrl, { timeout: Math.min(timeout, 5), signal });
|
|
129
|
+
|
|
130
|
+
if (searchResult.ok) {
|
|
131
|
+
try {
|
|
132
|
+
const searchData = JSON.parse(searchResult.content) as {
|
|
133
|
+
data?: Array<{ totalDownloads?: number }>;
|
|
134
|
+
};
|
|
135
|
+
totalDownloads = searchData.data?.[0]?.totalDownloads ?? null;
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Format markdown output
|
|
140
|
+
let md = `# ${targetEntry.id}\n\n`;
|
|
141
|
+
if (targetEntry.description) md += `${targetEntry.description}\n\n`;
|
|
142
|
+
|
|
143
|
+
md += `**Version:** ${targetEntry.version}`;
|
|
144
|
+
if (targetEntry.licenseExpression) {
|
|
145
|
+
md += ` · **License:** ${targetEntry.licenseExpression}`;
|
|
146
|
+
} else if (targetEntry.licenseUrl) {
|
|
147
|
+
md += ` · **License:** [View](${targetEntry.licenseUrl})`;
|
|
148
|
+
}
|
|
149
|
+
md += "\n";
|
|
150
|
+
|
|
151
|
+
if (totalDownloads !== null) {
|
|
152
|
+
md += `**Total Downloads:** ${formatCount(totalDownloads)}\n`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (targetEntry.authors) md += `**Authors:** ${targetEntry.authors}\n`;
|
|
156
|
+
if (targetEntry.projectUrl) md += `**Project URL:** ${targetEntry.projectUrl}\n`;
|
|
157
|
+
if (targetEntry.tags?.length) md += `**Tags:** ${targetEntry.tags.join(", ")}\n`;
|
|
158
|
+
if (targetEntry.published) {
|
|
159
|
+
const pubDate = targetEntry.published.split("T")[0];
|
|
160
|
+
md += `**Published:** ${pubDate}\n`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Show dependencies by target framework
|
|
164
|
+
if (targetEntry.dependencyGroups?.length) {
|
|
165
|
+
const hasAnyDeps = targetEntry.dependencyGroups.some((g) => g.dependencies?.length);
|
|
166
|
+
if (hasAnyDeps) {
|
|
167
|
+
md += `\n## Dependencies\n\n`;
|
|
168
|
+
for (const group of targetEntry.dependencyGroups) {
|
|
169
|
+
if (!group.dependencies?.length) continue;
|
|
170
|
+
const framework = group.targetFramework || "All Frameworks";
|
|
171
|
+
md += `### ${framework}\n\n`;
|
|
172
|
+
for (const dep of group.dependencies) {
|
|
173
|
+
md += `- ${dep.id} (${dep.range})\n`;
|
|
174
|
+
}
|
|
175
|
+
md += "\n";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Show recent versions from the latest page
|
|
181
|
+
if (latestPage.items && latestPage.items.length > 1) {
|
|
182
|
+
md += `## Recent Versions\n\n`;
|
|
183
|
+
const recentVersions = latestPage.items.slice(-5).reverse();
|
|
184
|
+
for (const item of recentVersions) {
|
|
185
|
+
const entry = item.catalogEntry;
|
|
186
|
+
const pubDate = entry.published?.split("T")[0] || "unknown";
|
|
187
|
+
md += `- **${entry.version}** (${pubDate})\n`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const output = finalizeOutput(md);
|
|
192
|
+
return {
|
|
193
|
+
url,
|
|
194
|
+
finalUrl: url,
|
|
195
|
+
contentType: "text/markdown",
|
|
196
|
+
method: "nuget",
|
|
197
|
+
content: output.content,
|
|
198
|
+
fetchedAt,
|
|
199
|
+
truncated: output.truncated,
|
|
200
|
+
notes: ["Fetched via NuGet API"],
|
|
201
|
+
};
|
|
202
|
+
} catch {}
|
|
203
|
+
|
|
204
|
+
return null;
|
|
205
|
+
};
|