@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,73 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handle cheat.sh / cht.sh URLs for command cheatsheets
|
|
6
|
+
*
|
|
7
|
+
* API: Plain text at https://cheat.sh/{topic}?T (T flag removes ANSI colors)
|
|
8
|
+
* Supports: commands, language/topic queries (e.g., python/list, go/slice)
|
|
9
|
+
*/
|
|
10
|
+
export const handleCheatSh: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = new URL(url);
|
|
13
|
+
if (parsed.hostname !== "cheat.sh" && parsed.hostname !== "cht.sh") return null;
|
|
14
|
+
|
|
15
|
+
// Extract topic from path (everything after /)
|
|
16
|
+
const topic = parsed.pathname.slice(1);
|
|
17
|
+
if (!topic || topic === "" || topic === "/") return null;
|
|
18
|
+
|
|
19
|
+
const fetchedAt = new Date().toISOString();
|
|
20
|
+
|
|
21
|
+
// Fetch from cheat.sh API with ?T to disable ANSI colors
|
|
22
|
+
const apiUrl = `https://cheat.sh/${encodeURIComponent(topic)}?T`;
|
|
23
|
+
const result = await loadPage(apiUrl, {
|
|
24
|
+
timeout,
|
|
25
|
+
headers: {
|
|
26
|
+
Accept: "text/plain",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!result.ok || !result.content.trim()) return null;
|
|
31
|
+
|
|
32
|
+
// Format the cheatsheet as markdown
|
|
33
|
+
const decodedTopic = decodeURIComponent(topic);
|
|
34
|
+
let md = `# cheat.sh/${decodedTopic}\n\n`;
|
|
35
|
+
|
|
36
|
+
// The content is already plain text, wrap in code block for readability
|
|
37
|
+
// Detect if it looks like code/commands vs prose
|
|
38
|
+
const content = result.content.trim();
|
|
39
|
+
const lines = content.split("\n");
|
|
40
|
+
const hasCodeIndicators = lines.some(
|
|
41
|
+
(line) =>
|
|
42
|
+
line.startsWith("$") ||
|
|
43
|
+
line.startsWith("#") ||
|
|
44
|
+
line.includes("()") ||
|
|
45
|
+
line.includes("=>") ||
|
|
46
|
+
/^\s*(if|for|while|def|func|fn|let|const|var)\b/.test(line),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (hasCodeIndicators || decodedTopic.includes("/")) {
|
|
50
|
+
// Likely code examples - use code block
|
|
51
|
+
// Try to detect language from topic
|
|
52
|
+
const lang = decodedTopic.split("/")[0] || "bash";
|
|
53
|
+
md += `\`\`\`${lang}\n${content}\n\`\`\`\n`;
|
|
54
|
+
} else {
|
|
55
|
+
// Command cheatsheet - format as-is (already has good structure)
|
|
56
|
+
md += `\`\`\`\n${content}\n\`\`\`\n`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const output = finalizeOutput(md);
|
|
60
|
+
return {
|
|
61
|
+
url,
|
|
62
|
+
finalUrl: url,
|
|
63
|
+
contentType: "text/markdown",
|
|
64
|
+
method: "cheat.sh",
|
|
65
|
+
content: output.content,
|
|
66
|
+
fetchedAt,
|
|
67
|
+
truncated: output.truncated,
|
|
68
|
+
notes: ["Fetched via cheat.sh"],
|
|
69
|
+
};
|
|
70
|
+
} catch {}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface NuGetODataEntry {
|
|
5
|
+
Id: string;
|
|
6
|
+
Version: string;
|
|
7
|
+
Title?: string;
|
|
8
|
+
Description?: string;
|
|
9
|
+
Summary?: string;
|
|
10
|
+
Authors?: string;
|
|
11
|
+
ProjectUrl?: string;
|
|
12
|
+
PackageSourceUrl?: string;
|
|
13
|
+
Tags?: string;
|
|
14
|
+
DownloadCount?: number;
|
|
15
|
+
VersionDownloadCount?: number;
|
|
16
|
+
Published?: string;
|
|
17
|
+
LicenseUrl?: string;
|
|
18
|
+
ReleaseNotes?: string;
|
|
19
|
+
Dependencies?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface NuGetODataResponse {
|
|
23
|
+
d?: {
|
|
24
|
+
results?: NuGetODataEntry[];
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Handle Chocolatey package URLs via NuGet v2 OData API
|
|
30
|
+
*/
|
|
31
|
+
export const handleChocolatey: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = new URL(url);
|
|
34
|
+
if (!parsed.hostname.includes("chocolatey.org")) return null;
|
|
35
|
+
|
|
36
|
+
// Extract package name from /packages/{name} or /packages/{name}/{version}
|
|
37
|
+
const match = parsed.pathname.match(/^\/packages\/([^/]+)(?:\/([^/]+))?/);
|
|
38
|
+
if (!match) return null;
|
|
39
|
+
|
|
40
|
+
const packageName = decodeURIComponent(match[1]);
|
|
41
|
+
const specificVersion = match[2] ? decodeURIComponent(match[2]) : null;
|
|
42
|
+
|
|
43
|
+
const fetchedAt = new Date().toISOString();
|
|
44
|
+
|
|
45
|
+
// Build OData query - filter by Id and optionally version
|
|
46
|
+
let apiUrl = `https://community.chocolatey.org/api/v2/Packages()?$filter=Id%20eq%20'${encodeURIComponent(packageName)}'`;
|
|
47
|
+
if (specificVersion) {
|
|
48
|
+
apiUrl += `%20and%20Version%20eq%20'${encodeURIComponent(specificVersion)}'`;
|
|
49
|
+
} else {
|
|
50
|
+
// Get latest version by ordering and taking first
|
|
51
|
+
apiUrl += "&$orderby=Version%20desc&$top=1";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const result = await loadPage(apiUrl, {
|
|
55
|
+
timeout,
|
|
56
|
+
headers: {
|
|
57
|
+
Accept: "application/json",
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!result.ok) return null;
|
|
62
|
+
|
|
63
|
+
let data: NuGetODataResponse;
|
|
64
|
+
try {
|
|
65
|
+
data = JSON.parse(result.content);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const pkg = data.d?.results?.[0];
|
|
71
|
+
if (!pkg) return null;
|
|
72
|
+
|
|
73
|
+
// Build markdown output
|
|
74
|
+
let md = `# ${pkg.Title || pkg.Id}\n\n`;
|
|
75
|
+
|
|
76
|
+
if (pkg.Summary) {
|
|
77
|
+
md += `${pkg.Summary}\n\n`;
|
|
78
|
+
} else if (pkg.Description) {
|
|
79
|
+
// Use first paragraph of description as summary
|
|
80
|
+
const firstPara = pkg.Description.split(/\n\n/)[0];
|
|
81
|
+
md += `${firstPara}\n\n`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
md += `**Version:** ${pkg.Version}`;
|
|
85
|
+
if (pkg.Authors) md += ` · **Authors:** ${pkg.Authors}`;
|
|
86
|
+
md += "\n";
|
|
87
|
+
|
|
88
|
+
if (pkg.DownloadCount !== undefined) {
|
|
89
|
+
md += `**Total Downloads:** ${formatCount(pkg.DownloadCount)}`;
|
|
90
|
+
if (pkg.VersionDownloadCount !== undefined) {
|
|
91
|
+
md += ` · **Version Downloads:** ${formatCount(pkg.VersionDownloadCount)}`;
|
|
92
|
+
}
|
|
93
|
+
md += "\n";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (pkg.Published) {
|
|
97
|
+
const date = new Date(pkg.Published);
|
|
98
|
+
md += `**Published:** ${date.toISOString().split("T")[0]}\n`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
md += "\n";
|
|
102
|
+
|
|
103
|
+
if (pkg.ProjectUrl) md += `**Project URL:** ${pkg.ProjectUrl}\n`;
|
|
104
|
+
if (pkg.PackageSourceUrl) md += `**Source:** ${pkg.PackageSourceUrl}\n`;
|
|
105
|
+
if (pkg.LicenseUrl) md += `**License:** ${pkg.LicenseUrl}\n`;
|
|
106
|
+
|
|
107
|
+
if (pkg.Tags) {
|
|
108
|
+
const tags = pkg.Tags.split(/\s+/).filter((t) => t.length > 0);
|
|
109
|
+
if (tags.length > 0) {
|
|
110
|
+
md += `**Tags:** ${tags.join(", ")}\n`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Full description if different from summary
|
|
115
|
+
if (pkg.Description && pkg.Description !== pkg.Summary) {
|
|
116
|
+
md += `\n## Description\n\n${pkg.Description}\n`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (pkg.ReleaseNotes) {
|
|
120
|
+
md += `\n## Release Notes\n\n${pkg.ReleaseNotes}\n`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (pkg.Dependencies) {
|
|
124
|
+
// Dependencies format: "id:version|id:version"
|
|
125
|
+
const deps = pkg.Dependencies.split("|").filter((d) => d.trim().length > 0);
|
|
126
|
+
if (deps.length > 0) {
|
|
127
|
+
md += `\n## Dependencies\n\n`;
|
|
128
|
+
for (const dep of deps) {
|
|
129
|
+
const [depId, depVersion] = dep.split(":");
|
|
130
|
+
if (depId) {
|
|
131
|
+
md += `- ${depId}${depVersion ? `: ${depVersion}` : ""}\n`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
md += `\n---\n**Install:** \`choco install ${packageName}\`\n`;
|
|
138
|
+
|
|
139
|
+
const output = finalizeOutput(md);
|
|
140
|
+
return {
|
|
141
|
+
url,
|
|
142
|
+
finalUrl: url,
|
|
143
|
+
contentType: "text/markdown",
|
|
144
|
+
method: "chocolatey",
|
|
145
|
+
content: output.content,
|
|
146
|
+
fetchedAt,
|
|
147
|
+
truncated: output.truncated,
|
|
148
|
+
notes: ["Fetched via Chocolatey NuGet API"],
|
|
149
|
+
};
|
|
150
|
+
} catch {}
|
|
151
|
+
|
|
152
|
+
return null;
|
|
153
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface CoinGeckoResponse {
|
|
5
|
+
id: string;
|
|
6
|
+
symbol: string;
|
|
7
|
+
name: string;
|
|
8
|
+
description?: { en?: string };
|
|
9
|
+
links?: {
|
|
10
|
+
homepage?: string[];
|
|
11
|
+
blockchain_site?: string[];
|
|
12
|
+
repos_url?: { github?: string[] };
|
|
13
|
+
};
|
|
14
|
+
market_data?: {
|
|
15
|
+
current_price?: { usd?: number };
|
|
16
|
+
market_cap?: { usd?: number };
|
|
17
|
+
total_volume?: { usd?: number };
|
|
18
|
+
price_change_percentage_24h?: number;
|
|
19
|
+
ath?: { usd?: number };
|
|
20
|
+
ath_date?: { usd?: string };
|
|
21
|
+
circulating_supply?: number;
|
|
22
|
+
total_supply?: number;
|
|
23
|
+
max_supply?: number;
|
|
24
|
+
};
|
|
25
|
+
categories?: string[];
|
|
26
|
+
genesis_date?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Handle CoinGecko cryptocurrency URLs via API
|
|
31
|
+
*/
|
|
32
|
+
export const handleCoinGecko: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = new URL(url);
|
|
35
|
+
if (!parsed.hostname.includes("coingecko.com")) return null;
|
|
36
|
+
|
|
37
|
+
// Extract coin ID from /coins/{id} or /en/coins/{id}
|
|
38
|
+
const match = parsed.pathname.match(/^(?:\/[a-z]{2})?\/coins\/([^/?#]+)/);
|
|
39
|
+
if (!match) return null;
|
|
40
|
+
|
|
41
|
+
const coinId = decodeURIComponent(match[1]);
|
|
42
|
+
const fetchedAt = new Date().toISOString();
|
|
43
|
+
|
|
44
|
+
// Fetch from CoinGecko API
|
|
45
|
+
const apiUrl = `https://api.coingecko.com/api/v3/coins/${coinId}?localization=false&tickers=false&community_data=false&developer_data=false`;
|
|
46
|
+
const result = await loadPage(apiUrl, {
|
|
47
|
+
timeout,
|
|
48
|
+
headers: { Accept: "application/json" },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!result.ok) return null;
|
|
52
|
+
|
|
53
|
+
let coin: CoinGeckoResponse;
|
|
54
|
+
try {
|
|
55
|
+
coin = JSON.parse(result.content);
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const market = coin.market_data;
|
|
61
|
+
|
|
62
|
+
let md = `# ${coin.name} (${coin.symbol.toUpperCase()})\n\n`;
|
|
63
|
+
|
|
64
|
+
// Price and market data
|
|
65
|
+
if (market?.current_price?.usd !== undefined) {
|
|
66
|
+
md += `**Price:** $${formatPrice(market.current_price.usd)}`;
|
|
67
|
+
if (market.price_change_percentage_24h !== undefined) {
|
|
68
|
+
const change = market.price_change_percentage_24h;
|
|
69
|
+
const sign = change >= 0 ? "+" : "";
|
|
70
|
+
md += ` (${sign}${change.toFixed(2)}% 24h)`;
|
|
71
|
+
}
|
|
72
|
+
md += "\n";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (market?.market_cap?.usd) {
|
|
76
|
+
md += `**Market Cap:** $${formatLargeNumber(market.market_cap.usd)}\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (market?.total_volume?.usd) {
|
|
80
|
+
md += `**24h Volume:** $${formatLargeNumber(market.total_volume.usd)}\n`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (market?.ath?.usd !== undefined) {
|
|
84
|
+
md += `**All-Time High:** $${formatPrice(market.ath.usd)}`;
|
|
85
|
+
if (market.ath_date?.usd) {
|
|
86
|
+
const athDate = new Date(market.ath_date.usd).toLocaleDateString("en-US", {
|
|
87
|
+
year: "numeric",
|
|
88
|
+
month: "short",
|
|
89
|
+
day: "numeric",
|
|
90
|
+
});
|
|
91
|
+
md += ` (${athDate})`;
|
|
92
|
+
}
|
|
93
|
+
md += "\n";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
md += "\n";
|
|
97
|
+
|
|
98
|
+
// Supply info
|
|
99
|
+
if (market?.circulating_supply) {
|
|
100
|
+
md += `**Circulating Supply:** ${formatCount(Math.round(market.circulating_supply))}`;
|
|
101
|
+
if (market.max_supply) {
|
|
102
|
+
const percent = ((market.circulating_supply / market.max_supply) * 100).toFixed(1);
|
|
103
|
+
md += ` / ${formatCount(Math.round(market.max_supply))} (${percent}%)`;
|
|
104
|
+
} else if (market.total_supply) {
|
|
105
|
+
md += ` / ${formatCount(Math.round(market.total_supply))} total`;
|
|
106
|
+
}
|
|
107
|
+
md += "\n";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (coin.genesis_date) {
|
|
111
|
+
md += `**Launch Date:** ${coin.genesis_date}\n`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (coin.categories?.length) {
|
|
115
|
+
md += `**Categories:** ${coin.categories.join(", ")}\n`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Links
|
|
119
|
+
const links: string[] = [];
|
|
120
|
+
if (coin.links?.homepage?.[0]) {
|
|
121
|
+
links.push(`[Website](${coin.links.homepage[0]})`);
|
|
122
|
+
}
|
|
123
|
+
if (coin.links?.blockchain_site?.[0]) {
|
|
124
|
+
links.push(`[Explorer](${coin.links.blockchain_site[0]})`);
|
|
125
|
+
}
|
|
126
|
+
if (coin.links?.repos_url?.github?.[0]) {
|
|
127
|
+
links.push(`[GitHub](${coin.links.repos_url.github[0]})`);
|
|
128
|
+
}
|
|
129
|
+
if (links.length) {
|
|
130
|
+
md += `**Links:** ${links.join(" · ")}\n`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Description
|
|
134
|
+
if (coin.description?.en) {
|
|
135
|
+
const desc = coin.description.en
|
|
136
|
+
.replace(/<[^>]+>/g, "") // Strip HTML
|
|
137
|
+
.replace(/\r\n/g, "\n")
|
|
138
|
+
.trim();
|
|
139
|
+
if (desc) {
|
|
140
|
+
md += `\n## About\n\n${desc}\n`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const output = finalizeOutput(md);
|
|
145
|
+
return {
|
|
146
|
+
url,
|
|
147
|
+
finalUrl: url,
|
|
148
|
+
contentType: "text/markdown",
|
|
149
|
+
method: "coingecko",
|
|
150
|
+
content: output.content,
|
|
151
|
+
fetchedAt,
|
|
152
|
+
truncated: output.truncated,
|
|
153
|
+
notes: ["Fetched via CoinGecko API"],
|
|
154
|
+
};
|
|
155
|
+
} catch {}
|
|
156
|
+
|
|
157
|
+
return null;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Format price with appropriate decimal places
|
|
162
|
+
*/
|
|
163
|
+
function formatPrice(price: number): string {
|
|
164
|
+
if (price >= 1000) return price.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
|
165
|
+
if (price >= 1) return price.toFixed(2);
|
|
166
|
+
if (price >= 0.01) return price.toFixed(4);
|
|
167
|
+
if (price >= 0.0001) return price.toFixed(6);
|
|
168
|
+
return price.toFixed(8);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Format large numbers with B/M/K suffixes
|
|
173
|
+
*/
|
|
174
|
+
function formatLargeNumber(n: number): string {
|
|
175
|
+
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)}B`;
|
|
176
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
|
|
177
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(2)}K`;
|
|
178
|
+
return n.toFixed(2);
|
|
179
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if content looks like HTML
|
|
6
|
+
*/
|
|
7
|
+
function looksLikeHtml(content: string): boolean {
|
|
8
|
+
const trimmed = content.trim().toLowerCase();
|
|
9
|
+
return (
|
|
10
|
+
trimmed.startsWith("<!doctype") ||
|
|
11
|
+
trimmed.startsWith("<html") ||
|
|
12
|
+
trimmed.startsWith("<head") ||
|
|
13
|
+
trimmed.startsWith("<body")
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Handle crates.io URLs via API
|
|
19
|
+
*/
|
|
20
|
+
export const handleCratesIo: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = new URL(url);
|
|
23
|
+
if (parsed.hostname !== "crates.io" && parsed.hostname !== "www.crates.io") return null;
|
|
24
|
+
|
|
25
|
+
// Extract crate name from /crates/name or /crates/name/version
|
|
26
|
+
const match = parsed.pathname.match(/^\/crates\/([^/]+)/);
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
|
|
29
|
+
const crateName = decodeURIComponent(match[1]);
|
|
30
|
+
const fetchedAt = new Date().toISOString();
|
|
31
|
+
|
|
32
|
+
// Fetch from crates.io API
|
|
33
|
+
const apiUrl = `https://crates.io/api/v1/crates/${crateName}`;
|
|
34
|
+
const result = await loadPage(apiUrl, {
|
|
35
|
+
timeout,
|
|
36
|
+
headers: { "User-Agent": "omp-web-fetch/1.0 (https://github.com/anthropics)" },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!result.ok) return null;
|
|
40
|
+
|
|
41
|
+
let data: {
|
|
42
|
+
crate: {
|
|
43
|
+
name: string;
|
|
44
|
+
description: string | null;
|
|
45
|
+
downloads: number;
|
|
46
|
+
recent_downloads: number;
|
|
47
|
+
max_version: string;
|
|
48
|
+
repository: string | null;
|
|
49
|
+
homepage: string | null;
|
|
50
|
+
documentation: string | null;
|
|
51
|
+
categories: string[];
|
|
52
|
+
keywords: string[];
|
|
53
|
+
created_at: string;
|
|
54
|
+
updated_at: string;
|
|
55
|
+
};
|
|
56
|
+
versions: Array<{
|
|
57
|
+
num: string;
|
|
58
|
+
downloads: number;
|
|
59
|
+
created_at: string;
|
|
60
|
+
license: string | null;
|
|
61
|
+
rust_version: string | null;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
data = JSON.parse(result.content);
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const crate = data.crate;
|
|
72
|
+
const latestVersion = data.versions?.[0];
|
|
73
|
+
|
|
74
|
+
// Format download counts
|
|
75
|
+
const formatDownloads = (n: number): string =>
|
|
76
|
+
n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1_000 ? `${(n / 1_000).toFixed(1)}K` : String(n);
|
|
77
|
+
|
|
78
|
+
let md = `# ${crate.name}\n\n`;
|
|
79
|
+
if (crate.description) md += `${crate.description}\n\n`;
|
|
80
|
+
|
|
81
|
+
md += `**Latest:** ${crate.max_version}`;
|
|
82
|
+
if (latestVersion?.license) md += ` · **License:** ${latestVersion.license}`;
|
|
83
|
+
if (latestVersion?.rust_version) md += ` · **MSRV:** ${latestVersion.rust_version}`;
|
|
84
|
+
md += "\n";
|
|
85
|
+
md += `**Downloads:** ${formatDownloads(crate.downloads)} total · ${formatDownloads(crate.recent_downloads)} recent\n\n`;
|
|
86
|
+
|
|
87
|
+
if (crate.repository) md += `**Repository:** ${crate.repository}\n`;
|
|
88
|
+
if (crate.homepage && crate.homepage !== crate.repository) md += `**Homepage:** ${crate.homepage}\n`;
|
|
89
|
+
if (crate.documentation) md += `**Docs:** ${crate.documentation}\n`;
|
|
90
|
+
if (crate.keywords?.length) md += `**Keywords:** ${crate.keywords.join(", ")}\n`;
|
|
91
|
+
if (crate.categories?.length) md += `**Categories:** ${crate.categories.join(", ")}\n`;
|
|
92
|
+
|
|
93
|
+
// Show recent versions
|
|
94
|
+
if (data.versions?.length > 0) {
|
|
95
|
+
md += `\n## Recent Versions\n\n`;
|
|
96
|
+
for (const ver of data.versions.slice(0, 5)) {
|
|
97
|
+
const date = ver.created_at.split("T")[0];
|
|
98
|
+
md += `- **${ver.num}** (${date}) - ${formatDownloads(ver.downloads)} downloads\n`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Try to fetch README from docs.rs or repository
|
|
103
|
+
const docsRsUrl = `https://docs.rs/crate/${crateName}/${crate.max_version}/source/README.md`;
|
|
104
|
+
const readmeResult = await loadPage(docsRsUrl, { timeout: Math.min(timeout, 5) });
|
|
105
|
+
if (readmeResult.ok && readmeResult.content.length > 100 && !looksLikeHtml(readmeResult.content)) {
|
|
106
|
+
md += `\n---\n\n## README\n\n${readmeResult.content}\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const output = finalizeOutput(md);
|
|
110
|
+
return {
|
|
111
|
+
url,
|
|
112
|
+
finalUrl: url,
|
|
113
|
+
contentType: "text/markdown",
|
|
114
|
+
method: "crates.io",
|
|
115
|
+
content: output.content,
|
|
116
|
+
fetchedAt,
|
|
117
|
+
truncated: output.truncated,
|
|
118
|
+
notes: ["Fetched via crates.io API"],
|
|
119
|
+
};
|
|
120
|
+
} catch {}
|
|
121
|
+
|
|
122
|
+
return null;
|
|
123
|
+
};
|