agent-sh 0.14.8 → 0.14.10
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/dist/agent/agent-loop.d.ts +0 -4
- package/dist/agent/agent-loop.js +8 -166
- package/dist/agent/entry-format.d.ts +5 -0
- package/dist/agent/entry-format.js +9 -0
- package/dist/agent/extensions/rolling-history/constants.d.ts +1 -0
- package/dist/agent/extensions/rolling-history/constants.js +1 -0
- package/dist/agent/extensions/rolling-history/index.d.ts +4 -0
- package/dist/agent/extensions/rolling-history/index.js +203 -0
- package/dist/agent/extensions/rolling-history/recall.d.ts +4 -0
- package/dist/agent/extensions/rolling-history/recall.js +122 -0
- package/dist/agent/extensions/rolling-history/strategy.d.ts +70 -0
- package/dist/agent/extensions/rolling-history/strategy.js +336 -0
- package/dist/agent/host-types.d.ts +0 -3
- package/dist/agent/index.js +50 -5
- package/dist/agent/live-view.d.ts +57 -0
- package/dist/agent/live-view.js +238 -0
- package/dist/agent/llm-client.d.ts +1 -0
- package/dist/agent/llm-client.js +1 -1
- package/dist/agent/providers/ollama.d.ts +11 -0
- package/dist/agent/providers/ollama.js +72 -0
- package/dist/agent/providers/opencode.d.ts +10 -0
- package/dist/agent/providers/opencode.js +112 -0
- package/dist/agent/providers/zai-coding-plan.d.ts +5 -0
- package/dist/agent/providers/zai-coding-plan.js +26 -0
- package/dist/agent/session-store.d.ts +90 -0
- package/dist/agent/session-store.js +288 -0
- package/dist/agent/store.d.ts +74 -0
- package/dist/agent/store.js +284 -0
- package/dist/agent/subagent.js +2 -2
- package/dist/agent/tool-protocol.d.ts +11 -11
- package/dist/cli/args.js +2 -2
- package/dist/cli/index.js +4 -2
- package/dist/core/index.d.ts +0 -1
- package/dist/core/index.js +0 -1
- package/dist/core/settings.d.ts +5 -1
- package/dist/core/settings.js +62 -1
- package/dist/extensions/index.d.ts +1 -0
- package/dist/shell/events.d.ts +1 -0
- package/dist/shell/input-handler.js +4 -0
- package/dist/shell/tui-renderer.js +5 -2
- package/dist/utils/diff-renderer.js +9 -7
- package/examples/extensions/ads/index.ts +695 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +1 -2
- package/examples/extensions/ash-scheme/index.ts +77 -3
- package/examples/extensions/ashi/package.json +2 -2
- package/examples/extensions/ashi/src/capture.ts +1 -1
- package/examples/extensions/ashi/src/cli.ts +5 -6
- package/examples/extensions/ashi/src/compaction.ts +6 -2
- package/examples/extensions/ashi/src/frontend.ts +13 -13
- package/examples/extensions/ashi/src/multi-session-store.ts +35 -12
- package/examples/extensions/ashi/src/session-commands.ts +1 -1
- package/examples/extensions/ashi/src/user-shell-intents.ts +17 -0
- package/package.json +13 -1
- package/dist/agent/conversation-state.d.ts +0 -142
- package/dist/agent/conversation-state.js +0 -788
- package/dist/agent/history-file.d.ts +0 -81
- package/dist/agent/history-file.js +0 -271
- package/examples/extensions/ashi/src/session-store.ts +0 -363
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADS extension — NASA Astrophysics Data System tools for agent-sh.
|
|
3
|
+
*
|
|
4
|
+
* Provides four tools:
|
|
5
|
+
* - ads_search: Search ADS for papers
|
|
6
|
+
* - ads_paper: Fetch details of a specific paper by bibcode/DOI/arXiv ID
|
|
7
|
+
* - ads_citations: Find citing or referenced papers
|
|
8
|
+
* - ads_download_pdf: Download paper PDFs
|
|
9
|
+
*
|
|
10
|
+
* Requires: ADS_API_TOKEN environment variable
|
|
11
|
+
* Get one at https://ui.adsabs.harvard.edu/#user/settings/token
|
|
12
|
+
*
|
|
13
|
+
* Configuration (~/.agent-sh/settings.json):
|
|
14
|
+
* {
|
|
15
|
+
* "ads": { "pdfDownloadDir": "~/papers" }
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
import type { AgentContext } from "agent-sh/types";
|
|
19
|
+
import * as path from "path";
|
|
20
|
+
import * as fs from "fs/promises";
|
|
21
|
+
import { homedir } from "os";
|
|
22
|
+
import { fileURLToPath } from "url";
|
|
23
|
+
|
|
24
|
+
// ── Constants ────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const ADS_SEARCH_API = "https://api.adsabs.harvard.edu/v1/search/query";
|
|
27
|
+
const ADS_EXPORT_API = "https://api.adsabs.harvard.edu/v1/export";
|
|
28
|
+
const ADS_LINK_GATEWAY = "https://ui.adsabs.harvard.edu/link_gateway";
|
|
29
|
+
|
|
30
|
+
const SEARCH_FIELDS = [
|
|
31
|
+
"bibcode", "title", "author", "abstract", "pubdate", "year",
|
|
32
|
+
"citation_count", "read_count", "doi", "identifier", "pub",
|
|
33
|
+
"arxiv_class", "keyword", "database", "doctype", "aff",
|
|
34
|
+
"volume", "issue", "page", "bibstem",
|
|
35
|
+
].join(",");
|
|
36
|
+
|
|
37
|
+
const PDF_SOURCES = {
|
|
38
|
+
publisher: "PUB_PDF",
|
|
39
|
+
ads: "ADS_PDF",
|
|
40
|
+
arxiv: "EPRINT_PDF",
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
type PDFSource = keyof typeof PDF_SOURCES;
|
|
44
|
+
|
|
45
|
+
const PDF_UA =
|
|
46
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " +
|
|
47
|
+
"(KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36";
|
|
48
|
+
|
|
49
|
+
// ── Types ────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
interface ADSPaper {
|
|
52
|
+
bibcode: string;
|
|
53
|
+
title: string;
|
|
54
|
+
authors: string[];
|
|
55
|
+
affiliations: string[];
|
|
56
|
+
abstract: string;
|
|
57
|
+
pubdate: string;
|
|
58
|
+
year: string;
|
|
59
|
+
citationCount: number;
|
|
60
|
+
readCount: number;
|
|
61
|
+
doi: string[];
|
|
62
|
+
identifier: string[];
|
|
63
|
+
pub: string;
|
|
64
|
+
bibstem: string;
|
|
65
|
+
volume: string;
|
|
66
|
+
issue: string;
|
|
67
|
+
page: string;
|
|
68
|
+
arxivClass: string[];
|
|
69
|
+
keywords: string[];
|
|
70
|
+
database: string[];
|
|
71
|
+
doctype: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function parseDoc(doc: any): ADSPaper {
|
|
77
|
+
return {
|
|
78
|
+
bibcode: doc.bibcode ?? "",
|
|
79
|
+
title: Array.isArray(doc.title) ? doc.title.join(" ") : (doc.title ?? ""),
|
|
80
|
+
authors: Array.isArray(doc.author) ? doc.author : doc.author ? [doc.author] : [],
|
|
81
|
+
affiliations: Array.isArray(doc.aff) ? doc.aff : doc.aff ? [doc.aff] : [],
|
|
82
|
+
abstract: doc.abstract ?? "",
|
|
83
|
+
pubdate: doc.pubdate ?? "",
|
|
84
|
+
year: doc.year ?? "",
|
|
85
|
+
citationCount: doc.citation_count ?? 0,
|
|
86
|
+
readCount: doc.read_count ?? 0,
|
|
87
|
+
doi: Array.isArray(doc.doi) ? doc.doi : doc.doi ? [doc.doi] : [],
|
|
88
|
+
identifier: Array.isArray(doc.identifier) ? doc.identifier : doc.identifier ? [doc.identifier] : [],
|
|
89
|
+
pub: doc.pub ?? "",
|
|
90
|
+
bibstem: Array.isArray(doc.bibstem) ? doc.bibstem[0] ?? "" : (doc.bibstem ?? ""),
|
|
91
|
+
volume: doc.volume ?? "",
|
|
92
|
+
issue: doc.issue ?? "",
|
|
93
|
+
page: doc.page ?? "",
|
|
94
|
+
arxivClass: Array.isArray(doc.arxiv_class) ? doc.arxiv_class : doc.arxiv_class ? [doc.arxiv_class] : [],
|
|
95
|
+
keywords: Array.isArray(doc.keyword) ? doc.keyword : doc.keyword ? [doc.keyword] : [],
|
|
96
|
+
database: Array.isArray(doc.database) ? doc.database : doc.database ? [doc.database] : [],
|
|
97
|
+
doctype: doc.doctype ?? "",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function truncateAuthors(authors: string[], maxAuthors = 10): string {
|
|
102
|
+
if (authors.length <= maxAuthors) return authors.join("; ");
|
|
103
|
+
return `${authors.slice(0, maxAuthors).join("; ")}; ... +${authors.length - maxAuthors} more`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatPaper(p: ADSPaper, index?: number): string {
|
|
107
|
+
const prefix = index !== undefined ? `[${index + 1}] ` : "";
|
|
108
|
+
const lines: string[] = [
|
|
109
|
+
`${prefix}${p.title}`,
|
|
110
|
+
` Bibcode: ${p.bibcode}`,
|
|
111
|
+
` Authors: ${truncateAuthors(p.authors)}`,
|
|
112
|
+
` Published: ${p.pubdate} Year: ${p.year}`,
|
|
113
|
+
` Journal: ${p.pub}`,
|
|
114
|
+
];
|
|
115
|
+
if (p.doi.length > 0) lines.push(` DOI: ${p.doi.join(", ")}`);
|
|
116
|
+
if (p.arxivClass.length > 0) lines.push(` arXiv: ${p.arxivClass.join(", ")}`);
|
|
117
|
+
if (p.keywords.length > 0) lines.push(` Keywords: ${p.keywords.slice(0, 10).join(", ")}`);
|
|
118
|
+
lines.push(` Citations: ${p.citationCount} Reads: ${p.readCount}`);
|
|
119
|
+
lines.push(` ADS URL: ${ADS_LINK_GATEWAY}/${p.bibcode}`);
|
|
120
|
+
if (p.abstract) lines.push(` Abstract: ${p.abstract}`);
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatPaperCompact(p: ADSPaper, index?: number): string {
|
|
125
|
+
const prefix = index !== undefined ? `[${index + 1}] ` : "";
|
|
126
|
+
const authorStr = p.authors.length > 0
|
|
127
|
+
? p.authors.length === 1 ? p.authors[0] : `${p.authors[0]} et al.`
|
|
128
|
+
: "Unknown";
|
|
129
|
+
return `${prefix}${p.title}\n ${p.bibcode} | ${authorStr} | ${p.year} | ${p.bibstem || p.pub} | ${p.citationCount} cit`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getToken(): string {
|
|
133
|
+
const token = process.env.ADS_API_TOKEN ?? "";
|
|
134
|
+
if (!token) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
"ADS_API_TOKEN environment variable not set. " +
|
|
137
|
+
"Get a token from https://ui.adsabs.harvard.edu/#user/settings/token"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return token;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function adsFetch(url: string, options?: RequestInit): Promise<any> {
|
|
144
|
+
const token = getToken();
|
|
145
|
+
const resp = await fetch(url, {
|
|
146
|
+
...options,
|
|
147
|
+
headers: {
|
|
148
|
+
Authorization: `Bearer ${token}`,
|
|
149
|
+
"Content-Type": "application/json",
|
|
150
|
+
...(options?.headers ?? {}),
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
if (!resp.ok) {
|
|
154
|
+
const body = await resp.text().catch(() => "");
|
|
155
|
+
throw new Error(`ADS API error: ${resp.status} ${resp.statusText}${body ? ` - ${body}` : ""}`);
|
|
156
|
+
}
|
|
157
|
+
return resp.json();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildIdQuery(id: string): string {
|
|
161
|
+
const trimmed = id.trim();
|
|
162
|
+
if (trimmed.startsWith("10.") || trimmed.startsWith("doi:")) {
|
|
163
|
+
return `doi:"${trimmed.replace(/^doi:/, "")}"`;
|
|
164
|
+
} else if (trimmed.toLowerCase().startsWith("arxiv:") || /^\d{4}\.\d{4,5}/.test(trimmed)) {
|
|
165
|
+
return `arxiv:"${trimmed.replace(/^arxiv:/i, "")}"`;
|
|
166
|
+
}
|
|
167
|
+
return `bibcode:${trimmed}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function resolveBibcode(id: string): Promise<{ bibcode: string; title: string }> {
|
|
171
|
+
const query = buildIdQuery(id);
|
|
172
|
+
const url = `${ADS_SEARCH_API}?q=${encodeURIComponent(query)}&fl=bibcode,title&rows=1`;
|
|
173
|
+
const data = await adsFetch(url);
|
|
174
|
+
const docs: any[] = (data.response ?? data).docs ?? [];
|
|
175
|
+
if (docs.length === 0) throw new Error(`Paper not found: ${id}`);
|
|
176
|
+
return {
|
|
177
|
+
bibcode: docs[0].bibcode ?? "",
|
|
178
|
+
title: Array.isArray(docs[0].title) ? docs[0].title.join(" ") : (docs[0].title ?? ""),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function buildSortParam(sortBy: string): string {
|
|
183
|
+
const map: Record<string, string> = {
|
|
184
|
+
relevance: "score desc",
|
|
185
|
+
date: "date desc",
|
|
186
|
+
citation_count: "citation_count desc",
|
|
187
|
+
read_count: "read_count desc",
|
|
188
|
+
};
|
|
189
|
+
return map[sortBy] ?? "score desc";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isBibcode(id: string): boolean {
|
|
193
|
+
return /^\d{4}.{15}$/.test(id);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function bibcodeToFilename(bibcode: string): string {
|
|
197
|
+
return bibcode.replace(/\./g, "_");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function formatFileSize(bytes: number): string {
|
|
201
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
202
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
203
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function getPdfDownloadDir(): Promise<string> {
|
|
207
|
+
const settingsPath = path.join(homedir(), ".agent-sh", "settings.json");
|
|
208
|
+
try {
|
|
209
|
+
const raw = await fs.readFile(settingsPath, "utf8");
|
|
210
|
+
const settings = JSON.parse(raw);
|
|
211
|
+
const dir = settings?.ads?.pdfDownloadDir;
|
|
212
|
+
if (typeof dir === "string" && dir.length > 0) {
|
|
213
|
+
return dir.replace(/^~/, homedir());
|
|
214
|
+
}
|
|
215
|
+
} catch { /* settings may not exist */ }
|
|
216
|
+
// Default: ~/.agent-sh/papers
|
|
217
|
+
return path.join(homedir(), ".agent-sh", "papers");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function fetchPDF(bibcode: string, sourceKey: PDFSource): Promise<ArrayBuffer> {
|
|
221
|
+
const url = `${ADS_LINK_GATEWAY}/${encodeURIComponent(bibcode)}/${PDF_SOURCES[sourceKey]}`;
|
|
222
|
+
const resp = await fetch(url, {
|
|
223
|
+
redirect: "follow",
|
|
224
|
+
headers: { "User-Agent": PDF_UA },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (!resp.ok) {
|
|
228
|
+
throw new Error(`HTTP ${resp.status} ${resp.statusText} from ${sourceKey} source`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const contentType = resp.headers.get("content-type") ?? "";
|
|
232
|
+
if (contentType.includes("text/html")) {
|
|
233
|
+
const body = await resp.text();
|
|
234
|
+
if (body.includes("CAPTCHA")) {
|
|
235
|
+
throw new Error(`CAPTCHA detected from ${sourceKey} source. Try a different source.`);
|
|
236
|
+
}
|
|
237
|
+
throw new Error(`Expected PDF but got HTML from ${sourceKey} source. The paper may be behind a paywall. Try source='arxiv'.`);
|
|
238
|
+
}
|
|
239
|
+
if (!contentType.includes("application/pdf")) {
|
|
240
|
+
throw new Error(`Unexpected Content-Type '${contentType}' from ${sourceKey} source.`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return resp.arrayBuffer();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Extension entry point ────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
export default function activate(ctx: AgentContext): void {
|
|
249
|
+
// Register the ADS query syntax skill — grouped with our tools in the system prompt
|
|
250
|
+
const skillPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "SKILL.md");
|
|
251
|
+
ctx.agent.registerSkill(
|
|
252
|
+
"ads",
|
|
253
|
+
"Full ADS query syntax reference and search strategy. Load before composing ADS queries — the tool descriptions only cover basic field prefixes.",
|
|
254
|
+
skillPath,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// ── ads_search ─────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
ctx.agent.registerTool({
|
|
260
|
+
name: "ads_search",
|
|
261
|
+
displayName: "search",
|
|
262
|
+
description:
|
|
263
|
+
"Search astronomy/physics papers (ADS) — preferred over web_search for literature queries.\n" +
|
|
264
|
+
"Supports fielded syntax (title:, author:, year:, bibcode:, doi:), database filters, sort options.\n" +
|
|
265
|
+
"⚠️ Load the ads skill BEFORE using this tool for advanced syntax and search strategies.\n" +
|
|
266
|
+
"Requires ADS_API_TOKEN.",
|
|
267
|
+
input_schema: {
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
query: {
|
|
271
|
+
type: "string",
|
|
272
|
+
description:
|
|
273
|
+
'Search query. Supports fielded searches like "title:exoplanets", ' +
|
|
274
|
+
'"author:\\"Hubble, E\\"", "keyword:dark matter", "year:2023". ' +
|
|
275
|
+
'Unfielded terms search all metadata. Use quotes for phrases, +/- for inclusion/exclusion.',
|
|
276
|
+
},
|
|
277
|
+
database: {
|
|
278
|
+
type: "string",
|
|
279
|
+
enum: ["astronomy", "physics", "general"],
|
|
280
|
+
description: "Filter by ADS database. Default: searches all databases.",
|
|
281
|
+
},
|
|
282
|
+
doctype: {
|
|
283
|
+
type: "string",
|
|
284
|
+
enum: ["article", "eprint", "inproceedings", "proceedings", "inbook", "book", "phdthesis", "mastersthesis", "catalog", "software", "proposal"],
|
|
285
|
+
description: "Filter by document type.",
|
|
286
|
+
},
|
|
287
|
+
refereed: {
|
|
288
|
+
type: "boolean",
|
|
289
|
+
description: "Filter to only refereed (peer-reviewed) papers.",
|
|
290
|
+
},
|
|
291
|
+
year_from: {
|
|
292
|
+
type: "string",
|
|
293
|
+
description: "Start year for date range filter, e.g. '2020'.",
|
|
294
|
+
},
|
|
295
|
+
year_to: {
|
|
296
|
+
type: "string",
|
|
297
|
+
description: "End year for date range filter, e.g. '2024'.",
|
|
298
|
+
},
|
|
299
|
+
max_results: {
|
|
300
|
+
type: "number",
|
|
301
|
+
description: "Max papers to return (default 10, max 50).",
|
|
302
|
+
},
|
|
303
|
+
sort_by: {
|
|
304
|
+
type: "string",
|
|
305
|
+
enum: ["relevance", "date", "citation_count", "read_count"],
|
|
306
|
+
description: "Sort order (default: relevance).",
|
|
307
|
+
},
|
|
308
|
+
start: {
|
|
309
|
+
type: "number",
|
|
310
|
+
description: "Start index for pagination (default 0).",
|
|
311
|
+
},
|
|
312
|
+
detail: {
|
|
313
|
+
type: "string",
|
|
314
|
+
enum: ["compact", "full"],
|
|
315
|
+
description: "Output detail level. 'compact' (default): title, first author, year, bibcode, citations. 'full': includes abstract, all authors, keywords, DOI.",
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
required: ["query"],
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
async execute(args) {
|
|
322
|
+
const query = args.query as string;
|
|
323
|
+
const maxResults = Math.min((args.max_results as number) ?? 10, 50);
|
|
324
|
+
const start = (args.start as number) ?? 0;
|
|
325
|
+
const sort = buildSortParam((args.sort_by as string) ?? "relevance");
|
|
326
|
+
const detail = (args.detail as string) ?? "compact";
|
|
327
|
+
|
|
328
|
+
const fq: string[] = [];
|
|
329
|
+
if (args.database) fq.push(`database:${args.database}`);
|
|
330
|
+
if (args.doctype) fq.push(`doctype:${args.doctype}`);
|
|
331
|
+
if (args.refereed) fq.push("property:refereed");
|
|
332
|
+
if (args.year_from || args.year_to) {
|
|
333
|
+
fq.push(`year:[${args.year_from ?? "*"} TO ${args.year_to ?? "*"}]`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const fqParam = fq.map(f => `&fq=${encodeURIComponent(f)}`).join("");
|
|
337
|
+
const url =
|
|
338
|
+
`${ADS_SEARCH_API}?q=${encodeURIComponent(query)}` +
|
|
339
|
+
`&fl=${SEARCH_FIELDS}&rows=${maxResults}&start=${start}` +
|
|
340
|
+
`&sort=${encodeURIComponent(sort)}${fqParam}`;
|
|
341
|
+
|
|
342
|
+
const data = await adsFetch(url);
|
|
343
|
+
const response = data.response ?? data;
|
|
344
|
+
const docs: any[] = response.docs ?? [];
|
|
345
|
+
const totalResults = response.numFound ?? 0;
|
|
346
|
+
|
|
347
|
+
if (docs.length === 0) {
|
|
348
|
+
return { content: `No papers found for query: ${query}`, exitCode: 0, isError: false };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const papers = docs.map(parseDoc);
|
|
352
|
+
const formatter = detail === "full" ? formatPaper : formatPaperCompact;
|
|
353
|
+
let text = `Found ${totalResults} papers (showing ${start + 1}-${start + papers.length}):\n`;
|
|
354
|
+
text += papers.map((p, i) => formatter(p, i)).join("\n\n");
|
|
355
|
+
|
|
356
|
+
const nextStart = start + papers.length;
|
|
357
|
+
if (nextStart < totalResults) {
|
|
358
|
+
text += `\n\n→ Use start=${nextStart} to see the next page of results.`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return { content: text, exitCode: 0, isError: false };
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
getDisplayInfo(args) {
|
|
365
|
+
return { kind: "search" };
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
formatCall(args) {
|
|
369
|
+
const parts: string[] = [];
|
|
370
|
+
if (args.query) parts.push(String(args.query));
|
|
371
|
+
if (args.year_from || args.year_to) parts.push(`${args.year_from ?? "*"}–${args.year_to ?? "*"}`);
|
|
372
|
+
if (args.database) parts.push(`db:${args.database}`);
|
|
373
|
+
if (args.sort_by && args.sort_by !== "relevance") parts.push(`sort:${args.sort_by}`);
|
|
374
|
+
return parts.join(" ");
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// ── ads_paper ──────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
ctx.agent.registerTool({
|
|
381
|
+
name: "ads_paper",
|
|
382
|
+
displayName: "paper",
|
|
383
|
+
description:
|
|
384
|
+
"Fetch a paper's details from ADS by bibcode, DOI, or arXiv ID.\n" +
|
|
385
|
+
"Optional BibTeX return. ⚠️ Load the ads skill BEFORE using for field syntax help.\n" +
|
|
386
|
+
"Requires ADS_API_TOKEN.",
|
|
387
|
+
input_schema: {
|
|
388
|
+
type: "object",
|
|
389
|
+
properties: {
|
|
390
|
+
id: {
|
|
391
|
+
type: "string",
|
|
392
|
+
description:
|
|
393
|
+
'Paper identifier: ADS bibcode (e.g. "2023ApJ...950L..12A"), ' +
|
|
394
|
+
'DOI (e.g. "10.3847/2041-8213/acb7e0"), or arXiv ID (e.g. "arXiv:2301.01234").',
|
|
395
|
+
},
|
|
396
|
+
bibtex: {
|
|
397
|
+
type: "boolean",
|
|
398
|
+
description: "Include BibTeX citation (default: false).",
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
required: ["id"],
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
async execute(args) {
|
|
405
|
+
const id = (args.id as string).trim();
|
|
406
|
+
const query = buildIdQuery(id);
|
|
407
|
+
const url = `${ADS_SEARCH_API}?q=${encodeURIComponent(query)}&fl=${SEARCH_FIELDS}&rows=1`;
|
|
408
|
+
|
|
409
|
+
const data = await adsFetch(url);
|
|
410
|
+
const docs: any[] = (data.response ?? data).docs ?? [];
|
|
411
|
+
|
|
412
|
+
if (docs.length === 0) {
|
|
413
|
+
return { content: `Paper not found: ${id}`, exitCode: 1, isError: true };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const paper = parseDoc(docs[0]);
|
|
417
|
+
let text = formatPaper(paper);
|
|
418
|
+
|
|
419
|
+
if (args.bibtex && paper.bibcode) {
|
|
420
|
+
try {
|
|
421
|
+
const exportData = await adsFetch(`${ADS_EXPORT_API}/bibtex`, {
|
|
422
|
+
method: "POST",
|
|
423
|
+
body: JSON.stringify({ bibcode: [paper.bibcode] }),
|
|
424
|
+
});
|
|
425
|
+
const bibtex = exportData.export ?? "";
|
|
426
|
+
if (bibtex) text += `\n\nBibTeX:\n${bibtex}`;
|
|
427
|
+
} catch {
|
|
428
|
+
text += "\n\n[Failed to fetch BibTeX]";
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return { content: text, exitCode: 0, isError: false };
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
getDisplayInfo(args) {
|
|
436
|
+
return { kind: "read" };
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
formatCall(args) {
|
|
440
|
+
return String(args.id ?? "");
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// ── ads_citations ──────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
ctx.agent.registerTool({
|
|
447
|
+
name: "ads_citations",
|
|
448
|
+
displayName: "citations",
|
|
449
|
+
description:
|
|
450
|
+
"List citations or references of an ADS bibcode.\n" +
|
|
451
|
+
"detail='compact'|'full'. ⚠️ Load the ads skill BEFORE using for syntax help.\n" +
|
|
452
|
+
"Requires ADS_API_TOKEN.",
|
|
453
|
+
input_schema: {
|
|
454
|
+
type: "object",
|
|
455
|
+
properties: {
|
|
456
|
+
bibcode: {
|
|
457
|
+
type: "string",
|
|
458
|
+
description: 'ADS bibcode of the paper, e.g. "2023ApJ...950L..12A".',
|
|
459
|
+
},
|
|
460
|
+
direction: {
|
|
461
|
+
type: "string",
|
|
462
|
+
enum: ["citations", "references"],
|
|
463
|
+
description: '"citations" = papers that cite this paper (default). "references" = papers this paper cites.',
|
|
464
|
+
},
|
|
465
|
+
max_results: {
|
|
466
|
+
type: "number",
|
|
467
|
+
description: "Max papers to return (default 10, max 50).",
|
|
468
|
+
},
|
|
469
|
+
sort_by: {
|
|
470
|
+
type: "string",
|
|
471
|
+
enum: ["date", "citation_count", "read_count"],
|
|
472
|
+
description: "Sort order (default: date).",
|
|
473
|
+
},
|
|
474
|
+
start: {
|
|
475
|
+
type: "number",
|
|
476
|
+
description: "Start index for pagination (default 0).",
|
|
477
|
+
},
|
|
478
|
+
detail: {
|
|
479
|
+
type: "string",
|
|
480
|
+
enum: ["compact", "full"],
|
|
481
|
+
description: "Output detail level. 'compact' (default) or 'full' with abstracts.",
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
required: ["bibcode"],
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
async execute(args) {
|
|
488
|
+
const bibcode = args.bibcode as string;
|
|
489
|
+
const direction = (args.direction as string) ?? "citations";
|
|
490
|
+
const maxResults = Math.min((args.max_results as number) ?? 10, 50);
|
|
491
|
+
const start = (args.start as number) ?? 0;
|
|
492
|
+
const sort = buildSortParam((args.sort_by as string) ?? "date");
|
|
493
|
+
const detail = (args.detail as string) ?? "compact";
|
|
494
|
+
|
|
495
|
+
const queryFunc = direction === "citations" ? "citations" : "references";
|
|
496
|
+
const query = `${queryFunc}(${bibcode})`;
|
|
497
|
+
|
|
498
|
+
const url =
|
|
499
|
+
`${ADS_SEARCH_API}?q=${encodeURIComponent(query)}` +
|
|
500
|
+
`&fl=${SEARCH_FIELDS}&rows=${maxResults}&start=${start}` +
|
|
501
|
+
`&sort=${encodeURIComponent(sort)}`;
|
|
502
|
+
|
|
503
|
+
const data = await adsFetch(url);
|
|
504
|
+
const response = data.response ?? data;
|
|
505
|
+
const docs: any[] = response.docs ?? [];
|
|
506
|
+
const totalResults = response.numFound ?? 0;
|
|
507
|
+
|
|
508
|
+
if (docs.length === 0) {
|
|
509
|
+
const label = direction === "citations" ? "citing" : "referenced by";
|
|
510
|
+
return { content: `No papers ${label} ${bibcode}`, exitCode: 0, isError: false };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const papers = docs.map(parseDoc);
|
|
514
|
+
const formatter = detail === "full" ? formatPaper : formatPaperCompact;
|
|
515
|
+
const dirLabel = direction === "citations" ? "citing papers" : "referenced papers";
|
|
516
|
+
let text = `Found ${totalResults} ${dirLabel} (showing ${start + 1}-${start + papers.length}):\n`;
|
|
517
|
+
text += papers.map((p, i) => formatter(p, i)).join("\n\n");
|
|
518
|
+
|
|
519
|
+
const nextStart = start + papers.length;
|
|
520
|
+
if (nextStart < totalResults) {
|
|
521
|
+
text += `\n\n→ Use start=${nextStart} to see the next page of results.`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return { content: text, exitCode: 0, isError: false };
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
getDisplayInfo(args) {
|
|
528
|
+
return { kind: "search" };
|
|
529
|
+
},
|
|
530
|
+
|
|
531
|
+
formatCall(args) {
|
|
532
|
+
const dir = (args.direction as string) ?? "citations";
|
|
533
|
+
return `${args.bibcode} ${dir}`;
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// ── ads_download_pdf ───────────────────────────────────────────────
|
|
538
|
+
|
|
539
|
+
ctx.agent.registerTool({
|
|
540
|
+
name: "ads_download_pdf",
|
|
541
|
+
displayName: "download",
|
|
542
|
+
description:
|
|
543
|
+
"Download a paper's PDF (bibcode/DOI/arXiv ID).\n" +
|
|
544
|
+
"Fallback: publisher → ADS → arXiv. ⚠️ Load the ads skill BEFORE using.\n" +
|
|
545
|
+
"Requires ADS_API_TOKEN.",
|
|
546
|
+
input_schema: {
|
|
547
|
+
type: "object",
|
|
548
|
+
properties: {
|
|
549
|
+
id: {
|
|
550
|
+
type: "string",
|
|
551
|
+
description:
|
|
552
|
+
'Paper identifier: ADS bibcode (e.g. "2023ApJ...950L..12A"), ' +
|
|
553
|
+
'DOI (e.g. "10.3847/2041-8213/acb7e0"), or arXiv ID (e.g. "arXiv:2301.01234").',
|
|
554
|
+
},
|
|
555
|
+
output_path: {
|
|
556
|
+
type: "string",
|
|
557
|
+
description:
|
|
558
|
+
"File path to save the PDF. Defaults to ~/.agent-sh/papers/{bibcode}.pdf. " +
|
|
559
|
+
"If a directory path is given, saves as {dir}/{bibcode}.pdf.",
|
|
560
|
+
},
|
|
561
|
+
source: {
|
|
562
|
+
type: "string",
|
|
563
|
+
enum: ["auto", "publisher", "ads", "arxiv"],
|
|
564
|
+
description:
|
|
565
|
+
"Preferred PDF source. 'auto' (default) tries publisher → ADS → arXiv. " +
|
|
566
|
+
"'publisher' = PUB_PDF, 'ads' = ADS_PDF, 'arxiv' = EPRINT_PDF.",
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
required: ["id"],
|
|
570
|
+
},
|
|
571
|
+
modifiesFiles: true,
|
|
572
|
+
|
|
573
|
+
async execute(args) {
|
|
574
|
+
const sourceParam = (args.source as string) ?? "auto";
|
|
575
|
+
const trimmedId = (args.id as string).trim();
|
|
576
|
+
const idIsBibcode = isBibcode(trimmedId);
|
|
577
|
+
const downloadDir = await getPdfDownloadDir();
|
|
578
|
+
|
|
579
|
+
// Fast path: check if already downloaded (bibcode only)
|
|
580
|
+
if (!args.output_path && idIsBibcode) {
|
|
581
|
+
const candidatePath = downloadDir
|
|
582
|
+
? path.join(downloadDir, `${bibcodeToFilename(trimmedId)}.pdf`)
|
|
583
|
+
: path.join(process.cwd(), `${bibcodeToFilename(trimmedId)}.pdf`);
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
const stat = await fs.stat(candidatePath);
|
|
587
|
+
return {
|
|
588
|
+
content:
|
|
589
|
+
`PDF already exists: ${candidatePath}\n` +
|
|
590
|
+
` Bibcode: ${trimmedId}\n` +
|
|
591
|
+
` Size: ${formatFileSize(stat.size)}\n` +
|
|
592
|
+
` Modified: ${stat.mtime.toISOString()}\n\n` +
|
|
593
|
+
`To re-download, delete the file first or specify a different output_path.`,
|
|
594
|
+
exitCode: 0,
|
|
595
|
+
isError: false,
|
|
596
|
+
};
|
|
597
|
+
} catch { /* not cached, continue */ }
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Resolve identifier to bibcode
|
|
601
|
+
let resolved: { bibcode: string; title: string };
|
|
602
|
+
try {
|
|
603
|
+
resolved = await resolveBibcode(args.id as string);
|
|
604
|
+
} catch {
|
|
605
|
+
return { content: `Paper not found: ${args.id}`, exitCode: 1, isError: true };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const { bibcode, title } = resolved;
|
|
609
|
+
const safeName = `${bibcodeToFilename(bibcode)}.pdf`;
|
|
610
|
+
|
|
611
|
+
let outputPath: string;
|
|
612
|
+
if (!args.output_path) {
|
|
613
|
+
outputPath = downloadDir
|
|
614
|
+
? path.join(downloadDir, safeName)
|
|
615
|
+
: path.join(process.cwd(), safeName);
|
|
616
|
+
} else {
|
|
617
|
+
const op = args.output_path as string;
|
|
618
|
+
outputPath = (op.endsWith("/") || !path.extname(op))
|
|
619
|
+
? path.join(op, safeName)
|
|
620
|
+
: op;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Check if already exists (post-resolution)
|
|
624
|
+
try {
|
|
625
|
+
const stat = await fs.stat(outputPath);
|
|
626
|
+
return {
|
|
627
|
+
content:
|
|
628
|
+
`PDF already exists: ${outputPath}\n` +
|
|
629
|
+
` Bibcode: ${bibcode}\n` +
|
|
630
|
+
` Title: ${title}\n` +
|
|
631
|
+
` Size: ${formatFileSize(stat.size)}\n` +
|
|
632
|
+
` Modified: ${stat.mtime.toISOString()}\n\n` +
|
|
633
|
+
`To re-download, delete the file first or specify a different output_path.`,
|
|
634
|
+
exitCode: 0,
|
|
635
|
+
isError: false,
|
|
636
|
+
};
|
|
637
|
+
} catch { /* proceed with download */ }
|
|
638
|
+
|
|
639
|
+
// Try sources in order
|
|
640
|
+
const sourceOrder: PDFSource[] =
|
|
641
|
+
sourceParam === "auto"
|
|
642
|
+
? ["publisher", "ads", "arxiv"]
|
|
643
|
+
: [sourceParam as PDFSource];
|
|
644
|
+
|
|
645
|
+
let pdfBuffer: ArrayBuffer | undefined;
|
|
646
|
+
let usedSource: PDFSource | undefined;
|
|
647
|
+
let lastError = "";
|
|
648
|
+
|
|
649
|
+
for (const src of sourceOrder) {
|
|
650
|
+
try {
|
|
651
|
+
pdfBuffer = await fetchPDF(bibcode, src);
|
|
652
|
+
usedSource = src;
|
|
653
|
+
break;
|
|
654
|
+
} catch (err: any) {
|
|
655
|
+
lastError = err.message ?? String(err);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (!pdfBuffer || !usedSource) {
|
|
660
|
+
return {
|
|
661
|
+
content:
|
|
662
|
+
`Failed to download PDF for ${bibcode} (${title}).\n` +
|
|
663
|
+
`Tried sources: ${sourceOrder.join(" → ")}.\n` +
|
|
664
|
+
`Last error: ${lastError}\n\n` +
|
|
665
|
+
`Try downloading manually: ${ADS_LINK_GATEWAY}/${bibcode}/EPRINT_PDF`,
|
|
666
|
+
exitCode: 1,
|
|
667
|
+
isError: true,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
672
|
+
await fs.writeFile(outputPath, Buffer.from(pdfBuffer));
|
|
673
|
+
|
|
674
|
+
const sourceLabel = { publisher: "publisher", ads: "ADS", arxiv: "arXiv" }[usedSource];
|
|
675
|
+
return {
|
|
676
|
+
content:
|
|
677
|
+
`Downloaded PDF for: ${title}\n` +
|
|
678
|
+
` Bibcode: ${bibcode}\n` +
|
|
679
|
+
` Source: ${sourceLabel}\n` +
|
|
680
|
+
` Saved to: ${outputPath}\n` +
|
|
681
|
+
` Size: ${formatFileSize(pdfBuffer.byteLength)}`,
|
|
682
|
+
exitCode: 0,
|
|
683
|
+
isError: false,
|
|
684
|
+
};
|
|
685
|
+
},
|
|
686
|
+
|
|
687
|
+
getDisplayInfo(args) {
|
|
688
|
+
return { kind: "write" };
|
|
689
|
+
},
|
|
690
|
+
|
|
691
|
+
formatCall(args) {
|
|
692
|
+
return String(args.id ?? "");
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
}
|