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
package/src/ui/repl.js
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive REPL - Claude Code / Gemini CLI Style
|
|
3
|
+
* Clean, natural language interface
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import readline from 'readline';
|
|
7
|
+
import { runAgent } from '../agent.js';
|
|
8
|
+
import { isModelAvailable, DEFAULT_MODEL } from '../ollama.js';
|
|
9
|
+
import { setProgressCallback } from '../tools/deep-research.js';
|
|
10
|
+
import { handleStats, handleListReports, handleDemo, handleExport, handleListModels, handleModelSelect } from '../commands.js';
|
|
11
|
+
import {
|
|
12
|
+
displayBanner,
|
|
13
|
+
showToolExecution,
|
|
14
|
+
showThinking,
|
|
15
|
+
showResponse,
|
|
16
|
+
showError,
|
|
17
|
+
showInfo,
|
|
18
|
+
showSuccess,
|
|
19
|
+
showSimmering,
|
|
20
|
+
showDivider,
|
|
21
|
+
showWelcomeCode,
|
|
22
|
+
getPrompt,
|
|
23
|
+
clearLine,
|
|
24
|
+
colors,
|
|
25
|
+
chalk
|
|
26
|
+
} from './formatter.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Start the interactive REPL
|
|
30
|
+
* @param {Object} options - REPL options
|
|
31
|
+
*/
|
|
32
|
+
export async function startREPL(options = {}) {
|
|
33
|
+
const {
|
|
34
|
+
model = DEFAULT_MODEL,
|
|
35
|
+
showThinkingOutput = false, // Hidden by default like Claude Code
|
|
36
|
+
} = options;
|
|
37
|
+
|
|
38
|
+
const settings = { model, showThinkingOutput };
|
|
39
|
+
|
|
40
|
+
// Display welcome banner
|
|
41
|
+
displayBanner();
|
|
42
|
+
|
|
43
|
+
// Check Ollama connection
|
|
44
|
+
try {
|
|
45
|
+
const available = await isModelAvailable(settings.model);
|
|
46
|
+
if (!available) {
|
|
47
|
+
showError(`Model '${settings.model}' not found. Run: ollama pull ${settings.model}`);
|
|
48
|
+
} else {
|
|
49
|
+
showSuccess(`Connected to ${settings.model}`);
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
showError('Could not connect to Ollama. Make sure it is running.');
|
|
53
|
+
showInfo('Start with: ollama serve');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Show the welcome code block like Claude Code
|
|
57
|
+
showWelcomeCode();
|
|
58
|
+
|
|
59
|
+
showSimmering();
|
|
60
|
+
|
|
61
|
+
// Create readline interface
|
|
62
|
+
const rl = readline.createInterface({
|
|
63
|
+
input: process.stdin,
|
|
64
|
+
output: process.stdout,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Conversation history
|
|
68
|
+
let history = [];
|
|
69
|
+
let isClosing = false;
|
|
70
|
+
|
|
71
|
+
// Main loop
|
|
72
|
+
const prompt = () => {
|
|
73
|
+
if (isClosing || rl.closed) return;
|
|
74
|
+
rl.question(getPrompt(), async (input) => {
|
|
75
|
+
if (isClosing || rl.closed) return;
|
|
76
|
+
const trimmedInput = input.trim();
|
|
77
|
+
|
|
78
|
+
// Handle empty input
|
|
79
|
+
if (!trimmedInput) {
|
|
80
|
+
prompt();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Handle exit
|
|
85
|
+
if (trimmedInput.toLowerCase() === 'exit' || trimmedInput.toLowerCase() === 'quit') {
|
|
86
|
+
isClosing = true;
|
|
87
|
+
console.log();
|
|
88
|
+
showInfo('Goodbye! 👋');
|
|
89
|
+
console.log();
|
|
90
|
+
rl.close();
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle slash commands and retain the older bare command aliases.
|
|
95
|
+
const command = parseCommand(trimmedInput);
|
|
96
|
+
if (command) {
|
|
97
|
+
const shouldContinue = await handleCommand(command, settings, () => {
|
|
98
|
+
history = [];
|
|
99
|
+
}, (question) => new Promise((resolve) => rl.question(question, resolve)));
|
|
100
|
+
if (shouldContinue && !isClosing && !rl.closed) {
|
|
101
|
+
prompt();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (isClosing || rl.closed) return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Show help
|
|
108
|
+
if (trimmedInput.toLowerCase() === 'help' || trimmedInput === '?') {
|
|
109
|
+
showHelp();
|
|
110
|
+
prompt();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Handle clear
|
|
115
|
+
if (trimmedInput.toLowerCase() === 'clear') {
|
|
116
|
+
history = [];
|
|
117
|
+
displayBanner();
|
|
118
|
+
showSuccess('Conversation cleared');
|
|
119
|
+
showWelcomeCode();
|
|
120
|
+
prompt();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Handle stats command
|
|
125
|
+
if (trimmedInput.toLowerCase() === 'stats') {
|
|
126
|
+
await handleStats();
|
|
127
|
+
prompt();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Handle reports command
|
|
132
|
+
if (trimmedInput.toLowerCase() === 'reports') {
|
|
133
|
+
await handleListReports();
|
|
134
|
+
prompt();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Handle export command
|
|
139
|
+
if (trimmedInput.toLowerCase().startsWith('export ')) {
|
|
140
|
+
const format = trimmedInput.split(' ')[1] || 'md';
|
|
141
|
+
await handleExport(format);
|
|
142
|
+
prompt();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Handle demo command
|
|
147
|
+
if (trimmedInput.toLowerCase() === 'demo') {
|
|
148
|
+
await handleDemo();
|
|
149
|
+
prompt();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Process with agent
|
|
154
|
+
await processInput(trimmedInput, history, settings);
|
|
155
|
+
|
|
156
|
+
prompt();
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
prompt();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseCommand(input) {
|
|
164
|
+
if (!input.startsWith('/')) return null;
|
|
165
|
+
const [name, ...args] = input.slice(1).trim().split(/\s+/);
|
|
166
|
+
return name ? { name: name.toLowerCase(), args } : { name: 'help', args: [] };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function handleCommand(command, settings, clearHistory, ask) {
|
|
170
|
+
const arg = command.args[0]?.toLowerCase();
|
|
171
|
+
|
|
172
|
+
switch (command.name) {
|
|
173
|
+
case 'help':
|
|
174
|
+
case '?':
|
|
175
|
+
showHelp();
|
|
176
|
+
return true;
|
|
177
|
+
case 'list':
|
|
178
|
+
case 'models':
|
|
179
|
+
await handleListModels(settings.model);
|
|
180
|
+
return true;
|
|
181
|
+
case 'model': {
|
|
182
|
+
const selected = await handleModelSelect(settings.model, ask, command.args.join(' '));
|
|
183
|
+
if (selected) {
|
|
184
|
+
settings.model = selected;
|
|
185
|
+
showSuccess(`Model changed to ${settings.model}`);
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
case 'settings':
|
|
190
|
+
showSettings(settings);
|
|
191
|
+
return true;
|
|
192
|
+
case 'think':
|
|
193
|
+
if (!['on', 'off', 'toggle'].includes(arg)) {
|
|
194
|
+
showInfo('Usage: /think on | /think off | /think toggle');
|
|
195
|
+
} else {
|
|
196
|
+
settings.showThinkingOutput = arg === 'toggle' ? !settings.showThinkingOutput : arg === 'on';
|
|
197
|
+
showSuccess(`Thinking output ${settings.showThinkingOutput ? 'enabled' : 'disabled'}`);
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
case 'clear':
|
|
201
|
+
clearHistory();
|
|
202
|
+
displayBanner();
|
|
203
|
+
showSuccess('Conversation cleared');
|
|
204
|
+
showWelcomeCode();
|
|
205
|
+
return true;
|
|
206
|
+
case 'stats':
|
|
207
|
+
await handleStats();
|
|
208
|
+
return true;
|
|
209
|
+
case 'reports':
|
|
210
|
+
await handleListReports();
|
|
211
|
+
return true;
|
|
212
|
+
case 'demo':
|
|
213
|
+
await handleDemo();
|
|
214
|
+
return true;
|
|
215
|
+
case 'export':
|
|
216
|
+
await handleExport(command.args[0] || 'md');
|
|
217
|
+
return true;
|
|
218
|
+
case 'exit':
|
|
219
|
+
case 'quit':
|
|
220
|
+
showInfo('Goodbye! 👋');
|
|
221
|
+
process.exit(0);
|
|
222
|
+
default:
|
|
223
|
+
showError(`Unknown command '/${command.name}'. Type /help for available commands.`);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function showSettings(settings) {
|
|
229
|
+
console.log();
|
|
230
|
+
console.log(colors.bold.white(' Current Settings'));
|
|
231
|
+
console.log(colors.dim(' ' + '─'.repeat(30)));
|
|
232
|
+
console.log(` Model: ${colors.accent(settings.model)}`);
|
|
233
|
+
console.log(` Thinking output: ${colors.accent(settings.showThinkingOutput ? 'on' : 'off')}`);
|
|
234
|
+
console.log(` Ollama host: ${colors.dim(process.env.OLLAMA_HOST || 'http://localhost:11434')}`);
|
|
235
|
+
console.log();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Show help for @ triggers
|
|
240
|
+
*/
|
|
241
|
+
function showHelp() {
|
|
242
|
+
console.log();
|
|
243
|
+
console.log(colors.bold.white(' 🎯 Available Commands'));
|
|
244
|
+
console.log(colors.dim(' ─'.repeat(50)));
|
|
245
|
+
console.log();
|
|
246
|
+
|
|
247
|
+
console.log(colors.accent(' @ Triggers:'));
|
|
248
|
+
console.log(colors.dim(' @deepresearch, @deep') + colors.white(' <query>'));
|
|
249
|
+
console.log(colors.dim(' • Comprehensive multi-source research'));
|
|
250
|
+
console.log(colors.dim(' • Searches arXiv, Hacker News, Wikipedia'));
|
|
251
|
+
console.log(colors.dim(' • Generates structured report with citations'));
|
|
252
|
+
console.log(colors.dim(' • Shows live progress updates'));
|
|
253
|
+
console.log();
|
|
254
|
+
console.log(colors.dim(' @quick, @fast') + colors.white(' <question>'));
|
|
255
|
+
console.log(colors.dim(' • Fast response without deep research'));
|
|
256
|
+
console.log();
|
|
257
|
+
|
|
258
|
+
console.log(colors.accent(' Utility Commands:'));
|
|
259
|
+
console.log(colors.dim(' /help, /?') + colors.white(' - Show this help message'));
|
|
260
|
+
console.log(colors.dim(' /list ') + colors.white(' - List installed Ollama models'));
|
|
261
|
+
console.log(colors.dim(' /model ') + colors.white(' [name] - Select or switch model'));
|
|
262
|
+
console.log(colors.dim(' /settings') + colors.white(' - Show current settings'));
|
|
263
|
+
console.log(colors.dim(' /think ') + colors.white(' on|off|toggle - Toggle thinking output'));
|
|
264
|
+
console.log(colors.dim(' /demo ') + colors.white(' - See all UI features (tables, charts)'));
|
|
265
|
+
console.log(colors.dim(' /stats ') + colors.white(' - Show session statistics'));
|
|
266
|
+
console.log(colors.dim(' /reports ') + colors.white(' - List saved research reports'));
|
|
267
|
+
console.log(colors.dim(' /export ') + colors.white(' [md|json] - Export last report'));
|
|
268
|
+
console.log(colors.dim(' /clear ') + colors.white(' - Clear conversation history'));
|
|
269
|
+
console.log(colors.dim(' /exit ') + colors.white(' - Quit Scorpion'));
|
|
270
|
+
console.log();
|
|
271
|
+
|
|
272
|
+
console.log(colors.accent(' Auto-Detection:'));
|
|
273
|
+
console.log(colors.dim(' Queries containing "research", "analyze", "explain in detail"'));
|
|
274
|
+
console.log(colors.dim(' automatically trigger deep research mode.'));
|
|
275
|
+
console.log();
|
|
276
|
+
|
|
277
|
+
console.log(colors.accent(' Examples:'));
|
|
278
|
+
console.log(colors.cyan(' @deep transformer architecture in ML'));
|
|
279
|
+
console.log(colors.cyan(' research quantum computing applications'));
|
|
280
|
+
console.log(colors.cyan(' demo'));
|
|
281
|
+
console.log(colors.cyan(' stats'));
|
|
282
|
+
console.log();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Parse @ triggers and mode from input
|
|
287
|
+
* @param {string} input - Raw user input
|
|
288
|
+
* @returns {Object} - { mode, cleanInput }
|
|
289
|
+
*/
|
|
290
|
+
function parseMode(input) {
|
|
291
|
+
const triggerMatch = input.match(/^@(\w+)\s+(.+)$/);
|
|
292
|
+
|
|
293
|
+
if (triggerMatch) {
|
|
294
|
+
const [, trigger, cleanInput] = triggerMatch;
|
|
295
|
+
|
|
296
|
+
if (trigger === 'deepresearch' || trigger === 'deep') {
|
|
297
|
+
return { mode: 'deep_research', cleanInput, explicit: true };
|
|
298
|
+
}
|
|
299
|
+
if (trigger === 'quick' || trigger === 'fast') {
|
|
300
|
+
return { mode: 'quick_response', cleanInput, explicit: true };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Auto-detect deep research intent
|
|
305
|
+
const deepKeywords = ['research', 'analyze', 'explain in detail', 'comprehensive', 'deep dive', 'study'];
|
|
306
|
+
const inputLower = input.toLowerCase();
|
|
307
|
+
|
|
308
|
+
for (const keyword of deepKeywords) {
|
|
309
|
+
if (inputLower.includes(keyword)) {
|
|
310
|
+
return { mode: 'deep_research', cleanInput: input, explicit: false };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return { mode: 'normal', cleanInput: input, explicit: false };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Process user input through the agent
|
|
319
|
+
* @param {string} input - User input
|
|
320
|
+
* @param {Array} history - Conversation history
|
|
321
|
+
* @param {Object} options - Processing options
|
|
322
|
+
*/
|
|
323
|
+
async function processInput(input, history, options = {}) {
|
|
324
|
+
const { model, showThinkingOutput = false } = options;
|
|
325
|
+
|
|
326
|
+
// Parse mode
|
|
327
|
+
const { mode, cleanInput, explicit } = parseMode(input);
|
|
328
|
+
|
|
329
|
+
// If explicit @ trigger, show what mode we're in
|
|
330
|
+
if (explicit) {
|
|
331
|
+
console.log();
|
|
332
|
+
if (mode === 'deep_research') {
|
|
333
|
+
console.log(colors.accent(' 🔬 ') + colors.bold.white('Deep Research Mode'));
|
|
334
|
+
} else if (mode === 'quick_response') {
|
|
335
|
+
console.log(colors.accent(' ⚡ ') + colors.bold.white('Quick Response Mode'));
|
|
336
|
+
}
|
|
337
|
+
} else if (mode === 'deep_research') {
|
|
338
|
+
// Auto-detected deep research
|
|
339
|
+
console.log();
|
|
340
|
+
console.log(colors.dim(' 🔍 Auto-detected: ') + colors.accent('Deep Research Mode'));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Start timer
|
|
344
|
+
const startTime = Date.now();
|
|
345
|
+
|
|
346
|
+
// Show initial thinking state
|
|
347
|
+
console.log();
|
|
348
|
+
process.stdout.write(colors.accent(' ✦ ') + colors.dim('Thinking...'));
|
|
349
|
+
|
|
350
|
+
let hasStartedOutput = false;
|
|
351
|
+
let toolCallCount = 0;
|
|
352
|
+
let thinkingShown = false;
|
|
353
|
+
let isDeepResearch = false;
|
|
354
|
+
let contentBuffer = ''; // Buffer for Markdown rendering
|
|
355
|
+
|
|
356
|
+
// Setup progress callback for deep research
|
|
357
|
+
setProgressCallback((event, data) => {
|
|
358
|
+
if (event === 'start') {
|
|
359
|
+
console.log();
|
|
360
|
+
console.log(colors.accent(' 🔬 ') + colors.bold.white('Deep Research In Progress'));
|
|
361
|
+
console.log(colors.dim(' ─'.repeat(30)));
|
|
362
|
+
} else if (event === 'subquery') {
|
|
363
|
+
console.log(colors.dim(' ├─ ') + colors.info(`Exploring [${data.type}]: `) + colors.white(`"${data.query.slice(0, 50)}..."`));
|
|
364
|
+
} else if (event === 'searching') {
|
|
365
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
366
|
+
console.log(colors.dim(' │ ') + colors.cyan(`Searching ${data.source}...`) + colors.dim(` (${elapsed}s)`));
|
|
367
|
+
} else if (event === 'found') {
|
|
368
|
+
if (data.count > 0) {
|
|
369
|
+
console.log(colors.dim(' │ ') + colors.success(`✓ Found ${data.count} results from ${data.source}`));
|
|
370
|
+
} else {
|
|
371
|
+
console.log(colors.dim(' │ ') + colors.muted(`⊗ No results from ${data.source}`));
|
|
372
|
+
}
|
|
373
|
+
} else if (event === 'generating') {
|
|
374
|
+
console.log(colors.dim(' ├─ ') + colors.accent('Generating report...'));
|
|
375
|
+
} else if (event === 'complete') {
|
|
376
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
377
|
+
console.log(colors.dim(' └─ ') + colors.success('Research Complete!'));
|
|
378
|
+
console.log();
|
|
379
|
+
console.log(colors.dim(' 📊 Stats:'));
|
|
380
|
+
console.log(colors.dim(' • ') + `Sources: ${chalk.cyan(data.sources)}`);
|
|
381
|
+
console.log(colors.dim(' • ') + `Confidence: ${chalk.cyan(data.confidence + '%')}`);
|
|
382
|
+
console.log(colors.dim(' • ') + `Time: ${chalk.cyan(elapsed + 's')}`);
|
|
383
|
+
console.log();
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const result = await runAgent(cleanInput, {
|
|
389
|
+
history,
|
|
390
|
+
model,
|
|
391
|
+
|
|
392
|
+
onThinking: (text) => {
|
|
393
|
+
if (!hasStartedOutput && !isDeepResearch) {
|
|
394
|
+
clearLine();
|
|
395
|
+
hasStartedOutput = true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (showThinkingOutput && !thinkingShown) {
|
|
399
|
+
showThinking(text);
|
|
400
|
+
thinkingShown = true;
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
onContent: (text) => {
|
|
405
|
+
if (!hasStartedOutput) {
|
|
406
|
+
clearLine();
|
|
407
|
+
console.log(); // Clear the "Thinking..." line
|
|
408
|
+
hasStartedOutput = true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Buffer content for Markdown rendering
|
|
412
|
+
contentBuffer += text;
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
onToolCall: ({ name, args }) => {
|
|
416
|
+
toolCallCount++;
|
|
417
|
+
|
|
418
|
+
// Check if this is deep research
|
|
419
|
+
if (name === 'deep_research') {
|
|
420
|
+
isDeepResearch = true;
|
|
421
|
+
clearLine();
|
|
422
|
+
hasStartedOutput = true;
|
|
423
|
+
return; // Progress callback will handle display
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!hasStartedOutput) {
|
|
427
|
+
clearLine();
|
|
428
|
+
hasStartedOutput = true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Show human-readable action for other tools
|
|
432
|
+
showToolExecution(name, args);
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
onToolResult: ({ name, result }) => {
|
|
436
|
+
// Parse and show brief result if relevant
|
|
437
|
+
try {
|
|
438
|
+
const parsed = JSON.parse(result);
|
|
439
|
+
if (parsed.success === false && parsed.error) {
|
|
440
|
+
console.log(colors.error(` ✗ ${parsed.error}`));
|
|
441
|
+
}
|
|
442
|
+
} catch {
|
|
443
|
+
// Ignore parse errors
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Render buffered content with Markdown formatting
|
|
449
|
+
if (contentBuffer) {
|
|
450
|
+
showResponse(contentBuffer);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Show elapsed time in a nice format
|
|
454
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
455
|
+
console.log();
|
|
456
|
+
console.log(colors.dim(' ─'.repeat(30)));
|
|
457
|
+
console.log(colors.accent(' ⏱ ') + colors.white(`${elapsed}s`));
|
|
458
|
+
console.log();
|
|
459
|
+
|
|
460
|
+
// Update history
|
|
461
|
+
history.push({ role: 'user', content: cleanInput });
|
|
462
|
+
if (contentBuffer) {
|
|
463
|
+
history.push({ role: 'assistant', content: contentBuffer });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
} catch (error) {
|
|
467
|
+
clearLine();
|
|
468
|
+
|
|
469
|
+
if (error.message?.includes('ECONNREFUSED') || error.message?.includes('fetch failed')) {
|
|
470
|
+
showError('Could not connect to Ollama. Make sure it is running.');
|
|
471
|
+
} else {
|
|
472
|
+
showError(error.message);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export { processInput, parseCommand };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spinner Utilities
|
|
3
|
+
* Beautiful loading animations for long operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import { colors } from './formatter.js';
|
|
8
|
+
|
|
9
|
+
let currentSpinner = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Start a spinner
|
|
13
|
+
* @param {string} text - Loading text
|
|
14
|
+
* @param {string} spinnerType - Spinner type (dots, line, etc.)
|
|
15
|
+
*/
|
|
16
|
+
export function startSpinner(text, spinnerType = 'dots') {
|
|
17
|
+
if (currentSpinner) {
|
|
18
|
+
currentSpinner.stop();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
currentSpinner = ora({
|
|
22
|
+
text: colors.dim(text),
|
|
23
|
+
spinner: spinnerType,
|
|
24
|
+
color: 'cyan',
|
|
25
|
+
indent: 2
|
|
26
|
+
}).start();
|
|
27
|
+
|
|
28
|
+
return currentSpinner;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Update spinner text
|
|
33
|
+
* @param {string} text - New text
|
|
34
|
+
*/
|
|
35
|
+
export function updateSpinner(text) {
|
|
36
|
+
if (currentSpinner) {
|
|
37
|
+
currentSpinner.text = colors.dim(text);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Stop spinner with success
|
|
43
|
+
* @param {string} text - Success message
|
|
44
|
+
*/
|
|
45
|
+
export function succeedSpinner(text) {
|
|
46
|
+
if (currentSpinner) {
|
|
47
|
+
currentSpinner.succeed(colors.success(text));
|
|
48
|
+
currentSpinner = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Stop spinner with failure
|
|
54
|
+
* @param {string} text - Failure message
|
|
55
|
+
*/
|
|
56
|
+
export function failSpinner(text) {
|
|
57
|
+
if (currentSpinner) {
|
|
58
|
+
currentSpinner.fail(colors.error(text));
|
|
59
|
+
currentSpinner = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Stop spinner with info
|
|
65
|
+
* @param {string} text - Info message
|
|
66
|
+
*/
|
|
67
|
+
export function infoSpinner(text) {
|
|
68
|
+
if (currentSpinner) {
|
|
69
|
+
currentSpinner.info(colors.info(text));
|
|
70
|
+
currentSpinner = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Stop spinner without message
|
|
76
|
+
*/
|
|
77
|
+
export function stopSpinner() {
|
|
78
|
+
if (currentSpinner) {
|
|
79
|
+
currentSpinner.stop();
|
|
80
|
+
currentSpinner = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get current spinner instance
|
|
86
|
+
*/
|
|
87
|
+
export function getCurrentSpinner() {
|
|
88
|
+
return currentSpinner;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default {
|
|
92
|
+
startSpinner,
|
|
93
|
+
updateSpinner,
|
|
94
|
+
succeedSpinner,
|
|
95
|
+
failSpinner,
|
|
96
|
+
infoSpinner,
|
|
97
|
+
stopSpinner,
|
|
98
|
+
getCurrentSpinner
|
|
99
|
+
};
|