slek-ai-cli 1.1.11 → 1.1.13
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/.env +1 -0
- package/auth.js +98 -128
- package/cli.js +168 -93
- package/package.json +1 -1
package/.env
CHANGED
|
@@ -5,3 +5,4 @@ FIREBASE_API_KEY="AIzaSyBoQHn_adTTj1ZaYZBMHCMSAblCGCIbQG4"
|
|
|
5
5
|
FIREBASE_AUTH_DOMAIN="charm-f004f.firebaseapp.com"
|
|
6
6
|
GOOGLE_CLIENT_ID="763614479011-v2m44ees6jhjaprj5ndoa0up83gf1qah.apps.googleusercontent.com"
|
|
7
7
|
GOOGLE_CLIENT_SECRET="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"
|
|
8
|
+
FIREBASE_PROJECT_ID="charm-f004f"
|
package/auth.js
CHANGED
|
@@ -10,25 +10,19 @@ const axios = require('axios');
|
|
|
10
10
|
const chalk = require('chalk');
|
|
11
11
|
const boxen = require('boxen');
|
|
12
12
|
|
|
13
|
-
// Load environment variables from .env
|
|
14
13
|
require('dotenv').config({ path: path.join(__dirname, '.env') });
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const FIREBASE_AUTH_DOMAIN = process.env.FIREBASE_AUTH_DOMAIN || 'charm-f004f.firebaseapp.com';
|
|
19
|
-
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '763614479011-v2m44ees6jhjaprj5ndoa0up83gf1qah.apps.googleusercontent.com';
|
|
15
|
+
const FIREBASE_API_KEY = process.env.FIREBASE_API_KEY || 'AIzaSyBoQHn_adTTj1ZaYZBMHCMSAblCGCIbQG4';
|
|
16
|
+
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '763614479011-v2m44ees6jhjaprj5ndoa0up83gf1qah.apps.googleusercontent.com';
|
|
20
17
|
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || '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';
|
|
18
|
+
const REDIRECT_URI = 'http://localhost:9876/callback';
|
|
19
|
+
const AUTH_PORT = 9876;
|
|
21
20
|
|
|
22
|
-
const REDIRECT_URI = 'http://localhost:9876/callback';
|
|
23
|
-
const AUTH_PORT = 9876;
|
|
24
|
-
|
|
25
|
-
// ─── Token storage (saved in user home directory) ─────────────────────────────
|
|
26
21
|
const TOKEN_PATH = path.join(
|
|
27
22
|
process.env.APPDATA || process.env.HOME || __dirname,
|
|
28
23
|
'.slek-auth.json'
|
|
29
24
|
);
|
|
30
25
|
|
|
31
|
-
// ─── Token helpers ────────────────────────────────────────────────────────────
|
|
32
26
|
function saveToken(data) {
|
|
33
27
|
fs.writeFileSync(TOKEN_PATH, JSON.stringify(data, null, 2), 'utf8');
|
|
34
28
|
}
|
|
@@ -49,15 +43,55 @@ function clearToken() {
|
|
|
49
43
|
function isLoggedIn() {
|
|
50
44
|
const token = loadToken();
|
|
51
45
|
if (!token) return false;
|
|
52
|
-
if
|
|
46
|
+
// Consider logged in if token exists (refresh will handle expiry)
|
|
53
47
|
return true;
|
|
54
48
|
}
|
|
55
49
|
|
|
56
|
-
|
|
50
|
+
// ─── Auto-refresh Firebase token ─────────────────────────────────────────────
|
|
51
|
+
async function refreshIdToken(token) {
|
|
52
|
+
try {
|
|
53
|
+
const res = await axios.post(
|
|
54
|
+
`https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`,
|
|
55
|
+
{
|
|
56
|
+
grant_type: 'refresh_token',
|
|
57
|
+
refresh_token: token.refreshToken,
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
const updated = {
|
|
61
|
+
...token,
|
|
62
|
+
idToken: res.data.id_token,
|
|
63
|
+
refreshToken: res.data.refresh_token,
|
|
64
|
+
expiresAt: Date.now() + parseInt(res.data.expires_in) * 1000,
|
|
65
|
+
};
|
|
66
|
+
saveToken(updated);
|
|
67
|
+
return updated;
|
|
68
|
+
} catch (_) {
|
|
69
|
+
clearToken();
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Get user with auto-refresh ───────────────────────────────────────────────
|
|
75
|
+
async function getUser() {
|
|
76
|
+
let token = loadToken();
|
|
77
|
+
if (!token) return null;
|
|
78
|
+
// Refresh if expired or about to expire (within 5 min)
|
|
79
|
+
if (!token.expiresAt || Date.now() > token.expiresAt - 5 * 60 * 1000) {
|
|
80
|
+
if (token.refreshToken) {
|
|
81
|
+
token = await refreshIdToken(token);
|
|
82
|
+
} else {
|
|
83
|
+
clearToken();
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return token;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Sync version for quick checks ───────────────────────────────────────────
|
|
91
|
+
function getUserSync() {
|
|
57
92
|
return loadToken();
|
|
58
93
|
}
|
|
59
94
|
|
|
60
|
-
// ─── Open browser cross-platform ─────────────────────────────────────────────
|
|
61
95
|
function openBrowser(targetUrl) {
|
|
62
96
|
try {
|
|
63
97
|
const platform = process.platform;
|
|
@@ -75,26 +109,24 @@ function openBrowser(targetUrl) {
|
|
|
75
109
|
}
|
|
76
110
|
}
|
|
77
111
|
|
|
78
|
-
// ─── Google OAuth URL ────────────────────────────────────────────────────────
|
|
79
112
|
function buildGoogleAuthUrl() {
|
|
80
113
|
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
|
|
81
|
-
authUrl.searchParams.set('client_id',
|
|
82
|
-
authUrl.searchParams.set('redirect_uri',
|
|
114
|
+
authUrl.searchParams.set('client_id', GOOGLE_CLIENT_ID);
|
|
115
|
+
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
83
116
|
authUrl.searchParams.set('response_type', 'code');
|
|
84
|
-
authUrl.searchParams.set('scope',
|
|
85
|
-
authUrl.searchParams.set('access_type',
|
|
86
|
-
authUrl.searchParams.set('prompt',
|
|
117
|
+
authUrl.searchParams.set('scope', 'openid email profile');
|
|
118
|
+
authUrl.searchParams.set('access_type', 'offline');
|
|
119
|
+
authUrl.searchParams.set('prompt', 'select_account');
|
|
87
120
|
return authUrl.toString();
|
|
88
121
|
}
|
|
89
122
|
|
|
90
|
-
// ─── Exchange Google code to Firebase token ──────────────────────────────────
|
|
91
123
|
async function exchangeCodeForToken(code) {
|
|
92
124
|
const tokenRes = await axios.post('https://oauth2.googleapis.com/token', {
|
|
93
125
|
code,
|
|
94
|
-
client_id:
|
|
126
|
+
client_id: GOOGLE_CLIENT_ID,
|
|
95
127
|
client_secret: GOOGLE_CLIENT_SECRET,
|
|
96
|
-
redirect_uri:
|
|
97
|
-
grant_type:
|
|
128
|
+
redirect_uri: REDIRECT_URI,
|
|
129
|
+
grant_type: 'authorization_code',
|
|
98
130
|
});
|
|
99
131
|
|
|
100
132
|
const { id_token } = tokenRes.data;
|
|
@@ -102,91 +134,49 @@ async function exchangeCodeForToken(code) {
|
|
|
102
134
|
const firebaseRes = await axios.post(
|
|
103
135
|
`https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp?key=${FIREBASE_API_KEY}`,
|
|
104
136
|
{
|
|
105
|
-
postBody:
|
|
106
|
-
requestUri:
|
|
137
|
+
postBody: `id_token=${id_token}&providerId=google.com`,
|
|
138
|
+
requestUri: REDIRECT_URI,
|
|
107
139
|
returnIdpCredential: true,
|
|
108
|
-
returnSecureToken:
|
|
140
|
+
returnSecureToken: true,
|
|
109
141
|
}
|
|
110
142
|
);
|
|
111
143
|
|
|
112
|
-
const { displayName, email, idToken, expiresIn, photoUrl } = firebaseRes.data;
|
|
144
|
+
const { displayName, email, idToken, refreshToken, expiresIn, photoUrl } = firebaseRes.data;
|
|
113
145
|
return {
|
|
114
|
-
name:
|
|
146
|
+
name: displayName,
|
|
115
147
|
email,
|
|
116
|
-
photo:
|
|
148
|
+
photo: photoUrl || '',
|
|
117
149
|
idToken,
|
|
118
|
-
|
|
150
|
+
refreshToken,
|
|
151
|
+
expiresAt: Date.now() + parseInt(expiresIn) * 1000,
|
|
119
152
|
};
|
|
120
153
|
}
|
|
121
154
|
|
|
122
|
-
// ─── Login ────────────────────────────────────────────────────────────────────
|
|
123
155
|
async function login() {
|
|
124
|
-
if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) {
|
|
125
|
-
console.log(
|
|
126
|
-
boxen(
|
|
127
|
-
chalk.red('✗ Google OAuth Client configuration missing!\n\n') +
|
|
128
|
-
chalk.white('Add GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET to .env'),
|
|
129
|
-
{ padding: 1, margin: { left: 2 }, borderStyle: 'round', borderColor: 'red' }
|
|
130
|
-
)
|
|
131
|
-
);
|
|
132
|
-
process.exit(1);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
156
|
const authUrl = buildGoogleAuthUrl();
|
|
136
157
|
|
|
137
158
|
return new Promise((resolve, reject) => {
|
|
138
159
|
const server = http.createServer(async (req, res) => {
|
|
139
160
|
const parsed = urlModule.parse(req.url, true);
|
|
161
|
+
if (parsed.pathname !== '/callback') { res.writeHead(404); res.end(); return; }
|
|
140
162
|
|
|
141
|
-
// Ignore any request that isn't the OAuth callback
|
|
142
|
-
if (parsed.pathname !== '/callback') {
|
|
143
|
-
res.writeHead(404);
|
|
144
|
-
res.end();
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Send success page to browser immediately
|
|
149
163
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
150
|
-
res.end(
|
|
151
|
-
<html>
|
|
152
|
-
<body style="
|
|
153
|
-
background:#0a0a0a;color:#fff;font-family:monospace;
|
|
154
|
-
display:flex;align-items:center;justify-content:center;
|
|
155
|
-
height:100vh;margin:0;">
|
|
156
|
-
<div style="text-align:center;">
|
|
157
|
-
<h1 style="color:#76b900;">✓ Login Successful!</h1>
|
|
158
|
-
<p style="color:#aaa;">You can close this tab and return to the terminal.</p>
|
|
159
|
-
</div>
|
|
160
|
-
</body>
|
|
161
|
-
</html>
|
|
162
|
-
`);
|
|
163
|
-
|
|
164
|
+
res.end(`<html><body style="background:#0a0a0a;color:#fff;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;"><div style="text-align:center;"><h1 style="color:#76b900;">✓ Login Successful!</h1><p style="color:#aaa;">You can close this tab and return to the terminal.</p></div></body></html>`);
|
|
164
165
|
server.close();
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
const error = parsed.query.error;
|
|
168
|
-
if (error) {
|
|
169
|
-
console.log(chalk.red('\n ✗ Login cancelled.\n'));
|
|
170
|
-
reject(new Error(error));
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
167
|
+
if (parsed.query.error) { console.log(chalk.red('\n ✗ Login cancelled.\n')); reject(new Error(parsed.query.error)); return; }
|
|
173
168
|
|
|
174
169
|
try {
|
|
175
170
|
process.stdout.write(chalk.cyan('\n ⏳ Verifying with Firebase...\n'));
|
|
176
|
-
const userData = await exchangeCodeForToken(code);
|
|
171
|
+
const userData = await exchangeCodeForToken(parsed.query.code);
|
|
177
172
|
saveToken(userData);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
'\n' +
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)
|
|
186
|
-
);
|
|
187
|
-
console.log(
|
|
188
|
-
chalk.gray('\n Now run ') + chalk.yellow('slek') + chalk.gray(' to start chatting!\n')
|
|
189
|
-
);
|
|
173
|
+
console.log('\n' + boxen(
|
|
174
|
+
chalk.green('✓ Logged in successfully!\n\n') +
|
|
175
|
+
chalk.cyan('Name : ') + chalk.white(userData.name) + '\n' +
|
|
176
|
+
chalk.cyan('Email : ') + chalk.white(userData.email),
|
|
177
|
+
{ padding: 1, margin: { left: 2 }, borderStyle: 'round', borderColor: 'green' }
|
|
178
|
+
));
|
|
179
|
+
console.log(chalk.gray('\n Now run ') + chalk.yellow('slek') + chalk.gray(' to start chatting!\n'));
|
|
190
180
|
resolve(userData);
|
|
191
181
|
} catch (err) {
|
|
192
182
|
console.log(chalk.red('\n ✗ Auth failed: ') + err.message + '\n');
|
|
@@ -194,65 +184,45 @@ async function login() {
|
|
|
194
184
|
}
|
|
195
185
|
});
|
|
196
186
|
|
|
197
|
-
server.on('error', (err)
|
|
198
|
-
console.log(chalk.red('\n ✗ Server error: ') + err.message);
|
|
199
|
-
reject(err);
|
|
200
|
-
});
|
|
187
|
+
server.on('error', err => { console.log(chalk.red('\n ✗ Server error: ') + err.message); reject(err); });
|
|
201
188
|
|
|
202
189
|
server.listen(AUTH_PORT, '127.0.0.1', () => {
|
|
203
|
-
console.log(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
)
|
|
213
|
-
);
|
|
190
|
+
console.log(boxen(
|
|
191
|
+
chalk.bold.white('🔐 SLEK AI — Google Login\n') +
|
|
192
|
+
chalk.gray('─'.repeat(38)) + '\n\n' +
|
|
193
|
+
chalk.cyan('Opening browser...\n') +
|
|
194
|
+
chalk.gray('Waiting for Google authentication...\n\n') +
|
|
195
|
+
chalk.gray('If browser does not open, paste this URL:\n') +
|
|
196
|
+
chalk.yellow(authUrl.toString()),
|
|
197
|
+
{ padding: 1, margin: { left: 2 }, borderStyle: 'double', borderColor: 'green' }
|
|
198
|
+
));
|
|
214
199
|
setTimeout(() => openBrowser(authUrl.toString()), 500);
|
|
215
200
|
});
|
|
216
201
|
|
|
217
|
-
|
|
218
|
-
setTimeout(() => {
|
|
219
|
-
server.close();
|
|
220
|
-
console.log(chalk.red('\n ✗ Login timed out (2 min). Please try again.\n'));
|
|
221
|
-
reject(new Error('Login timeout'));
|
|
222
|
-
}, 120_000);
|
|
202
|
+
setTimeout(() => { server.close(); console.log(chalk.red('\n ✗ Login timed out. Try again.\n')); reject(new Error('timeout')); }, 120_000);
|
|
223
203
|
});
|
|
224
204
|
}
|
|
225
205
|
|
|
226
|
-
// ─── Logout ───────────────────────────────────────────────────────────────────
|
|
227
206
|
function logout() {
|
|
228
207
|
clearToken();
|
|
229
|
-
console.log(
|
|
230
|
-
'\n' +
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
{ padding: 1, margin: { left: 2 }, borderStyle: 'round', borderColor: 'yellow' }
|
|
234
|
-
) + '\n'
|
|
235
|
-
);
|
|
208
|
+
console.log('\n' + boxen(
|
|
209
|
+
chalk.yellow('👋 Logged out!\n\n') + chalk.gray('Run ') + chalk.yellow('slek login') + chalk.gray(' to sign in again.'),
|
|
210
|
+
{ padding: 1, margin: { left: 2 }, borderStyle: 'round', borderColor: 'yellow' }
|
|
211
|
+
) + '\n');
|
|
236
212
|
}
|
|
237
213
|
|
|
238
|
-
// ─── Status ───────────────────────────────────────────────────────────────────
|
|
239
214
|
function status() {
|
|
240
|
-
const user =
|
|
215
|
+
const user = getUserSync();
|
|
241
216
|
if (user) {
|
|
242
|
-
console.log(
|
|
243
|
-
'\n' +
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
) + '\n'
|
|
249
|
-
);
|
|
217
|
+
console.log('\n' + boxen(
|
|
218
|
+
chalk.green('✓ Logged in\n\n') +
|
|
219
|
+
chalk.cyan('Name : ') + chalk.white(user.name) + '\n' +
|
|
220
|
+
chalk.cyan('Email : ') + chalk.white(user.email),
|
|
221
|
+
{ padding: 1, margin: { left: 2 }, borderStyle: 'round', borderColor: 'green' }
|
|
222
|
+
) + '\n');
|
|
250
223
|
} else {
|
|
251
|
-
console.log(
|
|
252
|
-
chalk.yellow('\n Not logged in. Run: ') + chalk.white('slek login\n')
|
|
253
|
-
);
|
|
224
|
+
console.log(chalk.yellow('\n Not logged in. Run: ') + chalk.white('slek login\n'));
|
|
254
225
|
}
|
|
255
226
|
}
|
|
256
227
|
|
|
257
|
-
|
|
258
|
-
module.exports = { isLoggedIn, getUser, login, logout, status };
|
|
228
|
+
module.exports = { isLoggedIn, getUser, getUserSync, login, logout, status };
|
package/cli.js
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
require('dotenv').config({
|
|
5
|
+
path: require('path').join(__dirname, '.env')
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// ─── Auth check ───────────────────────────────────────────────────────────────
|
|
5
9
|
const auth = require('./auth');
|
|
6
10
|
|
|
7
11
|
const arg = process.argv[2];
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
if (arg === '
|
|
11
|
-
|
|
13
|
+
// Handle: slek login / slek logout / slek status
|
|
14
|
+
if (arg === 'login') {
|
|
15
|
+
auth.login().catch(() => process.exit(1));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (arg === 'logout') {
|
|
19
|
+
auth.logout();
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
if (arg === 'status') {
|
|
23
|
+
auth.status();
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
12
26
|
|
|
13
27
|
// Block access if not logged in
|
|
14
28
|
if (!auth.isLoggedIn()) {
|
|
@@ -26,31 +40,31 @@ if (!auth.isLoggedIn()) {
|
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
const readline = require('readline');
|
|
29
|
-
const axios
|
|
30
|
-
const chalk
|
|
31
|
-
const boxen
|
|
43
|
+
const axios = require('axios');
|
|
44
|
+
const chalk = require('chalk');
|
|
45
|
+
const boxen = require('boxen');
|
|
32
46
|
const gradient = require('gradient-string');
|
|
33
|
-
const figlet
|
|
47
|
+
const figlet = require('figlet');
|
|
34
48
|
|
|
49
|
+
// ─── NOTE: ora v5 is required (CommonJS compatible) ──────────────────────────
|
|
50
|
+
// If ora gives errors, run: npm install ora@5
|
|
35
51
|
let ora;
|
|
36
52
|
try {
|
|
37
53
|
ora = require('ora');
|
|
38
54
|
} catch {
|
|
55
|
+
// Fallback spinner if ora fails
|
|
39
56
|
ora = (opts) => ({
|
|
40
|
-
start: () => {
|
|
41
|
-
process.stdout.write(chalk.cyan(' ⏳ ' + (opts.text || 'Loading...') + '\n'));
|
|
42
|
-
return { stop: () => {}, fail: () => {} };
|
|
43
|
-
},
|
|
57
|
+
start: () => { process.stdout.write(chalk.cyan(' ⏳ ' + (opts.text || 'Loading...') + '\n')); return { stop: () => {}, fail: () => {} }; },
|
|
44
58
|
stop: () => {},
|
|
45
59
|
fail: () => {},
|
|
46
60
|
});
|
|
47
61
|
}
|
|
48
62
|
|
|
49
|
-
// ─── API Config
|
|
50
|
-
const NVIDIA_API_BASE = 'https://
|
|
51
|
-
const DEFAULT_MODEL
|
|
63
|
+
// ─── NVIDIA API Config ────────────────────────────────────────────────────────
|
|
64
|
+
const NVIDIA_API_BASE = 'https://integrate.api.nvidia.com/v1';
|
|
65
|
+
const DEFAULT_MODEL = 'qwen/qwen3.5-122b-a10b';
|
|
52
66
|
|
|
53
|
-
// ─── User config path
|
|
67
|
+
// ─── User config path (stored in user's home, not in project) ─────────────────
|
|
54
68
|
const CONFIG_PATH = require('path').join(
|
|
55
69
|
process.env.APPDATA || process.env.HOME || __dirname,
|
|
56
70
|
'.slek-config.json'
|
|
@@ -61,29 +75,25 @@ function loadConfig() {
|
|
|
61
75
|
if (require('fs').existsSync(CONFIG_PATH)) {
|
|
62
76
|
return JSON.parse(require('fs').readFileSync(CONFIG_PATH, 'utf8'));
|
|
63
77
|
}
|
|
64
|
-
} catch
|
|
78
|
+
} catch { }
|
|
65
79
|
return {};
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
function saveConfig(data) {
|
|
69
83
|
const current = loadConfig();
|
|
70
|
-
require('fs').writeFileSync(
|
|
71
|
-
CONFIG_PATH,
|
|
72
|
-
JSON.stringify({ ...current, ...data }, null, 2),
|
|
73
|
-
'utf8'
|
|
74
|
-
);
|
|
84
|
+
require('fs').writeFileSync(CONFIG_PATH, JSON.stringify({ ...current, ...data }, null, 2), 'utf8');
|
|
75
85
|
}
|
|
76
86
|
|
|
77
|
-
// ───
|
|
87
|
+
// ─── Gradient Colors ─────────────────────────────────────────────────────────
|
|
78
88
|
const nvidiaGradient = gradient(['#76b900', '#00c8ff']);
|
|
79
|
-
const userGradient
|
|
80
|
-
const aiGradient
|
|
81
|
-
const thinkGradient
|
|
89
|
+
const userGradient = gradient(['#ff6b6b', '#feca57']);
|
|
90
|
+
const aiGradient = gradient(['#48dbfb', '#ff9ff3']);
|
|
91
|
+
const thinkGradient = gradient(['#f7971e', '#ffd200']);
|
|
82
92
|
|
|
83
|
-
// ─── State
|
|
84
|
-
let chatHistory
|
|
85
|
-
let apiKey
|
|
86
|
-
const currentUser = auth.
|
|
93
|
+
// ─── State ───────────────────────────────────────────────────────────────────
|
|
94
|
+
let chatHistory = [];
|
|
95
|
+
let apiKey = process.env.NVIDIA_API_KEY || '';
|
|
96
|
+
const currentUser = auth.getUserSync();
|
|
87
97
|
|
|
88
98
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
89
99
|
function clearLine() {
|
|
@@ -93,14 +103,17 @@ function clearLine() {
|
|
|
93
103
|
// ─── Banner ───────────────────────────────────────────────────────────────────
|
|
94
104
|
async function showBanner() {
|
|
95
105
|
console.clear();
|
|
96
|
-
const banner = figlet.textSync('SLEK AI', {
|
|
106
|
+
const banner = figlet.textSync('SLEK AI', {
|
|
107
|
+
font: 'ANSI Shadow',
|
|
108
|
+
horizontalLayout: 'default',
|
|
109
|
+
});
|
|
97
110
|
console.log(nvidiaGradient(banner));
|
|
98
111
|
console.log(
|
|
99
112
|
boxen(
|
|
100
113
|
chalk.bold.white('🚀 SLEK AI CLI') + chalk.gray(' — Powered by NVIDIA API\n') +
|
|
101
|
-
chalk.gray('
|
|
114
|
+
chalk.gray('━'.repeat(42)) + '\n' +
|
|
102
115
|
chalk.cyan(' User : ') + chalk.green(currentUser ? currentUser.name : 'Unknown') + '\n' +
|
|
103
|
-
chalk.gray('
|
|
116
|
+
chalk.gray('━'.repeat(42)) + '\n' +
|
|
104
117
|
chalk.white(' Type ') + chalk.yellow('/help') + chalk.white(' for available commands'),
|
|
105
118
|
{
|
|
106
119
|
padding: 1,
|
|
@@ -115,37 +128,87 @@ async function showBanner() {
|
|
|
115
128
|
|
|
116
129
|
// ─── Help ─────────────────────────────────────────────────────────────────────
|
|
117
130
|
function showHelp() {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
const helpBox = boxen(
|
|
132
|
+
chalk.bold.cyan('📋 Available Commands\n') +
|
|
133
|
+
chalk.gray('═'.repeat(42)) + '\n\n' +
|
|
134
|
+
chalk.yellow('/help ') + chalk.white('Show this help menu\n') +
|
|
135
|
+
chalk.yellow('/clear ') + chalk.white('Clear chat history\n') +
|
|
136
|
+
chalk.yellow('/history ') + chalk.white('Show chat history\n') +
|
|
137
|
+
chalk.yellow('/save ') + chalk.white('Save chat to file\n') +
|
|
138
|
+
chalk.yellow('/info ') + chalk.white('Show current settings\n') +
|
|
139
|
+
chalk.yellow('/exit ') + chalk.white('Exit the CLI\n\n') +
|
|
140
|
+
chalk.gray('Or just type anything to chat with AI!'),
|
|
141
|
+
{
|
|
142
|
+
padding: 1,
|
|
143
|
+
margin: { left: 2 },
|
|
144
|
+
borderStyle: 'round',
|
|
145
|
+
borderColor: 'cyan',
|
|
146
|
+
}
|
|
131
147
|
);
|
|
148
|
+
console.log(helpBox);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── Show Models ──────────────────────────────────────────────────────────────
|
|
152
|
+
function showModels() {
|
|
153
|
+
console.log('\n' + chalk.bold.cyan(' 🤖 Available Models:\n'));
|
|
154
|
+
Object.entries(MODELS).forEach(([key, model]) => {
|
|
155
|
+
const active = model.id === currentModel ? chalk.green(' ◄ ACTIVE') : '';
|
|
156
|
+
const thinking = model.thinking ? chalk.yellow(' [🧠 Thinking]') : '';
|
|
157
|
+
console.log(` ${chalk.yellow(key + '.')} ${chalk.white(model.name)}${thinking}${active}`);
|
|
158
|
+
});
|
|
159
|
+
console.log();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── Change Model ─────────────────────────────────────────────────────────────
|
|
163
|
+
async function changeModel(rl) {
|
|
164
|
+
showModels();
|
|
165
|
+
return new Promise(resolve => {
|
|
166
|
+
rl.question(chalk.cyan(' Enter model number (1-7): '), answer => {
|
|
167
|
+
const chosen = MODELS[answer.trim()];
|
|
168
|
+
if (chosen) {
|
|
169
|
+
currentModel = chosen.id;
|
|
170
|
+
enableThinking = chosen.thinking || false;
|
|
171
|
+
const thinkNote = chosen.thinking ? chalk.yellow(' (Thinking mode ON)') : '';
|
|
172
|
+
console.log(chalk.green(`\n ✓ Model: ${chalk.yellow(chosen.name.trim())}${thinkNote}\n`));
|
|
173
|
+
} else {
|
|
174
|
+
console.log(chalk.red('\n ✗ Invalid choice\n'));
|
|
175
|
+
}
|
|
176
|
+
resolve();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── Toggle Thinking ──────────────────────────────────────────────────────────
|
|
182
|
+
function toggleThinking() {
|
|
183
|
+
enableThinking = !enableThinking;
|
|
184
|
+
if (enableThinking) {
|
|
185
|
+
console.log(chalk.green('\n 🧠 Thinking mode: ON\n'));
|
|
186
|
+
} else {
|
|
187
|
+
console.log(chalk.gray('\n 💤 Thinking mode: OFF\n'));
|
|
188
|
+
}
|
|
132
189
|
}
|
|
133
190
|
|
|
134
|
-
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
// ─── Show Info ────────────────────────────────────────────────────────────────
|
|
135
194
|
function showInfo() {
|
|
136
|
-
|
|
137
|
-
'\n' +
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
195
|
+
const infoBox = boxen(
|
|
196
|
+
chalk.bold.white('⚙ Current Settings\n') +
|
|
197
|
+
chalk.gray('─'.repeat(40)) + '\n\n' +
|
|
198
|
+
chalk.cyan('User : ') + chalk.green(currentUser ? currentUser.name : 'Unknown') + '\n' +
|
|
199
|
+
chalk.cyan('Messages : ') + chalk.white(chatHistory.length + ' in history') + '\n' +
|
|
200
|
+
chalk.cyan('API URL : ') + chalk.gray(NVIDIA_API_BASE),
|
|
201
|
+
{
|
|
202
|
+
padding: 1,
|
|
203
|
+
margin: { left: 2 },
|
|
204
|
+
borderStyle: 'single',
|
|
205
|
+
borderColor: 'yellow',
|
|
206
|
+
}
|
|
145
207
|
);
|
|
208
|
+
console.log('\n' + infoBox);
|
|
146
209
|
}
|
|
147
210
|
|
|
148
|
-
// ─── History
|
|
211
|
+
// ─── Show History ─────────────────────────────────────────────────────────────
|
|
149
212
|
function showHistory() {
|
|
150
213
|
if (chatHistory.length === 0) {
|
|
151
214
|
console.log(chalk.yellow('\n No chat history yet.\n'));
|
|
@@ -176,7 +239,7 @@ async function saveChat() {
|
|
|
176
239
|
|
|
177
240
|
let content = `SLEK AI CLI - Chat Log\n`;
|
|
178
241
|
content += `Date: ${new Date().toLocaleString()}\n`;
|
|
179
|
-
content += `Model: ${
|
|
242
|
+
content += `Model: ${currentModel}\n`;
|
|
180
243
|
content += `${'═'.repeat(50)}\n\n`;
|
|
181
244
|
|
|
182
245
|
chatHistory.forEach(msg => {
|
|
@@ -189,13 +252,14 @@ async function saveChat() {
|
|
|
189
252
|
});
|
|
190
253
|
|
|
191
254
|
fs.writeFileSync(filepath, content, 'utf8');
|
|
192
|
-
console.log(chalk.green(`\n
|
|
255
|
+
console.log(chalk.green(`\n ✓ Chat saved to: ${chalk.yellow(filename)}\n`));
|
|
193
256
|
}
|
|
194
257
|
|
|
195
258
|
// ─── Stream API Call ──────────────────────────────────────────────────────────
|
|
196
259
|
async function callNvidiaAPIStream(userMessage) {
|
|
197
|
-
|
|
198
|
-
|
|
260
|
+
const user = await auth.getUser(); // auto-refresh if expired
|
|
261
|
+
if (!user || !user.idToken) {
|
|
262
|
+
throw new Error('Not authenticated. Please run: slek login');
|
|
199
263
|
}
|
|
200
264
|
|
|
201
265
|
chatHistory.push({ role: 'user', content: userMessage });
|
|
@@ -210,20 +274,22 @@ async function callNvidiaAPIStream(userMessage) {
|
|
|
210
274
|
...chatHistory,
|
|
211
275
|
];
|
|
212
276
|
|
|
277
|
+
const payload = {
|
|
278
|
+
model: DEFAULT_MODEL,
|
|
279
|
+
messages,
|
|
280
|
+
max_tokens: 16384,
|
|
281
|
+
temperature: 0.60,
|
|
282
|
+
top_p: 0.95,
|
|
283
|
+
stream: true,
|
|
284
|
+
chat_template_kwargs: { enable_thinking: true },
|
|
285
|
+
};
|
|
286
|
+
|
|
213
287
|
const response = await axios.post(
|
|
214
288
|
`${NVIDIA_API_BASE}/chat-free`,
|
|
215
|
-
|
|
216
|
-
model: DEFAULT_MODEL,
|
|
217
|
-
messages,
|
|
218
|
-
max_tokens: 16384,
|
|
219
|
-
temperature: 0.60,
|
|
220
|
-
top_p: 0.95,
|
|
221
|
-
stream: true,
|
|
222
|
-
chat_template_kwargs: { enable_thinking: true },
|
|
223
|
-
},
|
|
289
|
+
payload,
|
|
224
290
|
{
|
|
225
291
|
headers: {
|
|
226
|
-
Authorization: `Bearer ${
|
|
292
|
+
Authorization: `Bearer ${user.idToken}`,
|
|
227
293
|
'Content-Type': 'application/json',
|
|
228
294
|
Accept: 'text/event-stream',
|
|
229
295
|
},
|
|
@@ -233,8 +299,8 @@ async function callNvidiaAPIStream(userMessage) {
|
|
|
233
299
|
);
|
|
234
300
|
|
|
235
301
|
return new Promise((resolve, reject) => {
|
|
236
|
-
let fullResponse
|
|
237
|
-
let inThinkBlock
|
|
302
|
+
let fullResponse = '';
|
|
303
|
+
let inThinkBlock = false;
|
|
238
304
|
|
|
239
305
|
process.stdout.write('\n ' + aiGradient('✦ SLEK AI: ') + '\n\n ');
|
|
240
306
|
|
|
@@ -260,10 +326,12 @@ async function callNvidiaAPIStream(userMessage) {
|
|
|
260
326
|
continue;
|
|
261
327
|
}
|
|
262
328
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
329
|
+
if (inThinkBlock) {
|
|
330
|
+
process.stdout.write(chalk.dim.yellow(token));
|
|
331
|
+
} else {
|
|
332
|
+
process.stdout.write(chalk.white(token));
|
|
333
|
+
}
|
|
334
|
+
} catch { /* ignore malformed SSE lines */ }
|
|
267
335
|
}
|
|
268
336
|
}
|
|
269
337
|
});
|
|
@@ -279,7 +347,7 @@ async function callNvidiaAPIStream(userMessage) {
|
|
|
279
347
|
});
|
|
280
348
|
}
|
|
281
349
|
|
|
282
|
-
// ─── Process Input
|
|
350
|
+
// ─── Process User Input ───────────────────────────────────────────────────────
|
|
283
351
|
async function processInput(input, rl) {
|
|
284
352
|
const trimmed = input.trim();
|
|
285
353
|
if (!trimmed) return;
|
|
@@ -287,14 +355,14 @@ async function processInput(input, rl) {
|
|
|
287
355
|
if (trimmed.startsWith('/')) {
|
|
288
356
|
const cmd = trimmed.toLowerCase().split(' ')[0];
|
|
289
357
|
switch (cmd) {
|
|
290
|
-
case '/help': showHelp();
|
|
358
|
+
case '/help': showHelp(); break;
|
|
291
359
|
case '/clear':
|
|
292
360
|
chatHistory = [];
|
|
293
|
-
console.log(chalk.green('\n
|
|
361
|
+
console.log(chalk.green('\n ✓ Chat history cleared!\n'));
|
|
294
362
|
break;
|
|
295
|
-
case '/history': showHistory();
|
|
296
|
-
case '/save': await saveChat();
|
|
297
|
-
case '/info': showInfo();
|
|
363
|
+
case '/history': showHistory(); break;
|
|
364
|
+
case '/save': await saveChat(); break;
|
|
365
|
+
case '/info': showInfo(); break;
|
|
298
366
|
case '/exit':
|
|
299
367
|
case '/quit':
|
|
300
368
|
console.log('\n' + nvidiaGradient(' 👋 Goodbye! Thanks for using SLEK AI CLI\n'));
|
|
@@ -308,9 +376,9 @@ async function processInput(input, rl) {
|
|
|
308
376
|
}
|
|
309
377
|
|
|
310
378
|
const spinner = ora({
|
|
311
|
-
text:
|
|
312
|
-
spinner:
|
|
313
|
-
color:
|
|
379
|
+
text: chalk.cyan(' Connecting to NVIDIA API...'),
|
|
380
|
+
spinner: 'dots',
|
|
381
|
+
color: 'cyan',
|
|
314
382
|
prefixText: ' ',
|
|
315
383
|
}).start();
|
|
316
384
|
|
|
@@ -326,10 +394,13 @@ async function processInput(input, rl) {
|
|
|
326
394
|
err.message ||
|
|
327
395
|
'Unknown error';
|
|
328
396
|
console.log(
|
|
329
|
-
'\n' +
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
397
|
+
'\n' +
|
|
398
|
+
boxen(chalk.red('✗ Error: ') + chalk.white(errMsg), {
|
|
399
|
+
padding: 1,
|
|
400
|
+
margin: { left: 2 },
|
|
401
|
+
borderStyle: 'round',
|
|
402
|
+
borderColor: 'red',
|
|
403
|
+
}) + '\n'
|
|
333
404
|
);
|
|
334
405
|
}
|
|
335
406
|
}
|
|
@@ -338,7 +409,9 @@ async function processInput(input, rl) {
|
|
|
338
409
|
function showPrompt(rl) {
|
|
339
410
|
const prompt =
|
|
340
411
|
'\n' +
|
|
341
|
-
chalk.gray(' ┌─') +
|
|
412
|
+
chalk.gray(' ┌─') +
|
|
413
|
+
userGradient(' You ') +
|
|
414
|
+
chalk.gray('─────────────────────────────────\n') +
|
|
342
415
|
chalk.gray(' └▶ ');
|
|
343
416
|
rl.setPrompt(prompt);
|
|
344
417
|
rl.prompt();
|
|
@@ -348,9 +421,11 @@ function showPrompt(rl) {
|
|
|
348
421
|
async function main() {
|
|
349
422
|
await showBanner();
|
|
350
423
|
|
|
424
|
+
|
|
425
|
+
|
|
351
426
|
const rl = readline.createInterface({
|
|
352
|
-
input:
|
|
353
|
-
output:
|
|
427
|
+
input: process.stdin,
|
|
428
|
+
output: process.stdout,
|
|
354
429
|
terminal: true,
|
|
355
430
|
});
|
|
356
431
|
|