diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce87d61..ac501cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,5 +37,8 @@ jobs: restore-keys: | ${{ runner.os }}-xcode-${{ matrix.xcode }}-derived- + - name: Lint + run: swift format lint --strict --recursive . + - name: Build run: xcodebuild -quiet -scheme iMCP -configuration Debug -destination "platform=macOS" build diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..a0de283 --- /dev/null +++ b/.swift-format @@ -0,0 +1,15 @@ +{ + "version": 1, + "indentation": { + "spaces": 4 + }, + "lineLength": 120, + "maximumBlankLines": 1, + "respectsExistingLineBreaks": true, + "lineBreakBeforeEachArgument": true, + "multiElementCollectionTrailingCommas": true, + "spacesAroundRangeFormationOperators": true, + "rules": { + "AlwaysUseLowerCamelCase": false + } +} \ No newline at end of file diff --git a/App/Controllers/ServerController.swift b/App/Controllers/ServerController.swift index 1964ac9..b5dbbda 100644 --- a/App/Controllers/ServerController.swift +++ b/App/Controllers/ServerController.swift @@ -160,10 +160,10 @@ final class ServerController: ObservableObject { @AppStorage("captureEnabled") private var captureEnabled = false @AppStorage("contactsEnabled") private var contactsEnabled = false @AppStorage("locationEnabled") private var locationEnabled = false - @AppStorage("mapsEnabled") private var mapsEnabled = true // Default for maps + @AppStorage("mapsEnabled") private var mapsEnabled = true // Default enabled @AppStorage("messagesEnabled") private var messagesEnabled = false @AppStorage("remindersEnabled") private var remindersEnabled = false - @AppStorage("utilitiesEnabled") private var utilitiesEnabled = true // Default for utilities + @AppStorage("utilitiesEnabled") private var utilitiesEnabled = true // Default enabled @AppStorage("weatherEnabled") private var weatherEnabled = false // MARK: - AppStorage for Trusted Clients @@ -252,7 +252,7 @@ final class ServerController: ObservableObject { init() { Task { - // Set initial bindings before starting the server, using own @AppStorage values + // Initialize bindings from AppStorage before the server starts. await networkManager.updateServiceBindings(self.currentServiceBindings) await self.networkManager.start() self.updateServerStatus("Running") @@ -265,7 +265,7 @@ final class ServerController: ObservableObject { log.debug("ServerManager: Approval handler called for client \(clientInfo.name)") - // Create a continuation to wait for the user's response + // Bridge approval UI actions back into the async handler. return await withCheckedContinuation { continuation in let resumeGate = ResumeGate() let resumeOnce: (Bool) async -> Void = { value in @@ -290,8 +290,7 @@ final class ServerController: ObservableObject { } func updateServiceBindings(_ bindings: [String: Binding]) async { - // This function is still called by ContentView's onChange when user toggles services. - // It ensures ServerNetworkManager is updated and clients are notified. + // Called by the UI when service toggles change. await networkManager.updateServiceBindings(bindings) } @@ -337,16 +336,18 @@ final class ServerController: ObservableObject { } private func showConnectionApprovalAlert( - clientID: String, approve: @escaping () -> Void, deny: @escaping () -> Void + clientID: String, + approve: @escaping () -> Void, + deny: @escaping () -> Void ) { log.notice("Connection approval requested for client: \(clientID)") - // Check if this client is already trusted + // Trusted clients auto-approve without showing the dialog. if isClientTrusted(clientID) { log.notice("Client \(clientID) is already trusted, auto-approving") approve() - // Send notification for trusted connections + // Notify the user on auto-approved connections. sendClientConnectionNotification(clientName: clientID) return @@ -354,7 +355,7 @@ final class ServerController: ObservableObject { self.pendingConnectionID = clientID - // Check if there's already an active dialog for this client + // Coalesce concurrent approvals for the same client. guard !activeApprovalDialogs.contains(clientID) else { log.info("Adding to pending approvals for client: \(clientID)") pendingApprovals.append((clientID, approve, deny)) @@ -363,7 +364,7 @@ final class ServerController: ObservableObject { activeApprovalDialogs.insert(clientID) - // Set up the SwiftUI approval dialog + // Present the approval window and wire callbacks. pendingClientName = clientID currentApprovalHandlers = (approve: approve, deny: deny) @@ -373,7 +374,7 @@ final class ServerController: ObservableObject { if alwaysTrust { self.addTrustedClient(clientID) - // Request notification permissions so that the user can be notified when a trusted client connects + // Ask for notification permission to alert on future trusted connections. UNUserNotificationCenter.current().requestAuthorization(options: [ .alert, .sound, .badge, ]) { granted, error in @@ -399,15 +400,12 @@ final class ServerController: ObservableObject { ) NSApp.activate(ignoringOtherApps: true) - - // Handle any pending approvals for the same client after this one completes - // We'll check for pending approvals when the dialog is dismissed } } // MARK: - Connection Management Components -/// Manages a single MCP connection +// Manages a single MCP connection. actor MCPConnectionManager { private let connectionID: UUID private let connection: NWConnection @@ -426,7 +424,7 @@ actor MCPConnectionManager { bufferConfig: .unlimited ) - // Create the MCP server + // MCP server instance for this connection. self.server = MCP.Server( name: Bundle.main.name ?? "iMCP", version: Bundle.main.shortVersionString ?? "unknown", @@ -444,7 +442,7 @@ actor MCPConnectionManager { log.info("Received initialize request from client: \(clientInfo.name)") - // Request user approval + // Request user approval for the connection. let approved = await approvalHandler(clientInfo) log.info( "Approval result for connection \(connectionID): \(approved ? "Approved" : "Denied")" @@ -458,10 +456,10 @@ actor MCPConnectionManager { log.notice("MCP Server started successfully for connection: \(self.connectionID)") - // Register handlers after successful approval + // Register handlers after successful approval. await registerHandlers() - // Monitor connection health + // Monitor connection health for early disconnects. await startHealthMonitoring() } catch { log.error("Failed to start MCP server: \(error.localizedDescription)") @@ -474,7 +472,7 @@ actor MCPConnectionManager { } private func startHealthMonitoring() async { - // Set up a connection health monitoring task + // Monitor until the manager stops or the connection fails. Task { outer: while await parentManager.isRunning() { switch connection.state { @@ -494,7 +492,6 @@ actor MCPConnectionManager { log.debug("Connection \(self.connectionID) in unknown state, skipping") } - // Check again after 30 seconds try? await Task.sleep(nanoseconds: 30_000_000_000) // 30 seconds } } @@ -507,7 +504,7 @@ actor MCPConnectionManager { } catch { log.error("Failed to notify client of tool list change: \(error)") - // If the error is related to connection issues, clean up the connection + // Clean up if the underlying NWConnection is closed. if let nwError = error as? NWError, nwError.errorCode == 57 || nwError.errorCode == 54 { @@ -523,7 +520,7 @@ actor MCPConnectionManager { } } -/// Manages Bonjour service discovery and advertisement +// Manages Bonjour service discovery and advertisement. actor NetworkDiscoveryManager { private let serviceType: String private let serviceDomain: String @@ -534,7 +531,7 @@ actor NetworkDiscoveryManager { self.serviceType = serviceType self.serviceDomain = serviceDomain - // Set up network parameters + // Local-only Bonjour advertisement. let parameters = NWParameters.tcp parameters.acceptLocalOnly = true parameters.includePeerToPeer = false @@ -545,11 +542,11 @@ actor NetworkDiscoveryManager { tcpOptions.version = .v4 } - // Create the listener with service discovery + // Listen and advertise via Bonjour. self.listener = try NWListener(using: parameters) self.listener.service = NWListener.Service(type: serviceType, domain: serviceDomain) - // Set up browser for debugging/monitoring + // Browser is used for monitoring and diagnostics. self.browser = NWBrowser( for: .bonjour(type: serviceType, domain: serviceDomain), using: parameters @@ -562,13 +559,10 @@ actor NetworkDiscoveryManager { stateHandler: @escaping @Sendable (NWListener.State) -> Void, connectionHandler: @escaping @Sendable (NWConnection) -> Void ) { - // Set up state handler listener.stateUpdateHandler = stateHandler - // Set up connection handler listener.newConnectionHandler = connectionHandler - // Start the listener and browser listener.start(queue: .main) browser.start(queue: .main) @@ -582,10 +576,9 @@ actor NetworkDiscoveryManager { } func restartWithRandomPort() async throws { - // Cancel the current listener listener.cancel() - // Create new parameters with a random port + // Recreate listener on an ephemeral port. let parameters: NWParameters = NWParameters.tcp // Explicit type parameters.acceptLocalOnly = true parameters.includePeerToPeer = false @@ -596,12 +589,10 @@ actor NetworkDiscoveryManager { tcpOptions.version = .v4 } - // Create a new listener with the updated parameters - let newListener: NWListener = try NWListener(using: parameters) // Explicit type - let service = NWListener.Service(type: self.serviceType, domain: self.serviceDomain) // Explicitly create service + let newListener: NWListener = try NWListener(using: parameters) + let service = NWListener.Service(type: self.serviceType, domain: self.serviceDomain) newListener.service = service - // Update the state handler and connection handler if let currentStateHandler = listener.stateUpdateHandler { newListener.stateUpdateHandler = currentStateHandler } @@ -610,10 +601,9 @@ actor NetworkDiscoveryManager { newListener.newConnectionHandler = currentConnectionHandler } - // Start the new listener newListener.start(queue: .main) - self.listener = newListener // Update the instance member + self.listener = newListener log.notice("Restarted listener with a dynamic port") } @@ -630,7 +620,6 @@ actor ServerNetworkManager { typealias ConnectionApprovalHandler = @Sendable (UUID, MCP.Client.Info) async -> Bool private var connectionApprovalHandler: ConnectionApprovalHandler? - // Use ServiceRegistry for services private let services = ServiceRegistry.services private var serviceBindings: [String: Binding] = [:] @@ -663,7 +652,6 @@ actor ServerNetworkManager { return } - // Configure listener state handler await discoveryManager.start( stateHandler: { [weak self] (state: NWListener.State) -> Void in guard let strongSelf = self else { return } @@ -681,14 +669,12 @@ actor ServerNetworkManager { } ) - // Start a monitoring task to check service health periodically + // Monitor listener health and auto-restart if it stops advertising. Task { - while self.isRunningState { // Explicit self. - // Check if the listener is in a ready state - if let currentDM = self.discoveryManager, // Explicit self. - self.isRunningState // Ensure still running before proceeding + while self.isRunningState { + if let currentDM = self.discoveryManager, + self.isRunningState { - // Fetch the state of the listener explicitly. let listenerState: NWListener.State = await currentDM.listener.state if listenerState != .ready { @@ -713,7 +699,6 @@ actor ServerNetworkManager { } } - // Sleep for 10 seconds before checking again try? await Task.sleep(nanoseconds: 10_000_000_000) // 10s } } @@ -728,14 +713,12 @@ actor ServerNetworkManager { case .waiting(let error): log.warning("Server waiting: \(error)") - // If the port is already in use, try to restart with a different port + // If the port is already in use, try a new one. if error.errorCode == 48 { log.error("Port already in use, will try to restart service") - // Wait a bit and restart try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds - // Try to restart with a different port if isRunningState { try? await discoveryManager?.restartWithRandomPort() } @@ -743,12 +726,11 @@ actor ServerNetworkManager { case .failed(let error): log.error("Server failed: \(error)") - // Attempt recovery + // Attempt recovery after a brief delay. if isRunningState { log.info("Attempting to recover from server failure") try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second - // Try to restart the listener try? await discoveryManager?.restartWithRandomPort() } case .cancelled: @@ -762,7 +744,6 @@ actor ServerNetworkManager { log.info("Stopping network manager") isRunningState = false - // Stop all connections for (id, connectionManager) in connections { log.debug("Stopping connection: \(id)") await connectionManager.stop() @@ -773,62 +754,52 @@ actor ServerNetworkManager { connectionTasks.removeAll() pendingConnections.removeAll() - // Stop discovery await discoveryManager?.stop() } func removeConnection(_ id: UUID) async { log.debug("Removing connection: \(id)") - // Stop the connection manager if let connectionManager = connections[id] { await connectionManager.stop() } - // Cancel any associated tasks if let task = connectionTasks[id] { task.cancel() } - // Remove from all collections connections.removeValue(forKey: id) connectionTasks.removeValue(forKey: id) pendingConnections.removeValue(forKey: id) } - // Handle new incoming connections + // Handle new incoming connections. private func handleNewConnection(_ connection: NWConnection) async { let connectionID = UUID() log.info("Handling new connection: \(connectionID)") - // Create a connection manager let connectionManager = MCPConnectionManager( connectionID: connectionID, connection: connection, parentManager: self ) - // Store the connection manager connections[connectionID] = connectionManager - // Start a task to monitor connection state + // Drive the MCP handshake and approval flow. let task = Task { - // Ensure this task is removed from the registry upon completion (success or handled failure) - // so the timeout logic below doesn't act on an already completed task. + // Ensure this task is removed so the timeout logic doesn't fire afterward. defer { - // This runs on ServerNetworkManager's actor context self.connectionTasks.removeValue(forKey: connectionID) } do { - // Set up the connection approval handler guard let approvalHandler = self.connectionApprovalHandler else { log.error("No connection approval handler set, rejecting connection") await removeConnection(connectionID) return } - // Start the MCP server with our approval handler try await connectionManager.start { clientInfo in await approvalHandler(connectionID, clientInfo) } @@ -840,20 +811,16 @@ actor ServerNetworkManager { } } - // Store the task connectionTasks[connectionID] = task - // Set up a timeout to ensure the connection becomes ready in a reasonable time + // Time out stalled setups to avoid orphaned connections. Task { try? await Task.sleep(nanoseconds: 10_000_000_000) // 10 seconds - // Check if the setup task is still in the registry. If so, it implies - // it hasn't completed its defer block (e.g., it's stuck or genuinely timed out) - // and wasn't cleaned up by an error path calling removeConnection. - // Also, ensure the connection object itself still exists. - if self.connectionTasks[connectionID] != nil, // Task entry still exists (meaning it hasn't completed defer) + // If the setup task is still registered, treat it as timed out. + if self.connectionTasks[connectionID] != nil, self.connections[connectionID] != nil - { // Connection object still exists + { log.warning( "Connection \(connectionID) setup timed out (task still in registry), closing it" ) @@ -863,19 +830,16 @@ actor ServerNetworkManager { } func registerHandlers(for server: MCP.Server, connectionID: UUID) async { - // Register prompts/list handler await server.withMethodHandler(ListPrompts.self) { _ in log.debug("Handling ListPrompts request for \(connectionID)") return ListPrompts.Result(prompts: []) } - // Register the resources/list handler await server.withMethodHandler(ListResources.self) { _ in log.debug("Handling ListResources request for \(connectionID)") return ListResources.Result(resources: []) } - // Register tools/list handler await server.withMethodHandler(ListTools.self) { [weak self] _ in guard let self = self else { return ListTools.Result(tools: []) @@ -888,7 +852,7 @@ actor ServerNetworkManager { for service in await self.services { let serviceId = String(describing: type(of: service)) - // Get the binding value in an actor-safe way + // Read binding on the actor for consistency. if let isServiceEnabled = await self.serviceBindings[serviceId]?.wrappedValue, isServiceEnabled { @@ -911,7 +875,6 @@ actor ServerNetworkManager { return ListTools.Result(tools: tools) } - // Register tools/call handler await server.withMethodHandler(CallTool.self) { [weak self] params in guard let self = self else { return CallTool.Result( @@ -933,7 +896,7 @@ actor ServerNetworkManager { for service in await self.services { let serviceId = String(describing: type(of: service)) - // Get the binding value in an actor-safe way + // Read binding on the actor for consistency. if let isServiceEnabled = await self.serviceBindings[serviceId]?.wrappedValue, isServiceEnabled { @@ -956,7 +919,9 @@ actor ServerNetworkManager { data: data.base64EncodedString(), mimeType: mimeType ) - ], isError: false) + ], + isError: false + ) case .data(let mimeType?, let data) where mimeType.hasPrefix("image/"): return CallTool.Result( content: [ @@ -965,7 +930,9 @@ actor ServerNetworkManager { mimeType: mimeType, metadata: nil ) - ], isError: false) + ], + isError: false + ) default: let encoder = JSONEncoder() encoder.userInfo[Ontology.DateTime.timeZoneOverrideKey] = @@ -979,7 +946,8 @@ actor ServerNetworkManager { } } catch { log.error( - "Error executing tool \(params.name): \(error.localizedDescription)") + "Error executing tool \(params.name): \(error.localizedDescription)" + ) return CallTool.Result(content: [.text("Error: \(error)")], isError: true) } } @@ -993,15 +961,15 @@ actor ServerNetworkManager { } } - // Update the enabled state and notify clients + // Update the enabled state and notify clients. func setEnabled(_ enabled: Bool) async { - // Only do something if the state actually changes + // Only act on changes. guard isEnabledState != enabled else { return } isEnabledState = enabled log.info("iMCP enabled state changed to: \(enabled)") - // Notify all connected clients that the tool list has changed + // Notify all connected clients that the tool list has changed. for (_, connectionManager) in connections { Task { await connectionManager.notifyToolListChanged() @@ -1009,11 +977,11 @@ actor ServerNetworkManager { } } - // Update service bindings + // Update service bindings. func updateServiceBindings(_ newBindings: [String: Binding]) async { self.serviceBindings = newBindings - // Notify clients that tool availability may have changed + // Notify clients that tool availability may have changed. Task { for (_, connectionManager) in connections { await connectionManager.notifyToolListChanged() diff --git a/App/Extensions/CoreGraphics+Extensions.swift b/App/Extensions/CoreGraphics+Extensions.swift index a7a1a37..2c090fa 100644 --- a/App/Extensions/CoreGraphics+Extensions.swift +++ b/App/Extensions/CoreGraphics+Extensions.swift @@ -8,7 +8,11 @@ extension CGImage { let mutableData = NSMutableData() guard let destination = CGImageDestinationCreateWithData( - mutableData, UTType.png.identifier as CFString, 1, nil) + mutableData, + UTType.png.identifier as CFString, + 1, + nil + ) else { return nil } @@ -23,7 +27,11 @@ extension CGImage { let mutableData = NSMutableData() guard let destination = CGImageDestinationCreateWithData( - mutableData, UTType.jpeg.identifier as CFString, 1, nil) + mutableData, + UTType.jpeg.identifier as CFString, + 1, + nil + ) else { return nil } diff --git a/App/Extensions/Foundation+Extensions.swift b/App/Extensions/Foundation+Extensions.swift index 9579177..53f3121 100644 --- a/App/Extensions/Foundation+Extensions.swift +++ b/App/Extensions/Foundation+Extensions.swift @@ -10,12 +10,23 @@ extension ISO8601DateFormatter { let formatter = ISO8601DateFormatter() let optionsToTry: [ISO8601DateFormatter.Options] = [ - [.withInternetDateTime, .withFractionalSeconds], // `yyyy-MM-dd'T'HH:mm:ss.SSSZ`, `yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ` - [.withInternetDateTime], // `yyyy-MM-dd'T'HH:mm:ssZ`, `yyyy-MM-dd'T'HH:mm:ssZZZZZ` - [.withFullDate, .withFullTime, .withFractionalSeconds], // `yyyy-MM-dd'T'HH:mm:ss.SSS` (no zone) - [.withFullDate, .withFullTime], // `yyyy-MM-dd'T'HH:mm:ss` (no zone) - [.withFullDate, .withFullTime, .withSpaceBetweenDateAndTime, .withFractionalSeconds], // `yyyy-MM-dd HH:mm:ss.SSSZZZZZ` - [.withFullDate, .withFullTime, .withSpaceBetweenDateAndTime], // `yyyy-MM-dd HH:mm:ssZZZZZ` + // `yyyy-MM-dd'T'HH:mm:ss.SSSZ`, `yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ` + [.withInternetDateTime, .withFractionalSeconds], + + // `yyyy-MM-dd'T'HH:mm:ssZ`, `yyyy-MM-dd'T'HH:mm:ssZZZZZ` + [.withInternetDateTime], + + // `yyyy-MM-dd'T'HH:mm:ss.SSS` (no timezone) + [.withFullDate, .withFullTime, .withFractionalSeconds], + + // `yyyy-MM-dd'T'HH:mm:ss` (no timezone) + [.withFullDate, .withFullTime], + + // `yyyy-MM-dd HH:mm:ss.SSSZZZZZ` + [.withFullDate, .withFullTime, .withSpaceBetweenDateAndTime, .withFractionalSeconds], + + // `yyyy-MM-dd HH:mm:ssZZZZZ` + [.withFullDate, .withFullTime, .withSpaceBetweenDateAndTime], ] for options in optionsToTry { @@ -58,6 +69,11 @@ extension ISO8601DateFormatter { return nil } + /// Attempts to parse a date string using common ISO 8601 variants, + /// falling back to local-time parsing when no timezone is present. + /// - Parameters: + /// - dateString: The string representation of the date. + /// - Returns: A tuple containing the `Date` object and a boolean indicating if the date is date-only. static func parsedLenientISO8601Date( fromISO8601String dateString: String ) -> (date: Date, isDateOnly: Bool)? { @@ -68,16 +84,30 @@ extension ISO8601DateFormatter { return (date, isDateOnly) } + /// Checks if a date string is a date-only ISO 8601 string. + /// - Parameters: + /// - dateString: The string representation of the date. + /// - Returns: A boolean indicating if the date is date-only. static func isDateOnlyISO8601String(_ dateString: String) -> Bool { dateString.range(of: #"^\d{4}-\d{2}-\d{2}$"#, options: .regularExpression) != nil } } extension Calendar { + /// Normalizes a start date to ensure it is a date-only date. + /// - Parameters: + /// - date: The date to normalize. + /// - isDateOnly: A boolean indicating if the date is date-only. + /// - Returns: The normalized date. func normalizedStartDate(from date: Date, isDateOnly: Bool) -> Date { isDateOnly ? startOfDay(for: date) : date } + /// Normalizes an end date to ensure it is a date-only date. + /// - Parameters: + /// - date: The date to normalize. + /// - isDateOnly: A boolean indicating if the date is date-only. + /// - Returns: The normalized date. func normalizedEndDate(from date: Date, isDateOnly: Bool) -> Date { guard isDateOnly else { return date } let startOfDay = startOfDay(for: date) diff --git a/App/Extensions/ScreenCaptureKit+Extensions.swift b/App/Extensions/ScreenCaptureKit+Extensions.swift index 4b44c78..c2242e2 100644 --- a/App/Extensions/ScreenCaptureKit+Extensions.swift +++ b/App/Extensions/ScreenCaptureKit+Extensions.swift @@ -90,7 +90,9 @@ enum ScreenshotFormat: String, Hashable, CaseIterable { extension SCShareableContent { static func getAvailableContent() async throws -> SCShareableContent { return try await SCShareableContent.excludingDesktopWindows( - false, onScreenWindowsOnly: true) + false, + onScreenWindowsOnly: true + ) } } diff --git a/App/Integrations/ClaudeDesktop.swift b/App/Integrations/ClaudeDesktop.swift index feb7d14..4f12e46 100644 --- a/App/Integrations/ClaudeDesktop.swift +++ b/App/Integrations/ClaudeDesktop.swift @@ -91,7 +91,8 @@ private func getSecurityScopedConfigURL() throws -> URL? { resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, - bookmarkDataIsStale: &isStale) + bookmarkDataIsStale: &isStale + ) if isStale { log.debug("Bookmark data is stale but URL was resolved: \(url.path). Attempting to use it.") @@ -118,7 +119,8 @@ private func loadConfig() throws -> ([String: Value], ClaudeDesktop.Config.MCPSe let imcpServer = ClaudeDesktop.Config.MCPServer( command: Bundle.main.bundleURL .appendingPathComponent("Contents/MacOS/imcp-server") - .path) + .path + ) var loadedConfiguration: [String: Value]? @@ -143,11 +145,13 @@ private func loadConfig() throws -> ([String: Value], ClaudeDesktop.Config.MCPSe } } else { log.debug( - "Security-scoped URL \(secureURL.path) does not point to an existing file.") + "Security-scoped URL \(secureURL.path) does not point to an existing file." + ) } } else { log.debug( - "Failed to start accessing security-scoped resource for URL: \(secureURL.path)") + "Failed to start accessing security-scoped resource for URL: \(secureURL.path)" + ) } } else { log.debug("No security-scoped URL obtained or an error occurred retrieving it.") diff --git a/App/Services/Calendar.swift b/App/Services/Calendar.swift index f61c9d4..4eeefbb 100644 --- a/App/Services/Calendar.swift +++ b/App/Services/Calendar.swift @@ -39,7 +39,8 @@ final class CalendarService: Service { guard EKEventStore.authorizationStatus(for: .event) == .fullAccess else { log.error("Calendar access not authorized") throw NSError( - domain: "CalendarError", code: 1, + domain: "CalendarError", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Calendar access not authorized"] ) } @@ -105,7 +106,8 @@ final class CalendarService: Service { guard EKEventStore.authorizationStatus(for: .event) == .fullAccess else { log.error("Calendar access not authorized") throw NSError( - domain: "CalendarError", code: 1, + domain: "CalendarError", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Calendar access not authorized"] ) } @@ -131,7 +133,8 @@ final class CalendarService: Service { if case .string(let start) = arguments["start"], let parsedStart = ISO8601DateFormatter.parsedLenientISO8601Date( - fromISO8601String: start) + fromISO8601String: start + ) { hasStart = true startDate = parsedStart.date @@ -140,7 +143,8 @@ final class CalendarService: Service { if case .string(let end) = arguments["end"], let parsedEnd = ISO8601DateFormatter.parsedLenientISO8601Date( - fromISO8601String: end) + fromISO8601String: end + ) { hasEnd = true endDate = parsedEnd.date @@ -155,8 +159,10 @@ final class CalendarService: Service { if startIsDateOnly { endDate = calendar.normalizedEndDate(from: startDate, isDateOnly: true) } else if let nextWeek = calendar.date( - byAdding: .weekOfYear, value: 1, to: startDate) - { + byAdding: .weekOfYear, + value: 1, + to: startDate + ) { endDate = nextWeek } } @@ -335,7 +341,8 @@ final class CalendarService: Service { guard EKEventStore.authorizationStatus(for: .event) == .fullAccess else { log.error("Calendar access not authorized") throw NSError( - domain: "CalendarError", code: 1, + domain: "CalendarError", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Calendar access not authorized"] ) } @@ -346,7 +353,8 @@ final class CalendarService: Service { // Set required properties guard case .string(let title) = arguments["title"] else { throw NSError( - domain: "CalendarError", code: 2, + domain: "CalendarError", + code: 2, userInfo: [NSLocalizedDescriptionKey: "Event title is required"] ) } @@ -355,13 +363,16 @@ final class CalendarService: Service { // Parse dates guard case .string(let startDateStr) = arguments["start"], let parsedStart = ISO8601DateFormatter.parsedLenientISO8601Date( - fromISO8601String: startDateStr), + fromISO8601String: startDateStr + ), case .string(let endDateStr) = arguments["end"], let parsedEnd = ISO8601DateFormatter.parsedLenientISO8601Date( - fromISO8601String: endDateStr) + fromISO8601String: endDateStr + ) else { throw NSError( - domain: "CalendarError", code: 2, + domain: "CalendarError", + code: 2, userInfo: [ NSLocalizedDescriptionKey: "Invalid start or end date format. Expected ISO 8601 format." @@ -382,7 +393,9 @@ final class CalendarService: Service { // For all-day events, ensure we use local midnight if case .bool(true) = arguments["isAllDay"] { var startComponents = calendar.dateComponents( - [.year, .month, .day], from: startDate) + [.year, .month, .day], + from: startDate + ) startComponents.hour = 0 startComponents.minute = 0 startComponents.second = 0 @@ -453,8 +466,8 @@ final class CalendarService: Service { "Absolute alarm datetime must include time component: \(datetimeStr, privacy: .public)" ) } else if let absoluteDate = ISO8601DateFormatter.lenientDate( - fromISO8601String: datetimeStr) - { + fromISO8601String: datetimeStr + ) { alarm = EKAlarm(absoluteDate: absoluteDate) } } @@ -469,7 +482,9 @@ final class CalendarService: Service { // Create structured location let structuredLocation = EKStructuredLocation(title: locationTitle) structuredLocation.geoLocation = CLLocation( - latitude: latitude, longitude: longitude) + latitude: latitude, + longitude: longitude + ) if case .double(let radius) = config["radius"] { structuredLocation.radius = radius @@ -487,7 +502,8 @@ final class CalendarService: Service { default: log.error( - "Unexpected alarm type encountered: \(alarmType, privacy: .public)") + "Unexpected alarm type encountered: \(alarmType, privacy: .public)" + ) continue } diff --git a/App/Services/Capture.swift b/App/Services/Capture.swift index 75da317..55b2456 100644 --- a/App/Services/Capture.swift +++ b/App/Services/Capture.swift @@ -166,12 +166,14 @@ final class CaptureService: NSObject, Service { let format = ImageFormat( - rawValue: arguments["format"]?.stringValue ?? ImageFormat.default.rawValue) + rawValue: arguments["format"]?.stringValue ?? ImageFormat.default.rawValue + ) ?? .jpeg let quality = arguments["quality"]?.doubleValue ?? 0.8 let preset = SessionPreset( - rawValue: arguments["preset"]?.stringValue ?? SessionPreset.default.rawValue) + rawValue: arguments["preset"]?.stringValue ?? SessionPreset.default.rawValue + ) ?? .photo let device = CaptureDeviceType( @@ -181,7 +183,8 @@ final class CaptureService: NSObject, Service { let position = CaptureDevicePosition( rawValue: arguments["position"]?.stringValue - ?? CaptureDevicePosition.default.rawValue) ?? .unspecified + ?? CaptureDevicePosition.default.rawValue + ) ?? .unspecified let flash = FlashMode(rawValue: arguments["flash"]?.stringValue ?? FlashMode.default.rawValue) ?? .auto @@ -245,7 +248,8 @@ final class CaptureService: NSObject, Service { return try await withCheckedThrowingContinuation { continuation in let resumeGate = ResumeGate() let resumeOnce: (Result, (() async -> Void)?) async -> Void = { - result, cleanup in + result, + cleanup in guard await resumeGate.shouldResume() else { return } if let cleanup = cleanup { await cleanup() @@ -339,7 +343,8 @@ final class CaptureService: NSObject, Service { let format = AudioFormat( - rawValue: arguments["format"]?.stringValue ?? AudioFormat.default.rawValue) + rawValue: arguments["format"]?.stringValue ?? AudioFormat.default.rawValue + ) ?? .mp4 let duration = arguments["duration"].flatMap { Double($0) } ?? 10.0 let quality = arguments["quality"]?.stringValue ?? "medium" @@ -551,7 +556,9 @@ final class CaptureService: NSObject, Service { ) } contentFilter = SCContentFilter( - display: firstDisplay, including: appWindows) + display: firstDisplay, + including: appWindows + ) } // Create stream configuration @@ -572,7 +579,8 @@ final class CaptureService: NSObject, Service { return try await withCheckedThrowingContinuation { continuation in let resumeGate = ResumeGate() let resumeOnce: (Result, (() async -> Void)?) async -> Void = { - result, _ in + result, + _ in guard await resumeGate.shouldResume() else { return } continuation.resume(with: result) } @@ -684,7 +692,9 @@ private class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate { domain: "CaptureServiceError", code: 6, userInfo: [NSLocalizedDescriptionKey: "Failed to get image data"] - ))) + ) + ) + ) return } diff --git a/App/Services/Contacts.swift b/App/Services/Contacts.swift index 8ca1d9b..0af05e7 100644 --- a/App/Services/Contacts.swift +++ b/App/Services/Contacts.swift @@ -99,13 +99,15 @@ final class ContactsService: Service { case .denied: log.error("Contacts access denied") throw NSError( - domain: "ContactsService", code: 1, + domain: "ContactsService", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Contacts access denied"] ) case .restricted: log.error("Contacts access restricted") throw NSError( - domain: "ContactsService", code: 1, + domain: "ContactsService", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Contacts access restricted"] ) case .notDetermined: @@ -114,7 +116,8 @@ final class ContactsService: Service { @unknown default: log.error("Unknown contacts authorization status") throw NSError( - domain: "ContactsService", code: 1, + domain: "ContactsService", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Unknown contacts authorization status"] ) } @@ -182,13 +185,15 @@ final class ContactsService: Service { let normalizedEmail = email.trimmingCharacters(in: .whitespaces).lowercased() if !normalizedEmail.isEmpty { predicates.append( - CNContact.predicateForContacts(matchingEmailAddress: normalizedEmail)) + CNContact.predicateForContacts(matchingEmailAddress: normalizedEmail) + ) } } guard !predicates.isEmpty else { throw NSError( - domain: "ContactsService", code: 1, + domain: "ContactsService", + code: 1, userInfo: [ NSLocalizedDescriptionKey: "At least one valid search parameter is required" ] @@ -219,7 +224,9 @@ final class ContactsService: Service { description: "Unique identifier of the contact to update" ) ] as OrderedDictionary).merging( - contactProperties, uniquingKeysWith: { new, _ in new }), + contactProperties, + uniquingKeysWith: { new, _ in new } + ), required: ["identifier"] ), annotations: .init( @@ -231,7 +238,8 @@ final class ContactsService: Service { ) { arguments in guard case let .string(identifier) = arguments["identifier"], !identifier.isEmpty else { throw NSError( - domain: "ContactsService", code: 1, + domain: "ContactsService", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Valid contact identifier required"] ) } @@ -245,7 +253,8 @@ final class ContactsService: Service { guard let updatedContact = contact else { throw NSError( - domain: "ContactsService", code: 2, + domain: "ContactsService", + code: 2, userInfo: [ NSLocalizedDescriptionKey: "Contact not found with identifier: \(identifier)" @@ -287,7 +296,8 @@ final class ContactsService: Service { // Validate that given name is provided and not empty if newContact.givenName.isEmpty { throw NSError( - domain: "ContactsService", code: 1, + domain: "ContactsService", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Given name is required"] ) } diff --git a/App/Services/Location.swift b/App/Services/Location.swift index 0a26d98..4912fc5 100644 --- a/App/Services/Location.swift +++ b/App/Services/Location.swift @@ -66,7 +66,8 @@ final class LocationService: NSObject, Service, CLLocationManagerDelegate { domain: "LocationServiceError", code: 7, userInfo: [NSLocalizedDescriptionKey: "Location access denied"] - )) + ) + ) self.authorizationContinuation = nil case .notDetermined: // Need to request authorization @@ -80,7 +81,8 @@ final class LocationService: NSObject, Service, CLLocationManagerDelegate { domain: "LocationServiceError", code: 8, userInfo: [NSLocalizedDescriptionKey: "Unknown authorization status"] - )) + ) + ) self.authorizationContinuation = nil } } @@ -109,18 +111,21 @@ final class LocationService: NSObject, Service, CLLocationManagerDelegate { log.error("Location access not authorized") continuation.resume( throwing: NSError( - domain: "LocationServiceError", code: 1, + domain: "LocationServiceError", + code: 1, userInfo: [ NSLocalizedDescriptionKey: "Location access not authorized" ] - )) + ) + ) return } // If we already have a recent location, use it if let location = self.latestLocation { continuation.resume( - returning: GeoCoordinates(location)) + returning: GeoCoordinates(location) + ) return } @@ -158,13 +163,16 @@ final class LocationService: NSObject, Service, CLLocationManagerDelegate { if let location = location { continuation.resume( - returning: GeoCoordinates(location)) + returning: GeoCoordinates(location) + ) } else { continuation.resume( throwing: NSError( - domain: "LocationServiceError", code: 2, + domain: "LocationServiceError", + code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to get location"] - )) + ) + ) } } } @@ -190,7 +198,8 @@ final class LocationService: NSObject, Service, CLLocationManagerDelegate { ) { arguments in guard let address = arguments["address"]?.stringValue else { throw NSError( - domain: "LocationServiceError", code: 3, + domain: "LocationServiceError", + code: 3, userInfo: [NSLocalizedDescriptionKey: "Invalid address"] ) } @@ -209,11 +218,13 @@ final class LocationService: NSObject, Service, CLLocationManagerDelegate { else { continuation.resume( throwing: NSError( - domain: "LocationServiceError", code: 4, + domain: "LocationServiceError", + code: 4, userInfo: [ NSLocalizedDescriptionKey: "No location found for address" ] - )) + ) + ) return } @@ -286,7 +297,8 @@ final class LocationService: NSObject, Service, CLLocationManagerDelegate { else { log.error("Invalid coordinates") throw NSError( - domain: "LocationServiceError", code: 5, + domain: "LocationServiceError", + code: 5, userInfo: [NSLocalizedDescriptionKey: "Invalid coordinates"] ) } @@ -305,11 +317,13 @@ final class LocationService: NSObject, Service, CLLocationManagerDelegate { guard let placemark = placemarks?.first else { continuation.resume( throwing: NSError( - domain: "LocationServiceError", code: 6, + domain: "LocationServiceError", + code: 6, userInfo: [ NSLocalizedDescriptionKey: "No address found for location" ] - )) + ) + ) return } @@ -376,7 +390,8 @@ final class LocationService: NSObject, Service, CLLocationManagerDelegate { } func locationManager( - _ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus + _ manager: CLLocationManager, + didChangeAuthorization status: CLAuthorizationStatus ) { switch status { case .authorizedWhenInUse, .authorizedAlways: @@ -390,7 +405,8 @@ final class LocationService: NSObject, Service, CLLocationManagerDelegate { domain: "LocationServiceError", code: 7, userInfo: [NSLocalizedDescriptionKey: "Location access denied"] - )) + ) + ) authorizationContinuation = nil case .notDetermined: log.debug("Location access not determined") diff --git a/App/Services/Maps.swift b/App/Services/Maps.swift index 68fa610..33d82c3 100644 --- a/App/Services/Maps.swift +++ b/App/Services/Maps.swift @@ -69,7 +69,8 @@ final class MapsService: NSObject, Service { ) { arguments in guard let query = arguments["query"]?.stringValue else { throw NSError( - domain: "MapsServiceError", code: 1, + domain: "MapsServiceError", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Search query is required"] ) } @@ -106,7 +107,8 @@ final class MapsService: NSObject, Service { guard let response = response else { continuation.resume( throwing: NSError( - domain: "MapsServiceError", code: 2, + domain: "MapsServiceError", + code: 2, userInfo: [NSLocalizedDescriptionKey: "No search results"] ) ) @@ -174,7 +176,8 @@ final class MapsService: NSObject, Service { != nil) else { throw NSError( - domain: "MapsServiceError", code: 3, + domain: "MapsServiceError", + code: 3, userInfo: [NSLocalizedDescriptionKey: "Origin address or coordinates required"] ) } @@ -188,7 +191,8 @@ final class MapsService: NSObject, Service { .doubleValue != nil) else { throw NSError( - domain: "MapsServiceError", code: 4, + domain: "MapsServiceError", + code: 4, userInfo: [ NSLocalizedDescriptionKey: "Destination address or coordinates required" ] @@ -236,7 +240,8 @@ final class MapsService: NSObject, Service { guard let response = response, !response.routes.isEmpty else { continuation.resume( throwing: NSError( - domain: "MapsServiceError", code: 5, + domain: "MapsServiceError", + code: 5, userInfo: [NSLocalizedDescriptionKey: "No routes found"] ) ) @@ -284,7 +289,8 @@ final class MapsService: NSObject, Service { let longitude = arguments["longitude"]?.doubleValue else { throw NSError( - domain: "MapsServiceError", code: 6, + domain: "MapsServiceError", + code: 6, userInfo: [NSLocalizedDescriptionKey: "Category and coordinates are required"] ) } @@ -294,7 +300,8 @@ final class MapsService: NSObject, Service { guard let category = MKPointOfInterestCategory.from(string: categoryString) else { throw NSError( - domain: "MapsServiceError", code: 7, + domain: "MapsServiceError", + code: 7, userInfo: [NSLocalizedDescriptionKey: "Invalid POI category"] ) } @@ -319,7 +326,8 @@ final class MapsService: NSObject, Service { guard let response = response else { continuation.resume( throwing: NSError( - domain: "MapsServiceError", code: 8, + domain: "MapsServiceError", + code: 8, userInfo: [NSLocalizedDescriptionKey: "No POI results found"] ) ) @@ -369,7 +377,8 @@ final class MapsService: NSObject, Service { let destLng = arguments["destinationLongitude"]?.doubleValue else { throw NSError( - domain: "MapsServiceError", code: 9, + domain: "MapsServiceError", + code: 9, userInfo: [ NSLocalizedDescriptionKey: "Origin and destination coordinates required" ] @@ -422,7 +431,8 @@ final class MapsService: NSObject, Service { guard let response = response else { continuation.resume( throwing: NSError( - domain: "MapsServiceError", code: 10, + domain: "MapsServiceError", + code: 10, userInfo: [NSLocalizedDescriptionKey: "Could not calculate ETA"] ) ) @@ -499,7 +509,8 @@ final class MapsService: NSObject, Service { let longitudeDelta = arguments["longitudeDelta"]?.doubleValue else { throw NSError( - domain: "MapsServiceError", code: 13, + domain: "MapsServiceError", + code: 13, userInfo: [ NSLocalizedDescriptionKey: "Latitude, longitude, latitudeDelta, and longitudeDelta are required" @@ -515,7 +526,9 @@ final class MapsService: NSObject, Service { let center = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) let span = MKCoordinateSpan( - latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta) + latitudeDelta: latitudeDelta, + longitudeDelta: longitudeDelta + ) options.region = MKCoordinateRegion(center: center, span: span) switch mapTypeString { @@ -575,7 +588,8 @@ final class MapsService: NSObject, Service { log.error("Map snapshot failed: No snapshot data") continuation.resume( throwing: NSError( - domain: "MapsServiceError", code: 14, + domain: "MapsServiceError", + code: 14, userInfo: [ NSLocalizedDescriptionKey: "Failed to generate map snapshot" ] @@ -592,7 +606,8 @@ final class MapsService: NSObject, Service { log.error("Map snapshot failed: Could not convert image to PNG") continuation.resume( throwing: NSError( - domain: "MapsServiceError", code: 15, + domain: "MapsServiceError", + code: 15, userInfo: [ NSLocalizedDescriptionKey: "Failed to convert snapshot to PNG format" @@ -625,7 +640,8 @@ final class MapsService: NSObject, Service { guard let mapItem = response.mapItems.first else { throw NSError( - domain: "MapsServiceError", code: 11, + domain: "MapsServiceError", + code: 11, userInfo: [ NSLocalizedDescriptionKey: "Could not find location for address: \(address)" ] @@ -645,7 +661,8 @@ final class MapsService: NSObject, Service { return MKMapItem(placemark: placemark) } else { throw NSError( - domain: "MapsServiceError", code: 12, + domain: "MapsServiceError", + code: 12, userInfo: [ NSLocalizedDescriptionKey: "Either address or coordinates must be provided" ] diff --git a/App/Services/Messages.swift b/App/Services/Messages.swift index 5a37bac..7a517be 100644 --- a/App/Services/Messages.swift +++ b/App/Services/Messages.swift @@ -97,9 +97,11 @@ final class MessageService: NSObject, Service, NSOpenSavePanelDelegate { if let startDateStr = arguments["start"]?.stringValue, let endDateStr = arguments["end"]?.stringValue, let parsedStart = ISO8601DateFormatter.parsedLenientISO8601Date( - fromISO8601String: startDateStr), + fromISO8601String: startDateStr + ), let parsedEnd = ISO8601DateFormatter.parsedLenientISO8601Date( - fromISO8601String: endDateStr) + fromISO8601String: endDateStr + ) { let calendar = Calendar.current let normalizedStart = calendar.normalizedStartDate( @@ -111,7 +113,7 @@ final class MessageService: NSObject, Service, NSOpenSavePanelDelegate { isDateOnly: parsedEnd.isDateOnly ) - dateRange = normalizedStart..(_ url: URL, _ operation: (URL) throws -> T) throws -> T - { + private func withSecurityScopedAccess(_ url: URL, _ operation: (URL) throws -> T) throws -> T { guard url.startAccessingSecurityScopedResource() else { log.error("Failed to start accessing security-scoped resource") throw DatabaseAccessError.securityScopeAccessFailed @@ -304,7 +305,8 @@ final class MessageService: NSObject, Service, NSOpenSavePanelDelegate { func panel(_ sender: Any, shouldEnable url: URL) -> Bool { let shouldEnable = url.lastPathComponent == "chat.db" log.debug( - "File selection panel: \(shouldEnable ? "enabling" : "disabling") URL: \(url.path)") + "File selection panel: \(shouldEnable ? "enabling" : "disabling") URL: \(url.path)" + ) return shouldEnable } } diff --git a/App/Services/Reminders.swift b/App/Services/Reminders.swift index aeed313..43f957d 100644 --- a/App/Services/Reminders.swift +++ b/App/Services/Reminders.swift @@ -37,7 +37,8 @@ final class RemindersService: Service { guard EKEventStore.authorizationStatus(for: .reminder) == .fullAccess else { log.error("Reminders access not authorized") throw NSError( - domain: "RemindersError", code: 1, + domain: "RemindersError", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Reminders access not authorized"] ) } @@ -96,7 +97,8 @@ final class RemindersService: Service { guard EKEventStore.authorizationStatus(for: .reminder) == .fullAccess else { log.error("Reminders access not authorized") throw NSError( - domain: "RemindersError", code: 1, + domain: "RemindersError", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Reminders access not authorized"] ) } @@ -107,7 +109,8 @@ final class RemindersService: Service { !listNames.isEmpty { let requestedNames = Set( - listNames.compactMap { $0.stringValue?.lowercased() }) + listNames.compactMap { $0.stringValue?.lowercased() } + ) reminderLists = reminderLists.filter { requestedNames.contains($0.title.lowercased()) } @@ -121,14 +124,16 @@ final class RemindersService: Service { if case .string(let start) = arguments["start"], let parsedStart = ISO8601DateFormatter.parsedLenientISO8601Date( - fromISO8601String: start) + fromISO8601String: start + ) { startDate = parsedStart.date startIsDateOnly = parsedStart.isDateOnly } if case .string(let end) = arguments["end"], let parsedEnd = ISO8601DateFormatter.parsedLenientISO8601Date( - fromISO8601String: end) + fromISO8601String: end + ) { endDate = parsedEnd.date endIsDateOnly = parsedEnd.isDateOnly @@ -137,7 +142,9 @@ final class RemindersService: Service { let calendar = Calendar.current if let startDateValue = startDate { startDate = calendar.normalizedStartDate( - from: startDateValue, isDateOnly: startIsDateOnly) + from: startDateValue, + isDateOnly: startIsDateOnly + ) } if let endDateValue = endDate { endDate = calendar.normalizedEndDate(from: endDateValue, isDateOnly: endIsDateOnly) @@ -224,7 +231,8 @@ final class RemindersService: Service { guard EKEventStore.authorizationStatus(for: .reminder) == .fullAccess else { log.error("Reminders access not authorized") throw NSError( - domain: "RemindersError", code: 1, + domain: "RemindersError", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Reminders access not authorized"] ) } @@ -234,7 +242,8 @@ final class RemindersService: Service { // Set required properties guard case .string(let title) = arguments["title"] else { throw NSError( - domain: "RemindersError", code: 2, + domain: "RemindersError", + code: 2, userInfo: [NSLocalizedDescriptionKey: "Reminder title is required"] ) } @@ -254,7 +263,8 @@ final class RemindersService: Service { // Set optional properties if case .string(let dueDateStr) = arguments["due"], let parsedDueDate = ISO8601DateFormatter.parsedLenientISO8601Date( - fromISO8601String: dueDateStr) + fromISO8601String: dueDateStr + ) { let calendar = Calendar.current let dueDate = calendar.normalizedStartDate( @@ -262,7 +272,9 @@ final class RemindersService: Service { isDateOnly: parsedDueDate.isDateOnly ) reminder.dueDateComponents = calendar.dateComponents( - [.year, .month, .day, .hour, .minute, .second], from: dueDate) + [.year, .month, .day, .hour, .minute, .second], + from: dueDate + ) } if case .string(let notes) = arguments["notes"] { diff --git a/App/Services/Utilities.swift b/App/Services/Utilities.swift index bfb44f1..b5ca984 100644 --- a/App/Services/Utilities.swift +++ b/App/Services/Utilities.swift @@ -15,7 +15,8 @@ final class UtilitiesService: Service { properties: [ "sound": .string( default: .string(Sound.default.rawValue), - enum: Sound.allCases.map { .string($0.rawValue) }) + enum: Sound.allCases.map { .string($0.rawValue) } + ) ], required: ["sound"], additionalProperties: false @@ -30,10 +31,12 @@ final class UtilitiesService: Service { guard let sound = Sound(rawValue: rawValue) else { log.error("Invalid sound: \(rawValue)") throw NSError( - domain: "SoundError", code: 1, + domain: "SoundError", + code: 1, userInfo: [ NSLocalizedDescriptionKey: "Invalid sound" - ]) + ] + ) } return NSSound.play(sound) diff --git a/App/Services/Weather.swift b/App/Services/Weather.swift index 8503327..6173fdc 100644 --- a/App/Services/Weather.swift +++ b/App/Services/Weather.swift @@ -35,14 +35,17 @@ final class WeatherService: Service { else { log.error("Invalid coordinates") throw NSError( - domain: "WeatherServiceError", code: 1, + domain: "WeatherServiceError", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid coordinates"] ) } let location = CLLocation(latitude: latitude, longitude: longitude) let currentWeather = try await self.weatherService.weather( - for: location, including: .current) + for: location, + including: .current + ) return WeatherConditions(currentWeather) } @@ -75,7 +78,8 @@ final class WeatherService: Service { else { log.error("Invalid coordinates") throw NSError( - domain: "WeatherServiceError", code: 1, + domain: "WeatherServiceError", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid coordinates"] ) } @@ -86,11 +90,13 @@ final class WeatherService: Service { } else if case let .double(daysRequested) = arguments["days"] { days = Int(daysRequested) } - days = days.clamped(to: 1...10) + days = days.clamped(to: 1 ... 10) let location = CLLocation(latitude: latitude, longitude: longitude) let dailyForecast = try await self.weatherService.weather( - for: location, including: .daily) + for: location, + including: .daily + ) return dailyForecast.prefix(days).map { WeatherForecast($0) } } @@ -123,7 +129,8 @@ final class WeatherService: Service { else { log.error("Invalid coordinates") throw NSError( - domain: "WeatherServiceError", code: 1, + domain: "WeatherServiceError", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid coordinates"] ) } @@ -140,7 +147,9 @@ final class WeatherService: Service { let location = CLLocation(latitude: latitude, longitude: longitude) let hourlyForecasts = try await self.weatherService.weather( - for: location, including: .hourly) + for: location, + including: .hourly + ) return hourlyForecasts.prefix(hours).map { WeatherForecast($0) } } @@ -173,7 +182,8 @@ final class WeatherService: Service { else { log.error("Invalid coordinates") throw NSError( - domain: "WeatherServiceError", code: 1, + domain: "WeatherServiceError", + code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid coordinates"] ) } @@ -184,15 +194,18 @@ final class WeatherService: Service { } else if case let .double(minutesRequested) = arguments["minutes"] { minutes = Int(minutesRequested) } - minutes = minutes.clamped(to: 1...120) + minutes = minutes.clamped(to: 1 ... 120) let location = CLLocation(latitude: latitude, longitude: longitude) guard let minuteByMinuteForecast = try await self.weatherService.weather( - for: location, including: .minute) + for: location, + including: .minute + ) else { throw NSError( - domain: "WeatherServiceError", code: 2, + domain: "WeatherServiceError", + code: 2, userInfo: [NSLocalizedDescriptionKey: "No minute-by-minute forecast available"] ) } diff --git a/App/Views/AboutWindow.swift b/App/Views/AboutWindow.swift index 3d033a8..357a937 100644 --- a/App/Views/AboutWindow.swift +++ b/App/Views/AboutWindow.swift @@ -42,7 +42,8 @@ private struct AboutView: View { Button("Report an Issue...") { NSWorkspace.shared.open( - URL(string: "https://github.com/mattt/iMCP/issues/new")!) + URL(string: "https://github.com/mattt/iMCP/issues/new")! + ) } } } diff --git a/App/Views/ConnectionApprovalView.swift b/App/Views/ConnectionApprovalView.swift index 54def1b..1df3614 100644 --- a/App/Views/ConnectionApprovalView.swift +++ b/App/Views/ConnectionApprovalView.swift @@ -69,7 +69,9 @@ struct CheckboxToggleStyle: ToggleStyle { HStack { Image(systemName: configuration.isOn ? "checkmark.square.fill" : "square") .foregroundColor(configuration.isOn ? .accentColor : .secondary) - .accessibilityLabel(configuration.isOn ? "Always trust this client, checked" : "Always trust this client, unchecked") + .accessibilityLabel( + configuration.isOn ? "Always trust this client, checked" : "Always trust this client, unchecked" + ) .onTapGesture { configuration.isOn.toggle() } diff --git a/App/Views/ContentView.swift b/App/Views/ContentView.swift index 102ef38..8b6aa6e 100644 --- a/App/Views/ContentView.swift +++ b/App/Views/ContentView.swift @@ -18,7 +18,8 @@ struct ContentView: View { Dictionary( uniqueKeysWithValues: serviceConfigs.map { ($0.id, $0.binding) - }) + } + ) } init( @@ -176,7 +177,8 @@ private struct MenuButton: View { .fill( isPressed ? Color.accentColor - : isHighlighted ? Color.accentColor.opacity(0.7) : Color.clear) + : isHighlighted ? Color.accentColor.opacity(0.7) : Color.clear + ) ) .onHover { state in guard isEnabled else { return } diff --git a/App/Views/ServiceToggleView.swift b/App/Views/ServiceToggleView.swift index ce4f991..a0ca742 100644 --- a/App/Views/ServiceToggleView.swift +++ b/App/Views/ServiceToggleView.swift @@ -4,11 +4,11 @@ import AppKit struct ServiceToggleView: View { let config: ServiceConfig @State private var isServiceActivated = false - + // MARK: Environment @Environment(\.colorScheme) private var colorScheme @Environment(\.isEnabled) private var isEnabled - + // MARK: Private State private let buttonSize: CGFloat = 26 private let imagePadding: CGFloat = 5 @@ -41,7 +41,7 @@ struct ServiceToggleView: View { .buttonStyle(PlainButtonStyle()) .disabled(!isEnabled) .frame(width: buttonSize, height: buttonSize) - + Text(config.name) .frame(maxWidth: .infinity, alignment: .leading) .foregroundColor(isEnabled ? Color.primary : .primary.opacity(0.5)) @@ -52,7 +52,7 @@ struct ServiceToggleView: View { isServiceActivated = await config.isActivated } } - + private var buttonBackgroundColor: Color { if config.binding.wrappedValue { return config.color.opacity(isEnabled ? 1.0 : 0.4) diff --git a/CLI/main.swift b/CLI/main.swift index ae2b673..abafb74 100644 --- a/CLI/main.swift +++ b/CLI/main.swift @@ -102,7 +102,10 @@ actor StdioProxy { connection.stateUpdateHandler = { state in Task { [weak self] in await self?.handleConnectionState( - state, continuation: continuation, connectionState: connectionState) + state, + continuation: continuation, + connectionState: connectionState + ) } } } @@ -224,7 +227,8 @@ actor StdioProxy { // Also check connection state if connection.state != .ready && connection.state != .preparing { await log.debug( - "Connection state changed to \(connection.state), stopping stdin handler") + "Connection state changed to \(connection.state), stopping stdin handler" + ) throw StdioProxyError.connectionClosed } @@ -242,7 +246,7 @@ actor StdioProxy { if bytesRead > 0 { // Append the read bytes to pending data - pendingData.append(contentsOf: buffer[0..) in connection.receive(minimumIncompleteLength: 1, maximumLength: bufferSize) { - data, _, isComplete, error in + data, + _, + isComplete, + error in if let error = error { continuation.resume(throwing: error) return @@ -489,7 +499,8 @@ actor MCPService: Service { if await connectionState.checkAndSetResumed() { await log.error("Bonjour service discovery timed out after 30 seconds") continuation.resume( - throwing: MCPError.internalError("Service discovery timeout")) + throwing: MCPError.internalError("Service discovery timeout") + ) } } @@ -536,7 +547,8 @@ actor MCPService: Service { // Prefer services with iMCP in the description selectedService = imcpServices.first! await log.info( - "Selected iMCP service: \(selectedService.endpoint)") + "Selected iMCP service: \(selectedService.endpoint)" + ) } else { // Fall back to the first available service selectedService = results.first!