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 +110 -49
- package/lib/auth.js +188 -73
- package/lib/chat.js +92 -30
- package/lib/workspace.js +31 -39
- package/package.json +22 -8
- package/lib/auth_test.js +0 -11
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,218 @@
|
|
|
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();
|
|
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
|
-
|
|
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
186
|
const chalk = require("chalk");
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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() {
|
|
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 = {
|
|
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
|
-
|
|
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 };
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
console.log();
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"description": "BLUN AI Assistant - Command Line Interface",
|
|
5
|
-
"bin": {
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
"
|
|
25
|
+
"figlet": "^1.7.0",
|
|
15
26
|
"gradient-string": "^2.0.2",
|
|
16
|
-
"
|
|
27
|
+
"inquirer": "^8.2.7",
|
|
28
|
+
"ora": "^5.4.1"
|
|
17
29
|
},
|
|
18
|
-
"engines": {
|
|
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';
|