@oh-my-pi/pi-coding-agent 3.25.0 → 3.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/package.json +4 -4
- package/src/core/tools/complete.ts +2 -4
- package/src/core/tools/jtd-to-json-schema.ts +174 -196
- package/src/core/tools/read.ts +4 -4
- package/src/core/tools/task/executor.ts +146 -20
- package/src/core/tools/task/name-generator.ts +1544 -214
- package/src/core/tools/task/types.ts +19 -5
- package/src/core/tools/task/worker.ts +103 -13
- package/src/core/tools/web-fetch-handlers/academic.test.ts +239 -0
- package/src/core/tools/web-fetch-handlers/artifacthub.ts +210 -0
- package/src/core/tools/web-fetch-handlers/arxiv.ts +84 -0
- package/src/core/tools/web-fetch-handlers/aur.ts +171 -0
- package/src/core/tools/web-fetch-handlers/biorxiv.ts +136 -0
- package/src/core/tools/web-fetch-handlers/bluesky.ts +277 -0
- package/src/core/tools/web-fetch-handlers/brew.ts +173 -0
- package/src/core/tools/web-fetch-handlers/business.test.ts +82 -0
- package/src/core/tools/web-fetch-handlers/cheatsh.ts +73 -0
- package/src/core/tools/web-fetch-handlers/chocolatey.ts +153 -0
- package/src/core/tools/web-fetch-handlers/coingecko.ts +179 -0
- package/src/core/tools/web-fetch-handlers/crates-io.ts +123 -0
- package/src/core/tools/web-fetch-handlers/dev-platforms.test.ts +254 -0
- package/src/core/tools/web-fetch-handlers/devto.ts +173 -0
- package/src/core/tools/web-fetch-handlers/discogs.ts +303 -0
- package/src/core/tools/web-fetch-handlers/dockerhub.ts +156 -0
- package/src/core/tools/web-fetch-handlers/documentation.test.ts +85 -0
- package/src/core/tools/web-fetch-handlers/finance-media.test.ts +144 -0
- package/src/core/tools/web-fetch-handlers/git-hosting.test.ts +272 -0
- package/src/core/tools/web-fetch-handlers/github-gist.ts +64 -0
- package/src/core/tools/web-fetch-handlers/github.ts +424 -0
- package/src/core/tools/web-fetch-handlers/gitlab.ts +444 -0
- package/src/core/tools/web-fetch-handlers/go-pkg.ts +271 -0
- package/src/core/tools/web-fetch-handlers/hackage.ts +89 -0
- package/src/core/tools/web-fetch-handlers/hackernews.ts +208 -0
- package/src/core/tools/web-fetch-handlers/hex.ts +121 -0
- package/src/core/tools/web-fetch-handlers/huggingface.ts +385 -0
- package/src/core/tools/web-fetch-handlers/iacr.ts +82 -0
- package/src/core/tools/web-fetch-handlers/index.ts +69 -0
- package/src/core/tools/web-fetch-handlers/lobsters.ts +186 -0
- package/src/core/tools/web-fetch-handlers/mastodon.ts +302 -0
- package/src/core/tools/web-fetch-handlers/maven.ts +147 -0
- package/src/core/tools/web-fetch-handlers/mdn.ts +174 -0
- package/src/core/tools/web-fetch-handlers/media.test.ts +138 -0
- package/src/core/tools/web-fetch-handlers/metacpan.ts +247 -0
- package/src/core/tools/web-fetch-handlers/npm.ts +107 -0
- package/src/core/tools/web-fetch-handlers/nuget.ts +201 -0
- package/src/core/tools/web-fetch-handlers/nvd.ts +238 -0
- package/src/core/tools/web-fetch-handlers/opencorporates.ts +273 -0
- package/src/core/tools/web-fetch-handlers/openlibrary.ts +313 -0
- package/src/core/tools/web-fetch-handlers/osv.ts +184 -0
- package/src/core/tools/web-fetch-handlers/package-managers-2.test.ts +199 -0
- package/src/core/tools/web-fetch-handlers/package-managers.test.ts +171 -0
- package/src/core/tools/web-fetch-handlers/package-registries.test.ts +259 -0
- package/src/core/tools/web-fetch-handlers/packagist.ts +170 -0
- package/src/core/tools/web-fetch-handlers/pub-dev.ts +185 -0
- package/src/core/tools/web-fetch-handlers/pubmed.ts +174 -0
- package/src/core/tools/web-fetch-handlers/pypi.ts +125 -0
- package/src/core/tools/web-fetch-handlers/readthedocs.ts +122 -0
- package/src/core/tools/web-fetch-handlers/reddit.ts +100 -0
- package/src/core/tools/web-fetch-handlers/repology.ts +257 -0
- package/src/core/tools/web-fetch-handlers/research.test.ts +107 -0
- package/src/core/tools/web-fetch-handlers/rfc.ts +205 -0
- package/src/core/tools/web-fetch-handlers/rubygems.ts +112 -0
- package/src/core/tools/web-fetch-handlers/sec-edgar.ts +269 -0
- package/src/core/tools/web-fetch-handlers/security.test.ts +103 -0
- package/src/core/tools/web-fetch-handlers/semantic-scholar.ts +190 -0
- package/src/core/tools/web-fetch-handlers/social-extended.test.ts +192 -0
- package/src/core/tools/web-fetch-handlers/social.test.ts +259 -0
- package/src/core/tools/web-fetch-handlers/spotify.ts +218 -0
- package/src/core/tools/web-fetch-handlers/stackexchange.test.ts +120 -0
- package/src/core/tools/web-fetch-handlers/stackoverflow.ts +123 -0
- package/src/core/tools/web-fetch-handlers/standards.test.ts +122 -0
- package/src/core/tools/web-fetch-handlers/terraform.ts +296 -0
- package/src/core/tools/web-fetch-handlers/tldr.ts +47 -0
- package/src/core/tools/web-fetch-handlers/twitter.ts +84 -0
- package/src/core/tools/web-fetch-handlers/types.ts +163 -0
- package/src/core/tools/web-fetch-handlers/utils.ts +91 -0
- package/src/core/tools/web-fetch-handlers/vimeo.ts +152 -0
- package/src/core/tools/web-fetch-handlers/wikidata.ts +349 -0
- package/src/core/tools/web-fetch-handlers/wikipedia.test.ts +73 -0
- package/src/core/tools/web-fetch-handlers/wikipedia.ts +91 -0
- package/src/core/tools/web-fetch-handlers/youtube.test.ts +198 -0
- package/src/core/tools/web-fetch-handlers/youtube.ts +319 -0
- package/src/core/tools/web-fetch.ts +152 -1324
- package/src/utils/tools-manager.ts +110 -8
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { parse as parseHtml } from "node-html-parser";
|
|
2
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
4
|
+
import { convertWithMarkitdown, fetchBinary } from "./utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handle arXiv URLs via arXiv API
|
|
8
|
+
*/
|
|
9
|
+
export const handleArxiv: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
|
|
10
|
+
try {
|
|
11
|
+
const parsed = new URL(url);
|
|
12
|
+
if (parsed.hostname !== "arxiv.org") return null;
|
|
13
|
+
|
|
14
|
+
// Extract paper ID from various URL formats
|
|
15
|
+
// /abs/1234.56789, /pdf/1234.56789, /abs/cs/0123456
|
|
16
|
+
const match = parsed.pathname.match(/\/(abs|pdf)\/(.+?)(?:\.pdf)?$/);
|
|
17
|
+
if (!match) return null;
|
|
18
|
+
|
|
19
|
+
const paperId = match[2];
|
|
20
|
+
const fetchedAt = new Date().toISOString();
|
|
21
|
+
const notes: string[] = [];
|
|
22
|
+
|
|
23
|
+
// Fetch metadata via arXiv API
|
|
24
|
+
const apiUrl = `https://export.arxiv.org/api/query?id_list=${paperId}`;
|
|
25
|
+
const result = await loadPage(apiUrl, { timeout });
|
|
26
|
+
|
|
27
|
+
if (!result.ok) return null;
|
|
28
|
+
|
|
29
|
+
// Parse the Atom feed response
|
|
30
|
+
const doc = parseHtml(result.content, { parseNoneClosedTags: true });
|
|
31
|
+
const entry = doc.querySelector("entry");
|
|
32
|
+
|
|
33
|
+
if (!entry) return null;
|
|
34
|
+
|
|
35
|
+
const title = entry.querySelector("title")?.text?.trim()?.replace(/\s+/g, " ");
|
|
36
|
+
const summary = entry.querySelector("summary")?.text?.trim();
|
|
37
|
+
const authors = entry
|
|
38
|
+
.querySelectorAll("author name")
|
|
39
|
+
.map((n) => n.text?.trim())
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
const published = entry.querySelector("published")?.text?.trim()?.split("T")[0];
|
|
42
|
+
const categories = entry
|
|
43
|
+
.querySelectorAll("category")
|
|
44
|
+
.map((c) => c.getAttribute("term"))
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
const pdfLink = entry.querySelector('link[title="pdf"]')?.getAttribute("href");
|
|
47
|
+
|
|
48
|
+
let md = `# ${title || "arXiv Paper"}\n\n`;
|
|
49
|
+
if (authors.length) md += `**Authors:** ${authors.join(", ")}\n`;
|
|
50
|
+
if (published) md += `**Published:** ${published}\n`;
|
|
51
|
+
if (categories.length) md += `**Categories:** ${categories.join(", ")}\n`;
|
|
52
|
+
md += `**arXiv:** ${paperId}\n\n`;
|
|
53
|
+
md += `---\n\n## Abstract\n\n${summary || "No abstract available."}\n\n`;
|
|
54
|
+
|
|
55
|
+
// If it was a PDF link or we want full content, try to fetch and convert PDF
|
|
56
|
+
if (match[1] === "pdf" || parsed.pathname.includes(".pdf")) {
|
|
57
|
+
if (pdfLink) {
|
|
58
|
+
notes.push("Fetching PDF for full content...");
|
|
59
|
+
const pdfResult = await fetchBinary(pdfLink, timeout);
|
|
60
|
+
if (pdfResult.ok) {
|
|
61
|
+
const converted = await convertWithMarkitdown(pdfResult.buffer, ".pdf", timeout);
|
|
62
|
+
if (converted.ok && converted.content.length > 500) {
|
|
63
|
+
md += `---\n\n## Full Paper\n\n${converted.content}\n`;
|
|
64
|
+
notes.push("PDF converted via markitdown");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const output = finalizeOutput(md);
|
|
71
|
+
return {
|
|
72
|
+
url,
|
|
73
|
+
finalUrl: url,
|
|
74
|
+
contentType: "text/markdown",
|
|
75
|
+
method: "arxiv",
|
|
76
|
+
content: output.content,
|
|
77
|
+
fetchedAt,
|
|
78
|
+
truncated: output.truncated,
|
|
79
|
+
notes: notes.length ? notes : ["Fetched via arXiv API"],
|
|
80
|
+
};
|
|
81
|
+
} catch {}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface AurPackage {
|
|
5
|
+
Name: string;
|
|
6
|
+
Version: string;
|
|
7
|
+
Description?: string;
|
|
8
|
+
Maintainer?: string;
|
|
9
|
+
NumVotes: number;
|
|
10
|
+
Popularity: number;
|
|
11
|
+
Depends?: string[];
|
|
12
|
+
MakeDepends?: string[];
|
|
13
|
+
OptDepends?: string[];
|
|
14
|
+
CheckDepends?: string[];
|
|
15
|
+
LastModified: number;
|
|
16
|
+
FirstSubmitted: number;
|
|
17
|
+
URL?: string;
|
|
18
|
+
URLPath?: string;
|
|
19
|
+
PackageBase: string;
|
|
20
|
+
OutOfDate?: number | null;
|
|
21
|
+
License?: string[];
|
|
22
|
+
Keywords?: string[];
|
|
23
|
+
Conflicts?: string[];
|
|
24
|
+
Provides?: string[];
|
|
25
|
+
Replaces?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface AurResponse {
|
|
29
|
+
version: number;
|
|
30
|
+
type: string;
|
|
31
|
+
resultcount: number;
|
|
32
|
+
results: AurPackage[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Handle AUR (Arch User Repository) URLs via RPC API
|
|
37
|
+
*/
|
|
38
|
+
export const handleAur: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = new URL(url);
|
|
41
|
+
if (parsed.hostname !== "aur.archlinux.org") return null;
|
|
42
|
+
|
|
43
|
+
// Extract package name from /packages/{name}
|
|
44
|
+
const match = parsed.pathname.match(/^\/packages\/([^/?#]+)/);
|
|
45
|
+
if (!match) return null;
|
|
46
|
+
|
|
47
|
+
const packageName = decodeURIComponent(match[1]);
|
|
48
|
+
const fetchedAt = new Date().toISOString();
|
|
49
|
+
|
|
50
|
+
// Fetch from AUR RPC API
|
|
51
|
+
const apiUrl = `https://aur.archlinux.org/rpc/?v=5&type=info&arg=${encodeURIComponent(packageName)}`;
|
|
52
|
+
const result = await loadPage(apiUrl, { timeout });
|
|
53
|
+
|
|
54
|
+
if (!result.ok) return null;
|
|
55
|
+
|
|
56
|
+
let data: AurResponse;
|
|
57
|
+
try {
|
|
58
|
+
data = JSON.parse(result.content);
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (data.resultcount === 0 || !data.results[0]) return null;
|
|
64
|
+
|
|
65
|
+
const pkg = data.results[0];
|
|
66
|
+
|
|
67
|
+
let md = `# ${pkg.Name}\n\n`;
|
|
68
|
+
if (pkg.Description) md += `${pkg.Description}\n\n`;
|
|
69
|
+
|
|
70
|
+
// Package info
|
|
71
|
+
md += `**Version:** ${pkg.Version}`;
|
|
72
|
+
if (pkg.OutOfDate) {
|
|
73
|
+
const outOfDateDate = new Date(pkg.OutOfDate * 1000).toISOString().split("T")[0];
|
|
74
|
+
md += ` (flagged out-of-date: ${outOfDateDate})`;
|
|
75
|
+
}
|
|
76
|
+
md += "\n";
|
|
77
|
+
|
|
78
|
+
if (pkg.Maintainer) {
|
|
79
|
+
md += `**Maintainer:** [${pkg.Maintainer}](https://aur.archlinux.org/account/${pkg.Maintainer})\n`;
|
|
80
|
+
} else {
|
|
81
|
+
md += "**Maintainer:** Orphaned\n";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
md += `**Votes:** ${formatCount(pkg.NumVotes)} · **Popularity:** ${pkg.Popularity.toFixed(2)}\n`;
|
|
85
|
+
|
|
86
|
+
// Timestamps
|
|
87
|
+
const lastModified = new Date(pkg.LastModified * 1000).toISOString().split("T")[0];
|
|
88
|
+
const firstSubmitted = new Date(pkg.FirstSubmitted * 1000).toISOString().split("T")[0];
|
|
89
|
+
md += `**Last Updated:** ${lastModified} · **First Submitted:** ${firstSubmitted}\n`;
|
|
90
|
+
|
|
91
|
+
if (pkg.License?.length) md += `**License:** ${pkg.License.join(", ")}\n`;
|
|
92
|
+
if (pkg.URL) md += `**Upstream:** ${pkg.URL}\n`;
|
|
93
|
+
if (pkg.Keywords?.length) md += `**Keywords:** ${pkg.Keywords.join(", ")}\n`;
|
|
94
|
+
|
|
95
|
+
// Dependencies
|
|
96
|
+
if (pkg.Depends?.length) {
|
|
97
|
+
md += `\n## Dependencies (${pkg.Depends.length})\n\n`;
|
|
98
|
+
for (const dep of pkg.Depends) {
|
|
99
|
+
md += `- ${dep}\n`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (pkg.MakeDepends?.length) {
|
|
104
|
+
md += `\n## Make Dependencies (${pkg.MakeDepends.length})\n\n`;
|
|
105
|
+
for (const dep of pkg.MakeDepends) {
|
|
106
|
+
md += `- ${dep}\n`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (pkg.OptDepends?.length) {
|
|
111
|
+
md += `\n## Optional Dependencies\n\n`;
|
|
112
|
+
for (const dep of pkg.OptDepends) {
|
|
113
|
+
md += `- ${dep}\n`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (pkg.CheckDepends?.length) {
|
|
118
|
+
md += `\n## Check Dependencies\n\n`;
|
|
119
|
+
for (const dep of pkg.CheckDepends) {
|
|
120
|
+
md += `- ${dep}\n`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Package relationships
|
|
125
|
+
if (pkg.Provides?.length) {
|
|
126
|
+
md += `\n## Provides\n\n`;
|
|
127
|
+
for (const p of pkg.Provides) {
|
|
128
|
+
md += `- ${p}\n`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (pkg.Conflicts?.length) {
|
|
133
|
+
md += `\n## Conflicts\n\n`;
|
|
134
|
+
for (const c of pkg.Conflicts) {
|
|
135
|
+
md += `- ${c}\n`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (pkg.Replaces?.length) {
|
|
140
|
+
md += `\n## Replaces\n\n`;
|
|
141
|
+
for (const r of pkg.Replaces) {
|
|
142
|
+
md += `- ${r}\n`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Installation instructions
|
|
147
|
+
md += `\n---\n\n## Installation\n\n`;
|
|
148
|
+
md += "```bash\n";
|
|
149
|
+
md += `# Using an AUR helper (e.g., yay, paru)\n`;
|
|
150
|
+
md += `yay -S ${pkg.Name}\n\n`;
|
|
151
|
+
md += `# Manual installation\n`;
|
|
152
|
+
md += `git clone https://aur.archlinux.org/${pkg.PackageBase}.git\n`;
|
|
153
|
+
md += `cd ${pkg.PackageBase}\n`;
|
|
154
|
+
md += `makepkg -si\n`;
|
|
155
|
+
md += "```\n";
|
|
156
|
+
|
|
157
|
+
const output = finalizeOutput(md);
|
|
158
|
+
return {
|
|
159
|
+
url,
|
|
160
|
+
finalUrl: url,
|
|
161
|
+
contentType: "text/markdown",
|
|
162
|
+
method: "aur",
|
|
163
|
+
content: output.content,
|
|
164
|
+
fetchedAt,
|
|
165
|
+
truncated: output.truncated,
|
|
166
|
+
notes: ["Fetched via AUR RPC API"],
|
|
167
|
+
};
|
|
168
|
+
} catch {}
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface BiorxivPaper {
|
|
5
|
+
biorxiv_doi?: string;
|
|
6
|
+
medrxiv_doi?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
authors?: string;
|
|
9
|
+
author_corresponding?: string;
|
|
10
|
+
author_corresponding_institution?: string;
|
|
11
|
+
abstract?: string;
|
|
12
|
+
date?: string;
|
|
13
|
+
category?: string;
|
|
14
|
+
version?: string;
|
|
15
|
+
type?: string;
|
|
16
|
+
license?: string;
|
|
17
|
+
jatsxml?: string;
|
|
18
|
+
published?: string; // Journal DOI if published
|
|
19
|
+
server?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface BiorxivResponse {
|
|
23
|
+
collection?: BiorxivPaper[];
|
|
24
|
+
messages?: { status: string; count: number }[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Handle bioRxiv and medRxiv preprint URLs via their API
|
|
29
|
+
*/
|
|
30
|
+
export const handleBiorxiv: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = new URL(url);
|
|
33
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
34
|
+
|
|
35
|
+
// Check if it's bioRxiv or medRxiv
|
|
36
|
+
const isBiorxiv = hostname === "www.biorxiv.org" || hostname === "biorxiv.org";
|
|
37
|
+
const isMedrxiv = hostname === "www.medrxiv.org" || hostname === "medrxiv.org";
|
|
38
|
+
|
|
39
|
+
if (!isBiorxiv && !isMedrxiv) return null;
|
|
40
|
+
|
|
41
|
+
// Extract DOI from URL path: /content/10.1101/2024.01.01.123456
|
|
42
|
+
const match = parsed.pathname.match(/\/content\/(10\.\d{4,}\/[^\s?#]+)/);
|
|
43
|
+
if (!match) return null;
|
|
44
|
+
|
|
45
|
+
let doi = match[1];
|
|
46
|
+
// Remove version suffix if present (e.g., v1, v2)
|
|
47
|
+
doi = doi.replace(/v\d+$/, "");
|
|
48
|
+
// Remove trailing .full or .full.pdf
|
|
49
|
+
doi = doi.replace(/\.full(\.pdf)?$/, "");
|
|
50
|
+
|
|
51
|
+
const server = isBiorxiv ? "biorxiv" : "medrxiv";
|
|
52
|
+
const apiUrl = `https://api.${server}.org/details/${server}/${doi}/na/json`;
|
|
53
|
+
|
|
54
|
+
const result = await loadPage(apiUrl, {
|
|
55
|
+
timeout,
|
|
56
|
+
headers: { Accept: "application/json" },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!result.ok) return null;
|
|
60
|
+
|
|
61
|
+
let data: BiorxivResponse;
|
|
62
|
+
try {
|
|
63
|
+
data = JSON.parse(result.content);
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!data.collection || data.collection.length === 0) return null;
|
|
69
|
+
|
|
70
|
+
// Get the latest version (last in array)
|
|
71
|
+
const paper = data.collection[data.collection.length - 1];
|
|
72
|
+
if (!paper) return null;
|
|
73
|
+
|
|
74
|
+
const serverName = isBiorxiv ? "bioRxiv" : "medRxiv";
|
|
75
|
+
const paperDoi = paper.biorxiv_doi || paper.medrxiv_doi || doi;
|
|
76
|
+
|
|
77
|
+
// Build markdown output
|
|
78
|
+
let md = `# ${paper.title || "Untitled Preprint"}\n\n`;
|
|
79
|
+
|
|
80
|
+
// Metadata section
|
|
81
|
+
if (paper.authors) {
|
|
82
|
+
md += `**Authors:** ${paper.authors}\n`;
|
|
83
|
+
}
|
|
84
|
+
if (paper.author_corresponding) {
|
|
85
|
+
let correspondingLine = `**Corresponding Author:** ${paper.author_corresponding}`;
|
|
86
|
+
if (paper.author_corresponding_institution) {
|
|
87
|
+
correspondingLine += ` (${paper.author_corresponding_institution})`;
|
|
88
|
+
}
|
|
89
|
+
md += `${correspondingLine}\n`;
|
|
90
|
+
}
|
|
91
|
+
if (paper.date) {
|
|
92
|
+
md += `**Posted:** ${paper.date}\n`;
|
|
93
|
+
}
|
|
94
|
+
if (paper.category) {
|
|
95
|
+
md += `**Category:** ${paper.category}\n`;
|
|
96
|
+
}
|
|
97
|
+
if (paper.version) {
|
|
98
|
+
md += `**Version:** ${paper.version}\n`;
|
|
99
|
+
}
|
|
100
|
+
if (paper.license) {
|
|
101
|
+
md += `**License:** ${paper.license}\n`;
|
|
102
|
+
}
|
|
103
|
+
md += `**DOI:** [${paperDoi}](https://doi.org/${paperDoi})\n`;
|
|
104
|
+
md += `**Server:** ${serverName}\n`;
|
|
105
|
+
|
|
106
|
+
// Published status
|
|
107
|
+
if (paper.published) {
|
|
108
|
+
md += `\n> **Published in journal:** [${paper.published}](https://doi.org/${paper.published})\n`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Abstract
|
|
112
|
+
md += `\n---\n\n## Abstract\n\n${paper.abstract || "No abstract available."}\n`;
|
|
113
|
+
|
|
114
|
+
// Links section
|
|
115
|
+
md += `\n---\n\n## Links\n\n`;
|
|
116
|
+
md += `- [View on ${serverName}](https://www.${server}.org/content/${paperDoi})\n`;
|
|
117
|
+
md += `- [PDF](https://www.${server}.org/content/${paperDoi}.full.pdf)\n`;
|
|
118
|
+
if (paper.jatsxml) {
|
|
119
|
+
md += `- [JATS XML](${paper.jatsxml})\n`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const output = finalizeOutput(md);
|
|
123
|
+
return {
|
|
124
|
+
url,
|
|
125
|
+
finalUrl: url,
|
|
126
|
+
contentType: "text/markdown",
|
|
127
|
+
method: server,
|
|
128
|
+
content: output.content,
|
|
129
|
+
fetchedAt: new Date().toISOString(),
|
|
130
|
+
truncated: output.truncated,
|
|
131
|
+
notes: [`Fetched via ${serverName} API`],
|
|
132
|
+
};
|
|
133
|
+
} catch {}
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
const API_BASE = "https://public.api.bsky.app/xrpc";
|
|
5
|
+
|
|
6
|
+
interface BlueskyProfile {
|
|
7
|
+
did: string;
|
|
8
|
+
handle: string;
|
|
9
|
+
displayName?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
avatar?: string;
|
|
12
|
+
followersCount?: number;
|
|
13
|
+
followsCount?: number;
|
|
14
|
+
postsCount?: number;
|
|
15
|
+
createdAt?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface BlueskyPost {
|
|
19
|
+
uri: string;
|
|
20
|
+
cid: string;
|
|
21
|
+
author: BlueskyProfile;
|
|
22
|
+
record: {
|
|
23
|
+
text: string;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
embed?: {
|
|
26
|
+
$type: string;
|
|
27
|
+
external?: { uri: string; title?: string; description?: string };
|
|
28
|
+
images?: Array<{ alt?: string; image: unknown }>;
|
|
29
|
+
record?: { uri: string };
|
|
30
|
+
};
|
|
31
|
+
facets?: Array<{
|
|
32
|
+
features: Array<{ $type: string; uri?: string; tag?: string; did?: string }>;
|
|
33
|
+
index: { byteStart: number; byteEnd: number };
|
|
34
|
+
}>;
|
|
35
|
+
};
|
|
36
|
+
likeCount?: number;
|
|
37
|
+
repostCount?: number;
|
|
38
|
+
replyCount?: number;
|
|
39
|
+
quoteCount?: number;
|
|
40
|
+
embed?: {
|
|
41
|
+
$type: string;
|
|
42
|
+
external?: { uri: string; title?: string; description?: string };
|
|
43
|
+
images?: Array<{ alt?: string; fullsize?: string; thumb?: string }>;
|
|
44
|
+
record?: { uri: string; value?: { text?: string }; author?: BlueskyProfile };
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface ThreadViewPost {
|
|
49
|
+
post: BlueskyPost;
|
|
50
|
+
parent?: ThreadViewPost | { $type: string };
|
|
51
|
+
replies?: Array<ThreadViewPost | { $type: string }>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a handle to DID using the profile API
|
|
56
|
+
*/
|
|
57
|
+
async function resolveHandle(handle: string, timeout: number): Promise<string | null> {
|
|
58
|
+
const url = `${API_BASE}/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`;
|
|
59
|
+
const result = await loadPage(url, {
|
|
60
|
+
timeout,
|
|
61
|
+
headers: { Accept: "application/json" },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!result.ok) return null;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const data = JSON.parse(result.content) as BlueskyProfile;
|
|
68
|
+
return data.did;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Format a post as markdown
|
|
76
|
+
*/
|
|
77
|
+
function formatPost(post: BlueskyPost, isQuote = false): string {
|
|
78
|
+
const author = post.author;
|
|
79
|
+
const name = author.displayName || author.handle;
|
|
80
|
+
const handle = `@${author.handle}`;
|
|
81
|
+
const date = new Date(post.record.createdAt).toLocaleString("en-US", {
|
|
82
|
+
year: "numeric",
|
|
83
|
+
month: "short",
|
|
84
|
+
day: "numeric",
|
|
85
|
+
hour: "2-digit",
|
|
86
|
+
minute: "2-digit",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
let md = "";
|
|
90
|
+
|
|
91
|
+
if (isQuote) {
|
|
92
|
+
md += `> **${name}** (${handle}) - ${date}\n>\n`;
|
|
93
|
+
md += post.record.text
|
|
94
|
+
.split("\n")
|
|
95
|
+
.map((line) => `> ${line}`)
|
|
96
|
+
.join("\n");
|
|
97
|
+
md += "\n";
|
|
98
|
+
} else {
|
|
99
|
+
md += `**${name}** (${handle})\n`;
|
|
100
|
+
md += `*${date}*\n\n`;
|
|
101
|
+
md += `${post.record.text}\n`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Handle embeds
|
|
105
|
+
const embed = post.embed;
|
|
106
|
+
if (embed) {
|
|
107
|
+
if (embed.$type === "app.bsky.embed.external#view" && embed.external) {
|
|
108
|
+
const ext = embed.external;
|
|
109
|
+
md += `\n📎 [${ext.title || ext.uri}](${ext.uri})`;
|
|
110
|
+
if (ext.description) md += `\n*${ext.description}*`;
|
|
111
|
+
md += "\n";
|
|
112
|
+
} else if (embed.$type === "app.bsky.embed.images#view" && embed.images) {
|
|
113
|
+
md += `\n🖼️ ${embed.images.length} image(s)`;
|
|
114
|
+
for (const img of embed.images) {
|
|
115
|
+
if (img.alt) md += `\n- Alt: "${img.alt}"`;
|
|
116
|
+
}
|
|
117
|
+
md += "\n";
|
|
118
|
+
} else if (
|
|
119
|
+
(embed.$type === "app.bsky.embed.record#view" || embed.$type === "app.bsky.embed.recordWithMedia#view") &&
|
|
120
|
+
embed.record
|
|
121
|
+
) {
|
|
122
|
+
const rec = embed.record;
|
|
123
|
+
if (rec.value?.text && rec.author) {
|
|
124
|
+
md += "\n**Quoted post:**\n";
|
|
125
|
+
md += `> **${rec.author.displayName || rec.author.handle}** (@${rec.author.handle})\n`;
|
|
126
|
+
md += rec.value.text
|
|
127
|
+
.split("\n")
|
|
128
|
+
.map((line) => `> ${line}`)
|
|
129
|
+
.join("\n");
|
|
130
|
+
md += "\n";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Stats
|
|
136
|
+
if (!isQuote) {
|
|
137
|
+
const stats: string[] = [];
|
|
138
|
+
if (post.likeCount) stats.push(`❤️ ${formatCount(post.likeCount)}`);
|
|
139
|
+
if (post.repostCount) stats.push(`🔁 ${formatCount(post.repostCount)}`);
|
|
140
|
+
if (post.replyCount) stats.push(`💬 ${formatCount(post.replyCount)}`);
|
|
141
|
+
if (post.quoteCount) stats.push(`📝 ${formatCount(post.quoteCount)}`);
|
|
142
|
+
if (stats.length) md += `\n${stats.join(" • ")}\n`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return md;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Handle Bluesky post URLs
|
|
150
|
+
*/
|
|
151
|
+
export const handleBluesky: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
|
|
152
|
+
try {
|
|
153
|
+
const parsed = new URL(url);
|
|
154
|
+
if (!["bsky.app", "www.bsky.app"].includes(parsed.hostname)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const fetchedAt = new Date().toISOString();
|
|
159
|
+
const pathParts = parsed.pathname.split("/").filter(Boolean);
|
|
160
|
+
|
|
161
|
+
// /profile/{handle}
|
|
162
|
+
if (pathParts[0] === "profile" && pathParts[1]) {
|
|
163
|
+
const handle = pathParts[1];
|
|
164
|
+
|
|
165
|
+
// /profile/{handle}/post/{rkey}
|
|
166
|
+
if (pathParts[2] === "post" && pathParts[3]) {
|
|
167
|
+
const rkey = pathParts[3];
|
|
168
|
+
|
|
169
|
+
// First resolve handle to DID
|
|
170
|
+
const did = await resolveHandle(handle, timeout);
|
|
171
|
+
if (!did) return null;
|
|
172
|
+
|
|
173
|
+
// Construct AT URI and fetch thread
|
|
174
|
+
const atUri = `at://${did}/app.bsky.feed.post/${rkey}`;
|
|
175
|
+
const threadUrl = `${API_BASE}/app.bsky.feed.getPostThread?uri=${encodeURIComponent(atUri)}&depth=6&parentHeight=3`;
|
|
176
|
+
|
|
177
|
+
const result = await loadPage(threadUrl, {
|
|
178
|
+
timeout,
|
|
179
|
+
headers: { Accept: "application/json" },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (!result.ok) return null;
|
|
183
|
+
|
|
184
|
+
const data = JSON.parse(result.content) as { thread: ThreadViewPost };
|
|
185
|
+
const thread = data.thread;
|
|
186
|
+
|
|
187
|
+
if (!thread.post) return null;
|
|
188
|
+
|
|
189
|
+
let md = `# Bluesky Post\n\n`;
|
|
190
|
+
|
|
191
|
+
// Show parent context if exists
|
|
192
|
+
if (thread.parent && "post" in thread.parent) {
|
|
193
|
+
md += "**Replying to:**\n";
|
|
194
|
+
md += formatPost(thread.parent.post, true);
|
|
195
|
+
md += "\n---\n\n";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Main post
|
|
199
|
+
md += formatPost(thread.post);
|
|
200
|
+
|
|
201
|
+
// Show replies
|
|
202
|
+
if (thread.replies?.length) {
|
|
203
|
+
md += "\n---\n\n## Replies\n\n";
|
|
204
|
+
let replyCount = 0;
|
|
205
|
+
for (const reply of thread.replies) {
|
|
206
|
+
if (replyCount >= 10) break;
|
|
207
|
+
if ("post" in reply) {
|
|
208
|
+
md += formatPost(reply.post);
|
|
209
|
+
md += "\n---\n\n";
|
|
210
|
+
replyCount++;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const output = finalizeOutput(md);
|
|
216
|
+
return {
|
|
217
|
+
url,
|
|
218
|
+
finalUrl: url,
|
|
219
|
+
contentType: "text/markdown",
|
|
220
|
+
method: "bluesky-api",
|
|
221
|
+
content: output.content,
|
|
222
|
+
fetchedAt,
|
|
223
|
+
truncated: output.truncated,
|
|
224
|
+
notes: [`AT URI: ${atUri}`],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Profile only
|
|
229
|
+
const profileUrl = `${API_BASE}/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`;
|
|
230
|
+
const result = await loadPage(profileUrl, {
|
|
231
|
+
timeout,
|
|
232
|
+
headers: { Accept: "application/json" },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (!result.ok) return null;
|
|
236
|
+
|
|
237
|
+
const profile = JSON.parse(result.content) as BlueskyProfile;
|
|
238
|
+
|
|
239
|
+
let md = `# ${profile.displayName || profile.handle}\n\n`;
|
|
240
|
+
md += `**@${profile.handle}**\n\n`;
|
|
241
|
+
|
|
242
|
+
if (profile.description) {
|
|
243
|
+
md += `${profile.description}\n\n`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
md += "---\n\n";
|
|
247
|
+
md += `- **Followers:** ${formatCount(profile.followersCount || 0)}\n`;
|
|
248
|
+
md += `- **Following:** ${formatCount(profile.followsCount || 0)}\n`;
|
|
249
|
+
md += `- **Posts:** ${formatCount(profile.postsCount || 0)}\n`;
|
|
250
|
+
|
|
251
|
+
if (profile.createdAt) {
|
|
252
|
+
const joined = new Date(profile.createdAt).toLocaleDateString("en-US", {
|
|
253
|
+
year: "numeric",
|
|
254
|
+
month: "long",
|
|
255
|
+
day: "numeric",
|
|
256
|
+
});
|
|
257
|
+
md += `- **Joined:** ${joined}\n`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
md += `\n**DID:** \`${profile.did}\`\n`;
|
|
261
|
+
|
|
262
|
+
const output = finalizeOutput(md);
|
|
263
|
+
return {
|
|
264
|
+
url,
|
|
265
|
+
finalUrl: url,
|
|
266
|
+
contentType: "text/markdown",
|
|
267
|
+
method: "bluesky-api",
|
|
268
|
+
content: output.content,
|
|
269
|
+
fetchedAt,
|
|
270
|
+
truncated: output.truncated,
|
|
271
|
+
notes: ["Fetched via AT Protocol API"],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
} catch {}
|
|
275
|
+
|
|
276
|
+
return null;
|
|
277
|
+
};
|