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/README.md +119 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2979 -0
- package/dist/cli.js.map +1 -0
- package/dist/compressor.d.ts +84 -0
- package/dist/compressor.d.ts.map +1 -0
- package/dist/compressor.js +237 -0
- package/dist/compressor.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +81 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +165 -0
- package/dist/middleware.js.map +1 -0
- package/dist/proxy.d.ts +105 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +548 -0
- package/dist/proxy.js.map +1 -0
- package/dist/rules.d.ts +30 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +201 -0
- package/dist/rules.js.map +1 -0
- package/dist/session.d.ts +156 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +351 -0
- package/dist/session.js.map +1 -0
- package/package.json +51 -0
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
|