@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,275 @@
1
+ import type { RenderResult, SpecialHandler } from "./types";
2
+ import { finalizeOutput, loadPage } from "./types";
3
+
4
+ interface Officer {
5
+ id: number;
6
+ name: string;
7
+ position?: string;
8
+ start_date?: string;
9
+ end_date?: string;
10
+ occupation?: string;
11
+ nationality?: string;
12
+ inactive?: boolean;
13
+ }
14
+
15
+ interface Address {
16
+ street_address?: string;
17
+ locality?: string;
18
+ region?: string;
19
+ postal_code?: string;
20
+ country?: string;
21
+ }
22
+
23
+ interface CompanyData {
24
+ name: string;
25
+ company_number: string;
26
+ jurisdiction_code: string;
27
+ incorporation_date?: string;
28
+ dissolution_date?: string;
29
+ company_type?: string;
30
+ registry_url?: string;
31
+ branch?: string;
32
+ branch_status?: string;
33
+ inactive?: boolean;
34
+ current_status?: string;
35
+ created_at?: string;
36
+ updated_at?: string;
37
+ retrieved_at?: string;
38
+ opencorporates_url?: string;
39
+ source?: {
40
+ publisher?: string;
41
+ url?: string;
42
+ retrieved_at?: string;
43
+ };
44
+ registered_address?: Address;
45
+ registered_address_in_full?: string;
46
+ industry_codes?: Array<{
47
+ code: string;
48
+ description?: string;
49
+ code_scheme_name?: string;
50
+ }>;
51
+ identifiers?: Array<{
52
+ identifier_system_code: string;
53
+ identifier_system_name?: string;
54
+ identifier_uid: string;
55
+ }>;
56
+ previous_names?: Array<{
57
+ company_name: string;
58
+ con_date?: string;
59
+ }>;
60
+ alternative_names?: Array<{
61
+ company_name: string;
62
+ type?: string;
63
+ }>;
64
+ officers?: Array<{ officer: Officer }>;
65
+ agent_name?: string;
66
+ agent_address?: string;
67
+ number_of_employees?: string;
68
+ native_company_number?: string;
69
+ }
70
+
71
+ interface ApiResponse {
72
+ api_version: string;
73
+ results: {
74
+ company: CompanyData;
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Handle OpenCorporates URLs via API
80
+ */
81
+ export const handleOpenCorporates: SpecialHandler = async (
82
+ url: string,
83
+ timeout: number,
84
+ signal?: AbortSignal,
85
+ ): Promise<RenderResult | null> => {
86
+ try {
87
+ const parsed = new URL(url);
88
+ if (!parsed.hostname.includes("opencorporates.com")) return null;
89
+
90
+ // Extract jurisdiction and company number from /companies/{jurisdiction}/{number}
91
+ const match = parsed.pathname.match(/^\/companies\/([^/]+)\/([^/]+)/);
92
+ if (!match) return null;
93
+
94
+ const jurisdiction = decodeURIComponent(match[1]);
95
+ const companyNumber = decodeURIComponent(match[2]);
96
+
97
+ const fetchedAt = new Date().toISOString();
98
+
99
+ // Fetch from OpenCorporates API
100
+ const apiUrl = `https://api.opencorporates.com/v0.4/companies/${jurisdiction}/${companyNumber}`;
101
+ const result = await loadPage(apiUrl, {
102
+ timeout,
103
+ headers: { Accept: "application/json" },
104
+ signal,
105
+ });
106
+
107
+ if (!result.ok) return null;
108
+
109
+ let data: ApiResponse;
110
+ try {
111
+ data = JSON.parse(result.content);
112
+ } catch {
113
+ return null;
114
+ }
115
+
116
+ const company = data.results?.company;
117
+ if (!company) return null;
118
+
119
+ let md = `# ${company.name}\n\n`;
120
+
121
+ // Basic info table
122
+ md += "| Field | Value |\n|-------|-------|\n";
123
+ md += `| **Company Number** | ${company.company_number} |\n`;
124
+ md += `| **Jurisdiction** | ${company.jurisdiction_code.toUpperCase()} |\n`;
125
+ if (company.current_status) {
126
+ md += `| **Status** | ${company.current_status} |\n`;
127
+ }
128
+ if (company.company_type) {
129
+ md += `| **Company Type** | ${company.company_type} |\n`;
130
+ }
131
+ if (company.incorporation_date) {
132
+ md += `| **Incorporated** | ${company.incorporation_date} |\n`;
133
+ }
134
+ if (company.dissolution_date) {
135
+ md += `| **Dissolved** | ${company.dissolution_date} |\n`;
136
+ }
137
+ if (company.branch) {
138
+ md += `| **Branch** | ${company.branch}${company.branch_status ? ` (${company.branch_status})` : ""} |\n`;
139
+ }
140
+ if (company.native_company_number && company.native_company_number !== company.company_number) {
141
+ md += `| **Native Number** | ${company.native_company_number} |\n`;
142
+ }
143
+ md += "\n";
144
+
145
+ // Registered address
146
+ if (company.registered_address_in_full) {
147
+ md += `## Registered Address\n\n${company.registered_address_in_full}\n\n`;
148
+ } else if (company.registered_address) {
149
+ const addr = company.registered_address;
150
+ const parts = [addr.street_address, addr.locality, addr.region, addr.postal_code, addr.country].filter(
151
+ Boolean,
152
+ );
153
+ if (parts.length > 0) {
154
+ md += `## Registered Address\n\n${parts.join(", ")}\n\n`;
155
+ }
156
+ }
157
+
158
+ // Agent info
159
+ if (company.agent_name) {
160
+ md += `## Registered Agent\n\n**${company.agent_name}**`;
161
+ if (company.agent_address) {
162
+ md += `\n${company.agent_address}`;
163
+ }
164
+ md += "\n\n";
165
+ }
166
+
167
+ // Officers/Directors
168
+ if (company.officers && company.officers.length > 0) {
169
+ const activeOfficers = company.officers.filter((o) => !o.officer.inactive && !o.officer.end_date);
170
+ const inactiveOfficers = company.officers.filter((o) => o.officer.inactive || o.officer.end_date);
171
+
172
+ if (activeOfficers.length > 0) {
173
+ md += `## Current Officers (${activeOfficers.length})\n\n`;
174
+ for (const { officer } of activeOfficers) {
175
+ md += `- **${officer.name}**`;
176
+ if (officer.position) md += ` - ${officer.position}`;
177
+ if (officer.start_date) md += ` (since ${officer.start_date})`;
178
+ if (officer.occupation) md += ` [${officer.occupation}]`;
179
+ if (officer.nationality) md += ` (${officer.nationality})`;
180
+ md += "\n";
181
+ }
182
+ md += "\n";
183
+ }
184
+
185
+ if (inactiveOfficers.length > 0) {
186
+ md += `## Former Officers (${inactiveOfficers.length})\n\n`;
187
+ for (const { officer } of inactiveOfficers.slice(0, 10)) {
188
+ md += `- **${officer.name}**`;
189
+ if (officer.position) md += ` - ${officer.position}`;
190
+ if (officer.start_date && officer.end_date) {
191
+ md += ` (${officer.start_date} to ${officer.end_date})`;
192
+ } else if (officer.end_date) {
193
+ md += ` (until ${officer.end_date})`;
194
+ }
195
+ md += "\n";
196
+ }
197
+ if (inactiveOfficers.length > 10) {
198
+ md += `\n*...and ${inactiveOfficers.length - 10} more former officers*\n`;
199
+ }
200
+ md += "\n";
201
+ }
202
+ }
203
+
204
+ // Industry codes
205
+ if (company.industry_codes && company.industry_codes.length > 0) {
206
+ md += `## Industry Codes\n\n`;
207
+ for (const ic of company.industry_codes) {
208
+ md += `- **${ic.code}**`;
209
+ if (ic.description) md += `: ${ic.description}`;
210
+ if (ic.code_scheme_name) md += ` (${ic.code_scheme_name})`;
211
+ md += "\n";
212
+ }
213
+ md += "\n";
214
+ }
215
+
216
+ // Identifiers
217
+ if (company.identifiers && company.identifiers.length > 0) {
218
+ md += `## Identifiers\n\n`;
219
+ for (const id of company.identifiers) {
220
+ md += `- **${id.identifier_system_name || id.identifier_system_code}**: ${id.identifier_uid}\n`;
221
+ }
222
+ md += "\n";
223
+ }
224
+
225
+ // Previous names
226
+ if (company.previous_names && company.previous_names.length > 0) {
227
+ md += `## Previous Names\n\n`;
228
+ for (const pn of company.previous_names) {
229
+ md += `- ${pn.company_name}`;
230
+ if (pn.con_date) md += ` (until ${pn.con_date})`;
231
+ md += "\n";
232
+ }
233
+ md += "\n";
234
+ }
235
+
236
+ // Alternative names
237
+ if (company.alternative_names && company.alternative_names.length > 0) {
238
+ md += `## Alternative Names\n\n`;
239
+ for (const an of company.alternative_names) {
240
+ md += `- ${an.company_name}`;
241
+ if (an.type) md += ` (${an.type})`;
242
+ md += "\n";
243
+ }
244
+ md += "\n";
245
+ }
246
+
247
+ // Source info
248
+ md += "---\n\n";
249
+ if (company.source?.publisher) {
250
+ md += `**Source:** ${company.source.publisher}`;
251
+ if (company.source.url) md += ` ([registry](${company.source.url}))`;
252
+ md += "\n";
253
+ }
254
+ if (company.registry_url) {
255
+ md += `**Official Registry:** ${company.registry_url}\n`;
256
+ }
257
+ if (company.retrieved_at) {
258
+ md += `**Data Retrieved:** ${company.retrieved_at}\n`;
259
+ }
260
+
261
+ const output = finalizeOutput(md);
262
+ return {
263
+ url,
264
+ finalUrl: company.opencorporates_url || url,
265
+ contentType: "text/markdown",
266
+ method: "opencorporates",
267
+ content: output.content,
268
+ fetchedAt,
269
+ truncated: output.truncated,
270
+ notes: ["Fetched via OpenCorporates API"],
271
+ };
272
+ } catch {}
273
+
274
+ return null;
275
+ };
@@ -0,0 +1,319 @@
1
+ import type { RenderResult, SpecialHandler } from "./types";
2
+ import { finalizeOutput, loadPage } from "./types";
3
+
4
+ interface OpenLibraryAuthor {
5
+ name?: string;
6
+ url?: string;
7
+ }
8
+
9
+ interface OpenLibrarySubject {
10
+ name: string;
11
+ url?: string;
12
+ }
13
+
14
+ interface OpenLibraryPublisher {
15
+ name: string;
16
+ }
17
+
18
+ interface OpenLibraryCover {
19
+ small?: string;
20
+ medium?: string;
21
+ large?: string;
22
+ }
23
+
24
+ interface OpenLibraryWork {
25
+ title: string;
26
+ authors?: Array<{ author: { key: string } }>;
27
+ description?: string | { value: string };
28
+ subjects?: string[];
29
+ subject_places?: string[];
30
+ subject_times?: string[];
31
+ covers?: number[];
32
+ first_publish_date?: string;
33
+ }
34
+
35
+ interface OpenLibraryEdition {
36
+ title: string;
37
+ authors?: Array<{ key: string }>;
38
+ publishers?: string[];
39
+ publish_date?: string;
40
+ number_of_pages?: number;
41
+ isbn_10?: string[];
42
+ isbn_13?: string[];
43
+ covers?: number[];
44
+ description?: string | { value: string };
45
+ subjects?: string[];
46
+ works?: Array<{ key: string }>;
47
+ }
48
+
49
+ interface OpenLibraryBooksApiResponse {
50
+ [key: string]: {
51
+ title: string;
52
+ authors?: OpenLibraryAuthor[];
53
+ publishers?: OpenLibraryPublisher[];
54
+ publish_date?: string;
55
+ number_of_pages?: number;
56
+ subjects?: OpenLibrarySubject[];
57
+ cover?: OpenLibraryCover;
58
+ url?: string;
59
+ identifiers?: {
60
+ isbn_10?: string[];
61
+ isbn_13?: string[];
62
+ openlibrary?: string[];
63
+ };
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Handle Open Library URLs via their API
69
+ */
70
+ export const handleOpenLibrary: SpecialHandler = async (
71
+ url: string,
72
+ timeout: number,
73
+ signal?: AbortSignal,
74
+ ): Promise<RenderResult | null> => {
75
+ try {
76
+ const parsed = new URL(url);
77
+ if (!parsed.hostname.includes("openlibrary.org")) return null;
78
+
79
+ const fetchedAt = new Date().toISOString();
80
+ const path = parsed.pathname;
81
+
82
+ // Match URL patterns
83
+ const workMatch = path.match(/^\/works\/(OL\d+W)/i);
84
+ const editionMatch = path.match(/^\/books\/(OL\d+M)/i);
85
+ const isbnMatch = path.match(/^\/isbn\/(\d{10}|\d{13})/i);
86
+
87
+ let md: string | null = null;
88
+
89
+ if (workMatch) {
90
+ md = await fetchWork(workMatch[1], timeout, signal);
91
+ } else if (editionMatch) {
92
+ md = await fetchEdition(editionMatch[1], timeout, signal);
93
+ } else if (isbnMatch) {
94
+ md = await fetchByIsbn(isbnMatch[1], timeout, signal);
95
+ }
96
+
97
+ if (!md) return null;
98
+
99
+ const output = finalizeOutput(md);
100
+ return {
101
+ url,
102
+ finalUrl: url,
103
+ contentType: "text/markdown",
104
+ method: "openlibrary",
105
+ content: output.content,
106
+ fetchedAt,
107
+ truncated: output.truncated,
108
+ notes: ["Fetched via Open Library API"],
109
+ };
110
+ } catch {}
111
+
112
+ return null;
113
+ };
114
+
115
+ async function fetchWork(workId: string, timeout: number, signal?: AbortSignal): Promise<string | null> {
116
+ const apiUrl = `https://openlibrary.org/works/${workId}.json`;
117
+ const result = await loadPage(apiUrl, { timeout, signal });
118
+ if (!result.ok) return null;
119
+
120
+ let work: OpenLibraryWork;
121
+ try {
122
+ work = JSON.parse(result.content);
123
+ } catch {
124
+ return null;
125
+ }
126
+
127
+ let md = `# ${work.title}\n\n`;
128
+
129
+ // Fetch author names if we have author keys
130
+ if (work.authors?.length) {
131
+ const authorNames = await fetchAuthorNames(
132
+ work.authors.map((a) => a.author.key),
133
+ timeout,
134
+ signal,
135
+ );
136
+ if (authorNames.length) {
137
+ md += `**Authors:** ${authorNames.join(", ")}\n`;
138
+ }
139
+ }
140
+
141
+ if (work.first_publish_date) {
142
+ md += `**First Published:** ${work.first_publish_date}\n`;
143
+ }
144
+
145
+ if (work.covers?.length) {
146
+ const coverId = work.covers[0];
147
+ md += `**Cover:** https://covers.openlibrary.org/b/id/${coverId}-L.jpg\n`;
148
+ }
149
+
150
+ md += `**Open Library:** https://openlibrary.org/works/${workId}\n`;
151
+ md += "\n";
152
+
153
+ const description = extractDescription(work.description);
154
+ if (description) {
155
+ md += `## Description\n\n${description}\n\n`;
156
+ }
157
+
158
+ if (work.subjects?.length) {
159
+ md += `## Subjects\n\n${work.subjects.slice(0, 20).join(", ")}\n`;
160
+ }
161
+
162
+ return md;
163
+ }
164
+
165
+ async function fetchEdition(editionId: string, timeout: number, signal?: AbortSignal): Promise<string | null> {
166
+ const apiUrl = `https://openlibrary.org/books/${editionId}.json`;
167
+ const result = await loadPage(apiUrl, { timeout, signal });
168
+ if (!result.ok) return null;
169
+
170
+ let edition: OpenLibraryEdition;
171
+ try {
172
+ edition = JSON.parse(result.content);
173
+ } catch {
174
+ return null;
175
+ }
176
+
177
+ let md = `# ${edition.title}\n\n`;
178
+
179
+ // Fetch author names
180
+ if (edition.authors?.length) {
181
+ const authorNames = await fetchAuthorNames(
182
+ edition.authors.map((a) => a.key),
183
+ timeout,
184
+ signal,
185
+ );
186
+ if (authorNames.length) {
187
+ md += `**Authors:** ${authorNames.join(", ")}\n`;
188
+ }
189
+ }
190
+
191
+ if (edition.publishers?.length) {
192
+ md += `**Publishers:** ${edition.publishers.join(", ")}\n`;
193
+ }
194
+
195
+ if (edition.publish_date) {
196
+ md += `**Published:** ${edition.publish_date}\n`;
197
+ }
198
+
199
+ if (edition.number_of_pages) {
200
+ md += `**Pages:** ${edition.number_of_pages}\n`;
201
+ }
202
+
203
+ const isbns = [...(edition.isbn_13 || []), ...(edition.isbn_10 || [])];
204
+ if (isbns.length) {
205
+ md += `**ISBN:** ${isbns[0]}\n`;
206
+ }
207
+
208
+ if (edition.covers?.length) {
209
+ const coverId = edition.covers[0];
210
+ md += `**Cover:** https://covers.openlibrary.org/b/id/${coverId}-L.jpg\n`;
211
+ }
212
+
213
+ md += `**Open Library:** https://openlibrary.org/books/${editionId}\n`;
214
+
215
+ if (edition.works?.length) {
216
+ const workKey = edition.works[0].key.replace("/works/", "");
217
+ md += `**Work:** https://openlibrary.org/works/${workKey}\n`;
218
+ }
219
+
220
+ md += "\n";
221
+
222
+ const description = extractDescription(edition.description);
223
+ if (description) {
224
+ md += `## Description\n\n${description}\n\n`;
225
+ }
226
+
227
+ if (edition.subjects?.length) {
228
+ md += `## Subjects\n\n${edition.subjects.slice(0, 20).join(", ")}\n`;
229
+ }
230
+
231
+ return md;
232
+ }
233
+
234
+ async function fetchByIsbn(isbn: string, timeout: number, signal?: AbortSignal): Promise<string | null> {
235
+ const apiUrl = `https://openlibrary.org/api/books?bibkeys=ISBN:${isbn}&format=json&jscmd=data`;
236
+ const result = await loadPage(apiUrl, { timeout, signal });
237
+ if (!result.ok) return null;
238
+
239
+ let data: OpenLibraryBooksApiResponse;
240
+ try {
241
+ data = JSON.parse(result.content);
242
+ } catch {
243
+ return null;
244
+ }
245
+
246
+ const key = `ISBN:${isbn}`;
247
+ const book = data[key];
248
+ if (!book) return null;
249
+
250
+ let md = `# ${book.title}\n\n`;
251
+
252
+ if (book.authors?.length) {
253
+ md += `**Authors:** ${book.authors.map((a) => a.name).join(", ")}\n`;
254
+ }
255
+
256
+ if (book.publishers?.length) {
257
+ md += `**Publishers:** ${book.publishers.map((p) => p.name).join(", ")}\n`;
258
+ }
259
+
260
+ if (book.publish_date) {
261
+ md += `**Published:** ${book.publish_date}\n`;
262
+ }
263
+
264
+ if (book.number_of_pages) {
265
+ md += `**Pages:** ${book.number_of_pages}\n`;
266
+ }
267
+
268
+ md += `**ISBN:** ${isbn}\n`;
269
+
270
+ if (book.cover?.large || book.cover?.medium) {
271
+ md += `**Cover:** ${book.cover.large || book.cover.medium}\n`;
272
+ }
273
+
274
+ if (book.url) {
275
+ md += `**Open Library:** ${book.url}\n`;
276
+ }
277
+
278
+ md += "\n";
279
+
280
+ if (book.subjects?.length) {
281
+ md += `## Subjects\n\n${book.subjects
282
+ .slice(0, 20)
283
+ .map((s) => s.name)
284
+ .join(", ")}\n`;
285
+ }
286
+
287
+ return md;
288
+ }
289
+
290
+ async function fetchAuthorNames(authorKeys: string[], timeout: number, signal?: AbortSignal): Promise<string[]> {
291
+ const names: string[] = [];
292
+
293
+ // Fetch authors in parallel (limit to first 5)
294
+ const promises = authorKeys.slice(0, 5).map(async (key) => {
295
+ const authorKey = key.startsWith("/authors/") ? key : `/authors/${key}`;
296
+ const apiUrl = `https://openlibrary.org${authorKey}.json`;
297
+ try {
298
+ const result = await loadPage(apiUrl, { timeout: Math.min(timeout, 5), signal });
299
+ if (result.ok) {
300
+ const author = JSON.parse(result.content) as { name?: string };
301
+ return author.name || null;
302
+ }
303
+ } catch {}
304
+ return null;
305
+ });
306
+
307
+ const results = await Promise.all(promises);
308
+ for (const name of results) {
309
+ if (name) names.push(name);
310
+ }
311
+
312
+ return names;
313
+ }
314
+
315
+ function extractDescription(desc: string | { value: string } | undefined): string | null {
316
+ if (!desc) return null;
317
+ if (typeof desc === "string") return desc;
318
+ return desc.value || null;
319
+ }