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.
Files changed (3) hide show
  1. package/lib/chat.js +470 -235
  2. package/lib/ui.js +21 -0
  3. package/package.json +1 -1
package/lib/chat.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * BLUN King CLI v7.1.0 - Chat Module
3
- * Full-screen REPL with Settings Panel, Slash Suggestions, Theme support
4
- * Adapted from Codex 1 patterns for blun.ai remote API
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
- // ── Slash Commands ────────────────────────────────────────
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
- ui.renderResponse(
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
- ui.renderResponse("Local mode - no API calls available", "system");
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
- var rl = readline.createInterface({
78
- input: process.stdin,
79
- output: process.stdout,
80
- prompt: ui.renderPrompt(),
81
- terminal: true,
82
- completer: function (line) {
83
- if (line.startsWith("/")) {
84
- var hits = slashNames.filter(function (c) { return c.indexOf(line) === 0; });
85
- return [hits.length ? hits : slashNames, line];
86
- }
87
- return [[], line];
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
- function showPrompt() {
92
- rl.setPrompt(chalk.bold.green("> "));
93
- rl.prompt();
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 via inquirer (Windows-compatible) ──────
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
- showPrompt();
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) { showPrompt(); return; }
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
- ui.renderResponse(field.label + ": " + ans2.value, "system");
144
- showPrompt();
446
+ printToScrollArea(chalk.yellow(" [SYS] " + field.label + ": " + ans2.value));
447
+ done();
145
448
  });
146
449
  });
147
450
  }
148
451
 
149
- console.log();
150
- showPrompt();
151
-
152
- return new Promise(function (resolveChat) {
153
- rl.on("line", function (line) {
154
- var input = line.trim();
155
-
156
- if (!input) {
157
- showPrompt();
158
- return;
159
- }
160
-
161
- console.log();
162
-
163
- // ── Commands ────────────────────────────────
164
- if (input === "/exit" || input === "/quit") {
165
- ui.renderResponse("Goodbye!", "system");
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
- if (input === "/settings") {
178
- openSettingsPanel();
179
- return;
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
- if (input === "/status") {
183
- var stats = ui.getStats();
184
- var t = ui.getTheme();
185
- console.log();
186
- console.log(t.header(" Status"));
187
- console.log(t.muted(" " + "\u2500".repeat(40)));
188
- console.log(" Model: " + chalk.white(settings.model || "auto"));
189
- console.log(" Permission: " + ui.formatSettingValue({ key: "permissionMode" }, settings.permissionMode));
190
- console.log(" Theme: " + chalk.white(settings.theme || "neon"));
191
- console.log(" Tokens: " + chalk.white(ui.formatNumber(stats.tokens)));
192
- console.log(" Cost: " + chalk.white(ui.estimateCost(stats.tokens, settings.model)));
193
- console.log(" Uptime: " + chalk.white(ui.formatDuration(stats.uptime)));
194
- console.log();
195
- showPrompt();
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
- if (input === "/perm" || input === "/permission") {
200
- var modes = ui.PERMISSION_MODES;
201
- var idx = modes.indexOf(settings.permissionMode);
202
- settings.permissionMode = modes[(idx + 1) % modes.length];
203
- auth.saveSettings(settings);
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
- if (input.indexOf("/theme") === 0) {
210
- var parts = input.split(/\s+/);
211
- if (parts[1] && ui.THEMES[parts[1]]) {
212
- settings.theme = parts[1];
213
- ui.setTheme(parts[1]);
214
- auth.saveSettings(settings);
215
- ui.renderResponse("Theme: " + parts[1], "system");
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
- showPrompt();
513
+ drawInputPanel();
225
514
  return;
226
515
  }
227
516
 
228
- if (input.indexOf("/model") === 0) {
229
- var parts = input.split(/\s+/);
230
- if (parts[1]) {
231
- settings.model = parts[1];
232
- model = parts[1];
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
- if (input === "/login") {
262
- auth.login().then(function () {
263
- localMode = false;
264
- creds = auth.getStoredCredentials();
265
- username = (creds ? creds.name : null) || "local";
266
- ui.renderResponse("Logged in!", "system");
267
- }).catch(function (e) {
268
- ui.renderResponse(e.message, "error");
269
- }).then(function () { showPrompt(); });
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
- // ── Send message ────────────────────────────
327
- if (localMode) {
328
- ui.renderResponse("Local mode - cannot send messages. Use /login to authenticate.", "error");
329
- showPrompt();
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
- var spinner = ui.renderSpinner("Thinking...");
334
- client.sendMessage(input, { model: settings.model || "auto" })
335
- .then(function (res) {
336
- ui.stopSpinner(true, "Response received");
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
- rl.on("close", function () {
365
- console.log("");
366
- resolveChat();
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "7.1.1",
3
+ "version": "7.2.1",
4
4
  "description": "BLUN AI Assistant - Command Line Interface",
5
5
  "bin": {
6
6
  "blun": "./bin/blun.js"