@oh-my-pi/pi-coding-agent 3.24.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 +34 -0
- package/package.json +4 -4
- package/src/core/custom-commands/bundled/wt/index.ts +3 -0
- package/src/core/sdk.ts +7 -0
- package/src/core/tools/complete.ts +129 -0
- package/src/core/tools/index.test.ts +9 -1
- package/src/core/tools/index.ts +18 -5
- package/src/core/tools/jtd-to-json-schema.ts +252 -0
- package/src/core/tools/output.ts +125 -14
- package/src/core/tools/read.ts +4 -4
- package/src/core/tools/task/artifacts.ts +6 -9
- package/src/core/tools/task/executor.ts +189 -24
- package/src/core/tools/task/index.ts +23 -18
- package/src/core/tools/task/name-generator.ts +1577 -0
- package/src/core/tools/task/render.ts +137 -8
- package/src/core/tools/task/types.ts +26 -5
- package/src/core/tools/task/worker-protocol.ts +1 -0
- package/src/core/tools/task/worker.ts +136 -14
- 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/prompts/task.md +14 -50
- package/src/prompts/tools/output.md +2 -1
- package/src/prompts/tools/task.md +3 -1
- package/src/utils/tools-manager.ts +110 -8
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface BrewFormula {
|
|
5
|
+
name: string;
|
|
6
|
+
full_name?: string;
|
|
7
|
+
desc?: string;
|
|
8
|
+
homepage?: string;
|
|
9
|
+
license?: string;
|
|
10
|
+
versions?: {
|
|
11
|
+
stable?: string;
|
|
12
|
+
head?: string;
|
|
13
|
+
bottle?: boolean;
|
|
14
|
+
};
|
|
15
|
+
dependencies?: string[];
|
|
16
|
+
build_dependencies?: string[];
|
|
17
|
+
optional_dependencies?: string[];
|
|
18
|
+
conflicts_with?: string[];
|
|
19
|
+
caveats?: string;
|
|
20
|
+
analytics?: {
|
|
21
|
+
install?: {
|
|
22
|
+
"30d"?: Record<string, number>;
|
|
23
|
+
"90d"?: Record<string, number>;
|
|
24
|
+
"365d"?: Record<string, number>;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface BrewCask {
|
|
30
|
+
token: string;
|
|
31
|
+
name?: string[];
|
|
32
|
+
desc?: string;
|
|
33
|
+
homepage?: string;
|
|
34
|
+
version?: string;
|
|
35
|
+
sha256?: string;
|
|
36
|
+
caveats?: string;
|
|
37
|
+
depends_on?: {
|
|
38
|
+
macos?: Record<string, string[]>;
|
|
39
|
+
};
|
|
40
|
+
conflicts_with?: {
|
|
41
|
+
cask?: string[];
|
|
42
|
+
};
|
|
43
|
+
analytics?: {
|
|
44
|
+
install?: {
|
|
45
|
+
"30d"?: Record<string, number>;
|
|
46
|
+
"90d"?: Record<string, number>;
|
|
47
|
+
"365d"?: Record<string, number>;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getInstallCount(analytics?: { install?: { "30d"?: Record<string, number> } }): number | null {
|
|
53
|
+
if (!analytics?.install?.["30d"]) return null;
|
|
54
|
+
const counts = Object.values(analytics.install["30d"]);
|
|
55
|
+
return counts.reduce((sum, n) => sum + n, 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Handle Homebrew formulae and cask URLs via API
|
|
60
|
+
*/
|
|
61
|
+
export const handleBrew: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
|
|
62
|
+
try {
|
|
63
|
+
const parsed = new URL(url);
|
|
64
|
+
if (parsed.hostname !== "formulae.brew.sh") return null;
|
|
65
|
+
|
|
66
|
+
const formulaMatch = parsed.pathname.match(/^\/formula\/([^/]+)\/?$/);
|
|
67
|
+
const caskMatch = parsed.pathname.match(/^\/cask\/([^/]+)\/?$/);
|
|
68
|
+
|
|
69
|
+
if (!formulaMatch && !caskMatch) return null;
|
|
70
|
+
|
|
71
|
+
const fetchedAt = new Date().toISOString();
|
|
72
|
+
const isFormula = Boolean(formulaMatch);
|
|
73
|
+
const name = decodeURIComponent(isFormula ? formulaMatch![1] : caskMatch![1]);
|
|
74
|
+
|
|
75
|
+
const apiUrl = isFormula
|
|
76
|
+
? `https://formulae.brew.sh/api/formula/${encodeURIComponent(name)}.json`
|
|
77
|
+
: `https://formulae.brew.sh/api/cask/${encodeURIComponent(name)}.json`;
|
|
78
|
+
|
|
79
|
+
const result = await loadPage(apiUrl, { timeout });
|
|
80
|
+
if (!result.ok) return null;
|
|
81
|
+
|
|
82
|
+
let md: string;
|
|
83
|
+
|
|
84
|
+
if (isFormula) {
|
|
85
|
+
const formula: BrewFormula = JSON.parse(result.content);
|
|
86
|
+
|
|
87
|
+
md = `# ${formula.full_name || formula.name}\n\n`;
|
|
88
|
+
if (formula.desc) md += `${formula.desc}\n\n`;
|
|
89
|
+
|
|
90
|
+
md += `**Version:** ${formula.versions?.stable || "unknown"}`;
|
|
91
|
+
if (formula.license) md += ` · **License:** ${formula.license}`;
|
|
92
|
+
md += "\n";
|
|
93
|
+
|
|
94
|
+
const installs = getInstallCount(formula.analytics);
|
|
95
|
+
if (installs !== null) {
|
|
96
|
+
md += `**Installs (30d):** ${formatCount(installs)}\n`;
|
|
97
|
+
}
|
|
98
|
+
md += "\n";
|
|
99
|
+
|
|
100
|
+
md += `\`\`\`bash\nbrew install ${formula.name}\n\`\`\`\n\n`;
|
|
101
|
+
|
|
102
|
+
if (formula.homepage) md += `**Homepage:** ${formula.homepage}\n`;
|
|
103
|
+
|
|
104
|
+
if (formula.dependencies?.length) {
|
|
105
|
+
md += `\n## Dependencies\n\n`;
|
|
106
|
+
for (const dep of formula.dependencies) {
|
|
107
|
+
md += `- ${dep}\n`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (formula.build_dependencies?.length) {
|
|
112
|
+
md += `\n## Build Dependencies\n\n`;
|
|
113
|
+
for (const dep of formula.build_dependencies) {
|
|
114
|
+
md += `- ${dep}\n`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (formula.conflicts_with?.length) {
|
|
119
|
+
md += `\n## Conflicts With\n\n`;
|
|
120
|
+
for (const conflict of formula.conflicts_with) {
|
|
121
|
+
md += `- ${conflict}\n`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (formula.caveats) {
|
|
126
|
+
md += `\n## Caveats\n\n${formula.caveats}\n`;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
const cask: BrewCask = JSON.parse(result.content);
|
|
130
|
+
|
|
131
|
+
const displayName = cask.name?.[0] || cask.token;
|
|
132
|
+
md = `# ${displayName}\n\n`;
|
|
133
|
+
if (cask.desc) md += `${cask.desc}\n\n`;
|
|
134
|
+
|
|
135
|
+
md += `**Version:** ${cask.version || "unknown"}\n`;
|
|
136
|
+
|
|
137
|
+
const installs = getInstallCount(cask.analytics);
|
|
138
|
+
if (installs !== null) {
|
|
139
|
+
md += `**Installs (30d):** ${formatCount(installs)}\n`;
|
|
140
|
+
}
|
|
141
|
+
md += "\n";
|
|
142
|
+
|
|
143
|
+
md += `\`\`\`bash\nbrew install --cask ${cask.token}\n\`\`\`\n\n`;
|
|
144
|
+
|
|
145
|
+
if (cask.homepage) md += `**Homepage:** ${cask.homepage}\n`;
|
|
146
|
+
|
|
147
|
+
if (cask.conflicts_with?.cask?.length) {
|
|
148
|
+
md += `\n## Conflicts With\n\n`;
|
|
149
|
+
for (const conflict of cask.conflicts_with.cask) {
|
|
150
|
+
md += `- ${conflict}\n`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (cask.caveats) {
|
|
155
|
+
md += `\n## Caveats\n\n${cask.caveats}\n`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const output = finalizeOutput(md);
|
|
160
|
+
return {
|
|
161
|
+
url,
|
|
162
|
+
finalUrl: url,
|
|
163
|
+
contentType: "text/markdown",
|
|
164
|
+
method: "brew",
|
|
165
|
+
content: output.content,
|
|
166
|
+
fetchedAt,
|
|
167
|
+
truncated: output.truncated,
|
|
168
|
+
notes: [`Fetched via Homebrew ${isFormula ? "formula" : "cask"} API`],
|
|
169
|
+
};
|
|
170
|
+
} catch {}
|
|
171
|
+
|
|
172
|
+
return null;
|
|
173
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { handleOpenCorporates } from "./opencorporates";
|
|
3
|
+
import { handleSecEdgar } from "./sec-edgar";
|
|
4
|
+
|
|
5
|
+
const SKIP = !process.env.WEB_FETCH_INTEGRATION;
|
|
6
|
+
|
|
7
|
+
describe.skipIf(SKIP)("handleSecEdgar", () => {
|
|
8
|
+
it("returns null for non-matching URLs", async () => {
|
|
9
|
+
const result = await handleSecEdgar("https://example.com", 20);
|
|
10
|
+
expect(result).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns null for SEC URLs without valid CIK", async () => {
|
|
14
|
+
const result = await handleSecEdgar("https://www.sec.gov/about.html", 20);
|
|
15
|
+
expect(result).toBeNull();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("fetches Apple Inc filings by CIK query param", async () => {
|
|
19
|
+
const result = await handleSecEdgar(
|
|
20
|
+
"https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=0000320193",
|
|
21
|
+
20,
|
|
22
|
+
);
|
|
23
|
+
expect(result).not.toBeNull();
|
|
24
|
+
expect(result?.method).toBe("sec-edgar");
|
|
25
|
+
expect(result?.content).toContain("APPLE INC");
|
|
26
|
+
expect(result?.content).toContain("0000320193");
|
|
27
|
+
expect(result?.content).toContain("10-K"); // Apple files 10-K annually
|
|
28
|
+
expect(result?.contentType).toBe("text/markdown");
|
|
29
|
+
expect(result?.fetchedAt).toBeTruthy();
|
|
30
|
+
expect(result?.truncated).toBeDefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("fetches via data.sec.gov submissions URL", async () => {
|
|
34
|
+
const result = await handleSecEdgar("https://data.sec.gov/submissions/CIK0000320193.json", 20);
|
|
35
|
+
expect(result).not.toBeNull();
|
|
36
|
+
expect(result?.method).toBe("sec-edgar");
|
|
37
|
+
expect(result?.content).toContain("APPLE INC");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("fetches via Archives path", async () => {
|
|
41
|
+
// Any filing URL with CIK in Archives path
|
|
42
|
+
const result = await handleSecEdgar(
|
|
43
|
+
"https://www.sec.gov/Archives/edgar/data/320193/000032019324000123/aapl-20240928.htm",
|
|
44
|
+
20,
|
|
45
|
+
);
|
|
46
|
+
expect(result).not.toBeNull();
|
|
47
|
+
expect(result?.method).toBe("sec-edgar");
|
|
48
|
+
expect(result?.content).toContain("APPLE INC");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe.skipIf(SKIP)("handleOpenCorporates", () => {
|
|
53
|
+
it("returns null for non-matching URLs", async () => {
|
|
54
|
+
const result = await handleOpenCorporates("https://example.com", 20);
|
|
55
|
+
expect(result).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns null for OpenCorporates URLs without company path", async () => {
|
|
59
|
+
const result = await handleOpenCorporates("https://opencorporates.com/about", 20);
|
|
60
|
+
expect(result).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("fetches Apple Inc Delaware registration", async () => {
|
|
64
|
+
const result = await handleOpenCorporates("https://opencorporates.com/companies/us_de/2927442", 20);
|
|
65
|
+
expect(result).not.toBeNull();
|
|
66
|
+
expect(result?.method).toBe("opencorporates");
|
|
67
|
+
expect(result?.content).toContain("APPLE INC");
|
|
68
|
+
expect(result?.content).toContain("2927442");
|
|
69
|
+
expect(result?.content).toContain("US_DE");
|
|
70
|
+
expect(result?.contentType).toBe("text/markdown");
|
|
71
|
+
expect(result?.fetchedAt).toBeTruthy();
|
|
72
|
+
expect(result?.truncated).toBeDefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("fetches Microsoft Corporation", async () => {
|
|
76
|
+
// Microsoft is registered in Washington state
|
|
77
|
+
const result = await handleOpenCorporates("https://opencorporates.com/companies/us_wa/600413485", 20);
|
|
78
|
+
expect(result).not.toBeNull();
|
|
79
|
+
expect(result?.method).toBe("opencorporates");
|
|
80
|
+
expect(result?.content).toMatch(/microsoft/i);
|
|
81
|
+
});
|
|
82
|
+
});
|