erosolar-cli 1.7.14 → 1.7.16

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.
Files changed (61) hide show
  1. package/dist/core/responseVerifier.d.ts +79 -0
  2. package/dist/core/responseVerifier.d.ts.map +1 -0
  3. package/dist/core/responseVerifier.js +443 -0
  4. package/dist/core/responseVerifier.js.map +1 -0
  5. package/dist/shell/interactiveShell.d.ts +10 -0
  6. package/dist/shell/interactiveShell.d.ts.map +1 -1
  7. package/dist/shell/interactiveShell.js +80 -0
  8. package/dist/shell/interactiveShell.js.map +1 -1
  9. package/dist/ui/ShellUIAdapter.d.ts +3 -0
  10. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  11. package/dist/ui/ShellUIAdapter.js +4 -10
  12. package/dist/ui/ShellUIAdapter.js.map +1 -1
  13. package/dist/ui/persistentPrompt.d.ts +4 -0
  14. package/dist/ui/persistentPrompt.d.ts.map +1 -1
  15. package/dist/ui/persistentPrompt.js +10 -11
  16. package/dist/ui/persistentPrompt.js.map +1 -1
  17. package/package.json +1 -1
  18. package/dist/bin/core/agent.js +0 -362
  19. package/dist/bin/core/agentProfileManifest.js +0 -187
  20. package/dist/bin/core/agentProfiles.js +0 -34
  21. package/dist/bin/core/agentRulebook.js +0 -135
  22. package/dist/bin/core/agentSchemaLoader.js +0 -233
  23. package/dist/bin/core/contextManager.js +0 -412
  24. package/dist/bin/core/contextWindow.js +0 -122
  25. package/dist/bin/core/customCommands.js +0 -80
  26. package/dist/bin/core/errors/apiKeyErrors.js +0 -114
  27. package/dist/bin/core/errors/errorTypes.js +0 -340
  28. package/dist/bin/core/errors/safetyValidator.js +0 -304
  29. package/dist/bin/core/errors.js +0 -32
  30. package/dist/bin/core/modelDiscovery.js +0 -755
  31. package/dist/bin/core/preferences.js +0 -224
  32. package/dist/bin/core/schemaValidator.js +0 -92
  33. package/dist/bin/core/secretStore.js +0 -199
  34. package/dist/bin/core/sessionStore.js +0 -187
  35. package/dist/bin/core/toolRuntime.js +0 -290
  36. package/dist/bin/core/types.js +0 -1
  37. package/dist/bin/shell/bracketedPasteManager.js +0 -350
  38. package/dist/bin/shell/fileChangeTracker.js +0 -65
  39. package/dist/bin/shell/interactiveShell.js +0 -2908
  40. package/dist/bin/shell/liveStatus.js +0 -78
  41. package/dist/bin/shell/shellApp.js +0 -290
  42. package/dist/bin/shell/systemPrompt.js +0 -60
  43. package/dist/bin/shell/updateManager.js +0 -108
  44. package/dist/bin/ui/ShellUIAdapter.js +0 -459
  45. package/dist/bin/ui/UnifiedUIController.js +0 -183
  46. package/dist/bin/ui/animation/AnimationScheduler.js +0 -430
  47. package/dist/bin/ui/codeHighlighter.js +0 -854
  48. package/dist/bin/ui/designSystem.js +0 -121
  49. package/dist/bin/ui/display.js +0 -1222
  50. package/dist/bin/ui/interrupts/InterruptManager.js +0 -437
  51. package/dist/bin/ui/layout.js +0 -139
  52. package/dist/bin/ui/orchestration/StatusOrchestrator.js +0 -403
  53. package/dist/bin/ui/outputMode.js +0 -38
  54. package/dist/bin/ui/persistentPrompt.js +0 -183
  55. package/dist/bin/ui/richText.js +0 -338
  56. package/dist/bin/ui/shortcutsHelp.js +0 -87
  57. package/dist/bin/ui/telemetry/UITelemetry.js +0 -443
  58. package/dist/bin/ui/textHighlighter.js +0 -210
  59. package/dist/bin/ui/theme.js +0 -116
  60. package/dist/bin/ui/toolDisplay.js +0 -423
  61. package/dist/bin/ui/toolDisplayAdapter.js +0 -357
