blun-king-cli 6.1.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/blun.js CHANGED
@@ -1,38 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
  const path = require("path");
3
3
  const pkg = require("../package.json");
4
-
5
4
  const args = process.argv.slice(2);
6
5
  const command = args[0] || "chat";
7
6
 
8
7
  async function main() {
9
8
  const ui = require("../lib/ui");
10
9
  const auth = require("../lib/auth");
11
- const client = require("../lib/client");
12
10
  const workspace = require("../lib/workspace");
13
11
 
14
12
  switch (command) {
15
- case "version":
16
- case "--version":
17
- case "-v":
18
- console.log("@blun/cli v" + pkg.version);
13
+ case "version": case "--version": case "-v":
14
+ console.log("blun-king-cli v" + pkg.version);
19
15
  break;
20
16
 
21
- case "help":
22
- case "--help":
23
- case "-h":
17
+ case "help": case "--help": case "-h":
24
18
  ui.renderBanner();
25
19
  ui.renderBox("BLUN CLI Commands", [
26
- "blun Start interactive chat (default)",
27
- "blun chat Interactive chat mode",
28
- "blun ask \"...\" One-shot question",
29
- "blun login Device login flow",
30
- "blun logout Remove stored credentials",
31
- "blun auth --api-key KEY Authenticate with API key",
32
- "blun status Show system health",
33
- "blun init Initialize BLUN project in current dir",
34
- "blun version Show version",
35
- "blun help Show this help"
20
+ "blun Start interactive chat",
21
+ "blun chat Interactive chat mode",
22
+ "blun ask \"...\" One-shot question",
23
+ "blun login Login with email + password",
24
+ "blun register Create new account",
25
+ "blun logout Remove credentials",
26
+ "blun auth --api-key Authenticate with API key",
27
+ "blun keys create Create new API key",
28
+ "blun keys list List your API keys",
29
+ "blun status System health check",
30
+ "blun init Initialize BLUN project",
31
+ "blun version Show version",
32
+ "blun help Show this help"
36
33
  ].join("\n"), "cyan");
37
34
  break;
38
35
 
@@ -40,6 +37,10 @@ async function main() {
40
37
  try { await auth.login(); } catch (e) { ui.renderResponse(e.message, "error"); process.exit(1); }
41
38
  break;
42
39
 
40
+ case "register":
41
+ try { await auth.register(); } catch (e) { ui.renderResponse(e.message, "error"); process.exit(1); }
42
+ break;
43
+
43
44
  case "logout":
44
45
  auth.logout();
45
46
  break;
@@ -47,79 +48,67 @@ async function main() {
47
48
  case "auth":
48
49
  if (args[1] === "--api-key" && args[2]) {
49
50
  try { await auth.loginWithApiKey(args[2]); } catch (e) { ui.renderResponse(e.message, "error"); process.exit(1); }
50
- } else {
51
- console.error("Usage: blun auth --api-key <KEY>");
52
- process.exit(1);
53
- }
51
+ } else { console.error("Usage: blun auth --api-key <KEY>"); process.exit(1); }
52
+ break;
53
+
54
+ case "keys":
55
+ if (args[1] === "create") { try { await auth.createApiKey(args[2]); } catch (e) { ui.renderResponse(e.message, "error"); } }
56
+ else if (args[1] === "list") { try { await auth.listApiKeys(); } catch (e) { ui.renderResponse(e.message, "error"); } }
57
+ else { console.log("Usage: blun keys create | blun keys list"); }
54
58
  break;
55
59
 
56
60
  case "ask": {
57
61
  const question = args.slice(1).join(" ");
58
62
  if (!question) { console.error("Usage: blun ask \"your question\""); process.exit(1); }
63
+ if (!auth.isAuthenticated()) { console.log("Not logged in. Starting auth..."); await auth.showWelcomeMenu(); }
59
64
  try {
60
- auth.ensureAuth();
65
+ const client = require("../lib/client");
61
66
  const spinner = ui.renderSpinner("Thinking...");
62
67
  const res = await client.sendMessage(question);
63
68
  ui.stopSpinner(true);
64
69
  const text = res.data?.response || res.data?.text || res.data?.message || JSON.stringify(res.data);
65
70
  ui.renderResponse(text, "agent");
66
- } catch (e) {
67
- ui.renderResponse(e.message, "error");
68
- process.exit(1);
69
- }
71
+ } catch (e) { ui.renderResponse(e.message, "error"); process.exit(1); }
70
72
  break;
71
73
  }
72
74
 
73
75
  case "status":
74
76
  try {
77
+ const client = require("../lib/client");
75
78
  const spinner = ui.renderSpinner("Checking health...");
76
79
  const res = await client.getHealth();
77
80
  ui.stopSpinner(true);
78
81
  if (res.status === 200 && typeof res.data === "object") {
79
- const d = res.data;
80
- ui.renderTable(
81
- ["Metric", "Value"],
82
- Object.entries(d).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : String(v)])
83
- );
84
- } else {
85
- ui.renderResponse("Health check returned: " + res.status, "error");
86
- }
87
- } catch (e) {
88
- ui.renderResponse("Health check failed: " + e.message, "error");
89
- process.exit(1);
90
- }
82
+ ui.renderTable(["Metric", "Value"], Object.entries(res.data).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : String(v)]));
83
+ } else { ui.renderResponse("Health: " + res.status, "error"); }
84
+ } catch (e) { ui.renderResponse("Health failed: " + e.message, "error"); }
91
85
  break;
