blun-king-cli 6.3.5 → 6.4.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 +36 -22
  2. package/lib/ui.js +86 -14
  3. package/package.json +1 -1
package/lib/chat.js CHANGED
@@ -5,6 +5,12 @@ const auth = require('./auth');
5
5
  const client = require('./client');
6
6
 
7
7
  let localMode = false;
8
+ let firstPrompt = true;
9
+
10
+ function showPrompt(rl, username, model) {
11
+ ui.renderInputBox(username, model);
12
+ rl.prompt();
13
+ }
8
14
 
9
15
  async function startChat() {
10
16
  ui.renderBanner();
@@ -34,39 +40,43 @@ async function startChat() {
34
40
  }
35
41
 
36
42
  const settings = auth.getSettings();
43
+ const username = (creds ? creds.name : null) || 'local';
44
+ const model = settings.model || 'auto';
45
+
37
46
  const rl = readline.createInterface({
38
47
  input: process.stdin,
39
48
  output: process.stdout,
40
- prompt: ui.renderPrompt((creds ? creds.name : null) || 'local', settings.model || 'auto'),
49
+ prompt: ui.renderPrompt(username, model),
41
50
  terminal: true
42
51
  });
43
52
 
44
- console.log(' Type /help for commands, /exit to quit.\n');
45
- rl.prompt();
53
+ showPrompt(rl, username, model);
46
54
 
47
55
  return new Promise((resolveChat) => {
48
56
  rl.on('line', async (line) => {
49
57
  const input = line.trim();
50
- if (!input) { rl.prompt(); return; }
58
+ ui.renderInputBoxClose();
59
+ if (!input) { showPrompt(rl, username, model); return; }
51
60
 
52
61
  if (input === '/exit' || input === '/quit') {
53
62
  ui.renderResponse('Goodbye!', 'system'); process.exit(0);
54
63
  }
55
- if (input === '/clear') { console.clear(); ui.renderBanner(); rl.prompt(); return; }
56
- if (input === '/status') { ui.renderStatusBar(ui.getStats()); rl.prompt(); return; }
64
+ if (input === '/clear') { console.clear(); ui.renderBanner(); showPrompt(rl, username, model); return; }
65
+ if (input === '/status') { ui.renderStatusBar(ui.getStats()); showPrompt(rl, username, model); return; }
57
66
  if (input === '/login') {
58
67
  try { await auth.login(); localMode = false; ui.renderResponse('Logged in!', 'system'); }
59
68
  catch(e) { ui.renderResponse(e.message, 'error'); }
60
- rl.prompt(); return;
69
+ showPrompt(rl, username, model); return;
61
70
  }
62
71
  if (input === '/whoami') {
63
72
  try { await auth.whoami(); } catch(e) { ui.renderResponse(e.message, 'error'); }
64
- rl.prompt(); return;
73
+ showPrompt(rl, username, model); return;
65
74
  }
66
75
  if (input === '/keys') {
67
76
  try { await auth.listApiKeys(); } catch(e) { ui.renderResponse(e.message, 'error'); }
68
- rl.prompt(); return;
69
- } if (input === '/model') {
77
+ showPrompt(rl, username, model); return;
78
+ }
79
+ if (input === '/model') {
70
80
  const s = auth.getSettings();
71
81
  const choice = await auth.promptMenu('Select Model', [
72
82
  { label: 'Auto (recommended)', value: 'auto' },
@@ -75,15 +85,15 @@ async function startChat() {
75
85
  { label: 'Llama', value: 'llama' }
76
86
  ]);
77
87
  if (choice) { s.model = choice; auth.saveSettings(s); ui.renderResponse('Model set to: ' + choice, 'system'); }
78
- rl.prompt(); return;
88
+ showPrompt(rl, username, model); return;
79
89
  }
80
90
  if (input === '/settings') {
81
91
  const s = auth.getSettings();
82
- console.log(chalk.bold.cyan('\n Current Settings:'));
83
- console.log(chalk.white(' Model: ') + (s.model || 'auto'));
84
- console.log(chalk.white(' Language: ') + (s.language || 'de'));
85
- console.log(chalk.white(' Voice: ') + (s.voice ? 'on' : 'off'));
86
- console.log('');
92
+ ui.renderBox('Settings', [
93
+ 'Model: ' + (s.model || 'auto'),
94
+ 'Language: ' + (s.language || 'de'),
95
+ 'Voice: ' + (s.voice ? 'on' : 'off')
96
+ ].join('\n'), 'cyan');
87
97
  const action = await auth.promptMenu('Change Setting', [
88
98
  { label: 'Model', value: 'model' },
89
99
  { label: 'Language (de/en)', value: 'language' },
@@ -102,7 +112,7 @@ async function startChat() {
102
112
  } else if (action === 'voice') {
103
113
  s.voice = !s.voice; auth.saveSettings(s); ui.renderResponse('Voice: ' + (s.voice ? 'on' : 'off'), 'system');
104
114
  }
105
- rl.prompt(); return;
115
+ showPrompt(rl, username, model); return;
106
116
  }
107
117
  if (input === '/help') {
108
118
  ui.renderBox('Chat Commands', [
@@ -116,12 +126,12 @@ async function startChat() {
116
126
  '/settings Show/change settings',
117
127
  '/help Show this help'
118
128
  ].join('\n'), 'cyan');
119
- rl.prompt(); return;
129
+ showPrompt(rl, username, model); return;
120
130
  }
121
131
 
122
132
  if (localMode) {
123
133
  ui.renderResponse('Local mode - cannot send messages. Use /login to authenticate.', 'error');
124
- rl.prompt(); return;
134
+ showPrompt(rl, username, model); return;
125
135
  }
126
136
 
127
137
  const spinner = ui.renderSpinner('Thinking...');
@@ -132,7 +142,11 @@ async function startChat() {
132
142
  if (res.status === 200 && res.data) {
133
143
  const text = res.data.answer || res.data.response || res.data.text || res.data.message || JSON.stringify(res.data);
134
144
  ui.renderResponse(text, 'agent');
135
- if (res.data.usage) ui.addTokens(res.data.usage.total_tokens || 0, 0.000003);
145
+ const tok = res.data.tokens || res.data.usage || {};
146
+ const tokTotal = tok.total || tok.total_tokens || 0;
147
+ if (tokTotal) ui.addTokens(tokTotal, 0.000003);
148
+ const usedModel = res.data.routing && res.data.routing.task && res.data.routing.task.type || settings.model || 'auto';
149
+ ui.renderMiniStatus(usedModel, tokTotal);
136
150
  } else if (res.status === 401 || res.status === 403) {
137
151
  ui.renderResponse('Auth error. Use /login to re-authenticate.', 'error');
138
152
  } else {
@@ -142,11 +156,11 @@ async function startChat() {
142
156
  ui.stopSpinner(false, e.message);
143
157
  ui.renderResponse('Request failed: ' + e.message + '\n Try /login if auth expired.', 'error');
144
158
  }
145
- rl.prompt();
159
+ showPrompt(rl, username, model);
146
160
  });
147
161
 
148
162
  rl.on('close', () => { console.log(''); resolveChat(); });
149
163
  });
150
164
  }
151
165
 
152
- module.exports = { startChat };
166
+ module.exports = { startChat };
package/lib/ui.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * BLUN King CLI - Rich Terminal UI Module
3
- * Task 11: Claude Code-style terminal experience
3
+ * Claude Code-style terminal experience
4
4
  * CommonJS - chalk@4, ora@5, boxen@5
5
5
  */
6
6
 
@@ -47,27 +47,82 @@ function renderBanner() {
47
47
  console.log(chalk.dim(" " + "\u2500".repeat(53) + "\n"));
48
48
  }
49
49
 
50
+ function getInputBoxWidth() {
51
+ return Math.min((process.stdout.columns || 80) - 2, 76);
52
+ }
53
+
54
+ function renderInputBox(username, model) {
55
+ var w = getInputBoxWidth();
56
+ var u = username || "user";
57
+ var m = model || "auto";
58
+ var hints = "/help \u2502 /settings \u2502 /model \u2502 /exit";
59
+ var labelText = " " + u + " [" + m + "] ";
60
+ var hintsText = " " + hints + " ";
61
+ var topFill = w - labelText.length - hintsText.length;
62
+ if (topFill < 2) topFill = 2;
63
+ var topLine = chalk.dim("\u256d") + chalk.cyan(labelText) + chalk.dim("\u2500".repeat(topFill)) + chalk.dim(hintsText) + chalk.dim("\u256e");
64
+ console.log(topLine);
65
+ }
66
+
67
+ function renderInputBoxClose() {
68
+ var w = getInputBoxWidth();
69
+ console.log(chalk.dim("\u2570" + "\u2500".repeat(w) + "\u256f"));
70
+ }
71
+
50
72
  function renderPrompt(username, model) {
51
- var u = chalk.cyan.bold(username || "user");
52
- var m = chalk.dim("[" + (model || "auto") + "]");
53
- return chalk.dim("\u250c") + " " + u + " " + m + "\n" + chalk.dim("\u2514") + " " + chalk.green.bold("\u276f") + " ";
73
+ // Called by readline as the prompt string
74
+ // Show left border + cursor arrow
75
+ return chalk.dim("\u2502") + " " + chalk.green.bold("\u276f") + " ";
76
+ }
77
+
78
+ function renderWelcomeInfo() {
79
+ var w = getInputBoxWidth();
80
+ console.log(chalk.dim("\u2502"));
81
+ console.log(chalk.dim("\u2502") + chalk.dim(" Tip: Type a message and press Enter. Use /help for all commands."));
82
+ console.log(chalk.dim("\u2570" + "\u2500".repeat(w) + "\u256f"));
54
83
  }
55
84
 
56
85
  function renderResponse(text, type) {
57
86
  var colorFn = colors[type] || colors.agent;
58
87
  var labels = { user: "YOU", agent: "BLUN", system: "SYS", error: "ERR" };
59
88
  var label = labels[type] || "BLUN";
60
- var prefix = colorFn.bold(" [" + label + "] ");
61
89
  var rendered = renderMarkdown(text);
62
- var lines = rendered.split("\n");
63
- lines.forEach(function (line, i) {
64
- if (i === 0) {
65
- console.log(prefix + colorFn(line));
66
- } else {
67
- console.log(" " + colorFn(line));
68
- }
69
- });
70
- console.log();
90
+
91
+ if (type === "agent") {
92
+ // Agent responses in a box
93
+ console.log(
94
+ boxen(rendered, {
95
+ title: chalk.green.bold(" \u2728 BLUN "),
96
+ titleAlignment: "left",
97
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
98
+ margin: { top: 0, bottom: 0, left: 1, right: 1 },
99
+ borderStyle: "round",
100
+ borderColor: "green",
101
+ })
102
+ );
103
+ } else if (type === "error") {
104
+ console.log(
105
+ boxen(chalk.red(rendered), {
106
+ title: chalk.red.bold(" \u2718 Error "),
107
+ titleAlignment: "left",
108
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
109
+ margin: { top: 0, bottom: 0, left: 1, right: 1 },
110
+ borderStyle: "round",
111
+ borderColor: "red",
112
+ })
113
+ );
114
+ } else {
115
+ var prefix = colorFn.bold(" [" + label + "] ");
116
+ var lines = rendered.split("\n");
117
+ lines.forEach(function (line, i) {
118
+ if (i === 0) {
119
+ console.log(prefix + colorFn(line));
120
+ } else {
121
+ console.log(" " + colorFn(line));
122
+ }
123
+ });
124
+ console.log();
125
+ }
71
126
  }
72
127
 
73
128
  function renderMarkdown(text) {
@@ -154,6 +209,19 @@ function renderCodeBlock(code, language) {
154
209
  console.log();
155
210
  }
156
211
 
212
+ function renderMiniStatus(model, tokens) {
213
+ var elapsed = Math.floor((Date.now() - startTime) / 1000);
214
+ var mins = Math.floor(elapsed / 60);
215
+ var secs = elapsed % 60;
216
+ var parts = [];
217
+ parts.push(chalk.bgCyan.black(" " + (model || "auto") + " "));
218
+ if (tokens) parts.push(chalk.dim("Tokens: " + tokens.toLocaleString()));
219
+ parts.push(chalk.dim("Total: " + totalTokens.toLocaleString()));
220
+ parts.push(chalk.dim(mins + "m " + secs + "s"));
221
+ console.log(" " + parts.join(chalk.dim(" \u2502 ")));
222
+ console.log();
223
+ }
224
+
157
225
  function renderStatusBar(stats) {
158
226
  var s = stats || {};
159
227
  var model = s.model || "auto";
@@ -200,6 +268,10 @@ function getStats() {
200
268
  module.exports = {
201
269
  renderBanner: renderBanner,
202
270
  renderPrompt: renderPrompt,
271
+ renderInputBox: renderInputBox,
272
+ renderInputBoxClose: renderInputBoxClose,
273
+ renderWelcomeInfo: renderWelcomeInfo,
274
+ renderMiniStatus: renderMiniStatus,
203
275
  renderResponse: renderResponse,
204
276
  renderSpinner: renderSpinner,
205
277
  stopSpinner: stopSpinner,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "6.3.5",
3
+ "version": "6.4.1",
4
4
  "description": "BLUN AI Assistant - Command Line Interface",
5
5
  "bin": {
6
6
  "blun": "./bin/blun.js"