npm-noxyai 1.0.17 → 1.0.20

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 +686 -122
  2. package/package.json +1 -1
package/index-noxyai.js CHANGED
@@ -3,23 +3,82 @@
3
3
  const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
- const { spawn } = require('child_process');
6
+ const { spawn, execSync } = require('child_process');
7
7
  const readline = require('readline');
8
+ const crypto = require('crypto');
8
9
 
9
10
  const BASE_URL = 'https://www.noxyai.com';
10
11
  const CONFIG_FILE = path.join(os.homedir(), '.noxyai.json');
12
+ const SESSION_FILE = path.join(os.homedir(), '.noxyai_session.json');
13
+ const CACHE_DIR = path.join(os.homedir(), '.noxyai_cache');
11
14
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
12
15
 
16
+ // Ensure cache directory exists
17
+ if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
18
+
13
19
  const args = process.argv.slice(2);
14
20
  const command = args[0];
15
21
 
22
+ // Rate limiting store (in-memory, resets on CLI restart)
23
+ const rateLimitStore = new Map();
24
+
16
25
  function loadConfig() {
17
26
  if (fs.existsSync(CONFIG_FILE)) {
18
- try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch(e){}
27
+ try {
28
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
29
+ // Decrypt token if it was encrypted
30
+ if (config.token && config.token.startsWith('enc:')) {
31
+ config.token = decryptToken(config.token.substring(4));
32
+ }
33
+ return config;
34
+ } catch(e) {
35
+ console.error('Config corruption detected, resetting...');
36
+ return { token: null, model: 'auto', agentMode: false, autoApprove: false, safeMode: true };
37
+ }
38
+ }
39
+ return { token: null, model: 'auto', agentMode: false, autoApprove: false, safeMode: true };
40
+ }
41
+
42
+ function saveConfig(config) {
43
+ // Encrypt token for security
44
+ const configToSave = { ...config };
45
+ if (configToSave.token && !configToSave.token.startsWith('enc:')) {
46
+ configToSave.token = 'enc:' + encryptToken(configToSave.token);
47
+ }
48
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(configToSave, null, 2));
49
+ }
50
+
51
+ // Simple token encryption using machine-specific key
52
+ function getMachineKey() {
53
+ const networkInterfaces = os.networkInterfaces();
54
+ const mac = Object.values(networkInterfaces)
55
+ .flat()
56
+ .find(i => !i.internal && i.mac !== '00:00:00:00:00:00')?.mac || os.hostname();
57
+ return crypto.createHash('sha256').update(mac + os.userInfo().username).digest('hex');
58
+ }
59
+
60
+ function encryptToken(token) {
61
+ const key = getMachineKey();
62
+ const iv = crypto.randomBytes(16);
63
+ const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
64
+ let encrypted = cipher.update(token, 'utf8', 'hex');
65
+ encrypted += cipher.final('hex');
66
+ return iv.toString('hex') + ':' + encrypted;
67
+ }
68
+
69
+ function decryptToken(encryptedData) {
70
+ try {
71
+ const key = getMachineKey();
72
+ const [ivHex, encrypted] = encryptedData.split(':');
73
+ const iv = Buffer.from(ivHex, 'hex');
74
+ const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
75
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
76
+ decrypted += decipher.final('utf8');
77
+ return decrypted;
78
+ } catch (e) {
79
+ return null;
19
80
  }
20
- return { token: null, model: 'auto', agentMode: false };
21
81
  }
22
- function saveConfig(config) { fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); }
23
82
 
