blun-king-cli 6.2.0 → 6.3.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/bin/blun.js CHANGED
@@ -1,35 +1,43 @@
1
1
  #!/usr/bin/env node
2
2
  const path = require("path");
3
3
  const pkg = require("../package.json");
4
+
4
5
  const args = process.argv.slice(2);
5
6
  const command = args[0] || "chat";
6
7
 
7
8
  async function main() {
8
9
  const ui = require("../lib/ui");
9
10
  const auth = require("../lib/auth");
11
+ const client = require("../lib/client");
10
12
  const workspace = require("../lib/workspace");
11
13
 
12
14
  switch (command) {
13
- case "version": case "--version": case "-v":
14
- console.log("blun-king-cli v" + pkg.version);
15
+ case "version":
16
+ case "--version":
17
+ case "-v":
18
+ console.log("@blun/cli v" + pkg.version);
15
19
  break;
16
20
 
17
- case "help": case "--help": case "-h":
21
+ case "help":
22
+ case "--help":
23
+ case "-h":
18
24
  ui.renderBanner();
19
25
  ui.renderBox("BLUN CLI Commands", [
20
- "blun Start interactive chat",
21
- "blun chat Interactive chat mode",
22
- "blun ask \"...\" One-shot question",
23
- "blun login Login with email + password",
24
- "blun register Create new account",
25
- "blun logout Remove credentials",
26
- "blun auth --api-key Authenticate with API key",
27
- "blun keys create Create new API key",
28
- "blun keys list List your API keys",
29
- "blun status System health check",
30
- "blun init Initialize BLUN project",
31
- "blun version Show version",
32
- "blun help Show this help"
26
+ "blun Start interactive chat (default)",
27
+ "blun chat Interactive chat mode",
28
+ 'blun ask \"...\" One-shot question',
29
+ "blun login Login with email + password",
30
+ "blun register Register new account",
31
+ "blun logout Remove stored credentials",
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",
37
+ "blun status Show system health",
38
+ "blun init Initialize BLUN project in current dir",
39
+ "blun version Show version",
40
+ "blun help Show this help"
33
41
  ].join("\n"), "cyan");
34
42
  break;
35
43
 
@@ -45,68 +53,121 @@ async function main() {
45
53
  auth.logout();
46
54
  break;
47
55
 
48
- case "auth":
49
- if (args[1] === "--api-key" && args[2]) {
50
- try { await auth.loginWithApiKey(args[2]); } catch (e) { ui.renderResponse(e.message, "error"); process.exit(1); }
51
- } else { console.error("Usage: blun auth --api-key <KEY>"); process.exit(1); }
56
+ case "whoami":
57
+ try { await auth.whoami(); } catch (e) { ui.renderResponse(e.message, "error"); }
52
58
  break;
53
59
 
54
60
  case "keys":
55
- if (args[1] === "create") { try { await auth.createApiKey(args[2]); } catch (e) { ui.renderResponse(e.message, "error"); } }
56
- else if (args[1] === "list") { try { await auth.listApiKeys(); } catch (e) { ui.renderResponse(e.message, "error"); } }
57
- else { console.log("Usage: blun keys create | blun keys list"); }
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
+
99
+ case "auth":
100
+ if (args[1] === "--api-key" && args[2]) {
101
+ try { await auth.loginWithApiKey(args[2]); } catch (e) { ui.renderResponse(e.message, "error"); process.exit(1); }
102
+ } else {
103
+ console.error("Usage: blun auth --api-key <KEY>");
104
+ process.exit(1);
105
+ }
58
106
  break;
59
107
 
60
108
  case "ask": {
61
109
  const question = args.slice(1).join(" ");
62
- if (!question) { console.error("Usage: blun ask \"your question\""); process.exit(1); }
63
- if (!auth.isAuthenticated()) { console.log("Not logged in. Starting auth..."); await auth.showWelcomeMenu(); }
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...");
64
115
  try {
65
- const client = require("../lib/client");
66
- const spinner = ui.renderSpinner("Thinking...");
67
116
  const res = await client.sendMessage(question);
68
117
  ui.stopSpinner(true);
69
- 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);
70
119
  ui.renderResponse(text, "agent");
71
- } catch (e) { ui.renderResponse(e.message, "error"); process.exit(1); }
120
+ } catch (e) {
121
+ ui.stopSpinner(false);
122
+ ui.renderResponse(e.message, "error");
123
+ process.exit(1);
124
+ }
72
125
  break;
73
126
  }
74
127
 
75
128
  case "status":
76
129
  try {
77
- const client = require("../lib/client");
78
130
  const spinner = ui.renderSpinner("Checking health...");
79
131
  const res = await client.getHealth();
80
132
  ui.stopSpinner(true);
81
133
  if (res.status === 200 && typeof res.data === "object") {
82
- ui.renderTable(["Metric", "Value"], Object.entries(res.data).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : String(v)]));
83
- } else { ui.renderResponse("Health: " + res.status, "error"); }
84
- } catch (e) { ui.renderResponse("Health failed: " + e.message, "error"); }
134
+ ui.renderTable(
135
+ ["Metric", "Value"],
136
+ Object.entries(res.data).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : String(v)])
137
+ );
138
+ } else {
139
+ ui.renderResponse("Health check returned: " + res.status, "error");
140
+ }
141
+ } catch (e) {
142
+ ui.renderResponse("Health check failed: " + e.message, "error");
143
+ process.exit(1);
144
+ }
85
145
  break;
