blun-king-cli 5.3.7 → 6.0.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 +100 -0
- package/lib/auth.js +162 -0
- package/lib/chat.js +82 -0
- package/lib/client.js +65 -0
- package/lib/ui.js +215 -0
- package/package.json +14 -18
- package/api.js +0 -965
- package/blun-cli.js +0 -3534
- package/blunking-api.js +0 -7
- package/bot.js +0 -188
- package/browser-controller.js +0 -102
- package/chat-memory.js +0 -103
- package/file-helper.js +0 -63
- package/fuzzy-match.js +0 -78
- package/identities.js +0 -106
- package/installer.js +0 -160
- package/job-manager.js +0 -146
- package/local-data.js +0 -71
- package/message-builder.js +0 -28
- package/noisy-evals.js +0 -38
- package/palace-memory.js +0 -246
- package/reference-inspector.js +0 -256
- package/runtime.js +0 -569
- package/task-executor.js +0 -104
- package/tests/browser-controller.test.js +0 -47
- package/tests/cli.test.js +0 -93
- package/tests/file-helper.test.js +0 -18
- package/tests/installer.test.js +0 -39
- package/tests/job-manager.test.js +0 -99
- package/tests/merge-compat.test.js +0 -77
- package/tests/messages.test.js +0 -23
- package/tests/noisy-evals.test.js +0 -12
- package/tests/noisy-intent-corpus.test.js +0 -45
- package/tests/reference-inspector.test.js +0 -42
- package/tests/runtime.test.js +0 -119
- package/tests/task-executor.test.js +0 -40
- package/tests/tools.test.js +0 -23
- package/tests/user-profile.test.js +0 -66
- package/tests/website-builder.test.js +0 -66
- package/tmp-build-smoke/nicrazy-landing/index.html +0 -53
- package/tmp-build-smoke/nicrazy-landing/style.css +0 -110
- package/tmp-shot-smoke/website-shot-1776006760424.png +0 -0
- package/tmp-shot-smoke/website-shot-1776007850007.png +0 -0
- package/tmp-shot-smoke/website-shot-1776007886209.png +0 -0
- package/tmp-shot-smoke/website-shot-1776007903766.png +0 -0
- package/tmp-shot-smoke/website-shot-1776008737117.png +0 -0
- package/tmp-shot-smoke/website-shot-1776008988859.png +0 -0
- package/tmp-smoke/nicrazy-landing/index.html +0 -66
- package/tmp-smoke/nicrazy-landing/style.css +0 -104
- package/tools.js +0 -177
- package/user-profile.js +0 -395
- package/website-builder.js +0 -394
- package/website-shot-1776010648230.png +0 -0
- package/website_builder.txt +0 -38
package/bin/blun.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const pkg = require('../package.json');
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const command = args[0] || 'chat';
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const ui = require('../lib/ui');
|
|
10
|
+
const auth = require('../lib/auth');
|
|
11
|
+
const client = require('../lib/client');
|
|
12
|
+
|
|
13
|
+
switch (command) {
|
|
14
|
+
case 'version':
|
|
15
|
+
case '--version':
|
|
16
|
+
case '-v':
|
|
17
|
+
console.log('@blun/cli v' + pkg.version);
|
|
18
|
+
break;
|
|
19
|
+
|
|
20
|
+
case 'help':
|
|
21
|
+
case '--help':
|
|
22
|
+
case '-h':
|
|
23
|
+
ui.renderBanner();
|
|
24
|
+
ui.renderBox('BLUN CLI Commands', [
|
|
25
|
+
'blun Start interactive chat (default)',
|
|
26
|
+
'blun chat Interactive chat mode',
|
|
27
|
+
'blun ask "..." One-shot question',
|
|
28
|
+
'blun login Device login flow',
|
|
29
|
+
'blun logout Remove stored credentials',
|
|
30
|
+
'blun auth --api-key KEY Authenticate with API key',
|
|
31
|
+
'blun status Show system health',
|
|
32
|
+
'blun version Show version',
|
|
33
|
+
'blun help Show this help'
|
|
34
|
+
].join('\n'), 'cyan');
|
|
35
|
+
break;
|
|
36
|
+
|
|
37
|
+
case 'login':
|
|
38
|
+
try { await auth.login(); } catch (e) { ui.renderResponse(e.message, 'error'); process.exit(1); }
|
|
39
|
+
break;
|
|
40
|
+
|
|
41
|
+
case 'logout':
|
|
42
|
+
auth.logout();
|
|
43
|
+
break;
|
|
44
|
+
|
|
45
|
+
case 'auth':
|
|
46
|
+
if (args[1] === '--api-key' && args[2]) {
|
|
47
|
+
try { await auth.loginWithApiKey(args[2]); } catch (e) { ui.renderResponse(e.message, 'error'); process.exit(1); }
|
|
48
|
+
} else {
|
|
49
|
+
console.error('Usage: blun auth --api-key <KEY>');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
|
|
54
|
+
case 'ask': {
|
|
55
|
+
const question = args.slice(1).join(' ');
|
|
56
|
+
if (!question) { console.error('Usage: blun ask "your question"'); process.exit(1); }
|
|
57
|
+
try {
|
|
58
|
+
auth.ensureAuth();
|
|
59
|
+
const spinner = ui.renderSpinner('Thinking...');
|
|
60
|
+
const res = await client.sendMessage(question);
|
|
61
|
+
ui.stopSpinner(true);
|
|
62
|
+
const text = res.data?.response || res.data?.text || res.data?.message || JSON.stringify(res.data);
|
|
63
|
+
ui.renderResponse(text, 'agent');
|
|
64
|
+
} catch (e) {
|
|
65
|
+
ui.renderResponse(e.message, 'error');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case 'status':
|
|
72
|
+
try {
|
|
73
|
+
const spinner = ui.renderSpinner('Checking health...');
|
|
74
|
+
const res = await client.getHealth();
|
|
75
|
+
ui.stopSpinner(true);
|
|
76
|
+
if (res.status === 200 && typeof res.data === 'object') {
|
|
77
|
+
const d = res.data;
|
|
78
|
+
ui.renderTable(
|
|
79
|
+
['Metric', 'Value'],
|
|
80
|
+
Object.entries(d).map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v) : String(v)])
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
ui.renderResponse('Health check returned: ' + res.status, 'error');
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
ui.renderResponse('Health check failed: ' + e.message, 'error');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
|
|
91
|
+
case 'chat':
|
|
92
|
+
default: {
|
|
93
|
+
const { startChat } = require('../lib/chat');
|
|
94
|
+
await startChat();
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
main().catch((e) => { console.error('Fatal:', e.message); process.exit(1); });
|
package/lib/auth.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
|
|
22
|
+
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; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isAuthenticated() {
|
|
35
|
+
return getStoredCredentials() !== null;
|
|
36
|
+
}
|
|
37
|
+
|
|
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) {
|
|
52
|
+
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();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
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
|
+
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
|
+
}
|
|
121
|
+
|
|
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.');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
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
|
+
});
|
|
136
|
+
|
|
137
|
+
if (res.status !== 200 || !res.data.valid) {
|
|
138
|
+
throw new Error('Invalid API key: ' + (res.data.message || 'validation failed'));
|
|
139
|
+
}
|
|
140
|
+
|
|
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;
|
|
151
|
+
}
|
|
152
|
+
|
|
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
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = { login, loginWithApiKey, logout, getStoredCredentials, isAuthenticated, getAuthHeader, ensureAuth };
|
package/lib/chat.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const ui = require('./ui');
|
|
3
|
+
const auth = require('./auth');
|
|
4
|
+
const client = require('./client');
|
|
5
|
+
|
|
6
|
+
async function startChat() {
|
|
7
|
+
ui.renderBanner();
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
auth.ensureAuth();
|
|
11
|
+
} catch (e) {
|
|
12
|
+
ui.renderResponse(e.message, 'error');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const creds = auth.getStoredCredentials();
|
|
17
|
+
ui.renderResponse('Connected as ' + (creds.name || creds.email || 'user') + ' | Plan: ' + (creds.plan || 'free'), 'system');
|
|
18
|
+
|
|
19
|
+
const rl = readline.createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout,
|
|
22
|
+
prompt: ui.renderPrompt(creds.name || 'user', 'auto'),
|
|
23
|
+
terminal: true
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.log(' Type /help for commands, /exit to quit.\n');
|
|
27
|
+
rl.prompt();
|
|
28
|
+
|
|
29
|
+
rl.on('line', async (line) => {
|
|
30
|
+
const input = line.trim();
|
|
31
|
+
if (!input) { rl.prompt(); return; }
|
|
32
|
+
|
|
33
|
+
if (input === '/exit' || input === '/quit') {
|
|
34
|
+
ui.renderResponse('Goodbye!', 'system');
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
if (input === '/clear') {
|
|
38
|
+
console.clear();
|
|
39
|
+
ui.renderBanner();
|
|
40
|
+
rl.prompt();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (input === '/status') {
|
|
44
|
+
ui.renderStatusBar(ui.getStats());
|
|
45
|
+
rl.prompt();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (input === '/help') {
|
|
49
|
+
ui.renderBox('Chat Commands', [
|
|
50
|
+
'/exit - Exit chat',
|
|
51
|
+
'/clear - Clear screen',
|
|
52
|
+
'/status - Show token usage & uptime',
|
|
53
|
+
'/help - Show this help'
|
|
54
|
+
].join('\n'), 'cyan');
|
|
55
|
+
rl.prompt();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const spinner = ui.renderSpinner('Thinking...');
|
|
60
|
+
try {
|
|
61
|
+
const res = await client.sendMessage(input);
|
|
62
|
+
ui.stopSpinner(true, 'Response received');
|
|
63
|
+
if (res.status === 200 && res.data) {
|
|
64
|
+
const text = res.data.response || res.data.text || res.data.message || JSON.stringify(res.data);
|
|
65
|
+
ui.renderResponse(text, 'agent');
|
|
66
|
+
if (res.data.usage) {
|
|
67
|
+
ui.addTokens(res.data.usage.total_tokens || 0, 0.000003);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
ui.renderResponse('Error: ' + JSON.stringify(res.data), 'error');
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
ui.stopSpinner(false, e.message);
|
|
74
|
+
ui.renderResponse('Request failed: ' + e.message, 'error');
|
|
75
|
+
}
|
|
76
|
+
rl.prompt();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
rl.on('close', () => { console.log(''); process.exit(0); });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { startChat };
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const auth = require('./auth');
|
|
3
|
+
|
|
4
|
+
const BASE_HOST = 'blun.ai';
|
|
5
|
+
|
|
6
|
+
function request(method, urlPath, body) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const authHeader = auth.getAuthHeader() || {};
|
|
9
|
+
const headers = { 'Content-Type': 'application/json', ...authHeader };
|
|
10
|
+
const req = https.request({ hostname: BASE_HOST, path: urlPath, method, headers }, (res) => {
|
|
11
|
+
let data = '';
|
|
12
|
+
res.on('data', (c) => data += c);
|
|
13
|
+
res.on('end', () => {
|
|
14
|
+
try { resolve({ status: res.statusCode, data: JSON.parse(data) }); }
|
|
15
|
+
catch { resolve({ status: res.statusCode, data }); }
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
req.on('error', reject);
|
|
19
|
+
if (body) req.write(JSON.stringify(body));
|
|
20
|
+
req.end();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function sendMessage(message, options) {
|
|
25
|
+
options = options || {};
|
|
26
|
+
const body = { message, model: options.model || 'auto', stream: false };
|
|
27
|
+
return request('POST', '/api/king/chat', body);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function sendMessageStream(message, options) {
|
|
31
|
+
options = options || {};
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const body = JSON.stringify({ message, model: options.model || 'auto', stream: true });
|
|
34
|
+
const authHeader = auth.getAuthHeader() || {};
|
|
35
|
+
const headers = { 'Content-Type': 'application/json', ...authHeader };
|
|
36
|
+
const req = https.request({ hostname: BASE_HOST, path: '/api/king/chat', method: 'POST', headers }, (res) => {
|
|
37
|
+
let fullText = '';
|
|
38
|
+
res.on('data', (chunk) => {
|
|
39
|
+
const lines = chunk.toString().split('\n');
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
if (line.startsWith('data: ')) {
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(line.slice(6));
|
|
44
|
+
if (parsed.text) {
|
|
45
|
+
fullText += parsed.text;
|
|
46
|
+
if (options.onChunk) options.onChunk(parsed.text);
|
|
47
|
+
}
|
|
48
|
+
if (parsed.usage && options.onUsage) options.onUsage(parsed.usage);
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
res.on('end', () => resolve({ text: fullText }));
|
|
54
|
+
});
|
|
55
|
+
req.on('error', reject);
|
|
56
|
+
req.write(body);
|
|
57
|
+
req.end();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function getHealth() {
|
|
62
|
+
return request('GET', '/api/monitoring/health');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { sendMessage, sendMessageStream, getHealth };
|
package/lib/ui.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BLUN King CLI - Rich Terminal UI Module
|
|
3
|
+
* Task 11: Claude Code-style terminal experience
|
|
4
|
+
* CommonJS - chalk@4, ora@5, boxen@5
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
var chalk = require("chalk");
|
|
8
|
+
var ora = require("ora");
|
|
9
|
+
var Table = require("cli-table3");
|
|
10
|
+
var boxen = require("boxen");
|
|
11
|
+
var gradient = require("gradient-string");
|
|
12
|
+
var figlet = require("figlet");
|
|
13
|
+
|
|
14
|
+
var activeSpinner = null;
|
|
15
|
+
var startTime = Date.now();
|
|
16
|
+
var totalTokens = 0;
|
|
17
|
+
var totalCost = 0;
|
|
18
|
+
|
|
19
|
+
var colors = {
|
|
20
|
+
user: chalk.cyan,
|
|
21
|
+
agent: chalk.green,
|
|
22
|
+
system: chalk.yellow,
|
|
23
|
+
error: chalk.red,
|
|
24
|
+
dim: chalk.gray,
|
|
25
|
+
accent: chalk.magenta,
|
|
26
|
+
bold: chalk.bold,
|
|
27
|
+
code: chalk.bgGray.white,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function renderBanner() {
|
|
31
|
+
var art;
|
|
32
|
+
try {
|
|
33
|
+
art = figlet.textSync("BLUN King", { font: "ANSI Shadow", horizontalLayout: "fitted" });
|
|
34
|
+
} catch (e) {
|
|
35
|
+
art = figlet.textSync("BLUN King", { font: "Standard" });
|
|
36
|
+
}
|
|
37
|
+
console.log(gradient.pastel.multiline(art));
|
|
38
|
+
console.log(chalk.dim(" " + "\u2500".repeat(53)));
|
|
39
|
+
console.log(
|
|
40
|
+
chalk.dim(" ") +
|
|
41
|
+
chalk.bold.white("BLUN King CLI") +
|
|
42
|
+
chalk.dim(" \u2502 ") +
|
|
43
|
+
chalk.green("v5.7.0") +
|
|
44
|
+
chalk.dim(" \u2502 ") +
|
|
45
|
+
chalk.yellow("Multi-KI Engine")
|
|
46
|
+
);
|
|
47
|
+
console.log(chalk.dim(" " + "\u2500".repeat(53) + "\n"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderPrompt(username, model) {
|
|
51
|
+
var u = chalk.cyan.bold(username || "user");
|
|
52
|
+
var m = chalk.dim("[" + (model || "auto") + "]");
|
|
53
|
+
return chalk.dim("\u250c") + " " + u + " " + m + "\n" + chalk.dim("\u2514") + " " + chalk.green.bold("\u276f") + " ";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderResponse(text, type) {
|
|
57
|
+
var colorFn = colors[type] || colors.agent;
|
|
58
|
+
var labels = { user: "YOU", agent: "BLUN", system: "SYS", error: "ERR" };
|
|
59
|
+
var label = labels[type] || "BLUN";
|
|
60
|
+
var prefix = colorFn.bold(" [" + label + "] ");
|
|
61
|
+
var rendered = renderMarkdown(text);
|
|
62
|
+
var lines = rendered.split("\n");
|
|
63
|
+
lines.forEach(function (line, i) {
|
|
64
|
+
if (i === 0) {
|
|
65
|
+
console.log(prefix + colorFn(line));
|
|
66
|
+
} else {
|
|
67
|
+
console.log(" " + colorFn(line));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
console.log();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderMarkdown(text) {
|
|
74
|
+
var out = text;
|
|
75
|
+
out = out.replace(/^### (.+)$/gm, function (_, h) { return chalk.bold.underline(h); });
|
|
76
|
+
out = out.replace(/^## (.+)$/gm, function (_, h) { return chalk.bold.yellow(h); });
|
|
77
|
+
out = out.replace(/^# (.+)$/gm, function (_, h) { return chalk.bold.magenta.underline(h); });
|
|
78
|
+
out = out.replace(/\*\*\*(.+?)\*\*\*/g, function (_, t) { return chalk.bold.italic(t); });
|
|
79
|
+
out = out.replace(/\*\*(.+?)\*\*/g, function (_, t) { return chalk.bold(t); });
|
|
80
|
+
out = out.replace(/\*(.+?)\*/g, function (_, t) { return chalk.italic(t); });
|
|
81
|
+
out = out.replace(/`([^`]+)`/g, function (_, c) { return chalk.bgGray.white(" " + c + " "); });
|
|
82
|
+
out = out.replace(/^(\s*)[-*] (.+)$/gm, function (_, sp, t) { return sp + chalk.green("\u25cf") + " " + t; });
|
|
83
|
+
out = out.replace(/^(\s*)\d+\. (.+)$/gm, function (_, sp, t) { return sp + chalk.yellow("\u25b8") + " " + t; });
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderSpinner(message) {
|
|
88
|
+
if (activeSpinner) activeSpinner.stop();
|
|
89
|
+
activeSpinner = ora({
|
|
90
|
+
text: chalk.dim(message || "Thinking..."),
|
|
91
|
+
spinner: "dots",
|
|
92
|
+
color: "cyan",
|
|
93
|
+
}).start();
|
|
94
|
+
return activeSpinner;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function stopSpinner(success, message) {
|
|
98
|
+
if (!activeSpinner) return;
|
|
99
|
+
if (success === false) {
|
|
100
|
+
activeSpinner.fail(chalk.red(message || "Failed"));
|
|
101
|
+
} else {
|
|
102
|
+
activeSpinner.succeed(chalk.green(message || "Done"));
|
|
103
|
+
}
|
|
104
|
+
activeSpinner = null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderTable(headers, rows) {
|
|
108
|
+
var table = new Table({
|
|
109
|
+
head: headers.map(function (h) { return chalk.cyan.bold(h); }),
|
|
110
|
+
chars: {
|
|
111
|
+
"top": "\u2500", "top-mid": "\u252c", "top-left": "\u250c", "top-right": "\u2510",
|
|
112
|
+
"bottom": "\u2500", "bottom-mid": "\u2534", "bottom-left": "\u2514", "bottom-right": "\u2518",
|
|
113
|
+
"left": "\u2502", "left-mid": "\u251c", "mid": "\u2500", "mid-mid": "\u253c",
|
|
114
|
+
"right": "\u2502", "right-mid": "\u2524", "middle": "\u2502",
|
|
115
|
+
},
|
|
116
|
+
style: { "padding-left": 1, "padding-right": 1 },
|
|
117
|
+
});
|
|
118
|
+
rows.forEach(function (r) { table.push(r); });
|
|
119
|
+
console.log(table.toString());
|
|
120
|
+
console.log();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderBox(title, content, color) {
|
|
124
|
+
var borderColor = color || "green";
|
|
125
|
+
var rendered = renderMarkdown(content);
|
|
126
|
+
console.log(
|
|
127
|
+
boxen(rendered, {
|
|
128
|
+
title: title ? chalk.bold(title) : undefined,
|
|
129
|
+
titleAlignment: "center",
|
|
130
|
+
padding: 1,
|
|
131
|
+
margin: { top: 0, bottom: 1, left: 2, right: 2 },
|
|
132
|
+
borderStyle: "round",
|
|
133
|
+
borderColor: borderColor,
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function renderCodeBlock(code, language) {
|
|
139
|
+
var lang = language ? chalk.dim.italic(" " + language + " ") : "";
|
|
140
|
+
var lineLen = Math.max(1, 50 - (language || "").length);
|
|
141
|
+
console.log(chalk.dim(" \u250c\u2500\u2500") + lang + chalk.dim("\u2500".repeat(lineLen)));
|
|
142
|
+
code.split("\n").forEach(function (line) {
|
|
143
|
+
var highlighted = line;
|
|
144
|
+
highlighted = highlighted.replace(/"[^"]*"/g, function (m) { return chalk.yellow(m); });
|
|
145
|
+
highlighted = highlighted.replace(/'[^']*'/g, function (m) { return chalk.yellow(m); });
|
|
146
|
+
var kws = ["const","let","var","function","return","if","else","for","while","class","import","export","from","require","async","await","try","catch","throw","new"];
|
|
147
|
+
var kwRe = new RegExp("\b(" + kws.join("|") + ")\b", "g");
|
|
148
|
+
highlighted = highlighted.replace(kwRe, function (m) { return chalk.magenta(m); });
|
|
149
|
+
highlighted = highlighted.replace(/\b(\d+\.?\d*)\b/g, function (m) { return chalk.cyan(m); });
|
|
150
|
+
highlighted = highlighted.replace(/(\/\/.*)$/g, function (m) { return chalk.dim(m); });
|
|
151
|
+
console.log(chalk.dim(" \u2502 ") + highlighted);
|
|
152
|
+
});
|
|
153
|
+
console.log(chalk.dim(" \u2514" + "\u2500".repeat(54)));
|
|
154
|
+
console.log();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderStatusBar(stats) {
|
|
158
|
+
var s = stats || {};
|
|
159
|
+
var model = s.model || "auto";
|
|
160
|
+
var tokens = s.tokens || totalTokens;
|
|
161
|
+
var cost = s.cost || totalCost;
|
|
162
|
+
var elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
163
|
+
var mins = Math.floor(elapsed / 60);
|
|
164
|
+
var secs = elapsed % 60;
|
|
165
|
+
var uptime = mins + "m " + secs + "s";
|
|
166
|
+
var parts = [
|
|
167
|
+
chalk.bgCyan.black(" " + model + " "),
|
|
168
|
+
chalk.bgGreen.black(" Tokens: " + tokens.toLocaleString() + " "),
|
|
169
|
+
chalk.bgYellow.black(" Cost: $" + cost.toFixed(4) + " "),
|
|
170
|
+
chalk.bgMagenta.black(" Uptime: " + uptime + " "),
|
|
171
|
+
];
|
|
172
|
+
var bar = parts.join(chalk.dim(" \u2502 "));
|
|
173
|
+
var width = process.stdout.columns || 80;
|
|
174
|
+
console.log(chalk.dim("\u2500".repeat(width)));
|
|
175
|
+
console.log(bar);
|
|
176
|
+
console.log(chalk.dim("\u2500".repeat(width)));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderProgress(current, total, label) {
|
|
180
|
+
var width = 40;
|
|
181
|
+
var pct = Math.min(1, current / total);
|
|
182
|
+
var filled = Math.round(width * pct);
|
|
183
|
+
var empty = width - filled;
|
|
184
|
+
var bar = chalk.green("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty));
|
|
185
|
+
var pctStr = chalk.bold(Math.round(pct * 100) + "%");
|
|
186
|
+
var lbl = label ? chalk.dim(" " + label) : "";
|
|
187
|
+
process.stdout.write("\r " + bar + " " + pctStr + lbl + " (" + current + "/" + total + ")");
|
|
188
|
+
if (current >= total) console.log();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function addTokens(count, costPerToken) {
|
|
192
|
+
totalTokens += count;
|
|
193
|
+
totalCost += count * (costPerToken || 0);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getStats() {
|
|
197
|
+
return { tokens: totalTokens, cost: totalCost, uptime: Date.now() - startTime };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = {
|
|
201
|
+
renderBanner: renderBanner,
|
|
202
|
+
renderPrompt: renderPrompt,
|
|
203
|
+
renderResponse: renderResponse,
|
|
204
|
+
renderSpinner: renderSpinner,
|
|
205
|
+
stopSpinner: stopSpinner,
|
|
206
|
+
renderTable: renderTable,
|
|
207
|
+
renderBox: renderBox,
|
|
208
|
+
renderCodeBlock: renderCodeBlock,
|
|
209
|
+
renderStatusBar: renderStatusBar,
|
|
210
|
+
renderProgress: renderProgress,
|
|
211
|
+
renderMarkdown: renderMarkdown,
|
|
212
|
+
addTokens: addTokens,
|
|
213
|
+
getStats: getStats,
|
|
214
|
+
colors: colors,
|
|
215
|
+
};
|
package/package.json
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blun-king-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "BLUN
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"start": "node api.js",
|
|
11
|
-
"cli": "node blun-cli.js",
|
|
12
|
-
"bot": "node bot.js",
|
|
13
|
-
"test": "node --test tests/*.test.js"
|
|
14
|
-
},
|
|
3
|
+
"version": "6.0.0",
|
|
4
|
+
"description": "BLUN AI Assistant - Command Line Interface",
|
|
5
|
+
"bin": { "blun": "./bin/blun.js" },
|
|
6
|
+
"files": ["bin/", "lib/", "README.md"],
|
|
7
|
+
"keywords": ["ai", "cli", "blun", "assistant"],
|
|
8
|
+
"author": "BLUN AI <info@blun.ai>",
|
|
9
|
+
"license": "MIT",
|
|
15
10
|
"dependencies": {
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
11
|
+
"chalk": "^4.1.2",
|
|
12
|
+
"ora": "^5.4.1",
|
|
13
|
+
"cli-table3": "^0.6.3",
|
|
14
|
+
"boxen": "^5.1.2",
|
|
15
|
+
"gradient-string": "^2.0.2",
|
|
16
|
+
"figlet": "^1.7.0"
|
|
19
17
|
},
|
|
20
|
-
"
|
|
21
|
-
"author": "BLUN AI",
|
|
22
|
-
"license": "MIT"
|
|
18
|
+
"engines": { "node": ">=18" }
|
|
23
19
|
}
|