clearai-dsh 0.1.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 (154) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +201 -0
  3. package/README.md +138 -0
  4. package/README.zh-CN.md +138 -0
  5. package/bin/clearai.mjs +224 -0
  6. package/brand/README.md +41 -0
  7. package/brand/logo-512-dark.png +0 -0
  8. package/brand/logo-512.png +0 -0
  9. package/brand/logo-lockup-dark.png +0 -0
  10. package/brand/logo-lockup.png +0 -0
  11. package/brand/logo-lockup.svg +12 -0
  12. package/brand/logo-wordmark.svg +6 -0
  13. package/brand/logo.svg +19 -0
  14. package/cordis.patch.yml +39 -0
  15. package/lib/client.js +3071 -0
  16. package/lib/fold.js +1576 -0
  17. package/lib/host.js +605 -0
  18. package/package.json +65 -0
  19. package/presets/clearai/agent.cordis.yml +226 -0
  20. package/presets/clearai/plugins/brain.js +547 -0
  21. package/presets/clearai/plugins/clearai-kernel.js +5485 -0
  22. package/presets/clearai/plugins/ontology.js +306 -0
  23. package/presets/clearai/plugins/prompts.js +312 -0
  24. package/presets/clearai/preset.yml +5 -0
  25. package/presets/clearai/skills/clearai-loop/SKILL.md +89 -0
  26. package/presets/clearai/template/knowledge/README.md +25 -0
  27. package/presets/clearai/template/memory/README.md +34 -0
  28. package/presets/clearai/template/project.md +49 -0
  29. package/presets/clearai/template/skills/README.md +37 -0
  30. package/presets/clearai/template/skills/chart-diagram-qa/SKILL.md +43 -0
  31. package/presets/clearai/template/skills/citation-management/SKILL.md +73 -0
  32. package/presets/clearai/template/skills/citation-management/references/bibtex_formatting.md +908 -0
  33. package/presets/clearai/template/skills/citation-management/references/citation_validation.md +794 -0
  34. package/presets/clearai/template/skills/citation-management/references/google_scholar_search.md +725 -0
  35. package/presets/clearai/template/skills/citation-management/references/metadata_extraction.md +870 -0
  36. package/presets/clearai/template/skills/citation-management/references/pubmed_search.md +839 -0
  37. package/presets/clearai/template/skills/citation-management/scripts/doi_to_bibtex.py +204 -0
  38. package/presets/clearai/template/skills/citation-management/scripts/extract_metadata.py +569 -0
  39. package/presets/clearai/template/skills/citation-management/scripts/format_bibtex.py +349 -0
  40. package/presets/clearai/template/skills/citation-management/scripts/generate_schematic.py +139 -0
  41. package/presets/clearai/template/skills/citation-management/scripts/generate_schematic_ai.py +817 -0
  42. package/presets/clearai/template/skills/citation-management/scripts/search_google_scholar.py +282 -0
  43. package/presets/clearai/template/skills/citation-management/scripts/search_pubmed.py +398 -0
  44. package/presets/clearai/template/skills/citation-management/scripts/validate_citations.py +497 -0
  45. package/presets/clearai/template/skills/data-analysis/SKILL.md +92 -0
  46. package/presets/clearai/template/skills/data-analysis/checklists/readiness_check.md +23 -0
  47. package/presets/clearai/template/skills/data-analysis/templates/analysis_report.md.tpl +63 -0
  48. package/presets/clearai/template/skills/data-analysis/templates/cleaning_rules_draft.yaml.tpl +32 -0
  49. package/presets/clearai/template/skills/data-analysis/templates/data_dictionary.md.tpl +12 -0
  50. package/presets/clearai/template/skills/data-analysis/templates/domain_knowledge_template.md.tpl +316 -0
  51. package/presets/clearai/template/skills/data-analysis/templates/feature_candidates.json.tpl +20 -0
  52. package/presets/clearai/template/skills/data-analysis/templates/quality_scorecard.md.tpl +30 -0
  53. package/presets/clearai/template/skills/data-analysis/workflows/01-data-profiling.md +42 -0
  54. package/presets/clearai/template/skills/data-analysis/workflows/02-quality-audit.md +36 -0
  55. package/presets/clearai/template/skills/data-analysis/workflows/03-physical-correlation.md +25 -0
  56. package/presets/clearai/template/skills/data-analysis/workflows/04-unstructured-mining.md +26 -0
  57. package/presets/clearai/template/skills/data-qa-analysis/SKILL.md +102 -0
  58. package/presets/clearai/template/skills/data-qa-analysis/checklists/readiness_check.md +62 -0
  59. package/presets/clearai/template/skills/data-qa-analysis/templates/best_in_class_report.md.tpl +56 -0
  60. package/presets/clearai/template/skills/data-qa-analysis/templates/cleaning_rules_draft.yaml.tpl +56 -0
  61. package/presets/clearai/template/skills/data-qa-analysis/templates/data_dictionary.md.tpl +13 -0
  62. package/presets/clearai/template/skills/data-qa-analysis/templates/data_source_inventory_and_lineage.md.tpl +146 -0
  63. package/presets/clearai/template/skills/data-qa-analysis/templates/data_status_report.md.tpl +60 -0
  64. package/presets/clearai/template/skills/data-qa-analysis/templates/steady_state_rules.yaml.tpl +41 -0
  65. package/presets/clearai/template/skills/data-qa-analysis/templates/subsystem_registry.md.tpl +101 -0
  66. package/presets/clearai/template/skills/data-qa-analysis/templates/unified_execution_plan.md.tpl +100 -0
  67. package/presets/clearai/template/skills/data-qa-analysis/workflows/01-data-source-inventory-and-lineage.md +194 -0
  68. package/presets/clearai/template/skills/data-qa-analysis/workflows/02-data-alignment-and-tag-semantics.md +122 -0
  69. package/presets/clearai/template/skills/data-qa-analysis/workflows/03-steady-state-identification.md +126 -0
  70. package/presets/clearai/template/skills/data-qa-analysis/workflows/04-consumption-analysis.md +152 -0
  71. package/presets/clearai/template/skills/data-qa-analysis/workflows/05-best-in-class-and-optimization-space.md +78 -0
  72. package/presets/clearai/template/skills/domain-presearch/SKILL.md +131 -0
  73. package/presets/clearai/template/skills/domain-presearch/checklists/domain_checklist.md +24 -0
  74. package/presets/clearai/template/skills/domain-presearch/references/figure_code.md +78 -0
  75. package/presets/clearai/template/skills/domain-presearch/references/strategic_frameworks.md +38 -0
  76. package/presets/clearai/template/skills/exploration-loop/SKILL.md +81 -0
  77. package/presets/clearai/template/skills/exploratory-data-analysis/SKILL.md +77 -0
  78. package/presets/clearai/template/skills/exploratory-data-analysis/references/bioinformatics_genomics_formats.md +664 -0
  79. package/presets/clearai/template/skills/exploratory-data-analysis/references/chemistry_molecular_formats.md +664 -0
  80. package/presets/clearai/template/skills/exploratory-data-analysis/references/general_scientific_formats.md +518 -0
  81. package/presets/clearai/template/skills/exploratory-data-analysis/references/microscopy_imaging_formats.md +620 -0
  82. package/presets/clearai/template/skills/exploratory-data-analysis/references/proteomics_metabolomics_formats.md +517 -0
  83. package/presets/clearai/template/skills/exploratory-data-analysis/references/spectroscopy_analytical_formats.md +633 -0
  84. package/presets/clearai/template/skills/exploratory-data-analysis/scripts/eda_analyzer.py +547 -0
  85. package/presets/clearai/template/skills/hypothesis-generation/SKILL.md +73 -0
  86. package/presets/clearai/template/skills/hypothesis-generation/references/experimental_design_patterns.md +329 -0
  87. package/presets/clearai/template/skills/hypothesis-generation/references/hypothesis_quality_criteria.md +198 -0
  88. package/presets/clearai/template/skills/hypothesis-generation/references/literature_search_strategies.md +622 -0
  89. package/presets/clearai/template/skills/hypothesis-generation/scripts/generate_schematic.py +139 -0
  90. package/presets/clearai/template/skills/hypothesis-generation/scripts/generate_schematic_ai.py +817 -0
  91. package/presets/clearai/template/skills/literature-review/SKILL.md +72 -0
  92. package/presets/clearai/template/skills/literature-review/references/citation_styles.md +166 -0
  93. package/presets/clearai/template/skills/literature-review/references/database_strategies.md +455 -0
  94. package/presets/clearai/template/skills/literature-review/scripts/generate_pdf.py +176 -0
  95. package/presets/clearai/template/skills/literature-review/scripts/generate_schematic.py +139 -0
  96. package/presets/clearai/template/skills/literature-review/scripts/generate_schematic_ai.py +817 -0
  97. package/presets/clearai/template/skills/literature-review/scripts/search_databases.py +303 -0
  98. package/presets/clearai/template/skills/literature-review/scripts/verify_citations.py +221 -0
  99. package/presets/clearai/template/skills/paper-lookup/SKILL.md +59 -0
  100. package/presets/clearai/template/skills/paper-lookup/references/arxiv.md +161 -0
  101. package/presets/clearai/template/skills/paper-lookup/references/biorxiv.md +118 -0
  102. package/presets/clearai/template/skills/paper-lookup/references/core.md +150 -0
  103. package/presets/clearai/template/skills/paper-lookup/references/crossref.md +181 -0
  104. package/presets/clearai/template/skills/paper-lookup/references/medrxiv.md +104 -0
  105. package/presets/clearai/template/skills/paper-lookup/references/openalex.md +174 -0
  106. package/presets/clearai/template/skills/paper-lookup/references/pmc.md +152 -0
  107. package/presets/clearai/template/skills/paper-lookup/references/pubmed.md +124 -0
  108. package/presets/clearai/template/skills/paper-lookup/references/semantic-scholar.md +203 -0
  109. package/presets/clearai/template/skills/paper-lookup/references/unpaywall.md +127 -0
  110. package/presets/clearai/template/skills/process-presearch/SKILL.md +196 -0
  111. package/presets/clearai/template/skills/process-presearch/checklists/process_checklist.md +18 -0
  112. package/presets/clearai/template/skills/process-presearch/references/figure_code.md +107 -0
  113. package/presets/clearai/template/skills/process-presearch/references/source_attribution_example.md +22 -0
  114. package/presets/clearai/template/skills/process-understanding-extraction/SKILL.md +69 -0
  115. package/presets/clearai/template/skills/process-understanding-extraction/checklists/readiness_check.md +34 -0
  116. package/presets/clearai/template/skills/process-understanding-extraction/templates/docx_raw_dump_extractor.py.tpl +132 -0
  117. package/presets/clearai/template/skills/process-understanding-extraction/templates/entity_map_unit_topology.json.tpl +86 -0
  118. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_brief.md.tpl +89 -0
  119. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_brief_builder_from_raw_dump.py.tpl +203 -0
  120. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_flow_mermaid.md.tpl +41 -0
  121. package/presets/clearai/template/skills/process-understanding-extraction/templates/unified_execution_plan.md.tpl +53 -0
  122. package/presets/clearai/template/skills/process-understanding-extraction/workflows/01-process-doc-discovery.md +173 -0
  123. package/presets/clearai/template/skills/process-understanding-extraction/workflows/02-process-understanding-and-diagramming.md +106 -0
  124. package/presets/clearai/template/skills/scientific-brainstorming/SKILL.md +64 -0
  125. package/presets/clearai/template/skills/scientific-brainstorming/references/brainstorming_methods.md +326 -0
  126. package/presets/clearai/template/skills/scientific-critical-thinking/SKILL.md +72 -0
  127. package/presets/clearai/template/skills/scientific-critical-thinking/references/common_biases.md +364 -0
  128. package/presets/clearai/template/skills/scientific-critical-thinking/references/evidence_hierarchy.md +485 -0
  129. package/presets/clearai/template/skills/scientific-critical-thinking/references/experimental_design.md +496 -0
  130. package/presets/clearai/template/skills/scientific-critical-thinking/references/logical_fallacies.md +478 -0
  131. package/presets/clearai/template/skills/scientific-critical-thinking/references/scientific_method.md +169 -0
  132. package/presets/clearai/template/skills/scientific-critical-thinking/references/statistical_pitfalls.md +506 -0
  133. package/presets/clearai/template/skills/skill-creator/SKILL.md +109 -0
  134. package/presets/clearai/template/skills/skill-creator/references/authoring-guide.md +89 -0
  135. package/presets/clearai/template/skills/statistical-analysis/SKILL.md +79 -0
  136. package/presets/clearai/template/skills/statistical-analysis/references/assumptions_and_diagnostics.md +369 -0
  137. package/presets/clearai/template/skills/statistical-analysis/references/bayesian_statistics.md +653 -0
  138. package/presets/clearai/template/skills/statistical-analysis/references/effect_sizes_and_power.md +578 -0
  139. package/presets/clearai/template/skills/statistical-analysis/references/reporting_standards.md +469 -0
  140. package/presets/clearai/template/skills/statistical-analysis/references/test_selection_guide.md +129 -0
  141. package/presets/clearai/template/skills/statistical-analysis/scripts/assumption_checks.py +538 -0
  142. package/presets/clearai/template/skills/web-artifact/SKILL.md +165 -0
  143. package/presets/clearai/template/skills/web-artifact/assets/renderer/renderer.css +229 -0
  144. package/presets/clearai/template/skills/web-artifact/assets/renderer/renderer.js +373 -0
  145. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/LICENSE +263 -0
  146. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/UPSTREAM.md +26 -0
  147. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/elk.bundled.js +6605 -0
  148. package/presets/clearai/template/skills/web-artifact/references/when-drawing-a-topology.md +150 -0
  149. package/presets/clearai/template/skills/web-artifact/references/when-the-page-must-work-offline.md +62 -0
  150. package/presets/clearai/template/skills/web-artifact/scripts/check_artifact.py +167 -0
  151. package/presets/clearai/template/skills/web-artifact/scripts/render_topology.js +272 -0
  152. package/presets/clearai/template/skills/what-if-oracle/LICENSE.txt +5 -0
  153. package/presets/clearai/template/skills/what-if-oracle/SKILL.md +72 -0
  154. package/presets/clearai/template/skills/what-if-oracle/references/scenario-templates.md +154 -0
