vibecodingmachine-core 2026.1.3-2209 → 2026.1.22-1441

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 (32) hide show
  1. package/__tests__/provider-manager-fallback.test.js +43 -0
  2. package/__tests__/provider-manager-rate-limit.test.js +61 -0
  3. package/package.json +1 -1
  4. package/src/compliance/compliance-manager.js +5 -2
  5. package/src/database/migrations.js +135 -12
  6. package/src/database/user-database-client.js +63 -8
  7. package/src/database/user-schema.js +7 -0
  8. package/src/health-tracking/__tests__/ide-health-tracker.test.js +420 -0
  9. package/src/health-tracking/__tests__/interaction-recorder.test.js +392 -0
  10. package/src/health-tracking/errors.js +50 -0
  11. package/src/health-tracking/health-reporter.js +331 -0
  12. package/src/health-tracking/ide-health-tracker.js +446 -0
  13. package/src/health-tracking/interaction-recorder.js +161 -0
  14. package/src/health-tracking/json-storage.js +276 -0
  15. package/src/health-tracking/storage-interface.js +63 -0
  16. package/src/health-tracking/validators.js +277 -0
  17. package/src/ide-integration/applescript-manager.cjs +1062 -4
  18. package/src/ide-integration/applescript-manager.js +560 -11
  19. package/src/ide-integration/provider-manager.cjs +158 -28
  20. package/src/ide-integration/quota-detector.cjs +339 -16
  21. package/src/ide-integration/quota-detector.js +6 -1
  22. package/src/index.cjs +32 -1
  23. package/src/index.js +16 -0
  24. package/src/localization/translations/en.js +13 -1
  25. package/src/localization/translations/es.js +12 -0
  26. package/src/utils/admin-utils.js +33 -0
  27. package/src/utils/error-reporter.js +12 -4
  28. package/src/utils/requirement-helpers.js +34 -4
  29. package/src/utils/requirements-parser.js +3 -3
  30. package/tests/health-tracking/health-reporter.test.js +329 -0
  31. package/tests/health-tracking/ide-health-tracker.test.js +368 -0
  32. package/tests/health-tracking/interaction-recorder.test.js +309 -0
@@ -23,7 +23,7 @@ class ProviderManager {
23
23
  if (fs.existsSync(this.rateLimitFile)) {
24
24
  const data = fs.readFileSync(this.rateLimitFile, 'utf8');
25
25
  const limits = JSON.parse(data);
26
-
26
+
27
27
  // Auto-cleanup expired rate limits
28
28
  const now = Date.now();
29
29
  let cleaned = false;
@@ -33,12 +33,12 @@ class ProviderManager {
33
33
  cleaned = true;
34
34
  }
35
35
  }
36
-
36
+
37
37
  // Save cleaned data if any expired limits were removed
38
38
  if (cleaned) {
39
39
  this.saveRateLimitsInternal(limits);
40
40
  }
41
-
41
+
42
42
  return limits;
43
43
  }
44
44
  } catch (err) {
@@ -46,7 +46,7 @@ class ProviderManager {
46
46
  }
47
47
  return {};
48
48
  }
49
-
49
+
50
50
  /**
51
51
  * Internal save without reload (to prevent infinite loop during cleanup)
52
52
  */
