@ncukondo/reference-manager 0.24.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +53 -1
  2. package/dist/chunks/{SearchableMultiSelect-CKRfbnka.js → SearchableMultiSelect-D2IzthN4.js} +2 -2
  3. package/dist/chunks/{SearchableMultiSelect-CKRfbnka.js.map → SearchableMultiSelect-D2IzthN4.js.map} +1 -1
  4. package/dist/chunks/{action-menu-BCgv1m3v.js → action-menu-DWdoHTFh.js} +3 -3
  5. package/dist/chunks/{action-menu-BCgv1m3v.js.map → action-menu-DWdoHTFh.js.map} +1 -1
  6. package/dist/chunks/checker-CKfdG8Ia.js +170 -0
  7. package/dist/chunks/checker-CKfdG8Ia.js.map +1 -0
  8. package/dist/chunks/crossref-client-Gs75LMVf.js +94 -0
  9. package/dist/chunks/crossref-client-Gs75LMVf.js.map +1 -0
  10. package/dist/chunks/fix-interaction-DNXbmlPr.js +262 -0
  11. package/dist/chunks/fix-interaction-DNXbmlPr.js.map +1 -0
  12. package/dist/chunks/{index-JA964gjc.js → index-BEQ4YIXx.js} +356 -27
  13. package/dist/chunks/index-BEQ4YIXx.js.map +1 -0
  14. package/dist/chunks/{index-zafRwZEZ.js → index-k67fQbe4.js} +3 -3
  15. package/dist/chunks/index-k67fQbe4.js.map +1 -0
  16. package/dist/chunks/{index-DWAtvFtp.js → index-of6eJn8N.js} +209 -21
  17. package/dist/chunks/index-of6eJn8N.js.map +1 -0
  18. package/dist/chunks/{index-C36Us_zC.js → index-tdmbNN9b.js} +4 -4
  19. package/dist/chunks/{index-C36Us_zC.js.map → index-tdmbNN9b.js.map} +1 -1
  20. package/dist/chunks/{loader-CV71qNY2.js → loader-B-fte1uv.js} +19 -8
  21. package/dist/chunks/loader-B-fte1uv.js.map +1 -0
  22. package/dist/chunks/metadata-comparator-C5zfoYdK.js +137 -0
  23. package/dist/chunks/metadata-comparator-C5zfoYdK.js.map +1 -0
  24. package/dist/chunks/pubmed-client-CGReJIOz.js +51 -0
  25. package/dist/chunks/pubmed-client-CGReJIOz.js.map +1 -0
  26. package/dist/chunks/{reference-select-Bbl-roAS.js → reference-select-i1Cnmc16.js} +3 -3
  27. package/dist/chunks/{reference-select-Bbl-roAS.js.map → reference-select-i1Cnmc16.js.map} +1 -1
  28. package/dist/chunks/{style-select-BeAktUts.js → style-select-COnY01qb.js} +3 -3
  29. package/dist/chunks/{style-select-BeAktUts.js.map → style-select-COnY01qb.js.map} +1 -1
  30. package/dist/cli/commands/check.d.ts +43 -0
  31. package/dist/cli/commands/check.d.ts.map +1 -0
  32. package/dist/cli/commands/index.d.ts +2 -0
  33. package/dist/cli/commands/index.d.ts.map +1 -1
  34. package/dist/cli/index.d.ts.map +1 -1
  35. package/dist/cli/server-client.d.ts +2 -1
  36. package/dist/cli/server-client.d.ts.map +1 -1
  37. package/dist/cli.js +1 -1
  38. package/dist/config/defaults.d.ts.map +1 -1
  39. package/dist/config/env-override.d.ts.map +1 -1
  40. package/dist/config/key-parser.d.ts.map +1 -1
  41. package/dist/config/loader.d.ts.map +1 -1
  42. package/dist/config/schema.d.ts +3 -0
  43. package/dist/config/schema.d.ts.map +1 -1
  44. package/dist/features/check/checker.d.ts +19 -0
  45. package/dist/features/check/checker.d.ts.map +1 -0
  46. package/dist/features/check/crossref-client.d.ts +40 -0
  47. package/dist/features/check/crossref-client.d.ts.map +1 -0
  48. package/dist/features/check/fix-actions.d.ts +16 -0
  49. package/dist/features/check/fix-actions.d.ts.map +1 -0
  50. package/dist/features/check/fix-interaction.d.ts +11 -0
  51. package/dist/features/check/fix-interaction.d.ts.map +1 -0
  52. package/dist/features/check/metadata-comparator.d.ts +37 -0
  53. package/dist/features/check/metadata-comparator.d.ts.map +1 -0
  54. package/dist/features/check/metadata-similarity.d.ts +22 -0
  55. package/dist/features/check/metadata-similarity.d.ts.map +1 -0
  56. package/dist/features/check/pubmed-client.d.ts +20 -0
  57. package/dist/features/check/pubmed-client.d.ts.map +1 -0
  58. package/dist/features/check/types.d.ts +28 -0
  59. package/dist/features/check/types.d.ts.map +1 -0
  60. package/dist/features/operations/check.d.ts +29 -0
  61. package/dist/features/operations/check.d.ts.map +1 -0
  62. package/dist/features/operations/index.d.ts +1 -0
  63. package/dist/features/operations/index.d.ts.map +1 -1
  64. package/dist/features/operations/library-operations.d.ts +8 -0
  65. package/dist/features/operations/library-operations.d.ts.map +1 -1
  66. package/dist/features/operations/operations-library.d.ts +2 -0
  67. package/dist/features/operations/operations-library.d.ts.map +1 -1
  68. package/dist/index.js +1 -1
  69. package/dist/mcp/tools/check.d.ts +11 -0
  70. package/dist/mcp/tools/check.d.ts.map +1 -0
  71. package/dist/mcp/tools/index.d.ts.map +1 -1
  72. package/dist/server/index.d.ts.map +1 -1
  73. package/dist/server/routes/check.d.ts +4 -0
  74. package/dist/server/routes/check.d.ts.map +1 -0
  75. package/dist/server.js +3 -3
  76. package/package.json +1 -1
  77. package/dist/chunks/index-DWAtvFtp.js.map +0 -1
  78. package/dist/chunks/index-JA964gjc.js.map +0 -1
  79. package/dist/chunks/index-zafRwZEZ.js.map +0 -1
  80. package/dist/chunks/loader-CV71qNY2.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"action-menu-BCgv1m3v.js","sources":["../../src/features/interactive/action-menu.ts"],"sourcesContent":["/**\n * Action menu for interactive search mode.\n * Allows users to perform actions on selected references.\n */\n\nimport { render } from \"ink\";\nimport { createElement } from \"react\";\nimport type React from \"react\";\nimport { stringify as yamlStringify } from \"yaml\";\nimport type { CitationKeyFormat } from \"../../config/schema.js\";\nimport type { CslItem } from \"../../core/csl-json/types.js\";\nimport { formatBibliographyCSL, formatBibtex } from \"../format/index.js\";\nimport { restoreStdinAfterInk } from \"./alternate-screen.js\";\nimport { Select, type SelectOption } from \"./components/index.js\";\n\n/**\n * Action types available in the action menu.\n */\nexport type ActionType =\n | \"key-default\"\n | \"cite-default\"\n | \"cite-choose\"\n | \"open-url\"\n | \"open-fulltext\"\n | \"manage-attachments\"\n | \"edit\"\n | \"output-format\"\n | \"remove\"\n | \"cancel\";\n\n/**\n * Output format types for the output format submenu.\n */\nexport type OutputFormatType =\n | \"output-ids\"\n | \"output-csl-json\"\n | \"output-bibtex\"\n | \"output-yaml\"\n | \"cancel\";\n\n/**\n * Side-effect action types that perform operations rather than producing stdout output.\n */\nconst SIDE_EFFECT_ACTIONS: ReadonlySet<ActionType> = new Set([\n \"open-url\",\n \"open-fulltext\",\n \"manage-attachments\",\n \"edit\",\n \"remove\",\n]);\n\n/**\n * Check if an action is a side-effect action.\n */\nexport function isSideEffectAction(action: ActionType): boolean {\n return SIDE_EFFECT_ACTIONS.has(action);\n}\n\n/**\n * Result from action menu selection.\n */\nexport interface ActionMenuResult {\n /** Selected action type */\n action: ActionType;\n /** Generated output (empty for cancel and side-effect actions) */\n output: string;\n /** Whether the prompt was cancelled */\n cancelled: boolean;\n /** Selected items (for side-effect actions) */\n selectedItems?: CslItem[];\n}\n\n/**\n * Result from style selection prompt.\n */\nexport interface StyleSelectResult {\n /** Selected style (undefined if cancelled) */\n style?: string;\n /** Whether the prompt was cancelled */\n cancelled: boolean;\n}\n\n/**\n * Config options for action choices.\n */\nexport interface ActionChoicesConfig {\n defaultKeyFormat?: CitationKeyFormat | undefined;\n}\n\n/**\n * Config options for output generation.\n */\nexport interface GenerateOutputConfig {\n defaultKeyFormat?: CitationKeyFormat | undefined;\n defaultStyle?: string | undefined;\n}\n\n/**\n * Generate action choices for the action menu.\n * Returns different choices based on the number of selected entries.\n */\nexport function getActionChoices(\n count: number,\n config: ActionChoicesConfig = {}\n): SelectOption<ActionType>[] {\n const { defaultKeyFormat = \"pandoc\" } = config;\n const isSingle = count === 1;\n const formatLabel = defaultKeyFormat === \"latex\" ? \"LaTeX\" : \"Pandoc\";\n\n const keyLabel = isSingle ? `Citation key (${formatLabel})` : `Citation keys (${formatLabel})`;\n\n const choices: SelectOption<ActionType>[] = [\n { label: keyLabel, value: \"key-default\" },\n { label: \"Generate citation\", value: \"cite-default\" },\n { label: \"Generate citation (choose style)\", value: \"cite-choose\" },\n ];\n\n if (isSingle) {\n choices.push(\n { label: \"Open URL\", value: \"open-url\" },\n { label: \"Open fulltext\", value: \"open-fulltext\" },\n { label: \"Manage attachments\", value: \"manage-attachments\" }\n );\n }\n\n choices.push(\n { label: isSingle ? \"Edit reference\" : \"Edit references\", value: \"edit\" },\n { label: \"Output (choose format)\", value: \"output-format\" },\n { label: \"Remove\", value: \"remove\" },\n { label: \"Cancel\", value: \"cancel\" }\n );\n\n return choices;\n}\n\n/**\n * Available action choices for the action menu (default, single entry).\n * @deprecated Use getActionChoices(count, config) for dynamic choices.\n */\nexport const ACTION_CHOICES: SelectOption<ActionType>[] = getActionChoices(1);\n\n/**\n * Output format choices for the output format submenu.\n */\nexport const OUTPUT_FORMAT_CHOICES: SelectOption<OutputFormatType>[] = [\n { label: \"IDs (citation keys)\", value: \"output-ids\" },\n { label: \"CSL-JSON\", value: \"output-csl-json\" },\n { label: \"BibTeX\", value: \"output-bibtex\" },\n { label: \"YAML\", value: \"output-yaml\" },\n { label: \"Cancel\", value: \"cancel\" },\n];\n\n/**\n * Available style choices for citation style selection.\n */\nexport const STYLE_CHOICES: SelectOption<string>[] = [\n { label: \"APA\", value: \"apa\" },\n { label: \"Vancouver\", value: \"vancouver\" },\n { label: \"Harvard\", value: \"harvard\" },\n];\n\n/**\n * Props for the ActionMenuApp component\n */\ninterface ActionMenuAppProps {\n message: string;\n options: SelectOption<ActionType>[];\n onSelect: (value: ActionType) => void;\n onCancel: () => void;\n}\n\n/**\n * ActionMenuApp component - wraps Select for action menu\n */\nfunction ActionMenuApp({\n message,\n options,\n onSelect,\n onCancel,\n}: ActionMenuAppProps): React.ReactElement {\n return createElement(Select<ActionType>, {\n options,\n message,\n onSelect,\n onCancel,\n });\n}\n\n/**\n * Props for the StyleSelectApp component\n */\ninterface StyleSelectAppProps {\n options: SelectOption<string>[];\n onSelect: (value: string) => void;\n onCancel: () => void;\n}\n\n/**\n * StyleSelectApp component - wraps Select for style selection\n */\nfunction StyleSelectApp({ options, onSelect, onCancel }: StyleSelectAppProps): React.ReactElement {\n return createElement(Select<string>, {\n options,\n message: \"Select citation style:\",\n onSelect,\n onCancel,\n });\n}\n\n/**\n * Generate output for the given action and items.\n */\nexport function generateOutput(\n action: ActionType | OutputFormatType,\n items: CslItem[],\n config: GenerateOutputConfig = {}\n): string {\n const { defaultKeyFormat = \"pandoc\", defaultStyle = \"apa\" } = config;\n\n switch (action) {\n case \"output-ids\":\n return items.map((item) => item.id).join(\"\\n\");\n\n case \"output-csl-json\":\n return JSON.stringify(items, null, 2);\n\n case \"output-bibtex\":\n return formatBibtex(items);\n\n case \"output-yaml\":\n return yamlStringify(items).trimEnd();\n\n // cite-default uses config.defaultStyle; cite-choose has its style\n // overridden by the caller (processAction/SearchFlowApp) before reaching here.\n case \"cite-default\":\n case \"cite-choose\":\n return formatBibliographyCSL(items, { style: defaultStyle });\n\n case \"key-default\":\n if (defaultKeyFormat === \"latex\") {\n return `\\\\cite{${items.map((i) => i.id).join(\",\")}}`;\n }\n return items.map((i) => `@${i.id}`).join(\"; \");\n\n case \"cancel\":\n return \"\";\n\n default:\n return \"\";\n }\n}\n\n/**\n * Run the style selection prompt.\n */\nexport async function runStyleSelectPrompt(): Promise<StyleSelectResult> {\n return new Promise<StyleSelectResult>((resolve) => {\n let result: StyleSelectResult = { cancelled: true };\n\n const handleSelect = (value: string): void => {\n result = {\n style: value,\n cancelled: false,\n };\n };\n\n const handleCancel = (): void => {\n result = {\n cancelled: true,\n };\n };\n\n // Render the Ink app\n const { waitUntilExit } = render(\n createElement(StyleSelectApp, {\n options: STYLE_CHOICES,\n onSelect: handleSelect,\n onCancel: handleCancel,\n })\n );\n\n // Wait for the app to exit, then resolve\n waitUntilExit()\n .then(() => {\n restoreStdinAfterInk();\n resolve(result);\n })\n .catch(() => {\n restoreStdinAfterInk();\n resolve({\n cancelled: true,\n });\n });\n });\n}\n\n/**\n * Process the selected action and generate result.\n */\nasync function processAction(\n action: ActionType,\n items: CslItem[],\n config: GenerateOutputConfig = {}\n): Promise<ActionMenuResult> {\n // Handle cite-choose: prompt for style first\n if (action === \"cite-choose\") {\n const styleResult = await runStyleSelectPrompt();\n if (styleResult.cancelled) {\n return {\n action: \"cancel\",\n output: \"\",\n cancelled: true,\n };\n }\n return {\n action,\n output: generateOutput(action, items, {\n ...config,\n defaultStyle: styleResult.style,\n }),\n cancelled: false,\n };\n }\n\n // Handle cancel\n if (action === \"cancel\") {\n return {\n action,\n output: \"\",\n cancelled: true,\n };\n }\n\n // Handle side-effect actions\n if (isSideEffectAction(action)) {\n return {\n action,\n output: \"\",\n cancelled: false,\n selectedItems: items,\n };\n }\n\n // Handle other actions\n return {\n action,\n output: generateOutput(action, items, config),\n cancelled: false,\n };\n}\n\n/**\n * Run the action menu for selected references.\n *\n * @param items - Selected references\n * @param config - Output generation config\n * @returns Action result with output\n */\nexport async function runActionMenu(\n items: CslItem[],\n config: GenerateOutputConfig = {}\n): Promise<ActionMenuResult> {\n const count = items.length;\n const refWord = count === 1 ? \"reference\" : \"references\";\n const message = `Action for ${count} selected ${refWord}:`;\n const actionChoices = getActionChoices(count, {\n defaultKeyFormat: config.defaultKeyFormat,\n });\n\n return new Promise<ActionMenuResult>((resolve) => {\n let selectedAction: ActionType | null = null;\n\n const handleSelect = (action: ActionType): void => {\n selectedAction = action;\n };\n\n const handleCancel = (): void => {\n selectedAction = null;\n };\n\n // Render the Ink app\n const { waitUntilExit } = render(\n createElement(ActionMenuApp, {\n message,\n options: actionChoices,\n onSelect: handleSelect,\n onCancel: handleCancel,\n })\n );\n\n // Wait for the app to exit, then process the action\n waitUntilExit()\n .then(async () => {\n restoreStdinAfterInk();\n\n if (selectedAction === null) {\n resolve({\n action: \"cancel\",\n output: \"\",\n cancelled: true,\n });\n } else {\n const result = await processAction(selectedAction, items, config);\n resolve(result);\n }\n })\n .catch(() => {\n restoreStdinAfterInk();\n resolve({\n action: \"cancel\",\n output: \"\",\n cancelled: true,\n });\n });\n });\n}\n"],"names":["yamlStringify"],"mappings":";;;;;AA2CA,MAAM,0CAAmD,IAAI;AAAA,EAC3D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAKM,SAAS,mBAAmB,QAA6B;AAC9D,SAAO,oBAAoB,IAAI,MAAM;AACvC;AA6CO,SAAS,iBACd,OACA,SAA8B,IACF;AAC5B,QAAM,EAAE,mBAAmB,SAAA,IAAa;AACxC,QAAM,WAAW,UAAU;AAC3B,QAAM,cAAc,qBAAqB,UAAU,UAAU;AAE7D,QAAM,WAAW,WAAW,iBAAiB,WAAW,MAAM,kBAAkB,WAAW;AAE3F,QAAM,UAAsC;AAAA,IAC1C,EAAE,OAAO,UAAU,OAAO,cAAA;AAAA,IAC1B,EAAE,OAAO,qBAAqB,OAAO,eAAA;AAAA,IACrC,EAAE,OAAO,oCAAoC,OAAO,cAAA;AAAA,EAAc;AAGpE,MAAI,UAAU;AACZ,YAAQ;AAAA,MACN,EAAE,OAAO,YAAY,OAAO,WAAA;AAAA,MAC5B,EAAE,OAAO,iBAAiB,OAAO,gBAAA;AAAA,MACjC,EAAE,OAAO,sBAAsB,OAAO,qBAAA;AAAA,IAAqB;AAAA,EAE/D;AAEA,UAAQ;AAAA,IACN,EAAE,OAAO,WAAW,mBAAmB,mBAAmB,OAAO,OAAA;AAAA,IACjE,EAAE,OAAO,0BAA0B,OAAO,gBAAA;AAAA,IAC1C,EAAE,OAAO,UAAU,OAAO,SAAA;AAAA,IAC1B,EAAE,OAAO,UAAU,OAAO,SAAA;AAAA,EAAS;AAGrC,SAAO;AACT;AAM0D,iBAAiB,CAAC;AAKrE,MAAM,wBAA0D;AAAA,EACrE,EAAE,OAAO,uBAAuB,OAAO,aAAA;AAAA,EACvC,EAAE,OAAO,YAAY,OAAO,kBAAA;AAAA,EAC5B,EAAE,OAAO,UAAU,OAAO,gBAAA;AAAA,EAC1B,EAAE,OAAO,QAAQ,OAAO,cAAA;AAAA,EACxB,EAAE,OAAO,UAAU,OAAO,SAAA;AAC5B;AAKO,MAAM,gBAAwC;AAAA,EACnD,EAAE,OAAO,OAAO,OAAO,MAAA;AAAA,EACvB,EAAE,OAAO,aAAa,OAAO,YAAA;AAAA,EAC7B,EAAE,OAAO,WAAW,OAAO,UAAA;AAC7B;AAqDO,SAAS,eACd,QACA,OACA,SAA+B,CAAA,GACvB;AACR,QAAM,EAAE,mBAAmB,UAAU,eAAe,UAAU;AAE9D,UAAQ,QAAA;AAAA,IACN,KAAK;AACH,aAAO,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE,EAAE,KAAK,IAAI;AAAA,IAE/C,KAAK;AACH,aAAO,KAAK,UAAU,OAAO,MAAM,CAAC;AAAA,IAEtC,KAAK;AACH,aAAO,aAAa,KAAK;AAAA,IAE3B,KAAK;AACH,aAAOA,UAAc,KAAK,EAAE,QAAA;AAAA;AAAA;AAAA,IAI9B,KAAK;AAAA,IACL,KAAK;AACH,aAAO,sBAAsB,OAAO,EAAE,OAAO,cAAc;AAAA,IAE7D,KAAK;AACH,UAAI,qBAAqB,SAAS;AAChC,eAAO,UAAU,MAAM,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,KAAK,GAAG,CAAC;AAAA,MACnD;AACA,aAAO,MAAM,IAAI,CAAC,MAAM,IAAI,EAAE,EAAE,EAAE,EAAE,KAAK,IAAI;AAAA,IAE/C,KAAK;AACH,aAAO;AAAA,IAET;AACE,aAAO;AAAA,EAAA;AAEb;"}
