@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
@@ -0,0 +1,158 @@
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.openaire.eu";
6
+
7
+ /**
8
+ * Parse OpenAIRE XML response wrapped in JSON.
9
+ * OpenAIRE returns XML by default; with format=json it wraps XML nodes in JSON objects.
10
+ */
11
+ function parseOpenAireResult(result: Record<string, unknown>): Record<string, unknown> {
12
+ const metadata = result.metadata as Record<string, unknown> | undefined;
13
+ const oafEntity = metadata?.["oaf:entity"] as Record<string, unknown> | undefined;
14
+ const oafResult = oafEntity?.["oaf:result"] as Record<string, unknown> | undefined;
15
+
16
+ if (!oafResult) {
17
+ // Flat JSON format (newer API versions)
18
+ return {
19
+ title: result.title,
20
+ authors: result.authors,
21
+ doi: result.doi,
22
+ url: result.url,
23
+ };
24
+ }
25
+
26
+ // Extract title
27
+ const titleRaw = oafResult.title;
28
+ let title = "";
29
+ if (Array.isArray(titleRaw)) {
30
+ const main = titleRaw.find((t: Record<string, string>) => t["@classid"] === "main title");
31
+ title = (main ?? titleRaw[0])?.["$"] ?? "";
32
+ } else if (typeof titleRaw === "object" && titleRaw !== null) {
33
+ title = (titleRaw as Record<string, string>)["$"] ?? "";
34
+ }
35
+
36
+ // Extract authors
37
+ const creatorsRaw = oafResult.creator;
38
+ const authors: string[] = [];
39
+ if (Array.isArray(creatorsRaw)) {
40
+ for (const c of creatorsRaw) {
41
+ const name = typeof c === "string" ? c : (c as Record<string, string>)["$"];
42
+ if (name) authors.push(name);
43
+ }
44
+ } else if (creatorsRaw) {
45
+ const name = typeof creatorsRaw === "string" ? creatorsRaw : (creatorsRaw as Record<string, string>)["$"];
46
+ if (name) authors.push(name);
47
+ }
48
+
49
+ // Extract DOI and other identifiers
50
+ const pidRaw = oafResult.pid;
51
+ let doi: string | undefined;
52
+ if (Array.isArray(pidRaw)) {
53
+ const doiPid = pidRaw.find((p: Record<string, string>) => p["@classid"] === "doi");
54
+ doi = (doiPid as Record<string, string> | undefined)?.["$"];
55
+ } else if (typeof pidRaw === "object" && pidRaw !== null) {
56
+ if ((pidRaw as Record<string, string>)["@classid"] === "doi") {
57
+ doi = (pidRaw as Record<string, string>)["$"];
58
+ }
59
+ }
60
+
61
+ // Extract dates
62
+ const dateRaw = oafResult.dateofacceptance;
63
+ const date = typeof dateRaw === "string" ? dateRaw : (dateRaw as Record<string, string> | undefined)?.["$"];
64
+
65
+ // Extract best access right
66
+ const accessRaw = oafResult.bestaccessright;
67
+ const isOa = typeof accessRaw === "object" && accessRaw !== null
68
+ ? (accessRaw as Record<string, string>)["@classid"] === "OPEN"
69
+ : false;
70
+
71
+ return {
72
+ title,
73
+ authors,
74
+ doi,
75
+ date,
76
+ is_oa: isOa,
77
+ url: doi ? `https://doi.org/${doi}` : undefined,
78
+ };
79
+ }
80
+
81
+ export function createOpenAireTools(
82
+ _ctx: OpenClawPluginToolContext,
83
+ _api: OpenClawPluginApi,
84
+ ) {
85
+ return [
86
+ {
87
+ name: "search_openaire",
88
+ label: "Search Publications (OpenAIRE)",
89
+ description:
90
+ "Search OpenAIRE for publications (170M+ records). Uniquely supports filtering by EU funding source (EC, NSF, etc.) and project ID.",
91
+ parameters: Type.Object({
92
+ keywords: Type.String({
93
+ description: "Search keywords",
94
+ }),
95
+ author: Type.Optional(Type.String({ description: "Author name" })),
96
+ doi: Type.Optional(Type.String({ description: "DOI" })),
97
+ from_date: Type.Optional(
98
+ Type.String({ description: "From date (YYYY-MM-DD)" }),
99
+ ),
100
+ to_date: Type.Optional(
101
+ Type.String({ description: "To date (YYYY-MM-DD)" }),
102
+ ),
103
+ oa_only: Type.Optional(
104
+ Type.Boolean({ description: "Only open access results" }),
105
+ ),
106
+ funder: Type.Optional(
107
+ Type.String({
108
+ description: "Funder abbreviation: 'EC' (EU), 'NSF', 'NIH', 'UKRI', 'DFG', etc.",
109
+ }),
110
+ ),
111
+ max_results: Type.Optional(
112
+ Type.Number({ description: "Max results (default 10, max 50)" }),
113
+ ),
114
+ }),
115
+ execute: async (input: {
116
+ keywords: string;
117
+ author?: string;
118
+ doi?: string;
119
+ from_date?: string;
120
+ to_date?: string;
121
+ oa_only?: boolean;
122
+ funder?: string;
123
+ max_results?: number;
124
+ }) => {
125
+ const params = new URLSearchParams({
126
+ keywords: input.keywords,
127
+ format: "json",
128
+ size: String(Math.min(input.max_results ?? 10, 50)),
129
+ });
130
+ if (input.author) params.set("author", input.author);
131
+ if (input.doi) params.set("doi", input.doi);
132
+ if (input.from_date) params.set("fromDateAccepted", input.from_date);
133
+ if (input.to_date) params.set("toDateAccepted", input.to_date);
134
+ if (input.oa_only) params.set("OA", "true");
135
+ if (input.funder) params.set("funder", input.funder);
136
+
137
+ const tracked = await trackedFetch("openaire", `${BASE}/search/publications?${params}`, undefined, 15_000);
138
+ if (isTrackedError(tracked)) return tracked;
139
+ const data = await tracked.res.json();
140
+
141
+ const response = data.response as Record<string, unknown> | undefined;
142
+ const header = response?.header as Record<string, unknown> | undefined;
143
+ const total = header?.total as Record<string, string> | undefined;
144
+ const results = response?.results as Record<string, unknown> | undefined;
145
+ const resultList = results?.result;
146
+
147
+ const papers = (Array.isArray(resultList) ? resultList : []).map(
148
+ (r: Record<string, unknown>) => parseOpenAireResult(r),
149
+ );
150
+
151
+ return toolResult({
152
+ total_results: total?.["$"] ? parseInt(total["$"], 10) : papers.length,
153
+ papers,
154
+ });
155
+ },
156
+ },
157
+ ];
158
+ }
@@ -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.openalex.org";
6
6
 
