@ashdev/codex-plugin-recommendations-anilist 1.10.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,127 @@
1
+ # @ashdev/codex-plugin-recommendations-anilist
2
+
3
+ A Codex plugin for personalized manga recommendations powered by [AniList](https://anilist.co) community data. Generates recommendations based on your reading history and ratings.
4
+
5
+ ## Features
6
+
7
+ - Personalized manga recommendations from AniList
8
+ - Based on your library ratings and reading history
9
+ - Configurable maximum number of recommendations
10
+ - Uses AniList's recommendation and user list APIs
11
+
12
+ ## Authentication
13
+
14
+ This plugin supports two authentication methods:
15
+
16
+ ### OAuth (Recommended)
17
+
18
+ If your Codex administrator has configured OAuth:
19
+
20
+ 1. Go to **Settings** > **Integrations**
21
+ 2. Click **Connect with AniList Recommendations**
22
+ 3. Authorize Codex on AniList
23
+ 4. You're connected!
24
+
25
+ ### Personal Access Token
26
+
27
+ If OAuth is not configured by the admin:
28
+
29
+ 1. Go to [AniList Developer Settings](https://anilist.co/settings/developer)
30
+ 2. Click **Create New Client**
31
+ 3. Set the redirect URL to `https://anilist.co/api/v2/oauth/pin`
32
+ 4. Click **Save**, then **Authorize** your new client
33
+ 5. Copy the token shown on the pin page
34
+ 6. In Codex, go to **Settings** > **Integrations**
35
+ 7. Paste the token in the access token field and click **Save Token**
36
+
37
+ ## Admin Setup
38
+
39
+ ### Adding the Plugin to Codex
40
+
41
+ 1. Log in to Codex as an administrator
42
+ 2. Navigate to **Settings** > **Plugins**
43
+ 3. Click **Add Plugin**
44
+ 4. Fill in the form:
45
+ - **Name**: `recommendations-anilist`
46
+ - **Display Name**: `AniList Recommendations`
47
+ - **Command**: `npx`
48
+ - **Arguments**: `-y @ashdev/codex-plugin-recommendations-anilist@1.9.3`
49
+ 5. Click **Save**
50
+ 6. Click **Test Connection** to verify the plugin works
51
+
52
+ ### Configuring OAuth (Optional)
53
+
54
+ To enable OAuth login for your users:
55
+
56
+ 1. Go to [AniList Developer Settings](https://anilist.co/settings/developer)
57
+ 2. Click **Create New Client**
58
+ 3. Set the redirect URL to `{your-codex-url}/api/v1/user/plugins/oauth/callback`
59
+ 4. Save and copy the **Client ID**
60
+ 5. In Codex, go to **Settings** > **Plugins** > click the gear icon on AniList Recommendations
61
+ 6. Go to the **OAuth** tab
62
+ 7. Paste the **Client ID** (and optionally the **Client Secret**)
63
+ 8. Click **Save Changes**
64
+
65
+ Without OAuth configured, users can still connect by pasting a personal access token.
66
+
67
+ ### npx Options
68
+
69
+ | Configuration | Arguments | Description |
70
+ |--------------|-----------|-------------|
71
+ | Latest version | `-y @ashdev/codex-plugin-recommendations-anilist` | Always uses latest |
72
+ | Pinned version | `-y @ashdev/codex-plugin-recommendations-anilist@1.9.3` | Recommended for production |
73
+ | Fast startup | `-y --prefer-offline @ashdev/codex-plugin-recommendations-anilist@1.9.3` | Skips version check if cached |
74
+
75
+ ## Configuration
76
+
77
+ ### Plugin Config
78
+
79
+ | Parameter | Type | Default | Description |
80
+ |-----------|------|---------|-------------|
81
+ | `maxRecommendations` | number | `20` | Maximum number of recommendations to generate (1-50) |
82
+
83
+ ## Using the Plugin
84
+
85
+ Once connected, recommendations appear in the Codex UI:
86
+
87
+ 1. Go to **Settings** > **Integrations** and verify the plugin shows as **Connected**
88
+ 2. Recommendations are generated based on your library ratings and reading history
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ # Install dependencies
94
+ npm install
95
+
96
+ # Build the plugin
97
+ npm run build
98
+
99
+ # Type check
100
+ npm run typecheck
101
+
102
+ # Run tests
103
+ npm test
104
+
105
+ # Lint
106
+ npm run lint
107
+ ```
108
+
109
+ ## Project Structure
110
+
111
+ ```
112
+ plugins/recommendations-anilist/
113
+ ├── src/
114
+ │ ├── index.ts # Plugin entry point
115
+ │ ├── manifest.ts # Plugin manifest
116
+ │ ├── anilist.ts # AniList API client
117
+ │ └── anilist.test.ts # API client tests
118
+ ├── dist/
119
+ │ └── index.js # Built bundle (excluded from git)
120
+ ├── package.json
121
+ ├── tsconfig.json
122
+ └── README.md
123
+ ```
124
+
125
+ ## License
126
+
127
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,1025 @@
1
+ #!/usr/bin/env node
2
+
3
+ // node_modules/@ashdev/codex-plugin-sdk/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
+ // node_modules/@ashdev/codex-plugin-sdk/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 AuthError = class extends PluginError {
60
+ code = PLUGIN_ERROR_CODES.AUTH_FAILED;
61
+ constructor(message) {
62
+ super(message ?? "Authentication failed");
63
+ }
64
+ };
65
+ var ApiError = class extends PluginError {
66
+ code = PLUGIN_ERROR_CODES.API_ERROR;
67
+ statusCode;
68
+ constructor(message, statusCode) {
69
+ super(message, statusCode !== void 0 ? { statusCode } : void 0);
70
+ this.statusCode = statusCode;
71
+ }
72
+ };
73
+
74
+ // node_modules/@ashdev/codex-plugin-sdk/dist/logger.js
75
+ var LOG_LEVELS = {
76
+ debug: 0,
77
+ info: 1,
78
+ warn: 2,
79
+ error: 3
80
+ };
81
+ var Logger = class {
82
+ name;
83
+ minLevel;
84
+ timestamps;
85
+ constructor(options) {
86
+ this.name = options.name;
87
+ this.minLevel = LOG_LEVELS[options.level ?? "info"];
88
+ this.timestamps = options.timestamps ?? true;
89
+ }
90
+ shouldLog(level) {
91
+ return LOG_LEVELS[level] >= this.minLevel;
92
+ }
93
+ format(level, message, data) {
94
+ const parts = [];
95
+ if (this.timestamps) {
96
+ parts.push((/* @__PURE__ */ new Date()).toISOString());
97
+ }
98
+ parts.push(`[${level.toUpperCase()}]`);
99
+ parts.push(`[${this.name}]`);
100
+ parts.push(message);
101
+ if (data !== void 0) {
102
+ if (data instanceof Error) {
103
+ parts.push(`- ${data.message}`);
104
+ if (data.stack) {
105
+ parts.push(`
106
+ ${data.stack}`);
107
+ }
108
+ } else if (typeof data === "object") {
109
+ parts.push(`- ${JSON.stringify(data)}`);
110
+ } else {
111
+ parts.push(`- ${String(data)}`);
112
+ }
113
+ }
114
+ return parts.join(" ");
115
+ }
116
+ log(level, message, data) {
117
+ if (this.shouldLog(level)) {
118
+ process.stderr.write(`${this.format(level, message, data)}
119
+ `);
120
+ }
121
+ }
122
+ debug(message, data) {
123
+ this.log("debug", message, data);
124
+ }
125
+ info(message, data) {
126
+ this.log("info", message, data);
127
+ }
128
+ warn(message, data) {
129
+ this.log("warn", message, data);
130
+ }
131
+ error(message, data) {
132
+ this.log("error", message, data);
133
+ }
134
+ };
135
+ function createLogger(options) {
136
+ return new Logger(options);
137
+ }
138
+
139
+ // node_modules/@ashdev/codex-plugin-sdk/dist/server.js
140
+ import { createInterface } from "node:readline";
141
+
142
+ // node_modules/@ashdev/codex-plugin-sdk/dist/storage.js
143
+ var StorageError = class extends Error {
144
+ code;
145
+ data;
146
+ constructor(message, code, data) {
147
+ super(message);
148
+ this.code = code;
149
+ this.data = data;
150
+ this.name = "StorageError";
151
+ }
152
+ };
153
+ var PluginStorage = class {
154
+ nextId = 1;
155
+ pendingRequests = /* @__PURE__ */ new Map();
156
+ writeFn;
157
+ /**
158
+ * Create a new storage client.
159
+ *
160
+ * @param writeFn - Optional custom write function (defaults to process.stdout.write).
161
+ * Useful for testing or custom transport layers.
162
+ */
163
+ constructor(writeFn) {
164
+ this.writeFn = writeFn ?? ((line) => {
165
+ process.stdout.write(line);
166
+ });
167
+ }
168
+ /**
169
+ * Get a value by key
170
+ *
171
+ * @param key - Storage key to retrieve
172
+ * @returns The stored data and optional expiration, or null data if key doesn't exist
173
+ */
174
+ async get(key) {
175
+ return await this.sendRequest("storage/get", { key });
176
+ }
177
+ /**
178
+ * Set a value by key (upsert - creates or updates)
179
+ *
180
+ * @param key - Storage key
181
+ * @param data - JSON-serializable data to store
182
+ * @param expiresAt - Optional expiration timestamp (ISO 8601)
183
+ * @returns Success indicator
184
+ */
185
+ async set(key, data, expiresAt) {
186
+ const params = { key, data };
187
+ if (expiresAt !== void 0) {
188
+ params.expiresAt = expiresAt;
189
+ }
190
+ return await this.sendRequest("storage/set", params);
191
+ }
192
+ /**
193
+ * Delete a value by key
194
+ *
195
+ * @param key - Storage key to delete
196
+ * @returns Whether the key existed and was deleted
197
+ */
198
+ async delete(key) {
199
+ return await this.sendRequest("storage/delete", { key });
200
+ }
201
+ /**
202
+ * List all keys for this plugin instance (excluding expired)
203
+ *
204
+ * @returns List of key entries with metadata
205
+ */
206
+ async list() {
207
+ return await this.sendRequest("storage/list", {});
208
+ }
209
+ /**
210
+ * Clear all data for this plugin instance
211
+ *
212
+ * @returns Number of entries deleted
213
+ */
214
+ async clear() {
215
+ return await this.sendRequest("storage/clear", {});
216
+ }
217
+ /**
218
+ * Handle an incoming JSON-RPC response line from the host.
219
+ *
220
+ * Call this method from your readline handler to deliver responses
221
+ * back to pending storage requests.
222
+ */
223
+ handleResponse(line) {
224
+ const trimmed = line.trim();
225
+ if (!trimmed)
226
+ return;
227
+ let parsed;
228
+ try {
229
+ parsed = JSON.parse(trimmed);
230
+ } catch {
231
+ return;
232
+ }
233
+ const obj = parsed;
234
+ if (obj.method !== void 0) {
235
+ return;
236
+ }
237
+ const id = obj.id;
238
+ if (id === void 0 || id === null)
239
+ return;
240
+ const pending = this.pendingRequests.get(id);
241
+ if (!pending)
242
+ return;
243
+ this.pendingRequests.delete(id);
244
+ if ("error" in obj && obj.error) {
245
+ const err = obj.error;
246
+ pending.reject(new StorageError(err.message, err.code, err.data));
247
+ } else {
248
+ pending.resolve(obj.result);
249
+ }
250
+ }
251
+ /**
252
+ * Cancel all pending requests (e.g. on shutdown).
253
+ */
254
+ cancelAll() {
255
+ for (const [, pending] of this.pendingRequests) {
256
+ pending.reject(new StorageError("Storage client stopped", -1));
257
+ }
258
+ this.pendingRequests.clear();
259
+ }
260
+ // ===========================================================================
261
+ // Internal
262
+ // ===========================================================================
263
+ sendRequest(method, params) {
264
+ const id = this.nextId++;
265
+ const request = {
266
+ jsonrpc: "2.0",
267
+ id,
268
+ method,
269
+ params
270
+ };
271
+ return new Promise((resolve, reject) => {
272
+ this.pendingRequests.set(id, { resolve, reject });
273
+ try {
274
+ this.writeFn(`${JSON.stringify(request)}
275
+ `);
276
+ } catch (err) {
277
+ this.pendingRequests.delete(id);
278
+ const message = err instanceof Error ? err.message : "Unknown write error";
279
+ reject(new StorageError(`Failed to send request: ${message}`, -1));
280
+ }
281
+ });
282
+ }
283
+ };
284
+
285
+ // node_modules/@ashdev/codex-plugin-sdk/dist/server.js
286
+ function validateStringFields(params, fields) {
287
+ if (params === null || params === void 0) {
288
+ return { field: "params", message: "params is required" };
289
+ }
290
+ if (typeof params !== "object") {
291
+ return { field: "params", message: "params must be an object" };
292
+ }
293
+ const obj = params;
294
+ for (const field of fields) {
295
+ const value = obj[field];
296
+ if (value === void 0 || value === null) {
297
+ return { field, message: `${field} is required` };
298
+ }
299
+ if (typeof value !== "string") {
300
+ return { field, message: `${field} must be a string` };
301
+ }
302
+ if (value.trim() === "") {
303
+ return { field, message: `${field} cannot be empty` };
304
+ }
305
+ }
306
+ return null;
307
+ }
308
+ function invalidParamsError(id, error) {
309
+ return {
310
+ jsonrpc: "2.0",
311
+ id,
312
+ error: {
313
+ code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
314
+ message: `Invalid params: ${error.message}`,
315
+ data: { field: error.field }
316
+ }
317
+ };
318
+ }
319
+ function createPluginServer(options) {
320
+ const { manifest: manifest2, onInitialize, logLevel = "info", label, router } = options;
321
+ const logger2 = createLogger({ name: manifest2.name, level: logLevel });
322
+ const prefix = label ? `${label} plugin` : "plugin";
323
+ const storage2 = new PluginStorage();
324
+ logger2.info(`Starting ${prefix}: ${manifest2.displayName} v${manifest2.version}`);
325
+ const rl = createInterface({
326
+ input: process.stdin,
327
+ terminal: false
328
+ });
329
+ rl.on("line", (line) => {
330
+ void handleLine(line, manifest2, onInitialize, router, logger2, storage2);
331
+ });
332
+ rl.on("close", () => {
333
+ logger2.info("stdin closed, shutting down");
334
+ storage2.cancelAll();
335
+ process.exit(0);
336
+ });
337
+ process.on("uncaughtException", (error) => {
338
+ logger2.error("Uncaught exception", error);
339
+ process.exit(1);
340
+ });
341
+ process.on("unhandledRejection", (reason) => {
342
+ logger2.error("Unhandled rejection", reason);
343
+ });
344
+ }
345
+ function isJsonRpcResponse(obj) {
346
+ if (obj.method !== void 0)
347
+ return false;
348
+ if (obj.id === void 0 || obj.id === null)
349
+ return false;
350
+ return "result" in obj || "error" in obj;
351
+ }
352
+ async function handleLine(line, manifest2, onInitialize, router, logger2, storage2) {
353
+ const trimmed = line.trim();
354
+ if (!trimmed)
355
+ return;
356
+ let parsed;
357
+ try {
358
+ parsed = JSON.parse(trimmed);
359
+ } catch {
360
+ }
361
+ if (parsed && isJsonRpcResponse(parsed)) {
362
+ logger2.debug("Routing storage response", { id: parsed.id });
363
+ storage2.handleResponse(trimmed);
364
+ return;
365
+ }
366
+ let id = null;
367
+ try {
368
+ const request = parsed ?? JSON.parse(trimmed);
369
+ id = request.id;
370
+ logger2.debug(`Received request: ${request.method}`, { id: request.id });
371
+ const response = await handleRequest(request, manifest2, onInitialize, router, logger2, storage2);
372
+ if (response !== null) {
373
+ writeResponse(response);
374
+ }
375
+ } catch (error) {
376
+ if (error instanceof SyntaxError) {
377
+ writeResponse({
378
+ jsonrpc: "2.0",
379
+ id: null,
380
+ error: {
381
+ code: JSON_RPC_ERROR_CODES.PARSE_ERROR,
382
+ message: "Parse error: invalid JSON"
383
+ }
384
+ });
385
+ } else if (error instanceof PluginError) {
386
+ writeResponse({
387
+ jsonrpc: "2.0",
388
+ id,
389
+ error: error.toJsonRpcError()
390
+ });
391
+ } else {
392
+ const message = error instanceof Error ? error.message : "Unknown error";
393
+ logger2.error("Request failed", error);
394
+ writeResponse({
395
+ jsonrpc: "2.0",
396
+ id,
397
+ error: {
398
+ code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
399
+ message
400
+ }
401
+ });
402
+ }
403
+ }
404
+ }
405
+ async function handleRequest(request, manifest2, onInitialize, router, logger2, storage2) {
406
+ const { method, params, id } = request;
407
+ switch (method) {
408
+ case "initialize": {
409
+ const initParams = params ?? {};
410
+ initParams.storage = storage2;
411
+ if (onInitialize) {
412
+ await onInitialize(initParams);
413
+ }
414
+ return { jsonrpc: "2.0", id, result: manifest2 };
415
+ }
416
+ case "ping":
417
+ return { jsonrpc: "2.0", id, result: "pong" };
418
+ case "shutdown": {
419
+ logger2.info("Shutdown requested");
420
+ storage2.cancelAll();
421
+ const response2 = { jsonrpc: "2.0", id, result: null };
422
+ process.stdout.write(`${JSON.stringify(response2)}
423
+ `, () => {
424
+ process.exit(0);
425
+ });
426
+ return null;
427
+ }
428
+ }
429
+ const response = await router(method, params, id);
430
+ if (response !== null) {
431
+ return response;
432
+ }
433
+ return {
434
+ jsonrpc: "2.0",
435
+ id,
436
+ error: {
437
+ code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
438
+ message: `Method not found: ${method}`
439
+ }
440
+ };
441
+ }
442
+ function writeResponse(response) {
443
+ process.stdout.write(`${JSON.stringify(response)}
444
+ `);
445
+ }
446
+ function methodNotFound(id, message) {
447
+ return {
448
+ jsonrpc: "2.0",
449
+ id,
450
+ error: {
451
+ code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
452
+ message
453
+ }
454
+ };
455
+ }
456
+ function success(id, result) {
457
+ return { jsonrpc: "2.0", id, result };
458
+ }
459
+ function createRecommendationPlugin(options) {
460
+ const { manifest: manifest2, provider: provider2, onInitialize, logLevel } = options;
461
+ const router = async (method, params, id) => {
462
+ switch (method) {
463
+ case "recommendations/get":
464
+ return success(id, await provider2.get(params));
465
+ case "recommendations/updateProfile": {
466
+ if (!provider2.updateProfile)
467
+ return methodNotFound(id, "This plugin does not support recommendations/updateProfile");
468
+ return success(id, await provider2.updateProfile(params));
469
+ }
470
+ case "recommendations/clear": {
471
+ if (!provider2.clear)
472
+ return methodNotFound(id, "This plugin does not support recommendations/clear");
473
+ return success(id, await provider2.clear());
474
+ }
475
+ case "recommendations/dismiss": {
476
+ if (!provider2.dismiss)
477
+ return methodNotFound(id, "This plugin does not support recommendations/dismiss");
478
+ const err = validateStringFields(params, ["externalId"]);
479
+ if (err)
480
+ return invalidParamsError(id, err);
481
+ return success(id, await provider2.dismiss(params));
482
+ }
483
+ default:
484
+ return null;
485
+ }
486
+ };
487
+ createPluginServer({ manifest: manifest2, onInitialize, logLevel, label: "recommendation", router });
488
+ }
489
+
490
+ // node_modules/@ashdev/codex-plugin-sdk/dist/types/manifest.js
491
+ var EXTERNAL_ID_SOURCE_ANILIST = "api:anilist";
492
+
493
+ // src/anilist.ts
494
+ var ANILIST_API_URL = "https://graphql.anilist.co";
495
+ var VIEWER_QUERY = `
496
+ query {
497
+ Viewer {
498
+ id
499
+ name
500
+ }
501
+ }
502
+ `;
503
+ var MEDIA_RECOMMENDATIONS_QUERY = `
504
+ query ($mediaId: Int!, $page: Int, $perPage: Int) {
505
+ Media(id: $mediaId, type: MANGA) {
506
+ id
507
+ title {
508
+ romaji
509
+ english
510
+ }
511
+ recommendations(page: $page, perPage: $perPage, sort: RATING_DESC) {
512
+ pageInfo {
513
+ hasNextPage
514
+ }
515
+ nodes {
516
+ rating
517
+ mediaRecommendation {
518
+ id
519
+ title {
520
+ romaji
521
+ english
522
+ }
523
+ coverImage {
524
+ large
525
+ }
526
+ description(asHtml: false)
527
+ genres
528
+ averageScore
529
+ siteUrl
530
+ }
531
+ }
532
+ }
533
+ }
534
+ }
535
+ `;
536
+ var SEARCH_MANGA_QUERY = `
537
+ query ($search: String!) {
538
+ Media(search: $search, type: MANGA) {
539
+ id
540
+ title {
541
+ romaji
542
+ english
543
+ }
544
+ }
545
+ }
546
+ `;
547
+ var USER_MANGA_IDS_QUERY = `
548
+ query ($userId: Int!, $page: Int, $perPage: Int) {
549
+ Page(page: $page, perPage: $perPage) {
550
+ pageInfo {
551
+ hasNextPage
552
+ currentPage
553
+ }
554
+ mediaList(userId: $userId, type: MANGA) {
555
+ mediaId
556
+ }
557
+ }
558
+ }
559
+ `;
560
+ var AniListRecommendationClient = class {
561
+ accessToken;
562
+ constructor(accessToken) {
563
+ this.accessToken = accessToken;
564
+ }
565
+ async query(queryStr, variables) {
566
+ return this.executeQuery(queryStr, variables, true);
567
+ }
568
+ async executeQuery(queryStr, variables, allowRetry) {
569
+ let response;
570
+ try {
571
+ response = await fetch(ANILIST_API_URL, {
572
+ method: "POST",
573
+ signal: AbortSignal.timeout(3e4),
574
+ headers: {
575
+ "Content-Type": "application/json",
576
+ Accept: "application/json",
577
+ Authorization: `Bearer ${this.accessToken}`
578
+ },
579
+ body: JSON.stringify({ query: queryStr, variables })
580
+ });
581
+ } catch (error) {
582
+ if (error instanceof DOMException && error.name === "TimeoutError") {
583
+ throw new ApiError("AniList API request timed out after 30 seconds");
584
+ }
585
+ throw error;
586
+ }
587
+ if (response.status === 401) {
588
+ throw new AuthError("AniList access token is invalid or expired");
589
+ }
590
+ if (response.status === 429) {
591
+ const retryAfter = response.headers.get("Retry-After");
592
+ const retrySeconds = retryAfter ? Number.parseInt(retryAfter, 10) : 60;
593
+ const waitSeconds = Number.isNaN(retrySeconds) ? 60 : retrySeconds;
594
+ if (allowRetry) {
595
+ await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1e3));
596
+ return this.executeQuery(queryStr, variables, false);
597
+ }
598
+ throw new RateLimitError(waitSeconds, "AniList rate limit exceeded");
599
+ }
600
+ if (!response.ok) {
601
+ const body = await response.text().catch(() => "");
602
+ throw new ApiError(
603
+ `AniList API error: ${response.status} ${response.statusText}${body ? ` - ${body}` : ""}`
604
+ );
605
+ }
606
+ const json = await response.json();
607
+ if (json.errors?.length) {
608
+ const message = json.errors.map((e) => e.message).join("; ");
609
+ throw new ApiError(`AniList GraphQL error: ${message}`);
610
+ }
611
+ if (!json.data) {
612
+ throw new ApiError("AniList returned empty data");
613
+ }
614
+ return json.data;
615
+ }
616
+ /** Get the authenticated viewer's ID */
617
+ async getViewerId() {
618
+ const data = await this.query(VIEWER_QUERY);
619
+ return data.Viewer.id;
620
+ }
621
+ /** Search for a manga by title and return its AniList ID */
622
+ async searchManga(title) {
623
+ try {
624
+ const data = await this.query(SEARCH_MANGA_QUERY, {
625
+ search: title
626
+ });
627
+ return data.Media;
628
+ } catch {
629
+ return null;
630
+ }
631
+ }
632
+ /** Get community recommendations for a specific manga (up to maxPages pages) */
633
+ async getRecommendationsForMedia(mediaId, perPage = 10, maxPages = 3) {
634
+ const allNodes = [];
635
+ let page = 1;
636
+ let hasMore = true;
637
+ while (hasMore && page <= maxPages) {
638
+ const data = await this.query(MEDIA_RECOMMENDATIONS_QUERY, { mediaId, page, perPage });
639
+ allNodes.push(...data.Media.recommendations.nodes);
640
+ hasMore = data.Media.recommendations.pageInfo.hasNextPage;
641
+ page++;
642
+ }
643
+ return allNodes;
644
+ }
645
+ /** Get all manga IDs in the user's list (for deduplication) */
646
+ async getUserMangaIds(userId) {
647
+ const ids = /* @__PURE__ */ new Set();
648
+ let page = 1;
649
+ let hasMore = true;
650
+ while (hasMore) {
651
+ const data = await this.query(USER_MANGA_IDS_QUERY, { userId, page, perPage: 50 });
652
+ for (const entry of data.Page.mediaList) {
653
+ ids.add(entry.mediaId);
654
+ }
655
+ hasMore = data.Page.pageInfo.hasNextPage;
656
+ page++;
657
+ }
658
+ return ids;
659
+ }
660
+ };
661
+ function getBestTitle(title) {
662
+ return title.english || title.romaji || "Unknown";
663
+ }
664
+ var HTML_ENTITIES = {
665
+ "&amp;": "&",
666
+ "&lt;": "<",
667
+ "&gt;": ">",
668
+ "&quot;": '"',
669
+ "&#39;": "'",
670
+ "&apos;": "'",
671
+ "&nbsp;": " ",
672
+ "&mdash;": "\u2014",
673
+ "&ndash;": "\u2013",
674
+ "&hellip;": "\u2026"
675
+ };
676
+ var ENTITY_PATTERN = /&(?:#(\d+)|#x([0-9a-fA-F]+)|[a-zA-Z]+);/g;
677
+ function stripHtml(html) {
678
+ if (!html) return void 0;
679
+ return html.replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]*>/g, "").replace(ENTITY_PATTERN, (match, decimal, hex) => {
680
+ if (decimal) return String.fromCharCode(Number.parseInt(decimal, 10));
681
+ if (hex) return String.fromCharCode(Number.parseInt(hex, 16));
682
+ return HTML_ENTITIES[match] ?? match;
683
+ }).trim();
684
+ }
685
+
686
+ // package.json
687
+ var package_default = {
688
+ name: "@ashdev/codex-plugin-recommendations-anilist",
689
+ version: "1.10.0",
690
+ description: "AniList recommendation provider plugin for Codex - generates personalized manga recommendations based on your reading history",
691
+ main: "dist/index.js",
692
+ bin: "dist/index.js",
693
+ type: "module",
694
+ files: [
695
+ "dist",
696
+ "README.md"
697
+ ],
698
+ repository: {
699
+ type: "git",
700
+ url: "https://github.com/AshDevFr/codex.git",
701
+ directory: "plugins/recommendations-anilist"
702
+ },
703
+ scripts: {
704
+ build: "esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --sourcemap --banner:js='#!/usr/bin/env node'",
705
+ dev: "npm run build -- --watch",
706
+ clean: "rm -rf dist",
707
+ start: "node dist/index.js",
708
+ lint: "biome check .",
709
+ "lint:fix": "biome check --write .",
710
+ typecheck: "tsc --noEmit",
711
+ test: "vitest run --passWithNoTests",
712
+ "test:watch": "vitest",
713
+ prepublishOnly: "npm run lint && npm run build"
714
+ },
715
+ keywords: [
716
+ "codex",
717
+ "plugin",
718
+ "anilist",
719
+ "recommendations",
720
+ "manga"
721
+ ],
722
+ author: "Codex",
723
+ license: "MIT",
724
+ engines: {
725
+ node: ">=22.0.0"
726
+ },
727
+ dependencies: {
728
+ "@ashdev/codex-plugin-sdk": "^1.10.0"
729
+ },
730
+ devDependencies: {
731
+ "@biomejs/biome": "^2.3.13",
732
+ "@types/node": "^22.0.0",
733
+ esbuild: "^0.24.0",
734
+ typescript: "^5.7.0",
735
+ vitest: "^3.0.0"
736
+ }
737
+ };
738
+
739
+ // src/manifest.ts
740
+ var manifest = {
741
+ name: "recommendations-anilist",
742
+ displayName: "AniList Recommendations",
743
+ version: package_default.version,
744
+ description: "Personalized manga recommendations from AniList based on your reading history and ratings.",
745
+ author: "Codex",
746
+ homepage: "https://github.com/AshDevFr/codex",
747
+ protocolVersion: "1.0",
748
+ capabilities: {
749
+ userRecommendationProvider: true
750
+ },
751
+ requiredCredentials: [
752
+ {
753
+ key: "access_token",
754
+ label: "AniList Access Token",
755
+ description: "OAuth access token for AniList API",
756
+ type: "password",
757
+ required: true,
758
+ sensitive: true
759
+ }
760
+ ],
761
+ configSchema: {
762
+ description: "Recommendation configuration",
763
+ fields: [
764
+ {
765
+ key: "maxRecommendations",
766
+ label: "Maximum Recommendations",
767
+ description: "Maximum number of recommendations to generate (1-50)",
768
+ type: "number",
769
+ required: false,
770
+ default: 20
771
+ },
772
+ {
773
+ key: "maxSeeds",
774
+ label: "Maximum Seed Titles",
775
+ description: "Number of top-rated library titles used to generate recommendations (1-25)",
776
+ type: "number",
777
+ required: false,
778
+ default: 10
779
+ }
780
+ ]
781
+ },
782
+ userConfigSchema: {
783
+ description: "Per-user recommendation settings",
784
+ fields: [
785
+ {
786
+ key: "searchFallback",
787
+ label: "Search Fallback",
788
+ description: "When a series has no AniList ID, search by title to find a match. Disable for strict matching only.",
789
+ type: "boolean",
790
+ required: false,
791
+ default: true
792
+ }
793
+ ]
794
+ },
795
+ oauth: {
796
+ authorizationUrl: "https://anilist.co/api/v2/oauth/authorize",
797
+ tokenUrl: "https://anilist.co/api/v2/oauth/token",
798
+ scopes: [],
799
+ pkce: false
800
+ },
801
+ userDescription: "Personalized manga recommendations powered by AniList community data",
802
+ adminSetupInstructions: "To enable OAuth login, create an AniList API client at https://anilist.co/settings/developer. Set the redirect URL to {your-codex-url}/api/v1/user/plugins/oauth/callback. Enter the Client ID below. Without OAuth configured, users can still connect by pasting a personal access token.",
803
+ userSetupInstructions: "Connect your AniList account via OAuth, or paste a personal access token. To generate a token, visit https://anilist.co/settings/developer, create a client with redirect URL https://anilist.co/api/v2/oauth/pin, then authorize it to receive your token."
804
+ };
805
+
806
+ // src/index.ts
807
+ var logger = createLogger({ name: "recommendations-anilist", level: "debug" });
808
+ var client = null;
809
+ var viewerId = null;
810
+ var maxRecommendations = 20;
811
+ var maxSeeds = 10;
812
+ var searchFallback = true;
813
+ var storage = null;
814
+ function setClient(c) {
815
+ client = c;
816
+ }
817
+ function setSearchFallback(enabled) {
818
+ searchFallback = enabled;
819
+ }
820
+ var DISMISSED_STORAGE_KEY = "dismissed_ids";
821
+ var dismissedIds = /* @__PURE__ */ new Set();
822
+ async function loadDismissedIds() {
823
+ if (!storage) return;
824
+ try {
825
+ const result = await storage.get(DISMISSED_STORAGE_KEY);
826
+ if (Array.isArray(result.data)) {
827
+ dismissedIds.clear();
828
+ for (const id of result.data) {
829
+ if (typeof id === "string") {
830
+ dismissedIds.add(id);
831
+ }
832
+ }
833
+ logger.debug(`Loaded ${dismissedIds.size} dismissed IDs from storage`);
834
+ }
835
+ } catch (err) {
836
+ const msg = err instanceof Error ? err.message : "Unknown error";
837
+ logger.warn(`Failed to load dismissed IDs from storage: ${msg}`);
838
+ }
839
+ }
840
+ async function saveDismissedIds() {
841
+ if (!storage) return;
842
+ try {
843
+ await storage.set(DISMISSED_STORAGE_KEY, [...dismissedIds]);
844
+ } catch (err) {
845
+ const msg = err instanceof Error ? err.message : "Unknown error";
846
+ logger.warn(`Failed to save dismissed IDs to storage: ${msg}`);
847
+ }
848
+ }
849
+ async function resolveAniListIds(entries) {
850
+ if (!client) throw new Error("Plugin not initialized");
851
+ const resolved = /* @__PURE__ */ new Map();
852
+ for (const entry of entries) {
853
+ const anilistExt = entry.externalIds?.find(
854
+ (e) => e.source === EXTERNAL_ID_SOURCE_ANILIST || e.source === "anilist" || e.source === "AniList"
855
+ );
856
+ if (anilistExt) {
857
+ const id = Number.parseInt(anilistExt.externalId, 10);
858
+ if (!Number.isNaN(id)) {
859
+ resolved.set(entry.seriesId, {
860
+ anilistId: id,
861
+ title: entry.title,
862
+ rating: entry.userRating ?? 0
863
+ });
864
+ continue;
865
+ }
866
+ }
867
+ if (searchFallback) {
868
+ const result = await client.searchManga(entry.title);
869
+ if (result) {
870
+ resolved.set(entry.seriesId, {
871
+ anilistId: result.id,
872
+ title: entry.title,
873
+ rating: entry.userRating ?? 0
874
+ });
875
+ }
876
+ }
877
+ }
878
+ return resolved;
879
+ }
880
+ function pickSeedEntries(entries, maxSeeds2) {
881
+ const sorted = [...entries].sort((a, b) => {
882
+ const ratingDiff = (b.userRating ?? 0) - (a.userRating ?? 0);
883
+ if (ratingDiff !== 0) return ratingDiff;
884
+ return b.booksRead - a.booksRead;
885
+ });
886
+ return sorted.slice(0, maxSeeds2);
887
+ }
888
+ function convertRecommendations(nodes, basedOnTitle, userMangaIds, excludeIds) {
889
+ const results = [];
890
+ for (const node of nodes) {
891
+ if (!node.mediaRecommendation) continue;
892
+ const media = node.mediaRecommendation;
893
+ const externalId = String(media.id);
894
+ if (excludeIds.has(externalId) || dismissedIds.has(externalId)) continue;
895
+ const inLibrary = userMangaIds.has(media.id);
896
+ const communityScore = Math.max(0, Math.min(node.rating, 100)) / 100;
897
+ const avgScore = media.averageScore ? media.averageScore / 100 : 0.5;
898
+ const score = Math.round((communityScore * 0.6 + avgScore * 0.4) * 100) / 100;
899
+ results.push({
900
+ externalId,
901
+ externalUrl: media.siteUrl,
902
+ title: getBestTitle(media.title),
903
+ coverUrl: media.coverImage.large ?? void 0,
904
+ summary: stripHtml(media.description),
905
+ genres: media.genres ?? [],
906
+ score: Math.max(0, Math.min(score, 1)),
907
+ reason: `Recommended because you liked ${basedOnTitle}`,
908
+ basedOn: [basedOnTitle],
909
+ inLibrary
910
+ });
911
+ }
912
+ return results;
913
+ }
914
+ var provider = {
915
+ async get(params) {
916
+ if (!client) {
917
+ throw new Error("Plugin not initialized - no AniList client");
918
+ }
919
+ if (viewerId === null) {
920
+ viewerId = await client.getViewerId();
921
+ logger.info(`Authenticated as viewer ${viewerId}`);
922
+ }
923
+ const { library, limit, excludeIds: rawExcludeIds = [] } = params;
924
+ const effectiveLimit = Math.min(limit ?? maxRecommendations, 50);
925
+ const excludeIds = new Set(rawExcludeIds);
926
+ if (!library || library.length === 0) {
927
+ logger.info("Empty library \u2014 returning no recommendations");
928
+ return { recommendations: [], generatedAt: (/* @__PURE__ */ new Date()).toISOString(), cached: false };
929
+ }
930
+ const userMangaIds = await client.getUserMangaIds(viewerId);
931
+ logger.debug(`User has ${userMangaIds.size} manga in AniList list`);
932
+ const seeds = pickSeedEntries(library, maxSeeds);
933
+ logger.debug(`Using ${seeds.length} seed entries from library of ${library.length}`);
934
+ const resolved = await resolveAniListIds(seeds);
935
+ logger.debug(`Resolved ${resolved.size} AniList IDs from ${seeds.length} seeds`);
936
+ const allRecs = /* @__PURE__ */ new Map();
937
+ for (const [, { anilistId, title }] of resolved) {
938
+ try {
939
+ const nodes = await client.getRecommendationsForMedia(anilistId, 10);
940
+ const recs = convertRecommendations(nodes, title, userMangaIds, excludeIds);
941
+ for (const rec of recs) {
942
+ const existing = allRecs.get(rec.externalId);
943
+ if (existing) {
944
+ const mergedBasedOn = [.../* @__PURE__ */ new Set([...existing.basedOn, ...rec.basedOn])];
945
+ const boostedScore = Math.min(existing.score + 0.05, 1);
946
+ allRecs.set(rec.externalId, {
947
+ ...existing,
948
+ score: Math.round(boostedScore * 100) / 100,
949
+ basedOn: mergedBasedOn,
950
+ reason: mergedBasedOn.length > 1 ? `Recommended based on ${mergedBasedOn.join(", ")}` : existing.reason
951
+ });
952
+ } else {
953
+ allRecs.set(rec.externalId, rec);
954
+ }
955
+ }
956
+ } catch (error) {
957
+ const msg = error instanceof Error ? error.message : "Unknown error";
958
+ logger.warn(`Failed to get recommendations for AniList ID ${anilistId}: ${msg}`);
959
+ }
960
+ }
961
+ const sorted = [...allRecs.values()].sort((a, b) => b.score - a.score).slice(0, effectiveLimit);
962
+ logger.info(`Generated ${sorted.length} recommendations from ${resolved.size} seed titles`);
963
+ return {
964
+ recommendations: sorted,
965
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
966
+ cached: false
967
+ };
968
+ },
969
+ async dismiss(params) {
970
+ dismissedIds.add(params.externalId);
971
+ logger.debug(
972
+ `Dismissed recommendation: ${params.externalId} (reason: ${params.reason ?? "none"})`
973
+ );
974
+ await saveDismissedIds();
975
+ return { dismissed: true };
976
+ },
977
+ async clear() {
978
+ const count = dismissedIds.size;
979
+ dismissedIds.clear();
980
+ logger.info(`Cleared ${count} dismissed recommendations`);
981
+ await saveDismissedIds();
982
+ return { cleared: true };
983
+ }
984
+ };
985
+ createRecommendationPlugin({
986
+ manifest,
987
+ provider,
988
+ logLevel: "debug",
989
+ async onInitialize(params) {
990
+ const accessToken = params.credentials?.access_token;
991
+ if (accessToken) {
992
+ client = new AniListRecommendationClient(accessToken);
993
+ logger.info("AniList client initialized with access token");
994
+ } else {
995
+ logger.warn("No access token provided - recommendation operations will fail");
996
+ }
997
+ const rawMax = params.adminConfig?.maxRecommendations;
998
+ if (typeof rawMax === "number") {
999
+ maxRecommendations = Math.max(1, Math.min(Math.round(rawMax), 50));
1000
+ logger.info(`Max recommendations set to: ${maxRecommendations}`);
1001
+ }
1002
+ const rawSeeds = params.adminConfig?.maxSeeds;
1003
+ if (typeof rawSeeds === "number") {
1004
+ maxSeeds = Math.max(1, Math.min(Math.round(rawSeeds), 25));
1005
+ logger.info(`Max seeds set to: ${maxSeeds}`);
1006
+ }
1007
+ const uc = params.userConfig;
1008
+ if (uc && typeof uc.searchFallback === "boolean") {
1009
+ searchFallback = uc.searchFallback;
1010
+ logger.info(`Search fallback set to: ${searchFallback}`);
1011
+ }
1012
+ storage = params.storage;
1013
+ await loadDismissedIds();
1014
+ }
1015
+ });
1016
+ logger.info("AniList recommendations plugin started");
1017
+ export {
1018
+ convertRecommendations,
1019
+ dismissedIds,
1020
+ pickSeedEntries,
1021
+ resolveAniListIds,
1022
+ setClient,
1023
+ setSearchFallback
1024
+ };
1025
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../node_modules/@ashdev/codex-plugin-sdk/src/types/rpc.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/errors.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/logger.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/server.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/storage.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/types/manifest.ts", "../src/anilist.ts", "../package.json", "../src/manifest.ts", "../src/index.ts"],
4
+ "sourcesContent": [null, null, null, null, null, null, "/**\n * AniList GraphQL API client for recommendations\n *\n * Uses AniList's recommendations and user list data to generate\n * personalized manga suggestions.\n */\n\nimport { ApiError, AuthError, RateLimitError } from \"@ashdev/codex-plugin-sdk\";\n\nconst ANILIST_API_URL = \"https://graphql.anilist.co\";\n\n// =============================================================================\n// GraphQL Queries\n// =============================================================================\n\nconst VIEWER_QUERY = `\n query {\n Viewer {\n id\n name\n }\n }\n`;\n\n/** Get recommendations for a specific manga */\nconst MEDIA_RECOMMENDATIONS_QUERY = `\n query ($mediaId: Int!, $page: Int, $perPage: Int) {\n Media(id: $mediaId, type: MANGA) {\n id\n title {\n romaji\n english\n }\n recommendations(page: $page, perPage: $perPage, sort: RATING_DESC) {\n pageInfo {\n hasNextPage\n }\n nodes {\n rating\n mediaRecommendation {\n id\n title {\n romaji\n english\n }\n coverImage {\n large\n }\n description(asHtml: false)\n genres\n averageScore\n siteUrl\n }\n }\n }\n }\n }\n`;\n\n/** Search for a manga by title to find its AniList ID */\nconst SEARCH_MANGA_QUERY = `\n query ($search: String!) {\n Media(search: $search, type: MANGA) {\n id\n title {\n romaji\n english\n }\n }\n }\n`;\n\n/** Get the user's manga list to know what they've already seen */\nconst USER_MANGA_IDS_QUERY = `\n query ($userId: Int!, $page: Int, $perPage: Int) {\n Page(page: $page, perPage: $perPage) {\n pageInfo {\n hasNextPage\n currentPage\n }\n mediaList(userId: $userId, type: MANGA) {\n mediaId\n }\n }\n }\n`;\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface AniListRecommendationNode {\n rating: number;\n mediaRecommendation: {\n id: number;\n title: { romaji?: string; english?: string };\n coverImage: { large?: string };\n description: string | null;\n genres: string[];\n averageScore: number | null;\n siteUrl: string;\n } | null;\n}\n\ninterface SearchResult {\n id: number;\n title: { romaji?: string; english?: string };\n}\n\n// =============================================================================\n// Client\n// =============================================================================\n\nexport class AniListRecommendationClient {\n private accessToken: string;\n\n constructor(accessToken: string) {\n this.accessToken = accessToken;\n }\n\n private async query<T>(queryStr: string, variables?: Record<string, unknown>): Promise<T> {\n return this.executeQuery<T>(queryStr, variables, true);\n }\n\n private async executeQuery<T>(\n queryStr: string,\n variables: Record<string, unknown> | undefined,\n allowRetry: boolean,\n ): Promise<T> {\n let response: Response;\n try {\n response = await fetch(ANILIST_API_URL, {\n method: \"POST\",\n signal: AbortSignal.timeout(30_000),\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n Authorization: `Bearer ${this.accessToken}`,\n },\n body: JSON.stringify({ query: queryStr, variables }),\n });\n } catch (error) {\n if (error instanceof DOMException && error.name === \"TimeoutError\") {\n throw new ApiError(\"AniList API request timed out after 30 seconds\");\n }\n throw error;\n }\n\n if (response.status === 401) {\n throw new AuthError(\"AniList access token is invalid or expired\");\n }\n\n if (response.status === 429) {\n const retryAfter = response.headers.get(\"Retry-After\");\n const retrySeconds = retryAfter ? Number.parseInt(retryAfter, 10) : 60;\n const waitSeconds = Number.isNaN(retrySeconds) ? 60 : retrySeconds;\n\n if (allowRetry) {\n await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000));\n return this.executeQuery<T>(queryStr, variables, false);\n }\n\n throw new RateLimitError(waitSeconds, \"AniList rate limit exceeded\");\n }\n\n if (!response.ok) {\n const body = await response.text().catch(() => \"\");\n throw new ApiError(\n `AniList API error: ${response.status} ${response.statusText}${body ? ` - ${body}` : \"\"}`,\n );\n }\n\n const json = (await response.json()) as {\n data?: T;\n errors?: Array<{ message: string }>;\n };\n\n if (json.errors?.length) {\n const message = json.errors.map((e) => e.message).join(\"; \");\n throw new ApiError(`AniList GraphQL error: ${message}`);\n }\n\n if (!json.data) {\n throw new ApiError(\"AniList returned empty data\");\n }\n\n return json.data;\n }\n\n /** Get the authenticated viewer's ID */\n async getViewerId(): Promise<number> {\n const data = await this.query<{ Viewer: { id: number; name: string } }>(VIEWER_QUERY);\n return data.Viewer.id;\n }\n\n /** Search for a manga by title and return its AniList ID */\n async searchManga(title: string): Promise<SearchResult | null> {\n try {\n const data = await this.query<{ Media: SearchResult | null }>(SEARCH_MANGA_QUERY, {\n search: title,\n });\n return data.Media;\n } catch {\n return null;\n }\n }\n\n /** Get community recommendations for a specific manga (up to maxPages pages) */\n async getRecommendationsForMedia(\n mediaId: number,\n perPage = 10,\n maxPages = 3,\n ): Promise<AniListRecommendationNode[]> {\n const allNodes: AniListRecommendationNode[] = [];\n let page = 1;\n let hasMore = true;\n\n while (hasMore && page <= maxPages) {\n const data = await this.query<{\n Media: {\n id: number;\n title: { romaji?: string; english?: string };\n recommendations: {\n pageInfo: { hasNextPage: boolean };\n nodes: AniListRecommendationNode[];\n };\n };\n }>(MEDIA_RECOMMENDATIONS_QUERY, { mediaId, page, perPage });\n\n allNodes.push(...data.Media.recommendations.nodes);\n hasMore = data.Media.recommendations.pageInfo.hasNextPage;\n page++;\n }\n\n return allNodes;\n }\n\n /** Get all manga IDs in the user's list (for deduplication) */\n async getUserMangaIds(userId: number): Promise<Set<number>> {\n const ids = new Set<number>();\n let page = 1;\n let hasMore = true;\n\n while (hasMore) {\n const data = await this.query<{\n Page: {\n pageInfo: { hasNextPage: boolean; currentPage: number };\n mediaList: Array<{ mediaId: number }>;\n };\n }>(USER_MANGA_IDS_QUERY, { userId, page, perPage: 50 });\n\n for (const entry of data.Page.mediaList) {\n ids.add(entry.mediaId);\n }\n\n hasMore = data.Page.pageInfo.hasNextPage;\n page++;\n }\n\n return ids;\n }\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\n/** Get the best title from an AniList title object */\nexport function getBestTitle(title: { romaji?: string; english?: string }): string {\n return title.english || title.romaji || \"Unknown\";\n}\n\n/** Common HTML entities to decode */\nconst HTML_ENTITIES: Record<string, string> = {\n \"&amp;\": \"&\",\n \"&lt;\": \"<\",\n \"&gt;\": \">\",\n \"&quot;\": '\"',\n \"&#39;\": \"'\",\n \"&apos;\": \"'\",\n \"&nbsp;\": \" \",\n \"&mdash;\": \"\\u2014\",\n \"&ndash;\": \"\\u2013\",\n \"&hellip;\": \"\\u2026\",\n};\n\nconst ENTITY_PATTERN = /&(?:#(\\d+)|#x([0-9a-fA-F]+)|[a-zA-Z]+);/g;\n\n/** Strip HTML tags and decode HTML entities */\nexport function stripHtml(html: string | null): string | undefined {\n if (!html) return undefined;\n return html\n .replace(/<br\\s*\\/?>/gi, \"\\n\")\n .replace(/<[^>]*>/g, \"\")\n .replace(ENTITY_PATTERN, (match, decimal, hex) => {\n if (decimal) return String.fromCharCode(Number.parseInt(decimal, 10));\n if (hex) return String.fromCharCode(Number.parseInt(hex, 16));\n return HTML_ENTITIES[match] ?? match;\n })\n .trim();\n}\n", "{\n \"name\": \"@ashdev/codex-plugin-recommendations-anilist\",\n \"version\": \"1.10.0\",\n \"description\": \"AniList recommendation provider plugin for Codex - generates personalized manga recommendations based on your reading history\",\n \"main\": \"dist/index.js\",\n \"bin\": \"dist/index.js\",\n \"type\": \"module\",\n \"files\": [\n \"dist\",\n \"README.md\"\n ],\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/AshDevFr/codex.git\",\n \"directory\": \"plugins/recommendations-anilist\"\n },\n \"scripts\": {\n \"build\": \"esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --sourcemap --banner:js='#!/usr/bin/env node'\",\n \"dev\": \"npm run build -- --watch\",\n \"clean\": \"rm -rf dist\",\n \"start\": \"node dist/index.js\",\n \"lint\": \"biome check .\",\n \"lint:fix\": \"biome check --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run --passWithNoTests\",\n \"test:watch\": \"vitest\",\n \"prepublishOnly\": \"npm run lint && npm run build\"\n },\n \"keywords\": [\n \"codex\",\n \"plugin\",\n \"anilist\",\n \"recommendations\",\n \"manga\"\n ],\n \"author\": \"Codex\",\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=22.0.0\"\n },\n \"dependencies\": {\n \"@ashdev/codex-plugin-sdk\": \"^1.10.0\"\n },\n \"devDependencies\": {\n \"@biomejs/biome\": \"^2.3.13\",\n \"@types/node\": \"^22.0.0\",\n \"esbuild\": \"^0.24.0\",\n \"typescript\": \"^5.7.0\",\n \"vitest\": \"^3.0.0\"\n }\n}\n", "import type { PluginManifest } from \"@ashdev/codex-plugin-sdk\";\nimport packageJson from \"../package.json\" with { type: \"json\" };\n\nexport const manifest = {\n name: \"recommendations-anilist\",\n displayName: \"AniList Recommendations\",\n version: packageJson.version,\n description:\n \"Personalized manga recommendations from AniList based on your reading history and ratings.\",\n author: \"Codex\",\n homepage: \"https://github.com/AshDevFr/codex\",\n protocolVersion: \"1.0\",\n capabilities: {\n userRecommendationProvider: true,\n },\n requiredCredentials: [\n {\n key: \"access_token\",\n label: \"AniList Access Token\",\n description: \"OAuth access token for AniList API\",\n type: \"password\" as const,\n required: true,\n sensitive: true,\n },\n ],\n configSchema: {\n description: \"Recommendation configuration\",\n fields: [\n {\n key: \"maxRecommendations\",\n label: \"Maximum Recommendations\",\n description: \"Maximum number of recommendations to generate (1-50)\",\n type: \"number\" as const,\n required: false,\n default: 20,\n },\n {\n key: \"maxSeeds\",\n label: \"Maximum Seed Titles\",\n description: \"Number of top-rated library titles used to generate recommendations (1-25)\",\n type: \"number\" as const,\n required: false,\n default: 10,\n },\n ],\n },\n userConfigSchema: {\n description: \"Per-user recommendation settings\",\n fields: [\n {\n key: \"searchFallback\",\n label: \"Search Fallback\",\n description:\n \"When a series has no AniList ID, search by title to find a match. Disable for strict matching only.\",\n type: \"boolean\" as const,\n required: false,\n default: true,\n },\n ],\n },\n oauth: {\n authorizationUrl: \"https://anilist.co/api/v2/oauth/authorize\",\n tokenUrl: \"https://anilist.co/api/v2/oauth/token\",\n scopes: [],\n pkce: false,\n },\n userDescription: \"Personalized manga recommendations powered by AniList community data\",\n adminSetupInstructions:\n \"To enable OAuth login, create an AniList API client at https://anilist.co/settings/developer. Set the redirect URL to {your-codex-url}/api/v1/user/plugins/oauth/callback. Enter the Client ID below. Without OAuth configured, users can still connect by pasting a personal access token.\",\n userSetupInstructions:\n \"Connect your AniList account via OAuth, or paste a personal access token. To generate a token, visit https://anilist.co/settings/developer, create a client with redirect URL https://anilist.co/api/v2/oauth/pin, then authorize it to receive your token.\",\n} as const satisfies PluginManifest & {\n capabilities: { userRecommendationProvider: true };\n};\n", "/**\n * AniList Recommendations Plugin for Codex\n *\n * Generates personalized manga recommendations by:\n * 1. Matching user's library entries to AniList manga IDs\n * 2. Fetching community recommendations for highly-rated titles\n * 3. Scoring and deduplicating results\n * 4. Returning the top recommendations\n *\n * Communicates via JSON-RPC over stdio using the Codex plugin SDK.\n */\n\nimport {\n createLogger,\n createRecommendationPlugin,\n EXTERNAL_ID_SOURCE_ANILIST,\n type InitializeParams,\n type PluginStorage,\n type Recommendation,\n type RecommendationClearResponse,\n type RecommendationDismissRequest,\n type RecommendationDismissResponse,\n type RecommendationProvider,\n type RecommendationRequest,\n type RecommendationResponse,\n type UserLibraryEntry,\n} from \"@ashdev/codex-plugin-sdk\";\nimport {\n AniListRecommendationClient,\n type AniListRecommendationNode,\n getBestTitle,\n stripHtml,\n} from \"./anilist.js\";\nimport { manifest } from \"./manifest.js\";\n\nconst logger = createLogger({ name: \"recommendations-anilist\", level: \"debug\" });\n\n// Plugin state (set during initialization)\nlet client: AniListRecommendationClient | null = null;\nlet viewerId: number | null = null;\nlet maxRecommendations = 20;\nlet maxSeeds = 10;\nlet searchFallback = true;\nlet storage: PluginStorage | null = null;\n\n/** Set the AniList client (exported for testing) */\nexport function setClient(c: AniListRecommendationClient | null): void {\n client = c;\n}\n\n/** Set the searchFallback flag (exported for testing) */\nexport function setSearchFallback(enabled: boolean): void {\n searchFallback = enabled;\n}\n\n/** Storage key for persisted dismissed recommendation IDs */\nconst DISMISSED_STORAGE_KEY = \"dismissed_ids\";\n\n// In-memory cache of dismissed IDs (synced with storage).\n// Loaded from storage on initialize, updated on dismiss/clear.\nexport const dismissedIds = new Set<string>();\n\n/**\n * Load dismissed IDs from persistent storage into the in-memory cache.\n */\nasync function loadDismissedIds(): Promise<void> {\n if (!storage) return;\n try {\n const result = await storage.get(DISMISSED_STORAGE_KEY);\n if (Array.isArray(result.data)) {\n dismissedIds.clear();\n for (const id of result.data) {\n if (typeof id === \"string\") {\n dismissedIds.add(id);\n }\n }\n logger.debug(`Loaded ${dismissedIds.size} dismissed IDs from storage`);\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown error\";\n logger.warn(`Failed to load dismissed IDs from storage: ${msg}`);\n }\n}\n\n/**\n * Persist the current dismissed IDs set to storage.\n */\nasync function saveDismissedIds(): Promise<void> {\n if (!storage) return;\n try {\n await storage.set(DISMISSED_STORAGE_KEY, [...dismissedIds]);\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown error\";\n logger.warn(`Failed to save dismissed IDs to storage: ${msg}`);\n }\n}\n\n// =============================================================================\n// Recommendation Generation\n// =============================================================================\n\n/**\n * Find AniList IDs for library entries.\n * Tries external_ids first, falls back to title search.\n */\nexport async function resolveAniListIds(\n entries: UserLibraryEntry[],\n): Promise<Map<string, { anilistId: number; title: string; rating: number }>> {\n if (!client) throw new Error(\"Plugin not initialized\");\n\n const resolved = new Map<string, { anilistId: number; title: string; rating: number }>();\n\n for (const entry of entries) {\n // Check if we already have an AniList external ID\n // Prefer api:anilist (new convention), fall back to legacy source names\n const anilistExt = entry.externalIds?.find(\n (e) =>\n e.source === EXTERNAL_ID_SOURCE_ANILIST || e.source === \"anilist\" || e.source === \"AniList\",\n );\n\n if (anilistExt) {\n const id = Number.parseInt(anilistExt.externalId, 10);\n if (!Number.isNaN(id)) {\n resolved.set(entry.seriesId, {\n anilistId: id,\n title: entry.title,\n rating: entry.userRating ?? 0,\n });\n continue;\n }\n }\n\n // Fall back to title search (when enabled)\n if (searchFallback) {\n const result = await client.searchManga(entry.title);\n if (result) {\n resolved.set(entry.seriesId, {\n anilistId: result.id,\n title: entry.title,\n rating: entry.userRating ?? 0,\n });\n }\n }\n }\n\n return resolved;\n}\n\n/**\n * Pick the best entries from the user's library to seed recommendations.\n * Prioritizes highly-rated, recently-read titles.\n */\nexport function pickSeedEntries(entries: UserLibraryEntry[], maxSeeds: number): UserLibraryEntry[] {\n // Sort by rating (desc), then by recency\n const sorted = [...entries].sort((a, b) => {\n const ratingDiff = (b.userRating ?? 0) - (a.userRating ?? 0);\n if (ratingDiff !== 0) return ratingDiff;\n // Fall back to books read as a proxy for engagement\n return b.booksRead - a.booksRead;\n });\n\n return sorted.slice(0, maxSeeds);\n}\n\n/**\n * Convert AniList recommendation nodes into Recommendation objects.\n */\nexport function convertRecommendations(\n nodes: AniListRecommendationNode[],\n basedOnTitle: string,\n userMangaIds: Set<number>,\n excludeIds: Set<string>,\n): Recommendation[] {\n const results: Recommendation[] = [];\n\n for (const node of nodes) {\n if (!node.mediaRecommendation) continue;\n\n const media = node.mediaRecommendation;\n const externalId = String(media.id);\n\n // Skip if excluded or dismissed\n if (excludeIds.has(externalId) || dismissedIds.has(externalId)) continue;\n\n const inLibrary = userMangaIds.has(media.id);\n\n // Compute a relevance score based on community rating and AniList average score\n const communityScore = Math.max(0, Math.min(node.rating, 100)) / 100;\n const avgScore = media.averageScore ? media.averageScore / 100 : 0.5;\n const score = Math.round((communityScore * 0.6 + avgScore * 0.4) * 100) / 100;\n\n results.push({\n externalId,\n externalUrl: media.siteUrl,\n title: getBestTitle(media.title),\n coverUrl: media.coverImage.large ?? undefined,\n summary: stripHtml(media.description),\n genres: media.genres ?? [],\n score: Math.max(0, Math.min(score, 1)),\n reason: `Recommended because you liked ${basedOnTitle}`,\n basedOn: [basedOnTitle],\n inLibrary,\n });\n }\n\n return results;\n}\n\n// =============================================================================\n// Provider Implementation\n// =============================================================================\n\nconst provider: RecommendationProvider = {\n async get(params: RecommendationRequest): Promise<RecommendationResponse> {\n if (!client) {\n throw new Error(\"Plugin not initialized - no AniList client\");\n }\n\n if (viewerId === null) {\n viewerId = await client.getViewerId();\n logger.info(`Authenticated as viewer ${viewerId}`);\n }\n\n const { library, limit, excludeIds: rawExcludeIds = [] } = params;\n const effectiveLimit = Math.min(limit ?? maxRecommendations, 50);\n const excludeIds = new Set(rawExcludeIds);\n\n // Return early if library is empty \u2014 no seeds to work with\n if (!library || library.length === 0) {\n logger.info(\"Empty library \u2014 returning no recommendations\");\n return { recommendations: [], generatedAt: new Date().toISOString(), cached: false };\n }\n\n // Get user's existing manga IDs for dedup\n const userMangaIds = await client.getUserMangaIds(viewerId);\n logger.debug(`User has ${userMangaIds.size} manga in AniList list`);\n\n // Pick seed entries (top-rated from user's library)\n const seeds = pickSeedEntries(library, maxSeeds);\n logger.debug(`Using ${seeds.length} seed entries from library of ${library.length}`);\n\n // Resolve AniList IDs for seed entries\n const resolved = await resolveAniListIds(seeds);\n logger.debug(`Resolved ${resolved.size} AniList IDs from ${seeds.length} seeds`);\n\n // Fetch recommendations for each seed\n const allRecs = new Map<string, Recommendation>();\n\n for (const [, { anilistId, title }] of resolved) {\n try {\n const nodes = await client.getRecommendationsForMedia(anilistId, 10);\n const recs = convertRecommendations(nodes, title, userMangaIds, excludeIds);\n\n for (const rec of recs) {\n // If we've seen this recommendation before, merge basedOn and keep higher score\n const existing = allRecs.get(rec.externalId);\n if (existing) {\n // Merge basedOn titles\n const mergedBasedOn = [...new Set([...existing.basedOn, ...rec.basedOn])];\n // Boost score slightly for multiply-recommended titles\n const boostedScore = Math.min(existing.score + 0.05, 1.0);\n allRecs.set(rec.externalId, {\n ...existing,\n score: Math.round(boostedScore * 100) / 100,\n basedOn: mergedBasedOn,\n reason:\n mergedBasedOn.length > 1\n ? `Recommended based on ${mergedBasedOn.join(\", \")}`\n : existing.reason,\n });\n } else {\n allRecs.set(rec.externalId, rec);\n }\n }\n } catch (error) {\n const msg = error instanceof Error ? error.message : \"Unknown error\";\n logger.warn(`Failed to get recommendations for AniList ID ${anilistId}: ${msg}`);\n }\n }\n\n // Sort by score descending and take top N\n const sorted = [...allRecs.values()].sort((a, b) => b.score - a.score).slice(0, effectiveLimit);\n\n logger.info(`Generated ${sorted.length} recommendations from ${resolved.size} seed titles`);\n\n return {\n recommendations: sorted,\n generatedAt: new Date().toISOString(),\n cached: false,\n };\n },\n\n async dismiss(params: RecommendationDismissRequest): Promise<RecommendationDismissResponse> {\n dismissedIds.add(params.externalId);\n logger.debug(\n `Dismissed recommendation: ${params.externalId} (reason: ${params.reason ?? \"none\"})`,\n );\n await saveDismissedIds();\n return { dismissed: true };\n },\n\n async clear(): Promise<RecommendationClearResponse> {\n const count = dismissedIds.size;\n dismissedIds.clear();\n logger.info(`Cleared ${count} dismissed recommendations`);\n await saveDismissedIds();\n return { cleared: true };\n },\n};\n\n// =============================================================================\n// Plugin Initialization\n// =============================================================================\n\ncreateRecommendationPlugin({\n manifest,\n provider,\n logLevel: \"debug\",\n async onInitialize(params: InitializeParams) {\n const accessToken = params.credentials?.access_token;\n if (accessToken) {\n client = new AniListRecommendationClient(accessToken);\n logger.info(\"AniList client initialized with access token\");\n } else {\n logger.warn(\"No access token provided - recommendation operations will fail\");\n }\n\n // Read maxRecommendations from adminConfig (defined in configSchema)\n const rawMax = params.adminConfig?.maxRecommendations;\n if (typeof rawMax === \"number\") {\n maxRecommendations = Math.max(1, Math.min(Math.round(rawMax), 50));\n logger.info(`Max recommendations set to: ${maxRecommendations}`);\n }\n\n // Read maxSeeds from adminConfig (defined in configSchema)\n const rawSeeds = params.adminConfig?.maxSeeds;\n if (typeof rawSeeds === \"number\") {\n maxSeeds = Math.max(1, Math.min(Math.round(rawSeeds), 25));\n logger.info(`Max seeds set to: ${maxSeeds}`);\n }\n\n // Read searchFallback from userConfig (default: true \u2014 preserve existing behavior)\n const uc = params.userConfig;\n if (uc && typeof uc.searchFallback === \"boolean\") {\n searchFallback = uc.searchFallback;\n logger.info(`Search fallback set to: ${searchFallback}`);\n }\n\n // Capture the storage client and restore persisted dismissed IDs\n storage = params.storage;\n await loadDismissedIds();\n },\n});\n\nlogger.info(\"AniList recommendations plugin started\");\n"],
5
+ "mappings": "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",
6
+ "names": ["manifest", "logger", "storage", "response", "manifest", "provider", "maxSeeds"]
7
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@ashdev/codex-plugin-recommendations-anilist",
3
+ "version": "1.10.0",
4
+ "description": "AniList recommendation provider plugin for Codex - generates personalized manga recommendations based on your reading history",
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/recommendations-anilist"
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 --passWithNoTests",
26
+ "test:watch": "vitest",
27
+ "prepublishOnly": "npm run lint && npm run build"
28
+ },
29
+ "keywords": [
30
+ "codex",
31
+ "plugin",
32
+ "anilist",
33
+ "recommendations",
34
+ "manga"
35
+ ],
36
+ "author": "Codex",
37
+ "license": "MIT",
38
+ "engines": {
39
+ "node": ">=22.0.0"
40
+ },
41
+ "dependencies": {
42
+ "@ashdev/codex-plugin-sdk": "^1.10.0"
43
+ },
44
+ "devDependencies": {
45
+ "@biomejs/biome": "^2.3.13",
46
+ "@types/node": "^22.0.0",
47
+ "esbuild": "^0.24.0",
48
+ "typescript": "^5.7.0",
49
+ "vitest": "^3.0.0"
50
+ }
51
+ }