@@ -1,116 +0,0 @@
1
- import chalk from 'chalk';
2
- import gradientString from 'gradient-string';
3
- /**
4
- * Theme system matching the Erosolar CLI aesthetics
5
- */
6
- export const theme = {
7
- primary: chalk.hex('#6366F1'), // Indigo
8
- secondary: chalk.hex('#8B5CF6'), // Purple
9
- accent: chalk.hex('#EC4899'), // Pink
10
- success: chalk.hex('#10B981'), // Green
11
- warning: chalk.hex('#F59E0B'), // Amber
12
- error: chalk.hex('#EF4444'), // Red
13
- info: chalk.hex('#3B82F6'), // Blue
14
- dim: chalk.dim,
15
- bold: chalk.bold,
16
- italic: chalk.italic,
17
- underline: chalk.underline,
18
- gradient: {
19
- primary: gradientString(['#6366F1', '#8B5CF6', '#EC4899']),
20
- cool: gradientString(['#3B82F6', '#6366F1', '#8B5CF6']),
21
- warm: gradientString(['#F59E0B', '#EC4899', '#EF4444']),
22
- success: gradientString(['#10B981', '#34D399']),
23
- },
24
- ui: {
25
- border: chalk.hex('#4B5563'),
26
- background: chalk.bgHex('#1F2937'),
27
- userPromptBackground: chalk.bgHex('#4C1D95'),
28
- muted: chalk.hex('#9CA3AF'),
29
- text: chalk.hex('#F3F4F6'),
30
- highlight: chalk.hex('#FCD34D').bold, // Important text
31
- emphasis: chalk.hex('#F472B6').bold, // Emphasized text
32
- code: chalk.hex('#A78BFA'), // Inline code
33
- number: chalk.hex('#60A5FA'), // Numbers
34
- string: chalk.hex('#34D399'), // Strings
35
- keyword: chalk.hex('#F472B6'), // Keywords
36
- operator: chalk.hex('#9CA3AF'), // Operators
37
- },
38
- metrics: {
39
- elapsedLabel: chalk.hex('#FBBF24').bold,
40
- elapsedValue: chalk.hex('#F472B6'),
41
- },
42
- fields: {
43
- label: chalk.hex('#FCD34D').bold,
44
- agent: chalk.hex('#F472B6'),
45
- profile: chalk.hex('#C084FC'),
46
- model: chalk.hex('#A855F7'),
47
- workspace: chalk.hex('#38BDF8'),
48
- },
49
- link: {
50
- label: chalk.hex('#F472B6').underline,
51
- url: chalk.hex('#38BDF8'),
52
- },
53
- diff: {
54
- header: chalk.hex('#FBBF24'),
55
- hunk: chalk.hex('#60A5FA'),
56
- added: chalk.hex('#10B981'),
57
- removed: chalk.hex('#EF4444'),
58
- meta: chalk.hex('#9CA3AF'),
59
- },
60
- user: chalk.hex('#3B82F6'),
61
- assistant: chalk.hex('#8B5CF6'),
62
- system: chalk.hex('#6B7280'),
63
- tool: chalk.hex('#10B981'),
64
- };
65
- /**
66
- * Claude Code style icons
67
- * Following the official Claude Code UI conventions:
68
- * - ⏺ (action): Used for tool calls, actions, and thinking/reasoning
69
- * - ⎿ (subaction): Used for results, details, and nested information
70
- * - ─ (separator): Horizontal lines for dividing sections (not in this object)
71
- * - > (user prompt): User input prefix (used in formatUserPrompt)
72
- */
73
- export const icons = {
74
- success: '✓',
75
- error: '✗',
76
- warning: '⚠',
77
- info: 'ℹ',
78
- arrow: '→',
79
- bullet: '•',
80
- thinking: '◐',
81
- tool: '⚙',
82
- user: '❯',
83
- assistant: '◆',
84
- loading: '⣾',
85
- action: '⏺', // Claude Code: tool actions and thoughts
86
- subaction: '⎿', // Claude Code: results and details
87
- sparkle: '✨', // Erosolar branding
88
- };
89
- export function formatBanner(profileLabel, model) {
90
- const name = profileLabel || 'Agent';
91
- const title = theme.gradient.primary(name);
92
- const subtitle = theme.ui.muted(`${model} • Interactive Shell`);
93
- return `\n${title}\n${subtitle}\n`;
94
- }
95
- export function formatUserPrompt(_profile) {
96
- // Simple, stable prompt - no complex animations
97
- const glyph = theme.user('>');
98
- return `${glyph} `;
99
- }
100
- export function formatToolCall(name, status) {
101
- const statusIcon = status === 'running' ? icons.thinking :
102
- status === 'success' ? icons.success : icons.error;
103
- const statusColor = status === 'running' ? theme.info :
104
- status === 'success' ? theme.success : theme.error;
105
- return `${statusColor(statusIcon)} ${theme.tool(name)}`;
106
- }
107
- export function formatMessage(role, content) {
108
- switch (role) {
109
- case 'user':
110
- return `${theme.user('You:')} ${content}`;
111
- case 'assistant':
112
- return `${theme.assistant('Assistant:')} ${content}`;
113
- case 'system':
114
- return theme.system(`[System] ${content}`);
115
- }
116
- }
@@ -1,423 +0,0 @@
1
- /**
2
- * Tool Display Formatter - Claude Code style
3
- *
4
- * Implements the clean, informative tool execution display that Claude Code uses:
5
- * - Tool call indicators with inline args
6
- * - Result summaries with status indicators
7
- * - Expandable content with previews
8
- * - Diff formatting with colors
9
- */
10
- import { theme } from './theme.js';
11
- /**
12
- * Format tool call display (Claude Code style)
13
- *
14
- * Example output:
15
- * ⏺ Read(src/core/agent.ts)
16
- * ⏺ Search(pattern: "TODO|FIXME", output_mode: "content", head_limit: 15)
17
- */
18
- export function formatToolCall(call, options = {}) {
19
- const includePrefix = options.includePrefix ?? true;
20
- const symbol = includePrefix ? `${theme.info('⏺')} ` : '';
21
- const toolName = theme.tool(call.name);
22
- // Format args inline (only show relevant ones)
23
- const argsDisplay = formatInlineArgs(call.args);
24
- return `${symbol}${toolName}${argsDisplay}`;
25
- }
26
- /**
27
- * Format tool result display (Claude Code style)
28
- *
29
- * Example output:
30
- * ⎿ Read 340 lines
31
- * ⎿ Found 15 lines (ctrl+o to expand)
32
- * ⎿ Completed
33
- */
34
- export function formatToolResult(result, options = {}) {
35
- const includePrefix = options.includePrefix ?? true;
36
- const prefix = includePrefix
37
- ? result.status === 'error'
38
- ? `${theme.error('⎿')} `
39
- : `${theme.success('⎿')} `
40
- : '';
41
- let output = `${prefix}${result.summary}`;
42
- // Add expandable indicator if there's hidden content
43
- if (result.totalLines && result.linesShown && result.totalLines > result.linesShown) {
44
- const expandHint = theme.ui.muted('(ctrl+o to expand)');
45
- output += ` ${expandHint}`;
46
- }
47
- return output;
48
- }
49
- /**
50
- * Format inline args for tool call display
51
- * Keeps it concise, shows only non-default values
52
- * Prioritizes important args like paths, patterns, commands
53
- */
54
- function formatInlineArgs(args) {
55
- // Priority order for different argument types
56
- const priorityArgs = [
57
- 'file_path', 'path', 'pattern', 'command', 'query', 'url',
58
- 'output_mode', 'glob', 'type', 'head_limit', 'offset',
59
- ];
60
- // Special handling for specific args
61
- const skipDefaults = new Set([
62
- 'dangerouslyDisableSandbox',
63
- 'run_in_background',
64
- 'description',
65
- ]);
66
- const formattedArgs = [];
67
- for (const [key, value] of Object.entries(args)) {
68
- // Skip empty/null/undefined
69
- if (value === null || value === undefined || value === '')
70
- continue;
71
- // Skip common defaults
72
- if (skipDefaults.has(key))
73
- continue;
74
- if (key === 'path' && value === '.')
75
- continue;
76
- if (key === 'format' && value === 'plain')
77
- continue;
78
- if (key === 'output_mode' && value === 'files_with_matches')
79
- continue;
80
- // Determine priority
81
- const priority = priorityArgs.indexOf(key);
82
- const actualPriority = priority === -1 ? 999 : priority;
83
- // Format value
84
- let formatted;
85
- if (typeof value === 'string') {
86
- // Smart truncation for paths
87
- if (key === 'file_path' || key === 'path') {
88
- formatted = truncatePathForDisplay(value, 60);
89
- }
90
- else if (key === 'pattern' || key === 'query') {
91
- // Show patterns/queries more prominently
92
- if (value.length > 40) {
93
- formatted = `"${value.slice(0, 37)}..."`;
94
- }
95
- else {
96
- formatted = `"${value}"`;
97
- }
98
- }
99
- else if (key === 'command') {
100
- // Truncate commands intelligently
101
- if (value.length > 60) {
102
- formatted = `"${value.slice(0, 57)}..."`;
103
- }
104
- else {
105
- formatted = `"${value}"`;
106
- }
107
- }
108
- else {
109
- // Regular string truncation
110
- if (value.length > 50) {
111
- formatted = `"${value.slice(0, 47)}..."`;
112
- }
113
- else {
114
- formatted = `"${value}"`;
115
- }
116
- }
117
- }
118
- else if (typeof value === 'boolean') {
119
- // Only show boolean if true (false is usually default)
120
- if (!value)
121
- continue;
122
- formatted = 'true';
123
- }
124
- else if (typeof value === 'number') {
125
- formatted = String(value);
126
- }
127
- else if (Array.isArray(value)) {
128
- // Show array length
129
- formatted = `[${value.length} items]`;
130
- }
131
- else {
132
- formatted = JSON.stringify(value);
133
- }
134
- formattedArgs.push({ key, value: formatted, priority: actualPriority });
135
- }
136
- // Sort by priority and limit to most important args
137
- formattedArgs.sort((a, b) => a.priority - b.priority);
138
- const displayArgs = formattedArgs.slice(0, 5); // Show max 5 args
139
- if (displayArgs.length === 0) {
140
- return '';
141
- }
142
- // Format as inline args
143
- const argStrings = displayArgs.map(arg => {
144
- // For primary args (path, pattern, command), show value directly
145
- if (['file_path', 'path', 'pattern', 'command', 'query', 'url'].includes(arg.key)) {
146
- return arg.value;
147
- }
148
- // For others, show key: value
149
- return `${arg.key}: ${arg.value}`;
150
- });
151
- return `(${argStrings.join(', ')})`;
152
- }
153
- /**
154
- * Truncate path intelligently for display
155
- * Keeps filename and important parent dirs
156
- */
157
- function truncatePathForDisplay(path, maxLength) {
158
- if (path.length <= maxLength)
159
- return path;
160
- const parts = path.split('/').filter(p => p);
161
- if (parts.length <= 2) {
162
- // Short path, just truncate
163
- return path.slice(0, maxLength - 3) + '...';
164
- }
165
- // Keep filename and truncate middle
166
- const filename = parts[parts.length - 1];
167
- if (!filename) {
168
- return path.slice(0, maxLength - 3) + '...';
169
- }
170
- const firstPart = parts.slice(0, 2).join('/');
171
- if (firstPart.length + filename.length + 4 <= maxLength) {
172
- return `${firstPart}/.../${filename}`;
173
- }
174
- // Just show filename if still too long
175
- if (filename.length <= maxLength - 3) {
176
- return `.../${filename}`;
177
- }
178
- return filename.slice(0, maxLength - 3) + '...';
179
- }
180
- /**
181
- * Format expandable content preview
182
- *
183
- * Example:
184
- * import { foo } from 'bar';
185
- * … +312 lines (ctrl+o to expand)
186
- */
187
- export function formatExpandablePreview(content, maxLines = 5) {
188
- const lines = content.split('\n');
189
- const totalLines = lines.length;
190
- if (totalLines <= maxLines) {
191
- return {
192
- preview: content,
193
- isExpanded: true,
194
- totalLines,
195
- };
196
- }
197
- // Show first N lines
198
- const previewLines = lines.slice(0, maxLines);
199
- const remainingLines = totalLines - maxLines;
200
- const ellipsis = theme.ui.muted(`… +${remainingLines} lines`);
201
- const hint = theme.ui.muted('(ctrl+o to expand)');
202
- previewLines.push(`${ellipsis} ${hint}`);
203
- return {
204
- preview: previewLines.join('\n'),
205
- isExpanded: false,
206
- totalLines,
207
- };
208
- }
209
- /**
210
- * Format diff output (Claude Code style)
211
- *
212
- * Example:
213
- * Update(src/core/agent.ts)
214
- * ⎿ Updated src/core/agent.ts with 2 additions and 1 removal
215
- * 75 private async processConversation(): Promise<string> {
216
- * 76 while (true) {
217
- * 77 - this.pruneMessagesIfNeeded();
218
- * 77 + await this.pruneMessagesIfNeeded();
219
- * 78
220
- */
221
- export function formatDiff(diff) {
222
- const lines = [];
223
- for (const line of diff) {
224
- let formatted;
225
- switch (line.type) {
226
- case 'add':
227
- // Green + with line number
228
- const addNum = line.lineNumber ? theme.ui.muted(String(line.lineNumber).padStart(6)) : ' ';
229
- const addSymbol = theme.diff.added('+');
230
- const addContent = theme.diff.added(line.content);
231
- formatted = `${addNum} ${addSymbol} ${addContent}`;
232
- break;
233
- case 'remove':
234
- // Red - with line number
235
- const remNum = line.lineNumber ? theme.ui.muted(String(line.lineNumber).padStart(6)) : ' ';
236
- const remSymbol = theme.diff.removed('-');
237
- const remContent = theme.diff.removed(line.content);
238
- formatted = `${remNum} ${remSymbol} ${remContent}`;
239
- break;
240
- case 'context':
241
- // Gray line number, regular content
242
- const ctxNum = line.lineNumber ? theme.ui.muted(String(line.lineNumber).padStart(6)) : ' ';
243
- formatted = `${ctxNum} ${line.content}`;
244
- break;
245
- case 'info':
246
- // Special info line (file headers, etc.)
247
- formatted = theme.diff.header(line.content);
248
- break;
249
- }
250
- lines.push(formatted);
251
- }
252
- return lines.join('\n');
253
- }
254
- /**
255
- * Format diff summary
256
- * Example: "Updated src/core/agent.ts with 2 additions and 1 removal"
257
- */
258
- export function formatDiffSummary(file, additions, removals) {
259
- const parts = ['Updated', file, 'with'];
260
- if (additions > 0) {
261
- parts.push(theme.diff.added(`${additions} addition${additions === 1 ? '' : 's'}`));
262
- }
263
- if (removals > 0) {
264
- if (additions > 0) {
265
- parts.push('and');
266
- }
267
- parts.push(theme.diff.removed(`${removals} removal${removals === 1 ? '' : 's'}`));
268
- }
269
- return parts.join(' ');
270
- }
271
- /**
272
- * Format multiline content with indentation (Claude Code style)
273
- *
274
- * Adds proper indentation and line wrapping
275
- */
276
- export function formatIndentedContent(content, indent = 4) {
277
- const lines = content.split('\n');
278
- const indentStr = ' '.repeat(indent);
279
- return lines.map(line => `${indentStr}${line}`).join('\n');
280
- }
281
- /**
282
- * Format token usage indicator
283
- * Example: "• Context 5% used (7.2k tokens)"
284
- */
285
- export function formatTokenUsage(percentage, tokens) {
286
- const bullet = theme.info('•');
287
- let pct;
288
- if (percentage >= 90) {
289
- pct = theme.error(`${percentage}%`);
290
- }
291
- else if (percentage >= 70) {
292
- pct = theme.warning(`${percentage}%`);
293
- }
294
- else {
295
- pct = theme.success(`${percentage}%`);
296
- }
297
- const tokensStr = tokens ? ` (${formatTokenCount(tokens)} tokens)` : '';
298
- return `${bullet} Context ${pct} used${tokensStr}`;
299
- }
300
- /**
301
- * Format token count with k/M suffixes
302
- */
303
- function formatTokenCount(tokens) {
304
- if (tokens >= 1000000) {
305
- return `${(tokens / 1000000).toFixed(1)}M`;
306
- }
307
- if (tokens >= 1000) {
308
- return `${(tokens / 1000).toFixed(1)}k`;
309
- }
310
- return String(tokens);
311
- }
312
- /**
313
- * Format timing information
314
- * Example: "(1m 43s)" or "(250ms)"
315
- */
316
- export function formatDuration(ms) {
317
- if (ms < 1000) {
318
- return `${Math.round(ms)}ms`;
319
- }
320
- const seconds = Math.floor(ms / 1000);
321
- const minutes = Math.floor(seconds / 60);
322
- const remainingSeconds = seconds % 60;
323
- if (minutes > 0) {
324
- return `${minutes}m ${remainingSeconds}s`;
325
- }
326
- return `${seconds}s`;
327
- }
328
- /**
329
- * Create a "thinking" indicator
330
- * Example: "∴ Thinking…" or "∴ Thought for 2s (ctrl+o to show thinking)"
331
- */
332
- export function formatThinking(durationMs, hasContent = false) {
333
- const symbol = theme.info('∴');
334
- if (durationMs === undefined) {
335
- return `${symbol} Thinking…`;
336
- }
337
- const duration = formatDuration(durationMs);
338
- const hint = hasContent ? theme.ui.muted('(ctrl+o to show thinking)') : '';
339
- return `${symbol} Thought for ${duration}${hint ? ` ${hint}` : ''}`;
340
- }
341
- /**
342
- * Format status line at bottom
343
- * Example: "• Ready for prompts (250ms)"
344
- */
345
- export function formatStatusLine(message, durationMs, tokenUsage) {
346
- const parts = [];
347
- // Add token usage first if available
348
- if (tokenUsage) {
349
- parts.push(formatTokenUsage(tokenUsage.percentage, tokenUsage.tokens));
350
- }
351
- // Add message with duration
352
- const duration = durationMs ? ` (${formatDuration(durationMs)})` : '';
353
- parts.push(message + duration);
354
- return parts.join(' · ');
355
- }
356
- /**
357
- * Smart truncate for shell commands
358
- * Preserves important parts like command name and key flags
359
- */
360
- export function truncateCommand(command, maxLength = 60) {
361
- if (command.length <= maxLength)
362
- return command;
363
- // Try to keep command name and first few args
364
- const parts = command.split(/\s+/).filter(p => p);
365
- if (parts.length === 0 || !parts[0]) {
366
- return command.slice(0, maxLength - 3) + '...';
367
- }
368
- if (parts.length === 1) {
369
- // Single long command/path
370
- return command.slice(0, maxLength - 3) + '...';
371
- }
372
- // Keep first part (command) and truncate rest
373
- const cmdName = parts[0];
374
- const rest = parts.slice(1).join(' ');
375
- if (cmdName.length + 7 > maxLength) {
376
- return cmdName.slice(0, maxLength - 3) + '...';
377
- }
378
- const availableSpace = maxLength - cmdName.length - 4; // -4 for " ..."
379
- if (rest.length <= availableSpace) {
380
- return command;
381
- }
382
- return `${cmdName} ${rest.slice(0, availableSpace)}...`;
383
- }
384
- /**
385
- * Format file size in human-readable format
386
- */
387
- export function formatFileSize(bytes) {
388
- if (bytes < 1024)
389
- return `${bytes}B`;
390
- if (bytes < 1024 * 1024)
391
- return `${(bytes / 1024).toFixed(1)}KB`;
392
- if (bytes < 1024 * 1024 * 1024)
393
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
394
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
395
- }
396
- /**
397
- * Format JSON output with syntax highlighting
398
- */
399
- export function formatJSON(data, compact = false) {
400
- const json = JSON.stringify(data, null, compact ? 0 : 2);
401
- // Add basic syntax highlighting
402
- return json
403
- .replace(/"([^"]+)":/g, (_, key) => `${theme.info(`"${key}"`)}: `) // Keys in blue
404
- .replace(/: "([^"]*)"/g, (_, value) => `: ${theme.success(`"${value}"`)}`) // String values in green
405
- .replace(/: (\d+\.?\d*)/g, (_, num) => `: ${theme.warning(num)}`) // Numbers in amber
406
- .replace(/: (true|false|null)/g, (_, bool) => `: ${theme.secondary(bool)}`); // Booleans in purple
407
- }
408
- /**
409
- * Format a list of items (Claude Code style)
410
- * Example:
411
- * • Item 1
412
- * • Item 2
413
- * • Item 3
414
- */
415
- export function formatList(items, bullet = '•') {
416
- return items.map(item => ` ${theme.info(bullet)} ${item}`).join('\n');
417
- }
418
- /**
419
- * Format a key-value pair (Claude Code style)
420
- */
421
- export function formatKeyValue(key, value) {
422
- return `${theme.ui.muted(key)}: ${value}`;
423
- }