npm-noxyai 1.0.20 → 1.0.22
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/index-noxyai.js +279 -138
- package/package.json +1 -1
package/index-noxyai.js
CHANGED
|
@@ -9,8 +9,9 @@ const crypto = require('crypto');
|
|
|
9
9
|
|
|
10
10
|
const BASE_URL = 'https://www.noxyai.com';
|
|
11
11
|
const CONFIG_FILE = path.join(os.homedir(), '.noxyai.json');
|
|
12
|
-
const SESSION_FILE = path.join(os.homedir(), '.noxyai_session.json');
|
|
13
12
|
const CACHE_DIR = path.join(os.homedir(), '.noxyai_cache');
|
|
13
|
+
const VERSION = '2.0.0';
|
|
14
|
+
|
|
14
15
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
15
16
|
|
|
16
17
|
// Ensure cache directory exists
|
|
@@ -19,20 +20,19 @@ if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
|
19
20
|
const args = process.argv.slice(2);
|
|
20
21
|
const command = args[0];
|
|
21
22
|
|
|
22
|
-
//
|
|
23
|
-
|
|
23
|
+
// ============================================
|
|
24
|
+
// CONFIG & SECURITY
|
|
25
|
+
// ============================================
|
|
24
26
|
|
|
25
27
|
function loadConfig() {
|
|
26
28
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
27
29
|
try {
|
|
28
30
|
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
29
|
-
// Decrypt token if it was encrypted
|
|
30
31
|
if (config.token && config.token.startsWith('enc:')) {
|
|
31
32
|
config.token = decryptToken(config.token.substring(4));
|
|
32
33
|
}
|
|
33
34
|
return config;
|
|
34
35
|
} catch(e) {
|
|
35
|
-
console.error('Config corruption detected, resetting...');
|
|
36
36
|
return { token: null, model: 'auto', agentMode: false, autoApprove: false, safeMode: true };
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -40,7 +40,6 @@ function loadConfig() {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function saveConfig(config) {
|
|
43
|
-
// Encrypt token for security
|
|
44
43
|
const configToSave = { ...config };
|
|
45
44
|
if (configToSave.token && !configToSave.token.startsWith('enc:')) {
|
|
46
45
|
configToSave.token = 'enc:' + encryptToken(configToSave.token);
|
|
@@ -48,7 +47,6 @@ function saveConfig(config) {
|
|
|
48
47
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(configToSave, null, 2));
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
// Simple token encryption using machine-specific key
|
|
52
50
|
function getMachineKey() {
|
|
53
51
|
const networkInterfaces = os.networkInterfaces();
|
|
54
52
|
const mac = Object.values(networkInterfaces)
|
|
@@ -80,6 +78,19 @@ function decryptToken(encryptedData) {
|
|
|
80
78
|
}
|
|
81
79
|
}
|
|
82
80
|
|
|
81
|
+
function getDeviceInfo() {
|
|
82
|
+
return {
|
|
83
|
+
platform: os.platform(),
|
|
84
|
+
name: os.hostname(),
|
|
85
|
+
userAgent: `NoxyAI-CLI/${VERSION} (${os.platform()}; ${os.hostname()})`,
|
|
86
|
+
fingerprint: crypto.createHash('sha256').update(os.platform() + os.hostname()).digest('hex')
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================
|
|
91
|
+
// UI UTILITIES
|
|
92
|
+
// ============================================
|
|
93
|
+
|
|
83
94
|
function printLogo() {
|
|
84
95
|
console.log('\x1b[36m' + `
|
|
85
96
|
███╗ ██╗ ██████╗ ██╗ ██╗██╗ ██╗ █████╗ ██╗
|
|
@@ -89,6 +100,7 @@ function printLogo() {
|
|
|
89
100
|
██║ ╚████║╚██████╔╝██╔╝ ██╗ ██║ ██║ ██║██║
|
|
90
101
|
╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝
|
|
91
102
|
` + '\x1b[0m');
|
|
103
|
+
console.log('\x1b[90m' + ` v${VERSION} - Secure Terminal Agent` + '\x1b[0m\n');
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
let spinnerInterval;
|
|
@@ -106,14 +118,26 @@ function stopSpinner() {
|
|
|
106
118
|
process.stdout.write('\r\x1b[K\x1b[?25h');
|
|
107
119
|
}
|
|
108
120
|
|
|
109
|
-
|
|
121
|
+
const askConfirm = (query) => new Promise(resolve => rl.question(query, resolve));
|
|
122
|
+
|
|
123
|
+
// ============================================
|
|
124
|
+
// AUTHENTICATION
|
|
125
|
+
// ============================================
|
|
126
|
+
|
|
110
127
|
async function login() {
|
|
111
128
|
printLogo();
|
|
112
129
|
console.log('\x1b[33m🔐 Initializing secure device login...\x1b[0m\n');
|
|
130
|
+
|
|
131
|
+
const deviceInfo = getDeviceInfo();
|
|
132
|
+
|
|
113
133
|
try {
|
|
114
134
|
const initRes = await fetch(BASE_URL + '/api/cli/init', {
|
|
115
135
|
method: 'POST',
|
|
116
|
-
headers: {
|
|
136
|
+
headers: {
|
|
137
|
+
'Content-Type': 'application/json',
|
|
138
|
+
'X-Client-Platform': deviceInfo.platform,
|
|
139
|
+
'X-Client-Name': deviceInfo.name
|
|
140
|
+
}
|
|
117
141
|
});
|
|
118
142
|
|
|
119
143
|
if (!initRes.ok) throw new Error(`Server error: ${initRes.status}`);
|
|
@@ -124,21 +148,18 @@ async function login() {
|
|
|
124
148
|
console.log(`\x1b[34m🌐 Login URL:\x1b[0m ${verificationUrl}`);
|
|
125
149
|
console.log(`\n\x1b[90mThis code expires in ${Math.floor(expiresIn / 60)} minutes.\x1b[0m\n`);
|
|
126
150
|
|
|
127
|
-
//
|
|
151
|
+
// Copy to clipboard
|
|
128
152
|
try {
|
|
129
153
|
const platform = os.platform();
|
|
130
154
|
if (platform === 'darwin') execSync(`echo "${userCode}" | pbcopy`);
|
|
131
155
|
else if (platform === 'win32') execSync(`echo ${userCode} | clip`);
|
|
132
|
-
else if (platform === 'linux'
|
|
133
|
-
execSync(`echo "${userCode}" | xclip -selection clipboard`);
|
|
156
|
+
else if (platform === 'linux') execSync(`echo "${userCode}" | xclip -selection clipboard 2>/dev/null || echo "${userCode}" | wl-copy 2>/dev/null || true`);
|
|
134
157
|
console.log('\x1b[32m✓ Code copied to clipboard!\x1b[0m');
|
|
135
|
-
} catch (e) {
|
|
136
|
-
// Clipboard not available, ignore
|
|
137
|
-
}
|
|
158
|
+
} catch (e) {}
|
|
138
159
|
|
|
139
160
|
rl.question('\x1b[36mPress ENTER to open browser or type "q" to cancel:\x1b[0m ', (answer) => {
|
|
140
161
|
if (answer.toLowerCase() === 'q') {
|
|
141
|
-
console.log('\x1b[31mLogin cancelled.\x1b[0m');
|
|
162
|
+
console.log('\n\x1b[31mLogin cancelled.\x1b[0m');
|
|
142
163
|
process.exit(0);
|
|
143
164
|
}
|
|
144
165
|
|
|
@@ -164,7 +185,11 @@ async function login() {
|
|
|
164
185
|
try {
|
|
165
186
|
const pollRes = await fetch(BASE_URL + '/api/cli/poll', {
|
|
166
187
|
method: 'POST',
|
|
167
|
-
headers: {
|
|
188
|
+
headers: {
|
|
189
|
+
'Content-Type': 'application/json',
|
|
190
|
+
'X-Client-Platform': deviceInfo.platform,
|
|
191
|
+
'X-Client-Name': deviceInfo.name
|
|
192
|
+
},
|
|
168
193
|
body: JSON.stringify({ deviceCode })
|
|
169
194
|
});
|
|
170
195
|
|
|
@@ -177,33 +202,154 @@ async function login() {
|
|
|
177
202
|
config.token = data.token;
|
|
178
203
|
config.userEmail = data.email;
|
|
179
204
|
config.userId = data.userId;
|
|
180
|
-
config.
|
|
205
|
+
config.expiresAt = data.expiresAt;
|
|
206
|
+
config.deviceName = deviceInfo.name;
|
|
207
|
+
config.devicePlatform = deviceInfo.platform;
|
|
181
208
|
saveConfig(config);
|
|
182
209
|
console.clear();
|
|
183
210
|
printLogo();
|
|
184
|
-
console.log(`\x1b[32m✅ Successfully logged in as ${data.email}!\x1b[0m
|
|
185
|
-
console.log(`\x1b[
|
|
211
|
+
console.log(`\x1b[32m✅ Successfully logged in as ${data.email}!\x1b[0m`);
|
|
212
|
+
console.log(`\x1b[90mSession expires: ${new Date(data.expiresAt).toLocaleDateString()}\x1b[0m`);
|
|
213
|
+
console.log(`\x1b[90mDevice: ${deviceInfo.name} (${deviceInfo.platform})\x1b[0m\n`);
|
|
214
|
+
console.log(`\x1b[90mType 'noxyai' to start.\x1b[0m\n`);
|
|
186
215
|
process.exit(0);
|
|
187
|
-
} else if (data.status === 'pending') {
|
|
188
|
-
// Still waiting
|
|
189
216
|
} else if (data.error) {
|
|
190
217
|
clearInterval(pollInterval);
|
|
191
218
|
stopSpinner();
|
|
192
219
|
console.error(`\n\x1b[31m❌ Login failed: ${data.error}\x1b[0m`);
|
|
193
220
|
process.exit(1);
|
|
194
221
|
}
|
|
195
|
-
} catch (error) {
|
|
196
|
-
// Network errors shouldn't break the polling
|
|
197
|
-
}
|
|
222
|
+
} catch (error) {}
|
|
198
223
|
}, (interval || 5) * 1000);
|
|
199
224
|
} catch (error) {
|
|
200
225
|
stopSpinner();
|
|
201
|
-
console.error('\x1b[31m❌ Failed to connect:', error.message, '\x1b[0m');
|
|
226
|
+
console.error('\n\x1b[31m❌ Failed to connect:', error.message, '\x1b[0m');
|
|
202
227
|
process.exit(1);
|
|
203
228
|
}
|
|
204
229
|
}
|
|
205
230
|
|
|
206
|
-
|
|
231
|
+
async function logout() {
|
|
232
|
+
const config = loadConfig();
|
|
233
|
+
|
|
234
|
+
if (!config.token) {
|
|
235
|
+
console.log('\x1b[33mNot logged in.\x1b[0m\n');
|
|
236
|
+
process.exit(0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const res = await fetch(BASE_URL + '/api/cli/logout', {
|
|
241
|
+
method: 'POST',
|
|
242
|
+
headers: {
|
|
243
|
+
'Authorization': 'Bearer ' + config.token,
|
|
244
|
+
'Content-Type': 'application/json'
|
|
245
|
+
},
|
|
246
|
+
body: JSON.stringify({ allDevices: false })
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
config.token = null;
|
|
250
|
+
config.userEmail = null;
|
|
251
|
+
config.userId = null;
|
|
252
|
+
config.expiresAt = null;
|
|
253
|
+
saveConfig(config);
|
|
254
|
+
|
|
255
|
+
console.log('\n\x1b[32m✓ Successfully logged out.\x1b[0m\n');
|
|
256
|
+
} catch (error) {
|
|
257
|
+
config.token = null;
|
|
258
|
+
saveConfig(config);
|
|
259
|
+
console.log('\n\x1b[32m✓ Logged out locally.\x1b[0m\n');
|
|
260
|
+
}
|
|
261
|
+
process.exit(0);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function logoutAll() {
|
|
265
|
+
const config = loadConfig();
|
|
266
|
+
|
|
267
|
+
if (!config.token) {
|
|
268
|
+
console.error('\n\x1b[31m❌ Not logged in.\x1b[0m\n');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return new Promise((resolve) => {
|
|
273
|
+
rl.question('\x1b[33m⚠️ This will logout from ALL devices. Continue? [y/N]: \x1b[0m', async (answer) => {
|
|
274
|
+
if (answer.toLowerCase() !== 'y') {
|
|
275
|
+
console.log('\x1b[31mCancelled.\x1b[0m\n');
|
|
276
|
+
resolve();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const res = await fetch(BASE_URL + '/api/cli/logout', {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
headers: {
|
|
284
|
+
'Authorization': 'Bearer ' + config.token,
|
|
285
|
+
'Content-Type': 'application/json'
|
|
286
|
+
},
|
|
287
|
+
body: JSON.stringify({ allDevices: true })
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (res.ok) {
|
|
291
|
+
const data = await res.json();
|
|
292
|
+
config.token = null;
|
|
293
|
+
config.userEmail = null;
|
|
294
|
+
saveConfig(config);
|
|
295
|
+
console.log(`\n\x1b[32m✓ ${data.message}\x1b[0m\n`);
|
|
296
|
+
process.exit(0);
|
|
297
|
+
} else {
|
|
298
|
+
console.error('\n\x1b[31m❌ Logout failed.\x1b[0m\n');
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.error('\n\x1b[31m❌ Error during logout.\x1b[0m\n');
|
|
302
|
+
}
|
|
303
|
+
resolve();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function listSessions() {
|
|
309
|
+
const config = loadConfig();
|
|
310
|
+
|
|
311
|
+
if (!config.token) {
|
|
312
|
+
console.error('\n\x1b[31m❌ Not logged in.\x1b[0m\n');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const res = await fetch(BASE_URL + '/api/cli/sessions', {
|
|
318
|
+
headers: {
|
|
319
|
+
'Authorization': 'Bearer ' + config.token
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (!res.ok) {
|
|
324
|
+
console.error('\n\x1b[31m❌ Failed to fetch sessions.\x1b[0m\n');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const data = await res.json();
|
|
329
|
+
|
|
330
|
+
console.log('\n\x1b[36m📱 Active CLI Sessions:\x1b[0m');
|
|
331
|
+
console.log('\x1b[90m' + '─'.repeat(60) + '\x1b[0m');
|
|
332
|
+
|
|
333
|
+
data.sessions.forEach((session, i) => {
|
|
334
|
+
const isCurrent = session.current ? '\x1b[32m (current)\x1b[0m' : '';
|
|
335
|
+
const lastUsed = new Date(session.last_used_at).toLocaleString();
|
|
336
|
+
|
|
337
|
+
console.log(`${i + 1}. ${session.device_name} (${session.device_platform})${isCurrent}`);
|
|
338
|
+
console.log(` Last used: ${lastUsed}`);
|
|
339
|
+
console.log(` IP: ${session.ip_address || 'unknown'}`);
|
|
340
|
+
console.log('');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
console.log('\x1b[90m' + '─'.repeat(60) + '\x1b[0m');
|
|
344
|
+
console.log('\x1b[33m/logout-all - Logout from all devices\x1b[0m\n');
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error('\n\x1b[31m❌ Error fetching sessions.\x1b[0m\n');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ============================================
|
|
351
|
+
// TERMINAL COMMANDS
|
|
352
|
+
// ============================================
|
|
207
353
|
|
|
208
354
|
function runTerminalCommand(cmd, options = {}) {
|
|
209
355
|
return new Promise((resolve, reject) => {
|
|
@@ -251,7 +397,10 @@ function runTerminalCommand(cmd, options = {}) {
|
|
|
251
397
|
});
|
|
252
398
|
}
|
|
253
399
|
|
|
254
|
-
//
|
|
400
|
+
// ============================================
|
|
401
|
+
// CONTEXT & CACHE
|
|
402
|
+
// ============================================
|
|
403
|
+
|
|
255
404
|
function getLocalContext() {
|
|
256
405
|
try {
|
|
257
406
|
const cwd = process.cwd();
|
|
@@ -263,19 +412,17 @@ function getLocalContext() {
|
|
|
263
412
|
? JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'))
|
|
264
413
|
: null;
|
|
265
414
|
|
|
266
|
-
// FIX: Silent Git branch check - no error output
|
|
267
415
|
const gitBranch = (() => {
|
|
268
416
|
try {
|
|
269
|
-
// Check if .git directory exists first
|
|
270
417
|
if (fs.existsSync(path.join(cwd, '.git'))) {
|
|
271
418
|
return execSync('git branch --show-current', {
|
|
272
419
|
encoding: 'utf8',
|
|
273
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
420
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
274
421
|
}).trim();
|
|
275
422
|
}
|
|
276
423
|
return null;
|
|
277
424
|
} catch (e) {
|
|
278
|
-
return null;
|
|
425
|
+
return null;
|
|
279
426
|
}
|
|
280
427
|
})();
|
|
281
428
|
|
|
@@ -292,12 +439,11 @@ Shell: ${process.env.SHELL || 'unknown'}
|
|
|
292
439
|
}
|
|
293
440
|
}
|
|
294
441
|
|
|
295
|
-
// Cache management for search results
|
|
296
442
|
function getCachedSearch(query) {
|
|
297
443
|
const cacheFile = path.join(CACHE_DIR, `search_${crypto.createHash('md5').update(query).digest('hex')}.json`);
|
|
298
444
|
if (fs.existsSync(cacheFile)) {
|
|
299
445
|
const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
|
300
|
-
if (Date.now() - cache.timestamp < 3600000) {
|
|
446
|
+
if (Date.now() - cache.timestamp < 3600000) {
|
|
301
447
|
return cache.results;
|
|
302
448
|
}
|
|
303
449
|
}
|
|
@@ -313,59 +459,21 @@ function cacheSearchResults(query, results) {
|
|
|
313
459
|
}));
|
|
314
460
|
}
|
|
315
461
|
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// Check if session is recent (within 24h)
|
|
321
|
-
if (config.lastLogin && Date.now() - config.lastLogin < 86400000) {
|
|
322
|
-
return true;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
try {
|
|
326
|
-
const res = await fetch(BASE_URL + '/api/cli/validate', {
|
|
327
|
-
method: 'POST',
|
|
328
|
-
headers: {
|
|
329
|
-
'Authorization': 'Bearer ' + config.token,
|
|
330
|
-
'Content-Type': 'application/json'
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
if (res.ok) {
|
|
335
|
-
config.lastLogin = Date.now();
|
|
336
|
-
saveConfig(config);
|
|
337
|
-
return true;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return false;
|
|
341
|
-
} catch (e) {
|
|
342
|
-
// Network error - assume valid if we have a token
|
|
343
|
-
return true;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
462
|
+
// ============================================
|
|
463
|
+
// MAIN CHAT FUNCTION
|
|
464
|
+
// ============================================
|
|
346
465
|
|
|
347
466
|
async function chat(prompt, depth = 0) {
|
|
348
467
|
const config = loadConfig();
|
|
468
|
+
const deviceInfo = getDeviceInfo();
|
|
349
469
|
|
|
350
|
-
// FIXED: Better session validation
|
|
351
470
|
if (!config.token) {
|
|
352
|
-
console.error('\x1b[31m❌ Not logged in. Run "noxyai login" first\x1b[0m');
|
|
471
|
+
console.error('\n\x1b[31m❌ Not logged in. Run "noxyai login" first\x1b[0m');
|
|
353
472
|
return;
|
|
354
473
|
}
|
|
355
474
|
|
|
356
|
-
// FIXED: Don't immediately invalidate session on network errors
|
|
357
|
-
const isValid = await validateSession(config);
|
|
358
|
-
if (!isValid) {
|
|
359
|
-
console.error('\x1b[31m❌ Session expired. Please login again.\x1b[0m');
|
|
360
|
-
config.token = null;
|
|
361
|
-
config.userEmail = null;
|
|
362
|
-
config.lastLogin = null;
|
|
363
|
-
saveConfig(config);
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
475
|
if (depth > 8) {
|
|
368
|
-
console.error('\n\x1b[31m❌ Maximum recursion depth reached
|
|
476
|
+
console.error('\n\x1b[31m❌ Maximum recursion depth reached.\x1b[0m');
|
|
369
477
|
return;
|
|
370
478
|
}
|
|
371
479
|
|
|
@@ -381,8 +489,10 @@ async function chat(prompt, depth = 0) {
|
|
|
381
489
|
headers: {
|
|
382
490
|
'Content-Type': 'application/json',
|
|
383
491
|
'Authorization': 'Bearer ' + config.token,
|
|
384
|
-
'X-Client-Version':
|
|
385
|
-
'X-Client-Platform':
|
|
492
|
+
'X-Client-Version': VERSION,
|
|
493
|
+
'X-Client-Platform': deviceInfo.platform,
|
|
494
|
+
'X-Client-Name': deviceInfo.name,
|
|
495
|
+
'User-Agent': deviceInfo.userAgent
|
|
386
496
|
},
|
|
387
497
|
body: JSON.stringify({
|
|
388
498
|
prompt: enhancedPrompt,
|
|
@@ -398,28 +508,33 @@ async function chat(prompt, depth = 0) {
|
|
|
398
508
|
|
|
399
509
|
stopSpinner();
|
|
400
510
|
|
|
511
|
+
// Handle token rotation from header
|
|
512
|
+
const newToken = res.headers.get('X-New-Token');
|
|
513
|
+
if (newToken) {
|
|
514
|
+
config.token = newToken;
|
|
515
|
+
saveConfig(config);
|
|
516
|
+
console.log('\x1b[90m[Security] Token automatically rotated\x1b[0m');
|
|
517
|
+
}
|
|
518
|
+
|
|
401
519
|
if (!res.ok) {
|
|
402
520
|
const errorText = await res.text();
|
|
403
521
|
|
|
404
|
-
// FIXED: Better error handling
|
|
405
522
|
if (res.status === 401) {
|
|
406
|
-
console.error('\x1b[31m❌
|
|
407
|
-
console.error('\x1b[
|
|
523
|
+
console.error('\n\x1b[31m❌ Session invalid or expired.\x1b[0m');
|
|
524
|
+
console.error('\x1b[33mRun:\x1b[0m \x1b[36mnoxyai login\x1b[0m\n');
|
|
408
525
|
config.token = null;
|
|
409
|
-
config.userEmail = null;
|
|
410
|
-
config.lastLogin = null;
|
|
411
526
|
saveConfig(config);
|
|
412
527
|
return;
|
|
413
528
|
}
|
|
414
529
|
|
|
415
530
|
if (res.status === 402) {
|
|
416
|
-
console.error('\x1b[31m❌ Insufficient credits.\x1b[0m');
|
|
531
|
+
console.error('\n\x1b[31m❌ Insufficient credits.\x1b[0m');
|
|
417
532
|
console.error('\x1b[33mVisit:\x1b[0m \x1b[36mhttps://noxyai.com/pricing\x1b[0m\n');
|
|
418
533
|
return;
|
|
419
534
|
}
|
|
420
535
|
|
|
421
536
|
if (res.status === 429) {
|
|
422
|
-
console.error('\x1b[31m❌ Rate limit exceeded. Please wait
|
|
537
|
+
console.error('\x1b[31m❌ Rate limit exceeded. Please wait.\x1b[0m');
|
|
423
538
|
return;
|
|
424
539
|
}
|
|
425
540
|
|
|
@@ -433,12 +548,11 @@ async function chat(prompt, depth = 0) {
|
|
|
433
548
|
const decoder = new TextDecoder('utf-8');
|
|
434
549
|
let fullResponse = "";
|
|
435
550
|
|
|
436
|
-
// UI State for Boxed Reasoning
|
|
437
551
|
let isFirstReasoning = true;
|
|
438
552
|
let reasoningClosed = false;
|
|
439
|
-
let actionsPerformed = [];
|
|
440
553
|
let totalCost = 0;
|
|
441
554
|
let totalTokens = 0;
|
|
555
|
+
let actionsPerformed = 0;
|
|
442
556
|
|
|
443
557
|
while (true) {
|
|
444
558
|
const { done, value } = await reader.read();
|
|
@@ -454,7 +568,12 @@ async function chat(prompt, depth = 0) {
|
|
|
454
568
|
try {
|
|
455
569
|
const parsed = JSON.parse(data);
|
|
456
570
|
|
|
457
|
-
//
|
|
571
|
+
// Handle token rotation in stream
|
|
572
|
+
if (parsed.token_rotated) {
|
|
573
|
+
config.token = parsed.token_rotated;
|
|
574
|
+
saveConfig(config);
|
|
575
|
+
}
|
|
576
|
+
|
|
458
577
|
if (parsed.isReasoning) {
|
|
459
578
|
if (isFirstReasoning) {
|
|
460
579
|
process.stdout.write('\n\x1b[35m┌── 🧠 Deep Reasoning ─────────────────────\x1b[0m\n\x1b[31m│ \x1b[0m');
|
|
@@ -483,15 +602,12 @@ async function chat(prompt, depth = 0) {
|
|
|
483
602
|
}
|
|
484
603
|
console.log(`\x1b[90m${'─'.repeat(40)}\x1b[0m\n`);
|
|
485
604
|
} else if (parsed.actions) {
|
|
486
|
-
// Real-time action updates
|
|
487
605
|
parsed.actions.forEach(action => {
|
|
488
606
|
const icon = { search: '🔍', read: '📖', execute: '⚡', file: '📝', notify: '🔔' }[action.type] || '•';
|
|
489
607
|
console.log(`\x1b[36m${icon} Agent Action:\x1b[0m ${action.type}`);
|
|
490
608
|
});
|
|
491
609
|
}
|
|
492
|
-
} catch (e) {
|
|
493
|
-
// Ignore parsing errors
|
|
494
|
-
}
|
|
610
|
+
} catch (e) {}
|
|
495
611
|
}
|
|
496
612
|
}
|
|
497
613
|
}
|
|
@@ -520,8 +636,6 @@ async function chat(prompt, depth = 0) {
|
|
|
520
636
|
spawn('termux-notification', ['-t', 'NoxyAI', '-c', msg], { stdio: 'ignore' });
|
|
521
637
|
} else if (platform === 'darwin') {
|
|
522
638
|
spawn('osascript', ['-e', `display notification "${msg.replace(/"/g, '\\"')}" with title "NoxyAI"`]);
|
|
523
|
-
} else if (platform === 'win32') {
|
|
524
|
-
spawn('powershell', ['-Command', `New-BurntToastNotification -Text "NoxyAI", "${msg}"`], { stdio: 'ignore' });
|
|
525
639
|
} else if (platform === 'linux') {
|
|
526
640
|
spawn('notify-send', ['NoxyAI', msg], { stdio: 'ignore' });
|
|
527
641
|
}
|
|
@@ -532,7 +646,6 @@ async function chat(prompt, depth = 0) {
|
|
|
532
646
|
while ((match = readRegex.exec(fullResponse)) !== null) {
|
|
533
647
|
const filePath = match[1].trim();
|
|
534
648
|
|
|
535
|
-
// Security check
|
|
536
649
|
if (filePath.includes('..') || filePath.includes('/etc/') || filePath.includes('.env')) {
|
|
537
650
|
console.log(`\x1b[31m⚠️ Security: Access denied to ${filePath}\x1b[0m`);
|
|
538
651
|
agentFeedback += `\n[SECURITY BLOCK: Cannot access ${filePath}]\n`;
|
|
@@ -551,13 +664,12 @@ async function chat(prompt, depth = 0) {
|
|
|
551
664
|
}
|
|
552
665
|
}
|
|
553
666
|
|
|
554
|
-
// Tool: Web Search
|
|
667
|
+
// Tool: Web Search
|
|
555
668
|
const searchRegex = /<search>([\s\S]*?)<\/search>/g;
|
|
556
669
|
while ((match = searchRegex.exec(fullResponse)) !== null) {
|
|
557
670
|
const query = match[1].trim();
|
|
558
671
|
console.log(`\x1b[35m🔍 Searching:\x1b[0m ${query}`);
|
|
559
672
|
|
|
560
|
-
// Check cache first
|
|
561
673
|
const cached = getCachedSearch(query);
|
|
562
674
|
if (cached) {
|
|
563
675
|
console.log(`\x1b[90m ↳ Using cached results (${cached.length} items)\x1b[0m`);
|
|
@@ -610,7 +722,6 @@ async function chat(prompt, depth = 0) {
|
|
|
610
722
|
while ((match = fileRegex.exec(fullResponse)) !== null) {
|
|
611
723
|
const filePath = match[1], content = match[2];
|
|
612
724
|
|
|
613
|
-
// Security checks
|
|
614
725
|
if (filePath.includes('..') || filePath.includes('/etc/') ||
|
|
615
726
|
filePath.includes('.env') || filePath.includes('.ssh/')) {
|
|
616
727
|
console.log(`\x1b[31m⚠️ Security: Write blocked for ${filePath}\x1b[0m`);
|
|
@@ -626,7 +737,6 @@ async function chat(prompt, depth = 0) {
|
|
|
626
737
|
fs.mkdirSync(dir, { recursive: true });
|
|
627
738
|
}
|
|
628
739
|
|
|
629
|
-
// Backup existing file
|
|
630
740
|
if (fs.existsSync(resolvedPath)) {
|
|
631
741
|
const backup = resolvedPath + '.bak';
|
|
632
742
|
fs.copyFileSync(resolvedPath, backup);
|
|
@@ -642,7 +752,7 @@ async function chat(prompt, depth = 0) {
|
|
|
642
752
|
}
|
|
643
753
|
}
|
|
644
754
|
|
|
645
|
-
// Tool: Execute Commands
|
|
755
|
+
// Tool: Execute Commands
|
|
646
756
|
const execRegex = /<execute>([\s\S]*?)<\/execute>/g;
|
|
647
757
|
const commands = [];
|
|
648
758
|
while ((match = execRegex.exec(fullResponse)) !== null) {
|
|
@@ -650,7 +760,6 @@ async function chat(prompt, depth = 0) {
|
|
|
650
760
|
}
|
|
651
761
|
|
|
652
762
|
for (const cmd of commands) {
|
|
653
|
-
// Security: Block dangerous commands
|
|
654
763
|
const dangerous = ['rm -rf /', 'sudo rm', 'dd if=', 'mkfs', ':(){', 'chmod 777 /', '> /dev/sda'];
|
|
655
764
|
if (dangerous.some(d => cmd.toLowerCase().includes(d))) {
|
|
656
765
|
console.log(`\x1b[31m🛑 BLOCKED:\x1b[0m Dangerous command prevented`);
|
|
@@ -672,7 +781,7 @@ async function chat(prompt, depth = 0) {
|
|
|
672
781
|
} else if (lower === 'a') {
|
|
673
782
|
config.autoApprove = true;
|
|
674
783
|
saveConfig(config);
|
|
675
|
-
console.log('\x1b[32m✓ Auto-approve enabled
|
|
784
|
+
console.log('\x1b[32m✓ Auto-approve enabled\x1b[0m');
|
|
676
785
|
} else if (lower === 'b') {
|
|
677
786
|
console.log('\x1b[36m⚡ Running in background...\x1b[0m');
|
|
678
787
|
try {
|
|
@@ -696,20 +805,17 @@ async function chat(prompt, depth = 0) {
|
|
|
696
805
|
}
|
|
697
806
|
}
|
|
698
807
|
|
|
699
|
-
// Reset auto-approve after session
|
|
700
808
|
if (config.autoApprove) {
|
|
701
809
|
config.autoApprove = false;
|
|
702
810
|
saveConfig(config);
|
|
703
811
|
}
|
|
704
812
|
|
|
705
|
-
// Recursive feedback loop
|
|
706
813
|
if (agentFeedback) {
|
|
707
814
|
console.log(`\n\x1b[33m🔄 Processing results with Agent...\x1b[0m`);
|
|
708
815
|
await chat(
|
|
709
816
|
`Action results:\n${agentFeedback}\n\n` +
|
|
710
817
|
`Based on these results, what should be the next step? ` +
|
|
711
|
-
`Respond with appropriate XML tags
|
|
712
|
-
`or "TASK_COMPLETE" if finished.`,
|
|
818
|
+
`Respond with appropriate XML tags or "TASK_COMPLETE" if finished.`,
|
|
713
819
|
depth + 1
|
|
714
820
|
);
|
|
715
821
|
return;
|
|
@@ -722,10 +828,14 @@ async function chat(prompt, depth = 0) {
|
|
|
722
828
|
} catch (error) {
|
|
723
829
|
stopSpinner();
|
|
724
830
|
console.error('\n\x1b[31m❌ Connection error:\x1b[0m ' + error.message);
|
|
725
|
-
console.error('\x1b[33mTip: Check your internet connection
|
|
831
|
+
console.error('\x1b[33mTip: Check your internet connection.\x1b[0m');
|
|
726
832
|
}
|
|
727
833
|
}
|
|
728
834
|
|
|
835
|
+
// ============================================
|
|
836
|
+
// INTERACTIVE MODE
|
|
837
|
+
// ============================================
|
|
838
|
+
|
|
729
839
|
function startInteractiveMode() {
|
|
730
840
|
const config = loadConfig();
|
|
731
841
|
printLogo();
|
|
@@ -737,12 +847,14 @@ function startInteractiveMode() {
|
|
|
737
847
|
}
|
|
738
848
|
|
|
739
849
|
console.log(`\x1b[33mWelcome back${config.userEmail ? ', ' + config.userEmail : ''}!\x1b[0m`);
|
|
740
|
-
console.log(`\n\x1b[90m${'─'.repeat(
|
|
741
|
-
console.log(`Model: \x1b[32m${config.model}\x1b[0m`);
|
|
742
|
-
console.log(`
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
850
|
+
console.log(`\n\x1b[90m${'─'.repeat(45)}\x1b[0m`);
|
|
851
|
+
console.log(`Model: \x1b[32m${config.model}\x1b[0m | Agent: ${config.agentMode ? '\x1b[32m● ON\x1b[0m' : '\x1b[31m○ OFF\x1b[0m'} | Safe: ${config.safeMode ? '\x1b[32m● ON\x1b[0m' : '\x1b[33m○ OFF\x1b[0m'}`);
|
|
852
|
+
console.log(`Device: ${config.deviceName || 'unknown'} (${config.devicePlatform || 'unknown'})`);
|
|
853
|
+
if (config.expiresAt) {
|
|
854
|
+
console.log(`Expires: ${new Date(config.expiresAt).toLocaleDateString()}`);
|
|
855
|
+
}
|
|
856
|
+
console.log(`\x1b[90m${'─'.repeat(45)}\x1b[0m`);
|
|
857
|
+
console.log(`\nCommands: \x1b[36m/model\x1b[0m \x1b[36m/agent\x1b[0m \x1b[36m/safe\x1b[0m \x1b[36m/clear\x1b[0m \x1b[36m/status\x1b[0m \x1b[36m/sessions\x1b[0m \x1b[36m/exit\x1b[0m`);
|
|
746
858
|
console.log(`Type your question or task below.\n`);
|
|
747
859
|
|
|
748
860
|
function ask() {
|
|
@@ -761,24 +873,33 @@ function startInteractiveMode() {
|
|
|
761
873
|
const c = loadConfig();
|
|
762
874
|
c.agentMode = !c.agentMode;
|
|
763
875
|
saveConfig(c);
|
|
764
|
-
console.log(`\n\x1b[33mAgent Mode: ${c.agentMode ? '\x1b[32m● ON (File/Web/Execute
|
|
876
|
+
console.log(`\n\x1b[33mAgent Mode: ${c.agentMode ? '\x1b[32m● ON (File/Web/Execute)\x1b[0m' : '\x1b[31m○ OFF (Chat only)\x1b[0m'}\n`);
|
|
765
877
|
ask();
|
|
766
878
|
} else if (lower === '/safe') {
|
|
767
879
|
const c = loadConfig();
|
|
768
880
|
c.safeMode = !c.safeMode;
|
|
769
881
|
saveConfig(c);
|
|
770
|
-
console.log(`\n\x1b[33mSafe Mode: ${c.safeMode ? '\x1b[32m● ON (
|
|
882
|
+
console.log(`\n\x1b[33mSafe Mode: ${c.safeMode ? '\x1b[32m● ON (Confirm commands)\x1b[0m' : '\x1b[33m○ OFF (Auto-execute)\x1b[0m'}\n`);
|
|
771
883
|
ask();
|
|
772
884
|
} else if (lower === '/status') {
|
|
773
885
|
const c = loadConfig();
|
|
774
|
-
console.log(`\n\x1b[90m${'─'.repeat(
|
|
886
|
+
console.log(`\n\x1b[90m${'─'.repeat(45)}\x1b[0m`);
|
|
775
887
|
console.log(`\x1b[36mSession Status:\x1b[0m`);
|
|
776
888
|
console.log(` • User: ${c.userEmail || 'Unknown'}`);
|
|
777
889
|
console.log(` • Model: ${c.model}`);
|
|
778
|
-
console.log(` • Agent
|
|
779
|
-
console.log(` • Safe
|
|
890
|
+
console.log(` • Agent: ${c.agentMode ? 'ON' : 'OFF'}`);
|
|
891
|
+
console.log(` • Safe: ${c.safeMode ? 'ON' : 'OFF'}`);
|
|
780
892
|
console.log(` • CWD: ${process.cwd()}`);
|
|
781
|
-
console.log(
|
|
893
|
+
console.log(` • Device: ${c.deviceName || 'unknown'} (${c.devicePlatform || 'unknown'})`);
|
|
894
|
+
console.log(` • Expires: ${c.expiresAt ? new Date(c.expiresAt).toLocaleDateString() : 'N/A'}`);
|
|
895
|
+
console.log(`\x1b[90m${'─'.repeat(45)}\x1b[0m\n`);
|
|
896
|
+
ask();
|
|
897
|
+
} else if (lower === '/sessions') {
|
|
898
|
+
await listSessions();
|
|
899
|
+
ask();
|
|
900
|
+
} else if (lower === '/logout-all') {
|
|
901
|
+
await logoutAll();
|
|
902
|
+
if (!loadConfig().token) process.exit(0);
|
|
782
903
|
ask();
|
|
783
904
|
} else if (lower === '/model') {
|
|
784
905
|
console.log('\n\x1b[33mSelect AI Model:\x1b[0m');
|
|
@@ -811,48 +932,57 @@ function startInteractiveMode() {
|
|
|
811
932
|
ask();
|
|
812
933
|
}
|
|
813
934
|
|
|
814
|
-
//
|
|
935
|
+
// ============================================
|
|
936
|
+
// CLI ROUTER
|
|
937
|
+
// ============================================
|
|
938
|
+
|
|
815
939
|
if (command === 'login') {
|
|
816
940
|
login();
|
|
817
|
-
} else if (command === 'logout') {
|
|
818
|
-
|
|
819
|
-
c.token = null;
|
|
820
|
-
c.userEmail = null;
|
|
821
|
-
c.lastLogin = null;
|
|
822
|
-
saveConfig(c);
|
|
823
|
-
console.log('\x1b[32m✓ Successfully logged out.\x1b[0m\n');
|
|
824
|
-
process.exit(0);
|
|
941
|
+
} else if (command === 'logout') {
|
|
942
|
+
logout();
|
|
825
943
|
} else if (command === 'help' || command === '--help' || command === '-h') {
|
|
826
944
|
printLogo();
|
|
827
945
|
console.log(`
|
|
828
|
-
\x1b[33mNoxyAI -
|
|
946
|
+
\x1b[33mNoxyAI - Secure Autonomous Terminal Agent\x1b[0m
|
|
829
947
|
|
|
830
948
|
\x1b[36mUsage:\x1b[0m
|
|
831
949
|
noxyai Start interactive mode
|
|
832
|
-
noxyai login Authenticate with
|
|
833
|
-
noxyai logout
|
|
950
|
+
noxyai login Authenticate with device code
|
|
951
|
+
noxyai logout Logout from this device
|
|
834
952
|
noxyai help Show this help message
|
|
953
|
+
noxyai version Show version
|
|
835
954
|
|
|
836
955
|
\x1b[36mInteractive Commands:\x1b[0m
|
|
837
|
-
/model Switch AI model
|
|
838
|
-
/agent Toggle agent mode (file/web/execute
|
|
956
|
+
/model Switch AI model (Auto/Qwen3/Mamba)
|
|
957
|
+
/agent Toggle agent mode (file/web/execute)
|
|
839
958
|
/safe Toggle safe mode (command confirmation)
|
|
840
959
|
/status Show current session status
|
|
960
|
+
/sessions List all active CLI sessions
|
|
961
|
+
/logout-all Logout from ALL devices
|
|
841
962
|
/clear Clear the screen
|
|
842
963
|
/exit Exit the program
|
|
843
964
|
|
|
844
965
|
\x1b[36mExamples:\x1b[0m
|
|
845
966
|
$ noxyai login
|
|
846
967
|
$ noxyai
|
|
847
|
-
> Create a React component
|
|
848
|
-
> Search for Python
|
|
968
|
+
> Create a React component
|
|
969
|
+
> Search for Python async best practices
|
|
849
970
|
> Optimize this Dockerfile
|
|
850
971
|
|
|
972
|
+
\x1b[36mSecurity Features:\x1b[0m
|
|
973
|
+
• Device fingerprint binding
|
|
974
|
+
• Automatic token rotation (7 days)
|
|
975
|
+
• 30-day session expiration
|
|
976
|
+
• Local token encryption
|
|
977
|
+
• Rate limiting protection
|
|
978
|
+
|
|
851
979
|
\x1b[90mDocumentation: https://noxyai.com/docs\x1b[0m
|
|
852
980
|
`);
|
|
853
981
|
process.exit(0);
|
|
854
982
|
} else if (command === 'version' || command === '--version' || command === '-v') {
|
|
855
|
-
console.log(
|
|
983
|
+
console.log(`NoxyAI CLI v${VERSION}`);
|
|
984
|
+
console.log(`Platform: ${os.platform()} ${os.release()}`);
|
|
985
|
+
console.log(`Node: ${process.version}`);
|
|
856
986
|
process.exit(0);
|
|
857
987
|
} else if (!command) {
|
|
858
988
|
startInteractiveMode();
|
|
@@ -872,3 +1002,14 @@ if (command === 'login') {
|
|
|
872
1002
|
|
|
873
1003
|
chat(query).then(() => process.exit(0));
|
|
874
1004
|
}
|
|
1005
|
+
|
|
1006
|
+
// Handle Ctrl+C gracefully
|
|
1007
|
+
process.on('SIGINT', () => {
|
|
1008
|
+
console.log('\n\n\x1b[36mGoodbye! 👋\x1b[0m\n');
|
|
1009
|
+
process.exit(0);
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
process.on('SIGTERM', () => {
|
|
1013
|
+
console.log('\n\n\x1b[36mSession terminated.\x1b[0m\n');
|
|
1014
|
+
process.exit(0);
|
|
1015
|
+
});
|