@fbraza/pi-cite 0.2.0 → 0.3.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,199 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { Type, type Static } from "typebox";
3
- import { renderProviderSearchResult } from "./rendering.ts";
4
- import { emitProgress, textResult, type TextToolUpdate } from "./tool-output.ts";
5
- import { fetchJson, formatPaperText, normalizeDoi } from "./shared.ts";
6
- import type { FullTextRouteResult, PaperRecord } from "./types.ts";
7
-
8
- export const SEMANTIC_SCHOLAR_PARAMS = Type.Object({
9
- query: Type.String({ description: "Search query" }),
10
- max_results: Type.Optional(
11
- Type.Number({
12
- description: "Maximum results to return (default 20, max 100)",
13
- }),
14
- ),
15
- year_from: Type.Optional(
16
- Type.Number({ description: "Minimum publication year" }),
17
- ),
18
- year_to: Type.Optional(
19
- Type.Number({ description: "Maximum publication year" }),
20
- ),
21
- fields_of_study: Type.Optional(
22
- Type.Array(Type.String({ description: "Field of study" })),
23
- ),
24
- min_citation_count: Type.Optional(
25
- Type.Number({ description: "Minimum citation count" }),
26
- ),
27
- open_access_only: Type.Optional(
28
- Type.Boolean({ description: "Only keep open access papers" }),
29
- ),
30
- });
31
-
32
- export type SemanticScholarSearchParams = Static<typeof SEMANTIC_SCHOLAR_PARAMS>;
33
-
34
- type SemanticScholarPaperResponse = {
35
- paperId?: string;
36
- title?: string;
37
- abstract?: string | null;
38
- year?: number | null;
39
- citationCount?: number | null;
40
- tldr?: { text?: string | null } | null;
41
- externalIds?: Record<string, string | undefined>;
42
- openAccessPdf?: { url?: string | null } | null;
43
- fieldsOfStudy?: string[] | null;
44
- authors?: Array<{ name?: string | null }> | null;
45
- };
46
-
47
- export async function trySemanticScholarOpenAccess(
48
- doi: string,
49
- signal?: AbortSignal,
50
- ): Promise<FullTextRouteResult> {
51
- const url = new URL("https://api.semanticscholar.org/graph/v1/paper/search");
52
- url.searchParams.set("query", doi);
53
- url.searchParams.set("limit", "5");
54
- url.searchParams.set(
55
- "fields",
56
- "title,openAccessPdf,externalIds,isOpenAccess",
57
- );
58
- const data = await fetchJson<{
59
- data?: Array<{
60
- openAccessPdf?: { url?: string };
61
- externalIds?: Record<string, string>;
62
- }>;
63
- }>(url.toString(), signal);
64
- const match = (data.data ?? []).find(
65
- (item) =>
66
- normalizeDoi(item.externalIds?.DOI) === normalizeDoi(doi) &&
67
- item.openAccessPdf?.url,
68
- );
69
- const pdfUrl = match?.openAccessPdf?.url;
70
- if (!pdfUrl) {
71
- return {
72
- source: "not_found",
73
- access_note: "No open-access PDF found via Semantic Scholar",
74
- };
75
- }
76
- return {
77
- source: "semantic_scholar_oa",
78
- pdf_url: pdfUrl,
79
- access_note: "Open-access PDF found via Semantic Scholar openAccessPdf",
80
- };
81
- }
82
-
83
- export type SemanticScholarSearchResult = {
84
- count: number;
85
- papers: PaperRecord[];
86
- query?: string;
87
- };
88
-
89
- export async function searchSemanticScholar(
90
- params: SemanticScholarSearchParams,
91
- signal?: AbortSignal,
92
- onUpdate?: TextToolUpdate,
93
- ): Promise<SemanticScholarSearchResult> {
94
- const maxResults = Math.min(
95
- 100,
96
- Math.max(1, Math.floor(params.max_results ?? 20)),
97
- );
98
- const url = new URL(
99
- "https://api.semanticscholar.org/graph/v1/paper/search",
100
- );
101
- url.searchParams.set("query", params.query);
102
- url.searchParams.set("limit", String(maxResults));
103
- url.searchParams.set(
104
- "fields",
105
- [
106
- "paperId",
107
- "title",
108
- "abstract",
109
- "year",
110
- "citationCount",
111
- "tldr",
112
- "externalIds",
113
- "openAccessPdf",
114
- "fieldsOfStudy",
115
- "isOpenAccess",
116
- "authors",
117
- ].join(","),
118
- );
119
- if (params.year_from)
120
- url.searchParams.set(
121
- "year",
122
- `${params.year_from}-${params.year_to ?? ""}`,
123
- );
124
- emitProgress(onUpdate, `Searching Semantic Scholar for: ${params.query}`);
125
- const apiKey = process.env.SEMANTIC_SCHOLAR_API_KEY?.trim();
126
- const response = await fetchJson<{ data?: SemanticScholarPaperResponse[] }>(
127
- url.toString(),
128
- signal,
129
- apiKey ? { "x-api-key": apiKey } : undefined,
130
- );
131
- let papers: PaperRecord[] = (response.data ?? []).map((item) => ({
132
- s2_id: item.paperId,
133
- title: item.title ?? "Untitled",
134
- abstract: item.abstract ?? undefined,
135
- year: item.year ?? undefined,
136
- citation_count: item.citationCount ?? undefined,
137
- tldr: item.tldr?.text ?? undefined,
138
- open_access_pdf: item.openAccessPdf?.url ?? undefined,
139
- external_ids: item.externalIds ?? undefined,
140
- doi: normalizeDoi(item.externalIds?.DOI),
141
- pmid: item.externalIds?.PubMed ?? item.externalIds?.PMID,
142
- authors: Array.isArray(item.authors)
143
- ? item.authors.map((author) => author.name).filter(Boolean)
144
- : [],
145
- source: "semantic_scholar",
146
- }));
147
- if (params.fields_of_study?.length) {
148
- const wanted = new Set(
149
- params.fields_of_study.map((item: string) => item.toLowerCase()),
150
- );
151
- papers = papers.filter((paper, index) => {
152
- const fields = (response.data?.[index]?.fieldsOfStudy ?? []).map(
153
- (item: string) => item.toLowerCase(),
154
- );
155
- return fields.some((item: string) => wanted.has(item));
156
- });
157
- }
158
- if (params.min_citation_count !== undefined)
159
- papers = papers.filter(
160
- (paper) => (paper.citation_count ?? 0) >= params.min_citation_count,
161
- );
162
- if (params.open_access_only)
163
- papers = papers.filter((paper) => !!paper.open_access_pdf);
164
- if (params.year_from !== undefined)
165
- papers = papers.filter((paper) => (paper.year ?? 0) >= params.year_from);
166
- if (params.year_to !== undefined)
167
- papers = papers.filter((paper) => (paper.year ?? 9999) <= params.year_to);
168
- return { count: papers.length, papers, query: params.query };
169
- }
170
-
171
- export function createSemanticScholarSearchTool() {
172
- return {
173
- name: "semantic_scholar_search",
174
- label: "Semantic Scholar Search",
175
- description:
176
- "Search Semantic Scholar for relevance-ranked papers, citation counts, and open-access metadata.",
177
- parameters: SEMANTIC_SCHOLAR_PARAMS,
178
- async execute(
179
- _toolCallId: string,
180
- params: SemanticScholarSearchParams,
181
- signal?: AbortSignal,
182
- onUpdate?: TextToolUpdate,
183
- ) {
184
- const result = await searchSemanticScholar(params, signal, onUpdate);
185
- return textResult(formatPaperText(result.papers), result);
186
- },
187
- renderResult(
188
- result: Parameters<typeof renderProviderSearchResult>[1],
189
- options: Parameters<typeof renderProviderSearchResult>[2],
190
- theme: Parameters<typeof renderProviderSearchResult>[3],
191
- ) {
192
- return renderProviderSearchResult("semantic_scholar", result, options, theme);
193
- },
194
- };
195
- }
196
-
197
- export function registerSemanticScholarSearchTool(pi: ExtensionAPI): void {
198
- pi.registerTool(createSemanticScholarSearchTool());
199
- }