avg-nexus 1.0.10 → 1.0.12

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/bin/avg-nexus.js +756 -119
  2. package/package.json +26 -26
package/bin/avg-nexus.js CHANGED
@@ -1,63 +1,259 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const { execSync, spawn } = require('child_process');
5
- const http = require('http');
6
- const path = require('path');
7
- const fs = require('fs');
4
+ // ═══════════════════════════════════════════════════════════════
5
+ // AVG-NEXUS v4.0 — NexusAI Local Proxy + API Key Manager
6
+ // • Ollama OpenRouter proxy (port 11435)
7
+ // • Device ID → API key tizimi
8
+ // • 10 ta OpenRouter key rotatsiya
9
+ // • Admin panel API (port 11436)
10
+ // • NexusAI saytni avtomatik ishga tushirish
11
+ // ═══════════════════════════════════════════════════════════════
8
12
 
13
+ const { execSync, spawn } = require('child_process');
14
+ const http = require('http');
15
+ const https = require('https');
16
+ const path = require('path');
17
+ const fs = require('fs');
18
+ const os = require('os');
19
+ const crypto = require('crypto');
20
+
21
+ // ── Ranglar ──────────────────────────────────────────────────
9
22
  const C = {
10
- reset: '\x1b[0m',
11
- green: '\x1b[32m',
12
- red: '\x1b[31m',
13
- yellow: '\x1b[33m',
14
- cyan: '\x1b[36m',
15
- bold: '\x1b[1m',
16
- dim: '\x1b[2m',
23
+ reset:'\x1b[0m', bold:'\x1b[1m', dim:'\x1b[2m',
24
+ red:'\x1b[31m', green:'\x1b[32m', yellow:'\x1b[33m',
25
+ cyan:'\x1b[36m', purple:'\x1b[35m', blue:'\x1b[34m',
26
+ gray:'\x1b[90m', white:'\x1b[37m',
17
27
  };
18
28
 