86
146
 
87
147
  case "init":
88
148
  workspace.initProject(process.cwd());
89
149
  break;
90
150
 
91
- case "chat": default: {
92
- ui.renderBanner();
93
-
94
- // Auth check - show welcome menu if not authenticated
95
- if (!auth.isAuthenticated()) {
96
- await auth.showWelcomeMenu();
97
- } else {
98
- const creds = auth.getStoredCredentials();
99
- const chalk = require("chalk");
100
- console.log(chalk.green(" Logged in as " + (creds.name || creds.email) + " (" + creds.plan + ")\n"));
101
- }
102
-
103
- // Workspace trust flow
151
+ case "chat":
152
+ default: {
104
153
  let initialFolder = process.cwd();
105
154
  if (command !== "chat" && command !== undefined) {
106
- try { const fs = require("fs"); const r = path.resolve(command); if (fs.existsSync(r) && fs.statSync(r).isDirectory()) initialFolder = r; } catch {}
155
+ try {
156
+ const fs = require("fs");
157
+ const resolved = path.resolve(command);
158
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
159
+ initialFolder = resolved;
160
+ }
161
+ } catch {}
107
162
  }
108
163
  if (args[1]) {
109
- try { const fs = require("fs"); const r = path.resolve(args[1]); if (fs.existsSync(r) && fs.statSync(r).isDirectory()) initialFolder = r; } catch {}
164
+ try {
165
+ const fs = require("fs");
166
+ const resolved = path.resolve(args[1]);
167
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
168
+ initialFolder = resolved;
169
+ }
170
+ } catch {}
110
171
  }
111
172
 
112
173
  const trustedFolder = await workspace.selectFolder(initialFolder);
@@ -120,4 +181,4 @@ async function main() {
120
181
  }
121
182
  }
122
183
 
123
- 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
@@ -1,103 +1,218 @@
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");
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');
6
7
 
7
- const CREDS_DIR = path.join(os.homedir(), ".blun");
8
- const CREDS_FILE = path.join(CREDS_DIR, "credentials.json");
9
- const BASE = "blun.ai";
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';
10
12
 
