@wentorai/research-plugins 1.2.0 → 1.2.2

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "research-plugins",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "name": "Research Plugins",
5
5
  "description": "487 academic research skills, 13 agent tools, 150 MCP configs & 6 curated lists for Research-Claw",
6
6
  "skills": ["./skills"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wentorai/research-plugins",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "type": "module",
5
5
  "description": "487 academic research skills, 150 MCP configs, 13 agent tools, and 6 curated lists for Research-Claw and AI agents",
6
6
  "keywords": [
@@ -1,5 +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
4
 
4
5
  const BASE = "https://export.arxiv.org/api/query";
5
6
 
@@ -59,9 +60,10 @@ export function createArxivTools(
59
60
  return [
60
61
  {
61
62
  name: "search_arxiv",
63
+ label: "Search Papers (arXiv)",
62
64
  description:
63
65
  "Search arXiv preprint repository. Covers physics, math, CS, biology, quantitative finance, statistics, and more.",
64
- inputSchema: Type.Object({
66
+ parameters: Type.Object({
65
67
  query: Type.String({
66
68
  description:
67
69
  "Search query. Supports field prefixes: ti: (title), au: (author), abs: (abstract), cat: (category). E.g. 'ti:transformer AND cat:cs.CL'",
@@ -79,7 +81,7 @@ export function createArxivTools(
79
81
  Type.String({ description: "Sort order: 'ascending' or 'descending'" }),
80
82
  ),
81
83
  }),
82
- handler: async (input: {
84
+ execute: async (input: {
83
85
  query: string;
84
86
  max_results?: number;
85
87
  sort_by?: string;
@@ -93,7 +95,7 @@ export function createArxivTools(
93
95
  });
94
96
 
95
97
  const res = await fetch(`${BASE}?${params}`);
96
- if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
98
+ if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
97
99
  const xml = await res.text();
98
100
 
99
101
  const totalMatch = xml.match(
@@ -101,30 +103,31 @@ export function createArxivTools(
101
103
  );
102
104
  const total = totalMatch ? parseInt(totalMatch[1], 10) : 0;
103
105
 
104
- return {
106
+ return toolResult({
105
107
  total_results: total,
106
108
  papers: parseArxivXml(xml),
107
- };
109
+ });
108
110
  },
109
111
  },
110
112
  {
111
113
  name: "get_arxiv_paper",
114
+ label: "Get Paper Details (arXiv)",
112
115
  description:
113
116
  "Get detailed information about a specific arXiv paper by its ID.",
114
- inputSchema: Type.Object({
117
+ parameters: Type.Object({
115
118
  arxiv_id: Type.String({
116
119
  description: "arXiv paper ID, e.g. '2301.00001' or '2301.00001v2'",
117
120
  }),
118
121
  }),
119
- handler: async (input: { arxiv_id: string }) => {
122
+ execute: async (input: { arxiv_id: string }) => {
120
123
  const id = input.arxiv_id.replace("arXiv:", "");
121
124
  const params = new URLSearchParams({ id_list: id });
122
125
  const res = await fetch(`${BASE}?${params}`);
123
- if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
126
+ if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
124
127
  const xml = await res.text();
125
128
  const papers = parseArxivXml(xml);
126
- if (papers.length === 0) return { error: "Paper not found" };
127
- return papers[0];
129
+ if (papers.length === 0) return toolResult({ error: "Paper not found" });
130
+ return toolResult(papers[0]);
128
131
  },
129
132
  },
130
133
  ];
@@ -1,5 +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
4
 
4
5
  const BASE = "https://api.crossref.org";
5
6
 
@@ -14,22 +15,23 @@ export function createCrossRefTools(
14
15
  return [
15
16
  {
16
17
  name: "resolve_doi",
18
+ label: "Resolve DOI (CrossRef)",
17
19
  description:
18
20
  "Resolve a DOI to get full bibliographic metadata from CrossRef (title, authors, journal, dates, references).",
19
- inputSchema: Type.Object({
21
+ parameters: Type.Object({
20
22
  doi: Type.String({
21
23
  description: "DOI to resolve, e.g. '10.1038/nature12373'",
22
24
  }),
23
25
  }),
24
- handler: async (input: { doi: string }) => {
26
+ execute: async (input: { doi: string }) => {
25
27
  const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
26
28
  const res = await fetch(`${BASE}/works/${encodeURIComponent(doi)}`, {
27
29
  headers,
28
30
  });
29
- if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
31
+ if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
30
32
  const data = await res.json();
31
33
  const w = data.message;
32
- return {
34
+ return toolResult({
33
35
  doi: w.DOI,
34
36
  title: w.title?.[0],
35
37
  authors: w.author?.map(
@@ -44,14 +46,15 @@ export function createCrossRefTools(
44
46
  url: w.URL,
45
47
  abstract: w.abstract,
46
48
  license: w.license?.[0]?.URL,
47
- };
49
+ });
48
50
  },
49
51
  },
50
52
  {
51
53
  name: "search_crossref",
54
+ label: "Search Works (CrossRef)",
52
55
  description:
53
56
  "Search CrossRef for scholarly works by query. Covers 150M+ DOIs across all publishers.",
54
- inputSchema: Type.Object({
57
+ parameters: Type.Object({
55
58
  query: Type.String({ description: "Search query" }),
56
59
  limit: Type.Optional(
57
60
  Type.Number({ description: "Max results (default 10, max 100)" }),
@@ -71,7 +74,7 @@ export function createCrossRefTools(
71
74
  }),
72
75
  ),
73
76
  }),
74
- handler: async (input: {
77
+ execute: async (input: {
75
78
  query: string;
76
79
  limit?: number;
77
80
  from_year?: number;
@@ -89,9 +92,9 @@ export function createCrossRefTools(
89
92
  if (input.sort) params.set("sort", input.sort);
90
93
 
91
94
  const res = await fetch(`${BASE}/works?${params}`, { headers });
92
- if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
95
+ if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
93
96
  const data = await res.json();
94
- return {
97
+ return toolResult({
95
98
  total_results: data.message?.["total-results"],
96
99
  items: data.message?.items?.map((w: Record<string, unknown>) => ({
97
100
  doi: w.DOI,
@@ -105,7 +108,7 @@ export function createCrossRefTools(
105
108
  type: w.type,
106
109
  cited_by: w["is-referenced-by-count"],
107
110
  })),
108
- };
111
+ });
109
112
  },
110
113
  },
111
114
  ];
@@ -1,5 +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
4
 
4
5
  const BASE = "https://api.openalex.org";
5
6
 
@@ -14,9 +15,10 @@ export function createOpenAlexTools(
14
15
  return [
15
16
  {
16
17
  name: "search_openalex",
18
+ label: "Search Works (OpenAlex)",
17
19
  description:
18
20
  "Search academic works via OpenAlex (free, no key required). Covers 250M+ works across all disciplines.",
19
- inputSchema: Type.Object({
21
+ parameters: Type.Object({
20
22
  query: Type.String({ description: "Search query for works" }),
21
23
  limit: Type.Optional(
22
24
  Type.Number({ description: "Max results (default 10, max 200)" }),
@@ -36,7 +38,7 @@ export function createOpenAlexTools(
36
38
  }),
37
39
  ),
38
40
  }),
39
- handler: async (input: {
41
+ execute: async (input: {
40
42
  query: string;
41
43
  limit?: number;
42
44
  from_year?: number;
@@ -57,9 +59,9 @@ export function createOpenAlexTools(
57
59
  if (input.sort_by) params.set("sort", input.sort_by);
58
60
 
59
61
  const res = await fetch(`${BASE}/works?${params}`, { headers });
60
- if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
62
+ if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
61
63
  const data = await res.json();
62
- return {
64
+ return toolResult({
63
65
  total_count: data.meta?.count,
64
66
  results: data.results?.map((w: Record<string, unknown>) => ({
65
67
  id: w.id,
@@ -76,29 +78,30 @@ export function createOpenAlexTools(
76
78
  )
77
79
  : [],
78
80
  })),
79
- };
81
+ });
80
82
  },
81
83
  },
82
84
  {
83
85
  name: "get_work",
86
+ label: "Get Work Details (OpenAlex)",
84
87
  description:
85
88
  "Get full details of a work by its OpenAlex ID, DOI, or other identifier.",
86
- inputSchema: Type.Object({
89
+ parameters: Type.Object({
87
90
  work_id: Type.String({
88
91
  description:
89
92
  "Work identifier: OpenAlex ID (e.g. 'W2741809807'), DOI URL, or PMID",
90
93
  }),
91
94
  }),
92
- handler: async (input: { work_id: string }) => {
95
+ execute: async (input: { work_id: string }) => {
93
96
  const id = input.work_id.startsWith("10.")
94
97
  ? `https://doi.org/${input.work_id}`
95
98
  : input.work_id;
96
99
  const res = await fetch(`${BASE}/works/${encodeURIComponent(id)}`, {
97
100
  headers,
98
101
  });
99
- if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
102
+ if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
100
103
  const w = await res.json();
101
- return {
104
+ return toolResult({
102
105
  id: w.id,
103
106
  doi: w.doi,
104
107
  title: w.title,
@@ -117,20 +120,21 @@ export function createOpenAlexTools(
117
120
  ?.slice(0, 10)
118
121
  .map((c: Record<string, unknown>) => c.display_name),
119
122
  referenced_works_count: w.referenced_works?.length,
120
- };
123
+ });
121
124
  },
122
125
  },
123
126
  {
124
127
  name: "get_author_openalex",
128
+ label: "Get Author (OpenAlex)",
125
129
  description:
126
130
  "Get author information from OpenAlex including publications, h-index, and affiliations.",
127
- inputSchema: Type.Object({
131
+ parameters: Type.Object({
128
132
  author_id: Type.String({
129
133
  description:
130
134
  "Author identifier: OpenAlex ID (e.g. 'A5023888391'), ORCID, or name search",
131
135
  }),
132
136
  }),
133
- handler: async (input: { author_id: string }) => {
137
+ execute: async (input: { author_id: string }) => {
134
138
  let url: string;
135
139
  if (
136
140
  input.author_id.startsWith("A") ||
@@ -145,14 +149,14 @@ export function createOpenAlexTools(
145
149
  per_page: "5",
146
150
  });
147
151
  const res = await fetch(`${BASE}/authors?${params}`, { headers });
148
- if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
149
- return res.json();
152
+ if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
153
+ return toolResult(await res.json());
150
154
  }
151
155
 
152
156
  const res = await fetch(url, { headers });
153
- if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
157
+ if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
154
158
  const a = await res.json();
155
- return {
159
+ return toolResult({
156
160
  id: a.id,
157
161
  display_name: a.display_name,
158
162
  orcid: a.orcid,
@@ -167,7 +171,7 @@ export function createOpenAlexTools(
167
171
  top_concepts: a.x_concepts
168
172
  ?.slice(0, 5)
169
173
  .map((c: Record<string, unknown>) => c.display_name),
170
- };
174
+ });
171
175
  },
172
176
  },
173
177
  ];
@@ -1,5 +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
4
 
4
5
  const EUTILS = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
5
6
 
@@ -10,9 +11,10 @@ export function createPubMedTools(
10
11
  return [
11
12
  {
12
13
  name: "search_pubmed",
14
+ label: "Search Articles (PubMed)",
13
15
  description:
14
16
  "Search PubMed biomedical literature database. Covers 36M+ citations from MEDLINE, life science journals, and online books.",
15
- inputSchema: Type.Object({
17
+ parameters: Type.Object({
16
18
  query: Type.String({
17
19
  description:
18
20
  "PubMed search query. Supports MeSH terms and field tags, e.g. 'CRISPR[Title] AND 2024[PDAT]'",
@@ -32,7 +34,7 @@ export function createPubMedTools(
32
34
  Type.String({ description: "Max publication date (YYYY/MM/DD)" }),
33
35
  ),
34
36
  }),
35
- handler: async (input: {
37
+ execute: async (input: {
36
38
  query: string;
37
39
  max_results?: number;
38
40
  sort?: string;
@@ -53,11 +55,11 @@ export function createPubMedTools(
53
55
 
54
56
  const searchRes = await fetch(`${EUTILS}/esearch.fcgi?${searchParams}`);
55
57
  if (!searchRes.ok)
56
- return { error: `Search error: ${searchRes.status} ${searchRes.statusText}` };
58
+ return toolResult({ error: `Search error: ${searchRes.status} ${searchRes.statusText}` });
57
59
  const searchData = await searchRes.json();
58
60
  const ids: string[] = searchData.esearchresult?.idlist ?? [];
59
61
 
60
- if (ids.length === 0) return { total_count: searchData.esearchresult?.count ?? 0, articles: [] };
62
+ if (ids.length === 0) return toolResult({ total_count: searchData.esearchresult?.count ?? 0, articles: [] });
61
63
 
62
64
  const summaryParams = new URLSearchParams({
63
65
  db: "pubmed",
@@ -66,7 +68,7 @@ export function createPubMedTools(
66
68
  });
67
69
  const summaryRes = await fetch(`${EUTILS}/esummary.fcgi?${summaryParams}`);
68
70
  if (!summaryRes.ok)
69
- return { error: `Summary error: ${summaryRes.status} ${summaryRes.statusText}` };
71
+ return toolResult({ error: `Summary error: ${summaryRes.status} ${summaryRes.statusText}` });
70
72
  const summaryData = await summaryRes.json();
71
73
 
72
74
  const articles = ids.map((id) => {
@@ -87,27 +89,28 @@ export function createPubMedTools(
87
89
  };
88
90
  });
89
91
 
90
- return {
92
+ return toolResult({
91
93
  total_count: parseInt(searchData.esearchresult?.count ?? "0", 10),
92
94
  articles,
93
- };
95
+ });
94
96
  },
95
97
  },
96
98
  {
97
99
  name: "get_article",
100
+ label: "Get Article Details (PubMed)",
98
101
  description:
99
102
  "Get detailed article metadata from PubMed by PMID, including abstract.",
100
- inputSchema: Type.Object({
103
+ parameters: Type.Object({
101
104
  pmid: Type.String({ description: "PubMed ID (numeric string)" }),
102
105
  }),
103
- handler: async (input: { pmid: string }) => {
106
+ execute: async (input: { pmid: string }) => {
104
107
  const params = new URLSearchParams({
105
108
  db: "pubmed",
106
109
  id: input.pmid,
107
110
  retmode: "xml",
108
111
  });
109
112
  const res = await fetch(`${EUTILS}/efetch.fcgi?${params}`);
110
- if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
113
+ if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
111
114
  const xml = await res.text();
112
115
 
113
116
  const getText = (tag: string) => {
@@ -148,7 +151,7 @@ export function createPubMedTools(
148
151
  xml.match(/<ArticleId IdType="pmc">([\s\S]*?)<\/ArticleId>/)?.[1]?.trim() ??
149
152
  "";
150
153
 
151
- return {
154
+ return toolResult({
152
155
  pmid: input.pmid,
153
156
  title: getText("ArticleTitle"),
154
157
  abstract: getAbstract(),
@@ -159,7 +162,7 @@ export function createPubMedTools(
159
162
  pmc,
160
163
  mesh_terms: getMesh(),
161
164
  url: `https://pubmed.ncbi.nlm.nih.gov/${input.pmid}/`,
162
- };
165
+ });
163
166
  },
164
167
  },
165
168
  ];
@@ -1,5 +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
4
 
4
5
  const BASE = "https://api.semanticscholar.org/graph/v1";
5
6
 
@@ -15,9 +16,10 @@ export function createSemanticScholarTools(
15
16
  return [
16
17
  {
17
18
  name: "search_papers",
19
+ label: "Search Papers (Semantic Scholar)",
18
20
  description:
19
21
  "Search academic papers on Semantic Scholar. Returns titles, abstracts, authors, citation counts.",
20
- inputSchema: Type.Object({
22
+ parameters: Type.Object({
21
23
  query: Type.String({ description: "Search query keywords" }),
22
24
  limit: Type.Optional(
23
25
  Type.Number({ description: "Max results to return (default 10, max 100)" }),
@@ -35,7 +37,7 @@ export function createSemanticScholarTools(
35
37
  Type.Boolean({ description: "Only return open access papers" }),
36
38
  ),
37
39
  }),
38
- handler: async (input: {
40
+ execute: async (input: {
39
41
  query: string;
40
42
  limit?: number;
41
43
  year?: string;
@@ -54,36 +56,38 @@ export function createSemanticScholarTools(
54
56
  if (input.open_access_only) params.set("openAccessPdf", "");
55
57
 
56
58
  const res = await fetch(`${BASE}/paper/search?${params}`, { headers });
57
- if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
58
- return res.json();
59
+ if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
60
+ return toolResult(await res.json());
59
61
  },
60
62
  },
61
63
  {
62
64
  name: "get_paper",
65
+ label: "Get Paper Details (Semantic Scholar)",
63
66
  description:
64
67
  "Get detailed information about a specific paper by its Semantic Scholar ID, DOI, or ArXiv ID.",
65
- inputSchema: Type.Object({
68
+ parameters: Type.Object({
66
69
  paper_id: Type.String({
67
70
  description:
68
71
  "Paper identifier: Semantic Scholar ID, DOI (e.g. '10.1234/...'), or ArXiv ID (e.g. 'arXiv:2301.00001')",
69
72
  }),
70
73
  }),
71
- handler: async (input: { paper_id: string }) => {
74
+ execute: async (input: { paper_id: string }) => {
72
75
  const fields =
73
76
  "title,abstract,authors,year,citationCount,referenceCount,tldr,url,venue,isOpenAccess,openAccessPdf,fieldsOfStudy,publicationDate";
74
77
  const res = await fetch(
75
78
  `${BASE}/paper/${encodeURIComponent(input.paper_id)}?fields=${fields}`,
76
79
  { headers },
77
80
  );
78
- if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
79
- return res.json();
81
+ if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
82
+ return toolResult(await res.json());
80
83
  },
81
84
  },
82
85
  {
83
86
  name: "get_citations",
87
+ label: "Get Citations (Semantic Scholar)",
84
88
  description:
85
89
  "Get papers that cite a given paper. Useful for forward citation tracking.",
86
- inputSchema: Type.Object({
90
+ parameters: Type.Object({
87
91
  paper_id: Type.String({ description: "Paper identifier (S2 ID, DOI, or ArXiv ID)" }),
88
92
  limit: Type.Optional(
89
93
  Type.Number({ description: "Max citations to return (default 20, max 100)" }),
@@ -92,7 +96,7 @@ export function createSemanticScholarTools(
92
96
  Type.Number({ description: "Pagination offset" }),
93
97
  ),
94
98
  }),
95
- handler: async (input: { paper_id: string; limit?: number; offset?: number }) => {
99
+ execute: async (input: { paper_id: string; limit?: number; offset?: number }) => {
96
100
  const fields = "title,authors,year,citationCount,url,abstract";
97
101
  const limit = Math.min(input.limit ?? 20, 100);
98
102
  const offset = input.offset ?? 0;
@@ -100,8 +104,8 @@ export function createSemanticScholarTools(
100
104
  `${BASE}/paper/${encodeURIComponent(input.paper_id)}/citations?fields=${fields}&limit=${limit}&offset=${offset}`,
101
105
  { headers },
102
106
  );
103
- if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
104
- return res.json();
107
+ if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
108
+ return toolResult(await res.json());
105
109
  },
106
110
  },
107
111
  ];
@@ -1,5 +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
4
 
4
5
  const BASE = "https://api.unpaywall.org/v2";
5
6
 
@@ -12,21 +13,22 @@ export function createUnpaywallTools(
12
13
  return [
13
14
  {
14
15
  name: "find_oa_version",
16
+ label: "Find Open Access Version (Unpaywall)",
15
17
  description:
16
18
  "Find open access versions of a paper by DOI using Unpaywall. Returns free PDF links from repositories, preprint servers, and publisher OA pages.",
17
- inputSchema: Type.Object({
19
+ parameters: Type.Object({
18
20
  doi: Type.String({
19
21
  description: "DOI of the paper, e.g. '10.1038/nature12373'",
20
22
  }),
21
23
  }),
22
- handler: async (input: { doi: string }) => {
24
+ execute: async (input: { doi: string }) => {
23
25
  const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
24
26
  const res = await fetch(
25
27
  `${BASE}/${encodeURIComponent(doi)}?email=${email}`,
26
28
  );
27
29
  if (!res.ok) {
28
- if (res.status === 404) return { error: "DOI not found in Unpaywall" };
29
- return { error: `API error: ${res.status} ${res.statusText}` };
30
+ if (res.status === 404) return toolResult({ error: "DOI not found in Unpaywall" });
31
+ return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
30
32
  }
31
33
  const data = await res.json();
32
34
 
@@ -40,7 +42,7 @@ export function createUnpaywallTools(
40
42
  version: loc.version,
41
43
  }));
42
44
 
43
- return {
45
+ return toolResult({
44
46
  doi: data.doi,
45
47
  title: data.title,
46
48
  is_oa: data.is_oa,
@@ -51,7 +53,7 @@ export function createUnpaywallTools(
51
53
  publisher: data.publisher,
52
54
  published_date: data.published_date,
53
55
  oa_locations: oaLocations,
54
- };
56
+ });
55
57
  },
56
58
  },
57
59
  ];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Wrap raw data into the OpenClaw AgentTool return format.
3
+ *
4
+ * Expected shape: { content: [{ type: "text", text: string }], details: unknown }
5
+ * - content → displayed to the user / consumed by the model as text
6
+ * - details → structured data for programmatic access by downstream tools
7
+ */
8
+ export function toolResult(data: unknown) {
9
+ const text = JSON.stringify(data, null, 2);
10
+ return { content: [{ type: "text" as const, text }], details: data };
11
+ }