@@ -86,33 +86,107 @@ class ProviderManager {
86
86
  * "Session limit reached ∙ resets 12pm" -> ms until 12pm today/tomorrow
87
87
  */
88
88
  parseRateLimitDuration(errorMessage) {
89
+ // Gemini / Google AI style: "You can resume using this model at 1/12/2026, 4:07:27 PM"
90
+ // Also seen as: "resume using this model at <date>, <time>"
91
+ 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);
92
+ if (resumeAtMatch) {
93
+ try {
94
+ const datePart = resumeAtMatch[1];
95
+ const timePart = resumeAtMatch[2];
96
+ const meridiem = resumeAtMatch[3];
97
+ const parsed = new Date(`${datePart}, ${timePart} ${meridiem.toUpperCase()}`);
98
+ if (!Number.isNaN(parsed.getTime())) {
99
+ const ms = parsed.getTime() - Date.now();
100
+ return ms > 0 ? ms : 0;
101
+ }
102
+ } catch (_) { }
103
+ }
104
+
105
+ // Check for "resets [Month] [Day] at [Time]" format (e.g. "Spending cap reached resets Jan 17 at 12pm")
106
+ 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);
107
+ if (dateMatch) {
108
+ try {
109
+ const monthStr = dateMatch[1]; // Jan, February, etc.
110
+ const day = parseInt(dateMatch[2]);
111
+ const timeStr = dateMatch[3];
112
+ const meridiem = dateMatch[4] ? dateMatch[4].toLowerCase() : null;
113
+
114
+ // Parse time
115
+ let [hours, minutes] = timeStr.split(':').map(n => parseInt(n));
116
+ if (!minutes || Number.isNaN(minutes)) minutes = 0;
117
+
118
+ if (meridiem === 'pm' && hours !== 12) hours += 12;
119
+ if (meridiem === 'am' && hours === 12) hours = 0;
120
+
121
+ // Parse month
122
+ const months = {
123
+ jan: 0, january: 0,
124
+ feb: 1, february: 1,
125
+ mar: 2, march: 2,
126
+ apr: 3, april: 3,
127
+ may: 4,
128
+ jun: 5, june: 5,
129
+ jul: 6, july: 6,
130
+ aug: 7, august: 7,
131
+ sep: 8, sept: 8, september: 8,
132
+ oct: 9, october: 9,
133
+ nov: 10, november: 10,
134
+ dec: 11, december: 11
135
+ };
136
+
137
+ const monthIndex = months[monthStr.toLowerCase().substring(0, 3)] ?? months[monthStr.toLowerCase()];
138
+
139
+ if (monthIndex !== undefined) {
140
+ const currentYear = new Date().getFullYear();
141
+ let parsed = new Date(currentYear, monthIndex, day, hours, minutes);
142
+ const now = Date.now();
143
+
144
+ // Logic to handle year crossing (reset in Jan, currently Dec)
145
+ if (parsed.getTime() < now) {
146
+ if (!Number.isNaN(parsed.getTime())) {
147
+ if (parsed.getTime() < now) {
148
+ const nextYearDate = new Date(parsed);
149
+ nextYearDate.setFullYear(nextYearDate.getFullYear() + 1);
150
+ if (nextYearDate.getTime() > now) {
151
+ parsed = nextYearDate;
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ const ms = parsed.getTime() - now;
158
+ return ms > 0 ? ms : 0;
159
+ }
160
+ } catch (_) { }
161
+ }
162
+
89
163
  // Check for Claude Code session limit format: "Session limit reached ∙ resets 12pm"
90
164
  const sessionMatch = errorMessage.match(/resets?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
91
165
  if (sessionMatch) {
92
166
  let hour = parseInt(sessionMatch[1]);
93
167
  const minute = sessionMatch[2] ? parseInt(sessionMatch[2]) : 0;
94
168
  const meridiem = sessionMatch[3] ? sessionMatch[3].toLowerCase() : null;
95
-
169
+
96
170
  // Convert to 24-hour format
97
171
  if (meridiem === 'pm' && hour !== 12) {
98
172
  hour += 12;
99
173
  } else if (meridiem === 'am' && hour === 12) {
100
174
  hour = 0;
101
175
  }
102
-
176
+
103
177
  // Calculate reset time
104
178
  const now = new Date();
105
179
  const resetTime = new Date(now);
106
180
  resetTime.setHours(hour, minute, 0, 0);
107
-
181
+
108
182
  // If reset time is in the past, it's tomorrow
109
183
  if (resetTime <= now) {
110
184
  resetTime.setDate(resetTime.getDate() + 1);
111
185
  }
112
-
186
+
113
187
  return resetTime.getTime() - now.getTime();
114
188
  }
115
-
189
+
116
190
  // Match patterns like "15m5.472s" or "1h30m" or "45s"
117
191
  const timeMatch = errorMessage.match(/try again in ([\d.]+h)?\s?([\d.]+m)?\s?([\d.]+s)?/i);
118
192
  if (!timeMatch) return null;
@@ -147,8 +221,11 @@ class ProviderManager {
147
221
  * @param {string} errorMessage - Full error message containing duration
148
222
  */
149
223
  markRateLimited(provider, model, errorMessage) {
224
+ // Normalize model: if model is missing/undefined, assume it's the provider name.
225
+ model = model || provider;
226
+
150
227
  let duration = this.parseRateLimitDuration(errorMessage);
151
- if (!duration) {
228
+ if (duration === null || duration === undefined) {
152
229
  console.warn(`Could not parse rate limit duration from: ${errorMessage}`);
153
230
  // Default to 15 minutes if we can't parse
154
231
  duration = 15 * 60 * 1000;
@@ -162,6 +239,7 @@ class ProviderManager {
162
239
  model,
163
240
  resetTime,
164
241
  resetDate: new Date(resetTime).toISOString(),
242
+ reason: errorMessage,
165
243
  markedAt: new Date().toISOString()
166
244
  };
167
245
 
@@ -196,17 +274,28 @@ class ProviderManager {
196
274
  const key = `${provider}:${model}`;
197
275
  const limit = this.rateLimits[key];
198
276
 
199
- if (!limit) return false;
277
+ if (limit) {
278
+ // Check if rate limit has expired
279
+ if (Date.now() >= limit.resetTime) {
280
+ // Clean up expired rate limit
281
+ delete this.rateLimits[key];
282
+ this.saveRateLimits();
283
+ return false;
284
+ }
200
285
 
201
- // Check if rate limit has expired
202
- if (Date.now() >= limit.resetTime) {
203
- // Clean up expired rate limit
204
- delete this.rateLimits[key];
205
- this.saveRateLimits();
206
- return false;
286
+ return true;
207
287
  }
208
288
 
209
- return true;
289
+ // Fall back to any provider-level entries (handles older or model-agnostic keys)
290
+ const providerPrefix = `${provider}:`;
291
+ for (const k in this.rateLimits) {
292
+ if (k.startsWith(providerPrefix)) {
293
+ const l = this.rateLimits[k];
294
+ if (l && Date.now() < l.resetTime) return true;
295
+ }
296
+ }
297
+
298
+ return false;
210
299
  }
211
300
 
212
301
  /**
@@ -252,6 +341,31 @@ class ProviderManager {
252
341
  reason: limit.reason
253
342
  };
254
343
  }
344
+
345
+ // Fallback: check other keys for this provider and return earliest reset
346
+ const providerPrefix = `${provider}:`;
347
+ let earliestReset = null;
348
+ let reason = null;
349
+
350
+ for (const k in this.rateLimits) {
351
+ if (k.startsWith(providerPrefix)) {
352
+ const l = this.rateLimits[k];
353
+ if (l && Date.now() < l.resetTime) {
354
+ if (!earliestReset || l.resetTime < earliestReset) {
355
+ earliestReset = l.resetTime;
356
+ reason = l.reason;
357
+ }
358
+ }
359
+ }
360
+ }
361
+
362
+ if (earliestReset) {
363
+ return {
364
+ isRateLimited: true,
365
+ resetTime: earliestReset,
366
+ reason
367
+ };
368
+ }
255
369
  }
256
370
 
257
371
  return {
@@ -285,9 +399,25 @@ class ProviderManager {
285
399
  const key = `${provider}:${model}`;
286
400
  const limit = this.rateLimits[key];
287
401
 
288
- if (!limit) return null;
402
+ if (limit && Date.now() < limit.resetTime) {
403
+ const remaining = limit.resetTime - Date.now();
404
+ return remaining > 0 ? remaining : null;
405
+ }
289
406
 
290
- const remaining = limit.resetTime - Date.now();
407
+ // Fallback: check other keys for this provider (handles legacy or model-agnostic entries)
408
+ const providerPrefix = `${provider}:`;
409
+ let earliest = null;
410
+ for (const k in this.rateLimits) {
411
+ if (k.startsWith(providerPrefix)) {
412
+ const l = this.rateLimits[k];
413
+ if (l && Date.now() < l.resetTime) {
414
+ if (!earliest || l.resetTime < earliest) earliest = l.resetTime;
415
+ }
416
+ }
417
+ }
418
+
419
+ if (!earliest) return null;
420
+ const remaining = earliest - Date.now();
291
421
  return remaining > 0 ? remaining : null;
292
422
  }
293
423
 
@@ -356,7 +486,7 @@ class ProviderManager {
356
486
  */
357
487
  recordPerformance(provider, model, durationMs) {
358
488
  const key = `${provider}:${model}`;
359
-
489
+
360
490
  if (!this.performance[key]) {
361
491
  this.performance[key] = {
362
492
  provider,
@@ -365,19 +495,19 @@ class ProviderManager {
365
495
  avgSpeed: 0
366
496
  };
367
497
  }
368
-
498
+
369
499
  const perf = this.performance[key];
370
500
  perf.samples.push(durationMs);
371
-
501
+
372
502
  // Keep only last 10 samples
373
503
  if (perf.samples.length > 10) {
374
504
  perf.samples.shift();
375
505
  }
376
-
506
+
377
507
  // Calculate average
378
508
  perf.avgSpeed = perf.samples.reduce((a, b) => a + b, 0) / perf.samples.length;
379
509
  perf.lastUsed = Date.now();
380
-
510
+
381
511
  this.savePerformance();
382
512
  }
383
513
 
@@ -388,10 +518,10 @@ class ProviderManager {
388
518
  */
389
519
  getFastestAvailable(providers) {
390
520
  const available = providers.filter(p => !this.isRateLimited(p.provider, p.model));
391
-
521
+
392
522
  if (available.length === 0) return null;
393
523
  if (available.length === 1) return available[0];
394
-
524
+
395
525
  // Sort by average speed (fastest first)
396
526
  available.sort((a, b) => {
397
527
  const aKey = `${a.provider}:${a.model}`;
@@ -402,7 +532,7 @@ class ProviderManager {
402
532
  const bSpeed = (bAvg ?? b.estimatedSpeed ?? Infinity);
403
533
  return aSpeed - bSpeed;
404
534
  });
405
-
535
+
406
536
  return available[0];
407
537
  }
408
538
 
@@ -1,33 +1,356 @@
1
- // @vibecodingmachine/core - Quota Detector (CommonJS) - Stub
1
+ // @vibecodingmachine/core - Quota Detector (CommonJS)
2
2
  // Handles quota detection for different IDEs using CDP and AppleScript
3
3
 
4
+ const CDP = require('chrome-remote-interface');
5
+ const { AppleScriptManager } = require('./applescript-manager.cjs');
6
+
7
+ /**
8
+ * Quota Detector for IDE interactions
9
+ * Detects quota warnings in different IDEs using CDP and AppleScript
10
+ */
4
11
  class QuotaDetector {
5
12
  constructor() {
6
13
  this.logger = console;
14
+ this.appleScriptManager = new AppleScriptManager();
15
+ }
16
+
17
+ /**
18
+ * Timeout utility function
19
+ * @param {number} ms - Timeout in milliseconds
20
+ * @param {Promise} promise - Promise to timeout
21
+ * @returns {Promise} Promise with timeout
22
+ */
23
+ timeout(ms, promise) {
24
+ return Promise.race([
25
+ promise,
26
+ new Promise((_, reject) =>
27
+ setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms)
28
+ )
29
+ ]);
7
30
  }
8
31
 
32
+ /**
33
+ * Detect quota warnings in an IDE
34
+ * @param {string} ide - The IDE name ('vscode', 'cursor', 'windsurf')
35
+ * @returns {Promise<Object>} Quota detection result
36
+ */
9
37
  async detectQuotaWarning(ide = 'vscode') {
10
- this.logger.log(`🔍 Quota detection stub for ${ide}`);
11
- return {
12
- hasQuotaWarning: false,
13
- method: 'stub',
14
- note: 'Quota detection not implemented in stub'
15
- };
38
+ this.logger.log(`🔍 Starting quota detection for ${ide}...`);
39
+
40
+ // Determine the port based on the IDE
41
+ const port = ide === 'windsurf' ? 9224 : ide === 'cursor' ? 9225 : 9222;
42
+ const ideName = ide === 'windsurf' ? 'Windsurf' : ide === 'cursor' ? 'Cursor' : 'VS Code';
43
+
44
+ this.logger.log(`🔌 Checking ${ideName} on port ${port}...`);
45
+
46
+ // For Windsurf, use AppleScript detection since CDP is not supported
47
+ if (ide === 'windsurf') {
48
+ this.logger.log('🔍 Windsurf detected - using AppleScript quota detection');
49
+ return await this.appleScriptManager.detectQuotaWarning(ide);
50
+ }
51
+
52
+ // For VS Code and Cursor, use CDP detection
53
+ try {
54
+ this.logger.log(`🔍 Attempting CDP connection to ${ideName} on port ${port}...`);
55
+
56
+ // Try to connect to CDP
57
+ const targets = await this.timeout(5000, CDP.List({ port }));
58
+
59
+ if (!targets || targets.length === 0) {
60
+ this.logger.log(`❌ No CDP targets found on port ${port}`);
61
+
62
+ // For Windsurf, fall back to AppleScript
63
+ if (ide === 'windsurf') {
64
+ this.logger.log('🔍 Falling back to enhanced AppleScript quota detection...');
65
+ return await this.appleScriptManager.detectQuotaWarning(ide);
66
+ }
67
+
68
+ return {
69
+ hasQuotaWarning: false,
70
+ error: `Could not find ${ideName}. Make sure ${ideName} is running with --remote-debugging-port=${port}`,
71
+ note: 'CDP connection failed'
72
+ };
73
+ }
74
+
75
+ this.logger.log(`✅ Found ${targets.length} CDP targets`);
76
+
77
+ // For Cursor, prefer the workspace target over settings
78
+ let workbench;
79
+ if (ide === 'cursor') {
80
+ workbench = targets.find(t => t.title !== 'Cursor Settings' && t.type === 'page') ||
81
+ targets.find(t => t.url && t.url.includes('workbench')) ||
82
+ targets[0];
83
+ } else {
84
+ workbench = targets.find(t => t.url && t.url.includes('workbench')) || targets[0];
85
+ }
86
+
87
+ if (!workbench) {
88
+ this.logger.log(`❌ No suitable workbench target found`);
89
+ return {
90
+ hasQuotaWarning: false,
91
+ error: `No ${ideName} workbench target found.`,
92
+ note: 'No workbench target available'
93
+ };
94
+ }
95
+
96
+ this.logger.log(`✅ Selected workbench: ${workbench.title}`);
97
+
98
+ // Connect to CDP
99
+ const client = await this.timeout(10000, CDP({ port, target: workbench }));
100
+ const { Runtime, Page } = client;
101
+
102
+ await this.timeout(5000, Runtime.enable());
103
+ if (Page && Page.bringToFront) {
104
+ try {
105
+ await this.timeout(3000, Page.bringToFront());
106
+ } catch (error) {
107
+ this.logger.log(`⚠️ Bring to front failed: ${error.message}`);
108
+ }
109
+ }
110
+
111
+ // Execute JavaScript to detect quota warnings
112
+ const detectionScript = `
113
+ (() => {
114
+ console.log('🔍 Starting quota detection in ${ideName}...');
115
+
116
+ // Common quota warning patterns
117
+ const quotaPatterns = [
118
+ 'not enough credits',
119
+ 'upgrade to a paid plan',
120
+ 'switch to swe-1-lite',
121
+ 'quota exceeded',
122
+ 'credits exhausted',
123
+ 'payment required',
124
+ 'out of credits',
125
+ 'insufficient credits',
126
+ 'billing required',
127
+ 'subscription needed',
128
+ 'monthly chat messages quota',
129
+ 'upgrade to copilot pro',
130
+ 'allowance to renew',
131
+ 'reached your monthly',
132
+ 'wait for your allowance'
133
+ ];
134
+
135
+ // Function to check if text contains quota warnings
136
+ function containsQuotaWarning(text) {
137
+ if (!text) return false;
138
+ const lowerText = text.toLowerCase();
139
+ return quotaPatterns.some(pattern => lowerText.includes(pattern));
140
+ }
141
+
142
+ // Method 1: Check all text content in the document
143
+ const allText = document.body.innerText || document.body.textContent || '';
144
+ if (containsQuotaWarning(allText)) {
145
+ console.log('❌ Quota warning detected in document text');
146
+ return {
147
+ hasQuotaWarning: true,
148
+ method: 'document-text',
149
+ matchedText: allText.substring(0, 200) + '...'
150
+ };
151
+ }
152
+
153
+ // Method 2: Check specific elements that might contain quota warnings
154
+ const quotaElements = [
155
+ // VS Code specific selectors
156
+ '.monaco-workbench .part.auxiliarybar',
157
+ '.monaco-workbench .part.sidebar.right',
158
+ '.monaco-workbench .chat-view',
159
+ '.monaco-workbench .chat-input',
160
+
161
+ // Cursor specific selectors
162
+ '.aislash-editor-input',
163
+ '.aislash-chat-container',
164
+ '.aislash-panel',
165
+
166
+ // Generic selectors
167
+ '[data-testid*="chat"]',
168
+ '[aria-label*="chat"]',
169
+ '[class*="chat"]',
170
+ '[class*="quota"]',
171
+ '[class*="credits"]',
172
+ '[class*="billing"]'
173
+ ];
174
+
175
+ for (const selector of quotaElements) {
176
+ const elements = document.querySelectorAll(selector);
177
+ for (const element of elements) {
178
+ const elementText = element.innerText || element.textContent || '';
179
+ if (containsQuotaWarning(elementText)) {
180
+ console.log('❌ Quota warning detected in element:', selector);
181
+ return {
182
+ hasQuotaWarning: true,
183
+ method: 'element-specific',
184
+ selector: selector,
185
+ matchedText: elementText.substring(0, 200) + '...'
186
+ };
187
+ }
188
+ }
189
+ }
190
+
191
+ // Method 3: Check for disabled input fields (common when quota is exceeded)
192
+ const disabledInputs = document.querySelectorAll('input[disabled], textarea[disabled], [contenteditable="true"][aria-disabled="true"]');
193
+ if (disabledInputs.length > 0) {
194
+ console.log('⚠️ Found disabled input fields, checking for quota context...');
195
+
196
+ // Check if there are quota-related messages near disabled inputs
197
+ for (const input of disabledInputs) {
198
+ const parentText = input.parentElement?.innerText || '';
199
+ if (containsQuotaWarning(parentText)) {
200
+ console.log('❌ Quota warning detected near disabled input');
201
+ return {
202
+ hasQuotaWarning: true,
203
+ method: 'disabled-input-context',
204
+ matchedText: parentText.substring(0, 200) + '...'
205
+ };
206
+ }
207
+ }
208
+ }
209
+
210
+ // Method 4: Check for specific UI elements that indicate quota issues
211
+ const quotaButtons = document.querySelectorAll('button, a, [role="button"]');
212
+ for (const button of quotaButtons) {
213
+ const buttonText = button.innerText || button.textContent || '';
214
+ if (containsQuotaWarning(buttonText)) {
215
+ console.log('❌ Quota warning detected in button:', buttonText);
216
+ return {
217
+ hasQuotaWarning: true,
218
+ method: 'button-text',
219
+ matchedText: buttonText
220
+ };
221
+ }
222
+ }
223
+
224
+ // Method 5: Check for error messages or notifications
225
+ const errorElements = document.querySelectorAll('[class*="error"], [class*="warning"], [class*="notification"], [role="alert"]');
226
+ for (const error of errorElements) {
227
+ const errorText = error.innerText || error.textContent || '';
228
+ if (containsQuotaWarning(errorText)) {
229
+ console.log('❌ Quota warning detected in error element');
230
+ return {
231
+ hasQuotaWarning: true,
232
+ method: 'error-element',
233
+ matchedText: errorText.substring(0, 200) + '...'
234
+ };
235
+ }
236
+ }
237
+
238
+ console.log('✅ No quota warnings detected');
239
+ return {
240
+ hasQuotaWarning: false,
241
+ method: 'cdp-comprehensive',
242
+ note: 'No quota warnings found in ${ideName}'
243
+ };
244
+ })()
245
+ `;
246
+
247
+ const { result } = await this.timeout(10000, Runtime.evaluate({
248
+ expression: detectionScript,
249
+ returnByValue: true
250
+ }));
251
+
252
+ const detectionResult = result.value || result.unserializableValue;
253
+
254
+ this.logger.log('🔍 CDP quota detection result:', detectionResult);
255
+
256
+ if (detectionResult && detectionResult.hasQuotaWarning) {
257
+ this.logger.log('❌ Quota warning detected via CDP');
258
+ return {
259
+ hasQuotaWarning: true,
260
+ method: detectionResult.method,
261
+ matchedText: detectionResult.matchedText || 'Quota warning detected via CDP',
262
+ note: detectionResult.matchedText || 'Quota warning detected via CDP',
263
+ debug: detectionResult
264
+ };
265
+ }
266
+
267
+ this.logger.log('✅ No quota warnings detected via CDP');
268
+ return {
269
+ hasQuotaWarning: false,
270
+ method: 'cdp-comprehensive',
271
+ note: 'No quota warnings found in ' + ideName,
272
+ debug: detectionResult
273
+ };
274
+
275
+ } catch (error) {
276
+ this.logger.log(`❌ CDP quota detection failed: ${error.message}`);
277
+
278
+ // For Windsurf, fall back to AppleScript
279
+ if (ide === 'windsurf') {
280
+ this.logger.log('🔍 Falling back to AppleScript quota detection...');
281
+ return await this.appleScriptManager.detectQuotaWarning(ide);
282
+ }
283
+
284
+ return {
285
+ hasQuotaWarning: false,
286
+ error: error.message,
287
+ note: `CDP quota detection failed for ${ideName}`
288
+ };
289
+ }
16
290
  }
17
291
 
292
+ /**
293
+ * Get available IDEs without quota warnings
294
+ * @param {Array<string>} ides - Array of IDE names to check
295
+ * @returns {Promise<Array<string>>} Array of available IDE names
296
+ */
18
297
  async getAvailableIdes(ides = ['vscode', 'cursor', 'windsurf']) {
19
- this.logger.log('🔍 Available IDEs stub');
20
- return ides;
298
+ this.logger.log('🔍 Checking available IDEs without quota warnings...');
299
+
300
+ const availableIdes = [];
301
+
302
+ for (const ide of ides) {
303
+ try {
304
+ const quotaResult = await this.detectQuotaWarning(ide);
305
+
306
+ if (!quotaResult.hasQuotaWarning) {
307
+ availableIdes.push(ide);
308
+ this.logger.log(`✅ ${ide} is available (no quota warnings)`);
309
+ } else {
310
+ this.logger.log(`❌ ${ide} has quota warnings: ${quotaResult.note || 'Unknown quota issue'}`);
311
+ }
312
+ } catch (error) {
313
+ this.logger.log(`⚠️ Error checking ${ide}: ${error.message}`);
314
+ // If we can't check quota, assume the IDE is available
315
+ availableIdes.push(ide);
316
+ }
317
+ }
318
+
319
+ this.logger.log(`📋 Available IDEs: ${availableIdes.join(', ')}`);
320
+ return availableIdes;
21
321
  }
22
322
 
323
+ /**
324
+ * Test quota detection with a test message
325
+ * @param {string} ide - The IDE name to test
326
+ * @returns {Promise<Object>} Test result
327
+ */
23
328
  async testQuotaDetection(ide) {
24
- this.logger.log(`🧪 Quota detection test stub for ${ide}`);
25
- return {
26
- ide,
27
- success: true,
28
- hasQuotaWarning: false,
29
- method: 'stub'
30
- };
329
+ this.logger.log(`🧪 Testing quota detection for ${ide}...`);
330
+
331
+ try {
332
+ const quotaResult = await this.detectQuotaWarning(ide);
333
+
334
+ this.logger.log(`🧪 Quota detection test result for ${ide}:`, quotaResult);
335
+
336
+ return {
337
+ ide,
338
+ success: true,
339
+ hasQuotaWarning: quotaResult.hasQuotaWarning,
340
+ method: quotaResult.method,
341
+ note: quotaResult.note,
342
+ debug: quotaResult
343
+ };
344
+ } catch (error) {
345
+ this.logger.log(`❌ Quota detection test failed for ${ide}: ${error.message}`);
346
+
347
+ return {
348
+ ide,
349
+ success: false,
350
+ error: error.message,
351
+ note: `Quota detection test failed for ${ide}`
352
+ };
353
+ }
31
354
  }
32
355
  }
33
356
 
@@ -124,7 +124,12 @@ export class QuotaDetector {
124
124
  'out of credits',
125
125
  'insufficient credits',
126
126
  'billing required',
127
- 'subscription needed'
127
+ 'subscription needed',
128
+ 'monthly chat messages quota',
129
+ 'upgrade to copilot pro',
130
+ 'allowance to renew',
131
+ 'reached your monthly',
132
+ 'wait for your allowance'
128
133
  ];
129
134
 
130
135
  // Function to check if text contains quota warnings