@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.
Files changed (157) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/package.json +5 -5
  3. package/src/cli/args.ts +4 -0
  4. package/src/core/agent-session.ts +29 -2
  5. package/src/core/bash-executor.ts +2 -1
  6. package/src/core/custom-commands/bundled/review/index.ts +369 -14
  7. package/src/core/custom-commands/bundled/wt/index.ts +1 -1
  8. package/src/core/session-manager.ts +158 -246
  9. package/src/core/session-storage.ts +379 -0
  10. package/src/core/settings-manager.ts +155 -4
  11. package/src/core/system-prompt.ts +62 -64
  12. package/src/core/tools/ask.ts +5 -4
  13. package/src/core/tools/bash-interceptor.ts +26 -61
  14. package/src/core/tools/bash.ts +13 -8
  15. package/src/core/tools/complete.ts +2 -4
  16. package/src/core/tools/edit-diff.ts +11 -4
  17. package/src/core/tools/edit.ts +7 -13
  18. package/src/core/tools/find.ts +111 -50
  19. package/src/core/tools/gemini-image.ts +128 -147
  20. package/src/core/tools/grep.ts +397 -415
  21. package/src/core/tools/index.test.ts +5 -1
  22. package/src/core/tools/index.ts +6 -8
  23. package/src/core/tools/jtd-to-json-schema.ts +174 -196
  24. package/src/core/tools/ls.ts +12 -10
  25. package/src/core/tools/lsp/client.ts +58 -9
  26. package/src/core/tools/lsp/config.ts +205 -656
  27. package/src/core/tools/lsp/defaults.json +465 -0
  28. package/src/core/tools/lsp/index.ts +55 -32
  29. package/src/core/tools/lsp/rust-analyzer.ts +49 -10
  30. package/src/core/tools/lsp/types.ts +1 -0
  31. package/src/core/tools/lsp/utils.ts +1 -1
  32. package/src/core/tools/read.ts +152 -76
  33. package/src/core/tools/render-utils.ts +70 -10
  34. package/src/core/tools/review.ts +38 -126
  35. package/src/core/tools/task/artifacts.ts +5 -4
  36. package/src/core/tools/task/executor.ts +204 -67
  37. package/src/core/tools/task/index.ts +129 -92
  38. package/src/core/tools/task/name-generator.ts +1544 -214
  39. package/src/core/tools/task/parallel.ts +30 -3
  40. package/src/core/tools/task/render.ts +85 -39
  41. package/src/core/tools/task/types.ts +34 -11
  42. package/src/core/tools/task/worker.ts +152 -27
  43. package/src/core/tools/web-fetch.ts +220 -1657
  44. package/src/core/tools/web-scrapers/academic.test.ts +239 -0
  45. package/src/core/tools/web-scrapers/artifacthub.ts +215 -0
  46. package/src/core/tools/web-scrapers/arxiv.ts +88 -0
  47. package/src/core/tools/web-scrapers/aur.ts +175 -0
  48. package/src/core/tools/web-scrapers/biorxiv.ts +141 -0
  49. package/src/core/tools/web-scrapers/bluesky.ts +284 -0
  50. package/src/core/tools/web-scrapers/brew.ts +177 -0
  51. package/src/core/tools/web-scrapers/business.test.ts +82 -0
  52. package/src/core/tools/web-scrapers/cheatsh.ts +78 -0
  53. package/src/core/tools/web-scrapers/chocolatey.ts +158 -0
  54. package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
  55. package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
  56. package/src/core/tools/web-scrapers/clojars.ts +180 -0
  57. package/src/core/tools/web-scrapers/coingecko.ts +184 -0
  58. package/src/core/tools/web-scrapers/crates-io.ts +128 -0
  59. package/src/core/tools/web-scrapers/crossref.ts +149 -0
  60. package/src/core/tools/web-scrapers/dev-platforms.test.ts +254 -0
  61. package/src/core/tools/web-scrapers/devto.ts +177 -0
  62. package/src/core/tools/web-scrapers/discogs.ts +308 -0
  63. package/src/core/tools/web-scrapers/discourse.ts +221 -0
  64. package/src/core/tools/web-scrapers/dockerhub.ts +160 -0
  65. package/src/core/tools/web-scrapers/documentation.test.ts +85 -0
  66. package/src/core/tools/web-scrapers/fdroid.ts +158 -0
  67. package/src/core/tools/web-scrapers/finance-media.test.ts +144 -0
  68. package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
  69. package/src/core/tools/web-scrapers/flathub.ts +239 -0
  70. package/src/core/tools/web-scrapers/git-hosting.test.ts +272 -0
  71. package/src/core/tools/web-scrapers/github-gist.ts +68 -0
  72. package/src/core/tools/web-scrapers/github.ts +455 -0
  73. package/src/core/tools/web-scrapers/gitlab.ts +456 -0
  74. package/src/core/tools/web-scrapers/go-pkg.ts +275 -0
  75. package/src/core/tools/web-scrapers/hackage.ts +94 -0
  76. package/src/core/tools/web-scrapers/hackernews.ts +208 -0
  77. package/src/core/tools/web-scrapers/hex.ts +121 -0
  78. package/src/core/tools/web-scrapers/huggingface.ts +385 -0
  79. package/src/core/tools/web-scrapers/iacr.ts +86 -0
  80. package/src/core/tools/web-scrapers/index.ts +250 -0
  81. package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
  82. package/src/core/tools/web-scrapers/lemmy.ts +220 -0
  83. package/src/core/tools/web-scrapers/lobsters.ts +186 -0
  84. package/src/core/tools/web-scrapers/mastodon.ts +310 -0
  85. package/src/core/tools/web-scrapers/maven.ts +152 -0
  86. package/src/core/tools/web-scrapers/mdn.ts +174 -0
  87. package/src/core/tools/web-scrapers/media.test.ts +138 -0
  88. package/src/core/tools/web-scrapers/metacpan.ts +253 -0
  89. package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
  90. package/src/core/tools/web-scrapers/npm.ts +114 -0
  91. package/src/core/tools/web-scrapers/nuget.ts +205 -0
  92. package/src/core/tools/web-scrapers/nvd.ts +243 -0
  93. package/src/core/tools/web-scrapers/ollama.ts +267 -0
  94. package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
  95. package/src/core/tools/web-scrapers/opencorporates.ts +275 -0
  96. package/src/core/tools/web-scrapers/openlibrary.ts +319 -0
  97. package/src/core/tools/web-scrapers/orcid.ts +299 -0
  98. package/src/core/tools/web-scrapers/osv.ts +189 -0
  99. package/src/core/tools/web-scrapers/package-managers-2.test.ts +199 -0
  100. package/src/core/tools/web-scrapers/package-managers.test.ts +171 -0
  101. package/src/core/tools/web-scrapers/package-registries.test.ts +259 -0
  102. package/src/core/tools/web-scrapers/packagist.ts +174 -0
  103. package/src/core/tools/web-scrapers/pub-dev.ts +185 -0
  104. package/src/core/tools/web-scrapers/pubmed.ts +178 -0
  105. package/src/core/tools/web-scrapers/pypi.ts +129 -0
  106. package/src/core/tools/web-scrapers/rawg.ts +124 -0
  107. package/src/core/tools/web-scrapers/readthedocs.ts +126 -0
  108. package/src/core/tools/web-scrapers/reddit.ts +104 -0
  109. package/src/core/tools/web-scrapers/repology.ts +262 -0
  110. package/src/core/tools/web-scrapers/research.test.ts +107 -0
  111. package/src/core/tools/web-scrapers/rfc.ts +209 -0
  112. package/src/core/tools/web-scrapers/rubygems.ts +117 -0
  113. package/src/core/tools/web-scrapers/searchcode.ts +217 -0
  114. package/src/core/tools/web-scrapers/sec-edgar.ts +274 -0
  115. package/src/core/tools/web-scrapers/security.test.ts +103 -0
  116. package/src/core/tools/web-scrapers/semantic-scholar.ts +190 -0
  117. package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
  118. package/src/core/tools/web-scrapers/social-extended.test.ts +192 -0
  119. package/src/core/tools/web-scrapers/social.test.ts +259 -0
  120. package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
  121. package/src/core/tools/web-scrapers/spdx.ts +121 -0
  122. package/src/core/tools/web-scrapers/spotify.ts +218 -0
  123. package/src/core/tools/web-scrapers/stackexchange.test.ts +120 -0
  124. package/src/core/tools/web-scrapers/stackoverflow.ts +124 -0
  125. package/src/core/tools/web-scrapers/standards.test.ts +122 -0
  126. package/src/core/tools/web-scrapers/terraform.ts +304 -0
  127. package/src/core/tools/web-scrapers/tldr.ts +51 -0
  128. package/src/core/tools/web-scrapers/twitter.ts +96 -0
  129. package/src/core/tools/web-scrapers/types.ts +234 -0
  130. package/src/core/tools/web-scrapers/utils.ts +162 -0
  131. package/src/core/tools/web-scrapers/vimeo.ts +152 -0
  132. package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
  133. package/src/core/tools/web-scrapers/w3c.ts +163 -0
  134. package/src/core/tools/web-scrapers/wikidata.ts +357 -0
  135. package/src/core/tools/web-scrapers/wikipedia.test.ts +73 -0
  136. package/src/core/tools/web-scrapers/wikipedia.ts +95 -0
  137. package/src/core/tools/web-scrapers/youtube.test.ts +198 -0
  138. package/src/core/tools/web-scrapers/youtube.ts +371 -0
  139. package/src/core/tools/write.ts +21 -18
  140. package/src/core/voice.ts +3 -2
  141. package/src/lib/worktree/collapse.ts +2 -1
  142. package/src/lib/worktree/git.ts +2 -18
  143. package/src/main.ts +59 -3
  144. package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
  145. package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
  146. package/src/modes/interactive/components/hook-editor.ts +2 -1
  147. package/src/modes/interactive/components/model-selector.ts +19 -4
  148. package/src/modes/interactive/interactive-mode.ts +41 -38
  149. package/src/modes/interactive/theme/theme.ts +58 -58
  150. package/src/modes/rpc/rpc-mode.ts +10 -9
  151. package/src/prompts/review-request.md +27 -0
  152. package/src/prompts/reviewer.md +64 -68
  153. package/src/prompts/tools/output.md +22 -3
  154. package/src/prompts/tools/task.md +32 -33
  155. package/src/utils/clipboard.ts +2 -1
  156. package/src/utils/tools-manager.ts +110 -8
  157. package/examples/extensions/subagent/agents/reviewer.md +0 -35
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Web Fetch Special Handlers Index
3
+ *
4
+ * Exports all special handlers for site-specific content extraction.
5
+ */
6
+
7
+ import { handleArtifactHub } from "./artifacthub";
8
+ import { handleArxiv } from "./arxiv";
9
+ import { handleAur } from "./aur";
10
+ import { handleBiorxiv } from "./biorxiv";
11
+ import { handleBluesky } from "./bluesky";
12
+ import { handleBrew } from "./brew";
13
+ import { handleCheatSh } from "./cheatsh";
14
+ import { handleChocolatey } from "./chocolatey";
15
+ import { handleChooseALicense } from "./choosealicense";
16
+ import { handleCisaKev } from "./cisa-kev";
17
+ import { handleClojars } from "./clojars";
18
+ import { handleCoinGecko } from "./coingecko";
19
+ import { handleCratesIo } from "./crates-io";
20
+ import { handleCrossref } from "./crossref";
21
+ import { handleDevTo } from "./devto";
22
+ import { handleDiscogs } from "./discogs";
23
+ import { handleDiscourse } from "./discourse";
24
+ import { handleDockerHub } from "./dockerhub";
25
+ import { handleFdroid } from "./fdroid";
26
+ import { handleFirefoxAddons } from "./firefox-addons";
27
+ import { handleFlathub } from "./flathub";
28
+ import { fetchGitHubApi, handleGitHub } from "./github";
29
+ import { handleGitHubGist } from "./github-gist";
30
+ import { handleGitLab } from "./gitlab";
31
+ import { handleGoPkg } from "./go-pkg";
32
+ import { handleHackage } from "./hackage";
33
+ import { handleHackerNews } from "./hackernews";
34
+ import { handleHex } from "./hex";
35
+ import { handleHuggingFace } from "./huggingface";
36
+ import { handleIacr } from "./iacr";
37
+ import { handleJetBrainsMarketplace } from "./jetbrains-marketplace";
38
+ import { handleLemmy } from "./lemmy";
39
+ import { handleLobsters } from "./lobsters";
40
+ import { handleMastodon } from "./mastodon";
41
+ import { handleMaven } from "./maven";
42
+ import { handleMDN } from "./mdn";
43
+ import { handleMetaCPAN } from "./metacpan";
44
+ import { handleMusicBrainz } from "./musicbrainz";
45
+ import { handleNpm } from "./npm";
46
+ import { handleNuGet } from "./nuget";
47
+ import { handleNvd } from "./nvd";
48
+ import { handleOllama } from "./ollama";
49
+ import { handleOpenVsx } from "./open-vsx";
50
+ import { handleOpenCorporates } from "./opencorporates";
51
+ import { handleOpenLibrary } from "./openlibrary";
52
+ import { handleOrcid } from "./orcid";
53
+ import { handleOsv } from "./osv";
54
+ import { handlePackagist } from "./packagist";
55
+ import { handlePubDev } from "./pub-dev";
56
+ import { handlePubMed } from "./pubmed";
57
+ import { handlePyPI } from "./pypi";
58
+ import { handleRawg } from "./rawg";
59
+ import { handleReadTheDocs } from "./readthedocs";
60
+ import { handleReddit } from "./reddit";
61
+ import { handleRepology } from "./repology";
62
+ import { handleRfc } from "./rfc";
63
+ import { handleRubyGems } from "./rubygems";
64
+ import { handleSearchcode } from "./searchcode";
65
+ import { handleSecEdgar } from "./sec-edgar";
66
+ import { handleSemanticScholar } from "./semantic-scholar";
67
+ import { handleSnapcraft } from "./snapcraft";
68
+ import { handleSourcegraph } from "./sourcegraph";
69
+ import { handleSpdx } from "./spdx";
70
+ import { handleSpotify } from "./spotify";
71
+ import { handleStackOverflow } from "./stackoverflow";
72
+ import { handleTerraform } from "./terraform";
73
+ import { handleTldr } from "./tldr";
74
+ import { handleTwitter } from "./twitter";
75
+ import type { SpecialHandler } from "./types";
76
+ import { handleVimeo } from "./vimeo";
77
+ import { handleVscodeMarketplace } from "./vscode-marketplace";
78
+ import { handleW3c } from "./w3c";
79
+ import { handleWikidata } from "./wikidata";
80
+ import { handleWikipedia } from "./wikipedia";
81
+ import { handleYouTube } from "./youtube";
82
+
83
+ export type { RenderResult, SpecialHandler } from "./types";
84
+
85
+ export {
86
+ fetchGitHubApi,
87
+ handleArtifactHub,
88
+ handleArxiv,
89
+ handleAur,
90
+ handleBiorxiv,
91
+ handleBluesky,
92
+ handleBrew,
93
+ handleCheatSh,
94
+ handleCisaKev,
95
+ handleChocolatey,
96
+ handleClojars,
97
+ handleChooseALicense,
98
+ handleCoinGecko,
99
+ handleCratesIo,
100
+ handleCrossref,
101
+ handleDevTo,
102
+ handleDiscogs,
103
+ handleDiscourse,
104
+ handleDockerHub,
105
+ handleFdroid,
106
+ handleFlathub,
107
+ handleFirefoxAddons,
108
+ handleGitHub,
109
+ handleGitHubGist,
110
+ handleGitLab,
111
+ handleGoPkg,
112
+ handleHackage,
113
+ handleHackerNews,
114
+ handleHex,
115
+ handleHuggingFace,
116
+ handleIacr,
117
+ handleJetBrainsMarketplace,
118
+ handleLemmy,
119
+ handleLobsters,
120
+ handleMastodon,
121
+ handleMaven,
122
+ handleMDN,
123
+ handleMetaCPAN,
124
+ handleMusicBrainz,
125
+ handleNpm,
126
+ handleNuGet,
127
+ handleNvd,
128
+ handleOllama,
129
+ handleOpenCorporates,
130
+ handleOpenLibrary,
131
+ handleOrcid,
132
+ handleOpenVsx,
133
+ handleOsv,
134
+ handlePackagist,
135
+ handlePubDev,
136
+ handlePubMed,
137
+ handlePyPI,
138
+ handleRawg,
139
+ handleReadTheDocs,
140
+ handleReddit,
141
+ handleRepology,
142
+ handleRfc,
143
+ handleRubyGems,
144
+ handleSecEdgar,
145
+ handleSearchcode,
146
+ handleSemanticScholar,
147
+ handleSnapcraft,
148
+ handleSourcegraph,
149
+ handleSpotify,
150
+ handleSpdx,
151
+ handleStackOverflow,
152
+ handleTerraform,
153
+ handleTldr,
154
+ handleTwitter,
155
+ handleVimeo,
156
+ handleVscodeMarketplace,
157
+ handleW3c,
158
+ handleWikidata,
159
+ handleWikipedia,
160
+ handleYouTube,
161
+ };
162
+
163
+ export const specialHandlers: SpecialHandler[] = [
164
+ // Git hosting
165
+ handleGitHubGist,
166
+ handleGitHub,
167
+ handleGitLab,
168
+ // Video/Media
169
+ handleYouTube,
170
+ handleVimeo,
171
+ handleSpotify,
172
+ handleDiscogs,
173
+ handleMusicBrainz,
174
+ // Games
175
+ handleRawg,
176
+ // Social/News
177
+ handleTwitter,
178
+ handleBluesky,
179
+ handleMastodon,
180
+ handleLemmy,
181
+ handleHackerNews,
182
+ handleLobsters,
183
+ handleReddit,
184
+ handleDiscourse,
185
+ // Developer content
186
+ handleStackOverflow,
187
+ handleDevTo,
188
+ handleMDN,
189
+ handleReadTheDocs,
190
+ handleSearchcode,
191
+ handleSourcegraph,
192
+ handleTldr,
193
+ handleCheatSh,
194
+ // Package registries
195
+ handleNpm,
196
+ handleFirefoxAddons,
197
+ handleVscodeMarketplace,
198
+ handleNuGet,
199
+ handleChocolatey,
200
+ handleClojars,
201
+ handleBrew,
202
+ handlePyPI,
203
+ handleCratesIo,
204
+ handleDockerHub,
205
+ handleFdroid,
206
+ handleFlathub,
207
+ handleGoPkg,
208
+ handleHex,
209
+ handlePackagist,
210
+ handlePubDev,
211
+ handleMaven,
212
+ handleJetBrainsMarketplace,
213
+ handleOpenVsx,
214
+ handleArtifactHub,
215
+ handleRubyGems,
216
+ handleTerraform,
217
+ handleAur,
218
+ handleHackage,
219
+ handleMetaCPAN,
220
+ handleRepology,
221
+ handleSnapcraft,
222
+ // ML/AI
223
+ handleHuggingFace,
224
+ handleOllama,
225
+ // Academic
226
+ handleArxiv,
227
+ handleBiorxiv,
228
+ handleCrossref,
229
+ handleIacr,
230
+ handleOrcid,
231
+ handleSemanticScholar,
232
+ handlePubMed,
233
+ handleRfc,
234
+ // Security
235
+ handleCisaKev,
236
+ handleNvd,
237
+ handleOsv,
238
+ // Crypto
239
+ handleCoinGecko,
240
+ // Business
241
+ handleOpenCorporates,
242
+ handleSecEdgar,
243
+ // Reference
244
+ handleOpenLibrary,
245
+ handleChooseALicense,
246
+ handleW3c,
247
+ handleSpdx,
248
+ handleWikidata,
249
+ handleWikipedia,
250
+ ];
@@ -0,0 +1,169 @@
1
+ import type { RenderResult, SpecialHandler } from "./types";
2
+ import { finalizeOutput, formatCount, htmlToBasicMarkdown, loadPage } from "./types";
3
+
4
+ interface PluginVendor {
5
+ name?: string;
6
+ publicName?: string;
7
+ url?: string;
8
+ }
9
+
10
+ interface PluginTag {
11
+ name?: string;
12
+ }
13
+
14
+ type PluginRating =
15
+ | number
16
+ | {
17
+ rating?: number;
18
+ value?: number;
19
+ score?: number;
20
+ votes?: number;
21
+ totalVotes?: number;
22
+ count?: number;
23
+ };
24
+
25
+ interface PluginData {
26
+ id?: number;
27
+ name?: string;
28
+ description?: string;
29
+ preview?: string;
30
+ vendor?: PluginVendor;
31
+ rating?: PluginRating;
32
+ ratingCount?: number;
33
+ downloads?: number;
34
+ tags?: PluginTag[];
35
+ urls?: {
36
+ url?: string;
37
+ docUrl?: string;
38
+ sourceCodeUrl?: string;
39
+ bugtrackerUrl?: string;
40
+ };
41
+ }
42
+
43
+ interface UpdateData {
44
+ version?: string;
45
+ since?: string;
46
+ until?: string;
47
+ sinceUntil?: string;
48
+ channel?: string;
49
+ downloads?: number;
50
+ compatibleVersions?: Record<string, string>;
51
+ cdate?: string | number;
52
+ }
53
+
54
+ const MARKETPLACE_HOSTS = new Set(["plugins.jetbrains.com"]);
55
+
56
+ function extractRating(plugin: PluginData): { value: number | null; votes: number | null } {
57
+ const rating = plugin.rating;
58
+ if (typeof rating === "number" && Number.isFinite(rating)) {
59
+ return { value: rating, votes: plugin.ratingCount ?? null };
60
+ }
61
+ if (rating && typeof rating === "object") {
62
+ const value = rating.rating ?? rating.value ?? rating.score ?? null;
63
+ const votes = rating.votes ?? rating.totalVotes ?? rating.count ?? plugin.ratingCount ?? null;
64
+ return { value: typeof value === "number" ? value : null, votes: typeof votes === "number" ? votes : null };
65
+ }
66
+ return { value: null, votes: plugin.ratingCount ?? null };
67
+ }
68
+
69
+ function formatBuildCompatibility(update: UpdateData): string | null {
70
+ if (update.sinceUntil) return update.sinceUntil;
71
+ if (update.since && update.until) return `${update.since} - ${update.until}`;
72
+ if (update.since) return `${update.since}+`;
73
+ return null;
74
+ }
75
+
76
+ export const handleJetBrainsMarketplace: SpecialHandler = async (
77
+ url: string,
78
+ timeout: number,
79
+ signal?: AbortSignal,
80
+ ): Promise<RenderResult | null> => {
81
+ try {
82
+ const parsed = new URL(url);
83
+ if (!MARKETPLACE_HOSTS.has(parsed.hostname)) return null;
84
+
85
+ const match = parsed.pathname.match(/^\/plugin\/(\d+)(?:-[^/]+)?(?:\/|$)/);
86
+ if (!match) return null;
87
+
88
+ const pluginId = match[1];
89
+ const fetchedAt = new Date().toISOString();
90
+
91
+ const pluginUrl = `https://plugins.jetbrains.com/api/plugins/${pluginId}`;
92
+ const updatesUrl = `https://plugins.jetbrains.com/api/plugins/${pluginId}/updates?size=1`;
93
+
94
+ const [pluginResult, updatesResult] = await Promise.all([
95
+ loadPage(pluginUrl, { timeout, signal }),
96
+ loadPage(updatesUrl, { timeout, signal }),
97
+ ]);
98
+
99
+ if (!pluginResult.ok || !updatesResult.ok) return null;
100
+
101
+ let plugin: PluginData;
102
+ let updates: UpdateData[];
103
+
104
+ try {
105
+ plugin = JSON.parse(pluginResult.content) as PluginData;
106
+ updates = JSON.parse(updatesResult.content) as UpdateData[];
107
+ } catch {
108
+ return null;
109
+ }
110
+
111
+ const update = updates[0];
112
+ if (!plugin?.name) return null;
113
+
114
+ const vendorName = plugin.vendor?.name ?? plugin.vendor?.publicName;
115
+ const descriptionSource = plugin.description ?? plugin.preview ?? "";
116
+ const description = descriptionSource ? htmlToBasicMarkdown(descriptionSource) : "";
117
+ const tags = (plugin.tags ?? []).map((tag) => tag.name).filter(Boolean) as string[];
118
+ const rating = extractRating(plugin);
119
+ const buildCompatibility = update ? formatBuildCompatibility(update) : null;
120
+
121
+ let md = `# ${plugin.name}\n\n`;
122
+ if (description) md += `${description}\n\n`;
123
+
124
+ md += `**Plugin ID:** ${pluginId}\n`;
125
+ if (vendorName) md += `**Vendor:** ${vendorName}\n`;
126
+ if (plugin.downloads !== undefined) {
127
+ md += `**Downloads:** ${formatCount(plugin.downloads)}\n`;
128
+ }
129
+ if (rating.value !== null) {
130
+ md += `**Rating:** ${rating.value.toFixed(2)}`;
131
+ if (rating.votes !== null) md += ` (${formatCount(rating.votes)} votes)`;
132
+ md += "\n";
133
+ }
134
+ if (tags.length) md += `**Tags:** ${tags.join(", ")}\n`;
135
+
136
+ if (update) {
137
+ md += "\n## Latest Release\n\n";
138
+ if (update.version) md += `**Version:** ${update.version}\n`;
139
+ if (update.channel) md += `**Channel:** ${update.channel}\n`;
140
+ if (buildCompatibility) md += `**Build Compatibility:** ${buildCompatibility}\n`;
141
+ if (update.downloads !== undefined) {
142
+ md += `**Release Downloads:** ${formatCount(update.downloads)}\n`;
143
+ }
144
+ }
145
+
146
+ const compatibility = update?.compatibleVersions ?? {};
147
+ const compatibilityEntries = Object.entries(compatibility).sort(([a], [b]) => a.localeCompare(b));
148
+ if (compatibilityEntries.length) {
149
+ md += "\n## IDE Compatibility\n\n";
150
+ for (const [product, version] of compatibilityEntries) {
151
+ md += `- ${product}: ${version}\n`;
152
+ }
153
+ }
154
+
155
+ const output = finalizeOutput(md);
156
+ return {
157
+ url,
158
+ finalUrl: url,
159
+ contentType: "text/markdown",
160
+ method: "jetbrains-marketplace",
161
+ content: output.content,
162
+ fetchedAt,
163
+ truncated: output.truncated,
164
+ notes: ["Fetched via JetBrains Marketplace API"],
165
+ };
166
+ } catch {}
167
+
168
+ return null;
169
+ };
@@ -0,0 +1,220 @@
1
+ import type { RenderResult, SpecialHandler } from "./types";
2
+ import { finalizeOutput, loadPage } from "./types";
3
+
4
+ interface LemmyCreator {
5
+ name: string;
6
+ actor_id?: string;
7
+ }
8
+
9
+ interface LemmyCommunity {
10
+ name: string;
11
+ actor_id?: string;
12
+ }
13
+
14
+ interface LemmyCounts {
15
+ score: number;
16
+ comments?: number;
17
+ }
18
+
19
+ interface LemmyPost {
20
+ id: number;
21
+ name: string;
22
+ body?: string;
23
+ url?: string;
24
+ }
25
+
26
+ interface LemmyPostView {
27
+ post: LemmyPost;
28
+ creator: LemmyCreator;
29
+ community: LemmyCommunity;
30
+ counts: LemmyCounts;
31
+ }
32
+
33
+ interface LemmyPostResponse {
34
+ post_view?: LemmyPostView;
35
+ }
36
+
37
+ interface LemmyComment {
38
+ id: number;
39
+ content?: string;
40
+ path?: string;
41
+ parent_id?: number | null;
42
+ post_id?: number;
43
+ }
44
+
45
+ interface LemmyCommentView {
46
+ comment: LemmyComment;
47
+ creator: LemmyCreator;
48
+ counts: LemmyCounts;
49
+ }
50
+
51
+ interface LemmyCommentListResponse {
52
+ comments?: LemmyCommentView[];
53
+ }
54
+
55
+ interface LemmyCommentResponse {
56
+ comment_view?: LemmyCommentView;
57
+ }
58
+
59
+ function parseJson<T>(content: string): T | null {
60
+ try {
61
+ return JSON.parse(content) as T;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function formatCommunity(community: LemmyCommunity): string {
68
+ if (community.actor_id) {
69
+ try {
70
+ const host = new URL(community.actor_id).hostname;
71
+ return `!${community.name}@${host}`;
72
+ } catch {}
73
+ }
74
+ return `!${community.name}`;
75
+ }
76
+
77
+ function formatAuthor(creator: LemmyCreator): string {
78
+ if (creator.actor_id) {
79
+ try {
80
+ const host = new URL(creator.actor_id).hostname;
81
+ return `@${creator.name}@${host}`;
82
+ } catch {}
83
+ }
84
+ return creator.name;
85
+ }
86
+
87
+ function indentBlock(text: string, indent: string): string {
88
+ return text
89
+ .split("\n")
90
+ .map((line) => `${indent}${line}`)
91
+ .join("\n");
92
+ }
93
+
94
+ function renderComments(comments: LemmyCommentView[]): string {
95
+ const childrenByParent = new Map<number, LemmyCommentView[]>();
96
+
97
+ const commentIds = new Set(comments.map((view) => view.comment.id));
98
+
99
+ for (const commentView of comments) {
100
+ const parentId = commentView.comment.parent_id;
101
+ const resolvedParent = parentId && commentIds.has(parentId) ? parentId : 0;
102
+ const list = childrenByParent.get(resolvedParent);
103
+ if (list) {
104
+ list.push(commentView);
105
+ } else {
106
+ childrenByParent.set(resolvedParent, [commentView]);
107
+ }
108
+ }
109
+
110
+ const renderThread = (parentId: number, depth: number): string => {
111
+ const items = childrenByParent.get(parentId) ?? [];
112
+ let output = "";
113
+
114
+ for (const view of items) {
115
+ const author = view.creator?.name ? formatAuthor(view.creator) : "unknown";
116
+ const score = view.counts?.score ?? 0;
117
+ const content = (view.comment.content ?? "").trim();
118
+ const indent = " ".repeat(depth);
119
+
120
+ output += `${indent}- **${author}** · ${score} points\n`;
121
+ if (content) {
122
+ output += `${indentBlock(content, `${indent} `)}\n`;
123
+ }
124
+
125
+ output += renderThread(view.comment.id, depth + 1);
126
+ output += "\n";
127
+ }
128
+
129
+ return output;
130
+ };
131
+
132
+ return renderThread(0, 0).trim();
133
+ }
134
+
135
+ export const handleLemmy: SpecialHandler = async (
136
+ url: string,
137
+ timeout: number,
138
+ signal?: AbortSignal,
139
+ ): Promise<RenderResult | null> => {
140
+ try {
141
+ const parsed = new URL(url);
142
+ const match = parsed.pathname.match(/^\/(post|comment)\/(\d+)/);
143
+ if (!match) return null;
144
+
145
+ const kind = match[1];
146
+ const id = Number.parseInt(match[2], 10);
147
+ if (!Number.isFinite(id)) return null;
148
+
149
+ const baseUrl = parsed.origin;
150
+ const fetchedAt = new Date().toISOString();
151
+
152
+ let postId = id;
153
+ if (kind === "comment") {
154
+ const commentUrl = `${baseUrl}/api/v3/comment?id=${id}`;
155
+ const commentResult = await loadPage(commentUrl, { timeout, signal });
156
+ if (!commentResult.ok) return null;
157
+
158
+ const commentData = parseJson<LemmyCommentResponse>(commentResult.content);
159
+ const commentView = commentData?.comment_view;
160
+ const commentPostId = commentView?.comment?.post_id;
161
+ if (!commentPostId) return null;
162
+ postId = commentPostId;
163
+ }
164
+
165
+ const postUrl = `${baseUrl}/api/v3/post?id=${postId}`;
166
+ const commentsUrl = `${baseUrl}/api/v3/comment/list?post_id=${postId}`;
167
+
168
+ const [postResult, commentsResult] = await Promise.all([
169
+ loadPage(postUrl, { timeout, signal }),
170
+ loadPage(commentsUrl, { timeout, signal }),
171
+ ]);
172
+
173
+ if (!postResult.ok || !commentsResult.ok) return null;
174
+
175
+ const postData = parseJson<LemmyPostResponse>(postResult.content);
176
+ const postView = postData?.post_view;
177
+ if (!postView) return null;
178
+
179
+ const commentsData = parseJson<LemmyCommentListResponse>(commentsResult.content);
180
+ const comments = commentsData?.comments ?? [];
181
+
182
+ let md = `# ${postView.post.name}\n\n`;
183
+
184
+ const communityLabel = formatCommunity(postView.community);
185
+ const authorLabel = formatAuthor(postView.creator);
186
+ const score = postView.counts?.score ?? 0;
187
+ const commentCount = postView.counts?.comments ?? comments.length;
188
+
189
+ md += `**Community:** ${communityLabel} · **Author:** ${authorLabel} · **Score:** ${score} · **Comments:** ${commentCount}\n`;
190
+ if (postView.post.url) {
191
+ md += `**Link:** ${postView.post.url}\n`;
192
+ }
193
+ md += "\n";
194
+
195
+ if (postView.post.body) {
196
+ md += `---\n\n${postView.post.body}\n\n`;
197
+ }
198
+
199
+ if (comments.length > 0) {
200
+ const threadedComments = renderComments(comments);
201
+ if (threadedComments) {
202
+ md += `---\n\n## Comments\n\n${threadedComments}\n`;
203
+ }
204
+ }
205
+
206
+ const output = finalizeOutput(md);
207
+ return {
208
+ url,
209
+ finalUrl: url,
210
+ contentType: "text/markdown",
211
+ method: "lemmy-api",
212
+ content: output.content,
213
+ fetchedAt,
214
+ truncated: output.truncated,
215
+ notes: ["Fetched via Lemmy API"],
216
+ };
217
+ } catch {}
218
+
219
+ return null;
220
+ };