@wentorai/research-plugins 1.3.2 → 1.4.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 (229) hide show
  1. package/README.md +32 -56
  2. package/curated/analysis/README.md +1 -13
  3. package/curated/domains/README.md +1 -5
  4. package/curated/literature/README.md +1 -10
  5. package/curated/research/README.md +1 -18
  6. package/curated/tools/README.md +1 -12
  7. package/curated/writing/README.md +1 -5
  8. package/index.ts +88 -5
  9. package/openclaw.plugin.json +3 -12
  10. package/package.json +3 -5
  11. package/skills/analysis/statistics/SKILL.md +1 -1
  12. package/skills/analysis/statistics/meta-analysis-guide/SKILL.md +1 -1
  13. package/skills/domains/ai-ml/SKILL.md +3 -2
  14. package/skills/domains/ai-ml/generative-ai-guide/SKILL.md +1 -0
  15. package/skills/domains/ai-ml/huggingface-api/SKILL.md +251 -0
  16. package/skills/domains/biomedical/SKILL.md +9 -2
  17. package/skills/domains/biomedical/alphafold-api/SKILL.md +227 -0
  18. package/skills/domains/biomedical/biothings-api/SKILL.md +296 -0
  19. package/skills/domains/biomedical/clinicaltrials-api-v2/SKILL.md +216 -0
  20. package/skills/domains/biomedical/enrichr-api/SKILL.md +264 -0
  21. package/skills/domains/biomedical/ensembl-rest-api/SKILL.md +204 -0
  22. package/skills/domains/biomedical/medical-data-api/SKILL.md +197 -0
  23. package/skills/domains/biomedical/pdb-structure-api/SKILL.md +219 -0
  24. package/skills/domains/business/SKILL.md +2 -3
  25. package/skills/domains/chemistry/SKILL.md +3 -2
  26. package/skills/domains/chemistry/catalysis-hub-api/SKILL.md +171 -0
  27. package/skills/domains/education/SKILL.md +2 -3
  28. package/skills/domains/law/SKILL.md +3 -2
  29. package/skills/domains/law/uk-legislation-api/SKILL.md +179 -0
  30. package/skills/literature/fulltext/SKILL.md +3 -2
  31. package/skills/literature/fulltext/arxiv-latex-source/SKILL.md +195 -0
  32. package/skills/literature/search/SKILL.md +2 -3
  33. package/skills/research/automation/SKILL.md +2 -3
  34. package/skills/research/automation/datagen-research-guide/SKILL.md +1 -0
  35. package/skills/research/automation/mle-agent-guide/SKILL.md +1 -0
  36. package/skills/research/automation/paper-to-agent-guide/SKILL.md +1 -0
  37. package/skills/research/deep-research/auto-deep-research-guide/SKILL.md +1 -0
  38. package/skills/research/methodology/SKILL.md +1 -1
  39. package/skills/research/methodology/claude-scientific-guide/SKILL.md +1 -0
  40. package/skills/research/methodology/qualitative-research-guide/SKILL.md +1 -1
  41. package/skills/research/paper-review/SKILL.md +1 -1
  42. package/skills/research/paper-review/peer-review-guide/SKILL.md +1 -1
  43. package/skills/tools/knowledge-graph/SKILL.md +2 -3
  44. package/skills/tools/ocr-translate/zotero-pdf2zh-guide/SKILL.md +1 -0
  45. package/skills/writing/citation/obsidian-citation-guide/SKILL.md +1 -0
  46. package/skills/writing/citation/obsidian-zotero-guide/SKILL.md +1 -0
  47. package/skills/writing/citation/papersgpt-zotero-guide/SKILL.md +1 -0
  48. package/skills/writing/citation/zotero-mdnotes-guide/SKILL.md +1 -0
  49. package/skills/writing/citation/zotero-reference-guide/SKILL.md +1 -0
  50. package/skills/writing/composition/scientific-writing-resources/SKILL.md +1 -0
  51. package/skills/writing/latex/latex-drawing-collection/SKILL.md +1 -0
  52. package/skills/writing/latex/latex-templates-collection/SKILL.md +1 -0
  53. package/skills/writing/templates/novathesis-guide/SKILL.md +1 -0
  54. package/src/tools/arxiv.ts +78 -30
  55. package/src/tools/biorxiv.ts +142 -0
  56. package/src/tools/crossref.ts +60 -22
  57. package/src/tools/datacite.ts +188 -0
  58. package/src/tools/dblp.ts +125 -0
  59. package/src/tools/doaj.ts +82 -0
  60. package/src/tools/europe-pmc.ts +159 -0
  61. package/src/tools/hal.ts +118 -0
  62. package/src/tools/inspire-hep.ts +165 -0
  63. package/src/tools/openaire.ts +158 -0
  64. package/src/tools/openalex.ts +20 -15
  65. package/src/tools/opencitations.ts +103 -0
  66. package/src/tools/orcid.ts +136 -0
  67. package/src/tools/osf-preprints.ts +104 -0
  68. package/src/tools/pubmed.ts +19 -13
  69. package/src/tools/ror.ts +118 -0
  70. package/src/tools/unpaywall.ts +12 -6
  71. package/src/tools/util.ts +141 -0
  72. package/src/tools/zenodo.ts +154 -0
  73. package/mcp-configs/academic-db/ChatSpatial.json +0 -17
  74. package/mcp-configs/academic-db/academia-mcp.json +0 -17
  75. package/mcp-configs/academic-db/academic-paper-explorer.json +0 -17
  76. package/mcp-configs/academic-db/academic-search-mcp-server.json +0 -17
  77. package/mcp-configs/academic-db/agentinterviews-mcp.json +0 -17
  78. package/mcp-configs/academic-db/all-in-mcp.json +0 -17
  79. package/mcp-configs/academic-db/alphafold-mcp.json +0 -20
  80. package/mcp-configs/academic-db/apple-health-mcp.json +0 -17
  81. package/mcp-configs/academic-db/arxiv-latex-mcp.json +0 -17
  82. package/mcp-configs/academic-db/arxiv-mcp-server.json +0 -17
  83. package/mcp-configs/academic-db/bgpt-mcp.json +0 -17
  84. package/mcp-configs/academic-db/biomcp.json +0 -17
  85. package/mcp-configs/academic-db/biothings-mcp.json +0 -17
  86. package/mcp-configs/academic-db/brightspace-mcp.json +0 -21
  87. package/mcp-configs/academic-db/catalysishub-mcp-server.json +0 -17
  88. package/mcp-configs/academic-db/climatiq-mcp.json +0 -20
  89. package/mcp-configs/academic-db/clinicaltrialsgov-mcp-server.json +0 -17
  90. package/mcp-configs/academic-db/deep-research-mcp.json +0 -17
  91. package/mcp-configs/academic-db/dicom-mcp.json +0 -17
  92. package/mcp-configs/academic-db/enrichr-mcp-server.json +0 -17
  93. package/mcp-configs/academic-db/fec-mcp-server.json +0 -17
  94. package/mcp-configs/academic-db/fhir-mcp-server-themomentum.json +0 -17
  95. package/mcp-configs/academic-db/fhir-mcp.json +0 -19
  96. package/mcp-configs/academic-db/gget-mcp.json +0 -17
  97. package/mcp-configs/academic-db/gibs-mcp.json +0 -20
  98. package/mcp-configs/academic-db/gis-mcp-server.json +0 -22
  99. package/mcp-configs/academic-db/google-earth-engine-mcp.json +0 -21
  100. package/mcp-configs/academic-db/google-researcher-mcp.json +0 -17
  101. package/mcp-configs/academic-db/idea-reality-mcp.json +0 -17
  102. package/mcp-configs/academic-db/legiscan-mcp.json +0 -19
  103. package/mcp-configs/academic-db/lex.json +0 -17
  104. package/mcp-configs/academic-db/m4-clinical-mcp.json +0 -21
  105. package/mcp-configs/academic-db/medical-mcp.json +0 -21
  106. package/mcp-configs/academic-db/nexonco-mcp.json +0 -20
  107. package/mcp-configs/academic-db/omop-mcp.json +0 -20
  108. package/mcp-configs/academic-db/onekgpd-mcp.json +0 -20
  109. package/mcp-configs/academic-db/openedu-mcp.json +0 -20
  110. package/mcp-configs/academic-db/opengenes-mcp.json +0 -20
  111. package/mcp-configs/academic-db/openstax-mcp.json +0 -21
  112. package/mcp-configs/academic-db/openstreetmap-mcp.json +0 -21
  113. package/mcp-configs/academic-db/opentargets-mcp.json +0 -21
  114. package/mcp-configs/academic-db/pdb-mcp.json +0 -21
  115. package/mcp-configs/academic-db/smithsonian-mcp.json +0 -20
  116. package/mcp-configs/ai-platform/Adaptive-Graph-of-Thoughts-MCP-server.json +0 -17
  117. package/mcp-configs/ai-platform/ai-counsel.json +0 -17
  118. package/mcp-configs/ai-platform/atlas-mcp-server.json +0 -17
  119. package/mcp-configs/ai-platform/counsel-mcp.json +0 -17
  120. package/mcp-configs/ai-platform/cross-llm-mcp.json +0 -17
  121. package/mcp-configs/ai-platform/gptr-mcp.json +0 -17
  122. package/mcp-configs/ai-platform/magi-researchers.json +0 -21
  123. package/mcp-configs/ai-platform/mcp-academic-researcher.json +0 -22
  124. package/mcp-configs/ai-platform/open-paper-machine.json +0 -21
  125. package/mcp-configs/ai-platform/paper-intelligence.json +0 -21
  126. package/mcp-configs/ai-platform/paper-reader.json +0 -21
  127. package/mcp-configs/ai-platform/paperdebugger.json +0 -21
  128. package/mcp-configs/browser/decipher-research-agent.json +0 -17
  129. package/mcp-configs/browser/deep-research.json +0 -17
  130. package/mcp-configs/browser/everything-claude-code.json +0 -17
  131. package/mcp-configs/browser/exa-mcp.json +0 -20
  132. package/mcp-configs/browser/gpt-researcher.json +0 -17
  133. package/mcp-configs/browser/heurist-agent-framework.json +0 -17
  134. package/mcp-configs/browser/mcp-searxng.json +0 -21
  135. package/mcp-configs/browser/mcp-webresearch.json +0 -20
  136. package/mcp-configs/cloud-docs/confluence-mcp.json +0 -37
  137. package/mcp-configs/cloud-docs/google-drive-mcp.json +0 -35
  138. package/mcp-configs/cloud-docs/notion-mcp.json +0 -29
  139. package/mcp-configs/communication/discord-mcp.json +0 -29
  140. package/mcp-configs/communication/discourse-mcp.json +0 -21
  141. package/mcp-configs/communication/slack-mcp.json +0 -29
  142. package/mcp-configs/communication/telegram-mcp.json +0 -28
  143. package/mcp-configs/data-platform/4everland-hosting-mcp.json +0 -17
  144. package/mcp-configs/data-platform/automl-stat-mcp.json +0 -21
  145. package/mcp-configs/data-platform/context-keeper.json +0 -17
  146. package/mcp-configs/data-platform/context7.json +0 -19
  147. package/mcp-configs/data-platform/contextstream-mcp.json +0 -17
  148. package/mcp-configs/data-platform/email-mcp.json +0 -17
  149. package/mcp-configs/data-platform/jefferson-stats-mcp.json +0 -22
  150. package/mcp-configs/data-platform/mcp-excel-server.json +0 -21
  151. package/mcp-configs/data-platform/mcp-stata.json +0 -21
  152. package/mcp-configs/data-platform/mcpstack-jupyter.json +0 -21
  153. package/mcp-configs/data-platform/ml-mcp.json +0 -21
  154. package/mcp-configs/data-platform/nasdaq-data-link-mcp.json +0 -20
  155. package/mcp-configs/data-platform/numpy-mcp.json +0 -21
  156. package/mcp-configs/database/neo4j-mcp.json +0 -37
  157. package/mcp-configs/database/postgres-mcp.json +0 -28
  158. package/mcp-configs/database/sqlite-mcp.json +0 -29
  159. package/mcp-configs/dev-platform/geogebra-mcp.json +0 -21
  160. package/mcp-configs/dev-platform/github-mcp.json +0 -31
  161. package/mcp-configs/dev-platform/gitlab-mcp.json +0 -34
  162. package/mcp-configs/dev-platform/latex-mcp-server.json +0 -21
  163. package/mcp-configs/dev-platform/manim-mcp.json +0 -20
  164. package/mcp-configs/dev-platform/mcp-echarts.json +0 -20
  165. package/mcp-configs/dev-platform/panel-viz-mcp.json +0 -20
  166. package/mcp-configs/dev-platform/paperbanana.json +0 -20
  167. package/mcp-configs/dev-platform/texflow-mcp.json +0 -20
  168. package/mcp-configs/dev-platform/texmcp.json +0 -20
  169. package/mcp-configs/dev-platform/typst-mcp.json +0 -21
  170. package/mcp-configs/dev-platform/vizro-mcp.json +0 -20
  171. package/mcp-configs/email/email-mcp.json +0 -40
  172. package/mcp-configs/email/gmail-mcp.json +0 -37
  173. package/mcp-configs/note-knowledge/ApeRAG.json +0 -17
  174. package/mcp-configs/note-knowledge/In-Memoria.json +0 -17
  175. package/mcp-configs/note-knowledge/agent-memory.json +0 -17
  176. package/mcp-configs/note-knowledge/aimemo.json +0 -17
  177. package/mcp-configs/note-knowledge/biel-mcp.json +0 -19
  178. package/mcp-configs/note-knowledge/cognee.json +0 -17
  179. package/mcp-configs/note-knowledge/context-awesome.json +0 -17
  180. package/mcp-configs/note-knowledge/context-mcp.json +0 -17
  181. package/mcp-configs/note-knowledge/conversation-handoff-mcp.json +0 -17
  182. package/mcp-configs/note-knowledge/cortex.json +0 -17
  183. package/mcp-configs/note-knowledge/devrag.json +0 -17
  184. package/mcp-configs/note-knowledge/easy-obsidian-mcp.json +0 -17
  185. package/mcp-configs/note-knowledge/engram.json +0 -17
  186. package/mcp-configs/note-knowledge/gnosis-mcp.json +0 -17
  187. package/mcp-configs/note-knowledge/graphlit-mcp-server.json +0 -19
  188. package/mcp-configs/note-knowledge/local-faiss-mcp.json +0 -21
  189. package/mcp-configs/note-knowledge/mcp-memory-service.json +0 -21
  190. package/mcp-configs/note-knowledge/mcp-obsidian.json +0 -23
  191. package/mcp-configs/note-knowledge/mcp-ragdocs.json +0 -20
  192. package/mcp-configs/note-knowledge/mcp-summarizer.json +0 -21
  193. package/mcp-configs/note-knowledge/mediawiki-mcp.json +0 -21
  194. package/mcp-configs/note-knowledge/openzim-mcp.json +0 -20
  195. package/mcp-configs/note-knowledge/zettelkasten-mcp.json +0 -21
  196. package/mcp-configs/reference-mgr/academic-paper-mcp-http.json +0 -20
  197. package/mcp-configs/reference-mgr/academix.json +0 -20
  198. package/mcp-configs/reference-mgr/arxiv-cli.json +0 -17
  199. package/mcp-configs/reference-mgr/arxiv-research-mcp.json +0 -21
  200. package/mcp-configs/reference-mgr/arxiv-search-mcp.json +0 -17
  201. package/mcp-configs/reference-mgr/chiken.json +0 -17
  202. package/mcp-configs/reference-mgr/claude-scholar.json +0 -17
  203. package/mcp-configs/reference-mgr/devonthink-mcp.json +0 -17
  204. package/mcp-configs/reference-mgr/google-scholar-abstract-mcp.json +0 -19
  205. package/mcp-configs/reference-mgr/google-scholar-mcp.json +0 -20
  206. package/mcp-configs/reference-mgr/mcp-paperswithcode.json +0 -21
  207. package/mcp-configs/reference-mgr/mcp-scholarly.json +0 -20
  208. package/mcp-configs/reference-mgr/mcp-simple-arxiv.json +0 -20
  209. package/mcp-configs/reference-mgr/mcp-simple-pubmed.json +0 -20
  210. package/mcp-configs/reference-mgr/mcp-zotero.json +0 -21
  211. package/mcp-configs/reference-mgr/mendeley-mcp.json +0 -20
  212. package/mcp-configs/reference-mgr/ncbi-mcp-server.json +0 -22
  213. package/mcp-configs/reference-mgr/onecite.json +0 -21
  214. package/mcp-configs/reference-mgr/paper-search-mcp.json +0 -21
  215. package/mcp-configs/reference-mgr/pubmed-search-mcp.json +0 -21
  216. package/mcp-configs/reference-mgr/scholar-mcp.json +0 -21
  217. package/mcp-configs/reference-mgr/scholar-multi-mcp.json +0 -21
  218. package/mcp-configs/reference-mgr/seerai.json +0 -21
  219. package/mcp-configs/reference-mgr/semantic-scholar-fastmcp.json +0 -21
  220. package/mcp-configs/reference-mgr/sourcelibrary.json +0 -20
  221. package/mcp-configs/registry.json +0 -476
  222. package/mcp-configs/repository/dataverse-mcp.json +0 -33
  223. package/mcp-configs/repository/huggingface-mcp.json +0 -29
  224. package/skills/domains/business/xpert-bi-guide/SKILL.md +0 -84
  225. package/skills/domains/education/edumcp-guide/SKILL.md +0 -74
  226. package/skills/literature/search/paper-search-mcp-guide/SKILL.md +0 -107
  227. package/skills/research/automation/mcp-server-guide/SKILL.md +0 -211
  228. package/skills/tools/knowledge-graph/paperpile-notion-guide/SKILL.md +0 -84
  229. package/src/tools/semantic-scholar.ts +0 -66
