vibecodingmachine-core 2026.3.9-907 → 2026.3.10-1548

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 (42) hide show
  1. package/package.json +1 -1
  2. package/src/auth/access-denied.html +119 -119
  3. package/src/auth/shared-auth-storage.js +267 -267
  4. package/src/autonomous-mode/feature-implementer.cjs +70 -70
  5. package/src/autonomous-mode/feature-implementer.js +425 -425
  6. package/src/beta-request.js +160 -160
  7. package/src/chat-management/chat-manager.cjs +71 -71
  8. package/src/chat-management/chat-manager.js +342 -342
  9. package/src/compliance/compliance-prompt.js +183 -183
  10. package/src/ide-integration/aider-cli-manager.cjs +850 -850
  11. package/src/ide-integration/applescript-manager.cjs +3215 -3215
  12. package/src/ide-integration/applescript-utils.js +314 -314
  13. package/src/ide-integration/cdp-manager.cjs +221 -221
  14. package/src/ide-integration/claude-code-cli-manager.cjs +456 -456
  15. package/src/ide-integration/cline-cli-manager.cjs +2252 -2252
  16. package/src/ide-integration/continue-cli-manager.js +431 -431
  17. package/src/ide-integration/provider-manager.cjs +595 -595
  18. package/src/ide-integration/quota-detector.cjs +399 -399
  19. package/src/ide-integration/windows-automation-manager.js +532 -4
  20. package/src/ide-integration/windows-ide-manager.js +12 -3
  21. package/src/index.cjs +142 -142
  22. package/src/llm/direct-llm-manager.cjs +1299 -1299
  23. package/src/localization/index.js +147 -147
  24. package/src/quota-management/index.js +108 -108
  25. package/src/requirement-numbering.js +164 -164
  26. package/src/sync/aws-setup.js +445 -445
  27. package/src/ui/ButtonComponents.js +247 -247
  28. package/src/ui/ChatInterface.js +499 -499
  29. package/src/ui/StateManager.js +259 -259
  30. package/src/utils/audit-logger.cjs +116 -116
  31. package/src/utils/config-helpers.cjs +94 -94
  32. package/src/utils/config-helpers.js +94 -94
  33. package/src/utils/env-helpers.js +54 -54
  34. package/src/utils/error-reporter.js +117 -117
  35. package/src/utils/gcloud-auth.cjs +394 -394
  36. package/src/utils/git-branch-manager.js +278 -278
  37. package/src/utils/logger.cjs +193 -193
  38. package/src/utils/logger.js +191 -191
  39. package/src/utils/repo-helpers.cjs +120 -120
  40. package/src/utils/repo-helpers.js +120 -120
  41. package/src/utils/update-checker.js +246 -246
  42. package/src/utils/version-checker.js +170 -170
