From 32720bd3726d833d1e57c29c2e3fcf1161c98f9a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Dec 2025 05:20:50 +0100 Subject: [PATCH] feat(agent): add rpc status command and tests; rpc only path --- apps/macos/Sources/Clawdis/AgentRPC.swift | 20 +++++++++++++++++++ apps/macos/Sources/Clawdis/XPCService.swift | 4 ++++ apps/macos/Sources/ClawdisCLI/main.swift | 4 ++++ apps/macos/Sources/ClawdisIPC/IPC.swift | 9 +++++++++ .../Tests/ClawdisIPCTests/AgentRPCTests.swift | 17 ++++++++++++++++ 5 files changed, 54 insertions(+) create mode 100644 apps/macos/Tests/ClawdisIPCTests/AgentRPCTests.swift diff --git a/apps/macos/Sources/Clawdis/AgentRPC.swift b/apps/macos/Sources/Clawdis/AgentRPC.swift index b790f26b8..0460b1b0a 100644 --- a/apps/macos/Sources/Clawdis/AgentRPC.swift +++ b/apps/macos/Sources/Clawdis/AgentRPC.swift @@ -53,6 +53,26 @@ actor AgentRPC { } } + func status() async -> (ok: Bool, error: String?) { + do { + try await ensureRunning() + let payload: [String: Any] = ["type": "status"] + let data = try JSONSerialization.data(withJSONObject: payload) + guard let stdinHandle else { throw RpcError(message: "stdin missing") } + stdinHandle.write(data) + stdinHandle.write(Data([0x0A])) + + let line = try await nextLine() + let parsed = try JSONSerialization.jsonObject(with: Data(line.utf8)) as? [String: Any] + if let ok = parsed?["ok"] as? Bool, ok { return (true, nil) } + return (false, parsed?["error"] as? String ?? "rpc status failed") + } catch { + logger.error("rpc status failed: \(error.localizedDescription, privacy: .public)") + await stop() + return (false, error.localizedDescription) + } + } + // MARK: - Process lifecycle private func ensureRunning() async throws { diff --git a/apps/macos/Sources/Clawdis/XPCService.swift b/apps/macos/Sources/Clawdis/XPCService.swift index e02638bae..f1839f5d6 100644 --- a/apps/macos/Sources/Clawdis/XPCService.swift +++ b/apps/macos/Sources/Clawdis/XPCService.swift @@ -51,6 +51,10 @@ final class ClawdisXPCService: NSObject, ClawdisXPCProtocol { case .status: return Response(ok: true, message: "ready") + case .rpcStatus: + let result = await AgentRPC.shared.status() + return Response(ok: result.ok, message: result.error) + case let .screenshot(displayID, windowID, _): let authorized = await PermissionManager .ensure([.screenRecording], interactive: false)[.screenRecording] ?? false diff --git a/apps/macos/Sources/ClawdisCLI/main.swift b/apps/macos/Sources/ClawdisCLI/main.swift index b78e4f21c..93cca326e 100644 --- a/apps/macos/Sources/ClawdisCLI/main.swift +++ b/apps/macos/Sources/ClawdisCLI/main.swift @@ -133,6 +133,9 @@ struct ClawdisCLI { case "status": return .status + case "rpc-status": + return .rpcStatus + case "agent": var message: String? var thinking: String? @@ -174,6 +177,7 @@ struct ClawdisCLI { clawdis-mac screenshot [--display-id ] [--window-id ] clawdis-mac run [--cwd ] [--env KEY=VAL] [--timeout ] [--needs-screen-recording] clawdis-mac status + clawdis-mac rpc-status clawdis-mac agent --message [--thinking ] [--session ] clawdis-mac --help diff --git a/apps/macos/Sources/ClawdisIPC/IPC.swift b/apps/macos/Sources/ClawdisIPC/IPC.swift index d7a61d424..03531485e 100644 --- a/apps/macos/Sources/ClawdisIPC/IPC.swift +++ b/apps/macos/Sources/ClawdisIPC/IPC.swift @@ -26,6 +26,7 @@ public enum Request: Sendable { needsScreenRecording: Bool) case status case agent(message: String, thinking: String?, session: String?) + case rpcStatus } // MARK: - Responses @@ -53,6 +54,7 @@ extension Request: Codable { case displayID, windowID, format case command, cwd, env, timeoutSec, needsScreenRecording case message, thinking, session + case rpcStatus } private enum Kind: String, Codable { @@ -62,6 +64,7 @@ extension Request: Codable { case runShell case status case agent + case rpcStatus } public func encode(to encoder: Encoder) throws { @@ -100,6 +103,9 @@ extension Request: Codable { try container.encode(message, forKey: .message) try container.encodeIfPresent(thinking, forKey: .thinking) try container.encodeIfPresent(session, forKey: .session) + + case .rpcStatus: + try container.encode(Kind.rpcStatus, forKey: .type) } } @@ -140,6 +146,9 @@ extension Request: Codable { let thinking = try container.decodeIfPresent(String.self, forKey: .thinking) let session = try container.decodeIfPresent(String.self, forKey: .session) self = .agent(message: message, thinking: thinking, session: session) + + case .rpcStatus: + self = .rpcStatus } } } diff --git a/apps/macos/Tests/ClawdisIPCTests/AgentRPCTests.swift b/apps/macos/Tests/ClawdisIPCTests/AgentRPCTests.swift new file mode 100644 index 000000000..0f155c7b4 --- /dev/null +++ b/apps/macos/Tests/ClawdisIPCTests/AgentRPCTests.swift @@ -0,0 +1,17 @@ +import Clawdis +import Testing +@testable import ClawdisIPC + +@Suite(.serialized) struct AgentRPCTests { + @Test func statusFailsWhenProcessMissing() async { + let result = await AgentRPC.shared.status() + // We don't assert ok because the worker may not be available in CI. + // Instead, ensure the call returns without throwing and provides a message. + #expect(result.ok == true || result.error != nil) + } + + @Test func rejectEmptyMessage() async { + let result = await AgentRPC.shared.send(text: "", thinking: nil, session: "main") + #expect(result.ok == false) + } +}