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.
- package/index-noxyai.js +686 -122
- 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 {
|
|
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', {
|
|
55
|
-
|
|
56
|
-
|
|
114
|
+
const initRes = await fetch(BASE_URL + '/api/cli/init', {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: { 'Content-Type': 'application/json' }
|
|
117
|
+
});
|
|
57
118
|
|
|
58
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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)
|
|
110
|
-
|
|
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: {
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
457
|
+
// Boxed Reasoning UI
|
|
143
458
|
if (parsed.isReasoning) {
|
|
144
459
|
if (isFirstReasoning) {
|
|
145
|
-
process.stdout.write('\n\x1b[35m┌── 🧠
|
|
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
|
-
|
|
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) {
|
|
503
|
+
if (!config.agentMode) {
|
|
504
|
+
console.log();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
170
507
|
|
|
171
|
-
console.log('\
|
|
508
|
+
console.log('\x1b[32m[Agent] Executing actions...\x1b[0m');
|
|
172
509
|
let agentFeedback = "";
|
|
173
510
|
|
|
174
|
-
// Tool:
|
|
175
|
-
const notifyRegex = /<notify>([\s\S]*?)<\/notify>/g;
|
|
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
|
|
516
|
+
console.log(`\x1b[32m🔔 Notification:\x1b[0m ${msg}`);
|
|
517
|
+
|
|
179
518
|
const platform = os.platform();
|
|
180
|
-
if (platform === 'android')
|
|
181
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
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: {
|
|
204
|
-
|
|
574
|
+
headers: {
|
|
575
|
+
'Content-Type': 'application/json',
|
|
576
|
+
'Authorization': 'Bearer ' + config.token
|
|
577
|
+
},
|
|
578
|
+
body: JSON.stringify({ query, num: 5 })
|
|
205
579
|
});
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (searchData.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
223
|
-
agentFeedback += `\n[SEARCH
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
//
|
|
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)
|
|
648
|
+
while ((match = execRegex.exec(fullResponse)) !== null) {
|
|
649
|
+
commands.push(match[1].trim());
|
|
650
|
+
}
|
|
241
651
|
|
|
242
652
|
for (const cmd of commands) {
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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🔄
|
|
255
|
-
await chat(
|
|
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
|
|
260
|
-
console.log(`\x1b[32m✨ Task
|
|
261
|
-
console.log('\x1b[32m
|
|
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) {
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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[
|
|
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')
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
console.
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
console.log('
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
293
|
-
if (choice === '
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
else if (
|
|
310
|
-
|
|
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
|
+
}
|