@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
@@ -86,11 +86,11 @@ function parseGitLabUrl(url: string): GitLabUrl | null {
86
86
  /**
87
87
  * Get project ID from namespace/project path
88
88
  */
89
- async function getProjectId(gl: GitLabUrl, timeout: number): Promise<number | null> {
89
+ async function getProjectId(gl: GitLabUrl, timeout: number, signal?: AbortSignal): Promise<number | null> {
90
90
  const encodedPath = encodeURIComponent(`${gl.namespace}/${gl.project}`);
91
91
  const apiUrl = `https://gitlab.com/api/v4/projects/${encodedPath}`;
92
92
 
93
- const result = await loadPage(apiUrl, { timeout });
93
+ const result = await loadPage(apiUrl, { timeout, signal });
94
94
  if (!result.ok) return null;
95
95
 
96
96
  try {
@@ -104,11 +104,15 @@ async function getProjectId(gl: GitLabUrl, timeout: number): Promise<number | nu
104
104
  /**
105
105
  * Render GitLab repository
106
106
  */
107
- async function renderGitLabRepo(gl: GitLabUrl, timeout: number): Promise<{ content: string; ok: boolean }> {
107
+ async function renderGitLabRepo(
108
+ gl: GitLabUrl,
109
+ timeout: number,
110
+ signal?: AbortSignal,
111
+ ): Promise<{ content: string; ok: boolean }> {
108
112
  const encodedPath = encodeURIComponent(`${gl.namespace}/${gl.project}`);
109
113
  const apiUrl = `https://gitlab.com/api/v4/projects/${encodedPath}`;
110
114
 
111
- const result = await loadPage(apiUrl, { timeout });
115
+ const result = await loadPage(apiUrl, { timeout, signal });
112
116
  if (!result.ok) return { content: "", ok: false };
113
117
 
114
118
  try {
@@ -137,7 +141,7 @@ async function renderGitLabRepo(gl: GitLabUrl, timeout: number): Promise<{ conte
137
141
 
138
142
  // Try to fetch README
139
143
  if (repo.readme_url) {
140
- const readmeResult = await loadPage(repo.readme_url, { timeout });
144
+ const readmeResult = await loadPage(repo.readme_url, { timeout, signal });
141
145
  if (readmeResult.ok && readmeResult.content.trim().length > 0) {
142
146
  md += `---\n\n## README\n\n${readmeResult.content}\n`;
143
147
  }
@@ -156,11 +160,12 @@ async function renderGitLabFile(
156
160
  gl: GitLabUrl,
157
161
  projectId: number,
158
162
  timeout: number,
163
+ signal?: AbortSignal,
159
164
  ): Promise<{ content: string; ok: boolean }> {
160
165
  const encodedPath = encodeURIComponent(gl.path!);
161
166
  const apiUrl = `https://gitlab.com/api/v4/projects/${projectId}/repository/files/${encodedPath}/raw?ref=${gl.ref}`;
162
167
 
163
- const result = await loadPage(apiUrl, { timeout });
168
+ const result = await loadPage(apiUrl, { timeout, signal });
164
169
  if (!result.ok) return { content: "", ok: false };
165
170
 
166
171
  return { content: result.content, ok: true };
@@ -173,10 +178,11 @@ async function renderGitLabTree(
173
178
  gl: GitLabUrl,
174
179
  projectId: number,
175
180
  timeout: number,
181
+ signal?: AbortSignal,
176
182
  ): Promise<{ content: string; ok: boolean }> {
177
183
  const apiUrl = `https://gitlab.com/api/v4/projects/${projectId}/repository/tree?ref=${gl.ref}&path=${gl.path || ""}&per_page=100`;
178
184
 
179
- const result = await loadPage(apiUrl, { timeout });
185
+ const result = await loadPage(apiUrl, { timeout, signal });
180
186
  if (!result.ok) return { content: "", ok: false };
181
187
 
182
188
  try {
@@ -222,10 +228,11 @@ async function renderGitLabIssue(
222
228
  gl: GitLabUrl,
223
229
  projectId: number,
224
230
  timeout: number,
231
+ signal?: AbortSignal,
225
232
  ): Promise<{ content: string; ok: boolean }> {
226
233
  const apiUrl = `https://gitlab.com/api/v4/projects/${projectId}/issues/${gl.id}`;
227
234
 
228
- const result = await loadPage(apiUrl, { timeout });
235
+ const result = await loadPage(apiUrl, { timeout, signal });
229
236
  if (!result.ok) return { content: "", ok: false };
230
237
 
231
238
  try {
@@ -272,10 +279,11 @@ async function renderGitLabMR(
272
279
  gl: GitLabUrl,
273
280
  projectId: number,
274
281
  timeout: number,
282
+ signal?: AbortSignal,
275
283
  ): Promise<{ content: string; ok: boolean }> {
276
284
  const apiUrl = `https://gitlab.com/api/v4/projects/${projectId}/merge_requests/${gl.id}`;
277
285
 
278
- const result = await loadPage(apiUrl, { timeout });
286
+ const result = await loadPage(apiUrl, { timeout, signal });
279
287
  if (!result.ok) return { content: "", ok: false };
280
288
 
281
289
  try {
@@ -324,7 +332,11 @@ async function renderGitLabMR(
324
332
  /**
325
333
  * Handle GitLab URLs specially
326
334
  */
327
- export const handleGitLab: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
335
+ export const handleGitLab: SpecialHandler = async (
336
+ url: string,
337
+ timeout: number,
338
+ signal?: AbortSignal,
339
+ ): Promise<RenderResult | null> => {
328
340
  const gl = parseGitLabUrl(url);
329
341
  if (!gl) return null;
330
342
 
@@ -333,11 +345,11 @@ export const handleGitLab: SpecialHandler = async (url: string, timeout: number)
333
345
 
334
346
  switch (gl.type) {
335
347
  case "blob": {
336
- const projectId = await getProjectId(gl, timeout);
348
+ const projectId = await getProjectId(gl, timeout, signal);
337
349
  if (!projectId) break;
338
350
 
339
351
  notes.push(`Fetched raw file via GitLab API`);
340
- const result = await renderGitLabFile(gl, projectId, timeout);
352
+ const result = await renderGitLabFile(gl, projectId, timeout, signal);
341
353
  if (result.ok) {
342
354
  const output = finalizeOutput(result.content);
343
355
  return {
@@ -355,11 +367,11 @@ export const handleGitLab: SpecialHandler = async (url: string, timeout: number)
355
367
  }
356
368
 
357
369
  case "tree": {
358
- const projectId = await getProjectId(gl, timeout);
370
+ const projectId = await getProjectId(gl, timeout, signal);
359
371
  if (!projectId) break;
360
372
 
361
373
  notes.push(`Fetched directory tree via GitLab API`);
362
- const result = await renderGitLabTree(gl, projectId, timeout);
374
+ const result = await renderGitLabTree(gl, projectId, timeout, signal);
363
375
  if (result.ok) {
364
376
  const output = finalizeOutput(result.content);
365
377
  return {
@@ -377,11 +389,11 @@ export const handleGitLab: SpecialHandler = async (url: string, timeout: number)
377
389
  }
378
390
 
379
391
  case "issue": {
380
- const projectId = await getProjectId(gl, timeout);
392
+ const projectId = await getProjectId(gl, timeout, signal);
381
393
  if (!projectId) break;
382
394
 
383
395
  notes.push(`Fetched issue via GitLab API`);
384
- const result = await renderGitLabIssue(gl, projectId, timeout);
396
+ const result = await renderGitLabIssue(gl, projectId, timeout, signal);
385
397
  if (result.ok) {
386
398
  const output = finalizeOutput(result.content);
387
399
  return {
@@ -399,11 +411,11 @@ export const handleGitLab: SpecialHandler = async (url: string, timeout: number)
399
411
  }
400
412
 
401
413
  case "merge_request": {
402
- const projectId = await getProjectId(gl, timeout);
414
+ const projectId = await getProjectId(gl, timeout, signal);
403
415
  if (!projectId) break;
404
416
 
405
417
  notes.push(`Fetched merge request via GitLab API`);
406
- const result = await renderGitLabMR(gl, projectId, timeout);
418
+ const result = await renderGitLabMR(gl, projectId, timeout, signal);
407
419
  if (result.ok) {
408
420
  const output = finalizeOutput(result.content);
409
421
  return {
@@ -422,7 +434,7 @@ export const handleGitLab: SpecialHandler = async (url: string, timeout: number)
422
434
 
423
435
  case "repo": {
424
436
  notes.push(`Fetched repository via GitLab API`);
425
- const result = await renderGitLabRepo(gl, timeout);
437
+ const result = await renderGitLabRepo(gl, timeout, signal);
426
438
  if (result.ok) {
427
439
  const output = finalizeOutput(result.content);
428
440
  return {
@@ -10,7 +10,11 @@ interface GoModuleInfo {
10
10
  /**
11
11
  * Handle pkg.go.dev URLs via proxy API and page parsing
12
12
  */
13
- export const handleGoPkg: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
13
+ export const handleGoPkg: SpecialHandler = async (
14
+ url: string,
15
+ timeout: number,
16
+ signal?: AbortSignal,
17
+ ): Promise<RenderResult | null> => {
14
18
  try {
15
19
  const parsed = new URL(url);
16
20
  if (parsed.hostname !== "pkg.go.dev") return null;
@@ -58,7 +62,7 @@ export const handleGoPkg: SpecialHandler = async (url: string, timeout: number):
58
62
  if (version === "latest") {
59
63
  try {
60
64
  const proxyUrl = `https://proxy.golang.org/${encodeURIComponent(modulePath)}/@latest`;
61
- const proxyResult = await loadPage(proxyUrl, { timeout });
65
+ const proxyResult = await loadPage(proxyUrl, { timeout, signal });
62
66
 
63
67
  if (proxyResult.ok) {
64
68
  moduleInfo = JSON.parse(proxyResult.content) as GoModuleInfo;
@@ -70,7 +74,7 @@ export const handleGoPkg: SpecialHandler = async (url: string, timeout: number):
70
74
  } else {
71
75
  try {
72
76
  const proxyUrl = `https://proxy.golang.org/${encodeURIComponent(modulePath)}/@v/${encodeURIComponent(version)}.info`;
73
- const proxyResult = await loadPage(proxyUrl, { timeout });
77
+ const proxyResult = await loadPage(proxyUrl, { timeout, signal });
74
78
 
75
79
  if (proxyResult.ok) {
76
80
  moduleInfo = JSON.parse(proxyResult.content) as GoModuleInfo;
@@ -81,7 +85,7 @@ export const handleGoPkg: SpecialHandler = async (url: string, timeout: number):
81
85
  }
82
86
 
83
87
  // Fetch the pkg.go.dev page
84
- const pageResult = await loadPage(url, { timeout });
88
+ const pageResult = await loadPage(url, { timeout, signal });
85
89
  if (!pageResult.ok) {
86
90
  return {
87
91
  url,
@@ -19,7 +19,11 @@ interface HackagePackage {
19
19
  /**
20
20
  * Handle Hackage (Haskell package registry) URLs via JSON API
21
21
  */
22
- export const handleHackage: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
22
+ export const handleHackage: SpecialHandler = async (
23
+ url: string,
24
+ timeout: number,
25
+ signal?: AbortSignal,
26
+ ): Promise<RenderResult | null> => {
23
27
  try {
24
28
  const parsed = new URL(url);
25
29
  if (parsed.hostname !== "hackage.haskell.org") return null;
@@ -36,6 +40,7 @@ export const handleHackage: SpecialHandler = async (url: string, timeout: number
36
40
  const result = await loadPage(apiUrl, {
37
41
  timeout,
38
42
  headers: { Accept: "application/json" },
43
+ signal,
39
44
  });
40
45
 
41
46
  if (!result.ok) return null;
@@ -21,15 +21,15 @@ interface HNItem {
21
21
 
22
22
  const API_BASE = "https://hacker-news.firebaseio.com/v0";
23
23
 
24
- async function fetchItem(id: number, timeout: number): Promise<HNItem | null> {
24
+ async function fetchItem(id: number, timeout: number, signal?: AbortSignal): Promise<HNItem | null> {
25
25
  const url = `${API_BASE}/item/${id}.json`;
26
- const { content, ok } = await loadPage(url, { timeout });
26
+ const { content, ok } = await loadPage(url, { timeout, signal });
27
27
  if (!ok) return null;
28
28
  return JSON.parse(content) as HNItem;
29
29
  }
30
30
 
31
- async function fetchItems(ids: number[], timeout: number, limit = 20): Promise<HNItem[]> {
32
- const promises = ids.slice(0, limit).map((id) => fetchItem(id, timeout));
31
+ async function fetchItems(ids: number[], timeout: number, limit = 20, signal?: AbortSignal): Promise<HNItem[]> {
32
+ const promises = ids.slice(0, limit).map((id) => fetchItem(id, timeout, signal));
33
33
  const results = await Promise.all(promises);
34
34
  return results.filter((item): item is HNItem => item !== null && !item.deleted && !item.dead);
35
35
  }
@@ -69,7 +69,7 @@ function formatTimestamp(unixTime: number): string {
69
69
  return `${minutes}m ago`;
70
70
  }
71
71
 
72
- async function renderStory(item: HNItem, timeout: number, depth = 0): Promise<string> {
72
+ async function renderStory(item: HNItem, timeout: number, depth = 0, signal?: AbortSignal): Promise<string> {
73
73
  let output = "";
74
74
 
75
75
  if (depth === 0) {
@@ -90,7 +90,7 @@ async function renderStory(item: HNItem, timeout: number, depth = 0): Promise<st
90
90
 
91
91
  if (item.kids && item.kids.length > 0 && depth < 2) {
92
92
  const topComments = item.kids.slice(0, depth === 0 ? 20 : 10);
93
- const comments = await fetchItems(topComments, timeout, topComments.length);
93
+ const comments = await fetchItems(topComments, timeout, topComments.length, signal);
94
94
 
95
95
  if (comments.length > 0) {
96
96
  if (depth === 0) output += "---\n\n## Comments\n\n";
@@ -107,7 +107,7 @@ async function renderStory(item: HNItem, timeout: number, depth = 0): Promise<st
107
107
  }
108
108
 
109
109
  if (comment.kids && comment.kids.length > 0 && depth < 1) {
110
- const childOutput = await renderStory(comment, timeout, depth + 1);
110
+ const childOutput = await renderStory(comment, timeout, depth + 1, signal);
111
111
  output += childOutput;
112
112
  }
113
113
  }
@@ -117,9 +117,9 @@ async function renderStory(item: HNItem, timeout: number, depth = 0): Promise<st
117
117
  return output;
118
118
  }
119
119
 
120
- async function renderListing(ids: number[], timeout: number, title: string): Promise<string> {
120
+ async function renderListing(ids: number[], timeout: number, title: string, signal?: AbortSignal): Promise<string> {
121
121
  let output = `# ${title}\n\n`;
122
- const stories = await fetchItems(ids, timeout, 20);
122
+ const stories = await fetchItems(ids, timeout, 20, signal);
123
123
 
124
124
  for (let i = 0; i < stories.length; i++) {
125
125
  const story = stories[i];
@@ -137,7 +137,7 @@ async function renderListing(ids: number[], timeout: number, title: string): Pro
137
137
  return output;
138
138
  }
139
139
 
140
- export const handleHackerNews: SpecialHandler = async (url, timeout) => {
140
+ export const handleHackerNews: SpecialHandler = async (url, timeout, signal) => {
141
141
  const parsed = new URL(url);
142
142
  if (!parsed.hostname.includes("news.ycombinator.com")) return null;
143
143
 
@@ -149,28 +149,28 @@ export const handleHackerNews: SpecialHandler = async (url, timeout) => {
149
149
  const itemId = parsed.searchParams.get("id");
150
150
 
151
151
  if (itemId) {
152
- const item = await fetchItem(parseInt(itemId, 10), timeout);
152
+ const item = await fetchItem(parseInt(itemId, 10), timeout, signal);
153
153
  if (!item) throw new Error(`Failed to fetch item ${itemId}`);
154
154
 
155
- content = await renderStory(item, timeout);
155
+ content = await renderStory(item, timeout, 0, signal);
156
156
  notes.push(`Fetched HN item ${itemId} with top-level comments (depth 2)`);
157
157
  } else if (parsed.pathname === "/" || parsed.pathname === "/news") {
158
- const { content: raw, ok } = await loadPage(`${API_BASE}/topstories.json`, { timeout });
158
+ const { content: raw, ok } = await loadPage(`${API_BASE}/topstories.json`, { timeout, signal });
159
159
  if (!ok) throw new Error("Failed to fetch top stories");
160
160
  const ids = JSON.parse(raw) as number[];
161
- content = await renderListing(ids, timeout, "Hacker News - Top Stories");
161
+ content = await renderListing(ids, timeout, "Hacker News - Top Stories", signal);
162
162
  notes.push("Fetched top 20 stories from HN front page");
163
163
  } else if (parsed.pathname === "/newest") {
164
- const { content: raw, ok } = await loadPage(`${API_BASE}/newstories.json`, { timeout });
164
+ const { content: raw, ok } = await loadPage(`${API_BASE}/newstories.json`, { timeout, signal });
165
165
  if (!ok) throw new Error("Failed to fetch new stories");
166
166
  const ids = JSON.parse(raw) as number[];
167
- content = await renderListing(ids, timeout, "Hacker News - New Stories");
167
+ content = await renderListing(ids, timeout, "Hacker News - New Stories", signal);
168
168
  notes.push("Fetched top 20 new stories");
169
169
  } else if (parsed.pathname === "/best") {
170
- const { content: raw, ok } = await loadPage(`${API_BASE}/beststories.json`, { timeout });
170
+ const { content: raw, ok } = await loadPage(`${API_BASE}/beststories.json`, { timeout, signal });
171
171
  if (!ok) throw new Error("Failed to fetch best stories");
172
172
  const ids = JSON.parse(raw) as number[];
173
- content = await renderListing(ids, timeout, "Hacker News - Best Stories");
173
+ content = await renderListing(ids, timeout, "Hacker News - Best Stories", signal);
174
174
  notes.push("Fetched top 20 best stories");
175
175
  } else {
176
176
  return null;
@@ -4,7 +4,7 @@ import { finalizeOutput, formatCount, loadPage } from "./types";
4
4
  /**
5
5
  * Handle Hex.pm (Elixir package registry) URLs via API
6
6
  */
7
- export const handleHex: SpecialHandler = async (url, timeout) => {
7
+ export const handleHex: SpecialHandler = async (url, timeout, signal) => {
8
8
  try {
9
9
  const parsed = new URL(url);
10
10
  if (parsed.hostname !== "hex.pm" && parsed.hostname !== "www.hex.pm") return null;
@@ -18,7 +18,7 @@ export const handleHex: SpecialHandler = async (url, timeout) => {
18
18
 
19
19
  // Fetch from Hex.pm API
20
20
  const apiUrl = `https://hex.pm/api/packages/${packageName}`;
21
- const result = await loadPage(apiUrl, { timeout });
21
+ const result = await loadPage(apiUrl, { timeout, signal });
22
22
 
23
23
  if (!result.ok) return null;
24
24
 
@@ -74,7 +74,7 @@ export const handleHex: SpecialHandler = async (url, timeout) => {
74
74
  // Fetch releases if available
75
75
  if (data.releases?.length) {
76
76
  const releasesUrl = `https://hex.pm/api/packages/${packageName}/releases/${version}`;
77
- const releaseResult = await loadPage(releasesUrl, { timeout: Math.min(timeout, 5) });
77
+ const releaseResult = await loadPage(releasesUrl, { timeout: Math.min(timeout, 5), signal });
78
78
 
79
79
  if (releaseResult.ok) {
80
80
  try {
@@ -106,7 +106,7 @@ function parseHuggingFaceUrl(url: string): {
106
106
  }
107
107
  }
108
108
 
109
- export const handleHuggingFace: SpecialHandler = async (url: string, timeout: number) => {
109
+ export const handleHuggingFace: SpecialHandler = async (url: string, timeout: number, signal?: AbortSignal) => {
110
110
  const parsed = parseHuggingFaceUrl(url);
111
111
  if (!parsed) return null;
112
112
 
@@ -120,8 +120,8 @@ export const handleHuggingFace: SpecialHandler = async (url: string, timeout: nu
120
120
  const readmeUrl = `https://huggingface.co/${parsed.id}/raw/main/README.md`;
121
121
 
122
122
  const [apiResult, readmeResult] = await Promise.all([
123
- loadPage(apiUrl, { timeout }),
124
- loadPage(readmeUrl, { timeout: Math.min(timeout, 5) }),
123
+ loadPage(apiUrl, { timeout, signal }),
124
+ loadPage(readmeUrl, { timeout: Math.min(timeout, 5), signal }),
125
125
  ]);
126
126
 
127
127
  if (!apiResult.ok) return null;
@@ -186,8 +186,8 @@ export const handleHuggingFace: SpecialHandler = async (url: string, timeout: nu
186
186
  const readmeUrl = `https://huggingface.co/datasets/${parsed.id}/raw/main/README.md`;
187
187
 
188
188
  const [apiResult, readmeResult] = await Promise.all([
189
- loadPage(apiUrl, { timeout }),
190
- loadPage(readmeUrl, { timeout: Math.min(timeout, 5) }),
189
+ loadPage(apiUrl, { timeout, signal }),
190
+ loadPage(readmeUrl, { timeout: Math.min(timeout, 5), signal }),
191
191
  ]);
192
192
 
193
193
  if (!apiResult.ok) return null;
@@ -251,8 +251,8 @@ export const handleHuggingFace: SpecialHandler = async (url: string, timeout: nu
251
251
  const readmeUrl = `https://huggingface.co/spaces/${parsed.id}/raw/main/README.md`;
252
252
 
253
253
  const [apiResult, readmeResult] = await Promise.all([
254
- loadPage(apiUrl, { timeout }),
255
- loadPage(readmeUrl, { timeout: Math.min(timeout, 5) }),
254
+ loadPage(apiUrl, { timeout, signal }),
255
+ loadPage(readmeUrl, { timeout: Math.min(timeout, 5), signal }),
256
256
  ]);
257
257
 
258
258
  if (!apiResult.ok) return null;
@@ -303,7 +303,7 @@ export const handleHuggingFace: SpecialHandler = async (url: string, timeout: nu
303
303
  case "model_or_user": {
304
304
  // Try model API first
305
305
  const modelApiUrl = `https://huggingface.co/api/models/${parsed.id}`;
306
- const modelResult = await loadPage(modelApiUrl, { timeout });
306
+ const modelResult = await loadPage(modelApiUrl, { timeout, signal });
307
307
 
308
308
  if (modelResult.ok) {
309
309
  let model: HfModelData | null = null;
@@ -314,7 +314,7 @@ export const handleHuggingFace: SpecialHandler = async (url: string, timeout: nu
314
314
  }
315
315
  if (model) {
316
316
  const readmeUrl = `https://huggingface.co/${parsed.id}/raw/main/README.md`;
317
- const readmeResult = await loadPage(readmeUrl, { timeout: Math.min(timeout, 5) });
317
+ const readmeResult = await loadPage(readmeUrl, { timeout: Math.min(timeout, 5), signal });
318
318
 
319
319
  let md = `# ${model.modelId}\n\n`;
320
320
  if (model.pipeline_tag) md += `**Task:** ${model.pipeline_tag}\n`;
@@ -343,7 +343,7 @@ export const handleHuggingFace: SpecialHandler = async (url: string, timeout: nu
343
343
 
344
344
  // Fall back to user API
345
345
  const userApiUrl = `https://huggingface.co/api/users/${parsed.id}`;
346
- const userResult = await loadPage(userApiUrl, { timeout });
346
+ const userResult = await loadPage(userApiUrl, { timeout, signal });
347
347
  if (!userResult.ok) return null;
348
348
 
349
349
  let user: HfUserData;
@@ -6,7 +6,11 @@ import { convertWithMarkitdown, fetchBinary } from "./utils";
6
6
  /**
7
7
  * Handle IACR ePrint Archive URLs
8
8
  */
9
- export const handleIacr: SpecialHandler = async (url: string, timeout: number): Promise<RenderResult | null> => {
9
+ export const handleIacr: SpecialHandler = async (
10
+ url: string,
11
+ timeout: number,
12
+ signal?: AbortSignal,
13
+ ): Promise<RenderResult | null> => {
10
14
  try {
11
15
  const parsed = new URL(url);
12
16
  if (parsed.hostname !== "eprint.iacr.org") return null;
@@ -22,7 +26,7 @@ export const handleIacr: SpecialHandler = async (url: string, timeout: number):
22
26
 
23
27
  // Fetch the HTML page for metadata
24
28
  const pageUrl = `https://eprint.iacr.org/${paperId}`;
25
- const result = await loadPage(pageUrl, { timeout });
29
+ const result = await loadPage(pageUrl, { timeout, signal });
26
30
 
27
31
  if (!result.ok) return null;
28
32
 
@@ -55,9 +59,9 @@ export const handleIacr: SpecialHandler = async (url: string, timeout: number):
55
59
  if (parsed.pathname.endsWith(".pdf")) {
56
60
  const pdfUrl = `https://eprint.iacr.org/${paperId}.pdf`;
57
61
  notes.push("Fetching PDF for full content...");
58
- const pdfResult = await fetchBinary(pdfUrl, timeout);
62
+ const pdfResult = await fetchBinary(pdfUrl, timeout, signal);
59
63
  if (pdfResult.ok) {
60
- const converted = await convertWithMarkitdown(pdfResult.buffer, ".pdf", timeout);
64
+ const converted = await convertWithMarkitdown(pdfResult.buffer, ".pdf", timeout, signal);
61
65
  if (converted.ok && converted.content.length > 500) {
62
66
  md += `---\n\n## Full Paper\n\n${converted.content}\n`;
63
67
  notes.push("PDF converted via markitdown");