@oh-my-pi/pi-coding-agent 12.18.1 → 12.19.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 +47 -0
- package/package.json +7 -7
- package/src/async/index.ts +1 -0
- package/src/async/job-manager.ts +341 -0
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/list-models.ts +3 -17
- package/src/cli/stats-cli.ts +3 -22
- package/src/cli/web-search-cli.ts +8 -16
- package/src/commit/agentic/agent.ts +6 -9
- package/src/commit/agentic/index.ts +44 -50
- package/src/commit/agentic/state.ts +0 -9
- package/src/commit/agentic/tools/propose-commit.ts +1 -30
- package/src/commit/agentic/tools/schemas.ts +31 -0
- package/src/commit/agentic/tools/split-commit.ts +1 -30
- package/src/commit/agentic/validation.ts +1 -18
- package/src/commit/analysis/conventional.ts +3 -50
- package/src/commit/analysis/summary.ts +2 -13
- package/src/commit/changelog/detect.ts +4 -1
- package/src/commit/changelog/generate.ts +2 -25
- package/src/commit/changelog/index.ts +1 -2
- package/src/commit/cli.ts +4 -12
- package/src/commit/map-reduce/reduce-phase.ts +2 -43
- package/src/commit/pipeline.ts +7 -15
- package/src/commit/utils.ts +44 -0
- package/src/config/prompt-templates.ts +1 -81
- package/src/config/settings-schema.ts +20 -1
- package/src/config.ts +2 -3
- package/src/debug/index.ts +1 -6
- package/src/debug/system-info.ts +2 -6
- package/src/discovery/builtin.ts +5 -9
- package/src/discovery/helpers.ts +0 -26
- package/src/discovery/ssh.ts +1 -8
- package/src/exa/company.ts +8 -39
- package/src/exa/factory.ts +64 -0
- package/src/exa/index.ts +0 -16
- package/src/exa/linkedin.ts +8 -39
- package/src/exa/mcp-client.ts +0 -64
- package/src/exa/researcher.ts +17 -59
- package/src/exa/search.ts +30 -154
- package/src/extensibility/custom-tools/loader.ts +3 -41
- package/src/extensibility/extensions/loader.ts +2 -9
- package/src/extensibility/hooks/loader.ts +3 -20
- package/src/extensibility/hooks/runner.ts +3 -19
- package/src/extensibility/plugins/installer.ts +2 -1
- package/src/extensibility/plugins/loader.ts +29 -117
- package/src/extensibility/skills.ts +2 -89
- package/src/extensibility/slash-commands.ts +1 -63
- package/src/extensibility/utils.ts +38 -0
- package/src/index.ts +9 -25
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/jobs-protocol.ts +118 -0
- package/src/ipy/kernel.ts +2 -0
- package/src/lsp/config.ts +1 -5
- package/src/lsp/lspmux.ts +0 -17
- package/src/lsp/utils.ts +2 -24
- package/src/main.ts +16 -24
- package/src/mcp/client.ts +1 -46
- package/src/mcp/render.ts +8 -1
- package/src/mcp/tool-cache.ts +1 -5
- package/src/mcp/transports/http.ts +2 -7
- package/src/mcp/transports/stdio.ts +2 -7
- package/src/modes/components/bash-execution.ts +2 -16
- package/src/modes/components/extensions/inspector-panel.ts +8 -18
- package/src/modes/components/footer.ts +10 -50
- package/src/modes/components/model-selector.ts +2 -21
- package/src/modes/components/python-execution.ts +2 -16
- package/src/modes/components/settings-selector.ts +1 -10
- package/src/modes/components/status-line/segments.ts +8 -25
- package/src/modes/components/status-line.ts +14 -31
- package/src/modes/components/tool-execution.ts +8 -2
- package/src/modes/controllers/command-controller.ts +71 -30
- package/src/modes/controllers/event-controller.ts +34 -4
- package/src/modes/controllers/mcp-command-controller.ts +3 -34
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/controllers/ssh-command-controller.ts +3 -34
- package/src/modes/interactive-mode.ts +6 -2
- package/src/modes/rpc/rpc-client.ts +1 -5
- package/src/modes/shared.ts +73 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +26 -2
- package/src/patch/index.ts +4 -4
- package/src/patch/normalize.ts +22 -65
- package/src/patch/shared.ts +16 -16
- package/src/prompts/system/custom-system-prompt.md +0 -10
- package/src/prompts/system/system-prompt.md +69 -89
- package/src/prompts/tools/async-result.md +5 -0
- package/src/prompts/tools/bash.md +5 -0
- package/src/prompts/tools/cancel-job.md +7 -0
- package/src/prompts/tools/poll-jobs.md +7 -0
- package/src/prompts/tools/task.md +4 -0
- package/src/sdk.ts +70 -6
- package/src/session/agent-session.ts +40 -6
- package/src/session/agent-storage.ts +69 -278
- package/src/session/auth-storage.ts +14 -1430
- package/src/session/session-manager.ts +69 -5
- package/src/session/session-storage.ts +1 -5
- package/src/session/streaming-output.ts +637 -76
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/ssh/connection-manager.ts +4 -12
- package/src/ssh/sshfs-mount.ts +3 -7
- package/src/ssh/utils.ts +8 -0
- package/src/system-prompt.ts +24 -90
- package/src/task/executor.ts +11 -1
- package/src/task/index.ts +258 -13
- package/src/task/parallel.ts +32 -0
- package/src/task/render.ts +15 -7
- package/src/task/types.ts +5 -0
- package/src/tools/ask.ts +4 -7
- package/src/tools/bash-interactive.ts +4 -5
- package/src/tools/bash.ts +125 -41
- package/src/tools/cancel-job.ts +93 -0
- package/src/tools/fetch.ts +7 -27
- package/src/tools/find.ts +3 -3
- package/src/tools/gemini-image.ts +15 -14
- package/src/tools/grep.ts +3 -3
- package/src/tools/index.ts +13 -29
- package/src/tools/json-tree.ts +12 -1
- package/src/tools/jtd-to-json-schema.ts +10 -74
- package/src/tools/jtd-to-typescript.ts +10 -72
- package/src/tools/jtd-utils.ts +102 -0
- package/src/tools/notebook.ts +4 -9
- package/src/tools/output-meta.ts +52 -26
- package/src/tools/path-utils.ts +13 -7
- package/src/tools/poll-jobs.ts +178 -0
- package/src/tools/python.ts +32 -35
- package/src/tools/read.ts +61 -82
- package/src/tools/render-utils.ts +8 -159
- package/src/tools/ssh.ts +7 -20
- package/src/tools/submit-result.ts +1 -1
- package/src/tools/tool-errors.ts +0 -30
- package/src/tools/tool-result.ts +1 -2
- package/src/tools/write.ts +8 -10
- package/src/tui/code-cell.ts +8 -3
- package/src/tui/status-line.ts +4 -4
- package/src/tui/types.ts +0 -1
- package/src/tui/utils.ts +1 -14
- package/src/utils/command-args.ts +76 -0
- package/src/utils/file-mentions.ts +15 -19
- package/src/utils/frontmatter.ts +5 -10
- package/src/utils/shell-snapshot.ts +0 -11
- package/src/utils/title-generator.ts +0 -12
- package/src/web/scrapers/artifacthub.ts +7 -16
- package/src/web/scrapers/arxiv.ts +3 -8
- package/src/web/scrapers/aur.ts +8 -22
- package/src/web/scrapers/biorxiv.ts +5 -14
- package/src/web/scrapers/bluesky.ts +13 -36
- package/src/web/scrapers/brew.ts +5 -10
- package/src/web/scrapers/cheatsh.ts +2 -12
- package/src/web/scrapers/chocolatey.ts +63 -26
- package/src/web/scrapers/choosealicense.ts +3 -18
- package/src/web/scrapers/cisa-kev.ts +4 -18
- package/src/web/scrapers/clojars.ts +6 -33
- package/src/web/scrapers/coingecko.ts +25 -33
- package/src/web/scrapers/crates-io.ts +7 -26
- package/src/web/scrapers/crossref.ts +4 -18
- package/src/web/scrapers/devto.ts +11 -41
- package/src/web/scrapers/discogs.ts +7 -10
- package/src/web/scrapers/discourse.ts +6 -31
- package/src/web/scrapers/dockerhub.ts +12 -35
- package/src/web/scrapers/fdroid.ts +8 -33
- package/src/web/scrapers/firefox-addons.ts +10 -34
- package/src/web/scrapers/flathub.ts +7 -24
- package/src/web/scrapers/github-gist.ts +2 -12
- package/src/web/scrapers/github.ts +9 -47
- package/src/web/scrapers/gitlab.ts +130 -185
- package/src/web/scrapers/go-pkg.ts +12 -22
- package/src/web/scrapers/hackage.ts +88 -43
- package/src/web/scrapers/hackernews.ts +25 -45
- package/src/web/scrapers/hex.ts +19 -36
- package/src/web/scrapers/huggingface.ts +26 -91
- package/src/web/scrapers/iacr.ts +3 -8
- package/src/web/scrapers/jetbrains-marketplace.ts +9 -20
- package/src/web/scrapers/lemmy.ts +5 -23
- package/src/web/scrapers/lobsters.ts +16 -28
- package/src/web/scrapers/mastodon.ts +24 -43
- package/src/web/scrapers/maven.ts +6 -21
- package/src/web/scrapers/mdn.ts +7 -11
- package/src/web/scrapers/metacpan.ts +9 -41
- package/src/web/scrapers/musicbrainz.ts +4 -28
- package/src/web/scrapers/npm.ts +8 -25
- package/src/web/scrapers/nuget.ts +14 -37
- package/src/web/scrapers/nvd.ts +6 -28
- package/src/web/scrapers/ollama.ts +7 -34
- package/src/web/scrapers/open-vsx.ts +5 -19
- package/src/web/scrapers/opencorporates.ts +30 -14
- package/src/web/scrapers/openlibrary.ts +49 -33
- package/src/web/scrapers/orcid.ts +4 -18
- package/src/web/scrapers/osv.ts +7 -24
- package/src/web/scrapers/packagist.ts +9 -24
- package/src/web/scrapers/pub-dev.ts +7 -50
- package/src/web/scrapers/pubmed.ts +54 -21
- package/src/web/scrapers/pypi.ts +8 -26
- package/src/web/scrapers/rawg.ts +11 -19
- package/src/web/scrapers/readthedocs.ts +4 -9
- package/src/web/scrapers/reddit.ts +5 -15
- package/src/web/scrapers/repology.ts +8 -20
- package/src/web/scrapers/rfc.ts +5 -14
- package/src/web/scrapers/rubygems.ts +6 -21
- package/src/web/scrapers/searchcode.ts +8 -36
- package/src/web/scrapers/sec-edgar.ts +4 -18
- package/src/web/scrapers/semantic-scholar.ts +15 -35
- package/src/web/scrapers/snapcraft.ts +5 -19
- package/src/web/scrapers/sourcegraph.ts +5 -43
- package/src/web/scrapers/spdx.ts +4 -18
- package/src/web/scrapers/spotify.ts +4 -23
- package/src/web/scrapers/stackoverflow.ts +8 -13
- package/src/web/scrapers/terraform.ts +9 -37
- package/src/web/scrapers/tldr.ts +3 -7
- package/src/web/scrapers/twitter.ts +3 -7
- package/src/web/scrapers/types.ts +105 -27
- package/src/web/scrapers/utils.ts +97 -103
- package/src/web/scrapers/vimeo.ts +7 -27
- package/src/web/scrapers/vscode-marketplace.ts +8 -17
- package/src/web/scrapers/w3c.ts +6 -14
- package/src/web/scrapers/wikidata.ts +5 -19
- package/src/web/scrapers/wikipedia.ts +2 -12
- package/src/web/scrapers/youtube.ts +5 -34
- package/src/web/search/index.ts +0 -9
- package/src/web/search/providers/anthropic.ts +3 -2
- package/src/web/search/providers/brave.ts +3 -18
- package/src/web/search/providers/exa.ts +1 -12
- package/src/web/search/providers/kimi.ts +5 -44
- package/src/web/search/providers/perplexity.ts +1 -12
- package/src/web/search/providers/synthetic.ts +3 -26
- package/src/web/search/providers/utils.ts +36 -0
- package/src/web/search/providers/zai.ts +9 -50
- package/src/web/search/types.ts +0 -28
- package/src/web/search/utils.ts +17 -0
- package/src/tools/output-utils.ts +0 -63
- package/src/tools/truncate.ts +0 -385
- package/src/web/search/auth.ts +0 -178
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
-
import {
|
|
2
|
+
import { buildResult, formatNumber, loadPage, tryParseJson } from "./types";
|
|
3
3
|
|
|
4
4
|
interface SnapcraftPublisher {
|
|
5
5
|
"display-name"?: string;
|
|
@@ -122,12 +122,8 @@ export const handleSnapcraft: SpecialHandler = async (
|
|
|
122
122
|
});
|
|
123
123
|
if (!result.ok) return null;
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
data = JSON.parse(result.content) as SnapcraftResponse;
|
|
128
|
-
} catch {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
125
|
+
const data = tryParseJson<SnapcraftResponse>(result.content);
|
|
126
|
+
if (!data) return null;
|
|
131
127
|
|
|
132
128
|
const snapInfo = data.snap ?? data;
|
|
133
129
|
const name = snapInfo.title ?? snapInfo.name ?? data.name ?? snapName;
|
|
@@ -163,7 +159,7 @@ export const handleSnapcraft: SpecialHandler = async (
|
|
|
163
159
|
if (base) md += ` · **Base:** ${base}`;
|
|
164
160
|
md += "\n";
|
|
165
161
|
if (publisher) md += `**Publisher:** ${publisher}\n`;
|
|
166
|
-
if (downloads !== null) md += `**Downloads:** ${
|
|
162
|
+
if (downloads !== null) md += `**Downloads:** ${formatNumber(downloads)}\n`;
|
|
167
163
|
md += "\n";
|
|
168
164
|
|
|
169
165
|
if (channels.size > 0) {
|
|
@@ -183,17 +179,7 @@ export const handleSnapcraft: SpecialHandler = async (
|
|
|
183
179
|
md += `## Description\n\n${descriptionText}\n`;
|
|
184
180
|
}
|
|
185
181
|
|
|
186
|
-
|
|
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
|
-
};
|
|
182
|
+
return buildResult(md, { url, method: "snapcraft", fetchedAt, notes: ["Fetched via Snapcraft API"] });
|
|
197
183
|
} catch {}
|
|
198
184
|
|
|
199
185
|
return null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
-
import {
|
|
2
|
+
import { buildResult, loadPage, tryParseJson } from "./types";
|
|
3
3
|
|
|
4
4
|
const GRAPHQL_ENDPOINT = "https://sourcegraph.com/.api/graphql";
|
|
5
5
|
const GRAPHQL_HEADERS = {
|
|
@@ -160,14 +160,6 @@ function parseSourcegraphUrl(url: string): SourcegraphTarget | null {
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
function safeParseJson<T>(content: string): T | null {
|
|
164
|
-
try {
|
|
165
|
-
return JSON.parse(content) as T;
|
|
166
|
-
} catch {
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
163
|
async function fetchGraphql<T>(
|
|
172
164
|
query: string,
|
|
173
165
|
variables: Record<string, unknown>,
|
|
@@ -184,7 +176,7 @@ async function fetchGraphql<T>(
|
|
|
184
176
|
});
|
|
185
177
|
if (!result.ok) return null;
|
|
186
178
|
|
|
187
|
-
const parsed =
|
|
179
|
+
const parsed = tryParseJson<{ data?: T; errors?: unknown }>(result.content);
|
|
188
180
|
if (!parsed?.data) return null;
|
|
189
181
|
if (Array.isArray(parsed.errors) && parsed.errors.length > 0) return null;
|
|
190
182
|
return parsed.data;
|
|
@@ -323,48 +315,18 @@ export const handleSourcegraph: SpecialHandler = async (
|
|
|
323
315
|
case "search": {
|
|
324
316
|
const result = await renderSearch(target.query, timeout, signal);
|
|
325
317
|
if (!result.ok) return null;
|
|
326
|
-
|
|
327
|
-
return {
|
|
328
|
-
url,
|
|
329
|
-
finalUrl: url,
|
|
330
|
-
contentType: "text/markdown",
|
|
331
|
-
method: "sourcegraph-search",
|
|
332
|
-
content: output.content,
|
|
333
|
-
fetchedAt,
|
|
334
|
-
truncated: output.truncated,
|
|
335
|
-
notes,
|
|
336
|
-
};
|
|
318
|
+
return buildResult(result.content, { url, method: "sourcegraph-search", fetchedAt, notes });
|
|
337
319
|
}
|
|
338
320
|
case "file": {
|
|
339
321
|
const rev = target.rev ?? "HEAD";
|
|
340
322
|
const result = await renderFile(target.repoName, target.filePath, rev, timeout, signal);
|
|
341
323
|
if (!result.ok) return null;
|
|
342
|
-
|
|
343
|
-
return {
|
|
344
|
-
url,
|
|
345
|
-
finalUrl: url,
|
|
346
|
-
contentType: "text/markdown",
|
|
347
|
-
method: "sourcegraph-file",
|
|
348
|
-
content: output.content,
|
|
349
|
-
fetchedAt,
|
|
350
|
-
truncated: output.truncated,
|
|
351
|
-
notes,
|
|
352
|
-
};
|
|
324
|
+
return buildResult(result.content, { url, method: "sourcegraph-file", fetchedAt, notes });
|
|
353
325
|
}
|
|
354
326
|
case "repo": {
|
|
355
327
|
const result = await renderRepo(target.repoName, timeout, signal);
|
|
356
328
|
if (!result.ok) return null;
|
|
357
|
-
|
|
358
|
-
return {
|
|
359
|
-
url,
|
|
360
|
-
finalUrl: url,
|
|
361
|
-
contentType: "text/markdown",
|
|
362
|
-
method: "sourcegraph-repo",
|
|
363
|
-
content: output.content,
|
|
364
|
-
fetchedAt,
|
|
365
|
-
truncated: output.truncated,
|
|
366
|
-
notes,
|
|
367
|
-
};
|
|
329
|
+
return buildResult(result.content, { url, method: "sourcegraph-repo", fetchedAt, notes });
|
|
368
330
|
}
|
|
369
331
|
}
|
|
370
332
|
} catch {}
|
package/src/web/scrapers/spdx.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
-
import {
|
|
2
|
+
import { buildResult, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types";
|
|
3
3
|
|
|
4
4
|
interface SpdxCrossRef {
|
|
5
5
|
url?: string;
|
|
@@ -67,12 +67,8 @@ export const handleSpdx: SpecialHandler = async (
|
|
|
67
67
|
|
|
68
68
|
if (!result.ok) return null;
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
license = JSON.parse(result.content);
|
|
73
|
-
} catch {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
70
|
+
const license = tryParseJson<SpdxLicense>(result.content);
|
|
71
|
+
if (!license) return null;
|
|
76
72
|
|
|
77
73
|
const title = license.name || license.licenseId || licenseId;
|
|
78
74
|
let md = `# ${title}\n\n`;
|
|
@@ -104,17 +100,7 @@ export const handleSpdx: SpecialHandler = async (
|
|
|
104
100
|
md += `\n## License Text\n\n\`\`\`\n${licenseText}\n\`\`\`\n`;
|
|
105
101
|
}
|
|
106
102
|
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
url,
|
|
110
|
-
finalUrl: url,
|
|
111
|
-
contentType: "text/markdown",
|
|
112
|
-
method: "spdx-api",
|
|
113
|
-
content: output.content,
|
|
114
|
-
fetchedAt,
|
|
115
|
-
truncated: output.truncated,
|
|
116
|
-
notes: ["Fetched via SPDX license API"],
|
|
117
|
-
};
|
|
103
|
+
return buildResult(md, { url, method: "spdx-api", fetchedAt, notes: ["Fetched via SPDX license API"] });
|
|
118
104
|
} catch {}
|
|
119
105
|
|
|
120
106
|
return null;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* from Spotify URLs without requiring authentication.
|
|
6
6
|
*/
|
|
7
7
|
import type { SpecialHandler } from "./types";
|
|
8
|
-
import {
|
|
8
|
+
import { buildResult, formatMediaDuration, loadPage } from "./types";
|
|
9
9
|
|
|
10
10
|
interface SpotifyOEmbedResponse {
|
|
11
11
|
title?: string;
|
|
@@ -71,21 +71,13 @@ function getContentType(url: string): string | null {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* Format duration from seconds
|
|
74
|
+
* Format duration from seconds string
|
|
75
75
|
*/
|
|
76
76
|
function formatDuration(seconds: string | undefined): string | null {
|
|
77
77
|
if (!seconds) return null;
|
|
78
78
|
const num = parseInt(seconds, 10);
|
|
79
79
|
if (Number.isNaN(num)) return null;
|
|
80
|
-
|
|
81
|
-
const hours = Math.floor(num / 3600);
|
|
82
|
-
const minutes = Math.floor((num % 3600) / 60);
|
|
83
|
-
const secs = num % 60;
|
|
84
|
-
|
|
85
|
-
if (hours > 0) {
|
|
86
|
-
return `${hours}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
87
|
-
}
|
|
88
|
-
return `${minutes}:${secs.toString().padStart(2, "0")}`;
|
|
80
|
+
return formatMediaDuration(num);
|
|
89
81
|
}
|
|
90
82
|
|
|
91
83
|
/**
|
|
@@ -202,16 +194,5 @@ export const handleSpotify: SpecialHandler = async (url: string, timeout: number
|
|
|
202
194
|
|
|
203
195
|
// Format output
|
|
204
196
|
const output = formatOutput(contentType, oEmbedData, ogData, url);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return {
|
|
208
|
-
url,
|
|
209
|
-
finalUrl: url,
|
|
210
|
-
contentType: "text/markdown",
|
|
211
|
-
method: "spotify",
|
|
212
|
-
content,
|
|
213
|
-
fetchedAt: new Date().toISOString(),
|
|
214
|
-
truncated,
|
|
215
|
-
notes,
|
|
216
|
-
};
|
|
197
|
+
return buildResult(output, { url, method: "spotify", fetchedAt: new Date().toISOString(), notes });
|
|
217
198
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
-
import {
|
|
2
|
+
import { buildResult, formatIsoDate, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types";
|
|
3
3
|
|
|
4
4
|
interface SOQuestion {
|
|
5
5
|
title: string;
|
|
@@ -79,8 +79,8 @@ export const handleStackOverflow: SpecialHandler = async (
|
|
|
79
79
|
|
|
80
80
|
if (!qResult.ok) return null;
|
|
81
81
|
|
|
82
|
-
const qData =
|
|
83
|
-
if (!qData
|
|
82
|
+
const qData = tryParseJson<{ items: SOQuestion[] }>(qResult.content);
|
|
83
|
+
if (!qData?.items?.length) return null;
|
|
84
84
|
|
|
85
85
|
const question = qData.items[0];
|
|
86
86
|
|
|
@@ -88,7 +88,7 @@ export const handleStackOverflow: SpecialHandler = async (
|
|
|
88
88
|
md += `**Score:** ${question.score} · **Answers:** ${question.answer_count}`;
|
|
89
89
|
md += question.is_answered ? " (Answered)" : "";
|
|
90
90
|
md += `\n**Tags:** ${question.tags.join(", ")}\n`;
|
|
91
|
-
md += `**Asked by:** ${question.owner.display_name} · ${
|
|
91
|
+
md += `**Asked by:** ${question.owner.display_name} · ${formatIsoDate(question.creation_date * 1000)}\n\n`;
|
|
92
92
|
md += `---\n\n## Question\n\n${htmlToBasicMarkdown(question.body)}\n\n`;
|
|
93
93
|
|
|
94
94
|
// Fetch answers
|
|
@@ -96,8 +96,8 @@ export const handleStackOverflow: SpecialHandler = async (
|
|
|
96
96
|
const aResult = await loadPage(aUrl, { timeout, signal });
|
|
97
97
|
|
|
98
98
|
if (aResult.ok) {
|
|
99
|
-
const aData =
|
|
100
|
-
if (aData
|
|
99
|
+
const aData = tryParseJson<{ items: SOAnswer[] }>(aResult.content);
|
|
100
|
+
if (aData?.items?.length) {
|
|
101
101
|
md += `---\n\n## Answers\n\n`;
|
|
102
102
|
for (const answer of aData.items.slice(0, 5)) {
|
|
103
103
|
const accepted = answer.is_accepted ? " (Accepted)" : "";
|
|
@@ -107,17 +107,12 @@ export const handleStackOverflow: SpecialHandler = async (
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
return {
|
|
110
|
+
return buildResult(md, {
|
|
112
111
|
url,
|
|
113
|
-
finalUrl: url,
|
|
114
|
-
contentType: "text/markdown",
|
|
115
112
|
method: "stackexchange",
|
|
116
|
-
content: output.content,
|
|
117
113
|
fetchedAt,
|
|
118
|
-
truncated: output.truncated,
|
|
119
114
|
notes: [`Fetched via Stack Exchange API (site=${site})`],
|
|
120
|
-
};
|
|
115
|
+
});
|
|
121
116
|
} catch {}
|
|
122
117
|
|
|
123
118
|
return null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
-
import {
|
|
2
|
+
import { buildResult, formatNumber, loadPage, tryParseJson } from "./types";
|
|
3
3
|
|
|
4
4
|
interface TerraformModule {
|
|
5
5
|
id: string;
|
|
@@ -113,12 +113,8 @@ async function handleModuleUrl(
|
|
|
113
113
|
|
|
114
114
|
if (!result.ok) return null;
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
mod = JSON.parse(result.content);
|
|
119
|
-
} catch {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
116
|
+
const mod = tryParseJson<TerraformModule>(result.content);
|
|
117
|
+
if (!mod) return null;
|
|
122
118
|
|
|
123
119
|
let md = `# ${mod.namespace}/${mod.name}/${mod.provider}\n\n`;
|
|
124
120
|
|
|
@@ -128,7 +124,7 @@ async function handleModuleUrl(
|
|
|
128
124
|
md += `**Version:** ${mod.version}`;
|
|
129
125
|
if (mod.verified) md += " ✓ Verified";
|
|
130
126
|
md += `\n`;
|
|
131
|
-
md += `**Downloads:** ${
|
|
127
|
+
md += `**Downloads:** ${formatNumber(mod.downloads)}\n`;
|
|
132
128
|
if (mod.published_at) {
|
|
133
129
|
md += `**Published:** ${new Date(mod.published_at).toLocaleDateString()}\n`;
|
|
134
130
|
}
|
|
@@ -212,17 +208,7 @@ async function handleModuleUrl(
|
|
|
212
208
|
}
|
|
213
209
|
}
|
|
214
210
|
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
url,
|
|
218
|
-
finalUrl: url,
|
|
219
|
-
contentType: "text/markdown",
|
|
220
|
-
method: "terraform",
|
|
221
|
-
content: output.content,
|
|
222
|
-
fetchedAt,
|
|
223
|
-
truncated: output.truncated,
|
|
224
|
-
notes: ["Fetched via Terraform Registry API"],
|
|
225
|
-
};
|
|
211
|
+
return buildResult(md, { url, method: "terraform", fetchedAt, notes: ["Fetched via Terraform Registry API"] });
|
|
226
212
|
}
|
|
227
213
|
|
|
228
214
|
async function handleProviderUrl(
|
|
@@ -242,12 +228,8 @@ async function handleProviderUrl(
|
|
|
242
228
|
|
|
243
229
|
if (!result.ok) return null;
|
|
244
230
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
provider = JSON.parse(result.content);
|
|
248
|
-
} catch {
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
231
|
+
const provider = tryParseJson<TerraformProvider>(result.content);
|
|
232
|
+
if (!provider) return null;
|
|
251
233
|
|
|
252
234
|
let md = `# ${provider.namespace}/${provider.name}\n\n`;
|
|
253
235
|
|
|
@@ -256,7 +238,7 @@ async function handleProviderUrl(
|
|
|
256
238
|
// Metadata
|
|
257
239
|
md += `**Version:** ${provider.version}\n`;
|
|
258
240
|
if (provider.tier) md += `**Tier:** ${provider.tier}\n`;
|
|
259
|
-
md += `**Downloads:** ${
|
|
241
|
+
md += `**Downloads:** ${formatNumber(provider.downloads)}\n`;
|
|
260
242
|
if (provider.published_at) {
|
|
261
243
|
md += `**Published:** ${new Date(provider.published_at).toLocaleDateString()}\n`;
|
|
262
244
|
}
|
|
@@ -290,15 +272,5 @@ async function handleProviderUrl(
|
|
|
290
272
|
}
|
|
291
273
|
}
|
|
292
274
|
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
url,
|
|
296
|
-
finalUrl: url,
|
|
297
|
-
contentType: "text/markdown",
|
|
298
|
-
method: "terraform",
|
|
299
|
-
content: output.content,
|
|
300
|
-
fetchedAt,
|
|
301
|
-
truncated: output.truncated,
|
|
302
|
-
notes: ["Fetched via Terraform Registry API"],
|
|
303
|
-
};
|
|
275
|
+
return buildResult(md, { url, method: "terraform", fetchedAt, notes: ["Fetched via Terraform Registry API"] });
|
|
304
276
|
}
|
package/src/web/scrapers/tldr.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
-
import {
|
|
2
|
+
import { buildResult, loadPage } from "./types";
|
|
3
3
|
|
|
4
4
|
const TLDR_BASE = "https://raw.githubusercontent.com/tldr-pages/tldr/main/pages";
|
|
5
5
|
const PLATFORMS = ["common", "linux", "osx"] as const;
|
|
@@ -30,17 +30,13 @@ export const handleTldr: SpecialHandler = async (
|
|
|
30
30
|
const result = await loadPage(rawUrl, { timeout, signal });
|
|
31
31
|
|
|
32
32
|
if (result.ok && result.content.trim()) {
|
|
33
|
-
|
|
34
|
-
return {
|
|
33
|
+
return buildResult(result.content, {
|
|
35
34
|
url,
|
|
36
35
|
finalUrl: rawUrl,
|
|
37
|
-
contentType: "text/markdown",
|
|
38
36
|
method: "tldr",
|
|
39
|
-
content: output.content,
|
|
40
37
|
fetchedAt,
|
|
41
|
-
truncated: output.truncated,
|
|
42
38
|
notes: [`Fetched from tldr-pages (${platform})`],
|
|
43
|
-
};
|
|
39
|
+
});
|
|
44
40
|
}
|
|
45
41
|
}
|
|
46
42
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parse as parseHtml } from "node-html-parser";
|
|
2
2
|
import { ToolAbortError } from "../../tools/tool-errors";
|
|
3
3
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
4
|
-
import {
|
|
4
|
+
import { buildResult, loadPage } from "./types";
|
|
5
5
|
|
|
6
6
|
const NITTER_INSTANCES = [
|
|
7
7
|
"nitter.privacyredirect.com",
|
|
@@ -58,17 +58,13 @@ export const handleTwitter: SpecialHandler = async (
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
return {
|
|
61
|
+
return buildResult(md, {
|
|
63
62
|
url,
|
|
64
63
|
finalUrl: nitterUrl,
|
|
65
|
-
contentType: "text/markdown",
|
|
66
64
|
method: "twitter-nitter",
|
|
67
|
-
content: output.content,
|
|
68
65
|
fetchedAt,
|
|
69
|
-
truncated: output.truncated,
|
|
70
66
|
notes: [`Via Nitter: ${instance}`],
|
|
71
|
-
};
|
|
67
|
+
});
|
|
72
68
|
}
|
|
73
69
|
}
|
|
74
70
|
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { ptree } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { ToolAbortError } from "../../tools/tool-errors";
|
|
6
6
|
|
|
7
|
+
export { formatNumber } from "@oh-my-pi/pi-utils";
|
|
8
|
+
|
|
7
9
|
export interface RenderResult {
|
|
8
10
|
url: string;
|
|
9
11
|
finalUrl: string;
|
|
@@ -18,7 +20,7 @@ export interface RenderResult {
|
|
|
18
20
|
export type SpecialHandler = (url: string, timeout: number, signal?: AbortSignal) => Promise<RenderResult | null>;
|
|
19
21
|
|
|
20
22
|
export const MAX_OUTPUT_CHARS = 500_000;
|
|
21
|
-
const MAX_BYTES = 50 * 1024 * 1024;
|
|
23
|
+
export const MAX_BYTES = 50 * 1024 * 1024;
|
|
22
24
|
|
|
23
25
|
const USER_AGENTS = [
|
|
24
26
|
"curl/8.0",
|
|
@@ -151,50 +153,126 @@ export async function loadPage(url: string, options: LoadPageOptions = {}): Prom
|
|
|
151
153
|
return { content: "", contentType: "", finalUrl: url, ok: false };
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
/**
|
|
155
|
-
* Format large numbers (1000 -> 1K, 1000000 -> 1M)
|
|
156
|
-
*/
|
|
157
|
-
export function formatCount(n: number): string {
|
|
158
|
-
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
159
|
-
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
160
|
-
return String(n);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
156
|
/**
|
|
164
157
|
* Convert basic HTML to markdown
|
|
165
158
|
*/
|
|
166
159
|
export function htmlToBasicMarkdown(html: string): string {
|
|
167
|
-
|
|
168
|
-
.replace(/<pre
|
|
160
|
+
const stripped = html
|
|
161
|
+
.replace(/<pre[^>]*><code[^>]*>/g, "\n```\n")
|
|
169
162
|
.replace(/<\/code><\/pre>/g, "\n```\n")
|
|
170
|
-
.replace(/<code
|
|
163
|
+
.replace(/<code[^>]*>/g, "`")
|
|
171
164
|
.replace(/<\/code>/g, "`")
|
|
172
|
-
.replace(/<strong
|
|
165
|
+
.replace(/<strong[^>]*>/g, "**")
|
|
173
166
|
.replace(/<\/strong>/g, "**")
|
|
174
|
-
.replace(/<b
|
|
167
|
+
.replace(/<b[^>]*>/g, "**")
|
|
175
168
|
.replace(/<\/b>/g, "**")
|
|
176
|
-
.replace(/<em
|
|
169
|
+
.replace(/<em[^>]*>/g, "*")
|
|
177
170
|
.replace(/<\/em>/g, "*")
|
|
178
|
-
.replace(/<i
|
|
171
|
+
.replace(/<i[^>]*>/g, "*")
|
|
179
172
|
.replace(/<\/i>/g, "*")
|
|
180
|
-
.replace(
|
|
181
|
-
|
|
173
|
+
.replace(
|
|
174
|
+
/<a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g,
|
|
175
|
+
(_, href, text) => `[${text.replace(/<[^>]+>/g, "").trim()}](${href})`,
|
|
176
|
+
)
|
|
177
|
+
.replace(/<p[^>]*>/g, "\n\n")
|
|
182
178
|
.replace(/<\/p>/g, "")
|
|
183
179
|
.replace(/<br\s*\/?>/g, "\n")
|
|
184
|
-
.replace(/<li
|
|
180
|
+
.replace(/<li[^>]*>/g, "- ")
|
|
185
181
|
.replace(/<\/li>/g, "\n")
|
|
186
|
-
.replace(/<\/?[uo]l
|
|
187
|
-
.replace(/<h(\d)
|
|
182
|
+
.replace(/<\/?[uo]l[^>]*>/g, "\n")
|
|
183
|
+
.replace(/<h(\d)[^>]*>/g, (_, n) => `\n${"#".repeat(parseInt(n, 10))} `)
|
|
188
184
|
.replace(/<\/h\d>/g, "\n")
|
|
189
|
-
.replace(/<blockquote
|
|
185
|
+
.replace(/<blockquote[^>]*>/g, "\n> ")
|
|
190
186
|
.replace(/<\/blockquote>/g, "\n")
|
|
191
187
|
.replace(/<[^>]+>/g, "")
|
|
188
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
189
|
+
.trim();
|
|
190
|
+
return decodeHtmlEntities(stripped);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Try to parse JSON, returning null on failure.
|
|
195
|
+
*/
|
|
196
|
+
export function tryParseJson<T = unknown>(content: string): T | null {
|
|
197
|
+
try {
|
|
198
|
+
return JSON.parse(content) as T;
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Build a RenderResult from markdown content. Calls finalizeOutput internally.
|
|
206
|
+
*/
|
|
207
|
+
export function buildResult(
|
|
208
|
+
md: string,
|
|
209
|
+
opts: { url: string; finalUrl?: string; method: string; fetchedAt: string; notes?: string[]; contentType?: string },
|
|
210
|
+
): RenderResult {
|
|
211
|
+
const output = finalizeOutput(md);
|
|
212
|
+
return {
|
|
213
|
+
url: opts.url,
|
|
214
|
+
finalUrl: opts.finalUrl ?? opts.url,
|
|
215
|
+
contentType: opts.contentType ?? "text/markdown",
|
|
216
|
+
method: opts.method,
|
|
217
|
+
content: output.content,
|
|
218
|
+
fetchedAt: opts.fetchedAt,
|
|
219
|
+
truncated: output.truncated,
|
|
220
|
+
notes: opts.notes ?? [],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Format a date value as YYYY-MM-DD. Returns empty string on invalid input.
|
|
226
|
+
*/
|
|
227
|
+
export function formatIsoDate(value?: string | number | Date): string {
|
|
228
|
+
if (value == null) return "";
|
|
229
|
+
if (typeof value === "string") {
|
|
230
|
+
const datePrefix = value.match(/^\d{4}-\d{2}-\d{2}/);
|
|
231
|
+
if (datePrefix) return datePrefix[0];
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
return new Date(value).toISOString().split("T")[0];
|
|
235
|
+
} catch {
|
|
236
|
+
return "";
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Decode common HTML entities.
|
|
242
|
+
*/
|
|
243
|
+
export function decodeHtmlEntities(text: string): string {
|
|
244
|
+
return text
|
|
192
245
|
.replace(/</g, "<")
|
|
193
246
|
.replace(/>/g, ">")
|
|
194
247
|
.replace(/&/g, "&")
|
|
195
248
|
.replace(/"/g, '"')
|
|
196
|
-
.replace(/'/g, "'")
|
|
197
|
-
.replace(
|
|
198
|
-
.replace(
|
|
199
|
-
.
|
|
249
|
+
.replace(/�?39;/g, "'")
|
|
250
|
+
.replace(/'/g, "'")
|
|
251
|
+
.replace(///g, "/")
|
|
252
|
+
.replace(/ /g, " ");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Format seconds into HH:MM:SS or MM:SS.
|
|
257
|
+
*/
|
|
258
|
+
export function formatMediaDuration(totalSeconds: number): string {
|
|
259
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
260
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
261
|
+
const secs = Math.floor(totalSeconds % 60);
|
|
262
|
+
if (hours > 0) return `${hours}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
|
263
|
+
return `${minutes}:${String(secs).padStart(2, "0")}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Extract localized text, preferring en-US/en.
|
|
268
|
+
*/
|
|
269
|
+
export type LocalizedText = string | Record<string, string | null> | null | undefined;
|
|
270
|
+
|
|
271
|
+
export function getLocalizedText(value: LocalizedText, defaultLocale?: string): string | undefined {
|
|
272
|
+
if (value == null) return undefined;
|
|
273
|
+
if (typeof value === "string") return value;
|
|
274
|
+
if (defaultLocale && value[defaultLocale]) return value[defaultLocale];
|
|
275
|
+
return (
|
|
276
|
+
value["en-US"] ?? value.en_US ?? value.en ?? Object.values(value).find(v => typeof v === "string") ?? undefined
|
|
277
|
+
);
|
|
200
278
|
}
|