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.
@@ -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
+ };
@@ -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
+ };