blun-king-cli 7.1.0 → 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 +480 -243
  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,173 +82,193 @@ 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";
76
-
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
- },
89
- });
90
-
91
- function showPrompt() {
92
- ui.renderInputPanel(settings);
93
- rl.setPrompt(ui.renderPrompt());
94
- rl.prompt();
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));
95
99
  }
96
100
 
97
- // ── Interactive Settings Panel ──────────────────────
98
- function openSettingsPanel() {
99
- var selection = 0;
100
-
101
- function render() {
102
- // Clear and redraw
103
- readline.cursorTo(process.stdout, 0);
104
- readline.clearScreenDown(process.stdout);
105
- var lines = ui.buildSettingsPanel(settings, selection);
106
- console.log(lines.join("\n"));
107
- }
108
-
109
- function cleanup() {
110
- process.stdin.removeListener("keypress", onKeypress);
111
- if (process.stdin.isTTY && process.stdin.setRawMode) process.stdin.setRawMode(false);
112
- process.stdin.resume();
113
- console.log();
114
- showPrompt();
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));
115
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
+ }
116
160
 
117
- function onKeypress(_, key) {
118
- key = key || {};
119
- if (key.name === "up") {
120
- selection = (selection + ui.SETTINGS_FIELDS.length - 1) % ui.SETTINGS_FIELDS.length;
121
- render();
122
- } else if (key.name === "down") {
123
- selection = (selection + 1) % ui.SETTINGS_FIELDS.length;
124
- render();
125
- } else if (key.name === "right") {
126
- var field = ui.SETTINGS_FIELDS[selection];
127
- settings = ui.cycleSetting(settings, field.key, 1);
128
- if (field.key === "theme") ui.setTheme(settings.theme);
129
- auth.saveSettings(settings);
130
- render();
131
- } else if (key.name === "left") {
132
- var field = ui.SETTINGS_FIELDS[selection];
133
- settings = ui.cycleSetting(settings, field.key, -1);
134
- if (field.key === "theme") ui.setTheme(settings.theme);
135
- auth.saveSettings(settings);
136
- render();
137
- } else if (key.name === "return" || key.name === "escape") {
138
- cleanup();
139
- } else if (key.ctrl && key.name === "c") {
140
- cleanup();
141
- }
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]);
142
172
  }
143
-
144
- // Pause readline, take raw input
145
- rl.pause();
146
- readline.emitKeypressEvents(process.stdin);
147
- if (process.stdin.isTTY && process.stdin.setRawMode) process.stdin.setRawMode(true);
148
- process.stdin.on("keypress", onKeypress);
149
- render();
173
+ process.stdout.write(restoreCursor());
174
+ drawInputPanel();
150
175
  }
151
176
 
152
- console.log();
153
- showPrompt();
154
-
155
- return new Promise(function (resolveChat) {
156
- rl.on("line", function (line) {
157
- var input = line.trim();
158
-
159
- if (!input) {
160
- showPrompt();
161
- return;
162
- }
177
+ // ── Handle resize ────────────────────────────────────
178
+ process.stdout.on("resize", function () {
179
+ setupLayout();
180
+ drawInputPanel();
181
+ });
163
182
 
164
- ui.renderInputPanelClose(settings);
183
+ // ── Process command ──────────────────────────────────
184
+ function processCommand(input) {
185
+ if (!input) return;
165
186
 
166
- // ── Commands ────────────────────────────────
167
- if (input === "/exit" || input === "/quit") {
168
- ui.renderResponse("Goodbye!", "system");
169
- rl.close();
170
- return;
171
- }
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
+ }
172
194
 
173
- if (input === "/clear") {
174
- console.clear();
175
- ui.renderBanner();
176
- showPrompt();
177
- return;
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());
178
201
  }
202
+ process.stdout.write(cursorTo(1, 1));
203
+ drawInputPanel();
204
+ return;
205
+ }
179
206
 
180
- if (input === "/settings") {
181
- openSettingsPanel();
182
- return;
183
- }
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
+ }
184
218
 
185
- if (input === "/status") {
186
- var stats = ui.getStats();
187
- var t = ui.getTheme();
188
- console.log();
189
- console.log(t.header(" Status"));
190
- console.log(t.muted(" " + "\u2500".repeat(40)));
191
- console.log(" Model: " + chalk.white(settings.model || "auto"));
192
- console.log(" Permission: " + ui.formatSettingValue({ key: "permissionMode" }, settings.permissionMode));
193
- console.log(" Theme: " + chalk.white(settings.theme || "neon"));
194
- console.log(" Tokens: " + chalk.white(ui.formatNumber(stats.tokens)));
195
- console.log(" Cost: " + chalk.white(ui.estimateCost(stats.tokens, settings.model)));
196
- console.log(" Uptime: " + chalk.white(ui.formatDuration(stats.uptime)));
197
- console.log();
198
- showPrompt();
199
- return;
200
- }
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
+ }
201
234
 