@@ -58,9 +58,9 @@ export function createOpenAlexTools(
58
58
  if (filters.length > 0) params.set("filter", filters.join(","));
59
59
  if (input.sort_by) params.set("sort", input.sort_by);
60
60
 
61
- const res = await fetch(`${BASE}/works?${params}`, { headers });
62
- if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
63
- const data = await res.json();
61
+ const tracked = await trackedFetch("openalex", `${BASE}/works?${params}`, { headers });
62
+ if (isTrackedError(tracked)) return tracked;
63
+ const data = await tracked.res.json();
64
64
  return toolResult({
65
65
  total_count: data.meta?.count,
66
66
  results: data.results?.map((w: Record<string, unknown>) => ({
@@ -78,6 +78,7 @@ export function createOpenAlexTools(
78
78
  )
79
79
  : [],
80
80
  })),
81
+ _source_health: { source: "openalex", latency_ms: tracked.latency_ms },
81
82
  });
82
83
  },
83
84
  },
@@ -96,11 +97,9 @@ export function createOpenAlexTools(
96
97
  const id = input.work_id.startsWith("10.")
97
98
  ? `https://doi.org/${input.work_id}`
98
99
  : input.work_id;
99
- const res = await fetch(`${BASE}/works/${encodeURIComponent(id)}`, {
100
- headers,
101
- });
102
- if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
103
- const w = await res.json();
100
+ const tracked = await trackedFetch("openalex", `${BASE}/works/${encodeURIComponent(id)}`, { headers });
101
+ if (isTrackedError(tracked)) return tracked;
102
+ const w = await tracked.res.json();
104
103
  return toolResult({
105
104
  id: w.id,
106
105
  doi: w.doi,
@@ -120,6 +119,7 @@ export function createOpenAlexTools(
120
119
  ?.slice(0, 10)
121
120
  .map((c: Record<string, unknown>) => c.display_name),
122
121
  referenced_works_count: w.referenced_works?.length,
122
+ _source_health: { source: "openalex", latency_ms: tracked.latency_ms },
123
123
  });
124
124
  },
125
125
  },
