@semalt-ai/code 1.8.3 → 1.8.4
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 +14 -7
- package/lib/agent.js +189 -58
- package/lib/api.js +11 -34
- package/lib/commands.js +206 -121
- package/lib/config.js +1 -0
- package/lib/constants.js +1 -1
- package/lib/permissions.js +9 -8
- package/lib/prompts.js +4 -7
- package/lib/tools.js +14 -7
- package/lib/ui/chat-history.js +19 -8
- package/lib/ui/create-ui.js +63 -38
- package/lib/ui/diff.js +4 -3
- package/lib/ui/format.js +247 -0
- package/lib/ui/input-field.js +134 -59
- package/lib/ui/layout.js +0 -2
- package/lib/ui/messages.js +44 -0
- package/lib/ui/select.js +114 -0
- package/lib/ui/status-bar.js +135 -28
- package/lib/ui/stream.js +8 -12
- package/lib/ui/terminal.js +2 -0
- package/lib/ui/theme.js +25 -4
- package/lib/ui/utils.js +94 -27
- package/lib/ui/writer.js +393 -45
- package/lib/ui.js +6 -6
- package/package.json +1 -1
- package/lib/ui/legacy.js +0 -130
package/lib/commands.js
CHANGED
|
@@ -8,6 +8,10 @@ const { getSystemPrompt } = require('./prompts');
|
|
|
8
8
|
const { SessionStorage } = require('./storage');
|
|
9
9
|
const { getSkippedOps, setUIActive } = require('./tools');
|
|
10
10
|
const { AUDIT_LOG } = require('./audit');
|
|
11
|
+
const { formatToolLine } = require('./ui/format');
|
|
12
|
+
const writerModule = require('./ui/writer');
|
|
13
|
+
const writer = writerModule;
|
|
14
|
+
const msgs = require('./ui/messages');
|
|
11
15
|
|
|
12
16
|
function formatTimeAgo(ts) {
|
|
13
17
|
const diffMs = Date.now() - ts;
|
|
@@ -41,9 +45,8 @@ function createCommands({
|
|
|
41
45
|
FG_TEAL,
|
|
42
46
|
FG_YELLOW,
|
|
43
47
|
RST,
|
|
44
|
-
|
|
48
|
+
approxTokens,
|
|
45
49
|
getCols,
|
|
46
|
-
hr,
|
|
47
50
|
boxLine,
|
|
48
51
|
interactiveSelect,
|
|
49
52
|
createUI,
|
|
@@ -100,29 +103,54 @@ function createCommands({
|
|
|
100
103
|
return null;
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
// Pick the first dashboard model when the user is authenticated but has
|
|
107
|
+
// not selected one yet. Persists credentials to config and returns
|
|
108
|
+
// { name, modelId } on success; null otherwise (not logged in, already
|
|
109
|
+
// selected, empty list, or API error).
|
|
110
|
+
async function ensureDefaultModel() {
|
|
111
|
+
const config = getConfig();
|
|
112
|
+
if (!config.auth_token) return null;
|
|
113
|
+
if (config.default_model && config.dashboard_model_id) return null;
|
|
114
|
+
let response;
|
|
115
|
+
try { response = await dashboardListModels(); } catch { return null; }
|
|
116
|
+
const models = Array.isArray(response && response.models) ? response.models : [];
|
|
117
|
+
if (!models.length) return null;
|
|
118
|
+
const first = models[0];
|
|
119
|
+
let credResp;
|
|
120
|
+
try { credResp = await dashboardGetModelForCli(first.id); } catch { return null; }
|
|
121
|
+
const model = credResp && credResp.model ? credResp.model : null;
|
|
122
|
+
if (!model) return null;
|
|
123
|
+
const contextLength = (Number.isInteger(model.context_length) && model.context_length > 0 ? model.context_length : null)
|
|
124
|
+
|| (Number.isInteger(model.max_tokens) && model.max_tokens > 0 ? model.max_tokens : null);
|
|
125
|
+
const updated = { ...config, api_base: model.base_url, api_key: model.api_key, default_model: model.model_id, dashboard_model_id: model.id };
|
|
126
|
+
if (contextLength !== null) updated.context_length = contextLength;
|
|
127
|
+
setConfig(updated);
|
|
128
|
+
return { name: model.name, modelId: model.model_id };
|
|
113
129
|
}
|
|
114
130
|
|
|
115
131
|
async function cmdChat(opts) {
|
|
132
|
+
await ensureDefaultModel();
|
|
133
|
+
|
|
134
|
+
// Build the three end-of-session artifacts that teardown emits as
|
|
135
|
+
// scrollback. Returning them as a plain object lets both exit paths
|
|
136
|
+
// (/exit submit and Ctrl+C onInterrupt) route through writer.teardown,
|
|
137
|
+
// which is the only place that can append them below the erased live
|
|
138
|
+
// region in a single atomic write.
|
|
139
|
+
function buildExitArtifacts() {
|
|
140
|
+
return {
|
|
141
|
+
summary: sessionMetrics ? sessionMetrics.summary() : '',
|
|
142
|
+
resumeHint: currentChatId !== null
|
|
143
|
+
? ` ${FG_DARK}Resume this chat: ${FG_CYAN}semalt-code --resume ${currentChatId}${RST}`
|
|
144
|
+
: '',
|
|
145
|
+
goodbye: ` ${FG_GRAY}Goodbye!${RST}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
116
149
|
const { chatHistory, statusBar, inputField, layout, destroy, redrawFixed } = createUI({
|
|
117
150
|
showThink: opts.showThink || false,
|
|
118
151
|
onInterrupt: (destroyFn) => {
|
|
119
152
|
saveSession();
|
|
120
|
-
destroyFn();
|
|
121
|
-
if (sessionMetrics) console.log('\n' + sessionMetrics.summary());
|
|
122
|
-
if (currentChatId !== null) {
|
|
123
|
-
console.log(` ${FG_DARK}Resume this chat: ${FG_CYAN}semalt-code --resume ${currentChatId}${RST}`);
|
|
124
|
-
}
|
|
125
|
-
console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
|
|
153
|
+
destroyFn(buildExitArtifacts());
|
|
126
154
|
process.exit(0);
|
|
127
155
|
},
|
|
128
156
|
});
|
|
@@ -157,6 +185,10 @@ function createCommands({
|
|
|
157
185
|
let currentModel = opts.model || getConfig().default_model;
|
|
158
186
|
let resolvedTokenLimit = await resolveTokenLimit(currentModel);
|
|
159
187
|
statusBar.setModel(currentModel);
|
|
188
|
+
// Seed the context indicator with the profile's limit up-front so it
|
|
189
|
+
// renders "0 / 200,000 tok (0%)" before the first API response, instead
|
|
190
|
+
// of appearing out of thin air once a turn completes.
|
|
191
|
+
statusBar.setContextLimit(resolvedTokenLimit);
|
|
160
192
|
let sessionMetrics = null;
|
|
161
193
|
// system prompt is prepended fresh on every API call in agent.js — never stored in history
|
|
162
194
|
let messages = [];
|
|
@@ -300,10 +332,12 @@ function createCommands({
|
|
|
300
332
|
}
|
|
301
333
|
}
|
|
302
334
|
|
|
303
|
-
// Pending selection state (for in-chat /history, /models, /chats)
|
|
335
|
+
// Pending selection state (for in-chat /history, /models, /chats).
|
|
336
|
+
// The picker renders into the writer's modal region — same band as the
|
|
337
|
+
// permission picker — so navigation redraws in place and only the final
|
|
338
|
+
// selection (or cancellation) leaves a line in scrollback.
|
|
304
339
|
let pendingAction = null;
|
|
305
340
|
const PAGE_SIZE = 5;
|
|
306
|
-
let listMsg = null;
|
|
307
341
|
|
|
308
342
|
function getNavSearchText(type, item) {
|
|
309
343
|
if (type === 'history') {
|
|
@@ -361,31 +395,27 @@ function createCommands({
|
|
|
361
395
|
return parts.join('\n');
|
|
362
396
|
}
|
|
363
397
|
|
|
364
|
-
function collapseListMsg(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
listMsg = null;
|
|
398
|
+
function collapseListMsg(_type, _item) {
|
|
399
|
+
// Modal is transient — clearing it removes the picker from view; the
|
|
400
|
+
// selection's success line is emitted to scrollback by
|
|
401
|
+
// handlePendingSelection.
|
|
402
|
+
writer.clearModal();
|
|
370
403
|
}
|
|
371
404
|
|
|
372
405
|
function showPendingStep() {
|
|
373
406
|
if (!pendingAction) return;
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
407
|
+
const lines = buildListContent().split('\n');
|
|
408
|
+
// Match the system-message bubble look so the modal reads as part of
|
|
409
|
+
// the same chat block: muted bullet on the title row, indented
|
|
410
|
+
// continuations underneath.
|
|
411
|
+
const modalLines = lines.length > 0
|
|
412
|
+
? [` ${FG_GRAY}●${RST} ${FG_GRAY}${lines[0]}${RST}`].concat(lines.slice(1).map((l) => ` ${l}`))
|
|
413
|
+
: [];
|
|
414
|
+
writer.setModal(modalLines);
|
|
382
415
|
}
|
|
383
416
|
|
|
384
417
|
function finalizeListMsg() {
|
|
385
|
-
|
|
386
|
-
chatHistory.removeById(listMsg.id);
|
|
387
|
-
listMsg = null;
|
|
388
|
-
}
|
|
418
|
+
writer.clearModal();
|
|
389
419
|
}
|
|
390
420
|
|
|
391
421
|
function activateNavCapture() {
|
|
@@ -447,6 +477,7 @@ function createCommands({
|
|
|
447
477
|
currentModel = loaded.model;
|
|
448
478
|
resolvedTokenLimit = await resolveTokenLimit(currentModel);
|
|
449
479
|
statusBar.setModel(currentModel);
|
|
480
|
+
statusBar.setContextLimit(resolvedTokenLimit);
|
|
450
481
|
}
|
|
451
482
|
displayLoadedMessages(messages);
|
|
452
483
|
chatHistory.addMessage({ role: 'system', content: `✓ Session loaded. Model → ${currentModel}` });
|
|
@@ -478,6 +509,7 @@ function createCommands({
|
|
|
478
509
|
currentModel = model.model_id;
|
|
479
510
|
resolvedTokenLimit = await resolveTokenLimit(currentModel);
|
|
480
511
|
statusBar.setModel(currentModel);
|
|
512
|
+
statusBar.setContextLimit(resolvedTokenLimit);
|
|
481
513
|
currentChatId = null;
|
|
482
514
|
chatHistory.addMessage({ role: 'system', content: `✓ Model → ${model.name} (${model.model_id})` });
|
|
483
515
|
statusBar.update('idle');
|
|
@@ -535,7 +567,7 @@ function createCommands({
|
|
|
535
567
|
// Exit
|
|
536
568
|
if (['exit', 'quit', '/exit', '/quit'].includes(text.toLowerCase())) {
|
|
537
569
|
saveSession();
|
|
538
|
-
destroy();
|
|
570
|
+
destroy(buildExitArtifacts());
|
|
539
571
|
resolveExit();
|
|
540
572
|
return;
|
|
541
573
|
}
|
|
@@ -569,6 +601,7 @@ function createCommands({
|
|
|
569
601
|
}
|
|
570
602
|
|
|
571
603
|
if (text === '/history') {
|
|
604
|
+
if (!getConfig().auth_token) { chatHistory.addMessage({ role: 'system', content: 'Not logged in. Run /login first.' }); return; }
|
|
572
605
|
const sessions = storage.list();
|
|
573
606
|
if (!sessions.length) { chatHistory.addMessage({ role: 'system', content: 'No saved sessions.' }); return; }
|
|
574
607
|
refreshInputSearchItems();
|
|
@@ -617,6 +650,14 @@ function createCommands({
|
|
|
617
650
|
inputField.setDisabled(true);
|
|
618
651
|
statusBar.update('thinking', 'Starting login...');
|
|
619
652
|
await _loginFlow(chatHistory, statusBar);
|
|
653
|
+
const picked = await ensureDefaultModel();
|
|
654
|
+
if (picked) {
|
|
655
|
+
currentModel = picked.modelId;
|
|
656
|
+
resolvedTokenLimit = await resolveTokenLimit(currentModel);
|
|
657
|
+
statusBar.setModel(currentModel);
|
|
658
|
+
statusBar.setContextLimit(resolvedTokenLimit);
|
|
659
|
+
chatHistory.addMessage({ role: 'system', content: `✓ Model → ${picked.name} (${picked.modelId})` });
|
|
660
|
+
}
|
|
620
661
|
statusBar.update('idle');
|
|
621
662
|
inputField.setDisabled(false);
|
|
622
663
|
return;
|
|
@@ -666,6 +707,7 @@ function createCommands({
|
|
|
666
707
|
}
|
|
667
708
|
|
|
668
709
|
if (text === '/models') {
|
|
710
|
+
if (!getConfig().auth_token) { chatHistory.addMessage({ role: 'system', content: 'Not logged in. Run /login first.' }); return; }
|
|
669
711
|
inputField.setDisabled(true);
|
|
670
712
|
statusBar.update('thinking', 'Loading models...');
|
|
671
713
|
try {
|
|
@@ -688,14 +730,17 @@ function createCommands({
|
|
|
688
730
|
}
|
|
689
731
|
|
|
690
732
|
if (text === '/model') {
|
|
733
|
+
if (!getConfig().auth_token) { chatHistory.addMessage({ role: 'system', content: 'Not logged in. Run /login first.' }); return; }
|
|
691
734
|
chatHistory.addMessage({ role: 'system', content: `Current model: ${currentModel}` });
|
|
692
735
|
return;
|
|
693
736
|
}
|
|
694
737
|
|
|
695
738
|
if (text.startsWith('/model ')) {
|
|
739
|
+
if (!getConfig().auth_token) { chatHistory.addMessage({ role: 'system', content: 'Not logged in. Run /login first.' }); return; }
|
|
696
740
|
currentModel = text.slice(7).trim();
|
|
697
741
|
resolvedTokenLimit = await resolveTokenLimit(currentModel);
|
|
698
742
|
statusBar.setModel(currentModel);
|
|
743
|
+
statusBar.setContextLimit(resolvedTokenLimit);
|
|
699
744
|
chatHistory.addMessage({ role: 'system', content: `✓ Model → ${currentModel}` });
|
|
700
745
|
return;
|
|
701
746
|
}
|
|
@@ -803,6 +848,11 @@ function createCommands({
|
|
|
803
848
|
inputField.setDisabled(true);
|
|
804
849
|
chatHistory.addMessage({ role: 'user', content: text });
|
|
805
850
|
statusBar.update('thinking', 'Thinking...');
|
|
851
|
+
// Bump the context-size indicator with this user message's approximate
|
|
852
|
+
// token count. It'll be overwritten with the exact prompt_tokens from
|
|
853
|
+
// the API response when the first turn completes — this just keeps the
|
|
854
|
+
// indicator reactive in the gap before that.
|
|
855
|
+
statusBar.addPendingTokens(approxTokens(text));
|
|
806
856
|
await createChatIfNeeded(text);
|
|
807
857
|
messages.push({ role: 'user', content: text });
|
|
808
858
|
|
|
@@ -845,29 +895,57 @@ function createCommands({
|
|
|
845
895
|
chatHistory.addMessage({ role: 'think', content });
|
|
846
896
|
statusBar.update('streaming', 'Streaming response');
|
|
847
897
|
},
|
|
848
|
-
onToolStart: (tag, input,
|
|
898
|
+
onToolStart: (tag, input, ctx) => {
|
|
849
899
|
const actionLabel = TAG_REGISTRY[tag]?.label || tag;
|
|
850
|
-
const short = input.length > 40 ? input.slice(0, 40) + '…' : input;
|
|
900
|
+
const short = input && input.length > 40 ? input.slice(0, 40) + '…' : (input || '');
|
|
851
901
|
const isDownload = tag === 'download' || tag === 'http_get';
|
|
852
902
|
if (isDownload) {
|
|
853
903
|
statusBar.update('waiting_download', `Waiting for download: ${short}`);
|
|
854
904
|
} else {
|
|
855
905
|
statusBar.update('tool', `${actionLabel}: ${short}`);
|
|
856
906
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
907
|
+
// Register the invocation with the writer's activity region.
|
|
908
|
+
// The render function is re-invoked by the writer on every
|
|
909
|
+
// redraw so the pending line's elapsed time stays current with
|
|
910
|
+
// the ticker cadence without an explicit refresh timer.
|
|
911
|
+
if (ctx && ctx.id) {
|
|
912
|
+
writerModule.startActivity(ctx.id, (elapsedMs) => formatToolLine({
|
|
913
|
+
status: 'pending',
|
|
863
914
|
tag,
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
915
|
+
arg: input,
|
|
916
|
+
attrs: ctx.attrs,
|
|
917
|
+
durationMs: elapsedMs,
|
|
918
|
+
}));
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
onToolEnd: (tag, result, durationMs, ctx) => {
|
|
922
|
+
const hasError = !!(ctx && ctx.error);
|
|
923
|
+
const finalLine = formatToolLine({
|
|
924
|
+
status: hasError ? 'failure' : 'success',
|
|
925
|
+
tag,
|
|
926
|
+
arg: ctx && ctx.attrs ? (ctx.attrs.command || ctx.attrs.path || ctx.attrs.url || ctx.attrs.src || ctx.attrs.key || ctx.attrs.name || ctx.attrs.pattern) : '',
|
|
927
|
+
attrs: ctx ? ctx.attrs : null,
|
|
928
|
+
durationMs,
|
|
929
|
+
meta: ctx ? ctx.meta : null,
|
|
930
|
+
error: ctx ? ctx.error : null,
|
|
931
|
+
});
|
|
932
|
+
if (ctx && ctx.id) {
|
|
933
|
+
writerModule.endActivity(ctx.id, finalLine);
|
|
868
934
|
} else {
|
|
869
|
-
|
|
870
|
-
|
|
935
|
+
// No invocation id means the agent-loop wasn't upgraded to pass
|
|
936
|
+
// structured context (shouldn't happen in practice). Fall back
|
|
937
|
+
// to a direct scrollback line so the tool still leaves a trace.
|
|
938
|
+
writerModule.scrollback(finalLine);
|
|
939
|
+
}
|
|
940
|
+
if (hasError) {
|
|
941
|
+
// Preserve the expandable error body as a follow-up tool
|
|
942
|
+
// bubble. Empty content suppresses its header so the scrollback
|
|
943
|
+
// line above (written by endActivity) isn't duplicated.
|
|
944
|
+
const body = typeof result === 'string' && result.trim() ? result : null;
|
|
945
|
+
if (body) {
|
|
946
|
+
chatHistory.addMessage({ role: 'tool', tag, content: '', output: body, isError: true });
|
|
947
|
+
}
|
|
948
|
+
statusBar.update('streaming', 'Streaming response');
|
|
871
949
|
}
|
|
872
950
|
},
|
|
873
951
|
onToken: (token) => {
|
|
@@ -958,18 +1036,13 @@ function createCommands({
|
|
|
958
1036
|
saveSession();
|
|
959
1037
|
});
|
|
960
1038
|
|
|
961
|
-
// Wait until user exits
|
|
1039
|
+
// Wait until user exits. The /exit submit handler already ran
|
|
1040
|
+
// destroy(buildExitArtifacts()), so the session summary, resume hint,
|
|
1041
|
+
// and goodbye have been emitted as scrollback inside teardown's
|
|
1042
|
+
// single atomic write. Nothing more to print here.
|
|
962
1043
|
await exitPromise;
|
|
963
1044
|
setUIActive(false);
|
|
964
1045
|
saveSession();
|
|
965
|
-
if (sessionMetrics) {
|
|
966
|
-
// Show summary in terminal after destroy
|
|
967
|
-
console.log('\n' + sessionMetrics.summary());
|
|
968
|
-
}
|
|
969
|
-
if (currentChatId !== null) {
|
|
970
|
-
console.log(` ${FG_DARK}Resume this chat: ${FG_CYAN}semalt-code --resume ${currentChatId}${RST}`);
|
|
971
|
-
}
|
|
972
|
-
console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
|
|
973
1046
|
}
|
|
974
1047
|
|
|
975
1048
|
async function _loginFlow(chatHistory, statusBar) {
|
|
@@ -1005,8 +1078,9 @@ function createCommands({
|
|
|
1005
1078
|
}
|
|
1006
1079
|
|
|
1007
1080
|
async function cmdCode(opts, promptArgs) {
|
|
1008
|
-
if (!promptArgs.length) {
|
|
1009
|
-
if (!getConfig().auth_token) {
|
|
1081
|
+
if (!promptArgs.length) { writer.scrollback(` ${FG_RED}Usage: semalt-code code <prompt>${RST}`); return; }
|
|
1082
|
+
if (!getConfig().auth_token) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Not logged in. Run semalt-code login first.${RST}\n`); return; }
|
|
1083
|
+
await ensureDefaultModel();
|
|
1010
1084
|
const model = opts.model || getConfig().default_model;
|
|
1011
1085
|
const userPrompt = promptArgs.join(' ');
|
|
1012
1086
|
const context = opts.file ? readFileContext(opts.file) : '';
|
|
@@ -1016,62 +1090,60 @@ function createCommands({
|
|
|
1016
1090
|
try { resolvedSystemPrompt = fs.readFileSync(opts.systemPromptFile, 'utf8'); } catch {}
|
|
1017
1091
|
}
|
|
1018
1092
|
let messages = [{ role: 'user', content: fullPrompt }];
|
|
1019
|
-
|
|
1020
|
-
statusBar.update({ model, status: 'thinking' });
|
|
1093
|
+
writer.scrollback(` ${FG_GRAY}◆ ${model}${RST}`);
|
|
1021
1094
|
const codeResult = await runAgentLoop(messages, model, undefined, null, {
|
|
1022
1095
|
debug: opts.debug || false,
|
|
1023
1096
|
systemPrompt: resolvedSystemPrompt,
|
|
1024
1097
|
systemPromptMode: getConfig().system_prompt_mode || 'system_role',
|
|
1025
1098
|
});
|
|
1026
1099
|
messages = codeResult.messages;
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
if (codeResult.metrics) console.log(codeResult.metrics.summary());
|
|
1100
|
+
writer.scrollback('\n');
|
|
1101
|
+
if (codeResult.metrics) writer.scrollback(codeResult.metrics.summary());
|
|
1030
1102
|
if (opts.dryRun) printDryRunSummary();
|
|
1031
1103
|
}
|
|
1032
1104
|
|
|
1033
1105
|
async function cmdEdit(opts, filePath, instructionArgs) {
|
|
1034
|
-
if (!filePath) {
|
|
1035
|
-
if (!getConfig().auth_token) {
|
|
1036
|
-
if (!fs.existsSync(filePath)) {
|
|
1106
|
+
if (!filePath) { writer.scrollback(` ${FG_RED}Usage: semalt-code edit <file> <instruction>${RST}`); return; }
|
|
1107
|
+
if (!getConfig().auth_token) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Not logged in. Run semalt-code login first.${RST}\n`); return; }
|
|
1108
|
+
if (!fs.existsSync(filePath)) { writer.scrollback(` ${FG_RED}✗ File not found: ${filePath}${RST}`); return; }
|
|
1109
|
+
await ensureDefaultModel();
|
|
1037
1110
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
1038
1111
|
const instruction = instructionArgs.join(' ');
|
|
1039
1112
|
const messages = [
|
|
1040
1113
|
{ role: 'system', content: 'You are Semalt.AI. Output ONLY the modified file. No explanations, no fences.' },
|
|
1041
1114
|
{ role: 'user', content: `File: ${filePath}\n\n\`\`\`\n${content}\n\`\`\`\n\nInstruction: ${instruction}` },
|
|
1042
1115
|
];
|
|
1043
|
-
|
|
1044
|
-
const editStatusBar = new StatusBar();
|
|
1045
|
-
editStatusBar.update({ model: opts.model || getConfig().default_model, status: 'editing' });
|
|
1116
|
+
writer.scrollback(` ${FG_GRAY}Editing ${filePath}...${RST}`);
|
|
1046
1117
|
let result = await chatSync(messages, { model: opts.model });
|
|
1047
|
-
editStatusBar.destroy();
|
|
1048
1118
|
if (result && !opts.dryRun) {
|
|
1049
1119
|
if (result.startsWith('```')) { const lines = result.split('\n'); result = lines.at(-1).trim() === '```' ? lines.slice(1, -1).join('\n') : lines.slice(1).join('\n'); }
|
|
1050
1120
|
fs.writeFileSync(filePath, result);
|
|
1051
|
-
|
|
1121
|
+
writer.scrollback(` ${FG_GREEN}✓ Saved: ${filePath}${RST}`);
|
|
1052
1122
|
} else if (opts.dryRun) {
|
|
1053
|
-
|
|
1123
|
+
writer.scrollback(` ${FG_YELLOW}⚠ Dry run — not modified${RST}`);
|
|
1054
1124
|
}
|
|
1055
1125
|
}
|
|
1056
1126
|
|
|
1057
1127
|
async function cmdShell(opts, commandArgs) {
|
|
1058
1128
|
const command = commandArgs.join(' ');
|
|
1059
|
-
if (!command) {
|
|
1129
|
+
if (!command) { writer.scrollback(` ${FG_RED}Usage: semalt-code shell <command>${RST}`); return; }
|
|
1060
1130
|
const result = await agentExecShell(command);
|
|
1061
1131
|
if (opts.analyze) {
|
|
1062
|
-
if (!getConfig().auth_token) {
|
|
1132
|
+
if (!getConfig().auth_token) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Not logged in. Run semalt-code login first.${RST}\n`); return; }
|
|
1133
|
+
await ensureDefaultModel();
|
|
1063
1134
|
const messages = [
|
|
1064
1135
|
{ role: 'system', content: 'You are Semalt.AI. Analyze the command output concisely.' },
|
|
1065
1136
|
{ role: 'user', content: `Command: ${command}\nExit: ${result.exit_code}\nStdout:\n${result.stdout}\nStderr:\n${result.stderr}` },
|
|
1066
1137
|
];
|
|
1067
|
-
|
|
1138
|
+
writer.scrollback(`\n ${FG_TEAL}${BOLD}◆ Semalt.AI${RST}\n`);
|
|
1139
|
+
// audit: allowed — non-TUI streaming prefix, must precede StreamRenderer sync writes.
|
|
1068
1140
|
process.stdout.write(' ');
|
|
1069
1141
|
try {
|
|
1070
1142
|
await chatStream(messages, { model: opts.model });
|
|
1071
1143
|
} catch (err) {
|
|
1072
|
-
|
|
1144
|
+
msgs.netError(err.message);
|
|
1073
1145
|
}
|
|
1074
|
-
|
|
1146
|
+
writer.scrollback('\n');
|
|
1075
1147
|
}
|
|
1076
1148
|
}
|
|
1077
1149
|
|
|
@@ -1079,10 +1151,10 @@ function createCommands({
|
|
|
1079
1151
|
const config = getConfig();
|
|
1080
1152
|
let response;
|
|
1081
1153
|
try { response = await dashboardListModels(); }
|
|
1082
|
-
catch (err) {
|
|
1154
|
+
catch (err) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}${err.message}${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
1083
1155
|
const models = Array.isArray(response && response.models) ? response.models : [];
|
|
1084
|
-
if (!models.length) {
|
|
1085
|
-
|
|
1156
|
+
if (!models.length) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}No models available.${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
1157
|
+
writer.scrollback(`\n ${FG_TEAL}${BOLD}◆ Your Models${RST}\n ${FG_DARK}${'─'.repeat(60)}${RST}`);
|
|
1086
1158
|
const activeIndex = models.findIndex((m) => m.base_url === config.api_base && m.model_id === config.default_model);
|
|
1087
1159
|
const selectedIndex = await interactiveSelect(models, (model, isSelected, isFinal) => {
|
|
1088
1160
|
const active = model.base_url === config.api_base && model.model_id === config.default_model;
|
|
@@ -1091,18 +1163,18 @@ function createCommands({
|
|
|
1091
1163
|
const nameStyle = isSelected && !isFinal ? `${BG_SELECTED}${FG_CYAN}` : (isSelected ? FG_CYAN : FG_GRAY);
|
|
1092
1164
|
return ` ${marker} ${cursor} ${nameStyle}${model.name} · ${model.model_id} @ ${model.base_url}${RST}`;
|
|
1093
1165
|
}, { initialIndex: Math.max(0, activeIndex) });
|
|
1094
|
-
if (selectedIndex === null) {
|
|
1166
|
+
if (selectedIndex === null) { writer.scrollback(` ${FG_DARK}Cancelled${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
1095
1167
|
const selectedModel = models[selectedIndex];
|
|
1096
1168
|
let credentialsResponse;
|
|
1097
1169
|
try { credentialsResponse = await dashboardGetModelForCli(selectedModel.id); }
|
|
1098
|
-
catch (err) {
|
|
1170
|
+
catch (err) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}${err.message}${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
1099
1171
|
const model = credentialsResponse && credentialsResponse.model ? credentialsResponse.model : null;
|
|
1100
|
-
if (!model) {
|
|
1172
|
+
if (!model) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Unable to load selected model.${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
1101
1173
|
const contextLength = (Number.isInteger(model.context_length) && model.context_length > 0 ? model.context_length : null) || (Number.isInteger(model.max_tokens) && model.max_tokens > 0 ? model.max_tokens : null);
|
|
1102
1174
|
const updatedConfig = { ...config, api_base: model.base_url, api_key: model.api_key, default_model: model.model_id, dashboard_model_id: model.id };
|
|
1103
1175
|
if (contextLength !== null) updatedConfig.context_length = contextLength;
|
|
1104
1176
|
setConfig(updatedConfig);
|
|
1105
|
-
|
|
1177
|
+
writer.scrollback(` ${FG_GREEN}✓${RST} ${FG_GRAY}Current model → ${model.name} (${model.model_id})${RST}\n`);
|
|
1106
1178
|
return { model: model.model_id, dbId: model.id };
|
|
1107
1179
|
}
|
|
1108
1180
|
|
|
@@ -1113,55 +1185,59 @@ function createCommands({
|
|
|
1113
1185
|
api_key: opts.apiKey || 'any',
|
|
1114
1186
|
dashboard_url: opts.dashboardUrl || current.dashboard_url,
|
|
1115
1187
|
auth_token: current.auth_token || '',
|
|
1116
|
-
default_model: opts.defaultModel || '
|
|
1188
|
+
default_model: opts.defaultModel || '',
|
|
1117
1189
|
temperature: 0.7,
|
|
1118
1190
|
request_timeout_ms: DEFAULT_API_TIMEOUT_MS,
|
|
1119
1191
|
stream: true,
|
|
1120
1192
|
models: current.models,
|
|
1121
1193
|
};
|
|
1122
1194
|
setConfig(cfg);
|
|
1123
|
-
|
|
1124
|
-
console.log(` ${FG_GRAY}${JSON.stringify(cfg, null, 2)}${RST}\n`);
|
|
1195
|
+
writer.scrollback(`\n ${FG_GREEN}✓${RST} Config saved to ${CONFIG_PATH}\n ${FG_GRAY}${JSON.stringify(cfg, null, 2)}${RST}\n`);
|
|
1125
1196
|
}
|
|
1126
1197
|
|
|
1127
1198
|
async function cmdLogin() {
|
|
1128
|
-
|
|
1199
|
+
writer.scrollback(`\n ${FG_TEAL}${BOLD}◆ CLI Login${RST}\n ${FG_DARK}${'─'.repeat(40)}${RST}`);
|
|
1129
1200
|
let loginRequest;
|
|
1130
1201
|
try { loginRequest = await requestCliLogin(); }
|
|
1131
|
-
catch (err) {
|
|
1132
|
-
|
|
1133
|
-
console.log(` ${FG_CYAN}${loginRequest.verification_url}${RST}`);
|
|
1134
|
-
console.log(` ${FG_DARK}Waiting for confirmation...${RST}`);
|
|
1202
|
+
catch (err) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Unable to start login via ${getConfig().dashboard_url}: ${err.message}${RST}\n`); return; }
|
|
1203
|
+
writer.scrollback(` ${FG_GRAY}Open this URL in your browser and confirm the login:${RST}\n ${FG_CYAN}${loginRequest.verification_url}${RST}\n ${FG_DARK}Waiting for confirmation...${RST}`);
|
|
1135
1204
|
const startedAt = Date.now();
|
|
1136
1205
|
while (Date.now() - startedAt < LOGIN_TIMEOUT_MS) {
|
|
1137
1206
|
await new Promise((r) => setTimeout(r, LOGIN_POLL_INTERVAL_MS));
|
|
1138
1207
|
let status;
|
|
1139
1208
|
try { status = await getCliLoginStatus(loginRequest.id, loginRequest.hash); }
|
|
1140
|
-
catch (err) { if (err.statusCode === 404 || err.statusCode === 410) {
|
|
1141
|
-
if (status.status === 'authorized') { const config = getConfig(); setConfig({ ...config, dashboard_url: config.dashboard_url, auth_token: loginRequest.token });
|
|
1142
|
-
if (status.status === 'expired') {
|
|
1209
|
+
catch (err) { if (err.statusCode === 404 || err.statusCode === 410) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Login token is no longer valid.${RST}\n`); return; } continue; }
|
|
1210
|
+
if (status.status === 'authorized') { const config = getConfig(); setConfig({ ...config, dashboard_url: config.dashboard_url, auth_token: loginRequest.token }); writer.scrollback(` ${FG_GREEN}✓${RST} ${FG_GRAY}CLI token saved to ${CONFIG_PATH}${RST}\n`); return; }
|
|
1211
|
+
if (status.status === 'expired') { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Login token expired.${RST}\n`); return; }
|
|
1143
1212
|
}
|
|
1144
|
-
|
|
1213
|
+
writer.scrollback(` ${FG_YELLOW}⚠${RST} ${FG_GRAY}Login timed out.${RST}\n`);
|
|
1145
1214
|
}
|
|
1146
1215
|
|
|
1147
1216
|
async function cmdWhoAmI() {
|
|
1148
1217
|
let response;
|
|
1149
|
-
try { response = await dashboardWhoAmI(); } catch (err) {
|
|
1218
|
+
try { response = await dashboardWhoAmI(); } catch (err) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}${err.message}${RST}\n`); return; }
|
|
1150
1219
|
const user = response && response.user ? response.user : null;
|
|
1151
|
-
if (!user) {
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1220
|
+
if (!user) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Unable to load current user.${RST}\n`); return; }
|
|
1221
|
+
const lines = [
|
|
1222
|
+
'',
|
|
1223
|
+
` ${FG_TEAL}${BOLD}◆ Current User${RST}`,
|
|
1224
|
+
` ${FG_DARK}${'─'.repeat(40)}${RST}`,
|
|
1225
|
+
formatUserLine('ID', user.id),
|
|
1226
|
+
formatUserLine('Email', user.email || '-'),
|
|
1227
|
+
formatUserLine('Name', user.name || '-'),
|
|
1228
|
+
formatUserLine('Provider', user.provider || '-'),
|
|
1229
|
+
];
|
|
1230
|
+
if (user.avatar_url) lines.push(formatUserLine('Avatar', user.avatar_url));
|
|
1231
|
+
lines.push('');
|
|
1232
|
+
writer.scrollback(lines.join('\n'));
|
|
1157
1233
|
}
|
|
1158
1234
|
|
|
1159
1235
|
async function cmdLogout() {
|
|
1160
1236
|
const config = getConfig();
|
|
1161
|
-
if (!config.auth_token) {
|
|
1162
|
-
try { await dashboardLogout(); } catch (err) { if (err.statusCode !== 401) {
|
|
1237
|
+
if (!config.auth_token) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Not logged in.${RST}\n`); return; }
|
|
1238
|
+
try { await dashboardLogout(); } catch (err) { if (err.statusCode !== 401) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}${err.message}${RST}\n`); return; } }
|
|
1163
1239
|
setConfig({ ...config, auth_token: '' });
|
|
1164
|
-
|
|
1240
|
+
writer.scrollback(` ${FG_GREEN}✓${RST} ${FG_GRAY}Logged out and cleared local CLI token.${RST}\n`);
|
|
1165
1241
|
}
|
|
1166
1242
|
|
|
1167
1243
|
function printDryRunSummary() {
|
|
@@ -1174,15 +1250,24 @@ function createCommands({
|
|
|
1174
1250
|
const stripA = (s) => s.replace(/\x1b\[[^m]*m/g, '');
|
|
1175
1251
|
const row = (content) => { const visible = stripA(content).length; const pad = ' '.repeat(Math.max(0, INNER - visible)); return isTTY ? `${FG_TEAL}║${RST}${content}${pad}${FG_TEAL}║${RST}` : `║${stripA(content)}${pad}║`; };
|
|
1176
1252
|
const hr40 = (tl, fill, tr) => { const line = tl + fill.repeat(INNER) + tr; return isTTY ? `${FG_TEAL}${line}${RST}` : line; };
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1253
|
+
const out = [
|
|
1254
|
+
'',
|
|
1255
|
+
hr40('╔','═','╗'),
|
|
1256
|
+
row(` ${isTTY ? BOLD : ''}DRY-RUN SUMMARY${isTTY ? RST : ''}`),
|
|
1257
|
+
hr40('╠','═','╣'),
|
|
1258
|
+
row(` ✎ Files that would change: ${files.length} `),
|
|
1259
|
+
row(` ▶ Commands that would run: ${cmds.length} `),
|
|
1260
|
+
row(` ↓ Network calls: ${nets.length} `),
|
|
1261
|
+
hr40('╚','═','╝'),
|
|
1262
|
+
];
|
|
1263
|
+
if (ops.length > 0) {
|
|
1264
|
+
out.push('');
|
|
1265
|
+
for (const op of ops) {
|
|
1266
|
+
out.push(isTTY ? ` ${op.symbol} ${FG_GRAY}${op.desc}${RST}` : ` ${op.symbol} ${op.desc}`);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
out.push('');
|
|
1270
|
+
writer.scrollback(out.join('\n'));
|
|
1186
1271
|
}
|
|
1187
1272
|
|
|
1188
1273
|
return {
|
package/lib/config.js
CHANGED
|
@@ -20,6 +20,7 @@ function _maybeWarnApiKeyAny(cfg) {
|
|
|
20
20
|
}
|
|
21
21
|
if (_LOCAL_HOSTS.has(host)) return;
|
|
22
22
|
_apiKeyAnyWarned = true;
|
|
23
|
+
// audit: allowed — pre-UI startup warning, fires once before TUI initialises.
|
|
23
24
|
process.stderr.write(
|
|
24
25
|
"⚠ api_key='any' against non-local endpoint — requests will likely fail " +
|
|
25
26
|
"with 401. Run 'semalt-code config set api_key <key>' to set a real key.\n"
|
package/lib/constants.js
CHANGED