202
- if (input === "/perm" || input === "/permission") {
203
- var modes = ui.PERMISSION_MODES;
204
- var idx = modes.indexOf(settings.permissionMode);
205
- settings.permissionMode = modes[(idx + 1) % modes.length];
206
- auth.saveSettings(settings);
207
- ui.renderResponse("Permission: " + settings.permissionMode, "system");
208
- showPrompt();
209
- return;
210
- }
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
+ }
211
244
 
212
- if (input.indexOf("/theme") === 0) {
213
- var parts = input.split(/\s+/);
214
- if (parts[1] && ui.THEMES[parts[1]]) {
215
- settings.theme = parts[1];
216
- ui.setTheme(parts[1]);
217
- auth.saveSettings(settings);
218
- ui.renderResponse("Theme: " + parts[1], "system");
219
- } else {
220
- var modes = Object.keys(ui.THEMES);
221
- var idx = modes.indexOf(settings.theme || "neon");
222
- settings.theme = modes[(idx + 1) % modes.length];
223
- ui.setTheme(settings.theme);
224
- auth.saveSettings(settings);
225
- ui.renderResponse("Theme: " + settings.theme, "system");
226
- }
227
- showPrompt();
228
- return;
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];
229
253
  }
254
+ ui.setTheme(settings.theme);
255
+ auth.saveSettings(settings);
256
+ printToScrollArea(chalk.yellow(" [SYS] Theme: " + settings.theme));
257
+ drawInputPanel();
258
+ return;
259
+ }
230
260
 
231
- if (input.indexOf("/model") === 0) {
232
- var parts = input.split(/\s+/);
233
- if (parts[1]) {
234
- settings.model = parts[1];
235
- model = parts[1];
236
- auth.saveSettings(settings);
237
- ui.renderResponse("Model: " + parts[1], "system");
238
- showPrompt();
239
- } else {
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) {
240
272
  var inquirer = require("inquirer");
241
273
  inquirer.prompt([{
242
274
  type: "list",
@@ -253,57 +285,24 @@ function startChat() {
253
285
  model = ans.model;
254
286
  settings.model = ans.model;
255
287
  auth.saveSettings(settings);
256
- ui.renderResponse("Model: " + ans.model, "system");
288
+ printToScrollArea(chalk.yellow(" [SYS] Model: " + ans.model));
257
289
  }
258
- showPrompt();
290
+ done();
259
291
  });
260
- }
261
- return;
262
- }
263
-
264
- if (input === "/login") {
265
- auth.login().then(function () {
266
- localMode = false;
267
- creds = auth.getStoredCredentials();
268
- username = (creds ? creds.name : null) || "local";
269
- ui.renderResponse("Logged in!", "system");
270
- }).catch(function (e) {
271
- ui.renderResponse(e.message, "error");
272
- }).then(function () { showPrompt(); });
273
- return;
274
- }
275
-
276
- if (input === "/whoami") {
277
- auth.whoami().catch(function (e) {
278
- ui.renderResponse(e.message, "error");
279
- }).then(function () { showPrompt(); });
280
- return;
281
- }
282
-
283
- if (input === "/keys") {
284
- auth.listApiKeys().catch(function (e) {
285
- ui.renderResponse(e.message, "error");
286
- }).then(function () { showPrompt(); });
287
- return;
288
- }
289
-
290
- if (input === "/help") {
291
- var t = ui.getTheme();
292
- console.log();
293
- console.log(t.header(" Commands"));
294
- console.log(t.muted(" " + "\u2500".repeat(40)));
295
- SLASH_COMMANDS.forEach(function (c) {
296
- console.log(" " + chalk.green(c[0]) + " ".repeat(Math.max(1, 16 - c[0].length)) + chalk.gray(c[1]));
297
292
  });
298
- console.log();
299
- console.log(t.muted(" Tip: Type / and press Tab for autocomplete"));
300
- console.log();
301
- showPrompt();
302
- return;
303
293
  }
294
+ return;
295
+ }
296
+
297
+ if (input === "/settings") {
298
+ enterMenuMode(function (done) {
299
+ openSettingsPanel(done);
300
+ });
301
+ return;
302
+ }
304
303
 
