@oh-my-pi/pi-coding-agent 3.30.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 +71 -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/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/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 +150 -74
- 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 +94 -83
- package/src/core/tools/task/index.ts +129 -92
- 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 +15 -6
- package/src/core/tools/task/worker.ts +124 -89
- package/src/core/tools/web-fetch.ts +112 -377
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
- 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-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
- package/src/core/tools/web-scrapers/fdroid.ts +158 -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-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
- 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-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
- 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-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -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-fetch-handlers → web-scrapers}/spotify.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
- 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-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
- 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/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/web-fetch-handlers/index.ts +0 -69
- package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
|
@@ -13,9 +13,64 @@ export interface RenderResult {
|
|
|
13
13
|
notes: string[];
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export type SpecialHandler = (url: string, timeout: number) => Promise<RenderResult | null>;
|
|
16
|
+
export type SpecialHandler = (url: string, timeout: number, signal?: AbortSignal) => Promise<RenderResult | null>;
|
|
17
17
|
|
|
18
18
|
export const MAX_OUTPUT_CHARS = 500_000;
|
|
19
|
+
const MAX_BYTES = 50 * 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
const USER_AGENTS = [
|
|
22
|
+
"curl/8.0",
|
|
23
|
+
"Mozilla/5.0 (compatible; TextBot/1.0)",
|
|
24
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export interface RequestSignal {
|
|
28
|
+
signal: AbortSignal;
|
|
29
|
+
cleanup: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createRequestSignal(timeoutMs: number, signal?: AbortSignal): RequestSignal {
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined = setTimeout(() => controller.abort(), timeoutMs);
|
|
35
|
+
const abortHandler = () => controller.abort();
|
|
36
|
+
|
|
37
|
+
if (signal) {
|
|
38
|
+
if (signal.aborted) {
|
|
39
|
+
clearTimeout(timeoutId);
|
|
40
|
+
timeoutId = undefined;
|
|
41
|
+
controller.abort();
|
|
42
|
+
} else {
|
|
43
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const cleanup = () => {
|
|
48
|
+
if (timeoutId !== undefined) {
|
|
49
|
+
clearTimeout(timeoutId);
|
|
50
|
+
timeoutId = undefined;
|
|
51
|
+
}
|
|
52
|
+
if (signal) {
|
|
53
|
+
signal.removeEventListener("abort", abortHandler);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return { signal: controller.signal, cleanup };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isBotBlocked(status: number, content: string): boolean {
|
|
61
|
+
if (status === 403 || status === 503) {
|
|
62
|
+
const lower = content.toLowerCase();
|
|
63
|
+
return (
|
|
64
|
+
lower.includes("cloudflare") ||
|
|
65
|
+
lower.includes("captcha") ||
|
|
66
|
+
lower.includes("challenge") ||
|
|
67
|
+
lower.includes("blocked") ||
|
|
68
|
+
lower.includes("access denied") ||
|
|
69
|
+
lower.includes("bot detection")
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
19
74
|
|
|
20
75
|
/**
|
|
21
76
|
* Truncate and cleanup output
|
|
@@ -29,30 +84,41 @@ export function finalizeOutput(content: string): { content: string; truncated: b
|
|
|
29
84
|
};
|
|
30
85
|
}
|
|
31
86
|
|
|
87
|
+
export interface LoadPageOptions {
|
|
88
|
+
timeout?: number;
|
|
89
|
+
headers?: Record<string, string>;
|
|
90
|
+
method?: string;
|
|
91
|
+
body?: string;
|
|
92
|
+
maxBytes?: number;
|
|
93
|
+
signal?: AbortSignal;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface LoadPageResult {
|
|
97
|
+
content: string;
|
|
98
|
+
contentType: string;
|
|
99
|
+
finalUrl: string;
|
|
100
|
+
ok: boolean;
|
|
101
|
+
status?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
32
104
|
/**
|
|
33
105
|
* Fetch a page with timeout and size limit
|
|
34
106
|
*/
|
|
35
|
-
export async function loadPage(
|
|
36
|
-
|
|
37
|
-
options: { timeout?: number; headers?: Record<string, string>; maxBytes?: number } = {},
|
|
38
|
-
): Promise<{ content: string; contentType: string; finalUrl: string; ok: boolean; status?: number }> {
|
|
39
|
-
const { timeout = 20, headers = {}, maxBytes = 50 * 1024 * 1024 } = options;
|
|
107
|
+
export async function loadPage(url: string, options: LoadPageOptions = {}): Promise<LoadPageResult> {
|
|
108
|
+
const { timeout = 20, headers = {}, maxBytes = MAX_BYTES, signal, method = "GET", body } = options;
|
|
40
109
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
];
|
|
110
|
+
for (let attempt = 0; attempt < USER_AGENTS.length; attempt++) {
|
|
111
|
+
if (signal?.aborted) {
|
|
112
|
+
return { content: "", contentType: "", finalUrl: url, ok: false };
|
|
113
|
+
}
|
|
46
114
|
|
|
47
|
-
|
|
48
|
-
const
|
|
115
|
+
const userAgent = USER_AGENTS[attempt];
|
|
116
|
+
const { signal: requestSignal, cleanup } = createRequestSignal(timeout * 1000, signal);
|
|
49
117
|
|
|
50
118
|
try {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const response = await fetch(url, {
|
|
55
|
-
signal: controller.signal,
|
|
119
|
+
const requestInit: RequestInit = {
|
|
120
|
+
signal: requestSignal,
|
|
121
|
+
method,
|
|
56
122
|
headers: {
|
|
57
123
|
"User-Agent": userAgent,
|
|
58
124
|
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
@@ -60,9 +126,13 @@ export async function loadPage(
|
|
|
60
126
|
...headers,
|
|
61
127
|
},
|
|
62
128
|
redirect: "follow",
|
|
63
|
-
}
|
|
129
|
+
};
|
|
64
130
|
|
|
65
|
-
|
|
131
|
+
if (body !== undefined) {
|
|
132
|
+
requestInit.body = body;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const response = await fetch(url, requestInit);
|
|
66
136
|
|
|
67
137
|
const contentType = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
68
138
|
const finalUrl = response.url;
|
|
@@ -91,12 +161,8 @@ export async function loadPage(
|
|
|
91
161
|
const decoder = new TextDecoder();
|
|
92
162
|
const content = decoder.decode(Buffer.concat(chunks));
|
|
93
163
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const lower = content.toLowerCase();
|
|
97
|
-
if (lower.includes("cloudflare") || lower.includes("captcha") || lower.includes("blocked")) {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
164
|
+
if (isBotBlocked(response.status, content) && attempt < USER_AGENTS.length - 1) {
|
|
165
|
+
continue;
|
|
100
166
|
}
|
|
101
167
|
|
|
102
168
|
if (!response.ok) {
|
|
@@ -105,9 +171,14 @@ export async function loadPage(
|
|
|
105
171
|
|
|
106
172
|
return { content, contentType, finalUrl, ok: true, status: response.status };
|
|
107
173
|
} catch (_err) {
|
|
108
|
-
if (
|
|
174
|
+
if (signal?.aborted) {
|
|
175
|
+
return { content: "", contentType: "", finalUrl: url, ok: false };
|
|
176
|
+
}
|
|
177
|
+
if (attempt === USER_AGENTS.length - 1) {
|
|
109
178
|
return { content: "", contentType: "", finalUrl: url, ok: false };
|
|
110
179
|
}
|
|
180
|
+
} finally {
|
|
181
|
+
cleanup();
|
|
111
182
|
}
|
|
112
183
|
}
|
|
113
184
|
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { nanoid } from "nanoid";
|
|
4
|
+
import { ensureTool } from "../../../utils/tools-manager";
|
|
5
|
+
import { createRequestSignal } from "./types";
|
|
6
|
+
|
|
7
|
+
const MAX_BYTES = 50 * 1024 * 1024; // 50MB for binary files
|
|
8
|
+
|
|
9
|
+
interface ExecResult {
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
ok: boolean;
|
|
13
|
+
exitCode: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type SpawnSyncOptions = NonNullable<Parameters<typeof Bun.spawnSync>[1]>;
|
|
17
|
+
|
|
18
|
+
function exec(cmd: string, args: string[], options?: { timeout?: number; input?: string | Buffer }): ExecResult {
|
|
19
|
+
const stdin = (options?.input ?? "ignore") as SpawnSyncOptions["stdin"];
|
|
20
|
+
const result = Bun.spawnSync([cmd, ...args], {
|
|
21
|
+
stdin,
|
|
22
|
+
stdout: "pipe",
|
|
23
|
+
stderr: "pipe",
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
stdout: result.stdout?.toString() ?? "",
|
|
27
|
+
stderr: result.stderr?.toString() ?? "",
|
|
28
|
+
ok: result.exitCode === 0,
|
|
29
|
+
exitCode: result.exitCode ?? -1,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ConvertResult {
|
|
34
|
+
content: string;
|
|
35
|
+
ok: boolean;
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BinaryFetchResult {
|
|
40
|
+
buffer: Buffer;
|
|
41
|
+
contentType: string;
|
|
42
|
+
contentDisposition?: string;
|
|
43
|
+
ok: boolean;
|
|
44
|
+
status?: number;
|
|
45
|
+
error?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function convertWithMarkitdown(
|
|
49
|
+
content: Buffer,
|
|
50
|
+
extensionHint: string,
|
|
51
|
+
timeout: number,
|
|
52
|
+
signal?: AbortSignal,
|
|
53
|
+
): Promise<ConvertResult> {
|
|
54
|
+
if (signal?.aborted) {
|
|
55
|
+
return { content: "", ok: false, error: "aborted" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const markitdown = await ensureTool("markitdown", true);
|
|
59
|
+
if (!markitdown) {
|
|
60
|
+
return { content: "", ok: false, error: "markitdown not available" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Write to temp file with extension hint
|
|
64
|
+
const ext = extensionHint || ".bin";
|
|
65
|
+
const tmpDir = tmpdir();
|
|
66
|
+
const tmpFile = path.join(tmpDir, `omp-convert-${nanoid()}${ext}`);
|
|
67
|
+
|
|
68
|
+
if (content.length > MAX_BYTES) {
|
|
69
|
+
return { content: "", ok: false, error: `content exceeds ${MAX_BYTES} bytes` };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await Bun.write(tmpFile, content);
|
|
74
|
+
const result = exec(markitdown, [tmpFile], { timeout });
|
|
75
|
+
if (!result.ok) {
|
|
76
|
+
const stderr = result.stderr.trim();
|
|
77
|
+
return {
|
|
78
|
+
content: result.stdout,
|
|
79
|
+
ok: false,
|
|
80
|
+
error: stderr.length > 0 ? stderr : `markitdown failed (exit ${result.exitCode})`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return { content: result.stdout, ok: true };
|
|
84
|
+
} finally {
|
|
85
|
+
try {
|
|
86
|
+
await Bun.$`rm ${tmpFile}`.quiet();
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function fetchBinary(url: string, timeout: number, signal?: AbortSignal): Promise<BinaryFetchResult> {
|
|
92
|
+
if (signal?.aborted) {
|
|
93
|
+
return { buffer: Buffer.alloc(0), contentType: "", ok: false, error: "aborted" };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { signal: requestSignal, cleanup } = createRequestSignal(timeout * 1000, signal);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetch(url, {
|
|
100
|
+
signal: requestSignal,
|
|
101
|
+
headers: {
|
|
102
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0.0.0",
|
|
103
|
+
},
|
|
104
|
+
redirect: "follow",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
108
|
+
const contentDisposition = response.headers.get("content-disposition") ?? undefined;
|
|
109
|
+
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
return {
|
|
112
|
+
buffer: Buffer.alloc(0),
|
|
113
|
+
contentType,
|
|
114
|
+
contentDisposition,
|
|
115
|
+
ok: false,
|
|
116
|
+
status: response.status,
|
|
117
|
+
error: `status ${response.status}`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const contentLength = response.headers.get("content-length");
|
|
122
|
+
if (contentLength) {
|
|
123
|
+
const size = Number.parseInt(contentLength, 10);
|
|
124
|
+
if (Number.isFinite(size) && size > MAX_BYTES) {
|
|
125
|
+
return {
|
|
126
|
+
buffer: Buffer.alloc(0),
|
|
127
|
+
contentType,
|
|
128
|
+
contentDisposition,
|
|
129
|
+
ok: false,
|
|
130
|
+
status: response.status,
|
|
131
|
+
error: `content-length ${size} exceeds ${MAX_BYTES}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
137
|
+
if (buffer.length > MAX_BYTES) {
|
|
138
|
+
return {
|
|
139
|
+
buffer: Buffer.alloc(0),
|
|
140
|
+
contentType,
|
|
141
|
+
contentDisposition,
|
|
142
|
+
ok: false,
|
|
143
|
+
status: response.status,
|
|
144
|
+
error: `response exceeds ${MAX_BYTES} bytes`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { buffer, contentType, contentDisposition, ok: true, status: response.status };
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (signal?.aborted) {
|
|
151
|
+
return { buffer: Buffer.alloc(0), contentType: "", ok: false, error: "aborted" };
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
buffer: Buffer.alloc(0),
|
|
155
|
+
contentType: "",
|
|
156
|
+
ok: false,
|
|
157
|
+
error: `request failed: ${String(err)}`,
|
|
158
|
+
};
|
|
159
|
+
} finally {
|
|
160
|
+
cleanup();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -79,7 +79,7 @@ function extractVideoId(url: string): string | null {
|
|
|
79
79
|
/**
|
|
80
80
|
* Handle Vimeo URLs via oEmbed API
|
|
81
81
|
*/
|
|
82
|
-
export const handleVimeo: SpecialHandler = async (url: string, timeout: number) => {
|
|
82
|
+
export const handleVimeo: SpecialHandler = async (url: string, timeout: number, signal?: AbortSignal) => {
|
|
83
83
|
try {
|
|
84
84
|
const parsed = new URL(url);
|
|
85
85
|
if (!parsed.hostname.includes("vimeo.com")) return null;
|
|
@@ -92,7 +92,7 @@ export const handleVimeo: SpecialHandler = async (url: string, timeout: number)
|
|
|
92
92
|
// Use canonical URL for oEmbed (handles staffpicks and other URL formats)
|
|
93
93
|
const canonicalUrl = `https://vimeo.com/${videoId}`;
|
|
94
94
|
const oembedUrl = `https://vimeo.com/api/oembed.json?url=${encodeURIComponent(canonicalUrl)}`;
|
|
95
|
-
const oembedResult = await loadPage(oembedUrl, { timeout });
|
|
95
|
+
const oembedResult = await loadPage(oembedUrl, { timeout, signal });
|
|
96
96
|
|
|
97
97
|
if (!oembedResult.ok) return null;
|
|
98
98
|
|
|
@@ -117,7 +117,7 @@ export const handleVimeo: SpecialHandler = async (url: string, timeout: number)
|
|
|
117
117
|
// Try to get additional details from video config
|
|
118
118
|
try {
|
|
119
119
|
const configUrl = `https://player.vimeo.com/video/${videoId}/config`;
|
|
120
|
-
const configResult = await loadPage(configUrl, { timeout: Math.min(timeout, 5) });
|
|
120
|
+
const configResult = await loadPage(configUrl, { timeout: Math.min(timeout, 5), signal });
|
|
121
121
|
|
|
122
122
|
if (configResult.ok) {
|
|
123
123
|
const config = JSON.parse(configResult.content) as VimeoVideoConfig;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface MarketplaceProperty {
|
|
5
|
+
key?: string;
|
|
6
|
+
value?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface MarketplaceVersion {
|
|
10
|
+
version?: string;
|
|
11
|
+
properties?: MarketplaceProperty[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface MarketplaceStatistic {
|
|
15
|
+
statisticName?: string;
|
|
16
|
+
value?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface MarketplacePublisher {
|
|
20
|
+
publisherName?: string;
|
|
21
|
+
displayName?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface MarketplaceExtension {
|
|
25
|
+
extensionName?: string;
|
|
26
|
+
displayName?: string;
|
|
27
|
+
shortDescription?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
publisher?: MarketplacePublisher;
|
|
30
|
+
versions?: MarketplaceVersion[];
|
|
31
|
+
statistics?: MarketplaceStatistic[];
|
|
32
|
+
categories?: string[];
|
|
33
|
+
tags?: string[];
|
|
34
|
+
properties?: MarketplaceProperty[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface MarketplaceResponse {
|
|
38
|
+
results?: Array<{ extensions?: MarketplaceExtension[] }>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const MARKETPLACE_HOSTS = new Set(["marketplace.visualstudio.com", "www.marketplace.visualstudio.com"]);
|
|
42
|
+
|
|
43
|
+
function getItemName(parsed: URL): string | null {
|
|
44
|
+
if (!parsed.pathname.startsWith("/items")) return null;
|
|
45
|
+
const itemName = parsed.searchParams.get("itemName");
|
|
46
|
+
if (!itemName) return null;
|
|
47
|
+
const decoded = decodeURIComponent(itemName);
|
|
48
|
+
if (!decoded.includes(".")) return null;
|
|
49
|
+
return decoded;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toStatMap(stats: MarketplaceStatistic[] | undefined): Map<string, number> {
|
|
53
|
+
const map = new Map<string, number>();
|
|
54
|
+
if (!stats) return map;
|
|
55
|
+
for (const stat of stats) {
|
|
56
|
+
if (!stat.statisticName || typeof stat.value !== "number") continue;
|
|
57
|
+
map.set(stat.statisticName.trim().toLowerCase(), stat.value);
|
|
58
|
+
}
|
|
59
|
+
return map;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatRating(averageRating?: number, ratingCount?: number): string | null {
|
|
63
|
+
if (averageRating === undefined && ratingCount === undefined) return null;
|
|
64
|
+
if (averageRating !== undefined) {
|
|
65
|
+
const formatted = averageRating.toFixed(2).replace(/\.0+$/, "").replace(/\.$/, "");
|
|
66
|
+
if (ratingCount !== undefined) {
|
|
67
|
+
return `${formatted} (${formatCount(ratingCount)} ratings)`;
|
|
68
|
+
}
|
|
69
|
+
return formatted;
|
|
70
|
+
}
|
|
71
|
+
if (ratingCount !== undefined) {
|
|
72
|
+
return `${formatCount(ratingCount)} ratings`;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function extractRepoLink(properties: MarketplaceProperty[] | undefined): string | null {
|
|
78
|
+
if (!properties) return null;
|
|
79
|
+
for (const prop of properties) {
|
|
80
|
+
const key = prop.key?.trim().toLowerCase();
|
|
81
|
+
const value = prop.value?.trim();
|
|
82
|
+
if (!key || !value) continue;
|
|
83
|
+
if (!value.startsWith("http")) continue;
|
|
84
|
+
if (key.includes("links.source") || key.includes("repository")) return value;
|
|
85
|
+
}
|
|
86
|
+
for (const prop of properties) {
|
|
87
|
+
const key = prop.key?.trim().toLowerCase();
|
|
88
|
+
const value = prop.value?.trim();
|
|
89
|
+
if (!key || !value) continue;
|
|
90
|
+
if (!value.startsWith("http")) continue;
|
|
91
|
+
if (key === "source" || key.endsWith(".source")) return value;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Handle VS Code Marketplace URLs via extension query API
|
|
98
|
+
*/
|
|
99
|
+
export const handleVscodeMarketplace: SpecialHandler = async (
|
|
100
|
+
url: string,
|
|
101
|
+
timeout: number,
|
|
102
|
+
signal?: AbortSignal,
|
|
103
|
+
): Promise<RenderResult | null> => {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = new URL(url);
|
|
106
|
+
if (!MARKETPLACE_HOSTS.has(parsed.hostname)) return null;
|
|
107
|
+
|
|
108
|
+
const itemName = getItemName(parsed);
|
|
109
|
+
if (!itemName) return null;
|
|
110
|
+
|
|
111
|
+
const [publisherFromUrl, ...nameParts] = itemName.split(".");
|
|
112
|
+
const extensionFromUrl = nameParts.join(".");
|
|
113
|
+
|
|
114
|
+
const fetchedAt = new Date().toISOString();
|
|
115
|
+
const apiUrl = "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery";
|
|
116
|
+
const payload = JSON.stringify({
|
|
117
|
+
filters: [
|
|
118
|
+
{
|
|
119
|
+
criteria: [{ filterType: 7, value: itemName }],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
flags: 950,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const result = await loadPage(apiUrl, {
|
|
126
|
+
timeout,
|
|
127
|
+
signal,
|
|
128
|
+
method: "POST",
|
|
129
|
+
body: payload,
|
|
130
|
+
headers: {
|
|
131
|
+
"Content-Type": "application/json",
|
|
132
|
+
Accept: "application/json;api-version=7.2-preview.1",
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!result.ok) return null;
|
|
137
|
+
|
|
138
|
+
let data: MarketplaceResponse;
|
|
139
|
+
try {
|
|
140
|
+
data = JSON.parse(result.content) as MarketplaceResponse;
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const extension = data.results?.[0]?.extensions?.[0];
|
|
146
|
+
if (!extension) return null;
|
|
147
|
+
|
|
148
|
+
const extensionName = extension.extensionName ?? extensionFromUrl;
|
|
149
|
+
const displayName = extension.displayName ?? extensionName ?? itemName;
|
|
150
|
+
const description = extension.shortDescription ?? extension.description;
|
|
151
|
+
|
|
152
|
+
const publisherName = extension.publisher?.publisherName ?? publisherFromUrl;
|
|
153
|
+
const publisherDisplayName = extension.publisher?.displayName;
|
|
154
|
+
const publisherLabel =
|
|
155
|
+
publisherDisplayName && publisherName && publisherDisplayName !== publisherName
|
|
156
|
+
? `${publisherDisplayName} (${publisherName})`
|
|
157
|
+
: (publisherDisplayName ?? publisherName);
|
|
158
|
+
|
|
159
|
+
const version = extension.versions?.[0]?.version;
|
|
160
|
+
const statMap = toStatMap(extension.statistics);
|
|
161
|
+
const installs = statMap.get("install") ?? statMap.get("installs");
|
|
162
|
+
const averageRating = statMap.get("averagerating");
|
|
163
|
+
const ratingCount = statMap.get("ratingcount");
|
|
164
|
+
const ratingLabel = formatRating(averageRating, ratingCount);
|
|
165
|
+
|
|
166
|
+
const repoLink = extractRepoLink(extension.versions?.[0]?.properties) ?? extractRepoLink(extension.properties);
|
|
167
|
+
|
|
168
|
+
const identifier = publisherName && extensionName ? `${publisherName}.${extensionName}` : itemName;
|
|
169
|
+
|
|
170
|
+
let md = `# ${displayName}\n\n`;
|
|
171
|
+
if (description) md += `${description}\n\n`;
|
|
172
|
+
md += `**Identifier:** ${identifier}\n`;
|
|
173
|
+
if (publisherLabel) md += `**Publisher:** ${publisherLabel}\n`;
|
|
174
|
+
if (version) md += `**Version:** ${version}\n`;
|
|
175
|
+
if (installs !== undefined) md += `**Installs:** ${formatCount(installs)}\n`;
|
|
176
|
+
if (ratingLabel) md += `**Rating:** ${ratingLabel}\n`;
|
|
177
|
+
if (extension.categories?.length) md += `**Categories:** ${extension.categories.join(", ")}\n`;
|
|
178
|
+
if (extension.tags?.length) md += `**Tags:** ${extension.tags.join(", ")}\n`;
|
|
179
|
+
if (repoLink) md += `**Repository:** ${repoLink}\n`;
|
|
180
|
+
|
|
181
|
+
const output = finalizeOutput(md);
|
|
182
|
+
return {
|
|
183
|
+
url,
|
|
184
|
+
finalUrl: url,
|
|
185
|
+
contentType: "text/markdown",
|
|
186
|
+
method: "vscode-marketplace",
|
|
187
|
+
content: output.content,
|
|
188
|
+
fetchedAt,
|
|
189
|
+
truncated: output.truncated,
|
|
190
|
+
notes: ["Fetched via VS Code Marketplace API"],
|
|
191
|
+
};
|
|
192
|
+
} catch {}
|
|
193
|
+
|
|
194
|
+
return null;
|
|
195
|
+
};
|