@ncukondo/reference-manager 0.27.1 → 0.28.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 (78) hide show
  1. package/dist/chunks/{SearchableMultiSelect-CixI0oIE.js → SearchableMultiSelect-LIMpJm8A.js} +2 -2
  2. package/dist/chunks/{SearchableMultiSelect-CixI0oIE.js.map → SearchableMultiSelect-LIMpJm8A.js.map} +1 -1
  3. package/dist/chunks/{action-menu-DKfv85rC.js → action-menu-CdJjcmdk.js} +3 -3
  4. package/dist/chunks/{action-menu-DKfv85rC.js.map → action-menu-CdJjcmdk.js.map} +1 -1
  5. package/dist/chunks/{checker-B-SL7krG.js → checker-CP8vSC-S.js} +5 -5
  6. package/dist/chunks/{checker-B-SL7krG.js.map → checker-CP8vSC-S.js.map} +1 -1
  7. package/dist/chunks/{crossref-client-D6g3pLUI.js → crossref-client-C1HVx8LA.js} +2 -2
  8. package/dist/chunks/{crossref-client-D6g3pLUI.js.map → crossref-client-C1HVx8LA.js.map} +1 -1
  9. package/dist/chunks/{file-watcher-Dlx0PolG.js → file-watcher-CWHg1yol.js} +48 -2
  10. package/dist/chunks/file-watcher-CWHg1yol.js.map +1 -0
  11. package/dist/chunks/{fix-interaction-CAIeqdx5.js → fix-interaction-BR3VVyHq.js} +5 -5
  12. package/dist/chunks/{fix-interaction-CAIeqdx5.js.map → fix-interaction-BR3VVyHq.js.map} +1 -1
  13. package/dist/chunks/{index-DS3BUDRy.js → index-BP_TPa_d.js} +4 -4
  14. package/dist/chunks/{index-DS3BUDRy.js.map → index-BP_TPa_d.js.map} +1 -1
  15. package/dist/chunks/{index-B1pKaejY.js → index-C9Gc8dcm.js} +98 -43
  16. package/dist/chunks/index-C9Gc8dcm.js.map +1 -0
  17. package/dist/chunks/{index-D6flikkH.js → index-CHe855EM.js} +3 -3
  18. package/dist/chunks/index-CHe855EM.js.map +1 -0
  19. package/dist/chunks/{index-DUpYvm-W.js → index-T-edKSzd.js} +364 -50
  20. package/dist/chunks/index-T-edKSzd.js.map +1 -0
  21. package/dist/chunks/{loader-B-fte1uv.js → loader-B6sytmQd.js} +2 -2
  22. package/dist/chunks/{loader-B-fte1uv.js.map → loader-B6sytmQd.js.map} +1 -1
  23. package/dist/chunks/{metadata-comparator-C5zfoYdK.js → metadata-comparator-DvqzC5tX.js} +5 -3
  24. package/dist/chunks/metadata-comparator-DvqzC5tX.js.map +1 -0
  25. package/dist/chunks/{pubmed-client-mGn5jDIc.js → pubmed-client-DEo6eaH7.js} +2 -2
  26. package/dist/chunks/{pubmed-client-mGn5jDIc.js.map → pubmed-client-DEo6eaH7.js.map} +1 -1
  27. package/dist/chunks/{reference-select-BpTCTnrO.js → reference-select-B54Upu7p.js} +4 -4
  28. package/dist/chunks/{reference-select-BpTCTnrO.js.map → reference-select-B54Upu7p.js.map} +1 -1
  29. package/dist/chunks/{style-select-RS-lt5lQ.js → style-select-BUwM-Azt.js} +3 -3
  30. package/dist/chunks/{style-select-RS-lt5lQ.js.map → style-select-BUwM-Azt.js.map} +1 -1
  31. package/dist/cli/commands/fulltext.d.ts.map +1 -1
  32. package/dist/cli/completion.d.ts.map +1 -1
  33. package/dist/cli/index.d.ts.map +1 -1
  34. package/dist/cli.js +2 -2
  35. package/dist/core/csl-json/types.d.ts +67 -0
  36. package/dist/core/csl-json/types.d.ts.map +1 -1
  37. package/dist/features/attachments/types.d.ts +8 -16
  38. package/dist/features/attachments/types.d.ts.map +1 -1
  39. package/dist/features/check/metadata-similarity.d.ts +1 -0
  40. package/dist/features/check/metadata-similarity.d.ts.map +1 -1
  41. package/dist/features/duplicate/detector.d.ts.map +1 -1
  42. package/dist/features/duplicate/types.d.ts +2 -1
  43. package/dist/features/duplicate/types.d.ts.map +1 -1
  44. package/dist/features/format/citation-fallback.d.ts.map +1 -1
  45. package/dist/features/format/pretty.d.ts.map +1 -1
  46. package/dist/features/format/resource-indicators.d.ts.map +1 -1
  47. package/dist/features/import/cache.d.ts +7 -1
  48. package/dist/features/import/cache.d.ts.map +1 -1
  49. package/dist/features/import/detector.d.ts +15 -2
  50. package/dist/features/import/detector.d.ts.map +1 -1
  51. package/dist/features/import/fetcher.d.ts +7 -0
  52. package/dist/features/import/fetcher.d.ts.map +1 -1
  53. package/dist/features/import/importer.d.ts.map +1 -1
  54. package/dist/features/import/normalizer.d.ts +16 -0
  55. package/dist/features/import/normalizer.d.ts.map +1 -1
  56. package/dist/features/import/rate-limiter.d.ts +1 -1
  57. package/dist/features/import/rate-limiter.d.ts.map +1 -1
  58. package/dist/features/operations/attachments/get.d.ts.map +1 -1
  59. package/dist/features/operations/attachments/list.d.ts.map +1 -1
  60. package/dist/features/operations/attachments/open.d.ts.map +1 -1
  61. package/dist/features/operations/fulltext/convert.d.ts.map +1 -1
  62. package/dist/features/operations/fulltext/detach.d.ts.map +1 -1
  63. package/dist/features/operations/fulltext/discover.d.ts.map +1 -1
  64. package/dist/features/operations/fulltext/fetch.d.ts +27 -4
  65. package/dist/features/operations/fulltext/fetch.d.ts.map +1 -1
  66. package/dist/features/operations/fulltext/get.d.ts.map +1 -1
  67. package/dist/features/operations/fulltext/open.d.ts.map +1 -1
  68. package/dist/features/operations/remove.d.ts.map +1 -1
  69. package/dist/features/search/matcher.d.ts.map +1 -1
  70. package/dist/index.js +3 -3
  71. package/dist/mcp/tools/fulltext.d.ts.map +1 -1
  72. package/dist/server.js +2 -2
  73. package/package.json +2 -2
  74. package/dist/chunks/file-watcher-Dlx0PolG.js.map +0 -1
  75. package/dist/chunks/index-B1pKaejY.js.map +0 -1
  76. package/dist/chunks/index-D6flikkH.js.map +0 -1
  77. package/dist/chunks/index-DUpYvm-W.js.map +0 -1
  78. package/dist/chunks/metadata-comparator-C5zfoYdK.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { Hono } from "hono";
