@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,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
|
+
});
|
|
@@ -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
|
+
}
|