@ashdev/codex-plugin-metadata-openlibrary 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1070 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/api.ts
13
+ var api_exports = {};
14
+ __export(api_exports, {
15
+ buildOpenLibraryUrl: () => buildOpenLibraryUrl,
16
+ clearCache: () => clearCache,
17
+ extractOlid: () => extractOlid,
18
+ getAuthor: () => getAuthor,
19
+ getCoverUrlById: () => getCoverUrlById,
20
+ getCoverUrlByIsbn: () => getCoverUrlByIsbn,
21
+ getCoverUrlByOlid: () => getCoverUrlByOlid,
22
+ getEditionByIsbn: () => getEditionByIsbn,
23
+ getWork: () => getWork,
24
+ getWorkEditions: () => getWorkEditions,
25
+ isValidIsbn: () => isValidIsbn,
26
+ normalizeIsbn: () => normalizeIsbn,
27
+ parseDescription: () => parseDescription,
28
+ parseLanguage: () => parseLanguage,
29
+ parseYear: () => parseYear,
30
+ searchBooks: () => searchBooks
31
+ });
32
+ function getCached(key) {
33
+ const entry = cache.get(key);
34
+ if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {
35
+ return entry.data;
36
+ }
37
+ if (entry) {
38
+ cache.delete(key);
39
+ }
40
+ return null;
41
+ }
42
+ function setCache(key, data) {
43
+ cache.set(key, { data, timestamp: Date.now() });
44
+ }
45
+ async function fetchJson(url, description) {
46
+ const cached = getCached(url);
47
+ if (cached !== null) {
48
+ return cached;
49
+ }
50
+ try {
51
+ const response = await fetch(url, {
52
+ headers: {
53
+ "User-Agent": "Codex/1.0 (https://github.com/AshDevFr/codex; codex-plugin)",
54
+ Accept: "application/json"
55
+ }
56
+ });
57
+ if (!response.ok) {
58
+ if (response.status === 404) {
59
+ return null;
60
+ }
61
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
62
+ }
63
+ const data = await response.json();
64
+ setCache(url, data);
65
+ return data;
66
+ } catch (error) {
67
+ console.error(`[openlibrary] Failed to fetch ${description}:`, error);
68
+ return null;
69
+ }
70
+ }
71
+ function normalizeIsbn(isbn) {
72
+ return isbn.replace(/[-\s]/g, "").toUpperCase();
73
+ }
74
+ function isValidIsbn(isbn) {
75
+ const normalized = normalizeIsbn(isbn);
76
+ return normalized.length === 10 || normalized.length === 13;
77
+ }
78
+ async function getEditionByIsbn(isbn) {
79
+ const normalized = normalizeIsbn(isbn);
80
+ const url = `${BASE_URL}/isbn/${normalized}.json`;
81
+ return fetchJson(url, `edition by ISBN ${normalized}`);
82
+ }
83
+ async function getWork(workKey) {
84
+ const key = workKey.startsWith("/works/") ? workKey : `/works/${workKey}`;
85
+ const url = `${BASE_URL}${key}.json`;
86
+ return fetchJson(url, `work ${key}`);
87
+ }
88
+ async function getWorkEditions(workKey, limit = 5) {
89
+ const key = workKey.startsWith("/works/") ? workKey : `/works/${workKey}`;
90
+ const url = `${BASE_URL}${key}/editions.json?limit=${limit}`;
91
+ const response = await fetchJson(url, `editions for ${key}`);
92
+ return response?.entries || [];
93
+ }
94
+ async function getAuthor(authorKey) {
95
+ const key = authorKey.startsWith("/authors/") ? authorKey : `/authors/${authorKey}`;
96
+ const url = `${BASE_URL}${key}.json`;
97
+ return fetchJson(url, `author ${key}`);
98
+ }
99
+ async function searchBooks(query, options = {}) {
100
+ const { author, limit = 10 } = options;
101
+ if (author) {
102
+ const params2 = new URLSearchParams({
103
+ title: query,
104
+ author,
105
+ fields: SEARCH_FIELDS,
106
+ limit: String(limit)
107
+ });
108
+ const url2 = `${BASE_URL}/search.json?${params2}`;
109
+ const response = await fetchJson(
110
+ url2,
111
+ `search title="${query}" author="${author}"`
112
+ );
113
+ if (response?.docs?.length) {
114
+ return response;
115
+ }
116
+ }
117
+ const params = new URLSearchParams({
118
+ q: query,
119
+ fields: SEARCH_FIELDS,
120
+ limit: String(limit)
121
+ });
122
+ if (author) {
123
+ params.set("author", author);
124
+ }
125
+ const url = `${BASE_URL}/search.json?${params}`;
126
+ return fetchJson(url, `search "${query}"`);
127
+ }
128
+ function getCoverUrlByIsbn(isbn, size) {
129
+ const normalized = normalizeIsbn(isbn);
130
+ return `${COVERS_BASE_URL}/b/isbn/${normalized}-${size}.jpg`;
131
+ }
132
+ function getCoverUrlById(coverId, size) {
133
+ return `${COVERS_BASE_URL}/b/id/${coverId}-${size}.jpg`;
134
+ }
135
+ function getCoverUrlByOlid(olid, size) {
136
+ const id = olid.replace(/^\/(?:books|works)\//, "");
137
+ return `${COVERS_BASE_URL}/b/olid/${id}-${size}.jpg`;
138
+ }
139
+ function parseYear(dateStr) {
140
+ if (!dateStr) return void 0;
141
+ const match = dateStr.match(/(?:^|[^0-9])(1[89]\d{2}|20\d{2})(?:[^0-9]|$)/);
142
+ if (match) {
143
+ return Number.parseInt(match[1], 10);
144
+ }
145
+ return void 0;
146
+ }
147
+ function parseDescription(desc) {
148
+ if (!desc) return void 0;
149
+ const raw = typeof desc === "string" ? desc : desc.value;
150
+ return stripHtml(raw);
151
+ }
152
+ function stripHtml(html) {
153
+ let text = html;
154
+ text = text.replace(/<\/(p|div|li|tr|h[1-6])>/gi, "\n");
155
+ text = text.replace(/<br\s*\/?>/gi, "\n");
156
+ text = text.replace(/<[^>]+>/g, "");
157
+ text = text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&apos;/g, "'").replace(/&nbsp;/g, " ");
158
+ text = text.replace(/[^\S\n]+/g, " ");
159
+ text = text.replace(/\n{3,}/g, "\n\n");
160
+ text = text.split("\n").map((line) => line.trim()).join("\n").trim();
161
+ return text || void 0;
162
+ }
163
+ function parseLanguage(langRef) {
164
+ if (!langRef) return void 0;
165
+ const match = langRef.match(/\/languages\/(\w+)$/);
166
+ if (!match) return void 0;
167
+ const code = match[1].toLowerCase();
168
+ const languageMap = {
169
+ eng: "en",
170
+ spa: "es",
171
+ fre: "fr",
172
+ fra: "fr",
173
+ ger: "de",
174
+ deu: "de",
175
+ ita: "it",
176
+ por: "pt",
177
+ rus: "ru",
178
+ jpn: "ja",
179
+ chi: "zh",
180
+ zho: "zh",
181
+ kor: "ko",
182
+ ara: "ar",
183
+ hin: "hi",
184
+ pol: "pl",
185
+ tur: "tr",
186
+ dut: "nl",
187
+ nld: "nl",
188
+ swe: "sv",
189
+ nor: "no",
190
+ dan: "da",
191
+ fin: "fi",
192
+ cze: "cs",
193
+ ces: "cs",
194
+ gre: "el",
195
+ ell: "el",
196
+ heb: "he",
197
+ hun: "hu",
198
+ rom: "ro",
199
+ ron: "ro",
200
+ tha: "th",
201
+ vie: "vi",
202
+ ind: "id",
203
+ mal: "ms",
204
+ msa: "ms",
205
+ ukr: "uk",
206
+ cat: "ca",
207
+ lat: "la"
208
+ };
209
+ return languageMap[code] || code;
210
+ }
211
+ function extractOlid(key) {
212
+ return key.replace(/^\/(?:works|books|authors)\//, "");
213
+ }
214
+ function buildOpenLibraryUrl(key) {
215
+ return `${BASE_URL}${key.startsWith("/") ? key : `/${key}`}`;
216
+ }
217
+ function clearCache() {
218
+ cache.clear();
219
+ }
220
+ var BASE_URL, COVERS_BASE_URL, CACHE_TTL_MS, cache, SEARCH_FIELDS;
221
+ var init_api = __esm({
222
+ "src/api.ts"() {
223
+ "use strict";
224
+ BASE_URL = "https://openlibrary.org";
225
+ COVERS_BASE_URL = "https://covers.openlibrary.org";
226
+ CACHE_TTL_MS = 15 * 60 * 1e3;
227
+ cache = /* @__PURE__ */ new Map();
228
+ SEARCH_FIELDS = [
229
+ "key",
230
+ "title",
231
+ "subtitle",
232
+ "author_name",
233
+ "author_key",
234
+ "first_publish_year",
235
+ "publish_year",
236
+ "publisher",
237
+ "isbn",
238
+ "number_of_pages_median",
239
+ "cover_i",
240
+ "cover_edition_key",
241
+ "edition_count",
242
+ "language",
243
+ "subject",
244
+ "ratings_average",
245
+ "ratings_count"
246
+ ].join(",");
247
+ }
248
+ });
249
+
250
+ // ../sdk-typescript/dist/types/rpc.js
251
+ var JSON_RPC_ERROR_CODES = {
252
+ /** Invalid JSON was received */
253
+ PARSE_ERROR: -32700,
254
+ /** The JSON sent is not a valid Request object */
255
+ INVALID_REQUEST: -32600,
256
+ /** The method does not exist / is not available */
257
+ METHOD_NOT_FOUND: -32601,
258
+ /** Invalid method parameter(s) */
259
+ INVALID_PARAMS: -32602,
260
+ /** Internal JSON-RPC error */
261
+ INTERNAL_ERROR: -32603
262
+ };
263
+
264
+ // ../sdk-typescript/dist/errors.js
265
+ var PluginError = class extends Error {
266
+ data;
267
+ constructor(message, data) {
268
+ super(message);
269
+ this.name = this.constructor.name;
270
+ this.data = data;
271
+ }
272
+ /**
273
+ * Convert to JSON-RPC error format
274
+ */
275
+ toJsonRpcError() {
276
+ return {
277
+ code: this.code,
278
+ message: this.message,
279
+ data: this.data
280
+ };
281
+ }
282
+ };
283
+
284
+ // ../sdk-typescript/dist/logger.js
285
+ var LOG_LEVELS = {
286
+ debug: 0,
287
+ info: 1,
288
+ warn: 2,
289
+ error: 3
290
+ };
291
+ var Logger = class {
292
+ name;
293
+ minLevel;
294
+ timestamps;
295
+ constructor(options) {
296
+ this.name = options.name;
297
+ this.minLevel = LOG_LEVELS[options.level ?? "info"];
298
+ this.timestamps = options.timestamps ?? true;
299
+ }
300
+ shouldLog(level) {
301
+ return LOG_LEVELS[level] >= this.minLevel;
302
+ }
303
+ format(level, message, data) {
304
+ const parts = [];
305
+ if (this.timestamps) {
306
+ parts.push((/* @__PURE__ */ new Date()).toISOString());
307
+ }
308
+ parts.push(`[${level.toUpperCase()}]`);
309
+ parts.push(`[${this.name}]`);
310
+ parts.push(message);
311
+ if (data !== void 0) {
312
+ if (data instanceof Error) {
313
+ parts.push(`- ${data.message}`);
314
+ if (data.stack) {
315
+ parts.push(`
316
+ ${data.stack}`);
317
+ }
318
+ } else if (typeof data === "object") {
319
+ parts.push(`- ${JSON.stringify(data)}`);
320
+ } else {
321
+ parts.push(`- ${String(data)}`);
322
+ }
323
+ }
324
+ return parts.join(" ");
325
+ }
326
+ log(level, message, data) {
327
+ if (this.shouldLog(level)) {
328
+ process.stderr.write(`${this.format(level, message, data)}
329
+ `);
330
+ }
331
+ }
332
+ debug(message, data) {
333
+ this.log("debug", message, data);
334
+ }
335
+ info(message, data) {
336
+ this.log("info", message, data);
337
+ }
338
+ warn(message, data) {
339
+ this.log("warn", message, data);
340
+ }
341
+ error(message, data) {
342
+ this.log("error", message, data);
343
+ }
344
+ };
345
+ function createLogger(options) {
346
+ return new Logger(options);
347
+ }
348
+
349
+ // ../sdk-typescript/dist/server.js
350
+ import { createInterface } from "node:readline";
351
+ function validateStringFields(params, fields) {
352
+ if (params === null || params === void 0) {
353
+ return { field: "params", message: "params is required" };
354
+ }
355
+ if (typeof params !== "object") {
356
+ return { field: "params", message: "params must be an object" };
357
+ }
358
+ const obj = params;
359
+ for (const field of fields) {
360
+ const value = obj[field];
361
+ if (value === void 0 || value === null) {
362
+ return { field, message: `${field} is required` };
363
+ }
364
+ if (typeof value !== "string") {
365
+ return { field, message: `${field} must be a string` };
366
+ }
367
+ if (value.trim() === "") {
368
+ return { field, message: `${field} cannot be empty` };
369
+ }
370
+ }
371
+ return null;
372
+ }
373
+ function validateSearchParams(params) {
374
+ return validateStringFields(params, ["query"]);
375
+ }
376
+ function validateGetParams(params) {
377
+ return validateStringFields(params, ["externalId"]);
378
+ }
379
+ function validateMatchParams(params) {
380
+ return validateStringFields(params, ["title"]);
381
+ }
382
+ function invalidParamsError(id, error) {
383
+ return {
384
+ jsonrpc: "2.0",
385
+ id,
386
+ error: {
387
+ code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
388
+ message: `Invalid params: ${error.message}`,
389
+ data: { field: error.field }
390
+ }
391
+ };
392
+ }
393
+ function createMetadataPlugin(options) {
394
+ const { manifest: manifest2, provider, onInitialize, logLevel = "info" } = options;
395
+ const logger2 = createLogger({ name: manifest2.name, level: logLevel });
396
+ logger2.info(`Starting plugin: ${manifest2.displayName} v${manifest2.version}`);
397
+ const rl = createInterface({
398
+ input: process.stdin,
399
+ terminal: false
400
+ });
401
+ rl.on("line", (line) => {
402
+ void handleLine(line, manifest2, provider, onInitialize, logger2);
403
+ });
404
+ rl.on("close", () => {
405
+ logger2.info("stdin closed, shutting down");
406
+ process.exit(0);
407
+ });
408
+ process.on("uncaughtException", (error) => {
409
+ logger2.error("Uncaught exception", error);
410
+ process.exit(1);
411
+ });
412
+ process.on("unhandledRejection", (reason) => {
413
+ logger2.error("Unhandled rejection", reason);
414
+ });
415
+ }
416
+ async function handleLine(line, manifest2, provider, onInitialize, logger2) {
417
+ const trimmed = line.trim();
418
+ if (!trimmed)
419
+ return;
420
+ let id = null;
421
+ try {
422
+ const request = JSON.parse(trimmed);
423
+ id = request.id;
424
+ logger2.debug(`Received request: ${request.method}`, { id: request.id });
425
+ const response = await handleRequest(request, manifest2, provider, onInitialize, logger2);
426
+ if (response !== null) {
427
+ writeResponse(response);
428
+ }
429
+ } catch (error) {
430
+ if (error instanceof SyntaxError) {
431
+ writeResponse({
432
+ jsonrpc: "2.0",
433
+ id: null,
434
+ error: {
435
+ code: JSON_RPC_ERROR_CODES.PARSE_ERROR,
436
+ message: "Parse error: invalid JSON"
437
+ }
438
+ });
439
+ } else if (error instanceof PluginError) {
440
+ writeResponse({
441
+ jsonrpc: "2.0",
442
+ id,
443
+ error: error.toJsonRpcError()
444
+ });
445
+ } else {
446
+ const message = error instanceof Error ? error.message : "Unknown error";
447
+ logger2.error("Request failed", error);
448
+ writeResponse({
449
+ jsonrpc: "2.0",
450
+ id,
451
+ error: {
452
+ code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
453
+ message
454
+ }
455
+ });
456
+ }
457
+ }
458
+ }
459
+ async function handleRequest(request, manifest2, provider, onInitialize, logger2) {
460
+ const { method, params, id } = request;
461
+ switch (method) {
462
+ case "initialize":
463
+ if (onInitialize) {
464
+ await onInitialize(params);
465
+ }
466
+ return {
467
+ jsonrpc: "2.0",
468
+ id,
469
+ result: manifest2
470
+ };
471
+ case "ping":
472
+ return {
473
+ jsonrpc: "2.0",
474
+ id,
475
+ result: "pong"
476
+ };
477
+ case "shutdown": {
478
+ logger2.info("Shutdown requested");
479
+ const response = {
480
+ jsonrpc: "2.0",
481
+ id,
482
+ result: null
483
+ };
484
+ process.stdout.write(`${JSON.stringify(response)}
485
+ `, () => {
486
+ process.exit(0);
487
+ });
488
+ return null;
489
+ }
490
+ // Series metadata methods (scoped by content type)
491
+ case "metadata/series/search": {
492
+ const validationError = validateSearchParams(params);
493
+ if (validationError) {
494
+ return invalidParamsError(id, validationError);
495
+ }
496
+ return {
497
+ jsonrpc: "2.0",
498
+ id,
499
+ result: await provider.search(params)
500
+ };
501
+ }
502
+ case "metadata/series/get": {
503
+ const validationError = validateGetParams(params);
504
+ if (validationError) {
505
+ return invalidParamsError(id, validationError);
506
+ }
507
+ return {
508
+ jsonrpc: "2.0",
509
+ id,
510
+ result: await provider.get(params)
511
+ };
512
+ }
513
+ case "metadata/series/match": {
514
+ if (!provider.match) {
515
+ return {
516
+ jsonrpc: "2.0",
517
+ id,
518
+ error: {
519
+ code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
520
+ message: "This plugin does not support match"
521
+ }
522
+ };
523
+ }
524
+ const validationError = validateMatchParams(params);
525
+ if (validationError) {
526
+ return invalidParamsError(id, validationError);
527
+ }
528
+ return {
529
+ jsonrpc: "2.0",
530
+ id,
531
+ result: await provider.match(params)
532
+ };
533
+ }
534
+ // Future: book metadata methods
535
+ // case "metadata/book/search":
536
+ // case "metadata/book/get":
537
+ // case "metadata/book/match":
538
+ default:
539
+ return {
540
+ jsonrpc: "2.0",
541
+ id,
542
+ error: {
543
+ code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
544
+ message: `Method not found: ${method}`
545
+ }
546
+ };
547
+ }
548
+ }
549
+ function writeResponse(response) {
550
+ process.stdout.write(`${JSON.stringify(response)}
551
+ `);
552
+ }
553
+
554
+ // src/index.ts
555
+ init_api();
556
+
557
+ // package.json
558
+ var package_default = {
559
+ name: "@ashdev/codex-plugin-metadata-openlibrary",
560
+ version: "1.0.0",
561
+ description: "Open Library metadata plugin for Codex - fetches book metadata by ISBN or title search",
562
+ main: "dist/index.js",
563
+ bin: "dist/index.js",
564
+ type: "module",
565
+ files: [
566
+ "dist",
567
+ "README.md"
568
+ ],
569
+ repository: {
570
+ type: "git",
571
+ url: "https://github.com/AshDevFr/codex.git",
572
+ directory: "plugins/metadata-openlibrary"
573
+ },
574
+ scripts: {
575
+ build: "esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --sourcemap --banner:js='#!/usr/bin/env node'",
576
+ dev: "npm run build -- --watch",
577
+ clean: "rm -rf dist",
578
+ start: "node dist/index.js",
579
+ lint: "biome check .",
580
+ "lint:fix": "biome check --write .",
581
+ typecheck: "tsc --noEmit",
582
+ test: "vitest run",
583
+ "test:watch": "vitest",
584
+ prepublishOnly: "npm run lint && npm run build"
585
+ },
586
+ keywords: [
587
+ "codex",
588
+ "plugin",
589
+ "openlibrary",
590
+ "metadata",
591
+ "books",
592
+ "isbn"
593
+ ],
594
+ author: "Codex",
595
+ license: "MIT",
596
+ engines: {
597
+ node: ">=22.0.0"
598
+ },
599
+ dependencies: {
600
+ "@ashdev/codex-plugin-sdk": "file:../sdk-typescript"
601
+ },
602
+ devDependencies: {
603
+ "@biomejs/biome": "^2.3.13",
604
+ "@types/node": "^22.0.0",
605
+ esbuild: "^0.24.0",
606
+ typescript: "^5.7.0",
607
+ vitest: "^3.0.0"
608
+ }
609
+ };
610
+
611
+ // src/manifest.ts
612
+ var DEFAULT_MAX_RESULTS = 10;
613
+ var manifest = {
614
+ name: "metadata-openlibrary",
615
+ displayName: "Open Library",
616
+ version: package_default.version,
617
+ description: "Fetches book metadata from Open Library (openlibrary.org). Supports ISBN lookup and title search for EPUBs, PDFs, and other book formats.",
618
+ author: "Codex",
619
+ homepage: "https://openlibrary.org",
620
+ protocolVersion: "1.0",
621
+ capabilities: {
622
+ // Book metadata provider only (not series)
623
+ metadataProvider: ["book"]
624
+ },
625
+ configSchema: {
626
+ description: "Configuration options for the Open Library plugin",
627
+ fields: [
628
+ {
629
+ key: "maxResults",
630
+ label: "Maximum Results",
631
+ description: "Maximum number of results to return for search queries (1-50)",
632
+ type: "number",
633
+ required: false,
634
+ default: DEFAULT_MAX_RESULTS,
635
+ example: 20
636
+ }
637
+ ]
638
+ }
639
+ };
640
+
641
+ // src/mapper.ts
642
+ init_api();
643
+ function mapSearchDocToSearchResult(doc) {
644
+ const year = doc.first_publish_year;
645
+ const coverUrl = doc.cover_i ? getCoverUrlById(doc.cover_i, "M") : void 0;
646
+ let relevanceScore = 0.5;
647
+ if (doc.author_name?.length) relevanceScore += 0.1;
648
+ if (doc.isbn?.length) relevanceScore += 0.15;
649
+ if (doc.cover_i) relevanceScore += 0.1;
650
+ if (doc.first_publish_year) relevanceScore += 0.05;
651
+ if (doc.subject?.length) relevanceScore += 0.05;
652
+ if (doc.ratings_count && doc.ratings_count > 0) relevanceScore += 0.05;
653
+ return {
654
+ externalId: doc.key,
655
+ // Work key, e.g., "/works/OL45883W"
656
+ title: doc.title,
657
+ alternateTitles: doc.subtitle ? [doc.subtitle] : [],
658
+ year,
659
+ coverUrl,
660
+ relevanceScore: Math.min(1, relevanceScore),
661
+ preview: {
662
+ genres: doc.subject?.slice(0, 5) || [],
663
+ rating: doc.ratings_average ? Math.round(doc.ratings_average * 2) / 2 : void 0,
664
+ authors: doc.author_name?.slice(0, 3) || [],
665
+ description: doc.publisher?.length ? `Published by ${doc.publisher[0]}` : void 0
666
+ }
667
+ };
668
+ }
669
+ async function resolveAuthors(authorRefs) {
670
+ if (!authorRefs?.length) return [];
671
+ const authors = [];
672
+ for (const ref of authorRefs) {
673
+ const key = ref.author?.key || ref.key;
674
+ if (!key) continue;
675
+ const authorData = await getAuthor(key);
676
+ if (authorData) {
677
+ authors.push({
678
+ name: authorData.name,
679
+ key,
680
+ sortName: authorData.personal_name || void 0
681
+ });
682
+ }
683
+ }
684
+ return authors;
685
+ }
686
+ function mapToBookAuthors(authors) {
687
+ return authors.map((author) => ({
688
+ name: author.name,
689
+ role: "author",
690
+ sortName: author.sortName
691
+ }));
692
+ }
693
+ function buildCoverUrls(isbn, coverId) {
694
+ const covers = [];
695
+ if (isbn) {
696
+ covers.push({
697
+ url: getCoverUrlByIsbn(isbn, "S"),
698
+ size: "small"
699
+ });
700
+ covers.push({
701
+ url: getCoverUrlByIsbn(isbn, "M"),
702
+ size: "medium"
703
+ });
704
+ covers.push({
705
+ url: getCoverUrlByIsbn(isbn, "L"),
706
+ size: "large"
707
+ });
708
+ } else if (coverId) {
709
+ covers.push({
710
+ url: getCoverUrlById(coverId, "S"),
711
+ size: "small"
712
+ });
713
+ covers.push({
714
+ url: getCoverUrlById(coverId, "M"),
715
+ size: "medium"
716
+ });
717
+ covers.push({
718
+ url: getCoverUrlById(coverId, "L"),
719
+ size: "large"
720
+ });
721
+ }
722
+ return covers;
723
+ }
724
+ function buildExternalLinks(editionKey, workKey) {
725
+ const links = [
726
+ {
727
+ url: buildOpenLibraryUrl(editionKey),
728
+ label: "Open Library (Edition)",
729
+ linkType: "provider"
730
+ }
731
+ ];
732
+ if (workKey) {
733
+ links.push({
734
+ url: buildOpenLibraryUrl(workKey),
735
+ label: "Open Library (Work)",
736
+ linkType: "provider"
737
+ });
738
+ }
739
+ return links;
740
+ }
741
+ function collectIsbns(edition) {
742
+ const isbns = [];
743
+ if (edition.isbn_13?.length) {
744
+ isbns.push(...edition.isbn_13);
745
+ }
746
+ if (edition.isbn_10?.length) {
747
+ isbns.push(...edition.isbn_10);
748
+ }
749
+ return [...new Set(isbns)];
750
+ }
751
+ async function mapEditionToBookMetadata(edition, workData) {
752
+ const authorRefs = edition.authors || workData?.authors;
753
+ const authors = await resolveAuthors(authorRefs);
754
+ const isbns = collectIsbns(edition);
755
+ const primaryIsbn = isbns[0];
756
+ const coverId = edition.covers?.[0] || workData?.covers?.[0];
757
+ const description = parseDescription(edition.description) || parseDescription(workData?.description);
758
+ const subjects = [...edition.subjects || [], ...workData?.subjects || []];
759
+ const uniqueSubjects = [...new Set(subjects)];
760
+ const year = parseYear(edition.publish_date);
761
+ const originalYear = parseYear(workData?.first_publish_date);
762
+ const language = parseLanguage(edition.languages?.[0]?.key);
763
+ const externalRatings = [];
764
+ const workKey = edition.works?.[0]?.key || workData?.key;
765
+ const externalId = workKey || edition.key;
766
+ return {
767
+ externalId,
768
+ externalUrl: buildOpenLibraryUrl(externalId),
769
+ // Core fields
770
+ title: edition.title,
771
+ subtitle: edition.subtitle || workData?.subtitle,
772
+ alternateTitles: [],
773
+ summary: description,
774
+ bookType: detectBookType(edition),
775
+ // Book-specific fields
776
+ pageCount: edition.number_of_pages,
777
+ year,
778
+ // ISBN
779
+ isbn: primaryIsbn,
780
+ isbns,
781
+ // Edition info
782
+ edition: edition.edition_name,
783
+ originalTitle: workData?.title !== edition.title ? workData?.title : void 0,
784
+ originalYear,
785
+ language,
786
+ // Taxonomy
787
+ genres: [],
788
+ // Open Library doesn't have genres, just subjects
789
+ tags: [],
790
+ subjects: uniqueSubjects.slice(0, 20),
791
+ // Limit to 20 subjects
792
+ // Credits
793
+ authors: mapToBookAuthors(authors),
794
+ artists: [],
795
+ // Open Library doesn't track artists separately
796
+ publisher: edition.publishers?.[0],
797
+ // Media
798
+ coverUrl: primaryIsbn ? getCoverUrlByIsbn(primaryIsbn, "L") : coverId ? getCoverUrlById(coverId, "L") : void 0,
799
+ covers: buildCoverUrls(primaryIsbn, coverId),
800
+ // Rating
801
+ externalRatings,
802
+ awards: [],
803
+ // Links
804
+ externalLinks: buildExternalLinks(edition.key, workKey)
805
+ };
806
+ }
807
+ function detectBookType(edition) {
808
+ const format = edition.physical_format?.toLowerCase();
809
+ if (format) {
810
+ if (format.includes("comic") || format.includes("graphic novel")) {
811
+ return "graphic_novel";
812
+ }
813
+ if (format.includes("manga")) {
814
+ return "manga";
815
+ }
816
+ if (format.includes("magazine") || format.includes("periodical")) {
817
+ return "magazine";
818
+ }
819
+ }
820
+ const subjects = (edition.subjects || []).join(" ").toLowerCase();
821
+ if (subjects.includes("graphic novel") || subjects.includes("comics")) {
822
+ return "graphic_novel";
823
+ }
824
+ if (subjects.includes("manga")) {
825
+ return "manga";
826
+ }
827
+ return "novel";
828
+ }
829
+ async function getFullBookMetadata(editionOrWorkKey, isbn) {
830
+ if (isbn) {
831
+ const { getEditionByIsbn: getEditionByIsbn2 } = await Promise.resolve().then(() => (init_api(), api_exports));
832
+ const edition = await getEditionByIsbn2(isbn);
833
+ if (edition) {
834
+ const workKey = edition.works?.[0]?.key;
835
+ const workData = workKey ? await getWork(workKey) : null;
836
+ return mapEditionToBookMetadata(edition, workData);
837
+ }
838
+ }
839
+ if (editionOrWorkKey.includes("/works/")) {
840
+ const workData = await getWork(editionOrWorkKey);
841
+ if (!workData) return null;
842
+ const editions = await getWorkEditions(editionOrWorkKey, 5);
843
+ if (editions.length > 0) {
844
+ const editionWithIsbn = editions.find((e) => e.isbn_13?.length || e.isbn_10?.length);
845
+ const edition = editionWithIsbn || editions[0];
846
+ return mapEditionToBookMetadata(edition, workData);
847
+ }
848
+ const authors = await resolveAuthors(workData.authors);
849
+ const coverId = workData.covers?.[0];
850
+ return {
851
+ externalId: workData.key,
852
+ externalUrl: buildOpenLibraryUrl(workData.key),
853
+ title: workData.title,
854
+ subtitle: workData.subtitle,
855
+ alternateTitles: [],
856
+ summary: parseDescription(workData.description),
857
+ isbns: [],
858
+ genres: [],
859
+ tags: [],
860
+ subjects: workData.subjects?.slice(0, 20) || [],
861
+ authors: mapToBookAuthors(authors),
862
+ artists: [],
863
+ coverUrl: coverId ? getCoverUrlById(coverId, "L") : void 0,
864
+ covers: coverId ? [
865
+ { url: getCoverUrlById(coverId, "S"), size: "small" },
866
+ { url: getCoverUrlById(coverId, "M"), size: "medium" },
867
+ { url: getCoverUrlById(coverId, "L"), size: "large" }
868
+ ] : [],
869
+ externalRatings: [],
870
+ awards: [],
871
+ externalLinks: [
872
+ {
873
+ url: buildOpenLibraryUrl(workData.key),
874
+ label: "Open Library",
875
+ linkType: "provider"
876
+ }
877
+ ]
878
+ };
879
+ }
880
+ const url = `https://openlibrary.org${editionOrWorkKey}.json`;
881
+ try {
882
+ const response = await fetch(url, {
883
+ headers: {
884
+ "User-Agent": "Codex/1.0 (https://github.com/AshDevFr/codex; codex-plugin)",
885
+ Accept: "application/json"
886
+ }
887
+ });
888
+ if (response.ok) {
889
+ const edition = await response.json();
890
+ const workKey = edition.works?.[0]?.key;
891
+ const workData = workKey ? await getWork(workKey) : null;
892
+ return mapEditionToBookMetadata(edition, workData);
893
+ }
894
+ } catch {
895
+ }
896
+ return null;
897
+ }
898
+
899
+ // src/index.ts
900
+ var logger = createLogger({ name: "openlibrary", level: "info" });
901
+ var config = {
902
+ maxResults: DEFAULT_MAX_RESULTS
903
+ };
904
+ var bookProvider = {
905
+ /**
906
+ * Search for books by ISBN or title/author query
907
+ *
908
+ * If ISBN is provided, it takes priority for direct lookup.
909
+ * Otherwise, falls back to title/author search.
910
+ */
911
+ async search(params) {
912
+ const { isbn, query, author, limit } = params;
913
+ const maxResults = Math.min(limit || config.maxResults, 50);
914
+ if (isbn && isValidIsbn(isbn)) {
915
+ const edition = await getEditionByIsbn(isbn);
916
+ if (edition) {
917
+ const workKey = edition.works?.[0]?.key;
918
+ const workData = workKey ? await getWork(workKey) : null;
919
+ const metadata = await mapEditionToBookMetadata(edition, workData);
920
+ return {
921
+ results: [
922
+ {
923
+ externalId: metadata.externalId,
924
+ title: metadata.title || "Unknown",
925
+ alternateTitles: metadata.subtitle ? [metadata.subtitle] : [],
926
+ year: metadata.year,
927
+ coverUrl: metadata.coverUrl,
928
+ relevanceScore: 1,
929
+ // Perfect match by ISBN
930
+ preview: {
931
+ genres: metadata.subjects.slice(0, 5),
932
+ authors: metadata.authors.map((a) => a.name)
933
+ }
934
+ }
935
+ ]
936
+ };
937
+ }
938
+ if (!query) {
939
+ return { results: [] };
940
+ }
941
+ }
942
+ if (!query) {
943
+ return { results: [] };
944
+ }
945
+ const searchResponse = await searchBooks(query, {
946
+ author,
947
+ limit: maxResults
948
+ });
949
+ if (!searchResponse?.docs?.length) {
950
+ return { results: [] };
951
+ }
952
+ return {
953
+ results: searchResponse.docs.map(mapSearchDocToSearchResult)
954
+ };
955
+ },
956
+ /**
957
+ * Get full book metadata by external ID
958
+ *
959
+ * The external ID can be:
960
+ * - A work key: "/works/OL45883W"
961
+ * - An edition key: "/books/OL7353617M"
962
+ */
963
+ async get(params) {
964
+ const { externalId } = params;
965
+ const metadata = await getFullBookMetadata(externalId);
966
+ if (metadata) {
967
+ return metadata;
968
+ }
969
+ return {
970
+ externalId,
971
+ externalUrl: `https://openlibrary.org${externalId.startsWith("/") ? externalId : `/${externalId}`}`,
972
+ alternateTitles: [],
973
+ isbns: [],
974
+ genres: [],
975
+ tags: [],
976
+ subjects: [],
977
+ authors: [],
978
+ artists: [],
979
+ covers: [],
980
+ externalRatings: [],
981
+ awards: [],
982
+ externalLinks: [
983
+ {
984
+ url: `https://openlibrary.org${externalId.startsWith("/") ? externalId : `/${externalId}`}`,
985
+ label: "Open Library",
986
+ linkType: "provider"
987
+ }
988
+ ]
989
+ };
990
+ },
991
+ /**
992
+ * Auto-match a book using available identifiers
993
+ *
994
+ * Match priority:
995
+ * 1. ISBN (if provided) - highest confidence
996
+ * 2. Title + author search - lower confidence
997
+ */
998
+ async match(params) {
999
+ const { title, authors, isbn, year } = params;
1000
+ if (isbn && isValidIsbn(isbn)) {
1001
+ const edition = await getEditionByIsbn(isbn);
1002
+ if (edition) {
1003
+ const workKey = edition.works?.[0]?.key;
1004
+ const workData = workKey ? await getWork(workKey) : null;
1005
+ const metadata = await mapEditionToBookMetadata(edition, workData);
1006
+ return {
1007
+ match: {
1008
+ externalId: metadata.externalId,
1009
+ title: metadata.title || "Unknown",
1010
+ alternateTitles: metadata.subtitle ? [metadata.subtitle] : [],
1011
+ year: metadata.year,
1012
+ coverUrl: metadata.coverUrl,
1013
+ relevanceScore: 1,
1014
+ preview: {
1015
+ genres: metadata.subjects.slice(0, 5),
1016
+ authors: metadata.authors.map((a) => a.name)
1017
+ }
1018
+ },
1019
+ confidence: 0.99,
1020
+ // Very high confidence for ISBN match
1021
+ alternatives: []
1022
+ };
1023
+ }
1024
+ }
1025
+ const searchQuery = authors?.length ? `${title} ${authors[0]}` : title;
1026
+ const searchResponse = await searchBooks(searchQuery, {
1027
+ limit: 5
1028
+ });
1029
+ if (!searchResponse?.docs?.length) {
1030
+ return {
1031
+ match: null,
1032
+ confidence: 0,
1033
+ alternatives: []
1034
+ };
1035
+ }
1036
+ const results = searchResponse.docs.map(mapSearchDocToSearchResult);
1037
+ const bestMatch = results[0];
1038
+ let confidence = bestMatch.relevanceScore || 0.5;
1039
+ const normalizedTitle = title.toLowerCase().trim();
1040
+ const normalizedMatchTitle = bestMatch.title.toLowerCase().trim();
1041
+ if (normalizedTitle === normalizedMatchTitle) {
1042
+ confidence = Math.min(1, confidence + 0.3);
1043
+ } else if (normalizedMatchTitle.includes(normalizedTitle) || normalizedTitle.includes(normalizedMatchTitle)) {
1044
+ confidence = Math.min(1, confidence + 0.15);
1045
+ }
1046
+ if (year && bestMatch.year === year) {
1047
+ confidence = Math.min(1, confidence + 0.1);
1048
+ }
1049
+ confidence = Math.min(confidence, 0.85);
1050
+ return {
1051
+ match: bestMatch,
1052
+ confidence,
1053
+ alternatives: results.slice(1)
1054
+ };
1055
+ }
1056
+ };
1057
+ createMetadataPlugin({
1058
+ manifest,
1059
+ bookProvider,
1060
+ logLevel: "info",
1061
+ onInitialize(params) {
1062
+ const maxResults = params.config?.maxResults;
1063
+ if (maxResults !== void 0) {
1064
+ config.maxResults = Math.min(Math.max(1, maxResults), 50);
1065
+ }
1066
+ logger.info(`Plugin initialized (maxResults: ${config.maxResults})`);
1067
+ }
1068
+ });
1069
+ logger.info("Open Library plugin started");
1070
+ //# sourceMappingURL=index.js.map