92
86
 
93
87
  case "init":
94
88
  workspace.initProject(process.cwd());
95
89
  break;
96
90
 
97
- case "chat":
98
- default: {
99
- // Determine initial folder: path arg or cwd
91
+ case "chat": default: {
92
+ ui.renderBanner();
93
+
94
+ // Auth check - show welcome menu if not authenticated
95
+ if (!auth.isAuthenticated()) {
96
+ await auth.showWelcomeMenu();
97
+ } else {
98
+ const creds = auth.getStoredCredentials();
99
+ const chalk = require("chalk");
100
+ console.log(chalk.green(" Logged in as " + (creds.name || creds.email) + " (" + creds.plan + ")\n"));
101
+ }
102
+
103
+ // Workspace trust flow
100
104
  let initialFolder = process.cwd();
101
105
  if (command !== "chat" && command !== undefined) {
102
- // default case command might be a path
103
- try {
104
- const fs = require("fs");
105
- const resolved = path.resolve(command);
106
- if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
107
- initialFolder = resolved;
108
- }
109
- } catch {}
106
+ try { const fs = require("fs"); const r = path.resolve(command); if (fs.existsSync(r) && fs.statSync(r).isDirectory()) initialFolder = r; } catch {}
110
107
  }
111
- // Also check args[1] for "blun chat /path"
112
108
  if (args[1]) {
113
- try {
114
- const fs = require("fs");
115
- const resolved = path.resolve(args[1]);
116
- if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
117
- initialFolder = resolved;
118
- }
119
- } catch {}
109
+ try { const fs = require("fs"); const r = path.resolve(args[1]); if (fs.existsSync(r) && fs.statSync(r).isDirectory()) initialFolder = r; } catch {}
120
110
  }
121
111
 
122
- // Workspace trust flow
123
112
  const trustedFolder = await workspace.selectFolder(initialFolder);
124
113
  process.chdir(trustedFolder);
125
114
  workspace.loadProjectConfig(trustedFolder);
@@ -131,4 +120,4 @@ async function main() {
131
120
  }
132
121
  }
133
122
 
