diff --git a/Sources/ICloudCLICore/CommandLine.swift b/Sources/ICloudCLICore/CommandLine.swift
index 8420660..6d9a8cc 100644
--- a/Sources/ICloudCLICore/CommandLine.swift
+++ b/Sources/ICloudCLICore/CommandLine.swift
@@ -77,6 +77,36 @@ public struct ShortcutsListOptions: Equatable, Sendable {
}
}
+public struct StorageStatusOptions: Equatable, Sendable {
+ public var format: OutputFormat
+ public var cacheFile: URL
+
+ public init(format: OutputFormat = .json, cacheFile: URL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Preferences/MobileMeAccounts.plist")) {
+ self.format = format
+ self.cacheFile = cacheFile
+ }
+}
+
+public struct FocusStatusOptions: Equatable, Sendable {
+ public var format: OutputFormat
+ public var focusDirectory: URL
+
+ public init(format: OutputFormat = .json, focusDirectory: URL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/DoNotDisturb")) {
+ self.format = format
+ self.focusDirectory = focusDirectory
+ }
+}
+
+public struct DevicesListOptions: Equatable, Sendable {
+ public var format: OutputFormat
+ public var cacheFile: URL
+
+ public init(format: OutputFormat = .json, cacheFile: URL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Preferences/MobileMeAccounts.plist")) {
+ self.format = format
+ self.cacheFile = cacheFile
+ }
+}
+
public struct CloudTabsProbeOptions: Equatable, Sendable {
public var format: OutputFormat
public var safariDirectory: URL
@@ -89,13 +119,16 @@ public struct CloudTabsProbeOptions: Equatable, Sendable {
public enum CLICommand: Equatable, Sendable {
case cloudTabsProbe(CloudTabsProbeOptions)
+ case devicesList(DevicesListOptions)
case driveContainers(DriveContainersOptions)
case driveList(DriveListOptions)
+ case focusStatus(FocusStatusOptions)
case safariBookmarks(SafariBookmarksOptions)
case safariFrequentlyVisited(SafariFrequentlyVisitedOptions)
case safariReadingList(SafariBookmarksOptions)
case safariTabs(SafariTabsOptions)
case shortcutsList(ShortcutsListOptions)
+ case storageStatus(StorageStatusOptions)
case help
case version
}
@@ -125,6 +158,21 @@ public struct CLIParser: Sendable {
if tokens == ["--version"] || tokens == ["-V"] { return .version }
let topCommand = tokens.removeFirst()
+ if topCommand == "storage" {
+ guard tokens.first == "status" else { throw CLIParseError.unknownCommand((["storage"] + tokens).joined(separator: " ")) }
+ tokens.removeFirst()
+ return .storageStatus(try parseStorageStatusOptions(tokens))
+ }
+ if topCommand == "focus" {
+ guard tokens.first == "status" else { throw CLIParseError.unknownCommand((["focus"] + tokens).joined(separator: " ")) }
+ tokens.removeFirst()
+ return .focusStatus(try parseFocusStatusOptions(tokens))
+ }
+ if topCommand == "devices" {
+ guard tokens.first == "list" else { throw CLIParseError.unknownCommand((["devices"] + tokens).joined(separator: " ")) }
+ tokens.removeFirst()
+ return .devicesList(try parseDevicesListOptions(tokens))
+ }
if topCommand == "drive" {
guard let driveCommand = tokens.first else { throw CLIParseError.unknownCommand("drive") }
tokens.removeFirst()
@@ -259,6 +307,48 @@ public struct CLIParser: Sendable {
return options
}
+ private func parseStorageStatusOptions(_ tokens: [String]) throws -> StorageStatusOptions {
+ var options = StorageStatusOptions(); var index = 0
+ while index < tokens.count {
+ let token = tokens[index]
+ switch token {
+ case "--format": options.format = try parseFormat(after: token, in: tokens, at: &index)
+ case "--cache-file": options.cacheFile = try parseURL(after: token, in: tokens, at: &index)
+ default: throw CLIParseError.unknownCommand(token)
+ }
+ index += 1
+ }
+ return options
+ }
+
+ private func parseFocusStatusOptions(_ tokens: [String]) throws -> FocusStatusOptions {
+ var options = FocusStatusOptions(); var index = 0
+ while index < tokens.count {
+ let token = tokens[index]
+ switch token {
+ case "--format": options.format = try parseFormat(after: token, in: tokens, at: &index)
+ case "--focus-dir": options.focusDirectory = try parseURL(after: token, in: tokens, at: &index)
+ default: throw CLIParseError.unknownCommand(token)
+ }
+ index += 1
+ }
+ return options
+ }
+
+ private func parseDevicesListOptions(_ tokens: [String]) throws -> DevicesListOptions {
+ var options = DevicesListOptions(); var index = 0
+ while index < tokens.count {
+ let token = tokens[index]
+ switch token {
+ case "--format": options.format = try parseFormat(after: token, in: tokens, at: &index)
+ case "--cache-file": options.cacheFile = try parseURL(after: token, in: tokens, at: &index)
+ default: throw CLIParseError.unknownCommand(token)
+ }
+ index += 1
+ }
+ return options
+ }
+
private func parseCloudTabsProbeOptions(_ tokens: [String]) throws -> CloudTabsProbeOptions {
var options = CloudTabsProbeOptions(); var index = 0
while index < tokens.count {
@@ -299,6 +389,9 @@ public enum CLIHelp {
icloud-cli \(version)
Usage:
+ icloud-cli storage status [--format json|text] [--cache-file PATH]
+ icloud-cli focus status [--format json|text] [--focus-dir PATH]
+ icloud-cli devices list [--format json|text] [--cache-file PATH]
icloud-cli drive list [--path PATH] [--depth N] [--format json|text] [--icloud-root PATH]
icloud-cli drive containers [--sort-by size|modified|name] [--format json|text] [--icloud-root PATH]
icloud-cli shortcuts list [--name PATTERN] [--format json|text] [--shortcuts-dir PATH]
@@ -309,6 +402,9 @@ Usage:
icloud-cli safari cloud-tabs probe [--format json|text] [--safari-dir PATH]
Commands:
+ storage status Report locally cached iCloud storage quota.
+ focus status Report locally cached Focus / Do Not Disturb status.
+ devices list List locally cached iCloud registered devices.
drive list List files under the local iCloud Drive root without reading file contents.
drive containers
List top-level iCloud app containers.
diff --git a/Sources/ICloudCLICore/CommandRunner.swift b/Sources/ICloudCLICore/CommandRunner.swift
index 5c6143d..95292d7 100644
--- a/Sources/ICloudCLICore/CommandRunner.swift
+++ b/Sources/ICloudCLICore/CommandRunner.swift
@@ -29,6 +29,10 @@ public struct CommandRunner: Sendable {
let report = CloudTabsProbe(safariDirectory: options.safariDirectory).probe()
output(try render(report, format: options.format))
return 0
+ case .devicesList(let options):
+ let devices = try ICloudDevicesReader(cacheFile: options.cacheFile).listDevices()
+ output(try render(devices, format: options.format))
+ return 0
case .driveContainers(let options):
let containers = try ICloudDriveInventoryReader(rootDirectory: options.rootDirectory).listContainers(sortBy: options.sortBy)
output(try render(containers, format: options.format))
@@ -37,6 +41,10 @@ public struct CommandRunner: Sendable {
let files = try ICloudDriveInventoryReader(rootDirectory: options.rootDirectory).listFiles(path: options.path, depth: options.depth)
output(try render(files, format: options.format))
return 0
+ case .focusStatus(let options):
+ let status = try FocusStatusReader(focusDirectory: options.focusDirectory).readStatus()
+ output(try render(status, format: options.format))
+ return 0
case .safariBookmarks(let options):
let bookmarks = try SafariBookmarksReader(safariDirectory: options.safariDirectory).readBookmarks()
output(try render(bookmarks, format: options.format))
@@ -57,6 +65,10 @@ public struct CommandRunner: Sendable {
let shortcuts = try ShortcutsInventoryReader(shortcutsDirectory: options.shortcutsDirectory).listShortcuts(namePattern: options.namePattern)
output(try render(shortcuts, format: options.format))
return 0
+ case .storageStatus(let options):
+ let status = try ICloudStorageStatusReader(cacheFile: options.cacheFile).readStatus()
+ output(try render(status, format: options.format))
+ return 0
}
} catch {
errorOutput(error.localizedDescription)
@@ -171,6 +183,56 @@ public struct CommandRunner: Sendable {
}
}
+ public func render(_ status: ICloudStorageStatus, format: OutputFormat) throws -> String {
+ switch format {
+ case .json:
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
+ encoder.dateEncodingStrategy = .iso8601
+ return String(decoding: try encoder.encode(status), as: UTF8.self)
+ case .text:
+ let used = Double(status.usedBytes) / 1_000_000_000
+ let total = Double(status.totalBytes) / 1_000_000_000
+ let available = Double(status.availableBytes) / 1_000_000_000
+ let account = status.accountEmail ?? "unknown account"
+ return String(format: "iCloud storage: %.1f GB used of %.1f GB, %.1f GB available (%@)", used, total, available, account)
+ }
+ }
+
+ public func render(_ status: FocusStatus, format: OutputFormat) throws -> String {
+ switch format {
+ case .json:
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
+ encoder.dateEncodingStrategy = .iso8601
+ return String(decoding: try encoder.encode(status), as: UTF8.self)
+ case .text:
+ guard let active = status.activeFocus else { return "Focus: none" }
+ if let endsAt = status.endsAt {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "HH:mm"
+ return "Focus: \(active) (until \(formatter.string(from: endsAt)))"
+ }
+ return "Focus: \(active)"
+ }
+ }
+
+ public func render(_ devices: [ICloudDevice], format: OutputFormat) throws -> String {
+ switch format {
+ case .json:
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
+ encoder.dateEncodingStrategy = .iso8601
+ return String(decoding: try encoder.encode(devices), as: UTF8.self)
+ case .text:
+ return devices.map { device in
+ let current = device.isCurrentDevice ? " current" : ""
+ let os = device.osVersion.map { " (\($0))" } ?? ""
+ return "\(device.name) - \(device.model)\(os)\(current)"
+ }.joined(separator: "\n")
+ }
+ }
+
public func render(_ report: CloudTabsProbeReport, format: OutputFormat) throws -> String {
switch format {
case .json:
diff --git a/Sources/ICloudCLICore/SystemStatusReaders.swift b/Sources/ICloudCLICore/SystemStatusReaders.swift
new file mode 100644
index 0000000..f494114
--- /dev/null
+++ b/Sources/ICloudCLICore/SystemStatusReaders.swift
@@ -0,0 +1,176 @@
+import Foundation
+
+public struct ICloudStorageStatus: Codable, Equatable, Sendable {
+ public let totalBytes: Int64
+ public let usedBytes: Int64
+ public let availableBytes: Int64
+ public let accountEmail: String?
+ public let lastRefreshedAt: Date?
+}
+
+public enum ICloudStorageStatusError: Error, LocalizedError, Equatable {
+ case unavailable
+
+ public var errorDescription: String? {
+ switch self {
+ case .unavailable:
+ return "iCloud quota cache not available; ensure iCloud is signed in and has synced recently"
+ }
+ }
+}
+
+public struct ICloudStorageStatusReader: Sendable {
+ public let cacheFile: URL
+
+ public init(cacheFile: URL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Preferences/MobileMeAccounts.plist")) {
+ self.cacheFile = cacheFile
+ }
+
+ public func readStatus() throws -> ICloudStorageStatus {
+ guard let plist = NSDictionary(contentsOf: cacheFile) else { throw ICloudStorageStatusError.unavailable }
+ if let status = parseStatus(from: plist) { return status }
+ throw ICloudStorageStatusError.unavailable
+ }
+
+ private func parseStatus(from plist: NSDictionary) -> ICloudStorageStatus? {
+ let candidates = nestedDictionaries(in: plist)
+ for dictionary in candidates {
+ guard let total = int64Value(dictionary["totalBytes"] ?? dictionary["quotaTotalBytes"] ?? dictionary["totalStorageBytes"] ?? dictionary["quotaTotal"]),
+ let used = int64Value(dictionary["usedBytes"] ?? dictionary["quotaUsedBytes"] ?? dictionary["usedStorageBytes"] ?? dictionary["quotaUsed"]) else { continue }
+ let available = int64Value(dictionary["availableBytes"] ?? dictionary["quotaAvailableBytes"] ?? dictionary["availableStorageBytes"]) ?? max(0, total - used)
+ let email = stringValue(dictionary["accountEmail"] ?? dictionary["AccountID"] ?? dictionary["DSIDAccountEmail"] ?? dictionary["email"])
+ let refreshed = dateValue(dictionary["lastRefreshedAt"] ?? dictionary["LastSuccessfulSyncDate"] ?? dictionary["lastUpdated"] ?? dictionary["modifiedAt"])
+ return ICloudStorageStatus(totalBytes: total, usedBytes: used, availableBytes: available, accountEmail: email, lastRefreshedAt: refreshed)
+ }
+ return nil
+ }
+}
+
+public struct FocusStatus: Codable, Equatable, Sendable {
+ public let activeFocus: String?
+ public let startedAt: Date?
+ public let endsAt: Date?
+ public let allFocusModes: [String]
+}
+
+public enum FocusStatusError: Error, LocalizedError, Equatable {
+ case unreadable(String)
+
+ public var errorDescription: String? {
+ switch self {
+ case .unreadable(let path): return "Focus status cache is unreadable: \(path)"
+ }
+ }
+}
+
+public struct FocusStatusReader: Sendable {
+ public let focusDirectory: URL
+
+ public init(focusDirectory: URL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/DoNotDisturb")) {
+ self.focusDirectory = focusDirectory
+ }
+
+ public func readStatus() throws -> FocusStatus {
+ guard FileManager.default.fileExists(atPath: focusDirectory.path) else { throw FocusStatusError.unreadable(focusDirectory.path) }
+ guard let enumerator = FileManager.default.enumerator(at: focusDirectory, includingPropertiesForKeys: [.isRegularFileKey], options: []) else { throw FocusStatusError.unreadable(focusDirectory.path) }
+ var dictionaries: [NSDictionary] = []
+ for case let url as URL in enumerator where url.pathExtension == "plist" {
+ if let dictionary = NSDictionary(contentsOf: url) { dictionaries.append(dictionary) }
+ }
+ guard !dictionaries.isEmpty else { throw FocusStatusError.unreadable(focusDirectory.path) }
+
+ let all = dictionaries.flatMap { nestedDictionaries(in: $0) }
+ let modes = uniqueSorted(all.compactMap { stringValue($0["name"] ?? $0["displayName"] ?? $0["focusName"]) })
+ let active = all.first { boolValue($0["active"] ?? $0["isActive"] ?? $0["enabled"]) == true }
+ let activeName = active.flatMap { stringValue($0["name"] ?? $0["displayName"] ?? $0["focusName"]) }
+ return FocusStatus(activeFocus: activeName, startedAt: active.flatMap { dateValue($0["startedAt"] ?? $0["startDate"]) }, endsAt: active.flatMap { dateValue($0["endsAt"] ?? $0["endDate"]) }, allFocusModes: modes)
+ }
+}
+
+public struct ICloudDevice: Codable, Equatable, Sendable {
+ public let name: String
+ public let model: String
+ public let osVersion: String?
+ public let isCurrentDevice: Bool
+ public let lastSeenAt: Date?
+}
+
+public enum ICloudDevicesError: Error, LocalizedError, Equatable {
+ case unreadable(String)
+
+ public var errorDescription: String? {
+ switch self {
+ case .unreadable(let path): return "iCloud devices cache is unreadable: \(path)"
+ }
+ }
+}
+
+public struct ICloudDevicesReader: Sendable {
+ public let cacheFile: URL
+
+ public init(cacheFile: URL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Preferences/MobileMeAccounts.plist")) {
+ self.cacheFile = cacheFile
+ }
+
+ public func listDevices() throws -> [ICloudDevice] {
+ guard let plist = NSDictionary(contentsOf: cacheFile) else { throw ICloudDevicesError.unreadable(cacheFile.path) }
+ let devices = nestedDictionaries(in: plist).compactMap(device(from:))
+ if devices.isEmpty { throw ICloudDevicesError.unreadable(cacheFile.path) }
+ return devices.sorted { lhs, rhs in lhs.name.localizedStandardCompare(rhs.name) == .orderedAscending }
+ }
+
+ private func device(from dictionary: NSDictionary) -> ICloudDevice? {
+ guard let name = stringValue(dictionary["name"] ?? dictionary["deviceName"] ?? dictionary["DeviceName"]),
+ let model = stringValue(dictionary["model"] ?? dictionary["modelDisplayName"] ?? dictionary["deviceModel"] ?? dictionary["ProductType"]) else { return nil }
+ let os = stringValue(dictionary["osVersion"] ?? dictionary["systemVersion"] ?? dictionary["OSVersion"])
+ let current = boolValue(dictionary["isCurrentDevice"] ?? dictionary["currentDevice"] ?? dictionary["IsThisDevice"]) ?? false
+ let lastSeen = dateValue(dictionary["lastSeenAt"] ?? dictionary["lastSeen"] ?? dictionary["LastSeenDate"])
+ return ICloudDevice(name: name, model: model, osVersion: os, isCurrentDevice: current, lastSeenAt: lastSeen)
+ }
+}
+
+private func nestedDictionaries(in root: Any) -> [NSDictionary] {
+ var result: [NSDictionary] = []
+ func walk(_ value: Any) {
+ if let dictionary = value as? NSDictionary {
+ result.append(dictionary)
+ for child in dictionary.allValues { walk(child) }
+ } else if let array = value as? [Any] {
+ for child in array { walk(child) }
+ }
+ }
+ walk(root)
+ return result
+}
+
+private func int64Value(_ value: Any?) -> Int64? {
+ if let number = value as? NSNumber { return number.int64Value }
+ if let string = value as? String { return Int64(string) }
+ return nil
+}
+
+private func stringValue(_ value: Any?) -> String? {
+ if let string = value as? String, !string.isEmpty { return string }
+ return nil
+}
+
+private func boolValue(_ value: Any?) -> Bool? {
+ if let bool = value as? Bool { return bool }
+ if let number = value as? NSNumber { return number.boolValue }
+ if let string = value as? String { return ["true", "yes", "1"].contains(string.lowercased()) }
+ return nil
+}
+
+private func dateValue(_ value: Any?) -> Date? {
+ if let date = value as? Date { return date }
+ if let number = value as? NSNumber { return Date(timeIntervalSince1970: number.doubleValue) }
+ if let string = value as? String {
+ if let date = ISO8601DateFormatter().date(from: string) { return date }
+ if let timestamp = Double(string) { return Date(timeIntervalSince1970: timestamp) }
+ }
+ return nil
+}
+
+private func uniqueSorted(_ values: [String]) -> [String] {
+ Array(Set(values)).sorted { $0.localizedStandardCompare($1) == .orderedAscending }
+}
diff --git a/Tests/Fixtures/SystemStatus/Focus/Status.plist b/Tests/Fixtures/SystemStatus/Focus/Status.plist
new file mode 100644
index 0000000..57303d6
--- /dev/null
+++ b/Tests/Fixtures/SystemStatus/Focus/Status.plist
@@ -0,0 +1,19 @@
+
+
+
+
+ modes
+
+
+ nameWork
+ active
+ startedAt2026-05-14T08:00:00Z
+ endsAt2026-05-14T18:00:00Z
+
+
+ nameSleep
+ active
+
+
+
+
diff --git a/Tests/Fixtures/SystemStatus/MobileMeAccounts.plist b/Tests/Fixtures/SystemStatus/MobileMeAccounts.plist
new file mode 100644
index 0000000..efb7955
--- /dev/null
+++ b/Tests/Fixtures/SystemStatus/MobileMeAccounts.plist
@@ -0,0 +1,34 @@
+
+
+
+
+ Accounts
+
+
+ AccountIDoperator@example.com
+ QuotaInfo
+
+ accountEmailoperator@example.com
+ totalBytes200000000000
+ usedBytes75000000000
+ lastRefreshedAt2026-05-14T10:00:00Z
+
+ Devices
+
+
+ nameExample Mac
+ modelMacBook Pro
+ osVersion15.5
+ isCurrentDevice
+ lastSeenAt2026-05-14T09:30:00Z
+
+
+ deviceNameExample Phone
+ deviceModeliPhone
+ systemVersion18.4
+
+
+
+
+
+
diff --git a/Tests/ICloudCLICoreTests/CLIParserTests.swift b/Tests/ICloudCLICoreTests/CLIParserTests.swift
index 3b7a5a1..bd56c0e 100644
--- a/Tests/ICloudCLICoreTests/CLIParserTests.swift
+++ b/Tests/ICloudCLICoreTests/CLIParserTests.swift
@@ -145,3 +145,34 @@ import Testing
#expect(options.format == .text)
#expect(options.shortcutsDirectory.path == "/tmp/shortcuts")
}
+
+
+@Test func parsesStorageStatusCommand() throws {
+ let command = try CLIParser().parse(arguments: ["icloud-cli", "storage", "status", "--format", "text", "--cache-file", "/tmp/mobileme.plist"])
+ guard case .storageStatus(let options) = command else {
+ Issue.record("Expected storage status command")
+ return
+ }
+ #expect(options.format == .text)
+ #expect(options.cacheFile.path == "/tmp/mobileme.plist")
+}
+
+@Test func parsesFocusStatusCommand() throws {
+ let command = try CLIParser().parse(arguments: ["icloud-cli", "focus", "status", "--format", "json", "--focus-dir", "/tmp/focus"])
+ guard case .focusStatus(let options) = command else {
+ Issue.record("Expected focus status command")
+ return
+ }
+ #expect(options.format == .json)
+ #expect(options.focusDirectory.path == "/tmp/focus")
+}
+
+@Test func parsesDevicesListCommand() throws {
+ let command = try CLIParser().parse(arguments: ["icloud-cli", "devices", "list", "--format", "text", "--cache-file", "/tmp/mobileme.plist"])
+ guard case .devicesList(let options) = command else {
+ Issue.record("Expected devices list command")
+ return
+ }
+ #expect(options.format == .text)
+ #expect(options.cacheFile.path == "/tmp/mobileme.plist")
+}
diff --git a/Tests/ICloudCLICoreTests/SystemStatusReadersTests.swift b/Tests/ICloudCLICoreTests/SystemStatusReadersTests.swift
new file mode 100644
index 0000000..d719c05
--- /dev/null
+++ b/Tests/ICloudCLICoreTests/SystemStatusReadersTests.swift
@@ -0,0 +1,42 @@
+import Foundation
+import Testing
+@testable import ICloudCLICore
+
+private let fixtures = URL(fileURLWithPath: #filePath)
+ .deletingLastPathComponent()
+ .deletingLastPathComponent()
+ .appendingPathComponent("Fixtures/SystemStatus")
+
+@Test func readsStorageQuotaStatusFixture() throws {
+ let status = try ICloudStorageStatusReader(cacheFile: fixtures.appendingPathComponent("MobileMeAccounts.plist")).readStatus()
+
+ #expect(status.totalBytes == 200_000_000_000)
+ #expect(status.usedBytes == 75_000_000_000)
+ #expect(status.availableBytes == 125_000_000_000)
+ #expect(status.accountEmail == "operator@example.com")
+}
+
+@Test func readsFocusStatusFixture() throws {
+ let status = try FocusStatusReader(focusDirectory: fixtures.appendingPathComponent("Focus")).readStatus()
+
+ #expect(status.activeFocus == "Work")
+ #expect(status.allFocusModes == ["Sleep", "Work"])
+ #expect(status.endsAt != nil)
+}
+
+@Test func readsDevicesFixture() throws {
+ let devices = try ICloudDevicesReader(cacheFile: fixtures.appendingPathComponent("MobileMeAccounts.plist")).listDevices()
+
+ #expect(devices.count == 2)
+ #expect(devices.first?.name == "Example Mac")
+ #expect(devices.first?.isCurrentDevice == true)
+}
+
+@Test func rendersStatusCommandsAsText() throws {
+ let runner = CommandRunner()
+ let storage = ICloudStorageStatus(totalBytes: 200_000_000_000, usedBytes: 75_000_000_000, availableBytes: 125_000_000_000, accountEmail: "operator@example.com", lastRefreshedAt: nil)
+ #expect(try runner.render(storage, format: .text).contains("75.0 GB used"))
+
+ let focus = FocusStatus(activeFocus: nil, startedAt: nil, endsAt: nil, allFocusModes: [])
+ #expect(try runner.render(focus, format: .text) == "Focus: none")
+}
diff --git a/docs/privacy.md b/docs/privacy.md
index 292a8e0..86d9a0a 100644
--- a/docs/privacy.md
+++ b/docs/privacy.md
@@ -37,6 +37,9 @@ OpenClaw integrations should default to local retention. Exporting raw browsing
| `icloud-cli safari frequently-visited` | `~/Library/Safari/TopSites.plist` or compatible frequently visited site cache | Terminal or calling process may need Full Disk Access to read Safari metadata files. |
| Future `icloud-cli safari tabs --include-cloud` | Safari iCloud sync storage, likely under `~/Library/Safari` | Full Disk Access is expected; schema and safety constraints must be documented before implementation. |
| `icloud-cli shortcuts list` | `~/Library/Shortcuts/*.shortcut` metadata including shortcut names, action counts, dates, and input capability | Terminal or calling process may need access to the Shortcuts library. The command is read-only and never executes shortcuts. |
+| `icloud-cli storage status` | Locally cached iCloud quota metadata, currently `~/Library/Preferences/MobileMeAccounts.plist` when available | Normal user file access. The command is read-only and makes no live network requests. Account email is direct operator output only; logs should redact to `user@…`. |
+| `icloud-cli focus status` | Local Focus / Do Not Disturb preference plists under `~/Library/DoNotDisturb` | Normal user file access. The command is read-only and does not modify Focus state. |
+| `icloud-cli devices list` | Locally cached iCloud registered-device metadata, currently `~/Library/Preferences/MobileMeAccounts.plist` when available | Normal user file access. Device names may be personally identifying; logs should report only count/model summary. |
| Future iCloud settings commands | Local Apple account or system settings state | Document per-command read surfaces before implementation; do not require Automation unless a command actually controls an app. |
Automation permission is not required for the current Safari tab reader because it reads local files. Any future command that controls Safari, System Settings, or another app must document the Automation prompt and failure mode before merge.
@@ -65,3 +68,8 @@ This keeps static privacy checks and Swift tests ahead of the standalone build.
## Shortcuts inventory
`icloud-cli shortcuts list` reads local Shortcuts metadata only. It does not execute shortcuts, request Automation permission, or read action payload contents beyond counting action entries in the shortcut plist. Shortcut names are operator-sensitive metadata; logs and status summaries should avoid dumping full JSON unless explicitly requested.
+
+
+## Local iCloud status surfaces
+
+`icloud-cli storage status`, `icloud-cli focus status`, and `icloud-cli devices list` read cached local metadata only. They do not contact iCloud, mutate system settings, or require Automation permission. Storage and devices currently use the local MobileMe/iCloud account preferences cache when present; Focus reads the local Do Not Disturb preference directory. Direct command output may include the real account email and device names because the operator requested them. OpenClaw logs and PR summaries should redact account emails and avoid listing device names unless explicitly requested.