neuro-cli 4.3.0 → 5.0.1

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.
@@ -0,0 +1,589 @@
1
+ // ============================================================
2
+ // NeuroCLI - Rate Limit Manager
3
+ // Intelligent rate limiting with token bucket, per-model
4
+ // cooldown tracking, model rotation, and adaptive backoff
5
+ // ============================================================
6
+ import chalk from 'chalk';
7
+ import { MODELS, getFreeModelsWithTools } from '../api/models.js';
8
+ // ---------------------------------------------------------------------------
9
+ // RateLimitManager
10
+ // ---------------------------------------------------------------------------
11
+ export class RateLimitManager {
12
+ config;
13
+ modelStates = new Map();
14
+ globalRequestTimestamps = [];
15
+ requestQueue = [];
16
+ queueProcessTimer = null;
17
+ rotationPool = [];
18
+ currentRotationIndex = 0;
19
+ requestIdCounter = 0;
20
+ // Adaptive learning data
21
+ rateLimitPatterns = new Map();
22
+ constructor(config) {
23
+ this.config = {
24
+ globalRpm: 20,
25
+ perModelRpm: 10,
26
+ baseCooldownMs: 10000,
27
+ maxCooldownMs: 120000,
28
+ backoffMultiplier: 2.0,
29
+ rotationPoolSize: 5,
30
+ enableQueue: true,
31
+ maxQueueSize: 50,
32
+ maxQueueWaitMs: 60000,
33
+ adaptiveLearning: true,
34
+ verbose: true,
35
+ ...config,
36
+ };
37
+ // Initialize rotation pool with free models that support tools
38
+ this.initializeRotationPool();
39
+ // Start queue processor
40
+ this.startQueueProcessor();
41
+ }
42
+ // =========================================================================
43
+ // Pre-Request Checking
44
+ // =========================================================================
45
+ /**
46
+ * Check if a request can be made to a specific model.
47
+ * Returns a RateLimitCheck with the decision and optional alternative model.
48
+ */
49
+ checkRateLimit(modelId) {
50
+ const now = Date.now();
51
+ // 1. Check global RPM
52
+ this.cleanupGlobalTimestamps(now);
53
+ if (this.globalRequestTimestamps.length >= this.config.globalRpm) {
54
+ const oldestInWindow = this.globalRequestTimestamps[0];
55
+ const waitMs = Math.max(0, 60000 - (now - oldestInWindow));
56
+ const alternative = this.findAvailableModel(modelId);
57
+ if (alternative) {
58
+ return {
59
+ allowed: false,
60
+ waitMs: 0,
61
+ suggestedModel: alternative,
62
+ reason: `Global RPM limit reached (${this.config.globalRpm}/min). Suggesting alternative: ${MODELS[alternative]?.name || alternative}`,
63
+ };
64
+ }
65
+ return {
66
+ allowed: false,
67
+ waitMs,
68
+ suggestedModel: null,
69
+ reason: `Global RPM limit reached (${this.config.globalRpm}/min). Wait ${Math.ceil(waitMs / 1000)}s or try a different model.`,
70
+ };
71
+ }
72
+ // 2. Check per-model cooldown
73
+ const state = this.getOrCreateState(modelId);
74
+ if (state.cooldownUntil > now) {
75
+ const waitMs = state.cooldownUntil - now;
76
+ const alternative = this.findAvailableModel(modelId);
77
+ if (alternative) {
78
+ return {
79
+ allowed: false,
80
+ waitMs: 0,
81
+ suggestedModel: alternative,
82
+ reason: `${MODELS[modelId]?.name || modelId} is in cooldown (${Math.ceil(waitMs / 1000)}s left). Rotating to ${MODELS[alternative]?.name || alternative}`,
83
+ };
84
+ }
85
+ return {
86
+ allowed: false,
87
+ waitMs,
88
+ suggestedModel: null,
89
+ reason: `${MODELS[modelId]?.name || modelId} is in cooldown. Wait ${Math.ceil(waitMs / 1000)}s.`,
90
+ };
91
+ }
92
+ // 3. Check per-model RPM
93
+ this.cleanupModelTimestamps(state, now);
94
+ if (state.requestsThisMinute >= this.config.perModelRpm) {
95
+ const alternative = this.findAvailableModel(modelId);
96
+ if (alternative) {
97
+ return {
98
+ allowed: false,
99
+ waitMs: 0,
100
+ suggestedModel: alternative,
101
+ reason: `${MODELS[modelId]?.name || modelId} RPM limit (${this.config.perModelRpm}/min). Rotating to ${MODELS[alternative]?.name || alternative}`,
102
+ };
103
+ }
104
+ const minuteElapsed = now - state.minuteWindowStart;
105
+ const waitMs = Math.max(0, 60000 - minuteElapsed);
106
+ return {
107
+ allowed: false,
108
+ waitMs,
109
+ suggestedModel: null,
110
+ reason: `${MODELS[modelId]?.name || modelId} RPM limit reached. Wait ${Math.ceil(waitMs / 1000)}s.`,
111
+ };
112
+ }
113
+ // 4. Check health score - if model is unhealthy, suggest alternative
114
+ if (state.healthScore < 30 && state.consecutive429s > 3) {
115
+ const alternative = this.findAvailableModel(modelId);
116
+ if (alternative) {
117
+ return {
118
+ allowed: true,
119
+ waitMs: 0,
120
+ suggestedModel: alternative,
121
+ reason: `${MODELS[modelId]?.name || modelId} health is low (${state.healthScore}/100). Suggesting ${MODELS[alternative]?.name || alternative} for better reliability.`,
122
+ };
123
+ }
124
+ }
125
+ // All checks passed
126
+ return {
127
+ allowed: true,
128
+ waitMs: 0,
129
+ suggestedModel: null,
130
+ reason: 'OK',
131
+ };
132
+ }
133
+ /**
134
+ * Acquire permission to make a request. Resolves when the request can proceed.
135
+ * If queuing is enabled, waits in queue. Otherwise, rejects if rate limited.
136
+ */
137
+ async acquire(modelId) {
138
+ const check = this.checkRateLimit(modelId);
139
+ if (check.allowed) {
140
+ // If there's a suggested model due to low health, we still allow but suggest
141
+ this.recordRequestStart(modelId);
142
+ return check;
143
+ }
144
+ // If there's an alternative model suggested, switch to it
145
+ if (check.suggestedModel) {
146
+ const altCheck = this.checkRateLimit(check.suggestedModel);
147
+ if (altCheck.allowed) {
148
+ this.recordRequestStart(check.suggestedModel);
149
+ if (this.config.verbose) {
150
+ console.log(chalk.cyan(` 🔄 Rate limiter: switching ${MODELS[modelId]?.name || modelId} → ${MODELS[check.suggestedModel]?.name || check.suggestedModel}`));
151
+ }
152
+ return { ...altCheck, suggestedModel: check.suggestedModel, reason: check.reason };
153
+ }
154
+ }
155
+ // If queuing is enabled, queue the request
156
+ if (this.config.enableQueue && check.waitMs > 0) {
157
+ return this.enqueueRequest(modelId);
158
+ }
159
+ // Can't proceed, return the check with wait info
160
+ if (this.config.verbose) {
161
+ console.log(chalk.yellow(` ⏳ Rate limited: ${check.reason}`));
162
+ }
163
+ return check;
164
+ }
165
+ /**
166
+ * Record a successful request completion
167
+ */
168
+ recordSuccess(modelId, responseTimeMs) {
169
+ const state = this.getOrCreateState(modelId);
170
+ state.consecutive429s = 0;
171
+ state.totalSuccesses++;
172
+ state.lastRequestAt = Date.now();
173
+ // Update average response time (exponential moving average)
174
+ if (state.avgResponseMs === 0) {
175
+ state.avgResponseMs = responseTimeMs;
176
+ }
177
+ else {
178
+ state.avgResponseMs = state.avgResponseMs * 0.8 + responseTimeMs * 0.2;
179
+ }
180
+ // Recover health score
181
+ state.healthScore = Math.min(100, state.healthScore + 5);
182
+ }
183
+ /**
184
+ * Record a rate limit (429) response
185
+ */
186
+ recordRateLimit(modelId, retryAfterSeconds) {
187
+ const state = this.getOrCreateState(modelId);
188
+ const now = Date.now();
189
+ state.consecutive429s++;
190
+ state.total429s++;
191
+ state.lastRequestAt = now;
192
+ // Calculate cooldown duration
193
+ let cooldownMs;
194
+ if (retryAfterSeconds && retryAfterSeconds > 0) {
195
+ // Respect server-provided retry-after
196
+ cooldownMs = retryAfterSeconds * 1000;
197
+ }
198
+ else {
199
+ // Exponential backoff based on consecutive 429s
200
+ cooldownMs = Math.min(this.config.baseCooldownMs * Math.pow(this.config.backoffMultiplier, state.consecutive429s - 1), this.config.maxCooldownMs);
201
+ }
202
+ state.cooldownUntil = now + cooldownMs;
203
+ // Decrease health score
204
+ state.healthScore = Math.max(0, state.healthScore - (10 + state.consecutive429s * 5));
205
+ // Adaptive learning: record the pattern
206
+ if (this.config.adaptiveLearning) {
207
+ this.recordRateLimitPattern(modelId, cooldownMs);
208
+ }
209
+ if (this.config.verbose) {
210
+ console.log(chalk.yellow(` 🚫 Rate limited on ${MODELS[modelId]?.name || modelId}: ` +
211
+ `cooldown ${Math.ceil(cooldownMs / 1000)}s ` +
212
+ `(consecutive: ${state.consecutive429s}, health: ${state.healthScore}/100)`));
213
+ }
214
+ }
215
+ /**
216
+ * Record a server error (5xx) - treat as potential overload
217
+ */
218
+ recordServerError(modelId) {
219
+ const state = this.getOrCreateState(modelId);
220
+ const now = Date.now();
221
+ // Apply a shorter cooldown for server errors
222
+ state.cooldownUntil = now + this.config.baseCooldownMs / 2;
223
+ state.healthScore = Math.max(0, state.healthScore - 15);
224
+ if (this.config.verbose) {
225
+ console.log(chalk.yellow(` ⚠️ Server error on ${MODELS[modelId]?.name || modelId}: ` +
226
+ `cooldown ${Math.ceil(this.config.baseCooldownMs / 2000)}s, health: ${state.healthScore}/100`));
227
+ }
228
+ }
229
+ // =========================================================================
230
+ // Model Rotation
231
+ // =========================================================================
232
+ /**
233
+ * Get the next available model from the rotation pool.
234
+ * Skips models that are in cooldown or have low health.
235
+ */
236
+ getNextRotationModel(excludeModel) {
237
+ const now = Date.now();
238
+ const poolSize = this.rotationPool.length;
239
+ if (poolSize === 0)
240
+ return null;
241
+ // Try each model in the pool, starting from current index
242
+ for (let i = 0; i < poolSize; i++) {
243
+ const idx = (this.currentRotationIndex + i) % poolSize;
244
+ const model = this.rotationPool[idx];
245
+ if (model === excludeModel)
246
+ continue;
247
+ const state = this.getOrCreateState(model);
248
+ // Skip if in cooldown
249
+ if (state.cooldownUntil > now)
250
+ continue;
251
+ // Skip if health is too low
252
+ if (state.healthScore < 20)
253
+ continue;
254
+ // Move rotation index forward
255
+ this.currentRotationIndex = (idx + 1) % poolSize;
256
+ return model;
257
+ }
258
+ // All models in pool are rate limited or unhealthy
259
+ // Find the one with the shortest wait time
260
+ let bestModel = null;
261
+ let shortestWait = Infinity;
262
+ for (const model of this.rotationPool) {
263
+ if (model === excludeModel)
264
+ continue;
265
+ const state = this.getOrCreateState(model);
266
+ const waitMs = Math.max(0, state.cooldownUntil - now);
267
+ if (waitMs < shortestWait) {
268
+ shortestWait = waitMs;
269
+ bestModel = model;
270
+ }
271
+ }
272
+ return bestModel;
273
+ }
274
+ /**
275
+ * Get the best available model for a given requested model.
276
+ * Tries the requested model first, then falls back through the rotation pool.
277
+ */
278
+ getBestAvailableModel(requestedModel) {
279
+ // Check if requested model is available
280
+ const check = this.checkRateLimit(requestedModel);
281
+ if (check.allowed)
282
+ return requestedModel;
283
+ // Try rotation pool
284
+ const alternative = this.findAvailableModel(requestedModel);
285
+ if (alternative)
286
+ return alternative;
287
+ // Last resort: return requested model (will wait)
288
+ return requestedModel;
289
+ }
290
+ // =========================================================================
291
+ // Status & Diagnostics
292
+ // =========================================================================
293
+ /**
294
+ * Get rate limit status for all tracked models
295
+ */
296
+ getStatus() {
297
+ const now = Date.now();
298
+ const result = [];
299
+ for (const [modelId, state] of this.modelStates) {
300
+ this.cleanupModelTimestamps(state, now);
301
+ result.push({
302
+ model: modelId,
303
+ name: MODELS[modelId]?.name || modelId,
304
+ health: state.healthScore,
305
+ cooldown: Math.max(0, Math.ceil((state.cooldownUntil - now) / 1000)),
306
+ consecutive429s: state.consecutive429s,
307
+ total429s: state.total429s,
308
+ totalSuccesses: state.totalSuccesses,
309
+ rpm: state.requestsThisMinute,
310
+ avgResponseMs: Math.round(state.avgResponseMs),
311
+ });
312
+ }
313
+ return result.sort((a, b) => b.health - a.health);
314
+ }
315
+ /**
316
+ * Get queue status
317
+ */
318
+ getQueueStatus() {
319
+ if (this.requestQueue.length === 0)
320
+ return { size: 0, oldestWaitMs: 0 };
321
+ return {
322
+ size: this.requestQueue.length,
323
+ oldestWaitMs: Date.now() - this.requestQueue[0].enqueuedAt,
324
+ };
325
+ }
326
+ /**
327
+ * Reset all rate limit state (useful for testing)
328
+ */
329
+ reset() {
330
+ for (const state of this.modelStates.values()) {
331
+ state.cooldownUntil = 0;
332
+ state.consecutive429s = 0;
333
+ state.healthScore = 100;
334
+ state.requestsThisMinute = 0;
335
+ }
336
+ this.globalRequestTimestamps = [];
337
+ this.requestQueue.forEach(r => r.reject(new Error('Rate limiter reset')));
338
+ this.requestQueue = [];
339
+ if (this.config.verbose) {
340
+ console.log(chalk.gray(' ↻ Rate limiter state reset'));
341
+ }
342
+ }
343
+ /**
344
+ * Reset a specific model's rate limit state
345
+ */
346
+ resetModel(modelId) {
347
+ const state = this.modelStates.get(modelId);
348
+ if (state) {
349
+ state.cooldownUntil = 0;
350
+ state.consecutive429s = 0;
351
+ state.healthScore = 100;
352
+ state.requestsThisMinute = 0;
353
+ }
354
+ }
355
+ /**
356
+ * Update configuration at runtime
357
+ */
358
+ updateConfig(updates) {
359
+ Object.assign(this.config, updates);
360
+ if (updates.rotationPoolSize !== undefined) {
361
+ this.initializeRotationPool();
362
+ }
363
+ }
364
+ /**
365
+ * Print a formatted status table
366
+ */
367
+ printStatus() {
368
+ const status = this.getStatus();
369
+ const now = Date.now();
370
+ console.log(chalk.bold('\n📊 Rate Limiter Status'));
371
+ console.log(chalk.gray('─'.repeat(80)));
372
+ for (const s of status) {
373
+ const healthBar = this.getHealthBar(s.health);
374
+ const cooldownStr = s.cooldown > 0 ? chalk.red(`${s.cooldown}s`) : chalk.green('✓');
375
+ const rpmStr = `${s.rpm}/${this.config.perModelRpm}`;
376
+ console.log(` ${s.name.padEnd(35)} ${healthBar} ${cooldownStr.padEnd(8)} ` +
377
+ `429s: ${chalk.yellow(String(s.total429s).padEnd(4))} ` +
378
+ `OK: ${chalk.green(String(s.totalSuccesses).padEnd(4))} ` +
379
+ `RPM: ${rpmStr.padEnd(6)} ` +
380
+ `avg: ${s.avgResponseMs}ms`);
381
+ }
382
+ const queueStatus = this.getQueueStatus();
383
+ if (queueStatus.size > 0) {
384
+ console.log(chalk.yellow(`\n Queue: ${queueStatus.size} requests waiting (oldest: ${Math.ceil(queueStatus.oldestWaitMs / 1000)}s)`));
385
+ }
386
+ console.log(chalk.gray('─'.repeat(80)));
387
+ }
388
+ // =========================================================================
389
+ // Private Methods
390
+ // =========================================================================
391
+ getOrCreateState(modelId) {
392
+ if (!this.modelStates.has(modelId)) {
393
+ this.modelStates.set(modelId, {
394
+ modelId,
395
+ cooldownUntil: 0,
396
+ consecutive429s: 0,
397
+ total429s: 0,
398
+ totalSuccesses: 0,
399
+ requestsThisMinute: 0,
400
+ minuteWindowStart: Date.now(),
401
+ lastRequestAt: 0,
402
+ avgResponseMs: 0,
403
+ inRotationPool: this.rotationPool.includes(modelId),
404
+ healthScore: 100,
405
+ });
406
+ }
407
+ return this.modelStates.get(modelId);
408
+ }
409
+ recordRequestStart(modelId) {
410
+ const now = Date.now();
411
+ const state = this.getOrCreateState(modelId);
412
+ // Update per-model minute window
413
+ this.cleanupModelTimestamps(state, now);
414
+ state.requestsThisMinute++;
415
+ state.lastRequestAt = now;
416
+ // Update global timestamps
417
+ this.cleanupGlobalTimestamps(now);
418
+ this.globalRequestTimestamps.push(now);
419
+ }
420
+ cleanupGlobalTimestamps(now) {
421
+ const oneMinuteAgo = now - 60000;
422
+ while (this.globalRequestTimestamps.length > 0 && this.globalRequestTimestamps[0] < oneMinuteAgo) {
423
+ this.globalRequestTimestamps.shift();
424
+ }
425
+ }
426
+ cleanupModelTimestamps(state, now) {
427
+ if (now - state.minuteWindowStart >= 60000) {
428
+ state.requestsThisMinute = 0;
429
+ state.minuteWindowStart = now;
430
+ }
431
+ }
432
+ findAvailableModel(excludeModel) {
433
+ const now = Date.now();
434
+ // First, try the rotation pool
435
+ for (const model of this.rotationPool) {
436
+ if (model === excludeModel)
437
+ continue;
438
+ const state = this.getOrCreateState(model);
439
+ if (state.cooldownUntil <= now && state.healthScore >= 20) {
440
+ this.cleanupModelTimestamps(state, now);
441
+ if (state.requestsThisMinute < this.config.perModelRpm) {
442
+ return model;
443
+ }
444
+ }
445
+ }
446
+ // Then try any tracked model that's available
447
+ for (const [modelId, state] of this.modelStates) {
448
+ if (modelId === excludeModel)
449
+ continue;
450
+ if (state.cooldownUntil <= now && state.healthScore >= 20) {
451
+ this.cleanupModelTimestamps(state, now);
452
+ if (state.requestsThisMinute < this.config.perModelRpm) {
453
+ return modelId;
454
+ }
455
+ }
456
+ }
457
+ return null;
458
+ }
459
+ initializeRotationPool() {
460
+ const freeModels = getFreeModelsWithTools();
461
+ // Sort by context window (larger is better for coding)
462
+ const sorted = freeModels.sort((a, b) => b.contextWindow - a.contextWindow);
463
+ // Take top N models for rotation
464
+ this.rotationPool = sorted
465
+ .slice(0, Math.min(this.config.rotationPoolSize, sorted.length))
466
+ .map(m => m.id);
467
+ // Mark all as in rotation pool
468
+ for (const modelId of this.rotationPool) {
469
+ const state = this.getOrCreateState(modelId);
470
+ state.inRotationPool = true;
471
+ }
472
+ if (this.config.verbose) {
473
+ console.log(chalk.gray(` 🔄 Rate limiter rotation pool: ${this.rotationPool.map(m => MODELS[m]?.name || m).join(', ')}`));
474
+ }
475
+ }
476
+ enqueueRequest(modelId) {
477
+ return new Promise((resolve, reject) => {
478
+ if (this.requestQueue.length >= this.config.maxQueueSize) {
479
+ reject(new Error(`Rate limiter queue full (${this.config.maxQueueSize}). Try again later.`));
480
+ return;
481
+ }
482
+ const id = `req_${++this.requestIdCounter}`;
483
+ const queued = {
484
+ id,
485
+ model: modelId,
486
+ enqueuedAt: Date.now(),
487
+ resolve,
488
+ reject,
489
+ };
490
+ this.requestQueue.push(queued);
491
+ if (this.config.verbose) {
492
+ console.log(chalk.gray(` ⏳ Request queued for ${MODELS[modelId]?.name || modelId} (queue: ${this.requestQueue.length})`));
493
+ }
494
+ // Set a timeout to reject if waiting too long
495
+ setTimeout(() => {
496
+ const idx = this.requestQueue.findIndex(r => r.id === id);
497
+ if (idx !== -1) {
498
+ this.requestQueue.splice(idx, 1);
499
+ reject(new Error(`Request timed out waiting in rate limiter queue (${this.config.maxQueueWaitMs / 1000}s)`));
500
+ }
501
+ }, this.config.maxQueueWaitMs);
502
+ });
503
+ }
504
+ startQueueProcessor() {
505
+ // Process queue every 2 seconds
506
+ this.queueProcessTimer = setInterval(() => {
507
+ this.processQueue();
508
+ }, 2000);
509
+ }
510
+ processQueue() {
511
+ if (this.requestQueue.length === 0)
512
+ return;
513
+ const now = Date.now();
514
+ const toProcess = [];
515
+ // Find requests that can now proceed
516
+ for (let i = this.requestQueue.length - 1; i >= 0; i--) {
517
+ const req = this.requestQueue[i];
518
+ // Check if request has been waiting too long
519
+ if (now - req.enqueuedAt > this.config.maxQueueWaitMs) {
520
+ this.requestQueue.splice(i, 1);
521
+ req.reject(new Error('Request timed out in rate limiter queue'));
522
+ continue;
523
+ }
524
+ const check = this.checkRateLimit(req.model);
525
+ if (check.allowed) {
526
+ toProcess.push(req);
527
+ this.requestQueue.splice(i, 1);
528
+ }
529
+ else if (check.suggestedModel) {
530
+ // Try alternative model
531
+ const altCheck = this.checkRateLimit(check.suggestedModel);
532
+ if (altCheck.allowed) {
533
+ this.recordRequestStart(check.suggestedModel);
534
+ req.resolve({ ...altCheck, suggestedModel: check.suggestedModel, reason: check.reason });
535
+ this.requestQueue.splice(i, 1);
536
+ }
537
+ }
538
+ }
539
+ // Process approved requests
540
+ for (const req of toProcess) {
541
+ this.recordRequestStart(req.model);
542
+ req.resolve({ allowed: true, waitMs: 0, suggestedModel: null, reason: 'Queued request approved' });
543
+ }
544
+ }
545
+ recordRateLimitPattern(modelId, cooldownMs) {
546
+ const hour = new Date().getHours();
547
+ let patterns = this.rateLimitPatterns.get(modelId);
548
+ if (!patterns) {
549
+ patterns = [];
550
+ this.rateLimitPatterns.set(modelId, patterns);
551
+ }
552
+ let existing = patterns.find(p => p.hourOfDay === hour);
553
+ if (!existing) {
554
+ existing = { hourOfDay: hour, avgCooldownMs: cooldownMs, count: 1 };
555
+ patterns.push(existing);
556
+ }
557
+ else {
558
+ existing.avgCooldownMs = (existing.avgCooldownMs * existing.count + cooldownMs) / (existing.count + 1);
559
+ existing.count++;
560
+ }
561
+ }
562
+ getHealthBar(health) {
563
+ const filled = Math.round(health / 10);
564
+ const empty = 10 - filled;
565
+ let color;
566
+ if (health >= 70)
567
+ color = chalk.green;
568
+ else if (health >= 40)
569
+ color = chalk.yellow;
570
+ else
571
+ color = chalk.red;
572
+ return color('█'.repeat(filled) + '░'.repeat(empty));
573
+ }
574
+ /**
575
+ * Cleanup - call when shutting down
576
+ */
577
+ dispose() {
578
+ if (this.queueProcessTimer) {
579
+ clearInterval(this.queueProcessTimer);
580
+ this.queueProcessTimer = null;
581
+ }
582
+ // Reject all queued requests
583
+ for (const req of this.requestQueue) {
584
+ req.reject(new Error('Rate limiter shutting down'));
585
+ }
586
+ this.requestQueue = [];
587
+ }
588
+ }
589
+ //# sourceMappingURL=rate-limiter.js.map
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // ============================================================
3
3
  // NeuroCLI - Advanced AI Terminal Coding Assistant
