@ncukondo/search-hub 0.12.1 → 0.13.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 (423) hide show
  1. package/dist/_virtual/_commonjsHelpers.js +30 -0
  2. package/dist/_virtual/_commonjsHelpers.js.map +1 -0
  3. package/dist/_virtual/aliases.js +5 -0
  4. package/dist/_virtual/aliases.js.map +1 -0
  5. package/dist/_virtual/attributes.js +5 -0
  6. package/dist/_virtual/attributes.js.map +1 -0
  7. package/dist/_virtual/back.js +5 -0
  8. package/dist/_virtual/back.js.map +1 -0
  9. package/dist/_virtual/comment.js +5 -0
  10. package/dist/_virtual/comment.js.map +1 -0
  11. package/dist/_virtual/compile.js +5 -0
  12. package/dist/_virtual/compile.js.map +1 -0
  13. package/dist/_virtual/compile2.js +5 -0
  14. package/dist/_virtual/compile2.js.map +1 -0
  15. package/dist/_virtual/decode-data-html.js +5 -0
  16. package/dist/_virtual/decode-data-html.js.map +1 -0
  17. package/dist/_virtual/decode-data-xml.js +5 -0
  18. package/dist/_virtual/decode-data-xml.js.map +1 -0
  19. package/dist/_virtual/decode.js +5 -0
  20. package/dist/_virtual/decode.js.map +1 -0
  21. package/dist/_virtual/decode_codepoint.js +5 -0
  22. package/dist/_virtual/decode_codepoint.js.map +1 -0
  23. package/dist/_virtual/encode-html.js +5 -0
  24. package/dist/_virtual/encode-html.js.map +1 -0
  25. package/dist/_virtual/encode.js +5 -0
  26. package/dist/_virtual/encode.js.map +1 -0
  27. package/dist/_virtual/escape.js +5 -0
  28. package/dist/_virtual/escape.js.map +1 -0
  29. package/dist/_virtual/feeds.js +5 -0
  30. package/dist/_virtual/feeds.js.map +1 -0
  31. package/dist/_virtual/filters.js +5 -0
  32. package/dist/_virtual/filters.js.map +1 -0
  33. package/dist/_virtual/foreignNames.js +5 -0
  34. package/dist/_virtual/foreignNames.js.map +1 -0
  35. package/dist/_virtual/general.js +5 -0
  36. package/dist/_virtual/general.js.map +1 -0
  37. package/dist/_virtual/he.js +5 -0
  38. package/dist/_virtual/he.js.map +1 -0
  39. package/dist/_virtual/helpers.js +5 -0
  40. package/dist/_virtual/helpers.js.map +1 -0
  41. package/dist/_virtual/html.js +5 -0
  42. package/dist/_virtual/html.js.map +1 -0
  43. package/dist/_virtual/index.js +6 -0
  44. package/dist/_virtual/index.js.map +1 -0
  45. package/dist/_virtual/index10.js +5 -0
  46. package/dist/_virtual/index10.js.map +1 -0
  47. package/dist/_virtual/index11.js +5 -0
  48. package/dist/_virtual/index11.js.map +1 -0
  49. package/dist/_virtual/index2.js +5 -0
  50. package/dist/_virtual/index2.js.map +1 -0
  51. package/dist/_virtual/index3.js +5 -0
  52. package/dist/_virtual/index3.js.map +1 -0
  53. package/dist/_virtual/index4.js +5 -0
  54. package/dist/_virtual/index4.js.map +1 -0
  55. package/dist/_virtual/index5.js +7 -0
  56. package/dist/_virtual/index5.js.map +1 -0
  57. package/dist/_virtual/index6.js +5 -0
  58. package/dist/_virtual/index6.js.map +1 -0
  59. package/dist/_virtual/index7.js +5 -0
  60. package/dist/_virtual/index7.js.map +1 -0
  61. package/dist/_virtual/index8.js +5 -0
  62. package/dist/_virtual/index8.js.map +1 -0
  63. package/dist/_virtual/index9.js +5 -0
  64. package/dist/_virtual/index9.js.map +1 -0
  65. package/dist/_virtual/legacy.js +5 -0
  66. package/dist/_virtual/legacy.js.map +1 -0
  67. package/dist/_virtual/manipulation.js +5 -0
  68. package/dist/_virtual/manipulation.js.map +1 -0
  69. package/dist/_virtual/matcher.js +5 -0
  70. package/dist/_virtual/matcher.js.map +1 -0
  71. package/dist/_virtual/node.js +5 -0
  72. package/dist/_virtual/node.js.map +1 -0
  73. package/dist/_virtual/node2.js +5 -0
  74. package/dist/_virtual/node2.js.map +1 -0
  75. package/dist/_virtual/parse.js +5 -0
  76. package/dist/_virtual/parse.js.map +1 -0
  77. package/dist/_virtual/parse2.js +5 -0
  78. package/dist/_virtual/parse2.js.map +1 -0
  79. package/dist/_virtual/pseudos.js +5 -0
  80. package/dist/_virtual/pseudos.js.map +1 -0
  81. package/dist/_virtual/querying.js +5 -0
  82. package/dist/_virtual/querying.js.map +1 -0
  83. package/dist/_virtual/sort.js +5 -0
  84. package/dist/_virtual/sort.js.map +1 -0
  85. package/dist/_virtual/stringify.js +5 -0
  86. package/dist/_virtual/stringify.js.map +1 -0
  87. package/dist/_virtual/subselects.js +5 -0
  88. package/dist/_virtual/subselects.js.map +1 -0
  89. package/dist/_virtual/text.js +5 -0
  90. package/dist/_virtual/text.js.map +1 -0
  91. package/dist/_virtual/traversal.js +5 -0
  92. package/dist/_virtual/traversal.js.map +1 -0
  93. package/dist/_virtual/type.js +5 -0
  94. package/dist/_virtual/type.js.map +1 -0
  95. package/dist/_virtual/valid.js +5 -0
  96. package/dist/_virtual/valid.js.map +1 -0
  97. package/dist/_virtual/void-tag.js +5 -0
  98. package/dist/_virtual/void-tag.js.map +1 -0
  99. package/dist/cli/commands/diff.js +2 -2
  100. package/dist/cli/commands/diff.js.map +1 -1
  101. package/dist/cli/commands/fulltext/attach.js +1 -1
  102. package/dist/cli/commands/fulltext/attach.js.map +1 -1
  103. package/dist/cli/commands/fulltext/check.d.ts +1 -2
  104. package/dist/cli/commands/fulltext/check.d.ts.map +1 -1
  105. package/dist/cli/commands/fulltext/check.js +4 -2
  106. package/dist/cli/commands/fulltext/check.js.map +1 -1
  107. package/dist/cli/commands/fulltext/convert.d.ts.map +1 -1
  108. package/dist/cli/commands/fulltext/convert.js +8 -8
  109. package/dist/cli/commands/fulltext/convert.js.map +1 -1
  110. package/dist/cli/commands/fulltext/fetch.d.ts.map +1 -1
  111. package/dist/cli/commands/fulltext/fetch.js +10 -6
  112. package/dist/cli/commands/fulltext/fetch.js.map +1 -1
  113. package/dist/cli/commands/fulltext/index.d.ts.map +1 -1
  114. package/dist/cli/commands/fulltext/index.js +2 -0
  115. package/dist/cli/commands/fulltext/index.js.map +1 -1
  116. package/dist/cli/commands/fulltext/init.d.ts.map +1 -1
  117. package/dist/cli/commands/fulltext/init.js +6 -5
  118. package/dist/cli/commands/fulltext/init.js.map +1 -1
  119. package/dist/cli/commands/fulltext/pending.d.ts +1 -1
  120. package/dist/cli/commands/fulltext/pending.d.ts.map +1 -1
  121. package/dist/cli/commands/fulltext/pending.js +4 -2
  122. package/dist/cli/commands/fulltext/pending.js.map +1 -1
  123. package/dist/cli/commands/fulltext/status.d.ts.map +1 -1
  124. package/dist/cli/commands/fulltext/status.js +4 -2
  125. package/dist/cli/commands/fulltext/status.js.map +1 -1
  126. package/dist/cli/commands/fulltext/sync.d.ts.map +1 -1
  127. package/dist/cli/commands/fulltext/sync.js +6 -2
  128. package/dist/cli/commands/fulltext/sync.js.map +1 -1
  129. package/dist/cli/commands/query/init.d.ts +5 -0
  130. package/dist/cli/commands/query/init.d.ts.map +1 -1
  131. package/dist/cli/commands/query/init.js +9 -1
  132. package/dist/cli/commands/query/init.js.map +1 -1
  133. package/dist/cli/commands/query/translate.d.ts.map +1 -1
  134. package/dist/cli/commands/query/translate.js +5 -0
  135. package/dist/cli/commands/query/translate.js.map +1 -1
  136. package/dist/cli/commands/query/validate.d.ts +22 -1
  137. package/dist/cli/commands/query/validate.d.ts.map +1 -1
  138. package/dist/cli/commands/query/validate.js +65 -22
  139. package/dist/cli/commands/query/validate.js.map +1 -1
  140. package/dist/cli/commands/review/extract.d.ts.map +1 -1
  141. package/dist/cli/commands/review/extract.js +1 -2
  142. package/dist/cli/commands/review/extract.js.map +1 -1
  143. package/dist/cli/commands/review/finalize.d.ts.map +1 -1
  144. package/dist/cli/commands/review/finalize.js +1 -2
  145. package/dist/cli/commands/review/finalize.js.map +1 -1
  146. package/dist/cli/commands/review/init.d.ts.map +1 -1
  147. package/dist/cli/commands/review/init.js +2 -5
  148. package/dist/cli/commands/review/init.js.map +1 -1
  149. package/dist/cli/commands/review/merge.d.ts.map +1 -1
  150. package/dist/cli/commands/review/merge.js +1 -2
  151. package/dist/cli/commands/review/merge.js.map +1 -1
  152. package/dist/cli/commands/review/types.d.ts +1 -1
  153. package/dist/cli/commands/review/types.d.ts.map +1 -1
  154. package/dist/cli/commands/review/types.js.map +1 -1
  155. package/dist/cli/index.d.ts.map +1 -1
  156. package/dist/cli/index.js +81 -7
  157. package/dist/cli/index.js.map +1 -1
  158. package/dist/cli/suggestions/index.d.ts.map +1 -1
  159. package/dist/cli/suggestions/index.js +10 -0
  160. package/dist/cli/suggestions/index.js.map +1 -1
  161. package/dist/cli/suggestions/rules.d.ts.map +1 -1
  162. package/dist/cli/suggestions/rules.js +21 -8
  163. package/dist/cli/suggestions/rules.js.map +1 -1
  164. package/dist/cli/suggestions/types.d.ts +11 -0
  165. package/dist/cli/suggestions/types.d.ts.map +1 -1
  166. package/dist/config/schema.d.ts +2 -0
  167. package/dist/config/schema.d.ts.map +1 -1
  168. package/dist/config/schema.js +6 -0
  169. package/dist/config/schema.js.map +1 -1
  170. package/dist/index.js +5 -0
  171. package/dist/index.js.map +1 -1
  172. package/dist/{fulltext → integration}/attach-shared.d.ts +2 -2
  173. package/dist/integration/attach-shared.d.ts.map +1 -0
  174. package/dist/integration/attach-shared.js.map +1 -0
  175. package/dist/integration/fulltext-attach.js +1 -1
  176. package/dist/integration/fulltext-attach.js.map +1 -1
  177. package/dist/{fulltext → node_modules/@ncukondo/academic-fulltext/dist}/citation-key.js +1 -1
  178. package/dist/node_modules/@ncukondo/academic-fulltext/dist/citation-key.js.map +1 -0
  179. package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/arxiv-html-parser.js +434 -0
  180. package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/arxiv-html-parser.js.map +1 -0
  181. package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/index.js +93 -0
  182. package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/index.js.map +1 -0
  183. package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js +1060 -0
  184. package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js.map +1 -0
  185. package/dist/{fulltext → node_modules/@ncukondo/academic-fulltext/dist}/convert/markdown-writer.js +146 -117
  186. package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/markdown-writer.js.map +1 -0
  187. package/dist/{fulltext → node_modules/@ncukondo/academic-fulltext/dist}/discovery/arxiv.js +8 -1
  188. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/arxiv.js.map +1 -0
  189. package/dist/{fulltext → node_modules/@ncukondo/academic-fulltext/dist}/discovery/core.js +6 -3
  190. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/core.js.map +1 -0
  191. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/index.js +139 -0
  192. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/index.js.map +1 -0
  193. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/ncbi-id-converter.js +46 -0
  194. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/ncbi-id-converter.js.map +1 -0
  195. package/dist/{fulltext → node_modules/@ncukondo/academic-fulltext/dist}/discovery/pmc.js +8 -4
  196. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/pmc.js.map +1 -0
  197. package/dist/{fulltext → node_modules/@ncukondo/academic-fulltext/dist}/discovery/unpaywall.js +43 -9
  198. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/unpaywall.js.map +1 -0
  199. package/dist/node_modules/@ncukondo/academic-fulltext/dist/download/arxiv-html.js +48 -0
  200. package/dist/node_modules/@ncukondo/academic-fulltext/dist/download/arxiv-html.js.map +1 -0
  201. package/dist/node_modules/@ncukondo/academic-fulltext/dist/download/downloader.js +64 -0
  202. package/dist/node_modules/@ncukondo/academic-fulltext/dist/download/downloader.js.map +1 -0
  203. package/dist/node_modules/@ncukondo/academic-fulltext/dist/download/orchestrator.js +236 -0
  204. package/dist/node_modules/@ncukondo/academic-fulltext/dist/download/orchestrator.js.map +1 -0
  205. package/dist/{fulltext → node_modules/@ncukondo/academic-fulltext/dist}/download/pmc-xml.js +2 -1
  206. package/dist/node_modules/@ncukondo/academic-fulltext/dist/download/pmc-xml.js.map +1 -0
  207. package/dist/{fulltext → node_modules/@ncukondo/academic-fulltext/dist}/meta.js +15 -10
  208. package/dist/node_modules/@ncukondo/academic-fulltext/dist/meta.js.map +1 -0
  209. package/dist/node_modules/@ncukondo/academic-fulltext/dist/paths.js.map +1 -0
  210. package/dist/{fulltext → node_modules/@ncukondo/academic-fulltext/dist}/readme.js +8 -4
  211. package/dist/node_modules/@ncukondo/academic-fulltext/dist/readme.js.map +1 -0
  212. package/dist/node_modules/boolbase/index.js +19 -0
  213. package/dist/node_modules/boolbase/index.js.map +1 -0
  214. package/dist/node_modules/css-select/lib/attributes.js +203 -0
  215. package/dist/node_modules/css-select/lib/attributes.js.map +1 -0
  216. package/dist/node_modules/css-select/lib/compile.js +141 -0
  217. package/dist/node_modules/css-select/lib/compile.js.map +1 -0
  218. package/dist/node_modules/css-select/lib/general.js +154 -0
  219. package/dist/node_modules/css-select/lib/general.js.map +1 -0
  220. package/dist/node_modules/css-select/lib/index.js +128 -0
  221. package/dist/node_modules/css-select/lib/index.js.map +1 -0
  222. package/dist/node_modules/css-select/lib/pseudo-selectors/aliases.js +40 -0
  223. package/dist/node_modules/css-select/lib/pseudo-selectors/aliases.js.map +1 -0
  224. package/dist/node_modules/css-select/lib/pseudo-selectors/filters.js +163 -0
  225. package/dist/node_modules/css-select/lib/pseudo-selectors/filters.js.map +1 -0
  226. package/dist/node_modules/css-select/lib/pseudo-selectors/index.js +71 -0
  227. package/dist/node_modules/css-select/lib/pseudo-selectors/index.js.map +1 -0
  228. package/dist/node_modules/css-select/lib/pseudo-selectors/pseudos.js +93 -0
  229. package/dist/node_modules/css-select/lib/pseudo-selectors/pseudos.js.map +1 -0
  230. package/dist/node_modules/css-select/lib/pseudo-selectors/subselects.js +111 -0
  231. package/dist/node_modules/css-select/lib/pseudo-selectors/subselects.js.map +1 -0
  232. package/dist/node_modules/css-select/lib/sort.js +78 -0
  233. package/dist/node_modules/css-select/lib/sort.js.map +1 -0
  234. package/dist/node_modules/css-what/lib/es/index.js +12 -0
  235. package/dist/node_modules/css-what/lib/es/index.js.map +1 -0
  236. package/dist/node_modules/css-what/lib/es/parse.js +349 -0
  237. package/dist/node_modules/css-what/lib/es/parse.js.map +1 -0
  238. package/dist/node_modules/css-what/lib/es/stringify.js +102 -0
  239. package/dist/node_modules/css-what/lib/es/stringify.js.map +1 -0
  240. package/dist/node_modules/css-what/lib/es/types.js +37 -0
  241. package/dist/node_modules/css-what/lib/es/types.js.map +1 -0
  242. package/dist/node_modules/dom-serializer/lib/foreignNames.js +117 -0
  243. package/dist/node_modules/dom-serializer/lib/foreignNames.js.map +1 -0
  244. package/dist/node_modules/dom-serializer/lib/index.js +207 -0
  245. package/dist/node_modules/dom-serializer/lib/index.js.map +1 -0
  246. package/dist/node_modules/dom-serializer/node_modules/entities/lib/decode.js +368 -0
  247. package/dist/node_modules/dom-serializer/node_modules/entities/lib/decode.js.map +1 -0
  248. package/dist/node_modules/dom-serializer/node_modules/entities/lib/decode_codepoint.js +70 -0
  249. package/dist/node_modules/dom-serializer/node_modules/entities/lib/decode_codepoint.js.map +1 -0
  250. package/dist/node_modules/dom-serializer/node_modules/entities/lib/encode.js +61 -0
  251. package/dist/node_modules/dom-serializer/node_modules/entities/lib/encode.js.map +1 -0
  252. package/dist/node_modules/dom-serializer/node_modules/entities/lib/escape.js +79 -0
  253. package/dist/node_modules/dom-serializer/node_modules/entities/lib/escape.js.map +1 -0
  254. package/dist/node_modules/dom-serializer/node_modules/entities/lib/generated/decode-data-html.js +18 -0
  255. package/dist/node_modules/dom-serializer/node_modules/entities/lib/generated/decode-data-html.js.map +1 -0
  256. package/dist/node_modules/dom-serializer/node_modules/entities/lib/generated/decode-data-xml.js +18 -0
  257. package/dist/node_modules/dom-serializer/node_modules/entities/lib/generated/decode-data-xml.js.map +1 -0
  258. package/dist/node_modules/dom-serializer/node_modules/entities/lib/generated/encode-html.js +19 -0
  259. package/dist/node_modules/dom-serializer/node_modules/entities/lib/generated/encode-html.js.map +1 -0
  260. package/dist/node_modules/dom-serializer/node_modules/entities/lib/index.js +139 -0
  261. package/dist/node_modules/dom-serializer/node_modules/entities/lib/index.js.map +1 -0
  262. package/dist/node_modules/domelementtype/lib/index.js +40 -0
  263. package/dist/node_modules/domelementtype/lib/index.js.map +1 -0
  264. package/dist/node_modules/domhandler/lib/index.js +167 -0
  265. package/dist/node_modules/domhandler/lib/index.js.map +1 -0
  266. package/dist/node_modules/domhandler/lib/node.js +439 -0
  267. package/dist/node_modules/domhandler/lib/node.js.map +1 -0
  268. package/dist/node_modules/domutils/lib/feeds.js +146 -0
  269. package/dist/node_modules/domutils/lib/feeds.js.map +1 -0
  270. package/dist/node_modules/domutils/lib/helpers.js +97 -0
  271. package/dist/node_modules/domutils/lib/helpers.js.map +1 -0
  272. package/dist/node_modules/domutils/lib/index.js +65 -0
  273. package/dist/node_modules/domutils/lib/index.js.map +1 -0
  274. package/dist/node_modules/domutils/lib/legacy.js +124 -0
  275. package/dist/node_modules/domutils/lib/legacy.js.map +1 -0
  276. package/dist/node_modules/domutils/lib/manipulation.js +107 -0
  277. package/dist/node_modules/domutils/lib/manipulation.js.map +1 -0
  278. package/dist/node_modules/domutils/lib/querying.js +102 -0
  279. package/dist/node_modules/domutils/lib/querying.js.map +1 -0
  280. package/dist/node_modules/domutils/lib/stringify.js +65 -0
  281. package/dist/node_modules/domutils/lib/stringify.js.map +1 -0
  282. package/dist/node_modules/domutils/lib/traversal.js +69 -0
  283. package/dist/node_modules/domutils/lib/traversal.js.map +1 -0
  284. package/dist/node_modules/he/he.js +256 -0
  285. package/dist/node_modules/he/he.js.map +1 -0
  286. package/dist/node_modules/node-html-parser/dist/back.js +16 -0
  287. package/dist/node_modules/node-html-parser/dist/back.js.map +1 -0
  288. package/dist/node_modules/node-html-parser/dist/index.js +48 -0
  289. package/dist/node_modules/node-html-parser/dist/index.js.map +1 -0
  290. package/dist/node_modules/node-html-parser/dist/matcher.js +112 -0
  291. package/dist/node_modules/node-html-parser/dist/matcher.js.map +1 -0
  292. package/dist/node_modules/node-html-parser/dist/nodes/comment.js +41 -0
  293. package/dist/node_modules/node-html-parser/dist/nodes/comment.js.map +1 -0
  294. package/dist/node_modules/node-html-parser/dist/nodes/html.js +1048 -0
  295. package/dist/node_modules/node-html-parser/dist/nodes/html.js.map +1 -0
  296. package/dist/node_modules/node-html-parser/dist/nodes/node.js +49 -0
  297. package/dist/node_modules/node-html-parser/dist/nodes/node.js.map +1 -0
  298. package/dist/node_modules/node-html-parser/dist/nodes/text.js +106 -0
  299. package/dist/node_modules/node-html-parser/dist/nodes/text.js.map +1 -0
  300. package/dist/node_modules/node-html-parser/dist/nodes/type.js +19 -0
  301. package/dist/node_modules/node-html-parser/dist/nodes/type.js.map +1 -0
  302. package/dist/node_modules/node-html-parser/dist/parse.js +20 -0
  303. package/dist/node_modules/node-html-parser/dist/parse.js.map +1 -0
  304. package/dist/node_modules/node-html-parser/dist/valid.js +19 -0
  305. package/dist/node_modules/node-html-parser/dist/valid.js.map +1 -0
  306. package/dist/node_modules/node-html-parser/dist/void-tag.js +36 -0
  307. package/dist/node_modules/node-html-parser/dist/void-tag.js.map +1 -0
  308. package/dist/node_modules/nth-check/lib/compile.js +76 -0
  309. package/dist/node_modules/nth-check/lib/compile.js.map +1 -0
  310. package/dist/node_modules/nth-check/lib/index.js +36 -0
  311. package/dist/node_modules/nth-check/lib/index.js.map +1 -0
  312. package/dist/node_modules/nth-check/lib/parse.js +69 -0
  313. package/dist/node_modules/nth-check/lib/parse.js.map +1 -0
  314. package/dist/providers/arxiv/translator.d.ts.map +1 -1
  315. package/dist/providers/arxiv/translator.js +5 -2
  316. package/dist/providers/arxiv/translator.js.map +1 -1
  317. package/dist/providers/base/types.d.ts +2 -0
  318. package/dist/providers/base/types.d.ts.map +1 -1
  319. package/dist/providers/base/types.js.map +1 -1
  320. package/dist/providers/base/warnings.d.ts +14 -0
  321. package/dist/providers/base/warnings.d.ts.map +1 -0
  322. package/dist/providers/base/warnings.js +33 -0
  323. package/dist/providers/base/warnings.js.map +1 -0
  324. package/dist/providers/eric/translator.d.ts.map +1 -1
  325. package/dist/providers/eric/translator.js +5 -2
  326. package/dist/providers/eric/translator.js.map +1 -1
  327. package/dist/providers/pubmed/translator.d.ts.map +1 -1
  328. package/dist/providers/pubmed/translator.js +5 -2
  329. package/dist/providers/pubmed/translator.js.map +1 -1
  330. package/dist/providers/scopus/translator.d.ts.map +1 -1
  331. package/dist/providers/scopus/translator.js +22 -5
  332. package/dist/providers/scopus/translator.js.map +1 -1
  333. package/dist/query/__test-helpers__/mock-mesh-client.d.ts +12 -0
  334. package/dist/query/__test-helpers__/mock-mesh-client.d.ts.map +1 -0
  335. package/dist/query/index.d.ts +4 -0
  336. package/dist/query/index.d.ts.map +1 -1
  337. package/dist/query/json-schema.d.ts +3 -0
  338. package/dist/query/json-schema.d.ts.map +1 -0
  339. package/dist/query/json-schema.js +48 -0
  340. package/dist/query/json-schema.js.map +1 -0
  341. package/dist/query/mesh-lookup.d.ts +47 -0
  342. package/dist/query/mesh-lookup.d.ts.map +1 -0
  343. package/dist/query/mesh-lookup.js +151 -0
  344. package/dist/query/mesh-lookup.js.map +1 -0
  345. package/dist/query/parser.js +1 -1
  346. package/dist/query/parser.js.map +1 -1
  347. package/dist/query/types.d.ts +2 -2
  348. package/dist/query/types.d.ts.map +1 -1
  349. package/dist/query/validator.d.ts +5 -5
  350. package/dist/query/validator.d.ts.map +1 -1
  351. package/dist/query/validator.js +5 -2
  352. package/dist/query/validator.js.map +1 -1
  353. package/dist/query/vocab-cache.d.ts +15 -0
  354. package/dist/query/vocab-cache.d.ts.map +1 -0
  355. package/dist/query/vocab-cache.js +44 -0
  356. package/dist/query/vocab-cache.js.map +1 -0
  357. package/dist/query/vocab-validator.d.ts +71 -0
  358. package/dist/query/vocab-validator.d.ts.map +1 -0
  359. package/dist/query/vocab-validator.js +153 -0
  360. package/dist/query/vocab-validator.js.map +1 -0
  361. package/dist/utils/levenshtein.d.ts +6 -0
  362. package/dist/utils/levenshtein.d.ts.map +1 -0
  363. package/dist/utils/levenshtein.js +21 -0
  364. package/dist/utils/levenshtein.js.map +1 -0
  365. package/package.json +2 -2
  366. package/dist/fulltext/attach-shared.d.ts.map +0 -1
  367. package/dist/fulltext/attach-shared.js.map +0 -1
  368. package/dist/fulltext/citation-key.d.ts +0 -15
  369. package/dist/fulltext/citation-key.d.ts.map +0 -1
  370. package/dist/fulltext/citation-key.js.map +0 -1
  371. package/dist/fulltext/convert/index.d.ts +0 -20
  372. package/dist/fulltext/convert/index.d.ts.map +0 -1
  373. package/dist/fulltext/convert/index.js +0 -50
  374. package/dist/fulltext/convert/index.js.map +0 -1
  375. package/dist/fulltext/convert/jats-parser.d.ts +0 -36
  376. package/dist/fulltext/convert/jats-parser.d.ts.map +0 -1
  377. package/dist/fulltext/convert/jats-parser.js +0 -887
  378. package/dist/fulltext/convert/jats-parser.js.map +0 -1
  379. package/dist/fulltext/convert/markdown-writer.d.ts +0 -6
  380. package/dist/fulltext/convert/markdown-writer.d.ts.map +0 -1
  381. package/dist/fulltext/convert/markdown-writer.js.map +0 -1
  382. package/dist/fulltext/convert/types.d.ts +0 -141
  383. package/dist/fulltext/convert/types.d.ts.map +0 -1
  384. package/dist/fulltext/discovery/arxiv.d.ts +0 -11
  385. package/dist/fulltext/discovery/arxiv.d.ts.map +0 -1
  386. package/dist/fulltext/discovery/arxiv.js.map +0 -1
  387. package/dist/fulltext/discovery/core.d.ts +0 -11
  388. package/dist/fulltext/discovery/core.d.ts.map +0 -1
  389. package/dist/fulltext/discovery/core.js.map +0 -1
  390. package/dist/fulltext/discovery/index.d.ts +0 -28
  391. package/dist/fulltext/discovery/index.d.ts.map +0 -1
  392. package/dist/fulltext/discovery/index.js +0 -75
  393. package/dist/fulltext/discovery/index.js.map +0 -1
  394. package/dist/fulltext/discovery/pmc.d.ts +0 -19
  395. package/dist/fulltext/discovery/pmc.d.ts.map +0 -1
  396. package/dist/fulltext/discovery/pmc.js.map +0 -1
  397. package/dist/fulltext/discovery/unpaywall.d.ts +0 -11
  398. package/dist/fulltext/discovery/unpaywall.d.ts.map +0 -1
  399. package/dist/fulltext/discovery/unpaywall.js.map +0 -1
  400. package/dist/fulltext/download/downloader.d.ts +0 -21
  401. package/dist/fulltext/download/downloader.d.ts.map +0 -1
  402. package/dist/fulltext/download/downloader.js +0 -59
  403. package/dist/fulltext/download/downloader.js.map +0 -1
  404. package/dist/fulltext/download/orchestrator.d.ts +0 -33
  405. package/dist/fulltext/download/orchestrator.d.ts.map +0 -1
  406. package/dist/fulltext/download/orchestrator.js +0 -125
  407. package/dist/fulltext/download/orchestrator.js.map +0 -1
  408. package/dist/fulltext/download/pmc-xml.d.ts +0 -13
  409. package/dist/fulltext/download/pmc-xml.d.ts.map +0 -1
  410. package/dist/fulltext/download/pmc-xml.js.map +0 -1
  411. package/dist/fulltext/meta.d.ts +0 -25
  412. package/dist/fulltext/meta.d.ts.map +0 -1
  413. package/dist/fulltext/meta.js.map +0 -1
  414. package/dist/fulltext/paths.d.ts +0 -12
  415. package/dist/fulltext/paths.d.ts.map +0 -1
  416. package/dist/fulltext/paths.js.map +0 -1
  417. package/dist/fulltext/readme.d.ts +0 -4
  418. package/dist/fulltext/readme.d.ts.map +0 -1
  419. package/dist/fulltext/readme.js.map +0 -1
  420. package/dist/fulltext/types.d.ts +0 -90
  421. package/dist/fulltext/types.d.ts.map +0 -1
  422. /package/dist/{fulltext → integration}/attach-shared.js +0 -0
  423. /package/dist/{fulltext → node_modules/@ncukondo/academic-fulltext/dist}/paths.js +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"diff.js","sources":["../../../src/cli/commands/diff.ts"],"sourcesContent":["import type { Article } from '../../providers/base/types.js';\nimport type { QueryAST, FieldType, TermBlock } from '../../query/types.js';\nimport { getArticleKeys } from './session-utils.js';\n\nexport interface DiffResult {\n session1Count: number;\n session2Count: number;\n added: Article[];\n removed: Article[];\n common: Article[];\n}\n\n/**\n * Compute the diff between two sets of articles.\n *\n * Articles are matched by identifiers (DOI, PMID, arXiv ID, Scopus ID, ERIC ID).\n * Two articles are considered the same if they share any identifier.\n * Articles without identifiers cannot be matched.\n */\nexport function computeDiff(session1: Article[], session2: Article[]): DiffResult {\n // Build a set of all identifier keys from session1\n const session1Keys = new Set<string>();\n for (const article of session1) {\n for (const key of getArticleKeys(article)) {\n session1Keys.add(key);\n }\n }\n\n // Build a set of all identifier keys from session2\n const session2Keys = new Set<string>();\n for (const article of session2) {\n for (const key of getArticleKeys(article)) {\n session2Keys.add(key);\n }\n }\n\n // Classify session1 articles as common or removed\n const common: Article[] = [];\n const removed: Article[] = [];\n for (const article of session1) {\n const keys = getArticleKeys(article);\n if (keys.length === 0) {\n // No identifiers - cannot match, treat as removed\n removed.push(article);\n continue;\n }\n const isInSession2 = keys.some((key) => session2Keys.has(key));\n if (isInSession2) {\n common.push(article);\n } else {\n removed.push(article);\n }\n }\n\n // Classify session2 articles as added (those not in session1)\n const added: Article[] = [];\n for (const article of session2) {\n const keys = getArticleKeys(article);\n if (keys.length === 0) {\n // No identifiers - cannot match, treat as added\n added.push(article);\n continue;\n }\n const isInSession1 = keys.some((key) => session1Keys.has(key));\n if (!isInSession1) {\n added.push(article);\n }\n }\n\n return {\n session1Count: session1.length,\n session2Count: session2.length,\n added,\n removed,\n common,\n };\n}\n\nexport type ShowFilter = 'added' | 'removed' | 'common';\n\n/**\n * Extract a year string from publicationDate, or return empty string.\n */\nfunction extractYear(publicationDate: string | undefined): string {\n if (!publicationDate) return '';\n const year = publicationDate.substring(0, 4);\n return /^\\d{4}$/.test(year) ? year : '';\n}\n\n/**\n * Format an article line for display.\n */\nfunction formatArticleLine(prefix: string, article: Article): string {\n const year = extractYear(article.publicationDate);\n const yearPart = year ? `[${year}] ` : '';\n return ` ${prefix} ${yearPart}${article.title}`;\n}\n\n/**\n * Options for formatting diff with query information.\n */\nexport interface FormatDiffOptions {\n queryDiff?: QueryDiff | undefined;\n noQueryDiff?: boolean | undefined;\n showQueryDiffPlaceholder?: boolean | undefined;\n}\n\n/**\n * Format diff result as human-readable text.\n */\nexport function formatDiff(\n diff: DiffResult,\n session1Id: string,\n session2Id: string,\n show?: ShowFilter,\n options?: FormatDiffOptions,\n): string {\n const lines: string[] = [];\n\n // Header\n lines.push(`Diff: ${session1Id} → ${session2Id}`);\n lines.push(` Session 1: ${diff.session1Count} articles (${session1Id})`);\n lines.push(` Session 2: ${diff.session2Count} articles (${session2Id})`);\n lines.push('');\n\n // Query changes section (if available and not disabled)\n const shouldShowQueryDiff = options?.queryDiff && !options?.noQueryDiff;\n const shouldShowPlaceholder = options?.showQueryDiffPlaceholder && !options?.queryDiff && !options?.noQueryDiff;\n\n if (shouldShowPlaceholder) {\n lines.push('Query changes: (query data not available)');\n lines.push('');\n } else if (shouldShowQueryDiff) {\n lines.push(formatQueryDiff(options.queryDiff!));\n lines.push('');\n lines.push('Result changes:');\n }\n\n // Summary counts\n lines.push(` Common: ${diff.common.length} articles`);\n lines.push(` Added: ${diff.added.length} articles (in ${session2Id} but not ${session1Id})`);\n lines.push(` Removed: ${diff.removed.length} articles (in ${session1Id} but not ${session2Id})`);\n\n // Article lists based on show filter\n const showAdded = !show || show === 'added';\n const showRemoved = !show || show === 'removed';\n const showCommon = show === 'common';\n\n if (showAdded && diff.added.length > 0) {\n lines.push('');\n lines.push(`Added (+${diff.added.length}):`);\n for (const article of diff.added) {\n lines.push(formatArticleLine('+', article));\n }\n }\n\n if (showRemoved && diff.removed.length > 0) {\n lines.push('');\n lines.push(`Removed (-${diff.removed.length}):`);\n for (const article of diff.removed) {\n lines.push(formatArticleLine('-', article));\n }\n }\n\n if (showCommon && diff.common.length > 0) {\n lines.push('');\n lines.push(`Common (${diff.common.length}):`);\n for (const article of diff.common) {\n lines.push(formatArticleLine('=', article));\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format diff result as JSON.\n */\ninterface DiffJsonOutput {\n session1: string;\n session2: string;\n summary: {\n session1Count: number;\n session2Count: number;\n commonCount: number;\n addedCount: number;\n removedCount: number;\n };\n queryDiff?: QueryDiff;\n added?: Article[];\n removed?: Article[];\n common?: Article[];\n}\n\nexport function formatDiffJson(\n diff: DiffResult,\n session1Id: string,\n session2Id: string,\n show?: ShowFilter,\n options?: FormatDiffOptions,\n): string {\n const result: DiffJsonOutput = {\n session1: session1Id,\n session2: session2Id,\n summary: {\n session1Count: diff.session1Count,\n session2Count: diff.session2Count,\n commonCount: diff.common.length,\n addedCount: diff.added.length,\n removedCount: diff.removed.length,\n },\n };\n\n // Add queryDiff if available and not disabled\n if (options?.queryDiff && !options?.noQueryDiff) {\n result.queryDiff = options.queryDiff;\n }\n\n if (!show || show === 'added') {\n result.added = diff.added;\n }\n if (!show || show === 'removed') {\n result.removed = diff.removed;\n }\n if (!show || show === 'common') {\n result.common = diff.common;\n }\n\n return JSON.stringify(result, null, 2);\n}\n\n/**\n * Diff result for a single query block.\n */\nexport interface BlockDiff {\n index: number;\n field: FieldType;\n added: string[];\n removed: string[];\n meshAdded?: string[];\n meshRemoved?: string[];\n emtreeAdded?: string[];\n emtreeRemoved?: string[];\n excludeAdded?: string[];\n excludeRemoved?: string[];\n hasChanges: boolean;\n isNew?: boolean;\n isRemoved?: boolean;\n}\n\n/**\n * Diff result for query filters.\n */\nexport interface FilterDiff {\n yearFromChanged: boolean;\n oldYearFrom?: number | undefined;\n newYearFrom?: number | undefined;\n yearToChanged: boolean;\n oldYearTo?: number | undefined;\n newYearTo?: number | undefined;\n languagesAdded: string[];\n languagesRemoved: string[];\n}\n\n/**\n * Complete query diff result.\n */\nexport interface QueryDiff {\n blocks: BlockDiff[];\n filters: FilterDiff;\n}\n\n/**\n * Compute the set difference: elements in arr2 not in arr1.\n */\nfunction setDiff(arr1: string[], arr2: string[]): string[] {\n const set1 = new Set(arr1);\n return arr2.filter((item) => !set1.has(item));\n}\n\n/**\n * Compare two query blocks and return the differences.\n */\nfunction compareBlocks(\n block1: TermBlock | undefined,\n block2: TermBlock | undefined,\n index: number,\n field: FieldType,\n): BlockDiff {\n const emptyTerms = { keywords: [] as string[], mesh: [] as string[], emtree: [] as string[], exclude: [] as string[] };\n const terms1 = block1 ?? emptyTerms;\n const terms2 = block2 ?? emptyTerms;\n\n const added = setDiff(terms1.keywords, terms2.keywords);\n const removed = setDiff(terms2.keywords, terms1.keywords);\n const meshAdded = setDiff(terms1.mesh ?? [], terms2.mesh ?? []);\n const meshRemoved = setDiff(terms2.mesh ?? [], terms1.mesh ?? []);\n const emtreeAdded = setDiff(terms1.emtree ?? [], terms2.emtree ?? []);\n const emtreeRemoved = setDiff(terms2.emtree ?? [], terms1.emtree ?? []);\n const excludeAdded = setDiff(terms1.exclude ?? [], terms2.exclude ?? []);\n const excludeRemoved = setDiff(terms2.exclude ?? [], terms1.exclude ?? []);\n\n const hasChanges =\n added.length > 0 ||\n removed.length > 0 ||\n meshAdded.length > 0 ||\n meshRemoved.length > 0 ||\n emtreeAdded.length > 0 ||\n emtreeRemoved.length > 0 ||\n excludeAdded.length > 0 ||\n excludeRemoved.length > 0;\n\n const result: BlockDiff = {\n index,\n field,\n added,\n removed,\n hasChanges,\n };\n\n if (meshAdded.length > 0 || meshRemoved.length > 0) {\n result.meshAdded = meshAdded;\n result.meshRemoved = meshRemoved;\n }\n if (emtreeAdded.length > 0 || emtreeRemoved.length > 0) {\n result.emtreeAdded = emtreeAdded;\n result.emtreeRemoved = emtreeRemoved;\n }\n if (excludeAdded.length > 0 || excludeRemoved.length > 0) {\n result.excludeAdded = excludeAdded;\n result.excludeRemoved = excludeRemoved;\n }\n\n if (!block1) {\n result.isNew = true;\n result.hasChanges = true;\n }\n if (!block2) {\n result.isRemoved = true;\n result.hasChanges = true;\n }\n\n return result;\n}\n\n/**\n * Compute the diff between two QueryAST objects.\n */\nexport function computeQueryDiff(query1: QueryAST, query2: QueryAST): QueryDiff {\n const blocks: BlockDiff[] = [];\n\n const maxBlocks = Math.max(query1.blocks.length, query2.blocks.length);\n\n for (let i = 0; i < maxBlocks; i++) {\n const block1 = query1.blocks[i];\n const block2 = query2.blocks[i];\n\n const field = block2?.field ?? block1?.field ?? 'all';\n const blockDiff = compareBlocks(\n block1?.terms,\n block2?.terms,\n i,\n field,\n );\n blocks.push(blockDiff);\n }\n\n // Compare filters\n const filters: FilterDiff = {\n yearFromChanged: query1.filters.yearFrom !== query2.filters.yearFrom,\n yearToChanged: query1.filters.yearTo !== query2.filters.yearTo,\n languagesAdded: setDiff(query1.filters.languages ?? [], query2.filters.languages ?? []),\n languagesRemoved: setDiff(query2.filters.languages ?? [], query1.filters.languages ?? []),\n };\n\n if (filters.yearFromChanged) {\n filters.oldYearFrom = query1.filters.yearFrom;\n filters.newYearFrom = query2.filters.yearFrom;\n }\n if (filters.yearToChanged) {\n filters.oldYearTo = query1.filters.yearTo;\n filters.newYearTo = query2.filters.yearTo;\n }\n\n return { blocks, filters };\n}\n\n/**\n * Format query diff as human-readable text.\n */\nexport function formatQueryDiff(queryDiff: QueryDiff): string {\n const lines: string[] = [];\n\n lines.push('Query changes:');\n\n // Format block changes\n for (const block of queryDiff.blocks) {\n const blockNum = block.index + 1;\n let blockHeader = ` Block ${blockNum} (${block.field})`;\n if (block.isNew) {\n blockHeader += ' (new block)';\n } else if (block.isRemoved) {\n blockHeader += ' (removed block)';\n }\n\n if (!block.hasChanges) {\n lines.push(`${blockHeader}: no changes`);\n } else {\n lines.push(`${blockHeader}:`);\n\n // Added keywords\n for (const keyword of block.added) {\n lines.push(` + ${keyword}`);\n }\n\n // Removed keywords\n for (const keyword of block.removed) {\n lines.push(` - ${keyword}`);\n }\n\n // MeSH changes\n if (block.meshAdded) {\n for (const term of block.meshAdded) {\n lines.push(` + [MeSH] ${term}`);\n }\n }\n if (block.meshRemoved) {\n for (const term of block.meshRemoved) {\n lines.push(` - [MeSH] ${term}`);\n }\n }\n\n // Emtree changes\n if (block.emtreeAdded) {\n for (const term of block.emtreeAdded) {\n lines.push(` + [Emtree] ${term}`);\n }\n }\n if (block.emtreeRemoved) {\n for (const term of block.emtreeRemoved) {\n lines.push(` - [Emtree] ${term}`);\n }\n }\n\n // Exclude changes\n if (block.excludeAdded) {\n for (const term of block.excludeAdded) {\n lines.push(` + [exclude] ${term}`);\n }\n }\n if (block.excludeRemoved) {\n for (const term of block.excludeRemoved) {\n lines.push(` - [exclude] ${term}`);\n }\n }\n }\n }\n\n // Format filter changes\n const hasFilterChanges =\n queryDiff.filters.yearFromChanged ||\n queryDiff.filters.yearToChanged ||\n queryDiff.filters.languagesAdded.length > 0 ||\n queryDiff.filters.languagesRemoved.length > 0;\n\n if (hasFilterChanges) {\n lines.push('');\n lines.push(' Filters:');\n\n if (queryDiff.filters.yearFromChanged) {\n const oldVal = queryDiff.filters.oldYearFrom ?? '(none)';\n const newVal = queryDiff.filters.newYearFrom ?? '(none)';\n lines.push(` yearFrom: ${oldVal} → ${newVal}`);\n }\n\n if (queryDiff.filters.yearToChanged) {\n const oldVal = queryDiff.filters.oldYearTo ?? '(none)';\n const newVal = queryDiff.filters.newYearTo ?? '(none)';\n lines.push(` yearTo: ${oldVal} → ${newVal}`);\n }\n\n if (queryDiff.filters.languagesAdded.length > 0 || queryDiff.filters.languagesRemoved.length > 0) {\n lines.push(' languages:');\n for (const lang of queryDiff.filters.languagesAdded) {\n lines.push(` + ${lang}`);\n }\n for (const lang of queryDiff.filters.languagesRemoved) {\n lines.push(` - ${lang}`);\n }\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"diff.js","sources":["../../../src/cli/commands/diff.ts"],"sourcesContent":["import type { Article } from '../../providers/base/types.js';\nimport type { QueryAST, FieldType, TermBlock } from '../../query/types.js';\nimport { getArticleKeys } from './session-utils.js';\n\nexport interface DiffResult {\n session1Count: number;\n session2Count: number;\n added: Article[];\n removed: Article[];\n common: Article[];\n}\n\n/**\n * Compute the diff between two sets of articles.\n *\n * Articles are matched by identifiers (DOI, PMID, arXiv ID, Scopus ID, ERIC ID).\n * Two articles are considered the same if they share any identifier.\n * Articles without identifiers cannot be matched.\n */\nexport function computeDiff(session1: Article[], session2: Article[]): DiffResult {\n // Build a set of all identifier keys from session1\n const session1Keys = new Set<string>();\n for (const article of session1) {\n for (const key of getArticleKeys(article)) {\n session1Keys.add(key);\n }\n }\n\n // Build a set of all identifier keys from session2\n const session2Keys = new Set<string>();\n for (const article of session2) {\n for (const key of getArticleKeys(article)) {\n session2Keys.add(key);\n }\n }\n\n // Classify session1 articles as common or removed\n const common: Article[] = [];\n const removed: Article[] = [];\n for (const article of session1) {\n const keys = getArticleKeys(article);\n if (keys.length === 0) {\n // No identifiers - cannot match, treat as removed\n removed.push(article);\n continue;\n }\n const isInSession2 = keys.some((key) => session2Keys.has(key));\n if (isInSession2) {\n common.push(article);\n } else {\n removed.push(article);\n }\n }\n\n // Classify session2 articles as added (those not in session1)\n const added: Article[] = [];\n for (const article of session2) {\n const keys = getArticleKeys(article);\n if (keys.length === 0) {\n // No identifiers - cannot match, treat as added\n added.push(article);\n continue;\n }\n const isInSession1 = keys.some((key) => session1Keys.has(key));\n if (!isInSession1) {\n added.push(article);\n }\n }\n\n return {\n session1Count: session1.length,\n session2Count: session2.length,\n added,\n removed,\n common,\n };\n}\n\nexport type ShowFilter = 'added' | 'removed' | 'common';\n\n/**\n * Extract a year string from publicationDate, or return empty string.\n */\nfunction extractYear(publicationDate: string | undefined): string {\n if (!publicationDate) return '';\n const year = publicationDate.substring(0, 4);\n return /^\\d{4}$/.test(year) ? year : '';\n}\n\n/**\n * Format an article line for display.\n */\nfunction formatArticleLine(prefix: string, article: Article): string {\n const year = extractYear(article.publicationDate);\n const yearPart = year ? `[${year}] ` : '';\n return ` ${prefix} ${yearPart}${article.title}`;\n}\n\n/**\n * Options for formatting diff with query information.\n */\nexport interface FormatDiffOptions {\n queryDiff?: QueryDiff | undefined;\n noQueryDiff?: boolean | undefined;\n showQueryDiffPlaceholder?: boolean | undefined;\n}\n\n/**\n * Format diff result as human-readable text.\n */\nexport function formatDiff(\n diff: DiffResult,\n session1Id: string,\n session2Id: string,\n show?: ShowFilter,\n options?: FormatDiffOptions,\n): string {\n const lines: string[] = [];\n\n // Header\n lines.push(`Diff: ${session1Id} → ${session2Id}`);\n lines.push(` Session 1: ${diff.session1Count} articles (${session1Id})`);\n lines.push(` Session 2: ${diff.session2Count} articles (${session2Id})`);\n lines.push('');\n\n // Query changes section (if available and not disabled)\n const shouldShowQueryDiff = options?.queryDiff && !options?.noQueryDiff;\n const shouldShowPlaceholder = options?.showQueryDiffPlaceholder && !options?.queryDiff && !options?.noQueryDiff;\n\n if (shouldShowPlaceholder) {\n lines.push('Query changes: (query data not available)');\n lines.push('');\n } else if (shouldShowQueryDiff) {\n lines.push(formatQueryDiff(options.queryDiff!));\n lines.push('');\n lines.push('Result changes:');\n }\n\n // Summary counts\n lines.push(` Common: ${diff.common.length} articles`);\n lines.push(` Added: ${diff.added.length} articles (in ${session2Id} but not ${session1Id})`);\n lines.push(` Removed: ${diff.removed.length} articles (in ${session1Id} but not ${session2Id})`);\n\n // Article lists based on show filter\n const showAdded = !show || show === 'added';\n const showRemoved = !show || show === 'removed';\n const showCommon = show === 'common';\n\n if (showAdded && diff.added.length > 0) {\n lines.push('');\n lines.push(`Added (+${diff.added.length}):`);\n for (const article of diff.added) {\n lines.push(formatArticleLine('+', article));\n }\n }\n\n if (showRemoved && diff.removed.length > 0) {\n lines.push('');\n lines.push(`Removed (-${diff.removed.length}):`);\n for (const article of diff.removed) {\n lines.push(formatArticleLine('-', article));\n }\n }\n\n if (showCommon && diff.common.length > 0) {\n lines.push('');\n lines.push(`Common (${diff.common.length}):`);\n for (const article of diff.common) {\n lines.push(formatArticleLine('=', article));\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format diff result as JSON.\n */\ninterface DiffJsonOutput {\n session1: string;\n session2: string;\n summary: {\n session1Count: number;\n session2Count: number;\n commonCount: number;\n addedCount: number;\n removedCount: number;\n };\n queryDiff?: QueryDiff;\n added?: Article[];\n removed?: Article[];\n common?: Article[];\n}\n\nexport function formatDiffJson(\n diff: DiffResult,\n session1Id: string,\n session2Id: string,\n show?: ShowFilter,\n options?: FormatDiffOptions,\n): string {\n const result: DiffJsonOutput = {\n session1: session1Id,\n session2: session2Id,\n summary: {\n session1Count: diff.session1Count,\n session2Count: diff.session2Count,\n commonCount: diff.common.length,\n addedCount: diff.added.length,\n removedCount: diff.removed.length,\n },\n };\n\n // Add queryDiff if available and not disabled\n if (options?.queryDiff && !options?.noQueryDiff) {\n result.queryDiff = options.queryDiff;\n }\n\n if (!show || show === 'added') {\n result.added = diff.added;\n }\n if (!show || show === 'removed') {\n result.removed = diff.removed;\n }\n if (!show || show === 'common') {\n result.common = diff.common;\n }\n\n return JSON.stringify(result, null, 2);\n}\n\n/**\n * Diff result for a single query block.\n */\nexport interface BlockDiff {\n index: number;\n field: FieldType;\n added: string[];\n removed: string[];\n meshAdded?: string[];\n meshRemoved?: string[];\n emtreeAdded?: string[];\n emtreeRemoved?: string[];\n excludeAdded?: string[];\n excludeRemoved?: string[];\n hasChanges: boolean;\n isNew?: boolean;\n isRemoved?: boolean;\n}\n\n/**\n * Diff result for query filters.\n */\nexport interface FilterDiff {\n yearFromChanged: boolean;\n oldYearFrom?: number | undefined;\n newYearFrom?: number | undefined;\n yearToChanged: boolean;\n oldYearTo?: number | undefined;\n newYearTo?: number | undefined;\n languagesAdded: string[];\n languagesRemoved: string[];\n}\n\n/**\n * Complete query diff result.\n */\nexport interface QueryDiff {\n blocks: BlockDiff[];\n filters: FilterDiff;\n}\n\n/**\n * Compute the set difference: elements in arr2 not in arr1.\n */\nfunction setDiff(arr1: string[], arr2: string[]): string[] {\n const set1 = new Set(arr1);\n return arr2.filter((item) => !set1.has(item));\n}\n\n/**\n * Compare two query blocks and return the differences.\n */\nfunction compareBlocks(\n block1: TermBlock | undefined,\n block2: TermBlock | undefined,\n index: number,\n field: FieldType,\n): BlockDiff {\n const emptyTerms = { keywords: [] as string[], mesh: [] as string[], emtree: [] as string[], exclude: [] as string[] };\n const terms1 = block1 ?? emptyTerms;\n const terms2 = block2 ?? emptyTerms;\n\n const added = setDiff(terms1.keywords ?? [], terms2.keywords ?? []);\n const removed = setDiff(terms2.keywords ?? [], terms1.keywords ?? []);\n const meshAdded = setDiff(terms1.mesh ?? [], terms2.mesh ?? []);\n const meshRemoved = setDiff(terms2.mesh ?? [], terms1.mesh ?? []);\n const emtreeAdded = setDiff(terms1.emtree ?? [], terms2.emtree ?? []);\n const emtreeRemoved = setDiff(terms2.emtree ?? [], terms1.emtree ?? []);\n const excludeAdded = setDiff(terms1.exclude ?? [], terms2.exclude ?? []);\n const excludeRemoved = setDiff(terms2.exclude ?? [], terms1.exclude ?? []);\n\n const hasChanges =\n added.length > 0 ||\n removed.length > 0 ||\n meshAdded.length > 0 ||\n meshRemoved.length > 0 ||\n emtreeAdded.length > 0 ||\n emtreeRemoved.length > 0 ||\n excludeAdded.length > 0 ||\n excludeRemoved.length > 0;\n\n const result: BlockDiff = {\n index,\n field,\n added,\n removed,\n hasChanges,\n };\n\n if (meshAdded.length > 0 || meshRemoved.length > 0) {\n result.meshAdded = meshAdded;\n result.meshRemoved = meshRemoved;\n }\n if (emtreeAdded.length > 0 || emtreeRemoved.length > 0) {\n result.emtreeAdded = emtreeAdded;\n result.emtreeRemoved = emtreeRemoved;\n }\n if (excludeAdded.length > 0 || excludeRemoved.length > 0) {\n result.excludeAdded = excludeAdded;\n result.excludeRemoved = excludeRemoved;\n }\n\n if (!block1) {\n result.isNew = true;\n result.hasChanges = true;\n }\n if (!block2) {\n result.isRemoved = true;\n result.hasChanges = true;\n }\n\n return result;\n}\n\n/**\n * Compute the diff between two QueryAST objects.\n */\nexport function computeQueryDiff(query1: QueryAST, query2: QueryAST): QueryDiff {\n const blocks: BlockDiff[] = [];\n\n const maxBlocks = Math.max(query1.blocks.length, query2.blocks.length);\n\n for (let i = 0; i < maxBlocks; i++) {\n const block1 = query1.blocks[i];\n const block2 = query2.blocks[i];\n\n const field = block2?.field ?? block1?.field ?? 'all';\n const blockDiff = compareBlocks(\n block1?.terms,\n block2?.terms,\n i,\n field,\n );\n blocks.push(blockDiff);\n }\n\n // Compare filters\n const filters: FilterDiff = {\n yearFromChanged: query1.filters.yearFrom !== query2.filters.yearFrom,\n yearToChanged: query1.filters.yearTo !== query2.filters.yearTo,\n languagesAdded: setDiff(query1.filters.languages ?? [], query2.filters.languages ?? []),\n languagesRemoved: setDiff(query2.filters.languages ?? [], query1.filters.languages ?? []),\n };\n\n if (filters.yearFromChanged) {\n filters.oldYearFrom = query1.filters.yearFrom;\n filters.newYearFrom = query2.filters.yearFrom;\n }\n if (filters.yearToChanged) {\n filters.oldYearTo = query1.filters.yearTo;\n filters.newYearTo = query2.filters.yearTo;\n }\n\n return { blocks, filters };\n}\n\n/**\n * Format query diff as human-readable text.\n */\nexport function formatQueryDiff(queryDiff: QueryDiff): string {\n const lines: string[] = [];\n\n lines.push('Query changes:');\n\n // Format block changes\n for (const block of queryDiff.blocks) {\n const blockNum = block.index + 1;\n let blockHeader = ` Block ${blockNum} (${block.field})`;\n if (block.isNew) {\n blockHeader += ' (new block)';\n } else if (block.isRemoved) {\n blockHeader += ' (removed block)';\n }\n\n if (!block.hasChanges) {\n lines.push(`${blockHeader}: no changes`);\n } else {\n lines.push(`${blockHeader}:`);\n\n // Added keywords\n for (const keyword of block.added) {\n lines.push(` + ${keyword}`);\n }\n\n // Removed keywords\n for (const keyword of block.removed) {\n lines.push(` - ${keyword}`);\n }\n\n // MeSH changes\n if (block.meshAdded) {\n for (const term of block.meshAdded) {\n lines.push(` + [MeSH] ${term}`);\n }\n }\n if (block.meshRemoved) {\n for (const term of block.meshRemoved) {\n lines.push(` - [MeSH] ${term}`);\n }\n }\n\n // Emtree changes\n if (block.emtreeAdded) {\n for (const term of block.emtreeAdded) {\n lines.push(` + [Emtree] ${term}`);\n }\n }\n if (block.emtreeRemoved) {\n for (const term of block.emtreeRemoved) {\n lines.push(` - [Emtree] ${term}`);\n }\n }\n\n // Exclude changes\n if (block.excludeAdded) {\n for (const term of block.excludeAdded) {\n lines.push(` + [exclude] ${term}`);\n }\n }\n if (block.excludeRemoved) {\n for (const term of block.excludeRemoved) {\n lines.push(` - [exclude] ${term}`);\n }\n }\n }\n }\n\n // Format filter changes\n const hasFilterChanges =\n queryDiff.filters.yearFromChanged ||\n queryDiff.filters.yearToChanged ||\n queryDiff.filters.languagesAdded.length > 0 ||\n queryDiff.filters.languagesRemoved.length > 0;\n\n if (hasFilterChanges) {\n lines.push('');\n lines.push(' Filters:');\n\n if (queryDiff.filters.yearFromChanged) {\n const oldVal = queryDiff.filters.oldYearFrom ?? '(none)';\n const newVal = queryDiff.filters.newYearFrom ?? '(none)';\n lines.push(` yearFrom: ${oldVal} → ${newVal}`);\n }\n\n if (queryDiff.filters.yearToChanged) {\n const oldVal = queryDiff.filters.oldYearTo ?? '(none)';\n const newVal = queryDiff.filters.newYearTo ?? '(none)';\n lines.push(` yearTo: ${oldVal} → ${newVal}`);\n }\n\n if (queryDiff.filters.languagesAdded.length > 0 || queryDiff.filters.languagesRemoved.length > 0) {\n lines.push(' languages:');\n for (const lang of queryDiff.filters.languagesAdded) {\n lines.push(` + ${lang}`);\n }\n for (const lang of queryDiff.filters.languagesRemoved) {\n lines.push(` - ${lang}`);\n }\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { refExport, refFulltextAttach } from "../../../integration/ref-cli.js";
4
- import { processFulltextEntries } from "../../../fulltext/attach-shared.js";
4
+ import { processFulltextEntries } from "../../../integration/attach-shared.js";
5
5
  async function buildRefLookupFromLibrary(libraryPath, refCliOptions) {
6
6
  const lookup = /* @__PURE__ */ new Map();
7
7
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"attach.js","sources":["../../../../src/cli/commands/fulltext/attach.ts"],"sourcesContent":["/**\n * Standalone fulltext attach command.\n * Attaches fulltext files to existing reference-manager entries.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { FulltextAttachResult } from '../../../integration/types.js';\nimport { refFulltextAttach, refExport, type RefCliOptions } from '../../../integration/ref-cli.js';\nimport { processFulltextEntries } from '../../../fulltext/attach-shared.js';\n\nexport interface FulltextAttachCommandOptions {\n sessionDir: string;\n dryRun: boolean;\n onProgress?: (current: number, total: number) => void;\n}\n\n/**\n * Load the reference library and build a lookup map from identifiers to ref IDs.\n * Returns a map with keys like \"doi:10.1234/test\" and \"pmid:12345678\".\n */\nasync function buildRefLookupFromLibrary(\n libraryPath: string,\n refCliOptions: RefCliOptions,\n): Promise<Map<string, string>> {\n const lookup = new Map<string, string>();\n\n try {\n // Try to read library directly from JSON file\n const content = await readFile(libraryPath, 'utf-8');\n const parsed: unknown = JSON.parse(content);\n\n if (!Array.isArray(parsed)) {\n console.warn('Warning: Reference library file is not a JSON array. Falling back to ref export.');\n throw new Error('Not an array');\n }\n\n const entries = parsed as Array<Record<string, unknown>>;\n for (const entry of entries) {\n const id = entry['id'] as string;\n if (!id) continue;\n\n const doi = entry['DOI'];\n if (doi && typeof doi === 'string') {\n lookup.set(`doi:${doi}`, id);\n }\n const pmid = entry['PMID'];\n if (pmid && typeof pmid === 'string') {\n lookup.set(`pmid:${pmid}`, id);\n }\n }\n } catch {\n // If library doesn't exist or can't be read, try ref export\n try {\n const entries = await refExport('*', refCliOptions) as Array<Record<string, unknown>>;\n if (Array.isArray(entries)) {\n for (const entry of entries) {\n const id = entry['id'] as string;\n if (!id) continue;\n const doi = entry['DOI'];\n if (doi && typeof doi === 'string') {\n lookup.set(`doi:${doi}`, id);\n }\n const pmid = entry['PMID'];\n if (pmid && typeof pmid === 'string') {\n lookup.set(`pmid:${pmid}`, id);\n }\n }\n }\n } catch {\n console.warn('Warning: Could not read reference library. All articles will be skipped as \"not_in_ref\".');\n }\n }\n\n return lookup;\n}\n\n/**\n * Execute the standalone fulltext attach command.\n * Reads the session's fulltext directories and attaches files to ref entries.\n */\nexport async function executeFulltextAttach(\n options: FulltextAttachCommandOptions,\n): Promise<FulltextAttachResult> {\n const { sessionDir, dryRun, onProgress } = options;\n const fulltextDir = join(sessionDir, 'fulltext');\n const libraryPath = join(sessionDir, 'references.json');\n const refCliOptions: RefCliOptions = { libraryPath };\n\n // Build ref lookup from library\n const refLookup = await buildRefLookupFromLibrary(libraryPath, refCliOptions);\n\n return processFulltextEntries({\n fulltextDir,\n refLookup,\n attachFile: (refId, filePath) => refFulltextAttach(refId, filePath, refCliOptions),\n dryRun,\n ...(onProgress ? { onProgress } : {}),\n });\n}\n"],"names":[],"mappings":";;;;AAqBA,eAAe,0BACb,aACA,eAC8B;AAC9B,QAAM,6BAAa,IAAA;AAEnB,MAAI;AAEF,UAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,UAAM,SAAkB,KAAK,MAAM,OAAO;AAE1C,QAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC1B,cAAQ,KAAK,kFAAkF;AAC/F,YAAM,IAAI,MAAM,cAAc;AAAA,IAChC;AAEA,UAAM,UAAU;AAChB,eAAW,SAAS,SAAS;AAC3B,YAAM,KAAK,MAAM,IAAI;AACrB,UAAI,CAAC,GAAI;AAET,YAAM,MAAM,MAAM,KAAK;AACvB,UAAI,OAAO,OAAO,QAAQ,UAAU;AAClC,eAAO,IAAI,OAAO,GAAG,IAAI,EAAE;AAAA,MAC7B;AACA,YAAM,OAAO,MAAM,MAAM;AACzB,UAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,eAAO,IAAI,QAAQ,IAAI,IAAI,EAAE;AAAA,MAC/B;AAAA,IACF;AAAA,EACF,QAAQ;AAEN,QAAI;AACF,YAAM,UAAU,MAAM,UAAU,KAAK,aAAa;AAClD,UAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,mBAAW,SAAS,SAAS;AAC3B,gBAAM,KAAK,MAAM,IAAI;AACrB,cAAI,CAAC,GAAI;AACT,gBAAM,MAAM,MAAM,KAAK;AACvB,cAAI,OAAO,OAAO,QAAQ,UAAU;AAClC,mBAAO,IAAI,OAAO,GAAG,IAAI,EAAE;AAAA,UAC7B;AACA,gBAAM,OAAO,MAAM,MAAM;AACzB,cAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,mBAAO,IAAI,QAAQ,IAAI,IAAI,EAAE;AAAA,UAC/B;AAAA,QACF;AAAA,MACF;AAAA,IACF,QAAQ;AACN,cAAQ,KAAK,0FAA0F;AAAA,IACzG;AAAA,EACF;AAEA,SAAO;AACT;AAMA,eAAsB,sBACpB,SAC+B;AAC/B,QAAM,EAAE,YAAY,QAAQ,WAAA,IAAe;AAC3C,QAAM,cAAc,KAAK,YAAY,UAAU;AAC/C,QAAM,cAAc,KAAK,YAAY,iBAAiB;AACtD,QAAM,gBAA+B,EAAE,YAAA;AAGvC,QAAM,YAAY,MAAM,0BAA0B,aAAa,aAAa;AAE5E,SAAO,uBAAuB;AAAA,IAC5B;AAAA,IACA;AAAA,IACA,YAAY,CAAC,OAAO,aAAa,kBAAkB,OAAO,UAAU,aAAa;AAAA,IACjF;AAAA,IACA,GAAI,aAAa,EAAE,eAAe,CAAA;AAAA,EAAC,CACpC;AACH;"}
1
+ {"version":3,"file":"attach.js","sources":["../../../../src/cli/commands/fulltext/attach.ts"],"sourcesContent":["/**\n * Standalone fulltext attach command.\n * Attaches fulltext files to existing reference-manager entries.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { FulltextAttachResult } from '../../../integration/types.js';\nimport { refFulltextAttach, refExport, type RefCliOptions } from '../../../integration/ref-cli.js';\nimport { processFulltextEntries } from '../../../integration/attach-shared.js';\n\nexport interface FulltextAttachCommandOptions {\n sessionDir: string;\n dryRun: boolean;\n onProgress?: (current: number, total: number) => void;\n}\n\n/**\n * Load the reference library and build a lookup map from identifiers to ref IDs.\n * Returns a map with keys like \"doi:10.1234/test\" and \"pmid:12345678\".\n */\nasync function buildRefLookupFromLibrary(\n libraryPath: string,\n refCliOptions: RefCliOptions,\n): Promise<Map<string, string>> {\n const lookup = new Map<string, string>();\n\n try {\n // Try to read library directly from JSON file\n const content = await readFile(libraryPath, 'utf-8');\n const parsed: unknown = JSON.parse(content);\n\n if (!Array.isArray(parsed)) {\n console.warn('Warning: Reference library file is not a JSON array. Falling back to ref export.');\n throw new Error('Not an array');\n }\n\n const entries = parsed as Array<Record<string, unknown>>;\n for (const entry of entries) {\n const id = entry['id'] as string;\n if (!id) continue;\n\n const doi = entry['DOI'];\n if (doi && typeof doi === 'string') {\n lookup.set(`doi:${doi}`, id);\n }\n const pmid = entry['PMID'];\n if (pmid && typeof pmid === 'string') {\n lookup.set(`pmid:${pmid}`, id);\n }\n }\n } catch {\n // If library doesn't exist or can't be read, try ref export\n try {\n const entries = await refExport('*', refCliOptions) as Array<Record<string, unknown>>;\n if (Array.isArray(entries)) {\n for (const entry of entries) {\n const id = entry['id'] as string;\n if (!id) continue;\n const doi = entry['DOI'];\n if (doi && typeof doi === 'string') {\n lookup.set(`doi:${doi}`, id);\n }\n const pmid = entry['PMID'];\n if (pmid && typeof pmid === 'string') {\n lookup.set(`pmid:${pmid}`, id);\n }\n }\n }\n } catch {\n console.warn('Warning: Could not read reference library. All articles will be skipped as \"not_in_ref\".');\n }\n }\n\n return lookup;\n}\n\n/**\n * Execute the standalone fulltext attach command.\n * Reads the session's fulltext directories and attaches files to ref entries.\n */\nexport async function executeFulltextAttach(\n options: FulltextAttachCommandOptions,\n): Promise<FulltextAttachResult> {\n const { sessionDir, dryRun, onProgress } = options;\n const fulltextDir = join(sessionDir, 'fulltext');\n const libraryPath = join(sessionDir, 'references.json');\n const refCliOptions: RefCliOptions = { libraryPath };\n\n // Build ref lookup from library\n const refLookup = await buildRefLookupFromLibrary(libraryPath, refCliOptions);\n\n return processFulltextEntries({\n fulltextDir,\n refLookup,\n attachFile: (refId, filePath) => refFulltextAttach(refId, filePath, refCliOptions),\n dryRun,\n ...(onProgress ? { onProgress } : {}),\n });\n}\n"],"names":[],"mappings":";;;;AAqBA,eAAe,0BACb,aACA,eAC8B;AAC9B,QAAM,6BAAa,IAAA;AAEnB,MAAI;AAEF,UAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,UAAM,SAAkB,KAAK,MAAM,OAAO;AAE1C,QAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC1B,cAAQ,KAAK,kFAAkF;AAC/F,YAAM,IAAI,MAAM,cAAc;AAAA,IAChC;AAEA,UAAM,UAAU;AAChB,eAAW,SAAS,SAAS;AAC3B,YAAM,KAAK,MAAM,IAAI;AACrB,UAAI,CAAC,GAAI;AAET,YAAM,MAAM,MAAM,KAAK;AACvB,UAAI,OAAO,OAAO,QAAQ,UAAU;AAClC,eAAO,IAAI,OAAO,GAAG,IAAI,EAAE;AAAA,MAC7B;AACA,YAAM,OAAO,MAAM,MAAM;AACzB,UAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,eAAO,IAAI,QAAQ,IAAI,IAAI,EAAE;AAAA,MAC/B;AAAA,IACF;AAAA,EACF,QAAQ;AAEN,QAAI;AACF,YAAM,UAAU,MAAM,UAAU,KAAK,aAAa;AAClD,UAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,mBAAW,SAAS,SAAS;AAC3B,gBAAM,KAAK,MAAM,IAAI;AACrB,cAAI,CAAC,GAAI;AACT,gBAAM,MAAM,MAAM,KAAK;AACvB,cAAI,OAAO,OAAO,QAAQ,UAAU;AAClC,mBAAO,IAAI,OAAO,GAAG,IAAI,EAAE;AAAA,UAC7B;AACA,gBAAM,OAAO,MAAM,MAAM;AACzB,cAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,mBAAO,IAAI,QAAQ,IAAI,IAAI,EAAE;AAAA,UAC/B;AAAA,QACF;AAAA,MACF;AAAA,IACF,QAAQ;AACN,cAAQ,KAAK,0FAA0F;AAAA,IACzG;AAAA,EACF;AAEA,SAAO;AACT;AAMA,eAAsB,sBACpB,SAC+B;AAC/B,QAAM,EAAE,YAAY,QAAQ,WAAA,IAAe;AAC3C,QAAM,cAAc,KAAK,YAAY,UAAU;AAC/C,QAAM,cAAc,KAAK,YAAY,iBAAiB;AACtD,QAAM,gBAA+B,EAAE,YAAA;AAGvC,QAAM,YAAY,MAAM,0BAA0B,aAAa,aAAa;AAE5E,SAAO,uBAAuB;AAAA,IAC5B;AAAA,IACA;AAAA,IACA,YAAY,CAAC,OAAO,aAAa,kBAAkB,OAAO,UAAU,aAAa;AAAA,IACjF;AAAA,IACA,GAAI,aAAa,EAAE,eAAe,CAAA;AAAA,EAAC,CACpC;AACH;"}
@@ -1,5 +1,4 @@
1
- import { DiscoveryConfig } from '../../../fulltext/discovery/index';
2
- import { OAStatus } from '../../../fulltext/types';
1
+ import { DiscoveryConfig, OAStatus } from '@ncukondo/academic-fulltext';
3
2
  export interface FulltextCheckOptions {
4
3
  sessionDir: string;
5
4
  config: DiscoveryConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/check.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAAc,KAAK,eAAe,EAAyB,MAAM,mCAAmC,CAAC;AAE5G,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAMxD,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,eAAe,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,0BAA0B;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,QAAQ,EAAE,0BAA0B,EAAE,CAAC;CACxC;AAoHD;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAiC9B"}
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/check.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAAkC,KAAK,eAAe,EAAyB,KAAK,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAMzI,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,eAAe,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,0BAA0B;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,QAAQ,EAAE,0BAA0B,EAAE,CAAC;CACxC;AAoHD;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAiC9B"}
@@ -1,8 +1,10 @@
1
1
  import { readFile, access, readdir } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { parse } from "yaml";
4
- import { discoverOA } from "../../../fulltext/discovery/index.js";
5
- import { loadMeta, saveMeta } from "../../../fulltext/meta.js";
4
+ import { discoverOA } from "../../../node_modules/@ncukondo/academic-fulltext/dist/discovery/index.js";
5
+ import { loadMeta, saveMeta } from "../../../node_modules/@ncukondo/academic-fulltext/dist/meta.js";
6
+ import "../../../node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js";
7
+ import "node:crypto";
6
8
  const DEFAULT_CONCURRENCY = 3;
7
9
  async function loadIncludedArticles(sessionDir) {
8
10
  const reviewsPath = join(sessionDir, ".internal", "reviews.yaml");
@@ -1 +1 @@
1
- {"version":3,"file":"check.js","sources":["../../../../src/cli/commands/fulltext/check.ts"],"sourcesContent":["/**\n * Fulltext check command.\n * Checks OA availability for included articles in a session.\n */\n\nimport { readFile, readdir, access } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport { discoverOA, type DiscoveryConfig, type DiscoveryArticle } from '../../../fulltext/discovery/index';\nimport { loadMeta, saveMeta } from '../../../fulltext/meta';\nimport type { OAStatus } from '../../../fulltext/types';\nimport type { ReviewFile, ArticleEntry } from '../review/types';\n\n/** Default concurrency for parallel article processing */\nconst DEFAULT_CONCURRENCY = 3;\n\nexport interface FulltextCheckOptions {\n sessionDir: string;\n config: DiscoveryConfig;\n concurrency?: number;\n}\n\nexport interface FulltextCheckArticleResult {\n doi?: string;\n pmid?: string;\n title: string;\n oaStatus: OAStatus;\n locationCount: number;\n}\n\nexport interface FulltextCheckResult {\n summary: {\n total: number;\n open: number;\n closed: number;\n unknown: number;\n };\n articles: FulltextCheckArticleResult[];\n}\n\n/**\n * Load included articles from reviews.yaml\n */\nasync function loadIncludedArticles(sessionDir: string): Promise<ArticleEntry[]> {\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(content) as ReviewFile;\n return reviewFile.articles.filter((a) => a.finalDecision === 'include');\n}\n\n/**\n * Try to find a meta.json matching an article in the fulltext directory.\n * Returns the dirName if found, null otherwise.\n */\nasync function findArticleDir(\n sessionDir: string,\n article: ArticleEntry\n): Promise<string | null> {\n const fulltextDir = join(sessionDir, 'fulltext');\n try {\n await access(fulltextDir);\n } catch {\n return null;\n }\n\n let entries: string[];\n try {\n const dirEntries = await readdir(fulltextDir);\n entries = dirEntries.map(String);\n } catch {\n return null;\n }\n\n for (const entry of entries) {\n try {\n const metaPath = join(fulltextDir, entry, 'meta.json');\n const meta = await loadMeta(metaPath);\n // Match by DOI or PMID\n if (article.doi && meta.doi === article.doi) return entry;\n if (article.pmid && meta.pmid === article.pmid) return entry;\n } catch {\n // Skip entries without valid meta.json\n }\n }\n return null;\n}\n\n/**\n * Process a single article: run OA discovery and optionally update meta.json.\n */\nasync function processArticle(\n article: ArticleEntry,\n sessionDir: string,\n config: DiscoveryConfig\n): Promise<FulltextCheckArticleResult> {\n const discoveryArticle: DiscoveryArticle = {};\n if (article.doi) discoveryArticle.doi = article.doi;\n if (article.pmid) discoveryArticle.pmid = article.pmid;\n if (article.arxivId) discoveryArticle.arxivId = article.arxivId;\n const discoveryResult = await discoverOA(discoveryArticle, config);\n\n const articleResult: FulltextCheckArticleResult = {\n title: article.title,\n oaStatus: discoveryResult.oaStatus,\n locationCount: discoveryResult.locations.length,\n };\n if (article.doi) articleResult.doi = article.doi;\n if (article.pmid) articleResult.pmid = article.pmid;\n\n // Try to update meta.json if a fulltext directory exists for this article\n const dirName = await findArticleDir(sessionDir, article);\n if (dirName) {\n try {\n const metaPath = join(sessionDir, 'fulltext', dirName, 'meta.json');\n const meta = await loadMeta(metaPath);\n meta.oaStatus = discoveryResult.oaStatus;\n meta.oaLocations = discoveryResult.locations;\n meta.checkedAt = new Date().toISOString();\n await saveMeta(metaPath, meta);\n } catch {\n // Meta update is best-effort\n }\n }\n\n return articleResult;\n}\n\n/**\n * Run async tasks with a concurrency limit.\n */\nasync function runWithConcurrency<T>(\n tasks: Array<() => Promise<T>>,\n concurrency: number\n): Promise<PromiseSettledResult<T>[]> {\n const results: PromiseSettledResult<T>[] = new Array(tasks.length);\n let nextIndex = 0;\n\n async function worker(): Promise<void> {\n while (nextIndex < tasks.length) {\n const index = nextIndex++;\n try {\n const value = await tasks[index]!();\n results[index] = { status: 'fulfilled', value };\n } catch (reason) {\n results[index] = { status: 'rejected', reason };\n }\n }\n }\n\n const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker());\n await Promise.all(workers);\n return results;\n}\n\n/**\n * Execute the fulltext check command.\n * Checks OA availability for all included articles in a session,\n * processing articles in parallel with a concurrency limit.\n */\nexport async function executeFulltextCheck(\n options: FulltextCheckOptions\n): Promise<FulltextCheckResult> {\n const { sessionDir, config, concurrency = DEFAULT_CONCURRENCY } = options;\n\n // Load included articles\n const articles = await loadIncludedArticles(sessionDir);\n\n const summary = { total: articles.length, open: 0, closed: 0, unknown: 0 };\n\n const tasks = articles.map(\n (article) => () => processArticle(article, sessionDir, config)\n );\n\n const settled = await runWithConcurrency(tasks, concurrency);\n\n const results: FulltextCheckArticleResult[] = [];\n for (const result of settled) {\n if (result.status === 'fulfilled') {\n results.push(result.value);\n switch (result.value.oaStatus) {\n case 'open':\n summary.open++;\n break;\n case 'closed':\n summary.closed++;\n break;\n case 'unknown':\n summary.unknown++;\n break;\n }\n }\n }\n\n return { summary, articles: results };\n}\n"],"names":["parseYaml"],"mappings":"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"}
1
+ {"version":3,"file":"check.js","sources":["../../../../src/cli/commands/fulltext/check.ts"],"sourcesContent":["/**\n * Fulltext check command.\n * Checks OA availability for included articles in a session.\n */\n\nimport { readFile, readdir, access } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport { discoverOA, loadMeta, saveMeta, type DiscoveryConfig, type DiscoveryArticle, type OAStatus } from '@ncukondo/academic-fulltext';\nimport type { ReviewFile, ArticleEntry } from '../review/types';\n\n/** Default concurrency for parallel article processing */\nconst DEFAULT_CONCURRENCY = 3;\n\nexport interface FulltextCheckOptions {\n sessionDir: string;\n config: DiscoveryConfig;\n concurrency?: number;\n}\n\nexport interface FulltextCheckArticleResult {\n doi?: string;\n pmid?: string;\n title: string;\n oaStatus: OAStatus;\n locationCount: number;\n}\n\nexport interface FulltextCheckResult {\n summary: {\n total: number;\n open: number;\n closed: number;\n unknown: number;\n };\n articles: FulltextCheckArticleResult[];\n}\n\n/**\n * Load included articles from reviews.yaml\n */\nasync function loadIncludedArticles(sessionDir: string): Promise<ArticleEntry[]> {\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(content) as ReviewFile;\n return reviewFile.articles.filter((a) => a.finalDecision === 'include');\n}\n\n/**\n * Try to find a meta.json matching an article in the fulltext directory.\n * Returns the dirName if found, null otherwise.\n */\nasync function findArticleDir(\n sessionDir: string,\n article: ArticleEntry\n): Promise<string | null> {\n const fulltextDir = join(sessionDir, 'fulltext');\n try {\n await access(fulltextDir);\n } catch {\n return null;\n }\n\n let entries: string[];\n try {\n const dirEntries = await readdir(fulltextDir);\n entries = dirEntries.map(String);\n } catch {\n return null;\n }\n\n for (const entry of entries) {\n try {\n const metaPath = join(fulltextDir, entry, 'meta.json');\n const meta = await loadMeta(metaPath);\n // Match by DOI or PMID\n if (article.doi && meta.doi === article.doi) return entry;\n if (article.pmid && meta.pmid === article.pmid) return entry;\n } catch {\n // Skip entries without valid meta.json\n }\n }\n return null;\n}\n\n/**\n * Process a single article: run OA discovery and optionally update meta.json.\n */\nasync function processArticle(\n article: ArticleEntry,\n sessionDir: string,\n config: DiscoveryConfig\n): Promise<FulltextCheckArticleResult> {\n const discoveryArticle: DiscoveryArticle = {};\n if (article.doi) discoveryArticle.doi = article.doi;\n if (article.pmid) discoveryArticle.pmid = article.pmid;\n if (article.arxivId) discoveryArticle.arxivId = article.arxivId;\n const discoveryResult = await discoverOA(discoveryArticle, config);\n\n const articleResult: FulltextCheckArticleResult = {\n title: article.title,\n oaStatus: discoveryResult.oaStatus,\n locationCount: discoveryResult.locations.length,\n };\n if (article.doi) articleResult.doi = article.doi;\n if (article.pmid) articleResult.pmid = article.pmid;\n\n // Try to update meta.json if a fulltext directory exists for this article\n const dirName = await findArticleDir(sessionDir, article);\n if (dirName) {\n try {\n const metaPath = join(sessionDir, 'fulltext', dirName, 'meta.json');\n const meta = await loadMeta(metaPath);\n meta.oaStatus = discoveryResult.oaStatus;\n meta.oaLocations = discoveryResult.locations;\n meta.checkedAt = new Date().toISOString();\n await saveMeta(metaPath, meta);\n } catch {\n // Meta update is best-effort\n }\n }\n\n return articleResult;\n}\n\n/**\n * Run async tasks with a concurrency limit.\n */\nasync function runWithConcurrency<T>(\n tasks: Array<() => Promise<T>>,\n concurrency: number\n): Promise<PromiseSettledResult<T>[]> {\n const results: PromiseSettledResult<T>[] = new Array(tasks.length);\n let nextIndex = 0;\n\n async function worker(): Promise<void> {\n while (nextIndex < tasks.length) {\n const index = nextIndex++;\n try {\n const value = await tasks[index]!();\n results[index] = { status: 'fulfilled', value };\n } catch (reason) {\n results[index] = { status: 'rejected', reason };\n }\n }\n }\n\n const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker());\n await Promise.all(workers);\n return results;\n}\n\n/**\n * Execute the fulltext check command.\n * Checks OA availability for all included articles in a session,\n * processing articles in parallel with a concurrency limit.\n */\nexport async function executeFulltextCheck(\n options: FulltextCheckOptions\n): Promise<FulltextCheckResult> {\n const { sessionDir, config, concurrency = DEFAULT_CONCURRENCY } = options;\n\n // Load included articles\n const articles = await loadIncludedArticles(sessionDir);\n\n const summary = { total: articles.length, open: 0, closed: 0, unknown: 0 };\n\n const tasks = articles.map(\n (article) => () => processArticle(article, sessionDir, config)\n );\n\n const settled = await runWithConcurrency(tasks, concurrency);\n\n const results: FulltextCheckArticleResult[] = [];\n for (const result of settled) {\n if (result.status === 'fulfilled') {\n results.push(result.value);\n switch (result.value.oaStatus) {\n case 'open':\n summary.open++;\n break;\n case 'closed':\n summary.closed++;\n break;\n case 'unknown':\n summary.unknown++;\n break;\n }\n }\n }\n\n return { summary, articles: results };\n}\n"],"names":["parseYaml"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/convert.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,oBAAoB,EAAE,CAAC;CAClC;AAwCD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,sBAAsB,EAC/B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,CAAC,CA6D/B"}
1
+ {"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/convert.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,oBAAoB,EAAE,CAAC;CAClC;AAwCD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,sBAAsB,EAC/B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,CAAC,CA+D/B"}
@@ -1,7 +1,8 @@
1
1
  import { readdir, stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { getArticleDir, getMetaPath, getFulltextDir } from "../../../fulltext/paths.js";
4
- import { convertPmcXmlToMarkdown } from "../../../fulltext/convert/index.js";
3
+ import { getArticleDir, getMetaPath, getFulltextDir } from "../../../node_modules/@ncukondo/academic-fulltext/dist/paths.js";
4
+ import { convertPmcXmlToMarkdown, convertArxivHtmlToMarkdown } from "../../../node_modules/@ncukondo/academic-fulltext/dist/convert/index.js";
5
+ import "node:crypto";
5
6
  async function fileExists(path) {
6
7
  try {
7
8
  await stat(path);
@@ -36,9 +37,12 @@ async function executeFulltextConvert(options, sessionsDir) {
36
37
  for (const dirName of articleDirs) {
37
38
  const articleDir = getArticleDir(sessionDir, dirName);
38
39
  const xmlPath = join(articleDir, "fulltext.xml");
40
+ const htmlPath = join(articleDir, "fulltext.html");
39
41
  const mdPath = join(articleDir, "fulltext.md");
40
42
  const metaPath = getMetaPath(sessionDir, dirName);
41
- if (!await fileExists(xmlPath)) {
43
+ const hasXml = await fileExists(xmlPath);
44
+ const hasHtml = await fileExists(htmlPath);
45
+ if (!hasXml && !hasHtml) {
42
46
  continue;
43
47
  }
44
48
  if (await fileExists(mdPath)) {
@@ -47,11 +51,7 @@ async function executeFulltextConvert(options, sessionsDir) {
47
51
  continue;
48
52
  }
49
53
  const metaPathExists = await fileExists(metaPath);
50
- const result = await convertPmcXmlToMarkdown(
51
- xmlPath,
52
- mdPath,
53
- metaPathExists ? metaPath : void 0
54
- );
54
+ const result = hasXml ? await convertPmcXmlToMarkdown(xmlPath, mdPath, metaPathExists ? metaPath : void 0) : await convertArxivHtmlToMarkdown(htmlPath, mdPath, metaPathExists ? metaPath : void 0);
55
55
  if (result.success) {
56
56
  converted++;
57
57
  articles.push({
@@ -1 +1 @@
1
- {"version":3,"file":"convert.js","sources":["../../../../src/cli/commands/fulltext/convert.ts"],"sourcesContent":["/**\n * Fulltext convert command - converts PMC XML to Markdown.\n */\n\nimport { readdir, stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { getFulltextDir, getArticleDir, getMetaPath } from '../../../fulltext/paths.js';\nimport { convertPmcXmlToMarkdown } from '../../../fulltext/convert/index.js';\n\nexport interface FulltextConvertOptions {\n sessionId: string;\n article?: string;\n}\n\nexport interface ConvertArticleResult {\n dirName: string;\n title: string;\n status: 'converted' | 'skipped' | 'failed';\n error?: string;\n}\n\nexport interface ConvertCommandResult {\n success: boolean;\n converted: number;\n skipped: number;\n failed: number;\n articles: ConvertArticleResult[];\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Get list of article directory names to process.\n */\nasync function getArticleDirs(\n sessionDir: string,\n articleFilter?: string,\n): Promise<string[]> {\n const fulltextDir = getFulltextDir(sessionDir);\n\n if (articleFilter) {\n // Filter to specific article\n const articlePath = getArticleDir(sessionDir, articleFilter);\n if (await fileExists(articlePath)) {\n return [articleFilter];\n }\n return [];\n }\n\n // List all directories in fulltext/\n try {\n const entries = await readdir(fulltextDir, { withFileTypes: true });\n return entries\n .filter((e) => e.isDirectory())\n .map((e) => e.name);\n } catch {\n return [];\n }\n}\n\nexport async function executeFulltextConvert(\n options: FulltextConvertOptions,\n sessionsDir: string,\n): Promise<ConvertCommandResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n const articleDirs = await getArticleDirs(sessionDir, options.article);\n\n const articles: ConvertArticleResult[] = [];\n let converted = 0;\n let skipped = 0;\n let failed = 0;\n\n for (const dirName of articleDirs) {\n const articleDir = getArticleDir(sessionDir, dirName);\n const xmlPath = join(articleDir, 'fulltext.xml');\n const mdPath = join(articleDir, 'fulltext.md');\n const metaPath = getMetaPath(sessionDir, dirName);\n\n // Check if XML exists\n if (!(await fileExists(xmlPath))) {\n continue; // No XML to convert, skip silently\n }\n\n // Check if already converted\n if (await fileExists(mdPath)) {\n skipped++;\n articles.push({ dirName, title: dirName, status: 'skipped' });\n continue;\n }\n\n // Convert\n const metaPathExists = await fileExists(metaPath);\n const result = await convertPmcXmlToMarkdown(\n xmlPath,\n mdPath,\n metaPathExists ? metaPath : undefined,\n );\n\n if (result.success) {\n converted++;\n articles.push({\n dirName,\n title: result.title ?? dirName,\n status: 'converted',\n });\n } else {\n failed++;\n const articleResult: ConvertArticleResult = {\n dirName,\n title: dirName,\n status: 'failed',\n };\n if (result.error) articleResult.error = result.error;\n articles.push(articleResult);\n }\n }\n\n return {\n success: failed === 0,\n converted,\n skipped,\n failed,\n articles,\n };\n}\n"],"names":[],"mappings":";;;;AA6BA,eAAe,WAAW,MAAgC;AACxD,MAAI;AACF,UAAM,KAAK,IAAI;AACf,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAe,eACb,YACA,eACmB;AACnB,QAAM,cAAc,eAAe,UAAU;AAE7C,MAAI,eAAe;AAEjB,UAAM,cAAc,cAAc,YAAY,aAAa;AAC3D,QAAI,MAAM,WAAW,WAAW,GAAG;AACjC,aAAO,CAAC,aAAa;AAAA,IACvB;AACA,WAAO,CAAA;AAAA,EACT;AAGA,MAAI;AACF,UAAM,UAAU,MAAM,QAAQ,aAAa,EAAE,eAAe,MAAM;AAClE,WAAO,QACJ,OAAO,CAAC,MAAM,EAAE,aAAa,EAC7B,IAAI,CAAC,MAAM,EAAE,IAAI;AAAA,EACtB,QAAQ;AACN,WAAO,CAAA;AAAA,EACT;AACF;AAEA,eAAsB,uBACpB,SACA,aAC+B;AAC/B,QAAM,aAAa,KAAK,aAAa,QAAQ,SAAS;AACtD,QAAM,cAAc,MAAM,eAAe,YAAY,QAAQ,OAAO;AAEpE,QAAM,WAAmC,CAAA;AACzC,MAAI,YAAY;AAChB,MAAI,UAAU;AACd,MAAI,SAAS;AAEb,aAAW,WAAW,aAAa;AACjC,UAAM,aAAa,cAAc,YAAY,OAAO;AACpD,UAAM,UAAU,KAAK,YAAY,cAAc;AAC/C,UAAM,SAAS,KAAK,YAAY,aAAa;AAC7C,UAAM,WAAW,YAAY,YAAY,OAAO;AAGhD,QAAI,CAAE,MAAM,WAAW,OAAO,GAAI;AAChC;AAAA,IACF;AAGA,QAAI,MAAM,WAAW,MAAM,GAAG;AAC5B;AACA,eAAS,KAAK,EAAE,SAAS,OAAO,SAAS,QAAQ,WAAW;AAC5D;AAAA,IACF;AAGA,UAAM,iBAAiB,MAAM,WAAW,QAAQ;AAChD,UAAM,SAAS,MAAM;AAAA,MACnB;AAAA,MACA;AAAA,MACA,iBAAiB,WAAW;AAAA,IAAA;AAG9B,QAAI,OAAO,SAAS;AAClB;AACA,eAAS,KAAK;AAAA,QACZ;AAAA,QACA,OAAO,OAAO,SAAS;AAAA,QACvB,QAAQ;AAAA,MAAA,CACT;AAAA,IACH,OAAO;AACL;AACA,YAAM,gBAAsC;AAAA,QAC1C;AAAA,QACA,OAAO;AAAA,QACP,QAAQ;AAAA,MAAA;AAEV,UAAI,OAAO,MAAO,eAAc,QAAQ,OAAO;AAC/C,eAAS,KAAK,aAAa;AAAA,IAC7B;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS,WAAW;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"convert.js","sources":["../../../../src/cli/commands/fulltext/convert.ts"],"sourcesContent":["/**\n * Fulltext convert command - converts PMC XML to Markdown.\n */\n\nimport { readdir, stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { getFulltextDir, getArticleDir, getMetaPath, convertPmcXmlToMarkdown, convertArxivHtmlToMarkdown } from '@ncukondo/academic-fulltext';\n\nexport interface FulltextConvertOptions {\n sessionId: string;\n article?: string;\n}\n\nexport interface ConvertArticleResult {\n dirName: string;\n title: string;\n status: 'converted' | 'skipped' | 'failed';\n error?: string;\n}\n\nexport interface ConvertCommandResult {\n success: boolean;\n converted: number;\n skipped: number;\n failed: number;\n articles: ConvertArticleResult[];\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Get list of article directory names to process.\n */\nasync function getArticleDirs(\n sessionDir: string,\n articleFilter?: string,\n): Promise<string[]> {\n const fulltextDir = getFulltextDir(sessionDir);\n\n if (articleFilter) {\n // Filter to specific article\n const articlePath = getArticleDir(sessionDir, articleFilter);\n if (await fileExists(articlePath)) {\n return [articleFilter];\n }\n return [];\n }\n\n // List all directories in fulltext/\n try {\n const entries = await readdir(fulltextDir, { withFileTypes: true });\n return entries\n .filter((e) => e.isDirectory())\n .map((e) => e.name);\n } catch {\n return [];\n }\n}\n\nexport async function executeFulltextConvert(\n options: FulltextConvertOptions,\n sessionsDir: string,\n): Promise<ConvertCommandResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n const articleDirs = await getArticleDirs(sessionDir, options.article);\n\n const articles: ConvertArticleResult[] = [];\n let converted = 0;\n let skipped = 0;\n let failed = 0;\n\n for (const dirName of articleDirs) {\n const articleDir = getArticleDir(sessionDir, dirName);\n const xmlPath = join(articleDir, 'fulltext.xml');\n const htmlPath = join(articleDir, 'fulltext.html');\n const mdPath = join(articleDir, 'fulltext.md');\n const metaPath = getMetaPath(sessionDir, dirName);\n\n const hasXml = await fileExists(xmlPath);\n const hasHtml = await fileExists(htmlPath);\n\n // No convertible source found\n if (!hasXml && !hasHtml) {\n continue;\n }\n\n // Check if already converted\n if (await fileExists(mdPath)) {\n skipped++;\n articles.push({ dirName, title: dirName, status: 'skipped' });\n continue;\n }\n\n // Convert: prefer PMC XML, fall back to arXiv HTML\n const metaPathExists = await fileExists(metaPath);\n const result = hasXml\n ? await convertPmcXmlToMarkdown(xmlPath, mdPath, metaPathExists ? metaPath : undefined)\n : await convertArxivHtmlToMarkdown(htmlPath, mdPath, metaPathExists ? metaPath : undefined);\n\n if (result.success) {\n converted++;\n articles.push({\n dirName,\n title: result.title ?? dirName,\n status: 'converted',\n });\n } else {\n failed++;\n const articleResult: ConvertArticleResult = {\n dirName,\n title: dirName,\n status: 'failed',\n };\n if (result.error) articleResult.error = result.error;\n articles.push(articleResult);\n }\n }\n\n return {\n success: failed === 0,\n converted,\n skipped,\n failed,\n articles,\n };\n}\n"],"names":[],"mappings":";;;;;AA4BA,eAAe,WAAW,MAAgC;AACxD,MAAI;AACF,UAAM,KAAK,IAAI;AACf,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAe,eACb,YACA,eACmB;AACnB,QAAM,cAAc,eAAe,UAAU;AAE7C,MAAI,eAAe;AAEjB,UAAM,cAAc,cAAc,YAAY,aAAa;AAC3D,QAAI,MAAM,WAAW,WAAW,GAAG;AACjC,aAAO,CAAC,aAAa;AAAA,IACvB;AACA,WAAO,CAAA;AAAA,EACT;AAGA,MAAI;AACF,UAAM,UAAU,MAAM,QAAQ,aAAa,EAAE,eAAe,MAAM;AAClE,WAAO,QACJ,OAAO,CAAC,MAAM,EAAE,aAAa,EAC7B,IAAI,CAAC,MAAM,EAAE,IAAI;AAAA,EACtB,QAAQ;AACN,WAAO,CAAA;AAAA,EACT;AACF;AAEA,eAAsB,uBACpB,SACA,aAC+B;AAC/B,QAAM,aAAa,KAAK,aAAa,QAAQ,SAAS;AACtD,QAAM,cAAc,MAAM,eAAe,YAAY,QAAQ,OAAO;AAEpE,QAAM,WAAmC,CAAA;AACzC,MAAI,YAAY;AAChB,MAAI,UAAU;AACd,MAAI,SAAS;AAEb,aAAW,WAAW,aAAa;AACjC,UAAM,aAAa,cAAc,YAAY,OAAO;AACpD,UAAM,UAAU,KAAK,YAAY,cAAc;AAC/C,UAAM,WAAW,KAAK,YAAY,eAAe;AACjD,UAAM,SAAS,KAAK,YAAY,aAAa;AAC7C,UAAM,WAAW,YAAY,YAAY,OAAO;AAEhD,UAAM,SAAS,MAAM,WAAW,OAAO;AACvC,UAAM,UAAU,MAAM,WAAW,QAAQ;AAGzC,QAAI,CAAC,UAAU,CAAC,SAAS;AACvB;AAAA,IACF;AAGA,QAAI,MAAM,WAAW,MAAM,GAAG;AAC5B;AACA,eAAS,KAAK,EAAE,SAAS,OAAO,SAAS,QAAQ,WAAW;AAC5D;AAAA,IACF;AAGA,UAAM,iBAAiB,MAAM,WAAW,QAAQ;AAChD,UAAM,SAAS,SACX,MAAM,wBAAwB,SAAS,QAAQ,iBAAiB,WAAW,MAAS,IACpF,MAAM,2BAA2B,UAAU,QAAQ,iBAAiB,WAAW,MAAS;AAE5F,QAAI,OAAO,SAAS;AAClB;AACA,eAAS,KAAK;AAAA,QACZ;AAAA,QACA,OAAO,OAAO,SAAS;AAAA,QACvB,QAAQ;AAAA,MAAA,CACT;AAAA,IACH,OAAO;AACL;AACA,YAAM,gBAAsC;AAAA,QAC1C;AAAA,QACA,OAAO;AAAA,QACP,QAAQ;AAAA,MAAA;AAEV,UAAI,OAAO,MAAO,eAAc,QAAQ,OAAO;AAC/C,eAAS,KAAK,aAAa;AAAA,IAC7B;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS,WAAW;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/fetch.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,QAAQ,EAAE,oBAAoB,EAAE,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CA8G9B"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/fetch.ts"],"names":[],"mappings":"AAAA;;GAEG;AAQH,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,QAAQ,EAAE,oBAAoB,EAAE,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAkH9B"}
@@ -1,8 +1,10 @@
1
1
  import { join } from "node:path";
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
3
  import { parse, stringify } from "yaml";
4
- import { loadMeta } from "../../../fulltext/meta.js";
5
- import { fetchAllFulltexts } from "../../../fulltext/download/orchestrator.js";
4
+ import { fetchAllFulltexts } from "../../../node_modules/@ncukondo/academic-fulltext/dist/download/orchestrator.js";
5
+ import "../../../node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js";
6
+ import { loadMeta } from "../../../node_modules/@ncukondo/academic-fulltext/dist/meta.js";
7
+ import "node:crypto";
6
8
  import { executeFulltextConvert } from "./convert.js";
7
9
  async function executeFulltextFetch(options) {
8
10
  const { sessionId, sessionsDir, source, dryRun, concurrency = 3, retryDelay = 1e3 } = options;
@@ -42,7 +44,8 @@ async function executeFulltextFetch(options) {
42
44
  toFetch.push({
43
45
  dirName,
44
46
  oaLocations: locations,
45
- ...meta.pmcid ? { pmcid: meta.pmcid } : {}
47
+ ...meta.pmcid ? { pmcid: meta.pmcid } : {},
48
+ ...meta.arxivId ? { arxivId: meta.arxivId } : {}
46
49
  });
47
50
  }
48
51
  if (dryRun) {
@@ -65,10 +68,10 @@ async function executeFulltextFetch(options) {
65
68
  const results = await fetchAllFulltexts(toFetch, sessionDir, fetchOpts);
66
69
  const convertMarkdown = options.convertMarkdown !== false;
67
70
  if (convertMarkdown) {
68
- const xmlArticles = results.filter(
69
- (r) => r.status === "downloaded" && r.filesDownloaded?.includes("fulltext.xml")
71
+ const convertibleArticles = results.filter(
72
+ (r) => r.status === "downloaded" && (r.filesDownloaded?.includes("fulltext.xml") || r.filesDownloaded?.includes("fulltext.html"))
70
73
  );
71
- for (const article of xmlArticles) {
74
+ for (const article of convertibleArticles) {
72
75
  await executeFulltextConvert({ sessionId, article: article.dirName }, sessionsDir);
73
76
  }
74
77
  }
@@ -111,6 +114,7 @@ async function updateReviews(sessionDir, results) {
111
114
  article.fulltext.hasFiles = {
112
115
  pdf: article.fulltext.hasFiles.pdf || result.filesDownloaded.includes("fulltext.pdf"),
113
116
  xml: article.fulltext.hasFiles.xml || result.filesDownloaded.includes("fulltext.xml"),
117
+ html: article.fulltext.hasFiles.html || result.filesDownloaded.includes("fulltext.html"),
114
118
  markdown: article.fulltext.hasFiles.markdown || result.filesDownloaded.includes("fulltext.md")
115
119
  };
116
120
  changed = true;
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.js","sources":["../../../../src/cli/commands/fulltext/fetch.ts"],"sourcesContent":["/**\n * Fulltext fetch command - downloads OA fulltexts for session articles.\n */\n\nimport { join } from 'node:path';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport type { ReviewFile } from '../review/types.js';\nimport type { FulltextMeta } from '../../../fulltext/types.js';\nimport { loadMeta } from '../../../fulltext/meta.js';\nimport { fetchAllFulltexts, type FetchArticle } from '../../../fulltext/download/orchestrator.js';\nimport { executeFulltextConvert } from './convert.js';\nexport interface FulltextFetchOptions {\n sessionId: string;\n sessionsDir: string;\n source?: string[];\n convertMarkdown?: boolean;\n dryRun?: boolean;\n concurrency?: number;\n retryDelay?: number;\n}\n\nexport interface FulltextFetchArticle {\n dirName: string;\n title: string;\n oaStatus: string;\n locationCount: number;\n}\n\nexport interface FulltextFetchResult {\n summary: {\n total: number;\n downloaded: number;\n failed: number;\n skipped: number;\n };\n articles: FulltextFetchArticle[];\n dryRun?: boolean;\n}\n\n/**\n * Execute the fulltext fetch command.\n * Loads articles from reviews.yaml, checks OA status via meta.json,\n * and downloads PDFs/XMLs from OA sources.\n */\nexport async function executeFulltextFetch(\n options: FulltextFetchOptions,\n): Promise<FulltextFetchResult> {\n const { sessionId, sessionsDir, source, dryRun, concurrency = 3, retryDelay = 1000 } = options;\n const sessionDir = join(sessionsDir, sessionId);\n\n // Load reviews.yaml to find articles with fulltext dirs\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const reviewContent = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(reviewContent) as ReviewFile;\n\n const includedArticles = reviewFile.articles.filter(\n (a) => a.finalDecision === 'include' && a.fulltext?.dirName,\n );\n\n // Load meta.json for each article to check OA status\n const toFetch: FetchArticle[] = [];\n const allArticles: FulltextFetchArticle[] = [];\n let skippedCount = 0;\n\n const fulltextDir = join(sessionDir, 'fulltext');\n for (const article of includedArticles) {\n const dirName = article.fulltext?.dirName;\n if (!dirName) continue;\n\n let meta: FulltextMeta;\n try {\n const metaPath = join(fulltextDir, dirName, 'meta.json');\n meta = await loadMeta(metaPath);\n } catch {\n skippedCount++;\n continue;\n }\n\n const locations = meta.oaLocations ?? [];\n allArticles.push({\n dirName,\n title: meta.title,\n oaStatus: meta.oaStatus,\n locationCount: locations.length,\n });\n\n // Skip if already has PDF or no OA locations\n if (meta.files.pdf || locations.length === 0) {\n skippedCount++;\n continue;\n }\n\n toFetch.push({\n dirName,\n oaLocations: locations,\n ...(meta.pmcid ? { pmcid: meta.pmcid } : {}),\n });\n }\n\n // Dry run: return what would be downloaded\n if (dryRun) {\n return {\n summary: {\n total: includedArticles.length,\n downloaded: 0,\n failed: 0,\n skipped: skippedCount,\n },\n articles: allArticles,\n dryRun: true,\n };\n }\n\n // Execute downloads\n const fetchOpts: Parameters<typeof fetchAllFulltexts>[2] = {\n concurrency,\n retryDelay,\n };\n if (source) fetchOpts.sourceFilter = source;\n const results = await fetchAllFulltexts(toFetch, sessionDir, fetchOpts);\n\n // Auto-convert PMC XML to Markdown (unless --no-convert-markdown)\n const convertMarkdown = options.convertMarkdown !== false;\n if (convertMarkdown) {\n const xmlArticles = results.filter(\n (r) => r.status === 'downloaded' && r.filesDownloaded?.includes('fulltext.xml'),\n );\n for (const article of xmlArticles) {\n await executeFulltextConvert({ sessionId, article: article.dirName }, sessionsDir);\n }\n }\n\n let downloadedCount = 0;\n let failedCount = 0;\n for (const result of results) {\n if (result.status === 'downloaded') {\n downloadedCount++;\n } else if (result.status === 'failed') {\n failedCount++;\n } else if (result.status === 'skipped') {\n skippedCount++;\n }\n }\n\n // Update reviews.yaml with download results\n await updateReviews(sessionDir, results);\n\n return {\n summary: {\n total: includedArticles.length,\n downloaded: downloadedCount,\n failed: failedCount,\n skipped: skippedCount,\n },\n articles: allArticles,\n };\n}\n\n/**\n * Update reviews.yaml fulltext.hasFiles after downloads.\n */\nasync function updateReviews(\n sessionDir: string,\n results: Array<{ dirName: string; status: string; filesDownloaded?: string[] }>,\n): Promise<void> {\n const downloadedDirs = new Set(\n results\n .filter((r) => r.status === 'downloaded')\n .map((r) => r.dirName),\n );\n\n if (downloadedDirs.size === 0) return;\n\n try {\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(content) as ReviewFile;\n let changed = false;\n\n for (const article of reviewFile.articles) {\n if (article.fulltext?.dirName && downloadedDirs.has(article.fulltext.dirName)) {\n const result = results.find((r) => r.dirName === article.fulltext?.dirName);\n if (result?.filesDownloaded) {\n article.fulltext.hasFiles = {\n pdf: article.fulltext.hasFiles.pdf || result.filesDownloaded.includes('fulltext.pdf'),\n xml: article.fulltext.hasFiles.xml || result.filesDownloaded.includes('fulltext.xml'),\n markdown: article.fulltext.hasFiles.markdown || result.filesDownloaded.includes('fulltext.md'),\n };\n changed = true;\n }\n }\n }\n\n if (changed) {\n await writeFile(reviewsPath, stringifyYaml(reviewFile), 'utf-8');\n }\n } catch (e) {\n console.error('Warning: failed to update reviews.yaml:', e);\n }\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":"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"}
1
+ {"version":3,"file":"fetch.js","sources":["../../../../src/cli/commands/fulltext/fetch.ts"],"sourcesContent":["/**\n * Fulltext fetch command - downloads OA fulltexts for session articles.\n */\n\nimport { join } from 'node:path';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport type { ReviewFile } from '../review/types.js';\nimport { loadMeta, fetchAllFulltexts, type FulltextMeta, type FetchArticle } from '@ncukondo/academic-fulltext';\nimport { executeFulltextConvert } from './convert.js';\nexport interface FulltextFetchOptions {\n sessionId: string;\n sessionsDir: string;\n source?: string[];\n convertMarkdown?: boolean;\n dryRun?: boolean;\n concurrency?: number;\n retryDelay?: number;\n}\n\nexport interface FulltextFetchArticle {\n dirName: string;\n title: string;\n oaStatus: string;\n locationCount: number;\n}\n\nexport interface FulltextFetchResult {\n summary: {\n total: number;\n downloaded: number;\n failed: number;\n skipped: number;\n };\n articles: FulltextFetchArticle[];\n dryRun?: boolean;\n}\n\n/**\n * Execute the fulltext fetch command.\n * Loads articles from reviews.yaml, checks OA status via meta.json,\n * and downloads PDFs/XMLs from OA sources.\n */\nexport async function executeFulltextFetch(\n options: FulltextFetchOptions,\n): Promise<FulltextFetchResult> {\n const { sessionId, sessionsDir, source, dryRun, concurrency = 3, retryDelay = 1000 } = options;\n const sessionDir = join(sessionsDir, sessionId);\n\n // Load reviews.yaml to find articles with fulltext dirs\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const reviewContent = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(reviewContent) as ReviewFile;\n\n const includedArticles = reviewFile.articles.filter(\n (a) => a.finalDecision === 'include' && a.fulltext?.dirName,\n );\n\n // Load meta.json for each article to check OA status\n const toFetch: FetchArticle[] = [];\n const allArticles: FulltextFetchArticle[] = [];\n let skippedCount = 0;\n\n const fulltextDir = join(sessionDir, 'fulltext');\n for (const article of includedArticles) {\n const dirName = article.fulltext?.dirName;\n if (!dirName) continue;\n\n let meta: FulltextMeta;\n try {\n const metaPath = join(fulltextDir, dirName, 'meta.json');\n meta = await loadMeta(metaPath);\n } catch {\n skippedCount++;\n continue;\n }\n\n const locations = meta.oaLocations ?? [];\n allArticles.push({\n dirName,\n title: meta.title,\n oaStatus: meta.oaStatus,\n locationCount: locations.length,\n });\n\n // Skip if already has PDF or no OA locations\n if (meta.files.pdf || locations.length === 0) {\n skippedCount++;\n continue;\n }\n\n toFetch.push({\n dirName,\n oaLocations: locations,\n ...(meta.pmcid ? { pmcid: meta.pmcid } : {}),\n ...(meta.arxivId ? { arxivId: meta.arxivId } : {}),\n });\n }\n\n // Dry run: return what would be downloaded\n if (dryRun) {\n return {\n summary: {\n total: includedArticles.length,\n downloaded: 0,\n failed: 0,\n skipped: skippedCount,\n },\n articles: allArticles,\n dryRun: true,\n };\n }\n\n // Execute downloads\n const fetchOpts: Parameters<typeof fetchAllFulltexts>[2] = {\n concurrency,\n retryDelay,\n };\n if (source) fetchOpts.sourceFilter = source;\n const results = await fetchAllFulltexts(toFetch, sessionDir, fetchOpts);\n\n // Auto-convert PMC XML / arXiv HTML to Markdown (unless --no-convert-markdown)\n const convertMarkdown = options.convertMarkdown !== false;\n if (convertMarkdown) {\n const convertibleArticles = results.filter(\n (r) => r.status === 'downloaded' && (\n r.filesDownloaded?.includes('fulltext.xml') ||\n r.filesDownloaded?.includes('fulltext.html')\n ),\n );\n for (const article of convertibleArticles) {\n await executeFulltextConvert({ sessionId, article: article.dirName }, sessionsDir);\n }\n }\n\n let downloadedCount = 0;\n let failedCount = 0;\n for (const result of results) {\n if (result.status === 'downloaded') {\n downloadedCount++;\n } else if (result.status === 'failed') {\n failedCount++;\n } else if (result.status === 'skipped') {\n skippedCount++;\n }\n }\n\n // Update reviews.yaml with download results\n await updateReviews(sessionDir, results);\n\n return {\n summary: {\n total: includedArticles.length,\n downloaded: downloadedCount,\n failed: failedCount,\n skipped: skippedCount,\n },\n articles: allArticles,\n };\n}\n\n/**\n * Update reviews.yaml fulltext.hasFiles after downloads.\n */\nasync function updateReviews(\n sessionDir: string,\n results: Array<{ dirName: string; status: string; filesDownloaded?: string[] }>,\n): Promise<void> {\n const downloadedDirs = new Set(\n results\n .filter((r) => r.status === 'downloaded')\n .map((r) => r.dirName),\n );\n\n if (downloadedDirs.size === 0) return;\n\n try {\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(content) as ReviewFile;\n let changed = false;\n\n for (const article of reviewFile.articles) {\n if (article.fulltext?.dirName && downloadedDirs.has(article.fulltext.dirName)) {\n const result = results.find((r) => r.dirName === article.fulltext?.dirName);\n if (result?.filesDownloaded) {\n article.fulltext.hasFiles = {\n pdf: article.fulltext.hasFiles.pdf || result.filesDownloaded.includes('fulltext.pdf'),\n xml: article.fulltext.hasFiles.xml || result.filesDownloaded.includes('fulltext.xml'),\n html: article.fulltext.hasFiles.html || result.filesDownloaded.includes('fulltext.html'),\n markdown: article.fulltext.hasFiles.markdown || result.filesDownloaded.includes('fulltext.md'),\n };\n changed = true;\n }\n }\n }\n\n if (changed) {\n await writeFile(reviewsPath, stringifyYaml(reviewFile), 'utf-8');\n }\n } catch (e) {\n console.error('Warning: failed to update reviews.yaml:', e);\n }\n}\n"],"names":["parseYaml","stringifyYaml"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAMpD,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,MAAM,CAAC,GACvD,IAAI,CAgVN"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAMpD,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,MAAM,CAAC,GACvD,IAAI,CAkVN"}
@@ -168,6 +168,8 @@ Fulltext ${prefix.toLowerCase()}:`);
168
168
  config: {
169
169
  unpaywallEmail: config.fulltext?.sources?.unpaywall_email ?? "",
170
170
  coreApiKey: config.fulltext?.sources?.core_api_key ?? "",
171
+ ncbiEmail: config.fulltext?.sources?.ncbi_email ?? "",
172
+ ncbiTool: config.fulltext?.sources?.ncbi_tool ?? "search-hub",
171
173
  preferSources: config.fulltext?.sources?.prefer_sources ?? ["pmc", "arxiv", "unpaywall", "core"]
172
174
  }
173
175
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/cli/commands/fulltext/index.ts"],"sourcesContent":["/**\n * Fulltext command group registration.\n */\n\nimport type { Command } from 'commander';\nimport { executeFulltextInit } from './init.js';\nimport { executeFulltextSync } from './sync.js';\nimport { executeFulltextConvert } from './convert.js';\nimport { executeFulltextCheck } from './check.js';\nimport { executeFulltextFetch } from './fetch.js';\nimport { executeFulltextAttach } from './attach.js';\nimport { executeFulltextStatus, type FulltextStatusResult } from './status.js';\nimport { executeFulltextPending, type PendingArticle } from './pending.js';\nimport { formatInitOutput, formatSyncOutput } from './format.js';\nimport type { GlobalOptions } from '../../index.js';\nimport { EXIT_CODES } from '../../exit-codes.js';\nimport { sessionExists } from '../../../session/manager.js';\nimport { loadConfig } from '../../../config/index.js';\nimport { join } from 'node:path';\n\nexport function registerFulltextCommands(\n program: Command,\n getSessionsDir: (opts: GlobalOptions) => Promise<string>,\n): void {\n const fulltextCommand = program\n .command('fulltext')\n .description('Fulltext management: retrieval, conversion, attachment')\n .addHelpText('after', `\nExamples:\n $ search-hub fulltext init SESSION_ID # Create directories for included articles\n $ search-hub fulltext init SESSION_ID --dry-run # Preview what would be created\n $ search-hub fulltext sync SESSION_ID # Detect and register added files\n $ search-hub fulltext sync SESSION_ID --dry-run # Preview what would be synced\n $ search-hub fulltext convert SESSION_ID # Convert PMC XML to Markdown\n $ search-hub fulltext check --session SESSION_ID # Check OA availability\n $ search-hub fulltext fetch SESSION_ID # Download available OA articles\n $ search-hub fulltext fetch SESSION_ID --dry-run # Preview what would be downloaded\n $ search-hub fulltext fetch SESSION_ID --source pmc # Download from specific sources\n $ search-hub fulltext attach SESSION_ID # Attach fulltexts to ref entries\n $ search-hub fulltext attach SESSION_ID --dry-run # Preview what would be attached\n $ search-hub fulltext status SESSION_ID # Show fulltext retrieval status\n $ search-hub fulltext pending SESSION_ID # List articles needing download`);\n\n fulltextCommand\n .command('init')\n .description('Create directories for included articles with meta.json and README')\n .argument('<session-id>', 'session ID')\n .option('--dry-run', 'show what would be created without creating', false)\n .action(async (sessionId: string, options: { dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n const result = await executeFulltextInit({\n sessionId,\n sessionsDir,\n dryRun: options.dryRun,\n });\n\n if (!globalOpts.quiet) {\n console.log(formatInitOutput(result));\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error(\n 'Error:',\n error instanceof Error ? error.message : error,\n );\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('sync')\n .description('Detect and register manually added fulltext files')\n .argument('<session-id>', 'session ID')\n .option('--dry-run', 'show what would be synced without modifying', false)\n .action(async (sessionId: string, options: { dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n const result = await executeFulltextSync({\n sessionId,\n sessionsDir,\n dryRun: options.dryRun,\n });\n\n if (!globalOpts.quiet) {\n console.log(formatSyncOutput(result));\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error(\n 'Error:',\n error instanceof Error ? error.message : error,\n );\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('convert')\n .description('Convert PMC XML files to Markdown')\n .argument('<session-id>', 'session ID')\n .option('--article <dir>', 'convert specific article directory')\n .action(async (sessionId: string, options?: { article?: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n if (!(await sessionExists(sessionId, sessionsDir))) {\n if (!globalOpts.quiet) {\n console.error(`Error: session '${sessionId}' not found`);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n return;\n }\n\n const convertOpts: Parameters<typeof executeFulltextConvert>[0] = { sessionId };\n if (options?.article) convertOpts.article = options.article;\n const result = await executeFulltextConvert(convertOpts, sessionsDir);\n\n if (!globalOpts.quiet) {\n console.log(`Converted: ${result.converted} Skipped: ${result.skipped} Failed: ${result.failed}`);\n for (const article of result.articles) {\n const icon = article.status === 'converted' ? '+' : article.status === 'skipped' ? '-' : '!';\n console.log(` [${icon}] ${article.dirName}: ${article.title}`);\n }\n }\n\n process.exitCode = result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.SESSION_ERROR;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('attach')\n .description('Attach fulltext files to reference-manager entries')\n .argument('<session-id>', 'session ID')\n .option('--dry-run', 'show what would be attached without attaching', false)\n .action(async (sessionId: string, options: { dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n if (!(await sessionExists(sessionId, sessionsDir))) {\n if (!globalOpts.quiet) {\n console.error(`Error: session '${sessionId}' not found`);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n return;\n }\n\n const sessionDir = join(sessionsDir, sessionId);\n\n const result = await executeFulltextAttach({\n sessionDir,\n dryRun: options.dryRun,\n });\n\n if (!globalOpts.quiet) {\n const prefix = options.dryRun ? 'Would attach' : 'Attached';\n console.log(`\\nFulltext ${prefix.toLowerCase()}:`);\n if (result.summary.attached > 0) {\n const totalFiles = result.attached.reduce((sum, a) => sum + a.files.length, 0);\n console.log(` ✓ ${result.summary.attached} articles (${totalFiles} files)`);\n for (const item of result.attached) {\n console.log(` ${item.refId}: ${item.files.join(', ')}`);\n }\n }\n if (result.summary.skipped > 0) {\n console.log(` ⚠ ${result.summary.skipped} skipped`);\n }\n if (result.summary.failed > 0) {\n console.log(` ✗ ${result.summary.failed} failed`);\n for (const item of result.failed) {\n console.log(` ${item.dirName}: ${item.reason}`);\n }\n }\n if (result.summary.total === 0) {\n console.log(' No fulltext directories found.');\n }\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error(\n 'Error:',\n error instanceof Error ? error.message : error,\n );\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('check')\n .description('Check Open Access availability for included articles')\n .requiredOption('--session <id>', 'session ID')\n .option('--format <format>', 'output format (table or json)', 'table')\n .action(async (options: { session: string; format: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n const sessionDir = join(sessionsDir, options.session);\n const config = await loadConfig(\n globalOpts.config ? { globalConfigPath: globalOpts.config } : {}\n );\n const result = await executeFulltextCheck({\n sessionDir,\n config: {\n unpaywallEmail: config.fulltext?.sources?.unpaywall_email ?? '',\n coreApiKey: config.fulltext?.sources?.core_api_key ?? '',\n preferSources: config.fulltext?.sources?.prefer_sources ?? ['pmc', 'arxiv', 'unpaywall', 'core'],\n },\n });\n\n if (options.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (!globalOpts.quiet) {\n console.log(`\\nOA Status Summary:`);\n console.log(` Open Access: ${result.summary.open}`);\n console.log(` Closed Access: ${result.summary.closed}`);\n console.log(` Unknown: ${result.summary.unknown}`);\n console.log(` Total: ${result.summary.total}`);\n if (result.summary.open > 0) {\n console.log(`\\nRun \\`fulltext fetch\\` to download available OA articles.`);\n }\n }\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('fetch')\n .description('Download available OA fulltexts')\n .argument('<session-id>', 'session ID')\n .option('--source <sources>', 'filter by source (comma-separated: pmc,arxiv,unpaywall,core)')\n .option('--no-convert-markdown', 'skip auto-conversion of PMC XML to Markdown')\n .option('--dry-run', 'show what would be downloaded without downloading', false)\n .action(async (sessionId: string, options: { source?: string; convertMarkdown: boolean; dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n const fetchOpts: Parameters<typeof executeFulltextFetch>[0] = {\n sessionId,\n sessionsDir,\n convertMarkdown: options.convertMarkdown,\n dryRun: options.dryRun,\n };\n if (options.source) fetchOpts.source = options.source.split(',');\n const result = await executeFulltextFetch(fetchOpts);\n\n if (!globalOpts.quiet) {\n if (result.dryRun) {\n console.log(`\\nDry run: would fetch ${result.articles.length} articles`);\n for (const article of result.articles) {\n console.log(` ${article.dirName}: ${article.title} (${article.locationCount} sources)`);\n }\n } else {\n console.log(`\\nFulltext Fetch Summary:`);\n console.log(` Downloaded: ${result.summary.downloaded}`);\n console.log(` Failed: ${result.summary.failed}`);\n console.log(` Skipped: ${result.summary.skipped}`);\n console.log(` Total: ${result.summary.total}`);\n }\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('status')\n .description('Show overall fulltext retrieval status')\n .argument('<session-id>', 'session ID')\n .option('--format <format>', 'output format (table or json)', 'table')\n .action(async (sessionId: string, options: { format: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n const sessionDir = join(sessionsDir, sessionId);\n const result = await executeFulltextStatus({\n sessionDir,\n format: options.format as 'table' | 'json',\n });\n\n if (options.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (!globalOpts.quiet) {\n console.log(formatStatusOutput(result, sessionId));\n }\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('pending')\n .description('List articles needing manual fulltext download')\n .argument('<session-id>', 'session ID')\n .option('--format <format>', 'output format (table or json)', 'table')\n .option('--export <file>', 'export URLs to file for batch download')\n .action(async (sessionId: string, options: { format: string; export?: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n const sessionDir = join(sessionsDir, sessionId);\n const result = await executeFulltextPending({\n sessionDir,\n format: options.format as 'table' | 'json',\n exportPath: options.export,\n });\n\n if (options.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (!globalOpts.quiet) {\n console.log(formatPendingOutput(result.articles, result.totalPending));\n if (options.export) {\n console.log(`\\nExported URLs to ${options.export}`);\n } else if (result.totalPending > 0) {\n console.log(`\\nExport URLs: fulltext pending ${sessionId} --export urls.txt`);\n }\n }\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n}\n\nfunction formatStatusOutput(result: FulltextStatusResult, sessionId: string): string {\n const lines = [\n `Fulltext Status: ${sessionId}`,\n '',\n ` Included articles: ${result.totalIncluded}`,\n ` With fulltext: ${result.withFulltext}`,\n ` - PDF only: ${result.pdfOnly}`,\n ` - Markdown only: ${result.markdownOnly}`,\n ` - Both: ${result.both}`,\n ` Pending: ${result.pending} (directories created, no files)`,\n ` Not initialized: ${result.notInitialized} (no directory)`,\n ];\n return lines.join('\\n');\n}\n\nfunction formatPendingOutput(articles: PendingArticle[], total: number): string {\n if (total === 0) {\n return 'All included articles have fulltext.';\n }\n\n const lines: string[] = [`${total} articles need fulltext:`, ''];\n\n for (const [i, article] of articles.entries()) {\n const num = i + 1;\n const identifier = article.dirName ?? '(not initialized)';\n lines.push(`${num}. ${identifier} - \"${article.title}\"`);\n if (article.doi) {\n lines.push(` DOI: ${article.doi}`);\n }\n if (article.publisherUrl) {\n lines.push(` Publisher: ${article.publisherUrl}`);\n }\n if (article.oaLocations) {\n for (const loc of article.oaLocations) {\n lines.push(` ${loc.source}: ${loc.url}`);\n }\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/cli/commands/fulltext/index.ts"],"sourcesContent":["/**\n * Fulltext command group registration.\n */\n\nimport type { Command } from 'commander';\nimport { executeFulltextInit } from './init.js';\nimport { executeFulltextSync } from './sync.js';\nimport { executeFulltextConvert } from './convert.js';\nimport { executeFulltextCheck } from './check.js';\nimport { executeFulltextFetch } from './fetch.js';\nimport { executeFulltextAttach } from './attach.js';\nimport { executeFulltextStatus, type FulltextStatusResult } from './status.js';\nimport { executeFulltextPending, type PendingArticle } from './pending.js';\nimport { formatInitOutput, formatSyncOutput } from './format.js';\nimport type { GlobalOptions } from '../../index.js';\nimport { EXIT_CODES } from '../../exit-codes.js';\nimport { sessionExists } from '../../../session/manager.js';\nimport { loadConfig } from '../../../config/index.js';\nimport { join } from 'node:path';\n\nexport function registerFulltextCommands(\n program: Command,\n getSessionsDir: (opts: GlobalOptions) => Promise<string>,\n): void {\n const fulltextCommand = program\n .command('fulltext')\n .description('Fulltext management: retrieval, conversion, attachment')\n .addHelpText('after', `\nExamples:\n $ search-hub fulltext init SESSION_ID # Create directories for included articles\n $ search-hub fulltext init SESSION_ID --dry-run # Preview what would be created\n $ search-hub fulltext sync SESSION_ID # Detect and register added files\n $ search-hub fulltext sync SESSION_ID --dry-run # Preview what would be synced\n $ search-hub fulltext convert SESSION_ID # Convert PMC XML to Markdown\n $ search-hub fulltext check --session SESSION_ID # Check OA availability\n $ search-hub fulltext fetch SESSION_ID # Download available OA articles\n $ search-hub fulltext fetch SESSION_ID --dry-run # Preview what would be downloaded\n $ search-hub fulltext fetch SESSION_ID --source pmc # Download from specific sources\n $ search-hub fulltext attach SESSION_ID # Attach fulltexts to ref entries\n $ search-hub fulltext attach SESSION_ID --dry-run # Preview what would be attached\n $ search-hub fulltext status SESSION_ID # Show fulltext retrieval status\n $ search-hub fulltext pending SESSION_ID # List articles needing download`);\n\n fulltextCommand\n .command('init')\n .description('Create directories for included articles with meta.json and README')\n .argument('<session-id>', 'session ID')\n .option('--dry-run', 'show what would be created without creating', false)\n .action(async (sessionId: string, options: { dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n const result = await executeFulltextInit({\n sessionId,\n sessionsDir,\n dryRun: options.dryRun,\n });\n\n if (!globalOpts.quiet) {\n console.log(formatInitOutput(result));\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error(\n 'Error:',\n error instanceof Error ? error.message : error,\n );\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('sync')\n .description('Detect and register manually added fulltext files')\n .argument('<session-id>', 'session ID')\n .option('--dry-run', 'show what would be synced without modifying', false)\n .action(async (sessionId: string, options: { dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n const result = await executeFulltextSync({\n sessionId,\n sessionsDir,\n dryRun: options.dryRun,\n });\n\n if (!globalOpts.quiet) {\n console.log(formatSyncOutput(result));\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error(\n 'Error:',\n error instanceof Error ? error.message : error,\n );\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('convert')\n .description('Convert PMC XML files to Markdown')\n .argument('<session-id>', 'session ID')\n .option('--article <dir>', 'convert specific article directory')\n .action(async (sessionId: string, options?: { article?: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n if (!(await sessionExists(sessionId, sessionsDir))) {\n if (!globalOpts.quiet) {\n console.error(`Error: session '${sessionId}' not found`);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n return;\n }\n\n const convertOpts: Parameters<typeof executeFulltextConvert>[0] = { sessionId };\n if (options?.article) convertOpts.article = options.article;\n const result = await executeFulltextConvert(convertOpts, sessionsDir);\n\n if (!globalOpts.quiet) {\n console.log(`Converted: ${result.converted} Skipped: ${result.skipped} Failed: ${result.failed}`);\n for (const article of result.articles) {\n const icon = article.status === 'converted' ? '+' : article.status === 'skipped' ? '-' : '!';\n console.log(` [${icon}] ${article.dirName}: ${article.title}`);\n }\n }\n\n process.exitCode = result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.SESSION_ERROR;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('attach')\n .description('Attach fulltext files to reference-manager entries')\n .argument('<session-id>', 'session ID')\n .option('--dry-run', 'show what would be attached without attaching', false)\n .action(async (sessionId: string, options: { dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n if (!(await sessionExists(sessionId, sessionsDir))) {\n if (!globalOpts.quiet) {\n console.error(`Error: session '${sessionId}' not found`);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n return;\n }\n\n const sessionDir = join(sessionsDir, sessionId);\n\n const result = await executeFulltextAttach({\n sessionDir,\n dryRun: options.dryRun,\n });\n\n if (!globalOpts.quiet) {\n const prefix = options.dryRun ? 'Would attach' : 'Attached';\n console.log(`\\nFulltext ${prefix.toLowerCase()}:`);\n if (result.summary.attached > 0) {\n const totalFiles = result.attached.reduce((sum, a) => sum + a.files.length, 0);\n console.log(` ✓ ${result.summary.attached} articles (${totalFiles} files)`);\n for (const item of result.attached) {\n console.log(` ${item.refId}: ${item.files.join(', ')}`);\n }\n }\n if (result.summary.skipped > 0) {\n console.log(` ⚠ ${result.summary.skipped} skipped`);\n }\n if (result.summary.failed > 0) {\n console.log(` ✗ ${result.summary.failed} failed`);\n for (const item of result.failed) {\n console.log(` ${item.dirName}: ${item.reason}`);\n }\n }\n if (result.summary.total === 0) {\n console.log(' No fulltext directories found.');\n }\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error(\n 'Error:',\n error instanceof Error ? error.message : error,\n );\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('check')\n .description('Check Open Access availability for included articles')\n .requiredOption('--session <id>', 'session ID')\n .option('--format <format>', 'output format (table or json)', 'table')\n .action(async (options: { session: string; format: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n const sessionDir = join(sessionsDir, options.session);\n const config = await loadConfig(\n globalOpts.config ? { globalConfigPath: globalOpts.config } : {}\n );\n const result = await executeFulltextCheck({\n sessionDir,\n config: {\n unpaywallEmail: config.fulltext?.sources?.unpaywall_email ?? '',\n coreApiKey: config.fulltext?.sources?.core_api_key ?? '',\n ncbiEmail: config.fulltext?.sources?.ncbi_email ?? '',\n ncbiTool: config.fulltext?.sources?.ncbi_tool ?? 'search-hub',\n preferSources: config.fulltext?.sources?.prefer_sources ?? ['pmc', 'arxiv', 'unpaywall', 'core'],\n },\n });\n\n if (options.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (!globalOpts.quiet) {\n console.log(`\\nOA Status Summary:`);\n console.log(` Open Access: ${result.summary.open}`);\n console.log(` Closed Access: ${result.summary.closed}`);\n console.log(` Unknown: ${result.summary.unknown}`);\n console.log(` Total: ${result.summary.total}`);\n if (result.summary.open > 0) {\n console.log(`\\nRun \\`fulltext fetch\\` to download available OA articles.`);\n }\n }\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('fetch')\n .description('Download available OA fulltexts')\n .argument('<session-id>', 'session ID')\n .option('--source <sources>', 'filter by source (comma-separated: pmc,arxiv,unpaywall,core)')\n .option('--no-convert-markdown', 'skip auto-conversion of PMC XML to Markdown')\n .option('--dry-run', 'show what would be downloaded without downloading', false)\n .action(async (sessionId: string, options: { source?: string; convertMarkdown: boolean; dryRun: boolean }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n\n const fetchOpts: Parameters<typeof executeFulltextFetch>[0] = {\n sessionId,\n sessionsDir,\n convertMarkdown: options.convertMarkdown,\n dryRun: options.dryRun,\n };\n if (options.source) fetchOpts.source = options.source.split(',');\n const result = await executeFulltextFetch(fetchOpts);\n\n if (!globalOpts.quiet) {\n if (result.dryRun) {\n console.log(`\\nDry run: would fetch ${result.articles.length} articles`);\n for (const article of result.articles) {\n console.log(` ${article.dirName}: ${article.title} (${article.locationCount} sources)`);\n }\n } else {\n console.log(`\\nFulltext Fetch Summary:`);\n console.log(` Downloaded: ${result.summary.downloaded}`);\n console.log(` Failed: ${result.summary.failed}`);\n console.log(` Skipped: ${result.summary.skipped}`);\n console.log(` Total: ${result.summary.total}`);\n }\n }\n\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('status')\n .description('Show overall fulltext retrieval status')\n .argument('<session-id>', 'session ID')\n .option('--format <format>', 'output format (table or json)', 'table')\n .action(async (sessionId: string, options: { format: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n const sessionDir = join(sessionsDir, sessionId);\n const result = await executeFulltextStatus({\n sessionDir,\n format: options.format as 'table' | 'json',\n });\n\n if (options.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (!globalOpts.quiet) {\n console.log(formatStatusOutput(result, sessionId));\n }\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n\n fulltextCommand\n .command('pending')\n .description('List articles needing manual fulltext download')\n .argument('<session-id>', 'session ID')\n .option('--format <format>', 'output format (table or json)', 'table')\n .option('--export <file>', 'export URLs to file for batch download')\n .action(async (sessionId: string, options: { format: string; export?: string }) => {\n const globalOpts = program.opts() as GlobalOptions;\n try {\n const sessionsDir = await getSessionsDir(globalOpts);\n const sessionDir = join(sessionsDir, sessionId);\n const result = await executeFulltextPending({\n sessionDir,\n format: options.format as 'table' | 'json',\n exportPath: options.export,\n });\n\n if (options.format === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (!globalOpts.quiet) {\n console.log(formatPendingOutput(result.articles, result.totalPending));\n if (options.export) {\n console.log(`\\nExported URLs to ${options.export}`);\n } else if (result.totalPending > 0) {\n console.log(`\\nExport URLs: fulltext pending ${sessionId} --export urls.txt`);\n }\n }\n process.exitCode = EXIT_CODES.SUCCESS;\n } catch (error) {\n if (!globalOpts.quiet) {\n console.error('Error:', error instanceof Error ? error.message : error);\n }\n process.exitCode = EXIT_CODES.SESSION_ERROR;\n }\n });\n}\n\nfunction formatStatusOutput(result: FulltextStatusResult, sessionId: string): string {\n const lines = [\n `Fulltext Status: ${sessionId}`,\n '',\n ` Included articles: ${result.totalIncluded}`,\n ` With fulltext: ${result.withFulltext}`,\n ` - PDF only: ${result.pdfOnly}`,\n ` - Markdown only: ${result.markdownOnly}`,\n ` - Both: ${result.both}`,\n ` Pending: ${result.pending} (directories created, no files)`,\n ` Not initialized: ${result.notInitialized} (no directory)`,\n ];\n return lines.join('\\n');\n}\n\nfunction formatPendingOutput(articles: PendingArticle[], total: number): string {\n if (total === 0) {\n return 'All included articles have fulltext.';\n }\n\n const lines: string[] = [`${total} articles need fulltext:`, ''];\n\n for (const [i, article] of articles.entries()) {\n const num = i + 1;\n const identifier = article.dirName ?? '(not initialized)';\n lines.push(`${num}. ${identifier} - \"${article.title}\"`);\n if (article.doi) {\n lines.push(` DOI: ${article.doi}`);\n }\n if (article.publisherUrl) {\n lines.push(` Publisher: ${article.publisherUrl}`);\n }\n if (article.oaLocations) {\n for (const loc of article.oaLocations) {\n lines.push(` ${loc.source}: ${loc.url}`);\n }\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAaH,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,kBAAkB,CAAC,CA8G7B"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AASH,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,kBAAkB,CAAC,CA8G7B"}
@@ -2,10 +2,11 @@ import { join } from "node:path";
2
2
  import { readFile, mkdir, writeFile } from "node:fs/promises";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { parse, stringify } from "yaml";
5
- import { generateCitationKey, generateDirName } from "../../../fulltext/citation-key.js";
6
- import { createMeta, saveMeta } from "../../../fulltext/meta.js";
7
- import { generateReadme } from "../../../fulltext/readme.js";
8
- import { getArticleDir, getMetaPath, getReadmePath, getFulltextDir } from "../../../fulltext/paths.js";
5
+ import { createMeta, saveMeta } from "../../../node_modules/@ncukondo/academic-fulltext/dist/meta.js";
6
+ import { getArticleDir, getMetaPath, getReadmePath, getFulltextDir } from "../../../node_modules/@ncukondo/academic-fulltext/dist/paths.js";
7
+ import "../../../node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js";
8
+ import { generateCitationKey, generateDirName } from "../../../node_modules/@ncukondo/academic-fulltext/dist/citation-key.js";
9
+ import { generateReadme } from "../../../node_modules/@ncukondo/academic-fulltext/dist/readme.js";
9
10
  async function executeFulltextInit(options) {
10
11
  const { sessionId, sessionsDir, dryRun } = options;
11
12
  const sessionDir = join(sessionsDir, sessionId);
@@ -50,7 +51,7 @@ async function executeFulltextInit(options) {
50
51
  const meta = createMeta(metaOpts);
51
52
  const fulltextRef = {
52
53
  dirName,
53
- hasFiles: { pdf: false, xml: false, markdown: false }
54
+ hasFiles: { pdf: false, xml: false, html: false, markdown: false }
54
55
  };
55
56
  if (!dryRun) {
56
57
  const articleDir = getArticleDir(sessionDir, dirName);