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/export.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export Utilities
|
|
3
|
+
* Save research reports and data to files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { colors } from './formatter.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Save content to a file
|
|
12
|
+
* @param {string} content - Content to save
|
|
13
|
+
* @param {string} filename - Filename (will be placed in reports/ directory)
|
|
14
|
+
* @param {string} format - Format (md, txt, json)
|
|
15
|
+
*/
|
|
16
|
+
export async function saveToFile(content, filename, format = 'md') {
|
|
17
|
+
try {
|
|
18
|
+
// Create reports directory if it doesn't exist
|
|
19
|
+
const reportsDir = path.join(process.cwd(), 'reports');
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(reportsDir);
|
|
22
|
+
} catch {
|
|
23
|
+
await fs.mkdir(reportsDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Generate filename with timestamp if not provided
|
|
27
|
+
if (!filename) {
|
|
28
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
29
|
+
filename = `research-${timestamp}.${format}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Ensure extension
|
|
33
|
+
if (!filename.endsWith(`.${format}`)) {
|
|
34
|
+
filename += `.${format}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const filepath = path.join(reportsDir, filename);
|
|
38
|
+
await fs.writeFile(filepath, content, 'utf-8');
|
|
39
|
+
|
|
40
|
+
console.log();
|
|
41
|
+
console.log(colors.success(` ✓ Saved to: ${filepath}`));
|
|
42
|
+
console.log();
|
|
43
|
+
|
|
44
|
+
return filepath;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(colors.error(` ✗ Failed to save file: ${error.message}`));
|
|
48
|
+
console.log();
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Save research report
|
|
55
|
+
* @param {Object} report - Report data
|
|
56
|
+
* @param {string} filename - Optional custom filename
|
|
57
|
+
*/
|
|
58
|
+
export async function saveResearchReport(report, filename = null) {
|
|
59
|
+
if (typeof report === 'string') {
|
|
60
|
+
// Already formatted as markdown
|
|
61
|
+
return saveToFile(report, filename, 'md');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Convert object to markdown
|
|
65
|
+
let markdown = `# ${report.title || 'Research Report'}\n\n`;
|
|
66
|
+
|
|
67
|
+
if (report.metadata) {
|
|
68
|
+
markdown += `**Generated:** ${report.metadata.generatedAt || new Date().toISOString()}\n`;
|
|
69
|
+
markdown += `**Confidence:** ${report.metadata.confidenceScore || 'N/A'}%\n`;
|
|
70
|
+
markdown += `**Sources:** ${report.metadata.totalSources || 0}\n\n`;
|
|
71
|
+
markdown += `---\n\n`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (report.sections && Array.isArray(report.sections)) {
|
|
75
|
+
for (const section of report.sections) {
|
|
76
|
+
markdown += `## ${section.title}\n\n`;
|
|
77
|
+
markdown += section.content + '\n\n';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (report.citations && Array.isArray(report.citations)) {
|
|
82
|
+
markdown += `---\n\n## Sources\n\n`;
|
|
83
|
+
for (const cite of report.citations) {
|
|
84
|
+
markdown += `[${cite.index}] ${cite.title}\n`;
|
|
85
|
+
markdown += ` ${cite.url}\n\n`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return saveToFile(markdown, filename, 'md');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Save JSON data
|
|
94
|
+
* @param {Object} data - Data to save
|
|
95
|
+
* @param {string} filename - Optional custom filename
|
|
96
|
+
*/
|
|
97
|
+
export async function saveJSON(data, filename = null) {
|
|
98
|
+
const json = JSON.stringify(data, null, 2);
|
|
99
|
+
return saveToFile(json, filename, 'json');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* List saved reports
|
|
104
|
+
*/
|
|
105
|
+
export async function listReports() {
|
|
106
|
+
try {
|
|
107
|
+
const reportsDir = path.join(process.cwd(), 'reports');
|
|
108
|
+
const files = await fs.readdir(reportsDir);
|
|
109
|
+
|
|
110
|
+
if (files.length === 0) {
|
|
111
|
+
console.log(colors.dim(' No saved reports'));
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log();
|
|
116
|
+
console.log(colors.bold.white(' Saved Reports:'));
|
|
117
|
+
console.log(colors.dim(' ─'.repeat(30)));
|
|
118
|
+
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
const filepath = path.join(reportsDir, file);
|
|
121
|
+
const stats = await fs.stat(filepath);
|
|
122
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
123
|
+
const modified = stats.mtime.toLocaleDateString();
|
|
124
|
+
|
|
125
|
+
console.log(colors.dim(' • ') + colors.cyan(file) + colors.dim(` (${sizeKB}KB, ${modified})`));
|
|
126
|
+
}
|
|
127
|
+
console.log();
|
|
128
|
+
|
|
129
|
+
return files;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.log(colors.dim(' No reports directory'));
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default {
|
|
137
|
+
saveToFile,
|
|
138
|
+
saveResearchReport,
|
|
139
|
+
saveJSON,
|
|
140
|
+
listReports
|
|
141
|
+
};
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Formatter - Claude Code / Gemini CLI Style
|
|
3
|
+
* Clean, minimal, beautiful terminal output
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import gradient from 'gradient-string';
|
|
8
|
+
|
|
9
|
+
// Custom gradients
|
|
10
|
+
const bannerGradient = gradient(['#ff6b35', '#f7c531', '#f7931e']);
|
|
11
|
+
const accentGradient = gradient(['#667eea', '#764ba2']);
|
|
12
|
+
const successGradient = gradient(['#11998e', '#38ef7d']);
|
|
13
|
+
|
|
14
|
+
// Colors
|
|
15
|
+
const colors = {
|
|
16
|
+
accent: chalk.hex('#f7931e'), // Orange accent
|
|
17
|
+
dim: chalk.dim,
|
|
18
|
+
success: chalk.hex('#38ef7d'),
|
|
19
|
+
error: chalk.hex('#ff6b6b'),
|
|
20
|
+
info: chalk.hex('#74b9ff'),
|
|
21
|
+
muted: chalk.hex('#636e72'),
|
|
22
|
+
white: chalk.white,
|
|
23
|
+
bold: chalk.bold,
|
|
24
|
+
cyan: chalk.cyan,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* ASCII Art Banner for Scorpion
|
|
29
|
+
*/
|
|
30
|
+
const BANNER = `
|
|
31
|
+
███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██╗ ██████╗ ███╗ ██╗
|
|
32
|
+
██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔══██╗██║██╔═══██╗████╗ ██║
|
|
33
|
+
███████╗██║ ██║ ██║██████╔╝██████╔╝██║██║ ██║██╔██╗ ██║
|
|
34
|
+
╚════██║██║ ██║ ██║██╔══██╗██╔═══╝ ██║██║ ██║██║╚██╗██║
|
|
35
|
+
███████║╚██████╗╚██████╔╝██║ ██║██║ ██║╚██████╔╝██║ ╚████║
|
|
36
|
+
╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Display the welcome banner
|
|
41
|
+
*/
|
|
42
|
+
export function displayBanner() {
|
|
43
|
+
console.clear();
|
|
44
|
+
console.log(bannerGradient(BANNER));
|
|
45
|
+
|
|
46
|
+
console.log(colors.muted(' ─────────────────────────────────────────────────────────────'));
|
|
47
|
+
console.log();
|
|
48
|
+
console.log(colors.accent(' ✦ ') + colors.white('Welcome to ') + colors.bold.hex('#f7931e')('Scorpion') + colors.white('!'));
|
|
49
|
+
console.log();
|
|
50
|
+
console.log(colors.dim(' Tips for getting started:'));
|
|
51
|
+
console.log(colors.dim(' 1. Ask questions, edit files, or run commands.'));
|
|
52
|
+
console.log(colors.dim(' 2. Be specific for the best results.'));
|
|
53
|
+
console.log(colors.dim(' 3. Type ') + colors.accent('exit') + colors.dim(' to quit.'));
|
|
54
|
+
console.log();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Display a status message (like "Searching the web for...")
|
|
59
|
+
* @param {string} action - The action being performed
|
|
60
|
+
* @param {string} detail - Optional detail
|
|
61
|
+
*/
|
|
62
|
+
export function showStatus(action, detail = '') {
|
|
63
|
+
const prefix = colors.muted(' ○ ');
|
|
64
|
+
const actionText = colors.muted(action);
|
|
65
|
+
const detailText = detail ? colors.white(detail) : '';
|
|
66
|
+
|
|
67
|
+
process.stdout.write(`\r${prefix}${actionText} ${detailText}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Update status to show completion
|
|
72
|
+
* @param {string} action - The action that completed
|
|
73
|
+
* @param {string} detail - Optional detail
|
|
74
|
+
*/
|
|
75
|
+
export function completeStatus(action, detail = '') {
|
|
76
|
+
const prefix = colors.success(' ● ');
|
|
77
|
+
const actionText = colors.muted(action);
|
|
78
|
+
const detailText = detail ? colors.white(detail) : '';
|
|
79
|
+
|
|
80
|
+
console.log(`\r${prefix}${actionText} ${detailText}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Show tool execution in Gemini CLI style
|
|
85
|
+
* @param {string} toolName - Name of the tool
|
|
86
|
+
* @param {Object} args - Tool arguments
|
|
87
|
+
*/
|
|
88
|
+
export function showToolExecution(toolName, args) {
|
|
89
|
+
// Convert tool name to human-readable action
|
|
90
|
+
const action = getHumanReadableAction(toolName, args);
|
|
91
|
+
console.log();
|
|
92
|
+
console.log(colors.info(' ◆ ') + colors.white(action));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Convert tool name to human-readable action
|
|
97
|
+
*/
|
|
98
|
+
function getHumanReadableAction(toolName, args) {
|
|
99
|
+
const actions = {
|
|
100
|
+
'run_command': `Running command: ${chalk.cyan(args?.command?.slice(0, 50) || '...')}`,
|
|
101
|
+
'run_powershell_script': 'Executing PowerShell script...',
|
|
102
|
+
'create_file': `Creating file: ${chalk.cyan(args?.path || '...')}`,
|
|
103
|
+
'read_file': `Reading file: ${chalk.cyan(args?.path || '...')}`,
|
|
104
|
+
'write_file': `Writing to file: ${chalk.cyan(args?.path || '...')}`,
|
|
105
|
+
'list_directory': `Listing directory: ${chalk.cyan(args?.path || '...')}`,
|
|
106
|
+
'search_files': `Searching for: ${chalk.cyan(args?.pattern || '...')}`,
|
|
107
|
+
'delete_file': `Deleting: ${chalk.cyan(args?.path || '...')}`,
|
|
108
|
+
'get_file_info': `Getting file info: ${chalk.cyan(args?.path || '...')}`,
|
|
109
|
+
'get_cpu_usage': 'Checking CPU usage...',
|
|
110
|
+
'get_memory_usage': 'Checking memory usage...',
|
|
111
|
+
'get_disk_usage': 'Checking disk usage...',
|
|
112
|
+
'get_running_processes': 'Getting running processes...',
|
|
113
|
+
'get_system_info': 'Getting system information...',
|
|
114
|
+
'analyze_performance': 'Analyzing system performance...',
|
|
115
|
+
'get_network_info': 'Getting network information...',
|
|
116
|
+
'web_search': `Searching the web for: ${chalk.cyan(`"${args?.query || '...'}"`)}`,
|
|
117
|
+
'web_fetch': `Fetching: ${chalk.cyan(args?.url || '...')}`,
|
|
118
|
+
'research_topic': `Researching in-depth: ${chalk.cyan(`"${args?.topic || '...'}"`,)} (fetching multiple sources)`,
|
|
119
|
+
'deep_research': `🔬 Deep Research: ${chalk.cyan(`"${args?.query || '...'}"`)} [${args?.depth || 'standard'} depth]`,
|
|
120
|
+
'save_report': `Saving report: ${chalk.cyan(args?.path || '...')}`,
|
|
121
|
+
'create_summary': 'Creating summary...',
|
|
122
|
+
'create_table': 'Creating table...',
|
|
123
|
+
'append_to_document': `Appending to: ${chalk.cyan(args?.path || '...')}`,
|
|
124
|
+
// Context tools
|
|
125
|
+
'get_current_datetime': 'Getting current date and time...',
|
|
126
|
+
'get_current_context': 'Getting current context...',
|
|
127
|
+
'check_latest_version': `Checking latest version of: ${chalk.cyan(args?.name || '...')}`,
|
|
128
|
+
'calculate_time_difference': 'Calculating time difference...',
|
|
129
|
+
'get_timezone_time': `Getting time in: ${chalk.cyan(args?.timezone || '...')}`,
|
|
130
|
+
'set_reminder': 'Setting reminder...',
|
|
131
|
+
'list_reminders': 'Listing reminders...',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return actions[toolName] || `Executing: ${toolName}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Show thinking indicator
|
|
139
|
+
* @param {string} text - Thinking text (optional, for extended thinking)
|
|
140
|
+
*/
|
|
141
|
+
export function showThinking(text = '') {
|
|
142
|
+
if (text) {
|
|
143
|
+
// Show abbreviated thinking
|
|
144
|
+
const abbreviated = text.length > 100 ? text.slice(0, 100) + '...' : text;
|
|
145
|
+
console.log(colors.dim(` 💭 ${abbreviated}`));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Show the waiting/simmering indicator
|
|
151
|
+
*/
|
|
152
|
+
export function showSimmering() {
|
|
153
|
+
console.log();
|
|
154
|
+
console.log(colors.accent(' ✦ ') + colors.dim('Thinking...'));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Format Markdown content for terminal display
|
|
159
|
+
* @param {string} content - Markdown content
|
|
160
|
+
* @returns {string} - Formatted content
|
|
161
|
+
*/
|
|
162
|
+
function formatMarkdown(content) {
|
|
163
|
+
let result = content;
|
|
164
|
+
|
|
165
|
+
// Headers with better spacing and visual hierarchy
|
|
166
|
+
result = result.replace(/^### (.+)$/gm, (_, text) => '\n' + chalk.bold.cyan(` ▸ ${text}`) + '\n');
|
|
167
|
+
result = result.replace(/^## (.+)$/gm, (_, text) => '\n' + chalk.bold.hex('#f7931e')(` ═ ${text}`) + '\n' + chalk.dim(' ' + '─'.repeat(Math.min(text.length + 4, 50))) + '\n');
|
|
168
|
+
result = result.replace(/^# (.+)$/gm, (_, text) => '\n' + chalk.bold.white(` ${text}`) + '\n' + chalk.dim(' ' + '═'.repeat(Math.min(text.length, 50))) + '\n');
|
|
169
|
+
|
|
170
|
+
// Bold with better contrast
|
|
171
|
+
result = result.replace(/\*\*(.+?)\*\*/g, (_, text) => chalk.bold.hex('#00d9ff')(text));
|
|
172
|
+
|
|
173
|
+
// Inline code with background effect
|
|
174
|
+
result = result.replace(/`([^`]+)`/g, (_, code) => chalk.bgHex('#1a1a1a').cyan(` ${code} `));
|
|
175
|
+
|
|
176
|
+
// Code blocks with better borders
|
|
177
|
+
result = result.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
|
|
178
|
+
const lines = code.trim().split('\n');
|
|
179
|
+
const formattedLines = lines.map(line => chalk.dim(' │ ') + chalk.green(line));
|
|
180
|
+
const langLabel = lang ? chalk.cyan(lang) : chalk.dim('code');
|
|
181
|
+
return '\n' + chalk.dim(' ┌─[ ') + langLabel + chalk.dim(' ]─') + '\n' + formattedLines.join('\n') + '\n' + chalk.dim(' └─────────') + '\n';
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Bullets with better spacing
|
|
185
|
+
result = result.replace(/^- (.+)$/gm, (_, text) => chalk.hex('#00d9ff')(' • ') + text);
|
|
186
|
+
result = result.replace(/^\* (.+)$/gm, (_, text) => chalk.hex('#00d9ff')(' • ') + text);
|
|
187
|
+
|
|
188
|
+
// Numbered lists with colored numbers
|
|
189
|
+
result = result.replace(/^(\d+)\. (.+)$/gm, (_, num, text) => chalk.hex('#00d9ff')(` ${num}. `) + text);
|
|
190
|
+
|
|
191
|
+
// Horizontal rules with better style
|
|
192
|
+
result = result.replace(/^---$/gm, '\n' + chalk.dim(' ' + '─'.repeat(55)) + '\n');
|
|
193
|
+
|
|
194
|
+
// Links with better visibility
|
|
195
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => chalk.bold.cyan(text) + chalk.dim(` → ${url.slice(0, 50)}${url.length > 50 ? '...' : ''}`));
|
|
196
|
+
|
|
197
|
+
// Blockquotes (simulated collapsible sections)
|
|
198
|
+
result = result.replace(/^> (.+)$/gm, (_, text) => chalk.dim(' ┃ ') + chalk.italic(text));
|
|
199
|
+
|
|
200
|
+
// Tables (basic support)
|
|
201
|
+
result = result.replace(/^\|(.+)\|$/gm, (match) => {
|
|
202
|
+
return chalk.dim(' ') + match.split('|').map(cell => cell.trim()).filter(Boolean).join(chalk.dim(' │ '));
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Show AI response with Markdown formatting
|
|
210
|
+
* @param {string} content - The response content
|
|
211
|
+
*/
|
|
212
|
+
export function showResponse(content) {
|
|
213
|
+
console.log();
|
|
214
|
+
|
|
215
|
+
// Format Markdown
|
|
216
|
+
const formatted = formatMarkdown(content);
|
|
217
|
+
|
|
218
|
+
// Add slight indent to response
|
|
219
|
+
const lines = formatted.split('\n');
|
|
220
|
+
for (const line of lines) {
|
|
221
|
+
// Skip extra indentation for already-formatted lines
|
|
222
|
+
if (line.startsWith(' ')) {
|
|
223
|
+
console.log(line);
|
|
224
|
+
} else {
|
|
225
|
+
console.log(' ' + line);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
console.log();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Show error
|
|
233
|
+
* @param {string} message - Error message
|
|
234
|
+
*/
|
|
235
|
+
export function showError(message) {
|
|
236
|
+
console.log();
|
|
237
|
+
console.log(colors.error(' ✗ ') + colors.error(message));
|
|
238
|
+
console.log();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Show success
|
|
243
|
+
* @param {string} message - Success message
|
|
244
|
+
*/
|
|
245
|
+
export function showSuccess(message) {
|
|
246
|
+
console.log(colors.success(' ✓ ') + colors.white(message));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Show info
|
|
251
|
+
* @param {string} message - Info message
|
|
252
|
+
*/
|
|
253
|
+
export function showInfo(message) {
|
|
254
|
+
console.log(colors.info(' ℹ ') + colors.dim(message));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get the prompt string
|
|
259
|
+
*/
|
|
260
|
+
export function getPrompt() {
|
|
261
|
+
return colors.white(' > ');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Show a horizontal divider
|
|
266
|
+
*/
|
|
267
|
+
export function showDivider() {
|
|
268
|
+
console.log(colors.muted(' ─────────────────────────────────────────────────────────────'));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Clear the current line
|
|
273
|
+
*/
|
|
274
|
+
export function clearLine() {
|
|
275
|
+
process.stdout.write('\r\x1b[K');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Show welcome code block (like Claude Code)
|
|
280
|
+
*/
|
|
281
|
+
export function showWelcomeCode() {
|
|
282
|
+
console.log();
|
|
283
|
+
console.log(colors.dim(' while(curious) {'));
|
|
284
|
+
console.log(colors.dim(' question_everything();'));
|
|
285
|
+
console.log(colors.dim(' dig_deeper();'));
|
|
286
|
+
console.log(colors.dim(' connect_dots(unexpected);'));
|
|
287
|
+
console.log();
|
|
288
|
+
console.log(colors.dim(' if (stuck) {'));
|
|
289
|
+
console.log(colors.accent(' keep_thinking();'));
|
|
290
|
+
console.log(colors.dim(' }'));
|
|
291
|
+
console.log(colors.dim(' }'));
|
|
292
|
+
console.log();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Show a progress bar
|
|
297
|
+
* @param {number} current - Current value
|
|
298
|
+
* @param {number} total - Total value
|
|
299
|
+
* @param {string} label - Label for the progress
|
|
300
|
+
*/
|
|
301
|
+
export function showProgress(current, total, label = '') {
|
|
302
|
+
const percentage = Math.round((current / total) * 100);
|
|
303
|
+
const barWidth = 30;
|
|
304
|
+
const filled = Math.round(barWidth * (current / total));
|
|
305
|
+
const empty = barWidth - filled;
|
|
306
|
+
|
|
307
|
+
const bar = colors.success('█'.repeat(filled)) + colors.dim('░'.repeat(empty));
|
|
308
|
+
const text = colors.dim(` ${label} `) + bar + colors.dim(` ${percentage}%`);
|
|
309
|
+
|
|
310
|
+
process.stdout.write(`\r${text}`);
|
|
311
|
+
if (current === total) console.log();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Show a section header (for reports)
|
|
316
|
+
* @param {string} title - Section title
|
|
317
|
+
* @param {string} icon - Optional icon
|
|
318
|
+
*/
|
|
319
|
+
export function showSection(title, icon = '◆') {
|
|
320
|
+
console.log();
|
|
321
|
+
console.log(colors.accent(` ${icon} `) + colors.bold.white(title));
|
|
322
|
+
console.log(colors.dim(' ' + '─'.repeat(50)));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Show a key-value pair
|
|
327
|
+
* @param {string} key - Key
|
|
328
|
+
* @param {string} value - Value
|
|
329
|
+
*/
|
|
330
|
+
export function showKeyValue(key, value) {
|
|
331
|
+
console.log(colors.dim(` ${key}: `) + colors.white(value));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Show a confidence badge
|
|
336
|
+
* @param {number} score - Confidence score (0-100)
|
|
337
|
+
*/
|
|
338
|
+
export function showConfidence(score) {
|
|
339
|
+
let color, label;
|
|
340
|
+
if (score >= 80) { color = colors.success; label = 'HIGH'; }
|
|
341
|
+
else if (score >= 50) { color = colors.info; label = 'MEDIUM'; }
|
|
342
|
+
else { color = colors.error; label = 'LOW'; }
|
|
343
|
+
|
|
344
|
+
console.log(colors.dim(' Confidence: ') + color(`${score}% (${label})`));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Show a citation
|
|
349
|
+
* @param {number} index - Citation index
|
|
350
|
+
* @param {string} title - Source title
|
|
351
|
+
* @param {string} url - Source URL
|
|
352
|
+
*/
|
|
353
|
+
export function showCitation(index, title, url) {
|
|
354
|
+
console.log(colors.dim(` [${index}] `) + colors.cyan(title.slice(0, 60)));
|
|
355
|
+
console.log(colors.dim(` ${url.slice(0, 70)}`));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export {
|
|
359
|
+
chalk,
|
|
360
|
+
gradient,
|
|
361
|
+
colors,
|
|
362
|
+
bannerGradient,
|
|
363
|
+
accentGradient,
|
|
364
|
+
successGradient
|
|
365
|
+
};
|
package/src/ui/images.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Display Utility
|
|
3
|
+
* Supports iTerm2 and Kitty terminal protocols
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { colors } from './formatter.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detect terminal type
|
|
12
|
+
*/
|
|
13
|
+
function detectTerminal() {
|
|
14
|
+
const term = process.env.TERM_PROGRAM || process.env.TERM || '';
|
|
15
|
+
|
|
16
|
+
if (term.includes('iTerm')) return 'iterm2';
|
|
17
|
+
if (term.includes('kitty') || process.env.KITTY_WINDOW_ID) return 'kitty';
|
|
18
|
+
if (term.includes('wezterm')) return 'wezterm';
|
|
19
|
+
|
|
20
|
+
return 'unsupported';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Display image in iTerm2
|
|
25
|
+
* @param {Buffer} imageBuffer - Image buffer
|
|
26
|
+
* @param {Object} options - Display options
|
|
27
|
+
*/
|
|
28
|
+
function displayImageITerm2(imageBuffer, options = {}) {
|
|
29
|
+
const { width = 'auto', height = 'auto', inline = true } = options;
|
|
30
|
+
|
|
31
|
+
const base64 = imageBuffer.toString('base64');
|
|
32
|
+
const escape = '\x1B]';
|
|
33
|
+
const bell = '\x07';
|
|
34
|
+
|
|
35
|
+
// iTerm2 inline image protocol
|
|
36
|
+
const params = [
|
|
37
|
+
'inline=1',
|
|
38
|
+
`width=${width}`,
|
|
39
|
+
`height=${height}`,
|
|
40
|
+
'preserveAspectRatio=1'
|
|
41
|
+
].join(';');
|
|
42
|
+
|
|
43
|
+
process.stdout.write(`${escape}1337;File=${params}:${base64}${bell}\n`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Display image in Kitty
|
|
48
|
+
* @param {Buffer} imageBuffer - Image buffer
|
|
49
|
+
* @param {Object} options - Display options
|
|
50
|
+
*/
|
|
51
|
+
function displayImageKitty(imageBuffer, options = {}) {
|
|
52
|
+
const { width = 80, height = 24 } = options;
|
|
53
|
+
|
|
54
|
+
const base64 = imageBuffer.toString('base64');
|
|
55
|
+
const chunks = base64.match(/.{1,4096}/g) || [];
|
|
56
|
+
|
|
57
|
+
// Kitty graphics protocol
|
|
58
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
59
|
+
const isLast = i === chunks.length - 1;
|
|
60
|
+
const action = i === 0 ? 'T' : 't';
|
|
61
|
+
const more = isLast ? 0 : 1;
|
|
62
|
+
|
|
63
|
+
process.stdout.write(`\x1B_G${action}=d,f=100,m=${more};${chunks[i]}\x1B\\\n`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Display an image file
|
|
69
|
+
* @param {string} imagePath - Path to image file
|
|
70
|
+
* @param {Object} options - Display options
|
|
71
|
+
*/
|
|
72
|
+
export async function displayImage(imagePath, options = {}) {
|
|
73
|
+
try {
|
|
74
|
+
const terminal = detectTerminal();
|
|
75
|
+
|
|
76
|
+
if (terminal === 'unsupported') {
|
|
77
|
+
console.log(colors.dim(` 📷 Image: ${path.basename(imagePath)}`));
|
|
78
|
+
console.log(colors.dim(` (Terminal doesn't support inline images)`));
|
|
79
|
+
console.log(colors.dim(` Path: ${imagePath}`));
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Read image file
|
|
84
|
+
const imageBuffer = await fs.readFile(imagePath);
|
|
85
|
+
|
|
86
|
+
console.log();
|
|
87
|
+
console.log(colors.dim(` 📷 ${path.basename(imagePath)}`));
|
|
88
|
+
console.log();
|
|
89
|
+
|
|
90
|
+
// Display based on terminal type
|
|
91
|
+
if (terminal === 'iterm2') {
|
|
92
|
+
displayImageITerm2(imageBuffer, options);
|
|
93
|
+
} else if (terminal === 'kitty' || terminal === 'wezterm') {
|
|
94
|
+
displayImageKitty(imageBuffer, options);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log();
|
|
98
|
+
return true;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.log(colors.error(` ✗ Failed to display image: ${error.message}`));
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if terminal supports images
|
|
107
|
+
*/
|
|
108
|
+
export function supportsImages() {
|
|
109
|
+
return detectTerminal() !== 'unsupported';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Display image from URL (download and display)
|
|
114
|
+
* @param {string} url - Image URL
|
|
115
|
+
* @param {Object} options - Display options
|
|
116
|
+
*/
|
|
117
|
+
export async function displayImageFromURL(url, options = {}) {
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(url);
|
|
120
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
121
|
+
|
|
122
|
+
const terminal = detectTerminal();
|
|
123
|
+
|
|
124
|
+
if (terminal === 'unsupported') {
|
|
125
|
+
console.log(colors.dim(` 📷 Image: ${url}`));
|
|
126
|
+
console.log(colors.dim(` (Terminal doesn't support inline images)`));
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log();
|
|
131
|
+
console.log(colors.dim(` 📷 Image from: ${url.slice(0, 60)}...`));
|
|
132
|
+
console.log();
|
|
133
|
+
|
|
134
|
+
if (terminal === 'iterm2') {
|
|
135
|
+
displayImageITerm2(buffer, options);
|
|
136
|
+
} else if (terminal === 'kitty' || terminal === 'wezterm') {
|
|
137
|
+
displayImageKitty(buffer, options);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log();
|
|
141
|
+
return true;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.log(colors.error(` ✗ Failed to display image: ${error.message}`));
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default {
|
|
149
|
+
displayImage,
|
|
150
|
+
displayImageFromURL,
|
|
151
|
+
supportsImages,
|
|
152
|
+
detectTerminal
|
|
153
|
+
};
|