@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,185 @@
|
|
|
1
|
+
import { finalizeOutput, formatCount, loadPage, type SpecialHandler } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handle pub.dev URLs via API
|
|
5
|
+
*/
|
|
6
|
+
export const handlePubDev: SpecialHandler = async (url: string, timeout: number, signal?: AbortSignal) => {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = new URL(url);
|
|
9
|
+
if (parsed.hostname !== "pub.dev" && parsed.hostname !== "www.pub.dev") return null;
|
|
10
|
+
|
|
11
|
+
// Extract package name from /packages/{package}
|
|
12
|
+
const match = parsed.pathname.match(/^\/packages\/([^/]+)/);
|
|
13
|
+
if (!match) return null;
|
|
14
|
+
|
|
15
|
+
const packageName = decodeURIComponent(match[1]);
|
|
16
|
+
const fetchedAt = new Date().toISOString();
|
|
17
|
+
|
|
18
|
+
// Fetch from pub.dev API
|
|
19
|
+
const apiUrl = `https://pub.dev/api/packages/${encodeURIComponent(packageName)}`;
|
|
20
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
21
|
+
|
|
22
|
+
if (!result.ok) return null;
|
|
23
|
+
|
|
24
|
+
let data: {
|
|
25
|
+
name: string;
|
|
26
|
+
latest: {
|
|
27
|
+
version: string;
|
|
28
|
+
pubspec: {
|
|
29
|
+
description?: string;
|
|
30
|
+
homepage?: string;
|
|
31
|
+
repository?: string;
|
|
32
|
+
documentation?: string;
|
|
33
|
+
environment?: Record<string, string>;
|
|
34
|
+
dependencies?: Record<string, unknown>;
|
|
35
|
+
dev_dependencies?: Record<string, unknown>;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
publisherId?: string;
|
|
39
|
+
metrics?: {
|
|
40
|
+
score?: {
|
|
41
|
+
likeCount?: number;
|
|
42
|
+
grantedPoints?: number;
|
|
43
|
+
maxPoints?: number;
|
|
44
|
+
popularityScore?: number;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
data = JSON.parse(result.content);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { name, latest, publisherId, metrics } = data;
|
|
56
|
+
const pubspec = latest.pubspec;
|
|
57
|
+
|
|
58
|
+
let md = `# ${name}\n\n`;
|
|
59
|
+
if (pubspec.description) md += `${pubspec.description}\n\n`;
|
|
60
|
+
|
|
61
|
+
md += `**Latest:** ${latest.version}`;
|
|
62
|
+
if (publisherId) md += ` · **Publisher:** ${publisherId}`;
|
|
63
|
+
md += "\n";
|
|
64
|
+
|
|
65
|
+
// Add metrics if available
|
|
66
|
+
const score = metrics?.score;
|
|
67
|
+
if (score) {
|
|
68
|
+
const likes = score.likeCount;
|
|
69
|
+
const points = score.grantedPoints;
|
|
70
|
+
const maxPoints = score.maxPoints;
|
|
71
|
+
const popularity = score.popularityScore;
|
|
72
|
+
|
|
73
|
+
if (likes !== undefined) md += `**Likes:** ${formatCount(likes)}`;
|
|
74
|
+
if (points !== undefined && maxPoints !== undefined) {
|
|
75
|
+
md += ` · **Pub Points:** ${points}/${maxPoints}`;
|
|
76
|
+
}
|
|
77
|
+
if (popularity !== undefined) {
|
|
78
|
+
md += ` · **Popularity:** ${Math.round(popularity * 100)}%`;
|
|
79
|
+
}
|
|
80
|
+
md += "\n";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
md += "\n";
|
|
84
|
+
|
|
85
|
+
if (pubspec.homepage) md += `**Homepage:** ${pubspec.homepage}\n`;
|
|
86
|
+
if (pubspec.repository) md += `**Repository:** ${pubspec.repository}\n`;
|
|
87
|
+
if (pubspec.documentation) md += `**Documentation:** ${pubspec.documentation}\n`;
|
|
88
|
+
|
|
89
|
+
// SDK constraints
|
|
90
|
+
if (pubspec.environment) {
|
|
91
|
+
const constraints: string[] = [];
|
|
92
|
+
for (const [key, value] of Object.entries(pubspec.environment)) {
|
|
93
|
+
constraints.push(`${key}: ${value}`);
|
|
94
|
+
}
|
|
95
|
+
if (constraints.length > 0) {
|
|
96
|
+
md += `**SDK:** ${constraints.join(", ")}\n`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
md += "\n";
|
|
101
|
+
|
|
102
|
+
// Dependencies
|
|
103
|
+
if (pubspec.dependencies) {
|
|
104
|
+
const deps = Object.keys(pubspec.dependencies);
|
|
105
|
+
if (deps.length > 0) {
|
|
106
|
+
md += `## Dependencies (${deps.length})\n\n`;
|
|
107
|
+
for (const dep of deps.slice(0, 20)) {
|
|
108
|
+
const constraint = pubspec.dependencies[dep];
|
|
109
|
+
const constraintStr =
|
|
110
|
+
typeof constraint === "string" ? constraint : typeof constraint === "object" ? "complex" : "";
|
|
111
|
+
md += `- ${dep}`;
|
|
112
|
+
if (constraintStr) md += `: ${constraintStr}`;
|
|
113
|
+
md += "\n";
|
|
114
|
+
}
|
|
115
|
+
if (deps.length > 20) {
|
|
116
|
+
md += `\n*...and ${deps.length - 20} more*\n`;
|
|
117
|
+
}
|
|
118
|
+
md += "\n";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Try to fetch README from pub.dev
|
|
123
|
+
const readmeUrl = `https://pub.dev/packages/${encodeURIComponent(packageName)}/versions/${encodeURIComponent(latest.version)}/readme`;
|
|
124
|
+
try {
|
|
125
|
+
const readmeResult = await loadPage(readmeUrl, { timeout: Math.min(timeout, 10), signal });
|
|
126
|
+
if (readmeResult.ok) {
|
|
127
|
+
// Extract README content from HTML
|
|
128
|
+
const readmeMatch = readmeResult.content.match(
|
|
129
|
+
/<div[^>]*class="[^"]*markdown-body[^"]*"[^>]*>([\s\S]*?)<\/div>/i,
|
|
130
|
+
);
|
|
131
|
+
if (readmeMatch) {
|
|
132
|
+
// Basic HTML to markdown conversion for README
|
|
133
|
+
const readme = readmeMatch[1]
|
|
134
|
+
.replace(/<h(\d)[^>]*>(.*?)<\/h\d>/gi, (_, level, text) => {
|
|
135
|
+
const stripped = text.replace(/<[^>]+>/g, "");
|
|
136
|
+
return `${"#".repeat(parseInt(level, 10))} ${stripped}\n\n`;
|
|
137
|
+
})
|
|
138
|
+
.replace(/<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, code) => {
|
|
139
|
+
const decoded = code
|
|
140
|
+
.replace(/</g, "<")
|
|
141
|
+
.replace(/>/g, ">")
|
|
142
|
+
.replace(/&/g, "&")
|
|
143
|
+
.replace(/"/g, '"');
|
|
144
|
+
return `\n\`\`\`\n${decoded}\n\`\`\`\n\n`;
|
|
145
|
+
})
|
|
146
|
+
.replace(/<code[^>]*>(.*?)<\/code>/gi, "`$1`")
|
|
147
|
+
.replace(/<a[^>]*href="([^"]+)"[^>]*>(.*?)<\/a>/gi, "[$2]($1)")
|
|
148
|
+
.replace(/<strong[^>]*>(.*?)<\/strong>/gi, "**$1**")
|
|
149
|
+
.replace(/<em[^>]*>(.*?)<\/em>/gi, "*$1*")
|
|
150
|
+
.replace(/<li[^>]*>(.*?)<\/li>/gi, "- $1\n")
|
|
151
|
+
.replace(/<\/?(ul|ol|p|br)[^>]*>/gi, "\n")
|
|
152
|
+
.replace(/<[^>]+>/g, "")
|
|
153
|
+
.replace(/</g, "<")
|
|
154
|
+
.replace(/>/g, ">")
|
|
155
|
+
.replace(/&/g, "&")
|
|
156
|
+
.replace(/"/g, '"')
|
|
157
|
+
.replace(/'/g, "'")
|
|
158
|
+
.replace(/ /g, " ")
|
|
159
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
160
|
+
.trim();
|
|
161
|
+
|
|
162
|
+
if (readme.length > 100) {
|
|
163
|
+
md += `## README\n\n${readme}\n`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// README fetch failed, continue without it
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const output = finalizeOutput(md);
|
|
172
|
+
return {
|
|
173
|
+
url,
|
|
174
|
+
finalUrl: url,
|
|
175
|
+
contentType: "text/markdown",
|
|
176
|
+
method: "pub.dev",
|
|
177
|
+
content: output.content,
|
|
178
|
+
fetchedAt,
|
|
179
|
+
truncated: output.truncated,
|
|
180
|
+
notes: ["Fetched via pub.dev API"],
|
|
181
|
+
};
|
|
182
|
+
} catch {}
|
|
183
|
+
|
|
184
|
+
return null;
|
|
185
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PubMed handler for web-fetch
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
6
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Handle PubMed URLs - fetch article metadata, abstract, MeSH terms
|
|
10
|
+
*/
|
|
11
|
+
export const handlePubMed: SpecialHandler = async (
|
|
12
|
+
url: string,
|
|
13
|
+
timeout: number,
|
|
14
|
+
signal?: AbortSignal,
|
|
15
|
+
): Promise<RenderResult | null> => {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = new URL(url);
|
|
18
|
+
|
|
19
|
+
// Match pubmed.ncbi.nlm.nih.gov/{pmid} or ncbi.nlm.nih.gov/pubmed/{pmid}
|
|
20
|
+
if (
|
|
21
|
+
parsed.hostname !== "pubmed.ncbi.nlm.nih.gov" &&
|
|
22
|
+
!(parsed.hostname === "ncbi.nlm.nih.gov" && parsed.pathname.startsWith("/pubmed"))
|
|
23
|
+
) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Extract PMID from URL
|
|
28
|
+
let pmid: string | null = null;
|
|
29
|
+
if (parsed.hostname === "pubmed.ncbi.nlm.nih.gov") {
|
|
30
|
+
// Format: pubmed.ncbi.nlm.nih.gov/12345678/
|
|
31
|
+
const match = parsed.pathname.match(/\/(\d+)/);
|
|
32
|
+
if (match) pmid = match[1];
|
|
33
|
+
} else {
|
|
34
|
+
// Format: ncbi.nlm.nih.gov/pubmed/12345678
|
|
35
|
+
const match = parsed.pathname.match(/\/pubmed\/(\d+)/);
|
|
36
|
+
if (match) pmid = match[1];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!pmid) return null;
|
|
40
|
+
|
|
41
|
+
const fetchedAt = new Date().toISOString();
|
|
42
|
+
const notes: string[] = [];
|
|
43
|
+
|
|
44
|
+
// Fetch summary metadata
|
|
45
|
+
const summaryUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&id=${pmid}&retmode=json`;
|
|
46
|
+
const summaryResult = await loadPage(summaryUrl, { timeout, signal });
|
|
47
|
+
|
|
48
|
+
if (!summaryResult.ok) return null;
|
|
49
|
+
|
|
50
|
+
let summaryData: {
|
|
51
|
+
result?: {
|
|
52
|
+
[pmid: string]: {
|
|
53
|
+
title?: string;
|
|
54
|
+
authors?: Array<{ name: string }>;
|
|
55
|
+
fulljournalname?: string;
|
|
56
|
+
pubdate?: string;
|
|
57
|
+
volume?: string;
|
|
58
|
+
issue?: string;
|
|
59
|
+
pages?: string;
|
|
60
|
+
elocationid?: string; // DOI
|
|
61
|
+
articleids?: Array<{ idtype: string; value: string }>;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
summaryData = JSON.parse(summaryResult.content);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const article = summaryData.result?.[pmid];
|
|
73
|
+
if (!article) return null;
|
|
74
|
+
|
|
75
|
+
// Fetch abstract
|
|
76
|
+
const abstractUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=${pmid}&rettype=abstract&retmode=text`;
|
|
77
|
+
const abstractResult = await loadPage(abstractUrl, { timeout, signal });
|
|
78
|
+
|
|
79
|
+
let abstractText = "";
|
|
80
|
+
if (abstractResult.ok) {
|
|
81
|
+
abstractText = abstractResult.content.trim();
|
|
82
|
+
notes.push("Fetched abstract via NCBI E-utilities");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Extract DOI and PMCID
|
|
86
|
+
let doi = "";
|
|
87
|
+
let pmcid = "";
|
|
88
|
+
if (article.articleids) {
|
|
89
|
+
for (const id of article.articleids) {
|
|
90
|
+
if (id.idtype === "doi") doi = id.value;
|
|
91
|
+
if (id.idtype === "pmc") pmcid = id.value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!doi && article.elocationid) {
|
|
95
|
+
doi = article.elocationid;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Build markdown output
|
|
99
|
+
let md = `# ${article.title || "PubMed Article"}\n\n`;
|
|
100
|
+
|
|
101
|
+
// Authors
|
|
102
|
+
if (article.authors && article.authors.length > 0) {
|
|
103
|
+
const authorNames = article.authors.map((a) => a.name).join(", ");
|
|
104
|
+
md += `**Authors:** ${authorNames}\n`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Journal info
|
|
108
|
+
if (article.fulljournalname) {
|
|
109
|
+
md += `**Journal:** ${article.fulljournalname}`;
|
|
110
|
+
if (article.pubdate) md += ` (${article.pubdate})`;
|
|
111
|
+
md += "\n";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Volume/Issue/Pages
|
|
115
|
+
const citation: string[] = [];
|
|
116
|
+
if (article.volume) citation.push(`Vol ${article.volume}`);
|
|
117
|
+
if (article.issue) citation.push(`Issue ${article.issue}`);
|
|
118
|
+
if (article.pages) citation.push(`pp ${article.pages}`);
|
|
119
|
+
if (citation.length > 0) {
|
|
120
|
+
md += `**Citation:** ${citation.join(", ")}\n`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// IDs
|
|
124
|
+
md += `**PMID:** ${pmid}\n`;
|
|
125
|
+
if (doi) md += `**DOI:** ${doi}\n`;
|
|
126
|
+
if (pmcid) md += `**PMCID:** ${pmcid}\n`;
|
|
127
|
+
|
|
128
|
+
md += "\n---\n\n";
|
|
129
|
+
|
|
130
|
+
// Abstract section
|
|
131
|
+
if (abstractText) {
|
|
132
|
+
md += `## Abstract\n\n${abstractText}\n`;
|
|
133
|
+
} else {
|
|
134
|
+
md += `## Abstract\n\nNo abstract available.\n`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Try to fetch MeSH terms
|
|
138
|
+
try {
|
|
139
|
+
const meshUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=${pmid}&rettype=medline&retmode=text`;
|
|
140
|
+
const meshResult = await loadPage(meshUrl, { timeout: Math.min(timeout, 5), signal });
|
|
141
|
+
|
|
142
|
+
if (meshResult.ok) {
|
|
143
|
+
const meshTerms: string[] = [];
|
|
144
|
+
const lines = meshResult.content.split("\n");
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
if (line.startsWith("MH - ")) {
|
|
147
|
+
const term = line.slice(6).trim();
|
|
148
|
+
meshTerms.push(term);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (meshTerms.length > 0) {
|
|
153
|
+
md += `\n## MeSH Terms\n\n`;
|
|
154
|
+
for (const term of meshTerms) {
|
|
155
|
+
md += `- ${term}\n`;
|
|
156
|
+
}
|
|
157
|
+
notes.push("Fetched MeSH terms via NCBI E-utilities");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// MeSH terms are optional
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const output = finalizeOutput(md);
|
|
165
|
+
return {
|
|
166
|
+
url,
|
|
167
|
+
finalUrl: url,
|
|
168
|
+
contentType: "text/markdown",
|
|
169
|
+
method: "pubmed",
|
|
170
|
+
content: output.content,
|
|
171
|
+
fetchedAt,
|
|
172
|
+
truncated: output.truncated,
|
|
173
|
+
notes: notes.length > 0 ? notes : ["Fetched via NCBI E-utilities"],
|
|
174
|
+
};
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handle PyPI URLs via JSON API
|
|
6
|
+
*/
|
|
7
|
+
export const handlePyPI: 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 !== "pypi.org" && parsed.hostname !== "www.pypi.org") return null;
|
|
15
|
+
|
|
16
|
+
// Extract package name from /project/{package} or /project/{package}/{version}
|
|
17
|
+
const match = parsed.pathname.match(/^\/project\/([^/]+)/);
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
|
|
20
|
+
const packageName = decodeURIComponent(match[1]);
|
|
21
|
+
const fetchedAt = new Date().toISOString();
|
|
22
|
+
|
|
23
|
+
// Fetch from PyPI JSON API
|
|
24
|
+
const apiUrl = `https://pypi.org/pypi/${packageName}/json`;
|
|
25
|
+
const downloadsUrl = `https://pypistats.org/api/packages/${packageName}/recent`;
|
|
26
|
+
|
|
27
|
+
// Fetch package info and download stats in parallel
|
|
28
|
+
const [result, downloadsResult] = await Promise.all([
|
|
29
|
+
loadPage(apiUrl, { timeout, signal }),
|
|
30
|
+
loadPage(downloadsUrl, { timeout: Math.min(timeout, 5), signal }),
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
if (!result.ok) return null;
|
|
34
|
+
|
|
35
|
+
// Parse download stats
|
|
36
|
+
let weeklyDownloads: number | null = null;
|
|
37
|
+
if (downloadsResult.ok) {
|
|
38
|
+
try {
|
|
39
|
+
const dlData = JSON.parse(downloadsResult.content) as { data?: { last_week?: number } };
|
|
40
|
+
weeklyDownloads = dlData.data?.last_week ?? null;
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let pkg: {
|
|
45
|
+
info: {
|
|
46
|
+
name: string;
|
|
47
|
+
version: string;
|
|
48
|
+
summary?: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
author?: string;
|
|
51
|
+
author_email?: string;
|
|
52
|
+
license?: string;
|
|
53
|
+
home_page?: string;
|
|
54
|
+
project_urls?: Record<string, string>;
|
|
55
|
+
requires_python?: string;
|
|
56
|
+
keywords?: string;
|
|
57
|
+
classifiers?: string[];
|
|
58
|
+
};
|
|
59
|
+
urls?: Array<{ filename: string; size: number; upload_time: string }>;
|
|
60
|
+
releases?: Record<string, unknown>;
|
|
61
|
+
requires_dist?: string[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
pkg = JSON.parse(result.content);
|
|
66
|
+
} catch {
|
|
67
|
+
return null; // JSON parse failed
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const info = pkg.info;
|
|
71
|
+
let md = `# ${info.name}\n\n`;
|
|
72
|
+
if (info.summary) md += `${info.summary}\n\n`;
|
|
73
|
+
|
|
74
|
+
md += `**Latest:** ${info.version}`;
|
|
75
|
+
if (info.license) md += ` · **License:** ${info.license}`;
|
|
76
|
+
md += "\n";
|
|
77
|
+
|
|
78
|
+
if (weeklyDownloads !== null) {
|
|
79
|
+
md += `**Weekly Downloads:** ${formatCount(weeklyDownloads)}\n`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
md += "\n";
|
|
83
|
+
|
|
84
|
+
if (info.author) {
|
|
85
|
+
md += `**Author:** ${info.author}`;
|
|
86
|
+
if (info.author_email) md += ` <${info.author_email}>`;
|
|
87
|
+
md += "\n";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (info.requires_python) md += `**Python:** ${info.requires_python}\n`;
|
|
91
|
+
if (info.home_page) md += `**Homepage:** ${info.home_page}\n`;
|
|
92
|
+
|
|
93
|
+
if (info.project_urls && Object.keys(info.project_urls).length > 0) {
|
|
94
|
+
md += "\n**Project URLs:**\n";
|
|
95
|
+
for (const [label, url] of Object.entries(info.project_urls)) {
|
|
96
|
+
md += `- ${label}: ${url}\n`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (info.keywords) md += `\n**Keywords:** ${info.keywords}\n`;
|
|
101
|
+
|
|
102
|
+
// Dependencies
|
|
103
|
+
if (pkg.requires_dist && pkg.requires_dist.length > 0) {
|
|
104
|
+
md += `\n## Dependencies\n\n`;
|
|
105
|
+
for (const dep of pkg.requires_dist) {
|
|
106
|
+
md += `- ${dep}\n`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// README/Description
|
|
111
|
+
if (info.description) {
|
|
112
|
+
md += `\n---\n\n## Description\n\n${info.description}\n`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const output = finalizeOutput(md);
|
|
116
|
+
return {
|
|
117
|
+
url,
|
|
118
|
+
finalUrl: url,
|
|
119
|
+
contentType: "text/markdown",
|
|
120
|
+
method: "pypi",
|
|
121
|
+
content: output.content,
|
|
122
|
+
fetchedAt,
|
|
123
|
+
truncated: output.truncated,
|
|
124
|
+
notes: ["Fetched via PyPI JSON API"],
|
|
125
|
+
};
|
|
126
|
+
} catch {}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, htmlToBasicMarkdown, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface RawgPlatformEntry {
|
|
5
|
+
platform?: {
|
|
6
|
+
name?: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface RawgGenreEntry {
|
|
11
|
+
name?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface RawgGameResponse {
|
|
15
|
+
name?: string;
|
|
16
|
+
released?: string;
|
|
17
|
+
rating?: number;
|
|
18
|
+
platforms?: RawgPlatformEntry[];
|
|
19
|
+
genres?: RawgGenreEntry[];
|
|
20
|
+
description?: string;
|
|
21
|
+
description_raw?: string;
|
|
22
|
+
detail?: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const handleRawg: SpecialHandler = async (
|
|
27
|
+
url: string,
|
|
28
|
+
timeout: number,
|
|
29
|
+
signal?: AbortSignal,
|
|
30
|
+
): Promise<RenderResult | null> => {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = new URL(url);
|
|
33
|
+
if (!isRawgHostname(parsed.hostname)) return null;
|
|
34
|
+
|
|
35
|
+
const slug = extractGameSlug(parsed.pathname);
|
|
36
|
+
if (!slug) return null;
|
|
37
|
+
|
|
38
|
+
const fetchedAt = new Date().toISOString();
|
|
39
|
+
const apiUrl = `https://api.rawg.io/api/games/${encodeURIComponent(slug)}`;
|
|
40
|
+
const result = await loadPage(apiUrl, { timeout, signal, headers: { Accept: "application/json" } });
|
|
41
|
+
|
|
42
|
+
if (!result.ok) return null;
|
|
43
|
+
|
|
44
|
+
let game: RawgGameResponse;
|
|
45
|
+
try {
|
|
46
|
+
game = JSON.parse(result.content);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (requiresApiKey(game)) return null;
|
|
52
|
+
|
|
53
|
+
const title = game.name?.trim() || slug;
|
|
54
|
+
let md = `# ${title}\n\n`;
|
|
55
|
+
|
|
56
|
+
if (game.released) md += `**Released:** ${game.released}\n`;
|
|
57
|
+
if (typeof game.rating === "number" && !Number.isNaN(game.rating)) {
|
|
58
|
+
md += `**Rating:** ${game.rating.toFixed(2)} / 5\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const platforms = collectNames(game.platforms?.map((entry) => entry.platform?.name));
|
|
62
|
+
if (platforms.length) md += `**Platforms:** ${platforms.join(", ")}\n`;
|
|
63
|
+
|
|
64
|
+
const genres = collectNames(game.genres?.map((entry) => entry.name));
|
|
65
|
+
if (genres.length) md += `**Genres:** ${genres.join(", ")}\n`;
|
|
66
|
+
|
|
67
|
+
md += `**RAWG:** https://rawg.io/games/${encodeURIComponent(slug)}\n`;
|
|
68
|
+
md += "\n";
|
|
69
|
+
|
|
70
|
+
const description = extractDescription(game);
|
|
71
|
+
if (description) {
|
|
72
|
+
md += `## Description\n\n${description}\n`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const output = finalizeOutput(md);
|
|
76
|
+
return {
|
|
77
|
+
url,
|
|
78
|
+
finalUrl: url,
|
|
79
|
+
contentType: "text/markdown",
|
|
80
|
+
method: "rawg",
|
|
81
|
+
content: output.content,
|
|
82
|
+
fetchedAt,
|
|
83
|
+
truncated: output.truncated,
|
|
84
|
+
notes: ["Fetched via RAWG API"],
|
|
85
|
+
};
|
|
86
|
+
} catch {}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function isRawgHostname(hostname: string): boolean {
|
|
92
|
+
return hostname === "rawg.io" || hostname === "www.rawg.io";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function extractGameSlug(pathname: string): string | null {
|
|
96
|
+
const match = pathname.match(/^\/games\/([^/?#]+)/);
|
|
97
|
+
if (!match) return null;
|
|
98
|
+
|
|
99
|
+
const slug = decodeURIComponent(match[1]);
|
|
100
|
+
return slug ? slug.trim() : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function requiresApiKey(game: RawgGameResponse): boolean {
|
|
104
|
+
const detail = `${game.detail ?? ""} ${game.error ?? ""}`.toLowerCase();
|
|
105
|
+
return detail.includes("api key") || detail.includes("key is required") || detail.includes("apikey");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractDescription(game: RawgGameResponse): string | null {
|
|
109
|
+
if (game.description_raw) return game.description_raw.trim();
|
|
110
|
+
if (!game.description) return null;
|
|
111
|
+
|
|
112
|
+
const markdown = htmlToBasicMarkdown(game.description).trim();
|
|
113
|
+
return markdown || null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function collectNames(values?: Array<string | undefined>): string[] {
|
|
117
|
+
if (!values?.length) return [];
|
|
118
|
+
const names = new Set<string>();
|
|
119
|
+
for (const value of values) {
|
|
120
|
+
const trimmed = value?.trim();
|
|
121
|
+
if (trimmed) names.add(trimmed);
|
|
122
|
+
}
|
|
123
|
+
return Array.from(names);
|
|
124
|
+
}
|