2
- import { h as CslItemSchema, g as detectDuplicate, m as generateId, a as sortOrderSchema, b as sortFieldSchema, p as pickDefined, t as tokenize, s as search$1, f as sortResults, y as searchSortFieldSchema, L as Library, F as FileWatcher } from "./file-watcher-Dlx0PolG.js";
2
+ import { h as CslItemSchema, g as detectDuplicate, m as generateId, a as sortOrderSchema, b as sortFieldSchema, p as pickDefined, t as tokenize, s as search$1, f as sortResults, y as searchSortFieldSchema, L as Library, F as FileWatcher } from "./file-watcher-CWHg1yol.js";
3
3
  import * as fs from "node:fs";
4
4
  import { mkdtempSync, writeFileSync, existsSync, readFileSync } from "node:fs";
5
5
  import { Cite, plugins } from "@citation-js/core";
@@ -770,12 +770,14 @@ async function checkUnpaywallDetailed(doi, email) {
770
770
  const DEFAULT_SOURCE_ORDER = ["pmc", "arxiv", "unpaywall", "core"];
771
771
  function checkArxivSource(article) {
772
772
  if (!article.arxivId)
773
- return void 0;
773
+ return { skipped: "no arXiv ID available" };
774
774
  return checkArxiv(article.arxivId);
775
775
  }
776
776
  async function checkCoreSource(article, config) {
777
- if (!config.coreApiKey || !article.doi)
778
- return void 0;
777
+ if (!config.coreApiKey)
778
+ return { skipped: "coreApiKey not configured" };
779
+ if (!article.doi)
780
+ return { skipped: "no DOI available" };
779
781
  return await checkCore(article.doi, config.coreApiKey);
780
782
  }
781
783
  const sourceCheckers = {
@@ -819,9 +821,16 @@ async function enrichArticleIds(article, config) {
819
821
  return { enriched: article, discoveredIds };
820
822
  }
821
823
  async function checkUnpaywallSource(enriched, config, state) {
822
- if (!config.unpaywallEmail || !enriched.doi)
824
+ if (!config.unpaywallEmail) {
825
+ state.skipped.push({ source: "unpaywall", reason: "unpaywallEmail not configured" });
823
826
  return;
827
+ }
828
+ if (!enriched.doi) {
829
+ state.skipped.push({ source: "unpaywall", reason: "no DOI available" });
830
+ return;
831
+ }
824
832
  state.sourcesChecked++;
833
+ state.checkedSources.push("unpaywall");
825
834
  try {
826
835
  const detailed = await checkUnpaywallDetailed(enriched.doi, config.unpaywallEmail);
827
836
  if (!detailed)
@@ -835,14 +844,17 @@ async function checkUnpaywallSource(enriched, config, state) {
835
844
  }
836
845
  }
837
846
  async function checkPmcSourceWithIds(enriched, state, discoveredIds) {
838
- if (!enriched.pmid && !enriched.pmcid)
847
+ if (!enriched.pmid && !enriched.pmcid) {
848
+ state.skipped.push({ source: "pmc", reason: "no PMCID or PMID available" });
839
849
  return;
850
+ }
840
851
  const ids = {};
841
852
  if (enriched.pmid)
842
853
  ids.pmid = enriched.pmid;
843
854
  if (enriched.pmcid)
844
855
  ids.pmcid = enriched.pmcid;
845
856
  state.sourcesChecked++;
857
+ state.checkedSources.push("pmc");
846
858
  try {
847
859
  const result = await checkPmc(ids);
848
860
  if (!result)
@@ -860,9 +872,12 @@ async function checkGenericSource(source, enriched, config, state) {
860
872
  if (!checker)
861
873
  return;
862
874
  const result = await runSourceChecker(checker, enriched, config);
863
- if (result.skipped)
875
+ if (result.skipReason) {
876
+ state.skipped.push({ source, reason: result.skipReason });
864
877
  return;
878
+ }
865
879
  state.sourcesChecked++;
880
+ state.checkedSources.push(source);
866
881
  if (result.error) {
867
882
  state.errors.push({ source, error: result.error });
868
883
  } else if (result.locations) {
@@ -873,6 +888,7 @@ async function lazyPmcCheck(enriched, state, discoveredIds) {
873
888
  if (!state.unpaywallPmcid || enriched.pmcid)
874
889
  return;
875
890
  discoveredIds.pmcid = discoveredIds.pmcid ?? state.unpaywallPmcid;
891
+ state.checkedSources.push("pmc-lazy");
876
892
  try {
877
893
  const pmcResult = await checkPmc({ pmcid: state.unpaywallPmcid });
878
894
  if (pmcResult) {
@@ -884,7 +900,13 @@ async function lazyPmcCheck(enriched, state, discoveredIds) {
884
900
  }
885
901
  async function discoverOA(article, config) {
886
902
  const { enriched, discoveredIds } = await enrichArticleIds(article, config);
887
- const state = { locations: [], errors: [], sourcesChecked: 0 };
903
+ const state = {
904
+ locations: [],
905
+ errors: [],
906
+ skipped: [],
907
+ checkedSources: [],
908
+ sourcesChecked: 0
909
+ };
888
910
  const sourceOrder = config.preferSources.length > 0 ? config.preferSources : DEFAULT_SOURCE_ORDER;
889
911
  for (const source of sourceOrder) {
890
912
  if (source === "unpaywall") {
@@ -897,16 +919,24 @@ async function discoverOA(article, config) {
897
919
  }
898
920
  await lazyPmcCheck(enriched, state, discoveredIds);
899
921
  const oaStatus = determineOAStatus(state.locations, state.errors, state.sourcesChecked);
900
- return { oaStatus, locations: state.locations, errors: state.errors, discoveredIds };
922
+ return {
923
+ oaStatus,
924
+ locations: state.locations,
925
+ errors: state.errors,
926
+ skipped: state.skipped,
927
+ checkedSources: state.checkedSources,
928
+ discoveredIds
929
+ };
901
930
  }
902
931
  async function runSourceChecker(checker, article, config) {
903
932
  try {
904
933
  const result = await checker(article, config);
905
- if (result === void 0)
906
- return { skipped: true };
907
- return { skipped: false, locations: result ?? [] };
934
+ if (result !== null && typeof result === "object" && "skipped" in result) {
935
+ return { skipReason: result.skipped };
936
+ }
937
+ return { locations: result ?? [] };
908
938
  } catch (err) {
909
- return { skipped: false, error: String(err) };
939
+ return { error: String(err) };
910
940
  }
911
941
  }
912
942
  const NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([400, 401, 403, 404, 405, 410]);
@@ -10240,6 +10270,7 @@ async function fulltextDiscover(library, options) {
10240
10270
  if (doi) article.doi = doi;
10241
10271
  if (pmid) article.pmid = pmid;
10242
10272
  if (pmcid) article.pmcid = pmcid;
10273
+ if (item.custom?.arxiv_id) article.arxivId = item.custom.arxiv_id;
10243
10274
  const config = {
10244
10275
  unpaywallEmail: fulltextConfig.sources.unpaywallEmail ?? "",
10245
10276
  coreApiKey: fulltextConfig.sources.coreApiKey ?? "",
@@ -10275,6 +10306,7 @@ function buildDiscoveryArticle(item) {
10275
10306
  if (item.DOI) article.doi = item.DOI;
10276
10307
  if (item.PMID) article.pmid = item.PMID;
10277
10308
  if (item.PMCID) article.pmcid = item.PMCID;
10309
+ if (item.custom?.arxiv_id) article.arxivId = item.custom.arxiv_id;
10278
10310
  return article;
10279
10311
  }
10280
10312
  function buildDiscoveryConfig(fulltextConfig) {
@@ -10287,13 +10319,22 @@ function buildDiscoveryConfig(fulltextConfig) {
10287
10319
  if (fulltextConfig.sources.ncbiTool) config.ncbiTool = fulltextConfig.sources.ncbiTool;
10288
10320
  return config;
10289
10321
  }
10290
- async function tryDownloadPdf(locations, tempDir, ctx) {
10322
+ async function tryDownloadPdf(locations, tempDir, ctx, attempts) {
10291
10323
  const pdfLocations = locations.filter((loc) => loc.urlType === "pdf");
10292
10324
  if (pdfLocations.length === 0) return { attached: false, source: "" };
10293
10325
  const pdfPath = join(tempDir, "fulltext.pdf");
10294
10326
  for (const pdfLocation of pdfLocations) {
10295
10327
  const pdfResult = await downloadPdf(pdfLocation.url, pdfPath);
10296
- if (!pdfResult.success) continue;
10328
+ if (!pdfResult.success) {
10329
+ attempts.push({
10330
+ source: pdfLocation.source,
10331
+ phase: "download",
10332
+ url: pdfLocation.url,
10333
+ fileType: "pdf",
10334
+ error: pdfResult.error ?? "Download failed"
10335
+ });
10336
+ continue;
10337
+ }
10297
10338
  const attachResult = await fulltextAttach(ctx.library, {
10298
10339
  identifier: ctx.identifier,
10299
10340
  idType: ctx.idType,
@@ -10306,16 +10347,39 @@ async function tryDownloadPdf(locations, tempDir, ctx) {
10306
10347
  if (attachResult.success) {
10307
10348
  return { attached: true, source: pdfLocation.source };
10308
10349
  }
10350
+ attempts.push({
10351
+ source: pdfLocation.source,
10352
+ phase: "attach",
10353
+ url: pdfLocation.url,
10354
+ fileType: "pdf",
10355
+ error: "Failed to attach file"
10356
+ });
10309
10357
  }
10310
10358
  return { attached: false, source: pdfLocations[0]?.source ?? "" };
10311
10359
  }
10312
- async function tryDownloadPmcXmlAndConvert(pmcid, tempDir, ctx) {
10360
+ async function tryDownloadPmcXmlAndConvert(pmcid, tempDir, ctx, attempts) {
10313
10361
  const xmlPath = join(tempDir, "fulltext.xml");
10314
10362
  const xmlResult = await downloadPmcXml(pmcid, xmlPath);
10315
- if (!xmlResult.success) return false;
10363
+ if (!xmlResult.success) {
10364
+ attempts.push({
10365
+ source: "pmc",
10366
+ phase: "download",
10367
+ fileType: "xml",
10368
+ error: xmlResult.error ?? "Download failed"
10369
+ });
10370
+ return false;
10371
+ }
10316
10372
  const mdPath = join(tempDir, "fulltext.md");
10317
10373
  const convertResult2 = await convertPmcXmlToMarkdown(xmlPath, mdPath);
10318
- if (!convertResult2.success) return false;
10374
+ if (!convertResult2.success) {
10375
+ attempts.push({
10376
+ source: "pmc",
10377
+ phase: "convert",
10378
+ fileType: "xml",
10379
+ error: convertResult2.error ?? "Conversion failed"
10380
+ });
10381
+ return false;
10382
+ }
10319
10383
  const attachResult = await fulltextAttach(ctx.library, {
10320
10384
  identifier: ctx.identifier,
10321
10385
  idType: ctx.idType,
@@ -10325,19 +10389,43 @@ async function tryDownloadPmcXmlAndConvert(pmcid, tempDir, ctx) {
10325
10389
  move: true,
10326
10390
  fulltextDirectory: ctx.fulltextDirectory
10327
10391
  });
10392
+ if (!attachResult.success) {
10393
+ attempts.push({
10394
+ source: "pmc",
10395
+ phase: "attach",
10396
+ fileType: "markdown",
10397
+ error: "Failed to attach file"
10398
+ });
10399
+ }
10328
10400
  return attachResult.success;
10329
10401
  }
10330
10402
  function extractArxivId(url) {
10331
10403
  const match = url.match(/arxiv\.org\/(?:abs|html|pdf)\/(\d{4}\.\d{4,5}(?:v\d+)?)/);
10332
10404
  return match?.[1];
10333
10405
  }
10334
- async function tryDownloadArxivHtmlAndConvert(arxivId, tempDir, ctx) {
10406
+ async function tryDownloadArxivHtmlAndConvert(arxivId, tempDir, ctx, attempts) {
10335
10407
  const htmlPath = join(tempDir, "fulltext.html");
10336
10408
  const htmlResult = await downloadArxivHtml(arxivId, htmlPath);
10337
- if (!htmlResult.success) return false;
10409
+ if (!htmlResult.success) {
10410
+ attempts.push({
10411
+ source: "arxiv",
10412
+ phase: "download",
10413
+ fileType: "html",
10414
+ error: htmlResult.error ?? "Download failed"
10415
+ });
10416
+ return false;
10417
+ }
10338
10418
  const mdPath = join(tempDir, "fulltext.md");
10339
10419
  const convertResult2 = await convertArxivHtmlToMarkdown(htmlPath, mdPath);
10340
- if (!convertResult2.success) return false;
10420
+ if (!convertResult2.success) {
10421
+ attempts.push({
10422
+ source: "arxiv",
10423
+ phase: "convert",
10424
+ fileType: "html",
10425
+ error: convertResult2.error ?? "Conversion failed"
10426
+ });
10427
+ return false;
10428
+ }
10341
10429
  const attachResult = await fulltextAttach(ctx.library, {
10342
10430
  identifier: ctx.identifier,
10343
10431
  idType: ctx.idType,
@@ -10347,12 +10435,36 @@ async function tryDownloadArxivHtmlAndConvert(arxivId, tempDir, ctx) {
10347
10435
  move: true,
10348
10436
  fulltextDirectory: ctx.fulltextDirectory
10349
10437
  });
10438
+ if (!attachResult.success) {
10439
+ attempts.push({
10440
+ source: "arxiv",
10441
+ phase: "attach",
10442
+ fileType: "markdown",
10443
+ error: "Failed to attach file"
10444
+ });
10445
+ }
10350
10446
  return attachResult.success;
10351
10447
  }
10352
10448
  async function checkExistingFulltext(library, identifier, idType, fulltextDirectory) {
10353
10449
  const existing = await fulltextGet(library, { identifier, idType, fulltextDirectory });
10354
10450
  return existing.success && existing.paths !== void 0;
10355
10451
  }
10452
+ function buildHintUrls(item) {
10453
+ const urls = [];
10454
+ if (item.DOI) urls.push(`https://doi.org/${item.DOI}`);
10455
+ if (item.PMID) urls.push(`https://pubmed.ncbi.nlm.nih.gov/${item.PMID}/`);
10456
+ return urls;
10457
+ }
10458
+ function formatHint(prefix, urls) {
10459
+ if (urls.length === 0) return prefix;
10460
+ if (urls.length === 1) return `${prefix}: ${urls[0]}`;
10461
+ return `${prefix}:
10462
+ ${urls.map((u) => ` ${u}`).join("\n")}`;
10463
+ }
10464
+ function buildNoSourcesHint(item) {
10465
+ const urls = buildHintUrls(item);
10466
+ return urls.length > 0 ? formatHint("open to download manually", urls) : void 0;
10467
+ }
10356
10468
  async function fulltextFetch(library, options) {
10357
10469
  const { identifier, idType = "id", fulltextConfig, fulltextDirectory, source, force } = options;
10358
10470
  const item = await library.find(identifier, { idType });
@@ -10375,12 +10487,22 @@ async function fulltextFetch(library, options) {
10375
10487
  buildDiscoveryArticle(item),
10376
10488
  buildDiscoveryConfig(fulltextConfig)
10377
10489
  );
10490
+ const discoveryErrors = discovery.errors.length > 0 ? discovery.errors : void 0;
10491
+ const skipped = discovery.skipped.length > 0 ? discovery.skipped : void 0;
10492
+ const checkedSources = discovery.checkedSources.length > 0 ? discovery.checkedSources : void 0;
10378
10493
  let locations = discovery.locations;
10379
10494
  if (source) {
10380
10495
  locations = locations.filter((loc) => loc.source === source);
10381
10496
  }
10382
10497
  if (locations.length === 0) {
10383
- return { success: false, error: `No OA sources found for ${identifier}` };
10498
+ return {
10499
+ success: false,
10500
+ error: `No OA sources found for ${identifier}`,
10501
+ discoveryErrors,
10502
+ checkedSources,
10503
+ skipped,
10504
+ hint: buildNoSourcesHint(item)
10505
+ };
10384
10506
  }
10385
10507
  const effectivePmcid = item.PMCID ?? discovery.discoveredIds?.pmcid ?? extractPmcidFromLocations(locations);
10386
10508
  const tempDir = await mkdtemp(join(tmpdir(), "ref-fulltext-"));
@@ -10392,49 +10514,61 @@ async function fulltextFetch(library, options) {
10392
10514
  force: force ?? false
10393
10515
  };
10394
10516
  try {
10395
- return await downloadAndAttach(locations, effectivePmcid, tempDir, ctx, item.id, identifier);
10517
+ const result = await downloadAndAttach(
10518
+ locations,
10519
+ effectivePmcid,
10520
+ tempDir,
10521
+ ctx,
10522
+ item.id,
10523
+ identifier
10524
+ );
10525
+ return { ...result, discoveryErrors, checkedSources, skipped };
10396
10526
  } finally {
10397
10527
  await rm(tempDir, { recursive: true, force: true }).catch(() => {
10398
10528
  });
10399
10529
  }
10400
10530
  }
10401
- async function tryArxivHtmlFromLocations(locations, tempDir, ctx) {
10531
+ async function tryArxivHtmlFromLocations(locations, tempDir, ctx, attempts) {
10402
10532
  const arxivHtmlLocation = locations.find(
10403
10533
  (loc) => loc.source === "arxiv" && loc.urlType === "html"
10404
10534
  );
10405
10535
  if (!arxivHtmlLocation) return { attached: false, source: "" };
10406
10536
  const arxivId = extractArxivId(arxivHtmlLocation.url);
10407
10537
  if (!arxivId) return { attached: false, source: "arxiv" };
10408
- const mdAttached = await tryDownloadArxivHtmlAndConvert(arxivId, tempDir, ctx);
10538
+ const mdAttached = await tryDownloadArxivHtmlAndConvert(arxivId, tempDir, ctx, attempts);
10409
10539
  return { attached: mdAttached, source: "arxiv" };
10410
10540
  }
10411
- function buildDownloadError(locations, identifier) {
10541
+ function buildDownloadError(locations, identifier, attempts) {
10542
+ const attemptUrls = attempts.filter((a) => a.url).map((a) => a.url);
10543
+ const hint = attemptUrls.length > 0 ? formatHint("open to download manually (may require institutional access)", attemptUrls) : void 0;
10412
10544
  const pdfLocation = locations.find((loc) => loc.urlType === "pdf");
10413
10545
  if (pdfLocation) {
10414
10546
  return {
10415
10547
  success: false,
10416
- error: `Failed to download from ${pdfLocation.source}: download failed`
10548
+ error: `Failed to download from ${pdfLocation.source}: download failed`,
10549
+ hint
10417
10550
  };
10418
10551
  }
10419
- return { success: false, error: `Failed to download fulltext for ${identifier}` };
10552
+ return { success: false, error: `Failed to download fulltext for ${identifier}`, hint };
10420
10553
  }
10421
10554
  async function downloadAndAttach(locations, pmcid, tempDir, ctx, referenceId, identifier) {
10422
10555
  const attachedFiles = [];
10423
10556
  let usedSource = "";
10424
- const pdfResult = await tryDownloadPdf(locations, tempDir, ctx);
10557
+ const attempts = [];
10558
+ const pdfResult = await tryDownloadPdf(locations, tempDir, ctx, attempts);
10425
10559
  if (pdfResult.attached) {
10426
10560
  attachedFiles.push("pdf");
10427
10561
  usedSource = pdfResult.source;
10428
10562
  }
10429
10563
  if (pmcid) {
10430
- const mdAttached = await tryDownloadPmcXmlAndConvert(pmcid, tempDir, ctx);
10564
+ const mdAttached = await tryDownloadPmcXmlAndConvert(pmcid, tempDir, ctx, attempts);
10431
10565
  if (mdAttached) {
10432
10566
  attachedFiles.push("markdown");
10433
10567
  if (!usedSource) usedSource = "pmc";
10434
10568
  }
10435
10569
  }
10436
10570
  if (!attachedFiles.includes("markdown")) {
10437
- const arxivResult = await tryArxivHtmlFromLocations(locations, tempDir, ctx);
10571
+ const arxivResult = await tryArxivHtmlFromLocations(locations, tempDir, ctx, attempts);
10438
10572
  if (arxivResult.attached) {
10439
10573
  attachedFiles.push("markdown");
10440
10574
  if (!usedSource) usedSource = arxivResult.source;
@@ -10443,7 +10577,10 @@ async function downloadAndAttach(locations, pmcid, tempDir, ctx, referenceId, id
10443
10577
  if (attachedFiles.length > 0) {
10444
10578
  return { success: true, referenceId, source: usedSource, attachedFiles };
10445
10579
  }
10446
- return buildDownloadError(locations, identifier);
10580
+ return {
10581
+ ...buildDownloadError(locations, identifier, attempts),
10582
+ attempts: attempts.length > 0 ? attempts : void 0
10583
+ };
10447
10584
  }
10448
10585
  const fetch$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
10449
10586
  __proto__: null,
@@ -10511,6 +10648,7 @@ function formatFirstAuthor(item) {
10511
10648
  if (!firstAuthor) {
10512
10649
  return "Unknown";
10513
10650
  }
10651
+ if (firstAuthor.literal) return firstAuthor.literal;
10514
10652
  const family = firstAuthor.family || "Unknown";
10515
10653
  const givenInitial = firstAuthor.given ? firstAuthor.given[0] : "";
10516
10654
  if (givenInitial) {
@@ -10600,6 +10738,7 @@ function getFirstAuthorFamilyName(item) {
10600
10738
  if (!firstAuthor) {
10601
10739
  return "Unknown";
10602
10740
  }
10741
+ if (firstAuthor.literal) return firstAuthor.literal;
10603
10742
  return firstAuthor.family || "Unknown";
10604
10743
  }
10605
10744
  function formatInTextEntry(item) {
@@ -10950,6 +11089,13 @@ function getIsbnFromCache(isbn) {
10950
11089
  function cacheIsbnResult(isbn, item, config) {
10951
11090
  storeInCache(isbnCache, isbn, item);
10952
11091
  }
11092
+ const arxivCache = /* @__PURE__ */ new Map();
11093
+ function getArxivFromCache(arxivId) {
11094
+ return getFromCache(arxivCache, arxivId);
11095
+ }
11096
+ function cacheArxivResult(arxivId, item, config) {
11097
+ storeInCache(arxivCache, arxivId, item);
11098
+ }
10953
11099
  const DOI_URL_PREFIXES$1 = [
10954
11100
  "https://doi.org/",
10955
11101
  "http://doi.org/",
@@ -10990,6 +11136,28 @@ function normalizeIsbn(isbn) {
10990
11136
  normalized = normalized.toUpperCase();
10991
11137
  return normalized;
10992
11138
  }
11139
+ const ARXIV_URL_PREFIXES = [
11140
+ "https://arxiv.org/abs/",
11141
+ "http://arxiv.org/abs/",
11142
+ "https://arxiv.org/pdf/",
11143
+ "http://arxiv.org/pdf/",
11144
+ "https://arxiv.org/html/",
11145
+ "http://arxiv.org/html/"
11146
+ ];
11147
+ function normalizeArxiv(arxiv) {
11148
+ const trimmed = arxiv.trim();
11149
+ if (!trimmed) {
11150
+ return "";
11151
+ }
11152
+ const lowerInput = trimmed.toLowerCase();
11153
+ for (const prefix of ARXIV_URL_PREFIXES) {
11154
+ if (lowerInput.startsWith(prefix.toLowerCase())) {
11155
+ return trimmed.slice(prefix.length);
11156
+ }
11157
+ }
11158
+ const withoutPrefix = trimmed.replace(/^arxiv:\s*/i, "");
11159
+ return withoutPrefix;
11160
+ }
10993
11161
  const EXTENSION_MAP = {
10994
11162
  ".json": "json",
10995
11163
  ".bib": "bibtex",
@@ -11050,6 +11218,9 @@ function detectSingleIdentifier(input) {
11050
11218
  if (isDoi(input)) {
11051
11219
  return "doi";
11052
11220
  }
11221
+ if (isArxiv(input)) {
11222
+ return "arxiv";
11223
+ }
11053
11224
  if (isIsbn(input)) {
11054
11225
  return "isbn";
11055
11226
  }
@@ -11112,6 +11283,17 @@ function isIsbn(input) {
11112
11283
  }
11113
11284
  return true;
11114
11285
  }
11286
+ const ARXIV_ID_PATTERN = /^\d{4}\.\d{4,5}(v\d+)?$/;
11287
+ function isArxiv(input) {
11288
+ if (!input || input.length === 0) {
11289
+ return false;
11290
+ }
11291
+ const normalized = normalizeArxiv(input);
11292
+ if (!normalized) {
11293
+ return false;
11294
+ }
11295
+ return ARXIV_ID_PATTERN.test(normalized);
11296
+ }
11115
11297
  const RATE_LIMITS = {
11116
11298
  pubmed: {
11117
11299
  withoutApiKey: 3,
@@ -11121,8 +11303,10 @@ const RATE_LIMITS = {
11121
11303
  },
11122
11304
  crossref: 50,
11123
11305
  // 50 req/sec
11124
- isbn: 10
11306
+ isbn: 10,
11125
11307
  // 10 req/sec (conservative for Google Books API daily limit)
11308
+ arxiv: 1
11309
+ // 1 req/sec (arXiv API is strict about rate limiting)
11126
11310
  };
11127
11311
  class RateLimiterImpl {
11128
11312
  requestsPerSecond;
@@ -11175,6 +11359,8 @@ function getRequestsPerSecond(api, config) {
11175
11359
  return RATE_LIMITS.crossref;
11176
11360
  case "isbn":
11177
11361
  return RATE_LIMITS.isbn;
11362
+ case "arxiv":
11363
+ return RATE_LIMITS.arxiv;
11178
11364
  }
11179
11365
  }
11180
11366
  const PMC_API_BASE = "https://pmc.ncbi.nlm.nih.gov/api/ctxp/v1/pubmed/";
@@ -11354,8 +11540,108 @@ async function fetchIsbn(isbn) {
11354
11540
  };
11355
11541
  }
11356
11542
  }
11543
+ const ARXIV_API_BASE = "https://export.arxiv.org/api/query";
11544
+ function extractXmlText(xml, tagName) {
11545
+ const regex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`);
11546
+ const match = regex.exec(xml);
11547
+ return match?.[1]?.trim() ?? "";
11548
+ }
11549
+ function extractAuthors(entryXml) {
11550
+ const authors = [];
11551
+ const matches = entryXml.matchAll(/<author>[\s\S]*?<name>([^<]+)<\/name>[\s\S]*?<\/author>/g);
11552
+ for (const match of matches) {
11553
+ const name = match[1]?.trim();
11554
+ if (name) {
11555
+ authors.push({ literal: name });
11556
+ }
11557
+ }
11558
+ return authors;
11559
+ }
11560
+ function extractJournalDoi(entryXml) {
11561
+ const match = /<arxiv:doi[^>]*>([^<]+)<\/arxiv:doi>/.exec(entryXml);
11562
+ return match?.[1]?.trim();
11563
+ }
11564
+ function parseIssuedDate(dateStr) {
11565
+ if (!dateStr) return void 0;
11566
+ const date = new Date(dateStr);
11567
+ if (Number.isNaN(date.getTime())) return void 0;
11568
+ return {
11569
+ "date-parts": [[date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()]]
11570
+ };
11571
+ }
11572
+ async function fetchArxiv(arxivId) {
11573
+ if (!ARXIV_ID_PATTERN.test(arxivId)) {
11574
+ return {
11575
+ success: false,
11576
+ error: `Invalid arXiv ID format: ${arxivId}`,
11577
+ reason: "validation_error"
11578
+ };
11579
+ }
11580
+ const rateLimiter = getRateLimiter("arxiv", {});
11581
+ await rateLimiter.acquire();
11582
+ try {
11583
+ const url = `${ARXIV_API_BASE}?id_list=${encodeURIComponent(arxivId)}`;
11584
+ const response = await fetch(url, {
11585
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
11586
+ });
11587
+ if (!response.ok) {
11588
+ return {
11589
+ success: false,
11590
+ error: `arXiv API returned status ${response.status}`,
11591
+ reason: "fetch_error"
11592
+ };
11593
+ }
11594
+ const xml = await response.text();
11595
+ const entryMatch = /<entry>([\s\S]*?)<\/entry>/.exec(xml);
11596
+ if (!entryMatch) {
11597
+ return {
11598
+ success: false,
11599
+ error: `No results found for arXiv ID ${arxivId}`,
11600
+ reason: "not_found"
11601
+ };
11602
+ }
11603
+ const entryXml = entryMatch[1] ?? "";
11604
+ const title = extractXmlText(entryXml, "title");
11605
+ const summary = extractXmlText(entryXml, "summary");
11606
+ const published = extractXmlText(entryXml, "published");
11607
+ const authors = extractAuthors(entryXml);
11608
+ const journalDoi = extractJournalDoi(entryXml);
11609
+ const baseId = arxivId.replace(/v\d+$/, "");
11610
+ const doi = journalDoi ?? `10.48550/arXiv.${baseId}`;
11611
+ const item = {
11612
+ id: "",
11613
+ type: "article",
11614
+ title,
11615
+ author: authors,
11616
+ abstract: summary || void 0,
11617
+ issued: parseIssuedDate(published),
11618
+ DOI: doi,
11619
+ URL: `https://arxiv.org/abs/${arxivId}`,
11620
+ custom: {
11621
+ arxiv_id: arxivId
11622
+ }
11623
+ };
11624
+ const parseResult = CslItemSchema.safeParse(item);
11625
+ if (!parseResult.success) {
11626
+ return {
11627
+ success: false,
11628
+ error: `Invalid CSL-JSON data for arXiv ${arxivId}: ${parseResult.error.message}`,
11629
+ reason: "validation_error"
11630
+ };
11631
+ }
11632
+ return { success: true, item: parseResult.data };
11633
+ } catch (error) {
11634
+ const errorMsg = error instanceof Error ? error.message : String(error);
11635
+ return {
11636
+ success: false,
11637
+ error: errorMsg,
11638
+ reason: "fetch_error"
11639
+ };
11640
+ }
11641
+ }
11357
11642
  const fetcher = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
11358
11643
  __proto__: null,
11644
+ fetchArxiv,
11359
11645
  fetchDoi,
11360
11646
  fetchIsbn,
11361
11647
  fetchPmids
@@ -11550,19 +11836,22 @@ function classifyIdentifiers(identifiers) {
11550
11836
  const pmids = [];
11551
11837
  const dois = [];
11552
11838
  const isbns = [];
11839
+ const arxivs = [];
11553
11840
  const unknowns = [];
11554
11841
  for (const id of identifiers) {
11555
- if (isPmid(id)) {
11556
- pmids.push(normalizePmid(id));
11557
- } else if (isDoi(id)) {
11842
+ if (isDoi(id)) {
11558
11843
  dois.push(normalizeDoi(id));
11844
+ } else if (isArxiv(id)) {
11845
+ arxivs.push(normalizeArxiv(id));
11559
11846
  } else if (isIsbn(id)) {
11560
11847
  isbns.push(normalizeIsbn(id));
11848
+ } else if (isPmid(id)) {
11849
+ pmids.push(normalizePmid(id));
11561
11850
  } else {
11562
11851
  unknowns.push(id);
11563
11852
  }
11564
11853
  }
11565
- return { pmids, dois, isbns, unknowns };
11854
+ return { pmids, dois, isbns, arxivs, unknowns };
11566
11855
  }
11567
11856
  function buildUnknownResults(unknowns) {
11568
11857
  return unknowns.map((unknown) => ({
@@ -11655,6 +11944,29 @@ async function fetchIsbnsWithCache(isbns) {
11655
11944
  }
11656
11945
  return results;
11657
11946
  }
11947
+ async function fetchArxivsWithCache(arxivIds) {
11948
+ const results = [];
11949
+ for (const arxivId of arxivIds) {
11950
+ const cached = getArxivFromCache(arxivId);
11951
+ if (cached) {
11952
+ results.push({ success: true, item: clearItemId(cached), source: arxivId });
11953
+ continue;
11954
+ }
11955
+ const fetchResult = await fetchArxiv(arxivId);
11956
+ if (fetchResult.success) {
11957
+ cacheArxivResult(arxivId, fetchResult.item);
11958
+ results.push({ success: true, item: clearItemId(fetchResult.item), source: arxivId });
11959
+ } else {
11960
+ results.push({
11961
+ success: false,
11962
+ error: fetchResult.error,
11963
+ source: arxivId,
11964
+ reason: fetchResult.reason
11965
+ });
11966
+ }
11967
+ }
11968
+ return results;
11969
+ }
11658
11970
  function parseJsonContent(content) {
11659
11971
  try {
11660
11972
  const parsed = JSON.parse(content);
@@ -11811,7 +12123,7 @@ async function importFromIdentifiers(identifiers, options) {
11811
12123
  if (identifiers.length === 0) {
11812
12124
  return { results: [] };
11813
12125
  }
11814
- const { pmids, dois, isbns, unknowns } = classifyIdentifiers(identifiers);
12126
+ const { pmids, dois, isbns, arxivs, unknowns } = classifyIdentifiers(identifiers);
11815
12127
  const results = [];
11816
12128
  results.push(...buildUnknownResults(unknowns));
11817
12129
  const pmidResults = await fetchPmidsWithCache(pmids, options.pubmedConfig ?? {});
@@ -11820,6 +12132,8 @@ async function importFromIdentifiers(identifiers, options) {
11820
12132
  results.push(...doiResults);
11821
12133
  const isbnResults = await fetchIsbnsWithCache(isbns);
11822
12134
  results.push(...isbnResults);
12135
+ const arxivResults = await fetchArxivsWithCache(arxivs);
12136
+ results.push(...arxivResults);
11823
12137
  return { results };
11824
12138
  }
11825
12139
  function looksLikeFilePath(input) {
@@ -11861,13 +12175,14 @@ async function processIdentifiers(inputs, options) {
11861
12175
  const isValidPmid = isPmid(input);
11862
12176
  const isValidDoi = isDoi(input);
11863
12177
  const isValidIsbn = isIsbn(input);
11864
- if (isValidPmid || isValidDoi || isValidIsbn) {
12178
+ const isValidArxiv = isArxiv(input);
12179
+ if (isValidPmid || isValidDoi || isValidIsbn || isValidArxiv) {
11865
12180
  validIdentifiers.push(input);
11866
12181
  } else {
11867
12182
  const hint = looksLikeFilePath(input) ? " Hint: If this is a file path, check that the file exists." : "";
11868
12183
  results.push({
11869
12184
  success: false,
11870
- error: `Cannot interpret '${input}' as identifier (not a valid PMID, DOI, or ISBN).${hint}`,
12185
+ error: `Cannot interpret '${input}' as identifier (not a valid PMID, DOI, ISBN, or arXiv ID).${hint}`,
11871
12186
  source: input,
11872
12187
  reason: "validation_error"
11873
12188
  });
@@ -11911,7 +12226,7 @@ async function processStdinContent(content, options) {
11911
12226
  source: r.source === "content" ? "stdin" : r.source
11912
12227
  }));
11913
12228
  }
11914
- if (format === "pmid" || format === "doi" || format === "isbn") {
12229
+ if (format === "pmid" || format === "doi" || format === "isbn" || format === "arxiv") {
11915
12230
  const identifiers2 = content.split(/\s+/).filter((s) => s.length > 0);
11916
12231
  return processIdentifiers(identifiers2, options);
11917
12232
  }
@@ -12084,7 +12399,7 @@ function createAddRoute(library, config) {
12084
12399
  }
12085
12400
  const CHECK_CONCURRENCY = 5;
12086
12401
  async function checkReferences(library, options) {
12087
- const { checkReference } = await import("./checker-B-SL7krG.js");
12402
+ const { checkReference } = await import("./checker-CP8vSC-S.js");
12088
12403
  const save = options.save !== false;
12089
12404
  const skipDays = options.skipDays ?? 7;
12090
12405
  const items = await resolveItems(library, options);
@@ -12114,7 +12429,7 @@ function fillSkippedResults(tasks, results) {
12114
12429
  uuid: task.item.custom?.uuid ?? "",
12115
12430
  status: "skipped",
12116
12431
  findings: [],
12117
- checkedAt: task.item.custom?.check?.checked_at,
12432
+ checkedAt: task.item.custom?.check?.checked_at ?? "",
12118
12433
  checkedSources: []
12119
12434
  };
12120
12435
  }
@@ -12176,11 +12491,11 @@ async function resolveItems(library, options) {
12176
12491
  }
12177
12492
  function shouldSkipRecentCheck(item, skipDays) {
12178
12493
  if (skipDays <= 0) return false;
12179
- const check2 = item.custom?.check;
12180
- if (!check2?.checked_at) return false;
12181
- const checkedAt = new Date(check2.checked_at);
12494
+ const checkedAt = item.custom?.check?.checked_at;
12495
+ if (!checkedAt) return false;
12496
+ const checkedAtDate = new Date(checkedAt);
12182
12497
  const now = /* @__PURE__ */ new Date();
12183
- const daysSince = (now.getTime() - checkedAt.getTime()) / (1e3 * 60 * 60 * 24);
12498
+ const daysSince = (now.getTime() - checkedAtDate.getTime()) / (1e3 * 60 * 60 * 24);
12184
12499
  return daysSince < skipDays;
12185
12500
  }
12186
12501
  async function saveCheckResult(library, item, result) {
@@ -12193,9 +12508,8 @@ async function saveCheckResult(library, item, result) {
12193
12508
  ...f.details ? { details: snakeCaseKeys(f.details) } : {}
12194
12509
  }))
12195
12510
  };
12196
- const existingCustom = item.custom ?? {};
12197
12511
  await library.update(item.id, {
12198
- custom: { ...existingCustom, check: checkData }
12512
+ custom: { ...item.custom, check: checkData }
12199
12513
  });
12200
12514
  }
12201
12515
  function snakeCaseKeys(obj) {
@@ -12764,4 +13078,4 @@ export {
12764
13078
  fetcher as y,
12765
13079
  add as z
12766
13080
  };
12767
- //# sourceMappingURL=index-DUpYvm-W.js.map
13081
+ //# sourceMappingURL=index-T-edKSzd.js.map