305
- // ── Slash command picker (just "/") ──────────
306
- if (input === "/") {
304
+ if (input === "/") {
305
+ enterMenuMode(function (done) {
307
306
  var inquirer = require("inquirer");
308
307
  inquirer.prompt([{
309
308
  type: "list",
@@ -317,56 +316,294 @@ function startChat() {
317
316
  ]),
318
317
  pageSize: 12,
319
318
  }]).then(function (ans) {
319
+ done();
320
320
  if (ans.cmd) {
321
- rl.emit("line", ans.cmd);
322
- } else {
323
- showPrompt();
321
+ processCommand(ans.cmd);
324
322
  }
325
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
+ });
411
+ }
412
+
413
+ // ── Settings Panel (inquirer) ─────────────────────────
414
+ function openSettingsPanel(done) {
415
+ var inquirer = require("inquirer");
416
+ var t = ui.getTheme();
417
+
418
+ inquirer.prompt([{
419
+ type: "list",
420
+ name: "field",
421
+ message: "Change setting",
422
+ choices: ui.SETTINGS_FIELDS.map(function (f) {
423
+ return { name: f.label + " — " + ui.formatSettingValue(f, settings[f.key]), value: f.key };
424
+ }).concat([{ name: chalk.dim("Back"), value: "back" }]),
425
+ pageSize: 12,
426
+ }]).then(function (ans) {
427
+ if (ans.field === "back") {
428
+ done();
429
+ return;
430
+ }
431
+ var field = null;
432
+ for (var i = 0; i < ui.SETTINGS_FIELDS.length; i++) {
433
+ if (ui.SETTINGS_FIELDS[i].key === ans.field) { field = ui.SETTINGS_FIELDS[i]; break; }
434
+ }
435
+ if (!field) { done(); return; }
436
+
437
+ return inquirer.prompt([{
438
+ type: "list",
439
+ name: "value",
440
+ message: field.label + " (" + (field.help || "") + ")",
441
+ choices: field.values.map(function (v) {
442
+ var isCurrent = settings[field.key] === v;
443
+ var label = String(v);
444
+ if (typeof v === "boolean") label = v ? "On" : "Off";
445
+ return { name: label + (isCurrent ? chalk.green(" (current)") : ""), value: v };
446
+ }),
447
+ }]).then(function (ans2) {
448
+ settings[field.key] = ans2.value;
449
+ if (field.key === "theme") ui.setTheme(settings.theme);
450
+ auth.saveSettings(settings);
451
+ printToScrollArea(chalk.yellow(" [SYS] " + field.label + ": " + ans2.value));
452
+ done();
453
+ });
454
+ });
455
+ }
456
+
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
+ }
474
+
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
+ }
483
+
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);
326
499
  return;
327
500
  }
328
501
 
329
- // ── Send message ────────────────────────────
330
- if (localMode) {
331
- ui.renderResponse("Local mode - cannot send messages. Use /login to authenticate.", "error");
332
- showPrompt();
502
+ // Escape
503
+ if (key === "\u001b" && key.length === 1) {
504
+ cleanup();
505
+ console.log(chalk.yellow("\n Goodbye!\n"));
506
+ process.exit(0);
333
507
  return;
334
508
  }
335
509
 
336
- var spinner = ui.renderSpinner("Thinking...");
337
- client.sendMessage(input, { model: settings.model || "auto" })
338
- .then(function (res) {
339
- ui.stopSpinner(true, "Response received");
340
- if (res.status === 200 && res.data) {
341
- var text = res.data.answer || res.data.response || res.data.text || res.data.message || JSON.stringify(res.data);
342
- var tok = res.data.tokens || res.data.usage || {};
343
- var tokTotal = tok.total || tok.total_tokens || 0;
344
- if (!tokTotal && text) {
345
- tokTotal = Math.ceil(text.length / 4) + Math.ceil(input.length / 4);
346
- }
347
- if (tokTotal) ui.addTokens(tokTotal);
348
- ui.renderResponse(text, "agent", {
349
- model: settings.model || "auto",
350
- tokens: tokTotal,
351
- cost: settings.costVisibility,
352
- });
353
- } else if (res.status === 401 || res.status === 403) {
354
- ui.renderResponse("Auth error. Use /login to re-authenticate.", "error");
355
- } else {
356
- ui.renderResponse("Error: " + JSON.stringify(res.data), "error");
510
+ // Enter
511
+ if (key === "\r" || key === "\n") {
512
+ var cmd = inputBuffer.trim();
513
+ inputBuffer = "";
514
+ cursorPos = 0;
515
+ if (cmd) {
516
+ processCommand(cmd);
517
+ }
518
+ drawInputPanel();
519
+ return;
520
+ }
521
+
522
+ // Backspace
523
+ if (key === "\u007f" || key === "\b") {
524
+ if (cursorPos > 0) {
525
+ inputBuffer = inputBuffer.slice(0, cursorPos - 1) + inputBuffer.slice(cursorPos);
526
+ cursorPos--;
527
+ }
528
+ drawInputPanel();
529
+ return;
530
+ }
531
+
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(" ")));
357
541
  }
358
- showPrompt();
359
- })
360
- .catch(function (e) {
361
- ui.stopSpinner(false, e.message);
362
- ui.renderResponse("Request failed: " + e.message + "\n Try /login if auth expired.", "error");
363
- showPrompt();
364
- });
542
+ }
543
+ drawInputPanel();
544
+ return;
545
+ }
546
+
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
582
+ return;
583
+ }
584
+
585
+ // Normal character
586
+ inputBuffer = inputBuffer.slice(0, cursorPos) + key + inputBuffer.slice(cursorPos);
587
+ cursorPos += key.length;
588
+ drawInputPanel();
365
589
  });
590
+ }
366
591
 
367
- rl.on("close", function () {
368
- console.log("");
369
- 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);
370
607
  });
371
608
  });
372
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.0",
3
+ "version": "7.2.0",
4
4
  "description": "BLUN AI Assistant - Command Line Interface",
5
5
  "bin": {
6
6
  "blun": "./bin/blun.js"