blun-king-cli 7.1.1 → 7.2.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/chat.js +475 -235
- package/lib/ui.js +21 -0
- package/package.json +1 -1
package/lib/chat.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* BLUN King CLI v7.
|
|
3
|
-
* Full-screen
|
|
4
|
-
*
|
|
2
|
+
* BLUN King CLI v7.2.0 - Chat Module
|
|
3
|
+
* Full-screen TUI with ANSI Scroll Regions
|
|
4
|
+
* Fixed input panel at bottom, responses scroll above
|
|
5
5
|
* CommonJS
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -13,7 +13,17 @@ var client = require("./client");
|
|
|
13
13
|
|
|
14
14
|
var localMode = false;
|
|
15
15
|
|
|
16
|
-
// ──
|
|
16
|
+
// ── ANSI Escape Helpers ──────────────────────────────────
|
|
17
|
+
var ESC = "\x1b[";
|
|
18
|
+
function cursorTo(row, col) { return ESC + row + ";" + col + "H"; }
|
|
19
|
+
function clearLine() { return ESC + "2K"; }
|
|
20
|
+
function setScrollRegion(top, bottom) { return ESC + top + ";" + bottom + "r"; }
|
|
21
|
+
function saveCursor() { return ESC + "s"; }
|
|
22
|
+
function restoreCursor() { return ESC + "u"; }
|
|
23
|
+
function hideCursor() { return ESC + "?25l"; }
|
|
24
|
+
function showCursor() { return ESC + "?25h"; }
|
|
25
|
+
|
|
26
|
+
// ── Slash Commands ───────────────────────────────────────
|
|
17
27
|
var SLASH_COMMANDS = [
|
|
18
28
|
["/help", "Show all commands"],
|
|
19
29
|
["/settings", "Open interactive settings panel"],
|
|
@@ -29,6 +39,12 @@ var SLASH_COMMANDS = [
|
|
|
29
39
|
];
|
|
30
40
|
var slashNames = SLASH_COMMANDS.map(function (c) { return c[0]; });
|
|
31
41
|
|
|
42
|
+
// ── Layout Constants ─────────────────────────────────────
|
|
43
|
+
var INPUT_PANEL_HEIGHT = 5; // top-frame + input + blank + status + bottom-frame
|
|
44
|
+
|
|
45
|
+
function getRows() { return process.stdout.rows || 24; }
|
|
46
|
+
function getCols() { return process.stdout.columns || 80; }
|
|
47
|
+
|
|
32
48
|
function startChat() {
|
|
33
49
|
ui.renderBanner();
|
|
34
50
|
|
|
@@ -53,16 +69,12 @@ function startChat() {
|
|
|
53
69
|
return authPromise.then(function () {
|
|
54
70
|
var creds = auth.getStoredCredentials();
|
|
55
71
|
if (creds) {
|
|
56
|
-
|
|
57
|
-
"Connected as " + (creds.name || creds.email || "user") + " | Plan: " + (creds.plan || "free"),
|
|
58
|
-
"system"
|
|
59
|
-
);
|
|
72
|
+
console.log(chalk.yellow(" [SYS] Connected as " + (creds.name || creds.email || "user") + " | Plan: " + (creds.plan || "free")));
|
|
60
73
|
} else if (localMode) {
|
|
61
|
-
|
|
74
|
+
console.log(chalk.yellow(" [SYS] Local mode - no API calls available"));
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
var settings = auth.getSettings();
|
|
65
|
-
// Ensure all settings fields exist
|
|
66
78
|
if (!settings.theme) settings.theme = "neon";
|
|
67
79
|
if (!settings.streamLevel) settings.streamLevel = "normal";
|
|
68
80
|
if (!settings.permissionMode) settings.permissionMode = "normal";
|
|
@@ -70,43 +82,339 @@ function startChat() {
|
|
|
70
82
|
if (!settings.statuslineMode) settings.statuslineMode = "full";
|
|
71
83
|
|
|
72
84
|
ui.setTheme(settings.theme || "neon");
|
|
73
|
-
|
|
74
|
-
var username = (creds ? creds.name : null) || "local";
|
|
75
85
|
var model = settings.model || "auto";
|
|
86
|
+
var inputBuffer = "";
|
|
87
|
+
var cursorPos = 0;
|
|
88
|
+
var isInMenu = false;
|
|
89
|
+
|
|
90
|
+
// ── Setup full-screen layout ─────────────────────────
|
|
91
|
+
function setupLayout() {
|
|
92
|
+
var rows = getRows();
|
|
93
|
+
var scrollBottom = rows - INPUT_PANEL_HEIGHT;
|
|
94
|
+
if (scrollBottom < 3) scrollBottom = 3;
|
|
95
|
+
// Set scroll region to top area only
|
|
96
|
+
process.stdout.write(setScrollRegion(1, scrollBottom));
|
|
97
|
+
// Move cursor to scroll area to print initial content
|
|
98
|
+
process.stdout.write(cursorTo(scrollBottom, 1));
|
|
99
|
+
}
|
|
76
100
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
101
|
+
function drawInputPanel() {
|
|
102
|
+
var rows = getRows();
|
|
103
|
+
var cols = getCols();
|
|
104
|
+
var t = ui.getTheme();
|
|
105
|
+
var panelTop = rows - INPUT_PANEL_HEIGHT + 1;
|
|
106
|
+
var inner = Math.min(cols - 2, 96);
|
|
107
|
+
|
|
108
|
+
// Permission label
|
|
109
|
+
var perm = settings.permissionMode || "normal";
|
|
110
|
+
var permColors = { safe: chalk.green, normal: chalk.yellow, god: chalk.red };
|
|
111
|
+
var permLabel = (permColors[perm] || chalk.white)(perm);
|
|
112
|
+
|
|
113
|
+
var stats = ui.getStats();
|
|
114
|
+
var tokens = ui.formatNumber(stats.tokens);
|
|
115
|
+
var elapsed = ui.formatDuration(stats.uptime);
|
|
116
|
+
var cost = ui.estimateCost(stats.tokens, settings.model);
|
|
117
|
+
|
|
118
|
+
// Build status parts
|
|
119
|
+
var statusParts = [];
|
|
120
|
+
statusParts.push(permLabel);
|
|
121
|
+
statusParts.push(chalk.dim("model:") + chalk.white(settings.model || "auto"));
|
|
122
|
+
statusParts.push(chalk.dim("tokens:") + chalk.white(tokens));
|
|
123
|
+
statusParts.push(chalk.dim(elapsed));
|
|
124
|
+
if (settings.costVisibility !== false) {
|
|
125
|
+
statusParts.push(chalk.dim(cost));
|
|
126
|
+
}
|
|
127
|
+
var statusLine = statusParts.join(chalk.dim(" │ "));
|
|
128
|
+
|
|
129
|
+
// Save cursor position in scroll region
|
|
130
|
+
process.stdout.write(saveCursor());
|
|
131
|
+
|
|
132
|
+
// Row 1: Top frame
|
|
133
|
+
process.stdout.write(cursorTo(panelTop, 1) + clearLine());
|
|
134
|
+
process.stdout.write(t.frame("╭─") + t.frame.bold(" BLUN ") + t.frame("─".repeat(Math.max(2, inner - 10))) + t.frame("╮"));
|
|
135
|
+
|
|
136
|
+
// Row 2: Input line
|
|
137
|
+
var displayInput = inputBuffer || "";
|
|
138
|
+
var maxInputLen = inner - 4;
|
|
139
|
+
var showInput = displayInput.length > maxInputLen ? displayInput.slice(displayInput.length - maxInputLen) : displayInput;
|
|
140
|
+
process.stdout.write(cursorTo(panelTop + 1, 1) + clearLine());
|
|
141
|
+
process.stdout.write(t.frame("│ ") + chalk.bold.green("> ") + chalk.white(showInput) + " ".repeat(Math.max(0, maxInputLen - showInput.length)) + t.frame(" │"));
|
|
142
|
+
|
|
143
|
+
// Row 3: Empty separator
|
|
144
|
+
process.stdout.write(cursorTo(panelTop + 2, 1) + clearLine());
|
|
145
|
+
process.stdout.write(t.frame("│") + " ".repeat(inner) + t.frame("│"));
|
|
146
|
+
|
|
147
|
+
// Row 4: Status line
|
|
148
|
+
process.stdout.write(cursorTo(panelTop + 3, 1) + clearLine());
|
|
149
|
+
process.stdout.write(t.frame("│ ") + statusLine);
|
|
150
|
+
|
|
151
|
+
// Row 5: Bottom frame
|
|
152
|
+
process.stdout.write(cursorTo(panelTop + 4, 1) + clearLine());
|
|
153
|
+
process.stdout.write(t.frame("╰─ ") + chalk.dim("Tab: autocomplete /: commands Esc: exit") + t.frame(" ─") + t.frame("─".repeat(Math.max(1, inner - 48))) + t.frame("╯"));
|
|
154
|
+
|
|
155
|
+
// Restore cursor back to input line
|
|
156
|
+
var cursorCol = 5 + cursorPos; // "│ > " = 4 chars + 1
|
|
157
|
+
if (cursorPos > maxInputLen) cursorCol = 5 + maxInputLen;
|
|
158
|
+
process.stdout.write(cursorTo(panelTop + 1, cursorCol));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function printToScrollArea(text) {
|
|
162
|
+
var rows = getRows();
|
|
163
|
+
var scrollBottom = rows - INPUT_PANEL_HEIGHT;
|
|
164
|
+
// Save cursor, move to scroll area, print, restore
|
|
165
|
+
process.stdout.write(saveCursor());
|
|
166
|
+
process.stdout.write(setScrollRegion(1, scrollBottom));
|
|
167
|
+
process.stdout.write(cursorTo(scrollBottom, 1));
|
|
168
|
+
// Print text - each line scrolls the region up
|
|
169
|
+
var lines = text.split("\n");
|
|
170
|
+
for (var i = 0; i < lines.length; i++) {
|
|
171
|
+
process.stdout.write("\n" + lines[i]);
|
|
172
|
+
}
|
|
173
|
+
process.stdout.write(restoreCursor());
|
|
174
|
+
drawInputPanel();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Handle resize ────────────────────────────────────
|
|
178
|
+
process.stdout.on("resize", function () {
|
|
179
|
+
setupLayout();
|
|
180
|
+
drawInputPanel();
|
|
89
181
|
});
|
|
90
182
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
183
|
+
// ── Process command ──────────────────────────────────
|
|
184
|
+
function processCommand(input) {
|
|
185
|
+
if (!input) return;
|
|
186
|
+
|
|
187
|
+
// ── Commands ───────────────────────────────────
|
|
188
|
+
if (input === "/exit" || input === "/quit") {
|
|
189
|
+
cleanup();
|
|
190
|
+
console.log(chalk.yellow("\n Goodbye!\n"));
|
|
191
|
+
process.exit(0);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (input === "/clear") {
|
|
196
|
+
var rows = getRows();
|
|
197
|
+
var scrollBottom = rows - INPUT_PANEL_HEIGHT;
|
|
198
|
+
process.stdout.write(setScrollRegion(1, scrollBottom));
|
|
199
|
+
for (var i = 1; i <= scrollBottom; i++) {
|
|
200
|
+
process.stdout.write(cursorTo(i, 1) + clearLine());
|
|
201
|
+
}
|
|
202
|
+
process.stdout.write(cursorTo(1, 1));
|
|
203
|
+
drawInputPanel();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (input === "/help") {
|
|
208
|
+
var helpLines = [];
|
|
209
|
+
helpLines.push(chalk.cyan.bold(" Commands"));
|
|
210
|
+
helpLines.push(chalk.dim(" " + "─".repeat(40)));
|
|
211
|
+
SLASH_COMMANDS.forEach(function (c) {
|
|
212
|
+
helpLines.push(" " + chalk.green(c[0]) + " ".repeat(Math.max(1, 16 - c[0].length)) + chalk.gray(c[1]));
|
|
213
|
+
});
|
|
214
|
+
helpLines.push("");
|
|
215
|
+
printToScrollArea(helpLines.join("\n"));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (input === "/status") {
|
|
220
|
+
var stats = ui.getStats();
|
|
221
|
+
var statusLines = [];
|
|
222
|
+
statusLines.push(chalk.cyan.bold(" Status"));
|
|
223
|
+
statusLines.push(chalk.dim(" " + "─".repeat(40)));
|
|
224
|
+
statusLines.push(" Model: " + chalk.white(settings.model || "auto"));
|
|
225
|
+
statusLines.push(" Permission: " + ui.formatSettingValue({ key: "permissionMode" }, settings.permissionMode));
|
|
226
|
+
statusLines.push(" Theme: " + chalk.white(settings.theme || "neon"));
|
|
227
|
+
statusLines.push(" Tokens: " + chalk.white(ui.formatNumber(stats.tokens)));
|
|
228
|
+
statusLines.push(" Cost: " + chalk.white(ui.estimateCost(stats.tokens, settings.model)));
|
|
229
|
+
statusLines.push(" Uptime: " + chalk.white(ui.formatDuration(stats.uptime)));
|
|
230
|
+
statusLines.push("");
|
|
231
|
+
printToScrollArea(statusLines.join("\n"));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (input === "/perm" || input === "/permission") {
|
|
236
|
+
var modes = ui.PERMISSION_MODES;
|
|
237
|
+
var idx = modes.indexOf(settings.permissionMode);
|
|
238
|
+
settings.permissionMode = modes[(idx + 1) % modes.length];
|
|
239
|
+
auth.saveSettings(settings);
|
|
240
|
+
printToScrollArea(chalk.yellow(" [SYS] Permission: " + settings.permissionMode));
|
|
241
|
+
drawInputPanel();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (input.indexOf("/theme") === 0) {
|
|
246
|
+
var parts = input.split(/\s+/);
|
|
247
|
+
if (parts[1] && ui.THEMES[parts[1]]) {
|
|
248
|
+
settings.theme = parts[1];
|
|
249
|
+
} else {
|
|
250
|
+
var themeNames = Object.keys(ui.THEMES);
|
|
251
|
+
var tidx = themeNames.indexOf(settings.theme || "neon");
|
|
252
|
+
settings.theme = themeNames[(tidx + 1) % themeNames.length];
|
|
253
|
+
}
|
|
254
|
+
ui.setTheme(settings.theme);
|
|
255
|
+
auth.saveSettings(settings);
|
|
256
|
+
printToScrollArea(chalk.yellow(" [SYS] Theme: " + settings.theme));
|
|
257
|
+
drawInputPanel();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (input.indexOf("/model") === 0) {
|
|
262
|
+
var parts = input.split(/\s+/);
|
|
263
|
+
if (parts[1]) {
|
|
264
|
+
settings.model = parts[1];
|
|
265
|
+
model = parts[1];
|
|
266
|
+
auth.saveSettings(settings);
|
|
267
|
+
printToScrollArea(chalk.yellow(" [SYS] Model: " + parts[1]));
|
|
268
|
+
drawInputPanel();
|
|
269
|
+
} else {
|
|
270
|
+
// Open inquirer menu
|
|
271
|
+
enterMenuMode(function (done) {
|
|
272
|
+
var inquirer = require("inquirer");
|
|
273
|
+
inquirer.prompt([{
|
|
274
|
+
type: "list",
|
|
275
|
+
name: "model",
|
|
276
|
+
message: chalk.cyan("Select Model"),
|
|
277
|
+
choices: [
|
|
278
|
+
{ name: "Auto (recommended)", value: "auto" },
|
|
279
|
+
{ name: "Gemma 4", value: "gemma4" },
|
|
280
|
+
{ name: "Claude", value: "claude" },
|
|
281
|
+
{ name: "Llama", value: "llama" },
|
|
282
|
+
],
|
|
283
|
+
}]).then(function (ans) {
|
|
284
|
+
if (ans.model) {
|
|
285
|
+
model = ans.model;
|
|
286
|
+
settings.model = ans.model;
|
|
287
|
+
auth.saveSettings(settings);
|
|
288
|
+
printToScrollArea(chalk.yellow(" [SYS] Model: " + ans.model));
|
|
289
|
+
}
|
|
290
|
+
done();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (input === "/settings") {
|
|
298
|
+
enterMenuMode(function (done) {
|
|
299
|
+
openSettingsPanel(done);
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (input === "/") {
|
|
305
|
+
enterMenuMode(function (done) {
|
|
306
|
+
var inquirer = require("inquirer");
|
|
307
|
+
inquirer.prompt([{
|
|
308
|
+
type: "list",
|
|
309
|
+
name: "cmd",
|
|
310
|
+
message: chalk.cyan("Select command"),
|
|
311
|
+
choices: SLASH_COMMANDS.map(function (c) {
|
|
312
|
+
return { name: chalk.green(c[0]) + chalk.gray(" " + c[1]), value: c[0] };
|
|
313
|
+
}).concat([
|
|
314
|
+
new inquirer.Separator(),
|
|
315
|
+
{ name: chalk.dim("Cancel"), value: "" },
|
|
316
|
+
]),
|
|
317
|
+
pageSize: 12,
|
|
318
|
+
}]).then(function (ans) {
|
|
319
|
+
done();
|
|
320
|
+
if (ans.cmd) {
|
|
321
|
+
processCommand(ans.cmd);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (input === "/login") {
|
|
329
|
+
enterMenuMode(function (done) {
|
|
330
|
+
auth.login().then(function () {
|
|
331
|
+
localMode = false;
|
|
332
|
+
creds = auth.getStoredCredentials();
|
|
333
|
+
printToScrollArea(chalk.yellow(" [SYS] Logged in!"));
|
|
334
|
+
}).catch(function (e) {
|
|
335
|
+
printToScrollArea(chalk.red(" ✘ " + e.message));
|
|
336
|
+
}).then(function () { done(); });
|
|
337
|
+
});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (input === "/whoami") {
|
|
342
|
+
enterMenuMode(function (done) {
|
|
343
|
+
auth.whoami().catch(function (e) {
|
|
344
|
+
printToScrollArea(chalk.red(" ✘ " + e.message));
|
|
345
|
+
}).then(function () { done(); });
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (input === "/keys") {
|
|
351
|
+
enterMenuMode(function (done) {
|
|
352
|
+
auth.listApiKeys().catch(function (e) {
|
|
353
|
+
printToScrollArea(chalk.red(" ✘ " + e.message));
|
|
354
|
+
}).then(function () { done(); });
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── Send message ──────────────────────────────────
|
|
360
|
+
if (localMode) {
|
|
361
|
+
printToScrollArea(chalk.red(" ✘ Local mode - cannot send messages. Use /login to authenticate."));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Show user message in scroll area
|
|
366
|
+
printToScrollArea(chalk.bold.blue(" > " + input));
|
|
367
|
+
|
|
368
|
+
// Show thinking indicator
|
|
369
|
+
printToScrollArea(chalk.dim(" ⠋ Thinking..."));
|
|
370
|
+
|
|
371
|
+
client.sendMessage(input, { model: settings.model || "auto" })
|
|
372
|
+
.then(function (res) {
|
|
373
|
+
if (res.status === 200 && res.data) {
|
|
374
|
+
var text = res.data.answer || res.data.response || res.data.text || res.data.message || JSON.stringify(res.data);
|
|
375
|
+
var tok = res.data.tokens || res.data.usage || {};
|
|
376
|
+
var tokTotal = tok.total || tok.total_tokens || 0;
|
|
377
|
+
if (!tokTotal && text) {
|
|
378
|
+
tokTotal = Math.ceil(text.length / 4) + Math.ceil(input.length / 4);
|
|
379
|
+
}
|
|
380
|
+
if (tokTotal) ui.addTokens(tokTotal);
|
|
381
|
+
|
|
382
|
+
// Render markdown
|
|
383
|
+
var rendered = ui.renderMarkdown(text);
|
|
384
|
+
var lines = rendered.split("\n");
|
|
385
|
+
var output = [];
|
|
386
|
+
for (var i = 0; i < lines.length; i++) {
|
|
387
|
+
output.push(" " + lines[i]);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Meta line
|
|
391
|
+
var metaParts = [];
|
|
392
|
+
metaParts.push(chalk.bgCyan.black(" " + (settings.model || "auto") + " "));
|
|
393
|
+
if (tokTotal) metaParts.push(chalk.dim("tokens:" + ui.formatNumber(tokTotal)));
|
|
394
|
+
metaParts.push(chalk.dim("total:" + ui.formatNumber(ui.getStats().tokens)));
|
|
395
|
+
if (settings.costVisibility !== false) metaParts.push(chalk.dim(ui.estimateCost(ui.getStats().tokens, settings.model)));
|
|
396
|
+
output.push(" " + metaParts.join(chalk.dim(" │ ")));
|
|
397
|
+
output.push("");
|
|
398
|
+
|
|
399
|
+
printToScrollArea(output.join("\n"));
|
|
400
|
+
} else if (res.status === 401 || res.status === 403) {
|
|
401
|
+
printToScrollArea(chalk.red(" ✘ Auth error. Use /login to re-authenticate."));
|
|
402
|
+
} else {
|
|
403
|
+
printToScrollArea(chalk.red(" ✘ Error: " + JSON.stringify(res.data)));
|
|
404
|
+
}
|
|
405
|
+
drawInputPanel();
|
|
406
|
+
})
|
|
407
|
+
.catch(function (e) {
|
|
408
|
+
printToScrollArea(chalk.red(" ✘ Request failed: " + e.message + "\n Try /login if auth expired."));
|
|
409
|
+
drawInputPanel();
|
|
410
|
+
});
|
|
94
411
|
}
|
|
95
412
|
|
|
96
|
-
// ── Settings
|
|
97
|
-
function openSettingsPanel() {
|
|
413
|
+
// ── Settings Panel (inquirer) ─────────────────────────
|
|
414
|
+
function openSettingsPanel(done) {
|
|
98
415
|
var inquirer = require("inquirer");
|
|
99
416
|
var t = ui.getTheme();
|
|
100
417
|
|
|
101
|
-
// Show current settings
|
|
102
|
-
console.log();
|
|
103
|
-
console.log(t.header(" Current Settings"));
|
|
104
|
-
console.log(t.muted(" " + "\u2500".repeat(40)));
|
|
105
|
-
ui.SETTINGS_FIELDS.forEach(function (f) {
|
|
106
|
-
console.log(" " + chalk.white(f.label + ":") + " ".repeat(Math.max(1, 14 - f.label.length)) + ui.formatSettingValue(f, settings[f.key]));
|
|
107
|
-
});
|
|
108
|
-
console.log();
|
|
109
|
-
|
|
110
418
|
inquirer.prompt([{
|
|
111
419
|
type: "list",
|
|
112
420
|
name: "field",
|
|
@@ -117,14 +425,14 @@ function startChat() {
|
|
|
117
425
|
pageSize: 12,
|
|
118
426
|
}]).then(function (ans) {
|
|
119
427
|
if (ans.field === "back") {
|
|
120
|
-
|
|
428
|
+
done();
|
|
121
429
|
return;
|
|
122
430
|
}
|
|
123
431
|
var field = null;
|
|
124
432
|
for (var i = 0; i < ui.SETTINGS_FIELDS.length; i++) {
|
|
125
433
|
if (ui.SETTINGS_FIELDS[i].key === ans.field) { field = ui.SETTINGS_FIELDS[i]; break; }
|
|
126
434
|
}
|
|
127
|
-
if (!field) {
|
|
435
|
+
if (!field) { done(); return; }
|
|
128
436
|
|
|
129
437
|
return inquirer.prompt([{
|
|
130
438
|
type: "list",
|
|
@@ -140,230 +448,162 @@ function startChat() {
|
|
|
140
448
|
settings[field.key] = ans2.value;
|
|
141
449
|
if (field.key === "theme") ui.setTheme(settings.theme);
|
|
142
450
|
auth.saveSettings(settings);
|
|
143
|
-
|
|
144
|
-
|
|
451
|
+
printToScrollArea(chalk.yellow(" [SYS] " + field.label + ": " + ans2.value));
|
|
452
|
+
done();
|
|
145
453
|
});
|
|
146
454
|
});
|
|
147
455
|
}
|
|
148
456
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
rl.close();
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (input === "/clear") {
|
|
171
|
-
console.clear();
|
|
172
|
-
ui.renderBanner();
|
|
173
|
-
showPrompt();
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
457
|
+
// ── Menu Mode (temporarily disable raw input) ────────
|
|
458
|
+
function enterMenuMode(fn) {
|
|
459
|
+
isInMenu = true;
|
|
460
|
+
// Reset scroll region for inquirer to work properly
|
|
461
|
+
var rows = getRows();
|
|
462
|
+
process.stdout.write(setScrollRegion(1, rows));
|
|
463
|
+
process.stdin.setRawMode(false);
|
|
464
|
+
process.stdin.removeAllListeners("data");
|
|
465
|
+
|
|
466
|
+
fn(function () {
|
|
467
|
+
// Re-enter TUI mode
|
|
468
|
+
isInMenu = false;
|
|
469
|
+
setupLayout();
|
|
470
|
+
drawInputPanel();
|
|
471
|
+
startRawInput();
|
|
472
|
+
});
|
|
473
|
+
}
|
|
176
474
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
475
|
+
// ── Cleanup ──────────────────────────────────────────
|
|
476
|
+
function cleanup() {
|
|
477
|
+
var rows = getRows();
|
|
478
|
+
process.stdout.write(setScrollRegion(1, rows));
|
|
479
|
+
process.stdout.write(cursorTo(rows, 1));
|
|
480
|
+
process.stdout.write(showCursor());
|
|
481
|
+
try { process.stdin.setRawMode(false); } catch (e) {}
|
|
482
|
+
}
|
|
181
483
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
484
|
+
// ── Raw Input Handler ────────────────────────────────
|
|
485
|
+
function startRawInput() {
|
|
486
|
+
if (!process.stdin.isTTY) return;
|
|
487
|
+
process.stdin.setRawMode(true);
|
|
488
|
+
process.stdin.resume();
|
|
489
|
+
process.stdin.setEncoding("utf8");
|
|
490
|
+
|
|
491
|
+
process.stdin.on("data", function onData(key) {
|
|
492
|
+
if (isInMenu) return;
|
|
493
|
+
|
|
494
|
+
// Ctrl+C / Ctrl+D
|
|
495
|
+
if (key === "\u0003" || key === "\u0004") {
|
|
496
|
+
cleanup();
|
|
497
|
+
console.log(chalk.yellow("\n Goodbye!\n"));
|
|
498
|
+
process.exit(0);
|
|
196
499
|
return;
|
|
197
500
|
}
|
|
198
501
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
ui.renderResponse("Permission: " + settings.permissionMode, "system");
|
|
205
|
-
showPrompt();
|
|
502
|
+
// Escape
|
|
503
|
+
if (key === "\u001b" && key.length === 1) {
|
|
504
|
+
cleanup();
|
|
505
|
+
console.log(chalk.yellow("\n Goodbye!\n"));
|
|
506
|
+
process.exit(0);
|
|
206
507
|
return;
|
|
207
508
|
}
|
|
208
509
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
} else {
|
|
217
|
-
var modes = Object.keys(ui.THEMES);
|
|
218
|
-
var idx = modes.indexOf(settings.theme || "neon");
|
|
219
|
-
settings.theme = modes[(idx + 1) % modes.length];
|
|
220
|
-
ui.setTheme(settings.theme);
|
|
221
|
-
auth.saveSettings(settings);
|
|
222
|
-
ui.renderResponse("Theme: " + settings.theme, "system");
|
|
510
|
+
// Enter
|
|
511
|
+
if (key === "\r" || key === "\n") {
|
|
512
|
+
var cmd = inputBuffer.trim();
|
|
513
|
+
inputBuffer = "";
|
|
514
|
+
cursorPos = 0;
|
|
515
|
+
if (cmd) {
|
|
516
|
+
processCommand(cmd);
|
|
223
517
|
}
|
|
224
|
-
|
|
518
|
+
drawInputPanel();
|
|
225
519
|
return;
|
|
226
520
|
}
|
|
227
521
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
auth.saveSettings(settings);
|
|
234
|
-
ui.renderResponse("Model: " + parts[1], "system");
|
|
235
|
-
showPrompt();
|
|
236
|
-
} else {
|
|
237
|
-
var inquirer = require("inquirer");
|
|
238
|
-
inquirer.prompt([{
|
|
239
|
-
type: "list",
|
|
240
|
-
name: "model",
|
|
241
|
-
message: chalk.cyan("Select Model"),
|
|
242
|
-
choices: [
|
|
243
|
-
{ name: "Auto (recommended)", value: "auto" },
|
|
244
|
-
{ name: "Gemma 4", value: "gemma4" },
|
|
245
|
-
{ name: "Claude", value: "claude" },
|
|
246
|
-
{ name: "Llama", value: "llama" },
|
|
247
|
-
],
|
|
248
|
-
}]).then(function (ans) {
|
|
249
|
-
if (ans.model) {
|
|
250
|
-
model = ans.model;
|
|
251
|
-
settings.model = ans.model;
|
|
252
|
-
auth.saveSettings(settings);
|
|
253
|
-
ui.renderResponse("Model: " + ans.model, "system");
|
|
254
|
-
}
|
|
255
|
-
showPrompt();
|
|
256
|
-
});
|
|
522
|
+
// Backspace
|
|
523
|
+
if (key === "\u007f" || key === "\b") {
|
|
524
|
+
if (cursorPos > 0) {
|
|
525
|
+
inputBuffer = inputBuffer.slice(0, cursorPos - 1) + inputBuffer.slice(cursorPos);
|
|
526
|
+
cursorPos--;
|
|
257
527
|
}
|
|
528
|
+
drawInputPanel();
|
|
258
529
|
return;
|
|
259
530
|
}
|
|
260
531
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (input === "/whoami") {
|
|
274
|
-
auth.whoami().catch(function (e) {
|
|
275
|
-
ui.renderResponse(e.message, "error");
|
|
276
|
-
}).then(function () { showPrompt(); });
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (input === "/keys") {
|
|
281
|
-
auth.listApiKeys().catch(function (e) {
|
|
282
|
-
ui.renderResponse(e.message, "error");
|
|
283
|
-
}).then(function () { showPrompt(); });
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (input === "/help") {
|
|
288
|
-
var t = ui.getTheme();
|
|
289
|
-
console.log();
|
|
290
|
-
console.log(t.header(" Commands"));
|
|
291
|
-
console.log(t.muted(" " + "\u2500".repeat(40)));
|
|
292
|
-
SLASH_COMMANDS.forEach(function (c) {
|
|
293
|
-
console.log(" " + chalk.green(c[0]) + " ".repeat(Math.max(1, 16 - c[0].length)) + chalk.gray(c[1]));
|
|
294
|
-
});
|
|
295
|
-
console.log();
|
|
296
|
-
console.log(t.muted(" Tip: Type / and press Tab for autocomplete"));
|
|
297
|
-
console.log();
|
|
298
|
-
showPrompt();
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ── Slash command picker (just "/") ──────────
|
|
303
|
-
if (input === "/") {
|
|
304
|
-
var inquirer = require("inquirer");
|
|
305
|
-
inquirer.prompt([{
|
|
306
|
-
type: "list",
|
|
307
|
-
name: "cmd",
|
|
308
|
-
message: chalk.cyan("Select command"),
|
|
309
|
-
choices: SLASH_COMMANDS.map(function (c) {
|
|
310
|
-
return { name: chalk.green(c[0]) + chalk.gray(" " + c[1]), value: c[0] };
|
|
311
|
-
}).concat([
|
|
312
|
-
new inquirer.Separator(),
|
|
313
|
-
{ name: chalk.dim("Cancel"), value: "" },
|
|
314
|
-
]),
|
|
315
|
-
pageSize: 12,
|
|
316
|
-
}]).then(function (ans) {
|
|
317
|
-
if (ans.cmd) {
|
|
318
|
-
rl.emit("line", ans.cmd);
|
|
319
|
-
} else {
|
|
320
|
-
showPrompt();
|
|
532
|
+
// Tab - autocomplete
|
|
533
|
+
if (key === "\t") {
|
|
534
|
+
if (inputBuffer.startsWith("/")) {
|
|
535
|
+
var hits = slashNames.filter(function (c) { return c.indexOf(inputBuffer) === 0; });
|
|
536
|
+
if (hits.length === 1) {
|
|
537
|
+
inputBuffer = hits[0];
|
|
538
|
+
cursorPos = inputBuffer.length;
|
|
539
|
+
} else if (hits.length > 1) {
|
|
540
|
+
printToScrollArea(chalk.dim(" " + hits.join(" ")));
|
|
321
541
|
}
|
|
322
|
-
}
|
|
542
|
+
}
|
|
543
|
+
drawInputPanel();
|
|
323
544
|
return;
|
|
324
545
|
}
|
|
325
546
|
|
|
326
|
-
//
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
547
|
+
// Arrow keys (escape sequences)
|
|
548
|
+
if (key.length > 1 && key[0] === "\u001b") {
|
|
549
|
+
// Left arrow
|
|
550
|
+
if (key === "\u001b[D") {
|
|
551
|
+
if (cursorPos > 0) cursorPos--;
|
|
552
|
+
drawInputPanel();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
// Right arrow
|
|
556
|
+
if (key === "\u001b[C") {
|
|
557
|
+
if (cursorPos < inputBuffer.length) cursorPos++;
|
|
558
|
+
drawInputPanel();
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
// Home
|
|
562
|
+
if (key === "\u001b[H") {
|
|
563
|
+
cursorPos = 0;
|
|
564
|
+
drawInputPanel();
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// End
|
|
568
|
+
if (key === "\u001b[F") {
|
|
569
|
+
cursorPos = inputBuffer.length;
|
|
570
|
+
drawInputPanel();
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
// Delete
|
|
574
|
+
if (key === "\u001b[3~") {
|
|
575
|
+
if (cursorPos < inputBuffer.length) {
|
|
576
|
+
inputBuffer = inputBuffer.slice(0, cursorPos) + inputBuffer.slice(cursorPos + 1);
|
|
577
|
+
}
|
|
578
|
+
drawInputPanel();
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
// Ignore other escape sequences
|
|
330
582
|
return;
|
|
331
583
|
}
|
|
332
584
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (res.status === 200 && res.data) {
|
|
338
|
-
var text = res.data.answer || res.data.response || res.data.text || res.data.message || JSON.stringify(res.data);
|
|
339
|
-
var tok = res.data.tokens || res.data.usage || {};
|
|
340
|
-
var tokTotal = tok.total || tok.total_tokens || 0;
|
|
341
|
-
if (!tokTotal && text) {
|
|
342
|
-
tokTotal = Math.ceil(text.length / 4) + Math.ceil(input.length / 4);
|
|
343
|
-
}
|
|
344
|
-
if (tokTotal) ui.addTokens(tokTotal);
|
|
345
|
-
ui.renderResponse(text, "agent", {
|
|
346
|
-
model: settings.model || "auto",
|
|
347
|
-
tokens: tokTotal,
|
|
348
|
-
cost: settings.costVisibility,
|
|
349
|
-
});
|
|
350
|
-
} else if (res.status === 401 || res.status === 403) {
|
|
351
|
-
ui.renderResponse("Auth error. Use /login to re-authenticate.", "error");
|
|
352
|
-
} else {
|
|
353
|
-
ui.renderResponse("Error: " + JSON.stringify(res.data), "error");
|
|
354
|
-
}
|
|
355
|
-
showPrompt();
|
|
356
|
-
})
|
|
357
|
-
.catch(function (e) {
|
|
358
|
-
ui.stopSpinner(false, e.message);
|
|
359
|
-
ui.renderResponse("Request failed: " + e.message + "\n Try /login if auth expired.", "error");
|
|
360
|
-
showPrompt();
|
|
361
|
-
});
|
|
585
|
+
// Normal character
|
|
586
|
+
inputBuffer = inputBuffer.slice(0, cursorPos) + key + inputBuffer.slice(cursorPos);
|
|
587
|
+
cursorPos += key.length;
|
|
588
|
+
drawInputPanel();
|
|
362
589
|
});
|
|
590
|
+
}
|
|
363
591
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
592
|
+
// ── Initialize TUI ───────────────────────────────────
|
|
593
|
+
console.log(chalk.dim("\n Type your message or / for commands. Tab for autocomplete.\n"));
|
|
594
|
+
|
|
595
|
+
// Wait a tick for all console.log output to flush, then enter TUI mode
|
|
596
|
+
return new Promise(function (resolveChat) {
|
|
597
|
+
setTimeout(function () {
|
|
598
|
+
setupLayout();
|
|
599
|
+
drawInputPanel();
|
|
600
|
+
startRawInput();
|
|
601
|
+
}, 100);
|
|
602
|
+
|
|
603
|
+
// Handle clean exit
|
|
604
|
+
process.on("SIGINT", function () {
|
|
605
|
+
cleanup();
|
|
606
|
+
process.exit(0);
|
|
367
607
|
});
|
|
368
608
|
});
|
|
369
609
|
});
|
package/lib/ui.js
CHANGED
|
@@ -355,6 +355,25 @@ function getStats() {
|
|
|
355
355
|
return { tokens: totalTokens, cost: totalCost, uptime: Date.now() - startTime };
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
// ── Box & Table (for non-chat CLI commands) ─────────────
|
|
359
|
+
function renderBox(title, content, color) {
|
|
360
|
+
var c = chalk[color] || chalk.cyan;
|
|
361
|
+
console.log();
|
|
362
|
+
console.log(c.bold(" " + title));
|
|
363
|
+
console.log(c(" " + "─".repeat(50)));
|
|
364
|
+
content.split("\n").forEach(function (line) {
|
|
365
|
+
console.log(" " + line);
|
|
366
|
+
});
|
|
367
|
+
console.log();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function renderTable(headers, rows) {
|
|
371
|
+
var Table = require("cli-table3");
|
|
372
|
+
var t = new Table({ head: headers.map(function (h) { return chalk.cyan(h); }) });
|
|
373
|
+
rows.forEach(function (r) { t.push(r); });
|
|
374
|
+
console.log(t.toString());
|
|
375
|
+
}
|
|
376
|
+
|
|
358
377
|
module.exports = {
|
|
359
378
|
// Core
|
|
360
379
|
renderBanner: renderBanner,
|
|
@@ -385,4 +404,6 @@ module.exports = {
|
|
|
385
404
|
formatDuration: formatDuration,
|
|
386
405
|
PERMISSION_MODES: PERMISSION_MODES,
|
|
387
406
|
THEMES: THEMES,
|
|
407
|
+
renderBox: renderBox,
|
|
408
|
+
renderTable: renderTable,
|
|
388
409
|
};
|