@@ -0,0 +1,569 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Metadata Extraction Tool
4
+ Extract citation metadata from DOI, PMID, arXiv ID, or URL using various APIs.
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ import requests
10
+ import argparse
11
+ import time
12
+ import re
13
+ import json
14
+ import xml.etree.ElementTree as ET
15
+ from typing import Optional, Dict, List, Tuple
16
+ from urllib.parse import urlparse
17
+
18
+ class MetadataExtractor:
19
+ """Extract metadata from various sources and generate BibTeX."""
20
+
21
+ def __init__(self, email: Optional[str] = None):
22
+ """
23
+ Initialize extractor.
24
+
25
+ Args:
26
+ email: Email for Entrez API (recommended for PubMed)
27
+ """
28
+ self.session = requests.Session()
29
+ self.session.headers.update({
30
+ 'User-Agent': 'MetadataExtractor/1.0 (Citation Management Tool)'
31
+ })
32
+ self.email = email or os.getenv('NCBI_EMAIL', '')
33
+
34
+ def identify_type(self, identifier: str) -> Tuple[str, str]:
35
+ """
36
+ Identify the type of identifier.
37
+
38
+ Args:
39
+ identifier: DOI, PMID, arXiv ID, or URL
40
+
41
+ Returns:
42
+ Tuple of (type, cleaned_identifier)
43
+ """
44
+ identifier = identifier.strip()
45
+
46
+ # Check if URL
47
+ if identifier.startswith('http://') or identifier.startswith('https://'):
48
+ return self._parse_url(identifier)
49
+
50
+ # Check for DOI
51
+ if identifier.startswith('10.'):
52
+ return ('doi', identifier)
53
+
54
+ # Check for arXiv ID
55
+ if re.match(r'^\d{4}\.\d{4,5}(v\d+)?$', identifier):
56
+ return ('arxiv', identifier)
57
+ if identifier.startswith('arXiv:'):
58
+ return ('arxiv', identifier.replace('arXiv:', ''))
59
+
60
+ # Check for PMID (8-digit number typically)
61
+ if identifier.isdigit() and len(identifier) >= 7:
62
+ return ('pmid', identifier)
63
+
64
+ # Check for PMCID
65
+ if identifier.upper().startswith('PMC') and identifier[3:].isdigit():
66
+ return ('pmcid', identifier.upper())
67
+
68
+ return ('unknown', identifier)
69
+
70
+ def _parse_url(self, url: str) -> Tuple[str, str]:
71
+ """Parse URL to extract identifier type and value."""
72
+ parsed = urlparse(url)
73
+
74
+ # DOI URLs
75
+ if 'doi.org' in parsed.netloc:
76
+ doi = parsed.path.lstrip('/')
77
+ return ('doi', doi)
78
+
79
+ # PubMed URLs
80
+ if 'pubmed.ncbi.nlm.nih.gov' in parsed.netloc or 'ncbi.nlm.nih.gov/pubmed' in url:
81
+ pmid = re.search(r'/(\d+)', parsed.path)
82
+ if pmid:
83
+ return ('pmid', pmid.group(1))
84
+
85
+ # arXiv URLs
86
+ if 'arxiv.org' in parsed.netloc:
87
+ arxiv_id = re.search(r'/abs/(\d{4}\.\d{4,5})', parsed.path)
88
+ if arxiv_id:
89
+ return ('arxiv', arxiv_id.group(1))
90
+
91
+ # Nature, Science, Cell, etc. - try to extract DOI from URL
92
+ doi_match = re.search(r'10\.\d{4,}/[^\s/]+', url)
93
+ if doi_match:
94
+ return ('doi', doi_match.group())
95
+
96
+ return ('url', url)
97
+
98
+ def extract_from_doi(self, doi: str) -> Optional[Dict]:
99
+ """
100
+ Extract metadata from DOI using CrossRef API.
101
+
102
+ Args:
103
+ doi: Digital Object Identifier
104
+
105
+ Returns:
106
+ Metadata dictionary or None
107
+ """
108
+ url = f'https://api.crossref.org/works/{doi}'
109
+
110
+ try:
111
+ response = self.session.get(url, timeout=15)
112
+
113
+ if response.status_code == 200:
114
+ data = response.json()
115
+ message = data.get('message', {})
116
+
117
+ metadata = {
118
+ 'type': 'doi',
119
+ 'entry_type': self._crossref_type_to_bibtex(message.get('type')),
120
+ 'doi': doi,
121
+ 'title': message.get('title', [''])[0],
122
+ 'authors': self._format_authors_crossref(message.get('author', [])),
123
+ 'year': self._extract_year_crossref(message),
124
+ 'journal': message.get('container-title', [''])[0] if message.get('container-title') else '',
125
+ 'volume': str(message.get('volume', '')) if message.get('volume') else '',
126
+ 'issue': str(message.get('issue', '')) if message.get('issue') else '',
127
+ 'pages': message.get('page', ''),
128
+ 'publisher': message.get('publisher', ''),
129
+ 'url': f'https://doi.org/{doi}'
130
+ }
131
+
132
+ return metadata
133
+ else:
134
+ print(f'Error: CrossRef API returned status {response.status_code} for DOI: {doi}', file=sys.stderr)
135
+ return None
136
+
137
+ except Exception as e:
138
+ print(f'Error extracting metadata from DOI {doi}: {e}', file=sys.stderr)
139
+ return None
140
+
141
+ def extract_from_pmid(self, pmid: str) -> Optional[Dict]:
142
+ """
143
+ Extract metadata from PMID using PubMed E-utilities.
144
+
145
+ Args:
146
+ pmid: PubMed ID
147
+
148
+ Returns:
149
+ Metadata dictionary or None
150
+ """
151
+ url = f'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi'
152
+ params = {
153
+ 'db': 'pubmed',
154
+ 'id': pmid,
155
+ 'retmode': 'xml',
156
+ 'rettype': 'abstract'
157
+ }
158
+
159
+ if self.email:
160
+ params['email'] = self.email
161
+
162
+ api_key = os.getenv('NCBI_API_KEY')
163
+ if api_key:
164
+ params['api_key'] = api_key
165
+
166
+ try:
167
+ response = self.session.get(url, params=params, timeout=15)
168
+
169
+ if response.status_code == 200:
170
+ root = ET.fromstring(response.content)
171
+ article = root.find('.//PubmedArticle')
172
+
173
+ if article is None:
174
+ print(f'Error: No article found for PMID: {pmid}', file=sys.stderr)
175
+ return None
176
+
177
+ # Extract metadata from XML
178
+ medline_citation = article.find('.//MedlineCitation')
179
+ article_elem = medline_citation.find('.//Article')
180
+ journal = article_elem.find('.//Journal')
181
+
182
+ # Get DOI if available
183
+ doi = None
184
+ article_ids = article.findall('.//ArticleId')
185
+ for article_id in article_ids:
186
+ if article_id.get('IdType') == 'doi':
187
+ doi = article_id.text
188
+ break
189
+
190
+ metadata = {
191
+ 'type': 'pmid',
192
+ 'entry_type': 'article',
193
+ 'pmid': pmid,
194
+ 'title': article_elem.findtext('.//ArticleTitle', ''),
195
+ 'authors': self._format_authors_pubmed(article_elem.findall('.//Author')),
196
+ 'year': self._extract_year_pubmed(article_elem),
197
+ 'journal': journal.findtext('.//Title', ''),
198
+ 'volume': journal.findtext('.//JournalIssue/Volume', ''),
199
+ 'issue': journal.findtext('.//JournalIssue/Issue', ''),
200
+ 'pages': article_elem.findtext('.//Pagination/MedlinePgn', ''),
201
+ 'doi': doi
202
+ }
203
+
204
+ return metadata
205
+ else:
206
+ print(f'Error: PubMed API returned status {response.status_code} for PMID: {pmid}', file=sys.stderr)
207
+ return None
208
+
209
+ except Exception as e:
210
+ print(f'Error extracting metadata from PMID {pmid}: {e}', file=sys.stderr)
211
+ return None
212
+
213
+ def extract_from_arxiv(self, arxiv_id: str) -> Optional[Dict]:
214
+ """
215
+ Extract metadata from arXiv ID using arXiv API.
216
+
217
+ Args:
218
+ arxiv_id: arXiv identifier
219
+
220
+ Returns:
221
+ Metadata dictionary or None
222
+ """
223
+ url = 'http://export.arxiv.org/api/query'
224
+ params = {
225
+ 'id_list': arxiv_id,
226
+ 'max_results': 1
227
+ }
228
+
229
+ try:
230
+ response = self.session.get(url, params=params, timeout=15)
231
+
232
+ if response.status_code == 200:
233
+ # Parse Atom XML
234
+ root = ET.fromstring(response.content)
235
+ ns = {'atom': 'http://www.w3.org/2005/Atom', 'arxiv': 'http://arxiv.org/schemas/atom'}
236
+
237
+ entry = root.find('atom:entry', ns)
238
+ if entry is None:
239
+ print(f'Error: No entry found for arXiv ID: {arxiv_id}', file=sys.stderr)
240
+ return None
241
+
242
+ # Extract DOI if published
243
+ doi_elem = entry.find('arxiv:doi', ns)
244
+ doi = doi_elem.text if doi_elem is not None else None
245
+
246
+ # Extract journal reference if published
247
+ journal_ref_elem = entry.find('arxiv:journal_ref', ns)
248
+ journal_ref = journal_ref_elem.text if journal_ref_elem is not None else None
249
+
250
+ # Get publication date
251
+ published = entry.findtext('atom:published', '', ns)
252
+ year = published[:4] if published else ''
253
+
254
+ # Get authors
255
+ authors = []
256
+ for author in entry.findall('atom:author', ns):
257
+ name = author.findtext('atom:name', '', ns)
258
+ if name:
259
+ authors.append(name)
260
+
261
+ metadata = {
262
+ 'type': 'arxiv',
263
+ 'entry_type': 'misc' if not doi else 'article',
264
+ 'arxiv_id': arxiv_id,
265
+ 'title': entry.findtext('atom:title', '', ns).strip().replace('\n', ' '),
266
+ 'authors': ' and '.join(authors),
267
+ 'year': year,
268
+ 'doi': doi,
269
+ 'journal_ref': journal_ref,
270
+ 'abstract': entry.findtext('atom:summary', '', ns).strip().replace('\n', ' '),
271
+ 'url': f'https://arxiv.org/abs/{arxiv_id}'
272
+ }
273
+
274
+ return metadata
275
+ else:
276
+ print(f'Error: arXiv API returned status {response.status_code} for ID: {arxiv_id}', file=sys.stderr)
277
+ return None
278
+
279
+ except Exception as e:
280
+ print(f'Error extracting metadata from arXiv {arxiv_id}: {e}', file=sys.stderr)
281
+ return None
282
+
283
+ def metadata_to_bibtex(self, metadata: Dict, citation_key: Optional[str] = None) -> str:
284
+ """
285
+ Convert metadata dictionary to BibTeX format.
286
+
287
+ Args:
288
+ metadata: Metadata dictionary
289
+ citation_key: Optional custom citation key
290
+
291
+ Returns:
292
+ BibTeX string
293
+ """
294
+ if not citation_key:
295
+ citation_key = self._generate_citation_key(metadata)
296
+
297
+ entry_type = metadata.get('entry_type', 'misc')
298
+
299
+ # Build BibTeX entry
300
+ lines = [f'@{entry_type}{{{citation_key},']
301
+
302
+ # Add fields
303
+ if metadata.get('authors'):
304
+ lines.append(f' author = {{{metadata["authors"]}}},')
305
+
306
+ if metadata.get('title'):
307
+ # Protect capitalization
308
+ title = self._protect_title(metadata['title'])
309
+ lines.append(f' title = {{{title}}},')
310
+
311
+ if entry_type == 'article' and metadata.get('journal'):
312
+ lines.append(f' journal = {{{metadata["journal"]}}},')
313
+ elif entry_type == 'misc' and metadata.get('type') == 'arxiv':
314
+ lines.append(f' howpublished = {{arXiv}},')
315
+
316
+ if metadata.get('year'):
317
+ lines.append(f' year = {{{metadata["year"]}}},')
318
+
319
+ if metadata.get('volume'):
320
+ lines.append(f' volume = {{{metadata["volume"]}}},')
321
+
322
+ if metadata.get('issue'):
323
+ lines.append(f' number = {{{metadata["issue"]}}},')
324
+
325
+ if metadata.get('pages'):
326
+ pages = metadata['pages'].replace('-', '--') # En-dash
327
+ lines.append(f' pages = {{{pages}}},')
328
+
329
+ if metadata.get('doi'):
330
+ lines.append(f' doi = {{{metadata["doi"]}}},')
331
+ elif metadata.get('url'):
332
+ lines.append(f' url = {{{metadata["url"]}}},')
333
+
334
+ if metadata.get('pmid'):
335
+ lines.append(f' note = {{PMID: {metadata["pmid"]}}},')
336
+
337
+ if metadata.get('type') == 'arxiv' and not metadata.get('doi'):
338
+ lines.append(f' note = {{Preprint}},')
339
+
340
+ # Remove trailing comma from last field
341
+ if lines[-1].endswith(','):
342
+ lines[-1] = lines[-1][:-1]
343
+
344
+ lines.append('}')
345
+
346
+ return '\n'.join(lines)
347
+
348
+ def _crossref_type_to_bibtex(self, crossref_type: str) -> str:
349
+ """Map CrossRef type to BibTeX entry type."""
350
+ type_map = {
351
+ 'journal-article': 'article',
352
+ 'book': 'book',
353
+ 'book-chapter': 'incollection',
354
+ 'proceedings-article': 'inproceedings',
355
+ 'posted-content': 'misc',
356
+ 'dataset': 'misc',
357
+ 'report': 'techreport'
358
+ }
359
+ return type_map.get(crossref_type, 'misc')
360
+
361
+ def _format_authors_crossref(self, authors: List[Dict]) -> str:
362
+ """Format author list from CrossRef data."""
363
+ if not authors:
364
+ return ''
365
+
366
+ formatted = []
367
+ for author in authors:
368
+ given = author.get('given', '')
369
+ family = author.get('family', '')
370
+ if family:
371
+ if given:
372
+ formatted.append(f'{family}, {given}')
373
+ else:
374
+ formatted.append(family)
375
+
376
+ return ' and '.join(formatted)
377
+
378
+ def _format_authors_pubmed(self, authors: List) -> str:
379
+ """Format author list from PubMed XML."""
380
+ formatted = []
381
+ for author in authors:
382
+ last_name = author.findtext('.//LastName', '')
383
+ fore_name = author.findtext('.//ForeName', '')
384
+ if last_name:
385
+ if fore_name:
386
+ formatted.append(f'{last_name}, {fore_name}')
387
+ else:
388
+ formatted.append(last_name)
389
+
390
+ return ' and '.join(formatted)
391
+
392
+ def _extract_year_crossref(self, message: Dict) -> str:
393
+ """Extract year from CrossRef message."""
394
+ # Try published-print first, then published-online
395
+ date_parts = message.get('published-print', {}).get('date-parts', [[]])
396
+ if not date_parts or not date_parts[0]:
397
+ date_parts = message.get('published-online', {}).get('date-parts', [[]])
398
+
399
+ if date_parts and date_parts[0]:
400
+ return str(date_parts[0][0])
401
+ return ''
402
+
403
+ def _extract_year_pubmed(self, article: ET.Element) -> str:
404
+ """Extract year from PubMed XML."""
405
+ year = article.findtext('.//Journal/JournalIssue/PubDate/Year', '')
406
+ if not year:
407
+ medline_date = article.findtext('.//Journal/JournalIssue/PubDate/MedlineDate', '')
408
+ if medline_date:
409
+ year_match = re.search(r'\d{4}', medline_date)
410
+ if year_match:
411
+ year = year_match.group()
412
+ return year
413
+
414
+ def _generate_citation_key(self, metadata: Dict) -> str:
415
+ """Generate a citation key from metadata."""
416
+ # Get first author last name
417
+ authors = metadata.get('authors', '')
418
+ if authors:
419
+ first_author = authors.split(' and ')[0]
420
+ if ',' in first_author:
421
+ last_name = first_author.split(',')[0].strip()
422
+ else:
423
+ last_name = first_author.split()[-1] if first_author else 'Unknown'
424
+ else:
425
+ last_name = 'Unknown'
426
+
427
+ # Get year
428
+ year = metadata.get('year', '').strip()
429
+ if not year:
430
+ year = 'XXXX'
431
+
432
+ # Clean last name (remove special characters)
433
+ last_name = re.sub(r'[^a-zA-Z]', '', last_name)
434
+
435
+ # Get keyword from title
436
+ title = metadata.get('title', '')
437
+ words = re.findall(r'\b[a-zA-Z]{4,}\b', title)
438
+ keyword = words[0].lower() if words else 'paper'
439
+
440
+ return f'{last_name}{year}{keyword}'
441
+
442
+ def _protect_title(self, title: str) -> str:
443
+ """Protect capitalization in title for BibTeX."""
444
+ # Protect common acronyms and proper nouns
445
+ protected_words = [
446
+ 'DNA', 'RNA', 'CRISPR', 'COVID', 'HIV', 'AIDS', 'AlphaFold',
447
+ 'Python', 'AI', 'ML', 'GPU', 'CPU', 'USA', 'UK', 'EU'
448
+ ]
449
+
450
+ for word in protected_words:
451
+ title = re.sub(rf'\b{word}\b', f'{{{word}}}', title, flags=re.IGNORECASE)
452
+
453
+ return title
454
+
455
+ def extract(self, identifier: str) -> Optional[str]:
456
+ """
457
+ Extract metadata and return BibTeX.
458
+
459
+ Args:
460
+ identifier: DOI, PMID, arXiv ID, or URL
461
+
462
+ Returns:
463
+ BibTeX string or None
464
+ """
465
+ id_type, clean_id = self.identify_type(identifier)
466
+
467
+ print(f'Identified as {id_type}: {clean_id}', file=sys.stderr)
468
+
469
+ metadata = None
470
+
471
+ if id_type == 'doi':
472
+ metadata = self.extract_from_doi(clean_id)
473
+ elif id_type == 'pmid':
474
+ metadata = self.extract_from_pmid(clean_id)
475
+ elif id_type == 'arxiv':
476
+ metadata = self.extract_from_arxiv(clean_id)
477
+ else:
478
+ print(f'Error: Unknown identifier type: {identifier}', file=sys.stderr)
479
+ return None
480
+
481
+ if metadata:
482
+ return self.metadata_to_bibtex(metadata)
483
+ else:
484
+ return None
485
+
486
+
487
+ def main():
488
+ """Command-line interface."""
489
+ parser = argparse.ArgumentParser(
490
+ description='Extract citation metadata from DOI, PMID, arXiv ID, or URL',
491
+ epilog='Example: python extract_metadata.py --doi 10.1038/s41586-021-03819-2'
492
+ )
493
+
494
+ parser.add_argument('--doi', help='Digital Object Identifier')
495
+ parser.add_argument('--pmid', help='PubMed ID')
496
+ parser.add_argument('--arxiv', help='arXiv ID')
497
+ parser.add_argument('--url', help='URL to article')
498
+ parser.add_argument('-i', '--input', help='Input file with identifiers (one per line)')
499
+ parser.add_argument('-o', '--output', help='Output file for BibTeX (default: stdout)')
500
+ parser.add_argument('--format', choices=['bibtex', 'json'], default='bibtex', help='Output format')
501
+ parser.add_argument('--email', help='Email for NCBI E-utilities (recommended)')
502
+
503
+ args = parser.parse_args()
504
+
505
+ # Collect identifiers
506
+ identifiers = []
507
+ if args.doi:
508
+ identifiers.append(args.doi)
509
+ if args.pmid:
510
+ identifiers.append(args.pmid)
511
+ if args.arxiv:
512
+ identifiers.append(args.arxiv)
513
+ if args.url:
514
+ identifiers.append(args.url)
515
+
516
+ if args.input:
517
+ try:
518
+ with open(args.input, 'r', encoding='utf-8') as f:
519
+ file_ids = [line.strip() for line in f if line.strip()]
520
+ identifiers.extend(file_ids)
521
+ except Exception as e:
522
+ print(f'Error reading input file: {e}', file=sys.stderr)
523
+ sys.exit(1)
524
+
525
+ if not identifiers:
526
+ parser.print_help()
527
+ sys.exit(1)
528
+
529
+ # Extract metadata
530
+ extractor = MetadataExtractor(email=args.email)
531
+ bibtex_entries = []
532
+
533
+ for i, identifier in enumerate(identifiers):
534
+ print(f'\nProcessing {i+1}/{len(identifiers)}...', file=sys.stderr)
535
+ bibtex = extractor.extract(identifier)
536
+ if bibtex:
537
+ bibtex_entries.append(bibtex)
538
+
539
+ # Rate limiting
540
+ if i < len(identifiers) - 1:
541
+ time.sleep(0.5)
542
+
543
+ if not bibtex_entries:
544
+ print('Error: No successful extractions', file=sys.stderr)
545
+ sys.exit(1)
546
+
547
+ # Format output
548
+ if args.format == 'bibtex':
549
+ output = '\n\n'.join(bibtex_entries) + '\n'
550
+ else: # json
551
+ output = json.dumps({
552
+ 'count': len(bibtex_entries),
553
+ 'entries': bibtex_entries
554
+ }, indent=2)
555
+
556
+ # Write output
557
+ if args.output:
558
+ with open(args.output, 'w', encoding='utf-8') as f:
559
+ f.write(output)
560
+ print(f'\nSuccessfully wrote {len(bibtex_entries)} entries to {args.output}', file=sys.stderr)
561
+ else:
562
+ print(output)
563
+
564
+ print(f'\nExtracted {len(bibtex_entries)}/{len(identifiers)} entries', file=sys.stderr)
565
+
566
+
567
+ if __name__ == '__main__':
568
+ main()
569
+