@ashdev/codex-plugin-metadata-openlibrary 1.37.0 → 1.38.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 CHANGED
@@ -9,606 +9,407 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
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(",");
12
+ // node_modules/@ashdev/codex-plugin-sdk/dist/types/rpc.js
13
+ var JSON_RPC_ERROR_CODES;
14
+ var init_rpc = __esm({
15
+ "node_modules/@ashdev/codex-plugin-sdk/dist/types/rpc.js"() {
16
+ JSON_RPC_ERROR_CODES = {
17
+ /** Invalid JSON was received */
18
+ PARSE_ERROR: -32700,
19
+ /** The JSON sent is not a valid Request object */
20
+ INVALID_REQUEST: -32600,
21
+ /** The method does not exist / is not available */
22
+ METHOD_NOT_FOUND: -32601,
23
+ /** Invalid method parameter(s) */
24
+ INVALID_PARAMS: -32602,
25
+ /** Internal JSON-RPC error */
26
+ INTERNAL_ERROR: -32603
27
+ };
247
28
  }
248
29
  });
249
30
 
250
- // node_modules/@ashdev/codex-plugin-sdk/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
31
  // node_modules/@ashdev/codex-plugin-sdk/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
32
+ var PluginError;
33
+ var init_errors = __esm({
34
+ "node_modules/@ashdev/codex-plugin-sdk/dist/errors.js"() {
35
+ init_rpc();
36
+ PluginError = class extends Error {
37
+ data;
38
+ constructor(message, data) {
39
+ super(message);
40
+ this.name = this.constructor.name;
41
+ this.data = data;
42
+ }
43
+ /**
44
+ * Convert to JSON-RPC error format
45
+ */
46
+ toJsonRpcError() {
47
+ return {
48
+ code: this.code,
49
+ message: this.message,
50
+ data: this.data
51
+ };
52
+ }
280
53
  };
281
54
  }
282
- };
55
+ });
283
56
 
284
57
  // node_modules/@ashdev/codex-plugin-sdk/dist/request-context.js
285
58
  import { AsyncLocalStorage } from "node:async_hooks";
286
- var store = new AsyncLocalStorage();
287
59
  function runWithParentRequestId(forwardRequestId, fn) {
288
60
  return store.run(forwardRequestId, fn);
289
61
  }