11
- function ensureDir() { if (!fs.existsSync(CREDS_DIR)) fs.mkdirSync(CREDS_DIR, { mode: 0o700, recursive: true }); }
12
- function saveCredentials(c) { ensureDir(); fs.writeFileSync(CREDS_FILE, JSON.stringify(c, null, 2), { mode: 0o600 }); }
13
+ function ensureDir() {
14
+ if (!fs.existsSync(CREDS_DIR)) fs.mkdirSync(CREDS_DIR, { mode: 0o700, recursive: true });
15
+ }
16
+ function saveCredentials(creds) { ensureDir(); fs.writeFileSync(CREDS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 }); }
13
17
  function getStoredCredentials() {
14
- try { if (!fs.existsSync(CREDS_FILE)) return null; const d = JSON.parse(fs.readFileSync(CREDS_FILE, "utf8")); if (d.expires_at && new Date(d.expires_at) < new Date()) return null; return d; } catch { return null; }
18
+ try {
19
+ if (!fs.existsSync(CREDS_FILE)) return null;
20
+ const data = JSON.parse(fs.readFileSync(CREDS_FILE, 'utf8'));
21
+ if (data.expires_at && new Date(data.expires_at) < new Date()) return null;
22
+ return data;
23
+ } catch { return null; }
24
+ }
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 }; }
15
30
  }
31
+ function saveSettings(s) { ensureDir(); fs.writeFileSync(SETTINGS_FILE, JSON.stringify(s, null, 2)); }
16
32
  function isAuthenticated() { return getStoredCredentials() !== null; }
17
- function getAuthHeader() { const c = getStoredCredentials(); if (!c) return null; return c.apiKey ? { "x-blun-key": c.token } : { Authorization: "Bearer " + c.token }; }
18
- function ensureAuth() { if (!isAuthenticated()) throw new Error("Not authenticated."); return getStoredCredentials(); }
19
-
20
- function ask(q) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise(r => rl.question(q, a => { rl.close(); r(a.trim()); })); }
33
+ function getAuthHeader() {
34
+ const c = getStoredCredentials(); if (!c) return null;
35
+ if (c.apiKey) return { 'x-blun-key': c.apiKey };
36
+ return { Authorization: 'Bearer ' + c.token };
37
+ }
38
+ function ensureAuth() {
39
+ if (!isAuthenticated()) throw new Error('Not authenticated. Run "blun login" first.');
40
+ return getStoredCredentials();
41
+ }
21
42
 
22
- function httpsReq(opts, body) {
43
+ function httpsRequest(options, body) {
23
44
  return new Promise((resolve, reject) => {
24
- const req = https.request(opts, res => { let d = ""; res.on("data", c => d += c); res.on("end", () => { try { resolve({ status: res.statusCode, data: JSON.parse(d) }); } catch { resolve({ status: res.statusCode, data: d }); } }); });
25
- req.on("error", reject); if (body) req.write(JSON.stringify(body)); req.end();
45
+ const req = https.request(options, (res) => {
46
+ let data = ''; res.on('data', c => data += c);
47
+ res.on('end', () => {
48
+ try { resolve({ status: res.statusCode, data: JSON.parse(data) }); }
49
+ catch { resolve({ status: res.statusCode, data }); }
50
+ });
51
+ }); req.on('error', reject);
52
+ if (body) req.write(JSON.stringify(body)); req.end();
26
53
  });
27
54
  }
28
55
 
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
+ });
61
+ }
62
+
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);
79
+ });
80
+ }
81
+
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));
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
+ }
29
91
  async function login() {
30
- const chalk = require("chalk");
31
- console.log(chalk.cyan.bold("\n BLUN Login\n"));
32
- const email = await ask(chalk.white(" Email: "));
33
- const password = await ask(chalk.white(" Password: "));
34
- if (!email || !password) throw new Error("Email and password required.");
35
- const res = await httpsReq({ hostname: BASE, path: "/api/king/auth/login", method: "POST", headers: { "Content-Type": "application/json" } }, { email, password });
36
- if (res.status !== 200 || !res.data.token) throw new Error(res.data.error || res.data.message || "Login failed.");
37
- const creds = { token: res.data.token, email: res.data.email || email, name: res.data.name || "", plan: res.data.plan || "free" };
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
+ };
38
108
  saveCredentials(creds);
