blun-king-cli 6.1.0 → 6.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.
package/bin/blun.js CHANGED
@@ -25,12 +25,17 @@ async function main() {
25
25
  ui.renderBox("BLUN CLI Commands", [
26
26
  "blun Start interactive chat (default)",
27
27
  "blun chat Interactive chat mode",
28
- "blun ask \"...\" One-shot question",
29
- "blun login Device login flow",
28
+ 'blun ask \"...\" One-shot question',
29
+ "blun login Login with email + password",
30
+ "blun register Register new account",
30
31
  "blun logout Remove stored credentials",
31
32
  "blun auth --api-key KEY Authenticate with API key",
33
+ "blun keys list List your API keys",
34
+ "blun keys create Create a new API key",
35
+ "blun whoami Show current user info",
36
+ "blun settings Show/change settings",
32
37
  "blun status Show system health",
33
- "blun init Initialize BLUN project in current dir",
38
+ "blun init Initialize BLUN project in current dir",
34
39
  "blun version Show version",
35
40
  "blun help Show this help"
36
41
  ].join("\n"), "cyan");
@@ -40,10 +45,57 @@ async function main() {
40
45
  try { await auth.login(); } catch (e) { ui.renderResponse(e.message, "error"); process.exit(1); }
41
46
  break;
42
47
 
48
+ case "register":
49
+ try { await auth.register(); } catch (e) { ui.renderResponse(e.message, "error"); process.exit(1); }
50
+ break;
51
+
43
52
  case "logout":
44
53
  auth.logout();
45
54
  break;
46
55
 
56
+ case "whoami":
57
+ try { await auth.whoami(); } catch (e) { ui.renderResponse(e.message, "error"); }
58
+ break;
59
+
60
+ case "keys":
61
+ if (args[1] === "create") {
62
+ try { await auth.createApiKey(args[2]); } catch (e) { ui.renderResponse(e.message, "error"); process.exit(1); }
63
+ } else if (args[1] === "list" || !args[1]) {
64
+ try { await auth.listApiKeys(); } catch (e) { ui.renderResponse(e.message, "error"); process.exit(1); }
65
+ } else {
66
+ console.error("Usage: blun keys [list|create]");
67
+ process.exit(1);
68
+ }
69
+ break;
70
+ case "settings": {
71
+ const chalk = require("chalk");
72
+ const s = auth.getSettings();
73
+ console.log(chalk.bold.cyan("\n Current Settings:"));
74
+ console.log(chalk.white(" Model: ") + (s.model || "auto"));
75
+ console.log(chalk.white(" Language: ") + (s.language || "de"));
76
+ console.log(chalk.white(" Voice: ") + (s.voice ? "on" : "off"));
77
+ console.log("");
78
+ const action = await auth.promptMenu("Change Setting", [
79
+ { label: "Model", value: "model" },
80
+ { label: "Language (de/en)", value: "language" },
81
+ { label: "Voice on/off", value: "voice" },
82
+ { label: "Exit", value: "exit" }
83
+ ]);
84
+ if (action === "model") {
85
+ const m = await auth.promptMenu("Select Model", [
86
+ { label: "Auto", value: "auto" }, { label: "Gemma 4", value: "gemma4" },
87
+ { label: "Claude", value: "claude" }, { label: "Llama", value: "llama" }
88
+ ]);
89
+ if (m) { s.model = m; auth.saveSettings(s); console.log(chalk.green(" Model set to: " + m)); }
90
+ } else if (action === "language") {
91
+ const l = await auth.prompt(chalk.green(" Language (de/en): "));
92
+ if (l) { s.language = l; auth.saveSettings(s); console.log(chalk.green(" Language set to: " + l)); }
93
+ } else if (action === "voice") {
94
+ s.voice = !s.voice; auth.saveSettings(s); console.log(chalk.green(" Voice: " + (s.voice ? "on" : "off")));
95
+ }
96
+ break;
97
+ }
98
+
47
99
  case "auth":
48
100
  if (args[1] === "--api-key" && args[2]) {
49
101
  try { await auth.loginWithApiKey(args[2]); } catch (e) { ui.renderResponse(e.message, "error"); process.exit(1); }
@@ -55,15 +107,18 @@ async function main() {
55
107
 
56
108
  case "ask": {
57
109
  const question = args.slice(1).join(" ");
58
- if (!question) { console.error("Usage: blun ask \"your question\""); process.exit(1); }
110
+ if (!question) { console.error('Usage: blun ask \"your question\"'); process.exit(1); }
111
+ if (!auth.isAuthenticated()) {
112
+ ui.renderResponse("Not logged in. Run: blun login", "error"); process.exit(1);
113
+ }
114
+ const spinner = ui.renderSpinner("Thinking...");
59
115
  try {
60
- auth.ensureAuth();
61
- const spinner = ui.renderSpinner("Thinking...");
62
116
  const res = await client.sendMessage(question);
63
117
  ui.stopSpinner(true);
64
- const text = res.data?.response || res.data?.text || res.data?.message || JSON.stringify(res.data);
118
+ const text = (res.data && (res.data.response || res.data.text || res.data.message)) || JSON.stringify(res.data);
65
119
  ui.renderResponse(text, "agent");
66
120
  } catch (e) {
121
+ ui.stopSpinner(false);
67
122
  ui.renderResponse(e.message, "error");
68
123
  process.exit(1);
69
124
  }
@@ -76,10 +131,9 @@ async function main() {
76
131
  const res = await client.getHealth();
77
132
  ui.stopSpinner(true);
78
133
  if (res.status === 200 && typeof res.data === "object") {
79
- const d = res.data;
80
134
  ui.renderTable(
81
135
  ["Metric", "Value"],
82
- Object.entries(d).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : String(v)])
136
+ Object.entries(res.data).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : String(v)])
83
137
  );
84
138
  } else {
85
139
  ui.renderResponse("Health check returned: " + res.status, "error");
@@ -96,10 +150,8 @@ async function main() {
96
150
 
97
151
  case "chat":
98
152
  default: {
99
- // Determine initial folder: path arg or cwd
100
153
  let initialFolder = process.cwd();
101
154
  if (command !== "chat" && command !== undefined) {
102
- // default case — command might be a path
103
155
  try {
104
156
  const fs = require("fs");
105
157
  const resolved = path.resolve(command);
@@ -108,7 +160,6 @@ async function main() {
108
160
  }
109
161
  } catch {}
110
162
  }
111
- // Also check args[1] for "blun chat /path"
112
163
  if (args[1]) {
113
164
  try {
114
165
  const fs = require("fs");
@@ -119,7 +170,6 @@ async function main() {
119
170
  } catch {}
120
171
  }
121
172
 
122
- // Workspace trust flow
123
173
  const trustedFolder = await workspace.selectFolder(initialFolder);
124
174
  process.chdir(trustedFolder);
125
175
  workspace.loadProjectConfig(trustedFolder);
@@ -131,4 +181,4 @@ async function main() {
131
181
  }
132
182
  }
133
183
 
134
- main().catch((e) => { console.error("Fatal:", e.message); process.exit(1); });
184
+ main().catch((e) => { console.error("Fatal:", e.message); process.exit(1); });
package/lib/auth.js CHANGED
@@ -2,161 +2,216 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
  const https = require('https');
5
- const { execSync } = require('child_process');
5
+ const readline = require('readline');
6
+ const chalk = require('chalk');
6
7
 
7
8
  const CREDS_DIR = path.join(os.homedir(), '.blun');
8
9
  const CREDS_FILE = path.join(CREDS_DIR, 'credentials.json');
10
+ const SETTINGS_FILE = path.join(CREDS_DIR, 'settings.json');
9
11
  const BASE_URL = 'blun.ai';
10
12
 
11
13
  function ensureDir() {
12
- if (!fs.existsSync(CREDS_DIR)) {
13
- fs.mkdirSync(CREDS_DIR, { mode: 0o700, recursive: true });
14
- }
15
- }
16
-
17
- function saveCredentials(creds) {
18
- ensureDir();
19
- fs.writeFileSync(CREDS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
14
+ if (!fs.existsSync(CREDS_DIR)) fs.mkdirSync(CREDS_DIR, { mode: 0o700, recursive: true });
20
15
  }
21
-
16
+ function saveCredentials(creds) { ensureDir(); fs.writeFileSync(CREDS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 }); }
22
17
  function getStoredCredentials() {
23
18
  try {
24
19
  if (!fs.existsSync(CREDS_FILE)) return null;
25
20
  const data = JSON.parse(fs.readFileSync(CREDS_FILE, 'utf8'));
26
- if (data.expires_at && new Date(data.expires_at) < new Date()) {
27
- console.error('[BLUN Auth] Token expired. Please run: blun login');
28
- return null;
29
- }
21
+ if (data.expires_at && new Date(data.expires_at) < new Date()) return null;
30
22
  return data;
31
23
  } catch { return null; }
32
24
  }
33
-
34
- function isAuthenticated() {
35
- return getStoredCredentials() !== null;
25
+ function getSettings() {
26
+ try {
27
+ if (!fs.existsSync(SETTINGS_FILE)) return { model: 'auto', language: 'de', voice: false };
28
+ return JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
29
+ } catch { return { model: 'auto', language: 'de', voice: false }; }
36
30
  }
37
-
31
+ function saveSettings(s) { ensureDir(); fs.writeFileSync(SETTINGS_FILE, JSON.stringify(s, null, 2)); }
32
+ function isAuthenticated() { return getStoredCredentials() !== null; }
38
33
  function getAuthHeader() {
39
- const creds = getStoredCredentials();
40
- if (!creds) return null;
41
- return { Authorization: 'Bearer ' + creds.token };
34
+ const c = getStoredCredentials(); if (!c) return null;
35
+ if (c.apiKey) return { 'x-blun-key': c.apiKey };
36
+ return { Authorization: 'Bearer ' + c.token };
42
37
  }
43
-
44
38
  function ensureAuth() {
45
- if (!isAuthenticated()) {
46
- throw new Error('Not authenticated. Run "blun login" or "blun auth --api-key <key>" first.');
47
- }
39
+ if (!isAuthenticated()) throw new Error('Not authenticated. Run "blun login" first.');
48
40
  return getStoredCredentials();
49
41
  }
50
42
 
51
43
  function httpsRequest(options, body) {
52
44
  return new Promise((resolve, reject) => {
53
45
  const req = https.request(options, (res) => {
54
- let data = '';
55
- res.on('data', (c) => data += c);
46
+ let data = ''; res.on('data', c => data += c);
56
47
  res.on('end', () => {
57
48
  try { resolve({ status: res.statusCode, data: JSON.parse(data) }); }
58
49
  catch { resolve({ status: res.statusCode, data }); }
59
50
  });
60
- });
61
- req.on('error', reject);
62
- if (body) req.write(JSON.stringify(body));
63
- req.end();
51
+ }); req.on('error', reject);
52
+ if (body) req.write(JSON.stringify(body)); req.end();
64
53
  });
65
54
  }
66
55
 
67
- function openBrowser(url) {
68
- try {
69
- const platform = process.platform;
70
- if (platform === 'win32') execSync('start "" "' + url + '"', { stdio: 'ignore' });
71
- else if (platform === 'darwin') execSync('open "' + url + '"', { stdio: 'ignore' });
72
- else execSync('xdg-open "' + url + '"', { stdio: 'ignore' });
73
- } catch {
74
- console.log('Open this URL manually: ' + url);
75
- }
56
+ function prompt(question) {
57
+ return new Promise(resolve => {
58
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
59
+ rl.question(question, a => { rl.close(); resolve(a.trim()); });
60
+ });
76
61
  }
77
62
 
78
- async function login() {
79
- console.log('[BLUN Auth] Starting device login flow...');
80
- const startRes = await httpsRequest({
81
- hostname: BASE_URL, path: '/api/king/auth/device/start',
82
- method: 'POST', headers: { 'Content-Type': 'application/json' }
63
+ function promptPassword(question) {
64
+ return new Promise(resolve => {
65
+ process.stdout.write(question); let pw = '';
66
+ const stdin = process.stdin; const wasRaw = stdin.isRaw;
67
+ if (stdin.setRawMode) stdin.setRawMode(true); stdin.resume();
68
+ const onData = (ch) => {
69
+ const c = ch.toString('utf8');
70
+ if (c === '\n' || c === '\r' || c === '\u0004') {
71
+ if (stdin.setRawMode) stdin.setRawMode(wasRaw || false);
72
+ stdin.removeListener('data', onData); process.stdout.write('\n'); resolve(pw);
73
+ } else if (c === '\u007f' || c === '\b') {
74
+ if (pw.length > 0) { pw = pw.slice(0, -1); process.stdout.write('\b \b'); }
75
+ } else if (c === '\u0003') { process.exit(0); }
76
+ else { pw += c; process.stdout.write('*'); }
77
+ };
78
+ stdin.on('data', onData);
83
79
  });
80
+ }
84
81
 
85
- if (startRes.status !== 200 || !startRes.data.code) {
86
- throw new Error('Failed to start device flow: ' + JSON.stringify(startRes.data));
87
- }
88
-
89
- const { code, verification_url } = startRes.data;
90
- const url = verification_url || 'https://blun.ai/api/king/auth/device/start';
91
-
92
- console.log('');
93
- console.log(' Enter this code in your browser: ' + code);
94
- console.log(' URL: ' + url);
82
+ async function promptMenu(title, options) {
83
+ console.log(''); console.log(chalk.bold.cyan(' ' + title));
84
+ console.log(chalk.dim(' ' + '\u2500'.repeat(40)));
85
+ options.forEach((opt, i) => console.log(chalk.yellow(' ' + (i + 1) + ')') + ' ' + opt.label));
95
86
  console.log('');
87
+ const answer = await prompt(chalk.green(' > '));
88
+ const idx = parseInt(answer, 10) - 1;
89
+ return (idx >= 0 && idx < options.length) ? options[idx].value : null;
90
+ }
91
+ async function login() {
92
+ console.log(chalk.bold.cyan('\n BLUN Login\n'));
93
+ const email = await prompt(chalk.green(' Email: '));
94
+ const password = await promptPassword(chalk.green(' Password: '));
95
+ if (!email || !password) throw new Error('Email and password required.');
96
+ const res = await httpsRequest({
97
+ hostname: BASE_URL, path: '/api/king/auth/login',
98
+ method: 'POST', headers: { 'Content-Type': 'application/json' }
99
+ }, { email, password });
100
+ if (res.status !== 200 || !res.data.token) {
101
+ throw new Error('Login failed: ' + (res.data.message || res.data.error || JSON.stringify(res.data)));
102
+ }
103
+ const creds = {
104
+ token: res.data.token, email: res.data.email || email,
105
+ name: res.data.name || (res.data.user ? res.data.user.name : '') || '',
106
+ plan: res.data.plan || (res.data.user ? res.data.user.plan : '') || 'free'
107
+ };
108
+ saveCredentials(creds);
109
+ console.log(chalk.green('\n Login successful! Welcome, ' + (creds.name || creds.email)));
110
+ return creds;
111
+ }
96
112
 
97
- openBrowser(url);
98
- console.log('Waiting for approval...');
99
-
100
- const maxAttempts = 120;
101
- for (let i = 0; i < maxAttempts; i++) {
102
- await new Promise(r => setTimeout(r, 2000));
103
- const pollRes = await httpsRequest({
104
- hostname: BASE_URL,
105
- path: '/api/king/auth/device/poll?code=' + encodeURIComponent(code),
106
- method: 'GET'
107
- });
108
-
109
- if (pollRes.status === 200 && pollRes.data.token) {
110
- const creds = {
111
- token: pollRes.data.token,
112
- email: pollRes.data.email || '',
113
- name: pollRes.data.name || '',
114
- expires_at: pollRes.data.expires_at || '',
115
- plan: pollRes.data.plan || 'free'
116
- };
117
- saveCredentials(creds);
118
- console.log('[BLUN Auth] Login successful! Welcome, ' + (creds.name || creds.email || 'user'));
119
- return creds;
120
- }
121
-
122
- if (pollRes.data.status === 'denied' || pollRes.data.error === 'denied') {
123
- throw new Error('Login denied by user.');
124
- }
113
+ async function register() {
114
+ console.log(chalk.bold.cyan('\n BLUN Registration\n'));
115
+ const name = await prompt(chalk.green(' Name: '));
116
+ const email = await prompt(chalk.green(' Email: '));
117
+ const password = await promptPassword(chalk.green(' Password: '));
118
+ if (!email || !password || !name) throw new Error('Name, email and password required.');
119
+ const res = await httpsRequest({
120
+ hostname: BASE_URL, path: '/api/king/auth/register',
121
+ method: 'POST', headers: { 'Content-Type': 'application/json' }
122
+ }, { name, email, password });
123
+ if (res.status !== 200 && res.status !== 201) {
124
+ throw new Error('Registration failed: ' + (res.data.message || res.data.error || JSON.stringify(res.data)));
125
+ }
126
+ if (res.data.token) {
127
+ const creds = { token: res.data.token, email: res.data.email || email, name: res.data.name || name, plan: res.data.plan || 'free' };
128
+ saveCredentials(creds); console.log(chalk.green('\n Account created! Welcome, ' + name)); return creds;
125
129
  }
126
- throw new Error('Login timed out after 4 minutes.');
130
+ console.log(chalk.green('\n Account created! Please login with: blun login')); return null;
127
131
  }
128
132
 
129
133
  async function loginWithApiKey(apiKey) {
130
- console.log('[BLUN Auth] Validating API key...');
131
- const res = await httpsRequest({
132
- hostname: BASE_URL, path: '/api/king/auth/validate',
133
- method: 'POST',
134
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + apiKey }
135
- });
134
+ console.log(chalk.dim(' Validating API key...'));
135
+ const res = await httpsRequest({ hostname: BASE_URL, path: '/api/king/auth/me', method: 'GET', headers: { 'x-blun-key': apiKey } });
136
+ if (res.status !== 200) throw new Error('Invalid API key: ' + (res.data.message || 'validation failed'));
137
+ const creds = { apiKey, token: apiKey, email: res.data.email || '', name: res.data.name || '', plan: res.data.plan || 'free' };
138
+ saveCredentials(creds); console.log(chalk.green(' API key validated and saved.')); return creds;
139
+ }
136
140
 
137
- if (res.status !== 200 || !res.data.valid) {
138
- throw new Error('Invalid API key: ' + (res.data.message || 'validation failed'));
141
+ async function createApiKey(name) {
142
+ ensureAuth();
143
+ const keyName = name || await prompt(chalk.green(' Key name: '));
144
+ const headers = { 'Content-Type': 'application/json' };
145
+ const ah = getAuthHeader(); if (ah) Object.assign(headers, ah);
146
+ const res = await httpsRequest({ hostname: BASE_URL, path: '/api/king/auth/api-keys', method: 'POST', headers }, { name: keyName });
147
+ if (res.status !== 200 && res.status !== 201) throw new Error('Failed to create key: ' + (res.data.message || JSON.stringify(res.data)));
148
+ console.log(chalk.green('\n API Key created:'));
149
+ console.log(chalk.yellow(' ' + (res.data.key || res.data.apiKey || JSON.stringify(res.data))));
150
+ console.log(chalk.dim(' Save this key - it will not be shown again.\n'));
151
+ return res.data;
152
+ }
153
+ async function listApiKeys() {
154
+ ensureAuth();
155
+ const headers = {}; const ah = getAuthHeader(); if (ah) Object.assign(headers, ah);
156
+ const res = await httpsRequest({ hostname: BASE_URL, path: '/api/king/auth/api-keys', method: 'GET', headers });
157
+ if (res.status !== 200) throw new Error('Failed to list keys: ' + (res.data.message || JSON.stringify(res.data)));
158
+ const keys = res.data.keys || res.data || [];
159
+ if (keys.length === 0) { console.log(chalk.dim('\n No API keys found. Create one with: blun keys create\n')); }
160
+ else {
161
+ console.log(chalk.bold.cyan('\n Your API Keys:\n'));
162
+ keys.forEach((k, i) => console.log(chalk.yellow(' ' + (i+1) + ') ') + chalk.white(k.name || 'unnamed') + chalk.dim(' - ' + (k.prefix || k.id || '***'))));
163
+ console.log('');
139
164
  }
165
+ return keys;
166
+ }
140
167
 
141
- const creds = {
142
- token: apiKey,
143
- email: res.data.email || '',
144
- name: res.data.name || '',
145
- expires_at: res.data.expires_at || '',
146
- plan: res.data.plan || 'free'
147
- };
148
- saveCredentials(creds);
149
- console.log('[BLUN Auth] API key validated and saved.');
150
- return creds;
168
+ async function whoami() {
169
+ const creds = getStoredCredentials();
170
+ if (!creds) { console.log(chalk.dim('\n Not logged in.\n')); return null; }
171
+ const headers = {}; const ah = getAuthHeader(); if (ah) Object.assign(headers, ah);
172
+ const res = await httpsRequest({ hostname: BASE_URL, path: '/api/king/auth/me', method: 'GET', headers });
173
+ if (res.status === 200) {
174
+ console.log(chalk.bold.cyan('\n Current User:'));
175
+ console.log(chalk.white(' Name: ') + (res.data.name || creds.name || '-'));
176
+ console.log(chalk.white(' Email: ') + (res.data.email || creds.email || '-'));
177
+ console.log(chalk.white(' Plan: ') + (res.data.plan || creds.plan || 'free'));
178
+ console.log(''); return res.data;
179
+ } else {
180
+ console.log(chalk.yellow('\n Stored: ') + (creds.name || creds.email || '-'));
181
+ console.log(''); return creds;
182
+ }
151
183
  }
152
184
 
153
- function logout() {
154
- try {
155
- if (fs.existsSync(CREDS_FILE)) fs.unlinkSync(CREDS_FILE);
156
- console.log('[BLUN Auth] Logged out. Credentials removed.');
157
- } catch (e) {
158
- throw new Error('Failed to logout: ' + e.message);
185
+ async function showWelcomeMenu() {
186
+ const choice = await promptMenu('Welcome to BLUN CLI!', [
187
+ { label: 'Login (email + password)', value: 'login' },
188
+ { label: 'Register new account', value: 'register' },
189
+ { label: 'Enter API Key', value: 'apikey' },
190
+ { label: 'Continue without auth (local mode)', value: 'local' },
191
+ { label: 'Exit', value: 'exit' }
192
+ ]);
193
+ switch (choice) {
194
+ case 'login': try { await login(); return true; } catch(e) { console.log(chalk.red(' ' + e.message)); return false; }
195
+ case 'register': try { await register(); return true; } catch(e) { console.log(chalk.red(' ' + e.message)); return false; }
196
+ case 'apikey': {
197
+ const key = await prompt(chalk.green(' API Key: '));
198
+ try { await loginWithApiKey(key); return true; } catch(e) { console.log(chalk.red(' ' + e.message)); return false; }
199
+ }
200
+ case 'local': console.log(chalk.yellow('\n Running in local mode (limited functionality)\n')); return 'local';
201
+ case 'exit': process.exit(0);
202
+ default: return false;
159
203
  }
160
204
  }
161
205
 
162
- module.exports = { login, loginWithApiKey, logout, getStoredCredentials, isAuthenticated, getAuthHeader, ensureAuth };
206
+ function logout() {
207
+ try { if (fs.existsSync(CREDS_FILE)) fs.unlinkSync(CREDS_FILE); console.log(chalk.green(' Logged out.')); }
208
+ catch (e) { throw new Error('Failed to logout: ' + e.message); }
209
+ }
210
+
211
+ module.exports = {
212
+ login, register, loginWithApiKey, logout,
213
+ getStoredCredentials, isAuthenticated, getAuthHeader, ensureAuth,
214
+ createApiKey, listApiKeys, whoami,
215
+ showWelcomeMenu, promptMenu, prompt,
216
+ getSettings, saveSettings
217
+ };
@@ -0,0 +1,11 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const https = require('https');
5
+ const readline = require('readline');
6
+ const chalk = require('chalk');
7
+
8
+ const CREDS_DIR = path.join(os.homedir(), '.blun');
9
+ const CREDS_FILE = path.join(CREDS_DIR, 'credentials.json');
10
+ const SETTINGS_FILE = path.join(CREDS_DIR, 'settings.json');
11
+ const BASE_URL = 'blun.ai';
package/lib/chat.js CHANGED
@@ -1,25 +1,37 @@
1
1
  const readline = require('readline');
2
+ const chalk = require('chalk');
2
3
  const ui = require('./ui');
3
4
  const auth = require('./auth');
4
5
  const client = require('./client');
5
6
 
7
+ let localMode = false;
8
+
6
9
  async function startChat() {
7
10
  ui.renderBanner();
8
11
 
9
- try {
10
- auth.ensureAuth();
11
- } catch (e) {
12
- ui.renderResponse(e.message, 'error');
13
- process.exit(1);
12
+ if (!auth.isAuthenticated()) {
13
+ const result = await auth.showWelcomeMenu();
14
+ if (result === false) {
15
+ console.log(chalk.yellow(' Auth failed. Showing menu again...\n'));
16
+ const retry = await auth.showWelcomeMenu();
17
+ if (retry === false) { process.exit(1); }
18
+ if (retry === 'local') localMode = true;
19
+ }
20
+ if (result === 'local') localMode = true;
14
21
  }
15
22
 
16
23
  const creds = auth.getStoredCredentials();
17
- ui.renderResponse('Connected as ' + (creds.name || creds.email || 'user') + ' | Plan: ' + (creds.plan || 'free'), 'system');
24
+ if (creds) {
25
+ ui.renderResponse('Connected as ' + (creds.name || creds.email || 'user') + ' | Plan: ' + (creds.plan || 'free'), 'system');
26
+ } else if (localMode) {
27
+ ui.renderResponse('Local mode - no API calls available', 'system');
28
+ }
18
29
 
30
+ const settings = auth.getSettings();
19
31
  const rl = readline.createInterface({
20
32
  input: process.stdin,
21
33
  output: process.stdout,
22
- prompt: ui.renderPrompt(creds.name || 'user', 'auto'),
34
+ prompt: ui.renderPrompt((creds ? creds.name : null) || 'local', settings.model || 'auto'),
23
35
  terminal: true
24
36
  });
25
37
 
@@ -31,47 +43,97 @@ async function startChat() {
31
43
  if (!input) { rl.prompt(); return; }
32
44
 
33
45
  if (input === '/exit' || input === '/quit') {
34
- ui.renderResponse('Goodbye!', 'system');
35
- process.exit(0);
46
+ ui.renderResponse('Goodbye!', 'system'); process.exit(0);
47
+ }
48
+ if (input === '/clear') { console.clear(); ui.renderBanner(); rl.prompt(); return; }
49
+ if (input === '/status') { ui.renderStatusBar(ui.getStats()); rl.prompt(); return; }
50
+ if (input === '/login') {
51
+ try { await auth.login(); localMode = false; ui.renderResponse('Logged in!', 'system'); }
52
+ catch(e) { ui.renderResponse(e.message, 'error'); }
53
+ rl.prompt(); return;
36
54
  }
37
- if (input === '/clear') {
38
- console.clear();
39
- ui.renderBanner();
40
- rl.prompt();
41
- return;
55
+ if (input === '/whoami') {
56
+ try { await auth.whoami(); } catch(e) { ui.renderResponse(e.message, 'error'); }
57
+ rl.prompt(); return;
42
58
  }
43
- if (input === '/status') {
44
- ui.renderStatusBar(ui.getStats());
45
- rl.prompt();
46
- return;
59
+ if (input === '/keys') {
60
+ try { await auth.listApiKeys(); } catch(e) { ui.renderResponse(e.message, 'error'); }
61
+ rl.prompt(); return;
62
+ } if (input === '/model') {
63
+ const s = auth.getSettings();
64
+ const choice = await auth.promptMenu('Select Model', [
65
+ { label: 'Auto (recommended)', value: 'auto' },
66
+ { label: 'Gemma 4', value: 'gemma4' },
67
+ { label: 'Claude', value: 'claude' },
68
+ { label: 'Llama', value: 'llama' }
69
+ ]);
70
+ if (choice) { s.model = choice; auth.saveSettings(s); ui.renderResponse('Model set to: ' + choice, 'system'); }
71
+ rl.prompt(); return;
72
+ }
73
+ if (input === '/settings') {
74
+ const s = auth.getSettings();
75
+ console.log(chalk.bold.cyan('\n Current Settings:'));
76
+ console.log(chalk.white(' Model: ') + (s.model || 'auto'));
77
+ console.log(chalk.white(' Language: ') + (s.language || 'de'));
78
+ console.log(chalk.white(' Voice: ') + (s.voice ? 'on' : 'off'));
79
+ console.log('');
80
+ const action = await auth.promptMenu('Change Setting', [
81
+ { label: 'Model', value: 'model' },
82
+ { label: 'Language (de/en)', value: 'language' },
83
+ { label: 'Voice on/off', value: 'voice' },
84
+ { label: 'Back', value: 'back' }
85
+ ]);
86
+ if (action === 'model') {
87
+ const m = await auth.promptMenu('Select Model', [
88
+ { label: 'Auto', value: 'auto' }, { label: 'Gemma 4', value: 'gemma4' },
89
+ { label: 'Claude', value: 'claude' }, { label: 'Llama', value: 'llama' }
90
+ ]);
91
+ if (m) { s.model = m; auth.saveSettings(s); ui.renderResponse('Model: ' + m, 'system'); }
92
+ } else if (action === 'language') {
93
+ const l = await auth.prompt(chalk.green(' Language (de/en): '));
94
+ if (l) { s.language = l; auth.saveSettings(s); ui.renderResponse('Language: ' + l, 'system'); }
95
+ } else if (action === 'voice') {
96
+ s.voice = !s.voice; auth.saveSettings(s); ui.renderResponse('Voice: ' + (s.voice ? 'on' : 'off'), 'system');
97
+ }
98
+ rl.prompt(); return;
47
99
  }
48
100
  if (input === '/help') {
49
101
  ui.renderBox('Chat Commands', [
50
- '/exit - Exit chat',
51
- '/clear - Clear screen',
52
- '/status - Show token usage & uptime',
53
- '/help - Show this help'
102
+ '/exit Exit chat',
103
+ '/clear Clear screen',
104
+ '/status Show token usage & uptime',
105
+ '/login Login from chat',
106
+ '/whoami Show current user info',
107
+ '/keys List API keys',
108
+ '/model Switch AI model',
109
+ '/settings Show/change settings',
110
+ '/help Show this help'
54
111
  ].join('\n'), 'cyan');
55
- rl.prompt();
56
- return;
112
+ rl.prompt(); return;
113
+ }
114
+
115
+ if (localMode) {
116
+ ui.renderResponse('Local mode - cannot send messages. Use /login to authenticate.', 'error');
117
+ rl.prompt(); return;
57
118
  }
58
119
 
59
120
  const spinner = ui.renderSpinner('Thinking...');
60
121
  try {
61
- const res = await client.sendMessage(input);
122
+ const settings = auth.getSettings();
123
+ const res = await client.sendMessage(input, { model: settings.model || 'auto' });
62
124
  ui.stopSpinner(true, 'Response received');
63
125
  if (res.status === 200 && res.data) {
64
126
  const text = res.data.response || res.data.text || res.data.message || JSON.stringify(res.data);
65
127
  ui.renderResponse(text, 'agent');
66
- if (res.data.usage) {
67
- ui.addTokens(res.data.usage.total_tokens || 0, 0.000003);
68
- }
128
+ if (res.data.usage) ui.addTokens(res.data.usage.total_tokens || 0, 0.000003);
129
+ } else if (res.status === 401 || res.status === 403) {
130
+ ui.renderResponse('Auth error. Use /login to re-authenticate.', 'error');
69
131
  } else {
70
132
  ui.renderResponse('Error: ' + JSON.stringify(res.data), 'error');
71
133
  }
72
134
  } catch (e) {
73
135
  ui.stopSpinner(false, e.message);
74
- ui.renderResponse('Request failed: ' + e.message, 'error');
136
+ ui.renderResponse('Request failed: ' + e.message + '\n Try /login if auth expired.', 'error');
75
137
  }
76
138
  rl.prompt();
77
139
  });
@@ -79,4 +141,4 @@ async function startChat() {
79
141
  rl.on('close', () => { console.log(''); process.exit(0); });
80
142
  }
81
143
 
82
- module.exports = { startChat };
144
+ module.exports = { startChat };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "6.1.0",
3
+ "version": "6.2.1",
4
4
  "description": "BLUN AI Assistant - Command Line Interface",
5
5
  "bin": { "blun": "./bin/blun.js" },
6
6
  "files": ["bin/", "lib/", "README.md"],