blun-king-cli 6.2.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 +110 -49
- package/lib/auth.js +188 -74
- package/lib/chat.js +92 -30
- package/package.json +1 -1
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":
|
|
14
|
-
|
|
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":
|
|
21
|
+
case "help":
|
|
22
|
+
case "--help":
|
|
23
|
+
case "-h":
|
|
18
24
|
ui.renderBanner();
|
|
19
25
|
ui.renderBox("BLUN CLI Commands", [
|
|
20
|
-
"blun
|
|
21
|
-
"blun chat
|
|
22
|
-
|
|
23
|
-
"blun login
|
|
24
|
-
"blun register
|
|
25
|
-
"blun logout
|
|
26
|
-
"blun auth --api-key
|
|
27
|
-
"blun keys
|
|
28
|
-
"blun keys
|
|
29
|
-
"blun
|
|
30
|
-
"blun
|
|
31
|
-
"blun
|
|
32
|
-
"blun
|
|
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 "
|
|
49
|
-
|
|
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") {
|
|
56
|
-
|
|
57
|
-
else
|
|
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(
|
|
63
|
-
if (!auth.isAuthenticated()) {
|
|
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
|
|
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) {
|
|
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(
|
|
83
|
-
|
|
84
|
-
|
|
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":
|
|
92
|
-
|
|
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 {
|
|
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 {
|
|
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,217 @@
|
|
|
1
|
-
const fs = require(
|
|
2
|
-
const path = require(
|
|
3
|
-
const os = require(
|
|
4
|
-
const https = require(
|
|
5
|
-
const readline = require(
|
|
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(),
|
|
8
|
-
const CREDS_FILE = path.join(CREDS_DIR,
|
|
9
|
-
const
|
|
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() {
|
|
12
|
-
|
|
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 {
|
|
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() {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
43
|
+
function httpsRequest(options, body) {
|
|
23
44
|
return new Promise((resolve, reject) => {
|
|
24
|
-
const req = https.request(
|
|
25
|
-
|
|
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();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
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()); });
|
|
26
60
|
});
|
|
27
61
|
}
|
|
28
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
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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(
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
saveCredentials(
|
|
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(
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
76
|
-
const
|
|
77
|
-
const res = await
|
|
78
|
-
if (res.status !== 200) throw new Error(
|
|
79
|
-
const keys = res.data.
|
|
80
|
-
if (
|
|
81
|
-
else {
|
|
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
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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;
|
|
203
|
+
}
|
|
99
204
|
}
|
|
100
205
|
|
|
101
|
-
function logout() {
|
|
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
|
+
}
|
|
102
210
|
|
|
103
|
-
module.exports = {
|
|
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/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 };
|