@oh-my-pi/pi-coding-agent 3.30.0 → 3.32.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 +85 -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 +367 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/sdk.ts +10 -2
- 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/slash-commands.ts +39 -13
- 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 +8 -4
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +84 -19
- 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 +72 -35
- 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/commands.ts +4 -0
- package/src/core/tools/task/executor.ts +94 -83
- package/src/core/tools/task/index.ts +130 -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 -63
- 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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface SearchcodeResult {
|
|
5
|
+
id?: number | string;
|
|
6
|
+
filename?: string;
|
|
7
|
+
repo?: string;
|
|
8
|
+
language?: string;
|
|
9
|
+
code?: string;
|
|
10
|
+
lines?: number | string | Array<number | string>;
|
|
11
|
+
location?: string;
|
|
12
|
+
url?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface SearchcodeSearchResponse {
|
|
16
|
+
query?: string;
|
|
17
|
+
results?: SearchcodeResult[];
|
|
18
|
+
total?: number;
|
|
19
|
+
total_results?: number;
|
|
20
|
+
nextpage?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const VALID_HOSTS = new Set(["searchcode.com", "www.searchcode.com"]);
|
|
24
|
+
|
|
25
|
+
function parseLineNumbers(lines: SearchcodeResult["lines"]): number[] | null {
|
|
26
|
+
if (typeof lines === "number" && Number.isFinite(lines)) return [lines];
|
|
27
|
+
|
|
28
|
+
if (typeof lines === "string") {
|
|
29
|
+
const parts = lines.split(/[,\s]+/).filter(Boolean);
|
|
30
|
+
const parsed = parts.map((part) => Number.parseInt(part, 10)).filter((value) => Number.isFinite(value));
|
|
31
|
+
return parsed.length ? parsed : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (Array.isArray(lines)) {
|
|
35
|
+
const parsed = lines.map((part) => Number.parseInt(String(part), 10)).filter((value) => Number.isFinite(value));
|
|
36
|
+
return parsed.length ? parsed : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatLineNumbers(lines: number[] | null): string | null {
|
|
43
|
+
if (!lines || lines.length === 0) return null;
|
|
44
|
+
if (lines.length <= 10) return lines.join(", ");
|
|
45
|
+
const min = Math.min(...lines);
|
|
46
|
+
const max = Math.max(...lines);
|
|
47
|
+
return `${min}-${max} (${lines.length} lines)`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatCodeBlock(
|
|
51
|
+
code: string | undefined,
|
|
52
|
+
language: string | undefined,
|
|
53
|
+
lines: number[] | null,
|
|
54
|
+
): string | null {
|
|
55
|
+
if (!code) return null;
|
|
56
|
+
|
|
57
|
+
const normalized = code.replace(/\r\n/g, "\n").trimEnd();
|
|
58
|
+
const codeLines = normalized.split("\n");
|
|
59
|
+
const languageTag = typeof language === "string" ? language.trim().toLowerCase() : "";
|
|
60
|
+
|
|
61
|
+
let displayLines = codeLines;
|
|
62
|
+
if (lines && lines.length === codeLines.length) {
|
|
63
|
+
displayLines = codeLines.map((line, index) => `${lines[index]}: ${line}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const fence = languageTag ? languageTag : "";
|
|
67
|
+
return `\n\n\`\`\`${fence}\n${displayLines.join("\n")}\n\`\`\`\n`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const handleSearchcode: SpecialHandler = async (
|
|
71
|
+
url: string,
|
|
72
|
+
timeout: number,
|
|
73
|
+
signal?: AbortSignal,
|
|
74
|
+
): Promise<RenderResult | null> => {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = new URL(url);
|
|
77
|
+
if (!VALID_HOSTS.has(parsed.hostname)) return null;
|
|
78
|
+
|
|
79
|
+
const fetchedAt = new Date().toISOString();
|
|
80
|
+
const viewMatch = parsed.pathname.match(/^\/codesearch\/view\/([^/?#]+)/);
|
|
81
|
+
if (viewMatch) {
|
|
82
|
+
const id = viewMatch[1];
|
|
83
|
+
const apiUrl = `https://searchcode.com/api/result/${encodeURIComponent(id)}/`;
|
|
84
|
+
const result = await loadPage(apiUrl, { timeout, signal, headers: { Accept: "application/json" } });
|
|
85
|
+
if (!result.ok) return null;
|
|
86
|
+
|
|
87
|
+
let data: SearchcodeResult;
|
|
88
|
+
try {
|
|
89
|
+
data = JSON.parse(result.content) as SearchcodeResult;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const filename = data.filename || data.location || `Result ${id}`;
|
|
95
|
+
const lineNumbers = parseLineNumbers(data.lines);
|
|
96
|
+
const formattedLines = formatLineNumbers(lineNumbers);
|
|
97
|
+
const viewUrl = data.url || `https://searchcode.com/codesearch/view/${id}`;
|
|
98
|
+
const snippetBlock = formatCodeBlock(data.code, data.language, lineNumbers);
|
|
99
|
+
|
|
100
|
+
let md = `# ${filename}\n\n`;
|
|
101
|
+
md += `## Description\n\n`;
|
|
102
|
+
md += "Code snippet from searchcode.com.\n\n";
|
|
103
|
+
md += `## Metadata\n\n`;
|
|
104
|
+
if (data.repo) md += `**Repository:** ${data.repo}\n`;
|
|
105
|
+
if (data.language) md += `**Language:** ${data.language}\n`;
|
|
106
|
+
if (data.filename) md += `**File:** ${data.filename}\n`;
|
|
107
|
+
if (data.location) md += `**Location:** ${data.location}\n`;
|
|
108
|
+
if (formattedLines) md += `**Lines:** ${formattedLines}\n`;
|
|
109
|
+
md += `**Result ID:** ${id}\n`;
|
|
110
|
+
md += `**URL:** ${viewUrl}\n`;
|
|
111
|
+
|
|
112
|
+
md += `\n## Snippet`;
|
|
113
|
+
if (snippetBlock) {
|
|
114
|
+
md += snippetBlock;
|
|
115
|
+
} else {
|
|
116
|
+
md += "\n\n_No snippet available._\n";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const output = finalizeOutput(md);
|
|
120
|
+
return {
|
|
121
|
+
url,
|
|
122
|
+
finalUrl: url,
|
|
123
|
+
contentType: "text/markdown",
|
|
124
|
+
method: "searchcode",
|
|
125
|
+
content: output.content,
|
|
126
|
+
fetchedAt,
|
|
127
|
+
truncated: output.truncated,
|
|
128
|
+
notes: ["Fetched via searchcode API"],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const query = parsed.searchParams.get("q");
|
|
133
|
+
const isSearchPage =
|
|
134
|
+
parsed.pathname === "/" || parsed.pathname === "/codesearch" || parsed.pathname === "/codesearch/";
|
|
135
|
+
if (!query || !isSearchPage) return null;
|
|
136
|
+
|
|
137
|
+
const pageRaw = parsed.searchParams.get("p") ?? parsed.searchParams.get("page");
|
|
138
|
+
const pageNumber = pageRaw ? Number.parseInt(pageRaw, 10) : 0;
|
|
139
|
+
const page = Number.isFinite(pageNumber) && pageNumber >= 0 ? pageNumber : 0;
|
|
140
|
+
const apiUrl = `https://searchcode.com/api/codesearch_I/?q=${encodeURIComponent(query)}&p=${page}`;
|
|
141
|
+
const result = await loadPage(apiUrl, { timeout, signal, headers: { Accept: "application/json" } });
|
|
142
|
+
if (!result.ok) return null;
|
|
143
|
+
|
|
144
|
+
let data: SearchcodeSearchResponse;
|
|
145
|
+
try {
|
|
146
|
+
data = JSON.parse(result.content) as SearchcodeSearchResponse;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const results = Array.isArray(data.results) ? data.results : [];
|
|
152
|
+
const total =
|
|
153
|
+
typeof data.total === "number"
|
|
154
|
+
? data.total
|
|
155
|
+
: typeof data.total_results === "number"
|
|
156
|
+
? data.total_results
|
|
157
|
+
: null;
|
|
158
|
+
|
|
159
|
+
let md = `# Searchcode Results\n\n`;
|
|
160
|
+
md += `## Description\n\n`;
|
|
161
|
+
md += `Search results for \`${query}\` on searchcode.com.\n\n`;
|
|
162
|
+
md += `## Metadata\n\n`;
|
|
163
|
+
md += `**Query:** \`${query}\`\n`;
|
|
164
|
+
md += `**Page:** ${page}\n`;
|
|
165
|
+
if (total !== null) md += `**Total Results:** ${formatCount(total)}\n`;
|
|
166
|
+
md += `**Result Count:** ${results.length}\n`;
|
|
167
|
+
if (typeof data.nextpage === "number") md += `**Next Page:** ${data.nextpage}\n`;
|
|
168
|
+
|
|
169
|
+
md += `\n## Results\n\n`;
|
|
170
|
+
|
|
171
|
+
if (results.length === 0) {
|
|
172
|
+
md += "_No results found._\n";
|
|
173
|
+
} else {
|
|
174
|
+
const maxResults = 10;
|
|
175
|
+
for (const resultItem of results.slice(0, maxResults)) {
|
|
176
|
+
const id = resultItem.id !== undefined ? String(resultItem.id) : null;
|
|
177
|
+
const filename = resultItem.filename || resultItem.location || "Result";
|
|
178
|
+
const lineNumbers = parseLineNumbers(resultItem.lines);
|
|
179
|
+
const formattedLines = formatLineNumbers(lineNumbers);
|
|
180
|
+
const viewUrl = resultItem.url || (id ? `https://searchcode.com/codesearch/view/${id}` : null);
|
|
181
|
+
const snippetBlock = formatCodeBlock(resultItem.code, resultItem.language, lineNumbers);
|
|
182
|
+
|
|
183
|
+
md += `### ${filename}\n\n`;
|
|
184
|
+
if (resultItem.repo) md += `**Repository:** ${resultItem.repo}\n`;
|
|
185
|
+
if (resultItem.language) md += `**Language:** ${resultItem.language}\n`;
|
|
186
|
+
if (resultItem.filename) md += `**File:** ${resultItem.filename}\n`;
|
|
187
|
+
if (resultItem.location) md += `**Location:** ${resultItem.location}\n`;
|
|
188
|
+
if (formattedLines) md += `**Lines:** ${formattedLines}\n`;
|
|
189
|
+
if (viewUrl) md += `**URL:** ${viewUrl}\n`;
|
|
190
|
+
|
|
191
|
+
if (snippetBlock) {
|
|
192
|
+
md += `${snippetBlock}\n`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
md += "\n";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (results.length > maxResults) {
|
|
199
|
+
md += `\n_Only showing first ${maxResults} results._\n`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const output = finalizeOutput(md);
|
|
204
|
+
return {
|
|
205
|
+
url,
|
|
206
|
+
finalUrl: url,
|
|
207
|
+
contentType: "text/markdown",
|
|
208
|
+
method: "searchcode",
|
|
209
|
+
content: output.content,
|
|
210
|
+
fetchedAt,
|
|
211
|
+
truncated: output.truncated,
|
|
212
|
+
notes: ["Fetched via searchcode API"],
|
|
213
|
+
};
|
|
214
|
+
} catch {}
|
|
215
|
+
|
|
216
|
+
return null;
|
|
217
|
+
};
|
|
@@ -153,7 +153,11 @@ function buildFilingUrl(cik: string, accessionNumber: string, document: string):
|
|
|
153
153
|
/**
|
|
154
154
|
* Handle SEC EDGAR URLs via data.sec.gov API
|
|
155
155
|
*/
|
|
156
|
-
export const handleSecEdgar: SpecialHandler = async (
|
|
156
|
+
export const handleSecEdgar: SpecialHandler = async (
|
|
157
|
+
url: string,
|
|
158
|
+
timeout: number,
|
|
159
|
+
signal?: AbortSignal,
|
|
160
|
+
): Promise<RenderResult | null> => {
|
|
157
161
|
try {
|
|
158
162
|
const parsed = new URL(url);
|
|
159
163
|
|
|
@@ -171,6 +175,7 @@ export const handleSecEdgar: SpecialHandler = async (url: string, timeout: numbe
|
|
|
171
175
|
const apiUrl = `https://data.sec.gov/submissions/CIK${cik}.json`;
|
|
172
176
|
const result = await loadPage(apiUrl, {
|
|
173
177
|
timeout,
|
|
178
|
+
signal,
|
|
174
179
|
headers: {
|
|
175
180
|
"User-Agent": "CodingAgent/1.0 (research tool)",
|
|
176
181
|
Accept: "application/json",
|
|
@@ -43,7 +43,7 @@ function extractPaperId(url: string): string | null {
|
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export const handleSemanticScholar: SpecialHandler = async (url: string, timeout: number) => {
|
|
46
|
+
export const handleSemanticScholar: SpecialHandler = async (url: string, timeout: number, signal?: AbortSignal) => {
|
|
47
47
|
if (!url.includes("semanticscholar.org")) return null;
|
|
48
48
|
|
|
49
49
|
const paperId = extractPaperId(url);
|
|
@@ -77,7 +77,7 @@ export const handleSemanticScholar: SpecialHandler = async (url: string, timeout
|
|
|
77
77
|
|
|
78
78
|
const apiUrl = `https://api.semanticscholar.org/graph/v1/paper/${paperId}?fields=${fields}`;
|
|
79
79
|
|
|
80
|
-
const { content, ok, finalUrl } = await loadPage(apiUrl, { timeout });
|
|
80
|
+
const { content, ok, finalUrl } = await loadPage(apiUrl, { timeout, signal });
|
|
81
81
|
|
|
82
82
|
if (!ok || !content) {
|
|
83
83
|
return {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface SnapcraftPublisher {
|
|
5
|
+
"display-name"?: string;
|
|
6
|
+
username?: string;
|
|
7
|
+
id?: string;
|
|
8
|
+
validation?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SnapcraftChannel {
|
|
12
|
+
name?: string;
|
|
13
|
+
track?: string;
|
|
14
|
+
risk?: string;
|
|
15
|
+
branch?: string | null;
|
|
16
|
+
architecture?: string;
|
|
17
|
+
"released-at"?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SnapcraftDownload {
|
|
21
|
+
size?: number;
|
|
22
|
+
url?: string;
|
|
23
|
+
"sha3-384"?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SnapcraftChannelMapEntry {
|
|
27
|
+
channel?: SnapcraftChannel;
|
|
28
|
+
version?: string;
|
|
29
|
+
revision?: number | string;
|
|
30
|
+
download?: SnapcraftDownload;
|
|
31
|
+
type?: string;
|
|
32
|
+
"created-at"?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface SnapcraftSnap {
|
|
36
|
+
name?: string;
|
|
37
|
+
title?: string;
|
|
38
|
+
summary?: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
publisher?: SnapcraftPublisher;
|
|
41
|
+
version?: string;
|
|
42
|
+
confinement?: string;
|
|
43
|
+
base?: string;
|
|
44
|
+
downloads?: number;
|
|
45
|
+
download?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface SnapcraftResponse {
|
|
49
|
+
name?: string;
|
|
50
|
+
title?: string;
|
|
51
|
+
summary?: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
publisher?: SnapcraftPublisher;
|
|
54
|
+
version?: string;
|
|
55
|
+
confinement?: string;
|
|
56
|
+
base?: string;
|
|
57
|
+
downloads?: number;
|
|
58
|
+
download?: number;
|
|
59
|
+
snap?: SnapcraftSnap;
|
|
60
|
+
"channel-map"?: SnapcraftChannelMapEntry[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatPublisher(publisher?: SnapcraftPublisher): string | null {
|
|
64
|
+
if (!publisher) return null;
|
|
65
|
+
const displayName = publisher["display-name"] ?? publisher.username ?? publisher.id;
|
|
66
|
+
if (!displayName) return null;
|
|
67
|
+
if (publisher.username && displayName !== publisher.username) {
|
|
68
|
+
return `${displayName} (@${publisher.username})`;
|
|
69
|
+
}
|
|
70
|
+
return displayName;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatChannelName(channel?: SnapcraftChannel): string | null {
|
|
74
|
+
if (!channel) return null;
|
|
75
|
+
if (channel.name?.includes("/")) return channel.name;
|
|
76
|
+
if (channel.track && channel.risk) {
|
|
77
|
+
const branch = channel.branch ? `/${channel.branch}` : "";
|
|
78
|
+
return `${channel.track}/${channel.risk}${branch}`;
|
|
79
|
+
}
|
|
80
|
+
return channel.name ?? null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function pickVersionFromChannels(entries: SnapcraftChannelMapEntry[]): string | undefined {
|
|
84
|
+
const stable = entries.find((entry) => entry.channel?.risk === "stable" && entry.version);
|
|
85
|
+
if (stable?.version) return stable.version;
|
|
86
|
+
const first = entries.find((entry) => entry.version);
|
|
87
|
+
return first?.version;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extractDownloads(snapInfo: SnapcraftSnap | SnapcraftResponse, data: SnapcraftResponse): number | null {
|
|
91
|
+
const candidates = [snapInfo.downloads, snapInfo.download, data.downloads, data.download];
|
|
92
|
+
for (const value of candidates) {
|
|
93
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const handleSnapcraft: SpecialHandler = async (
|
|
99
|
+
url: string,
|
|
100
|
+
timeout: number,
|
|
101
|
+
signal?: AbortSignal,
|
|
102
|
+
): Promise<RenderResult | null> => {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = new URL(url);
|
|
105
|
+
if (parsed.hostname !== "snapcraft.io" && parsed.hostname !== "www.snapcraft.io") return null;
|
|
106
|
+
|
|
107
|
+
const installMatch = parsed.pathname.match(/^\/install\/([^/]+)\/?$/);
|
|
108
|
+
const directMatch = parsed.pathname.match(/^\/([^/]+)\/?$/);
|
|
109
|
+
if (!installMatch && !directMatch) return null;
|
|
110
|
+
|
|
111
|
+
const snapName = decodeURIComponent((installMatch ?? directMatch)![1]);
|
|
112
|
+
const fetchedAt = new Date().toISOString();
|
|
113
|
+
|
|
114
|
+
const apiUrl = `https://api.snapcraft.io/v2/snaps/info/${encodeURIComponent(snapName)}`;
|
|
115
|
+
const result = await loadPage(apiUrl, {
|
|
116
|
+
timeout,
|
|
117
|
+
signal,
|
|
118
|
+
headers: {
|
|
119
|
+
Accept: "application/json",
|
|
120
|
+
"Snap-Device-Series": "16",
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
if (!result.ok) return null;
|
|
124
|
+
|
|
125
|
+
let data: SnapcraftResponse;
|
|
126
|
+
try {
|
|
127
|
+
data = JSON.parse(result.content) as SnapcraftResponse;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const snapInfo = data.snap ?? data;
|
|
133
|
+
const name = snapInfo.title ?? snapInfo.name ?? data.name ?? snapName;
|
|
134
|
+
const summary = snapInfo.summary ?? data.summary;
|
|
135
|
+
const description = snapInfo.description ?? data.description;
|
|
136
|
+
const publisher = formatPublisher(snapInfo.publisher ?? data.publisher);
|
|
137
|
+
const confinement = snapInfo.confinement ?? data.confinement;
|
|
138
|
+
const base = snapInfo.base ?? data.base;
|
|
139
|
+
|
|
140
|
+
const channelMap = data["channel-map"] ?? [];
|
|
141
|
+
let version = snapInfo.version ?? data.version;
|
|
142
|
+
if (!version && channelMap.length > 0) {
|
|
143
|
+
version = pickVersionFromChannels(channelMap);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const downloads = extractDownloads(snapInfo, data);
|
|
147
|
+
|
|
148
|
+
const channels = new Map<string, { version?: string; architectures: Set<string> }>();
|
|
149
|
+
for (const entry of channelMap) {
|
|
150
|
+
const channelName = formatChannelName(entry.channel);
|
|
151
|
+
if (!channelName) continue;
|
|
152
|
+
const existing = channels.get(channelName) ?? { architectures: new Set<string>() };
|
|
153
|
+
if (!existing.version && entry.version) existing.version = entry.version;
|
|
154
|
+
if (entry.channel?.architecture) existing.architectures.add(entry.channel.architecture);
|
|
155
|
+
channels.set(channelName, existing);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let md = `# ${name}\n\n`;
|
|
159
|
+
if (summary) md += `${summary}\n\n`;
|
|
160
|
+
|
|
161
|
+
md += `**Version:** ${version ?? "unknown"}`;
|
|
162
|
+
if (confinement) md += ` · **Confinement:** ${confinement}`;
|
|
163
|
+
if (base) md += ` · **Base:** ${base}`;
|
|
164
|
+
md += "\n";
|
|
165
|
+
if (publisher) md += `**Publisher:** ${publisher}\n`;
|
|
166
|
+
if (downloads !== null) md += `**Downloads:** ${formatCount(downloads)}\n`;
|
|
167
|
+
md += "\n";
|
|
168
|
+
|
|
169
|
+
if (channels.size > 0) {
|
|
170
|
+
md += "## Channels\n\n";
|
|
171
|
+
const sortedChannels = Array.from(channels.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
172
|
+
for (const [channelName, info] of sortedChannels) {
|
|
173
|
+
const arches = Array.from(info.architectures).sort();
|
|
174
|
+
const versionSuffix = info.version ? `: ${info.version}` : "";
|
|
175
|
+
const archSuffix = arches.length > 0 ? ` (${arches.join(", ")})` : "";
|
|
176
|
+
md += `- ${channelName}${versionSuffix}${archSuffix}\n`;
|
|
177
|
+
}
|
|
178
|
+
md += "\n";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const descriptionText = description ?? summary;
|
|
182
|
+
if (descriptionText) {
|
|
183
|
+
md += `## Description\n\n${descriptionText}\n`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const output = finalizeOutput(md);
|
|
187
|
+
return {
|
|
188
|
+
url,
|
|
189
|
+
finalUrl: url,
|
|
190
|
+
contentType: "text/markdown",
|
|
191
|
+
method: "snapcraft",
|
|
192
|
+
content: output.content,
|
|
193
|
+
fetchedAt,
|
|
194
|
+
truncated: output.truncated,
|
|
195
|
+
notes: ["Fetched via Snapcraft API"],
|
|
196
|
+
};
|
|
197
|
+
} catch {}
|
|
198
|
+
|
|
199
|
+
return null;
|
|
200
|
+
};
|