@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 +791 -673
- package/dist/index.js.map +4 -4
- package/package.json +2 -2
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
|
-
//
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'").replace(/ /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
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
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
|
-
|
|
605
|
-
|
|
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
|
-
|
|
793
|
-
|
|
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
|
|
802
|
-
|
|
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
|
|
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
|
|
816
|
-
|
|
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
|
|
819
|
-
|
|
820
|
-
const
|
|
821
|
-
if (
|
|
822
|
-
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'").replace(/ /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
|
-
|
|
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.
|
|
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.
|
|
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);
|