@@ -1,43 +1,65 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
3
- import { toolResult } from "./util.js";
3
+ import { toolResult, trackedFetch, isTrackedError } from "./util.js";
4
4
 
5
5
  const BASE = "https://export.arxiv.org/api/query";
6
6
 
7
- function parseArxivXml(xml: string) {
7
+ /**
8
+ * Parse arXiv Atom XML response into structured paper objects.
9
+ *
10
+ * Uses regex-based extraction (arXiv returns Atom 1.0 XML).
11
+ * Handles edge cases: namespace prefixes, missing fields, HTML error pages.
12
+ */
13
+ function parseArxivXml(xml: string): Record<string, unknown>[] {
14
+ // Guard: if arXiv returned an error page (HTML) or empty response
15
+ if (!xml || !xml.includes("<entry>")) return [];
16
+
8
17
  const entries: Record<string, unknown>[] = [];
9
- const entryBlocks = xml.split("<entry>").slice(1);
10
18
 
11
- for (const block of entryBlocks) {
12
- const getText = (tag: string) => {
19
+ // Use regex to extract <entry>...</entry> blocks robustly
20
+ const entryRegex = /<entry>([\s\S]*?)<\/entry>/g;
21
+ let entryMatch: RegExpExecArray | null;
22
+
23
+ while ((entryMatch = entryRegex.exec(xml)) !== null) {
24
+ const block = entryMatch[1];
25
+
26
+ const getText = (tag: string): string => {
27
+ // Match tags with or without namespace prefixes and attributes
13
28
  const m = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`));
14
29
  return m ? m[1].trim() : "";
15
30
  };
16
31
 
17
- const getAll = (tag: string) => {
32
+ const getAll = (tag: string): string[] => {
18
33
  const results: string[] = [];
19
34
  const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "g");
20
- let m;
35
+ let m: RegExpExecArray | null;
21
36
  while ((m = re.exec(block)) !== null) results.push(m[1].trim());
22
37
  return results;
23
38
  };
24
39
 
25
- const getAttr = (tag: string, attr: string) => {
26
- const m = block.match(new RegExp(`<${tag}[^>]*${attr}="([^"]*)"[^>]*/?>`, "g"));
27
- return m
28
- ? m.map((s) => {
29
- const am = s.match(new RegExp(`${attr}="([^"]*)"`));
30
- return am ? am[1] : "";
31
- })
32
- : [];
40
+ const getAttr = (tag: string, attr: string): string[] => {
41
+ const results: string[] = [];
42
+ const re = new RegExp(`<${tag}[^>]*?${attr}="([^"]*)"[^>]*/?>`, "g");
43
+ let m: RegExpExecArray | null;
44
+ while ((m = re.exec(block)) !== null) {
45
+ if (m[1]) results.push(m[1]);
46
+ }
47
+ return results;
33
48
  };
34
49
 
35
- const id = getText("id");
36
- const arxivId = id.replace("http://arxiv.org/abs/", "").replace(/v\d+$/, "");
50
+ const rawId = getText("id");
51
+ if (!rawId) continue; // skip malformed entries
52
+
53
+ const arxivId = rawId
54
+ .replace(/https?:\/\/arxiv\.org\/abs\//, "")
55
+ .replace(/v\d+$/, "");
56
+
57
+ const title = getText("title").replace(/\s+/g, " ");
58
+ if (!title) continue; // skip entries without title
37
59
 
38
60
  entries.push({
39
61
  arxiv_id: arxivId,
40
- title: getText("title").replace(/\s+/g, " "),
62
+ title,
41
63
  summary: getText("summary").replace(/\s+/g, " "),
42
64
  authors: getAll("name"),
43
65
  published: getText("published"),
@@ -45,8 +67,8 @@ function parseArxivXml(xml: string) {
45
67
  categories: getAttr("category", "term"),
46
68
  pdf_url: `https://arxiv.org/pdf/${arxivId}`,
47
69
  abs_url: `https://arxiv.org/abs/${arxivId}`,
48
- doi: getText("arxiv:doi"),
49
- comment: getText("arxiv:comment"),
70
+ doi: getText("arxiv:doi") || undefined,
71
+ comment: getText("arxiv:comment") || undefined,
50
72
  });
51
73
  }
52
74
 
@@ -62,7 +84,7 @@ export function createArxivTools(
62
84
  name: "search_arxiv",
63
85
  label: "Search Papers (arXiv)",
64
86
  description:
65
- "Search arXiv preprint repository. Covers physics, math, CS, biology, quantitative finance, statistics, and more.",
87
+ "Search arXiv preprint repository. Covers physics, math, CS, biology, quantitative finance, statistics, and more. All results are open access.",
66
88
  parameters: Type.Object({
67
89
  query: Type.String({
68
90
  description:
@@ -94,9 +116,17 @@ export function createArxivTools(
94
116
  sortOrder: input.sort_order ?? "descending",
95
117
  });
96
118
 
97
- const res = await fetch(`${BASE}?${params}`);
98
- if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
99
- const xml = await res.text();
119
+ const tracked = await trackedFetch("arxiv", `${BASE}?${params}`, undefined, 15_000);
120
+ if (isTrackedError(tracked)) return tracked;
121
+ const xml = await tracked.res.text();
122
+
123
+ // Check if response is actually XML (not HTML error page)
124
+ if (!xml.includes("<feed")) {
125
+ return toolResult({
126
+ error: "arXiv returned non-XML response (possibly rate-limited or error page)",
127
+ _source_health: { source: "arxiv", latency_ms: tracked.latency_ms },
128
+ });
129
+ }
100
130
 
101
131
  const totalMatch = xml.match(
102
132
  /<opensearch:totalResults[^>]*>(\d+)<\/opensearch:totalResults>/,
@@ -106,6 +136,7 @@ export function createArxivTools(
106
136
  return toolResult({
107
137
  total_results: total,
108
138
  papers: parseArxivXml(xml),
139
+ _source_health: { source: "arxiv", latency_ms: tracked.latency_ms },
109
140
  });
110
141
  },
111
142
  },
@@ -120,14 +151,31 @@ export function createArxivTools(
120
151
  }),
121
152
  }),
122
153
  execute: async (input: { arxiv_id: string }) => {
123
- const id = input.arxiv_id.replace("arXiv:", "");
154
+ const id = input.arxiv_id.replace("arXiv:", "").replace(/https?:\/\/arxiv\.org\/abs\//, "");
124
155
  const params = new URLSearchParams({ id_list: id });
125
- const res = await fetch(`${BASE}?${params}`);
126
- if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
127
- const xml = await res.text();
156
+
157
+ const tracked = await trackedFetch("arxiv", `${BASE}?${params}`, undefined, 15_000);
158
+ if (isTrackedError(tracked)) return tracked;
159
+ const xml = await tracked.res.text();
160
+
161
+ if (!xml.includes("<feed")) {
162
+ return toolResult({
163
+ error: "arXiv returned non-XML response",
164
+ _source_health: { source: "arxiv", latency_ms: tracked.latency_ms },
165
+ });
166
+ }
167
+
128
168
  const papers = parseArxivXml(xml);
129
- if (papers.length === 0) return toolResult({ error: "Paper not found" });
130
- return toolResult(papers[0]);
169
+ if (papers.length === 0) {
170
+ return toolResult({
171
+ error: `Paper not found: ${id}`,
172
+ _source_health: { source: "arxiv", latency_ms: tracked.latency_ms },
173
+ });
174
+ }
175
+ return toolResult({
176
+ ...papers[0],
177
+ _source_health: { source: "arxiv", latency_ms: tracked.latency_ms },
178
+ });
131
179
  },
132
180
  },
133
181
  ];
@@ -0,0 +1,142 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
3
+ import { toolResult, trackedFetch, isTrackedError } from "./util.js";
4
+
5
+ const BASE = "https://api.biorxiv.org";
6
+
7
+ export function createBiorxivTools(
8
+ _ctx: OpenClawPluginToolContext,
9
+ _api: OpenClawPluginApi,
10
+ ) {
11
+ return [
12
+ {
13
+ name: "search_biorxiv",
14
+ label: "Search Preprints (bioRxiv)",
15
+ description:
16
+ "Get recent bioRxiv preprints by date range. Covers biology preprints (300K+). Use date range (e.g. '2026-03-01/2026-03-18'), recent count (e.g. '50'), or recent days (e.g. '7d').",
17
+ parameters: Type.Object({
18
+ interval: Type.String({
19
+ description:
20
+ "Date range 'YYYY-MM-DD/YYYY-MM-DD', recent count '50', or recent days '7d'",
21
+ }),
22
+ cursor: Type.Optional(
23
+ Type.Number({ description: "Pagination offset (default 0, each page returns up to 100)" }),
24
+ ),
25
+ }),
26
+ execute: async (input: { interval: string; cursor?: number }) => {
27
+ const cursor = input.cursor ?? 0;
28
+ const tracked = await trackedFetch("biorxiv", `${BASE}/details/biorxiv/${input.interval}/${cursor}/json`, undefined, 30_000);
29
+ if (isTrackedError(tracked)) return tracked;
30
+ const data = await tracked.res.json();
31
+
32
+ const meta = data.messages?.[0];
33
+ const papers = (data.collection ?? []).map(
34
+ (p: Record<string, unknown>) => ({
35
+ doi: p.doi,
36
+ title: p.title,
37
+ authors: typeof p.authors === "string"
38
+ ? (p.authors as string).split("; ").filter(Boolean)
39
+ : [],
40
+ abstract: p.abstract,
41
+ date: p.date,
42
+ category: p.category,
43
+ version: p.version,
44
+ license: p.license,
45
+ url: p.doi ? `https://www.biorxiv.org/content/${p.doi}` : undefined,
46
+ pdf_url: p.doi ? `https://www.biorxiv.org/content/${p.doi}v${p.version ?? 1}.full.pdf` : undefined,
47
+ source: "biorxiv",
48
+ }),
49
+ );
50
+
51
+ return toolResult({
52
+ total_results: meta?.total ?? papers.length,
53
+ cursor: meta?.cursor,
54
+ papers,
55
+ });
56
+ },
57
+ },
58
+ {
59
+ name: "search_medrxiv",
60
+ label: "Search Preprints (medRxiv)",
61
+ description:
62
+ "Get recent medRxiv preprints by date range. Covers medical/health science preprints (100K+).",
63
+ parameters: Type.Object({
64
+ interval: Type.String({
65
+ description:
66
+ "Date range 'YYYY-MM-DD/YYYY-MM-DD', recent count '50', or recent days '7d'",
67
+ }),
68
+ cursor: Type.Optional(
69
+ Type.Number({ description: "Pagination offset (default 0)" }),
70
+ ),
71
+ }),
72
+ execute: async (input: { interval: string; cursor?: number }) => {
73
+ const cursor = input.cursor ?? 0;
74
+ const tracked = await trackedFetch("medrxiv", `${BASE}/details/medrxiv/${input.interval}/${cursor}/json`, undefined, 30_000);
75
+ if (isTrackedError(tracked)) return tracked;
76
+ const data = await tracked.res.json();
77
+
78
+ const meta = data.messages?.[0];
79
+ const papers = (data.collection ?? []).map(
80
+ (p: Record<string, unknown>) => ({
81
+ doi: p.doi,
82
+ title: p.title,
83
+ authors: typeof p.authors === "string"
84
+ ? (p.authors as string).split("; ").filter(Boolean)
85
+ : [],
86
+ abstract: p.abstract,
87
+ date: p.date,
88
+ category: p.category,
89
+ version: p.version,
90
+ url: p.doi ? `https://www.medrxiv.org/content/${p.doi}` : undefined,
91
+ pdf_url: p.doi ? `https://www.medrxiv.org/content/${p.doi}v${p.version ?? 1}.full.pdf` : undefined,
92
+ source: "medrxiv",
93
+ }),
94
+ );
95
+
96
+ return toolResult({
97
+ total_results: meta?.total ?? papers.length,
98
+ cursor: meta?.cursor,
99
+ papers,
100
+ });
101
+ },
102
+ },
103
+ {
104
+ name: "get_preprint_by_doi",
105
+ label: "Get Preprint by DOI (bioRxiv/medRxiv)",
106
+ description:
107
+ "Get a specific bioRxiv or medRxiv preprint by its DOI.",
108
+ parameters: Type.Object({
109
+ doi: Type.String({ description: "DOI of the preprint (e.g. '10.1101/2024.01.15.575123')" }),
110
+ server: Type.Optional(
111
+ Type.String({ description: "Server: 'biorxiv' or 'medrxiv' (default: biorxiv)" }),
112
+ ),
113
+ }),
114
+ execute: async (input: { doi: string; server?: string }) => {
115
+ const server = input.server ?? "biorxiv";
116
+ const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
117
+ const tracked = await trackedFetch(server, `${BASE}/details/${server}/${doi}/na/json`, undefined, 15_000);
118
+ if (isTrackedError(tracked)) return tracked;
119
+ const data = await tracked.res.json();
120
+ const papers = data.collection ?? [];
121
+ if (papers.length === 0) return toolResult({ error: "Preprint not found" });
122
+
123
+ const p = papers[0] as Record<string, unknown>;
124
+ return toolResult({
125
+ doi: p.doi,
126
+ title: p.title,
127
+ authors: typeof p.authors === "string"
128
+ ? (p.authors as string).split("; ").filter(Boolean)
129
+ : [],
130
+ abstract: p.abstract,
131
+ date: p.date,
132
+ category: p.category,
133
+ version: p.version,
134
+ license: p.license,
135
+ url: `https://www.${server}.org/content/${p.doi}`,
136
+ pdf_url: `https://www.${server}.org/content/${p.doi}v${p.version ?? 1}.full.pdf`,
137
+ source: server,
138
+ });
139
+ },
140
+ },
141
+ ];
142
+ }
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
3
- import { toolResult } from "./util.js";
3
+ import { toolResult, trackedFetch, isTrackedError } from "./util.js";
4
4
 
5
5
  const BASE = "https://api.crossref.org";
6
6
 
@@ -25,11 +25,9 @@ export function createCrossRefTools(
25
25
  }),
26
26
  execute: async (input: { doi: string }) => {
27
27
  const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
28
- const res = await fetch(`${BASE}/works/${encodeURIComponent(doi)}`, {
29
- headers,
30
- });
31
- if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
32
- const data = await res.json();
28
+ const tracked = await trackedFetch("crossref", `${BASE}/works/${encodeURIComponent(doi)}`, { headers });
29
+ if (isTrackedError(tracked)) return tracked;
30
+ const data = await tracked.res.json();
33
31
  const w = data.message;
34
32
  return toolResult({
35
33
  doi: w.DOI,
@@ -46,6 +44,7 @@ export function createCrossRefTools(
46
44
  url: w.URL,
47
45
  abstract: w.abstract,
48
46
  license: w.license?.[0]?.URL,
47
+ _source_health: { source: "crossref", latency_ms: tracked.latency_ms },
49
48
  });
50
49
  },
51
50
  },
@@ -53,47 +52,82 @@ export function createCrossRefTools(
53
52
  name: "search_crossref",
54
53
  label: "Search Works (CrossRef)",
55
54
  description:
56
- "Search CrossRef for scholarly works by query. Covers 150M+ DOIs across all publishers.",
55
+ "Search CrossRef for scholarly works. Covers 150M+ DOIs across ALL publishers and disciplines. Supports journal/ISSN filtering, year range, work type, and citation-count sorting. Best general-purpose academic search tool.",
57
56
  parameters: Type.Object({
58
- query: Type.String({ description: "Search query" }),
59
- limit: Type.Optional(
60
- Type.Number({ description: "Max results (default 10, max 100)" }),
57
+ query: Type.String({ description: "Search query (keywords)" }),
58
+ journal: Type.Optional(
59
+ Type.String({
60
+ description:
61
+ "Journal name filter (container-title). E.g. 'Nature', 'American Economic Review', 'The Lancet'",
62
+ }),
63
+ ),
64
+ issn: Type.Optional(
65
+ Type.String({
66
+ description: "Journal ISSN filter. E.g. '0028-0836' for Nature, '0002-8282' for AER",
67
+ }),
61
68
  ),
62
69
  from_year: Type.Optional(
63
- Type.Number({ description: "Published from this year onward" }),
70
+ Type.Number({ description: "Published from this year onward (inclusive)" }),
71
+ ),
72
+ until_year: Type.Optional(
73
+ Type.Number({ description: "Published until this year (inclusive)" }),
64
74
  ),
65
75
  type: Type.Optional(
66
76
  Type.String({
67
77
  description:
68
- "Work type filter: 'journal-article', 'book-chapter', 'proceedings-article', etc.",
78
+ "Work type: 'journal-article', 'book-chapter', 'proceedings-article', 'posted-content' (preprint), 'dissertation'",
69
79
  }),
70
80
  ),
81
+ has_abstract: Type.Optional(
82
+ Type.Boolean({ description: "Only results with abstracts" }),
83
+ ),
71
84
  sort: Type.Optional(
72
85
  Type.String({
73
- description: "Sort by: 'relevance', 'published', 'is-referenced-by-count'",
86
+ description:
87
+ "Sort by: 'relevance' (default), 'published' (newest first), 'is-referenced-by-count' (most cited)",
74
88
  }),
75
89
  ),
90
+ limit: Type.Optional(
91
+ Type.Number({ description: "Max results (default 10, max 100)" }),
92
+ ),
76
93
  }),
77
94
  execute: async (input: {
78
95
  query: string;
79
- limit?: number;
96
+ journal?: string;
97
+ issn?: string;
80
98
  from_year?: number;
99
+ until_year?: number;
81
100
  type?: string;
101
+ has_abstract?: boolean;
82
102
  sort?: string;
103
+ limit?: number;
83
104
  }) => {
84
105
  const params = new URLSearchParams({
85
106
  query: input.query,
86
107
  rows: String(Math.min(input.limit ?? 10, 100)),
87
108
  });
88
- if (input.from_year)
89
- params.set("filter", `from-pub-date:${input.from_year}`);
90
- if (input.type)
91
- params.append("filter", `type:${input.type}`);
92
- if (input.sort) params.set("sort", input.sort);
93
109
 
94
- const res = await fetch(`${BASE}/works?${params}`, { headers });
95
- if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
96
- const data = await res.json();
110
+ // Build filter chain
111
+ const filters: string[] = [];
112
+ if (input.from_year) filters.push(`from-pub-date:${input.from_year}`);
113
+ if (input.until_year) filters.push(`until-pub-date:${input.until_year}`);
114
+ if (input.type) filters.push(`type:${input.type}`);
115
+ if (input.has_abstract) filters.push("has-abstract:true");
116
+ if (input.issn) filters.push(`issn:${input.issn}`);
117
+ if (filters.length > 0) params.set("filter", filters.join(","));
118
+
119
+ // Journal name as query.container-title (separate from filter)
120
+ if (input.journal) params.set("query.container-title", input.journal);
121
+
122
+ if (input.sort) {
123
+ params.set("sort", input.sort);
124
+ params.set("order", "desc");
125
+ }
126
+
127
+ const tracked = await trackedFetch("crossref", `${BASE}/works?${params}`, { headers });
128
+ if (isTrackedError(tracked)) return tracked;
129
+ const data = await tracked.res.json();
130
+
97
131
  return toolResult({
98
132
  total_results: data.message?.["total-results"],
99
133
  items: data.message?.items?.map((w: Record<string, unknown>) => ({
@@ -107,7 +141,11 @@ export function createCrossRefTools(
107
141
  (w.published as Record<string, unknown>)?.["date-parts"],
108
142
  type: w.type,
109
143
  cited_by: w["is-referenced-by-count"],
144
+ abstract: typeof w.abstract === "string"
145
+ ? (w.abstract as string).replace(/<[^>]*>/g, "").slice(0, 300)
146
+ : undefined,
110
147
  })),
148
+ _source_health: { source: "crossref", latency_ms: tracked.latency_ms },
111
149
  });
112
150
  },
113
151
  },
@@ -0,0 +1,188 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
3
+ import { toolResult, trackedFetch, isTrackedError } from "./util.js";
4
+
5
+ const BASE = "https://api.datacite.org";
6
+
7
+ export function createDataCiteTools(
8
+ _ctx: OpenClawPluginToolContext,
9
+ _api: OpenClawPluginApi,
10
+ ) {
11
+ return [
12
+ {
13
+ name: "search_datacite",
14
+ label: "Search Datasets & DOIs (DataCite)",
15
+ description:
16
+ "Search DataCite for datasets, software, and other research outputs (50M+ DOIs). Covers Zenodo, Figshare, Dryad, and 2000+ repositories. Best for finding datasets, software, and non-journal outputs.",
17
+ parameters: Type.Object({
18
+ query: Type.String({
19
+ description: "Search query for datasets, software, or other research outputs",
20
+ }),
21
+ max_results: Type.Optional(
22
+ Type.Number({ description: "Max results (default 10, max 100)" }),
23
+ ),
24
+ resource_type: Type.Optional(
25
+ Type.String({
26
+ description:
27
+ "Filter by type: 'Dataset', 'Software', 'Text', 'Collection', 'Audiovisual', 'Image', etc.",
28
+ }),
29
+ ),
30
+ from_year: Type.Optional(
31
+ Type.Number({ description: "Published from this year onward" }),
32
+ ),
33
+ }),
34
+ execute: async (input: {
35
+ query: string;
36
+ max_results?: number;
37
+ resource_type?: string;
38
+ from_year?: number;
39
+ }) => {
40
+ const pageSize = Math.min(input.max_results ?? 10, 100);
41
+ const params = new URLSearchParams({
42
+ query: input.query,
43
+ "page[size]": String(pageSize),
44
+ });
45
+ if (input.resource_type) {
46
+ params.set("resource-type-id", input.resource_type.toLowerCase());
47
+ }
48
+ if (input.from_year) {
49
+ params.set("query", `${input.query} AND publicationYear:[${input.from_year} TO *]`);
50
+ }
51
+
52
+ const tracked = await trackedFetch("datacite", `${BASE}/dois?${params}`, undefined, 15_000);
53
+ if (isTrackedError(tracked)) return tracked;
54
+ const data = await tracked.res.json();
55
+
56
+ const items = (data.data ?? []).map(
57
+ (item: Record<string, unknown>) => {
58
+ const attrs = item.attributes as Record<string, unknown> | undefined;
59
+ if (!attrs) return { id: item.id };
60
+
61
+ const titles = attrs.titles as Array<{ title: string }> | undefined;
62
+ const creators = attrs.creators as Array<{
63
+ name: string;
64
+ nameType?: string;
65
+ givenName?: string;
66
+ familyName?: string;
67
+ }> | undefined;
68
+ const types = attrs.types as Record<string, string> | undefined;
69
+ const descriptions = attrs.descriptions as Array<{
70
+ description: string;
71
+ descriptionType: string;
72
+ }> | undefined;
73
+ const dates = attrs.dates as Array<{
74
+ date: string;
75
+ dateType: string;
76
+ }> | undefined;
77
+
78
+ const issuedDate = dates?.find((d) => d.dateType === "Issued")?.date;
79
+
80
+ return {
81
+ doi: attrs.doi,
82
+ title: titles?.[0]?.title,
83
+ creators: creators?.slice(0, 5).map((c) =>
84
+ c.givenName && c.familyName
85
+ ? `${c.givenName} ${c.familyName}`
86
+ : c.name,
87
+ ),
88
+ publisher: attrs.publisher,
89
+ publication_year: attrs.publicationYear,
90
+ resource_type: types?.resourceTypeGeneral,
91
+ description: descriptions?.find(
92
+ (d) => d.descriptionType === "Abstract",
93
+ )?.description,
94
+ url: attrs.url,
95
+ issued_date: issuedDate,
96
+ citation_count: attrs.citationCount,
97
+ view_count: attrs.viewCount,
98
+ download_count: attrs.downloadCount,
99
+ };
100
+ },
101
+ );
102
+
103
+ return toolResult({
104
+ total_results: data.meta?.total,
105
+ items,
106
+ _source_health: { source: "datacite", latency_ms: tracked.latency_ms },
107
+ });
108
+ },
109
+ },
110
+ {
111
+ name: "resolve_datacite_doi",
112
+ label: "Resolve DOI (DataCite)",
113
+ description:
114
+ "Resolve a DataCite DOI to get full metadata. Best for DOIs from Zenodo, Figshare, Dryad, and other data repositories (10.5281/*, 10.6084/*, etc.).",
115
+ parameters: Type.Object({
116
+ doi: Type.String({
117
+ description: "DOI to resolve, e.g. '10.5281/zenodo.1234567'",
118
+ }),
119
+ }),
120
+ execute: async (input: { doi: string }) => {
121
+ const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
122
+
123
+ const tracked = await trackedFetch(
124
+ "datacite",
125
+ `${BASE}/dois/${encodeURIComponent(doi)}`,
126
+ undefined,
127
+ 15_000,
128
+ );
129
+ if (isTrackedError(tracked)) return tracked;
130
+ const data = await tracked.res.json();
131
+
132
+ const attrs = data.data?.attributes as Record<string, unknown> | undefined;
133
+ if (!attrs) return toolResult({ error: "DOI not found or no attributes" });
134
+
135
+ const titles = attrs.titles as Array<{ title: string }> | undefined;
136
+ const creators = attrs.creators as Array<{
137
+ name: string;
138
+ givenName?: string;
139
+ familyName?: string;
140
+ }> | undefined;
141
+ const types = attrs.types as Record<string, string> | undefined;
142
+ const descriptions = attrs.descriptions as Array<{
143
+ description: string;
144
+ descriptionType: string;
145
+ }> | undefined;
146
+ const dates = attrs.dates as Array<{
147
+ date: string;
148
+ dateType: string;
149
+ }> | undefined;
150
+ const rights = attrs.rightsList as Array<{
151
+ rights: string;
152
+ rightsUri?: string;
153
+ rightsIdentifier?: string;
154
+ }> | undefined;
155
+ const subjects = attrs.subjects as Array<{
156
+ subject: string;
157
+ }> | undefined;
158
+
159
+ return toolResult({
160
+ doi: attrs.doi,
161
+ title: titles?.[0]?.title,
162
+ creators: creators?.map((c) =>
163
+ c.givenName && c.familyName
164
+ ? `${c.givenName} ${c.familyName}`
165
+ : c.name,
166
+ ),
167
+ publisher: attrs.publisher,
168
+ publication_year: attrs.publicationYear,
169
+ resource_type: types?.resourceTypeGeneral,
170
+ resource_type_specific: types?.resourceType || undefined,
171
+ description: descriptions?.find(
172
+ (d) => d.descriptionType === "Abstract",
173
+ )?.description,
174
+ url: attrs.url,
175
+ dates: dates?.map((d) => ({ date: d.date, type: d.dateType })),
176
+ license: rights?.[0]?.rightsIdentifier ?? rights?.[0]?.rights,
177
+ license_url: rights?.[0]?.rightsUri,
178
+ subjects: subjects?.map((s) => s.subject),
179
+ citation_count: attrs.citationCount,
180
+ view_count: attrs.viewCount,
181
+ download_count: attrs.downloadCount,
182
+ version: attrs.version,
183
+ _source_health: { source: "datacite", latency_ms: tracked.latency_ms },
184
+ });
185
+ },
186
+ },
187
+ ];
188
+ }