39
- console.log(chalk.green("\n Login successful! Welcome, " + (creds.name || creds.email)));
109
+ console.log(chalk.green('\n Login successful! Welcome, ' + (creds.name || creds.email)));
40
110
  return creds;
41
111
  }
42
112
 
43
113
  async function register() {
44
- const chalk = require("chalk");
45
- console.log(chalk.cyan.bold("\n BLUN Registration\n"));
46
- const name = await ask(chalk.white(" Name: "));
47
- const email = await ask(chalk.white(" Email: "));
48
- const password = await ask(chalk.white(" Password: "));
49
- if (!email || !password || !name) throw new Error("All fields required.");
50
- const res = await httpsReq({ hostname: BASE, path: "/api/king/auth/register", method: "POST", headers: { "Content-Type": "application/json" } }, { name, email, password });
51
- if (res.status !== 200 && res.status !== 201) throw new Error(res.data.error || "Registration failed.");
52
- if (res.data.token) { saveCredentials({ token: res.data.token, email, name, plan: "free" }); console.log(chalk.green("\n Account created! You are logged in.")); }
53
- else console.log(chalk.green("\n Account created! Run: blun login"));
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;
129
+ }
130
+ console.log(chalk.green('\n Account created! Please login with: blun login')); return null;
54
131
  }
55
132
 
56
133
  async function loginWithApiKey(apiKey) {
57
- const chalk = require("chalk");
58
- console.log(chalk.cyan(" Validating API key..."));
59
- const res = await httpsReq({ hostname: BASE, path: "/api/king/auth/me", method: "GET", headers: { "x-blun-key": apiKey } });
60
- if (res.status !== 200) throw new Error("Invalid API key.");
61
- saveCredentials({ token: apiKey, apiKey: true, email: res.data.email || "", name: res.data.name || "", plan: res.data.plan || "free" });
62
- console.log(chalk.green(" API key saved!"));
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;
63
139
  }
64
140
 
65
- async function createApiKey(keyName) {
66
- const chalk = require("chalk"); const c = ensureAuth();
67
- const name = keyName || await ask(chalk.white(" Key name: "));
68
- const h = c.apiKey ? { "x-blun-key": c.token, "Content-Type": "application/json" } : { Authorization: "Bearer " + c.token, "Content-Type": "application/json" };
69
- const res = await httpsReq({ hostname: BASE, path: "/api/king/auth/api-keys", method: "POST", headers: h }, { name });
70
- if (res.status !== 200 && res.status !== 201) throw new Error(res.data.error || "Failed.");
71
- console.log(chalk.green("\n API Key created:")); console.log(chalk.yellow(" " + (res.data.api_key || res.data.apiKey || res.data.key)));
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;
72
152
  }
73
-
74
153
  async function listApiKeys() {
75
- const chalk = require("chalk"); const c = ensureAuth();
76
- const h = c.apiKey ? { "x-blun-key": c.token } : { Authorization: "Bearer " + c.token };
77
- const res = await httpsReq({ hostname: BASE, path: "/api/king/auth/api-keys", method: "GET", headers: h });
78
- if (res.status !== 200) throw new Error("Failed.");
79
- const keys = res.data.api_keys || [];
80
- if (!keys.length) console.log(chalk.dim("\n No API keys. Create: blun keys create"));
81
- else { console.log(chalk.cyan.bold("\n Your API Keys:")); keys.forEach((k, i) => console.log(" " + (i+1) + ". " + (k.name || k.id || "unnamed"))); }
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('');
164
+ }
165
+ return keys;
166
+ }
167
+
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
+ }
82
183
  }
83
184
 
