@oh-my-pi/pi-coding-agent 3.30.0 → 3.32.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 (158) hide show
  1. package/CHANGELOG.md +85 -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 +367 -14
  7. package/src/core/custom-commands/bundled/wt/index.ts +1 -1
  8. package/src/core/sdk.ts +10 -2
  9. package/src/core/session-manager.ts +158 -246
  10. package/src/core/session-storage.ts +379 -0
  11. package/src/core/settings-manager.ts +155 -4
  12. package/src/core/slash-commands.ts +39 -13
  13. package/src/core/system-prompt.ts +62 -64
  14. package/src/core/tools/ask.ts +5 -4
  15. package/src/core/tools/bash-interceptor.ts +26 -61
  16. package/src/core/tools/bash.ts +13 -8
  17. package/src/core/tools/edit-diff.ts +11 -4
  18. package/src/core/tools/edit.ts +7 -13
  19. package/src/core/tools/find.ts +111 -50
  20. package/src/core/tools/gemini-image.ts +128 -147
  21. package/src/core/tools/grep.ts +397 -415
  22. package/src/core/tools/index.test.ts +5 -1
  23. package/src/core/tools/index.ts +8 -4
  24. package/src/core/tools/ls.ts +12 -10
  25. package/src/core/tools/lsp/client.ts +84 -19
  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 +72 -35
  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 +150 -74
  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/commands.ts +4 -0
  37. package/src/core/tools/task/executor.ts +94 -83
  38. package/src/core/tools/task/index.ts +130 -92
  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 +15 -6
  42. package/src/core/tools/task/worker.ts +124 -89
  43. package/src/core/tools/web-fetch.ts +112 -377
  44. package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
  45. package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
  46. package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
  47. package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
  48. package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
  49. package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
  50. package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
  51. package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
  52. package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
  53. package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
  54. package/src/core/tools/web-scrapers/clojars.ts +180 -0
  55. package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
  56. package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
  57. package/src/core/tools/web-scrapers/crossref.ts +149 -0
  58. package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
  59. package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
  60. package/src/core/tools/web-scrapers/discourse.ts +221 -0
  61. package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
  62. package/src/core/tools/web-scrapers/fdroid.ts +158 -0
  63. package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
  64. package/src/core/tools/web-scrapers/flathub.ts +239 -0
  65. package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
  66. package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
  67. package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
  68. package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
  69. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
  70. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
  71. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
  72. package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
  73. package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
  74. package/src/core/tools/web-scrapers/index.ts +250 -0
  75. package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
  76. package/src/core/tools/web-scrapers/lemmy.ts +220 -0
  77. package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
  78. package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
  79. package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
  80. package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
  81. package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
  82. package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
  83. package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
  84. package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
  85. package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
  86. package/src/core/tools/web-scrapers/ollama.ts +267 -0
  87. package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
  88. package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
  89. package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
  90. package/src/core/tools/web-scrapers/orcid.ts +299 -0
  91. package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
  92. package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
  93. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
  94. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
  95. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
  96. package/src/core/tools/web-scrapers/rawg.ts +124 -0
  97. package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
  98. package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
  99. package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
  100. package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
  101. package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
  102. package/src/core/tools/web-scrapers/searchcode.ts +217 -0
  103. package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
  104. package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
  105. package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
  106. package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
  107. package/src/core/tools/web-scrapers/spdx.ts +121 -0
  108. package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
  109. package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
  110. package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
  111. package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
  112. package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
  113. package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
  114. package/src/core/tools/web-scrapers/utils.ts +162 -0
  115. package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
  116. package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
  117. package/src/core/tools/web-scrapers/w3c.ts +163 -0
  118. package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
  119. package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
  120. package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
  121. package/src/core/tools/write.ts +21 -18
  122. package/src/core/voice.ts +3 -2
  123. package/src/lib/worktree/collapse.ts +2 -1
  124. package/src/lib/worktree/git.ts +2 -18
  125. package/src/main.ts +59 -3
  126. package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
  127. package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
  128. package/src/modes/interactive/components/hook-editor.ts +2 -1
  129. package/src/modes/interactive/components/model-selector.ts +19 -4
  130. package/src/modes/interactive/interactive-mode.ts +41 -63
  131. package/src/modes/interactive/theme/theme.ts +58 -58
  132. package/src/modes/rpc/rpc-mode.ts +10 -9
  133. package/src/prompts/review-request.md +27 -0
  134. package/src/prompts/reviewer.md +64 -68
  135. package/src/prompts/tools/output.md +22 -3
  136. package/src/prompts/tools/task.md +32 -33
  137. package/src/utils/clipboard.ts +2 -1
  138. package/examples/extensions/subagent/agents/reviewer.md +0 -35
  139. package/src/core/tools/web-fetch-handlers/index.ts +0 -69
  140. package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
  141. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
  142. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
  143. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
  144. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
  145. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
  146. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
  147. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
  148. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
  149. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
  150. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
  151. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
  152. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
  153. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
  154. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
  155. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
  156. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
  157. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
  158. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
@@ -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
+ };
@@ -74,7 +74,7 @@ function renderComments(comments: LobstersComment[], maxDepth = 5): string {
74
74
  /**
75
75
  * Handle Lobste.rs URLs via JSON API
76
76
  */
77
- export const handleLobsters: SpecialHandler = async (url: string, timeout: number) => {
77
+ export const handleLobsters: SpecialHandler = async (url: string, timeout: number, signal?: AbortSignal) => {
78
78
  try {
79
79
  const parsed = new URL(url);
80
80
  if (!parsed.hostname.includes("lobste.rs")) return null;
@@ -87,7 +87,7 @@ export const handleLobsters: SpecialHandler = async (url: string, timeout: numbe
87
87
  const storyMatch = parsed.pathname.match(/^\/s\/([^/]+)/);
88
88
  if (storyMatch) {
89
89
  jsonUrl = `https://lobste.rs/s/${storyMatch[1]}.json`;
90
- const result = await loadPage(jsonUrl, { timeout });
90
+ const result = await loadPage(jsonUrl, { timeout, signal });
91
91
  if (!result.ok) return null;
92
92
 
93
93
  const story = JSON.parse(result.content) as LobstersStoryResponse;
@@ -140,7 +140,7 @@ export const handleLobsters: SpecialHandler = async (url: string, timeout: numbe
140
140
 
141
141
  if (!jsonUrl) return null;
142
142
 
143
- const result = await loadPage(jsonUrl, { timeout });
143
+ const result = await loadPage(jsonUrl, { timeout, signal });
144
144
  if (!result.ok) return null;
145
145
 
146
146
  const stories = JSON.parse(result.content) as LobstersStory[];