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.
Files changed (2) hide show
  1. package/index-noxyai.js +279 -138
  2. 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
- // Rate limiting store (in-memory, resets on CLI restart)
23
- const rateLimitStore = new Map();
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
- // Enhanced login with device flow
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: { 'Content-Type': 'application/json' }
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
- // Try to copy code to clipboard
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' && execSync('which xclip 2>/dev/null').toString())
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: { 'Content-Type': 'application/json' },
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.lastLogin = Date.now();
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\n`);
185
- console.log(`\x1b[90mType 'noxyai' to start the interactive shell.\x1b[0m\n`);
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
- const askConfirm = (query) => new Promise(resolve => rl.question(query, resolve));
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
- // FIXED: Safe context gathering without Git errors
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'] // Suppress stderr
420
+ stdio: ['pipe', 'pipe', 'pipe']
274
421
  }).trim();
275
422
  }
276
423
  return null;
277
424
  } catch (e) {
278
- return null; // Silently fail
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) { // 1 hour cache
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
- // FIXED: Better session validation
317
- async function validateSession(config) {
318
- if (!config.token) return false;
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. Breaking loop.\x1b[0m');
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': '1.0.0',
385
- 'X-Client-Platform': os.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❌ Invalid or expired session.\x1b[0m');
407
- console.error('\x1b[33mPlease run:\x1b[0m \x1b[36mnoxyai login\x1b[0m\n');
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 a moment.\x1b[0m');
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
- // Boxed Reasoning UI
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 (Enhanced with Google)
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 (Safe Mode)
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 for this session\x1b[0m');
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 (<read>, <search>, <file>, <execute>, <notify>) ` +
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 and try again.\x1b[0m');
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(40)}\x1b[0m`);
741
- console.log(`Model: \x1b[32m${config.model}\x1b[0m`);
742
- console.log(`Agent Mode: ${config.agentMode ? '\x1b[32m● ON\x1b[0m' : '\x1b[31m○ OFF\x1b[0m'}`);
743
- console.log(`Safe Mode: ${config.safeMode ? '\x1b[32m● ON\x1b[0m' : '\x1b[33m○ OFF\x1b[0m'}`);
744
- console.log(`\x1b[90m${'─'.repeat(40)}\x1b[0m`);
745
- 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/exit\x1b[0m`);
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 access)' : '\x1b[31m○ OFF (Chat only)'}\x1b[0m\n`);
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 (Command confirmation required)' : '\x1b[33m○ OFF (Auto-execute enabled - use with caution!)'}\x1b[0m\n`);
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(40)}\x1b[0m`);
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 Mode: ${c.agentMode ? 'ON' : 'OFF'}`);
779
- console.log(` • Safe Mode: ${c.safeMode ? 'ON' : 'OFF'}`);
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(`\x1b[90m${''.repeat(40)}\x1b[0m\n`);
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
- // CLI Command Router
935
+ // ============================================
936
+ // CLI ROUTER
937
+ // ============================================
938
+
815
939
  if (command === 'login') {
816
940
  login();
817
- } else if (command === 'logout') {
818
- const c = loadConfig();
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 - Your Autonomous Terminal Agent\x1b[0m
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 your NoxyAI account
833
- noxyai logout Remove local authentication
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 access)
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 for a login form
848
- > Search for Python asyncio best practices
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('NoxyAI CLI v1.0.1');
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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npm-noxyai",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "description": "CLI for NoxyAI",
5
5
  "main": "index-noxyai.js",
6
6
  "bin": {