1
+ {"version":3,"file":"action-menu-DWdoHTFh.js","sources":["../../src/features/interactive/action-menu.ts"],"sourcesContent":["/**\n * Action menu for interactive search mode.\n * Allows users to perform actions on selected references.\n */\n\nimport { render } from \"ink\";\nimport { createElement } from \"react\";\nimport type React from \"react\";\nimport { stringify as yamlStringify } from \"yaml\";\nimport type { CitationKeyFormat } from \"../../config/schema.js\";\nimport type { CslItem } from \"../../core/csl-json/types.js\";\nimport { formatBibliographyCSL, formatBibtex } from \"../format/index.js\";\nimport { restoreStdinAfterInk } from \"./alternate-screen.js\";\nimport { Select, type SelectOption } from \"./components/index.js\";\n\n/**\n * Action types available in the action menu.\n */\nexport type ActionType =\n | \"key-default\"\n | \"cite-default\"\n | \"cite-choose\"\n | \"open-url\"\n | \"open-fulltext\"\n | \"manage-attachments\"\n | \"edit\"\n | \"output-format\"\n | \"remove\"\n | \"cancel\";\n\n/**\n * Output format types for the output format submenu.\n */\nexport type OutputFormatType =\n | \"output-ids\"\n | \"output-csl-json\"\n | \"output-bibtex\"\n | \"output-yaml\"\n | \"cancel\";\n\n/**\n * Side-effect action types that perform operations rather than producing stdout output.\n */\nconst SIDE_EFFECT_ACTIONS: ReadonlySet<ActionType> = new Set([\n \"open-url\",\n \"open-fulltext\",\n \"manage-attachments\",\n \"edit\",\n \"remove\",\n]);\n\n/**\n * Check if an action is a side-effect action.\n */\nexport function isSideEffectAction(action: ActionType): boolean {\n return SIDE_EFFECT_ACTIONS.has(action);\n}\n\n/**\n * Result from action menu selection.\n */\nexport interface ActionMenuResult {\n /** Selected action type */\n action: ActionType;\n /** Generated output (empty for cancel and side-effect actions) */\n output: string;\n /** Whether the prompt was cancelled */\n cancelled: boolean;\n /** Selected items (for side-effect actions) */\n selectedItems?: CslItem[];\n}\n\n/**\n * Result from style selection prompt.\n */\nexport interface StyleSelectResult {\n /** Selected style (undefined if cancelled) */\n style?: string;\n /** Whether the prompt was cancelled */\n cancelled: boolean;\n}\n\n/**\n * Config options for action choices.\n */\nexport interface ActionChoicesConfig {\n defaultKeyFormat?: CitationKeyFormat | undefined;\n}\n\n/**\n * Config options for output generation.\n */\nexport interface GenerateOutputConfig {\n defaultKeyFormat?: CitationKeyFormat | undefined;\n defaultStyle?: string | undefined;\n}\n\n/**\n * Generate action choices for the action menu.\n * Returns different choices based on the number of selected entries.\n */\nexport function getActionChoices(\n count: number,\n config: ActionChoicesConfig = {}\n): SelectOption<ActionType>[] {\n const { defaultKeyFormat = \"pandoc\" } = config;\n const isSingle = count === 1;\n const formatLabel = defaultKeyFormat === \"latex\" ? \"LaTeX\" : \"Pandoc\";\n\n const keyLabel = isSingle ? `Citation key (${formatLabel})` : `Citation keys (${formatLabel})`;\n\n const choices: SelectOption<ActionType>[] = [\n { label: keyLabel, value: \"key-default\" },\n { label: \"Generate citation\", value: \"cite-default\" },\n { label: \"Generate citation (choose style)\", value: \"cite-choose\" },\n ];\n\n if (isSingle) {\n choices.push(\n { label: \"Open URL\", value: \"open-url\" },\n { label: \"Open fulltext\", value: \"open-fulltext\" },\n { label: \"Manage attachments\", value: \"manage-attachments\" }\n );\n }\n\n choices.push(\n { label: isSingle ? \"Edit reference\" : \"Edit references\", value: \"edit\" },\n { label: \"Output (choose format)\", value: \"output-format\" },\n { label: \"Remove\", value: \"remove\" },\n { label: \"Cancel\", value: \"cancel\" }\n );\n\n return choices;\n}\n\n/**\n * Available action choices for the action menu (default, single entry).\n * @deprecated Use getActionChoices(count, config) for dynamic choices.\n */\nexport const ACTION_CHOICES: SelectOption<ActionType>[] = getActionChoices(1);\n\n/**\n * Output format choices for the output format submenu.\n */\nexport const OUTPUT_FORMAT_CHOICES: SelectOption<OutputFormatType>[] = [\n { label: \"IDs (citation keys)\", value: \"output-ids\" },\n { label: \"CSL-JSON\", value: \"output-csl-json\" },\n { label: \"BibTeX\", value: \"output-bibtex\" },\n { label: \"YAML\", value: \"output-yaml\" },\n { label: \"Cancel\", value: \"cancel\" },\n];\n\n/**\n * Available style choices for citation style selection.\n */\nexport const STYLE_CHOICES: SelectOption<string>[] = [\n { label: \"APA\", value: \"apa\" },\n { label: \"Vancouver\", value: \"vancouver\" },\n { label: \"Harvard\", value: \"harvard\" },\n];\n\n/**\n * Props for the ActionMenuApp component\n */\ninterface ActionMenuAppProps {\n message: string;\n options: SelectOption<ActionType>[];\n onSelect: (value: ActionType) => void;\n onCancel: () => void;\n}\n\n/**\n * ActionMenuApp component - wraps Select for action menu\n */\nfunction ActionMenuApp({\n message,\n options,\n onSelect,\n onCancel,\n}: ActionMenuAppProps): React.ReactElement {\n return createElement(Select<ActionType>, {\n options,\n message,\n onSelect,\n onCancel,\n });\n}\n\n/**\n * Props for the StyleSelectApp component\n */\ninterface StyleSelectAppProps {\n options: SelectOption<string>[];\n onSelect: (value: string) => void;\n onCancel: () => void;\n}\n\n/**\n * StyleSelectApp component - wraps Select for style selection\n */\nfunction StyleSelectApp({ options, onSelect, onCancel }: StyleSelectAppProps): React.ReactElement {\n return createElement(Select<string>, {\n options,\n message: \"Select citation style:\",\n onSelect,\n onCancel,\n });\n}\n\n/**\n * Generate output for the given action and items.\n */\nexport function generateOutput(\n action: ActionType | OutputFormatType,\n items: CslItem[],\n config: GenerateOutputConfig = {}\n): string {\n const { defaultKeyFormat = \"pandoc\", defaultStyle = \"apa\" } = config;\n\n switch (action) {\n case \"output-ids\":\n return items.map((item) => item.id).join(\"\\n\");\n\n case \"output-csl-json\":\n return JSON.stringify(items, null, 2);\n\n case \"output-bibtex\":\n return formatBibtex(items);\n\n case \"output-yaml\":\n return yamlStringify(items).trimEnd();\n\n // cite-default uses config.defaultStyle; cite-choose has its style\n // overridden by the caller (processAction/SearchFlowApp) before reaching here.\n case \"cite-default\":\n case \"cite-choose\":\n return formatBibliographyCSL(items, { style: defaultStyle });\n\n case \"key-default\":\n if (defaultKeyFormat === \"latex\") {\n return `\\\\cite{${items.map((i) => i.id).join(\",\")}}`;\n }\n return items.map((i) => `@${i.id}`).join(\"; \");\n\n case \"cancel\":\n return \"\";\n\n default:\n return \"\";\n }\n}\n\n/**\n * Run the style selection prompt.\n */\nexport async function runStyleSelectPrompt(): Promise<StyleSelectResult> {\n return new Promise<StyleSelectResult>((resolve) => {\n let result: StyleSelectResult = { cancelled: true };\n\n const handleSelect = (value: string): void => {\n result = {\n style: value,\n cancelled: false,\n };\n };\n\n const handleCancel = (): void => {\n result = {\n cancelled: true,\n };\n };\n\n // Render the Ink app\n const { waitUntilExit } = render(\n createElement(StyleSelectApp, {\n options: STYLE_CHOICES,\n onSelect: handleSelect,\n onCancel: handleCancel,\n })\n );\n\n // Wait for the app to exit, then resolve\n waitUntilExit()\n .then(() => {\n restoreStdinAfterInk();\n resolve(result);\n })\n .catch(() => {\n restoreStdinAfterInk();\n resolve({\n cancelled: true,\n });\n });\n });\n}\n\n/**\n * Process the selected action and generate result.\n */\nasync function processAction(\n action: ActionType,\n items: CslItem[],\n config: GenerateOutputConfig = {}\n): Promise<ActionMenuResult> {\n // Handle cite-choose: prompt for style first\n if (action === \"cite-choose\") {\n const styleResult = await runStyleSelectPrompt();\n if (styleResult.cancelled) {\n return {\n action: \"cancel\",\n output: \"\",\n cancelled: true,\n };\n }\n return {\n action,\n output: generateOutput(action, items, {\n ...config,\n defaultStyle: styleResult.style,\n }),\n cancelled: false,\n };\n }\n\n // Handle cancel\n if (action === \"cancel\") {\n return {\n action,\n output: \"\",\n cancelled: true,\n };\n }\n\n // Handle side-effect actions\n if (isSideEffectAction(action)) {\n return {\n action,\n output: \"\",\n cancelled: false,\n selectedItems: items,\n };\n }\n\n // Handle other actions\n return {\n action,\n output: generateOutput(action, items, config),\n cancelled: false,\n };\n}\n\n/**\n * Run the action menu for selected references.\n *\n * @param items - Selected references\n * @param config - Output generation config\n * @returns Action result with output\n */\nexport async function runActionMenu(\n items: CslItem[],\n config: GenerateOutputConfig = {}\n): Promise<ActionMenuResult> {\n const count = items.length;\n const refWord = count === 1 ? \"reference\" : \"references\";\n const message = `Action for ${count} selected ${refWord}:`;\n const actionChoices = getActionChoices(count, {\n defaultKeyFormat: config.defaultKeyFormat,\n });\n\n return new Promise<ActionMenuResult>((resolve) => {\n let selectedAction: ActionType | null = null;\n\n const handleSelect = (action: ActionType): void => {\n selectedAction = action;\n };\n\n const handleCancel = (): void => {\n selectedAction = null;\n };\n\n // Render the Ink app\n const { waitUntilExit } = render(\n createElement(ActionMenuApp, {\n message,\n options: actionChoices,\n onSelect: handleSelect,\n onCancel: handleCancel,\n })\n );\n\n // Wait for the app to exit, then process the action\n waitUntilExit()\n .then(async () => {\n restoreStdinAfterInk();\n\n if (selectedAction === null) {\n resolve({\n action: \"cancel\",\n output: \"\",\n cancelled: true,\n });\n } else {\n const result = await processAction(selectedAction, items, config);\n resolve(result);\n }\n })\n .catch(() => {\n restoreStdinAfterInk();\n resolve({\n action: \"cancel\",\n output: \"\",\n cancelled: true,\n });\n });\n });\n}\n"],"names":["yamlStringify"],"mappings":";;;;;AA2CA,MAAM,0CAAmD,IAAI;AAAA,EAC3D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAKM,SAAS,mBAAmB,QAA6B;AAC9D,SAAO,oBAAoB,IAAI,MAAM;AACvC;AA6CO,SAAS,iBACd,OACA,SAA8B,IACF;AAC5B,QAAM,EAAE,mBAAmB,SAAA,IAAa;AACxC,QAAM,WAAW,UAAU;AAC3B,QAAM,cAAc,qBAAqB,UAAU,UAAU;AAE7D,QAAM,WAAW,WAAW,iBAAiB,WAAW,MAAM,kBAAkB,WAAW;AAE3F,QAAM,UAAsC;AAAA,IAC1C,EAAE,OAAO,UAAU,OAAO,cAAA;AAAA,IAC1B,EAAE,OAAO,qBAAqB,OAAO,eAAA;AAAA,IACrC,EAAE,OAAO,oCAAoC,OAAO,cAAA;AAAA,EAAc;AAGpE,MAAI,UAAU;AACZ,YAAQ;AAAA,MACN,EAAE,OAAO,YAAY,OAAO,WAAA;AAAA,MAC5B,EAAE,OAAO,iBAAiB,OAAO,gBAAA;AAAA,MACjC,EAAE,OAAO,sBAAsB,OAAO,qBAAA;AAAA,IAAqB;AAAA,EAE/D;AAEA,UAAQ;AAAA,IACN,EAAE,OAAO,WAAW,mBAAmB,mBAAmB,OAAO,OAAA;AAAA,IACjE,EAAE,OAAO,0BAA0B,OAAO,gBAAA;AAAA,IAC1C,EAAE,OAAO,UAAU,OAAO,SAAA;AAAA,IAC1B,EAAE,OAAO,UAAU,OAAO,SAAA;AAAA,EAAS;AAGrC,SAAO;AACT;AAM0D,iBAAiB,CAAC;AAKrE,MAAM,wBAA0D;AAAA,EACrE,EAAE,OAAO,uBAAuB,OAAO,aAAA;AAAA,EACvC,EAAE,OAAO,YAAY,OAAO,kBAAA;AAAA,EAC5B,EAAE,OAAO,UAAU,OAAO,gBAAA;AAAA,EAC1B,EAAE,OAAO,QAAQ,OAAO,cAAA;AAAA,EACxB,EAAE,OAAO,UAAU,OAAO,SAAA;AAC5B;AAKO,MAAM,gBAAwC;AAAA,EACnD,EAAE,OAAO,OAAO,OAAO,MAAA;AAAA,EACvB,EAAE,OAAO,aAAa,OAAO,YAAA;AAAA,EAC7B,EAAE,OAAO,WAAW,OAAO,UAAA;AAC7B;AAqDO,SAAS,eACd,QACA,OACA,SAA+B,CAAA,GACvB;AACR,QAAM,EAAE,mBAAmB,UAAU,eAAe,UAAU;AAE9D,UAAQ,QAAA;AAAA,IACN,KAAK;AACH,aAAO,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE,EAAE,KAAK,IAAI;AAAA,IAE/C,KAAK;AACH,aAAO,KAAK,UAAU,OAAO,MAAM,CAAC;AAAA,IAEtC,KAAK;AACH,aAAO,aAAa,KAAK;AAAA,IAE3B,KAAK;AACH,aAAOA,UAAc,KAAK,EAAE,QAAA;AAAA;AAAA;AAAA,IAI9B,KAAK;AAAA,IACL,KAAK;AACH,aAAO,sBAAsB,OAAO,EAAE,OAAO,cAAc;AAAA,IAE7D,KAAK;AACH,UAAI,qBAAqB,SAAS;AAChC,eAAO,UAAU,MAAM,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,KAAK,GAAG,CAAC;AAAA,MACnD;AACA,aAAO,MAAM,IAAI,CAAC,MAAM,IAAI,EAAE,EAAE,EAAE,EAAE,KAAK,IAAI;AAAA,IAE/C,KAAK;AACH,aAAO;AAAA,IAET;AACE,aAAO;AAAA,EAAA;AAEb;"}
@@ -0,0 +1,170 @@
1
+ async function checkReference(item, config) {
2
+ const id = item.id;
3
+ const uuid = item.custom?.uuid ?? "";
4
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
5
+ const findings = [];
6
+ const checkedSources = [];
7
+ const hasDoi = !!item.DOI;
8
+ const hasPmid = !!item.PMID;
9
+ if (!hasDoi && !hasPmid) {
10
+ return { id, uuid, status: "skipped", findings: [], checkedAt, checkedSources: [] };
11
+ }
12
+ let crossrefMetadata;
13
+ if (hasDoi) {
14
+ checkedSources.push("crossref");
15
+ const crossrefResult = await checkCrossref(item.DOI, config);
16
+ findings.push(...crossrefResult.findings);
17
+ crossrefMetadata = crossrefResult.metadata;
18
+ }
19
+ if (hasPmid) {
20
+ checkedSources.push("pubmed");
21
+ const pubmedFindings = await checkPubmed(item.PMID, config);
22
+ addUniqueFindings(findings, pubmedFindings);
23
+ }
24
+ const metadataFinding = await checkMetadata(item, config, crossrefMetadata, hasPmid, hasDoi);
25
+ if (metadataFinding) {
26
+ findings.push(metadataFinding);
27
+ }
28
+ const status = findings.length > 0 ? "warning" : "ok";
29
+ return { id, uuid, status, findings, checkedAt, checkedSources };
30
+ }
31
+ function addUniqueFindings(target, source) {
32
+ for (const finding of source) {
33
+ if (!target.some((f) => f.type === finding.type)) {
34
+ target.push(finding);
35
+ }
36
+ }
37
+ }
38
+ async function checkMetadata(item, config, crossrefMetadata, hasPmid, hasDoi) {
39
+ if (config?.metadata === false) return null;
40
+ if (crossrefMetadata) {
41
+ return compareItemMetadata(item, crossrefMetadata);
42
+ }
43
+ if (hasPmid && !hasDoi) {
44
+ return comparePubmedMetadata(item, config);
45
+ }
46
+ return null;
47
+ }
48
+ async function checkCrossref(doi, config) {
49
+ const { queryCrossref } = await import("./crossref-client-Gs75LMVf.js");
50
+ const crossrefConfig = config?.email ? { email: config.email } : void 0;
51
+ const result = await queryCrossref(doi, crossrefConfig);
52
+ if (!result.success) return { findings: [] };
53
+ const findings = [];
54
+ for (const update of result.updates) {
55
+ const finding = mapCrossrefUpdate(update);
56
+ if (finding) {
57
+ findings.push(finding);
58
+ }
59
+ }
60
+ return result.metadata ? { findings, metadata: result.metadata } : { findings };
61
+ }
62
+ async function compareItemMetadata(item, remoteMetadata) {
63
+ const { compareMetadata } = await import("./metadata-comparator-C5zfoYdK.js");
64
+ const local = extractLocalMetadata(item);
65
+ const comparison = compareMetadata(local, remoteMetadata);
66
+ if (comparison.classification === "no_change") return null;
67
+ const type = comparison.classification;
68
+ const message = type === "metadata_mismatch" ? "Local metadata significantly differs from the remote record" : "Remote metadata has been updated since import";
69
+ return {
70
+ type,
71
+ message,
72
+ details: {
73
+ updatedFields: comparison.changedFields,
74
+ fieldDiffs: comparison.fieldDiffs
75
+ }
76
+ };
77
+ }
78
+ async function comparePubmedMetadata(item, config) {
79
+ const { fetchPmids } = await import("./index-of6eJn8N.js").then((n) => n.y);
80
+ const pubmedConfig = config?.pubmed ?? {};
81
+ const results = await fetchPmids([item.PMID], pubmedConfig);
82
+ const result = results[0];
83
+ if (!result || !result.success) {
84
+ console.error(
85
+ `PubMed metadata fetch failed for PMID ${item.PMID}: ${result?.error ?? "unknown error"}`
86
+ );
87
+ return null;
88
+ }
89
+ const remoteMetadata = cslItemToRemoteMetadata(result.item);
90
+ return compareItemMetadata(item, remoteMetadata);
91
+ }
92
+ function cslItemToRemoteMetadata(item) {
93
+ const metadata = {};
94
+ if (item.title !== void 0) metadata.title = item.title;
95
+ if (item.author !== void 0) {
96
+ metadata.author = item.author;
97
+ }
98
+ if (item["container-title"] !== void 0) metadata.containerTitle = item["container-title"];
99
+ if (item.type !== void 0) metadata.type = item.type;
100
+ if (item.page !== void 0) metadata.page = item.page;
101
+ if (item.volume !== void 0) metadata.volume = item.volume;
102
+ if (item.issue !== void 0) metadata.issue = item.issue;
103
+ if (item.issued !== void 0) {
104
+ metadata.issued = item.issued;
105
+ }
106
+ return metadata;
107
+ }
108
+ function extractLocalMetadata(item) {
109
+ const local = {};
110
+ if (item.title !== void 0) local.title = item.title;
111
+ if (item.author !== void 0)
112
+ local.author = item.author;
113
+ if (item["container-title"] !== void 0) local["container-title"] = item["container-title"];
114
+ if (item.type !== void 0) local.type = item.type;
115
+ if (item.page !== void 0) local.page = item.page;
116
+ if (item.volume !== void 0) local.volume = item.volume;
117
+ if (item.issue !== void 0) local.issue = item.issue;
118
+ if (item.issued !== void 0) local.issued = item.issued;
119
+ return local;
120
+ }
121
+ async function checkPubmed(pmid, config) {
122
+ const { queryPubmed } = await import("./pubmed-client-CGReJIOz.js");
123
+ const result = await queryPubmed(pmid, config?.pubmed);
124
+ if (!result.success) return [];
125
+ const findings = [];
126
+ if (result.isRetracted) {
127
+ findings.push({
128
+ type: "retracted",
129
+ message: "This article is marked as retracted in PubMed"
130
+ });
131
+ }
132
+ if (result.hasConcern) {
133
+ findings.push({
134
+ type: "concern",
135
+ message: "Expression of concern noted in PubMed"
136
+ });
137
+ }
138
+ return findings;
139
+ }
140
+ function mapCrossrefUpdate(update) {
141
+ const doiDetail = update.doi ? { retractionDoi: update.doi } : {};
142
+ const dateDetail = update.date ? { retractionDate: update.date } : {};
143
+ const newDoiDetail = update.doi ? { newDoi: update.doi } : {};
144
+ switch (update.type) {
145
+ case "retraction":
146
+ return {
147
+ type: "retracted",
148
+ message: update.date ? `This article was retracted on ${update.date}` : "This article was retracted",
149
+ details: { ...doiDetail, ...dateDetail }
150
+ };
151
+ case "expression-of-concern":
152
+ return {
153
+ type: "concern",
154
+ message: update.date ? `Expression of concern issued on ${update.date}` : "Expression of concern issued",
155
+ details: { ...doiDetail, ...dateDetail }
156
+ };
157
+ case "new_version":
158
+ return {
159
+ type: "version_changed",
160
+ message: update.doi ? `Published version available: ${update.doi}` : "Published version available",
161
+ details: newDoiDetail
162
+ };
163
+ default:
164
+ return null;
165
+ }
166
+ }
167
+ export {
168
+ checkReference
169
+ };
170
+ //# sourceMappingURL=checker-CKfdG8Ia.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checker-CKfdG8Ia.js","sources":["../../src/features/check/checker.ts"],"sourcesContent":["import type { CslItem } from \"../../core/csl-json/types.js\";\nimport type { CrossrefUpdateInfo, RemoteMetadata } from \"./crossref-client.js\";\nimport type { CheckFinding, CheckResult } from \"./types.js\";\n\nexport interface CheckConfig {\n email?: string;\n pubmed?: { email?: string; apiKey?: string };\n metadata?: boolean;\n}\n\ninterface CrossrefCheckResult {\n findings: CheckFinding[];\n metadata?: RemoteMetadata;\n}\n\n/**\n * Check a single reference against external sources for status changes.\n *\n * @param item - The CSL-JSON item to check\n * @param config - Optional config for API credentials\n * @returns Check result with findings\n */\nexport async function checkReference(item: CslItem, config?: CheckConfig): Promise<CheckResult> {\n const id = item.id;\n const uuid = (item.custom?.uuid as string) ?? \"\";\n const checkedAt = new Date().toISOString();\n const findings: CheckFinding[] = [];\n const checkedSources: string[] = [];\n\n const hasDoi = !!item.DOI;\n const hasPmid = !!item.PMID;\n\n // Skip references with neither DOI nor PMID\n if (!hasDoi && !hasPmid) {\n return { id, uuid, status: \"skipped\", findings: [], checkedAt, checkedSources: [] };\n }\n\n let crossrefMetadata: RemoteMetadata | undefined;\n\n // Query Crossref if DOI is present\n if (hasDoi) {\n checkedSources.push(\"crossref\");\n const crossrefResult = await checkCrossref(item.DOI as string, config);\n findings.push(...crossrefResult.findings);\n crossrefMetadata = crossrefResult.metadata;\n }\n\n // Query PubMed if PMID is present\n if (hasPmid) {\n checkedSources.push(\"pubmed\");\n const pubmedFindings = await checkPubmed(item.PMID as string, config);\n addUniqueFindings(findings, pubmedFindings);\n }\n\n // Metadata comparison (default: enabled)\n const metadataFinding = await checkMetadata(item, config, crossrefMetadata, hasPmid, hasDoi);\n if (metadataFinding) {\n findings.push(metadataFinding);\n }\n\n const status = findings.length > 0 ? \"warning\" : \"ok\";\n return { id, uuid, status, findings, checkedAt, checkedSources };\n}\n\n/**\n * Add findings that aren't already present (by type) to the target list.\n */\nfunction addUniqueFindings(target: CheckFinding[], source: CheckFinding[]): void {\n for (const finding of source) {\n if (!target.some((f) => f.type === finding.type)) {\n target.push(finding);\n }\n }\n}\n\n/**\n * Perform metadata comparison if enabled.\n */\nasync function checkMetadata(\n item: CslItem,\n config: CheckConfig | undefined,\n crossrefMetadata: RemoteMetadata | undefined,\n hasPmid: boolean,\n hasDoi: boolean\n): Promise<CheckFinding | null> {\n if (config?.metadata === false) return null;\n\n if (crossrefMetadata) {\n // DOI-based: compare against Crossref metadata\n return compareItemMetadata(item, crossrefMetadata);\n }\n\n if (hasPmid && !hasDoi) {\n // PubMed-only: fetch remote CSL-JSON via PubMed and compare\n return comparePubmedMetadata(item, config);\n }\n\n return null;\n}\n\n/**\n * Query Crossref and return findings plus metadata.\n */\nasync function checkCrossref(doi: string, config?: CheckConfig): Promise<CrossrefCheckResult> {\n const { queryCrossref } = await import(\"./crossref-client.js\");\n const crossrefConfig = config?.email ? { email: config.email } : undefined;\n const result = await queryCrossref(doi, crossrefConfig);\n if (!result.success) return { findings: [] };\n\n const findings: CheckFinding[] = [];\n for (const update of result.updates) {\n const finding = mapCrossrefUpdate(update);\n if (finding) {\n findings.push(finding);\n }\n }\n return result.metadata ? { findings, metadata: result.metadata } : { findings };\n}\n\n/**\n * Compare item metadata against Crossref metadata.\n */\nasync function compareItemMetadata(\n item: CslItem,\n remoteMetadata: RemoteMetadata\n): Promise<CheckFinding | null> {\n const { compareMetadata } = await import(\"./metadata-comparator.js\");\n\n const local = extractLocalMetadata(item);\n const comparison = compareMetadata(local, remoteMetadata);\n\n if (comparison.classification === \"no_change\") return null;\n\n const type = comparison.classification;\n const message =\n type === \"metadata_mismatch\"\n ? \"Local metadata significantly differs from the remote record\"\n : \"Remote metadata has been updated since import\";\n\n return {\n type,\n message,\n details: {\n updatedFields: comparison.changedFields,\n fieldDiffs: comparison.fieldDiffs,\n },\n };\n}\n\n/**\n * Fetch PubMed CSL-JSON and compare metadata for PMID-only references.\n */\nasync function comparePubmedMetadata(\n item: CslItem,\n config?: CheckConfig\n): Promise<CheckFinding | null> {\n const { fetchPmids } = await import(\"../import/fetcher.js\");\n const pubmedConfig = config?.pubmed ?? {};\n const results = await fetchPmids([item.PMID as string], pubmedConfig);\n const result = results[0];\n if (!result || !result.success) {\n console.error(\n `PubMed metadata fetch failed for PMID ${item.PMID}: ${result?.error ?? \"unknown error\"}`\n );\n return null;\n }\n\n const remoteMetadata = cslItemToRemoteMetadata(result.item);\n return compareItemMetadata(item, remoteMetadata);\n}\n\n/**\n * Convert a CslItem (from PubMed) to RemoteMetadata format for comparison.\n */\nfunction cslItemToRemoteMetadata(item: CslItem): RemoteMetadata {\n const metadata: RemoteMetadata = {};\n if (item.title !== undefined) metadata.title = item.title;\n if (item.author !== undefined) {\n metadata.author = item.author as Array<{ family?: string; given?: string }>;\n }\n if (item[\"container-title\"] !== undefined) metadata.containerTitle = item[\"container-title\"];\n if (item.type !== undefined) metadata.type = item.type;\n if (item.page !== undefined) metadata.page = item.page;\n if (item.volume !== undefined) metadata.volume = item.volume;\n if (item.issue !== undefined) metadata.issue = item.issue;\n if (item.issued !== undefined) {\n metadata.issued = item.issued as { \"date-parts\"?: number[][] };\n }\n return metadata;\n}\n\n/**\n * Extract local metadata fields from a CslItem for comparison.\n */\nfunction extractLocalMetadata(\n item: CslItem\n): import(\"./metadata-comparator.js\").LocalMetadataFields {\n const local: import(\"./metadata-comparator.js\").LocalMetadataFields = {};\n if (item.title !== undefined) local.title = item.title;\n if (item.author !== undefined)\n local.author = item.author as Array<{ family?: string; given?: string }>;\n if (item[\"container-title\"] !== undefined) local[\"container-title\"] = item[\"container-title\"];\n if (item.type !== undefined) local.type = item.type;\n if (item.page !== undefined) local.page = item.page;\n if (item.volume !== undefined) local.volume = item.volume;\n if (item.issue !== undefined) local.issue = item.issue;\n if (item.issued !== undefined) local.issued = item.issued as { \"date-parts\"?: number[][] };\n return local;\n}\n\n/**\n * Query PubMed and return findings.\n */\nasync function checkPubmed(pmid: string, config?: CheckConfig): Promise<CheckFinding[]> {\n const { queryPubmed } = await import(\"./pubmed-client.js\");\n const result = await queryPubmed(pmid, config?.pubmed);\n if (!result.success) return [];\n\n const findings: CheckFinding[] = [];\n if (result.isRetracted) {\n findings.push({\n type: \"retracted\",\n message: \"This article is marked as retracted in PubMed\",\n });\n }\n if (result.hasConcern) {\n findings.push({\n type: \"concern\",\n message: \"Expression of concern noted in PubMed\",\n });\n }\n return findings;\n}\n\n/**\n * Map a Crossref update-to entry to a CheckFinding.\n */\nfunction mapCrossrefUpdate(update: CrossrefUpdateInfo): CheckFinding | null {\n const doiDetail = update.doi ? { retractionDoi: update.doi } : {};\n const dateDetail = update.date ? { retractionDate: update.date } : {};\n const newDoiDetail = update.doi ? { newDoi: update.doi } : {};\n\n switch (update.type) {\n case \"retraction\":\n return {\n type: \"retracted\",\n message: update.date\n ? `This article was retracted on ${update.date}`\n : \"This article was retracted\",\n details: { ...doiDetail, ...dateDetail },\n };\n case \"expression-of-concern\":\n return {\n type: \"concern\",\n message: update.date\n ? `Expression of concern issued on ${update.date}`\n : \"Expression of concern issued\",\n details: { ...doiDetail, ...dateDetail },\n };\n case \"new_version\":\n return {\n type: \"version_changed\",\n message: update.doi\n ? `Published version available: ${update.doi}`\n : \"Published version available\",\n details: newDoiDetail,\n };\n default:\n return null;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ import { g as getRateLimiter } from "./index-of6eJn8N.js";
2
+ const CROSSREF_API_BASE = "https://api.crossref.org/works";
3
+ function formatDateParts(updated) {
4
+ if (!updated || typeof updated !== "object") return {};
5
+ const dateParts = updated["date-parts"];
6
+ if (!Array.isArray(dateParts) || dateParts.length === 0) return {};
7
+ const parts = dateParts[0];
8
+ if (!Array.isArray(parts) || parts.length === 0) return {};
9
+ const [year, month, day] = parts;
10
+ const m = String(month ?? 1).padStart(2, "0");
11
+ const d = String(day ?? 1).padStart(2, "0");
12
+ return { date: `${year}-${m}-${d}` };
13
+ }
14
+ function extractMetadata(message) {
15
+ if (!message) return void 0;
16
+ const metadata = {};
17
+ const titleArr = message.title;
18
+ const firstTitle = Array.isArray(titleArr) ? titleArr[0] : void 0;
19
+ if (firstTitle) {
20
+ metadata.title = firstTitle;
21
+ }
22
+ const authorArr = message.author;
23
+ if (Array.isArray(authorArr) && authorArr.length > 0) {
24
+ metadata.author = authorArr.map((a) => ({
25
+ ...a.family ? { family: a.family } : {},
26
+ ...a.given ? { given: a.given } : {}
27
+ }));
28
+ }
29
+ const containerArr = message["container-title"];
30
+ const firstContainer = Array.isArray(containerArr) ? containerArr[0] : void 0;
31
+ if (firstContainer) {
32
+ metadata.containerTitle = firstContainer;
33
+ }
34
+ if (typeof message.type === "string") {
35
+ metadata.type = message.type;
36
+ }
37
+ if (typeof message.page === "string") metadata.page = message.page;
38
+ if (typeof message.volume === "string") metadata.volume = message.volume;
39
+ if (typeof message.issue === "string") metadata.issue = message.issue;
40
+ const issued = message.issued;
41
+ if (issued && Array.isArray(issued["date-parts"])) {
42
+ metadata.issued = issued;
43
+ }
44
+ if (Object.keys(metadata).length === 0) return void 0;
45
+ return metadata;
46
+ }
47
+ async function queryCrossref(doi, config) {
48
+ const rateLimiter = getRateLimiter("crossref", {});
49
+ await rateLimiter.acquire();
50
+ try {
51
+ const url = new URL(`${CROSSREF_API_BASE}/${encodeURIComponent(doi)}`);
52
+ if (config?.email) {
53
+ url.searchParams.set("mailto", config.email);
54
+ }
55
+ const response = await fetch(url.toString());
56
+ if (!response.ok) {
57
+ return {
58
+ success: false,
59
+ error: `Crossref API returned ${response.status} ${response.statusText}`
60
+ };
61
+ }
62
+ const data = await response.json();
63
+ const message = data.message;
64
+ const updateTo = message?.["update-to"] ?? [];
65
+ const updatedBy = message?.["updated-by"] ?? [];
66
+ const allEntries = [...updateTo, ...updatedBy].map((e) => {
67
+ const datePart = formatDateParts(e.updated);
68
+ return {
69
+ type: String(e.type ?? ""),
70
+ ...e.DOI ? { doi: String(e.DOI) } : {},
71
+ ...e.label ? { label: String(e.label) } : {},
72
+ ...datePart.date ? { date: datePart.date } : {}
73
+ };
74
+ });
75
+ const seen = /* @__PURE__ */ new Set();
76
+ const updates = allEntries.filter((e) => {
77
+ const key = `${e.type}:${e.doi ?? ""}`;
78
+ if (seen.has(key)) return false;
79
+ seen.add(key);
80
+ return true;
81
+ });
82
+ const metadata = extractMetadata(message);
83
+ return metadata ? { success: true, updates, metadata } : { success: true, updates };
84
+ } catch (error) {
85
+ return {
86
+ success: false,
87
+ error: error instanceof Error ? error.message : String(error)
88
+ };
89
+ }
90
+ }
91
+ export {
92
+ queryCrossref
93
+ };
94
+ //# sourceMappingURL=crossref-client-Gs75LMVf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crossref-client-Gs75LMVf.js","sources":["../../src/features/check/crossref-client.ts"],"sourcesContent":["import { getRateLimiter } from \"../import/rate-limiter.js\";\n\nconst CROSSREF_API_BASE = \"https://api.crossref.org/works\";\n\nexport interface CrossrefUpdateInfo {\n type: string;\n doi?: string;\n label?: string;\n date?: string;\n}\n\nexport interface RemoteMetadata {\n title?: string;\n author?: Array<{ family?: string; given?: string }>;\n containerTitle?: string;\n type?: string;\n page?: string;\n volume?: string;\n issue?: string;\n issued?: { \"date-parts\"?: number[][] };\n}\n\nexport type CrossrefResult =\n | { success: true; updates: CrossrefUpdateInfo[]; metadata?: RemoteMetadata }\n | { success: false; error: string };\n\n/**\n * Format date-parts from Crossref API response to ISO date string.\n */\nfunction formatDateParts(updated: unknown): { date?: string } {\n if (!updated || typeof updated !== \"object\") return {};\n const dateParts = (updated as Record<string, unknown>)[\"date-parts\"];\n if (!Array.isArray(dateParts) || dateParts.length === 0) return {};\n const parts = dateParts[0] as number[];\n if (!Array.isArray(parts) || parts.length === 0) return {};\n const [year, month, day] = parts;\n const m = String(month ?? 1).padStart(2, \"0\");\n const d = String(day ?? 1).padStart(2, \"0\");\n return { date: `${year}-${m}-${d}` };\n}\n\n/**\n * Extract comparable metadata fields from a Crossref message object.\n */\nfunction extractMetadata(message: Record<string, unknown> | undefined): RemoteMetadata | undefined {\n if (!message) return undefined;\n\n const metadata: RemoteMetadata = {};\n\n // Title: array in Crossref, take first element\n const titleArr = message.title as string[] | undefined;\n const firstTitle = Array.isArray(titleArr) ? titleArr[0] : undefined;\n if (firstTitle) {\n metadata.title = firstTitle;\n }\n\n // Author\n const authorArr = message.author as Array<{ family?: string; given?: string }> | undefined;\n if (Array.isArray(authorArr) && authorArr.length > 0) {\n metadata.author = authorArr.map((a) => ({\n ...(a.family ? { family: a.family } : {}),\n ...(a.given ? { given: a.given } : {}),\n }));\n }\n\n // Container title: array in Crossref, take first\n const containerArr = message[\"container-title\"] as string[] | undefined;\n const firstContainer = Array.isArray(containerArr) ? containerArr[0] : undefined;\n if (firstContainer) {\n metadata.containerTitle = firstContainer;\n }\n\n // Type\n if (typeof message.type === \"string\") {\n metadata.type = message.type;\n }\n\n // Page, volume, issue\n if (typeof message.page === \"string\") metadata.page = message.page;\n if (typeof message.volume === \"string\") metadata.volume = message.volume;\n if (typeof message.issue === \"string\") metadata.issue = message.issue;\n\n // Issued\n const issued = message.issued as { \"date-parts\"?: number[][] } | undefined;\n if (issued && Array.isArray(issued[\"date-parts\"])) {\n metadata.issued = issued;\n }\n\n // Return undefined if no metadata fields were populated (prevents spurious diffs)\n if (Object.keys(metadata).length === 0) return undefined;\n\n return metadata;\n}\n\n/**\n * Query Crossref REST API for a DOI and extract update-to information.\n *\n * @param doi - The DOI to query\n * @param config - Optional config with email for polite pool\n * @returns Crossref result with update information\n */\nexport async function queryCrossref(\n doi: string,\n config?: { email?: string }\n): Promise<CrossrefResult> {\n const rateLimiter = getRateLimiter(\"crossref\", {});\n await rateLimiter.acquire();\n\n try {\n const url = new URL(`${CROSSREF_API_BASE}/${encodeURIComponent(doi)}`);\n if (config?.email) {\n url.searchParams.set(\"mailto\", config.email);\n }\n const response = await fetch(url.toString());\n\n if (!response.ok) {\n return {\n success: false,\n error: `Crossref API returned ${response.status} ${response.statusText}`,\n };\n }\n\n const data = (await response.json()) as Record<string, unknown>;\n const message = data.message as Record<string, unknown> | undefined;\n\n const updateTo = (message?.[\"update-to\"] ?? []) as Record<string, unknown>[];\n const updatedBy = (message?.[\"updated-by\"] ?? []) as Record<string, unknown>[];\n const allEntries: CrossrefUpdateInfo[] = [...updateTo, ...updatedBy].map((e) => {\n const datePart = formatDateParts(e.updated);\n return {\n type: String(e.type ?? \"\"),\n ...(e.DOI ? { doi: String(e.DOI) } : {}),\n ...(e.label ? { label: String(e.label) } : {}),\n ...(datePart.date ? { date: datePart.date } : {}),\n };\n });\n\n const seen = new Set<string>();\n const updates = allEntries.filter((e) => {\n const key = `${e.type}:${e.doi ?? \"\"}`;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n\n const metadata = extractMetadata(message);\n\n return metadata ? { success: true, updates, metadata } : { success: true, updates };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,262 @@
1
+ import { render } from "ink";
2
+ import { createElement } from "react";
3
+ import { S as Select, r as restoreStdinAfterInk } from "./index-BEQ4YIXx.js";
4
+ function getFixActionsForFinding(finding) {
5
+ switch (finding.type) {
6
+ case "retracted":
7
+ return [
8
+ { type: "add_retracted_tag", label: 'Add tag "retracted"' },
9
+ { type: "add_retraction_note", label: "Add note with retraction details" },
10
+ { type: "remove_from_library", label: "Remove from library" },
11
+ { type: "skip", label: "Skip" }
12
+ ];
13
+ case "version_changed":
14
+ return [
15
+ { type: "update_from_published", label: "Update metadata from published version" },
16
+ { type: "add_version_tag", label: 'Add tag "has-published-version"' },
17
+ { type: "skip", label: "Skip" }
18
+ ];
19
+ case "concern":
20
+ return [
21
+ { type: "add_concern_tag", label: 'Add tag "expression-of-concern"' },
22
+ { type: "add_concern_note", label: "Add note with concern details" },
23
+ { type: "skip", label: "Skip" }
24
+ ];
25
+ case "metadata_mismatch":
26
+ case "metadata_outdated":
27
+ return [
28
+ { type: "update_all_fields", label: "Update all changed fields from remote" },
29
+ { type: "skip", label: "Skip" }
30
+ ];
31
+ default:
32
+ return [];
33
+ }
34
+ }
35
+ function addTag(item, tag) {
36
+ const existing = item.custom?.tags ?? [];
37
+ if (existing.includes(tag)) {
38
+ return existing;
39
+ }
40
+ return [...existing, tag];
41
+ }
42
+ function buildNoteText(prefix, finding) {
43
+ const parts = [prefix];
44
+ if (finding.details?.retractionDate) {
45
+ parts.push(`Date: ${finding.details.retractionDate}`);
46
+ }
47
+ if (finding.details?.retractionDoi) {
48
+ parts.push(`DOI: ${finding.details.retractionDoi}`);
49
+ }
50
+ return parts.join(". ");
51
+ }
52
+ function appendNote(existingNote, newNote) {
53
+ if (existingNote) {
54
+ return `${existingNote}
55
+
56
+ ${newNote}`;
57
+ }
58
+ return newNote;
59
+ }
60
+ async function applyTagAction(library, item, tag) {
61
+ const tags = addTag(item, tag);
62
+ await library.update(item.id, { custom: { ...item.custom, tags } }, {
63
+ idType: "id"
64
+ });
65
+ await library.save();
66
+ return { applied: true, message: `Added tag "${tag}"` };
67
+ }
68
+ async function applyNoteAction(library, item, prefix, finding) {
69
+ const noteText = buildNoteText(prefix, finding);
70
+ const note = appendNote(item.note, noteText);
71
+ await library.update(item.id, { note }, { idType: "id" });
72
+ await library.save();
73
+ return { applied: true, message: `Added note: ${noteText}` };
74
+ }
75
+ async function applyUpdateFromPublished(library, item, finding) {
76
+ const newDoi = finding.details?.newDoi;
77
+ if (!newDoi) {
78
+ return { applied: false, message: "No published DOI available in finding details" };
79
+ }
80
+ const { fetchDoi } = await import("./index-of6eJn8N.js").then((n) => n.y);
81
+ const fetchResult = await fetchDoi(newDoi);
82
+ if (!fetchResult.success) {
83
+ return {
84
+ applied: false,
85
+ message: `Failed to fetch metadata for ${newDoi}: ${fetchResult.error}`
86
+ };
87
+ }
88
+ const { id: _id, custom: _custom, ...metadata } = fetchResult.item;
89
+ await library.update(item.id, metadata, { idType: "id" });
90
+ await library.save();
91
+ return { applied: true, message: `Updated metadata from ${newDoi}` };
92
+ }
93
+ async function applyUpdateAllFields(library, item, finding) {
94
+ let fetchedItem;
95
+ if (item.DOI) {
96
+ const { fetchDoi } = await import("./index-of6eJn8N.js").then((n) => n.y);
97
+ const fetchResult = await fetchDoi(item.DOI);
98
+ if (!fetchResult.success) {
99
+ return {
100
+ applied: false,
101
+ message: `Failed to fetch metadata for DOI ${item.DOI}: ${fetchResult.error}`
102
+ };
103
+ }
104
+ fetchedItem = fetchResult.item;
105
+ } else if (item.PMID) {
106
+ const { fetchPmids } = await import("./index-of6eJn8N.js").then((n) => n.y);
107
+ const results = await fetchPmids([item.PMID], {});
108
+ const result = results[0];
109
+ if (!result || !result.success) {
110
+ return {
111
+ applied: false,
112
+ message: `Failed to fetch metadata for PMID ${item.PMID}: ${result?.error ?? "unknown error"}`
113
+ };
114
+ }
115
+ fetchedItem = result.item;
116
+ } else {
117
+ return { applied: false, message: "No DOI or PMID available for metadata update" };
118
+ }
119
+ const updatedFields = finding.details?.updatedFields ?? [];
120
+ const updates = {};
121
+ for (const field of updatedFields) {
122
+ if (field in fetchedItem) {
123
+ updates[field] = fetchedItem[field];
124
+ }
125
+ }
126
+ if (Object.keys(updates).length === 0) {
127
+ return { applied: false, message: "No fields to update" };
128
+ }
129
+ await library.update(item.id, updates, { idType: "id" });
130
+ await library.save();
131
+ return { applied: true, message: `Updated fields: ${updatedFields.join(", ")}` };
132
+ }
133
+ async function applyFixAction(library, item, finding, actionType) {
134
+ switch (actionType) {
135
+ case "add_retracted_tag":
136
+ return applyTagAction(library, item, "retracted");
137
+ case "add_retraction_note":
138
+ return applyNoteAction(library, item, "RETRACTED", finding);
139
+ case "add_concern_tag":
140
+ return applyTagAction(library, item, "expression-of-concern");
141
+ case "add_concern_note":
142
+ return applyNoteAction(library, item, "EXPRESSION OF CONCERN", finding);
143
+ case "add_version_tag":
144
+ return applyTagAction(library, item, "has-published-version");
145
+ case "remove_from_library": {
146
+ const removeResult = await library.remove(item.id, { idType: "id" });
147
+ if (!removeResult.removed) {
148
+ return { applied: false, message: `Failed to remove ${item.id}` };
149
+ }
150
+ await library.save();
151
+ return { applied: true, message: `Removed ${item.id}`, removed: true };
152
+ }
153
+ case "update_from_published":
154
+ return applyUpdateFromPublished(library, item, finding);
155
+ case "update_all_fields":
156
+ return applyUpdateAllFields(library, item, finding);
157
+ // Placeholder for programmatic API (MCP/server) use.
158
+ // Individual field selection is not available in interactive CLI mode,
159
+ // but can be used via the MCP tool or HTTP server API.
160
+ case "update_selected_fields":
161
+ return { applied: false, message: "Field selection not available in CLI mode" };
162
+ case "skip":
163
+ return { applied: true, message: "Skipped" };
164
+ default:
165
+ return { applied: false, message: `Unknown action: ${actionType}` };
166
+ }
167
+ }
168
+ function selectFixAction(message, options) {
169
+ return new Promise((resolve) => {
170
+ let selected = null;
171
+ const { waitUntilExit } = render(
172
+ createElement(Select, {
173
+ options,
174
+ message,
175
+ onSelect: (value) => {
176
+ selected = value;
177
+ },
178
+ onCancel: () => {
179
+ selected = null;
180
+ }
181
+ })
182
+ );
183
+ waitUntilExit().then(() => {
184
+ restoreStdinAfterInk();
185
+ resolve(selected);
186
+ }).catch(() => {
187
+ restoreStdinAfterInk();
188
+ resolve(null);
189
+ });
190
+ });
191
+ }
192
+ function getStatusLabel(type) {
193
+ switch (type) {
194
+ case "retracted":
195
+ return "RETRACTED";
196
+ case "concern":
197
+ return "CONCERN";
198
+ case "version_changed":
199
+ return "VERSION";
200
+ case "metadata_mismatch":
201
+ return "MISMATCH";
202
+ case "metadata_outdated":
203
+ return "OUTDATED";
204
+ default:
205
+ return "WARNING";
206
+ }
207
+ }
208
+ function buildSelectOptions(finding) {
209
+ return getFixActionsForFinding(finding).map((a) => ({
210
+ label: a.label,
211
+ value: a.type
212
+ }));
213
+ }
214
+ async function processFinding(result, library, item, resultId, finding) {
215
+ result.totalFindings++;
216
+ const options = buildSelectOptions(finding);
217
+ if (options.length === 0) return;
218
+ const label = getStatusLabel(finding.type);
219
+ const message = `[${label}] ${resultId}: ${finding.message}`;
220
+ const selectedAction = await selectFixAction(message, options);
221
+ if (selectedAction === null) {
222
+ result.skipped++;
223
+ return;
224
+ }
225
+ const actionResult = await applyFixAction(library, item, finding, selectedAction);
226
+ if (!actionResult.applied) {
227
+ process.stderr.write(` Error: ${actionResult.message}
228
+ `);
229
+ return;
230
+ }
231
+ process.stderr.write(` ${actionResult.message}
232
+ `);
233
+ if (selectedAction === "skip") {
234
+ result.skipped++;
235
+ } else {
236
+ result.applied++;
237
+ if (actionResult.removed) {
238
+ result.removed.push(resultId);
239
+ }
240
+ }
241
+ }
242
+ async function runFixInteraction(results, library, findItem) {
243
+ const interactionResult = {
244
+ totalFindings: 0,
245
+ applied: 0,
246
+ skipped: 0,
247
+ removed: []
248
+ };
249
+ const warningResults = results.filter((r) => r.status === "warning");
250
+ for (const checkResult of warningResults) {
251
+ const item = findItem(checkResult.id);
252
+ if (!item) continue;
253
+ for (const finding of checkResult.findings) {
254
+ await processFinding(interactionResult, library, item, checkResult.id, finding);
255
+ }
256
+ }
257
+ return interactionResult;
258
+ }
259
+ export {
260
+ runFixInteraction
261
+ };
262
+ //# sourceMappingURL=fix-interaction-DNXbmlPr.js.map