From c452f8c4307a2d9946e67551a91f9f671b3db0bd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 17 Dec 2025 20:03:30 +0000 Subject: [PATCH] clawdis-mac: enrich node list output --- .../Clawdis/ControlRequestHandler.swift | 67 +++++++++++++++++-- .../macos/Sources/ClawdisCLI/ClawdisCLI.swift | 41 +++++++++++- docs/clawdis-mac.md | 3 + 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/apps/macos/Sources/Clawdis/ControlRequestHandler.swift b/apps/macos/Sources/Clawdis/ControlRequestHandler.swift index e6e9e4140..e9606651e 100644 --- a/apps/macos/Sources/Clawdis/ControlRequestHandler.swift +++ b/apps/macos/Sources/Clawdis/ControlRequestHandler.swift @@ -5,6 +5,23 @@ import OSLog enum ControlRequestHandler { private static let cameraCapture = CameraCaptureService() + struct NodeListNode: Codable { + var nodeId: String + var displayName: String? + var platform: String? + var version: String? + var remoteAddress: String? + var connected: Bool + var capabilities: [String]? + } + + struct NodeListResult: Codable { + var ts: Int + var connectedNodeIds: [String] + var pairedNodeIds: [String] + var nodes: [NodeListNode] + } + static func process( request: Request, notifier: NotificationManager = NotificationManager(), @@ -394,14 +411,56 @@ enum ControlRequestHandler { } private static func handleNodeList() async -> Response { - let ids = await BridgeServer.shared.connectedNodeIds() - let payload = (try? JSONSerialization.data( - withJSONObject: ["connectedNodeIds": ids], - options: [.prettyPrinted])) + let paired = await BridgeServer.shared.pairedNodes() + let connected = await BridgeServer.shared.connectedNodes() + let result = self.buildNodeListResult(paired: paired, connected: connected) + + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + let payload = (try? encoder.encode(result)) .flatMap { String(data: $0, encoding: .utf8) } ?? "{}" return Response(ok: true, payload: Data(payload.utf8)) } + static func buildNodeListResult(paired: [PairedNode], connected: [BridgeNodeInfo]) -> NodeListResult { + let connectedById = Dictionary(uniqueKeysWithValues: connected.map { ($0.nodeId, $0) }) + + var nodesById: [String: NodeListNode] = [:] + + for p in paired { + let live = connectedById[p.nodeId] + nodesById[p.nodeId] = NodeListNode( + nodeId: p.nodeId, + displayName: (live?.displayName ?? p.displayName), + platform: (live?.platform ?? p.platform), + version: (live?.version ?? p.version), + remoteAddress: live?.remoteAddress, + connected: live != nil, + capabilities: live?.caps) + } + + for c in connected where nodesById[c.nodeId] == nil { + nodesById[c.nodeId] = NodeListNode( + nodeId: c.nodeId, + displayName: c.displayName, + platform: c.platform, + version: c.version, + remoteAddress: c.remoteAddress, + connected: true, + capabilities: c.caps) + } + + let nodes = nodesById.values.sorted { a, b in + (a.displayName ?? a.nodeId) < (b.displayName ?? b.nodeId) + } + + return NodeListResult( + ts: Int(Date().timeIntervalSince1970 * 1000), + connectedNodeIds: connected.map(\.nodeId).sorted(), + pairedNodeIds: paired.map(\.nodeId).sorted(), + nodes: nodes) + } + private static func handleNodeInvoke( nodeId: String, command: String, diff --git a/apps/macos/Sources/ClawdisCLI/ClawdisCLI.swift b/apps/macos/Sources/ClawdisCLI/ClawdisCLI.swift index 8acbbfcff..437621dad 100644 --- a/apps/macos/Sources/ClawdisCLI/ClawdisCLI.swift +++ b/apps/macos/Sources/ClawdisCLI/ClawdisCLI.swift @@ -433,6 +433,45 @@ struct ClawdisCLI { return } + if case .nodeList = parsed.request, let payload = response.payload { + struct NodeListResult: Decodable { + struct Node: Decodable { + var nodeId: String + var displayName: String? + var remoteAddress: String? + var connected: Bool + var capabilities: [String]? + } + + var pairedNodeIds: [String]? + var connectedNodeIds: [String]? + var nodes: [Node] + } + + if let decoded = try? JSONDecoder().decode(NodeListResult.self, from: payload) { + let pairedCount = decoded.pairedNodeIds?.count ?? decoded.nodes.count + let connectedCount = decoded.connectedNodeIds?.count ?? decoded.nodes.filter(\.connected).count + print("Paired: \(pairedCount) · Connected: \(connectedCount)") + + for n in decoded.nodes { + let nameTrimmed = n.displayName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let name = nameTrimmed.isEmpty ? n.nodeId : nameTrimmed + + let ipTrimmed = n.remoteAddress?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let ip = ipTrimmed.isEmpty ? nil : ipTrimmed + let caps = n.capabilities?.sorted().joined(separator: ",") + let capsText = caps.map { "[\($0)]" } ?? "?" + + var parts: [String] = ["- \(name)", n.nodeId] + if let ip { parts.append(ip) } + parts.append(n.connected ? "connected" : "disconnected") + parts.append("caps: \(capsText)") + print(parts.joined(separator: " · ")) + } + return + } + } + switch parsed.kind { case .generic: if let payload = response.payload, let text = String(data: payload, encoding: .utf8), !text.isEmpty { @@ -509,7 +548,7 @@ struct ClawdisCLI { [--session ] [--deliver] [--to ] Nodes: - clawdis-mac node list + clawdis-mac node list # paired + connected nodes (+ capabilities when available) clawdis-mac node invoke --node --command [--params-json ] Canvas: diff --git a/docs/clawdis-mac.md b/docs/clawdis-mac.md index fa866a1c6..8e8b5e1a5 100644 --- a/docs/clawdis-mac.md +++ b/docs/clawdis-mac.md @@ -74,6 +74,9 @@ UI automation is not part of `ClawdisIPC.Request`: - UI automation + capture: use `peekaboo …` (Clawdis hosts PeekabooBridge; see `docs/mac/peekaboo.md`) - `run -- cmd args... [--cwd] [--env KEY=VAL] [--timeout 30] [--needs-screen-recording]` - `status` + - Nodes (bridge-connected companions): + - `node list` — lists paired + currently connected nodes, including advertised capabilities (e.g. `canvas`, `camera`). + - `node invoke --node --command [--params-json ]` - Sounds: supply any macOS alert name with `--sound` per notification; omit the flag to use the system default. There is no longer a persisted “default sound” in the app UI. - Priority: `timeSensitive` is best-effort and falls back to `active` unless the app is signed with the Time Sensitive Notifications entitlement. - Delivery: `overlay` and `auto` show an in-app toast panel (bypasses Notification Center/Focus).