Skip to main content
Glama

Peekaboo MCP

by steipete
DialogCommand.swiftβ€’14.4 kB
import ApplicationServices import ArgumentParser import AXorcist import Foundation import PeekabooCore /// Interact with system dialogs and alerts struct DialogCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "dialog", abstract: "Interact with system dialogs and alerts", discussion: """ EXAMPLES: # Click a button in a dialog peekaboo dialog click --button "OK" peekaboo dialog click --button "Don't Save" # Type in a dialog text field peekaboo dialog input --text "password123" --field "Password" # Handle file dialogs peekaboo dialog file --path "/Users/me/Documents/file.txt" peekaboo dialog file --name "report.pdf" --select "Save" # Dismiss dialogs peekaboo dialog dismiss peekaboo dialog dismiss --force # Press Escape """, subcommands: [ ClickSubcommand.self, InputSubcommand.self, FileSubcommand.self, DismissSubcommand.self, ListSubcommand.self, ] ) // MARK: - Click Dialog Button struct ClickSubcommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "click", abstract: "Click a button in a dialog using DialogService" ) @Option(help: "Button text to click (e.g., 'OK', 'Cancel', 'Save')") var button: String @Option(help: "Specific window/sheet title to target") var window: String? @Flag(help: "Output in JSON format") var jsonOutput = false @MainActor mutating func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) do { // Click the button using the service let result = try await PeekabooServices.shared.dialogs.clickButton( buttonText: self.button, windowTitle: self.window ) // Output result if self.jsonOutput { struct DialogClickResult: Codable { let action: String let button: String let window: String } let outputData = DialogClickResult( action: "dialog_click", button: result.details["button"] ?? self.button, window: result.details["window"] ?? "Dialog" ) outputSuccessCodable(data: outputData) } else { print("βœ“ Clicked '\(result.details["button"] ?? self.button)' button") } } catch let error as DialogError { handleDialogServiceError(error, jsonOutput: jsonOutput) throw ExitCode(1) } catch { handleGenericError(error, jsonOutput: self.jsonOutput) throw ExitCode(1) } } } // MARK: - Input Text in Dialog struct InputSubcommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "input", abstract: "Enter text in a dialog field using DialogService" ) @Option(help: "Text to enter") var text: String @Option(help: "Field label or placeholder to target") var field: String? @Option(help: "Field index (0-based) if multiple fields") var index: Int? @Flag(help: "Clear existing text first") var clear = false @Flag(help: "Output in JSON format") var jsonOutput = false @MainActor mutating func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) do { // Determine field identifier (index or label) let fieldIdentifier = self.field ?? self.index.map { String($0) } // Enter text using the service let result = try await PeekabooServices.shared.dialogs.enterText( text: self.text, fieldIdentifier: fieldIdentifier, clearExisting: self.clear, windowTitle: nil ) // Output result if self.jsonOutput { struct DialogInputResult: Codable { let action: String let field: String let textLength: String let cleared: String } let outputData = DialogInputResult( action: "dialog_input", field: result.details["field"] ?? "Text Field", textLength: result.details["text_length"] ?? String(self.text.count), cleared: result.details["cleared"] ?? String(self.clear) ) outputSuccessCodable(data: outputData) } else { print("βœ“ Entered text in '\(result.details["field"] ?? "field")'") } } catch let error as DialogError { handleDialogServiceError(error, jsonOutput: jsonOutput) throw ExitCode(1) } catch { handleGenericError(error, jsonOutput: self.jsonOutput) throw ExitCode(1) } } } // MARK: - Handle File Dialog struct FileSubcommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "file", abstract: "Handle file save/open dialogs using DialogService" ) @Option(help: "Full file path to navigate to") var path: String? @Option(help: "File name to enter (for save dialogs)") var name: String? @Option(help: "Button to click after entering path/name") var select: String = "Save" @Flag(help: "Output in JSON format") var jsonOutput = false @MainActor mutating func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) do { // Handle file dialog using the service let result = try await PeekabooServices.shared.dialogs.handleFileDialog( path: self.path, filename: self.name, actionButton: self.select ) // Output result if self.jsonOutput { struct FileDialogResult: Codable { let action: String let path: String? let name: String? let buttonClicked: String } let outputData = FileDialogResult( action: "file_dialog", path: result.details["path"], name: result.details["filename"], buttonClicked: result.details["button_clicked"] ?? self.select ) outputSuccessCodable(data: outputData) } else { print("βœ“ Handled file dialog") if let p = result.details["path"] { print(" Path: \(p)") } if let n = result.details["filename"] { print(" Name: \(n)") } print(" Action: \(result.details["button_clicked"] ?? self.select)") } } catch let error as DialogError { handleDialogServiceError(error, jsonOutput: jsonOutput) throw ExitCode(1) } catch { handleGenericError(error, jsonOutput: self.jsonOutput) throw ExitCode(1) } } } // MARK: - Dismiss Dialog struct DismissSubcommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "dismiss", abstract: "Dismiss a dialog using DialogService" ) @Flag(help: "Force dismiss with Escape key") var force = false @Option(help: "Specific window/sheet title to target") var window: String? @Flag(help: "Output in JSON format") var jsonOutput = false @MainActor mutating func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) do { // Dismiss dialog using the service let result = try await PeekabooServices.shared.dialogs.dismissDialog( force: self.force, windowTitle: self.window ) // Output result if self.jsonOutput { struct DialogDismissResult: Codable { let action: String let method: String let button: String? } let outputData = DialogDismissResult( action: "dialog_dismiss", method: result.details["method"] ?? "unknown", button: result.details["button"] ) outputSuccessCodable(data: outputData) } else { if result.details["method"] == "escape" { print("βœ“ Dismissed dialog with Escape") } else if let button = result.details["button"] { print("βœ“ Dismissed dialog by clicking '\(button)'") } else { print("βœ“ Dismissed dialog") } } } catch let error as DialogError { handleDialogServiceError(error, jsonOutput: jsonOutput) throw ExitCode(1) } catch { handleGenericError(error, jsonOutput: self.jsonOutput) throw ExitCode(1) } } } // MARK: - List Dialog Elements struct ListSubcommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "list", abstract: "List elements in current dialog using DialogService" ) @Flag(help: "Output in JSON format") var jsonOutput = false @MainActor func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) do { // List dialog elements using the service let elements = try await PeekabooServices.shared.dialogs.listDialogElements(windowTitle: nil) // Output result if self.jsonOutput { struct DialogListResult: Codable { let title: String let role: String let buttons: [String] let textFields: [TextField] let textElements: [String] struct TextField: Codable { let title: String let value: String let placeholder: String } } let textFields = elements.textFields.map { field in DialogListResult.TextField( title: field.title ?? "", value: field.value ?? "", placeholder: field.placeholder ?? "" ) } let outputData = DialogListResult( title: elements.dialogInfo.title, role: elements.dialogInfo.role, buttons: elements.buttons.map(\.title), textFields: textFields, textElements: elements.staticTexts ) outputSuccessCodable(data: outputData) } else { print("Dialog: \(elements.dialogInfo.title)") if !elements.buttons.isEmpty { print("\nButtons:") elements.buttons.forEach { print(" β€’ \($0.title)") } } if !elements.textFields.isEmpty { print("\nText Fields:") for field in elements.textFields { let title = field.title ?? "Untitled" let placeholder = field.placeholder ?? "" print(" β€’ \(title) [\(placeholder)]") } } if !elements.staticTexts.isEmpty { print("\nText:") elements.staticTexts.forEach { print(" \($0)") } } } } catch let error as DialogError { handleDialogServiceError(error, jsonOutput: jsonOutput) throw ExitCode(1) } catch { handleGenericError(error, jsonOutput: self.jsonOutput) throw ExitCode(1) } } } } // MARK: - Error Handling private func handleDialogServiceError(_ error: DialogError, jsonOutput: Bool) { let errorCode: ErrorCode = switch error { case .noActiveDialog: .NO_ACTIVE_DIALOG case .dialogNotFound: .ELEMENT_NOT_FOUND case .noFileDialog: .ELEMENT_NOT_FOUND case .buttonNotFound: .ELEMENT_NOT_FOUND case .fieldNotFound: .ELEMENT_NOT_FOUND case .invalidFieldIndex: .INVALID_INPUT case .noTextFields: .ELEMENT_NOT_FOUND case .noDismissButton: .ELEMENT_NOT_FOUND } if jsonOutput { let response = JSONResponse( success: false, error: ErrorInfo( message: error.localizedDescription, code: errorCode ) ) outputJSON(response) } else { fputs("❌ \(error.localizedDescription)\n", stderr) } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/steipete/Peekaboo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server