npm-noxyai 1.0.16 → 1.0.19

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