blun-king-cli 7.1.1 → 7.2.1
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 +470 -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,334 @@ 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 = cols - 2;
|
|
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 — pad to right border
|
|
148
|
+
process.stdout.write(cursorTo(panelTop + 3, 1) + clearLine());
|
|
149
|
+
process.stdout.write(t.frame("│ ") + statusLine);
|
|
150
|
+
process.stdout.write(cursorTo(panelTop + 3, cols) + t.frame("│"));
|
|
151
|
+
|
|
152
|
+
// Row 5: Bottom frame
|
|
153
|
+
process.stdout.write(cursorTo(panelTop + 4, 1) + clearLine());
|
|
154
|
+
process.stdout.write(t.frame("╰─ ") + chalk.dim("Tab: autocomplete /: commands Esc: exit") + t.frame(" ─") + t.frame("─".repeat(Math.max(1, inner - 48))) + t.frame("╯"));
|
|
155
|
+
|
|
156
|
+
// Restore cursor back to input line
|
|
157
|
+
var cursorCol = 5 + cursorPos; // "│ > " = 4 chars + 1
|
|
158
|
+
if (cursorPos > maxInputLen) cursorCol = 5 + maxInputLen;
|
|
159
|
+
process.stdout.write(cursorTo(panelTop + 1, cursorCol));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function printToScrollArea(text) {
|
|
163
|
+
var rows = getRows();
|
|
164
|
+
var scrollBottom = rows - INPUT_PANEL_HEIGHT;
|
|
165
|
+
// Save cursor, move to scroll area, print, restore
|
|
166
|
+
process.stdout.write(saveCursor());
|
|
167
|
+
process.stdout.write(setScrollRegion(1, scrollBottom));
|
|
168
|
+
process.stdout.write(cursorTo(scrollBottom, 1));
|
|
169
|
+
// Print text - each line scrolls the region up
|
|
170
|
+
var lines = text.split("\n");
|
|
171
|
+
for (var i = 0; i < lines.length; i++) {
|
|
172
|
+
process.stdout.write("\n" + lines[i]);
|
|
173
|
+
}
|
|
174
|
+
process.stdout.write(restoreCursor());
|
|
175
|
+
drawInputPanel();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Handle resize ────────────────────────────────────
|
|
179
|
+
process.stdout.on("resize", function () {
|
|
180
|
+
setupLayout();
|
|
181
|
+
drawInputPanel();
|
|
89
182
|
});
|
|
90
183
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
184
|
+
// ── Process command ──────────────────────────────────
|
|
185
|
+
function processCommand(input) {
|
|
186
|
+
if (!input) return;
|
|
187
|
+
|
|
188
|
+
// ── Commands ───────────────────────────────────
|
|
189
|
+
if (input === "/exit" || input === "/quit") {
|
|
190
|
+
cleanup();
|
|
191
|
+
console.log(chalk.yellow("\n Goodbye!\n"));
|
|
192
|
+
process.exit(0);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (input === "/clear") {
|
|
197
|
+
var rows = getRows();
|
|
198
|
+
var scrollBottom = rows - INPUT_PANEL_HEIGHT;
|
|
199
|
+
process.stdout.write(setScrollRegion(1, scrollBottom));
|
|
200
|
+
for (var i = 1; i <= scrollBottom; i++) {
|
|
201
|
+
process.stdout.write(cursorTo(i, 1) + clearLine());
|
|
202
|
+
}
|
|
203
|
+
process.stdout.write(cursorTo(1, 1));
|
|
204
|
+
drawInputPanel();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (input === "/help") {
|
|
209
|
+
var helpLines = [];
|
|
210
|
+
helpLines.push(chalk.cyan.bold(" Commands"));
|
|
211
|
+
helpLines.push(chalk.dim(" " + "─".repeat(40)));
|
|
212
|
+
SLASH_COMMANDS.forEach(function (c) {
|
|
213
|
+
helpLines.push(" " + chalk.green(c[0]) + " ".repeat(Math.max(1, 16 - c[0].length)) + chalk.gray(c[1]));
|
|
214
|
+
});
|
|
215
|
+
helpLines.push("");
|
|
216
|
+
printToScrollArea(helpLines.join("\n"));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (input === "/status") {
|
|
221
|
+
var stats = ui.getStats();
|
|
222
|
+
var statusLines = [];
|
|
223
|
+
statusLines.push(chalk.cyan.bold(" Status"));
|
|
224
|
+
statusLines.push(chalk.dim(" " + "─".repeat(40)));
|
|
225
|
+
statusLines.push(" Model: " + chalk.white(settings.model || "auto"));
|
|
226
|
+
statusLines.push(" Permission: " + ui.formatSettingValue({ key: "permissionMode" }, settings.permissionMode));
|
|
227
|
+
statusLines.push(" Theme: " + chalk.white(settings.theme || "neon"));
|
|
228
|
+
statusLines.push(" Tokens: " + chalk.white(ui.formatNumber(stats.tokens)));
|
|
229
|
+
statusLines.push(" Cost: " + chalk.white(ui.estimateCost(stats.tokens, settings.model)));
|
|
230
|
+
statusLines.push(" Uptime: " + chalk.white(ui.formatDuration(stats.uptime)));
|
|
231
|
+
statusLines.push("");
|
|
232
|
+
printToScrollArea(statusLines.join("\n"));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (input === "/perm" || input === "/permission") {
|
|
237
|
+
var modes = ui.PERMISSION_MODES;
|
|
238
|
+
var idx = modes.indexOf(settings.permissionMode);
|
|
239
|
+
settings.permissionMode = modes[(idx + 1) % modes.length];
|
|
240
|
+
auth.saveSettings(settings);
|
|
241
|
+
printToScrollArea(chalk.yellow(" [SYS] Permission: " + settings.permissionMode));
|
|
242
|
+
drawInputPanel();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (input.indexOf("/theme") === 0) {
|
|
247
|
+
var parts = input.split(/\s+/);
|
|
248
|
+
if (parts[1] && ui.THEMES[parts[1]]) {
|
|
249
|
+
settings.theme = parts[1];
|
|
250
|
+
} else {
|
|
251
|
+
var themeNames = Object.keys(ui.THEMES);
|
|
252
|
+
var tidx = themeNames.indexOf(settings.theme || "neon");
|
|
253
|
+
settings.theme = themeNames[(tidx + 1) % themeNames.length];
|
|
254
|
+
}
|
|
255
|
+
ui.setTheme(settings.theme);
|
|
256
|
+
auth.saveSettings(settings);
|
|
257
|
+
printToScrollArea(chalk.yellow(" [SYS] Theme: " + settings.theme));
|
|
258
|
+
drawInputPanel();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (input.indexOf("/model") === 0) {
|
|
263
|
+
var parts = input.split(/\s+/);
|
|
264
|
+
if (parts[1]) {
|
|
265
|
+
settings.model = parts[1];
|
|
266
|
+
model = parts[1];
|
|
267
|
+
auth.saveSettings(settings);
|
|
268
|
+
printToScrollArea(chalk.yellow(" [SYS] Model: " + parts[1]));
|
|
269
|
+
drawInputPanel();
|
|
270
|
+
} else {
|
|
271
|
+
// Open inquirer menu
|
|
272
|
+
enterMenuMode(function (done) {
|
|
273
|
+
var inquirer = require("inquirer");
|
|
274
|
+
inquirer.prompt([{
|
|
275
|
+
type: "list",
|
|
276
|
+
name: "model",
|
|
277
|
+
message: chalk.cyan("Select Model"),
|
|
278
|
+
choices: [
|
|
279
|
+
{ name: "Auto (recommended)", value: "auto" },
|
|
280
|
+
{ name: "Gemma 4", value: "gemma4" },
|
|
281
|
+
{ name: "Claude", value: "claude" },
|
|
282
|
+
{ name: "Llama", value: "llama" },
|
|
283
|
+
],
|
|
284
|
+
}]).then(function (ans) {
|
|
285
|
+
if (ans.model) {
|
|
286
|
+
model = ans.model;
|
|
287
|
+
settings.model = ans.model;
|
|
288
|
+
auth.saveSettings(settings);
|
|
289
|
+
printToScrollArea(chalk.yellow(" [SYS] Model: " + ans.model));
|
|
290
|
+
}
|
|
291
|
+
done();
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (input === "/settings") {
|
|
299
|
+
enterMenuMode(function (done) {
|
|
300
|
+
openSettingsPanel(done);
|
|
301
|
+
});
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (input === "/") {
|
|
306
|
+
enterMenuMode(function (done) {
|
|
307
|
+
var inquirer = require("inquirer");
|
|
308
|
+
inquirer.prompt([{
|
|
309
|
+
type: "list",
|
|
310
|
+
name: "cmd",
|
|
311
|
+
message: chalk.cyan("Select command"),
|
|
312
|
+
choices: SLASH_COMMANDS.map(function (c) {
|
|
313
|
+
return { name: chalk.green(c[0]) + chalk.gray(" " + c[1]), value: c[0] };
|
|
314
|
+
}).concat([
|
|
315
|
+
new inquirer.Separator(),
|
|
316
|
+
{ name: chalk.dim("Cancel"), value: "" },
|
|
317
|
+
]),
|
|
318
|
+
pageSize: 12,
|
|
319
|
+
}]).then(function (ans) {
|
|
320
|
+
done();
|
|
321
|
+
if (ans.cmd) {
|
|
322
|
+
processCommand(ans.cmd);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (input === "/login") {
|
|
330
|
+
enterMenuMode(function (done) {
|
|
331
|
+
auth.login().then(function () {
|
|
332
|
+
localMode = false;
|
|
333
|
+
creds = auth.getStoredCredentials();
|
|
334
|
+
printToScrollArea(chalk.yellow(" [SYS] Logged in!"));
|
|
335
|
+
}).catch(function (e) {
|
|
336
|
+
printToScrollArea(chalk.red(" ✘ " + e.message));
|
|
337
|
+
}).then(function () { done(); });
|
|
338
|
+
});
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (input === "/whoami") {
|
|
343
|
+
enterMenuMode(function (done) {
|
|
344
|
+
auth.whoami().catch(function (e) {
|
|
345
|
+
printToScrollArea(chalk.red(" ✘ " + e.message));
|
|
346
|
+
}).then(function () { done(); });
|
|
347
|
+
});
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (input === "/keys") {
|
|
352
|
+
enterMenuMode(function (done) {
|
|
353
|
+
auth.listApiKeys().catch(function (e) {
|
|
354
|
+
printToScrollArea(chalk.red(" ✘ " + e.message));
|
|
355
|
+
}).then(function () { done(); });
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── Send message ──────────────────────────────────
|
|
361
|
+
if (localMode) {
|
|
362
|
+
printToScrollArea(chalk.red(" ✘ Local mode - cannot send messages. Use /login to authenticate."));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Show user message in scroll area
|
|
367
|
+
printToScrollArea(chalk.bold.blue(" > " + input));
|
|
368
|
+
|
|
369
|
+
// Show thinking indicator
|
|
370
|
+
printToScrollArea(chalk.dim(" ⠋ Thinking..."));
|
|
371
|
+
|
|
372
|
+
client.sendMessage(input, { model: settings.model || "auto" })
|
|
373
|
+
.then(function (res) {
|
|
374
|
+
if (res.status === 200 && res.data) {
|
|
375
|
+
var text = res.data.answer || res.data.response || res.data.text || res.data.message || JSON.stringify(res.data);
|
|
376
|
+
var tok = res.data.tokens || res.data.usage || {};
|
|
377
|
+
var tokTotal = tok.total || tok.total_tokens || 0;
|
|
378
|
+
if (!tokTotal && text) {
|
|
379
|
+
tokTotal = Math.ceil(text.length / 4) + Math.ceil(input.length / 4);
|
|
380
|
+
}
|
|
381
|
+
if (tokTotal) ui.addTokens(tokTotal);
|
|
382
|
+
|
|
383
|
+
// Render markdown
|
|
384
|
+
var rendered = ui.renderMarkdown(text);
|
|
385
|
+
var lines = rendered.split("\n");
|
|
386
|
+
var output = [];
|
|
387
|
+
for (var i = 0; i < lines.length; i++) {
|
|
388
|
+
output.push(" " + lines[i]);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
output.push("");
|
|
392
|
+
output.push("");
|
|
393
|
+
|
|
394
|
+
printToScrollArea(output.join("\n"));
|
|
395
|
+
} else if (res.status === 401 || res.status === 403) {
|
|
396
|
+
printToScrollArea(chalk.red(" ✘ Auth error. Use /login to re-authenticate."));
|
|
397
|
+
} else {
|
|
398
|
+
printToScrollArea(chalk.red(" ✘ Error: " + JSON.stringify(res.data)));
|
|
399
|
+
}
|
|
400
|
+
drawInputPanel();
|
|
401
|
+
})
|
|
402
|
+
.catch(function (e) {
|
|
403
|
+
printToScrollArea(chalk.red(" ✘ Request failed: " + e.message + "\n Try /login if auth expired."));
|
|
404
|
+
drawInputPanel();
|
|
405
|
+
});
|
|
94
406
|
}
|
|
95
407
|
|
|
96
|
-
// ── Settings
|
|
97
|
-
function openSettingsPanel() {
|
|
408
|
+
// ── Settings Panel (inquirer) ─────────────────────────
|
|
409
|
+
function openSettingsPanel(done) {
|
|
98
410
|
var inquirer = require("inquirer");
|
|
99
411
|
var t = ui.getTheme();
|
|
100
412
|
|
|
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
413
|
inquirer.prompt([{
|
|
111
414
|
type: "list",
|
|
112
415
|
name: "field",
|
|
@@ -117,14 +420,14 @@ function startChat() {
|
|
|
117
420
|
pageSize: 12,
|
|
118
421
|
}]).then(function (ans) {
|
|
119
422
|
if (ans.field === "back") {
|
|
120
|
-
|
|
423
|
+
done();
|
|
121
424
|
return;
|
|
122
425
|
}
|
|
123
426
|
var field = null;
|
|
124
427
|
for (var i = 0; i < ui.SETTINGS_FIELDS.length; i++) {
|
|
125
428
|
if (ui.SETTINGS_FIELDS[i].key === ans.field) { field = ui.SETTINGS_FIELDS[i]; break; }
|
|
126
429
|
}
|
|
127
|
-
if (!field) {
|
|
430
|
+
if (!field) { done(); return; }
|
|
128
431
|
|
|
129
432
|
return inquirer.prompt([{
|
|
130
433
|
type: "list",
|
|
@@ -140,230 +443,162 @@ function startChat() {
|
|
|
140
443
|
settings[field.key] = ans2.value;
|
|
141
444
|
if (field.key === "theme") ui.setTheme(settings.theme);
|
|
142
445
|
auth.saveSettings(settings);
|
|
143
|
-
|
|
144
|
-
|
|
446
|
+
printToScrollArea(chalk.yellow(" [SYS] " + field.label + ": " + ans2.value));
|
|
447
|
+
done();
|
|
145
448
|
});
|
|
146
449
|
});
|
|
147
450
|
}
|
|
148
451
|
|
|
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
|
-
}
|
|
452
|
+
// ── Menu Mode (temporarily disable raw input) ────────
|
|
453
|
+
function enterMenuMode(fn) {
|
|
454
|
+
isInMenu = true;
|
|
455
|
+
// Reset scroll region for inquirer to work properly
|
|
456
|
+
var rows = getRows();
|
|
457
|
+
process.stdout.write(setScrollRegion(1, rows));
|
|
458
|
+
process.stdin.setRawMode(false);
|
|
459
|
+
process.stdin.removeAllListeners("data");
|
|
460
|
+
|
|
461
|
+
fn(function () {
|
|
462
|
+
// Re-enter TUI mode
|
|
463
|
+
isInMenu = false;
|
|
464
|
+
setupLayout();
|
|
465
|
+
drawInputPanel();
|
|
466
|
+
startRawInput();
|
|
467
|
+
});
|
|
468
|
+
}
|
|
176
469
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
470
|
+
// ── Cleanup ──────────────────────────────────────────
|
|
471
|
+
function cleanup() {
|
|
472
|
+
var rows = getRows();
|
|
473
|
+
process.stdout.write(setScrollRegion(1, rows));
|
|
474
|
+
process.stdout.write(cursorTo(rows, 1));
|
|
475
|
+
process.stdout.write(showCursor());
|
|
476
|
+
try { process.stdin.setRawMode(false); } catch (e) {}
|
|
477
|
+
}
|
|
181
478
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
479
|
+
// ── Raw Input Handler ────────────────────────────────
|
|
480
|
+
function startRawInput() {
|
|
481
|
+
if (!process.stdin.isTTY) return;
|
|
482
|
+
process.stdin.setRawMode(true);
|
|
483
|
+
process.stdin.resume();
|
|
484
|
+
process.stdin.setEncoding("utf8");
|
|
485
|
+
|
|
486
|
+
process.stdin.on("data", function onData(key) {
|
|
487
|
+
if (isInMenu) return;
|
|
488
|
+
|
|
489
|
+
// Ctrl+C / Ctrl+D
|
|
490
|
+
if (key === "\u0003" || key === "\u0004") {
|
|
491
|
+
cleanup();
|
|
492
|
+
console.log(chalk.yellow("\n Goodbye!\n"));
|
|
493
|
+
process.exit(0);
|
|
196
494
|
return;
|
|
197
495
|
}
|
|
198
496
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
ui.renderResponse("Permission: " + settings.permissionMode, "system");
|
|
205
|
-
showPrompt();
|
|
497
|
+
// Escape
|
|
498
|
+
if (key === "\u001b" && key.length === 1) {
|
|
499
|
+
cleanup();
|
|
500
|
+
console.log(chalk.yellow("\n Goodbye!\n"));
|
|
501
|
+
process.exit(0);
|
|
206
502
|
return;
|
|
207
503
|
}
|
|
208
504
|
|
|
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");
|
|
505
|
+
// Enter
|
|
506
|
+
if (key === "\r" || key === "\n") {
|
|
507
|
+
var cmd = inputBuffer.trim();
|
|
508
|
+
inputBuffer = "";
|
|
509
|
+
cursorPos = 0;
|
|
510
|
+
if (cmd) {
|
|
511
|
+
processCommand(cmd);
|
|
223
512
|
}
|
|
224
|
-
|
|
513
|
+
drawInputPanel();
|
|
225
514
|
return;
|
|
226
515
|
}
|
|
227
516
|
|
|
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
|
-
});
|
|
517
|
+
// Backspace
|
|
518
|
+
if (key === "\u007f" || key === "\b") {
|
|
519
|
+
if (cursorPos > 0) {
|
|
520
|
+
inputBuffer = inputBuffer.slice(0, cursorPos - 1) + inputBuffer.slice(cursorPos);
|
|
521
|
+
cursorPos--;
|
|
257
522
|
}
|
|
523
|
+
drawInputPanel();
|
|
258
524
|
return;
|
|
259
525
|
}
|
|
260
526
|
|
|
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();
|
|
527
|
+
// Tab - autocomplete
|
|
528
|
+
if (key === "\t") {
|
|
529
|
+
if (inputBuffer.startsWith("/")) {
|
|
530
|
+
var hits = slashNames.filter(function (c) { return c.indexOf(inputBuffer) === 0; });
|
|
531
|
+
if (hits.length === 1) {
|
|
532
|
+
inputBuffer = hits[0];
|
|
533
|
+
cursorPos = inputBuffer.length;
|
|
534
|
+
} else if (hits.length > 1) {
|
|
535
|
+
printToScrollArea(chalk.dim(" " + hits.join(" ")));
|
|
321
536
|
}
|
|
322
|
-
}
|
|
537
|
+
}
|
|
538
|
+
drawInputPanel();
|
|
323
539
|
return;
|
|
324
540
|
}
|
|
325
541
|
|
|
326
|
-
//
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
542
|
+
// Arrow keys (escape sequences)
|
|
543
|
+
if (key.length > 1 && key[0] === "\u001b") {
|
|
544
|
+
// Left arrow
|
|
545
|
+
if (key === "\u001b[D") {
|
|
546
|
+
if (cursorPos > 0) cursorPos--;
|
|
547
|
+
drawInputPanel();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
// Right arrow
|
|
551
|
+
if (key === "\u001b[C") {
|
|
552
|
+
if (cursorPos < inputBuffer.length) cursorPos++;
|
|
553
|
+
drawInputPanel();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
// Home
|
|
557
|
+
if (key === "\u001b[H") {
|
|
558
|
+
cursorPos = 0;
|
|
559
|
+
drawInputPanel();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
// End
|
|
563
|
+
if (key === "\u001b[F") {
|
|
564
|
+
cursorPos = inputBuffer.length;
|
|
565
|
+
drawInputPanel();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
// Delete
|
|
569
|
+
if (key === "\u001b[3~") {
|
|
570
|
+
if (cursorPos < inputBuffer.length) {
|
|
571
|
+
inputBuffer = inputBuffer.slice(0, cursorPos) + inputBuffer.slice(cursorPos + 1);
|
|
572
|
+
}
|
|
573
|
+
drawInputPanel();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
// Ignore other escape sequences
|
|
330
577
|
return;
|
|
331
578
|
}
|
|
332
579
|
|
|
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
|
-
});
|
|
580
|
+
// Normal character
|
|
581
|
+
inputBuffer = inputBuffer.slice(0, cursorPos) + key + inputBuffer.slice(cursorPos);
|
|
582
|
+
cursorPos += key.length;
|
|
583
|
+
drawInputPanel();
|
|
362
584
|
});
|
|
585
|
+
}
|
|
363
586
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
587
|
+
// ── Initialize TUI ───────────────────────────────────
|
|
588
|
+
console.log(chalk.dim("\n Type your message or / for commands. Tab for autocomplete.\n"));
|
|
589
|
+
|
|
590
|
+
// Wait a tick for all console.log output to flush, then enter TUI mode
|
|
591
|
+
return new Promise(function (resolveChat) {
|
|
592
|
+
setTimeout(function () {
|
|
593
|
+
setupLayout();
|
|
594
|
+
drawInputPanel();
|
|
595
|
+
startRawInput();
|
|
596
|
+
}, 100);
|
|
597
|
+
|
|
598
|
+
// Handle clean exit
|
|
599
|
+
process.on("SIGINT", function () {
|
|
600
|
+
cleanup();
|
|
601
|
+
process.exit(0);
|
|
367
602
|
});
|
|
368
603
|
});
|
|
369
604
|
});
|
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
|
};
|