sona-code 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/dist/cli.js ADDED
@@ -0,0 +1,2979 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * SONA CODE
5
+ *
6
+ * AI coding assistant with 100x cost reduction.
7
+ * Full CLI capabilities powered by DeepSeek + SMR compression.
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ const commander_1 = require("commander");
44
+ const fs_1 = require("fs");
45
+ const readline_1 = require("readline");
46
+ const child_process_1 = require("child_process");
47
+ const compressor_js_1 = require("./compressor.js");
48
+ const proxy_js_1 = require("./proxy.js");
49
+ const session_js_1 = require("./session.js");
50
+ const rules_js_1 = require("./rules.js");
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const VERSION = '0.1.0';
54
+ const APP_NAME = 'SONA CODE';
55
+ // Config file path for persistent settings
56
+ const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.sona');
57
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
58
+ // Default configuration
59
+ const DEFAULT_CONFIG = {
60
+ provider: 'deepseek',
61
+ model: 'deepseek-chat',
62
+ apiKey: '',
63
+ pricePerMillion: 0.14, // DeepSeek is much cheaper!
64
+ };
65
+ /**
66
+ * Load configuration from file
67
+ */
68
+ function loadConfig() {
69
+ try {
70
+ if (fs.existsSync(CONFIG_FILE)) {
71
+ const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
72
+ return { ...DEFAULT_CONFIG, ...JSON.parse(data) };
73
+ }
74
+ }
75
+ catch {
76
+ // Ignore errors, use default
77
+ }
78
+ return { ...DEFAULT_CONFIG };
79
+ }
80
+ /**
81
+ * Save configuration to file
82
+ */
83
+ function saveConfig(config) {
84
+ try {
85
+ if (!fs.existsSync(CONFIG_DIR)) {
86
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
87
+ }
88
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
89
+ }
90
+ catch (error) {
91
+ console.error('Failed to save config:', error);
92
+ }
93
+ }
94
+ const MEMORY_VERSION = 2;
95
+ let pendingSave = null;
96
+ let currentMemory = null;
97
+ /**
98
+ * Get the memory file path for current workspace
99
+ */
100
+ function getHistoryPath() {
101
+ const workspaceDir = path.join(process.cwd(), '.sona');
102
+ return path.join(workspaceDir, 'memory.json');
103
+ }
104
+ /**
105
+ * Initialize empty workspace memory
106
+ */
107
+ function createEmptyMemory() {
108
+ return {
109
+ version: MEMORY_VERSION,
110
+ workspace: process.cwd(),
111
+ lastUpdated: new Date().toISOString(),
112
+ currentTask: null,
113
+ taskHistory: [],
114
+ filesRead: [],
115
+ filesWritten: [],
116
+ filesCreated: [],
117
+ commandsRun: [],
118
+ codebaseNotes: [],
119
+ recentExchanges: [],
120
+ stats: {
121
+ totalSessions: 1,
122
+ totalExchanges: 0,
123
+ firstSession: new Date().toISOString()
124
+ }
125
+ };
126
+ }
127
+ /**
128
+ * Load workspace memory
129
+ */
130
+ function loadHistory() {
131
+ try {
132
+ const memoryPath = getHistoryPath();
133
+ if (fs.existsSync(memoryPath)) {
134
+ const data = fs.readFileSync(memoryPath, 'utf-8');
135
+ const memory = JSON.parse(data);
136
+ if (memory.version === MEMORY_VERSION) {
137
+ memory.stats.totalSessions++;
138
+ currentMemory = memory;
139
+ return memory;
140
+ }
141
+ }
142
+ }
143
+ catch {
144
+ // Ignore errors - corrupted file will be overwritten
145
+ }
146
+ currentMemory = createEmptyMemory();
147
+ return null;
148
+ }
149
+ /**
150
+ * Track a file operation
151
+ */
152
+ function trackFileOp(op, filePath) {
153
+ if (!currentMemory)
154
+ currentMemory = createEmptyMemory();
155
+ const relativePath = filePath.startsWith(process.cwd())
156
+ ? filePath.slice(process.cwd().length + 1)
157
+ : filePath;
158
+ const list = op === 'read' ? currentMemory.filesRead
159
+ : op === 'write' ? currentMemory.filesWritten
160
+ : currentMemory.filesCreated;
161
+ if (!list.includes(relativePath)) {
162
+ list.push(relativePath);
163
+ // Keep lists bounded
164
+ if (list.length > 50)
165
+ list.shift();
166
+ }
167
+ }
168
+ /**
169
+ * Track a command execution
170
+ */
171
+ function trackCommand(cmd, success) {
172
+ if (!currentMemory)
173
+ currentMemory = createEmptyMemory();
174
+ currentMemory.commandsRun.push({
175
+ cmd: cmd.slice(0, 200),
176
+ success,
177
+ timestamp: new Date().toISOString()
178
+ });
179
+ // Keep last 30 commands
180
+ if (currentMemory.commandsRun.length > 30) {
181
+ currentMemory.commandsRun.shift();
182
+ }
183
+ }
184
+ /**
185
+ * Update current task
186
+ */
187
+ function updateTask(task) {
188
+ if (!currentMemory)
189
+ currentMemory = createEmptyMemory();
190
+ if (currentMemory.currentTask && currentMemory.currentTask !== task) {
191
+ currentMemory.taskHistory.push(currentMemory.currentTask);
192
+ if (currentMemory.taskHistory.length > 10) {
193
+ currentMemory.taskHistory.shift();
194
+ }
195
+ }
196
+ currentMemory.currentTask = task.slice(0, 500);
197
+ }
198
+ /**
199
+ * Add a codebase discovery note
200
+ */
201
+ function addCodebaseNote(note) {
202
+ if (!currentMemory)
203
+ currentMemory = createEmptyMemory();
204
+ if (!currentMemory.codebaseNotes.includes(note)) {
205
+ currentMemory.codebaseNotes.push(note.slice(0, 300));
206
+ if (currentMemory.codebaseNotes.length > 20) {
207
+ currentMemory.codebaseNotes.shift();
208
+ }
209
+ }
210
+ }
211
+ /**
212
+ * Debounced save - batches rapid writes into single file operation
213
+ */
214
+ function saveHistory(messages, existingHistory) {
215
+ if (pendingSave)
216
+ clearTimeout(pendingSave);
217
+ pendingSave = setTimeout(() => {
218
+ try {
219
+ const workspaceDir = path.join(process.cwd(), '.sona');
220
+ if (!fs.existsSync(workspaceDir)) {
221
+ fs.mkdirSync(workspaceDir, { recursive: true });
222
+ }
223
+ if (!currentMemory)
224
+ currentMemory = existingHistory || createEmptyMemory();
225
+ // Extract recent exchanges
226
+ const filtered = messages.filter(m => m.role === 'user' || m.role === 'assistant');
227
+ const pairs = [];
228
+ for (let i = 0; i < filtered.length - 1; i += 2) {
229
+ if (filtered[i]?.role === 'user' && filtered[i + 1]?.role === 'assistant') {
230
+ pairs.push({
231
+ user: filtered[i].content.slice(0, 2000),
232
+ assistant: filtered[i + 1].content.slice(0, 3000),
233
+ timestamp: new Date().toISOString()
234
+ });
235
+ }
236
+ }
237
+ // Keep last 10 exchanges - enough for full context
238
+ currentMemory.recentExchanges = pairs.slice(-10);
239
+ currentMemory.lastUpdated = new Date().toISOString();
240
+ currentMemory.stats.totalExchanges = (existingHistory?.stats.totalExchanges || 0) + pairs.length;
241
+ // Try to extract task from first user message
242
+ if (pairs.length > 0 && !currentMemory.currentTask) {
243
+ const firstMsg = pairs[0].user;
244
+ if (firstMsg.length > 10) {
245
+ currentMemory.currentTask = firstMsg.slice(0, 200);
246
+ }
247
+ }
248
+ // Atomic write
249
+ const tempPath = getHistoryPath() + '.tmp';
250
+ fs.writeFileSync(tempPath, JSON.stringify(currentMemory, null, 2));
251
+ fs.renameSync(tempPath, getHistoryPath());
252
+ }
253
+ catch {
254
+ // Silently fail
255
+ }
256
+ }, 2000);
257
+ }
258
+ /**
259
+ * Build rich context from workspace memory
260
+ */
261
+ function buildHistoryContext(memory) {
262
+ if (!memory)
263
+ return '';
264
+ const parts = [];
265
+ parts.push('## Workspace Memory (from previous sessions)');
266
+ // Current/recent tasks - most important
267
+ if (memory.currentTask) {
268
+ parts.push(`\n**Last task:** ${memory.currentTask}`);
269
+ }
270
+ if (memory.taskHistory.length > 0) {
271
+ parts.push(`**Previous tasks:** ${memory.taskHistory.slice(-3).join(' → ')}`);
272
+ }
273
+ // Files touched - crucial for continuation
274
+ if (memory.filesWritten.length > 0) {
275
+ parts.push(`\n**Files modified:** ${memory.filesWritten.slice(-15).join(', ')}`);
276
+ }
277
+ if (memory.filesCreated.length > 0) {
278
+ parts.push(`**Files created:** ${memory.filesCreated.slice(-10).join(', ')}`);
279
+ }
280
+ if (memory.filesRead.length > 0) {
281
+ parts.push(`**Files explored:** ${memory.filesRead.slice(-15).join(', ')}`);
282
+ }
283
+ // Recent commands
284
+ if (memory.commandsRun.length > 0) {
285
+ const recentCmds = memory.commandsRun.slice(-5).map(c => `${c.success ? '✓' : '✗'} ${c.cmd}`);
286
+ parts.push(`\n**Recent commands:**\n${recentCmds.join('\n')}`);
287
+ }
288
+ // Codebase notes
289
+ if (memory.codebaseNotes.length > 0) {
290
+ parts.push(`\n**Codebase notes:**\n- ${memory.codebaseNotes.join('\n- ')}`);
291
+ }
292
+ // Recent conversation
293
+ if (memory.recentExchanges.length > 0) {
294
+ parts.push('\n**Recent conversation:**');
295
+ memory.recentExchanges.slice(-5).forEach(ex => {
296
+ parts.push(`User: ${ex.user.slice(0, 300)}${ex.user.length > 300 ? '...' : ''}`);
297
+ parts.push(`You: ${ex.assistant.slice(0, 400)}${ex.assistant.length > 400 ? '...' : ''}`);
298
+ });
299
+ }
300
+ // Stats
301
+ parts.push(`\n(Session ${memory.stats.totalSessions}, ${memory.stats.totalExchanges} total exchanges since ${memory.stats.firstSession.split('T')[0]})`);
302
+ return parts.join('\n');
303
+ }
304
+ // ═══════════════════════════════════════════════════════════════════════════
305
+ // SONA CODE - Retro Terminal UI System
306
+ // Broken white theme with pixel-art ASCII aesthetic
307
+ // ═══════════════════════════════════════════════════════════════════════════
308
+ // Get terminal width for responsive layout
309
+ function getTerminalWidth() {
310
+ return process.stdout.columns || 80;
311
+ }
312
+ function isWideTerminal() {
313
+ return getTerminalWidth() >= 100;
314
+ }
315
+ // ASCII Art logo - pixel/retro style (simulates Press Start 2P at 40px)
316
+ const SONA_LOGO = `
317
+ ███████╗ ██████╗ ███╗ ██╗ █████╗
318
+ ██╔════╝██╔═══██╗████╗ ██║██╔══██╗
319
+ ███████╗██║ ██║██╔██╗ ██║███████║
320
+ ╚════██║██║ ██║██║╚██╗██║██╔══██║
321
+ ███████║╚██████╔╝██║ ╚████║██║ ██║
322
+ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝`;
323
+ // ANSI color codes - Broken white/gray theme
324
+ const colors = {
325
+ // Primary - broken white (off-white, warm)
326
+ brokenWhite: (s) => `\x1b[38;5;253m${s}\x1b[0m`, // #dadada - broken white
327
+ warmWhite: (s) => `\x1b[38;5;230m${s}\x1b[0m`, // Warm off-white
328
+ // Grays (tailwind-inspired)
329
+ gray200: (s) => `\x1b[38;5;252m${s}\x1b[0m`, // Light gray - lines
330
+ gray400: (s) => `\x1b[38;5;246m${s}\x1b[0m`, // Medium gray
331
+ gray500: (s) => `\x1b[38;5;244m${s}\x1b[0m`, // Gray
332
+ gray600: (s) => `\x1b[38;5;242m${s}\x1b[0m`, // Darker gray
333
+ gray700: (s) => `\x1b[38;5;240m${s}\x1b[0m`, // thinking text
334
+ gray800: (s) => `\x1b[38;5;238m${s}\x1b[0m`, // Very dark
335
+ // Accent colors (muted)
336
+ cyan: (s) => `\x1b[38;5;117m${s}\x1b[0m`, // Soft cyan
337
+ green: (s) => `\x1b[38;5;114m${s}\x1b[0m`, // Soft green
338
+ red: (s) => `\x1b[38;5;174m${s}\x1b[0m`, // Soft red
339
+ yellow: (s) => `\x1b[38;5;186m${s}\x1b[0m`, // Soft yellow
340
+ blue: (s) => `\x1b[38;5;110m${s}\x1b[0m`, // Soft blue
341
+ magenta: (s) => `\x1b[38;5;182m${s}\x1b[0m`, // Soft magenta
342
+ orange: (s) => `\x1b[38;5;216m${s}\x1b[0m`, // Soft orange
343
+ // Legacy aliases
344
+ white: (s) => `\x1b[38;5;253m${s}\x1b[0m`,
345
+ lightGray: (s) => `\x1b[38;5;252m${s}\x1b[0m`,
346
+ gray: (s) => `\x1b[38;5;244m${s}\x1b[0m`,
347
+ dimGray: (s) => `\x1b[38;5;240m${s}\x1b[0m`,
348
+ darkGray: (s) => `\x1b[38;5;236m${s}\x1b[0m`,
349
+ // Text styles
350
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
351
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
352
+ italic: (s) => `\x1b[3m${s}\x1b[0m`,
353
+ underline: (s) => `\x1b[4m${s}\x1b[0m`,
354
+ // Semantic styles - SONA theme
355
+ title: (s) => `\x1b[1m\x1b[38;5;253m${s}\x1b[0m`, // Bold broken white - headers
356
+ subtitle: (s) => `\x1b[38;5;250m${s}\x1b[0m`, // Lighter
357
+ body: (s) => `\x1b[38;5;252m${s}\x1b[0m`, // Gray 200 - main text
358
+ small: (s) => `\x1b[38;5;244m${s}\x1b[0m`, // Gray 500 - small text
359
+ muted: (s) => `\x1b[38;5;240m${s}\x1b[0m`, // Gray 700 - muted
360
+ accent: (s) => `\x1b[38;5;253m${s}\x1b[0m`, // Broken white accent
361
+ success: (s) => `\x1b[38;5;114m${s}\x1b[0m`, // Soft green
362
+ warning: (s) => `\x1b[38;5;186m${s}\x1b[0m`, // Soft yellow
363
+ error: (s) => `\x1b[38;5;174m${s}\x1b[0m`, // Soft red
364
+ info: (s) => `\x1b[38;5;110m${s}\x1b[0m`, // Soft blue
365
+ highlight: (s) => `\x1b[48;5;236m\x1b[38;5;253m${s}\x1b[0m`,
366
+ // File types
367
+ folder: (s) => `\x1b[38;5;110m${s}\x1b[0m`,
368
+ file: (s) => `\x1b[38;5;252m${s}\x1b[0m`,
369
+ exec: (s) => `\x1b[38;5;114m${s}\x1b[0m`,
370
+ link: (s) => `\x1b[38;5;117m${s}\x1b[0m`,
371
+ };
372
+ // Unicode characters for clean UI
373
+ const ui = {
374
+ // Box drawing (clean lines)
375
+ topLeft: '┌',
376
+ topRight: '┐',
377
+ bottomLeft: '└',
378
+ bottomRight: '┘',
379
+ horizontal: '─',
380
+ vertical: '│',
381
+ leftT: '├',
382
+ rightT: '┤',
383
+ topT: '┬',
384
+ bottomT: '┴',
385
+ crossBox: '┼',
386
+ // Double lines for emphasis
387
+ dHorizontal: '═',
388
+ dVertical: '║',
389
+ // Icons (minimal)
390
+ bullet: '•',
391
+ arrow: '→',
392
+ arrowRight: '▸',
393
+ arrowDown: '▾',
394
+ star: '★',
395
+ dot: '·',
396
+ diamond: '◆',
397
+ circle: '○',
398
+ circleFilled: '●',
399
+ square: '□',
400
+ // Spinners
401
+ spinner: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
402
+ thinking: ['◐', '◓', '◑', '◒'],
403
+ // Status
404
+ success: '✓',
405
+ error: '✗',
406
+ warning: '⚠',
407
+ info: 'ℹ',
408
+ check: '✓',
409
+ cross: '✗',
410
+ // File icons (simple)
411
+ folderIcon: '▸',
412
+ fileIcon: '·',
413
+ git: '⎇',
414
+ chart: '📊',
415
+ money: '💰',
416
+ };
417
+ // Formatting helpers
418
+ const fmt = {
419
+ // Create a horizontal line
420
+ line: (width = 60) => ui.horizontal.repeat(width),
421
+ // Create a box around text
422
+ box: (title, content, width = 56) => {
423
+ const lines = [];
424
+ const innerWidth = width - 4;
425
+ // Top border with title
426
+ const titlePad = Math.max(0, innerWidth - title.length - 2);
427
+ lines.push(colors.accent(ui.topLeft + ui.horizontal + ' ') + colors.title(title) + ' ' + colors.accent(ui.horizontal.repeat(titlePad) + ui.topRight));
428
+ // Content lines
429
+ for (const line of content) {
430
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, ''); // Strip ANSI for length calc
431
+ const padding = Math.max(0, innerWidth - stripped.length);
432
+ lines.push(colors.accent(ui.vertical) + ' ' + line + ' '.repeat(padding) + ' ' + colors.accent(ui.vertical));
433
+ }
434
+ // Bottom border
435
+ lines.push(colors.accent(ui.bottomLeft + ui.horizontal.repeat(width - 2) + ui.bottomRight));
436
+ return lines.join('\n');
437
+ },
438
+ // Indent text
439
+ indent: (s, spaces = 2) => ' '.repeat(spaces) + s,
440
+ // Format a key-value pair
441
+ kv: (key, value, keyWidth = 14) => {
442
+ return colors.muted(key.padEnd(keyWidth)) + colors.body(value);
443
+ },
444
+ // Format a list item
445
+ li: (text, indent = 0) => {
446
+ return ' '.repeat(indent) + colors.accent(ui.bullet) + ' ' + colors.body(text);
447
+ },
448
+ // Format numbered list item
449
+ num: (n, text, indent = 0) => {
450
+ return ' '.repeat(indent) + colors.accent(`${n}.`) + ' ' + colors.body(text);
451
+ },
452
+ // Section header
453
+ section: (title) => {
454
+ return '\n' + colors.title(title) + '\n' + colors.dimGray(ui.horizontal.repeat(title.length));
455
+ },
456
+ // Subtle divider
457
+ divider: () => colors.dimGray(' ' + ui.dot.repeat(40)),
458
+ };
459
+ // Response formatter - broken white theme, reduced line spacing
460
+ function formatResponse(text) {
461
+ const lines = text.split('\n');
462
+ const formatted = [];
463
+ let inCodeBlock = false;
464
+ let inList = false;
465
+ for (let i = 0; i < lines.length; i++) {
466
+ let line = lines[i];
467
+ // Code blocks - gray 500
468
+ if (line.startsWith('```')) {
469
+ inCodeBlock = !inCodeBlock;
470
+ formatted.push(colors.gray600(' ' + line));
471
+ continue;
472
+ }
473
+ if (inCodeBlock) {
474
+ formatted.push(colors.gray400(' ' + line));
475
+ continue;
476
+ }
477
+ // Headers - broken white bold
478
+ if (line.startsWith('### ')) {
479
+ formatted.push(colors.brokenWhite(' ' + line.slice(4)));
480
+ continue;
481
+ }
482
+ if (line.startsWith('## ')) {
483
+ formatted.push(colors.bold(colors.brokenWhite(' ' + line.slice(3))));
484
+ continue;
485
+ }
486
+ if (line.startsWith('# ')) {
487
+ formatted.push(colors.bold(colors.brokenWhite(' ' + line.slice(2))));
488
+ continue;
489
+ }
490
+ // Numbered lists with bold (1. **Item**)
491
+ const numMatch = line.match(/^(\d+)\.\s+\*\*(.+?)\*\*:?\s*(.*)/);
492
+ if (numMatch) {
493
+ inList = true;
494
+ const [, num, boldPart, rest] = numMatch;
495
+ formatted.push(` ${colors.gray500(num + '.')} ${colors.brokenWhite(boldPart)}${rest ? ' ' + colors.gray200(rest) : ''}`);
496
+ continue;
497
+ }
498
+ // Simple numbered lists
499
+ const simpleNumMatch = line.match(/^(\d+)\.\s+(.*)/);
500
+ if (simpleNumMatch) {
501
+ inList = true;
502
+ const [, num, content] = simpleNumMatch;
503
+ formatted.push(` ${colors.gray500(num + '.')} ${colors.gray200(content)}`);
504
+ continue;
505
+ }
506
+ // Bullet points
507
+ if (line.match(/^[-*]\s+/)) {
508
+ inList = true;
509
+ const content = line.replace(/^[-*]\s+/, '');
510
+ formatted.push(` ${colors.gray500('·')} ${colors.gray200(content)}`);
511
+ continue;
512
+ }
513
+ // Sub-bullets
514
+ if (line.match(/^\s+[-*]\s+/)) {
515
+ const content = line.replace(/^\s+[-*]\s+/, '');
516
+ formatted.push(` ${colors.gray600('·')} ${colors.gray400(content)}`);
517
+ continue;
518
+ }
519
+ // Empty line resets list state - reduced spacing
520
+ if (line.trim() === '') {
521
+ inList = false;
522
+ // Only add empty line if not consecutive
523
+ if (formatted.length > 0 && formatted[formatted.length - 1] !== '') {
524
+ formatted.push('');
525
+ }
526
+ continue;
527
+ }
528
+ // Bold text **text**
529
+ line = line.replace(/\*\*(.+?)\*\*/g, (_, text) => colors.brokenWhite(text));
530
+ // Inline code `code`
531
+ line = line.replace(/`([^`]+)`/g, (_, code) => colors.gray400(code));
532
+ // Regular paragraph - gray 200
533
+ formatted.push(' ' + colors.gray200(line));
534
+ }
535
+ return formatted.join('\n');
536
+ }
537
+ const program = new commander_1.Command();
538
+ program
539
+ .name('sona')
540
+ .description('SONA CODE - AI coding assistant with 100x cost reduction')
541
+ .version(VERSION)
542
+ .option('--deepseek-key <key>', 'DeepSeek API key')
543
+ .option('--openai-key <key>', 'OpenAI API key')
544
+ .option('--anthropic-key <key>', 'Anthropic API key')
545
+ .option('-m, --model <model>', 'Model to use (deepseek-chat, gpt-4o-mini, etc.)')
546
+ .option('--provider <provider>', 'Provider: deepseek, openai, or anthropic')
547
+ .option('-s, --system <prompt>', 'System prompt')
548
+ .option('--no-compress', 'Disable compression')
549
+ .action(async (opts) => {
550
+ // Default action: start chat
551
+ await startChat(opts);
552
+ });
553
+ // ============================================================
554
+ // PROXY COMMANDS
555
+ // ============================================================
556
+ /**
557
+ * Start proxy server
558
+ */
559
+ program
560
+ .command('start')
561
+ .alias('proxy')
562
+ .description('Start the transparent proxy server')
563
+ .option('-p, --port <number>', 'Port to listen on', '8787')
564
+ .option('-v, --verbose', 'Show request logs')
565
+ .option('--openai-key <key>', 'OpenAI API key')
566
+ .option('--anthropic-key <key>', 'Anthropic API key')
567
+ .option('--openai-url <url>', 'OpenAI API base URL', 'https://api.openai.com')
568
+ .option('--anthropic-url <url>', 'Anthropic API base URL', 'https://api.anthropic.com')
569
+ .option('--price <number>', 'Price per million tokens for cost estimation', '15')
570
+ .action(async (opts) => {
571
+ const port = parseInt(opts.port);
572
+ console.log('');
573
+ console.log(colors.cyan('═'.repeat(60)));
574
+ console.log(colors.bold(' SONA CODE Proxy'));
575
+ console.log(colors.cyan('═'.repeat(60)));
576
+ console.log('');
577
+ // Check for API keys
578
+ let openaiKey = opts.openaiKey || process.env.OPENAI_API_KEY;
579
+ let anthropicKey = opts.anthropicKey || process.env.ANTHROPIC_API_KEY;
580
+ // If no keys provided, prompt user
581
+ if (!openaiKey && !anthropicKey) {
582
+ console.log(colors.yellow(' No API keys found. Please provide at least one:'));
583
+ console.log('');
584
+ const rl = (0, readline_1.createInterface)({
585
+ input: process.stdin,
586
+ output: process.stdout,
587
+ });
588
+ const question = (prompt) => {
589
+ return new Promise((resolve) => {
590
+ rl.question(prompt, (answer) => {
591
+ resolve(answer.trim());
592
+ });
593
+ });
594
+ };
595
+ console.log(colors.gray(' Press Enter to skip if you don\'t have a key.'));
596
+ console.log('');
597
+ openaiKey = await question(colors.cyan(' OpenAI API Key: '));
598
+ anthropicKey = await question(colors.cyan(' Anthropic API Key: '));
599
+ rl.close();
600
+ console.log('');
601
+ if (!openaiKey && !anthropicKey) {
602
+ console.log(colors.red(' Error: At least one API key is required.'));
603
+ console.log('');
604
+ console.log(' You can also set environment variables:');
605
+ console.log(colors.dim(' export OPENAI_API_KEY=sk-...'));
606
+ console.log(colors.dim(' export ANTHROPIC_API_KEY=sk-ant-...'));
607
+ console.log('');
608
+ console.log(' Or pass keys directly:');
609
+ console.log(colors.dim(' sona start --openai-key sk-... --anthropic-key sk-ant-...'));
610
+ console.log('');
611
+ process.exit(1);
612
+ }
613
+ }
614
+ // Show which APIs are configured
615
+ console.log(colors.bold(' API Configuration:'));
616
+ if (openaiKey) {
617
+ console.log(` ${colors.green('✓')} OpenAI: ${colors.green('configured')} ${colors.gray(`(${maskKey(openaiKey)})`)}`);
618
+ }
619
+ else {
620
+ console.log(` ${colors.gray('○')} OpenAI: ${colors.gray('not configured')}`);
621
+ }
622
+ if (anthropicKey) {
623
+ console.log(` ${colors.green('✓')} Anthropic: ${colors.green('configured')} ${colors.gray(`(${maskKey(anthropicKey)})`)}`);
624
+ }
625
+ else {
626
+ console.log(` ${colors.gray('○')} Anthropic: ${colors.gray('not configured')}`);
627
+ }
628
+ console.log('');
629
+ try {
630
+ const proxy = await (0, proxy_js_1.startProxy)({
631
+ port,
632
+ verbose: opts.verbose,
633
+ openaiBaseUrl: opts.openaiUrl,
634
+ anthropicBaseUrl: opts.anthropicUrl,
635
+ pricePerMillionTokens: parseFloat(opts.price),
636
+ openaiApiKey: openaiKey,
637
+ anthropicApiKey: anthropicKey,
638
+ });
639
+ console.log(` ${colors.green('●')} Proxy running on ${colors.bold(`http://localhost:${port}`)}`);
640
+ console.log('');
641
+ console.log(colors.bold(' How to use:'));
642
+ console.log('');
643
+ console.log(' ' + colors.cyan('Option 1:') + ' Set environment variables');
644
+ console.log(colors.dim(' export OPENAI_BASE_URL=http://localhost:' + port));
645
+ console.log(colors.dim(' export ANTHROPIC_BASE_URL=http://localhost:' + port));
646
+ console.log('');
647
+ console.log(' ' + colors.cyan('Option 2:') + ' Use one-liner');
648
+ console.log(colors.dim(` eval $(sona env -p ${port})`));
649
+ console.log('');
650
+ console.log(' ' + colors.cyan('Option 3:') + ' Wrap commands directly');
651
+ console.log(colors.dim(' sona wrap codex "your prompt"'));
652
+ console.log('');
653
+ console.log(colors.bold(' Dashboard: ') + colors.blue(`http://localhost:${port}`));
654
+ console.log('');
655
+ console.log(colors.gray(' Press Ctrl+C to stop'));
656
+ console.log('');
657
+ console.log(colors.cyan('═'.repeat(60)));
658
+ console.log('');
659
+ // Handle shutdown
660
+ process.on('SIGINT', async () => {
661
+ console.log('');
662
+ console.log(colors.yellow(' Shutting down...'));
663
+ const stats = proxy.getStats();
664
+ if (stats.totalRequests > 0) {
665
+ console.log('');
666
+ console.log(colors.bold(' Session Summary:'));
667
+ console.log(` Requests: ${stats.totalRequests}`);
668
+ console.log(` Tokens saved: ${colors.green(stats.totalTokensSaved.toLocaleString())}`);
669
+ console.log(` Cost saved: ${colors.green('$' + stats.estimatedCostSaved.toFixed(4))}`);
670
+ }
671
+ await proxy.stop();
672
+ console.log('');
673
+ process.exit(0);
674
+ });
675
+ }
676
+ catch (error) {
677
+ console.error(colors.red(` Error: ${error.message}`));
678
+ process.exit(1);
679
+ }
680
+ });
681
+ /**
682
+ * Mask API key for display
683
+ */
684
+ function maskKey(key) {
685
+ if (key.length < 10)
686
+ return '***';
687
+ return key.substring(0, 7) + '...' + key.substring(key.length - 4);
688
+ }
689
+ /**
690
+ * Output environment variables for shell eval
691
+ */
692
+ program
693
+ .command('env')
694
+ .description('Output environment variables (use with eval)')
695
+ .option('-p, --port <number>', 'Proxy port', '8787')
696
+ .option('--fish', 'Output for fish shell')
697
+ .option('--powershell', 'Output for PowerShell')
698
+ .action((opts) => {
699
+ const port = opts.port;
700
+ if (opts.fish) {
701
+ console.log(`set -gx OPENAI_BASE_URL http://localhost:${port}`);
702
+ console.log(`set -gx ANTHROPIC_BASE_URL http://localhost:${port}`);
703
+ }
704
+ else if (opts.powershell) {
705
+ console.log(`$env:OPENAI_BASE_URL = "http://localhost:${port}"`);
706
+ console.log(`$env:ANTHROPIC_BASE_URL = "http://localhost:${port}"`);
707
+ }
708
+ else {
709
+ console.log(`export OPENAI_BASE_URL=http://localhost:${port}`);
710
+ console.log(`export ANTHROPIC_BASE_URL=http://localhost:${port}`);
711
+ }
712
+ });
713
+ /**
714
+ * One-line setup
715
+ */
716
+ program
717
+ .command('init')
718
+ .description('Initialize SONA CODE in current shell')
719
+ .option('-p, --port <number>', 'Proxy port', '8787')
720
+ .action(async (opts) => {
721
+ const port = parseInt(opts.port);
722
+ console.log('');
723
+ console.log(colors.cyan('SONA CODE - Quick Setup'));
724
+ console.log('');
725
+ console.log('Add this to your shell profile (~/.zshrc, ~/.bashrc):');
726
+ console.log('');
727
+ console.log(colors.gray(' # SONA CODE Proxy'));
728
+ console.log(colors.cyan(` alias sona-start='sona start -p ${port} &'`));
729
+ console.log(colors.cyan(` export OPENAI_BASE_URL=http://localhost:${port}`));
730
+ console.log(colors.cyan(` export ANTHROPIC_BASE_URL=http://localhost:${port}`));
731
+ console.log('');
732
+ console.log('Then restart your shell or run:');
733
+ console.log(colors.yellow(' source ~/.zshrc'));
734
+ console.log('');
735
+ console.log(colors.gray('After setup, run `sona start` and use your tools normally.'));
736
+ console.log('');
737
+ });
738
+ // ============================================================
739
+ // WRAP COMMAND - Run any command with compression
740
+ // ============================================================
741
+ /**
742
+ * Wrap a command with SONA compression
743
+ */
744
+ program
745
+ .command('wrap <command...>')
746
+ .description('Run a command with SONA compression enabled')
747
+ .option('-p, --port <number>', 'Proxy port (starts proxy if needed)', '8787')
748
+ .action(async (command, opts) => {
749
+ const port = parseInt(opts.port);
750
+ // Set environment variables for the child process
751
+ const env = {
752
+ ...process.env,
753
+ OPENAI_BASE_URL: `http://localhost:${port}`,
754
+ ANTHROPIC_BASE_URL: `http://localhost:${port}`,
755
+ };
756
+ // Check if proxy is running
757
+ try {
758
+ const response = await fetch(`http://localhost:${port}/_sona/health`);
759
+ if (!response.ok)
760
+ throw new Error('Proxy not healthy');
761
+ }
762
+ catch {
763
+ console.error(colors.yellow(`Starting SONA proxy on port ${port}...`));
764
+ // Start proxy in background
765
+ const proxyProcess = (0, child_process_1.spawn)('node', [process.argv[1], 'start', '-p', port.toString()], {
766
+ detached: true,
767
+ stdio: 'ignore',
768
+ env,
769
+ });
770
+ proxyProcess.unref();
771
+ // Wait for proxy to start
772
+ await new Promise(resolve => setTimeout(resolve, 1000));
773
+ }
774
+ // Run the wrapped command
775
+ const [cmd, ...args] = command;
776
+ const child = (0, child_process_1.spawn)(cmd, args, {
777
+ stdio: 'inherit',
778
+ env,
779
+ shell: true,
780
+ });
781
+ child.on('exit', (code) => {
782
+ process.exit(code || 0);
783
+ });
784
+ });
785
+ // ============================================================
786
+ // DOCUMENT PROCESSING COMMANDS
787
+ // ============================================================
788
+ /**
789
+ * Process command - Process documents for LLM with continuous compression
790
+ */
791
+ program
792
+ .command('process')
793
+ .description('Process documents for LLM with streaming compression')
794
+ .argument('<files...>', 'Input files to process')
795
+ .option('-o, --output <dir>', 'Output directory for compressed files')
796
+ .option('-c, --chunk-size <number>', 'Chunk size in characters', '4000')
797
+ .option('-p, --protect <terms...>', 'Terms to protect from compression')
798
+ .option('--json', 'Output as JSON with full metrics')
799
+ .option('-v, --verbose', 'Show detailed progress')
800
+ .action(async (files, opts) => {
801
+ const session = (0, session_js_1.createSession)({
802
+ protectedTerms: opts.protect || [],
803
+ verbose: opts.verbose,
804
+ });
805
+ const chunkSize = parseInt(opts.chunkSize);
806
+ const results = [];
807
+ console.log('');
808
+ console.log(colors.cyan('═'.repeat(60)));
809
+ console.log(colors.bold(' SONA CODE Document Processing'));
810
+ console.log(colors.cyan('═'.repeat(60)));
811
+ console.log('');
812
+ for (const file of files) {
813
+ if (!(0, fs_1.existsSync)(file)) {
814
+ console.error(colors.red(` ✗ File not found: ${file}`));
815
+ continue;
816
+ }
817
+ const fileName = path.basename(file);
818
+ console.log(` Processing: ${colors.cyan(fileName)}`);
819
+ session.startDocument(file, fileName);
820
+ const content = (0, fs_1.readFileSync)(file, 'utf-8');
821
+ const chunks = splitIntoChunks(content, chunkSize);
822
+ const compressedChunks = [];
823
+ for (let i = 0; i < chunks.length; i++) {
824
+ const result = session.compressChunk(chunks[i]);
825
+ compressedChunks.push(result.compressedText);
826
+ if (opts.verbose) {
827
+ process.stdout.write(`\r Chunk ${i + 1}/${chunks.length}: -${result.tokenSavings} tokens`);
828
+ }
829
+ }
830
+ if (opts.verbose) {
831
+ console.log('');
832
+ }
833
+ const docStats = session.endDocument();
834
+ if (docStats) {
835
+ results.push({
836
+ file: fileName,
837
+ originalTokens: docStats.originalTokens,
838
+ compressedTokens: docStats.compressedTokens,
839
+ tokensSaved: docStats.tokensSaved,
840
+ savingsPercent: docStats.savingsPercent,
841
+ chunks: docStats.chunksProcessed,
842
+ });
843
+ console.log(` ${colors.green('✓')} ${docStats.chunksProcessed} chunks, ${colors.green(`${docStats.tokensSaved} tokens saved`)} (${docStats.savingsPercent.toFixed(1)}%)`);
844
+ // Save compressed output if requested
845
+ if (opts.output) {
846
+ if (!(0, fs_1.existsSync)(opts.output)) {
847
+ fs.mkdirSync(opts.output, { recursive: true });
848
+ }
849
+ const outPath = path.join(opts.output, `compressed_${fileName}`);
850
+ (0, fs_1.writeFileSync)(outPath, compressedChunks.join('\n\n'));
851
+ console.log(` ${colors.gray(`→ Saved to ${outPath}`)}`);
852
+ }
853
+ }
854
+ }
855
+ console.log('');
856
+ console.log(colors.cyan('─'.repeat(60)));
857
+ const stats = session.getStats();
858
+ if (opts.json) {
859
+ console.log(JSON.stringify({
860
+ session: stats.sessionId,
861
+ files: results,
862
+ totals: {
863
+ originalTokens: stats.totalOriginalInputTokens,
864
+ compressedTokens: stats.totalCompressedInputTokens,
865
+ tokensSaved: stats.totalInputTokensSaved,
866
+ savingsPercent: stats.inputSavingsPercent,
867
+ estimatedCostSaved: stats.estimatedInputCostSaved,
868
+ },
869
+ }, null, 2));
870
+ }
871
+ else {
872
+ console.log(colors.bold(' Summary:'));
873
+ console.log(` Files processed: ${results.length}`);
874
+ console.log(` Total chunks: ${stats.documentChunksProcessed}`);
875
+ console.log(` Original tokens: ${stats.totalOriginalInputTokens.toLocaleString()}`);
876
+ console.log(` Compressed tokens: ${stats.totalCompressedInputTokens.toLocaleString()}`);
877
+ console.log(` ${colors.green(`Tokens saved: ${stats.totalInputTokensSaved.toLocaleString()} (${stats.inputSavingsPercent.toFixed(1)}%)`)}`);
878
+ console.log(` ${colors.yellow(`Est. cost saved: $${stats.estimatedInputCostSaved.toFixed(4)}`)}`);
879
+ }
880
+ console.log('');
881
+ console.log(colors.cyan('═'.repeat(60)));
882
+ console.log('');
883
+ });
884
+ /**
885
+ * Execute a shell command and return output
886
+ */
887
+ async function execCommand(cmd) {
888
+ const { exec } = await import('child_process');
889
+ return new Promise((resolve) => {
890
+ exec(cmd, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
891
+ resolve({
892
+ stdout: stdout.toString(),
893
+ stderr: stderr.toString(),
894
+ code: error?.code || 0,
895
+ });
896
+ });
897
+ });
898
+ }
899
+ const fileCache = new Map();
900
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
901
+ function getCachedFile(filePath) {
902
+ const entry = fileCache.get(filePath);
903
+ if (!entry)
904
+ return null;
905
+ // Check if file was modified
906
+ try {
907
+ const stats = fs.statSync(filePath);
908
+ if (stats.mtimeMs > entry.mtime) {
909
+ fileCache.delete(filePath);
910
+ return null;
911
+ }
912
+ }
913
+ catch {
914
+ return null;
915
+ }
916
+ // Check TTL
917
+ if (Date.now() - entry.lastAccess > CACHE_TTL) {
918
+ fileCache.delete(filePath);
919
+ return null;
920
+ }
921
+ entry.accessCount++;
922
+ entry.lastAccess = Date.now();
923
+ return entry.content;
924
+ }
925
+ function setCachedFile(filePath, content) {
926
+ try {
927
+ const stats = fs.statSync(filePath);
928
+ fileCache.set(filePath, {
929
+ content,
930
+ mtime: stats.mtimeMs,
931
+ accessCount: 1,
932
+ lastAccess: Date.now(),
933
+ });
934
+ }
935
+ catch {
936
+ // Ignore cache errors
937
+ }
938
+ }
939
+ function invalidateCache(filePath) {
940
+ fileCache.delete(filePath);
941
+ }
942
+ // ═══════════════════════════════════════════════════════════════════════════
943
+ // TOOLS - Claude Code-like capabilities
944
+ // ═══════════════════════════════════════════════════════════════════════════
945
+ const TOOLS = [
946
+ // ─────────────────────────────────────────────────────────────────────────
947
+ // FILE SYSTEM OPERATIONS
948
+ // ─────────────────────────────────────────────────────────────────────────
949
+ {
950
+ type: 'function',
951
+ function: {
952
+ name: 'read_file',
953
+ description: 'Read a file from the filesystem. Supports reading specific line ranges for large files to save tokens.',
954
+ parameters: {
955
+ type: 'object',
956
+ properties: {
957
+ path: { type: 'string', description: 'Path to the file to read' },
958
+ start_line: { type: 'number', description: 'Start reading from this line (1-indexed). Omit to read from beginning.' },
959
+ end_line: { type: 'number', description: 'Stop reading at this line (inclusive). Omit to read to end.' },
960
+ },
961
+ required: ['path'],
962
+ },
963
+ },
964
+ },
965
+ {
966
+ type: 'function',
967
+ function: {
968
+ name: 'write_file',
969
+ description: 'Write content to a file. Creates parent directories if needed.',
970
+ parameters: {
971
+ type: 'object',
972
+ properties: {
973
+ path: { type: 'string', description: 'Path to the file to write' },
974
+ content: { type: 'string', description: 'Content to write to the file' },
975
+ },
976
+ required: ['path', 'content'],
977
+ },
978
+ },
979
+ },
980
+ {
981
+ type: 'function',
982
+ function: {
983
+ name: 'edit_file',
984
+ description: 'Make surgical edits to a file using search and replace. More efficient than rewriting entire file.',
985
+ parameters: {
986
+ type: 'object',
987
+ properties: {
988
+ path: { type: 'string', description: 'Path to the file to edit' },
989
+ search: { type: 'string', description: 'Exact text to search for (must be unique in file)' },
990
+ replace: { type: 'string', description: 'Text to replace with' },
991
+ },
992
+ required: ['path', 'search', 'replace'],
993
+ },
994
+ },
995
+ },
996
+ {
997
+ type: 'function',
998
+ function: {
999
+ name: 'delete_file',
1000
+ description: 'Delete a file or empty directory from the filesystem.',
1001
+ parameters: {
1002
+ type: 'object',
1003
+ properties: {
1004
+ path: { type: 'string', description: 'Path to file or directory to delete' },
1005
+ },
1006
+ required: ['path'],
1007
+ },
1008
+ },
1009
+ },
1010
+ {
1011
+ type: 'function',
1012
+ function: {
1013
+ name: 'move_file',
1014
+ description: 'Move or rename a file or directory.',
1015
+ parameters: {
1016
+ type: 'object',
1017
+ properties: {
1018
+ source: { type: 'string', description: 'Current path' },
1019
+ destination: { type: 'string', description: 'New path' },
1020
+ },
1021
+ required: ['source', 'destination'],
1022
+ },
1023
+ },
1024
+ },
1025
+ {
1026
+ type: 'function',
1027
+ function: {
1028
+ name: 'list_directory',
1029
+ description: 'List files and directories with details (size, type, modified date).',
1030
+ parameters: {
1031
+ type: 'object',
1032
+ properties: {
1033
+ path: { type: 'string', description: 'Directory path (default: current directory)' },
1034
+ recursive: { type: 'boolean', description: 'List recursively (default: false)' },
1035
+ max_depth: { type: 'number', description: 'Max depth for recursive listing (default: 3)' },
1036
+ },
1037
+ required: [],
1038
+ },
1039
+ },
1040
+ },
1041
+ // ─────────────────────────────────────────────────────────────────────────
1042
+ // SEARCH & DISCOVERY
1043
+ // ─────────────────────────────────────────────────────────────────────────
1044
+ {
1045
+ type: 'function',
1046
+ function: {
1047
+ name: 'glob_search',
1048
+ description: 'Find files matching a glob pattern. Fast file discovery.',
1049
+ parameters: {
1050
+ type: 'object',
1051
+ properties: {
1052
+ pattern: { type: 'string', description: 'Glob pattern (e.g., "**/*.ts", "src/**/*.{js,jsx}")' },
1053
+ path: { type: 'string', description: 'Base directory (default: current directory)' },
1054
+ },
1055
+ required: ['pattern'],
1056
+ },
1057
+ },
1058
+ },
1059
+ {
1060
+ type: 'function',
1061
+ function: {
1062
+ name: 'grep_search',
1063
+ description: 'Search file contents using regex. Like ripgrep. Returns matching lines with context.',
1064
+ parameters: {
1065
+ type: 'object',
1066
+ properties: {
1067
+ pattern: { type: 'string', description: 'Regex pattern to search for' },
1068
+ path: { type: 'string', description: 'Directory or file to search (default: current directory)' },
1069
+ include: { type: 'string', description: 'File pattern to include (e.g., "*.ts")' },
1070
+ context: { type: 'number', description: 'Lines of context around matches (default: 2)' },
1071
+ max_results: { type: 'number', description: 'Maximum results to return (default: 20)' },
1072
+ },
1073
+ required: ['pattern'],
1074
+ },
1075
+ },
1076
+ },
1077
+ // ─────────────────────────────────────────────────────────────────────────
1078
+ // TERMINAL & COMMANDS
1079
+ // ─────────────────────────────────────────────────────────────────────────
1080
+ {
1081
+ type: 'function',
1082
+ function: {
1083
+ name: 'run_command',
1084
+ description: 'Execute a shell command. For git, npm, builds, tests, etc.',
1085
+ parameters: {
1086
+ type: 'object',
1087
+ properties: {
1088
+ command: { type: 'string', description: 'Shell command to execute' },
1089
+ cwd: { type: 'string', description: 'Working directory (default: current directory)' },
1090
+ timeout: { type: 'number', description: 'Timeout in seconds (default: 30)' },
1091
+ },
1092
+ required: ['command'],
1093
+ },
1094
+ },
1095
+ },
1096
+ // ─────────────────────────────────────────────────────────────────────────
1097
+ // GIT OPERATIONS
1098
+ // ─────────────────────────────────────────────────────────────────────────
1099
+ {
1100
+ type: 'function',
1101
+ function: {
1102
+ name: 'git_status',
1103
+ description: 'Get git status: branch, changed files, staged files, etc.',
1104
+ parameters: {
1105
+ type: 'object',
1106
+ properties: {},
1107
+ required: [],
1108
+ },
1109
+ },
1110
+ },
1111
+ {
1112
+ type: 'function',
1113
+ function: {
1114
+ name: 'git_diff',
1115
+ description: 'Show git diff. Unstaged changes by default.',
1116
+ parameters: {
1117
+ type: 'object',
1118
+ properties: {
1119
+ staged: { type: 'boolean', description: 'Show staged changes instead' },
1120
+ file: { type: 'string', description: 'Specific file to diff' },
1121
+ },
1122
+ required: [],
1123
+ },
1124
+ },
1125
+ },
1126
+ {
1127
+ type: 'function',
1128
+ function: {
1129
+ name: 'git_log',
1130
+ description: 'Show recent git commits.',
1131
+ parameters: {
1132
+ type: 'object',
1133
+ properties: {
1134
+ count: { type: 'number', description: 'Number of commits to show (default: 10)' },
1135
+ file: { type: 'string', description: 'Show commits for specific file' },
1136
+ },
1137
+ required: [],
1138
+ },
1139
+ },
1140
+ },
1141
+ {
1142
+ type: 'function',
1143
+ function: {
1144
+ name: 'git_commit',
1145
+ description: 'Stage files and create a commit.',
1146
+ parameters: {
1147
+ type: 'object',
1148
+ properties: {
1149
+ message: { type: 'string', description: 'Commit message' },
1150
+ files: { type: 'array', items: { type: 'string' }, description: 'Files to stage (default: all changed)' },
1151
+ },
1152
+ required: ['message'],
1153
+ },
1154
+ },
1155
+ },
1156
+ ];
1157
+ // Internal compression tools (not advertised to LLM but still functional)
1158
+ const INTERNAL_TOOLS = ['compress_document', 'compare_compression'];
1159
+ /**
1160
+ * Word wrap text to specified width
1161
+ */
1162
+ function wordWrap(text, maxWidth) {
1163
+ const lines = [];
1164
+ for (const paragraph of text.split('\n')) {
1165
+ if (paragraph.length <= maxWidth) {
1166
+ lines.push(paragraph);
1167
+ continue;
1168
+ }
1169
+ const words = paragraph.split(' ');
1170
+ let currentLine = '';
1171
+ for (const word of words) {
1172
+ if (currentLine.length + word.length + 1 <= maxWidth) {
1173
+ currentLine += (currentLine ? ' ' : '') + word;
1174
+ }
1175
+ else {
1176
+ if (currentLine)
1177
+ lines.push(currentLine);
1178
+ currentLine = word;
1179
+ }
1180
+ }
1181
+ if (currentLine)
1182
+ lines.push(currentLine);
1183
+ }
1184
+ return lines.join('\n');
1185
+ }
1186
+ /**
1187
+ * Show spinner - shimmering * animation, gray 700
1188
+ */
1189
+ async function withSpinner(message, work) {
1190
+ let frameIndex = 0;
1191
+ const start = Date.now();
1192
+ const shimmer = ['*', '✦', '✧', '·', '✧', '✦'];
1193
+ process.stdout.write(colors.gray700(` ${shimmer[0]} ${message}`));
1194
+ const interval = setInterval(() => {
1195
+ frameIndex = (frameIndex + 1) % shimmer.length;
1196
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
1197
+ process.stdout.write(`\r` + colors.gray700(` ${shimmer[frameIndex]} ${message} ${elapsed}s`));
1198
+ }, 150);
1199
+ try {
1200
+ const result = await work();
1201
+ clearInterval(interval);
1202
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
1203
+ process.stdout.write(`\r` + colors.gray500(` ✓ ${message} ${elapsed}s`) + '\n');
1204
+ return result;
1205
+ }
1206
+ catch (error) {
1207
+ clearInterval(interval);
1208
+ process.stdout.write(`\r` + colors.error(` ✗ ${message}`) + '\n');
1209
+ throw error;
1210
+ }
1211
+ }
1212
+ /**
1213
+ * Show file reading progress - gray 700, minimal
1214
+ */
1215
+ function showFileProgress(filePath, size) {
1216
+ const fileName = path.basename(filePath);
1217
+ const sizeStr = size > 1024 ? `${(size / 1024).toFixed(1)}KB` : `${size}B`;
1218
+ console.log(colors.gray700(` · ${fileName} ${sizeStr}`));
1219
+ }
1220
+ /**
1221
+ * Show a collapsible summary for file content (don't dump everything)
1222
+ */
1223
+ function summarizeContent(content, _maxPreview = 200) {
1224
+ const lines = content.split('\n');
1225
+ const lineCount = lines.length;
1226
+ if (lineCount <= 10) {
1227
+ return content;
1228
+ }
1229
+ return content;
1230
+ }
1231
+ /**
1232
+ * Execute a tool call with nice visual feedback
1233
+ */
1234
+ async function executeTool(name, args) {
1235
+ try {
1236
+ switch (name) {
1237
+ // ─────────────────────────────────────────────────────────────────────
1238
+ // FILE OPERATIONS
1239
+ // ─────────────────────────────────────────────────────────────────────
1240
+ case 'read_file': {
1241
+ const filePath = args.path;
1242
+ const startLine = args.start_line;
1243
+ const endLine = args.end_line;
1244
+ if (!fs.existsSync(filePath)) {
1245
+ console.log(` ${colors.error(ui.cross)} File not found: ${filePath}`);
1246
+ return `Error: File not found: ${filePath}`;
1247
+ }
1248
+ // Check cache first
1249
+ const cached = getCachedFile(filePath);
1250
+ let content;
1251
+ if (cached && !startLine && !endLine) {
1252
+ content = cached;
1253
+ // 12px style - compact cached message
1254
+ console.log(colors.small(` ${ui.success} cached: ${path.basename(filePath)}`));
1255
+ }
1256
+ else {
1257
+ const stats = fs.statSync(filePath);
1258
+ showFileProgress(filePath, stats.size);
1259
+ content = fs.readFileSync(filePath, 'utf-8');
1260
+ setCachedFile(filePath, content);
1261
+ }
1262
+ // Handle line ranges - 3px gap style
1263
+ if (startLine || endLine) {
1264
+ const lines = content.split('\n');
1265
+ const start = Math.max(1, startLine || 1) - 1;
1266
+ const end = endLine ? Math.min(endLine, lines.length) : lines.length;
1267
+ content = lines.slice(start, end).map((line, i) => `${start + i + 1}| ${line}`).join('\n');
1268
+ console.log(colors.small(` ${lines.length} lines, showing ${start + 1}-${end}`));
1269
+ }
1270
+ else {
1271
+ const lineCount = content.split('\n').length;
1272
+ console.log(colors.small(` ${lineCount} lines`));
1273
+ }
1274
+ // Track file read for workspace memory
1275
+ trackFileOp('read', filePath);
1276
+ return content.slice(0, 50000);
1277
+ }
1278
+ case 'write_file': {
1279
+ const filePath = args.path;
1280
+ const content = args.content;
1281
+ const isNew = !fs.existsSync(filePath);
1282
+ // Show file creation/update preview with broken white strip style
1283
+ const diffWidth = getTerminalWidth() - 2;
1284
+ const contentLines = content.split('\n');
1285
+ // Header
1286
+ console.log('');
1287
+ console.log(colors.gray500(` ${isNew ? 'creating' : 'writing'} ${path.basename(filePath)}`));
1288
+ console.log('');
1289
+ // Added lines: dark text on broken white background - continuous block
1290
+ // Using dim style to simulate smaller 12px font
1291
+ contentLines.forEach((line, i) => {
1292
+ const showLine = i < 8 || i >= contentLines.length - 2;
1293
+ const isEllipsis = i === 8 && contentLines.length > 10;
1294
+ if (showLine) {
1295
+ const displayLine = ` ${line}`.slice(0, diffWidth).padEnd(diffWidth);
1296
+ // \x1b[48;5;253m = broken white bg, \x1b[38;5;235m = dark text, \x1b[2m = dim (smaller feel)
1297
+ console.log(`\x1b[48;5;253m\x1b[38;5;235m\x1b[2m${displayLine}\x1b[0m`);
1298
+ }
1299
+ else if (isEllipsis) {
1300
+ const moreLine = ` ... ${contentLines.length - 10} more ...`.slice(0, diffWidth).padEnd(diffWidth);
1301
+ console.log(`\x1b[48;5;253m\x1b[38;5;240m\x1b[2m${moreLine}\x1b[0m`);
1302
+ }
1303
+ });
1304
+ console.log('');
1305
+ // Write the file
1306
+ const dir = path.dirname(filePath);
1307
+ if (!fs.existsSync(dir)) {
1308
+ fs.mkdirSync(dir, { recursive: true });
1309
+ }
1310
+ fs.writeFileSync(filePath, content);
1311
+ invalidateCache(filePath);
1312
+ // Track file operation for workspace memory
1313
+ trackFileOp(isNew ? 'create' : 'write', filePath);
1314
+ console.log(colors.gray500(` ✓ ${contentLines.length} lines`));
1315
+ return `File written: ${filePath}`;
1316
+ }
1317
+ case 'edit_file': {
1318
+ const filePath = args.path;
1319
+ const search = args.search;
1320
+ const replace = args.replace;
1321
+ if (!fs.existsSync(filePath)) {
1322
+ console.log(colors.error(` ✗ not found: ${filePath}`));
1323
+ return `Error: File not found: ${filePath}`;
1324
+ }
1325
+ const content = fs.readFileSync(filePath, 'utf-8');
1326
+ const occurrences = content.split(search).length - 1;
1327
+ if (occurrences === 0) {
1328
+ console.log(colors.warning(` ! search text not found`));
1329
+ return `Error: Search text not found in ${filePath}`;
1330
+ }
1331
+ if (occurrences > 1) {
1332
+ console.log(colors.warning(` ! ${occurrences} occurrences found (must be unique)`));
1333
+ return `Error: Found ${occurrences} occurrences. Search text must be unique.`;
1334
+ }
1335
+ // Show diff-style edit preview
1336
+ const diffWidth = getTerminalWidth() - 2;
1337
+ const searchLines = search.split('\n');
1338
+ const replaceLines = replace.split('\n');
1339
+ // Header
1340
+ console.log('');
1341
+ console.log(colors.gray500(` editing ${path.basename(filePath)}`));
1342
+ console.log('');
1343
+ // Removed lines: broken white text on terminal bg - dim for 12px feel
1344
+ searchLines.forEach((line, i) => {
1345
+ const showLine = i < 6 || i >= searchLines.length - 2;
1346
+ const isEllipsis = i === 6 && searchLines.length > 8;
1347
+ if (showLine) {
1348
+ const displayLine = ` ${line}`.slice(0, diffWidth);
1349
+ // Dim broken white text, no background
1350
+ console.log(`\x1b[38;5;250m\x1b[2m${displayLine}\x1b[0m`);
1351
+ }
1352
+ else if (isEllipsis) {
1353
+ console.log(`\x1b[38;5;242m\x1b[2m ... ${searchLines.length - 8} more ...\x1b[0m`);
1354
+ }
1355
+ });
1356
+ console.log(''); // gap between removed and added
1357
+ // Added lines: dark text on broken white background - continuous block
1358
+ // Using dim style to simulate smaller 12px font
1359
+ replaceLines.forEach((line, i) => {
1360
+ const showLine = i < 6 || i >= replaceLines.length - 2;
1361
+ const isEllipsis = i === 6 && replaceLines.length > 8;
1362
+ if (showLine) {
1363
+ const displayLine = ` ${line}`.slice(0, diffWidth).padEnd(diffWidth);
1364
+ // \x1b[48;5;253m = broken white bg, \x1b[38;5;235m = dark text, \x1b[2m = dim (smaller feel)
1365
+ console.log(`\x1b[48;5;253m\x1b[38;5;235m\x1b[2m${displayLine}\x1b[0m`);
1366
+ }
1367
+ else if (isEllipsis) {
1368
+ const moreLine = ` ... ${replaceLines.length - 8} more ...`.slice(0, diffWidth).padEnd(diffWidth);
1369
+ console.log(`\x1b[48;5;253m\x1b[38;5;240m\x1b[2m${moreLine}\x1b[0m`);
1370
+ }
1371
+ });
1372
+ console.log('');
1373
+ // Apply the edit
1374
+ const newContent = content.replace(search, replace);
1375
+ fs.writeFileSync(filePath, newContent);
1376
+ invalidateCache(filePath);
1377
+ // Track file operation for workspace memory
1378
+ trackFileOp('write', filePath);
1379
+ const linesChanged = replaceLines.length - searchLines.length;
1380
+ console.log(colors.gray500(` ✓ ${linesChanged >= 0 ? '+' : ''}${linesChanged} lines`));
1381
+ return `File edited: ${filePath}`;
1382
+ }
1383
+ case 'delete_file': {
1384
+ const filePath = args.path;
1385
+ if (!fs.existsSync(filePath)) {
1386
+ console.log(` ${colors.error(ui.cross)} Not found: ${filePath}`);
1387
+ return `Error: File not found: ${filePath}`;
1388
+ }
1389
+ await withSpinner(`Deleting: ${path.basename(filePath)}`, async () => {
1390
+ const stats = fs.statSync(filePath);
1391
+ if (stats.isDirectory()) {
1392
+ fs.rmdirSync(filePath);
1393
+ }
1394
+ else {
1395
+ fs.unlinkSync(filePath);
1396
+ }
1397
+ invalidateCache(filePath);
1398
+ });
1399
+ return `Deleted: ${filePath}`;
1400
+ }
1401
+ case 'move_file': {
1402
+ const source = args.source;
1403
+ const destination = args.destination;
1404
+ if (!fs.existsSync(source)) {
1405
+ console.log(` ${colors.error(ui.cross)} Source not found: ${source}`);
1406
+ return `Error: Source not found: ${source}`;
1407
+ }
1408
+ await withSpinner(`Moving: ${path.basename(source)} → ${path.basename(destination)}`, async () => {
1409
+ const destDir = path.dirname(destination);
1410
+ if (!fs.existsSync(destDir)) {
1411
+ fs.mkdirSync(destDir, { recursive: true });
1412
+ }
1413
+ fs.renameSync(source, destination);
1414
+ invalidateCache(source);
1415
+ });
1416
+ return `Moved: ${source} → ${destination}`;
1417
+ }
1418
+ case 'list_directory': {
1419
+ const dirPath = args.path || process.cwd();
1420
+ const recursive = args.recursive || false;
1421
+ const maxDepth = args.max_depth || 3;
1422
+ if (!fs.existsSync(dirPath)) {
1423
+ console.log(` ${colors.error(ui.cross)} Directory not found: ${dirPath}`);
1424
+ return `Error: Directory not found: ${dirPath}`;
1425
+ }
1426
+ if (recursive) {
1427
+ const result = await withSpinner(`Scanning: ${path.basename(dirPath) || dirPath}`, async () => {
1428
+ return await execCommand(`find "${dirPath}" -maxdepth ${maxDepth} -type f -o -type d 2>/dev/null | head -100`);
1429
+ });
1430
+ return result.stdout || 'Empty directory';
1431
+ }
1432
+ const entries = await withSpinner(`Listing: ${path.basename(dirPath) || dirPath}`, async () => {
1433
+ return fs.readdirSync(dirPath, { withFileTypes: true });
1434
+ });
1435
+ const dirs = entries.filter(e => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
1436
+ const files = entries.filter(e => !e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
1437
+ console.log(colors.dimGray(` └─ ${dirs.length} folders, ${files.length} files`));
1438
+ const output = [];
1439
+ dirs.forEach(d => output.push(`📁 ${d.name}/`));
1440
+ files.forEach(f => {
1441
+ try {
1442
+ const stats = fs.statSync(path.join(dirPath, f.name));
1443
+ const size = stats.size > 1024 ? `${(stats.size / 1024).toFixed(1)}KB` : `${stats.size}B`;
1444
+ output.push(`📄 ${f.name} (${size})`);
1445
+ }
1446
+ catch {
1447
+ output.push(`📄 ${f.name}`);
1448
+ }
1449
+ });
1450
+ return output.join('\n');
1451
+ }
1452
+ // ─────────────────────────────────────────────────────────────────────
1453
+ // SEARCH & DISCOVERY
1454
+ // ─────────────────────────────────────────────────────────────────────
1455
+ case 'glob_search': {
1456
+ const pattern = args.pattern;
1457
+ const searchPath = args.path || '.';
1458
+ const result = await withSpinner(`Searching: ${pattern}`, async () => {
1459
+ // Use find with pattern conversion
1460
+ const findPattern = pattern.replace(/\*\*/g, '').replace(/\*/g, '*');
1461
+ return await execCommand(`find "${searchPath}" -name "${findPattern}" 2>/dev/null | head -50`);
1462
+ });
1463
+ const matches = result.stdout.trim().split('\n').filter(Boolean);
1464
+ if (matches.length > 0) {
1465
+ console.log(colors.dimGray(` └─ ${matches.length} files found`));
1466
+ }
1467
+ return result.stdout || 'No files found';
1468
+ }
1469
+ case 'grep_search': {
1470
+ const pattern = args.pattern;
1471
+ const searchPath = args.path || '.';
1472
+ const include = args.include;
1473
+ const context = args.context || 2;
1474
+ const maxResults = args.max_results || 20;
1475
+ let cmd = `grep -rn --color=never -C${context}`;
1476
+ if (include) {
1477
+ cmd += ` --include="${include}"`;
1478
+ }
1479
+ cmd += ` "${pattern}" "${searchPath}" 2>/dev/null | head -${maxResults * 5}`;
1480
+ const result = await withSpinner(`Searching: "${pattern}"`, async () => {
1481
+ return await execCommand(cmd);
1482
+ });
1483
+ if (result.stdout.trim()) {
1484
+ const lines = result.stdout.trim().split('\n');
1485
+ console.log(colors.dimGray(` └─ ${Math.min(lines.length, maxResults)} matches`));
1486
+ }
1487
+ return result.stdout || 'No matches found';
1488
+ }
1489
+ // ─────────────────────────────────────────────────────────────────────
1490
+ // TERMINAL & COMMANDS
1491
+ // ─────────────────────────────────────────────────────────────────────
1492
+ case 'run_command': {
1493
+ const cmd = args.command;
1494
+ const cwd = args.cwd;
1495
+ const shortCmd = cmd.length > 50 ? cmd.slice(0, 47) + '...' : cmd;
1496
+ const result = await withSpinner(`Running: ${shortCmd}`, async () => {
1497
+ if (cwd) {
1498
+ return await execCommand(`cd "${cwd}" && ${cmd}`);
1499
+ }
1500
+ return await execCommand(cmd);
1501
+ });
1502
+ const output = result.stdout || result.stderr || '(no output)';
1503
+ // Smart output truncation for common commands
1504
+ let displayOutput = output.trim();
1505
+ if (cmd.includes('npm install') || cmd.includes('yarn add')) {
1506
+ // Summarize package install output
1507
+ const lines = displayOutput.split('\n');
1508
+ const added = lines.find(l => l.includes('added') || l.includes('packages'));
1509
+ if (added && lines.length > 5) {
1510
+ displayOutput = added;
1511
+ }
1512
+ }
1513
+ const outputLines = displayOutput.split('\n');
1514
+ if (outputLines.length > 10) {
1515
+ console.log(colors.dimGray(` ${ui.topLeft}${ui.horizontal} Output (${outputLines.length} lines)`));
1516
+ outputLines.slice(0, 5).forEach(line => {
1517
+ console.log(colors.dimGray(` ${ui.vertical} `) + colors.body(line.slice(0, 70)));
1518
+ });
1519
+ console.log(colors.dimGray(` ${ui.vertical} `) + colors.muted(`... ${outputLines.length - 5} more lines`));
1520
+ console.log(colors.dimGray(` ${ui.bottomLeft}${ui.horizontal.repeat(3)}`));
1521
+ }
1522
+ else if (outputLines.length > 0 && displayOutput) {
1523
+ console.log(colors.dimGray(` ${ui.topLeft}${ui.horizontal} Output`));
1524
+ outputLines.forEach(line => {
1525
+ console.log(colors.dimGray(` ${ui.vertical} `) + colors.body(line.slice(0, 80)));
1526
+ });
1527
+ console.log(colors.dimGray(` ${ui.bottomLeft}${ui.horizontal.repeat(3)}`));
1528
+ }
1529
+ // Track command for workspace memory
1530
+ trackCommand(cmd, result.code === 0);
1531
+ if (result.code !== 0) {
1532
+ return `Exit code ${result.code}:\n${output}`;
1533
+ }
1534
+ return output.slice(0, 10000);
1535
+ }
1536
+ // ─────────────────────────────────────────────────────────────────────
1537
+ // GIT OPERATIONS
1538
+ // ─────────────────────────────────────────────────────────────────────
1539
+ case 'git_status': {
1540
+ const result = await withSpinner('Git status', async () => {
1541
+ return await execCommand('git status --porcelain -b');
1542
+ });
1543
+ const lines = result.stdout.trim().split('\n');
1544
+ const branch = lines[0]?.replace('## ', '') || 'unknown';
1545
+ const changes = lines.slice(1).length;
1546
+ console.log(colors.dimGray(` └─ ${branch}, ${changes} changed files`));
1547
+ // Parse into structured output
1548
+ const output = [`Branch: ${branch}`];
1549
+ if (changes > 0) {
1550
+ output.push('\nChanges:');
1551
+ lines.slice(1).forEach(line => {
1552
+ const status = line.slice(0, 2);
1553
+ const file = line.slice(3);
1554
+ const icon = status.includes('M') ? '~' : status.includes('A') ? '+' : status.includes('D') ? '-' : '?';
1555
+ output.push(` ${icon} ${file}`);
1556
+ });
1557
+ }
1558
+ return output.join('\n');
1559
+ }
1560
+ case 'git_diff': {
1561
+ const staged = args.staged || false;
1562
+ const file = args.file;
1563
+ let cmd = staged ? 'git diff --cached' : 'git diff';
1564
+ if (file)
1565
+ cmd += ` -- "${file}"`;
1566
+ cmd += ' | head -200';
1567
+ const result = await withSpinner(staged ? 'Git diff (staged)' : 'Git diff', async () => {
1568
+ return await execCommand(cmd);
1569
+ });
1570
+ const lines = result.stdout.trim().split('\n').length;
1571
+ if (result.stdout.trim()) {
1572
+ console.log(colors.dimGray(` └─ ${lines} lines of diff`));
1573
+ }
1574
+ return result.stdout || 'No changes';
1575
+ }
1576
+ case 'git_log': {
1577
+ const count = args.count || 10;
1578
+ const file = args.file;
1579
+ let cmd = `git log --oneline -${count}`;
1580
+ if (file)
1581
+ cmd += ` -- "${file}"`;
1582
+ const result = await withSpinner('Git log', async () => {
1583
+ return await execCommand(cmd);
1584
+ });
1585
+ return result.stdout || 'No commits';
1586
+ }
1587
+ case 'git_commit': {
1588
+ const message = args.message;
1589
+ const files = args.files;
1590
+ // Stage files
1591
+ const stageCmd = files && files.length > 0
1592
+ ? `git add ${files.map(f => `"${f}"`).join(' ')}`
1593
+ : 'git add -A';
1594
+ await withSpinner('Staging files', async () => {
1595
+ return await execCommand(stageCmd);
1596
+ });
1597
+ // Commit
1598
+ const result = await withSpinner('Committing', async () => {
1599
+ return await execCommand(`git commit -m "${message.replace(/"/g, '\\"')}"`);
1600
+ });
1601
+ if (result.code !== 0) {
1602
+ return `Commit failed: ${result.stderr || result.stdout}`;
1603
+ }
1604
+ // Get commit hash
1605
+ const hashResult = await execCommand('git rev-parse --short HEAD');
1606
+ const hash = hashResult.stdout.trim();
1607
+ console.log(colors.dimGray(` └─ Committed: ${hash}`));
1608
+ return `Committed: ${hash} - ${message}`;
1609
+ }
1610
+ case 'compress_document': {
1611
+ const filePath = args.path;
1612
+ const returnText = args.return_text || false;
1613
+ const pricePerMillion = args.price_per_million || 15;
1614
+ if (!fs.existsSync(filePath)) {
1615
+ console.log(` ${colors.red('✗')} File not found: ${filePath}`);
1616
+ return `Error: File not found: ${filePath}`;
1617
+ }
1618
+ // Read file content (handle PDF via pdf-parse)
1619
+ let content;
1620
+ const ext = path.extname(filePath).toLowerCase();
1621
+ if (ext === '.pdf') {
1622
+ content = await withSpinner(`Extracting PDF: ${path.basename(filePath)}`, async () => {
1623
+ try {
1624
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1625
+ const pdfParse = require('pdf-parse');
1626
+ const dataBuffer = fs.readFileSync(filePath);
1627
+ const data = await pdfParse(dataBuffer);
1628
+ return data.text || '';
1629
+ }
1630
+ catch {
1631
+ // Fallback to pdftotext CLI
1632
+ const result = await execCommand(`pdftotext "${filePath}" - 2>/dev/null`);
1633
+ return result.stdout || '';
1634
+ }
1635
+ });
1636
+ if (!content.trim()) {
1637
+ return 'Error: Could not extract text from PDF.';
1638
+ }
1639
+ }
1640
+ else {
1641
+ content = fs.readFileSync(filePath, 'utf-8');
1642
+ }
1643
+ const stats = fs.statSync(filePath);
1644
+ showFileProgress(filePath, stats.size);
1645
+ // Compress using SONA
1646
+ const compressor = new compressor_js_1.SonaCompressor();
1647
+ const result = await withSpinner('Compressing with SMR...', async () => {
1648
+ return compressor.compress(content);
1649
+ });
1650
+ // Calculate costs
1651
+ const costOriginal = (result.originalTokens / 1_000_000) * pricePerMillion;
1652
+ const costCompressed = (result.compressedTokens / 1_000_000) * pricePerMillion;
1653
+ const costSaved = costOriginal - costCompressed;
1654
+ // Display metrics with premium styling
1655
+ console.log('');
1656
+ console.log(colors.accent(` ${ui.topLeft}${ui.horizontal.repeat(48)}${ui.topRight}`));
1657
+ console.log(colors.accent(` ${ui.vertical}`) + colors.title(' ' + ui.chart + ' Compression Results') + ' ' + colors.accent(ui.vertical));
1658
+ console.log(colors.accent(` ${ui.leftT}${ui.horizontal.repeat(48)}${ui.rightT}`));
1659
+ console.log(colors.accent(` ${ui.vertical}`) + colors.muted(' Original') + colors.body(` ${result.originalTokens.toLocaleString().padStart(10)} tokens`) + ' ' + colors.accent(ui.vertical));
1660
+ console.log(colors.accent(` ${ui.vertical}`) + colors.muted(' Compressed') + colors.body(` ${result.compressedTokens.toLocaleString().padStart(10)} tokens`) + ' ' + colors.accent(ui.vertical));
1661
+ console.log(colors.accent(` ${ui.vertical}`) + colors.muted(' Saved') + colors.success(` ${result.tokenSavings.toLocaleString().padStart(10)} tokens`) + colors.success(` (${result.tokenSavingsPercent.toFixed(1)}%)`) + ' ' + colors.accent(ui.vertical));
1662
+ console.log(colors.accent(` ${ui.leftT}${ui.horizontal.repeat(48)}${ui.rightT}`));
1663
+ console.log(colors.accent(` ${ui.vertical}`) + colors.success(` ${ui.money} Cost saved: $${costSaved.toFixed(4)} per request`) + ' ' + colors.accent(ui.vertical));
1664
+ console.log(colors.accent(` ${ui.bottomLeft}${ui.horizontal.repeat(48)}${ui.bottomRight}`));
1665
+ console.log('');
1666
+ const metrics = {
1667
+ file: filePath,
1668
+ originalTokens: result.originalTokens,
1669
+ compressedTokens: result.compressedTokens,
1670
+ tokensSaved: result.tokenSavings,
1671
+ compressionRatio: result.tokenSavingsPercent,
1672
+ costOriginal: costOriginal,
1673
+ costCompressed: costCompressed,
1674
+ costSaved: costSaved,
1675
+ pricePerMillion: pricePerMillion,
1676
+ };
1677
+ if (returnText) {
1678
+ return JSON.stringify({ metrics, compressedText: result.compressedText }, null, 2);
1679
+ }
1680
+ return JSON.stringify(metrics, null, 2);
1681
+ }
1682
+ case 'compare_compression': {
1683
+ const filePath = args.path;
1684
+ const pricePerMillion = args.price_per_million || 15;
1685
+ const outputReport = args.output_report;
1686
+ if (!fs.existsSync(filePath)) {
1687
+ console.log(` ${colors.red('✗')} File not found: ${filePath}`);
1688
+ return `Error: File not found: ${filePath}`;
1689
+ }
1690
+ // Read file
1691
+ let content;
1692
+ const ext = path.extname(filePath).toLowerCase();
1693
+ if (ext === '.pdf') {
1694
+ content = await withSpinner(`Extracting PDF: ${path.basename(filePath)}`, async () => {
1695
+ try {
1696
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1697
+ const pdfParse = require('pdf-parse');
1698
+ const dataBuffer = fs.readFileSync(filePath);
1699
+ const data = await pdfParse(dataBuffer);
1700
+ return data.text || '';
1701
+ }
1702
+ catch {
1703
+ const result = await execCommand(`pdftotext "${filePath}" - 2>/dev/null`);
1704
+ return result.stdout || '';
1705
+ }
1706
+ });
1707
+ if (!content.trim()) {
1708
+ return 'Error: Could not extract text from PDF.';
1709
+ }
1710
+ }
1711
+ else {
1712
+ content = fs.readFileSync(filePath, 'utf-8');
1713
+ }
1714
+ const stats = fs.statSync(filePath);
1715
+ showFileProgress(filePath, stats.size);
1716
+ // Compress
1717
+ const compressor = new compressor_js_1.SonaCompressor();
1718
+ const result = await withSpinner('Running compression analysis...', async () => {
1719
+ return compressor.compress(content);
1720
+ });
1721
+ // Calculate costs for different scales
1722
+ const scales = [1, 100, 1000, 10000];
1723
+ const costAnalysis = scales.map(n => ({
1724
+ requests: n,
1725
+ withoutSona: (result.originalTokens * n / 1_000_000) * pricePerMillion,
1726
+ withSona: (result.compressedTokens * n / 1_000_000) * pricePerMillion,
1727
+ saved: (result.tokenSavings * n / 1_000_000) * pricePerMillion,
1728
+ }));
1729
+ // Build report
1730
+ const report = `# SONA CODE Compression Analysis Report
1731
+
1732
+ ## Document: ${path.basename(filePath)}
1733
+
1734
+ **Analysis Date:** ${new Date().toISOString().split('T')[0]}
1735
+ **Price Model:** $${pricePerMillion} per million tokens (GPT-4o)
1736
+
1737
+ ---
1738
+
1739
+ ## 📊 Compression Metrics
1740
+
1741
+ | Metric | Value |
1742
+ |--------|-------|
1743
+ | Original Tokens | ${result.originalTokens.toLocaleString()} |
1744
+ | Compressed Tokens | ${result.compressedTokens.toLocaleString()} |
1745
+ | Tokens Saved | ${result.tokenSavings.toLocaleString()} |
1746
+ | **Compression Ratio** | **${result.tokenSavingsPercent.toFixed(2)}%** |
1747
+
1748
+ ---
1749
+
1750
+ ## 💰 Cost Analysis
1751
+
1752
+ ### Per-Request Cost
1753
+ | Scenario | Cost |
1754
+ |----------|------|
1755
+ | Without SONA | $${((result.originalTokens / 1_000_000) * pricePerMillion).toFixed(6)} |
1756
+ | With SONA | $${((result.compressedTokens / 1_000_000) * pricePerMillion).toFixed(6)} |
1757
+ | **Savings** | **$${((result.tokenSavings / 1_000_000) * pricePerMillion).toFixed(6)}** |
1758
+
1759
+ ### Scaled Cost Savings
1760
+
1761
+ | Requests | Without SONA | With SONA | Savings |
1762
+ |----------|--------------|-----------|---------|
1763
+ ${costAnalysis.map(c => `| ${c.requests.toLocaleString()} | $${c.withoutSona.toFixed(4)} | $${c.withSona.toFixed(4)} | $${c.saved.toFixed(4)} |`).join('\n')}
1764
+
1765
+ ---
1766
+
1767
+ ## 🎯 Key Findings
1768
+
1769
+ 1. **Token Reduction:** ${result.tokenSavingsPercent.toFixed(1)}% fewer tokens sent to the LLM
1770
+ 2. **Quality Preserved:** Semantic compression maintains document meaning
1771
+ 3. **ROI at Scale:** Processing 10,000 documents saves ~$${costAnalysis[3].saved.toFixed(2)}
1772
+
1773
+ ---
1774
+
1775
+ ## How It Works
1776
+
1777
+ SONA CODE uses **Structural Memory Reconstruction (SMR)** compression:
1778
+ - Removes filler phrases ("it is important to note that" → "notably")
1779
+ - Applies semantic abbreviations ("implementation" → "impl")
1780
+ - Preserves technical accuracy and context
1781
+
1782
+ > Note: Compression ratios vary by document type. Legal/technical documents typically see 15-25% reduction.
1783
+ `;
1784
+ // Display summary with premium styling
1785
+ console.log('');
1786
+ console.log(colors.accent(` ${ui.topLeft}${ui.horizontal.repeat(52)}${ui.topRight}`));
1787
+ console.log(colors.accent(` ${ui.vertical}`) + colors.title(` ${ui.chart} Compression Analysis`) + ' ' + colors.accent(ui.vertical));
1788
+ console.log(colors.accent(` ${ui.leftT}${ui.horizontal.repeat(52)}${ui.rightT}`));
1789
+ console.log(colors.accent(` ${ui.vertical}`) + ' ' + colors.accent(ui.vertical));
1790
+ console.log(colors.accent(` ${ui.vertical}`) + colors.muted(' Original tokens') + colors.body(` ${result.originalTokens.toLocaleString().padStart(12)}`) + ' ' + colors.accent(ui.vertical));
1791
+ console.log(colors.accent(` ${ui.vertical}`) + colors.muted(' Compressed tokens') + colors.body(` ${result.compressedTokens.toLocaleString().padStart(12)}`) + ' ' + colors.accent(ui.vertical));
1792
+ console.log(colors.accent(` ${ui.vertical}`) + colors.success(` Reduction`) + colors.success(` ${result.tokenSavingsPercent.toFixed(1).padStart(11)}%`) + ' ' + colors.accent(ui.vertical));
1793
+ console.log(colors.accent(` ${ui.vertical}`) + ' ' + colors.accent(ui.vertical));
1794
+ console.log(colors.accent(` ${ui.leftT}${ui.horizontal.repeat(52)}${ui.rightT}`));
1795
+ console.log(colors.accent(` ${ui.vertical}`) + colors.title(` ${ui.money} Cost @ 10,000 requests`) + ' ' + colors.accent(ui.vertical));
1796
+ console.log(colors.accent(` ${ui.leftT}${ui.horizontal.repeat(52)}${ui.rightT}`));
1797
+ console.log(colors.accent(` ${ui.vertical}`) + colors.muted(' Without SONA') + colors.body(` $${costAnalysis[3].withoutSona.toFixed(2).padStart(12)}`) + ' ' + colors.accent(ui.vertical));
1798
+ console.log(colors.accent(` ${ui.vertical}`) + colors.muted(' With SONA') + colors.body(` $${costAnalysis[3].withSona.toFixed(2).padStart(12)}`) + ' ' + colors.accent(ui.vertical));
1799
+ console.log(colors.accent(` ${ui.vertical}`) + colors.success(` You save`) + colors.success(` $${costAnalysis[3].saved.toFixed(2).padStart(12)}`) + ' ' + colors.accent(ui.vertical));
1800
+ console.log(colors.accent(` ${ui.vertical}`) + ' ' + colors.accent(ui.vertical));
1801
+ console.log(colors.accent(` ${ui.bottomLeft}${ui.horizontal.repeat(52)}${ui.bottomRight}`));
1802
+ console.log('');
1803
+ // Save report if requested
1804
+ if (outputReport) {
1805
+ await withSpinner(`Writing report: ${path.basename(outputReport)}`, async () => {
1806
+ fs.writeFileSync(outputReport, report);
1807
+ });
1808
+ console.log(colors.dim(` └─ Report saved to ${outputReport}`));
1809
+ }
1810
+ return outputReport
1811
+ ? `Analysis complete. Report saved to ${outputReport}\n\n${report}`
1812
+ : report;
1813
+ }
1814
+ default:
1815
+ return `Unknown tool: ${name}`;
1816
+ }
1817
+ }
1818
+ catch (error) {
1819
+ console.log(` ${colors.red('✗')} Error: ${error.message}`);
1820
+ return `Error: ${error.message}`;
1821
+ }
1822
+ }
1823
+ /**
1824
+ * Main chat function - the default sona experience
1825
+ */
1826
+ async function startChat(opts) {
1827
+ // Load saved configuration
1828
+ let config = loadConfig();
1829
+ // Override with environment variables
1830
+ const envDeepseekKey = process.env.DEEPSEEK_API_KEY;
1831
+ const envOpenaiKey = process.env.OPENAI_API_KEY;
1832
+ const envAnthropicKey = process.env.ANTHROPIC_API_KEY;
1833
+ // Use saved config or env vars
1834
+ let provider = (opts.provider || config.provider || 'deepseek');
1835
+ let model = opts.model || config.model || 'deepseek-chat';
1836
+ let apiKey = opts.deepseekKey || config.apiKey || envDeepseekKey || envOpenaiKey || envAnthropicKey || '';
1837
+ // Set terminal title to "sona"
1838
+ process.stdout.write('\x1b]0;sona\x07');
1839
+ // First-time setup if no API key configured
1840
+ if (!apiKey) {
1841
+ console.log('\x1b[2J\x1b[H'); // Clear screen
1842
+ console.log('');
1843
+ console.log(colors.title(' SONA CODE'));
1844
+ console.log(colors.dimGray(' ' + ui.horizontal.repeat(40)));
1845
+ console.log('');
1846
+ console.log(colors.body(' Welcome! Let\'s configure your API.'));
1847
+ console.log('');
1848
+ console.log(colors.small(' Select a provider:'));
1849
+ console.log('');
1850
+ console.log(colors.cyan(' 1.') + colors.body(' DeepSeek') + colors.success(' $0.14/M') + colors.small(' (recommended)'));
1851
+ console.log(colors.dimGray(' 2.') + colors.body(' OpenAI') + colors.dimGray(' $15/M'));
1852
+ console.log(colors.dimGray(' 3.') + colors.body(' Anthropic') + colors.dimGray(' $15/M'));
1853
+ console.log('');
1854
+ const rl = (0, readline_1.createInterface)({
1855
+ input: process.stdin,
1856
+ output: process.stdout,
1857
+ });
1858
+ const question = (prompt) => {
1859
+ return new Promise((resolve) => {
1860
+ rl.question(prompt, (answer) => resolve(answer.trim()));
1861
+ });
1862
+ };
1863
+ // Provider selection
1864
+ const providerChoice = await question(colors.accent(` ${ui.arrowRight} Select provider (1/2/3): `));
1865
+ switch (providerChoice) {
1866
+ case '1':
1867
+ provider = 'deepseek';
1868
+ model = 'deepseek-chat';
1869
+ console.log('');
1870
+ console.log(colors.muted(' Get your API key at: ') + colors.info('https://platform.deepseek.com'));
1871
+ break;
1872
+ case '2':
1873
+ provider = 'openai';
1874
+ model = 'gpt-4o-mini';
1875
+ console.log('');
1876
+ console.log(colors.muted(' Get your API key at: ') + colors.info('https://platform.openai.com'));
1877
+ break;
1878
+ case '3':
1879
+ provider = 'anthropic';
1880
+ model = 'claude-3-5-sonnet-20241022';
1881
+ console.log('');
1882
+ console.log(colors.muted(' Get your API key at: ') + colors.info('https://console.anthropic.com'));
1883
+ break;
1884
+ default:
1885
+ provider = 'deepseek';
1886
+ model = 'deepseek-chat';
1887
+ console.log(colors.muted(' Using DeepSeek (default)'));
1888
+ console.log('');
1889
+ console.log(colors.muted(' Get your API key at: ') + colors.info('https://platform.deepseek.com'));
1890
+ }
1891
+ console.log('');
1892
+ apiKey = await question(colors.accent(` ${ui.arrowRight} API Key: `));
1893
+ if (!apiKey) {
1894
+ console.log('');
1895
+ console.log(colors.error(' API key required.'));
1896
+ console.log(colors.muted(' You can also set DEEPSEEK_API_KEY environment variable.'));
1897
+ console.log('');
1898
+ rl.close();
1899
+ process.exit(1);
1900
+ }
1901
+ // Save configuration
1902
+ config = { ...config, provider, model, apiKey };
1903
+ saveConfig(config);
1904
+ console.log('');
1905
+ console.log(colors.success(` ${ui.success} Saved to ~/.sona/config.json`));
1906
+ console.log(colors.small(' Use /api to reconfigure'));
1907
+ console.log('');
1908
+ rl.close();
1909
+ }
1910
+ const compress = opts.compress !== false;
1911
+ const session = (0, session_js_1.createSession)({
1912
+ protectedTerms: opts.protect || [],
1913
+ });
1914
+ // Check for SONA.md project instructions
1915
+ let projectInstructions = '';
1916
+ const sonaMdPath = path.join(process.cwd(), 'SONA.md');
1917
+ if (fs.existsSync(sonaMdPath)) {
1918
+ try {
1919
+ projectInstructions = fs.readFileSync(sonaMdPath, 'utf-8');
1920
+ }
1921
+ catch { /* ignore */ }
1922
+ }
1923
+ // Load previous conversation history for this workspace
1924
+ const history = loadHistory();
1925
+ const historyContext = buildHistoryContext(history);
1926
+ // System prompt - senior engineer personality
1927
+ const systemPrompt = opts.system || `You're a senior engineer pair-programming with the user. Full access to their codebase, terminal, and filesystem.
1928
+
1929
+ Be natural. Talk like a smart colleague, not a chatbot. Skip formalities when a simple answer works.
1930
+
1931
+ Capabilities:
1932
+ - Read/write/edit files (use line ranges for large files)
1933
+ - Run any shell command
1934
+ - Search code (glob, regex)
1935
+ - Git operations
1936
+ - Navigate and understand codebases quickly
1937
+
1938
+ Working in: ${process.cwd()}
1939
+
1940
+ Style:
1941
+ - Direct and concise
1942
+ - Don't list capabilities unless asked
1943
+ - Just do things - don't ask permission for read operations
1944
+ - Brief greetings - one line max
1945
+ - Be honest when things break
1946
+ - No unnecessary pleasantries
1947
+ - Think step by step on complex tasks
1948
+ - Be thorough - explore before concluding
1949
+
1950
+ ${projectInstructions ? `Project context (from SONA.md):\n${projectInstructions}\n` : ''}${historyContext}`;
1951
+ const messages = [
1952
+ { role: 'system', content: systemPrompt }
1953
+ ];
1954
+ // Show if we have previous context - richer memory indicator
1955
+ if (history) {
1956
+ const memoryParts = [];
1957
+ if (history.stats.totalSessions > 1)
1958
+ memoryParts.push(`session ${history.stats.totalSessions}`);
1959
+ if (history.filesWritten.length > 0)
1960
+ memoryParts.push(`${history.filesWritten.length} files modified`);
1961
+ if (history.currentTask)
1962
+ memoryParts.push(`task: ${history.currentTask.slice(0, 40)}...`);
1963
+ if (memoryParts.length > 0) {
1964
+ console.log(colors.gray600(` ↻ ${memoryParts.join(' · ')}`));
1965
+ }
1966
+ }
1967
+ // SONA CODE Header - Retro pixel aesthetic
1968
+ const cwd = process.cwd();
1969
+ const shortCwd = cwd.split('/').slice(-2).join('/');
1970
+ const termRows = process.stdout.rows || 24;
1971
+ // Calculate header height (logo lines + status lines + gaps)
1972
+ const logoLines = SONA_LOGO.split('\n').filter(l => l.length > 0);
1973
+ const headerHeight = 1 + logoLines.length + 4 + 1; // top gap + logo + separator + version + path + help + bottom gap
1974
+ // Function to draw header
1975
+ const drawHeader = () => {
1976
+ // Save cursor position
1977
+ process.stdout.write('\x1b7');
1978
+ // Move to top
1979
+ process.stdout.write('\x1b[H');
1980
+ // Clear header area
1981
+ for (let i = 0; i < headerHeight; i++) {
1982
+ process.stdout.write('\x1b[2K\n'); // Clear line and move down
1983
+ }
1984
+ // Move back to top
1985
+ process.stdout.write('\x1b[H');
1986
+ // Top gap
1987
+ console.log('');
1988
+ // ASCII Logo - bold broken white
1989
+ logoLines.forEach(line => {
1990
+ console.log(colors.bold(colors.brokenWhite(line)));
1991
+ });
1992
+ // Thin line separator - gray 200
1993
+ console.log(colors.gray200(' ' + '─'.repeat(40)));
1994
+ // Status - compact, gray 200
1995
+ console.log(colors.gray200(` sona code v${VERSION} · ${provider}`));
1996
+ console.log(colors.gray500(` ${shortCwd}`));
1997
+ console.log(colors.gray700(` /help · /api · /quit`));
1998
+ // Bottom gap
1999
+ console.log('');
2000
+ // Restore cursor position
2001
+ process.stdout.write('\x1b8');
2002
+ };
2003
+ // Initial clear and setup
2004
+ process.stdout.write('\x1b[2J\x1b[H'); // Clear screen
2005
+ // Draw initial header
2006
+ // Top gap
2007
+ console.log('');
2008
+ // ASCII Logo - bold broken white
2009
+ logoLines.forEach(line => {
2010
+ console.log(colors.bold(colors.brokenWhite(line)));
2011
+ });
2012
+ // Thin line separator - gray 200
2013
+ console.log(colors.gray200(' ' + '─'.repeat(40)));
2014
+ // Status - compact, gray 200
2015
+ console.log(colors.gray200(` sona code v${VERSION} · ${provider}`));
2016
+ console.log(colors.gray500(` ${shortCwd}`));
2017
+ console.log(colors.gray700(` /help · /api · /quit`));
2018
+ // Bottom gap
2019
+ console.log('');
2020
+ // Set up scrolling region: from header bottom to terminal bottom
2021
+ process.stdout.write(`\x1b[${headerHeight + 1};${termRows}r`);
2022
+ // Move cursor to scrolling region
2023
+ process.stdout.write(`\x1b[${headerHeight + 1};1H`);
2024
+ const rl = (0, readline_1.createInterface)({
2025
+ input: process.stdin,
2026
+ output: process.stdout,
2027
+ });
2028
+ // Clean up on exit
2029
+ const cleanup = () => {
2030
+ process.stdout.write('\x1b[r'); // Reset scrolling region
2031
+ process.stdout.write('\x1b[?25h'); // Show cursor
2032
+ process.stdout.write('\x1b[2J\x1b[H'); // Clear screen
2033
+ };
2034
+ process.on('exit', cleanup);
2035
+ process.on('SIGINT', () => { cleanup(); process.exit(0); });
2036
+ // Redraw header periodically to keep it sticky (handles terminal resize too)
2037
+ const headerInterval = setInterval(() => {
2038
+ drawHeader();
2039
+ }, 5000); // Every 5 seconds
2040
+ // Also redraw on terminal resize
2041
+ process.stdout.on('resize', () => {
2042
+ const newRows = process.stdout.rows || 24;
2043
+ process.stdout.write(`\x1b[${headerHeight + 1};${newRows}r`);
2044
+ drawHeader();
2045
+ });
2046
+ const chat = async () => {
2047
+ rl.question(colors.brokenWhite('> '), async (input) => {
2048
+ const trimmed = input.trim();
2049
+ if (!trimmed) {
2050
+ chat();
2051
+ return;
2052
+ }
2053
+ // Handle commands
2054
+ if (trimmed.startsWith('/')) {
2055
+ const [cmd, ...args] = trimmed.slice(1).split(' ');
2056
+ switch (cmd) {
2057
+ case 'help':
2058
+ case 'h':
2059
+ console.log(colors.gray200(' ─────────────────────'));
2060
+ console.log(colors.gray500(' /api') + colors.gray700(' provider'));
2061
+ console.log(colors.gray500(' /model') + colors.gray700(' model'));
2062
+ console.log(colors.gray500(' /clear') + colors.gray700(' reset session'));
2063
+ console.log(colors.gray500(' /forget') + colors.gray700(' clear history'));
2064
+ console.log(colors.gray500(' /config') + colors.gray700(' config'));
2065
+ console.log(colors.gray500(' /quit') + colors.gray700(' exit'));
2066
+ console.log(colors.gray200(' ─────────────────────'));
2067
+ console.log(colors.gray400(' deepseek-chat') + colors.green(' $0.14'));
2068
+ console.log(colors.gray600(' gpt-4o-mini $15'));
2069
+ console.log(colors.gray600(' claude-sonnet $15'));
2070
+ break;
2071
+ case 'api':
2072
+ // Interactive API configuration
2073
+ console.log(colors.gray200(' ─────────────────────'));
2074
+ console.log(colors.gray400(' 1. DeepSeek') + colors.green(' $0.14'));
2075
+ console.log(colors.gray600(' 2. OpenAI $15'));
2076
+ console.log(colors.gray600(' 3. Anthropic $15'));
2077
+ const apiRl = (0, readline_1.createInterface)({
2078
+ input: process.stdin,
2079
+ output: process.stdout,
2080
+ });
2081
+ const apiQuestion = (prompt) => {
2082
+ return new Promise((resolve) => {
2083
+ apiRl.question(prompt, (answer) => resolve(answer.trim()));
2084
+ });
2085
+ };
2086
+ const providerChoice = await apiQuestion(colors.accent(` ${ui.arrowRight} Provider (1/2/3): `));
2087
+ let newProvider = 'deepseek';
2088
+ let newModel = 'deepseek-chat';
2089
+ switch (providerChoice) {
2090
+ case '2':
2091
+ newProvider = 'openai';
2092
+ newModel = 'gpt-4o-mini';
2093
+ console.log(colors.muted(' Get key at: ') + colors.info('https://platform.openai.com'));
2094
+ break;
2095
+ case '3':
2096
+ newProvider = 'anthropic';
2097
+ newModel = 'claude-3-5-sonnet-20241022';
2098
+ console.log(colors.muted(' Get key at: ') + colors.info('https://console.anthropic.com'));
2099
+ break;
2100
+ default:
2101
+ newProvider = 'deepseek';
2102
+ newModel = 'deepseek-chat';
2103
+ console.log(colors.muted(' Get key at: ') + colors.info('https://platform.deepseek.com'));
2104
+ }
2105
+ console.log('');
2106
+ const newKey = await apiQuestion(colors.accent(` ${ui.arrowRight} API Key: `));
2107
+ apiRl.close();
2108
+ if (newKey) {
2109
+ provider = newProvider;
2110
+ model = newModel;
2111
+ apiKey = newKey;
2112
+ // Save to config
2113
+ config = { ...config, provider, model, apiKey };
2114
+ saveConfig(config);
2115
+ console.log('');
2116
+ console.log(colors.success(` ${ui.check} Configuration saved!`));
2117
+ console.log(colors.muted(` Provider: ${provider}, Model: ${model}`));
2118
+ }
2119
+ else {
2120
+ console.log(colors.muted(' Cancelled.'));
2121
+ }
2122
+ console.log('');
2123
+ break;
2124
+ case 'config':
2125
+ console.log('');
2126
+ console.log(colors.title(' Config'));
2127
+ console.log(colors.dimGray(' ' + ui.horizontal.repeat(20)));
2128
+ console.log('');
2129
+ console.log(colors.small(' provider ') + colors.body(`${provider}`));
2130
+ console.log(colors.small(' model ') + colors.body(`${model}`));
2131
+ console.log(colors.small(' api key ') + colors.dimGray(`${apiKey.slice(0, 8)}...`));
2132
+ console.log(colors.small(' file ') + colors.dimGray(`~/.sona/config.json`));
2133
+ console.log('');
2134
+ break;
2135
+ case 'stats':
2136
+ case 's':
2137
+ const s = session.getStats();
2138
+ console.log('');
2139
+ console.log(colors.title(' Stats'));
2140
+ console.log(colors.dimGray(' ' + ui.horizontal.repeat(20)));
2141
+ console.log('');
2142
+ console.log(colors.small(' messages ') + colors.body(`${s.conversationTurns}`));
2143
+ console.log(colors.small(' saved ') + colors.success(`${s.totalInputTokensSaved}`) + colors.small(` tokens`));
2144
+ console.log(colors.small(' cost ') + colors.success(`$${s.estimatedInputCostSaved.toFixed(4)}`));
2145
+ console.log('');
2146
+ break;
2147
+ case 'clear':
2148
+ messages.length = 1; // Keep system prompt
2149
+ // Clear only the scrolling region (below header)
2150
+ process.stdout.write(`\x1b[${headerHeight + 1};1H`); // Move to scroll region start
2151
+ process.stdout.write('\x1b[J'); // Clear from cursor to end
2152
+ console.log(colors.gray700(' cleared'));
2153
+ break;
2154
+ case 'forget':
2155
+ // Clear saved history for this workspace
2156
+ messages.length = 1; // Keep system prompt
2157
+ try {
2158
+ const historyPath = getHistoryPath();
2159
+ if (fs.existsSync(historyPath)) {
2160
+ fs.unlinkSync(historyPath);
2161
+ }
2162
+ console.log(colors.gray700(' history cleared'));
2163
+ }
2164
+ catch {
2165
+ console.log(colors.gray700(' no history to clear'));
2166
+ }
2167
+ break;
2168
+ case 'model':
2169
+ if (args[0]) {
2170
+ model = args[0];
2171
+ // Auto-detect provider from model name
2172
+ if (model.startsWith('deepseek')) {
2173
+ provider = 'deepseek';
2174
+ }
2175
+ else if (model.startsWith('gpt') || model.startsWith('o1')) {
2176
+ provider = 'openai';
2177
+ }
2178
+ else if (model.startsWith('claude')) {
2179
+ provider = 'anthropic';
2180
+ }
2181
+ config = { ...config, model, provider };
2182
+ saveConfig(config);
2183
+ console.log(colors.success(` ${ui.check} Model: ${model} (${provider})`));
2184
+ }
2185
+ else {
2186
+ console.log(colors.muted(` Current: ${model}`));
2187
+ console.log(colors.muted(' Usage: /model <model-name>'));
2188
+ }
2189
+ console.log('');
2190
+ break;
2191
+ case 'pwd':
2192
+ console.log(colors.body(` ${process.cwd()}`));
2193
+ console.log('');
2194
+ break;
2195
+ case 'quit':
2196
+ case 'exit':
2197
+ case 'q':
2198
+ clearInterval(headerInterval);
2199
+ cleanup();
2200
+ // Flush any pending history save before exit
2201
+ if (pendingSave) {
2202
+ clearTimeout(pendingSave);
2203
+ saveHistory(messages, history);
2204
+ }
2205
+ console.log(colors.gray500(' bye'));
2206
+ rl.close();
2207
+ process.exit(0);
2208
+ default:
2209
+ console.log(colors.muted(` Unknown command: /${cmd}`));
2210
+ console.log(colors.muted(' Type /help for available commands.'));
2211
+ console.log('');
2212
+ }
2213
+ chat();
2214
+ return;
2215
+ }
2216
+ // Compress the user message
2217
+ let userContent = trimmed;
2218
+ let tokensSaved = 0;
2219
+ if (compress) {
2220
+ const result = session.compressTurn(trimmed);
2221
+ userContent = result.compressedText;
2222
+ tokensSaved = result.tokenSavings;
2223
+ }
2224
+ messages.push({ role: 'user', content: userContent });
2225
+ // Show compression indicator inline
2226
+ if (compress && tokensSaved > 0) {
2227
+ console.log(colors.gray(` [-${tokensSaved} tokens]`));
2228
+ }
2229
+ console.log('');
2230
+ try {
2231
+ // Shimmering * thinking indicator - gray 700
2232
+ let thinkingInterval = null;
2233
+ let shimmerFrame = 0;
2234
+ const shimmerFrames = ['*', '✦', '✧', '·', '✧', '✦']; // Shimmer animation
2235
+ const startThinking = () => {
2236
+ process.stdout.write(colors.gray700(` ${shimmerFrames[0]} thinking..`));
2237
+ thinkingInterval = setInterval(() => {
2238
+ shimmerFrame = (shimmerFrame + 1) % shimmerFrames.length;
2239
+ const dots = '.'.repeat((shimmerFrame % 3) + 1).padEnd(3);
2240
+ process.stdout.write(`\r` + colors.gray700(` ${shimmerFrames[shimmerFrame]} thinking${dots}`));
2241
+ }, 200);
2242
+ };
2243
+ const stopThinking = () => {
2244
+ if (thinkingInterval) {
2245
+ clearInterval(thinkingInterval);
2246
+ process.stdout.write('\r' + ' '.repeat(20) + '\r');
2247
+ }
2248
+ };
2249
+ startThinking();
2250
+ // Call LLM with tools
2251
+ let response = await callLLMWithTools(provider, model, messages, apiKey, TOOLS);
2252
+ stopThinking();
2253
+ // Handle tool calls
2254
+ while (response.tool_calls && response.tool_calls.length > 0) {
2255
+ // Show what the assistant is doing
2256
+ if (response.content) {
2257
+ console.log(colors.blue(' ') + response.content.split('\n').join('\n '));
2258
+ console.log('');
2259
+ }
2260
+ messages.push({
2261
+ role: 'assistant',
2262
+ content: response.content || '',
2263
+ tool_calls: response.tool_calls
2264
+ });
2265
+ // Execute each tool call
2266
+ for (const toolCall of response.tool_calls) {
2267
+ const toolName = toolCall.function.name;
2268
+ const toolArgs = JSON.parse(toolCall.function.arguments);
2269
+ const toolResult = await executeTool(toolName, toolArgs);
2270
+ messages.push({
2271
+ role: 'tool',
2272
+ tool_call_id: toolCall.id,
2273
+ content: toolResult,
2274
+ });
2275
+ }
2276
+ console.log('');
2277
+ startThinking();
2278
+ // Get next response
2279
+ response = await callLLMWithTools(provider, model, messages, apiKey, TOOLS);
2280
+ stopThinking();
2281
+ }
2282
+ // Print final response with premium formatting
2283
+ if (response.content) {
2284
+ console.log(formatResponse(response.content));
2285
+ }
2286
+ console.log('');
2287
+ messages.push({ role: 'assistant', content: response.content || '' });
2288
+ // Save conversation history after each exchange (debounced)
2289
+ saveHistory(messages, history);
2290
+ }
2291
+ catch (error) {
2292
+ console.log(colors.red(` Error: ${error.message}`));
2293
+ console.log('');
2294
+ messages.pop();
2295
+ }
2296
+ chat();
2297
+ });
2298
+ };
2299
+ chat();
2300
+ }
2301
+ /**
2302
+ * Chat command - alias for default behavior
2303
+ */
2304
+ program
2305
+ .command('chat')
2306
+ .alias('c')
2307
+ .description('Chat with LLM using automatic token compression')
2308
+ .option('--openai-key <key>', 'OpenAI API key')
2309
+ .option('--anthropic-key <key>', 'Anthropic API key')
2310
+ .option('-m, --model <model>', 'Model to use')
2311
+ .option('--provider <provider>', 'Provider: openai or anthropic')
2312
+ .option('-s, --system <prompt>', 'System prompt')
2313
+ .option('-p, --protect <terms...>', 'Terms to protect from compression')
2314
+ .option('--no-compress', 'Disable compression')
2315
+ .action(startChat);
2316
+ /**
2317
+ * Call LLM API with tool support
2318
+ */
2319
+ async function callLLMWithTools(provider, model, messages, apiKey, tools) {
2320
+ const https = await import('https');
2321
+ return new Promise((resolve, reject) => {
2322
+ if (provider === 'deepseek' || provider === 'openai') {
2323
+ // DeepSeek uses OpenAI-compatible API
2324
+ const hostname = provider === 'deepseek' ? 'api.deepseek.com' : 'api.openai.com';
2325
+ // Format messages for OpenAI/DeepSeek
2326
+ const formattedMessages = messages.map(m => {
2327
+ if (m.role === 'tool') {
2328
+ return { role: 'tool', tool_call_id: m.tool_call_id, content: m.content };
2329
+ }
2330
+ if (m.tool_calls) {
2331
+ return { role: 'assistant', content: m.content, tool_calls: m.tool_calls };
2332
+ }
2333
+ return { role: m.role, content: m.content };
2334
+ });
2335
+ const data = JSON.stringify({
2336
+ model,
2337
+ messages: formattedMessages,
2338
+ tools,
2339
+ tool_choice: 'auto',
2340
+ });
2341
+ const req = https.request({
2342
+ hostname,
2343
+ path: '/v1/chat/completions',
2344
+ method: 'POST',
2345
+ headers: {
2346
+ 'Content-Type': 'application/json',
2347
+ 'Authorization': `Bearer ${apiKey}`,
2348
+ },
2349
+ }, (res) => {
2350
+ let body = '';
2351
+ res.on('data', chunk => body += chunk);
2352
+ res.on('end', () => {
2353
+ try {
2354
+ const json = JSON.parse(body);
2355
+ if (json.error) {
2356
+ reject(new Error(json.error.message || JSON.stringify(json.error)));
2357
+ }
2358
+ else {
2359
+ const choice = json.choices[0];
2360
+ resolve({
2361
+ content: choice.message.content,
2362
+ tool_calls: choice.message.tool_calls,
2363
+ });
2364
+ }
2365
+ }
2366
+ catch (e) {
2367
+ reject(new Error('Failed to parse response: ' + body.slice(0, 200)));
2368
+ }
2369
+ });
2370
+ });
2371
+ req.on('error', reject);
2372
+ req.write(data);
2373
+ req.end();
2374
+ }
2375
+ else {
2376
+ // Anthropic with tools
2377
+ const systemMsg = messages.find(m => m.role === 'system');
2378
+ const chatMessages = messages
2379
+ .filter(m => m.role !== 'system')
2380
+ .map(m => {
2381
+ if (m.role === 'tool') {
2382
+ return {
2383
+ role: 'user',
2384
+ content: [{
2385
+ type: 'tool_result',
2386
+ tool_use_id: m.tool_call_id,
2387
+ content: m.content,
2388
+ }],
2389
+ };
2390
+ }
2391
+ if (m.tool_calls && m.tool_calls.length > 0) {
2392
+ return {
2393
+ role: 'assistant',
2394
+ content: m.tool_calls.map(tc => ({
2395
+ type: 'tool_use',
2396
+ id: tc.id,
2397
+ name: tc.function.name,
2398
+ input: JSON.parse(tc.function.arguments),
2399
+ })),
2400
+ };
2401
+ }
2402
+ return { role: m.role, content: m.content };
2403
+ });
2404
+ // Convert tools to Anthropic format
2405
+ const anthropicTools = tools.map((t) => ({
2406
+ name: t.function.name,
2407
+ description: t.function.description,
2408
+ input_schema: t.function.parameters,
2409
+ }));
2410
+ const data = JSON.stringify({
2411
+ model,
2412
+ max_tokens: 4096,
2413
+ system: systemMsg?.content,
2414
+ messages: chatMessages,
2415
+ tools: anthropicTools,
2416
+ });
2417
+ const req = https.request({
2418
+ hostname: 'api.anthropic.com',
2419
+ path: '/v1/messages',
2420
+ method: 'POST',
2421
+ headers: {
2422
+ 'Content-Type': 'application/json',
2423
+ 'x-api-key': apiKey,
2424
+ 'anthropic-version': '2023-06-01',
2425
+ },
2426
+ }, (res) => {
2427
+ let body = '';
2428
+ res.on('data', chunk => body += chunk);
2429
+ res.on('end', () => {
2430
+ try {
2431
+ const json = JSON.parse(body);
2432
+ if (json.error) {
2433
+ reject(new Error(json.error.message));
2434
+ }
2435
+ else {
2436
+ // Extract text and tool uses
2437
+ let content = '';
2438
+ const toolCalls = [];
2439
+ for (const block of json.content) {
2440
+ if (block.type === 'text') {
2441
+ content += block.text;
2442
+ }
2443
+ else if (block.type === 'tool_use') {
2444
+ toolCalls.push({
2445
+ id: block.id,
2446
+ function: {
2447
+ name: block.name,
2448
+ arguments: JSON.stringify(block.input),
2449
+ },
2450
+ });
2451
+ }
2452
+ }
2453
+ resolve({
2454
+ content: content || null,
2455
+ tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
2456
+ });
2457
+ }
2458
+ }
2459
+ catch (e) {
2460
+ reject(new Error('Failed to parse response: ' + body.slice(0, 200)));
2461
+ }
2462
+ });
2463
+ });
2464
+ req.on('error', reject);
2465
+ req.write(data);
2466
+ req.end();
2467
+ }
2468
+ });
2469
+ }
2470
+ /**
2471
+ * Session command - Start an interactive document processing session
2472
+ */
2473
+ program
2474
+ .command('session')
2475
+ .description('Start an interactive document processing session')
2476
+ .option('-p, --protect <terms...>', 'Terms to protect from compression')
2477
+ .action(async (opts) => {
2478
+ const session = (0, session_js_1.createSession)({
2479
+ protectedTerms: opts.protect || [],
2480
+ });
2481
+ console.log('');
2482
+ console.log(colors.cyan('═'.repeat(60)));
2483
+ console.log(colors.bold(' SONA CODE Document Session'));
2484
+ console.log(colors.cyan('═'.repeat(60)));
2485
+ console.log('');
2486
+ console.log(' Paste text to compress. Commands:');
2487
+ console.log(colors.gray(' /doc <name> Start a document'));
2488
+ console.log(colors.gray(' /end End document'));
2489
+ console.log(colors.gray(' /stats Show stats'));
2490
+ console.log(colors.gray(' /quit Exit'));
2491
+ console.log('');
2492
+ const rl = (0, readline_1.createInterface)({
2493
+ input: process.stdin,
2494
+ output: process.stdout,
2495
+ });
2496
+ let inDocument = false;
2497
+ const prompt = () => {
2498
+ rl.question(colors.cyan('> '), (line) => {
2499
+ const trimmed = line.trim();
2500
+ if (trimmed.startsWith('/')) {
2501
+ const [cmd, ...args] = trimmed.slice(1).split(' ');
2502
+ switch (cmd) {
2503
+ case 'doc':
2504
+ const docName = args.join(' ') || `doc_${Date.now()}`;
2505
+ session.startDocument(docName, docName);
2506
+ inDocument = true;
2507
+ console.log(colors.green(` Started: ${docName}`));
2508
+ break;
2509
+ case 'end':
2510
+ if (inDocument) {
2511
+ const stats = session.endDocument();
2512
+ if (stats)
2513
+ console.log(colors.green(` Done: ${stats.tokensSaved} tokens saved`));
2514
+ inDocument = false;
2515
+ }
2516
+ break;
2517
+ case 'stats':
2518
+ const s = session.getStats();
2519
+ console.log(` Saved: ${colors.green(s.totalInputTokensSaved + '')} tokens (${s.inputSavingsPercent.toFixed(1)}%)`);
2520
+ break;
2521
+ case 'quit':
2522
+ case 'exit':
2523
+ rl.close();
2524
+ return;
2525
+ }
2526
+ }
2527
+ else if (trimmed) {
2528
+ const result = inDocument ? session.compressChunk(trimmed) : session.compressTurn(trimmed);
2529
+ console.log(colors.dim(result.compressedText));
2530
+ console.log(colors.gray(` [-${result.tokenSavings} tokens]`));
2531
+ }
2532
+ prompt();
2533
+ });
2534
+ };
2535
+ prompt();
2536
+ });
2537
+ // ============================================================
2538
+ // COMPRESSION COMMANDS
2539
+ // ============================================================
2540
+ /**
2541
+ * Compress command - compress a file or stdin
2542
+ */
2543
+ program
2544
+ .command('compress')
2545
+ .description('Compress text to reduce tokens')
2546
+ .argument('[input]', 'Input file (use - for stdin)')
2547
+ .option('-o, --output <file>', 'Output file (default: stdout)')
2548
+ .option('-q, --quiet', 'Only output compressed text')
2549
+ .option('--json', 'Output as JSON with metrics')
2550
+ .option('-p, --protect <terms...>', 'Terms to protect from compression')
2551
+ .action(async (input, opts) => {
2552
+ try {
2553
+ let text;
2554
+ if (!input || input === '-') {
2555
+ text = await readStdin();
2556
+ }
2557
+ else {
2558
+ if (!(0, fs_1.existsSync)(input)) {
2559
+ console.error(colors.red(`Error: File not found: ${input}`));
2560
+ process.exit(1);
2561
+ }
2562
+ text = (0, fs_1.readFileSync)(input, 'utf-8');
2563
+ }
2564
+ const compressor = new compressor_js_1.SonaCompressor({
2565
+ protectedTerms: opts.protect || [],
2566
+ });
2567
+ const result = compressor.compress(text);
2568
+ if (opts.json) {
2569
+ const output = JSON.stringify({
2570
+ compressed: result.compressedText,
2571
+ metrics: {
2572
+ originalTokens: result.originalTokens,
2573
+ compressedTokens: result.compressedTokens,
2574
+ tokenSavings: result.tokenSavings,
2575
+ tokenSavingsPercent: Math.round(result.tokenSavingsPercent * 10) / 10,
2576
+ rulesApplied: result.rulesApplied.length,
2577
+ },
2578
+ }, null, 2);
2579
+ if (opts.output) {
2580
+ (0, fs_1.writeFileSync)(opts.output, output);
2581
+ }
2582
+ else {
2583
+ console.log(output);
2584
+ }
2585
+ }
2586
+ else if (opts.quiet) {
2587
+ if (opts.output) {
2588
+ (0, fs_1.writeFileSync)(opts.output, result.compressedText);
2589
+ }
2590
+ else {
2591
+ process.stdout.write(result.compressedText);
2592
+ }
2593
+ }
2594
+ else {
2595
+ if (opts.output) {
2596
+ (0, fs_1.writeFileSync)(opts.output, result.compressedText);
2597
+ }
2598
+ else {
2599
+ console.log(result.compressedText);
2600
+ }
2601
+ console.error('');
2602
+ console.error(colors.cyan('─'.repeat(40)));
2603
+ console.error(colors.bold('📊 Compression Results'));
2604
+ console.error(colors.cyan('─'.repeat(40)));
2605
+ console.error(` Original tokens: ${colors.yellow(result.originalTokens.toString())}`);
2606
+ console.error(` Compressed tokens: ${colors.green(result.compressedTokens.toString())}`);
2607
+ console.error(` Tokens saved: ${colors.bold(colors.green(`${result.tokenSavings} (${result.tokenSavingsPercent.toFixed(1)}%)`))}`);
2608
+ console.error(` Rules applied: ${result.rulesApplied.length}`);
2609
+ console.error(colors.cyan('─'.repeat(40)));
2610
+ }
2611
+ }
2612
+ catch (error) {
2613
+ console.error(colors.red(`Error: ${error.message}`));
2614
+ process.exit(1);
2615
+ }
2616
+ });
2617
+ /**
2618
+ * Analyze command
2619
+ */
2620
+ program
2621
+ .command('analyze')
2622
+ .description('Analyze text and show compression potential')
2623
+ .argument('<input>', 'Input file')
2624
+ .option('--json', 'Output as JSON')
2625
+ .action(async (input, opts) => {
2626
+ try {
2627
+ if (!(0, fs_1.existsSync)(input)) {
2628
+ console.error(colors.red(`Error: File not found: ${input}`));
2629
+ process.exit(1);
2630
+ }
2631
+ const text = (0, fs_1.readFileSync)(input, 'utf-8');
2632
+ const result = (0, compressor_js_1.compress)(text);
2633
+ if (opts.json) {
2634
+ console.log(JSON.stringify({
2635
+ file: input,
2636
+ analysis: {
2637
+ originalChars: result.originalChars,
2638
+ compressedChars: result.compressedChars,
2639
+ charSavings: result.charSavings,
2640
+ charSavingsPercent: Math.round(result.charSavingsPercent * 10) / 10,
2641
+ originalTokens: result.originalTokens,
2642
+ compressedTokens: result.compressedTokens,
2643
+ tokenSavings: result.tokenSavings,
2644
+ tokenSavingsPercent: Math.round(result.tokenSavingsPercent * 10) / 10,
2645
+ rulesApplied: result.rulesApplied,
2646
+ estimatedCostSavings: result.estimatedCostSavings,
2647
+ },
2648
+ }, null, 2));
2649
+ }
2650
+ else {
2651
+ console.log('');
2652
+ console.log(colors.cyan('═'.repeat(50)));
2653
+ console.log(colors.bold(`📋 Analysis: ${input}`));
2654
+ console.log(colors.cyan('═'.repeat(50)));
2655
+ console.log('');
2656
+ console.log(colors.bold('Characters:'));
2657
+ console.log(` Original: ${result.originalChars.toLocaleString()}`);
2658
+ console.log(` Compressed: ${result.compressedChars.toLocaleString()}`);
2659
+ console.log(` Savings: ${colors.green(`${result.charSavings.toLocaleString()} (${result.charSavingsPercent.toFixed(1)}%)`)}`);
2660
+ console.log('');
2661
+ console.log(colors.bold('Tokens:'));
2662
+ console.log(` Original: ${result.originalTokens.toLocaleString()}`);
2663
+ console.log(` Compressed: ${result.compressedTokens.toLocaleString()}`);
2664
+ console.log(` Savings: ${colors.green(`${result.tokenSavings.toLocaleString()} (${result.tokenSavingsPercent.toFixed(1)}%)`)}`);
2665
+ console.log('');
2666
+ console.log(colors.bold('Cost Impact (at $15/1M tokens):'));
2667
+ console.log(` Savings per request: ${colors.green(`$${result.estimatedCostSavings.toFixed(6)}`)}`);
2668
+ console.log('');
2669
+ console.log(colors.bold('Rules Applied:'));
2670
+ if (result.rulesApplied.length === 0) {
2671
+ console.log(colors.gray(' (none - text already optimal)'));
2672
+ }
2673
+ else {
2674
+ result.rulesApplied.slice(0, 10).forEach(rule => {
2675
+ console.log(` • ${rule}`);
2676
+ });
2677
+ if (result.rulesApplied.length > 10) {
2678
+ console.log(colors.gray(` ... and ${result.rulesApplied.length - 10} more`));
2679
+ }
2680
+ }
2681
+ console.log('');
2682
+ console.log(colors.cyan('═'.repeat(50)));
2683
+ }
2684
+ }
2685
+ catch (error) {
2686
+ console.error(colors.red(`Error: ${error.message}`));
2687
+ process.exit(1);
2688
+ }
2689
+ });
2690
+ /**
2691
+ * Pipe command
2692
+ */
2693
+ program
2694
+ .command('pipe')
2695
+ .description('Process stdin continuously (for piping)')
2696
+ .option('-p, --protect <terms...>', 'Terms to protect from compression')
2697
+ .action(async (opts) => {
2698
+ const compressor = new compressor_js_1.SonaCompressor({
2699
+ protectedTerms: opts.protect || [],
2700
+ });
2701
+ const rl = (0, readline_1.createInterface)({
2702
+ input: process.stdin,
2703
+ output: process.stdout,
2704
+ terminal: false,
2705
+ });
2706
+ rl.on('line', (line) => {
2707
+ const result = compressor.compress(line);
2708
+ console.log(result.compressedText);
2709
+ });
2710
+ });
2711
+ // ============================================================
2712
+ // MCP SERVER
2713
+ // ============================================================
2714
+ /**
2715
+ * MCP command
2716
+ */
2717
+ program
2718
+ .command('mcp')
2719
+ .description('Start MCP server for Claude Code / Codex integration')
2720
+ .option('--stdio', 'Use stdio transport (default)')
2721
+ .action(async () => {
2722
+ await startMcpServer();
2723
+ });
2724
+ // ============================================================
2725
+ // INFO & INTERACTIVE
2726
+ // ============================================================
2727
+ /**
2728
+ * Info command
2729
+ */
2730
+ program
2731
+ .command('info')
2732
+ .description('Show SONA CODE information and configuration')
2733
+ .action(() => {
2734
+ console.log('');
2735
+ console.log(colors.cyan('═'.repeat(60)));
2736
+ console.log(colors.bold(' SONA CODE - SMR Token Compression'));
2737
+ console.log(colors.cyan('═'.repeat(60)));
2738
+ console.log('');
2739
+ console.log(` Version: ${colors.green(VERSION)}`);
2740
+ console.log(` Rules loaded: ${colors.green((0, rules_js_1.getRuleCount)().toString())}`);
2741
+ console.log('');
2742
+ console.log(colors.bold(' Integration Options:'));
2743
+ console.log('');
2744
+ console.log(` ${colors.cyan('1.')} ${colors.bold('Transparent Proxy')} ${colors.gray('(recommended)')}`);
2745
+ console.log(' Start proxy and route all LLM traffic through it:');
2746
+ console.log(colors.dim(' $ sona start'));
2747
+ console.log(colors.dim(' $ eval $(sona env)'));
2748
+ console.log('');
2749
+ console.log(` ${colors.cyan('2.')} ${colors.bold('Wrap Command')}`);
2750
+ console.log(' Run any command with compression enabled:');
2751
+ console.log(colors.dim(' $ sona wrap codex "explain this code"'));
2752
+ console.log(colors.dim(' $ sona wrap claude "summarize file.txt"'));
2753
+ console.log('');
2754
+ console.log(` ${colors.cyan('3.')} ${colors.bold('SDK Middleware')}`);
2755
+ console.log(' Wrap OpenAI/Anthropic SDKs in your code:');
2756
+ console.log(colors.dim(" import { createMiddleware } from 'sona-code';"));
2757
+ console.log(colors.dim(' const openai = createMiddleware().wrapOpenAI(new OpenAI());'));
2758
+ console.log('');
2759
+ console.log(` ${colors.cyan('4.')} ${colors.bold('MCP Server')}`);
2760
+ console.log(' For Claude Code / Codex native integration:');
2761
+ console.log(colors.dim(' $ sona mcp'));
2762
+ console.log('');
2763
+ console.log(` ${colors.cyan('5.')} ${colors.bold('CLI Piping')}`);
2764
+ console.log(' Compress text before sending to any tool:');
2765
+ console.log(colors.dim(' $ cat prompt.txt | sona compress -q | your-tool'));
2766
+ console.log('');
2767
+ console.log(colors.cyan('═'.repeat(60)));
2768
+ console.log('');
2769
+ });
2770
+ /**
2771
+ * Interactive mode
2772
+ */
2773
+ program
2774
+ .command('interactive')
2775
+ .alias('i')
2776
+ .description('Start interactive compression mode')
2777
+ .action(async () => {
2778
+ const compressor = new compressor_js_1.SonaCompressor();
2779
+ console.log('');
2780
+ console.log(colors.cyan('═'.repeat(50)));
2781
+ console.log(colors.bold('SONA CODE Interactive Mode'));
2782
+ console.log(colors.cyan('═'.repeat(50)));
2783
+ console.log('');
2784
+ console.log('Enter text to compress. Press Ctrl+D to exit.');
2785
+ console.log('Use /stats to see session statistics.');
2786
+ console.log('');
2787
+ const rl = (0, readline_1.createInterface)({
2788
+ input: process.stdin,
2789
+ output: process.stdout,
2790
+ });
2791
+ let totalSaved = 0;
2792
+ let totalProcessed = 0;
2793
+ const prompt = () => {
2794
+ rl.question(colors.cyan('> '), (input) => {
2795
+ if (input === '/stats') {
2796
+ console.log('');
2797
+ console.log(colors.bold('Session Statistics:'));
2798
+ console.log(` Requests: ${totalProcessed}`);
2799
+ console.log(` Tokens saved: ${colors.green(totalSaved.toString())}`);
2800
+ console.log('');
2801
+ prompt();
2802
+ return;
2803
+ }
2804
+ if (input === '/exit' || input === '/quit') {
2805
+ console.log(colors.gray('Goodbye!'));
2806
+ rl.close();
2807
+ return;
2808
+ }
2809
+ const result = compressor.compress(input);
2810
+ totalProcessed++;
2811
+ totalSaved += result.tokenSavings;
2812
+ console.log('');
2813
+ console.log(colors.bold('Compressed:'));
2814
+ console.log(result.compressedText);
2815
+ console.log('');
2816
+ console.log(colors.gray(`[${result.tokenSavings} tokens saved (${result.tokenSavingsPercent.toFixed(1)}%)]`));
2817
+ console.log('');
2818
+ prompt();
2819
+ });
2820
+ };
2821
+ prompt();
2822
+ });
2823
+ // ============================================================
2824
+ // HELPERS
2825
+ // ============================================================
2826
+ async function readStdin() {
2827
+ return new Promise((resolve, reject) => {
2828
+ let data = '';
2829
+ process.stdin.setEncoding('utf8');
2830
+ process.stdin.on('readable', () => {
2831
+ let chunk;
2832
+ while ((chunk = process.stdin.read()) !== null) {
2833
+ data += chunk;
2834
+ }
2835
+ });
2836
+ process.stdin.on('end', () => resolve(data));
2837
+ process.stdin.on('error', reject);
2838
+ });
2839
+ }
2840
+ /**
2841
+ * Split text into chunks, trying to break at paragraph/sentence boundaries
2842
+ */
2843
+ function splitIntoChunks(text, maxChunkSize) {
2844
+ const chunks = [];
2845
+ const paragraphs = text.split(/\n\n+/);
2846
+ let currentChunk = '';
2847
+ for (const para of paragraphs) {
2848
+ if (currentChunk.length + para.length + 2 <= maxChunkSize) {
2849
+ currentChunk += (currentChunk ? '\n\n' : '') + para;
2850
+ }
2851
+ else {
2852
+ // Current chunk is full, save it
2853
+ if (currentChunk) {
2854
+ chunks.push(currentChunk);
2855
+ }
2856
+ // If paragraph itself is too long, split by sentences
2857
+ if (para.length > maxChunkSize) {
2858
+ const sentences = para.split(/(?<=[.!?])\s+/);
2859
+ currentChunk = '';
2860
+ for (const sentence of sentences) {
2861
+ if (currentChunk.length + sentence.length + 1 <= maxChunkSize) {
2862
+ currentChunk += (currentChunk ? ' ' : '') + sentence;
2863
+ }
2864
+ else {
2865
+ if (currentChunk) {
2866
+ chunks.push(currentChunk);
2867
+ }
2868
+ // If even a single sentence is too long, just truncate
2869
+ if (sentence.length > maxChunkSize) {
2870
+ chunks.push(sentence.slice(0, maxChunkSize));
2871
+ }
2872
+ else {
2873
+ currentChunk = sentence;
2874
+ }
2875
+ }
2876
+ }
2877
+ }
2878
+ else {
2879
+ currentChunk = para;
2880
+ }
2881
+ }
2882
+ }
2883
+ // Don't forget the last chunk
2884
+ if (currentChunk) {
2885
+ chunks.push(currentChunk);
2886
+ }
2887
+ return chunks;
2888
+ }
2889
+ async function startMcpServer() {
2890
+ const compressor = new compressor_js_1.SonaCompressor();
2891
+ const rl = (0, readline_1.createInterface)({
2892
+ input: process.stdin,
2893
+ output: process.stdout,
2894
+ terminal: false,
2895
+ });
2896
+ const handleMessage = (line) => {
2897
+ try {
2898
+ const message = JSON.parse(line);
2899
+ if (message.method === 'initialize') {
2900
+ respond(message.id, {
2901
+ protocolVersion: '2024-11-05',
2902
+ capabilities: { tools: {} },
2903
+ serverInfo: { name: 'sona', version: VERSION },
2904
+ });
2905
+ }
2906
+ else if (message.method === 'tools/list') {
2907
+ respond(message.id, {
2908
+ tools: [
2909
+ {
2910
+ name: 'compress',
2911
+ description: 'Compress text to reduce LLM tokens by removing verbose phrases while preserving meaning. Reduces costs by ~20%.',
2912
+ inputSchema: {
2913
+ type: 'object',
2914
+ properties: {
2915
+ text: { type: 'string', description: 'The text to compress' },
2916
+ protectedTerms: { type: 'array', items: { type: 'string' }, description: 'Terms that should not be modified' },
2917
+ },
2918
+ required: ['text'],
2919
+ },
2920
+ },
2921
+ {
2922
+ name: 'analyze',
2923
+ description: 'Analyze text and show compression potential without modifying it',
2924
+ inputSchema: {
2925
+ type: 'object',
2926
+ properties: { text: { type: 'string', description: 'The text to analyze' } },
2927
+ required: ['text'],
2928
+ },
2929
+ },
2930
+ ],
2931
+ });
2932
+ }
2933
+ else if (message.method === 'tools/call') {
2934
+ const { name, arguments: args } = message.params;
2935
+ if (name === 'compress') {
2936
+ const comp = new compressor_js_1.SonaCompressor({ protectedTerms: args.protectedTerms || [] });
2937
+ const result = comp.compress(args.text);
2938
+ respond(message.id, {
2939
+ content: [{
2940
+ type: 'text',
2941
+ text: JSON.stringify({
2942
+ compressed: result.compressedText,
2943
+ originalTokens: result.originalTokens,
2944
+ compressedTokens: result.compressedTokens,
2945
+ tokensSaved: result.tokenSavings,
2946
+ savingsPercent: `${result.tokenSavingsPercent.toFixed(1)}%`,
2947
+ }, null, 2),
2948
+ }],
2949
+ });
2950
+ }
2951
+ else if (name === 'analyze') {
2952
+ const result = (0, compressor_js_1.compress)(args.text);
2953
+ respond(message.id, {
2954
+ content: [{
2955
+ type: 'text',
2956
+ text: JSON.stringify({
2957
+ originalTokens: result.originalTokens,
2958
+ compressedTokens: result.compressedTokens,
2959
+ potentialSavings: result.tokenSavings,
2960
+ savingsPercent: `${result.tokenSavingsPercent.toFixed(1)}%`,
2961
+ rulesMatched: result.rulesApplied.length,
2962
+ }, null, 2),
2963
+ }],
2964
+ });
2965
+ }
2966
+ }
2967
+ }
2968
+ catch {
2969
+ // Ignore parse errors
2970
+ }
2971
+ };
2972
+ const respond = (id, result) => {
2973
+ console.log(JSON.stringify({ jsonrpc: '2.0', id, result }));
2974
+ };
2975
+ rl.on('line', handleMessage);
2976
+ await new Promise(() => { });
2977
+ }
2978
+ program.parse();
2979
+ //# sourceMappingURL=cli.js.map