19
- function log(icon, msg, color = C.reset) {
29
+ const log = (icon, msg, color = C.reset) =>
20
30
  console.log(`${color}${icon} ${msg}${C.reset}`);
31
+
32
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
33
+
34
+ // ── Portlar ───────────────────────────────────────────────────
35
+ const PROXY_PORT = 11435; // nexusai-team .env.local ga mos
36
+ const ADMIN_PORT = 11436; // avg-ai CLI boshqaruv
37
+ const OLLAMA_PORT = 11434; // Ollama default
38
+
39
+ // ── Ma'lumot papkasi ──────────────────────────────────────────
40
+ const DATA_DIR = path.join(os.homedir(), '.avg-nexus');
41
+ const KEYS_FILE = path.join(DATA_DIR, 'keys.json');
42
+ const DEVICES_FILE = path.join(DATA_DIR, 'devices.json');
43
+ const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
44
+
45
+ function ensureDataDir() {
46
+ if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
47
+ }
48
+
49
+ // ══════════════════════════════════════════════════════════════
50
+ // KEY POOL — 10 ta OpenRouter kalit boshqaruvi
51
+ // ══════════════════════════════════════════════════════════════
52
+
53
+ function loadKeys() {
54
+ ensureDataDir();
55
+ try {
56
+ if (fs.existsSync(KEYS_FILE)) {
57
+ return JSON.parse(fs.readFileSync(KEYS_FILE, 'utf8'));
58
+ }
59
+ } catch {}
60
+ return { keys: [], currentIndex: 0 };
61
+ }
62
+
63
+ function saveKeys(data) {
64
+ ensureDataDir();
65
+ fs.writeFileSync(KEYS_FILE, JSON.stringify(data, null, 2));
21
66
  }
22
67
 
23
- function sleep(ms) {
24
- return new Promise(r => setTimeout(r, ms));
68
+ function addApiKey(key) {
69
+ const data = loadKeys();
70
+ if (!data.keys.includes(key)) {
71
+ data.keys.push(key);
72
+ saveKeys(data);
73
+ return true;
74
+ }
75
+ return false;
76
+ }
77
+
78
+ function removeApiKey(key) {
79
+ const data = loadKeys();
80
+ const before = data.keys.length;
81
+ data.keys = data.keys.filter(k => k !== key);
82
+ if (data.currentIndex >= data.keys.length) data.currentIndex = 0;
83
+ saveKeys(data);
84
+ return data.keys.length < before;
85
+ }
86
+
87
+ function getNextKey() {
88
+ const data = loadKeys();
89
+ if (data.keys.length === 0) return null;
90
+ const key = data.keys[data.currentIndex % data.keys.length];
91
+ data.currentIndex = (data.currentIndex + 1) % data.keys.length;
92
+ saveKeys(data);
93
+ return key;
94
+ }
95
+
96
+ function getAllKeys() {
97
+ return loadKeys().keys;
25
98
  }
26
99
 
27
- // ── Ollama topish — PATH + barcha disklar ──
100
+ // Key holati tracking
101
+ const keyStats = new Map();
102
+
103
+ function getKeyStats(key) {
104
+ if (!keyStats.has(key)) {
105
+ keyStats.set(key, {
106
+ calls: 0, success: 0, fail: 0,
107
+ lastUsed: null, lastError: null,
108
+ cooldownUntil: 0,
109
+ });
110
+ }
111
+ return keyStats.get(key);
112
+ }
113
+
114
+ function markSuccess(key) {
115
+ const s = getKeyStats(key);
116
+ s.calls++; s.success++; s.lastUsed = Date.now(); s.cooldownUntil = 0;
117
+ }
118
+
119
+ function markFail(key, err) {
120
+ const s = getKeyStats(key);
121
+ s.calls++; s.fail++; s.lastError = err;
122
+ // Exponential backoff: 5s, 10s, 20s, max 60s
123
+ const backoff = Math.min(5000 * Math.pow(2, s.fail - 1), 60000);
124
+ s.cooldownUntil = Date.now() + backoff;
125
+ }
126
+
127
+ function isAvailable(key) {
128
+ const s = keyStats.get(key);
129
+ if (!s) return true;
130
+ return Date.now() > s.cooldownUntil;
131
+ }
132
+
133
+ function getBestKey(keys) {
134
+ const available = keys.filter(isAvailable);
135
+ if (available.length === 0) return keys[Math.floor(Math.random() * keys.length)];
136
+ // Eng kam ishlatilgani
137
+ available.sort((a, b) => {
138
+ const sa = getKeyStats(a), sb = getKeyStats(b);
139
+ return (sa.lastUsed || 0) - (sb.lastUsed || 0);
140
+ });
141
+ return available[0];
142
+ }
143
+
144
+ // ══════════════════════════════════════════════════════════════
145
+ // DEVICE MANAGER — har bir qurilma uchun API key
146
+ // ══════════════════════════════════════════════════════════════
147
+
148
+ function loadDevices() {
149
+ ensureDataDir();
150
+ try {
151
+ if (fs.existsSync(DEVICES_FILE)) {
152
+ return JSON.parse(fs.readFileSync(DEVICES_FILE, 'utf8'));
153
+ }
154
+ } catch {}
155
+ return {};
156
+ }
157
+
158
+ function saveDevices(data) {
159
+ ensureDataDir();
160
+ fs.writeFileSync(DEVICES_FILE, JSON.stringify(data, null, 2));
161
+ }
162
+
163
+ function registerDevice(deviceId, info = {}) {
164
+ const devices = loadDevices();
165
+ if (!devices[deviceId]) {
166
+ devices[deviceId] = {
167
+ id: deviceId,
168
+ registeredAt: Date.now(),
169
+ lastSeen: Date.now(),
170
+ info,
171
+ apiKey: null,
172
+ calls: 0,
173
+ };
174
+ saveDevices(devices);
175
+ log('📱', `Yangi qurilma: ${deviceId}`, C.cyan);
176
+ } else {
177
+ devices[deviceId].lastSeen = Date.now();
178
+ if (info.hostname) devices[deviceId].info = { ...devices[deviceId].info, ...info };
179
+ saveDevices(devices);
180
+ }
181
+ return devices[deviceId];
182
+ }
183
+
184
+ function assignKeyToDevice(deviceId, apiKey) {
185
+ const devices = loadDevices();
186
+ if (!devices[deviceId]) return false;
187
+ devices[deviceId].apiKey = apiKey;
188
+ saveDevices(devices);
189
+ return true;
190
+ }
191
+
192
+ function getDeviceKey(deviceId) {
193
+ if (!deviceId) return null;
194
+ const devices = loadDevices();
195
+ return devices[deviceId]?.apiKey || null;
196
+ }
197
+
198
+ // ══════════════════════════════════════════════════════════════
199
+ // OLLAMA TOPISH
200
+ // ══════════════════════════════════════════════════════════════
201
+
28
202
  function findOllama() {
203
+ // 1. PATH
29
204
  try {
30
- const r = execSync('where ollama', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
205
+ const r = execSync(os.platform() === 'win32' ? 'where ollama' : 'which ollama',
206
+ { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
31
207
  if (r) return r.split('\n')[0].trim();
32
208
  } catch {}
33
209
 
34
- const drives = ['A','B','C','D','E','F','G','H','I','J'];
35
- const locations = [
36
- 'Ollama\\ollama.exe',
37
- 'Program Files\\Ollama\\ollama.exe',
38
- `Users\\${process.env.USERNAME || 'user'}\\AppData\\Local\\Programs\\Ollama\\ollama.exe`,
39
- ];
210
+ // 2. Windows disk scan
211
+ if (os.platform() === 'win32') {
212
+ // Registry
213
+ try {
214
+ const reg = execSync(
215
+ 'reg query "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\ollama.exe" /ve',
216
+ { stdio: 'pipe' }).toString();
217
+ const m = reg.match(/REG_SZ\s+(.+)/);
218
+ if (m && fs.existsSync(m[1].trim())) return m[1].trim();
219
+ } catch {}
40
220
 
41
- for (const d of drives) {
42
- for (const l of locations) {
43
- const p = `${d}:\\${l}`;
44
- try { if (fs.existsSync(p)) return p; } catch {}
221
+ // Disk scan
222
+ const drives = ['C','D','E','F','G','H'];
223
+ const locs = [
224
+ `AppData\\Local\\Programs\\Ollama\\ollama.exe`,
225
+ `Users\\${process.env.USERNAME || ''}\\AppData\\Local\\Programs\\Ollama\\ollama.exe`,
226
+ ];
227
+ for (const d of drives) {
228
+ for (const l of locs) {
229
+ const p = `${d}:\\${l}`;
230
+ try { if (fs.existsSync(p)) return p; } catch {}
231
+ }
232
+ // Direct locations
233
+ for (const name of ['Ollama\\ollama.exe', 'ollama\\ollama.exe']) {
234
+ const p = `${d}:\\${name}`;
235
+ try { if (fs.existsSync(p)) return p; } catch {}
236
+ }
237
+ }
238
+ if (process.env.LOCALAPPDATA) {
239
+ const p = path.join(process.env.LOCALAPPDATA, 'Programs', 'Ollama', 'ollama.exe');
240
+ if (fs.existsSync(p)) return p;
45
241
  }
46
242
  }
47
243
  return null;
48
244
  }
49
245
 
50
- // ── Model topish — ollama list CLI orqali ──
246
+ // ══════════════════════════════════════════════════════════════
247
+ // MODEL TOPISH
248
+ // ══════════════════════════════════════════════════════════════
249
+
51
250
  function findBestModel() {
52
- const preferred = ['gemma4', 'gemma3', 'gemma2', 'qwen', 'llama', 'mistral', 'deepseek', 'phi'];
251
+ const preferred = ['gemma4','gemma3','gemma2','qwen2.5','qwen','llama','mistral','deepseek','phi'];
53
252
  try {
54
253
  const result = execSync('ollama list', {
55
- encoding: 'utf8',
56
- stdio: ['pipe', 'pipe', 'pipe'],
57
- timeout: 10000,
254
+ encoding: 'utf8', stdio: ['pipe','pipe','pipe'], timeout: 15000,
58
255
  });
59
- const lines = result
60
- .split('\n')
256
+ const lines = result.split('\n')
61
257
  .filter(l => l.trim() && !l.toLowerCase().startsWith('name'));
62
258
 
63
259
  if (lines.length > 0) {
@@ -65,8 +261,7 @@ function findBestModel() {
65
261
  const found = lines.find(l => l.toLowerCase().includes(pref));
66
262
  if (found) {
67
263
  const name = found.split(/\s+/)[0].trim();
68
- log('✅', `Model topildi: ${name}`, C.green);
69
- return name;
264
+ if (name) { log('✅', `Model: ${name}`, C.green); return name; }
70
265
  }
71
266
  }
72
267
  const name = lines[0].split(/\s+/)[0].trim();
@@ -74,94 +269,434 @@ function findBestModel() {
74
269
  return name;
75
270
  }
76
271
  } catch (e) {
77
- log('⚠️', `ollama list xatosi: ${e.message}`, C.yellow);
272
+ log('⚠️', `ollama list: ${e.message}`, C.yellow);
78
273
  }
79
-
80
- // Model topilmadi — gemma4:e4b yuklab olish
81
- log('📥', 'Model topilmadi. gemma4:e4b yuklanmoqda...', C.yellow);
82
- execSync('ollama pull gemma4:e4b', { stdio: 'inherit' });
274
+ log('📥', 'Model topilmadi, gemma4:e4b yuklanmoqda...', C.yellow);
275
+ try {
276
+ execSync('ollama pull gemma4:e4b', { stdio: 'inherit' });
277
+ } catch {}
83
278
  return 'gemma4:e4b';
84
279
  }
85
280
 
86
- // ── Ollama serve ishga tushirish ──
87
- function startOllama(ollamaPath) {
281
+ // ══════════════════════════════════════════════════════════════
282
+ // OLLAMA SERVE
283
+ // ══════════════════════════════════════════════════════════════
284
+
285
+ async function startOllama(ollamaPath) {
286
+ // Allaqachon ishlayaptimi?
88
287
  try {
89
- execSync('curl -s http://localhost:11434', { timeout: 2000, stdio: 'pipe' });
288
+ await httpGet('http://localhost:11434/api/tags', 2000);
90
289
  log('✅', 'Ollama allaqachon ishlamoqda', C.green);
91
290
  return;
92
291
  } catch {}
292
+
93
293
  log('🚀', 'Ollama ishga tushirilmoqda...', C.cyan);
94
294
  const proc = spawn(ollamaPath, ['serve'], {
95
- detached: true,
96
- stdio: 'ignore',
97
- windowsHide: true,
295
+ detached: true, stdio: 'ignore', windowsHide: true,
98
296
  });
99
297
  proc.unref();
298
+ await sleep(3000);
100
299
  log('✅', 'Ollama serve ishga tushdi', C.green);
101
300
  }
102
301
 
103
- // ── OpenRouter → Ollama Proxy ──
104
- function startProxy(model) {
105
- const PORT = 11435;
302
+ // ══════════════════════════════════════════════════════════════
303
+ // HTTP HELPER
304
+ // ══════════════════════════════════════════════════════════════
305
+
306
+ function httpGet(url, timeout = 5000) {
307
+ return new Promise((resolve, reject) => {
308
+ const req = http.get(url, { timeout }, (res) => {
309
+ let data = '';
310
+ res.on('data', c => data += c);
311
+ res.on('end', () => resolve(data));
312
+ });
313
+ req.on('error', reject);
314
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
315
+ });
316
+ }
317
+
318
+ // ══════════════════════════════════════════════════════════════
319
+ // PROXY SERVER — OpenRouter API format → Ollama
320
+ // ══════════════════════════════════════════════════════════════
321
+
322
+ function startProxy(localModel) {
106
323
  const server = http.createServer(async (req, res) => {
324
+ // CORS
107
325
  res.setHeader('Access-Control-Allow-Origin', '*');
108
326
  res.setHeader('Access-Control-Allow-Headers', '*');
109
- res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
327
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
328
+ if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
110
329
 
111
- if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
330
+ const url = req.url || '/';
112
331
 
113
- if (req.url && req.url.includes('/models')) {
114
- res.writeHead(200, { 'Content-Type': 'application/json' });
115
- res.end(JSON.stringify({ data: [{ id: model, name: model, context_length: 8192 }] }));
332
+ // ── Models endpoint ──
333
+ if (url.includes('/models')) {
334
+ try {
335
+ const result = execSync('ollama list', {
336
+ encoding: 'utf8', stdio: ['pipe','pipe','pipe'], timeout: 5000,
337
+ });
338
+ const lines = result.split('\n')
339
+ .filter(l => l.trim() && !l.toLowerCase().startsWith('name'));
340
+ const models = lines.map(l => {
341
+ const name = l.split(/\s+/)[0].trim();
342
+ return {
343
+ id: name, object: 'model',
344
+ created: Math.floor(Date.now() / 1000),
345
+ owned_by: 'ollama', name,
346
+ context_length: 8192,
347
+ pricing: { prompt: '0', completion: '0' },
348
+ };
349
+ });
350
+ res.writeHead(200, { 'Content-Type': 'application/json' });
351
+ res.end(JSON.stringify({ object: 'list', data: models }));
352
+ } catch {
353
+ res.writeHead(200, { 'Content-Type': 'application/json' });
354
+ res.end(JSON.stringify({
355
+ object: 'list',
356
+ data: [{ id: localModel, name: localModel, object: 'model',
357
+ owned_by: 'ollama', context_length: 8192,
358
+ pricing: { prompt: '0', completion: '0' } }],
359
+ }));
360
+ }
116
361
  return;
117
362
  }
118
363
 
119
- let body = '';
120
- req.on('data', c => body += c);
121
- req.on('end', async () => {
122
- try {
123
- let d = {};
124
- try { d = JSON.parse(body); } catch {}
125
- const ollamaRes = await fetch('http://localhost:11434/api/chat', {
126
- method: 'POST',
127
- headers: { 'Content-Type': 'application/json' },
128
- body: JSON.stringify({ model: d.model || model, messages: d.messages || [], stream: true }),
129
- });
130
- res.writeHead(200, { 'Content-Type': 'text/event-stream' });
131
- const reader = ollamaRes.body.getReader();
132
- const dec = new TextDecoder();
133
- while (true) {
134
- const { done, value } = await reader.read();
135
- if (done) break;
136
- for (const line of dec.decode(value).split('\n').filter(l => l.trim())) {
364
+ // ── Chat completions ──
365
+ if (url.includes('/chat/completions')) {
366
+ let body = '';
367
+ req.on('data', c => body += c);
368
+ req.on('end', async () => {
369
+ try {
370
+ const d = JSON.parse(body || '{}');
371
+ const model = localModel;
372
+ const isStream = d.stream !== false;
373
+
374
+ // Device ID dan key olish (Authorization header dan)
375
+ const authHeader = req.headers['authorization'] || '';
376
+ const deviceId = authHeader.replace('Bearer ', '').trim();
377
+ const deviceApiKey = getDeviceKey(deviceId);
378
+
379
+ // Agar device o'ziga specific OpenRouter key bor bo'lsa — undan foydalanish
380
+ // Aks holda Ollama local
381
+ const allKeys = getAllKeys();
382
+ const useOnline = (deviceApiKey || allKeys.length > 0) &&
383
+ deviceId && deviceId.startsWith('sk-') === false &&
384
+ !['ollama-local','local','offline'].includes(deviceId);
385
+
386
+ if (deviceId && !deviceId.startsWith('sk-') && deviceId !== 'ollama-local') {
387
+ registerDevice(deviceId, {});
388
+ }
389
+
390
+ // Ollama ga yo'naltir
391
+ await proxyToOllama(req, res, d, model, isStream);
392
+
393
+ } catch (e) {
394
+ res.writeHead(500, { 'Content-Type': 'application/json' });
395
+ res.end(JSON.stringify({ error: { message: e.message, type: 'proxy_error' } }));
396
+ }
397
+ });
398
+ return;
399
+ }
400
+
401
+ // ── Boshqalar ──
402
+ res.writeHead(404, { 'Content-Type': 'application/json' });
403
+ res.end(JSON.stringify({ error: 'Not found' }));
404
+ });
405
+
406
+ server.listen(PROXY_PORT, '127.0.0.1', () => {
407
+ log('✅', `Proxy: http://localhost:${PROXY_PORT}`, C.green);
408
+ });
409
+
410
+ server.on('error', err => {
411
+ if (err.code === 'EADDRINUSE') {
412
+ log('⚠️', `Port ${PROXY_PORT} band — proxy allaqachon ishlamoqda`, C.yellow);
413
+ } else {
414
+ log('❌', `Proxy xatosi: ${err.message}`, C.red);
415
+ }
416
+ });
417
+
418
+ return server;
419
+ }
420
+
421
+ async function proxyToOllama(req, res, body, model, isStream) {
422
+ const ollamaBody = JSON.stringify({
423
+ model: model,
424
+ messages: body.messages || [],
425
+ stream: isStream,
426
+ options: {
427
+ temperature: body.temperature || 0.7,
428
+ num_predict: body.max_tokens || 2048,
429
+ },
430
+ });
431
+
432
+ return new Promise((resolve, reject) => {
433
+ const proxyReq = http.request({
434
+ hostname: '127.0.0.1', port: OLLAMA_PORT,
435
+ path: '/api/chat', method: 'POST',
436
+ headers: {
437
+ 'Content-Type': 'application/json',
438
+ 'Content-Length': Buffer.byteLength(ollamaBody),
439
+ },
440
+ }, (proxyRes) => {
441
+ if (isStream) {
442
+ res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' });
443
+ const decoder = require('string_decoder').StringDecoder;
444
+ const dec = new (decoder)('utf8');
445
+ proxyRes.on('data', chunk => {
446
+ const text = dec.write(chunk);
447
+ for (const line of text.split('\n')) {
448
+ if (!line.trim()) continue;
137
449
  try {
138
450
  const j = JSON.parse(line);
451
+ const content = j.message?.content || '';
452
+ const done = j.done || false;
139
453
  res.write('data: ' + JSON.stringify({
140
454
  id: 'chatcmpl-' + Date.now(),
141
455
  object: 'chat.completion.chunk',
142
- choices: [{ delta: { content: j.message?.content || '' }, finish_reason: j.done ? 'stop' : null }],
456
+ choices: [{
457
+ delta: { content },
458
+ finish_reason: done ? 'stop' : null,
459
+ index: 0,
460
+ }],
143
461
  }) + '\n\n');
144
462
  } catch {}
145
463
  }
146
- }
147
- res.write('data: [DONE]\n\n');
148
- res.end();
149
- } catch (e) { res.writeHead(500); res.end(JSON.stringify({ error: e.message })); }
464
+ });
465
+ proxyRes.on('end', () => {
466
+ res.write('data: [DONE]\n\n');
467
+ res.end();
468
+ resolve();
469
+ });
470
+ } else {
471
+ let data = '';
472
+ proxyRes.on('data', c => data += c);
473
+ proxyRes.on('end', () => {
474
+ try {
475
+ // Ollama javoblarini OpenRouter formatiga aylantirish
476
+ const lines = data.split('\n').filter(l => l.trim());
477
+ let fullContent = '';
478
+ for (const line of lines) {
479
+ try {
480
+ const j = JSON.parse(line);
481
+ fullContent += j.message?.content || '';
482
+ } catch {}
483
+ }
484
+ res.writeHead(200, { 'Content-Type': 'application/json' });
485
+ res.end(JSON.stringify({
486
+ id: 'chatcmpl-' + Date.now(),
487
+ object: 'chat.completion',
488
+ model: model,
489
+ choices: [{
490
+ message: { role: 'assistant', content: fullContent },
491
+ finish_reason: 'stop', index: 0,
492
+ }],
493
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
494
+ }));
495
+ } catch (e) {
496
+ res.writeHead(500, { 'Content-Type': 'application/json' });
497
+ res.end(JSON.stringify({ error: { message: e.message } }));
498
+ }
499
+ resolve();
500
+ });
501
+ }
502
+ proxyRes.on('error', reject);
150
503
  });
504
+ proxyReq.on('error', reject);
505
+ proxyReq.write(ollamaBody);
506
+ proxyReq.end();
507
+ });
508
+ }
509
+
510
+ // ══════════════════════════════════════════════════════════════
511
+ // ADMIN SERVER — avg-ai CLI boshqaruvi (port 11436)
512
+ // ══════════════════════════════════════════════════════════════
513
+
514
+ function startAdminServer(localModel) {
515
+ const server = http.createServer((req, res) => {
516
+ res.setHeader('Access-Control-Allow-Origin', '*');
517
+ res.setHeader('Access-Control-Allow-Headers', '*');
518
+ res.setHeader('Content-Type', 'application/json');
519
+
520
+ if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
521
+
522
+ const url = req.url || '/';
523
+ let body = '';
524
+ req.on('data', c => body += c);
525
+ req.on('end', () => {
526
+ try {
527
+ handleAdminRequest(req, res, url, body, localModel);
528
+ } catch (e) {
529
+ res.writeHead(500);
530
+ res.end(JSON.stringify({ error: e.message }));
531
+ }
532
+ });
533
+ });
534
+
535
+ server.listen(ADMIN_PORT, '127.0.0.1', () => {
536
+ log('✅', `Admin API: http://localhost:${ADMIN_PORT}`, C.green);
537
+ });
538
+
539
+ server.on('error', err => {
540
+ if (err.code !== 'EADDRINUSE') {
541
+ log('❌', `Admin server: ${err.message}`, C.red);
542
+ }
151
543
  });
152
- server.listen(PORT, () => log('✅', `Proxy: http://localhost:${PORT}`, C.green));
153
- return server;
154
544
  }
155
545
 
156
- // ── NexusAI loyiha topish ──
546
+ function handleAdminRequest(req, res, url, body, localModel) {
547
+ let parsed = {};
548
+ try { parsed = JSON.parse(body); } catch {}
549
+
550
+ // ── Status ──
551
+ if (url === '/status' || url === '/') {
552
+ const keys = getAllKeys();
553
+ const devices = loadDevices();
554
+ const keyStatus = keys.map((k, i) => {
555
+ const s = getKeyStats(k);
556
+ return {
557
+ index: i + 1,
558
+ preview: `...${k.slice(-6)}`,
559
+ available: isAvailable(k),
560
+ calls: s.calls, success: s.success, fail: s.fail,
561
+ cooldownMs: Math.max(0, s.cooldownUntil - Date.now()),
562
+ };
563
+ });
564
+ res.writeHead(200);
565
+ res.end(JSON.stringify({
566
+ status: 'running',
567
+ model: localModel,
568
+ proxyPort: PROXY_PORT,
569
+ adminPort: ADMIN_PORT,
570
+ ollamaPort: OLLAMA_PORT,
571
+ keys: { total: keys.length, status: keyStatus },
572
+ devices: { total: Object.keys(devices).length, list: Object.values(devices).map(d => ({
573
+ id: d.id, lastSeen: d.lastSeen, hasKey: !!d.apiKey, calls: d.calls,
574
+ })) },
575
+ uptime: process.uptime(),
576
+ }));
577
+ return;
578
+ }
579
+
580
+ // ── Keys: qo'shish ──
581
+ if (url === '/keys/add' && req.method === 'POST') {
582
+ const key = parsed.key?.trim();
583
+ if (!key || !key.startsWith('sk-')) {
584
+ res.writeHead(400); res.end(JSON.stringify({ error: 'sk- bilan boshlanishi kerak' }));
585
+ return;
586
+ }
587
+ const added = addApiKey(key);
588
+ res.writeHead(200);
589
+ res.end(JSON.stringify({ success: true, added, total: getAllKeys().length }));
590
+ return;
591
+ }
592
+
593
+ // ── Keys: o'chirish ──
594
+ if (url === '/keys/remove' && req.method === 'POST') {
595
+ const key = parsed.key?.trim();
596
+ const removed = removeApiKey(key);
597
+ res.writeHead(200);
598
+ res.end(JSON.stringify({ success: removed, total: getAllKeys().length }));
599
+ return;
600
+ }
601
+
602
+ // ── Keys: ro'yxat ──
603
+ if (url === '/keys/list') {
604
+ const keys = getAllKeys();
605
+ res.writeHead(200);
606
+ res.end(JSON.stringify({
607
+ total: keys.length,
608
+ keys: keys.map((k, i) => {
609
+ const s = getKeyStats(k);
610
+ return {
611
+ index: i + 1, preview: `sk-or-...${k.slice(-8)}`,
612
+ available: isAvailable(k), calls: s.calls,
613
+ success: s.success, fail: s.fail,
614
+ };
615
+ }),
616
+ }));
617
+ return;
618
+ }
619
+
620
+ // ── Device: ro'yxat ──
621
+ if (url === '/devices') {
622
+ const devices = loadDevices();
623
+ res.writeHead(200);
624
+ res.end(JSON.stringify({ devices: Object.values(devices) }));
625
+ return;
626
+ }
627
+
628
+ // ── Device: key tayinlash ──
629
+ if (url === '/devices/assign' && req.method === 'POST') {
630
+ const { deviceId, key } = parsed;
631
+ if (!deviceId || !key) {
632
+ res.writeHead(400); res.end(JSON.stringify({ error: 'deviceId va key kerak' }));
633
+ return;
634
+ }
635
+ const ok = assignKeyToDevice(deviceId, key);
636
+ res.writeHead(200);
637
+ res.end(JSON.stringify({ success: ok }));
638
+ return;
639
+ }
640
+
641
+ // ── Ollama: modellar ──
642
+ if (url === '/ollama/models') {
643
+ try {
644
+ const result = execSync('ollama list', {
645
+ encoding: 'utf8', stdio: ['pipe','pipe','pipe'], timeout: 10000,
646
+ });
647
+ const lines = result.split('\n').filter(l => l.trim() && !l.toLowerCase().startsWith('name'));
648
+ const models = lines.map(l => ({
649
+ name: l.split(/\s+/)[0].trim(),
650
+ size: l.split(/\s+/)[2] || '',
651
+ modified: l.split(/\s+/).slice(3).join(' ') || '',
652
+ })).filter(m => m.name);
653
+ res.writeHead(200);
654
+ res.end(JSON.stringify({ models, current: localModel }));
655
+ } catch (e) {
656
+ res.writeHead(500);
657
+ res.end(JSON.stringify({ error: e.message }));
658
+ }
659
+ return;
660
+ }
661
+
662
+ // ── Env: .env.local yangilash ──
663
+ if (url === '/env/update' && req.method === 'POST') {
664
+ const { projectDir } = parsed;
665
+ if (!projectDir || !fs.existsSync(projectDir)) {
666
+ res.writeHead(400); res.end(JSON.stringify({ error: 'projectDir topilmadi' })); return;
667
+ }
668
+ const keys = getAllKeys();
669
+ const envLines = [
670
+ `OPENROUTER_BASE_URL=http://localhost:${PROXY_PORT}/api/v1`,
671
+ ];
672
+ if (keys.length > 0) {
673
+ keys.slice(0, 10).forEach((k, i) => envLines.push(`OPENROUTER_API_KEY_${i+1}=${k}`));
674
+ } else {
675
+ for (let i = 1; i <= 10; i++) envLines.push(`OPENROUTER_API_KEY_${i}=ollama-local`);
676
+ }
677
+ const hfKey = process.env.HUGGINGFACE_API_KEY || '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';
678
+ envLines.push(`HUGGINGFACE_API_KEY=${hfKey}`);
679
+ fs.writeFileSync(path.join(projectDir, '.env.local'), envLines.join('\n'));
680
+ res.writeHead(200);
681
+ res.end(JSON.stringify({ success: true, keysCount: keys.length }));
682
+ return;
683
+ }
684
+
685
+ res.writeHead(404);
686
+ res.end(JSON.stringify({ error: 'Not found' }));
687
+ }
688
+
689
+ // ══════════════════════════════════════════════════════════════
690
+ // NEXUSAI LOYIHA TOPISH
691
+ // ══════════════════════════════════════════════════════════════
692
+
157
693
  function findProject() {
158
- const cfgs = ['next.config.js', 'next.config.ts', 'next.config.mjs'];
694
+ const cfgs = ['next.config.js','next.config.ts','next.config.mjs'];
159
695
  const cwd = process.cwd();
160
- for (const c of cfgs) {
161
- if (fs.existsSync(path.join(cwd, c))) return cwd;
162
- }
696
+ if (cfgs.some(c => fs.existsSync(path.join(cwd, c)))) return cwd;
697
+
163
698
  const drives = ['E','D','C','F','G'];
164
- const names = ['nexusai-team','NexusAi-team','NexusAI-team','nexus-team','nexusai'];
699
+ const names = ['nexusai-team','NexusAi-team','NexusAI-team','nexus-team','nexusai','NexusAI'];
165
700
  for (const d of drives) {
166
701
  for (const n of names) {
167
702
  const p = `${d}:\\${n}`;
@@ -170,56 +705,118 @@ function findProject() {
170
705
  } catch {}
171
706
  }
172
707
  }
708
+
709
+ // Install papkasini ham tekshir
710
+ const installDir = `C:\\NexusAI`;
711
+ if (fs.existsSync(installDir) && cfgs.some(c => fs.existsSync(path.join(installDir, c)))) {
712
+ return installDir;
713
+ }
714
+
173
715
  return null;
174
716
  }
175
717
 
176
- // ── .env.local yozish ──
718
+ // ══════════════════════════════════════════════════════════════
719
+ // .env.local SOZLASH
720
+ // ══════════════════════════════════════════════════════════════
721
+
177
722
  function setupEnv(dir) {
178
723
  const envPath = path.join(dir, '.env.local');
179
- const content = [
180
- 'OPENROUTER_BASE_URL=http://localhost:11435/api/v1',
181
- 'OPENROUTER_API_KEY_1=ollama-local',
182
- 'OPENROUTER_API_KEY_2=ollama-local',
183
- 'OPENROUTER_API_KEY_3=ollama-local',
184
- ].join('\n');
185
- if (fs.existsSync(envPath)) {
186
- const ex = fs.readFileSync(envPath, 'utf8');
187
- if (ex.includes('ollama-local')) return;
188
- fs.writeFileSync(envPath, content + '\n' + ex);
724
+ const keys = getAllKeys();
725
+ const lines = [
726
+ `OPENROUTER_BASE_URL=http://localhost:${PROXY_PORT}/api/v1`,
727
+ ];
728
+
729
+ if (keys.length > 0) {
730
+ keys.slice(0, 10).forEach((k, i) => lines.push(`OPENROUTER_API_KEY_${i+1}=${k}`));
189
731
  } else {
190
- fs.writeFileSync(envPath, content);
732
+ for (let i = 1; i <= 10; i++) lines.push(`OPENROUTER_API_KEY_${i}=ollama-local`);
733
+ }
734
+
735
+ const hfKey = process.env.HUGGINGFACE_API_KEY || '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';
736
+ lines.push(`HUGGINGFACE_API_KEY=${hfKey}`);
737
+
738
+ // Mavjud .env.local dan boshqa env varlarni saqlab qolish
739
+ let extra = '';
740
+ if (fs.existsSync(envPath)) {
741
+ const existing = fs.readFileSync(envPath, 'utf8').split('\n');
742
+ const keep = existing.filter(l => {
743
+ const key = l.split('=')[0].trim();
744
+ return l.trim() && !l.startsWith('#') &&
745
+ !key.startsWith('OPENROUTER_') && key !== 'HUGGINGFACE_API_KEY';
746
+ });
747
+ extra = keep.join('\n');
191
748
  }
192
- log('✅', '.env.local sozlandi', C.green);
749
+
750
+ const content = lines.join('\n') + (extra ? '\n' + extra : '');
751
+ fs.writeFileSync(envPath, content);
752
+ log('✅', `.env.local sozlandi (${keys.length} key)`, C.green);
193
753
  }
194
754
 
195
- // ── Next.js ishga tushirish ──
755
+ // ══════════════════════════════════════════════════════════════
756
+ // NEXT.JS ISHGA TUSHIRISH
757
+ // ══════════════════════════════════════════════════════════════
758
+
196
759
  function startNextJS(dir) {
197
760
  return new Promise(resolve => {
198
761
  setupEnv(dir);
762
+
199
763
  if (!fs.existsSync(path.join(dir, 'node_modules'))) {
200
764
  log('📦', 'npm install...', C.yellow);
201
- execSync('npm install', { cwd: dir, stdio: 'inherit' });
765
+ try {
766
+ execSync('npm install', { cwd: dir, stdio: 'inherit' });
767
+ } catch (e) {
768
+ log('❌', `npm install xatosi: ${e.message}`, C.red);
769
+ }
202
770
  }
771
+
203
772
  log('🌐', 'NexusAI ishga tushmoqda...', C.cyan);
773
+ const env = {
774
+ ...process.env,
775
+ PORT: '3000',
776
+ OPENROUTER_BASE_URL: `http://localhost:${PROXY_PORT}/api/v1`,
777
+ };
778
+
779
+ // API keylarni ham environment ga qo'shamiz
780
+ const keys = getAllKeys();
781
+ keys.slice(0, 10).forEach((k, i) => {
782
+ env[`OPENROUTER_API_KEY_${i+1}`] = k;
783
+ });
784
+ if (keys.length === 0) {
785
+ for (let i = 1; i <= 10; i++) env[`OPENROUTER_API_KEY_${i}`] = 'ollama-local';
786
+ }
787
+
204
788
  const dev = spawn('npm', ['run', 'dev'], {
205
789
  cwd: dir, shell: true,
206
790
  stdio: ['ignore', 'pipe', 'pipe'],
207
- env: { ...process.env, PORT: '3000', OPENROUTER_BASE_URL: 'http://localhost:11435/api/v1', OPENROUTER_API_KEY_1: 'ollama-local' },
791
+ env,
208
792
  });
793
+
794
+ let started = false;
209
795
  dev.stdout.on('data', d => {
210
796
  const t = d.toString();
211
- if (t.includes('Ready') || t.includes('ready') || t.includes('localhost:3000')) {
797
+ process.stdout.write(C.dim + t + C.reset);
798
+ if (!started && (t.includes('Ready') || t.includes('ready') || t.includes('localhost:3000'))) {
799
+ started = true;
212
800
  log('✅', 'Sayt tayyor: http://localhost:3000', C.green);
213
801
  setTimeout(resolve, 500);
214
802
  }
215
803
  });
216
- dev.on('error', e => log('❌', e.message, C.red));
217
- setTimeout(resolve, 60000);
804
+ dev.stderr.on('data', d => {
805
+ const t = d.toString();
806
+ if (t.includes('Error') || t.includes('error')) {
807
+ process.stdout.write(C.red + t + C.reset);
808
+ }
809
+ });
810
+ dev.on('error', e => { log('❌', e.message, C.red); resolve(); });
811
+ setTimeout(() => { if (!started) resolve(); }, 90000);
218
812
  });
219
813
  }
220
814
 
221
- // ── MAIN ──
222
- async function main() {
815
+ // ══════════════════════════════════════════════════════════════
816
+ // BANNER
817
+ // ══════════════════════════════════════════════════════════════
818
+
819
+ function showBanner() {
223
820
  console.log(`\n${C.bold}${C.cyan}
224
821
  ███╗ ██╗███████╗██╗ ██╗██╗ ██╗███████╗ █████╗ ██╗
225
822
  ████╗ ██║██╔════╝╚██╗██╔╝██║ ██║██╔════╝██╔══██╗██║
@@ -227,39 +824,79 @@ async function main() {
227
824
  ██║╚██╗██║██╔══╝ ██╔██╗ ██║ ██║╚════██║██╔══██║██║
228
825
  ██║ ╚████║███████╗██╔╝ ██╗╚██████╔╝███████║██║ ██║██║
229
826
  ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝
230
- ${C.reset}${C.dim} TEAM v3.0 15 AI Agents${C.reset}\n`);
827
+ ${C.reset}${C.yellow} AVG-NEXUS v4.0 NexusAI Local Proxy${C.reset}\n`);
828
+ }
231
829
 
830
+ // ══════════════════════════════════════════════════════════════
831
+ // MAIN
832
+ // ══════════════════════════════════════════════════════════════
833
+
834
+ async function main() {
835
+ ensureDataDir();
836
+ showBanner();
837
+
838
+ // 1. Ollama topish
232
839
  log('🔍', 'Ollama qidirilmoqda...', C.cyan);
233
840
  const ollamaPath = findOllama();
234
841
  if (!ollamaPath) {
235
842
  log('❌', 'Ollama topilmadi! https://ollama.com/download', C.red);
843
+ log('💡', 'Ollama o\'rnatib qayta ishga tushiring', C.yellow);
236
844
  process.exit(1);
237
845
  }
238
846
  log('✅', `Ollama: ${ollamaPath}`, C.green);
239
847
 
240
- startOllama(ollamaPath);
241
- await sleep(2000);
848
+ // 2. Ollama serve
849
+ await startOllama(ollamaPath);
242
850
 
851
+ // 3. Model topish
243
852
  log('🔍', 'AI model qidirilmoqda...', C.cyan);
244
853
  const model = findBestModel();
245
854
 
855
+ // 4. Proxy server
856
+ log('🔀', 'Proxy server ishga tushirilmoqda...', C.cyan);
246
857
  startProxy(model);
247
858
 
859
+ // 5. Admin server
860
+ startAdminServer(model);
861
+
862
+ // 6. Keys holati
863
+ const keys = getAllKeys();
864
+ if (keys.length > 0) {
865
+ log('🔑', `${keys.length} ta OpenRouter key topildi`, C.green);
866
+ } else {
867
+ log('🦙', 'API key yo\'q — faqat Ollama (offline rejim)', C.yellow);
868
+ log('💡', 'Key qo\'shish: avg-ai yoqing va /keys add sk-or-... bering', C.dim);
869
+ }
870
+
871
+ // 7. NexusAI loyiha
248
872
  log('🔍', 'NexusAI loyiha qidirilmoqda...', C.cyan);
249
873
  const dir = findProject();
250
874
  if (!dir) {
251
- log('', 'NexusAI loyiha topilmadi!', C.red);
252
- log('💡', 'nexusai-team papkasida turib avg-nexus bering', C.yellow);
253
- process.exit(1);
875
+ log('⚠️', 'NexusAI loyiha topilmadi — faqat proxy ishlaydi', C.yellow);
876
+ log('💡', 'nexusai-team papkasida turib avg-nexus bering', C.dim);
877
+ log('', `\n${C.green}${C.bold}✅ Proxy tayyor: http://localhost:${PROXY_PORT}${C.reset}`, '');
878
+ log('', `${C.green}Admin API: http://localhost:${ADMIN_PORT}${C.reset}\n`, '');
879
+ } else {
880
+ log('✅', `Loyiha: ${dir}`, C.green);
881
+ await startNextJS(dir);
882
+ log('', `\n${C.bold}${C.green}🎉 NexusAI tayyor! http://localhost:3000${C.reset}`, '');
883
+ log('', `${C.dim}Proxy: :${PROXY_PORT} Admin: :${ADMIN_PORT} Ctrl+C to'xtatish${C.reset}\n`, '');
254
884
  }
255
- log('✅', `Loyiha: ${dir}`, C.green);
256
885
 
257
- await startNextJS(dir);
258
-
259
- log('', `\n${C.bold}${C.green}🎉 NexusAI tayyor! http://localhost:3000${C.reset}\n`, '');
260
- log('', `${C.dim}To'xtatish: Ctrl+C${C.reset}\n`, '');
886
+ // Chiqishni ushlab turish
887
+ process.on('SIGINT', () => {
888
+ log('👋', 'To\'xtatildi', C.yellow);
889
+ process.exit(0);
890
+ });
261
891
 
262
- process.on('SIGINT', () => { log('👋', 'To\'xtatildi', C.yellow); process.exit(0); });
892
+ // Har 60 soniyada status
893
+ setInterval(() => {
894
+ const keys = getAllKeys();
895
+ const available = keys.filter(isAvailable).length;
896
+ if (keys.length > 0) {
897
+ log('📊', `Keys: ${available}/${keys.length} available | Uptime: ${Math.floor(process.uptime())}s`, C.dim);
898
+ }
899
+ }, 60000);
263
900
  }
264
901
 
265
- main().catch(e => { log('❌', e.message, C.red); process.exit(1); });
902
+ main().catch(e => { log('❌', e.message, C.red); process.exit(1); });
package/package.json CHANGED
@@ -1,26 +1,26 @@
1
- {
2
- "name": "avg-nexus",
3
- "version": "1.0.10",
4
- "description": "Offline/Online proxy for NexusAI Team runs with local Ollama",
5
- "main": "bin/avg-nexus.js",
6
- "type": "commonjs",
7
- "bin": {
8
- "avgnexus": "./bin/avg-nexus.js"
9
- },
10
- "scripts": {
11
- "start": "node bin/avg-nexus.js"
12
- },
13
- "keywords": [
14
- "ollama",
15
- "openrouter",
16
- "proxy",
17
- "nexusai",
18
- "offline",
19
- "ai"
20
- ],
21
- "author": "avg1002",
22
- "license": "MIT",
23
- "engines": {
24
- "node": ">=18.0.0"
25
- }
26
- }
1
+ {
2
+ "name": "avg-nexus",
3
+ "version": "1.0.12",
4
+ "description": "NexusAI Local Proxy Ollama + OpenRouter + Device API Manager",
5
+ "main": "bin/avg-nexus.js",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "avg-nexus": "bin/avg-nexus.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/avg-nexus.js"
12
+ },
13
+ "keywords": [
14
+ "ollama",
15
+ "openrouter",
16
+ "proxy",
17
+ "nexusai",
18
+ "offline",
19
+ "ai"
20
+ ],
21
+ "author": "avg1002",
22
+ "license": "MIT",
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ }
26
+ }