134
- main().catch((e) => { console.error("Fatal:", e.message); process.exit(1); });
123
+ main().catch(e => { console.error("Fatal:", e.message); process.exit(1); });
package/lib/auth.js CHANGED
@@ -1,162 +1,103 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
- const https = require('https');
5
- const { execSync } = require('child_process');
6
-
7
- const CREDS_DIR = path.join(os.homedir(), '.blun');
8
- const CREDS_FILE = path.join(CREDS_DIR, 'credentials.json');
9
- const BASE_URL = 'blun.ai';
10
-
11
- 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 });
20
- }
21
-
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
+
7
+ const CREDS_DIR = path.join(os.homedir(), ".blun");
8
+ const CREDS_FILE = path.join(CREDS_DIR, "credentials.json");
9
+ const BASE = "blun.ai";
10
+
11
+ function ensureDir() { if (!fs.existsSync(CREDS_DIR)) fs.mkdirSync(CREDS_DIR, { mode: 0o700, recursive: true }); }
12
+ function saveCredentials(c) { ensureDir(); fs.writeFileSync(CREDS_FILE, JSON.stringify(c, null, 2), { mode: 0o600 }); }
22
13
  function getStoredCredentials() {
23
- try {
24
- if (!fs.existsSync(CREDS_FILE)) return null;
25
- 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
- }
30
- return data;
31
- } catch { return null; }
14
+ try { if (!fs.existsSync(CREDS_FILE)) return null; const d = JSON.parse(fs.readFileSync(CREDS_FILE, "utf8")); if (d.expires_at && new Date(d.expires_at) < new Date()) return null; return d; } catch { return null; }
32
15
  }
16
+ function isAuthenticated() { return getStoredCredentials() !== null; }
17
+ function getAuthHeader() { const c = getStoredCredentials(); if (!c) return null; return c.apiKey ? { "x-blun-key": c.token } : { Authorization: "Bearer " + c.token }; }
18
+ function ensureAuth() { if (!isAuthenticated()) throw new Error("Not authenticated."); return getStoredCredentials(); }
33
19
 
34
- function isAuthenticated() {
35
- return getStoredCredentials() !== null;
36
- }
20
+ function ask(q) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise(r => rl.question(q, a => { rl.close(); r(a.trim()); })); }
37
21
 