@@ -148,14 +148,18 @@ export function createOpenAlexTools(
148
148
  search: input.author_id,
149
149
  per_page: "5",
150
150
  });
151
- const res = await fetch(`${BASE}/authors?${params}`, { headers });
152
- if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
153
- return toolResult(await res.json());
151
+ const tracked = await trackedFetch("openalex", `${BASE}/authors?${params}`, { headers });
152
+ if (isTrackedError(tracked)) return tracked;
153
+ const data = await tracked.res.json();
154
+ return toolResult({
155
+ ...data,
156
+ _source_health: { source: "openalex", latency_ms: tracked.latency_ms },
157
+ });
154
158
  }
155
159
 
156
- const res = await fetch(url, { headers });
157
- if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
158
- const a = await res.json();
160
+ const tracked = await trackedFetch("openalex", url, { headers });
161
+ if (isTrackedError(tracked)) return tracked;
162
+ const a = await tracked.res.json();
159
163
  return toolResult({
160
164
  id: a.id,
161
165
  display_name: a.display_name,
@@ -171,6 +175,7 @@ export function createOpenAlexTools(
171
175
  top_concepts: a.x_concepts
172
176
  ?.slice(0, 5)
173
177
  .map((c: Record<string, unknown>) => c.display_name),
178
+ _source_health: { source: "openalex", latency_ms: tracked.latency_ms },
174
179
  });
175
180
  },
176
181
  },
