@semalt-ai/code 1.4.4 → 1.6.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/lib/agent.js ADDED
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ function createAgentRunner({ chatStream, extractToolCalls, agentExecShell, agentExecFile, ui }) {
4
+ const { BOLD, FG_DARK, FG_GRAY, FG_TEAL, FG_YELLOW, RST, getCols } = ui;
5
+
6
+ async function runAgentLoop(messages, model, maxIterations = 10) {
7
+ const cols = getCols();
8
+
9
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
10
+ console.log();
11
+ console.log(` ${FG_DARK}${'─'.repeat(Math.min(cols, 70) - 4)}${RST}`);
12
+ process.stdout.write(` ${FG_TEAL}${BOLD}◆ Semalt.AI${RST}`);
13
+ if (iteration > 0) process.stdout.write(` ${FG_DARK}(step ${iteration + 1})${RST}`);
14
+ console.log();
15
+ console.log(` ${FG_DARK}${'─'.repeat(Math.min(cols, 70) - 4)}${RST}`);
16
+ console.log();
17
+ process.stdout.write(' ');
18
+
19
+ const reply = await chatStream(messages, { model });
20
+ if (!reply) break;
21
+
22
+ messages.push({ role: 'assistant', content: reply });
23
+
24
+ const toolCalls = extractToolCalls(reply);
25
+ if (toolCalls.length === 0) break;
26
+
27
+ console.log(`\n ${FG_TEAL}◆${RST} ${FG_GRAY}Found ${toolCalls.length} action(s) to execute${RST}`);
28
+
29
+ const results = [];
30
+ let aborted = false;
31
+
32
+ for (const call of toolCalls) {
33
+ if (call[0] === 'shell') {
34
+ const result = await agentExecShell(call[1]);
35
+ if (result.stderr === 'Permission denied by user') {
36
+ results.push(`Command \`${call[1]}\`: Permission denied by user.`);
37
+ aborted = true;
38
+ } else {
39
+ let out = result.stdout;
40
+ if (result.stderr) out += `\nSTDERR: ${result.stderr}`;
41
+ results.push(`Command \`${call[1]}\`:\nExit code: ${result.exit_code}\n${out}`);
42
+ }
43
+ continue;
44
+ }
45
+
46
+ if (call[0] === 'read') {
47
+ const result = await agentExecFile('read', call[1]);
48
+ if (result.error) results.push(`Read ${call[1]}: Error — ${result.error}`);
49
+ else results.push(`File ${call[1]}:\n${result.content}`);
50
+ continue;
51
+ }
52
+
53
+ if (call[0] === 'write') {
54
+ const result = await agentExecFile('write', call[1], call[2]);
55
+ if (result.error) results.push(`Write ${call[1]}: Error — ${result.error}`);
56
+ else results.push(`Wrote ${result.bytes} bytes to ${call[1]}`);
57
+ }
58
+ }
59
+
60
+ const feedback = results.join('\n\n');
61
+ messages.push({
62
+ role: 'user',
63
+ content: `Tool execution results:\n\n${feedback}\n\nContinue with the task. If everything is done, summarize what was accomplished.`,
64
+ });
65
+
66
+ if (aborted) {
67
+ console.log(`\n ${FG_YELLOW}⚠${RST} ${FG_GRAY}Some actions were denied. Continuing with partial results.${RST}`);
68
+ }
69
+ }
70
+
71
+ return messages;
72
+ }
73
+
74
+ return {
75
+ runAgentLoop,
76
+ };
77
+ }
78
+
79
+ module.exports = {
80
+ createAgentRunner,
81
+ };
package/lib/api.js ADDED
@@ -0,0 +1,282 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const https = require('https');
5
+ const { URL } = require('url');
6
+
7
+ function createApiClient({ getConfig, saveConfig, ui }) {
8
+ const {
9
+ BOLD,
10
+ DIM,
11
+ FG_DARK,
12
+ FG_GRAY,
13
+ FG_GREEN,
14
+ FG_RED,
15
+ FG_TEAL,
16
+ RST,
17
+ StreamRenderer,
18
+ getCols,
19
+ printStatusBar,
20
+ } = ui;
21
+
22
+ function apiUrl(urlPath) {
23
+ const config = getConfig();
24
+ const base = (config.api_base || '').replace(/\/$/, '');
25
+ const normalizedBase = /\/v1$/i.test(base) ? base : `${base}/v1`;
26
+ const normalizedPath = urlPath.startsWith('/v1/') ? urlPath.slice(3) : urlPath;
27
+ return `${normalizedBase}${normalizedPath}`;
28
+ }
29
+
30
+ function describeModelProfile(profile) {
31
+ return `${profile.model} @ ${profile.api_base}`;
32
+ }
33
+
34
+ function setActiveModelProfile(profile) {
35
+ const config = getConfig();
36
+ config.api_base = profile.api_base;
37
+ config.api_key = profile.api_key;
38
+ config.default_model = profile.model;
39
+ saveConfig(config);
40
+ }
41
+
42
+ function chooseSavedModelProfile(rl, currentModel, cwd, onDone) {
43
+ const config = getConfig();
44
+ if (!config.models.length) {
45
+ console.log(` ${FG_RED}✗${RST} ${FG_GRAY}No saved model profiles. Use semalt-code models add first.${RST}`);
46
+ onDone(currentModel);
47
+ return;
48
+ }
49
+
50
+ console.log();
51
+ console.log(` ${FG_TEAL}${BOLD}◆ Saved Models${RST}`);
52
+ console.log(` ${FG_DARK}${'─'.repeat(40)}${RST}`);
53
+ config.models.forEach((profile, index) => {
54
+ const active = profile.api_base === config.api_base &&
55
+ profile.api_key === config.api_key &&
56
+ profile.model === currentModel;
57
+ const marker = active ? `${FG_GREEN}●${RST}` : `${FG_DARK}○${RST}`;
58
+ console.log(` ${marker} ${ui.FG_CYAN}${index + 1}.${RST} ${describeModelProfile(profile)}`);
59
+ });
60
+ console.log();
61
+
62
+ rl.question(` ${FG_TEAL}${BOLD}Select model>${RST} `, (answer) => {
63
+ const selected = Number((answer || '').trim());
64
+ if (!Number.isInteger(selected) || selected < 1 || selected > config.models.length) {
65
+ console.log(` ${FG_RED}✗${RST} ${FG_GRAY}Invalid selection${RST}`);
66
+ onDone(currentModel);
67
+ return;
68
+ }
69
+
70
+ const profile = config.models[selected - 1];
71
+ setActiveModelProfile(profile);
72
+ console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Model profile → ${describeModelProfile(profile)}${RST}`);
73
+ printStatusBar(profile.model, cwd);
74
+ onDone(profile.model);
75
+ });
76
+ }
77
+
78
+ function estimateTokens(text) {
79
+ return Math.floor((text || '').length / 4);
80
+ }
81
+
82
+ function httpRequest(urlStr, options, body) {
83
+ return new Promise((resolve, reject) => {
84
+ const url = new URL(urlStr);
85
+ const lib = url.protocol === 'https:' ? https : http;
86
+ const reqOpts = {
87
+ hostname: url.hostname,
88
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
89
+ path: url.pathname + url.search,
90
+ method: options.method || 'GET',
91
+ headers: options.headers || {},
92
+ };
93
+
94
+ const req = lib.request(reqOpts, (res) => resolve(res));
95
+ req.on('error', reject);
96
+
97
+ if (options.timeout) {
98
+ req.setTimeout(options.timeout, () => {
99
+ req.destroy(new Error('Request timed out'));
100
+ });
101
+ }
102
+
103
+ if (body) req.write(body);
104
+ req.end();
105
+ });
106
+ }
107
+
108
+ async function chatStream(messages, { model, temperature, maxTokens } = {}) {
109
+ const config = getConfig();
110
+ const payload = {
111
+ model: model || config.default_model,
112
+ messages,
113
+ temperature: temperature !== undefined ? temperature : config.temperature,
114
+ stream: true,
115
+ };
116
+
117
+ if (maxTokens !== undefined) payload.max_tokens = maxTokens;
118
+
119
+ const body = JSON.stringify(payload);
120
+ let res;
121
+
122
+ try {
123
+ res = await httpRequest(apiUrl('/v1/chat/completions'), {
124
+ method: 'POST',
125
+ timeout: config.request_timeout_ms,
126
+ headers: {
127
+ 'Content-Type': 'application/json',
128
+ 'Authorization': `Bearer ${config.api_key}`,
129
+ 'Content-Length': Buffer.byteLength(body),
130
+ },
131
+ }, body);
132
+ } catch (error) {
133
+ process.stdout.write(`\n ${FG_RED}✗ ${error.message}${RST}\n`);
134
+ return '';
135
+ }
136
+
137
+ if (res.statusCode !== 200) {
138
+ process.stdout.write(`\n ${FG_RED}✗ Error: HTTP ${res.statusCode}${RST}\n`);
139
+ res.resume();
140
+ return '';
141
+ }
142
+
143
+ return new Promise((resolve) => {
144
+ const startTime = Date.now();
145
+ let fullText = '';
146
+ let reasoningText = '';
147
+ let tokenCount = 0;
148
+ let inReasoning = false;
149
+ const renderer = new StreamRenderer();
150
+ let lineBuffer = '';
151
+
152
+ res.setEncoding('utf8');
153
+
154
+ res.on('data', (chunk) => {
155
+ lineBuffer += chunk;
156
+ const lines = lineBuffer.split('\n');
157
+ lineBuffer = lines.pop();
158
+
159
+ for (const line of lines) {
160
+ if (!line.startsWith('data: ')) continue;
161
+ const data = line.slice(6).trim();
162
+ if (data === '[DONE]') continue;
163
+
164
+ try {
165
+ const obj = JSON.parse(data);
166
+ const delta = ((obj.choices || [])[0] || {}).delta || {};
167
+
168
+ const reasoning = delta.reasoning_content || '';
169
+ if (reasoning) {
170
+ if (!inReasoning) {
171
+ inReasoning = true;
172
+ process.stdout.write(`\n ${FG_DARK}${DIM}⟨thinking⟩${RST}`);
173
+ }
174
+ reasoningText += reasoning;
175
+ tokenCount++;
176
+ if (tokenCount % 20 === 0) process.stdout.write(`${FG_DARK}.${RST}`);
177
+ }
178
+
179
+ const content = delta.content || '';
180
+ if (content) {
181
+ if (inReasoning) {
182
+ inReasoning = false;
183
+ process.stdout.write(`${FG_DARK}⟨/thinking⟩${RST}\n`);
184
+ }
185
+ renderer.feed(content);
186
+ fullText += content;
187
+ tokenCount++;
188
+ }
189
+ } catch {}
190
+ }
191
+ });
192
+
193
+ res.on('end', () => {
194
+ renderer.flush();
195
+ const elapsed = (Date.now() - startTime) / 1000;
196
+ const estTokens = estimateTokens(fullText + reasoningText);
197
+ const tps = tokenCount / (elapsed || 1);
198
+ const cols = getCols();
199
+ process.stdout.write(`\n ${FG_DARK}${'─'.repeat(Math.min(cols, 60) - 4)}${RST}\n`);
200
+ let costLine = `${FG_DARK}~${estTokens} tokens · ${elapsed.toFixed(1)}s · ${Math.round(tps)} tok/s${RST}`;
201
+ if (reasoningText) costLine += ` ${FG_DARK}· ${estimateTokens(reasoningText)} thinking${RST}`;
202
+ process.stdout.write(` ${costLine}\n`);
203
+ resolve(fullText);
204
+ });
205
+
206
+ res.on('error', (error) => {
207
+ process.stdout.write(`\n ${FG_RED}✗ ${error.message}${RST}\n`);
208
+ resolve('');
209
+ });
210
+ });
211
+ }
212
+
213
+ async function chatSync(messages, { model } = {}) {
214
+ const config = getConfig();
215
+ const payload = {
216
+ model: model || config.default_model,
217
+ messages,
218
+ temperature: config.temperature,
219
+ stream: false,
220
+ };
221
+
222
+ const body = JSON.stringify(payload);
223
+ let res;
224
+
225
+ try {
226
+ res = await httpRequest(apiUrl('/v1/chat/completions'), {
227
+ method: 'POST',
228
+ timeout: config.request_timeout_ms,
229
+ headers: {
230
+ 'Content-Type': 'application/json',
231
+ 'Authorization': `Bearer ${config.api_key}`,
232
+ 'Content-Length': Buffer.byteLength(body),
233
+ },
234
+ }, body);
235
+ } catch (error) {
236
+ console.log(` ${FG_RED}✗ ${error.message}${RST}`);
237
+ return '';
238
+ }
239
+
240
+ return new Promise((resolve) => {
241
+ let data = '';
242
+ res.setEncoding('utf8');
243
+ res.on('data', (chunk) => {
244
+ data += chunk;
245
+ });
246
+ res.on('end', () => {
247
+ if (res.statusCode !== 200) {
248
+ console.log(` ${FG_RED}✗ Error: HTTP ${res.statusCode} — ${data}${RST}`);
249
+ resolve('');
250
+ return;
251
+ }
252
+
253
+ try {
254
+ const parsed = JSON.parse(data);
255
+ const content = parsed.choices[0].message.content;
256
+ console.log(content);
257
+ resolve(content);
258
+ } catch (error) {
259
+ console.log(` ${FG_RED}✗ Parse error: ${error.message}${RST}`);
260
+ resolve('');
261
+ }
262
+ });
263
+ res.on('error', (error) => {
264
+ console.log(` ${FG_RED}✗ ${error.message}${RST}`);
265
+ resolve('');
266
+ });
267
+ });
268
+ }
269
+
270
+ return {
271
+ chatStream,
272
+ chatSync,
273
+ chooseSavedModelProfile,
274
+ describeModelProfile,
275
+ estimateTokens,
276
+ setActiveModelProfile,
277
+ };
278
+ }
279
+
280
+ module.exports = {
281
+ createApiClient,
282
+ };
package/lib/args.js ADDED
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ function parseArgs(argv) {
4
+ const opts = {};
5
+ const positional = [];
6
+ let i = 0;
7
+
8
+ while (i < argv.length) {
9
+ switch (argv[i]) {
10
+ case '-m':
11
+ case '--model':
12
+ opts.model = argv[++i];
13
+ break;
14
+ case '-f':
15
+ case '--file':
16
+ (opts.file = opts.file || []).push(argv[++i]);
17
+ break;
18
+ case '-a':
19
+ case '--analyze':
20
+ opts.analyze = true;
21
+ break;
22
+ case '--dry-run':
23
+ opts.dryRun = true;
24
+ break;
25
+ case '--api-base':
26
+ opts.apiBase = argv[++i];
27
+ break;
28
+ case '--api-key':
29
+ opts.apiKey = argv[++i];
30
+ break;
31
+ case '--default-model':
32
+ opts.defaultModel = argv[++i];
33
+ break;
34
+ default:
35
+ positional.push(argv[i]);
36
+ }
37
+ i++;
38
+ }
39
+
40
+ return { opts, positional };
41
+ }
42
+
43
+ module.exports = {
44
+ parseArgs,
45
+ };