Skip to main content
Glama
DaemonCommand.swift16.6 kB
import Commander import Foundation import PeekabooBridge import PeekabooCore import PeekabooFoundation /// Manage the Peekaboo headless daemon lifecycle. @MainActor struct DaemonCommand: ParsableCommand { static let commandDescription = CommandDescription( commandName: "daemon", abstract: "Manage the headless Peekaboo daemon", discussion: """ Control the on-demand Peekaboo daemon. Examples: peekaboo daemon start peekaboo daemon status peekaboo daemon stop """, subcommands: [Start.self, Stop.self, Status.self, Run.self], defaultSubcommand: Status.self, showHelpOnEmptyInvocation: false ) } extension DaemonCommand { @MainActor struct Start: OutputFormattable, RuntimeOptionsConfigurable { nonisolated(unsafe) static var commandDescription: CommandDescription { MainActorCommandDescription.describe { CommandDescription( commandName: "start", abstract: "Start the Peekaboo daemon (on-demand)" ) } } @Option(name: .long, help: "Override bridge socket path") var bridgeSocket: String? @Option(name: .long, help: "Window tracker poll interval in milliseconds (default 1000)") var pollIntervalMs: Int? @Option(name: .long, help: "Seconds to wait for daemon startup (default 3)") var waitSeconds: Int = 3 @RuntimeStorage private var runtime: CommandRuntime? var runtimeOptions = CommandRuntimeOptions() private var resolvedRuntime: CommandRuntime { guard let runtime else { preconditionFailure("CommandRuntime must be configured before accessing runtime resources") } return runtime } private var logger: Logger { self.resolvedRuntime.logger } var outputLogger: Logger { self.logger } var jsonOutput: Bool { self.resolvedRuntime.configuration.jsonOutput } mutating func run(using runtime: CommandRuntime) async throws { self.runtime = runtime let socketPath = self.bridgeSocket ?? PeekabooBridgeConstants.peekabooSocketPath let client = DaemonControlClient(socketPath: socketPath) if let status = await client.fetchStatus() { self.output(status) { DaemonStatusPrinter.render(status: status) } return } let executable = Self.resolveExecutablePath() let process = Process() process.executableURL = URL(fileURLWithPath: executable) var args = ["daemon", "run", "--mode", "manual"] if let bridgeSocket { args.append(contentsOf: ["--bridge-socket", bridgeSocket]) } if let pollIntervalMs { args.append(contentsOf: ["--poll-interval-ms", "\(pollIntervalMs)"]) } process.arguments = args let logURL = DaemonPaths.daemonLogURL() let logHandle = try? FileHandle(forWritingTo: logURL) logHandle?.seekToEndOfFile() process.standardOutput = logHandle process.standardError = logHandle process.standardInput = FileHandle.nullDevice try process.run() let deadline = Date().addingTimeInterval(TimeInterval(self.waitSeconds)) while Date() < deadline { if let status = await client.fetchStatus() { self.output(status) { DaemonStatusPrinter.render(status: status) } return } try await Task.sleep(nanoseconds: 200_000_000) } throw PeekabooError.operationError(message: "Daemon did not start within \(self.waitSeconds)s") } private static func resolveExecutablePath() -> String { if let path = CommandLine.arguments.first { return path } return "/usr/local/bin/peekaboo" } } @MainActor struct Stop: OutputFormattable, RuntimeOptionsConfigurable { nonisolated(unsafe) static var commandDescription: CommandDescription { MainActorCommandDescription.describe { CommandDescription( commandName: "stop", abstract: "Stop the Peekaboo daemon" ) } } @Option(name: .long, help: "Override bridge socket path") var bridgeSocket: String? @Option(name: .long, help: "Seconds to wait for daemon shutdown (default 3)") var waitSeconds: Int = 3 @RuntimeStorage private var runtime: CommandRuntime? var runtimeOptions = CommandRuntimeOptions() private var resolvedRuntime: CommandRuntime { guard let runtime else { preconditionFailure("CommandRuntime must be configured before accessing runtime resources") } return runtime } private var logger: Logger { self.resolvedRuntime.logger } var outputLogger: Logger { self.logger } var jsonOutput: Bool { self.resolvedRuntime.configuration.jsonOutput } mutating func run(using runtime: CommandRuntime) async throws { self.runtime = runtime let socketPath = self.bridgeSocket ?? PeekabooBridgeConstants.peekabooSocketPath let client = DaemonControlClient(socketPath: socketPath) guard let status = await client.fetchStatus() else { let stopped = PeekabooDaemonStatus(running: false) self.output(stopped) { DaemonStatusPrinter.render(status: stopped) } return } if status.mode == nil { throw PeekabooError.operationError(message: "Connected host does not support daemon stop") } let stopped = try await client.stopDaemon() guard stopped else { throw PeekabooError.operationError(message: "Daemon refused stop request") } let deadline = Date().addingTimeInterval(TimeInterval(self.waitSeconds)) while Date() < deadline { if await client.fetchStatus() == nil { let stopped = PeekabooDaemonStatus(running: false) self.output(stopped) { DaemonStatusPrinter.render(status: stopped) } return } try await Task.sleep(nanoseconds: 200_000_000) } throw PeekabooError.operationError(message: "Daemon did not stop within \(self.waitSeconds)s") } } @MainActor struct Status: OutputFormattable, RuntimeOptionsConfigurable { nonisolated(unsafe) static var commandDescription: CommandDescription { MainActorCommandDescription.describe { CommandDescription( commandName: "status", abstract: "Show daemon status" ) } } @Option(name: .long, help: "Override bridge socket path") var bridgeSocket: String? @RuntimeStorage private var runtime: CommandRuntime? var runtimeOptions = CommandRuntimeOptions() private var resolvedRuntime: CommandRuntime { guard let runtime else { preconditionFailure("CommandRuntime must be configured before accessing runtime resources") } return runtime } private var logger: Logger { self.resolvedRuntime.logger } var outputLogger: Logger { self.logger } var jsonOutput: Bool { self.resolvedRuntime.configuration.jsonOutput } mutating func run(using runtime: CommandRuntime) async throws { self.runtime = runtime let socketPath = self.bridgeSocket ?? PeekabooBridgeConstants.peekabooSocketPath let client = DaemonControlClient(socketPath: socketPath) if let status = await client.fetchStatus() { self.output(status) { DaemonStatusPrinter.render(status: status) } } else { let stopped = PeekabooDaemonStatus(running: false) self.output(stopped) { DaemonStatusPrinter.render(status: stopped) } } } } @MainActor struct Run: AsyncRuntimeCommand, CommanderBindableCommand, RuntimeOptionsConfigurable { nonisolated(unsafe) static var commandDescription: CommandDescription { MainActorCommandDescription.describe { CommandDescription( commandName: "run", abstract: "Run the daemon (internal)" ) } } @Option(name: .long, help: "Daemon mode (manual, mcp)") var mode: String = "manual" @Option(name: .long, help: "Override bridge socket path") var bridgeSocket: String? @Option(name: .long, help: "Window tracker poll interval in milliseconds (default 1000)") var pollIntervalMs: Int? @RuntimeStorage private var runtime: CommandRuntime? var runtimeOptions = CommandRuntimeOptions() mutating func run(using runtime: CommandRuntime) async throws { self.runtime = runtime let pollInterval = TimeInterval(Double(self.pollIntervalMs ?? 1000) / 1000.0) let socketPath = self.bridgeSocket ?? PeekabooBridgeConstants.peekabooSocketPath let config: PeekabooDaemon.Configuration = if self.mode.lowercased() == "mcp" { .mcp(bridgeSocketPath: socketPath, windowPollInterval: pollInterval) } else { .manual(bridgeSocketPath: socketPath, windowPollInterval: pollInterval) } let daemon = PeekabooDaemon(configuration: config) await daemon.runUntilStop() } mutating func applyCommanderValues(_ values: CommanderBindableValues) throws { if let modeOption = values.singleOption("mode") { self.mode = modeOption } if let socketOption = values.singleOption("bridge-socket") { self.bridgeSocket = socketOption } if let pollMs = try values.decodeOption("pollIntervalMs", as: Int.self) { self.pollIntervalMs = pollMs } } } } extension DaemonCommand.Start: AsyncRuntimeCommand {} @MainActor extension DaemonCommand.Start: CommanderBindableCommand { mutating func applyCommanderValues(_ values: CommanderBindableValues) throws { self.bridgeSocket = values.singleOption("bridge-socket") self.pollIntervalMs = try values.decodeOption("pollIntervalMs", as: Int.self) if let waitSeconds = try values.decodeOption("waitSeconds", as: Int.self) { self.waitSeconds = waitSeconds } } } extension DaemonCommand.Stop: AsyncRuntimeCommand {} @MainActor extension DaemonCommand.Stop: CommanderBindableCommand { mutating func applyCommanderValues(_ values: CommanderBindableValues) throws { self.bridgeSocket = values.singleOption("bridge-socket") if let waitSeconds = try values.decodeOption("waitSeconds", as: Int.self) { self.waitSeconds = waitSeconds } } } extension DaemonCommand.Status: AsyncRuntimeCommand {} @MainActor extension DaemonCommand.Status: CommanderBindableCommand { mutating func applyCommanderValues(_ values: CommanderBindableValues) throws { self.bridgeSocket = values.singleOption("bridge-socket") } } private struct DaemonControlClient { let socketPath: String func fetchStatus() async -> PeekabooDaemonStatus? { let client = PeekabooBridgeClient(socketPath: self.socketPath) do { return try await client.daemonStatus() } catch let envelope as PeekabooBridgeErrorEnvelope { if envelope.code == .operationNotSupported { return await self.fallbackHandshake(client: client) } return nil } catch { return nil } } func stopDaemon() async throws -> Bool { let client = PeekabooBridgeClient(socketPath: self.socketPath) return try await client.daemonStop() } private func fallbackHandshake(client: PeekabooBridgeClient) async -> PeekabooDaemonStatus? { let identity = PeekabooBridgeClientIdentity( bundleIdentifier: Bundle.main.bundleIdentifier, teamIdentifier: nil, processIdentifier: getpid(), hostname: Host.current().name ) do { let handshake = try await client.handshake(client: identity) let bridge = PeekabooDaemonBridgeStatus( socketPath: self.socketPath, hostKind: handshake.hostKind, allowedOperations: handshake.supportedOperations ) return PeekabooDaemonStatus( running: true, pid: nil, startedAt: nil, mode: nil, bridge: bridge, permissions: handshake.permissions, snapshots: nil, windowTracker: nil ) } catch { return nil } } } private enum DaemonPaths { static func daemonLogURL() -> URL { let root = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent(".peekaboo") try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) return root.appendingPathComponent("daemon.log") } } private enum DaemonStatusPrinter { static func render(status: PeekabooDaemonStatus) { print("Peekaboo Daemon") print("==============") guard status.running else { print("Status: not running") return } if let mode = status.mode { print("Mode: \(mode.rawValue)") } if let pid = status.pid { print("PID: \(pid)") } if let startedAt = status.startedAt { print("Started: \(Self.formatDate(startedAt))") } if let bridge = status.bridge { print("") print("Bridge") print("------") print("Socket: \(bridge.socketPath)") print("Host: \(bridge.hostKind.rawValue)") print("Ops: \(bridge.allowedOperations.count)") } if let permissions = status.permissions { print("") print("Permissions") print("-----------") print("Screen Recording: \(permissions.screenRecording ? "granted" : "missing")") print("Accessibility: \(permissions.accessibility ? "granted" : "missing")") if permissions.appleScript { print("AppleScript: granted") } } if let snapshots = status.snapshots { print("") print("Snapshots") print("---------") print("Backend: \(snapshots.backend)") print("Count: \(snapshots.snapshotCount)") if let lastAccessedAt = snapshots.lastAccessedAt { print("Last Access: \(Self.formatDate(lastAccessedAt))") } print("Path: \(snapshots.storagePath)") } if let tracker = status.windowTracker { print("") print("Window Tracker") print("--------------") print("Tracked Windows: \(tracker.trackedWindows)") if let lastEventAt = tracker.lastEventAt { print("Last Event: \(Self.formatDate(lastEventAt))") } if let lastPollAt = tracker.lastPollAt { print("Last Poll: \(Self.formatDate(lastPollAt))") } print("AX Observers: \(tracker.axObserverCount)") print("Poll Interval: \(tracker.cgPollIntervalMs)ms") } } private static func formatDate(_ date: Date) -> String { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter.string(from: date) } }

Latest Blog Posts

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