gyoshu 0.1.0 → 0.2.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.
- package/README.md +11 -9
- package/bin/gyoshu.js +2 -2
- package/install.sh +1 -1
- package/package.json +1 -1
- package/src/lib/report-markdown.ts +21 -63
- package/src/lib/literature-client.ts +0 -1048
- package/src/skill/scientific-method/SKILL.md +0 -331
- package/src/tool/literature-search.ts +0 -389
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Literature Search Tool - OpenCode tool for finding and citing academic papers
|
|
3
|
-
*
|
|
4
|
-
* Provides access to academic literature via Crossref and arXiv APIs with:
|
|
5
|
-
* - Search across multiple sources (Crossref, arXiv)
|
|
6
|
-
* - Citation formatting (APA, BibTeX)
|
|
7
|
-
* - Related paper discovery for research context
|
|
8
|
-
*
|
|
9
|
-
* Usage via tool invocation:
|
|
10
|
-
* - search: Find papers matching a query
|
|
11
|
-
* - cite: Get formatted citation for a DOI or arXiv ID
|
|
12
|
-
* - related: Find papers related to a research topic
|
|
13
|
-
*
|
|
14
|
-
* @module literature-search
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { tool } from "@opencode-ai/plugin";
|
|
18
|
-
import {
|
|
19
|
-
searchCrossref,
|
|
20
|
-
searchArxiv,
|
|
21
|
-
searchLiterature,
|
|
22
|
-
getCitationByDOI,
|
|
23
|
-
getArxivPaper,
|
|
24
|
-
formatCitationAPA,
|
|
25
|
-
formatCitationBibTeX,
|
|
26
|
-
LiteratureAPIError,
|
|
27
|
-
type Citation,
|
|
28
|
-
type SearchResult,
|
|
29
|
-
} from "../lib/literature-client";
|
|
30
|
-
|
|
31
|
-
// =============================================================================
|
|
32
|
-
// TYPES
|
|
33
|
-
// =============================================================================
|
|
34
|
-
|
|
35
|
-
type LiteratureSource = "crossref" | "arxiv" | "both";
|
|
36
|
-
type CitationFormat = "apa" | "bibtex";
|
|
37
|
-
|
|
38
|
-
// =============================================================================
|
|
39
|
-
// HELPER FUNCTIONS
|
|
40
|
-
// =============================================================================
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Detect if an identifier is a DOI or arXiv ID.
|
|
44
|
-
*
|
|
45
|
-
* @param identifier - The identifier to classify
|
|
46
|
-
* @returns "doi" | "arxiv" | "unknown"
|
|
47
|
-
*/
|
|
48
|
-
function detectIdentifierType(identifier: string): "doi" | "arxiv" | "unknown" {
|
|
49
|
-
const normalized = identifier.trim();
|
|
50
|
-
|
|
51
|
-
// DOI patterns: 10.xxxx/... or https://doi.org/10.xxxx/...
|
|
52
|
-
if (/^(https?:\/\/doi\.org\/)?10\.\d{4,}\/\S+$/i.test(normalized)) {
|
|
53
|
-
return "doi";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// arXiv patterns: 2301.12345, arxiv:2301.12345, or arXiv:2301.12345v2
|
|
57
|
-
if (/^(arxiv:)?(\d{4}\.\d{4,5})(v\d+)?$/i.test(normalized)) {
|
|
58
|
-
return "arxiv";
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Old arXiv format: hep-th/9901001
|
|
62
|
-
if (/^[a-z-]+\/\d{7}$/i.test(normalized)) {
|
|
63
|
-
return "arxiv";
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return "unknown";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Format a citation based on the requested format.
|
|
71
|
-
*
|
|
72
|
-
* @param citation - The citation to format
|
|
73
|
-
* @param format - The output format
|
|
74
|
-
* @returns Formatted citation string
|
|
75
|
-
*/
|
|
76
|
-
function formatCitation(citation: Citation, format: CitationFormat): string {
|
|
77
|
-
switch (format) {
|
|
78
|
-
case "apa":
|
|
79
|
-
return formatCitationAPA(citation);
|
|
80
|
-
case "bibtex":
|
|
81
|
-
return formatCitationBibTeX(citation);
|
|
82
|
-
default:
|
|
83
|
-
return formatCitationAPA(citation);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Format a list of citations as a summary for display.
|
|
89
|
-
*
|
|
90
|
-
* @param citations - Array of citations
|
|
91
|
-
* @param format - Output format for each citation
|
|
92
|
-
* @returns Formatted results
|
|
93
|
-
*/
|
|
94
|
-
function formatCitationList(
|
|
95
|
-
citations: Citation[],
|
|
96
|
-
format: CitationFormat = "apa"
|
|
97
|
-
): string[] {
|
|
98
|
-
return citations.map((c, i) => {
|
|
99
|
-
const formatted = formatCitation(c, format);
|
|
100
|
-
const source = c.source === "arxiv" ? `arXiv:${c.arxivId}` : c.doi ? `DOI:${c.doi}` : "";
|
|
101
|
-
return `[${i + 1}] ${formatted}${source ? `\n ${source}` : ""}`;
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// =============================================================================
|
|
106
|
-
// TOOL EXPORT
|
|
107
|
-
// =============================================================================
|
|
108
|
-
|
|
109
|
-
export default tool({
|
|
110
|
-
name: "literature-search",
|
|
111
|
-
description:
|
|
112
|
-
"Search and cite academic papers from Crossref and arXiv. " +
|
|
113
|
-
"Actions: search (find papers by query), cite (format DOI/arXiv ID as APA/BibTeX), " +
|
|
114
|
-
"related (find papers related to a research topic). Results are cached for 7 days.",
|
|
115
|
-
|
|
116
|
-
args: {
|
|
117
|
-
action: tool.schema
|
|
118
|
-
.enum(["search", "cite", "related"])
|
|
119
|
-
.describe(
|
|
120
|
-
"Operation to perform: " +
|
|
121
|
-
"search (find papers matching query), " +
|
|
122
|
-
"cite (format a DOI or arXiv ID as citation), " +
|
|
123
|
-
"related (find papers related to a topic for research context)"
|
|
124
|
-
),
|
|
125
|
-
query: tool.schema
|
|
126
|
-
.string()
|
|
127
|
-
.optional()
|
|
128
|
-
.describe(
|
|
129
|
-
"Search query for search/related actions. " +
|
|
130
|
-
"For arXiv, supports field prefixes: ti: (title), au: (author), abs: (abstract), all: (all fields)"
|
|
131
|
-
),
|
|
132
|
-
source: tool.schema
|
|
133
|
-
.enum(["crossref", "arxiv", "both"])
|
|
134
|
-
.optional()
|
|
135
|
-
.describe(
|
|
136
|
-
"Which source to search. Defaults to 'both'. " +
|
|
137
|
-
"crossref: Published papers with DOIs. " +
|
|
138
|
-
"arxiv: Preprints in physics, math, CS, etc."
|
|
139
|
-
),
|
|
140
|
-
limit: tool.schema
|
|
141
|
-
.number()
|
|
142
|
-
.optional()
|
|
143
|
-
.describe("Maximum number of results to return (default: 10, max: 100)"),
|
|
144
|
-
identifier: tool.schema
|
|
145
|
-
.string()
|
|
146
|
-
.optional()
|
|
147
|
-
.describe(
|
|
148
|
-
"DOI or arXiv ID for cite action. " +
|
|
149
|
-
"DOI format: '10.1038/nature12373' or 'https://doi.org/10.1038/nature12373'. " +
|
|
150
|
-
"arXiv format: '2301.12345' or 'arxiv:2301.12345v2'"
|
|
151
|
-
),
|
|
152
|
-
format: tool.schema
|
|
153
|
-
.enum(["apa", "bibtex"])
|
|
154
|
-
.optional()
|
|
155
|
-
.describe("Citation format for cite action. Defaults to 'apa'"),
|
|
156
|
-
},
|
|
157
|
-
|
|
158
|
-
async execute(args) {
|
|
159
|
-
const { action, query, source = "both", limit = 10, identifier, format = "apa" } = args;
|
|
160
|
-
|
|
161
|
-
switch (action) {
|
|
162
|
-
// =========================================================================
|
|
163
|
-
// SEARCH ACTION
|
|
164
|
-
// =========================================================================
|
|
165
|
-
case "search": {
|
|
166
|
-
if (!query || query.trim().length === 0) {
|
|
167
|
-
throw new Error("query is required for search action");
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const clampedLimit = Math.min(Math.max(1, limit), 100);
|
|
171
|
-
let result: SearchResult;
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
if (source === "crossref") {
|
|
175
|
-
result = await searchCrossref(query, clampedLimit);
|
|
176
|
-
} else if (source === "arxiv") {
|
|
177
|
-
result = await searchArxiv(query, clampedLimit);
|
|
178
|
-
} else {
|
|
179
|
-
// Search both sources
|
|
180
|
-
result = await searchLiterature(query, {
|
|
181
|
-
limit: clampedLimit,
|
|
182
|
-
sources: ["crossref", "arxiv"],
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
} catch (error) {
|
|
186
|
-
if (error instanceof LiteratureAPIError) {
|
|
187
|
-
throw new Error(`Literature search failed: ${error.message}`);
|
|
188
|
-
}
|
|
189
|
-
throw error;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const formattedCitations = formatCitationList(result.citations, "apa");
|
|
193
|
-
|
|
194
|
-
return JSON.stringify(
|
|
195
|
-
{
|
|
196
|
-
success: true,
|
|
197
|
-
action: "search",
|
|
198
|
-
query,
|
|
199
|
-
source: source === "both" ? "crossref+arxiv" : source,
|
|
200
|
-
totalResults: result.totalResults,
|
|
201
|
-
returnedResults: result.citations.length,
|
|
202
|
-
fromCache: result.fromCache,
|
|
203
|
-
citations: result.citations.map((c) => ({
|
|
204
|
-
title: c.title,
|
|
205
|
-
authors: c.authors,
|
|
206
|
-
year: c.year,
|
|
207
|
-
journal: c.journal,
|
|
208
|
-
doi: c.doi,
|
|
209
|
-
arxivId: c.arxivId,
|
|
210
|
-
url: c.url,
|
|
211
|
-
abstract: c.abstract?.substring(0, 500) + (c.abstract && c.abstract.length > 500 ? "..." : ""),
|
|
212
|
-
source: c.source,
|
|
213
|
-
})),
|
|
214
|
-
formatted: formattedCitations,
|
|
215
|
-
},
|
|
216
|
-
null,
|
|
217
|
-
2
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// =========================================================================
|
|
222
|
-
// CITE ACTION
|
|
223
|
-
// =========================================================================
|
|
224
|
-
case "cite": {
|
|
225
|
-
if (!identifier || identifier.trim().length === 0) {
|
|
226
|
-
throw new Error("identifier (DOI or arXiv ID) is required for cite action");
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const idType = detectIdentifierType(identifier);
|
|
230
|
-
|
|
231
|
-
if (idType === "unknown") {
|
|
232
|
-
throw new Error(
|
|
233
|
-
`Could not detect identifier type. ` +
|
|
234
|
-
`Expected DOI (e.g., '10.1038/nature12373') or arXiv ID (e.g., '2301.12345'). ` +
|
|
235
|
-
`Got: '${identifier}'`
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
let citation: Citation | null;
|
|
240
|
-
|
|
241
|
-
try {
|
|
242
|
-
if (idType === "doi") {
|
|
243
|
-
citation = await getCitationByDOI(identifier);
|
|
244
|
-
} else {
|
|
245
|
-
citation = await getArxivPaper(identifier);
|
|
246
|
-
}
|
|
247
|
-
} catch (error) {
|
|
248
|
-
if (error instanceof LiteratureAPIError) {
|
|
249
|
-
throw new Error(`Failed to fetch citation: ${error.message}`);
|
|
250
|
-
}
|
|
251
|
-
throw error;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (!citation) {
|
|
255
|
-
throw new Error(
|
|
256
|
-
`${idType === "doi" ? "DOI" : "arXiv paper"} not found: ${identifier}`
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const formattedCitation = formatCitation(citation, format);
|
|
261
|
-
|
|
262
|
-
return JSON.stringify(
|
|
263
|
-
{
|
|
264
|
-
success: true,
|
|
265
|
-
action: "cite",
|
|
266
|
-
identifier,
|
|
267
|
-
identifierType: idType,
|
|
268
|
-
format,
|
|
269
|
-
citation: {
|
|
270
|
-
title: citation.title,
|
|
271
|
-
authors: citation.authors,
|
|
272
|
-
year: citation.year,
|
|
273
|
-
journal: citation.journal,
|
|
274
|
-
doi: citation.doi,
|
|
275
|
-
arxivId: citation.arxivId,
|
|
276
|
-
url: citation.url,
|
|
277
|
-
abstract: citation.abstract,
|
|
278
|
-
source: citation.source,
|
|
279
|
-
},
|
|
280
|
-
formatted: formattedCitation,
|
|
281
|
-
},
|
|
282
|
-
null,
|
|
283
|
-
2
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// =========================================================================
|
|
288
|
-
// RELATED ACTION
|
|
289
|
-
// =========================================================================
|
|
290
|
-
case "related": {
|
|
291
|
-
if (!query || query.trim().length === 0) {
|
|
292
|
-
throw new Error("query (research topic) is required for related action");
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// For related papers, we search both sources with a slightly higher limit
|
|
296
|
-
// to provide more context for researchers
|
|
297
|
-
const clampedLimit = Math.min(Math.max(1, limit), 100);
|
|
298
|
-
|
|
299
|
-
let result: SearchResult;
|
|
300
|
-
|
|
301
|
-
try {
|
|
302
|
-
// Determine sources based on user preference
|
|
303
|
-
const sources: Array<"crossref" | "arxiv"> =
|
|
304
|
-
source === "crossref"
|
|
305
|
-
? ["crossref"]
|
|
306
|
-
: source === "arxiv"
|
|
307
|
-
? ["arxiv"]
|
|
308
|
-
: ["crossref", "arxiv"];
|
|
309
|
-
|
|
310
|
-
result = await searchLiterature(query, {
|
|
311
|
-
limit: clampedLimit,
|
|
312
|
-
sources,
|
|
313
|
-
});
|
|
314
|
-
} catch (error) {
|
|
315
|
-
if (error instanceof LiteratureAPIError) {
|
|
316
|
-
throw new Error(`Related paper search failed: ${error.message}`);
|
|
317
|
-
}
|
|
318
|
-
throw error;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Group citations by year for easier review
|
|
322
|
-
const byYear: Record<string, Citation[]> = {};
|
|
323
|
-
for (const c of result.citations) {
|
|
324
|
-
const yearKey = c.year?.toString() ?? "Unknown";
|
|
325
|
-
if (!byYear[yearKey]) {
|
|
326
|
-
byYear[yearKey] = [];
|
|
327
|
-
}
|
|
328
|
-
byYear[yearKey].push(c);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Extract key topics from abstracts for context
|
|
332
|
-
const allAbstracts = result.citations
|
|
333
|
-
.filter((c) => c.abstract)
|
|
334
|
-
.map((c) => c.abstract!)
|
|
335
|
-
.join(" ");
|
|
336
|
-
|
|
337
|
-
// Simple summary: list papers with their key info
|
|
338
|
-
const summaryList = result.citations.slice(0, 5).map((c) => ({
|
|
339
|
-
title: c.title,
|
|
340
|
-
authors: c.authors.split(",")[0] + (c.authors.includes(",") ? " et al." : ""),
|
|
341
|
-
year: c.year,
|
|
342
|
-
source: c.source,
|
|
343
|
-
identifier: c.doi ? `doi:${c.doi}` : c.arxivId ? `arXiv:${c.arxivId}` : null,
|
|
344
|
-
}));
|
|
345
|
-
|
|
346
|
-
return JSON.stringify(
|
|
347
|
-
{
|
|
348
|
-
success: true,
|
|
349
|
-
action: "related",
|
|
350
|
-
topic: query,
|
|
351
|
-
source: source === "both" ? "crossref+arxiv" : source,
|
|
352
|
-
totalResults: result.totalResults,
|
|
353
|
-
returnedResults: result.citations.length,
|
|
354
|
-
fromCache: result.fromCache,
|
|
355
|
-
topPapers: summaryList,
|
|
356
|
-
byYear: Object.keys(byYear)
|
|
357
|
-
.sort((a, b) => (b === "Unknown" ? -1 : a === "Unknown" ? 1 : parseInt(b) - parseInt(a)))
|
|
358
|
-
.slice(0, 5)
|
|
359
|
-
.reduce(
|
|
360
|
-
(acc, year) => ({
|
|
361
|
-
...acc,
|
|
362
|
-
[year]: byYear[year].length,
|
|
363
|
-
}),
|
|
364
|
-
{} as Record<string, number>
|
|
365
|
-
),
|
|
366
|
-
citations: result.citations.map((c) => ({
|
|
367
|
-
title: c.title,
|
|
368
|
-
authors: c.authors,
|
|
369
|
-
year: c.year,
|
|
370
|
-
journal: c.journal,
|
|
371
|
-
doi: c.doi,
|
|
372
|
-
arxivId: c.arxivId,
|
|
373
|
-
url: c.url,
|
|
374
|
-
abstract: c.abstract?.substring(0, 300) + (c.abstract && c.abstract.length > 300 ? "..." : ""),
|
|
375
|
-
source: c.source,
|
|
376
|
-
})),
|
|
377
|
-
hint:
|
|
378
|
-
"Use the 'cite' action with a DOI or arXiv ID to get formatted citations for papers you want to reference.",
|
|
379
|
-
},
|
|
380
|
-
null,
|
|
381
|
-
2
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
default:
|
|
386
|
-
throw new Error(`Unknown action: ${action}`);
|
|
387
|
-
}
|
|
388
|
-
},
|
|
389
|
-
});
|