@ashdev/codex-plugin-metadata-mangabaka 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,260 @@
1
+ # @ashdev/codex-plugin-metadata-mangabaka
2
+
3
+ A Codex metadata plugin for fetching manga metadata from [MangaBaka](https://mangabaka.org). MangaBaka aggregates metadata from multiple sources including AniList, MyAnimeList, MangaDex, and more.
4
+
5
+ ## Features
6
+
7
+ - Search for manga/manhwa/manhua by title
8
+ - Fetch comprehensive metadata including:
9
+ - Titles in multiple languages (English, Japanese, Korean, Chinese)
10
+ - Synopsis/description
11
+ - Publication status (ongoing, completed, hiatus, cancelled)
12
+ - Genres and tags
13
+ - Authors and artists
14
+ - Cover images
15
+ - Ratings
16
+ - External links to AniList, MAL, MangaDex
17
+
18
+ ## Prerequisites
19
+
20
+ You need a MangaBaka API key to use this plugin:
21
+
22
+ 1. Create an account at [mangabaka.org](https://mangabaka.org)
23
+ 2. Go to [Settings > API](https://mangabaka.org/settings/api)
24
+ 3. Generate an API key
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ npm install -g @ashdev/codex-plugin-metadata-mangabaka
30
+ ```
31
+
32
+ Or run directly with npx (no installation required).
33
+
34
+ ## Adding the Plugin to Codex
35
+
36
+ ### Using npx (Recommended)
37
+
38
+ 1. Log in to Codex as an administrator
39
+ 2. Navigate to **Settings** > **Plugins**
40
+ 3. Click **Add Plugin**
41
+ 4. Fill in the form:
42
+ - **Name**: `metadata-mangabaka`
43
+ - **Display Name**: `MangaBaka Metadata`
44
+ - **Command**: `npx`
45
+ - **Arguments**: `-y @ashdev/codex-plugin-metadata-mangabaka@1.0.0`
46
+ - **Scopes**: Select `series:detail`
47
+ 5. In the **Credentials** tab:
48
+ - **Credential Delivery**: Select `Initialize Message` or `Both`
49
+ - **Credentials**: `{"api_key": "your-mangabaka-api-key"}`
50
+ 6. Click **Save**
51
+ 7. Click **Test Connection** to verify the plugin works
52
+ 8. Toggle **Enabled** to activate the plugin
53
+
54
+ ### npx Options
55
+
56
+ | Configuration | Arguments | Description |
57
+ |--------------|-----------|-------------|
58
+ | Latest version | `-y @ashdev/codex-plugin-metadata-mangabaka` | Always uses latest |
59
+ | Pinned version | `-y @ashdev/codex-plugin-metadata-mangabaka@1.0.0` | Recommended for production |
60
+ | Fast startup | `-y --prefer-offline @ashdev/codex-plugin-metadata-mangabaka@1.0.0` | Skips version check if cached |
61
+
62
+ **Flags:**
63
+ - `-y`: Auto-confirms installation (required for containers)
64
+ - `--prefer-offline`: Uses cached version without checking npm registry
65
+
66
+ ### Using Docker
67
+
68
+ For Docker deployments, use npx with `--prefer-offline` for faster startup:
69
+
70
+ ```
71
+ Command: npx
72
+ Arguments: -y --prefer-offline @ashdev/codex-plugin-metadata-mangabaka@1.0.0
73
+ ```
74
+
75
+ Pre-warm the cache in your Dockerfile:
76
+
77
+ ```dockerfile
78
+ # Pre-cache plugin during image build
79
+ RUN npx -y @ashdev/codex-plugin-metadata-mangabaka@1.0.0 --version || true
80
+ ```
81
+
82
+ ### Manual Installation (Alternative)
83
+
84
+ For maximum performance, install globally:
85
+
86
+ ```bash
87
+ npm install -g @ashdev/codex-plugin-metadata-mangabaka
88
+ ```
89
+
90
+ Then configure:
91
+ - **Command**: `codex-plugin-metadata-mangabaka`
92
+ - **Arguments**: (leave empty)
93
+
94
+ ## Configuration
95
+
96
+ ### Credentials
97
+
98
+ The plugin requires a MangaBaka API key. Configure it in the Codex UI or via the API:
99
+
100
+ ```json
101
+ {
102
+ "api_key": "mb-123412341234"
103
+ }
104
+ ```
105
+
106
+ ### Credential Delivery Method
107
+
108
+ This plugin receives credentials via the `initialize` message, so you must set the **Credential Delivery** option appropriately:
109
+
110
+ | Method | Value | Description |
111
+ |--------|-------|-------------|
112
+ | Initialize Message | `init_message` | Credentials passed in the JSON-RPC `initialize` request (recommended) |
113
+ | Both | `both` | Credentials passed as both environment variables and in `initialize` |
114
+
115
+ **Note:** The `env` (environment variables only) method will **not work** with this plugin because it reads credentials from the `onInitialize` callback, not from environment variables.
116
+
117
+ ### Parameters
118
+
119
+ The plugin supports optional parameters to customize behavior:
120
+
121
+ | Parameter | Type | Default | Description |
122
+ |-----------|------|---------|-------------|
123
+ | `base_url` | string | `https://api.mangabaka.org` | Override the API base URL |
124
+
125
+ Example parameters configuration:
126
+
127
+ ```json
128
+ {
129
+ "base_url": "https://api.mangabaka.org"
130
+ }
131
+ ```
132
+
133
+ ### Rate Limiting
134
+
135
+ The plugin automatically handles rate limiting from the MangaBaka API:
136
+
137
+ - When rate limited (HTTP 429), the plugin returns a `RateLimitError` with the retry delay
138
+ - The `Retry-After` header is used to determine wait time (defaults to 60 seconds)
139
+ - Codex will automatically retry requests after the specified delay
140
+
141
+ If you encounter frequent rate limiting, consider spacing out your metadata refresh operations.
142
+
143
+ ## Using the Plugin
144
+
145
+ Once enabled, the MangaBaka plugin appears in the series detail page:
146
+
147
+ 1. Navigate to any series in your library
148
+ 2. Click the **Metadata** button (or look for the plugin icon)
149
+ 3. Click **Search MangaBaka Metadata**
150
+ 4. Enter the series title to search
151
+ 5. Select the best match from the results
152
+ 6. Preview the metadata changes
153
+ 7. Click **Apply** to update your series metadata
154
+
155
+ The plugin will show:
156
+ - **Will Apply**: Fields that will be updated
157
+ - **Locked**: Fields you've locked (won't be changed)
158
+ - **Unchanged**: Fields that already match
159
+
160
+ ## Development
161
+
162
+ ```bash
163
+ # Install dependencies
164
+ npm install
165
+
166
+ # Build the plugin
167
+ npm run build
168
+
169
+ # Type check
170
+ npm run typecheck
171
+
172
+ # Run tests
173
+ npm test
174
+
175
+ # Lint
176
+ npm run lint
177
+ ```
178
+
179
+ ## Project Structure
180
+
181
+ ```
182
+ plugins/metadata-mangabaka/
183
+ ├── src/
184
+ │ ├── index.ts # Plugin entry point
185
+ │ ├── manifest.ts # Plugin manifest
186
+ │ ├── api.ts # MangaBaka API client
187
+ │ ├── mappers.ts # Response mappers
188
+ │ ├── types.ts # MangaBaka API types
189
+ │ └── handlers/
190
+ │ ├── search.ts # Search handler
191
+ │ ├── get.ts # Get metadata handler
192
+ │ └── match.ts # Auto-match handler
193
+ ├── dist/
194
+ │ └── index.js # Built bundle (excluded from git)
195
+ ├── package.json
196
+ ├── tsconfig.json
197
+ └── README.md
198
+ ```
199
+
200
+ ## API Reference
201
+
202
+ ### Search
203
+
204
+ Searches MangaBaka for series matching a query.
205
+
206
+ **Parameters:**
207
+ - `query`: Search string
208
+ - `contentType`: Always `"series"`
209
+ - `limit`: Max results (default: 20)
210
+ - `cursor`: Page cursor for pagination
211
+
212
+ **Returns:**
213
+ - `results`: Array of search results with `relevanceScore` (0.0-1.0)
214
+ - `nextCursor`: Cursor for next page (if available)
215
+
216
+ ### Get
217
+
218
+ Fetches full metadata for a specific series.
219
+
220
+ **Parameters:**
221
+ - `externalId`: MangaBaka series ID
222
+ - `contentType`: Always `"series"`
223
+
224
+ **Returns:**
225
+ - Full series metadata including titles, summary, genres, etc.
226
+
227
+ ### Match
228
+
229
+ Finds the best match for an existing series (used for auto-matching).
230
+
231
+ **Parameters:**
232
+ - `title`: Series title to match
233
+ - `year`: Publication year (optional hint)
234
+ - `contentType`: Always `"series"`
235
+
236
+ **Returns:**
237
+ - `match`: Best matching result or `null`
238
+ - `confidence`: Match confidence (0.0-1.0)
239
+ - `alternatives`: Other potential matches if confidence is low
240
+
241
+ ## Troubleshooting
242
+
243
+ ### "api_key credential is required"
244
+
245
+ Make sure you've configured the API key in the plugin credentials section.
246
+
247
+ ### "Plugin not initialized"
248
+
249
+ The plugin hasn't received credentials yet. Check that:
250
+ 1. The plugin is properly configured in Settings > Plugins
251
+ 2. Credentials are saved
252
+ 3. Try disabling and re-enabling the plugin
253
+
254
+ ### "Rate limited"
255
+
256
+ MangaBaka has API rate limits. The plugin will report the retry delay from the API. Wait for the specified time before retrying. See the [Rate Limiting](#rate-limiting) section for more details.
257
+
258
+ ## License
259
+
260
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,800 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../sdk-typescript/dist/types/rpc.js
4
+ var JSON_RPC_ERROR_CODES = {
5
+ /** Invalid JSON was received */
6
+ PARSE_ERROR: -32700,
7
+ /** The JSON sent is not a valid Request object */
8
+ INVALID_REQUEST: -32600,
9
+ /** The method does not exist / is not available */
10
+ METHOD_NOT_FOUND: -32601,
11
+ /** Invalid method parameter(s) */
12
+ INVALID_PARAMS: -32602,
13
+ /** Internal JSON-RPC error */
14
+ INTERNAL_ERROR: -32603
15
+ };
16
+ var PLUGIN_ERROR_CODES = {
17
+ /** Rate limited by external API */
18
+ RATE_LIMITED: -32001,
19
+ /** Resource not found (e.g., series ID doesn't exist) */
20
+ NOT_FOUND: -32002,
21
+ /** Authentication failed (invalid credentials) */
22
+ AUTH_FAILED: -32003,
23
+ /** External API error */
24
+ API_ERROR: -32004,
25
+ /** Plugin configuration error */
26
+ CONFIG_ERROR: -32005
27
+ };
28
+
29
+ // ../sdk-typescript/dist/errors.js
30
+ var PluginError = class extends Error {
31
+ data;
32
+ constructor(message, data) {
33
+ super(message);
34
+ this.name = this.constructor.name;
35
+ this.data = data;
36
+ }
37
+ /**
38
+ * Convert to JSON-RPC error format
39
+ */
40
+ toJsonRpcError() {
41
+ return {
42
+ code: this.code,
43
+ message: this.message,
44
+ data: this.data
45
+ };
46
+ }
47
+ };
48
+ var RateLimitError = class extends PluginError {
49
+ code = PLUGIN_ERROR_CODES.RATE_LIMITED;
50
+ /** Seconds to wait before retrying */
51
+ retryAfterSeconds;
52
+ constructor(retryAfterSeconds, message) {
53
+ super(message ?? `Rate limited, retry after ${retryAfterSeconds}s`, {
54
+ retryAfterSeconds
55
+ });
56
+ this.retryAfterSeconds = retryAfterSeconds;
57
+ }
58
+ };
59
+ var NotFoundError = class extends PluginError {
60
+ code = PLUGIN_ERROR_CODES.NOT_FOUND;
61
+ };
62
+ var AuthError = class extends PluginError {
63
+ code = PLUGIN_ERROR_CODES.AUTH_FAILED;
64
+ constructor(message) {
65
+ super(message ?? "Authentication failed");
66
+ }
67
+ };
68
+ var ApiError = class extends PluginError {
69
+ code = PLUGIN_ERROR_CODES.API_ERROR;
70
+ statusCode;
71
+ constructor(message, statusCode) {
72
+ super(message, statusCode !== void 0 ? { statusCode } : void 0);
73
+ this.statusCode = statusCode;
74
+ }
75
+ };
76
+ var ConfigError = class extends PluginError {
77
+ code = PLUGIN_ERROR_CODES.CONFIG_ERROR;
78
+ };
79
+
80
+ // ../sdk-typescript/dist/logger.js
81
+ var LOG_LEVELS = {
82
+ debug: 0,
83
+ info: 1,
84
+ warn: 2,
85
+ error: 3
86
+ };
87
+ var Logger = class {
88
+ name;
89
+ minLevel;
90
+ timestamps;
91
+ constructor(options) {
92
+ this.name = options.name;
93
+ this.minLevel = LOG_LEVELS[options.level ?? "info"];
94
+ this.timestamps = options.timestamps ?? true;
95
+ }
96
+ shouldLog(level) {
97
+ return LOG_LEVELS[level] >= this.minLevel;
98
+ }
99
+ format(level, message, data) {
100
+ const parts = [];
101
+ if (this.timestamps) {
102
+ parts.push((/* @__PURE__ */ new Date()).toISOString());
103
+ }
104
+ parts.push(`[${level.toUpperCase()}]`);
105
+ parts.push(`[${this.name}]`);
106
+ parts.push(message);
107
+ if (data !== void 0) {
108
+ if (data instanceof Error) {
109
+ parts.push(`- ${data.message}`);
110
+ if (data.stack) {
111
+ parts.push(`
112
+ ${data.stack}`);
113
+ }
114
+ } else if (typeof data === "object") {
115
+ parts.push(`- ${JSON.stringify(data)}`);
116
+ } else {
117
+ parts.push(`- ${String(data)}`);
118
+ }
119
+ }
120
+ return parts.join(" ");
121
+ }
122
+ log(level, message, data) {
123
+ if (this.shouldLog(level)) {
124
+ process.stderr.write(`${this.format(level, message, data)}
125
+ `);
126
+ }
127
+ }
128
+ debug(message, data) {
129
+ this.log("debug", message, data);
130
+ }
131
+ info(message, data) {
132
+ this.log("info", message, data);
133
+ }
134
+ warn(message, data) {
135
+ this.log("warn", message, data);
136
+ }
137
+ error(message, data) {
138
+ this.log("error", message, data);
139
+ }
140
+ };
141
+ function createLogger(options) {
142
+ return new Logger(options);
143
+ }
144
+
145
+ // ../sdk-typescript/dist/server.js
146
+ import { createInterface } from "node:readline";
147
+ function validateStringFields(params, fields) {
148
+ if (params === null || params === void 0) {
149
+ return { field: "params", message: "params is required" };
150
+ }
151
+ if (typeof params !== "object") {
152
+ return { field: "params", message: "params must be an object" };
153
+ }
154
+ const obj = params;
155
+ for (const field of fields) {
156
+ const value = obj[field];
157
+ if (value === void 0 || value === null) {
158
+ return { field, message: `${field} is required` };
159
+ }
160
+ if (typeof value !== "string") {
161
+ return { field, message: `${field} must be a string` };
162
+ }
163
+ if (value.trim() === "") {
164
+ return { field, message: `${field} cannot be empty` };
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+ function validateSearchParams(params) {
170
+ return validateStringFields(params, ["query"]);
171
+ }
172
+ function validateGetParams(params) {
173
+ return validateStringFields(params, ["externalId"]);
174
+ }
175
+ function validateMatchParams(params) {
176
+ return validateStringFields(params, ["title"]);
177
+ }
178
+ function invalidParamsError(id, error) {
179
+ return {
180
+ jsonrpc: "2.0",
181
+ id,
182
+ error: {
183
+ code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
184
+ message: `Invalid params: ${error.message}`,
185
+ data: { field: error.field }
186
+ }
187
+ };
188
+ }
189
+ function createMetadataPlugin(options) {
190
+ const { manifest: manifest2, provider: provider2, onInitialize, logLevel = "info" } = options;
191
+ const logger5 = createLogger({ name: manifest2.name, level: logLevel });
192
+ logger5.info(`Starting plugin: ${manifest2.displayName} v${manifest2.version}`);
193
+ const rl = createInterface({
194
+ input: process.stdin,
195
+ terminal: false
196
+ });
197
+ rl.on("line", (line) => {
198
+ void handleLine(line, manifest2, provider2, onInitialize, logger5);
199
+ });
200
+ rl.on("close", () => {
201
+ logger5.info("stdin closed, shutting down");
202
+ process.exit(0);
203
+ });
204
+ process.on("uncaughtException", (error) => {
205
+ logger5.error("Uncaught exception", error);
206
+ process.exit(1);
207
+ });
208
+ process.on("unhandledRejection", (reason) => {
209
+ logger5.error("Unhandled rejection", reason);
210
+ });
211
+ }
212
+ async function handleLine(line, manifest2, provider2, onInitialize, logger5) {
213
+ const trimmed = line.trim();
214
+ if (!trimmed)
215
+ return;
216
+ let id = null;
217
+ try {
218
+ const request = JSON.parse(trimmed);
219
+ id = request.id;
220
+ logger5.debug(`Received request: ${request.method}`, { id: request.id });
221
+ const response = await handleRequest(request, manifest2, provider2, onInitialize, logger5);
222
+ if (response !== null) {
223
+ writeResponse(response);
224
+ }
225
+ } catch (error) {
226
+ if (error instanceof SyntaxError) {
227
+ writeResponse({
228
+ jsonrpc: "2.0",
229
+ id: null,
230
+ error: {
231
+ code: JSON_RPC_ERROR_CODES.PARSE_ERROR,
232
+ message: "Parse error: invalid JSON"
233
+ }
234
+ });
235
+ } else if (error instanceof PluginError) {
236
+ writeResponse({
237
+ jsonrpc: "2.0",
238
+ id,
239
+ error: error.toJsonRpcError()
240
+ });
241
+ } else {
242
+ const message = error instanceof Error ? error.message : "Unknown error";
243
+ logger5.error("Request failed", error);
244
+ writeResponse({
245
+ jsonrpc: "2.0",
246
+ id,
247
+ error: {
248
+ code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
249
+ message
250
+ }
251
+ });
252
+ }
253
+ }
254
+ }
255
+ async function handleRequest(request, manifest2, provider2, onInitialize, logger5) {
256
+ const { method, params, id } = request;
257
+ switch (method) {
258
+ case "initialize":
259
+ if (onInitialize) {
260
+ await onInitialize(params);
261
+ }
262
+ return {
263
+ jsonrpc: "2.0",
264
+ id,
265
+ result: manifest2
266
+ };
267
+ case "ping":
268
+ return {
269
+ jsonrpc: "2.0",
270
+ id,
271
+ result: "pong"
272
+ };
273
+ case "shutdown": {
274
+ logger5.info("Shutdown requested");
275
+ const response = {
276
+ jsonrpc: "2.0",
277
+ id,
278
+ result: null
279
+ };
280
+ process.stdout.write(`${JSON.stringify(response)}
281
+ `, () => {
282
+ process.exit(0);
283
+ });
284
+ return null;
285
+ }
286
+ // Series metadata methods (scoped by content type)
287
+ case "metadata/series/search": {
288
+ const validationError = validateSearchParams(params);
289
+ if (validationError) {
290
+ return invalidParamsError(id, validationError);
291
+ }
292
+ return {
293
+ jsonrpc: "2.0",
294
+ id,
295
+ result: await provider2.search(params)
296
+ };
297
+ }
298
+ case "metadata/series/get": {
299
+ const validationError = validateGetParams(params);
300
+ if (validationError) {
301
+ return invalidParamsError(id, validationError);
302
+ }
303
+ return {
304
+ jsonrpc: "2.0",
305
+ id,
306
+ result: await provider2.get(params)
307
+ };
308
+ }
309
+ case "metadata/series/match": {
310
+ if (!provider2.match) {
311
+ return {
312
+ jsonrpc: "2.0",
313
+ id,
314
+ error: {
315
+ code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
316
+ message: "This plugin does not support match"
317
+ }
318
+ };
319
+ }
320
+ const validationError = validateMatchParams(params);
321
+ if (validationError) {
322
+ return invalidParamsError(id, validationError);
323
+ }
324
+ return {
325
+ jsonrpc: "2.0",
326
+ id,
327
+ result: await provider2.match(params)
328
+ };
329
+ }
330
+ // Future: book metadata methods
331
+ // case "metadata/book/search":
332
+ // case "metadata/book/get":
333
+ // case "metadata/book/match":
334
+ default:
335
+ return {
336
+ jsonrpc: "2.0",
337
+ id,
338
+ error: {
339
+ code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
340
+ message: `Method not found: ${method}`
341
+ }
342
+ };
343
+ }
344
+ }
345
+ function writeResponse(response) {
346
+ process.stdout.write(`${JSON.stringify(response)}
347
+ `);
348
+ }
349
+
350
+ // src/api.ts
351
+ var BASE_URL = "https://api.mangabaka.dev";
352
+ var logger = createLogger({ name: "mangabaka-api", level: "debug" });
353
+ var MangaBakaClient = class {
354
+ apiKey;
355
+ constructor(apiKey) {
356
+ if (!apiKey) {
357
+ throw new AuthError("API key is required");
358
+ }
359
+ this.apiKey = apiKey;
360
+ }
361
+ /**
362
+ * Search for series by query
363
+ */
364
+ async search(query, page = 1, perPage = 20) {
365
+ logger.debug(`Searching for: "${query}" (page ${page})`);
366
+ const params = new URLSearchParams({
367
+ q: query,
368
+ page: String(page),
369
+ limit: String(perPage)
370
+ });
371
+ const response = await this.request(`/v1/series/search?${params.toString()}`);
372
+ return {
373
+ data: response.data,
374
+ total: response.pagination?.total ?? response.data.length,
375
+ page: response.pagination?.page ?? page,
376
+ totalPages: response.pagination?.total_pages ?? 1
377
+ };
378
+ }
379
+ /**
380
+ * Get full series details by ID
381
+ */
382
+ async getSeries(id) {
383
+ logger.debug(`Getting series: ${id}`);
384
+ const response = await this.request(`/v1/series/${id}`);
385
+ return response.data;
386
+ }
387
+ /**
388
+ * Make an authenticated request to the MangaBaka API
389
+ */
390
+ async request(path) {
391
+ const url = `${BASE_URL}${path}`;
392
+ const headers = {
393
+ "x-api-key": this.apiKey,
394
+ Accept: "application/json"
395
+ };
396
+ try {
397
+ const response = await fetch(url, {
398
+ method: "GET",
399
+ headers
400
+ });
401
+ if (response.status === 429) {
402
+ const retryAfter = response.headers.get("Retry-After");
403
+ const seconds = retryAfter ? Number.parseInt(retryAfter, 10) : 60;
404
+ throw new RateLimitError(seconds);
405
+ }
406
+ if (response.status === 401 || response.status === 403) {
407
+ throw new AuthError("Invalid API key");
408
+ }
409
+ if (response.status === 404) {
410
+ throw new NotFoundError(`Resource not found: ${path}`);
411
+ }
412
+ if (!response.ok) {
413
+ const text = await response.text();
414
+ logger.error(`API error: ${response.status}`, { body: text });
415
+ throw new ApiError(`API error: ${response.status} ${response.statusText}`, response.status);
416
+ }
417
+ return response.json();
418
+ } catch (error) {
419
+ if (error instanceof RateLimitError || error instanceof AuthError || error instanceof NotFoundError || error instanceof ApiError) {
420
+ throw error;
421
+ }
422
+ const message = error instanceof Error ? error.message : "Unknown error";
423
+ logger.error("Request failed", error);
424
+ throw new ApiError(`Request failed: ${message}`);
425
+ }
426
+ }
427
+ };
428
+
429
+ // src/mappers.ts
430
+ function mapStatus(mbStatus) {
431
+ switch (mbStatus) {
432
+ case "completed":
433
+ return "ended";
434
+ case "releasing":
435
+ case "upcoming":
436
+ return "ongoing";
437
+ case "hiatus":
438
+ return "hiatus";
439
+ case "cancelled":
440
+ return "abandoned";
441
+ default:
442
+ return "unknown";
443
+ }
444
+ }
445
+ function formatGenre(genre) {
446
+ return genre.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
447
+ }
448
+ function detectLanguageFromCountry(country) {
449
+ if (!country) return void 0;
450
+ const countryLower = country.toLowerCase();
451
+ if (countryLower === "jp" || countryLower === "japan") return "ja";
452
+ if (countryLower === "kr" || countryLower === "korea" || countryLower === "south korea")
453
+ return "ko";
454
+ if (countryLower === "cn" || countryLower === "china") return "zh";
455
+ if (countryLower === "tw" || countryLower === "taiwan") return "zh-TW";
456
+ return void 0;
457
+ }
458
+ function mapContentRating(rating) {
459
+ if (!rating) return void 0;
460
+ switch (rating) {
461
+ case "safe":
462
+ return 0;
463
+ // All ages
464
+ case "suggestive":
465
+ return 13;
466
+ // Teen
467
+ case "erotica":
468
+ return 16;
469
+ // Mature
470
+ case "pornographic":
471
+ return 18;
472
+ // Adults only
473
+ default:
474
+ return void 0;
475
+ }
476
+ }
477
+ function extractRating(rating) {
478
+ if (rating == null) return void 0;
479
+ if (typeof rating === "number") return rating;
480
+ return rating.bayesian ?? rating.average ?? void 0;
481
+ }
482
+ function inferReadingDirection(seriesType, country) {
483
+ if (seriesType === "manhwa" || seriesType === "manhua") {
484
+ return "ltr";
485
+ }
486
+ if (seriesType === "manga") {
487
+ return "rtl";
488
+ }
489
+ if (seriesType === "oel") {
490
+ return "ltr";
491
+ }
492
+ if (country) {
493
+ const countryLower = country.toLowerCase();
494
+ if (countryLower === "jp" || countryLower === "japan") return "rtl";
495
+ if (countryLower === "kr" || countryLower === "korea" || countryLower === "south korea")
496
+ return "ltr";
497
+ if (countryLower === "cn" || countryLower === "china") return "ltr";
498
+ if (countryLower === "tw" || countryLower === "taiwan") return "ltr";
499
+ }
500
+ return void 0;
501
+ }
502
+ function mapSearchResult(series) {
503
+ const coverUrl = series.cover?.x250?.x1 ?? series.cover?.raw?.url ?? void 0;
504
+ const alternateTitles = [];
505
+ if (series.native_title && series.native_title !== series.title) {
506
+ alternateTitles.push(series.native_title);
507
+ }
508
+ if (series.romanized_title && series.romanized_title !== series.title) {
509
+ alternateTitles.push(series.romanized_title);
510
+ }
511
+ return {
512
+ externalId: String(series.id),
513
+ title: series.title,
514
+ alternateTitles,
515
+ year: series.year ?? void 0,
516
+ coverUrl: coverUrl ?? void 0,
517
+ preview: {
518
+ status: mapStatus(series.status),
519
+ genres: (series.genres ?? []).slice(0, 3).map(formatGenre),
520
+ rating: extractRating(series.rating),
521
+ description: series.description?.slice(0, 200) ?? void 0
522
+ }
523
+ };
524
+ }
525
+ function mapSeriesMetadata(series) {
526
+ const alternateTitles = [];
527
+ if (series.native_title && series.native_title !== series.title) {
528
+ alternateTitles.push({
529
+ title: series.native_title,
530
+ language: detectLanguageFromCountry(series.country_of_origin),
531
+ titleType: "native"
532
+ });
533
+ }
534
+ if (series.romanized_title && series.romanized_title !== series.title) {
535
+ alternateTitles.push({
536
+ title: series.romanized_title,
537
+ language: "en",
538
+ titleType: "romaji"
539
+ });
540
+ }
541
+ if (series.secondary_titles) {
542
+ for (const [langCode, titleList] of Object.entries(series.secondary_titles)) {
543
+ if (titleList) {
544
+ for (const titleEntry of titleList) {
545
+ if (titleEntry.title !== series.title) {
546
+ alternateTitles.push({
547
+ title: titleEntry.title,
548
+ language: langCode
549
+ });
550
+ }
551
+ }
552
+ }
553
+ }
554
+ }
555
+ const authors = series.authors ?? [];
556
+ const artists = series.artists ?? [];
557
+ const genres = (series.genres ?? []).map(formatGenre);
558
+ const coverUrl = series.cover?.raw?.url ?? series.cover?.x350?.x1 ?? void 0;
559
+ const externalLinks = [
560
+ {
561
+ url: `https://mangabaka.org/${series.id}`,
562
+ label: "MangaBaka",
563
+ linkType: "provider"
564
+ }
565
+ ];
566
+ const sourceConfig = {
567
+ anilist: {
568
+ label: "AniList",
569
+ ratingKey: "anilist",
570
+ urlPattern: "https://anilist.co/manga/{id}"
571
+ },
572
+ my_anime_list: {
573
+ label: "MyAnimeList",
574
+ ratingKey: "myanimelist",
575
+ urlPattern: "https://myanimelist.net/manga/{id}"
576
+ },
577
+ mangadex: {
578
+ label: "MangaDex",
579
+ ratingKey: "mangadex",
580
+ urlPattern: "https://mangadex.org/title/{id}"
581
+ },
582
+ manga_updates: {
583
+ label: "MangaUpdates",
584
+ ratingKey: "mangaupdates",
585
+ urlPattern: "https://www.mangaupdates.com/series/{id}"
586
+ },
587
+ kitsu: { label: "Kitsu", ratingKey: "kitsu", urlPattern: "https://kitsu.app/manga/{id}" },
588
+ anime_planet: {
589
+ label: "Anime-Planet",
590
+ ratingKey: "animeplanet",
591
+ urlPattern: "https://www.anime-planet.com/manga/{id}"
592
+ },
593
+ anime_news_network: { label: "Anime News Network", ratingKey: "animenewsnetwork" },
594
+ shikimori: {
595
+ label: "Shikimori",
596
+ ratingKey: "shikimori",
597
+ urlPattern: "https://shikimori.one/mangas/{id}"
598
+ }
599
+ };
600
+ const externalRatings = [];
601
+ if (series.source) {
602
+ for (const [key, info] of Object.entries(series.source)) {
603
+ if (!info) continue;
604
+ const config = sourceConfig[key];
605
+ const ratingKey = config?.ratingKey ?? key.replace(/_/g, "");
606
+ if (info.id != null && config?.urlPattern) {
607
+ externalLinks.push({
608
+ url: config.urlPattern.replace("{id}", String(info.id)),
609
+ label: config.label,
610
+ linkType: "provider"
611
+ });
612
+ }
613
+ if (info.rating_normalized != null) {
614
+ externalRatings.push({ score: info.rating_normalized, source: ratingKey });
615
+ }
616
+ }
617
+ }
618
+ const publisher = series.publishers?.[0]?.name ?? void 0;
619
+ return {
620
+ externalId: String(series.id),
621
+ externalUrl: `https://mangabaka.org/${series.id}`,
622
+ title: series.title,
623
+ alternateTitles,
624
+ summary: series.description ?? void 0,
625
+ status: mapStatus(series.status),
626
+ year: series.year ?? void 0,
627
+ // Extended metadata
628
+ publisher,
629
+ totalBookCount: series.final_volume ? Number.parseInt(series.final_volume, 10) : void 0,
630
+ ageRating: mapContentRating(series.content_rating),
631
+ readingDirection: inferReadingDirection(series.type, series.country_of_origin),
632
+ // Taxonomy
633
+ genres,
634
+ tags: series.tags ?? [],
635
+ authors,
636
+ artists,
637
+ coverUrl: coverUrl ?? void 0,
638
+ rating: (() => {
639
+ const r = extractRating(series.rating);
640
+ return r != null ? { score: r, source: "mangabaka" } : void 0;
641
+ })(),
642
+ externalRatings: externalRatings.length > 0 ? externalRatings : void 0,
643
+ externalLinks
644
+ };
645
+ }
646
+
647
+ // src/handlers/get.ts
648
+ async function handleGet(params, client2) {
649
+ const seriesId = Number.parseInt(params.externalId, 10);
650
+ if (Number.isNaN(seriesId)) {
651
+ throw new NotFoundError(`Invalid external ID: ${params.externalId}`);
652
+ }
653
+ const response = await client2.getSeries(seriesId);
654
+ return mapSeriesMetadata(response);
655
+ }
656
+
657
+ // src/handlers/match.ts
658
+ var logger2 = createLogger({ name: "mangabaka-match", level: "info" });
659
+ function similarity(a, b) {
660
+ const aLower = a.toLowerCase().trim();
661
+ const bLower = b.toLowerCase().trim();
662
+ if (aLower === bLower) return 1;
663
+ if (aLower.length === 0 || bLower.length === 0) return 0;
664
+ if (aLower.includes(bLower) || bLower.includes(aLower)) {
665
+ return 0.8;
666
+ }
667
+ const aWords = new Set(aLower.split(/\s+/));
668
+ const bWords = new Set(bLower.split(/\s+/));
669
+ const intersection = [...aWords].filter((w) => bWords.has(w));
670
+ const union = /* @__PURE__ */ new Set([...aWords, ...bWords]);
671
+ return intersection.length / union.size;
672
+ }
673
+ function scoreResult(result, params) {
674
+ let score = 0;
675
+ const titleScore = similarity(result.title, params.title);
676
+ score += titleScore * 0.6;
677
+ if (params.year && result.year) {
678
+ if (result.year === params.year) {
679
+ score += 0.2;
680
+ } else if (Math.abs(result.year - params.year) <= 1) {
681
+ score += 0.1;
682
+ }
683
+ }
684
+ if (result.title.toLowerCase() === params.title.toLowerCase()) {
685
+ score += 0.2;
686
+ }
687
+ return Math.min(1, score);
688
+ }
689
+ async function handleMatch(params, client2) {
690
+ logger2.debug(`Matching: "${params.title}"`);
691
+ const response = await client2.search(params.title, 1, 10);
692
+ if (response.data.length === 0) {
693
+ return {
694
+ match: null,
695
+ confidence: 0
696
+ };
697
+ }
698
+ const scoredResults = response.data.map((series) => {
699
+ const result = mapSearchResult(series);
700
+ const score = scoreResult(result, params);
701
+ return { result, score };
702
+ }).sort((a, b) => b.score - a.score);
703
+ const best = scoredResults[0];
704
+ if (!best) {
705
+ return {
706
+ match: null,
707
+ confidence: 0
708
+ };
709
+ }
710
+ const alternatives = best.score < 0.8 ? scoredResults.slice(1, 4).map((s) => ({
711
+ ...s.result,
712
+ relevanceScore: s.score
713
+ })) : void 0;
714
+ return {
715
+ match: {
716
+ ...best.result,
717
+ relevanceScore: best.score
718
+ },
719
+ confidence: best.score,
720
+ alternatives
721
+ };
722
+ }
723
+
724
+ // src/handlers/search.ts
725
+ var logger3 = createLogger({ name: "mangabaka-search", level: "debug" });
726
+ async function handleSearch(params, client2) {
727
+ logger3.debug("Search params received:", params);
728
+ const limit = params.limit ?? 20;
729
+ const page = params.cursor ? Number.parseInt(params.cursor, 10) : 1;
730
+ logger3.debug(`Searching for: "${params.query}" (page ${page}, limit ${limit})`);
731
+ const response = await client2.search(params.query, page, limit);
732
+ const results = response.data.map(mapSearchResult);
733
+ const hasNextPage = response.page < response.totalPages;
734
+ const nextCursor = hasNextPage ? String(response.page + 1) : void 0;
735
+ return {
736
+ results,
737
+ nextCursor
738
+ };
739
+ }
740
+
741
+ // src/manifest.ts
742
+ var manifest = {
743
+ name: "metadata-mangabaka",
744
+ displayName: "MangaBaka Metadata",
745
+ version: "1.0.0",
746
+ description: "Fetch manga metadata from MangaBaka - aggregated data from multiple sources",
747
+ author: "Codex",
748
+ homepage: "https://mangabaka.org",
749
+ protocolVersion: "1.0",
750
+ capabilities: {
751
+ metadataProvider: ["series"]
752
+ },
753
+ requiredCredentials: [
754
+ {
755
+ key: "api_key",
756
+ label: "API Key",
757
+ description: "Get your API key at https://mangabaka.org/settings/api (requires account)",
758
+ required: true,
759
+ sensitive: true,
760
+ type: "password",
761
+ placeholder: "mb-..."
762
+ }
763
+ ]
764
+ };
765
+
766
+ // src/index.ts
767
+ var logger4 = createLogger({ name: "mangabaka", level: "info" });
768
+ var client = null;
769
+ function getClient() {
770
+ if (!client) {
771
+ throw new ConfigError("Plugin not initialized - missing API key");
772
+ }
773
+ return client;
774
+ }
775
+ var provider = {
776
+ async search(params) {
777
+ return handleSearch(params, getClient());
778
+ },
779
+ async get(params) {
780
+ return handleGet(params, getClient());
781
+ },
782
+ async match(params) {
783
+ return handleMatch(params, getClient());
784
+ }
785
+ };
786
+ createMetadataPlugin({
787
+ manifest,
788
+ provider,
789
+ logLevel: "info",
790
+ onInitialize(params) {
791
+ const apiKey = params.credentials?.api_key;
792
+ if (!apiKey) {
793
+ throw new ConfigError("api_key credential is required");
794
+ }
795
+ client = new MangaBakaClient(apiKey);
796
+ logger4.info("MangaBaka client initialized");
797
+ }
798
+ });
799
+ logger4.info("MangaBaka plugin started");
800
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../sdk-typescript/src/types/rpc.ts", "../../sdk-typescript/src/errors.ts", "../../sdk-typescript/src/logger.ts", "../../sdk-typescript/src/server.ts", "../src/api.ts", "../src/mappers.ts", "../src/handlers/get.ts", "../src/handlers/match.ts", "../src/handlers/search.ts", "../src/manifest.ts", "../src/index.ts"],
4
+ "sourcesContent": ["/**\n * JSON-RPC 2.0 types for plugin communication\n */\n\nexport interface JsonRpcRequest {\n jsonrpc: \"2.0\";\n id: string | number | null;\n method: string;\n params?: unknown;\n}\n\nexport interface JsonRpcSuccessResponse {\n jsonrpc: \"2.0\";\n id: string | number | null;\n result: unknown;\n}\n\nexport interface JsonRpcErrorResponse {\n jsonrpc: \"2.0\";\n id: string | number | null;\n error: JsonRpcError;\n}\n\nexport interface JsonRpcError {\n code: number;\n message: string;\n data?: unknown;\n}\n\nexport type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse;\n\n/**\n * Standard JSON-RPC error codes\n */\nexport const JSON_RPC_ERROR_CODES = {\n /** Invalid JSON was received */\n PARSE_ERROR: -32700,\n /** The JSON sent is not a valid Request object */\n INVALID_REQUEST: -32600,\n /** The method does not exist / is not available */\n METHOD_NOT_FOUND: -32601,\n /** Invalid method parameter(s) */\n INVALID_PARAMS: -32602,\n /** Internal JSON-RPC error */\n INTERNAL_ERROR: -32603,\n} as const;\n\n/**\n * Plugin-specific error codes (in the -32000 to -32099 range)\n */\nexport const PLUGIN_ERROR_CODES = {\n /** Rate limited by external API */\n RATE_LIMITED: -32001,\n /** Resource not found (e.g., series ID doesn't exist) */\n NOT_FOUND: -32002,\n /** Authentication failed (invalid credentials) */\n AUTH_FAILED: -32003,\n /** External API error */\n API_ERROR: -32004,\n /** Plugin configuration error */\n CONFIG_ERROR: -32005,\n} as const;\n", "/**\n * Plugin error classes for structured error handling\n */\n\nimport { type JsonRpcError, PLUGIN_ERROR_CODES } from \"./types/rpc.js\";\n\n/**\n * Base class for plugin errors that map to JSON-RPC errors\n */\nexport abstract class PluginError extends Error {\n abstract readonly code: number;\n readonly data?: unknown;\n\n constructor(message: string, data?: unknown) {\n super(message);\n this.name = this.constructor.name;\n this.data = data;\n }\n\n /**\n * Convert to JSON-RPC error format\n */\n toJsonRpcError(): JsonRpcError {\n return {\n code: this.code,\n message: this.message,\n data: this.data,\n };\n }\n}\n\n/**\n * Thrown when rate limited by an external API\n */\nexport class RateLimitError extends PluginError {\n readonly code = PLUGIN_ERROR_CODES.RATE_LIMITED;\n /** Seconds to wait before retrying */\n readonly retryAfterSeconds: number;\n\n constructor(retryAfterSeconds: number, message?: string) {\n super(message ?? `Rate limited, retry after ${retryAfterSeconds}s`, {\n retryAfterSeconds,\n });\n this.retryAfterSeconds = retryAfterSeconds;\n }\n}\n\n/**\n * Thrown when a requested resource is not found\n */\nexport class NotFoundError extends PluginError {\n readonly code = PLUGIN_ERROR_CODES.NOT_FOUND;\n}\n\n/**\n * Thrown when authentication fails (invalid credentials)\n */\nexport class AuthError extends PluginError {\n readonly code = PLUGIN_ERROR_CODES.AUTH_FAILED;\n\n constructor(message?: string) {\n super(message ?? \"Authentication failed\");\n }\n}\n\n/**\n * Thrown when an external API returns an error\n */\nexport class ApiError extends PluginError {\n readonly code = PLUGIN_ERROR_CODES.API_ERROR;\n readonly statusCode: number | undefined;\n\n constructor(message: string, statusCode?: number) {\n super(message, statusCode !== undefined ? { statusCode } : undefined);\n this.statusCode = statusCode;\n }\n}\n\n/**\n * Thrown when the plugin is misconfigured\n */\nexport class ConfigError extends PluginError {\n readonly code = PLUGIN_ERROR_CODES.CONFIG_ERROR;\n}\n", "/**\n * Logging utilities for plugins\n *\n * IMPORTANT: Plugins must ONLY write to stderr for logging.\n * stdout is reserved for JSON-RPC communication.\n */\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nconst LOG_LEVELS: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n};\n\nexport interface LoggerOptions {\n /** Plugin name to prefix log messages */\n name: string;\n /** Minimum log level (default: \"info\") */\n level?: LogLevel;\n /** Whether to include timestamps (default: true) */\n timestamps?: boolean;\n}\n\n/**\n * Logger that writes to stderr (safe for plugins)\n */\nexport class Logger {\n private readonly name: string;\n private readonly minLevel: number;\n private readonly timestamps: boolean;\n\n constructor(options: LoggerOptions) {\n this.name = options.name;\n this.minLevel = LOG_LEVELS[options.level ?? \"info\"];\n this.timestamps = options.timestamps ?? true;\n }\n\n private shouldLog(level: LogLevel): boolean {\n return LOG_LEVELS[level] >= this.minLevel;\n }\n\n private format(level: LogLevel, message: string, data?: unknown): string {\n const parts: string[] = [];\n\n if (this.timestamps) {\n parts.push(new Date().toISOString());\n }\n\n parts.push(`[${level.toUpperCase()}]`);\n parts.push(`[${this.name}]`);\n parts.push(message);\n\n if (data !== undefined) {\n if (data instanceof Error) {\n parts.push(`- ${data.message}`);\n if (data.stack) {\n parts.push(`\\n${data.stack}`);\n }\n } else if (typeof data === \"object\") {\n parts.push(`- ${JSON.stringify(data)}`);\n } else {\n parts.push(`- ${String(data)}`);\n }\n }\n\n return parts.join(\" \");\n }\n\n private log(level: LogLevel, message: string, data?: unknown): void {\n if (this.shouldLog(level)) {\n // Write to stderr (not stdout!) - stdout is for JSON-RPC only\n process.stderr.write(`${this.format(level, message, data)}\\n`);\n }\n }\n\n debug(message: string, data?: unknown): void {\n this.log(\"debug\", message, data);\n }\n\n info(message: string, data?: unknown): void {\n this.log(\"info\", message, data);\n }\n\n warn(message: string, data?: unknown): void {\n this.log(\"warn\", message, data);\n }\n\n error(message: string, data?: unknown): void {\n this.log(\"error\", message, data);\n }\n}\n\n/**\n * Create a logger for a plugin\n */\nexport function createLogger(options: LoggerOptions): Logger {\n return new Logger(options);\n}\n", "/**\n * Plugin server - handles JSON-RPC communication over stdio\n */\n\nimport { createInterface } from \"node:readline\";\nimport { PluginError } from \"./errors.js\";\nimport { createLogger, type Logger } from \"./logger.js\";\nimport type { MetadataContentType, MetadataProvider } from \"./types/capabilities.js\";\nimport type { PluginManifest } from \"./types/manifest.js\";\nimport type {\n MetadataGetParams,\n MetadataMatchParams,\n MetadataSearchParams,\n} from \"./types/protocol.js\";\nimport {\n JSON_RPC_ERROR_CODES,\n type JsonRpcError,\n type JsonRpcRequest,\n type JsonRpcResponse,\n} from \"./types/rpc.js\";\n\n// =============================================================================\n// Parameter Validation\n// =============================================================================\n\ninterface ValidationError {\n field: string;\n message: string;\n}\n\n/**\n * Validate that the required string fields are present and non-empty\n */\nfunction validateStringFields(params: unknown, fields: string[]): ValidationError | null {\n if (params === null || params === undefined) {\n return { field: \"params\", message: \"params is required\" };\n }\n if (typeof params !== \"object\") {\n return { field: \"params\", message: \"params must be an object\" };\n }\n\n const obj = params as Record<string, unknown>;\n for (const field of fields) {\n const value = obj[field];\n if (value === undefined || value === null) {\n return { field, message: `${field} is required` };\n }\n if (typeof value !== \"string\") {\n return { field, message: `${field} must be a string` };\n }\n if (value.trim() === \"\") {\n return { field, message: `${field} cannot be empty` };\n }\n }\n\n return null;\n}\n\n/**\n * Validate MetadataSearchParams\n */\nfunction validateSearchParams(params: unknown): ValidationError | null {\n return validateStringFields(params, [\"query\"]);\n}\n\n/**\n * Validate MetadataGetParams\n */\nfunction validateGetParams(params: unknown): ValidationError | null {\n return validateStringFields(params, [\"externalId\"]);\n}\n\n/**\n * Validate MetadataMatchParams\n */\nfunction validateMatchParams(params: unknown): ValidationError | null {\n return validateStringFields(params, [\"title\"]);\n}\n\n/**\n * Create an INVALID_PARAMS error response\n */\nfunction invalidParamsError(id: string | number | null, error: ValidationError): JsonRpcResponse {\n return {\n jsonrpc: \"2.0\",\n id,\n error: {\n code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,\n message: `Invalid params: ${error.message}`,\n data: { field: error.field },\n } as JsonRpcError,\n };\n}\n\n/**\n * Initialize parameters received from Codex\n */\nexport interface InitializeParams {\n /** Plugin configuration */\n config?: Record<string, unknown>;\n /** Plugin credentials (API keys, tokens, etc.) */\n credentials?: Record<string, string>;\n}\n\n/**\n * Options for creating a metadata plugin\n */\nexport interface MetadataPluginOptions {\n /** Plugin manifest - must have capabilities.metadataProvider with content types */\n manifest: PluginManifest & {\n capabilities: { metadataProvider: MetadataContentType[] };\n };\n /** MetadataProvider implementation */\n provider: MetadataProvider;\n /** Called when plugin receives initialize with credentials/config */\n onInitialize?: (params: InitializeParams) => void | Promise<void>;\n /** Log level (default: \"info\") */\n logLevel?: \"debug\" | \"info\" | \"warn\" | \"error\";\n}\n\n/**\n * Create and run a metadata provider plugin\n *\n * Creates a plugin server that handles JSON-RPC communication over stdio.\n * The TypeScript compiler will ensure you implement all required methods.\n *\n * @example\n * ```typescript\n * import { createMetadataPlugin, type MetadataProvider } from \"@ashdev/codex-plugin-sdk\";\n *\n * const provider: MetadataProvider = {\n * async search(params) {\n * return {\n * results: [{\n * externalId: \"123\",\n * title: \"Example\",\n * alternateTitles: [],\n * relevanceScore: 0.95,\n * }],\n * };\n * },\n * async get(params) {\n * return {\n * externalId: params.externalId,\n * externalUrl: \"https://example.com/123\",\n * alternateTitles: [],\n * genres: [],\n * tags: [],\n * authors: [],\n * artists: [],\n * externalLinks: [],\n * };\n * },\n * };\n *\n * createMetadataPlugin({\n * manifest: {\n * name: \"my-plugin\",\n * displayName: \"My Plugin\",\n * version: \"1.0.0\",\n * description: \"Example plugin\",\n * author: \"Me\",\n * protocolVersion: \"1.0\",\n * capabilities: { metadataProvider: [\"series\"] },\n * },\n * provider,\n * });\n * ```\n */\nexport function createMetadataPlugin(options: MetadataPluginOptions): void {\n const { manifest, provider, onInitialize, logLevel = \"info\" } = options;\n const logger = createLogger({ name: manifest.name, level: logLevel });\n\n logger.info(`Starting plugin: ${manifest.displayName} v${manifest.version}`);\n\n const rl = createInterface({\n input: process.stdin,\n terminal: false,\n });\n\n rl.on(\"line\", (line) => {\n void handleLine(line, manifest, provider, onInitialize, logger);\n });\n\n rl.on(\"close\", () => {\n logger.info(\"stdin closed, shutting down\");\n process.exit(0);\n });\n\n // Handle uncaught errors\n process.on(\"uncaughtException\", (error) => {\n logger.error(\"Uncaught exception\", error);\n process.exit(1);\n });\n\n process.on(\"unhandledRejection\", (reason) => {\n logger.error(\"Unhandled rejection\", reason);\n });\n}\n\n// =============================================================================\n// Backwards Compatibility (deprecated)\n// =============================================================================\n\n/**\n * @deprecated Use createMetadataPlugin instead\n */\nexport function createSeriesMetadataPlugin(options: SeriesMetadataPluginOptions): void {\n // Convert legacy options to new format\n const newOptions: MetadataPluginOptions = {\n ...options,\n manifest: {\n ...options.manifest,\n capabilities: {\n ...options.manifest.capabilities,\n metadataProvider: [\"series\"] as MetadataContentType[],\n },\n },\n };\n createMetadataPlugin(newOptions);\n}\n\n/**\n * @deprecated Use MetadataPluginOptions instead\n */\nexport interface SeriesMetadataPluginOptions {\n /** Plugin manifest - must have capabilities.seriesMetadataProvider: true */\n manifest: PluginManifest & {\n capabilities: { seriesMetadataProvider: true };\n };\n /** SeriesMetadataProvider implementation */\n provider: MetadataProvider;\n /** Called when plugin receives initialize with credentials/config */\n onInitialize?: (params: InitializeParams) => void | Promise<void>;\n /** Log level (default: \"info\") */\n logLevel?: \"debug\" | \"info\" | \"warn\" | \"error\";\n}\n\n// =============================================================================\n// Internal Implementation\n// =============================================================================\n\nasync function handleLine(\n line: string,\n manifest: PluginManifest,\n provider: MetadataProvider,\n onInitialize: ((params: InitializeParams) => void | Promise<void>) | undefined,\n logger: Logger,\n): Promise<void> {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n let id: string | number | null = null;\n\n try {\n const request = JSON.parse(trimmed) as JsonRpcRequest;\n id = request.id;\n\n logger.debug(`Received request: ${request.method}`, { id: request.id });\n\n const response = await handleRequest(request, manifest, provider, onInitialize, logger);\n // Shutdown handler writes response directly and returns null\n if (response !== null) {\n writeResponse(response);\n }\n } catch (error) {\n if (error instanceof SyntaxError) {\n // JSON parse error\n writeResponse({\n jsonrpc: \"2.0\",\n id: null,\n error: {\n code: JSON_RPC_ERROR_CODES.PARSE_ERROR,\n message: \"Parse error: invalid JSON\",\n },\n });\n } else if (error instanceof PluginError) {\n writeResponse({\n jsonrpc: \"2.0\",\n id,\n error: error.toJsonRpcError(),\n });\n } else {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n logger.error(\"Request failed\", error);\n writeResponse({\n jsonrpc: \"2.0\",\n id,\n error: {\n code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,\n message,\n },\n });\n }\n }\n}\n\nasync function handleRequest(\n request: JsonRpcRequest,\n manifest: PluginManifest,\n provider: MetadataProvider,\n onInitialize: ((params: InitializeParams) => void | Promise<void>) | undefined,\n logger: Logger,\n): Promise<JsonRpcResponse> {\n const { method, params, id } = request;\n\n switch (method) {\n case \"initialize\":\n // Call onInitialize callback if provided (to receive credentials/config)\n if (onInitialize) {\n await onInitialize(params as InitializeParams);\n }\n return {\n jsonrpc: \"2.0\",\n id,\n result: manifest,\n };\n\n case \"ping\":\n return {\n jsonrpc: \"2.0\",\n id,\n result: \"pong\",\n };\n\n case \"shutdown\": {\n logger.info(\"Shutdown requested\");\n // Write response directly with callback to ensure it's flushed before exit\n const response: JsonRpcResponse = {\n jsonrpc: \"2.0\",\n id,\n result: null,\n };\n process.stdout.write(`${JSON.stringify(response)}\\n`, () => {\n // Callback is called after the write is flushed to the OS\n process.exit(0);\n });\n // Return a sentinel that handleLine will recognize and skip normal writeResponse\n return null as unknown as JsonRpcResponse;\n }\n\n // Series metadata methods (scoped by content type)\n case \"metadata/series/search\": {\n const validationError = validateSearchParams(params);\n if (validationError) {\n return invalidParamsError(id, validationError);\n }\n return {\n jsonrpc: \"2.0\",\n id,\n result: await provider.search(params as MetadataSearchParams),\n };\n }\n\n case \"metadata/series/get\": {\n const validationError = validateGetParams(params);\n if (validationError) {\n return invalidParamsError(id, validationError);\n }\n return {\n jsonrpc: \"2.0\",\n id,\n result: await provider.get(params as MetadataGetParams),\n };\n }\n\n case \"metadata/series/match\": {\n if (!provider.match) {\n return {\n jsonrpc: \"2.0\",\n id,\n error: {\n code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,\n message: \"This plugin does not support match\",\n },\n };\n }\n const validationError = validateMatchParams(params);\n if (validationError) {\n return invalidParamsError(id, validationError);\n }\n return {\n jsonrpc: \"2.0\",\n id,\n result: await provider.match(params as MetadataMatchParams),\n };\n }\n\n // Future: book metadata methods\n // case \"metadata/book/search\":\n // case \"metadata/book/get\":\n // case \"metadata/book/match\":\n\n default:\n return {\n jsonrpc: \"2.0\",\n id,\n error: {\n code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,\n message: `Method not found: ${method}`,\n },\n };\n }\n}\n\nfunction writeResponse(response: JsonRpcResponse): void {\n // Write to stdout - this is the JSON-RPC channel\n process.stdout.write(`${JSON.stringify(response)}\\n`);\n}\n", "/**\n * MangaBaka API client\n * API docs: https://mangabaka.org/api\n */\n\nimport {\n ApiError,\n AuthError,\n createLogger,\n NotFoundError,\n RateLimitError,\n} from \"@ashdev/codex-plugin-sdk\";\nimport type { MbGetSeriesResponse, MbSearchResponse, MbSeries } from \"./types.js\";\n\nconst BASE_URL = \"https://api.mangabaka.dev\";\nconst logger = createLogger({ name: \"mangabaka-api\", level: \"debug\" });\n\nexport class MangaBakaClient {\n private readonly apiKey: string;\n\n constructor(apiKey: string) {\n if (!apiKey) {\n throw new AuthError(\"API key is required\");\n }\n this.apiKey = apiKey;\n }\n\n /**\n * Search for series by query\n */\n async search(\n query: string,\n page = 1,\n perPage = 20,\n ): Promise<{ data: MbSeries[]; total: number; page: number; totalPages: number }> {\n logger.debug(`Searching for: \"${query}\" (page ${page})`);\n\n const params = new URLSearchParams({\n q: query,\n page: String(page),\n limit: String(perPage),\n });\n\n const response = await this.request<MbSearchResponse>(`/v1/series/search?${params.toString()}`);\n\n return {\n data: response.data,\n total: response.pagination?.total ?? response.data.length,\n page: response.pagination?.page ?? page,\n totalPages: response.pagination?.total_pages ?? 1,\n };\n }\n\n /**\n * Get full series details by ID\n */\n async getSeries(id: number): Promise<MbSeries> {\n logger.debug(`Getting series: ${id}`);\n\n const response = await this.request<MbGetSeriesResponse>(`/v1/series/${id}`);\n\n return response.data;\n }\n\n /**\n * Make an authenticated request to the MangaBaka API\n */\n private async request<T>(path: string): Promise<T> {\n const url = `${BASE_URL}${path}`;\n const headers: Record<string, string> = {\n \"x-api-key\": this.apiKey,\n Accept: \"application/json\",\n };\n\n try {\n const response = await fetch(url, {\n method: \"GET\",\n headers,\n });\n\n // Handle rate limiting\n if (response.status === 429) {\n const retryAfter = response.headers.get(\"Retry-After\");\n const seconds = retryAfter ? Number.parseInt(retryAfter, 10) : 60;\n throw new RateLimitError(seconds);\n }\n\n // Handle auth errors\n if (response.status === 401 || response.status === 403) {\n throw new AuthError(\"Invalid API key\");\n }\n\n // Handle not found\n if (response.status === 404) {\n throw new NotFoundError(`Resource not found: ${path}`);\n }\n\n // Handle other errors\n if (!response.ok) {\n const text = await response.text();\n logger.error(`API error: ${response.status}`, { body: text });\n throw new ApiError(`API error: ${response.status} ${response.statusText}`, response.status);\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n // Re-throw plugin errors\n if (\n error instanceof RateLimitError ||\n error instanceof AuthError ||\n error instanceof NotFoundError ||\n error instanceof ApiError\n ) {\n throw error;\n }\n\n // Wrap other errors\n const message = error instanceof Error ? error.message : \"Unknown error\";\n logger.error(\"Request failed\", error);\n throw new ApiError(`Request failed: ${message}`);\n }\n }\n}\n", "/**\n * Mappers to convert MangaBaka API responses to Codex plugin protocol types\n */\n\nimport type {\n AlternateTitle,\n ExternalLink,\n ExternalRating,\n PluginSeriesMetadata,\n ReadingDirection,\n SearchResult,\n SeriesStatus,\n} from \"@ashdev/codex-plugin-sdk\";\nimport type { MbContentRating, MbSeries, MbSeriesType, MbStatus } from \"./types.js\";\n\n/**\n * Map MangaBaka status to protocol SeriesStatus\n * MangaBaka uses: cancelled, completed, hiatus, releasing, unknown, upcoming\n * Codex uses: ongoing, ended, hiatus, abandoned, unknown\n */\nfunction mapStatus(mbStatus: MbStatus): SeriesStatus {\n switch (mbStatus) {\n case \"completed\":\n return \"ended\";\n case \"releasing\":\n case \"upcoming\":\n return \"ongoing\";\n case \"hiatus\":\n return \"hiatus\";\n case \"cancelled\":\n return \"abandoned\";\n default:\n return \"unknown\";\n }\n}\n\n/**\n * Format genre from snake_case to Title Case\n */\nfunction formatGenre(genre: string): string {\n return genre\n .split(\"_\")\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(\" \");\n}\n\n/**\n * Detect language code from country of origin\n */\nfunction detectLanguageFromCountry(country: string | null | undefined): string | undefined {\n if (!country) return undefined;\n\n const countryLower = country.toLowerCase();\n if (countryLower === \"jp\" || countryLower === \"japan\") return \"ja\";\n if (countryLower === \"kr\" || countryLower === \"korea\" || countryLower === \"south korea\")\n return \"ko\";\n if (countryLower === \"cn\" || countryLower === \"china\") return \"zh\";\n if (countryLower === \"tw\" || countryLower === \"taiwan\") return \"zh-TW\";\n\n return undefined;\n}\n\n/**\n * Map MangaBaka content rating to numeric age rating\n */\nfunction mapContentRating(rating: MbContentRating | null | undefined): number | undefined {\n if (!rating) return undefined;\n\n switch (rating) {\n case \"safe\":\n return 0; // All ages\n case \"suggestive\":\n return 13; // Teen\n case \"erotica\":\n return 16; // Mature\n case \"pornographic\":\n return 18; // Adults only\n default:\n return undefined;\n }\n}\n\n/**\n * Extract rating value from either a number or an object with bayesian/average\n */\nfunction extractRating(\n rating: number | { bayesian?: number | null; average?: number | null } | null | undefined,\n): number | undefined {\n if (rating == null) return undefined;\n if (typeof rating === \"number\") return rating;\n return rating.bayesian ?? rating.average ?? undefined;\n}\n\n/**\n * Infer reading direction from series type and country\n */\nfunction inferReadingDirection(\n seriesType: MbSeriesType,\n country: string | null | undefined,\n): ReadingDirection | undefined {\n // Manhwa (Korean) and Manhua (Chinese) are typically left-to-right\n if (seriesType === \"manhwa\" || seriesType === \"manhua\") {\n return \"ltr\";\n }\n\n // Manga (Japanese) is typically right-to-left\n if (seriesType === \"manga\") {\n return \"rtl\";\n }\n\n // OEL (Original English Language) is left-to-right\n if (seriesType === \"oel\") {\n return \"ltr\";\n }\n\n // Fall back to country-based detection\n if (country) {\n const countryLower = country.toLowerCase();\n if (countryLower === \"jp\" || countryLower === \"japan\") return \"rtl\";\n if (countryLower === \"kr\" || countryLower === \"korea\" || countryLower === \"south korea\")\n return \"ltr\";\n if (countryLower === \"cn\" || countryLower === \"china\") return \"ltr\";\n if (countryLower === \"tw\" || countryLower === \"taiwan\") return \"ltr\";\n }\n\n return undefined;\n}\n\n/**\n * Map a MangaBaka series to a protocol SearchResult\n */\nexport function mapSearchResult(series: MbSeries): SearchResult {\n // Get cover URL - prefer x250 for search results\n const coverUrl = series.cover?.x250?.x1 ?? series.cover?.raw?.url ?? undefined;\n\n // Build alternate titles array\n const alternateTitles: string[] = [];\n if (series.native_title && series.native_title !== series.title) {\n alternateTitles.push(series.native_title);\n }\n if (series.romanized_title && series.romanized_title !== series.title) {\n alternateTitles.push(series.romanized_title);\n }\n\n // Note: relevanceScore is omitted - the API already returns results in relevance order\n return {\n externalId: String(series.id),\n title: series.title,\n alternateTitles,\n year: series.year ?? undefined,\n coverUrl: coverUrl ?? undefined,\n preview: {\n status: mapStatus(series.status),\n genres: (series.genres ?? []).slice(0, 3).map(formatGenre),\n rating: extractRating(series.rating),\n description: series.description?.slice(0, 200) ?? undefined,\n },\n };\n}\n\n/**\n * Map full series response to protocol PluginSeriesMetadata\n */\nexport function mapSeriesMetadata(series: MbSeries): PluginSeriesMetadata {\n // Build alternate titles array with language info\n const alternateTitles: AlternateTitle[] = [];\n\n // Add native title\n if (series.native_title && series.native_title !== series.title) {\n alternateTitles.push({\n title: series.native_title,\n language: detectLanguageFromCountry(series.country_of_origin),\n titleType: \"native\",\n });\n }\n\n // Add romanized title\n if (series.romanized_title && series.romanized_title !== series.title) {\n alternateTitles.push({\n title: series.romanized_title,\n language: \"en\",\n titleType: \"romaji\",\n });\n }\n\n // Add secondary titles from all languages\n if (series.secondary_titles) {\n for (const [langCode, titleList] of Object.entries(series.secondary_titles)) {\n if (titleList) {\n for (const titleEntry of titleList) {\n if (titleEntry.title !== series.title) {\n alternateTitles.push({\n title: titleEntry.title,\n language: langCode,\n });\n }\n }\n }\n }\n }\n\n // Extract authors and artists as string arrays\n const authors = series.authors ?? [];\n const artists = series.artists ?? [];\n\n // Format genres\n const genres = (series.genres ?? []).map(formatGenre);\n\n // Get cover URL - prefer raw for full metadata\n const coverUrl = series.cover?.raw?.url ?? series.cover?.x350?.x1 ?? undefined;\n\n // Build external links from sources\n // Always include MangaBaka link first\n const externalLinks: ExternalLink[] = [\n {\n url: `https://mangabaka.org/${series.id}`,\n label: \"MangaBaka\",\n linkType: \"provider\",\n },\n ];\n\n // Source configuration: display name, rating key, and URL pattern\n // URL pattern uses {id} as placeholder for the source ID\n const sourceConfig: Record<string, { label: string; ratingKey: string; urlPattern?: string }> = {\n anilist: {\n label: \"AniList\",\n ratingKey: \"anilist\",\n urlPattern: \"https://anilist.co/manga/{id}\",\n },\n my_anime_list: {\n label: \"MyAnimeList\",\n ratingKey: \"myanimelist\",\n urlPattern: \"https://myanimelist.net/manga/{id}\",\n },\n mangadex: {\n label: \"MangaDex\",\n ratingKey: \"mangadex\",\n urlPattern: \"https://mangadex.org/title/{id}\",\n },\n manga_updates: {\n label: \"MangaUpdates\",\n ratingKey: \"mangaupdates\",\n urlPattern: \"https://www.mangaupdates.com/series/{id}\",\n },\n kitsu: { label: \"Kitsu\", ratingKey: \"kitsu\", urlPattern: \"https://kitsu.app/manga/{id}\" },\n anime_planet: {\n label: \"Anime-Planet\",\n ratingKey: \"animeplanet\",\n urlPattern: \"https://www.anime-planet.com/manga/{id}\",\n },\n anime_news_network: { label: \"Anime News Network\", ratingKey: \"animenewsnetwork\" },\n shikimori: {\n label: \"Shikimori\",\n ratingKey: \"shikimori\",\n urlPattern: \"https://shikimori.one/mangas/{id}\",\n },\n };\n\n // Build external links and ratings from sources in a single pass\n const externalRatings: ExternalRating[] = [];\n\n if (series.source) {\n for (const [key, info] of Object.entries(series.source)) {\n if (!info) continue;\n\n const config = sourceConfig[key];\n // Use config if available, otherwise generate defaults from key\n const ratingKey = config?.ratingKey ?? key.replace(/_/g, \"\");\n\n // Add external link if source has an ID and URL pattern\n if (info.id != null && config?.urlPattern) {\n externalLinks.push({\n url: config.urlPattern.replace(\"{id}\", String(info.id)),\n label: config.label,\n linkType: \"provider\",\n });\n }\n\n // Add external rating if source has a normalized rating\n if (info.rating_normalized != null) {\n externalRatings.push({ score: info.rating_normalized, source: ratingKey });\n }\n }\n }\n\n // Get publisher name (pick first one if available)\n const publisher = series.publishers?.[0]?.name ?? undefined;\n\n return {\n externalId: String(series.id),\n externalUrl: `https://mangabaka.org/${series.id}`,\n title: series.title,\n alternateTitles,\n summary: series.description ?? undefined,\n status: mapStatus(series.status),\n year: series.year ?? undefined,\n // Extended metadata\n publisher,\n totalBookCount: series.final_volume ? Number.parseInt(series.final_volume, 10) : undefined,\n ageRating: mapContentRating(series.content_rating),\n readingDirection: inferReadingDirection(series.type, series.country_of_origin),\n // Taxonomy\n genres,\n tags: series.tags ?? [],\n authors,\n artists,\n coverUrl: coverUrl ?? undefined,\n rating: (() => {\n const r = extractRating(series.rating);\n return r != null ? { score: r, source: \"mangabaka\" } : undefined;\n })(),\n externalRatings: externalRatings.length > 0 ? externalRatings : undefined,\n externalLinks,\n };\n}\n", "import {\n type MetadataGetParams,\n NotFoundError,\n type PluginSeriesMetadata,\n} from \"@ashdev/codex-plugin-sdk\";\nimport type { MangaBakaClient } from \"../api.js\";\nimport { mapSeriesMetadata } from \"../mappers.js\";\n\nexport async function handleGet(\n params: MetadataGetParams,\n client: MangaBakaClient,\n): Promise<PluginSeriesMetadata> {\n const seriesId = Number.parseInt(params.externalId, 10);\n\n if (Number.isNaN(seriesId)) {\n throw new NotFoundError(`Invalid external ID: ${params.externalId}`);\n }\n\n const response = await client.getSeries(seriesId);\n\n return mapSeriesMetadata(response);\n}\n", "import {\n createLogger,\n type MetadataMatchParams,\n type MetadataMatchResponse,\n type SearchResult,\n} from \"@ashdev/codex-plugin-sdk\";\nimport type { MangaBakaClient } from \"../api.js\";\nimport { mapSearchResult } from \"../mappers.js\";\n\nconst logger = createLogger({ name: \"mangabaka-match\", level: \"info\" });\n\n/**\n * Calculate string similarity using word overlap\n * Returns a value between 0 and 1\n */\nfunction similarity(a: string, b: string): number {\n const aLower = a.toLowerCase().trim();\n const bLower = b.toLowerCase().trim();\n\n if (aLower === bLower) return 1.0;\n if (aLower.length === 0 || bLower.length === 0) return 0;\n\n // Check if one contains the other\n if (aLower.includes(bLower) || bLower.includes(aLower)) {\n return 0.8;\n }\n\n // Simple word overlap scoring\n const aWords = new Set(aLower.split(/\\s+/));\n const bWords = new Set(bLower.split(/\\s+/));\n const intersection = [...aWords].filter((w) => bWords.has(w));\n const union = new Set([...aWords, ...bWords]);\n\n return intersection.length / union.size;\n}\n\n/**\n * Score a search result against the match parameters\n * Returns a value between 0 and 1\n */\nfunction scoreResult(result: SearchResult, params: MetadataMatchParams): number {\n let score = 0;\n\n // Title similarity (up to 0.6)\n const titleScore = similarity(result.title, params.title);\n score += titleScore * 0.6;\n\n // Year match (up to 0.2)\n if (params.year && result.year) {\n if (result.year === params.year) {\n score += 0.2;\n } else if (Math.abs(result.year - params.year) <= 1) {\n score += 0.1;\n }\n }\n\n // Boost for exact title match (up to 0.2)\n if (result.title.toLowerCase() === params.title.toLowerCase()) {\n score += 0.2;\n }\n\n return Math.min(1.0, score);\n}\n\nexport async function handleMatch(\n params: MetadataMatchParams,\n client: MangaBakaClient,\n): Promise<MetadataMatchResponse> {\n logger.debug(`Matching: \"${params.title}\"`);\n\n // Search for the title\n const response = await client.search(params.title, 1, 10);\n\n if (response.data.length === 0) {\n return {\n match: null,\n confidence: 0,\n };\n }\n\n // Map and score results\n const scoredResults = response.data\n .map((series) => {\n const result = mapSearchResult(series);\n const score = scoreResult(result, params);\n return { result, score };\n })\n .sort((a, b) => b.score - a.score);\n\n const best = scoredResults[0];\n\n if (!best) {\n return {\n match: null,\n confidence: 0,\n };\n }\n\n // If confidence is low, include alternatives\n const alternatives =\n best.score < 0.8\n ? scoredResults.slice(1, 4).map((s) => ({\n ...s.result,\n relevanceScore: s.score,\n }))\n : undefined;\n\n return {\n match: {\n ...best.result,\n relevanceScore: best.score,\n },\n confidence: best.score,\n alternatives,\n };\n}\n", "import {\n createLogger,\n type MetadataSearchParams,\n type MetadataSearchResponse,\n} from \"@ashdev/codex-plugin-sdk\";\nimport type { MangaBakaClient } from \"../api.js\";\nimport { mapSearchResult } from \"../mappers.js\";\n\nconst logger = createLogger({ name: \"mangabaka-search\", level: \"debug\" });\n\nexport async function handleSearch(\n params: MetadataSearchParams,\n client: MangaBakaClient,\n): Promise<MetadataSearchResponse> {\n logger.debug(\"Search params received:\", params);\n\n const limit = params.limit ?? 20;\n\n // Parse cursor as page number (default to 1)\n const page = params.cursor ? Number.parseInt(params.cursor, 10) : 1;\n\n logger.debug(`Searching for: \"${params.query}\" (page ${page}, limit ${limit})`);\n\n const response = await client.search(params.query, page, limit);\n\n // Map results - API already returns them sorted by relevance\n const results = response.data.map(mapSearchResult);\n\n // Calculate next cursor (next page number) if there are more results\n const hasNextPage = response.page < response.totalPages;\n const nextCursor = hasNextPage ? String(response.page + 1) : undefined;\n\n return {\n results,\n nextCursor,\n };\n}\n", "import type { MetadataContentType, PluginManifest } from \"@ashdev/codex-plugin-sdk\";\n\nexport const manifest = {\n name: \"metadata-mangabaka\",\n displayName: \"MangaBaka Metadata\",\n version: \"1.0.0\",\n description: \"Fetch manga metadata from MangaBaka - aggregated data from multiple sources\",\n author: \"Codex\",\n homepage: \"https://mangabaka.org\",\n protocolVersion: \"1.0\",\n capabilities: {\n metadataProvider: [\"series\"] as MetadataContentType[],\n },\n requiredCredentials: [\n {\n key: \"api_key\",\n label: \"API Key\",\n description: \"Get your API key at https://mangabaka.org/settings/api (requires account)\",\n required: true,\n sensitive: true,\n type: \"password\",\n placeholder: \"mb-...\",\n },\n ],\n} as const satisfies PluginManifest & {\n capabilities: { metadataProvider: MetadataContentType[] };\n};\n", "/**\n * MangaBaka Plugin - Fetch manga metadata from MangaBaka\n *\n * MangaBaka aggregates metadata from multiple sources (AniList, MAL, MangaDex, etc.)\n * and provides a unified API for manga/novel metadata.\n *\n * API docs: https://mangabaka.org/api\n *\n * Credentials are provided by Codex via the initialize message.\n * Required credential: api_key (get one at https://mangabaka.org/settings/api)\n */\n\nimport {\n ConfigError,\n createLogger,\n createMetadataPlugin,\n type InitializeParams,\n type MetadataProvider,\n} from \"@ashdev/codex-plugin-sdk\";\nimport { MangaBakaClient } from \"./api.js\";\nimport { handleGet } from \"./handlers/get.js\";\nimport { handleMatch } from \"./handlers/match.js\";\nimport { handleSearch } from \"./handlers/search.js\";\nimport { manifest } from \"./manifest.js\";\n\nconst logger = createLogger({ name: \"mangabaka\", level: \"info\" });\n\n// Client is initialized when we receive credentials from Codex\nlet client: MangaBakaClient | null = null;\n\nfunction getClient(): MangaBakaClient {\n if (!client) {\n throw new ConfigError(\"Plugin not initialized - missing API key\");\n }\n return client;\n}\n\n// Create the MetadataProvider implementation\nconst provider: MetadataProvider = {\n async search(params) {\n return handleSearch(params, getClient());\n },\n\n async get(params) {\n return handleGet(params, getClient());\n },\n\n async match(params) {\n return handleMatch(params, getClient());\n },\n};\n\n// Start the plugin server\ncreateMetadataPlugin({\n manifest,\n provider,\n logLevel: \"info\",\n onInitialize(params: InitializeParams) {\n const apiKey = params.credentials?.api_key;\n if (!apiKey) {\n throw new ConfigError(\"api_key credential is required\");\n }\n client = new MangaBakaClient(apiKey);\n logger.info(\"MangaBaka client initialized\");\n },\n});\n\nlogger.info(\"MangaBaka plugin started\");\n"],
5
+ "mappings": "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",
6
+ "names": ["manifest", "provider", "logger", "manifest", "provider", "logger", "client", "logger", "client", "logger", "client", "logger"]
7
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@ashdev/codex-plugin-metadata-mangabaka",
3
+ "version": "1.0.0",
4
+ "description": "MangaBaka metadata provider plugin for Codex",
5
+ "main": "dist/index.js",
6
+ "bin": "dist/index.js",
7
+ "type": "module",
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/AshDevFr/codex.git",
15
+ "directory": "plugins/metadata-mangabaka"
16
+ },
17
+ "scripts": {
18
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --sourcemap --banner:js='#!/usr/bin/env node'",
19
+ "dev": "npm run build -- --watch",
20
+ "clean": "rm -rf dist",
21
+ "start": "node dist/index.js",
22
+ "lint": "biome check .",
23
+ "lint:fix": "biome check --write .",
24
+ "typecheck": "tsc --noEmit",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "prepublishOnly": "npm run lint && npm run build"
28
+ },
29
+ "keywords": [
30
+ "codex",
31
+ "plugin",
32
+ "mangabaka",
33
+ "manga",
34
+ "metadata"
35
+ ],
36
+ "author": "Codex",
37
+ "license": "MIT",
38
+ "engines": {
39
+ "node": ">=22.0.0"
40
+ },
41
+ "dependencies": {
42
+ "@ashdev/codex-plugin-sdk": "^1.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@biomejs/biome": "^2.3.11",
46
+ "@types/node": "^22.0.0",
47
+ "esbuild": "^0.24.0",
48
+ "typescript": "^5.7.0",
49
+ "vitest": "^3.0.0"
50
+ }
51
+ }