@@ -1,595 +1,595 @@
1
- /**
2
- * Provider Manager - Handles LLM provider rotation and rate limit tracking
3
- */
4
-
5
- const fs = require('fs');
6
- const path = require('path');
7
- const os = require('os');
8
-
9
- class ProviderManager {
10
- constructor() {
11
- this.rateLimitFile = path.join(os.homedir(), '.config', 'allnightai', 'rate-limits.json');
12
- this.performanceFile = path.join(os.homedir(), '.config', 'allnightai', 'provider-performance.json');
13
- this.rateLimits = this.loadRateLimits();
14
- this.performance = this.loadPerformance();
15
- }
16
-
17
- clearRateLimit(provider, model) {
18
- if (!provider) return false;
19
- const key = `${provider}:${model || provider}`;
20
- if (this.rateLimits[key]) {
21
- delete this.rateLimits[key];
22
- this.saveRateLimits();
23
- return true;
24
- }
25
- return false;
26
- }
27
-
28
- clearProviderRateLimits(provider) {
29
- if (!provider) return 0;
30
- const providerPrefix = `${provider}:`;
31
- let cleared = 0;
32
- for (const key of Object.keys(this.rateLimits)) {
33
- if (key.startsWith(providerPrefix)) {
34
- delete this.rateLimits[key];
35
- cleared += 1;
36
- }
37
- }
38
- if (cleared > 0) {
39
- this.saveRateLimits();
40
- }
41
- return cleared;
42
- }
43
-
44
- /**
45
- * Load rate limit tracking data from file
46
- * Auto-cleanup expired rate limits
47
- */
48
- loadRateLimits() {
49
- try {
50
- if (fs.existsSync(this.rateLimitFile)) {
51
- const data = fs.readFileSync(this.rateLimitFile, 'utf8');
52
- const limits = JSON.parse(data);
53
-
54
- // Auto-cleanup expired rate limits
55
- const now = Date.now();
56
- let cleaned = false;
57
- for (const key in limits) {
58
- if (limits[key].resetTime && limits[key].resetTime <= now) {
59
- delete limits[key];
60
- cleaned = true;
61
- }
62
- }
63
-
64
- // Save cleaned data if any expired limits were removed
65
- if (cleaned) {
66
- this.saveRateLimitsInternal(limits);
67
- }
68
-
69
- return limits;
70
- }
71
- } catch (err) {
72
- console.error('Error loading rate limits:', err.message);
73
- }
74
- return {};
75
- }
76
-
77
- /**
78
- * Internal save without reload (to prevent infinite loop during cleanup)
79
- */
80
- saveRateLimitsInternal(limits) {
81
- try {
82
- const dir = path.dirname(this.rateLimitFile);
83
- if (!fs.existsSync(dir)) {
84
- fs.mkdirSync(dir, { recursive: true });
85
- }
86
- fs.writeFileSync(this.rateLimitFile, JSON.stringify(limits, null, 2));
87
- } catch (err) {
88
- console.error('Error saving rate limits:', err.message);
89
- }
90
- }
91
-
92
- /**
93
- * Save rate limit tracking data to file
94
- */
95
- saveRateLimits() {
96
- try {
97
- const dir = path.dirname(this.rateLimitFile);
98
- if (!fs.existsSync(dir)) {
99
- fs.mkdirSync(dir, { recursive: true });
100
- }
101
- fs.writeFileSync(this.rateLimitFile, JSON.stringify(this.rateLimits, null, 2));
102
- } catch (err) {
103
- console.error('Error saving rate limits:', err.message);
104
- }
105
- }
106
-
107
- /**
108
- * Parse rate limit duration from error message
109
- * Examples:
110
- * "Please try again in 15m5.472s" -> 905472 ms
111
- * "Please try again in 13m21.792s" -> 801792 ms
112
- * "Please try again in 1h30m" -> 5400000 ms
113
- * "Session limit reached ∙ resets 12pm" -> ms until 12pm today/tomorrow
114
- */
115
- parseRateLimitDuration(errorMessage) {
116
- // Gemini / Google AI style: "You can resume using this model at 1/12/2026, 4:07:27 PM"
117
- // Also seen as: "resume using this model at <date>, <time>"
118
- const resumeAtMatch = errorMessage.match(/resume\s+(?:using\s+this\s+model\s+)?at\s+(\d{1,2}\/\d{1,2}\/\d{4})\s*,?\s*(\d{1,2}:\d{2}(?::\d{2})?)\s*(am|pm)/i);
119
- if (resumeAtMatch) {
120
- try {
121
- const datePart = resumeAtMatch[1];
122
- const timePart = resumeAtMatch[2];
123
- const meridiem = resumeAtMatch[3];
124
- const parsed = new Date(`${datePart}, ${timePart} ${meridiem.toUpperCase()}`);
125
- if (!Number.isNaN(parsed.getTime())) {
126
- const ms = parsed.getTime() - Date.now();
127
- return ms > 0 ? ms : 0;
128
- }
129
- } catch (_) { }
130
- }
131
-
132
- // Check for "resets [Month] [Day] at [Time]" format (e.g. "Spending cap reached resets Jan 17 at 12pm")
133
- const dateMatch = errorMessage.match(/resets\s+([a-zA-Z]+)\s+(\d{1,2})\s+at\s+(\d{1,2}(?::\d{2})?)\s*(am|pm)?/i);
134
- if (dateMatch) {
135
- try {
136
- const monthStr = dateMatch[1]; // Jan, February, etc.
137
- const day = parseInt(dateMatch[2]);
138
- const timeStr = dateMatch[3];
139
- const meridiem = dateMatch[4] ? dateMatch[4].toLowerCase() : null;
140
-
141
- // Parse time
142
- let [hours, minutes] = timeStr.split(':').map(n => parseInt(n));
143
- if (!minutes || Number.isNaN(minutes)) minutes = 0;
144
-
145
- if (meridiem === 'pm' && hours !== 12) hours += 12;
146
- if (meridiem === 'am' && hours === 12) hours = 0;
147
-
148
- // Parse month
149
- const months = {
150
- jan: 0, january: 0,
151
- feb: 1, february: 1,
152
- mar: 2, march: 2,
153
- apr: 3, april: 3,
154
- may: 4,
155
- jun: 5, june: 5,
156
- jul: 6, july: 6,
157
- aug: 7, august: 7,
158
- sep: 8, sept: 8, september: 8,
159
- oct: 9, october: 9,
160
- nov: 10, november: 10,
161
- dec: 11, december: 11
162
- };
163
-
164
- const monthIndex = months[monthStr.toLowerCase().substring(0, 3)] ?? months[monthStr.toLowerCase()];
165
-
166
- if (monthIndex !== undefined) {
167
- const currentYear = new Date().getFullYear();
168
- let parsed = new Date(currentYear, monthIndex, day, hours, minutes);
169
- const now = Date.now();
170
-
171
- // Logic to handle year crossing (reset in Jan, currently Dec)
172
- if (parsed.getTime() < now) {
173
- if (!Number.isNaN(parsed.getTime())) {
174
- if (parsed.getTime() < now) {
175
- const nextYearDate = new Date(parsed);
176
- nextYearDate.setFullYear(nextYearDate.getFullYear() + 1);
177
- if (nextYearDate.getTime() > now) {
178
- parsed = nextYearDate;
179
- }
180
- }
181
- }
182
- }
183
-
184
- const ms = parsed.getTime() - now;
185
- return ms > 0 ? ms : 0;
186
- }
187
- } catch (_) { }
188
- }
189
-
190
- // Check for Claude Code session limit format: "Session limit reached ∙ resets 12pm"
191
- const sessionMatch = errorMessage.match(/resets?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
192
- if (sessionMatch) {
193
- let hour = parseInt(sessionMatch[1]);
194
- const minute = sessionMatch[2] ? parseInt(sessionMatch[2]) : 0;
195
- const meridiem = sessionMatch[3] ? sessionMatch[3].toLowerCase() : null;
196
-
197
- // Convert to 24-hour format
198
- if (meridiem === 'pm' && hour !== 12) {
199
- hour += 12;
200
- } else if (meridiem === 'am' && hour === 12) {
201
- hour = 0;
202
- }
203
-
204
- // Calculate reset time
205
- const now = new Date();
206
- const resetTime = new Date(now);
207
- resetTime.setHours(hour, minute, 0, 0);
208
-
209
- // If reset time is in the past, it's tomorrow
210
- if (resetTime <= now) {
211
- resetTime.setDate(resetTime.getDate() + 1);
212
- }
213
-
214
- return resetTime.getTime() - now.getTime();
215
- }
216
-
217
- // Match patterns like "15m5.472s" or "1h30m" or "45s"
218
- const timeMatch = errorMessage.match(/try again in ([\d.]+h)?\s?([\d.]+m)?\s?([\d.]+s)?/i);
219
- if (!timeMatch) return null;
220
-
221
- let totalMs = 0;
222
-
223
- // Hours
224
- if (timeMatch[1]) {
225
- const hours = parseFloat(timeMatch[1].replace('h', ''));
226
- totalMs += hours * 60 * 60 * 1000;
227
- }
228
-
229
- // Minutes
230
- if (timeMatch[2]) {
231
- const minutes = parseFloat(timeMatch[2].replace('m', ''));
232
- totalMs += minutes * 60 * 1000;
233
- }
234
-
235
- // Seconds
236
- if (timeMatch[3]) {
237
- const seconds = parseFloat(timeMatch[3].replace('s', ''));
238
- totalMs += seconds * 1000;
239
- }
240
-
241
- return totalMs;
242
- }
243
-
244
- /**
245
- * Mark a provider as rate limited
246
- * @param {string} provider - Provider name (e.g., "groq", "anthropic")
247
- * @param {string} model - Model name (e.g., "llama-3.3-70b-versatile")
248
- * @param {string} errorMessage - Full error message containing duration
249
- */
250
- markRateLimited(provider, model, errorMessage) {
251
- // Normalize model: if model is missing/undefined, assume it's the provider name.
252
- model = model || provider;
253
-
254
- let duration = this.parseRateLimitDuration(errorMessage);
255
- if (duration === null || duration === undefined) {
256
- console.warn(`Could not parse rate limit duration from: ${errorMessage}`);
257
- // Don't guess the duration - just mark as rate limited without a specific reset time
258
- duration = null;
259
- }
260
-
261
- const resetTime = duration ? Date.now() + duration : null;
262
- const key = `${provider}:${model}`;
263
-
264
- this.rateLimits[key] = {
265
- provider,
266
- model,
267
- resetTime,
268
- resetDate: resetTime ? new Date(resetTime).toISOString() : null,
269
- reason: errorMessage,
270
- markedAt: new Date().toISOString()
271
- };
272
-
273
- this.saveRateLimits();
274
-
275
- if (resetTime) {
276
- const chalk = require('chalk');
277
- const resetMinutes = Math.ceil(duration / 60000);
278
- const resetDateTime = new Date(resetTime);
279
- const timeString = resetDateTime.toLocaleTimeString('en-US', {
280
- timeZone: 'America/Denver',
281
- hour: 'numeric',
282
- minute: '2-digit',
283
- hour12: true
284
- });
285
- const dateString = resetDateTime.toLocaleDateString('en-US', {
286
- timeZone: 'America/Denver',
287
- weekday: 'short',
288
- month: 'short',
289
- day: 'numeric'
290
- });
291
- console.log(chalk.yellow(`📊 Provider ${provider}/${model} rate limited until ${timeString} MST on ${dateString} (~${resetMinutes}m)`));
292
- } else {
293
- const chalk = require('chalk');
294
- console.log(chalk.yellow(`📊 Provider ${provider}/${model} rate limited (reset time unknown)`));
295
- }
296
- }
297
-
298
- /**
299
- * Check if a provider is currently rate limited
300
- * @param {string} provider - Provider name
301
- * @param {string} model - Model name (optional, will check all models if not provided)
302
- * @returns {boolean}
303
- */
304
- isRateLimited(provider, model) {
305
- if (!model) {
306
- // Check if ANY model for this provider is rate limited
307
- const providerPrefix = `${provider}:`;
308
- for (const key in this.rateLimits) {
309
- if (key.startsWith(providerPrefix)) {
310
- const limit = this.rateLimits[key];
311
- if (limit && Date.now() < limit.resetTime) {
312
- return true;
313
- }
314
- }
315
- }
316
- return false;
317
- }
318
-
319
- const key = `${provider}:${model}`;
320
- const limit = this.rateLimits[key];
321
-
322
- if (limit) {
323
- // Check if rate limit has expired
324
- if (Date.now() >= limit.resetTime) {
325
- // Clean up expired rate limit
326
- delete this.rateLimits[key];
327
- this.saveRateLimits();
328
- return false;
329
- }
330
-
331
- return true;
332
- }
333
-
334
- // Fall back to any provider-level entries (handles older or model-agnostic keys)
335
- const providerPrefix = `${provider}:`;
336
- for (const k in this.rateLimits) {
337
- if (k.startsWith(providerPrefix)) {
338
- const l = this.rateLimits[k];
339
- if (l && Date.now() < l.resetTime) return true;
340
- }
341
- }
342
-
343
- return false;
344
- }
345
-
346
- /**
347
- * Get rate limit information for a provider
348
- * @param {string} provider - Provider name
349
- * @param {string} model - Model name (optional)
350
- * @returns {Object} - {isRateLimited: boolean, resetTime: number, reason: string}
351
- */
352
- getRateLimitInfo(provider, model) {
353
- // If model not specified, check all models for this provider
354
- if (!model) {
355
- const providerPrefix = `${provider}:`;
356
- let earliestReset = null;
357
- let reason = null;
358
-
359
- for (const key in this.rateLimits) {
360
- if (key.startsWith(providerPrefix)) {
361
- const limit = this.rateLimits[key];
362
- if (limit && Date.now() < limit.resetTime) {
363
- if (!earliestReset || limit.resetTime < earliestReset) {
364
- earliestReset = limit.resetTime;
365
- reason = limit.reason;
366
- }
367
- }
368
- }
369
- }
370
-
371
- if (earliestReset) {
372
- return {
373
- isRateLimited: true,
374
- resetTime: earliestReset,
375
- reason: reason
376
- };
377
- }
378
- } else {
379
- const key = `${provider}:${model}`;
380
- const limit = this.rateLimits[key];
381
-
382
- if (limit && Date.now() < limit.resetTime) {
383
- return {
384
- isRateLimited: true,
385
- resetTime: limit.resetTime,
386
- reason: limit.reason
387
- };
388
- }
389
-
390
- // Fallback: check other keys for this provider and return earliest reset
391
- const providerPrefix = `${provider}:`;
392
- let earliestReset = null;
393
- let reason = null;
394
-
395
- for (const k in this.rateLimits) {
396
- if (k.startsWith(providerPrefix)) {
397
- const l = this.rateLimits[k];
398
- if (l && Date.now() < l.resetTime) {
399
- if (!earliestReset || l.resetTime < earliestReset) {
400
- earliestReset = l.resetTime;
401
- reason = l.reason;
402
- }
403
- }
404
- }
405
- }
406
-
407
- if (earliestReset) {
408
- return {
409
- isRateLimited: true,
410
- resetTime: earliestReset,
411
- reason
412
- };
413
- }
414
- }
415
-
416
- return {
417
- isRateLimited: false,
418
- resetTime: null,
419
- reason: null
420
- };
421
- }
422
-
423
- /**
424
- * Get available provider (not rate limited)
425
- * @param {Array} providers - Array of {provider, model, apiKey} objects
426
- * @returns {Object|null} - Available provider object or null
427
- */
428
- getAvailableProvider(providers) {
429
- for (const providerConfig of providers) {
430
- if (!this.isRateLimited(providerConfig.provider, providerConfig.model)) {
431
- return providerConfig;
432
- }
433
- }
434
- return null;
435
- }
436
-
437
- /**
438
- * Get time remaining until provider is available again
439
- * @param {string} provider
440
- * @param {string} model
441
- * @returns {number|null} - Milliseconds until reset, or null if not rate limited
442
- */
443
- getTimeUntilReset(provider, model) {
444
- const key = `${provider}:${model}`;
445
- const limit = this.rateLimits[key];
446
-
447
- if (limit && Date.now() < limit.resetTime) {
448
- const remaining = limit.resetTime - Date.now();
449
- return remaining > 0 ? remaining : null;
450
- }
451
-
452
- // Fallback: check other keys for this provider (handles legacy or model-agnostic entries)
453
- const providerPrefix = `${provider}:`;
454
- let earliest = null;
455
- for (const k in this.rateLimits) {
456
- if (k.startsWith(providerPrefix)) {
457
- const l = this.rateLimits[k];
458
- if (l && Date.now() < l.resetTime) {
459
- if (!earliest || l.resetTime < earliest) earliest = l.resetTime;
460
- }
461
- }
462
- }
463
-
464
- if (!earliest) return null;
465
- const remaining = earliest - Date.now();
466
- return remaining > 0 ? remaining : null;
467
- }
468
-
469
- /**
470
- * Get status of all providers
471
- * @param {Array} providers - Array of provider configs
472
- * @returns {Array} - Array of provider status objects
473
- */
474
- getProviderStatus(providers) {
475
- return providers.map(p => {
476
- const isLimited = this.isRateLimited(p.provider, p.model);
477
- const timeUntilReset = this.getTimeUntilReset(p.provider, p.model);
478
-
479
- return {
480
- provider: p.provider,
481
- model: p.model,
482
- available: !isLimited,
483
- rateLimited: isLimited,
484
- resetIn: timeUntilReset ? Math.ceil(timeUntilReset / 60000) + 'm' : null
485
- };
486
- });
487
- }
488
-
489
- /**
490
- * Clear all rate limits (useful for testing)
491
- */
492
- clearAllRateLimits() {
493
- this.rateLimits = {};
494
- this.saveRateLimits();
495
- }
496
-
497
- /**
498
- * Load performance tracking data
499
- */
500
- loadPerformance() {
501
- try {
502
- if (fs.existsSync(this.performanceFile)) {
503
- return JSON.parse(fs.readFileSync(this.performanceFile, 'utf8'));
504
- }
505
- } catch (err) {
506
- // Ignore errors
507
- }
508
- return {};
509
- }
510
-
511
- /**
512
- * Save performance tracking data
513
- */
514
- savePerformance() {
515
- try {
516
- const dir = path.dirname(this.performanceFile);
517
- if (!fs.existsSync(dir)) {
518
- fs.mkdirSync(dir, { recursive: true });
519
- }
520
- fs.writeFileSync(this.performanceFile, JSON.stringify(this.performance, null, 2));
521
- } catch (err) {
522
- // Ignore errors
523
- }
524
- }
525
-
526
- /**
527
- * Record performance metric for a provider
528
- * @param {string} provider - Provider name
529
- * @param {string} model - Model name
530
- * @param {number} durationMs - Duration in milliseconds
531
- */
532
- recordPerformance(provider, model, durationMs) {
533
- const key = `${provider}:${model}`;
534
-
535
- if (!this.performance[key]) {
536
- this.performance[key] = {
537
- provider,
538
- model,
539
- samples: [],
540
- avgSpeed: 0
541
- };
542
- }
543
-
544
- const perf = this.performance[key];
545
- perf.samples.push(durationMs);
546
-
547
- // Keep only last 10 samples
548
- if (perf.samples.length > 10) {
549
- perf.samples.shift();
550
- }
551
-
552
- // Calculate average
553
- perf.avgSpeed = perf.samples.reduce((a, b) => a + b, 0) / perf.samples.length;
554
- perf.lastUsed = Date.now();
555
-
556
- this.savePerformance();
557
- }
558
-
559
- /**
560
- * Get fastest available provider from a list
561
- * @param {Array} providers - Array of {provider, model, ...} objects
562
- * @returns {Object|null} - Fastest available provider or null
563
- */
564
- getFastestAvailable(providers) {
565
- const available = providers.filter(p => !this.isRateLimited(p.provider, p.model));
566
-
567
- if (available.length === 0) return null;
568
- if (available.length === 1) return available[0];
569
-
570
- // Sort by average speed (fastest first)
571
- available.sort((a, b) => {
572
- const aKey = `${a.provider}:${a.model}`;
573
- const bKey = `${b.provider}:${b.model}`;
574
- const aAvg = this.performance[aKey]?.avgSpeed;
575
- const bAvg = this.performance[bKey]?.avgSpeed;
576
- const aSpeed = (aAvg ?? a.estimatedSpeed ?? Infinity);
577
- const bSpeed = (bAvg ?? b.estimatedSpeed ?? Infinity);
578
- return aSpeed - bSpeed;
579
- });
580
-
581
- return available[0];
582
- }
583
-
584
- /**
585
- * Get provider rankings by speed
586
- * @returns {Array} - Sorted array of {provider, model, avgSpeed}
587
- */
588
- getProviderRankings() {
589
- return Object.values(this.performance)
590
- .filter(p => p.avgSpeed > 0)
591
- .sort((a, b) => a.avgSpeed - b.avgSpeed);
592
- }
593
- }
594
-
595
- module.exports = ProviderManager;
1
+ /**
2
+ * Provider Manager - Handles LLM provider rotation and rate limit tracking
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ class ProviderManager {
10
+ constructor() {
11
+ this.rateLimitFile = path.join(os.homedir(), '.config', 'allnightai', 'rate-limits.json');
12
+ this.performanceFile = path.join(os.homedir(), '.config', 'allnightai', 'provider-performance.json');
13
+ this.rateLimits = this.loadRateLimits();
14
+ this.performance = this.loadPerformance();
15
+ }
16
+
17
+ clearRateLimit(provider, model) {
18
+ if (!provider) return false;
19
+ const key = `${provider}:${model || provider}`;
20
+ if (this.rateLimits[key]) {
21
+ delete this.rateLimits[key];
22
+ this.saveRateLimits();
23
+ return true;
24
+ }
25
+ return false;
26
+ }
27
+
28
+ clearProviderRateLimits(provider) {
29
+ if (!provider) return 0;
30
+ const providerPrefix = `${provider}:`;
31
+ let cleared = 0;
32
+ for (const key of Object.keys(this.rateLimits)) {
33
+ if (key.startsWith(providerPrefix)) {
34
+ delete this.rateLimits[key];
35
+ cleared += 1;
36
+ }
37
+ }
38
+ if (cleared > 0) {
39
+ this.saveRateLimits();
40
+ }
41
+ return cleared;
42
+ }
43
+
44
+ /**
45
+ * Load rate limit tracking data from file
46
+ * Auto-cleanup expired rate limits
47
+ */
48
+ loadRateLimits() {
49
+ try {
50
+ if (fs.existsSync(this.rateLimitFile)) {
51
+ const data = fs.readFileSync(this.rateLimitFile, 'utf8');
52
+ const limits = JSON.parse(data);
53
+
54
+ // Auto-cleanup expired rate limits
55
+ const now = Date.now();
56
+ let cleaned = false;
57
+ for (const key in limits) {
58
+ if (limits[key].resetTime && limits[key].resetTime <= now) {
59
+ delete limits[key];
60
+ cleaned = true;
61
+ }
62
+ }
63
+
64
+ // Save cleaned data if any expired limits were removed
65
+ if (cleaned) {
66
+ this.saveRateLimitsInternal(limits);
67
+ }
68
+
69
+ return limits;
70
+ }
71
+ } catch (err) {
72
+ console.error('Error loading rate limits:', err.message);
73
+ }
74
+ return {};
75
+ }
76
+
77
+ /**
78
+ * Internal save without reload (to prevent infinite loop during cleanup)
79
+ */
80
+ saveRateLimitsInternal(limits) {
81
+ try {
82
+ const dir = path.dirname(this.rateLimitFile);
83
+ if (!fs.existsSync(dir)) {
84
+ fs.mkdirSync(dir, { recursive: true });
85
+ }
86
+ fs.writeFileSync(this.rateLimitFile, JSON.stringify(limits, null, 2));
87
+ } catch (err) {
88
+ console.error('Error saving rate limits:', err.message);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Save rate limit tracking data to file
94
+ */
95
+ saveRateLimits() {
96
+ try {
97
+ const dir = path.dirname(this.rateLimitFile);
98
+ if (!fs.existsSync(dir)) {
99
+ fs.mkdirSync(dir, { recursive: true });
100
+ }
101
+ fs.writeFileSync(this.rateLimitFile, JSON.stringify(this.rateLimits, null, 2));
102
+ } catch (err) {
103
+ console.error('Error saving rate limits:', err.message);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Parse rate limit duration from error message
109
+ * Examples:
110
+ * "Please try again in 15m5.472s" -> 905472 ms
111
+ * "Please try again in 13m21.792s" -> 801792 ms
112
+ * "Please try again in 1h30m" -> 5400000 ms
113
+ * "Session limit reached ∙ resets 12pm" -> ms until 12pm today/tomorrow
114
+ */
115
+ parseRateLimitDuration(errorMessage) {
116
+ // Gemini / Google AI style: "You can resume using this model at 1/12/2026, 4:07:27 PM"
117
+ // Also seen as: "resume using this model at <date>, <time>"
118
+ const resumeAtMatch = errorMessage.match(/resume\s+(?:using\s+this\s+model\s+)?at\s+(\d{1,2}\/\d{1,2}\/\d{4})\s*,?\s*(\d{1,2}:\d{2}(?::\d{2})?)\s*(am|pm)/i);
119
+ if (resumeAtMatch) {
120
+ try {
121
+ const datePart = resumeAtMatch[1];
122
+ const timePart = resumeAtMatch[2];
123
+ const meridiem = resumeAtMatch[3];
124
+ const parsed = new Date(`${datePart}, ${timePart} ${meridiem.toUpperCase()}`);
125
+ if (!Number.isNaN(parsed.getTime())) {
126
+ const ms = parsed.getTime() - Date.now();
127
+ return ms > 0 ? ms : 0;
128
+ }
129
+ } catch (_) { }
130
+ }
131
+
132
+ // Check for "resets [Month] [Day] at [Time]" format (e.g. "Spending cap reached resets Jan 17 at 12pm")
133
+ const dateMatch = errorMessage.match(/resets\s+([a-zA-Z]+)\s+(\d{1,2})\s+at\s+(\d{1,2}(?::\d{2})?)\s*(am|pm)?/i);
134
+ if (dateMatch) {
135
+ try {
136
+ const monthStr = dateMatch[1]; // Jan, February, etc.
137
+ const day = parseInt(dateMatch[2]);
138
+ const timeStr = dateMatch[3];
139
+ const meridiem = dateMatch[4] ? dateMatch[4].toLowerCase() : null;
140
+
141
+ // Parse time
142
+ let [hours, minutes] = timeStr.split(':').map(n => parseInt(n));
143
+ if (!minutes || Number.isNaN(minutes)) minutes = 0;
144
+
145
+ if (meridiem === 'pm' && hours !== 12) hours += 12;
146
+ if (meridiem === 'am' && hours === 12) hours = 0;
147
+
148
+ // Parse month
149
+ const months = {
150
+ jan: 0, january: 0,
151
+ feb: 1, february: 1,
152
+ mar: 2, march: 2,
153
+ apr: 3, april: 3,
154
+ may: 4,
155
+ jun: 5, june: 5,
156
+ jul: 6, july: 6,
157
+ aug: 7, august: 7,
158
+ sep: 8, sept: 8, september: 8,
159
+ oct: 9, october: 9,
160
+ nov: 10, november: 10,
161
+ dec: 11, december: 11
162
+ };
163
+
164
+ const monthIndex = months[monthStr.toLowerCase().substring(0, 3)] ?? months[monthStr.toLowerCase()];
165
+
166
+ if (monthIndex !== undefined) {
167
+ const currentYear = new Date().getFullYear();
168
+ let parsed = new Date(currentYear, monthIndex, day, hours, minutes);
169
+ const now = Date.now();
170
+
171
+ // Logic to handle year crossing (reset in Jan, currently Dec)
172
+ if (parsed.getTime() < now) {
173
+ if (!Number.isNaN(parsed.getTime())) {
174
+ if (parsed.getTime() < now) {
175
+ const nextYearDate = new Date(parsed);
176
+ nextYearDate.setFullYear(nextYearDate.getFullYear() + 1);
177
+ if (nextYearDate.getTime() > now) {
178
+ parsed = nextYearDate;
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ const ms = parsed.getTime() - now;
185
+ return ms > 0 ? ms : 0;
186
+ }
187
+ } catch (_) { }
188
+ }
189
+
190
+ // Check for Claude Code session limit format: "Session limit reached ∙ resets 12pm"
191
+ const sessionMatch = errorMessage.match(/resets?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
192
+ if (sessionMatch) {
193
+ let hour = parseInt(sessionMatch[1]);
194
+ const minute = sessionMatch[2] ? parseInt(sessionMatch[2]) : 0;
195
+ const meridiem = sessionMatch[3] ? sessionMatch[3].toLowerCase() : null;
196
+
197
+ // Convert to 24-hour format
198
+ if (meridiem === 'pm' && hour !== 12) {
199
+ hour += 12;
200
+ } else if (meridiem === 'am' && hour === 12) {
201
+ hour = 0;
202
+ }
203
+
204
+ // Calculate reset time
205
+ const now = new Date();
206
+ const resetTime = new Date(now);
207
+ resetTime.setHours(hour, minute, 0, 0);
208
+
209
+ // If reset time is in the past, it's tomorrow
210
+ if (resetTime <= now) {
211
+ resetTime.setDate(resetTime.getDate() + 1);
212
+ }
213
+
214
+ return resetTime.getTime() - now.getTime();
215
+ }
216
+
217
+ // Match patterns like "15m5.472s" or "1h30m" or "45s"
218
+ const timeMatch = errorMessage.match(/try again in ([\d.]+h)?\s?([\d.]+m)?\s?([\d.]+s)?/i);
219
+ if (!timeMatch) return null;
220
+
221
+ let totalMs = 0;
222
+
223
+ // Hours
224
+ if (timeMatch[1]) {
225
+ const hours = parseFloat(timeMatch[1].replace('h', ''));
226
+ totalMs += hours * 60 * 60 * 1000;
227
+ }
228
+
229
+ // Minutes
230
+ if (timeMatch[2]) {
231
+ const minutes = parseFloat(timeMatch[2].replace('m', ''));
232
+ totalMs += minutes * 60 * 1000;
233
+ }
234
+
235
+ // Seconds
236
+ if (timeMatch[3]) {
237
+ const seconds = parseFloat(timeMatch[3].replace('s', ''));
238
+ totalMs += seconds * 1000;
239
+ }
240
+
241
+ return totalMs;
242
+ }
243
+
244
+ /**
245
+ * Mark a provider as rate limited
246
+ * @param {string} provider - Provider name (e.g., "groq", "anthropic")
247
+ * @param {string} model - Model name (e.g., "llama-3.3-70b-versatile")
248
+ * @param {string} errorMessage - Full error message containing duration
249
+ */
250
+ markRateLimited(provider, model, errorMessage) {
251
+ // Normalize model: if model is missing/undefined, assume it's the provider name.
252
+ model = model || provider;
253
+
254
+ let duration = this.parseRateLimitDuration(errorMessage);
255
+ if (duration === null || duration === undefined) {
256
+ console.warn(`Could not parse rate limit duration from: ${errorMessage}`);
257
+ // Don't guess the duration - just mark as rate limited without a specific reset time
258
+ duration = null;
259
+ }
260
+
261
+ const resetTime = duration ? Date.now() + duration : null;
262
+ const key = `${provider}:${model}`;
263
+
264
+ this.rateLimits[key] = {
265
+ provider,
266
+ model,
267
+ resetTime,
268
+ resetDate: resetTime ? new Date(resetTime).toISOString() : null,
269
+ reason: errorMessage,
270
+ markedAt: new Date().toISOString()
271
+ };
272
+
273
+ this.saveRateLimits();
274
+
275
+ if (resetTime) {
276
+ const chalk = require('chalk');
277
+ const resetMinutes = Math.ceil(duration / 60000);
278
+ const resetDateTime = new Date(resetTime);
279
+ const timeString = resetDateTime.toLocaleTimeString('en-US', {
280
+ timeZone: 'America/Denver',
281
+ hour: 'numeric',
282
+ minute: '2-digit',
283
+ hour12: true
284
+ });
285
+ const dateString = resetDateTime.toLocaleDateString('en-US', {
286
+ timeZone: 'America/Denver',
287
+ weekday: 'short',
288
+ month: 'short',
289
+ day: 'numeric'
290
+ });
291
+ console.log(chalk.yellow(`📊 Provider ${provider}/${model} rate limited until ${timeString} MST on ${dateString} (~${resetMinutes}m)`));
292
+ } else {
293
+ const chalk = require('chalk');
294
+ console.log(chalk.yellow(`📊 Provider ${provider}/${model} rate limited (reset time unknown)`));
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Check if a provider is currently rate limited
300
+ * @param {string} provider - Provider name
301
+ * @param {string} model - Model name (optional, will check all models if not provided)
302
+ * @returns {boolean}
303
+ */
304
+ isRateLimited(provider, model) {
305
+ if (!model) {
306
+ // Check if ANY model for this provider is rate limited
307
+ const providerPrefix = `${provider}:`;
308
+ for (const key in this.rateLimits) {
309
+ if (key.startsWith(providerPrefix)) {
310
+ const limit = this.rateLimits[key];
311
+ if (limit && Date.now() < limit.resetTime) {
312
+ return true;
313
+ }
314
+ }
315
+ }
316
+ return false;
317
+ }
318
+
319
+ const key = `${provider}:${model}`;
320
+ const limit = this.rateLimits[key];
321
+
322
+ if (limit) {
323
+ // Check if rate limit has expired
324
+ if (Date.now() >= limit.resetTime) {
325
+ // Clean up expired rate limit
326
+ delete this.rateLimits[key];
327
+ this.saveRateLimits();
328
+ return false;
329
+ }
330
+
331
+ return true;
332
+ }
333
+
334
+ // Fall back to any provider-level entries (handles older or model-agnostic keys)
335
+ const providerPrefix = `${provider}:`;
336
+ for (const k in this.rateLimits) {
337
+ if (k.startsWith(providerPrefix)) {
338
+ const l = this.rateLimits[k];
339
+ if (l && Date.now() < l.resetTime) return true;
340
+ }
341
+ }
342
+
343
+ return false;
344
+ }
345
+
346
+ /**
347
+ * Get rate limit information for a provider
348
+ * @param {string} provider - Provider name
349
+ * @param {string} model - Model name (optional)
350
+ * @returns {Object} - {isRateLimited: boolean, resetTime: number, reason: string}
351
+ */
352
+ getRateLimitInfo(provider, model) {
353
+ // If model not specified, check all models for this provider
354
+ if (!model) {
355
+ const providerPrefix = `${provider}:`;
356
+ let earliestReset = null;
357
+ let reason = null;
358
+
359
+ for (const key in this.rateLimits) {
360
+ if (key.startsWith(providerPrefix)) {
361
+ const limit = this.rateLimits[key];
362
+ if (limit && Date.now() < limit.resetTime) {
363
+ if (!earliestReset || limit.resetTime < earliestReset) {
364
+ earliestReset = limit.resetTime;
365
+ reason = limit.reason;
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ if (earliestReset) {
372
+ return {
373
+ isRateLimited: true,
374
+ resetTime: earliestReset,
375
+ reason: reason
376
+ };
377
+ }
378
+ } else {
379
+ const key = `${provider}:${model}`;
380
+ const limit = this.rateLimits[key];
381
+
382
+ if (limit && Date.now() < limit.resetTime) {
383
+ return {
384
+ isRateLimited: true,
385
+ resetTime: limit.resetTime,
386
+ reason: limit.reason
387
+ };
388
+ }
389
+
390
+ // Fallback: check other keys for this provider and return earliest reset
391
+ const providerPrefix = `${provider}:`;
392
+ let earliestReset = null;
393
+ let reason = null;
394
+
395
+ for (const k in this.rateLimits) {
396
+ if (k.startsWith(providerPrefix)) {
397
+ const l = this.rateLimits[k];
398
+ if (l && Date.now() < l.resetTime) {
399
+ if (!earliestReset || l.resetTime < earliestReset) {
400
+ earliestReset = l.resetTime;
401
+ reason = l.reason;
402
+ }
403
+ }
404
+ }
405
+ }
406
+
407
+ if (earliestReset) {
408
+ return {
409
+ isRateLimited: true,
410
+ resetTime: earliestReset,
411
+ reason
412
+ };
413
+ }
414
+ }
415
+
416
+ return {
417
+ isRateLimited: false,
418
+ resetTime: null,
419
+ reason: null
420
+ };
421
+ }
422
+
423
+ /**
424
+ * Get available provider (not rate limited)
425
+ * @param {Array} providers - Array of {provider, model, apiKey} objects
426
+ * @returns {Object|null} - Available provider object or null
427
+ */
428
+ getAvailableProvider(providers) {
429
+ for (const providerConfig of providers) {
430
+ if (!this.isRateLimited(providerConfig.provider, providerConfig.model)) {
431
+ return providerConfig;
432
+ }
433
+ }
434
+ return null;
435
+ }
436
+
437
+ /**
438
+ * Get time remaining until provider is available again
439
+ * @param {string} provider
440
+ * @param {string} model
441
+ * @returns {number|null} - Milliseconds until reset, or null if not rate limited
442
+ */
443
+ getTimeUntilReset(provider, model) {
444
+ const key = `${provider}:${model}`;
445
+ const limit = this.rateLimits[key];
446
+
447
+ if (limit && Date.now() < limit.resetTime) {
448
+ const remaining = limit.resetTime - Date.now();
449
+ return remaining > 0 ? remaining : null;
450
+ }
451
+
452
+ // Fallback: check other keys for this provider (handles legacy or model-agnostic entries)
453
+ const providerPrefix = `${provider}:`;
454
+ let earliest = null;
455
+ for (const k in this.rateLimits) {
456
+ if (k.startsWith(providerPrefix)) {
457
+ const l = this.rateLimits[k];
458
+ if (l && Date.now() < l.resetTime) {
459
+ if (!earliest || l.resetTime < earliest) earliest = l.resetTime;
460
+ }
461
+ }
462
+ }
463
+
464
+ if (!earliest) return null;
465
+ const remaining = earliest - Date.now();
466
+ return remaining > 0 ? remaining : null;
467
+ }
468
+
469
+ /**
470
+ * Get status of all providers
471
+ * @param {Array} providers - Array of provider configs
472
+ * @returns {Array} - Array of provider status objects
473
+ */
474
+ getProviderStatus(providers) {
475
+ return providers.map(p => {
476
+ const isLimited = this.isRateLimited(p.provider, p.model);
477
+ const timeUntilReset = this.getTimeUntilReset(p.provider, p.model);
478
+
479
+ return {
480
+ provider: p.provider,
481
+ model: p.model,
482
+ available: !isLimited,
483
+ rateLimited: isLimited,
484
+ resetIn: timeUntilReset ? Math.ceil(timeUntilReset / 60000) + 'm' : null
485
+ };
486
+ });
487
+ }
488
+
489
+ /**
490
+ * Clear all rate limits (useful for testing)
491
+ */
492
+ clearAllRateLimits() {
493
+ this.rateLimits = {};
494
+ this.saveRateLimits();
495
+ }
496
+
497
+ /**
498
+ * Load performance tracking data
499
+ */
500
+ loadPerformance() {
501
+ try {
502
+ if (fs.existsSync(this.performanceFile)) {
503
+ return JSON.parse(fs.readFileSync(this.performanceFile, 'utf8'));
504
+ }
505
+ } catch (err) {
506
+ // Ignore errors
507
+ }
508
+ return {};
509
+ }
510
+
511
+ /**
512
+ * Save performance tracking data
513
+ */
514
+ savePerformance() {
515
+ try {
516
+ const dir = path.dirname(this.performanceFile);
517
+ if (!fs.existsSync(dir)) {
518
+ fs.mkdirSync(dir, { recursive: true });
519
+ }
520
+ fs.writeFileSync(this.performanceFile, JSON.stringify(this.performance, null, 2));
521
+ } catch (err) {
522
+ // Ignore errors
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Record performance metric for a provider
528
+ * @param {string} provider - Provider name
529
+ * @param {string} model - Model name
530
+ * @param {number} durationMs - Duration in milliseconds
531
+ */
532
+ recordPerformance(provider, model, durationMs) {
533
+ const key = `${provider}:${model}`;
534
+
535
+ if (!this.performance[key]) {
536
+ this.performance[key] = {
537
+ provider,
538
+ model,
539
+ samples: [],
540
+ avgSpeed: 0
541
+ };
542
+ }
543
+
544
+ const perf = this.performance[key];
545
+ perf.samples.push(durationMs);
546
+
547
+ // Keep only last 10 samples
548
+ if (perf.samples.length > 10) {
549
+ perf.samples.shift();
550
+ }
551
+
552
+ // Calculate average
553
+ perf.avgSpeed = perf.samples.reduce((a, b) => a + b, 0) / perf.samples.length;
554
+ perf.lastUsed = Date.now();
555
+
556
+ this.savePerformance();
557
+ }
558
+
559
+ /**
560
+ * Get fastest available provider from a list
561
+ * @param {Array} providers - Array of {provider, model, ...} objects
562
+ * @returns {Object|null} - Fastest available provider or null
563
+ */
564
+ getFastestAvailable(providers) {
565
+ const available = providers.filter(p => !this.isRateLimited(p.provider, p.model));
566
+
567
+ if (available.length === 0) return null;
568
+ if (available.length === 1) return available[0];
569
+
570
+ // Sort by average speed (fastest first)
571
+ available.sort((a, b) => {
572
+ const aKey = `${a.provider}:${a.model}`;
573
+ const bKey = `${b.provider}:${b.model}`;
574
+ const aAvg = this.performance[aKey]?.avgSpeed;
575
+ const bAvg = this.performance[bKey]?.avgSpeed;
576
+ const aSpeed = (aAvg ?? a.estimatedSpeed ?? Infinity);
577
+ const bSpeed = (bAvg ?? b.estimatedSpeed ?? Infinity);
578
+ return aSpeed - bSpeed;
579
+ });
580
+
581
+ return available[0];
582
+ }
583
+
584
+ /**
585
+ * Get provider rankings by speed
586
+ * @returns {Array} - Sorted array of {provider, model, avgSpeed}
587
+ */
588
+ getProviderRankings() {
589
+ return Object.values(this.performance)
590
+ .filter(p => p.avgSpeed > 0)
591
+ .sort((a, b) => a.avgSpeed - b.avgSpeed);
592
+ }
593
+ }
594
+
595
+ module.exports = ProviderManager;