@oh-my-pi/pi-coding-agent 3.25.0 → 3.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +369 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/complete.ts +2 -4
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +6 -8
- package/src/core/tools/jtd-to-json-schema.ts +174 -196
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +58 -9
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +55 -32
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +152 -76
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/executor.ts +204 -67
- package/src/core/tools/task/index.ts +129 -92
- package/src/core/tools/task/name-generator.ts +1544 -214
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +34 -11
- package/src/core/tools/task/worker.ts +152 -27
- package/src/core/tools/web-fetch.ts +220 -1657
- package/src/core/tools/web-scrapers/academic.test.ts +239 -0
- package/src/core/tools/web-scrapers/artifacthub.ts +215 -0
- package/src/core/tools/web-scrapers/arxiv.ts +88 -0
- package/src/core/tools/web-scrapers/aur.ts +175 -0
- package/src/core/tools/web-scrapers/biorxiv.ts +141 -0
- package/src/core/tools/web-scrapers/bluesky.ts +284 -0
- package/src/core/tools/web-scrapers/brew.ts +177 -0
- package/src/core/tools/web-scrapers/business.test.ts +82 -0
- package/src/core/tools/web-scrapers/cheatsh.ts +78 -0
- package/src/core/tools/web-scrapers/chocolatey.ts +158 -0
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/web-scrapers/coingecko.ts +184 -0
- package/src/core/tools/web-scrapers/crates-io.ts +128 -0
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/web-scrapers/dev-platforms.test.ts +254 -0
- package/src/core/tools/web-scrapers/devto.ts +177 -0
- package/src/core/tools/web-scrapers/discogs.ts +308 -0
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/web-scrapers/dockerhub.ts +160 -0
- package/src/core/tools/web-scrapers/documentation.test.ts +85 -0
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/finance-media.test.ts +144 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/web-scrapers/git-hosting.test.ts +272 -0
- package/src/core/tools/web-scrapers/github-gist.ts +68 -0
- package/src/core/tools/web-scrapers/github.ts +455 -0
- package/src/core/tools/web-scrapers/gitlab.ts +456 -0
- package/src/core/tools/web-scrapers/go-pkg.ts +275 -0
- package/src/core/tools/web-scrapers/hackage.ts +94 -0
- package/src/core/tools/web-scrapers/hackernews.ts +208 -0
- package/src/core/tools/web-scrapers/hex.ts +121 -0
- package/src/core/tools/web-scrapers/huggingface.ts +385 -0
- package/src/core/tools/web-scrapers/iacr.ts +86 -0
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/web-scrapers/lobsters.ts +186 -0
- package/src/core/tools/web-scrapers/mastodon.ts +310 -0
- package/src/core/tools/web-scrapers/maven.ts +152 -0
- package/src/core/tools/web-scrapers/mdn.ts +174 -0
- package/src/core/tools/web-scrapers/media.test.ts +138 -0
- package/src/core/tools/web-scrapers/metacpan.ts +253 -0
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/web-scrapers/npm.ts +114 -0
- package/src/core/tools/web-scrapers/nuget.ts +205 -0
- package/src/core/tools/web-scrapers/nvd.ts +243 -0
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/web-scrapers/opencorporates.ts +275 -0
- package/src/core/tools/web-scrapers/openlibrary.ts +319 -0
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/web-scrapers/osv.ts +189 -0
- package/src/core/tools/web-scrapers/package-managers-2.test.ts +199 -0
- package/src/core/tools/web-scrapers/package-managers.test.ts +171 -0
- package/src/core/tools/web-scrapers/package-registries.test.ts +259 -0
- package/src/core/tools/web-scrapers/packagist.ts +174 -0
- package/src/core/tools/web-scrapers/pub-dev.ts +185 -0
- package/src/core/tools/web-scrapers/pubmed.ts +178 -0
- package/src/core/tools/web-scrapers/pypi.ts +129 -0
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/web-scrapers/readthedocs.ts +126 -0
- package/src/core/tools/web-scrapers/reddit.ts +104 -0
- package/src/core/tools/web-scrapers/repology.ts +262 -0
- package/src/core/tools/web-scrapers/research.test.ts +107 -0
- package/src/core/tools/web-scrapers/rfc.ts +209 -0
- package/src/core/tools/web-scrapers/rubygems.ts +117 -0
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/web-scrapers/sec-edgar.ts +274 -0
- package/src/core/tools/web-scrapers/security.test.ts +103 -0
- package/src/core/tools/web-scrapers/semantic-scholar.ts +190 -0
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/social-extended.test.ts +192 -0
- package/src/core/tools/web-scrapers/social.test.ts +259 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/web-scrapers/spotify.ts +218 -0
- package/src/core/tools/web-scrapers/stackexchange.test.ts +120 -0
- package/src/core/tools/web-scrapers/stackoverflow.ts +124 -0
- package/src/core/tools/web-scrapers/standards.test.ts +122 -0
- package/src/core/tools/web-scrapers/terraform.ts +304 -0
- package/src/core/tools/web-scrapers/tldr.ts +51 -0
- package/src/core/tools/web-scrapers/twitter.ts +96 -0
- package/src/core/tools/web-scrapers/types.ts +234 -0
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/web-scrapers/vimeo.ts +152 -0
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/web-scrapers/wikidata.ts +357 -0
- package/src/core/tools/web-scrapers/wikipedia.test.ts +73 -0
- package/src/core/tools/web-scrapers/wikipedia.ts +95 -0
- package/src/core/tools/web-scrapers/youtube.test.ts +198 -0
- package/src/core/tools/web-scrapers/youtube.ts +371 -0
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -38
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/src/utils/tools-manager.ts +110 -8
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface RfcMetadata {
|
|
5
|
+
doc_id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
authors?: Array<{ name: string; affiliation?: string }>;
|
|
8
|
+
pub_status?: string;
|
|
9
|
+
current_status?: string;
|
|
10
|
+
stream?: string;
|
|
11
|
+
area?: string;
|
|
12
|
+
wg_acronym?: string;
|
|
13
|
+
pub_date?: string;
|
|
14
|
+
page_count?: number;
|
|
15
|
+
abstract?: string;
|
|
16
|
+
keywords?: string[];
|
|
17
|
+
obsoletes?: string[];
|
|
18
|
+
obsoleted_by?: string[];
|
|
19
|
+
updates?: string[];
|
|
20
|
+
updated_by?: string[];
|
|
21
|
+
see_also?: string[];
|
|
22
|
+
errata_url?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extract RFC number from various URL patterns
|
|
27
|
+
*/
|
|
28
|
+
function extractRfcNumber(url: URL): string | null {
|
|
29
|
+
const { hostname, pathname } = url;
|
|
30
|
+
|
|
31
|
+
// https://www.rfc-editor.org/rfc/rfc{number}
|
|
32
|
+
// https://www.rfc-editor.org/rfc/rfc{number}.html
|
|
33
|
+
// https://www.rfc-editor.org/rfc/rfc{number}.txt
|
|
34
|
+
if (hostname === "www.rfc-editor.org" || hostname === "rfc-editor.org") {
|
|
35
|
+
const match = pathname.match(/\/rfc\/rfc(\d+)(?:\.(?:html|txt|pdf))?$/i);
|
|
36
|
+
if (match) return match[1];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// https://datatracker.ietf.org/doc/rfc{number}/
|
|
40
|
+
// https://datatracker.ietf.org/doc/html/rfc{number}
|
|
41
|
+
if (hostname === "datatracker.ietf.org") {
|
|
42
|
+
const match = pathname.match(/\/doc\/(?:html\/)?rfc(\d+)\/?$/i);
|
|
43
|
+
if (match) return match[1];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// https://tools.ietf.org/html/rfc{number}
|
|
47
|
+
if (hostname === "tools.ietf.org") {
|
|
48
|
+
const match = pathname.match(/\/html\/rfc(\d+)$/i);
|
|
49
|
+
if (match) return match[1];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Clean up RFC plain text - remove page headers/footers and extra formatting
|
|
57
|
+
*/
|
|
58
|
+
function cleanRfcText(text: string): string {
|
|
59
|
+
const lines = text.split("\n");
|
|
60
|
+
const cleaned: string[] = [];
|
|
61
|
+
let skipNext = 0;
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < lines.length; i++) {
|
|
64
|
+
const line = lines[i];
|
|
65
|
+
|
|
66
|
+
// Skip lines we've marked to skip (form feeds and surrounding blank lines)
|
|
67
|
+
if (skipNext > 0) {
|
|
68
|
+
skipNext--;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Skip form feed characters and page headers (RFC NNNN ... Month Year pattern)
|
|
73
|
+
if (line.includes("\f")) {
|
|
74
|
+
// Skip the form feed line and typically 2-3 following header lines
|
|
75
|
+
skipNext = 3;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Skip page footer lines (typically just a page number or "[Page N]")
|
|
80
|
+
if (/^\s*\[Page \d+\]\s*$/.test(line)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
cleaned.push(line);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return cleaned.join("\n").replace(/\n{4,}/g, "\n\n\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Handle RFC Editor URLs - fetches IETF RFCs
|
|
92
|
+
*/
|
|
93
|
+
export const handleRfc: SpecialHandler = async (
|
|
94
|
+
url: string,
|
|
95
|
+
timeout: number,
|
|
96
|
+
signal?: AbortSignal,
|
|
97
|
+
): Promise<RenderResult | null> => {
|
|
98
|
+
try {
|
|
99
|
+
const parsed = new URL(url);
|
|
100
|
+
const rfcNumber = extractRfcNumber(parsed);
|
|
101
|
+
|
|
102
|
+
if (!rfcNumber) return null;
|
|
103
|
+
|
|
104
|
+
const fetchedAt = new Date().toISOString();
|
|
105
|
+
const notes: string[] = [];
|
|
106
|
+
|
|
107
|
+
// Fetch metadata JSON and plain text in parallel
|
|
108
|
+
const metadataUrl = `https://www.rfc-editor.org/rfc/rfc${rfcNumber}.json`;
|
|
109
|
+
const textUrl = `https://www.rfc-editor.org/rfc/rfc${rfcNumber}.txt`;
|
|
110
|
+
|
|
111
|
+
const [metaResult, textResult] = await Promise.all([
|
|
112
|
+
loadPage(metadataUrl, { timeout: Math.min(timeout, 10), signal }),
|
|
113
|
+
loadPage(textUrl, { timeout, signal }),
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
// We need at least the text content
|
|
117
|
+
if (!textResult.ok) return null;
|
|
118
|
+
|
|
119
|
+
let metadata: RfcMetadata | null = null;
|
|
120
|
+
if (metaResult.ok) {
|
|
121
|
+
try {
|
|
122
|
+
metadata = JSON.parse(metaResult.content);
|
|
123
|
+
notes.push("Metadata from RFC Editor JSON API");
|
|
124
|
+
} catch {
|
|
125
|
+
// JSON parse failed, continue without metadata
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Build markdown output
|
|
130
|
+
let md = "";
|
|
131
|
+
|
|
132
|
+
if (metadata) {
|
|
133
|
+
md += `# RFC ${rfcNumber}: ${metadata.title}\n\n`;
|
|
134
|
+
|
|
135
|
+
// Authors
|
|
136
|
+
if (metadata.authors?.length) {
|
|
137
|
+
const authorList = metadata.authors
|
|
138
|
+
.map((a) => (a.affiliation ? `${a.name} (${a.affiliation})` : a.name))
|
|
139
|
+
.join(", ");
|
|
140
|
+
md += `**Authors:** ${authorList}\n`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Publication info
|
|
144
|
+
if (metadata.pub_date) md += `**Published:** ${metadata.pub_date}\n`;
|
|
145
|
+
if (metadata.current_status) md += `**Status:** ${metadata.current_status}\n`;
|
|
146
|
+
if (metadata.stream) md += `**Stream:** ${metadata.stream}\n`;
|
|
147
|
+
if (metadata.area) md += `**Area:** ${metadata.area}\n`;
|
|
148
|
+
if (metadata.wg_acronym) md += `**Working Group:** ${metadata.wg_acronym}\n`;
|
|
149
|
+
if (metadata.page_count) md += `**Pages:** ${metadata.page_count}\n`;
|
|
150
|
+
|
|
151
|
+
// Related RFCs
|
|
152
|
+
if (metadata.obsoletes?.length) {
|
|
153
|
+
md += `**Obsoletes:** ${metadata.obsoletes.join(", ")}\n`;
|
|
154
|
+
}
|
|
155
|
+
if (metadata.obsoleted_by?.length) {
|
|
156
|
+
md += `**Obsoleted by:** ${metadata.obsoleted_by.join(", ")}\n`;
|
|
157
|
+
}
|
|
158
|
+
if (metadata.updates?.length) {
|
|
159
|
+
md += `**Updates:** ${metadata.updates.join(", ")}\n`;
|
|
160
|
+
}
|
|
161
|
+
if (metadata.updated_by?.length) {
|
|
162
|
+
md += `**Updated by:** ${metadata.updated_by.join(", ")}\n`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Keywords
|
|
166
|
+
if (metadata.keywords?.length) {
|
|
167
|
+
md += `**Keywords:** ${metadata.keywords.join(", ")}\n`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Errata
|
|
171
|
+
if (metadata.errata_url) {
|
|
172
|
+
md += `**Errata:** ${metadata.errata_url}\n`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
md += "\n";
|
|
176
|
+
|
|
177
|
+
// Abstract from metadata
|
|
178
|
+
if (metadata.abstract) {
|
|
179
|
+
md += `## Abstract\n\n${metadata.abstract}\n\n`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
md += "---\n\n";
|
|
183
|
+
} else {
|
|
184
|
+
// No metadata, use simple header
|
|
185
|
+
md += `# RFC ${rfcNumber}\n\n`;
|
|
186
|
+
notes.push("Metadata not available, showing plain text only");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Add full text content
|
|
190
|
+
md += "## Full Text\n\n";
|
|
191
|
+
md += "```\n";
|
|
192
|
+
md += cleanRfcText(textResult.content);
|
|
193
|
+
md += "\n```\n";
|
|
194
|
+
|
|
195
|
+
const output = finalizeOutput(md);
|
|
196
|
+
return {
|
|
197
|
+
url,
|
|
198
|
+
finalUrl: `https://www.rfc-editor.org/rfc/rfc${rfcNumber}`,
|
|
199
|
+
contentType: "text/markdown",
|
|
200
|
+
method: "rfc",
|
|
201
|
+
content: output.content,
|
|
202
|
+
fetchedAt,
|
|
203
|
+
truncated: output.truncated,
|
|
204
|
+
notes: notes.length ? notes : ["Fetched from RFC Editor"],
|
|
205
|
+
};
|
|
206
|
+
} catch {}
|
|
207
|
+
|
|
208
|
+
return null;
|
|
209
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface RubyGemsDependency {
|
|
5
|
+
name: string;
|
|
6
|
+
requirements: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface RubyGemsResponse {
|
|
10
|
+
name: string;
|
|
11
|
+
version: string;
|
|
12
|
+
version_created_at?: string;
|
|
13
|
+
authors?: string;
|
|
14
|
+
info?: string;
|
|
15
|
+
licenses?: string[];
|
|
16
|
+
homepage_uri?: string;
|
|
17
|
+
source_code_uri?: string;
|
|
18
|
+
documentation_uri?: string;
|
|
19
|
+
project_uri?: string;
|
|
20
|
+
downloads: number;
|
|
21
|
+
version_downloads?: number;
|
|
22
|
+
gem_uri?: string;
|
|
23
|
+
dependencies?: {
|
|
24
|
+
development?: RubyGemsDependency[];
|
|
25
|
+
runtime?: RubyGemsDependency[];
|
|
26
|
+
};
|
|
27
|
+
metadata?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Handle RubyGems URLs via API
|
|
32
|
+
*/
|
|
33
|
+
export const handleRubyGems: SpecialHandler = async (
|
|
34
|
+
url: string,
|
|
35
|
+
timeout: number,
|
|
36
|
+
signal?: AbortSignal,
|
|
37
|
+
): Promise<RenderResult | null> => {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = new URL(url);
|
|
40
|
+
if (parsed.hostname !== "rubygems.org" && parsed.hostname !== "www.rubygems.org") return null;
|
|
41
|
+
|
|
42
|
+
// Extract gem name from /gems/{name}
|
|
43
|
+
const match = parsed.pathname.match(/^\/gems\/([^/]+)/);
|
|
44
|
+
if (!match) return null;
|
|
45
|
+
|
|
46
|
+
const gemName = decodeURIComponent(match[1]);
|
|
47
|
+
const fetchedAt = new Date().toISOString();
|
|
48
|
+
|
|
49
|
+
// Fetch from RubyGems API
|
|
50
|
+
const apiUrl = `https://rubygems.org/api/v1/gems/${encodeURIComponent(gemName)}.json`;
|
|
51
|
+
const result = await loadPage(apiUrl, {
|
|
52
|
+
timeout,
|
|
53
|
+
signal,
|
|
54
|
+
headers: { Accept: "application/json" },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!result.ok) return null;
|
|
58
|
+
|
|
59
|
+
let gem: RubyGemsResponse;
|
|
60
|
+
try {
|
|
61
|
+
gem = JSON.parse(result.content);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let md = `# ${gem.name}\n\n`;
|
|
67
|
+
if (gem.info) md += `${gem.info}\n\n`;
|
|
68
|
+
|
|
69
|
+
// Version and license
|
|
70
|
+
md += `**Version:** ${gem.version}`;
|
|
71
|
+
if (gem.licenses?.length) md += ` · **License:** ${gem.licenses.join(", ")}`;
|
|
72
|
+
md += "\n";
|
|
73
|
+
|
|
74
|
+
// Downloads
|
|
75
|
+
md += `**Total Downloads:** ${formatCount(gem.downloads)}`;
|
|
76
|
+
if (gem.version_downloads) md += ` · **Version Downloads:** ${formatCount(gem.version_downloads)}`;
|
|
77
|
+
md += "\n\n";
|
|
78
|
+
|
|
79
|
+
// Links
|
|
80
|
+
if (gem.homepage_uri) md += `**Homepage:** ${gem.homepage_uri}\n`;
|
|
81
|
+
if (gem.source_code_uri) md += `**Source Code:** ${gem.source_code_uri}\n`;
|
|
82
|
+
if (gem.documentation_uri) md += `**Documentation:** ${gem.documentation_uri}\n`;
|
|
83
|
+
if (gem.authors) md += `**Authors:** ${gem.authors}\n`;
|
|
84
|
+
|
|
85
|
+
// Runtime dependencies
|
|
86
|
+
const runtimeDeps = gem.dependencies?.runtime;
|
|
87
|
+
if (runtimeDeps && runtimeDeps.length > 0) {
|
|
88
|
+
md += `\n## Runtime Dependencies\n\n`;
|
|
89
|
+
for (const dep of runtimeDeps) {
|
|
90
|
+
md += `- ${dep.name} ${dep.requirements}\n`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Development dependencies
|
|
95
|
+
const devDeps = gem.dependencies?.development;
|
|
96
|
+
if (devDeps && devDeps.length > 0) {
|
|
97
|
+
md += `\n## Development Dependencies\n\n`;
|
|
98
|
+
for (const dep of devDeps) {
|
|
99
|
+
md += `- ${dep.name} ${dep.requirements}\n`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const output = finalizeOutput(md);
|
|
104
|
+
return {
|
|
105
|
+
url,
|
|
106
|
+
finalUrl: url,
|
|
107
|
+
contentType: "text/markdown",
|
|
108
|
+
method: "rubygems",
|
|
109
|
+
content: output.content,
|
|
110
|
+
fetchedAt,
|
|
111
|
+
truncated: output.truncated,
|
|
112
|
+
notes: ["Fetched via RubyGems API"],
|
|
113
|
+
};
|
|
114
|
+
} catch {}
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
};
|
|
@@ -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
|
+
};
|