citeclaw 2.0.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 (1723) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +152 -0
  3. package/app.js +318 -0
  4. package/config.dev.yaml +124 -0
  5. package/config.prod.yaml +123 -0
  6. package/config.yaml +0 -0
  7. package/lib/Citation.js +101 -0
  8. package/lib/CitoidError.js +61 -0
  9. package/lib/CitoidRequest.js +150 -0
  10. package/lib/CitoidResponse.js +44 -0
  11. package/lib/CitoidService.js +1118 -0
  12. package/lib/Exporter.js +1052 -0
  13. package/lib/OutgoingRequestThrottle.js +96 -0
  14. package/lib/Scraper.js +617 -0
  15. package/lib/Translator.js +76 -0
  16. package/lib/ZoteroService.js +220 -0
  17. package/lib/api-util.js +125 -0
  18. package/lib/external-apis/CrossRefService.js +122 -0
  19. package/lib/external-apis/OpenAlexService.js +245 -0
  20. package/lib/external-apis/PubMedService.js +82 -0
  21. package/lib/external-apis/SemanticScholarService.js +221 -0
  22. package/lib/external-apis/WaybackMachine.js +92 -0
  23. package/lib/hostIsAllowed.js +128 -0
  24. package/lib/swagger-ui.js +104 -0
  25. package/lib/translators/README.md +12 -0
  26. package/lib/translators/bePress.js +319 -0
  27. package/lib/translators/coins.js +360 -0
  28. package/lib/translators/crossRef.js +485 -0
  29. package/lib/translators/dublinCore.js +215 -0
  30. package/lib/translators/general.js +157 -0
  31. package/lib/translators/openGraph.js +177 -0
  32. package/lib/translators/util/index.js +209 -0
  33. package/lib/unshorten.js +109 -0
  34. package/lib/util.js +410 -0
  35. package/lib/zotero/cachedTypes.js +232 -0
  36. package/lib/zotero/typeSchemaData.js +366 -0
  37. package/package.json +121 -0
  38. package/routes/citoid.js +101 -0
  39. package/routes/info.js +81 -0
  40. package/routes/root.js +51 -0
  41. package/scripts/botcite.js +5021 -0
  42. package/scripts/citeclaw.js +10 -0
  43. package/server.js +12 -0
  44. package/spec.yaml +156 -0
  45. package/targets.yaml +2 -0
  46. package/vendor/translators-official/ABC News Australia.js +224 -0
  47. package/vendor/translators-official/ACLS Humanities EBook.js +165 -0
  48. package/vendor/translators-official/ACLWeb.js +600 -0
  49. package/vendor/translators-official/ACM Digital Library.js +708 -0
  50. package/vendor/translators-official/ACM Queue.js +391 -0
  51. package/vendor/translators-official/ACS Publications.js +633 -0
  52. package/vendor/translators-official/ADS Bibcode.js +664 -0
  53. package/vendor/translators-official/AEA Web.js +450 -0
  54. package/vendor/translators-official/AGRIS.js +311 -0
  55. package/vendor/translators-official/AIP.js +295 -0
  56. package/vendor/translators-official/AMS Journals.js +136 -0
  57. package/vendor/translators-official/AMS MathSciNet (Legacy).js +313 -0
  58. package/vendor/translators-official/AMS MathSciNet.js +288 -0
  59. package/vendor/translators-official/APA PsycNET.js +915 -0
  60. package/vendor/translators-official/APN.ru.js +132 -0
  61. package/vendor/translators-official/APS-Physics.js +204 -0
  62. package/vendor/translators-official/APS.js +542 -0
  63. package/vendor/translators-official/ARTFL Encyclopedie.js +283 -0
  64. package/vendor/translators-official/ARTnews.js +155 -0
  65. package/vendor/translators-official/ARTstor.js +619 -0
  66. package/vendor/translators-official/ASCE.js +199 -0
  67. package/vendor/translators-official/ASCO Meeting Library.js +153 -0
  68. package/vendor/translators-official/ASTIS.js +337 -0
  69. package/vendor/translators-official/ATS International Journal.js +290 -0
  70. package/vendor/translators-official/Access Engineering.js +384 -0
  71. package/vendor/translators-official/Access Medicine.js +343 -0
  72. package/vendor/translators-official/Access Science.js +391 -0
  73. package/vendor/translators-official/Adam Matthew Digital.js +405 -0
  74. package/vendor/translators-official/Agencia del ISBN.js +77 -0
  75. package/vendor/translators-official/Ahval News.js +309 -0
  76. package/vendor/translators-official/Air University Journals.js +236 -0
  77. package/vendor/translators-official/Airiti.js +423 -0
  78. package/vendor/translators-official/Alexander Street Press.js +198 -0
  79. package/vendor/translators-official/AllAfrica.js +291 -0
  80. package/vendor/translators-official/Alsharekh.js +242 -0
  81. package/vendor/translators-official/AlterNet.js +134 -0
  82. package/vendor/translators-official/Aluka.js +261 -0
  83. package/vendor/translators-official/Amazon.js +1043 -0
  84. package/vendor/translators-official/American Archive of Public Broadcasting.js +592 -0
  85. package/vendor/translators-official/American Institute of Aeronautics and Astronautics.js +210 -0
  86. package/vendor/translators-official/American Prospect.js +114 -0
  87. package/vendor/translators-official/Anarchist Library.js +382 -0
  88. package/vendor/translators-official/Ancestry.com US Federal Census.js +164 -0
  89. package/vendor/translators-official/Annual Reviews.js +329 -0
  90. package/vendor/translators-official/Antikvarium.hu.js +280 -0
  91. package/vendor/translators-official/AquaDocs.js +390 -0
  92. package/vendor/translators-official/Archeion.js +249 -0
  93. package/vendor/translators-official/Archiv fuer Sozialgeschichte.js +157 -0
  94. package/vendor/translators-official/Archive Ouverte en Sciences de l'Information et de la Communication (AOSIC).js +179 -0
  95. package/vendor/translators-official/Archives Canada.js +142 -0
  96. package/vendor/translators-official/Ariana News.js +203 -0
  97. package/vendor/translators-official/Art Institute of Chicago.js +279 -0
  98. package/vendor/translators-official/Artefacts Canada.js +192 -0
  99. package/vendor/translators-official/Artforum.js +438 -0
  100. package/vendor/translators-official/Atlanta Journal-Constitution.js +311 -0
  101. package/vendor/translators-official/Atypon Journals.js +1204 -0
  102. package/vendor/translators-official/AustLII and NZLII.js +604 -0
  103. package/vendor/translators-official/Australian Dictionary of Biography.js +196 -0
  104. package/vendor/translators-official/BAILII.js +176 -0
  105. package/vendor/translators-official/BBC Genome.js +254 -0
  106. package/vendor/translators-official/BBC.js +430 -0
  107. package/vendor/translators-official/BIBSYS.js +112 -0
  108. package/vendor/translators-official/BOCC.js +217 -0
  109. package/vendor/translators-official/BOE.js +229 -0
  110. package/vendor/translators-official/BOFiP-Impots.js +132 -0
  111. package/vendor/translators-official/Baidu Scholar.js +276 -0
  112. package/vendor/translators-official/Bangkok Post.js +216 -0
  113. package/vendor/translators-official/Baruch Foundation.js +175 -0
  114. package/vendor/translators-official/Beobachter.js +175 -0
  115. package/vendor/translators-official/Bezneng Gajit.js +92 -0
  116. package/vendor/translators-official/BibLaTeX.js +888 -0
  117. package/vendor/translators-official/BibTeX.js +4236 -0
  118. package/vendor/translators-official/Biblio.com.js +216 -0
  119. package/vendor/translators-official/Bibliontology RDF.js +1166 -0
  120. package/vendor/translators-official/Biblioteca Nacional de Maestros.js +213 -0
  121. package/vendor/translators-official/Bibliotheque et Archives Nationale du Quebec (Pistard).js +147 -0
  122. package/vendor/translators-official/Bibliotheque et Archives Nationales du Quebec.js +266 -0
  123. package/vendor/translators-official/Bibliotheque nationale de France.js +872 -0
  124. package/vendor/translators-official/BioMed Central.js +585 -0
  125. package/vendor/translators-official/BioOne.js +233 -0
  126. package/vendor/translators-official/Bioconductor.js +343 -0
  127. package/vendor/translators-official/Blaetter.js +192 -0
  128. package/vendor/translators-official/Blogger.js +244 -0
  129. package/vendor/translators-official/Bloomberg.js +205 -0
  130. package/vendor/translators-official/Bloomsbury Food Library.js +296 -0
  131. package/vendor/translators-official/Bluesky.js +195 -0
  132. package/vendor/translators-official/BnF ISBN.js +129 -0
  133. package/vendor/translators-official/Bookmarks.js +282 -0
  134. package/vendor/translators-official/Bookshop.org.js +223 -0
  135. package/vendor/translators-official/Boston Review.js +332 -0
  136. package/vendor/translators-official/Bosworth Toller's Anglo-Saxon Dictionary Online.js +496 -0
  137. package/vendor/translators-official/Bracero History Archive.js +200 -0
  138. package/vendor/translators-official/Brill.js +434 -0
  139. package/vendor/translators-official/Brukerhandboken.js +153 -0
  140. package/vendor/translators-official/Bryn Mawr Classical Review.js +303 -0
  141. package/vendor/translators-official/Bundesgesetzblatt.js +231 -0
  142. package/vendor/translators-official/Business Standard.js +151 -0
  143. package/vendor/translators-official/CABI - CAB Abstracts.js +317 -0
  144. package/vendor/translators-official/CAOD.js +260 -0
  145. package/vendor/translators-official/CBC.js +439 -0
  146. package/vendor/translators-official/CCfr (BnF).js +162 -0
  147. package/vendor/translators-official/CERN Document Server.js +277 -0
  148. package/vendor/translators-official/CEUR Workshop Proceedings.js +145 -0
  149. package/vendor/translators-official/CFF References.js +170 -0
  150. package/vendor/translators-official/CFF.js +113 -0
  151. package/vendor/translators-official/CIA World Factbook.js +128 -0
  152. package/vendor/translators-official/CLACSO.js +226 -0
  153. package/vendor/translators-official/CLASE.js +135 -0
  154. package/vendor/translators-official/CNKI.js +731 -0
  155. package/vendor/translators-official/COBISS.js +1945 -0
  156. package/vendor/translators-official/COinS.js +336 -0
  157. package/vendor/translators-official/CQ Press.js +418 -0
  158. package/vendor/translators-official/CROSBI.js +227 -0
  159. package/vendor/translators-official/CSIRO Publishing.js +139 -0
  160. package/vendor/translators-official/CSL JSON.js +203 -0
  161. package/vendor/translators-official/CSV.js +294 -0
  162. package/vendor/translators-official/Cairn.info.js +368 -0
  163. package/vendor/translators-official/CalMatters.js +228 -0
  164. package/vendor/translators-official/Calisphere.js +355 -0
  165. package/vendor/translators-official/Camara Brasileira do Livro ISBN.js +842 -0
  166. package/vendor/translators-official/Cambridge Core.js +580 -0
  167. package/vendor/translators-official/Cambridge Engage Preprints.js +276 -0
  168. package/vendor/translators-official/CanLII.js +284 -0
  169. package/vendor/translators-official/Canada.com.js +171 -0
  170. package/vendor/translators-official/Canadian Letters and Images.js +148 -0
  171. package/vendor/translators-official/Canadiana.ca.js +196 -0
  172. package/vendor/translators-official/Cascadilla Proceedings Project.js +390 -0
  173. package/vendor/translators-official/Cell Press.js +516 -0
  174. package/vendor/translators-official/Central and Eastern European Online Library Journals.js +313 -0
  175. package/vendor/translators-official/Champlain Society - Collection.js +220 -0
  176. package/vendor/translators-official/ChatGPT.js +185 -0
  177. package/vendor/translators-official/Chicago Journal of Theoretical Computer Science.js +203 -0
  178. package/vendor/translators-official/Christian Science Monitor.js +148 -0
  179. package/vendor/translators-official/Chronicling America.js +131 -0
  180. package/vendor/translators-official/CiNii Research.js +168 -0
  181. package/vendor/translators-official/Citavi 5 XML.js +516 -0
  182. package/vendor/translators-official/CiteSeer.js +196 -0
  183. package/vendor/translators-official/Citizen Lab.js +347 -0
  184. package/vendor/translators-official/Civilization.ca.js +146 -0
  185. package/vendor/translators-official/Climate Change and Human Health Literature Portal.js +283 -0
  186. package/vendor/translators-official/Clinical Key.js +367 -0
  187. package/vendor/translators-official/Code4Lib Journal.js +143 -0
  188. package/vendor/translators-official/Colorado State Legislature.js +215 -0
  189. package/vendor/translators-official/Columbia University Press.js +238 -0
  190. package/vendor/translators-official/Common-Place.js +218 -0
  191. package/vendor/translators-official/Computer History Museum Archive.js +1319 -0
  192. package/vendor/translators-official/Copernicus.js +461 -0
  193. package/vendor/translators-official/Cornell LII.js +252 -0
  194. package/vendor/translators-official/Cornell University Press.js +235 -0
  195. package/vendor/translators-official/CourtListener.js +265 -0
  196. package/vendor/translators-official/Crossref REST.js +1757 -0
  197. package/vendor/translators-official/Crossref Unixref XML.js +965 -0
  198. package/vendor/translators-official/Current Affairs.js +160 -0
  199. package/vendor/translators-official/DABI.js +476 -0
  200. package/vendor/translators-official/DAI-Zenon.js +392 -0
  201. package/vendor/translators-official/DART-Europe.js +176 -0
  202. package/vendor/translators-official/DBLP Computer Science Bibliography.js +462 -0
  203. package/vendor/translators-official/DBpia.js +233 -0
  204. package/vendor/translators-official/DEPATISnet.js +373 -0
  205. package/vendor/translators-official/DOAJ.js +243 -0
  206. package/vendor/translators-official/DOI Content Negotiation.js +352 -0
  207. package/vendor/translators-official/DOI.js +322 -0
  208. package/vendor/translators-official/DPLA.js +425 -0
  209. package/vendor/translators-official/DSpace Intermediate Metadata.js +208 -0
  210. package/vendor/translators-official/Dagens Nyheter.js +369 -0
  211. package/vendor/translators-official/Dagstuhl Research Online Publication Server.js +276 -0
  212. package/vendor/translators-official/Dar Almandumah.js +253 -0
  213. package/vendor/translators-official/Data.gov.js +225 -0
  214. package/vendor/translators-official/Databrary.js +184 -0
  215. package/vendor/translators-official/Datacite JSON.js +1182 -0
  216. package/vendor/translators-official/Dataverse.js +418 -0
  217. package/vendor/translators-official/Daum News.js +139 -0
  218. package/vendor/translators-official/De Gruyter Brill.js +460 -0
  219. package/vendor/translators-official/Defense Technical Information Center.js +146 -0
  220. package/vendor/translators-official/Delpher.js +413 -0
  221. package/vendor/translators-official/Demographic Research.js +164 -0
  222. package/vendor/translators-official/Denik CZ.js +273 -0
  223. package/vendor/translators-official/Der Freitag.js +236 -0
  224. package/vendor/translators-official/Der Spiegel.js +479 -0
  225. package/vendor/translators-official/Desiring God.js +216 -0
  226. package/vendor/translators-official/Deutsche Fotothek.js +364 -0
  227. package/vendor/translators-official/Deutsche Nationalbibliothek.js +594 -0
  228. package/vendor/translators-official/Dialnet.js +258 -0
  229. package/vendor/translators-official/Die Zeit.js +378 -0
  230. package/vendor/translators-official/DigiZeitschriften.js +269 -0
  231. package/vendor/translators-official/Digital Humanities Quarterly.js +207 -0
  232. package/vendor/translators-official/Digital Spy.js +215 -0
  233. package/vendor/translators-official/Dimensions.js +233 -0
  234. package/vendor/translators-official/Douban.js +316 -0
  235. package/vendor/translators-official/Dreier Neuerscheinungsdienst.js +209 -0
  236. package/vendor/translators-official/DrugBank.ca.js +209 -0
  237. package/vendor/translators-official/Dryad Digital Repository.js +323 -0
  238. package/vendor/translators-official/Duke University Press Books.js +250 -0
  239. package/vendor/translators-official/E-periodica Switzerland.js +347 -0
  240. package/vendor/translators-official/EBSCO Discovery Layer.js +159 -0
  241. package/vendor/translators-official/EBSCOhost.js +592 -0
  242. package/vendor/translators-official/EIDR.js +184 -0
  243. package/vendor/translators-official/EPA National Library Catalog.js +245 -0
  244. package/vendor/translators-official/ERIC.js +986 -0
  245. package/vendor/translators-official/ESpacenet.js +465 -0
  246. package/vendor/translators-official/EUR-Lex.js +418 -0
  247. package/vendor/translators-official/Eastview.js +336 -0
  248. package/vendor/translators-official/Edinburgh University Press Journals.js +178 -0
  249. package/vendor/translators-official/Education Week.js +197 -0
  250. package/vendor/translators-official/El Comercio (Peru).js +347 -0
  251. package/vendor/translators-official/El Pais.js +262 -0
  252. package/vendor/translators-official/Electronic Colloquium on Computational Complexity.js +230 -0
  253. package/vendor/translators-official/Elicit.js +120 -0
  254. package/vendor/translators-official/Elsevier Health Journals.js +531 -0
  255. package/vendor/translators-official/Elsevier Pure.js +420 -0
  256. package/vendor/translators-official/Embedded Metadata.js +1978 -0
  257. package/vendor/translators-official/Emerald Insight.js +626 -0
  258. package/vendor/translators-official/Encyclopedia of Chicago.js +162 -0
  259. package/vendor/translators-official/Encyclopedia of Korean Culture.js +221 -0
  260. package/vendor/translators-official/Endnote XML.js +1407 -0
  261. package/vendor/translators-official/Engineering Village.js +139 -0
  262. package/vendor/translators-official/Envidat.js +331 -0
  263. package/vendor/translators-official/Epicurious.js +144 -0
  264. package/vendor/translators-official/Erudit.js +272 -0
  265. package/vendor/translators-official/Euclid.js +220 -0
  266. package/vendor/translators-official/EurasiaNet.js +109 -0
  267. package/vendor/translators-official/EurogamerUSgamer.js +380 -0
  268. package/vendor/translators-official/Europe PMC.js +448 -0
  269. package/vendor/translators-official/Evernote.js +104 -0
  270. package/vendor/translators-official/F1000 Research.js +655 -0
  271. package/vendor/translators-official/FAO Knowledge Repository.js +598 -0
  272. package/vendor/translators-official/FAOLEX Database.js +811 -0
  273. package/vendor/translators-official/FAZ.NET.js +231 -0
  274. package/vendor/translators-official/Fachportal Padagogik.js +839 -0
  275. package/vendor/translators-official/Factiva.js +302 -0
  276. package/vendor/translators-official/Failed Architecture.js +114 -0
  277. package/vendor/translators-official/Fairfax Australia.js +200 -0
  278. package/vendor/translators-official/Fatcat.js +425 -0
  279. package/vendor/translators-official/Figshare.js +191 -0
  280. package/vendor/translators-official/Financial Times.js +187 -0
  281. package/vendor/translators-official/Finna.js +357 -0
  282. package/vendor/translators-official/Flickr.js +318 -0
  283. package/vendor/translators-official/Foreign Affairs.js +485 -0
  284. package/vendor/translators-official/Foreign Policy.js +146 -0
  285. package/vendor/translators-official/FreeCite.js +95 -0
  286. package/vendor/translators-official/FreePatentsOnline.js +265 -0
  287. package/vendor/translators-official/Frieze.js +243 -0
  288. package/vendor/translators-official/Frontiers.js +667 -0
  289. package/vendor/translators-official/GMS German Medical Science.js +319 -0
  290. package/vendor/translators-official/GPO Access e-CFR.js +134 -0
  291. package/vendor/translators-official/Gale Databases.js +324 -0
  292. package/vendor/translators-official/GaleGDC.js +189 -0
  293. package/vendor/translators-official/Galegroup.js +327 -0
  294. package/vendor/translators-official/Gallica.js +194 -0
  295. package/vendor/translators-official/Game Studies.js +155 -0
  296. package/vendor/translators-official/GameSpot.js +199 -0
  297. package/vendor/translators-official/GameStar GamePro.js +198 -0
  298. package/vendor/translators-official/Gasyrlar Awazy.js +156 -0
  299. package/vendor/translators-official/Gemeinsamer Bibliotheksverbund ISBN.js +23 -0
  300. package/vendor/translators-official/Gene Ontology.js +128 -0
  301. package/vendor/translators-official/GitHub.js +501 -0
  302. package/vendor/translators-official/Globes.js +243 -0
  303. package/vendor/translators-official/Gmail.js +64 -0
  304. package/vendor/translators-official/Goodreads.js +219 -0
  305. package/vendor/translators-official/Google Books.js +552 -0
  306. package/vendor/translators-official/Google Gemini.js +116 -0
  307. package/vendor/translators-official/Google Patents.js +959 -0
  308. package/vendor/translators-official/Google Play.js +186 -0
  309. package/vendor/translators-official/Google Presentation.js +104 -0
  310. package/vendor/translators-official/Google Research.js +221 -0
  311. package/vendor/translators-official/Google Scholar.js +1361 -0
  312. package/vendor/translators-official/Gulag Many Days, Many Lives.js +179 -0
  313. package/vendor/translators-official/HAL.js +536 -0
  314. package/vendor/translators-official/HCSP.js +312 -0
  315. package/vendor/translators-official/HUDOC.js +650 -0
  316. package/vendor/translators-official/Haaretz.js +562 -0
  317. package/vendor/translators-official/Habr.js +232 -0
  318. package/vendor/translators-official/Handelszeitung.js +273 -0
  319. package/vendor/translators-official/Hanrei Watch.js +123 -0
  320. package/vendor/translators-official/Harper's Magazine.js +365 -0
  321. package/vendor/translators-official/Harvard Business Review.js +326 -0
  322. package/vendor/translators-official/Harvard Caselaw Access Project.js +244 -0
  323. package/vendor/translators-official/Harvard University Press Books.js +286 -0
  324. package/vendor/translators-official/HathiTrust.js +311 -0
  325. package/vendor/translators-official/HeinOnline.js +280 -0
  326. package/vendor/translators-official/Heise.js +200 -0
  327. package/vendor/translators-official/Herder.js +236 -0
  328. package/vendor/translators-official/HighBeam.js +198 -0
  329. package/vendor/translators-official/HighWire 2.0.js +1355 -0
  330. package/vendor/translators-official/HighWire.js +209 -0
  331. package/vendor/translators-official/Hindawi Publishers.js +177 -0
  332. package/vendor/translators-official/Hispanic-American Periodical Index.js +236 -0
  333. package/vendor/translators-official/Homeland Security Digital Library.js +426 -0
  334. package/vendor/translators-official/Hrvatska enciklopedija.js +118 -0
  335. package/vendor/translators-official/Huff Post.js +223 -0
  336. package/vendor/translators-official/Human Rights Watch.js +450 -0
  337. package/vendor/translators-official/IBISWorld.js +171 -0
  338. package/vendor/translators-official/IDEA ALM.js +536 -0
  339. package/vendor/translators-official/IEEE Computer Society.js +429 -0
  340. package/vendor/translators-official/IEEE Xplore.js +1000 -0
  341. package/vendor/translators-official/IETF.js +451 -0
  342. package/vendor/translators-official/IGN.js +136 -0
  343. package/vendor/translators-official/IMDb.js +566 -0
  344. package/vendor/translators-official/INSPIRE.js +247 -0
  345. package/vendor/translators-official/IPCC.js +757 -0
  346. package/vendor/translators-official/ISTC.js +181 -0
  347. package/vendor/translators-official/Idref.js +179 -0
  348. package/vendor/translators-official/In These Times.js +196 -0
  349. package/vendor/translators-official/Ined.js +558 -0
  350. package/vendor/translators-official/InfoTrac.js +279 -0
  351. package/vendor/translators-official/Informationssystem Medienpaedagogik.js +305 -0
  352. package/vendor/translators-official/IngentaConnect.js +312 -0
  353. package/vendor/translators-official/Inside Higher Ed.js +167 -0
  354. package/vendor/translators-official/Insignia OPAC.js +114 -0
  355. package/vendor/translators-official/Institute of Contemporary Art.js +180 -0
  356. package/vendor/translators-official/Institute of Physics.js +340 -0
  357. package/vendor/translators-official/Integrum.js +173 -0
  358. package/vendor/translators-official/Intellixir.js +264 -0
  359. package/vendor/translators-official/Inter-Research Science Center.js +185 -0
  360. package/vendor/translators-official/International Nuclear Information System.js +285 -0
  361. package/vendor/translators-official/Internet Archive Scholar.js +106 -0
  362. package/vendor/translators-official/Internet Archive Wayback Machine.js +152 -0
  363. package/vendor/translators-official/Internet Archive.js +552 -0
  364. package/vendor/translators-official/InvenioRDM.js +1250 -0
  365. package/vendor/translators-official/Isidore.js +235 -0
  366. package/vendor/translators-official/J-Stage.js +272 -0
  367. package/vendor/translators-official/JETS.js +122 -0
  368. package/vendor/translators-official/JISC Historical Texts.js +273 -0
  369. package/vendor/translators-official/JRC Publications Repository.js +569 -0
  370. package/vendor/translators-official/JSTOR.js +821 -0
  371. package/vendor/translators-official/Jahrbuch.js +213 -0
  372. package/vendor/translators-official/Japan Times Online.js +138 -0
  373. package/vendor/translators-official/Journal of Electronic Publishing.js +147 -0
  374. package/vendor/translators-official/Journal of Extension.js +288 -0
  375. package/vendor/translators-official/Journal of Machine Learning Research.js +441 -0
  376. package/vendor/translators-official/Journal of Religion and Society.js +174 -0
  377. package/vendor/translators-official/JurPC.js +274 -0
  378. package/vendor/translators-official/Juricaf.js +141 -0
  379. package/vendor/translators-official/Juris.js +728 -0
  380. package/vendor/translators-official/K10plus ISBN.js +386 -0
  381. package/vendor/translators-official/KStudy.js +632 -0
  382. package/vendor/translators-official/Kanopy.js +358 -0
  383. package/vendor/translators-official/Khaama Press.js +249 -0
  384. package/vendor/translators-official/KiM.js +259 -0
  385. package/vendor/translators-official/KitapYurdu.com.js +376 -0
  386. package/vendor/translators-official/Kommersant.js +174 -0
  387. package/vendor/translators-official/Korean National Library.js +621 -0
  388. package/vendor/translators-official/L'Annee Philologique.js +267 -0
  389. package/vendor/translators-official/LA Times.js +239 -0
  390. package/vendor/translators-official/LIBRIS ISBN.js +103 -0
  391. package/vendor/translators-official/LIVIVO.js +260 -0
  392. package/vendor/translators-official/LWN.net.js +350 -0
  393. package/vendor/translators-official/La Croix.js +237 -0
  394. package/vendor/translators-official/La Nacion (Argentina).js +231 -0
  395. package/vendor/translators-official/La Presse.js +218 -0
  396. package/vendor/translators-official/La Republica (Peru).js +174 -0
  397. package/vendor/translators-official/Lagen.nu.js +214 -0
  398. package/vendor/translators-official/Landesbibliographie Baden-Wurttemberg.js +324 -0
  399. package/vendor/translators-official/Lapham's Quarterly.js +1062 -0
  400. package/vendor/translators-official/Le Devoir.js +167 -0
  401. package/vendor/translators-official/Le Figaro.js +225 -0
  402. package/vendor/translators-official/Le Maitron.js +484 -0
  403. package/vendor/translators-official/Le Monde.js +245 -0
  404. package/vendor/translators-official/Le monde diplomatique.js +200 -0
  405. package/vendor/translators-official/Legifrance.js +641 -0
  406. package/vendor/translators-official/Legislative Insight.js +406 -0
  407. package/vendor/translators-official/Lexis+.js +264 -0
  408. package/vendor/translators-official/LexisNexis.js +505 -0
  409. package/vendor/translators-official/Libraries Tasmania.js +778 -0
  410. package/vendor/translators-official/Library Catalog (Aleph).js +385 -0
  411. package/vendor/translators-official/Library Catalog (Amicus).js +254 -0
  412. package/vendor/translators-official/Library Catalog (Aquabrowser).js +253 -0
  413. package/vendor/translators-official/Library Catalog (BiblioCommons).js +249 -0
  414. package/vendor/translators-official/Library Catalog (Blacklight).js +341 -0
  415. package/vendor/translators-official/Library Catalog (Capita Prism).js +162 -0
  416. package/vendor/translators-official/Library Catalog (DRA).js +203 -0
  417. package/vendor/translators-official/Library Catalog (Dynix).js +315 -0
  418. package/vendor/translators-official/Library Catalog (EBSCO Locate).js +442 -0
  419. package/vendor/translators-official/Library Catalog (Encore).js +211 -0
  420. package/vendor/translators-official/Library Catalog (InnoPAC).js +305 -0
  421. package/vendor/translators-official/Library Catalog (Koha).js +225 -0
  422. package/vendor/translators-official/Library Catalog (Mango).js +285 -0
  423. package/vendor/translators-official/Library Catalog (OPALS).js +200 -0
  424. package/vendor/translators-official/Library Catalog (PICA).js +2120 -0
  425. package/vendor/translators-official/Library Catalog (PICA2).js +331 -0
  426. package/vendor/translators-official/Library Catalog (Pika).js +488 -0
  427. package/vendor/translators-official/Library Catalog (Polaris).js +183 -0
  428. package/vendor/translators-official/Library Catalog (Quolto).js +320 -0
  429. package/vendor/translators-official/Library Catalog (RERO ILS).js +754 -0
  430. package/vendor/translators-official/Library Catalog (SIRSI eLibrary).js +407 -0
  431. package/vendor/translators-official/Library Catalog (SIRSI).js +452 -0
  432. package/vendor/translators-official/Library Catalog (SLIMS).js +146 -0
  433. package/vendor/translators-official/Library Catalog (TIND ILS).js +773 -0
  434. package/vendor/translators-official/Library Catalog (TLCYouSeeMore).js +132 -0
  435. package/vendor/translators-official/Library Catalog (TinREAD).js +277 -0
  436. package/vendor/translators-official/Library Catalog (VTLS).js +125 -0
  437. package/vendor/translators-official/Library Catalog (Visual Library 2021).js +765 -0
  438. package/vendor/translators-official/Library Catalog (Voyager 7).js +199 -0
  439. package/vendor/translators-official/Library Catalog (Voyager).js +212 -0
  440. package/vendor/translators-official/Library Genesis.js +337 -0
  441. package/vendor/translators-official/Library Hub Discover.js +359 -0
  442. package/vendor/translators-official/Library of Congress Digital Collections.js +869 -0
  443. package/vendor/translators-official/Library of Congress ISBN.js +86 -0
  444. package/vendor/translators-official/LingBuzz.js +359 -0
  445. package/vendor/translators-official/Lippincott Williams and Wilkins.js +341 -0
  446. package/vendor/translators-official/Literary Hub.js +950 -0
  447. package/vendor/translators-official/Litres.js +236 -0
  448. package/vendor/translators-official/LiveJournal.js +219 -0
  449. package/vendor/translators-official/London Review of Books.js +291 -0
  450. package/vendor/translators-official/LookUs.js +260 -0
  451. package/vendor/translators-official/Lulu.js +264 -0
  452. package/vendor/translators-official/MAB2.js +373 -0
  453. package/vendor/translators-official/MARC.js +1080 -0
  454. package/vendor/translators-official/MARCXML.js +489 -0
  455. package/vendor/translators-official/MCV.js +258 -0
  456. package/vendor/translators-official/MDPI Journals.js +356 -0
  457. package/vendor/translators-official/MEDLINEnbib.js +1561 -0
  458. package/vendor/translators-official/METS.js +384 -0
  459. package/vendor/translators-official/MIDAS Journals.js +343 -0
  460. package/vendor/translators-official/MIT Press Books.js +637 -0
  461. package/vendor/translators-official/MODS.js +2545 -0
  462. package/vendor/translators-official/MPG PuRe.js +541 -0
  463. package/vendor/translators-official/Mailman.js +136 -0
  464. package/vendor/translators-official/Mainichi Daily News.js +189 -0
  465. package/vendor/translators-official/Mastodon.js +198 -0
  466. package/vendor/translators-official/Matbugat.ru.js +110 -0
  467. package/vendor/translators-official/Max Planck Institute for the History of Science Virtual Laboratory Library.js +136 -0
  468. package/vendor/translators-official/Medium.js +208 -0
  469. package/vendor/translators-official/MetaLib.js +126 -0
  470. package/vendor/translators-official/Microbiology Society Journals.js +343 -0
  471. package/vendor/translators-official/Microsoft Academic.js +424 -0
  472. package/vendor/translators-official/Mikromarc.js +207 -0
  473. package/vendor/translators-official/Milli Kutuphane.js +244 -0
  474. package/vendor/translators-official/Musee du Louvre.js +265 -0
  475. package/vendor/translators-official/NASA ADS.js +889 -0
  476. package/vendor/translators-official/NASA NTRS.js +610 -0
  477. package/vendor/translators-official/NCBI Nucleotide.js +227 -0
  478. package/vendor/translators-official/NPR.js +341 -0
  479. package/vendor/translators-official/NRC Research Press.js +181 -0
  480. package/vendor/translators-official/NRC.nl.js +226 -0
  481. package/vendor/translators-official/NTSB Accident Reports.js +226 -0
  482. package/vendor/translators-official/NYPL Menus.js +275 -0
  483. package/vendor/translators-official/NYPL Research Catalog.js +458 -0
  484. package/vendor/translators-official/NYTimes.com.js +947 -0
  485. package/vendor/translators-official/NZZ.ch.js +272 -0
  486. package/vendor/translators-official/Nagoya University OPAC.js +336 -0
  487. package/vendor/translators-official/National Academies Press.js +243 -0
  488. package/vendor/translators-official/National Agriculture Library.js +524 -0
  489. package/vendor/translators-official/National Archives UK Case Law.js +509 -0
  490. package/vendor/translators-official/National Archives of Australia.js +522 -0
  491. package/vendor/translators-official/National Archives of South Africa.js +122 -0
  492. package/vendor/translators-official/National Bureau of Economic Research.js +363 -0
  493. package/vendor/translators-official/National Diet Library Catalogue.js +333 -0
  494. package/vendor/translators-official/National Gallery of Art - USA.js +149 -0
  495. package/vendor/translators-official/National Gallery of Australia.js +252 -0
  496. package/vendor/translators-official/National Library of Australia (new catalog).js +223 -0
  497. package/vendor/translators-official/National Library of Belarus.js +171 -0
  498. package/vendor/translators-official/National Library of Norway.js +310 -0
  499. package/vendor/translators-official/National Library of Poland ISBN.js +140 -0
  500. package/vendor/translators-official/National Post.js +186 -0
  501. package/vendor/translators-official/National Technical Reports Library.js +276 -0
  502. package/vendor/translators-official/National Transportation Library ROSA P.js +562 -0
  503. package/vendor/translators-official/Nature Publishing Group.js +3394 -0
  504. package/vendor/translators-official/Neural Information Processing Systems.js +385 -0
  505. package/vendor/translators-official/New Left Review.js +245 -0
  506. package/vendor/translators-official/New Zealand Herald.js +282 -0
  507. package/vendor/translators-official/Newlines Magazine.js +190 -0
  508. package/vendor/translators-official/News Corp Australia.js +251 -0
  509. package/vendor/translators-official/NewsBank.js +202 -0
  510. package/vendor/translators-official/NewsnetTamedia.js +509 -0
  511. package/vendor/translators-official/Noor Digital Library.js +267 -0
  512. package/vendor/translators-official/Note HTML.js +206 -0
  513. package/vendor/translators-official/Note Markdown.js +1638 -0
  514. package/vendor/translators-official/Notre Dame Philosophical Reviews.js +201 -0
  515. package/vendor/translators-official/OAPEN.js +331 -0
  516. package/vendor/translators-official/OCLC WorldCat FirstSearch.js +213 -0
  517. package/vendor/translators-official/OECD.js +216 -0
  518. package/vendor/translators-official/ORCID.js +104 -0
  519. package/vendor/translators-official/OSF Preprints.js +1287 -0
  520. package/vendor/translators-official/OSTI Energy Citations.js +202 -0
  521. package/vendor/translators-official/OVID Tagged.js +1226 -0
  522. package/vendor/translators-official/OZON.ru.js +465 -0
  523. package/vendor/translators-official/OhioLINK.js +43 -0
  524. package/vendor/translators-official/Old Bailey Online.js +315 -0
  525. package/vendor/translators-official/Open Conf.js +172 -0
  526. package/vendor/translators-official/Open Knowledge Repository.js +811 -0
  527. package/vendor/translators-official/Open WorldCat.js +1394 -0
  528. package/vendor/translators-official/OpenAlex JSON.js +740 -0
  529. package/vendor/translators-official/OpenAlex.js +444 -0
  530. package/vendor/translators-official/OpenEdition Books.js +155 -0
  531. package/vendor/translators-official/OpenEdition Journals.js +432 -0
  532. package/vendor/translators-official/Optical Society of America.js +562 -0
  533. package/vendor/translators-official/Optimization Online.js +418 -0
  534. package/vendor/translators-official/Ovid OCE.js +353 -0
  535. package/vendor/translators-official/Ovid.js +302 -0
  536. package/vendor/translators-official/Oxford Dictionaries Premium.js +194 -0
  537. package/vendor/translators-official/Oxford English Dictionary.js +122 -0
  538. package/vendor/translators-official/Oxford Music and Art Online.js +120 -0
  539. package/vendor/translators-official/Oxford Reference.js +202 -0
  540. package/vendor/translators-official/Oxford University Press.js +277 -0
  541. package/vendor/translators-official/PC Gamer.js +302 -0
  542. package/vendor/translators-official/PC Games.js +128 -0
  543. package/vendor/translators-official/PEI Archival Information Network.js +242 -0
  544. package/vendor/translators-official/PEP Web.js +165 -0
  545. package/vendor/translators-official/PKP Catalog Systems.js +1455 -0
  546. package/vendor/translators-official/PLoS Journals.js +284 -0
  547. package/vendor/translators-official/PRC History Review.js +298 -0
  548. package/vendor/translators-official/Pajhwok Afghan News.js +198 -0
  549. package/vendor/translators-official/Papers Past.js +509 -0
  550. package/vendor/translators-official/Paris Review.js +227 -0
  551. package/vendor/translators-official/Pastebin.js +199 -0
  552. package/vendor/translators-official/Patents - USPTO.js +210 -0
  553. package/vendor/translators-official/Peeters.js +587 -0
  554. package/vendor/translators-official/Perceiving Systems.js +350 -0
  555. package/vendor/translators-official/Perlego.js +416 -0
  556. package/vendor/translators-official/PhilPapers.js +233 -0
  557. package/vendor/translators-official/Philosopher's Imprint.js +169 -0
  558. package/vendor/translators-official/Pleade.js +329 -0
  559. package/vendor/translators-official/Polygon.js +270 -0
  560. package/vendor/translators-official/Potsdamer Neueste Nachrichten.js +188 -0
  561. package/vendor/translators-official/Premium Times.js +144 -0
  562. package/vendor/translators-official/Preprints.org.js +305 -0
  563. package/vendor/translators-official/Prime 9ja Online.js +402 -0
  564. package/vendor/translators-official/Primo 2018.js +126 -0
  565. package/vendor/translators-official/Primo Normalized XML.js +1075 -0
  566. package/vendor/translators-official/Primo.js +322 -0
  567. package/vendor/translators-official/ProMED.js +66 -0
  568. package/vendor/translators-official/ProQuest Ebook Central.js +226 -0
  569. package/vendor/translators-official/ProQuest PolicyFile.js +115 -0
  570. package/vendor/translators-official/ProQuest.js +2210 -0
  571. package/vendor/translators-official/Probing the Past.js +151 -0
  572. package/vendor/translators-official/Project Gutenberg.js +191 -0
  573. package/vendor/translators-official/Project MUSE.js +451 -0
  574. package/vendor/translators-official/Protein Data Bank.js +445 -0
  575. package/vendor/translators-official/PubFactory Journals.js +712 -0
  576. package/vendor/translators-official/PubMed Central.js +563 -0
  577. package/vendor/translators-official/PubMed XML.js +1240 -0
  578. package/vendor/translators-official/PubMed.js +1317 -0
  579. package/vendor/translators-official/PubPub.js +530 -0
  580. package/vendor/translators-official/Public Record Office Victoria.js +252 -0
  581. package/vendor/translators-official/Publications Office of the European Union.js +540 -0
  582. package/vendor/translators-official/Publications du Quebec.js +225 -0
  583. package/vendor/translators-official/PyPI.js +288 -0
  584. package/vendor/translators-official/Qatar Digital Library.js +802 -0
  585. package/vendor/translators-official/Queensland State Archives.js +303 -0
  586. package/vendor/translators-official/R-Packages.js +410 -0
  587. package/vendor/translators-official/RAND.js +283 -0
  588. package/vendor/translators-official/RDF.js +1782 -0
  589. package/vendor/translators-official/REDALYC.js +196 -0
  590. package/vendor/translators-official/RIS.js +7292 -0
  591. package/vendor/translators-official/RSC Publishing.js +250 -0
  592. package/vendor/translators-official/Radio Free Europe Radio Liberty.js +706 -0
  593. package/vendor/translators-official/RePEc - Econpapers.js +725 -0
  594. package/vendor/translators-official/RePEc - IDEAS.js +337 -0
  595. package/vendor/translators-official/Rechtspraak.nl.js +295 -0
  596. package/vendor/translators-official/RefWorks Tagged.js +1185 -0
  597. package/vendor/translators-official/ReferBibIX.js +463 -0
  598. package/vendor/translators-official/Regeringskansliet.js +89 -0
  599. package/vendor/translators-official/Research Square.js +335 -0
  600. package/vendor/translators-official/ResearchGate.js +394 -0
  601. package/vendor/translators-official/Retsinformation.js +348 -0
  602. package/vendor/translators-official/Reuters.js +255 -0
  603. package/vendor/translators-official/Rock, Paper, Shotgun.js +304 -0
  604. package/vendor/translators-official/Roll Call.js +173 -0
  605. package/vendor/translators-official/Russian State Library.js +1517 -0
  606. package/vendor/translators-official/SAE Papers.js +241 -0
  607. package/vendor/translators-official/SAGE Journals.js +429 -0
  608. package/vendor/translators-official/SAGE Knowledge.js +505 -0
  609. package/vendor/translators-official/SAILDART.js +364 -0
  610. package/vendor/translators-official/SALT Research Archives.js +196 -0
  611. package/vendor/translators-official/SFU IPinCH.js +946 -0
  612. package/vendor/translators-official/SIPRI.js +256 -0
  613. package/vendor/translators-official/SIRS Knowledge Source.js +546 -0
  614. package/vendor/translators-official/SLUB Dresden.js +300 -0
  615. package/vendor/translators-official/SORA.js +340 -0
  616. package/vendor/translators-official/SSOAR.js +399 -0
  617. package/vendor/translators-official/SSRN.js +235 -0
  618. package/vendor/translators-official/SVT Nyheter.js +381 -0
  619. package/vendor/translators-official/Sacramento Bee.js +274 -0
  620. package/vendor/translators-official/Safari Books Online.js +391 -0
  621. package/vendor/translators-official/Scholars Portal Journals.js +220 -0
  622. package/vendor/translators-official/Scholia.js +189 -0
  623. package/vendor/translators-official/Schweizer Radio und Fernsehen SRF.js +152 -0
  624. package/vendor/translators-official/SciELO.js +440 -0
  625. package/vendor/translators-official/ScienceDirect.js +1306 -0
  626. package/vendor/translators-official/Scopus.js +418 -0
  627. package/vendor/translators-official/Semantic Scholar.js +513 -0
  628. package/vendor/translators-official/Silverchair.js +886 -0
  629. package/vendor/translators-official/Slate.js +228 -0
  630. package/vendor/translators-official/SlideShare.js +112 -0
  631. package/vendor/translators-official/Springer Link.js +696 -0
  632. package/vendor/translators-official/Stack Exchange.js +134 -0
  633. package/vendor/translators-official/Standard Ebooks.js +198 -0
  634. package/vendor/translators-official/Stanford Encyclopedia of Philosophy.js +285 -0
  635. package/vendor/translators-official/Stanford University Press.js +343 -0
  636. package/vendor/translators-official/State Records Office of Western Australia.js +439 -0
  637. package/vendor/translators-official/Stitcher.js +141 -0
  638. package/vendor/translators-official/Store norske leksikon.js +291 -0
  639. package/vendor/translators-official/Stuff.co.nz.js +446 -0
  640. package/vendor/translators-official/Substack.js +237 -0
  641. package/vendor/translators-official/Sud Ouest.js +181 -0
  642. package/vendor/translators-official/Sueddeutsche.de.js +205 -0
  643. package/vendor/translators-official/Summon 2.js +117 -0
  644. package/vendor/translators-official/Superlib.js +647 -0
  645. package/vendor/translators-official/Svenska Dagbladet.js +274 -0
  646. package/vendor/translators-official/Sveriges radio.js +416 -0
  647. package/vendor/translators-official/TEI.js +648 -0
  648. package/vendor/translators-official/TV by the Numbers.js +194 -0
  649. package/vendor/translators-official/TVNZ.js +165 -0
  650. package/vendor/translators-official/Tagesspiegel.js +249 -0
  651. package/vendor/translators-official/Talis Aspire.js +316 -0
  652. package/vendor/translators-official/TalisPrism.js +445 -0
  653. package/vendor/translators-official/Tatknigafund.js +174 -0
  654. package/vendor/translators-official/Tatpressa.ru.js +121 -0
  655. package/vendor/translators-official/Taylor & Francis eBooks.js +334 -0
  656. package/vendor/translators-official/Taylor and Francis+NEJM.js +813 -0
  657. package/vendor/translators-official/Tesis Doctorals en Xarxa.js +233 -0
  658. package/vendor/translators-official/The Art Newspaper.js +268 -0
  659. package/vendor/translators-official/The Atlantic.js +535 -0
  660. package/vendor/translators-official/The Boston Globe.js +285 -0
  661. package/vendor/translators-official/The Chronicle of Higher Education.js +308 -0
  662. package/vendor/translators-official/The Daily Beast.js +183 -0
  663. package/vendor/translators-official/The Economic Times - The Times of India.js +305 -0
  664. package/vendor/translators-official/The Economist.js +207 -0
  665. package/vendor/translators-official/The Free Dictionary.js +58 -0
  666. package/vendor/translators-official/The Globe and Mail.js +241 -0
  667. package/vendor/translators-official/The Guardian.js +717 -0
  668. package/vendor/translators-official/The Hamilton Spectator.js +129 -0
  669. package/vendor/translators-official/The Hindu.js +318 -0
  670. package/vendor/translators-official/The Independent.js +184 -0
  671. package/vendor/translators-official/The Intercept.js +258 -0
  672. package/vendor/translators-official/The Met.js +276 -0
  673. package/vendor/translators-official/The Microfinance Gateway.js +174 -0
  674. package/vendor/translators-official/The Nation.js +188 -0
  675. package/vendor/translators-official/The National Archives (UK).js +242 -0
  676. package/vendor/translators-official/The New Republic.js +160 -0
  677. package/vendor/translators-official/The New York Review of Books.js +467 -0
  678. package/vendor/translators-official/The New Yorker.js +360 -0
  679. package/vendor/translators-official/The Open Library.js +220 -0
  680. package/vendor/translators-official/The Straits Times.js +528 -0
  681. package/vendor/translators-official/The Telegraph.js +337 -0
  682. package/vendor/translators-official/The Times and Sunday Times.js +180 -0
  683. package/vendor/translators-official/The Times of Israel.js +151 -0
  684. package/vendor/translators-official/TheMarker.js +108 -0
  685. package/vendor/translators-official/Theory of Computing.js +488 -0
  686. package/vendor/translators-official/ThesesFR.js +405 -0
  687. package/vendor/translators-official/Thieme.js +326 -0
  688. package/vendor/translators-official/Time.com.js +557 -0
  689. package/vendor/translators-official/TimesMachine.js +222 -0
  690. package/vendor/translators-official/Tony Blair Institute for Global Change.js +195 -0
  691. package/vendor/translators-official/Toronto Star.js +298 -0
  692. package/vendor/translators-official/Transportation Research Board.js +253 -0
  693. package/vendor/translators-official/Treesearch.js +535 -0
  694. package/vendor/translators-official/Trove.js +616 -0
  695. package/vendor/translators-official/Tumblr.js +179 -0
  696. package/vendor/translators-official/Twitter.js +413 -0
  697. package/vendor/translators-official/UChicago VuFind.js +907 -0
  698. package/vendor/translators-official/UNZ Print Archive.js +276 -0
  699. package/vendor/translators-official/UPCommons.js +227 -0
  700. package/vendor/translators-official/US National Archives Research Catalog.js +275 -0
  701. package/vendor/translators-official/USENIX.js +151 -0
  702. package/vendor/translators-official/Ubiquity Journals.js +228 -0
  703. package/vendor/translators-official/University Press Scholarship.js +301 -0
  704. package/vendor/translators-official/University of California Press Books.js +403 -0
  705. package/vendor/translators-official/University of Chicago Press Books.js +378 -0
  706. package/vendor/translators-official/University of Wisconsin-Madison Libraries Catalog.js +298 -0
  707. package/vendor/translators-official/Unqualified Dublin Core RDF.js +153 -0
  708. package/vendor/translators-official/UpToDate References.js +203 -0
  709. package/vendor/translators-official/Vanity Fair.js +242 -0
  710. package/vendor/translators-official/Verniana-Jules Verne Studies.js +190 -0
  711. package/vendor/translators-official/Verso Books.js +311 -0
  712. package/vendor/translators-official/Vice.js +445 -0
  713. package/vendor/translators-official/Victoria & Albert Museum.js +277 -0
  714. package/vendor/translators-official/Vimeo.js +188 -0
  715. package/vendor/translators-official/VoxEU.js +151 -0
  716. package/vendor/translators-official/WHO.js +458 -0
  717. package/vendor/translators-official/WIPO.js +189 -0
  718. package/vendor/translators-official/Wall Street Journal.js +809 -0
  719. package/vendor/translators-official/Wanfang Data.js +310 -0
  720. package/vendor/translators-official/Washington Monthly.js +195 -0
  721. package/vendor/translators-official/Washington Post.js +447 -0
  722. package/vendor/translators-official/Web of Science Nextgen.js +832 -0
  723. package/vendor/translators-official/Web of Science Tagged.js +1247 -0
  724. package/vendor/translators-official/Web of Science.js +228 -0
  725. package/vendor/translators-official/Welt Online.js +161 -0
  726. package/vendor/translators-official/WestLaw UK.js +190 -0
  727. package/vendor/translators-official/WikiLeaks PlusD.js +182 -0
  728. package/vendor/translators-official/Wikidata QuickStatements.js +324 -0
  729. package/vendor/translators-official/Wikidata.js +911 -0
  730. package/vendor/translators-official/Wikimedia Commons.js +285 -0
  731. package/vendor/translators-official/Wikipedia Citation Templates.js +432 -0
  732. package/vendor/translators-official/Wikipedia.js +385 -0
  733. package/vendor/translators-official/Wikisource.js +399 -0
  734. package/vendor/translators-official/Wikiwand.js +122 -0
  735. package/vendor/translators-official/Wiktionary.js +141 -0
  736. package/vendor/translators-official/Wildlife Biology in Practice.js +172 -0
  737. package/vendor/translators-official/Wiley Online Library.js +1412 -0
  738. package/vendor/translators-official/Wilson Center Digital Archive.js +321 -0
  739. package/vendor/translators-official/Winnipeg Free Press.js +204 -0
  740. package/vendor/translators-official/Wired.js +362 -0
  741. package/vendor/translators-official/Womennews.js +230 -0
  742. package/vendor/translators-official/World Digital Library.js +403 -0
  743. package/vendor/translators-official/World History Connected.js +185 -0
  744. package/vendor/translators-official/World Shakespeare Bibliography Online.js +595 -0
  745. package/vendor/translators-official/WorldCat Discovery Service.js +630 -0
  746. package/vendor/translators-official/XML ContextObject.js +274 -0
  747. package/vendor/translators-official/YPSF.js +224 -0
  748. package/vendor/translators-official/Yandex Books.js +265 -0
  749. package/vendor/translators-official/Ynet.js +171 -0
  750. package/vendor/translators-official/YouTube.js +209 -0
  751. package/vendor/translators-official/ZIPonline.js +267 -0
  752. package/vendor/translators-official/ZOBODAT.js +341 -0
  753. package/vendor/translators-official/Zotero RDF.js +588 -0
  754. package/vendor/translators-official/ZoteroBib.js +94 -0
  755. package/vendor/translators-official/arXiv.org.js +1100 -0
  756. package/vendor/translators-official/artnet.js +207 -0
  757. package/vendor/translators-official/beck-online.js +1339 -0
  758. package/vendor/translators-official/clinicaltrials.gov.js +331 -0
  759. package/vendor/translators-official/dLibra.js +367 -0
  760. package/vendor/translators-official/dejure.org.js +242 -0
  761. package/vendor/translators-official/dhistory.js +103 -0
  762. package/vendor/translators-official/digibib.net.js +245 -0
  763. package/vendor/translators-official/eLibrary.ru.js +867 -0
  764. package/vendor/translators-official/eLife.js +462 -0
  765. package/vendor/translators-official/eMJA.js +246 -0
  766. package/vendor/translators-official/eMedicine.js +123 -0
  767. package/vendor/translators-official/ePrint IACR.js +608 -0
  768. package/vendor/translators-official/ebrary.js +140 -0
  769. package/vendor/translators-official/etatar.ru.js +104 -0
  770. package/vendor/translators-official/feb-web.ru.js +167 -0
  771. package/vendor/translators-official/fishpond.co.nz.js +224 -0
  772. package/vendor/translators-official/fr-online.de.js +209 -0
  773. package/vendor/translators-official/govinfo.js +189 -0
  774. package/vendor/translators-official/informIT database.js +248 -0
  775. package/vendor/translators-official/io-port.js +68 -0
  776. package/vendor/translators-official/jurion.js +377 -0
  777. package/vendor/translators-official/mEDRA.js +597 -0
  778. package/vendor/translators-official/magazines.russ.ru.js +120 -0
  779. package/vendor/translators-official/medes.js +284 -0
  780. package/vendor/translators-official/newshub.co.nz.js +206 -0
  781. package/vendor/translators-official/newspapers.com.js +335 -0
  782. package/vendor/translators-official/openJur.js +161 -0
  783. package/vendor/translators-official/reddit.js +275 -0
  784. package/vendor/translators-official/sbn.it.js +186 -0
  785. package/vendor/translators-official/scinapse.js +273 -0
  786. package/vendor/translators-official/semantics Visual Library.js +643 -0
  787. package/vendor/translators-official/taz.de.js +200 -0
  788. package/vendor/translators-official/unAPI.js +481 -0
  789. package/vendor/translators-official/wiso.js +314 -0
  790. package/vendor/translators-official/zbMATH.js +278 -0
  791. package/vendor/translators-official/zotero.org.js +311 -0
  792. package/vendor/translators_CN/Angle.js +468 -0
  793. package/vendor/translators_CN/Baidu Baike.js +152 -0
  794. package/vendor/translators_CN/Baidu Scholar.js +639 -0
  795. package/vendor/translators_CN/Belt and Road Database.js +496 -0
  796. package/vendor/translators_CN/BiliBili.js +776 -0
  797. package/vendor/translators_CN/CCPINFO.js +416 -0
  798. package/vendor/translators_CN/CHINESE JOURNAL OF LAW.js +169 -0
  799. package/vendor/translators_CN/CNBKSY.js +385 -0
  800. package/vendor/translators_CN/CNKI CHKD.js +692 -0
  801. package/vendor/translators_CN/CNKI Gongjushu.js +271 -0
  802. package/vendor/translators_CN/CNKI Industry.js +726 -0
  803. package/vendor/translators_CN/CNKI Law.js +842 -0
  804. package/vendor/translators_CN/CNKI RefWorks.js +826 -0
  805. package/vendor/translators_CN/CNKI Refer.js +811 -0
  806. package/vendor/translators_CN/CNKI Scholar.js +656 -0
  807. package/vendor/translators_CN/CNKI TIKS.js +1028 -0
  808. package/vendor/translators_CN/CNKI e-Books.js +309 -0
  809. package/vendor/translators_CN/CNKI thinker.js +274 -0
  810. package/vendor/translators_CN/CNKI.js +2381 -0
  811. package/vendor/translators_CN/CNSDA.js +382 -0
  812. package/vendor/translators_CN/CQVIP Knowledge.js +969 -0
  813. package/vendor/translators_CN/CQVIP Qikan.js +418 -0
  814. package/vendor/translators_CN/CQVIP.js +686 -0
  815. package/vendor/translators_CN/CSDN.js +153 -0
  816. package/vendor/translators_CN/China Judgements Online.js +282 -0
  817. package/vendor/translators_CN/China Social Science Library.js +533 -0
  818. package/vendor/translators_CN/ChinaXiv.js +375 -0
  819. package/vendor/translators_CN/Cubox.js +246 -0
  820. package/vendor/translators_CN/Dangdang.js +176 -0
  821. package/vendor/translators_CN/Douban.js +984 -0
  822. package/vendor/translators_CN/Duxiu.js +746 -0
  823. package/vendor/translators_CN/E-Tiller.js +555 -0
  824. package/vendor/translators_CN/Encyclopedia of China 3rd.js +177 -0
  825. package/vendor/translators_CN/Founder.js +483 -0
  826. package/vendor/translators_CN/GFSOSO.js +694 -0
  827. package/vendor/translators_CN/Jd.js +235 -0
  828. package/vendor/translators_CN/Jikan Full Text Database.js +188 -0
  829. package/vendor/translators_CN/KouShare.js +139 -0
  830. package/vendor/translators_CN/LICENSE +661 -0
  831. package/vendor/translators_CN/Lawbank.js +818 -0
  832. package/vendor/translators_CN/MagTech.js +473 -0
  833. package/vendor/translators_CN/Modern History.js +788 -0
  834. package/vendor/translators_CN/NDLTD.js +404 -0
  835. package/vendor/translators_CN/NTU Digital Library of Buddhist Studies.js +957 -0
  836. package/vendor/translators_CN/National Public Service Platform for Standards Information - China.js +348 -0
  837. package/vendor/translators_CN/National Science and Technology Library - China.js +646 -0
  838. package/vendor/translators_CN/National Science and Technology Report Service - China.js +271 -0
  839. package/vendor/translators_CN/National Standards Open System - China.js +296 -0
  840. package/vendor/translators_CN/Ncpssd.js +555 -0
  841. package/vendor/translators_CN/PKULaw.js +1123 -0
  842. package/vendor/translators_CN/PatentStar.js +386 -0
  843. package/vendor/translators_CN/People's Daily Database.js +186 -0
  844. package/vendor/translators_CN/People's Daily Epaper.js +279 -0
  845. package/vendor/translators_CN/People's Daily Online.js +292 -0
  846. package/vendor/translators_CN/Pishu Database.js +383 -0
  847. package/vendor/translators_CN/ProQuestCN Thesis.js +329 -0
  848. package/vendor/translators_CN/Pss-System.js +199 -0
  849. package/vendor/translators_CN/PubScholar.js +588 -0
  850. package/vendor/translators_CN/Publications Data Center - China.js +1119 -0
  851. package/vendor/translators_CN/QStheory.js +251 -0
  852. package/vendor/translators_CN/RDFYBK.js +396 -0
  853. package/vendor/translators_CN/README.md +42 -0
  854. package/vendor/translators_CN/RHHZ.js +383 -0
  855. package/vendor/translators_CN/Rural Studies Database.js +402 -0
  856. package/vendor/translators_CN/SKCTK.js +177 -0
  857. package/vendor/translators_CN/Samson.js +768 -0
  858. package/vendor/translators_CN/SciEngine.js +450 -0
  859. package/vendor/translators_CN/Science Reading.js +338 -0
  860. package/vendor/translators_CN/Sina Weibo.js +254 -0
  861. package/vendor/translators_CN/Soopat.js +427 -0
  862. package/vendor/translators_CN/Spc.org.cn.js +275 -0
  863. package/vendor/translators_CN/Standard Full-text Database - NLC.js +230 -0
  864. package/vendor/translators_CN/SuperLib.js +757 -0
  865. package/vendor/translators_CN/TOAJ.js +422 -0
  866. package/vendor/translators_CN/Wanfang Data.js +3433 -0
  867. package/vendor/translators_CN/Wanfang Med.js +671 -0
  868. package/vendor/translators_CN/Web of Science Tagged.js +1341 -0
  869. package/vendor/translators_CN/Weixin.js +130 -0
  870. package/vendor/translators_CN/Wenjin.js +492 -0
  871. package/vendor/translators_CN/Xinhuanet.js +283 -0
  872. package/vendor/translators_CN/Yiigle.js +394 -0
  873. package/vendor/translators_CN/Zhihu.js +530 -0
  874. package/vendor/translators_CN/Zhihuiya.js +246 -0
  875. package/vendor/translators_CN/Zhizhen.js +869 -0
  876. package/vendor/translators_CN/chaoxingqikan.js +360 -0
  877. package/vendor/translators_CN/doc.taixueshu.js +355 -0
  878. package/vendor/translators_CN/dpaper.js +183 -0
  879. package/vendor/translators_CN/epaper.gmw.cn.js +286 -0
  880. package/vendor/translators_CN/flk.npc.gov.cn.js +344 -0
  881. package/vendor/translators_CN/gov.cn Policy.js +559 -0
  882. package/vendor/translators_CN/incoPat.js +195 -0
  883. package/vendor/translators_CN/pm.tsgyun.js +177 -0
  884. package/vendor/translators_CN/sanmin.com.tw.js +327 -0
  885. package/vendor/translators_CN/sharing.com.tw.js +333 -0
  886. package/vendor/translators_CN/stats.gov.cn.js +170 -0
  887. package/vendor/translators_CN/xiaoyuzhoufm.js +162 -0
  888. package/vendor/translators_CN/zhangqiaokeyan.js +1130 -0
  889. package/vendor/zotero/README.md +120 -0
  890. package/vendor/zotero/config/custom-environment-variables.json +3 -0
  891. package/vendor/zotero/config/default.json5 +16 -0
  892. package/vendor/zotero/config/production.json5 +7 -0
  893. package/vendor/zotero/config/test.json5 +4 -0
  894. package/vendor/zotero/modules/translate/README.md +43 -0
  895. package/vendor/zotero/modules/translate/src/debug.js +186 -0
  896. package/vendor/zotero/modules/translate/src/http.js +158 -0
  897. package/vendor/zotero/modules/translate/src/promise.js +73 -0
  898. package/vendor/zotero/modules/translate/src/proxy.js +354 -0
  899. package/vendor/zotero/modules/translate/src/rdf/identity.js +503 -0
  900. package/vendor/zotero/modules/translate/src/rdf/init.js +56 -0
  901. package/vendor/zotero/modules/translate/src/rdf/n3parser.js +1392 -0
  902. package/vendor/zotero/modules/translate/src/rdf/rdfparser.js +578 -0
  903. package/vendor/zotero/modules/translate/src/rdf/serialize.js +886 -0
  904. package/vendor/zotero/modules/translate/src/rdf/term.js +524 -0
  905. package/vendor/zotero/modules/translate/src/rdf/uri.js +147 -0
  906. package/vendor/zotero/modules/translate/src/repo.js +80 -0
  907. package/vendor/zotero/modules/translate/src/tlds.js +274 -0
  908. package/vendor/zotero/modules/translate/src/translation/sandboxManager.js +112 -0
  909. package/vendor/zotero/modules/translate/src/translation/translate.js +3283 -0
  910. package/vendor/zotero/modules/translate/src/translation/translate_item.js +120 -0
  911. package/vendor/zotero/modules/translate/src/translator.js +186 -0
  912. package/vendor/zotero/modules/translate/src/translators.js +174 -0
  913. package/vendor/zotero/modules/translate/src/utilities_translate.js +487 -0
  914. package/vendor/zotero/modules/translate/src/zotero.js +94 -0
  915. package/vendor/zotero/modules/translators/A Contra Corriente.js +206 -0
  916. package/vendor/zotero/modules/translators/ABC News Australia.js +224 -0
  917. package/vendor/zotero/modules/translators/ACLS Humanities EBook.js +165 -0
  918. package/vendor/zotero/modules/translators/ACLWeb.js +600 -0
  919. package/vendor/zotero/modules/translators/ACM Digital Library.js +710 -0
  920. package/vendor/zotero/modules/translators/ACS Publications.js +633 -0
  921. package/vendor/zotero/modules/translators/ADS Bibcode.js +664 -0
  922. package/vendor/zotero/modules/translators/AEA Web.js +450 -0
  923. package/vendor/zotero/modules/translators/AGRIS.js +311 -0
  924. package/vendor/zotero/modules/translators/AIP.js +295 -0
  925. package/vendor/zotero/modules/translators/AMS Journals.js +136 -0
  926. package/vendor/zotero/modules/translators/AMS MathSciNet (Legacy).js +313 -0
  927. package/vendor/zotero/modules/translators/AMS MathSciNet.js +288 -0
  928. package/vendor/zotero/modules/translators/APA PsycNET.js +915 -0
  929. package/vendor/zotero/modules/translators/APN.ru.js +132 -0
  930. package/vendor/zotero/modules/translators/APS-Physics.js +204 -0
  931. package/vendor/zotero/modules/translators/APS.js +542 -0
  932. package/vendor/zotero/modules/translators/ARTFL Encyclopedie.js +283 -0
  933. package/vendor/zotero/modules/translators/ARTnews.js +155 -0
  934. package/vendor/zotero/modules/translators/ARTstor.js +619 -0
  935. package/vendor/zotero/modules/translators/ASCE.js +199 -0
  936. package/vendor/zotero/modules/translators/ASCO Meeting Library.js +153 -0
  937. package/vendor/zotero/modules/translators/ASTIS.js +337 -0
  938. package/vendor/zotero/modules/translators/ATS International Journal.js +290 -0
  939. package/vendor/zotero/modules/translators/Access Engineering.js +384 -0
  940. package/vendor/zotero/modules/translators/Access Medicine.js +343 -0
  941. package/vendor/zotero/modules/translators/Access Science.js +391 -0
  942. package/vendor/zotero/modules/translators/Adam Matthew Digital.js +405 -0
  943. package/vendor/zotero/modules/translators/Agencia del ISBN.js +77 -0
  944. package/vendor/zotero/modules/translators/Ahval News.js +309 -0
  945. package/vendor/zotero/modules/translators/Air University Journals.js +236 -0
  946. package/vendor/zotero/modules/translators/Airiti.js +423 -0
  947. package/vendor/zotero/modules/translators/Alexander Street Press.js +198 -0
  948. package/vendor/zotero/modules/translators/AllAfrica.js +291 -0
  949. package/vendor/zotero/modules/translators/Alsharekh.js +242 -0
  950. package/vendor/zotero/modules/translators/AlterNet.js +134 -0
  951. package/vendor/zotero/modules/translators/Aluka.js +261 -0
  952. package/vendor/zotero/modules/translators/Amazon.js +1043 -0
  953. package/vendor/zotero/modules/translators/American Archive of Public Broadcasting.js +592 -0
  954. package/vendor/zotero/modules/translators/American Institute of Aeronautics and Astronautics.js +210 -0
  955. package/vendor/zotero/modules/translators/American Prospect.js +114 -0
  956. package/vendor/zotero/modules/translators/Ancestry.com US Federal Census.js +164 -0
  957. package/vendor/zotero/modules/translators/Annual Reviews.js +329 -0
  958. package/vendor/zotero/modules/translators/Antikvarium.hu.js +280 -0
  959. package/vendor/zotero/modules/translators/AquaDocs.js +390 -0
  960. package/vendor/zotero/modules/translators/Archeion.js +249 -0
  961. package/vendor/zotero/modules/translators/Archiv fuer Sozialgeschichte.js +157 -0
  962. package/vendor/zotero/modules/translators/Archive Ouverte en Sciences de l'Information et de la Communication (AOSIC).js +179 -0
  963. package/vendor/zotero/modules/translators/Archives Canada.js +142 -0
  964. package/vendor/zotero/modules/translators/Ariana News.js +203 -0
  965. package/vendor/zotero/modules/translators/Art Institute of Chicago.js +279 -0
  966. package/vendor/zotero/modules/translators/Artefacts Canada.js +192 -0
  967. package/vendor/zotero/modules/translators/Artforum.js +438 -0
  968. package/vendor/zotero/modules/translators/Atlanta Journal Constitution.js +155 -0
  969. package/vendor/zotero/modules/translators/Atlanta Journal-Constitution.js +311 -0
  970. package/vendor/zotero/modules/translators/Atypon Journals.js +1204 -0
  971. package/vendor/zotero/modules/translators/AustLII and NZLII.js +604 -0
  972. package/vendor/zotero/modules/translators/Australian Dictionary of Biography.js +196 -0
  973. package/vendor/zotero/modules/translators/BAILII.js +176 -0
  974. package/vendor/zotero/modules/translators/BBC Genome.js +254 -0
  975. package/vendor/zotero/modules/translators/BBC.js +430 -0
  976. package/vendor/zotero/modules/translators/BIBSYS.js +112 -0
  977. package/vendor/zotero/modules/translators/BOCC.js +217 -0
  978. package/vendor/zotero/modules/translators/BOE.js +229 -0
  979. package/vendor/zotero/modules/translators/BOFiP-Impots.js +132 -0
  980. package/vendor/zotero/modules/translators/Baidu Scholar.js +276 -0
  981. package/vendor/zotero/modules/translators/Bangkok Post.js +216 -0
  982. package/vendor/zotero/modules/translators/Baruch Foundation.js +175 -0
  983. package/vendor/zotero/modules/translators/Beobachter.js +175 -0
  984. package/vendor/zotero/modules/translators/Bezneng Gajit.js +92 -0
  985. package/vendor/zotero/modules/translators/BibLaTeX.js +888 -0
  986. package/vendor/zotero/modules/translators/BibTeX.js +4236 -0
  987. package/vendor/zotero/modules/translators/Biblio.com.js +216 -0
  988. package/vendor/zotero/modules/translators/Bibliontology RDF.js +1166 -0
  989. package/vendor/zotero/modules/translators/Biblioteca Nacional de Maestros.js +213 -0
  990. package/vendor/zotero/modules/translators/Bibliotheque et Archives Nationale du Quebec (Pistard).js +147 -0
  991. package/vendor/zotero/modules/translators/Bibliotheque et Archives Nationales du Quebec.js +266 -0
  992. package/vendor/zotero/modules/translators/Bibliotheque nationale de France.js +872 -0
  993. package/vendor/zotero/modules/translators/BioMed Central.js +585 -0
  994. package/vendor/zotero/modules/translators/BioOne.js +233 -0
  995. package/vendor/zotero/modules/translators/Bioconductor.js +343 -0
  996. package/vendor/zotero/modules/translators/Blaetter.js +192 -0
  997. package/vendor/zotero/modules/translators/Blogger.js +244 -0
  998. package/vendor/zotero/modules/translators/Bloomberg.js +205 -0
  999. package/vendor/zotero/modules/translators/Bloomsbury Food Library.js +296 -0
  1000. package/vendor/zotero/modules/translators/Bluesky.js +195 -0
  1001. package/vendor/zotero/modules/translators/BnF ISBN.js +129 -0
  1002. package/vendor/zotero/modules/translators/Bookmarks.js +282 -0
  1003. package/vendor/zotero/modules/translators/Bookshop.org.js +223 -0
  1004. package/vendor/zotero/modules/translators/Boston Review.js +332 -0
  1005. package/vendor/zotero/modules/translators/Bosworth Toller's Anglo-Saxon Dictionary Online.js +496 -0
  1006. package/vendor/zotero/modules/translators/Bracero History Archive.js +200 -0
  1007. package/vendor/zotero/modules/translators/Brill Journals.js +168 -0
  1008. package/vendor/zotero/modules/translators/Brill.js +434 -0
  1009. package/vendor/zotero/modules/translators/Brukerhandboken.js +153 -0
  1010. package/vendor/zotero/modules/translators/Bryn Mawr Classical Review.js +303 -0
  1011. package/vendor/zotero/modules/translators/Bundesgesetzblatt.js +231 -0
  1012. package/vendor/zotero/modules/translators/Business Standard.js +151 -0
  1013. package/vendor/zotero/modules/translators/CABI - CAB Abstracts.js +317 -0
  1014. package/vendor/zotero/modules/translators/CAOD.js +260 -0
  1015. package/vendor/zotero/modules/translators/CBC.js +439 -0
  1016. package/vendor/zotero/modules/translators/CCfr (BnF).js +162 -0
  1017. package/vendor/zotero/modules/translators/CERN Document Server.js +277 -0
  1018. package/vendor/zotero/modules/translators/CEUR Workshop Proceedings.js +145 -0
  1019. package/vendor/zotero/modules/translators/CFF References.js +170 -0
  1020. package/vendor/zotero/modules/translators/CFF.js +113 -0
  1021. package/vendor/zotero/modules/translators/CIA World Factbook.js +128 -0
  1022. package/vendor/zotero/modules/translators/CLACSO.js +226 -0
  1023. package/vendor/zotero/modules/translators/CLASE.js +135 -0
  1024. package/vendor/zotero/modules/translators/CNKI.js +731 -0
  1025. package/vendor/zotero/modules/translators/COBISS.js +1945 -0
  1026. package/vendor/zotero/modules/translators/COinS.js +336 -0
  1027. package/vendor/zotero/modules/translators/CQ Press.js +418 -0
  1028. package/vendor/zotero/modules/translators/CROSBI.js +227 -0
  1029. package/vendor/zotero/modules/translators/CSIRO Publishing.js +139 -0
  1030. package/vendor/zotero/modules/translators/CSL JSON.js +203 -0
  1031. package/vendor/zotero/modules/translators/CSV.js +294 -0
  1032. package/vendor/zotero/modules/translators/Cairn.info.js +368 -0
  1033. package/vendor/zotero/modules/translators/CalMatters.js +228 -0
  1034. package/vendor/zotero/modules/translators/Calisphere.js +355 -0
  1035. package/vendor/zotero/modules/translators/Camara Brasileira do Livro ISBN.js +842 -0
  1036. package/vendor/zotero/modules/translators/Cambridge Core.js +580 -0
  1037. package/vendor/zotero/modules/translators/Cambridge Engage Preprints.js +276 -0
  1038. package/vendor/zotero/modules/translators/CanLII.js +284 -0
  1039. package/vendor/zotero/modules/translators/Canada.com.js +171 -0
  1040. package/vendor/zotero/modules/translators/Canadian Letters and Images.js +148 -0
  1041. package/vendor/zotero/modules/translators/Canadiana.ca.js +196 -0
  1042. package/vendor/zotero/modules/translators/Cascadilla Proceedings Project.js +390 -0
  1043. package/vendor/zotero/modules/translators/Cell Press.js +516 -0
  1044. package/vendor/zotero/modules/translators/Central and Eastern European Online Library Journals.js +313 -0
  1045. package/vendor/zotero/modules/translators/Champlain Society - Collection.js +220 -0
  1046. package/vendor/zotero/modules/translators/Chicago Journal of Theoretical Computer Science.js +203 -0
  1047. package/vendor/zotero/modules/translators/Christian Science Monitor.js +148 -0
  1048. package/vendor/zotero/modules/translators/Chronicling America.js +131 -0
  1049. package/vendor/zotero/modules/translators/CiNii Research.js +168 -0
  1050. package/vendor/zotero/modules/translators/Citavi 5 XML.js +516 -0
  1051. package/vendor/zotero/modules/translators/CiteSeer.js +196 -0
  1052. package/vendor/zotero/modules/translators/CiteULike.js +170 -0
  1053. package/vendor/zotero/modules/translators/Citizen Lab.js +347 -0
  1054. package/vendor/zotero/modules/translators/Civilization.ca.js +146 -0
  1055. package/vendor/zotero/modules/translators/Climate Change and Human Health Literature Portal.js +283 -0
  1056. package/vendor/zotero/modules/translators/Clinical Key.js +367 -0
  1057. package/vendor/zotero/modules/translators/Code4Lib Journal.js +143 -0
  1058. package/vendor/zotero/modules/translators/Colorado State Legislature.js +215 -0
  1059. package/vendor/zotero/modules/translators/Columbia University Press.js +238 -0
  1060. package/vendor/zotero/modules/translators/Common-Place.js +218 -0
  1061. package/vendor/zotero/modules/translators/Computer History Museum Archive.js +1319 -0
  1062. package/vendor/zotero/modules/translators/Copernicus.js +461 -0
  1063. package/vendor/zotero/modules/translators/Cornell LII.js +252 -0
  1064. package/vendor/zotero/modules/translators/Cornell University Press.js +235 -0
  1065. package/vendor/zotero/modules/translators/CourtListener.js +265 -0
  1066. package/vendor/zotero/modules/translators/CrossRef.js +486 -0
  1067. package/vendor/zotero/modules/translators/Crossref REST.js +1757 -0
  1068. package/vendor/zotero/modules/translators/Crossref Unixref XML.js +965 -0
  1069. package/vendor/zotero/modules/translators/Current Affairs.js +160 -0
  1070. package/vendor/zotero/modules/translators/Cyberpresse.js +131 -0
  1071. package/vendor/zotero/modules/translators/DABI.js +476 -0
  1072. package/vendor/zotero/modules/translators/DAI-Zenon.js +392 -0
  1073. package/vendor/zotero/modules/translators/DART-Europe.js +176 -0
  1074. package/vendor/zotero/modules/translators/DBLP Computer Science Bibliography.js +462 -0
  1075. package/vendor/zotero/modules/translators/DBpia.js +233 -0
  1076. package/vendor/zotero/modules/translators/DEPATISnet.js +373 -0
  1077. package/vendor/zotero/modules/translators/DOAJ.js +243 -0
  1078. package/vendor/zotero/modules/translators/DOI Content Negotiation.js +352 -0
  1079. package/vendor/zotero/modules/translators/DOI.js +322 -0
  1080. package/vendor/zotero/modules/translators/DPLA.js +425 -0
  1081. package/vendor/zotero/modules/translators/DSpace Intermediate Metadata.js +208 -0
  1082. package/vendor/zotero/modules/translators/DTU Orbit.js +155 -0
  1083. package/vendor/zotero/modules/translators/Dagens Nyheter.js +369 -0
  1084. package/vendor/zotero/modules/translators/Dagstuhl Research Online Publication Server.js +276 -0
  1085. package/vendor/zotero/modules/translators/Dar Almandumah.js +253 -0
  1086. package/vendor/zotero/modules/translators/Data.gov.js +225 -0
  1087. package/vendor/zotero/modules/translators/DataCite.js +125 -0
  1088. package/vendor/zotero/modules/translators/Databrary.js +184 -0
  1089. package/vendor/zotero/modules/translators/Datacite JSON.js +1182 -0
  1090. package/vendor/zotero/modules/translators/Dataverse.js +418 -0
  1091. package/vendor/zotero/modules/translators/Daum News.js +139 -0
  1092. package/vendor/zotero/modules/translators/De Gruyter Brill.js +674 -0
  1093. package/vendor/zotero/modules/translators/DeGruyter.js +457 -0
  1094. package/vendor/zotero/modules/translators/Defense Technical Information Center.js +146 -0
  1095. package/vendor/zotero/modules/translators/Delpher.js +413 -0
  1096. package/vendor/zotero/modules/translators/Demographic Research.js +164 -0
  1097. package/vendor/zotero/modules/translators/Denik CZ.js +273 -0
  1098. package/vendor/zotero/modules/translators/Der Freitag.js +236 -0
  1099. package/vendor/zotero/modules/translators/Der Spiegel.js +479 -0
  1100. package/vendor/zotero/modules/translators/Desiring God.js +216 -0
  1101. package/vendor/zotero/modules/translators/Deutsche Fotothek.js +364 -0
  1102. package/vendor/zotero/modules/translators/Deutsche Nationalbibliothek.js +594 -0
  1103. package/vendor/zotero/modules/translators/Dialnet.js +258 -0
  1104. package/vendor/zotero/modules/translators/Die Zeit.js +378 -0
  1105. package/vendor/zotero/modules/translators/DigiZeitschriften.js +269 -0
  1106. package/vendor/zotero/modules/translators/Digital Humanities Quarterly.js +207 -0
  1107. package/vendor/zotero/modules/translators/Digital Medievalist.js +149 -0
  1108. package/vendor/zotero/modules/translators/Digital Spy.js +215 -0
  1109. package/vendor/zotero/modules/translators/Dimensions.js +233 -0
  1110. package/vendor/zotero/modules/translators/Douban.js +316 -0
  1111. package/vendor/zotero/modules/translators/Dreier Neuerscheinungsdienst.js +209 -0
  1112. package/vendor/zotero/modules/translators/DrugBank.ca.js +209 -0
  1113. package/vendor/zotero/modules/translators/Dryad Digital Repository.js +323 -0
  1114. package/vendor/zotero/modules/translators/Duke University Press Books.js +250 -0
  1115. package/vendor/zotero/modules/translators/E-periodica Switzerland.js +347 -0
  1116. package/vendor/zotero/modules/translators/EBSCO Discovery Layer.js +147 -0
  1117. package/vendor/zotero/modules/translators/EBSCOhost.js +592 -0
  1118. package/vendor/zotero/modules/translators/EIDR.js +184 -0
  1119. package/vendor/zotero/modules/translators/EPA National Library Catalog.js +245 -0
  1120. package/vendor/zotero/modules/translators/ERIC.js +986 -0
  1121. package/vendor/zotero/modules/translators/ESpacenet.js +465 -0
  1122. package/vendor/zotero/modules/translators/EUR-Lex.js +423 -0
  1123. package/vendor/zotero/modules/translators/Eastview.js +336 -0
  1124. package/vendor/zotero/modules/translators/Edinburgh University Press Journals.js +178 -0
  1125. package/vendor/zotero/modules/translators/Education Week.js +197 -0
  1126. package/vendor/zotero/modules/translators/El Comercio (Peru).js +347 -0
  1127. package/vendor/zotero/modules/translators/El Pais.js +262 -0
  1128. package/vendor/zotero/modules/translators/Electronic Colloquium on Computational Complexity.js +230 -0
  1129. package/vendor/zotero/modules/translators/Elicit.js +120 -0
  1130. package/vendor/zotero/modules/translators/Elsevier Health Journals.js +531 -0
  1131. package/vendor/zotero/modules/translators/Elsevier Pure.js +420 -0
  1132. package/vendor/zotero/modules/translators/Embedded Metadata.js +1951 -0
  1133. package/vendor/zotero/modules/translators/Emerald Insight.js +626 -0
  1134. package/vendor/zotero/modules/translators/Encyclopedia of Chicago.js +162 -0
  1135. package/vendor/zotero/modules/translators/Encyclopedia of Korean Culture.js +221 -0
  1136. package/vendor/zotero/modules/translators/Endnote XML.js +1407 -0
  1137. package/vendor/zotero/modules/translators/Engineering Village.js +139 -0
  1138. package/vendor/zotero/modules/translators/Envidat.js +331 -0
  1139. package/vendor/zotero/modules/translators/Epicurious.js +144 -0
  1140. package/vendor/zotero/modules/translators/Erudit.js +272 -0
  1141. package/vendor/zotero/modules/translators/Euclid.js +220 -0
  1142. package/vendor/zotero/modules/translators/EurasiaNet.js +109 -0
  1143. package/vendor/zotero/modules/translators/Eurogamer.js +112 -0
  1144. package/vendor/zotero/modules/translators/EurogamerUSgamer.js +380 -0
  1145. package/vendor/zotero/modules/translators/Europe PMC.js +448 -0
  1146. package/vendor/zotero/modules/translators/Evernote.js +104 -0
  1147. package/vendor/zotero/modules/translators/F1000 Research.js +655 -0
  1148. package/vendor/zotero/modules/translators/FAO Publications.js +1139 -0
  1149. package/vendor/zotero/modules/translators/FAZ.NET.js +231 -0
  1150. package/vendor/zotero/modules/translators/Fachportal Padagogik.js +839 -0
  1151. package/vendor/zotero/modules/translators/Factiva.js +302 -0
  1152. package/vendor/zotero/modules/translators/Failed Architecture.js +114 -0
  1153. package/vendor/zotero/modules/translators/Fairfax Australia.js +200 -0
  1154. package/vendor/zotero/modules/translators/Fatcat.js +425 -0
  1155. package/vendor/zotero/modules/translators/Figshare.js +191 -0
  1156. package/vendor/zotero/modules/translators/Financial Times.js +187 -0
  1157. package/vendor/zotero/modules/translators/Finna.js +357 -0
  1158. package/vendor/zotero/modules/translators/Flickr.js +318 -0
  1159. package/vendor/zotero/modules/translators/Foreign Affairs.js +485 -0
  1160. package/vendor/zotero/modules/translators/Foreign Policy.js +146 -0
  1161. package/vendor/zotero/modules/translators/FreeCite.js +95 -0
  1162. package/vendor/zotero/modules/translators/FreePatentsOnline.js +265 -0
  1163. package/vendor/zotero/modules/translators/Frieze.js +243 -0
  1164. package/vendor/zotero/modules/translators/Frontiers.js +667 -0
  1165. package/vendor/zotero/modules/translators/GMS German Medical Science.js +319 -0
  1166. package/vendor/zotero/modules/translators/GPO Access e-CFR.js +134 -0
  1167. package/vendor/zotero/modules/translators/Gale Databases.js +324 -0
  1168. package/vendor/zotero/modules/translators/GaleGDC.js +189 -0
  1169. package/vendor/zotero/modules/translators/Galegroup.js +327 -0
  1170. package/vendor/zotero/modules/translators/Gallica.js +194 -0
  1171. package/vendor/zotero/modules/translators/Game Studies.js +155 -0
  1172. package/vendor/zotero/modules/translators/GameSpot.js +199 -0
  1173. package/vendor/zotero/modules/translators/GameStar GamePro.js +198 -0
  1174. package/vendor/zotero/modules/translators/Gasyrlar Awazy.js +156 -0
  1175. package/vendor/zotero/modules/translators/Gemeinsamer Bibliotheksverbund ISBN.js +23 -0
  1176. package/vendor/zotero/modules/translators/Gene Ontology.js +128 -0
  1177. package/vendor/zotero/modules/translators/Github.js +268 -0
  1178. package/vendor/zotero/modules/translators/Globes.js +243 -0
  1179. package/vendor/zotero/modules/translators/Gmail.js +64 -0
  1180. package/vendor/zotero/modules/translators/Goodreads.js +219 -0
  1181. package/vendor/zotero/modules/translators/Google Books.js +552 -0
  1182. package/vendor/zotero/modules/translators/Google Patents.js +959 -0
  1183. package/vendor/zotero/modules/translators/Google Play.js +186 -0
  1184. package/vendor/zotero/modules/translators/Google Presentation.js +104 -0
  1185. package/vendor/zotero/modules/translators/Google Research.js +221 -0
  1186. package/vendor/zotero/modules/translators/Google Scholar.js +1361 -0
  1187. package/vendor/zotero/modules/translators/Gulag Many Days, Many Lives.js +179 -0
  1188. package/vendor/zotero/modules/translators/HAL Archives Ouvertes.js +429 -0
  1189. package/vendor/zotero/modules/translators/HCSP.js +312 -0
  1190. package/vendor/zotero/modules/translators/HUDOC.js +650 -0
  1191. package/vendor/zotero/modules/translators/Haaretz.js +562 -0
  1192. package/vendor/zotero/modules/translators/Habr.js +232 -0
  1193. package/vendor/zotero/modules/translators/Handelszeitung.js +273 -0
  1194. package/vendor/zotero/modules/translators/Hanrei Watch.js +123 -0
  1195. package/vendor/zotero/modules/translators/Harper's Magazine.js +365 -0
  1196. package/vendor/zotero/modules/translators/Harpers.js +209 -0
  1197. package/vendor/zotero/modules/translators/Harvard Business Review.js +326 -0
  1198. package/vendor/zotero/modules/translators/Harvard Caselaw Access Project.js +244 -0
  1199. package/vendor/zotero/modules/translators/Harvard University Press Books.js +286 -0
  1200. package/vendor/zotero/modules/translators/Hathi Trust.js +164 -0
  1201. package/vendor/zotero/modules/translators/HathiTrust.js +311 -0
  1202. package/vendor/zotero/modules/translators/HeinOnline.js +266 -0
  1203. package/vendor/zotero/modules/translators/Heise.js +200 -0
  1204. package/vendor/zotero/modules/translators/Herder.js +236 -0
  1205. package/vendor/zotero/modules/translators/HighBeam.js +198 -0
  1206. package/vendor/zotero/modules/translators/HighWire 2.0.js +1355 -0
  1207. package/vendor/zotero/modules/translators/HighWire.js +209 -0
  1208. package/vendor/zotero/modules/translators/Hindawi Publishers.js +177 -0
  1209. package/vendor/zotero/modules/translators/Hispanic-American Periodical Index.js +236 -0
  1210. package/vendor/zotero/modules/translators/Homeland Security Digital Library.js +426 -0
  1211. package/vendor/zotero/modules/translators/Hoovers.js +89 -0
  1212. package/vendor/zotero/modules/translators/Huff Post.js +223 -0
  1213. package/vendor/zotero/modules/translators/Human Rights Watch.js +450 -0
  1214. package/vendor/zotero/modules/translators/IBISWorld.js +171 -0
  1215. package/vendor/zotero/modules/translators/IDEA ALM.js +536 -0
  1216. package/vendor/zotero/modules/translators/IEEE Computer Society.js +429 -0
  1217. package/vendor/zotero/modules/translators/IEEE Xplore.js +1000 -0
  1218. package/vendor/zotero/modules/translators/IETF.js +451 -0
  1219. package/vendor/zotero/modules/translators/IGN.js +136 -0
  1220. package/vendor/zotero/modules/translators/IMDb.js +566 -0
  1221. package/vendor/zotero/modules/translators/INSPIRE.js +247 -0
  1222. package/vendor/zotero/modules/translators/IPCC.js +757 -0
  1223. package/vendor/zotero/modules/translators/ISTC.js +181 -0
  1224. package/vendor/zotero/modules/translators/Idref.js +179 -0
  1225. package/vendor/zotero/modules/translators/In These Times.js +196 -0
  1226. package/vendor/zotero/modules/translators/InfoTrac.js +279 -0
  1227. package/vendor/zotero/modules/translators/Informationssystem Medienpaedagogik.js +305 -0
  1228. package/vendor/zotero/modules/translators/IngentaConnect.js +312 -0
  1229. package/vendor/zotero/modules/translators/Inside Higher Ed.js +167 -0
  1230. package/vendor/zotero/modules/translators/Insignia OPAC.js +114 -0
  1231. package/vendor/zotero/modules/translators/Institute of Contemporary Art.js +180 -0
  1232. package/vendor/zotero/modules/translators/Institute of Physics.js +340 -0
  1233. package/vendor/zotero/modules/translators/Integrum.js +173 -0
  1234. package/vendor/zotero/modules/translators/Intellixir.js +264 -0
  1235. package/vendor/zotero/modules/translators/Inter-Research Science Center.js +185 -0
  1236. package/vendor/zotero/modules/translators/International Nuclear Information System.js +285 -0
  1237. package/vendor/zotero/modules/translators/Internet Archive Scholar.js +106 -0
  1238. package/vendor/zotero/modules/translators/Internet Archive Wayback Machine.js +152 -0
  1239. package/vendor/zotero/modules/translators/Internet Archive.js +552 -0
  1240. package/vendor/zotero/modules/translators/InvenioRDM.js +1260 -0
  1241. package/vendor/zotero/modules/translators/Isidore.js +235 -0
  1242. package/vendor/zotero/modules/translators/J-Stage.js +272 -0
  1243. package/vendor/zotero/modules/translators/JETS.js +122 -0
  1244. package/vendor/zotero/modules/translators/JISC Historical Texts.js +273 -0
  1245. package/vendor/zotero/modules/translators/JRC Publications Repository.js +569 -0
  1246. package/vendor/zotero/modules/translators/JSTOR.js +821 -0
  1247. package/vendor/zotero/modules/translators/Jahrbuch.js +213 -0
  1248. package/vendor/zotero/modules/translators/Japan Times Online.js +138 -0
  1249. package/vendor/zotero/modules/translators/Journal of Electronic Publishing.js +147 -0
  1250. package/vendor/zotero/modules/translators/Journal of Extension.js +288 -0
  1251. package/vendor/zotero/modules/translators/Journal of Machine Learning Research.js +441 -0
  1252. package/vendor/zotero/modules/translators/Journal of Religion and Society.js +174 -0
  1253. package/vendor/zotero/modules/translators/JurPC.js +274 -0
  1254. package/vendor/zotero/modules/translators/Juricaf.js +141 -0
  1255. package/vendor/zotero/modules/translators/Juris.js +728 -0
  1256. package/vendor/zotero/modules/translators/K10plus ISBN.js +386 -0
  1257. package/vendor/zotero/modules/translators/KStudy.js +632 -0
  1258. package/vendor/zotero/modules/translators/Kanopy.js +358 -0
  1259. package/vendor/zotero/modules/translators/Khaama Press.js +249 -0
  1260. package/vendor/zotero/modules/translators/KitapYurdu.com.js +376 -0
  1261. package/vendor/zotero/modules/translators/Kommersant.js +174 -0
  1262. package/vendor/zotero/modules/translators/Korean National Library.js +621 -0
  1263. package/vendor/zotero/modules/translators/L'Annee Philologique.js +267 -0
  1264. package/vendor/zotero/modules/translators/LA Times.js +239 -0
  1265. package/vendor/zotero/modules/translators/LIBRIS ISBN.js +103 -0
  1266. package/vendor/zotero/modules/translators/LIVIVO.js +260 -0
  1267. package/vendor/zotero/modules/translators/LWN.net.js +350 -0
  1268. package/vendor/zotero/modules/translators/La Croix.js +237 -0
  1269. package/vendor/zotero/modules/translators/La Nacion (Argentina).js +231 -0
  1270. package/vendor/zotero/modules/translators/La Presse.js +218 -0
  1271. package/vendor/zotero/modules/translators/La Republica (Peru).js +174 -0
  1272. package/vendor/zotero/modules/translators/Lagen.nu.js +214 -0
  1273. package/vendor/zotero/modules/translators/Landesbibliographie Baden-Wurttemberg.js +324 -0
  1274. package/vendor/zotero/modules/translators/Lapham's Quarterly.js +1062 -0
  1275. package/vendor/zotero/modules/translators/Le Devoir.js +167 -0
  1276. package/vendor/zotero/modules/translators/Le Figaro.js +225 -0
  1277. package/vendor/zotero/modules/translators/Le Maitron.js +484 -0
  1278. package/vendor/zotero/modules/translators/Le Monde.js +258 -0
  1279. package/vendor/zotero/modules/translators/Le monde diplomatique.js +200 -0
  1280. package/vendor/zotero/modules/translators/Legifrance.js +641 -0
  1281. package/vendor/zotero/modules/translators/Legislative Insight.js +406 -0
  1282. package/vendor/zotero/modules/translators/Lexis+.js +264 -0
  1283. package/vendor/zotero/modules/translators/LexisNexis.js +505 -0
  1284. package/vendor/zotero/modules/translators/Libraries Tasmania.js +778 -0
  1285. package/vendor/zotero/modules/translators/Library Catalog (Aleph).js +385 -0
  1286. package/vendor/zotero/modules/translators/Library Catalog (Amicus).js +254 -0
  1287. package/vendor/zotero/modules/translators/Library Catalog (Aquabrowser).js +253 -0
  1288. package/vendor/zotero/modules/translators/Library Catalog (BiblioCommons).js +249 -0
  1289. package/vendor/zotero/modules/translators/Library Catalog (Blacklight).js +341 -0
  1290. package/vendor/zotero/modules/translators/Library Catalog (Capita Prism).js +162 -0
  1291. package/vendor/zotero/modules/translators/Library Catalog (DRA).js +203 -0
  1292. package/vendor/zotero/modules/translators/Library Catalog (Dynix).js +315 -0
  1293. package/vendor/zotero/modules/translators/Library Catalog (EBSCO Locate).js +349 -0
  1294. package/vendor/zotero/modules/translators/Library Catalog (Encore).js +211 -0
  1295. package/vendor/zotero/modules/translators/Library Catalog (GEAC).js +104 -0
  1296. package/vendor/zotero/modules/translators/Library Catalog (InnoPAC).js +305 -0
  1297. package/vendor/zotero/modules/translators/Library Catalog (Koha).js +225 -0
  1298. package/vendor/zotero/modules/translators/Library Catalog (Mango).js +285 -0
  1299. package/vendor/zotero/modules/translators/Library Catalog (OPALS).js +200 -0
  1300. package/vendor/zotero/modules/translators/Library Catalog (PICA).js +2120 -0
  1301. package/vendor/zotero/modules/translators/Library Catalog (PICA2).js +331 -0
  1302. package/vendor/zotero/modules/translators/Library Catalog (Pika).js +488 -0
  1303. package/vendor/zotero/modules/translators/Library Catalog (Polaris).js +183 -0
  1304. package/vendor/zotero/modules/translators/Library Catalog (Quolto).js +320 -0
  1305. package/vendor/zotero/modules/translators/Library Catalog (RERO ILS).js +754 -0
  1306. package/vendor/zotero/modules/translators/Library Catalog (SIRSI eLibrary).js +407 -0
  1307. package/vendor/zotero/modules/translators/Library Catalog (SIRSI).js +452 -0
  1308. package/vendor/zotero/modules/translators/Library Catalog (SLIMS).js +146 -0
  1309. package/vendor/zotero/modules/translators/Library Catalog (TIND ILS).js +773 -0
  1310. package/vendor/zotero/modules/translators/Library Catalog (TLCYouSeeMore).js +132 -0
  1311. package/vendor/zotero/modules/translators/Library Catalog (TinREAD).js +277 -0
  1312. package/vendor/zotero/modules/translators/Library Catalog (VTLS).js +125 -0
  1313. package/vendor/zotero/modules/translators/Library Catalog (Visual Library 2021).js +765 -0
  1314. package/vendor/zotero/modules/translators/Library Catalog (Voyager 7).js +199 -0
  1315. package/vendor/zotero/modules/translators/Library Catalog (Voyager).js +212 -0
  1316. package/vendor/zotero/modules/translators/Library Genesis.js +337 -0
  1317. package/vendor/zotero/modules/translators/Library Hub Discover.js +359 -0
  1318. package/vendor/zotero/modules/translators/Library of Congress Digital Collections.js +869 -0
  1319. package/vendor/zotero/modules/translators/Library of Congress ISBN.js +86 -0
  1320. package/vendor/zotero/modules/translators/LingBuzz.js +359 -0
  1321. package/vendor/zotero/modules/translators/Lippincott Williams and Wilkins.js +341 -0
  1322. package/vendor/zotero/modules/translators/Literary Hub.js +950 -0
  1323. package/vendor/zotero/modules/translators/Litres.js +236 -0
  1324. package/vendor/zotero/modules/translators/LiveJournal.js +219 -0
  1325. package/vendor/zotero/modules/translators/London Review of Books.js +291 -0
  1326. package/vendor/zotero/modules/translators/LookUs.js +260 -0
  1327. package/vendor/zotero/modules/translators/Lulu.js +264 -0
  1328. package/vendor/zotero/modules/translators/MAB2.js +373 -0
  1329. package/vendor/zotero/modules/translators/MARC.js +1080 -0
  1330. package/vendor/zotero/modules/translators/MARCXML.js +489 -0
  1331. package/vendor/zotero/modules/translators/MCV.js +258 -0
  1332. package/vendor/zotero/modules/translators/MDPI Journals.js +356 -0
  1333. package/vendor/zotero/modules/translators/MEDLINEnbib.js +1561 -0
  1334. package/vendor/zotero/modules/translators/METS.js +384 -0
  1335. package/vendor/zotero/modules/translators/MIDAS Journals.js +343 -0
  1336. package/vendor/zotero/modules/translators/MIT Press Books.js +637 -0
  1337. package/vendor/zotero/modules/translators/MIT Press Journals.js +176 -0
  1338. package/vendor/zotero/modules/translators/MODS.js +2545 -0
  1339. package/vendor/zotero/modules/translators/MPG PuRe.js +541 -0
  1340. package/vendor/zotero/modules/translators/Mailman.js +136 -0
  1341. package/vendor/zotero/modules/translators/Mainichi Daily News.js +189 -0
  1342. package/vendor/zotero/modules/translators/Mastodon.js +198 -0
  1343. package/vendor/zotero/modules/translators/Matbugat.ru.js +110 -0
  1344. package/vendor/zotero/modules/translators/Max Planck Institute for the History of Science Virtual Laboratory Library.js +136 -0
  1345. package/vendor/zotero/modules/translators/Medium.js +208 -0
  1346. package/vendor/zotero/modules/translators/MetaLib.js +126 -0
  1347. package/vendor/zotero/modules/translators/Microbiology Society Journals.js +343 -0
  1348. package/vendor/zotero/modules/translators/Microsoft Academic.js +424 -0
  1349. package/vendor/zotero/modules/translators/Mikromarc.js +207 -0
  1350. package/vendor/zotero/modules/translators/Milli Kutuphane.js +244 -0
  1351. package/vendor/zotero/modules/translators/Musee du Louvre.js +265 -0
  1352. package/vendor/zotero/modules/translators/NASA ADS.js +889 -0
  1353. package/vendor/zotero/modules/translators/NASA NTRS.js +610 -0
  1354. package/vendor/zotero/modules/translators/NCBI Nucleotide.js +227 -0
  1355. package/vendor/zotero/modules/translators/NPR.js +341 -0
  1356. package/vendor/zotero/modules/translators/NRC Research Press.js +181 -0
  1357. package/vendor/zotero/modules/translators/NRC.nl.js +226 -0
  1358. package/vendor/zotero/modules/translators/NTSB Accident Reports.js +226 -0
  1359. package/vendor/zotero/modules/translators/NYPL Menus.js +275 -0
  1360. package/vendor/zotero/modules/translators/NYPL Research Catalog.js +458 -0
  1361. package/vendor/zotero/modules/translators/NYTimes.com.js +858 -0
  1362. package/vendor/zotero/modules/translators/NZZ.ch.js +272 -0
  1363. package/vendor/zotero/modules/translators/Nagoya University OPAC.js +336 -0
  1364. package/vendor/zotero/modules/translators/National Academies Press.js +243 -0
  1365. package/vendor/zotero/modules/translators/National Agriculture Library.js +524 -0
  1366. package/vendor/zotero/modules/translators/National Archive of the UK.js +237 -0
  1367. package/vendor/zotero/modules/translators/National Archives of Australia.js +522 -0
  1368. package/vendor/zotero/modules/translators/National Archives of South Africa.js +122 -0
  1369. package/vendor/zotero/modules/translators/National Archives of the United States.js +278 -0
  1370. package/vendor/zotero/modules/translators/National Bureau of Economic Research.js +363 -0
  1371. package/vendor/zotero/modules/translators/National Diet Library Catalogue.js +333 -0
  1372. package/vendor/zotero/modules/translators/National Gallery of Art - USA.js +149 -0
  1373. package/vendor/zotero/modules/translators/National Gallery of Australia.js +252 -0
  1374. package/vendor/zotero/modules/translators/National Library of Australia (new catalog).js +223 -0
  1375. package/vendor/zotero/modules/translators/National Library of Belarus.js +171 -0
  1376. package/vendor/zotero/modules/translators/National Library of Norway.js +310 -0
  1377. package/vendor/zotero/modules/translators/National Library of Poland ISBN.js +140 -0
  1378. package/vendor/zotero/modules/translators/National Post.js +186 -0
  1379. package/vendor/zotero/modules/translators/National Technical Reports Library.js +276 -0
  1380. package/vendor/zotero/modules/translators/National Transportation Library ROSA P.js +562 -0
  1381. package/vendor/zotero/modules/translators/Nature Publishing Group.js +3394 -0
  1382. package/vendor/zotero/modules/translators/Neural Information Processing Systems.js +385 -0
  1383. package/vendor/zotero/modules/translators/New Left Review.js +245 -0
  1384. package/vendor/zotero/modules/translators/New Zealand Herald.js +282 -0
  1385. package/vendor/zotero/modules/translators/Newlines Magazine.js +190 -0
  1386. package/vendor/zotero/modules/translators/News Corp Australia.js +251 -0
  1387. package/vendor/zotero/modules/translators/NewsBank.js +202 -0
  1388. package/vendor/zotero/modules/translators/NewsnetTamedia.js +509 -0
  1389. package/vendor/zotero/modules/translators/Noor Digital Library.js +267 -0
  1390. package/vendor/zotero/modules/translators/Note HTML.js +206 -0
  1391. package/vendor/zotero/modules/translators/Note Markdown.js +1638 -0
  1392. package/vendor/zotero/modules/translators/Notre Dame Philosophical Reviews.js +201 -0
  1393. package/vendor/zotero/modules/translators/Nuclear Receptor Signaling.js +241 -0
  1394. package/vendor/zotero/modules/translators/OAPEN.js +331 -0
  1395. package/vendor/zotero/modules/translators/OECD.js +216 -0
  1396. package/vendor/zotero/modules/translators/ORCID.js +104 -0
  1397. package/vendor/zotero/modules/translators/OSF Preprints.js +417 -0
  1398. package/vendor/zotero/modules/translators/OSTI Energy Citations.js +202 -0
  1399. package/vendor/zotero/modules/translators/OVID Tagged.js +1226 -0
  1400. package/vendor/zotero/modules/translators/OZON.ru.js +465 -0
  1401. package/vendor/zotero/modules/translators/OhioLINK.js +43 -0
  1402. package/vendor/zotero/modules/translators/Old Bailey Online.js +315 -0
  1403. package/vendor/zotero/modules/translators/Open Conf.js +172 -0
  1404. package/vendor/zotero/modules/translators/Open Journal Systems.js +729 -0
  1405. package/vendor/zotero/modules/translators/Open Knowledge Repository.js +811 -0
  1406. package/vendor/zotero/modules/translators/OpenAlex JSON.js +740 -0
  1407. package/vendor/zotero/modules/translators/OpenAlex.js +444 -0
  1408. package/vendor/zotero/modules/translators/OpenEdition Books.js +155 -0
  1409. package/vendor/zotero/modules/translators/OpenEdition Journals.js +432 -0
  1410. package/vendor/zotero/modules/translators/Optical Society of America.js +562 -0
  1411. package/vendor/zotero/modules/translators/Optimization Online.js +418 -0
  1412. package/vendor/zotero/modules/translators/Ovid OCE.js +353 -0
  1413. package/vendor/zotero/modules/translators/Ovid.js +302 -0
  1414. package/vendor/zotero/modules/translators/Oxford Dictionaries Premium.js +194 -0
  1415. package/vendor/zotero/modules/translators/Oxford English Dictionary.js +122 -0
  1416. package/vendor/zotero/modules/translators/Oxford Music and Art Online.js +120 -0
  1417. package/vendor/zotero/modules/translators/Oxford Reference.js +202 -0
  1418. package/vendor/zotero/modules/translators/Oxford University Press.js +277 -0
  1419. package/vendor/zotero/modules/translators/PC Gamer.js +302 -0
  1420. package/vendor/zotero/modules/translators/PC Games.js +128 -0
  1421. package/vendor/zotero/modules/translators/PEI Archival Information Network.js +242 -0
  1422. package/vendor/zotero/modules/translators/PEP Web.js +165 -0
  1423. package/vendor/zotero/modules/translators/PKP Catalog Systems.js +1455 -0
  1424. package/vendor/zotero/modules/translators/PLoS Journals.js +284 -0
  1425. package/vendor/zotero/modules/translators/PRC History Review.js +298 -0
  1426. package/vendor/zotero/modules/translators/Pajhwok Afghan News.js +198 -0
  1427. package/vendor/zotero/modules/translators/Papers Past.js +297 -0
  1428. package/vendor/zotero/modules/translators/Paris Review.js +227 -0
  1429. package/vendor/zotero/modules/translators/Pastebin.js +199 -0
  1430. package/vendor/zotero/modules/translators/Patents - USPTO.js +210 -0
  1431. package/vendor/zotero/modules/translators/Peeters.js +587 -0
  1432. package/vendor/zotero/modules/translators/Perceiving Systems.js +350 -0
  1433. package/vendor/zotero/modules/translators/Perlego.js +416 -0
  1434. package/vendor/zotero/modules/translators/PhilPapers.js +233 -0
  1435. package/vendor/zotero/modules/translators/Philosopher's Imprint.js +169 -0
  1436. package/vendor/zotero/modules/translators/Pleade.js +329 -0
  1437. package/vendor/zotero/modules/translators/Polygon.js +270 -0
  1438. package/vendor/zotero/modules/translators/Potsdamer Neueste Nachrichten.js +188 -0
  1439. package/vendor/zotero/modules/translators/Premium Times.js +144 -0
  1440. package/vendor/zotero/modules/translators/Preprints.org.js +305 -0
  1441. package/vendor/zotero/modules/translators/Prime 9ja Online.js +289 -0
  1442. package/vendor/zotero/modules/translators/Primo 2018.js +126 -0
  1443. package/vendor/zotero/modules/translators/Primo Normalized XML.js +987 -0
  1444. package/vendor/zotero/modules/translators/Primo.js +322 -0
  1445. package/vendor/zotero/modules/translators/ProMED.js +66 -0
  1446. package/vendor/zotero/modules/translators/ProQuest Ebook Central.js +226 -0
  1447. package/vendor/zotero/modules/translators/ProQuest PolicyFile.js +115 -0
  1448. package/vendor/zotero/modules/translators/ProQuest.js +2210 -0
  1449. package/vendor/zotero/modules/translators/Probing the Past.js +151 -0
  1450. package/vendor/zotero/modules/translators/Project Gutenberg.js +191 -0
  1451. package/vendor/zotero/modules/translators/Project MUSE.js +451 -0
  1452. package/vendor/zotero/modules/translators/Protein Data Bank.js +445 -0
  1453. package/vendor/zotero/modules/translators/PubFactory Journals.js +712 -0
  1454. package/vendor/zotero/modules/translators/PubMed Central.js +577 -0
  1455. package/vendor/zotero/modules/translators/PubMed XML.js +1139 -0
  1456. package/vendor/zotero/modules/translators/PubMed.js +1317 -0
  1457. package/vendor/zotero/modules/translators/PubPub.js +530 -0
  1458. package/vendor/zotero/modules/translators/Pubget.js +164 -0
  1459. package/vendor/zotero/modules/translators/Public Record Office Victoria.js +252 -0
  1460. package/vendor/zotero/modules/translators/Publications Office of the European Union.js +540 -0
  1461. package/vendor/zotero/modules/translators/Publications du Quebec.js +225 -0
  1462. package/vendor/zotero/modules/translators/PyPI.js +288 -0
  1463. package/vendor/zotero/modules/translators/Qatar Digital Library.js +802 -0
  1464. package/vendor/zotero/modules/translators/Queensland State Archives.js +303 -0
  1465. package/vendor/zotero/modules/translators/R-Packages.js +410 -0
  1466. package/vendor/zotero/modules/translators/RAND.js +283 -0
  1467. package/vendor/zotero/modules/translators/RDF.js +1780 -0
  1468. package/vendor/zotero/modules/translators/REDALYC.js +196 -0
  1469. package/vendor/zotero/modules/translators/RIS.js +7265 -0
  1470. package/vendor/zotero/modules/translators/RSC Publishing.js +250 -0
  1471. package/vendor/zotero/modules/translators/Radio Free Europe Radio Liberty.js +706 -0
  1472. package/vendor/zotero/modules/translators/RePEc - Econpapers.js +725 -0
  1473. package/vendor/zotero/modules/translators/RePEc - IDEAS.js +337 -0
  1474. package/vendor/zotero/modules/translators/Rechtspraak.nl.js +295 -0
  1475. package/vendor/zotero/modules/translators/RefWorks Tagged.js +1185 -0
  1476. package/vendor/zotero/modules/translators/ReferBibIX.js +463 -0
  1477. package/vendor/zotero/modules/translators/Regeringskansliet.js +89 -0
  1478. package/vendor/zotero/modules/translators/Research Square.js +335 -0
  1479. package/vendor/zotero/modules/translators/ResearchGate.js +394 -0
  1480. package/vendor/zotero/modules/translators/Retsinformation.js +348 -0
  1481. package/vendor/zotero/modules/translators/Reuters.js +255 -0
  1482. package/vendor/zotero/modules/translators/Revues.org.js +378 -0
  1483. package/vendor/zotero/modules/translators/Rock, Paper, Shotgun.js +304 -0
  1484. package/vendor/zotero/modules/translators/Roll Call.js +173 -0
  1485. package/vendor/zotero/modules/translators/Russian State Library.js +1517 -0
  1486. package/vendor/zotero/modules/translators/SAE Papers.js +241 -0
  1487. package/vendor/zotero/modules/translators/SAGE Journals.js +427 -0
  1488. package/vendor/zotero/modules/translators/SAGE Knowledge.js +505 -0
  1489. package/vendor/zotero/modules/translators/SAILDART.js +364 -0
  1490. package/vendor/zotero/modules/translators/SALT Research Archives.js +196 -0
  1491. package/vendor/zotero/modules/translators/SFU IPinCH.js +946 -0
  1492. package/vendor/zotero/modules/translators/SIPRI.js +256 -0
  1493. package/vendor/zotero/modules/translators/SIRS Knowledge Source.js +546 -0
  1494. package/vendor/zotero/modules/translators/SLUB Dresden.js +300 -0
  1495. package/vendor/zotero/modules/translators/SORA.js +340 -0
  1496. package/vendor/zotero/modules/translators/SSOAR.js +399 -0
  1497. package/vendor/zotero/modules/translators/SSRN.js +235 -0
  1498. package/vendor/zotero/modules/translators/SVT Nyheter.js +381 -0
  1499. package/vendor/zotero/modules/translators/Sacramento Bee.js +274 -0
  1500. package/vendor/zotero/modules/translators/Safari Books Online.js +391 -0
  1501. package/vendor/zotero/modules/translators/Scholars Portal Journals.js +220 -0
  1502. package/vendor/zotero/modules/translators/Scholia.js +189 -0
  1503. package/vendor/zotero/modules/translators/Schweizer Radio und Fernsehen SRF.js +152 -0
  1504. package/vendor/zotero/modules/translators/SciELO.js +440 -0
  1505. package/vendor/zotero/modules/translators/ScienceDirect.js +1304 -0
  1506. package/vendor/zotero/modules/translators/Scopus.js +418 -0
  1507. package/vendor/zotero/modules/translators/Semantic Scholar.js +513 -0
  1508. package/vendor/zotero/modules/translators/Silverchair.js +886 -0
  1509. package/vendor/zotero/modules/translators/Slate.js +228 -0
  1510. package/vendor/zotero/modules/translators/SlideShare.js +112 -0
  1511. package/vendor/zotero/modules/translators/Spiegel Online.js +343 -0
  1512. package/vendor/zotero/modules/translators/Springer Link.js +696 -0
  1513. package/vendor/zotero/modules/translators/Stack Exchange.js +134 -0
  1514. package/vendor/zotero/modules/translators/Standard Ebooks.js +198 -0
  1515. package/vendor/zotero/modules/translators/Stanford Encyclopedia of Philosophy.js +285 -0
  1516. package/vendor/zotero/modules/translators/Stanford University Press.js +343 -0
  1517. package/vendor/zotero/modules/translators/State Records Office of Western Australia.js +439 -0
  1518. package/vendor/zotero/modules/translators/Stitcher.js +141 -0
  1519. package/vendor/zotero/modules/translators/Store norske leksikon.js +291 -0
  1520. package/vendor/zotero/modules/translators/Stuff.co.nz.js +446 -0
  1521. package/vendor/zotero/modules/translators/Substack.js +237 -0
  1522. package/vendor/zotero/modules/translators/Sud Ouest.js +181 -0
  1523. package/vendor/zotero/modules/translators/Sueddeutsche.de.js +205 -0
  1524. package/vendor/zotero/modules/translators/Summon 2.js +117 -0
  1525. package/vendor/zotero/modules/translators/Superlib.js +647 -0
  1526. package/vendor/zotero/modules/translators/Svenska Dagbladet.js +274 -0
  1527. package/vendor/zotero/modules/translators/Sveriges radio.js +416 -0
  1528. package/vendor/zotero/modules/translators/TEI.js +648 -0
  1529. package/vendor/zotero/modules/translators/TV by the Numbers.js +194 -0
  1530. package/vendor/zotero/modules/translators/TVNZ.js +165 -0
  1531. package/vendor/zotero/modules/translators/Tagesspiegel.js +249 -0
  1532. package/vendor/zotero/modules/translators/Talis Aspire.js +316 -0
  1533. package/vendor/zotero/modules/translators/TalisPrism.js +445 -0
  1534. package/vendor/zotero/modules/translators/Tatknigafund.js +174 -0
  1535. package/vendor/zotero/modules/translators/Tatpressa.ru.js +121 -0
  1536. package/vendor/zotero/modules/translators/Taylor & Francis eBooks.js +334 -0
  1537. package/vendor/zotero/modules/translators/Taylor and Francis+NEJM.js +813 -0
  1538. package/vendor/zotero/modules/translators/Tesis Doctorals en Xarxa.js +233 -0
  1539. package/vendor/zotero/modules/translators/The Art Newspaper.js +268 -0
  1540. package/vendor/zotero/modules/translators/The Atlantic.js +535 -0
  1541. package/vendor/zotero/modules/translators/The Australian.js +105 -0
  1542. package/vendor/zotero/modules/translators/The Boston Globe.js +285 -0
  1543. package/vendor/zotero/modules/translators/The Chronicle of Higher Education.js +308 -0
  1544. package/vendor/zotero/modules/translators/The Daily Beast.js +183 -0
  1545. package/vendor/zotero/modules/translators/The Economic Times - The Times of India.js +305 -0
  1546. package/vendor/zotero/modules/translators/The Economic Times.js +191 -0
  1547. package/vendor/zotero/modules/translators/The Economist.js +207 -0
  1548. package/vendor/zotero/modules/translators/The Free Dictionary.js +58 -0
  1549. package/vendor/zotero/modules/translators/The Globe and Mail.js +241 -0
  1550. package/vendor/zotero/modules/translators/The Guardian.js +717 -0
  1551. package/vendor/zotero/modules/translators/The Hamilton Spectator.js +129 -0
  1552. package/vendor/zotero/modules/translators/The Hindu (old).js +128 -0
  1553. package/vendor/zotero/modules/translators/The Hindu.js +318 -0
  1554. package/vendor/zotero/modules/translators/The Independent.js +184 -0
  1555. package/vendor/zotero/modules/translators/The Intercept.js +258 -0
  1556. package/vendor/zotero/modules/translators/The Met.js +300 -0
  1557. package/vendor/zotero/modules/translators/The Microfinance Gateway.js +174 -0
  1558. package/vendor/zotero/modules/translators/The Nation.js +188 -0
  1559. package/vendor/zotero/modules/translators/The National Archives (UK).js +242 -0
  1560. package/vendor/zotero/modules/translators/The New Republic.js +160 -0
  1561. package/vendor/zotero/modules/translators/The New York Review of Books.js +467 -0
  1562. package/vendor/zotero/modules/translators/The New Yorker.js +360 -0
  1563. package/vendor/zotero/modules/translators/The Open Library.js +220 -0
  1564. package/vendor/zotero/modules/translators/The Straits Times.js +528 -0
  1565. package/vendor/zotero/modules/translators/The Telegraph.js +337 -0
  1566. package/vendor/zotero/modules/translators/The Times and Sunday Times.js +180 -0
  1567. package/vendor/zotero/modules/translators/The Times of Israel.js +151 -0
  1568. package/vendor/zotero/modules/translators/TheMarker.js +108 -0
  1569. package/vendor/zotero/modules/translators/Theory of Computing.js +488 -0
  1570. package/vendor/zotero/modules/translators/ThesesFR.js +350 -0
  1571. package/vendor/zotero/modules/translators/Thieme.js +326 -0
  1572. package/vendor/zotero/modules/translators/Time.com.js +557 -0
  1573. package/vendor/zotero/modules/translators/TimesMachine.js +222 -0
  1574. package/vendor/zotero/modules/translators/Tony Blair Institute for Global Change.js +195 -0
  1575. package/vendor/zotero/modules/translators/Toronto Star.js +331 -0
  1576. package/vendor/zotero/modules/translators/Transportation Research Board.js +253 -0
  1577. package/vendor/zotero/modules/translators/Treesearch.js +535 -0
  1578. package/vendor/zotero/modules/translators/Trove.js +616 -0
  1579. package/vendor/zotero/modules/translators/Tumblr.js +179 -0
  1580. package/vendor/zotero/modules/translators/Twitter.js +413 -0
  1581. package/vendor/zotero/modules/translators/UChicago VuFind.js +907 -0
  1582. package/vendor/zotero/modules/translators/UNZ Print Archive.js +276 -0
  1583. package/vendor/zotero/modules/translators/UPCommons.js +227 -0
  1584. package/vendor/zotero/modules/translators/US National Archives Research Catalog.js +275 -0
  1585. package/vendor/zotero/modules/translators/USENIX.js +151 -0
  1586. package/vendor/zotero/modules/translators/USgamer.js +120 -0
  1587. package/vendor/zotero/modules/translators/Ubiquity Journals.js +228 -0
  1588. package/vendor/zotero/modules/translators/University Press Scholarship.js +301 -0
  1589. package/vendor/zotero/modules/translators/University of California Press Books.js +403 -0
  1590. package/vendor/zotero/modules/translators/University of Chicago Press Books.js +378 -0
  1591. package/vendor/zotero/modules/translators/University of Wisconsin-Madison Libraries Catalog.js +298 -0
  1592. package/vendor/zotero/modules/translators/Unqualified Dublin Core RDF.js +153 -0
  1593. package/vendor/zotero/modules/translators/UpToDate References.js +203 -0
  1594. package/vendor/zotero/modules/translators/Vanity Fair.js +242 -0
  1595. package/vendor/zotero/modules/translators/Verniana-Jules Verne Studies.js +190 -0
  1596. package/vendor/zotero/modules/translators/Verso Books.js +311 -0
  1597. package/vendor/zotero/modules/translators/Vice.js +445 -0
  1598. package/vendor/zotero/modules/translators/Victoria & Albert Museum.js +277 -0
  1599. package/vendor/zotero/modules/translators/Vimeo.js +188 -0
  1600. package/vendor/zotero/modules/translators/VoxEU.js +151 -0
  1601. package/vendor/zotero/modules/translators/WHO.js +458 -0
  1602. package/vendor/zotero/modules/translators/WIPO.js +189 -0
  1603. package/vendor/zotero/modules/translators/Wall Street Journal.js +525 -0
  1604. package/vendor/zotero/modules/translators/Wanfang Data.js +310 -0
  1605. package/vendor/zotero/modules/translators/Washington Monthly.js +195 -0
  1606. package/vendor/zotero/modules/translators/Washington Post.js +447 -0
  1607. package/vendor/zotero/modules/translators/Web of Science Nextgen.js +832 -0
  1608. package/vendor/zotero/modules/translators/Web of Science Tagged.js +1247 -0
  1609. package/vendor/zotero/modules/translators/Web of Science.js +228 -0
  1610. package/vendor/zotero/modules/translators/Welt Online.js +161 -0
  1611. package/vendor/zotero/modules/translators/WestLaw UK.js +190 -0
  1612. package/vendor/zotero/modules/translators/WikiLeaks PlusD.js +182 -0
  1613. package/vendor/zotero/modules/translators/Wikidata QuickStatements.js +324 -0
  1614. package/vendor/zotero/modules/translators/Wikidata.js +911 -0
  1615. package/vendor/zotero/modules/translators/Wikimedia Commons.js +285 -0
  1616. package/vendor/zotero/modules/translators/Wikipedia Citation Templates.js +432 -0
  1617. package/vendor/zotero/modules/translators/Wikipedia.js +385 -0
  1618. package/vendor/zotero/modules/translators/Wikisource.js +399 -0
  1619. package/vendor/zotero/modules/translators/Wikiwand.js +122 -0
  1620. package/vendor/zotero/modules/translators/Wiktionary.js +141 -0
  1621. package/vendor/zotero/modules/translators/Wildlife Biology in Practice.js +172 -0
  1622. package/vendor/zotero/modules/translators/Wiley Online Library.js +1412 -0
  1623. package/vendor/zotero/modules/translators/Wilson Center Digital Archive.js +321 -0
  1624. package/vendor/zotero/modules/translators/Winnipeg Free Press.js +204 -0
  1625. package/vendor/zotero/modules/translators/Wired.js +249 -0
  1626. package/vendor/zotero/modules/translators/Womennews.js +230 -0
  1627. package/vendor/zotero/modules/translators/World Digital Library.js +403 -0
  1628. package/vendor/zotero/modules/translators/World History Connected.js +185 -0
  1629. package/vendor/zotero/modules/translators/World Shakespeare Bibliography Online.js +595 -0
  1630. package/vendor/zotero/modules/translators/XML ContextObject.js +274 -0
  1631. package/vendor/zotero/modules/translators/YPSF.js +224 -0
  1632. package/vendor/zotero/modules/translators/Yandex Books.js +265 -0
  1633. package/vendor/zotero/modules/translators/Ynet.js +171 -0
  1634. package/vendor/zotero/modules/translators/YouTube.js +209 -0
  1635. package/vendor/zotero/modules/translators/ZIPonline.js +267 -0
  1636. package/vendor/zotero/modules/translators/ZOBODAT.js +341 -0
  1637. package/vendor/zotero/modules/translators/Zotero RDF.js +583 -0
  1638. package/vendor/zotero/modules/translators/ZoteroBib.js +94 -0
  1639. package/vendor/zotero/modules/translators/arXiv.org.js +1099 -0
  1640. package/vendor/zotero/modules/translators/artnet.js +207 -0
  1641. package/vendor/zotero/modules/translators/beck-online.js +1339 -0
  1642. package/vendor/zotero/modules/translators/clinicaltrials.gov.js +331 -0
  1643. package/vendor/zotero/modules/translators/dLibra.js +367 -0
  1644. package/vendor/zotero/modules/translators/dejure.org.js +242 -0
  1645. package/vendor/zotero/modules/translators/dhistory.js +103 -0
  1646. package/vendor/zotero/modules/translators/digibib.net.js +245 -0
  1647. package/vendor/zotero/modules/translators/eLibrary.ru.js +867 -0
  1648. package/vendor/zotero/modules/translators/eLife.js +462 -0
  1649. package/vendor/zotero/modules/translators/eMJA.js +246 -0
  1650. package/vendor/zotero/modules/translators/eMedicine.js +123 -0
  1651. package/vendor/zotero/modules/translators/ePrint IACR.js +608 -0
  1652. package/vendor/zotero/modules/translators/ebrary.js +140 -0
  1653. package/vendor/zotero/modules/translators/etatar.ru.js +104 -0
  1654. package/vendor/zotero/modules/translators/feb-web.ru.js +167 -0
  1655. package/vendor/zotero/modules/translators/fishpond.co.nz.js +224 -0
  1656. package/vendor/zotero/modules/translators/fr-online.de.js +209 -0
  1657. package/vendor/zotero/modules/translators/govinfo.js +189 -0
  1658. package/vendor/zotero/modules/translators/informIT database.js +248 -0
  1659. package/vendor/zotero/modules/translators/io-port.js +68 -0
  1660. package/vendor/zotero/modules/translators/jmlr.js +299 -0
  1661. package/vendor/zotero/modules/translators/jurion.js +377 -0
  1662. package/vendor/zotero/modules/translators/mEDRA.js +597 -0
  1663. package/vendor/zotero/modules/translators/magazines.russ.ru.js +120 -0
  1664. package/vendor/zotero/modules/translators/medes.js +284 -0
  1665. package/vendor/zotero/modules/translators/newshub.co.nz.js +206 -0
  1666. package/vendor/zotero/modules/translators/newspapers.com.js +335 -0
  1667. package/vendor/zotero/modules/translators/openJur.js +161 -0
  1668. package/vendor/zotero/modules/translators/reddit.js +275 -0
  1669. package/vendor/zotero/modules/translators/sbn.it.js +186 -0
  1670. package/vendor/zotero/modules/translators/scinapse.js +273 -0
  1671. package/vendor/zotero/modules/translators/semantics Visual Library.js +643 -0
  1672. package/vendor/zotero/modules/translators/taz.de.js +200 -0
  1673. package/vendor/zotero/modules/translators/unAPI.js +481 -0
  1674. package/vendor/zotero/modules/translators/washingtonpost.com.js +257 -0
  1675. package/vendor/zotero/modules/translators/wiso.js +314 -0
  1676. package/vendor/zotero/modules/translators/zbMATH.js +278 -0
  1677. package/vendor/zotero/modules/translators/zotero.org.js +311 -0
  1678. package/vendor/zotero/modules/utilities/README.md +25 -0
  1679. package/vendor/zotero/modules/utilities/cachedTypes.js +193 -0
  1680. package/vendor/zotero/modules/utilities/date.js +958 -0
  1681. package/vendor/zotero/modules/utilities/openurl.js +458 -0
  1682. package/vendor/zotero/modules/utilities/resource/README.md +4 -0
  1683. package/vendor/zotero/modules/utilities/resource/dateFormats.json +1322 -0
  1684. package/vendor/zotero/modules/utilities/resource/schema/global/README.md +15 -0
  1685. package/vendor/zotero/modules/utilities/resource/schema/global/schema.json +12072 -0
  1686. package/vendor/zotero/modules/utilities/resource/zoteroTypeSchemaData.js +5 -0
  1687. package/vendor/zotero/modules/utilities/schema.js +65 -0
  1688. package/vendor/zotero/modules/utilities/utilities.js +1841 -0
  1689. package/vendor/zotero/modules/utilities/utilities_item.js +1082 -0
  1690. package/vendor/zotero/modules/utilities/xregexp-all.js +4652 -0
  1691. package/vendor/zotero/modules/utilities/xregexp-unicode-zotero.js +58 -0
  1692. package/vendor/zotero/modules/zotero-schema/README.md +15 -0
  1693. package/vendor/zotero/modules/zotero-schema/schema.json +12072 -0
  1694. package/vendor/zotero/package-lock.json +4332 -0
  1695. package/vendor/zotero/package.json +44 -0
  1696. package/vendor/zotero/spec.yaml +142 -0
  1697. package/vendor/zotero/src/args.js +43 -0
  1698. package/vendor/zotero/src/cors.js +28 -0
  1699. package/vendor/zotero/src/debug.js +189 -0
  1700. package/vendor/zotero/src/exportEndpoint.js +97 -0
  1701. package/vendor/zotero/src/formats.js +43 -0
  1702. package/vendor/zotero/src/http.js +478 -0
  1703. package/vendor/zotero/src/importEndpoint.js +54 -0
  1704. package/vendor/zotero/src/lambda.js +75 -0
  1705. package/vendor/zotero/src/proxy.js +356 -0
  1706. package/vendor/zotero/src/searchEndpoint.js +97 -0
  1707. package/vendor/zotero/src/server.js +85 -0
  1708. package/vendor/zotero/src/specEndpoint.js +135 -0
  1709. package/vendor/zotero/src/testEndpoint.js +50 -0
  1710. package/vendor/zotero/src/textSearch.js +527 -0
  1711. package/vendor/zotero/src/translation/sandboxManager.js +109 -0
  1712. package/vendor/zotero/src/translation/translate.js +101 -0
  1713. package/vendor/zotero/src/translation/translate_item.js +88 -0
  1714. package/vendor/zotero/src/translators.js +264 -0
  1715. package/vendor/zotero/src/utilities.js +49 -0
  1716. package/vendor/zotero/src/webEndpoint.js +113 -0
  1717. package/vendor/zotero/src/webSession.js +476 -0
  1718. package/vendor/zotero/src/zotero.js +117 -0
  1719. package/vendor/zotero/translate_export +10 -0
  1720. package/vendor/zotero/translate_search +9 -0
  1721. package/vendor/zotero/translate_url +9 -0
  1722. package/vendor/zotero/translate_url_multiple +9 -0
  1723. package/vendor/zotero/translate_url_single +9 -0
@@ -0,0 +1,5021 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+ /* eslint n/no-process-exit: "off" */
5
+
6
+ const fs = require( 'fs' );
7
+ const os = require( 'os' );
8
+ const path = require( 'path' );
9
+ const net = require( 'net' );
10
+ const http = require( 'http' );
11
+ const https = require( 'https' );
12
+ const readline = require( 'readline' );
13
+ const crypto = require( 'crypto' );
14
+ const { spawn, spawnSync } = require( 'child_process' );
15
+ const BBPromise = require( 'bluebird' );
16
+ const bunyan = require( 'bunyan' );
17
+ const CSL = require( 'citeproc' );
18
+ const yaml = require( 'js-yaml' );
19
+ const citoidApp = require( '../app.js' );
20
+
21
+ const rootDir = path.resolve( __dirname, '..' );
22
+ const zoteroDir = process.env.ZOTERO_DIR || path.join( rootDir, 'vendor', 'zotero' );
23
+ const cnTranslatorsDir = process.env.CN_TRANSLATORS_DIR || path.join( rootDir, 'vendor', 'translators_CN' );
24
+ const officialTranslatorsDir = process.env.OFFICIAL_TRANSLATORS_DIR ||
25
+ path.join( rootDir, 'vendor', 'translators-official' );
26
+ const vendoredStylesDir = path.join( rootDir, 'vendor', 'styles' );
27
+ const vendoredOfficialStylesDir = path.join( rootDir, 'vendor', 'styles-official' );
28
+ const localDir = path.join( rootDir, '.local' );
29
+ const logDir = path.join( localDir, 'logs' );
30
+ const stateDir = path.join( localDir, 'state' );
31
+ const mergedTranslatorsDir = process.env.LOCAL_TRANSLATORS_DIR ||
32
+ path.join( localDir, 'translators' );
33
+ const stylesRootDir = process.env.LOCAL_STYLES_DIR ||
34
+ path.join( localDir, 'styles' );
35
+ const cslDir = path.join( stylesRootDir, 'csl' );
36
+ const localeDir = path.join( stylesRootDir, 'locales' );
37
+ const defaultStyleSources = [ vendoredOfficialStylesDir, vendoredStylesDir ];
38
+ const defaultPdfFetchIntervalMs = parseInt( process.env.CITOID_LOCAL_FETCH_INTERVAL_MS || '800', 10 );
39
+ const defaultRequestTimeoutMs = parseInt( process.env.CITOID_LOCAL_FETCH_TIMEOUT_MS || '15000', 10 );
40
+ const defaultProbeBodyBytes = parseInt( process.env.CITOID_LOCAL_PROBE_BODY_BYTES || '1572864', 10 );
41
+ const defaultFetchConcurrency = parseInt( process.env.CITOID_LOCAL_FETCH_CONCURRENCY || '4', 10 );
42
+ const defaultBatchConcurrency = parseInt( process.env.CITOID_LOCAL_BATCH_CONCURRENCY || '4', 10 );
43
+ const defaultOpenUrlBase = process.env.OPENURL_BASE || '';
44
+ const defaultZoteroApiBase = process.env.ZOTERO_API_BASE || 'https://api.zotero.org';
45
+ const defaultWmfCitoidBase = process.env.WMF_CITOID_BASE || 'https://en.wikipedia.org/api/rest_v1/data/citation';
46
+ const defaultZoteroUserId = process.env.ZOTERO_USER_ID || '';
47
+ const defaultZoteroApiKey = process.env.ZOTERO_API_KEY || '';
48
+ const defaultZoteroLibraryType = process.env.ZOTERO_LIBRARY_TYPE || 'users';
49
+ const defaultZoteroLibraryId = process.env.ZOTERO_LIBRARY_ID || '';
50
+ const defaultS2ApiKey = process.env.S2_API_KEY || '';
51
+ const zoteroAuthPath = path.join( stateDir, 'zotero-auth.json' );
52
+ const cacheRootDir = process.env.LOCAL_CACHE_DIR || path.join( localDir, 'cache' );
53
+ const cacheMetaPath = path.join( cacheRootDir, 'cache-meta.json' );
54
+ const pdfCacheDir = path.join( cacheRootDir, 'pdfs' );
55
+ const defaultCacheTtlSec = parseInt( process.env.CITOID_LOCAL_CACHE_TTL_SEC || '86400', 10 );
56
+ let cacheMetaMemo = null;
57
+
58
+ BBPromise.onPossiblyUnhandledRejection( () => {
59
+ } );
60
+
61
+ function usage() {
62
+ console.error( 'usage:' );
63
+ console.error( ' citeclaw mcp' );
64
+ console.error( ' citeclaw setup' );
65
+ console.error( ' citeclaw citoid <format> <query>' );
66
+ console.error( ' citeclaw citoid formats' );
67
+ console.error( ' citeclaw citation <format> <query>' );
68
+ console.error( ' citeclaw crossref <doi|query>' );
69
+ console.error( ' citeclaw semantic-scholar <doi|arxiv|query>' );
70
+ console.error( ' citeclaw semantic-scholar api <path> [--method GET|POST] [--params <json>] [--body <json|@file>]' );
71
+ console.error( ' citeclaw semantic-scholar paper <paperId>' );
72
+ console.error( ' citeclaw semantic-scholar paper-search <query> [--limit <n>] [--offset <n>]' );
73
+ console.error( ' citeclaw semantic-scholar paper-search-bulk <query> [--token <token>]' );
74
+ console.error( ' citeclaw semantic-scholar paper-batch <id1,id2,...|@file>' );
75
+ console.error( ' citeclaw semantic-scholar author <authorId>' );
76
+ console.error( ' citeclaw semantic-scholar author-papers <authorId> [--limit <n>] [--offset <n>]' );
77
+ console.error( ' citeclaw semantic-scholar author-batch <id1,id2,...|@file>' );
78
+ console.error( ' citeclaw api [--headers] <path>' );
79
+ console.error( ' citeclaw cite [--headers] <format> <query>' );
80
+ console.error( ' citeclaw cite-pdf [--headers] [--debug-pdf] <pdf-path>' );
81
+ console.error( ' citeclaw fetch-pdf [--base <openurl-base>] [--out <file.pdf>] <doi|arxiv|url>' );
82
+ console.error( ' citeclaw openurl-resolve [--base <openurl-base>] <doi|arxiv|url>' );
83
+ console.error( ' citeclaw zotero <login|logout|whoami|query|dump|cite|add|delete|update|note|sync-cite|dedup|enrich|export|watch|templates|safe-mode> [...]' );
84
+ console.error( ' citeclaw batch --op <cite|cite-style|fetch-pdf|openurl-resolve> --in <file>' );
85
+ console.error( ' citeclaw styles sync [--repo <git-url>]' );
86
+ console.error( ' citeclaw cite-style [--plain] [--style <name-or-path>] [--locale zh-CN] <query>' );
87
+ console.error( ' citeclaw info' );
88
+ console.error( ' citeclaw spec' );
89
+ console.error( 'examples:' );
90
+ console.error( " citeclaw api --headers '/_info'" );
91
+ console.error( " citeclaw api '/?spec'" );
92
+ console.error( ' citeclaw cite bibtex 10.1145/3368089.3409741' );
93
+ console.error( ' citeclaw citoid bibtex 10.1145/3368089.3409741' );
94
+ console.error( ' citeclaw citoid formats' );
95
+ console.error( ' citeclaw crossref 10.1021/acsomega.2c05310' );
96
+ console.error( ' citeclaw semantic-scholar 10.1021/acsomega.2c05310' );
97
+ console.error( ' citeclaw semantic-scholar paper-search "transformer attention" --limit 5' );
98
+ console.error( ' citeclaw cite mediawiki https://arxiv.org/abs/1706.03762' );
99
+ console.error( ' citeclaw cite-pdf ./paper.pdf' );
100
+ console.error( ' citeclaw cite-pdf --json --debug-pdf ./paper.pdf' );
101
+ console.error( ' citeclaw fetch-pdf 10.1038/s41586-020-2649-2' );
102
+ console.error( ' citeclaw fetch-pdf 1706.03762 --out ./attention.pdf' );
103
+ console.error( " citeclaw openurl-resolve --base 'https://example.edu/openurl' 10.1038/s41586-020-2649-2" );
104
+ console.error( ' citeclaw zotero login --user-id 123456 --api-key xxxx' );
105
+ console.error( ' citeclaw zotero whoami' );
106
+ console.error( " citeclaw zotero query 'transformer'" );
107
+ console.error( ' citeclaw zotero cite AB12CD34' );
108
+ console.error( ' citeclaw batch --op cite --format bibtex --in ./ids.txt --out-jsonl ./result.jsonl' );
109
+ console.error( 'options:' );
110
+ console.error( ' --concurrency <n> batch worker count (default: 4)' );
111
+ console.error( ' --user-id <id> Zotero user id (or set ZOTERO_USER_ID)' );
112
+ console.error( ' --api-key <key> Zotero API key (or set ZOTERO_API_KEY)' );
113
+ console.error( ' --library-type users|groups (default: users)' );
114
+ console.error( ' --library-id <id> Zotero library id (group id for groups)' );
115
+ console.error( ' --s2-api-key <k> Semantic Scholar API key (optional for higher limits)' );
116
+ console.error( ' --parent <ref> parent item key/url (zotero note search)' );
117
+ console.error( ' --limit <n> Zotero query/dump limit (1-100)' );
118
+ console.error( ' -y, --yes skip interactive confirmation (delete)' );
119
+ console.error( ' --profile print timing diagnostics to stderr' );
120
+ console.error( ' citeclaw styles sync' );
121
+ console.error( " citeclaw cite-style --locale zh-CN '10.1145/3368089.3409741'" );
122
+ console.error( " citeclaw cite-style --plain --locale zh-CN '10.1145/3368089.3409741'" );
123
+ }
124
+
125
+ function usageZotero( subAction = '' ) {
126
+ const action = String( subAction || '' ).trim().toLowerCase();
127
+ if ( action === 'login' ) {
128
+ console.error( 'usage:' );
129
+ console.error( ' citeclaw zotero login --user-id <id> --api-key <key> [--zotero-api-base <url>]' );
130
+ console.error( ' citeclaw zotero login --library-type groups --library-id <group-id> --api-key <key>' );
131
+ console.error( 'notes:' );
132
+ console.error( ' - personal library defaults to library-type users' );
133
+ console.error( ' - credentials are saved to .local/state/zotero-auth.json' );
134
+ console.error( ' - API key can be created at https://www.zotero.org/settings/keys' );
135
+ return;
136
+ }
137
+ if ( action === 'logout' ) {
138
+ console.error( 'usage:' );
139
+ console.error( ' citeclaw zotero logout' );
140
+ console.error( 'notes:' );
141
+ console.error( ' - removes local credentials from .local/state/zotero-auth.json' );
142
+ return;
143
+ }
144
+ if ( action === 'whoami' ) {
145
+ console.error( 'usage:' );
146
+ console.error( ' citeclaw zotero whoami [--api-key <key>] [--zotero-api-base <url>]' );
147
+ console.error( 'notes:' );
148
+ console.error( ' - reads account identity from /keys/current' );
149
+ console.error( ' - useful for discovering user-id from an API key' );
150
+ return;
151
+ }
152
+ if ( action === 'query' ) {
153
+ console.error( 'usage:' );
154
+ console.error( " citeclaw zotero query <text> [--limit <1-100>]" );
155
+ return;
156
+ }
157
+ if ( action === 'dump' ) {
158
+ console.error( 'usage:' );
159
+ console.error( ' citeclaw zotero dump [--limit <1-100>]' );
160
+ return;
161
+ }
162
+ if ( action === 'cite' ) {
163
+ console.error( 'usage:' );
164
+ console.error( ' citeclaw zotero cite <item-key|zotero-url>' );
165
+ console.error( 'examples:' );
166
+ console.error( ' citeclaw zotero cite AB12CD34' );
167
+ console.error( ' citeclaw zotero cite https://www.zotero.org/users/123/items/AB12CD34' );
168
+ return;
169
+ }
170
+ if ( action === 'add' ) {
171
+ console.error( 'usage:' );
172
+ console.error( " citeclaw zotero add '<json>'" );
173
+ console.error( ' citeclaw zotero add @./item.json' );
174
+ console.error( 'notes:' );
175
+ console.error( ' - strict sanity checks are applied before write' );
176
+ console.error( ' - payload must include itemType' );
177
+ return;
178
+ }
179
+ if ( action === 'delete' ) {
180
+ console.error( 'usage:' );
181
+ console.error( ' citeclaw zotero delete <item-key|zotero-url>' );
182
+ console.error( ' citeclaw zotero delete -y <item-key|zotero-url>' );
183
+ console.error( 'notes:' );
184
+ console.error( ' - uses item version precondition to prevent stale delete' );
185
+ console.error( ' - default requires interactive confirmation: type \"yes\" to continue' );
186
+ return;
187
+ }
188
+ if ( action === 'update' ) {
189
+ console.error( 'usage:' );
190
+ console.error( " citeclaw zotero update <item-key|zotero-url> '<json-patch>'" );
191
+ console.error( ' citeclaw zotero update <item-key|zotero-url> @./patch.json' );
192
+ console.error( 'notes:' );
193
+ console.error( ' - strict sanity checks are applied before write' );
194
+ console.error( ' - key/version/library fields are rejected in patch payload' );
195
+ return;
196
+ }
197
+ if ( action === 'note' ) {
198
+ console.error( 'usage:' );
199
+ console.error( " citeclaw zotero note add <parent-item-key|zotero-url> '<note-html-or-text>'" );
200
+ console.error( ' citeclaw zotero note add <parent-item-key|zotero-url> @./note.html' );
201
+ console.error( ' citeclaw zotero note list <parent-item-key|zotero-url> [--limit <1-100>]' );
202
+ console.error( " citeclaw zotero note search <text> [--limit <1-100>] [--parent <item-key|zotero-url>]" );
203
+ console.error( " citeclaw zotero note cite-links [<text-filter>] [--apply] [--parent <item-key|zotero-url>]" );
204
+ console.error( " citeclaw zotero note update <note-key|zotero-url> '<note-html-or-text>'" );
205
+ console.error( ' citeclaw zotero note update <note-key|zotero-url> @./note.html' );
206
+ console.error( ' citeclaw zotero note delete <note-key|zotero-url>' );
207
+ console.error( ' citeclaw zotero note delete -y <note-key|zotero-url>' );
208
+ console.error( 'notes:' );
209
+ console.error( ' - add/update run strict sanity checks on note content' );
210
+ console.error( ' - delete requires yes/no confirmation unless -y is provided' );
211
+ return;
212
+ }
213
+ if ( action === 'sync-cite' ) {
214
+ console.error( 'usage:' );
215
+ console.error( ' citeclaw zotero sync-cite [--limit <n>] [--apply] [--dry-run]' );
216
+ return;
217
+ }
218
+ if ( action === 'dedup' ) {
219
+ console.error( 'usage:' );
220
+ console.error( ' citeclaw zotero dedup [--limit <n>]' );
221
+ return;
222
+ }
223
+ if ( action === 'enrich' ) {
224
+ console.error( 'usage:' );
225
+ console.error( ' citeclaw zotero enrich [--limit <n>] [--apply] [--dry-run]' );
226
+ return;
227
+ }
228
+ if ( action === 'export' ) {
229
+ console.error( 'usage:' );
230
+ console.error( ' citeclaw zotero export md [--out <file.md>] [--limit <n>]' );
231
+ return;
232
+ }
233
+ if ( action === 'watch' ) {
234
+ console.error( 'usage:' );
235
+ console.error( " citeclaw zotero watch <query> [--interval <sec>] [--out-bib <file.bib>] [--limit <n>]" );
236
+ return;
237
+ }
238
+ if ( action === 'templates' ) {
239
+ console.error( 'usage:' );
240
+ console.error( ' citeclaw zotero templates [paper|book|webpage] [--apply]' );
241
+ return;
242
+ }
243
+ if ( action === 'safe-mode' ) {
244
+ console.error( 'usage:' );
245
+ console.error( ' citeclaw zotero safe-mode <on|off|status>' );
246
+ return;
247
+ }
248
+ console.error( 'usage:' );
249
+ console.error( ' citeclaw zotero <login|logout|whoami|query|dump|cite|add|delete|update|note|sync-cite|dedup|enrich|export|watch|templates|safe-mode> [...]' );
250
+ console.error( 'commands:' );
251
+ console.error( ' login save Zotero API credentials locally' );
252
+ console.error( ' logout clear saved credentials' );
253
+ console.error( ' whoami show account identity from current API key' );
254
+ console.error( ' query full-text search in configured library' );
255
+ console.error( ' dump list library items as JSON' );
256
+ console.error( ' cite fetch BibTeX for one item' );
257
+ console.error( ' add add one item with strict sanity checks' );
258
+ console.error( ' delete delete one item with version precondition' );
259
+ console.error( ' update update one item with strict sanity checks' );
260
+ console.error( ' note manage child notes (add/list/update/delete)' );
261
+ console.error( ' sync-cite fill missing Citation Key in extra field' );
262
+ console.error( ' dedup suggest duplicate items by DOI/title+year fingerprint' );
263
+ console.error( ' enrich fill missing fields from citoid metadata' );
264
+ console.error( ' export export library data (currently md)' );
265
+ console.error( ' watch poll query and append new cites to bib file' );
266
+ console.error( ' templates show/apply item templates' );
267
+ console.error( ' safe-mode persistent dry-run guardrail for write ops' );
268
+ console.error( 'examples:' );
269
+ console.error( ' citeclaw zotero login --user-id 123456 --api-key xxxx' );
270
+ console.error( ' citeclaw zotero whoami' );
271
+ console.error( " citeclaw zotero query 'transformer'" );
272
+ console.error( ' citeclaw zotero dump --limit 10' );
273
+ console.error( ' citeclaw zotero cite AB12CD34' );
274
+ console.error( " citeclaw zotero add '{\"itemType\":\"journalArticle\",\"title\":\"Demo\"}'" );
275
+ console.error( " citeclaw zotero note add AB12CD34 '<p>Important note</p>'" );
276
+ console.error( ' citeclaw zotero note list AB12CD34' );
277
+ console.error( " citeclaw zotero note search 'transformer'" );
278
+ console.error( " citeclaw zotero note cite-links 'doi' --apply" );
279
+ console.error( ' citeclaw zotero sync-cite --apply' );
280
+ console.error( ' citeclaw zotero dedup' );
281
+ console.error( ' citeclaw zotero enrich --apply' );
282
+ console.error( ' citeclaw zotero export md --out ./library.md' );
283
+ console.error( " citeclaw zotero watch 'transformer' --out-bib ./watch.bib --interval 60" );
284
+ console.error( ' citeclaw zotero templates paper' );
285
+ console.error( ' citeclaw zotero safe-mode on' );
286
+ console.error( " citeclaw zotero note search 'transformer'" );
287
+ console.error( " citeclaw zotero note search 'transformer' --parent AB12CD34" );
288
+ console.error( ' citeclaw zotero delete AB12CD34' );
289
+ console.error( " citeclaw zotero update AB12CD34 '{\"title\":\"New title\"}'" );
290
+ console.error( 'help:' );
291
+ console.error( ' citeclaw zotero login --help' );
292
+ }
293
+
294
+ function ensureDirs() {
295
+ fs.mkdirSync( logDir, { recursive: true } );
296
+ fs.mkdirSync( stateDir, { recursive: true } );
297
+ fs.mkdirSync( stylesRootDir, { recursive: true } );
298
+ fs.mkdirSync( cacheRootDir, { recursive: true } );
299
+ fs.mkdirSync( pdfCacheDir, { recursive: true } );
300
+ }
301
+
302
+ function fileExists( filePath ) {
303
+ try {
304
+ fs.accessSync( filePath );
305
+ return true;
306
+ } catch ( error ) {
307
+ return false;
308
+ }
309
+ }
310
+
311
+ function commandExists( command ) {
312
+ const args = process.platform === 'win32' ? [ '/c', 'where', command ] : [ '-lc', `command -v ${ command }` ];
313
+ const found = spawnSync( process.platform === 'win32' ? 'cmd' : 'bash', args, {
314
+ stdio: 'pipe',
315
+ encoding: 'utf8'
316
+ } );
317
+ return found.status === 0;
318
+ }
319
+
320
+ function stableStringify( value ) {
321
+ if ( value === null || typeof value !== 'object' ) {
322
+ return JSON.stringify( value );
323
+ }
324
+ if ( Array.isArray( value ) ) {
325
+ return `[${ value.map( stableStringify ).join( ',' ) }]`;
326
+ }
327
+ const keys = Object.keys( value ).sort();
328
+ return `{${ keys.map( ( key ) => `${ JSON.stringify( key ) }:${ stableStringify( value[ key ] ) }` ).join( ',' ) }`;
329
+ }
330
+
331
+ function makeCacheKey( namespace, payload ) {
332
+ const raw = `${ namespace }|${ stableStringify( payload ) }`;
333
+ return crypto.createHash( 'sha1' ).update( raw ).digest( 'hex' );
334
+ }
335
+
336
+ function loadCacheMeta() {
337
+ if ( cacheMetaMemo ) {
338
+ return cacheMetaMemo;
339
+ }
340
+ if ( !fileExists( cacheMetaPath ) ) {
341
+ cacheMetaMemo = {};
342
+ return cacheMetaMemo;
343
+ }
344
+ try {
345
+ const text = fs.readFileSync( cacheMetaPath, 'utf8' );
346
+ const parsed = JSON.parse( text );
347
+ cacheMetaMemo = parsed && typeof parsed === 'object' ? parsed : {};
348
+ return cacheMetaMemo;
349
+ } catch ( error ) {
350
+ cacheMetaMemo = {};
351
+ return cacheMetaMemo;
352
+ }
353
+ }
354
+
355
+ function saveCacheMeta( meta ) {
356
+ cacheMetaMemo = meta;
357
+ fs.writeFileSync( cacheMetaPath, `${ JSON.stringify( meta ) }\n` );
358
+ }
359
+
360
+ function profileLog( options, stage, startedAt, extra = '' ) {
361
+ if ( options && options.profile ) {
362
+ const suffix = extra ? ` ${ extra }` : '';
363
+ console.error( `[profile] ${ stage } ${ Date.now() - startedAt }ms${ suffix }` );
364
+ }
365
+ }
366
+
367
+ function getCachedValue( cacheKey ) {
368
+ const meta = loadCacheMeta();
369
+ const entry = meta[ cacheKey ];
370
+ if ( !entry || typeof entry !== 'object' ) {
371
+ return null;
372
+ }
373
+ if ( entry.expiresAt && Date.now() > entry.expiresAt ) {
374
+ delete meta[ cacheKey ];
375
+ saveCacheMeta( meta );
376
+ return null;
377
+ }
378
+ return entry.value;
379
+ }
380
+
381
+ function setCachedValue( cacheKey, value, ttlSec ) {
382
+ const ttlMs = Math.max( 0, ( Number.isFinite( ttlSec ) ? ttlSec : defaultCacheTtlSec ) * 1000 );
383
+ const meta = loadCacheMeta();
384
+ meta[ cacheKey ] = {
385
+ expiresAt: Date.now() + ttlMs,
386
+ value
387
+ };
388
+ saveCacheMeta( meta );
389
+ }
390
+
391
+ async function readThroughCache( namespace, payload, ttlSec, compute ) {
392
+ const cacheKey = makeCacheKey( namespace, payload );
393
+ const cached = getCachedValue( cacheKey );
394
+ if ( cached !== null ) {
395
+ return { value: cached, cacheHit: true };
396
+ }
397
+ const value = await compute();
398
+ setCachedValue( cacheKey, value, ttlSec );
399
+ return { value, cacheHit: false };
400
+ }
401
+
402
+ function jsonOut( value ) {
403
+ process.stdout.write( `${ JSON.stringify( value, null, 2 ) }\n` );
404
+ }
405
+
406
+ function runCommandText( command, args ) {
407
+ const result = spawnSync( command, args, {
408
+ stdio: 'pipe',
409
+ encoding: 'utf8'
410
+ } );
411
+ if ( result.status !== 0 ) {
412
+ if ( result.error ) {
413
+ throw new Error( result.error.message || `${ command } failed to start` );
414
+ }
415
+ const stderr = ( result.stderr || '' ).trim();
416
+ throw new Error( stderr || `${ command } failed` );
417
+ }
418
+ return result.stdout || '';
419
+ }
420
+
421
+ function runCommandOrThrow( command, args, cwd ) {
422
+ const result = spawnSync( command, args, {
423
+ cwd: cwd || rootDir,
424
+ stdio: 'pipe',
425
+ encoding: 'utf8'
426
+ } );
427
+ if ( result.status !== 0 ) {
428
+ if ( result.error ) {
429
+ throw new Error( result.error.message || `${ command } failed to start` );
430
+ }
431
+ const stderr = ( result.stderr || '' ).trim();
432
+ throw new Error( stderr || `${ command } failed` );
433
+ }
434
+ return result;
435
+ }
436
+
437
+ function resolveInstallCommand() {
438
+ if ( commandExists( 'npm' ) ) {
439
+ return { command: 'npm', args: [ 'install' ] };
440
+ }
441
+ if ( commandExists( 'bun' ) ) {
442
+ return { command: 'bun', args: [ 'install' ] };
443
+ }
444
+ throw new Error( 'no supported package manager found; install npm or bun' );
445
+ }
446
+
447
+ function repoReady( repoPath ) {
448
+ try {
449
+ return fs.statSync( repoPath ).isDirectory();
450
+ } catch ( error ) {
451
+ return false;
452
+ }
453
+ }
454
+
455
+ function repoHeadOrMissing( repoPath ) {
456
+ if ( !repoReady( repoPath ) ) {
457
+ return 'missing';
458
+ }
459
+ if ( fileExists( path.join( repoPath, '.git' ) ) ) {
460
+ try {
461
+ return runCommandText( 'git', [ '-C', repoPath, 'rev-parse', 'HEAD' ] ).trim() || 'unknown';
462
+ } catch ( error ) {
463
+ return 'unknown';
464
+ }
465
+ }
466
+ const stat = fs.statSync( repoPath );
467
+ return `vendored-${ Math.floor( stat.mtimeMs ) }`;
468
+ }
469
+
470
+ function translatorsNeedSync() {
471
+ const stampPath = path.join( stateDir, 'translators-sync.stamp' );
472
+ const hasMerged = fileExists( mergedTranslatorsDir ) &&
473
+ fs.readdirSync( mergedTranslatorsDir ).some( ( name ) => name.endsWith( '.js' ) );
474
+ if ( !hasMerged ) {
475
+ return true;
476
+ }
477
+
478
+ const currentStamp = `zotero=${ repoHeadOrMissing( path.join( zoteroDir, 'modules', 'translators' ) ) };official=${ repoHeadOrMissing( officialTranslatorsDir ) };cn=${ repoHeadOrMissing( cnTranslatorsDir ) }`;
479
+ let previousStamp = '';
480
+ if ( fileExists( stampPath ) ) {
481
+ previousStamp = fs.readFileSync( stampPath, 'utf8' ).trim();
482
+ }
483
+ return currentStamp !== previousStamp;
484
+ }
485
+
486
+ function writeTranslatorStamp() {
487
+ const stampPath = path.join( stateDir, 'translators-sync.stamp' );
488
+ const stamp = `zotero=${ repoHeadOrMissing( path.join( zoteroDir, 'modules', 'translators' ) ) };official=${ repoHeadOrMissing( officialTranslatorsDir ) };cn=${ repoHeadOrMissing( cnTranslatorsDir ) }`;
489
+ fs.writeFileSync( stampPath, `${ stamp }\n` );
490
+ }
491
+
492
+ function syncMergedTranslators() {
493
+ fs.rmSync( mergedTranslatorsDir, { recursive: true, force: true } );
494
+ fs.mkdirSync( mergedTranslatorsDir, { recursive: true } );
495
+ const zoteroTranslators = path.join( zoteroDir, 'modules', 'translators' );
496
+ if ( fileExists( zoteroTranslators ) ) {
497
+ fs.readdirSync( zoteroTranslators )
498
+ .filter( ( name ) => name.endsWith( '.js' ) )
499
+ .forEach( ( name ) => {
500
+ fs.copyFileSync(
501
+ path.join( zoteroTranslators, name ),
502
+ path.join( mergedTranslatorsDir, name )
503
+ );
504
+ } );
505
+ }
506
+ if ( fileExists( officialTranslatorsDir ) ) {
507
+ fs.readdirSync( officialTranslatorsDir )
508
+ .filter( ( name ) => name.endsWith( '.js' ) )
509
+ .forEach( ( name ) => {
510
+ fs.copyFileSync(
511
+ path.join( officialTranslatorsDir, name ),
512
+ path.join( mergedTranslatorsDir, name )
513
+ );
514
+ } );
515
+ }
516
+ if ( fileExists( cnTranslatorsDir ) ) {
517
+ fs.readdirSync( cnTranslatorsDir )
518
+ .filter( ( name ) => name.endsWith( '.js' ) )
519
+ .forEach( ( name ) => {
520
+ fs.copyFileSync(
521
+ path.join( cnTranslatorsDir, name ),
522
+ path.join( mergedTranslatorsDir, name )
523
+ );
524
+ } );
525
+ }
526
+ writeTranslatorStamp();
527
+ }
528
+
529
+ function bootstrapLocalEnvironment() {
530
+ ensureDirs();
531
+ const installer = resolveInstallCommand();
532
+ if ( !repoReady( zoteroDir ) || !repoReady( cnTranslatorsDir ) || !repoReady( officialTranslatorsDir ) ) {
533
+ throw new Error(
534
+ 'missing vendored repos under vendor/. pull the complete repository content.'
535
+ );
536
+ }
537
+
538
+ if ( !fileExists( path.join( rootDir, 'node_modules' ) ) ) {
539
+ runCommandOrThrow( installer.command, installer.args, rootDir );
540
+ }
541
+ if ( !fileExists( path.join( zoteroDir, 'modules', 'translators' ) ) ) {
542
+ throw new Error( 'missing vendored zotero contents under vendor/zotero/modules/translators' );
543
+ }
544
+ if ( !fileExists( path.join( zoteroDir, 'node_modules' ) ) ) {
545
+ runCommandOrThrow( installer.command, installer.args, zoteroDir );
546
+ }
547
+ if ( translatorsNeedSync() ) {
548
+ syncMergedTranslators();
549
+ }
550
+ }
551
+
552
+ function ensureInstalled() {
553
+ if ( fileExists( path.join( rootDir, 'node_modules' ) ) &&
554
+ fileExists( path.join( zoteroDir, 'node_modules' ) ) &&
555
+ fileExists( mergedTranslatorsDir ) ) {
556
+ return;
557
+ }
558
+
559
+ console.error( 'dependencies missing; bootstrapping runtime...' );
560
+ try {
561
+ bootstrapLocalEnvironment();
562
+ } catch ( error ) {
563
+ process.exitCode = 1;
564
+ throw error;
565
+ }
566
+ }
567
+
568
+ function ensureStyleRuntime() {
569
+ if ( fileExists( cslDir ) && fileExists( localeDir ) ) {
570
+ return;
571
+ }
572
+ throw new Error( 'Styles are not synced. Run: citeclaw styles sync' );
573
+ }
574
+
575
+ function walkFiles( dirPath ) {
576
+ const entries = fs.readdirSync( dirPath, { withFileTypes: true } );
577
+ let result = [];
578
+ entries.forEach( ( entry ) => {
579
+ const fullPath = path.join( dirPath, entry.name );
580
+ if ( entry.isDirectory() ) {
581
+ result = result.concat( walkFiles( fullPath ) );
582
+ } else if ( entry.isFile() ) {
583
+ result.push( fullPath );
584
+ }
585
+ } );
586
+ return result;
587
+ }
588
+
589
+ function normalizeDoi( raw ) {
590
+ return String( raw || '' )
591
+ .replace( /^(?:https?:\/\/(?:dx\.)?doi\.org\/|doi:\s*)/i, '' )
592
+ .replace( /^[\s"'`([{]+/g, '' )
593
+ .replace( /[\s"'`)\].,;:}>]+$/g, '' )
594
+ .trim();
595
+ }
596
+
597
+ function lineLooksLikeReferenceEntry( line ) {
598
+ return /^\s*(?:\[\d+\]|\d+\.)\s+/.test( line ) ||
599
+ /\b(?:references?|bibliography|works cited)\b/i.test( line ) ||
600
+ /\b(?:vol\.?|no\.?|pp?\.|et al\.)\b/i.test( line ) && /\(\d{4}\)/.test( line );
601
+ }
602
+
603
+ function scoreDoiCandidate( candidate ) {
604
+ let score = 0;
605
+ const lowerLine = candidate.line.toLowerCase();
606
+ const prevLower = candidate.prevLine.toLowerCase();
607
+
608
+ if ( /(?:doi|doi\.org|identifier|citation_doi|dc\.identifier)/.test( lowerLine ) ) {
609
+ score += 60;
610
+ }
611
+ if ( /(?:doi|identifier)/.test( prevLower ) ) {
612
+ score += 20;
613
+ }
614
+ if ( candidate.lineIndex < 12 ) {
615
+ score += 35;
616
+ } else if ( candidate.lineIndex < 40 ) {
617
+ score += 20;
618
+ } else if ( candidate.lineIndex < 80 ) {
619
+ score += 10;
620
+ }
621
+ if ( lineLooksLikeReferenceEntry( candidate.line ) ) {
622
+ score -= 45;
623
+ }
624
+ if ( /\b(?:references?|bibliography|works cited)\b/.test( prevLower ) ) {
625
+ score -= 70;
626
+ }
627
+ score -= Math.min( candidate.lineIndex, 120 );
628
+ score -= candidate.position * 0.01;
629
+ return score;
630
+ }
631
+
632
+ function extractBestDoiCandidate( text ) {
633
+ const doiRegex = /(?:https?:\/\/(?:dx\.)?doi\.org\/|doi:\s*)?(10\.\d{4,9}\/[-._;()/:A-Z0-9<>\[\]]+)/ig;
634
+ const candidates = [];
635
+ const lines = String( text || '' ).split( /\r?\n/ );
636
+ let offset = 0;
637
+
638
+ lines.forEach( ( line, index ) => {
639
+ let match;
640
+ doiRegex.lastIndex = 0;
641
+ while ( ( match = doiRegex.exec( line ) ) ) {
642
+ const value = normalizeDoi( match[ 1 ] || match[ 0 ] );
643
+ if ( value ) {
644
+ candidates.push( {
645
+ value,
646
+ line,
647
+ prevLine: index > 0 ? lines[ index - 1 ] : '',
648
+ lineIndex: index,
649
+ position: offset + match.index
650
+ } );
651
+ }
652
+ }
653
+ offset += line.length + 1;
654
+ } );
655
+
656
+ if ( candidates.length === 0 ) {
657
+ return null;
658
+ }
659
+
660
+ const deduped = [];
661
+ const seen = new Set();
662
+ candidates.forEach( ( candidate ) => {
663
+ const key = candidate.value.toLowerCase();
664
+ if ( !seen.has( key ) ) {
665
+ seen.add( key );
666
+ candidate.score = scoreDoiCandidate( candidate );
667
+ deduped.push( candidate );
668
+ }
669
+ } );
670
+
671
+ deduped.sort( ( a, b ) => b.score - a.score || a.position - b.position );
672
+ return deduped[ 0 ].value;
673
+ }
674
+
675
+ function isLikelyTitleLine( line ) {
676
+ return line.length > 20 &&
677
+ line.length < 300 &&
678
+ !/^(abstract|keywords?|introduction|references?)\b/i.test( line ) &&
679
+ !/doi\.org\//i.test( line ) &&
680
+ !/^\d+\s*$/.test( line );
681
+ }
682
+
683
+ function uniqueKeepOrder( items ) {
684
+ const seen = new Set();
685
+ const result = [];
686
+ items.forEach( ( item ) => {
687
+ const key = item.trim();
688
+ if ( key && !seen.has( key ) ) {
689
+ seen.add( key );
690
+ result.push( key );
691
+ }
692
+ } );
693
+ return result;
694
+ }
695
+
696
+ function extractVisibleTextFromPdf( pdfPath ) {
697
+ try {
698
+ return runCommandText( 'pdftotext', [ '-f', '1', '-l', '2', '-layout', pdfPath, '-' ] );
699
+ } catch ( error ) {
700
+ return runCommandText( 'pdftotext', [ '-f', '1', '-l', '2', pdfPath, '-' ] );
701
+ }
702
+ }
703
+
704
+ function shouldAttemptPdfOcr( text, metadataTitle ) {
705
+ const visibleText = String( text || '' )
706
+ .replace( /\s+/g, '' )
707
+ .trim();
708
+ if ( visibleText.length > 0 ) {
709
+ return false;
710
+ }
711
+ const titleText = String( metadataTitle || '' )
712
+ .replace( /\s+/g, '' )
713
+ .trim();
714
+ return titleText.length === 0;
715
+ }
716
+
717
+ function extractOcrTextFromPdf( pdfPath ) {
718
+ if ( !commandExists( 'pdftoppm' ) || !commandExists( 'tesseract' ) ) {
719
+ return '';
720
+ }
721
+
722
+ const tmpDir = fs.mkdtempSync( path.join( os.tmpdir(), 'citeclaw-ocr-' ) );
723
+ const prefix = path.join( tmpDir, 'page' );
724
+ try {
725
+ runCommandOrThrow( 'pdftoppm', [ '-f', '1', '-l', '2', '-png', '-r', '200', pdfPath, prefix ] );
726
+ const images = fs.readdirSync( tmpDir )
727
+ .filter( ( name ) => /^page-\d+\.png$/i.test( name ) )
728
+ .sort();
729
+ const chunks = [];
730
+ images.forEach( ( name ) => {
731
+ try {
732
+ const text = runCommandText( 'tesseract', [
733
+ path.join( tmpDir, name ),
734
+ 'stdout',
735
+ '--psm',
736
+ '6'
737
+ ] );
738
+ if ( text.trim() ) {
739
+ chunks.push( text );
740
+ }
741
+ } catch ( error ) {
742
+ }
743
+ } );
744
+ return chunks.join( '\n' );
745
+ } finally {
746
+ fs.rmSync( tmpDir, { recursive: true, force: true } );
747
+ }
748
+ }
749
+
750
+ function detectPdfIdentifierCandidates( text, metadataTitle ) {
751
+ const combined = `${ metadataTitle || '' }\n${ text || '' }`;
752
+ const doi = extractBestDoiCandidate( combined );
753
+ if ( doi ) {
754
+ return {
755
+ type: 'doi',
756
+ value: doi,
757
+ titles: [],
758
+ extraction_source: 'text'
759
+ };
760
+ }
761
+
762
+ const arxivMatch = combined.match( /\b(?:arxiv:\s*)?((?:\d{4}\.\d{4,5}|[a-z-]+(?:\.[A-Z]{2})?\/\d{7})(?:v\d+)?)\b/i );
763
+ if ( arxivMatch && arxivMatch[ 1 ] ) {
764
+ return {
765
+ type: 'arxiv',
766
+ value: `https://arxiv.org/abs/${ arxivMatch[ 1 ] }`,
767
+ titles: [],
768
+ extraction_source: 'text'
769
+ };
770
+ }
771
+
772
+ const lines = combined.split( /\r?\n/ )
773
+ .map( ( line ) => line.trim() )
774
+ .filter( ( line ) => line )
775
+ .filter( isLikelyTitleLine );
776
+
777
+ const titleCandidates = uniqueKeepOrder( [
778
+ metadataTitle || '',
779
+ ...lines.slice( 0, 8 )
780
+ ] ).slice( 0, 5 );
781
+
782
+ if ( titleCandidates.length ) {
783
+ return {
784
+ type: 'title',
785
+ value: titleCandidates[ 0 ],
786
+ titles: titleCandidates,
787
+ extraction_source: 'text'
788
+ };
789
+ }
790
+
791
+ return null;
792
+ }
793
+
794
+ function extractPdfCandidates( pdfPath, options = {} ) {
795
+ if ( !fileExists( pdfPath ) ) {
796
+ throw new Error( `PDF not found: ${ pdfPath }` );
797
+ }
798
+ if ( !commandExists( 'pdftotext' ) ) {
799
+ throw new Error( 'pdftotext is required. Install poppler-utils first.' );
800
+ }
801
+
802
+ let text = extractVisibleTextFromPdf( pdfPath );
803
+ const debug = {
804
+ pdf_path: pdfPath,
805
+ text_chars: String( text || '' ).replace( /\s+/g, '' ).length,
806
+ metadata_title_present: false,
807
+ ocr_attempted: false,
808
+ ocr_text_chars: 0
809
+ };
810
+
811
+ let metadataTitle = null;
812
+ if ( commandExists( 'pdfinfo' ) ) {
813
+ try {
814
+ const infoText = runCommandText( 'pdfinfo', [ pdfPath ] );
815
+ const titleMatch = infoText.match( /^\s*Title:\s+(.+)$/im );
816
+ if ( titleMatch && titleMatch[ 1 ] ) {
817
+ metadataTitle = titleMatch[ 1 ].trim();
818
+ debug.metadata_title_present = true;
819
+ }
820
+ } catch ( error ) {
821
+ }
822
+ }
823
+
824
+ let candidates = detectPdfIdentifierCandidates( text, metadataTitle );
825
+ if ( candidates ) {
826
+ if ( options.debugPdf ) {
827
+ candidates.debug = {
828
+ ...debug,
829
+ result_type: candidates.type,
830
+ result_value: candidates.value
831
+ };
832
+ }
833
+ return candidates;
834
+ }
835
+
836
+ if ( shouldAttemptPdfOcr( text, metadataTitle ) ) {
837
+ debug.ocr_attempted = true;
838
+ const ocrText = extractOcrTextFromPdf( pdfPath );
839
+ if ( ocrText.trim() ) {
840
+ debug.ocr_text_chars = ocrText.replace( /\s+/g, '' ).length;
841
+ text = `${ text }\n${ ocrText }`.trim();
842
+ candidates = detectPdfIdentifierCandidates( text, metadataTitle );
843
+ if ( candidates ) {
844
+ candidates.extraction_source = 'ocr';
845
+ if ( options.debugPdf ) {
846
+ candidates.debug = {
847
+ ...debug,
848
+ result_type: candidates.type,
849
+ result_value: candidates.value
850
+ };
851
+ }
852
+ return candidates;
853
+ }
854
+ }
855
+ }
856
+
857
+ throw new Error( 'Could not detect DOI/arXiv/title from PDF.' );
858
+ }
859
+
860
+ function getFreePort() {
861
+ return new Promise( ( resolve, reject ) => {
862
+ const server = net.createServer();
863
+ server.listen( 0, '127.0.0.1', () => {
864
+ const address = server.address();
865
+ server.close( () => resolve( address.port ) );
866
+ } );
867
+ server.on( 'error', reject );
868
+ } );
869
+ }
870
+
871
+ function waitForPort( port, host = '127.0.0.1', timeoutMs = 30000 ) {
872
+ const start = Date.now();
873
+
874
+ return new Promise( ( resolve, reject ) => {
875
+ function retry() {
876
+ if ( Date.now() - start >= timeoutMs ) {
877
+ reject( new Error( `timeout waiting for ${ host }:${ port }` ) );
878
+ return;
879
+ }
880
+ setTimeout( poll, 500 );
881
+ }
882
+
883
+ function poll() {
884
+ const socket = net.createConnection( { port, host }, () => {
885
+ socket.end();
886
+ resolve();
887
+ } );
888
+ socket.on( 'error', retry );
889
+ }
890
+
891
+ poll();
892
+ } );
893
+ }
894
+
895
+ function terminateProcess( child ) {
896
+ return new Promise( ( resolve ) => {
897
+ if ( !child || child.exitCode !== null || child.killed ) {
898
+ resolve();
899
+ return;
900
+ }
901
+
902
+ child.once( 'exit', () => resolve() );
903
+ child.kill( 'SIGTERM' );
904
+ setTimeout( () => {
905
+ if ( child.exitCode === null ) {
906
+ child.kill( 'SIGKILL' );
907
+ }
908
+ }, 3000 ).unref();
909
+ } );
910
+ }
911
+
912
+ function httpGet( url ) {
913
+ return new Promise( ( resolve, reject ) => {
914
+ const req = http.get( url, ( res ) => {
915
+ let body = '';
916
+ res.setEncoding( 'utf8' );
917
+ res.on( 'data', ( chunk ) => {
918
+ body += chunk;
919
+ } );
920
+ res.on( 'end', () => {
921
+ if ( res.statusCode && res.statusCode >= 200 && res.statusCode < 300 ) {
922
+ resolve( {
923
+ statusCode: res.statusCode,
924
+ headers: res.headers,
925
+ body
926
+ } );
927
+ return;
928
+ }
929
+ const error = new Error( `request failed with status ${ res.statusCode }` );
930
+ error.statusCode = res.statusCode;
931
+ error.headers = res.headers;
932
+ error.body = body;
933
+ reject( error );
934
+ } );
935
+ } );
936
+ req.on( 'error', reject );
937
+ } );
938
+ }
939
+
940
+ function createSilentLogger() {
941
+ const discard = {
942
+ write( chunk, encoding, callback ) {
943
+ if ( callback ) {
944
+ callback();
945
+ }
946
+ }
947
+ };
948
+
949
+ const wrap = ( logger ) => ( {
950
+ _logger: logger,
951
+ log( levelPath, ...args ) {
952
+ const level = String( levelPath ).split( '/' )[ 0 ];
953
+ if ( typeof logger[ level ] === 'function' ) {
954
+ logger[ level ]( ...args );
955
+ }
956
+ },
957
+ child( fields ) {
958
+ return wrap( logger.child( fields ) );
959
+ }
960
+ } );
961
+
962
+ return wrap( bunyan.createLogger( {
963
+ name: 'citeclaw',
964
+ streams: [ { type: 'raw', stream: discard } ]
965
+ } ) );
966
+ }
967
+
968
+ function createNoopMetrics() {
969
+ const noopMetric = {
970
+ increment() {
971
+ },
972
+ endTiming() {
973
+ }
974
+ };
975
+
976
+ return {
977
+ makeMetric() {
978
+ return noopMetric;
979
+ },
980
+ getServiceLabel() {
981
+ return {};
982
+ }
983
+ };
984
+ }
985
+
986
+ function printResponse( response, options ) {
987
+ if ( options.headers ) {
988
+ process.stdout.write( `HTTP ${ response.statusCode }\n` );
989
+ Object.entries( response.headers ).forEach( ( [ key, value ] ) => {
990
+ process.stdout.write( `${ key }: ${ value }\n` );
991
+ } );
992
+ process.stdout.write( '\n' );
993
+ }
994
+
995
+ process.stdout.write( response.body );
996
+ if ( !response.body.endsWith( '\n' ) ) {
997
+ process.stdout.write( '\n' );
998
+ }
999
+ }
1000
+
1001
+ function parseOptions( args ) {
1002
+ const options = {
1003
+ headers: false,
1004
+ plain: false,
1005
+ style: null,
1006
+ locale: 'en-US',
1007
+ repo: '',
1008
+ base: defaultOpenUrlBase,
1009
+ zoteroApiBase: defaultZoteroApiBase,
1010
+ wmfCitoidBase: defaultWmfCitoidBase,
1011
+ zoteroUserId: defaultZoteroUserId,
1012
+ zoteroApiKey: defaultZoteroApiKey,
1013
+ zoteroLibraryType: defaultZoteroLibraryType,
1014
+ zoteroLibraryId: defaultZoteroLibraryId,
1015
+ s2ApiKey: defaultS2ApiKey,
1016
+ parent: '',
1017
+ limit: 20,
1018
+ offset: 0,
1019
+ intervalSec: 60,
1020
+ outBib: '',
1021
+ apply: false,
1022
+ dryRun: false,
1023
+ method: 'GET',
1024
+ params: '',
1025
+ body: '',
1026
+ fields: '',
1027
+ sort: '',
1028
+ year: '',
1029
+ token: '',
1030
+ minCitationCount: '',
1031
+ publicationTypes: '',
1032
+ venue: '',
1033
+ fieldsOfStudy: '',
1034
+ openAccessPdf: '',
1035
+ yes: false,
1036
+ out: '',
1037
+ op: '',
1038
+ in: '',
1039
+ outJsonl: '',
1040
+ json: false,
1041
+ format: '',
1042
+ concurrency: defaultBatchConcurrency,
1043
+ cacheTtlSec: defaultCacheTtlSec,
1044
+ profile: false,
1045
+ debugPdf: false,
1046
+ silent: false,
1047
+ args: []
1048
+ };
1049
+
1050
+ for ( let i = 0; i < args.length; i++ ) {
1051
+ const arg = args[ i ];
1052
+ if ( arg === '--headers' ) {
1053
+ options.headers = true;
1054
+ } else if ( arg === '--plain' ) {
1055
+ options.plain = true;
1056
+ } else if ( arg === '--style' ) {
1057
+ options.style = args[ i + 1 ] || '';
1058
+ i++;
1059
+ } else if ( arg === '--locale' ) {
1060
+ options.locale = args[ i + 1 ] || 'en-US';
1061
+ i++;
1062
+ } else if ( arg === '--repo' ) {
1063
+ options.repo = args[ i + 1 ] || '';
1064
+ i++;
1065
+ } else if ( arg === '--out' ) {
1066
+ options.out = args[ i + 1 ] || '';
1067
+ i++;
1068
+ } else if ( arg === '--base' ) {
1069
+ options.base = args[ i + 1 ] || defaultOpenUrlBase;
1070
+ i++;
1071
+ } else if ( arg === '--zotero-api-base' ) {
1072
+ options.zoteroApiBase = args[ i + 1 ] || defaultZoteroApiBase;
1073
+ i++;
1074
+ } else if ( arg === '--wmf-citoid-base' ) {
1075
+ options.wmfCitoidBase = args[ i + 1 ] || defaultWmfCitoidBase;
1076
+ i++;
1077
+ } else if ( arg === '--user-id' || arg === '--zotero-user-id' ) {
1078
+ options.zoteroUserId = args[ i + 1 ] || '';
1079
+ i++;
1080
+ } else if ( arg === '--api-key' || arg === '--zotero-api-key' ) {
1081
+ options.zoteroApiKey = args[ i + 1 ] || '';
1082
+ i++;
1083
+ } else if ( arg === '--library-type' || arg === '--zotero-library-type' ) {
1084
+ options.zoteroLibraryType = args[ i + 1 ] || defaultZoteroLibraryType;
1085
+ i++;
1086
+ } else if ( arg === '--library-id' || arg === '--zotero-library-id' ) {
1087
+ options.zoteroLibraryId = args[ i + 1 ] || '';
1088
+ i++;
1089
+ } else if ( arg === '--s2-api-key' ) {
1090
+ options.s2ApiKey = args[ i + 1 ] || defaultS2ApiKey;
1091
+ i++;
1092
+ } else if ( arg === '--parent' ) {
1093
+ options.parent = args[ i + 1 ] || '';
1094
+ i++;
1095
+ } else if ( arg === '--limit' ) {
1096
+ const raw = args[ i + 1 ];
1097
+ options.limit = parseInt( raw || '20', 10 );
1098
+ i++;
1099
+ } else if ( arg === '--offset' ) {
1100
+ const raw = args[ i + 1 ];
1101
+ options.offset = parseInt( raw || '0', 10 );
1102
+ i++;
1103
+ } else if ( arg === '--interval' ) {
1104
+ const raw = args[ i + 1 ];
1105
+ options.intervalSec = parseInt( raw || '60', 10 );
1106
+ i++;
1107
+ } else if ( arg === '--method' ) {
1108
+ options.method = ( args[ i + 1 ] || 'GET' ).toUpperCase();
1109
+ i++;
1110
+ } else if ( arg === '--params' ) {
1111
+ options.params = args[ i + 1 ] || '';
1112
+ i++;
1113
+ } else if ( arg === '--body' ) {
1114
+ options.body = args[ i + 1 ] || '';
1115
+ i++;
1116
+ } else if ( arg === '--fields' ) {
1117
+ options.fields = args[ i + 1 ] || '';
1118
+ i++;
1119
+ } else if ( arg === '--sort' ) {
1120
+ options.sort = args[ i + 1 ] || '';
1121
+ i++;
1122
+ } else if ( arg === '--year' ) {
1123
+ options.year = args[ i + 1 ] || '';
1124
+ i++;
1125
+ } else if ( arg === '--token' ) {
1126
+ options.token = args[ i + 1 ] || '';
1127
+ i++;
1128
+ } else if ( arg === '--min-citation-count' ) {
1129
+ options.minCitationCount = args[ i + 1 ] || '';
1130
+ i++;
1131
+ } else if ( arg === '--publication-types' ) {
1132
+ options.publicationTypes = args[ i + 1 ] || '';
1133
+ i++;
1134
+ } else if ( arg === '--venue' ) {
1135
+ options.venue = args[ i + 1 ] || '';
1136
+ i++;
1137
+ } else if ( arg === '--fields-of-study' ) {
1138
+ options.fieldsOfStudy = args[ i + 1 ] || '';
1139
+ i++;
1140
+ } else if ( arg === '--open-access-pdf' ) {
1141
+ options.openAccessPdf = args[ i + 1 ] || '';
1142
+ i++;
1143
+ } else if ( arg === '--out-bib' ) {
1144
+ options.outBib = args[ i + 1 ] || '';
1145
+ i++;
1146
+ } else if ( arg === '--apply' ) {
1147
+ options.apply = true;
1148
+ } else if ( arg === '--dry-run' ) {
1149
+ options.dryRun = true;
1150
+ } else if ( arg === '--yes' || arg === '-y' ) {
1151
+ options.yes = true;
1152
+ } else if ( arg === '--op' ) {
1153
+ options.op = args[ i + 1 ] || '';
1154
+ i++;
1155
+ } else if ( arg === '--in' ) {
1156
+ options.in = args[ i + 1 ] || '';
1157
+ i++;
1158
+ } else if ( arg === '--out-jsonl' ) {
1159
+ options.outJsonl = args[ i + 1 ] || '';
1160
+ i++;
1161
+ } else if ( arg === '--json' ) {
1162
+ options.json = true;
1163
+ } else if ( arg === '--format' ) {
1164
+ options.format = args[ i + 1 ] || '';
1165
+ i++;
1166
+ } else if ( arg === '--cache-ttl-sec' ) {
1167
+ const ttlRaw = args[ i + 1 ];
1168
+ options.cacheTtlSec = parseInt( ttlRaw || String( defaultCacheTtlSec ), 10 );
1169
+ i++;
1170
+ } else if ( arg === '--concurrency' ) {
1171
+ const raw = args[ i + 1 ];
1172
+ options.concurrency = parseInt( raw || String( defaultBatchConcurrency ), 10 );
1173
+ i++;
1174
+ } else if ( arg === '--profile' ) {
1175
+ options.profile = true;
1176
+ } else if ( arg === '--debug-pdf' ) {
1177
+ options.debugPdf = true;
1178
+ } else {
1179
+ options.args.push( arg );
1180
+ }
1181
+ }
1182
+
1183
+ return options;
1184
+ }
1185
+
1186
+ async function withRunningServices( callback ) {
1187
+ ensureDirs();
1188
+ ensureInstalled();
1189
+
1190
+ const zoteroPort = await getFreePort();
1191
+ const timestamp = `${ Date.now() }-${ process.pid }`;
1192
+ const zoteroLogPath = path.join( logDir, `zotero-once-${ timestamp }.log` );
1193
+ const citoidLogPath = path.join( logDir, `citoid-once-${ timestamp }.log` );
1194
+ const baseConfig = yaml.load( fs.readFileSync( path.join( rootDir, 'config.dev.yaml' ), 'utf8' ) );
1195
+ const serviceConf = baseConfig.services[ 0 ].conf;
1196
+ serviceConf.port = 0;
1197
+ serviceConf.interface = '127.0.0.1';
1198
+ serviceConf.zoteroInterface = '127.0.0.1';
1199
+ serviceConf.zoteroPort = zoteroPort;
1200
+ serviceConf.user_agent = 'citeclaw-cli';
1201
+ serviceConf.mailto = process.env.MAILTO || 'example@example.com';
1202
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
1203
+ fs.writeFileSync( zoteroLogPath, '' );
1204
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
1205
+ fs.writeFileSync( citoidLogPath, '' );
1206
+
1207
+ const zoteroEnv = {
1208
+ ...process.env,
1209
+ NODE_CONFIG: JSON.stringify( {
1210
+ port: zoteroPort,
1211
+ host: '127.0.0.1',
1212
+ translatorsDirectory: mergedTranslatorsDir
1213
+ } ),
1214
+ USER_AGENT: process.env.USER_AGENT ||
1215
+ `Mozilla/5.0 CitoidLocal/1.0 (${ process.env.MAILTO || 'example@example.com' })`
1216
+ };
1217
+
1218
+ const zoteroProc = spawn( 'node', [ 'src/server.js' ], {
1219
+ cwd: zoteroDir,
1220
+ env: zoteroEnv,
1221
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
1222
+ stdio: [ 'ignore', fs.openSync( zoteroLogPath, 'a' ), fs.openSync( zoteroLogPath, 'a' ) ]
1223
+ } );
1224
+
1225
+ let citoidServer;
1226
+
1227
+ try {
1228
+ await waitForPort( zoteroPort );
1229
+
1230
+ const logger = createSilentLogger();
1231
+ const metrics = createNoopMetrics();
1232
+ const app = await citoidApp.buildApp( {
1233
+ config: serviceConf,
1234
+ logger,
1235
+ metrics
1236
+ } );
1237
+ citoidServer = await citoidApp.createServer( app );
1238
+ const citoidPort = citoidServer.address().port;
1239
+ return await callback( { citoidPort, zoteroLogPath, citoidLogPath } );
1240
+ } finally {
1241
+ if ( citoidServer ) {
1242
+ await new Promise( ( resolve ) => {
1243
+ citoidServer.shutdown( resolve );
1244
+ } );
1245
+ }
1246
+ await terminateProcess( zoteroProc );
1247
+ }
1248
+ }
1249
+
1250
+ async function runApiPath( requestPath, options = {} ) {
1251
+ const startedAt = Date.now();
1252
+ const normalizedPath = requestPath.startsWith( '/' ) ? requestPath : `/${ requestPath }`;
1253
+ const response = await withRunningServices(
1254
+ async ( ctx ) => httpGet( `http://127.0.0.1:${ ctx.citoidPort }${ normalizedPath }` )
1255
+ );
1256
+ if ( options.json ) {
1257
+ jsonOut( {
1258
+ ok: true,
1259
+ command: 'api',
1260
+ stage: 'done',
1261
+ elapsed_ms: Date.now() - startedAt,
1262
+ path: normalizedPath,
1263
+ response
1264
+ } );
1265
+ return response;
1266
+ }
1267
+ printResponse( response, options );
1268
+ profileLog( options, 'api', startedAt, `path=${ normalizedPath }` );
1269
+ return response;
1270
+ }
1271
+
1272
+ async function runCitation( format, query, options ) {
1273
+ const startedAt = Date.now();
1274
+ const rawQuery = String( query || '' ).trim();
1275
+ const arxivId = normalizeArxivId( rawQuery );
1276
+ if ( format === 'bibtex' && isLikelyPdfUrl( rawQuery ) && !arxivId ) {
1277
+ const tmpPdfPath = path.join(
1278
+ cacheRootDir,
1279
+ 'tmp',
1280
+ `${ makeCacheKey( 'cite-pdf-url', { query: rawQuery } ) }.pdf`
1281
+ );
1282
+ ensurePdfOutputDir( tmpPdfPath );
1283
+ await runFetchPdf( rawQuery, {
1284
+ ...options,
1285
+ json: false,
1286
+ silent: true,
1287
+ out: tmpPdfPath
1288
+ } );
1289
+ return runCitationFromPdf( tmpPdfPath, options );
1290
+ }
1291
+ const normalized = normalizeCitationQuery( query );
1292
+ const cached = await readThroughCache(
1293
+ 'cite-response',
1294
+ { format, query: normalized.query },
1295
+ options.cacheTtlSec,
1296
+ async () => withRunningServices( ( ctx ) => queryCitationWithContext( ctx, format, normalized.query ) )
1297
+ );
1298
+ const response = cached.value;
1299
+ if ( options.json ) {
1300
+ jsonOut( {
1301
+ ok: true,
1302
+ command: 'cite',
1303
+ cache_hit: cached.cacheHit,
1304
+ elapsed_ms: Date.now() - startedAt,
1305
+ stage: 'done',
1306
+ format,
1307
+ query,
1308
+ query_used: normalized.query,
1309
+ query_normalized: normalized.normalized,
1310
+ response
1311
+ } );
1312
+ return response;
1313
+ }
1314
+ if ( !options.silent ) {
1315
+ printResponse( response, options );
1316
+ }
1317
+ profileLog(
1318
+ options,
1319
+ 'cite',
1320
+ startedAt,
1321
+ `cache_hit=${ cached.cacheHit } normalized=${ normalized.normalized }`
1322
+ );
1323
+ return response;
1324
+ }
1325
+
1326
+ async function queryCitationWithContext( ctx, format, query ) {
1327
+ return httpGet( `http://127.0.0.1:${ ctx.citoidPort }/${ format }/${ encodeURIComponent( query ) }` );
1328
+ }
1329
+
1330
+ async function runCitationFromPdf( pdfPath, options ) {
1331
+ const startedAt = Date.now();
1332
+ const candidates = extractPdfCandidates( path.resolve( pdfPath ), options );
1333
+ if ( candidates.type === 'doi' || candidates.type === 'arxiv' ) {
1334
+ const query = candidates.value;
1335
+ const jsonOptions = options.json ? { ...options, json: false, silent: true } : options;
1336
+ const response = await runCitation( 'bibtex', query, jsonOptions );
1337
+ if ( options.json ) {
1338
+ jsonOut( {
1339
+ ok: true,
1340
+ command: 'cite-pdf',
1341
+ stage: 'done',
1342
+ elapsed_ms: Date.now() - startedAt,
1343
+ source: candidates.type,
1344
+ extraction_source: candidates.extraction_source || 'text',
1345
+ query,
1346
+ pdf_debug: options.debugPdf ? candidates.debug || null : undefined,
1347
+ response
1348
+ } );
1349
+ }
1350
+ return response;
1351
+ }
1352
+
1353
+ return withRunningServices( async ( ctx ) => {
1354
+ let lastError;
1355
+ for ( const title of candidates.titles ) {
1356
+ try {
1357
+ const response = await queryCitationWithContext( ctx, 'bibtex', title );
1358
+ if ( options.json ) {
1359
+ jsonOut( {
1360
+ ok: true,
1361
+ command: 'cite-pdf',
1362
+ stage: 'done',
1363
+ elapsed_ms: Date.now() - startedAt,
1364
+ source: 'title-fallback',
1365
+ extraction_source: candidates.extraction_source || 'text',
1366
+ query: title,
1367
+ pdf_debug: options.debugPdf ? candidates.debug || null : undefined,
1368
+ response
1369
+ } );
1370
+ } else {
1371
+ printResponse( response, options );
1372
+ }
1373
+ return response;
1374
+ } catch ( error ) {
1375
+ lastError = error;
1376
+ }
1377
+ }
1378
+
1379
+ if ( lastError ) {
1380
+ throw lastError;
1381
+ }
1382
+ throw new Error( 'Unable to resolve citation from extracted PDF titles.' );
1383
+ } );
1384
+ }
1385
+
1386
+ async function syncStyles( options ) {
1387
+ ensureDirs();
1388
+ fs.mkdirSync( cslDir, { recursive: true } );
1389
+ fs.mkdirSync( localeDir, { recursive: true } );
1390
+ const configuredSourceDirs = options.repo ?
1391
+ [
1392
+ path.isAbsolute( options.repo || '' ) ?
1393
+ options.repo :
1394
+ path.join( rootDir, options.repo || 'vendor/styles' )
1395
+ ] :
1396
+ defaultStyleSources;
1397
+ const sourceDirs = configuredSourceDirs.filter( ( sourceDir ) => fileExists( sourceDir ) );
1398
+ if ( options.repo && !sourceDirs.length ) {
1399
+ throw new Error( `styles source not found: ${ configuredSourceDirs.join( ', ' ) }` );
1400
+ }
1401
+ if ( !options.repo && !sourceDirs.length ) {
1402
+ throw new Error(
1403
+ 'no bundled style sources found in this package; provide --repo <styles-dir> or run from a full source checkout'
1404
+ );
1405
+ }
1406
+
1407
+ let copiedCount = 0;
1408
+ sourceDirs.forEach( ( sourceDir ) => {
1409
+ const cslFiles = walkFiles( sourceDir )
1410
+ .filter( ( filePath ) => filePath.endsWith( '.csl' ) );
1411
+ cslFiles.forEach( ( src ) => {
1412
+ const dest = path.join( cslDir, path.basename( src ) );
1413
+ fs.copyFileSync( src, dest );
1414
+ copiedCount++;
1415
+ } );
1416
+ } );
1417
+
1418
+ const localeTargets = [ 'en-US', 'zh-CN' ];
1419
+ if ( !commandExists( 'curl' ) ) {
1420
+ throw new Error( 'curl is required for styles sync' );
1421
+ }
1422
+ for ( const locale of localeTargets ) {
1423
+ const localeUrl = `https://raw.githubusercontent.com/citation-style-language/locales/master/locales-${ locale }.xml`;
1424
+ const dest = path.join( localeDir, `locales-${ locale }.xml` );
1425
+ runCommandOrThrow( 'curl', [ '-fsSL', localeUrl, '-o', dest ] );
1426
+ }
1427
+
1428
+ process.stdout.write( `styles synced to ${ cslDir } from ${ sourceDirs.join( ', ' ) } (copied ${ copiedCount } files)\n` );
1429
+ }
1430
+
1431
+ function pickDefaultStylePath() {
1432
+ const styles = fs.readdirSync( cslDir )
1433
+ .filter( ( name ) => name.endsWith( '.csl' ) );
1434
+ if ( !styles.length ) {
1435
+ throw new Error( `No CSL styles found in ${ cslDir }` );
1436
+ }
1437
+ const preferred = styles.find( ( name ) => /gb[-—–]?t[-—–]?7714/i.test( name ) );
1438
+ return path.join( cslDir, preferred || styles[ 0 ] );
1439
+ }
1440
+
1441
+ function resolveStylePath( styleOption ) {
1442
+ ensureStyleRuntime();
1443
+ if ( styleOption ) {
1444
+ const maybePath = path.resolve( styleOption );
1445
+ if ( fileExists( maybePath ) ) {
1446
+ return maybePath;
1447
+ }
1448
+ const withExt = styleOption.endsWith( '.csl' ) ? styleOption : `${ styleOption }.csl`;
1449
+ const inCslDir = path.join( cslDir, withExt );
1450
+ if ( fileExists( inCslDir ) ) {
1451
+ return inCslDir;
1452
+ }
1453
+ throw new Error( `Style not found: ${ styleOption }` );
1454
+ }
1455
+ return pickDefaultStylePath();
1456
+ }
1457
+
1458
+ function parseIssued( rawDate ) {
1459
+ if ( !rawDate || typeof rawDate !== 'string' ) {
1460
+ return undefined;
1461
+ }
1462
+ const parts = rawDate.split( /[-/]/ ).map( ( p ) => parseInt( p, 10 ) ).filter( Number.isFinite );
1463
+ if ( !parts.length ) {
1464
+ return undefined;
1465
+ }
1466
+ return { 'date-parts': [ parts ] };
1467
+ }
1468
+
1469
+ function creatorsToCslAuthors( creators ) {
1470
+ if ( !Array.isArray( creators ) ) {
1471
+ return undefined;
1472
+ }
1473
+ return creators.map( ( creator ) => {
1474
+ if ( creator.lastName || creator.firstName ) {
1475
+ return {
1476
+ family: creator.lastName || '',
1477
+ given: creator.firstName || ''
1478
+ };
1479
+ }
1480
+ if ( creator.name ) {
1481
+ return { literal: creator.name };
1482
+ }
1483
+ return null;
1484
+ } ).filter( Boolean );
1485
+ }
1486
+
1487
+ function mapItemTypeToCsl( itemType ) {
1488
+ const typeMap = {
1489
+ journalArticle: 'article-journal',
1490
+ conferencePaper: 'paper-conference',
1491
+ book: 'book',
1492
+ bookSection: 'chapter',
1493
+ report: 'report',
1494
+ thesis: 'thesis',
1495
+ webpage: 'webpage',
1496
+ preprint: 'article'
1497
+ };
1498
+ return typeMap[ itemType ] || 'article';
1499
+ }
1500
+
1501
+ function zoteroToCsl( zoteroItem ) {
1502
+ const csl = {
1503
+ id: 'item-1',
1504
+ type: mapItemTypeToCsl( zoteroItem.itemType ),
1505
+ title: zoteroItem.title,
1506
+ URL: zoteroItem.url,
1507
+ DOI: zoteroItem.DOI,
1508
+ ISBN: zoteroItem.ISBN,
1509
+ ISSN: zoteroItem.ISSN,
1510
+ volume: zoteroItem.volume,
1511
+ issue: zoteroItem.issue,
1512
+ page: zoteroItem.pages,
1513
+ publisher: zoteroItem.publisher,
1514
+ 'publisher-place': zoteroItem.place,
1515
+ abstract: zoteroItem.abstractNote,
1516
+ 'container-title': zoteroItem.publicationTitle || zoteroItem.websiteTitle
1517
+ };
1518
+ const authors = creatorsToCslAuthors( zoteroItem.creators );
1519
+ if ( authors && authors.length ) {
1520
+ csl.author = authors;
1521
+ }
1522
+ const issued = parseIssued( zoteroItem.date );
1523
+ if ( issued ) {
1524
+ csl.issued = issued;
1525
+ }
1526
+ return csl;
1527
+ }
1528
+
1529
+ function renderWithCsl( cslItem, stylePath, localeCode ) {
1530
+ const styleXml = fs.readFileSync( stylePath, 'utf8' );
1531
+ const localePath = path.join( localeDir, `locales-${ localeCode }.xml` );
1532
+ if ( !fileExists( localePath ) ) {
1533
+ throw new Error( `Locale file not found: ${ localeCode }. Run styles sync first.` );
1534
+ }
1535
+ const localeXml = fs.readFileSync( localePath, 'utf8' );
1536
+ const sys = {
1537
+ retrieveLocale() {
1538
+ return localeXml;
1539
+ },
1540
+ retrieveItem( id ) {
1541
+ return cslItem.id === id ? cslItem : null;
1542
+ }
1543
+ };
1544
+ const engine = new CSL.Engine( sys, styleXml, localeCode );
1545
+ engine.updateItems( [ cslItem.id ] );
1546
+ const bibliography = engine.makeBibliography();
1547
+ return bibliography[ 1 ].join( '\n' );
1548
+ }
1549
+
1550
+ function htmlToPlainText( html ) {
1551
+ return html
1552
+ .replace( /<[^>]+>/g, ' ' )
1553
+ .replace( /&nbsp;/g, ' ' )
1554
+ .replace( /&amp;/g, '&' )
1555
+ .replace( /&lt;/g, '<' )
1556
+ .replace( /&gt;/g, '>' )
1557
+ .replace( /&quot;/g, '"' )
1558
+ .replace( /&#39;/g, '\'' )
1559
+ .replace( /\s+/g, ' ' )
1560
+ .trim();
1561
+ }
1562
+
1563
+ function sleep( ms ) {
1564
+ return new Promise( ( resolve ) => {
1565
+ setTimeout( resolve, ms );
1566
+ } );
1567
+ }
1568
+
1569
+ function isHttpUrl( raw ) {
1570
+ try {
1571
+ const parsed = new URL( raw );
1572
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
1573
+ } catch ( error ) {
1574
+ return false;
1575
+ }
1576
+ }
1577
+
1578
+ function normalizeArxivId( raw ) {
1579
+ const value = String( raw || '' ).trim();
1580
+ const id = value
1581
+ .replace( /^https?:\/\/arxiv\.org\/(?:abs|pdf)\//i, '' )
1582
+ .replace( /\.pdf$/i, '' )
1583
+ .replace( /^arxiv:/i, '' )
1584
+ .trim();
1585
+ const match = id.match( /^((?:\d{4}\.\d{4,5}|[a-z-]+(?:\.[A-Z]{2})?\/\d{7})(?:v\d+)?)$/i );
1586
+ return match ? match[ 1 ] : null;
1587
+ }
1588
+
1589
+ function detectIdentifierType( input ) {
1590
+ const value = String( input || '' ).trim();
1591
+ const arxivId = normalizeArxivId( value );
1592
+ if ( arxivId ) {
1593
+ return { type: 'arxiv', value: arxivId };
1594
+ }
1595
+ if ( isHttpUrl( value ) ) {
1596
+ return { type: 'url', value };
1597
+ }
1598
+ const doiMatch = value.match( /\b10\.\d{4,9}\/[-._;()/:A-Z0-9]+\b/i );
1599
+ if ( doiMatch ) {
1600
+ return { type: 'doi', value: normalizeDoi( doiMatch[ 0 ] ) };
1601
+ }
1602
+ throw new Error( `Unsupported identifier: ${ input }` );
1603
+ }
1604
+
1605
+ function normalizeCitationQuery( rawQuery ) {
1606
+ const query = String( rawQuery || '' ).trim();
1607
+ const arxivId = normalizeArxivId( query );
1608
+ if ( arxivId ) {
1609
+ return {
1610
+ query: `https://arxiv.org/abs/${ arxivId }`,
1611
+ normalized: true
1612
+ };
1613
+ }
1614
+ return {
1615
+ query,
1616
+ normalized: false
1617
+ };
1618
+ }
1619
+
1620
+ function normalizeZoteroLibraryType( rawType ) {
1621
+ const normalized = String( rawType || '' ).trim().toLowerCase();
1622
+ if ( normalized === 'users' || normalized === 'groups' ) {
1623
+ return normalized;
1624
+ }
1625
+ throw new Error( `Unsupported Zotero library type: ${ rawType } (use users or groups)` );
1626
+ }
1627
+
1628
+ function loadZoteroAuth() {
1629
+ if ( !fileExists( zoteroAuthPath ) ) {
1630
+ return null;
1631
+ }
1632
+ try {
1633
+ const parsed = JSON.parse( fs.readFileSync( zoteroAuthPath, 'utf8' ) );
1634
+ if ( parsed && typeof parsed === 'object' ) {
1635
+ return parsed;
1636
+ }
1637
+ return null;
1638
+ } catch ( error ) {
1639
+ return null;
1640
+ }
1641
+ }
1642
+
1643
+ function saveZoteroAuth( auth ) {
1644
+ ensureDirs();
1645
+ fs.writeFileSync( zoteroAuthPath, `${ JSON.stringify( auth, null, 2 ) }\n`, { mode: 0o600 } );
1646
+ }
1647
+
1648
+ function clearZoteroAuth() {
1649
+ if ( fileExists( zoteroAuthPath ) ) {
1650
+ fs.rmSync( zoteroAuthPath, { force: true } );
1651
+ }
1652
+ }
1653
+
1654
+ function mergeZoteroAuth(options) {
1655
+ const saved = loadZoteroAuth() || {};
1656
+ const merged = {
1657
+ apiBase: String( options.zoteroApiBase || saved.apiBase || defaultZoteroApiBase ).trim(),
1658
+ userId: String( options.zoteroUserId || saved.userId || defaultZoteroUserId ).trim(),
1659
+ apiKey: String( options.zoteroApiKey || saved.apiKey || defaultZoteroApiKey ).trim(),
1660
+ libraryType: String( options.zoteroLibraryType || saved.libraryType || defaultZoteroLibraryType ).trim(),
1661
+ libraryId: String( options.zoteroLibraryId || saved.libraryId || defaultZoteroLibraryId ).trim(),
1662
+ safeMode: options.dryRun || !!saved.safeMode
1663
+ };
1664
+ merged.libraryType = normalizeZoteroLibraryType( merged.libraryType );
1665
+ if ( merged.libraryType === 'users' && !merged.libraryId ) {
1666
+ merged.libraryId = merged.userId;
1667
+ }
1668
+ if ( merged.libraryType === 'users' && !merged.userId ) {
1669
+ merged.userId = merged.libraryId;
1670
+ }
1671
+ return merged;
1672
+ }
1673
+
1674
+ function isDryRun(options, auth) {
1675
+ return !!( options && options.dryRun ) || !!( auth && auth.safeMode );
1676
+ }
1677
+
1678
+ function requireZoteroLibrary( auth ) {
1679
+ if ( !auth.libraryId ) {
1680
+ if ( auth.libraryType === 'users' ) {
1681
+ throw new Error( 'Missing Zotero user id. Run: citeclaw zotero login --user-id <id> --api-key <key>' );
1682
+ }
1683
+ throw new Error( 'Missing Zotero group id. Use --library-type groups --library-id <id> during login.' );
1684
+ }
1685
+ }
1686
+
1687
+ function parseZoteroItemReference( rawReference, auth ) {
1688
+ const input = String( rawReference || '' ).trim();
1689
+ if ( !input ) {
1690
+ throw new Error( 'Missing Zotero item reference.' );
1691
+ }
1692
+
1693
+ const keyOnly = input.match( /^[A-Z0-9]{8}$/i );
1694
+ if ( keyOnly ) {
1695
+ return {
1696
+ libraryType: auth.libraryType,
1697
+ libraryId: auth.libraryId,
1698
+ itemKey: keyOnly[ 0 ].toUpperCase()
1699
+ };
1700
+ }
1701
+
1702
+ const webUser = input.match( /zotero\.org\/users\/(\d+)\/items\/([A-Z0-9]{8})/i );
1703
+ if ( webUser ) {
1704
+ return {
1705
+ libraryType: 'users',
1706
+ libraryId: webUser[ 1 ],
1707
+ itemKey: webUser[ 2 ].toUpperCase()
1708
+ };
1709
+ }
1710
+
1711
+ const webGroup = input.match( /zotero\.org\/groups\/(\d+)\/items\/([A-Z0-9]{8})/i );
1712
+ if ( webGroup ) {
1713
+ return {
1714
+ libraryType: 'groups',
1715
+ libraryId: webGroup[ 1 ],
1716
+ itemKey: webGroup[ 2 ].toUpperCase()
1717
+ };
1718
+ }
1719
+
1720
+ const localUser = input.match( /^zotero:\/\/select\/library\/items\/([A-Z0-9]{8})$/i );
1721
+ if ( localUser ) {
1722
+ return {
1723
+ libraryType: auth.libraryType,
1724
+ libraryId: auth.libraryId,
1725
+ itemKey: localUser[ 1 ].toUpperCase()
1726
+ };
1727
+ }
1728
+
1729
+ const localGroup = input.match( /^zotero:\/\/select\/groups\/(\d+)\/items\/([A-Z0-9]{8})$/i );
1730
+ if ( localGroup ) {
1731
+ return {
1732
+ libraryType: 'groups',
1733
+ libraryId: localGroup[ 1 ],
1734
+ itemKey: localGroup[ 2 ].toUpperCase()
1735
+ };
1736
+ }
1737
+
1738
+ throw new Error( `Unsupported Zotero reference: ${ input }` );
1739
+ }
1740
+
1741
+ function buildZoteroApiUrl( auth, pathname, queryObj = {} ) {
1742
+ const base = String( auth.apiBase || defaultZoteroApiBase ).replace( /\/+$/, '' );
1743
+ const url = new URL( `${ base }${ pathname }` );
1744
+ Object.entries( queryObj ).forEach( ( [ key, value ] ) => {
1745
+ if ( value !== undefined && value !== null && value !== '' ) {
1746
+ url.searchParams.set( key, String( value ) );
1747
+ }
1748
+ } );
1749
+ return url.toString();
1750
+ }
1751
+
1752
+ async function zoteroApiRequest( auth, pathname, queryObj = {} ) {
1753
+ const url = buildZoteroApiUrl( auth, pathname, queryObj );
1754
+ const headers = {
1755
+ Accept: 'application/json, text/plain;q=0.9, */*;q=0.1'
1756
+ };
1757
+ if ( auth.apiKey ) {
1758
+ headers[ 'Zotero-API-Key' ] = auth.apiKey;
1759
+ }
1760
+ const limiter = new HostRateLimiter( 0 );
1761
+ const response = await requestExternal( url, limiter, { headers } );
1762
+ const body = bodyToText( response );
1763
+ return { url, response, body };
1764
+ }
1765
+
1766
+ function parseZoteroJsonArray( body, url ) {
1767
+ try {
1768
+ const parsed = JSON.parse( body );
1769
+ if ( Array.isArray( parsed ) ) {
1770
+ return parsed;
1771
+ }
1772
+ throw new Error( `Unexpected Zotero payload from ${ url }` );
1773
+ } catch ( error ) {
1774
+ throw new Error( `Invalid JSON from Zotero API: ${ error.message }` );
1775
+ }
1776
+ }
1777
+
1778
+ function stripToSafeAuthView( auth ) {
1779
+ return {
1780
+ api_base: auth.apiBase,
1781
+ library_type: auth.libraryType,
1782
+ library_id: auth.libraryId,
1783
+ user_id: auth.userId || '',
1784
+ has_api_key: !!auth.apiKey
1785
+ };
1786
+ }
1787
+
1788
+ function parseJsonInputArg( rawInput, label ) {
1789
+ const raw = String( rawInput || '' ).trim();
1790
+ if ( !raw ) {
1791
+ throw new Error( `Missing JSON payload for zotero ${ label }` );
1792
+ }
1793
+ let text = raw;
1794
+ if ( raw.startsWith( '@' ) ) {
1795
+ const filePath = path.resolve( raw.slice( 1 ) );
1796
+ if ( !fileExists( filePath ) ) {
1797
+ throw new Error( `Payload file not found: ${ filePath }` );
1798
+ }
1799
+ text = fs.readFileSync( filePath, 'utf8' );
1800
+ }
1801
+ try {
1802
+ return JSON.parse( text );
1803
+ } catch ( error ) {
1804
+ throw new Error( `Invalid JSON for zotero ${ label }: ${ error.message }` );
1805
+ }
1806
+ }
1807
+
1808
+ function ensurePlainObject( value, label ) {
1809
+ if ( !value || typeof value !== 'object' || Array.isArray( value ) ) {
1810
+ throw new Error( `${ label } must be a JSON object` );
1811
+ }
1812
+ }
1813
+
1814
+ function validateJsonNode( value, pathLabel, depth = 0 ) {
1815
+ if ( depth > 20 ) {
1816
+ throw new Error( `Payload is too deeply nested at ${ pathLabel }` );
1817
+ }
1818
+ if ( value === null ) {
1819
+ return;
1820
+ }
1821
+ const type = typeof value;
1822
+ if ( type === 'string' ) {
1823
+ if ( value.length > 20000 ) {
1824
+ throw new Error( `String too long at ${ pathLabel }` );
1825
+ }
1826
+ if ( /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/.test( value ) ) {
1827
+ throw new Error( `Control characters are not allowed at ${ pathLabel }` );
1828
+ }
1829
+ return;
1830
+ }
1831
+ if ( type === 'number' ) {
1832
+ if ( !Number.isFinite( value ) ) {
1833
+ throw new Error( `Non-finite number at ${ pathLabel }` );
1834
+ }
1835
+ return;
1836
+ }
1837
+ if ( type === 'boolean' ) {
1838
+ return;
1839
+ }
1840
+ if ( Array.isArray( value ) ) {
1841
+ if ( value.length > 2000 ) {
1842
+ throw new Error( `Array too large at ${ pathLabel }` );
1843
+ }
1844
+ value.forEach( ( item, index ) => {
1845
+ validateJsonNode( item, `${ pathLabel }[${ index }]`, depth + 1 );
1846
+ } );
1847
+ return;
1848
+ }
1849
+ if ( type === 'object' ) {
1850
+ Object.entries( value ).forEach( ( [ key, item ] ) => {
1851
+ validateJsonNode( item, `${ pathLabel }.${ key }`, depth + 1 );
1852
+ } );
1853
+ return;
1854
+ }
1855
+ throw new Error( `Unsupported value type at ${ pathLabel }` );
1856
+ }
1857
+
1858
+ function validateCreatorsField( creators, label ) {
1859
+ if ( creators === undefined ) {
1860
+ return;
1861
+ }
1862
+ if ( !Array.isArray( creators ) ) {
1863
+ throw new Error( `${ label }.creators must be an array` );
1864
+ }
1865
+ creators.forEach( ( creator, index ) => {
1866
+ ensurePlainObject( creator, `${ label }.creators[${ index }]` );
1867
+ const hasName = typeof creator.name === 'string' && creator.name.trim();
1868
+ const hasLast = typeof creator.lastName === 'string' && creator.lastName.trim();
1869
+ if ( !hasName && !hasLast ) {
1870
+ throw new Error( `${ label }.creators[${ index }] needs name or lastName` );
1871
+ }
1872
+ if ( creator.creatorType && typeof creator.creatorType !== 'string' ) {
1873
+ throw new Error( `${ label }.creators[${ index }].creatorType must be string` );
1874
+ }
1875
+ } );
1876
+ }
1877
+
1878
+ function validateTagsField( tags, label ) {
1879
+ if ( tags === undefined ) {
1880
+ return;
1881
+ }
1882
+ if ( !Array.isArray( tags ) ) {
1883
+ throw new Error( `${ label }.tags must be an array` );
1884
+ }
1885
+ tags.forEach( ( tag, index ) => {
1886
+ if ( typeof tag === 'string' ) {
1887
+ return;
1888
+ }
1889
+ ensurePlainObject( tag, `${ label }.tags[${ index }]` );
1890
+ if ( typeof tag.tag !== 'string' || !tag.tag.trim() ) {
1891
+ throw new Error( `${ label }.tags[${ index }].tag must be non-empty string` );
1892
+ }
1893
+ } );
1894
+ }
1895
+
1896
+ function validateCollectionsField( collections, label ) {
1897
+ if ( collections === undefined ) {
1898
+ return;
1899
+ }
1900
+ if ( !Array.isArray( collections ) ) {
1901
+ throw new Error( `${ label }.collections must be an array` );
1902
+ }
1903
+ collections.forEach( ( key, index ) => {
1904
+ if ( typeof key !== 'string' || !/^[A-Z0-9]{8}$/i.test( key ) ) {
1905
+ throw new Error( `${ label }.collections[${ index }] must be an 8-char collection key` );
1906
+ }
1907
+ } );
1908
+ }
1909
+
1910
+ function validateRelationsField( relations, label ) {
1911
+ if ( relations === undefined ) {
1912
+ return;
1913
+ }
1914
+ ensurePlainObject( relations, `${ label }.relations` );
1915
+ Object.entries( relations ).forEach( ( [ relKey, relVal ] ) => {
1916
+ if ( typeof relVal === 'string' ) {
1917
+ return;
1918
+ }
1919
+ if ( Array.isArray( relVal ) && relVal.every( ( x ) => typeof x === 'string' ) ) {
1920
+ return;
1921
+ }
1922
+ throw new Error( `${ label }.relations.${ relKey } must be string or string[]` );
1923
+ } );
1924
+ }
1925
+
1926
+ function validateZoteroAddPayload( payload ) {
1927
+ ensurePlainObject( payload, 'zotero add payload' );
1928
+ validateJsonNode( payload, 'payload' );
1929
+ const serialized = JSON.stringify( payload );
1930
+ if ( serialized.length > 300000 ) {
1931
+ throw new Error( 'zotero add payload is too large' );
1932
+ }
1933
+ const blocked = [ 'key', 'version', 'libraryID', 'links', 'meta' ];
1934
+ blocked.forEach( ( key ) => {
1935
+ if ( Object.prototype.hasOwnProperty.call( payload, key ) ) {
1936
+ throw new Error( `zotero add payload cannot include reserved field: ${ key }` );
1937
+ }
1938
+ } );
1939
+ if ( typeof payload.itemType !== 'string' || !payload.itemType.trim() ) {
1940
+ throw new Error( 'zotero add payload requires non-empty itemType' );
1941
+ }
1942
+ validateCreatorsField( payload.creators, 'payload' );
1943
+ validateTagsField( payload.tags, 'payload' );
1944
+ validateCollectionsField( payload.collections, 'payload' );
1945
+ validateRelationsField( payload.relations, 'payload' );
1946
+ }
1947
+
1948
+ function validateZoteroUpdatePayload( payload ) {
1949
+ ensurePlainObject( payload, 'zotero update payload' );
1950
+ validateJsonNode( payload, 'payload' );
1951
+ const serialized = JSON.stringify( payload );
1952
+ if ( serialized.length > 200000 ) {
1953
+ throw new Error( 'zotero update payload is too large' );
1954
+ }
1955
+ const blocked = [ 'key', 'version', 'libraryID', 'itemType', 'links', 'meta' ];
1956
+ blocked.forEach( ( key ) => {
1957
+ if ( Object.prototype.hasOwnProperty.call( payload, key ) ) {
1958
+ throw new Error( `zotero update payload cannot include reserved field: ${ key }` );
1959
+ }
1960
+ } );
1961
+ if ( Object.keys( payload ).length === 0 ) {
1962
+ throw new Error( 'zotero update payload must update at least one field' );
1963
+ }
1964
+ validateCreatorsField( payload.creators, 'payload' );
1965
+ validateTagsField( payload.tags, 'payload' );
1966
+ validateCollectionsField( payload.collections, 'payload' );
1967
+ validateRelationsField( payload.relations, 'payload' );
1968
+ }
1969
+
1970
+ async function zoteroApiWriteRequest( auth, method, pathname, payload, extraHeaders = {} ) {
1971
+ const url = buildZoteroApiUrl( auth, pathname );
1972
+ const headers = {
1973
+ Accept: 'application/json, text/plain;q=0.9, */*;q=0.1',
1974
+ 'Content-Type': 'application/json',
1975
+ ...extraHeaders
1976
+ };
1977
+ if ( auth.apiKey ) {
1978
+ headers[ 'Zotero-API-Key' ] = auth.apiKey;
1979
+ }
1980
+ const bodyText = payload === undefined ? '' : JSON.stringify( payload );
1981
+ const limiter = new HostRateLimiter( 0 );
1982
+ const response = await requestExternal( url, limiter, {
1983
+ method,
1984
+ headers,
1985
+ bodyText
1986
+ } );
1987
+ return {
1988
+ url,
1989
+ response,
1990
+ body: bodyToText( response )
1991
+ };
1992
+ }
1993
+
1994
+ async function fetchZoteroItemVersion( auth, ref ) {
1995
+ const { response, body } = await zoteroApiRequest(
1996
+ auth,
1997
+ `/${ ref.libraryType }/${ encodeURIComponent( ref.libraryId ) }/items/${ encodeURIComponent( ref.itemKey ) }`,
1998
+ { format: 'json' }
1999
+ );
2000
+ if ( response.statusCode === 404 ) {
2001
+ throw new Error( `Zotero item not found: ${ ref.libraryType }/${ ref.libraryId }/${ ref.itemKey }` );
2002
+ }
2003
+ if ( response.statusCode < 200 || response.statusCode >= 300 ) {
2004
+ throw new Error( `Failed to read item version (${ response.statusCode })` );
2005
+ }
2006
+ const headerVersion = String( response.headers[ 'last-modified-version' ] || '' ).trim();
2007
+ if ( headerVersion ) {
2008
+ return headerVersion;
2009
+ }
2010
+ try {
2011
+ const parsed = JSON.parse( body );
2012
+ const candidate = parsed && parsed.data && parsed.data.version;
2013
+ return candidate ? String( candidate ) : '';
2014
+ } catch ( error ) {
2015
+ return '';
2016
+ }
2017
+ }
2018
+
2019
+ async function confirmDelete(ref, options) {
2020
+ if ( options.yes ) {
2021
+ return true;
2022
+ }
2023
+ if ( !process.stdin.isTTY ) {
2024
+ throw new Error( 'Delete requires interactive confirmation. Re-run with -y/--yes in non-interactive mode.' );
2025
+ }
2026
+ const prompt = `Delete Zotero item ${ ref.libraryType }/${ ref.libraryId }/${ ref.itemKey }? (yes/no): `;
2027
+ return new Promise( ( resolve ) => {
2028
+ const rl = readline.createInterface( {
2029
+ input: process.stdin,
2030
+ output: process.stdout
2031
+ } );
2032
+ rl.question( prompt, ( answer ) => {
2033
+ rl.close();
2034
+ resolve( String( answer || '' ).trim().toLowerCase() === 'yes' );
2035
+ } );
2036
+ } );
2037
+ }
2038
+
2039
+ async function fetchZoteroKeyInfo( auth ) {
2040
+ if ( !auth.apiKey ) {
2041
+ throw new Error( 'Missing Zotero API key. Pass --api-key or set ZOTERO_API_KEY.' );
2042
+ }
2043
+ const { url, response, body } = await zoteroApiRequest( auth, '/keys/current', { format: 'json' } );
2044
+ if ( response.statusCode === 403 ) {
2045
+ throw new Error( 'Zotero key check failed with 403. Verify API key and permissions.' );
2046
+ }
2047
+ if ( response.statusCode < 200 || response.statusCode >= 300 ) {
2048
+ throw new Error( `Zotero key check failed (${ response.statusCode }) at ${ url }` );
2049
+ }
2050
+ let parsed;
2051
+ try {
2052
+ parsed = JSON.parse( body );
2053
+ } catch ( error ) {
2054
+ throw new Error( `Invalid JSON from Zotero key check: ${ error.message }` );
2055
+ }
2056
+ const userID = String( parsed && parsed.userID || '' ).trim();
2057
+ const username = String( parsed && parsed.username || '' ).trim();
2058
+ return { userID, username, raw: parsed };
2059
+ }
2060
+
2061
+ async function runZoteroLogin( options ) {
2062
+ const auth = mergeZoteroAuth( options );
2063
+ if ( !auth.apiKey ) {
2064
+ throw new Error( 'Missing Zotero API key. Pass --api-key or set ZOTERO_API_KEY.' );
2065
+ }
2066
+ // Fail-safe: if user-id is omitted, infer it from the API key.
2067
+ if ( auth.libraryType === 'users' && !auth.libraryId ) {
2068
+ const keyInfo = await fetchZoteroKeyInfo( auth );
2069
+ if ( !keyInfo.userID ) {
2070
+ throw new Error( 'Could not infer user-id from Zotero API key. Pass --user-id explicitly.' );
2071
+ }
2072
+ auth.userId = keyInfo.userID;
2073
+ auth.libraryId = keyInfo.userID;
2074
+ }
2075
+ requireZoteroLibrary( auth );
2076
+ saveZoteroAuth( {
2077
+ apiBase: auth.apiBase,
2078
+ userId: auth.userId,
2079
+ apiKey: auth.apiKey,
2080
+ libraryType: auth.libraryType,
2081
+ libraryId: auth.libraryId,
2082
+ savedAt: new Date().toISOString()
2083
+ } );
2084
+ if ( options.json ) {
2085
+ jsonOut( { ok: true, command: 'zotero', stage: 'login', auth: stripToSafeAuthView( auth ) } );
2086
+ return;
2087
+ }
2088
+ process.stdout.write( `zotero login saved (${ auth.libraryType }/${ auth.libraryId })\n` );
2089
+ }
2090
+
2091
+ async function runZoteroWhoami( options ) {
2092
+ const auth = mergeZoteroAuth( options );
2093
+ const keyInfo = await fetchZoteroKeyInfo( auth );
2094
+ const output = {
2095
+ api_base: auth.apiBase,
2096
+ user_id: keyInfo.userID || null,
2097
+ username: keyInfo.username || null
2098
+ };
2099
+ if ( options.json ) {
2100
+ jsonOut( { ok: true, command: 'zotero', stage: 'whoami', ...output } );
2101
+ return output;
2102
+ }
2103
+ process.stdout.write( `${ JSON.stringify( output, null, 2 ) }\n` );
2104
+ return output;
2105
+ }
2106
+
2107
+ async function runZoteroLogout( options ) {
2108
+ clearZoteroAuth();
2109
+ if ( options.json ) {
2110
+ jsonOut( { ok: true, command: 'zotero', stage: 'logout' } );
2111
+ return;
2112
+ }
2113
+ process.stdout.write( 'zotero login cleared\n' );
2114
+ }
2115
+
2116
+ async function runZoteroQuery( query, options ) {
2117
+ const auth = mergeZoteroAuth( options );
2118
+ requireZoteroLibrary( auth );
2119
+ const q = String( query || '' ).trim();
2120
+ if ( !q ) {
2121
+ throw new Error( 'Missing query text for zotero query' );
2122
+ }
2123
+ const limit = Math.max( 1, Math.min( 100, Number.isFinite( options.limit ) ? options.limit : 20 ) );
2124
+ const { url, response, body } = await zoteroApiRequest(
2125
+ auth,
2126
+ `/${ auth.libraryType }/${ encodeURIComponent( auth.libraryId ) }/items`,
2127
+ {
2128
+ q,
2129
+ format: 'json',
2130
+ include: 'data',
2131
+ limit
2132
+ }
2133
+ );
2134
+ if ( response.statusCode < 200 || response.statusCode >= 300 ) {
2135
+ throw new Error( `Zotero query failed (${ response.statusCode }) at ${ url }` );
2136
+ }
2137
+ const rows = parseZoteroJsonArray( body, url ).map( ( item ) => ( {
2138
+ key: item && item.key,
2139
+ title: item && item.data && item.data.title,
2140
+ itemType: item && item.data && item.data.itemType,
2141
+ date: item && item.data && item.data.date,
2142
+ DOI: item && item.data && item.data.DOI
2143
+ } ) );
2144
+ if ( options.json ) {
2145
+ jsonOut( {
2146
+ ok: true,
2147
+ command: 'zotero',
2148
+ stage: 'query',
2149
+ query: q,
2150
+ count: rows.length,
2151
+ results: rows
2152
+ } );
2153
+ return rows;
2154
+ }
2155
+ process.stdout.write( `${ JSON.stringify( rows, null, 2 ) }\n` );
2156
+ return rows;
2157
+ }
2158
+
2159
+ async function runZoteroDump( options ) {
2160
+ const auth = mergeZoteroAuth( options );
2161
+ requireZoteroLibrary( auth );
2162
+ const limit = Math.max( 1, Math.min( 100, Number.isFinite( options.limit ) ? options.limit : 50 ) );
2163
+ const { url, response, body } = await zoteroApiRequest(
2164
+ auth,
2165
+ `/${ auth.libraryType }/${ encodeURIComponent( auth.libraryId ) }/items`,
2166
+ {
2167
+ format: 'json',
2168
+ include: 'data',
2169
+ limit
2170
+ }
2171
+ );
2172
+ if ( response.statusCode < 200 || response.statusCode >= 300 ) {
2173
+ throw new Error( `Zotero dump failed (${ response.statusCode }) at ${ url }` );
2174
+ }
2175
+ const rows = parseZoteroJsonArray( body, url );
2176
+ if ( options.json ) {
2177
+ jsonOut( {
2178
+ ok: true,
2179
+ command: 'zotero',
2180
+ stage: 'dump',
2181
+ count: rows.length,
2182
+ items: rows
2183
+ } );
2184
+ return rows;
2185
+ }
2186
+ process.stdout.write( `${ JSON.stringify( rows, null, 2 ) }\n` );
2187
+ return rows;
2188
+ }
2189
+
2190
+ async function runZoteroCite( reference, options ) {
2191
+ const auth = mergeZoteroAuth( options );
2192
+ requireZoteroLibrary( auth );
2193
+ const ref = parseZoteroItemReference( reference, auth );
2194
+ const useAuth = {
2195
+ ...auth,
2196
+ libraryType: ref.libraryType,
2197
+ libraryId: ref.libraryId
2198
+ };
2199
+ const { url, response, body } = await zoteroApiRequest(
2200
+ useAuth,
2201
+ `/${ ref.libraryType }/${ encodeURIComponent( ref.libraryId ) }/items/${ encodeURIComponent( ref.itemKey ) }`,
2202
+ {
2203
+ format: 'bibtex'
2204
+ }
2205
+ );
2206
+ if ( response.statusCode === 403 ) {
2207
+ throw new Error( 'Zotero cite failed with 403. Check API key scope and library permissions.' );
2208
+ }
2209
+ if ( response.statusCode === 404 ) {
2210
+ throw new Error( `Zotero item not found: ${ ref.libraryType }/${ ref.libraryId }/${ ref.itemKey }` );
2211
+ }
2212
+ if ( response.statusCode < 200 || response.statusCode >= 300 ) {
2213
+ throw new Error( `Zotero cite failed (${ response.statusCode }) at ${ url }` );
2214
+ }
2215
+ if ( options.json ) {
2216
+ jsonOut( {
2217
+ ok: true,
2218
+ command: 'zotero',
2219
+ stage: 'cite',
2220
+ reference,
2221
+ library_type: ref.libraryType,
2222
+ library_id: ref.libraryId,
2223
+ item_key: ref.itemKey,
2224
+ bibtex: body
2225
+ } );
2226
+ return body;
2227
+ }
2228
+ if ( !options.silent ) {
2229
+ process.stdout.write( body );
2230
+ if ( !body.endsWith( '\n' ) ) {
2231
+ process.stdout.write( '\n' );
2232
+ }
2233
+ }
2234
+ return body;
2235
+ }
2236
+
2237
+ function escapeHtml( text ) {
2238
+ return String( text )
2239
+ .replace( /&/g, '&amp;' )
2240
+ .replace( /</g, '&lt;' )
2241
+ .replace( />/g, '&gt;' )
2242
+ .replace( /"/g, '&quot;' )
2243
+ .replace( /'/g, '&#39;' );
2244
+ }
2245
+
2246
+ function normalizeNoteContent( noteInput ) {
2247
+ let raw = String( noteInput || '' ).trim();
2248
+ if ( !raw ) {
2249
+ throw new Error( 'Note content cannot be empty' );
2250
+ }
2251
+ if ( raw.startsWith( '@' ) ) {
2252
+ const filePath = path.resolve( raw.slice( 1 ) );
2253
+ if ( !fileExists( filePath ) ) {
2254
+ throw new Error( `Note file not found: ${ filePath }` );
2255
+ }
2256
+ raw = fs.readFileSync( filePath, 'utf8' ).trim();
2257
+ }
2258
+ if ( !raw ) {
2259
+ throw new Error( 'Note content cannot be empty' );
2260
+ }
2261
+ const normalized = /<[^>]+>/.test( raw ) ?
2262
+ raw :
2263
+ `<p>${ escapeHtml( raw ).replace( /\r?\n/g, '<br/>' ) }</p>`;
2264
+ if ( normalized.length > 200000 ) {
2265
+ throw new Error( 'Note content is too large' );
2266
+ }
2267
+ if ( /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/.test( normalized ) ) {
2268
+ throw new Error( 'Note content contains unsupported control characters' );
2269
+ }
2270
+ return normalized;
2271
+ }
2272
+
2273
+ function summarizeNote( html ) {
2274
+ return htmlToPlainText( String( html || '' ) ).slice( 0, 160 );
2275
+ }
2276
+
2277
+ async function runZoteroAddPayload( payload, options ) {
2278
+ const auth = mergeZoteroAuth( options );
2279
+ requireZoteroLibrary( auth );
2280
+ if ( !auth.apiKey ) {
2281
+ throw new Error( 'zotero add requires API key with write permission' );
2282
+ }
2283
+ validateZoteroAddPayload( payload );
2284
+ if ( isDryRun( options, auth ) ) {
2285
+ const preview = {
2286
+ dry_run: true,
2287
+ action: 'zotero-add',
2288
+ library_type: auth.libraryType,
2289
+ library_id: auth.libraryId,
2290
+ itemType: payload.itemType,
2291
+ title: payload.title || ''
2292
+ };
2293
+ if ( options.json ) {
2294
+ jsonOut( { ok: true, command: 'zotero', stage: 'add', ...preview } );
2295
+ }
2296
+ return preview;
2297
+ }
2298
+ const writeToken = crypto.randomBytes( 16 ).toString( 'hex' );
2299
+ const { url, response, body } = await zoteroApiWriteRequest(
2300
+ auth,
2301
+ 'POST',
2302
+ `/${ auth.libraryType }/${ encodeURIComponent( auth.libraryId ) }/items`,
2303
+ [ payload ],
2304
+ { 'Zotero-Write-Token': writeToken }
2305
+ );
2306
+ if ( response.statusCode === 412 ) {
2307
+ throw new Error( 'zotero add precondition failed (412). Retry and ensure library is up to date.' );
2308
+ }
2309
+ if ( response.statusCode < 200 || response.statusCode >= 300 ) {
2310
+ throw new Error( `zotero add failed (${ response.statusCode }) at ${ url }: ${ body.slice( 0, 300 ) }` );
2311
+ }
2312
+ let parsed = null;
2313
+ try {
2314
+ parsed = JSON.parse( body );
2315
+ } catch ( error ) {
2316
+ }
2317
+ if ( options.json ) {
2318
+ jsonOut( {
2319
+ ok: true,
2320
+ command: 'zotero',
2321
+ stage: 'add',
2322
+ status_code: response.statusCode,
2323
+ result: parsed || body
2324
+ } );
2325
+ }
2326
+ return parsed || body;
2327
+ }
2328
+
2329
+ async function runZoteroAdd( payloadInput, options ) {
2330
+ const payload = parseJsonInputArg( payloadInput, 'add' );
2331
+ const result = await runZoteroAddPayload( payload, options );
2332
+ if ( options.json ) {
2333
+ return result;
2334
+ }
2335
+ const body = typeof result === 'string' ? result : JSON.stringify( result, null, 2 );
2336
+ process.stdout.write( `${ body || 'zotero add ok' }\n` );
2337
+ return result;
2338
+ }
2339
+
2340
+ async function runZoteroDelete( reference, options ) {
2341
+ const auth = mergeZoteroAuth( options );
2342
+ requireZoteroLibrary( auth );
2343
+ if ( !auth.apiKey ) {
2344
+ throw new Error( 'zotero delete requires API key with write permission' );
2345
+ }
2346
+ const ref = parseZoteroItemReference( reference, auth );
2347
+ const useAuth = {
2348
+ ...auth,
2349
+ libraryType: ref.libraryType,
2350
+ libraryId: ref.libraryId
2351
+ };
2352
+ if ( isDryRun( options, auth ) ) {
2353
+ const preview = {
2354
+ ok: true,
2355
+ command: 'zotero',
2356
+ stage: 'delete',
2357
+ dry_run: true,
2358
+ library_type: ref.libraryType,
2359
+ library_id: ref.libraryId,
2360
+ item_key: ref.itemKey
2361
+ };
2362
+ if ( options.json ) {
2363
+ jsonOut( preview );
2364
+ } else {
2365
+ process.stdout.write( `[dry-run] delete ${ ref.libraryType }/${ ref.libraryId }/${ ref.itemKey }\n` );
2366
+ }
2367
+ return;
2368
+ }
2369
+ const confirmed = await confirmDelete( ref, options );
2370
+ if ( !confirmed ) {
2371
+ if ( options.json ) {
2372
+ jsonOut( {
2373
+ ok: false,
2374
+ command: 'zotero',
2375
+ stage: 'delete',
2376
+ error: 'Delete cancelled by user'
2377
+ } );
2378
+ return;
2379
+ }
2380
+ process.stdout.write( 'delete cancelled\n' );
2381
+ return;
2382
+ }
2383
+ const currentVersion = await fetchZoteroItemVersion( useAuth, ref );
2384
+ if ( !currentVersion ) {
2385
+ throw new Error( 'Could not determine current item version for safe delete' );
2386
+ }
2387
+ const { url, response, body } = await zoteroApiWriteRequest(
2388
+ useAuth,
2389
+ 'DELETE',
2390
+ `/${ ref.libraryType }/${ encodeURIComponent( ref.libraryId ) }/items/${ encodeURIComponent( ref.itemKey ) }`,
2391
+ undefined,
2392
+ { 'If-Unmodified-Since-Version': currentVersion }
2393
+ );
2394
+ if ( response.statusCode === 412 ) {
2395
+ throw new Error( 'zotero delete rejected: item changed remotely (412)' );
2396
+ }
2397
+ if ( response.statusCode < 200 || response.statusCode >= 299 ) {
2398
+ throw new Error( `zotero delete failed (${ response.statusCode }) at ${ url }: ${ body.slice( 0, 300 ) }` );
2399
+ }
2400
+ if ( options.json ) {
2401
+ jsonOut( {
2402
+ ok: true,
2403
+ command: 'zotero',
2404
+ stage: 'delete',
2405
+ library_type: ref.libraryType,
2406
+ library_id: ref.libraryId,
2407
+ item_key: ref.itemKey
2408
+ } );
2409
+ return;
2410
+ }
2411
+ process.stdout.write( `deleted ${ ref.libraryType }/${ ref.libraryId }/${ ref.itemKey }\n` );
2412
+ }
2413
+
2414
+ async function runZoteroUpdate( reference, payloadInput, options ) {
2415
+ const payload = parseJsonInputArg( payloadInput, 'update' );
2416
+ return runZoteroUpdatePayload( reference, payload, options );
2417
+ }
2418
+
2419
+ async function runZoteroUpdatePayload( reference, payload, options ) {
2420
+ const auth = mergeZoteroAuth( options );
2421
+ requireZoteroLibrary( auth );
2422
+ if ( !auth.apiKey ) {
2423
+ throw new Error( 'zotero update requires API key with write permission' );
2424
+ }
2425
+ const ref = parseZoteroItemReference( reference, auth );
2426
+ const useAuth = {
2427
+ ...auth,
2428
+ libraryType: ref.libraryType,
2429
+ libraryId: ref.libraryId
2430
+ };
2431
+ validateZoteroUpdatePayload( payload );
2432
+ if ( isDryRun( options, auth ) ) {
2433
+ const preview = {
2434
+ ok: true,
2435
+ command: 'zotero',
2436
+ stage: 'update',
2437
+ dry_run: true,
2438
+ library_type: ref.libraryType,
2439
+ library_id: ref.libraryId,
2440
+ item_key: ref.itemKey,
2441
+ changed_fields: Object.keys( payload )
2442
+ };
2443
+ if ( options.json ) {
2444
+ jsonOut( preview );
2445
+ } else {
2446
+ process.stdout.write( `[dry-run] update ${ ref.itemKey } fields=${ Object.keys( payload ).join( ',' ) }\n` );
2447
+ }
2448
+ return true;
2449
+ }
2450
+ const currentVersion = await fetchZoteroItemVersion( useAuth, ref );
2451
+ if ( !currentVersion ) {
2452
+ throw new Error( 'Could not determine current item version for safe update' );
2453
+ }
2454
+ const { url, response, body } = await zoteroApiWriteRequest(
2455
+ useAuth,
2456
+ 'PATCH',
2457
+ `/${ ref.libraryType }/${ encodeURIComponent( ref.libraryId ) }/items/${ encodeURIComponent( ref.itemKey ) }`,
2458
+ payload,
2459
+ { 'If-Unmodified-Since-Version': currentVersion }
2460
+ );
2461
+ if ( response.statusCode === 412 ) {
2462
+ throw new Error( 'zotero update rejected: item changed remotely (412)' );
2463
+ }
2464
+ if ( response.statusCode < 200 || response.statusCode >= 300 ) {
2465
+ throw new Error( `zotero update failed (${ response.statusCode }) at ${ url }: ${ body.slice( 0, 300 ) }` );
2466
+ }
2467
+ if ( options.json ) {
2468
+ jsonOut( {
2469
+ ok: true,
2470
+ command: 'zotero',
2471
+ stage: 'update',
2472
+ library_type: ref.libraryType,
2473
+ library_id: ref.libraryId,
2474
+ item_key: ref.itemKey
2475
+ } );
2476
+ return true;
2477
+ }
2478
+ process.stdout.write( `updated ${ ref.libraryType }/${ ref.libraryId }/${ ref.itemKey }\n` );
2479
+ return true;
2480
+ }
2481
+
2482
+ async function runZoteroNoteAdd( parentReference, noteInput, options ) {
2483
+ const auth = mergeZoteroAuth( options );
2484
+ requireZoteroLibrary( auth );
2485
+ const parent = parseZoteroItemReference( parentReference, auth );
2486
+ const payload = {
2487
+ itemType: 'note',
2488
+ parentItem: parent.itemKey,
2489
+ note: normalizeNoteContent( noteInput )
2490
+ };
2491
+ const result = await runZoteroAddPayload( payload, {
2492
+ ...options,
2493
+ json: false
2494
+ } );
2495
+ if ( options.json ) {
2496
+ jsonOut( {
2497
+ ok: true,
2498
+ command: 'zotero',
2499
+ stage: 'note-add',
2500
+ parent_item: parent.itemKey,
2501
+ result
2502
+ } );
2503
+ return result;
2504
+ }
2505
+ process.stdout.write( `note added under ${ parent.itemKey }\n` );
2506
+ return result;
2507
+ }
2508
+
2509
+ async function runZoteroNoteList( parentReference, options ) {
2510
+ const auth = mergeZoteroAuth( options );
2511
+ requireZoteroLibrary( auth );
2512
+ const parent = parseZoteroItemReference( parentReference, auth );
2513
+ const useAuth = {
2514
+ ...auth,
2515
+ libraryType: parent.libraryType,
2516
+ libraryId: parent.libraryId
2517
+ };
2518
+ const limit = Math.max( 1, Math.min( 100, Number.isFinite( options.limit ) ? options.limit : 20 ) );
2519
+ const { url, response, body } = await zoteroApiRequest(
2520
+ useAuth,
2521
+ `/${ parent.libraryType }/${ encodeURIComponent( parent.libraryId ) }/items/${ encodeURIComponent( parent.itemKey ) }/children`,
2522
+ {
2523
+ format: 'json',
2524
+ include: 'data',
2525
+ limit
2526
+ }
2527
+ );
2528
+ if ( response.statusCode < 200 || response.statusCode >= 300 ) {
2529
+ throw new Error( `zotero note list failed (${ response.statusCode }) at ${ url }` );
2530
+ }
2531
+ const rows = parseZoteroJsonArray( body, url )
2532
+ .filter( ( item ) => item && item.data && item.data.itemType === 'note' )
2533
+ .map( ( item ) => ( {
2534
+ key: item.key,
2535
+ parentItem: item.data.parentItem || parent.itemKey,
2536
+ preview: summarizeNote( item.data.note || '' ),
2537
+ dateModified: item.data.dateModified || ''
2538
+ } ) );
2539
+ if ( options.json ) {
2540
+ jsonOut( {
2541
+ ok: true,
2542
+ command: 'zotero',
2543
+ stage: 'note-list',
2544
+ parent_item: parent.itemKey,
2545
+ count: rows.length,
2546
+ notes: rows
2547
+ } );
2548
+ return rows;
2549
+ }
2550
+ process.stdout.write( `${ JSON.stringify( rows, null, 2 ) }\n` );
2551
+ return rows;
2552
+ }
2553
+
2554
+ async function runZoteroNoteSearch( query, options ) {
2555
+ const auth = mergeZoteroAuth( options );
2556
+ requireZoteroLibrary( auth );
2557
+ const q = String( query || '' ).trim();
2558
+ if ( !q ) {
2559
+ throw new Error( 'zotero note search requires query text' );
2560
+ }
2561
+ const limit = Math.max( 1, Math.min( 100, Number.isFinite( options.limit ) ? options.limit : 20 ) );
2562
+ let rows = [];
2563
+
2564
+ if ( options.parent ) {
2565
+ const parent = parseZoteroItemReference( options.parent, auth );
2566
+ const useAuth = {
2567
+ ...auth,
2568
+ libraryType: parent.libraryType,
2569
+ libraryId: parent.libraryId
2570
+ };
2571
+ const { url, response, body } = await zoteroApiRequest(
2572
+ useAuth,
2573
+ `/${ parent.libraryType }/${ encodeURIComponent( parent.libraryId ) }/items/${ encodeURIComponent( parent.itemKey ) }/children`,
2574
+ {
2575
+ format: 'json',
2576
+ include: 'data',
2577
+ limit: 100
2578
+ }
2579
+ );
2580
+ if ( response.statusCode < 200 || response.statusCode >= 300 ) {
2581
+ throw new Error( `zotero note search failed (${ response.statusCode }) at ${ url }` );
2582
+ }
2583
+ const queryLower = q.toLowerCase();
2584
+ rows = parseZoteroJsonArray( body, url )
2585
+ .filter( ( item ) => item && item.data && item.data.itemType === 'note' )
2586
+ .filter( ( item ) => htmlToPlainText( item.data.note || '' ).toLowerCase().includes( queryLower ) )
2587
+ .slice( 0, limit )
2588
+ .map( ( item ) => ( {
2589
+ key: item.key,
2590
+ parentItem: item.data.parentItem || parent.itemKey,
2591
+ preview: summarizeNote( item.data.note || '' ),
2592
+ dateModified: item.data.dateModified || ''
2593
+ } ) );
2594
+ } else {
2595
+ const { url, response, body } = await zoteroApiRequest(
2596
+ auth,
2597
+ `/${ auth.libraryType }/${ encodeURIComponent( auth.libraryId ) }/items`,
2598
+ {
2599
+ q,
2600
+ itemType: 'note',
2601
+ format: 'json',
2602
+ include: 'data',
2603
+ limit
2604
+ }
2605
+ );
2606
+ if ( response.statusCode < 200 || response.statusCode >= 300 ) {
2607
+ throw new Error( `zotero note search failed (${ response.statusCode }) at ${ url }` );
2608
+ }
2609
+ rows = parseZoteroJsonArray( body, url )
2610
+ .filter( ( item ) => item && item.data && item.data.itemType === 'note' )
2611
+ .map( ( item ) => ( {
2612
+ key: item.key,
2613
+ parentItem: item.data.parentItem || '',
2614
+ preview: summarizeNote( item.data.note || '' ),
2615
+ dateModified: item.data.dateModified || ''
2616
+ } ) );
2617
+ }
2618
+
2619
+ if ( options.json ) {
2620
+ jsonOut( {
2621
+ ok: true,
2622
+ command: 'zotero',
2623
+ stage: 'note-search',
2624
+ query: q,
2625
+ parent: options.parent || null,
2626
+ count: rows.length,
2627
+ notes: rows
2628
+ } );
2629
+ return rows;
2630
+ }
2631
+ process.stdout.write( `${ JSON.stringify( rows, null, 2 ) }\n` );
2632
+ return rows;
2633
+ }
2634
+
2635
+ async function runZoteroNoteUpdate( noteReference, noteInput, options ) {
2636
+ const payload = {
2637
+ note: normalizeNoteContent( noteInput )
2638
+ };
2639
+ const done = await runZoteroUpdatePayload( noteReference, payload, {
2640
+ ...options,
2641
+ json: false
2642
+ } );
2643
+ if ( options.json ) {
2644
+ jsonOut( {
2645
+ ok: true,
2646
+ command: 'zotero',
2647
+ stage: 'note-update',
2648
+ reference: noteReference
2649
+ } );
2650
+ return done;
2651
+ }
2652
+ return done;
2653
+ }
2654
+
2655
+ async function runZoteroNoteDelete( noteReference, options ) {
2656
+ const done = await runZoteroDelete( noteReference, options );
2657
+ return done;
2658
+ }
2659
+
2660
+ async function fetchZoteroItems(auth, query = {}) {
2661
+ const limit = Math.max( 1, Math.min( 100, Number.isFinite( query.limit ) ? query.limit : 100 ) );
2662
+ const start = Math.max( 0, Number.isFinite( query.start ) ? query.start : 0 );
2663
+ const { url, response, body } = await zoteroApiRequest(
2664
+ auth,
2665
+ `/${ auth.libraryType }/${ encodeURIComponent( auth.libraryId ) }/items`,
2666
+ {
2667
+ format: 'json',
2668
+ include: 'data',
2669
+ limit,
2670
+ start,
2671
+ q: query.q || '',
2672
+ itemType: query.itemType || ''
2673
+ }
2674
+ );
2675
+ if ( response.statusCode < 200 || response.statusCode >= 300 ) {
2676
+ throw new Error( `Zotero items fetch failed (${ response.statusCode }) at ${ url }` );
2677
+ }
2678
+ return parseZoteroJsonArray( body, url );
2679
+ }
2680
+
2681
+ async function fetchAllZoteroItems(auth, query = {}) {
2682
+ const totalLimit = Math.max( 1, Math.min( 1000, Number.isFinite( query.totalLimit ) ? query.totalLimit : 300 ) );
2683
+ let start = 0;
2684
+ const chunk = Math.min( 100, totalLimit );
2685
+ let rows = [];
2686
+ while ( rows.length < totalLimit ) {
2687
+ const page = await fetchZoteroItems( auth, {
2688
+ ...query,
2689
+ start,
2690
+ limit: Math.min( chunk, totalLimit - rows.length )
2691
+ } );
2692
+ rows = rows.concat( page );
2693
+ if ( page.length < chunk ) {
2694
+ break;
2695
+ }
2696
+ start += page.length;
2697
+ }
2698
+ return rows;
2699
+ }
2700
+
2701
+ function normalizeDoiLoose(raw) {
2702
+ return String( raw || '' )
2703
+ .trim()
2704
+ .toLowerCase()
2705
+ .replace( /^https?:\/\/(?:dx\.)?doi\.org\//, '' )
2706
+ .replace( /\s+/g, '' );
2707
+ }
2708
+
2709
+ function makeCitationKeyFromItem( itemData ) {
2710
+ const creators = Array.isArray( itemData.creators ) ? itemData.creators : [];
2711
+ const first = creators[ 0 ] || {};
2712
+ const rawName = first.lastName || first.name || 'item';
2713
+ const author = String( rawName ).toLowerCase().replace( /[^a-z0-9]+/g, '' ).slice( 0, 12 ) || 'item';
2714
+ const yearMatch = String( itemData.date || '' ).match( /\b(19|20)\d{2}\b/ );
2715
+ const year = yearMatch ? yearMatch[ 0 ] : 'nd';
2716
+ const titleToken = String( itemData.title || '' ).toLowerCase()
2717
+ .replace( /[^a-z0-9]+/g, ' ' )
2718
+ .trim()
2719
+ .split( /\s+/ )[ 0 ] || 'work';
2720
+ return `${ author }${ year }${ titleToken }`;
2721
+ }
2722
+
2723
+ function parseExtraMap(extra) {
2724
+ const map = {};
2725
+ const text = String( extra || '' );
2726
+ text.split( /\r?\n/ ).forEach( ( line ) => {
2727
+ const idx = line.indexOf( ':' );
2728
+ if ( idx > 0 ) {
2729
+ const key = line.slice( 0, idx ).trim().toLowerCase();
2730
+ const value = line.slice( idx + 1 ).trim();
2731
+ if ( key ) {
2732
+ map[ key ] = value;
2733
+ }
2734
+ }
2735
+ } );
2736
+ return map;
2737
+ }
2738
+
2739
+ function hasCitationKeyInExtra(extra) {
2740
+ const map = parseExtraMap( extra );
2741
+ return !!( map[ 'citation key' ] || map[ 'citationkey' ] );
2742
+ }
2743
+
2744
+ function withCitationKeyInExtra(extra, key) {
2745
+ const text = String( extra || '' ).trim();
2746
+ const line = `Citation Key: ${ key }`;
2747
+ if ( !text ) {
2748
+ return line;
2749
+ }
2750
+ if ( hasCitationKeyInExtra( text ) ) {
2751
+ return text.replace( /(^|\n)\s*citation\s*key\s*:[^\n]*/i, `$1${ line }` );
2752
+ }
2753
+ return `${ text }\n${ line }`;
2754
+ }
2755
+
2756
+ function itemFingerprint(data) {
2757
+ const doi = normalizeDoiLoose( data.DOI );
2758
+ if ( doi ) {
2759
+ return `doi:${ doi }`;
2760
+ }
2761
+ const title = String( data.title || '' ).toLowerCase().replace( /[^a-z0-9]+/g, ' ' ).trim();
2762
+ const yearMatch = String( data.date || '' ).match( /\b(19|20)\d{2}\b/ );
2763
+ const year = yearMatch ? yearMatch[ 0 ] : '';
2764
+ return title ? `ty:${ title }|${ year }` : '';
2765
+ }
2766
+
2767
+ function markdownEscape(text) {
2768
+ return String( text || '' ).replace( /([\\`*_{}\[\]()#+\-.!|])/g, '\\$1' );
2769
+ }
2770
+
2771
+ function zoteroItemToMarkdown( row ) {
2772
+ const data = row && row.data ? row.data : {};
2773
+ const title = data.title || '(untitled)';
2774
+ const creators = Array.isArray( data.creators ) ? data.creators.map( ( c ) => c.lastName || c.name || '' ).filter( Boolean ).join( ', ' ) : '';
2775
+ const date = data.date || '';
2776
+ const doi = data.DOI || '';
2777
+ const url = data.url || '';
2778
+ const abstract = data.abstractNote || '';
2779
+ let out = `## ${ markdownEscape( title ) }\n`;
2780
+ if ( creators ) out += `- Authors: ${ markdownEscape( creators ) }\n`;
2781
+ if ( date ) out += `- Date: ${ markdownEscape( date ) }\n`;
2782
+ if ( doi ) out += `- DOI: \`${ doi }\`\n`;
2783
+ if ( url ) out += `- URL: ${ url }\n`;
2784
+ if ( abstract ) out += `\n${ markdownEscape( abstract ) }\n`;
2785
+ return out;
2786
+ }
2787
+
2788
+ function extractTextReferences(text) {
2789
+ const raw = String( text || '' );
2790
+ const doiMatches = raw.match( /\b10\.\d{4,9}\/[-._;()/:A-Z0-9]+\b/ig ) || [];
2791
+ const urlMatches = raw.match( /https?:\/\/[^\s"'<>]+/g ) || [];
2792
+ const arxivMatches = raw.match( /\b\d{4}\.\d{4,5}(?:v\d+)?\b/g ) || [];
2793
+ const refs = uniqueKeepOrder( [ ...doiMatches, ...urlMatches, ...arxivMatches ] );
2794
+ return refs.slice( 0, 12 );
2795
+ }
2796
+
2797
+ async function runZoteroSyncCite(options) {
2798
+ const auth = mergeZoteroAuth( options );
2799
+ requireZoteroLibrary( auth );
2800
+ const rows = await fetchAllZoteroItems( auth, { totalLimit: options.limit || 200 } );
2801
+ const plans = [];
2802
+ for ( const item of rows ) {
2803
+ const data = item && item.data ? item.data : {};
2804
+ if ( data.itemType === 'note' || data.itemType === 'attachment' ) {
2805
+ continue;
2806
+ }
2807
+ const hasRefSignal = !!( data.DOI || data.url || data.title );
2808
+ if ( !hasRefSignal || hasCitationKeyInExtra( data.extra ) ) {
2809
+ continue;
2810
+ }
2811
+ const key = makeCitationKeyFromItem( data );
2812
+ plans.push( {
2813
+ itemKey: item.key,
2814
+ title: data.title || '',
2815
+ citationKey: key,
2816
+ patch: { extra: withCitationKeyInExtra( data.extra, key ) }
2817
+ } );
2818
+ }
2819
+ if ( isDryRun( options, auth ) || !options.apply ) {
2820
+ if ( options.json ) {
2821
+ jsonOut( { ok: true, command: 'zotero', stage: 'sync-cite', dry_run: true, count: plans.length, plans } );
2822
+ } else {
2823
+ process.stdout.write( `${ JSON.stringify( plans, null, 2 ) }\n` );
2824
+ }
2825
+ return plans;
2826
+ }
2827
+ const applied = [];
2828
+ for ( const plan of plans ) {
2829
+ await runZoteroUpdatePayload( plan.itemKey, plan.patch, { ...options, json: false, dryRun: false } );
2830
+ applied.push( { itemKey: plan.itemKey, citationKey: plan.citationKey } );
2831
+ }
2832
+ if ( options.json ) {
2833
+ jsonOut( { ok: true, command: 'zotero', stage: 'sync-cite', dry_run: false, applied_count: applied.length, applied } );
2834
+ } else {
2835
+ process.stdout.write( `${ JSON.stringify( applied, null, 2 ) }\n` );
2836
+ }
2837
+ return applied;
2838
+ }
2839
+
2840
+ async function runZoteroDedup(options) {
2841
+ const auth = mergeZoteroAuth( options );
2842
+ requireZoteroLibrary( auth );
2843
+ const rows = await fetchAllZoteroItems( auth, { totalLimit: options.limit || 400 } );
2844
+ const groups = new Map();
2845
+ rows.forEach( ( item ) => {
2846
+ const fp = itemFingerprint( item && item.data || {} );
2847
+ if ( !fp ) {
2848
+ return;
2849
+ }
2850
+ if ( !groups.has( fp ) ) {
2851
+ groups.set( fp, [] );
2852
+ }
2853
+ groups.get( fp ).push( {
2854
+ key: item.key,
2855
+ title: item.data && item.data.title || '',
2856
+ date: item.data && item.data.date || '',
2857
+ DOI: item.data && item.data.DOI || ''
2858
+ } );
2859
+ } );
2860
+ const duplicates = Array.from( groups.entries() )
2861
+ .filter( ( [ , list ] ) => list.length > 1 )
2862
+ .map( ( [ fp, list ] ) => ( {
2863
+ fingerprint: fp,
2864
+ keep: list[ 0 ].key,
2865
+ candidates: list.slice( 1 ).map( ( x ) => x.key ),
2866
+ items: list
2867
+ } ) );
2868
+ if ( options.json ) {
2869
+ jsonOut( { ok: true, command: 'zotero', stage: 'dedup', count: duplicates.length, duplicates } );
2870
+ } else {
2871
+ process.stdout.write( `${ JSON.stringify( duplicates, null, 2 ) }\n` );
2872
+ }
2873
+ return duplicates;
2874
+ }
2875
+
2876
+ async function runZoteroExportMd(options) {
2877
+ const auth = mergeZoteroAuth( options );
2878
+ requireZoteroLibrary( auth );
2879
+ const rows = await fetchAllZoteroItems( auth, { totalLimit: options.limit || 100 } );
2880
+ let md = '# Zotero Export\n\n';
2881
+ rows.forEach( ( row ) => {
2882
+ md += `${ zoteroItemToMarkdown( row ) }\n\n`;
2883
+ } );
2884
+ if ( options.out ) {
2885
+ const outPath = path.resolve( options.out );
2886
+ ensurePdfOutputDir( outPath );
2887
+ fs.writeFileSync( outPath, md );
2888
+ if ( options.json ) {
2889
+ jsonOut( { ok: true, command: 'zotero', stage: 'export-md', out_path: outPath, count: rows.length } );
2890
+ return outPath;
2891
+ }
2892
+ process.stdout.write( `${ outPath }\n` );
2893
+ return outPath;
2894
+ }
2895
+ process.stdout.write( md );
2896
+ return md;
2897
+ }
2898
+
2899
+ async function runZoteroEnrich(options) {
2900
+ const auth = mergeZoteroAuth( options );
2901
+ requireZoteroLibrary( auth );
2902
+ const rows = await fetchAllZoteroItems( auth, { totalLimit: options.limit || 50 } );
2903
+ const plans = [];
2904
+ for ( const item of rows ) {
2905
+ const data = item && item.data ? item.data : {};
2906
+ const query = data.DOI || data.url || data.title;
2907
+ if ( !query ) {
2908
+ continue;
2909
+ }
2910
+ if ( data.abstractNote && data.pages && data.publicationTitle ) {
2911
+ continue;
2912
+ }
2913
+ try {
2914
+ const response = await withRunningServices( ( ctx ) => httpGet(
2915
+ `http://127.0.0.1:${ ctx.citoidPort }/zotero/${ encodeURIComponent( query ) }`
2916
+ ) );
2917
+ const parsed = JSON.parse( response.body );
2918
+ if ( Array.isArray( parsed ) && parsed.length ) {
2919
+ const src = parsed[ 0 ];
2920
+ const patch = {};
2921
+ if ( !data.abstractNote && src.abstractNote ) patch.abstractNote = src.abstractNote;
2922
+ if ( !data.publicationTitle && src.publicationTitle ) patch.publicationTitle = src.publicationTitle;
2923
+ if ( !data.pages && src.pages ) patch.pages = src.pages;
2924
+ if ( !data.volume && src.volume ) patch.volume = src.volume;
2925
+ if ( !data.issue && src.issue ) patch.issue = src.issue;
2926
+ if ( !data.date && src.date ) patch.date = src.date;
2927
+ if ( !data.DOI && src.DOI ) patch.DOI = src.DOI;
2928
+ if ( Object.keys( patch ).length ) {
2929
+ plans.push( { itemKey: item.key, patch } );
2930
+ }
2931
+ }
2932
+ } catch ( error ) {
2933
+ }
2934
+ }
2935
+ if ( isDryRun( options, auth ) || !options.apply ) {
2936
+ if ( options.json ) {
2937
+ jsonOut( { ok: true, command: 'zotero', stage: 'enrich', dry_run: true, count: plans.length, plans } );
2938
+ } else {
2939
+ process.stdout.write( `${ JSON.stringify( plans, null, 2 ) }\n` );
2940
+ }
2941
+ return plans;
2942
+ }
2943
+ const applied = [];
2944
+ for ( const plan of plans ) {
2945
+ await runZoteroUpdatePayload( plan.itemKey, plan.patch, { ...options, json: false, dryRun: false } );
2946
+ applied.push( plan.itemKey );
2947
+ }
2948
+ if ( options.json ) {
2949
+ jsonOut( { ok: true, command: 'zotero', stage: 'enrich', dry_run: false, applied_count: applied.length, applied } );
2950
+ } else {
2951
+ process.stdout.write( `${ JSON.stringify( applied, null, 2 ) }\n` );
2952
+ }
2953
+ return applied;
2954
+ }
2955
+
2956
+ async function runZoteroTemplates(name, options) {
2957
+ const templates = {
2958
+ paper: { itemType: 'journalArticle', title: '', creators: [], publicationTitle: '', date: '', DOI: '', url: '' },
2959
+ book: { itemType: 'book', title: '', creators: [], publisher: '', place: '', date: '', ISBN: '' },
2960
+ webpage: { itemType: 'webpage', title: '', creators: [], websiteTitle: '', url: '', date: '' }
2961
+ };
2962
+ const picked = String( name || '' ).trim().toLowerCase();
2963
+ if ( picked ) {
2964
+ const template = templates[ picked ];
2965
+ if ( !template ) {
2966
+ throw new Error( `Unknown template: ${ picked }` );
2967
+ }
2968
+ if ( options.apply ) {
2969
+ await runZoteroAddPayload( template, options );
2970
+ return template;
2971
+ }
2972
+ process.stdout.write( `${ JSON.stringify( template, null, 2 ) }\n` );
2973
+ return template;
2974
+ }
2975
+ if ( options.json ) {
2976
+ jsonOut( { ok: true, command: 'zotero', stage: 'templates', templates } );
2977
+ return templates;
2978
+ }
2979
+ process.stdout.write( `${ JSON.stringify( templates, null, 2 ) }\n` );
2980
+ return templates;
2981
+ }
2982
+
2983
+ async function runZoteroSafeMode(mode, options) {
2984
+ const auth = mergeZoteroAuth( options );
2985
+ const action = String( mode || '' ).trim().toLowerCase();
2986
+ let safeMode = !!auth.safeMode;
2987
+ if ( action === 'on' ) {
2988
+ safeMode = true;
2989
+ } else if ( action === 'off' ) {
2990
+ safeMode = false;
2991
+ } else if ( action !== 'status' && action !== '' ) {
2992
+ throw new Error( 'safe-mode supports: on | off | status' );
2993
+ }
2994
+ saveZoteroAuth( {
2995
+ apiBase: auth.apiBase,
2996
+ userId: auth.userId,
2997
+ apiKey: auth.apiKey,
2998
+ libraryType: auth.libraryType,
2999
+ libraryId: auth.libraryId,
3000
+ safeMode,
3001
+ savedAt: new Date().toISOString()
3002
+ } );
3003
+ if ( options.json ) {
3004
+ jsonOut( { ok: true, command: 'zotero', stage: 'safe-mode', safe_mode: safeMode } );
3005
+ return safeMode;
3006
+ }
3007
+ process.stdout.write( `safe-mode: ${ safeMode ? 'on' : 'off' }\n` );
3008
+ return safeMode;
3009
+ }
3010
+
3011
+ function mapCrossrefWork(work) {
3012
+ const title = Array.isArray( work.title ) ? work.title[ 0 ] : '';
3013
+ const authors = Array.isArray( work.author ) ? work.author.map( ( a ) => `${ a.given || '' } ${ a.family || '' }`.trim() ).filter( Boolean ) : [];
3014
+ return {
3015
+ doi: work.DOI || '',
3016
+ title: title || '',
3017
+ type: work.type || '',
3018
+ year: work.issued && work.issued[ 'date-parts' ] && work.issued[ 'date-parts' ][ 0 ] ? work.issued[ 'date-parts' ][ 0 ][ 0 ] : null,
3019
+ authors,
3020
+ url: work.URL || '',
3021
+ container_title: Array.isArray( work[ 'container-title' ] ) ? work[ 'container-title' ][ 0 ] || '' : '',
3022
+ publisher: work.publisher || ''
3023
+ };
3024
+ }
3025
+
3026
+ async function runZoteroCrossref(query, options) {
3027
+ const q = String( query || '' ).trim();
3028
+ if ( !q ) {
3029
+ throw new Error( 'crossref requires <doi|query>' );
3030
+ }
3031
+ const limiter = new HostRateLimiter( 0 );
3032
+ let url;
3033
+ if ( /\b10\.\d{4,9}\/[-._;()/:A-Z0-9]+\b/i.test( q ) ) {
3034
+ const doi = normalizeDoi( q.match( /\b10\.\d{4,9}\/[-._;()/:A-Z0-9]+\b/i )[ 0 ] );
3035
+ url = `https://api.crossref.org/works/${ encodeURIComponent( doi ) }`;
3036
+ } else {
3037
+ const limit = Math.max( 1, Math.min( 20, Number.isFinite( options.limit ) ? options.limit : 5 ) );
3038
+ url = `https://api.crossref.org/works?query.bibliographic=${ encodeURIComponent( q ) }&rows=${ limit }`;
3039
+ }
3040
+ const response = await requestExternal( url, limiter, {
3041
+ headers: {
3042
+ Accept: 'application/json'
3043
+ }
3044
+ } );
3045
+ const body = bodyToText( response );
3046
+ if ( response.statusCode < 200 || response.statusCode >= 300 ) {
3047
+ throw new Error( `crossref request failed (${ response.statusCode })` );
3048
+ }
3049
+ const parsed = JSON.parse( body );
3050
+ const message = parsed && parsed.message ? parsed.message : {};
3051
+ let output;
3052
+ if ( message && message.DOI ) {
3053
+ output = mapCrossrefWork( message );
3054
+ } else {
3055
+ const items = Array.isArray( message.items ) ? message.items : [];
3056
+ output = items.map( mapCrossrefWork );
3057
+ }
3058
+ if ( options.json ) {
3059
+ jsonOut( { ok: true, command: 'crossref', stage: 'done', query: q, result: output } );
3060
+ return output;
3061
+ }
3062
+ if ( !options.silent ) {
3063
+ process.stdout.write( `${ JSON.stringify( output, null, 2 ) }\n` );
3064
+ }
3065
+ return output;
3066
+ }
3067
+
3068
+ async function runWmfCitoid(format, query, options) {
3069
+ const outFormat = String( format || '' ).trim();
3070
+ const outQuery = String( query || '' ).trim();
3071
+ if ( !outFormat || !outQuery ) {
3072
+ throw new Error( 'citoid requires <format> and <query>' );
3073
+ }
3074
+ const base = String( options.wmfCitoidBase || defaultWmfCitoidBase ).replace( /\/+$/, '' );
3075
+ const url = `${ base }/${ encodeURIComponent( outFormat ) }/${ encodeURIComponent( outQuery ) }`;
3076
+ const limiter = new HostRateLimiter( 0 );
3077
+ const response = await requestExternal( url, limiter, {
3078
+ headers: {
3079
+ Accept: 'text/plain, application/json;q=0.9',
3080
+ 'User-Agent': process.env.USER_AGENT ||
3081
+ `citeclaw/2.0 (${ process.env.MAILTO || 'example@example.com' })`,
3082
+ 'Api-User-Agent': process.env.USER_AGENT ||
3083
+ `citeclaw/2.0 (${ process.env.MAILTO || 'example@example.com' })`
3084
+ }
3085
+ } );
3086
+ const body = bodyToText( response );
3087
+ if ( response.statusCode < 200 || response.statusCode >= 300 ) {
3088
+ throw new Error( `wmf citoid failed (${ response.statusCode }): ${ body.slice( 0, 220 ) }` );
3089
+ }
3090
+ if ( options.headers ) {
3091
+ process.stdout.write( `HTTP ${ response.statusCode }\n` );
3092
+ Object.entries( response.headers || {} ).forEach( ( [ key, value ] ) => {
3093
+ process.stdout.write( `${ key }: ${ value }\n` );
3094
+ } );
3095
+ process.stdout.write( '\n' );
3096
+ }
3097
+ if ( options.json ) {
3098
+ jsonOut( {
3099
+ ok: true,
3100
+ command: 'citoid',
3101
+ stage: 'done',
3102
+ format: outFormat,
3103
+ query: outQuery,
3104
+ request_url: url,
3105
+ status_code: response.statusCode,
3106
+ headers: response.headers || {},
3107
+ body
3108
+ } );
3109
+ return body;
3110
+ }
3111
+ process.stdout.write( body );
3112
+ if ( !body.endsWith( '\n' ) ) {
3113
+ process.stdout.write( '\n' );
3114
+ }
3115
+ return body;
3116
+ }
3117
+
3118
+ function runWmfCitoidFormats(options) {
3119
+ const formats = [
3120
+ 'bibtex',
3121
+ 'mediawiki',
3122
+ 'mediawiki-basefields',
3123
+ 'zotero'
3124
+ ];
3125
+ if ( options.json ) {
3126
+ jsonOut( {
3127
+ ok: true,
3128
+ command: 'citoid',
3129
+ stage: 'formats',
3130
+ formats,
3131
+ wmf_citoid_base: options.wmfCitoidBase || defaultWmfCitoidBase
3132
+ } );
3133
+ return formats;
3134
+ }
3135
+ process.stdout.write( `${ formats.join( '\n' ) }\n` );
3136
+ return formats;
3137
+ }
3138
+
3139
+ function mapSemanticScholarPaper(p) {
3140
+ const authors = Array.isArray( p.authors ) ? p.authors.map( ( a ) => a.name ).filter( Boolean ) : [];
3141
+ return {
3142
+ paperId: p.paperId || '',
3143
+ title: p.title || '',
3144
+ year: p.year || null,
3145
+ venue: p.venue || '',
3146
+ citationCount: Number.isFinite( p.citationCount ) ? p.citationCount : 0,
3147
+ externalIds: p.externalIds || {},
3148
+ url: p.url || '',
3149
+ authors
3150
+ };
3151
+ }
3152
+
3153
+ function parseJsonOptionArg(raw, label) {
3154
+ if ( !raw ) {
3155
+ return {};
3156
+ }
3157
+ try {
3158
+ const parsed = JSON.parse( raw );
3159
+ if ( parsed && typeof parsed === 'object' && !Array.isArray( parsed ) ) {
3160
+ return parsed;
3161
+ }
3162
+ throw new Error( `${ label } must be JSON object` );
3163
+ } catch ( error ) {
3164
+ throw new Error( `Invalid JSON for ${ label }: ${ error.message }` );
3165
+ }
3166
+ }
3167
+
3168
+ function parseMaybeJsonBody(raw) {
3169
+ const text = String( raw || '' ).trim();
3170
+ if ( !text ) {
3171
+ return undefined;
3172
+ }
3173
+ if ( text.startsWith( '@' ) ) {
3174
+ const filePath = path.resolve( text.slice( 1 ) );
3175
+ if ( !fileExists( filePath ) ) {
3176
+ throw new Error( `Body file not found: ${ filePath }` );
3177
+ }
3178
+ return JSON.parse( fs.readFileSync( filePath, 'utf8' ) );
3179
+ }
3180
+ return JSON.parse( text );
3181
+ }
3182
+
3183
+ function parseIdsInput(raw) {
3184
+ const text = String( raw || '' ).trim();
3185
+ if ( !text ) {
3186
+ return [];
3187
+ }
3188
+ if ( text.startsWith( '@' ) ) {
3189
+ const filePath = path.resolve( text.slice( 1 ) );
3190
+ if ( !fileExists( filePath ) ) {
3191
+ throw new Error( `IDs file not found: ${ filePath }` );
3192
+ }
3193
+ return fs.readFileSync( filePath, 'utf8' )
3194
+ .split( /\r?\n/ )
3195
+ .map( ( x ) => x.trim() )
3196
+ .filter( Boolean );
3197
+ }
3198
+ return text.split( ',' ).map( ( x ) => x.trim() ).filter( Boolean );
3199
+ }
3200
+
3201
+ async function semanticScholarApiCall(pathname, options = {}) {
3202
+ const limiter = new HostRateLimiter( 0 );
3203
+ const headers = {
3204
+ Accept: 'application/json',
3205
+ 'User-Agent': process.env.USER_AGENT || `citeclaw/2.0 (${ process.env.MAILTO || 'example@example.com' })`,
3206
+ ...options.headers
3207
+ };
3208
+ const apiKey = String( options.s2ApiKey || defaultS2ApiKey ).trim();
3209
+ if ( apiKey ) {
3210
+ headers[ 'x-api-key' ] = apiKey;
3211
+ }
3212
+ const base = 'https://api.semanticscholar.org/graph/v1';
3213
+ const urlObj = new URL( `${ base }${ pathname.startsWith( '/' ) ? pathname : `/${ pathname }` }` );
3214
+ const params = options.params || {};
3215
+ Object.entries( params ).forEach( ( [ k, v ] ) => {
3216
+ if ( v !== undefined && v !== null && v !== '' ) {
3217
+ urlObj.searchParams.set( k, String( v ) );
3218
+ }
3219
+ } );
3220
+ const response = await requestExternal( urlObj.toString(), limiter, {
3221
+ method: options.method || 'GET',
3222
+ headers,
3223
+ bodyText: options.body !== undefined ? JSON.stringify( options.body ) : undefined
3224
+ } );
3225
+ const body = bodyToText( response );
3226
+ if ( response.statusCode < 200 || response.statusCode >= 300 ) {
3227
+ throw new Error( `semantic-scholar request failed (${ response.statusCode}): ${ body.slice( 0, 220 ) }` );
3228
+ }
3229
+ return {
3230
+ url: urlObj.toString(),
3231
+ response,
3232
+ parsed: JSON.parse( body )
3233
+ };
3234
+ }
3235
+
3236
+ async function runSemanticScholarLegacy(query, options) {
3237
+ const q = String( query || '' ).trim();
3238
+ if ( !q ) {
3239
+ throw new Error( 'semantic-scholar requires <doi|arxiv|query>' );
3240
+ }
3241
+ const fields = options.fields || 'title,year,venue,citationCount,externalIds,authors,url';
3242
+ const arxivId = normalizeArxivId( q );
3243
+ const doiMatch = q.match( /\b10\.\d{4,9}\/[-._;()/:A-Z0-9]+\b/i );
3244
+ let result;
3245
+ if ( doiMatch ) {
3246
+ const doi = normalizeDoi( doiMatch[ 0 ] );
3247
+ result = await semanticScholarApiCall( `/paper/DOI:${ encodeURIComponent( doi ) }`, {
3248
+ params: { fields },
3249
+ s2ApiKey: options.s2ApiKey
3250
+ } );
3251
+ } else if ( arxivId ) {
3252
+ result = await semanticScholarApiCall( `/paper/ARXIV:${ encodeURIComponent( arxivId ) }`, {
3253
+ params: { fields },
3254
+ s2ApiKey: options.s2ApiKey
3255
+ } );
3256
+ } else {
3257
+ result = await semanticScholarApiCall( '/paper/search', {
3258
+ params: {
3259
+ query: q,
3260
+ limit: Math.max( 1, Math.min( 100, Number.isFinite( options.limit ) ? options.limit : 5 ) ),
3261
+ offset: Math.max( 0, Number.isFinite( options.offset ) ? options.offset : 0 ),
3262
+ fields
3263
+ },
3264
+ s2ApiKey: options.s2ApiKey
3265
+ } );
3266
+ }
3267
+ const output = Array.isArray( result.parsed && result.parsed.data ) ?
3268
+ result.parsed.data.map( mapSemanticScholarPaper ) :
3269
+ mapSemanticScholarPaper( result.parsed );
3270
+ if ( options.json ) {
3271
+ jsonOut( { ok: true, command: 'semantic-scholar', stage: 'legacy', query: q, request_url: result.url, result: output } );
3272
+ return output;
3273
+ }
3274
+ if ( !options.silent ) {
3275
+ process.stdout.write( `${ JSON.stringify( output, null, 2 ) }\n` );
3276
+ }
3277
+ return output;
3278
+ }
3279
+
3280
+ async function runSemanticScholarApi(pathname, options) {
3281
+ const method = String( options.method || 'GET' ).toUpperCase();
3282
+ const extraParams = parseJsonOptionArg( options.params, '--params' );
3283
+ let body;
3284
+ try {
3285
+ body = parseMaybeJsonBody( options.body );
3286
+ } catch ( error ) {
3287
+ throw new Error( `Invalid --body JSON: ${ error.message }` );
3288
+ }
3289
+ const result = await semanticScholarApiCall( pathname, {
3290
+ method,
3291
+ params: extraParams,
3292
+ body,
3293
+ s2ApiKey: options.s2ApiKey
3294
+ } );
3295
+ if ( options.json ) {
3296
+ jsonOut( {
3297
+ ok: true,
3298
+ command: 'semantic-scholar',
3299
+ stage: 'api',
3300
+ method,
3301
+ request_url: result.url,
3302
+ status_code: result.response.statusCode,
3303
+ headers: result.response.headers || {},
3304
+ result: result.parsed
3305
+ } );
3306
+ return result.parsed;
3307
+ }
3308
+ if ( !options.silent ) {
3309
+ process.stdout.write( `${ JSON.stringify( result.parsed, null, 2 ) }\n` );
3310
+ }
3311
+ return result.parsed;
3312
+ }
3313
+
3314
+ async function runSemanticScholarSubcommand(subAction, options) {
3315
+ const action = String( subAction || '' ).trim().toLowerCase();
3316
+ if ( !action || action === 'legacy' ) {
3317
+ return runSemanticScholarLegacy( options.args.join( ' ' ).trim(), options );
3318
+ }
3319
+ if ( action === 'api' ) {
3320
+ const pathname = String( options.args[ 0 ] || '' ).trim();
3321
+ if ( !pathname ) throw new Error( 'semantic-scholar api requires <path>' );
3322
+ return runSemanticScholarApi( pathname, options );
3323
+ }
3324
+ if ( action === 'paper' ) {
3325
+ const paperId = String( options.args[ 0 ] || '' ).trim();
3326
+ if ( !paperId ) throw new Error( 'semantic-scholar paper requires <paperId>' );
3327
+ return runSemanticScholarApi( `/paper/${ paperId }`, {
3328
+ ...options,
3329
+ params: JSON.stringify( { ...parseJsonOptionArg( options.params, '--params' ), fields: options.fields || undefined } )
3330
+ } );
3331
+ }
3332
+ if ( action === 'paper-search' ) {
3333
+ const query = options.args.join( ' ' ).trim();
3334
+ if ( !query ) throw new Error( 'semantic-scholar paper-search requires <query>' );
3335
+ const params = {
3336
+ query,
3337
+ limit: Math.max( 1, Math.min( 100, Number.isFinite( options.limit ) ? options.limit : 10 ) ),
3338
+ offset: Math.max( 0, Number.isFinite( options.offset ) ? options.offset : 0 ),
3339
+ fields: options.fields || undefined,
3340
+ sort: options.sort || undefined,
3341
+ year: options.year || undefined
3342
+ };
3343
+ return runSemanticScholarApi( '/paper/search', { ...options, params: JSON.stringify( params ) } );
3344
+ }
3345
+ if ( action === 'paper-search-bulk' ) {
3346
+ const query = options.args.join( ' ' ).trim();
3347
+ if ( !query ) throw new Error( 'semantic-scholar paper-search-bulk requires <query>' );
3348
+ const params = {
3349
+ query,
3350
+ fields: options.fields || undefined,
3351
+ sort: options.sort || undefined,
3352
+ year: options.year || undefined,
3353
+ token: options.token || undefined,
3354
+ openAccessPdf: options.openAccessPdf || undefined,
3355
+ minCitationCount: options.minCitationCount || undefined,
3356
+ publicationTypes: options.publicationTypes || undefined,
3357
+ venue: options.venue || undefined,
3358
+ fieldsOfStudy: options.fieldsOfStudy || undefined
3359
+ };
3360
+ return runSemanticScholarApi( '/paper/search/bulk', { ...options, params: JSON.stringify( params ) } );
3361
+ }
3362
+ if ( action === 'paper-batch' ) {
3363
+ const ids = parseIdsInput( options.args.join( ' ' ) );
3364
+ if ( !ids.length ) throw new Error( 'semantic-scholar paper-batch requires IDs (csv or @file)' );
3365
+ const params = { fields: options.fields || undefined };
3366
+ return runSemanticScholarApi( '/paper/batch', {
3367
+ ...options,
3368
+ method: 'POST',
3369
+ params: JSON.stringify( params ),
3370
+ body: JSON.stringify( { ids } )
3371
+ } );
3372
+ }
3373
+ if ( action === 'author' ) {
3374
+ const authorId = String( options.args[ 0 ] || '' ).trim();
3375
+ if ( !authorId ) throw new Error( 'semantic-scholar author requires <authorId>' );
3376
+ return runSemanticScholarApi( `/author/${ authorId }`, {
3377
+ ...options,
3378
+ params: JSON.stringify( { fields: options.fields || undefined, ...parseJsonOptionArg( options.params, '--params' ) } )
3379
+ } );
3380
+ }
3381
+ if ( action === 'author-papers' ) {
3382
+ const authorId = String( options.args[ 0 ] || '' ).trim();
3383
+ if ( !authorId ) throw new Error( 'semantic-scholar author-papers requires <authorId>' );
3384
+ return runSemanticScholarApi( `/author/${ authorId }/papers`, {
3385
+ ...options,
3386
+ params: JSON.stringify( {
3387
+ fields: options.fields || undefined,
3388
+ limit: Math.max( 1, Math.min( 100, Number.isFinite( options.limit ) ? options.limit : 20 ) ),
3389
+ offset: Math.max( 0, Number.isFinite( options.offset ) ? options.offset : 0 ),
3390
+ ...parseJsonOptionArg( options.params, '--params' )
3391
+ } )
3392
+ } );
3393
+ }
3394
+ if ( action === 'author-batch' ) {
3395
+ const ids = parseIdsInput( options.args.join( ' ' ) );
3396
+ if ( !ids.length ) throw new Error( 'semantic-scholar author-batch requires IDs (csv or @file)' );
3397
+ return runSemanticScholarApi( '/author/batch', {
3398
+ ...options,
3399
+ method: 'POST',
3400
+ params: JSON.stringify( { fields: options.fields || undefined, ...parseJsonOptionArg( options.params, '--params' ) } ),
3401
+ body: JSON.stringify( { ids } )
3402
+ } );
3403
+ }
3404
+ throw new Error( `Unsupported semantic-scholar action: ${ action }` );
3405
+ }
3406
+
3407
+ async function runZoteroNoteCiteLinks(query, options) {
3408
+ const auth = mergeZoteroAuth( options );
3409
+ requireZoteroLibrary( auth );
3410
+ const q = String( query || '' ).trim();
3411
+ let normalizedNotes;
3412
+ if ( options.parent ) {
3413
+ const parent = parseZoteroItemReference( options.parent, auth );
3414
+ const useAuth = { ...auth, libraryType: parent.libraryType, libraryId: parent.libraryId };
3415
+ const rows = await fetchZoteroItems( useAuth, { limit: Math.max( 1, Math.min( 100, options.limit || 100 ) ) } );
3416
+ normalizedNotes = rows
3417
+ .filter( ( item ) => item && item.data && item.data.itemType === 'note' && item.data.parentItem === parent.itemKey )
3418
+ .map( ( item ) => ( { key: item.key, note: item.data.note || '' } ) );
3419
+ } else {
3420
+ const rows = await fetchAllZoteroItems( auth, { itemType: 'note', totalLimit: options.limit || 100 } );
3421
+ normalizedNotes = rows.map( ( item ) => ( { key: item.key, note: item.data && item.data.note || '' } ) );
3422
+ }
3423
+ const plans = [];
3424
+ for ( const n of normalizedNotes ) {
3425
+ const key = n.key || n.itemKey;
3426
+ const rawNote = n.note || ( n.data && n.data.note ) || '';
3427
+ const refs = extractTextReferences( rawNote ).filter( ( ref ) => !q || ref.toLowerCase().includes( q.toLowerCase() ) );
3428
+ if ( !refs.length ) continue;
3429
+ const bib = [];
3430
+ for ( const ref of refs.slice( 0, 3 ) ) {
3431
+ try {
3432
+ const result = await runCitation( 'bibtex', ref, { json: false, silent: true, cacheTtlSec: options.cacheTtlSec || defaultCacheTtlSec } );
3433
+ bib.push( result.body.trim() );
3434
+ } catch ( error ) {
3435
+ }
3436
+ }
3437
+ if ( !bib.length ) continue;
3438
+ const appendix = `\n\n<p><strong>Auto citations</strong></p>\n<pre>${ escapeHtml( bib.join( '\n\n' ) ) }</pre>`;
3439
+ plans.push( { noteKey: key, patch: { note: `${ rawNote }${ appendix }` }, refs } );
3440
+ }
3441
+ if ( isDryRun( options, auth ) || !options.apply ) {
3442
+ if ( options.json ) jsonOut( { ok: true, command: 'zotero', stage: 'note-cite-links', dry_run: true, count: plans.length, plans } );
3443
+ else process.stdout.write( `${ JSON.stringify( plans, null, 2 ) }\n` );
3444
+ return plans;
3445
+ }
3446
+ const applied = [];
3447
+ for ( const plan of plans ) {
3448
+ await runZoteroUpdatePayload( plan.noteKey, plan.patch, { ...options, json: false, dryRun: false } );
3449
+ applied.push( plan.noteKey );
3450
+ }
3451
+ if ( options.json ) jsonOut( { ok: true, command: 'zotero', stage: 'note-cite-links', dry_run: false, applied_count: applied.length, applied } );
3452
+ else process.stdout.write( `${ JSON.stringify( applied, null, 2 ) }\n` );
3453
+ return applied;
3454
+ }
3455
+
3456
+ async function runZoteroWatch(query, options) {
3457
+ const auth = mergeZoteroAuth( options );
3458
+ requireZoteroLibrary( auth );
3459
+ const q = String( query || '' ).trim();
3460
+ if ( !q ) {
3461
+ throw new Error( 'zotero watch requires query text' );
3462
+ }
3463
+ const intervalMs = Math.max( 5, Number.isFinite( options.intervalSec ) ? options.intervalSec : 60 ) * 1000;
3464
+ const outBibPath = path.resolve( options.outBib || 'zotero-watch.bib' );
3465
+ const seen = new Set();
3466
+ process.stdout.write( `watching query=\"${ q }\" every ${ Math.round( intervalMs / 1000 ) }s -> ${ outBibPath }\n` );
3467
+ while ( true ) {
3468
+ const rows = await fetchZoteroItems( auth, { q, limit: Math.max( 1, Math.min( 100, options.limit || 20 ) ) } );
3469
+ for ( const item of rows ) {
3470
+ if ( seen.has( item.key ) ) continue;
3471
+ seen.add( item.key );
3472
+ try {
3473
+ const bib = await runZoteroCite( item.key, { ...options, json: false, silent: true } );
3474
+ ensurePdfOutputDir( outBibPath );
3475
+ fs.appendFileSync( outBibPath, `${ bib.trim() }\n\n` );
3476
+ process.stdout.write( `[watch] appended ${ item.key }\n` );
3477
+ } catch ( error ) {
3478
+ process.stderr.write( `[watch] cite failed for ${ item.key }: ${ error.message }\n` );
3479
+ }
3480
+ }
3481
+ await sleep( intervalMs );
3482
+ }
3483
+ }
3484
+
3485
+ async function runZoteroNoteCommand( options ) {
3486
+ const action = String( options.args.shift() || '' ).trim().toLowerCase();
3487
+ if ( !action ) {
3488
+ usageZotero( 'note' );
3489
+ return;
3490
+ }
3491
+ if ( action === 'add' ) {
3492
+ const parentRef = String( options.args[ 0 ] || '' ).trim();
3493
+ const noteInput = options.args.slice( 1 ).join( ' ' ).trim();
3494
+ if ( !parentRef || !noteInput ) {
3495
+ throw new Error( 'zotero note add requires <parent-item-key|zotero-url> and <note>' );
3496
+ }
3497
+ await runZoteroNoteAdd( parentRef, noteInput, options );
3498
+ return;
3499
+ }
3500
+ if ( action === 'list' ) {
3501
+ const parentRef = String( options.args[ 0 ] || '' ).trim();
3502
+ if ( !parentRef ) {
3503
+ throw new Error( 'zotero note list requires <parent-item-key|zotero-url>' );
3504
+ }
3505
+ await runZoteroNoteList( parentRef, options );
3506
+ return;
3507
+ }
3508
+ if ( action === 'cite-links' ) {
3509
+ const q = options.args.join( ' ' ).trim();
3510
+ await runZoteroNoteCiteLinks( q, options );
3511
+ return;
3512
+ }
3513
+ if ( action === 'search' ) {
3514
+ const query = options.args.join( ' ' ).trim();
3515
+ if ( !query ) {
3516
+ throw new Error( 'zotero note search requires <text>' );
3517
+ }
3518
+ await runZoteroNoteSearch( query, options );
3519
+ return;
3520
+ }
3521
+ if ( action === 'update' ) {
3522
+ const noteRef = String( options.args[ 0 ] || '' ).trim();
3523
+ const noteInput = options.args.slice( 1 ).join( ' ' ).trim();
3524
+ if ( !noteRef || !noteInput ) {
3525
+ throw new Error( 'zotero note update requires <note-key|zotero-url> and <note>' );
3526
+ }
3527
+ await runZoteroNoteUpdate( noteRef, noteInput, options );
3528
+ return;
3529
+ }
3530
+ if ( action === 'delete' ) {
3531
+ const noteRef = String( options.args[ 0 ] || '' ).trim();
3532
+ if ( !noteRef ) {
3533
+ throw new Error( 'zotero note delete requires <note-key|zotero-url>' );
3534
+ }
3535
+ await runZoteroNoteDelete( noteRef, options );
3536
+ return;
3537
+ }
3538
+ throw new Error( `Unsupported zotero note action: ${ action }` );
3539
+ }
3540
+
3541
+ async function runZoteroCommand( subAction, options ) {
3542
+ const action = String( subAction || '' ).trim().toLowerCase();
3543
+ if ( action === 'login' ) {
3544
+ await runZoteroLogin( options );
3545
+ return;
3546
+ }
3547
+ if ( action === 'logout' ) {
3548
+ await runZoteroLogout( options );
3549
+ return;
3550
+ }
3551
+ if ( action === 'whoami' ) {
3552
+ await runZoteroWhoami( options );
3553
+ return;
3554
+ }
3555
+ if ( action === 'query' ) {
3556
+ const query = options.args.join( ' ' ).trim();
3557
+ await runZoteroQuery( query, options );
3558
+ return;
3559
+ }
3560
+ if ( action === 'dump' ) {
3561
+ await runZoteroDump( options );
3562
+ return;
3563
+ }
3564
+ if ( action === 'cite' ) {
3565
+ const reference = options.args.join( ' ' ).trim();
3566
+ if ( !reference ) {
3567
+ throw new Error( 'zotero cite requires <item-key|zotero-url>' );
3568
+ }
3569
+ await runZoteroCite( reference, options );
3570
+ return;
3571
+ }
3572
+ if ( action === 'add' ) {
3573
+ const payloadInput = options.args.join( ' ' ).trim();
3574
+ await runZoteroAdd( payloadInput, options );
3575
+ return;
3576
+ }
3577
+ if ( action === 'delete' ) {
3578
+ const reference = options.args.join( ' ' ).trim();
3579
+ if ( !reference ) {
3580
+ throw new Error( 'zotero delete requires <item-key|zotero-url>' );
3581
+ }
3582
+ await runZoteroDelete( reference, options );
3583
+ return;
3584
+ }
3585
+ if ( action === 'update' ) {
3586
+ const reference = String( options.args[ 0 ] || '' ).trim();
3587
+ const payloadInput = options.args.slice( 1 ).join( ' ' ).trim();
3588
+ if ( !reference || !payloadInput ) {
3589
+ throw new Error( 'zotero update requires <item-key|zotero-url> and <json-patch>' );
3590
+ }
3591
+ await runZoteroUpdate( reference, payloadInput, options );
3592
+ return;
3593
+ }
3594
+ if ( action === 'note' ) {
3595
+ await runZoteroNoteCommand( options );
3596
+ return;
3597
+ }
3598
+ if ( action === 'sync-cite' ) {
3599
+ await runZoteroSyncCite( options );
3600
+ return;
3601
+ }
3602
+ if ( action === 'dedup' ) {
3603
+ await runZoteroDedup( options );
3604
+ return;
3605
+ }
3606
+ if ( action === 'enrich' ) {
3607
+ await runZoteroEnrich( options );
3608
+ return;
3609
+ }
3610
+ if ( action === 'export' ) {
3611
+ const format = String( options.args.shift() || '' ).trim().toLowerCase();
3612
+ if ( format !== 'md' ) {
3613
+ throw new Error( 'zotero export currently supports only: md' );
3614
+ }
3615
+ await runZoteroExportMd( options );
3616
+ return;
3617
+ }
3618
+ if ( action === 'watch' ) {
3619
+ const query = options.args.join( ' ' ).trim();
3620
+ await runZoteroWatch( query, options );
3621
+ return;
3622
+ }
3623
+ if ( action === 'templates' ) {
3624
+ const name = String( options.args[ 0 ] || '' ).trim();
3625
+ await runZoteroTemplates( name, options );
3626
+ return;
3627
+ }
3628
+ if ( action === 'safe-mode' ) {
3629
+ const mode = String( options.args[ 0 ] || 'status' ).trim();
3630
+ await runZoteroSafeMode( mode, options );
3631
+ return;
3632
+ }
3633
+ throw new Error( `Unsupported zotero action: ${ action }` );
3634
+ }
3635
+
3636
+ function isLikelyPdfUrl( raw ) {
3637
+ try {
3638
+ const parsed = new URL( String( raw || '' ).trim() );
3639
+ if ( parsed.protocol !== 'http:' && parsed.protocol !== 'https:' ) {
3640
+ return false;
3641
+ }
3642
+ return /\.pdf(?:$|[?#])/i.test( parsed.pathname + parsed.search + parsed.hash );
3643
+ } catch ( error ) {
3644
+ return false;
3645
+ }
3646
+ }
3647
+
3648
+ function sanitizeFileName( raw ) {
3649
+ return raw
3650
+ .replace( /[^a-zA-Z0-9._-]+/g, '_' )
3651
+ .replace( /_+/g, '_' )
3652
+ .replace( /^_+|_+$/g, '' )
3653
+ .slice( 0, 120 ) || 'paper';
3654
+ }
3655
+
3656
+ function defaultPdfOutputPath( identifier ) {
3657
+ const marker = sanitizeFileName( identifier.replace( /^https?:\/\//, '' ) );
3658
+ return path.resolve( `${ marker }.pdf` );
3659
+ }
3660
+
3661
+ class HostRateLimiter {
3662
+ constructor( minIntervalMs ) {
3663
+ this.minIntervalMs = Math.max( 0, Number.isFinite( minIntervalMs ) ? minIntervalMs : 0 );
3664
+ this.lastRequestByHost = new Map();
3665
+ }
3666
+
3667
+ async wait( host ) {
3668
+ if ( !host || this.minIntervalMs <= 0 ) {
3669
+ return;
3670
+ }
3671
+ const now = Date.now();
3672
+ const last = this.lastRequestByHost.get( host ) || 0;
3673
+ const waitMs = last + this.minIntervalMs - now;
3674
+ if ( waitMs > 0 ) {
3675
+ await sleep( waitMs );
3676
+ }
3677
+ this.lastRequestByHost.set( host, Date.now() );
3678
+ }
3679
+ }
3680
+
3681
+ // Hard local throttle guard: do not send requests to the same host faster than 1 req/sec.
3682
+ const localHardRateLimiter = new HostRateLimiter( 1000 );
3683
+
3684
+ function requestExternal( urlString, limiter, options = {} ) {
3685
+ const timeoutMs = options.timeoutMs || defaultRequestTimeoutMs;
3686
+ const maxRedirects = options.maxRedirects || 8;
3687
+ const maxBodyBytes = options.maxBodyBytes > 0 ? options.maxBodyBytes : null;
3688
+ const requestBody = options.bodyBuffer ?
3689
+ options.bodyBuffer :
3690
+ ( typeof options.bodyText === 'string' ? Buffer.from( options.bodyText, 'utf8' ) : null );
3691
+
3692
+ return new Promise( ( resolve, reject ) => {
3693
+ const doRequest = async ( nextUrlString, redirectsLeft ) => {
3694
+ let parsed;
3695
+ try {
3696
+ parsed = new URL( nextUrlString );
3697
+ } catch ( error ) {
3698
+ reject( new Error( `Invalid URL: ${ nextUrlString }` ) );
3699
+ return;
3700
+ }
3701
+
3702
+ try {
3703
+ await localHardRateLimiter.wait( parsed.host );
3704
+ if ( limiter && limiter.minIntervalMs > localHardRateLimiter.minIntervalMs ) {
3705
+ await limiter.wait( parsed.host );
3706
+ }
3707
+ } catch ( error ) {
3708
+ reject( error );
3709
+ return;
3710
+ }
3711
+
3712
+ const transport = parsed.protocol === 'https:' ? https : http;
3713
+ const headers = {
3714
+ ...( options.headers || {} )
3715
+ };
3716
+ if ( requestBody && !headers[ 'Content-Length' ] && !headers[ 'content-length' ] ) {
3717
+ headers[ 'Content-Length' ] = String( requestBody.length );
3718
+ }
3719
+ const req = transport.request( nextUrlString, {
3720
+ method: options.method || 'GET',
3721
+ headers
3722
+ }, ( res ) => {
3723
+ const status = res.statusCode || 0;
3724
+ const location = res.headers.location;
3725
+ if ( status >= 300 && status < 400 && location ) {
3726
+ if ( redirectsLeft <= 0 ) {
3727
+ reject( new Error( `Too many redirects for ${ urlString }` ) );
3728
+ return;
3729
+ }
3730
+ const target = new URL( location, nextUrlString ).toString();
3731
+ res.resume();
3732
+ doRequest( target, redirectsLeft - 1 );
3733
+ return;
3734
+ }
3735
+
3736
+ const chunks = [];
3737
+ let keptBytes = 0;
3738
+ res.on( 'data', ( chunk ) => {
3739
+ if ( maxBodyBytes && keptBytes >= maxBodyBytes ) {
3740
+ return;
3741
+ }
3742
+ if ( maxBodyBytes && keptBytes + chunk.length > maxBodyBytes ) {
3743
+ const remain = maxBodyBytes - keptBytes;
3744
+ if ( remain > 0 ) {
3745
+ chunks.push( chunk.slice( 0, remain ) );
3746
+ keptBytes += remain;
3747
+ }
3748
+ return;
3749
+ }
3750
+ chunks.push( chunk );
3751
+ keptBytes += chunk.length;
3752
+ } );
3753
+ res.on( 'end', () => {
3754
+ resolve( {
3755
+ statusCode: status,
3756
+ headers: res.headers,
3757
+ bodyBuffer: Buffer.concat( chunks ),
3758
+ finalUrl: nextUrlString
3759
+ } );
3760
+ } );
3761
+ } );
3762
+
3763
+ req.setTimeout( timeoutMs, () => {
3764
+ req.destroy( new Error( `request timeout after ${ timeoutMs }ms` ) );
3765
+ } );
3766
+ req.on( 'error', reject );
3767
+ if ( requestBody ) {
3768
+ req.write( requestBody );
3769
+ }
3770
+ req.end();
3771
+ };
3772
+
3773
+ doRequest( urlString, maxRedirects );
3774
+ } );
3775
+ }
3776
+
3777
+ function bodyToText( response ) {
3778
+ const contentType = String( response.headers[ 'content-type' ] || '' ).toLowerCase();
3779
+ const isUtf8 = /charset\s*=\s*utf-8/.test( contentType );
3780
+ return isUtf8 ? response.bodyBuffer.toString( 'utf8' ) : response.bodyBuffer.toString();
3781
+ }
3782
+
3783
+ function isPdfResponse( response ) {
3784
+ const contentType = String( response.headers[ 'content-type' ] || '' ).toLowerCase();
3785
+ if ( contentType.includes( 'application/pdf' ) ) {
3786
+ return true;
3787
+ }
3788
+ return response.bodyBuffer.slice( 0, 5 ).toString() === '%PDF-';
3789
+ }
3790
+
3791
+ function extractPdfLinksFromHtml( html, baseUrl ) {
3792
+ const links = [];
3793
+ const hrefRegex = /href\s*=\s*["']([^"']+)["']/ig;
3794
+ let match;
3795
+ while ( ( match = hrefRegex.exec( html ) ) ) {
3796
+ const href = match[ 1 ].trim();
3797
+ const lowerHref = href.toLowerCase();
3798
+ if ( !href || href.startsWith( '#' ) || lowerHref.startsWith( 'mailto:' ) ||
3799
+ lowerHref.startsWith( `javascript${ ':' }` ) ) {
3800
+ continue;
3801
+ }
3802
+ let resolved;
3803
+ try {
3804
+ resolved = new URL( href, baseUrl ).toString();
3805
+ } catch ( error ) {
3806
+ continue;
3807
+ }
3808
+ if ( /\.pdf(?:$|[?#])/i.test( resolved ) || /\/pdf(?:$|[/?#])/i.test( resolved ) ) {
3809
+ links.push( resolved );
3810
+ }
3811
+ }
3812
+ return uniqueKeepOrder( links );
3813
+ }
3814
+
3815
+ function extractHttpLinksFromText( text ) {
3816
+ const matches = text.match( /https?:\/\/[^\s"'<>]+/g ) || [];
3817
+ return uniqueKeepOrder( matches.map( ( item ) => item.trim() ) );
3818
+ }
3819
+
3820
+ function isUsefulResolverLink( link ) {
3821
+ const lower = link.toLowerCase();
3822
+ if ( /\.(?:png|jpg|jpeg|gif|svg|webp|css|js|woff2?|ttf|ico)(?:$|[?#])/.test( lower ) ) {
3823
+ return false;
3824
+ }
3825
+ if ( /(?:\/pdf(?:$|[/?#])|\.pdf(?:$|[?#]))/.test( lower ) ) {
3826
+ return true;
3827
+ }
3828
+ if ( /(?:fulltext|download|article|doi\.org|dx\.doi\.org|arxiv\.org\/(?:abs|pdf))/.test( lower ) ) {
3829
+ return true;
3830
+ }
3831
+ return false;
3832
+ }
3833
+
3834
+ function filterUsefulResolverLinks( links ) {
3835
+ return uniqueKeepOrder( links.filter( isUsefulResolverLink ) );
3836
+ }
3837
+
3838
+ function buildOpenUrlQueries( normalized ) {
3839
+ const common = {
3840
+ url_ver: 'Z39.88-2004',
3841
+ ctx_ver: 'Z39.88-2004',
3842
+ sid: 'citeclaw:fetch-pdf'
3843
+ };
3844
+
3845
+ if ( normalized.type === 'doi' ) {
3846
+ return [ {
3847
+ ...common,
3848
+ genre: 'article',
3849
+ 'rft.doi': normalized.value,
3850
+ rft_id: `info:doi/${ normalized.value }`
3851
+ } ];
3852
+ }
3853
+
3854
+ if ( normalized.type === 'arxiv' ) {
3855
+ const absUrl = `https://arxiv.org/abs/${ normalized.value }`;
3856
+ return [ {
3857
+ ...common,
3858
+ genre: 'preprint',
3859
+ rft_id: absUrl
3860
+ }, {
3861
+ ...common,
3862
+ genre: 'preprint',
3863
+ rft_id: `info:arxiv/${ normalized.value }`
3864
+ } ];
3865
+ }
3866
+
3867
+ return [ {
3868
+ ...common,
3869
+ rft_id: normalized.value
3870
+ } ];
3871
+ }
3872
+
3873
+ function buildOpenUrlRequestUrl( openUrlBase, queryObj ) {
3874
+ const parsedBase = new URL( openUrlBase );
3875
+ const requestUrl = new URL( parsedBase.toString() );
3876
+ Object.entries( queryObj ).forEach( ( [ key, value ] ) => {
3877
+ if ( value !== undefined && value !== null && value !== '' ) {
3878
+ requestUrl.searchParams.set( key, String( value ) );
3879
+ }
3880
+ } );
3881
+ return requestUrl.toString();
3882
+ }
3883
+
3884
+ async function fetchFullPdfResponse( urlString, limiter ) {
3885
+ const response = await requestExternal( urlString, limiter );
3886
+ if ( response.statusCode >= 200 &&
3887
+ response.statusCode < 300 &&
3888
+ isPdfResponse( response ) ) {
3889
+ return response;
3890
+ }
3891
+ throw new Error( `Failed to fetch full PDF: ${ urlString }` );
3892
+ }
3893
+
3894
+ async function probeSingleCandidateForPdf( candidate, limiter ) {
3895
+ let res;
3896
+ try {
3897
+ res = await requestExternal( candidate, limiter, {
3898
+ maxBodyBytes: defaultProbeBodyBytes
3899
+ } );
3900
+ } catch ( error ) {
3901
+ return null;
3902
+ }
3903
+ if ( res.statusCode >= 200 && res.statusCode < 300 && isPdfResponse( res ) ) {
3904
+ const fullPdf = await fetchFullPdfResponse( res.finalUrl, limiter );
3905
+ return { pdfUrl: fullPdf.finalUrl, directResponse: fullPdf };
3906
+ }
3907
+ if ( res.statusCode < 200 || res.statusCode >= 300 ) {
3908
+ return null;
3909
+ }
3910
+
3911
+ const html = bodyToText( res );
3912
+ const nested = extractPdfLinksFromHtml( html, res.finalUrl );
3913
+ for ( const nestedUrl of nested ) {
3914
+ try {
3915
+ const nestedRes = await requestExternal( nestedUrl, limiter );
3916
+ if ( nestedRes.statusCode >= 200 &&
3917
+ nestedRes.statusCode < 300 &&
3918
+ isPdfResponse( nestedRes ) ) {
3919
+ return { pdfUrl: nestedRes.finalUrl, directResponse: nestedRes };
3920
+ }
3921
+ } catch ( error ) {
3922
+ }
3923
+ }
3924
+ return null;
3925
+ }
3926
+
3927
+ async function probeCandidatesForPdf( candidates, limiter, concurrency = defaultFetchConcurrency ) {
3928
+ const uniqueCandidates = uniqueKeepOrder( candidates );
3929
+ const limit = Math.max( 1, Math.min( concurrency || 1, uniqueCandidates.length ) );
3930
+ if ( !uniqueCandidates.length ) {
3931
+ return null;
3932
+ }
3933
+
3934
+ let index = 0;
3935
+ let found = null;
3936
+ const worker = async () => {
3937
+ while ( !found && index < uniqueCandidates.length ) {
3938
+ const current = uniqueCandidates[ index ];
3939
+ index++;
3940
+ const result = await probeSingleCandidateForPdf( current, limiter );
3941
+ if ( result ) {
3942
+ found = result;
3943
+ return;
3944
+ }
3945
+ }
3946
+ };
3947
+
3948
+ const workers = [];
3949
+ for ( let i = 0; i < limit; i++ ) {
3950
+ workers.push( worker() );
3951
+ }
3952
+ await Promise.all( workers );
3953
+ return found;
3954
+ }
3955
+
3956
+ async function resolvePdfFromOpenUrl( normalized, limiter, openUrlBase ) {
3957
+ if ( !openUrlBase ) {
3958
+ throw new Error( 'OPENURL_BASE is not configured' );
3959
+ }
3960
+ const queryOptions = buildOpenUrlQueries( normalized );
3961
+ let allCandidates = [];
3962
+
3963
+ for ( const queryObj of queryOptions ) {
3964
+ const requestUrl = buildOpenUrlRequestUrl( openUrlBase, queryObj );
3965
+ let response;
3966
+ try {
3967
+ response = await requestExternal( requestUrl, limiter, {
3968
+ maxBodyBytes: defaultProbeBodyBytes
3969
+ } );
3970
+ } catch ( error ) {
3971
+ continue;
3972
+ }
3973
+
3974
+ if ( response.statusCode >= 200 &&
3975
+ response.statusCode < 300 &&
3976
+ isPdfResponse( response ) ) {
3977
+ const fullPdf = await fetchFullPdfResponse( response.finalUrl, limiter );
3978
+ return { pdfUrl: fullPdf.finalUrl, directResponse: fullPdf };
3979
+ }
3980
+
3981
+ const text = bodyToText( response );
3982
+ const htmlLinks = extractPdfLinksFromHtml( text, response.finalUrl );
3983
+ const rawLinks = filterUsefulResolverLinks( extractHttpLinksFromText( text ) );
3984
+ allCandidates = allCandidates.concat( [
3985
+ response.finalUrl,
3986
+ ...htmlLinks,
3987
+ ...rawLinks
3988
+ ] );
3989
+ }
3990
+
3991
+ const resolved = await probeCandidatesForPdf( allCandidates, limiter );
3992
+ if ( resolved ) {
3993
+ return resolved;
3994
+ }
3995
+ throw new Error( 'OpenURL resolver did not yield a downloadable PDF' );
3996
+ }
3997
+
3998
+ async function runOpenUrlResolve( identifier, options ) {
3999
+ const normalized = detectIdentifierType( identifier );
4000
+ const limiter = new HostRateLimiter( defaultPdfFetchIntervalMs );
4001
+ const openUrlBase = options.base || defaultOpenUrlBase;
4002
+ if ( !openUrlBase ) {
4003
+ throw new Error( 'Missing OpenURL base. Pass --base or set OPENURL_BASE.' );
4004
+ }
4005
+ const startedAt = Date.now();
4006
+ const cached = await readThroughCache(
4007
+ 'openurl-resolve',
4008
+ { normalized, openUrlBase },
4009
+ options.cacheTtlSec,
4010
+ async () => {
4011
+ const queryOptions = buildOpenUrlQueries( normalized );
4012
+ const outputs = [];
4013
+ for ( const queryObj of queryOptions ) {
4014
+ const requestUrl = buildOpenUrlRequestUrl( openUrlBase, queryObj );
4015
+ let response;
4016
+ try {
4017
+ response = await requestExternal( requestUrl, limiter, {
4018
+ maxBodyBytes: defaultProbeBodyBytes
4019
+ } );
4020
+ } catch ( error ) {
4021
+ outputs.push( {
4022
+ request_url: requestUrl,
4023
+ error: error.message
4024
+ } );
4025
+ continue;
4026
+ }
4027
+
4028
+ const text = bodyToText( response );
4029
+ outputs.push( {
4030
+ request_url: requestUrl,
4031
+ final_url: response.finalUrl,
4032
+ status_code: response.statusCode,
4033
+ is_pdf: isPdfResponse( response ),
4034
+ candidate_links: filterUsefulResolverLinks( [
4035
+ ...extractPdfLinksFromHtml( text, response.finalUrl ),
4036
+ ...extractHttpLinksFromText( text )
4037
+ ] ).slice( 0, 100 )
4038
+ } );
4039
+ }
4040
+ return outputs;
4041
+ }
4042
+ );
4043
+
4044
+ const output = {
4045
+ identifier: normalized,
4046
+ openurl_base: openUrlBase,
4047
+ cache_hit: cached.cacheHit,
4048
+ elapsed_ms: Date.now() - startedAt,
4049
+ stage: 'done',
4050
+ results: cached.value
4051
+ };
4052
+ if ( options.json ) {
4053
+ jsonOut( {
4054
+ ok: true,
4055
+ command: 'openurl-resolve',
4056
+ ...output
4057
+ } );
4058
+ return output;
4059
+ }
4060
+ if ( !options.silent ) {
4061
+ process.stdout.write( `${ JSON.stringify( output, null, 2 ) }\n` );
4062
+ }
4063
+ return output;
4064
+ }
4065
+
4066
+ async function resolvePdfFromDoi( doi, limiter ) {
4067
+ const candidates = [];
4068
+ const doiUrl = `https://doi.org/${ encodeURIComponent( doi ) }`;
4069
+ candidates.push( doiUrl );
4070
+
4071
+ const landing = await requestExternal( doiUrl, limiter, {
4072
+ headers: {
4073
+ Accept: 'text/html,application/xhtml+xml'
4074
+ },
4075
+ maxBodyBytes: defaultProbeBodyBytes
4076
+ } );
4077
+
4078
+ if ( isPdfResponse( landing ) ) {
4079
+ const fullPdf = await fetchFullPdfResponse( landing.finalUrl, limiter );
4080
+ return { pdfUrl: fullPdf.finalUrl, directResponse: fullPdf };
4081
+ }
4082
+
4083
+ if ( landing.statusCode >= 200 && landing.statusCode < 300 ) {
4084
+ const html = bodyToText( landing );
4085
+ const htmlCandidates = extractPdfLinksFromHtml( html, landing.finalUrl );
4086
+ candidates.push( ...htmlCandidates );
4087
+ }
4088
+
4089
+ const email = process.env.UNPAYWALL_EMAIL || process.env.MAILTO;
4090
+ if ( email ) {
4091
+ const unpaywallUrl = `https://api.unpaywall.org/v2/${ encodeURIComponent( doi ) }?email=${ encodeURIComponent( email ) }`;
4092
+ try {
4093
+ const oaRes = await requestExternal( unpaywallUrl, limiter, {
4094
+ maxBodyBytes: defaultProbeBodyBytes
4095
+ } );
4096
+ if ( oaRes.statusCode >= 200 && oaRes.statusCode < 300 ) {
4097
+ const data = JSON.parse( bodyToText( oaRes ) );
4098
+ const urls = [
4099
+ data && data.best_oa_location && data.best_oa_location.url_for_pdf,
4100
+ data && data.best_oa_location && data.best_oa_location.url,
4101
+ ...( Array.isArray( data.oa_locations ) ?
4102
+ data.oa_locations.map( ( x ) => x && ( x.url_for_pdf || x.url ) ) : [] )
4103
+ ].filter( Boolean );
4104
+ candidates.push( ...urls );
4105
+ }
4106
+ } catch ( error ) {
4107
+ }
4108
+ }
4109
+
4110
+ const resolved = await probeCandidatesForPdf( candidates, limiter );
4111
+ if ( resolved ) {
4112
+ return resolved;
4113
+ }
4114
+
4115
+ throw new Error( `Could not find downloadable PDF for DOI: ${ doi }` );
4116
+ }
4117
+
4118
+ async function resolvePdfFromArxiv( arxivId, limiter ) {
4119
+ const pdfUrl = `https://arxiv.org/pdf/${ encodeURIComponent( arxivId ) }.pdf`;
4120
+ const res = await requestExternal( pdfUrl, limiter );
4121
+ if ( res.statusCode >= 200 && res.statusCode < 300 && isPdfResponse( res ) ) {
4122
+ return { pdfUrl: res.finalUrl, directResponse: res };
4123
+ }
4124
+ throw new Error( `Could not fetch arXiv PDF for ${ arxivId }` );
4125
+ }
4126
+
4127
+ async function resolvePdfFromUrl( urlString, limiter ) {
4128
+ const res = await requestExternal( urlString, limiter );
4129
+ if ( res.statusCode >= 200 && res.statusCode < 300 && isPdfResponse( res ) ) {
4130
+ return { pdfUrl: res.finalUrl, directResponse: res };
4131
+ }
4132
+ if ( res.statusCode >= 200 && res.statusCode < 300 ) {
4133
+ const html = bodyToText( res );
4134
+ const links = extractPdfLinksFromHtml( html, res.finalUrl );
4135
+ const resolved = await probeCandidatesForPdf( links, limiter );
4136
+ if ( resolved ) {
4137
+ return resolved;
4138
+ }
4139
+ }
4140
+ throw new Error( `Could not find PDF from URL: ${ urlString }` );
4141
+ }
4142
+
4143
+ function ensurePdfOutputDir( outPath ) {
4144
+ const dir = path.dirname( outPath );
4145
+ if ( !fileExists( dir ) ) {
4146
+ fs.mkdirSync( dir, { recursive: true } );
4147
+ }
4148
+ }
4149
+
4150
+ async function runFetchPdf( identifier, options ) {
4151
+ const normalized = detectIdentifierType( identifier );
4152
+ const limiter = new HostRateLimiter( defaultPdfFetchIntervalMs );
4153
+ const openUrlBase = options.base || defaultOpenUrlBase;
4154
+ const startedAt = Date.now();
4155
+ const outPath = path.resolve( options.out || defaultPdfOutputPath( identifier ) );
4156
+ ensurePdfOutputDir( outPath );
4157
+ const cachePayload = { normalized, openUrlBase };
4158
+ const cacheKey = makeCacheKey( 'fetch-pdf', cachePayload );
4159
+ const cachedMeta = getCachedValue( cacheKey );
4160
+ let cacheHit = false;
4161
+ let sourceUrl = '';
4162
+
4163
+ if ( cachedMeta && cachedMeta.cacheFile && fileExists( cachedMeta.cacheFile ) ) {
4164
+ fs.copyFileSync( cachedMeta.cacheFile, outPath );
4165
+ cacheHit = true;
4166
+ sourceUrl = cachedMeta.sourceUrl || '';
4167
+ } else {
4168
+ let resolved;
4169
+ if ( normalized.type === 'doi' ) {
4170
+ try {
4171
+ resolved = await resolvePdfFromDoi( normalized.value, limiter );
4172
+ } catch ( error ) {
4173
+ if ( openUrlBase ) {
4174
+ resolved = await resolvePdfFromOpenUrl( normalized, limiter, openUrlBase );
4175
+ } else {
4176
+ throw error;
4177
+ }
4178
+ }
4179
+ } else if ( normalized.type === 'arxiv' ) {
4180
+ try {
4181
+ resolved = await resolvePdfFromArxiv( normalized.value, limiter );
4182
+ } catch ( error ) {
4183
+ if ( openUrlBase ) {
4184
+ resolved = await resolvePdfFromOpenUrl( normalized, limiter, openUrlBase );
4185
+ } else {
4186
+ throw error;
4187
+ }
4188
+ }
4189
+ } else {
4190
+ try {
4191
+ resolved = await resolvePdfFromUrl( normalized.value, limiter );
4192
+ } catch ( error ) {
4193
+ if ( openUrlBase ) {
4194
+ resolved = await resolvePdfFromOpenUrl( normalized, limiter, openUrlBase );
4195
+ } else {
4196
+ throw error;
4197
+ }
4198
+ }
4199
+ }
4200
+
4201
+ const cachedFileName = `${ cacheKey }.pdf`;
4202
+ const cachedFilePath = path.join( pdfCacheDir, cachedFileName );
4203
+ fs.writeFileSync( cachedFilePath, resolved.directResponse.bodyBuffer );
4204
+ fs.copyFileSync( cachedFilePath, outPath );
4205
+ setCachedValue( cacheKey, {
4206
+ cacheFile: cachedFilePath,
4207
+ sourceUrl: resolved.pdfUrl || ''
4208
+ }, options.cacheTtlSec );
4209
+ sourceUrl = resolved.pdfUrl || '';
4210
+ }
4211
+
4212
+ if ( options.json ) {
4213
+ jsonOut( {
4214
+ ok: true,
4215
+ command: 'fetch-pdf',
4216
+ stage: 'done',
4217
+ elapsed_ms: Date.now() - startedAt,
4218
+ cache_hit: cacheHit,
4219
+ identifier,
4220
+ out_path: outPath,
4221
+ source_url: sourceUrl
4222
+ } );
4223
+ return outPath;
4224
+ }
4225
+ if ( !options.silent ) {
4226
+ process.stdout.write( `${ outPath }\n` );
4227
+ }
4228
+ return outPath;
4229
+ }
4230
+
4231
+ async function runCitationStyle( query, options ) {
4232
+ const stylePath = resolveStylePath( options.style );
4233
+ const localeCode = options.locale || 'en-US';
4234
+ const startedAt = Date.now();
4235
+ const cached = await readThroughCache(
4236
+ 'cite-style',
4237
+ { query, stylePath, localeCode, plain: options.plain },
4238
+ options.cacheTtlSec,
4239
+ async () => {
4240
+ const response = await withRunningServices( ( ctx ) => httpGet(
4241
+ `http://127.0.0.1:${ ctx.citoidPort }/zotero/${ encodeURIComponent( query ) }`
4242
+ ) );
4243
+ const data = JSON.parse( response.body );
4244
+ if ( !Array.isArray( data ) || !data.length ) {
4245
+ throw new Error( 'No citation result for styled output' );
4246
+ }
4247
+ const cslItem = zoteroToCsl( data[ 0 ] );
4248
+ const renderedHtml = renderWithCsl( cslItem, stylePath, localeCode );
4249
+ return options.plain ? htmlToPlainText( renderedHtml ) : renderedHtml;
4250
+ }
4251
+ );
4252
+ const rendered = cached.value;
4253
+ if ( options.json ) {
4254
+ jsonOut( {
4255
+ ok: true,
4256
+ command: 'cite-style',
4257
+ stage: 'done',
4258
+ elapsed_ms: Date.now() - startedAt,
4259
+ cache_hit: cached.cacheHit,
4260
+ query,
4261
+ style: stylePath,
4262
+ locale: localeCode,
4263
+ plain: !!options.plain,
4264
+ output: rendered
4265
+ } );
4266
+ return rendered;
4267
+ }
4268
+ if ( !options.silent ) {
4269
+ process.stdout.write( rendered );
4270
+ if ( !rendered.endsWith( '\n' ) ) {
4271
+ process.stdout.write( '\n' );
4272
+ }
4273
+ }
4274
+ return rendered;
4275
+ }
4276
+
4277
+ function readBatchLines( inputPath ) {
4278
+ if ( !inputPath ) {
4279
+ throw new Error( 'Missing --in for batch mode' );
4280
+ }
4281
+ const fullPath = path.resolve( inputPath );
4282
+ if ( !fileExists( fullPath ) ) {
4283
+ throw new Error( `Batch input not found: ${ fullPath }` );
4284
+ }
4285
+ return fs.readFileSync( fullPath, 'utf8' )
4286
+ .split( /\r?\n/ )
4287
+ .map( ( line ) => line.trim() )
4288
+ .filter( ( line ) => line && !line.startsWith( '#' ) );
4289
+ }
4290
+
4291
+ function writeJsonl( rows, outputPath ) {
4292
+ const text = rows.map( ( row ) => JSON.stringify( row ) ).join( '\n' ) + ( rows.length ? '\n' : '' );
4293
+ if ( outputPath ) {
4294
+ const fullOut = path.resolve( outputPath );
4295
+ ensurePdfOutputDir( fullOut );
4296
+ fs.writeFileSync( fullOut, text );
4297
+ return fullOut;
4298
+ }
4299
+ process.stdout.write( text );
4300
+ return '';
4301
+ }
4302
+
4303
+ async function mapWithConcurrency( items, concurrency, mapper ) {
4304
+ const list = Array.isArray( items ) ? items : [];
4305
+ const limit = Math.max( 1, Math.min( Number.isFinite( concurrency ) ? concurrency : 1, list.length || 1 ) );
4306
+ const results = new Array( list.length );
4307
+ let cursor = 0;
4308
+
4309
+ const worker = async () => {
4310
+ while ( true ) {
4311
+ const index = cursor;
4312
+ cursor++;
4313
+ if ( index >= list.length ) {
4314
+ return;
4315
+ }
4316
+ results[ index ] = await mapper( list[ index ], index );
4317
+ }
4318
+ };
4319
+
4320
+ const workers = [];
4321
+ for ( let i = 0; i < limit; i++ ) {
4322
+ workers.push( worker() );
4323
+ }
4324
+ await Promise.all( workers );
4325
+ return results;
4326
+ }
4327
+
4328
+ function makeBatchRow( index, input, startedAt, output, error ) {
4329
+ if ( error ) {
4330
+ return {
4331
+ index,
4332
+ input,
4333
+ ok: false,
4334
+ elapsed_ms: Date.now() - startedAt,
4335
+ error: error.message
4336
+ };
4337
+ }
4338
+ return {
4339
+ index,
4340
+ input,
4341
+ ok: true,
4342
+ elapsed_ms: Date.now() - startedAt,
4343
+ output
4344
+ };
4345
+ }
4346
+
4347
+ async function runBatch( options ) {
4348
+ const batchStartedAt = Date.now();
4349
+ const op = options.op || '';
4350
+ const lines = readBatchLines( options.in );
4351
+ const concurrency = Math.max( 1, Number.isFinite( options.concurrency ) ? options.concurrency : defaultBatchConcurrency );
4352
+ let rows = [];
4353
+
4354
+ if ( op === 'cite' ) {
4355
+ const format = options.format || 'bibtex';
4356
+ rows = await withRunningServices( async ( ctx ) => mapWithConcurrency( lines, concurrency, async ( item, i ) => {
4357
+ const startedAt = Date.now();
4358
+ try {
4359
+ const normalized = normalizeCitationQuery( item );
4360
+ const cached = await readThroughCache(
4361
+ 'cite-response',
4362
+ { format, query: normalized.query },
4363
+ options.cacheTtlSec,
4364
+ async () => queryCitationWithContext( ctx, format, normalized.query )
4365
+ );
4366
+ return makeBatchRow( i, item, startedAt, {
4367
+ status_code: cached.value.statusCode,
4368
+ body: cached.value.body,
4369
+ cache_hit: cached.cacheHit,
4370
+ query_used: normalized.query,
4371
+ query_normalized: normalized.normalized
4372
+ } );
4373
+ } catch ( error ) {
4374
+ return makeBatchRow( i, item, startedAt, null, error );
4375
+ }
4376
+ } ) );
4377
+ } else if ( op === 'cite-style' ) {
4378
+ const stylePath = resolveStylePath( options.style );
4379
+ const localeCode = options.locale || 'en-US';
4380
+ rows = await withRunningServices( async ( ctx ) => mapWithConcurrency( lines, concurrency, async ( item, i ) => {
4381
+ const startedAt = Date.now();
4382
+ try {
4383
+ const cached = await readThroughCache(
4384
+ 'cite-style',
4385
+ { query: item, stylePath, localeCode, plain: options.plain },
4386
+ options.cacheTtlSec,
4387
+ async () => {
4388
+ const response = await httpGet(
4389
+ `http://127.0.0.1:${ ctx.citoidPort }/zotero/${ encodeURIComponent( item ) }`
4390
+ );
4391
+ const data = JSON.parse( response.body );
4392
+ if ( !Array.isArray( data ) || !data.length ) {
4393
+ throw new Error( 'No citation result for styled output' );
4394
+ }
4395
+ const cslItem = zoteroToCsl( data[ 0 ] );
4396
+ const renderedHtml = renderWithCsl( cslItem, stylePath, localeCode );
4397
+ return options.plain ? htmlToPlainText( renderedHtml ) : renderedHtml;
4398
+ }
4399
+ );
4400
+ return makeBatchRow( i, item, startedAt, {
4401
+ output: cached.value,
4402
+ cache_hit: cached.cacheHit
4403
+ } );
4404
+ } catch ( error ) {
4405
+ return makeBatchRow( i, item, startedAt, null, error );
4406
+ }
4407
+ } ) );
4408
+ } else if ( op === 'fetch-pdf' || op === 'openurl-resolve' ) {
4409
+ rows = await mapWithConcurrency( lines, concurrency, async ( item, i ) => {
4410
+ const startedAt = Date.now();
4411
+ try {
4412
+ let output;
4413
+ if ( op === 'fetch-pdf' ) {
4414
+ const outPath = await runFetchPdf( item, {
4415
+ ...options,
4416
+ json: false,
4417
+ silent: true,
4418
+ out: ''
4419
+ } );
4420
+ output = { out_path: outPath };
4421
+ } else {
4422
+ output = await runOpenUrlResolve( item, {
4423
+ ...options,
4424
+ json: false,
4425
+ silent: true
4426
+ } );
4427
+ }
4428
+ return makeBatchRow( i, item, startedAt, output );
4429
+ } catch ( error ) {
4430
+ return makeBatchRow( i, item, startedAt, null, error );
4431
+ }
4432
+ } );
4433
+ } else {
4434
+ throw new Error( `Unsupported --op: ${ op }` );
4435
+ }
4436
+
4437
+ const outFile = writeJsonl( rows, options.outJsonl );
4438
+ profileLog( options, 'batch', batchStartedAt, `op=${ op } count=${ rows.length } concurrency=${ concurrency }` );
4439
+ if ( options.json ) {
4440
+ jsonOut( {
4441
+ ok: true,
4442
+ command: 'batch',
4443
+ stage: 'done',
4444
+ op,
4445
+ count: rows.length,
4446
+ ok_count: rows.filter( ( row ) => row.ok ).length,
4447
+ fail_count: rows.filter( ( row ) => !row.ok ).length,
4448
+ out_jsonl: outFile || null
4449
+ } );
4450
+ } else if ( outFile ) {
4451
+ process.stdout.write( `${ outFile }\n` );
4452
+ }
4453
+ }
4454
+
4455
+ function handleCommandError( error, options, command, stage ) {
4456
+ if ( options && options.json ) {
4457
+ jsonOut( {
4458
+ ok: false,
4459
+ command,
4460
+ stage: stage || 'failed',
4461
+ error: error.message
4462
+ } );
4463
+ } else {
4464
+ console.error( error.message );
4465
+ }
4466
+ process.exit( 1 );
4467
+ }
4468
+
4469
+ function writeMcpMessage( payload ) {
4470
+ const body = Buffer.from( JSON.stringify( payload ), 'utf8' );
4471
+ const header = Buffer.from( `Content-Length: ${ body.length }\r\n\r\n`, 'utf8' );
4472
+ process.stdout.write( Buffer.concat( [ header, body ] ) );
4473
+ }
4474
+
4475
+ function makeMcpTools() {
4476
+ return [
4477
+ {
4478
+ name: 'cite',
4479
+ description: 'Generate citation in target format from DOI/URL/title',
4480
+ inputSchema: {
4481
+ type: 'object',
4482
+ properties: {
4483
+ format: { type: 'string' },
4484
+ query: { type: 'string' }
4485
+ },
4486
+ required: [ 'format', 'query' ]
4487
+ }
4488
+ },
4489
+ {
4490
+ name: 'cite_pdf',
4491
+ description: 'Generate bibtex citation from a local PDF path',
4492
+ inputSchema: {
4493
+ type: 'object',
4494
+ properties: {
4495
+ pdf_path: { type: 'string' }
4496
+ },
4497
+ required: [ 'pdf_path' ]
4498
+ }
4499
+ },
4500
+ {
4501
+ name: 'fetch_pdf',
4502
+ description: 'Resolve and download PDF from DOI/arXiv/URL',
4503
+ inputSchema: {
4504
+ type: 'object',
4505
+ properties: {
4506
+ identifier: { type: 'string' },
4507
+ out: { type: 'string' },
4508
+ base: { type: 'string' }
4509
+ },
4510
+ required: [ 'identifier' ]
4511
+ }
4512
+ },
4513
+ {
4514
+ name: 'openurl_resolve',
4515
+ description: 'Run OpenURL resolver diagnostics for an identifier',
4516
+ inputSchema: {
4517
+ type: 'object',
4518
+ properties: {
4519
+ identifier: { type: 'string' },
4520
+ base: { type: 'string' }
4521
+ },
4522
+ required: [ 'identifier' ]
4523
+ }
4524
+ },
4525
+ {
4526
+ name: 'citoid',
4527
+ description: 'Wikimedia Citation REST call',
4528
+ inputSchema: {
4529
+ type: 'object',
4530
+ properties: {
4531
+ format: { type: 'string' },
4532
+ query: { type: 'string' },
4533
+ wmf_citoid_base: { type: 'string' }
4534
+ },
4535
+ required: [ 'format', 'query' ]
4536
+ }
4537
+ },
4538
+ {
4539
+ name: 'crossref',
4540
+ description: 'Crossref lookup by DOI or query',
4541
+ inputSchema: {
4542
+ type: 'object',
4543
+ properties: {
4544
+ query: { type: 'string' },
4545
+ limit: { type: 'number' }
4546
+ },
4547
+ required: [ 'query' ]
4548
+ }
4549
+ },
4550
+ {
4551
+ name: 'semantic_scholar',
4552
+ description: 'Semantic Scholar legacy query (doi/arxiv/query)',
4553
+ inputSchema: {
4554
+ type: 'object',
4555
+ properties: {
4556
+ query: { type: 'string' },
4557
+ limit: { type: 'number' },
4558
+ offset: { type: 'number' },
4559
+ fields: { type: 'string' },
4560
+ s2_api_key: { type: 'string' }
4561
+ },
4562
+ required: [ 'query' ]
4563
+ }
4564
+ },
4565
+ {
4566
+ name: 'semantic_scholar_api',
4567
+ description: 'Semantic Scholar Graph API passthrough',
4568
+ inputSchema: {
4569
+ type: 'object',
4570
+ properties: {
4571
+ path: { type: 'string' },
4572
+ method: { type: 'string' },
4573
+ params: { type: 'object' },
4574
+ body: {},
4575
+ s2_api_key: { type: 'string' }
4576
+ },
4577
+ required: [ 'path' ]
4578
+ }
4579
+ }
4580
+ ];
4581
+ }
4582
+
4583
+ async function callMcpTool( name, args ) {
4584
+ const options = { json: false, silent: true, headers: false };
4585
+ if ( name === 'cite' ) {
4586
+ const format = String( args && args.format || '' ).trim();
4587
+ const query = String( args && args.query || '' ).trim();
4588
+ if ( !format || !query ) {
4589
+ throw new Error( 'cite requires format and query' );
4590
+ }
4591
+ const response = await runCitation( format, query, options );
4592
+ return response.body || '';
4593
+ }
4594
+ if ( name === 'cite_pdf' ) {
4595
+ const pdfPath = String( args && args.pdf_path || '' ).trim();
4596
+ if ( !pdfPath ) {
4597
+ throw new Error( 'cite_pdf requires pdf_path' );
4598
+ }
4599
+ const response = await runCitationFromPdf( pdfPath, options );
4600
+ return response.body || '';
4601
+ }
4602
+ if ( name === 'fetch_pdf' ) {
4603
+ const identifier = String( args && args.identifier || '' ).trim();
4604
+ if ( !identifier ) {
4605
+ throw new Error( 'fetch_pdf requires identifier' );
4606
+ }
4607
+ const outPath = await runFetchPdf( identifier, {
4608
+ ...options,
4609
+ out: args && args.out ? String( args.out ) : '',
4610
+ base: args && args.base ? String( args.base ) : ''
4611
+ } );
4612
+ return outPath;
4613
+ }
4614
+ if ( name === 'openurl_resolve' ) {
4615
+ const identifier = String( args && args.identifier || '' ).trim();
4616
+ if ( !identifier ) {
4617
+ throw new Error( 'openurl_resolve requires identifier' );
4618
+ }
4619
+ const output = await runOpenUrlResolve( identifier, {
4620
+ ...options,
4621
+ base: args && args.base ? String( args.base ) : ''
4622
+ } );
4623
+ return JSON.stringify( output, null, 2 );
4624
+ }
4625
+ if ( name === 'citoid' ) {
4626
+ const format = String( args && args.format || '' ).trim();
4627
+ const query = String( args && args.query || '' ).trim();
4628
+ if ( !format || !query ) {
4629
+ throw new Error( 'citoid requires format and query' );
4630
+ }
4631
+ const body = await runWmfCitoid( format, query, {
4632
+ ...options,
4633
+ wmfCitoidBase: args && args.wmf_citoid_base ? String( args.wmf_citoid_base ) : defaultWmfCitoidBase
4634
+ } );
4635
+ return body;
4636
+ }
4637
+ if ( name === 'crossref' ) {
4638
+ const query = String( args && args.query || '' ).trim();
4639
+ if ( !query ) {
4640
+ throw new Error( 'crossref requires query' );
4641
+ }
4642
+ const output = await runZoteroCrossref( query, {
4643
+ ...options,
4644
+ limit: Number.isFinite( args && args.limit ) ? Number( args.limit ) : options.limit
4645
+ } );
4646
+ return JSON.stringify( output, null, 2 );
4647
+ }
4648
+ if ( name === 'semantic_scholar' ) {
4649
+ const query = String( args && args.query || '' ).trim();
4650
+ if ( !query ) {
4651
+ throw new Error( 'semantic_scholar requires query' );
4652
+ }
4653
+ const output = await runSemanticScholarLegacy( query, {
4654
+ ...options,
4655
+ limit: Number.isFinite( args && args.limit ) ? Number( args.limit ) : options.limit,
4656
+ offset: Number.isFinite( args && args.offset ) ? Number( args.offset ) : 0,
4657
+ fields: args && args.fields ? String( args.fields ) : '',
4658
+ s2ApiKey: args && args.s2_api_key ? String( args.s2_api_key ) : defaultS2ApiKey
4659
+ } );
4660
+ return JSON.stringify( output, null, 2 );
4661
+ }
4662
+ if ( name === 'semantic_scholar_api' ) {
4663
+ const pathArg = String( args && args.path || '' ).trim();
4664
+ if ( !pathArg ) {
4665
+ throw new Error( 'semantic_scholar_api requires path' );
4666
+ }
4667
+ const output = await runSemanticScholarApi( pathArg, {
4668
+ ...options,
4669
+ method: args && args.method ? String( args.method ) : 'GET',
4670
+ params: args && args.params ? JSON.stringify( args.params ) : '',
4671
+ body: args && Object.prototype.hasOwnProperty.call( args, 'body' ) ? JSON.stringify( args.body ) : '',
4672
+ s2ApiKey: args && args.s2_api_key ? String( args.s2_api_key ) : defaultS2ApiKey
4673
+ } );
4674
+ return JSON.stringify( output, null, 2 );
4675
+ }
4676
+ throw new Error( `unknown tool: ${ name }` );
4677
+ }
4678
+
4679
+ function serveMcp() {
4680
+ const tools = makeMcpTools();
4681
+ let buffer = Buffer.alloc( 0 );
4682
+
4683
+ const handleMessage = async ( msg ) => {
4684
+ if ( !msg || typeof msg !== 'object' || !msg.method ) {
4685
+ return;
4686
+ }
4687
+
4688
+ if ( msg.method === 'notifications/initialized' ) {
4689
+ return;
4690
+ }
4691
+
4692
+ const id = msg.id;
4693
+ if ( msg.method === 'initialize' ) {
4694
+ writeMcpMessage( {
4695
+ jsonrpc: '2.0',
4696
+ id,
4697
+ result: {
4698
+ protocolVersion: '2024-11-05',
4699
+ serverInfo: { name: 'citeclaw', version: '2.0.0' },
4700
+ capabilities: { tools: {} }
4701
+ }
4702
+ } );
4703
+ return;
4704
+ }
4705
+ if ( msg.method === 'ping' ) {
4706
+ writeMcpMessage( { jsonrpc: '2.0', id, result: {} } );
4707
+ return;
4708
+ }
4709
+ if ( msg.method === 'tools/list' ) {
4710
+ writeMcpMessage( { jsonrpc: '2.0', id, result: { tools } } );
4711
+ return;
4712
+ }
4713
+ if ( msg.method === 'tools/call' ) {
4714
+ try {
4715
+ const toolName = msg.params && msg.params.name;
4716
+ const toolArgs = msg.params && msg.params.arguments || {};
4717
+ const text = await callMcpTool( toolName, toolArgs );
4718
+ writeMcpMessage( {
4719
+ jsonrpc: '2.0',
4720
+ id,
4721
+ result: {
4722
+ content: [ { type: 'text', text } ],
4723
+ isError: false
4724
+ }
4725
+ } );
4726
+ } catch ( error ) {
4727
+ writeMcpMessage( {
4728
+ jsonrpc: '2.0',
4729
+ id,
4730
+ result: {
4731
+ content: [ { type: 'text', text: error.message } ],
4732
+ isError: true
4733
+ }
4734
+ } );
4735
+ }
4736
+ return;
4737
+ }
4738
+
4739
+ if ( id !== undefined ) {
4740
+ writeMcpMessage( {
4741
+ jsonrpc: '2.0',
4742
+ id,
4743
+ error: { code: -32601, message: `Method not found: ${ msg.method }` }
4744
+ } );
4745
+ }
4746
+ };
4747
+
4748
+ process.stdin.on( 'data', ( chunk ) => {
4749
+ buffer = Buffer.concat( [ buffer, chunk ] );
4750
+ while ( true ) {
4751
+ const headerEnd = buffer.indexOf( '\r\n\r\n' );
4752
+ if ( headerEnd < 0 ) {
4753
+ return;
4754
+ }
4755
+ const headerText = buffer.slice( 0, headerEnd ).toString( 'utf8' );
4756
+ const lengthMatch = headerText.match( /Content-Length:\s*(\d+)/i );
4757
+ if ( !lengthMatch ) {
4758
+ buffer = buffer.slice( headerEnd + 4 );
4759
+ continue;
4760
+ }
4761
+ const contentLength = parseInt( lengthMatch[ 1 ], 10 );
4762
+ const frameTotal = headerEnd + 4 + contentLength;
4763
+ if ( buffer.length < frameTotal ) {
4764
+ return;
4765
+ }
4766
+ const body = buffer.slice( headerEnd + 4, frameTotal ).toString( 'utf8' );
4767
+ buffer = buffer.slice( frameTotal );
4768
+ try {
4769
+ const msg = JSON.parse( body );
4770
+ Promise.resolve( handleMessage( msg ) ).catch( () => {} );
4771
+ } catch ( error ) {
4772
+ }
4773
+ }
4774
+ } );
4775
+ process.stdin.resume();
4776
+ }
4777
+
4778
+ function main() {
4779
+ const action = process.argv[ 2 ];
4780
+
4781
+ if ( action === 'mcp' ) {
4782
+ serveMcp();
4783
+ return;
4784
+ }
4785
+
4786
+ if ( action === 'setup' ) {
4787
+ try {
4788
+ bootstrapLocalEnvironment();
4789
+ process.stdout.write( 'local runtime ready\n' );
4790
+ process.exit( 0 );
4791
+ } catch ( error ) {
4792
+ console.error( error.message );
4793
+ process.exit( 1 );
4794
+ }
4795
+ }
4796
+
4797
+ if ( action === 'citoid' || action === 'citation' ) {
4798
+ const parsed = parseOptions( process.argv.slice( 3 ) );
4799
+ const formatOrSub = parsed.args[ 0 ];
4800
+ if ( String( formatOrSub || '' ).trim().toLowerCase() === 'formats' ) {
4801
+ runWmfCitoidFormats( parsed );
4802
+ return;
4803
+ }
4804
+ const format = formatOrSub;
4805
+ const query = parsed.args.slice( 1 ).join( ' ' ).trim();
4806
+ if ( !format || !query ) {
4807
+ usage();
4808
+ process.exit( 1 );
4809
+ }
4810
+ runWmfCitoid( format, query, parsed ).catch( ( error ) => {
4811
+ handleCommandError( error, parsed, action );
4812
+ } );
4813
+ return;
4814
+ }
4815
+
4816
+ if ( action === 'crossref' ) {
4817
+ const parsed = parseOptions( process.argv.slice( 3 ) );
4818
+ const query = parsed.args.join( ' ' ).trim();
4819
+ if ( !query ) {
4820
+ usage();
4821
+ process.exit( 1 );
4822
+ }
4823
+ runZoteroCrossref( query, parsed ).catch( ( error ) => {
4824
+ handleCommandError( error, parsed, 'crossref' );
4825
+ } );
4826
+ return;
4827
+ }
4828
+
4829
+ if ( action === 'semantic-scholar' ) {
4830
+ const parsed = parseOptions( process.argv.slice( 3 ) );
4831
+ if ( !parsed.args.length ) {
4832
+ usage();
4833
+ process.exit( 1 );
4834
+ }
4835
+ const subActions = new Set( [
4836
+ 'api',
4837
+ 'paper',
4838
+ 'paper-search',
4839
+ 'paper-search-bulk',
4840
+ 'paper-batch',
4841
+ 'author',
4842
+ 'author-papers',
4843
+ 'author-batch'
4844
+ ] );
4845
+ const maybeSub = String( parsed.args[ 0 ] || '' ).trim().toLowerCase();
4846
+ if ( subActions.has( maybeSub ) ) {
4847
+ const subAction = parsed.args.shift();
4848
+ runSemanticScholarSubcommand( subAction, parsed ).catch( ( error ) => {
4849
+ handleCommandError( error, parsed, 'semantic-scholar' );
4850
+ } );
4851
+ return;
4852
+ }
4853
+ runSemanticScholarSubcommand( 'legacy', parsed ).catch( ( error ) => {
4854
+ handleCommandError( error, parsed, 'semantic-scholar' );
4855
+ } );
4856
+ return;
4857
+ }
4858
+
4859
+ if ( action === 'styles' ) {
4860
+ const parsed = parseOptions( process.argv.slice( 3 ) );
4861
+ const subAction = parsed.args[ 0 ];
4862
+ if ( subAction !== 'sync' ) {
4863
+ usage();
4864
+ process.exit( 1 );
4865
+ }
4866
+ syncStyles( parsed ).catch( ( error ) => {
4867
+ handleCommandError( error, parsed, 'styles', 'sync' );
4868
+ } );
4869
+ return;
4870
+ }
4871
+
4872
+ if ( action === 'api' ) {
4873
+ const parsed = parseOptions( process.argv.slice( 3 ) );
4874
+ const requestPath = parsed.args.join( ' ' ).trim();
4875
+
4876
+ if ( !requestPath ) {
4877
+ usage();
4878
+ process.exit( 1 );
4879
+ }
4880
+
4881
+ runApiPath( requestPath, parsed ).catch( ( error ) => {
4882
+ handleCommandError( error, parsed, 'api' );
4883
+ } );
4884
+ return;
4885
+ }
4886
+
4887
+ if ( action === 'cite' ) {
4888
+ const parsed = parseOptions( process.argv.slice( 3 ) );
4889
+ const format = parsed.args[ 0 ];
4890
+ const query = parsed.args.slice( 1 ).join( ' ' ).trim();
4891
+
4892
+ if ( !format || !query ) {
4893
+ usage();
4894
+ process.exit( 1 );
4895
+ }
4896
+
4897
+ runCitation( format, query, parsed ).catch( ( error ) => {
4898
+ handleCommandError( error, parsed, 'cite' );
4899
+ } );
4900
+ return;
4901
+ }
4902
+
4903
+ if ( action === 'cite-pdf' ) {
4904
+ const parsed = parseOptions( process.argv.slice( 3 ) );
4905
+ const pdfPath = parsed.args.join( ' ' ).trim();
4906
+
4907
+ if ( !pdfPath ) {
4908
+ usage();
4909
+ process.exit( 1 );
4910
+ }
4911
+
4912
+ runCitationFromPdf( pdfPath, parsed ).catch( ( error ) => {
4913
+ handleCommandError( error, parsed, 'cite-pdf' );
4914
+ } );
4915
+ return;
4916
+ }
4917
+
4918
+ if ( action === 'cite-style' ) {
4919
+ const parsed = parseOptions( process.argv.slice( 3 ) );
4920
+ const query = parsed.args.join( ' ' ).trim();
4921
+ if ( !query ) {
4922
+ usage();
4923
+ process.exit( 1 );
4924
+ }
4925
+ runCitationStyle( query, parsed ).catch( ( error ) => {
4926
+ handleCommandError( error, parsed, 'cite-style' );
4927
+ } );
4928
+ return;
4929
+ }
4930
+
4931
+ if ( action === 'fetch-pdf' ) {
4932
+ const parsed = parseOptions( process.argv.slice( 3 ) );
4933
+ const identifier = parsed.args.join( ' ' ).trim();
4934
+ if ( !identifier ) {
4935
+ usage();
4936
+ process.exit( 1 );
4937
+ }
4938
+ runFetchPdf( identifier, parsed ).catch( ( error ) => {
4939
+ handleCommandError( error, parsed, 'fetch-pdf' );
4940
+ } );
4941
+ return;
4942
+ }
4943
+
4944
+ if ( action === 'openurl-resolve' ) {
4945
+ const parsed = parseOptions( process.argv.slice( 3 ) );
4946
+ const identifier = parsed.args.join( ' ' ).trim();
4947
+ if ( !identifier ) {
4948
+ usage();
4949
+ process.exit( 1 );
4950
+ }
4951
+ runOpenUrlResolve( identifier, parsed ).catch( ( error ) => {
4952
+ handleCommandError( error, parsed, 'openurl-resolve' );
4953
+ } );
4954
+ return;
4955
+ }
4956
+
4957
+ if ( action === 'zotero' ) {
4958
+ const rawArgs = process.argv.slice( 3 );
4959
+ if ( rawArgs.length === 0 ||
4960
+ rawArgs[ 0 ] === '--help' ||
4961
+ rawArgs[ 0 ] === '-h' ||
4962
+ rawArgs[ 0 ] === 'help' ) {
4963
+ usageZotero();
4964
+ process.exit( 0 );
4965
+ }
4966
+ const subAction = String( rawArgs[ 0 ] || '' ).trim();
4967
+ const parsed = parseOptions( rawArgs.slice( 1 ) );
4968
+ if ( parsed.args.includes( '--help' ) || parsed.args.includes( '-h' ) ) {
4969
+ usageZotero( subAction );
4970
+ process.exit( 0 );
4971
+ }
4972
+ runZoteroCommand( subAction, parsed ).catch( ( error ) => {
4973
+ handleCommandError( error, parsed, 'zotero', subAction );
4974
+ } );
4975
+ return;
4976
+ }
4977
+
4978
+ if ( action === 'batch' ) {
4979
+ const parsed = parseOptions( process.argv.slice( 3 ) );
4980
+ if ( !parsed.op || !parsed.in ) {
4981
+ usage();
4982
+ process.exit( 1 );
4983
+ }
4984
+ runBatch( parsed ).catch( ( error ) => {
4985
+ handleCommandError( error, parsed, 'batch' );
4986
+ } );
4987
+ return;
4988
+ }
4989
+
4990
+ if ( action === 'info' ) {
4991
+ const parsed = parseOptions( process.argv.slice( 3 ) );
4992
+ runApiPath( '/_info', parsed ).catch( ( error ) => {
4993
+ handleCommandError( error, parsed, 'info' );
4994
+ } );
4995
+ return;
4996
+ }
4997
+
4998
+ if ( action === 'spec' ) {
4999
+ const parsed = parseOptions( process.argv.slice( 3 ) );
5000
+ runApiPath( '/?spec', parsed ).catch( ( error ) => {
5001
+ handleCommandError( error, parsed, 'spec' );
5002
+ } );
5003
+ return;
5004
+ }
5005
+
5006
+ usage();
5007
+ process.exit( 1 );
5008
+ }
5009
+
5010
+ module.exports = {
5011
+ detectPdfIdentifierCandidates,
5012
+ extractBestDoiCandidate,
5013
+ extractPdfCandidates,
5014
+ main,
5015
+ normalizeDoi,
5016
+ shouldAttemptPdfOcr
5017
+ };
5018
+
5019
+ if ( require.main === module ) {
5020
+ main();
5021
+ }