4
- // Main Entry Point - v4.1.3 with robust error handling
4
+ // Main Entry Point - v5.0 (Claude Code-style Agentic Loop)
5
5
  // ============================================================
6
6
  import { Command } from 'commander';
7
7
  import { createInterface } from 'readline';
@@ -16,7 +16,7 @@ import { HeadlessMode } from './core/headless.js';
16
16
  import { ShellCompletionGenerator } from './core/shell-completion.js';
17
17
  import chalk from 'chalk';
18
18
  import { AutoUpdater } from './core/updater.js';
19
- const VERSION = '4.3.0';
19
+ const VERSION = '5.0.0';
20
20
  // ---- Global Error Handlers (prevent crashes) ----
21
21
  process.on('unhandledRejection', (reason) => {
22
22
  console.error(chalk.red('\n⚠️ Unhandled promise rejection:'), reason);
@@ -21,7 +21,7 @@ export declare class TerminalUI {
21
21
  approvalRequest(toolName: string, args: Record<string, unknown>, risk: 'low' | 'medium' | 'high'): boolean;
22
22
  tokenUsage(usage: TokenUsage, modelId: string): void;
23
23
  sessionStats(totalInput: number, totalOutput: number, totalCost: number): void;
24
- agentActivity(agentName: string, status: 'starting' | 'working' | 'done' | 'error'): void;
24
+ agentActivity(agentName: string, status: 'starting' | 'working' | 'done' | 'error', detail?: string): void;
25
25
  error(message: string): void;
26
26
  info(message: string): void;
27
27
  success(message: string): void;
@@ -165,7 +165,7 @@ export class TerminalUI {
165
165
  console.log(` ${this.theme.muted('Session:')}`, this.theme.dim(`${totalInput.toLocaleString()} in · ${totalOutput.toLocaleString()} out · $${totalCost.toFixed(4)}`));
166
166
  }
167
167
  // ── Agent Activity (subtle indicator) ───────────────────
168
- agentActivity(agentName, status) {
168
+ agentActivity(agentName, status, detail) {
169
169
  const indicators = {
170
170
  starting: this.theme.muted('○'),
171
171
  working: this.theme.accent('◎'),
@@ -178,7 +178,8 @@ export class TerminalUI {
178
178
  done: 'done',
179
179
  error: 'failed',
180
180
  };
181
- console.log(` ${indicators[status]} ${this.theme.muted(agentName)} ${this.theme.dim(statusText[status])}`);
181
+ const detailStr = detail ? ` ${this.theme.dim(detail)}` : '';
182
+ console.log(` ${indicators[status]} ${this.theme.muted(agentName)} ${this.theme.dim(statusText[status])}${detailStr}`);
182
183
  }
183
184
  // ── Status Messages (minimal) ───────────────────────────
184
185
  error(message) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuro-cli",
3
- "version": "4.3.0",
3
+ "version": "5.0.1",
4
4
  "description": "Advanced AI-powered terminal coding assistant with multi-agent orchestration, sub-agent spawning, ACP protocol, OS-level sandboxing, spec-driven development, smart monitoring, multi-model routing, MCP Apps, auto-updater, and 23+ free models",
5
5
  "main": "dist/index.js",
6
6
  "bin": {