38
- function getAuthHeader() {
39
- const creds = getStoredCredentials();
40
- if (!creds) return null;
41
- return { Authorization: 'Bearer ' + creds.token };
42
- }
43
-
44
- function ensureAuth() {
45
- if (!isAuthenticated()) {
46
- throw new Error('Not authenticated. Run "blun login" or "blun auth --api-key <key>" first.');
47
- }
48
- return getStoredCredentials();
49
- }
50
-
51
- function httpsRequest(options, body) {
22
+ function httpsReq(opts, body) {
52
23
  return new Promise((resolve, reject) => {
53
- const req = https.request(options, (res) => {
54
- let data = '';
55
- res.on('data', (c) => data += c);
56
- res.on('end', () => {
57
- try { resolve({ status: res.statusCode, data: JSON.parse(data) }); }
58
- catch { resolve({ status: res.statusCode, data }); }
59
- });
60
- });
61
- req.on('error', reject);
62
- if (body) req.write(JSON.stringify(body));
63
- req.end();
24
+ const req = https.request(opts, res => { let d = ""; res.on("data", c => d += c); res.on("end", () => { try { resolve({ status: res.statusCode, data: JSON.parse(d) }); } catch { resolve({ status: res.statusCode, data: d }); } }); });
25
+ req.on("error", reject); if (body) req.write(JSON.stringify(body)); req.end();
64
26
  });
65
27
  }
66
28
 
67
- function openBrowser(url) {
68
- try {
69
- const platform = process.platform;
70
- if (platform === 'win32') execSync('start "" "' + url + '"', { stdio: 'ignore' });
71
- else if (platform === 'darwin') execSync('open "' + url + '"', { stdio: 'ignore' });
72
- else execSync('xdg-open "' + url + '"', { stdio: 'ignore' });
73
- } catch {
74
- console.log('Open this URL manually: ' + url);
75
- }
76
- }
77
-
78
29
  async function login() {
79
- console.log('[BLUN Auth] Starting device login flow...');
80
- const startRes = await httpsRequest({
81
- hostname: BASE_URL, path: '/api/king/auth/device/start',
82
- method: 'POST', headers: { 'Content-Type': 'application/json' }
83
- });
84
-
85
- if (startRes.status !== 200 || !startRes.data.code) {
86
- throw new Error('Failed to start device flow: ' + JSON.stringify(startRes.data));
87
- }
88
-
89
- const { code, verification_url } = startRes.data;
90
- const url = verification_url || 'https://blun.ai/api/king/auth/device/start';
91
-
92
- console.log('');
93
- console.log(' Enter this code in your browser: ' + code);
94
- console.log(' URL: ' + url);
95
- console.log('');
96
-
97
- openBrowser(url);
98
- console.log('Waiting for approval...');
99
-
100
- const maxAttempts = 120;
101
- for (let i = 0; i < maxAttempts; i++) {
102
- await new Promise(r => setTimeout(r, 2000));
103
- const pollRes = await httpsRequest({
104
- hostname: BASE_URL,
105
- path: '/api/king/auth/device/poll?code=' + encodeURIComponent(code),
106
- method: 'GET'
107
- });
108
-
109
- if (pollRes.status === 200 && pollRes.data.token) {
110
- const creds = {
111
- token: pollRes.data.token,
112
- email: pollRes.data.email || '',
113
- name: pollRes.data.name || '',
114
- expires_at: pollRes.data.expires_at || '',
115
- plan: pollRes.data.plan || 'free'
116
- };
117
- saveCredentials(creds);
118
- console.log('[BLUN Auth] Login successful! Welcome, ' + (creds.name || creds.email || 'user'));
119
- return creds;
120
- }
30
+ const chalk = require("chalk");
31
+ console.log(chalk.cyan.bold("\n BLUN Login\n"));
32
+ const email = await ask(chalk.white(" Email: "));
33
+ const password = await ask(chalk.white(" Password: "));
34
+ if (!email || !password) throw new Error("Email and password required.");
35
+ const res = await httpsReq({ hostname: BASE, path: "/api/king/auth/login", method: "POST", headers: { "Content-Type": "application/json" } }, { email, password });
36
+ if (res.status !== 200 || !res.data.token) throw new Error(res.data.error || res.data.message || "Login failed.");
37
+ const creds = { token: res.data.token, email: res.data.email || email, name: res.data.name || "", plan: res.data.plan || "free" };
38
+ saveCredentials(creds);
39
+ console.log(chalk.green("\n Login successful! Welcome, " + (creds.name || creds.email)));
40
+ return creds;
41
+ }
121
42
 
122
- if (pollRes.data.status === 'denied' || pollRes.data.error === 'denied') {
123
- throw new Error('Login denied by user.');
124
- }
125
- }
126
- throw new Error('Login timed out after 4 minutes.');
43
+ async function register() {
44
+ const chalk = require("chalk");
45
+ console.log(chalk.cyan.bold("\n BLUN Registration\n"));
46
+ const name = await ask(chalk.white(" Name: "));
47
+ const email = await ask(chalk.white(" Email: "));
48
+ const password = await ask(chalk.white(" Password: "));
49
+ if (!email || !password || !name) throw new Error("All fields required.");
50
+ const res = await httpsReq({ hostname: BASE, path: "/api/king/auth/register", method: "POST", headers: { "Content-Type": "application/json" } }, { name, email, password });
51
+ if (res.status !== 200 && res.status !== 201) throw new Error(res.data.error || "Registration failed.");
52
+ if (res.data.token) { saveCredentials({ token: res.data.token, email, name, plan: "free" }); console.log(chalk.green("\n Account created! You are logged in.")); }
53
+ else console.log(chalk.green("\n Account created! Run: blun login"));
127
54
  }
128
55
 
129
56
  async function loginWithApiKey(apiKey) {
130
- console.log('[BLUN Auth] Validating API key...');
131
- const res = await httpsRequest({
132
- hostname: BASE_URL, path: '/api/king/auth/validate',
133
- method: 'POST',
134
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + apiKey }
135
- });
57
+ const chalk = require("chalk");
58
+ console.log(chalk.cyan(" Validating API key..."));
59
+ const res = await httpsReq({ hostname: BASE, path: "/api/king/auth/me", method: "GET", headers: { "x-blun-key": apiKey } });
60
+ if (res.status !== 200) throw new Error("Invalid API key.");
61
+ saveCredentials({ token: apiKey, apiKey: true, email: res.data.email || "", name: res.data.name || "", plan: res.data.plan || "free" });
62
+ console.log(chalk.green(" API key saved!"));
63
+ }
136
64
 
