scorpion-cli 0.1.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 +152 -0
- package/install.ps1 +22 -0
- package/install.sh +19 -0
- package/package.json +44 -0
- package/src/agent.js +226 -0
- package/src/commands.js +193 -0
- package/src/index.js +86 -0
- package/src/ollama.js +126 -0
- package/src/registry.js +112 -0
- package/src/tools/context.js +432 -0
- package/src/tools/deep-research.js +425 -0
- package/src/tools/documents.js +299 -0
- package/src/tools/filesystem.js +402 -0
- package/src/tools/shell.js +134 -0
- package/src/tools/system.js +379 -0
- package/src/tools/web.js +433 -0
- package/src/ui/charts.js +213 -0
- package/src/ui/export.js +141 -0
- package/src/ui/formatter.js +365 -0
- package/src/ui/images.js +153 -0
- package/src/ui/panels.js +150 -0
- package/src/ui/progress.js +131 -0
- package/src/ui/repl.js +477 -0
- package/src/ui/spinner.js +99 -0
- package/src/ui/table.js +145 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep Research with Live Progress
|
|
3
|
+
* Shows real-time status as research progresses
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as cheerio from 'cheerio';
|
|
7
|
+
import fetch from 'node-fetch';
|
|
8
|
+
|
|
9
|
+
// Progress callback - set by repl.js
|
|
10
|
+
let progressCallback = null;
|
|
11
|
+
|
|
12
|
+
export function setProgressCallback(callback) {
|
|
13
|
+
progressCallback = callback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function emitProgress(event, data) {
|
|
17
|
+
if (progressCallback) progressCallback(event, data);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ============= Configuration =============
|
|
21
|
+
|
|
22
|
+
const SOURCES = {
|
|
23
|
+
ARXIV: { name: 'arXiv', authority: 0.95, type: 'academic', icon: '📄' },
|
|
24
|
+
HACKERNEWS: { name: 'Hacker News', authority: 0.75, type: 'tech', icon: '🔶' },
|
|
25
|
+
WIKIPEDIA: { name: 'Wikipedia', authority: 0.85, type: 'reference', icon: '📚' }
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ============= Helpers =============
|
|
29
|
+
|
|
30
|
+
async function fetchWithTimeout(url, timeout = 6000) {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
33
|
+
setTimeout(() => { controller.abort(); reject(new Error('Timeout')); }, timeout)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await Promise.race([
|
|
38
|
+
fetch(url, {
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
headers: { 'User-Agent': 'Scorpion-Research/1.0' }
|
|
41
|
+
}),
|
|
42
|
+
timeoutPromise
|
|
43
|
+
]);
|
|
44
|
+
return response;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============= Search Functions =============
|
|
51
|
+
|
|
52
|
+
async function searchArxiv(query, maxResults = 5) {
|
|
53
|
+
emitProgress('searching', { source: 'arXiv', query });
|
|
54
|
+
try {
|
|
55
|
+
const searchUrl = `https://export.arxiv.org/api/query?search_query=all:${encodeURIComponent(query)}&start=0&max_results=${maxResults}`;
|
|
56
|
+
const response = await fetchWithTimeout(searchUrl, 8000);
|
|
57
|
+
const xml = await response.text();
|
|
58
|
+
const $ = cheerio.load(xml, { xmlMode: true });
|
|
59
|
+
|
|
60
|
+
const results = [];
|
|
61
|
+
$('entry').each((i, elem) => {
|
|
62
|
+
if (results.length >= maxResults) return false;
|
|
63
|
+
const $entry = $(elem);
|
|
64
|
+
const title = $entry.find('title').text().replace(/\s+/g, ' ').trim();
|
|
65
|
+
const summary = $entry.find('summary').text().replace(/\s+/g, ' ').trim();
|
|
66
|
+
const url = $entry.find('id').text().trim();
|
|
67
|
+
const published = $entry.find('published').text().trim();
|
|
68
|
+
const authors = [];
|
|
69
|
+
$entry.find('author name').each((j, auth) => {
|
|
70
|
+
authors.push($(auth).text().trim());
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (title && url) {
|
|
74
|
+
results.push({
|
|
75
|
+
title, url,
|
|
76
|
+
snippet: summary.slice(0, 400),
|
|
77
|
+
source: SOURCES.ARXIV,
|
|
78
|
+
published,
|
|
79
|
+
authors: authors.slice(0, 3),
|
|
80
|
+
type: 'academic'
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
emitProgress('found', { source: 'arXiv', count: results.length });
|
|
86
|
+
return results;
|
|
87
|
+
} catch (e) {
|
|
88
|
+
emitProgress('found', { source: 'arXiv', count: 0 });
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function searchHackerNews(query, maxResults = 5) {
|
|
94
|
+
emitProgress('searching', { source: 'Hacker News', query });
|
|
95
|
+
try {
|
|
96
|
+
const searchUrl = `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(query)}&tags=story&hitsPerPage=${maxResults}`;
|
|
97
|
+
const response = await fetchWithTimeout(searchUrl, 6000);
|
|
98
|
+
const data = await response.json();
|
|
99
|
+
|
|
100
|
+
const results = [];
|
|
101
|
+
const hits = data?.hits || [];
|
|
102
|
+
|
|
103
|
+
for (const hit of hits) {
|
|
104
|
+
if (results.length >= maxResults) break;
|
|
105
|
+
results.push({
|
|
106
|
+
title: hit.title,
|
|
107
|
+
url: hit.url || `https://news.ycombinator.com/item?id=${hit.objectID}`,
|
|
108
|
+
snippet: hit.title,
|
|
109
|
+
source: SOURCES.HACKERNEWS,
|
|
110
|
+
points: hit.points,
|
|
111
|
+
comments: hit.num_comments,
|
|
112
|
+
author: hit.author,
|
|
113
|
+
type: 'tech'
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
emitProgress('found', { source: 'Hacker News', count: results.length });
|
|
118
|
+
return results;
|
|
119
|
+
} catch (e) {
|
|
120
|
+
emitProgress('found', { source: 'Hacker News', count: 0 });
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function searchWikipedia(query, maxResults = 3) {
|
|
126
|
+
emitProgress('searching', { source: 'Wikipedia', query });
|
|
127
|
+
try {
|
|
128
|
+
const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&srlimit=${maxResults}&format=json&origin=*`;
|
|
129
|
+
const response = await fetchWithTimeout(searchUrl, 6000);
|
|
130
|
+
const data = await response.json();
|
|
131
|
+
|
|
132
|
+
const results = [];
|
|
133
|
+
const items = data?.query?.search || [];
|
|
134
|
+
|
|
135
|
+
for (const item of items) {
|
|
136
|
+
if (results.length >= maxResults) break;
|
|
137
|
+
const snippet = item.snippet.replace(/<[^>]+>/g, '');
|
|
138
|
+
results.push({
|
|
139
|
+
title: item.title,
|
|
140
|
+
url: `https://en.wikipedia.org/wiki/${encodeURIComponent(item.title.replace(/ /g, '_'))}`,
|
|
141
|
+
snippet,
|
|
142
|
+
source: SOURCES.WIKIPEDIA,
|
|
143
|
+
wordcount: item.wordcount,
|
|
144
|
+
type: 'reference'
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
emitProgress('found', { source: 'Wikipedia', count: results.length });
|
|
149
|
+
return results;
|
|
150
|
+
} catch (e) {
|
|
151
|
+
emitProgress('found', { source: 'Wikipedia', count: 0 });
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ============= Query Decomposition =============
|
|
157
|
+
|
|
158
|
+
function decomposeQuery(query) {
|
|
159
|
+
const subQueries = [];
|
|
160
|
+
const queryLower = query.toLowerCase();
|
|
161
|
+
const mainTopic = query.replace(/^(what|how|why|when|where|who|explain|describe|research|analyze)/i, '').trim();
|
|
162
|
+
|
|
163
|
+
subQueries.push({ query: mainTopic, type: 'main', priority: 1 });
|
|
164
|
+
|
|
165
|
+
if (!queryLower.includes('definition') && !queryLower.includes('what is')) {
|
|
166
|
+
subQueries.push({ query: `${mainTopic} definition overview`, type: 'definition', priority: 0.9 });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!queryLower.includes('latest') && !queryLower.includes('recent')) {
|
|
170
|
+
subQueries.push({ query: `${mainTopic} latest developments 2025`, type: 'recent', priority: 0.8 });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return subQueries;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============= Ranking =============
|
|
177
|
+
|
|
178
|
+
function calculateScore(result, query) {
|
|
179
|
+
let score = 0;
|
|
180
|
+
const queryLower = query.toLowerCase();
|
|
181
|
+
const titleLower = result.title?.toLowerCase() || '';
|
|
182
|
+
const snippetLower = result.snippet?.toLowerCase() || '';
|
|
183
|
+
|
|
184
|
+
score += (result.source?.authority || 0.5) * 40;
|
|
185
|
+
if (titleLower.includes(queryLower)) score += 30;
|
|
186
|
+
else {
|
|
187
|
+
const words = queryLower.split(' ');
|
|
188
|
+
const matches = words.filter(w => titleLower.includes(w)).length;
|
|
189
|
+
score += (matches / words.length) * 20;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const snippetWords = queryLower.split(' ');
|
|
193
|
+
const snippetMatches = snippetWords.filter(w => snippetLower.includes(w)).length;
|
|
194
|
+
score += (snippetMatches / snippetWords.length) * 15;
|
|
195
|
+
|
|
196
|
+
if (result.points) score += Math.min(result.points / 50, 10);
|
|
197
|
+
if (result.comments) score += Math.min(result.comments / 20, 5);
|
|
198
|
+
|
|
199
|
+
return score;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function rankResults(results, query) {
|
|
203
|
+
const seen = new Set();
|
|
204
|
+
const ranked = [];
|
|
205
|
+
|
|
206
|
+
for (const result of results) {
|
|
207
|
+
const urlKey = result.url?.replace(/^https?:\/\/(www\.)?/, '').slice(0, 100);
|
|
208
|
+
if (seen.has(urlKey)) continue;
|
|
209
|
+
seen.add(urlKey);
|
|
210
|
+
|
|
211
|
+
result.relevanceScore = calculateScore(result, query);
|
|
212
|
+
ranked.push(result);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
ranked.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
216
|
+
return ranked;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ============= Report Generation =============
|
|
220
|
+
|
|
221
|
+
function generateReport(query, subQueries, allResults, searchTime) {
|
|
222
|
+
const citations = [];
|
|
223
|
+
let citationIndex = 1;
|
|
224
|
+
|
|
225
|
+
const citationMap = new Map();
|
|
226
|
+
for (const result of allResults.slice(0, 15)) {
|
|
227
|
+
citationMap.set(result.url, {
|
|
228
|
+
index: citationIndex++,
|
|
229
|
+
...result
|
|
230
|
+
});
|
|
231
|
+
citations.push(citationMap.get(result.url));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const byType = {
|
|
235
|
+
academic: allResults.filter(r => r.type === 'academic').slice(0, 5),
|
|
236
|
+
tech: allResults.filter(r => r.type === 'tech').slice(0, 5),
|
|
237
|
+
reference: allResults.filter(r => r.type === 'reference').slice(0, 3)
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const confidenceScore = Math.min(100, Math.round(
|
|
241
|
+
(allResults.length / 15) * 40 +
|
|
242
|
+
(byType.academic.length > 0 ? 25 : 0) +
|
|
243
|
+
(byType.reference.length > 0 ? 20 : 0) +
|
|
244
|
+
(byType.tech.length > 0 ? 15 : 0)
|
|
245
|
+
));
|
|
246
|
+
|
|
247
|
+
const report = {
|
|
248
|
+
title: `Research Report: ${query}`,
|
|
249
|
+
metadata: {
|
|
250
|
+
generatedAt: new Date().toISOString(),
|
|
251
|
+
searchTime: searchTime.toFixed(2) + 's',
|
|
252
|
+
totalSources: allResults.length,
|
|
253
|
+
confidenceScore,
|
|
254
|
+
subQueriesUsed: subQueries.length
|
|
255
|
+
},
|
|
256
|
+
sections: [],
|
|
257
|
+
citations
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const topResults = allResults.slice(0, 3);
|
|
261
|
+
report.sections.push({
|
|
262
|
+
title: 'Executive Summary',
|
|
263
|
+
content: topResults.map(r => `${r.source?.icon || '•'} **${r.title}** - ${r.snippet?.slice(0, 150)}...`).join('\n\n')
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
if (byType.academic.length > 0) {
|
|
267
|
+
report.sections.push({
|
|
268
|
+
title: 'Academic Research',
|
|
269
|
+
icon: '📄',
|
|
270
|
+
content: byType.academic.map(r => {
|
|
271
|
+
const cite = citationMap.get(r.url);
|
|
272
|
+
return `- **${r.title}** [${cite?.index}]\n ${r.snippet?.slice(0, 200)}...\n *Authors: ${r.authors?.join(', ') || 'N/A'}*`;
|
|
273
|
+
}).join('\n\n')
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (byType.tech.length > 0) {
|
|
278
|
+
report.sections.push({
|
|
279
|
+
title: 'Technical Discussions',
|
|
280
|
+
icon: '🔶',
|
|
281
|
+
content: byType.tech.map(r => {
|
|
282
|
+
const cite = citationMap.get(r.url);
|
|
283
|
+
return `- **${r.title}** [${cite?.index}] (${r.points || 0} points, ${r.comments || 0} comments)`;
|
|
284
|
+
}).join('\n')
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (byType.reference.length > 0) {
|
|
289
|
+
report.sections.push({
|
|
290
|
+
title: 'Reference Materials',
|
|
291
|
+
icon: '📚',
|
|
292
|
+
content: byType.reference.map(r => {
|
|
293
|
+
const cite = citationMap.get(r.url);
|
|
294
|
+
return `- **${r.title}** [${cite?.index}]\n ${r.snippet?.slice(0, 200)}...`;
|
|
295
|
+
}).join('\n\n')
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return report;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function formatReportAsMarkdown(report) {
|
|
303
|
+
let md = `# ${report.title}\n\n`;
|
|
304
|
+
md += `**Generated:** ${new Date(report.metadata.generatedAt).toLocaleString()}\n`;
|
|
305
|
+
md += `**Sources:** ${report.metadata.totalSources} | **Confidence:** ${report.metadata.confidenceScore}% | **Time:** ${report.metadata.searchTime}\n\n`;
|
|
306
|
+
md += `---\n\n`;
|
|
307
|
+
|
|
308
|
+
for (const section of report.sections) {
|
|
309
|
+
md += `## ${section.icon || ''} ${section.title}\n\n`;
|
|
310
|
+
md += section.content + '\n\n';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
md += `---\n\n## 📑 Sources\n\n`;
|
|
314
|
+
for (const cite of report.citations.slice(0, 15)) {
|
|
315
|
+
md += `[${cite.index}] ${cite.title}\n ${cite.url}\n\n`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return md;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ============= Main Research =============
|
|
322
|
+
|
|
323
|
+
export async function conductResearch(query, options = {}) {
|
|
324
|
+
const {
|
|
325
|
+
depth = 'standard',
|
|
326
|
+
maxResults = 15,
|
|
327
|
+
timeout = 30000
|
|
328
|
+
} = options;
|
|
329
|
+
|
|
330
|
+
const startTime = Date.now();
|
|
331
|
+
|
|
332
|
+
emitProgress('start', { query, depth });
|
|
333
|
+
|
|
334
|
+
const subQueries = decomposeQuery(query);
|
|
335
|
+
const queryLimit = depth === 'quick' ? 2 : depth === 'deep' ? 3 : 2;
|
|
336
|
+
const activeSubQueries = subQueries.slice(0, queryLimit);
|
|
337
|
+
|
|
338
|
+
for (const sq of activeSubQueries) {
|
|
339
|
+
emitProgress('subquery', { query: sq.query, type: sq.type });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const allSearches = [];
|
|
343
|
+
for (const sq of activeSubQueries) {
|
|
344
|
+
allSearches.push(searchArxiv(sq.query, 3));
|
|
345
|
+
allSearches.push(searchHackerNews(sq.query, 3));
|
|
346
|
+
if (sq.type === 'main' || sq.type === 'definition') {
|
|
347
|
+
allSearches.push(searchWikipedia(sq.query, 2));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const timeoutPromise = new Promise(resolve => setTimeout(() => resolve([]), timeout));
|
|
352
|
+
const resultsArrays = await Promise.race([
|
|
353
|
+
Promise.all(allSearches.map(p => p.catch(() => []))),
|
|
354
|
+
timeoutPromise
|
|
355
|
+
]);
|
|
356
|
+
|
|
357
|
+
const allResults = (resultsArrays || []).flat();
|
|
358
|
+
const rankedResults = rankResults(allResults, query);
|
|
359
|
+
const searchTime = (Date.now() - startTime) / 1000;
|
|
360
|
+
|
|
361
|
+
emitProgress('generating', {});
|
|
362
|
+
const report = generateReport(query, activeSubQueries, rankedResults, searchTime);
|
|
363
|
+
|
|
364
|
+
emitProgress('complete', {
|
|
365
|
+
sources: report.metadata.totalSources,
|
|
366
|
+
confidence: report.metadata.confidenceScore,
|
|
367
|
+
time: searchTime
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
success: true,
|
|
372
|
+
query,
|
|
373
|
+
depth,
|
|
374
|
+
metadata: report.metadata,
|
|
375
|
+
report: formatReportAsMarkdown(report),
|
|
376
|
+
rawResults: rankedResults.slice(0, maxResults),
|
|
377
|
+
subQueries: activeSubQueries
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ============= Tool Export =============
|
|
382
|
+
|
|
383
|
+
export const deepResearchTool = {
|
|
384
|
+
schema: {
|
|
385
|
+
type: 'function',
|
|
386
|
+
function: {
|
|
387
|
+
name: 'deep_research',
|
|
388
|
+
description: 'Comprehensive multi-source research with live progress updates. Searches academic papers (arXiv), tech discussions (HN), and references (Wikipedia). Generates structured reports with citations.',
|
|
389
|
+
parameters: {
|
|
390
|
+
type: 'object',
|
|
391
|
+
required: ['query'],
|
|
392
|
+
properties: {
|
|
393
|
+
query: {
|
|
394
|
+
type: 'string',
|
|
395
|
+
description: 'Research topic or question'
|
|
396
|
+
},
|
|
397
|
+
depth: {
|
|
398
|
+
type: 'string',
|
|
399
|
+
enum: ['quick', 'standard', 'deep'],
|
|
400
|
+
description: 'Research depth'
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
execute: async ({ query, depth = 'standard' }) => {
|
|
407
|
+
try {
|
|
408
|
+
const result = await conductResearch(query, { depth });
|
|
409
|
+
|
|
410
|
+
return JSON.stringify({
|
|
411
|
+
success: true,
|
|
412
|
+
query: result.query,
|
|
413
|
+
depth: result.depth,
|
|
414
|
+
confidence: result.metadata.confidenceScore,
|
|
415
|
+
sources: result.metadata.totalSources,
|
|
416
|
+
searchTime: result.metadata.searchTime,
|
|
417
|
+
report: result.report
|
|
418
|
+
});
|
|
419
|
+
} catch (error) {
|
|
420
|
+
return JSON.stringify({ success: false, error: error.message, query });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
export default { conductResearch, deepResearchTool, setProgressCallback };
|