npm-noxyai 1.0.17 → 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 +613 -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,256 @@ 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
+ });
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`);
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').toString())
133
+ execSync(`echo "${userCode}" | xclip -selection clipboard`);
134
+ console.log('\x1b[32m✓ Code copied to clipboard!\x1b[0m');
135
+ } catch (e) {}
57
136
 
58
- rl.question('Press ENTER to automatically open the browser...', () => {
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' });
148
+
62
149
  startSpinner('Waiting for authentication...');
63
150
  });
64
151
 
152
+ const startTime = Date.now();
153
+ const maxTime = expiresIn * 1000;
65
154
  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);
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);
76
160
  }
77
- }, interval * 1000);
78
- } catch (error) { console.error('Failed to connect.', error.message); }
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
194
+ }
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
+ }
79
201
  }
80
202
 
81
203
  const askConfirm = (query) => new Promise(resolve => rl.question(query, resolve));
82
204
 
83
- function runTerminalCommand(cmd) {
205
+ function runTerminalCommand(cmd, options = {}) {
84
206
  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) {
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) {
89
230
  console.log(`\x1b[36m[i] Server detected. Running in foreground. Press Ctrl+C to stop.\x1b[0m`);
90
- setTimeout(() => resolve(), 2500);
231
+ setTimeout(() => resolve({ server: true }), 2500);
91
232
  }
92
- child.on('close', (code) => { if (!isServer) { if (code !== 0) reject(`Exit code ${code}`); else resolve(); } });
93
- child.on('error', (error) => reject(error.message));
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());
238
+ }
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));
94
248
  });
95
249
  }
96
250
 
251
+ // Enhanced context gathering
97
252
  function getLocalContext() {
98
253
  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 ""; }
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
+ }));
102
301
  }
103
302
 
104
303
  async function chat(prompt, depth = 0) {
105
304
  const config = loadConfig();
106
- if (!config.token) { console.error('❌ Unauthorized: Run "noxyai login"'); return; }
107
- 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
+ }
108
314
 
109
- const enhancedPrompt = (depth === 0 && config.agentMode) ? getLocalContext() + prompt : prompt;
110
- 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...');
111
320
 
112
321
  try {
113
322
  const res = await fetch(BASE_URL + '/api/io', {
114
323
  method: 'POST',
115
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + config.token },
116
- 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
+ })
117
340
  });
118
341
 
119
342
  stopSpinner();
120
- 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
+
121
359
  console.log(`\n🤖 \x1b[36mNoxyAI (${config.model}):\x1b[0m\n`);
122
360
 
123
361
  const reader = res.body.getReader();
@@ -127,22 +365,28 @@ async function chat(prompt, depth = 0) {
127
365
  // UI State for Boxed Reasoning
128
366
  let isFirstReasoning = true;
129
367
  let reasoningClosed = false;
368
+ let actionsPerformed = [];
369
+ let totalCost = 0;
370
+ let totalTokens = 0;
130
371
 
131
372
  while (true) {
132
373
  const { done, value } = await reader.read();
133
374
  if (done) break;
375
+
134
376
  const lines = decoder.decode(value, { stream: true }).split('\n');
377
+
135
378
  for (const line of lines) {
136
379
  if (line.startsWith('data: ')) {
137
380
  const data = line.slice(6).trim();
138
381
  if (data === '[DONE]') break;
382
+
139
383
  try {
140
384
  const parsed = JSON.parse(data);
141
385
 
142
- // 🚨 BOXED REASONING UI
386
+ // Boxed Reasoning UI
143
387
  if (parsed.isReasoning) {
144
388
  if (isFirstReasoning) {
145
- process.stdout.write('\n\x1b[35m┌── 🧠 NoxyAI Deep Analysis ────────────────\x1b[0m\n\x1b[31m│ \x1b[0m');
389
+ process.stdout.write('\n\x1b[35m┌── 🧠 Deep Reasoning ─────────────────────\x1b[0m\n\x1b[31m│ \x1b[0m');
146
390
  isFirstReasoning = false;
147
391
  }
148
392
  const text = parsed.text.replace(/\n/g, '\n\x1b[31m│ \x1b[0m');
@@ -155,9 +399,28 @@ async function chat(prompt, depth = 0) {
155
399
  fullResponse += parsed.text;
156
400
  process.stdout.write(parsed.text);
157
401
  } else if (parsed.cost) {
158
- console.log(`\n\n\x1b[33m💳 Tokens Used: ${parsed.tokens} | Low-Cost 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
+ });
159
420
  }
160
- } catch (e) {}
421
+ } catch (e) {
422
+ // Ignore parsing errors
423
+ }
161
424
  }
162
425
  }
163
426
  }
@@ -166,145 +429,373 @@ async function chat(prompt, depth = 0) {
166
429
  process.stdout.write('\n\x1b[35m└───────────────────────────────────────────\x1b[0m\n\n');
167
430
  }
168
431
 
169
- if (!config.agentMode) { console.log(); return; }
432
+ if (!config.agentMode) {
433
+ console.log();
434
+ return;
435
+ }
170
436
 