137
- if (res.status !== 200 || !res.data.valid) {
138
- throw new Error('Invalid API key: ' + (res.data.message || 'validation failed'));
139
- }
65
+ async function createApiKey(keyName) {
66
+ const chalk = require("chalk"); const c = ensureAuth();
67
+ const name = keyName || await ask(chalk.white(" Key name: "));
68
+ const h = c.apiKey ? { "x-blun-key": c.token, "Content-Type": "application/json" } : { Authorization: "Bearer " + c.token, "Content-Type": "application/json" };
69
+ const res = await httpsReq({ hostname: BASE, path: "/api/king/auth/api-keys", method: "POST", headers: h }, { name });
70
+ if (res.status !== 200 && res.status !== 201) throw new Error(res.data.error || "Failed.");
71
+ console.log(chalk.green("\n API Key created:")); console.log(chalk.yellow(" " + (res.data.api_key || res.data.apiKey || res.data.key)));
72
+ }
140
73
 
141
- const creds = {
142
- token: apiKey,
143
- email: res.data.email || '',
144
- name: res.data.name || '',
145
- expires_at: res.data.expires_at || '',
146
- plan: res.data.plan || 'free'
147
- };
148
- saveCredentials(creds);
149
- console.log('[BLUN Auth] API key validated and saved.');
150
- return creds;
74
+ async function listApiKeys() {
75
+ const chalk = require("chalk"); const c = ensureAuth();
76
+ const h = c.apiKey ? { "x-blun-key": c.token } : { Authorization: "Bearer " + c.token };
77
+ const res = await httpsReq({ hostname: BASE, path: "/api/king/auth/api-keys", method: "GET", headers: h });
78
+ if (res.status !== 200) throw new Error("Failed.");
79
+ const keys = res.data.api_keys || [];
80
+ if (!keys.length) console.log(chalk.dim("\n No API keys. Create: blun keys create"));
81
+ else { console.log(chalk.cyan.bold("\n Your API Keys:")); keys.forEach((k, i) => console.log(" " + (i+1) + ". " + (k.name || k.id || "unnamed"))); }
151
82
  }
152
83
 
153
- function logout() {
154
- try {
155
- if (fs.existsSync(CREDS_FILE)) fs.unlinkSync(CREDS_FILE);
156
- console.log('[BLUN Auth] Logged out. Credentials removed.');
157
- } catch (e) {
158
- throw new Error('Failed to logout: ' + e.message);
159
- }
84
+ async function showWelcomeMenu() {
85
+ const chalk = require("chalk");
86
+ console.log(chalk.cyan.bold("\n Welcome to BLUN CLI!\n"));
87
+ console.log(chalk.white(" 1. Login (email + password)"));
88
+ console.log(chalk.white(" 2. Register new account"));
89
+ console.log(chalk.white(" 3. Enter API Key"));
90
+ console.log(chalk.white(" 4. Continue without auth (local mode)"));
91
+ console.log(chalk.white(" 5. Exit\n"));
92
+ const choice = await ask(chalk.cyan(" Choice [1-5]: "));
93
+ if (choice === "1") { await login(); return true; }
94
+ if (choice === "2") { await register(); return true; }
95
+ if (choice === "3") { const key = await ask(chalk.white(" API Key: ")); await loginWithApiKey(key); return true; }
96
+ if (choice === "4") { console.log(chalk.yellow(" Running in local mode (limited features).")); return true; }
97
+ if (choice === "5") { process.exit(0); }
98
+ console.log(chalk.red(" Invalid choice.")); return await showWelcomeMenu();
160
99
  }
161
100
 
162
- module.exports = { login, loginWithApiKey, logout, getStoredCredentials, isAuthenticated, getAuthHeader, ensureAuth };
101
+ function logout() { try { if (fs.existsSync(CREDS_FILE)) fs.unlinkSync(CREDS_FILE); console.log(" Logged out."); } catch (e) { throw new Error("Logout failed: " + e.message); } }
102
+
103
+ module.exports = { login, register, loginWithApiKey, logout, createApiKey, listApiKeys, getStoredCredentials, isAuthenticated, getAuthHeader, ensureAuth, showWelcomeMenu };
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "description": "BLUN AI Assistant - Command Line Interface",
5
5
  "bin": { "blun": "./bin/blun.js" },
6
6
  "files": ["bin/", "lib/", "README.md"],