24
83
  function printLogo() {
25
84
  console.log('\x1b[36m' + `
@@ -47,77 +106,327 @@ function stopSpinner() {
47
106
  process.stdout.write('\r\x1b[K\x1b[?25h');
48
107
  }
49
108
 
109
+ // Enhanced login with device flow
50
110
  async function login() {
51
111
  printLogo();
52
- console.log('Initializing secure login...\n');
112
+ console.log('\x1b[33m🔐 Initializing secure device login...\x1b[0m\n');
53
113
  try {
54
- const initRes = await fetch(BASE_URL + '/api/cli/init', { method: 'POST' });
55
- const { deviceCode, verificationUrl, interval } = await initRes.json();
56
- console.log(`URL: ${verificationUrl}\n`);
114
+ const initRes = await fetch(BASE_URL + '/api/cli/init', {
115
+ method: 'POST',
116
+ headers: { 'Content-Type': 'application/json' }
117
+ });
57
118
 
58
- rl.question('Press ENTER to automatically open the browser...', () => {
119
+ if (!initRes.ok) throw new Error(`Server error: ${initRes.status}`);
120
+
121
+ const { deviceCode, verificationUrl, userCode, expiresIn, interval } = await initRes.json();
122
+
123
+ console.log(`\x1b[32m📱 Verification Code:\x1b[0m \x1b[33m${userCode}\x1b[0m`);
124
+ console.log(`\x1b[34m🌐 Login URL:\x1b[0m ${verificationUrl}`);
125
+ console.log(`\n\x1b[90mThis code expires in ${Math.floor(expiresIn / 60)} minutes.\x1b[0m\n`);
126
+
127
+ // Try to copy code to clipboard
128
+ try {
129
+ const platform = os.platform();
130
+ if (platform === 'darwin') execSync(`echo "${userCode}" | pbcopy`);
131
+ 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`);
134
+ console.log('\x1b[32m✓ Code copied to clipboard!\x1b[0m');
135
+ } catch (e) {
136
+ // Clipboard not available, ignore
137
+ }
138
+
139
+ rl.question('\x1b[36mPress ENTER to open browser or type "q" to cancel:\x1b[0m ', (answer) => {
140
+ if (answer.toLowerCase() === 'q') {
141
+ console.log('\x1b[31mLogin cancelled.\x1b[0m');
142
+ process.exit(0);
143
+ }
144
+
59
145
  const platform = os.platform();
60
146
  if (platform === 'android') spawn('termux-open-url', [verificationUrl], { stdio: 'ignore' });
147
+ else if (platform === 'darwin') spawn('open', [verificationUrl], { stdio: 'ignore' });
148
+ else if (platform === 'win32') spawn('start', [verificationUrl], { shell: true, stdio: 'ignore' });
61
149
  else spawn('xdg-open', [verificationUrl], { stdio: 'ignore' });
150
+
62
151
  startSpinner('Waiting for authentication...');
63
152
  });
64
153
 
154
+ const startTime = Date.now();
155
+ const maxTime = expiresIn * 1000;
65
156
  const pollInterval = setInterval(async () => {
66
- const pollRes = await fetch(BASE_URL + '/api/cli/poll', { method: 'POST', body: JSON.stringify({ deviceCode }) });
67
- const data = await pollRes.json();
68
- if (data.status === 'success') {
69
- clearInterval(pollInterval); stopSpinner();
70
- const config = loadConfig(); config.token = data.token; saveConfig(config);
71
- console.clear(); printLogo();
72
- console.log('✅ Login successful! Type "noxyai" to start.\n'); process.exit(0);
73
- } else if (data.error) {
74
- clearInterval(pollInterval); stopSpinner();
75
- console.error('\n❌ Login failed: ' + data.error); process.exit(1);
157
+ if (Date.now() - startTime > maxTime) {
158
+ clearInterval(pollInterval);
159
+ stopSpinner();
160
+ console.log('\n\x1b[31m❌ Login timeout. Please try again.\x1b[0m');
161
+ process.exit(1);
76
162
  }
77
- }, interval * 1000);
78
- } catch (error) { console.error('Failed to connect.', error.message); }
163
+
164
+ try {
165
+ const pollRes = await fetch(BASE_URL + '/api/cli/poll', {
166
+ method: 'POST',
167
+ headers: { 'Content-Type': 'application/json' },
168
+ body: JSON.stringify({ deviceCode })
169
+ });
170
+
171
+ const data = await pollRes.json();
172
+
173
+ if (data.status === 'success') {
174
+ clearInterval(pollInterval);
175
+ stopSpinner();
176
+ const config = loadConfig();
177
+ config.token = data.token;
178
+ config.userEmail = data.email;
179
+ config.userId = data.userId;
180
+ config.lastLogin = Date.now();
181
+ saveConfig(config);
182
+ console.clear();
183
+ 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`);
186
+ process.exit(0);
187
+ } else if (data.status === 'pending') {
188
+ // Still waiting
189
+ } else if (data.error) {
190
+ clearInterval(pollInterval);
191
+ stopSpinner();
192
+ console.error(`\n\x1b[31m❌ Login failed: ${data.error}\x1b[0m`);
193
+ process.exit(1);
194
+ }
195
+ } catch (error) {
196
+ // Network errors shouldn't break the polling
197
+ }
198
+ }, (interval || 5) * 1000);
199
+ } catch (error) {
200
+ stopSpinner();
201
+ console.error('\x1b[31m❌ Failed to connect:', error.message, '\x1b[0m');
202
+ process.exit(1);
203
+ }
79
204
  }
80
205
 
81
206
  const askConfirm = (query) => new Promise(resolve => rl.question(query, resolve));
82
207
 
83
- function runTerminalCommand(cmd) {
208
+ function runTerminalCommand(cmd, options = {}) {
84
209
  return new Promise((resolve, reject) => {
85
- console.log(`\n\x1b[33m⚡ Running:\x1b[0m ${cmd}\n`);
86
- const isServer = cmd.includes('dev') || cmd.includes('serve') || cmd.includes('host') || cmd.includes('start');
87
- const child = spawn(cmd, { shell: true, stdio: 'inherit' });
88
- if (isServer) {
210
+ const { silent = false, background = false } = options;
211
+
212
+ if (!silent) {
213
+ console.log(`\n\x1b[33m⚡ Executing:\x1b[0m \x1b[36m${cmd}\x1b[0m\n`);
214
+ }
215
+
216
+ const isServer = cmd.includes('dev') || cmd.includes('serve') ||
217
+ cmd.includes('host') || cmd.includes('start') ||
218
+ cmd.includes('watch');
219
+
220
+ const child = spawn(cmd, {
221
+ shell: true,
222
+ stdio: silent ? 'pipe' : 'inherit',
223
+ detached: background
224
+ });
225
+
226
+ if (background) {
227
+ child.unref();
228
+ resolve({ pid: child.pid, background: true });
229
+ return;
230
+ }
231
+
232
+ if (isServer && !silent) {
89
233
  console.log(`\x1b[36m[i] Server detected. Running in foreground. Press Ctrl+C to stop.\x1b[0m`);
90
- setTimeout(() => resolve(), 2500);
234
+ setTimeout(() => resolve({ server: true }), 2500);
91
235
  }
92
- child.on('close', (code) => { if (!isServer) { if (code !== 0) reject(`Exit code ${code}`); else resolve(); } });
93
- child.on('error', (error) => reject(error.message));
236
+
237
+ let stdout = '', stderr = '';
238
+ if (silent) {
239
+ child.stdout?.on('data', (data) => stdout += data.toString());
240
+ child.stderr?.on('data', (data) => stderr += data.toString());
241
+ }
242
+
243
+ child.on('close', (code) => {
244
+ if (!isServer || silent) {
245
+ if (code !== 0) reject(new Error(stderr || `Exit code ${code}`));
246
+ else resolve({ code, stdout, stderr });
247
+ }
248
+ });
249
+
250
+ child.on('error', (error) => reject(error));
94
251
  });
95
252
  }
96
253
 
254
+ // FIXED: Safe context gathering without Git errors
97
255
  function getLocalContext() {
98
256
  try {
99
- const files = fs.readdirSync(process.cwd()).filter(f => !f.startsWith('node_modules') && !f.startsWith('.git')).slice(0, 30);
100
- return `\n[SYSTEM CONTEXT: You are in ${process.cwd()}. Files here: ${files.join(', ')}]\n`;
101
- } catch (e) { return ""; }
257
+ const cwd = process.cwd();
258
+ const files = fs.readdirSync(cwd)
259
+ .filter(f => !f.startsWith('.') && f !== 'node_modules' && f !== '__pycache__')
260
+ .slice(0, 50);
261
+
262
+ const packageJson = files.includes('package.json')
263
+ ? JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'))
264
+ : null;
265
+
266
+ // FIX: Silent Git branch check - no error output
267
+ const gitBranch = (() => {
268
+ try {
269
+ // Check if .git directory exists first
270
+ if (fs.existsSync(path.join(cwd, '.git'))) {
271
+ return execSync('git branch --show-current', {
272
+ encoding: 'utf8',
273
+ stdio: ['pipe', 'pipe', 'pipe'] // Suppress stderr
274
+ }).trim();
275
+ }
276
+ return null;
277
+ } catch (e) {
278
+ return null; // Silently fail
279
+ }
280
+ })();
281
+
282
+ return `\n[SYSTEM CONTEXT]
283
+ Directory: ${cwd}
284
+ Files: ${files.join(', ')}
285
+ ${packageJson ? `Project: ${packageJson.name || 'unnamed'} v${packageJson.version || '0.0.0'}` : ''}
286
+ ${gitBranch ? `Git Branch: ${gitBranch}` : ''}
287
+ OS: ${os.platform()} ${os.release()}
288
+ Shell: ${process.env.SHELL || 'unknown'}
289
+ [/SYSTEM CONTEXT]\n`;
290
+ } catch (e) {
291
+ return `\n[SYSTEM CONTEXT] Directory: ${process.cwd()}\n[/SYSTEM CONTEXT]\n`;
292
+ }
293
+ }
294
+
295
+ // Cache management for search results
296
+ function getCachedSearch(query) {
297
+ const cacheFile = path.join(CACHE_DIR, `search_${crypto.createHash('md5').update(query).digest('hex')}.json`);
298
+ if (fs.existsSync(cacheFile)) {
299
+ const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
300
+ if (Date.now() - cache.timestamp < 3600000) { // 1 hour cache
301
+ return cache.results;
302
+ }
303
+ }
304
+ return null;
305
+ }
306
+
307
+ function cacheSearchResults(query, results) {
308
+ const cacheFile = path.join(CACHE_DIR, `search_${crypto.createHash('md5').update(query).digest('hex')}.json`);
309
+ fs.writeFileSync(cacheFile, JSON.stringify({
310
+ timestamp: Date.now(),
311
+ query,
312
+ results
313
+ }));
314
+ }
315
+
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
+ }
102
345
  }
103
346
 
104
347
  async function chat(prompt, depth = 0) {
105
348
  const config = loadConfig();
106
- if (!config.token) { console.error('❌ Unauthorized: Run "noxyai login"'); return; }
107
- if (depth > 5) { console.error('\n❌ Reached max reasoning depth. Stopping to prevent loop.'); return; }
349
+
350
+ // FIXED: Better session validation
351
+ if (!config.token) {
352
+ console.error('\x1b[31m❌ Not logged in. Run "noxyai login" first\x1b[0m');
353
+ return;
354
+ }
355
+
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
+ if (depth > 8) {
368
+ console.error('\n\x1b[31m❌ Maximum recursion depth reached. Breaking loop.\x1b[0m');
369
+ return;
370
+ }
108
371
 
109
- const enhancedPrompt = (depth === 0 && config.agentMode) ? getLocalContext() + prompt : prompt;
110
- startSpinner(depth === 0 ? 'NoxyAI is thinking...' : 'NoxyAI is analyzing results...');
372
+ const enhancedPrompt = (depth === 0 && config.agentMode)
373
+ ? getLocalContext() + prompt
374
+ : prompt;
375
+
376
+ startSpinner(depth === 0 ? 'NoxyAI is thinking...' : 'NoxyAI is processing feedback...');
111
377
 
112
378
  try {
113
379
  const res = await fetch(BASE_URL + '/api/io', {
114
380
  method: 'POST',
115
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + config.token },
116
- body: JSON.stringify({ prompt: enhancedPrompt, model: config.model, agentMode: config.agentMode })
381
+ headers: {
382
+ 'Content-Type': 'application/json',
383
+ 'Authorization': 'Bearer ' + config.token,
384
+ 'X-Client-Version': '1.0.0',
385
+ 'X-Client-Platform': os.platform()
386
+ },
387
+ body: JSON.stringify({
388
+ prompt: enhancedPrompt,
389
+ model: config.model,
390
+ agentMode: config.agentMode,
391
+ context: {
392
+ cwd: process.cwd(),
393
+ platform: os.platform(),
394
+ shell: process.env.SHELL
395
+ }
396
+ })
117
397
  });
118
398
 
119
399
  stopSpinner();
120
- if (!res.ok) return console.error('\n❌ API Error: ' + await res.text());
400
+
401
+ if (!res.ok) {
402
+ const errorText = await res.text();
403
+
404
+ // FIXED: Better error handling
405
+ 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');
408
+ config.token = null;
409
+ config.userEmail = null;
410
+ config.lastLogin = null;
411
+ saveConfig(config);
412
+ return;
413
+ }
414
+
415
+ if (res.status === 402) {
416
+ console.error('\x1b[31m❌ Insufficient credits.\x1b[0m');
417
+ console.error('\x1b[33mVisit:\x1b[0m \x1b[36mhttps://noxyai.com/pricing\x1b[0m\n');
418
+ return;
419
+ }
420
+
421
+ if (res.status === 429) {
422
+ console.error('\x1b[31m❌ Rate limit exceeded. Please wait a moment.\x1b[0m');
423
+ return;
424
+ }
425
+
426
+ console.error(`\x1b[31m❌ API Error (${res.status}):\x1b[0m ${errorText.substring(0, 200)}`);
427
+ return;
428
+ }
429
+
121
430
  console.log(`\n🤖 \x1b[36mNoxyAI (${config.model}):\x1b[0m\n`);
122
431
 
123
432
  const reader = res.body.getReader();
@@ -127,22 +436,28 @@ async function chat(prompt, depth = 0) {
127
436
  // UI State for Boxed Reasoning
128
437
  let isFirstReasoning = true;
129
438
  let reasoningClosed = false;
439
+ let actionsPerformed = [];
440
+ let totalCost = 0;
441
+ let totalTokens = 0;
130
442
 
131
443
  while (true) {
132
444
  const { done, value } = await reader.read();
133
445
  if (done) break;
446
+
134
447
  const lines = decoder.decode(value, { stream: true }).split('\n');
448
+
135
449
  for (const line of lines) {
136
450
  if (line.startsWith('data: ')) {
137
451
  const data = line.slice(6).trim();
138
452
  if (data === '[DONE]') break;
453
+
139
454
  try {
140
455
  const parsed = JSON.parse(data);
141
456
 
142
- // 🚨 BOXED REASONING UI
457
+ // Boxed Reasoning UI
143
458
  if (parsed.isReasoning) {
144
459
  if (isFirstReasoning) {
145
- process.stdout.write('\n\x1b[35m┌── 🧠 NoxyAI Deep Analysis ────────────────\x1b[0m\n\x1b[31m│ \x1b[0m');
460
+ process.stdout.write('\n\x1b[35m┌── 🧠 Deep Reasoning ─────────────────────\x1b[0m\n\x1b[31m│ \x1b[0m');
146
461
  isFirstReasoning = false;
147
462
  }
148
463
  const text = parsed.text.replace(/\n/g, '\n\x1b[31m│ \x1b[0m');
@@ -155,9 +470,28 @@ async function chat(prompt, depth = 0) {
155
470
  fullResponse += parsed.text;
156
471
  process.stdout.write(parsed.text);
157
472
  } else if (parsed.cost) {
158
- console.log(`\n\n\x1b[33m💳 Tokens Used: ${parsed.tokens} | Low-Cost Credits Deducted: ${parsed.cost}\x1b[0m`);
473
+ totalCost = parsed.cost;
474
+ totalTokens = parsed.tokens;
475
+ actionsPerformed = parsed.actions_performed || 0;
476
+
477
+ console.log(`\n\n\x1b[90m${'─'.repeat(40)}\x1b[0m`);
478
+ console.log(`\x1b[33m📊 Session Stats:\x1b[0m`);
479
+ console.log(` • Tokens: ${totalTokens}`);
480
+ console.log(` • Credits: ${totalCost}`);
481
+ if (actionsPerformed > 0) {
482
+ console.log(` • Actions: ${actionsPerformed}`);
483
+ }
484
+ console.log(`\x1b[90m${'─'.repeat(40)}\x1b[0m\n`);
485
+ } else if (parsed.actions) {
486
+ // Real-time action updates
487
+ parsed.actions.forEach(action => {
488
+ const icon = { search: '🔍', read: '📖', execute: '⚡', file: '📝', notify: '🔔' }[action.type] || '•';
489
+ console.log(`\x1b[36m${icon} Agent Action:\x1b[0m ${action.type}`);
490
+ });
159
491
  }
160
- } catch (e) {}
492
+ } catch (e) {
493
+ // Ignore parsing errors
494
+ }
161
495
  }
162
496
  }
163
497
  }
@@ -166,145 +500,375 @@ async function chat(prompt, depth = 0) {
166
500
  process.stdout.write('\n\x1b[35m└───────────────────────────────────────────\x1b[0m\n\n');
167
501
  }
168
502
 
169
- if (!config.agentMode) { console.log(); return; }
503
+ if (!config.agentMode) {
504
+ console.log();
505
+ return;
506
+ }
170
507
 
171
- console.log('\n\x1b[32m[Agent] Processing actions...\x1b[0m');
508
+ console.log('\x1b[32m[Agent] Executing actions...\x1b[0m');
172
509
  let agentFeedback = "";
173
510
 
174
- // Tool: Push Notifications
175
- const notifyRegex = /<notify>([\s\S]*?)<\/notify>/g; let match;
511
+ // Tool: Notifications
512
+ const notifyRegex = /<notify>([\s\S]*?)<\/notify>/g;
513
+ let match;
176
514
  while ((match = notifyRegex.exec(fullResponse)) !== null) {
177
515
  const msg = match[1].trim();
178
- console.log(`\x1b[32m🔔 Notification Sent:\x1b[0m ${msg}`);
516
+ console.log(`\x1b[32m🔔 Notification:\x1b[0m ${msg}`);
517
+
179
518
  const platform = os.platform();
180
- if (platform === 'android') spawn('termux-notification', ['-c', msg]);
181
- else if (platform === 'darwin') spawn('osascript', ['-e', `display notification "${msg}" with title "NoxyAI"`]);
519
+ if (platform === 'android') {
520
+ spawn('termux-notification', ['-t', 'NoxyAI', '-c', msg], { stdio: 'ignore' });
521
+ } else if (platform === 'darwin') {
522
+ 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
+ } else if (platform === 'linux') {
526
+ spawn('notify-send', ['NoxyAI', msg], { stdio: 'ignore' });
527
+ }
182
528
  }
183
529
 
184
- // Tool: Read
530
+ // Tool: Read Files
185
531
  const readRegex = /<read>([\s\S]*?)<\/read>/g;
186
532
  while ((match = readRegex.exec(fullResponse)) !== null) {
187
533
  const filePath = match[1].trim();
534
+
535
+ // Security check
536
+ if (filePath.includes('..') || filePath.includes('/etc/') || filePath.includes('.env')) {
537
+ console.log(`\x1b[31m⚠️ Security: Access denied to ${filePath}\x1b[0m`);
538
+ agentFeedback += `\n[SECURITY BLOCK: Cannot access ${filePath}]\n`;
539
+ continue;
540
+ }
541
+
188
542
  try {
189
- const content = fs.readFileSync(filePath, 'utf8');
190
- console.log(`\x1b[34m📖 Read file:\x1b[0m ${filePath}`);
191
- agentFeedback += `\n[FILE: ${filePath}]\n\`\`\`\n${content}\n\`\`\`\n`;
192
- } catch (err) { agentFeedback += `\n[ERROR reading ${filePath}: ${err.message}]\n`; }
543
+ const resolvedPath = path.resolve(filePath);
544
+ const content = fs.readFileSync(resolvedPath, 'utf8');
545
+ const truncated = content.length > 5000 ? content.substring(0, 5000) + '\n... [truncated]' : content;
546
+ console.log(`\x1b[34m📖 Read:\x1b[0m ${filePath} (${content.length} chars)`);
547
+ agentFeedback += `\n[FILE: ${filePath}]\n\`\`\`\n${truncated}\n\`\`\`\n`;
548
+ } catch (err) {
549
+ console.log(`\x1b[31m❌ Read failed:\x1b[0m ${filePath} - ${err.message}`);
550
+ agentFeedback += `\n[ERROR reading ${filePath}: ${err.message}]\n`;
551
+ }
193
552
  }
194
553
 
195
- // Tool: Search
554
+ // Tool: Web Search (Enhanced with Google)
196
555
  const searchRegex = /<search>([\s\S]*?)<\/search>/g;
197
556
  while ((match = searchRegex.exec(fullResponse)) !== null) {
198
557
  const query = match[1].trim();
199
- console.log(`\x1b[35m🔍 Searching Web:\x1b[0m ${query}`);
558
+ console.log(`\x1b[35m🔍 Searching:\x1b[0m ${query}`);
559
+
560
+ // Check cache first
561
+ const cached = getCachedSearch(query);
562
+ if (cached) {
563
+ console.log(`\x1b[90m ↳ Using cached results (${cached.length} items)\x1b[0m`);
564
+ let snippets = cached.map((r, i) =>
565
+ `${i + 1}. **${r.title}**\n ${r.snippet}\n 🔗 ${r.link}`
566
+ ).join('\n\n');
567
+ agentFeedback += `\n[SEARCH RESULTS (cached): "${query}"]\n${snippets}\n`;
568
+ continue;
569
+ }
570
+
200
571
  try {
201
572
  const searchRes = await fetch(BASE_URL + '/api/search', {
202
573
  method: 'POST',
203
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + config.token },
204
- body: JSON.stringify({ query })
574
+ headers: {
575
+ 'Content-Type': 'application/json',
576
+ 'Authorization': 'Bearer ' + config.token
577
+ },
578
+ body: JSON.stringify({ query, num: 5 })
205
579
  });
206
- const responseText = await searchRes.text();
207
- let searchData;
208
- try { searchData = JSON.parse(responseText); }
209
- catch (e) { throw new Error("Search backend failed."); }
210
- if (!searchRes.ok) throw new Error(searchData.error || "Search failed");
211
-
212
- let snippets = "No results found.";
213
- if (searchData.items) {
214
- console.log(`\x1b[32m📚 Sources Extracted:\x1b[0m`);
215
- snippets = searchData.items.map(i => {
216
- console.log(` - ${i.title}\n \x1b[31m${i.link}\x1b[0m`);
217
- return `- Title: ${i.title}\n Snippet: ${i.snippet}\n URL: ${i.link}`;
218
- }).join('\n\n');
580
+
581
+ const searchData = await searchRes.json();
582
+
583
+ if (!searchRes.ok) {
584
+ throw new Error(searchData.error || `HTTP ${searchRes.status}`);
585
+ }
586
+
587
+ if (searchData.results && searchData.results.length > 0) {
588
+ cacheSearchResults(query, searchData.results);
589
+
590
+ console.log(`\x1b[32m📚 Found ${searchData.results.length} results:\x1b[0m`);
591
+ let snippets = "";
592
+ searchData.results.forEach((item, i) => {
593
+ console.log(` \x1b[36m${i + 1}.\x1b[0m ${item.title}`);
594
+ console.log(` \x1b[90m${item.link}\x1b[0m`);
595
+ snippets += `${i + 1}. **${item.title}**\n ${item.snippet}\n 🔗 ${item.link}\n\n`;
596
+ });
597
+ agentFeedback += `\n[SEARCH RESULTS: "${query}"]\n${snippets}\n`;
598
+ } else {
599
+ console.log(`\x1b[33m⚠️ No results found\x1b[0m`);
600
+ agentFeedback += `\n[SEARCH: No results for "${query}"]\n`;
219
601
  }
220
- agentFeedback += `\n[SEARCH RESULTS FOR "${query}"]\n${snippets}\n`;
221
602
  } catch (err) {
222
- console.log(`\x1b[31m❌ Search Failed:\x1b[0m ${err.message}`);
223
- agentFeedback += `\n[SEARCH FAILED: ${err.message}]\n`;
603
+ console.log(`\x1b[31m❌ Search failed:\x1b[0m ${err.message}`);
604
+ agentFeedback += `\n[SEARCH ERROR: ${err.message}]\n`;
224
605
  }
225
606
  }
226
607
 
227
- // Tool: Write
608
+ // Tool: Write Files
228
609
  const fileRegex = /<file path="([^"]+)">([\s\S]*?)<\/file>/g;
229
610
  while ((match = fileRegex.exec(fullResponse)) !== null) {
230
611
  const filePath = match[1], content = match[2];
231
- const dir = path.dirname(filePath);
232
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
233
- fs.writeFileSync(filePath, content.trim());
234
- console.log(`\x1b[32m✔ Wrote file:\x1b[0m ${filePath}`);
612
+
613
+ // Security checks
614
+ if (filePath.includes('..') || filePath.includes('/etc/') ||
615
+ filePath.includes('.env') || filePath.includes('.ssh/')) {
616
+ console.log(`\x1b[31m⚠️ Security: Write blocked for ${filePath}\x1b[0m`);
617
+ agentFeedback += `\n[SECURITY BLOCK: Cannot write to ${filePath}]\n`;
618
+ continue;
619
+ }
620
+
621
+ try {
622
+ const resolvedPath = path.resolve(filePath);
623
+ const dir = path.dirname(resolvedPath);
624
+
625
+ if (!fs.existsSync(dir)) {
626
+ fs.mkdirSync(dir, { recursive: true });
627
+ }
628
+
629
+ // Backup existing file
630
+ if (fs.existsSync(resolvedPath)) {
631
+ const backup = resolvedPath + '.bak';
632
+ fs.copyFileSync(resolvedPath, backup);
633
+ console.log(`\x1b[90m ↳ Backup created: ${path.basename(backup)}\x1b[0m`);
634
+ }
635
+
636
+ fs.writeFileSync(resolvedPath, content.trim());
637
+ console.log(`\x1b[32m✔ Created:\x1b[0m ${filePath} (${content.length} chars)`);
638
+ agentFeedback += `\n[FILE CREATED: ${filePath}]\n`;
639
+ } catch (err) {
640
+ console.log(`\x1b[31m❌ Write failed:\x1b[0m ${filePath} - ${err.message}`);
641
+ agentFeedback += `\n[ERROR writing ${filePath}: ${err.message}]\n`;
642
+ }
235
643
  }
236
644
 
237
- // 🚨 SAFE EXECUTION LAYER
645
+ // Tool: Execute Commands (Safe Mode)
238
646
  const execRegex = /<execute>([\s\S]*?)<\/execute>/g;
239
647
  const commands = [];
240
- while ((match = execRegex.exec(fullResponse)) !== null) commands.push(match[1].trim());
648
+ while ((match = execRegex.exec(fullResponse)) !== null) {
649
+ commands.push(match[1].trim());
650
+ }
241
651
 
242
652
  for (const cmd of commands) {
243
- const answer = await askConfirm(`\n\x1b[33m⚠️ Agent wants to run:\x1b[0m \x1b[36m${cmd}\x1b[0m\nAllow? [Y/n]: `);
244
- if (answer.toLowerCase() === 'n') {
245
- console.log('\x1b[31mCommand skipped by user.\x1b[0m');
246
- agentFeedback += `\n[USER DENIED COMMAND: ${cmd}]\n`;
653
+ // Security: Block dangerous commands
654
+ const dangerous = ['rm -rf /', 'sudo rm', 'dd if=', 'mkfs', ':(){', 'chmod 777 /', '> /dev/sda'];
655
+ if (dangerous.some(d => cmd.toLowerCase().includes(d))) {
656
+ console.log(`\x1b[31m🛑 BLOCKED:\x1b[0m Dangerous command prevented`);
657
+ agentFeedback += `\n[SECURITY BLOCK: Command rejected - ${cmd}]\n`;
247
658
  continue;
248
659
  }
249
- try { await runTerminalCommand(cmd); }
250
- catch (err) { console.log(`\x1b[31m❌ Command Failed:\x1b[0m ${err}`); agentFeedback += `\n[COMMAND ERROR running "${cmd}": ${err}]\n`; }
660
+
661
+ if (config.safeMode && !config.autoApprove) {
662
+ const answer = await askConfirm(
663
+ `\n\x1b[33m⚠️ Execute:\x1b[0m \x1b[36m${cmd}\x1b[0m\n` +
664
+ `\x1b[90m[Y]es / [N]o / [A]lways for session / [B]ackground:\x1b[0m `
665
+ );
666
+
667
+ const lower = answer.toLowerCase();
668
+ if (lower === 'n') {
669
+ console.log('\x1b[31m✗ Skipped\x1b[0m');
670
+ agentFeedback += `\n[USER SKIPPED: ${cmd}]\n`;
671
+ continue;
672
+ } else if (lower === 'a') {
673
+ config.autoApprove = true;
674
+ saveConfig(config);
675
+ console.log('\x1b[32m✓ Auto-approve enabled for this session\x1b[0m');
676
+ } else if (lower === 'b') {
677
+ console.log('\x1b[36m⚡ Running in background...\x1b[0m');
678
+ try {
679
+ const result = await runTerminalCommand(cmd, { background: true });
680
+ console.log(`\x1b[32m✓ Started (PID: ${result.pid})\x1b[0m`);
681
+ agentFeedback += `\n[COMMAND STARTED (background): ${cmd}]\n`;
682
+ } catch (err) {
683
+ console.log(`\x1b[31m❌ Failed:\x1b[0m ${err.message}`);
684
+ agentFeedback += `\n[ERROR: ${err.message}]\n`;
685
+ }
686
+ continue;
687
+ }
688
+ }
689
+
690
+ try {
691
+ await runTerminalCommand(cmd);
692
+ agentFeedback += `\n[COMMAND SUCCEEDED: ${cmd}]\n`;
693
+ } catch (err) {
694
+ console.log(`\x1b[31m❌ Failed:\x1b[0m ${err.message}`);
695
+ agentFeedback += `\n[COMMAND ERROR: ${cmd} - ${err.message}]\n`;
696
+ }
697
+ }
698
+
699
+ // Reset auto-approve after session
700
+ if (config.autoApprove) {
701
+ config.autoApprove = false;
702
+ saveConfig(config);
251
703
  }
252
704
 
705
+ // Recursive feedback loop
253
706
  if (agentFeedback) {
254
- console.log(`\x1b[33m🔄 Sending data back to Agent...\x1b[0m`);
255
- await chat(`Action results:\n${agentFeedback}\nNext step? Output <file>, <notify>, or <execute> if ready, or <read>/<search>.`, depth + 1);
707
+ console.log(`\n\x1b[33m🔄 Processing results with Agent...\x1b[0m`);
708
+ await chat(
709
+ `Action results:\n${agentFeedback}\n\n` +
710
+ `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.`,
713
+ depth + 1
714
+ );
256
715
  return;
257
716
  }
258
717
 
259
- console.log('\n\x1b[32m════════════════════════════════════════\x1b[0m');
260
- console.log(`\x1b[32m✨ Task Completed!\x1b[0m`);
261
- console.log('\x1b[32m════════════════════════════════════════\x1b[0m\n');
718
+ console.log('\n\x1b[32m' + '═'.repeat(40) + '\x1b[0m');
719
+ console.log(`\x1b[32m✨ Task completed successfully!\x1b[0m`);
720
+ console.log('\x1b[32m' + '═'.repeat(40) + '\x1b[0m\n');
262
721
 
263
- } catch (error) { stopSpinner(); console.error('\n❌ Connection error: ' + error.message); }
722
+ } catch (error) {
723
+ stopSpinner();
724
+ 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');
726
+ }
264
727
  }
265
728
 
266
729
  function startInteractiveMode() {
267
730
  const config = loadConfig();
268
731
  printLogo();
269
- console.log(`\x1b[33mWelcome to NoxyAI Interactive OS!\x1b[0m`);
270
- console.log(`Model: \x1b[32m${config.model}\x1b[0m | Agent Mode: ${config.agentMode ? '\x1b[32mON\x1b[0m' : '\x1b[31mOFF\x1b[0m'}`);
271
- console.log(`Commands: \x1b[36m/model\x1b[0m, \x1b[36m/agent\x1b[0m, \x1b[36m/clear\x1b[0m, \x1b[36m/exit\x1b[0m\n`);
732
+
733
+ if (!config.token) {
734
+ console.log('\x1b[33mWelcome to NoxyAI!\x1b[0m');
735
+ console.log('\x1b[36mTo get started, run:\x1b[0m \x1b[32mnoxyai login\x1b[0m\n');
736
+ process.exit(0);
737
+ }
738
+
739
+ 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`);
746
+ console.log(`Type your question or task below.\n`);
272
747
 
273
748
  function ask() {
274
- rl.question('\x1b[36mNoxyAI > \x1b[0m', async (input) => {
749
+ rl.question('\x1b[36m⟫\x1b[0m ', async (input) => {
275
750
  const trimmed = input.trim();
276
751
  const lower = trimmed.toLowerCase();
277
752
 
278
- if (lower === 'exit' || lower === '/exit') process.exit(0);
279
- else if (lower === '/clear') { console.clear(); printLogo(); ask(); }
280
- else if (lower === '/agent') {
281
- const c = loadConfig(); c.agentMode = !c.agentMode; saveConfig(c);
282
- console.log(`\n\x1b[33mAgent Mode is now ${c.agentMode ? '\x1b[32mON (File/Web access enabled)' : '\x1b[31mOFF (Standard Chat)'}\x1b[0m\n`);
753
+ if (lower === 'exit' || lower === '/exit' || lower === '/quit') {
754
+ console.log('\n\x1b[36mGoodbye! 👋\x1b[0m\n');
755
+ process.exit(0);
756
+ } else if (lower === '/clear') {
757
+ console.clear();
758
+ printLogo();
759
+ ask();
760
+ } else if (lower === '/agent') {
761
+ const c = loadConfig();
762
+ c.agentMode = !c.agentMode;
763
+ 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`);
283
765
  ask();
284
- }
285
- else if (lower === '/model') {
286
- console.log('\n\x1b[33mSelect an AI Model:\x1b[0m');
287
- console.log(' \x1b[36m1)\x1b[0m Auto (Llama 3.3 70B)');
288
- console.log(' \x1b[36m2)\x1b[0m Qwen3 Next 80B (Deep Reasoning)');
289
- console.log(' \x1b[36m3)\x1b[0m Mistral Mamba Codestral 7B');
290
- rl.question('\nEnter number (1-3): ', (choice) => {
766
+ } else if (lower === '/safe') {
767
+ const c = loadConfig();
768
+ c.safeMode = !c.safeMode;
769
+ 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`);
771
+ ask();
772
+ } else if (lower === '/status') {
773
+ const c = loadConfig();
774
+ console.log(`\n\x1b[90m${'─'.repeat(40)}\x1b[0m`);
775
+ console.log(`\x1b[36mSession Status:\x1b[0m`);
776
+ console.log(` • User: ${c.userEmail || 'Unknown'}`);
777
+ console.log(` • Model: ${c.model}`);
778
+ console.log(` • Agent Mode: ${c.agentMode ? 'ON' : 'OFF'}`);
779
+ console.log(` • Safe Mode: ${c.safeMode ? 'ON' : 'OFF'}`);
780
+ console.log(` • CWD: ${process.cwd()}`);
781
+ console.log(`\x1b[90m${'─'.repeat(40)}\x1b[0m\n`);
782
+ ask();
783
+ } else if (lower === '/model') {
784
+ console.log('\n\x1b[33mSelect AI Model:\x1b[0m');
785
+ console.log(' \x1b[36m1)\x1b[0m Auto (Llama 3.3 70B) - Fast & Capable');
786
+ console.log(' \x1b[36m2)\x1b[0m Qwen3 Next 80B - Deep Reasoning');
787
+ console.log(' \x1b[36m3)\x1b[0m Mistral Mamba Codestral 7B - Code Optimized');
788
+
789
+ rl.question('\n\x1b[36mChoice (1-3):\x1b[0m ', (choice) => {
291
790
  let selected = 'auto';
292
- if (choice === '2') selected = 'Qwen3';
293
- if (choice === '3') selected = 'Mamba';
294
- const c = loadConfig(); c.model = selected; saveConfig(c);
295
- console.log(`\n\x1b[32m✔ Model changed to: ${selected}\x1b[0m\n`); ask();
791
+ let modelName = 'Auto (Llama 3.3 70B)';
792
+ if (choice === '2') { selected = 'Qwen3'; modelName = 'Qwen3 Next 80B'; }
793
+ if (choice === '3') { selected = 'Mamba'; modelName = 'Mistral Mamba Codestral 7B'; }
794
+
795
+ const c = loadConfig();
796
+ c.model = selected;
797
+ saveConfig(c);
798
+ console.log(`\n\x1b[32m✓ Model changed to: ${modelName}\x1b[0m\n`);
799
+ ask();
296
800
  });
297
801
  return;
298
- }
299
- else if (trimmed) { await chat(trimmed); ask(); }
300
- else ask();
802
+ } else if (trimmed) {
803
+ await chat(trimmed);
804
+ ask();
805
+ } else {
806
+ ask();
807
+ }
301
808
  });
302
809
  }
810
+
303
811
  ask();
304
812
  }
305
813
 
306
- if (command === 'login') login();
307
- else if (command === 'logout') { const c = loadConfig(); c.token = null; saveConfig(c); console.log('Logged out.'); process.exit(0); }
308
- else if (command === 'help' || command === '--help') { printLogo(); console.log(` \x1b[32mnoxyai\x1b[0m Start Interactive Mode\n`); process.exit(0); }
309
- else if (!command) startInteractiveMode();
310
- else { console.error(`❌ Unknown command.`); process.exit(1); }
814
+ // CLI Command Router
815
+ if (command === 'login') {
816
+ 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);
825
+ } else if (command === 'help' || command === '--help' || command === '-h') {
826
+ printLogo();
827
+ console.log(`
828
+ \x1b[33mNoxyAI - Your Autonomous Terminal Agent\x1b[0m
829
+
830
+ \x1b[36mUsage:\x1b[0m
831
+ noxyai Start interactive mode
832
+ noxyai login Authenticate with your NoxyAI account
833
+ noxyai logout Remove local authentication
834
+ noxyai help Show this help message
835
+
836
+ \x1b[36mInteractive Commands:\x1b[0m
837
+ /model Switch AI model
838
+ /agent Toggle agent mode (file/web/execute access)
839
+ /safe Toggle safe mode (command confirmation)
840
+ /status Show current session status
841
+ /clear Clear the screen
842
+ /exit Exit the program
843
+
844
+ \x1b[36mExamples:\x1b[0m
845
+ $ noxyai login
846
+ $ noxyai
847
+ > Create a React component for a login form
848
+ > Search for Python asyncio best practices
849
+ > Optimize this Dockerfile
850
+
851
+ \x1b[90mDocumentation: https://noxyai.com/docs\x1b[0m
852
+ `);
853
+ process.exit(0);
854
+ } else if (command === 'version' || command === '--version' || command === '-v') {
855
+ console.log('NoxyAI CLI v1.0.1');
856
+ process.exit(0);
857
+ } else if (!command) {
858
+ startInteractiveMode();
859
+ } else if (command.startsWith('-')) {
860
+ console.error(`\x1b[31m❌ Unknown option: ${command}\x1b[0m`);
861
+ console.error(`\x1b[90mRun 'noxyai help' for usage information.\x1b[0m\n`);
862
+ process.exit(1);
863
+ } else {
864
+ // Direct query mode: noxyai "your question here"
865
+ const query = args.join(' ');
866
+ const config = loadConfig();
867
+
868
+ if (!config.token) {
869
+ console.error('\x1b[31m❌ Not logged in. Run "noxyai login" first.\x1b[0m\n');
870
+ process.exit(1);
871
+ }
872
+
873
+ chat(query).then(() => process.exit(0));
874
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npm-noxyai",
3
- "version": "1.0.17",
3
+ "version": "1.0.20",
4
4
  "description": "CLI for NoxyAI",
5
5
  "main": "index-noxyai.js",
6
6
  "bin": {