@oh-my-pi/pi-coding-agent 3.25.0 → 3.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +369 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/complete.ts +2 -4
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +6 -8
- package/src/core/tools/jtd-to-json-schema.ts +174 -196
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +58 -9
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +55 -32
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +152 -76
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/executor.ts +204 -67
- package/src/core/tools/task/index.ts +129 -92
- package/src/core/tools/task/name-generator.ts +1544 -214
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +34 -11
- package/src/core/tools/task/worker.ts +152 -27
- package/src/core/tools/web-fetch.ts +220 -1657
- package/src/core/tools/web-scrapers/academic.test.ts +239 -0
- package/src/core/tools/web-scrapers/artifacthub.ts +215 -0
- package/src/core/tools/web-scrapers/arxiv.ts +88 -0
- package/src/core/tools/web-scrapers/aur.ts +175 -0
- package/src/core/tools/web-scrapers/biorxiv.ts +141 -0
- package/src/core/tools/web-scrapers/bluesky.ts +284 -0
- package/src/core/tools/web-scrapers/brew.ts +177 -0
- package/src/core/tools/web-scrapers/business.test.ts +82 -0
- package/src/core/tools/web-scrapers/cheatsh.ts +78 -0
- package/src/core/tools/web-scrapers/chocolatey.ts +158 -0
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/web-scrapers/coingecko.ts +184 -0
- package/src/core/tools/web-scrapers/crates-io.ts +128 -0
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/web-scrapers/dev-platforms.test.ts +254 -0
- package/src/core/tools/web-scrapers/devto.ts +177 -0
- package/src/core/tools/web-scrapers/discogs.ts +308 -0
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/web-scrapers/dockerhub.ts +160 -0
- package/src/core/tools/web-scrapers/documentation.test.ts +85 -0
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/finance-media.test.ts +144 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/web-scrapers/git-hosting.test.ts +272 -0
- package/src/core/tools/web-scrapers/github-gist.ts +68 -0
- package/src/core/tools/web-scrapers/github.ts +455 -0
- package/src/core/tools/web-scrapers/gitlab.ts +456 -0
- package/src/core/tools/web-scrapers/go-pkg.ts +275 -0
- package/src/core/tools/web-scrapers/hackage.ts +94 -0
- package/src/core/tools/web-scrapers/hackernews.ts +208 -0
- package/src/core/tools/web-scrapers/hex.ts +121 -0
- package/src/core/tools/web-scrapers/huggingface.ts +385 -0
- package/src/core/tools/web-scrapers/iacr.ts +86 -0
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/web-scrapers/lobsters.ts +186 -0
- package/src/core/tools/web-scrapers/mastodon.ts +310 -0
- package/src/core/tools/web-scrapers/maven.ts +152 -0
- package/src/core/tools/web-scrapers/mdn.ts +174 -0
- package/src/core/tools/web-scrapers/media.test.ts +138 -0
- package/src/core/tools/web-scrapers/metacpan.ts +253 -0
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/web-scrapers/npm.ts +114 -0
- package/src/core/tools/web-scrapers/nuget.ts +205 -0
- package/src/core/tools/web-scrapers/nvd.ts +243 -0
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/web-scrapers/opencorporates.ts +275 -0
- package/src/core/tools/web-scrapers/openlibrary.ts +319 -0
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/web-scrapers/osv.ts +189 -0
- package/src/core/tools/web-scrapers/package-managers-2.test.ts +199 -0
- package/src/core/tools/web-scrapers/package-managers.test.ts +171 -0
- package/src/core/tools/web-scrapers/package-registries.test.ts +259 -0
- package/src/core/tools/web-scrapers/packagist.ts +174 -0
- package/src/core/tools/web-scrapers/pub-dev.ts +185 -0
- package/src/core/tools/web-scrapers/pubmed.ts +178 -0
- package/src/core/tools/web-scrapers/pypi.ts +129 -0
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/web-scrapers/readthedocs.ts +126 -0
- package/src/core/tools/web-scrapers/reddit.ts +104 -0
- package/src/core/tools/web-scrapers/repology.ts +262 -0
- package/src/core/tools/web-scrapers/research.test.ts +107 -0
- package/src/core/tools/web-scrapers/rfc.ts +209 -0
- package/src/core/tools/web-scrapers/rubygems.ts +117 -0
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/web-scrapers/sec-edgar.ts +274 -0
- package/src/core/tools/web-scrapers/security.test.ts +103 -0
- package/src/core/tools/web-scrapers/semantic-scholar.ts +190 -0
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/social-extended.test.ts +192 -0
- package/src/core/tools/web-scrapers/social.test.ts +259 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/web-scrapers/spotify.ts +218 -0
- package/src/core/tools/web-scrapers/stackexchange.test.ts +120 -0
- package/src/core/tools/web-scrapers/stackoverflow.ts +124 -0
- package/src/core/tools/web-scrapers/standards.test.ts +122 -0
- package/src/core/tools/web-scrapers/terraform.ts +304 -0
- package/src/core/tools/web-scrapers/tldr.ts +51 -0
- package/src/core/tools/web-scrapers/twitter.ts +96 -0
- package/src/core/tools/web-scrapers/types.ts +234 -0
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/web-scrapers/vimeo.ts +152 -0
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/web-scrapers/wikidata.ts +357 -0
- package/src/core/tools/web-scrapers/wikipedia.test.ts +73 -0
- package/src/core/tools/web-scrapers/wikipedia.ts +95 -0
- package/src/core/tools/web-scrapers/youtube.test.ts +198 -0
- package/src/core/tools/web-scrapers/youtube.ts +371 -0
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -38
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/src/utils/tools-manager.ts +110 -8
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { handleArxiv } from "./arxiv";
|
|
3
|
+
import { handleIacr } from "./iacr";
|
|
4
|
+
import { handlePubMed } from "./pubmed";
|
|
5
|
+
import { handleSemanticScholar } from "./semantic-scholar";
|
|
6
|
+
|
|
7
|
+
const SKIP = !process.env.WEB_FETCH_INTEGRATION;
|
|
8
|
+
|
|
9
|
+
describe.skipIf(SKIP)("handleSemanticScholar", () => {
|
|
10
|
+
it("returns null for non-S2 URLs", async () => {
|
|
11
|
+
const result = await handleSemanticScholar("https://example.com", 10);
|
|
12
|
+
expect(result).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("fetches a known paper", async () => {
|
|
16
|
+
// "Attention Is All You Need" paper
|
|
17
|
+
const result = await handleSemanticScholar(
|
|
18
|
+
"https://www.semanticscholar.org/paper/Attention-is-All-you-Need-Vaswani-Shazeer/204e3073870fae3d05bcbc2f6a8e263d9b72e776",
|
|
19
|
+
20,
|
|
20
|
+
);
|
|
21
|
+
expect(result).not.toBeNull();
|
|
22
|
+
expect(result?.method).toBe("semantic-scholar");
|
|
23
|
+
// API may be rate-limited or fail, verify handler structure
|
|
24
|
+
if (
|
|
25
|
+
!result?.content.includes("Too Many Requests") &&
|
|
26
|
+
!result?.content.includes("Failed to fetch") &&
|
|
27
|
+
!result?.content.includes("Failed to parse")
|
|
28
|
+
) {
|
|
29
|
+
expect(result?.content).toContain("Attention");
|
|
30
|
+
expect(result?.contentType).toBe("text/markdown");
|
|
31
|
+
}
|
|
32
|
+
expect(result?.truncated).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("handles invalid paper ID format", async () => {
|
|
36
|
+
const result = await handleSemanticScholar("https://www.semanticscholar.org/paper/invalid", 20);
|
|
37
|
+
expect(result).not.toBeNull();
|
|
38
|
+
expect(result?.method).toBe("semantic-scholar");
|
|
39
|
+
expect(result?.content).toContain("Failed to extract paper ID");
|
|
40
|
+
expect(result?.notes).toContain("Invalid URL format");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("extracts paper ID from various URL formats", async () => {
|
|
44
|
+
const paperId = "204e3073870fae3d05bcbc2f6a8e263d9b72e776";
|
|
45
|
+
const urls = [
|
|
46
|
+
`https://www.semanticscholar.org/paper/Attention-is-All-you-Need-Vaswani-Shazeer/${paperId}`,
|
|
47
|
+
`https://www.semanticscholar.org/paper/${paperId}`,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
for (const url of urls) {
|
|
51
|
+
const result = await handleSemanticScholar(url, 20);
|
|
52
|
+
expect(result).not.toBeNull();
|
|
53
|
+
expect(result?.method).toBe("semantic-scholar");
|
|
54
|
+
// API may be rate-limited or fail
|
|
55
|
+
if (
|
|
56
|
+
!result?.content.includes("Too Many Requests") &&
|
|
57
|
+
!result?.content.includes("Failed to fetch") &&
|
|
58
|
+
!result?.content.includes("Failed to parse")
|
|
59
|
+
) {
|
|
60
|
+
expect(result?.content).toContain("Attention");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("includes metadata in formatted output", async () => {
|
|
66
|
+
const result = await handleSemanticScholar(
|
|
67
|
+
"https://www.semanticscholar.org/paper/Attention-is-All-you-Need-Vaswani-Shazeer/204e3073870fae3d05bcbc2f6a8e263d9b72e776",
|
|
68
|
+
20,
|
|
69
|
+
);
|
|
70
|
+
expect(result).not.toBeNull();
|
|
71
|
+
// Only check metadata if API call succeeded (not rate-limited)
|
|
72
|
+
if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
|
|
73
|
+
expect(result?.content).toMatch(/Year:/);
|
|
74
|
+
expect(result?.content).toMatch(/Citations:/);
|
|
75
|
+
expect(result?.content).toMatch(/Authors:/);
|
|
76
|
+
expect(result?.content).toContain("Vaswani");
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe.skipIf(SKIP)("handlePubMed", () => {
|
|
82
|
+
it("returns null for non-PubMed URLs", async () => {
|
|
83
|
+
const result = await handlePubMed("https://example.com", 10);
|
|
84
|
+
expect(result).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("fetches a known article from pubmed.ncbi.nlm.nih.gov", async () => {
|
|
88
|
+
// PMID 33782455 - COVID-19 vaccine paper
|
|
89
|
+
const result = await handlePubMed("https://pubmed.ncbi.nlm.nih.gov/33782455/", 20);
|
|
90
|
+
expect(result).not.toBeNull();
|
|
91
|
+
expect(result?.method).toBe("pubmed");
|
|
92
|
+
expect(result?.contentType).toBe("text/markdown");
|
|
93
|
+
expect(result?.truncated).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("fetches from ncbi.nlm.nih.gov/pubmed format", async () => {
|
|
97
|
+
const result = await handlePubMed("https://ncbi.nlm.nih.gov/pubmed/33782455", 20);
|
|
98
|
+
expect(result).not.toBeNull();
|
|
99
|
+
expect(result?.method).toBe("pubmed");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("includes PMID in output", async () => {
|
|
103
|
+
const result = await handlePubMed("https://pubmed.ncbi.nlm.nih.gov/33782455/", 20);
|
|
104
|
+
expect(result).not.toBeNull();
|
|
105
|
+
expect(result?.content).toContain("PMID:");
|
|
106
|
+
expect(result?.content).toContain("33782455");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("includes abstract section", async () => {
|
|
110
|
+
const result = await handlePubMed("https://pubmed.ncbi.nlm.nih.gov/33782455/", 20);
|
|
111
|
+
expect(result).not.toBeNull();
|
|
112
|
+
expect(result?.content).toContain("## Abstract");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("includes metadata fields", async () => {
|
|
116
|
+
const result = await handlePubMed("https://pubmed.ncbi.nlm.nih.gov/33782455/", 20);
|
|
117
|
+
expect(result).not.toBeNull();
|
|
118
|
+
expect(result?.content).toMatch(/Authors:/);
|
|
119
|
+
expect(result?.content).toMatch(/Journal:/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("returns null for invalid PMID format", async () => {
|
|
123
|
+
const result = await handlePubMed("https://pubmed.ncbi.nlm.nih.gov/invalid/", 20);
|
|
124
|
+
expect(result).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("handles non-existent PMID gracefully", async () => {
|
|
128
|
+
const result = await handlePubMed("https://pubmed.ncbi.nlm.nih.gov/99999999999/", 20);
|
|
129
|
+
// NCBI API returns a response even for non-existent PMIDs with minimal data
|
|
130
|
+
expect(result).not.toBeNull();
|
|
131
|
+
expect(result?.method).toBe("pubmed");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe.skipIf(SKIP)("handleArxiv", () => {
|
|
136
|
+
it("returns null for non-arXiv URLs", async () => {
|
|
137
|
+
const result = await handleArxiv("https://example.com", 10000);
|
|
138
|
+
expect(result).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("fetches a known paper", async () => {
|
|
142
|
+
// "Attention Is All You Need" paper
|
|
143
|
+
const result = await handleArxiv("https://arxiv.org/abs/1706.03762", 30000);
|
|
144
|
+
expect(result).not.toBeNull();
|
|
145
|
+
expect(result?.method).toBe("arxiv");
|
|
146
|
+
expect(result?.contentType).toBe("text/markdown");
|
|
147
|
+
// API may be rate-limited or fail
|
|
148
|
+
if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
|
|
149
|
+
expect(result?.content).toContain("Attention");
|
|
150
|
+
expect(result?.content).toContain("arXiv:");
|
|
151
|
+
expect(result?.content).toContain("1706.03762");
|
|
152
|
+
}
|
|
153
|
+
expect(result?.truncated).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("handles /pdf/ URLs", async () => {
|
|
157
|
+
const result = await handleArxiv("https://arxiv.org/pdf/1706.03762", 30000);
|
|
158
|
+
expect(result).not.toBeNull();
|
|
159
|
+
expect(result?.method).toBe("arxiv");
|
|
160
|
+
if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
|
|
161
|
+
expect(result?.content).toContain("Attention");
|
|
162
|
+
expect(result?.notes?.some((n) => n.includes("PDF"))).toBe(true);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("handles arxiv.org/abs/ format", async () => {
|
|
167
|
+
const result = await handleArxiv("https://arxiv.org/abs/1706.03762", 30000);
|
|
168
|
+
expect(result).not.toBeNull();
|
|
169
|
+
expect(result?.method).toBe("arxiv");
|
|
170
|
+
if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
|
|
171
|
+
expect(result?.content).toContain("1706.03762");
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("includes paper metadata", async () => {
|
|
176
|
+
const result = await handleArxiv("https://arxiv.org/abs/1706.03762", 30000);
|
|
177
|
+
expect(result).not.toBeNull();
|
|
178
|
+
if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
|
|
179
|
+
expect(result?.content).toMatch(/Authors:/);
|
|
180
|
+
expect(result?.content).toContain("Vaswani");
|
|
181
|
+
expect(result?.content).toMatch(/Abstract/);
|
|
182
|
+
expect(result?.content).toMatch(/Published:/);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("handles rate limiting gracefully", async () => {
|
|
187
|
+
const result = await handleArxiv("https://arxiv.org/abs/1706.03762", 5000);
|
|
188
|
+
expect(result).not.toBeNull();
|
|
189
|
+
expect(result?.method).toBe("arxiv");
|
|
190
|
+
// Should return something, even if rate limited
|
|
191
|
+
expect(result?.content).toBeTruthy();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe.skipIf(SKIP)("handleIacr", () => {
|
|
196
|
+
it("returns null for non-IACR URLs", async () => {
|
|
197
|
+
const result = await handleIacr("https://example.com", 10000);
|
|
198
|
+
expect(result).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("fetches a known ePrint", async () => {
|
|
202
|
+
// Using a well-known paper
|
|
203
|
+
const result = await handleIacr("https://eprint.iacr.org/2023/123", 30000);
|
|
204
|
+
expect(result).not.toBeNull();
|
|
205
|
+
expect(result?.method).toBe("iacr");
|
|
206
|
+
expect(result?.contentType).toBe("text/markdown");
|
|
207
|
+
if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
|
|
208
|
+
expect(result?.content).toContain("ePrint:");
|
|
209
|
+
expect(result?.content).toContain("2023/123");
|
|
210
|
+
}
|
|
211
|
+
expect(result?.truncated).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("includes paper metadata", async () => {
|
|
215
|
+
const result = await handleIacr("https://eprint.iacr.org/2023/123", 30000);
|
|
216
|
+
expect(result).not.toBeNull();
|
|
217
|
+
if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
|
|
218
|
+
expect(result?.content).toMatch(/Authors:/);
|
|
219
|
+
expect(result?.content).toMatch(/Abstract/);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("handles rate limiting gracefully", async () => {
|
|
224
|
+
const result = await handleIacr("https://eprint.iacr.org/2023/123", 5000);
|
|
225
|
+
expect(result).not.toBeNull();
|
|
226
|
+
expect(result?.method).toBe("iacr");
|
|
227
|
+
// Should return something, even if rate limited
|
|
228
|
+
expect(result?.content).toBeTruthy();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("handles PDF URLs", async () => {
|
|
232
|
+
const result = await handleIacr("https://eprint.iacr.org/2023/123.pdf", 30000);
|
|
233
|
+
expect(result).not.toBeNull();
|
|
234
|
+
expect(result?.method).toBe("iacr");
|
|
235
|
+
if (!result?.content.includes("Too Many Requests") && !result?.content.includes("Failed to fetch")) {
|
|
236
|
+
expect(result?.notes?.some((n) => n.includes("PDF"))).toBe(true);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface ArtifactHubMaintainer {
|
|
5
|
+
name: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ArtifactHubLink {
|
|
10
|
+
name: string;
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ArtifactHubRepository {
|
|
15
|
+
name: string;
|
|
16
|
+
display_name?: string;
|
|
17
|
+
url: string;
|
|
18
|
+
organization_name?: string;
|
|
19
|
+
organization_display_name?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ArtifactHubPackage {
|
|
23
|
+
package_id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
normalized_name: string;
|
|
26
|
+
display_name?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
version: string;
|
|
29
|
+
app_version?: string;
|
|
30
|
+
license?: string;
|
|
31
|
+
home_url?: string;
|
|
32
|
+
readme?: string;
|
|
33
|
+
install?: string;
|
|
34
|
+
keywords?: string[];
|
|
35
|
+
maintainers?: ArtifactHubMaintainer[];
|
|
36
|
+
links?: ArtifactHubLink[];
|
|
37
|
+
repository: ArtifactHubRepository;
|
|
38
|
+
ts: number;
|
|
39
|
+
created_at: number;
|
|
40
|
+
stars?: number;
|
|
41
|
+
official?: boolean;
|
|
42
|
+
signed?: boolean;
|
|
43
|
+
security_report_summary?: {
|
|
44
|
+
low?: number;
|
|
45
|
+
medium?: number;
|
|
46
|
+
high?: number;
|
|
47
|
+
critical?: number;
|
|
48
|
+
};
|
|
49
|
+
available_versions?: Array<{ version: string; ts: number }>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Handle Artifact Hub URLs via API
|
|
54
|
+
* Supports Helm charts, OLM operators, Falco rules, OPA policies, etc.
|
|
55
|
+
*/
|
|
56
|
+
export const handleArtifactHub: SpecialHandler = async (
|
|
57
|
+
url: string,
|
|
58
|
+
timeout: number,
|
|
59
|
+
signal?: AbortSignal,
|
|
60
|
+
): Promise<RenderResult | null> => {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = new URL(url);
|
|
63
|
+
if (parsed.hostname !== "artifacthub.io" && parsed.hostname !== "www.artifacthub.io") return null;
|
|
64
|
+
|
|
65
|
+
// Extract kind, repo, and package name from /packages/{kind}/{repo}/{name}
|
|
66
|
+
const match = parsed.pathname.match(/^\/packages\/([^/]+)\/([^/]+)\/([^/]+)/);
|
|
67
|
+
if (!match) return null;
|
|
68
|
+
|
|
69
|
+
const [, kind, repo, name] = match;
|
|
70
|
+
const fetchedAt = new Date().toISOString();
|
|
71
|
+
|
|
72
|
+
// Fetch from Artifact Hub API
|
|
73
|
+
const apiUrl = `https://artifacthub.io/api/v1/packages/${kind}/${repo}/${name}`;
|
|
74
|
+
const result = await loadPage(apiUrl, {
|
|
75
|
+
timeout,
|
|
76
|
+
headers: { Accept: "application/json" },
|
|
77
|
+
signal,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!result.ok) return null;
|
|
81
|
+
|
|
82
|
+
let pkg: ArtifactHubPackage;
|
|
83
|
+
try {
|
|
84
|
+
pkg = JSON.parse(result.content);
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const displayName = pkg.display_name || pkg.name;
|
|
90
|
+
const kindLabel = formatKindLabel(kind);
|
|
91
|
+
|
|
92
|
+
let md = `# ${displayName}\n\n`;
|
|
93
|
+
if (pkg.description) md += `${pkg.description}\n\n`;
|
|
94
|
+
|
|
95
|
+
// Basic info line
|
|
96
|
+
md += `**Type:** ${kindLabel}`;
|
|
97
|
+
md += ` · **Version:** ${pkg.version}`;
|
|
98
|
+
if (pkg.app_version) md += ` · **App Version:** ${pkg.app_version}`;
|
|
99
|
+
if (pkg.license) md += ` · **License:** ${pkg.license}`;
|
|
100
|
+
md += "\n";
|
|
101
|
+
|
|
102
|
+
// Stats and badges
|
|
103
|
+
const badges: string[] = [];
|
|
104
|
+
if (pkg.official) badges.push("Official");
|
|
105
|
+
if (pkg.signed) badges.push("Signed");
|
|
106
|
+
if (pkg.stars) badges.push(`${formatCount(pkg.stars)} stars`);
|
|
107
|
+
if (badges.length > 0) md += `**${badges.join(" · ")}**\n`;
|
|
108
|
+
md += "\n";
|
|
109
|
+
|
|
110
|
+
// Repository info
|
|
111
|
+
const repoDisplay =
|
|
112
|
+
pkg.repository.organization_display_name || pkg.repository.display_name || pkg.repository.name;
|
|
113
|
+
md += `**Repository:** ${repoDisplay}`;
|
|
114
|
+
if (pkg.repository.url) md += ` ([${pkg.repository.url}](${pkg.repository.url}))`;
|
|
115
|
+
md += "\n";
|
|
116
|
+
|
|
117
|
+
if (pkg.home_url) md += `**Homepage:** ${pkg.home_url}\n`;
|
|
118
|
+
if (pkg.keywords?.length) md += `**Keywords:** ${pkg.keywords.join(", ")}\n`;
|
|
119
|
+
|
|
120
|
+
// Maintainers
|
|
121
|
+
if (pkg.maintainers?.length) {
|
|
122
|
+
const maintainerNames = pkg.maintainers.map((m) => m.name).join(", ");
|
|
123
|
+
md += `**Maintainers:** ${maintainerNames}\n`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Security report summary
|
|
127
|
+
if (pkg.security_report_summary) {
|
|
128
|
+
const sec = pkg.security_report_summary;
|
|
129
|
+
const parts: string[] = [];
|
|
130
|
+
if (sec.critical) parts.push(`${sec.critical} critical`);
|
|
131
|
+
if (sec.high) parts.push(`${sec.high} high`);
|
|
132
|
+
if (sec.medium) parts.push(`${sec.medium} medium`);
|
|
133
|
+
if (sec.low) parts.push(`${sec.low} low`);
|
|
134
|
+
if (parts.length > 0) {
|
|
135
|
+
md += `**Security:** ${parts.join(", ")}\n`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Links
|
|
140
|
+
if (pkg.links?.length) {
|
|
141
|
+
md += `\n## Links\n\n`;
|
|
142
|
+
for (const link of pkg.links) {
|
|
143
|
+
md += `- [${link.name}](${link.url})\n`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Install instructions
|
|
148
|
+
if (pkg.install) {
|
|
149
|
+
md += `\n## Installation\n\n\`\`\`bash\n${pkg.install.trim()}\n\`\`\`\n`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Recent versions
|
|
153
|
+
if (pkg.available_versions?.length) {
|
|
154
|
+
md += `\n## Recent Versions\n\n`;
|
|
155
|
+
for (const ver of pkg.available_versions.slice(0, 5)) {
|
|
156
|
+
const date = new Date(ver.ts * 1000).toISOString().split("T")[0];
|
|
157
|
+
md += `- **${ver.version}** (${date})\n`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// README
|
|
162
|
+
if (pkg.readme) {
|
|
163
|
+
md += `\n---\n\n## README\n\n${pkg.readme}\n`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const output = finalizeOutput(md);
|
|
167
|
+
return {
|
|
168
|
+
url,
|
|
169
|
+
finalUrl: url,
|
|
170
|
+
contentType: "text/markdown",
|
|
171
|
+
method: "artifacthub",
|
|
172
|
+
content: output.content,
|
|
173
|
+
fetchedAt,
|
|
174
|
+
truncated: output.truncated,
|
|
175
|
+
notes: [`Fetched via Artifact Hub API (${kindLabel})`],
|
|
176
|
+
};
|
|
177
|
+
} catch {}
|
|
178
|
+
|
|
179
|
+
return null;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Convert kind slug to display label
|
|
184
|
+
*/
|
|
185
|
+
function formatKindLabel(kind: string): string {
|
|
186
|
+
const labels: Record<string, string> = {
|
|
187
|
+
helm: "Helm Chart",
|
|
188
|
+
"helm-plugin": "Helm Plugin",
|
|
189
|
+
falco: "Falco Rules",
|
|
190
|
+
opa: "OPA Policy",
|
|
191
|
+
olm: "OLM Operator",
|
|
192
|
+
tbaction: "Tinkerbell Action",
|
|
193
|
+
krew: "Krew Plugin",
|
|
194
|
+
tekton: "Tekton Task",
|
|
195
|
+
"tekton-pipeline": "Tekton Pipeline",
|
|
196
|
+
keda: "KEDA Scaler",
|
|
197
|
+
coredns: "CoreDNS Plugin",
|
|
198
|
+
keptn: "Keptn Integration",
|
|
199
|
+
container: "Container Image",
|
|
200
|
+
kubewarden: "Kubewarden Policy",
|
|
201
|
+
gatekeeper: "Gatekeeper Policy",
|
|
202
|
+
kyverno: "Kyverno Policy",
|
|
203
|
+
"knative-client": "Knative Client Plugin",
|
|
204
|
+
backstage: "Backstage Plugin",
|
|
205
|
+
argo: "Argo Template",
|
|
206
|
+
kubearmor: "KubeArmor Policy",
|
|
207
|
+
kcl: "KCL Module",
|
|
208
|
+
headlamp: "Headlamp Plugin",
|
|
209
|
+
inspektor: "Inspektor Gadget",
|
|
210
|
+
"meshery-design": "Meshery Design",
|
|
211
|
+
"opencost-plugin": "OpenCost Plugin",
|
|
212
|
+
radius: "Radius Recipe",
|
|
213
|
+
};
|
|
214
|
+
return labels[kind] || kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
215
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
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 (
|
|
10
|
+
url: string,
|
|
11
|
+
timeout: number,
|
|
12
|
+
signal?: AbortSignal,
|
|
13
|
+
): Promise<RenderResult | null> => {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = new URL(url);
|
|
16
|
+
if (parsed.hostname !== "arxiv.org") return null;
|
|
17
|
+
|
|
18
|
+
// Extract paper ID from various URL formats
|
|
19
|
+
// /abs/1234.56789, /pdf/1234.56789, /abs/cs/0123456
|
|
20
|
+
const match = parsed.pathname.match(/\/(abs|pdf)\/(.+?)(?:\.pdf)?$/);
|
|
21
|
+
if (!match) return null;
|
|
22
|
+
|
|
23
|
+
const paperId = match[2];
|
|
24
|
+
const fetchedAt = new Date().toISOString();
|
|
25
|
+
const notes: string[] = [];
|
|
26
|
+
|
|
27
|
+
// Fetch metadata via arXiv API
|
|
28
|
+
const apiUrl = `https://export.arxiv.org/api/query?id_list=${paperId}`;
|
|
29
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
30
|
+
|
|
31
|
+
if (!result.ok) return null;
|
|
32
|
+
|
|
33
|
+
// Parse the Atom feed response
|
|
34
|
+
const doc = parseHtml(result.content, { parseNoneClosedTags: true });
|
|
35
|
+
const entry = doc.querySelector("entry");
|
|
36
|
+
|
|
37
|
+
if (!entry) return null;
|
|
38
|
+
|
|
39
|
+
const title = entry.querySelector("title")?.text?.trim()?.replace(/\s+/g, " ");
|
|
40
|
+
const summary = entry.querySelector("summary")?.text?.trim();
|
|
41
|
+
const authors = entry
|
|
42
|
+
.querySelectorAll("author name")
|
|
43
|
+
.map((n) => n.text?.trim())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
const published = entry.querySelector("published")?.text?.trim()?.split("T")[0];
|
|
46
|
+
const categories = entry
|
|
47
|
+
.querySelectorAll("category")
|
|
48
|
+
.map((c) => c.getAttribute("term"))
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
const pdfLink = entry.querySelector('link[title="pdf"]')?.getAttribute("href");
|
|
51
|
+
|
|
52
|
+
let md = `# ${title || "arXiv Paper"}\n\n`;
|
|
53
|
+
if (authors.length) md += `**Authors:** ${authors.join(", ")}\n`;
|
|
54
|
+
if (published) md += `**Published:** ${published}\n`;
|
|
55
|
+
if (categories.length) md += `**Categories:** ${categories.join(", ")}\n`;
|
|
56
|
+
md += `**arXiv:** ${paperId}\n\n`;
|
|
57
|
+
md += `---\n\n## Abstract\n\n${summary || "No abstract available."}\n\n`;
|
|
58
|
+
|
|
59
|
+
// If it was a PDF link or we want full content, try to fetch and convert PDF
|
|
60
|
+
if (match[1] === "pdf" || parsed.pathname.includes(".pdf")) {
|
|
61
|
+
if (pdfLink) {
|
|
62
|
+
notes.push("Fetching PDF for full content...");
|
|
63
|
+
const pdfResult = await fetchBinary(pdfLink, timeout, signal);
|
|
64
|
+
if (pdfResult.ok) {
|
|
65
|
+
const converted = await convertWithMarkitdown(pdfResult.buffer, ".pdf", timeout, signal);
|
|
66
|
+
if (converted.ok && converted.content.length > 500) {
|
|
67
|
+
md += `---\n\n## Full Paper\n\n${converted.content}\n`;
|
|
68
|
+
notes.push("PDF converted via markitdown");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const output = finalizeOutput(md);
|
|
75
|
+
return {
|
|
76
|
+
url,
|
|
77
|
+
finalUrl: url,
|
|
78
|
+
contentType: "text/markdown",
|
|
79
|
+
method: "arxiv",
|
|
80
|
+
content: output.content,
|
|
81
|
+
fetchedAt,
|
|
82
|
+
truncated: output.truncated,
|
|
83
|
+
notes: notes.length ? notes : ["Fetched via arXiv API"],
|
|
84
|
+
};
|
|
85
|
+
} catch {}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
};
|