From f73a2ac73262e2a9373f09c84a47b5ac08805d82 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Fri, 30 Jan 2026 05:20:11 -0800 Subject: [PATCH 1/4] Add Shortcuts service --- App/Controllers/ServerController.swift | 11 ++ App/Services/Shortcuts.swift | 218 +++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 App/Services/Shortcuts.swift diff --git a/App/Controllers/ServerController.swift b/App/Controllers/ServerController.swift index b5dbbda..19f24ba 100644 --- a/App/Controllers/ServerController.swift +++ b/App/Controllers/ServerController.swift @@ -57,6 +57,7 @@ enum ServiceRegistry { MapsService.shared, MessageService.shared, RemindersService.shared, + ShortcutsService.shared, UtilitiesService.shared, ] #if WEATHERKIT_AVAILABLE @@ -73,6 +74,7 @@ enum ServiceRegistry { mapsEnabled: Binding, messagesEnabled: Binding, remindersEnabled: Binding, + shortcutsEnabled: Binding, utilitiesEnabled: Binding, weatherEnabled: Binding ) -> [ServiceConfig] { @@ -126,6 +128,13 @@ enum ServiceRegistry { service: RemindersService.shared, binding: remindersEnabled ), + ServiceConfig( + name: "Shortcuts", + iconName: "bolt.fill", + color: .yellow, + service: ShortcutsService.shared, + binding: shortcutsEnabled + ), ] #if WEATHERKIT_AVAILABLE configs.append( @@ -163,6 +172,7 @@ final class ServerController: ObservableObject { @AppStorage("mapsEnabled") private var mapsEnabled = true // Default enabled @AppStorage("messagesEnabled") private var messagesEnabled = false @AppStorage("remindersEnabled") private var remindersEnabled = false + @AppStorage("shortcutsEnabled") private var shortcutsEnabled = false @AppStorage("utilitiesEnabled") private var utilitiesEnabled = true // Default enabled @AppStorage("weatherEnabled") private var weatherEnabled = false @@ -179,6 +189,7 @@ final class ServerController: ObservableObject { mapsEnabled: $mapsEnabled, messagesEnabled: $messagesEnabled, remindersEnabled: $remindersEnabled, + shortcutsEnabled: $shortcutsEnabled, utilitiesEnabled: $utilitiesEnabled, weatherEnabled: $weatherEnabled ) diff --git a/App/Services/Shortcuts.swift b/App/Services/Shortcuts.swift new file mode 100644 index 0000000..626c140 --- /dev/null +++ b/App/Services/Shortcuts.swift @@ -0,0 +1,218 @@ +import Foundation +import JSONSchema +import OSLog + +private let log = Logger.service("shortcuts") + +final class ShortcutsService: Service { + static let shared = ShortcutsService() + + private let shortcutsPath = "/usr/bin/shortcuts" + private let executionTimeout: Duration = .seconds(300) + + var tools: [Tool] { + Tool( + name: "shortcuts_list", + description: "List all available shortcuts on this Mac", + inputSchema: .object( + properties: [:], + additionalProperties: false + ), + annotations: .init( + title: "List Shortcuts", + readOnlyHint: true, + openWorldHint: false + ) + ) { _ in + try await self.listShortcuts() + } + + Tool( + name: "shortcuts_run", + description: "Run a shortcut by name, optionally with text input", + inputSchema: .object( + properties: [ + "name": .string( + description: "The name of the shortcut to run" + ), + "input": .string( + description: "Optional text input to pass to the shortcut" + ), + ], + required: ["name"], + additionalProperties: false + ), + annotations: .init( + title: "Run Shortcut", + destructiveHint: true, + openWorldHint: true + ) + ) { arguments in + guard case let .string(name) = arguments["name"] else { + throw NSError( + domain: "ShortcutsError", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Shortcut name is required"] + ) + } + + let input = arguments["input"]?.stringValue + + return try await self.runShortcut(name: name, input: input) + } + } + + // MARK: - Private Implementation + + private func runProcess(_ process: Process) async throws { + try process.run() + await withCheckedContinuation { continuation in + process.terminationHandler = { _ in + continuation.resume() + } + } + } + + private func listShortcuts() async throws -> Value { + let process = Process() + process.executableURL = URL(fileURLWithPath: shortcutsPath) + process.arguments = ["list"] + + let outputPipe = Pipe() + let errorPipe = Pipe() + process.standardOutput = outputPipe + process.standardError = errorPipe + + do { + try await runProcess(process) + } catch { + log.error("Failed to run shortcuts command: \(error.localizedDescription)") + throw NSError( + domain: "ShortcutsError", + code: 2, + userInfo: [ + NSLocalizedDescriptionKey: "Failed to run shortcuts command: \(error.localizedDescription)" + ] + ) + } + + let outputData = (try? outputPipe.fileHandleForReading.readToEnd()) ?? Data() + let errorData = (try? errorPipe.fileHandleForReading.readToEnd()) ?? Data() + + guard process.terminationStatus == 0 else { + let errorMessage = String(data: errorData, encoding: .utf8) ?? "Unknown error" + log.error("shortcuts list failed: \(errorMessage)") + throw NSError( + domain: "ShortcutsError", + code: 3, + userInfo: [NSLocalizedDescriptionKey: "shortcuts list failed: \(errorMessage)"] + ) + } + + guard let output = String(data: outputData, encoding: .utf8) else { + return .array([]) + } + + let shortcuts = output + .split(separator: "\n") + .map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + + log.info("Found \(shortcuts.count) shortcuts") + + return .array(shortcuts.map { .string($0) }) + } + + private func runShortcut(name: String, input: String?) async throws -> Value { + log.info("Running shortcut: \(name, privacy: .public)") + + let tempDir = FileManager.default.temporaryDirectory + let outputFileURL = tempDir.appendingPathComponent("shortcut_output_\(UUID().uuidString).txt") + + var arguments = ["run", name, "--output-path", outputFileURL.path] + var inputFileURL: URL? + + if let input = input { + let inputURL = tempDir.appendingPathComponent("shortcut_input_\(UUID().uuidString).txt") + try input.write(to: inputURL, atomically: true, encoding: .utf8) + arguments.append(contentsOf: ["--input-path", inputURL.path]) + inputFileURL = inputURL + } + + defer { + if let inputURL = inputFileURL { + try? FileManager.default.removeItem(at: inputURL) + } + try? FileManager.default.removeItem(at: outputFileURL) + } + + let process = Process() + process.executableURL = URL(fileURLWithPath: shortcutsPath) + process.arguments = arguments + + let errorPipe = Pipe() + process.standardError = errorPipe + + do { + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await self.runProcess(process) + } + + group.addTask { + try await Task.sleep(for: self.executionTimeout) + process.terminate() + throw NSError( + domain: "ShortcutsError", + code: 6, + userInfo: [ + NSLocalizedDescriptionKey: + "Shortcut '\(name)' timed out after 5 minutes" + ] + ) + } + + _ = try await group.next() + group.cancelAll() + } + } catch { + if process.isRunning { + process.terminate() + } + log.error("Failed to run shortcut '\(name, privacy: .public)': \(error.localizedDescription)") + throw error + } + + let errorData = (try? errorPipe.fileHandleForReading.readToEnd()) ?? Data() + + guard process.terminationStatus == 0 else { + let errorMessage = String(data: errorData, encoding: .utf8) ?? "Unknown error" + log.error("Shortcut '\(name, privacy: .public)' failed: \(errorMessage)") + throw NSError( + domain: "ShortcutsError", + code: 5, + userInfo: [NSLocalizedDescriptionKey: "Shortcut '\(name)' failed: \(errorMessage)"] + ) + } + + var output: String? + if FileManager.default.fileExists(atPath: outputFileURL.path) { + output = try? String(contentsOf: outputFileURL, encoding: .utf8) + } + + log.info("Shortcut '\(name, privacy: .public)' completed successfully") + + if let output = output, !output.isEmpty { + return .object([ + "success": .bool(true), + "shortcut": .string(name), + "output": .string(output), + ]) + } + + return .object([ + "success": .bool(true), + "shortcut": .string(name), + ]) + } +} From 68a0de420846d5d578aa43134ff6b5ba7ed1edf9 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Fri, 30 Jan 2026 05:30:44 -0800 Subject: [PATCH 2/4] Represent shortcuts with indigo layer icon in menu bar --- App/Controllers/ServerController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/App/Controllers/ServerController.swift b/App/Controllers/ServerController.swift index 19f24ba..255c2bb 100644 --- a/App/Controllers/ServerController.swift +++ b/App/Controllers/ServerController.swift @@ -130,8 +130,8 @@ enum ServiceRegistry { ), ServiceConfig( name: "Shortcuts", - iconName: "bolt.fill", - color: .yellow, + iconName: "square.2.layers.3d", + color: .indigo, service: ShortcutsService.shared, binding: shortcutsEnabled ), From afbe1c76847b6e223942723b036d95027c678f0c Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Fri, 30 Jan 2026 05:31:12 -0800 Subject: [PATCH 3/4] swift format -i -r . --- App/Services/Shortcuts.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/App/Services/Shortcuts.swift b/App/Services/Shortcuts.swift index 626c140..d308404 100644 --- a/App/Services/Shortcuts.swift +++ b/App/Services/Shortcuts.swift @@ -113,7 +113,8 @@ final class ShortcutsService: Service { return .array([]) } - let shortcuts = output + let shortcuts = + output .split(separator: "\n") .map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } From d8086b2f0d9eea0a68d1378d68a8e39ba1775329 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Fri, 30 Jan 2026 05:49:33 -0800 Subject: [PATCH 4/4] Incorporate feedback from review --- App/Services/Shortcuts.swift | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/App/Services/Shortcuts.swift b/App/Services/Shortcuts.swift index d308404..2d0a481 100644 --- a/App/Services/Shortcuts.swift +++ b/App/Services/Shortcuts.swift @@ -83,6 +83,13 @@ final class ShortcutsService: Service { process.standardOutput = outputPipe process.standardError = errorPipe + let outputHandle = outputPipe.fileHandleForReading + let errorHandle = errorPipe.fileHandleForReading + defer { + outputHandle.closeFile() + errorHandle.closeFile() + } + do { try await runProcess(process) } catch { @@ -96,8 +103,8 @@ final class ShortcutsService: Service { ) } - let outputData = (try? outputPipe.fileHandleForReading.readToEnd()) ?? Data() - let errorData = (try? errorPipe.fileHandleForReading.readToEnd()) ?? Data() + let outputData = (try? outputHandle.readToEnd()) ?? Data() + let errorData = (try? errorHandle.readToEnd()) ?? Data() guard process.terminationStatus == 0 else { let errorMessage = String(data: errorData, encoding: .utf8) ?? "Unknown error" @@ -153,7 +160,10 @@ final class ShortcutsService: Service { let errorPipe = Pipe() process.standardError = errorPipe + let errorHandle = errorPipe.fileHandleForReading + defer { errorHandle.closeFile() } + var errorData = Data() do { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { @@ -180,11 +190,18 @@ final class ShortcutsService: Service { if process.isRunning { process.terminate() } + errorData = (try? errorHandle.readToEnd()) ?? Data() + if !errorData.isEmpty { + let stderrMessage = String(data: errorData, encoding: .utf8) ?? "Unknown error" + log.error("Shortcut '\(name, privacy: .public)' stderr: \(stderrMessage, privacy: .public)") + } log.error("Failed to run shortcut '\(name, privacy: .public)': \(error.localizedDescription)") throw error } - let errorData = (try? errorPipe.fileHandleForReading.readToEnd()) ?? Data() + if errorData.isEmpty { + errorData = (try? errorHandle.readToEnd()) ?? Data() + } guard process.terminationStatus == 0 else { let errorMessage = String(data: errorData, encoding: .utf8) ?? "Unknown error"