blun-king-cli 6.4.1 → 7.0.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.
package/lib/chat.js CHANGED
@@ -1,166 +1,370 @@
1
- const readline = require('readline');
2
- const chalk = require('chalk');
3
- const ui = require('./ui');
4
- const auth = require('./auth');
5
- const client = require('./client');
1
+ /**
2
+ * BLUN King CLI v7.0.0 - Chat Module
3
+ * Claude Code-style interactive chat with framed input, streaming, permission modes
4
+ * CommonJS
5
+ */
6
6
 
7
- let localMode = false;
8
- let firstPrompt = true;
7
+ var readline = require("readline");
8
+ var chalk = require("chalk");
9
+ var ui = require("./ui");
10
+ var auth = require("./auth");
11
+ var client = require("./client");
12
+
13
+ var localMode = false;
14
+ var permissionMode = "normal"; // safe, normal, god
9
15
 
10
16
  function showPrompt(rl, username, model) {
11
- ui.renderInputBox(username, model);
17
+ ui.renderInputBox(username, model, permissionMode);
12
18
  rl.prompt();
13
19
  }
14
20
 
15
- async function startChat() {
21
+ function cyclePermission() {
22
+ var modes = ui.PERMISSION_MODES;
23
+ var idx = modes.indexOf(permissionMode);
24
+ permissionMode = modes[(idx + 1) % modes.length];
25
+ return permissionMode;
26
+ }
27
+
28
+ function startChat() {
29
+ // Show banner
16
30
  ui.renderBanner();
17
31
 
32
+ // Check auth
33
+ var authPromise;
18
34
  if (!auth.isAuthenticated()) {
19
- try {
20
- const result = await auth.showWelcomeMenu();
21
- if (result === 'local') localMode = true;
22
- } catch (e) {
23
- console.log(chalk.red('\n ' + e.message));
24
- console.log(chalk.yellow(' Trying again...\n'));
25
- try {
26
- const retry = await auth.showWelcomeMenu();
27
- if (retry === 'local') localMode = true;
28
- } catch (e2) {
29
- console.log(chalk.red('\n ' + e2.message));
35
+ authPromise = auth.showWelcomeMenu().then(function (result) {
36
+ if (result === "local") localMode = true;
37
+ }).catch(function (e) {
38
+ console.log(chalk.red("\n " + e.message));
39
+ console.log(chalk.yellow(" Trying again...\n"));
40
+ return auth.showWelcomeMenu().then(function (retry) {
41
+ if (retry === "local") localMode = true;
42
+ }).catch(function (e2) {
43
+ console.log(chalk.red("\n " + e2.message));
30
44
  process.exit(1);
31
- }
32
- }
45
+ });
46
+ });
47
+ } else {
48
+ authPromise = Promise.resolve();
33
49
  }
34
50
 
35
- const creds = auth.getStoredCredentials();
36
- if (creds) {
37
- ui.renderResponse('Connected as ' + (creds.name || creds.email || 'user') + ' | Plan: ' + (creds.plan || 'free'), 'system');
38
- } else if (localMode) {
39
- ui.renderResponse('Local mode - no API calls available', 'system');
40
- }
51
+ return authPromise.then(function () {
52
+ var creds = auth.getStoredCredentials();
53
+ if (creds) {
54
+ ui.renderResponse(
55
+ "Connected as " +
56
+ (creds.name || creds.email || "user") +
57
+ " | Plan: " +
58
+ (creds.plan || "free"),
59
+ "system"
60
+ );
61
+ } else if (localMode) {
62
+ ui.renderResponse("Local mode - no API calls available", "system");
63
+ }
41
64
 
42
- const settings = auth.getSettings();
43
- const username = (creds ? creds.name : null) || 'local';
44
- const model = settings.model || 'auto';
65
+ var settings = auth.getSettings();
66
+ permissionMode = settings.permissionMode || "normal";
67
+ var username = (creds ? creds.name : null) || "local";
68
+ var model = settings.model || "auto";
45
69
 
46
- const rl = readline.createInterface({
47
- input: process.stdin,
48
- output: process.stdout,
49
- prompt: ui.renderPrompt(username, model),
50
- terminal: true
51
- });
70
+ // Show permission line
71
+ ui.renderPermissionLine(permissionMode);
72
+ console.log();
52
73
 
53
- showPrompt(rl, username, model);
74
+ var rl = readline.createInterface({
75
+ input: process.stdin,
76
+ output: process.stdout,
77
+ prompt: ui.renderPrompt(),
78
+ terminal: true,
79
+ });
54
80
 
55
- return new Promise((resolveChat) => {
56
- rl.on('line', async (line) => {
57
- const input = line.trim();
58
- ui.renderInputBoxClose();
59
- if (!input) { showPrompt(rl, username, model); return; }
81
+ showPrompt(rl, username, model);
60
82
 
61
- if (input === '/exit' || input === '/quit') {
62
- ui.renderResponse('Goodbye!', 'system'); process.exit(0);
63
- }
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; }
66
- if (input === '/login') {
67
- try { await auth.login(); localMode = false; ui.renderResponse('Logged in!', 'system'); }
68
- catch(e) { ui.renderResponse(e.message, 'error'); }
69
- showPrompt(rl, username, model); return;
70
- }
71
- if (input === '/whoami') {
72
- try { await auth.whoami(); } catch(e) { ui.renderResponse(e.message, 'error'); }
73
- showPrompt(rl, username, model); return;
74
- }
75
- if (input === '/keys') {
76
- try { await auth.listApiKeys(); } catch(e) { ui.renderResponse(e.message, 'error'); }
77
- showPrompt(rl, username, model); return;
78
- }
79
- if (input === '/model') {
80
- const s = auth.getSettings();
81
- const choice = await auth.promptMenu('Select Model', [
82
- { label: 'Auto (recommended)', value: 'auto' },
83
- { label: 'Gemma 4', value: 'gemma4' },
84
- { label: 'Claude', value: 'claude' },
85
- { label: 'Llama', value: 'llama' }
86
- ]);
87
- if (choice) { s.model = choice; auth.saveSettings(s); ui.renderResponse('Model set to: ' + choice, 'system'); }
88
- showPrompt(rl, username, model); return;
89
- }
90
- if (input === '/settings') {
91
- const s = auth.getSettings();
92
- ui.renderBox('Settings', [
93
- 'Model: ' + (s.model || 'auto'),
94
- 'Language: ' + (s.language || 'de'),
95
- 'Voice: ' + (s.voice ? 'on' : 'off')
96
- ].join('\n'), 'cyan');
97
- const action = await auth.promptMenu('Change Setting', [
98
- { label: 'Model', value: 'model' },
99
- { label: 'Language (de/en)', value: 'language' },
100
- { label: 'Voice on/off', value: 'voice' },
101
- { label: 'Back', value: 'back' }
102
- ]);
103
- if (action === 'model') {
104
- const m = await auth.promptMenu('Select Model', [
105
- { label: 'Auto', value: 'auto' }, { label: 'Gemma 4', value: 'gemma4' },
106
- { label: 'Claude', value: 'claude' }, { label: 'Llama', value: 'llama' }
107
- ]);
108
- if (m) { s.model = m; auth.saveSettings(s); ui.renderResponse('Model: ' + m, 'system'); }
109
- } else if (action === 'language') {
110
- const l = await auth.prompt(chalk.green(' Language (de/en): '));
111
- if (l) { s.language = l; auth.saveSettings(s); ui.renderResponse('Language: ' + l, 'system'); }
112
- } else if (action === 'voice') {
113
- s.voice = !s.voice; auth.saveSettings(s); ui.renderResponse('Voice: ' + (s.voice ? 'on' : 'off'), 'system');
114
- }
115
- showPrompt(rl, username, model); return;
116
- }
117
- if (input === '/help') {
118
- ui.renderBox('Chat Commands', [
119
- '/exit Exit chat',
120
- '/clear Clear screen',
121
- '/status Show token usage & uptime',
122
- '/login Login from chat',
123
- '/whoami Show current user info',
124
- '/keys List API keys',
125
- '/model Switch AI model',
126
- '/settings Show/change settings',
127
- '/help Show this help'
128
- ].join('\n'), 'cyan');
129
- showPrompt(rl, username, model); return;
130
- }
83
+ return new Promise(function (resolveChat) {
84
+ rl.on("line", function (line) {
85
+ var input = line.trim();
86
+ ui.renderInputBoxClose();
131
87
 
132
- if (localMode) {
133
- ui.renderResponse('Local mode - cannot send messages. Use /login to authenticate.', 'error');
134
- showPrompt(rl, username, model); return;
135
- }
88
+ if (!input) {
89
+ showPrompt(rl, username, model);
90
+ return;
91
+ }
136
92
 
137
- const spinner = ui.renderSpinner('Thinking...');
138
- try {
139
- const settings = auth.getSettings();
140
- const res = await client.sendMessage(input, { model: settings.model || 'auto' });
141
- ui.stopSpinner(true, 'Response received');
142
- if (res.status === 200 && res.data) {
143
- const text = res.data.answer || res.data.response || res.data.text || res.data.message || JSON.stringify(res.data);
144
- ui.renderResponse(text, 'agent');
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);
150
- } else if (res.status === 401 || res.status === 403) {
151
- ui.renderResponse('Auth error. Use /login to re-authenticate.', 'error');
152
- } else {
153
- ui.renderResponse('Error: ' + JSON.stringify(res.data), 'error');
154
- }
155
- } catch (e) {
156
- ui.stopSpinner(false, e.message);
157
- ui.renderResponse('Request failed: ' + e.message + '\n Try /login if auth expired.', 'error');
158
- }
159
- showPrompt(rl, username, model);
160
- });
93
+ // Commands
94
+ if (input === "/exit" || input === "/quit") {
95
+ ui.renderResponse("Goodbye!", "system");
96
+ rl.close();
97
+ return;
98
+ }
99
+
100
+ if (input === "/clear") {
101
+ console.clear();
102
+ ui.renderBanner();
103
+ showPrompt(rl, username, model);
104
+ return;
105
+ }
106
+
107
+ if (input === "/status") {
108
+ ui.renderStatusBar(ui.getStats());
109
+ showPrompt(rl, username, model);
110
+ return;
111
+ }
112
+
113
+ if (input === "/perm" || input === "/permission") {
114
+ var newMode = cyclePermission();
115
+ settings.permissionMode = newMode;
116
+ auth.saveSettings(settings);
117
+ ui.renderPermissionLine(newMode);
118
+ console.log();
119
+ showPrompt(rl, username, model);
120
+ return;
121
+ }
122
+
123
+ if (input === "/login") {
124
+ auth
125
+ .login()
126
+ .then(function () {
127
+ localMode = false;
128
+ creds = auth.getStoredCredentials();
129
+ username = (creds ? creds.name : null) || "local";
130
+ ui.renderResponse("Logged in!", "system");
131
+ })
132
+ .catch(function (e) {
133
+ ui.renderResponse(e.message, "error");
134
+ })
135
+ .then(function () {
136
+ showPrompt(rl, username, model);
137
+ });
138
+ return;
139
+ }
140
+
141
+ if (input === "/whoami") {
142
+ auth
143
+ .whoami()
144
+ .catch(function (e) {
145
+ ui.renderResponse(e.message, "error");
146
+ })
147
+ .then(function () {
148
+ showPrompt(rl, username, model);
149
+ });
150
+ return;
151
+ }
152
+
153
+ if (input === "/keys") {
154
+ auth
155
+ .listApiKeys()
156
+ .catch(function (e) {
157
+ ui.renderResponse(e.message, "error");
158
+ })
159
+ .then(function () {
160
+ showPrompt(rl, username, model);
161
+ });
162
+ return;
163
+ }
164
+
165
+ if (input === "/model") {
166
+ auth
167
+ .promptMenu("Select Model", [
168
+ { label: "Auto (recommended)", value: "auto" },
169
+ { label: "Gemma 4", value: "gemma4" },
170
+ { label: "Claude", value: "claude" },
171
+ { label: "Llama", value: "llama" },
172
+ ])
173
+ .then(function (choice) {
174
+ if (choice) {
175
+ model = choice;
176
+ settings.model = choice;
177
+ auth.saveSettings(settings);
178
+ ui.renderResponse("Model set to: " + choice, "system");
179
+ }
180
+ showPrompt(rl, username, model);
181
+ });
182
+ return;
183
+ }
184
+
185
+ if (input === "/settings") {
186
+ ui.renderBox(
187
+ "Settings",
188
+ [
189
+ "Model: " + (settings.model || "auto"),
190
+ "Language: " + (settings.language || "de"),
191
+ "Voice: " + (settings.voice ? "on" : "off"),
192
+ "Permission: " + (permissionMode || "normal"),
193
+ ].join("\n"),
194
+ "cyan"
195
+ );
196
+ auth
197
+ .promptMenu("Change Setting", [
198
+ { label: "Model", value: "model" },
199
+ { label: "Language (de/en)", value: "language" },
200
+ { label: "Voice on/off", value: "voice" },
201
+ { label: "Back", value: "back" },
202
+ ])
203
+ .then(function (action) {
204
+ if (action === "model") {
205
+ return auth
206
+ .promptMenu("Select Model", [
207
+ { label: "Auto", value: "auto" },
208
+ { label: "Gemma 4", value: "gemma4" },
209
+ { label: "Claude", value: "claude" },
210
+ { label: "Llama", value: "llama" },
211
+ ])
212
+ .then(function (m) {
213
+ if (m) {
214
+ model = m;
215
+ settings.model = m;
216
+ auth.saveSettings(settings);
217
+ ui.renderResponse("Model: " + m, "system");
218
+ }
219
+ });
220
+ } else if (action === "language") {
221
+ return auth.prompt(chalk.green(" Language (de/en): ")).then(function (l) {
222
+ if (l) {
223
+ settings.language = l;
224
+ auth.saveSettings(settings);
225
+ ui.renderResponse("Language: " + l, "system");
226
+ }
227
+ });
228
+ } else if (action === "voice") {
229
+ settings.voice = !settings.voice;
230
+ auth.saveSettings(settings);
231
+ ui.renderResponse(
232
+ "Voice: " + (settings.voice ? "on" : "off"),
233
+ "system"
234
+ );
235
+ }
236
+ })
237
+ .then(function () {
238
+ showPrompt(rl, username, model);
239
+ });
240
+ return;
241
+ }
242
+
243
+ if (input === "/help") {
244
+ ui.renderBox(
245
+ "Chat Commands",
246
+ [
247
+ "/exit Exit chat",
248
+ "/clear Clear screen",
249
+ "/status Show token usage & uptime",
250
+ "/login Login from chat",
251
+ "/whoami Show current user info",
252
+ "/keys List API keys",
253
+ "/model Switch AI model",
254
+ "/settings Show/change settings",
255
+ "/perm Cycle permission mode (safe/normal/god)",
256
+ "/help Show this help",
257
+ ].join("\n"),
258
+ "cyan"
259
+ );
260
+ showPrompt(rl, username, model);
261
+ return;
262
+ }
263
+
264
+ // Send message
265
+ if (localMode) {
266
+ ui.renderResponse(
267
+ "Local mode - cannot send messages. Use /login to authenticate.",
268
+ "error"
269
+ );
270
+ showPrompt(rl, username, model);
271
+ return;
272
+ }
273
+
274
+ // Use streaming
275
+ var spinner = ui.renderSpinner("Thinking...");
276
+ client
277
+ .sendMessageStream(input, {
278
+ model: settings.model || "auto",
279
+ onChunk: function (chunk) {
280
+ if (spinner) {
281
+ ui.stopSpinner(true, "Streaming...");
282
+ spinner = null;
283
+ ui.renderStreamStart();
284
+ }
285
+ ui.renderStreamChunk(chunk);
286
+ },
287
+ })
288
+ .then(function (result) {
289
+ if (spinner) {
290
+ // No streaming happened, fallback to non-stream
291
+ ui.stopSpinner(true, "Response received");
292
+ ui.renderResponse(result.text || "(empty response)", "agent");
293
+ } else {
294
+ ui.renderStreamEnd();
295
+ }
296
+
297
+ // Token tracking
298
+ var tokTotal = 0;
299
+ if (result.usage) {
300
+ tokTotal =
301
+ result.usage.total ||
302
+ result.usage.total_tokens ||
303
+ (result.usage.input || 0) + (result.usage.output || 0) ||
304
+ 0;
305
+ }
306
+ // Estimate tokens from text length if no usage data
307
+ if (!tokTotal && result.text) {
308
+ tokTotal = Math.ceil(result.text.length / 4) + Math.ceil(input.length / 4);
309
+ }
310
+ if (tokTotal) ui.addTokens(tokTotal);
311
+
312
+ var usedModel = settings.model || "chat";
313
+ ui.renderMiniStatus(usedModel, tokTotal);
314
+ showPrompt(rl, username, model);
315
+ })
316
+ .catch(function (e) {
317
+ if (spinner) {
318
+ ui.stopSpinner(false, e.message);
319
+ } else {
320
+ ui.renderStreamEnd();
321
+ }
322
+ // Fallback to non-streaming
323
+ ui.renderResponse("Stream failed, trying non-stream...", "system");
324
+ return client
325
+ .sendMessage(input, { model: settings.model || "auto" })
326
+ .then(function (res) {
327
+ if (res.status === 200 && res.data) {
328
+ var text =
329
+ res.data.answer ||
330
+ res.data.response ||
331
+ res.data.text ||
332
+ res.data.message ||
333
+ JSON.stringify(res.data);
334
+ ui.renderResponse(text, "agent");
335
+ var tok = res.data.tokens || res.data.usage || {};
336
+ var tokTotal = tok.total || tok.total_tokens || 0;
337
+ if (tokTotal) ui.addTokens(tokTotal);
338
+ ui.renderMiniStatus(settings.model || "chat", tokTotal);
339
+ } else if (res.status === 401 || res.status === 403) {
340
+ ui.renderResponse(
341
+ "Auth error. Use /login to re-authenticate.",
342
+ "error"
343
+ );
344
+ } else {
345
+ ui.renderResponse(
346
+ "Error: " + JSON.stringify(res.data),
347
+ "error"
348
+ );
349
+ }
350
+ showPrompt(rl, username, model);
351
+ })
352
+ .catch(function (e2) {
353
+ ui.renderResponse(
354
+ "Request failed: " + e2.message + "\n Try /login if auth expired.",
355
+ "error"
356
+ );
357
+ showPrompt(rl, username, model);
358
+ });
359
+ });
360
+ });
161
361
 
162
- rl.on('close', () => { console.log(''); resolveChat(); });
362
+ rl.on("close", function () {
363
+ console.log("");
364
+ resolveChat();
365
+ });
366
+ });
163
367
  });
164
368
  }
165
369
 
166
- module.exports = { startChat };
370
+ module.exports = { startChat: startChat };