@wentorai/research-plugins 1.4.0 → 1.4.3
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.
- package/README.en.md +143 -0
- package/README.md +98 -131
- package/curated/literature/README.md +2 -2
- package/curated/writing/README.md +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/literature/discovery/SKILL.md +1 -1
- package/skills/literature/discovery/citation-alert-guide/SKILL.md +2 -2
- package/skills/literature/discovery/conference-proceedings-guide/SKILL.md +2 -2
- package/skills/literature/discovery/literature-mapping-guide/SKILL.md +1 -1
- package/skills/literature/discovery/paper-recommendation-guide/SKILL.md +8 -14
- package/skills/literature/discovery/rss-paper-feeds/SKILL.md +20 -14
- package/skills/literature/discovery/semantic-paper-radar/SKILL.md +8 -8
- package/skills/literature/discovery/semantic-scholar-recs-guide/SKILL.md +103 -86
- package/skills/literature/fulltext/open-access-guide/SKILL.md +1 -1
- package/skills/literature/fulltext/open-access-mining-guide/SKILL.md +5 -5
- package/skills/literature/metadata/citation-network-guide/SKILL.md +3 -3
- package/skills/literature/metadata/h-index-guide/SKILL.md +0 -27
- package/skills/literature/search/SKILL.md +1 -1
- package/skills/literature/search/citation-chaining-guide/SKILL.md +42 -32
- package/skills/literature/search/database-comparison-guide/SKILL.md +1 -1
- package/skills/literature/search/semantic-scholar-api/SKILL.md +56 -53
- package/skills/research/automation/paper-to-agent-guide/SKILL.md +1 -1
- package/skills/research/deep-research/in-depth-research-guide/SKILL.md +1 -1
- package/skills/research/deep-research/kosmos-scientist-guide/SKILL.md +3 -3
- package/skills/research/deep-research/llm-scientific-discovery-guide/SKILL.md +1 -1
- package/skills/research/deep-research/local-deep-research-guide/SKILL.md +6 -6
- package/skills/research/deep-research/open-researcher-guide/SKILL.md +3 -3
- package/skills/research/deep-research/tongyi-deep-research-guide/SKILL.md +4 -4
- package/skills/research/methodology/grad-school-guide/SKILL.md +1 -1
- package/skills/research/paper-review/automated-review-guide/SKILL.md +1 -1
- package/skills/tools/diagram/excalidraw-diagram-guide/SKILL.md +1 -1
- package/skills/tools/diagram/mermaid-architect-guide/SKILL.md +1 -1
- package/skills/tools/diagram/plantuml-guide/SKILL.md +1 -1
- package/skills/tools/document/grobid-pdf-parsing/SKILL.md +1 -1
- package/skills/tools/document/paper-parse-guide/SKILL.md +2 -2
- package/skills/tools/knowledge-graph/citation-network-builder/SKILL.md +5 -5
- package/skills/tools/knowledge-graph/knowledge-graph-construction/SKILL.md +1 -1
- package/skills/tools/scraping/academic-web-scraping/SKILL.md +1 -2
- package/skills/tools/scraping/google-scholar-scraper/SKILL.md +7 -7
- package/skills/writing/citation/SKILL.md +1 -1
- package/skills/writing/citation/academic-citation-manager/SKILL.md +20 -17
- package/skills/writing/citation/citation-assistant-skill/SKILL.md +72 -58
- package/skills/writing/citation/onecite-reference-guide/SKILL.md +1 -1
- package/skills/writing/citation/zotero-reference-guide/SKILL.md +1 -1
- package/skills/writing/citation/zotero-scholar-guide/SKILL.md +1 -1
- package/src/tools/arxiv.ts +13 -3
- package/src/tools/biorxiv.ts +21 -5
- package/src/tools/crossref.ts +13 -6
- package/src/tools/datacite.ts +7 -3
- package/src/tools/doaj.ts +3 -2
- package/src/tools/europe-pmc.ts +4 -3
- package/src/tools/hal.ts +6 -4
- package/src/tools/inspire-hep.ts +3 -2
- package/src/tools/openaire.ts +11 -6
- package/src/tools/openalex.ts +17 -2
- package/src/tools/opencitations.ts +9 -0
- package/src/tools/orcid.ts +3 -0
- package/src/tools/osf-preprints.ts +3 -2
- package/src/tools/pubmed.ts +12 -5
- package/src/tools/unpaywall.ts +3 -0
- package/src/tools/util.ts +33 -0
- package/src/tools/zenodo.ts +10 -4
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: citation-assistant-skill
|
|
3
|
-
description: "Claude Code skill for citation workflow via
|
|
3
|
+
description: "Claude Code skill for citation workflow via OpenAlex and CrossRef"
|
|
4
4
|
metadata:
|
|
5
5
|
openclaw:
|
|
6
6
|
emoji: "📎"
|
|
7
7
|
category: "writing"
|
|
8
8
|
subcategory: "citation"
|
|
9
|
-
keywords: ["citation assistant", "
|
|
9
|
+
keywords: ["citation assistant", "OpenAlex", "Claude Code skill", "reference lookup", "academic citation"]
|
|
10
10
|
source: "https://github.com/ZhangNy301/citation-assistant"
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@ metadata:
|
|
|
14
14
|
|
|
15
15
|
## Overview
|
|
16
16
|
|
|
17
|
-
Citation Assistant is a Claude Code skill that integrates
|
|
17
|
+
Citation Assistant is a Claude Code skill that integrates OpenAlex and CrossRef APIs into the coding workflow for instant paper lookup, citation formatting, and reference management. Search for papers by title or keyword, get formatted BibTeX entries, find related works, and insert citations — all without leaving the terminal. Designed for researchers writing papers in LaTeX or Markdown.
|
|
18
18
|
|
|
19
19
|
## Installation
|
|
20
20
|
|
|
@@ -32,106 +32,118 @@ openclaw skills install citation-assistant
|
|
|
32
32
|
```python
|
|
33
33
|
import requests
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
OA_API = "https://api.openalex.org"
|
|
36
36
|
|
|
37
37
|
def search_papers(query, limit=5):
|
|
38
|
-
"""Search
|
|
38
|
+
"""Search OpenAlex for papers."""
|
|
39
39
|
resp = requests.get(
|
|
40
|
-
f"{
|
|
40
|
+
f"{OA_API}/works",
|
|
41
41
|
params={
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"fields": "title,authors,year,citationCount,"
|
|
45
|
-
"externalIds,abstract,venue",
|
|
42
|
+
"search": query,
|
|
43
|
+
"per_page": limit,
|
|
46
44
|
},
|
|
45
|
+
headers={"User-Agent": "ResearchPlugins/1.0 (https://wentor.ai)"},
|
|
47
46
|
)
|
|
48
|
-
return resp.json().get("
|
|
47
|
+
return resp.json().get("results", [])
|
|
49
48
|
|
|
50
49
|
papers = search_papers("attention mechanism transformer")
|
|
51
50
|
for p in papers:
|
|
52
|
-
authors = "
|
|
53
|
-
print(f"[{p
|
|
54
|
-
print(f" {authors} — Citations: {p
|
|
55
|
-
print(f" DOI: {p.get('
|
|
51
|
+
authors = [a["author"]["display_name"] for a in p.get("authorships", [])[:3]]
|
|
52
|
+
print(f"[{p.get('publication_year')}] {p.get('title')}")
|
|
53
|
+
print(f" {', '.join(authors)} — Citations: {p.get('cited_by_count')}")
|
|
54
|
+
print(f" DOI: {p.get('doi', 'N/A')}")
|
|
56
55
|
```
|
|
57
56
|
|
|
58
57
|
### BibTeX Generation
|
|
59
58
|
|
|
60
59
|
```python
|
|
61
|
-
def get_bibtex(
|
|
62
|
-
"""Get BibTeX for a
|
|
60
|
+
def get_bibtex(doi):
|
|
61
|
+
"""Get BibTeX for a paper via CrossRef DOI resolution."""
|
|
63
62
|
resp = requests.get(
|
|
64
|
-
f"
|
|
65
|
-
|
|
66
|
-
"fields": "title,authors,year,venue,externalIds,"
|
|
67
|
-
"journal,publicationTypes",
|
|
68
|
-
},
|
|
63
|
+
f"https://api.crossref.org/works/{doi}",
|
|
64
|
+
headers={"User-Agent": "ResearchPlugins/1.0 (https://wentor.ai; mailto:dev@wentor.ai)"},
|
|
69
65
|
)
|
|
70
|
-
|
|
66
|
+
msg = resp.json().get("message", {})
|
|
71
67
|
|
|
72
68
|
# Generate citation key
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
authors = msg.get("author", [])
|
|
70
|
+
first_author = authors[0].get("family", "unknown").lower() if authors else "unknown"
|
|
71
|
+
year = str(msg.get("published", {}).get("date-parts", [[""]])[0][0])
|
|
72
|
+
key = f"{first_author}{year}"
|
|
75
73
|
|
|
76
74
|
# Build BibTeX
|
|
77
|
-
authors_str = " and ".join(a
|
|
78
|
-
|
|
75
|
+
authors_str = " and ".join(f"{a.get('given', '')} {a.get('family', '')}".strip() for a in authors)
|
|
76
|
+
doi_str = msg.get("DOI", "")
|
|
77
|
+
|
|
78
|
+
title = msg.get("title", [""])[0] if isinstance(msg.get("title"), list) else msg.get("title", "")
|
|
79
|
+
journal = msg.get("container-title", [""])[0] if msg.get("container-title") else ""
|
|
79
80
|
|
|
80
81
|
bibtex = f"""@article{{{key},
|
|
81
|
-
title = {{{
|
|
82
|
+
title = {{{title}}},
|
|
82
83
|
author = {{{authors_str}}},
|
|
83
|
-
year = {{{
|
|
84
|
-
journal = {{{
|
|
85
|
-
doi = {{{
|
|
84
|
+
year = {{{year}}},
|
|
85
|
+
journal = {{{journal}}},
|
|
86
|
+
doi = {{{doi_str}}},
|
|
86
87
|
}}"""
|
|
87
88
|
return bibtex
|
|
88
89
|
|
|
89
90
|
# Example
|
|
90
|
-
bibtex = get_bibtex("
|
|
91
|
+
bibtex = get_bibtex("10.18653/v1/N19-1423")
|
|
91
92
|
print(bibtex)
|
|
92
93
|
```
|
|
93
94
|
|
|
94
95
|
### Citation Context
|
|
95
96
|
|
|
96
97
|
```python
|
|
97
|
-
def
|
|
98
|
-
"""Get papers that cite this work
|
|
98
|
+
def get_citing_works(openalex_id, limit=10):
|
|
99
|
+
"""Get papers that cite this work via OpenAlex."""
|
|
99
100
|
resp = requests.get(
|
|
100
|
-
f"{
|
|
101
|
+
f"{OA_API}/works",
|
|
101
102
|
params={
|
|
102
|
-
"
|
|
103
|
-
"
|
|
103
|
+
"filter": f"cites:{openalex_id}",
|
|
104
|
+
"per_page": limit,
|
|
105
|
+
"sort": "cited_by_count:desc",
|
|
104
106
|
},
|
|
107
|
+
headers={"User-Agent": "ResearchPlugins/1.0 (https://wentor.ai)"},
|
|
105
108
|
)
|
|
106
|
-
|
|
109
|
+
results = resp.json().get("results", [])
|
|
107
110
|
|
|
108
|
-
for
|
|
109
|
-
|
|
110
|
-
print(f"\n{paper
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
print(f" Intent: {cit.get('intents', ['unknown'])}")
|
|
111
|
+
for paper in results:
|
|
112
|
+
authors = [a["author"]["display_name"] for a in paper.get("authorships", [])[:3]]
|
|
113
|
+
print(f"\n{paper.get('title')} ({paper.get('publication_year', '?')})")
|
|
114
|
+
print(f" Authors: {', '.join(authors)}")
|
|
115
|
+
print(f" Citations: {paper.get('cited_by_count', 0)}")
|
|
114
116
|
|
|
115
|
-
|
|
117
|
+
get_citing_works("W2741809807")
|
|
116
118
|
```
|
|
117
119
|
|
|
118
120
|
### Related Paper Discovery
|
|
119
121
|
|
|
120
122
|
```python
|
|
121
|
-
def find_related(
|
|
122
|
-
"""Find papers related to a given paper."""
|
|
123
|
+
def find_related(openalex_id, limit=10):
|
|
124
|
+
"""Find papers related to a given paper via OpenAlex."""
|
|
125
|
+
# Get the paper's concepts, then search for similar works
|
|
123
126
|
resp = requests.get(
|
|
124
|
-
f"{
|
|
127
|
+
f"{OA_API}/works/{openalex_id}",
|
|
128
|
+
headers={"User-Agent": "ResearchPlugins/1.0 (https://wentor.ai)"},
|
|
129
|
+
)
|
|
130
|
+
paper = resp.json()
|
|
131
|
+
concepts = [c["display_name"] for c in paper.get("concepts", [])[:3]]
|
|
132
|
+
|
|
133
|
+
related_resp = requests.get(
|
|
134
|
+
f"{OA_API}/works",
|
|
125
135
|
params={
|
|
126
|
-
"
|
|
127
|
-
"
|
|
136
|
+
"search": " ".join(concepts),
|
|
137
|
+
"per_page": limit,
|
|
138
|
+
"sort": "cited_by_count:desc",
|
|
128
139
|
},
|
|
140
|
+
headers={"User-Agent": "ResearchPlugins/1.0 (https://wentor.ai)"},
|
|
129
141
|
)
|
|
130
|
-
return
|
|
142
|
+
return related_resp.json().get("results", [])
|
|
131
143
|
|
|
132
|
-
related = find_related("
|
|
144
|
+
related = find_related("W2741809807")
|
|
133
145
|
for p in related:
|
|
134
|
-
print(f"[{p
|
|
146
|
+
print(f"[{p.get('publication_year')}] {p.get('title')} ({p.get('cited_by_count')} cites)")
|
|
135
147
|
```
|
|
136
148
|
|
|
137
149
|
## Workflow Integration
|
|
@@ -161,10 +173,11 @@ def build_bibliography(queries, output_file="refs.bib"):
|
|
|
161
173
|
for query in queries:
|
|
162
174
|
papers = search_papers(query, limit=3)
|
|
163
175
|
for paper in papers:
|
|
164
|
-
|
|
165
|
-
if
|
|
166
|
-
seen_ids.add(
|
|
167
|
-
bibtex = get_bibtex(
|
|
176
|
+
doi = paper.get("doi")
|
|
177
|
+
if doi and doi not in seen_ids:
|
|
178
|
+
seen_ids.add(doi)
|
|
179
|
+
bibtex = get_bibtex(doi.replace("https://doi.org/", ""))
|
|
180
|
+
|
|
168
181
|
all_bibtex.append(bibtex)
|
|
169
182
|
|
|
170
183
|
with open(output_file, "w") as f:
|
|
@@ -189,4 +202,5 @@ build_bibliography([
|
|
|
189
202
|
## References
|
|
190
203
|
|
|
191
204
|
- [Citation Assistant GitHub](https://github.com/ZhangNy301/citation-assistant)
|
|
192
|
-
- [
|
|
205
|
+
- [OpenAlex API](https://api.openalex.org/)
|
|
206
|
+
- [CrossRef API](https://api.crossref.org/)
|
|
@@ -14,7 +14,7 @@ metadata:
|
|
|
14
14
|
|
|
15
15
|
## Overview
|
|
16
16
|
|
|
17
|
-
OneCite is an AI-powered toolkit for parsing, completing, and formatting academic references. Given incomplete or messy citation strings, it extracts structured metadata, fills in missing fields via API lookups (CrossRef,
|
|
17
|
+
OneCite is an AI-powered toolkit for parsing, completing, and formatting academic references. Given incomplete or messy citation strings, it extracts structured metadata, fills in missing fields via API lookups (CrossRef, OpenAlex), and outputs clean formatted references in any style (APA, MLA, BibTeX, Chicago). Available as a Python library and MCP server for agent integration.
|
|
18
18
|
|
|
19
19
|
## Installation
|
|
20
20
|
|
|
@@ -38,7 +38,7 @@ The plugin is particularly powerful for literature reviews, where you need to sy
|
|
|
38
38
|
- Identifies DOIs, arXiv IDs, and other persistent identifiers within references
|
|
39
39
|
|
|
40
40
|
**Database Matching**
|
|
41
|
-
- Matches parsed references against Crossref,
|
|
41
|
+
- Matches parsed references against Crossref, OpenAlex, and other academic databases
|
|
42
42
|
- Retrieves complete metadata for matched references (abstract, keywords, citation count)
|
|
43
43
|
- Displays match confidence to help users verify correct identification
|
|
44
44
|
- Supports manual correction when automatic matching fails
|
|
@@ -16,7 +16,7 @@ metadata:
|
|
|
16
16
|
|
|
17
17
|
Zotero is the most widely used open-source reference manager in academia, offering a powerful combination of local storage, cloud sync, browser integration, and a comprehensive API. While most researchers use Zotero through its desktop application and browser connector, the Zotero API enables programmatic access that is essential for automated research workflows—saving papers from scripts, batch-importing search results, organizing libraries algorithmically, and integrating reference management into custom research pipelines.
|
|
18
18
|
|
|
19
|
-
This skill covers using the Zotero Web API to create, read, update, and organize library items programmatically. It is designed for researchers who want to automate parts of their reference management workflow: importing papers from arXiv or
|
|
19
|
+
This skill covers using the Zotero Web API to create, read, update, and organize library items programmatically. It is designed for researchers who want to automate parts of their reference management workflow: importing papers from arXiv or OpenAlex searches directly into Zotero, auto-tagging papers based on content analysis, organizing collections programmatically, and exporting citations in various formats.
|
|
20
20
|
|
|
21
21
|
The skill complements the existing `zotero-api` skill by focusing specifically on practical scholar workflows—the common patterns a researcher uses when integrating Zotero into their daily literature management.
|
|
22
22
|
|
package/src/tools/arxiv.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
-
import { toolResult, trackedFetch, isTrackedError } from "./util.js";
|
|
3
|
+
import { toolResult, trackedFetch, isTrackedError, validEnum } from "./util.js";
|
|
4
4
|
|
|
5
5
|
const BASE = "https://export.arxiv.org/api/query";
|
|
6
6
|
|
|
@@ -109,11 +109,18 @@ export function createArxivTools(
|
|
|
109
109
|
sort_by?: string;
|
|
110
110
|
sort_order?: string;
|
|
111
111
|
}) => {
|
|
112
|
+
if (!input?.query || input.query.trim() === "" || input.query === "undefined") {
|
|
113
|
+
return toolResult({ error: "query parameter is required and must not be empty" });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const SORT_BY = ["relevance", "lastUpdatedDate", "submittedDate"] as const;
|
|
117
|
+
const SORT_ORDER = ["ascending", "descending"] as const;
|
|
118
|
+
|
|
112
119
|
const params = new URLSearchParams({
|
|
113
120
|
search_query: input.query,
|
|
114
121
|
max_results: String(Math.min(input.max_results ?? 10, 50)),
|
|
115
|
-
sortBy: input.sort_by
|
|
116
|
-
sortOrder: input.sort_order
|
|
122
|
+
sortBy: validEnum(input.sort_by, SORT_BY, "relevance"),
|
|
123
|
+
sortOrder: validEnum(input.sort_order, SORT_ORDER, "descending"),
|
|
117
124
|
});
|
|
118
125
|
|
|
119
126
|
const tracked = await trackedFetch("arxiv", `${BASE}?${params}`, undefined, 15_000);
|
|
@@ -151,6 +158,9 @@ export function createArxivTools(
|
|
|
151
158
|
}),
|
|
152
159
|
}),
|
|
153
160
|
execute: async (input: { arxiv_id: string }) => {
|
|
161
|
+
if (!input?.arxiv_id) {
|
|
162
|
+
return toolResult({ error: 'arxiv_id parameter is required (e.g., "2301.00001" or "2301.00001v2")' });
|
|
163
|
+
}
|
|
154
164
|
const id = input.arxiv_id.replace("arXiv:", "").replace(/https?:\/\/arxiv\.org\/abs\//, "");
|
|
155
165
|
const params = new URLSearchParams({ id_list: id });
|
|
156
166
|
|
package/src/tools/biorxiv.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
-
import { toolResult, trackedFetch, isTrackedError } from "./util.js";
|
|
3
|
+
import { toolResult, trackedFetch, isTrackedError, validEnum } from "./util.js";
|
|
4
4
|
|
|
5
5
|
const BASE = "https://api.biorxiv.org";
|
|
6
6
|
|
|
@@ -13,17 +13,24 @@ export function createBiorxivTools(
|
|
|
13
13
|
name: "search_biorxiv",
|
|
14
14
|
label: "Search Preprints (bioRxiv)",
|
|
15
15
|
description:
|
|
16
|
-
"Get recent bioRxiv preprints by date range. Covers biology preprints (300K+). Use date range
|
|
16
|
+
"Get recent bioRxiv preprints by date range. Covers biology preprints (300K+). Use a date range like '2026-03-01/2026-03-18'. Returns up to 100 preprints per page.",
|
|
17
17
|
parameters: Type.Object({
|
|
18
18
|
interval: Type.String({
|
|
19
19
|
description:
|
|
20
|
-
"Date range
|
|
20
|
+
"Date range in YYYY-MM-DD/YYYY-MM-DD format (e.g. '2026-03-01/2026-03-18'). Use today's date for most recent.",
|
|
21
21
|
}),
|
|
22
22
|
cursor: Type.Optional(
|
|
23
23
|
Type.Number({ description: "Pagination offset (default 0, each page returns up to 100)" }),
|
|
24
24
|
),
|
|
25
25
|
}),
|
|
26
26
|
execute: async (input: { interval: string; cursor?: number }) => {
|
|
27
|
+
if (!input?.interval) {
|
|
28
|
+
return toolResult({ error: 'interval parameter is required (date range in YYYY-MM-DD/YYYY-MM-DD format, e.g. "2026-03-01/2026-03-18")' });
|
|
29
|
+
}
|
|
30
|
+
// Validate date range format — bioRxiv API only accepts YYYY-MM-DD/YYYY-MM-DD
|
|
31
|
+
if (!/^\d{4}-\d{2}-\d{2}\/\d{4}-\d{2}-\d{2}$/.test(input.interval)) {
|
|
32
|
+
return toolResult({ error: `Invalid interval format "${input.interval}". Must be YYYY-MM-DD/YYYY-MM-DD (e.g. "2026-03-01/2026-03-18")` });
|
|
33
|
+
}
|
|
27
34
|
const cursor = input.cursor ?? 0;
|
|
28
35
|
const tracked = await trackedFetch("biorxiv", `${BASE}/details/biorxiv/${input.interval}/${cursor}/json`, undefined, 30_000);
|
|
29
36
|
if (isTrackedError(tracked)) return tracked;
|
|
@@ -63,13 +70,19 @@ export function createBiorxivTools(
|
|
|
63
70
|
parameters: Type.Object({
|
|
64
71
|
interval: Type.String({
|
|
65
72
|
description:
|
|
66
|
-
"Date range
|
|
73
|
+
"Date range in YYYY-MM-DD/YYYY-MM-DD format (e.g. '2026-03-01/2026-03-18'). Use today's date for most recent.",
|
|
67
74
|
}),
|
|
68
75
|
cursor: Type.Optional(
|
|
69
76
|
Type.Number({ description: "Pagination offset (default 0)" }),
|
|
70
77
|
),
|
|
71
78
|
}),
|
|
72
79
|
execute: async (input: { interval: string; cursor?: number }) => {
|
|
80
|
+
if (!input?.interval) {
|
|
81
|
+
return toolResult({ error: 'interval parameter is required (date range in YYYY-MM-DD/YYYY-MM-DD format, e.g. "2026-03-01/2026-03-18")' });
|
|
82
|
+
}
|
|
83
|
+
if (!/^\d{4}-\d{2}-\d{2}\/\d{4}-\d{2}-\d{2}$/.test(input.interval)) {
|
|
84
|
+
return toolResult({ error: `Invalid interval format "${input.interval}". Must be YYYY-MM-DD/YYYY-MM-DD (e.g. "2026-03-01/2026-03-18")` });
|
|
85
|
+
}
|
|
73
86
|
const cursor = input.cursor ?? 0;
|
|
74
87
|
const tracked = await trackedFetch("medrxiv", `${BASE}/details/medrxiv/${input.interval}/${cursor}/json`, undefined, 30_000);
|
|
75
88
|
if (isTrackedError(tracked)) return tracked;
|
|
@@ -112,7 +125,10 @@ export function createBiorxivTools(
|
|
|
112
125
|
),
|
|
113
126
|
}),
|
|
114
127
|
execute: async (input: { doi: string; server?: string }) => {
|
|
115
|
-
|
|
128
|
+
if (!input?.doi) {
|
|
129
|
+
return toolResult({ error: 'doi parameter is required (e.g., "10.1101/2024.01.15.575123")' });
|
|
130
|
+
}
|
|
131
|
+
const server = validEnum(input.server, ["biorxiv", "medrxiv"] as const, "biorxiv");
|
|
116
132
|
const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
|
|
117
133
|
const tracked = await trackedFetch(server, `${BASE}/details/${server}/${doi}/na/json`, undefined, 15_000);
|
|
118
134
|
if (isTrackedError(tracked)) return tracked;
|
package/src/tools/crossref.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
-
import { toolResult, trackedFetch, isTrackedError } from "./util.js";
|
|
3
|
+
import { toolResult, trackedFetch, isTrackedError, validParam } from "./util.js";
|
|
4
4
|
|
|
5
5
|
const BASE = "https://api.crossref.org";
|
|
6
6
|
|
|
@@ -24,6 +24,9 @@ export function createCrossRefTools(
|
|
|
24
24
|
}),
|
|
25
25
|
}),
|
|
26
26
|
execute: async (input: { doi: string }) => {
|
|
27
|
+
if (!input?.doi) {
|
|
28
|
+
return toolResult({ error: 'doi parameter is required (e.g., "10.1038/nature12373")' });
|
|
29
|
+
}
|
|
27
30
|
const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
|
|
28
31
|
const tracked = await trackedFetch("crossref", `${BASE}/works/${encodeURIComponent(doi)}`, { headers });
|
|
29
32
|
if (isTrackedError(tracked)) return tracked;
|
|
@@ -111,16 +114,20 @@ export function createCrossRefTools(
|
|
|
111
114
|
const filters: string[] = [];
|
|
112
115
|
if (input.from_year) filters.push(`from-pub-date:${input.from_year}`);
|
|
113
116
|
if (input.until_year) filters.push(`until-pub-date:${input.until_year}`);
|
|
114
|
-
|
|
117
|
+
const type = validParam(input.type);
|
|
118
|
+
if (type) filters.push(`type:${type}`);
|
|
115
119
|
if (input.has_abstract) filters.push("has-abstract:true");
|
|
116
|
-
|
|
120
|
+
const issn = validParam(input.issn);
|
|
121
|
+
if (issn) filters.push(`issn:${issn}`);
|
|
117
122
|
if (filters.length > 0) params.set("filter", filters.join(","));
|
|
118
123
|
|
|
119
124
|
// Journal name as query.container-title (separate from filter)
|
|
120
|
-
|
|
125
|
+
const journal = validParam(input.journal);
|
|
126
|
+
if (journal) params.set("query.container-title", journal);
|
|
121
127
|
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
const sort = validParam(input.sort);
|
|
129
|
+
if (sort) {
|
|
130
|
+
params.set("sort", sort);
|
|
124
131
|
params.set("order", "desc");
|
|
125
132
|
}
|
|
126
133
|
|
package/src/tools/datacite.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
-
import { toolResult, trackedFetch, isTrackedError } from "./util.js";
|
|
3
|
+
import { toolResult, trackedFetch, isTrackedError, validParam } from "./util.js";
|
|
4
4
|
|
|
5
5
|
const BASE = "https://api.datacite.org";
|
|
6
6
|
|
|
@@ -42,8 +42,9 @@ export function createDataCiteTools(
|
|
|
42
42
|
query: input.query,
|
|
43
43
|
"page[size]": String(pageSize),
|
|
44
44
|
});
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const resourceType = validParam(input.resource_type);
|
|
46
|
+
if (resourceType) {
|
|
47
|
+
params.set("resource-type-id", resourceType.toLowerCase());
|
|
47
48
|
}
|
|
48
49
|
if (input.from_year) {
|
|
49
50
|
params.set("query", `${input.query} AND publicationYear:[${input.from_year} TO *]`);
|
|
@@ -118,6 +119,9 @@ export function createDataCiteTools(
|
|
|
118
119
|
}),
|
|
119
120
|
}),
|
|
120
121
|
execute: async (input: { doi: string }) => {
|
|
122
|
+
if (!input?.doi) {
|
|
123
|
+
return toolResult({ error: 'doi parameter is required (e.g., "10.5281/zenodo.1234567")' });
|
|
124
|
+
}
|
|
121
125
|
const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
|
|
122
126
|
|
|
123
127
|
const tracked = await trackedFetch(
|
package/src/tools/doaj.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
-
import { toolResult, trackedFetch, isTrackedError } from "./util.js";
|
|
3
|
+
import { toolResult, trackedFetch, isTrackedError, validParam } from "./util.js";
|
|
4
4
|
|
|
5
5
|
const BASE = "https://doaj.org/api";
|
|
6
6
|
|
|
@@ -39,7 +39,8 @@ export function createDoajTools(
|
|
|
39
39
|
const page = input.page ?? 1;
|
|
40
40
|
|
|
41
41
|
let url = `${BASE}/search/articles/${encodeURIComponent(input.query)}?page=${page}&pageSize=${pageSize}`;
|
|
42
|
-
|
|
42
|
+
const sort = validParam(input.sort);
|
|
43
|
+
if (sort) url += `&sort=${encodeURIComponent(sort)}`;
|
|
43
44
|
|
|
44
45
|
const tracked = await trackedFetch("doaj", url);
|
|
45
46
|
if (isTrackedError(tracked)) return tracked;
|
package/src/tools/europe-pmc.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
-
import { toolResult, trackedFetch, isTrackedError } from "./util.js";
|
|
3
|
+
import { toolResult, trackedFetch, isTrackedError, validParam, validEnum } from "./util.js";
|
|
4
4
|
|
|
5
5
|
const BASE = "https://www.ebi.ac.uk/europepmc/webservices/rest";
|
|
6
6
|
|
|
@@ -43,8 +43,9 @@ export function createEuropePmcTools(
|
|
|
43
43
|
pageSize: String(Math.min(input.max_results ?? 10, 1000)),
|
|
44
44
|
resultType: "core",
|
|
45
45
|
});
|
|
46
|
-
|
|
47
|
-
params.set("
|
|
46
|
+
const sort = validParam(input.sort);
|
|
47
|
+
if (sort) params.set("sort", sort);
|
|
48
|
+
params.set("cursorMark", validParam(input.cursor) ?? "*");
|
|
48
49
|
|
|
49
50
|
const tracked = await trackedFetch("europe_pmc", `${BASE}/search?${params}`);
|
|
50
51
|
if (isTrackedError(tracked)) return tracked;
|
package/src/tools/hal.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
-
import { toolResult, trackedFetch, isTrackedError } from "./util.js";
|
|
3
|
+
import { toolResult, trackedFetch, isTrackedError, validParam, validEnum } from "./util.js";
|
|
4
4
|
|
|
5
5
|
const BASE = "https://api.archives-ouvertes.fr";
|
|
6
6
|
|
|
@@ -56,8 +56,9 @@ export function createHalTools(
|
|
|
56
56
|
doc_type?: string;
|
|
57
57
|
}) => {
|
|
58
58
|
let q = input.query;
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
const docType = validParam(input.doc_type);
|
|
60
|
+
if (docType) {
|
|
61
|
+
q = `(${q}) AND docType_s:${docType}`;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
const params = new URLSearchParams({
|
|
@@ -66,7 +67,8 @@ export function createHalTools(
|
|
|
66
67
|
rows: String(Math.min(input.rows ?? 10, 100)),
|
|
67
68
|
wt: "json",
|
|
68
69
|
});
|
|
69
|
-
|
|
70
|
+
const sort = validParam(input.sort);
|
|
71
|
+
if (sort) params.set("sort", sort);
|
|
70
72
|
|
|
71
73
|
const result = await trackedFetch(
|
|
72
74
|
"hal",
|
package/src/tools/inspire-hep.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
-
import { toolResult, trackedFetch, isTrackedError } from "./util.js";
|
|
3
|
+
import { toolResult, trackedFetch, isTrackedError, validEnum } from "./util.js";
|
|
4
4
|
|
|
5
5
|
const BASE = "https://inspirehep.net/api";
|
|
6
6
|
|
|
@@ -99,10 +99,11 @@ export function createInspireHepTools(
|
|
|
99
99
|
),
|
|
100
100
|
}),
|
|
101
101
|
execute: async (input: { query: string; size?: number; sort?: string }) => {
|
|
102
|
+
const sort = validEnum(input.sort, ["mostrecent", "mostcited", "bestmatch"] as const, "bestmatch");
|
|
102
103
|
const params = new URLSearchParams({
|
|
103
104
|
q: input.query,
|
|
104
105
|
size: String(Math.min(input.size ?? 10, 100)),
|
|
105
|
-
sort
|
|
106
|
+
sort,
|
|
106
107
|
});
|
|
107
108
|
|
|
108
109
|
const result = await trackedFetch(
|
package/src/tools/openaire.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
-
import { toolResult, trackedFetch, isTrackedError } from "./util.js";
|
|
3
|
+
import { toolResult, trackedFetch, isTrackedError, validParam } from "./util.js";
|
|
4
4
|
|
|
5
5
|
const BASE = "https://api.openaire.eu";
|
|
6
6
|
|
|
@@ -127,12 +127,17 @@ export function createOpenAireTools(
|
|
|
127
127
|
format: "json",
|
|
128
128
|
size: String(Math.min(input.max_results ?? 10, 50)),
|
|
129
129
|
});
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
if (
|
|
130
|
+
const author = validParam(input.author);
|
|
131
|
+
if (author) params.set("author", author);
|
|
132
|
+
const doi = validParam(input.doi);
|
|
133
|
+
if (doi) params.set("doi", doi);
|
|
134
|
+
const fromDate = validParam(input.from_date);
|
|
135
|
+
if (fromDate) params.set("fromDateAccepted", fromDate);
|
|
136
|
+
const toDate = validParam(input.to_date);
|
|
137
|
+
if (toDate) params.set("toDateAccepted", toDate);
|
|
134
138
|
if (input.oa_only) params.set("OA", "true");
|
|
135
|
-
|
|
139
|
+
const funder = validParam(input.funder);
|
|
140
|
+
if (funder) params.set("funder", funder);
|
|
136
141
|
|
|
137
142
|
const tracked = await trackedFetch("openaire", `${BASE}/search/publications?${params}`, undefined, 15_000);
|
|
138
143
|
if (isTrackedError(tracked)) return tracked;
|
package/src/tools/openalex.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
-
import { toolResult, trackedFetch, isTrackedError } from "./util.js";
|
|
3
|
+
import { toolResult, trackedFetch, isTrackedError, validParam, validEnum } from "./util.js";
|
|
4
4
|
|
|
5
5
|
const BASE = "https://api.openalex.org";
|
|
6
6
|
|
|
@@ -46,6 +46,12 @@ export function createOpenAlexTools(
|
|
|
46
46
|
open_access?: boolean;
|
|
47
47
|
sort_by?: string;
|
|
48
48
|
}) => {
|
|
49
|
+
if (!input?.query || input.query.trim() === "" || input.query === "undefined") {
|
|
50
|
+
return toolResult({ error: "query parameter is required and must not be empty" });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const SORT_BY = ["cited_by_count", "publication_date", "relevance_score"] as const;
|
|
54
|
+
|
|
49
55
|
const filters: string[] = [];
|
|
50
56
|
if (input.from_year) filters.push(`from_publication_date:${input.from_year}-01-01`);
|
|
51
57
|
if (input.to_year) filters.push(`to_publication_date:${input.to_year}-12-31`);
|
|
@@ -56,7 +62,10 @@ export function createOpenAlexTools(
|
|
|
56
62
|
per_page: String(Math.min(input.limit ?? 10, 200)),
|
|
57
63
|
});
|
|
58
64
|
if (filters.length > 0) params.set("filter", filters.join(","));
|
|
59
|
-
|
|
65
|
+
const sortBy = validParam(input.sort_by);
|
|
66
|
+
if (sortBy && (SORT_BY as readonly string[]).includes(sortBy)) {
|
|
67
|
+
params.set("sort", sortBy);
|
|
68
|
+
}
|
|
60
69
|
|
|
61
70
|
const tracked = await trackedFetch("openalex", `${BASE}/works?${params}`, { headers });
|
|
62
71
|
if (isTrackedError(tracked)) return tracked;
|
|
@@ -94,6 +103,9 @@ export function createOpenAlexTools(
|
|
|
94
103
|
}),
|
|
95
104
|
}),
|
|
96
105
|
execute: async (input: { work_id: string }) => {
|
|
106
|
+
if (!input?.work_id) {
|
|
107
|
+
return toolResult({ error: 'work_id parameter is required (e.g., "W2741809807" or a DOI like "10.1234/example")' });
|
|
108
|
+
}
|
|
97
109
|
const id = input.work_id.startsWith("10.")
|
|
98
110
|
? `https://doi.org/${input.work_id}`
|
|
99
111
|
: input.work_id;
|
|
@@ -135,6 +147,9 @@ export function createOpenAlexTools(
|
|
|
135
147
|
}),
|
|
136
148
|
}),
|
|
137
149
|
execute: async (input: { author_id: string }) => {
|
|
150
|
+
if (!input?.author_id) {
|
|
151
|
+
return toolResult({ error: 'author_id parameter is required (OpenAlex ID e.g. "A5023888391", ORCID, or author name)' });
|
|
152
|
+
}
|
|
138
153
|
let url: string;
|
|
139
154
|
if (
|
|
140
155
|
input.author_id.startsWith("A") ||
|
|
@@ -29,6 +29,9 @@ export function createOpenCitationsTools(
|
|
|
29
29
|
}),
|
|
30
30
|
}),
|
|
31
31
|
execute: async (input: { doi: string }) => {
|
|
32
|
+
if (!input?.doi) {
|
|
33
|
+
return toolResult({ error: 'doi parameter is required (e.g., "10.1038/nature12373")' });
|
|
34
|
+
}
|
|
32
35
|
const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
|
|
33
36
|
const tracked = await trackedFetch("opencitations", `${BASE}/index/v2/citations/doi:${encodeURIComponent(doi)}`);
|
|
34
37
|
if (isTrackedError(tracked)) return tracked;
|
|
@@ -58,6 +61,9 @@ export function createOpenCitationsTools(
|
|
|
58
61
|
}),
|
|
59
62
|
}),
|
|
60
63
|
execute: async (input: { doi: string }) => {
|
|
64
|
+
if (!input?.doi) {
|
|
65
|
+
return toolResult({ error: 'doi parameter is required (e.g., "10.1038/nature12373")' });
|
|
66
|
+
}
|
|
61
67
|
const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
|
|
62
68
|
const tracked = await trackedFetch("opencitations", `${BASE}/index/v2/references/doi:${encodeURIComponent(doi)}`);
|
|
63
69
|
if (isTrackedError(tracked)) return tracked;
|
|
@@ -87,6 +93,9 @@ export function createOpenCitationsTools(
|
|
|
87
93
|
}),
|
|
88
94
|
}),
|
|
89
95
|
execute: async (input: { doi: string }) => {
|
|
96
|
+
if (!input?.doi) {
|
|
97
|
+
return toolResult({ error: 'doi parameter is required (e.g., "10.1038/nature12373")' });
|
|
98
|
+
}
|
|
90
99
|
const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
|
|
91
100
|
const tracked = await trackedFetch("opencitations", `${BASE}/index/v2/citation-count/doi:${encodeURIComponent(doi)}`);
|
|
92
101
|
if (isTrackedError(tracked)) return tracked;
|