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.
@@ -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
- });