171
- console.log('\n\x1b[32m[Agent] Processing actions...\x1b[0m');
437
+ console.log('\x1b[32m[Agent] Executing actions...\x1b[0m');
172
438
  let agentFeedback = "";
173
439
 
174
- // Tool: Push Notifications
175
- const notifyRegex = /<notify>([\s\S]*?)<\/notify>/g; let match;
440
+ // Tool: Notifications
441
+ const notifyRegex = /<notify>([\s\S]*?)<\/notify>/g;
442
+ let match;
176
443
  while ((match = notifyRegex.exec(fullResponse)) !== null) {
177
444
  const msg = match[1].trim();
178
- console.log(`\x1b[32m🔔 Notification Sent:\x1b[0m ${msg}`);
445
+ console.log(`\x1b[32m🔔 Notification:\x1b[0m ${msg}`);
446
+
179
447
  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"`]);
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
+ }
182
457
  }
183
458
 
184
- // Tool: Read
459
+ // Tool: Read Files
185
460
  const readRegex = /<read>([\s\S]*?)<\/read>/g;
186
461
  while ((match = readRegex.exec(fullResponse)) !== null) {
187
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
+
188
471
  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`; }
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
+ }
193
481
  }
194
482
 
195
- // Tool: Search
483
+ // Tool: Web Search (Enhanced with Google)
196
484
  const searchRegex = /<search>([\s\S]*?)<\/search>/g;
197
485
  while ((match = searchRegex.exec(fullResponse)) !== null) {
198
486
  const query = match[1].trim();
199
- 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
+
200
500
  try {
201
501
  const searchRes = await fetch(BASE_URL + '/api/search', {
202
502
  method: 'POST',
203
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + config.token },
204
- body: JSON.stringify({ query })
503
+ headers: {
504
+ 'Content-Type': 'application/json',
505
+ 'Authorization': 'Bearer ' + config.token
506
+ },
507
+ body: JSON.stringify({ query, num: 5 })
205
508
  });
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');
509
+
510
+ const searchData = await searchRes.json();
511
+
512
+ if (!searchRes.ok) {
513
+ throw new Error(searchData.error || `HTTP ${searchRes.status}`);
514
+ }
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`;
219
530
  }
220
- agentFeedback += `\n[SEARCH RESULTS FOR "${query}"]\n${snippets}\n`;
221
531
  } catch (err) {
222
- console.log(`\x1b[31m❌ Search Failed:\x1b[0m ${err.message}`);
223
- agentFeedback += `\n[SEARCH FAILED: ${err.message}]\n`;
532
+ console.log(`\x1b[31m❌ Search failed:\x1b[0m ${err.message}`);
533
+ agentFeedback += `\n[SEARCH ERROR: ${err.message}]\n`;
224
534
  }
225
535
  }
226
536
 
227
- // Tool: Write
537
+ // Tool: Write Files
228
538
  const fileRegex = /<file path="([^"]+)">([\s\S]*?)<\/file>/g;
229
539
  while ((match = fileRegex.exec(fullResponse)) !== null) {
230
540
  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}`);
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
+ }
235
572
  }
236
573
 
237
- // 🚨 SAFE EXECUTION LAYER
574
+ // Tool: Execute Commands (Safe Mode)
238
575
  const execRegex = /<execute>([\s\S]*?)<\/execute>/g;
239
576
  const commands = [];
240
- 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
+ }
241
580
 
242
581
  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`;
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`;
247
587
  continue;
248
588
  }
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`; }
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
+ }
626
+ }
627
+
628
+ // Reset auto-approve after session
629
+ if (config.autoApprove) {
630
+ config.autoApprove = false;
631
+ saveConfig(config);
251
632
  }
252
633
 
634
+ // Recursive feedback loop
253
635
  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);
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
+ );
256
644
  return;
257
645
  }
258
646
 
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');
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');
262
650
 
263
- } 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
+ }
264
655
  }
265
656
 
266
657
  function startInteractiveMode() {
267
658
  const config = loadConfig();
268
659
  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`);
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`);
272
675
 
273
676
  function ask() {
274
- rl.question('\x1b[36mNoxyAI > \x1b[0m', async (input) => {
677
+ rl.question('\x1b[36m⟫\x1b[0m ', async (input) => {
275
678
  const trimmed = input.trim();
276
679
  const lower = trimmed.toLowerCase();
277
680
 
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`);
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`);
283
693
  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) => {
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) => {
291
718
  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();
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();
296
728
  });
297
729
  return;
298
- }
299
- else if (trimmed) { await chat(trimmed); ask(); }
300
- else ask();
730
+ } else if (trimmed) {
731
+ await chat(trimmed);
732
+ ask();
733
+ } else {
734
+ ask();
735
+ }
301
736
  });
302
737
  }
738
+
303
739
  ask();
304
740
  }
305
741
 
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); }
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.17",
3
+ "version": "1.0.19",
4
4
  "description": "CLI for NoxyAI",
5
5
  "main": "index-noxyai.js",
6
6
  "bin": {