vibecodingmachine-core 1.0.2 → 2025.11.2-7.1302
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/.babelrc +13 -13
- package/README.md +28 -28
- package/__tests__/applescript-manager-claude-fix.test.js +286 -286
- package/__tests__/requirement-2-auto-start-looping.test.js +69 -69
- package/__tests__/requirement-3-auto-start-looping.test.js +69 -69
- package/__tests__/requirement-4-auto-start-looping.test.js +69 -69
- package/__tests__/requirement-6-auto-start-looping.test.js +73 -73
- package/__tests__/requirement-7-status-tracking.test.js +332 -332
- package/jest.config.js +18 -18
- package/jest.setup.js +12 -12
- package/package.json +48 -48
- package/src/auth/access-denied.html +119 -119
- package/src/auth/shared-auth-storage.js +230 -230
- package/src/autonomous-mode/feature-implementer.cjs +70 -70
- package/src/autonomous-mode/feature-implementer.js +425 -425
- package/src/chat-management/chat-manager.cjs +71 -71
- package/src/chat-management/chat-manager.js +342 -342
- package/src/ide-integration/__tests__/applescript-manager-thread-closure.test.js +227 -227
- package/src/ide-integration/aider-cli-manager.cjs +850 -850
- package/src/ide-integration/applescript-manager.cjs +1088 -1088
- package/src/ide-integration/applescript-manager.js +2802 -2802
- package/src/ide-integration/applescript-utils.js +306 -306
- package/src/ide-integration/cdp-manager.cjs +221 -221
- package/src/ide-integration/cdp-manager.js +321 -321
- package/src/ide-integration/claude-code-cli-manager.cjs +301 -301
- package/src/ide-integration/cline-cli-manager.cjs +2252 -2252
- package/src/ide-integration/continue-cli-manager.js +431 -431
- package/src/ide-integration/provider-manager.cjs +354 -354
- package/src/ide-integration/quota-detector.cjs +34 -34
- package/src/ide-integration/quota-detector.js +349 -349
- package/src/ide-integration/windows-automation-manager.js +262 -262
- package/src/index.cjs +47 -43
- package/src/index.js +17 -17
- package/src/llm/direct-llm-manager.cjs +609 -609
- package/src/ui/ButtonComponents.js +247 -247
- package/src/ui/ChatInterface.js +499 -499
- package/src/ui/StateManager.js +259 -259
- package/src/utils/audit-logger.cjs +116 -116
- package/src/utils/config-helpers.cjs +94 -94
- package/src/utils/config-helpers.js +94 -94
- package/src/utils/electron-update-checker.js +113 -85
- package/src/utils/gcloud-auth.cjs +394 -394
- package/src/utils/logger.cjs +193 -193
- package/src/utils/logger.js +191 -191
- package/src/utils/repo-helpers.cjs +120 -120
- package/src/utils/repo-helpers.js +120 -120
- package/src/utils/requirement-helpers.js +432 -432
- package/src/utils/update-checker.js +227 -167
- package/src/utils/version-checker.js +169 -0
|
@@ -1,1088 +1,1088 @@
|
|
|
1
|
-
// @vibecodingmachine/core - AppleScript Manager (CommonJS)
|
|
2
|
-
// Handles AppleScript-based interactions with IDEs that don't support CDP
|
|
3
|
-
|
|
4
|
-
const { execSync } = require('child_process');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* AppleScript Manager for IDE interactions
|
|
8
|
-
* Handles AppleScript-based text sending and response reading for IDEs like Cursor and Windsurf
|
|
9
|
-
*/
|
|
10
|
-
class AppleScriptManager {
|
|
11
|
-
constructor() {
|
|
12
|
-
this.logger = console;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Open Cursor IDE
|
|
17
|
-
* @returns {Promise<Object>} Result object with success status and details
|
|
18
|
-
*/
|
|
19
|
-
async openCursor() {
|
|
20
|
-
try {
|
|
21
|
-
const appleScript = `
|
|
22
|
-
tell application "Cursor"
|
|
23
|
-
activate
|
|
24
|
-
end tell
|
|
25
|
-
`;
|
|
26
|
-
|
|
27
|
-
execSync(`osascript -e '${appleScript}'`, { encoding: 'utf8' });
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
success: true,
|
|
31
|
-
message: 'Cursor opened successfully'
|
|
32
|
-
};
|
|
33
|
-
} catch (error) {
|
|
34
|
-
return {
|
|
35
|
-
success: false,
|
|
36
|
-
error: `Failed to open Cursor: ${error.message}`
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Open Windsurf IDE
|
|
43
|
-
* @returns {Promise<Object>} Result object with success status and details
|
|
44
|
-
*/
|
|
45
|
-
async openWindsurf() {
|
|
46
|
-
try {
|
|
47
|
-
// Use multiple separate AppleScript calls for better reliability
|
|
48
|
-
try {
|
|
49
|
-
// First, try to launch
|
|
50
|
-
execSync('osascript -e \'tell application "Windsurf" to launch\'', { encoding: 'utf8', timeout: 5000 });
|
|
51
|
-
} catch (launchError) {
|
|
52
|
-
// If launch fails, it might already be running
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Wait a moment
|
|
56
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
57
|
-
|
|
58
|
-
// Then activate
|
|
59
|
-
execSync('osascript -e \'tell application "Windsurf" to activate\'', { encoding: 'utf8', timeout: 5000 });
|
|
60
|
-
|
|
61
|
-
// Wait a moment
|
|
62
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
63
|
-
|
|
64
|
-
// Finally, ensure it's frontmost
|
|
65
|
-
try {
|
|
66
|
-
execSync('osascript -e \'tell application "System Events" to tell process "Windsurf" to set frontmost to true\'', { encoding: 'utf8', timeout: 5000 });
|
|
67
|
-
} catch (frontmostError) {
|
|
68
|
-
// If setting frontmost fails, that's okay - the app is still activated
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
success: true,
|
|
73
|
-
message: 'Windsurf launched and activated successfully'
|
|
74
|
-
};
|
|
75
|
-
} catch (error) {
|
|
76
|
-
return {
|
|
77
|
-
success: false,
|
|
78
|
-
error: `Failed to open Windsurf: ${error.message}`
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async openAntigravity(repoPath = null) {
|
|
84
|
-
try {
|
|
85
|
-
this.logger.log('Opening Google Antigravity...');
|
|
86
|
-
|
|
87
|
-
let command = `open -a "Antigravity"`;
|
|
88
|
-
if (repoPath) {
|
|
89
|
-
command += ` "${repoPath}"`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
execSync(command, { encoding: 'utf8', timeout: 5000 });
|
|
93
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
success: true,
|
|
97
|
-
message: repoPath ? `Google Antigravity opened with repository: ${repoPath}` : 'Google Antigravity opened successfully'
|
|
98
|
-
};
|
|
99
|
-
} catch (error) {
|
|
100
|
-
return {
|
|
101
|
-
success: false,
|
|
102
|
-
error: `Failed to open Google Antigravity: ${error.message}`
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Handle Antigravity quota limit by automatically switching models
|
|
109
|
-
* @returns {Promise<{success: boolean, model?: string, error?: string}>}
|
|
110
|
-
*/
|
|
111
|
-
async handleAntigravityQuotaLimit() {
|
|
112
|
-
try {
|
|
113
|
-
this.logger.log('Attempting to handle Antigravity quota limit...');
|
|
114
|
-
|
|
115
|
-
const script = `
|
|
116
|
-
tell application "System Events"
|
|
117
|
-
tell process "Antigravity"
|
|
118
|
-
set frontmost to true
|
|
119
|
-
delay 0.8
|
|
120
|
-
|
|
121
|
-
-- Try to find and click "Select another model" button
|
|
122
|
-
try
|
|
123
|
-
set modelButtons to buttons of window 1 whose name contains "Select another model"
|
|
124
|
-
if (count of modelButtons) > 0 then
|
|
125
|
-
click item 1 of modelButtons
|
|
126
|
-
delay 1.5
|
|
127
|
-
|
|
128
|
-
-- Look for model dropdown/menu items
|
|
129
|
-
-- Try alternative models in order of preference
|
|
130
|
-
set modelNames to {"Gemini 3 Pro (Low)", "Claude Sonnet 4.5", "Claude Sonnet 4.5 (Thinking)", "GPT-OSS 120B (Medium)"}
|
|
131
|
-
|
|
132
|
-
repeat with modelName in modelNames
|
|
133
|
-
try
|
|
134
|
-
-- Try to find and click this model option
|
|
135
|
-
set modelItems to menu items of menu 1 of window 1 whose name is modelName
|
|
136
|
-
if (count of modelItems) > 0 then
|
|
137
|
-
click item 1 of modelItems
|
|
138
|
-
delay 0.8
|
|
139
|
-
|
|
140
|
-
-- Click "Accept all" or similar confirmation button
|
|
141
|
-
try
|
|
142
|
-
set acceptButtons to buttons of window 1 whose name contains "Accept"
|
|
143
|
-
if (count of acceptButtons) > 0 then
|
|
144
|
-
click item 1 of acceptButtons
|
|
145
|
-
delay 0.5
|
|
146
|
-
return "success:" & modelName
|
|
147
|
-
end if
|
|
148
|
-
end try
|
|
149
|
-
|
|
150
|
-
return "success:" & modelName
|
|
151
|
-
end if
|
|
152
|
-
end try
|
|
153
|
-
end repeat
|
|
154
|
-
|
|
155
|
-
return "error:no-models-available"
|
|
156
|
-
else
|
|
157
|
-
return "error:button-not-found"
|
|
158
|
-
end if
|
|
159
|
-
on error errMsg
|
|
160
|
-
return "error:" & errMsg
|
|
161
|
-
end try
|
|
162
|
-
end tell
|
|
163
|
-
end tell
|
|
164
|
-
`;
|
|
165
|
-
|
|
166
|
-
const result = execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, {
|
|
167
|
-
encoding: 'utf8',
|
|
168
|
-
timeout: 15000
|
|
169
|
-
}).trim();
|
|
170
|
-
|
|
171
|
-
this.logger.log('AppleScript result:', result);
|
|
172
|
-
|
|
173
|
-
if (result.startsWith('success:')) {
|
|
174
|
-
const model = result.substring(8);
|
|
175
|
-
this.logger.log(`Successfully switched to model: ${model}`);
|
|
176
|
-
return { success: true, model };
|
|
177
|
-
} else {
|
|
178
|
-
const error = result.substring(6);
|
|
179
|
-
this.logger.log(`Failed to switch model: ${error}`);
|
|
180
|
-
return { success: false, error };
|
|
181
|
-
}
|
|
182
|
-
} catch (error) {
|
|
183
|
-
this.logger.log('Error handling Antigravity quota limit:', error.message);
|
|
184
|
-
return { success: false, error: error.message };
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async openIDE(ide, repoPath = null) {
|
|
189
|
-
const ideLower = (ide || '').toLowerCase();
|
|
190
|
-
|
|
191
|
-
switch (ideLower) {
|
|
192
|
-
case 'cursor':
|
|
193
|
-
return await this.openCursor(repoPath);
|
|
194
|
-
case 'windsurf':
|
|
195
|
-
return await this.openWindsurf(repoPath);
|
|
196
|
-
case 'antigravity':
|
|
197
|
-
return await this.openAntigravity(repoPath);
|
|
198
|
-
default:
|
|
199
|
-
return {
|
|
200
|
-
success: false,
|
|
201
|
-
error: `Unknown IDE: ${ide}. Supported: cursor, windsurf, antigravity`
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Send text to an IDE using AppleScript
|
|
208
|
-
* @param {string} text - The text to send
|
|
209
|
-
* @param {string} ide - The IDE name ('cursor', 'windsurf', 'antigravity')
|
|
210
|
-
* @returns {Promise<Object>} Result object with success status and details
|
|
211
|
-
*/
|
|
212
|
-
async sendText(text, ide) {
|
|
213
|
-
if (typeof text !== 'string') {
|
|
214
|
-
return {
|
|
215
|
-
success: false,
|
|
216
|
-
error: `Invalid text type: ${typeof text}. Expected string.`,
|
|
217
|
-
debug: { textType: typeof text, textValue: text }
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const ideName = ide === 'windsurf' ? 'Windsurf' : ide === 'cursor' ? 'Cursor' : ide === 'antigravity' ? 'Antigravity' : 'Unknown IDE';
|
|
222
|
-
|
|
223
|
-
this.logger.log(`${ideName} detected - using AppleScript for reliable text sending`);
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
let appleScript;
|
|
227
|
-
|
|
228
|
-
if (ide === 'windsurf') {
|
|
229
|
-
// AppleScript for Windsurf - robust chat input targeting
|
|
230
|
-
appleScript = `
|
|
231
|
-
tell application "System Events"
|
|
232
|
-
tell process "Windsurf"
|
|
233
|
-
set frontmost to true
|
|
234
|
-
delay 1
|
|
235
|
-
|
|
236
|
-
-- Method 1: Try to find and click on the chat input field
|
|
237
|
-
try
|
|
238
|
-
set chatInput to text field 1 of group 1 of window 1
|
|
239
|
-
click chatInput
|
|
240
|
-
delay 0.5
|
|
241
|
-
on error
|
|
242
|
-
try
|
|
243
|
-
-- Method 2: Try to find text area for chat
|
|
244
|
-
set chatInput to text area 1 of group 1 of window 1
|
|
245
|
-
click chatInput
|
|
246
|
-
delay 0.5
|
|
247
|
-
on error
|
|
248
|
-
try
|
|
249
|
-
-- Method 3: Try to find any input field
|
|
250
|
-
set chatInput to text field 1 of window 1
|
|
251
|
-
click chatInput
|
|
252
|
-
delay 0.5
|
|
253
|
-
on error
|
|
254
|
-
-- Method 4: Just try typing (fallback)
|
|
255
|
-
delay 0.5
|
|
256
|
-
end try
|
|
257
|
-
end try
|
|
258
|
-
end try
|
|
259
|
-
|
|
260
|
-
-- Type the message
|
|
261
|
-
keystroke "${text.replace(/"/g, '\\"')}"
|
|
262
|
-
delay 0.5
|
|
263
|
-
|
|
264
|
-
-- Try multiple submission methods
|
|
265
|
-
key code 36
|
|
266
|
-
delay 0.5
|
|
267
|
-
key code 36 using {command down}
|
|
268
|
-
delay 0.5
|
|
269
|
-
key code 36 using {command down, shift down}
|
|
270
|
-
delay 0.5
|
|
271
|
-
key code 1 using {command down}
|
|
272
|
-
delay 0.5
|
|
273
|
-
end tell
|
|
274
|
-
end tell
|
|
275
|
-
`;
|
|
276
|
-
} else if (ide === 'cursor') {
|
|
277
|
-
// AppleScript for Cursor - proven to work (from commit 3403c17)
|
|
278
|
-
appleScript = `
|
|
279
|
-
tell application "System Events"
|
|
280
|
-
tell process "Cursor"
|
|
281
|
-
set frontmost to true
|
|
282
|
-
delay 1
|
|
283
|
-
|
|
284
|
-
-- DYNAMIC SCREENSHOT-BASED CHAT INPUT TARGETING
|
|
285
|
-
-- Method 1: Take screenshot of the monitor where Cursor is running
|
|
286
|
-
try
|
|
287
|
-
-- Get the bounds of the Cursor window to determine which monitor it's on
|
|
288
|
-
set cursorBounds to bounds of window 1
|
|
289
|
-
set cursorX to item 1 of cursorBounds
|
|
290
|
-
set cursorY to item 2 of cursorBounds
|
|
291
|
-
|
|
292
|
-
-- Take a screenshot of the specific monitor where Cursor is located
|
|
293
|
-
set screenshotPath to (path to desktop as string) & "cursor_screenshot.png"
|
|
294
|
-
|
|
295
|
-
-- Use screencapture with display selection based on Cursor window position
|
|
296
|
-
-- If Cursor is on the right side (aux monitor), use display 2
|
|
297
|
-
if cursorX > 1000 then
|
|
298
|
-
do shell script "screencapture -D 2 -x " & quoted form of POSIX path of screenshotPath
|
|
299
|
-
else
|
|
300
|
-
do shell script "screencapture -D 1 -x " & quoted form of POSIX path of screenshotPath
|
|
301
|
-
end if
|
|
302
|
-
delay 0.5
|
|
303
|
-
|
|
304
|
-
-- Use Python with PIL to analyze the screenshot and find text input area
|
|
305
|
-
set pythonScript to "
|
|
306
|
-
import sys
|
|
307
|
-
import os
|
|
308
|
-
from PIL import Image, ImageDraw
|
|
309
|
-
import subprocess
|
|
310
|
-
|
|
311
|
-
try:
|
|
312
|
-
# Read the screenshot
|
|
313
|
-
img = Image.open('" & POSIX path of screenshotPath & "')
|
|
314
|
-
width, height = img.size
|
|
315
|
-
|
|
316
|
-
# Convert to grayscale for analysis
|
|
317
|
-
gray = img.convert('L')
|
|
318
|
-
|
|
319
|
-
# Get screen dimensions to calculate relative positions
|
|
320
|
-
screen_width = width
|
|
321
|
-
screen_height = height
|
|
322
|
-
|
|
323
|
-
# Determine if this is the auxiliary monitor based on Cursor window position
|
|
324
|
-
cursor_x = " & cursorX & "
|
|
325
|
-
is_aux_monitor = cursor_x > 1000
|
|
326
|
-
|
|
327
|
-
if is_aux_monitor:
|
|
328
|
-
# For auxiliary monitor, adjust coordinates
|
|
329
|
-
# Chat input is typically in the right side of the aux monitor
|
|
330
|
-
candidates = [
|
|
331
|
-
(int(screen_width * 0.8), int(screen_height * 0.85)), # Bottom-right
|
|
332
|
-
(int(screen_width * 0.75), int(screen_height * 0.8)), # Slightly up
|
|
333
|
-
(int(screen_width * 0.85), int(screen_height * 0.8)), # More right
|
|
334
|
-
(int(screen_width * 0.7), int(screen_height * 0.9)), # Bottom-left of right area
|
|
335
|
-
]
|
|
336
|
-
else:
|
|
337
|
-
# For main monitor, use standard coordinates
|
|
338
|
-
candidates = [
|
|
339
|
-
(int(screen_width * 0.8), int(screen_height * 0.85)), # Bottom-right
|
|
340
|
-
(int(screen_width * 0.75), int(screen_height * 0.8)), # Slightly up
|
|
341
|
-
(int(screen_width * 0.85), int(screen_height * 0.8)), # More right
|
|
342
|
-
(int(screen_width * 0.7), int(screen_height * 0.9)), # Bottom-left of right area
|
|
343
|
-
]
|
|
344
|
-
|
|
345
|
-
# Use the first candidate (bottom-right area)
|
|
346
|
-
x, y = candidates[0]
|
|
347
|
-
print(f'{x},{y}')
|
|
348
|
-
|
|
349
|
-
except Exception as e:
|
|
350
|
-
# Fallback to estimated coordinates based on monitor
|
|
351
|
-
if " & cursorX & " > 1000:
|
|
352
|
-
print('1000,600') # Aux monitor fallback
|
|
353
|
-
else:
|
|
354
|
-
print('800,500') # Main monitor fallback
|
|
355
|
-
"
|
|
356
|
-
|
|
357
|
-
-- Execute Python script to find coordinates
|
|
358
|
-
set coordinates to do shell script "python3 -c " & quoted form of pythonScript
|
|
359
|
-
delay 0.3
|
|
360
|
-
|
|
361
|
-
-- Click at the detected coordinates
|
|
362
|
-
do shell script "/usr/local/bin/cliclick c:" & coordinates
|
|
363
|
-
delay 0.5
|
|
364
|
-
|
|
365
|
-
-- Clean up screenshot
|
|
366
|
-
do shell script "rm " & quoted form of POSIX path of screenshotPath
|
|
367
|
-
|
|
368
|
-
on error
|
|
369
|
-
-- Fallback: Use estimated coordinates if screenshot analysis fails
|
|
370
|
-
try
|
|
371
|
-
-- Click in the main editor area to escape terminal
|
|
372
|
-
do shell script "/usr/local/bin/cliclick c:600,400"
|
|
373
|
-
delay 0.3
|
|
374
|
-
|
|
375
|
-
-- Click in the chat panel area to focus it
|
|
376
|
-
do shell script "/usr/local/bin/cliclick c:1200,500"
|
|
377
|
-
delay 0.3
|
|
378
|
-
|
|
379
|
-
-- Click in the chat input area specifically
|
|
380
|
-
do shell script "/usr/local/bin/cliclick c:1100,700"
|
|
381
|
-
delay 0.3
|
|
382
|
-
|
|
383
|
-
on error
|
|
384
|
-
delay 0.3
|
|
385
|
-
end try
|
|
386
|
-
end try
|
|
387
|
-
|
|
388
|
-
-- Open new chat session with Cmd+T to prevent crashes
|
|
389
|
-
key code 17 using {command down} -- Cmd+T
|
|
390
|
-
delay 1.0
|
|
391
|
-
|
|
392
|
-
-- Type the message using clipboard (more reliable than keystroke)
|
|
393
|
-
set the clipboard to "${text.replace(/"/g, '\\"')}"
|
|
394
|
-
delay 0.5
|
|
395
|
-
key code 9 using {command down} -- Cmd+V to paste
|
|
396
|
-
delay 0.5
|
|
397
|
-
|
|
398
|
-
-- Press Cmd+Enter to send (more reliable than just Enter)
|
|
399
|
-
key code 36 using {command down}
|
|
400
|
-
delay 1
|
|
401
|
-
|
|
402
|
-
return "Message sent via AppleScript"
|
|
403
|
-
end tell
|
|
404
|
-
end tell
|
|
405
|
-
`;
|
|
406
|
-
} else if (ide === 'antigravity') {
|
|
407
|
-
// AppleScript for Google Antigravity - Cmd+Shift+L opens Agent chat (non-toggle)
|
|
408
|
-
appleScript = `
|
|
409
|
-
tell application "System Events"
|
|
410
|
-
tell process "Antigravity"
|
|
411
|
-
set frontmost to true
|
|
412
|
-
delay 1
|
|
413
|
-
|
|
414
|
-
-- Open Agent chat with Cmd+Shift+L (does not toggle)
|
|
415
|
-
key code 37 using {command down, shift down}
|
|
416
|
-
delay 1.5
|
|
417
|
-
|
|
418
|
-
-- Type the message
|
|
419
|
-
keystroke "${text.replace(/"/g, '\\"')}"
|
|
420
|
-
delay 0.5
|
|
421
|
-
|
|
422
|
-
-- Send with Enter
|
|
423
|
-
key code 36
|
|
424
|
-
delay 0.5
|
|
425
|
-
end tell
|
|
426
|
-
end tell
|
|
427
|
-
`;
|
|
428
|
-
} else {
|
|
429
|
-
return {
|
|
430
|
-
success: false,
|
|
431
|
-
error: `Unsupported IDE for AppleScript: ${ide}`,
|
|
432
|
-
note: 'AppleScript is only supported for Cursor, Windsurf, and Antigravity'
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
execSync(`osascript -e '${appleScript}'`, { stdio: 'pipe' });
|
|
437
|
-
|
|
438
|
-
this.logger.log(`Successfully sent message to ${ideName} via AppleScript`);
|
|
439
|
-
return {
|
|
440
|
-
success: true,
|
|
441
|
-
method: 'applescript',
|
|
442
|
-
message: `Message sent to ${ideName}: ${text}`,
|
|
443
|
-
note: 'Message sent via AppleScript automation'
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
} catch (error) {
|
|
447
|
-
this.logger.log('AppleScript interaction failed, falling back to simulated response:', error.message);
|
|
448
|
-
|
|
449
|
-
// Fallback to simulated response if AppleScript fails
|
|
450
|
-
return {
|
|
451
|
-
success: true,
|
|
452
|
-
method: 'simulated',
|
|
453
|
-
message: `Simulated ${ideName} response: ${text}`,
|
|
454
|
-
note: `${ideName} AppleScript automation failed. Using simulated response for testing.`
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Read chat response from an IDE using AppleScript
|
|
461
|
-
* @param {string} ide - The IDE name ('cursor' or 'windsurf')
|
|
462
|
-
* @returns {Promise<string>} The chat response text
|
|
463
|
-
*/
|
|
464
|
-
async readChatResponse(ide) {
|
|
465
|
-
if (ide !== 'windsurf' && ide !== 'cursor') {
|
|
466
|
-
return 'Error: AppleScript reading is only supported for Cursor and Windsurf';
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
this.logger.log(`${ide === 'windsurf' ? 'Windsurf' : 'Cursor'} detected - using AppleScript to read chat response`);
|
|
470
|
-
|
|
471
|
-
try {
|
|
472
|
-
// For Cursor, try CDP first if available
|
|
473
|
-
if (ide === 'cursor') {
|
|
474
|
-
try {
|
|
475
|
-
this.logger.log('🔧 Attempting CDP method for Cursor...');
|
|
476
|
-
const cdpResponse = await this.readCursorResponseViaCDP();
|
|
477
|
-
if (cdpResponse && cdpResponse.length > 20) {
|
|
478
|
-
this.logger.log('✅ Successfully read Cursor response via CDP');
|
|
479
|
-
return cdpResponse;
|
|
480
|
-
}
|
|
481
|
-
} catch (error) {
|
|
482
|
-
this.logger.log('⚠️ CDP method failed, falling back to AppleScript:', error.message);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Try clipboard-based response detection
|
|
487
|
-
if (ide === 'cursor') {
|
|
488
|
-
try {
|
|
489
|
-
this.logger.log('🔧 Attempting clipboard-based response detection...');
|
|
490
|
-
const clipboardResponse = await this.readCursorResponseViaClipboard();
|
|
491
|
-
if (clipboardResponse && clipboardResponse.length > 20) {
|
|
492
|
-
this.logger.log('✅ Successfully read Cursor response via clipboard');
|
|
493
|
-
return clipboardResponse;
|
|
494
|
-
}
|
|
495
|
-
} catch (error) {
|
|
496
|
-
this.logger.log('⚠️ Clipboard method failed, falling back to AppleScript:', error.message);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
} catch (error) {
|
|
501
|
-
this.logger.log('⚠️ Fallback methods failed:', error.message);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
try {
|
|
505
|
-
// AppleScript to read the chat response from IDE
|
|
506
|
-
const appleScript = `
|
|
507
|
-
tell application "System Events"
|
|
508
|
-
tell process "${ide === 'windsurf' ? 'Windsurf' : 'Cursor'}"
|
|
509
|
-
set frontmost to true
|
|
510
|
-
delay 1
|
|
511
|
-
|
|
512
|
-
${ide === 'cursor' ? `
|
|
513
|
-
-- Cursor-specific methods
|
|
514
|
-
-- Method 1: Try to get text from web view content (most likely location)
|
|
515
|
-
try
|
|
516
|
-
set webViewGroups to group of window 1
|
|
517
|
-
set responseText to ""
|
|
518
|
-
repeat with grp in webViewGroups
|
|
519
|
-
try
|
|
520
|
-
-- Look for text areas within groups (web view content)
|
|
521
|
-
set textAreas to text area of grp
|
|
522
|
-
repeat with txtArea in textAreas
|
|
523
|
-
try
|
|
524
|
-
set textValue to value of txtArea
|
|
525
|
-
if textValue is not "" and textValue is not missing value then
|
|
526
|
-
set responseText to responseText & textValue & "\\n"
|
|
527
|
-
end if
|
|
528
|
-
on error
|
|
529
|
-
-- Continue to next text area
|
|
530
|
-
end try
|
|
531
|
-
end repeat
|
|
532
|
-
|
|
533
|
-
-- Look for static text within groups
|
|
534
|
-
set staticTexts to static text of grp
|
|
535
|
-
repeat with txt in staticTexts
|
|
536
|
-
try
|
|
537
|
-
set textValue to value of txt
|
|
538
|
-
if textValue is not "" and textValue is not missing value then
|
|
539
|
-
set responseText to responseText & textValue & "\\n"
|
|
540
|
-
end if
|
|
541
|
-
on error
|
|
542
|
-
-- Continue to next static text
|
|
543
|
-
end try
|
|
544
|
-
end repeat
|
|
545
|
-
on error
|
|
546
|
-
-- Continue to next group
|
|
547
|
-
end try
|
|
548
|
-
end repeat
|
|
549
|
-
if responseText is not "" then
|
|
550
|
-
return responseText
|
|
551
|
-
end if
|
|
552
|
-
on error
|
|
553
|
-
-- Continue to next method
|
|
554
|
-
end try
|
|
555
|
-
|
|
556
|
-
-- Method 2: Try to get all static text from the window
|
|
557
|
-
try
|
|
558
|
-
set allText to value of static text of window 1
|
|
559
|
-
if allText is not "" then
|
|
560
|
-
return allText
|
|
561
|
-
end if
|
|
562
|
-
on error
|
|
563
|
-
-- Continue to next method
|
|
564
|
-
end try
|
|
565
|
-
|
|
566
|
-
-- Method 3: Try to get text from scroll areas
|
|
567
|
-
try
|
|
568
|
-
set scrollAreas to scroll area of window 1
|
|
569
|
-
set responseText to ""
|
|
570
|
-
repeat with scrollArea in scrollAreas
|
|
571
|
-
try
|
|
572
|
-
set scrollText to value of static text of scrollArea
|
|
573
|
-
if scrollText is not "" then
|
|
574
|
-
set responseText to responseText & scrollText & "\\n"
|
|
575
|
-
end if
|
|
576
|
-
on error
|
|
577
|
-
-- Continue to next scroll area
|
|
578
|
-
end try
|
|
579
|
-
end repeat
|
|
580
|
-
if responseText is not "" then
|
|
581
|
-
return responseText
|
|
582
|
-
end if
|
|
583
|
-
on error
|
|
584
|
-
-- Continue to next method
|
|
585
|
-
end try
|
|
586
|
-
|
|
587
|
-
-- Method 4: Try accessibility API approach
|
|
588
|
-
try
|
|
589
|
-
set uiElements to every UI element of window 1
|
|
590
|
-
set responseText to ""
|
|
591
|
-
repeat with element in uiElements
|
|
592
|
-
try
|
|
593
|
-
set elementText to value of element
|
|
594
|
-
if elementText is not "" and elementText is not missing value then
|
|
595
|
-
set responseText to responseText & elementText & "\\n"
|
|
596
|
-
end if
|
|
597
|
-
on error
|
|
598
|
-
-- Continue to next element
|
|
599
|
-
end try
|
|
600
|
-
end repeat
|
|
601
|
-
if responseText is not "" then
|
|
602
|
-
return responseText
|
|
603
|
-
end if
|
|
604
|
-
on error
|
|
605
|
-
-- Continue to next method
|
|
606
|
-
end try
|
|
607
|
-
|
|
608
|
-
-- Method 5: Return diagnostic information
|
|
609
|
-
return "DEBUG: Window count: " & count of windows & "\\nText areas found: " & count of text area of window 1 & "\\nStatic text found: " & value of static text of window 1 & "\\n\\n\\nRESPONSE: "
|
|
610
|
-
` : `
|
|
611
|
-
-- Windsurf-specific methods
|
|
612
|
-
-- Method 1: Try to get all static text from the window
|
|
613
|
-
try
|
|
614
|
-
set allText to value of static text of window 1
|
|
615
|
-
if allText is not "" then
|
|
616
|
-
return allText
|
|
617
|
-
end if
|
|
618
|
-
on error
|
|
619
|
-
-- Continue to next method
|
|
620
|
-
end try
|
|
621
|
-
|
|
622
|
-
-- Method 2: Try to get text from groups
|
|
623
|
-
try
|
|
624
|
-
set groups to group of window 1
|
|
625
|
-
set responseText to ""
|
|
626
|
-
repeat with grp in groups
|
|
627
|
-
try
|
|
628
|
-
set groupText to value of static text of grp
|
|
629
|
-
if groupText is not "" then
|
|
630
|
-
set responseText to responseText & groupText & "\\n"
|
|
631
|
-
end if
|
|
632
|
-
on error
|
|
633
|
-
-- Continue to next group
|
|
634
|
-
end try
|
|
635
|
-
end repeat
|
|
636
|
-
if responseText is not "" then
|
|
637
|
-
return responseText
|
|
638
|
-
end if
|
|
639
|
-
on error
|
|
640
|
-
-- Continue to next method
|
|
641
|
-
end try
|
|
642
|
-
|
|
643
|
-
-- Method 3: Try clipboard approach
|
|
644
|
-
try
|
|
645
|
-
key code 0 using {command down}
|
|
646
|
-
delay 0.5
|
|
647
|
-
key code 8 using {command down}
|
|
648
|
-
delay 0.5
|
|
649
|
-
set clipboardText to (the clipboard)
|
|
650
|
-
if clipboardText is not "" then
|
|
651
|
-
return clipboardText
|
|
652
|
-
end if
|
|
653
|
-
on error
|
|
654
|
-
-- Continue to next method
|
|
655
|
-
end try
|
|
656
|
-
|
|
657
|
-
-- Method 4: Return placeholder
|
|
658
|
-
return "No chat content found in Windsurf"
|
|
659
|
-
`}
|
|
660
|
-
end tell
|
|
661
|
-
end tell
|
|
662
|
-
`;
|
|
663
|
-
|
|
664
|
-
const result = execSync(`osascript -e '${appleScript}'`, { stdio: 'pipe', encoding: 'utf8' });
|
|
665
|
-
this.logger.log('Successfully read response via AppleScript');
|
|
666
|
-
this.logger.log('📖 AppleScript result:', result);
|
|
667
|
-
this.logger.log('📏 Result length:', result.length);
|
|
668
|
-
this.logger.log('🔍 Result preview:', result.substring(0, 200));
|
|
669
|
-
|
|
670
|
-
// Check if the result is just diagnostic information
|
|
671
|
-
const isDiagnosticResult = result.includes('DEBUG:') ||
|
|
672
|
-
result.includes('diagnostic') ||
|
|
673
|
-
result.includes('No chat content found') ||
|
|
674
|
-
result.includes('Text areas found: 0') ||
|
|
675
|
-
result.includes('Static text found:') ||
|
|
676
|
-
result.includes('Window count:') ||
|
|
677
|
-
result.includes('Sample text:') ||
|
|
678
|
-
result.includes('Could not read') ||
|
|
679
|
-
result.includes('No readable text content detected');
|
|
680
|
-
|
|
681
|
-
this.logger.log(`🔍 Diagnostic detection check: isDiagnosticResult = ${isDiagnosticResult}`);
|
|
682
|
-
this.logger.log(`🔍 Result contains 'DEBUG:': ${result.includes('DEBUG:')}`);
|
|
683
|
-
this.logger.log(`🔍 Result contains 'Window count:': ${result.includes('Window count:')}`);
|
|
684
|
-
|
|
685
|
-
// For Cursor, always try CDP and clipboard methods as fallbacks
|
|
686
|
-
if (ide === 'cursor') {
|
|
687
|
-
this.logger.log('🔧 For Cursor, always trying CDP and clipboard methods as fallbacks...');
|
|
688
|
-
|
|
689
|
-
// Try CDP method
|
|
690
|
-
try {
|
|
691
|
-
this.logger.log('🔧 Attempting CDP method for Cursor...');
|
|
692
|
-
const cdpResponse = await this.readCursorResponseViaCDP();
|
|
693
|
-
if (cdpResponse && cdpResponse.length > 20) {
|
|
694
|
-
this.logger.log('✅ Successfully read Cursor response via CDP');
|
|
695
|
-
return cdpResponse;
|
|
696
|
-
}
|
|
697
|
-
} catch (error) {
|
|
698
|
-
this.logger.log('⚠️ CDP method failed:', error.message);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// Try clipboard method
|
|
702
|
-
try {
|
|
703
|
-
this.logger.log('🔧 Attempting clipboard-based response detection...');
|
|
704
|
-
const clipboardResponse = await this.readCursorResponseViaClipboard();
|
|
705
|
-
if (clipboardResponse && clipboardResponse.length > 20) {
|
|
706
|
-
this.logger.log('✅ Successfully read Cursor response via clipboard');
|
|
707
|
-
return clipboardResponse;
|
|
708
|
-
}
|
|
709
|
-
} catch (error) {
|
|
710
|
-
this.logger.log('⚠️ Clipboard method failed:', error.message);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
if (isDiagnosticResult) {
|
|
715
|
-
this.logger.log('⚠️ AppleScript returned diagnostic info, all methods failed...');
|
|
716
|
-
return `Unable to read ${ide} response - all methods failed. AppleScript diagnostic: ${result.substring(0, 200)}...`;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
return result.trim();
|
|
720
|
-
|
|
721
|
-
} catch (error) {
|
|
722
|
-
this.logger.log('AppleScript reading failed:', error.message);
|
|
723
|
-
return 'Error: Could not read Windsurf response via AppleScript';
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/**
|
|
728
|
-
* Detect quota warnings using AppleScript
|
|
729
|
-
* @param {string} ide - The IDE name ('windsurf')
|
|
730
|
-
* @returns {Promise<Object>} Quota detection result
|
|
731
|
-
*/
|
|
732
|
-
async detectQuotaWarning(ide) {
|
|
733
|
-
if (ide !== 'windsurf') {
|
|
734
|
-
return {
|
|
735
|
-
hasQuotaWarning: false,
|
|
736
|
-
note: 'AppleScript quota detection is only supported for Windsurf'
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
this.logger.log('🔍 === APPLESCRIPT DETECTION START ===');
|
|
741
|
-
this.logger.log('🔍 Starting enhanced AppleScript quota detection...');
|
|
742
|
-
|
|
743
|
-
try {
|
|
744
|
-
// Test basic AppleScript execution first
|
|
745
|
-
this.logger.log('🔍 Testing basic AppleScript execution...');
|
|
746
|
-
const basicTest = execSync('osascript -e "return \\"test\\""', { stdio: 'pipe', encoding: 'utf8' });
|
|
747
|
-
this.logger.log('✅ Basic AppleScript test passed:', basicTest.trim());
|
|
748
|
-
} catch (basicError) {
|
|
749
|
-
this.logger.log('❌ Basic AppleScript test failed:', basicError.message);
|
|
750
|
-
return {
|
|
751
|
-
hasQuotaWarning: false,
|
|
752
|
-
error: 'AppleScript not available',
|
|
753
|
-
note: 'Basic AppleScript test failed'
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Try a much simpler AppleScript first
|
|
758
|
-
this.logger.log('🔍 Trying simple AppleScript approach...');
|
|
759
|
-
const simpleAppleScript = `
|
|
760
|
-
tell application "System Events"
|
|
761
|
-
tell process "Windsurf"
|
|
762
|
-
set frontmost to true
|
|
763
|
-
delay 1
|
|
764
|
-
|
|
765
|
-
-- Method 1: Look for static text containing quota warnings
|
|
766
|
-
try
|
|
767
|
-
set allText to value of static text of window 1
|
|
768
|
-
if allText contains "Not enough credits" or allText contains "Upgrade to a paid plan" or allText contains "switch to SWE-1-lite" then
|
|
769
|
-
return "QUOTA_WARNING_DETECTED: " & allText
|
|
770
|
-
end if
|
|
771
|
-
on error
|
|
772
|
-
-- Continue to next method
|
|
773
|
-
end try
|
|
774
|
-
|
|
775
|
-
-- Method 2: Look for disabled text areas (quota warnings often disable chat input)
|
|
776
|
-
try
|
|
777
|
-
set textAreas to text area of window 1
|
|
778
|
-
repeat with textArea in textAreas
|
|
779
|
-
try
|
|
780
|
-
if not enabled of textArea then
|
|
781
|
-
return "QUOTA_INDICATOR_DETECTED: Disabled text area found"
|
|
782
|
-
end if
|
|
783
|
-
on error
|
|
784
|
-
-- Continue to next text area
|
|
785
|
-
end try
|
|
786
|
-
end repeat
|
|
787
|
-
on error
|
|
788
|
-
-- Continue to next method
|
|
789
|
-
end try
|
|
790
|
-
|
|
791
|
-
-- Method 3: Look for specific UI elements that indicate quota issues
|
|
792
|
-
try
|
|
793
|
-
set buttons to button of window 1
|
|
794
|
-
repeat with btn in buttons
|
|
795
|
-
try
|
|
796
|
-
set buttonText to title of btn
|
|
797
|
-
if buttonText contains "Upgrade" or buttonText contains "credits" or buttonText contains "quota" then
|
|
798
|
-
return "QUOTA_UI_DETECTED: " & buttonText
|
|
799
|
-
end if
|
|
800
|
-
on error
|
|
801
|
-
-- Continue to next button
|
|
802
|
-
end try
|
|
803
|
-
end repeat
|
|
804
|
-
on error
|
|
805
|
-
-- Continue to next method
|
|
806
|
-
end try
|
|
807
|
-
|
|
808
|
-
-- Method 4: Look for groups that might contain quota information
|
|
809
|
-
try
|
|
810
|
-
set groups to group of window 1
|
|
811
|
-
repeat with grp in groups
|
|
812
|
-
try
|
|
813
|
-
set groupText to value of static text of grp
|
|
814
|
-
if groupText contains "credits" or groupText contains "quota" or groupText contains "upgrade" then
|
|
815
|
-
return "QUOTA_GROUP_DETECTED: " & groupText
|
|
816
|
-
end if
|
|
817
|
-
on error
|
|
818
|
-
-- Continue to next group
|
|
819
|
-
end try
|
|
820
|
-
end repeat
|
|
821
|
-
on error
|
|
822
|
-
-- Continue to next method
|
|
823
|
-
end try
|
|
824
|
-
|
|
825
|
-
return "NO_QUOTA_WARNING_DETECTED"
|
|
826
|
-
end tell
|
|
827
|
-
end tell
|
|
828
|
-
`;
|
|
829
|
-
|
|
830
|
-
this.logger.log('🔍 Simple AppleScript length:', simpleAppleScript.length);
|
|
831
|
-
this.logger.log('🔍 Simple AppleScript preview:', simpleAppleScript.substring(0, 200) + '...');
|
|
832
|
-
|
|
833
|
-
try {
|
|
834
|
-
this.logger.log('🔍 Executing simple AppleScript...');
|
|
835
|
-
this.logger.log('🔍 AppleScript command length:', simpleAppleScript.length);
|
|
836
|
-
|
|
837
|
-
const result = execSync(`osascript -e '${simpleAppleScript}'`, { stdio: 'pipe', encoding: 'utf8' });
|
|
838
|
-
this.logger.log('🔍 AppleScript execution completed');
|
|
839
|
-
this.logger.log('🔍 AppleScript result length:', result.length);
|
|
840
|
-
this.logger.log('🔍 AppleScript result:', result.trim());
|
|
841
|
-
this.logger.log('🔍 AppleScript result starts with:', result.trim().substring(0, 100));
|
|
842
|
-
|
|
843
|
-
// Check for quota warnings in the result
|
|
844
|
-
if (result.includes('QUOTA_WARNING_DETECTED')) {
|
|
845
|
-
this.logger.log('❌ Windsurf quota warning detected via enhanced AppleScript');
|
|
846
|
-
return {
|
|
847
|
-
hasQuotaWarning: true,
|
|
848
|
-
method: 'applescript-static-text',
|
|
849
|
-
note: 'Windsurf quota warning detected via enhanced AppleScript detection'
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
if (result.includes('QUOTA_INDICATOR_DETECTED')) {
|
|
854
|
-
this.logger.log('❌ Windsurf quota indicator detected via enhanced AppleScript');
|
|
855
|
-
return {
|
|
856
|
-
hasQuotaWarning: true,
|
|
857
|
-
method: 'applescript-disabled-input',
|
|
858
|
-
note: 'Windsurf quota indicator detected via enhanced AppleScript (disabled chat input)'
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
if (result.includes('QUOTA_UI_DETECTED') || result.includes('QUOTA_GROUP_DETECTED')) {
|
|
863
|
-
this.logger.log('⚠️ Windsurf potential quota text detected via enhanced AppleScript');
|
|
864
|
-
return {
|
|
865
|
-
hasQuotaWarning: true,
|
|
866
|
-
method: 'applescript-ui-elements',
|
|
867
|
-
note: 'Windsurf potential quota text detected via enhanced AppleScript'
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// Additional pattern matching for quota-related text
|
|
872
|
-
const quotaPatterns = [
|
|
873
|
-
'not enough credits',
|
|
874
|
-
'upgrade to a paid plan',
|
|
875
|
-
'switch to swe-1-lite',
|
|
876
|
-
'quota exceeded',
|
|
877
|
-
'credits exhausted',
|
|
878
|
-
'payment required'
|
|
879
|
-
];
|
|
880
|
-
|
|
881
|
-
const lowerResult = result.toLowerCase();
|
|
882
|
-
for (const pattern of quotaPatterns) {
|
|
883
|
-
if (lowerResult.includes(pattern)) {
|
|
884
|
-
this.logger.log('❌ Windsurf quota warning detected via text pattern matching');
|
|
885
|
-
return {
|
|
886
|
-
hasQuotaWarning: true,
|
|
887
|
-
method: 'applescript-pattern-matching',
|
|
888
|
-
pattern: pattern,
|
|
889
|
-
note: 'Windsurf quota warning detected via text pattern matching in AppleScript result'
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
this.logger.log('🔍 === APPLESCRIPT DETECTION END ===');
|
|
895
|
-
this.logger.log('🔍 Since AppleScript cannot access web view, trying alternative methods...');
|
|
896
|
-
|
|
897
|
-
// Since AppleScript cannot access web view content, return a note about the limitation
|
|
898
|
-
return {
|
|
899
|
-
hasQuotaWarning: false,
|
|
900
|
-
method: 'applescript-limited',
|
|
901
|
-
note: 'Windsurf quota warning is visible in UI but not detectable via AppleScript'
|
|
902
|
-
};
|
|
903
|
-
|
|
904
|
-
} catch (error) {
|
|
905
|
-
this.logger.log('❌ AppleScript error:', error.message);
|
|
906
|
-
return {
|
|
907
|
-
hasQuotaWarning: false,
|
|
908
|
-
error: error.message,
|
|
909
|
-
note: 'AppleScript cannot access web view content - Windsurf does not support CDP'
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
/**
|
|
915
|
-
* Read Cursor response via Chrome DevTools Protocol (CDP)
|
|
916
|
-
* @returns {Promise<string>} The chat response text
|
|
917
|
-
*/
|
|
918
|
-
async readCursorResponseViaCDP() {
|
|
919
|
-
try {
|
|
920
|
-
this.logger.log('🔧 Attempting CDP connection to Cursor on port 9225...');
|
|
921
|
-
|
|
922
|
-
// Import CDP dynamically to avoid issues in VSCode extension context
|
|
923
|
-
const CDP = require('chrome-remote-interface');
|
|
924
|
-
|
|
925
|
-
// List available targets
|
|
926
|
-
const targets = await CDP.List({ port: 9225 });
|
|
927
|
-
this.logger.log(`🔧 Found ${targets.length} CDP targets`);
|
|
928
|
-
|
|
929
|
-
// Find the main workbench target (not settings)
|
|
930
|
-
const workbench = targets.find(t =>
|
|
931
|
-
t.title !== 'Cursor Settings' &&
|
|
932
|
-
t.type === 'page' &&
|
|
933
|
-
t.url && t.url.includes('workbench')
|
|
934
|
-
) || targets[0];
|
|
935
|
-
|
|
936
|
-
if (!workbench) {
|
|
937
|
-
throw new Error('No suitable Cursor workbench target found');
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
this.logger.log(`🔧 Connecting to target: ${workbench.title}`);
|
|
941
|
-
|
|
942
|
-
// Connect to the target
|
|
943
|
-
const client = await CDP({ port: 9225, target: workbench });
|
|
944
|
-
const { Runtime, DOM } = client;
|
|
945
|
-
|
|
946
|
-
// Enable required domains
|
|
947
|
-
await Runtime.enable();
|
|
948
|
-
await DOM.enable();
|
|
949
|
-
|
|
950
|
-
this.logger.log('🔧 CDP connection established, searching for chat content...');
|
|
951
|
-
|
|
952
|
-
// Try multiple selectors to find chat content
|
|
953
|
-
const selectors = [
|
|
954
|
-
'.chat-content',
|
|
955
|
-
'.chat-messages',
|
|
956
|
-
'.response-content',
|
|
957
|
-
'.ai-response',
|
|
958
|
-
'.message-content',
|
|
959
|
-
'[data-testid="chat-message"]',
|
|
960
|
-
'.markdown-content',
|
|
961
|
-
'.prose',
|
|
962
|
-
'.chat-panel',
|
|
963
|
-
'.sidebar-panel'
|
|
964
|
-
];
|
|
965
|
-
|
|
966
|
-
for (const selector of selectors) {
|
|
967
|
-
try {
|
|
968
|
-
this.logger.log(`🔧 Trying selector: ${selector}`);
|
|
969
|
-
|
|
970
|
-
// Get document root
|
|
971
|
-
const { root } = await DOM.getDocument();
|
|
972
|
-
|
|
973
|
-
// Search for elements with the selector
|
|
974
|
-
const { nodeIds } = await DOM.querySelectorAll({ nodeId: root.nodeId, selector });
|
|
975
|
-
|
|
976
|
-
if (nodeIds.length > 0) {
|
|
977
|
-
this.logger.log(`🔧 Found ${nodeIds.length} elements with selector: ${selector}`);
|
|
978
|
-
|
|
979
|
-
// Get text content from the first matching element
|
|
980
|
-
const { nodeId } = await DOM.describeNode({ nodeId: nodeIds[0] });
|
|
981
|
-
const { outerHTML } = await DOM.getOuterHTML({ nodeId });
|
|
982
|
-
|
|
983
|
-
// Extract text content (remove HTML tags)
|
|
984
|
-
const textContent = outerHTML.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
985
|
-
|
|
986
|
-
if (textContent && textContent.length > 20) {
|
|
987
|
-
this.logger.log(`✅ Successfully extracted chat content via CDP: ${textContent.substring(0, 100)}...`);
|
|
988
|
-
await client.close();
|
|
989
|
-
return textContent;
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
} catch (error) {
|
|
993
|
-
this.logger.log(`⚠️ Selector ${selector} failed: ${error.message}`);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// If no specific selectors work, try to get all text content
|
|
998
|
-
this.logger.log('🔧 Trying to get all text content from the page...');
|
|
999
|
-
|
|
1000
|
-
const { root } = await DOM.getDocument();
|
|
1001
|
-
const { outerHTML } = await DOM.getOuterHTML({ nodeId: root.nodeId });
|
|
1002
|
-
|
|
1003
|
-
// Extract text content and look for AI responses
|
|
1004
|
-
const textContent = outerHTML.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
1005
|
-
|
|
1006
|
-
// Look for patterns that indicate AI responses
|
|
1007
|
-
const aiResponsePatterns = [
|
|
1008
|
-
/(?:AI|Assistant|Claude|GPT|Response):\s*([^]*?)(?=\n\n|\n[A-Z]|$)/gi,
|
|
1009
|
-
/(?:Here|This|The answer|The solution):\s*([^]*?)(?=\n\n|\n[A-Z]|$)/gi
|
|
1010
|
-
];
|
|
1011
|
-
|
|
1012
|
-
for (const pattern of aiResponsePatterns) {
|
|
1013
|
-
const matches = textContent.match(pattern);
|
|
1014
|
-
if (matches && matches.length > 0) {
|
|
1015
|
-
const response = matches[matches.length - 1]; // Get the latest response
|
|
1016
|
-
if (response && response.length > 20) {
|
|
1017
|
-
this.logger.log(`✅ Found AI response via CDP pattern matching: ${response.substring(0, 100)}...`);
|
|
1018
|
-
await client.close();
|
|
1019
|
-
return response;
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
await client.close();
|
|
1025
|
-
throw new Error('No chat content found via CDP');
|
|
1026
|
-
|
|
1027
|
-
} catch (error) {
|
|
1028
|
-
this.logger.log(`❌ CDP method failed: ${error.message}`);
|
|
1029
|
-
throw error;
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
/**
|
|
1034
|
-
* Read Cursor response via clipboard (select all and copy)
|
|
1035
|
-
* @returns {Promise<string>} The chat response text
|
|
1036
|
-
*/
|
|
1037
|
-
async readCursorResponseViaClipboard() {
|
|
1038
|
-
try {
|
|
1039
|
-
this.logger.log('🔧 Attempting clipboard-based response detection...');
|
|
1040
|
-
|
|
1041
|
-
// AppleScript to select all content in Cursor and copy to clipboard
|
|
1042
|
-
const clipboardScript = `
|
|
1043
|
-
tell application "System Events"
|
|
1044
|
-
tell process "Cursor"
|
|
1045
|
-
set frontmost to true
|
|
1046
|
-
delay 1
|
|
1047
|
-
|
|
1048
|
-
-- Try to focus on the chat area first
|
|
1049
|
-
try
|
|
1050
|
-
key code 1 using {command down} -- Cmd+A to select all
|
|
1051
|
-
delay 0.5
|
|
1052
|
-
key code 8 using {command down} -- Cmd+C to copy
|
|
1053
|
-
delay 0.5
|
|
1054
|
-
on error
|
|
1055
|
-
-- If that fails, try alternative approach
|
|
1056
|
-
key code 1 using {command down, shift down} -- Cmd+Shift+A to select all
|
|
1057
|
-
delay 0.5
|
|
1058
|
-
key code 8 using {command down} -- Cmd+C to copy
|
|
1059
|
-
delay 0.5
|
|
1060
|
-
end try
|
|
1061
|
-
end tell
|
|
1062
|
-
end tell
|
|
1063
|
-
`;
|
|
1064
|
-
|
|
1065
|
-
execSync(`osascript -e '${clipboardScript}'`, { stdio: 'pipe' });
|
|
1066
|
-
|
|
1067
|
-
// Read from clipboard
|
|
1068
|
-
const clipboardScript2 = `
|
|
1069
|
-
the clipboard
|
|
1070
|
-
`;
|
|
1071
|
-
|
|
1072
|
-
const clipboardContent = execSync(`osascript -e '${clipboardScript2}'`, { stdio: 'pipe', encoding: 'utf8' });
|
|
1073
|
-
|
|
1074
|
-
if (clipboardContent && clipboardContent.trim().length > 20) {
|
|
1075
|
-
this.logger.log(`✅ Successfully read clipboard content: ${clipboardContent.substring(0, 100)}...`);
|
|
1076
|
-
return clipboardContent.trim();
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
throw new Error('Clipboard content is empty or too short');
|
|
1080
|
-
|
|
1081
|
-
} catch (error) {
|
|
1082
|
-
this.logger.log(`❌ Clipboard method failed: ${error.message}`);
|
|
1083
|
-
throw error;
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
module.exports = { AppleScriptManager };
|
|
1
|
+
// @vibecodingmachine/core - AppleScript Manager (CommonJS)
|
|
2
|
+
// Handles AppleScript-based interactions with IDEs that don't support CDP
|
|
3
|
+
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AppleScript Manager for IDE interactions
|
|
8
|
+
* Handles AppleScript-based text sending and response reading for IDEs like Cursor and Windsurf
|
|
9
|
+
*/
|
|
10
|
+
class AppleScriptManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.logger = console;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Open Cursor IDE
|
|
17
|
+
* @returns {Promise<Object>} Result object with success status and details
|
|
18
|
+
*/
|
|
19
|
+
async openCursor() {
|
|
20
|
+
try {
|
|
21
|
+
const appleScript = `
|
|
22
|
+
tell application "Cursor"
|
|
23
|
+
activate
|
|
24
|
+
end tell
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
execSync(`osascript -e '${appleScript}'`, { encoding: 'utf8' });
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
message: 'Cursor opened successfully'
|
|
32
|
+
};
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
error: `Failed to open Cursor: ${error.message}`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Open Windsurf IDE
|
|
43
|
+
* @returns {Promise<Object>} Result object with success status and details
|
|
44
|
+
*/
|
|
45
|
+
async openWindsurf() {
|
|
46
|
+
try {
|
|
47
|
+
// Use multiple separate AppleScript calls for better reliability
|
|
48
|
+
try {
|
|
49
|
+
// First, try to launch
|
|
50
|
+
execSync('osascript -e \'tell application "Windsurf" to launch\'', { encoding: 'utf8', timeout: 5000 });
|
|
51
|
+
} catch (launchError) {
|
|
52
|
+
// If launch fails, it might already be running
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Wait a moment
|
|
56
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
57
|
+
|
|
58
|
+
// Then activate
|
|
59
|
+
execSync('osascript -e \'tell application "Windsurf" to activate\'', { encoding: 'utf8', timeout: 5000 });
|
|
60
|
+
|
|
61
|
+
// Wait a moment
|
|
62
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
63
|
+
|
|
64
|
+
// Finally, ensure it's frontmost
|
|
65
|
+
try {
|
|
66
|
+
execSync('osascript -e \'tell application "System Events" to tell process "Windsurf" to set frontmost to true\'', { encoding: 'utf8', timeout: 5000 });
|
|
67
|
+
} catch (frontmostError) {
|
|
68
|
+
// If setting frontmost fails, that's okay - the app is still activated
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
message: 'Windsurf launched and activated successfully'
|
|
74
|
+
};
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: `Failed to open Windsurf: ${error.message}`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async openAntigravity(repoPath = null) {
|
|
84
|
+
try {
|
|
85
|
+
this.logger.log('Opening Google Antigravity...');
|
|
86
|
+
|
|
87
|
+
let command = `open -a "Antigravity"`;
|
|
88
|
+
if (repoPath) {
|
|
89
|
+
command += ` "${repoPath}"`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
execSync(command, { encoding: 'utf8', timeout: 5000 });
|
|
93
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
success: true,
|
|
97
|
+
message: repoPath ? `Google Antigravity opened with repository: ${repoPath}` : 'Google Antigravity opened successfully'
|
|
98
|
+
};
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
error: `Failed to open Google Antigravity: ${error.message}`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Handle Antigravity quota limit by automatically switching models
|
|
109
|
+
* @returns {Promise<{success: boolean, model?: string, error?: string}>}
|
|
110
|
+
*/
|
|
111
|
+
async handleAntigravityQuotaLimit() {
|
|
112
|
+
try {
|
|
113
|
+
this.logger.log('Attempting to handle Antigravity quota limit...');
|
|
114
|
+
|
|
115
|
+
const script = `
|
|
116
|
+
tell application "System Events"
|
|
117
|
+
tell process "Antigravity"
|
|
118
|
+
set frontmost to true
|
|
119
|
+
delay 0.8
|
|
120
|
+
|
|
121
|
+
-- Try to find and click "Select another model" button
|
|
122
|
+
try
|
|
123
|
+
set modelButtons to buttons of window 1 whose name contains "Select another model"
|
|
124
|
+
if (count of modelButtons) > 0 then
|
|
125
|
+
click item 1 of modelButtons
|
|
126
|
+
delay 1.5
|
|
127
|
+
|
|
128
|
+
-- Look for model dropdown/menu items
|
|
129
|
+
-- Try alternative models in order of preference
|
|
130
|
+
set modelNames to {"Gemini 3 Pro (Low)", "Claude Sonnet 4.5", "Claude Sonnet 4.5 (Thinking)", "GPT-OSS 120B (Medium)"}
|
|
131
|
+
|
|
132
|
+
repeat with modelName in modelNames
|
|
133
|
+
try
|
|
134
|
+
-- Try to find and click this model option
|
|
135
|
+
set modelItems to menu items of menu 1 of window 1 whose name is modelName
|
|
136
|
+
if (count of modelItems) > 0 then
|
|
137
|
+
click item 1 of modelItems
|
|
138
|
+
delay 0.8
|
|
139
|
+
|
|
140
|
+
-- Click "Accept all" or similar confirmation button
|
|
141
|
+
try
|
|
142
|
+
set acceptButtons to buttons of window 1 whose name contains "Accept"
|
|
143
|
+
if (count of acceptButtons) > 0 then
|
|
144
|
+
click item 1 of acceptButtons
|
|
145
|
+
delay 0.5
|
|
146
|
+
return "success:" & modelName
|
|
147
|
+
end if
|
|
148
|
+
end try
|
|
149
|
+
|
|
150
|
+
return "success:" & modelName
|
|
151
|
+
end if
|
|
152
|
+
end try
|
|
153
|
+
end repeat
|
|
154
|
+
|
|
155
|
+
return "error:no-models-available"
|
|
156
|
+
else
|
|
157
|
+
return "error:button-not-found"
|
|
158
|
+
end if
|
|
159
|
+
on error errMsg
|
|
160
|
+
return "error:" & errMsg
|
|
161
|
+
end try
|
|
162
|
+
end tell
|
|
163
|
+
end tell
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
const result = execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, {
|
|
167
|
+
encoding: 'utf8',
|
|
168
|
+
timeout: 15000
|
|
169
|
+
}).trim();
|
|
170
|
+
|
|
171
|
+
this.logger.log('AppleScript result:', result);
|
|
172
|
+
|
|
173
|
+
if (result.startsWith('success:')) {
|
|
174
|
+
const model = result.substring(8);
|
|
175
|
+
this.logger.log(`Successfully switched to model: ${model}`);
|
|
176
|
+
return { success: true, model };
|
|
177
|
+
} else {
|
|
178
|
+
const error = result.substring(6);
|
|
179
|
+
this.logger.log(`Failed to switch model: ${error}`);
|
|
180
|
+
return { success: false, error };
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
this.logger.log('Error handling Antigravity quota limit:', error.message);
|
|
184
|
+
return { success: false, error: error.message };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async openIDE(ide, repoPath = null) {
|
|
189
|
+
const ideLower = (ide || '').toLowerCase();
|
|
190
|
+
|
|
191
|
+
switch (ideLower) {
|
|
192
|
+
case 'cursor':
|
|
193
|
+
return await this.openCursor(repoPath);
|
|
194
|
+
case 'windsurf':
|
|
195
|
+
return await this.openWindsurf(repoPath);
|
|
196
|
+
case 'antigravity':
|
|
197
|
+
return await this.openAntigravity(repoPath);
|
|
198
|
+
default:
|
|
199
|
+
return {
|
|
200
|
+
success: false,
|
|
201
|
+
error: `Unknown IDE: ${ide}. Supported: cursor, windsurf, antigravity`
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Send text to an IDE using AppleScript
|
|
208
|
+
* @param {string} text - The text to send
|
|
209
|
+
* @param {string} ide - The IDE name ('cursor', 'windsurf', 'antigravity')
|
|
210
|
+
* @returns {Promise<Object>} Result object with success status and details
|
|
211
|
+
*/
|
|
212
|
+
async sendText(text, ide) {
|
|
213
|
+
if (typeof text !== 'string') {
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
error: `Invalid text type: ${typeof text}. Expected string.`,
|
|
217
|
+
debug: { textType: typeof text, textValue: text }
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const ideName = ide === 'windsurf' ? 'Windsurf' : ide === 'cursor' ? 'Cursor' : ide === 'antigravity' ? 'Antigravity' : 'Unknown IDE';
|
|
222
|
+
|
|
223
|
+
this.logger.log(`${ideName} detected - using AppleScript for reliable text sending`);
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
let appleScript;
|
|
227
|
+
|
|
228
|
+
if (ide === 'windsurf') {
|
|
229
|
+
// AppleScript for Windsurf - robust chat input targeting
|
|
230
|
+
appleScript = `
|
|
231
|
+
tell application "System Events"
|
|
232
|
+
tell process "Windsurf"
|
|
233
|
+
set frontmost to true
|
|
234
|
+
delay 1
|
|
235
|
+
|
|
236
|
+
-- Method 1: Try to find and click on the chat input field
|
|
237
|
+
try
|
|
238
|
+
set chatInput to text field 1 of group 1 of window 1
|
|
239
|
+
click chatInput
|
|
240
|
+
delay 0.5
|
|
241
|
+
on error
|
|
242
|
+
try
|
|
243
|
+
-- Method 2: Try to find text area for chat
|
|
244
|
+
set chatInput to text area 1 of group 1 of window 1
|
|
245
|
+
click chatInput
|
|
246
|
+
delay 0.5
|
|
247
|
+
on error
|
|
248
|
+
try
|
|
249
|
+
-- Method 3: Try to find any input field
|
|
250
|
+
set chatInput to text field 1 of window 1
|
|
251
|
+
click chatInput
|
|
252
|
+
delay 0.5
|
|
253
|
+
on error
|
|
254
|
+
-- Method 4: Just try typing (fallback)
|
|
255
|
+
delay 0.5
|
|
256
|
+
end try
|
|
257
|
+
end try
|
|
258
|
+
end try
|
|
259
|
+
|
|
260
|
+
-- Type the message
|
|
261
|
+
keystroke "${text.replace(/"/g, '\\"')}"
|
|
262
|
+
delay 0.5
|
|
263
|
+
|
|
264
|
+
-- Try multiple submission methods
|
|
265
|
+
key code 36
|
|
266
|
+
delay 0.5
|
|
267
|
+
key code 36 using {command down}
|
|
268
|
+
delay 0.5
|
|
269
|
+
key code 36 using {command down, shift down}
|
|
270
|
+
delay 0.5
|
|
271
|
+
key code 1 using {command down}
|
|
272
|
+
delay 0.5
|
|
273
|
+
end tell
|
|
274
|
+
end tell
|
|
275
|
+
`;
|
|
276
|
+
} else if (ide === 'cursor') {
|
|
277
|
+
// AppleScript for Cursor - proven to work (from commit 3403c17)
|
|
278
|
+
appleScript = `
|
|
279
|
+
tell application "System Events"
|
|
280
|
+
tell process "Cursor"
|
|
281
|
+
set frontmost to true
|
|
282
|
+
delay 1
|
|
283
|
+
|
|
284
|
+
-- DYNAMIC SCREENSHOT-BASED CHAT INPUT TARGETING
|
|
285
|
+
-- Method 1: Take screenshot of the monitor where Cursor is running
|
|
286
|
+
try
|
|
287
|
+
-- Get the bounds of the Cursor window to determine which monitor it's on
|
|
288
|
+
set cursorBounds to bounds of window 1
|
|
289
|
+
set cursorX to item 1 of cursorBounds
|
|
290
|
+
set cursorY to item 2 of cursorBounds
|
|
291
|
+
|
|
292
|
+
-- Take a screenshot of the specific monitor where Cursor is located
|
|
293
|
+
set screenshotPath to (path to desktop as string) & "cursor_screenshot.png"
|
|
294
|
+
|
|
295
|
+
-- Use screencapture with display selection based on Cursor window position
|
|
296
|
+
-- If Cursor is on the right side (aux monitor), use display 2
|
|
297
|
+
if cursorX > 1000 then
|
|
298
|
+
do shell script "screencapture -D 2 -x " & quoted form of POSIX path of screenshotPath
|
|
299
|
+
else
|
|
300
|
+
do shell script "screencapture -D 1 -x " & quoted form of POSIX path of screenshotPath
|
|
301
|
+
end if
|
|
302
|
+
delay 0.5
|
|
303
|
+
|
|
304
|
+
-- Use Python with PIL to analyze the screenshot and find text input area
|
|
305
|
+
set pythonScript to "
|
|
306
|
+
import sys
|
|
307
|
+
import os
|
|
308
|
+
from PIL import Image, ImageDraw
|
|
309
|
+
import subprocess
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
# Read the screenshot
|
|
313
|
+
img = Image.open('" & POSIX path of screenshotPath & "')
|
|
314
|
+
width, height = img.size
|
|
315
|
+
|
|
316
|
+
# Convert to grayscale for analysis
|
|
317
|
+
gray = img.convert('L')
|
|
318
|
+
|
|
319
|
+
# Get screen dimensions to calculate relative positions
|
|
320
|
+
screen_width = width
|
|
321
|
+
screen_height = height
|
|
322
|
+
|
|
323
|
+
# Determine if this is the auxiliary monitor based on Cursor window position
|
|
324
|
+
cursor_x = " & cursorX & "
|
|
325
|
+
is_aux_monitor = cursor_x > 1000
|
|
326
|
+
|
|
327
|
+
if is_aux_monitor:
|
|
328
|
+
# For auxiliary monitor, adjust coordinates
|
|
329
|
+
# Chat input is typically in the right side of the aux monitor
|
|
330
|
+
candidates = [
|
|
331
|
+
(int(screen_width * 0.8), int(screen_height * 0.85)), # Bottom-right
|
|
332
|
+
(int(screen_width * 0.75), int(screen_height * 0.8)), # Slightly up
|
|
333
|
+
(int(screen_width * 0.85), int(screen_height * 0.8)), # More right
|
|
334
|
+
(int(screen_width * 0.7), int(screen_height * 0.9)), # Bottom-left of right area
|
|
335
|
+
]
|
|
336
|
+
else:
|
|
337
|
+
# For main monitor, use standard coordinates
|
|
338
|
+
candidates = [
|
|
339
|
+
(int(screen_width * 0.8), int(screen_height * 0.85)), # Bottom-right
|
|
340
|
+
(int(screen_width * 0.75), int(screen_height * 0.8)), # Slightly up
|
|
341
|
+
(int(screen_width * 0.85), int(screen_height * 0.8)), # More right
|
|
342
|
+
(int(screen_width * 0.7), int(screen_height * 0.9)), # Bottom-left of right area
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
# Use the first candidate (bottom-right area)
|
|
346
|
+
x, y = candidates[0]
|
|
347
|
+
print(f'{x},{y}')
|
|
348
|
+
|
|
349
|
+
except Exception as e:
|
|
350
|
+
# Fallback to estimated coordinates based on monitor
|
|
351
|
+
if " & cursorX & " > 1000:
|
|
352
|
+
print('1000,600') # Aux monitor fallback
|
|
353
|
+
else:
|
|
354
|
+
print('800,500') # Main monitor fallback
|
|
355
|
+
"
|
|
356
|
+
|
|
357
|
+
-- Execute Python script to find coordinates
|
|
358
|
+
set coordinates to do shell script "python3 -c " & quoted form of pythonScript
|
|
359
|
+
delay 0.3
|
|
360
|
+
|
|
361
|
+
-- Click at the detected coordinates
|
|
362
|
+
do shell script "/usr/local/bin/cliclick c:" & coordinates
|
|
363
|
+
delay 0.5
|
|
364
|
+
|
|
365
|
+
-- Clean up screenshot
|
|
366
|
+
do shell script "rm " & quoted form of POSIX path of screenshotPath
|
|
367
|
+
|
|
368
|
+
on error
|
|
369
|
+
-- Fallback: Use estimated coordinates if screenshot analysis fails
|
|
370
|
+
try
|
|
371
|
+
-- Click in the main editor area to escape terminal
|
|
372
|
+
do shell script "/usr/local/bin/cliclick c:600,400"
|
|
373
|
+
delay 0.3
|
|
374
|
+
|
|
375
|
+
-- Click in the chat panel area to focus it
|
|
376
|
+
do shell script "/usr/local/bin/cliclick c:1200,500"
|
|
377
|
+
delay 0.3
|
|
378
|
+
|
|
379
|
+
-- Click in the chat input area specifically
|
|
380
|
+
do shell script "/usr/local/bin/cliclick c:1100,700"
|
|
381
|
+
delay 0.3
|
|
382
|
+
|
|
383
|
+
on error
|
|
384
|
+
delay 0.3
|
|
385
|
+
end try
|
|
386
|
+
end try
|
|
387
|
+
|
|
388
|
+
-- Open new chat session with Cmd+T to prevent crashes
|
|
389
|
+
key code 17 using {command down} -- Cmd+T
|
|
390
|
+
delay 1.0
|
|
391
|
+
|
|
392
|
+
-- Type the message using clipboard (more reliable than keystroke)
|
|
393
|
+
set the clipboard to "${text.replace(/"/g, '\\"')}"
|
|
394
|
+
delay 0.5
|
|
395
|
+
key code 9 using {command down} -- Cmd+V to paste
|
|
396
|
+
delay 0.5
|
|
397
|
+
|
|
398
|
+
-- Press Cmd+Enter to send (more reliable than just Enter)
|
|
399
|
+
key code 36 using {command down}
|
|
400
|
+
delay 1
|
|
401
|
+
|
|
402
|
+
return "Message sent via AppleScript"
|
|
403
|
+
end tell
|
|
404
|
+
end tell
|
|
405
|
+
`;
|
|
406
|
+
} else if (ide === 'antigravity') {
|
|
407
|
+
// AppleScript for Google Antigravity - Cmd+Shift+L opens Agent chat (non-toggle)
|
|
408
|
+
appleScript = `
|
|
409
|
+
tell application "System Events"
|
|
410
|
+
tell process "Antigravity"
|
|
411
|
+
set frontmost to true
|
|
412
|
+
delay 1
|
|
413
|
+
|
|
414
|
+
-- Open Agent chat with Cmd+Shift+L (does not toggle)
|
|
415
|
+
key code 37 using {command down, shift down}
|
|
416
|
+
delay 1.5
|
|
417
|
+
|
|
418
|
+
-- Type the message
|
|
419
|
+
keystroke "${text.replace(/"/g, '\\"')}"
|
|
420
|
+
delay 0.5
|
|
421
|
+
|
|
422
|
+
-- Send with Enter
|
|
423
|
+
key code 36
|
|
424
|
+
delay 0.5
|
|
425
|
+
end tell
|
|
426
|
+
end tell
|
|
427
|
+
`;
|
|
428
|
+
} else {
|
|
429
|
+
return {
|
|
430
|
+
success: false,
|
|
431
|
+
error: `Unsupported IDE for AppleScript: ${ide}`,
|
|
432
|
+
note: 'AppleScript is only supported for Cursor, Windsurf, and Antigravity'
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
execSync(`osascript -e '${appleScript}'`, { stdio: 'pipe' });
|
|
437
|
+
|
|
438
|
+
this.logger.log(`Successfully sent message to ${ideName} via AppleScript`);
|
|
439
|
+
return {
|
|
440
|
+
success: true,
|
|
441
|
+
method: 'applescript',
|
|
442
|
+
message: `Message sent to ${ideName}: ${text}`,
|
|
443
|
+
note: 'Message sent via AppleScript automation'
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
} catch (error) {
|
|
447
|
+
this.logger.log('AppleScript interaction failed, falling back to simulated response:', error.message);
|
|
448
|
+
|
|
449
|
+
// Fallback to simulated response if AppleScript fails
|
|
450
|
+
return {
|
|
451
|
+
success: true,
|
|
452
|
+
method: 'simulated',
|
|
453
|
+
message: `Simulated ${ideName} response: ${text}`,
|
|
454
|
+
note: `${ideName} AppleScript automation failed. Using simulated response for testing.`
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Read chat response from an IDE using AppleScript
|
|
461
|
+
* @param {string} ide - The IDE name ('cursor' or 'windsurf')
|
|
462
|
+
* @returns {Promise<string>} The chat response text
|
|
463
|
+
*/
|
|
464
|
+
async readChatResponse(ide) {
|
|
465
|
+
if (ide !== 'windsurf' && ide !== 'cursor') {
|
|
466
|
+
return 'Error: AppleScript reading is only supported for Cursor and Windsurf';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
this.logger.log(`${ide === 'windsurf' ? 'Windsurf' : 'Cursor'} detected - using AppleScript to read chat response`);
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
// For Cursor, try CDP first if available
|
|
473
|
+
if (ide === 'cursor') {
|
|
474
|
+
try {
|
|
475
|
+
this.logger.log('🔧 Attempting CDP method for Cursor...');
|
|
476
|
+
const cdpResponse = await this.readCursorResponseViaCDP();
|
|
477
|
+
if (cdpResponse && cdpResponse.length > 20) {
|
|
478
|
+
this.logger.log('✅ Successfully read Cursor response via CDP');
|
|
479
|
+
return cdpResponse;
|
|
480
|
+
}
|
|
481
|
+
} catch (error) {
|
|
482
|
+
this.logger.log('⚠️ CDP method failed, falling back to AppleScript:', error.message);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Try clipboard-based response detection
|
|
487
|
+
if (ide === 'cursor') {
|
|
488
|
+
try {
|
|
489
|
+
this.logger.log('🔧 Attempting clipboard-based response detection...');
|
|
490
|
+
const clipboardResponse = await this.readCursorResponseViaClipboard();
|
|
491
|
+
if (clipboardResponse && clipboardResponse.length > 20) {
|
|
492
|
+
this.logger.log('✅ Successfully read Cursor response via clipboard');
|
|
493
|
+
return clipboardResponse;
|
|
494
|
+
}
|
|
495
|
+
} catch (error) {
|
|
496
|
+
this.logger.log('⚠️ Clipboard method failed, falling back to AppleScript:', error.message);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
} catch (error) {
|
|
501
|
+
this.logger.log('⚠️ Fallback methods failed:', error.message);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
// AppleScript to read the chat response from IDE
|
|
506
|
+
const appleScript = `
|
|
507
|
+
tell application "System Events"
|
|
508
|
+
tell process "${ide === 'windsurf' ? 'Windsurf' : 'Cursor'}"
|
|
509
|
+
set frontmost to true
|
|
510
|
+
delay 1
|
|
511
|
+
|
|
512
|
+
${ide === 'cursor' ? `
|
|
513
|
+
-- Cursor-specific methods
|
|
514
|
+
-- Method 1: Try to get text from web view content (most likely location)
|
|
515
|
+
try
|
|
516
|
+
set webViewGroups to group of window 1
|
|
517
|
+
set responseText to ""
|
|
518
|
+
repeat with grp in webViewGroups
|
|
519
|
+
try
|
|
520
|
+
-- Look for text areas within groups (web view content)
|
|
521
|
+
set textAreas to text area of grp
|
|
522
|
+
repeat with txtArea in textAreas
|
|
523
|
+
try
|
|
524
|
+
set textValue to value of txtArea
|
|
525
|
+
if textValue is not "" and textValue is not missing value then
|
|
526
|
+
set responseText to responseText & textValue & "\\n"
|
|
527
|
+
end if
|
|
528
|
+
on error
|
|
529
|
+
-- Continue to next text area
|
|
530
|
+
end try
|
|
531
|
+
end repeat
|
|
532
|
+
|
|
533
|
+
-- Look for static text within groups
|
|
534
|
+
set staticTexts to static text of grp
|
|
535
|
+
repeat with txt in staticTexts
|
|
536
|
+
try
|
|
537
|
+
set textValue to value of txt
|
|
538
|
+
if textValue is not "" and textValue is not missing value then
|
|
539
|
+
set responseText to responseText & textValue & "\\n"
|
|
540
|
+
end if
|
|
541
|
+
on error
|
|
542
|
+
-- Continue to next static text
|
|
543
|
+
end try
|
|
544
|
+
end repeat
|
|
545
|
+
on error
|
|
546
|
+
-- Continue to next group
|
|
547
|
+
end try
|
|
548
|
+
end repeat
|
|
549
|
+
if responseText is not "" then
|
|
550
|
+
return responseText
|
|
551
|
+
end if
|
|
552
|
+
on error
|
|
553
|
+
-- Continue to next method
|
|
554
|
+
end try
|
|
555
|
+
|
|
556
|
+
-- Method 2: Try to get all static text from the window
|
|
557
|
+
try
|
|
558
|
+
set allText to value of static text of window 1
|
|
559
|
+
if allText is not "" then
|
|
560
|
+
return allText
|
|
561
|
+
end if
|
|
562
|
+
on error
|
|
563
|
+
-- Continue to next method
|
|
564
|
+
end try
|
|
565
|
+
|
|
566
|
+
-- Method 3: Try to get text from scroll areas
|
|
567
|
+
try
|
|
568
|
+
set scrollAreas to scroll area of window 1
|
|
569
|
+
set responseText to ""
|
|
570
|
+
repeat with scrollArea in scrollAreas
|
|
571
|
+
try
|
|
572
|
+
set scrollText to value of static text of scrollArea
|
|
573
|
+
if scrollText is not "" then
|
|
574
|
+
set responseText to responseText & scrollText & "\\n"
|
|
575
|
+
end if
|
|
576
|
+
on error
|
|
577
|
+
-- Continue to next scroll area
|
|
578
|
+
end try
|
|
579
|
+
end repeat
|
|
580
|
+
if responseText is not "" then
|
|
581
|
+
return responseText
|
|
582
|
+
end if
|
|
583
|
+
on error
|
|
584
|
+
-- Continue to next method
|
|
585
|
+
end try
|
|
586
|
+
|
|
587
|
+
-- Method 4: Try accessibility API approach
|
|
588
|
+
try
|
|
589
|
+
set uiElements to every UI element of window 1
|
|
590
|
+
set responseText to ""
|
|
591
|
+
repeat with element in uiElements
|
|
592
|
+
try
|
|
593
|
+
set elementText to value of element
|
|
594
|
+
if elementText is not "" and elementText is not missing value then
|
|
595
|
+
set responseText to responseText & elementText & "\\n"
|
|
596
|
+
end if
|
|
597
|
+
on error
|
|
598
|
+
-- Continue to next element
|
|
599
|
+
end try
|
|
600
|
+
end repeat
|
|
601
|
+
if responseText is not "" then
|
|
602
|
+
return responseText
|
|
603
|
+
end if
|
|
604
|
+
on error
|
|
605
|
+
-- Continue to next method
|
|
606
|
+
end try
|
|
607
|
+
|
|
608
|
+
-- Method 5: Return diagnostic information
|
|
609
|
+
return "DEBUG: Window count: " & count of windows & "\\nText areas found: " & count of text area of window 1 & "\\nStatic text found: " & value of static text of window 1 & "\\n\\n\\nRESPONSE: "
|
|
610
|
+
` : `
|
|
611
|
+
-- Windsurf-specific methods
|
|
612
|
+
-- Method 1: Try to get all static text from the window
|
|
613
|
+
try
|
|
614
|
+
set allText to value of static text of window 1
|
|
615
|
+
if allText is not "" then
|
|
616
|
+
return allText
|
|
617
|
+
end if
|
|
618
|
+
on error
|
|
619
|
+
-- Continue to next method
|
|
620
|
+
end try
|
|
621
|
+
|
|
622
|
+
-- Method 2: Try to get text from groups
|
|
623
|
+
try
|
|
624
|
+
set groups to group of window 1
|
|
625
|
+
set responseText to ""
|
|
626
|
+
repeat with grp in groups
|
|
627
|
+
try
|
|
628
|
+
set groupText to value of static text of grp
|
|
629
|
+
if groupText is not "" then
|
|
630
|
+
set responseText to responseText & groupText & "\\n"
|
|
631
|
+
end if
|
|
632
|
+
on error
|
|
633
|
+
-- Continue to next group
|
|
634
|
+
end try
|
|
635
|
+
end repeat
|
|
636
|
+
if responseText is not "" then
|
|
637
|
+
return responseText
|
|
638
|
+
end if
|
|
639
|
+
on error
|
|
640
|
+
-- Continue to next method
|
|
641
|
+
end try
|
|
642
|
+
|
|
643
|
+
-- Method 3: Try clipboard approach
|
|
644
|
+
try
|
|
645
|
+
key code 0 using {command down}
|
|
646
|
+
delay 0.5
|
|
647
|
+
key code 8 using {command down}
|
|
648
|
+
delay 0.5
|
|
649
|
+
set clipboardText to (the clipboard)
|
|
650
|
+
if clipboardText is not "" then
|
|
651
|
+
return clipboardText
|
|
652
|
+
end if
|
|
653
|
+
on error
|
|
654
|
+
-- Continue to next method
|
|
655
|
+
end try
|
|
656
|
+
|
|
657
|
+
-- Method 4: Return placeholder
|
|
658
|
+
return "No chat content found in Windsurf"
|
|
659
|
+
`}
|
|
660
|
+
end tell
|
|
661
|
+
end tell
|
|
662
|
+
`;
|
|
663
|
+
|
|
664
|
+
const result = execSync(`osascript -e '${appleScript}'`, { stdio: 'pipe', encoding: 'utf8' });
|
|
665
|
+
this.logger.log('Successfully read response via AppleScript');
|
|
666
|
+
this.logger.log('📖 AppleScript result:', result);
|
|
667
|
+
this.logger.log('📏 Result length:', result.length);
|
|
668
|
+
this.logger.log('🔍 Result preview:', result.substring(0, 200));
|
|
669
|
+
|
|
670
|
+
// Check if the result is just diagnostic information
|
|
671
|
+
const isDiagnosticResult = result.includes('DEBUG:') ||
|
|
672
|
+
result.includes('diagnostic') ||
|
|
673
|
+
result.includes('No chat content found') ||
|
|
674
|
+
result.includes('Text areas found: 0') ||
|
|
675
|
+
result.includes('Static text found:') ||
|
|
676
|
+
result.includes('Window count:') ||
|
|
677
|
+
result.includes('Sample text:') ||
|
|
678
|
+
result.includes('Could not read') ||
|
|
679
|
+
result.includes('No readable text content detected');
|
|
680
|
+
|
|
681
|
+
this.logger.log(`🔍 Diagnostic detection check: isDiagnosticResult = ${isDiagnosticResult}`);
|
|
682
|
+
this.logger.log(`🔍 Result contains 'DEBUG:': ${result.includes('DEBUG:')}`);
|
|
683
|
+
this.logger.log(`🔍 Result contains 'Window count:': ${result.includes('Window count:')}`);
|
|
684
|
+
|
|
685
|
+
// For Cursor, always try CDP and clipboard methods as fallbacks
|
|
686
|
+
if (ide === 'cursor') {
|
|
687
|
+
this.logger.log('🔧 For Cursor, always trying CDP and clipboard methods as fallbacks...');
|
|
688
|
+
|
|
689
|
+
// Try CDP method
|
|
690
|
+
try {
|
|
691
|
+
this.logger.log('🔧 Attempting CDP method for Cursor...');
|
|
692
|
+
const cdpResponse = await this.readCursorResponseViaCDP();
|
|
693
|
+
if (cdpResponse && cdpResponse.length > 20) {
|
|
694
|
+
this.logger.log('✅ Successfully read Cursor response via CDP');
|
|
695
|
+
return cdpResponse;
|
|
696
|
+
}
|
|
697
|
+
} catch (error) {
|
|
698
|
+
this.logger.log('⚠️ CDP method failed:', error.message);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Try clipboard method
|
|
702
|
+
try {
|
|
703
|
+
this.logger.log('🔧 Attempting clipboard-based response detection...');
|
|
704
|
+
const clipboardResponse = await this.readCursorResponseViaClipboard();
|
|
705
|
+
if (clipboardResponse && clipboardResponse.length > 20) {
|
|
706
|
+
this.logger.log('✅ Successfully read Cursor response via clipboard');
|
|
707
|
+
return clipboardResponse;
|
|
708
|
+
}
|
|
709
|
+
} catch (error) {
|
|
710
|
+
this.logger.log('⚠️ Clipboard method failed:', error.message);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (isDiagnosticResult) {
|
|
715
|
+
this.logger.log('⚠️ AppleScript returned diagnostic info, all methods failed...');
|
|
716
|
+
return `Unable to read ${ide} response - all methods failed. AppleScript diagnostic: ${result.substring(0, 200)}...`;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return result.trim();
|
|
720
|
+
|
|
721
|
+
} catch (error) {
|
|
722
|
+
this.logger.log('AppleScript reading failed:', error.message);
|
|
723
|
+
return 'Error: Could not read Windsurf response via AppleScript';
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Detect quota warnings using AppleScript
|
|
729
|
+
* @param {string} ide - The IDE name ('windsurf')
|
|
730
|
+
* @returns {Promise<Object>} Quota detection result
|
|
731
|
+
*/
|
|
732
|
+
async detectQuotaWarning(ide) {
|
|
733
|
+
if (ide !== 'windsurf') {
|
|
734
|
+
return {
|
|
735
|
+
hasQuotaWarning: false,
|
|
736
|
+
note: 'AppleScript quota detection is only supported for Windsurf'
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
this.logger.log('🔍 === APPLESCRIPT DETECTION START ===');
|
|
741
|
+
this.logger.log('🔍 Starting enhanced AppleScript quota detection...');
|
|
742
|
+
|
|
743
|
+
try {
|
|
744
|
+
// Test basic AppleScript execution first
|
|
745
|
+
this.logger.log('🔍 Testing basic AppleScript execution...');
|
|
746
|
+
const basicTest = execSync('osascript -e "return \\"test\\""', { stdio: 'pipe', encoding: 'utf8' });
|
|
747
|
+
this.logger.log('✅ Basic AppleScript test passed:', basicTest.trim());
|
|
748
|
+
} catch (basicError) {
|
|
749
|
+
this.logger.log('❌ Basic AppleScript test failed:', basicError.message);
|
|
750
|
+
return {
|
|
751
|
+
hasQuotaWarning: false,
|
|
752
|
+
error: 'AppleScript not available',
|
|
753
|
+
note: 'Basic AppleScript test failed'
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Try a much simpler AppleScript first
|
|
758
|
+
this.logger.log('🔍 Trying simple AppleScript approach...');
|
|
759
|
+
const simpleAppleScript = `
|
|
760
|
+
tell application "System Events"
|
|
761
|
+
tell process "Windsurf"
|
|
762
|
+
set frontmost to true
|
|
763
|
+
delay 1
|
|
764
|
+
|
|
765
|
+
-- Method 1: Look for static text containing quota warnings
|
|
766
|
+
try
|
|
767
|
+
set allText to value of static text of window 1
|
|
768
|
+
if allText contains "Not enough credits" or allText contains "Upgrade to a paid plan" or allText contains "switch to SWE-1-lite" then
|
|
769
|
+
return "QUOTA_WARNING_DETECTED: " & allText
|
|
770
|
+
end if
|
|
771
|
+
on error
|
|
772
|
+
-- Continue to next method
|
|
773
|
+
end try
|
|
774
|
+
|
|
775
|
+
-- Method 2: Look for disabled text areas (quota warnings often disable chat input)
|
|
776
|
+
try
|
|
777
|
+
set textAreas to text area of window 1
|
|
778
|
+
repeat with textArea in textAreas
|
|
779
|
+
try
|
|
780
|
+
if not enabled of textArea then
|
|
781
|
+
return "QUOTA_INDICATOR_DETECTED: Disabled text area found"
|
|
782
|
+
end if
|
|
783
|
+
on error
|
|
784
|
+
-- Continue to next text area
|
|
785
|
+
end try
|
|
786
|
+
end repeat
|
|
787
|
+
on error
|
|
788
|
+
-- Continue to next method
|
|
789
|
+
end try
|
|
790
|
+
|
|
791
|
+
-- Method 3: Look for specific UI elements that indicate quota issues
|
|
792
|
+
try
|
|
793
|
+
set buttons to button of window 1
|
|
794
|
+
repeat with btn in buttons
|
|
795
|
+
try
|
|
796
|
+
set buttonText to title of btn
|
|
797
|
+
if buttonText contains "Upgrade" or buttonText contains "credits" or buttonText contains "quota" then
|
|
798
|
+
return "QUOTA_UI_DETECTED: " & buttonText
|
|
799
|
+
end if
|
|
800
|
+
on error
|
|
801
|
+
-- Continue to next button
|
|
802
|
+
end try
|
|
803
|
+
end repeat
|
|
804
|
+
on error
|
|
805
|
+
-- Continue to next method
|
|
806
|
+
end try
|
|
807
|
+
|
|
808
|
+
-- Method 4: Look for groups that might contain quota information
|
|
809
|
+
try
|
|
810
|
+
set groups to group of window 1
|
|
811
|
+
repeat with grp in groups
|
|
812
|
+
try
|
|
813
|
+
set groupText to value of static text of grp
|
|
814
|
+
if groupText contains "credits" or groupText contains "quota" or groupText contains "upgrade" then
|
|
815
|
+
return "QUOTA_GROUP_DETECTED: " & groupText
|
|
816
|
+
end if
|
|
817
|
+
on error
|
|
818
|
+
-- Continue to next group
|
|
819
|
+
end try
|
|
820
|
+
end repeat
|
|
821
|
+
on error
|
|
822
|
+
-- Continue to next method
|
|
823
|
+
end try
|
|
824
|
+
|
|
825
|
+
return "NO_QUOTA_WARNING_DETECTED"
|
|
826
|
+
end tell
|
|
827
|
+
end tell
|
|
828
|
+
`;
|
|
829
|
+
|
|
830
|
+
this.logger.log('🔍 Simple AppleScript length:', simpleAppleScript.length);
|
|
831
|
+
this.logger.log('🔍 Simple AppleScript preview:', simpleAppleScript.substring(0, 200) + '...');
|
|
832
|
+
|
|
833
|
+
try {
|
|
834
|
+
this.logger.log('🔍 Executing simple AppleScript...');
|
|
835
|
+
this.logger.log('🔍 AppleScript command length:', simpleAppleScript.length);
|
|
836
|
+
|
|
837
|
+
const result = execSync(`osascript -e '${simpleAppleScript}'`, { stdio: 'pipe', encoding: 'utf8' });
|
|
838
|
+
this.logger.log('🔍 AppleScript execution completed');
|
|
839
|
+
this.logger.log('🔍 AppleScript result length:', result.length);
|
|
840
|
+
this.logger.log('🔍 AppleScript result:', result.trim());
|
|
841
|
+
this.logger.log('🔍 AppleScript result starts with:', result.trim().substring(0, 100));
|
|
842
|
+
|
|
843
|
+
// Check for quota warnings in the result
|
|
844
|
+
if (result.includes('QUOTA_WARNING_DETECTED')) {
|
|
845
|
+
this.logger.log('❌ Windsurf quota warning detected via enhanced AppleScript');
|
|
846
|
+
return {
|
|
847
|
+
hasQuotaWarning: true,
|
|
848
|
+
method: 'applescript-static-text',
|
|
849
|
+
note: 'Windsurf quota warning detected via enhanced AppleScript detection'
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (result.includes('QUOTA_INDICATOR_DETECTED')) {
|
|
854
|
+
this.logger.log('❌ Windsurf quota indicator detected via enhanced AppleScript');
|
|
855
|
+
return {
|
|
856
|
+
hasQuotaWarning: true,
|
|
857
|
+
method: 'applescript-disabled-input',
|
|
858
|
+
note: 'Windsurf quota indicator detected via enhanced AppleScript (disabled chat input)'
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (result.includes('QUOTA_UI_DETECTED') || result.includes('QUOTA_GROUP_DETECTED')) {
|
|
863
|
+
this.logger.log('⚠️ Windsurf potential quota text detected via enhanced AppleScript');
|
|
864
|
+
return {
|
|
865
|
+
hasQuotaWarning: true,
|
|
866
|
+
method: 'applescript-ui-elements',
|
|
867
|
+
note: 'Windsurf potential quota text detected via enhanced AppleScript'
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Additional pattern matching for quota-related text
|
|
872
|
+
const quotaPatterns = [
|
|
873
|
+
'not enough credits',
|
|
874
|
+
'upgrade to a paid plan',
|
|
875
|
+
'switch to swe-1-lite',
|
|
876
|
+
'quota exceeded',
|
|
877
|
+
'credits exhausted',
|
|
878
|
+
'payment required'
|
|
879
|
+
];
|
|
880
|
+
|
|
881
|
+
const lowerResult = result.toLowerCase();
|
|
882
|
+
for (const pattern of quotaPatterns) {
|
|
883
|
+
if (lowerResult.includes(pattern)) {
|
|
884
|
+
this.logger.log('❌ Windsurf quota warning detected via text pattern matching');
|
|
885
|
+
return {
|
|
886
|
+
hasQuotaWarning: true,
|
|
887
|
+
method: 'applescript-pattern-matching',
|
|
888
|
+
pattern: pattern,
|
|
889
|
+
note: 'Windsurf quota warning detected via text pattern matching in AppleScript result'
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
this.logger.log('🔍 === APPLESCRIPT DETECTION END ===');
|
|
895
|
+
this.logger.log('🔍 Since AppleScript cannot access web view, trying alternative methods...');
|
|
896
|
+
|
|
897
|
+
// Since AppleScript cannot access web view content, return a note about the limitation
|
|
898
|
+
return {
|
|
899
|
+
hasQuotaWarning: false,
|
|
900
|
+
method: 'applescript-limited',
|
|
901
|
+
note: 'Windsurf quota warning is visible in UI but not detectable via AppleScript'
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
} catch (error) {
|
|
905
|
+
this.logger.log('❌ AppleScript error:', error.message);
|
|
906
|
+
return {
|
|
907
|
+
hasQuotaWarning: false,
|
|
908
|
+
error: error.message,
|
|
909
|
+
note: 'AppleScript cannot access web view content - Windsurf does not support CDP'
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Read Cursor response via Chrome DevTools Protocol (CDP)
|
|
916
|
+
* @returns {Promise<string>} The chat response text
|
|
917
|
+
*/
|
|
918
|
+
async readCursorResponseViaCDP() {
|
|
919
|
+
try {
|
|
920
|
+
this.logger.log('🔧 Attempting CDP connection to Cursor on port 9225...');
|
|
921
|
+
|
|
922
|
+
// Import CDP dynamically to avoid issues in VSCode extension context
|
|
923
|
+
const CDP = require('chrome-remote-interface');
|
|
924
|
+
|
|
925
|
+
// List available targets
|
|
926
|
+
const targets = await CDP.List({ port: 9225 });
|
|
927
|
+
this.logger.log(`🔧 Found ${targets.length} CDP targets`);
|
|
928
|
+
|
|
929
|
+
// Find the main workbench target (not settings)
|
|
930
|
+
const workbench = targets.find(t =>
|
|
931
|
+
t.title !== 'Cursor Settings' &&
|
|
932
|
+
t.type === 'page' &&
|
|
933
|
+
t.url && t.url.includes('workbench')
|
|
934
|
+
) || targets[0];
|
|
935
|
+
|
|
936
|
+
if (!workbench) {
|
|
937
|
+
throw new Error('No suitable Cursor workbench target found');
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
this.logger.log(`🔧 Connecting to target: ${workbench.title}`);
|
|
941
|
+
|
|
942
|
+
// Connect to the target
|
|
943
|
+
const client = await CDP({ port: 9225, target: workbench });
|
|
944
|
+
const { Runtime, DOM } = client;
|
|
945
|
+
|
|
946
|
+
// Enable required domains
|
|
947
|
+
await Runtime.enable();
|
|
948
|
+
await DOM.enable();
|
|
949
|
+
|
|
950
|
+
this.logger.log('🔧 CDP connection established, searching for chat content...');
|
|
951
|
+
|
|
952
|
+
// Try multiple selectors to find chat content
|
|
953
|
+
const selectors = [
|
|
954
|
+
'.chat-content',
|
|
955
|
+
'.chat-messages',
|
|
956
|
+
'.response-content',
|
|
957
|
+
'.ai-response',
|
|
958
|
+
'.message-content',
|
|
959
|
+
'[data-testid="chat-message"]',
|
|
960
|
+
'.markdown-content',
|
|
961
|
+
'.prose',
|
|
962
|
+
'.chat-panel',
|
|
963
|
+
'.sidebar-panel'
|
|
964
|
+
];
|
|
965
|
+
|
|
966
|
+
for (const selector of selectors) {
|
|
967
|
+
try {
|
|
968
|
+
this.logger.log(`🔧 Trying selector: ${selector}`);
|
|
969
|
+
|
|
970
|
+
// Get document root
|
|
971
|
+
const { root } = await DOM.getDocument();
|
|
972
|
+
|
|
973
|
+
// Search for elements with the selector
|
|
974
|
+
const { nodeIds } = await DOM.querySelectorAll({ nodeId: root.nodeId, selector });
|
|
975
|
+
|
|
976
|
+
if (nodeIds.length > 0) {
|
|
977
|
+
this.logger.log(`🔧 Found ${nodeIds.length} elements with selector: ${selector}`);
|
|
978
|
+
|
|
979
|
+
// Get text content from the first matching element
|
|
980
|
+
const { nodeId } = await DOM.describeNode({ nodeId: nodeIds[0] });
|
|
981
|
+
const { outerHTML } = await DOM.getOuterHTML({ nodeId });
|
|
982
|
+
|
|
983
|
+
// Extract text content (remove HTML tags)
|
|
984
|
+
const textContent = outerHTML.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
985
|
+
|
|
986
|
+
if (textContent && textContent.length > 20) {
|
|
987
|
+
this.logger.log(`✅ Successfully extracted chat content via CDP: ${textContent.substring(0, 100)}...`);
|
|
988
|
+
await client.close();
|
|
989
|
+
return textContent;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
} catch (error) {
|
|
993
|
+
this.logger.log(`⚠️ Selector ${selector} failed: ${error.message}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// If no specific selectors work, try to get all text content
|
|
998
|
+
this.logger.log('🔧 Trying to get all text content from the page...');
|
|
999
|
+
|
|
1000
|
+
const { root } = await DOM.getDocument();
|
|
1001
|
+
const { outerHTML } = await DOM.getOuterHTML({ nodeId: root.nodeId });
|
|
1002
|
+
|
|
1003
|
+
// Extract text content and look for AI responses
|
|
1004
|
+
const textContent = outerHTML.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
1005
|
+
|
|
1006
|
+
// Look for patterns that indicate AI responses
|
|
1007
|
+
const aiResponsePatterns = [
|
|
1008
|
+
/(?:AI|Assistant|Claude|GPT|Response):\s*([^]*?)(?=\n\n|\n[A-Z]|$)/gi,
|
|
1009
|
+
/(?:Here|This|The answer|The solution):\s*([^]*?)(?=\n\n|\n[A-Z]|$)/gi
|
|
1010
|
+
];
|
|
1011
|
+
|
|
1012
|
+
for (const pattern of aiResponsePatterns) {
|
|
1013
|
+
const matches = textContent.match(pattern);
|
|
1014
|
+
if (matches && matches.length > 0) {
|
|
1015
|
+
const response = matches[matches.length - 1]; // Get the latest response
|
|
1016
|
+
if (response && response.length > 20) {
|
|
1017
|
+
this.logger.log(`✅ Found AI response via CDP pattern matching: ${response.substring(0, 100)}...`);
|
|
1018
|
+
await client.close();
|
|
1019
|
+
return response;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
await client.close();
|
|
1025
|
+
throw new Error('No chat content found via CDP');
|
|
1026
|
+
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
this.logger.log(`❌ CDP method failed: ${error.message}`);
|
|
1029
|
+
throw error;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Read Cursor response via clipboard (select all and copy)
|
|
1035
|
+
* @returns {Promise<string>} The chat response text
|
|
1036
|
+
*/
|
|
1037
|
+
async readCursorResponseViaClipboard() {
|
|
1038
|
+
try {
|
|
1039
|
+
this.logger.log('🔧 Attempting clipboard-based response detection...');
|
|
1040
|
+
|
|
1041
|
+
// AppleScript to select all content in Cursor and copy to clipboard
|
|
1042
|
+
const clipboardScript = `
|
|
1043
|
+
tell application "System Events"
|
|
1044
|
+
tell process "Cursor"
|
|
1045
|
+
set frontmost to true
|
|
1046
|
+
delay 1
|
|
1047
|
+
|
|
1048
|
+
-- Try to focus on the chat area first
|
|
1049
|
+
try
|
|
1050
|
+
key code 1 using {command down} -- Cmd+A to select all
|
|
1051
|
+
delay 0.5
|
|
1052
|
+
key code 8 using {command down} -- Cmd+C to copy
|
|
1053
|
+
delay 0.5
|
|
1054
|
+
on error
|
|
1055
|
+
-- If that fails, try alternative approach
|
|
1056
|
+
key code 1 using {command down, shift down} -- Cmd+Shift+A to select all
|
|
1057
|
+
delay 0.5
|
|
1058
|
+
key code 8 using {command down} -- Cmd+C to copy
|
|
1059
|
+
delay 0.5
|
|
1060
|
+
end try
|
|
1061
|
+
end tell
|
|
1062
|
+
end tell
|
|
1063
|
+
`;
|
|
1064
|
+
|
|
1065
|
+
execSync(`osascript -e '${clipboardScript}'`, { stdio: 'pipe' });
|
|
1066
|
+
|
|
1067
|
+
// Read from clipboard
|
|
1068
|
+
const clipboardScript2 = `
|
|
1069
|
+
the clipboard
|
|
1070
|
+
`;
|
|
1071
|
+
|
|
1072
|
+
const clipboardContent = execSync(`osascript -e '${clipboardScript2}'`, { stdio: 'pipe', encoding: 'utf8' });
|
|
1073
|
+
|
|
1074
|
+
if (clipboardContent && clipboardContent.trim().length > 20) {
|
|
1075
|
+
this.logger.log(`✅ Successfully read clipboard content: ${clipboardContent.substring(0, 100)}...`);
|
|
1076
|
+
return clipboardContent.trim();
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
throw new Error('Clipboard content is empty or too short');
|
|
1080
|
+
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
this.logger.log(`❌ Clipboard method failed: ${error.message}`);
|
|
1083
|
+
throw error;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
module.exports = { AppleScriptManager };
|