@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/lib/tools.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
function createToolExecutor(permissionManager, ui) {
|
|
8
|
+
const { FG_DARK, FG_GRAY, FG_GREEN, FG_RED, RST } = ui;
|
|
9
|
+
|
|
10
|
+
async function agentExecShell(command) {
|
|
11
|
+
const approved = await permissionManager.askPermission('shell', command);
|
|
12
|
+
if (!approved) {
|
|
13
|
+
return { exit_code: -1, stdout: '', stderr: 'Permission denied by user' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log(` ${FG_DARK}$ ${command}${RST}`);
|
|
17
|
+
try {
|
|
18
|
+
const result = spawnSync(command, { shell: true, encoding: 'utf8', timeout: 60000 });
|
|
19
|
+
const stdout = result.stdout || '';
|
|
20
|
+
const stderr = result.stderr || '';
|
|
21
|
+
const combined = stdout + (stderr ? `\n${stderr}` : '');
|
|
22
|
+
const lines = combined.trim().split('\n').filter((line) => line !== '');
|
|
23
|
+
|
|
24
|
+
if (lines.length > 20) {
|
|
25
|
+
lines.slice(0, 15).forEach((line) => console.log(` ${FG_GRAY}${line}${RST}`));
|
|
26
|
+
console.log(` ${FG_DARK}... (${lines.length - 15} more lines)${RST}`);
|
|
27
|
+
} else {
|
|
28
|
+
lines.forEach((line) => console.log(` ${FG_GRAY}${line}${RST}`));
|
|
29
|
+
}
|
|
30
|
+
console.log();
|
|
31
|
+
|
|
32
|
+
return { exit_code: result.status ?? 0, stdout, stderr };
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.log(` ${FG_RED}✗ ${error.message}${RST}`);
|
|
35
|
+
return { exit_code: -1, stdout: '', stderr: error.message };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function agentExecFile(action, filePath, content = null) {
|
|
40
|
+
if (action === 'read') {
|
|
41
|
+
const approved = await permissionManager.askPermission('file', `Read ${filePath}`);
|
|
42
|
+
if (!approved) return { error: 'Permission denied' };
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const data = fs.readFileSync(filePath, 'utf8');
|
|
46
|
+
const lines = data.split('\n').length;
|
|
47
|
+
if (lines > 10) {
|
|
48
|
+
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Read ${filePath} (${lines} lines, ${data.length} chars)${RST}`);
|
|
49
|
+
} else {
|
|
50
|
+
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Read ${filePath}${RST}`);
|
|
51
|
+
}
|
|
52
|
+
return { content: data, path: filePath };
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.log(` ${FG_RED}✗ ${error.message}${RST}`);
|
|
55
|
+
return { error: error.message };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (action === 'write' || action === 'append') {
|
|
60
|
+
let desc = `${action === 'write' ? 'Write' : 'Append to'} ${filePath}`;
|
|
61
|
+
if (content) desc += ` (${content.length} chars)`;
|
|
62
|
+
const approved = await permissionManager.askPermission('file', desc);
|
|
63
|
+
if (!approved) return { error: 'Permission denied' };
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const dir = path.dirname(filePath);
|
|
67
|
+
if (dir && dir !== '.') fs.mkdirSync(dir, { recursive: true });
|
|
68
|
+
if (action === 'write') fs.writeFileSync(filePath, content || '');
|
|
69
|
+
else fs.appendFileSync(filePath, content || '');
|
|
70
|
+
const verb = action === 'write' ? 'Wrote' : 'Appended to';
|
|
71
|
+
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}${verb} ${filePath}${RST}`);
|
|
72
|
+
return { status: 'ok', path: filePath, bytes: (content || '').length };
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.log(` ${FG_RED}✗ ${error.message}${RST}`);
|
|
75
|
+
return { error: error.message };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { error: `Unknown action: ${action}` };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
agentExecFile,
|
|
84
|
+
agentExecShell,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extractToolCalls(text) {
|
|
89
|
+
const calls = [];
|
|
90
|
+
|
|
91
|
+
for (const match of text.matchAll(/```(?:shell|bash|sh)\n([\s\S]*?)```/g)) {
|
|
92
|
+
for (const line of match[1].trim().split('\n')) {
|
|
93
|
+
const cmd = line.trim();
|
|
94
|
+
if (cmd && !cmd.startsWith('#')) calls.push(['shell', cmd]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const match of text.matchAll(/<shell>([\s\S]*?)<\/shell>/g)) {
|
|
99
|
+
calls.push(['shell', match[1].trim()]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const match of text.matchAll(/<exec>([\s\S]*?)<\/exec>/g)) {
|
|
103
|
+
calls.push(['shell', match[1].trim()]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const match of text.matchAll(/<read_file>([\s\S]*?)<\/read_file>/g)) {
|
|
107
|
+
calls.push(['read', match[1].trim()]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const match of text.matchAll(/<write_file\s+path="([^"]+)">([\s\S]*?)<\/write_file>/g)) {
|
|
111
|
+
calls.push(['write', match[1], match[2]]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return calls;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
createToolExecutor,
|
|
119
|
+
extractToolCalls,
|
|
120
|
+
};
|
package/lib/ui.js
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
|
|
5
|
+
function getCols() {
|
|
6
|
+
return process.stdout.columns || 80;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const RST = '\x1b[0m';
|
|
10
|
+
const BOLD = '\x1b[1m';
|
|
11
|
+
const DIM = '\x1b[2m';
|
|
12
|
+
|
|
13
|
+
const FG_GRAY = '\x1b[38;5;245m';
|
|
14
|
+
const FG_DARK = '\x1b[38;5;240m';
|
|
15
|
+
const FG_BLUE = '\x1b[38;5;75m';
|
|
16
|
+
const FG_CYAN = '\x1b[38;5;116m';
|
|
17
|
+
const FG_GREEN = '\x1b[38;5;114m';
|
|
18
|
+
const FG_YELLOW = '\x1b[38;5;222m';
|
|
19
|
+
const FG_RED = '\x1b[38;5;203m';
|
|
20
|
+
const FG_TEAL = '\x1b[38;5;73m';
|
|
21
|
+
|
|
22
|
+
const BOX_H = '─';
|
|
23
|
+
const BOX_V = '│';
|
|
24
|
+
const BOX_TL = '╭';
|
|
25
|
+
const BOX_TR = '╮';
|
|
26
|
+
const BOX_BL = '╰';
|
|
27
|
+
const BOX_BR = '╯';
|
|
28
|
+
|
|
29
|
+
const FG_CODE_BG = '\x1b[48;5;236m';
|
|
30
|
+
const FG_CODE_BORDER = '\x1b[38;5;240m';
|
|
31
|
+
const FG_CODE_LANG = '\x1b[38;5;75m';
|
|
32
|
+
const FG_INLINE_CODE = '\x1b[38;5;215m';
|
|
33
|
+
const FG_HEADING = '\x1b[38;5;75m';
|
|
34
|
+
const FG_BULLET = '\x1b[38;5;114m';
|
|
35
|
+
const FG_BOLD_TEXT = '\x1b[38;5;255m';
|
|
36
|
+
const FG_DIFF_ADD = '\x1b[38;5;114m';
|
|
37
|
+
const FG_DIFF_DEL = '\x1b[38;5;203m';
|
|
38
|
+
const FG_DIFF_HDR = '\x1b[38;5;75m';
|
|
39
|
+
const FG_FILEPATH = '\x1b[38;5;222m';
|
|
40
|
+
const FG_TAG = '\x1b[38;5;176m';
|
|
41
|
+
|
|
42
|
+
const KEYWORDS = new Set([
|
|
43
|
+
'def', 'class', 'import', 'from', 'return', 'if', 'else', 'elif',
|
|
44
|
+
'for', 'while', 'try', 'except', 'finally', 'with', 'as', 'in',
|
|
45
|
+
'not', 'and', 'or', 'is', 'None', 'True', 'False', 'async', 'await',
|
|
46
|
+
'function', 'const', 'let', 'var', 'export', 'require', 'func',
|
|
47
|
+
'type', 'struct', 'interface', 'package', 'fn', 'pub', 'use',
|
|
48
|
+
'mod', 'impl', 'match', 'enum', 'self', 'print', 'len', 'range',
|
|
49
|
+
'yield', 'lambda', 'raise', 'pass', 'break', 'continue', 'del',
|
|
50
|
+
'global', 'nonlocal', 'assert',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
function hr(char = '─', color = FG_DARK) {
|
|
54
|
+
process.stdout.write(`${color}${char.repeat(getCols())}${RST}\n`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function stripAnsi(str) {
|
|
58
|
+
return str.replace(/\x1b\[[^m]*m/g, '');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function boxLine(text, width) {
|
|
62
|
+
const w = width || (getCols() - 4);
|
|
63
|
+
const visible = stripAnsi(text).length;
|
|
64
|
+
const pad = Math.max(0, w - visible);
|
|
65
|
+
return ` ${FG_DARK}${BOX_V}${RST} ${text}${' '.repeat(pad)}${FG_DARK}${BOX_V}${RST}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function insertCharAt(text, index, value) {
|
|
69
|
+
const chars = Array.from(text || '');
|
|
70
|
+
chars.splice(index, 0, value);
|
|
71
|
+
return chars.join('');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function removeCharAt(text, index) {
|
|
75
|
+
const chars = Array.from(text || '');
|
|
76
|
+
chars.splice(index, 1);
|
|
77
|
+
return chars.join('');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isPrintableKey(str, key = {}) {
|
|
81
|
+
if (!str || key.ctrl || key.meta) return false;
|
|
82
|
+
if (key.name === 'return' || key.name === 'enter' || key.name === 'tab') return false;
|
|
83
|
+
if (key.name && ['up', 'down', 'left', 'right', 'home', 'end', 'pageup', 'pagedown', 'escape', 'delete', 'backspace'].includes(key.name)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return !/[\x00-\x1f\x7f]/.test(str);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readInteractiveInput(promptText, options = {}) {
|
|
90
|
+
const {
|
|
91
|
+
allowed = null,
|
|
92
|
+
immediate = false,
|
|
93
|
+
trim = false,
|
|
94
|
+
allowCursorNavigation = false,
|
|
95
|
+
history = null,
|
|
96
|
+
} = options;
|
|
97
|
+
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
if (!process.stdin.isTTY) {
|
|
100
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
101
|
+
rl.question(promptText, (answer) => {
|
|
102
|
+
rl.close();
|
|
103
|
+
resolve({ type: 'submit', value: trim ? (answer || '').trim() : (answer || '') });
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const wasRaw = typeof process.stdin.isRaw === 'boolean' ? process.stdin.isRaw : false;
|
|
109
|
+
let buffer = '';
|
|
110
|
+
let cursor = 0;
|
|
111
|
+
let done = false;
|
|
112
|
+
let historyIndex = Array.isArray(history) ? history.length : -1;
|
|
113
|
+
let historyActive = false;
|
|
114
|
+
|
|
115
|
+
readline.emitKeypressEvents(process.stdin);
|
|
116
|
+
process.stdin.setRawMode(true);
|
|
117
|
+
process.stdin.resume();
|
|
118
|
+
|
|
119
|
+
const render = () => {
|
|
120
|
+
readline.cursorTo(process.stdout, 0);
|
|
121
|
+
readline.clearLine(process.stdout, 0);
|
|
122
|
+
process.stdout.write(`${promptText}${buffer}`);
|
|
123
|
+
const promptWidth = stripAnsi(promptText).length;
|
|
124
|
+
readline.cursorTo(process.stdout, promptWidth + cursor);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const finish = (result, addNewline = true) => {
|
|
128
|
+
if (done) return;
|
|
129
|
+
done = true;
|
|
130
|
+
process.stdin.setRawMode(wasRaw);
|
|
131
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
132
|
+
if (addNewline) process.stdout.write('\n');
|
|
133
|
+
resolve(result);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const onKeypress = (str, key = {}) => {
|
|
137
|
+
if (key.ctrl && key.name === 'c') {
|
|
138
|
+
if (buffer) {
|
|
139
|
+
buffer = '';
|
|
140
|
+
cursor = 0;
|
|
141
|
+
render();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
finish({ type: 'sigint' }, false);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (key.ctrl && key.name === 'd') {
|
|
149
|
+
if (!buffer) finish({ type: 'eof' }, false);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
154
|
+
finish({ type: 'submit', value: trim ? buffer.trim() : buffer });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (key.name === 'backspace' || key.name === 'delete') {
|
|
159
|
+
if (key.name === 'backspace' && cursor > 0) {
|
|
160
|
+
buffer = removeCharAt(buffer, cursor - 1);
|
|
161
|
+
cursor--;
|
|
162
|
+
historyActive = false;
|
|
163
|
+
render();
|
|
164
|
+
} else if (key.name === 'delete' && cursor < Array.from(buffer).length) {
|
|
165
|
+
buffer = removeCharAt(buffer, cursor);
|
|
166
|
+
historyActive = false;
|
|
167
|
+
render();
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (allowCursorNavigation && key.name === 'left') {
|
|
173
|
+
if (cursor > 0) {
|
|
174
|
+
cursor--;
|
|
175
|
+
render();
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (allowCursorNavigation && key.name === 'right') {
|
|
181
|
+
if (cursor < Array.from(buffer).length) {
|
|
182
|
+
cursor++;
|
|
183
|
+
render();
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (allowCursorNavigation && key.name === 'home') {
|
|
189
|
+
cursor = 0;
|
|
190
|
+
render();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (allowCursorNavigation && key.name === 'end') {
|
|
195
|
+
cursor = Array.from(buffer).length;
|
|
196
|
+
render();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (Array.isArray(history) && (key.name === 'up' || key.name === 'down')) {
|
|
201
|
+
const canEnterHistory = buffer.length === 0;
|
|
202
|
+
if (!history.length) return;
|
|
203
|
+
if (!historyActive && !canEnterHistory) return;
|
|
204
|
+
|
|
205
|
+
historyActive = true;
|
|
206
|
+
if (key.name === 'up') {
|
|
207
|
+
historyIndex = Math.max(0, historyIndex - 1);
|
|
208
|
+
} else {
|
|
209
|
+
historyIndex = Math.min(history.length, historyIndex + 1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
buffer = historyIndex >= history.length ? '' : history[historyIndex];
|
|
213
|
+
cursor = Array.from(buffer).length;
|
|
214
|
+
render();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (key.name && ['up', 'down', 'left', 'right', 'home', 'end', 'pageup', 'pagedown', 'escape', 'tab'].includes(key.name)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!isPrintableKey(str, key)) return;
|
|
223
|
+
if (allowed && !allowed.includes(str)) return;
|
|
224
|
+
|
|
225
|
+
if (immediate) {
|
|
226
|
+
buffer = str;
|
|
227
|
+
cursor = Array.from(buffer).length;
|
|
228
|
+
historyActive = false;
|
|
229
|
+
render();
|
|
230
|
+
finish({ type: 'submit', value: str });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
buffer = insertCharAt(buffer, cursor, str);
|
|
235
|
+
cursor++;
|
|
236
|
+
historyActive = false;
|
|
237
|
+
render();
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
process.stdin.on('keypress', onKeypress);
|
|
241
|
+
render();
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
class StreamRenderer {
|
|
246
|
+
constructor() {
|
|
247
|
+
this.buffer = '';
|
|
248
|
+
this.inCodeBlock = false;
|
|
249
|
+
this.codeLang = '';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
feed(chunk) {
|
|
253
|
+
this.buffer += chunk;
|
|
254
|
+
while (this.buffer.includes('\n')) {
|
|
255
|
+
const idx = this.buffer.indexOf('\n');
|
|
256
|
+
const line = this.buffer.slice(0, idx);
|
|
257
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
258
|
+
this._renderLine(line);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
flush() {
|
|
263
|
+
if (this.buffer) {
|
|
264
|
+
this._renderLine(this.buffer);
|
|
265
|
+
this.buffer = '';
|
|
266
|
+
}
|
|
267
|
+
if (this.inCodeBlock) {
|
|
268
|
+
process.stdout.write(` ${FG_CODE_BORDER}╰${'─'.repeat(50)}${RST}\n`);
|
|
269
|
+
this.inCodeBlock = false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
_renderLine(line) {
|
|
274
|
+
if (line.startsWith('```') && !this.inCodeBlock) {
|
|
275
|
+
this.inCodeBlock = true;
|
|
276
|
+
this.codeLang = line.slice(3).trim();
|
|
277
|
+
const label = this.codeLang ? ` ${this.codeLang} ` : '';
|
|
278
|
+
process.stdout.write(
|
|
279
|
+
` ${FG_CODE_BORDER}╭${'─'.repeat(20)}${RST}${FG_CODE_LANG}${label}${RST}` +
|
|
280
|
+
`${FG_CODE_BORDER}${'─'.repeat(Math.max(1, 30 - label.length))}${RST}\n`
|
|
281
|
+
);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (line.trim() === '```' && this.inCodeBlock) {
|
|
286
|
+
process.stdout.write(` ${FG_CODE_BORDER}╰${'─'.repeat(50)}${RST}\n`);
|
|
287
|
+
this.inCodeBlock = false;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (this.inCodeBlock) {
|
|
292
|
+
process.stdout.write(` ${FG_CODE_BORDER}│${RST} ${FG_CODE_BG}${this._colorizeCode(line)}${RST}\n`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (/^\+(?!\+\+)/.test(line)) {
|
|
297
|
+
process.stdout.write(` ${FG_DIFF_ADD}${line}${RST}\n`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (/^-(?!--)/.test(line)) {
|
|
301
|
+
process.stdout.write(` ${FG_DIFF_DEL}${line}${RST}\n`);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (line.startsWith('@@')) {
|
|
305
|
+
process.stdout.write(` ${FG_DIFF_HDR}${line}${RST}\n`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (/^<(exec|shell|read_file|write_file)/.test(line)) {
|
|
309
|
+
process.stdout.write(` ${FG_TAG}${line}${RST}\n`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (/^<\/(exec|shell|read_file|write_file)/.test(line)) {
|
|
313
|
+
process.stdout.write(` ${FG_TAG}${line}${RST}\n`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const hm = line.match(/^(#{1,4})\s+(.*)/);
|
|
318
|
+
if (hm) {
|
|
319
|
+
const text = hm[2];
|
|
320
|
+
if (hm[1].length <= 2) {
|
|
321
|
+
process.stdout.write(` ${FG_HEADING}${BOLD}${hm[1]} ${text}${RST}\n`);
|
|
322
|
+
} else {
|
|
323
|
+
process.stdout.write(` ${FG_HEADING}${hm[1]} ${text}${RST}\n`);
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const bm = line.match(/^(\s*)([-*•])\s+(.*)/);
|
|
329
|
+
if (bm) {
|
|
330
|
+
process.stdout.write(` ${bm[1]}${FG_BULLET}•${RST} ${this._inlineFormat(bm[3])}\n`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const nm = line.match(/^(\s*)(\d+\.)\s+(.*)/);
|
|
335
|
+
if (nm) {
|
|
336
|
+
process.stdout.write(` ${nm[1]}${FG_CYAN}${nm[2]}${RST} ${this._inlineFormat(nm[3])}\n`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
process.stdout.write(` ${this._inlineFormat(line)}\n`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
_inlineFormat(text) {
|
|
344
|
+
text = text.replace(/\*\*(.+?)\*\*/g, `${FG_BOLD_TEXT}${BOLD}$1${RST}`);
|
|
345
|
+
text = text.replace(/`([^`]+)`/g, `${FG_INLINE_CODE}$1${RST}`);
|
|
346
|
+
text = text.replace(/(?<!\w)((?:\/[\w\-.]+)+(?:\.\w+)?)/g, `${FG_FILEPATH}$1${RST}`);
|
|
347
|
+
return text;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
_colorizeCode(line) {
|
|
351
|
+
const C_KW = '\x1b[38;5;176m';
|
|
352
|
+
const C_STR = '\x1b[38;5;114m';
|
|
353
|
+
const C_CMT = '\x1b[38;5;242m';
|
|
354
|
+
const C_NUM = '\x1b[38;5;215m';
|
|
355
|
+
const C_RST = `${RST}${FG_CODE_BG}`;
|
|
356
|
+
|
|
357
|
+
let result = '';
|
|
358
|
+
let i = 0;
|
|
359
|
+
|
|
360
|
+
while (i < line.length) {
|
|
361
|
+
if (line[i] === '#' || (line[i] === '/' && line[i + 1] === '/')) {
|
|
362
|
+
result += `${C_CMT}${line.slice(i)}${C_RST}`;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (line[i] === '"' || line[i] === "'") {
|
|
367
|
+
const quote = line[i];
|
|
368
|
+
let j = i + 1;
|
|
369
|
+
while (j < line.length && line[j] !== quote) {
|
|
370
|
+
if (line[j] === '\\') j++;
|
|
371
|
+
j++;
|
|
372
|
+
}
|
|
373
|
+
j = Math.min(j + 1, line.length);
|
|
374
|
+
result += `${C_STR}${line.slice(i, j)}${C_RST}`;
|
|
375
|
+
i = j;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (/[a-zA-Z_]/.test(line[i])) {
|
|
380
|
+
let j = i;
|
|
381
|
+
while (j < line.length && /\w/.test(line[j])) j++;
|
|
382
|
+
const word = line.slice(i, j);
|
|
383
|
+
result += KEYWORDS.has(word) ? `${C_KW}${word}${C_RST}` : word;
|
|
384
|
+
i = j;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (/\d/.test(line[i])) {
|
|
389
|
+
let j = i;
|
|
390
|
+
while (j < line.length && /[\d.]/.test(line[j])) j++;
|
|
391
|
+
result += `${C_NUM}${line.slice(i, j)}${C_RST}`;
|
|
392
|
+
i = j;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
result += line[i++];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function printBanner() {
|
|
404
|
+
const w = Math.min(getCols() - 4, 60);
|
|
405
|
+
console.log();
|
|
406
|
+
console.log(` ${FG_DARK}${BOX_TL}${BOX_H.repeat(w + 2)}${BOX_TR}${RST}`);
|
|
407
|
+
console.log(boxLine('', w));
|
|
408
|
+
console.log(boxLine(`${FG_TEAL}${BOLD}◆ Semalt.AI${RST}`, w));
|
|
409
|
+
console.log(boxLine(`${FG_GRAY}Self-hosted AI coding assistant${RST}`, w));
|
|
410
|
+
console.log(boxLine('', w));
|
|
411
|
+
console.log(` ${FG_DARK}${BOX_BL}${BOX_H.repeat(w + 2)}${BOX_BR}${RST}`);
|
|
412
|
+
console.log();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function printStatusBar(model, cwd) {
|
|
416
|
+
const left = `${FG_TEAL}${BOLD}◆${RST} ${FG_GRAY}${model}${RST}`;
|
|
417
|
+
const right = `${FG_DARK}${cwd}${RST}`;
|
|
418
|
+
console.log(` ${left} ${FG_DARK}│${RST} ${right}`);
|
|
419
|
+
hr();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function printHelpHints() {
|
|
423
|
+
const hints = [
|
|
424
|
+
[`${FG_BLUE}/help${RST}`, 'commands'],
|
|
425
|
+
[`${FG_BLUE}/model${RST}`, 'switch'],
|
|
426
|
+
[`${FG_BLUE}/file${RST}`, 'context'],
|
|
427
|
+
[`${FG_BLUE}/clear${RST}`, 'reset'],
|
|
428
|
+
];
|
|
429
|
+
process.stdout.write(` ${FG_DARK}Tips:${RST}`);
|
|
430
|
+
for (const [cmd, desc] of hints) {
|
|
431
|
+
process.stdout.write(` ${cmd} ${FG_DARK}${desc}${RST}`);
|
|
432
|
+
}
|
|
433
|
+
console.log();
|
|
434
|
+
console.log();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
module.exports = {
|
|
438
|
+
BOLD,
|
|
439
|
+
DIM,
|
|
440
|
+
FG_BLUE,
|
|
441
|
+
FG_CYAN,
|
|
442
|
+
FG_DARK,
|
|
443
|
+
FG_GRAY,
|
|
444
|
+
FG_GREEN,
|
|
445
|
+
FG_RED,
|
|
446
|
+
FG_TEAL,
|
|
447
|
+
FG_YELLOW,
|
|
448
|
+
RST,
|
|
449
|
+
StreamRenderer,
|
|
450
|
+
getCols,
|
|
451
|
+
hr,
|
|
452
|
+
printBanner,
|
|
453
|
+
printHelpHints,
|
|
454
|
+
printStatusBar,
|
|
455
|
+
readInteractiveInput,
|
|
456
|
+
stripAnsi,
|
|
457
|
+
};
|