84
185
  async function showWelcomeMenu() {
85
186
  const chalk = require("chalk");
86
- console.log(chalk.cyan.bold("\n Welcome to BLUN CLI!\n"));
87
- console.log(chalk.white(" 1. Login (email + password)"));
88
- console.log(chalk.white(" 2. Register new account"));
89
- console.log(chalk.white(" 3. Enter API Key"));
90
- console.log(chalk.white(" 4. Continue without auth (local mode)"));
91
- console.log(chalk.white(" 5. Exit\n"));
92
- const choice = await ask(chalk.cyan(" Choice [1-5]: "));
93
- if (choice === "1") { await login(); return true; }
94
- if (choice === "2") { await register(); return true; }
95
- if (choice === "3") { const key = await ask(chalk.white(" API Key: ")); await loginWithApiKey(key); return true; }
96
- if (choice === "4") { console.log(chalk.yellow(" Running in local mode (limited features).")); return true; }
97
- if (choice === "5") { process.exit(0); }
98
- console.log(chalk.red(" Invalid choice.")); return await showWelcomeMenu();
187
+ const { action } = await inquirer.prompt([{
188
+ type: "list",
189
+ name: "action",
190
+ message: chalk.cyan("Welcome to BLUN CLI"),
191
+ choices: [
192
+ { name: "Login (email + password)", value: "login" },
193
+ { name: "Register new account", value: "register" },
194
+ { name: "Enter API Key", value: "apikey" },
195
+ { name: "Continue without auth (local mode)", value: "local" },
196
+ new inquirer.Separator(),
197
+ { name: "Exit", value: "exit" }
198
+ ]
199
+ }]);
200
+ if (action === "login") { await login(); return true; }
201
+ if (action === "register") { await register(); return true; }
202
+ if (action === "apikey") { const { key } = await inquirer.prompt([{ type: "password", name: "key", message: "API Key:" }]); await loginWithApiKey(key); return true; }
203
+ if (action === "local") { console.log(chalk.yellow(" Running in local mode.")); return true; }
204
+ if (action === "exit") { process.exit(0); }
99
205
  }
100
206
 
101
- function logout() { try { if (fs.existsSync(CREDS_FILE)) fs.unlinkSync(CREDS_FILE); console.log(" Logged out."); } catch (e) { throw new Error("Logout failed: " + e.message); } }
207
+ function logout() {
208
+ try { if (fs.existsSync(CREDS_FILE)) fs.unlinkSync(CREDS_FILE); console.log(chalk.green(' Logged out.')); }
209
+ catch (e) { throw new Error('Failed to logout: ' + e.message); }
210
+ }
102
211
 
103
- module.exports = { login, register, loginWithApiKey, logout, createApiKey, listApiKeys, getStoredCredentials, isAuthenticated, getAuthHeader, ensureAuth, showWelcomeMenu };
212
+ module.exports = {
213
+ login, register, loginWithApiKey, logout,
214
+ getStoredCredentials, isAuthenticated, getAuthHeader, ensureAuth,
215
+ createApiKey, listApiKeys, whoami,
216
+ showWelcomeMenu, promptMenu, prompt,
217
+ getSettings, saveSettings
218
+ };
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/lib/workspace.js CHANGED
@@ -1,3 +1,4 @@
1
+ const inquirer = require("inquirer");
1
2
  const fs = require("fs");
2
3
  const path = require("path");
3
4
  const os = require("os");
@@ -40,47 +41,38 @@ function prompt(question) {
40
41
  }
41
42
 
