{
  "format" : 1,
  "manifest" : {
    "capabilities" : [
      {
        "power" : {

        }
      }
    ],
    "homepage" : "https://github.com/navbytes/stackbar-plugins/tree/main/System/StayAwake",
    "id" : "io.navbytes.stackbar.stay-awake",
    "minimumStackbar" : "0.11.0",
    "name" : "Stay Awake",
    "summary" : "Keeps your Mac awake for a while, until a time, while an app runs or on a schedule.",
    "surfaces" : [
      "menuBar",
      "panel",
      "widget",
      "settings"
    ],
    "triggers" : [
      {
        "launch" : {

        }
      },
      {
        "every" : {
          "seconds" : 15
        }
      },
      {
        "wake" : {

        }
      }
    ],
    "version" : "1.0.1"
  },
  "publisher" : {
    "name" : "navbytes",
    "publicKey" : "4dpLtXEpFXdi+6cviY/KdYuUulJPbEmCxxIK0EEN2/w="
  },
  "signature" : "1TsbsmUnCEEy30frZzlG2/tnqfnMgeUtSVzQ3SenLdXb5JqS1kNq7u9p6qDrl4Cnr4yTWZKM2fhY9694FRA+AA==",
  "source" : "import StackbarKit\nimport Foundation\n\n/// Keeps the Mac awake: for a while, until a time, while an app runs, on a\n/// weekday schedule, or until you say stop. It lets go when the battery runs\n/// low, and the host releases the assertion if the plugin ever stops or\n/// crashes, so it can't leave the Mac awake by accident.\n@main\nfinal class StayAwake: StackbarPlugin {\n    static let manifest = Manifest(\n        id: \"io.navbytes.stackbar.stay-awake\",\n        name: \"Stay Awake\",\n        version: \"1.0.1\",\n        summary: \"Keeps your Mac awake for a while, until a time, while an app runs or on a schedule.\",\n        surfaces: [.menuBar, .panel, .widget, .settings],\n        triggers: [.launch, .every(.seconds(15)), .wake],\n        capabilities: [.power],\n        minimumStackbar: \"0.11.0\",\n        homepage: \"https://github.com/navbytes/stackbar-plugins/tree/main/System/StayAwake\"\n    )\n\n    /// What the current session waits for before it ends.\n    enum Mode: Codable, Equatable {\n        case indefinitely\n        case until(Date)\n        case whileRunning(appID: String, name: String)\n    }\n\n    struct Session: Codable, Equatable {\n        var mode: Mode\n        var started: Date\n        var total: Double? {\n            if case .until(let end) = mode { return end.timeIntervalSince(started) }\n            return nil\n        }\n    }\n\n    static let durations: [(title: String, minutes: Double)] = [(\"15 min\", 15), (\"30 min\", 30), (\"1 hour\", 60), (\"2 hours\", 120), (\"4 hours\", 240), (\"8 hours\", 480)]\n\n    @Stored(\"session\") var session: Session? = nil\n    @Stored(\"keepDisplayOn\") var keepDisplayOn = true\n    @Stored(\"batteryFloor\") var batteryFloor = 20.0\n    @Stored(\"pauseOnLowPower\") var pauseOnLowPower = true\n    @Stored(\"notifyOnEnd\") var notifyOnEnd = true\n    @Stored(\"scheduleOn\") var scheduleOn = false\n    @Stored(\"whilePluggedIn\") var whilePluggedIn = false\n    @Stored(\"whileExternalDisplay\") var whileExternalDisplay = false\n    @Stored(\"whileVPN\") var whileVPN = false\n    @Stored(\"whileDownloading\") var whileDownloading = false\n    /// What a click on the menu-bar cup does: -1 opens the panel, 0 starts\n    /// until stopped, otherwise starts for that many minutes. Clicking again\n    /// stops; a right click always opens the panel.\n    @Stored(\"clickMinutes\") var clickMinutes = 0.0\n    @Stored(\"scheduleStart\") var scheduleStart = \"09:00\"\n    @Stored(\"scheduleEnd\") var scheduleEnd = \"18:00\"\n    @Stored(\"scheduleWeekdaysOnly\") var scheduleWeekdaysOnly = true\n    /// Seconds awake per day (yyyy-MM-dd), for the last two weeks.\n    @Stored(\"awakeByDay\") var awakeByDay: [String: Double] = [:]\n\n    var status = SystemStatus()\n    /// What the host is holding for us right now. The host drops it when the\n    /// helper restarts, so this starts empty and the first refresh re-asserts.\n    var held: KeepAwakeRequest?\n    var heldReason: String?\n    var lastTick: Date?\n    var lastError: String?\n    var lastEnded: String?\n    /// The end time the 5-minute warning was sent for.\n    var warnedFor: Date?\n    /// Battery percentage once a minute while awake, for the session chart.\n    var batteryTrail: [Double] = []\n    var lastSample: Date?\n    /// The last network counter reading, for the download rate.\n    var lastBytes: (count: UInt64, at: Date)?\n    /// Bytes per second received, one sample per refresh (about 10 minutes).\n    var rateTrail: [Double] = []\n    /// A download keeps the Mac awake until this long after it goes quiet,\n    /// so a pause between files doesn't let it sleep.\n    var downloadUntil: Date?\n    static let downloadThreshold = 100_000.0\n    static let downloadGrace = 120.0\n\n    // MARK: Deciding\n\n    func refresh() async throws {\n        do {\n            status = try await Stackbar.systemStatus()\n        } catch {\n            lastError = \"\\(error)\"\n        }\n        endSessionIfDone()\n        sampleNetwork()\n        await apply()\n    }\n\n    /// Turns the host's byte counter into a rate and decides whether a\n    /// download is running.\n    func sampleNetwork(now: Date = Date()) {\n        guard let count = status.networkBytesReceived else { return }\n        defer { lastBytes = (count, now) }\n        guard let last = lastBytes, count >= last.count else { return }\n        let seconds = now.timeIntervalSince(last.at)\n        guard seconds >= 1 else { return }\n        let rate = Double(count - last.count) / seconds\n        rateTrail.append(rate)\n        if rateTrail.count > 40 { rateTrail.removeFirst(rateTrail.count - 40) }\n        if rate >= Self.downloadThreshold { downloadUntil = now.addingTimeInterval(Self.downloadGrace) }\n    }\n\n    var downloading: Bool { (downloadUntil ?? .distantPast) > Date() }\n\n    static func rate(_ bytes: Double) -> String {\n        if bytes >= 1_000_000 { return String(format: \"%.1f MB/s\", bytes / 1_000_000) }\n        return \"\\(Int((bytes / 1000).rounded())) KB/s\"\n    }\n\n    /// Why the Mac should stay awake right now, or `nil` to let it sleep.\n    var reason: String? {\n        if blocker != nil { return nil }\n        if let session { return describe(session.mode) }\n        if scheduleOn, inSchedule(Date()) { return \"Schedule until \\(scheduleEnd)\" }\n        if whilePluggedIn, status.onACPower { return \"While plugged in\" }\n        if whileExternalDisplay, (status.externalDisplays ?? 0) > 0 { return \"While a display is connected\" }\n        if whileVPN, status.vpnActive == true { return \"While the VPN is on\" }\n        if whileDownloading, downloading { return \"While a download is running\" }\n        return nil\n    }\n\n    /// Battery or Low Power Mode holding things back.\n    var blocker: String? {\n        if !status.onACPower, let level = status.batteryLevel, batteryFloor > 0, level * 100 < batteryFloor {\n            return \"Battery below \\(Int(batteryFloor))%\"\n        }\n        if pauseOnLowPower, status.lowPowerMode { return \"Low Power Mode is on\" }\n        return nil\n    }\n\n    func endSessionIfDone(now: Date = Date()) {\n        guard let session else { return }\n        switch session.mode {\n        case .indefinitely:\n            return\n        case .until(let end):\n            if now >= end {\n                finish(\"Time's up\")\n            } else if notifyOnEnd, end.timeIntervalSince(now) <= 5 * 60, warnedFor != end, session.total ?? 0 > 10 * 60 {\n                warnedFor = end\n                Stackbar.notify(\"Your Mac can sleep in 5 minutes\", body: \"Open Stay Awake to add more time.\")\n            }\n        case .whileRunning(let appID, let name):\n            if !status.runningApps.isEmpty, !status.runningApps.contains(where: { $0.id == appID }) { finish(\"\\(name) quit\") }\n        }\n    }\n\n    func finish(_ why: String) {\n        session = nil\n        lastEnded = why\n        if notifyOnEnd { Stackbar.notify(\"Your Mac can sleep again\", body: why) }\n    }\n\n    /// Brings the host's assertion in line with `reason`, and counts awake time.\n    func apply(now: Date = Date()) async {\n        if let lastTick, held != nil {\n            let day = Self.day(now)\n            awakeByDay[day, default: 0] += min(now.timeIntervalSince(lastTick), 60)\n            if awakeByDay.count > 14 { awakeByDay = awakeByDay.filter { $0.key >= Self.day(now.addingTimeInterval(-13 * 86400)) } }\n        }\n        lastTick = now\n        if held != nil, let level = status.batteryLevel, now.timeIntervalSince(lastSample ?? .distantPast) >= 60 {\n            batteryTrail.append((level * 1000).rounded() / 10)\n            if batteryTrail.count > 240 { batteryTrail.removeFirst(batteryTrail.count - 240) }\n            lastSample = now\n        }\n        let wanted = reason.map { KeepAwakeRequest(display: keepDisplayOn, reason: $0) }\n        guard wanted != held else { return }\n        do {\n            if let wanted {\n                try await Stackbar.keepAwake(display: wanted.display, reason: wanted.reason)\n            } else {\n                try await Stackbar.allowSleep()\n            }\n            held = wanted\n            lastError = nil\n        } catch {\n            lastError = \"\\(error)\"\n        }\n    }\n\n    func start(_ mode: Mode) async {\n        if session == nil { batteryTrail = [] }\n        session = Session(mode: mode, started: Date())\n        lastEnded = nil\n        await apply()\n    }\n\n    func stop() async {\n        session = nil\n        lastEnded = nil\n        await apply()\n    }\n\n    // MARK: Time helpers\n\n    func describe(_ mode: Mode) -> String {\n        switch mode {\n        case .indefinitely: return \"Until you stop it\"\n        case .until(let end): return \"Until \\(end.formatted(date: .omitted, time: .shortened))\"\n        case .whileRunning(_, let name): return \"While \\(name) is running\"\n        }\n    }\n\n    static func day(_ date: Date) -> String {\n        let parts = Calendar.current.dateComponents([.year, .month, .day], from: date)\n        return String(format: \"%04d-%02d-%02d\", parts.year ?? 0, parts.month ?? 0, parts.day ?? 0)\n    }\n\n    /// `\"17:30\"`, `\"9\"` or `\"5:30pm\"` as minutes after midnight.\n    static func minutes(_ text: String) -> Int? {\n        var text = text.lowercased().replacingOccurrences(of: \" \", with: \"\")\n        var meridiem: String?\n        for suffix in [\"am\", \"pm\"] where text.hasSuffix(suffix) {\n            meridiem = suffix\n            text.removeLast(2)\n        }\n        let parts = text.split(separator: \":\", omittingEmptySubsequences: false)\n        guard (1...2).contains(parts.count), let hour = Int(parts[0]) else { return nil }\n        guard let minute = parts.count == 2 ? Int(parts[1]) : 0, (0...59).contains(minute) else { return nil }\n        if let meridiem {\n            guard (1...12).contains(hour) else { return nil }\n            return (hour % 12 + (meridiem == \"pm\" ? 12 : 0)) * 60 + minute\n        }\n        guard (0...23).contains(hour) else { return nil }\n        return hour * 60 + minute\n    }\n\n    /// `\"90\"`, `\"90m\"`, `\"2h\"`, `\"1h30\"` or `\"1:30\"` as seconds.\n    static func duration(_ text: String) -> Double? {\n        let text = text.lowercased().replacingOccurrences(of: \" \", with: \"\").replacingOccurrences(of: \"min\", with: \"m\")\n        if let minutes = Double(text.hasSuffix(\"m\") ? String(text.dropLast()) : text), minutes > 0 { return minutes * 60 }\n        let parts = text.split(whereSeparator: { $0 == \"h\" || $0 == \":\" }).map(String.init)\n        guard text.contains(\"h\") || text.contains(\":\"), let hours = parts.first.flatMap(Double.init) else { return nil }\n        let minutes = parts.count > 1 ? Double(parts[1].replacingOccurrences(of: \"m\", with: \"\")) : 0\n        guard let minutes, hours * 60 + minutes > 0 else { return nil }\n        return (hours * 60 + minutes) * 60\n    }\n\n    /// The next time `text` (a clock time) happens, today or tomorrow.\n    static func next(_ text: String, after now: Date = Date()) -> Date? {\n        guard let minutes = minutes(text) else { return nil }\n        let today = Calendar.current.startOfDay(for: now).addingTimeInterval(Double(minutes) * 60)\n        return today > now ? today : Calendar.current.date(byAdding: .day, value: 1, to: today)\n    }\n\n    func inSchedule(_ date: Date) -> Bool {\n        guard let start = Self.minutes(scheduleStart), let end = Self.minutes(scheduleEnd) else { return false }\n        let calendar = Calendar.current\n        if scheduleWeekdaysOnly, calendar.isDateInWeekend(date) { return false }\n        let parts = calendar.dateComponents([.hour, .minute], from: date)\n        let now = (parts.hour ?? 0) * 60 + (parts.minute ?? 0)\n        return start <= end ? (start..<end).contains(now) : (now >= start || now < end)\n    }\n\n    static func clock(_ seconds: Double) -> String {\n        let total = max(0, Int(seconds.rounded(.up)))\n        let hours = total / 3600\n        let minutes = (total % 3600) / 60\n        if hours > 0 { return \"\\(hours)h \\(minutes)m\" }\n        if minutes > 0 { return \"\\(minutes)m\" }\n        return \"\\(total)s\"\n    }\n\n    var remaining: Double? {\n        if case .until(let end)? = session?.mode { return end.timeIntervalSinceNow }\n        return nil\n    }\n\n    // MARK: Surfaces\n\n    static let clickChoices: [(title: String, minutes: Double)] = [(\"Opens the panel\", -1), (\"Starts until I stop it\", 0)] + durations.map { (\"Starts for \\($0.title)\", $0.minutes) }\n\n    var menuBarClick: MenuBarAction? {\n        guard clickMinutes >= 0 else { return nil }\n        if session != nil {\n            return MenuBarAction(\"Click to stop keeping awake\") { await self.stop() }\n        }\n        let minutes = clickMinutes\n        let label = minutes == 0 ? \"until you stop it\" : \"for \\(Self.durations.first { $0.minutes == minutes }?.title ?? \"\\(Int(minutes)) min\")\"\n        return MenuBarAction(\"Click to keep awake \\(label)\") {\n            await self.start(minutes == 0 ? .indefinitely : .until(Date().addingTimeInterval(minutes * 60)))\n        }\n    }\n\n    var menuBar: ViewContent {\n        if held != nil {\n            Label(remaining.map(Self.clock) ?? \"\", symbol: \"cup.and.heat.waves.fill\").role(.accent)\n        } else if session != nil, blocker != nil {\n            Label(\"\", symbol: \"cup.and.saucer\").role(.warning)\n        } else {\n            Label(\"\", symbol: \"cup.and.saucer\").role(.secondary)\n        }\n    }\n\n    var panel: ViewContent {\n        if let reason, held != nil {\n            Label(\"Staying awake\", symbol: \"cup.and.heat.waves.fill\").role(.accent)\n            Text(reason + (keepDisplayOn ? \", display on\" : \", display may sleep\")).style(.caption).role(.secondary)\n            if let session, let total = session.total, let remaining {\n                ProgressBar(\"\\(Self.clock(remaining)) left\", value: total - remaining, total: total, role: .accent)\n            }\n            if session != nil {\n                HStack {\n                    Button(\"Stop\", symbol: \"stop.fill\", role: .critical) { await self.stop() }\n                    if let remaining {\n                        Button(\"+30 min\", symbol: \"plus\") { await self.start(.until(Date().addingTimeInterval(remaining + 1800))) }\n                    }\n                }\n            }\n        } else {\n            Label(\"Your Mac sleeps normally\", symbol: \"moon.zzz\").role(.secondary)\n            if let blocker, session != nil {\n                Text(\"Paused: \\(blocker)\").role(.warning)\n            } else if let lastEnded {\n                Text(\"Last session ended: \\(lastEnded)\").style(.caption).role(.secondary)\n            }\n        }\n        if let lastError {\n            Text(lastError).style(.caption).role(.warning)\n        }\n        Section(\"Keep awake\") {\n            Row(\"Until I stop it\", symbol: \"infinity\", key: \"indefinitely\")\n                .action(\"Start\") { await self.start(.indefinitely) }\n            ForEach(Self.durations) { option in\n                Row(\"For \\(option.title)\", symbol: \"timer\", key: \"for-\\(Int(option.minutes))\")\n                    .action(\"Start\") { await self.start(.until(Date().addingTimeInterval(option.minutes * 60))) }\n            }\n            TextField(\"For a while, like 90m or 1h30\") { text in\n                guard let seconds = Self.duration(text) else {\n                    self.lastError = \"\\\"\\(text)\\\" isn't a duration; try 45m, 2h or 1h30\"\n                    return\n                }\n                await self.start(.until(Date().addingTimeInterval(seconds)))\n            }\n            TextField(\"Until a time, like 17:30\") { text in\n                guard let end = Self.next(text) else {\n                    self.lastError = \"\\\"\\(text)\\\" isn't a time; try 17:30 or 5:30pm\"\n                    return\n                }\n                await self.start(.until(end))\n            }\n            let apps = status.runningApps.filter { $0.id != \"io.navbytes.stackbar\" }.sorted { $0.name < $1.name }\n            if !apps.isEmpty {\n                Picker(\"While an app runs\", options: apps.map(\\.name), selection: whileRunningName) { name in\n                    guard let app = apps.first(where: { $0.name == name }) else { return }\n                    await self.start(.whileRunning(appID: app.id, name: app.name))\n                }\n            }\n        }\n        Toggle(\"Keep the display on\", isOn: keepDisplayOn) { on in\n            self.keepDisplayOn = on\n            await self.apply()\n        }\n        Section(\"This week\") {\n            let week = hoursAwake(days: 7)\n            Chart(\"Hours awake\", values: week.values, kind: .bar, labels: week.labels, unit: \"h\", highlight: 6)\n            HStack {\n                LabeledContent(\"Today\", value: Self.clock(awakeByDay[Self.day(Date())] ?? 0))\n                LabeledContent(\"Average\", value: Self.clock(week.values.reduce(0, +) / 7 * 3600))\n            }\n        }\n        if whileDownloading, rateTrail.count >= 3, rateTrail.contains(where: { $0 >= Self.downloadThreshold }) {\n            Chart(\"Download speed\", values: rateTrail.map { ($0 / 10_000).rounded() / 100 }, kind: .area, role: downloading ? .accent : .secondary, unit: \"MB/s\", goal: Self.downloadThreshold / 1_000_000)\n            Text(downloading ? \"Downloading at \\(Self.rate(rateTrail.last ?? 0)); staying awake until it's quiet for 2 minutes\" : \"No download running\").style(.caption).role(.secondary)\n        }\n        if let level = status.batteryLevel {\n            if held != nil, batteryTrail.count >= 3 {\n                Chart(\"Battery while awake\", values: batteryTrail, kind: .area, role: level < 0.2 ? .critical : status.onACPower ? .success : .accent, unit: \"%\")\n            } else {\n                LabeledContent(\"Battery\", value: \"\\(Int((level * 100).rounded()))%\" + (status.onACPower ? \", on power\" : \"\"))\n            }\n        }\n    }\n\n    /// Hours awake per day for the last `days` days, oldest first, with\n    /// short weekday names (or day numbers past a week).\n    func hoursAwake(days: Int, now: Date = Date()) -> (values: [Double], labels: [String]) {\n        let calendar = Calendar.current\n        let dates = (0..<days).reversed().map { now.addingTimeInterval(Double(-$0) * 86400) }\n        let values = dates.map { ((awakeByDay[Self.day($0)] ?? 0) / 360).rounded() / 10 }\n        let labels = dates.map { date in\n            days <= 7 ? calendar.shortWeekdaySymbols[calendar.component(.weekday, from: date) - 1] : String(calendar.component(.day, from: date))\n        }\n        return (values, labels)\n    }\n\n    /// Stackbar's Settings window, so the panel stays about starting and stopping.\n    var settings: ViewContent {\n        Section(\"Menu bar\") {\n            Picker(\"Clicking the cup\", options: Self.clickChoices.map(\\.title), selection: Self.clickChoices.first { $0.minutes == clickMinutes }?.title ?? Self.clickChoices[1].title, key: \"click\") { title in\n                self.clickMinutes = Self.clickChoices.first { $0.title == title }?.minutes ?? 0\n            }\n            Text(\"A right click always opens the panel.\").style(.caption).role(.secondary)\n        }\n        Section(\"Stop or pause\") {\n            Slider(batteryFloor > 0 ? \"Stop below \\(Int(batteryFloor))% battery\" : \"Ignore the battery level\", value: batteryFloor, in: 0...50, step: 5, key: \"battery\") { value in\n                self.batteryFloor = value\n                await self.apply()\n            }\n            Toggle(\"Pause in Low Power Mode\", isOn: pauseOnLowPower) { on in\n                self.pauseOnLowPower = on\n                await self.apply()\n            }\n            Toggle(\"Notify 5 minutes before and when a session ends\", isOn: notifyOnEnd, key: \"notify\") { self.notifyOnEnd = $0 }\n        }\n        Section(\"Automatically\") {\n            Toggle(\"Always stay awake while plugged in\", isOn: whilePluggedIn) { on in\n                self.whilePluggedIn = on\n                await self.apply()\n            }\n            Toggle(\"While an external display is connected\", isOn: whileExternalDisplay, key: \"display\") { on in\n                self.whileExternalDisplay = on\n                await self.apply()\n            }\n            Toggle(\"While a VPN is connected\", isOn: whileVPN, key: \"vpn\") { on in\n                self.whileVPN = on\n                await self.apply()\n            }\n            Toggle(\"While a download is running\", isOn: whileDownloading, key: \"download\") { on in\n                self.whileDownloading = on\n                await self.apply()\n            }\n            Text(\"A download is anything over \\(Self.rate(Self.downloadThreshold)); the Mac stays awake until it's been quiet for 2 minutes.\").style(.caption).role(.secondary)\n            Text(\"To work with the lid closed, connect power and a display (macOS calls it clamshell mode), and turn on the display option above so it doesn't idle-sleep.\").style(.caption).role(.secondary)\n        }\n        Section(\"Schedule\") {\n            Toggle(scheduleWeekdaysOnly ? \"Weekdays \\(scheduleStart)–\\(scheduleEnd)\" : \"Every day \\(scheduleStart)–\\(scheduleEnd)\", isOn: scheduleOn, key: \"schedule\") { on in\n                self.scheduleOn = on\n                await self.apply()\n            }\n            TextField(\"Schedule starts (\\(scheduleStart))\", key: \"schedule-start\") { text in await self.setSchedule(start: text) }\n            TextField(\"Schedule ends (\\(scheduleEnd))\", key: \"schedule-end\") { text in await self.setSchedule(end: text) }\n            Toggle(\"Weekdays only\", isOn: scheduleWeekdaysOnly) { on in\n                self.scheduleWeekdaysOnly = on\n                await self.apply()\n            }\n        }\n        let fortnight = hoursAwake(days: 14)\n        if fortnight.values.contains(where: { $0 > 0 }) {\n            Section(\"Activity\") {\n                Chart(\"Hours awake, last 14 days\", values: fortnight.values, kind: .area, labels: fortnight.labels, unit: \"h\", highlight: 13)\n                LabeledContent(\"In all\", value: Self.clock(fortnight.values.reduce(0, +) * 3600))\n            }\n        }\n    }\n\n    var widget: ViewContent {\n        Label(held != nil ? \"Awake\" : \"Sleeps normally\", symbol: held != nil ? \"cup.and.heat.waves.fill\" : \"cup.and.saucer\")\n        if let reason, held != nil { Text(reason).style(.caption) }\n        LabeledContent(\"Today\", value: Self.clock(awakeByDay[Self.day(Date())] ?? 0))\n    }\n\n    var whileRunningName: String? {\n        if case .whileRunning(_, let name)? = session?.mode { return name }\n        return nil\n    }\n\n    func setSchedule(start: String? = nil, end: String? = nil) async {\n        for text in [start, end].compactMap({ $0 }) where Self.minutes(text) == nil {\n            lastError = \"\\\"\\(text)\\\" isn't a time; try 09:00 or 6pm\"\n            return\n        }\n        if let start { scheduleStart = start.trimmingCharacters(in: .whitespaces) }\n        if let end { scheduleEnd = end.trimmingCharacters(in: .whitespaces) }\n        lastError = nil\n        await apply()\n    }\n\n    // MARK: Agents\n\n    var tools: [AgentTool] {\n        AgentTool(\"awake_status\", description: \"Whether the Mac is being kept awake, why, and for how long\") { _ in\n            guard let reason = self.reason, self.held != nil else { return \"The Mac sleeps normally.\" + (self.blocker.map { \" (\\($0))\" } ?? \"\") }\n            return \"Staying awake: \\(reason)\" + (self.remaining.map { \", \\(Self.clock($0)) left\" } ?? \"\")\n        }\n        AgentTool(\"keep_awake\", description: \"Keep the Mac awake, for a number of minutes or until stopped\", parameters: [ToolParameter(\"minutes\", .number, description: \"Minutes; leave out to keep awake until stopped\", required: false)]) { arguments in\n            if let minutes = arguments.number(\"minutes\"), minutes > 0 {\n                await self.start(.until(Date().addingTimeInterval(minutes * 60)))\n                return \"Keeping the Mac awake for \\(Self.clock(minutes * 60)).\"\n            }\n            await self.start(.indefinitely)\n            return \"Keeping the Mac awake until stopped.\"\n        }\n        AgentTool(\"allow_sleep\", description: \"Stop keeping the Mac awake\") { _ in\n            await self.stop()\n            return \"The Mac can sleep again.\"\n        }\n    }\n}\n",
  "sourceDigest" : "b6e424fef65e8a638fac30ad6dce3962b07114e79e11f5c535e7333235b48b6c"
}