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
@@ -13,6 +13,589 @@ class AppleScriptManager {
13
13
  this.logger = console;
14
14
  }
15
15
 
16
+ /**
17
+ * Best-effort: switch Antigravity's active model to the requested label.
18
+ * @param {string} modelName
19
+ * @returns {Promise<{success: boolean, model?: string, error?: string}>}
20
+ */
21
+ async switchAntigravityModel(modelName) {
22
+ try {
23
+ if (!modelName || typeof modelName !== 'string') {
24
+ return { success: false, error: 'Invalid model name' };
25
+ }
26
+
27
+ const script = `
28
+ tell application "System Events"
29
+ tell process "Antigravity"
30
+ set frontmost to true
31
+ delay 0.8
32
+
33
+ try
34
+ set buttonTexts to {"Select another model", "Switch model", "Change model", "Select model", "Try another model", "Choose model"}
35
+ set foundButton to missing value
36
+
37
+ repeat with buttonText in buttonTexts
38
+ try
39
+ set matchingButtons to buttons of window 1 whose name contains buttonText
40
+ if (count of matchingButtons) > 0 then
41
+ set foundButton to item 1 of matchingButtons
42
+ exit repeat
43
+ end if
44
+ end try
45
+ end repeat
46
+
47
+ if foundButton is missing value then
48
+ repeat with buttonText in buttonTexts
49
+ try
50
+ set allGroups to groups of window 1
51
+ repeat with grp in allGroups
52
+ try
53
+ set matchingButtons to buttons of grp whose name contains buttonText
54
+ if (count of matchingButtons) > 0 then
55
+ set foundButton to item 1 of matchingButtons
56
+ exit repeat
57
+ end if
58
+ end try
59
+ end repeat
60
+ if foundButton is not missing value then exit repeat
61
+ end try
62
+ end repeat
63
+ end if
64
+
65
+ if foundButton is missing value then
66
+ return "error:button-not-found"
67
+ end if
68
+
69
+ click foundButton
70
+ delay 1.2
71
+
72
+ set modelItems to {}
73
+ try
74
+ set modelItems to menu items of menu 1 of window 1 whose name contains "${modelName.replace(/"/g, '\\"')}"
75
+ end try
76
+
77
+ if (count of modelItems) = 0 then
78
+ try
79
+ set modelItems to buttons of window 1 whose name contains "${modelName.replace(/"/g, '\\"')}"
80
+ end try
81
+ end if
82
+
83
+ if (count of modelItems) = 0 then
84
+ try
85
+ set allGroups to groups of window 1
86
+ repeat with grp in allGroups
87
+ try
88
+ set matchingItems to (every UI element of grp whose name contains "${modelName.replace(/"/g, '\\"')}")
89
+ if (count of matchingItems) > 0 then
90
+ set modelItems to matchingItems
91
+ exit repeat
92
+ end if
93
+ end try
94
+ end repeat
95
+ end try
96
+ end if
97
+
98
+ if (count of modelItems) = 0 then
99
+ return "error:model-not-found"
100
+ end if
101
+
102
+ click item 1 of modelItems
103
+ delay 0.6
104
+ return "success:${modelName.replace(/"/g, '\\"')}"
105
+ on error errMsg
106
+ return "error:" & errMsg
107
+ end try
108
+ end tell
109
+ end tell
110
+ `;
111
+
112
+ const result = execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, {
113
+ encoding: 'utf8',
114
+ timeout: 15000
115
+ }).trim();
116
+
117
+ if (result.startsWith('success:')) {
118
+ return { success: true, model: result.substring(8) };
119
+ }
120
+ return { success: false, error: result };
121
+ } catch (error) {
122
+ return { success: false, error: error.message };
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Check Antigravity UI for Gemini quota limit popup text.
128
+ * This is best-effort: Antigravity may render the popup in a WebView that AppleScript can't read.
129
+ * @returns {Promise<{isRateLimited: boolean, message?: string, resumeAt?: string, note?: string}>}
130
+ */
131
+ async checkAntigravityQuotaLimit() {
132
+ try {
133
+ const script = `
134
+ tell application "System Events"
135
+ if not (exists process "Antigravity") then
136
+ return "NOT_RUNNING"
137
+ end if
138
+ tell process "Antigravity"
139
+ set frontmost to true
140
+ delay 0.4
141
+
142
+ set allText to ""
143
+ try
144
+ set allText to allText & (value of static text of window 1 as string) & "\n"
145
+ end try
146
+
147
+ try
148
+ set groupsList to groups of window 1
149
+ repeat with g in groupsList
150
+ try
151
+ set allText to allText & (value of static text of g as string) & "\n"
152
+ end try
153
+ end repeat
154
+ end try
155
+
156
+ return allText
157
+ end tell
158
+ end tell
159
+ `;
160
+
161
+ const raw = execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, {
162
+ encoding: 'utf8',
163
+ timeout: 8000
164
+ }).trim();
165
+
166
+ if (!raw || raw === 'NOT_RUNNING') {
167
+ return {
168
+ isRateLimited: false,
169
+ note: raw === 'NOT_RUNNING' ? 'Antigravity not running' : 'No readable UI text'
170
+ };
171
+ }
172
+
173
+ const text = raw.replace(/\s+/g, ' ').trim();
174
+ const lower = text.toLowerCase();
175
+
176
+ const hasGeminiQuota = lower.includes('model quota limit exceeded') ||
177
+ (lower.includes('quota limit') && lower.includes('you can resume')) ||
178
+ lower.includes('you have reached the quota limit') ||
179
+ lower.includes('spending cap reached') ||
180
+ lower.includes('usage cap reached') ||
181
+ lower.includes('rate limit reached');
182
+
183
+ if (!hasGeminiQuota) {
184
+ return { isRateLimited: false };
185
+ }
186
+
187
+ const resumeAtMatch = text.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);
188
+ const resumeAt = resumeAtMatch
189
+ ? `${resumeAtMatch[1]}, ${resumeAtMatch[2]} ${resumeAtMatch[3].toUpperCase()}`
190
+ : undefined;
191
+
192
+ return {
193
+ isRateLimited: true,
194
+ message: text,
195
+ resumeAt
196
+ };
197
+ } catch (error) {
198
+ return {
199
+ isRateLimited: false,
200
+ note: `Failed to check Antigravity quota: ${error.message}`
201
+ };
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Check AWS Kiro UI for quota limit messages
207
+ * This is best-effort: Kiro may render quota messages in different UI elements or web-based UI
208
+ * @returns {Promise<{isRateLimited: boolean, message?: string, resumeAt?: string, note?: string}>}
209
+ */
210
+ async checkKiroQuotaLimit() {
211
+ try {
212
+ // First try AppleScript UI detection
213
+ const script = `
214
+ tell application "System Events"
215
+ set processName to "AWS Kiro"
216
+ try
217
+ if not (exists process "AWS Kiro") then
218
+ set processName to "Kiro"
219
+ end if
220
+ on error
221
+ set processName to "Kiro"
222
+ end try
223
+
224
+ if not (exists process processName) then
225
+ return "NOT_RUNNING"
226
+ end if
227
+
228
+ tell process processName
229
+ set frontmost to true
230
+ delay 0.4
231
+
232
+ set allText to ""
233
+
234
+ -- Get window title (might contain quota info)
235
+ try
236
+ set windowTitle to title of window 1
237
+ set allText to allText & "WINDOW_TITLE: " & windowTitle & "\\n"
238
+ end try
239
+
240
+ -- Get window description
241
+ try
242
+ set windowDesc to description of window 1
243
+ set allText to allText & "WINDOW_DESC: " & windowDesc & "\\n"
244
+ end try
245
+
246
+ -- Get window help text
247
+ try
248
+ set windowHelp to help of window 1
249
+ set allText to allText & "WINDOW_HELP: " & windowHelp & "\\n"
250
+ end try
251
+
252
+ -- Try to get text from main window
253
+ try
254
+ set windowText to (value of static text of window 1 as string)
255
+ set allText to allText & "WINDOW_STATIC: " & windowText & "\\n"
256
+ end try
257
+
258
+ -- Try to get text from groups in main window
259
+ try
260
+ set groupsList to groups of window 1
261
+ repeat with g in groupsList
262
+ try
263
+ set groupText to (value of static text of g as string)
264
+ set allText to allText & "GROUP: " & groupText & "\\n"
265
+ end try
266
+ end repeat
267
+ end try
268
+
269
+ -- Try to get text from buttons in main window
270
+ try
271
+ set buttonsList to buttons of window 1
272
+ repeat with b in buttonsList
273
+ try
274
+ set buttonText to (name of b as string)
275
+ set allText to allText & "BUTTON: " & buttonText & "\\n"
276
+ end try
277
+ end repeat
278
+ end try
279
+
280
+ -- Try to get text from text fields in main window
281
+ try
282
+ set textFieldsList to text fields of window 1
283
+ repeat with tf in textFieldsList
284
+ try
285
+ set fieldText to (value of tf as string)
286
+ set allText to allText & "TEXT_FIELD: " & fieldText & "\\n"
287
+ end try
288
+ end repeat
289
+ end try
290
+
291
+ -- Try to get text from text views in main window
292
+ try
293
+ set textViewsList to text views of window 1
294
+ repeat with tv in textViewsList
295
+ try
296
+ set viewText to (value of tv as string)
297
+ set allText to allText & "TEXT_VIEW: " & viewText & "\\n"
298
+ end try
299
+ end repeat
300
+ end try
301
+
302
+ -- Try to get text from sheets (dialogs/alerts)
303
+ try
304
+ set sheetsList to sheets of window 1
305
+ repeat with s in sheetsList
306
+ try
307
+ set sheetText to (value of static text of s as string)
308
+ set allText to allText & "SHEET: " & sheetText & "\\n"
309
+ end try
310
+ end repeat
311
+ end try
312
+
313
+ -- Try to get text from all UI elements (catch-all)
314
+ try
315
+ set uiElementsList to every UI element of window 1
316
+ repeat with element in uiElementsList
317
+ try
318
+ set elementText to value of element as string
319
+ if elementText is not missing value and elementText is not "" then
320
+ set allText to allText & "UI_ELEMENT: " & elementText & "\\n"
321
+ end if
322
+ end try
323
+ end repeat
324
+ end try
325
+
326
+ -- Try to get accessibility attributes
327
+ try
328
+ set windowRole to role of window 1
329
+ set allText to allText & "WINDOW_ROLE: " & windowRole & "\\n"
330
+ end try
331
+
332
+ try
333
+ set windowSubrole to subrole of window 1
334
+ set allText to allText & "WINDOW_SUBROLE: " & windowSubrole & "\\n"
335
+ end try
336
+
337
+ return allText
338
+ end tell
339
+ end tell
340
+ `;
341
+
342
+ const raw = execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, {
343
+ encoding: 'utf8',
344
+ timeout: 8000
345
+ }).trim();
346
+
347
+ if (!raw || raw === 'NOT_RUNNING') {
348
+ return {
349
+ isRateLimited: false,
350
+ note: raw === 'NOT_RUNNING' ? 'AWS Kiro not running' : 'No readable UI text'
351
+ };
352
+ }
353
+
354
+ const text = raw.replace(/\s+/g, ' ').trim();
355
+ const lower = text.toLowerCase();
356
+
357
+ // Check for Kiro-specific quota limit patterns (expanded for better detection)
358
+ const hasKiroQuota = lower.includes('quota limit exceeded') ||
359
+ lower.includes('usage limit reached') ||
360
+ lower.includes('rate limit exceeded') ||
361
+ lower.includes('api limit exceeded') ||
362
+ lower.includes('request limit exceeded') ||
363
+ lower.includes('too many requests') ||
364
+ lower.includes('quota exceeded') ||
365
+ lower.includes('limit exceeded') ||
366
+ lower.includes('usage exceeded') ||
367
+ lower.includes('rate limited') ||
368
+ lower.includes('api limited') ||
369
+ lower.includes('request limited') ||
370
+ lower.includes('quota reached') ||
371
+ lower.includes('limit reached') ||
372
+ lower.includes('usage reached') ||
373
+ lower.includes('cap reached') ||
374
+ lower.includes('cap exceeded') ||
375
+ lower.includes('spending cap') ||
376
+ lower.includes('usage cap') ||
377
+ lower.includes('daily limit') ||
378
+ lower.includes('monthly limit') ||
379
+ lower.includes('hourly limit') ||
380
+ lower.includes('request limit') ||
381
+ lower.includes('token limit') ||
382
+ lower.includes('credit limit') ||
383
+ lower.includes('billing limit') ||
384
+ lower.includes('subscription limit') ||
385
+ lower.includes('plan limit') ||
386
+ lower.includes('tier limit') ||
387
+ lower.includes('upgrade required') ||
388
+ lower.includes('upgrade needed') ||
389
+ lower.includes('upgrade to pro') ||
390
+ lower.includes('upgrade to premium') ||
391
+ lower.includes('upgrade plan') ||
392
+ lower.includes('increase limit') ||
393
+ lower.includes('exhausted') ||
394
+ lower.includes('no more requests') ||
395
+ lower.includes('no more credits') ||
396
+ lower.includes('insufficient credits') ||
397
+ lower.includes('insufficient quota') ||
398
+ lower.includes('maximum reached') ||
399
+ lower.includes('max requests') ||
400
+ lower.includes('max usage') ||
401
+ lower.includes('over limit') ||
402
+ lower.includes('over quota') ||
403
+ lower.includes('over usage') ||
404
+ lower.includes('out of credits') ||
405
+ lower.includes('out of credit') ||
406
+ lower.includes('credits exhausted') ||
407
+ lower.includes('credits depleted') ||
408
+ lower.includes('credits used up') ||
409
+ lower.includes('credit exhausted') ||
410
+ lower.includes('credit depleted') ||
411
+ lower.includes('credit used up') ||
412
+ lower.includes('credit balance is too low') ||
413
+ lower.includes('not enough credits') ||
414
+ lower.includes('credits: 0') ||
415
+ lower.includes('credit balance: 0') ||
416
+ lower.includes('no credits available') ||
417
+ lower.includes('purchase more credits') ||
418
+ lower.includes('credits: 0 remaining') ||
419
+ lower.includes('credit balance: 0') ||
420
+ (lower.includes('credits') && (lower.includes('out of') || lower.includes('exhausted') || lower.includes('depleted') || lower.includes('used up'))) ||
421
+ (lower.includes('credit') && (lower.includes('out of') || lower.includes('exhausted') || lower.includes('depleted') || lower.includes('used up') || lower.includes('balance is too low') || lower.includes('balance: 0'))) ||
422
+ (lower.includes('quota') && (lower.includes('exceeded') || lower.includes('reached') || lower.includes('exhaust'))) ||
423
+ (lower.includes('limit') && (lower.includes('exceeded') || lower.includes('reached') || lower.includes('exhaust'))) ||
424
+ (lower.includes('cap') && (lower.includes('exceeded') || lower.includes('reached') || lower.includes('exhaust')));
425
+
426
+ if (hasKiroQuota) {
427
+ // Try to extract reset time from various formats (expanded for better detection)
428
+ const resumeAtMatch = text.match(/(?:resume|reset|available|renew|refresh|restart|continue|try again)\s+(?:at|in|on)\s+(\d{1,2}\/\d{1,2}\/\d{4})\s*,?\s*(\d{1,2}:\d{2}(?::\d{2})?)\s*(am|pm)/i);
429
+ const resumeAt = resumeAtMatch
430
+ ? `${resumeAtMatch[1]}, ${resumeAtMatch[2]} ${resumeAtMatch[3].toUpperCase()}`
431
+ : undefined;
432
+
433
+ // Also try other time formats
434
+ if (!resumeAt) {
435
+ const timeMatch = text.match(/(?:try again|resume|reset|available|renew|refresh|restart|continue)\s+(?:in|after)\s+(\d+)\s*(?:minutes?|hours?|days?|seconds?)/i);
436
+ if (timeMatch) {
437
+ // For "in X minutes/hours/days" format, we'll return the raw text
438
+ return {
439
+ isRateLimited: true,
440
+ message: text,
441
+ resumeTime: timeMatch[1] + ' ' + timeMatch[2]
442
+ };
443
+ }
444
+ }
445
+
446
+ // Try specific time patterns like "resets at 12pm" or "resets at 3:30 PM"
447
+ if (!resumeAt) {
448
+ const timeOnlyMatch = text.match(/(?:resets|reset|resume|available)\s+(?:at|@)\s+(\d{1,2}:\d{2}(?::\d{2})?)\s*(am|pm)?/i);
449
+ if (timeOnlyMatch) {
450
+ const time = timeOnlyMatch[1] + (timeOnlyMatch[2] ? ' ' + timeOnlyMatch[2].toUpperCase() : '');
451
+ return {
452
+ isRateLimited: true,
453
+ message: text,
454
+ resumeTime: time
455
+ };
456
+ }
457
+ }
458
+
459
+ // Try date patterns like "resets on January 15" or "resets on 1/15"
460
+ if (!resumeAt) {
461
+ const dateMatch = text.match(/(?:resets|reset|resume|available)\s+(?:on|at)\s+(\d{1,2}\/\d{1,2})(?:\/\d{4})?/i);
462
+ if (dateMatch) {
463
+ return {
464
+ isRateLimited: true,
465
+ message: text,
466
+ resumeTime: dateMatch[1]
467
+ };
468
+ }
469
+ }
470
+
471
+ return {
472
+ isRateLimited: true,
473
+ message: text,
474
+ resumeAt
475
+ };
476
+ }
477
+
478
+ // If AppleScript didn't find quota messages, try fallback methods
479
+ return await this.checkKiroQuotaLimitFallback();
480
+
481
+ } catch (error) {
482
+ return {
483
+ isRateLimited: false,
484
+ note: `Failed to check AWS Kiro quota: ${error.message}`
485
+ };
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Fallback method to check Kiro quota using alternative approaches
491
+ * @returns {Promise<{isRateLimited: boolean, message?: string, note?: string}>}
492
+ */
493
+ async checkKiroQuotaLimitFallback() {
494
+ try {
495
+ // Method 1: Check if Kiro process is running and consuming high CPU (might indicate quota error)
496
+ const processCheck = execSync('ps aux | grep -i kiro | grep -v grep', {
497
+ encoding: 'utf8',
498
+ timeout: 3000
499
+ }).trim();
500
+
501
+ if (!processCheck) {
502
+ return {
503
+ isRateLimited: false,
504
+ note: 'AWS Kiro process not found'
505
+ };
506
+ }
507
+
508
+ // Method 2: Check for system notifications (macOS notifications about quota)
509
+ try {
510
+ const notifications = execSync('log show --predicate \'subsystem == "com.apple.notificationcenter"\' --info --last 5m | grep -i "kiro\\|quota\\|limit"', {
511
+ encoding: 'utf8',
512
+ timeout: 5000
513
+ }).trim();
514
+
515
+ if (notifications) {
516
+ const lower = notifications.toLowerCase();
517
+ if (lower.includes('quota') || lower.includes('limit') || lower.includes('exceeded')) {
518
+ return {
519
+ isRateLimited: true,
520
+ message: 'Quota limit detected in system notifications',
521
+ note: 'Detected via system notifications'
522
+ };
523
+ }
524
+ }
525
+ } catch (notifError) {
526
+ // Notifications check failed, continue
527
+ }
528
+
529
+ // Method 3: Check console logs for Kiro-related quota messages
530
+ try {
531
+ const consoleLogs = execSync('log show --predicate \'process == "Kiro" OR process == "AWS Kiro"\' --info --last 10m | grep -i "quota\\|limit\\|exceeded\\|error"', {
532
+ encoding: 'utf8',
533
+ timeout: 5000
534
+ }).trim();
535
+
536
+ if (consoleLogs) {
537
+ const lower = consoleLogs.toLowerCase();
538
+ if (lower.includes('quota') || lower.includes('limit') || lower.includes('exceeded')) {
539
+ return {
540
+ isRateLimited: true,
541
+ message: 'Quota limit detected in console logs',
542
+ note: 'Detected via console logs'
543
+ };
544
+ }
545
+ }
546
+ } catch (logError) {
547
+ // Console log check failed, continue
548
+ }
549
+
550
+ // Method 4: Check if Kiro window title contains quota information
551
+ try {
552
+ const windowTitle = execSync(`osascript -e '
553
+ tell application "System Events"
554
+ set processName to "AWS Kiro"
555
+ try
556
+ if not (exists process "AWS Kiro") then
557
+ set processName to "Kiro"
558
+ end if
559
+ on error
560
+ set processName to "Kiro"
561
+ end try
562
+
563
+ if exists process processName then
564
+ return title of window 1 of process processName
565
+ end if
566
+ end tell
567
+ '`, {
568
+ encoding: 'utf8',
569
+ timeout: 3000
570
+ }).trim();
571
+
572
+ if (windowTitle) {
573
+ const lower = windowTitle.toLowerCase();
574
+ if (lower.includes('quota') || lower.includes('limit') || lower.includes('exceeded')) {
575
+ return {
576
+ isRateLimited: true,
577
+ message: `Quota limit detected in window title: ${windowTitle}`,
578
+ note: 'Detected via window title'
579
+ };
580
+ }
581
+ }
582
+ } catch (titleError) {
583
+ // Window title check failed, continue
584
+ }
585
+
586
+ return {
587
+ isRateLimited: false,
588
+ note: 'No quota limit detected via fallback methods'
589
+ };
590
+
591
+ } catch (error) {
592
+ return {
593
+ isRateLimited: false,
594
+ note: `Fallback quota check failed: ${error.message}`
595
+ };
596
+ }
597
+ }
598
+
16
599
  /**
17
600
  * Open Cursor IDE
18
601
  * @returns {Promise<Object>} Result object with success status and details
@@ -169,6 +752,170 @@ class AppleScriptManager {
169
752
  }
170
753
  }
171
754
 
755
+ /**
756
+ * Open VS Code with optional repository path
757
+ * @param {string} repoPath - Optional repository path to open
758
+ * @returns {Promise<Object>} Result object with success status and details
759
+ */
760
+ async openVSCode(repoPath = null) {
761
+ try {
762
+ this.logger.log('Opening VS Code with remote debugging enabled...');
763
+
764
+ // First, check if VS Code is already running
765
+ try {
766
+ const isRunning = execSync('pgrep -x "Code"', { encoding: 'utf8', stdio: 'pipe' }).trim();
767
+ if (isRunning) {
768
+ this.logger.log('VS Code is already running - closing it to enable remote debugging...');
769
+ execSync('pkill -x "Code"', { stdio: 'pipe' });
770
+ await new Promise(resolve => setTimeout(resolve, 1000));
771
+ }
772
+ } catch (err) {
773
+ // VS Code not running, that's fine
774
+ }
775
+
776
+ // Launch VS Code with remote debugging enabled for quota detection
777
+ let command = 'open -a "Visual Studio Code" --args --remote-debugging-port=9222';
778
+ if (repoPath) {
779
+ command = `open -a "Visual Studio Code" "${repoPath}" --args --remote-debugging-port=9222`;
780
+ this.logger.log(`Opening VS Code with repository: ${repoPath}`);
781
+ }
782
+
783
+ execSync(command, { stdio: 'pipe' });
784
+ await new Promise(resolve => setTimeout(resolve, 3000));
785
+
786
+ // Verify remote debugging port is accessible
787
+ try {
788
+ const CDP = require('chrome-remote-interface');
789
+ await CDP.List({ port: 9222 });
790
+ this.logger.log('✅ VS Code remote debugging port 9222 is accessible');
791
+ } catch (cdpError) {
792
+ this.logger.log(`⚠️ Warning: VS Code remote debugging port not accessible: ${cdpError.message}`);
793
+ this.logger.log(' Waiting additional 2 seconds for VS Code to fully start...');
794
+ await new Promise(resolve => setTimeout(resolve, 2000));
795
+ }
796
+
797
+ this.logger.log('VS Code opened successfully with remote debugging');
798
+ return { success: true, message: `VS Code opened with repository: ${repoPath}`, method: 'applescript' };
799
+ } catch (error) {
800
+ this.logger.log('Error opening VS Code:', error.message);
801
+ return { success: false, error: error.message, method: 'applescript' };
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Open VS Code with a specific extension installed
807
+ * @param {string} extension - Extension key (e.g., 'github-copilot', 'amazon-q')
808
+ * @param {string} repoPath - Optional repository path to open
809
+ * @returns {Promise<Object>} Result object with success status and details
810
+ */
811
+ async openVSCodeWithExtension(extension, repoPath = null) {
812
+ try {
813
+ this.logger.log(`Opening VS Code with ${extension} extension...`);
814
+
815
+ // Extension IDs mapping
816
+ const extensionIds = {
817
+ 'github-copilot': 'GitHub.copilot',
818
+ 'amazon-q': 'amazonwebservices.amazon-q-vscode',
819
+ 'continue': 'Continue.continue'
820
+ };
821
+
822
+ const extensionId = extensionIds[extension];
823
+ if (!extensionId) {
824
+ return { success: false, error: `Unknown extension: ${extension}`, method: 'applescript' };
825
+ }
826
+
827
+ // First, check if the extension is installed
828
+ try {
829
+ const checkCmd = `code --list-extensions | grep -i "${extensionId}"`;
830
+ execSync(checkCmd, { stdio: 'pipe' });
831
+ this.logger.log(`Extension ${extension} is already installed`);
832
+ } catch (checkError) {
833
+ // Extension not found, try to install it
834
+ this.logger.log(`Extension ${extension} not found, installing...`);
835
+ try {
836
+ execSync(`code --install-extension ${extensionId} --force`, { stdio: 'pipe', timeout: 60000 });
837
+ this.logger.log(`Extension ${extension} installed successfully`);
838
+ } catch (installError) {
839
+ this.logger.log(`Failed to install extension ${extension}:`, installError.message);
840
+ return { success: false, error: `Failed to install extension: ${installError.message}`, method: 'applescript' };
841
+ }
842
+ }
843
+
844
+ // First, check if VS Code is already running
845
+ try {
846
+ const isRunning = execSync('pgrep -x "Code"', { encoding: 'utf8', stdio: 'pipe' }).trim();
847
+ if (isRunning) {
848
+ this.logger.log('VS Code is already running - closing it to enable remote debugging...');
849
+ execSync('pkill -x "Code"', { stdio: 'pipe' });
850
+ await new Promise(resolve => setTimeout(resolve, 1000));
851
+ }
852
+ } catch (err) {
853
+ // VS Code not running, that's fine
854
+ }
855
+
856
+ // Open VS Code with the repository and remote debugging enabled
857
+ let command = 'open -a "Visual Studio Code" --args --remote-debugging-port=9222';
858
+ if (repoPath) {
859
+ command = `open -a "Visual Studio Code" "${repoPath}" --args --remote-debugging-port=9222`;
860
+ this.logger.log(`Opening VS Code with repository: ${repoPath}`);
861
+ }
862
+
863
+ execSync(command, { stdio: 'pipe' });
864
+ await new Promise(resolve => setTimeout(resolve, 3000));
865
+
866
+ // Verify remote debugging port is accessible
867
+ try {
868
+ const CDP = require('chrome-remote-interface');
869
+ await CDP.List({ port: 9222 });
870
+ this.logger.log('✅ VS Code remote debugging port 9222 is accessible');
871
+ } catch (cdpError) {
872
+ this.logger.log(`⚠️ Warning: VS Code remote debugging port not accessible: ${cdpError.message}`);
873
+ this.logger.log(' Waiting additional 2 seconds for VS Code to fully start...');
874
+ await new Promise(resolve => setTimeout(resolve, 2000));
875
+ }
876
+
877
+ this.logger.log(`VS Code opened successfully with ${extension} extension and remote debugging`);
878
+ return { success: true, message: `VS Code opened with ${extension} extension`, method: 'applescript' };
879
+ } catch (error) {
880
+ this.logger.log(`Error opening VS Code with ${extension}:`, error.message);
881
+ return { success: false, error: error.message, method: 'applescript' };
882
+ }
883
+ }
884
+
885
+ /**
886
+ * Open Replit Agent with optional repository path
887
+ * @param {string} repoPath - Optional repository path to open
888
+ * @returns {Promise<Object>} Result object with success status and details
889
+ */
890
+ async openReplit(repoPath = null) {
891
+ try {
892
+ this.logger.log('Opening Replit...');
893
+
894
+ // Replit typically uses web browser, so we'll open the Replit website
895
+ const replitUrl = repoPath
896
+ ? `https://replit.com/@replit/agent?path=${encodeURIComponent(repoPath)}`
897
+ : 'https://replit.com/@replit/agent';
898
+
899
+ let command;
900
+ if (this.platform === 'darwin') {
901
+ command = `open "${replitUrl}"`;
902
+ } else if (this.platform === 'win32') {
903
+ command = `start "" "${replitUrl}"`;
904
+ } else {
905
+ command = `xdg-open "${replitUrl}"`;
906
+ }
907
+
908
+ execSync(command, { stdio: 'pipe' });
909
+ await new Promise(resolve => setTimeout(resolve, 1000));
910
+
911
+ this.logger.log('Replit opened successfully');
912
+ return { success: true, message: 'Replit opened in browser', method: 'applescript' };
913
+ } catch (error) {
914
+ this.logger.log('Error opening Replit:', error.message);
915
+ return { success: false, error: error.message, method: 'applescript' };
916
+ }
917
+ }
918
+
172
919
  /**
173
920
  * Handle Antigravity quota limit by automatically switching models
174
921
  * @returns {Promise<{success: boolean, model?: string, error?: string}>}
@@ -224,7 +971,7 @@ class AppleScriptManager {
224
971
 
225
972
  -- Look for model dropdown/menu items
226
973
  -- Try alternative models in order of preference
227
- set modelNames to {"Gemini 3 Pro (Low)", "Claude Sonnet 4.5", "Claude Sonnet 4.5 (Thinking)", "GPT-OSS 120B (Medium)"}
974
+ set modelNames to {"Gemini 3 Flash", "Gemini 3 Pro (High)", "Gemini 3 Pro (Low)", "Claude Sonnet 4.5", "Claude Sonnet 4.5 (Thinking)", "Claude Opus 4.5 (Thinking)", "GPT-OSS 120B (Medium)"}
228
975
 
229
976
  repeat with modelName in modelNames
230
977
  try
@@ -233,7 +980,7 @@ class AppleScriptManager {
233
980
 
234
981
  -- Try menu items
235
982
  try
236
- set modelItems to menu items of menu 1 of window 1 whose name is modelName
983
+ set modelItems to menu items of menu 1 of window 1 whose name contains modelName
237
984
  end try
238
985
 
239
986
  -- Try buttons if no menu items found
@@ -243,6 +990,22 @@ class AppleScriptManager {
243
990
  end try
244
991
  end if
245
992
 
993
+ -- Search in groups
994
+ if (count of modelItems) = 0 then
995
+ try
996
+ set allGroups to groups of window 1
997
+ repeat with grp in allGroups
998
+ try
999
+ set matchingItems to (every UI element of grp whose name contains modelName)
1000
+ if (count of matchingItems) > 0 then
1001
+ set modelItems to matchingItems
1002
+ exit repeat
1003
+ end if
1004
+ end try
1005
+ end repeat
1006
+ end try
1007
+ end if
1008
+
246
1009
  if (count of modelItems) > 0 then
247
1010
  click item 1 of modelItems
248
1011
  delay 0.8
@@ -310,12 +1073,27 @@ class AppleScriptManager {
310
1073
  return await this.openWindsurf(repoPath);
311
1074
  case 'antigravity':
312
1075
  return await this.openAntigravity(repoPath);
1076
+ case 'vscode':
1077
+ return await this.openVSCode(repoPath);
1078
+ case 'github-copilot':
1079
+ // Open VS Code with GitHub Copilot extension
1080
+ return await this.openVSCodeWithExtension('github-copilot', repoPath);
1081
+ case 'amazon-q':
1082
+ // Open VS Code with Amazon Q extension
1083
+ return await this.openVSCodeWithExtension('amazon-q', repoPath);
1084
+ case 'replit':
1085
+ return await this.openReplit(repoPath);
313
1086
  case 'kiro':
314
1087
  return await this.openKiro(repoPath);
1088
+ case 'claude':
1089
+ case 'claude-code':
1090
+ return await this.openClaude(repoPath);
1091
+ case 'gemini':
1092
+ return await this.openGemini(repoPath);
315
1093
  default:
316
1094
  return {
317
1095
  success: false,
318
- error: `Unknown IDE: ${ide}. Supported: cursor, windsurf, antigravity, kiro`
1096
+ error: `Unknown IDE: ${ide}. Supported: cursor, windsurf, antigravity, vscode, github-copilot, amazon-q, replit, kiro, claude, claude-code, gemini`
319
1097
  };
320
1098
  }
321
1099
  }
@@ -416,6 +1194,32 @@ class AppleScriptManager {
416
1194
  end tell
417
1195
  end tell
418
1196
  `;
1197
+ } else if (ide === 'vscode' || ide === 'github-copilot' || ide === 'amazon-q') {
1198
+ // AppleScript for VS Code (including GitHub Copilot and Amazon Q extensions)
1199
+ appleScript = `
1200
+ tell application "System Events"
1201
+ tell process "Code"
1202
+ set frontmost to true
1203
+ delay 1.0
1204
+
1205
+ -- Focus GitHub Copilot chat with Cmd+Shift+I or open chat panel
1206
+ key code 34 using {command down, shift down}
1207
+ delay 1.0
1208
+
1209
+ -- Type the message using clipboard (more reliable)
1210
+ set the clipboard to "${text.replace(/"/g, '\\"')}"
1211
+ delay 0.5
1212
+ key code 9 using {command down} -- Cmd+V to paste
1213
+ delay 0.5
1214
+
1215
+ -- Press Cmd+Enter to send
1216
+ key code 36 using {command down}
1217
+ delay 1.0
1218
+
1219
+ return "Message sent to VS Code"
1220
+ end tell
1221
+ end tell
1222
+ `;
419
1223
  } else if (ide === 'antigravity') {
420
1224
  // AppleScript for Google Antigravity - Cmd+Shift+L opens Agent chat (non-toggle)
421
1225
  appleScript = `
@@ -504,7 +1308,7 @@ class AppleScriptManager {
504
1308
  return {
505
1309
  success: false,
506
1310
  error: `Unsupported IDE for AppleScript: ${ide}`,
507
- note: 'AppleScript is only supported for Cursor, Windsurf, Antigravity, VS Code, AWS Kiro, and Claude'
1311
+ note: 'AppleScript is only supported for Cursor, Windsurf, Antigravity, VS Code, GitHub Copilot, Amazon Q, AWS Kiro, and Claude'
508
1312
  };
509
1313
  }
510
1314
 
@@ -1158,6 +1962,260 @@ class AppleScriptManager {
1158
1962
  throw error;
1159
1963
  }
1160
1964
  }
1965
+
1966
+ /**
1967
+ * Check if an IDE is running and accessible
1968
+ * @param {string} ide - The IDE name
1969
+ * @returns {Promise<Object>} Result object with success status and details
1970
+ */
1971
+ async getIDEStatus(ide) {
1972
+ if (!ide || typeof ide !== 'string') {
1973
+ return {
1974
+ success: false,
1975
+ error: 'Invalid IDE name provided',
1976
+ running: false
1977
+ };
1978
+ }
1979
+
1980
+ try {
1981
+ this.logger.log(`🔍 Checking status of ${ide}...`);
1982
+
1983
+ switch (ide.toLowerCase()) {
1984
+ case 'cursor':
1985
+ return await this._checkCursorStatus();
1986
+ case 'vscode':
1987
+ return await this._checkVSCodeStatus();
1988
+ case 'windsurf':
1989
+ return await this._checkWindsurfStatus();
1990
+ case 'antigravity':
1991
+ return await this._checkAntigravityStatus();
1992
+ case 'cline':
1993
+ return await this._checkClineStatus();
1994
+ default:
1995
+ return {
1996
+ success: false,
1997
+ error: `Unsupported IDE: ${ide}`,
1998
+ running: false
1999
+ };
2000
+ }
2001
+ } catch (error) {
2002
+ return {
2003
+ success: false,
2004
+ error: `Failed to check IDE status: ${error.message}`,
2005
+ running: false
2006
+ };
2007
+ }
2008
+ }
2009
+
2010
+ /**
2011
+ * Check Cursor status
2012
+ * @private
2013
+ */
2014
+ async _checkCursorStatus() {
2015
+ try {
2016
+ const script = `
2017
+ tell application "System Events"
2018
+ set isRunning to (exists process "Cursor")
2019
+ if isRunning then
2020
+ try
2021
+ tell application "Cursor" to activate
2022
+ delay 0.5
2023
+ return "running"
2024
+ on error
2025
+ return "error"
2026
+ end try
2027
+ else
2028
+ return "not_running"
2029
+ end if
2030
+ end tell
2031
+ `;
2032
+
2033
+ const result = execSync(`osascript -e '${script}'`, { stdio: 'pipe', encoding: 'utf8' });
2034
+ const running = result.includes('running');
2035
+
2036
+ return {
2037
+ success: true,
2038
+ running,
2039
+ status: running ? 'running' : 'not_running',
2040
+ message: running ? 'Cursor is running and accessible' : 'Cursor is not running'
2041
+ };
2042
+ } catch (error) {
2043
+ return {
2044
+ success: false,
2045
+ error: `Failed to check Cursor status: ${error.message}`,
2046
+ running: false
2047
+ };
2048
+ }
2049
+ }
2050
+
2051
+ /**
2052
+ * Check VS Code status
2053
+ * @private
2054
+ */
2055
+ async _checkVSCodeStatus() {
2056
+ try {
2057
+ const script = `
2058
+ tell application "System Events"
2059
+ set isRunning to (exists process "Code")
2060
+ if isRunning then
2061
+ try
2062
+ tell application "Visual Studio Code" to activate
2063
+ delay 0.5
2064
+ return "running"
2065
+ on error
2066
+ return "error"
2067
+ end try
2068
+ else
2069
+ return "not_running"
2070
+ end if
2071
+ end tell
2072
+ `;
2073
+
2074
+ const result = execSync(`osascript -e '${script}'`, { stdio: 'pipe', encoding: 'utf8' });
2075
+ const running = result.includes('running');
2076
+
2077
+ return {
2078
+ success: true,
2079
+ running,
2080
+ status: running ? 'running' : 'not_running',
2081
+ message: running ? 'VS Code is running and accessible' : 'VS Code is not running'
2082
+ };
2083
+ } catch (error) {
2084
+ return {
2085
+ success: false,
2086
+ error: `Failed to check VS Code status: ${error.message}`,
2087
+ running: false
2088
+ };
2089
+ }
2090
+ }
2091
+
2092
+ /**
2093
+ * Check Windsurf status
2094
+ * @private
2095
+ */
2096
+ async _checkWindsurfStatus() {
2097
+ try {
2098
+ const script = `
2099
+ tell application "System Events"
2100
+ set isRunning to (exists process "Windsurf")
2101
+ if isRunning then
2102
+ try
2103
+ tell application "Windsurf" to activate
2104
+ delay 0.5
2105
+ return "running"
2106
+ on error
2107
+ return "error"
2108
+ end try
2109
+ else
2110
+ return "not_running"
2111
+ end if
2112
+ end tell
2113
+ `;
2114
+
2115
+ const result = execSync(`osascript -e '${script}'`, { stdio: 'pipe', encoding: 'utf8' });
2116
+ const running = result.includes('running');
2117
+
2118
+ return {
2119
+ success: true,
2120
+ running,
2121
+ status: running ? 'running' : 'not_running',
2122
+ message: running ? 'Windsurf is running and accessible' : 'Windsurf is not running'
2123
+ };
2124
+ } catch (error) {
2125
+ return {
2126
+ success: false,
2127
+ error: `Failed to check Windsurf status: ${error.message}`,
2128
+ running: false
2129
+ };
2130
+ }
2131
+ }
2132
+
2133
+ /**
2134
+ * Check Antigravity status
2135
+ * @private
2136
+ */
2137
+ async _checkAntigravityStatus() {
2138
+ try {
2139
+ // Antigravity runs in browser, check if Chrome/Edge is running with Antigravity tab
2140
+ const script = `
2141
+ tell application "System Events"
2142
+ set isChromeRunning to (exists process "Google Chrome")
2143
+ set isEdgeRunning to (exists process "Microsoft Edge")
2144
+
2145
+ if isChromeRunning then
2146
+ try
2147
+ tell application "Google Chrome" to activate
2148
+ delay 0.5
2149
+ return "running"
2150
+ on error
2151
+ return "error"
2152
+ end try
2153
+ else if isEdgeRunning then
2154
+ try
2155
+ tell application "Microsoft Edge" to activate
2156
+ delay 0.5
2157
+ return "running"
2158
+ on error
2159
+ return "error"
2160
+ end try
2161
+ else
2162
+ return "not_running"
2163
+ end if
2164
+ end tell
2165
+ `;
2166
+
2167
+ const result = execSync(`osascript -e '${script}'`, { stdio: 'pipe', encoding: 'utf8' });
2168
+ const running = result.includes('running');
2169
+
2170
+ return {
2171
+ success: true,
2172
+ running,
2173
+ status: running ? 'running' : 'not_running',
2174
+ message: running ? 'Browser is running (Antigravity accessible)' : 'Browser is not running'
2175
+ };
2176
+ } catch (error) {
2177
+ return {
2178
+ success: false,
2179
+ error: `Failed to check Antigravity status: ${error.message}`,
2180
+ running: false
2181
+ };
2182
+ }
2183
+ }
2184
+
2185
+ /**
2186
+ * Check Cline status
2187
+ * @private
2188
+ */
2189
+ async _checkClineStatus() {
2190
+ try {
2191
+ // Cline runs in VS Code as an extension
2192
+ const vscodeStatus = await this._checkVSCodeStatus();
2193
+
2194
+ if (!vscodeStatus.running) {
2195
+ return {
2196
+ success: true,
2197
+ running: false,
2198
+ status: 'not_running',
2199
+ message: 'Cline requires VS Code to be running'
2200
+ };
2201
+ }
2202
+
2203
+ // For now, assume Cline is available if VS Code is running
2204
+ // In a more sophisticated implementation, we could check if the extension is installed
2205
+ return {
2206
+ success: true,
2207
+ running: true,
2208
+ status: 'running',
2209
+ message: 'Cline is available (VS Code running)'
2210
+ };
2211
+ } catch (error) {
2212
+ return {
2213
+ success: false,
2214
+ error: `Failed to check Cline status: ${error.message}`,
2215
+ running: false
2216
+ };
2217
+ }
2218
+ }
1161
2219
  }
1162
2220
 
1163
2221
  module.exports = { AppleScriptManager };