@semalt-ai/code 1.4.4 → 1.5.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/index.js +53 -1345
- package/lib/agent.js +81 -0
- package/lib/api.js +282 -0
- package/lib/args.js +45 -0
- package/lib/commands.js +344 -0
- package/lib/config.js +46 -0
- package/lib/constants.js +27 -0
- package/lib/context.js +71 -0
- package/lib/permissions.js +93 -0
- package/lib/prompts.js +29 -0
- package/lib/tools.js +120 -0
- package/lib/ui.js +457 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,1353 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const {
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const {
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const DEFAULT_API_TIMEOUT_MS = 15 * 60 * 1000;
|
|
16
|
-
|
|
17
|
-
// ── Config ────────────────────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
const DEFAULT_CONFIG = {
|
|
20
|
-
api_base: 'http://127.0.0.1:8800',
|
|
21
|
-
api_key: 'any',
|
|
22
|
-
default_model: 'default',
|
|
23
|
-
temperature: 0.7,
|
|
24
|
-
request_timeout_ms: DEFAULT_API_TIMEOUT_MS,
|
|
25
|
-
stream: true,
|
|
26
|
-
models: [],
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const CONFIG_PATH = path.join(os.homedir(), '.semalt-ai', 'config.json');
|
|
30
|
-
|
|
31
|
-
function normalizeConfig(cfg = {}) {
|
|
32
|
-
const merged = { ...DEFAULT_CONFIG, ...cfg };
|
|
33
|
-
merged.models = Array.isArray(cfg.models)
|
|
34
|
-
? cfg.models
|
|
35
|
-
.filter((entry) => entry &&
|
|
36
|
-
typeof entry.api_base === 'string' &&
|
|
37
|
-
typeof entry.api_key === 'string' &&
|
|
38
|
-
typeof entry.model === 'string' &&
|
|
39
|
-
entry.api_base.trim() &&
|
|
40
|
-
entry.model.trim())
|
|
41
|
-
.map((entry) => ({
|
|
42
|
-
api_base: entry.api_base.trim(),
|
|
43
|
-
api_key: entry.api_key,
|
|
44
|
-
model: entry.model.trim(),
|
|
45
|
-
}))
|
|
46
|
-
: [];
|
|
47
|
-
return merged;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function loadConfig() {
|
|
51
|
-
if (fs.existsSync(CONFIG_PATH)) {
|
|
52
|
-
try {
|
|
53
|
-
const data = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
54
|
-
return normalizeConfig(data);
|
|
55
|
-
} catch {}
|
|
56
|
-
}
|
|
57
|
-
return normalizeConfig();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function saveConfig(cfg) {
|
|
61
|
-
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
62
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(normalizeConfig(cfg), null, 2));
|
|
63
|
-
}
|
|
4
|
+
const { PACKAGE_JSON } = require('./lib/constants');
|
|
5
|
+
const { loadConfig, saveConfig } = require('./lib/config');
|
|
6
|
+
const ui = require('./lib/ui');
|
|
7
|
+
const { createPermissionManager } = require('./lib/permissions');
|
|
8
|
+
const { createToolExecutor, extractToolCalls } = require('./lib/tools');
|
|
9
|
+
const { readFileContext } = require('./lib/context');
|
|
10
|
+
const { createApiClient } = require('./lib/api');
|
|
11
|
+
const { createAgentRunner } = require('./lib/agent');
|
|
12
|
+
const { createCommands } = require('./lib/commands');
|
|
13
|
+
const { parseArgs } = require('./lib/args');
|
|
14
|
+
const { CONFIG_PATH } = require('./lib/constants');
|
|
64
15
|
|
|
65
16
|
let config = loadConfig();
|
|
66
17
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
function getCols() {
|
|
70
|
-
return process.stdout.columns || 80;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const RST = '\x1b[0m';
|
|
74
|
-
const BOLD = '\x1b[1m';
|
|
75
|
-
const DIM = '\x1b[2m';
|
|
76
|
-
|
|
77
|
-
const FG_GRAY = '\x1b[38;5;245m';
|
|
78
|
-
const FG_DARK = '\x1b[38;5;240m';
|
|
79
|
-
const FG_BLUE = '\x1b[38;5;75m';
|
|
80
|
-
const FG_CYAN = '\x1b[38;5;116m';
|
|
81
|
-
const FG_GREEN = '\x1b[38;5;114m';
|
|
82
|
-
const FG_YELLOW = '\x1b[38;5;222m';
|
|
83
|
-
const FG_RED = '\x1b[38;5;203m';
|
|
84
|
-
const FG_TEAL = '\x1b[38;5;73m';
|
|
85
|
-
|
|
86
|
-
const BOX_H = '─';
|
|
87
|
-
const BOX_V = '│';
|
|
88
|
-
const BOX_TL = '╭';
|
|
89
|
-
const BOX_TR = '╮';
|
|
90
|
-
const BOX_BL = '╰';
|
|
91
|
-
const BOX_BR = '╯';
|
|
92
|
-
|
|
93
|
-
function hr(char = '─', color = FG_DARK) {
|
|
94
|
-
process.stdout.write(`${color}${char.repeat(getCols())}${RST}\n`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function stripAnsi(str) {
|
|
98
|
-
return str.replace(/\x1b\[[^m]*m/g, '');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function boxLine(text, width) {
|
|
102
|
-
const w = width || (getCols() - 4);
|
|
103
|
-
const visible = stripAnsi(text).length;
|
|
104
|
-
const pad = Math.max(0, w - visible);
|
|
105
|
-
return ` ${FG_DARK}${BOX_V}${RST} ${text}${' '.repeat(pad)}${FG_DARK}${BOX_V}${RST}`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function dropLastChar(text) {
|
|
109
|
-
const chars = Array.from(text || '');
|
|
110
|
-
chars.pop();
|
|
111
|
-
return chars.join('');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function insertCharAt(text, index, value) {
|
|
115
|
-
const chars = Array.from(text || '');
|
|
116
|
-
chars.splice(index, 0, value);
|
|
117
|
-
return chars.join('');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function removeCharAt(text, index) {
|
|
121
|
-
const chars = Array.from(text || '');
|
|
122
|
-
chars.splice(index, 1);
|
|
123
|
-
return chars.join('');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function isPrintableKey(str, key = {}) {
|
|
127
|
-
if (!str || key.ctrl || key.meta) return false;
|
|
128
|
-
if (key.name === 'return' || key.name === 'enter' || key.name === 'tab') return false;
|
|
129
|
-
if (key.name && ['up', 'down', 'left', 'right', 'home', 'end', 'pageup', 'pagedown', 'escape', 'delete', 'backspace'].includes(key.name)) {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
return !/[\x00-\x1f\x7f]/.test(str);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function readInteractiveInput(promptText, options = {}) {
|
|
136
|
-
const {
|
|
137
|
-
allowed = null,
|
|
138
|
-
immediate = false,
|
|
139
|
-
trim = false,
|
|
140
|
-
allowCursorNavigation = false,
|
|
141
|
-
} = options;
|
|
142
|
-
|
|
143
|
-
return new Promise((resolve) => {
|
|
144
|
-
if (!process.stdin.isTTY) {
|
|
145
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
146
|
-
rl.question(promptText, (answer) => {
|
|
147
|
-
rl.close();
|
|
148
|
-
resolve({ type: 'submit', value: trim ? (answer || '').trim() : (answer || '') });
|
|
149
|
-
});
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const wasRaw = typeof process.stdin.isRaw === 'boolean' ? process.stdin.isRaw : false;
|
|
154
|
-
let buffer = '';
|
|
155
|
-
let cursor = 0;
|
|
156
|
-
let done = false;
|
|
157
|
-
|
|
158
|
-
readline.emitKeypressEvents(process.stdin);
|
|
159
|
-
process.stdin.setRawMode(true);
|
|
160
|
-
process.stdin.resume();
|
|
161
|
-
|
|
162
|
-
const render = () => {
|
|
163
|
-
readline.cursorTo(process.stdout, 0);
|
|
164
|
-
readline.clearLine(process.stdout, 0);
|
|
165
|
-
process.stdout.write(`${promptText}${buffer}`);
|
|
166
|
-
const promptWidth = stripAnsi(promptText).length;
|
|
167
|
-
readline.cursorTo(process.stdout, promptWidth + cursor);
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const finish = (result, addNewline = true) => {
|
|
171
|
-
if (done) return;
|
|
172
|
-
done = true;
|
|
173
|
-
process.stdin.setRawMode(wasRaw);
|
|
174
|
-
process.stdin.removeListener('keypress', onKeypress);
|
|
175
|
-
if (addNewline) process.stdout.write('\n');
|
|
176
|
-
resolve(result);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const onKeypress = (str, key = {}) => {
|
|
180
|
-
if (key.ctrl && key.name === 'c') {
|
|
181
|
-
if (buffer) {
|
|
182
|
-
buffer = '';
|
|
183
|
-
cursor = 0;
|
|
184
|
-
render();
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
finish({ type: 'sigint' }, false);
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (key.ctrl && key.name === 'd') {
|
|
192
|
-
if (!buffer) {
|
|
193
|
-
finish({ type: 'eof' }, false);
|
|
194
|
-
}
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (key.name === 'return' || key.name === 'enter') {
|
|
199
|
-
finish({ type: 'submit', value: trim ? buffer.trim() : buffer });
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (key.name === 'backspace' || key.name === 'delete') {
|
|
204
|
-
if (key.name === 'backspace' && cursor > 0) {
|
|
205
|
-
buffer = removeCharAt(buffer, cursor - 1);
|
|
206
|
-
cursor--;
|
|
207
|
-
render();
|
|
208
|
-
} else if (key.name === 'delete' && cursor < Array.from(buffer).length) {
|
|
209
|
-
buffer = removeCharAt(buffer, cursor);
|
|
210
|
-
render();
|
|
211
|
-
}
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (allowCursorNavigation && key.name === 'left') {
|
|
216
|
-
if (cursor > 0) {
|
|
217
|
-
cursor--;
|
|
218
|
-
render();
|
|
219
|
-
}
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (allowCursorNavigation && key.name === 'right') {
|
|
224
|
-
if (cursor < Array.from(buffer).length) {
|
|
225
|
-
cursor++;
|
|
226
|
-
render();
|
|
227
|
-
}
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (allowCursorNavigation && key.name === 'home') {
|
|
232
|
-
cursor = 0;
|
|
233
|
-
render();
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (allowCursorNavigation && key.name === 'end') {
|
|
238
|
-
cursor = Array.from(buffer).length;
|
|
239
|
-
render();
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (key.name && ['up', 'down', 'left', 'right', 'home', 'end', 'pageup', 'pagedown', 'escape', 'tab'].includes(key.name)) {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (!isPrintableKey(str, key)) return;
|
|
248
|
-
|
|
249
|
-
if (allowed && !allowed.includes(str)) return;
|
|
250
|
-
|
|
251
|
-
if (immediate) {
|
|
252
|
-
buffer = str;
|
|
253
|
-
cursor = Array.from(buffer).length;
|
|
254
|
-
render();
|
|
255
|
-
finish({ type: 'submit', value: str });
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
buffer = insertCharAt(buffer, cursor, str);
|
|
260
|
-
cursor++;
|
|
261
|
-
render();
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
process.stdin.on('keypress', onKeypress);
|
|
265
|
-
render();
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// ── Permission system ─────────────────────────────────────────────────────────
|
|
270
|
-
|
|
271
|
-
let AUTO_APPROVE_SHELL = false;
|
|
272
|
-
let AUTO_APPROVE_FILE = false;
|
|
273
|
-
|
|
274
|
-
function askPermissionLine(actionType) {
|
|
275
|
-
return actionType === 'shell'
|
|
276
|
-
? ' 1. Yes 2. Yes, always for shell 3. No'
|
|
277
|
-
: ' 1. Yes 2. Yes, always for files 3. No';
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function readPermissionChoice() {
|
|
281
|
-
return readInteractiveInput(` ${FG_YELLOW}?${RST} `, {
|
|
282
|
-
allowed: ['1', '2', '3'],
|
|
283
|
-
immediate: true,
|
|
284
|
-
onEmptyCtrlC: 'signal',
|
|
285
|
-
trim: true,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function askPermission(actionType, description) {
|
|
290
|
-
return new Promise((resolve) => {
|
|
291
|
-
if (actionType === 'shell' && AUTO_APPROVE_SHELL) {
|
|
292
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approved: ${description}${RST}`);
|
|
293
|
-
return resolve(true);
|
|
294
|
-
}
|
|
295
|
-
if (actionType === 'file' && AUTO_APPROVE_FILE) {
|
|
296
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approved: ${description}${RST}`);
|
|
297
|
-
return resolve(true);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
console.log();
|
|
301
|
-
console.log(` ${FG_YELLOW}${BOLD}⚠ Permission required${RST}`);
|
|
302
|
-
console.log(` ${FG_GRAY}${actionType}: ${description}${RST}`);
|
|
303
|
-
console.log();
|
|
304
|
-
console.log(` ${FG_CYAN}${askPermissionLine(actionType)}${RST}`);
|
|
305
|
-
console.log();
|
|
306
|
-
|
|
307
|
-
readPermissionChoice().then((result) => {
|
|
308
|
-
if (result.type === 'sigint' || result.type === 'eof') {
|
|
309
|
-
console.log(` ${FG_RED}✗${RST} ${FG_DARK}Denied${RST}`);
|
|
310
|
-
resolve(false);
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const choice = (result.value || '').trim().toLowerCase();
|
|
315
|
-
if (choice === '1' || choice === 'y' || choice === 'yes') {
|
|
316
|
-
resolve(true);
|
|
317
|
-
} else if (choice === '2' || choice === 'a' || choice === 'always') {
|
|
318
|
-
if (actionType === 'shell') AUTO_APPROVE_SHELL = true;
|
|
319
|
-
else AUTO_APPROVE_FILE = true;
|
|
320
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approve enabled for ${actionType} operations${RST}`);
|
|
321
|
-
resolve(true);
|
|
322
|
-
} else {
|
|
323
|
-
console.log(` ${FG_RED}✗${RST} ${FG_DARK}Denied${RST}`);
|
|
324
|
-
resolve(false);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// ── Agent: execute tools ──────────────────────────────────────────────────────
|
|
331
|
-
|
|
332
|
-
async function agentExecShell(command) {
|
|
333
|
-
const approved = await askPermission('shell', command);
|
|
334
|
-
if (!approved) {
|
|
335
|
-
return { exit_code: -1, stdout: '', stderr: 'Permission denied by user' };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
console.log(` ${FG_DARK}$ ${command}${RST}`);
|
|
339
|
-
try {
|
|
340
|
-
const result = spawnSync(command, { shell: true, encoding: 'utf8', timeout: 60000 });
|
|
341
|
-
const stdout = result.stdout || '';
|
|
342
|
-
const stderr = result.stderr || '';
|
|
343
|
-
const combined = stdout + (stderr ? '\n' + stderr : '');
|
|
344
|
-
const lines = combined.trim().split('\n').filter(l => l !== '');
|
|
345
|
-
|
|
346
|
-
if (lines.length > 20) {
|
|
347
|
-
lines.slice(0, 15).forEach(l => console.log(` ${FG_GRAY}${l}${RST}`));
|
|
348
|
-
console.log(` ${FG_DARK}... (${lines.length - 15} more lines)${RST}`);
|
|
349
|
-
} else {
|
|
350
|
-
lines.forEach(l => console.log(` ${FG_GRAY}${l}${RST}`));
|
|
351
|
-
}
|
|
352
|
-
console.log();
|
|
353
|
-
|
|
354
|
-
return { exit_code: result.status ?? 0, stdout, stderr };
|
|
355
|
-
} catch (e) {
|
|
356
|
-
console.log(` ${FG_RED}✗ ${e.message}${RST}`);
|
|
357
|
-
return { exit_code: -1, stdout: '', stderr: e.message };
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
async function agentExecFile(action, filePath, content = null) {
|
|
362
|
-
if (action === 'read') {
|
|
363
|
-
const approved = await askPermission('file', `Read ${filePath}`);
|
|
364
|
-
if (!approved) return { error: 'Permission denied' };
|
|
365
|
-
try {
|
|
366
|
-
const data = fs.readFileSync(filePath, 'utf8');
|
|
367
|
-
const lines = data.split('\n').length;
|
|
368
|
-
if (lines > 10) {
|
|
369
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Read ${filePath} (${lines} lines, ${data.length} chars)${RST}`);
|
|
370
|
-
} else {
|
|
371
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Read ${filePath}${RST}`);
|
|
372
|
-
}
|
|
373
|
-
return { content: data, path: filePath };
|
|
374
|
-
} catch (e) {
|
|
375
|
-
console.log(` ${FG_RED}✗ ${e.message}${RST}`);
|
|
376
|
-
return { error: e.message };
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (action === 'write' || action === 'append') {
|
|
381
|
-
let desc = `${action === 'write' ? 'Write' : 'Append to'} ${filePath}`;
|
|
382
|
-
if (content) desc += ` (${content.length} chars)`;
|
|
383
|
-
const approved = await askPermission('file', desc);
|
|
384
|
-
if (!approved) return { error: 'Permission denied' };
|
|
385
|
-
try {
|
|
386
|
-
const dir = path.dirname(filePath);
|
|
387
|
-
if (dir && dir !== '.') fs.mkdirSync(dir, { recursive: true });
|
|
388
|
-
if (action === 'write') {
|
|
389
|
-
fs.writeFileSync(filePath, content || '');
|
|
390
|
-
} else {
|
|
391
|
-
fs.appendFileSync(filePath, content || '');
|
|
392
|
-
}
|
|
393
|
-
const verb = action === 'write' ? 'Wrote' : 'Appended to';
|
|
394
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}${verb} ${filePath}${RST}`);
|
|
395
|
-
return { status: 'ok', path: filePath, bytes: (content || '').length };
|
|
396
|
-
} catch (e) {
|
|
397
|
-
console.log(` ${FG_RED}✗ ${e.message}${RST}`);
|
|
398
|
-
return { error: e.message };
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return { error: `Unknown action: ${action}` };
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// ── Tool call extraction ──────────────────────────────────────────────────────
|
|
406
|
-
|
|
407
|
-
function extractToolCalls(text) {
|
|
408
|
-
const calls = [];
|
|
409
|
-
|
|
410
|
-
// ```bash / ```shell / ```sh blocks
|
|
411
|
-
for (const m of text.matchAll(/```(?:shell|bash|sh)\n([\s\S]*?)```/g)) {
|
|
412
|
-
for (const line of m[1].trim().split('\n')) {
|
|
413
|
-
const cmd = line.trim();
|
|
414
|
-
if (cmd && !cmd.startsWith('#')) calls.push(['shell', cmd]);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// <shell> tags
|
|
419
|
-
for (const m of text.matchAll(/<shell>([\s\S]*?)<\/shell>/g))
|
|
420
|
-
calls.push(['shell', m[1].trim()]);
|
|
421
|
-
|
|
422
|
-
// <exec> tags
|
|
423
|
-
for (const m of text.matchAll(/<exec>([\s\S]*?)<\/exec>/g))
|
|
424
|
-
calls.push(['shell', m[1].trim()]);
|
|
425
|
-
|
|
426
|
-
// <read_file> tags
|
|
427
|
-
for (const m of text.matchAll(/<read_file>([\s\S]*?)<\/read_file>/g))
|
|
428
|
-
calls.push(['read', m[1].trim()]);
|
|
429
|
-
|
|
430
|
-
// <write_file> tags
|
|
431
|
-
for (const m of text.matchAll(/<write_file\s+path="([^"]+)">([\s\S]*?)<\/write_file>/g))
|
|
432
|
-
calls.push(['write', m[1], m[2]]);
|
|
433
|
-
|
|
434
|
-
return calls;
|
|
18
|
+
function getConfig() {
|
|
19
|
+
return config;
|
|
435
20
|
}
|
|
436
21
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const FG_CODE_BG = '\x1b[48;5;236m';
|
|
440
|
-
const FG_CODE_BORDER = '\x1b[38;5;240m';
|
|
441
|
-
const FG_CODE_LANG = '\x1b[38;5;75m';
|
|
442
|
-
const FG_INLINE_CODE = '\x1b[38;5;215m';
|
|
443
|
-
const FG_HEADING = '\x1b[38;5;75m';
|
|
444
|
-
const FG_BULLET = '\x1b[38;5;114m';
|
|
445
|
-
const FG_BOLD_TEXT = '\x1b[38;5;255m';
|
|
446
|
-
const FG_DIFF_ADD = '\x1b[38;5;114m';
|
|
447
|
-
const FG_DIFF_DEL = '\x1b[38;5;203m';
|
|
448
|
-
const FG_DIFF_HDR = '\x1b[38;5;75m';
|
|
449
|
-
const FG_FILEPATH = '\x1b[38;5;222m';
|
|
450
|
-
const FG_TAG = '\x1b[38;5;176m';
|
|
451
|
-
|
|
452
|
-
const KEYWORDS = new Set([
|
|
453
|
-
'def', 'class', 'import', 'from', 'return', 'if', 'else', 'elif',
|
|
454
|
-
'for', 'while', 'try', 'except', 'finally', 'with', 'as', 'in',
|
|
455
|
-
'not', 'and', 'or', 'is', 'None', 'True', 'False', 'async', 'await',
|
|
456
|
-
'function', 'const', 'let', 'var', 'export', 'require', 'func',
|
|
457
|
-
'type', 'struct', 'interface', 'package', 'fn', 'pub', 'use',
|
|
458
|
-
'mod', 'impl', 'match', 'enum', 'self', 'print', 'len', 'range',
|
|
459
|
-
'yield', 'lambda', 'raise', 'pass', 'break', 'continue', 'del',
|
|
460
|
-
'global', 'nonlocal', 'assert',
|
|
461
|
-
]);
|
|
462
|
-
|
|
463
|
-
class StreamRenderer {
|
|
464
|
-
constructor() {
|
|
465
|
-
this.buffer = '';
|
|
466
|
-
this.inCodeBlock = false;
|
|
467
|
-
this.codeLang = '';
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
feed(chunk) {
|
|
471
|
-
this.buffer += chunk;
|
|
472
|
-
while (this.buffer.includes('\n')) {
|
|
473
|
-
const idx = this.buffer.indexOf('\n');
|
|
474
|
-
const line = this.buffer.slice(0, idx);
|
|
475
|
-
this.buffer = this.buffer.slice(idx + 1);
|
|
476
|
-
this._renderLine(line);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
flush() {
|
|
481
|
-
if (this.buffer) {
|
|
482
|
-
this._renderLine(this.buffer);
|
|
483
|
-
this.buffer = '';
|
|
484
|
-
}
|
|
485
|
-
if (this.inCodeBlock) {
|
|
486
|
-
process.stdout.write(` ${FG_CODE_BORDER}╰${'─'.repeat(50)}${RST}\n`);
|
|
487
|
-
this.inCodeBlock = false;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
_renderLine(line) {
|
|
492
|
-
// Code block start
|
|
493
|
-
if (line.startsWith('```') && !this.inCodeBlock) {
|
|
494
|
-
this.inCodeBlock = true;
|
|
495
|
-
this.codeLang = line.slice(3).trim();
|
|
496
|
-
const label = this.codeLang ? ` ${this.codeLang} ` : '';
|
|
497
|
-
process.stdout.write(
|
|
498
|
-
` ${FG_CODE_BORDER}╭${'─'.repeat(20)}${RST}${FG_CODE_LANG}${label}${RST}` +
|
|
499
|
-
`${FG_CODE_BORDER}${'─'.repeat(Math.max(1, 30 - label.length))}${RST}\n`
|
|
500
|
-
);
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Code block end
|
|
505
|
-
if (line.trim() === '```' && this.inCodeBlock) {
|
|
506
|
-
process.stdout.write(` ${FG_CODE_BORDER}╰${'─'.repeat(50)}${RST}\n`);
|
|
507
|
-
this.inCodeBlock = false;
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Inside code block
|
|
512
|
-
if (this.inCodeBlock) {
|
|
513
|
-
process.stdout.write(` ${FG_CODE_BORDER}│${RST} ${FG_CODE_BG}${this._colorizeCode(line)}${RST}\n`);
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Diff lines
|
|
518
|
-
if (/^\+(?!\+\+)/.test(line)) { process.stdout.write(` ${FG_DIFF_ADD}${line}${RST}\n`); return; }
|
|
519
|
-
if (/^-(?!--)/.test(line)) { process.stdout.write(` ${FG_DIFF_DEL}${line}${RST}\n`); return; }
|
|
520
|
-
if (line.startsWith('@@')) { process.stdout.write(` ${FG_DIFF_HDR}${line}${RST}\n`); return; }
|
|
521
|
-
|
|
522
|
-
// Tool tags
|
|
523
|
-
if (/^<(exec|shell|read_file|write_file)/.test(line)) { process.stdout.write(` ${FG_TAG}${line}${RST}\n`); return; }
|
|
524
|
-
if (/^<\/(exec|shell|read_file|write_file)/.test(line)){ process.stdout.write(` ${FG_TAG}${line}${RST}\n`); return; }
|
|
525
|
-
|
|
526
|
-
// Headings
|
|
527
|
-
const hm = line.match(/^(#{1,4})\s+(.*)/);
|
|
528
|
-
if (hm) {
|
|
529
|
-
const text = hm[2];
|
|
530
|
-
if (hm[1].length <= 2) {
|
|
531
|
-
process.stdout.write(` ${FG_HEADING}${BOLD}${hm[1]} ${text}${RST}\n`);
|
|
532
|
-
} else {
|
|
533
|
-
process.stdout.write(` ${FG_HEADING}${hm[1]} ${text}${RST}\n`);
|
|
534
|
-
}
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Bullet points
|
|
539
|
-
const bm = line.match(/^(\s*)([-*•])\s+(.*)/);
|
|
540
|
-
if (bm) {
|
|
541
|
-
process.stdout.write(` ${bm[1]}${FG_BULLET}•${RST} ${this._inlineFormat(bm[3])}\n`);
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Numbered list
|
|
546
|
-
const nm = line.match(/^(\s*)(\d+\.)\s+(.*)/);
|
|
547
|
-
if (nm) {
|
|
548
|
-
process.stdout.write(` ${nm[1]}${FG_CYAN}${nm[2]}${RST} ${this._inlineFormat(nm[3])}\n`);
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Regular text
|
|
553
|
-
process.stdout.write(` ${this._inlineFormat(line)}\n`);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
_inlineFormat(text) {
|
|
557
|
-
text = text.replace(/\*\*(.+?)\*\*/g, `${FG_BOLD_TEXT}${BOLD}$1${RST}`);
|
|
558
|
-
text = text.replace(/`([^`]+)`/g, `${FG_INLINE_CODE}$1${RST}`);
|
|
559
|
-
text = text.replace(/(?<!\w)((?:\/[\w\-.]+)+(?:\.\w+)?)/g, `${FG_FILEPATH}$1${RST}`);
|
|
560
|
-
return text;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
_colorizeCode(line) {
|
|
564
|
-
const C_KW = '\x1b[38;5;176m';
|
|
565
|
-
const C_STR = '\x1b[38;5;114m';
|
|
566
|
-
const C_CMT = '\x1b[38;5;242m';
|
|
567
|
-
const C_NUM = '\x1b[38;5;215m';
|
|
568
|
-
const C_RST = `${RST}${FG_CODE_BG}`;
|
|
569
|
-
|
|
570
|
-
let result = '';
|
|
571
|
-
let i = 0;
|
|
572
|
-
|
|
573
|
-
while (i < line.length) {
|
|
574
|
-
// Comment
|
|
575
|
-
if (line[i] === '#' || (line[i] === '/' && line[i + 1] === '/')) {
|
|
576
|
-
result += `${C_CMT}${line.slice(i)}${C_RST}`;
|
|
577
|
-
break;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// String
|
|
581
|
-
if (line[i] === '"' || line[i] === "'") {
|
|
582
|
-
const q = line[i];
|
|
583
|
-
let j = i + 1;
|
|
584
|
-
while (j < line.length && line[j] !== q) {
|
|
585
|
-
if (line[j] === '\\') j++;
|
|
586
|
-
j++;
|
|
587
|
-
}
|
|
588
|
-
j = Math.min(j + 1, line.length);
|
|
589
|
-
result += `${C_STR}${line.slice(i, j)}${C_RST}`;
|
|
590
|
-
i = j;
|
|
591
|
-
continue;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Identifier / keyword
|
|
595
|
-
if (/[a-zA-Z_]/.test(line[i])) {
|
|
596
|
-
let j = i;
|
|
597
|
-
while (j < line.length && /\w/.test(line[j])) j++;
|
|
598
|
-
const word = line.slice(i, j);
|
|
599
|
-
result += KEYWORDS.has(word) ? `${C_KW}${word}${C_RST}` : word;
|
|
600
|
-
i = j;
|
|
601
|
-
continue;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Number
|
|
605
|
-
if (/\d/.test(line[i])) {
|
|
606
|
-
let j = i;
|
|
607
|
-
while (j < line.length && /[\d.]/.test(line[j])) j++;
|
|
608
|
-
result += `${C_NUM}${line.slice(i, j)}${C_RST}`;
|
|
609
|
-
i = j;
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
result += line[i++];
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return result;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// ── API Client ────────────────────────────────────────────────────────────────
|
|
621
|
-
|
|
622
|
-
function apiUrl(urlPath) {
|
|
623
|
-
const base = (config.api_base || '').replace(/\/$/, '');
|
|
624
|
-
const normalizedBase = /\/v1$/i.test(base) ? base : `${base}/v1`;
|
|
625
|
-
const normalizedPath = urlPath.startsWith('/v1/') ? urlPath.slice(3) : urlPath;
|
|
626
|
-
return `${normalizedBase}${normalizedPath}`;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function describeModelProfile(profile) {
|
|
630
|
-
return `${profile.model} @ ${profile.api_base}`;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
function setActiveModelProfile(profile) {
|
|
634
|
-
config.api_base = profile.api_base;
|
|
635
|
-
config.api_key = profile.api_key;
|
|
636
|
-
config.default_model = profile.model;
|
|
22
|
+
function setConfig(nextConfig) {
|
|
23
|
+
config = nextConfig;
|
|
637
24
|
saveConfig(config);
|
|
638
25
|
}
|
|
639
26
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const profile = config.models[selected - 1];
|
|
668
|
-
setActiveModelProfile(profile);
|
|
669
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Model profile → ${describeModelProfile(profile)}${RST}`);
|
|
670
|
-
printStatusBar(profile.model, cwd);
|
|
671
|
-
onDone(profile.model);
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function estimateTokens(text) {
|
|
676
|
-
return Math.floor((text || '').length / 4);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function httpRequest(urlStr, options, body) {
|
|
680
|
-
return new Promise((resolve, reject) => {
|
|
681
|
-
const url = new URL(urlStr);
|
|
682
|
-
const lib = url.protocol === 'https:' ? https : http;
|
|
683
|
-
const reqOpts = {
|
|
684
|
-
hostname: url.hostname,
|
|
685
|
-
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
686
|
-
path: url.pathname + url.search,
|
|
687
|
-
method: options.method || 'GET',
|
|
688
|
-
headers: options.headers || {},
|
|
689
|
-
};
|
|
690
|
-
|
|
691
|
-
const req = lib.request(reqOpts, (res) => {
|
|
692
|
-
resolve(res);
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
req.on('error', reject);
|
|
696
|
-
|
|
697
|
-
if (options.timeout) {
|
|
698
|
-
req.setTimeout(options.timeout, () => {
|
|
699
|
-
req.destroy(new Error('Request timed out'));
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
if (body) req.write(body);
|
|
704
|
-
req.end();
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
async function chatStream(messages, { model, temperature, maxTokens } = {}) {
|
|
709
|
-
const payload = {
|
|
710
|
-
model: model || config.default_model,
|
|
711
|
-
messages,
|
|
712
|
-
temperature: temperature !== undefined ? temperature : config.temperature,
|
|
713
|
-
stream: true,
|
|
714
|
-
};
|
|
715
|
-
|
|
716
|
-
if (maxTokens !== undefined) {
|
|
717
|
-
payload.max_tokens = maxTokens;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
const body = JSON.stringify(payload);
|
|
721
|
-
let res;
|
|
722
|
-
|
|
723
|
-
try {
|
|
724
|
-
res = await httpRequest(apiUrl('/v1/chat/completions'), {
|
|
725
|
-
method: 'POST',
|
|
726
|
-
timeout: config.request_timeout_ms,
|
|
727
|
-
headers: {
|
|
728
|
-
'Content-Type': 'application/json',
|
|
729
|
-
'Authorization': `Bearer ${config.api_key}`,
|
|
730
|
-
'Content-Length': Buffer.byteLength(body),
|
|
731
|
-
},
|
|
732
|
-
}, body);
|
|
733
|
-
} catch (e) {
|
|
734
|
-
process.stdout.write(`\n ${FG_RED}✗ ${e.message}${RST}\n`);
|
|
735
|
-
return '';
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
if (res.statusCode !== 200) {
|
|
739
|
-
process.stdout.write(`\n ${FG_RED}✗ Error: HTTP ${res.statusCode}${RST}\n`);
|
|
740
|
-
res.resume();
|
|
741
|
-
return '';
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
return new Promise((resolve) => {
|
|
745
|
-
const startTime = Date.now();
|
|
746
|
-
let fullText = '';
|
|
747
|
-
let reasoningText = '';
|
|
748
|
-
let tokenCount = 0;
|
|
749
|
-
let inReasoning = false;
|
|
750
|
-
const renderer = new StreamRenderer();
|
|
751
|
-
let lineBuffer = '';
|
|
752
|
-
|
|
753
|
-
res.setEncoding('utf8');
|
|
754
|
-
|
|
755
|
-
res.on('data', (chunk) => {
|
|
756
|
-
lineBuffer += chunk;
|
|
757
|
-
const lines = lineBuffer.split('\n');
|
|
758
|
-
lineBuffer = lines.pop();
|
|
759
|
-
|
|
760
|
-
for (const line of lines) {
|
|
761
|
-
if (!line.startsWith('data: ')) continue;
|
|
762
|
-
const data = line.slice(6).trim();
|
|
763
|
-
if (data === '[DONE]') continue;
|
|
764
|
-
try {
|
|
765
|
-
const obj = JSON.parse(data);
|
|
766
|
-
const delta = ((obj.choices || [])[0] || {}).delta || {};
|
|
767
|
-
|
|
768
|
-
const reasoning = delta.reasoning_content || '';
|
|
769
|
-
if (reasoning) {
|
|
770
|
-
if (!inReasoning) {
|
|
771
|
-
inReasoning = true;
|
|
772
|
-
process.stdout.write(`\n ${FG_DARK}${DIM}⟨thinking⟩${RST}`);
|
|
773
|
-
}
|
|
774
|
-
reasoningText += reasoning;
|
|
775
|
-
tokenCount++;
|
|
776
|
-
if (tokenCount % 20 === 0) process.stdout.write(`${FG_DARK}.${RST}`);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
const content = delta.content || '';
|
|
780
|
-
if (content) {
|
|
781
|
-
if (inReasoning) {
|
|
782
|
-
inReasoning = false;
|
|
783
|
-
process.stdout.write(`${FG_DARK}⟨/thinking⟩${RST}\n`);
|
|
784
|
-
}
|
|
785
|
-
renderer.feed(content);
|
|
786
|
-
fullText += content;
|
|
787
|
-
tokenCount++;
|
|
788
|
-
}
|
|
789
|
-
} catch {}
|
|
790
|
-
}
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
res.on('end', () => {
|
|
794
|
-
renderer.flush();
|
|
795
|
-
const elapsed = (Date.now() - startTime) / 1000;
|
|
796
|
-
const estTokens = estimateTokens(fullText + reasoningText);
|
|
797
|
-
const tps = tokenCount / (elapsed || 1);
|
|
798
|
-
const cols = getCols();
|
|
799
|
-
process.stdout.write(`\n ${FG_DARK}${'─'.repeat(Math.min(cols, 60) - 4)}${RST}\n`);
|
|
800
|
-
let costLine = `${FG_DARK}~${estTokens} tokens · ${elapsed.toFixed(1)}s · ${Math.round(tps)} tok/s${RST}`;
|
|
801
|
-
if (reasoningText) costLine += ` ${FG_DARK}· ${estimateTokens(reasoningText)} thinking${RST}`;
|
|
802
|
-
process.stdout.write(` ${costLine}\n`);
|
|
803
|
-
resolve(fullText);
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
res.on('error', (e) => {
|
|
807
|
-
process.stdout.write(`\n ${FG_RED}✗ ${e.message}${RST}\n`);
|
|
808
|
-
resolve('');
|
|
809
|
-
});
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
async function chatSync(messages, { model } = {}) {
|
|
814
|
-
const payload = {
|
|
815
|
-
model: model || config.default_model,
|
|
816
|
-
messages,
|
|
817
|
-
temperature: config.temperature,
|
|
818
|
-
stream: false,
|
|
819
|
-
};
|
|
820
|
-
|
|
821
|
-
const body = JSON.stringify(payload);
|
|
822
|
-
let res;
|
|
823
|
-
|
|
824
|
-
try {
|
|
825
|
-
res = await httpRequest(apiUrl('/v1/chat/completions'), {
|
|
826
|
-
method: 'POST',
|
|
827
|
-
timeout: config.request_timeout_ms,
|
|
828
|
-
headers: {
|
|
829
|
-
'Content-Type': 'application/json',
|
|
830
|
-
'Authorization': `Bearer ${config.api_key}`,
|
|
831
|
-
'Content-Length': Buffer.byteLength(body),
|
|
832
|
-
},
|
|
833
|
-
}, body);
|
|
834
|
-
} catch (e) {
|
|
835
|
-
console.log(` ${FG_RED}✗ ${e.message}${RST}`);
|
|
836
|
-
return '';
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
return new Promise((resolve) => {
|
|
840
|
-
let data = '';
|
|
841
|
-
res.setEncoding('utf8');
|
|
842
|
-
res.on('data', chunk => data += chunk);
|
|
843
|
-
res.on('end', () => {
|
|
844
|
-
if (res.statusCode !== 200) {
|
|
845
|
-
console.log(` ${FG_RED}✗ Error: HTTP ${res.statusCode} — ${data}${RST}`);
|
|
846
|
-
return resolve('');
|
|
847
|
-
}
|
|
848
|
-
try {
|
|
849
|
-
const parsed = JSON.parse(data);
|
|
850
|
-
const content = parsed.choices[0].message.content;
|
|
851
|
-
console.log(content);
|
|
852
|
-
resolve(content);
|
|
853
|
-
} catch (e) {
|
|
854
|
-
console.log(` ${FG_RED}✗ Parse error: ${e.message}${RST}`);
|
|
855
|
-
resolve('');
|
|
856
|
-
}
|
|
857
|
-
});
|
|
858
|
-
res.on('error', (e) => {
|
|
859
|
-
console.log(` ${FG_RED}✗ ${e.message}${RST}`);
|
|
860
|
-
resolve('');
|
|
861
|
-
});
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// ── Agent loop ────────────────────────────────────────────────────────────────
|
|
866
|
-
|
|
867
|
-
async function runAgentLoop(messages, model, maxIterations = 10) {
|
|
868
|
-
const cols = getCols();
|
|
869
|
-
|
|
870
|
-
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
871
|
-
console.log();
|
|
872
|
-
console.log(` ${FG_DARK}${'─'.repeat(Math.min(cols, 70) - 4)}${RST}`);
|
|
873
|
-
process.stdout.write(` ${FG_TEAL}${BOLD}◆ Semalt.AI${RST}`);
|
|
874
|
-
if (iteration > 0) process.stdout.write(` ${FG_DARK}(step ${iteration + 1})${RST}`);
|
|
875
|
-
console.log();
|
|
876
|
-
console.log(` ${FG_DARK}${'─'.repeat(Math.min(cols, 70) - 4)}${RST}`);
|
|
877
|
-
console.log();
|
|
878
|
-
process.stdout.write(' ');
|
|
879
|
-
|
|
880
|
-
const reply = await chatStream(messages, { model });
|
|
881
|
-
if (!reply) break;
|
|
882
|
-
|
|
883
|
-
messages.push({ role: 'assistant', content: reply });
|
|
884
|
-
|
|
885
|
-
const toolCalls = extractToolCalls(reply);
|
|
886
|
-
if (toolCalls.length === 0) break;
|
|
887
|
-
|
|
888
|
-
console.log(`\n ${FG_TEAL}◆${RST} ${FG_GRAY}Found ${toolCalls.length} action(s) to execute${RST}`);
|
|
889
|
-
|
|
890
|
-
const results = [];
|
|
891
|
-
let aborted = false;
|
|
892
|
-
|
|
893
|
-
for (const call of toolCalls) {
|
|
894
|
-
if (call[0] === 'shell') {
|
|
895
|
-
const result = await agentExecShell(call[1]);
|
|
896
|
-
if (result.stderr === 'Permission denied by user') {
|
|
897
|
-
results.push(`Command \`${call[1]}\`: Permission denied by user.`);
|
|
898
|
-
aborted = true;
|
|
899
|
-
} else {
|
|
900
|
-
let out = result.stdout;
|
|
901
|
-
if (result.stderr) out += '\nSTDERR: ' + result.stderr;
|
|
902
|
-
results.push(`Command \`${call[1]}\`:\nExit code: ${result.exit_code}\n${out}`);
|
|
903
|
-
}
|
|
904
|
-
} else if (call[0] === 'read') {
|
|
905
|
-
const result = await agentExecFile('read', call[1]);
|
|
906
|
-
if (result.error) results.push(`Read ${call[1]}: Error — ${result.error}`);
|
|
907
|
-
else results.push(`File ${call[1]}:\n${result.content}`);
|
|
908
|
-
} else if (call[0] === 'write') {
|
|
909
|
-
const result = await agentExecFile('write', call[1], call[2]);
|
|
910
|
-
if (result.error) results.push(`Write ${call[1]}: Error — ${result.error}`);
|
|
911
|
-
else results.push(`Wrote ${result.bytes} bytes to ${call[1]}`);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
const feedback = results.join('\n\n');
|
|
916
|
-
messages.push({
|
|
917
|
-
role: 'user',
|
|
918
|
-
content: `Tool execution results:\n\n${feedback}\n\nContinue with the task. If everything is done, summarize what was accomplished.`,
|
|
919
|
-
});
|
|
920
|
-
|
|
921
|
-
if (aborted) {
|
|
922
|
-
console.log(`\n ${FG_YELLOW}⚠${RST} ${FG_GRAY}Some actions were denied. Continuing with partial results.${RST}`);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
return messages;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// ── Banner / UI ───────────────────────────────────────────────────────────────
|
|
930
|
-
|
|
931
|
-
function printBanner() {
|
|
932
|
-
const w = Math.min(getCols() - 4, 60);
|
|
933
|
-
console.log();
|
|
934
|
-
console.log(` ${FG_DARK}${BOX_TL}${BOX_H.repeat(w + 2)}${BOX_TR}${RST}`);
|
|
935
|
-
console.log(boxLine('', w));
|
|
936
|
-
console.log(boxLine(`${FG_TEAL}${BOLD}◆ Semalt.AI${RST}`, w));
|
|
937
|
-
console.log(boxLine(`${FG_GRAY}Self-hosted AI coding assistant${RST}`, w));
|
|
938
|
-
console.log(boxLine('', w));
|
|
939
|
-
console.log(` ${FG_DARK}${BOX_BL}${BOX_H.repeat(w + 2)}${BOX_BR}${RST}`);
|
|
940
|
-
console.log();
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
function printStatusBar(model, cwd) {
|
|
944
|
-
const left = `${FG_TEAL}${BOLD}◆${RST} ${FG_GRAY}${model}${RST}`;
|
|
945
|
-
const right = `${FG_DARK}${cwd}${RST}`;
|
|
946
|
-
console.log(` ${left} ${FG_DARK}│${RST} ${right}`);
|
|
947
|
-
hr();
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
function printHelpHints() {
|
|
951
|
-
const hints = [
|
|
952
|
-
[`${FG_BLUE}/help${RST}`, 'commands'],
|
|
953
|
-
[`${FG_BLUE}/model${RST}`, 'switch'],
|
|
954
|
-
[`${FG_BLUE}/file${RST}`, 'context'],
|
|
955
|
-
[`${FG_BLUE}/clear${RST}`, 'reset'],
|
|
956
|
-
];
|
|
957
|
-
process.stdout.write(` ${FG_DARK}Tips:${RST}`);
|
|
958
|
-
for (const [cmd, desc] of hints) {
|
|
959
|
-
process.stdout.write(` ${cmd} ${FG_DARK}${desc}${RST}`);
|
|
960
|
-
}
|
|
961
|
-
console.log();
|
|
962
|
-
console.log();
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
// ── File context ──────────────────────────────────────────────────────────────
|
|
966
|
-
|
|
967
|
-
function readFileContext(filePaths) {
|
|
968
|
-
let context = '';
|
|
969
|
-
for (const fp of filePaths) {
|
|
970
|
-
if (!fs.existsSync(fp)) {
|
|
971
|
-
console.log(` ${FG_RED}✗${RST} ${FG_GRAY}Not found: ${fp}${RST}`);
|
|
972
|
-
continue;
|
|
973
|
-
}
|
|
974
|
-
const stat = fs.statSync(fp);
|
|
975
|
-
if (stat.isFile()) {
|
|
976
|
-
try {
|
|
977
|
-
const content = fs.readFileSync(fp, 'utf8');
|
|
978
|
-
context += `\n--- File: ${fp} ---\n${content}\n`;
|
|
979
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Loaded ${fp} (${content.length} chars)${RST}`);
|
|
980
|
-
} catch (e) {
|
|
981
|
-
console.log(` ${FG_RED}✗${RST} ${FG_GRAY}${fp}: ${e.message}${RST}`);
|
|
982
|
-
}
|
|
983
|
-
} else if (stat.isDirectory()) {
|
|
984
|
-
let count = 0;
|
|
985
|
-
function walkDir(dir) {
|
|
986
|
-
if (count >= 50) return;
|
|
987
|
-
let entries;
|
|
988
|
-
try { entries = fs.readdirSync(dir).sort(); } catch { return; }
|
|
989
|
-
for (const entry of entries) {
|
|
990
|
-
if (entry.startsWith('.')) continue;
|
|
991
|
-
const full = path.join(dir, entry);
|
|
992
|
-
let s;
|
|
993
|
-
try { s = fs.statSync(full); } catch { continue; }
|
|
994
|
-
if (s.isFile()) {
|
|
995
|
-
try {
|
|
996
|
-
const content = fs.readFileSync(full, 'utf8').slice(0, 10000);
|
|
997
|
-
context += `\n--- File: ${full} ---\n${content}\n`;
|
|
998
|
-
count++;
|
|
999
|
-
} catch {}
|
|
1000
|
-
} else if (s.isDirectory()) {
|
|
1001
|
-
walkDir(full);
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
walkDir(fp);
|
|
1006
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Loaded ${count} files from ${fp}${RST}`);
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
return context;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
// ── System prompt ─────────────────────────────────────────────────────────────
|
|
1013
|
-
|
|
1014
|
-
function getSystemPrompt() {
|
|
1015
|
-
return `You are Semalt.AI, an expert AI coding assistant running in the user's terminal. You have the ability to execute shell commands and file operations.
|
|
1016
|
-
|
|
1017
|
-
IMPORTANT: You CAN execute commands on the user's system. When you need to run a command, use this exact format:
|
|
1018
|
-
|
|
1019
|
-
To run a shell command:
|
|
1020
|
-
<exec>command here</exec>
|
|
1021
|
-
|
|
1022
|
-
To read a file:
|
|
1023
|
-
<read_file>/path/to/file</read_file>
|
|
1024
|
-
|
|
1025
|
-
To write a file:
|
|
1026
|
-
<write_file path="/path/to/file">file content here</write_file>
|
|
1027
|
-
|
|
1028
|
-
Rules:
|
|
1029
|
-
- When the user asks you to do something on their system (create files, install packages, check status, etc.), USE the tools above — do NOT just print instructions.
|
|
1030
|
-
- Each command will be shown to the user for approval before execution.
|
|
1031
|
-
- After execution, you will receive the output and can continue working.
|
|
1032
|
-
- You can chain multiple operations in one response.
|
|
1033
|
-
- Be concise. Provide working solutions.
|
|
1034
|
-
- Use markdown for code blocks in explanations.
|
|
1035
|
-
- Current working directory: ${process.cwd()}`;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
1039
|
-
|
|
1040
|
-
async function cmdChat(opts) {
|
|
1041
|
-
printBanner();
|
|
1042
|
-
const cwd = process.cwd();
|
|
1043
|
-
let currentModel = opts.model || config.default_model;
|
|
1044
|
-
let isRunningAgent = false;
|
|
1045
|
-
|
|
1046
|
-
printStatusBar(currentModel, cwd);
|
|
1047
|
-
printHelpHints();
|
|
1048
|
-
|
|
1049
|
-
let messages = [{ role: 'system', content: getSystemPrompt() }];
|
|
1050
|
-
const cols = getCols();
|
|
1051
|
-
|
|
1052
|
-
while (true) {
|
|
1053
|
-
const inputResult = await readInteractiveInput(` ${FG_TEAL}${BOLD}>${RST} `, {
|
|
1054
|
-
trim: false,
|
|
1055
|
-
allowCursorNavigation: true,
|
|
1056
|
-
});
|
|
1057
|
-
|
|
1058
|
-
if (inputResult.type === 'eof') {
|
|
1059
|
-
console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
if (inputResult.type === 'sigint') {
|
|
1064
|
-
if (!isRunningAgent) {
|
|
1065
|
-
console.log(`\n ${FG_YELLOW}Use Ctrl+D or type exit to quit.${RST}`);
|
|
1066
|
-
}
|
|
1067
|
-
continue;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
const text = (inputResult.value || '').trim();
|
|
1071
|
-
|
|
1072
|
-
if (!text) continue;
|
|
1073
|
-
|
|
1074
|
-
if (['exit', 'quit', '/exit', '/quit'].includes(text.toLowerCase())) {
|
|
1075
|
-
console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
if (text === '/help') {
|
|
1080
|
-
console.log(`
|
|
1081
|
-
${FG_BLUE}${BOLD}Commands:${RST}
|
|
1082
|
-
${FG_CYAN}/file <path>${RST} ${FG_GRAY}Load file or dir into context${RST}
|
|
1083
|
-
${FG_CYAN}/model${RST} ${FG_GRAY}Choose saved model profile${RST}
|
|
1084
|
-
${FG_CYAN}/model <name>${RST} ${FG_GRAY}Switch model manually${RST}
|
|
1085
|
-
${FG_CYAN}/models${RST} ${FG_GRAY}Choose saved model profile${RST}
|
|
1086
|
-
${FG_CYAN}/clear${RST} ${FG_GRAY}Clear conversation${RST}
|
|
1087
|
-
${FG_CYAN}/compact${RST} ${FG_GRAY}Show token usage${RST}
|
|
1088
|
-
${FG_CYAN}/shell <cmd>${RST} ${FG_GRAY}Run shell command directly${RST}
|
|
1089
|
-
${FG_CYAN}!<cmd>${RST} ${FG_GRAY}Run shell command directly${RST}
|
|
1090
|
-
${FG_CYAN}/approve${RST} ${FG_GRAY}Toggle auto-approve for all actions${RST}
|
|
1091
|
-
${FG_CYAN}/config${RST} ${FG_GRAY}Show config${RST}
|
|
1092
|
-
${FG_CYAN}exit${RST} ${FG_GRAY}Quit${RST}
|
|
1093
|
-
|
|
1094
|
-
${FG_DARK}The AI can execute commands — you'll be asked to approve each one.${RST}
|
|
1095
|
-
`);
|
|
1096
|
-
continue;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
if (text.startsWith('/file ')) {
|
|
1100
|
-
const fp = text.slice(6).trim();
|
|
1101
|
-
const ctx = readFileContext([fp]);
|
|
1102
|
-
if (ctx) messages.push({ role: 'user', content: `Here is the file context:\n${ctx}` });
|
|
1103
|
-
continue;
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
if (text === '/model' || text === '/models') {
|
|
1107
|
-
await new Promise((resolve) => {
|
|
1108
|
-
const rl = readline.createInterface({
|
|
1109
|
-
input: process.stdin,
|
|
1110
|
-
output: process.stdout,
|
|
1111
|
-
terminal: true,
|
|
1112
|
-
});
|
|
1113
|
-
chooseSavedModelProfile(rl, currentModel, cwd, (nextModel) => {
|
|
1114
|
-
currentModel = nextModel;
|
|
1115
|
-
rl.close();
|
|
1116
|
-
resolve();
|
|
1117
|
-
});
|
|
1118
|
-
});
|
|
1119
|
-
continue;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
if (text.startsWith('/model ')) {
|
|
1123
|
-
currentModel = text.slice(7).trim();
|
|
1124
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Model → ${currentModel}${RST}`);
|
|
1125
|
-
printStatusBar(currentModel, cwd);
|
|
1126
|
-
continue;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
if (text === '/clear') {
|
|
1130
|
-
messages = [{ role: 'system', content: getSystemPrompt() }];
|
|
1131
|
-
AUTO_APPROVE_SHELL = false;
|
|
1132
|
-
AUTO_APPROVE_FILE = false;
|
|
1133
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Conversation and approvals cleared${RST}\n`);
|
|
1134
|
-
continue;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
if (text === '/compact' || text === '/cost') {
|
|
1138
|
-
const total = messages.reduce((s, m) => s + estimateTokens(m.content), 0);
|
|
1139
|
-
console.log(` ${FG_GRAY}${messages.length - 1} messages · ~${total} tokens${RST}\n`);
|
|
1140
|
-
continue;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
if (text === '/config') {
|
|
1144
|
-
console.log(` ${FG_GRAY}${JSON.stringify(config, null, 2)}${RST}\n`);
|
|
1145
|
-
continue;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
if (text === '/approve') {
|
|
1149
|
-
AUTO_APPROVE_SHELL = !AUTO_APPROVE_SHELL;
|
|
1150
|
-
AUTO_APPROVE_FILE = !AUTO_APPROVE_FILE;
|
|
1151
|
-
const state = AUTO_APPROVE_SHELL ? 'ON' : 'OFF';
|
|
1152
|
-
const color = AUTO_APPROVE_SHELL ? FG_GREEN : FG_RED;
|
|
1153
|
-
console.log(` ${color}●${RST} ${FG_GRAY}Auto-approve: ${state}${RST}\n`);
|
|
1154
|
-
continue;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
if (text.startsWith('/shell ') || text.startsWith('!')) {
|
|
1158
|
-
const cmd = text.startsWith('/shell ') ? text.slice(7).trim() : text.slice(1).trim();
|
|
1159
|
-
await agentExecShell(cmd);
|
|
1160
|
-
continue;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
messages.push({ role: 'user', content: text });
|
|
1164
|
-
console.log(` ${FG_DARK}${'─'.repeat(Math.min(cols, 70) - 4)}${RST}`);
|
|
1165
|
-
|
|
1166
|
-
isRunningAgent = true;
|
|
1167
|
-
messages = await runAgentLoop(messages, currentModel);
|
|
1168
|
-
isRunningAgent = false;
|
|
1169
|
-
|
|
1170
|
-
console.log(` ${FG_DARK}${'━'.repeat(Math.min(cols, 70) - 4)}${RST}`);
|
|
1171
|
-
console.log();
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
async function cmdCode(opts, promptArgs) {
|
|
1176
|
-
if (!promptArgs.length) {
|
|
1177
|
-
console.log(` ${FG_RED}Usage: semalt-code code <prompt>${RST}`);
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
const userPrompt = promptArgs.join(' ');
|
|
1181
|
-
const context = opts.file ? readFileContext(opts.file) : '';
|
|
1182
|
-
const fullPrompt = context ? `Context files:\n${context}\n\nTask: ${userPrompt}` : userPrompt;
|
|
1183
|
-
|
|
1184
|
-
let messages = [
|
|
1185
|
-
{ role: 'system', content: getSystemPrompt() },
|
|
1186
|
-
{ role: 'user', content: fullPrompt },
|
|
1187
|
-
];
|
|
1188
|
-
messages = await runAgentLoop(messages, opts.model || config.default_model);
|
|
1189
|
-
console.log();
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
async function cmdEdit(opts, filePath, instructionArgs) {
|
|
1193
|
-
if (!filePath) {
|
|
1194
|
-
console.log(` ${FG_RED}Usage: semalt-code edit <file> <instruction>${RST}`);
|
|
1195
|
-
return;
|
|
1196
|
-
}
|
|
1197
|
-
if (!fs.existsSync(filePath)) {
|
|
1198
|
-
console.log(` ${FG_RED}✗ File not found: ${filePath}${RST}`);
|
|
1199
|
-
return;
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
1203
|
-
const instruction = instructionArgs.join(' ');
|
|
1204
|
-
|
|
1205
|
-
const messages = [
|
|
1206
|
-
{ role: 'system', content: 'You are Semalt.AI. Output ONLY the modified file. No explanations, no fences.' },
|
|
1207
|
-
{ role: 'user', content: `File: ${filePath}\n\n\`\`\`\n${content}\n\`\`\`\n\nInstruction: ${instruction}` },
|
|
1208
|
-
];
|
|
1209
|
-
|
|
1210
|
-
console.log(` ${FG_GRAY}Editing ${filePath}...${RST}`);
|
|
1211
|
-
let result = await chatSync(messages, { model: opts.model });
|
|
1212
|
-
|
|
1213
|
-
if (result && !opts.dryRun) {
|
|
1214
|
-
if (result.startsWith('```')) {
|
|
1215
|
-
const lines = result.split('\n');
|
|
1216
|
-
result = lines.at(-1).trim() === '```'
|
|
1217
|
-
? lines.slice(1, -1).join('\n')
|
|
1218
|
-
: lines.slice(1).join('\n');
|
|
1219
|
-
}
|
|
1220
|
-
fs.writeFileSync(filePath, result);
|
|
1221
|
-
console.log(` ${FG_GREEN}✓ Saved: ${filePath}${RST}`);
|
|
1222
|
-
} else if (opts.dryRun) {
|
|
1223
|
-
console.log(` ${FG_YELLOW}⚠ Dry run — not modified${RST}`);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
async function cmdShell(opts, commandArgs) {
|
|
1228
|
-
const command = commandArgs.join(' ');
|
|
1229
|
-
if (!command) {
|
|
1230
|
-
console.log(` ${FG_RED}Usage: semalt-code shell <command>${RST}`);
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1233
|
-
const result = await agentExecShell(command);
|
|
1234
|
-
|
|
1235
|
-
if (opts.analyze) {
|
|
1236
|
-
const messages = [
|
|
1237
|
-
{ role: 'system', content: 'You are Semalt.AI. Analyze the command output concisely.' },
|
|
1238
|
-
{ role: 'user', content: `Command: ${command}\nExit: ${result.exit_code}\nStdout:\n${result.stdout}\nStderr:\n${result.stderr}` },
|
|
1239
|
-
];
|
|
1240
|
-
console.log();
|
|
1241
|
-
console.log(` ${FG_TEAL}${BOLD}◆ Semalt.AI${RST}`);
|
|
1242
|
-
console.log();
|
|
1243
|
-
process.stdout.write(' ');
|
|
1244
|
-
await chatStream(messages, { model: opts.model });
|
|
1245
|
-
console.log();
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
async function cmdModels() {
|
|
1250
|
-
if (!config.models.length) {
|
|
1251
|
-
console.log(` ${FG_RED}✗${RST} ${FG_GRAY}No saved model profiles. Use semalt-code models add first.${RST}`);
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
console.log();
|
|
1256
|
-
console.log(` ${FG_TEAL}${BOLD}◆ Saved Models${RST}`);
|
|
1257
|
-
console.log(` ${FG_DARK}${'─'.repeat(40)}${RST}`);
|
|
1258
|
-
config.models.forEach((profile, index) => {
|
|
1259
|
-
const active = profile.api_base === config.api_base &&
|
|
1260
|
-
profile.api_key === config.api_key &&
|
|
1261
|
-
profile.model === config.default_model;
|
|
1262
|
-
const marker = active ? `${FG_GREEN}●${RST}` : `${FG_DARK}○${RST}`;
|
|
1263
|
-
console.log(` ${marker} ${FG_CYAN}${index + 1}.${RST} ${describeModelProfile(profile)}`);
|
|
1264
|
-
});
|
|
1265
|
-
console.log();
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
async function cmdModelsAdd() {
|
|
1269
|
-
const rl = readline.createInterface({
|
|
1270
|
-
input: process.stdin,
|
|
1271
|
-
output: process.stdout,
|
|
1272
|
-
terminal: true,
|
|
1273
|
-
});
|
|
1274
|
-
|
|
1275
|
-
function ask(question) {
|
|
1276
|
-
return new Promise((resolve) => {
|
|
1277
|
-
rl.question(question, (answer) => resolve((answer || '').trim()));
|
|
1278
|
-
});
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
console.log();
|
|
1282
|
-
console.log(` ${FG_TEAL}${BOLD}◆ Add Model Profile${RST}`);
|
|
1283
|
-
console.log(` ${FG_DARK}${'─'.repeat(40)}${RST}`);
|
|
1284
|
-
|
|
1285
|
-
const apiBase = await ask(` ${FG_CYAN}API Base URL:${RST} `);
|
|
1286
|
-
const apiKey = await ask(` ${FG_CYAN}API Key:${RST} `);
|
|
1287
|
-
const modelId = await ask(` ${FG_CYAN}Model ID:${RST} `);
|
|
1288
|
-
rl.close();
|
|
1289
|
-
|
|
1290
|
-
if (!apiBase || !modelId) {
|
|
1291
|
-
console.log(`\n ${FG_RED}✗${RST} ${FG_GRAY}API Base URL and Model ID are required.${RST}\n`);
|
|
1292
|
-
return;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
const profile = {
|
|
1296
|
-
api_base: apiBase,
|
|
1297
|
-
api_key: apiKey || 'any',
|
|
1298
|
-
model: modelId,
|
|
1299
|
-
};
|
|
1300
|
-
|
|
1301
|
-
config.models.push(profile);
|
|
1302
|
-
setActiveModelProfile(profile);
|
|
1303
|
-
console.log(`\n ${FG_GREEN}✓${RST} Saved model profile: ${describeModelProfile(profile)}\n`);
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
function cmdInit(opts) {
|
|
1307
|
-
const cfg = {
|
|
1308
|
-
api_base: opts.apiBase || 'http://127.0.0.1:8800',
|
|
1309
|
-
api_key: opts.apiKey || 'any',
|
|
1310
|
-
default_model: opts.defaultModel || 'default',
|
|
1311
|
-
temperature: 0.7,
|
|
1312
|
-
request_timeout_ms: DEFAULT_API_TIMEOUT_MS,
|
|
1313
|
-
stream: true,
|
|
1314
|
-
models: config.models,
|
|
1315
|
-
};
|
|
1316
|
-
saveConfig(cfg);
|
|
1317
|
-
config = cfg;
|
|
1318
|
-
console.log(`\n ${FG_GREEN}✓${RST} Config saved to ${CONFIG_PATH}`);
|
|
1319
|
-
console.log(` ${FG_GRAY}${JSON.stringify(cfg, null, 2)}${RST}\n`);
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
// ── CLI arg parser ────────────────────────────────────────────────────────────
|
|
1323
|
-
|
|
1324
|
-
function parseArgs(argv) {
|
|
1325
|
-
const opts = {};
|
|
1326
|
-
const positional = [];
|
|
1327
|
-
let i = 0;
|
|
1328
|
-
while (i < argv.length) {
|
|
1329
|
-
switch (argv[i]) {
|
|
1330
|
-
case '-m': case '--model': opts.model = argv[++i]; break;
|
|
1331
|
-
case '-f': case '--file': (opts.file = opts.file || []).push(argv[++i]); break;
|
|
1332
|
-
case '-a': case '--analyze': opts.analyze = true; break;
|
|
1333
|
-
case '--dry-run': opts.dryRun = true; break;
|
|
1334
|
-
case '--api-base': opts.apiBase = argv[++i]; break;
|
|
1335
|
-
case '--api-key': opts.apiKey = argv[++i]; break;
|
|
1336
|
-
case '--default-model': opts.defaultModel = argv[++i]; break;
|
|
1337
|
-
default: positional.push(argv[i]);
|
|
1338
|
-
}
|
|
1339
|
-
i++;
|
|
1340
|
-
}
|
|
1341
|
-
return { opts, positional };
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
27
|
+
const permissionManager = createPermissionManager(ui);
|
|
28
|
+
const { agentExecShell, agentExecFile } = createToolExecutor(permissionManager, ui);
|
|
29
|
+
const apiClient = createApiClient({
|
|
30
|
+
getConfig,
|
|
31
|
+
saveConfig: (nextConfig) => {
|
|
32
|
+
saveConfig(nextConfig);
|
|
33
|
+
config = nextConfig;
|
|
34
|
+
},
|
|
35
|
+
ui,
|
|
36
|
+
});
|
|
37
|
+
const { runAgentLoop } = createAgentRunner({
|
|
38
|
+
chatStream: apiClient.chatStream,
|
|
39
|
+
extractToolCalls,
|
|
40
|
+
agentExecShell,
|
|
41
|
+
agentExecFile,
|
|
42
|
+
ui,
|
|
43
|
+
});
|
|
44
|
+
const commands = createCommands({
|
|
45
|
+
getConfig,
|
|
46
|
+
setConfig,
|
|
47
|
+
permissionManager,
|
|
48
|
+
ui,
|
|
49
|
+
apiClient,
|
|
50
|
+
runAgentLoop,
|
|
51
|
+
readFileContext,
|
|
52
|
+
agentExecShell,
|
|
53
|
+
});
|
|
1345
54
|
|
|
1346
55
|
async function main() {
|
|
1347
56
|
const rawArgs = process.argv.slice(2);
|
|
1348
57
|
|
|
1349
58
|
if (rawArgs.length === 0) {
|
|
1350
|
-
await cmdChat({});
|
|
59
|
+
await commands.cmdChat({});
|
|
1351
60
|
return;
|
|
1352
61
|
}
|
|
1353
62
|
|
|
@@ -1391,30 +100,29 @@ Config: ${CONFIG_PATH}
|
|
|
1391
100
|
|
|
1392
101
|
if (command === 'chat') {
|
|
1393
102
|
const { opts } = parseArgs(rawArgs.slice(1));
|
|
1394
|
-
await cmdChat(opts);
|
|
103
|
+
await commands.cmdChat(opts);
|
|
1395
104
|
} else if (command === 'code') {
|
|
1396
105
|
const { opts, positional } = parseArgs(rawArgs.slice(1));
|
|
1397
|
-
await cmdCode(opts, positional);
|
|
106
|
+
await commands.cmdCode(opts, positional);
|
|
1398
107
|
} else if (command === 'edit') {
|
|
1399
108
|
const { opts, positional } = parseArgs(rawArgs.slice(1));
|
|
1400
|
-
await cmdEdit(opts, positional[0], positional.slice(1));
|
|
109
|
+
await commands.cmdEdit(opts, positional[0], positional.slice(1));
|
|
1401
110
|
} else if (command === 'shell') {
|
|
1402
111
|
const { opts, positional } = parseArgs(rawArgs.slice(1));
|
|
1403
|
-
await cmdShell(opts, positional);
|
|
112
|
+
await commands.cmdShell(opts, positional);
|
|
1404
113
|
} else if (command === 'models') {
|
|
1405
|
-
if (rawArgs[1] === 'add') await cmdModelsAdd();
|
|
1406
|
-
else await cmdModels();
|
|
114
|
+
if (rawArgs[1] === 'add') await commands.cmdModelsAdd();
|
|
115
|
+
else await commands.cmdModels();
|
|
1407
116
|
} else if (command === 'init') {
|
|
1408
117
|
const { opts } = parseArgs(rawArgs.slice(1));
|
|
1409
|
-
cmdInit(opts);
|
|
118
|
+
commands.cmdInit(opts);
|
|
1410
119
|
} else {
|
|
1411
|
-
// Unknown command — treat all args as chat mode
|
|
1412
120
|
const { opts } = parseArgs(rawArgs);
|
|
1413
|
-
await cmdChat(opts);
|
|
121
|
+
await commands.cmdChat(opts);
|
|
1414
122
|
}
|
|
1415
123
|
}
|
|
1416
124
|
|
|
1417
|
-
main().catch((
|
|
1418
|
-
process.stderr.write(`\n ${FG_RED}✗ Fatal: ${
|
|
125
|
+
main().catch((error) => {
|
|
126
|
+
process.stderr.write(`\n ${ui.FG_RED}✗ Fatal: ${error.message}${ui.RST}\n\n`);
|
|
1419
127
|
process.exit(1);
|
|
1420
128
|
});
|