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.
- package/__tests__/provider-manager-fallback.test.js +43 -0
- package/__tests__/provider-manager-rate-limit.test.js +61 -0
- package/package.json +1 -1
- package/src/compliance/compliance-manager.js +5 -2
- package/src/database/migrations.js +135 -12
- package/src/database/user-database-client.js +63 -8
- package/src/database/user-schema.js +7 -0
- package/src/health-tracking/__tests__/ide-health-tracker.test.js +420 -0
- package/src/health-tracking/__tests__/interaction-recorder.test.js +392 -0
- package/src/health-tracking/errors.js +50 -0
- package/src/health-tracking/health-reporter.js +331 -0
- package/src/health-tracking/ide-health-tracker.js +446 -0
- package/src/health-tracking/interaction-recorder.js +161 -0
- package/src/health-tracking/json-storage.js +276 -0
- package/src/health-tracking/storage-interface.js +63 -0
- package/src/health-tracking/validators.js +277 -0
- package/src/ide-integration/applescript-manager.cjs +1062 -4
- package/src/ide-integration/applescript-manager.js +560 -11
- package/src/ide-integration/provider-manager.cjs +158 -28
- package/src/ide-integration/quota-detector.cjs +339 -16
- package/src/ide-integration/quota-detector.js +6 -1
- package/src/index.cjs +32 -1
- package/src/index.js +16 -0
- package/src/localization/translations/en.js +13 -1
- package/src/localization/translations/es.js +12 -0
- package/src/utils/admin-utils.js +33 -0
- package/src/utils/error-reporter.js +12 -4
- package/src/utils/requirement-helpers.js +34 -4
- package/src/utils/requirements-parser.js +3 -3
- package/tests/health-tracking/health-reporter.test.js +329 -0
- package/tests/health-tracking/ide-health-tracker.test.js +368 -0
- 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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
402
|
+
if (limit && Date.now() < limit.resetTime) {
|
|
403
|
+
const remaining = limit.resetTime - Date.now();
|
|
404
|
+
return remaining > 0 ? remaining : null;
|
|
405
|
+
}
|
|
289
406
|
|
|
290
|
-
|
|
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)
|
|
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(`🔍
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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('🔍
|
|
20
|
-
|
|
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(`🧪
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|