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 +64 -14
- package/lib/auth.js +164 -109
- package/lib/auth_test.js +11 -0
- package/lib/chat.js +92 -30
- package/package.json +1 -1
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
|
-
|
|
29
|
-
"blun login
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
40
|
-
if (
|
|
41
|
-
return { Authorization: 'Bearer ' +
|
|
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.
|
|
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
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
98
|
-
console.log('
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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('
|
|
131
|
-
const res = await httpsRequest({
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/lib/auth_test.js
ADDED
|
@@ -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
|
-
|
|
10
|
-
auth.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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 || '
|
|
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
|
-
|
|
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 === '/
|
|
38
|
-
|
|
39
|
-
|
|
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 === '/
|
|
44
|
-
|
|
45
|
-
rl.prompt();
|
|
46
|
-
|
|
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
|
|
51
|
-
'/clear
|
|
52
|
-
'/status
|
|
53
|
-
'/
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 };
|