@ashdev/codex-plugin-recommendations-anilist 1.10.0 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -686,7 +686,7 @@ function stripHtml(html) {
686
686
  // package.json
687
687
  var package_default = {
688
688
  name: "@ashdev/codex-plugin-recommendations-anilist",
689
- version: "1.10.0",
689
+ version: "1.10.1",
690
690
  description: "AniList recommendation provider plugin for Codex - generates personalized manga recommendations based on your reading history",
691
691
  main: "dist/index.js",
692
692
  bin: "dist/index.js",
@@ -725,7 +725,7 @@ var package_default = {
725
725
  node: ">=22.0.0"
726
726
  },
727
727
  dependencies: {
728
- "@ashdev/codex-plugin-sdk": "^1.10.0"
728
+ "@ashdev/codex-plugin-sdk": "^1.10.1"
729
729
  },
730
730
  devDependencies: {
731
731
  "@biomejs/biome": "^2.3.13",
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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"],
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.1\",\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.1\"\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
5
  "mappings": "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",
6
6
  "names": ["manifest", "logger", "storage", "response", "manifest", "provider", "maxSeeds"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashdev/codex-plugin-recommendations-anilist",
3
- "version": "1.10.0",
3
+ "version": "1.10.1",
4
4
  "description": "AniList recommendation provider plugin for Codex - generates personalized manga recommendations based on your reading history",
5
5
  "main": "dist/index.js",
6
6
  "bin": "dist/index.js",
@@ -39,7 +39,7 @@
39
39
  "node": ">=22.0.0"
40
40
  },
41
41
  "dependencies": {
42
- "@ashdev/codex-plugin-sdk": "^1.10.0"
42
+ "@ashdev/codex-plugin-sdk": "^1.10.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@biomejs/biome": "^2.3.13",