@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,186 @@
|
|
|
1
|
+
import type { SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Lobste.rs Types
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
interface LobstersStory {
|
|
9
|
+
short_id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
submitter_user: {
|
|
14
|
+
username: string;
|
|
15
|
+
};
|
|
16
|
+
score: number;
|
|
17
|
+
comment_count: number;
|
|
18
|
+
created_at: string;
|
|
19
|
+
tags: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface LobstersComment {
|
|
23
|
+
short_id: string;
|
|
24
|
+
comment: string;
|
|
25
|
+
commenting_user: {
|
|
26
|
+
username: string;
|
|
27
|
+
};
|
|
28
|
+
score: number;
|
|
29
|
+
created_at: string;
|
|
30
|
+
indent_level: number;
|
|
31
|
+
comments?: LobstersComment[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface LobstersStoryResponse {
|
|
35
|
+
short_id: string;
|
|
36
|
+
title: string;
|
|
37
|
+
url?: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
submitter_user: {
|
|
40
|
+
username: string;
|
|
41
|
+
};
|
|
42
|
+
score: number;
|
|
43
|
+
comment_count: number;
|
|
44
|
+
created_at: string;
|
|
45
|
+
tags: string[];
|
|
46
|
+
comments: LobstersComment[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// Handler
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Render comments recursively
|
|
55
|
+
*/
|
|
56
|
+
function renderComments(comments: LobstersComment[], maxDepth = 5): string {
|
|
57
|
+
let md = "";
|
|
58
|
+
for (const comment of comments) {
|
|
59
|
+
if (comment.indent_level >= maxDepth) continue;
|
|
60
|
+
|
|
61
|
+
const indent = " ".repeat(comment.indent_level);
|
|
62
|
+
md += `${indent}### ${comment.commenting_user.username} · ${comment.score} points\n\n`;
|
|
63
|
+
md += `${indent}${comment.comment.split("\n").join(`\n${indent}`)}\n\n`;
|
|
64
|
+
|
|
65
|
+
if (comment.comments && comment.comments.length > 0) {
|
|
66
|
+
md += renderComments(comment.comments, maxDepth);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
md += `${indent}---\n\n`;
|
|
70
|
+
}
|
|
71
|
+
return md;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Handle Lobste.rs URLs via JSON API
|
|
76
|
+
*/
|
|
77
|
+
export const handleLobsters: SpecialHandler = async (url: string, timeout: number, signal?: AbortSignal) => {
|
|
78
|
+
try {
|
|
79
|
+
const parsed = new URL(url);
|
|
80
|
+
if (!parsed.hostname.includes("lobste.rs")) return null;
|
|
81
|
+
|
|
82
|
+
const fetchedAt = new Date().toISOString();
|
|
83
|
+
let jsonUrl = "";
|
|
84
|
+
let md = "";
|
|
85
|
+
|
|
86
|
+
// Story page: lobste.rs/s/{short_id}/{slug}
|
|
87
|
+
const storyMatch = parsed.pathname.match(/^\/s\/([^/]+)/);
|
|
88
|
+
if (storyMatch) {
|
|
89
|
+
jsonUrl = `https://lobste.rs/s/${storyMatch[1]}.json`;
|
|
90
|
+
const result = await loadPage(jsonUrl, { timeout, signal });
|
|
91
|
+
if (!result.ok) return null;
|
|
92
|
+
|
|
93
|
+
const story = JSON.parse(result.content) as LobstersStoryResponse;
|
|
94
|
+
|
|
95
|
+
md = `# ${story.title}\n\n`;
|
|
96
|
+
md += `**${story.submitter_user.username}** · ${story.score} points · ${story.comment_count} comments`;
|
|
97
|
+
if (story.tags.length > 0) {
|
|
98
|
+
md += ` · [${story.tags.join(", ")}]`;
|
|
99
|
+
}
|
|
100
|
+
md += `\n`;
|
|
101
|
+
md += `*${new Date(story.created_at).toISOString().split("T")[0]}*\n\n`;
|
|
102
|
+
|
|
103
|
+
if (story.description) {
|
|
104
|
+
md += `---\n\n${story.description}\n\n`;
|
|
105
|
+
} else if (story.url) {
|
|
106
|
+
md += `**Link:** ${story.url}\n\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add comments
|
|
110
|
+
if (story.comments && story.comments.length > 0) {
|
|
111
|
+
md += `---\n\n## Comments\n\n`;
|
|
112
|
+
md += renderComments(story.comments);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const output = finalizeOutput(md);
|
|
116
|
+
return {
|
|
117
|
+
url,
|
|
118
|
+
finalUrl: jsonUrl,
|
|
119
|
+
contentType: "text/markdown",
|
|
120
|
+
method: "lobsters",
|
|
121
|
+
content: output.content,
|
|
122
|
+
fetchedAt,
|
|
123
|
+
truncated: output.truncated,
|
|
124
|
+
notes: ["Fetched via Lobste.rs JSON API"],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Front page, newest, or tag page
|
|
129
|
+
if (parsed.pathname === "/" || parsed.pathname === "/newest" || parsed.pathname.startsWith("/t/")) {
|
|
130
|
+
if (parsed.pathname === "/") {
|
|
131
|
+
jsonUrl = "https://lobste.rs/hottest.json";
|
|
132
|
+
} else if (parsed.pathname === "/newest") {
|
|
133
|
+
jsonUrl = "https://lobste.rs/newest.json";
|
|
134
|
+
} else {
|
|
135
|
+
const tagMatch = parsed.pathname.match(/^\/t\/([^/]+)/);
|
|
136
|
+
if (tagMatch) {
|
|
137
|
+
jsonUrl = `https://lobste.rs/t/${tagMatch[1]}.json`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!jsonUrl) return null;
|
|
142
|
+
|
|
143
|
+
const result = await loadPage(jsonUrl, { timeout, signal });
|
|
144
|
+
if (!result.ok) return null;
|
|
145
|
+
|
|
146
|
+
const stories = JSON.parse(result.content) as LobstersStory[];
|
|
147
|
+
const listingStories = stories.slice(0, 20);
|
|
148
|
+
|
|
149
|
+
const title =
|
|
150
|
+
parsed.pathname === "/"
|
|
151
|
+
? "Lobste.rs Front Page"
|
|
152
|
+
: parsed.pathname === "/newest"
|
|
153
|
+
? "Lobste.rs Newest"
|
|
154
|
+
: `Lobste.rs Tag: ${parsed.pathname.split("/")[2]}`;
|
|
155
|
+
|
|
156
|
+
md = `# ${title}\n\n`;
|
|
157
|
+
|
|
158
|
+
for (const story of listingStories) {
|
|
159
|
+
md += `- **${story.title}** (${story.score} pts, ${story.comment_count} comments)\n`;
|
|
160
|
+
md += ` by ${story.submitter_user.username}`;
|
|
161
|
+
if (story.tags.length > 0) {
|
|
162
|
+
md += ` · [${story.tags.join(", ")}]`;
|
|
163
|
+
}
|
|
164
|
+
md += `\n`;
|
|
165
|
+
if (story.url) {
|
|
166
|
+
md += ` ${story.url}\n`;
|
|
167
|
+
}
|
|
168
|
+
md += ` https://lobste.rs/s/${story.short_id}\n\n`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const output = finalizeOutput(md);
|
|
172
|
+
return {
|
|
173
|
+
url,
|
|
174
|
+
finalUrl: jsonUrl,
|
|
175
|
+
contentType: "text/markdown",
|
|
176
|
+
method: "lobsters",
|
|
177
|
+
content: output.content,
|
|
178
|
+
fetchedAt,
|
|
179
|
+
truncated: output.truncated,
|
|
180
|
+
notes: ["Fetched via Lobste.rs JSON API"],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
} catch {}
|
|
184
|
+
|
|
185
|
+
return null;
|
|
186
|
+
};
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, htmlToBasicMarkdown, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface MastodonAccount {
|
|
5
|
+
id: string;
|
|
6
|
+
username: string;
|
|
7
|
+
acct: string;
|
|
8
|
+
display_name: string;
|
|
9
|
+
note: string;
|
|
10
|
+
url: string;
|
|
11
|
+
avatar: string;
|
|
12
|
+
header: string;
|
|
13
|
+
followers_count: number;
|
|
14
|
+
following_count: number;
|
|
15
|
+
statuses_count: number;
|
|
16
|
+
created_at: string;
|
|
17
|
+
bot: boolean;
|
|
18
|
+
fields?: Array<{ name: string; value: string }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface MastodonMediaAttachment {
|
|
22
|
+
id: string;
|
|
23
|
+
type: "image" | "video" | "gifv" | "audio" | "unknown";
|
|
24
|
+
url: string;
|
|
25
|
+
preview_url?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface MastodonStatus {
|
|
30
|
+
id: string;
|
|
31
|
+
created_at: string;
|
|
32
|
+
content: string;
|
|
33
|
+
url: string;
|
|
34
|
+
account: MastodonAccount;
|
|
35
|
+
reblogs_count: number;
|
|
36
|
+
favourites_count: number;
|
|
37
|
+
replies_count: number;
|
|
38
|
+
reblog?: MastodonStatus;
|
|
39
|
+
media_attachments: MastodonMediaAttachment[];
|
|
40
|
+
spoiler_text?: string;
|
|
41
|
+
sensitive: boolean;
|
|
42
|
+
visibility: "public" | "unlisted" | "private" | "direct";
|
|
43
|
+
in_reply_to_id?: string;
|
|
44
|
+
poll?: {
|
|
45
|
+
options: Array<{ title: string; votes_count: number }>;
|
|
46
|
+
votes_count: number;
|
|
47
|
+
expired: boolean;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if a domain is a Mastodon instance by probing the API
|
|
53
|
+
*/
|
|
54
|
+
async function isMastodonInstance(hostname: string, timeout: number, signal?: AbortSignal): Promise<boolean> {
|
|
55
|
+
try {
|
|
56
|
+
const result = await loadPage(`https://${hostname}/api/v1/instance`, {
|
|
57
|
+
timeout: Math.min(timeout, 5),
|
|
58
|
+
headers: { Accept: "application/json" },
|
|
59
|
+
signal,
|
|
60
|
+
});
|
|
61
|
+
if (!result.ok) return false;
|
|
62
|
+
const data = JSON.parse(result.content);
|
|
63
|
+
// Mastodon instances return uri/domain field
|
|
64
|
+
return !!(data.uri || data.domain || data.title);
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Format a date string to readable format
|
|
72
|
+
*/
|
|
73
|
+
function formatDate(isoDate: string): string {
|
|
74
|
+
try {
|
|
75
|
+
const date = new Date(isoDate);
|
|
76
|
+
return date.toLocaleDateString("en-US", {
|
|
77
|
+
year: "numeric",
|
|
78
|
+
month: "short",
|
|
79
|
+
day: "numeric",
|
|
80
|
+
hour: "2-digit",
|
|
81
|
+
minute: "2-digit",
|
|
82
|
+
});
|
|
83
|
+
} catch {
|
|
84
|
+
return isoDate;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Format a status/post as markdown
|
|
90
|
+
*/
|
|
91
|
+
function formatStatus(status: MastodonStatus, isReblog = false): string {
|
|
92
|
+
// Handle reblogs (boosts)
|
|
93
|
+
if (status.reblog && !isReblog) {
|
|
94
|
+
let md = `🔁 **${status.account.display_name || status.account.username}** boosted:\n\n`;
|
|
95
|
+
md += formatStatus(status.reblog, true);
|
|
96
|
+
return md;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const account = status.account;
|
|
100
|
+
let md = "";
|
|
101
|
+
|
|
102
|
+
if (!isReblog) {
|
|
103
|
+
md += `# Post by ${account.display_name || account.username}\n\n`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
md += `**@${account.acct}**`;
|
|
107
|
+
if (account.bot) md += " 🤖";
|
|
108
|
+
md += ` · ${formatDate(status.created_at)}`;
|
|
109
|
+
if (status.visibility !== "public") md += ` · ${status.visibility}`;
|
|
110
|
+
md += "\n\n";
|
|
111
|
+
|
|
112
|
+
// Content warning / spoiler
|
|
113
|
+
if (status.spoiler_text) {
|
|
114
|
+
md += `> ⚠️ **CW:** ${status.spoiler_text}\n\n`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Main content (convert HTML to markdown)
|
|
118
|
+
const content = htmlToBasicMarkdown(status.content);
|
|
119
|
+
md += `${content}\n\n`;
|
|
120
|
+
|
|
121
|
+
// Poll
|
|
122
|
+
if (status.poll) {
|
|
123
|
+
md += "**Poll:**\n";
|
|
124
|
+
for (const option of status.poll.options) {
|
|
125
|
+
const pct =
|
|
126
|
+
status.poll.votes_count > 0 ? ((option.votes_count / status.poll.votes_count) * 100).toFixed(1) : "0";
|
|
127
|
+
md += `- ${option.title} (${pct}%, ${option.votes_count} votes)\n`;
|
|
128
|
+
}
|
|
129
|
+
md += `Total: ${status.poll.votes_count} votes${status.poll.expired ? " (closed)" : ""}\n\n`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Media attachments
|
|
133
|
+
if (status.media_attachments.length > 0) {
|
|
134
|
+
md += "**Attachments:**\n";
|
|
135
|
+
for (const media of status.media_attachments) {
|
|
136
|
+
const desc = media.description ? ` - ${media.description}` : "";
|
|
137
|
+
md += `- [${media.type}](${media.url})${desc}\n`;
|
|
138
|
+
}
|
|
139
|
+
md += "\n";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Stats
|
|
143
|
+
md += `---\n`;
|
|
144
|
+
md += `💬 ${formatCount(status.replies_count)} replies · `;
|
|
145
|
+
md += `🔁 ${formatCount(status.reblogs_count)} boosts · `;
|
|
146
|
+
md += `⭐ ${formatCount(status.favourites_count)} favorites\n`;
|
|
147
|
+
|
|
148
|
+
return md;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Format an account/profile as markdown
|
|
153
|
+
*/
|
|
154
|
+
function formatAccount(account: MastodonAccount): string {
|
|
155
|
+
let md = `# ${account.display_name || account.username}\n\n`;
|
|
156
|
+
|
|
157
|
+
md += `**@${account.acct}**`;
|
|
158
|
+
if (account.bot) md += " 🤖 Bot";
|
|
159
|
+
md += "\n\n";
|
|
160
|
+
|
|
161
|
+
// Bio
|
|
162
|
+
if (account.note) {
|
|
163
|
+
const bio = htmlToBasicMarkdown(account.note);
|
|
164
|
+
if (bio && bio !== account.display_name) {
|
|
165
|
+
md += `${bio}\n\n`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Stats
|
|
170
|
+
md += `**Followers:** ${formatCount(account.followers_count)} · `;
|
|
171
|
+
md += `**Following:** ${formatCount(account.following_count)} · `;
|
|
172
|
+
md += `**Posts:** ${formatCount(account.statuses_count)}\n\n`;
|
|
173
|
+
|
|
174
|
+
md += `**Joined:** ${formatDate(account.created_at)}\n`;
|
|
175
|
+
md += `**Profile:** ${account.url}\n`;
|
|
176
|
+
|
|
177
|
+
// Profile fields (links, pronouns, etc.)
|
|
178
|
+
if (account.fields && account.fields.length > 0) {
|
|
179
|
+
md += "\n**Profile Fields:**\n";
|
|
180
|
+
for (const field of account.fields) {
|
|
181
|
+
const value = htmlToBasicMarkdown(field.value);
|
|
182
|
+
md += `- **${field.name}:** ${value}\n`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return md;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Handle Mastodon/Fediverse URLs
|
|
191
|
+
*/
|
|
192
|
+
export const handleMastodon: SpecialHandler = async (
|
|
193
|
+
url: string,
|
|
194
|
+
timeout: number,
|
|
195
|
+
signal?: AbortSignal,
|
|
196
|
+
): Promise<RenderResult | null> => {
|
|
197
|
+
try {
|
|
198
|
+
const parsed = new URL(url);
|
|
199
|
+
|
|
200
|
+
// Check for @user/postid or @user pattern
|
|
201
|
+
const postMatch = parsed.pathname.match(/^\/@([^/]+)\/(\d+)$/);
|
|
202
|
+
const profileMatch = parsed.pathname.match(/^\/@([^/]+)$/);
|
|
203
|
+
|
|
204
|
+
if (!postMatch && !profileMatch) return null;
|
|
205
|
+
|
|
206
|
+
// Verify this is a Mastodon instance
|
|
207
|
+
if (!(await isMastodonInstance(parsed.hostname, timeout, signal))) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const fetchedAt = new Date().toISOString();
|
|
212
|
+
const instance = parsed.hostname;
|
|
213
|
+
|
|
214
|
+
if (postMatch) {
|
|
215
|
+
// Fetch status/post
|
|
216
|
+
const [, , statusId] = postMatch;
|
|
217
|
+
const apiUrl = `https://${instance}/api/v1/statuses/${statusId}`;
|
|
218
|
+
|
|
219
|
+
const result = await loadPage(apiUrl, {
|
|
220
|
+
timeout,
|
|
221
|
+
headers: { Accept: "application/json" },
|
|
222
|
+
signal,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (!result.ok) return null;
|
|
226
|
+
|
|
227
|
+
let status: MastodonStatus;
|
|
228
|
+
try {
|
|
229
|
+
status = JSON.parse(result.content);
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const md = formatStatus(status);
|
|
235
|
+
const output = finalizeOutput(md);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
url,
|
|
239
|
+
finalUrl: status.url || url,
|
|
240
|
+
contentType: "text/markdown",
|
|
241
|
+
method: "mastodon",
|
|
242
|
+
content: output.content,
|
|
243
|
+
fetchedAt,
|
|
244
|
+
truncated: output.truncated,
|
|
245
|
+
notes: [`Fetched via Mastodon API (${instance})`],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (profileMatch) {
|
|
250
|
+
// Fetch account by username lookup
|
|
251
|
+
const [, username] = profileMatch;
|
|
252
|
+
const lookupUrl = `https://${instance}/api/v1/accounts/lookup?acct=${encodeURIComponent(username)}`;
|
|
253
|
+
|
|
254
|
+
const result = await loadPage(lookupUrl, {
|
|
255
|
+
timeout,
|
|
256
|
+
headers: { Accept: "application/json" },
|
|
257
|
+
signal,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (!result.ok) return null;
|
|
261
|
+
|
|
262
|
+
let account: MastodonAccount;
|
|
263
|
+
try {
|
|
264
|
+
account = JSON.parse(result.content);
|
|
265
|
+
} catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Fetch recent statuses
|
|
270
|
+
const statusesUrl = `https://${instance}/api/v1/accounts/${account.id}/statuses?limit=5&exclude_replies=true`;
|
|
271
|
+
const statusesResult = await loadPage(statusesUrl, {
|
|
272
|
+
timeout,
|
|
273
|
+
headers: { Accept: "application/json" },
|
|
274
|
+
signal,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
let md = formatAccount(account);
|
|
278
|
+
|
|
279
|
+
if (statusesResult.ok) {
|
|
280
|
+
try {
|
|
281
|
+
const statuses: MastodonStatus[] = JSON.parse(statusesResult.content);
|
|
282
|
+
if (statuses.length > 0) {
|
|
283
|
+
md += "\n---\n\n## Recent Posts\n\n";
|
|
284
|
+
for (const status of statuses.slice(0, 5)) {
|
|
285
|
+
md += `### ${formatDate(status.created_at)}\n\n`;
|
|
286
|
+
const content = htmlToBasicMarkdown(status.content);
|
|
287
|
+
md += `${content}\n\n`;
|
|
288
|
+
md += `💬 ${status.replies_count} · 🔁 ${status.reblogs_count} · ⭐ ${status.favourites_count}\n\n`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} catch {}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const output = finalizeOutput(md);
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
url,
|
|
298
|
+
finalUrl: account.url || url,
|
|
299
|
+
contentType: "text/markdown",
|
|
300
|
+
method: "mastodon",
|
|
301
|
+
content: output.content,
|
|
302
|
+
fetchedAt,
|
|
303
|
+
truncated: output.truncated,
|
|
304
|
+
notes: [`Fetched via Mastodon API (${instance})`],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
} catch {}
|
|
308
|
+
|
|
309
|
+
return null;
|
|
310
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface MavenDoc {
|
|
5
|
+
id: string;
|
|
6
|
+
g: string; // groupId
|
|
7
|
+
a: string; // artifactId
|
|
8
|
+
latestVersion: string;
|
|
9
|
+
repositoryId: string;
|
|
10
|
+
p: string; // packaging
|
|
11
|
+
timestamp: number;
|
|
12
|
+
versionCount: number;
|
|
13
|
+
text?: string[];
|
|
14
|
+
ec?: string[]; // extensions/classifiers
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface MavenResponse {
|
|
18
|
+
response: {
|
|
19
|
+
numFound: number;
|
|
20
|
+
docs: MavenDoc[];
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handle Maven Central URLs via Solr API
|
|
26
|
+
* Supports: search.maven.org/artifact/... and mvnrepository.com/artifact/...
|
|
27
|
+
*/
|
|
28
|
+
export const handleMaven: SpecialHandler = async (
|
|
29
|
+
url: string,
|
|
30
|
+
timeout: number,
|
|
31
|
+
signal?: AbortSignal,
|
|
32
|
+
): Promise<RenderResult | null> => {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = new URL(url);
|
|
35
|
+
const hostname = parsed.hostname;
|
|
36
|
+
|
|
37
|
+
// Check if this is a Maven URL
|
|
38
|
+
const isSearchMaven = hostname === "search.maven.org";
|
|
39
|
+
const isMvnRepository = hostname === "mvnrepository.com" || hostname === "www.mvnrepository.com";
|
|
40
|
+
|
|
41
|
+
if (!isSearchMaven && !isMvnRepository) return null;
|
|
42
|
+
|
|
43
|
+
let groupId: string | null = null;
|
|
44
|
+
let artifactId: string | null = null;
|
|
45
|
+
let version: string | null = null;
|
|
46
|
+
|
|
47
|
+
if (isSearchMaven) {
|
|
48
|
+
// Pattern: /artifact/{groupId}/{artifactId}[/{version}[/{packaging}]]
|
|
49
|
+
const match = parsed.pathname.match(/^\/artifact\/([^/]+)\/([^/]+)(?:\/([^/]+))?/);
|
|
50
|
+
if (!match) return null;
|
|
51
|
+
groupId = match[1];
|
|
52
|
+
artifactId = match[2];
|
|
53
|
+
version = match[3] || null;
|
|
54
|
+
} else if (isMvnRepository) {
|
|
55
|
+
// Pattern: /artifact/{groupId}/{artifactId}[/{version}]
|
|
56
|
+
const match = parsed.pathname.match(/^\/artifact\/([^/]+)\/([^/]+)(?:\/([^/]+))?/);
|
|
57
|
+
if (!match) return null;
|
|
58
|
+
groupId = match[1];
|
|
59
|
+
artifactId = match[2];
|
|
60
|
+
version = match[3] || null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!groupId || !artifactId) return null;
|
|
64
|
+
|
|
65
|
+
const fetchedAt = new Date().toISOString();
|
|
66
|
+
|
|
67
|
+
// Query Maven Central API
|
|
68
|
+
const apiUrl = `https://search.maven.org/solrsearch/select?q=g:${encodeURIComponent(groupId)}+AND+a:${encodeURIComponent(artifactId)}&wt=json&rows=1`;
|
|
69
|
+
const result = await loadPage(apiUrl, {
|
|
70
|
+
timeout,
|
|
71
|
+
headers: { Accept: "application/json" },
|
|
72
|
+
signal,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!result.ok) return null;
|
|
76
|
+
|
|
77
|
+
let data: MavenResponse;
|
|
78
|
+
try {
|
|
79
|
+
data = JSON.parse(result.content);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (data.response.numFound === 0) return null;
|
|
85
|
+
|
|
86
|
+
const doc = data.response.docs[0];
|
|
87
|
+
const displayVersion = version || doc.latestVersion;
|
|
88
|
+
|
|
89
|
+
let md = `# ${doc.g}:${doc.a}\n\n`;
|
|
90
|
+
md += `**Group ID:** ${doc.g}\n`;
|
|
91
|
+
md += `**Artifact ID:** ${doc.a}\n`;
|
|
92
|
+
md += `**Latest Version:** ${doc.latestVersion}`;
|
|
93
|
+
if (version && version !== doc.latestVersion) {
|
|
94
|
+
md += ` (viewing ${version})`;
|
|
95
|
+
}
|
|
96
|
+
md += "\n";
|
|
97
|
+
|
|
98
|
+
if (doc.p) md += `**Packaging:** ${doc.p}\n`;
|
|
99
|
+
if (doc.versionCount) md += `**Versions:** ${formatCount(doc.versionCount)}\n`;
|
|
100
|
+
if (doc.timestamp) {
|
|
101
|
+
const date = new Date(doc.timestamp);
|
|
102
|
+
md += `**Last Updated:** ${date.toISOString().split("T")[0]}\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add dependency snippets
|
|
106
|
+
md += `\n## Maven Dependency\n\n`;
|
|
107
|
+
md += "```xml\n";
|
|
108
|
+
md += `<dependency>\n`;
|
|
109
|
+
md += ` <groupId>${doc.g}</groupId>\n`;
|
|
110
|
+
md += ` <artifactId>${doc.a}</artifactId>\n`;
|
|
111
|
+
md += ` <version>${displayVersion}</version>\n`;
|
|
112
|
+
md += `</dependency>\n`;
|
|
113
|
+
md += "```\n";
|
|
114
|
+
|
|
115
|
+
md += `\n## Gradle Dependency\n\n`;
|
|
116
|
+
md += "```groovy\n";
|
|
117
|
+
md += `implementation '${doc.g}:${doc.a}:${displayVersion}'\n`;
|
|
118
|
+
md += "```\n";
|
|
119
|
+
|
|
120
|
+
md += `\n## Gradle (Kotlin DSL)\n\n`;
|
|
121
|
+
md += "```kotlin\n";
|
|
122
|
+
md += `implementation("${doc.g}:${doc.a}:${displayVersion}")\n`;
|
|
123
|
+
md += "```\n";
|
|
124
|
+
|
|
125
|
+
// Add available classifiers/extensions if present
|
|
126
|
+
if (doc.ec && doc.ec.length > 0) {
|
|
127
|
+
const extensions = doc.ec.filter((e) => e && e !== "-");
|
|
128
|
+
if (extensions.length > 0) {
|
|
129
|
+
md += `\n## Available Extensions\n\n`;
|
|
130
|
+
md += `${extensions.map((e) => `- ${e}`).join("\n")}\n`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
md += `\n## Links\n\n`;
|
|
135
|
+
md += `- [Maven Central](https://search.maven.org/artifact/${doc.g}/${doc.a}/${displayVersion}/jar)\n`;
|
|
136
|
+
md += `- [MVN Repository](https://mvnrepository.com/artifact/${doc.g}/${doc.a}/${displayVersion})\n`;
|
|
137
|
+
|
|
138
|
+
const output = finalizeOutput(md);
|
|
139
|
+
return {
|
|
140
|
+
url,
|
|
141
|
+
finalUrl: url,
|
|
142
|
+
contentType: "text/markdown",
|
|
143
|
+
method: "maven",
|
|
144
|
+
content: output.content,
|
|
145
|
+
fetchedAt,
|
|
146
|
+
truncated: output.truncated,
|
|
147
|
+
notes: ["Fetched via Maven Central API"],
|
|
148
|
+
};
|
|
149
|
+
} catch {}
|
|
150
|
+
|
|
151
|
+
return null;
|
|
152
|
+
};
|