42
43
  async function selectFolder(initialFolder) {
43
- let folder = path.resolve(initialFolder);
44
-
45
- while (true) {
46
- console.log();
47
- console.log(chalk.bold.cyan(" Workspace: ") + chalk.white(folder));
48
- console.log();
49
-
50
- if (isTrusted(folder)) {
51
- console.log(chalk.green(" ✓ Working in " + folder + " (trusted)"));
52
- return folder;
53
- }
54
-
55
- console.log(chalk.yellow(" Do you trust the files in this folder?"));
56
- console.log();
57
- console.log(" " + chalk.bold("1)") + " Yes, I trust this folder");
58
- console.log(" " + chalk.bold("2)") + " No, exit");
59
- console.log(" " + chalk.bold("3)") + " Choose a different folder");
60
- console.log();
61
-
62
- const answer = await prompt(chalk.cyan(" Your choice (1/2/3): "));
44
+ const chalk = require("chalk");
45
+ const fs = require("fs");
46
+ const folder = initialFolder || process.cwd();
47
+
48
+ if (isTrusted(folder)) {
49
+ console.log(chalk.green(" Working in " + folder + " (trusted)"));
50
+ return folder;
51
+ }
63
52
 
64
- if (answer === "1") {
65
- trustFolder(folder);
66
- console.log(chalk.green("\n Folder trusted and saved.\n"));
67
- return folder;
68
- } else if (answer === "2") {
69
- console.log(chalk.red("\n Exiting.\n"));
70
- process.exit(0);
71
- } else if (answer === "3") {
72
- const newPath = await prompt(chalk.cyan(" Enter folder path: "));
73
- if (!newPath) continue;
74
- const resolved = path.resolve(newPath);
75
- if (!fs.existsSync(resolved)) {
76
- console.log(chalk.red(" Folder does not exist: " + resolved));
77
- continue;
78
- }
79
- folder = resolved;
80
- } else {
81
- console.log(chalk.red(" Invalid choice."));
82
- }
53
+ console.log(chalk.cyan(" Workspace: ") + chalk.white(folder));
54
+
55
+ const { trust } = await inquirer.prompt([{
56
+ type: "list",
57
+ name: "trust",
58
+ message: chalk.yellow("Do you trust the files in this folder?"),
59
+ choices: [
60
+ { name: "Yes, I trust this folder", value: "yes" },
61
+ { name: "No, exit", value: "no" },
62
+ { name: "Choose a different folder", value: "choose" }
63
+ ]
64
+ }]);
65
+
66
+ if (trust === "no") { process.exit(0); }
67
+ if (trust === "choose") {
68
+ const { newFolder } = await inquirer.prompt([{ type: "input", name: "newFolder", message: "Folder path:" }]);
69
+ const resolved = require("path").resolve(newFolder);
70
+ if (!fs.existsSync(resolved)) { console.log(chalk.red(" Folder not found.")); process.exit(1); }
71
+ return selectFolder(resolved);
83
72
  }
73
+ trustFolder(folder);
74
+ console.log(chalk.green(" Folder trusted."));
75
+ return folder;
84
76
  }
85
77
 
86
78
  function loadProjectConfig(folder) {
package/package.json CHANGED
@@ -1,19 +1,33 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "6.2.0",
3
+ "version": "6.3.0",
4
4
  "description": "BLUN AI Assistant - Command Line Interface",
5
- "bin": { "blun": "./bin/blun.js" },
6
- "files": ["bin/", "lib/", "README.md"],
7
- "keywords": ["ai", "cli", "blun", "assistant"],
5
+ "bin": {
6
+ "blun": "./bin/blun.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "lib/",
11
+ "README.md"
12
+ ],
13
+ "keywords": [
14
+ "ai",
15
+ "cli",
16
+ "blun",
17
+ "assistant"
18
+ ],
8
19
  "author": "BLUN AI <info@blun.ai>",
9
20
  "license": "MIT",
10
21
  "dependencies": {
22
+ "boxen": "^5.1.2",
11
23
  "chalk": "^4.1.2",
12
- "ora": "^5.4.1",
13
24
  "cli-table3": "^0.6.3",
14
- "boxen": "^5.1.2",
25
+ "figlet": "^1.7.0",
15
26
  "gradient-string": "^2.0.2",
16
- "figlet": "^1.7.0"
27
+ "inquirer": "^8.2.7",
28
+ "ora": "^5.4.1"
17
29
  },
18
- "engines": { "node": ">=18" }
30
+ "engines": {
31
+ "node": ">=18"
32
+ }
19
33
  }
package/lib/auth_test.js DELETED
@@ -1,11 +0,0 @@
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';