290
62
  function currentParentRequestId() {
291
63
  return store.getStore();
292
- }
293
-
294
- // node_modules/@ashdev/codex-plugin-sdk/dist/host-rpc.js
295
- var HostRpcError = class extends Error {
296
- code;
297
- data;
298
- constructor(message, code, data) {
299
- super(message);
300
- this.code = code;
301
- this.data = data;
302
- this.name = "HostRpcError";
303
- }
304
- };
305
- var HostRpcClient = class {
306
- // Start the counter high so it can't collide with PluginStorage's id space.
307
- // `Number.MAX_SAFE_INTEGER` is far above this, so we have plenty of room
308
- // before wrapping (and we never expect a single plugin lifetime to issue
309
- // more than ~9 quintillion calls).
310
- nextId = 1e9;
311
- pendingRequests = /* @__PURE__ */ new Map();
312
- writeFn;
313
- /**
314
- * @param writeFn - Optional custom write function (defaults to
315
- * `process.stdout.write`). Useful for testing.
316
- */
317
- constructor(writeFn) {
318
- this.writeFn = writeFn ?? ((line) => {
319
- process.stdout.write(line);
320
- });
321
- }
322
- /**
323
- * Send a JSON-RPC request to the host and resolve with the result.
324
- *
325
- * @param method - JSON-RPC method name (e.g. `"releases/list_tracked"`).
326
- * @param params - Method-specific params. Pass `undefined` when the method
327
- * takes no params.
328
- */
329
- async call(method, params) {
330
- const id = this.nextId++;
331
- const parent = currentParentRequestId();
332
- const request = {
333
- jsonrpc: "2.0",
334
- id,
335
- method,
336
- params,
337
- ...parent !== void 0 ? { parentRequestId: parent } : {}
338
- };
339
- return new Promise((resolve, reject) => {
340
- this.pendingRequests.set(id, {
341
- resolve: (v) => resolve(v),
342
- reject
343
- });
344
- try {
345
- this.writeFn(`${JSON.stringify(request)}
346
- `);
347
- } catch (err) {
348
- this.pendingRequests.delete(id);
349
- const message = err instanceof Error ? err.message : "Unknown write error";
350
- reject(new HostRpcError(`Failed to send request: ${message}`, -1));
351
- }
352
- });
353
- }
354
- /**
355
- * Process an incoming JSON-RPC response line. Returns `true` if this
356
- * client owned the response id and resolved it, `false` otherwise (so
357
- * other clients can try).
358
- *
359
- * Called by the plugin server's main loop on every response.
360
- */
361
- handleResponse(line) {
362
- const trimmed = line.trim();
363
- if (!trimmed)
364
- return false;
365
- let parsed;
366
- try {
367
- parsed = JSON.parse(trimmed);
368
- } catch {
369
- return false;
370
- }
371
- const obj = parsed;
372
- if (obj.method !== void 0)
373
- return false;
374
- const rawId = obj.id;
375
- if (typeof rawId !== "number")
376
- return false;
377
- if (!this.pendingRequests.has(rawId))
378
- return false;
379
- const pending = this.pendingRequests.get(rawId);
380
- if (!pending)
381
- return false;
382
- this.pendingRequests.delete(rawId);
383
- if ("error" in obj && obj.error) {
384
- const err = obj.error;
385
- pending.reject(new HostRpcError(err.message, err.code, err.data));
386
- } else {
387
- pending.resolve(obj.result);
388
- }
389
- return true;
390
- }
391
- /** Reject all pending requests (e.g. on shutdown). */
392
- cancelAll() {
393
- for (const [, pending] of this.pendingRequests) {
394
- pending.reject(new HostRpcError("Host RPC client stopped", -1));
395
- }
396
- this.pendingRequests.clear();
397
- }
398
- };
399
-
400
- // node_modules/@ashdev/codex-plugin-sdk/dist/logger.js
401
- var LOG_LEVELS = {
402
- debug: 0,
403
- info: 1,
404
- warn: 2,
405
- error: 3
406
- };
407
- var Logger = class {
408
- name;
409
- minLevel;
410
- timestamps;
411
- constructor(options) {
412
- this.name = options.name;
413
- this.minLevel = LOG_LEVELS[options.level ?? "info"];
414
- this.timestamps = options.timestamps ?? true;
415
- }
416
- shouldLog(level) {
417
- return LOG_LEVELS[level] >= this.minLevel;
418
- }
419
- format(level, message, data) {
420
- const parts = [];
421
- if (this.timestamps) {
422
- parts.push((/* @__PURE__ */ new Date()).toISOString());
423
- }
424
- parts.push(`[${level.toUpperCase()}]`);
425
- parts.push(`[${this.name}]`);
426
- parts.push(message);
427
- if (data !== void 0) {
428
- if (data instanceof Error) {
429
- parts.push(`- ${data.message}`);
430
- if (data.stack) {
431
- parts.push(`
432
- ${data.stack}`);
433
- }
434
- } else if (typeof data === "object") {
435
- parts.push(`- ${JSON.stringify(data)}`);
436
- } else {
437
- parts.push(`- ${String(data)}`);
438
- }
439
- }
440
- return parts.join(" ");
441
- }
442
- log(level, message, data) {
443
- if (this.shouldLog(level)) {
444
- process.stderr.write(`${this.format(level, message, data)}
445
- `);
446
- }
447
- }
448
- debug(message, data) {
449
- this.log("debug", message, data);
450
- }
451
- info(message, data) {
452
- this.log("info", message, data);
453
- }
454
- warn(message, data) {
455
- this.log("warn", message, data);
456
- }
457
- error(message, data) {
458
- this.log("error", message, data);
459
- }
460
- };
461
- function createLogger(options) {
462
- return new Logger(options);
463
- }
464
-
465
- // node_modules/@ashdev/codex-plugin-sdk/dist/server.js
466
- import { createInterface } from "node:readline";
467
-
468
- // node_modules/@ashdev/codex-plugin-sdk/dist/storage.js
469
- var StorageError = class extends Error {
470
- code;
471
- data;
472
- constructor(message, code, data) {
473
- super(message);
474
- this.code = code;
475
- this.data = data;
476
- this.name = "StorageError";
477
- }
478
- };
479
- var PluginStorage = class {
480
- nextId = 1;
481
- pendingRequests = /* @__PURE__ */ new Map();
482
- writeFn;
483
- /**
484
- * Create a new storage client.
485
- *
486
- * @param writeFn - Optional custom write function (defaults to process.stdout.write).
487
- * Useful for testing or custom transport layers.
488
- */
489
- constructor(writeFn) {
490
- this.writeFn = writeFn ?? ((line) => {
491
- process.stdout.write(line);
492
- });
493
- }
494
- /**
495
- * Get a value by key
496
- *
497
- * @param key - Storage key to retrieve
498
- * @returns The stored data and optional expiration, or null data if key doesn't exist
499
- */
500
- async get(key) {
501
- return await this.sendRequest("storage/get", { key });
502
- }
503
- /**
504
- * Set a value by key (upsert - creates or updates)
505
- *
506
- * @param key - Storage key
507
- * @param data - JSON-serializable data to store
508
- * @param expiresAt - Optional expiration timestamp (ISO 8601)
509
- * @returns Success indicator
510
- */
511
- async set(key, data, expiresAt) {
512
- const params = { key, data };
513
- if (expiresAt !== void 0) {
514
- params.expiresAt = expiresAt;
515
- }
516
- return await this.sendRequest("storage/set", params);
517
- }
518
- /**
519
- * Delete a value by key
520
- *
521
- * @param key - Storage key to delete
522
- * @returns Whether the key existed and was deleted
523
- */
524
- async delete(key) {
525
- return await this.sendRequest("storage/delete", { key });
526
- }
527
- /**
528
- * List all keys for this plugin instance (excluding expired)
529
- *
530
- * @returns List of key entries with metadata
531
- */
532
- async list() {
533
- return await this.sendRequest("storage/list", {});
534
- }
535
- /**
536
- * Clear all data for this plugin instance
537
- *
538
- * @returns Number of entries deleted
539
- */
540
- async clear() {
541
- return await this.sendRequest("storage/clear", {});
542
- }
543
- /**
544
- * Handle an incoming JSON-RPC response line from the host.
545
- *
546
- * Call this method from your readline handler to deliver responses
547
- * back to pending storage requests.
548
- */
549
- handleResponse(line) {
550
- const trimmed = line.trim();
551
- if (!trimmed)
552
- return;
553
- let parsed;
554
- try {
555
- parsed = JSON.parse(trimmed);
556
- } catch {
557
- return;
558
- }
559
- const obj = parsed;
560
- if (obj.method !== void 0) {
561
- return;
562
- }
563
- const id = obj.id;
564
- if (id === void 0 || id === null)
565
- return;
566
- const pending = this.pendingRequests.get(id);
567
- if (!pending)
568
- return;
569
- this.pendingRequests.delete(id);
570
- if ("error" in obj && obj.error) {
571
- const err = obj.error;
572
- pending.reject(new StorageError(err.message, err.code, err.data));
573
- } else {
574
- pending.resolve(obj.result);
575
- }
64
+ }
65
+ var store;
66
+ var init_request_context = __esm({
67
+ "node_modules/@ashdev/codex-plugin-sdk/dist/request-context.js"() {
68
+ store = new AsyncLocalStorage();
576
69
  }
577
- /**
578
- * Cancel all pending requests (e.g. on shutdown).
579
- */
580
- cancelAll() {
581
- for (const [, pending] of this.pendingRequests) {
582
- pending.reject(new StorageError("Storage client stopped", -1));
583
- }
584
- this.pendingRequests.clear();
70
+ });
71
+
72
+ // node_modules/@ashdev/codex-plugin-sdk/dist/host-rpc.js
73
+ var HostRpcError, HostRpcClient;
74
+ var init_host_rpc = __esm({
75
+ "node_modules/@ashdev/codex-plugin-sdk/dist/host-rpc.js"() {
76
+ init_request_context();
77
+ HostRpcError = class extends Error {
78
+ code;
79
+ data;
80
+ constructor(message, code, data) {
81
+ super(message);
82
+ this.code = code;
83
+ this.data = data;
84
+ this.name = "HostRpcError";
85
+ }
86
+ };
87
+ HostRpcClient = class {
88
+ // Start the counter high so it can't collide with PluginStorage's id space.
89
+ // `Number.MAX_SAFE_INTEGER` is far above this, so we have plenty of room
90
+ // before wrapping (and we never expect a single plugin lifetime to issue
91
+ // more than ~9 quintillion calls).
92
+ nextId = 1e9;
93
+ pendingRequests = /* @__PURE__ */ new Map();
94
+ writeFn;
95
+ /**
96
+ * @param writeFn - Optional custom write function (defaults to
97
+ * `process.stdout.write`). Useful for testing.
98
+ */
99
+ constructor(writeFn) {
100
+ this.writeFn = writeFn ?? ((line) => {
101
+ process.stdout.write(line);
102
+ });
103
+ }
104
+ /**
105
+ * Send a JSON-RPC request to the host and resolve with the result.
106
+ *
107
+ * @param method - JSON-RPC method name (e.g. `"releases/list_tracked"`).
108
+ * @param params - Method-specific params. Pass `undefined` when the method
109
+ * takes no params.
110
+ */
111
+ async call(method, params) {
112
+ const id = this.nextId++;
113
+ const parent = currentParentRequestId();
114
+ const request = {
115
+ jsonrpc: "2.0",
116
+ id,
117
+ method,
118
+ params,
119
+ ...parent !== void 0 ? { parentRequestId: parent } : {}
120
+ };
121
+ return new Promise((resolve, reject) => {
122
+ this.pendingRequests.set(id, {
123
+ resolve: (v) => resolve(v),
124
+ reject
125
+ });
126
+ try {
127
+ this.writeFn(`${JSON.stringify(request)}
128
+ `);
129
+ } catch (err) {
130
+ this.pendingRequests.delete(id);
131
+ const message = err instanceof Error ? err.message : "Unknown write error";
132
+ reject(new HostRpcError(`Failed to send request: ${message}`, -1));
133
+ }
134
+ });
135
+ }
136
+ /**
137
+ * Process an incoming JSON-RPC response line. Returns `true` if this
138
+ * client owned the response id and resolved it, `false` otherwise (so
139
+ * other clients can try).
140
+ *
141
+ * Called by the plugin server's main loop on every response.
142
+ */
143
+ handleResponse(line) {
144
+ const trimmed = line.trim();
145
+ if (!trimmed)
146
+ return false;
147
+ let parsed;
148
+ try {
149
+ parsed = JSON.parse(trimmed);
150
+ } catch {
151
+ return false;
152
+ }
153
+ const obj = parsed;
154
+ if (obj.method !== void 0)
155
+ return false;
156
+ const rawId = obj.id;
157
+ if (typeof rawId !== "number")
158
+ return false;
159
+ if (!this.pendingRequests.has(rawId))
160
+ return false;
161
+ const pending = this.pendingRequests.get(rawId);
162
+ if (!pending)
163
+ return false;
164
+ this.pendingRequests.delete(rawId);
165
+ if ("error" in obj && obj.error) {
166
+ const err = obj.error;
167
+ pending.reject(new HostRpcError(err.message, err.code, err.data));
168
+ } else {
169
+ pending.resolve(obj.result);
170
+ }
171
+ return true;
172
+ }
173
+ /** Reject all pending requests (e.g. on shutdown). */
174
+ cancelAll() {
175
+ for (const [, pending] of this.pendingRequests) {
176
+ pending.reject(new HostRpcError("Host RPC client stopped", -1));
177
+ }
178
+ this.pendingRequests.clear();
179
+ }
180
+ };
585
181
  }
586
- // ===========================================================================
587
- // Internal
588
- // ===========================================================================
589
- sendRequest(method, params) {
590
- const id = this.nextId++;
591
- const request = {
592
- jsonrpc: "2.0",
593
- id,
594
- method,
595
- params
182
+ });
183
+
184
+ // node_modules/@ashdev/codex-plugin-sdk/dist/logger.js
185
+ function createLogger(options) {
186
+ return new Logger(options);
187
+ }
188
+ var LOG_LEVELS, Logger;
189
+ var init_logger = __esm({
190
+ "node_modules/@ashdev/codex-plugin-sdk/dist/logger.js"() {
191
+ LOG_LEVELS = {
192
+ debug: 0,
193
+ info: 1,
194
+ warn: 2,
195
+ error: 3
596
196
  };
597
- return new Promise((resolve, reject) => {
598
- this.pendingRequests.set(id, { resolve, reject });
599
- try {
600
- this.writeFn(`${JSON.stringify(request)}
197
+ Logger = class {
198
+ name;
199
+ minLevel;
200
+ timestamps;
201
+ constructor(options) {
202
+ this.name = options.name;
203
+ this.minLevel = LOG_LEVELS[options.level ?? "info"];
204
+ this.timestamps = options.timestamps ?? true;
205
+ }
206
+ /**
207
+ * Change the minimum level at runtime. Plugins typically call this from
208
+ * `onInitialize` with the host-supplied `logLevel` so debug output can be
209
+ * toggled centrally via the `plugins.log_level` Codex config without a
210
+ * rebuild.
211
+ */
212
+ setLevel(level) {
213
+ this.minLevel = LOG_LEVELS[level];
214
+ }
215
+ shouldLog(level) {
216
+ return LOG_LEVELS[level] >= this.minLevel;
217
+ }
218
+ format(level, message, data) {
219
+ const parts = [];
220
+ if (this.timestamps) {
221
+ parts.push((/* @__PURE__ */ new Date()).toISOString());
222
+ }
223
+ parts.push(`[${level.toUpperCase()}]`);
224
+ parts.push(`[${this.name}]`);
225
+ parts.push(message);
226
+ if (data !== void 0) {
227
+ if (data instanceof Error) {
228
+ parts.push(`- ${data.message}`);
229
+ if (data.stack) {
230
+ parts.push(`
231
+ ${data.stack}`);
232
+ }
233
+ } else if (typeof data === "object") {
234
+ parts.push(`- ${JSON.stringify(data)}`);
235
+ } else {
236
+ parts.push(`- ${String(data)}`);
237
+ }
238
+ }
239
+ return parts.join(" ");
240
+ }
241
+ log(level, message, data) {
242
+ if (this.shouldLog(level)) {
243
+ process.stderr.write(`${this.format(level, message, data)}
601
244
  `);
602
- } catch (err) {
245
+ }
246
+ }
247
+ debug(message, data) {
248
+ this.log("debug", message, data);
249
+ }
250
+ info(message, data) {
251
+ this.log("info", message, data);
252
+ }
253
+ warn(message, data) {
254
+ this.log("warn", message, data);
255
+ }
256
+ error(message, data) {
257
+ this.log("error", message, data);
258
+ }
259
+ };
260
+ }
261
+ });
262
+
263
+ // node_modules/@ashdev/codex-plugin-sdk/dist/storage.js
264
+ var StorageError, PluginStorage;
265
+ var init_storage = __esm({
266
+ "node_modules/@ashdev/codex-plugin-sdk/dist/storage.js"() {
267
+ StorageError = class extends Error {
268
+ code;
269
+ data;
270
+ constructor(message, code, data) {
271
+ super(message);
272
+ this.code = code;
273
+ this.data = data;
274
+ this.name = "StorageError";
275
+ }
276
+ };
277
+ PluginStorage = class {
278
+ nextId = 1;
279
+ pendingRequests = /* @__PURE__ */ new Map();
280
+ writeFn;
281
+ /**
282
+ * Create a new storage client.
283
+ *
284
+ * @param writeFn - Optional custom write function (defaults to process.stdout.write).
285
+ * Useful for testing or custom transport layers.
286
+ */
287
+ constructor(writeFn) {
288
+ this.writeFn = writeFn ?? ((line) => {
289
+ process.stdout.write(line);
290
+ });
291
+ }
292
+ /**
293
+ * Get a value by key
294
+ *
295
+ * @param key - Storage key to retrieve
296
+ * @returns The stored data and optional expiration, or null data if key doesn't exist
297
+ */
298
+ async get(key) {
299
+ return await this.sendRequest("storage/get", { key });
300
+ }
301
+ /**
302
+ * Set a value by key (upsert - creates or updates)
303
+ *
304
+ * @param key - Storage key
305
+ * @param data - JSON-serializable data to store
306
+ * @param expiresAt - Optional expiration timestamp (ISO 8601)
307
+ * @returns Success indicator
308
+ */
309
+ async set(key, data, expiresAt) {
310
+ const params = { key, data };
311
+ if (expiresAt !== void 0) {
312
+ params.expiresAt = expiresAt;
313
+ }
314
+ return await this.sendRequest("storage/set", params);
315
+ }
316
+ /**
317
+ * Delete a value by key
318
+ *
319
+ * @param key - Storage key to delete
320
+ * @returns Whether the key existed and was deleted
321
+ */
322
+ async delete(key) {
323
+ return await this.sendRequest("storage/delete", { key });
324
+ }
325
+ /**
326
+ * List all keys for this plugin instance (excluding expired)
327
+ *
328
+ * @returns List of key entries with metadata
329
+ */
330
+ async list() {
331
+ return await this.sendRequest("storage/list", {});
332
+ }
333
+ /**
334
+ * Clear all data for this plugin instance
335
+ *
336
+ * @returns Number of entries deleted
337
+ */
338
+ async clear() {
339
+ return await this.sendRequest("storage/clear", {});
340
+ }
341
+ /**
342
+ * Handle an incoming JSON-RPC response line from the host.
343
+ *
344
+ * Call this method from your readline handler to deliver responses
345
+ * back to pending storage requests.
346
+ */
347
+ handleResponse(line) {
348
+ const trimmed = line.trim();
349
+ if (!trimmed)
350
+ return;
351
+ let parsed;
352
+ try {
353
+ parsed = JSON.parse(trimmed);
354
+ } catch {
355
+ return;
356
+ }
357
+ const obj = parsed;
358
+ if (obj.method !== void 0) {
359
+ return;
360
+ }
361
+ const id = obj.id;
362
+ if (id === void 0 || id === null)
363
+ return;
364
+ const pending = this.pendingRequests.get(id);
365
+ if (!pending)
366
+ return;
603
367
  this.pendingRequests.delete(id);
604
- const message = err instanceof Error ? err.message : "Unknown write error";
605
- reject(new StorageError(`Failed to send request: ${message}`, -1));
368
+ if ("error" in obj && obj.error) {
369
+ const err = obj.error;
370
+ pending.reject(new StorageError(err.message, err.code, err.data));
371
+ } else {
372
+ pending.resolve(obj.result);
373
+ }
606
374
  }
607
- });
375
+ /**
376
+ * Cancel all pending requests (e.g. on shutdown).
377
+ */
378
+ cancelAll() {
379
+ for (const [, pending] of this.pendingRequests) {
380
+ pending.reject(new StorageError("Storage client stopped", -1));
381
+ }
382
+ this.pendingRequests.clear();
383
+ }
384
+ // ===========================================================================
385
+ // Internal
386
+ // ===========================================================================
387
+ sendRequest(method, params) {
388
+ const id = this.nextId++;
389
+ const request = {
390
+ jsonrpc: "2.0",
391
+ id,
392
+ method,
393
+ params
394
+ };
395
+ return new Promise((resolve, reject) => {
396
+ this.pendingRequests.set(id, { resolve, reject });
397
+ try {
398
+ this.writeFn(`${JSON.stringify(request)}
399
+ `);
400
+ } catch (err) {
401
+ this.pendingRequests.delete(id);
402
+ const message = err instanceof Error ? err.message : "Unknown write error";
403
+ reject(new StorageError(`Failed to send request: ${message}`, -1));
404
+ }
405
+ });
406
+ }
407
+ };
608
408
  }
609
- };
409
+ });
610
410
 
611
411
  // node_modules/@ashdev/codex-plugin-sdk/dist/server.js
412
+ import { createInterface } from "node:readline";
612
413
  function validateStringFields(params, fields) {
613
414
  if (params === null || params === void 0) {
614
415
  return { field: "params", message: "params is required" };
@@ -766,6 +567,9 @@ async function handleRequest(request, manifest2, onInitialize, router, logger2,
766
567
  const initParams = params ?? {};
767
568
  initParams.storage = storage;
768
569
  initParams.hostRpc = hostRpc;
570
+ if (initParams.logLevel) {
571
+ logger2.setLevel(initParams.logLevel);
572
+ }
769
573
  if (onInitialize) {
770
574
  await onInitialize(initParams);
771
575
  }
@@ -785,115 +589,413 @@ async function handleRequest(request, manifest2, onInitialize, router, logger2,
785
589
  return null;
786
590
  }
787
591
  }
788
- const response = await router(method, params, id);
789
- if (response !== null) {
790
- return response;
592
+ const response = await router(method, params, id);
593
+ if (response !== null) {
594
+ return response;
595
+ }
596
+ return {
597
+ jsonrpc: "2.0",
598
+ id,
599
+ error: {
600
+ code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
601
+ message: `Method not found: ${method}`
602
+ }
603
+ };
604
+ }
605
+ function writeResponse(response) {
606
+ process.stdout.write(`${JSON.stringify(response)}
607
+ `);
608
+ }
609
+ function methodNotFound(id, message) {
610
+ return {
611
+ jsonrpc: "2.0",
612
+ id,
613
+ error: {
614
+ code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
615
+ message
616
+ }
617
+ };
618
+ }
619
+ function success(id, result) {
620
+ return { jsonrpc: "2.0", id, result };
621
+ }
622
+ function createMetadataPlugin(options) {
623
+ const { manifest: manifest2, provider, bookProvider: bookProvider2, onInitialize, logLevel } = options;
624
+ const contentTypes = manifest2.capabilities.metadataProvider;
625
+ if (contentTypes.includes("series") && !provider) {
626
+ throw new Error("Series metadata provider is required when 'series' is in metadataProvider capabilities");
627
+ }
628
+ if (contentTypes.includes("book") && !bookProvider2) {
629
+ throw new Error("Book metadata provider is required when 'book' is in metadataProvider capabilities");
630
+ }
631
+ const router = async (method, params, id) => {
632
+ switch (method) {
633
+ // Series metadata methods
634
+ case "metadata/series/search": {
635
+ if (!provider)
636
+ return methodNotFound(id, "This plugin does not support series metadata");
637
+ const err = validateSearchParams(params);
638
+ if (err)
639
+ return invalidParamsError(id, err);
640
+ return success(id, await provider.search(params));
641
+ }
642
+ case "metadata/series/get": {
643
+ if (!provider)
644
+ return methodNotFound(id, "This plugin does not support series metadata");
645
+ const err = validateGetParams(params);
646
+ if (err)
647
+ return invalidParamsError(id, err);
648
+ return success(id, await provider.get(params));
649
+ }
650
+ case "metadata/series/match": {
651
+ if (!provider)
652
+ return methodNotFound(id, "This plugin does not support series metadata");
653
+ if (!provider.match)
654
+ return methodNotFound(id, "This plugin does not support series match");
655
+ const err = validateMatchParams(params);
656
+ if (err)
657
+ return invalidParamsError(id, err);
658
+ return success(id, await provider.match(params));
659
+ }
660
+ // Book metadata methods
661
+ case "metadata/book/search": {
662
+ if (!bookProvider2)
663
+ return methodNotFound(id, "This plugin does not support book metadata");
664
+ const err = validateBookSearchParams(params);
665
+ if (err)
666
+ return invalidParamsError(id, err);
667
+ return success(id, await bookProvider2.search(params));
668
+ }
669
+ case "metadata/book/get": {
670
+ if (!bookProvider2)
671
+ return methodNotFound(id, "This plugin does not support book metadata");
672
+ const err = validateGetParams(params);
673
+ if (err)
674
+ return invalidParamsError(id, err);
675
+ return success(id, await bookProvider2.get(params));
676
+ }
677
+ case "metadata/book/match": {
678
+ if (!bookProvider2)
679
+ return methodNotFound(id, "This plugin does not support book metadata");
680
+ if (!bookProvider2.match)
681
+ return methodNotFound(id, "This plugin does not support book match");
682
+ const err = validateBookMatchParams(params);
683
+ if (err)
684
+ return invalidParamsError(id, err);
685
+ return success(id, await bookProvider2.match(params));
686
+ }
687
+ default:
688
+ return null;
689
+ }
690
+ };
691
+ createPluginServer({ manifest: manifest2, onInitialize, logLevel, router });
692
+ }
693
+ var init_server = __esm({
694
+ "node_modules/@ashdev/codex-plugin-sdk/dist/server.js"() {
695
+ init_errors();
696
+ init_host_rpc();
697
+ init_logger();
698
+ init_request_context();
699
+ init_storage();
700
+ init_rpc();
701
+ }
702
+ });
703
+
704
+ // node_modules/@ashdev/codex-plugin-sdk/dist/types/manifest.js
705
+ var init_manifest = __esm({
706
+ "node_modules/@ashdev/codex-plugin-sdk/dist/types/manifest.js"() {
707
+ }
708
+ });
709
+
710
+ // node_modules/@ashdev/codex-plugin-sdk/dist/types/releases.js
711
+ var init_releases = __esm({
712
+ "node_modules/@ashdev/codex-plugin-sdk/dist/types/releases.js"() {
713
+ }
714
+ });
715
+
716
+ // node_modules/@ashdev/codex-plugin-sdk/dist/types/index.js
717
+ var init_types = __esm({
718
+ "node_modules/@ashdev/codex-plugin-sdk/dist/types/index.js"() {
719
+ init_manifest();
720
+ init_releases();
721
+ init_rpc();
722
+ }
723
+ });
724
+
725
+ // node_modules/@ashdev/codex-plugin-sdk/dist/index.js
726
+ var init_dist = __esm({
727
+ "node_modules/@ashdev/codex-plugin-sdk/dist/index.js"() {
728
+ init_errors();
729
+ init_host_rpc();
730
+ init_logger();
731
+ init_server();
732
+ init_storage();
733
+ init_types();
734
+ }
735
+ });
736
+
737
+ // src/logger.ts
738
+ var logger;
739
+ var init_logger2 = __esm({
740
+ "src/logger.ts"() {
741
+ "use strict";
742
+ init_dist();
743
+ logger = createLogger({ name: "openlibrary", level: "info" });
744
+ }
745
+ });
746
+
747
+ // src/api.ts
748
+ var api_exports = {};
749
+ __export(api_exports, {
750
+ buildOpenLibraryUrl: () => buildOpenLibraryUrl,
751
+ clearCache: () => clearCache,
752
+ extractOlid: () => extractOlid,
753
+ getAuthor: () => getAuthor,
754
+ getCoverUrlById: () => getCoverUrlById,
755
+ getCoverUrlByIsbn: () => getCoverUrlByIsbn,
756
+ getCoverUrlByOlid: () => getCoverUrlByOlid,
757
+ getEditionByIsbn: () => getEditionByIsbn,
758
+ getWork: () => getWork,
759
+ getWorkEditions: () => getWorkEditions,
760
+ isValidIsbn: () => isValidIsbn,
761
+ normalizeIsbn: () => normalizeIsbn,
762
+ parseDescription: () => parseDescription,
763
+ parseLanguage: () => parseLanguage,
764
+ parseYear: () => parseYear,
765
+ searchBooks: () => searchBooks
766
+ });
767
+ function getCached(key) {
768
+ const entry = cache.get(key);
769
+ if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {
770
+ return entry.data;
771
+ }
772
+ if (entry) {
773
+ cache.delete(key);
774
+ }
775
+ return null;
776
+ }
777
+ function setCache(key, data) {
778
+ cache.set(key, { data, timestamp: Date.now() });
779
+ }
780
+ async function fetchJson(url, description) {
781
+ const cached = getCached(url);
782
+ if (cached !== null) {
783
+ logger.debug(`cache hit: ${description}`);
784
+ return cached;
785
+ }
786
+ try {
787
+ logger.debug(`GET ${url} (${description})`);
788
+ const response = await fetch(url, {
789
+ headers: {
790
+ "User-Agent": "Codex/1.0 (https://github.com/AshDevFr/codex; codex-plugin)",
791
+ Accept: "application/json"
792
+ }
793
+ });
794
+ if (!response.ok) {
795
+ if (response.status === 404) {
796
+ logger.debug(`404 not found: ${description}`);
797
+ return null;
798
+ }
799
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
800
+ }
801
+ const data = await response.json();
802
+ setCache(url, data);
803
+ logger.debug(`${response.status} OK, cached: ${description}`);
804
+ return data;
805
+ } catch (error) {
806
+ logger.error(`Failed to fetch ${description}`, error);
807
+ return null;
808
+ }
809
+ }
810
+ function normalizeIsbn(isbn) {
811
+ return isbn.replace(/[-\s]/g, "").toUpperCase();
812
+ }
813
+ function isValidIsbn(isbn) {
814
+ const normalized = normalizeIsbn(isbn);
815
+ return normalized.length === 10 || normalized.length === 13;
816
+ }
817
+ async function getEditionByIsbn(isbn) {
818
+ const normalized = normalizeIsbn(isbn);
819
+ const url = `${BASE_URL}/isbn/${normalized}.json`;
820
+ return fetchJson(url, `edition by ISBN ${normalized}`);
821
+ }
822
+ async function getWork(workKey) {
823
+ const key = workKey.startsWith("/works/") ? workKey : `/works/${workKey}`;
824
+ const url = `${BASE_URL}${key}.json`;
825
+ return fetchJson(url, `work ${key}`);
826
+ }
827
+ async function getWorkEditions(workKey, limit = 5) {
828
+ const key = workKey.startsWith("/works/") ? workKey : `/works/${workKey}`;
829
+ const url = `${BASE_URL}${key}/editions.json?limit=${limit}`;
830
+ const response = await fetchJson(url, `editions for ${key}`);
831
+ return response?.entries || [];
832
+ }
833
+ async function getAuthor(authorKey) {
834
+ const key = authorKey.startsWith("/authors/") ? authorKey : `/authors/${authorKey}`;
835
+ const url = `${BASE_URL}${key}.json`;
836
+ return fetchJson(url, `author ${key}`);
837
+ }
838
+ async function searchBooks(query, options = {}) {
839
+ const { author, limit = 10 } = options;
840
+ if (author) {
841
+ const params2 = new URLSearchParams({
842
+ title: query,
843
+ author,
844
+ fields: SEARCH_FIELDS,
845
+ limit: String(limit)
846
+ });
847
+ const url2 = `${BASE_URL}/search.json?${params2}`;
848
+ const response = await fetchJson(
849
+ url2,
850
+ `search title="${query}" author="${author}"`
851
+ );
852
+ if (response?.docs?.length) {
853
+ return response;
854
+ }
855
+ }
856
+ const params = new URLSearchParams({
857
+ q: query,
858
+ fields: SEARCH_FIELDS,
859
+ limit: String(limit)
860
+ });
861
+ if (author) {
862
+ params.set("author", author);
791
863
  }
792
- return {
793
- jsonrpc: "2.0",
794
- id,
795
- error: {
796
- code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
797
- message: `Method not found: ${method}`
798
- }
799
- };
864
+ const url = `${BASE_URL}/search.json?${params}`;
865
+ return fetchJson(url, `search "${query}"`);
800
866
  }
801
- function writeResponse(response) {
802
- process.stdout.write(`${JSON.stringify(response)}
803
- `);
867
+ function getCoverUrlByIsbn(isbn, size) {
868
+ const normalized = normalizeIsbn(isbn);
869
+ return `${COVERS_BASE_URL}/b/isbn/${normalized}-${size}.jpg`;
804
870
  }
805
- function methodNotFound(id, message) {
806
- return {
807
- jsonrpc: "2.0",
808
- id,
809
- error: {
810
- code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
811
- message
812
- }
813
- };
871
+ function getCoverUrlById(coverId, size) {
872
+ return `${COVERS_BASE_URL}/b/id/${coverId}-${size}.jpg`;
814
873
  }
815
- function success(id, result) {
816
- return { jsonrpc: "2.0", id, result };
874
+ function getCoverUrlByOlid(olid, size) {
875
+ const id = olid.replace(/^\/(?:books|works)\//, "");
876
+ return `${COVERS_BASE_URL}/b/olid/${id}-${size}.jpg`;
817
877
  }
818
- function createMetadataPlugin(options) {
819
- const { manifest: manifest2, provider, bookProvider: bookProvider2, onInitialize, logLevel } = options;
820
- const contentTypes = manifest2.capabilities.metadataProvider;
821
- if (contentTypes.includes("series") && !provider) {
822
- throw new Error("Series metadata provider is required when 'series' is in metadataProvider capabilities");
823
- }
824
- if (contentTypes.includes("book") && !bookProvider2) {
825
- throw new Error("Book metadata provider is required when 'book' is in metadataProvider capabilities");
878
+ function parseYear(dateStr) {
879
+ if (!dateStr) return void 0;
880
+ const match = dateStr.match(/(?:^|[^0-9])(1[89]\d{2}|20\d{2})(?:[^0-9]|$)/);
881
+ if (match) {
882
+ return Number.parseInt(match[1], 10);
826
883
  }
827
- const router = async (method, params, id) => {
828
- switch (method) {
829
- // Series metadata methods
830
- case "metadata/series/search": {
831
- if (!provider)
832
- return methodNotFound(id, "This plugin does not support series metadata");
833
- const err = validateSearchParams(params);
834
- if (err)
835
- return invalidParamsError(id, err);
836
- return success(id, await provider.search(params));
837
- }
838
- case "metadata/series/get": {
839
- if (!provider)
840
- return methodNotFound(id, "This plugin does not support series metadata");
841
- const err = validateGetParams(params);
842
- if (err)
843
- return invalidParamsError(id, err);
844
- return success(id, await provider.get(params));
845
- }
846
- case "metadata/series/match": {
847
- if (!provider)
848
- return methodNotFound(id, "This plugin does not support series metadata");
849
- if (!provider.match)
850
- return methodNotFound(id, "This plugin does not support series match");
851
- const err = validateMatchParams(params);
852
- if (err)
853
- return invalidParamsError(id, err);
854
- return success(id, await provider.match(params));
855
- }
856
- // Book metadata methods
857
- case "metadata/book/search": {
858
- if (!bookProvider2)
859
- return methodNotFound(id, "This plugin does not support book metadata");
860
- const err = validateBookSearchParams(params);
861
- if (err)
862
- return invalidParamsError(id, err);
863
- return success(id, await bookProvider2.search(params));
864
- }
865
- case "metadata/book/get": {
866
- if (!bookProvider2)
867
- return methodNotFound(id, "This plugin does not support book metadata");
868
- const err = validateGetParams(params);
869
- if (err)
870
- return invalidParamsError(id, err);
871
- return success(id, await bookProvider2.get(params));
872
- }
873
- case "metadata/book/match": {
874
- if (!bookProvider2)
875
- return methodNotFound(id, "This plugin does not support book metadata");
876
- if (!bookProvider2.match)
877
- return methodNotFound(id, "This plugin does not support book match");
878
- const err = validateBookMatchParams(params);
879
- if (err)
880
- return invalidParamsError(id, err);
881
- return success(id, await bookProvider2.match(params));
882
- }
883
- default:
884
- return null;
885
- }
884
+ return void 0;
885
+ }
886
+ function parseDescription(desc) {
887
+ if (!desc) return void 0;
888
+ const raw = typeof desc === "string" ? desc : desc.value;
889
+ return stripHtml(raw);
890
+ }
891
+ function stripHtml(html) {
892
+ let text = html;
893
+ text = text.replace(/<\/(p|div|li|tr|h[1-6])>/gi, "\n");
894
+ text = text.replace(/<br\s*\/?>/gi, "\n");
895
+ text = text.replace(/<[^>]+>/g, "");
896
+ text = text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&apos;/g, "'").replace(/&nbsp;/g, " ");
897
+ text = text.replace(/[^\S\n]+/g, " ");
898
+ text = text.replace(/\n{3,}/g, "\n\n");
899
+ text = text.split("\n").map((line) => line.trim()).join("\n").trim();
900
+ return text || void 0;
901
+ }
902
+ function parseLanguage(langRef) {
903
+ if (!langRef) return void 0;
904
+ const match = langRef.match(/\/languages\/(\w+)$/);
905
+ if (!match) return void 0;
906
+ const code = match[1].toLowerCase();
907
+ const languageMap = {
908
+ eng: "en",
909
+ spa: "es",
910
+ fre: "fr",
911
+ fra: "fr",
912
+ ger: "de",
913
+ deu: "de",
914
+ ita: "it",
915
+ por: "pt",
916
+ rus: "ru",
917
+ jpn: "ja",
918
+ chi: "zh",
919
+ zho: "zh",
920
+ kor: "ko",
921
+ ara: "ar",
922
+ hin: "hi",
923
+ pol: "pl",
924
+ tur: "tr",
925
+ dut: "nl",
926
+ nld: "nl",
927
+ swe: "sv",
928
+ nor: "no",
929
+ dan: "da",
930
+ fin: "fi",
931
+ cze: "cs",
932
+ ces: "cs",
933
+ gre: "el",
934
+ ell: "el",
935
+ heb: "he",
936
+ hun: "hu",
937
+ rom: "ro",
938
+ ron: "ro",
939
+ tha: "th",
940
+ vie: "vi",
941
+ ind: "id",
942
+ mal: "ms",
943
+ msa: "ms",
944
+ ukr: "uk",
945
+ cat: "ca",
946
+ lat: "la"
886
947
  };
887
- createPluginServer({ manifest: manifest2, onInitialize, logLevel, router });
948
+ return languageMap[code] || code;
949
+ }
950
+ function extractOlid(key) {
951
+ return key.replace(/^\/(?:works|books|authors)\//, "");
952
+ }
953
+ function buildOpenLibraryUrl(key) {
954
+ return `${BASE_URL}${key.startsWith("/") ? key : `/${key}`}`;
888
955
  }
956
+ function clearCache() {
957
+ cache.clear();
958
+ }
959
+ var BASE_URL, COVERS_BASE_URL, CACHE_TTL_MS, cache, SEARCH_FIELDS;
960
+ var init_api = __esm({
961
+ "src/api.ts"() {
962
+ "use strict";
963
+ init_logger2();
964
+ BASE_URL = "https://openlibrary.org";
965
+ COVERS_BASE_URL = "https://covers.openlibrary.org";
966
+ CACHE_TTL_MS = 15 * 60 * 1e3;
967
+ cache = /* @__PURE__ */ new Map();
968
+ SEARCH_FIELDS = [
969
+ "key",
970
+ "title",
971
+ "subtitle",
972
+ "author_name",
973
+ "author_key",
974
+ "first_publish_year",
975
+ "publish_year",
976
+ "publisher",
977
+ "isbn",
978
+ "number_of_pages_median",
979
+ "cover_i",
980
+ "cover_edition_key",
981
+ "edition_count",
982
+ "language",
983
+ "subject",
984
+ "ratings_average",
985
+ "ratings_count"
986
+ ].join(",");
987
+ }
988
+ });
889
989
 
890
990
  // src/index.ts
991
+ init_dist();
891
992
  init_api();
993
+ init_logger2();
892
994
 
893
995
  // package.json
894
996
  var package_default = {
895
997
  name: "@ashdev/codex-plugin-metadata-openlibrary",
896
- version: "1.37.0",
998
+ version: "1.38.0",
897
999
  description: "Open Library metadata plugin for Codex - fetches book metadata by ISBN or title search",
898
1000
  main: "dist/index.js",
899
1001
  bin: "dist/index.js",
@@ -933,7 +1035,7 @@ var package_default = {
933
1035
  node: ">=22.0.0"
934
1036
  },
935
1037
  dependencies: {
936
- "@ashdev/codex-plugin-sdk": "^1.37.0"
1038
+ "@ashdev/codex-plugin-sdk": "^1.38.0"
937
1039
  },
938
1040
  devDependencies: {
939
1041
  "@biomejs/biome": "^2.4.4",
@@ -1240,7 +1342,6 @@ async function getFullBookMetadata(editionOrWorkKey, isbn) {
1240
1342
  }
1241
1343
 
1242
1344
  // src/index.ts
1243
- var logger = createLogger({ name: "openlibrary", level: "info" });
1244
1345
  var config = {
1245
1346
  maxResults: DEFAULT_MAX_RESULTS
1246
1347
  };
@@ -1254,6 +1355,9 @@ var bookProvider = {
1254
1355
  async search(params) {
1255
1356
  const { isbn, query, author, limit } = params;
1256
1357
  const maxResults = Math.min(limit || config.maxResults, 50);
1358
+ logger.debug(
1359
+ `search: isbn=${isbn ?? "-"} query=${JSON.stringify(query ?? "")} author=${JSON.stringify(author ?? "")} maxResults=${maxResults}`
1360
+ );
1257
1361
  if (isbn && isValidIsbn(isbn)) {
1258
1362
  const edition = await getEditionByIsbn(isbn);
1259
1363
  if (edition) {
@@ -1290,8 +1394,12 @@ var bookProvider = {
1290
1394
  limit: maxResults
1291
1395
  });
1292
1396
  if (!searchResponse?.docs?.length) {
1397
+ logger.debug(`search: no results for query=${JSON.stringify(query)}`);
1293
1398
  return { results: [] };
1294
1399
  }
1400
+ logger.debug(
1401
+ `search: ${searchResponse.docs.length} result(s) for query=${JSON.stringify(query)}`
1402
+ );
1295
1403
  return {
1296
1404
  results: searchResponse.docs.map(mapSearchDocToSearchResult)
1297
1405
  };
@@ -1305,10 +1413,12 @@ var bookProvider = {
1305
1413
  */
1306
1414
  async get(params) {
1307
1415
  const { externalId } = params;
1416
+ logger.debug(`get: externalId=${externalId}`);
1308
1417
  const metadata = await getFullBookMetadata(externalId);
1309
1418
  if (metadata) {
1310
1419
  return metadata;
1311
1420
  }
1421
+ logger.debug(`get: no full metadata for ${externalId}, returning minimal record`);
1312
1422
  return {
1313
1423
  externalId,
1314
1424
  externalUrl: `https://openlibrary.org${externalId.startsWith("/") ? externalId : `/${externalId}`}`,
@@ -1341,9 +1451,13 @@ var bookProvider = {
1341
1451
  */
1342
1452
  async match(params) {
1343
1453
  const { title, authors, isbn, year } = params;
1454
+ logger.debug(
1455
+ `match: title=${JSON.stringify(title)} authors=${JSON.stringify(authors ?? [])} isbn=${isbn ?? "-"} year=${year ?? "-"}`
1456
+ );
1344
1457
  if (isbn && isValidIsbn(isbn)) {
1345
1458
  const edition = await getEditionByIsbn(isbn);
1346
1459
  if (edition) {
1460
+ logger.debug(`match: ISBN ${isbn} resolved directly (confidence 0.99)`);
1347
1461
  const workKey = edition.works?.[0]?.key;
1348
1462
  const workData = workKey ? await getWork(workKey) : null;
1349
1463
  const metadata = await mapEditionToBookMetadata(edition, workData);
@@ -1391,6 +1505,9 @@ var bookProvider = {
1391
1505
  confidence = Math.min(1, confidence + 0.1);
1392
1506
  }
1393
1507
  confidence = Math.min(confidence, 0.85);
1508
+ logger.debug(
1509
+ `match: best=${JSON.stringify(bestMatch.title)} confidence=${confidence.toFixed(2)} (${results.length} candidate(s))`
1510
+ );
1394
1511
  return {
1395
1512
  match: bestMatch,
1396
1513
  confidence,
@@ -1403,6 +1520,7 @@ createMetadataPlugin({
1403
1520
  bookProvider,
1404
1521
  logLevel: "info",
1405
1522
  onInitialize(params) {
1523
+ if (params.logLevel) logger.setLevel(params.logLevel);
1406
1524
  const maxResults = params.adminConfig?.maxResults;
1407
1525
  if (maxResults !== void 0) {
1408
1526
  config.maxResults = Math.min(Math.max(1, maxResults), 50);