@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
|
@@ -4,20 +4,34 @@ import { type Static, Type } from "@sinclair/typebox";
|
|
|
4
4
|
/** Source of an agent definition */
|
|
5
5
|
export type AgentSource = "bundled" | "user" | "project";
|
|
6
6
|
|
|
7
|
+
function getEnv(name: string, defaultValue: number): number {
|
|
8
|
+
const value = process.env[name];
|
|
9
|
+
if (value === undefined) {
|
|
10
|
+
return defaultValue;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const number = Number.parseInt(value, 10);
|
|
14
|
+
if (!Number.isNaN(number) && number > 0) {
|
|
15
|
+
return number;
|
|
16
|
+
}
|
|
17
|
+
} catch {}
|
|
18
|
+
return defaultValue;
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
/** Maximum tasks per call */
|
|
8
|
-
export const MAX_PARALLEL_TASKS = 32;
|
|
22
|
+
export const MAX_PARALLEL_TASKS = getEnv("OMP_TASK_MAX_PARALLEL", 32);
|
|
9
23
|
|
|
10
24
|
/** Maximum concurrent workers */
|
|
11
|
-
export const MAX_CONCURRENCY = 16;
|
|
25
|
+
export const MAX_CONCURRENCY = getEnv("OMP_TASK_MAX_CONCURRENCY", 16);
|
|
12
26
|
|
|
13
27
|
/** Maximum output bytes per agent */
|
|
14
|
-
export const MAX_OUTPUT_BYTES = 500_000;
|
|
28
|
+
export const MAX_OUTPUT_BYTES = getEnv("OMP_TASK_MAX_OUTPUT_BYTES", 500_000);
|
|
15
29
|
|
|
16
30
|
/** Maximum output lines per agent */
|
|
17
|
-
export const MAX_OUTPUT_LINES = 5000;
|
|
31
|
+
export const MAX_OUTPUT_LINES = getEnv("OMP_TASK_MAX_OUTPUT_LINES", 5000);
|
|
18
32
|
|
|
19
33
|
/** Maximum agents to show in description */
|
|
20
|
-
export const MAX_AGENTS_IN_DESCRIPTION = 10;
|
|
34
|
+
export const MAX_AGENTS_IN_DESCRIPTION = getEnv("OMP_TASK_MAX_AGENTS_IN_DESCRIPTION", 10);
|
|
21
35
|
|
|
22
36
|
/** EventBus channel for raw subagent events */
|
|
23
37
|
export const TASK_SUBAGENT_EVENT_CHANNEL = "task:subagent:event";
|
|
@@ -24,7 +24,11 @@ import type { SubagentWorkerRequest, SubagentWorkerResponse, SubagentWorkerStart
|
|
|
24
24
|
type PostMessageFn = (message: SubagentWorkerResponse) => void;
|
|
25
25
|
|
|
26
26
|
const postMessageSafe: PostMessageFn = (message) => {
|
|
27
|
-
|
|
27
|
+
try {
|
|
28
|
+
(globalThis as typeof globalThis & { postMessage: PostMessageFn }).postMessage(message);
|
|
29
|
+
} catch {
|
|
30
|
+
// Parent may have terminated worker, nothing we can do
|
|
31
|
+
}
|
|
28
32
|
};
|
|
29
33
|
|
|
30
34
|
interface WorkerMessageEvent<T> {
|
|
@@ -51,7 +55,9 @@ const isAgentEvent = (event: AgentSessionEvent): event is AgentEvent => {
|
|
|
51
55
|
|
|
52
56
|
let running = false;
|
|
53
57
|
let abortRequested = false;
|
|
58
|
+
let doneSent = false;
|
|
54
59
|
let activeSession: { abort: () => Promise<void>; dispose: () => Promise<void> } | null = null;
|
|
60
|
+
let unsubscribe: (() => void) | null = null;
|
|
55
61
|
|
|
56
62
|
/**
|
|
57
63
|
* Resolve model string to Model object with optional thinking level.
|
|
@@ -145,6 +151,17 @@ async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
|
|
|
145
151
|
|
|
146
152
|
activeSession = session;
|
|
147
153
|
|
|
154
|
+
if (abortRequested) {
|
|
155
|
+
aborted = true;
|
|
156
|
+
exitCode = 1;
|
|
157
|
+
try {
|
|
158
|
+
await session.abort();
|
|
159
|
+
} catch {
|
|
160
|
+
// Ignore abort errors
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
148
165
|
// Initialize extensions (equivalent to CLI's extension initialization)
|
|
149
166
|
// Note: Does not support --extension CLI flag or extension CLI flags
|
|
150
167
|
const extensionRunner = session.extensionRunner;
|
|
@@ -174,7 +191,7 @@ async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
|
|
|
174
191
|
let completeCalled = false;
|
|
175
192
|
|
|
176
193
|
// Subscribe to events and forward to parent (equivalent to --mode json output)
|
|
177
|
-
session.subscribe((event: AgentSessionEvent) => {
|
|
194
|
+
unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
178
195
|
if (isAgentEvent(event)) {
|
|
179
196
|
postMessageSafe({ type: "event", event });
|
|
180
197
|
// Track when complete tool is called
|
|
@@ -222,24 +239,39 @@ Call complete now.`;
|
|
|
222
239
|
if (exitCode === 0) exitCode = 1;
|
|
223
240
|
}
|
|
224
241
|
|
|
225
|
-
|
|
242
|
+
if (unsubscribe) {
|
|
243
|
+
try {
|
|
244
|
+
unsubscribe();
|
|
245
|
+
} catch {
|
|
246
|
+
// Ignore unsubscribe errors
|
|
247
|
+
}
|
|
248
|
+
unsubscribe = null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Cleanup session with timeout to prevent hanging
|
|
226
252
|
if (activeSession) {
|
|
253
|
+
const session = activeSession;
|
|
254
|
+
activeSession = null;
|
|
227
255
|
try {
|
|
228
|
-
await
|
|
256
|
+
await Promise.race([session.dispose(), new Promise<void>((resolve) => setTimeout(resolve, 5000))]);
|
|
229
257
|
} catch {
|
|
230
258
|
// Ignore cleanup errors
|
|
231
259
|
}
|
|
232
|
-
activeSession = null;
|
|
233
260
|
}
|
|
234
261
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
262
|
+
running = false;
|
|
263
|
+
|
|
264
|
+
// Send completion message to parent (only once)
|
|
265
|
+
if (!doneSent) {
|
|
266
|
+
doneSent = true;
|
|
267
|
+
postMessageSafe({
|
|
268
|
+
type: "done",
|
|
269
|
+
exitCode,
|
|
270
|
+
durationMs: Date.now() - startTime,
|
|
271
|
+
error,
|
|
272
|
+
aborted,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
243
275
|
}
|
|
244
276
|
}
|
|
245
277
|
|
|
@@ -251,6 +283,64 @@ function handleAbort(): void {
|
|
|
251
283
|
}
|
|
252
284
|
}
|
|
253
285
|
|
|
286
|
+
// Global error handlers to ensure we always send a done message
|
|
287
|
+
// Using self instead of globalThis for proper worker scope typing
|
|
288
|
+
declare const self: {
|
|
289
|
+
addEventListener(type: "error", listener: (event: ErrorEvent) => void): void;
|
|
290
|
+
addEventListener(type: "unhandledrejection", listener: (event: { reason: unknown }) => void): void;
|
|
291
|
+
addEventListener(type: "messageerror", listener: (event: MessageEvent) => void): void;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
self.addEventListener("error", (event) => {
|
|
295
|
+
if (!running || doneSent) return;
|
|
296
|
+
doneSent = true;
|
|
297
|
+
abortRequested = true;
|
|
298
|
+
if (activeSession) {
|
|
299
|
+
void activeSession.abort();
|
|
300
|
+
}
|
|
301
|
+
postMessageSafe({
|
|
302
|
+
type: "done",
|
|
303
|
+
exitCode: 1,
|
|
304
|
+
durationMs: 0,
|
|
305
|
+
error: `Uncaught error: ${event.message || "Unknown error"}`,
|
|
306
|
+
aborted: false,
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
self.addEventListener("unhandledrejection", (event) => {
|
|
311
|
+
if (!running || doneSent) return;
|
|
312
|
+
doneSent = true;
|
|
313
|
+
abortRequested = true;
|
|
314
|
+
if (activeSession) {
|
|
315
|
+
void activeSession.abort();
|
|
316
|
+
}
|
|
317
|
+
const reason = event.reason;
|
|
318
|
+
const message = reason instanceof Error ? reason.stack || reason.message : String(reason);
|
|
319
|
+
postMessageSafe({
|
|
320
|
+
type: "done",
|
|
321
|
+
exitCode: 1,
|
|
322
|
+
durationMs: 0,
|
|
323
|
+
error: `Unhandled rejection: ${message}`,
|
|
324
|
+
aborted: false,
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
self.addEventListener("messageerror", () => {
|
|
329
|
+
if (doneSent) return;
|
|
330
|
+
doneSent = true;
|
|
331
|
+
abortRequested = true;
|
|
332
|
+
if (activeSession) {
|
|
333
|
+
void activeSession.abort();
|
|
334
|
+
}
|
|
335
|
+
postMessageSafe({
|
|
336
|
+
type: "done",
|
|
337
|
+
exitCode: 1,
|
|
338
|
+
durationMs: 0,
|
|
339
|
+
error: "Failed to deserialize parent message",
|
|
340
|
+
aborted: false,
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
254
344
|
// Message handler - receives start/abort commands from parent
|
|
255
345
|
globalThis.addEventListener("message", (event: WorkerMessageEvent<SubagentWorkerRequest>) => {
|
|
256
346
|
const message = event.data;
|
|
@@ -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,210 @@
|
|
|
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 (url: string, timeout: number): Promise<RenderResult | null> => {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = new URL(url);
|
|
59
|
+
if (parsed.hostname !== "artifacthub.io" && parsed.hostname !== "www.artifacthub.io") return null;
|
|
60
|
+
|
|
61
|
+
// Extract kind, repo, and package name from /packages/{kind}/{repo}/{name}
|
|
62
|
+
const match = parsed.pathname.match(/^\/packages\/([^/]+)\/([^/]+)\/([^/]+)/);
|
|
63
|
+
if (!match) return null;
|
|
64
|
+
|
|
65
|
+
const [, kind, repo, name] = match;
|
|
66
|
+
const fetchedAt = new Date().toISOString();
|
|
67
|
+
|
|
68
|
+
// Fetch from Artifact Hub API
|
|
69
|
+
const apiUrl = `https://artifacthub.io/api/v1/packages/${kind}/${repo}/${name}`;
|
|
70
|
+
const result = await loadPage(apiUrl, {
|
|
71
|
+
timeout,
|
|
72
|
+
headers: { Accept: "application/json" },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!result.ok) return null;
|
|
76
|
+
|
|
77
|
+
let pkg: ArtifactHubPackage;
|
|
78
|
+
try {
|
|
79
|
+
pkg = JSON.parse(result.content);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const displayName = pkg.display_name || pkg.name;
|
|
85
|
+
const kindLabel = formatKindLabel(kind);
|
|
86
|
+
|
|
87
|
+
let md = `# ${displayName}\n\n`;
|
|
88
|
+
if (pkg.description) md += `${pkg.description}\n\n`;
|
|
89
|
+
|
|
90
|
+
// Basic info line
|
|
91
|
+
md += `**Type:** ${kindLabel}`;
|
|
92
|
+
md += ` · **Version:** ${pkg.version}`;
|
|
93
|
+
if (pkg.app_version) md += ` · **App Version:** ${pkg.app_version}`;
|
|
94
|
+
if (pkg.license) md += ` · **License:** ${pkg.license}`;
|
|
95
|
+
md += "\n";
|
|
96
|
+
|
|
97
|
+
// Stats and badges
|
|
98
|
+
const badges: string[] = [];
|
|
99
|
+
if (pkg.official) badges.push("Official");
|
|
100
|
+
if (pkg.signed) badges.push("Signed");
|
|
101
|
+
if (pkg.stars) badges.push(`${formatCount(pkg.stars)} stars`);
|
|
102
|
+
if (badges.length > 0) md += `**${badges.join(" · ")}**\n`;
|
|
103
|
+
md += "\n";
|
|
104
|
+
|
|
105
|
+
// Repository info
|
|
106
|
+
const repoDisplay =
|
|
107
|
+
pkg.repository.organization_display_name || pkg.repository.display_name || pkg.repository.name;
|
|
108
|
+
md += `**Repository:** ${repoDisplay}`;
|
|
109
|
+
if (pkg.repository.url) md += ` ([${pkg.repository.url}](${pkg.repository.url}))`;
|
|
110
|
+
md += "\n";
|
|
111
|
+
|
|
112
|
+
if (pkg.home_url) md += `**Homepage:** ${pkg.home_url}\n`;
|
|
113
|
+
if (pkg.keywords?.length) md += `**Keywords:** ${pkg.keywords.join(", ")}\n`;
|
|
114
|
+
|
|
115
|
+
// Maintainers
|
|
116
|
+
if (pkg.maintainers?.length) {
|
|
117
|
+
const maintainerNames = pkg.maintainers.map((m) => m.name).join(", ");
|
|
118
|
+
md += `**Maintainers:** ${maintainerNames}\n`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Security report summary
|
|
122
|
+
if (pkg.security_report_summary) {
|
|
123
|
+
const sec = pkg.security_report_summary;
|
|
124
|
+
const parts: string[] = [];
|
|
125
|
+
if (sec.critical) parts.push(`${sec.critical} critical`);
|
|
126
|
+
if (sec.high) parts.push(`${sec.high} high`);
|
|
127
|
+
if (sec.medium) parts.push(`${sec.medium} medium`);
|
|
128
|
+
if (sec.low) parts.push(`${sec.low} low`);
|
|
129
|
+
if (parts.length > 0) {
|
|
130
|
+
md += `**Security:** ${parts.join(", ")}\n`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Links
|
|
135
|
+
if (pkg.links?.length) {
|
|
136
|
+
md += `\n## Links\n\n`;
|
|
137
|
+
for (const link of pkg.links) {
|
|
138
|
+
md += `- [${link.name}](${link.url})\n`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Install instructions
|
|
143
|
+
if (pkg.install) {
|
|
144
|
+
md += `\n## Installation\n\n\`\`\`bash\n${pkg.install.trim()}\n\`\`\`\n`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Recent versions
|
|
148
|
+
if (pkg.available_versions?.length) {
|
|
149
|
+
md += `\n## Recent Versions\n\n`;
|
|
150
|
+
for (const ver of pkg.available_versions.slice(0, 5)) {
|
|
151
|
+
const date = new Date(ver.ts * 1000).toISOString().split("T")[0];
|
|
152
|
+
md += `- **${ver.version}** (${date})\n`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// README
|
|
157
|
+
if (pkg.readme) {
|
|
158
|
+
md += `\n---\n\n## README\n\n${pkg.readme}\n`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const output = finalizeOutput(md);
|
|
162
|
+
return {
|
|
163
|
+
url,
|
|
164
|
+
finalUrl: url,
|
|
165
|
+
contentType: "text/markdown",
|
|
166
|
+
method: "artifacthub",
|
|
167
|
+
content: output.content,
|
|
168
|
+
fetchedAt,
|
|
169
|
+
truncated: output.truncated,
|
|
170
|
+
notes: [`Fetched via Artifact Hub API (${kindLabel})`],
|
|
171
|
+
};
|
|
172
|
+
} catch {}
|
|
173
|
+
|
|
174
|
+
return null;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Convert kind slug to display label
|
|
179
|
+
*/
|
|
180
|
+
function formatKindLabel(kind: string): string {
|
|
181
|
+
const labels: Record<string, string> = {
|
|
182
|
+
helm: "Helm Chart",
|
|
183
|
+
"helm-plugin": "Helm Plugin",
|
|
184
|
+
falco: "Falco Rules",
|
|
185
|
+
opa: "OPA Policy",
|
|
186
|
+
olm: "OLM Operator",
|
|
187
|
+
tbaction: "Tinkerbell Action",
|
|
188
|
+
krew: "Krew Plugin",
|
|
189
|
+
tekton: "Tekton Task",
|
|
190
|
+
"tekton-pipeline": "Tekton Pipeline",
|
|
191
|
+
keda: "KEDA Scaler",
|
|
192
|
+
coredns: "CoreDNS Plugin",
|
|
193
|
+
keptn: "Keptn Integration",
|
|
194
|
+
container: "Container Image",
|
|
195
|
+
kubewarden: "Kubewarden Policy",
|
|
196
|
+
gatekeeper: "Gatekeeper Policy",
|
|
197
|
+
kyverno: "Kyverno Policy",
|
|
198
|
+
"knative-client": "Knative Client Plugin",
|
|
199
|
+
backstage: "Backstage Plugin",
|
|
200
|
+
argo: "Argo Template",
|
|
201
|
+
kubearmor: "KubeArmor Policy",
|
|
202
|
+
kcl: "KCL Module",
|
|
203
|
+
headlamp: "Headlamp Plugin",
|
|
204
|
+
inspektor: "Inspektor Gadget",
|
|
205
|
+
"meshery-design": "Meshery Design",
|
|
206
|
+
"opencost-plugin": "OpenCost Plugin",
|
|
207
|
+
radius: "Radius Recipe",
|
|
208
|
+
};
|
|
209
|
+
return labels[kind] || kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
210
|
+
}
|