@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,275 @@
|
|
|
1
|
+
import { parse as parseHtml } from "node-html-parser";
|
|
2
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
|
+
import { finalizeOutput, htmlToBasicMarkdown, loadPage } from "./types";
|
|
4
|
+
|
|
5
|
+
interface GoModuleInfo {
|
|
6
|
+
Version: string;
|
|
7
|
+
Time: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handle pkg.go.dev URLs via proxy API and page parsing
|
|
12
|
+
*/
|
|
13
|
+
export const handleGoPkg: SpecialHandler = async (
|
|
14
|
+
url: string,
|
|
15
|
+
timeout: number,
|
|
16
|
+
signal?: AbortSignal,
|
|
17
|
+
): Promise<RenderResult | null> => {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = new URL(url);
|
|
20
|
+
if (parsed.hostname !== "pkg.go.dev") return null;
|
|
21
|
+
|
|
22
|
+
// Extract module path and version from URL
|
|
23
|
+
// Patterns: /module, /module@version, /module/subpackage
|
|
24
|
+
const pathname = parsed.pathname.slice(1); // remove leading /
|
|
25
|
+
if (!pathname) return null;
|
|
26
|
+
|
|
27
|
+
let modulePath: string;
|
|
28
|
+
let version = "latest";
|
|
29
|
+
let _subpackage = "";
|
|
30
|
+
|
|
31
|
+
// Parse @version if present
|
|
32
|
+
const atIndex = pathname.indexOf("@");
|
|
33
|
+
if (atIndex !== -1) {
|
|
34
|
+
const beforeAt = pathname.slice(0, atIndex);
|
|
35
|
+
const afterAt = pathname.slice(atIndex + 1);
|
|
36
|
+
|
|
37
|
+
// Check if there's a subpackage after version
|
|
38
|
+
const slashIndex = afterAt.indexOf("/");
|
|
39
|
+
if (slashIndex !== -1) {
|
|
40
|
+
version = afterAt.slice(0, slashIndex);
|
|
41
|
+
const remainder = afterAt.slice(slashIndex + 1);
|
|
42
|
+
modulePath = beforeAt;
|
|
43
|
+
_subpackage = remainder;
|
|
44
|
+
} else {
|
|
45
|
+
version = afterAt;
|
|
46
|
+
modulePath = beforeAt;
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
// No version specified, check for subpackage
|
|
50
|
+
// Need to determine where module ends and subpackage begins
|
|
51
|
+
// For now, treat the whole path as module path (we'll refine from proxy response)
|
|
52
|
+
modulePath = pathname;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const notes: string[] = [];
|
|
56
|
+
const sections: string[] = [];
|
|
57
|
+
|
|
58
|
+
// Fetch module info from proxy
|
|
59
|
+
let moduleInfo: GoModuleInfo | null = null;
|
|
60
|
+
let actualModulePath = modulePath;
|
|
61
|
+
|
|
62
|
+
if (version === "latest") {
|
|
63
|
+
try {
|
|
64
|
+
const proxyUrl = `https://proxy.golang.org/${encodeURIComponent(modulePath)}/@latest`;
|
|
65
|
+
const proxyResult = await loadPage(proxyUrl, { timeout, signal });
|
|
66
|
+
|
|
67
|
+
if (proxyResult.ok) {
|
|
68
|
+
moduleInfo = JSON.parse(proxyResult.content) as GoModuleInfo;
|
|
69
|
+
version = moduleInfo.Version;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// If @latest fails, might be a subpackage - will extract from page
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
try {
|
|
76
|
+
const proxyUrl = `https://proxy.golang.org/${encodeURIComponent(modulePath)}/@v/${encodeURIComponent(version)}.info`;
|
|
77
|
+
const proxyResult = await loadPage(proxyUrl, { timeout, signal });
|
|
78
|
+
|
|
79
|
+
if (proxyResult.ok) {
|
|
80
|
+
moduleInfo = JSON.parse(proxyResult.content) as GoModuleInfo;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Proxy lookup failed, will rely on page data
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Fetch the pkg.go.dev page
|
|
88
|
+
const pageResult = await loadPage(url, { timeout, signal });
|
|
89
|
+
if (!pageResult.ok) {
|
|
90
|
+
return {
|
|
91
|
+
url,
|
|
92
|
+
finalUrl: pageResult.finalUrl,
|
|
93
|
+
contentType: "text/plain",
|
|
94
|
+
method: "go-pkg",
|
|
95
|
+
content: `Failed to fetch pkg.go.dev page (status: ${pageResult.status ?? "unknown"})`,
|
|
96
|
+
fetchedAt: new Date().toISOString(),
|
|
97
|
+
truncated: false,
|
|
98
|
+
notes: ["error"],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const doc = parseHtml(pageResult.content);
|
|
103
|
+
|
|
104
|
+
// Extract module/package information
|
|
105
|
+
const breadcrumb = doc.querySelector(".go-Breadcrumb");
|
|
106
|
+
const _headerDiv = doc.querySelector(".go-Main-header");
|
|
107
|
+
|
|
108
|
+
// Extract actual module path from breadcrumb or header
|
|
109
|
+
if (breadcrumb) {
|
|
110
|
+
const moduleLink = breadcrumb.querySelector("a[href^='/']");
|
|
111
|
+
if (moduleLink) {
|
|
112
|
+
const href = moduleLink.getAttribute("href");
|
|
113
|
+
if (href) {
|
|
114
|
+
actualModulePath = href.slice(1).split("@")[0];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Extract version if not from proxy
|
|
120
|
+
if (!moduleInfo) {
|
|
121
|
+
const versionBadge = doc.querySelector(".go-Chip");
|
|
122
|
+
if (versionBadge) {
|
|
123
|
+
const versionText = versionBadge.textContent?.trim();
|
|
124
|
+
if (versionText?.startsWith("v")) {
|
|
125
|
+
version = versionText;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Extract license
|
|
131
|
+
const licenseLink = doc.querySelector("a[data-test-id='UnitHeader-license']");
|
|
132
|
+
const license = licenseLink?.textContent?.trim() || "Unknown";
|
|
133
|
+
|
|
134
|
+
// Extract import path
|
|
135
|
+
const importPathInput = doc.querySelector("input[data-test-id='UnitHeader-importPath']");
|
|
136
|
+
const importPath = importPathInput?.getAttribute("value") || actualModulePath;
|
|
137
|
+
|
|
138
|
+
// Build header
|
|
139
|
+
sections.push(`# ${importPath}`);
|
|
140
|
+
sections.push("");
|
|
141
|
+
sections.push(`**Module:** ${actualModulePath}`);
|
|
142
|
+
sections.push(`**Version:** ${version}`);
|
|
143
|
+
sections.push(`**License:** ${license}`);
|
|
144
|
+
sections.push("");
|
|
145
|
+
|
|
146
|
+
// Extract package synopsis
|
|
147
|
+
const synopsis = doc.querySelector(".go-Main-headerContent p");
|
|
148
|
+
if (synopsis) {
|
|
149
|
+
const synopsisText = synopsis.textContent?.trim();
|
|
150
|
+
if (synopsisText) {
|
|
151
|
+
sections.push(`## Synopsis`);
|
|
152
|
+
sections.push("");
|
|
153
|
+
sections.push(synopsisText);
|
|
154
|
+
sections.push("");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Extract documentation overview
|
|
159
|
+
const docSection = doc.querySelector("#section-documentation");
|
|
160
|
+
if (docSection) {
|
|
161
|
+
sections.push("## Documentation");
|
|
162
|
+
sections.push("");
|
|
163
|
+
|
|
164
|
+
// Get overview paragraph
|
|
165
|
+
const overview = docSection.querySelector(".go-Message");
|
|
166
|
+
if (overview) {
|
|
167
|
+
const overviewMd = htmlToBasicMarkdown(overview.innerHTML);
|
|
168
|
+
sections.push(overviewMd);
|
|
169
|
+
sections.push("");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Get package-level documentation
|
|
173
|
+
const docContent = docSection.querySelector(".Documentation-content");
|
|
174
|
+
if (docContent) {
|
|
175
|
+
// Extract first few paragraphs
|
|
176
|
+
const paragraphs = docContent.querySelectorAll("p");
|
|
177
|
+
const docParts: string[] = [];
|
|
178
|
+
for (let i = 0; i < Math.min(3, paragraphs.length); i++) {
|
|
179
|
+
const p = paragraphs[i];
|
|
180
|
+
const text = htmlToBasicMarkdown(p.innerHTML).trim();
|
|
181
|
+
if (text) {
|
|
182
|
+
docParts.push(text);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (docParts.length > 0) {
|
|
187
|
+
sections.push(docParts.join("\n\n"));
|
|
188
|
+
sections.push("");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Extract index of exported identifiers
|
|
194
|
+
const indexSection = doc.querySelector("#section-index");
|
|
195
|
+
if (indexSection) {
|
|
196
|
+
const indexList = indexSection.querySelector(".Documentation-indexList");
|
|
197
|
+
if (indexList) {
|
|
198
|
+
sections.push("## Index");
|
|
199
|
+
sections.push("");
|
|
200
|
+
|
|
201
|
+
const items = indexList.querySelectorAll("li");
|
|
202
|
+
const exported: string[] = [];
|
|
203
|
+
|
|
204
|
+
for (const item of items) {
|
|
205
|
+
const link = item.querySelector("a");
|
|
206
|
+
if (link) {
|
|
207
|
+
const name = link.textContent?.trim();
|
|
208
|
+
if (name) {
|
|
209
|
+
exported.push(`- ${name}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (exported.length > 0) {
|
|
215
|
+
// Limit to first 50 exports
|
|
216
|
+
sections.push(exported.slice(0, 50).join("\n"));
|
|
217
|
+
if (exported.length > 50) {
|
|
218
|
+
notes.push(`showing 50 of ${exported.length} exports`);
|
|
219
|
+
sections.push(`\n... and ${exported.length - 50} more`);
|
|
220
|
+
}
|
|
221
|
+
sections.push("");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Extract dependencies/imports
|
|
227
|
+
const importsSection = doc.querySelector("#section-imports");
|
|
228
|
+
if (importsSection) {
|
|
229
|
+
const importsList = importsSection.querySelector(".go-Message");
|
|
230
|
+
if (importsList) {
|
|
231
|
+
sections.push("## Imports");
|
|
232
|
+
sections.push("");
|
|
233
|
+
|
|
234
|
+
const links = importsList.querySelectorAll("a");
|
|
235
|
+
const imports: string[] = [];
|
|
236
|
+
|
|
237
|
+
for (const link of links) {
|
|
238
|
+
const imp = link.textContent?.trim();
|
|
239
|
+
if (imp) {
|
|
240
|
+
imports.push(`- ${imp}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (imports.length > 0) {
|
|
245
|
+
sections.push(imports.slice(0, 20).join("\n"));
|
|
246
|
+
if (imports.length > 20) {
|
|
247
|
+
notes.push(`showing 20 of ${imports.length} imports`);
|
|
248
|
+
sections.push(`\n... and ${imports.length - 20} more`);
|
|
249
|
+
}
|
|
250
|
+
sections.push("");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (moduleInfo) {
|
|
256
|
+
notes.push(`published ${moduleInfo.Time}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const content = sections.join("\n");
|
|
260
|
+
const { content: finalContent, truncated } = finalizeOutput(content);
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
url,
|
|
264
|
+
finalUrl: pageResult.finalUrl,
|
|
265
|
+
contentType: "text/markdown",
|
|
266
|
+
method: "go-pkg",
|
|
267
|
+
content: finalContent,
|
|
268
|
+
fetchedAt: new Date().toISOString(),
|
|
269
|
+
truncated,
|
|
270
|
+
notes,
|
|
271
|
+
};
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface HackagePackage {
|
|
5
|
+
name: string;
|
|
6
|
+
synopsis: string;
|
|
7
|
+
description: string;
|
|
8
|
+
license: string;
|
|
9
|
+
author: string;
|
|
10
|
+
maintainer: string;
|
|
11
|
+
version: string;
|
|
12
|
+
homepage?: string;
|
|
13
|
+
"bug-reports"?: string;
|
|
14
|
+
category?: string;
|
|
15
|
+
stability?: string;
|
|
16
|
+
dependencies?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handle Hackage (Haskell package registry) URLs via JSON API
|
|
21
|
+
*/
|
|
22
|
+
export const handleHackage: SpecialHandler = async (
|
|
23
|
+
url: string,
|
|
24
|
+
timeout: number,
|
|
25
|
+
signal?: AbortSignal,
|
|
26
|
+
): Promise<RenderResult | null> => {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = new URL(url);
|
|
29
|
+
if (parsed.hostname !== "hackage.haskell.org") return null;
|
|
30
|
+
|
|
31
|
+
// Match /package/{name} or /package/{name}-{version}
|
|
32
|
+
const match = parsed.pathname.match(/^\/package\/([^/]+)(?:\/|$)/);
|
|
33
|
+
if (!match) return null;
|
|
34
|
+
|
|
35
|
+
const packageId = decodeURIComponent(match[1]);
|
|
36
|
+
const fetchedAt = new Date().toISOString();
|
|
37
|
+
|
|
38
|
+
// Fetch package info with JSON accept header
|
|
39
|
+
const apiUrl = `https://hackage.haskell.org/package/${encodeURIComponent(packageId)}`;
|
|
40
|
+
const result = await loadPage(apiUrl, {
|
|
41
|
+
timeout,
|
|
42
|
+
headers: { Accept: "application/json" },
|
|
43
|
+
signal,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!result.ok) return null;
|
|
47
|
+
|
|
48
|
+
let pkg: HackagePackage;
|
|
49
|
+
try {
|
|
50
|
+
pkg = JSON.parse(result.content);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let md = `# ${pkg.name}\n\n`;
|
|
56
|
+
if (pkg.synopsis) md += `${pkg.synopsis}\n\n`;
|
|
57
|
+
|
|
58
|
+
md += `**Version:** ${pkg.version}`;
|
|
59
|
+
if (pkg.license) md += ` · **License:** ${pkg.license}`;
|
|
60
|
+
md += "\n";
|
|
61
|
+
|
|
62
|
+
if (pkg.author) md += `**Author:** ${pkg.author}\n`;
|
|
63
|
+
if (pkg.maintainer) md += `**Maintainer:** ${pkg.maintainer}\n`;
|
|
64
|
+
if (pkg.category) md += `**Category:** ${pkg.category}\n`;
|
|
65
|
+
if (pkg.stability) md += `**Stability:** ${pkg.stability}\n`;
|
|
66
|
+
if (pkg.homepage) md += `**Homepage:** ${pkg.homepage}\n`;
|
|
67
|
+
if (pkg["bug-reports"]) md += `**Bug Reports:** ${pkg["bug-reports"]}\n`;
|
|
68
|
+
|
|
69
|
+
if (pkg.description) {
|
|
70
|
+
md += `\n## Description\n\n${pkg.description}\n`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) {
|
|
74
|
+
md += `\n## Dependencies\n\n`;
|
|
75
|
+
for (const [dep, version] of Object.entries(pkg.dependencies)) {
|
|
76
|
+
md += `- ${dep}: ${version}\n`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const output = finalizeOutput(md);
|
|
81
|
+
return {
|
|
82
|
+
url,
|
|
83
|
+
finalUrl: url,
|
|
84
|
+
contentType: "text/markdown",
|
|
85
|
+
method: "hackage",
|
|
86
|
+
content: output.content,
|
|
87
|
+
fetchedAt,
|
|
88
|
+
truncated: output.truncated,
|
|
89
|
+
notes: ["Fetched via Hackage API"],
|
|
90
|
+
};
|
|
91
|
+
} catch {}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface HNItem {
|
|
5
|
+
id: number;
|
|
6
|
+
deleted?: boolean;
|
|
7
|
+
type?: "job" | "story" | "comment" | "poll" | "pollopt";
|
|
8
|
+
by?: string;
|
|
9
|
+
time?: number;
|
|
10
|
+
text?: string;
|
|
11
|
+
dead?: boolean;
|
|
12
|
+
parent?: number;
|
|
13
|
+
poll?: number;
|
|
14
|
+
kids?: number[];
|
|
15
|
+
url?: string;
|
|
16
|
+
score?: number;
|
|
17
|
+
title?: string;
|
|
18
|
+
parts?: number[];
|
|
19
|
+
descendants?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const API_BASE = "https://hacker-news.firebaseio.com/v0";
|
|
23
|
+
|
|
24
|
+
async function fetchItem(id: number, timeout: number, signal?: AbortSignal): Promise<HNItem | null> {
|
|
25
|
+
const url = `${API_BASE}/item/${id}.json`;
|
|
26
|
+
const { content, ok } = await loadPage(url, { timeout, signal });
|
|
27
|
+
if (!ok) return null;
|
|
28
|
+
return JSON.parse(content) as HNItem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function fetchItems(ids: number[], timeout: number, limit = 20, signal?: AbortSignal): Promise<HNItem[]> {
|
|
32
|
+
const promises = ids.slice(0, limit).map((id) => fetchItem(id, timeout, signal));
|
|
33
|
+
const results = await Promise.all(promises);
|
|
34
|
+
return results.filter((item): item is HNItem => item !== null && !item.deleted && !item.dead);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function decodeHNText(html: string): string {
|
|
38
|
+
return html
|
|
39
|
+
.replace(/<p>/g, "\n\n")
|
|
40
|
+
.replace(/<\/p>/g, "")
|
|
41
|
+
.replace(/<pre><code>/g, "\n```\n")
|
|
42
|
+
.replace(/<\/code><\/pre>/g, "\n```\n")
|
|
43
|
+
.replace(/<code>/g, "`")
|
|
44
|
+
.replace(/<\/code>/g, "`")
|
|
45
|
+
.replace(/<i>/g, "*")
|
|
46
|
+
.replace(/<\/i>/g, "*")
|
|
47
|
+
.replace(/<a href="([^"]+)"[^>]*>([^<]*)<\/a>/g, "[$2]($1)")
|
|
48
|
+
.replace(/<[^>]+>/g, "")
|
|
49
|
+
.replace(/"/g, '"')
|
|
50
|
+
.replace(/'/g, "'")
|
|
51
|
+
.replace(///g, "/")
|
|
52
|
+
.replace(/</g, "<")
|
|
53
|
+
.replace(/>/g, ">")
|
|
54
|
+
.replace(/&/g, "&")
|
|
55
|
+
.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatTimestamp(unixTime: number): string {
|
|
59
|
+
const date = new Date(unixTime * 1000);
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const diff = now - date.getTime();
|
|
62
|
+
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
63
|
+
const days = Math.floor(hours / 24);
|
|
64
|
+
|
|
65
|
+
if (days > 7) return date.toISOString().split("T")[0];
|
|
66
|
+
if (days > 0) return `${days}d ago`;
|
|
67
|
+
if (hours > 0) return `${hours}h ago`;
|
|
68
|
+
const minutes = Math.floor(diff / (1000 * 60));
|
|
69
|
+
return `${minutes}m ago`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function renderStory(item: HNItem, timeout: number, depth = 0, signal?: AbortSignal): Promise<string> {
|
|
73
|
+
let output = "";
|
|
74
|
+
|
|
75
|
+
if (depth === 0) {
|
|
76
|
+
output += `# ${item.title}\n\n`;
|
|
77
|
+
if (item.url) {
|
|
78
|
+
output += `**URL:** ${item.url}\n\n`;
|
|
79
|
+
}
|
|
80
|
+
output += `**Posted by:** ${item.by} | **Score:** ${item.score ?? 0} | **Time:** ${formatTimestamp(item.time ?? 0)}`;
|
|
81
|
+
if (item.descendants) {
|
|
82
|
+
output += ` | **Comments:** ${item.descendants}`;
|
|
83
|
+
}
|
|
84
|
+
output += "\n\n";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (item.text) {
|
|
88
|
+
output += `${decodeHNText(item.text)}\n\n`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (item.kids && item.kids.length > 0 && depth < 2) {
|
|
92
|
+
const topComments = item.kids.slice(0, depth === 0 ? 20 : 10);
|
|
93
|
+
const comments = await fetchItems(topComments, timeout, topComments.length, signal);
|
|
94
|
+
|
|
95
|
+
if (comments.length > 0) {
|
|
96
|
+
if (depth === 0) output += "---\n\n## Comments\n\n";
|
|
97
|
+
|
|
98
|
+
for (const comment of comments) {
|
|
99
|
+
const indent = " ".repeat(depth);
|
|
100
|
+
output += `${indent}**${comment.by}** (${formatTimestamp(comment.time ?? 0)})`;
|
|
101
|
+
if (comment.score !== undefined) output += ` [${comment.score}]`;
|
|
102
|
+
output += "\n";
|
|
103
|
+
if (comment.text) {
|
|
104
|
+
const text = decodeHNText(comment.text);
|
|
105
|
+
const lines = text.split("\n");
|
|
106
|
+
output += `${lines.map((line) => `${indent}${line}`).join("\n")}\n\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (comment.kids && comment.kids.length > 0 && depth < 1) {
|
|
110
|
+
const childOutput = await renderStory(comment, timeout, depth + 1, signal);
|
|
111
|
+
output += childOutput;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return output;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function renderListing(ids: number[], timeout: number, title: string, signal?: AbortSignal): Promise<string> {
|
|
121
|
+
let output = `# ${title}\n\n`;
|
|
122
|
+
const stories = await fetchItems(ids, timeout, 20, signal);
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < stories.length; i++) {
|
|
125
|
+
const story = stories[i];
|
|
126
|
+
output += `${i + 1}. **${story.title}**\n`;
|
|
127
|
+
if (story.url) {
|
|
128
|
+
output += ` ${story.url}\n`;
|
|
129
|
+
}
|
|
130
|
+
output += ` ${story.score ?? 0} points by ${story.by} | ${formatTimestamp(story.time ?? 0)}`;
|
|
131
|
+
if (story.descendants) {
|
|
132
|
+
output += ` | ${story.descendants} comments`;
|
|
133
|
+
}
|
|
134
|
+
output += `\n https://news.ycombinator.com/item?id=${story.id}\n\n`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return output;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const handleHackerNews: SpecialHandler = async (url, timeout, signal) => {
|
|
141
|
+
const parsed = new URL(url);
|
|
142
|
+
if (!parsed.hostname.includes("news.ycombinator.com")) return null;
|
|
143
|
+
|
|
144
|
+
const notes: string[] = [];
|
|
145
|
+
let content = "";
|
|
146
|
+
const fetchedAt = new Date().toISOString();
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const itemId = parsed.searchParams.get("id");
|
|
150
|
+
|
|
151
|
+
if (itemId) {
|
|
152
|
+
const item = await fetchItem(parseInt(itemId, 10), timeout, signal);
|
|
153
|
+
if (!item) throw new Error(`Failed to fetch item ${itemId}`);
|
|
154
|
+
|
|
155
|
+
content = await renderStory(item, timeout, 0, signal);
|
|
156
|
+
notes.push(`Fetched HN item ${itemId} with top-level comments (depth 2)`);
|
|
157
|
+
} else if (parsed.pathname === "/" || parsed.pathname === "/news") {
|
|
158
|
+
const { content: raw, ok } = await loadPage(`${API_BASE}/topstories.json`, { timeout, signal });
|
|
159
|
+
if (!ok) throw new Error("Failed to fetch top stories");
|
|
160
|
+
const ids = JSON.parse(raw) as number[];
|
|
161
|
+
content = await renderListing(ids, timeout, "Hacker News - Top Stories", signal);
|
|
162
|
+
notes.push("Fetched top 20 stories from HN front page");
|
|
163
|
+
} else if (parsed.pathname === "/newest") {
|
|
164
|
+
const { content: raw, ok } = await loadPage(`${API_BASE}/newstories.json`, { timeout, signal });
|
|
165
|
+
if (!ok) throw new Error("Failed to fetch new stories");
|
|
166
|
+
const ids = JSON.parse(raw) as number[];
|
|
167
|
+
content = await renderListing(ids, timeout, "Hacker News - New Stories", signal);
|
|
168
|
+
notes.push("Fetched top 20 new stories");
|
|
169
|
+
} else if (parsed.pathname === "/best") {
|
|
170
|
+
const { content: raw, ok } = await loadPage(`${API_BASE}/beststories.json`, { timeout, signal });
|
|
171
|
+
if (!ok) throw new Error("Failed to fetch best stories");
|
|
172
|
+
const ids = JSON.parse(raw) as number[];
|
|
173
|
+
content = await renderListing(ids, timeout, "Hacker News - Best Stories", signal);
|
|
174
|
+
notes.push("Fetched top 20 best stories");
|
|
175
|
+
} else {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const { content: finalContent, truncated } = finalizeOutput(content);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
url,
|
|
183
|
+
finalUrl: url,
|
|
184
|
+
contentType: "text/markdown",
|
|
185
|
+
method: "hackernews",
|
|
186
|
+
content: finalContent,
|
|
187
|
+
fetchedAt,
|
|
188
|
+
truncated,
|
|
189
|
+
notes,
|
|
190
|
+
};
|
|
191
|
+
} catch (err) {
|
|
192
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
193
|
+
notes.push(`Error: ${errorMsg}`);
|
|
194
|
+
const { content: finalContent, truncated } = finalizeOutput(
|
|
195
|
+
`# Error fetching Hacker News content\n\n${errorMsg}`,
|
|
196
|
+
);
|
|
197
|
+
return {
|
|
198
|
+
url,
|
|
199
|
+
finalUrl: url,
|
|
200
|
+
contentType: "text/markdown",
|
|
201
|
+
method: "hackernews",
|
|
202
|
+
content: finalContent,
|
|
203
|
+
fetchedAt,
|
|
204
|
+
truncated,
|
|
205
|
+
notes,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handle Hex.pm (Elixir package registry) URLs via API
|
|
6
|
+
*/
|
|
7
|
+
export const handleHex: SpecialHandler = async (url, timeout, signal) => {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = new URL(url);
|
|
10
|
+
if (parsed.hostname !== "hex.pm" && parsed.hostname !== "www.hex.pm") return null;
|
|
11
|
+
|
|
12
|
+
// Extract package name from /packages/name or /packages/name/version
|
|
13
|
+
const match = parsed.pathname.match(/^\/packages\/([^/]+)/);
|
|
14
|
+
if (!match) return null;
|
|
15
|
+
|
|
16
|
+
const packageName = decodeURIComponent(match[1]);
|
|
17
|
+
const fetchedAt = new Date().toISOString();
|
|
18
|
+
|
|
19
|
+
// Fetch from Hex.pm API
|
|
20
|
+
const apiUrl = `https://hex.pm/api/packages/${packageName}`;
|
|
21
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
22
|
+
|
|
23
|
+
if (!result.ok) return null;
|
|
24
|
+
|
|
25
|
+
let data: {
|
|
26
|
+
name: string;
|
|
27
|
+
meta?: {
|
|
28
|
+
description?: string;
|
|
29
|
+
links?: Record<string, string>;
|
|
30
|
+
licenses?: string[];
|
|
31
|
+
};
|
|
32
|
+
releases?: Array<{
|
|
33
|
+
version: string;
|
|
34
|
+
inserted_at: string;
|
|
35
|
+
}>;
|
|
36
|
+
downloads?: {
|
|
37
|
+
all?: number;
|
|
38
|
+
week?: number;
|
|
39
|
+
day?: number;
|
|
40
|
+
};
|
|
41
|
+
latest_version?: string;
|
|
42
|
+
latest_stable_version?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
data = JSON.parse(result.content);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let md = `# ${data.name}\n\n`;
|
|
52
|
+
if (data.meta?.description) md += `${data.meta.description}\n\n`;
|
|
53
|
+
|
|
54
|
+
const version = data.latest_stable_version || data.latest_version || "unknown";
|
|
55
|
+
md += `**Latest:** ${version}`;
|
|
56
|
+
if (data.meta?.licenses?.length) md += ` · **License:** ${data.meta.licenses.join(", ")}`;
|
|
57
|
+
md += "\n";
|
|
58
|
+
|
|
59
|
+
if (data.downloads?.all) {
|
|
60
|
+
md += `**Total Downloads:** ${formatCount(data.downloads.all)}`;
|
|
61
|
+
if (data.downloads.week) md += ` · **This Week:** ${formatCount(data.downloads.week)}`;
|
|
62
|
+
md += "\n";
|
|
63
|
+
}
|
|
64
|
+
md += "\n";
|
|
65
|
+
|
|
66
|
+
if (data.meta?.links && Object.keys(data.meta.links).length > 0) {
|
|
67
|
+
md += `## Links\n\n`;
|
|
68
|
+
for (const [key, value] of Object.entries(data.meta.links)) {
|
|
69
|
+
md += `- **${key}:** ${value}\n`;
|
|
70
|
+
}
|
|
71
|
+
md += "\n";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Fetch releases if available
|
|
75
|
+
if (data.releases?.length) {
|
|
76
|
+
const releasesUrl = `https://hex.pm/api/packages/${packageName}/releases/${version}`;
|
|
77
|
+
const releaseResult = await loadPage(releasesUrl, { timeout: Math.min(timeout, 5), signal });
|
|
78
|
+
|
|
79
|
+
if (releaseResult.ok) {
|
|
80
|
+
try {
|
|
81
|
+
const releaseData = JSON.parse(releaseResult.content) as {
|
|
82
|
+
requirements?: Record<string, { app?: string; optional: boolean; requirement: string }>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (releaseData.requirements && Object.keys(releaseData.requirements).length > 0) {
|
|
86
|
+
md += `## Dependencies (${version})\n\n`;
|
|
87
|
+
for (const [dep, info] of Object.entries(releaseData.requirements)) {
|
|
88
|
+
const optional = info.optional ? " (optional)" : "";
|
|
89
|
+
md += `- ${dep}: ${info.requirement}${optional}\n`;
|
|
90
|
+
}
|
|
91
|
+
md += "\n";
|
|
92
|
+
}
|
|
93
|
+
} catch {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Show recent releases
|
|
97
|
+
const recentReleases = data.releases.slice(0, 10);
|
|
98
|
+
if (recentReleases.length > 0) {
|
|
99
|
+
md += `## Recent Releases\n\n`;
|
|
100
|
+
for (const release of recentReleases) {
|
|
101
|
+
const date = new Date(release.inserted_at).toISOString().split("T")[0];
|
|
102
|
+
md += `- **${release.version}** (${date})\n`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const output = finalizeOutput(md);
|
|
108
|
+
return {
|
|
109
|
+
url,
|
|
110
|
+
finalUrl: url,
|
|
111
|
+
contentType: "text/markdown",
|
|
112
|
+
method: "hex",
|
|
113
|
+
content: output.content,
|
|
114
|
+
fetchedAt,
|
|
115
|
+
truncated: output.truncated,
|
|
116
|
+
notes: ["Fetched via Hex.pm API"],
|
|
117
|
+
};
|
|
118
|
+
} catch {}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
};
|