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.
Files changed (3) hide show
  1. package/lib/chat.js +475 -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,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
- 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 = 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
- function showPrompt() {
92
- rl.setPrompt(chalk.bold.green("> "));
93
- rl.prompt();
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 via inquirer (Windows-compatible) ──────
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
- showPrompt();
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) { showPrompt(); return; }
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
- ui.renderResponse(field.label + ": " + ans2.value, "system");
144
- showPrompt();
451
+ printToScrollArea(chalk.yellow(" [SYS] " + field.label + ": " + ans2.value));
452
+ done();
145
453
  });
146
454
  });
147
455
  }
148
456
 
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
- }
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
- if (input === "/settings") {
178
- openSettingsPanel();
179
- return;
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
- 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();
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
- 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();
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
- 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");
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
- showPrompt();
518
+ drawInputPanel();
225
519
  return;
226
520
  }
227
521
 
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
- });
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
- 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();
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
- // ── Send message ────────────────────────────
327
- if (localMode) {
328
- ui.renderResponse("Local mode - cannot send messages. Use /login to authenticate.", "error");
329
- showPrompt();
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
- 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
- });
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
- rl.on("close", function () {
365
- console.log("");
366
- resolveChat();
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
  };
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.0",
4
4
  "description": "BLUN AI Assistant - Command Line Interface",
5
5
  "bin": {
6
6
  "blun": "./bin/blun.js"