@semalt-ai/code 1.6.0 → 1.8.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/.claude/settings.local.json +8 -0
- package/ARCHITECTURE.md +99 -0
- package/CLAUDE.md +349 -0
- package/README.md +16 -2
- package/index.js +79 -7
- package/lib/agent.js +508 -39
- package/lib/api.js +347 -77
- package/lib/args.js +34 -0
- package/lib/audit.js +31 -0
- package/lib/commands.js +1018 -183
- package/lib/config.js +68 -5
- package/lib/constants.js +58 -0
- package/lib/context.js +2 -6
- package/lib/metrics.js +94 -0
- package/lib/permissions.js +180 -49
- package/lib/prompts.js +89 -13
- package/lib/storage.js +96 -0
- package/lib/tools.js +896 -35
- package/lib/ui/ansi.js +64 -0
- package/lib/ui/chat-history.js +217 -0
- package/lib/ui/create-ui.js +474 -0
- package/lib/ui/diff.js +243 -0
- package/lib/ui/input-field.js +1176 -0
- package/lib/ui/layout.js +53 -0
- package/lib/ui/legacy.js +130 -0
- package/lib/ui/status-bar.js +130 -0
- package/lib/ui/stream.js +158 -0
- package/lib/ui/utils.js +45 -0
- package/lib/ui.js +42 -598
- package/package.json +1 -1
- package/path +1 -0
package/lib/api.js
CHANGED
|
@@ -14,9 +14,8 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
14
14
|
FG_RED,
|
|
15
15
|
FG_TEAL,
|
|
16
16
|
RST,
|
|
17
|
+
StatusBar,
|
|
17
18
|
StreamRenderer,
|
|
18
|
-
getCols,
|
|
19
|
-
printStatusBar,
|
|
20
19
|
} = ui;
|
|
21
20
|
|
|
22
21
|
function apiUrl(urlPath) {
|
|
@@ -27,8 +26,21 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
27
26
|
return `${normalizedBase}${normalizedPath}`;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
function
|
|
31
|
-
|
|
29
|
+
function dashboardUrl(urlPath) {
|
|
30
|
+
const config = getConfig();
|
|
31
|
+
const base = (config.dashboard_url || '').replace(/\/$/, '');
|
|
32
|
+
const normalizedPath = urlPath.startsWith('/') ? urlPath : `/${urlPath}`;
|
|
33
|
+
return `${base}${normalizedPath}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requireAuthToken() {
|
|
37
|
+
const config = getConfig();
|
|
38
|
+
if (!config.auth_token) {
|
|
39
|
+
const error = new Error('Not logged in. Run semalt login first.');
|
|
40
|
+
error.statusCode = 401;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
return config.auth_token;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
function setActiveModelProfile(profile) {
|
|
@@ -39,46 +51,14 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
39
51
|
saveConfig(config);
|
|
40
52
|
}
|
|
41
53
|
|
|
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
54
|
function estimateTokens(text) {
|
|
79
55
|
return Math.floor((text || '').length / 4);
|
|
80
56
|
}
|
|
81
57
|
|
|
58
|
+
// Discovered context limit for this process lifetime.
|
|
59
|
+
// Set on the first context-overflow 400; used to proactively trim all subsequent calls.
|
|
60
|
+
let _sessionInputLimit = null;
|
|
61
|
+
|
|
82
62
|
function httpRequest(urlStr, options, body) {
|
|
83
63
|
return new Promise((resolve, reject) => {
|
|
84
64
|
const url = new URL(urlStr);
|
|
@@ -105,50 +85,309 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
105
85
|
});
|
|
106
86
|
}
|
|
107
87
|
|
|
108
|
-
async function
|
|
88
|
+
async function requestJson(urlStr, { method = 'GET', timeout, headers = {}, body } = {}) {
|
|
89
|
+
const requestBody = body === undefined ? undefined : JSON.stringify(body);
|
|
90
|
+
const finalHeaders = { ...headers };
|
|
91
|
+
if (requestBody !== undefined) {
|
|
92
|
+
finalHeaders['Content-Type'] = 'application/json';
|
|
93
|
+
finalHeaders['Content-Length'] = Buffer.byteLength(requestBody);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const res = await httpRequest(urlStr, {
|
|
97
|
+
method,
|
|
98
|
+
timeout: timeout || getConfig().request_timeout_ms,
|
|
99
|
+
headers: finalHeaders,
|
|
100
|
+
}, requestBody);
|
|
101
|
+
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
let data = '';
|
|
104
|
+
res.setEncoding('utf8');
|
|
105
|
+
res.on('data', (chunk) => {
|
|
106
|
+
data += chunk;
|
|
107
|
+
});
|
|
108
|
+
res.on('end', () => {
|
|
109
|
+
let parsed = null;
|
|
110
|
+
try {
|
|
111
|
+
parsed = data ? JSON.parse(data) : null;
|
|
112
|
+
} catch {
|
|
113
|
+
parsed = data ? { error: data } : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
117
|
+
const error = new Error((parsed && parsed.error) || `HTTP ${res.statusCode}`);
|
|
118
|
+
error.statusCode = res.statusCode;
|
|
119
|
+
error.data = parsed;
|
|
120
|
+
reject(error);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
resolve(parsed);
|
|
125
|
+
});
|
|
126
|
+
res.on('error', reject);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function requestCliLogin() {
|
|
131
|
+
return requestJson(dashboardUrl('/api/auth/cli/request'), {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
timeout: 15000,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getCliLoginStatus(id, hash) {
|
|
138
|
+
return requestJson(dashboardUrl('/api/auth/cli/status'), {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
timeout: 15000,
|
|
141
|
+
body: { id, hash },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function dashboardWhoAmI() {
|
|
146
|
+
const authToken = requireAuthToken();
|
|
147
|
+
return requestJson(dashboardUrl('/api/auth/me'), {
|
|
148
|
+
method: 'GET',
|
|
149
|
+
timeout: 15000,
|
|
150
|
+
headers: {
|
|
151
|
+
'Authorization': `Bearer ${authToken}`,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function dashboardLogout() {
|
|
157
|
+
const authToken = requireAuthToken();
|
|
158
|
+
return requestJson(dashboardUrl('/api/auth/logout'), {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
timeout: 15000,
|
|
161
|
+
headers: {
|
|
162
|
+
'Authorization': `Bearer ${authToken}`,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function dashboardListModels() {
|
|
168
|
+
const authToken = requireAuthToken();
|
|
169
|
+
return requestJson(dashboardUrl('/api/models'), {
|
|
170
|
+
method: 'GET',
|
|
171
|
+
timeout: 15000,
|
|
172
|
+
headers: {
|
|
173
|
+
'Authorization': `Bearer ${authToken}`,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function dashboardGetModelForCli(id) {
|
|
179
|
+
const authToken = requireAuthToken();
|
|
180
|
+
return requestJson(dashboardUrl(`/api/models/${encodeURIComponent(String(id))}/cli`), {
|
|
181
|
+
method: 'GET',
|
|
182
|
+
timeout: 15000,
|
|
183
|
+
headers: {
|
|
184
|
+
'Authorization': `Bearer ${authToken}`,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function dashboardCreateChat(title, modelDbId) {
|
|
190
|
+
const authToken = requireAuthToken();
|
|
191
|
+
return requestJson(dashboardUrl('/api/chats'), {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
timeout: 15000,
|
|
194
|
+
headers: { 'Authorization': `Bearer ${authToken}` },
|
|
195
|
+
body: { title, model_id: modelDbId },
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function dashboardListChats() {
|
|
200
|
+
const authToken = requireAuthToken();
|
|
201
|
+
return requestJson(dashboardUrl('/api/chats'), {
|
|
202
|
+
method: 'GET',
|
|
203
|
+
timeout: 15000,
|
|
204
|
+
headers: { 'Authorization': `Bearer ${authToken}` },
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function dashboardGetChat(id) {
|
|
209
|
+
const authToken = requireAuthToken();
|
|
210
|
+
return requestJson(dashboardUrl(`/api/chats/${encodeURIComponent(String(id))}`), {
|
|
211
|
+
method: 'GET',
|
|
212
|
+
timeout: 15000,
|
|
213
|
+
headers: { 'Authorization': `Bearer ${authToken}` },
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function dashboardSaveMessages(chatId, messages) {
|
|
218
|
+
const authToken = requireAuthToken();
|
|
219
|
+
return requestJson(dashboardUrl(`/api/chats/${encodeURIComponent(String(chatId))}/messages/batch`), {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
timeout: 15000,
|
|
222
|
+
headers: { 'Authorization': `Bearer ${authToken}` },
|
|
223
|
+
body: { messages },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function chatStream(messages, { model, temperature, maxTokens, linePrefix = '', showThink = false, onToken = null, silent = false } = {}) {
|
|
109
228
|
const config = getConfig();
|
|
229
|
+
|
|
230
|
+
// Fit messages into tokenBudget tokens.
|
|
231
|
+
// Uses chars/3 — conservative for token-dense content (code, JSON, HTML).
|
|
232
|
+
//
|
|
233
|
+
// Always keeps: system prompt + first non-system message (original task).
|
|
234
|
+
// Drops intermediate messages oldest-first, then truncates the last tail
|
|
235
|
+
// message (typically a large tool result) if still over budget.
|
|
236
|
+
function trimToTokenBudget(msgs, tokenBudget) {
|
|
237
|
+
const CHARS_PER_TOKEN = 3;
|
|
238
|
+
const system = msgs.filter((m) => m.role === 'system');
|
|
239
|
+
const nonSystem = msgs.filter((m) => m.role !== 'system');
|
|
240
|
+
if (nonSystem.length === 0) return [...system];
|
|
241
|
+
|
|
242
|
+
const pinned = nonSystem[0]; // original task — never dropped
|
|
243
|
+
let tail = nonSystem.slice(1);
|
|
244
|
+
|
|
245
|
+
const estimate = () => {
|
|
246
|
+
const all = tail.length > 0 ? [...system, pinned, ...tail] : [...system, pinned];
|
|
247
|
+
return Math.floor(JSON.stringify(all).length / CHARS_PER_TOKEN);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
while (tail.length > 1 && estimate() > tokenBudget) {
|
|
251
|
+
tail = tail.slice(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (tail.length === 1 && estimate() > tokenBudget) {
|
|
255
|
+
const msg = tail[0];
|
|
256
|
+
const otherChars = JSON.stringify([...system, pinned]).length;
|
|
257
|
+
const available = tokenBudget * CHARS_PER_TOKEN - otherChars - 200;
|
|
258
|
+
if (available > 0 && typeof msg.content === 'string' && msg.content.length > available) {
|
|
259
|
+
tail = [{ ...msg, content: '[…content truncated to fit model limit…]\n' + msg.content.slice(-available) }];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (tail.length === 0 && estimate() > tokenBudget) {
|
|
264
|
+
const systemChars = JSON.stringify(system).length;
|
|
265
|
+
const available = tokenBudget * CHARS_PER_TOKEN - systemChars - 200;
|
|
266
|
+
if (available > 0 && typeof pinned.content === 'string' && pinned.content.length > available) {
|
|
267
|
+
return [...system, { ...pinned, content: '[…content truncated to fit model limit…]\n' + pinned.content.slice(-available) }];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return tail.length > 0 ? [...system, pinned, ...tail] : [...system, pinned];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Proactive trim: apply the session input limit discovered from a prior 400.
|
|
275
|
+
let trimmedMessages = messages;
|
|
276
|
+
if (_sessionInputLimit !== null) {
|
|
277
|
+
if (Math.floor(JSON.stringify(messages).length / 3) > _sessionInputLimit) {
|
|
278
|
+
trimmedMessages = trimToTokenBudget(messages, _sessionInputLimit);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
110
282
|
const payload = {
|
|
111
283
|
model: model || config.default_model,
|
|
112
|
-
messages,
|
|
284
|
+
messages: trimmedMessages,
|
|
113
285
|
temperature: temperature !== undefined ? temperature : config.temperature,
|
|
114
286
|
stream: true,
|
|
115
287
|
};
|
|
116
288
|
|
|
117
289
|
if (maxTokens !== undefined) payload.max_tokens = maxTokens;
|
|
118
290
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
res = await httpRequest(apiUrl('/v1/chat/completions'), {
|
|
291
|
+
async function doRequest(msgs) {
|
|
292
|
+
const reqPayload = { ...payload, messages: msgs };
|
|
293
|
+
const reqBody = JSON.stringify(reqPayload);
|
|
294
|
+
const res = await httpRequest(apiUrl('/v1/chat/completions'), {
|
|
124
295
|
method: 'POST',
|
|
125
296
|
timeout: config.request_timeout_ms,
|
|
126
297
|
headers: {
|
|
127
298
|
'Content-Type': 'application/json',
|
|
128
299
|
'Authorization': `Bearer ${config.api_key}`,
|
|
129
|
-
'Content-Length': Buffer.byteLength(
|
|
300
|
+
'Content-Length': Buffer.byteLength(reqBody),
|
|
130
301
|
},
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
302
|
+
}, reqBody);
|
|
303
|
+
|
|
304
|
+
if (res.statusCode !== 200) {
|
|
305
|
+
const errBody = await new Promise((resolve) => {
|
|
306
|
+
let d = '';
|
|
307
|
+
res.setEncoding('utf8');
|
|
308
|
+
res.on('data', (c) => { d += c; });
|
|
309
|
+
res.on('end', () => resolve(d));
|
|
310
|
+
res.on('error', () => resolve(''));
|
|
311
|
+
});
|
|
312
|
+
let detail = '';
|
|
313
|
+
let parsedErr = null;
|
|
314
|
+
try {
|
|
315
|
+
parsedErr = JSON.parse(errBody);
|
|
316
|
+
detail = (parsedErr && (parsedErr.error?.message || parsedErr.error || parsedErr.message)) || '';
|
|
317
|
+
} catch { detail = errBody.slice(0, 200); }
|
|
318
|
+
const err = new Error(`HTTP ${res.statusCode}${detail ? `: ${detail}` : ''}`);
|
|
319
|
+
err.statusCode = res.statusCode;
|
|
320
|
+
err.parsedErr = parsedErr;
|
|
321
|
+
err.detail = detail;
|
|
322
|
+
throw err;
|
|
323
|
+
}
|
|
324
|
+
return res;
|
|
135
325
|
}
|
|
136
326
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
327
|
+
// On payload-too-large errors, trim and retry.
|
|
328
|
+
// 400 with context-overflow detail → parse exact context window, budget = window/2
|
|
329
|
+
// 413 Request Entity Too Large (Nginx/proxy) → no size hint, halve current estimate
|
|
330
|
+
// In both cases _sessionInputLimit is set so all subsequent calls are proactively trimmed.
|
|
331
|
+
let res;
|
|
332
|
+
try {
|
|
333
|
+
res = await doRequest(trimmedMessages);
|
|
334
|
+
} catch (err) {
|
|
335
|
+
const is400Overflow = err.statusCode === 400 && err.detail &&
|
|
336
|
+
/context.length|input.token|context_length|maximum.*token|token.*limit/i.test(err.detail);
|
|
337
|
+
const is413 = err.statusCode === 413;
|
|
338
|
+
|
|
339
|
+
if (is400Overflow || is413) {
|
|
340
|
+
let budget;
|
|
341
|
+
if (is400Overflow) {
|
|
342
|
+
const limitMatch = err.detail.match(/context length is only (\d+)/i) ||
|
|
343
|
+
err.detail.match(/maximum.*?(\d+)\s*token/i);
|
|
344
|
+
const contextWindow = limitMatch ? parseInt(limitMatch[1], 10) : null;
|
|
345
|
+
budget = contextWindow
|
|
346
|
+
? Math.floor(contextWindow / 2)
|
|
347
|
+
: Math.floor(Math.floor(JSON.stringify(trimmedMessages).length / 3) * 0.5);
|
|
348
|
+
} else {
|
|
349
|
+
// 413: no token info available — halve the estimated size of the current payload.
|
|
350
|
+
budget = Math.floor(Math.floor(JSON.stringify(trimmedMessages).length / 3) * 0.5);
|
|
351
|
+
}
|
|
352
|
+
_sessionInputLimit = budget;
|
|
353
|
+
trimmedMessages = trimToTokenBudget(trimmedMessages, budget);
|
|
354
|
+
res = await doRequest(trimmedMessages);
|
|
355
|
+
} else {
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
141
358
|
}
|
|
142
359
|
|
|
143
|
-
return new Promise((resolve) => {
|
|
360
|
+
return new Promise((resolve, reject) => {
|
|
144
361
|
const startTime = Date.now();
|
|
145
362
|
let fullText = '';
|
|
146
363
|
let reasoningText = '';
|
|
147
364
|
let tokenCount = 0;
|
|
148
365
|
let inReasoning = false;
|
|
149
|
-
|
|
366
|
+
let streamUsage = null;
|
|
367
|
+
let resolved = false;
|
|
368
|
+
const renderer = new StreamRenderer({ firstLinePrefix: linePrefix, showThink });
|
|
369
|
+
if (!silent) {
|
|
370
|
+
process.stdout.write('\n');
|
|
371
|
+
renderer._linesWritten = 1;
|
|
372
|
+
}
|
|
373
|
+
let firstContentToken = true;
|
|
150
374
|
let lineBuffer = '';
|
|
151
375
|
|
|
376
|
+
function finalize() {
|
|
377
|
+
if (resolved) return;
|
|
378
|
+
resolved = true;
|
|
379
|
+
if (!silent) renderer.flush();
|
|
380
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
381
|
+
const tps = tokenCount / (elapsed || 1);
|
|
382
|
+
if (StatusBar.current) {
|
|
383
|
+
let latency = `${Math.round(tps)} tok/s · ${elapsed.toFixed(1)}s`;
|
|
384
|
+
if (reasoningText) latency += ` · ${estimateTokens(reasoningText)} think`;
|
|
385
|
+
StatusBar.current.liveUpdate({ tokens: `${tokenCount} tok`, latency });
|
|
386
|
+
StatusBar.current.render();
|
|
387
|
+
}
|
|
388
|
+
resolve({ content: fullText, usage: streamUsage });
|
|
389
|
+
}
|
|
390
|
+
|
|
152
391
|
res.setEncoding('utf8');
|
|
153
392
|
|
|
154
393
|
res.on('data', (chunk) => {
|
|
@@ -159,53 +398,76 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
159
398
|
for (const line of lines) {
|
|
160
399
|
if (!line.startsWith('data: ')) continue;
|
|
161
400
|
const data = line.slice(6).trim();
|
|
162
|
-
if (data === '[DONE]')
|
|
401
|
+
if (data === '[DONE]') {
|
|
402
|
+
finalize();
|
|
403
|
+
res.destroy();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
163
406
|
|
|
164
407
|
try {
|
|
165
408
|
const obj = JSON.parse(data);
|
|
409
|
+
if (obj.usage && (obj.usage.prompt_tokens !== undefined || obj.usage.completion_tokens !== undefined)) {
|
|
410
|
+
streamUsage = obj.usage;
|
|
411
|
+
}
|
|
166
412
|
const delta = ((obj.choices || [])[0] || {}).delta || {};
|
|
167
413
|
|
|
168
414
|
const reasoning = delta.reasoning_content || '';
|
|
169
415
|
if (reasoning) {
|
|
170
416
|
if (!inReasoning) {
|
|
171
417
|
inReasoning = true;
|
|
172
|
-
|
|
418
|
+
if (showThink) {
|
|
419
|
+
process.stdout.write(`\n ${FG_DARK}${DIM}⟨thinking⟩${RST}`);
|
|
420
|
+
renderer._linesWritten++;
|
|
421
|
+
}
|
|
173
422
|
}
|
|
174
423
|
reasoningText += reasoning;
|
|
175
424
|
tokenCount++;
|
|
176
|
-
if (
|
|
425
|
+
if (showThink) {
|
|
426
|
+
process.stdout.write(`${FG_DARK}${DIM}${reasoning}${RST}`);
|
|
427
|
+
}
|
|
177
428
|
}
|
|
178
429
|
|
|
179
430
|
const content = delta.content || '';
|
|
180
431
|
if (content) {
|
|
181
432
|
if (inReasoning) {
|
|
182
433
|
inReasoning = false;
|
|
183
|
-
|
|
434
|
+
if (showThink && !silent) {
|
|
435
|
+
process.stdout.write(`${FG_DARK}⟨/thinking⟩${RST}\n`);
|
|
436
|
+
renderer._linesWritten++;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (onToken) {
|
|
440
|
+
if (firstContentToken) {
|
|
441
|
+
firstContentToken = false;
|
|
442
|
+
if (StatusBar.current) StatusBar.current.update({ status: 'streaming' });
|
|
443
|
+
}
|
|
444
|
+
onToken(content);
|
|
445
|
+
} else {
|
|
446
|
+
renderer.feed(content);
|
|
184
447
|
}
|
|
185
|
-
renderer.feed(content);
|
|
186
448
|
fullText += content;
|
|
187
449
|
tokenCount++;
|
|
450
|
+
if (tokenCount % 20 === 0 && StatusBar.current) {
|
|
451
|
+
const elapsedSec = (Date.now() - startTime) / 1000 || 0.001;
|
|
452
|
+
StatusBar.current.liveUpdate({
|
|
453
|
+
tokens: `${tokenCount} tok`,
|
|
454
|
+
latency: `${Math.round(tokenCount / elapsedSec)} tok/s`,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
188
457
|
}
|
|
189
458
|
} catch {}
|
|
190
459
|
}
|
|
191
460
|
});
|
|
192
461
|
|
|
193
462
|
res.on('end', () => {
|
|
194
|
-
|
|
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);
|
|
463
|
+
finalize();
|
|
204
464
|
});
|
|
205
465
|
|
|
206
466
|
res.on('error', (error) => {
|
|
207
|
-
|
|
208
|
-
|
|
467
|
+
if (!resolved) {
|
|
468
|
+
resolved = true;
|
|
469
|
+
reject(error);
|
|
470
|
+
}
|
|
209
471
|
});
|
|
210
472
|
});
|
|
211
473
|
}
|
|
@@ -270,9 +532,17 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
270
532
|
return {
|
|
271
533
|
chatStream,
|
|
272
534
|
chatSync,
|
|
273
|
-
|
|
274
|
-
|
|
535
|
+
dashboardCreateChat,
|
|
536
|
+
dashboardGetChat,
|
|
537
|
+
dashboardGetModelForCli,
|
|
538
|
+
dashboardListChats,
|
|
539
|
+
dashboardListModels,
|
|
540
|
+
dashboardLogout,
|
|
541
|
+
dashboardSaveMessages,
|
|
542
|
+
dashboardWhoAmI,
|
|
275
543
|
estimateTokens,
|
|
544
|
+
getCliLoginStatus,
|
|
545
|
+
requestCliLogin,
|
|
276
546
|
setActiveModelProfile,
|
|
277
547
|
};
|
|
278
548
|
}
|
package/lib/args.js
CHANGED
|
@@ -28,9 +28,43 @@ function parseArgs(argv) {
|
|
|
28
28
|
case '--api-key':
|
|
29
29
|
opts.apiKey = argv[++i];
|
|
30
30
|
break;
|
|
31
|
+
case '--dashboard-url':
|
|
32
|
+
opts.dashboardUrl = argv[++i];
|
|
33
|
+
break;
|
|
31
34
|
case '--default-model':
|
|
32
35
|
opts.defaultModel = argv[++i];
|
|
33
36
|
break;
|
|
37
|
+
case '-r':
|
|
38
|
+
case '--resume':
|
|
39
|
+
opts.resume = argv[++i];
|
|
40
|
+
break;
|
|
41
|
+
case '--allow-fs':
|
|
42
|
+
(opts.allowedTiers = opts.allowedTiers || []).push('fs');
|
|
43
|
+
break;
|
|
44
|
+
case '--allow-exec':
|
|
45
|
+
(opts.allowedTiers = opts.allowedTiers || []).push('exec');
|
|
46
|
+
break;
|
|
47
|
+
case '--allow-net':
|
|
48
|
+
(opts.allowedTiers = opts.allowedTiers || []).push('net');
|
|
49
|
+
break;
|
|
50
|
+
case '--allow-all':
|
|
51
|
+
opts.allowedTiers = ['fs', 'exec', 'net', 'sys'];
|
|
52
|
+
break;
|
|
53
|
+
case '--readonly':
|
|
54
|
+
opts.readonly = true;
|
|
55
|
+
break;
|
|
56
|
+
case '--new':
|
|
57
|
+
opts.new = true;
|
|
58
|
+
break;
|
|
59
|
+
case '--show-think':
|
|
60
|
+
opts.showThink = true;
|
|
61
|
+
break;
|
|
62
|
+
case '--debug':
|
|
63
|
+
opts.debug = true;
|
|
64
|
+
break;
|
|
65
|
+
case '--system-prompt':
|
|
66
|
+
opts.systemPromptFile = argv[++i];
|
|
67
|
+
break;
|
|
34
68
|
default:
|
|
35
69
|
positional.push(argv[i]);
|
|
36
70
|
}
|
package/lib/audit.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const AUDIT_LOG = path.join(os.homedir(), '.semalt-ai', 'audit.log');
|
|
8
|
+
|
|
9
|
+
function logToolCall(tag, input, approved, resultStatus) {
|
|
10
|
+
try {
|
|
11
|
+
let safeInput = input;
|
|
12
|
+
if (tag === 'write_file' && input !== null && typeof input === 'object' && 'content' in input) {
|
|
13
|
+
const n = typeof input.content === 'string' ? input.content.length : 0;
|
|
14
|
+
safeInput = { ...input, content: `<${n} bytes>` };
|
|
15
|
+
}
|
|
16
|
+
let inputStr = typeof safeInput === 'string' ? safeInput : JSON.stringify(safeInput);
|
|
17
|
+
if (inputStr.length > 200) inputStr = inputStr.slice(0, 197) + '...';
|
|
18
|
+
const entry = JSON.stringify({
|
|
19
|
+
ts: new Date().toISOString(),
|
|
20
|
+
tag,
|
|
21
|
+
input: inputStr,
|
|
22
|
+
approved: Boolean(approved),
|
|
23
|
+
result: resultStatus,
|
|
24
|
+
});
|
|
25
|
+
fs.appendFileSync(AUDIT_LOG, entry + '\n');
|
|
26
|
+
} catch {
|
|
27
|
+
// never throw
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { AUDIT_LOG, logToolCall };
|