@ashdev/codex-plugin-sync-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
@@ -741,7 +741,7 @@ function convertScoreFromAnilist(score, format) {
741
741
  // package.json
742
742
  var package_default = {
743
743
  name: "@ashdev/codex-plugin-sync-anilist",
744
- version: "1.10.0",
744
+ version: "1.10.1",
745
745
  description: "AniList reading progress sync plugin for Codex",
746
746
  main: "dist/index.js",
747
747
  bin: "dist/index.js",
@@ -781,7 +781,7 @@ var package_default = {
781
781
  node: ">=22.0.0"
782
782
  },
783
783
  dependencies: {
784
- "@ashdev/codex-plugin-sdk": "^1.10.0"
784
+ "@ashdev/codex-plugin-sdk": "^1.10.1"
785
785
  },
786
786
  devDependencies: {
787
787
  "@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\n *\n * Provides typed access to AniList's GraphQL API for reading progress sync.\n * See: https://anilist.gitbook.io/anilist-apiv2-docs/\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 avatar {\n large\n medium\n }\n siteUrl\n options {\n displayAdultContent\n }\n mediaListOptions {\n scoreFormat\n }\n }\n }\n`;\n\nconst MANGA_LIST_QUERY = `\n query ($userId: Int!, $page: Int, $perPage: Int) {\n Page(page: $page, perPage: $perPage) {\n pageInfo {\n total\n currentPage\n lastPage\n hasNextPage\n }\n mediaList(userId: $userId, type: MANGA, sort: UPDATED_TIME_DESC) {\n id\n mediaId\n status\n score\n progress\n progressVolumes\n startedAt {\n year\n month\n day\n }\n completedAt {\n year\n month\n day\n }\n notes\n updatedAt\n media {\n id\n title {\n romaji\n english\n native\n }\n siteUrl\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\nconst UPDATE_ENTRY_MUTATION = `\n mutation (\n $mediaId: Int!,\n $status: MediaListStatus,\n $score: Float,\n $progress: Int,\n $progressVolumes: Int,\n $startedAt: FuzzyDateInput,\n $completedAt: FuzzyDateInput,\n $notes: String\n ) {\n SaveMediaListEntry(\n mediaId: $mediaId,\n status: $status,\n score: $score,\n progress: $progress,\n progressVolumes: $progressVolumes,\n startedAt: $startedAt,\n completedAt: $completedAt,\n notes: $notes\n ) {\n id\n mediaId\n status\n score\n progress\n progressVolumes\n }\n }\n`;\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface AniListViewer {\n id: number;\n name: string;\n avatar: { large?: string; medium?: string };\n siteUrl: string;\n options: { displayAdultContent: boolean };\n mediaListOptions: { scoreFormat: string };\n}\n\nexport interface AniListSearchResult {\n id: number;\n title: { romaji?: string; english?: string };\n}\n\nexport interface AniListFuzzyDate {\n year?: number | null;\n month?: number | null;\n day?: number | null;\n}\n\nexport interface AniListMediaListEntry {\n id: number;\n mediaId: number;\n status: string;\n score: number;\n progress: number;\n progressVolumes: number;\n startedAt: AniListFuzzyDate;\n completedAt: AniListFuzzyDate;\n notes: string | null;\n updatedAt: number;\n media: {\n id: number;\n title: { romaji?: string; english?: string; native?: string };\n siteUrl: string;\n };\n}\n\nexport interface AniListPageInfo {\n total: number;\n currentPage: number;\n lastPage: number;\n hasNextPage: boolean;\n}\n\nexport interface AniListSaveResult {\n id: number;\n mediaId: number;\n status: string;\n score: number;\n progress: number;\n progressVolumes: number;\n}\n\n// =============================================================================\n// Client\n// =============================================================================\n\nexport class AniListClient {\n private accessToken: string;\n\n constructor(accessToken: string) {\n this.accessToken = accessToken;\n }\n\n /**\n * Execute a GraphQL query against the AniList API.\n * On rate limit (429), waits the requested duration and retries once.\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 { data?: T; errors?: Array<{ message: string }> };\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 /**\n * Get the authenticated user's info\n */\n async getViewer(): Promise<AniListViewer> {\n const data = await this.query<{ Viewer: AniListViewer }>(VIEWER_QUERY);\n return data.Viewer;\n }\n\n /**\n * Get the user's manga list (paginated)\n */\n async getMangaList(\n userId: number,\n page = 1,\n perPage = 50,\n ): Promise<{ pageInfo: AniListPageInfo; entries: AniListMediaListEntry[] }> {\n const variables: Record<string, unknown> = { userId, page, perPage };\n\n const data = await this.query<{\n Page: {\n pageInfo: AniListPageInfo;\n mediaList: AniListMediaListEntry[];\n };\n }>(MANGA_LIST_QUERY, variables);\n\n return {\n pageInfo: data.Page.pageInfo,\n entries: data.Page.mediaList,\n };\n }\n\n /**\n * Update or create a manga list entry\n */\n async saveEntry(variables: {\n mediaId: number;\n status?: string;\n score?: number;\n progress?: number;\n progressVolumes?: number;\n startedAt?: AniListFuzzyDate;\n completedAt?: AniListFuzzyDate;\n notes?: string;\n }): Promise<AniListSaveResult> {\n const data = await this.query<{ SaveMediaListEntry: AniListSaveResult }>(\n UPDATE_ENTRY_MUTATION,\n variables,\n );\n return data.SaveMediaListEntry;\n }\n\n /**\n * Search for a manga by title and return its AniList ID.\n * Returns null if no result found or an error occurs.\n */\n async searchManga(title: string): Promise<AniListSearchResult | null> {\n try {\n const data = await this.query<{ Media: AniListSearchResult | null }>(SEARCH_MANGA_QUERY, {\n search: title,\n });\n return data.Media;\n } catch {\n return null;\n }\n }\n}\n\n// =============================================================================\n// Status Mapping\n// =============================================================================\n\n/**\n * Map AniList MediaListStatus to Codex SyncReadingStatus\n */\nexport function anilistStatusToSync(\n status: string,\n): \"reading\" | \"completed\" | \"on_hold\" | \"dropped\" | \"plan_to_read\" {\n switch (status) {\n case \"CURRENT\":\n case \"REPEATING\":\n return \"reading\";\n case \"COMPLETED\":\n return \"completed\";\n case \"PAUSED\":\n return \"on_hold\";\n case \"DROPPED\":\n return \"dropped\";\n case \"PLANNING\":\n return \"plan_to_read\";\n default:\n return \"reading\";\n }\n}\n\n/**\n * Map Codex SyncReadingStatus to AniList MediaListStatus\n */\nexport function syncStatusToAnilist(\n status: string,\n): \"CURRENT\" | \"COMPLETED\" | \"PAUSED\" | \"DROPPED\" | \"PLANNING\" {\n switch (status) {\n case \"reading\":\n return \"CURRENT\";\n case \"completed\":\n return \"COMPLETED\";\n case \"on_hold\":\n return \"PAUSED\";\n case \"dropped\":\n return \"DROPPED\";\n case \"plan_to_read\":\n return \"PLANNING\";\n default:\n return \"CURRENT\";\n }\n}\n\n/**\n * Convert AniList FuzzyDate to ISO 8601 string\n */\nexport function fuzzyDateToIso(date: AniListFuzzyDate | null | undefined): string | undefined {\n if (!date?.year) return undefined;\n const month = date.month ? String(date.month).padStart(2, \"0\") : \"01\";\n const day = date.day ? String(date.day).padStart(2, \"0\") : \"01\";\n return `${date.year}-${month}-${day}T00:00:00Z`;\n}\n\n/**\n * Convert ISO 8601 string to AniList FuzzyDate\n */\nexport function isoToFuzzyDate(iso: string | undefined): AniListFuzzyDate | undefined {\n if (!iso) return undefined;\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return undefined;\n return {\n year: d.getUTCFullYear(),\n month: d.getUTCMonth() + 1,\n day: d.getUTCDate(),\n };\n}\n\n// =============================================================================\n// Score Conversion\n// =============================================================================\n\n/**\n * Convert a score from Codex's 1-100 scale to AniList's format\n */\nexport function convertScoreToAnilist(score: number, format: string): number {\n switch (format) {\n case \"POINT_100\":\n return Math.round(score);\n case \"POINT_10_DECIMAL\":\n return score / 10;\n case \"POINT_10\":\n return Math.round(score / 10);\n case \"POINT_5\":\n return Math.round(score / 20);\n case \"POINT_3\":\n if (score >= 70) return 3;\n if (score >= 40) return 2;\n return 1;\n default:\n return Math.round(score / 10);\n }\n}\n\n/**\n * Convert a score from AniList's format to Codex's 1-100 scale\n */\nexport function convertScoreFromAnilist(score: number, format: string): number {\n switch (format) {\n case \"POINT_100\":\n return score;\n case \"POINT_10_DECIMAL\":\n return score * 10;\n case \"POINT_10\":\n return score * 10;\n case \"POINT_5\":\n return score * 20;\n case \"POINT_3\":\n return Math.round(score * 33.3);\n default:\n return score * 10;\n }\n}\n", "{\n \"name\": \"@ashdev/codex-plugin-sync-anilist\",\n \"version\": \"1.10.0\",\n \"description\": \"AniList reading progress sync plugin for Codex\",\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/sync-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 \"sync\",\n \"manga\",\n \"reading-progress\"\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 { EXTERNAL_ID_SOURCE_ANILIST, type PluginManifest } from \"@ashdev/codex-plugin-sdk\";\nimport packageJson from \"../package.json\" with { type: \"json\" };\n\nexport const manifest = {\n name: \"sync-anilist\",\n displayName: \"AniList Sync\",\n version: packageJson.version,\n description:\n \"Sync manga reading progress between Codex and AniList. Supports push/pull of reading status, chapters read, scores, and dates.\",\n author: \"Codex\",\n homepage: \"https://github.com/AshDevFr/codex\",\n protocolVersion: \"1.0\",\n capabilities: {\n userReadSync: true,\n externalIdSource: EXTERNAL_ID_SOURCE_ANILIST,\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 userConfigSchema: {\n description: \"AniList-specific sync settings\",\n fields: [\n {\n key: \"progressUnit\",\n label: \"Progress Unit\",\n description:\n \"What each book in Codex represents in AniList. Use 'volumes' for manga volumes, 'chapters' for individual chapters\",\n type: \"string\" as const,\n required: false,\n default: \"volumes\",\n },\n {\n key: \"pauseAfterDays\",\n label: \"Auto-Pause After Days\",\n description:\n \"Automatically set in-progress series to Paused on AniList if no reading activity in this many days. Set to 0 to disable.\",\n type: \"number\" as const,\n required: false,\n default: 0,\n },\n {\n key: \"dropAfterDays\",\n label: \"Auto-Drop After Days\",\n description:\n \"Automatically set in-progress series to Dropped on AniList if no reading activity in this many days. Set to 0 to disable. When both pause and drop are set, the shorter threshold fires first.\",\n type: \"number\" as const,\n required: false,\n default: 0,\n },\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 and sync progress. Disable for strict matching only.\",\n type: \"boolean\" as const,\n required: false,\n default: false,\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: \"Sync manga reading progress between Codex and AniList\",\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: { userReadSync: true };\n};\n", "/**\n * AniList Sync Plugin for Codex\n *\n * Syncs manga reading progress between Codex and AniList.\n * Communicates via JSON-RPC over stdio using the Codex plugin SDK.\n *\n * Capabilities:\n * - Push reading progress from Codex to AniList\n * - Pull reading progress from AniList to Codex\n * - Get user info from AniList\n * - Status reporting for sync state\n */\n\nimport {\n createLogger,\n createSyncPlugin,\n type ExternalUserInfo,\n type InitializeParams,\n type SyncEntry,\n type SyncEntryResult,\n type SyncProvider,\n type SyncPullRequest,\n type SyncPullResponse,\n type SyncPushRequest,\n type SyncPushResponse,\n type SyncStatusResponse,\n} from \"@ashdev/codex-plugin-sdk\";\nimport {\n AniListClient,\n type AniListFuzzyDate,\n anilistStatusToSync,\n convertScoreFromAnilist,\n convertScoreToAnilist,\n fuzzyDateToIso,\n isoToFuzzyDate,\n syncStatusToAnilist,\n} from \"./anilist.js\";\nimport { manifest } from \"./manifest.js\";\n\nconst logger = createLogger({ name: \"sync-anilist\", level: \"debug\" });\n\n// Plugin state (set during initialization)\nlet client: AniListClient | null = null;\nlet viewerId: number | null = null;\nlet scoreFormat = \"POINT_10\";\n\n// Plugin-specific config (from userConfig, set during initialization)\nlet progressUnit: \"volumes\" | \"chapters\" = \"volumes\";\nlet pauseAfterDays = 0;\nlet dropAfterDays = 0;\nlet searchFallback = false;\n\n/** Set the AniList client (exported for testing) */\nexport function setClient(c: AniListClient | null): void {\n client = c;\n}\n\n/** Set the viewer ID (exported for testing) */\nexport function setViewerId(id: number | null): void {\n viewerId = id;\n}\n\n/** Set the searchFallback flag (exported for testing) */\nexport function setSearchFallback(enabled: boolean): void {\n searchFallback = enabled;\n}\n\n// =============================================================================\n// Staleness Logic\n// =============================================================================\n\n/**\n * Apply auto-pause/auto-drop for stale in-progress entries.\n *\n * Only applies to \"reading\" entries. Drop takes priority over pause\n * when both thresholds are met. A threshold of 0 means disabled.\n */\nexport function applyStaleness(\n status: SyncEntry[\"status\"],\n latestUpdatedAt: string | undefined,\n pauseDays: number,\n dropDays: number,\n now?: number,\n): SyncEntry[\"status\"] {\n if (status !== \"reading\") return status;\n if (pauseDays === 0 && dropDays === 0) return status;\n if (!latestUpdatedAt) return status;\n\n const lastActivity = new Date(latestUpdatedAt).getTime();\n if (Number.isNaN(lastActivity)) return status;\n\n const currentTime = now ?? Date.now();\n const daysInactive = Math.max(0, (currentTime - lastActivity) / (1000 * 60 * 60 * 24));\n\n // Drop takes priority (stronger action)\n if (dropDays > 0 && daysInactive >= dropDays) {\n return \"dropped\";\n }\n if (pauseDays > 0 && daysInactive >= pauseDays) {\n return \"on_hold\";\n }\n\n return status;\n}\n\n// =============================================================================\n// Sync Provider Implementation\n// =============================================================================\n\n/** Exported for testing */\nexport const provider: SyncProvider = {\n async getUserInfo(): Promise<ExternalUserInfo> {\n if (!client) {\n throw new Error(\"Plugin not initialized - no AniList client\");\n }\n\n const viewer = await client.getViewer();\n viewerId = viewer.id;\n scoreFormat = viewer.mediaListOptions.scoreFormat;\n\n logger.info(`Authenticated as ${viewer.name} (id: ${viewer.id}, scoreFormat: ${scoreFormat})`);\n\n return {\n externalId: String(viewer.id),\n username: viewer.name,\n avatarUrl: viewer.avatar.large || viewer.avatar.medium,\n profileUrl: viewer.siteUrl,\n };\n },\n\n async pushProgress(params: SyncPushRequest): Promise<SyncPushResponse> {\n if (!client || viewerId === null) {\n throw new Error(\"Plugin not initialized - call getUserInfo first\");\n }\n\n // Pre-fetch existing media IDs to distinguish \"created\" vs \"updated\"\n const existingMediaIds = new Set<number>();\n let page = 1;\n let hasMore = true;\n while (hasMore) {\n const result = await client.getMangaList(viewerId, page, 50);\n for (const entry of result.entries) {\n existingMediaIds.add(entry.mediaId);\n }\n hasMore = result.pageInfo.hasNextPage;\n page++;\n }\n\n const success: SyncEntryResult[] = [];\n const failed: SyncEntryResult[] = [];\n\n for (const entry of params.entries) {\n try {\n let mediaId = Number.parseInt(entry.externalId, 10);\n if (Number.isNaN(mediaId)) {\n // Try search fallback if enabled and entry has a title\n if (searchFallback && entry.title) {\n const result = await client.searchManga(entry.title);\n if (result) {\n mediaId = result.id;\n logger.info(`Search fallback resolved \"${entry.title}\" \u2192 AniList ID ${mediaId}`);\n }\n }\n\n if (Number.isNaN(mediaId)) {\n failed.push({\n externalId: entry.externalId,\n status: \"failed\",\n error: searchFallback\n ? `No AniList match found for \"${entry.title || entry.externalId}\"`\n : `Invalid media ID: ${entry.externalId}`,\n });\n continue;\n }\n }\n\n // Apply staleness logic: auto-pause or auto-drop stale in-progress entries\n const effectiveStatus = applyStaleness(\n entry.status,\n entry.latestUpdatedAt,\n pauseAfterDays,\n dropAfterDays,\n );\n if (effectiveStatus !== entry.status) {\n logger.debug(\n `Entry ${entry.externalId}: auto-${effectiveStatus === \"dropped\" ? \"dropped\" : \"paused\"} (was ${entry.status})`,\n );\n }\n\n const saveParams: {\n mediaId: number;\n status?: string;\n score?: number;\n progress?: number;\n progressVolumes?: number;\n startedAt?: AniListFuzzyDate;\n completedAt?: AniListFuzzyDate;\n notes?: string;\n } = {\n mediaId,\n status: syncStatusToAnilist(effectiveStatus),\n };\n\n // Map progress using the configured progressUnit.\n // Server always sends books-read as `volumes`. Based on\n // progressUnit, we map to AniList's `progress` (chapters)\n // or `progressVolumes` (volumes) field.\n const count = entry.progress?.volumes ?? entry.progress?.chapters;\n if (count !== undefined) {\n if (progressUnit === \"chapters\") {\n saveParams.progress = count;\n } else {\n saveParams.progressVolumes = count;\n }\n }\n\n // Map score (convert from 1-100 scale to AniList format)\n if (entry.score !== undefined) {\n saveParams.score = convertScoreToAnilist(entry.score, scoreFormat);\n }\n\n // Map dates\n if (entry.startedAt) {\n saveParams.startedAt = isoToFuzzyDate(entry.startedAt);\n }\n if (entry.completedAt) {\n saveParams.completedAt = isoToFuzzyDate(entry.completedAt);\n }\n\n // Map notes\n if (entry.notes !== undefined) {\n saveParams.notes = entry.notes;\n }\n\n const resolvedExternalId = String(mediaId);\n const existed = existingMediaIds.has(mediaId);\n const result = await client.saveEntry(saveParams);\n logger.debug(`Pushed entry ${resolvedExternalId}: status=${result.status}`);\n\n // Track newly created entries for subsequent pushes in the same batch\n existingMediaIds.add(mediaId);\n\n success.push({\n externalId: resolvedExternalId,\n status: existed ? \"updated\" : \"created\",\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n logger.error(`Failed to push entry ${entry.externalId}: ${message}`);\n failed.push({\n externalId: entry.externalId,\n status: \"failed\",\n error: message,\n });\n }\n }\n\n return { success, failed };\n },\n\n async pullProgress(params: SyncPullRequest): Promise<SyncPullResponse> {\n if (!client || viewerId === null) {\n throw new Error(\"Plugin not initialized - call getUserInfo first\");\n }\n\n // Parse pagination cursor (page number)\n const page = params.cursor ? Number.parseInt(params.cursor, 10) : 1;\n const perPage = params.limit ? Math.min(params.limit, 50) : 50;\n\n const result = await client.getMangaList(viewerId, page, perPage);\n\n const entries: SyncEntry[] = result.entries.map((entry) => ({\n externalId: String(entry.mediaId),\n status: anilistStatusToSync(entry.status),\n progress: {\n chapters: entry.progress || undefined,\n volumes: entry.progressVolumes || undefined,\n },\n score: entry.score > 0 ? convertScoreFromAnilist(entry.score, scoreFormat) : undefined,\n startedAt: fuzzyDateToIso(entry.startedAt),\n completedAt: fuzzyDateToIso(entry.completedAt),\n notes: entry.notes || undefined,\n }));\n\n logger.info(\n `Pulled ${entries.length} entries (page ${result.pageInfo.currentPage}/${result.pageInfo.lastPage})`,\n );\n\n return {\n entries,\n nextCursor: result.pageInfo.hasNextPage ? String(result.pageInfo.currentPage + 1) : undefined,\n hasMore: result.pageInfo.hasNextPage,\n };\n },\n\n async status(): Promise<SyncStatusResponse> {\n if (!client || viewerId === null) {\n return {\n pendingPush: 0,\n pendingPull: 0,\n conflicts: 0,\n };\n }\n\n // Get total count from AniList\n const result = await client.getMangaList(viewerId, 1, 1);\n\n return {\n externalCount: result.pageInfo.total,\n pendingPush: 0,\n pendingPull: 0,\n conflicts: 0,\n };\n },\n};\n\n// =============================================================================\n// Plugin Initialization\n// =============================================================================\n\ncreateSyncPlugin({\n manifest,\n provider,\n logLevel: \"debug\",\n onInitialize(params: InitializeParams) {\n // Get access token from credentials\n const accessToken = params.credentials?.access_token;\n if (accessToken) {\n client = new AniListClient(accessToken);\n logger.info(\"AniList client initialized with access token\");\n } else {\n logger.warn(\"No access token provided - sync operations will fail\");\n }\n\n // Read plugin-specific config from userConfig\n const uc = params.userConfig;\n if (uc) {\n const unit = uc.progressUnit;\n if (unit === \"chapters\" || unit === \"volumes\") {\n progressUnit = unit;\n }\n if (typeof uc.pauseAfterDays === \"number\" && uc.pauseAfterDays >= 0) {\n pauseAfterDays = uc.pauseAfterDays;\n }\n if (typeof uc.dropAfterDays === \"number\" && uc.dropAfterDays >= 0) {\n dropAfterDays = uc.dropAfterDays;\n }\n if (typeof uc.searchFallback === \"boolean\") {\n searchFallback = uc.searchFallback;\n }\n logger.info(\n `Plugin config: progressUnit=${progressUnit}, pauseAfterDays=${pauseAfterDays}, dropAfterDays=${dropAfterDays}, searchFallback=${searchFallback}`,\n );\n }\n },\n});\n\nlogger.info(\"AniList sync plugin started\");\n"],
4
+ "sourcesContent": [null, null, null, null, null, null, "/**\n * AniList GraphQL API client\n *\n * Provides typed access to AniList's GraphQL API for reading progress sync.\n * See: https://anilist.gitbook.io/anilist-apiv2-docs/\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 avatar {\n large\n medium\n }\n siteUrl\n options {\n displayAdultContent\n }\n mediaListOptions {\n scoreFormat\n }\n }\n }\n`;\n\nconst MANGA_LIST_QUERY = `\n query ($userId: Int!, $page: Int, $perPage: Int) {\n Page(page: $page, perPage: $perPage) {\n pageInfo {\n total\n currentPage\n lastPage\n hasNextPage\n }\n mediaList(userId: $userId, type: MANGA, sort: UPDATED_TIME_DESC) {\n id\n mediaId\n status\n score\n progress\n progressVolumes\n startedAt {\n year\n month\n day\n }\n completedAt {\n year\n month\n day\n }\n notes\n updatedAt\n media {\n id\n title {\n romaji\n english\n native\n }\n siteUrl\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\nconst UPDATE_ENTRY_MUTATION = `\n mutation (\n $mediaId: Int!,\n $status: MediaListStatus,\n $score: Float,\n $progress: Int,\n $progressVolumes: Int,\n $startedAt: FuzzyDateInput,\n $completedAt: FuzzyDateInput,\n $notes: String\n ) {\n SaveMediaListEntry(\n mediaId: $mediaId,\n status: $status,\n score: $score,\n progress: $progress,\n progressVolumes: $progressVolumes,\n startedAt: $startedAt,\n completedAt: $completedAt,\n notes: $notes\n ) {\n id\n mediaId\n status\n score\n progress\n progressVolumes\n }\n }\n`;\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface AniListViewer {\n id: number;\n name: string;\n avatar: { large?: string; medium?: string };\n siteUrl: string;\n options: { displayAdultContent: boolean };\n mediaListOptions: { scoreFormat: string };\n}\n\nexport interface AniListSearchResult {\n id: number;\n title: { romaji?: string; english?: string };\n}\n\nexport interface AniListFuzzyDate {\n year?: number | null;\n month?: number | null;\n day?: number | null;\n}\n\nexport interface AniListMediaListEntry {\n id: number;\n mediaId: number;\n status: string;\n score: number;\n progress: number;\n progressVolumes: number;\n startedAt: AniListFuzzyDate;\n completedAt: AniListFuzzyDate;\n notes: string | null;\n updatedAt: number;\n media: {\n id: number;\n title: { romaji?: string; english?: string; native?: string };\n siteUrl: string;\n };\n}\n\nexport interface AniListPageInfo {\n total: number;\n currentPage: number;\n lastPage: number;\n hasNextPage: boolean;\n}\n\nexport interface AniListSaveResult {\n id: number;\n mediaId: number;\n status: string;\n score: number;\n progress: number;\n progressVolumes: number;\n}\n\n// =============================================================================\n// Client\n// =============================================================================\n\nexport class AniListClient {\n private accessToken: string;\n\n constructor(accessToken: string) {\n this.accessToken = accessToken;\n }\n\n /**\n * Execute a GraphQL query against the AniList API.\n * On rate limit (429), waits the requested duration and retries once.\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 { data?: T; errors?: Array<{ message: string }> };\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 /**\n * Get the authenticated user's info\n */\n async getViewer(): Promise<AniListViewer> {\n const data = await this.query<{ Viewer: AniListViewer }>(VIEWER_QUERY);\n return data.Viewer;\n }\n\n /**\n * Get the user's manga list (paginated)\n */\n async getMangaList(\n userId: number,\n page = 1,\n perPage = 50,\n ): Promise<{ pageInfo: AniListPageInfo; entries: AniListMediaListEntry[] }> {\n const variables: Record<string, unknown> = { userId, page, perPage };\n\n const data = await this.query<{\n Page: {\n pageInfo: AniListPageInfo;\n mediaList: AniListMediaListEntry[];\n };\n }>(MANGA_LIST_QUERY, variables);\n\n return {\n pageInfo: data.Page.pageInfo,\n entries: data.Page.mediaList,\n };\n }\n\n /**\n * Update or create a manga list entry\n */\n async saveEntry(variables: {\n mediaId: number;\n status?: string;\n score?: number;\n progress?: number;\n progressVolumes?: number;\n startedAt?: AniListFuzzyDate;\n completedAt?: AniListFuzzyDate;\n notes?: string;\n }): Promise<AniListSaveResult> {\n const data = await this.query<{ SaveMediaListEntry: AniListSaveResult }>(\n UPDATE_ENTRY_MUTATION,\n variables,\n );\n return data.SaveMediaListEntry;\n }\n\n /**\n * Search for a manga by title and return its AniList ID.\n * Returns null if no result found or an error occurs.\n */\n async searchManga(title: string): Promise<AniListSearchResult | null> {\n try {\n const data = await this.query<{ Media: AniListSearchResult | null }>(SEARCH_MANGA_QUERY, {\n search: title,\n });\n return data.Media;\n } catch {\n return null;\n }\n }\n}\n\n// =============================================================================\n// Status Mapping\n// =============================================================================\n\n/**\n * Map AniList MediaListStatus to Codex SyncReadingStatus\n */\nexport function anilistStatusToSync(\n status: string,\n): \"reading\" | \"completed\" | \"on_hold\" | \"dropped\" | \"plan_to_read\" {\n switch (status) {\n case \"CURRENT\":\n case \"REPEATING\":\n return \"reading\";\n case \"COMPLETED\":\n return \"completed\";\n case \"PAUSED\":\n return \"on_hold\";\n case \"DROPPED\":\n return \"dropped\";\n case \"PLANNING\":\n return \"plan_to_read\";\n default:\n return \"reading\";\n }\n}\n\n/**\n * Map Codex SyncReadingStatus to AniList MediaListStatus\n */\nexport function syncStatusToAnilist(\n status: string,\n): \"CURRENT\" | \"COMPLETED\" | \"PAUSED\" | \"DROPPED\" | \"PLANNING\" {\n switch (status) {\n case \"reading\":\n return \"CURRENT\";\n case \"completed\":\n return \"COMPLETED\";\n case \"on_hold\":\n return \"PAUSED\";\n case \"dropped\":\n return \"DROPPED\";\n case \"plan_to_read\":\n return \"PLANNING\";\n default:\n return \"CURRENT\";\n }\n}\n\n/**\n * Convert AniList FuzzyDate to ISO 8601 string\n */\nexport function fuzzyDateToIso(date: AniListFuzzyDate | null | undefined): string | undefined {\n if (!date?.year) return undefined;\n const month = date.month ? String(date.month).padStart(2, \"0\") : \"01\";\n const day = date.day ? String(date.day).padStart(2, \"0\") : \"01\";\n return `${date.year}-${month}-${day}T00:00:00Z`;\n}\n\n/**\n * Convert ISO 8601 string to AniList FuzzyDate\n */\nexport function isoToFuzzyDate(iso: string | undefined): AniListFuzzyDate | undefined {\n if (!iso) return undefined;\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return undefined;\n return {\n year: d.getUTCFullYear(),\n month: d.getUTCMonth() + 1,\n day: d.getUTCDate(),\n };\n}\n\n// =============================================================================\n// Score Conversion\n// =============================================================================\n\n/**\n * Convert a score from Codex's 1-100 scale to AniList's format\n */\nexport function convertScoreToAnilist(score: number, format: string): number {\n switch (format) {\n case \"POINT_100\":\n return Math.round(score);\n case \"POINT_10_DECIMAL\":\n return score / 10;\n case \"POINT_10\":\n return Math.round(score / 10);\n case \"POINT_5\":\n return Math.round(score / 20);\n case \"POINT_3\":\n if (score >= 70) return 3;\n if (score >= 40) return 2;\n return 1;\n default:\n return Math.round(score / 10);\n }\n}\n\n/**\n * Convert a score from AniList's format to Codex's 1-100 scale\n */\nexport function convertScoreFromAnilist(score: number, format: string): number {\n switch (format) {\n case \"POINT_100\":\n return score;\n case \"POINT_10_DECIMAL\":\n return score * 10;\n case \"POINT_10\":\n return score * 10;\n case \"POINT_5\":\n return score * 20;\n case \"POINT_3\":\n return Math.round(score * 33.3);\n default:\n return score * 10;\n }\n}\n", "{\n \"name\": \"@ashdev/codex-plugin-sync-anilist\",\n \"version\": \"1.10.1\",\n \"description\": \"AniList reading progress sync plugin for Codex\",\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/sync-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 \"sync\",\n \"manga\",\n \"reading-progress\"\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 { EXTERNAL_ID_SOURCE_ANILIST, type PluginManifest } from \"@ashdev/codex-plugin-sdk\";\nimport packageJson from \"../package.json\" with { type: \"json\" };\n\nexport const manifest = {\n name: \"sync-anilist\",\n displayName: \"AniList Sync\",\n version: packageJson.version,\n description:\n \"Sync manga reading progress between Codex and AniList. Supports push/pull of reading status, chapters read, scores, and dates.\",\n author: \"Codex\",\n homepage: \"https://github.com/AshDevFr/codex\",\n protocolVersion: \"1.0\",\n capabilities: {\n userReadSync: true,\n externalIdSource: EXTERNAL_ID_SOURCE_ANILIST,\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 userConfigSchema: {\n description: \"AniList-specific sync settings\",\n fields: [\n {\n key: \"progressUnit\",\n label: \"Progress Unit\",\n description:\n \"What each book in Codex represents in AniList. Use 'volumes' for manga volumes, 'chapters' for individual chapters\",\n type: \"string\" as const,\n required: false,\n default: \"volumes\",\n },\n {\n key: \"pauseAfterDays\",\n label: \"Auto-Pause After Days\",\n description:\n \"Automatically set in-progress series to Paused on AniList if no reading activity in this many days. Set to 0 to disable.\",\n type: \"number\" as const,\n required: false,\n default: 0,\n },\n {\n key: \"dropAfterDays\",\n label: \"Auto-Drop After Days\",\n description:\n \"Automatically set in-progress series to Dropped on AniList if no reading activity in this many days. Set to 0 to disable. When both pause and drop are set, the shorter threshold fires first.\",\n type: \"number\" as const,\n required: false,\n default: 0,\n },\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 and sync progress. Disable for strict matching only.\",\n type: \"boolean\" as const,\n required: false,\n default: false,\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: \"Sync manga reading progress between Codex and AniList\",\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: { userReadSync: true };\n};\n", "/**\n * AniList Sync Plugin for Codex\n *\n * Syncs manga reading progress between Codex and AniList.\n * Communicates via JSON-RPC over stdio using the Codex plugin SDK.\n *\n * Capabilities:\n * - Push reading progress from Codex to AniList\n * - Pull reading progress from AniList to Codex\n * - Get user info from AniList\n * - Status reporting for sync state\n */\n\nimport {\n createLogger,\n createSyncPlugin,\n type ExternalUserInfo,\n type InitializeParams,\n type SyncEntry,\n type SyncEntryResult,\n type SyncProvider,\n type SyncPullRequest,\n type SyncPullResponse,\n type SyncPushRequest,\n type SyncPushResponse,\n type SyncStatusResponse,\n} from \"@ashdev/codex-plugin-sdk\";\nimport {\n AniListClient,\n type AniListFuzzyDate,\n anilistStatusToSync,\n convertScoreFromAnilist,\n convertScoreToAnilist,\n fuzzyDateToIso,\n isoToFuzzyDate,\n syncStatusToAnilist,\n} from \"./anilist.js\";\nimport { manifest } from \"./manifest.js\";\n\nconst logger = createLogger({ name: \"sync-anilist\", level: \"debug\" });\n\n// Plugin state (set during initialization)\nlet client: AniListClient | null = null;\nlet viewerId: number | null = null;\nlet scoreFormat = \"POINT_10\";\n\n// Plugin-specific config (from userConfig, set during initialization)\nlet progressUnit: \"volumes\" | \"chapters\" = \"volumes\";\nlet pauseAfterDays = 0;\nlet dropAfterDays = 0;\nlet searchFallback = false;\n\n/** Set the AniList client (exported for testing) */\nexport function setClient(c: AniListClient | null): void {\n client = c;\n}\n\n/** Set the viewer ID (exported for testing) */\nexport function setViewerId(id: number | null): void {\n viewerId = id;\n}\n\n/** Set the searchFallback flag (exported for testing) */\nexport function setSearchFallback(enabled: boolean): void {\n searchFallback = enabled;\n}\n\n// =============================================================================\n// Staleness Logic\n// =============================================================================\n\n/**\n * Apply auto-pause/auto-drop for stale in-progress entries.\n *\n * Only applies to \"reading\" entries. Drop takes priority over pause\n * when both thresholds are met. A threshold of 0 means disabled.\n */\nexport function applyStaleness(\n status: SyncEntry[\"status\"],\n latestUpdatedAt: string | undefined,\n pauseDays: number,\n dropDays: number,\n now?: number,\n): SyncEntry[\"status\"] {\n if (status !== \"reading\") return status;\n if (pauseDays === 0 && dropDays === 0) return status;\n if (!latestUpdatedAt) return status;\n\n const lastActivity = new Date(latestUpdatedAt).getTime();\n if (Number.isNaN(lastActivity)) return status;\n\n const currentTime = now ?? Date.now();\n const daysInactive = Math.max(0, (currentTime - lastActivity) / (1000 * 60 * 60 * 24));\n\n // Drop takes priority (stronger action)\n if (dropDays > 0 && daysInactive >= dropDays) {\n return \"dropped\";\n }\n if (pauseDays > 0 && daysInactive >= pauseDays) {\n return \"on_hold\";\n }\n\n return status;\n}\n\n// =============================================================================\n// Sync Provider Implementation\n// =============================================================================\n\n/** Exported for testing */\nexport const provider: SyncProvider = {\n async getUserInfo(): Promise<ExternalUserInfo> {\n if (!client) {\n throw new Error(\"Plugin not initialized - no AniList client\");\n }\n\n const viewer = await client.getViewer();\n viewerId = viewer.id;\n scoreFormat = viewer.mediaListOptions.scoreFormat;\n\n logger.info(`Authenticated as ${viewer.name} (id: ${viewer.id}, scoreFormat: ${scoreFormat})`);\n\n return {\n externalId: String(viewer.id),\n username: viewer.name,\n avatarUrl: viewer.avatar.large || viewer.avatar.medium,\n profileUrl: viewer.siteUrl,\n };\n },\n\n async pushProgress(params: SyncPushRequest): Promise<SyncPushResponse> {\n if (!client || viewerId === null) {\n throw new Error(\"Plugin not initialized - call getUserInfo first\");\n }\n\n // Pre-fetch existing media IDs to distinguish \"created\" vs \"updated\"\n const existingMediaIds = new Set<number>();\n let page = 1;\n let hasMore = true;\n while (hasMore) {\n const result = await client.getMangaList(viewerId, page, 50);\n for (const entry of result.entries) {\n existingMediaIds.add(entry.mediaId);\n }\n hasMore = result.pageInfo.hasNextPage;\n page++;\n }\n\n const success: SyncEntryResult[] = [];\n const failed: SyncEntryResult[] = [];\n\n for (const entry of params.entries) {\n try {\n let mediaId = Number.parseInt(entry.externalId, 10);\n if (Number.isNaN(mediaId)) {\n // Try search fallback if enabled and entry has a title\n if (searchFallback && entry.title) {\n const result = await client.searchManga(entry.title);\n if (result) {\n mediaId = result.id;\n logger.info(`Search fallback resolved \"${entry.title}\" \u2192 AniList ID ${mediaId}`);\n }\n }\n\n if (Number.isNaN(mediaId)) {\n failed.push({\n externalId: entry.externalId,\n status: \"failed\",\n error: searchFallback\n ? `No AniList match found for \"${entry.title || entry.externalId}\"`\n : `Invalid media ID: ${entry.externalId}`,\n });\n continue;\n }\n }\n\n // Apply staleness logic: auto-pause or auto-drop stale in-progress entries\n const effectiveStatus = applyStaleness(\n entry.status,\n entry.latestUpdatedAt,\n pauseAfterDays,\n dropAfterDays,\n );\n if (effectiveStatus !== entry.status) {\n logger.debug(\n `Entry ${entry.externalId}: auto-${effectiveStatus === \"dropped\" ? \"dropped\" : \"paused\"} (was ${entry.status})`,\n );\n }\n\n const saveParams: {\n mediaId: number;\n status?: string;\n score?: number;\n progress?: number;\n progressVolumes?: number;\n startedAt?: AniListFuzzyDate;\n completedAt?: AniListFuzzyDate;\n notes?: string;\n } = {\n mediaId,\n status: syncStatusToAnilist(effectiveStatus),\n };\n\n // Map progress using the configured progressUnit.\n // Server always sends books-read as `volumes`. Based on\n // progressUnit, we map to AniList's `progress` (chapters)\n // or `progressVolumes` (volumes) field.\n const count = entry.progress?.volumes ?? entry.progress?.chapters;\n if (count !== undefined) {\n if (progressUnit === \"chapters\") {\n saveParams.progress = count;\n } else {\n saveParams.progressVolumes = count;\n }\n }\n\n // Map score (convert from 1-100 scale to AniList format)\n if (entry.score !== undefined) {\n saveParams.score = convertScoreToAnilist(entry.score, scoreFormat);\n }\n\n // Map dates\n if (entry.startedAt) {\n saveParams.startedAt = isoToFuzzyDate(entry.startedAt);\n }\n if (entry.completedAt) {\n saveParams.completedAt = isoToFuzzyDate(entry.completedAt);\n }\n\n // Map notes\n if (entry.notes !== undefined) {\n saveParams.notes = entry.notes;\n }\n\n const resolvedExternalId = String(mediaId);\n const existed = existingMediaIds.has(mediaId);\n const result = await client.saveEntry(saveParams);\n logger.debug(`Pushed entry ${resolvedExternalId}: status=${result.status}`);\n\n // Track newly created entries for subsequent pushes in the same batch\n existingMediaIds.add(mediaId);\n\n success.push({\n externalId: resolvedExternalId,\n status: existed ? \"updated\" : \"created\",\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n logger.error(`Failed to push entry ${entry.externalId}: ${message}`);\n failed.push({\n externalId: entry.externalId,\n status: \"failed\",\n error: message,\n });\n }\n }\n\n return { success, failed };\n },\n\n async pullProgress(params: SyncPullRequest): Promise<SyncPullResponse> {\n if (!client || viewerId === null) {\n throw new Error(\"Plugin not initialized - call getUserInfo first\");\n }\n\n // Parse pagination cursor (page number)\n const page = params.cursor ? Number.parseInt(params.cursor, 10) : 1;\n const perPage = params.limit ? Math.min(params.limit, 50) : 50;\n\n const result = await client.getMangaList(viewerId, page, perPage);\n\n const entries: SyncEntry[] = result.entries.map((entry) => ({\n externalId: String(entry.mediaId),\n status: anilistStatusToSync(entry.status),\n progress: {\n chapters: entry.progress || undefined,\n volumes: entry.progressVolumes || undefined,\n },\n score: entry.score > 0 ? convertScoreFromAnilist(entry.score, scoreFormat) : undefined,\n startedAt: fuzzyDateToIso(entry.startedAt),\n completedAt: fuzzyDateToIso(entry.completedAt),\n notes: entry.notes || undefined,\n }));\n\n logger.info(\n `Pulled ${entries.length} entries (page ${result.pageInfo.currentPage}/${result.pageInfo.lastPage})`,\n );\n\n return {\n entries,\n nextCursor: result.pageInfo.hasNextPage ? String(result.pageInfo.currentPage + 1) : undefined,\n hasMore: result.pageInfo.hasNextPage,\n };\n },\n\n async status(): Promise<SyncStatusResponse> {\n if (!client || viewerId === null) {\n return {\n pendingPush: 0,\n pendingPull: 0,\n conflicts: 0,\n };\n }\n\n // Get total count from AniList\n const result = await client.getMangaList(viewerId, 1, 1);\n\n return {\n externalCount: result.pageInfo.total,\n pendingPush: 0,\n pendingPull: 0,\n conflicts: 0,\n };\n },\n};\n\n// =============================================================================\n// Plugin Initialization\n// =============================================================================\n\ncreateSyncPlugin({\n manifest,\n provider,\n logLevel: \"debug\",\n onInitialize(params: InitializeParams) {\n // Get access token from credentials\n const accessToken = params.credentials?.access_token;\n if (accessToken) {\n client = new AniListClient(accessToken);\n logger.info(\"AniList client initialized with access token\");\n } else {\n logger.warn(\"No access token provided - sync operations will fail\");\n }\n\n // Read plugin-specific config from userConfig\n const uc = params.userConfig;\n if (uc) {\n const unit = uc.progressUnit;\n if (unit === \"chapters\" || unit === \"volumes\") {\n progressUnit = unit;\n }\n if (typeof uc.pauseAfterDays === \"number\" && uc.pauseAfterDays >= 0) {\n pauseAfterDays = uc.pauseAfterDays;\n }\n if (typeof uc.dropAfterDays === \"number\" && uc.dropAfterDays >= 0) {\n dropAfterDays = uc.dropAfterDays;\n }\n if (typeof uc.searchFallback === \"boolean\") {\n searchFallback = uc.searchFallback;\n }\n logger.info(\n `Plugin config: progressUnit=${progressUnit}, pauseAfterDays=${pauseAfterDays}, dropAfterDays=${dropAfterDays}, searchFallback=${searchFallback}`,\n );\n }\n },\n});\n\nlogger.info(\"AniList sync plugin started\");\n"],
5
5
  "mappings": "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",
6
6
  "names": ["manifest", "logger", "response", "manifest", "provider", "success", "result"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashdev/codex-plugin-sync-anilist",
3
- "version": "1.10.0",
3
+ "version": "1.10.1",
4
4
  "description": "AniList reading progress sync plugin for Codex",
5
5
  "main": "dist/index.js",
6
6
  "bin": "dist/index.js",
@@ -40,7 +40,7 @@
40
40
  "node": ">=22.0.0"
41
41
  },
42
42
  "dependencies": {
43
- "@ashdev/codex-plugin-sdk": "^1.10.0"
43
+ "@ashdev/codex-plugin-sdk": "^1.10.1"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@biomejs/biome": "^2.3.13",