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.
@@ -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
- # Ensure the output directory exists
958
- os.makedirs(os.path.dirname(output_path), exist_ok=True)
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(output_path, capture_area, window_name)
983
+ result = _capture_screenshot_macos(full_output_path, capture_area, window_name)
970
984
  elif system_name == "linux":
971
- result = _capture_screenshot_linux(output_path, capture_area, window_name)
985
+ result = _capture_screenshot_linux(full_output_path, capture_area, window_name)
972
986
  elif system_name == "windows":
973
- result = _capture_screenshot_windows(output_path, capture_area, window_name)
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": output_path if result["success"] else None,
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
- result = capture_screenshot(output_path, capture_mode, debug)
1028
-
1029
- return [types.TextContent(type="text", text=json.dumps(result, indent=2))]
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))]