skydeckai-code 0.1.23__py3-none-any.whl → 0.1.25__py3-none-any.whl
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.
- aidd/tools/__init__.py +21 -0
- aidd/tools/code_tools.py +332 -0
- aidd/tools/file_tools.py +149 -25
- aidd/tools/git_tools.py +87 -0
- aidd/tools/other_tools.py +238 -0
- aidd/tools/screenshot_tool.py +161 -130
- aidd/tools/web_tools.py +160 -0
- {skydeckai_code-0.1.23.dist-info → skydeckai_code-0.1.25.dist-info}/METADATA +273 -25
- {skydeckai_code-0.1.23.dist-info → skydeckai_code-0.1.25.dist-info}/RECORD +12 -9
- {skydeckai_code-0.1.23.dist-info → skydeckai_code-0.1.25.dist-info}/WHEEL +0 -0
- {skydeckai_code-0.1.23.dist-info → skydeckai_code-0.1.25.dist-info}/entry_points.txt +0 -0
- {skydeckai_code-0.1.23.dist-info → skydeckai_code-0.1.25.dist-info}/licenses/LICENSE +0 -0
aidd/tools/screenshot_tool.py
CHANGED
@@ -45,15 +45,15 @@ PERMISSION_ERROR_MESSAGES = {
|
|
45
45
|
def _check_macos_screen_recording_permission() -> Dict[str, Any]:
|
46
46
|
"""
|
47
47
|
Check if the application has screen recording permission on macOS.
|
48
|
-
|
48
|
+
|
49
49
|
For macOS 11+, this function uses the official Apple API:
|
50
50
|
- CGPreflightScreenCaptureAccess() to check if permission is already granted
|
51
51
|
- CGRequestScreenCaptureAccess() to request permission if needed
|
52
|
-
|
52
|
+
|
53
53
|
Requesting access will present the system prompt and automatically add your app
|
54
54
|
in the list so the user just needs to enable access. The system prompt will only
|
55
55
|
appear once per app session.
|
56
|
-
|
56
|
+
|
57
57
|
Returns:
|
58
58
|
Dict with keys:
|
59
59
|
- has_permission (bool): Whether permission is granted
|
@@ -61,24 +61,24 @@ def _check_macos_screen_recording_permission() -> Dict[str, Any]:
|
|
61
61
|
- details (dict): Additional context about the permission check
|
62
62
|
"""
|
63
63
|
result = {"has_permission": False, "error": None, "details": {}}
|
64
|
-
|
64
|
+
|
65
65
|
# Check if Quartz is available
|
66
66
|
if not QUARTZ_AVAILABLE:
|
67
67
|
result["error"] = "Quartz framework not available. Cannot check screen recording permission."
|
68
68
|
result["details"] = {"error": "Quartz not available"}
|
69
69
|
return result
|
70
|
-
|
70
|
+
|
71
71
|
# Check if the API is available (macOS 11+)
|
72
72
|
if not hasattr(Quartz, 'CGPreflightScreenCaptureAccess'):
|
73
73
|
result["error"] = "CGPreflightScreenCaptureAccess not available. Your macOS version may be too old (requires macOS 11+)."
|
74
74
|
result["details"] = {"error": "API not available"}
|
75
75
|
return result
|
76
|
-
|
76
|
+
|
77
77
|
try:
|
78
78
|
# Check if we already have permission
|
79
79
|
has_permission = Quartz.CGPreflightScreenCaptureAccess()
|
80
80
|
result["details"]["preflight_result"] = has_permission
|
81
|
-
|
81
|
+
|
82
82
|
if has_permission:
|
83
83
|
# We already have permission
|
84
84
|
result["has_permission"] = True
|
@@ -88,7 +88,7 @@ def _check_macos_screen_recording_permission() -> Dict[str, Any]:
|
|
88
88
|
# This will show the system prompt to the user
|
89
89
|
permission_granted = Quartz.CGRequestScreenCaptureAccess()
|
90
90
|
result["details"]["request_result"] = permission_granted
|
91
|
-
|
91
|
+
|
92
92
|
if permission_granted:
|
93
93
|
result["has_permission"] = True
|
94
94
|
return result
|
@@ -99,7 +99,7 @@ def _check_macos_screen_recording_permission() -> Dict[str, Any]:
|
|
99
99
|
except Exception as e:
|
100
100
|
result["details"]["exception"] = str(e)
|
101
101
|
result["error"] = f"Error checking screen recording permission: {str(e)}"
|
102
|
-
|
102
|
+
|
103
103
|
return result
|
104
104
|
|
105
105
|
|
@@ -165,7 +165,7 @@ def _get_default_screenshot_path() -> str:
|
|
165
165
|
"""Generate a default path for saving screenshots."""
|
166
166
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
167
167
|
filename = f"screenshot_{timestamp}.png"
|
168
|
-
|
168
|
+
|
169
169
|
# Use the allowed directory from state if available, otherwise use temp directory
|
170
170
|
if hasattr(state, 'allowed_directory') and state.allowed_directory:
|
171
171
|
base_dir = os.path.join(state.allowed_directory, "screenshots")
|
@@ -173,18 +173,18 @@ def _get_default_screenshot_path() -> str:
|
|
173
173
|
os.makedirs(base_dir, exist_ok=True)
|
174
174
|
else:
|
175
175
|
base_dir = tempfile.gettempdir()
|
176
|
-
|
176
|
+
|
177
177
|
return os.path.join(base_dir, filename)
|
178
178
|
|
179
179
|
|
180
180
|
def _capture_with_mss(output_path: str, region: Optional[Dict[str, int]] = None) -> bool:
|
181
181
|
"""
|
182
182
|
Capture screenshot using MSS library.
|
183
|
-
|
183
|
+
|
184
184
|
Args:
|
185
185
|
output_path: Path where to save the screenshot
|
186
186
|
region: Optional dictionary with top, left, width, height for specific region
|
187
|
-
|
187
|
+
|
188
188
|
Returns:
|
189
189
|
bool: True if successful, False otherwise
|
190
190
|
"""
|
@@ -196,13 +196,13 @@ def _capture_with_mss(output_path: str, region: Optional[Dict[str, int]] = None)
|
|
196
196
|
else:
|
197
197
|
# Capture entire primary monitor
|
198
198
|
monitor = sct.monitors[1] # monitors[0] is all monitors combined, monitors[1] is the primary
|
199
|
-
|
199
|
+
|
200
200
|
# Grab the picture
|
201
201
|
sct_img = sct.grab(monitor)
|
202
|
-
|
202
|
+
|
203
203
|
# Save it to the output path
|
204
204
|
mss.tools.to_png(sct_img.rgb, sct_img.size, output=output_path)
|
205
|
-
|
205
|
+
|
206
206
|
return os.path.exists(output_path) and os.path.getsize(output_path) > 0
|
207
207
|
except Exception as e:
|
208
208
|
print(f"MSS screenshot error: {str(e)}")
|
@@ -212,10 +212,10 @@ def _capture_with_mss(output_path: str, region: Optional[Dict[str, int]] = None)
|
|
212
212
|
def _find_window_by_name(window_name: str) -> Tuple[Optional[Dict[str, int]], Dict[str, Any]]:
|
213
213
|
"""
|
214
214
|
Find a window by name and return its position and size along with debug info.
|
215
|
-
|
215
|
+
|
216
216
|
Args:
|
217
217
|
window_name: Name of the window to find
|
218
|
-
|
218
|
+
|
219
219
|
Returns:
|
220
220
|
Tuple containing:
|
221
221
|
- Window region dict with top, left, width, height (or None if not found)
|
@@ -242,29 +242,29 @@ def _find_window_by_name(window_name: str) -> Tuple[Optional[Dict[str, int]], Di
|
|
242
242
|
"quartz_available": QUARTZ_AVAILABLE,
|
243
243
|
"detailed_info": detailed_debug_info
|
244
244
|
}
|
245
|
-
|
245
|
+
|
246
246
|
# For non-macOS platforms, use PyGetWindow
|
247
247
|
if not PYGETWINDOW_AVAILABLE:
|
248
248
|
print("PyGetWindow is not available")
|
249
249
|
return None, {"error": "PyGetWindow is not available"}
|
250
|
-
|
250
|
+
|
251
251
|
try:
|
252
252
|
# Get all available windows
|
253
253
|
all_windows = gw.getAllWindows()
|
254
|
-
|
254
|
+
|
255
255
|
# Collect window titles for debugging
|
256
256
|
window_titles = []
|
257
257
|
for w in all_windows:
|
258
258
|
if w.title:
|
259
259
|
window_titles.append(f"'{w.title}' ({w.width}x{w.height})")
|
260
260
|
print(f" - '{w.title}' ({w.width}x{w.height})")
|
261
|
-
|
261
|
+
|
262
262
|
# Standard window matching (case-insensitive)
|
263
263
|
matching_windows = []
|
264
264
|
for window in all_windows:
|
265
265
|
if window.title and window_name.lower() in window.title.lower():
|
266
266
|
matching_windows.append(window)
|
267
|
-
|
267
|
+
|
268
268
|
if not matching_windows:
|
269
269
|
print(f"No window found with title containing '{window_name}'")
|
270
270
|
return None, {
|
@@ -273,11 +273,11 @@ def _find_window_by_name(window_name: str) -> Tuple[Optional[Dict[str, int]], Di
|
|
273
273
|
"matching_method": "case_insensitive_substring",
|
274
274
|
"all_windows": window_titles
|
275
275
|
}
|
276
|
-
|
276
|
+
|
277
277
|
# Get the first matching window
|
278
278
|
window = matching_windows[0]
|
279
279
|
print(f"Found matching window: '{window.title}'")
|
280
|
-
|
280
|
+
|
281
281
|
# Check if window dimensions are valid
|
282
282
|
if window.width <= 0 or window.height <= 0:
|
283
283
|
print(f"Window has invalid dimensions: {window.width}x{window.height}")
|
@@ -287,7 +287,7 @@ def _find_window_by_name(window_name: str) -> Tuple[Optional[Dict[str, int]], Di
|
|
287
287
|
"reason": f"Invalid dimensions: {window.width}x{window.height}",
|
288
288
|
"all_windows": window_titles
|
289
289
|
}
|
290
|
-
|
290
|
+
|
291
291
|
# Return the window position and size
|
292
292
|
return {
|
293
293
|
"top": window.top,
|
@@ -314,17 +314,17 @@ def _get_active_apps_macos() -> List[str]:
|
|
314
314
|
tell application "System Events"
|
315
315
|
set appList to {}
|
316
316
|
set allProcesses to application processes
|
317
|
-
|
317
|
+
|
318
318
|
repeat with proc in allProcesses
|
319
319
|
if windows of proc is not {} then
|
320
320
|
set end of appList to name of proc
|
321
321
|
end if
|
322
322
|
end repeat
|
323
|
-
|
323
|
+
|
324
324
|
return appList
|
325
325
|
end tell
|
326
326
|
'''
|
327
|
-
|
327
|
+
|
328
328
|
result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True)
|
329
329
|
if result.returncode == 0:
|
330
330
|
# Parse the comma-separated list from AppleScript
|
@@ -346,7 +346,7 @@ def _format_error_with_available_windows(window_name: str, debug_info: Dict[str,
|
|
346
346
|
if window['name']:
|
347
347
|
window_desc += f" - '{window['name']}'"
|
348
348
|
available_windows.append(window_desc)
|
349
|
-
|
349
|
+
|
350
350
|
# Create a formatted list of available windows for the error message
|
351
351
|
windows_list = ", ".join(available_windows) if available_windows else "No windows found"
|
352
352
|
result["error"] = f"Window '{window_name}' not found. Available windows: {windows_list}"
|
@@ -368,21 +368,21 @@ def _verify_screenshot_success(output_path: str) -> bool:
|
|
368
368
|
return os.path.exists(output_path) and os.path.getsize(output_path) > 0
|
369
369
|
|
370
370
|
|
371
|
-
def _try_mss_capture(output_path: str, window_region: Optional[Dict[str, int]], result: Dict[str, Any],
|
371
|
+
def _try_mss_capture(output_path: str, window_region: Optional[Dict[str, int]], result: Dict[str, Any],
|
372
372
|
window_name: Optional[str] = None, debug_info: Optional[Dict[str, Any]] = None) -> bool:
|
373
373
|
"""
|
374
374
|
Try to capture a screenshot using MSS library.
|
375
|
-
|
375
|
+
|
376
376
|
Args:
|
377
377
|
output_path: Path where the screenshot should be saved
|
378
378
|
window_region: Region to capture (with top, left, width, height keys) or None for full screen
|
379
379
|
result: Dictionary to store error information if capture fails
|
380
380
|
window_name: Optional name of the window being captured, for error messages
|
381
381
|
debug_info: Optional debug information to include in result on failure
|
382
|
-
|
382
|
+
|
383
383
|
Returns:
|
384
384
|
bool: True if capture was successful, False otherwise
|
385
|
-
|
385
|
+
|
386
386
|
Note:
|
387
387
|
- When window_region is None, captures the full primary screen.
|
388
388
|
- Updates the result dictionary with success=True on success.
|
@@ -411,28 +411,28 @@ def _try_mss_capture(output_path: str, window_region: Optional[Dict[str, int]],
|
|
411
411
|
def _capture_screenshot_macos(output_path: str, capture_area: str = "full", window_name: Optional[str] = None) -> Dict[str, Any]:
|
412
412
|
"""
|
413
413
|
Capture screenshot on macOS.
|
414
|
-
|
414
|
+
|
415
415
|
Returns:
|
416
416
|
Dict with success status and error message if failed
|
417
417
|
"""
|
418
418
|
result = {"success": False, "error": None}
|
419
419
|
internal_debug_info = None # Store debug info internally but don't add to result yet
|
420
|
-
|
420
|
+
|
421
421
|
# Check for screen recording permission first
|
422
422
|
perm_check = _check_macos_screen_recording_permission()
|
423
423
|
if not perm_check["has_permission"]:
|
424
424
|
result["error"] = perm_check["error"]
|
425
425
|
result["_debug_info"] = perm_check["details"] # Store with underscore prefix for later use
|
426
426
|
return result
|
427
|
-
|
427
|
+
|
428
428
|
# If window_name is specified, try to capture that specific window
|
429
429
|
if window_name:
|
430
430
|
# Try to find the window using our macOS-specific function
|
431
431
|
window_region, debug_info = _find_window_by_name(window_name)
|
432
|
-
|
432
|
+
|
433
433
|
# Store debug info internally but don't add to result yet
|
434
434
|
internal_debug_info = debug_info
|
435
|
-
|
435
|
+
|
436
436
|
if window_region:
|
437
437
|
# If we have a window ID from Quartz, use it directly without activating the window
|
438
438
|
if 'id' in window_region:
|
@@ -440,7 +440,7 @@ def _capture_screenshot_macos(output_path: str, capture_area: str = "full", wind
|
|
440
440
|
# Capture using the window ID without activating the window
|
441
441
|
cmd = ["screencapture", "-l", str(window_region['id']), output_path]
|
442
442
|
process = subprocess.run(cmd, capture_output=True)
|
443
|
-
|
443
|
+
|
444
444
|
# Check if file exists and has non-zero size
|
445
445
|
if _verify_screenshot_success(output_path):
|
446
446
|
result["success"] = True
|
@@ -451,7 +451,7 @@ def _capture_screenshot_macos(output_path: str, capture_area: str = "full", wind
|
|
451
451
|
result["error"] = f"Native screencapture failed with return code {process.returncode}"
|
452
452
|
except Exception as e:
|
453
453
|
result["error"] = f"Screenshot error: {str(e)}"
|
454
|
-
|
454
|
+
|
455
455
|
# If direct window ID capture failed or no ID available, try using MSS
|
456
456
|
if _try_mss_capture(output_path, window_region, result, window_name):
|
457
457
|
# If successful, store debug info for later use
|
@@ -460,7 +460,7 @@ def _capture_screenshot_macos(output_path: str, capture_area: str = "full", wind
|
|
460
460
|
else:
|
461
461
|
# Window not found - create a more detailed error message with available windows
|
462
462
|
_format_error_with_available_windows(window_name, internal_debug_info, result)
|
463
|
-
|
463
|
+
|
464
464
|
# No fallback to capturing the active window - return the result
|
465
465
|
return result
|
466
466
|
elif capture_area == "window":
|
@@ -468,7 +468,7 @@ def _capture_screenshot_macos(output_path: str, capture_area: str = "full", wind
|
|
468
468
|
try:
|
469
469
|
cmd = ["screencapture", "-w", output_path]
|
470
470
|
process = subprocess.run(cmd, capture_output=True)
|
471
|
-
|
471
|
+
|
472
472
|
# Check if file exists and has non-zero size
|
473
473
|
if _verify_screenshot_success(output_path):
|
474
474
|
result["success"] = True
|
@@ -477,19 +477,19 @@ def _capture_screenshot_macos(output_path: str, capture_area: str = "full", wind
|
|
477
477
|
result["error"] = f"Active window capture failed with return code {process.returncode}"
|
478
478
|
except Exception as e:
|
479
479
|
result["error"] = f"Active window screenshot error: {str(e)}"
|
480
|
-
|
480
|
+
|
481
481
|
# No fallback to full screen here either
|
482
482
|
return result
|
483
|
-
|
483
|
+
|
484
484
|
# For full screen capture
|
485
485
|
if _try_mss_capture(output_path, None, result):
|
486
486
|
return result
|
487
|
-
|
487
|
+
|
488
488
|
# Fall back to native macOS screencapture for full screen only
|
489
489
|
try:
|
490
490
|
cmd = ["screencapture", "-x", output_path]
|
491
491
|
process = subprocess.run(cmd, capture_output=True)
|
492
|
-
|
492
|
+
|
493
493
|
# Check if file exists and has non-zero size
|
494
494
|
if _verify_screenshot_success(output_path):
|
495
495
|
result["success"] = True
|
@@ -498,19 +498,19 @@ def _capture_screenshot_macos(output_path: str, capture_area: str = "full", wind
|
|
498
498
|
result["error"] = f"Native screencapture failed with return code {process.returncode}"
|
499
499
|
except Exception as e:
|
500
500
|
result["error"] = f"Screenshot error: {str(e)}"
|
501
|
-
|
501
|
+
|
502
502
|
return result
|
503
503
|
|
504
504
|
|
505
505
|
def _capture_screenshot_linux(output_path: str, capture_area: str = "full", window_name: Optional[str] = None) -> Dict[str, Any]:
|
506
506
|
"""
|
507
507
|
Capture screenshot on Linux.
|
508
|
-
|
508
|
+
|
509
509
|
Returns:
|
510
510
|
Dict with success status and error message if failed
|
511
511
|
"""
|
512
512
|
result = {"success": False, "error": None}
|
513
|
-
|
513
|
+
|
514
514
|
# If window_name is specified, try to capture that specific window
|
515
515
|
if window_name:
|
516
516
|
# Try to use MSS first if available
|
@@ -529,7 +529,7 @@ def _capture_screenshot_linux(output_path: str, capture_area: str = "full", wind
|
|
529
529
|
except Exception as e:
|
530
530
|
result["error"] = f"PyGetWindow error: {str(e)}"
|
531
531
|
return result
|
532
|
-
|
532
|
+
|
533
533
|
# Try native Linux methods only if MSS is not available
|
534
534
|
if not MSS_AVAILABLE or not PYGETWINDOW_AVAILABLE:
|
535
535
|
try:
|
@@ -538,16 +538,16 @@ def _capture_screenshot_linux(output_path: str, capture_area: str = "full", wind
|
|
538
538
|
# Search for the window
|
539
539
|
find_cmd = ["xdotool", "search", "--name", window_name]
|
540
540
|
result_cmd = subprocess.run(find_cmd, capture_output=True, text=True)
|
541
|
-
|
541
|
+
|
542
542
|
if result_cmd.returncode == 0 and result_cmd.stdout.strip():
|
543
543
|
# Get the first window ID
|
544
544
|
window_id = result_cmd.stdout.strip().split('\n')[0]
|
545
|
-
|
545
|
+
|
546
546
|
# Now capture the window
|
547
547
|
if subprocess.run(["which", "gnome-screenshot"], capture_output=True).returncode == 0:
|
548
548
|
cmd = ["gnome-screenshot", "-w", "-f", output_path, "-w", window_id]
|
549
549
|
process = subprocess.run(cmd, capture_output=True)
|
550
|
-
|
550
|
+
|
551
551
|
# Check if file exists and has non-zero size
|
552
552
|
if _verify_screenshot_success(output_path):
|
553
553
|
result["success"] = True
|
@@ -557,7 +557,7 @@ def _capture_screenshot_linux(output_path: str, capture_area: str = "full", wind
|
|
557
557
|
elif subprocess.run(["which", "scrot"], capture_output=True).returncode == 0:
|
558
558
|
cmd = ["scrot", "-u", output_path]
|
559
559
|
process = subprocess.run(cmd, capture_output=True)
|
560
|
-
|
560
|
+
|
561
561
|
# Check if file exists and has non-zero size
|
562
562
|
if _verify_screenshot_success(output_path):
|
563
563
|
result["success"] = True
|
@@ -574,7 +574,7 @@ def _capture_screenshot_linux(output_path: str, capture_area: str = "full", wind
|
|
574
574
|
result["error"] = "xdotool not available for window capture"
|
575
575
|
except Exception as e:
|
576
576
|
result["error"] = f"Screenshot error: {str(e)}"
|
577
|
-
|
577
|
+
|
578
578
|
# No fallback to full screen - just return the error
|
579
579
|
return result
|
580
580
|
elif capture_area == "window":
|
@@ -583,7 +583,7 @@ def _capture_screenshot_linux(output_path: str, capture_area: str = "full", wind
|
|
583
583
|
if subprocess.run(["which", "gnome-screenshot"], capture_output=True).returncode == 0:
|
584
584
|
cmd = ["gnome-screenshot", "-w", "-f", output_path]
|
585
585
|
process = subprocess.run(cmd, capture_output=True)
|
586
|
-
|
586
|
+
|
587
587
|
# Check if file exists and has non-zero size
|
588
588
|
if _verify_screenshot_success(output_path):
|
589
589
|
result["success"] = True
|
@@ -593,7 +593,7 @@ def _capture_screenshot_linux(output_path: str, capture_area: str = "full", wind
|
|
593
593
|
elif subprocess.run(["which", "scrot"], capture_output=True).returncode == 0:
|
594
594
|
cmd = ["scrot", "-u", output_path]
|
595
595
|
process = subprocess.run(cmd, capture_output=True)
|
596
|
-
|
596
|
+
|
597
597
|
# Check if file exists and has non-zero size
|
598
598
|
if _verify_screenshot_success(output_path):
|
599
599
|
result["success"] = True
|
@@ -604,20 +604,20 @@ def _capture_screenshot_linux(output_path: str, capture_area: str = "full", wind
|
|
604
604
|
result["error"] = "No screenshot tool found (gnome-screenshot or scrot)"
|
605
605
|
except Exception as e:
|
606
606
|
result["error"] = f"Active window screenshot error: {str(e)}"
|
607
|
-
|
607
|
+
|
608
608
|
# No fallback to full screen here either
|
609
609
|
return result
|
610
|
-
|
610
|
+
|
611
611
|
# For full screen capture
|
612
612
|
if _try_mss_capture(output_path, None, result):
|
613
613
|
return result
|
614
|
-
|
614
|
+
|
615
615
|
# Fall back to native Linux methods for full screen only
|
616
616
|
try:
|
617
617
|
if subprocess.run(["which", "gnome-screenshot"], capture_output=True).returncode == 0:
|
618
618
|
cmd = ["gnome-screenshot", "-f", output_path]
|
619
619
|
process = subprocess.run(cmd, capture_output=True)
|
620
|
-
|
620
|
+
|
621
621
|
# Check if file exists and has non-zero size
|
622
622
|
if _verify_screenshot_success(output_path):
|
623
623
|
result["success"] = True
|
@@ -627,7 +627,7 @@ def _capture_screenshot_linux(output_path: str, capture_area: str = "full", wind
|
|
627
627
|
elif subprocess.run(["which", "scrot"], capture_output=True).returncode == 0:
|
628
628
|
cmd = ["scrot", output_path]
|
629
629
|
process = subprocess.run(cmd, capture_output=True)
|
630
|
-
|
630
|
+
|
631
631
|
# Check if file exists and has non-zero size
|
632
632
|
if _verify_screenshot_success(output_path):
|
633
633
|
result["success"] = True
|
@@ -638,19 +638,19 @@ def _capture_screenshot_linux(output_path: str, capture_area: str = "full", wind
|
|
638
638
|
result["error"] = "No screenshot tool found (gnome-screenshot or scrot)"
|
639
639
|
except Exception as e:
|
640
640
|
result["error"] = f"Screenshot error: {str(e)}"
|
641
|
-
|
641
|
+
|
642
642
|
return result
|
643
643
|
|
644
644
|
|
645
645
|
def _capture_screenshot_windows(output_path: str, capture_area: str = "full", window_name: Optional[str] = None) -> Dict[str, Any]:
|
646
646
|
"""
|
647
647
|
Capture screenshot on Windows.
|
648
|
-
|
648
|
+
|
649
649
|
Returns:
|
650
650
|
Dict with success status and error message if failed
|
651
651
|
"""
|
652
652
|
result = {"success": False, "error": None}
|
653
|
-
|
653
|
+
|
654
654
|
# If window_name is specified, try to capture that specific window
|
655
655
|
if window_name:
|
656
656
|
# Try to use MSS first if available
|
@@ -669,37 +669,37 @@ def _capture_screenshot_windows(output_path: str, capture_area: str = "full", wi
|
|
669
669
|
except Exception as e:
|
670
670
|
result["error"] = f"PyGetWindow error: {str(e)}"
|
671
671
|
return result
|
672
|
-
|
672
|
+
|
673
673
|
# Try native Windows methods only if MSS is not available
|
674
674
|
if not MSS_AVAILABLE or not PYGETWINDOW_AVAILABLE:
|
675
675
|
try:
|
676
676
|
script = f"""
|
677
677
|
Add-Type -AssemblyName System.Windows.Forms
|
678
678
|
Add-Type -AssemblyName System.Drawing
|
679
|
-
|
679
|
+
|
680
680
|
# Function to find window by title
|
681
681
|
function Find-Window($title) {{
|
682
682
|
$processes = Get-Process | Where-Object {{$_.MainWindowTitle -like "*$title*"}}
|
683
683
|
return $processes
|
684
684
|
}}
|
685
|
-
|
685
|
+
|
686
686
|
$targetProcess = Find-Window("{window_name}")
|
687
|
-
|
687
|
+
|
688
688
|
if ($targetProcess -and $targetProcess.Count -gt 0) {{
|
689
689
|
# Use the first matching process
|
690
690
|
$process = $targetProcess[0]
|
691
|
-
|
691
|
+
|
692
692
|
# Get window bounds
|
693
693
|
$hwnd = $process.MainWindowHandle
|
694
694
|
$rect = New-Object System.Drawing.Rectangle
|
695
695
|
[void][System.Runtime.InteropServices.Marshal]::GetWindowRect($hwnd, [ref]$rect)
|
696
|
-
|
696
|
+
|
697
697
|
# Capture the window
|
698
698
|
$bitmap = New-Object System.Drawing.Bitmap ($rect.Width - $rect.X), ($rect.Height - $rect.Y)
|
699
699
|
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
700
700
|
$graphics.CopyFromScreen($rect.X, $rect.Y, 0, 0, $bitmap.Size)
|
701
701
|
$bitmap.Save('{output_path}')
|
702
|
-
|
702
|
+
|
703
703
|
return $true
|
704
704
|
}}
|
705
705
|
else {{
|
@@ -709,10 +709,10 @@ def _capture_screenshot_windows(output_path: str, capture_area: str = "full", wi
|
|
709
709
|
return $false
|
710
710
|
}}
|
711
711
|
"""
|
712
|
-
|
712
|
+
|
713
713
|
cmd = ["powershell", "-Command", script]
|
714
714
|
process = subprocess.run(cmd, capture_output=True, text=True)
|
715
|
-
|
715
|
+
|
716
716
|
output = process.stdout.strip()
|
717
717
|
if output.startswith("True"):
|
718
718
|
# Check if file exists and has non-zero size
|
@@ -732,7 +732,7 @@ def _capture_screenshot_windows(output_path: str, capture_area: str = "full", wi
|
|
732
732
|
result["error"] = f"Window '{window_name}' not found or could not be captured"
|
733
733
|
except Exception as e:
|
734
734
|
result["error"] = f"Screenshot error: {str(e)}"
|
735
|
-
|
735
|
+
|
736
736
|
# No fallback to full screen - just return the error
|
737
737
|
return result
|
738
738
|
elif capture_area == "window":
|
@@ -741,34 +741,34 @@ def _capture_screenshot_windows(output_path: str, capture_area: str = "full", wi
|
|
741
741
|
script = f"""
|
742
742
|
Add-Type -AssemblyName System.Windows.Forms
|
743
743
|
Add-Type -AssemblyName System.Drawing
|
744
|
-
|
744
|
+
|
745
745
|
function Get-ActiveWindow {{
|
746
746
|
$foregroundWindowHandle = [System.Windows.Forms.Form]::ActiveForm.Handle
|
747
747
|
if (-not $foregroundWindowHandle) {{
|
748
748
|
# If no active form, try to get the foreground window
|
749
749
|
$foregroundWindowHandle = [System.Runtime.InteropServices.Marshal]::GetForegroundWindow()
|
750
750
|
}}
|
751
|
-
|
751
|
+
|
752
752
|
if ($foregroundWindowHandle) {{
|
753
753
|
$rect = New-Object System.Drawing.Rectangle
|
754
754
|
[void][System.Runtime.InteropServices.Marshal]::GetWindowRect($foregroundWindowHandle, [ref]$rect)
|
755
|
-
|
755
|
+
|
756
756
|
$bitmap = New-Object System.Drawing.Bitmap ($rect.Width - $rect.X), ($rect.Height - $rect.Y)
|
757
757
|
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
758
758
|
$graphics.CopyFromScreen($rect.X, $rect.Y, 0, 0, $bitmap.Size)
|
759
759
|
$bitmap.Save('{output_path}')
|
760
|
-
|
760
|
+
|
761
761
|
return $true
|
762
762
|
}}
|
763
763
|
return $false
|
764
764
|
}}
|
765
|
-
|
765
|
+
|
766
766
|
Get-ActiveWindow
|
767
767
|
"""
|
768
|
-
|
768
|
+
|
769
769
|
cmd = ["powershell", "-Command", script]
|
770
770
|
process = subprocess.run(cmd, capture_output=True, text=True)
|
771
|
-
|
771
|
+
|
772
772
|
if process.stdout.strip() == "True":
|
773
773
|
# Check if file exists and has non-zero size
|
774
774
|
if _verify_screenshot_success(output_path):
|
@@ -780,14 +780,14 @@ def _capture_screenshot_windows(output_path: str, capture_area: str = "full", wi
|
|
780
780
|
result["error"] = "Failed to capture active window"
|
781
781
|
except Exception as e:
|
782
782
|
result["error"] = f"Active window screenshot error: {str(e)}"
|
783
|
-
|
783
|
+
|
784
784
|
# No fallback to full screen here either
|
785
785
|
return result
|
786
786
|
else:
|
787
787
|
# For full screen capture
|
788
788
|
if _try_mss_capture(output_path, None, result):
|
789
789
|
return result
|
790
|
-
|
790
|
+
|
791
791
|
# Fall back to native Windows methods for full screen only
|
792
792
|
try:
|
793
793
|
script = f"""
|
@@ -799,10 +799,10 @@ def _capture_screenshot_windows(output_path: str, capture_area: str = "full", wi
|
|
799
799
|
$graphics.CopyFromScreen($screen.X, $screen.Y, 0, 0, $screen.Size)
|
800
800
|
$bitmap.Save('{output_path}')
|
801
801
|
"""
|
802
|
-
|
802
|
+
|
803
803
|
cmd = ["powershell", "-Command", script]
|
804
804
|
process = subprocess.run(cmd, capture_output=True)
|
805
|
-
|
805
|
+
|
806
806
|
# Check if file exists and has non-zero size
|
807
807
|
if _verify_screenshot_success(output_path):
|
808
808
|
result["success"] = True
|
@@ -811,7 +811,7 @@ def _capture_screenshot_windows(output_path: str, capture_area: str = "full", wi
|
|
811
811
|
result["error"] = f"PowerShell screenshot failed with return code {process.returncode}"
|
812
812
|
except Exception as e:
|
813
813
|
result["error"] = f"Screenshot error: {str(e)}"
|
814
|
-
|
814
|
+
|
815
815
|
return result
|
816
816
|
|
817
817
|
|
@@ -820,26 +820,26 @@ def find_macos_window_by_name(window_name):
|
|
820
820
|
try:
|
821
821
|
if not QUARTZ_AVAILABLE:
|
822
822
|
return None, {"error": "Quartz not available"}
|
823
|
-
|
823
|
+
|
824
824
|
window_list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID)
|
825
|
-
|
825
|
+
|
826
826
|
# Collect debug info instead of printing
|
827
827
|
debug_info = {
|
828
828
|
"search_term": window_name,
|
829
829
|
"available_windows": []
|
830
830
|
}
|
831
|
-
|
831
|
+
|
832
832
|
all_windows = []
|
833
833
|
for window in window_list:
|
834
834
|
name = window.get('kCGWindowName', '')
|
835
835
|
owner = window.get('kCGWindowOwnerName', '')
|
836
836
|
layer = window.get('kCGWindowLayer', 0)
|
837
837
|
window_id = window.get('kCGWindowNumber', 0)
|
838
|
-
|
838
|
+
|
839
839
|
# Skip windows with layer > 0 (typically system UI elements)
|
840
840
|
if layer > 0:
|
841
841
|
continue
|
842
|
-
|
842
|
+
|
843
843
|
window_info = {
|
844
844
|
"id": window_id,
|
845
845
|
"name": name,
|
@@ -847,7 +847,7 @@ def find_macos_window_by_name(window_name):
|
|
847
847
|
"layer": layer
|
848
848
|
}
|
849
849
|
debug_info["available_windows"].append(window_info)
|
850
|
-
|
850
|
+
|
851
851
|
all_windows.append({
|
852
852
|
'id': window_id,
|
853
853
|
'name': name,
|
@@ -855,28 +855,28 @@ def find_macos_window_by_name(window_name):
|
|
855
855
|
'layer': layer,
|
856
856
|
'bounds': window.get('kCGWindowBounds', {})
|
857
857
|
})
|
858
|
-
|
858
|
+
|
859
859
|
# Define matching categories with different priorities
|
860
860
|
exact_app_matches = [] # Exact match on application name
|
861
861
|
exact_window_matches = [] # Exact match on window title
|
862
862
|
app_contains_matches = [] # Application name contains search term
|
863
863
|
window_contains_matches = [] # Window title contains search term
|
864
|
-
|
864
|
+
|
865
865
|
# Normalize the search term for comparison
|
866
866
|
search_term_lower = window_name.lower()
|
867
|
-
|
867
|
+
|
868
868
|
# First pass: categorize windows by match quality
|
869
869
|
for window in all_windows:
|
870
870
|
name = window['name'] or ''
|
871
871
|
owner = window['owner'] or ''
|
872
|
-
|
872
|
+
|
873
873
|
# Skip empty windows
|
874
874
|
if not name and not owner:
|
875
875
|
continue
|
876
|
-
|
876
|
+
|
877
877
|
name_lower = name.lower()
|
878
878
|
owner_lower = owner.lower()
|
879
|
-
|
879
|
+
|
880
880
|
# Check for exact matches first (case-insensitive)
|
881
881
|
if owner_lower == search_term_lower:
|
882
882
|
exact_app_matches.append(window)
|
@@ -887,7 +887,7 @@ def find_macos_window_by_name(window_name):
|
|
887
887
|
app_contains_matches.append(window)
|
888
888
|
elif search_term_lower in name_lower:
|
889
889
|
window_contains_matches.append(window)
|
890
|
-
|
890
|
+
|
891
891
|
# Process matches in priority order
|
892
892
|
for match_list, reason in [
|
893
893
|
(exact_app_matches, "Exact match on application name"),
|
@@ -906,7 +906,7 @@ def find_macos_window_by_name(window_name):
|
|
906
906
|
"layer": selected_window['layer'],
|
907
907
|
"selection_reason": reason
|
908
908
|
}
|
909
|
-
|
909
|
+
|
910
910
|
bounds = selected_window['bounds']
|
911
911
|
return {
|
912
912
|
'id': selected_window['id'],
|
@@ -915,7 +915,7 @@ def find_macos_window_by_name(window_name):
|
|
915
915
|
'width': bounds.get('Width', 0),
|
916
916
|
'height': bounds.get('Height', 0)
|
917
917
|
}, debug_info
|
918
|
-
|
918
|
+
|
919
919
|
debug_info["error"] = f"No matching window found for '{window_name}'"
|
920
920
|
return None, debug_info
|
921
921
|
except Exception as e:
|
@@ -925,7 +925,7 @@ def find_macos_window_by_name(window_name):
|
|
925
925
|
def capture_screenshot(output_path: Optional[str] = None, capture_mode: Optional[Dict[str, str]] = None, debug: bool = False) -> Dict[str, Any]:
|
926
926
|
"""
|
927
927
|
Capture a screenshot and save it to the specified path.
|
928
|
-
|
928
|
+
|
929
929
|
Args:
|
930
930
|
output_path: Path where the screenshot should be saved. If None, a default path will be used.
|
931
931
|
capture_mode: Dictionary specifying what to capture:
|
@@ -933,47 +933,61 @@ def capture_screenshot(output_path: Optional[str] = None, capture_mode: Optional
|
|
933
933
|
- window_name: Name of window to capture (required when type is 'named_window')
|
934
934
|
Windows can be captured in the background without bringing them to the front.
|
935
935
|
debug: Whether to include debug information in the response on failure
|
936
|
-
|
936
|
+
|
937
937
|
Returns:
|
938
938
|
Dictionary with success status and path to the saved screenshot.
|
939
939
|
"""
|
940
940
|
# Set defaults if capture_mode is not provided
|
941
941
|
if not capture_mode:
|
942
942
|
capture_mode = {"type": "full"}
|
943
|
-
|
943
|
+
|
944
944
|
# Extract capture type and window name
|
945
945
|
capture_type = capture_mode.get("type", "full")
|
946
946
|
window_name = capture_mode.get("window_name") if capture_type == "named_window" else None
|
947
|
-
|
947
|
+
|
948
948
|
if debug:
|
949
949
|
print(f"Capture mode: {capture_type}")
|
950
950
|
if window_name:
|
951
951
|
print(f"Window name: {window_name}")
|
952
|
-
|
952
|
+
|
953
953
|
# Use default path if none provided
|
954
954
|
if not output_path:
|
955
955
|
output_path = _get_default_screenshot_path()
|
956
|
-
|
957
|
-
#
|
958
|
-
os.
|
959
|
-
|
956
|
+
|
957
|
+
# Handle relative paths with respect to allowed directory
|
958
|
+
if not os.path.isabs(output_path) and hasattr(state, 'allowed_directory') and state.allowed_directory:
|
959
|
+
full_output_path = os.path.abspath(os.path.join(state.allowed_directory, output_path))
|
960
|
+
else:
|
961
|
+
full_output_path = os.path.abspath(output_path)
|
962
|
+
|
963
|
+
# Security check
|
964
|
+
if hasattr(state, 'allowed_directory') and state.allowed_directory:
|
965
|
+
if not full_output_path.startswith(state.allowed_directory):
|
966
|
+
return {
|
967
|
+
"success": False,
|
968
|
+
"error": f"Access denied: Path ({full_output_path}) must be within allowed directory"
|
969
|
+
}
|
970
|
+
|
971
|
+
# Ensure the output directory exists, creating it relative to the allowed directory
|
972
|
+
os.makedirs(os.path.dirname(full_output_path), exist_ok=True)
|
973
|
+
|
960
974
|
# Convert to old parameters for compatibility with existing functions
|
961
975
|
capture_area = "window" if capture_type in ["active_window", "named_window"] else "full"
|
962
|
-
|
976
|
+
|
963
977
|
# Capture screenshot based on platform
|
964
978
|
system_name = platform.system().lower()
|
965
979
|
if debug:
|
966
980
|
print(f"Detected platform: {system_name}")
|
967
|
-
|
981
|
+
|
968
982
|
if system_name == "darwin" or system_name == "macos":
|
969
|
-
result = _capture_screenshot_macos(
|
983
|
+
result = _capture_screenshot_macos(full_output_path, capture_area, window_name)
|
970
984
|
elif system_name == "linux":
|
971
|
-
result = _capture_screenshot_linux(
|
985
|
+
result = _capture_screenshot_linux(full_output_path, capture_area, window_name)
|
972
986
|
elif system_name == "windows":
|
973
|
-
result = _capture_screenshot_windows(
|
987
|
+
result = _capture_screenshot_windows(full_output_path, capture_area, window_name)
|
974
988
|
else:
|
975
989
|
result = {"success": False, "error": f"Unsupported platform: {system_name}"}
|
976
|
-
|
990
|
+
|
977
991
|
# Check if the error might be related to permission issues
|
978
992
|
if not result["success"] and result.get("error"):
|
979
993
|
# If the error already mentions permission, highlight it
|
@@ -981,29 +995,29 @@ def capture_screenshot(output_path: Optional[str] = None, capture_mode: Optional
|
|
981
995
|
# Make the error message more prominent for permission issues
|
982
996
|
modified_message = f"PERMISSION ERROR: {result['error']}"
|
983
997
|
result["error"] = modified_message
|
984
|
-
|
998
|
+
|
985
999
|
# Add additional hints for macOS
|
986
1000
|
if system_name == "darwin":
|
987
1001
|
result["error"] += " To fix this: Open System Settings > Privacy & Security > Screen Recording, and enable permission for this application."
|
988
|
-
|
1002
|
+
|
989
1003
|
# Extract debug info if present
|
990
1004
|
debug_info = result.pop("_debug_info", None) if "_debug_info" in result else None
|
991
|
-
|
1005
|
+
|
992
1006
|
# Format the final result
|
993
1007
|
response = {
|
994
1008
|
"success": result["success"],
|
995
|
-
"path":
|
1009
|
+
"path": full_output_path if result["success"] else None,
|
996
1010
|
"message": "Screenshot captured successfully" if result["success"] else result.get("error", "Failed to capture screenshot")
|
997
1011
|
}
|
998
|
-
|
1012
|
+
|
999
1013
|
# Add warning if present
|
1000
1014
|
if "warning" in result:
|
1001
1015
|
response["warning"] = result["warning"]
|
1002
|
-
|
1016
|
+
|
1003
1017
|
# Only include debug info if debug mode is enabled AND the operation failed
|
1004
1018
|
if debug and not result["success"] and debug_info:
|
1005
1019
|
response["debug_info"] = debug_info
|
1006
|
-
|
1020
|
+
|
1007
1021
|
return response
|
1008
1022
|
|
1009
1023
|
|
@@ -1011,11 +1025,11 @@ async def handle_capture_screenshot(arguments: dict) -> List[types.TextContent]:
|
|
1011
1025
|
"""Handle capturing a screenshot."""
|
1012
1026
|
output_path = arguments.get("output_path")
|
1013
1027
|
debug = arguments.get("debug", False)
|
1014
|
-
|
1028
|
+
|
1015
1029
|
# Handle legacy platform parameter (ignore it)
|
1016
1030
|
if "platform" in arguments:
|
1017
1031
|
print("Note: 'platform' parameter is deprecated and will be auto-detected")
|
1018
|
-
|
1032
|
+
|
1019
1033
|
# Enforce new parameter format requiring capture_mode
|
1020
1034
|
capture_mode = arguments.get("capture_mode")
|
1021
1035
|
if not capture_mode:
|
@@ -1024,6 +1038,23 @@ async def handle_capture_screenshot(arguments: dict) -> List[types.TextContent]:
|
|
1024
1038
|
"error": "Missing required parameter 'capture_mode'. Please provide a capture_mode object with 'type' field."
|
1025
1039
|
}
|
1026
1040
|
else:
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1041
|
+
# Ensure output_path is properly resolved within allowed directory
|
1042
|
+
if output_path:
|
1043
|
+
# Resolve using allowed directory
|
1044
|
+
if os.path.isabs(output_path):
|
1045
|
+
# If path is absolute, just use it directly (security check is done in capture_screenshot)
|
1046
|
+
resolved_path = output_path
|
1047
|
+
else:
|
1048
|
+
# For relative paths, resolve against the allowed directory
|
1049
|
+
resolved_path = os.path.join(state.allowed_directory, output_path)
|
1050
|
+
|
1051
|
+
# Create parent directory if needed
|
1052
|
+
dir_path = os.path.dirname(resolved_path)
|
1053
|
+
if not os.path.exists(dir_path):
|
1054
|
+
os.makedirs(dir_path, exist_ok=True)
|
1055
|
+
|
1056
|
+
result = capture_screenshot(resolved_path, capture_mode, debug)
|
1057
|
+
else:
|
1058
|
+
result = capture_screenshot(output_path, capture_mode, debug)
|
1059
|
+
|
1060
|
+
return [types.TextContent(type="text", text=json.dumps(result, indent=2))]
|