@@ -0,0 +1,103 @@
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.opencitations.net";
6
+
7
+ /**
8
+ * Extract DOI from OpenCitations multi-identifier string.
9
+ * Format: "omid:br/... doi:10.1038/nature12373 openalex:W..."
10
+ */
11
+ function extractDoi(multiId: string): string | undefined {
12
+ const match = multiId.match(/doi:(10\.\S+)/);
13
+ return match ? match[1] : undefined;
14
+ }
15
+
16
+ export function createOpenCitationsTools(
17
+ _ctx: OpenClawPluginToolContext,
18
+ _api: OpenClawPluginApi,
19
+ ) {
20
+ return [
21
+ {
22
+ name: "get_citations_open",
23
+ label: "Get Citations (OpenCitations)",
24
+ description:
25
+ "Get all papers citing a given DOI using OpenCitations (2B+ open citation links). Works across all disciplines.",
26
+ parameters: Type.Object({
27
+ doi: Type.String({
28
+ description: "DOI of the paper, e.g. '10.1038/nature12373'",
29
+ }),
30
+ }),
31
+ execute: async (input: { doi: string }) => {
32
+ const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
33
+ const tracked = await trackedFetch("opencitations", `${BASE}/index/v2/citations/doi:${encodeURIComponent(doi)}`);
34
+ if (isTrackedError(tracked)) return tracked;
35
+ const data = await tracked.res.json();
36
+
37
+ if (!Array.isArray(data)) return toolResult({ error: "Unexpected response format" });
38
+
39
+ return toolResult({
40
+ total_citations: data.length,
41
+ citations: data.slice(0, 100).map((c: Record<string, string>) => ({
42
+ citing_doi: extractDoi(c.citing ?? ""),
43
+ cited_doi: extractDoi(c.cited ?? ""),
44
+ creation_date: c.creation,
45
+ })),
46
+ _source_health: { source: "opencitations", latency_ms: tracked.latency_ms },
47
+ });
48
+ },
49
+ },
50
+ {
51
+ name: "get_references_open",
52
+ label: "Get References (OpenCitations)",
53
+ description:
54
+ "Get all references of a paper by DOI. Shows what a paper cites.",
55
+ parameters: Type.Object({
56
+ doi: Type.String({
57
+ description: "DOI of the paper, e.g. '10.1038/nature12373'",
58
+ }),
59
+ }),
60
+ execute: async (input: { doi: string }) => {
61
+ const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
62
+ const tracked = await trackedFetch("opencitations", `${BASE}/index/v2/references/doi:${encodeURIComponent(doi)}`);
63
+ if (isTrackedError(tracked)) return tracked;
64
+ const data = await tracked.res.json();
65
+
66
+ if (!Array.isArray(data)) return toolResult({ error: "Unexpected response format" });
67
+
68
+ return toolResult({
69
+ total_references: data.length,
70
+ references: data.slice(0, 100).map((r: Record<string, string>) => ({
71
+ cited_doi: extractDoi(r.cited ?? ""),
72
+ citing_doi: extractDoi(r.citing ?? ""),
73
+ creation_date: r.creation,
74
+ })),
75
+ _source_health: { source: "opencitations", latency_ms: tracked.latency_ms },
76
+ });
77
+ },
78
+ },
79
+ {
80
+ name: "get_citation_count",
81
+ label: "Get Citation Count (OpenCitations)",
82
+ description:
83
+ "Get the total citation count for a DOI from OpenCitations open data.",
84
+ parameters: Type.Object({
85
+ doi: Type.String({
86
+ description: "DOI of the paper, e.g. '10.1038/nature12373'",
87
+ }),
88
+ }),
89
+ execute: async (input: { doi: string }) => {
90
+ const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
91
+ const tracked = await trackedFetch("opencitations", `${BASE}/index/v2/citation-count/doi:${encodeURIComponent(doi)}`);
92
+ if (isTrackedError(tracked)) return tracked;
93
+ const data = await tracked.res.json();
94
+
95
+ const count = Array.isArray(data) && data[0]?.count
96
+ ? parseInt(data[0].count, 10)
97
+ : 0;
98
+
99
+ return toolResult({ doi, citation_count: count, _source_health: { source: "opencitations", latency_ms: tracked.latency_ms } });
100
+ },
101
+ },
102
+ ];
103
+ }
@@ -0,0 +1,136 @@
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://pub.orcid.org/v3.0";
6
+
7
+ const HEADERS: Record<string, string> = {
8
+ Accept: "application/json",
9
+ };
10
+
11
+ export function createOrcidTools(
12
+ _ctx: OpenClawPluginToolContext,
13
+ _api: OpenClawPluginApi,
14
+ ) {
15
+ return [
16
+ {
17
+ name: "search_orcid",
18
+ label: "Search Researchers (ORCID)",
19
+ description:
20
+ "Search the ORCID registry for researchers by name, affiliation, or keyword. Returns ORCID iDs that can be used with get_orcid_works.",
21
+ parameters: Type.Object({
22
+ query: Type.String({
23
+ description:
24
+ "Lucene-style query. Supports fields: family-name, given-names, affiliation-org-name, keyword, orcid, ringgold-org-id. E.g. 'family-name:Smith AND affiliation-org-name:MIT'",
25
+ }),
26
+ limit: Type.Optional(
27
+ Type.Number({ description: "Max results (default 10, max 100)" }),
28
+ ),
29
+ }),
30
+ execute: async (input: { query: string; limit?: number }) => {
31
+ const params = new URLSearchParams({
32
+ q: input.query,
33
+ rows: String(Math.min(input.limit ?? 10, 100)),
34
+ });
35
+
36
+ const result = await trackedFetch(
37
+ "orcid",
38
+ `${BASE}/search/?${params}`,
39
+ { headers: HEADERS },
40
+ 10_000,
41
+ );
42
+ if (isTrackedError(result)) return result;
43
+ const data = await result.res.json();
44
+
45
+ const researchers = (data.result ?? []).map(
46
+ (r: Record<string, unknown>) => {
47
+ const oi = r["orcid-identifier"] as Record<string, string> | undefined;
48
+ return {
49
+ orcid: oi?.path,
50
+ uri: oi?.uri,
51
+ source: "orcid",
52
+ };
53
+ },
54
+ );
55
+
56
+ return toolResult({
57
+ total_results: data["num-found"],
58
+ source: "orcid",
59
+ researchers,
60
+ _latency_ms: result.latency_ms,
61
+ });
62
+ },
63
+ },
64
+ {
65
+ name: "get_orcid_works",
66
+ label: "Get Works by ORCID (ORCID)",
67
+ description:
68
+ "Get the publication list for a researcher by their ORCID iD. Returns titles, DOIs, publication years, and work types.",
69
+ parameters: Type.Object({
70
+ orcid: Type.String({
71
+ description: "ORCID iD, e.g. '0000-0002-1825-0097'",
72
+ }),
73
+ }),
74
+ execute: async (input: { orcid: string }) => {
75
+ const orcid = input.orcid.replace(/^https?:\/\/orcid\.org\//, "");
76
+
77
+ const result = await trackedFetch(
78
+ "orcid",
79
+ `${BASE}/${encodeURIComponent(orcid)}/works`,
80
+ { headers: HEADERS },
81
+ 10_000,
82
+ );
83
+ if (isTrackedError(result)) return result;
84
+ const data = await result.res.json();
85
+
86
+ const groups = data.group as Array<Record<string, unknown>> | undefined;
87
+
88
+ const works = (groups ?? []).map((g) => {
89
+ const summaries = g["work-summary"] as Array<Record<string, unknown>> | undefined;
90
+ const first = summaries?.[0];
91
+ if (!first) return null;
92
+
93
+ // Extract title
94
+ const titleObj = first.title as Record<string, unknown> | undefined;
95
+ const title = (titleObj?.title as Record<string, string> | undefined)?.value;
96
+
97
+ // Extract publication date
98
+ const pubDate = first["publication-date"] as Record<string, unknown> | undefined;
99
+ const year = (pubDate?.year as Record<string, string> | undefined)?.value;
100
+ const month = (pubDate?.month as Record<string, string> | undefined)?.value;
101
+
102
+ // Extract DOI from external-ids
103
+ const extIds = first["external-ids"] as Record<string, unknown> | undefined;
104
+ const extIdList = extIds?.["external-id"] as Array<Record<string, unknown>> | undefined;
105
+ const doiEntry = extIdList?.find(
106
+ (e) => e["external-id-type"] === "doi" && e["external-id-relationship"] === "self",
107
+ );
108
+ const doi = doiEntry?.["external-id-value"] as string | undefined;
109
+
110
+ // Extract journal title
111
+ const journalTitle = first["journal-title"] as Record<string, string> | undefined;
112
+
113
+ return {
114
+ title,
115
+ doi,
116
+ year,
117
+ month,
118
+ type: first.type,
119
+ journal: journalTitle?.value,
120
+ url: doi ? `https://doi.org/${doi}` : undefined,
121
+ authors: undefined as string[] | undefined, // ORCID works endpoint does not include author lists
122
+ source: "orcid",
123
+ };
124
+ }).filter(Boolean);
125
+
126
+ return toolResult({
127
+ orcid,
128
+ total_works: works.length,
129
+ source: "orcid",
130
+ works,
131
+ _latency_ms: result.latency_ms,
132
+ });
133
+ },
134
+ },
135
+ ];
136
+ }
@@ -0,0 +1,104 @@
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.osf.io/v2";
6
+
7
+ export function createOsfPreprintsTools(
8
+ _ctx: OpenClawPluginToolContext,
9
+ _api: OpenClawPluginApi,
10
+ ) {
11
+ return [
12
+ {
13
+ name: "search_osf_preprints",
14
+ label: "Search Preprints (OSF)",
15
+ description:
16
+ "Search OSF Preprints, an aggregator covering 180K+ preprints across multiple providers including SocArXiv, PsyArXiv, EarthArXiv, EngrXiv, and NutriXiv. Covers social sciences, psychology, earth sciences, engineering, and more.",
17
+ parameters: Type.Object({
18
+ provider: Type.Optional(
19
+ Type.String({
20
+ description:
21
+ "Preprint provider filter: 'osf', 'socarxiv', 'psyarxiv', 'engrxiv', 'eartharxiv', 'nutrixiv'. Omit for all providers.",
22
+ }),
23
+ ),
24
+ size: Type.Optional(
25
+ Type.Number({ description: "Max results (default 10, max 100)" }),
26
+ ),
27
+ page: Type.Optional(
28
+ Type.Number({ description: "Page number (default 1)" }),
29
+ ),
30
+ }),
31
+ execute: async (input: {
32
+ provider?: string;
33
+ size?: number;
34
+ page?: number;
35
+ }) => {
36
+ const params = new URLSearchParams({
37
+ "page[size]": String(Math.min(input.size ?? 10, 100)),
38
+ });
39
+ if (input.provider) params.set("filter[provider]", input.provider);
40
+ if (input.page) params.set("page", String(input.page));
41
+
42
+ const result = await trackedFetch(
43
+ "osf_preprints",
44
+ `${BASE}/preprints/?${params}`,
45
+ {
46
+ headers: {
47
+ Accept: "application/vnd.api+json",
48
+ },
49
+ },
50
+ 20_000,
51
+ );
52
+ if (isTrackedError(result)) return result;
53
+ const data = await result.res.json();
54
+
55
+ const items = data.data as Array<Record<string, unknown>> | undefined;
56
+ const linksMeta = (data.links as Record<string, unknown>)?.meta as Record<string, unknown> | undefined;
57
+
58
+ return toolResult({
59
+ total_results: linksMeta?.total,
60
+ page: input.page ?? 1,
61
+ source: "osf_preprints",
62
+ preprints: (items ?? []).map((item) => {
63
+ const attrs = item.attributes as Record<string, unknown> | undefined;
64
+ const relationships = item.relationships as Record<string, unknown> | undefined;
65
+ const providerData = (
66
+ relationships?.provider as Record<string, unknown> | undefined
67
+ )?.data as Record<string, string> | undefined;
68
+ const subjects = attrs?.subjects as Array<Array<{ id: string; text: string }>> | undefined;
69
+
70
+ // Flatten nested subject arrays
71
+ const subjectNames = subjects
72
+ ?.flat()
73
+ .map((s) => s.text)
74
+ .filter(Boolean);
75
+
76
+ return {
77
+ id: item.id,
78
+ title: attrs?.title,
79
+ description: attrs?.description
80
+ ? String(attrs.description).slice(0, 500)
81
+ : undefined,
82
+ doi: attrs?.doi ?? undefined,
83
+ date_created: attrs?.date_created,
84
+ date_published: attrs?.date_published,
85
+ year: attrs?.date_published
86
+ ? String(attrs.date_published).slice(0, 4)
87
+ : attrs?.date_created
88
+ ? String(attrs.date_created).slice(0, 4)
89
+ : undefined,
90
+ provider: providerData?.id,
91
+ tags: attrs?.tags,
92
+ subjects: subjectNames,
93
+ is_published: attrs?.is_published,
94
+ authors: undefined as string[] | undefined, // OSF preprint list does not embed contributor names
95
+ url: `https://osf.io/preprints/${providerData?.id ?? "osf"}/${String(item.id).replace(/_v\d+$/, "")}`,
96
+ source: "osf_preprints",
97
+ };
98
+ }),
99
+ _latency_ms: result.latency_ms,
100
+ });
101
+ },
102
+ },
103
+ ];
104
+ }
@@ -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 EUTILS = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
6
6
 
@@ -53,23 +53,27 @@ export function createPubMedTools(
53
53
  if (input.max_date) searchParams.set("maxdate", input.max_date);
54
54
  if (input.min_date || input.max_date) searchParams.set("datetype", "pdat");
55
55
 
56
- const searchRes = await fetch(`${EUTILS}/esearch.fcgi?${searchParams}`);
57
- if (!searchRes.ok)
58
- return toolResult({ error: `Search error: ${searchRes.status} ${searchRes.statusText}` });
59
- const searchData = await searchRes.json();
56
+ const searchTracked = await trackedFetch("pubmed", `${EUTILS}/esearch.fcgi?${searchParams}`);
57
+ if (isTrackedError(searchTracked)) return searchTracked;
58
+ const searchData = await searchTracked.res.json();
60
59
  const ids: string[] = searchData.esearchresult?.idlist ?? [];
61
60
 
62
- if (ids.length === 0) return toolResult({ total_count: searchData.esearchresult?.count ?? 0, articles: [] });
61
+ if (ids.length === 0) {
62
+ return toolResult({
63
+ total_count: searchData.esearchresult?.count ?? 0,
64
+ articles: [],
65
+ _source_health: { source: "pubmed", latency_ms: searchTracked.latency_ms },
66
+ });
67
+ }
63
68
 
64
69
  const summaryParams = new URLSearchParams({
65
70
  db: "pubmed",
66
71
  id: ids.join(","),
67
72
  retmode: "json",
68
73
  });
69
- const summaryRes = await fetch(`${EUTILS}/esummary.fcgi?${summaryParams}`);
70
- if (!summaryRes.ok)
71
- return toolResult({ error: `Summary error: ${summaryRes.status} ${summaryRes.statusText}` });
72
- const summaryData = await summaryRes.json();
74
+ const summaryTracked = await trackedFetch("pubmed", `${EUTILS}/esummary.fcgi?${summaryParams}`);
75
+ if (isTrackedError(summaryTracked)) return summaryTracked;
76
+ const summaryData = await summaryTracked.res.json();
73
77
 
74
78
  const articles = ids.map((id) => {
75
79
  const doc = summaryData.result?.[id];
@@ -92,6 +96,7 @@ export function createPubMedTools(
92
96
  return toolResult({
93
97
  total_count: parseInt(searchData.esearchresult?.count ?? "0", 10),
94
98
  articles,
99
+ _source_health: { source: "pubmed", latency_ms: summaryTracked.latency_ms },
95
100
  });
96
101
  },
97
102
  },
@@ -109,9 +114,9 @@ export function createPubMedTools(
109
114
  id: input.pmid,
110
115
  retmode: "xml",
111
116
  });
112
- const res = await fetch(`${EUTILS}/efetch.fcgi?${params}`);
113
- if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
114
- const xml = await res.text();
117
+ const tracked = await trackedFetch("pubmed", `${EUTILS}/efetch.fcgi?${params}`);
118
+ if (isTrackedError(tracked)) return tracked;
119
+ const xml = await tracked.res.text();
115
120
 
116
121
  const getText = (tag: string) => {
117
122
  const m = xml.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`));
@@ -162,6 +167,7 @@ export function createPubMedTools(
162
167
  pmc,
163
168
  mesh_terms: getMesh(),
164
169
  url: `https://pubmed.ncbi.nlm.nih.gov/${input.pmid}/`,
170
+ _source_health: { source: "pubmed", latency_ms: tracked.latency_ms },
165
171
  });
166
172
  },
167
173
  },